DeepSeek Harness Plugin Hub

发布与管理完整 Harness Profiles,发现适合你的插件。

探索

插件目录环境预设文档中心动态

社区

发布插件联系我们报告问题

相关链接

Plugin Hub GitHubDeepSeek Harness 官方项目系统状态隐私说明
© 2026 DeepSeek Harness Plugin HubPowered byPaxTech

独立、非官方社区项目,与 DeepSeek 官方无隶属、授权或背书关系。

Sandbox Escalation Guard — DeepSeek Harness 插件(DSH Plugin)
DeepSeek Harness Plugin Hub
ProfilesPlugins分类动态文档登录管理 Profiles
ProfilesPlugins分类动态文档登录
← Plugins
S

dsh-sandbox-escalation-guard

Sandbox Escalation Guard

当调用会话无法授予权限时,停止向模型提供 DSH sandbox-escalation 字段,修复 GPT-family 在 danger-full-access 处的重试循环:每个已公布的枚举值都会在命令运行前被拒绝。

插件会安装到这里;不确定时保持 web。

npx -y @deepseek-ai/dsh plugin --profile web add github:iimaguest/dsh-sandbox-escalation-guard#e06a2ad03433b0d7e21479806d73f7c2c49f20b1
README兼容性版本
What the model is handed, before and afterGrantability by session modeWhere the guard sitsVerified against the harness

兼容性与来源证明

Sandbox Escalation Guard 以 dsh-sandbox-escalation-guard 发布,当前版本为 1.0.0。Plugin Hub 会校验它的 manifest,并保存精确安装来源,便于复现安装结果。

DSH 兼容范围
*
运行环境
any
发布来源
github
Registry 更新时间
2026/9/19

版本

1.0.0stable
2026/9/19

相关插件

正在加载相关插件…

最新版
1.0.0
DSH
*
HMR
重启进程
Tree shaking
未声明可安全裁剪
解包体积
未提供
文件数
未提供
Surface
any
许可证
Apache-2.0
发布源
github
GitHub
★ 0
周下载
0
最近提交
2026/9/19
查看源码 ↗
README Badge

点击下方 Badge 复制 Markdown,粘贴到 README 即可。

这是你的 Plugin?认领权益 · 优先安全扫描

验证 package.json 声明的 GitHub 仓库,即可管理这个公开页面。认领后,Hub 会优先安排当前版本的安全扫描,并在通过后公开展示结果。

认领这个 Plugin →
报告问题

相关插件

继续浏览 security-access 分类下经过校验的插件。

Doctor@linxin666/dsh-doctorDSH 配置档案的事务性救援模式,配备受监督的启动器、隔离的恢复容器、确定性修复、健康监控以及本地 Web 恢复控制台Pocketdsh-pocket把 DeepSeek Harness 装进你的口袋:一个包、一个设置页,手机扫码即同步访问电脑上的 DSH(局域网 + 公网,实时同屏)。DSCODE@toddzheng024/dscode-bundle完整的 DeepSeek 编码代理,支持持久化 shell、Ultra 协作和自动权限审查。Auto Reviewdsh-auto-review针对 DeepSeek Harness 审批请求的第二模型 AI 自动审查:只读审查子代理在审批应答链上决定允许或拒绝,并采用故障关闭回退机制和完整的会话日志审计。

README

dsh-sandbox-escalation-guard

A DeepSeek Harness plugin that stops the sandbox-escalation fields being offered to a model when the calling session cannot grant them.

It fixes a GPT-specific failure. GPT-family models complete every optional property in a tool's schema, and the two optional properties on bash, write and edit are exactly the two a full-access session can never use. So GPT fills them in on every call, every call is rejected before it runs, and the error text never says that the fix is to omit them. The turn dies with nothing executed.

A model that sends only what a call needs never sees this — which is why it looks like "GPT is broken" while other models are fine in the same session. The fix is not a better prompt: it is to stop publishing fields that cannot be filled. See why this presents as GPT-specific.


The bug

The DSH escalation fields — sandbox_permissions and justification — are advertised from a load-time capability fact but judged at execution against a per-session policy fact:

