Merge remote-tracking branch 'origin/master' into cross-family-fs-sandbox

# Conflicts:
#	docs/config-catalog.md
#	docs/cordis-catalog/events.md
#	docs/cordis-catalog/services.md
#	docs/event-producer-consumer.md
#	docs/persistence-catalog.md
#	examples/acp-agent/cordis.yml
#	examples/acp-agent/tests/snapshots/advanced-toolchain/session.1.jsonl
#	examples/acp-agent/tests/snapshots/advanced-toolchain/session.2.jsonl
#	examples/acp-agent/tests/snapshots/advanced-toolchain/session.jsonl
#	examples/acp-agent/tests/snapshots/both-mode-turn/session.jsonl
#	examples/acp-agent/tests/snapshots/permission-switching/session.jsonl
#	examples/acp-agent/tests/snapshots/skill-load/session.jsonl
#	examples/acp-agent/tests/snapshots/text-turn/session.jsonl
#	packages/bash/bash-sandbox/src/index.ts
#	packages/bash/bash/src/types.ts
#	packages/bash/tool-bash/src/index.ts
#	packages/fs/fs/src/index.ts
#	packages/sandbox/sandbox-policy/src/session-mode.ts
#	packages/ui/permission/src/index.ts
#	packages/ui/permission/tests/permission.spec.ts
#	scripts/doc-budgets.manifest.json
This commit is contained in:
kingwl
2026-07-14 21:25:36 +08:00
608 changed files with 7195 additions and 12508 deletions

View File

@@ -9,3 +9,24 @@ Policy rides the call, not the provider: two consumers may confine under differe
**Same-world confinement only.** A backend shares the host's filesystem and kernel (`bwrap`, Landlock, Seatbelt); `workspaceRoot` names a real host path. Containers, microVMs, and remote executors are NOT backends of this seam — they replace whole capability implementations (`ctx.bash`, `ctx.fs`) as environment-coherent groups. The boundary and its rationale: [the sandbox RFC](../../../docs/rfc/implemented/feature/2026-07-06-sandbox.md).
Implementations: [`@deepseek-ai/dsh-sandbox-local`](../sandbox-local/) (Linux: `bwrap`, else the per-platform Landlock launcher; macOS: `sandbox-exec`/Seatbelt). Consumers: [`@deepseek-ai/dsh-bash-sandbox`](../../bash/bash-sandbox/) (wraps `['bash', '-c', command]`).
## Model Experience
### Confinement error, indirectly
**What the model sees**: Through [`dsh-bash-sandbox`](../../bash/bash-sandbox/README.md) and [`dsh-tool-bash`](../../bash/tool-bash/README.md), failure to enforce a requested mode produces code `SANDBOX_UNAVAILABLE` and the exact error below. An execution-time runner failure adds ` Runner failure: <detail>`.
**Token effect**: Conditional error text is visible for that call and retained in history until compaction.
#### Exact error
```markdown
sandbox mode "<mode>" is requested but no sandbox backend is usable on this host; refusing to run the command unconfined. Install bubblewrap or run a Landlock-enforcing kernel (Linux), ensure sandbox-exec is usable (macOS) — Windows has no confinement backend yet — or switch the consumer to danger-full-access.
```
## Known Limitations and Deferred Work
- **File effects are the whole policy vocabulary** — the seam expresses no network, process, syscall, device, or credential restrictions.
- **Same-world confinement only** — containers, microVMs, and remote execution require replacing capability implementations rather than adding a provider here.
- **Denial reporting is a stderr dialect** — the seam returns backend signatures instead of a typed runtime denial channel, so consumers that need classification must infer it from the child process's output.
- **One provider per context** — composing different sandbox mechanisms simultaneously requires a provider-level ladder or separate Cordis contexts; callers choose policy per call, not backend identity.

View File

