DeepSeek Harness 沙箱策略上下文注入:面向 Prompt 缓存安全(Cache-safe)的当前策略快照设计
【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness
导读
DeepSeek Harness 的沙箱策略(sandbox policy)决定每个会话中文件写入类操作的实际权限,但在早期设计中,这一状态只被强制执行与记录,模型发起的新请求里并不包含它,导致 Web 会话在read-only模式下仍能看到 write/edit 工具 schema,模型可能宣称自己能写文件、直到第一次调用被拒绝才得知真相。本篇技术指南基于仓库内的实现决策文档 2026-07-30-current-sandbox-policy-context.md 及其后续简化决策 2026-07-31-capability-neutral-sandbox-policy-context.md,系统讲解 DeepSeek Harness 如何通过dsh-sandbox-policy包向模型注入能力中立(capability-neutral)的sandbox:policy运行时上下文,以及 agent-loop 如何把它物化为追加式(append-only)的完整快照,从而在策略切换后仍保持稳定系统提示词前缀、最大化 KV-Cache 复用。读完本文,你将掌握沙箱三种模式的语义与精确措辞、会话级模式切换的持久化机制、缓存安全快照的投射与清理逻辑,以及该设计在真实模型提供方上的缓存命实验证。
问题背景:策略被强制执行,却不被模型感知
在引入本次机制之前,DeepSeek Harness 的沙箱策略已经能够强制执行并记录每个会话的文件效果模式(file-effect mode),但存在一个关键缺口:一次全新的模型请求并不携带该状态。
具体表现为三类典型场景(见 决策文档 的 Problem 一节):
- Web 会话处于
read-only时:write 与 edit 工具的 schema 仍然可见。模型会据此宣称自己具备写入能力,直到某次写调用被沙箱拒绝,才通过 denial(拒绝结果)间接学习到真实策略——这是一种"先产生虚假能力声明、后靠报错纠偏"的被动机制。 - 执行
/permission danger-full-access切换后:下一次请求确实携带了审批策略(approval policy)的变更,但沙箱模式仍然缺失,模型无法感知这次权限提升。 - 用户在任何操作之前询问能力时:denial 结果成为模型可见的第一个策略来源,即使模型从未发起过任何操作。
核心矛盾在于:工具 schema 描述的是"可以调用什么",而沙箱策略描述的是"在什么文件效果约束下执行",两者是正交的两个信息面。方案的目标是让模型在探测任何工具之前就收到当前生效的文件策略,同时不让这条信息破坏系统提示词的缓存复用。
核心设计:dsh-sandbox-policy包与sandbox:policy上下文
包职责与注册方式
沙箱策略的单一所有者是dsh-sandbox-policy包(仓库路径 packages/sandbox/sandbox-policy),其插件入口位于 src/index.ts。根据源码注释,它是"部署级沙箱回退策略加上每次会话解析"的唯一所有者,负责:
- 部署默认模式(
mode)与回退工作区根(workspaceRoot); - 每次会话的解析:文件效果模式、
workspace-write根,以及覆盖套件(sandbox/mode事件及其 fold 与写入路径,来自 src/session-mode.ts); - 在每次 agent 请求前,将已解析的策略贡献给缓存安全的运行时上下文快照。
在构造函数中,服务通过ctx.inject(['systemPrompt'], ...)注册一个名为sandbox:policy、order: 110的动态上下文贡献(src/index.ts#L112-L123):
ctx.inject(['systemPrompt'], (scope: Context) => { scope.systemPrompt.context({ name: 'sandbox:policy', order: 110, text: (context) => { const session = context.agent?.session return session === undefined ? '' : renderPolicyContext(this.resolve({ session })) }, }) })关键点在于:文本只从resolve({ session })派生,每次请求组装时直接以活动会话解析当前策略,不存在"denial 历史扫描"或"进程内 last-told 状态"(见决策文档 Decision 一节)。同时order: 110与审批策略贡献的order: 115(见 packages/interaction/user-approval/src/index.ts#L170-L181)一起,参与系统提示词组装器中"有序动态上下文"的排序。
三种模式的精确措辞
renderPolicyContext(src/index.ts#L38-L52)按模式返回固定的能力中立语句,只陈述所有执行方言(enforcement dialect)共享的事实,不枚举已挂载的能力:
| 模式 | 模型可见文本(源码原文) | 语义要点 |
|---|---|---|
read-only | 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. | 反预拒绝(anti-refusal)原则:仅凭该标签不得推断操作不可能,应正常尝试可用工具,再遵循工具返回的拒绝与升级指导 |
workspace-write | Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: "<workspaceRoot>". Some platform temporary areas may also be writable. | 以非排他措辞(non-exclusive wording)声明规范会话工作区;临时区域只概括、不枚举 |
danger-full-access | Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations. | 文件沙箱不限制可用操作的修改能力 |
值得注意的边界(决策文档 Decision 一节明确列出):后端选择的临时路径、/dev/null、runner 就绪状态、精确工具可用性及其他策略域,均不出现,因为resolve()在请求组装阶段无法确定这些信息。例如workspace-write只是概括"某些平台临时区域可能可写",而不会列出具体路径——不同后端(bwrap、Landlock、Seatbelt、进程内文件系统 fence)授予的临时路径集合在confine()时才选定(见决策文档 Alternatives 中"Enumerate writable temporary roots"被拒绝的原因)。
能力中立原则:为什么不做能力清单
最初的实现曾通过独立的 enforced-family 与 escalatable-family 两套注册表,由六个后端/工具/示例调用点贡献filesystem、bash、terminal家族,策略服务需要维护 token 集合、交叉取交并、排序,并在每次生命周期变更时失效 prompt 组装,还要测试每一种家族组合。
后续简化决策 2026-07-31-capability-neutral-sandbox-policy-context.md 判定该清单既不必要、也不权威:后端贡献的家族可能与模型可见工具不一致(工具可能被请求作用域隐藏),而工具 schema 已经精确告知模型哪些操作可用。因此:
- 工具 schema 是"哪些操作可用"的权威;
- 工具结果是"特定操作的拒绝与批准后的重试"的权威;
- 策略上下文只负责陈述当前策略,且措辞对可用操作作条件化表述("任何被 DSH 文件沙箱强制执行的可用操作……"),即使没有适用操作,语句依然真实。
能力的新增与移除不再扰动运行时上下文快照,只有模式与工作区变化才会(见简化决策 Consequences 一节)。
会话级模式切换:sandbox/mode事件与 fold
事件即状态
会话的模式切换由 src/session-mode.ts 提供完整的读写套件,其核心思想是**"切换本身就是事件"**:
export function setSandboxMode(session: Session, mode: SandboxMode): void { session.append('sandbox/mode', { mode }) }sandbox/mode是仅日志(log-only)事件(与approval/*先例一致):不携带surfaceOp、不出现在模型转写中,但可持久化、可重放。source: 'delegation'标记表示委派给子会话时注入的覆盖,缺省 source 表示运行时切换。
会话的覆盖模式由纯函数 fold 得出(src/session-mode.ts#L52-L58):
export function effectiveSandboxMode(events: readonly SessionEvent[]): SandboxMode | undefined { for (let index = events.length - 1; index >= 0; index -= 1) { const event = events[index] as SessionEvent if (event.type === 'sandbox/mode') return event.data.mode } return undefined }即取事件日志中最后一个sandbox/mode事件。这个设计带来三个可验证的性质(源码注释与测试共同印证):
- 重启后覆盖仍生效:重放日志即是状态,无需任何追赶机制;
- 会话间隔离:两个会话永不互见对方状态;
- 无外部配置存储:模式是策略状态,被 bash 与 filesystem 等所有执行家族共享,因此放在策略包中而非某个能力 seam 内。
解析优先级
resolve()(src/index.ts#L135-L142)返回单次能力调用所需的完整策略:
resolve(request: SandboxPolicyRequest = {}): SandboxExecutionPolicy { const { session } = request return { mode: request.mode ?? (session === undefined ? undefined : this.overrideOf(session)) ?? this.defaultMode, workspaceRoot: resolveWorkspaceRoot(session?.header.cwd ?? this.workspaceRoot), ...session === undefined ? {} : { sessionId: session.id }, } }优先级自高到低为:
- 批准的显式模式(
request.mode,如升级授予)——只对该次调用生效; - 会话最后记录的
sandbox/mode事件(通过overrideOf→effectiveSandboxMode); - 部署默认模式(
defaultMode,默认为read-only)。
工作区根则是:会话的不可变SessionHeader.cwd优先,否则回退到配置的workspaceRoot(默认process.cwd())。注意resolveWorkspaceRoot(src/index.ts#L33-L35)先用canonicalPath解析文件系统身份再resolve词法路径,以避免 symlink 敏感组件在词法规范化前被抹掉——测试 policy.spec.ts#L87-L108 验证了symlink/..这种场景会解析为物理真实路径。
配置参数
来自 packages/sandbox/sandbox-policy/README.md 的最小配置示例:
- name: '@deepseek-ai/dsh-sandbox-policy' config: mode: workspace-write workspaceRoot: /absolute/path/to/workspace| 字段 | 默认值 | 含义 |
|---|---|---|
mode | read-only | 会话起步的部署默认模式,加载时经 schemastery 校验(z.union(['read-only', 'workspace-write', 'danger-full-access'])),非法值(如yolo)在插件加载时即被拒绝(测试 policy.spec.ts#L126-L130) |
workspaceRoot | process.cwd() | 无 agent 调用或会话无 cwd 时workspace-write的回退根;正常 agent 调用使用会话不可变 cwd,配置项无 schema 默认值,在构造函数中解析为绝对路径(src/index.ts#L104-L110) |
注意:runner 选择不属于这里(那是ctx.sandbox提供者的配置),本包是唯一共享的策略之家。
缓存安全:从"动态系统段"到"追加式完整快照"
为什么动态系统段是错的
决策文档记录了一个被真实提供方实验否定的方案:"把当前策略放进动态系统提示词段"。真实 Web fixture 量化了缺陷(决策文档 Wording evidence 一节):
- 首次
danger-full-access与workspace-write请求,cache-read 只有 256 tokens,而 uncached 输入高达 14,691 与 14,782 tokens; - 策略不变时后续步骤约 14.7k–15.5k cache-read tokens。
原因在于DeepSeek 按完整前缀匹配缓存:切换权限时系统段被重写,改变了第一条线上消息,导致"系统提示词 + 历史"的更长前缀无法复用。而且只搬动沙箱语句也无法修复——同一个 preset 切换同时改写了审批策略系统段。
agent-loop 的快照物化流程
现行设计让dsh-system-prompt组装器拥有有序动态上下文(与稳定系统段、工具 schema 并列)。核心流程在 packages/core/agent-loop/src/agent.ts 的preStep(#L232-L250):
const assembly = await this.loopCtx.systemPrompt.assemble(assembleContextFor(this, signal)) const sections = renderContextSections(assembly) const context = this.runtimeContext.project(joinContextSections(sections), sections) // ...进入 agent/pre-step waterfall,context 追加在 claimed 消息之后其中:
renderContextSections(system-prompt/src/index.ts#L302-L306)把组装结果渲染为带名字的贡献节({ name, text },空文本被过滤);joinContextSections(#L287-L291)生成显式取代语句的完整快照:
Current runtime context. This snapshot supersedes earlier runtime-context snapshots. <各贡献节按 order 升序、以空行连接>RuntimeContextProjection.project(packages/core/agent-loop/src/runtime-context.ts#L64-L75)仅在保留快照与当前文本不同时才产生一条候选 user 消息,且仅在以下情况追加带来源的user/message:无保留快照、字节发生变化、压缩(compaction)移除了保留消息、或最后一个贡献消失需要一条清理快照。清理快照的固定文本为Current runtime context: none. Earlier runtime-context snapshots no longer apply.(runtime-context.ts#L13)。
快照被追加在既有历史之后、step/start之前(agent.ts#L286-L291),因此策略变化时先前的"系统 + 对话"缓存前缀得到保留。会话事件本身即可重构精确的模型输入;仅策略上下文变化时,request/header保持逐字节一致。
归属与保留
RuntimeContextProjection构造时(runtime-context.ts#L34-L56)从会话事件中恢复最近一条仍保留在 surface 上的、由@deepseek-ai/dsh-system-prompt来源(source.kind === 'plugin')拥有的user/message,并忽略其他会话;之后通过session/event订阅跟随权威事件——新的归属消息被保留、替代性 surface 事件(isReplacementSurfaceEvent且sourceEventSeqs命中)将其置空。这正是"会话事件重构精确模型输入"的实现基础。
多所有者协调:为什么审批策略也进同一快照
决策明确"所有权保持狭窄":审批策略(approval:policy,order 115)把其完整当前的ask或never事实贡献给同一个完整快照。这是因为/permission切换同时改变两个所有者(沙箱模式与审批策略),若只迁移沙箱而不迁移审批,缓存缺陷依旧存在(决策文档 Decision 与 Alternatives 均有论证)。
其余边界:
- 计划模式仍是
plan:policy系统段(PLAN_POLICY: 500见 system-prompt/src/index.ts#L135); - 工具插件继续拥有 schema 及尝试、拒绝、升级指导;
- 执行边界仍是 filesystem、一次性 bash 与 terminal 后端消费同一份已解析策略(
resolve()),上下文只是陈述策略,不是强制护栏——运行时安全始终来自执行后端(决策文档 Consequences 一节)。
为什么不改为让每个策略所有者各自调用agent.inject()("Callagent.inject()independently"备选被拒):兄弟监听器顺序会决定模型顺序、分离的消息可能暴露不一致的中间快照、每个所有者还需自己的压缩保留扫描;由既有组装所有者排序并物化一条原子完整快照更优。
措辞的实验证据与反预拒绝原则
预先注册的正向对照
措辞实验把**预拒绝(preemptive refusal)**预先注册为主要终点,要求旧有固定语句在十二个全新会话中至少产生一次拒绝,才允许评估任何替代措辞(决策文档 Wording evidence 一节):
- 2026-07-30,commit
2bf4199:以deepseek-v4-flash运行 shipped Web 组合,使用精确正向对照句Bash commands run under the "read-only" file sandbox.加当前工具自有的尝试指导。结果:零预拒绝、零投机性升级;全部十二个会话都发起普通 bash 调用、观察到拒绝、同一回合内升级、获得批准、落地请求文件。无样本被排除。 - 缓存安全投递改造后,commit
10d4e0f:同一十二会话正向对照经由新的尾部上下文通道重复。同样零预拒绝、零投机性升级;八个会话落地了精确请求的文件,无样本被排除。
两个正向对照均未通过预先注册的敏感性门槛,因此正式的 Candidate A/B 十二会话实验并未运行——这些实验不选择也不验证当前措辞,它们确立的是:早前"十二分之五"的拒绝率在本任务与当前工具指导下不可复现;在做出模型行为率声明之前,需要更强的正向对照或不同的任务分布。确定性测试只确立真实请求构造与重放(见下节)。
缓存安全改造后的非统计验收
在缓存安全投递重构后,另行执行了一次独立的、非统计的验收对比,任务是中性 Web 任务Create the relative path policy-neutral.txt ...(决策文档 Wording evidence 一节):
- Candidate A(分类式
read-only直白陈述)产生纯文本拒绝、零工具调用; - Candidate B(仅对工具暴露升级的执行家族追加一句组合条件化语句)的措辞下,真实提供方运行发起普通
write、观察到 read-only 拒绝、同一回合内以sandbox_permissions: "workspace-write"重试同一操作、获得批准、读回文件并验证内容,无投机性升级。
在权限切换与四次变更步骤期间,cache reads 为14,848–15,872 tokens,而每次请求的 uncached 输入仅59–306 tokens,直接展示了稳定前缀收益。这也解释了反预拒绝原则的存活形态:它本身不陈述任何升级机制,只是告诉模型不要从固定标签推断不可能性,然后把拒绝与升级行为委托回可用工具。
模型侧词元与缓存效应
packages/sandbox/sandbox-policy/README.md 的 Model Experience 一节总结:
- Token effect:首次请求与每次策略实际变化时各有一条简明的持久上下文消息;不变请求不新增。
workspace-write只携带规范会话工作区路径,平台临时路径被概括、不引入主机相关字节(测试 policy.spec.ts#L164-L182 验证整个渲染 prompt 与上下文快照在TMPDIR变化时保持逐字节稳定)。 - KV Cache effect:稳定系统提示词在模式切换间保持逐字节一致;变化后的完整快照追加在保留历史之后,保留先前缓存前缀;后续不变请求复用该保留快照。
备选方案与拒绝理由
决策文档 Alternatives 一节记录了完整论证,此处按主题归纳:
| 备选方案 | 拒绝理由 |
|---|---|
| 只叙述模式变化(narrate only mode changes) | 全新会话仍不知情,第一次被拒操作成为策略发现机制;且需要不必要的基线定义 |
| 扫描 denial 历史或记住最后叙述的模式 | denial 事件描述的是"尝试过的操作"而非权威当前状态;进程内簿记无法跨重启存活;所有者可直接在每次请求上折叠持久策略 |
| 把当前策略放进动态系统段 | 真实证据显示首次权限切换使 cache reads 降至 256 tokens 而约 14.7k 输入未命中;DeepSeek 匹配完整前缀,改变第一条线上消息即阻断长前缀复用 |
每个策略所有者独立调用agent.inject() | 兄弟监听顺序决定模型顺序、可能暴露不一致中间快照、每个所有者需自己的压缩保留扫描 |
| 通用 runtime-facts 包 | 既有组装所有者已拥有分段、schema、变量、作用域与权威逐步瀑布,扩展它无需新包或第二注册服务 |
| 在上下文中重复工具 schema 或计划指导 | 那些表面已有所有者和独立生命周期;审批当前状态加入快照只因同一/permission切换会改变它 |
| 缓存安全改造后保留 Candidate A | 中性真实任务中模型返回纯文本拒绝且零工具调用,尽管已有 bash 尝试指导 |
| 因旧标签曾致预拒绝而完全省略沙箱模式 | 全新 Web 请求会暴露变更工具却隐藏其现行策略,在首次操作前产生虚假能力声明;旧测量是必备对照测试(十二分之五无工具调用),但工具自有尝试指导晚于该测量,替代措辞须在新实验下选择 |
| 单独 model-context 包 | 策略所有者可直接解析当前会话状态,既有组装服务可排序;新包只添加浅层组合层与文档/门面开销 |
| 枚举可写临时根 | 后端在confine()时才选定:bwrap、Landlock、Seatbelt、进程内 fence 不授予共同临时路径集合;主机特定路径既不稳定又过度声明 |
后果与测试保障
行为后果
- 模型在探测工具前即收到当前文件策略;
/permission后的下一个请求反映已提交的模式; - 稳定系统提示词不再因沙箱或审批状态而改变;变化的完整上下文快照在保留历史后追加,不变状态不新增消息;
- 历史中的旧快照仍在,但被最新完整快照显式取代;
- 该语句是指导而非强制护栏:运行时安全仍来自消费同一已解析策略的 filesystem、一次性 bash 与 terminal 后端。
确定性测试覆盖
聚焦测试固定了以下行为(见 policy.spec.ts 与 runtime-context.spec.ts):
- 三种模式的精确文本渲染且无能力清单(
it.each逐字断言,policy.spec.ts#L152-L162); - 部署默认
read-only与进程 cwd 根(#L43-L47); - 每会话模式与 cwd 一起解析、回退根不变(#L63-L85);
- symlink 敏感 cwd 的 POSIX 组件语义(#L87-L108);
- 批准显式模式压过会话模式但保留其根(#L110-L119);
- 无 cwd 会话使用配置根(#L121-L124);
- 非法模式在加载时被拒(#L126-L130);
- 子 fiber 中服务与上下文贡献的处置(HMR 安全,#L132-L141);
TMPDIR变化下完整渲染 prompt 与快照逐字节稳定(#L164-L182);- 最新持久切换在下一次组装中生效、其余情况逐字节稳定(#L184-L197);
- 从会话日志重建已恢复策略、无 agent 时省略诊断(#L199-L207);
setSandboxMode每次切换恰好追加一个sandbox/mode事件、effectiveSandboxModefold 到最后一次切换(#L210-L229);RuntimeContextProjection恢复最新可见归属快照、忽略其他会话、替换 surface 事件后不再投射(runtime-context.spec.ts#L16-L44)。
此外,无密钥(keyless)组装快照通过真实 Loader 组合固定持久上下文消息;无密钥重放拥有中性的"拒绝到升级"轨迹——它是结构性回归证明,而非措辞选择证据(决策文档 Consequences 一节)。
相关资源
- 决策文档:.agents/notes/implemented/feature/2026-07-30-current-sandbox-policy-context.md(本文主依据)
- 简化决策:.agents/notes/implemented/simplification/2026-07-31-capability-neutral-sandbox-policy-context.md
- 策略服务实现:packages/sandbox/sandbox-policy/src/index.ts 与 packages/sandbox/sandbox-policy/src/session-mode.ts
- 包文档:packages/sandbox/sandbox-policy/README.md
- 快照物化:packages/core/agent-loop/src/runtime-context.ts 与 packages/core/agent-loop/src/agent.ts
- 组装与渲染:packages/core/system-prompt/src/index.ts
- 测试:packages/sandbox/sandbox-policy/tests/policy.spec.ts、packages/core/agent-loop/tests/runtime-context.spec.ts
【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考