1
/**2
* The sandbox POLICY home (`ctx.sandboxPolicy`): the single owner of the3
* deployment's sandbox fallbacks plus per-session resolution: the file-effect4
* {@link SandboxMode}, the `workspace-write` root, and the override kit (the5
* `sandbox/mode` event, its fold, and its write path; the fold is the6
* `sandboxMode` session-projection unit registered here, while the event and7
* write path come from `./session-mode.ts`).8
* Before each agent request, the owner also contributes the resolved policy to9
* the cache-safe runtime-context snapshot. The agent loop logs that snapshot as10
* model history, so replay reconstructs the same mode and root the enforcing11
* consumers resolve without rewriting the stable system prompt.12
*13
* Enforcing filesystem, one-shot bash, and terminal backends read the SAME14
* resolved policy here. The context describes that policy without inventorying15
* capabilities, while each backend retains its own enforcement dialect and each16
* tool owns its operation-specific denial and escalation guidance. The service17
* reads session state once at each operation boundary; executors and providers18
* remain session-free.19
*20
* @module @deepseek-ai/dsh-sandbox-policy21
*/23
import { isAbsolute } from 'node:path'24
import { Context, Service } from '@deepseek-ai/cordis'25
import { z as zod } from 'zod'26
import z from '@deepseek-ai/schemastery'27
import type {} from '@deepseek-ai/dsh-agent'28
import type { SandboxExecutionPolicy, SandboxMode } from '@deepseek-ai/dsh-sandbox'29
import type { Session } from '@deepseek-ai/dsh-session'30
import type {} from '@deepseek-ai/dsh-session-projection'31
import type {} from '@deepseek-ai/dsh-system-prompt'33
export { SANDBOX_MODES, setSandboxMode } from './session-mode.ts'35
/** Preserve execution-world spelling; enforcing providers resolve filesystem identity on their host. */36
function resolveWorkspaceRoot(path: string): string {37
if (!isAbsolute(path)) throw new Error('sandbox-policy: workspace root must be an absolute execution-world path')38
return path39
}41
/** Render the policy without claiming which capabilities are mounted. */42
function renderPolicyContext(policy: SandboxExecutionPolicy): string {43
switch (policy.mode) {44
case 'read-only':45
return 'Current DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns.'46
case 'workspace-write':47
return `Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: ${JSON.stringify(policy.workspaceRoot)}. Some platform temporary areas may also be writable.`48
case 'danger-full-access':49
return 'Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.'50
/* v8 ignore next 4 -- SandboxMode is a typed same-process closed union; this branch is only the static exhaustiveness guard. */51
default: {52
const mode: never = policy.mode53
throw new Error(`unreachable sandbox mode: ${String(mode)}`)54
}55
}56
}58
declare module '@deepseek-ai/cordis' {59
interface Context {60
sandboxPolicy: SandboxPolicyService61
}62
}64
/**65
* Plugin config: the deployment's sandbox default. All optional — `Config`66
* supplies the defaults (`mode: 'read-only'` is the fail-safe default; a67
* deployment that wants a workspace-writable agent opts in explicitly). The68
* runner choice is NOT here (it is the `ctx.sandbox` provider's config), nor69
* is any per-family knob: this is the one shared policy home.70
*/71
export interface Config {72
/** File-sandbox mode a session starts from (default: `read-only`). */73
mode?: SandboxMode74
/**75
* Absolute fallback root for agentless calls and sessions without a cwd (default:76
* `process.cwd()`). Normal agent calls use their session cwd instead.77
*/78
workspaceRoot?: string79
}81
/** Inputs that select the sandbox policy for one capability call. */82
export interface SandboxPolicyRequest {83
/** Calling session; its immutable cwd becomes the workspace boundary. */84
session?: Session85
/** Explicit approved mode override, which outranks session policy. */86
mode?: SandboxMode87
}89
/** The sandbox-mode projection's state schema (state equals the public shape). */90
const sandboxModeStateSchema = zod.union([91
zod.literal('read-only'),92
zod.literal('workspace-write'),93
zod.literal('danger-full-access'),94
]).nullable()96
type SandboxModeState = zod.infer<typeof sandboxModeStateSchema>97
declare module '@deepseek-ai/dsh-session-projection/types' {98
interface SessionProjectionStateMap {99
/** Last logged sandbox-mode override, or null before one (deployment default applies at resolve time). */100
sandboxMode: SandboxModeState101
}102
}104
/**105
* The sandbox-policy service (`ctx.sandboxPolicy`). Owns the deployment106
* default mode, fallback workspace root, and current request-time policy107
* section. Tool layers call {@link resolve} for each execution so a session's108
* mode log and immutable cwd travel together to every enforcing capability.109
*/110
export class SandboxPolicyService extends Service {111
// Inline schema call: the config catalog walks `static Config` statically.112
static Config: z<Config> = z.object({113
mode: z.union(['read-only', 'workspace-write', 'danger-full-access'] as const).default('read-only'),114
// No schema default: process.cwd() is resolved in the constructor so the115
// stored root is always absolute regardless of how it was supplied.116
workspaceRoot: z.string(),117
})119
static inject = ['sessionProjections']121
/** The deployment default mode — the fallback beneath a session override. */122
readonly defaultMode: SandboxMode123
/** The absolute `workspace-write` fallback root for calls without a session cwd. */124
readonly workspaceRoot: string125
constructor(ctx: Context, config: Config) {126
super(ctx, 'sandboxPolicy')127
// schemastery (static Config) already filled `mode`; the cast records that128
// runtime fact. `workspaceRoot` has NO schema default, so its fallback to129
// the process cwd is real branching, resolved absolute either way.130
this.defaultMode = config.mode as SandboxMode131
this.workspaceRoot = resolveWorkspaceRoot(config.workspaceRoot ?? process.cwd())133
ctx.sessionProjections.register({134
key: 'sandboxMode',135
stateVersion: 1,136
stateSchema: sandboxModeStateSchema,137
init: () => null,138
apply: (state, event) => (event.type === 'sandbox/mode' ? event.data.mode : state),139
})141
ctx.inject(['systemPrompt'], (scope: Context) => {142
scope.systemPrompt.context({143
name: 'sandbox:policy',144
order: scope.systemPrompt.getContextOrder('SANDBOX_POLICY'),145
text: (context) => {146
const session = context.agent?.session147
return session === undefined148
? ''149
: renderPolicyContext(this.resolve({ session }))150
},151
})152
})153
}155
/**156
* Resolve the complete policy for one capability call. An approved explicit157
* mode outranks the session's last `sandbox/mode` event, which outranks the158
* deployment default. A session cwd is its workspace-write boundary; the159
* configured root is the fallback for agentless calls and sessions without a160
* cwd.161
* @param request - optional session and approved mode override.162
* @returns the fully resolved per-call mode and absolute workspace root.163
*/164
resolve(request: SandboxPolicyRequest = {}): SandboxExecutionPolicy {165
const { session } = request166
return {167
mode: request.mode ?? (session === undefined ? undefined : this.overrideOf(session)) ?? this.defaultMode,168
workspaceRoot: resolveWorkspaceRoot(session?.header.cwd ?? this.workspaceRoot),169
...session === undefined ? {} : { sessionId: session.id },170
}171
}173
/**174
* Read the session override without applying the deployment default.175
* @param session - session whose log supplies the override.176
* @returns the last logged mode, or `undefined` without one.177
*/178
overrideOf(session: Session): SandboxMode | undefined {179
return this.ctx.sessionProjections.stateOf(session, 'sandboxMode') ?? undefined180
}181
}183
export default SandboxPolicyService