fix: prove Linux Agent sandbox boundary

This commit is contained in:
2026-07-10 17:59:56 +08:00
parent f4968d6657
commit f07f280b8f
19 changed files with 381 additions and 55 deletions
@@ -25,7 +25,7 @@ unit is written or enabled.
Preflight validates the configured bind host/port, authenticated PostgreSQL
access, Node/cph/Prisma, configured and canonical path traversal, and a real
bubblewrap namespace. The post-provision phase runs the built Hub dependency
graph, a database query, cph and bubblewrap as the service user under
graph, a database query, cph, socat and bubblewrap as the service user under
`no_new_privs`, matching the systemd security context. HTTP binding is awaited
before the Feishu listener starts, so bind failure rejects startup instead of
leaving a listener-only process alive.
@@ -1,7 +1,7 @@
# Confine the agent runtime and protect service credentials
Type: task
Status: claimed
Status: resolved
## Question
@@ -10,3 +10,29 @@ deny sibling tenant workspaces and service files, expose only the minimum safe
process environment, use SDK credential protection for secrets, and add an
actual Linux sandbox test proving another workspace and every non-provider
credential are unreadable while required tools still work.
## Answer
The Agent subprocess now receives a replacement environment containing only
named runtime values and the selected provider protocol values; PostgreSQL,
Feishu and Hub session credentials never cross the process boundary. Provider
credentials are additionally denied inside Bash. The SDK must sandbox every
command (`failIfUnavailable=true`, `allowUnsandboxedCommands=false`), denies
host reads and writes from root, and re-opens only the canonical project
workspace plus the reviewed read-only system runtime needed by `cph`.
SDK config/cache/home and general temp storage live inside the workspace. A
short relative `CLAUDE_CODE_TMPDIR` prevents the pinned SDK from falling back
to host `/tmp`, while absolute workspace-local `TMPDIR`/`TMP`/`TEMP` remain
stable after Bash changes directory. Linux production and CI now require both
bubblewrap and `socat`; preflight probes both as the service user. Bounded,
credential-redacted SDK stderr is logged with run and project correlation.
The real Linux proof runs the actual Claude SDK, Bash, bubblewrap, `socat` and
release `cph` as UID 1000 with no capabilities and `no_new_privs`. It proves
parent, sibling-project, `/tmp` and `/var/tmp` writes are rejected and have no
host effect; sibling/service-secret reads fail; platform and provider secrets
are absent from Bash; workspace temp remains anchored after `cd`; and `cph`
still executes. Final local gates passed: TypeScript check/build, Prisma schema,
173 unit tests, 85 mock-provider integration tests, deployment shell syntax,
and the dedicated Linux sandbox test.
@@ -1,7 +1,7 @@
# Close MCP context and file-delivery escape paths
Type: task
Status: claimed
Status: resolved
## Question
@@ -9,3 +9,20 @@ Restrict file delivery to a realpath/no-follow target inside the current project
workspace, remove Git-root access, make inbound Hub writes symlink-safe, and
validate every Feishu message/thread result against the run's bound chat before
returning data to the model.
## Answer
All Agent-facing workspace file reads and writes are anchored to the configured
workspace root and current project. Linux uses descriptor-relative, no-follow
traversal so a symlink swap cannot escape after validation; unsupported unsafe
paths fail closed. Inbound Feishu files use the same boundary. Outbound file
delivery no longer accepts the repository Git root or arbitrary absolute
paths: it reads a no-follow snapshot from the current project and uploads that
snapshot, so the checked object cannot be swapped before delivery.
The Feishu MCP read surface carries the run's project and bound-chat context.
Message and thread results are rejected unless their returned chat matches that
binding, and file-delivery MCP calls remain scoped to the same run/project/chat
tuple. Unit and integration regressions cover symlink paths, sibling projects,
Git-root delivery attempts, swapped files, and cross-chat results; they are
included in the final 173-unit/85-integration green gate reported by Issue 16.
@@ -45,6 +45,8 @@ rollback and recovery, and no known critical security or data-integrity gaps.
- [Decide the platform-administrator identity and audit boundary](issues/42-decide-platform-admin-identity-audit.md) — use a dedicated platform Feishu issuer, one peer administrator role, guarded bootstrap and identity-bound invitations, revocable server-side sessions, atomic append-only Platform Audit, and dual-factor offline recovery without a standing break-glass account.
- [Restore the checker formatting gate](issues/09-restore-checker-format-gate.md) — pin local and CI Rust/rustfmt/clippy to 1.92.0, apply the canonical workspace format, and keep the full fmt + clippy + 53-test checker gate green.
- [Provision a runnable non-root Hub service](issues/10-provision-nonroot-service.md) — install a strictly validated service identity and persistent layout, reject release/workspace overlap, validate complete production configuration and real bind settings, and execute Hub/Prisma/cph/bubblewrap probes as the service user under `no_new_privs` before writing the unit.
- [Confine the agent runtime and protect service credentials](issues/16-confine-agent-and-protect-credentials.md) — replace the Agent environment, deny host reads/writes from root, keep every writable SDK path inside the canonical project, require bubblewrap plus socat, emit bounded correlated diagnostics, and prove the boundary with the real Linux SDK under a capability-free non-root identity.
- [Close MCP context and file-delivery escape paths](issues/17-close-mcp-data-egress-escapes.md) — use no-follow project-rooted file operations and delivery snapshots, remove Git-root delivery, and bind every Feishu MCP read to the run's project and chat.
## Fog