SourceQuestion answered
Advertisementctx.shell.sandboxModeIs a confining executor mounted?
Executionctx.sandboxPolicy.resolve().modeWhat mode is this session actually in?

They disagree in exactly the case that hurts: a session sitting at the ceiling.

A session whose effective mode is danger-full-access is handed:

"sandbox_permissions": {
  "type": "string",
  "enum": ["workspace-write", "danger-full-access"]
}

Both values are unusable. workspace-write is a reduction, and danger-full-access is the mode already in force — and the check is strictly wider, so neither can ever be granted. The system prompt simultaneously instructs the model to use that field to "escalate immediately".

A model that fills in every optional property it is shown — the GPT family does this — then cannot succeed and cannot tell why:

Error: invalid justification: expected a non-empty sentence
Error: sandbox escalation to "danger-full-access" is not strictly wider than this call's current "danger-full-access" mode
Error: sandbox escalation to "workspace-write" is not strictly wider than this call's current "danger-full-access" mode

Neither text names the only actual repair, which is to omit both fields. The observed result is the same command retried until the turn dies.

Why this presents as "GPT is broken"

Nothing here is model-specific in the harness. The difference is a habit, and it collides with this defect perfectly:

GPT-family models populate every optional property in a tool's schema. Given command (required) plus sandbox_permissions and justification (optional), they do not send the minimum the call needs — they send a fully-formed instance of the schema they were shown. Asked for one string, they fill in three.

That habit is usually harmless. Here it is fatal, because the two optional properties are the exact two that a full-access session can never use. So the model helpfully completes them on every call, each call is rejected before anything runs, and the error text points at the justification field rather than at the real instruction, which is to leave the fields out. Retrying with the same habit produces the same rejection. The turn ends with no command executed and no usable signal about why.

A model that only sends what the call needs never touches those fields and never sees the bug. From the outside, that looks like one model being broken — same session, same harness, same repository, different models, only one of them failing.

The fix follows directly from the diagnosis: if the model completes whatever it is shown, stop showing it things it cannot use. Once both fields are gone from the schema, there is nothing left for the habit to complete, and the failure mode disappears for the model that was most prone to it — without the model needing to know it existed, and without a prompt telling it to be careful, which is the kind of instruction a schema-completing model is least likely to reliably follow.

This is why the primary fix is suppression rather than a clearer error message. A better message asks the model to behave differently; removing the field does not depend on the model's cooperation at all. The tools/pre-execute correction exists only for the residual case — a cached schema, or a model that ignores the schema it was given.

Evidence

From one live session (effective mode danger-full-access, approval policy never), comparing a model that completes optional properties against one that does not:

modeltool callsemitted sandbox_permissionsbash callsbash + optional field
zai/glm-5.321108648
oauth-codex/gpt-6-astra10888

The GLM column is not "a model that tried and succeeded" — it emitted the field zero times in 211 calls. It never entered the failure mode, because it sends only the properties a call needs. The GPT column emitted it on every bash call and lost all eight to the same rejection.

Same session, same schema, same repository, same effective mode. The only variable is whether the model completes the optional properties it was shown — which is why this reads from the outside as "GPT is broken" rather than as a schema defect that one model's habits expose.

Confirmed at the wire level too: the request/header event for that session shows bash, write and edit each advertising sandbox_permissions with enum: ["workspace-write","danger-full-access"], so the field was genuinely published to the model rather than being something it invented.


What this plugin does

1. It stops publishing the impossible (the actual fix)

The tool set is still mutable at system-prompt/assemble, which is a sanctioned extension point and runs per request. The rewrite is decided by a session fact — whether an escalation could be approved at all — never by the current access level:

  • when the session's approval policy is never, and for any unrecognised mode, remove both fields entirely and strip the escalation paragraph from the tool description that would otherwise keep urging the model to use them;
  • when an escalation genuinely could be approved, leave the surface exactly as the harness built it.

