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:
+2
-1
@@ -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
@@ -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(¤t));
|
||||
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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user