mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-08 01:48:20 -10:00
fix(sandbox): default Linux workspace-write to bubblewrap, probed and disclosed
The workspace-write posture on Linux silently ran unrestricted: prefer_bwrap defaulted to false, so select_sandbox resolved SandboxType::None and the confined command became an unconfined one. Reproduced as an outside-workspace write succeeding under a workspace-write policy. Three parts, mirroring the extension host's proven pattern: - Config::prefers_bwrap() resolves the key as on-by-default; every consumer (engine config, exec agent, TUI init/frame, runtime threads, doctor/setup status) migrates to the accessor. prefer_bwrap = false remains a working opt-out and danger-full-access is untouched. - bwrap::is_available() now runs a real wrapped /bin/true once (built by build_bwrap_command itself, cached in a OnceLock). The exec bit alone lied on hosts that restrict user namespaces (Ubuntu 24.04 apparmor_restrict_unprivileged_userns), where every wrapped command would fail. - codewhale exec prints a stderr warning when the resolved policy wants a sandbox but no enforcing wrapper exists, so policy-only never goes silent for the human. The model already saw the same posture via turn_meta. Real Linux proof, not a marker check: new test bwrap_workspace_write_blocks_outside_write_allows_inside runs the same outside-workspace write unwrapped (lands on the host) and wrapped (EROFS, file absent), plus a normal inside-workspace write that succeeds under the wrapper. Evidence: cargo test -p codewhale-tui --lib --locked -- bwrap prefers_bwrap linux_ parity_manager -> 26 passed; 0 failed. Docs aligned across SANDBOX.md (en/zh), CONFIGURATION.md (en/zh), config.example.toml (also drops the stale Landlock comment), the FAQ page and dictionaries, features.toml, and the public-surface facts both sides of the drift gate.
This commit is contained in:
1 parent
3dd34faf14
commit
b69eccaacb
24 files changed
+311
-111
No files matched your search
+8
-8
@@ -285,21 +285,21 @@ sandbox_mode = "workspace-write" # read-only | workspace-write | danger-full-acc
|
||||
# TTY modes are not supported with external backends — all commands run
|
||||
# synchronously via HTTP.
|
||||
# ─────────────────────────────────────────────────────────────────────────────────
|
||||
# Bubblewrap (Linux only, additional filesystem isolation)
|
||||
# Bubblewrap (Linux only, OS-level filesystem isolation)
|
||||
# ─────────────────────────────────────────────────────────────────────────────────
|
||||
# When set to true and `/usr/bin/bwrap` is present, exec_shell commands are
|
||||
# routed through bubblewrap instead of relying solely on Landlock. Bubblewrap
|
||||
# creates a read-only view of the root filesystem with write access limited to
|
||||
# the working directory. Install separately:
|
||||
# On by default: when `/usr/bin/bwrap` is installed and a probe shows it can
|
||||
# create its namespaces on this host, sandboxed exec_shell commands are routed
|
||||
# through it. Bubblewrap creates a read-only view of the root filesystem with
|
||||
# write access limited to the sandbox mode's writable roots. Install separately:
|
||||
#
|
||||
# Ubuntu/Debian: apt install bubblewrap
|
||||
# Fedora: dnf install bubblewrap
|
||||
# Arch: pacman -S bubblewrap
|
||||
#
|
||||
# prefer_bwrap = false # default — use Landlock only
|
||||
# prefer_bwrap = false # opt out — Linux commands run unwrapped
|
||||
#
|
||||
# Env override: CODEWHALE_PREFER_BWRAP=true
|
||||
# Legacy alias (deprecated until 0.10.0): DEEPSEEK_PREFER_BWRAP=true
|
||||
# Env override: CODEWHALE_PREFER_BWRAP=true|false
|
||||
# Legacy alias (deprecated until 0.10.0): DEEPSEEK_PREFER_BWRAP=true|false
|
||||
#
|
||||
# With prefer_bwrap = true, the sandbox gets a private /dev and /proc plus a
|
||||
# writable isolated /tmp by default, so toolchains work without widening the
|
||||
|
||||
@@ -2349,10 +2349,14 @@ pub struct Config {
|
||||
/// Optional API key for the external sandbox backend (sent as Bearer token).
|
||||
#[serde(alias = "sandboxApiKey")]
|
||||
pub sandbox_api_key: Option<String>,
|
||||
/// When true and `/usr/bin/bwrap` is executable on Linux, route exec_shell
|
||||
/// through bubblewrap (#2184).
|
||||
/// Defaults to false. Requires the `bubblewrap` package to be installed
|
||||
/// separately — we do NOT vendor bwrap.
|
||||
/// When true and bubblewrap actually works on this Linux host, route
|
||||
/// sandboxed exec_shell commands through it (#2184).
|
||||
/// Defaults to true — an unset key means sandboxed commands run under
|
||||
/// bwrap whenever `/usr/bin/bwrap` is installed and can create its
|
||||
/// namespaces. An explicit `prefer_bwrap = false` opts out and leaves
|
||||
/// Linux commands unwrapped (the posture then reports policy-only).
|
||||
/// Requires the `bubblewrap` package to be installed separately — we do
|
||||
/// NOT vendor bwrap.
|
||||
#[serde(alias = "preferBwrap")]
|
||||
pub prefer_bwrap: Option<bool>,
|
||||
/// Additional host paths to bind read-only inside the bubblewrap sandbox
|
||||
@@ -3685,6 +3689,17 @@ impl Config {
|
||||
self.read_denylist().subtree_paths()
|
||||
}
|
||||
|
||||
/// Whether Linux shell commands prefer bubblewrap confinement.
|
||||
///
|
||||
/// On by default: an unset `prefer_bwrap` means sandboxed commands use
|
||||
/// the OS wrapper whenever `/usr/bin/bwrap` works on this host, matching
|
||||
/// the Seatbelt behavior macOS already has. An explicit `false` opts out
|
||||
/// and leaves Linux commands unwrapped.
|
||||
#[must_use]
|
||||
pub fn prefers_bwrap(&self) -> bool {
|
||||
self.prefer_bwrap.unwrap_or(true)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn stop_words(&self) -> Vec<String> {
|
||||
self.stop_words.clone().unwrap_or_else(default_stop_words)
|
||||
|
||||
@@ -2685,6 +2685,25 @@ fn legacy_prefer_bwrap_env_remains_a_compatible_alias() {
|
||||
assert_eq!(config.prefer_bwrap, Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prefers_bwrap_defaults_on_and_honors_explicit_opt_out() {
|
||||
assert!(Config::default().prefers_bwrap());
|
||||
assert!(
|
||||
Config {
|
||||
prefer_bwrap: Some(true),
|
||||
..Config::default()
|
||||
}
|
||||
.prefers_bwrap()
|
||||
);
|
||||
assert!(
|
||||
!Config {
|
||||
prefer_bwrap: Some(false),
|
||||
..Config::default()
|
||||
}
|
||||
.prefers_bwrap()
|
||||
);
|
||||
}
|
||||
|
||||
struct EnvGuard {
|
||||
// Seal path overrides through EnvVarGuard so default_config_path honors
|
||||
// this fixture instead of the isolated test root (#5355, #5359).
|
||||
|
||||
@@ -662,7 +662,8 @@ impl Default for EngineConfig {
|
||||
user_input_limits: crate::tools::user_input::UserInputLimits::default(),
|
||||
user_input_timeout: None,
|
||||
goal_max_steps: None,
|
||||
prefer_bwrap: false,
|
||||
// Mirrors `Config::prefers_bwrap`: on unless explicitly opted out.
|
||||
prefer_bwrap: true,
|
||||
bwrap_extensions: crate::sandbox::BwrapMountExtensions::default(),
|
||||
// Fail-closed (F7): `Engine::new` unconditionally installs this
|
||||
// list process-wide via `read_guard::set_active`, so a default
|
||||
|
||||
@@ -617,7 +617,7 @@ pub(crate) async fn run_exec_agent(
|
||||
subagent_heartbeat_timeout: std::time::Duration::from_secs(
|
||||
execution_config.subagent_heartbeat_timeout_secs_for_provider(&effective_identity),
|
||||
),
|
||||
prefer_bwrap: execution_config.prefer_bwrap.unwrap_or(false),
|
||||
prefer_bwrap: execution_config.prefers_bwrap(),
|
||||
bwrap_extensions: crate::sandbox::BwrapMountExtensions {
|
||||
read_only_roots: execution_config.bwrap_ro_roots.clone(),
|
||||
device_roots: execution_config.bwrap_dev_roots.clone(),
|
||||
@@ -691,6 +691,47 @@ pub(crate) async fn run_exec_agent(
|
||||
// The Full Access posture travels in the op's auto_approve/approval_mode
|
||||
// fields; modes no longer carry permission.
|
||||
let mode = AppMode::Agent;
|
||||
let turn_approval_mode = if auto_approve {
|
||||
ApprovalMode::Bypass
|
||||
} else {
|
||||
execution_config
|
||||
.approval_policy
|
||||
.as_deref()
|
||||
.and_then(ApprovalMode::from_config_value)
|
||||
.unwrap_or_default()
|
||||
};
|
||||
// A restricted posture that resolves to no enforcing sandbox must reach
|
||||
// the person running exec, not only the model's posture line: policy-only
|
||||
// is accepted (read-only already fails closed in the shell tool), but it
|
||||
// is never silent. Mirrors the enforcement detection in `Engine::new`.
|
||||
let turn_sandbox_policy = crate::core::authority::sandbox_policy_for_turn(
|
||||
mode,
|
||||
turn_approval_mode,
|
||||
execution_config.sandbox_mode.as_deref(),
|
||||
&workspace,
|
||||
crate::core::authority::SandboxNetworkAccess::from_config(
|
||||
execution_config.sandbox_network_access,
|
||||
),
|
||||
);
|
||||
let enforcement_unavailable = crate::sandbox::backend::SandboxKind::parse(
|
||||
execution_config
|
||||
.sandbox_backend
|
||||
.as_deref()
|
||||
.unwrap_or("none"),
|
||||
)
|
||||
.is_none()
|
||||
&& crate::sandbox::get_platform_sandbox_with_bwrap_preference(
|
||||
execution_config.prefers_bwrap(),
|
||||
)
|
||||
.is_none();
|
||||
if enforcement_unavailable && turn_sandbox_policy.should_sandbox() {
|
||||
eprintln!(
|
||||
"warning: {} — shell commands will run unrestricted",
|
||||
turn_sandbox_policy.posture_label_with_enforcement(
|
||||
crate::sandbox::policy::SandboxEnforcement::Unavailable,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
let resuming_session = resume_session.is_some();
|
||||
let mut loaded_session_id = None;
|
||||
@@ -762,15 +803,7 @@ pub(crate) async fn run_exec_agent(
|
||||
trust_mode,
|
||||
auto_approve,
|
||||
translation_enabled: false,
|
||||
approval_mode: if auto_approve {
|
||||
ApprovalMode::Bypass
|
||||
} else {
|
||||
execution_config
|
||||
.approval_policy
|
||||
.as_deref()
|
||||
.and_then(ApprovalMode::from_config_value)
|
||||
.unwrap_or_default()
|
||||
},
|
||||
approval_mode: turn_approval_mode,
|
||||
verbosity: execution_config.verbosity.clone(),
|
||||
provenance: crate::core::ops::UserInputProvenance::ExternalUser,
|
||||
// Headless exec does not correlate submissions.
|
||||
|
||||
+7
-10
@@ -4200,9 +4200,8 @@ fn run_setup(
|
||||
println!(" Next: run `/plugin validate`, review `example`, then trust and enable it.");
|
||||
}
|
||||
|
||||
let sandbox = crate::sandbox::get_platform_sandbox_with_bwrap_preference(
|
||||
config.prefer_bwrap.unwrap_or(false),
|
||||
);
|
||||
let sandbox =
|
||||
crate::sandbox::get_platform_sandbox_with_bwrap_preference(config.prefers_bwrap());
|
||||
if let Some(kind) = sandbox {
|
||||
println!(" ✓ Sandbox available: {kind}");
|
||||
} else {
|
||||
@@ -4592,9 +4591,8 @@ fn run_setup_status(
|
||||
crate::utils::display_path(&plugins_dir)
|
||||
);
|
||||
|
||||
let sandbox = crate::sandbox::get_platform_sandbox_with_bwrap_preference(
|
||||
config.prefer_bwrap.unwrap_or(false),
|
||||
);
|
||||
let sandbox =
|
||||
crate::sandbox::get_platform_sandbox_with_bwrap_preference(config.prefers_bwrap());
|
||||
match sandbox {
|
||||
Some(kind) => println!(
|
||||
" {} sandbox: {kind}",
|
||||
@@ -5884,9 +5882,8 @@ async fn run_doctor(
|
||||
println!(" OS: {}", std::env::consts::OS);
|
||||
println!(" Arch: {}", std::env::consts::ARCH);
|
||||
|
||||
let sandbox = crate::sandbox::get_platform_sandbox_with_bwrap_preference(
|
||||
config.prefer_bwrap.unwrap_or(false),
|
||||
);
|
||||
let sandbox =
|
||||
crate::sandbox::get_platform_sandbox_with_bwrap_preference(config.prefers_bwrap());
|
||||
if let Some(kind) = sandbox {
|
||||
println!(
|
||||
" {} sandbox available: {}",
|
||||
@@ -8052,7 +8049,7 @@ fn run_doctor_json(
|
||||
},
|
||||
},
|
||||
"sandbox": match crate::sandbox::get_platform_sandbox_with_bwrap_preference(
|
||||
config.prefer_bwrap.unwrap_or(false),
|
||||
config.prefers_bwrap(),
|
||||
) {
|
||||
Some(kind) => json!({"available": true, "kind": kind.to_string()}),
|
||||
None => json!({"available": false, "kind": null}),
|
||||
|
||||
@@ -15206,7 +15206,7 @@ impl RuntimeThreadManager {
|
||||
subagent_heartbeat_timeout: std::time::Duration::from_secs(
|
||||
cfg.subagent_heartbeat_timeout_secs_for_provider(&route_identity),
|
||||
),
|
||||
prefer_bwrap: cfg.prefer_bwrap.unwrap_or(false),
|
||||
prefer_bwrap: cfg.prefers_bwrap(),
|
||||
bwrap_extensions: crate::sandbox::BwrapMountExtensions {
|
||||
read_only_roots: cfg.bwrap_ro_roots.clone(),
|
||||
device_roots: cfg.bwrap_dev_roots.clone(),
|
||||
|
||||
@@ -6,9 +6,11 @@
|
||||
//!
|
||||
//! # How it works
|
||||
//!
|
||||
//! When `/usr/bin/bwrap` is executable AND the top-level config key
|
||||
//! `prefer_bwrap` is set to `true`, exec_shell commands are routed through
|
||||
//! bwrap. The bwrap invocation looks like:
|
||||
//! When `/usr/bin/bwrap` actually works — it is executable AND it can create
|
||||
//! its namespaces on this host — sandboxed exec_shell commands are routed
|
||||
//! through it by default. `prefer_bwrap = false` in the config opts out and
|
||||
//! leaves Linux commands unwrapped; the posture then reports policy-only.
|
||||
//! The bwrap invocation looks like:
|
||||
//!
|
||||
//! ```text
|
||||
//! bwrap \
|
||||
@@ -43,9 +45,10 @@
|
||||
//! - Fedora: `dnf install bubblewrap`
|
||||
//! - Arch: `pacman -S bubblewrap`
|
||||
//!
|
||||
//! If bwrap is not executable, Codewhale reports no Linux OS sandbox and runs
|
||||
//! the command without an OS wrapper. It never labels that fallback as
|
||||
//! sandboxed.
|
||||
//! If bwrap is missing or cannot run (e.g. user namespaces are restricted,
|
||||
//! as on Ubuntu 24.04 with `kernel.apparmor_restrict_unprivileged_userns`),
|
||||
//! Codewhale reports no Linux OS sandbox and runs the command without an OS
|
||||
//! wrapper. It never labels that fallback as sandboxed.
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
use super::policy::WritableRoot;
|
||||
@@ -66,10 +69,61 @@ pub(crate) fn existing_directory_shim(path: &Path) -> Option<PathBuf> {
|
||||
#[cfg(target_os = "linux")]
|
||||
pub const BWRAP_PATH: &str = "/usr/bin/bwrap";
|
||||
|
||||
/// Check if bubblewrap is installed and executable.
|
||||
/// How long the availability probe may take. A working bwrap runs a wrapped
|
||||
/// `/bin/true` in well under a second, even on a loaded host.
|
||||
#[cfg(target_os = "linux")]
|
||||
const PROBE_DEADLINE: std::time::Duration = std::time::Duration::from_secs(10);
|
||||
|
||||
/// Whether bwrap is executable AND can actually confine a child here.
|
||||
///
|
||||
/// The exec bit alone lies on hosts where user namespaces are restricted:
|
||||
/// bwrap starts but cannot create the sandbox, so every wrapped command
|
||||
/// would fail instead of running unsandboxed. The check therefore runs a
|
||||
/// real wrapped `/bin/true` once, built by [`build_bwrap_command`] itself so
|
||||
/// the probe covers the same argument shape the real path uses, and caches
|
||||
/// the verdict for the process.
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn is_available() -> bool {
|
||||
is_executable(std::path::Path::new(BWRAP_PATH))
|
||||
fn probe() -> bool {
|
||||
use wait_timeout::ChildExt as _;
|
||||
if !is_executable(std::path::Path::new(BWRAP_PATH)) {
|
||||
return false;
|
||||
}
|
||||
let command = build_bwrap_command(
|
||||
std::path::Path::new("/"),
|
||||
"/bin/true",
|
||||
&[],
|
||||
&[],
|
||||
false,
|
||||
&crate::sandbox::BwrapMountExtensions::default(),
|
||||
&[],
|
||||
&[],
|
||||
);
|
||||
let Some((program, args)) = command.split_first() else {
|
||||
return false;
|
||||
};
|
||||
let mut child = match std::process::Command::new(program)
|
||||
.args(args)
|
||||
.env_clear()
|
||||
.stdin(std::process::Stdio::null())
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.spawn()
|
||||
{
|
||||
Ok(child) => child,
|
||||
Err(_) => return false,
|
||||
};
|
||||
match child.wait_timeout(PROBE_DEADLINE) {
|
||||
Ok(Some(status)) => status.success(),
|
||||
_ => {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
static VERDICT: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
|
||||
*VERDICT.get_or_init(probe)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
|
||||
@@ -9,10 +9,11 @@
|
||||
//! # Platform Support
|
||||
//!
|
||||
//! - **macOS**: Uses Seatbelt (`sandbox-exec`) when the runtime probe succeeds
|
||||
//! - **Linux**: Uses bubblewrap only when the user opts in and `/usr/bin/bwrap`
|
||||
//! is executable. The seccomp helper is not wired into child execution and
|
||||
//! therefore is not advertised. The extension host is the exception to the
|
||||
//! opt-in: it uses bubblewrap whenever a probe shows it works
|
||||
//! - **Linux**: Uses bubblewrap by default whenever `/usr/bin/bwrap` is
|
||||
//! installed and a probe shows it can create its namespaces; `prefer_bwrap
|
||||
//! = false` opts out. The seccomp helper is not wired into child execution
|
||||
//! and therefore is not advertised. The extension host uses bubblewrap on
|
||||
//! the same condition, probed per launch
|
||||
//! (`extension_host::supervisor::plan_launch`).
|
||||
//! - **OpenHarmony**: No local Linux sandbox is advertised. Bubblewrap,
|
||||
//! seccomp, and Linux `prctl` hardening are gated out under `target_env =
|
||||
@@ -67,7 +68,7 @@ pub use policy::SandboxPolicy;
|
||||
// renaming it silently breaks that gate.
|
||||
pub const PUBLIC_SANDBOX_BACKENDS: &[&str] = &[
|
||||
"seatbelt (macOS, when available)",
|
||||
"bubblewrap (Linux, opt-in when installed)",
|
||||
"bubblewrap (Linux, default when installed and working)",
|
||||
];
|
||||
|
||||
/// Specification for a command to be executed, potentially within a sandbox.
|
||||
@@ -344,8 +345,11 @@ pub fn get_platform_sandbox() -> Option<SandboxType> {
|
||||
|
||||
/// Detect the sandbox wrapper the configured command path can actually use.
|
||||
///
|
||||
/// Linux bubblewrap is deliberately opt-in. Source-only sandbox prototypes do
|
||||
/// not make commands sandboxed unless the child launch path applies them.
|
||||
/// Linux bubblewrap is on by default via `Config::prefers_bwrap` (an
|
||||
/// explicit `prefer_bwrap = false` opts out) and only selected when a real
|
||||
/// wrapped probe run proves it works on this host. Source-only sandbox
|
||||
/// prototypes do not make commands sandboxed unless the child launch path
|
||||
/// applies them.
|
||||
pub fn get_platform_sandbox_with_bwrap_preference(prefer_bwrap: bool) -> Option<SandboxType> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
@@ -1309,6 +1313,71 @@ mod tests {
|
||||
let _ = env;
|
||||
}
|
||||
|
||||
/// Real Linux enforcement proof, not a marker check: the same
|
||||
/// outside-workspace write succeeds unsandboxed and fails under
|
||||
/// bubblewrap, and a normal workspace write succeeds inside the wrapper.
|
||||
/// Skips on hosts where the functional probe says bwrap cannot run.
|
||||
#[test]
|
||||
#[cfg(all(target_os = "linux", not(target_env = "ohos")))]
|
||||
fn bwrap_workspace_write_blocks_outside_write_allows_inside() {
|
||||
if !bwrap::is_available() {
|
||||
return;
|
||||
}
|
||||
let workspace = tempfile::tempdir().expect("workspace tempdir");
|
||||
let home = std::env::var_os("HOME").map(PathBuf::from).unwrap();
|
||||
let outside = tempfile::Builder::new()
|
||||
.prefix("cw_bwrap_outside")
|
||||
.tempdir_in(&home)
|
||||
.expect("outside-workspace tempdir under HOME");
|
||||
let outside_target = outside.path().join("must_not_persist");
|
||||
let inside_target = workspace.path().join("allowed_write");
|
||||
|
||||
let run = |manager: &SandboxManager, command: &str| -> (SandboxType, bool) {
|
||||
let spec = CommandSpec::shell(
|
||||
command,
|
||||
workspace.path().to_path_buf(),
|
||||
Duration::from_secs(15),
|
||||
)
|
||||
.with_policy(SandboxPolicy::default());
|
||||
let env = manager.prepare(&spec);
|
||||
let (program, args) = env.command.split_first().unwrap();
|
||||
let status = std::process::Command::new(program)
|
||||
.args(args)
|
||||
.current_dir(&env.cwd)
|
||||
.envs(&env.env)
|
||||
.status()
|
||||
.expect("spawn prepared command");
|
||||
(env.sandbox_type, status.success())
|
||||
};
|
||||
|
||||
// Control: without the wrapper the outside write lands on the host —
|
||||
// this proves the command itself is permitted and only the sandbox
|
||||
// confines it.
|
||||
let unwrapped = SandboxManager::default();
|
||||
let (kind, ok) = run(
|
||||
&unwrapped,
|
||||
&format!("echo x > {}", outside_target.display()),
|
||||
);
|
||||
assert_eq!(kind, SandboxType::None);
|
||||
assert!(ok);
|
||||
assert!(outside_target.exists());
|
||||
std::fs::remove_file(&outside_target).unwrap();
|
||||
|
||||
let wrapped = SandboxManager::with_bwrap_preference(true);
|
||||
let (kind, ok) = run(&wrapped, &format!("echo x > {}", outside_target.display()));
|
||||
assert_eq!(kind, SandboxType::LinuxBubblewrap);
|
||||
assert!(!ok);
|
||||
assert!(!outside_target.exists());
|
||||
|
||||
let (kind, ok) = run(&wrapped, &format!("echo ok > {}", inside_target.display()));
|
||||
assert_eq!(kind, SandboxType::LinuxBubblewrap);
|
||||
assert!(ok);
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(&inside_target).unwrap().trim(),
|
||||
"ok"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(all(target_os = "linux", not(target_env = "ohos")))]
|
||||
fn bwrap_read_only_policy_keeps_the_working_directory_read_only() {
|
||||
|
||||
@@ -1780,8 +1780,9 @@ pub struct App {
|
||||
pub configured_sandbox_network: Option<bool>,
|
||||
/// The sandbox backend this platform+config can actually enforce with,
|
||||
/// resolved once at startup. `None` means there is NO enforcement
|
||||
/// available (default Linux without `prefer_bwrap`, and all Windows), so
|
||||
/// surfaces must not claim the session is sandboxed (2026-08-04 audit).
|
||||
/// available (Linux with `prefer_bwrap = false` or no working bwrap, and
|
||||
/// all Windows), so surfaces must not claim the session is sandboxed
|
||||
/// (2026-08-04 audit).
|
||||
pub sandbox_backend: Option<crate::sandbox::SandboxType>,
|
||||
/// Off-event-loop worker for durable Lane control writes. `/lane interrupt`
|
||||
/// submits here instead of tearing down a Runtime on the composer thread
|
||||
|
||||
@@ -874,7 +874,7 @@ impl App {
|
||||
configured_sandbox_mode: config.sandbox_mode.clone(),
|
||||
configured_sandbox_network: config.sandbox_network_access,
|
||||
sandbox_backend: crate::sandbox::get_platform_sandbox_with_bwrap_preference(
|
||||
config.prefer_bwrap.unwrap_or(false),
|
||||
config.prefers_bwrap(),
|
||||
),
|
||||
// #4022: the worker thread is spawned lazily on first submit, so
|
||||
// constructing an App never costs a thread.
|
||||
|
||||
@@ -1039,7 +1039,7 @@ pub(crate) fn build_engine_config(app: &App, config: &Config) -> EngineConfig {
|
||||
|| config.subagent_heartbeat_timeout_secs(),
|
||||
|identity| config.subagent_heartbeat_timeout_secs_for_provider(identity),
|
||||
)),
|
||||
prefer_bwrap: config.prefer_bwrap.unwrap_or(false),
|
||||
prefer_bwrap: config.prefers_bwrap(),
|
||||
bwrap_extensions: crate::sandbox::BwrapMountExtensions {
|
||||
read_only_roots: config.bwrap_ro_roots.clone(),
|
||||
device_roots: config.bwrap_dev_roots.clone(),
|
||||
|
||||
@@ -2190,9 +2190,9 @@ reasoning contract, and all four membership ids omit generic sampling fields.
|
||||
```
|
||||
- `sandbox_mode` (string, optional): `read-only`, `workspace-write`, `danger-full-access`, `external-sandbox`.
|
||||
Platform support is not identical. macOS uses Seatbelt when its runtime
|
||||
probe succeeds. Linux uses bubblewrap only when `prefer_bwrap = true` and
|
||||
`/usr/bin/bwrap` is executable; without that opt-in it reports no OS command
|
||||
sandbox. Windows does not currently advertise an OS sandbox; its planned helper contract starts
|
||||
probe succeeds. Linux uses bubblewrap by default whenever `/usr/bin/bwrap`
|
||||
is installed and a probe proves it can confine a child; `prefer_bwrap =
|
||||
false` opts out and reports no OS command sandbox. Windows does not currently advertise an OS sandbox; its planned helper contract starts
|
||||
with process-tree containment only and must not be described as read-only
|
||||
filesystem isolation, workspace-write enforcement, network blocking,
|
||||
registry isolation, or AppContainer isolation until those are implemented.
|
||||
|
||||
+21
-14
@@ -16,8 +16,8 @@ run before execution reaches this boundary.
|
||||
| Mechanism | Platform | Selection | What Codewhale reports |
|
||||
|---|---|---|---|
|
||||
| Seatbelt (`sandbox-exec`) | macOS | Automatic when the runtime probe succeeds | `macos-seatbelt` |
|
||||
| Bubblewrap (`/usr/bin/bwrap`) | Linux | `prefer_bwrap = true` and the file is executable | `linux-bwrap` |
|
||||
| No OS wrapper | Linux without usable opt-in bwrap | Default | `none` |
|
||||
| Bubblewrap (`/usr/bin/bwrap`) | Linux | Default when installed and a wrapped probe run succeeds; `prefer_bwrap = false` opts out | `linux-bwrap` |
|
||||
| No OS wrapper | Linux without usable bwrap, or opted out | `prefer_bwrap = false`, or bwrap absent/unusable | `none` |
|
||||
| No OS wrapper | Windows | Current implementation | `none` |
|
||||
| OpenSandbox-compatible service | Any supported host | `sandbox_backend = "opensandbox"` | External execution path |
|
||||
|
||||
@@ -43,16 +43,21 @@ If the probe fails or `sandbox-exec` is unavailable, Codewhale reports no OS
|
||||
sandbox and launches the command without a Seatbelt wrapper. It does not set a
|
||||
Seatbelt marker on that fallback.
|
||||
|
||||
## Linux: opt-in bubblewrap
|
||||
## Linux: default bubblewrap
|
||||
|
||||
Linux command sandboxing is opt-in. Set the top-level configuration key:
|
||||
Linux command sandboxing is on by default whenever bubblewrap works. Set the
|
||||
top-level configuration key to opt out:
|
||||
|
||||
```toml
|
||||
prefer_bwrap = true
|
||||
prefer_bwrap = false
|
||||
```
|
||||
|
||||
Codewhale selects bubblewrap only when `/usr/bin/bwrap` is a regular executable
|
||||
file. The wrapper derives its mounts and network namespace from the resolved
|
||||
Codewhale selects bubblewrap only when `/usr/bin/bwrap` is a regular
|
||||
executable file AND a wrapped probe run proves it can create its namespaces
|
||||
on this host — the exec bit alone lies where user namespaces are restricted
|
||||
(e.g. Ubuntu 24.04 with `kernel.apparmor_restrict_unprivileged_userns`),
|
||||
where every wrapped command would fail rather than run unsandboxed. The
|
||||
wrapper derives its mounts and network namespace from the resolved
|
||||
`SandboxPolicy`:
|
||||
|
||||
```text
|
||||
@@ -91,11 +96,12 @@ by default. Codewhale adds `--share-net` only when the policy's
|
||||
`network_access` is true. `danger-full-access` and `external-sandbox` bypass the
|
||||
local wrapper entirely.
|
||||
|
||||
If the user does not opt in, or `/usr/bin/bwrap` is missing or non-executable,
|
||||
Codewhale reports `none` and launches the command without a Linux OS wrapper.
|
||||
There is no marker-only fallback to a different Linux sandbox.
|
||||
If the user opts out, or `/usr/bin/bwrap` is missing, non-executable, or
|
||||
cannot actually confine a child, Codewhale reports `none` and launches the
|
||||
command without a Linux OS wrapper. There is no marker-only fallback to a
|
||||
different Linux sandbox.
|
||||
|
||||
Install bubblewrap separately when this opt-in fits the workflow:
|
||||
Install bubblewrap separately to get enforcement on Linux:
|
||||
|
||||
- Ubuntu/Debian: `apt install bubblewrap`
|
||||
- Fedora: `dnf install bubblewrap`
|
||||
@@ -180,8 +186,9 @@ backend:
|
||||
- `CODEWHALE_SANDBOX_URL`
|
||||
- `CODEWHALE_SANDBOX_API_KEY`
|
||||
|
||||
There is no `CODEWHALE_PREFER_BWRAP` environment override; use the top-level
|
||||
`prefer_bwrap` config key.
|
||||
`CODEWHALE_PREFER_BWRAP` (legacy alias `DEEPSEEK_PREFER_BWRAP`) overrides the
|
||||
preference explicitly; the top-level `prefer_bwrap` config key is the durable
|
||||
setting.
|
||||
|
||||
## Diagnostics and failure attribution
|
||||
|
||||
@@ -219,7 +226,7 @@ Denial attribution is intentionally conservative:
|
||||
|
||||
- `crates/tui/src/sandbox/mod.rs` — truthful selection and public capability markers
|
||||
- `crates/tui/src/sandbox/seatbelt.rs` — macOS wrapper and availability probe
|
||||
- `crates/tui/src/sandbox/bwrap.rs` — Linux opt-in wrapper
|
||||
- `crates/tui/src/sandbox/bwrap.rs` — Linux wrapper and functional availability probe
|
||||
- `crates/tui/src/sandbox/process_hardening.rs` — Linux parent-process hardening
|
||||
- `crates/tui/src/sandbox/backend.rs` — external backend selection
|
||||
- `crates/tui/src/tools/diagnostics.rs` — machine-readable diagnostics
|
||||
+1
-1
@@ -277,7 +277,7 @@ owner = "crates/execpolicy/src/approval_mode.rs"
|
||||
[[feature]]
|
||||
id = "sandbox"
|
||||
name = "Command sandbox"
|
||||
summary = "Wraps commands in Seatbelt on macOS; bubblewrap on Linux is opt-in."
|
||||
summary = "Wraps commands in Seatbelt on macOS; bubblewrap on Linux is on by default once installed and verified."
|
||||
status = "stable"
|
||||
surfaces = { tui = "stable" }
|
||||
docs = "docs/SANDBOX.md"
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"toolCount": 80,
|
||||
"sandboxBackends": [
|
||||
"seatbelt (macOS, when available)",
|
||||
"bubblewrap (Linux, opt-in when installed)"
|
||||
"bubblewrap (Linux, default when installed and working)"
|
||||
],
|
||||
"sources": [
|
||||
"Cargo.toml",
|
||||
@@ -185,7 +185,7 @@
|
||||
"telemetry": "Codewhale 0.10.1 counts anonymous usage by default and discloses it at first launch (policy notice version 5, naming Codewhale and PostHog as processors), while the earlier 0.9.11 release asked first; turning it off is a saved choice that later versions keep, an opt-out recorded under the earlier opt-in policy stays off, and showing the notice never records any acceptance on the user's behalf; Codewhale does not collect conversations, code, prompts, files, file/repo/branch names, model content, credentials, or per-turn/per-tool timelines; while on, sessions post aggregate counts and closed enums to the first-party endpoint https://telemetry.codewhale.net/v1/telemetry, whose storage has no IP, country, or geo column and a fixed three-month retention; optional PostHog forwarding requires separate operator configuration and verified IP-safe egress, and its retention is a separate project setting; telemetry_endpoint = \"\" writes only to a local dry-run file; no mandatory hosted relay",
|
||||
"account": "no account required for the local runtime",
|
||||
"plan": "Plan centrally refuses file mutation and shell execution; policy-allowed research may contact external services and local session state can still be persisted.",
|
||||
"sandbox": "Seatbelt is used on macOS when available; Linux bubblewrap is opt-in and must be installed; Windows currently reports no OS sandbox",
|
||||
"sandbox": "Seatbelt is used on macOS when available; Linux bubblewrap is on by default once installed and verified working, and prefer_bwrap = false opts out; Windows currently reports no OS sandbox",
|
||||
"audit": "sensitive events append best-effort to $CODEWHALE_HOME/audit.log (default ~/.codewhale/audit.log); write failures are logged",
|
||||
"usage": "provider token and cache usage is shown locally when available",
|
||||
"sources": [
|
||||
|
||||
@@ -1194,7 +1194,7 @@ DeepSeek V4 前缀缓存让 token 标签变得重要。这些数量保持分离
|
||||
timeout_seconds = 300
|
||||
```
|
||||
|
||||
- `sandbox_mode`(字符串,可选):`read-only`、`workspace-write`、`danger-full-access`、`external-sandbox`。各平台的支持并不相同。macOS 在其运行时探测成功时使用 Seatbelt。Linux 只在 `prefer_bwrap = true` 且 `/usr/bin/bwrap` 可执行时使用 bubblewrap;没有这一选择加入时,它会报告没有 OS 命令沙箱。Windows 目前不宣称有 OS 沙箱;其规划中的辅助程序契约只从进程树隔离开始,在只读文件系统隔离、workspace-write 强制、网络阻断、注册表隔离或 AppContainer 隔离真正实现之前,不得被描述成这些能力。
|
||||
- `sandbox_mode`(字符串,可选):`read-only`、`workspace-write`、`danger-full-access`、`external-sandbox`。各平台的支持并不相同。macOS 在其运行时探测成功时使用 Seatbelt。Linux 默认在 `/usr/bin/bwrap` 已安装且探测证明它能限制子进程时使用 bubblewrap;`prefer_bwrap = false` 退出并报告没有 OS 命令沙箱。Windows 目前不宣称有 OS 沙箱;其规划中的辅助程序契约只从进程树隔离开始,在只读文件系统隔离、workspace-write 强制、网络阻断、注册表隔离或 AppContainer 隔离真正实现之前,不得被描述成这些能力。
|
||||
- 模式准入、hooks、已注册工具的要求、类型化规则、Auto-Review、仓库保护规则、人工审批和执行沙箱之间的跨层关系,定义在[授权顺序](../AUTHORIZATION_ORDER.md)中。
|
||||
- **读取拒绝列表。** 每一种沙箱档位——包括 `read-only`——都授予对整个文件系统的读取权限;这些档位的区别在于它们可以*写入*什么,以及能否访问网络。读取拒绝列表会收窄这一点:
|
||||
- `sandbox_read_denylist_defaults`(bool,默认 `true`):应用内置的凭据存储集合——`~/.ssh`、`~/.gnupg`、云凭据目录(`~/.aws`、`~/.config/gcloud`、`~/.azure`、`~/.kube` 等)、`~/.netrc`、`~/.npmrc`、`~/.git-credentials`、macOS 钥匙串、浏览器配置文件、Codewhale 自己的机密存储,以及 `.env` 文件(但不包括 `.env.example` 之类)。普通源码、`Cargo.toml`、`~/.gitconfig`、`~/.cargo` 和 `~/.npm` 保持可读,因此构建和测试仍能工作。设为 `false` 可恢复 0.9.12 之前的整盘读取行为。
|
||||
|
||||
+15
-11
@@ -15,8 +15,8 @@ Codewhale 可以执行由模型提出的 shell 命令。审批策略、感知工
|
||||
| 机制 | 平台 | 选择方式 | Codewhale 报告的结果 |
|
||||
|---|---|---|---|
|
||||
| Seatbelt(`sandbox-exec`) | macOS | 运行时探测成功时自动启用 | `macos-seatbelt` |
|
||||
| Bubblewrap(`/usr/bin/bwrap`) | Linux | `prefer_bwrap = true` 且该文件可执行 | `linux-bwrap` |
|
||||
| 无操作系统包装器 | Linux,没有可用且已启用的 bwrap | 默认 | `none` |
|
||||
| Bubblewrap(`/usr/bin/bwrap`) | Linux | 默认启用,前提是已安装且探测包装运行成功;`prefer_bwrap = false` 可退出 | `linux-bwrap` |
|
||||
| 无操作系统包装器 | Linux,bwrap 不可用或已退出 | `prefer_bwrap = false`,或 bwrap 不存在/不可用 | `none` |
|
||||
| 无操作系统包装器 | Windows | 当前实现 | `none` |
|
||||
| 兼容 OpenSandbox 的服务 | 任何受支持的主机 | `sandbox_backend = "opensandbox"` | 外部执行路径 |
|
||||
|
||||
@@ -39,15 +39,18 @@ Seatbelt profile。
|
||||
探测失败,或 `sandbox-exec` 不可用时,Codewhale 会报告未启用操作系统沙箱,
|
||||
直接启动命令,不套 Seatbelt 包装器。这条回退路径上也不会打任何 Seatbelt 标记。
|
||||
|
||||
## Linux:需要主动启用的 bubblewrap
|
||||
## Linux:默认启用的 bubblewrap
|
||||
|
||||
Linux 下的命令沙箱需要主动启用。设置顶层配置项:
|
||||
只要 bubblewrap 可用,Linux 下的命令沙箱默认启用。退出使用顶层配置项:
|
||||
|
||||
```toml
|
||||
prefer_bwrap = true
|
||||
prefer_bwrap = false
|
||||
```
|
||||
|
||||
只有当 `/usr/bin/bwrap` 是普通的可执行文件时,Codewhale 才会选用 bubblewrap。
|
||||
只有当 `/usr/bin/bwrap` 是普通的可执行文件,且一次实际的包装探测运行证明
|
||||
它能在这台主机上创建命名空间时,Codewhale 才会选用 bubblewrap —— 仅有可执行
|
||||
位会骗人:在限制用户命名空间的主机上(例如开启了 `kernel.apparmor_restrict_unprivileged_userns`
|
||||
的 Ubuntu 24.04),每条被包装的命令都会失败,而不是以未沙箱化方式运行。
|
||||
包装器根据解析后的 `SandboxPolicy` 推导自己的挂载点和网络命名空间:
|
||||
|
||||
```text
|
||||
@@ -84,11 +87,11 @@ prefer_bwrap = true
|
||||
时,Codewhale 才补上 `--share-net`。`danger-full-access` 和 `external-sandbox`
|
||||
完全绕过本地包装器。
|
||||
|
||||
如果用户没有主动启用,或者 `/usr/bin/bwrap` 不存在、不可执行,Codewhale 会
|
||||
报告 `none`,直接启动命令,不带任何 Linux 操作系统包装器。这里没有回退做法:
|
||||
不会只打个标记,就把它当成另一种 Linux 沙箱。
|
||||
如果用户选择退出,或者 `/usr/bin/bwrap` 不存在、不可执行、或实际上无法
|
||||
限制子进程,Codewhale 会报告 `none`,直接启动命令,不带任何 Linux 操作
|
||||
系统包装器。这里没有回退做法:不会只打个标记,就把它当成另一种 Linux 沙箱。
|
||||
|
||||
如果这套主动启用的方案适合你的工作流,请另行安装 bubblewrap:
|
||||
在 Linux 上获得强制力,请另行安装 bubblewrap:
|
||||
|
||||
- Ubuntu/Debian:`apt install bubblewrap`
|
||||
- Fedora:`dnf install bubblewrap`
|
||||
@@ -165,7 +168,8 @@ sandbox_mode = "workspace-write" # read-only | workspace-write | danger-full-acc
|
||||
- `CODEWHALE_SANDBOX_URL`
|
||||
- `CODEWHALE_SANDBOX_API_KEY`
|
||||
|
||||
不存在 `CODEWHALE_PREFER_BWRAP` 环境变量覆盖;请使用顶层的 `prefer_bwrap` 配置项。
|
||||
`CODEWHALE_PREFER_BWRAP`(旧别名 `DEEPSEEK_PREFER_BWRAP`)可显式覆盖此偏好;
|
||||
顶层 `prefer_bwrap` 配置项是持久设置。
|
||||
|
||||
## 诊断与失败归因
|
||||
|
||||
|
||||
@@ -257,7 +257,7 @@ codewhale --provider openrouter --model deepseek/deepseek-v4-pro
|
||||
provider you select receives the prompt, project context, tool definitions,
|
||||
and tool results required for that turn. Use a loopback local-model route to
|
||||
keep model inference local.
|
||||
OS command sandboxing is platform-specific: Codewhale uses <strong>Seatbelt</strong> on macOS when available. On Linux it uses <strong>bubblewrap</strong> only when <code className="inline">prefer_bwrap = true</code> and <code className="inline">/usr/bin/bwrap</code> is executable; otherwise commands have no Codewhale OS wrapper. Windows currently reports no OS sandbox.
|
||||
OS command sandboxing is platform-specific: Codewhale uses <strong>Seatbelt</strong> on macOS when available. On Linux it uses <strong>bubblewrap</strong> by default whenever <code className="inline">/usr/bin/bwrap</code> is installed and a probe shows it works; <code className="inline">prefer_bwrap = false</code> opts out. Otherwise commands have no Codewhale OS wrapper. Windows currently reports no OS sandbox.
|
||||
Workspace boundaries default to <code className="inline">--workspace</code>. <code className="inline">/trust</code> lifts them.
|
||||
Permission posture is configurable per session.
|
||||
</>
|
||||
@@ -618,7 +618,7 @@ codewhale --provider openrouter --model deepseek/deepseek-v4-pro
|
||||
可用 <code className="inline">codewhale config set telemetry false</code> 或
|
||||
<code className="inline">CODEWHALE_TELEMETRY=0</code> 关闭)。也不要求经过托管中继。你选择的托管 provider 会收到本轮所需的
|
||||
prompt、项目上下文、工具定义与工具结果。若要让模型推理也保持本地,请使用回环地址上的本地模型路由。
|
||||
OS 命令沙箱因平台而异:macOS 在可用时使用 <strong>Seatbelt</strong>。Linux 仅在 <code className="inline">prefer_bwrap = true</code> 且 <code className="inline">/usr/bin/bwrap</code> 可执行时使用 <strong>bubblewrap</strong>;否则命令没有 Codewhale OS 包装器。Windows 当前报告无 OS 沙箱。
|
||||
OS 命令沙箱因平台而异:macOS 在可用时使用 <strong>Seatbelt</strong>。Linux 默认在 <code className="inline">/usr/bin/bwrap</code> 已安装且探测可用时使用 <strong>bubblewrap</strong>;<code className="inline">prefer_bwrap = false</code> 退出。否则命令没有 Codewhale OS 包装器。Windows 当前报告无 OS 沙箱。
|
||||
工作区边界默认为 <code className="inline">--workspace</code>。<code className="inline">/trust</code> 可解除边界。
|
||||
权限姿态可按会话配置。
|
||||
</>
|
||||
|
||||
@@ -1206,7 +1206,7 @@
|
||||
],
|
||||
[
|
||||
"Linux",
|
||||
"Bubblewrap, but only if you turn it on (below). Without it, commands run with no OS sandbox."
|
||||
"Bubblewrap, automatically, once it is installed and verified working (opt out below). Without it, commands run with no OS sandbox."
|
||||
],
|
||||
[
|
||||
"Windows",
|
||||
@@ -1232,17 +1232,17 @@
|
||||
},
|
||||
{
|
||||
"id": "linux",
|
||||
"title": "Turn on the Linux sandbox",
|
||||
"title": "Enable (or turn off) the Linux sandbox",
|
||||
"blocks": [
|
||||
{
|
||||
"p": "Install bubblewrap, then opt in with one line in `~/.codewhale/config.toml`:"
|
||||
"p": "Install bubblewrap — Codewhale uses it by default. To run Linux commands unwrapped instead, opt out with one line in `~/.codewhale/config.toml`:"
|
||||
},
|
||||
{
|
||||
"code": "sudo apt install bubblewrap # Fedora: dnf install bubblewrap · Arch: pacman -S bubblewrap\n\n# ~/.codewhale/config.toml\nprefer_bwrap = true",
|
||||
"code": "sudo apt install bubblewrap # Fedora: dnf install bubblewrap · Arch: pacman -S bubblewrap\n\n# ~/.codewhale/config.toml — optional opt-out\nprefer_bwrap = false",
|
||||
"lang": "Terminal / config.toml"
|
||||
},
|
||||
{
|
||||
"p": "Codewhale uses `/usr/bin/bwrap` only when that file exists and is executable. Commands then see a read-only view of the system, write only where the sandbox mode allows, and have no network unless the mode enables it."
|
||||
"p": "Codewhale uses `/usr/bin/bwrap` only when that file exists, is executable, and a probe shows it can create its sandbox namespaces — an installed-but-blocked bwrap is treated as absent, not used. Commands then see a read-only view of the system, write only where the sandbox mode allows, and have no network unless the mode enables it."
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -1206,7 +1206,7 @@
|
||||
],
|
||||
[
|
||||
"Linux",
|
||||
"bubblewrap,但需要你手动开启(见下文)。不开启时,命令在没有操作系统沙箱的情况下运行。"
|
||||
"bubblewrap,安装并验证可用后自动启用(退出见下文)。不可用时,命令在没有操作系统沙箱的情况下运行。"
|
||||
],
|
||||
[
|
||||
"Windows",
|
||||
@@ -1232,17 +1232,17 @@
|
||||
},
|
||||
{
|
||||
"id": "linux",
|
||||
"title": "开启 Linux 沙箱",
|
||||
"title": "启用(或关闭)Linux 沙箱",
|
||||
"blocks": [
|
||||
{
|
||||
"p": "先安装 bubblewrap,再在 `~/.codewhale/config.toml` 中加一行来启用:"
|
||||
"p": "安装 bubblewrap 后 Codewhale 默认使用它。想让 Linux 命令不被包装,在 `~/.codewhale/config.toml` 中加一行退出:"
|
||||
},
|
||||
{
|
||||
"code": "sudo apt install bubblewrap # Fedora: dnf install bubblewrap · Arch: pacman -S bubblewrap\n\n# ~/.codewhale/config.toml\nprefer_bwrap = true",
|
||||
"code": "sudo apt install bubblewrap # Fedora: dnf install bubblewrap · Arch: pacman -S bubblewrap\n\n# ~/.codewhale/config.toml — 可选退出\nprefer_bwrap = false",
|
||||
"lang": "终端 / config.toml"
|
||||
},
|
||||
{
|
||||
"p": "只有当 `/usr/bin/bwrap` 存在且可执行时,Codewhale 才会使用它。此后,命令看到的是只读的系统视图,只能写入沙箱模式允许的位置,除非模式允许,否则无法联网。"
|
||||
"p": "只有当 `/usr/bin/bwrap` 存在、可执行,且探测证明它能创建沙箱命名空间时,Codewhale 才会使用它——装了但被系统拦住的 bwrap 按不存在处理。此后,命令看到的是只读的系统视图,只能写入沙箱模式允许的位置,除非模式允许,否则无法联网。"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -37,7 +37,7 @@ export interface RepoFacts {
|
||||
}
|
||||
|
||||
export const FACTS: RepoFacts = {
|
||||
"generatedAt": "2026-10-02T18:06:32.682Z",
|
||||
"generatedAt": "2026-10-06T00:44:34.685Z",
|
||||
"sourceRevision": null,
|
||||
"sourceCommittedAt": null,
|
||||
"version": "0.10.1",
|
||||
@@ -73,7 +73,7 @@ export const FACTS: RepoFacts = {
|
||||
],
|
||||
"sandboxBackends": [
|
||||
"seatbelt (macOS, when available)",
|
||||
"bubblewrap (Linux, opt-in when installed)"
|
||||
"bubblewrap (Linux, default when installed and working)"
|
||||
],
|
||||
"providers": [
|
||||
{
|
||||
|
||||
@@ -22,7 +22,7 @@ export const docsSandbox: DocsSandboxDict = {
|
||||
{
|
||||
rows: [
|
||||
["macOS", "Seatbelt, automatically, when its startup check succeeds. Commands get broad read access, writes limited by the sandbox mode, and network only when the mode allows it."],
|
||||
["Linux", "Bubblewrap, but only if you turn it on (below). Without it, commands run with no OS sandbox."],
|
||||
["Linux", "Bubblewrap, automatically, once it is installed and verified working (opt out below). Without it, commands run with no OS sandbox."],
|
||||
["Windows", "No OS sandbox today. Your approval setting and Windows permissions still apply."],
|
||||
["External service", "With `sandbox_backend = \"opensandbox\"`, shell commands run on an OpenSandbox-compatible service you configure; its isolation is that service's to guarantee."],
|
||||
],
|
||||
@@ -36,18 +36,18 @@ export const docsSandbox: DocsSandboxDict = {
|
||||
},
|
||||
{
|
||||
id: "linux",
|
||||
title: "Turn on the Linux sandbox",
|
||||
title: "Enable (or turn off) the Linux sandbox",
|
||||
blocks: [
|
||||
{ p: "Install bubblewrap, then opt in with one line in `~/.codewhale/config.toml`:" },
|
||||
{ p: "Install bubblewrap — Codewhale uses it by default. To run Linux commands unwrapped instead, opt out with one line in `~/.codewhale/config.toml`:" },
|
||||
{
|
||||
code: `sudo apt install bubblewrap # Fedora: dnf install bubblewrap · Arch: pacman -S bubblewrap
|
||||
|
||||
# ~/.codewhale/config.toml
|
||||
prefer_bwrap = true`,
|
||||
# ~/.codewhale/config.toml — optional opt-out
|
||||
prefer_bwrap = false`,
|
||||
lang: "Terminal / config.toml",
|
||||
},
|
||||
{
|
||||
p: "Codewhale uses `/usr/bin/bwrap` only when that file exists and is executable. Commands then see a read-only view of the system, write only where the sandbox mode allows, and have no network unless the mode enables it.",
|
||||
p: "Codewhale uses `/usr/bin/bwrap` only when that file exists, is executable, and a probe shows it can create its sandbox namespaces — an installed-but-blocked bwrap is treated as absent, not used. Commands then see a read-only view of the system, write only where the sandbox mode allows, and have no network unless the mode enables it.",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -17,7 +17,7 @@ export const docsSandbox: DocsSandboxDict = {
|
||||
{
|
||||
rows: [
|
||||
["macOS", "Seatbelt,启动检查通过后自动启用。命令可以广泛读取,写入范围由沙箱模式限定,只有模式允许时才能联网。"],
|
||||
["Linux", "bubblewrap,但需要你手动开启(见下文)。不开启时,命令在没有操作系统沙箱的情况下运行。"],
|
||||
["Linux", "bubblewrap,安装并验证可用后自动启用(退出见下文)。不可用时,命令在没有操作系统沙箱的情况下运行。"],
|
||||
["Windows", "目前没有操作系统沙箱。你的审批设置和 Windows 自身的权限仍然有效。"],
|
||||
["外部服务", "设置 `sandbox_backend = \"opensandbox\"` 后,shell 命令会在你配置的 OpenSandbox 兼容服务上运行;隔离效果由该服务负责保证。"],
|
||||
],
|
||||
@@ -31,18 +31,18 @@ export const docsSandbox: DocsSandboxDict = {
|
||||
},
|
||||
{
|
||||
id: "linux",
|
||||
title: "开启 Linux 沙箱",
|
||||
title: "启用(或关闭)Linux 沙箱",
|
||||
blocks: [
|
||||
{ p: "先安装 bubblewrap,再在 `~/.codewhale/config.toml` 中加一行来启用:" },
|
||||
{ p: "安装 bubblewrap 后 Codewhale 默认使用它。想让 Linux 命令不被包装,在 `~/.codewhale/config.toml` 中加一行退出:" },
|
||||
{
|
||||
code: `sudo apt install bubblewrap # Fedora: dnf install bubblewrap · Arch: pacman -S bubblewrap
|
||||
|
||||
# ~/.codewhale/config.toml
|
||||
prefer_bwrap = true`,
|
||||
# ~/.codewhale/config.toml — 可选退出
|
||||
prefer_bwrap = false`,
|
||||
lang: "终端 / config.toml",
|
||||
},
|
||||
{
|
||||
p: "只有当 `/usr/bin/bwrap` 存在且可执行时,Codewhale 才会使用它。此后,命令看到的是只读的系统视图,只能写入沙箱模式允许的位置,除非模式允许,否则无法联网。",
|
||||
p: "只有当 `/usr/bin/bwrap` 存在、可执行,且探测证明它能创建沙箱命名空间时,Codewhale 才会使用它——装了但被系统拦住的 bwrap 按不存在处理。此后,命令看到的是只读的系统视图,只能写入沙箱模式允许的位置,除非模式允许,否则无法联网。",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user