self-control-guard
English | 中文
针对 DeepSeek Harness 宿主进程的自控守卫插件。
为什么需要它
开发 DSH 插件时,agent 经常误触自杀——一条失手或意图错误的命令(pkill dsh、kill -9 <host-pid>、kill $PPID、killall dsh)直接杀死了自己的宿主进程。会话中途崩溃、工作丢失、无审计轨迹。本插件用于防止 agent 自杀:从 bash 工具拦截高置信度的宿主终止尝试、硬性拒绝、引导模型使用受控退出工具,并留下审计轨迹。
它做三件事:
1. 拦截 从 bash 工具终止宿主的高置信度尝试(规范形式 kill <host-pid>、kill $PPID、pkill dsh、killall dsh),单调硬拒——任何后续 pre-execute 监听器都无法 force-allow。 2. 引导 模型使用受控工具——dsh_self_exit 恒有,dsh_self_restart 仅在 restartEnabled: true 时(默认隐藏——TBD,见 Known Limitations):在拦截后的下一条消息注入固定引导文案。工具启动即注册但隐藏(hidden: true 使其不进 schemas()),未受指导的模型无法枚举发现;但按名可调用,任何从工具列表之外获知名字的调用方(守卫拒绝文案、用户指令、其他工具)都能立即调用。 3. 执行 token 确认的优雅退出/重启,走 launcher 既有缝:一次性模式用 headless runner 的 ctx.headlessIo.exit(code)(精确退出码),长驻面(web)复用 launcher 的 SIGTERM 优雅关闭。
定位:UX/拦截层,不是安全边界。 matcher 在有界 shell 命令列表里按简单命令粒度识别规范 kill 形式(; && || | |& &、换行、注释、引号、( list ) / { list; } 分组、重定向(目标词不透明));动态构造($VAR、$(...)、反引号、通配符、heredoc)、shell 函数、不支持的复合语法以及解析器资源上限失败一律 abstain。变量拼接、编码、解释器、进程组信号、PTY、MCP、Code Runtime、cordis_mount 与直接系统调用都在文档化的漏报面内(见[拦截覆盖](#拦截覆盖)与 [Known Limitations](#known-limitations-and-deferred-work))。OS 总能杀死宿主;守卫的职责是让模型改用受控工具,并在发生时留下审计轨迹。
安装
前置条件:Node.js 22 或更高版本,以及 @deepseek-ai/dsh@0.1.0-rc.6。插件以独立 bundle 形式安装进任意 DSH profile。
在插件目录内构建、校验并打包:
npm install
npm run check
npm pack把生成的 tarball 安装进 DSH profile,然后重启 dsh web。不要把源码目录作为 link 安装,因为宿主 peer 依赖由 DSH profile 提供:
npx @deepseek-ai/dsh@0.1.0-rc.6 plugin --profile web add ./self-control-guard-0.1.0.tgz
npx @deepseek-ai/dsh@0.1.0-rc.6 web该插件没有浏览器 bundle,可在 Web 设置的插件列表中验证。更新时先提高 package 版本并重新打包,再移除旧 bundle、添加新 tarball 并重启。卸载命令:
npx @deepseek-ai/dsh@0.1.0-rc.6 plugin --profile web remove self-control-guard配置
- id: self-control-guard
name: self-control-guard
config:
enabled: true # 总开关
confirmationCalls: 2 # 确认协议总调用数:首次武装 token,末次确认
confirmationTtlMs: 60000 # token 有效期
retryCooldownMs: 10000 # 达到调用上限后的锁定期
interceptModes: [kill-host-pid, kill-parent, pkill-dsh, killall-dsh]
exitCode: 0 # dsh_self_exit 请求的退出码
restartExitCode: 42 # dsh_self_restart 请求的退出码;必须与 exitCode 不同
restartEnabled: false # 默认隐藏(web 无 supervisor);设 true 才提供该工具
headlessRestart: deny # headless 一次性重启策略:deny | exit-code
allowedAgents: roots # 谁可请求宿主退出:roots | all
# privilegedBash: # 针对被拦截命令的显式逃生通道
# enabled: false # 默认关闭;工具不注册
# matchers: [kill-parent, pkill-dsh, killall-dsh]
# # 现在仅供参考(复检已放宽);重复仍加载期失败加载期 fail-loud:interceptModes 重复或为空、exitCode === restartExitCode、confirmationCalls < 2、TTL 非正、cooldown 为负、非法枚举成员、privilegedBash.matchers 重复——绝不静默回退。enabled: false 是彻底关闭拦截的唯一方式。
拦截覆盖
拦截(硬拒)
matcher(src/matcher.ts)是 bash 工具 command 参数上的纯函数:在有界 shell 子集(资源上限:64 KiB 输入、4096 token、嵌套深度 32)下扫描命令列表,并对每个提取出的简单命令应用规范 allowlist:
| matcher id | 规范形式 | |---|---| | kill-host-pid | kill//bin/kill//usr/bin/kill,可选 command/builtin/exec/env/nice 前缀(镜像 bash 的状态机:command -p 保持内建、exec/env/nice 切换外部二进制、非法链如 env exec kill abstain),任意 Bash 接受的信号拼写(-9\|-KILL\|-SIGKILL\|-TERM\|-15\|-HUP\|-INT\|-1\|-s <信号>\|-n <号>\|--;名称大小写不敏感且可带 SIG、内建实时 RTMIN+0..30/RTMAX-1..14、外部 RTMIN+n、数字 1..64;外部 kill 路径另接受 IOT/CLD/POLL 别名但不支持 -n),与一个包含字面宿主 pid 的 pid 列表(每个 pid 按 Bash 方式规范化——+4242/0004242 命中宿主 4242;42424 不会)。内建语义:选项在首个 operand 处结束,kill -TERM <宿主> -FOOBAR 仍命中、坏 operand 被跳过。外部 procps 语义(对照 3.3.17 验证,两阶段):skill_sig_option 先取首个裸 -<信号>(含 -0)作为初始信号,getopt 的 -s/--signal/附着 --signal=/-s<信号> 随后覆盖、最后一个胜出且与 argv 顺序无关(-s 0 -9 PID 是探测并 abstain;-s TERM -- -9 PID 命中;第二个裸 -<信号> 使 procps 中止,-9 -TERM PID abstain);-l/-L/--list/-h/--help/-V/--version/未知 flag 永不终止(abstain);-q/--queue 消费经校验的 C long 值(缺失/非数字/溢出 abstain;64 位上 -q 9007199254740992 命中);-- 之后或已有裸信号之后的 -0 是按位置的 operand(-TERM PID -- -0 因 PID 已先发送而命中、-TERM -- -0 PID 因 utility 先死而 abstain;任意位置的首个裸 -0 是探测或被 -s 覆盖,故 -s TERM -- -0 PID 命中),而 -- 之前的第二个 -0 是 getopt case-'?'、在任何 operand 之前退出(-TERM PID -0 abstain、目标存活);bash 内建的 -- 后 -0 是进程组 0 operand:在任何后续 pid 之前杀死 utility 自身(kill -s TERM -- -0 PID abstain、kill -s TERM -- PID -0 命中——其前 pid 已发送;外部 /bin/kill 不同:那里的首个裸 -0 被 -s TERM 覆盖,故 /bin/kill -s TERM -- -0 PID 命中);坏 operand 停止递送(/bin/kill bad 4242 不命中、/bin/kill 4242 bad 命中);-- 结束 getopt(仅第一个 --——第二个是坏 operand)但其后裸 -<信号> 仍设置信号(-- -TERM PID 命中);-- 后数值负 token 是进程组 operand(-TERM PID -- -9 命中)、命名负 token 是坏 operand(-TERM -- -TERM PID abstain) | | kill-parent | kill 同上的可选前缀/信号拼写与 $PPID,单独或混入数字 pid 列表(kill $PPID <pid>);同时含字面宿主 pid 与 $PPID 的列表归为 kill-host-pid(宿主 pid 优先——privileged 通道永久拒绝该 id) | | pkill-dsh | pkill [−9] [−f\|−x\|−P <n>] dsh,可带 command/exec/env/nice 前缀(不能 builtin——pkill 是外部二进制) | | killall-dsh | killall [−9\|−r] dsh,前缀规则同上 |
列表中任一简单命令命中已启用的规范形式即拒绝整个调用——所以 kill -9 $PPID; echo x 这类复合形式会被拦截。# 注释 被剥离、引号被尊重(单个纯 token 加引号/转义如 'kill'/"4242"/"/usr/bin/kill"/kill "+4242"/"bad_arg" 视为字面;含 $ 或空白的内容保持不透明)、( list ) / { list; } 分组递归进入;$PPID 仅在未加引号时匹配。
不拦截(abstain——放行)
不支持的构造一律 abstain 而非猜测,因此会真实执行:
- 动态构造:
$VAR、"$HOST"、${HOST}、$(...)、反引号、通配符、heredoc——matcher 在pre-execute时只看到未展开的 shell 源码,变量目标与任何其他动态值无法区分。 - shell 函数:函数体(
run() { kill ...; })或包裹 kill 的函数调用——matcher 不进入函数体。 - 复合/不支持语法:
case/if/for循环、数组、位置参数、算术/进程替换、eval、bash -c、alias、通配符、xargs、到解释器的管道、后台(&)。 - 信号探测/列信号:
kill -0 <pid>、kill -l/kill -L——永不终止,故 abstain(探测仍可行)。 - 赋值前缀:
X=1 kill ...。 - 非规范形式:
kill -1(对整组 HUP)、pidfd_send_signal、进程组信号(kill -- -<pgid>)、未知信号名、畸形运算符序列、解析器上限失败。 - 进程外表面:PTY(
node-pty在ctx.subprocess外 spawn)、MCP、Code Runtime、cordis_mount(同进程、与 bash 等信任级)、解释器、直接系统调用。 - 非宿主目标:杀死 PID 非宿主进程的任何命令不拦截。
遗留的绕过路径(已文档化)
因为这是 UX 层而非安全边界,以下方式仍可能终止宿主:
1. 上述任意 abstain 形式——最常见的是变量构造的 kill:kill "$HOST"、pid=...; kill "$pid"、循环/函数参数。matcher 只看到未展开源码而 abstain;bash 随后展开并真实杀死宿主——无任何引导或审计。模型看到的第一信号就是会话结束。见 Known Limitations。(可选的 block-dynamic-kill 严格策略——拒绝任何目标为变量的规范 kill——是可能的未来配置,但它也会误拦对非宿主进程的合法 kill,故不设默认。) 2. dsh_self_bash(若启用)——privileged 逃生通道按设计运行 root agent 确认的任意命令(matcher 复检已放宽):复合形式、重定向、管道、甚至字面宿主 pid,受 token + 审批门禁(danger-full-access / active goal 豁免)。这是合理终止场景的文档化出口,不是对无意图 kill 的普通 guard 的绕过。 3. OS 级逃逸——setsid、解释器、pidfd_send_signal、进程组信号、PTY/MCP/Code Runtime、cordis_mount、直接系统调用。阻止这些属于 OS 隔离工作(PID namespace、seccomp、supervisor),不在本插件范围内。
受控退出 / 重启
两个工具共用一套确认状态机:首次调用返回不可预测的 confirmation_token(32 随机字节,仅存 SHA-256 摘要)且不执行任何关闭;后续每次调用须在 TTL 内出示当前 token;末次调用消费它并在工具结果落盘(先 exec.concludeTurn())后的新 tick 派发退出。token 按 agent、按 action、限时有效;达到调用上限的错误 token 会触发短 cooldown。
- Headless 一次性:
ctx.headlessIo.exit(code)精确退出码。重启默认拒绝(headlessRestart: deny),因为重跑同一任务可能重复外部副作用;headlessRestart: exit-code可显式选择。 - 长驻面(web):复用 launcher 的
SIGTERM优雅关闭(process.kill(process.pid, 'SIGTERM'))。web 无自定义退出码通道,重启以 launcher 信号码退出,重启语义由 supervisor 的 "process vanished" 策略承载——见 Known Limitations。 - Agent 围栏:仅顶级 agent(
allowedAgents: roots,默认)可请求宿主退出;subagent 的请求被拒绝。
高权限 bash(dsh_self_bash)
针对合理终止被误报场景的逃生通道:被拦截的命令(kill $PPID、pkill dsh、killall dsh)若确属模型意图(终止另一个 dsh 实例、wrapper 改变了 $PPID 语义),可经此显式、带门禁的工具执行(类 sudo)。它不是 guard 绕过:普通 bash 的单调 guard 原样保留。默认关闭(privilegedBash.enabled: false);启用时与退出工具一样在加载时注册并隐藏(进程级——registry 无 per-agent 注册),执行受 root-only 围栏门禁。
三层门禁须同时成立才执行:
1. 仅 root-agent 围栏——工具永远只限顶层 agent,与 allowedAgents 无关:subagent 绝不可能获得能重新执行宿主-kill 命令的通道。 2. 命令绑定确认 token——首次调用只返回 confirmation_token 不执行;二次调用须出示 token 且命令逐字一致(SHA-256 指纹把 token 绑到命令,已验证的 token 不能换命令重放)。目标由 agent 自行确认——没有 matcher 复检,任意命令(复合形式、重定向、管道、甚至字面宿主 pid)都可 arm,token + 审批门禁承载控制面。 3. 一次性人工审批——经 ctx.approval,仅 allowed-once;never / 无审批服务 / 取消一律 fail-closed。豁免:已在 danger-full-access 下运行的 agent,或有 active goal(ctx.goals.get(agent).phase === 'active',自主续跑)的 agent,直接执行——两者都是显式全权/自主授予,审批无增量价值。token 门禁在每种模式下都强制。
privilegedBash.matchers 现在仅供参考(保留用于文档与遥测;重复仍加载期失败),kill-host-pid 也不再加载期拒绝——放宽立场信任 root agent 自身的确认。命令经 ctx.bash executor seam 在与普通 bash 工具相同的解析后 sandbox policy 与会话 cwd 下执行(相同的环境清洗 / 超时 / 取消语义),绝不走 ctx.tools.execute(会二次进入 guard)。确认执行追加 log-only guard/self-control privileged-bash-executed 事件,携带所用门禁(granted | full-access-exempt),前面还有在命令运行前写入的 privileged-bash-dispatch 事件——命令在运行中途终止宿主也留有派发痕迹。派发经 ctx.sessions.flush 冲刷后才执行;无持久化监听器时派发仅在内存。
重启恢复(roster manifest)
当重启被确认且配置了 manifestPath(opt-in;默认空 = 无 roster)时,守卫原子地写入当前存活顶级 session id 到该文件({ version, restartId, createdAt, sessions: [{ sessionId }] },随机名独占 tmp + rename,0600 权限,畸形输入容忍为 "no roster")。下一个宿主进程可读取它获知重启时哪些持久化 session 日志是存活的。
恢复侧是薄 RPC:session.resume(host apiproxy)经既有 agentFor() 冷恢复事务把一批持久化 session 恢复为活 agent——与浏览器打开 session 走的路径相同。逐项结果:resumed / already-live / failed(每项独立结算)。浏览器重连管线(client runtime)自动调用它:handleConnected 时 sessions manager 恢复上一连接代的所有内存 session 实例(代级作用域且可中止——断连使在途事务失效);failed 项以 scope-prune 生命周期驱动的列表移除形式呈现。对 manifest 的 roster 授权延后:RPC 尚不校验成员资格——调用方的内存认知就是 roster。批有界(50)且重复 id 被拒。
审计轨迹
每个转移追加 log-only guard/self-control 事件(shell-intercepted、confirmation-armed、confirmation-rejected、shutdown-requested、privileged-bash-dispatch、privileged-bash-executed),只携带 matcher id、action、reason、拒绝细节、退出码、privileged 执行所用门禁——绝不含命令文本、token 或自由文本。事件不带 surface 元数据。./invariant 伴生在权威流提交前校验协议:终结转移必须跟随同 action 的匹配 armed 转移(accepted shutdown 还须同 reason);privileged-bash-executed 必须跟随 self-bash armed 转移并携带 matcher 与审批门禁。
遥测脱敏
telemetry/record 脱敏器把被拦截的 bash 命令文本、dsh_self_bash 命令文本与 dsh_self_* 的 confirmation_token 值替换为固定占位符(ledger 导出)。规范 session 日志不动——模型必须能读回自己的 token。
模型体验
拦截引导(固定,逐字注入)
#### 模型看到的内容
规范宿主-kill 命令被拦截后,该 agent 收到下面的引导。受控工具启动即注册但隐藏,平时不会出现在工具列表——这段引导正是模型首次获知工具名与 payload 的方式。
You are attempting to terminate the DeepSeek Harness host process. This command was blocked. Do not rewrite, encode, or route around the denial.
For a controlled exit, call dsh_self_exit with:
{"reason_code":"user-request"}
For a controlled restart request, call dsh_self_restart with:
{"reason_code":"apply-configuration"}
(shown only when restartEnabled: true)
The first call returns a confirmation_token and performs no shutdown. Read that result, then call the same tool again with the unchanged reason_code and the returned confirmation_token.Known Limitations and Deferred Work
- 不是安全边界。 混淆、base64、命令替换、shell 函数、
setsid、解释器、kill -1、pidfd_send_signal、进程组信号、PTY(node-pty在ctx.subprocess外 spawn)、MCP、Code Runtime、cordis_mount(同进程、bash 等信任级)都能绕过 matcher。阻止宿主终止是 OS 隔离工作(PID namespace、seccomp、supervisor),不在此范围。 - web 重启无独立退出码。 web 面经
SIGTERM以 launcher 码退出;supervisor 无法凭码区分"请求重启"与普通退出。码精确通道需要 launcher 配合,已延后。退出只在 accepted 工具的 turn 结算(agent.whenIdle())后派发,带 10s 有界回退定时器。 - 重启工具默认隐藏(TBD)。
dsh_self_restart除非restartEnabled: true否则不注册——web 面无 supervisor,确认重启只会终止宿主而不自动复活它。代码路径保留;出现 supervisor 通道后再复活该流程。 - 隐藏注册是进程级且不撤销。 工具加载时注册在插件全局 context:所有 agent/session 按名可调用(执行仍受
allowedAgents围栏),但整个插件生命周期内不进模型工具列表;HMR/插件卸载随 effect 移除。没有披露事件:守卫拒绝、引导,模型调用已注册工具。 - 被拒命令的误报。 用户确实要终止另一个 dsh 实例、或 wrapper 改变了
$PPID语义时会被报告。matcher 刻意保守以控制误报面:绝不使用子串匹配;动态构造($VAR、$(...)、反引号、通配符、heredoc)、赋值前缀(X=1 kill ...)与不支持的 shell 语法一律 abstain。合理终止场景现在有文档化出口:dsh_self_bash在 token + 审批门禁(full access 豁免)下重新执行被拦截命令;kill-host-pid按设计排除。 - 普通变量用法不在覆盖内。 matcher 在
pre-execute时只看到未展开的 shell 源码:写成kill "$HOST"、pid=...; kill "$pid"、循环或函数参数、以及任何目标来自变量的形式都会 abstain 并被真实执行。只有字面宿主 PID(或$PPID/pkill dsh/killall dsh)会被拦截。这是脚本和 agent 最自然的写法,因此变量构造的宿主 kill 可以在没有任何引导或审计的情况下终止宿主——模型看到的第一信号就是会话结束。这是 UX/拦截定位下的刻意覆盖边界(解析时$VAR与任何其他动态值无法区分);必须同时阻止变量构造 kill 的部署需要 OS 级隔离(PID namespace、seccomp、supervisor),而非改 matcher。可选的block-dynamic-kill严格策略(拒绝任何目标为变量的规范 kill)是可能的未来配置,但它也会误拦对非宿主进程的合法 kill,故不设默认。 pkill dsh/killall dsh仍命中宿主。 这些形式无法表达"仅另一个实例"——它们匹配每个 dsh 进程包括宿主。它们允许出现在privilegedBash.matchers(interceptModes的子集),但始终带 token 门禁,且danger-full-access下的人工审批豁免意味着确认的pkill dsh可终止宿主。不允许此行为的部署应把它们移出 privileged 集。- 内存态确认。 从持久化恢复的 session 无 armed token;协议从头重新武装。每个 agent 同时持有一个确认槽,按 action + reason + token 键控;错误 token 耗尽剩余确认机会并触发 cooldown,期间拒绝重新武装。
- 恢复授权延后。
session.resume尚不校验restartId/manifest 成员资格(调用方内存认知即 roster),且挂起 inbox 项恢复后不自动启动(agent-loop 无startPending)。不在浏览器内存中的 session 由用户打开时经惰性agentFor路径恢复。