Add capability-scoped module API (Workstream D)
ModuleManifest gains a structured [[permissions]] field (bread-shared's
new ModulePermission/PermissionKind, shared between bread-cli and breadd
so the two never drift on what a permission "type" string means).
breadd now gives every third-party module's Lua chunk a scoped _ENV
instead of the shared global table: load_scoped_lua_file builds a fresh
`bread` table containing only baseline bindings (event subscription,
timers, json, module/store, logging, and the pure-Lua sugar built on top
of those) plus whatever the manifest's permissions grant, with a
metatable __index falling back to the real globals for everything else
(stdlib, require/package - so require("bread.devices") keeps working,
since builtins load unscoped and their closures capture that environment
lexically regardless of the caller's). _G is explicitly rebound to the
scoped table itself to close the obvious escape hatch. A module with no
manifest, or a manifest with no permissions key, keeps full ambient
access unchanged (today's behavior) but is now tracked as `ungated` in
module status and surfaced by `bread doctor`. An explicit `permissions =
[]` is scoped for real but not flagged, since that's a deliberate
declaration.
Adds `bread modules audit <name>`: a best-effort text scan of a module's
.lua files suggesting a [[permissions]] block to paste into its manifest.
Converts examples/modules/cpu-temp-widget.lua into a directory module
with a worked bread.module.toml (fs.read + widget) as the reference
example. Documentation.md gets a new "Capability-scoped modules" section
covering the taxonomy, the require()/closure mechanism, and an explicit
note that path/bin scoping is recorded but not yet enforced per-call -
that's the out-of-process module sandboxing workstream this manifest
schema is laid down for. API_VERSION bumped 1.4.0 -> 1.5.0.
This commit is contained in:
parent
96639516b1
commit
6841163620
13 changed files with 1329 additions and 14 deletions
|
|
@ -125,6 +125,9 @@ enum ModulesCommand {
|
|||
List,
|
||||
/// Show full manifest details for a module
|
||||
Info { name: String },
|
||||
/// Statically scan an installed module's Lua source and suggest a
|
||||
/// `[[permissions]]` block for its `bread.module.toml` manifest
|
||||
Audit { name: String },
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
|
|
@ -303,6 +306,55 @@ async fn handle_modules_cmd(cmd: ModulesCommand, socket: &Path) -> Result<()> {
|
|||
println!("source: {}", m.source);
|
||||
println!("installed_at: {}", m.installed_at);
|
||||
println!("status: {}", status);
|
||||
match &m.permissions {
|
||||
None => println!(
|
||||
"permissions: (none declared — full, ungated bread.* access; see 'bread doctor')"
|
||||
),
|
||||
Some(perms) if perms.is_empty() => {
|
||||
println!("permissions: (declared empty — baseline access only)")
|
||||
}
|
||||
Some(perms) => {
|
||||
println!("permissions:");
|
||||
for p in perms {
|
||||
let mut line = format!(" - {:?}", p.kind);
|
||||
if let Some(path) = &p.path {
|
||||
line.push_str(&format!(" path={path}"));
|
||||
}
|
||||
if let Some(bin) = &p.bin {
|
||||
line.push_str(&format!(" bin={bin}"));
|
||||
}
|
||||
println!("{line}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ModulesCommand::Audit { name } => {
|
||||
let module_dir = mods_dir.join(&name);
|
||||
if !module_dir.exists() {
|
||||
eprintln!("bread: module '{}' is not installed", name);
|
||||
std::process::exit(1);
|
||||
}
|
||||
let suggested = modules_mgmt::audit_module(&module_dir)?;
|
||||
if suggested.is_empty() {
|
||||
println!(
|
||||
"bread: no capability-gated bread.* calls found in '{}' — \
|
||||
it appears to only use baseline APIs (events, timers, json, \
|
||||
logging). Declaring `permissions = []` in bread.module.toml \
|
||||
documents that intentionally and avoids the 'no permissions \
|
||||
declared' warning from `bread doctor`.",
|
||||
name
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
println!(
|
||||
"bread: suggested permissions for '{}' (best-effort static scan — \
|
||||
review before pasting into bread.module.toml; false positives \
|
||||
are possible, missing an actually-needed permission should be rare \
|
||||
for direct bread.exec()-style call sites):\n",
|
||||
name
|
||||
);
|
||||
print!("{}", modules_mgmt::render_permissions_toml(&suggested)?);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
|
@ -602,14 +654,35 @@ fn render_doctor(health: &Value) {
|
|||
if let Some(modules) = health.get("modules").and_then(Value::as_array) {
|
||||
println!();
|
||||
println!("modules");
|
||||
let mut ungated_count = 0;
|
||||
for module in modules {
|
||||
let name = module.get("name").and_then(Value::as_str).unwrap_or("?");
|
||||
let status = module.get("status").and_then(Value::as_str).unwrap_or("?");
|
||||
let error = module.get("last_error").and_then(Value::as_str);
|
||||
let ungated = module
|
||||
.get("ungated")
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or(false);
|
||||
println!(" {:30} {}", name, status);
|
||||
if let Some(error) = error {
|
||||
println!(" └ {error}");
|
||||
}
|
||||
if ungated {
|
||||
ungated_count += 1;
|
||||
println!(
|
||||
" └ ⚠ running with full, ungated access — no permissions \
|
||||
manifest declared (add `[[permissions]]` to its \
|
||||
bread.module.toml, or run `bread modules audit {name}` \
|
||||
for a suggested block)"
|
||||
);
|
||||
}
|
||||
}
|
||||
if ungated_count > 0 {
|
||||
println!();
|
||||
println!(
|
||||
" {ungated_count} module(s) running with full, ungated bread.* access — \
|
||||
see above"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue