diff --git a/.agents/notes/implemented/bug-fix/2026-08-06-bwrap-private-pid-namespace.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-08-06-bwrap-private-pid-namespace.i18n.yaml new file mode 100644 index 0000000000..70b0c7f01d --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-08-06-bwrap-private-pid-namespace.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-06-bwrap-private-pid-namespace.md +2026-08-06-bwrap-private-pid-namespace.md: c7c83fd274c5dcd6634bdb78a909f1366e1926ba +2026-08-06-bwrap-private-pid-namespace.zh.md: bc3b12c4f6fff31e7dcac630ac390501e8c591b5 diff --git a/.agents/notes/implemented/bug-fix/2026-08-06-bwrap-private-pid-namespace.md b/.agents/notes/implemented/bug-fix/2026-08-06-bwrap-private-pid-namespace.md new file mode 100644 index 0000000000..c7c83fd274 --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-08-06-bwrap-private-pid-namespace.md @@ -0,0 +1,36 @@ +# Agent Note: isolate bwrap from the host PID namespace + +Status: implemented + +English | [中文](2026-08-06-bwrap-private-pid-namespace.zh.md) + +## Problem + +The bwrap backend mounted a fresh `/proc` while retaining the host PID namespace. A confined command could therefore see host processes and follow procfs magic links such as `/proc//root`, `/proc//fd`, or `/proc//cwd` into a host process's mount view. When access controls allowed following one of those links, the path escaped the profile's read-only host-root bind and `workspace-write` allow-list. Host ptrace restrictions sometimes blocked the path, but those deployment-dependent permissions were not a confinement boundary. + +The original [sandbox decision](../feature/2026-07-06-sandbox.md) deliberately left process visibility unchanged because `SandboxMode` promises file effects rather than general process isolation. Procfs magic links make host process visibility part of the file-effect boundary for bwrap, so that choice cannot preserve the promised modes. + +## Decision + +Every bwrap profile uses `--unshare-pid` and mounts `/proc` for that private namespace. The confined command can observe and control its descendants, while host processes and their procfs magic links are absent. Bubblewrap supplies the namespace's PID 1 process to reap descendants. + +The functional bwrap probe uses the same profile builder as real wraps. A host that cannot create the PID namespace therefore rejects bwrap during selection and falls back to Landlock instead of accepting a weaker probe and failing later. + +This is a bwrap backend invariant, not a new `SandboxMode` promise. Landlock and Seatbelt continue to leave process visibility unchanged, and no backend restricts network access. + +## Alternatives considered + +- **Mask selected procfs links while retaining host process visibility.** Per-process entries are dynamic, and covering only `root` would leave equivalent crossings through `fd`, `cwd`, `exe`, and future magic links. A blocklist cannot establish the boundary. +- **Rely on ptrace and procfs ownership checks.** Their behavior depends on kernel settings, container configuration, process credentials, and dumpability. Same-user processes can be reachable, so these checks are defense in depth rather than the profile's authority. +- **Remove `/proc` entirely.** Ordinary process tooling and descendant management expect procfs. A private PID namespace with matching procfs preserves those mechanics without exposing host processes. + +## Verification + +Profile unit tests pin PID unsharing in both confined modes. Real-bwrap tests verify that both modes report a PID-namespace identity different from the harness's, reject a write through `/proc/1/root`, leave the host target absent, and still allow the command to observe, terminate, and wait for its own descendant. + +## Consequences + +- bwrap-confined commands no longer inspect or signal host processes, including same-user processes. +- `read-only` and `workspace-write` no longer depend on host procfs access policy to prevent mount-profile escapes. +- Hosts without usable PID namespaces select the next supported Linux backend through the existing fail-closed ladder. +- The changed guarantee is kernel confinement rather than model-visible output, protocol, or transcript text, so the real-backend e2e is the assembled acceptance path and no snapshot changes. diff --git a/.agents/notes/implemented/bug-fix/2026-08-06-bwrap-private-pid-namespace.zh.md b/.agents/notes/implemented/bug-fix/2026-08-06-bwrap-private-pid-namespace.zh.md new file mode 100644 index 0000000000..bc3b12c4f6 --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-08-06-bwrap-private-pid-namespace.zh.md @@ -0,0 +1,36 @@ +# Agent Note: 将 bwrap 与宿主 PID 命名空间隔离 + +Status: implemented + +[English](2026-08-06-bwrap-private-pid-namespace.md) | 中文 + +## 问题 + +bwrap 后端挂载了全新的 `/proc`,但保留宿主 PID 命名空间。因此,受约束命令可以看到宿主进程,并沿 `/proc//root`、`/proc//fd`、`/proc//cwd` 等 procfs 魔法链接进入宿主进程的挂载视图。当访问控制允许跟随其中某条链接时,该路径便可越过 profile 对宿主根目录的只读绑定挂载,以及 `workspace-write` 的 allow-list。宿主的 ptrace 限制有时会阻断该路径,但这类取决于部署环境的权限并不构成约束边界。 + +最初的[沙箱决策](../feature/2026-07-06-sandbox.md)有意维持进程可见性不变,因为 `SandboxMode` 承诺的是文件影响,而不是一般性的进程隔离。对 bwrap 而言,procfs 魔法链接使宿主进程可见性成为文件影响边界的一部分,因此该选择无法维持这些模式承诺的边界。 + +## 决策 + +每个 bwrap profile 都使用 `--unshare-pid`,并为该私有命名空间挂载 `/proc`。受约束命令可以观察和控制自己的后代进程,但宿主进程及其 procfs 魔法链接不会出现。Bubblewrap 提供该命名空间的 PID 1 进程,用于回收后代进程。 + +bwrap 功能探测与实际包装使用同一个 profile builder。因此,无法创建 PID 命名空间的宿主会在选择阶段拒绝 bwrap 并回退到 Landlock,而不是让较弱的探测通过,随后才失败。 + +这是 bwrap 后端不变式,不是 `SandboxMode` 的新承诺。Landlock 与 Seatbelt 仍保持进程可见性不变,且没有后端限制网络访问。 + +## 曾考虑的替代方案 + +- **在保留宿主进程可见性的同时屏蔽部分 procfs 链接。** 每个进程的条目都会动态变化,只覆盖 `root` 仍会留下可通过 `fd`、`cwd`、`exe` 及未来魔法链接进行的等效越界路径。阻止列表无法建立该边界。 +- **依赖 ptrace 与 procfs 所有权检查。** 其行为取决于内核设置、容器配置、进程凭据,以及进程是否可转储。同一用户的进程可能仍可访问,因此这些检查只属于纵深防御,不能取代由 profile 建立的权威边界。 +- **完全移除 `/proc`。** 常规进程工具和后代进程管理依赖 procfs。私有 PID 命名空间配合对应的 procfs,既能保留这些机制,又不会暴露宿主进程。 + +## 验证 + +profile 单元测试固定两个受约束模式均取消共享 PID 命名空间。真实 bwrap 测试验证:两个模式报告的 PID 命名空间标识都与 harness 不同,拒绝通过 `/proc/1/root` 写入,确保宿主目标文件仍不存在,同时仍允许命令观察、终止并等待自己的后代进程。 + +## 后果 + +- 受 bwrap 约束的命令无法再检查宿主进程或向其发送信号,包括同一用户的进程。 +- `read-only` 与 `workspace-write` 无需再依赖宿主 procfs 访问策略来防止绕过挂载 profile。 +- 无法使用 PID 命名空间的宿主会通过现有的失败关闭阶梯,选择下一个受支持的 Linux 后端。 +- 此次改变的是内核约束保证,不是模型可见输出、协议或 transcript(文本记录)内容;因此,真实后端 e2e 是组装应用的验收路径,无需修改快照。 diff --git a/.agents/notes/implemented/feature/2026-07-06-sandbox.i18n.yaml b/.agents/notes/implemented/feature/2026-07-06-sandbox.i18n.yaml index 3cf7133534..e38566451c 100644 --- a/.agents/notes/implemented/feature/2026-07-06-sandbox.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-06-sandbox.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-06-sandbox.md -2026-07-06-sandbox.md: 62c46c99a2283b03cf75d8823783367dd6b3473a -2026-07-06-sandbox.zh.md: 82c2e7962800c007a207f0204bf47cef01f79a36 +2026-07-06-sandbox.md: 7c451d8fb2d59c20ad8170e8c74a93fad911573a +2026-07-06-sandbox.zh.md: 23ffdd62c2a8848cb687b4f2e63d05ca847445e8 diff --git a/.agents/notes/implemented/feature/2026-07-06-sandbox.md b/.agents/notes/implemented/feature/2026-07-06-sandbox.md index 62c46c99a2..7c451d8fb2 100644 --- a/.agents/notes/implemented/feature/2026-07-06-sandbox.md +++ b/.agents/notes/implemented/feature/2026-07-06-sandbox.md @@ -188,7 +188,7 @@ Costs and accepted limits: - **How is a BROKEN sandbox told apart from a failing command?** Any provider-argv spawn rejection proves the confined launch never started, but it identifies a broken runner only when the caller-owned workdir is usable and Node reports attributable `ENOENT` or `EACCES` for that argv[0]. A bare `syscall: 'spawn'` without an exact error path and all other rejections remain ordinary command-start errors. After a process starts, runner failure outranks denial only when one `runnerFailureRules` entry matches both its optional exit-code gate and a fatal stderr line after exact informational exclusions. Foreground failures throw structured `SANDBOX_UNAVAILABLE` with spawn or matched-line detail; an asynchronously rejected or settled background job stamps `sandbox.runnerFailed` and renders its own marker. A `SubprocessRuntime` that synchronously throws the same `ENOENT`/`EACCES` shape with the runner path makes background start throw the structured error; other synchronous errors propagate unchanged. A Landlock partial-enforcement notice plus an ordinary child failure remains a command result. - **What happens on a platform with no backend — Windows today?** `confine()` throws the fail-closed `SANDBOX_UNAVAILABLE` and the command never spawns; `win32` is a reserved EMPTY chain, pinned by test to fail closed identically until a Windows runner fills it (§ Deferred phases). - **`bwrap` is installed on my host but unusable (disabled unprivileged userns, an LSM denying `mount`) — what happens?** The chain probe is functional — it builds and enforces a real profile rather than checking `--version` — so a present-but-unusable `bwrap` fails its probe, selection falls to the packaged Landlock launcher, and the verdict is cached for the provider's lifetime. -- **Does the sandbox restrict network or process visibility?** No — `SandboxMode` claims FILE effects only; the bwrap profile deliberately does not unshare pid, and no backend claims network. Whether network restriction becomes its own knob is left open in § The seam. +- **Does the sandbox restrict network or process visibility?** `SandboxMode` claims FILE effects only, and no backend claims network. Process visibility is backend-specific: bwrap unshares PID and mounts matching procfs because host `/proc/` magic links otherwise bypass file confinement, while Landlock and Seatbelt leave process visibility unchanged ([decision](../bug-fix/2026-08-06-bwrap-private-pid-namespace.md)). Whether network restriction becomes its own knob is left open in § The seam. - **Which tools actually run confined?** OS subprocesses through `ctx.shell` — the bash tools, and hook commands transitively — plus the filesystem tools (`read`/`write`/`edit`) through the sandboxed `ctx.fs` provider (the [cross-family fs sandbox RFC](2026-07-14-cross-family-fs-sandbox.md)): bash confines via the OS runner, fs via an in-process path fence, both keying off the same `ctx.sandboxPolicy` mode. web/todo stay in-process and unfenced (web's only effect is network, outside the file-effect mode vocabulary). - **Does a granted escalation persist?** No. The grant is consumed by the exact foreground or background call that asked; every neighboring call keeps its own effective mode. A later background denial surfaces through `job_output` and may ground a new exact-command retry. - **When does a runtime mode switch take effect?** Once its session event commits, the next pre-step policy-context reconciliation and the next capability resolution fold the new mode. The sourced context message records what the model was told, and any later denial names the same policy at the point of use. diff --git a/.agents/notes/implemented/feature/2026-07-06-sandbox.zh.md b/.agents/notes/implemented/feature/2026-07-06-sandbox.zh.md index 82c2e79628..23ffdd62c2 100644 --- a/.agents/notes/implemented/feature/2026-07-06-sandbox.zh.md +++ b/.agents/notes/implemented/feature/2026-07-06-sandbox.zh.md @@ -188,7 +188,7 @@ fs/web/todo 在进程内执行,因此它们的沙箱语义是各自能力边 - **如何区分损坏的沙箱与失败的命令?** 提供方 argv 的任何 spawn 拒绝都能证明受限启动从未开始,但只有在调用方拥有的 workdir 可用,且 Node 为该 argv[0] 报告可归因的 `ENOENT` 或 `EACCES` 时,才能据此判定 runner 损坏。没有精确错误路径的裸 `syscall: 'spawn'` 和其他所有拒绝仍是普通的命令启动错误。进程启动后,只有当 `runnerFailureRules` 中某一条目同时匹配其可选退出码门控,以及排除整行精确信息性行后的一行致命 stderr 诊断时,runner 失败才会优先于拒绝。前台失败会抛出结构化的 `SANDBOX_UNAVAILABLE`,并附带 spawn 错误或匹配行作为详细信息;遭异步拒绝或已结算的后台任务则盖章 `sandbox.runnerFailed` 并渲染自己的标记。如果 `SubprocessRuntime` 同步抛出同样带有 runner 路径的 `ENOENT`/`EACCES` 形态,后台启动会抛出该结构化错误;其他同步错误原样传播。Landlock 部分强制执行通知加上普通子进程失败时,仍返回命令结果。 - **在没有后端的平台上会发生什么——今天的 Windows?** `confine()` 抛出失败关闭的 `SANDBOX_UNAVAILABLE`,命令永不 spawn;`win32` 是保留的空链,由测试固定为同样失败关闭,直到 Windows runner 填充它(§ 延迟阶段)。 - **`bwrap` 已安装在我的主机上但不可用(禁用了非特权 userns、LSM 拒绝 `mount`)——会发生什么?** 链探测是功能性的——它构建并强制一个真实 profile 而非检查 `--version`——因此存在但不可用的 `bwrap` 探测失败,选择落到已打包的 Landlock launcher,结论在提供方生命周期内缓存。 -- **沙箱限制网络或进程可见性吗?** 不——`SandboxMode` 仅声称文件操作;bwrap profile 刻意不 unshare pid,没有后端声称网络。网络限制是否成为自己的旋钮留在 § seam 中开放。 +- **沙箱限制网络或进程可见性吗?** `SandboxMode` 只声称文件影响,并且没有后端声称限制网络。进程可见性因后端而异:bwrap 会取消共享 PID 命名空间,并挂载与其匹配的 procfs,因为宿主 `/proc/` 的魔法链接会绕过文件约束;Landlock 与 Seatbelt 则保持进程可见性不变(见[相关决策](../bug-fix/2026-08-06-bwrap-private-pid-namespace.md))。网络限制是否成为自己的旋钮留在 § seam 中开放。 - **哪些工具实际在约束下运行?** 通过 `ctx.shell` 的 OS 子进程——bash 工具及传递性的钩子命令——再加上通过沙箱化 `ctx.fs` 提供方运行的文件系统工具(`read`/`write`/`edit`,见[跨工具族 fs 沙箱 RFC](2026-07-14-cross-family-fs-sandbox.md)):bash 通过 OS runner 约束,fs 通过进程内路径围栏约束,二者都以同一个 `ctx.sandboxPolicy` 模式为键。web/todo 仍在进程内且不受限制(web 的唯一效果是网络,不在文件效果模式词汇内)。 - **授权的升级会持久化吗?** 不会。授权由发起请求的确切前台或后台调用消费;每个相邻调用保留自己的有效模式。后续的后台拒绝通过 `job_output` 呈现,并且可以作为一次新的精确命令重试的依据。 - **运行时模式切换何时生效?** 一旦其会话事件提交,下一次 pre-step 策略上下文协调与下一次能力解析都会折叠新模式。带来源的上下文消息会记录模型收到的内容,之后的任何拒绝都会在使用点命名同一策略。 diff --git a/apps/cli/reference/README.i18n.yaml b/apps/cli/reference/README.i18n.yaml index a5851993dc..68d249bee0 100644 --- a/apps/cli/reference/README.i18n.yaml +++ b/apps/cli/reference/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write apps/cli/reference/README.md -README.md: e60f9d9e00dc77c8b2f22edcba67f9e8f3ba2f07 -README.zh.md: a99531a67d447039803a7a8248f2cc30a19d9be9 +README.md: dfddd177a78c348793d3e5c2d290fa62c5ac850b +README.zh.md: 8fc28bd8c08581236bd28cfe16942fb05d1659c3 diff --git a/apps/cli/reference/README.md b/apps/cli/reference/README.md index e60f9d9e00..dfddd177a7 100644 --- a/apps/cli/reference/README.md +++ b/apps/cli/reference/README.md @@ -80,7 +80,7 @@ Process shutdown gives the plugin tree up to five seconds to dispose. The first All modes treat the invoking directory as the default workspace root, load applicable `AGENTS.md` or `CLAUDE.md` instructions with a 65,536-byte render budget, and use an in-memory SQLite session content index. Every profile boot watches valid edits of both `cordis.patch.yml` layers (profile and home) and reapplies them transactionally; a one-shot surface exits through its bounded shutdown, which disposes the watchers. -New sessions default to the `workspace-write` permission preset. Bash and filesystem mutations are restricted to the session workspace and platform temporary roots; reads, network access, and process visibility are not confined. `DSH_PERMISSION_MODE` changes the process fallback. Stored General-settings permissions affect later Web sessions, not an already-open one. +New sessions default to the `workspace-write` permission preset. Bash and filesystem mutations are restricted to the session workspace and platform temporary roots; reads and network access are not confined, while process visibility depends on the selected sandbox backend — bwrap runs commands in a private PID namespace that hides host processes, and Landlock and Seatbelt leave host process visibility unchanged. `DSH_PERMISSION_MODE` changes the process fallback. Stored General-settings permissions affect later Web sessions, not an already-open one. `DSH_TOOLS_MODE` selects `native`, `code`, or `both` for the process; another value fails at boot. The shipped `minimal` agent preset keeps that deployment presentation, fixes the complete system prompt to `You are a helpful software engineer assistant.`, and composes only persistent `bash` plus `str_replace_editor`. Select 极简模式 when creating a Web session; every other prompt section and model-facing plugin remains absent from that agent while the shared browser, workspace, persistence, sandbox, and permission host stays in place. diff --git a/apps/cli/reference/README.zh.md b/apps/cli/reference/README.zh.md index a99531a67d..8fc28bd8c0 100644 --- a/apps/cli/reference/README.zh.md +++ b/apps/cli/reference/README.zh.md @@ -80,7 +80,7 @@ dsh web --help 所有模式都将运行命令时所在的目录作为默认 workspace 根目录,以 65,536 字节渲染预算加载适用的 `AGENTS.md` 或 `CLAUDE.md` 指令,并使用内存 SQLite 会话内容索引。每次启动 profile 时,系统都会监视 profile 与 home 两个 `cordis.patch.yml` 配置层的有效变更,并以事务方式重新应用;一次性运行模式通过有界关闭流程退出,该流程会先 dispose 监视器。 -新会话默认使用 `workspace-write` 权限预设。Bash 和文件系统修改仅限于会话 workspace 与平台临时根目录;读取、网络访问和进程可见性不受限制。`DSH_PERMISSION_MODE` 更改进程后备值。General settings 中存储的权限影响后续 Web 会话,不改变已打开的会话。 +新会话默认使用 `workspace-write` 权限预设。Bash 和文件系统修改仅限于会话 workspace 与平台临时根目录;读取和网络访问不受限制,进程可见性则取决于所选沙箱后端——bwrap 在私有 PID 命名空间中运行命令并隐藏宿主进程,Landlock 与 Seatbelt 保持宿主进程可见性不变。`DSH_PERMISSION_MODE` 更改进程后备值。General settings 中存储的权限影响后续 Web 会话,不改变已打开的会话。 `DSH_TOOLS_MODE` 为进程选择 `native`、`code` 或 `both`;其他值会导致启动失败。随附的 `minimal` agent preset 会保留该部署的呈现方式,将完整系统提示词固定为 `You are a helpful software engineer assistant.`,并且仅组合持久 `bash` 和 `str_replace_editor`。创建 Web 会话时请选择极简模式;该 agent 不包含任何其他提示词段落或面向模型的插件,而共享的浏览器、workspace、持久化、沙箱与权限宿主保持不变。 diff --git a/examples/acp-agent/tests/escalation.e2e.ts b/examples/acp-agent/tests/escalation.e2e.ts index 9f2bbf5f96..e754dc8799 100644 --- a/examples/acp-agent/tests/escalation.e2e.ts +++ b/examples/acp-agent/tests/escalation.e2e.ts @@ -13,6 +13,7 @@ import { type AgentUnderTest, type LaunchedAcpTestAgent, } from '@deepseek-ai/dsh-acp-snapshot' +import { bwrapProfileArgs } from '@deepseek-ai/dsh-sandbox-local/src/profiles.ts' import { cleanupAcpExampleTest } from './cleanup.ts' /** @@ -42,7 +43,7 @@ const AGENT: AgentUnderTest = { // bwrap on Linux, Seatbelt's sandbox-exec on macOS. Without one the strict // attempt would fail closed (SANDBOX_UNAVAILABLE) instead of producing the // denial this flow starts from. -const hasBwrap = spawnSync('bwrap', ['--ro-bind', '/', '/', '--dev', '/dev', '--proc', '/proc', '--die-with-parent', '--', 'true'], { +const hasBwrap = spawnSync('bwrap', [...bwrapProfileArgs({ mode: 'read-only', workspaceRoot: '/' }), '--', 'true'], { timeout: 5_000, stdio: 'ignore', }).status === 0 diff --git a/packages/examples/agent-spine-demo/tests/multi-project-sandbox.e2e.ts b/packages/examples/agent-spine-demo/tests/multi-project-sandbox.e2e.ts index c9fe5fe799..f06fe24924 100644 --- a/packages/examples/agent-spine-demo/tests/multi-project-sandbox.e2e.ts +++ b/packages/examples/agent-spine-demo/tests/multi-project-sandbox.e2e.ts @@ -10,7 +10,7 @@ import * as FsPolicy from '@deepseek-ai/dsh-fs-observation-policy' import SandboxedFileSystem from '@deepseek-ai/dsh-fs-sandbox' import { CallId } from '@deepseek-ai/dsh-llm' import { LocalSandboxProvider } from '@deepseek-ai/dsh-sandbox-local' -import { seatbeltProfileArgs } from '@deepseek-ai/dsh-sandbox-local/src/profiles.ts' +import { bwrapProfileArgs, seatbeltProfileArgs } from '@deepseek-ai/dsh-sandbox-local/src/profiles.ts' import SandboxPolicyService from '@deepseek-ai/dsh-sandbox-policy' import { SessionId } from '@deepseek-ai/dsh-session' import * as ToolFs from '@deepseek-ai/dsh-tool-fs' @@ -18,9 +18,7 @@ import type { ToolResult } from '@deepseek-ai/dsh-tools' import { launcherPath } from '@deepseek-ai/node-addon-landlock-run' import * as agentSpine from '../src/index.ts' -const bwrapUsable = spawnSync('bwrap', [ - '--ro-bind', '/', '/', '--dev', '/dev', '--proc', '/proc', '--die-with-parent', '--', 'true', -], { timeout: 5_000, stdio: 'ignore' }).status === 0 +const bwrapUsable = spawnSync('bwrap', [...bwrapProfileArgs({ mode: 'read-only', workspaceRoot: '/' }), '--', 'true'], { timeout: 5_000, stdio: 'ignore' }).status === 0 const landlockUsable = spawnSync(launcherPath(), ['--probe'], { timeout: 5_000, stdio: 'ignore' }).status === 0 const seatbeltUsable = process.platform === 'darwin' && spawnSync('sandbox-exec', [...seatbeltProfileArgs({ mode: 'workspace-write', workspaceRoot: homedir() }), '--', 'true'], { timeout: 5_000, stdio: 'ignore' }).status === 0 diff --git a/packages/sandbox/sandbox-local/README.i18n.yaml b/packages/sandbox/sandbox-local/README.i18n.yaml index d8155add50..fc7291c28f 100644 --- a/packages/sandbox/sandbox-local/README.i18n.yaml +++ b/packages/sandbox/sandbox-local/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/sandbox/sandbox-local/README.md -README.md: e1b59385a1380e0918e69c06dd86834c00cf2395 -README.zh.md: f4e31e220f3f69612243066a3183ebf79ed6f73a +README.md: 7f96ebd725280cc5b767c40dcf52e9353aaafe79 +README.zh.md: ce2dc2c2ce070d225ba6bd6aaf5fb5709f6061aa diff --git a/packages/sandbox/sandbox-local/README.md b/packages/sandbox/sandbox-local/README.md index e1b59385a1..7f96ebd725 100644 --- a/packages/sandbox/sandbox-local/README.md +++ b/packages/sandbox/sandbox-local/README.md @@ -6,10 +6,12 @@ Local implementation of the [`dsh-sandbox`](../sandbox/) seam. It selects and ca The package root exports the default and named `LocalSandboxProvider` plugin and `Config`; platform profile builders stay internal. -Unsupported platforms and unusable runners fail closed with `SANDBOX_UNAVAILABLE`; execution never silently falls through unconfined. Each wrap carries structured runner-failure rules so consumers can distinguish a broken sandbox from a command failure. The [sandbox Agent Note](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md) owns selection rationale and profile differences. +Unsupported platforms and unusable runners fail closed with `SANDBOX_UNAVAILABLE`; execution never silently falls through unconfined. Each wrap carries structured runner-failure rules so consumers can distinguish a broken sandbox from a command failure. Policy is per call; the provider stores only the mechanism and cached runner verdict. Each wrap reports enforcement completeness plus backend-specific denial signatures and runner-failure rules. Landlock requires exit 125 and a `landlock-run:` fatal line after excluding only the exact partial-enforcement notice; a notice with child exit 1, 2, or 125 remains a child outcome. Bubblewrap and Seatbelt remain signature-only because neither public contract reserves a launcher-failure status. Consumers spawn the returned argv directly, so a missing or unexecutable runner is an out-of-band spawn failure while a successfully launched child exit 126 or 127 remains ordinary. `runnerCommand` skips probes and requires one or more non-empty, single-line, case-insensitive `runnerFailureSignatures` entries for the custom runner's own fatal dialect. Because its mechanism is unknown, it carries both Linux denial dialects. `probeTimeoutMs` bounds functional probes. The [sandbox Agent Note](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md) owns selection and failure semantics. +The bwrap profile combines a read-only host root, fresh `/dev`, and private-PID `/proc`. Commands manage descendants but cannot see host processes; hiding host `/proc/` entries prevents magic links such as `root` and `fd` from bypassing its mounts. `workspace-write` adds ephemeral `/tmp` and a writable workspace bind. The [private-PID Agent Note](../../../.agents/notes/implemented/bug-fix/2026-08-06-bwrap-private-pid-namespace.md) records the boundary. + The Seatbelt profile is allow-default with `(deny file-write*)` plus write allow-lists, so exactly the mode's promised file effects are governed: `read-only` grants the `/dev/null` literal alone; `workspace-write` adds the workspace root, `/tmp`, and the per-user darwin temp dir (`os.tmpdir()` — the platform's real temp area for mkstemp-family tools), every root canonicalized because Seatbelt matches resolved paths (`/tmp` IS `/private/tmp`). Apple marks the `sandbox-exec` CLI deprecated but ships it on every macOS; the functional probe is what fails closed if that ever changes. The Windows rung keeps one deterministic write SID and standing ACE per workspace, but gives every live session/workspace pair a random private temp directory with a distinct SID and revocable ACE. Sessions sharing a workspace therefore share its intended write authority without inheriting one another's temp authority. A fresh provider always chooses a new temp path and SID, so crash residue cannot block or authorize a resumed session; agentless calls receive the same per-invocation isolation from the runner. A workspace equal to or containing the platform temp root fails before any ACL mutation because its inheritable workspace ACE would otherwise reach every private temp child. diff --git a/packages/sandbox/sandbox-local/README.zh.md b/packages/sandbox/sandbox-local/README.zh.md index f4e31e220f..ce2dc2c2ce 100644 --- a/packages/sandbox/sandbox-local/README.zh.md +++ b/packages/sandbox/sandbox-local/README.zh.md @@ -6,10 +6,12 @@ 包根目录导出默认及命名的 `LocalSandboxProvider` 插件和 `Config`;平台 profile builder 仍为内部实现。 -不受支持的平台和不可用 runner 会以 `SANDBOX_UNAVAILABLE` 拒绝执行;执行绝不会静默回退为不受限制。每次包装都携带结构化 runner 失败规则,使消费方能够区分损坏的沙箱与命令失败。[沙箱 Agent Note](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md) 负责说明选择依据与 profile 差异。 +不受支持的平台和不可用 runner 会以 `SANDBOX_UNAVAILABLE` 拒绝执行;执行绝不会静默回退为不受限制。每次包装都携带结构化 runner 失败规则,使消费方能够区分损坏的沙箱与命令失败。 策略逐调用传入;提供方只存储机制与缓存的 runner 结论。每次包装都会报告强制执行完整度,以及后端专用的拒绝签名和 runner 失败规则。Landlock 只有在退出码为 125,且仅排除完全匹配的部分强制执行通知后仍存在一行 `landlock-run:` 致命诊断时,才判定 runner 失败;携带该通知的子进程即使以 1、2 或 125 退出,也仍按子进程结果处理。Bubblewrap 和 Seatbelt 仍仅依据签名,因为两者的公开约定均未保留 launcher 失败状态。消费方会直接 spawn 返回的 argv,因此 runner 缺失或不可执行属于带外 spawn 失败,而成功启动的子进程以 126 或 127 退出时仍按普通结果处理。`runnerCommand` 会跳过探测,并要求为自定义 runner 自身的致命方言提供一个或多个非空、单行、不区分大小写的 `runnerFailureSignatures` 条目。由于其机制未知,它会同时携带两种 Linux 拒绝方言。`probeTimeoutMs` 限定功能探测的时长。[沙箱 Agent Note](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md) 负责说明选择与失败语义。 +bwrap profile 将只读宿主根目录、全新的 `/dev` 与使用私有 PID 命名空间的 `/proc` 组合起来。命令可管理后代进程,但看不到宿主进程;隐藏宿主的 `/proc/` 条目,可以防止 `root`、`fd` 等魔法链接绕过其挂载约束。`workspace-write` 另加临时的 `/tmp` 与可写工作区绑定挂载。[私有 PID Agent Note](../../../.agents/notes/implemented/bug-fix/2026-08-06-bwrap-private-pid-namespace.md)记录该边界。 + Seatbelt profile 默认允许,但带 `(deny file-write*)` 和写入 allow-list,因此恰好约束相应模式承诺的文件操作:`read-only` 只授予 `/dev/null` 字面路径;`workspace-write` 另加工作区根目录、`/tmp` 和逐用户 darwin 临时目录(`os.tmpdir()`,即平台供 mkstemp 家族工具使用的真实临时区域)。每个根目录都经过规范化,因为 Seatbelt 匹配解析后的路径(`/tmp` 就是 `/private/tmp`)。Apple 将 `sandbox-exec` CLI(命令行界面)标为 deprecated,但所有 macOS 系统仍会提供它;若情况发生变化,功能探测会使执行被拒绝。 Windows 档为每个工作区保留一个确定性写入 SID 和常驻 ACE,但为每个活跃的会话/工作区对分配一个随机私有临时目录,以及不同的 SID 和可撤销 ACE。因此,共享工作区的会话会共享预期的写权限,却不会继承彼此的临时目录权限。新的提供方总会选择新的临时路径和 SID,因此崩溃残留既无法阻止恢复的会话,也无法向其授权;runner 会为无 agent(智能体)的调用提供同样的逐调用隔离。如果工作区等于或包含平台临时根目录,调用会在任何 ACL 改动发生前失败,因为否则其可继承的工作区 ACE 会延伸到每个私有临时子目录。 diff --git a/packages/sandbox/sandbox-local/src/index.ts b/packages/sandbox/sandbox-local/src/index.ts index 8880da8103..5965d86d32 100644 --- a/packages/sandbox/sandbox-local/src/index.ts +++ b/packages/sandbox/sandbox-local/src/index.ts @@ -66,7 +66,7 @@ export interface Config { /** Probe whether `bwrap` can create the profile; the provider caches the bounded result. */ function defaultProbeBwrap(timeoutMs: number): boolean { - const probe = spawnSync('bwrap', ['--ro-bind', '/', '/', '--dev', '/dev', '--proc', '/proc', '--die-with-parent', '--', 'true'], { + const probe = spawnSync('bwrap', [...bwrapProfileArgs({ mode: 'read-only', workspaceRoot: '/' }), '--', 'true'], { timeout: timeoutMs, stdio: 'ignore', }) diff --git a/packages/sandbox/sandbox-local/src/profiles.ts b/packages/sandbox/sandbox-local/src/profiles.ts index 5b76390319..4d647580c5 100644 --- a/packages/sandbox/sandbox-local/src/profiles.ts +++ b/packages/sandbox/sandbox-local/src/profiles.ts @@ -14,7 +14,7 @@ import type { SandboxPolicy } from '@deepseek-ai/dsh-sandbox' * @returns profile arguments before the trailing separator and command argv. */ export function bwrapProfileArgs(policy: SandboxPolicy): string[] { - const args = ['--ro-bind', '/', '/', '--dev', '/dev', '--proc', '/proc', '--die-with-parent'] + const args = ['--ro-bind', '/', '/', '--dev', '/dev', '--unshare-pid', '--proc', '/proc', '--die-with-parent'] if (policy.mode === 'workspace-write') { args.push('--tmpfs', '/tmp') args.push('--bind', policy.workspaceRoot, policy.workspaceRoot) diff --git a/packages/sandbox/sandbox-local/tests/bwrap.e2e.ts b/packages/sandbox/sandbox-local/tests/bwrap.e2e.ts index 3da6787049..9a8289b95a 100644 --- a/packages/sandbox/sandbox-local/tests/bwrap.e2e.ts +++ b/packages/sandbox/sandbox-local/tests/bwrap.e2e.ts @@ -1,5 +1,5 @@ import { spawnSync } from 'node:child_process' -import { existsSync, readFileSync, rmSync } from 'node:fs' +import { existsSync, readFileSync, readlinkSync, rmSync } from 'node:fs' import { mkdtemp, rm } from 'node:fs/promises' import { homedir, tmpdir } from 'node:os' import { join } from 'node:path' @@ -78,6 +78,44 @@ describe.skipIf(!bwrapUsable)('sandbox-local: real bwrap confinement', () => { expect(result.stdout).toBe('dev-ok\n') }) + it.each(['read-only', 'workspace-write'] as const)( + '%s runs in a private PID namespace and blocks writes through procfs root magic links', + async (mode) => { + const workdir = await tempDir(homedir()) + const outside = await tempDir(homedir()) + const target = join(outside, 'escaped.txt') + const sandbox = await provider() + // Compare PID-namespace identity, not PID numbers: numeric /proc entries + // recur inside a private namespace, and the /proc/1/root write below is + // denied even in a shared namespace (host init is root-owned), so this + // comparison is the assertion that fails when --unshare-pid is lost. + const hostPidNamespace = readlinkSync('/proc/self/ns/pid') + const visibility = runConfined(sandbox, 'readlink /proc/self/ns/pid', { mode, workspaceRoot: workdir }) + expect(visibility.result.status).toBe(0) + expect(visibility.result.stdout.trim()).not.toBe('') + expect(visibility.result.stdout.trim()).not.toBe(hostPidNamespace) + + const escape = runConfined( + sandbox, + `printf escaped > /proc/1/root${target}`, + { mode, workspaceRoot: workdir }, + ) + expect(escape.result.status).not.toBe(0) + expect(existsSync(target)).toBe(false) + }, + ) + + it('keeps descendants observable and controllable inside the private PID namespace', async () => { + const workdir = await tempDir(homedir()) + const sandbox = await provider() + const { result } = runConfined( + sandbox, + 'sleep 30 & child=$!; kill -0 "$child" && kill "$child"; wait "$child"; status=$?; test "$status" -ge 128', + { mode: 'read-only', workspaceRoot: workdir }, + ) + expect(result.status).toBe(0) + }) + it('workspace-write lands a write inside the workspace root and still denies one beside it', async () => { const workdir = await tempDir(homedir()) const outside = await tempDir(homedir()) diff --git a/packages/sandbox/sandbox-local/tests/local.spec.ts b/packages/sandbox/sandbox-local/tests/local.spec.ts index 389f23a011..285afb865b 100644 --- a/packages/sandbox/sandbox-local/tests/local.spec.ts +++ b/packages/sandbox/sandbox-local/tests/local.spec.ts @@ -62,13 +62,13 @@ function fakeSeatbeltExec(status: number): string { const SEATBELT_RO_PROFILE = '(version 1) (allow default) (deny file-write*) (allow file-write* (literal "/dev/null"))' describe('profile dialects', () => { - it('bwrap read-only: whole tree read-only with fresh /dev and /proc, no writable mounts', () => { - expect(bwrapProfileArgs(RO)).toEqual(['--ro-bind', '/', '/', '--dev', '/dev', '--proc', '/proc', '--die-with-parent']) + it('bwrap read-only: whole tree read-only with fresh /dev and private PID-scoped /proc, no writable mounts', () => { + expect(bwrapProfileArgs(RO)).toEqual(['--ro-bind', '/', '/', '--dev', '/dev', '--unshare-pid', '--proc', '/proc', '--die-with-parent']) }) it('bwrap workspace-write: adds an ephemeral /tmp and rebinds the workspace root', () => { expect(bwrapProfileArgs(WW)).toEqual([ - '--ro-bind', '/', '/', '--dev', '/dev', '--proc', '/proc', '--die-with-parent', + '--ro-bind', '/', '/', '--dev', '/dev', '--unshare-pid', '--proc', '/proc', '--die-with-parent', '--tmpfs', '/tmp', '--bind', '/ws', '/ws', ]) }) diff --git a/packages/shell/bash-sandbox/README.i18n.yaml b/packages/shell/bash-sandbox/README.i18n.yaml index 6792ffa955..787ee4d2f1 100644 --- a/packages/shell/bash-sandbox/README.i18n.yaml +++ b/packages/shell/bash-sandbox/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/shell/bash-sandbox/README.md -README.md: fe9a0b1a891bd2b9102357f48141822c70465ddf -README.zh.md: 648264615dc476f9442b9bb9ae3a332196f4e104 +README.md: dd0f6dd80394ee8bf0349f706e8fd77cd3c14189 +README.zh.md: 6670058e3b8d14f72bf0133d40a433b82b17e4fa diff --git a/packages/shell/bash-sandbox/README.md b/packages/shell/bash-sandbox/README.md index fe9a0b1a89..dd0f6dd803 100644 --- a/packages/shell/bash-sandbox/README.md +++ b/packages/shell/bash-sandbox/README.md @@ -19,7 +19,7 @@ Semantics: - **Denials are result facts.** A failed run whose stderr carries the selected backend's own denial dialect — the signatures the provider stamps on every wrap (EROFS text under bwrap, EACCES under Landlock, EPERM under Seatbelt) — is reported as `ShellRunResult.sandbox.denied: true` (conservative classification, read from the collected stderr tail); every CONFINED run also carries the mode it executed under (`result.sandbox.mode`) and the provider's enforcement completeness (`result.sandbox.enforcement`: `full`, or `partial` on an older Landlock ABI). - **The runner path or syscall must match.** Before a process starts, a rejection is attributed to the runner only when the caller-owned workdir is independently usable and Node reports `ENOENT` or `EACCES` with either an `error.path` equal to provider argv[0] or, when `error.path` is absent, an exact `syscall: 'spawn '`. A present path also requires `syscall: 'spawn'` or the exact `spawn `. This covers a missing runner, a non-executable runner, or an executable script whose shebang interpreter is unavailable. A bare `syscall: 'spawn'` without an exact error path, any other code, an invalid or unusable workdir, a resource failure, an unrelated syscall, or an unstructured rejection retains the local executor's command-start failure semantics. Foreground execution throws `SANDBOX_UNAVAILABLE` with the original spawn detail, while asynchronous background settlement stamps `runnerFailed: true` and `denied: false`. If a `SubprocessRuntime` synchronously throws the same runner-identifying `ENOENT`/`EACCES` shape, background start throws `SANDBOX_UNAVAILABLE`; other synchronous errors propagate unchanged. After a process starts, a rule's optional exit-code check and a remaining fatal stderr line must both match after exact informational-line exclusions. A match takes priority over denial; foreground execution throws `SANDBOX_UNAVAILABLE` with the matched fatal line, while a settled background process stamps `process.sandbox.runnerFailed`, which the bash producer renders through generic `job_output`. Confined background handles retain their mode/enforcement facts and release per-process accounting in either path. - **Deployment fallback, per-call policy.** [`ctx.sandboxPolicy`](../../sandbox/sandbox-policy/) resolves a complete `SandboxExecutionPolicy` for every tool call: the calling session supplies its mode override and immutable cwd root, while deployment config supplies the fallbacks for agentless calls. An approved escalation changes only that policy's mode; its session root stays attached. `resolve()` carries the policy onto the spec, so overlapping commands from different projects run, classify, and report under their own roots and modes. The capability fact `ctx.shell.sandboxMode` reports the configured default so the tool layer advertises escalation only when this executor is mounted; the static bash tool description separately owns denial and escalation guidance. -- **File effects only.** Network and process visibility are deliberately not restricted — the mode vocabulary does not pretend to cover what the backend does not enforce. +- **File effects only.** The mode vocabulary claims only file effects. Network stays unrestricted; process visibility is backend-specific and documented by [`dsh-sandbox-local`](../../sandbox/sandbox-local/). - Process mechanics (spawn, process-group kills, output collection/spill, background handles, credential scrub) are inherited from [`dsh-bash-local`](../bash-local/); runner selection lives in [`dsh-sandbox-local`](../../sandbox/sandbox-local/). Deny-only at the seam: a denial is a reported fact, and this executor never negotiates permissions itself — the approval question lives in the tool layer (`dsh-tool-bash`), which drives the override this package honors. @@ -82,7 +82,7 @@ Append-only; newly visible content follows the reusable request prefix and does ## Known Limitations and Deferred Work -- **Confinement covers file effects only** — network access and process visibility are unchanged, so the modes are not a general-purpose security sandbox. +- **Confinement covers file effects only** — network restriction and a uniform process-visibility guarantee are absent, so the modes are not a general-purpose security sandbox. - **Denials are inferred from failed-command stderr** — backend signatures make the inference portable, but a matching application error can be classified as a denial and a denial omitted from the retained tail can be missed. - **An asynchronously observed background runner failure has no immediate error channel** — it is recorded on the settled process and surfaces when the caller reads the generic task with `job_output`; a synchronous `SubprocessRuntime` throw that names the runner path instead fails `start()` immediately. - **`danger-full-access` deliberately bypasses `ctx.sandbox`** — it is an explicit unconfined mode, not a wider sandbox profile. diff --git a/packages/shell/bash-sandbox/README.zh.md b/packages/shell/bash-sandbox/README.zh.md index 648264615d..6670058e3b 100644 --- a/packages/shell/bash-sandbox/README.zh.md +++ b/packages/shell/bash-sandbox/README.zh.md @@ -19,7 +19,7 @@ - **拒绝是结果事实。** 如果一次失败运行的 stderr 包含所选后端自身的拒绝方言,即提供方在每次包装时加上的特征(bwrap 下的 EROFS 文本、Landlock 下的 EACCES、Seatbelt 下的 EPERM),则结果报告 `ShellRunResult.sandbox.denied: true`(从已收集的 stderr 尾部进行保守分类)。每次受限制运行还会携带执行时模式(`result.sandbox.mode`)与提供方强制执行完整性(`result.sandbox.enforcement`:`full`,或在较旧 Landlock ABI 上为 `partial`)。 - **Runner 路径或 syscall 必须匹配。** 进程启动前,调用方拥有的 workdir 必须经独立验证可用,Node 必须报告 `ENOENT` 或 `EACCES`,并且错误必须符合以下一种形态:`error.path` 等于提供方返回的 `argv[0]`,同时 `syscall` 为 `'spawn'` 或精确的 `'spawn '`;或者 `error.path` 不存在,同时 `syscall` 为精确的 `'spawn '`。这样可以识别缺失的 runner、不可执行的 runner,或 shebang 解释器不可用的可执行脚本。没有精确错误路径的裸 `syscall: 'spawn'`、任何其他错误码、无效或不可用的 workdir、资源失败、无关 syscall 或无结构拒绝仍保留本地执行器的命令启动失败语义。前台执行会抛出 `SANDBOX_UNAVAILABLE` 并附带原始 spawn 错误详情,异步后台结算则会标记 `runnerFailed: true` 和 `denied: false`。如果 `SubprocessRuntime` 同步抛出同样能指明 runner 的 `ENOENT`/`EACCES` 形态,后台启动会抛出 `SANDBOX_UNAVAILABLE`;其他同步错误原样传播。进程启动后,先按整行精确匹配排除信息性行,随后规则的可选退出码检查和余下 stderr 中的一行致命诊断必须同时匹配。匹配结果优先于拒绝;前台执行会抛出 `SANDBOX_UNAVAILABLE` 并附带匹配到的致命行,已结算的后台进程则会标记 `process.sandbox.runnerFailed`,Bash 结果生成方通过通用 `job_output` 渲染它。无论走哪条路径,受限制的后台句柄都会保留自身的模式/强制执行事实,并释放每进程计数。 - **部署回退,每次调用策略。** [`ctx.sandboxPolicy`](../../sandbox/sandbox-policy/) 为每次工具调用解析完整的 `SandboxExecutionPolicy`:调用会话提供自身的模式覆盖与不可变 cwd 根目录,部署配置则为无 agent(智能体)调用提供回退。已批准的升权只更改该策略的模式,会话根目录仍然附着其上。`resolve()` 把策略带入 spec,因此来自不同项目的重叠命令会在各自的根目录与模式下运行、分类和报告。能力事实 `ctx.shell.sandboxMode` 报告已配置的默认值,因此工具层只在装载该执行器时才公布升权;静态 bash 工具描述则单独负责拒绝与升权引导。 -- **只限制文件影响。** 设计上不限制网络与进程可见性:模式词汇不会声称覆盖后端未强制执行的范围。 +- **只限制文件影响。** 模式词汇只声称文件影响。网络仍不受限制;进程可见性因后端而异,具体见 [`dsh-sandbox-local`](../../sandbox/sandbox-local/)。 - 进程机制(spawn、进程组终止、输出收集/spill、后台句柄、凭证清理)继承自 [`dsh-bash-local`](../bash-local/);runner 选择位于 [`dsh-sandbox-local`](../../sandbox/sandbox-local/)。 该 seam 只报告拒绝:拒绝是一项结果事实,本执行器绝不自行协商权限。批准问题位于工具层(`dsh-tool-bash`),由它设置本包所遵守的模式覆盖值。 @@ -82,7 +82,7 @@ ## 已知限制与暂缓事项 -- **限制只覆盖文件影响**:网络访问与进程可见性不变,因此这些模式不是通用安全沙箱。 +- **限制只覆盖文件影响**:不提供网络限制和统一的进程可见性保证,因此这些模式不是通用安全沙箱。 - **拒绝从失败命令的 stderr 推断**:后端特征使该推断可跨平台使用,但包含相同后端特征的应用错误可能被分类为拒绝,也可能遗漏未出现在保留尾部中的拒绝。 - **异步观测到的后台 runner 失败没有即时错误通道**:它记录在已结算进程上,并在调用方使用 `job_output` 读取通用任务时呈现;`SubprocessRuntime` 同步抛出的错误包含 runner 路径时,则会使 `start()` 立即失败。 - **`danger-full-access` 有意绕过 `ctx.sandbox`**:它是显式无约束模式,不是更宽的沙箱 profile。 diff --git a/scripts/prepare-ci-bubblewrap.sh b/scripts/prepare-ci-bubblewrap.sh index e5f0902750..1efaee200a 100755 --- a/scripts/prepare-ci-bubblewrap.sh +++ b/scripts/prepare-ci-bubblewrap.sh @@ -28,5 +28,5 @@ printf '%s\n' "$root/usr/bin" >> "$GITHUB_PATH" sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 \ || echo 'apparmor userns knob absent — the functional probe decides' "$root/usr/bin/bwrap" --version -"$root/usr/bin/bwrap" --ro-bind / / --dev /dev --proc /proc --die-with-parent -- true +"$root/usr/bin/bwrap" --ro-bind / / --dev /dev --unshare-pid --proc /proc --die-with-parent -- true echo 'bubblewrap functional probe passed'