Add --keep-fd to pass chosen inherited descriptors into the sandbox
This commit is contained in:
@@ -47,7 +47,7 @@ Settings can be stored in a TOML config file at `$XDG_CONFIG_HOME/agent-sandbox/
|
|||||||
ln -sf "$(pwd)/config-example.toml" "${XDG_CONFIG_HOME:-$HOME/.config}/agent-sandbox/config.toml"
|
ln -sf "$(pwd)/config-example.toml" "${XDG_CONFIG_HOME:-$HOME/.config}/agent-sandbox/config.toml"
|
||||||
```
|
```
|
||||||
|
|
||||||
Top-level keys set defaults; `[profile.<name>]` sections define named presets selectable with `--profile <name>`. CLI flags always take highest precedence, followed by the active profiles, then top-level defaults. When a profile is active, list-valued fields (`ro`, `rw`, `mask`, `env`, `unsetenv`, `bwrap-args`) append to the globals; scalar fields (modes, flags, `entrypoint`, `command`, `chdir`) replace them.
|
Top-level keys set defaults; `[profile.<name>]` sections define named presets selectable with `--profile <name>`. CLI flags always take highest precedence, followed by the active profiles, then top-level defaults. When a profile is active, list-valued fields (`ro`, `rw`, `mask`, `env`, `unsetenv`, `bwrap-args`, `keep-fd`) append to the globals; scalar fields (modes, flags, `entrypoint`, `command`, `chdir`) replace them.
|
||||||
|
|
||||||
Several profiles combine: `--profile claude,gpu` (or `--profile claude --profile gpu`, or `profile = ["claude", "gpu"]` in the config) merges them left to right under the same rules, so a later profile's scalars win. A CLI `--profile` replaces the config's list rather than extending it. A profile's own `profile = ...` names its parents, which merge before it; a profile reached twice merges once.
|
Several profiles combine: `--profile claude,gpu` (or `--profile claude --profile gpu`, or `profile = ["claude", "gpu"]` in the config) merges them left to right under the same rules, so a later profile's scalars win. A CLI `--profile` replaces the config's list rather than extending it. A profile's own `profile = ...` names its parents, which merge before it; a profile reached twice merges once.
|
||||||
|
|
||||||
@@ -84,11 +84,14 @@ A user `--rw`/`--ro` targeting `/run/agent-sandbox` overrides the directory, lik
|
|||||||
|
|
||||||
When the agent needs access to something the sandbox blocks, use `--rw` or `--ro` for paths and `--setenv`/`--unsetenv` for env vars. User overrides always win over the built-in policies.
|
When the agent needs access to something the sandbox blocks, use `--rw` or `--ro` for paths and `--setenv`/`--unsetenv` for env vars. User overrides always win over the built-in policies.
|
||||||
|
|
||||||
|
Inherited file descriptors above stderr never reach `bwrap` or the sandbox, because an open directory descriptor would bypass every mount restriction. Pass one through with `--keep-fd FD` (or `keep-fd = [FD]`), for example to feed a `bwrap` option that reads a descriptor.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
agent-sandbox --rw /var/run/docker.sock -- claude --dangerously-skip-permissions
|
agent-sandbox --rw /var/run/docker.sock -- claude --dangerously-skip-permissions
|
||||||
agent-sandbox --ro ~/.aws -- claude --dangerously-skip-permissions
|
agent-sandbox --ro ~/.aws -- claude --dangerously-skip-permissions
|
||||||
agent-sandbox --setenv DATABASE_URL=postgres://localhost/dev -- claude
|
agent-sandbox --setenv DATABASE_URL=postgres://localhost/dev -- claude
|
||||||
agent-sandbox --unsetenv HTTP_PROXY -- claude
|
agent-sandbox --unsetenv HTTP_PROXY -- claude
|
||||||
|
agent-sandbox --keep-fd 3 --bwrap-arg '--file 3 /tmp/input' -- claude 3<input.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
## Ubuntu 23.10+: AppArmor unprivileged userns restrictions
|
## Ubuntu 23.10+: AppArmor unprivileged userns restrictions
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ env = [
|
|||||||
# entrypoint = ["claude", "--dangerously-skip-permissions"] # binary + baked-in args
|
# entrypoint = ["claude", "--dangerously-skip-permissions"] # binary + baked-in args
|
||||||
# command = ["--model", "opus"] # default trailing args
|
# command = ["--model", "opus"] # default trailing args
|
||||||
# bwrap-args = ["--tmpfs /opt/scratch"] # raw bwrap escape hatch
|
# bwrap-args = ["--tmpfs /opt/scratch"] # raw bwrap escape hatch
|
||||||
|
# keep-fd = [3] # pass inherited fds through
|
||||||
|
|
||||||
profile = ["claude", "audio"]
|
profile = ["claude", "audio"]
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
|
use std::os::fd::RawFd;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
@@ -117,6 +118,11 @@ pub struct Args {
|
|||||||
#[arg(long = "bwrap-arg", value_name = "ARG", action = clap::ArgAction::Append)]
|
#[arg(long = "bwrap-arg", value_name = "ARG", action = clap::ArgAction::Append)]
|
||||||
pub bwrap_args: Vec<String>,
|
pub bwrap_args: Vec<String>,
|
||||||
|
|
||||||
|
/// Pass an inherited file descriptor into the sandbox (repeatable).
|
||||||
|
/// All other inherited descriptors above stderr are closed.
|
||||||
|
#[arg(long = "keep-fd", value_name = "FD", action = clap::ArgAction::Append)]
|
||||||
|
pub keep_fd: Vec<RawFd>,
|
||||||
|
|
||||||
/// Run this binary with the trailing args as its arguments
|
/// Run this binary with the trailing args as its arguments
|
||||||
#[arg(long, value_name = "CMD")]
|
#[arg(long, value_name = "CMD")]
|
||||||
pub entrypoint: Option<String>,
|
pub entrypoint: Option<String>,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
|
use std::os::fd::RawFd;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
@@ -93,6 +94,7 @@ pub fn build(args: Args, file_config: Option<FileConfig>) -> Result<SandboxConfi
|
|||||||
&profile.options.bwrap_args,
|
&profile.options.bwrap_args,
|
||||||
&globals.bwrap_args,
|
&globals.bwrap_args,
|
||||||
))?,
|
))?,
|
||||||
|
keep_fds: merge_vecs(args.keep_fd, &profile.options.keep_fd, &globals.keep_fd),
|
||||||
command,
|
command,
|
||||||
command_args,
|
command_args,
|
||||||
persistent_tmp_key,
|
persistent_tmp_key,
|
||||||
@@ -526,6 +528,8 @@ pub struct Options {
|
|||||||
pub unsetenv: Vec<String>,
|
pub unsetenv: Vec<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub bwrap_args: Vec<String>,
|
pub bwrap_args: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub keep_fd: Vec<RawFd>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Options {
|
impl Options {
|
||||||
@@ -551,6 +555,7 @@ impl Options {
|
|||||||
env: append(self.env, extra.env),
|
env: append(self.env, extra.env),
|
||||||
unsetenv: append(self.unsetenv, extra.unsetenv),
|
unsetenv: append(self.unsetenv, extra.unsetenv),
|
||||||
bwrap_args: append(self.bwrap_args, extra.bwrap_args),
|
bwrap_args: append(self.bwrap_args, extra.bwrap_args),
|
||||||
|
keep_fd: append(self.keep_fd, extra.keep_fd),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use std::os::fd::RawFd;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -36,6 +37,7 @@ pub enum SandboxError {
|
|||||||
PersistentTmpDir(PathBuf, String),
|
PersistentTmpDir(PathBuf, String),
|
||||||
ProcChildrenMissing,
|
ProcChildrenMissing,
|
||||||
InheritedFdsCloexec(std::io::Error),
|
InheritedFdsCloexec(std::io::Error),
|
||||||
|
KeepFd(RawFd, std::io::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for SandboxError {
|
impl std::fmt::Display for SandboxError {
|
||||||
@@ -117,6 +119,12 @@ impl std::fmt::Display for SandboxError {
|
|||||||
f,
|
f,
|
||||||
"the kernel does not provide /proc/<pid>/task/<tid>/children (CONFIG_PROC_CHILDREN); agent-sandbox needs it to find the processes it must clean up"
|
"the kernel does not provide /proc/<pid>/task/<tid>/children (CONFIG_PROC_CHILDREN); agent-sandbox needs it to find the processes it must clean up"
|
||||||
),
|
),
|
||||||
|
Self::KeepFd(fd, e) => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"cannot pass file descriptor {fd} into the sandbox (--keep-fd): {e}"
|
||||||
|
)
|
||||||
|
}
|
||||||
Self::InheritedFdsCloexec(e) => write!(
|
Self::InheritedFdsCloexec(e) => write!(
|
||||||
f,
|
f,
|
||||||
"cannot keep inherited file descriptors out of the sandbox (close_range with CLOSE_RANGE_CLOEXEC needs Linux 5.11+): {e}"
|
"cannot keep inherited file descriptors out of the sandbox (close_range with CLOSE_RANGE_CLOEXEC needs Linux 5.11+): {e}"
|
||||||
@@ -134,6 +142,7 @@ impl std::error::Error for SandboxError {
|
|||||||
Self::ConfigRead { source, .. } => Some(source),
|
Self::ConfigRead { source, .. } => Some(source),
|
||||||
Self::ConfigParse { source, .. } => Some(source),
|
Self::ConfigParse { source, .. } => Some(source),
|
||||||
Self::InheritedFdsCloexec(e) => Some(e),
|
Self::InheritedFdsCloexec(e) => Some(e),
|
||||||
|
Self::KeepFd(_, e) => Some(e),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-2
@@ -1,10 +1,20 @@
|
|||||||
use std::io;
|
use std::io;
|
||||||
|
use std::os::fd::RawFd;
|
||||||
|
|
||||||
use crate::SandboxError;
|
use crate::SandboxError;
|
||||||
|
|
||||||
const FIRST_FD_AFTER_STDIO: libc::c_uint = 3;
|
const FIRST_FD_AFTER_STDIO: libc::c_uint = 3;
|
||||||
|
|
||||||
pub fn keep_out_of_sandbox() -> Result<(), SandboxError> {
|
pub fn keep_out_of_sandbox(kept_fds: &[RawFd]) -> Result<(), SandboxError> {
|
||||||
|
mark_all_after_stdio_cloexec().map_err(SandboxError::InheritedFdsCloexec)?;
|
||||||
|
|
||||||
|
for &fd in kept_fds {
|
||||||
|
clear_cloexec(fd).map_err(|e| SandboxError::KeepFd(fd, e))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mark_all_after_stdio_cloexec() -> io::Result<()> {
|
||||||
let result = unsafe {
|
let result = unsafe {
|
||||||
libc::syscall(
|
libc::syscall(
|
||||||
libc::SYS_close_range,
|
libc::SYS_close_range,
|
||||||
@@ -14,7 +24,18 @@ pub fn keep_out_of_sandbox() -> Result<(), SandboxError> {
|
|||||||
)
|
)
|
||||||
};
|
};
|
||||||
if result < 0 {
|
if result < 0 {
|
||||||
return Err(SandboxError::InheritedFdsCloexec(io::Error::last_os_error()));
|
return Err(io::Error::last_os_error());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_cloexec(fd: RawFd) -> io::Result<()> {
|
||||||
|
let flags = unsafe { libc::fcntl(fd, libc::F_GETFD) };
|
||||||
|
if flags < 0 {
|
||||||
|
return Err(io::Error::last_os_error());
|
||||||
|
}
|
||||||
|
if unsafe { libc::fcntl(fd, libc::F_SETFD, flags & !libc::FD_CLOEXEC) } < 0 {
|
||||||
|
return Err(io::Error::last_os_error());
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -19,6 +19,7 @@ pub use signals::exit_with_status;
|
|||||||
|
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
use std::os::fd::RawFd;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::ExitStatus;
|
use std::process::ExitStatus;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@@ -67,6 +68,7 @@ pub struct SandboxConfig {
|
|||||||
pub env: Vec<EnvEntry>,
|
pub env: Vec<EnvEntry>,
|
||||||
pub unsetenv: Vec<String>,
|
pub unsetenv: Vec<String>,
|
||||||
pub bwrap_args: Vec<String>,
|
pub bwrap_args: Vec<String>,
|
||||||
|
pub keep_fds: Vec<RawFd>,
|
||||||
pub command: PathBuf,
|
pub command: PathBuf,
|
||||||
pub command_args: Vec<OsString>,
|
pub command_args: Vec<OsString>,
|
||||||
pub chdir: PathBuf,
|
pub chdir: PathBuf,
|
||||||
@@ -100,7 +102,7 @@ fn resolve_run_user_from_proc() -> Option<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn run(config: SandboxConfig) -> Result<ExitStatus, SandboxError> {
|
pub fn run(config: SandboxConfig) -> Result<ExitStatus, SandboxError> {
|
||||||
inherited_fds::keep_out_of_sandbox()?;
|
inherited_fds::keep_out_of_sandbox(&config.keep_fds)?;
|
||||||
preflight::check(&config)?;
|
preflight::check(&config)?;
|
||||||
|
|
||||||
let forwarded_signals = ForwardedSignals::detect();
|
let forwarded_signals = ForwardedSignals::detect();
|
||||||
|
|||||||
+3
-13
@@ -7,6 +7,7 @@ use std::process::Command;
|
|||||||
|
|
||||||
use crate::SandboxConfig;
|
use crate::SandboxConfig;
|
||||||
use crate::errors::SandboxError;
|
use crate::errors::SandboxError;
|
||||||
|
use crate::inherited_fds;
|
||||||
|
|
||||||
pub struct PersistentTmpDirs {
|
pub struct PersistentTmpDirs {
|
||||||
tmp: BindSource,
|
tmp: BindSource,
|
||||||
@@ -80,7 +81,8 @@ impl PersistentTmpDir {
|
|||||||
.map_err(|e| self.error(format!("open: {e}")))?;
|
.map_err(|e| self.error(format!("open: {e}")))?;
|
||||||
self.verify_owner(&file)?;
|
self.verify_owner(&file)?;
|
||||||
let fd: OwnedFd = file.into();
|
let fd: OwnedFd = file.into();
|
||||||
clear_cloexec(&fd).map_err(|e| self.error(format!("clear FD_CLOEXEC: {e}")))?;
|
inherited_fds::clear_cloexec(fd.as_raw_fd())
|
||||||
|
.map_err(|e| self.error(format!("clear FD_CLOEXEC: {e}")))?;
|
||||||
Ok(fd)
|
Ok(fd)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,18 +110,6 @@ impl PersistentTmpDir {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clear_cloexec(fd: &OwnedFd) -> io::Result<()> {
|
|
||||||
let raw = fd.as_raw_fd();
|
|
||||||
let flags = unsafe { libc::fcntl(raw, libc::F_GETFD) };
|
|
||||||
if flags < 0 {
|
|
||||||
return Err(io::Error::last_os_error());
|
|
||||||
}
|
|
||||||
if unsafe { libc::fcntl(raw, libc::F_SETFD, flags & !libc::FD_CLOEXEC) } < 0 {
|
|
||||||
return Err(io::Error::last_os_error());
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
+47
-8
@@ -500,11 +500,9 @@ fn persistent_tmp_leaks_no_descriptor_into_host_tmp() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn inherited_host_descriptor_does_not_reach_sandbox() {
|
fn inherited_host_descriptor_does_not_reach_sandbox() {
|
||||||
const PASSED_FD: i32 = 9;
|
|
||||||
let host_dir = TempDir::new().unwrap();
|
let host_dir = TempDir::new().unwrap();
|
||||||
fs::write(host_dir.path().join("canary"), "host-secret").unwrap();
|
fs::write(host_dir.path().join("canary"), "host-secret").unwrap();
|
||||||
let host_dir_handle = fs::File::open(host_dir.path()).unwrap();
|
let host_dir_handle = fs::File::open(host_dir.path()).unwrap();
|
||||||
let host_dir_fd = host_dir_handle.as_raw_fd();
|
|
||||||
|
|
||||||
let mut sandbox = Sandbox::new(&[]);
|
let mut sandbox = Sandbox::new(&[]);
|
||||||
sandbox.args([
|
sandbox.args([
|
||||||
@@ -513,12 +511,7 @@ fn inherited_host_descriptor_does_not_reach_sandbox() {
|
|||||||
"-c",
|
"-c",
|
||||||
&format!("cat /proc/self/fd/{PASSED_FD}/canary 2>/dev/null; true"),
|
&format!("cat /proc/self/fd/{PASSED_FD}/canary 2>/dev/null; true"),
|
||||||
]);
|
]);
|
||||||
unsafe {
|
pass_as_fd(&mut sandbox, &host_dir_handle, PASSED_FD);
|
||||||
sandbox.pre_exec(move || match libc::dup2(host_dir_fd, PASSED_FD) {
|
|
||||||
-1 => Err(std::io::Error::last_os_error()),
|
|
||||||
_ => Ok(()),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let output = sandbox
|
let output = sandbox
|
||||||
.output()
|
.output()
|
||||||
.expect("agent-sandbox binary failed to execute");
|
.expect("agent-sandbox binary failed to execute");
|
||||||
@@ -534,6 +527,52 @@ fn inherited_host_descriptor_does_not_reach_sandbox() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn kept_descriptor_reaches_sandbox() {
|
||||||
|
let scratch = TempDir::new().unwrap();
|
||||||
|
let kept_file = scratch.path().join("kept");
|
||||||
|
fs::write(&kept_file, "kept-content").unwrap();
|
||||||
|
let kept_handle = fs::File::open(&kept_file).unwrap();
|
||||||
|
|
||||||
|
let mut sandbox = Sandbox::new(&["--keep-fd", &PASSED_FD.to_string()]);
|
||||||
|
sandbox.args(["--", "bash", "-c", &format!("cat <&{PASSED_FD}")]);
|
||||||
|
pass_as_fd(&mut sandbox, &kept_handle, PASSED_FD);
|
||||||
|
let output = sandbox
|
||||||
|
.output()
|
||||||
|
.expect("agent-sandbox binary failed to execute");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
output.status.success(),
|
||||||
|
"stderr: {}",
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
|
assert_eq!(String::from_utf8_lossy(&output.stdout), "kept-content");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn keep_fd_rejects_descriptor_that_is_not_open() {
|
||||||
|
let output = Sandbox::new(&["--keep-fd", "999", "--dry-run"])
|
||||||
|
.args(["--", "true"])
|
||||||
|
.output()
|
||||||
|
.expect("agent-sandbox binary failed to execute");
|
||||||
|
|
||||||
|
assert!(!output.status.success());
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
assert!(stderr.contains("999"), "stderr: {stderr}");
|
||||||
|
}
|
||||||
|
|
||||||
|
const PASSED_FD: i32 = 9;
|
||||||
|
|
||||||
|
fn pass_as_fd(sandbox: &mut Sandbox, file: &fs::File, target_fd: i32) {
|
||||||
|
let source_fd = file.as_raw_fd();
|
||||||
|
unsafe {
|
||||||
|
sandbox.pre_exec(move || match libc::dup2(source_fd, target_fd) {
|
||||||
|
-1 => Err(std::io::Error::last_os_error()),
|
||||||
|
_ => Ok(()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn persistent_tmp_is_noop_in_blacklist_mode() {
|
fn persistent_tmp_is_noop_in_blacklist_mode() {
|
||||||
let label = format!("e2e-bl-noop-{}-{}", std::process::id(), rand_suffix());
|
let label = format!("e2e-bl-noop-{}-{}", std::process::id(), rand_suffix());
|
||||||
|
|||||||
@@ -1255,6 +1255,43 @@ fn build_mask_accumulates() {
|
|||||||
assert_eq!(config.mask.len(), 3);
|
assert_eq!(config.mask.len(), 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_keep_fd_accumulates_globals_then_profile_then_cli() {
|
||||||
|
let file_config = FileConfig {
|
||||||
|
options: Options {
|
||||||
|
keep_fd: vec![3],
|
||||||
|
..Options::default()
|
||||||
|
},
|
||||||
|
profiles: HashMap::from([(
|
||||||
|
"extra".into(),
|
||||||
|
Options {
|
||||||
|
keep_fd: vec![4],
|
||||||
|
..Options::default()
|
||||||
|
},
|
||||||
|
)]),
|
||||||
|
..FileConfig::default()
|
||||||
|
};
|
||||||
|
let args = Args {
|
||||||
|
profile: vec!["extra".into()],
|
||||||
|
keep_fd: vec![5],
|
||||||
|
..args_with_command()
|
||||||
|
};
|
||||||
|
let config = build(args, Some(file_config)).unwrap();
|
||||||
|
assert_eq!(config.keep_fds, vec![3, 4, 5]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn keep_fd_parsed_from_config() {
|
||||||
|
let config = FileConfig::parse("keep-fd = [3, 4]").unwrap();
|
||||||
|
assert_eq!(config.options.keep_fd, vec![3, 4]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn keep_fd_parsed_from_repeated_cli_flag() {
|
||||||
|
let args = Args::try_parse_from(["agent-sandbox", "--keep-fd", "3", "--keep-fd", "4"]).unwrap();
|
||||||
|
assert_eq!(args.keep_fd, vec![3, 4]);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn unknown_option_rejected() {
|
fn unknown_option_rejected() {
|
||||||
let toml = r#"
|
let toml = r#"
|
||||||
@@ -1460,16 +1497,19 @@ fn merge_options_lists_append_base_then_extra() {
|
|||||||
let base = Options {
|
let base = Options {
|
||||||
rw: vec!["/a".into()],
|
rw: vec!["/a".into()],
|
||||||
env: vec!["A=1".into()],
|
env: vec!["A=1".into()],
|
||||||
|
keep_fd: vec![3],
|
||||||
..Options::default()
|
..Options::default()
|
||||||
};
|
};
|
||||||
let extra = Options {
|
let extra = Options {
|
||||||
rw: vec!["/b".into()],
|
rw: vec!["/b".into()],
|
||||||
env: vec!["B=2".into()],
|
env: vec!["B=2".into()],
|
||||||
|
keep_fd: vec![4],
|
||||||
..Options::default()
|
..Options::default()
|
||||||
};
|
};
|
||||||
let merged = base.merge_with(extra);
|
let merged = base.merge_with(extra);
|
||||||
assert_eq!(merged.rw, vec!["/a", "/b"]);
|
assert_eq!(merged.rw, vec!["/a", "/b"]);
|
||||||
assert_eq!(merged.env, vec!["A=1", "B=2"]);
|
assert_eq!(merged.env, vec!["A=1", "B=2"]);
|
||||||
|
assert_eq!(merged.keep_fd, vec![3, 4]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user