forked from bai/curriculum-project-hub
fix(hub): 对齐 ADR-0018 agent 执行面边界
runner.ts: 启用 SDK 内置沙箱(bubblewrap/seatbelt),写入限制在 workspace, denyRead 敏感路径,failIfUnavailable 硬拒非沙箱运行。bypassPermissions 保留 (headless 无交互),沙箱是硬边界而非权限提示层。 install_service.sh: 默认 service user 从 root 改为 cph-hub;env 模板修正 OPENROUTER_API_KEY → ANTHROPIC_AUTH_TOKEN/ANTHROPIC_BASE_URL/ANTHROPIC_API_KEY (与 server.ts 实际读取一致);补 __BASE_DIR__ sed 替换。 cph-hub.service: AssertPathExists=/usr/bin/bwrap 强制沙箱依赖;NoNewPrivileges。 不加 ProtectSystem/ProtectHome(会破坏 cph render-cache 与 bwrap user-namespace)。 trigger.ts: 权限闸门去掉 if (senderOpenId !== '') 短路——缺 open_id 一律 fail-closed 拒绝,不再静默跳过;role gate 同步去掉冗余守卫(已由上游保证)。 顺手把 JSON.parse(msg.content) 的 inline cast 换成 Zod schema parse (FileMessageContentSchema / TextMessageContentSchema)。 ADR-0018: 状态改为 Accepted+implemented,机制段写定 SDK 沙箱,网络决定为开放, Open Questions 更新。
This commit is contained in:
@@ -2,9 +2,12 @@
|
||||
|
||||
## Status
|
||||
|
||||
Accepted. The decision is pinned; alignment of the current Hub
|
||||
implementation to it is a follow-up surfaced below (constitution rule 4:
|
||||
implementation diverging from contract must be surfaced, not silently kept).
|
||||
Accepted, and implemented. The mechanism is settled: the Claude Code SDK's
|
||||
built-in sandbox (bubblewrap on Linux, seatbelt on macOS) confines the agent
|
||||
process and its Bash subprocesses at the OS level. `bypassPermissions`
|
||||
remains (headless server — no interactive prompts), but the sandbox is the
|
||||
hard boundary, not the permission-prompt layer. Constitution rule 4
|
||||
divergence (ADR-0017's unbounded tools) is resolved.
|
||||
|
||||
Builds on **ADR-0001** (project group as collaboration space), **ADR-0002**
|
||||
(project lock scoped to `run_id`), **ADR-0004** (permission grants govern
|
||||
@@ -80,14 +83,35 @@ risk is compounded by root; it is enforced in the unit, not in `spec/` (it is
|
||||
not a semantic divergence between developer and agent — it is operational
|
||||
discipline).
|
||||
|
||||
### The mechanism is deliberately left open
|
||||
### The mechanism: SDK sandbox (OS-level, per agent process)
|
||||
|
||||
The boundary is enforced by the Claude Code SDK's built-in sandbox
|
||||
(bubblewrap on Linux, seatbelt on macOS), configured via `query()` options in
|
||||
`hub/src/agent/runner.ts`:
|
||||
|
||||
- `sandbox.enabled: true` + `failIfUnavailable: true` — the agent process and
|
||||
its Bash subprocesses run sandboxed; if the sandbox can't start, `query()`
|
||||
emits an error and exits rather than running unsandboxed.
|
||||
- `sandbox.filesystem.allowWrite: [workspaceDir]` — writes confined to the
|
||||
workspace (the ADR-0007 directory tree). Reads outside are allowed by
|
||||
default (the agent may read system files for context), but sensitive host
|
||||
paths are denied via `denyRead` (`/etc`, `/root`, `~/.ssh`, `~/.aws`, …).
|
||||
- `sandbox.filesystem.denyRead: SENSITIVE_READ_PATHS` — defense-in-depth;
|
||||
extends via `CPH_SANDBOX_EXTRA_DENY_READ` for deployment-specific secrets.
|
||||
- Network: open (see Open Questions).
|
||||
|
||||
`bypassPermissions` is kept (headless server — no interactive prompts); the
|
||||
sandbox is the hard boundary, not the permission-prompt layer. This is
|
||||
subprocess-level, not process-level: the Hub host process is not sandboxed
|
||||
(it manages DB, Feishu ws, logs), only the Claude Code agent process and its
|
||||
children. This keeps the sandbox config surface small and lets future
|
||||
external CLIs (Gitea, Lark) run inside the sandbox by giving their binaries
|
||||
read access, without widening the Hub's own filesystem surface.
|
||||
|
||||
How the boundary is *enforced* — path validation inside tool wrappers (the
|
||||
`workspace.ts` `confine()` shape), an OS-level sandbox (bubblewrap / container
|
||||
/ chroot), the SDK's own permission hooks, or some combination — is `OPEN`.
|
||||
The contract pins the **invariant** (operations stay in the workspace), not
|
||||
the **mechanism**. Any mechanism that upholds the invariant satisfies the
|
||||
contract; switching mechanisms is not a spec change.
|
||||
the **mechanism**. The SDK sandbox is the current mechanism; switching to a
|
||||
different one (tool wrappers, container) would not be a spec change as long
|
||||
as it upholds `AgentFileOp.Authorized`.
|
||||
|
||||
## Consequences
|
||||
|
||||
@@ -110,24 +134,30 @@ contract; switching mechanisms is not a spec change.
|
||||
change to the delivery path.
|
||||
- The `cph` subprocess (ADR-0016) already runs with `cwd = workspaceDir`
|
||||
(`hub/src/agent/cph.ts`), so it is inside the boundary by construction.
|
||||
- Force-release of a stuck lock (ADR-0002 admin override) does not cross the
|
||||
surface boundary — it is a lock-table operation, not an agent operation.
|
||||
|
||||
## Open Questions / Deferred
|
||||
|
||||
- **Enforcement mechanism.** Tool-wrapper path validation vs OS-level sandbox
|
||||
(bubblewrap/container) vs SDK permission hooks vs a combination. The
|
||||
contract pins the invariant; this picks the mechanism. Must be resolved
|
||||
before production, since the current mechanism is "none".
|
||||
- **`bypassPermissions` reconciliation.** Whether the Claude Code SDK can be
|
||||
configured to confine its built-in Read/Write/Glob/Grep to a subtree and to
|
||||
gate Bash, or whether the Hub must re-introduce custom tools wrapping
|
||||
`confine()`, is the concrete implementation question ADR-0017 left open and
|
||||
this ADR now forces.
|
||||
- **Network egress.** Decided: network is **open** in the initial
|
||||
production deployment. The sandbox confines filesystem writes to the
|
||||
workspace and denies reads of sensitive host paths, but does not restrict
|
||||
outbound network. Rationale: future Gitea/Lark CLI integration needs
|
||||
unconstrained network egress; `cph build` may fetch typst packages. The
|
||||
exfiltration risk (agent `curl`s workspace content out) is accepted as a
|
||||
tradeoff for CLI integration simplicity. This is revisitable — a network
|
||||
allowlist (`sandbox.network.allowedDomains`) can be added without a spec
|
||||
change if the threat model tightens.
|
||||
- **Sensitive read path coverage.** `SENSITIVE_READ_PATHS` in `runner.ts`
|
||||
covers `/etc`, `/root`, `/var/log`, `/proc`, `/sys`, and common
|
||||
credential dirs (`~/.ssh`, `~/.aws`, `~/.config/gcloud`, `~/.gnupg`).
|
||||
Deployments with additional secret locations extend via
|
||||
`CPH_SANDBOX_EXTRA_DENY_READ` (colon-separated). Whether this set should be
|
||||
spec-pinned rather than implementation-chosen is `OPEN` — it's plumbing, not
|
||||
a semantic divergence, so currently left out of `spec/`.
|
||||
- **Shell command vocabulary.** Whether the agent's Bash is restricted to a
|
||||
whitelist (e.g. `cph`, `ls`, `grep`) or allowed arbitrary commands whose
|
||||
*effects* are then confined, is a policy choice left to implementation. The
|
||||
invariant governs effects either way.
|
||||
invariant governs effects either way; current choice is arbitrary commands
|
||||
(sandbox confines effects).
|
||||
- **Per-user rate limiting and token budgets.** Orthogonal to the surface
|
||||
boundary (this ADR is about *what* the agent may touch, not *how often* it
|
||||
may run). Deferred to a separate concern.
|
||||
|
||||
Reference in New Issue
Block a user