Merge remote-tracking branch 'origin/master' into codex/pr-555-ci-fix

# Conflicts:
#	docs/module-graph.i18n.yaml
#	docs/module-graph.md
#	docs/module-graph.zh.md
#	packages/client/ui-conversation/src/client/input/hub.ts
#	packages/client/ui-conversation/tests/input-bar.spec.tsx
#	packages/client/ui-conversation/tests/service-orchestration.spec.ts
This commit is contained in:
creatixchu
2026-08-10 15:19:32 +08:00
259 changed files with 8182 additions and 626 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "@deepseek-ai/dsh-sandbox-local",
"description": "Local process-sandbox backends for the DeepSeek Harness sandbox seam: bwrap, the npm-distributed landlock-run launcher, or macOS Seatbelt — functionally probed, fail-closed",
"description": "Local process-sandbox backends for the DeepSeek Harness sandbox seam: bwrap, the npm-distributed landlock-run launcher, macOS Seatbelt, or the Windows ACL restricted-token runner — functionally probed, fail-closed",
"version": "0.0.1",
"private": true,
"type": "module",
@@ -28,9 +28,11 @@
"@deepseek-ai/dsh-invariants": "^0.0.1",
"@deepseek-ai/dsh-llm": "^0.0.1",
"@deepseek-ai/dsh-sandbox": "^0.0.1",
"@deepseek-ai/dsh-session": "^0.0.1",
"cordis": "^4.0.0-rc.7"
},
"dependencies": {
"@deepseek-ai/dsh-sandbox-windows-acl": "workspace:^",
"@deepseek-ai/node-addon-landlock-run": "workspace:*",
"schemastery": "^3.18.0"
},
@@ -38,6 +40,7 @@
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-sandbox": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"cordis": "^4.0.0-rc.7"
}
}

View File