@@ -1,24 +1,7 @@
/**
* The process-sandbox seam (`ctx.sandbox`): an abstract service defining WHAT
* platform confinement does — wrap a subprocess argv so it executes under a
* file-effect policy — without saying HOW. Implementations subclass
* {@link SandboxProvider} and register as the `sandbox` service;
* `@deepseek-ai/dsh-sandbox-local` (per-platform chains: Linux `bwrap` then the
* npm-distributed `landlock-run` launcher, macOS `sandbox-exec`/Seatbelt) is
* the first.
* Consumers hand over the exact argv they are about to spawn
* (`@deepseek-ai/dsh-bash-sandbox` wraps `['bash', '-c', command]`; a
* subagent backend wraps its child-agent argv) and spawn the returned argv
* instead.
*
* The seam confines SAME-WORLD subprocesses only: a backend shares the
* host's filesystem and kernel, and the policy's `workspaceRoot` names a
* real host path. Containers, microVMs, and remote executors are NOT
* backends of this seam — they are sibling implementations of whole
* capability seams (`ctx.bash`, `ctx.fs`), deployed as environment-coherent
* groups; the boundary is recorded in
* docs/rfc/implemented/feature/2026-07-06-sandbox.md.
*
* Same-world process-confinement seam: wrap exact subprocess argv under a
* host-path file policy. Containers, microVMs, and remote execution replace the
* surrounding capability seam instead; this service shares the host kernel and filesystem.
* @module @deepseek-ai/dsh-sandbox
*/
@@ -37,25 +20,10 @@ export type { EscalationApproval, EscalationApprover, EscalationOutcome, Escalat
export { canonicalPath, writableRoots } from './roots.ts'
/**
* File-effect policy a sandbox backend enforces on confined processes.
*
* - `read-only` — the process cannot write the filesystem anywhere; a
* write-shaped `/dev/null` sink stays available so `>/dev/null` redirects
* keep working (HOW is the backend's choice: bwrap mounts a fresh `/dev`,
* the Landlock launcher and Seatbelt grant the single `/dev/null` node).
* - `workspace-write` — writes are allowed only under the policy's
* workspace root and `/tmp`; everything else stays read-only. Which `/tmp`
* is backend-specific — an ephemeral mount under bwrap, the HOST `/tmp`
* under the Landlock launcher, the host `/private/tmp` plus the per-user
* darwin temp dir under Seatbelt: the seam promises the write boundary,
* not the mount's nature.
* - `danger-full-access` — no confinement; a consumer configured with it
* spawns its argv unwrapped and never calls the provider.
*
* The mode governs FILE effects only: network and process visibility are not
* restricted (a backend that cannot honestly enforce them must not pretend
* to). How completely the file effects themselves are enforced is likewise a
* reported fact, not an assumption — see {@link SandboxEnforcement}.
* File-effect policy for confined processes. `read-only` permits only required
* sinks such as `/dev/null`; `workspace-write` also permits the workspace and a
* backend-defined temp area; `danger-full-access` bypasses confinement. Network
* and process visibility are outside this vocabulary.
*/
export type SandboxMode = 'read-only' | 'workspace-write' | 'danger-full-access'
@@ -63,18 +31,9 @@ export type SandboxMode = 'read-only' | 'workspace-write' | 'danger-full-access'
export type ConfinedSandboxMode = Exclude<SandboxMode, 'danger-full-access'>
/**
* How completely the selected backend enforces a confined mode's file
* effects.
*
* - `full` — every file effect the mode promises to block is governed: the
* `bwrap` mount profile, a Landlock kernel enforcing the launcher's whole
* ruleset, or an operator-configured runner (configuring one asserts full
* enforcement along with existence).
* - `partial` — the backend is active but the kernel governs only the subset
* of accesses its ABI knows (an older Landlock ABI: path-based truncate is
* ungoverned before ABI v3), so a file effect the mode promises to block
* may still land. A caller that needs the mode's promise to be absolute
* must treat `partial` as outside that promise.
* Enforcement completeness for this host. `partial` means an active backend or
* older kernel ABI cannot govern every promised file effect; callers requiring
* an absolute boundary must not treat it as `full`.
*/
export type SandboxEnforcement = 'full' | 'partial'
@@ -114,36 +73,25 @@ export interface ConfinedArgv {
*/
denialSignatures: readonly string[]
/**
* How the RUNNER ITSELF failing identifies itself: case-insensitive stderr
* substrings produced when the sandbox binary is missing, refuses its
* profile, or fails closed before exec'ing the command (`bwrap: `,
* `landlock-run: `, `sandbox-exec: ` — each covers both the runner's own
* error prefix and the shell's runner-not-found message). ORTHOGONAL to
* {@link denialSignatures}: a denial is the confined COMMAND being blocked
* (the sandbox working as designed); a runner failure means the command
* NEVER RAN and must surface as a sandbox failure, not a task failure —
* consumers check these signatures FIRST (a runner's own error text may
* contain denial words, e.g. an unopenable grant root reporting
* `Permission denied`).
* Case-insensitive signatures for runner failure before command execution.
* Consumers check these before denial signatures: runner failure means the
* command never ran, while denial means confinement worked and blocked it.
*/
runnerFailureSignatures: readonly string[]
}
/**
* Error `code` carried by the infrastructure error a provider throws when a
* confined policy is requested but no backend is available or usable on this
* host: confinement FAILS CLOSED (refuses to run) rather than silently
* executing unconfined. Thrown as a `HarnessError`, it reaches the model
* through the structured `{ name, code }` error channel on `tool/result`, so
* callers can distinguish "the sandbox is missing" from a failing command.
* Error code for a requested confined mode when no backend is usable. The
* provider fails closed, and `HarnessError` carries the code through
* `tool/result` so callers can distinguish missing confinement from command
* failure.
*/
export const SANDBOX_UNAVAILABLE = 'SANDBOX_UNAVAILABLE'
/**
* Thrown by {@link SandboxProvider.confine} when a confined policy is
* requested but no backend is usable on this host: confinement fails closed.
* Carries the {@link SANDBOX_UNAVAILABLE} code through the structured
* `{ name, code }` error channel.
* Thrown when {@link SandboxProvider.confine} cannot enforce the requested
* mode. Carries {@link SANDBOX_UNAVAILABLE} through the structured error
* channel.
*/
export class SandboxUnavailableError extends HarnessError {
constructor(mode: ConfinedSandboxMode, detail?: string) {
@@ -166,27 +114,10 @@ declare module 'cordis' {
}
/**
* Abstract process-sandbox service. Subclass, implement {@link confine}, and
* load the subclass as a plugin — it registers as `ctx.sandbox` (one
* implementation per context; loading a second throws, cordis' standard
* duplicate-service behavior).
*
* Semantics every implementation must honor:
* - {@link confine} either returns an argv whose runner ENFORCES the policy
* or fails closed — at `confine` time with {@link SandboxUnavailableError}
* (no backend for this host), or at EXECUTION time by the runner itself
* refusing to run the command (exiting without exec'ing it, identified by
* {@link ConfinedArgv.runnerFailureSignatures}). A silent unconfined
* passthrough is never a legal outcome on either path.
* - Probing exists to ARBITRATE between multiple candidate backends and may
* be skipped when a platform has exactly one: the sole candidate is
* selected directly and the runner's exec-time fail-closed refusal carries
* the safety property. When probing does run, it is functional (actually
* enforcing a profile, not a version check), at most once per provider
* lifetime; `confine` itself spawns nothing beyond that one-time probing.
* - The returned {@link ConfinedArgv.enforcement} states the backend's
* actual completeness for THIS host; `partial` is reported, never silently
* upgraded to `full`.
* Abstract process-sandbox service. {@link confine} must return enforcing argv
* or fail closed at wrap or runner-execution time; silent unconfined passthrough
* is forbidden. Functional probes arbitrate multi-runner chains and may be
* skipped for a sole candidate, whose own refusal remains the fail-closed end.
*/
export abstract class SandboxProvider extends Service {
constructor(ctx: Context) {