feat: v1.0 — profils sandbox par agent : commandes autorisées (allowlist, stem Windows), périmètre de répertoires, politique réseau best-effort, refus journalisés pour l'audit, --no-sandbox pour contourner (closes #79)

- src/sandbox.rs : profile_of + enforce (basename/stem allowlist, cwd starts_with périmètre, proxy env si network:false), EventKind::Sandbox
- enforcement au run et au start (start_one, --parallel, restart) ; AgentDef.sandbox + doc config.yaml
- 6 tests : défaut non sandboxé, refus + journalisation, stem .exe, périmètre cwd, env réseau, bypass --no-sandbox
This commit is contained in:
2026-08-19 22:57:45 -04:00
parent 900f70c249
commit 92af59a71f
17 changed files with 326 additions and 19 deletions
+2 -1
View File
@@ -229,13 +229,14 @@ pub fn execute_command(app: &App, cmd: &Command) -> Result<i32> {
Command::Doctor { fix, watch } => {
doctor_cmd::run(app, *fix, *watch)
}
Command::Run { agent, model, provider, container, args } => {
Command::Run { agent, model, provider, container, no_sandbox, args } => {
run_cmd::run(
app,
agent,
model.as_deref(),
provider.as_deref(),
*container,
*no_sandbox,
args,
)
}
+42 -6
View File
@@ -105,7 +105,7 @@ pub fn start_with_cwd(app: &App, opts: &StartArgs, cwd: Option<&std::path::Path>
// waiting (the OS spawns them concurrently); the default waits each
// health check before the next member.
for m in members {
start_one(app, m, &extra_args, &extra_env, opts.notify, true, cwd)?;
start_one(app, m, &extra_args, &extra_env, opts.notify, true, opts.no_sandbox, cwd)?;
if !opts.parallel {
// Issue #57: block until healthy before the next member.
crate::automation::wait_health(&m.name, m.healthcheck.as_ref(), &app.log)?;
@@ -114,7 +114,16 @@ pub fn start_with_cwd(app: &App, opts: &StartArgs, cwd: Option<&std::path::Path>
return Ok(0);
}
let agent = require_agent(app, &target)?;
start_one(app, agent, &extra_args, &extra_env, opts.notify, opts.background, cwd)
start_one(
app,
agent,
&extra_args,
&extra_env,
opts.notify,
opts.background,
opts.no_sandbox,
cwd,
)
}
fn start_one(
@@ -124,6 +133,7 @@ fn start_one(
extra_env: &BTreeMap<String, String>,
notify: bool,
background: bool,
no_sandbox: bool,
cwd: Option<&std::path::Path>,
) -> Result<i32> {
let exec = resolve_exec(app, agent, extra_args, extra_env)?;
@@ -133,7 +143,10 @@ fn start_one(
let current = std::env::current_dir().unwrap_or_default();
crate::hooks::run_hooks(app, "on_start", cwd.unwrap_or(&current));
if background {
let pid = process::spawn_background(app, &agent.name, &bin, &exec.args, &exec.env, cwd)?;
// Issue #79: sandbox enforcement before spawning.
let mut env = exec.env.clone();
crate::sandbox::enforce(app, agent, &exec.program, &mut env, no_sandbox)?;
let pid = process::spawn_background(app, &agent.name, &bin, &exec.args, &env, cwd)?;
if app.dry_run() {
return Ok(0);
}
@@ -323,13 +336,31 @@ pub fn restart(app: &App, opts: &StartArgs, force: bool, timeout: Option<u64>) -
stop_one(app, m, force, timeout)?;
}
for m in members {
start_one(app, m, &extra_args, &extra_env, opts.notify, true, None)?;
start_one(
app,
m,
&extra_args,
&extra_env,
opts.notify,
true,
opts.no_sandbox,
None,
)?;
}
return Ok(0);
}
let agent = require_agent(app, &target)?;
stop_one(app, agent, force, timeout)?;
start_one(app, agent, &extra_args, &extra_env, opts.notify, opts.background, None)
start_one(
app,
agent,
&extra_args,
&extra_env,
opts.notify,
opts.background,
opts.no_sandbox,
None,
)
}
/// run: execute the agent command directly, no process management.
@@ -339,6 +370,7 @@ pub fn run(
model: Option<&str>,
provider: Option<&str>,
container: bool,
no_sandbox: bool,
extra: &[OsString],
) -> Result<i32> {
let agent = require_agent(app, target)?;
@@ -374,7 +406,7 @@ pub fn run(
},
}
}
let exec = resolve_exec(app, agent, &extra_args, &extra_env)?;
let mut exec = resolve_exec(app, agent, &extra_args, &extra_env)?;
if app.dry_run() {
app.log.dry(format!(
"would run {} {}",
@@ -387,6 +419,9 @@ pub fn run(
let (prog, prefix) = crate::runner::resolve_program(&exec.program);
let mut full_args = prefix;
full_args.extend(exec.args.iter().cloned());
// Issue #79: sandbox enforcement (command allowlist, cwd perimeter,
// network policy) — refused attempts are journalized for audit.
crate::sandbox::enforce(app, agent, &prog, &mut exec.env, no_sandbox)?;
let status = Command::new(&prog)
.args(&full_args)
.envs(&exec.env)
@@ -633,6 +668,7 @@ mod tests {
files: vec![],
provider_default: None,
}),
sandbox: None,
tags: vec![],
installable: false,
note: None,
+1
View File
@@ -199,6 +199,7 @@ mod tests {
provider: None,
model: None,
config: None,
sandbox: None,
tags: tags.iter().map(|s| s.to_string()).collect(),
installable: false,
note: None,