mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
# Conflicts: # .agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.i18n.yaml # .agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.md # .agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.zh.md # examples/acp-agent/README.md # packages/examples/agent-spine-demo/package.json # packages/fs/fs-sandbox/src/index.ts # packages/fs/tool-fs-search/tests/tools.spec.ts # packages/support/acp-snapshot/README.md # packages/support/acp-snapshot/src/suite.ts # pnpm-lock.yaml # scripts/type-equiv.manifest.json
152 lines
6.1 KiB
TypeScript
152 lines
6.1 KiB
TypeScript
/**
|
|
* 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
|
|
*/
|
|
|
|
import { Context, Service } from 'cordis'
|
|
import { HarnessError } from '@deepseek-ai/dsh-llm'
|
|
|
|
export {
|
|
ESCALATION_TARGETS,
|
|
WIDER_MODES,
|
|
approveEscalation,
|
|
escalationHintMarker,
|
|
sandboxDenialMarker,
|
|
validateEscalationArgs,
|
|
} from './escalation.ts'
|
|
export type { EscalationApproval, EscalationApprover, EscalationOutcome, EscalationRequest } from './escalation.ts'
|
|
export { canonicalPath, writableRoots } from './roots.ts'
|
|
|
|
/**
|
|
* 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'
|
|
|
|
/** A confining (non-`danger-full-access`) mode — the modes a {@link SandboxPolicy} can carry. */
|
|
export type ConfinedSandboxMode = Exclude<SandboxMode, 'danger-full-access'>
|
|
|
|
/**
|
|
* The complete file-effect policy resolved for one capability call. The root
|
|
* is carried even under modes that do not consume it so callers can resolve
|
|
* policy once before choosing the enforcement path.
|
|
*/
|
|
export interface SandboxExecutionPolicy {
|
|
/** The file-effect mode this execution runs under. */
|
|
mode: SandboxMode
|
|
/** Absolute root directory `workspace-write` may write under. */
|
|
workspaceRoot: string
|
|
}
|
|
|
|
/**
|
|
* 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'
|
|
|
|
/**
|
|
* What one confined execution is allowed to touch — carried PER CALL, not
|
|
* fixed on the provider: two consumers may confine under different policies
|
|
* at the same instant (bash under `read-only` while a confined child agent
|
|
* needs its state directory writable), and an approved escalated retry is a
|
|
* new call with a wider policy. Defaulting/resolution is an explicit step at
|
|
* the consumer boundary; the provider treats the policy as fully specified.
|
|
*/
|
|
export interface SandboxPolicy extends SandboxExecutionPolicy {
|
|
/** The file-effect mode this execution runs under. */
|
|
mode: ConfinedSandboxMode
|
|
}
|
|
|
|
/**
|
|
* A {@link SandboxProvider.confine} result: the argv to spawn in place of
|
|
* the caller's own, plus the enforcement completeness the selected backend
|
|
* achieves for it.
|
|
*/
|
|
export interface ConfinedArgv {
|
|
/** The wrapped argv (runner, profile, separator, then the caller's argv). */
|
|
argv: string[]
|
|
/** How completely the selected backend enforces the policy's file effects. */
|
|
enforcement: SandboxEnforcement
|
|
/**
|
|
* The selected backend's denial DIALECT: the case-insensitive stderr
|
|
* substrings a file effect denied by THIS backend produces (EROFS text
|
|
* under bwrap's read-only binds, EACCES under Landlock, EPERM under
|
|
* Seatbelt). A consumer that infers denials from a failed run's stderr
|
|
* matches against exactly these rather than a cross-backend union — the
|
|
* union claims denials a given backend never produces.
|
|
*/
|
|
denialSignatures: readonly string[]
|
|
/**
|
|
* 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 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 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) {
|
|
super(
|
|
`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.'
|
|
+ (detail === undefined ? '' : ` Runner failure: ${detail}`),
|
|
SANDBOX_UNAVAILABLE,
|
|
)
|
|
this.name = 'SandboxUnavailableError'
|
|
}
|
|
}
|
|
|
|
declare module 'cordis' {
|
|
interface Context {
|
|
sandbox: SandboxProvider
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 {
|
|
/* v8 ignore next -- Windows has no sandbox backend to instantiate this service. */
|
|
constructor(ctx: Context) {
|
|
super(ctx, 'sandbox')
|
|
}
|
|
|
|
/**
|
|
* Wrap `argv` so it executes confined under `policy` on this host; the
|
|
* caller spawns the returned argv in place of its own.
|
|
* @param argv - the exact argv the caller is about to spawn (program plus
|
|
* arguments), NOT a shell string — a shell-shaped consumer passes
|
|
* `['bash', '-c', command]`.
|
|
* @param policy - the file-effect policy this execution runs under,
|
|
* carried per call (see {@link SandboxPolicy}).
|
|
* @returns the argv to spawn instead, plus the enforcement completeness
|
|
* the selected backend achieves for it.
|
|
*/
|
|
abstract confine(argv: readonly string[], policy: SandboxPolicy): ConfinedArgv
|
|
}
|
|
|
|
export default SandboxProvider
|