An earlier revision instead kept only the enum values strictly wider than the current mode. That is gone, for two reasons:

  • It moved the cached prefix. The tools block is the front of the cache, so rewriting it on an access-level change discarded the entire cached conversation behind it.

  • The access level does not move on its own. The shipped permission presets bundle the two knobs, and switching one writes both:

    presetsandboxapproval
    read-onlyread-onlyask
    workspace-writeworkspace-writeask
    danger-full-accessdanger-full-accessnever

    So the access level never changes while the policy stays put. The policy is therefore the key that is constant for the whole life of a preset, which is exactly the lifetime over which the cached prefix must survive.

Because the deciding fact is the approval policy rather than the mode, the published surface is byte-identical on every request and does not move when the access level is changed in place. That is the property that protects the cache, and test/mount.test.mjs and test/real-services.test.mjs pin it directly.

There is no coverage lost in any shipped preset, which is worth stating because an earlier version of this file implied otherwise:

presetwhat the model is offeredcorrect?
read-only["workspace-write", "danger-full-access"]yes — both are genuinely grantable
workspace-write["workspace-write", "danger-full-access"]danger-full-access is grantable; workspace-write is the downward direction, and escalation only ever goes wider
danger-full-access(fields removed)yes — nothing is wider, so every value would be rejected. This is the reported failure.

Escalation is strictly widening, so a model never asks to narrow itself. The surface above is consequently the correct one for every preset the harness ships.

It still judges grantability by the same strictly-wider ladder @deepseek-ai/dsh-sandbox exports and approveEscalation enforces, so the two cannot disagree about what is grantable. That ladder is mirrored in lib/wider-modes.js rather than imported, which is why this package has no runtime dependency — it plugs into the host contract alone (apply + inject). A test/wider-modes.test.mjs drift guard compares the mirror against the harness's own export and fails if DSH ever changes the ladder.

For a session under a never policy the model now receives a bash tool with only command and description, and no escalation prose at all — in every mode.

2. It corrects a call that carries them anyway

A client holding a cached schema, or a model that ignores its schema, can still send an unusable request. tools/pre-execute then denies it with text that names the effective mode, states that no value is grantable, and names the repair — instead of the two opaque validations.

This is a fallback where (1) applies, and the only mechanism where it does not. In a session under an ask policy the surface is deliberately left intact, so this listener is what keeps an unusable call from failing opaquely.

The plugin never widens anything

It only stops offering modes that cannot be granted. It never grants a mode the harness would have refused, and it cannot: it removes capability from the advertised surface and adds none.


Suppression matrix

The headline guard is uniform across modes — that is what keeps the cached tools prefix byte-stable for the length of a session.

session can have escalation approved?advertised enumdescription prose
no — approval policy never, no approval service, or unrecognised mode(fields removed)escalation paragraph removed
yes — an approval channel exists and policy is not never(left exactly as the harness built it)kept

The two conditions are session facts, not properties of the current access level. ApprovalService.decide returns "rejected" outright when the effective policy is never, before any approver is consulted, so under that policy an escalation is impossible in every mode — and the right published surface is also the same in every mode.

An unrecognised mode grants nothing rather than everything: SandboxMode is a validated closed union, so this cannot arise from a healthy host, and offering an escalation would be the unsafe guess.

The residual, and the per-call correction

When escalation genuinely can be approved, the guard does not edit the surface at all. A call that still carries an unusable request is then corrected at tools/pre-execute, which re-evaluates per call and costs nothing to re-run. This covers:

  • a session under ask whose current mode happens to leave nothing wider — for example danger-full-access, from which no escalation is ever grantable;
  • a client holding a cached schema;
  • a model that ignores its schema entirely.

The correction names the effective mode, states that no value is grantable, and names the repair — unlike the two opaque validations it replaces. See What this plugin does and does not cover for why the enum is not trimmed per mode.

Install

dsh plugin --profile web add github:iimaguest/dsh-sandbox-escalation-guard

That installs from GitHub, writes the dependency, and adds the profile's bundles entry, so the cordis.patch.yml in this package mounts automatically. Confirm the row:

grep -A 14 '"bundles"' ~/.dsh/profiles/web/package.json

Activation is a profile load, not a file write. The plugin takes effect when the profile is next loaded.

Managing the install

