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
+1 -1
View File
@@ -477,7 +477,7 @@ alerte).
| [#76](https://git.dracodev.net/Projets/agent-manager/issues/76) | ✅ Télémétrie anonyme opt-in (compteurs agrégés uniquement) | M |
| [#77](https://git.dracodev.net/Projets/agent-manager/issues/77) | ✅ Registre communautaire — am registry (publication + recherche sur Gitea) | L |
| [#78](https://git.dracodev.net/Projets/agent-manager/issues/78) | ✅ am ask — langage naturel → commande am (fournisseur LLM optionnel) | L |
| [#79](https://git.dracodev.net/Projets/agent-manager/issues/79) | Profils sandbox par agent (commandes/répertoires autorisés) | L |
| [#79](https://git.dracodev.net/Projets/agent-manager/issues/79) | ✅ Profils sandbox par agent (commandes/répertoires autorisés) | L |
| [#80](https://git.dracodev.net/Projets/agent-manager/issues/80) | am serve --token — API HTTP + WebSocket pour piloter à distance | XL |
Critère de sortie du jalon : dashboard web complet + API distante.
+9
View File
@@ -64,6 +64,15 @@ settings:
# sources:
# - https://git.dracodev.net/bruno/am-catalog/raw/branch/main/am-catalog.yaml
# author: bruno
# Profil sandbox par agent (#79) : commandes autorisées (binaires), répertoires
# de travail et politique réseau. Désactivé par défaut (mode non sandboxé).
# Les tentatives refusées sont journalisées (events sandbox) pour l'audit.
# Exemple dans un agent :
# sandbox:
# enabled: true
# commands: [python, git]
# dirs: ["~/workspace"]
# network: false
# Registre des providers LLM (#88) : nom -> base_url, modèles, modèle par
# défaut. Le provider par défaut est utilisé à l'install/au run quand aucun
# n'est donné (issue #90). Les tokens ne vont JAMAIS ici — ils vivent dans
+5 -2
View File
@@ -4,7 +4,7 @@
.SH NAME
restart \- Restart an agent: stop, then start with the same options
.SH SYNOPSIS
\fBrestart\fR [\fB\-b\fR|\fB\-\-background\fR] [\fB\-f\fR|\fB\-\-foreground\fR] [\fB\-\-args\fR] [\fB\-\-env\fR] [\fB\-\-notify\fR] [\fB\-\-profile\fR] [\fB\-\-model\fR] [\fB\-\-provider\fR] [\fB\-\-parallel\fR] [\fB\-\-container\fR] [\fB\-\-force\fR] [\fB\-\-timeout\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fIAGENT\fR]
\fBrestart\fR [\fB\-b\fR|\fB\-\-background\fR] [\fB\-f\fR|\fB\-\-foreground\fR] [\fB\-\-args\fR] [\fB\-\-env\fR] [\fB\-\-notify\fR] [\fB\-\-profile\fR] [\fB\-\-model\fR] [\fB\-\-provider\fR] [\fB\-\-no\-sandbox\fR] [\fB\-\-parallel\fR] [\fB\-\-container\fR] [\fB\-\-force\fR] [\fB\-\-timeout\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fIAGENT\fR]
.SH DESCRIPTION
Restart an agent: stop, then start with the same options
.SH OPTIONS
@@ -31,7 +31,10 @@ Apply an environment profile (env + args, defined in the config)
Local model to use for this run (validated against the local runtimes)
.TP
\fB\-\-provider\fR \fI<PROVIDER>\fR
Provider to use for this run (issue #92): registry lookup, base_url + keyring token + model
Provider to use for this run (issue #92): resolved from the registry
.TP
\fB\-\-no\-sandbox\fR
Skip the agent\*(Aqs sandbox profile for this run (issue #79)
.TP
\fB\-\-parallel\fR
Start every member of a group simultaneously (issue #57)
+4 -1
View File
@@ -4,7 +4,7 @@
.SH NAME
run \- Run the agent command directly with the given arguments (no process management)
.SH SYNOPSIS
\fBrun\fR [\fB\-\-model\fR] [\fB\-\-provider\fR] [\fB\-\-container\fR] [\fB\-h\fR|\fB\-\-help\fR] <\fIAGENT\fR> [\fIARGS...\fR]
\fBrun\fR [\fB\-\-model\fR] [\fB\-\-provider\fR] [\fB\-\-no\-sandbox\fR] [\fB\-\-container\fR] [\fB\-h\fR|\fB\-\-help\fR] <\fIAGENT\fR> [\fIARGS...\fR]
.SH DESCRIPTION
Run the agent command directly with the given arguments (no process management)
.SH OPTIONS
@@ -15,6 +15,9 @@ Local model to use for this run (validated against the local runtimes)
\fB\-\-provider\fR \fI<PROVIDER>\fR
Provider to use for this run (issue #92): registry lookup, base_url + keyring token + model
.TP
\fB\-\-no\-sandbox\fR
Skip the agent\*(Aqs sandbox profile (issue #79)
.TP
\fB\-\-container\fR
Run the agent inside a container (issue #58)
.TP
+5 -2
View File
@@ -4,7 +4,7 @@
.SH NAME
start \- Start an agent (foreground by default, or detached with \-\-background)
.SH SYNOPSIS
\fBstart\fR [\fB\-b\fR|\fB\-\-background\fR] [\fB\-f\fR|\fB\-\-foreground\fR] [\fB\-\-args\fR] [\fB\-\-env\fR] [\fB\-\-notify\fR] [\fB\-\-profile\fR] [\fB\-\-model\fR] [\fB\-\-provider\fR] [\fB\-\-parallel\fR] [\fB\-\-container\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fIAGENT\fR]
\fBstart\fR [\fB\-b\fR|\fB\-\-background\fR] [\fB\-f\fR|\fB\-\-foreground\fR] [\fB\-\-args\fR] [\fB\-\-env\fR] [\fB\-\-notify\fR] [\fB\-\-profile\fR] [\fB\-\-model\fR] [\fB\-\-provider\fR] [\fB\-\-no\-sandbox\fR] [\fB\-\-parallel\fR] [\fB\-\-container\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fIAGENT\fR]
.SH DESCRIPTION
Start an agent (foreground by default, or detached with \-\-background)
.SH OPTIONS
@@ -31,7 +31,10 @@ Apply an environment profile (env + args, defined in the config)
Local model to use for this run (validated against the local runtimes)
.TP
\fB\-\-provider\fR \fI<PROVIDER>\fR
Provider to use for this run (issue #92): registry lookup, base_url + keyring token + model
Provider to use for this run (issue #92): resolved from the registry
.TP
\fB\-\-no\-sandbox\fR
Skip the agent\*(Aqs sandbox profile for this run (issue #79)
.TP
\fB\-\-parallel\fR
Start every member of a group simultaneously (issue #57)
+1
View File
@@ -417,6 +417,7 @@ mod tests {
provider,
model: None,
config: Some(config),
sandbox: None,
tags: vec![],
installable: false,
note: None,
+8 -2
View File
@@ -512,6 +512,9 @@ pub enum Command {
/// Provider to use for this run (issue #92): registry lookup, base_url + keyring token + model
#[arg(long, value_name = "PROVIDER")]
provider: Option<String>,
/// Skip the agent's sandbox profile (issue #79)
#[arg(long)]
no_sandbox: bool,
/// Run the agent inside a container (issue #58)
#[arg(long)]
container: bool,
@@ -583,9 +586,12 @@ pub struct StartArgs {
/// Local model to use for this run (validated against the local runtimes)
#[arg(long, value_name = "MODEL")]
pub model: Option<String>,
/// Provider to use for this run (issue #92): registry lookup, base_url + keyring token + model
#[arg(long, value_name = "PROVIDER")]
/// Provider to use for this run (issue #92): resolved from the registry
#[arg(long)]
pub provider: Option<String>,
/// Skip the agent's sandbox profile for this run (issue #79)
#[arg(long)]
pub no_sandbox: bool,
/// Start every member of a group simultaneously (issue #57)
#[arg(long, action = ArgAction::SetTrue)]
pub parallel: bool,
+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,
+28 -1
View File
@@ -212,6 +212,28 @@ pub struct RegistrySettings {
pub author: Option<String>,
}
/// Sandbox profile of an agent (issue #79): restrict which commands it may
/// run, which working directories it may use, and whether it may reach the
/// network. Enforcement is best-effort per platform — the launcher checks
/// the resolved command and the process cwd, and a warning is emitted when
/// the enforcement cannot be fully guaranteed.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct SandboxProfile {
/// Sandbox active for this agent (default false).
pub enabled: bool,
/// Allowed command names (basenames). Empty = no command restriction.
#[serde(default)]
pub commands: Vec<String>,
/// Allowed working directories (support ~ and env vars). Empty = no
/// directory restriction.
#[serde(default)]
pub dirs: Vec<String>,
/// Network access (default true). false = best-effort block.
#[serde(default = "default_true")]
pub network: bool,
}
/// One entry of the LLM provider registry (issue #88).
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(deny_unknown_fields)]
@@ -403,9 +425,14 @@ pub struct AgentDef {
/// provider's default_model when unset.
#[serde(default)]
pub model: Option<String>,
/// Post-install provider configuration: env_map + files (issue #91).
/// Post-install provider configuration (issue #91).
#[serde(default)]
pub config: Option<AgentConfig>,
/// Sandbox profile (issue #79): allowed commands, working directories
/// and network policy. Disabled by default — the unsandboxed mode
/// stays available and documented.
#[serde(default)]
pub sandbox: Option<SandboxProfile>,
#[serde(default)]
pub tags: Vec<String>,
/// When false, the agent is listed but cannot be installed locally.
+3
View File
@@ -50,6 +50,8 @@ pub enum EventKind {
Lab,
/// LLM provider registry change: add, remove, default (issue #88).
Provider,
/// Sandbox refusal journalized for audit (issue #79).
Sandbox,
}
impl EventKind {
@@ -76,6 +78,7 @@ impl EventKind {
EventKind::Alert => "alert",
EventKind::Lab => "lab",
EventKind::Provider => "provider",
EventKind::Sandbox => "sandbox",
}
}
}
+8 -1
View File
@@ -156,6 +156,12 @@ const START_FLAGS: &[HelpFlag] = &[
value: "",
desc: "Run inside the agent's container profile (issue #58)",
},
HelpFlag {
short: "",
long: "--no-sandbox",
value: "",
desc: "Skip the agent's sandbox profile (issue #79)",
},
HelpFlag {
short: "",
long: "--model",
@@ -969,7 +975,8 @@ pub static HELP_SPECS: &[HelpSpec] = &[
search_terms: &["exec", "pass-through", "container", "docker", "podman"],
flags: &[
HelpFlag { short: "", long: "--model", value: "MODEL", desc: "Model to use: local runtime first, then the provider registry (issue #71/#92)" },
HelpFlag { short: "", long: "--provider", value: "PROVIDER", desc: "Provider of the registry to use (base_url + keyring token + model) (issue #92)" },
HelpFlag { short: "", long: "--provider", value: "PROVIDER", desc: "Provider registry override at launch (issue #92)" },
HelpFlag { short: "", long: "--no-sandbox", value: "", desc: "Skip the agent's sandbox profile for this run (issue #79)" },
HelpFlag { short: "", long: "--container", value: "", desc: "Run inside the agent's container profile (issue #58)" },
],
subcommands: &[],
+1
View File
@@ -46,6 +46,7 @@ pub mod process;
pub mod registry;
pub mod repl;
pub mod runner;
pub mod sandbox;
pub mod secrets;
pub mod sessions;
pub mod shell;
+2
View File
@@ -1778,6 +1778,7 @@ fn handle_line(
profile: opt_value("--profile"),
model: opt_value("--model"),
provider: opt_value("--provider"),
no_sandbox: flag("--no-sandbox"),
..Default::default()
}),
"stop" => Command::Stop {
@@ -1888,6 +1889,7 @@ fn handle_line(
agent,
model: opt_value("--model"),
provider: opt_value("--provider"),
no_sandbox: flag("--no-sandbox"),
container: flag("--container"),
args: rest[1..].iter().map(|s| s.as_str().into()).collect(),
}
+204
View File
@@ -0,0 +1,204 @@
//! Sandbox profiles (issue #79): restrict which commands an agent may run,
//! which working directories it may use, and its network policy.
//!
//! Enforcement is best-effort per platform: the launcher checks the resolved
//! command against the allowlist and the process cwd against the allowed
//! directories BEFORE spawning; network blocking uses proxy environment
//! variables (a warning is emitted where it cannot be guaranteed). Every
//! refused attempt is journalized (EventKind::Sandbox) for audit.
use crate::app::App;
use crate::config::{AgentDef, SandboxProfile};
use anyhow::{anyhow, Result};
use std::collections::BTreeMap;
use std::path::Path;
/// Active profile of an agent (None = unsandboxed, the documented default).
pub fn profile_of(agent: &AgentDef) -> Option<&SandboxProfile> {
agent.sandbox.as_ref().filter(|p| p.enabled)
}
/// Check the resolved command and the process cwd against the profile, and
/// apply the network policy (mutating the environment). Returns an error
/// (and emits a Sandbox event) when the launch is refused.
pub fn enforce(
app: &App,
agent: &AgentDef,
prog: &str,
env: &mut BTreeMap<String, String>,
skip: bool,
) -> Result<()> {
let Some(profile) = profile_of(agent) else {
return Ok(());
};
if skip {
app.log.warn(&format!(
"sandbox de '{}' ignorée (--no-sandbox)",
agent.name
));
return Ok(());
}
// 1. Command allowlist (basename of the resolved program).
let bin = Path::new(prog)
.file_name()
.map(|b| b.to_string_lossy().to_string())
.unwrap_or_else(|| prog.to_string());
// Windows: "python.exe" and "python" both match the allowlist entry.
let bin_stem = Path::new(&bin)
.file_stem()
.map(|b| b.to_string_lossy().to_string())
.unwrap_or_else(|| bin.clone());
let allowed = |c: &String| c == &bin || c == &bin_stem;
if !profile.commands.is_empty() && !profile.commands.iter().any(allowed) {
let denied = format!("command:{bin}");
app.emit(
&crate::events::Event::now(crate::events::EventKind::Sandbox)
.with_agent(agent.name.clone())
.with_args(vec![denied]),
);
return Err(anyhow!(
"commande '{}' hors profil sandbox de '{}' — autorisées: {}",
bin,
agent.name,
profile.commands.join(", ")
));
}
// 2. Working-directory perimeter.
if !profile.dirs.is_empty() {
let cwd = std::env::current_dir().unwrap_or_default();
let allowed = profile
.dirs
.iter()
.any(|d| cwd.starts_with(crate::config::expand_path(d)));
if !allowed {
let denied = format!("cwd:{}", cwd.display());
app.emit(
&crate::events::Event::now(crate::events::EventKind::Sandbox)
.with_agent(agent.name.clone())
.with_args(vec![denied]),
);
return Err(anyhow!(
"répertoire '{}' hors périmètre sandbox de '{}' — autorisés: {}",
cwd.display(),
agent.name,
profile.dirs.join(", ")
));
}
}
// 3. Network policy: best-effort block through proxy env vars.
if !profile.network {
env.insert("HTTP_PROXY".to_string(), "http://127.0.0.1:1".to_string());
env.insert("HTTPS_PROXY".to_string(), "http://127.0.0.1:1".to_string());
env.insert("ALL_PROXY".to_string(), "http://127.0.0.1:1".to_string());
app.log.warn(&format!(
"réseau bloqué pour '{}' (best-effort via proxy) — non garanti sur cette plateforme",
agent.name
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
fn test_app(sandbox_yaml: &str) -> App {
let guard = tempfile::tempdir().unwrap();
let dir = guard.path().to_path_buf();
std::mem::forget(guard);
let cfg = dir.join("config.yaml");
std::fs::write(
&cfg,
format!(
"version: \"1.0\"\nsettings:\n auto_install_deps: false\n confirm_before_run: false\nagents:\n - name: demo\n installable: false\n run: demo\n {sandbox_yaml}\n"
),
)
.unwrap();
let cli = crate::cli::Cli::parse_from(["am", "--config", cfg.to_str().unwrap()]);
crate::app::App::from_cli(cli).unwrap()
}
fn agent(app: &App) -> &AgentDef {
app.catalog.resolve("demo").unwrap()
}
#[test]
fn no_profile_means_unsandboxed() {
let app = test_app("");
assert!(profile_of(agent(&app)).is_none());
let mut env = BTreeMap::new();
// No profile: everything allowed, nothing refused.
enforce(&app, agent(&app), "anything.exe", &mut env, false).unwrap();
assert!(env.is_empty());
}
#[test]
fn disallowed_command_is_refused_and_journalized() {
let app = test_app(
"sandbox:\n enabled: true\n commands: [python]\n dirs: []\n network: true\n",
);
let mut env = BTreeMap::new();
let err = enforce(&app, agent(&app), "C:/tools/powershell.exe", &mut env, false)
.unwrap_err();
assert!(err.to_string().contains("hors profil"), "{err}");
// The refusal is journalized for audit.
let dir = app.events_dir();
let journaled = std::fs::read_dir(&dir)
.map(|rd| {
rd.flatten().any(|e| {
let p = e.path();
p.extension().map(|x| x == "jsonl").unwrap_or(false)
&& std::fs::read_to_string(&p)
.map(|t| t.contains("sandbox"))
.unwrap_or(false)
})
})
.unwrap_or(false);
assert!(journaled, "refusal must be journalized");
}
#[test]
fn allowed_command_and_cwd_pass() {
let cwd = std::env::current_dir().unwrap();
let app = test_app(&format!(
"sandbox:\n enabled: true\n commands: [demo.exe]\n dirs: [\"{}\"]\n network: true\n",
cwd.display().to_string().replace('\\', "\\\\")
));
let mut env = BTreeMap::new();
enforce(&app, agent(&app), "demo.exe", &mut env, false).unwrap();
assert!(env.is_empty());
}
#[test]
fn cwd_outside_perimeter_is_blocked() {
let app = test_app(
"sandbox:\n enabled: true\n commands: []\n dirs: [\"C:/definitely/not/here\"]\n network: true\n",
);
let mut env = BTreeMap::new();
let err = enforce(&app, agent(&app), "demo.exe", &mut env, false).unwrap_err();
assert!(err.to_string().contains("hors périmètre"), "{err}");
}
#[test]
fn network_off_injects_blocking_proxy_env() {
let app = test_app(
"sandbox:\n enabled: true\n commands: []\n dirs: []\n network: false\n",
);
let mut env = BTreeMap::new();
enforce(&app, agent(&app), "demo.exe", &mut env, false).unwrap();
assert_eq!(env.get("HTTP_PROXY").map(String::as_str), Some("http://127.0.0.1:1"));
assert!(env.contains_key("HTTPS_PROXY"));
assert!(env.contains_key("ALL_PROXY"));
}
#[test]
fn no_sandbox_flag_bypasses_the_profile() {
let app = test_app(
"sandbox:\n enabled: true\n commands: [python]\n dirs: []\n network: true\n",
);
let mut env = BTreeMap::new();
// --no-sandbox: the disallowed command passes (with a warning).
enforce(&app, agent(&app), "powershell.exe", &mut env, true).unwrap();
}
}
+2 -2
View File
@@ -45,7 +45,7 @@ agents:
#[test]
fn run_emits_one_run_event_with_exit_code() {
let app = app_with_agent("run", &["run", "echo-agent"], ECHO_AGENT);
let code = run_cmd::run(&app, "echo-agent", None, None, false, &[OsString::from("hello")]).unwrap();
let code = run_cmd::run(&app, "echo-agent", None, None, false, false, &[OsString::from("hello")]).unwrap();
assert_eq!(code, 0);
let evs = events(&app);
assert_eq!(evs.len(), 1, "exactly one event expected, got {:?}", evs);
@@ -58,7 +58,7 @@ fn run_emits_one_run_event_with_exit_code() {
#[test]
fn dry_run_writes_no_event() {
let app = app_with_agent("dry", &["run", "echo-agent", "--dry-run"], ECHO_AGENT);
let code = run_cmd::run(&app, "echo-agent", None, None, false, &[]).unwrap();
let code = run_cmd::run(&app, "echo-agent", None, None, false, false, &[]).unwrap();
assert_eq!(code, 0);
assert!(events(&app).is_empty(), "dry-run must not append events");
assert!(events::journal_files(&app.events_dir()).is_empty());