| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135 |
- /**
- * The sandbox-escalation surface shared by the `write` and `edit` tools: the
- * per-call mode stamp, the advertised escalation fields, and the denial-marker
- * mapping — all delegating the vocabulary and the fail-closed approval
- * sequence to `@deepseek-ai/dsh-sandbox` (the same pieces `@deepseek-ai/dsh-tool-bash`
- * uses), so bash and fs escalate identically. Built ONCE per plugin from
- * `ctx.fs.sandboxMode` (the capability fact — is a confining backend mounted?)
- * and shared by both mutating tools.
- *
- * @module @deepseek-ai/dsh-tool-fs/sandbox
- */
- import type { Context } from 'cordis'
- import type { ToolExecution } from '@deepseek-ai/dsh-tools'
- import type { SandboxMode } from '@deepseek-ai/dsh-sandbox'
- import { ESCALATION_TARGETS, approveEscalation, escalationHintMarker, sandboxDenialMarker, validateEscalationArgs } from '@deepseek-ai/dsh-sandbox'
- import { effectiveSandboxMode } from '@deepseek-ai/dsh-sandbox-policy'
- import { FsError } from '@deepseek-ai/dsh-fs'
- /** The two escalation arguments a mutating tool may carry (advertised only under a confining backend). */
- export interface FsEscalationArgs {
- sandbox_permissions?: string
- justification?: string
- }
- /** The schema fields for the escalation arguments, spread into a tool's `parameters` when a confining backend is mounted. */
- export interface EscalationSchemaFields {
- sandbox_permissions: { type: 'string'; enum: string[]; description: string }
- justification: { type: 'string'; description: string }
- }
- /**
- * The filesystem escalation surface: advertisement gating, per-call mode
- * stamping (folding the session's `sandbox/mode` override), the one-approved
- * wider retry, and denial-marker mapping. A pure product of `ctx` at plugin
- * apply time.
- */
- export class FsSandboxSurface {
- /** The escalation targets this composition advertises (`[]` when no confining backend is mounted). */
- readonly escalationModes: readonly SandboxMode[]
- /** The backend's default mode, or `undefined` when `ctx.fs` does not confine. */
- private readonly defaultMode: SandboxMode | undefined
- constructor(private readonly ctx: Context) {
- this.defaultMode = ctx.fs.sandboxMode
- this.escalationModes = this.defaultMode === undefined ? [] : ESCALATION_TARGETS
- }
- /**
- * The escalation schema fields for a mutating tool's `parameters`. Call it
- * only under a confining backend (guard on {@link escalationModes}); the
- * enum pins the closed target vocabulary, the strict-wider check happens per
- * call at execution.
- * @returns the two escalation parameter specs.
- */
- schemaFields(): EscalationSchemaFields {
- return {
- sandbox_permissions: {
- type: 'string',
- enum: [...this.escalationModes],
- description: 'The wider sandbox mode this file operation needs. Only valid as a one-shot retry '
- + 'of an operation the sandbox just denied; requires justification and user approval.',
- },
- justification: {
- type: 'string',
- description: 'Required with sandbox_permissions: one sentence for the user explaining '
- + 'why this exact file operation needs the wider access.',
- },
- }
- }
- /**
- * The session's standing mode override for an ordinary (non-escalating)
- * call — the `sandbox/mode` fold of the calling agent's log. Undefined for a
- * non-confining backend and for agent-less callers.
- */
- private sessionOverride(exec: ToolExecution): SandboxMode | undefined {
- if (this.defaultMode === undefined || exec.agent === undefined) return undefined
- return effectiveSandboxMode(exec.agent.session.events)
- }
- /**
- * The mode to STAMP onto this mutation: an approved escalation grant (a
- * strictly wider retry resolved through `ctx.approval` before anything
- * executes), else the session's standing override, else `undefined` (the
- * backend applies its own default). Validates the escalation argument
- * pairing first.
- * @param toolName - the mutating tool's name, for the approval audit trail.
- * @param args - the call's escalation arguments.
- * @param exec - the tool-execution context (agent, callId, signal).
- * @returns the mode to pass to the mutation, or undefined for the backend default.
- */
- async stampMode(toolName: string, args: FsEscalationArgs, exec: ToolExecution): Promise<SandboxMode | undefined> {
- validateEscalationArgs(args.sandbox_permissions, args.justification)
- if (args.sandbox_permissions === undefined || args.justification === undefined) {
- return this.sessionOverride(exec)
- }
- if (this.escalationModes.length === 0) {
- throw new Error('sandbox_permissions is not available in this composition (no sandboxing filesystem to escalate)')
- }
- const effectiveMode = (this.sessionOverride(exec) ?? this.defaultMode) as SandboxMode
- return approveEscalation(
- { requestedMode: args.sandbox_permissions, justification: args.justification, effectiveMode, subject: 'operation' },
- {
- approver: this.ctx.get('approval'),
- agent: exec.agent,
- callId: exec.callId,
- toolName,
- ...exec.signal ? { signal: exec.signal } : {},
- },
- )
- }
- /**
- * Map a thrown provider error for the model: a `FS_SANDBOX_DENIED` becomes a
- * `FsError` whose text is the shared `[sandbox: …]` denial marker plus the
- * same-turn escalation hint, so a policy denial reads identically to bash's
- * WHILE keeping the structured `FS_SANDBOX_DENIED` code — `ToolRegistry`
- * populates `result.error` only for `HarnessError` instances, so a plain
- * `Error` would strip the code retry/observers key off. Any other error
- * passes through unchanged. A `FS_SANDBOX_DENIED` only arises under a
- * confining backend, which always advertises the escalation fields, so the
- * hint always applies here.
- * @param error - the error thrown by the mutation.
- * @param stampedMode - the mode stamped onto the call (names the mode in the marker).
- * @returns the error to throw — the marker `FsError` for a sandbox denial, else the original.
- */
- mapError(error: unknown, stampedMode: SandboxMode | undefined): unknown {
- if (!(error instanceof FsError) || error.code !== 'FS_SANDBOX_DENIED') return error
- // A FS_SANDBOX_DENIED only arises under a confining backend, so defaultMode
- // (hence the resolved mode) is defined here.
- const mode = (stampedMode ?? this.defaultMode) as SandboxMode
- return new FsError(`${sandboxDenialMarker(mode)}\n${escalationHintMarker('operation')}`, 'FS_SANDBOX_DENIED', { cause: error })
- }
- }
|