返回源码地图

packages/shell/bash-sandbox/src/index.ts

main snapshot · da00f7f5358f · 正文引用章节 10;完整原文可核对,不声称全文件人工逐行审计

完整原文供逐行核对;页面收录不代表每行都经过人工语义审核。MIT 许可见 许可证。

1/**
2 * Sandbox-consuming bash executor. It wraps the exact local bash argv through
3 * `ctx.sandbox`, inherits local process mechanics, and reports the selected
4 * mode, enforcement, and denial facts. Positive runner-executable evidence
5 * identifies a broken confinement runner: foreground calls throw
6 * `SANDBOX_UNAVAILABLE`, while background processes carry `runnerFailed`;
7 * other provider rejections retain stage-neutral local-executor semantics. The
8 * tool owns approval and passes a complete per-call policy.
9 * @module @deepseek-ai/dsh-bash-sandbox
10 */
11
12import { Context } from '@deepseek-ai/cordis'
13import type { ShellExecRequest, ShellExecSpec, ShellExecution, ShellProcess, ShellRunResult } from '@deepseek-ai/dsh-shell'
14import { SandboxUnavailableError } from '@deepseek-ai/dsh-sandbox'
15import type {
16 ConfinedArgv,
17 ConfinedSandboxMode,
18 RunnerFailureRule,
19 SandboxEnforcement,
20 SandboxExecutionPolicy,
21 SandboxMode,
22 SandboxPolicy,
23} from '@deepseek-ai/dsh-sandbox'
24import type {} from '@deepseek-ai/dsh-sandbox-policy'
25import { LocalBashExecutor } from '@deepseek-ai/dsh-bash-local'
26import type { Config as LocalConfig } from '@deepseek-ai/dsh-bash-local'
27import { classifyDenial, classifyRunnerFailure, isRunnerSpawnFailure, matchesSignature } from './helpers.ts'
28
29/**
30 * Plugin config: the local executor's knobs, verbatim. The sandbox policy —
31 * the default mode and fallback `workspace-write` root — is NOT here: it lives
32 * on `ctx.sandboxPolicy` (`@deepseek-ai/dsh-sandbox-policy`), which resolves
33 * each calling session's mode and cwd for every enforcing capability. The runner
34 * choice is likewise the `ctx.sandbox` provider's config, not this executor's.
35 */
36export type Config = LocalConfig
37
38/**
39 * Registers as `ctx.shell` in place of the local executor and requires a
40 * `ctx.sandbox` provider plus `ctx.sandboxPolicy`; the tool layer is
41 * unchanged. Tool calls pass the calling session's resolved policy; direct
42 * calls fall back to deployment policy. `result.sandbox` reports the mode and
43 * enforcement actually used.
44 */
45export class SandboxBashExecutor extends LocalBashExecutor {
46 static override inject = ['subprocess', 'sandbox', 'sandboxPolicy']
47
48 // No own Config: the sandbox default (mode + workspaceRoot) is owned by
49 // ctx.sandboxPolicy, so this executor inherits LocalBashExecutor's Config
50 // verbatim (the config catalog walks the inherited static).
51
52 private readonly mode: SandboxMode
53 /**
54 * Per-process confinement facts retained until settlement. Providers may
55 * vary enforcement and diagnostic dialect between overlapping calls, so a
56 * shared latest-wrap value would classify a process against the wrong facts.
57 * Unconfined processes have no entry.
58 */
59 private readonly processFacts = new Map<ShellProcess, {
60 mode: ConfinedSandboxMode
61 enforcement: SandboxEnforcement
62 denialSignatures: readonly string[]
63 runnerFailureRules: readonly RunnerFailureRule[]
64 runnerProgram: string | undefined
65 workdir: string
66 }>()
67
68 constructor(ctx: Context, config: Config) {
69 super(ctx, config)
70 // The default mode is the capability fact used for schema advertisement;
71 // actual tool executions carry their resolved per-call policy.
72 this.mode = ctx.sandboxPolicy.defaultMode
73 }
74
75 /** The configured default mode — the capability fact the tool layer reads. */
76 override get sandboxMode(): SandboxMode {
77 return this.mode
78 }
79
80 /**
81 * Stamp a complete per-call policy onto the spec. Tool calls supply the
82 * calling session's resolved mode and root; lower-level callers fall back to
83 * the deployment policy.
84 */
85 override resolve(request: ShellExecRequest): ShellExecSpec {
86 return { ...super.resolve(request), sandboxPolicy: request.sandboxPolicy ?? this.ctx.sandboxPolicy.resolve() }
87 }
88
89 override async execute(spec: ShellExecSpec): Promise<ShellExecution> {
90 const policy = spec.sandboxPolicy as SandboxExecutionPolicy
91 const { mode } = policy
92 if (mode === 'danger-full-access') {
93 return SandboxBashExecutor.decorateResult(
94 await super.execute(spec),
95 result => ({ ...result, sandbox: { mode, denied: false } }),
96 )
97 }
98 let confined: ConfinedArgv | undefined
99 const ex = await this.executeArgv(spec, async (signal) => {
100 const prepared = await this.confine(spec.command, { ...policy, mode }, signal)
101 signal.throwIfAborted()
102 confined = prepared
103 return prepared.argv
104 }, (process) => {
105 const facts = confined as ConfinedArgv
106 this.processFacts.set(process, {
107 mode,
108 enforcement: facts.enforcement,
109 denialSignatures: facts.denialSignatures,
110 runnerFailureRules: facts.runnerFailureRules,
111 runnerProgram: facts.argv[0],
112 workdir: spec.workdir,
113 })
114 })
115 return SandboxBashExecutor.decorateResult(ex, (result) => {
116 if (confined === undefined) return { ...result, sandbox: { mode, denied: false } }
117 const { enforcement, denialSignatures, runnerFailureRules } = confined
118 // Runner failure outranks denial because the command did not run. Carry
119 // the matched fatal line, not an informational line that preceded it.
120 const runnerFailure = classifyRunnerFailure(result.exitCode, result.stderr.text, runnerFailureRules)
121 if (runnerFailure !== undefined) {
122 throw new SandboxUnavailableError(mode, runnerFailure.detail)
123 }
124 return { ...result, sandbox: { mode, denied: classifyDenial(result, denialSignatures), enforcement } }
125 }, (error) => {
126 // An upstream abort remains cancellation even when it prevents spawn.
127 if (spec.signal?.aborted === true) spec.signal.throwIfAborted()
128 if (confined !== undefined && isRunnerSpawnFailure(error, confined.argv[0], spec.workdir)) {
129 throw new SandboxUnavailableError(mode, String(error))
130 }
131 throw error
132 })
133 }
134
135 /**
136 * Decorate the handle's foreground projection in place, memoized once. The
137 * handle keeps its identity (never wrapped in a second object) because the
138 * per-process facts and `onProcessDone` key on the exact instance.
139 */
140 private static decorateResult(
141 ex: ShellExecution,
142 map: (result: ShellRunResult) => ShellRunResult,
143 mapError?: (error: unknown) => never,
144 ): ShellExecution {
145 const base = ex.result.bind(ex)
146 let decorated: Promise<ShellRunResult> | undefined
147 ex.result = () => {
148 decorated ??= base().then(map, mapError)
149 return decorated
150 }
151 return ex
152 }
153
154 /**
155 * Stamp per-process sandbox facts before `done` settles. Full-access processes
156 * have no facts; signal deaths are not denials.
157 */
158 protected override onProcessDone(proc: ShellProcess, stderr: string, providerRejected: boolean, providerError?: unknown): void {
159 const facts = this.processFacts.get(proc)
160 if (facts !== undefined) {
161 this.processFacts.delete(proc)
162 // A provider rejection exposes no public failure stage. Attribute it to
163 // the confinement runner only when the error independently names argv[0].
164 // Otherwise settled runner failure outranks denial-like diagnostics.
165 const runnerFailed = providerRejected
166 ? isRunnerSpawnFailure(providerError, facts.runnerProgram, facts.workdir)
167 : classifyRunnerFailure(proc.exitCode, stderr, facts.runnerFailureRules) !== undefined
168 proc.sandbox = {
169 mode: facts.mode,
170 denied: !runnerFailed && matchesSignature(proc.exitCode, stderr, facts.denialSignatures),
171 enforcement: facts.enforcement,
172 ...(runnerFailed ? { runnerFailed } : {}),
173 }
174 }
175 super.onProcessDone(proc, stderr, providerRejected, providerError)
176 }
177
178 /**
179 * Wrap one shell command via the `ctx.sandbox` provider. Provider errors
180 * propagate unchanged; the returned argv is handed directly to the local
181 * executor's subprocess path.
182 * @param command - shell source for the confined inner `bash -c`.
183 * @param policy - resolved confined execution policy.
184 * @param signal - cancellation of confinement preparation.
185 * @returns the provider's exact argv and settlement-classification facts.
186 */
187 private confine(command: string, policy: SandboxPolicy, signal?: AbortSignal): Promise<ConfinedArgv> {
188 return this.ctx.sandbox.confine(['bash', '-c', command], policy, signal)
189 }
190}
191
192export default SandboxBashExecutor