# pull new commits that have landed on GitHub
dsh plugin --profile web update dsh-sandbox-escalation-guard

# remove completely
dsh plugin --profile web remove dsh-sandbox-escalation-guard

To disable it without uninstalling, remove dsh-sandbox-escalation-guard from the profile's bundles list. To develop against a local checkout instead of GitHub, dsh plugin --profile web add /path/to/dsh-sandbox-escalation-guard.

Configuration

- id: dsh-sandbox-escalation-guard
  name: dsh-sandbox-escalation-guard
  config:
    warn: true             # log each intervention to the host console
    resolveMode: ...       # testing seam only; omit in production
    escalationPossible: ... # testing seam only; omit in production
fielddefaultmeaning
warntrueLog when the schema is narrowed or a call is corrected.
resolveMode(unset)Overrides effective-mode resolution for the per-call correction. Testing seam; production reads the session's own sandbox/mode fold through sandboxPolicy.
escalationPossible(unset)Overrides whether the published surface keeps the escalation fields. Testing seam; production reads the approval policy.

What this plugin does and does not cover

This is stated plainly because the coverage changed during development, and the honest version is narrower than an earlier claim in this file.

It covers the case where escalation can never be approved. When the deployment's approval service exists and the session's policy is never, ApprovalService.decide returns "rejected" before any approver is consulted, so an escalation is impossible in every mode. The guard then removes the escalation fields from the published surface, uniformly, in every mode. This is the reported configuration (the failing session logged approval/policy: {policy: never}), and it is fixed.

It also corrects, per call, any unusable request that arrives anyway — a cached schema, a model ignoring its schema, or a session whose policy is ask. That check runs at tools/pre-execute, costs nothing to re-evaluate, and names the exact repair.

It does NOT narrow the advertised enum to the values grantable from the current mode. An earlier revision did, and that was wrong twice over:

  1. It broke the cache. The enum moved whenever the access level changed, and the tools block is the front of the cached prefix, so a mode switch discarded the entire cached conversation behind it.

  2. It published a value that was going to be rejected anyway. A workspace-write session advertised ["danger-full-access"]. Omitting a field is always safe; publishing a value that is later rejected is what breaks a model. The set that is safe in every mode a session can be switched into is the intersection across those modes — and it is empty as soon as the range includes danger-full-access:

    modegrantable from it
    read-onlyworkspace-write, danger-full-access
    workspace-writedanger-full-access
    danger-full-access(none)

    No value is grantable from all three, so a surface that never offers a rejected value cannot offer any value.

The one residual, stated plainly. The remaining gap needs a composition no shipped preset produces: an approval policy of ask paired with danger-full-access. There the fields survive (the policy says an escalation could be approved) while the mode says no value is wider, so a GPT-family model can still fill them and collect one rejected call — corrected by the tools/pre-execute listener rather than prevented. That listener names the effective mode and the exact repair, so the failure is a clear correction rather than the opaque loop that motivated this plugin.

If you build such a composition and want the tighter surface, set escalationPossible: false to suppress uniformly. That is a statement about your deployment, made by your deployment — not a guess this plugin makes from a mode it cannot know is stable.

Screenshots

What the model is handed, before and after

Grantability by session mode

Where the guard sits

Verified against the harness

These are diagrams of the harness's own contracts rather than captures of a UI, and every value in them is copied from verified output — generating them is npm run screenshots, and the mode table in the last image is exactly what npm run verify prints.

Reproducing the claim

npm install
npm run verify

Mounts the plugin against a real Cordis context and a real systemPrompt registry and prints the model-facing bash schema for each session mode — the plugin's observable claim, reproducible rather than described.

Tests

npm test

