Merge branch 'codex/invariant-package-registration-gate' into codex/package-invariant-checks

This commit is contained in:
Tianyi Cui
2026-07-20 19:55:46 +08:00
398 changed files with 15485 additions and 8603 deletions

View File

@@ -1,6 +1,6 @@
# @deepseek-ai/dsh-bash-sandbox
Sandbox-consuming implementation of the [`@deepseek-ai/dsh-bash`](../bash/) executor seam. Load it **instead of** `@deepseek-ai/dsh-bash-local`, together with a [`ctx.sandbox`](../../sandbox/sandbox/) provider (e.g. [`@deepseek-ai/dsh-sandbox-local`](../../sandbox/sandbox-local/)) — no alternate tool plugin is needed; `dsh-tool-bash` detects the executor's `sandboxMode` capability and adds the escalation fields.
Sandbox-consuming implementation of the [`@deepseek-ai/dsh-bash`](../bash/) executor seam. Load it **instead of** `@deepseek-ai/dsh-bash-local`, together with a [`ctx.sandbox`](../../sandbox/sandbox/) provider (e.g. [`@deepseek-ai/dsh-sandbox-local`](../../sandbox/sandbox-local/)) and a [`ctx.sandboxPolicy`](../../sandbox/sandbox-policy/) (which owns the default mode + workspace root, shared with the sandboxed filesystem) — no alternate tool plugin is needed; `dsh-tool-bash` detects the executor's `sandboxMode` capability and adds the escalation fields.
The package root exports the default and named `SandboxBashExecutor` plugin plus its `Config`; quoting and result-classification helpers stay internal.
@@ -16,7 +16,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 `BashRunResult.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).
- **Runner failures are sandbox failures, never command failures.** Foreground execution throws `SANDBOX_UNAVAILABLE`; a settled background process stamps `process.sandbox.runnerFailed`, which the bash producer renders through generic `task_output`. Spawn failures also pass through settlement, so confined background handles retain their mode/enforcement facts and release per-process accounting.
- **Config-time default, per-call policy.** The DEFAULT mode is fixed by this entry's config for the executor's lifetime; `resolve()` stamps it onto every spec, and an explicit request-level `sandboxMode` override — set by the tool layer only for a call whose wider mode a human granted through `ctx.approval` ([the sandbox Agent Note § Escalation](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md)) — makes THAT call run, classify, and report under its own mode while every neighbor keeps the default (background facts are stamped per task at settle). The capability fact `ctx.bash.sandboxMode` reports the configured default so the tool layer advertises escalation only when this executor is mounted. The model learns of the sandbox only through result facts — the static bash tool description explains the denial marker; there is no current-mode statement in the system prompt.
- **Deployment default, per-call policy.** The DEFAULT mode + workspace root are owned by [`ctx.sandboxPolicy`](../../sandbox/sandbox-policy/) (one home both enforcing families read), not this executor's config; `resolve()` stamps the default onto every spec, and an explicit request-level `sandboxMode` override — set by the tool layer only for a call whose wider mode a human granted through `ctx.approval` ([the sandbox Agent Note § Escalation](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md)) — makes THAT call run, classify, and report under its own mode while every neighbor keeps the default (background facts are stamped per task at settle). The capability fact `ctx.bash.sandboxMode` reports the configured default so the tool layer advertises escalation only when this executor is mounted. The model learns of the sandbox only through result facts — the static bash tool description explains the denial marker; there is no current-mode statement in the system prompt.
- **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.
- 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/).
@@ -25,11 +25,13 @@ Deny-only at the seam: a denial is a reported fact, and this executor never nego
```yaml
- id: sandbox
name: '@deepseek-ai/dsh-sandbox-local'
- id: bash
name: '@deepseek-ai/dsh-bash-sandbox'
- id: sandbox-policy
name: '@deepseek-ai/dsh-sandbox-policy'
config:
mode: read-only
workspaceRoot: !!js process.cwd()
- id: bash
name: '@deepseek-ai/dsh-bash-sandbox'
```
The keyless consumer-integration proofs are `tests/bwrap.e2e.ts`, `tests/landlock.e2e.ts`, and `tests/seatbelt.e2e.ts` (the real provider + real runner driven through `ctx.bash`, world-verified, each self-skipping where its runner is absent); see [the acp-agent example's default composition](../../../examples/acp-agent/) for the runnable demo.

View File

@@ -31,17 +31,16 @@
"@deepseek-ai/dsh-bash-local": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"@deepseek-ai/dsh-sandbox": "^0.0.1",
"@deepseek-ai/dsh-sandbox-policy": "^0.0.1",
"cordis": "^4.0.0-rc.7"
},
"dependencies": {
"schemastery": "^3.18.0"
},
"devDependencies": {
"@deepseek-ai/dsh-bash": "workspace:^",
"@deepseek-ai/dsh-bash-local": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-sandbox": "workspace:^",
"@deepseek-ai/dsh-sandbox-local": "workspace:^",
"@deepseek-ai/dsh-sandbox-policy": "workspace:^",
"cordis": "^4.0.0-rc.7",
"node-addon-landlock-run": "0.0.0-test.0"
}

View File

@@ -7,52 +7,39 @@
* @module @deepseek-ai/dsh-bash-sandbox
*/
import { resolve } from 'node:path'
import { Context } from 'cordis'
import z from 'schemastery'
import type { BashExecRequest, BashExecSpec, BashProcess, BashRunResult } from '@deepseek-ai/dsh-bash'
import { SandboxUnavailableError } from '@deepseek-ai/dsh-sandbox'
import type { ConfinedSandboxMode, SandboxEnforcement, SandboxMode } from '@deepseek-ai/dsh-sandbox'
import type {} from '@deepseek-ai/dsh-sandbox-policy'
import { LocalBashExecutor } from '@deepseek-ai/dsh-bash-local'
import type { Config as LocalConfig } from '@deepseek-ai/dsh-bash-local'
import { classifyDenial, classifyRunnerFailure, matchesSignature, shellQuote } from './helpers.ts'
/**
* Plugin config: the local executor's knobs plus the sandbox policy. All
* optional — `static Config` supplies the defaults (`mode: 'read-only'` is the
* fail-safe default; an example that wants a workspace-writable agent opts in
* explicitly). The runner choice is not configured here: which platform
* backend confines the command is the `ctx.sandbox` provider's config.
* Plugin config: the local executor's knobs, verbatim. The sandbox policy
* the default mode and the `workspace-write` boundary root — is NOT here: it
* lives on `ctx.sandboxPolicy` (`@deepseek-ai/dsh-sandbox-policy`), the one
* home both enforcing families read, so bash and fs can never confine to
* different roots. The runner choice is likewise the `ctx.sandbox` provider's
* config, not this executor's.
*/
export interface Config extends LocalConfig {
/** File-sandbox mode commands run under (default: `read-only`). */
mode?: SandboxMode
/**
* Root directory `workspace-write` mode may write under (default: the
* executor's default working directory — `cwd`, else `process.cwd()`).
*/
workspaceRoot?: string
}
export type Config = LocalConfig
/**
* Registers as `ctx.bash` in place of the local executor and requires a
* `ctx.sandbox` provider; the tool layer is unchanged. The configured mode is
* the fallback, while a session override or approved one-shot escalation may
* select each call's mode. The prompt does not state the standing mode;
* `result.sandbox` reports the mode and enforcement actually used.
* `ctx.sandbox` provider plus `ctx.sandboxPolicy`; the tool layer is
* unchanged. The policy default (mode + workspace root) is the fallback,
* while a session override or approved one-shot escalation may select each
* call's mode. The prompt does not state the standing mode; `result.sandbox`
* reports the mode and enforcement actually used.
*/
export class SandboxBashExecutor extends LocalBashExecutor {
static inject = ['sandbox']
static inject = ['sandbox', 'sandboxPolicy']
// The sandbox-specific fields intersect the local executor's Config as an
// inline schema call: the config catalog walks `static Config` statically.
static override Config: z<Config> = z.intersect([
LocalBashExecutor.Config,
z.object({
mode: z.union(['read-only', 'workspace-write', 'danger-full-access'] as const).default('read-only'),
workspaceRoot: z.string(),
}),
])
// No own Config: the sandbox default (mode + workspaceRoot) moved to
// ctx.sandboxPolicy, so this executor inherits LocalBashExecutor's Config
// verbatim (the config catalog walks the inherited static).
private readonly mode: SandboxMode
private readonly workspaceRoot: string
@@ -71,9 +58,11 @@ export class SandboxBashExecutor extends LocalBashExecutor {
constructor(ctx: Context, config: Config) {
super(ctx, config)
// Schemastery fills mode before construction; workspaceRoot and cwd retain runtime fallbacks.
this.mode = config.mode as SandboxMode
this.workspaceRoot = resolve(config.workspaceRoot ?? config.cwd ?? process.cwd())
// The sandbox default (mode + workspaceRoot) is the one shared policy home
// both enforcing families read; injecting sandboxPolicy guarantees it is
// constructed first. workspaceRoot arrives already resolved absolute.
this.mode = ctx.sandboxPolicy.defaultMode
this.workspaceRoot = ctx.sandboxPolicy.workspaceRoot
}
/** The configured default mode — the capability fact the tool layer reads. */

View File

@@ -6,6 +6,7 @@ import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { LocalSandboxProvider } from '@deepseek-ai/dsh-sandbox-local'
import { SandboxPolicyService } from '@deepseek-ai/dsh-sandbox-policy'
import { bwrapProfileArgs } from '@deepseek-ai/dsh-sandbox-local/src/profiles.ts'
import { SandboxBashExecutor } from '@deepseek-ai/dsh-bash-sandbox'
@@ -40,7 +41,8 @@ async function tempDir(base: string): Promise<string> {
async function sandboxedBash(workspace: string, mode: 'read-only' | 'workspace-write'): Promise<SandboxBashExecutor> {
ctx = new Context()
await ctx.plugin(LocalSandboxProvider, {})
await ctx.plugin(SandboxBashExecutor, { mode, cwd: workspace, workspaceRoot: workspace, timeoutMs: 30_000 })
await ctx.plugin(SandboxPolicyService, { mode, workspaceRoot: workspace })
await ctx.plugin(SandboxBashExecutor, { cwd: workspace, timeoutMs: 30_000 })
return ctx.bash as SandboxBashExecutor
}

View File

@@ -7,6 +7,7 @@ import { afterEach, describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { launcherPath } from 'node-addon-landlock-run'
import { LocalSandboxProvider } from '@deepseek-ai/dsh-sandbox-local'
import { SandboxPolicyService } from '@deepseek-ai/dsh-sandbox-policy'
import { SandboxBashExecutor } from '@deepseek-ai/dsh-bash-sandbox'
/**
@@ -45,7 +46,8 @@ async function sandboxedBash(workspace: string, mode: 'read-only' | 'workspace-w
ctx = new Context()
await ctx.plugin(LocalSandboxProvider, {})
;(ctx.sandbox as LocalSandboxProvider).internals = { probeBwrap: () => false }
await ctx.plugin(SandboxBashExecutor, { mode, cwd: workspace, workspaceRoot: workspace, timeoutMs: 30_000 })
await ctx.plugin(SandboxPolicyService, { mode, workspaceRoot: workspace })
await ctx.plugin(SandboxBashExecutor, { cwd: workspace, timeoutMs: 30_000 })
return ctx.bash as SandboxBashExecutor
}

View File

@@ -12,7 +12,8 @@ import { describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import type { BashRunResult, CollectedOutput } from '@deepseek-ai/dsh-bash'
import { SANDBOX_UNAVAILABLE, SandboxProvider, SandboxUnavailableError } from '@deepseek-ai/dsh-sandbox'
import type { ConfinedArgv, SandboxPolicy } from '@deepseek-ai/dsh-sandbox'
import type { ConfinedArgv, SandboxMode, SandboxPolicy } from '@deepseek-ai/dsh-sandbox'
import { SandboxPolicyService } from '@deepseek-ai/dsh-sandbox-policy'
import { SandboxBashExecutor } from '@deepseek-ai/dsh-bash-sandbox'
import { classifyDenial, classifyRunnerFailure, shellQuote } from '../src/helpers.ts'
import type { Config } from '@deepseek-ai/dsh-bash-sandbox'
@@ -39,7 +40,11 @@ const passthrough = (argv: readonly string[]): ConfinedArgv =>
* Boot a context with a recording fake `ctx.sandbox` (behavior injectable
* per test) and the executor under test on top of it.
*/
async function setup(config: Config = {}, behavior: (argv: readonly string[], policy: SandboxPolicy) => ConfinedArgv = passthrough) {
async function setup(
config: { mode?: SandboxMode; workspaceRoot?: string } & Config = {},
behavior: (argv: readonly string[], policy: SandboxPolicy) => ConfinedArgv = passthrough,
) {
const { mode, workspaceRoot, ...execConfig } = config
const calls: ConfineCall[] = []
class FakeSandboxProvider extends SandboxProvider {
confine(argv: readonly string[], policy: SandboxPolicy): ConfinedArgv {
@@ -49,7 +54,11 @@ async function setup(config: Config = {}, behavior: (argv: readonly string[], po
}
const ctx = new Context()
await ctx.plugin(FakeSandboxProvider)
await ctx.plugin(SandboxBashExecutor, { graceMs: 200, ...config })
await ctx.plugin(SandboxPolicyService, {
...mode !== undefined ? { mode } : {},
...workspaceRoot !== undefined ? { workspaceRoot } : {},
})
await ctx.plugin(SandboxBashExecutor, { graceMs: 200, ...execConfig })
const bash = ctx.bash as SandboxBashExecutor
bash.internals = { spillDir }
return { ctx, bash, calls }
@@ -84,14 +93,14 @@ describe('the provider hand-off', () => {
expect(result.sandbox).toEqual({ mode: 'read-only', denied: false, enforcement: 'full' })
})
it('workspace-write rides the policy, workspaceRoot falling back to cwd when not configured', async () => {
const { bash, calls } = await setup({ mode: 'workspace-write', cwd: tmpdir() })
it('workspace-write rides the policy, workspaceRoot falling back to process.cwd() when not configured', async () => {
const { bash, calls } = await setup({ mode: 'workspace-write' })
const result = await bash.run(bash.resolve({ command: 'true' }))
expect(result.sandbox).toEqual({ mode: 'workspace-write', denied: false, enforcement: 'full' })
expect(calls[0]?.policy).toEqual({ mode: 'workspace-write', workspaceRoot: resolve(tmpdir()) })
expect(calls[0]?.policy).toEqual({ mode: 'workspace-write', workspaceRoot: resolve(process.cwd()) })
})
it('an explicit workspaceRoot wins over cwd', async () => {
it('an explicit workspaceRoot on the policy wins', async () => {
const { calls, bash } = await setup({ mode: 'workspace-write', workspaceRoot: '/ws', cwd: tmpdir() })
await bash.run(bash.resolve({ command: 'true' }))
expect(calls[0]?.policy.workspaceRoot).toBe(resolve('/ws'))

View File

@@ -6,6 +6,7 @@ import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { LocalSandboxProvider } from '@deepseek-ai/dsh-sandbox-local'
import { SandboxPolicyService } from '@deepseek-ai/dsh-sandbox-policy'
import { seatbeltProfileArgs } from '@deepseek-ai/dsh-sandbox-local/src/profiles.ts'
import { SandboxBashExecutor } from '@deepseek-ai/dsh-bash-sandbox'
@@ -39,7 +40,8 @@ async function sandboxedBash(workspace: string, mode: 'read-only' | 'workspace-w
ctx = new Context()
await ctx.plugin(LocalSandboxProvider, {})
;(ctx.sandbox as LocalSandboxProvider).internals = { probeBwrap: () => false, probeLandlock: () => 'unusable' }
await ctx.plugin(SandboxBashExecutor, { mode, cwd: workspace, workspaceRoot: workspace, timeoutMs: 30_000 })
await ctx.plugin(SandboxPolicyService, { mode, workspaceRoot: workspace })
await ctx.plugin(SandboxBashExecutor, { cwd: workspace, timeoutMs: 30_000 })
return ctx.bash as SandboxBashExecutor
}

View File

@@ -14,9 +14,6 @@
{
"path": "../../../vendor/cordis"
},
{
"path": "../../../vendor/schemastery"
},
{
"path": "../../util/brand"
},
@@ -26,6 +23,9 @@
{
"path": "../../sandbox/sandbox"
},
{
"path": "../../sandbox/sandbox-policy"
},
{
"path": "../../bash/bash"
},