@@ -1,12 +1,29 @@
/**
* Local sandbox backend. It selects the platform runner chain (Linux bwrap then
* Landlock; macOS Seatbelt), functionally probes competing candidates once, and
* reports each wrap's enforcement and stderr classification facts. Missing or unusable
* confinement fails closed rather than returning the original argv.
* Landlock; macOS Seatbelt; Windows the ACL restricted-token runner), functionally probes
* competing candidates once, and reports each wrap's enforcement and stderr
* classification facts. Missing or unusable confinement fails closed rather
* than returning the original argv.
*
* The windows-acl rung additionally owns the write grants: the write SID is
* the per-WORKSPACE identity derived from the canonical workspace path
* (`workspaceWriteSid`), and the private temp subdirectory is DERIVED per
* session (session id + workspace — nothing stored). The
* workspace-root ACE materializes once per workspace per server lifetime
* and STANDS (the cross-session reuse cache — the exact-ACE skip makes
* every later provision O(1) instead of re-propagating the tree per
* session); the private-temp ACEs are revoked on dispose. The runner
* receives `--write-sid` (the derived identity; its presence marks the
* seam-managed contract) and stops managing DACLs itself.
* @module @deepseek-ai/dsh-sandbox-local
*/
import { spawnSync } from 'node:child_process'
import { createHash } from 'node:crypto'
import { existsSync, mkdirSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import {
LAUNCHER_BIN,
LAUNCHER_FAILURE_EXIT,
@@ -18,6 +35,8 @@ import z from 'schemastery'
import { assertNever } from '@deepseek-ai/dsh-llm'
import { SandboxProvider, SandboxUnavailableError } from '@deepseek-ai/dsh-sandbox'
import type { ConfinedArgv, ConfinedSandboxMode, RunnerFailureRule, SandboxEnforcement, SandboxPolicy } from '@deepseek-ai/dsh-sandbox'
import type { SessionId } from '@deepseek-ai/dsh-session'
import { AclWriteGrant, workspaceWriteSid } from '@deepseek-ai/dsh-sandbox-windows-acl'
import { bwrapProfileArgs, landlockProfileArgs, seatbeltProfileArgs } from './profiles.ts'
/** Plugin config. All optional — `static Config` supplies the defaults. */
@@ -70,6 +89,46 @@ function defaultProbeSeatbelt(seatbeltExec: string, timeoutMs: number): boolean
return probe.status === 0
}
/**
* Functional windows-acl probe: run the runner in read-only mode (zero grants,
* no ACL mutation) around `cmd /c exit 0` — exit 0 means the runner created
* the restricted token and spawned the child under it. The win32 chain is a
* sole candidate, so the product never probes; the probe exists for override
* chains and mirrors the other rungs' shape.
*/
function defaultProbeWindowsAcl(runnerInvocation: string[], timeoutMs: number): boolean {
const program = runnerInvocation[0]
if (program === undefined) return false
const probe = spawnSync(program, [
...runnerInvocation.slice(1),
'--workspace', tmpdir(), '--temp', tmpdir(), '--mode', 'read-only',
'--', 'cmd', '/c', 'exit', '0',
], {
timeout: timeoutMs,
stdio: 'ignore',
})
return probe.status === 0
}
/**
* The session's private temp subdirectory: `<tmpdir>\dsh-<16 hex>`, derived
* from the session id and its workspace instead of stored. The same session
* and workspace always name the same directory — a resumed session
* re-grants it (the exact-ACE skip keeps that O(1)) — while a fork's
* different session id names a fresh one. The name is predictable to anyone
* who knows the session id (the confined command sees it as
* `DSH_SESSION_ID`), so the provider creates the directory EXCLUSIVELY and
* rejects reparse points: a pre-placed entry fails the first confined run
* loudly, and cannot redirect the grant onto a foreign object.
* @param sessionId - the policy's calling-session identity.
* @param workspaceRoot - the resolved policy root.
* @returns the session's private temp subdirectory path.
*/
export function sessionTempDir(sessionId: SessionId, workspaceRoot: string): string {
const digest = createHash('sha256').update(String(sessionId)).update('\0').update(workspaceRoot).digest('hex')
return join(tmpdir(), `dsh-${digest.slice(0, 16)}`)
}
/** Test hook: inject probe verdicts / a fake launcher / a platform without real runners. */
export interface SandboxInternals {
/** Replaces `process.platform` for chain selection (exercise any platform's chain from any host). */
@@ -86,10 +145,18 @@ export interface SandboxInternals {
landlockLauncher?: string
/** Replaces the `sandbox-exec` executable the probe and wraps invoke (a fake script). */
seatbeltExec?: string
/** Replaces the resolved windows-acl runner argv prefix (a fake runner). */
windowsAclRunnerArgs?: string[]
/** Replaces the resolved windows-acl runner built entry path (a fake lib/runner.js location). */
windowsAclRunnerEntry?: string
/** Replaces the functional windows-acl probe (the win32 chain's sole rung — only consulted if that chain ever grows). */
probeWindowsAcl?: () => boolean
/** Replaces the private-temp-directory removal at provider dispose (a throwing fake exercises the cleanup-failure path). */
rmTempDir?: (path: string) => void
}
/** The chain's verdict: which runner confines, and how completely it enforces. */
type SelectedRunner = { runner: 'bwrap' | 'landlock' | 'seatbelt'; enforcement: SandboxEnforcement }
type SelectedRunner = { runner: 'bwrap' | 'landlock' | 'seatbelt' | 'windows-acl'; enforcement: SandboxEnforcement }
/**
* The runner chain per platform — selection is BY PLATFORM first, probes
@@ -103,11 +170,10 @@ type SelectedRunner = { runner: 'bwrap' | 'landlock' | 'seatbelt'; enforcement:
const PLATFORM_CHAINS: Record<string, readonly SelectedRunner['runner'][]> = {
linux: ['bwrap', 'landlock'],
darwin: ['seatbelt'],
// Reserved slot, deliberately empty: Windows support fills it with a confinement runner
// (AppContainer / restricted-token family, shipped from its own repository on the
// landlock-run template) plus a SelectedRunner['runner'] union member — the switches'
// assertNever guards then walk the implementer to every site.
win32: [],
// The Windows restricted-token runner (@deepseek-ai/dsh-sandbox-windows-acl):
// a sole candidate, selected without a probe — its execution-time refusal
// fails closed through its stderr signature (windows-acl-run:) and exit 127.
win32: ['windows-acl'],
}
/**
@@ -123,6 +189,13 @@ const STATIC_ENFORCEMENT: Record<SelectedRunner['runner'], SandboxEnforcement> =
bwrap: 'full',
landlock: 'full',
seatbelt: 'full',
// 'full' is the SUPPORTED-SURFACE promise: on NTFS both restricting lists
// close every ambient write (INTERACTIVE/LOCAL and Authenticated Users are
// absent from both — pinned by the runner's Public-probe and CIM-denial
// regressions). FAT-class (non-ACL) targets are declared unsupported
// (warn-only) in the backend README — outside the promise, not an
// exception to it.
'windows-acl': 'full',
}
/**
@@ -145,15 +218,26 @@ const DENIAL_SIGNATURES = {
bwrap: ['read-only file system'],
landlock: ['permission denied'],
seatbelt: ['operation not permitted'],
// pwsh/.NET: "Access to the path '...' is denied."; cmd: "Access is denied.";
// node EACCES: "permission denied".
'windows-acl': ['access is denied', 'access to the path', 'permission denied'],
runnerCommand: ['read-only file system', 'permission denied'],
} as const satisfies Record<SelectedRunner['runner'] | 'runnerCommand', readonly string[]>
/** The windows-acl runner's documented failure exit (its own RUNNER_FAILURE_EXIT contract, distinct from Landlock's 125). */
const WINDOWS_ACL_RUNNER_FAILURE_EXIT = 127
/**
* Runner-owned fatal diagnostics. Landlock has a versioned exit-125 plus
* fatal-line launcher-failure contract. Bubblewrap's current fatal paths exit
* 1 but its public contract does not reserve that status, while sandbox-exec
* publishes no launcher-failure status; those backends remain signature-only.
* Keep the Landlock tuple aligned with the assembled snapshot fixture at
* The windows-acl runner prints `windows-acl-run: <detail>` on every
* runner-side failure and exits 127 — the rule is exit-gated on that status
* so a confined command that merely PRINTS the signature (or a runner
* cleanup failure reported on a non-zero child exit) is never misclassified
* as "the command did not run". Keep the Landlock tuple aligned with the
* assembled snapshot fixture at
* `examples/acp-agent/tests/fixtures/partial-landlock-sandbox.ts`.
*/
const RUNNER_FAILURE_RULES = {
@@ -164,12 +248,15 @@ const RUNNER_FAILURE_RULES = {
informationalLines: [`${LAUNCHER_BIN}: partial enforcement (older Landlock ABI)`],
}],
seatbelt: [{ fatalSignatures: ['sandbox-exec: '] }],
'windows-acl': [{ allowedExitCodes: [WINDOWS_ACL_RUNNER_FAILURE_EXIT], fatalSignatures: ['windows-acl-run: '] }],
} as const satisfies Record<SelectedRunner['runner'], readonly RunnerFailureRule[]>
/**
* Local process-sandbox provider. Registers as `ctx.sandbox`. Stateless
* apart from the cached chain verdict — it spawns nothing but the one-time
* probes, so there is no disposal work beyond cordis' own.
* Local process-sandbox provider. Registers as `ctx.sandbox`. Caches the
* chain verdict and, on the windows-acl rung, the write grants
* ({@link AclWriteGrant}: the standing workspace-root grant per workspace
* and the revocable private-temp grant per session, the latter revoked on
* provider dispose); the one-time probes spawn nothing else.
*/
export class LocalSandboxProvider extends SandboxProvider {
// Inline schema call: the config catalog walks `static Config` statically.
@@ -187,6 +274,16 @@ export class LocalSandboxProvider extends SandboxProvider {
private readonly probeTimeoutMs: number
/** Cached chain verdict; undefined until the first confined wrap needs it. */
private selectedRunner: SelectedRunner | 'unavailable' | undefined
/**
* Server-lifetime write grants (windows-acl rung): the STANDING
* workspace-root grant per workspace (its ACE is the cross-session reuse
* cache and outlives the provider — never revoked) and the REVOCABLE
* private-temp grant per session (revoked on provider dispose).
*/
private readonly workspaceGrants = new Map<string, AclWriteGrant>()
private readonly tempGrants = new Map<string, AclWriteGrant>()
/** Session id → the private temp directory this provider created (removed on dispose). */
private readonly tempDirs = new Map<string, string>()
constructor(ctx: Context, config: Config) {
super(ctx)
@@ -208,6 +305,13 @@ export class LocalSandboxProvider extends SandboxProvider {
this.configuredRunnerFailureSignatures = runnerFailureSignatures
this.probeTimeoutMs = config.probeTimeoutMs as number
assertPositiveFinite('probeTimeoutMs', this.probeTimeoutMs)
// The temp grants are revoked with the provider: a clean server
// shutdown leaves no temp ACEs behind (workspace ACEs stand by design —
// the reuse cache; an unclean shutdown leaves them for the next
// provision's exact-ACE skip).
ctx.effect(() => () => {
this.revokeAclGrants()
})
}
/**
@@ -246,10 +350,154 @@ export class LocalSandboxProvider extends SandboxProvider {
case 'bwrap': return ['bwrap', ...bwrapProfileArgs(policy)]
case 'landlock': return [this.landlockLauncher(), ...landlockProfileArgs(policy)]
case 'seatbelt': return [this.seatbeltExec(), ...seatbeltProfileArgs(policy)]
case 'windows-acl': return this.windowsAclRunnerArgv(policy)
default: return assertNever(runner)
}
}
/**
* The windows-acl runner argv for one policy. With a calling session (the
* policy's `sessionId`), the write grants are materialized once per server
* lifetime — the standing workspace-root grant per workspace and the
* revocable private-temp grant per session — and the runner receives
* `--write-sid` (the workspace-derived identity; its presence marks the
* seam-managed DACL contract) plus, under workspace-write, the session's
* PRIVATE temp subdirectory (derived from session id + workspace) — it
* grants nothing and revokes nothing. Agentless calls pass the ambient
* temp root and no `--write-sid`: the runner self-manages its DACLs.
* @param policy - the resolved per-call policy.
* @returns the runner invocation.
*/
private windowsAclRunnerArgv(policy: SandboxPolicy): string[] {
const sessionId = policy.sessionId
if (sessionId === undefined) {
return [
...this.windowsAclRunnerInvocation(),
'--workspace', policy.workspaceRoot,
'--temp', tmpdir(),
'--mode', policy.mode,
]
}
this.materializeAclGrant(sessionId, policy.workspaceRoot, policy.mode)
return [
...this.windowsAclRunnerInvocation(),
'--workspace', policy.workspaceRoot,
// Workspace-write sessions confine their temp writes to the PRIVATE
// per-session subdirectory (bwrap --tmpfs /tmp semantics); read-only
// runs pass the ambient temp root — the runner validates it exists
// but grants nothing. The derived write SID is the per-workspace
// identity; the flag's presence marks the seam-managed DACL contract.
'--temp', policy.mode === 'workspace-write' ? sessionTempDir(sessionId, policy.workspaceRoot) : tmpdir(),
'--mode', policy.mode,
'--write-sid', workspaceWriteSid(policy.workspaceRoot),
]
}
/**
* Materialize the session's ACEs once per server lifetime: lazily at its
* first confined execution, reused for every later call (the map hits are
* the whole call). The write SID is the per-workspace identity derived
* from the workspace. Workspace-write grants the workspace root STANDING
* (the ACE outlives every session — the reuse cache) and the session's
* private temp subdirectory REVOCABLY — the directory is derived from
* session id + workspace, created here EXCLUSIVELY (a pre-existing entry
* or a reparse point fails the first confined run loudly, so the grant
* never lands on a foreign object); read-only materializes NOTHING — its
* token alone restricts every write, and the standing grant from an
* earlier workspace-write period is KEPT through a downgrade (never
* revoked): the read-only restricted token carries no write SID (the
* read-only list), so the ACE is inert there, while the map hit keeps the
* re-upgrade free of re-propagation. Fail-closed: a half-materialized
* temp grant is revoked before the error propagates.
* @param sessionId - the policy's calling-session identity.
* @param workspaceRoot - the resolved policy root.
* @param mode - the policy mode (grants exist only under workspace-write).
*/
private materializeAclGrant(sessionId: SessionId, workspaceRoot: string, mode: ConfinedSandboxMode): void {
if (mode === 'read-only') return
const writeSid = workspaceWriteSid(workspaceRoot)
const tempDir = sessionTempDir(sessionId, workspaceRoot)
if (!this.workspaceGrants.has(workspaceRoot)) {
const grant = AclWriteGrant.create(writeSid)
try {
grant.add(workspaceRoot, true)
} catch (error) {
// Free the SID; a standing ACE (if the apply succeeded before a
// post-apply throw) is the intended end state, not an error
// artifact — nothing to revoke.
try {
grant.dispose()
} catch (cleanupError) {
throw new AggregateError([error, cleanupError], 'sandbox-local windows-acl workspace grant failed and its cleanup also failed')
}
throw error
}
this.workspaceGrants.set(workspaceRoot, grant)
}
if (this.tempGrants.has(sessionId)) return
const grant = AclWriteGrant.create(writeSid)
// The directory is removed again in the catch only when THIS confine
// created it — a pre-existing entry (EEXIST) is a foreign object and is
// never deleted.
let created = false
try {
// Exclusive creation (no `recursive`): a pre-existing entry OR a
// reparse point both fail EEXIST — the grant never lands on a foreign
// object.
mkdirSync(tempDir)
created = true
grant.add(tempDir)
} catch (error) {
if (created) rmSync(tempDir, { recursive: true, force: true })
// Revoke whatever stands and free the SID — never leave a half-grant
// behind a failed confine (the runner never runs).
try {
grant.dispose()
} catch (cleanupError) {
throw new AggregateError([error, cleanupError], 'sandbox-local windows-acl temp grant materialization failed and its cleanup also failed')
}
throw error
}
this.tempGrants.set(sessionId, grant)
this.tempDirs.set(sessionId, tempDir)
}
/**
* Dispose every write grant (provider dispose): the revocable temp ACEs
* are revoked, the private temp directories this provider created are
* removed, and every SID allocation is freed; the standing workspace ACEs
* stay (the reuse cache). Cleanup failures are reported, not thrown:
* cordis teardown must not be aborted by grant cleanup. A crash skips all
* of it — the next resume then fails loudly at the exclusive creation and
* OS temp hygiene (or manual removal) recovers.
*/
private revokeAclGrants(): void {
if (this.workspaceGrants.size === 0 && this.tempGrants.size === 0) return
const failures: unknown[] = []
for (const grant of [...this.workspaceGrants.values(), ...this.tempGrants.values()]) {
try {
grant.dispose()
} catch (error) {
failures.push(error)
}
}
const rmTempDir = this.internals.rmTempDir ?? ((dir: string) => { rmSync(dir, { recursive: true, force: true }) })
for (const dir of this.tempDirs.values()) {
try {
rmTempDir(dir)
} catch (error) {
failures.push(error)
}
}
this.workspaceGrants.clear()
this.tempGrants.clear()
this.tempDirs.clear()
if (failures.length > 0) {
this.ctx.logger.warn(`sandbox-local: windows-acl grant cleanup completed with ${failures.length} failure(s)`)
for (const error of failures) this.ctx.logger.warn(error)
}
}
/**
* Resolve which runner confines commands, once, for the provider's
* lifetime: this platform's chain ({@link PLATFORM_CHAINS}), its sole
@@ -296,6 +544,11 @@ export class LocalSandboxProvider extends SandboxProvider {
const probe = this.internals.probeSeatbelt ?? (exec => defaultProbeSeatbelt(exec, this.probeTimeoutMs))
return probe(this.seatbeltExec()) ? 'full' : 'unusable'
}
case 'windows-acl': {
const probe = this.internals.probeWindowsAcl
?? (() => defaultProbeWindowsAcl(this.windowsAclRunnerInvocation(), this.probeTimeoutMs))
return probe() ? 'full' : 'unusable'
}
default: return assertNever(runner)
}
}
@@ -309,6 +562,21 @@ export class LocalSandboxProvider extends SandboxProvider {
private seatbeltExec(): string {
return this.internals.seatbeltExec ?? 'sandbox-exec'
}
/**
* The windows-acl runner argv prefix: the built lib/runner.js entry when
* present (production), else the package source through tsx (development).
* The prefix stays `[node, runner, ...]` — a future native-exe runner keeps
* the same argv contract and only swaps these entries.
*/
private windowsAclRunnerInvocation(): string[] {
const override = this.internals.windowsAclRunnerArgs
if (override !== undefined) return override
const builtEntry = this.internals.windowsAclRunnerEntry ?? fileURLToPath(import.meta.resolve('@deepseek-ai/dsh-sandbox-windows-acl/runner'))
if (existsSync(builtEntry)) return [process.execPath, builtEntry]
const sourceEntry = fileURLToPath(import.meta.resolve('@deepseek-ai/dsh-sandbox-windows-acl/src/runner.ts'))
return [process.execPath, '--import', 'tsx/esm', sourceEntry]
}
}
export default LocalSandboxProvider

View File

@@ -0,0 +1,404 @@
/**
* windows-acl write grants: the SERVER-LIFETIME ACE materialization
* (standing workspace grant per workspace, revocable private-temp grant per
* session) plus the derived private-temp identity, through the REAL
* LocalSandboxProvider.confine(). Win32 surface mocked at the package
* boundary (the workspace-derived SID mocked to a constant); the real-FFI
* grant behavior lives in sandbox-windows-acl's win32 tests.
*/
import { existsSync, mkdirSync, mkdtempSync, rmSync, symlinkSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { basename, join } from 'node:path'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import type { SandboxPolicy } from '@deepseek-ai/dsh-sandbox'
import { SessionId } from '@deepseek-ai/dsh-session'
import { LocalSandboxProvider, sessionTempDir } from '@deepseek-ai/dsh-sandbox-local'
/** Cross-file state shared with the vi.mock factory (hoisting contract). */
const mockState = vi.hoisted(() => ({
grants: [] as Array<{ writeSid: string; added: Array<{ path: string; standing: boolean }>; disposed: boolean }>,
addFailure: undefined as Error | undefined,
/** Restricts {@link addFailure} to this path (undefined = every add throws). */
addFailurePath: undefined as string | undefined,
disposeFailure: undefined as Error | undefined,
}))
vi.mock('@deepseek-ai/dsh-sandbox-windows-acl', () => {
class MockAclWriteGrant {
readonly writeSid: string
readonly added: Array<{ path: string; standing: boolean }> = []
disposed = false
constructor(writeSid: string) {
this.writeSid = writeSid
mockState.grants.push(this)
}
static create(writeSid: string): MockAclWriteGrant {
return new MockAclWriteGrant(writeSid)
}
add(path: string, standing = false): void {
if (mockState.addFailure !== undefined && (mockState.addFailurePath === undefined || mockState.addFailurePath === path)) {
throw mockState.addFailure
}
this.added.push({ path, standing })
}
dispose(): void {
if (mockState.disposeFailure !== undefined) throw mockState.disposeFailure
this.disposed = true
}
}
return { AclWriteGrant: MockAclWriteGrant, workspaceWriteSid: () => 'S-1-4-42-42' }
})
/** The workspace-derived write SID the mock pins for every workspace. */
const DERIVED_SID = 'S-1-4-42-42'
async function setup() {
const ctx = new Context()
const fiber = await ctx.plugin(LocalSandboxProvider, {})
const sandbox = ctx.sandbox as LocalSandboxProvider
sandbox.internals = { platform: 'win32', windowsAclRunnerArgs: ['node', 'windows-acl-runner.js'] }
return { ctx, sandbox, fiber }
}
/** A workspace root the policy carries. */
function workspaceRoot(): string {
return mkdtempSync(join(tmpdir(), 'dsh-acl-grants-ws-'))
}
describe('windows-acl write grants (LocalSandboxProvider)', () => {
const scratch: string[] = []
beforeEach(() => {
mockState.grants = []
mockState.addFailure = undefined
mockState.addFailurePath = undefined
mockState.disposeFailure = undefined
})
const cleanup = () => {
for (const dir of scratch.splice(0)) rmSync(dir, { recursive: true, force: true })
}
it('workspace-write: first confine materializes ONCE (standing workspace + revocable private temp), the derived temp dir rides the argv', async () => {
try {
const { sandbox, fiber } = await setup()
const ws = workspaceRoot()
scratch.push(ws)
const tempDir = sessionTempDir(SessionId('sess-1'), ws)
scratch.push(tempDir)
const policy: SandboxPolicy = { mode: 'workspace-write', workspaceRoot: ws, sessionId: SessionId('sess-1') }
const confined = sandbox.confine(['pwsh', '/Command', 'x'], policy)
expect(confined.argv).toEqual([
'node', 'windows-acl-runner.js',
'--workspace', ws,
'--temp', tempDir,
'--mode', 'workspace-write',
'--write-sid', DERIVED_SID,
'--',
'pwsh', '/Command', 'x',
])
expect(mockState.grants).toHaveLength(2)
expect(mockState.grants[0]).toMatchObject({
writeSid: DERIVED_SID,
added: [{ path: ws, standing: true }], // standing: the reuse cache, never revoked
disposed: false,
})
expect(mockState.grants[1]).toMatchObject({
writeSid: DERIVED_SID,
added: [{ path: tempDir, standing: false }],
disposed: false,
})
expect(existsSync(tempDir)).toBe(true) // created exclusively
// Reuse: the second confine is the map hits.
sandbox.confine(['pwsh', '/Command', 'x'], policy)
expect(mockState.grants).toHaveLength(2)
await fiber.dispose()
// dispose() runs on BOTH grants: the standing workspace ACE is left in
// place (the mock marks it disposed only as instance teardown).
expect(mockState.grants[0]!.disposed).toBe(true)
expect(mockState.grants[1]!.disposed).toBe(true)
} finally {
cleanup()
}
})
it('mode switch: read-only materializes nothing, the upgrade materializes ONCE with the derived SID, the downgrade keeps the standing grant', async () => {
try {
const { sandbox } = await setup()
const ws = workspaceRoot()
scratch.push(ws)
const tempDir = sessionTempDir(SessionId('sess-switch'), ws)
scratch.push(tempDir)
const readOnly: SandboxPolicy = { mode: 'read-only', workspaceRoot: ws, sessionId: SessionId('sess-switch') }
const workspaceWrite: SandboxPolicy = { mode: 'workspace-write', workspaceRoot: ws, sessionId: SessionId('sess-switch') }
// read-only first: nothing materialized, ambient temp.
const confinedRo = sandbox.confine(['true'], readOnly)
expect(confinedRo.argv).toEqual([
'node', 'windows-acl-runner.js',
'--workspace', ws,
'--temp', tmpdir(), // NOT the private subdir: read-only grants nothing
'--mode', 'read-only',
'--write-sid', DERIVED_SID,
'--',
'true',
])
expect(mockState.grants).toHaveLength(0)
expect(existsSync(tempDir)).toBe(false)
// Upgrade: first workspace-write materializes with the derived SID.
const upgraded = sandbox.confine(['true'], workspaceWrite)
expect(upgraded.argv).toEqual([
'node', 'windows-acl-runner.js',
'--workspace', ws,
'--temp', tempDir,
'--mode', 'workspace-write',
'--write-sid', DERIVED_SID,
'--',
'true',
])
expect(mockState.grants).toHaveLength(2)
expect(mockState.grants[0]).toMatchObject({ writeSid: DERIVED_SID, added: [{ path: ws, standing: true }], disposed: false })
expect(mockState.grants[1]).toMatchObject({
writeSid: DERIVED_SID,
added: [{ path: tempDir, standing: false }],
disposed: false,
})
expect(existsSync(tempDir)).toBe(true)
// Reuse: map hits.
sandbox.confine(['true'], workspaceWrite)
expect(mockState.grants).toHaveLength(2)
// Downgrade: standing grant KEPT (inert under read-only, free re-upgrade).
sandbox.confine(['true'], readOnly)
expect(mockState.grants).toHaveLength(2)
expect(mockState.grants[0]!.disposed).toBe(false)
} finally {
cleanup()
}
})
it('resume: a fresh provider derives the SAME temp dir for the same session and workspace and re-grants it', async () => {
try {
const ws = workspaceRoot()
scratch.push(ws)
const first = await setup()
const policy: SandboxPolicy = { mode: 'workspace-write', workspaceRoot: ws, sessionId: SessionId('resumed') }
const firstConfined = first.sandbox.confine(['true'], policy)
expect(mockState.grants).toHaveLength(2)
// Clean restart: dispose revokes the temp ACE and removes the private
// temp directory, so the fresh provider's exclusive creation succeeds.
await first.fiber.dispose()
mockState.grants = []
const second = await setup()
const secondConfined = second.sandbox.confine(['true'], policy)
expect(secondConfined.argv).toEqual(firstConfined.argv)
expect(mockState.grants).toHaveLength(2)
expect(mockState.grants[1]).toMatchObject({
writeSid: DERIVED_SID,
added: [{ path: sessionTempDir(SessionId('resumed'), ws), standing: false }],
})
await second.fiber.dispose()
} finally {
cleanup()
}
})
it('fork: a different session id derives a DIFFERENT private temp identity over the same workspace', async () => {
try {
const { sandbox } = await setup()
const ws = workspaceRoot()
scratch.push(ws)
const parentPolicy: SandboxPolicy = { mode: 'workspace-write', workspaceRoot: ws, sessionId: SessionId('parent') }
const childPolicy: SandboxPolicy = { mode: 'workspace-write', workspaceRoot: ws, sessionId: SessionId('child') }
sandbox.confine(['true'], parentPolicy)
const parentTemp = sessionTempDir(SessionId('parent'), ws)
scratch.push(parentTemp)
sandbox.confine(['true'], childPolicy)
const childTemp = sessionTempDir(SessionId('child'), ws)
scratch.push(childTemp)
// Fresh temp identity, NOT the parent's (the workspace SID is shared by
// derivation — the workspace is the same, so the standing grant is the
// map hit and only the child's temp grant joins).
expect(childTemp).not.toBe(parentTemp)
expect(mockState.grants).toHaveLength(3)
expect(mockState.grants[2]).toMatchObject({ added: [{ path: childTemp, standing: false }] })
} finally {
cleanup()
}
})
it('creates the private temp dir EXCLUSIVELY: a pre-existing entry or a reparse point fails EEXIST, never receiving the temp grant', async () => {
try {
const { sandbox } = await setup()
const ws = workspaceRoot()
scratch.push(ws)
// Pre-existing entry: exclusive mkdir throws EEXIST instead of adopting it.
const preexisting = sessionTempDir(SessionId('preexisting'), ws)
mkdirSync(preexisting)
scratch.push(preexisting)
const prePolicy: SandboxPolicy = { mode: 'workspace-write', workspaceRoot: ws, sessionId: SessionId('preexisting') }
expect(() => sandbox.confine(['true'], prePolicy)).toThrow(/EEXIST/)
// The standing workspace grant is the intended end state and stays; the
// failed temp grant self-disposes.
expect(mockState.grants).toHaveLength(2)
expect(mockState.grants[0]!.disposed).toBe(false)
expect(mockState.grants[1]!.disposed).toBe(true) // self-revoked
// Reparse point: same EEXIST (exclusive mkdir never follows links).
const target = mkdtempSync(join(tmpdir(), 'dsh-acl-junction-target-'))
scratch.push(target)
const linkPath = sessionTempDir(SessionId('reparse'), ws)
symlinkSync(target, linkPath)
scratch.push(linkPath)
const linkPolicy: SandboxPolicy = { mode: 'workspace-write', workspaceRoot: ws, sessionId: SessionId('reparse') }
expect(() => sandbox.confine(['true'], linkPolicy)).toThrow(/EEXIST/)
// Same workspace as the preexisting case: the standing workspace grant
// is the map hit (not recreated) — only the failed temp grant joins.
expect(mockState.grants).toHaveLength(3)
expect(mockState.grants[2]!.disposed).toBe(true)
// Temp-side cleanup failure: the standing workspace grant stays (map
// hit), the exclusive mkdir fails, AND the temp grant's dispose also
// fails — the temp cleanup AggregateError propagates.
mockState.grants = []
mockState.disposeFailure = new Error('temp cleanup exploded')
const dupTemp = sessionTempDir(SessionId('temp-cleanup-fail'), ws)
mkdirSync(dupTemp)
scratch.push(dupTemp)
const dupPolicy: SandboxPolicy = { mode: 'workspace-write', workspaceRoot: ws, sessionId: SessionId('temp-cleanup-fail') }
expect(() => sandbox.confine(['true'], dupPolicy)).toThrow(/temp grant materialization failed and its cleanup also failed/)
expect(mockState.grants).toHaveLength(1) // only the failed temp grant (the workspace grant was the map hit)
} finally {
cleanup()
}
})
it('a grant failure mid-materialization disposes the failed grant and rethrows (AggregateError when the cleanup also fails)', async () => {
try {
const { sandbox } = await setup()
const ws = workspaceRoot()
scratch.push(ws)
scratch.push(sessionTempDir(SessionId('sess-add-fail'), ws))
const policy: SandboxPolicy = { mode: 'workspace-write', workspaceRoot: ws, sessionId: SessionId('sess-add-fail') }
// add() throws on the FIRST (workspace) grant: cleanup dispose() runs, original error propagates.
mockState.addFailure = new Error('grant exploded')
expect(() => sandbox.confine(['true'], policy)).toThrow('grant exploded')
expect(mockState.grants).toHaveLength(1)
expect(mockState.grants[0]!.disposed).toBe(true)
// add() AND dispose() both throw: AggregateError.
mockState.grants = []
mockState.addFailure = new Error('grant exploded again')
mockState.disposeFailure = new Error('cleanup exploded')
expect(() => sandbox.confine(['true'], policy)).toThrow(AggregateError)
} finally {
cleanup()
}
})
it('a temp add failure after the exclusive mkdir removed the half-created directory again', async () => {
try {
const { sandbox } = await setup()
const ws = workspaceRoot()
scratch.push(ws)
const tempDir = sessionTempDir(SessionId('sess-temp-add-fail'), ws)
const policy: SandboxPolicy = { mode: 'workspace-write', workspaceRoot: ws, sessionId: SessionId('sess-temp-add-fail') }
// The workspace grant succeeds; only the TEMP grant's add throws (the
// path-targeted failure keeps the workspace branch intact).
mockState.addFailurePath = tempDir
mockState.addFailure = new Error('temp add exploded')
expect(() => sandbox.confine(['true'], policy)).toThrow('temp add exploded')
expect(existsSync(tempDir)).toBe(false) // the half-created directory is removed again
expect(mockState.grants).toHaveLength(2)
expect(mockState.grants[0]!.disposed).toBe(false) // the standing workspace grant stays
expect(mockState.grants[1]!.disposed).toBe(true) // the failed temp grant self-disposes
} finally {
cleanup()
}
})
it('agentless calls stay self-managed: no --write-sid, the ambient temp root, no grants', async () => {
try {
const { sandbox, fiber } = await setup()
const policy: SandboxPolicy = { mode: 'workspace-write', workspaceRoot: '/ws' }
const confined = sandbox.confine(['pwsh', '/Command', 'x'], policy)
expect(confined.argv).toEqual([
'node', 'windows-acl-runner.js',
'--workspace', '/ws',
'--temp', tmpdir(),
'--mode', 'workspace-write',
'--',
'pwsh', '/Command', 'x',
])
expect(mockState.grants).toHaveLength(0)
await fiber.dispose()
} finally {
cleanup()
}
})
it('a failing dispose at provider teardown is reported via ctx.logger.warn and never thrown into teardown', async () => {
try {
const { ctx, sandbox, fiber } = await setup()
const ws = workspaceRoot()
scratch.push(ws)
scratch.push(sessionTempDir(SessionId('sess-dispose'), ws))
const policy: SandboxPolicy = { mode: 'workspace-write', workspaceRoot: ws, sessionId: SessionId('sess-dispose') }
sandbox.confine(['true'], policy)
expect(mockState.grants).toHaveLength(2)
mockState.disposeFailure = new Error('revoke exploded')
const warn = vi.spyOn(ctx.logger, 'warn').mockImplementation(() => undefined)
await fiber.dispose()
// BOTH grants (standing workspace + revocable temp) fail their dispose.
expect(warn).toHaveBeenCalledWith(expect.stringContaining('cleanup completed with 2 failure(s)'))
expect(warn).toHaveBeenCalledWith(expect.objectContaining({ message: 'revoke exploded' }))
} finally {
cleanup()
}
})
it('a failing private-temp removal at provider teardown is reported via ctx.logger.warn and never thrown into teardown', async () => {
try {
const { ctx, sandbox, fiber } = await setup()
const ws = workspaceRoot()
scratch.push(ws)
scratch.push(sessionTempDir(SessionId('sess-rm-fail'), ws))
const policy: SandboxPolicy = { mode: 'workspace-write', workspaceRoot: ws, sessionId: SessionId('sess-rm-fail') }
sandbox.confine(['true'], policy)
expect(mockState.grants).toHaveLength(2)
sandbox.internals.rmTempDir = () => { throw new Error('rm exploded') }
const warn = vi.spyOn(ctx.logger, 'warn').mockImplementation(() => undefined)
await fiber.dispose()
// Both grants dispose cleanly; only the directory removal fails.
expect(warn).toHaveBeenCalledWith(expect.stringContaining('cleanup completed with 1 failure(s)'))
expect(warn).toHaveBeenCalledWith(expect.objectContaining({ message: 'rm exploded' }))
} finally {
cleanup()
}
})
it('sessionTempDir derives the same well-shaped name for the same session and workspace, distinct otherwise', () => {
const base = sessionTempDir(SessionId('sess-a'), '/ws/a')
expect(basename(base)).toMatch(/^dsh-[0-9a-f]{16}$/)
expect(sessionTempDir(SessionId('sess-a'), '/ws/a')).toBe(base)
expect(sessionTempDir(SessionId('sess-b'), '/ws/a')).not.toBe(base) // different session
expect(sessionTempDir(SessionId('sess-a'), '/ws/b')).not.toBe(base) // different workspace
// The separator prevents id/workspace collisions from merging inputs.
expect(sessionTempDir(SessionId('ab'), '/ws/c')).not.toBe(sessionTempDir(SessionId('a'), '/ws/bc'))
})
})

View File

@@ -209,13 +209,10 @@ describe('the platform chains', () => {
expect(probeSeatbelt).not.toHaveBeenCalled()
})
it('win32 is a reserved EMPTY chain: fails closed identically until a Windows runner fills it', async () => {
// The slot exists so Windows support is an additive fill-in (chain entry
// + runner union member), never a redesign — and reserving it must not
// weaken the fail-closed end in the meantime.
const { sandbox } = await setup({}, { platform: 'win32' })
expect(() => sandbox.confine(['true'], RO)).toThrow(expect.objectContaining({ code: SANDBOX_UNAVAILABLE }))
})
// The win32 chain's argv contract, denial dialect, and runner-failure rules
// live in @deepseek-ai/dsh-sandbox-windows-acl/tests/provider-chain.spec.ts
// (platform-independent assertions that run in every CI lane, including
// Windows where this package's POSIX-only suites are excluded).
it('caches the verdict for the provider lifetime: one chain walk across wraps', async () => {
const probeBwrap = vi.fn(() => true)
@@ -368,3 +365,63 @@ describe('the default seatbelt probe (sandbox-exec contract)', () => {
expect(() => sandbox.confine(['true'], RO)).toThrow(expect.objectContaining({ code: SANDBOX_UNAVAILABLE }))
})
})
describe('the windows-acl probe (runner invocation contract)', () => {
// The product chain reaches windows-acl only unprobed (win32's sole
// candidate), so the probe case and the runner-entry resolution are pinned
// through the chain seam, mirroring the seatbelt default-probe contract.
it('selects the rung when the injected probe passes, speaking the ACL dialect', async () => {
const probeWindowsAcl = vi.fn(() => true)
const { sandbox } = await setup({}, {
chain: ['windows-acl', 'bwrap'],
probeWindowsAcl,
probeBwrap: () => false,
windowsAclRunnerArgs: ['node', 'windows-acl-runner.js'],
})
const confined = sandbox.confine(['true'], RO)
expect(probeWindowsAcl).toHaveBeenCalledTimes(1)
expect(confined.argv.slice(-4)).toEqual(['--mode', 'read-only', '--', 'true'])
expect(confined.enforcement).toBe('full')
expect(confined.denialSignatures).toEqual(['access is denied', 'access to the path', 'permission denied'])
expect(confined.runnerFailureRules).toEqual([{ allowedExitCodes: [127], fatalSignatures: ['windows-acl-run: '] }])
})
it('reads a failing probe as unusable and walks to the next rung', async () => {
const probeWindowsAcl = vi.fn(() => false)
const { sandbox } = await setup({}, { chain: ['windows-acl', 'bwrap'], probeWindowsAcl, probeBwrap: () => true })
const confined = sandbox.confine(['true'], RO)
expect(confined.argv[0]).toBe('bwrap')
expect(probeWindowsAcl).toHaveBeenCalledTimes(1)
})
it('runs the REAL default probe against the resolved runner invocation when none is injected', async () => {
// The default probe spawns the exact runner argv confine would use — the
// runner source through tsx on a lib-less checkout. The windows-acl
// runner cannot init off win32, so the probe reads unusable and the walk
// falls through to the injected bwrap verdict on every host.
const { sandbox } = await setup({}, { chain: ['windows-acl', 'bwrap'], probeBwrap: () => true })
const confined = sandbox.confine(['true'], RO)
expect(confined.argv[0]).toBe('bwrap')
}, 30_000)
it('reads an empty runner invocation as unusable (the probe\'s empty-argv guard)', async () => {
// windowsAclRunnerInvocation always yields [node, ...] in product; an
// override returning [] exercises the default probe's empty-argv guard.
const { sandbox } = await setup({}, { chain: ['windows-acl', 'bwrap'], probeBwrap: () => true, windowsAclRunnerArgs: [] })
const confined = sandbox.confine(['true'], RO)
expect(confined.argv[0]).toBe('bwrap')
})
it('prefers the built lib/runner.js entry when the resolved file exists', async () => {
const dir = mkdtempSync(join(tmpdir(), 'dsh-fake-acl-entry-'))
const builtEntry = join(dir, 'runner.js')
writeFileSync(builtEntry, '')
const { sandbox } = await setup({}, {
chain: ['windows-acl', 'bwrap'],
probeWindowsAcl: () => true,
windowsAclRunnerEntry: builtEntry,
})
const confined = sandbox.confine(['true'], RO)
expect(confined.argv.slice(0, 2)).toEqual([process.execPath, builtEntry])
})
})

View File

@@ -28,6 +28,10 @@ const platformPackageName = `@deepseek-ai/node-addon-landlock-run-linux-${proces
/** The harness closure the consumer needs; native tarballs are packed through their mode-preserving release script. */
const WORKSPACE_CLOSURE = [
'packages/sandbox/sandbox-local',
// sandbox-local's win32 chain rung is a runtime dependency: a packed
// consumer resolves it like any other @deepseek-ai peer (koffi arrives
// from the registry).
'packages/sandbox/sandbox-windows-acl',
'packages/sandbox/sandbox',
'packages/llm/llm',
'packages/attachment/attachment',

View File

@@ -26,6 +26,12 @@
{
"path": "../sandbox"
},
{
"path": "../sandbox-windows-acl"
},
{
"path": "../../core/session"
},
{
"path": "../../support/invariants"
}