48 tests:

  • test/guard.test.mjs — the suppression matrix, the strictly-wider table against approveEscalation's judgement, and prose stripping.
  • test/wider-modes.test.mjs — drift guard: the mirrored ladder is compared against @deepseek-ai/dsh-sandbox's own export whenever the harness is resolvable, and skipped (not failed) when it is not.
  • test/mount.test.mjs — the plugin applied to a real Cordis context and a real systemPrompt registry, asserting what the model would be handed, that a session which can escalate keeps the harness's own bytes untouched, that the published surface does not move when the access level changes, that unmounting restores the original surface exactly (so this is an ordinary plugin row with no residue), and that the pre-execute correction fires only on unusable requests. Some of these mount the plugin before the policy service exists and then provide it, which is the real boot order — see the note below. Others mount neighbouring listeners on both sides of the guard, and re-assemble fifty times per mode to prove the output bytes never move.
  • test/real-services.test.mjs — the same contract mounted against the harness's real ApprovalService and SandboxPolicy, not the testing seam. This is the file that proves the gate reads the harness's own approval fact: a fake { effectivePolicy: () => 'never' } agrees with any implementation, including a wrong one. Skipped (not failed) when the harness is unresolvable.

The waterfall must be chained

ctx.waterfall dispatches through a chained thunk — (cbs.shift() ?? inner) — so a listener that returns without calling next() ends the chain for every listener registered after it. The first version of this plugin did exactly that, which silently suppressed the system-prompt/assemble listeners of dsh-session-reference and dsh-agent for the whole process. Their contributions never reached a request and nothing reported the loss; it was found by probing the dispatcher against two listeners, not by any single-listener test. The listener now awaits next() and narrows the result, which is also the correct order — the model must receive the final assembly.

The service-ordering trap

ctx.get('sandboxPolicy') must not be read once inside apply and closed over. A bundle patch is appended to the composition, while sandbox-policy sits earlier in the base tree and is provided asynchronously, so the row can load before the service exists. Closing over the result binds undefined and the plugin then narrows nothing for the entire session — silently, having printed a warning that looks like a missing dependency rather than a race.

Declaring sandboxPolicy in inject is the other wrong answer: it defers apply until the service appears, so on a composition that genuinely lacks it the plugin waits out the session with no hooks registered and no diagnostic at all. The guard resolves the service per request instead, mounts unconditionally, and reports a genuinely absent service once, at the first request it cannot serve. mount.test.mjs pins both halves: untouched before the service arrives, narrowing immediately after.

The escalation-prose fixtures in guard.test.mjs are copied verbatim from the shipped dsh-tool-bash and dsh-tool-fs descriptions. If a DSH upgrade reworks that wording, the prose tests fail loudly rather than this package silently ceasing to strip prose.


Known limits

It cannot repair the schema's origin. This plugin rewrites the schema on its way to the model; it does not change dsh-tool-bash or dsh-tool-fs. The underlying predicate — advertising from ctx.shell.sandboxMode while validating against ctx.sandboxPolicy.resolve() — remains the harness's own, and the correct upstream fix is to gate the fields on the session's resolved mode (and to accept the current mode as a no-op). This plugin is the durable workaround for an install that must not patch node_modules.

Prose stripping is anchored, not structural. The escalation paragraph is removed by matching its leading clause. If a future DSH rewrites that text the schema fix still applies and only the prose is left behind — the model keeps its instructions but loses the fields they refer to. The pinned tests make that visible.

The pre-execute fallback denies rather than executes. Letting the call proceed by stripping the arguments would require re-implementing tool dispatching, because exec.arguments is deep-frozen before any plugin observes it. A denial that names the repair is the safe choice; with (1) active it should never fire.

A listener registered after this one can still re-add the fields. The guard narrows the assembly returned by next(), which carries the work of every listener upstream of it in the chain. A plugin that appends tool schemas and happens to register after this one would put un-narrowable fields back. Middleware order is absolute and cannot be corrected from inside the waterfall. This does not arise with the shipped set: dsh-session-reference and dsh-agent both call next() and only read assembly.variables, adding no tools.

On prompt caching

The guard rewrites request content, which is the exact shape of change that can quietly destroy a provider's prefix cache. So this is measured against what the providers actually document, not assumed.

What the providers say

ProviderRule
OpenAIChanging tools changes "names, descriptions, schemas, ordering, or tool-specific instructions". Cache reuse "requires the entire rendered prefix to match". The cache-routing hash is taken over the initial tokens "including tool definitions when present".
AnthropicThe hierarchy is tools → system → messages; "Modifying tool definitions" invalidates the entire cache at every level.
DeepSeekCaching is automatic and disk-based; a request hits only by fully matching a persisted cache prefix unit.

