mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
Extend SandboxMode enforcement from bash to the filesystem tools, the sandbox RFC's deferred cross-family phase. - dsh-sandbox-policy (new, ctx.sandboxPolicy): the single home for the deployment default mode + workspaceRoot and the per-session override event, renamed bash/sandbox-mode -> sandbox/mode and moved here with its fold/setter. Decouples the bash seam from dsh-session. - dsh-fs-sandbox (new): SandboxedFileSystem extends LocalFileSystem and fences write/edit by the per-call mode (read-only denies, workspace-write contains to the workspace + temp roots via the shared writableRoots, danger passes through); reads pass through. Structured FS_SANDBOX_DENIED; in-lock parent re-canonicalization. A policy fence in trusted code, not a kernel boundary. - dsh-sandbox: the shared escalation kit (writableRoots, the strictly-wider ladder, denial/hint markers, approveEscalation) both tool families use; approveEscalation takes a structural approver so dsh-sandbox gains no approval/agent dependency, and both tools stay duplication-free. - tool-fs: write/edit advertise sandbox_permissions/justification under a confining ctx.fs, map FS_SANDBOX_DENIED to the shared [sandbox: ...] marker, and resolve the same one-approved-wider retry. - examples/acp-agent: composes sandbox-policy + fs-sandbox, drops the gating that disabled the fs stack under confined modes. RFC docs/rfc/implemented/feature/2026-07-14-cross-family-fs-sandbox.md; the old sandbox RFC's In-process/deferred/FAQ sections updated to shipped fact.
85 lines
3.6 KiB
TypeScript
85 lines
3.6 KiB
TypeScript
/**
|
|
* The sandbox POLICY home (`ctx.sandboxPolicy`): the single owner of the
|
|
* deployment's sandbox default — the file-effect {@link SandboxMode} a session
|
|
* starts from and the `workspace-write` boundary root — plus the per-session
|
|
* override kit (the `sandbox/mode` event, its fold, and its write path, from
|
|
* `./session-mode.ts`).
|
|
*
|
|
* Both enforcing capability families read the SAME policy here: the sandboxed
|
|
* bash executor (`@deepseek-ai/dsh-bash-sandbox`) and the sandboxed filesystem
|
|
* provider (`@deepseek-ai/dsh-fs-sandbox`) inject `ctx.sandboxPolicy` for the
|
|
* default mode and workspace root, so bash and fs can never confine to
|
|
* different roots — the split world the sandbox RFC warns about. The default
|
|
* lives here rather than on either executor's config precisely because it is
|
|
* one fact two families share.
|
|
*
|
|
* This service holds only the DEFAULT; the per-session fold
|
|
* ({@link effectiveSandboxMode}) is a pure function the tool layers apply to
|
|
* stamp each call, so neither the executor nor the provider depends on session
|
|
* events.
|
|
*
|
|
* @module @deepseek-ai/dsh-sandbox-policy
|
|
*/
|
|
|
|
import { resolve } from 'node:path'
|
|
import { Context, Service } from 'cordis'
|
|
import z from 'schemastery'
|
|
import type { SandboxMode } from '@deepseek-ai/dsh-sandbox'
|
|
|
|
export { SANDBOX_MODES, effectiveSandboxMode, setSandboxMode } from './session-mode.ts'
|
|
|
|
declare module 'cordis' {
|
|
interface Context {
|
|
sandboxPolicy: SandboxPolicyService
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Plugin config: the deployment's sandbox default. All optional — `Config`
|
|
* supplies the defaults (`mode: 'read-only'` is the fail-safe default; a
|
|
* deployment that wants a workspace-writable agent opts in explicitly). The
|
|
* runner choice is NOT here (it is the `ctx.sandbox` provider's config), nor
|
|
* is any per-family knob: this is the one shared policy home.
|
|
*/
|
|
export interface Config {
|
|
/** File-sandbox mode a session starts from (default: `read-only`). */
|
|
mode?: SandboxMode
|
|
/**
|
|
* Absolute root directory `workspace-write` may write under (default:
|
|
* `process.cwd()`). Both enforcing families fence against this SAME root.
|
|
*/
|
|
workspaceRoot?: string
|
|
}
|
|
|
|
/**
|
|
* The sandbox-policy service (`ctx.sandboxPolicy`). Owns the deployment
|
|
* default mode and workspace root; enforcing implementations read
|
|
* {@link defaultMode} and {@link workspaceRoot}, and the tool layers fold each
|
|
* session's `sandbox/mode` override with {@link effectiveSandboxMode} on top.
|
|
*/
|
|
export class SandboxPolicyService extends Service {
|
|
// Inline schema call: the config catalog walks `static Config` statically.
|
|
static Config: z<Config> = z.object({
|
|
mode: z.union(['read-only', 'workspace-write', 'danger-full-access'] as const).default('read-only'),
|
|
// No schema default: process.cwd() is resolved in the constructor so the
|
|
// stored root is always absolute regardless of how it was supplied.
|
|
workspaceRoot: z.string(),
|
|
})
|
|
|
|
/** The deployment default mode — the fallback beneath a session override. */
|
|
readonly defaultMode: SandboxMode
|
|
/** The absolute `workspace-write` boundary root both families fence against. */
|
|
readonly workspaceRoot: string
|
|
|
|
constructor(ctx: Context, config: Config) {
|
|
super(ctx, 'sandboxPolicy')
|
|
// schemastery (static Config) already filled `mode`; the cast records that
|
|
// runtime fact. `workspaceRoot` has NO schema default, so its fallback to
|
|
// the process cwd is real branching, resolved absolute either way.
|
|
this.defaultMode = config.mode as SandboxMode
|
|
this.workspaceRoot = resolve(config.workspaceRoot ?? process.cwd())
|
|
}
|
|
}
|
|
|
|
export default SandboxPolicyService
|