All three agree on the operative point: tool definitions sit at the front of the prefix, so they must be byte-stable for reuse to happen at all.

Why this plugin satisfies it

The bytes are constant for the length of a session. The effective mode is a session fact, so narrowTools yields identical output on every assembly. The schema is narrowed once, in effect, and every later turn sends the same bytes. The cache is written on the first request and read thereafter — unaffected in steady state.

Nothing varying is introduced. No timestamp, counter, identifier, token or regeneration enters the output, so the routing hash is stable too.

Neither name nor order changes. Only properties entries and the description text are filtered; the tool list keeps its order and every tool keeps its name. Property insertion order is preserved from the input, so a rewrite never reorders keys and changes bytes without changing meaning.

The result never depends on request history. Rendering a given mode produces the same bytes whether it is the first request or the hundredth.

It makes the cached prefix smaller, not different. The escalation fields are removed rather than rewritten, so the published definition is a strict subset of the original.

What is pinned by tests

test/mount.test.mjs asserts the contract directly:

  • 50 consecutive assemblies per mode produce exactly one distinct JSON.stringify of the tool surface;
  • the same mode renders identically no matter where it falls in a sequence of differing modes;
  • tool names and list order survive narrowing;
  • property key order survives narrowing.

Mid-session access changes: the honest cost

An earlier version of this section claimed the harness already invalidates the prefix on a mode change, via the sandbox:policy prompt context. That was wrong, and the correction matters because it changes who pays.

The policy text is an appended message, not part of the prefix. dsh-sandbox-policy registers a sandbox:policy context whose text is a function of the resolved mode, but dsh-agent-loop renders the whole context snapshot and appends it to the message list:

const context = this.runtimeContext.project(joinContextSections(sections), sections)
// ...
{ kind: "enter", messages: context === void 0 ? claimed : [...claimed, context] }

The snapshot even announces itself: "This snapshot supersedes earlier runtime-context snapshots." Content appended at the end of a conversation cannot invalidate anything cached ahead of it, so a mode change costs nothing by itself.

tools is the channel that carries the cost. assembly.tools feeds both buildRequest and toolsChanged(), which drives the request-series restart; headerEquals compares schemas by canonical JSON. The guard's rewrite is a tools rewrite, so it is the thing that moves the cached prefix.

And the tools block genuinely would not move without this plugin. The advertised escalation enum derives from a mount-time capture — dsh-tool-bash reads const defaultMode = ctx.shell.sandboxMode once in apply, and ESCALATION_TARGETS follows from it — while resolveSandboxPolicy resolves per call. That load-time/execution-time split is the root bug this plugin exists to fix, and a side effect of it is that the published schema is static across a mode change. So nothing in the harness would invalidate the prefix, and the guard's rewrite is the sole cause.

So the guard does introduce a cache write on a mode change — one that would not otherwise occur. Being precise about the size of that:

cost of one mid-session mode change
Harness alonenothing — the snapshot is appended, the tools block is static
With this guardone cache write, then reads resume normally

It is still the right trade, for a stated reason. Without the rewrite the model keeps a schema offering escalation fields that no longer work — which is exactly the failure this plugin exists to prevent, reinstated mid-session. A single cache write per mode change buys a surface that always matches the session's real permissions. Mode changes are user actions, not per-turn events, so the frequency is low by construction.

It cannot be avoided from inside this design. There is no way to correct a published schema without changing it, and the fields live in the tools block. The alternative is recovery rather than prevention — correcting the call after it is rejected instead of never advertising it — which costs no cache write but costs one failed tool call per affected turn. The related apex-mochen/dsh-sandbox-arg-guard takes that route. The two are complementary, and this is the specific axis on which they differ.

Steady state is unaffected, which is the property that matters day to day: with the mode unchanged, the tool bytes are identical on every request, so toolsChanged() is false, no series restarts, and there is no repeated cost. test/mount.test.mjs pins all three facts.

License

Apache-2.0