☰
从 DeepSeek Harness 开始,定制一个属于自己的 Linux AI Agent(Day7:命令执行机制)
2026/9/30 13:44:59 网站建设 项目流程

本文分析当前工程中一条 Bash 命令从 Tool Body 进入 Shell Service、Sandbox Provider 和 Subprocess Runtime,最终成为 Linux 进程并返回结果的完整路径。主要阅读范围包括packages/shell/tool-bash、packages/shell/shell、packages/shell/bash-local、packages/shell/bash-sandbox、packages/sandbox、packages/sandbox/sandbox-policy、packages/subprocess/subprocess与packages/subprocess/subprocess-local。当前代码与 DeepSeek Harness 官方仓库后续版本可能存在差异,文中的结论只针对本文读取的版本。

上一篇已经沿着一次 Tool Call 分析到ToolDefinition.execute():模型产生的调用先成为tool/call,随后经过 Tool Runtime 的准入、执行和结果流水线,最后作为tool/result回到 Session。对于 Bash 工具而言,Tool Body 并没有直接调用 Node.js 的exec(),而是继续把命令交给ctx.shell。Shell 层负责命令语义、默认值、超时与前后台进程抽象,Sandbox 层负责把原始 argv 包装为受限执行,Subprocess 层则负责环境、stdio、输出保留、进程范围所有权和终止收敛。

这条执行链的复杂度主要来自两个事实。第一,模型提交的是 Shell 源码,而底层进程服务只接受明确的 argv,不负责字符串解释;第二,Agent 取消一次命令时,停止顶层 Bash PID 并不足以证明命令已经结束,Bash 可能已经启动管道、脚本、编译器或后台子进程。当前实现因此没有把“子进程退出”与“受管理进程范围清空”视为同一个条件,也没有把“返回超时结果”与“实际工作已经停止”视为同一件事。

1. 命令执行链的分层结构

当前 Web 和 Headless 的基础 bundle 默认挂载bash-sandbox作为ctx.shell实现,同时挂载sandbox-policy、sandbox-local和subprocess-local。模型调用bash后,运行路径可以整理为:

ToolRuntime -> @deepseek-ai/dsh-tool-bash -> 参数补充、提权审批、工作目录、DSH 环境 -> ctx.shell.resolve() -> ctx.shell.run() / ctx.shell.start() -> @deepseek-ai/dsh-bash-sandbox -> ctx.sandbox.confine(['bash', '-c', command], policy) -> 受限模式返回 runner argv -> danger-full-access 直接使用原始 argv -> @deepseek-ai/dsh-bash-local -> timeout deadline -> SubprocessSpawnSpec -> ctx.subprocess.spawn() -> @deepseek-ai/dsh-subprocess-local -> 环境清理与 stdio 绑定 -> Linux systemd scope 或 detached PGID fallback -> 输出 tail / spill -> TERM -> grace -> KILL -> managed range quiescence -> ShellRunResult / ShellProcess -> Bash canonical output -> Tool Runtime output schema / render -> tool/result

这些模块的边界并不是按文件夹机械拆分。tool-bash是模型协议适配器,负责 Tool Schema、展示、Session 工作目录、后台 Job 和提权参数;ShellExecutor是命令执行能力接口,不依赖模型与 Session;LocalBashExecutor将 Shell 请求变成bash -c和 Subprocess Spec;SandboxBashExecutor继承本地执行机械,只替换 argv 准备和沙箱结果分类;SubprocessRuntime只处理完全展开的进程请求,不理解 Bash 字符串、Tool Call、Session 或模型输出。

2. Bash Tool 的输入契约

tool-bash使用defineTool()注册名为bash的工具。模型必须提供command和description,还可以提供timeoutMs、workdir与run_in_background。挂载受限执行器时,schema 额外公开sandbox_permissions和justification。其中 description 只是 UI 展示信息,不参与底层命令;command 才会原样成为bash -c的源码。

参数 schema 只覆盖结构类型,Tool Body 开头的validateBashArgs()继续检查 DSL 无法表达的约束:command 和 description 去除空白后必须非空,timeout 必须为正有限数,sandbox_permissions 与 justification 必须成对出现,justification 也不能是空句子。ParameterSchemaSpec的根对象默认开放额外字段,因此某个部署没有在 schema 中公开后台或提权字段时,执行路径仍会再次检查组合是否真正支持,不能把“没有展示给模型”等同于“不可能收到”。

Bash Tool 的输出是严格的 union。后台分支只返回{ kind: 'background', jobId };前台分支返回退出码、终止信号、timeout/abort 分类、有效 timeout、stdout、stderr 和可选 sandbox 信息。Tool Body 返回 canonical JSON value 后,上一篇分析过的 Tool Runtime 会再次依据 output schema 校验,再由 render 把结构化结果投影为模型看到的文本。因此 Bash 执行层保存的是明确状态,[exit code: N]、[timed out after ...]和[killed by signal: ...]只是最后的模型表现,不是内部状态的唯一来源。

3. Session 工作目录与执行目录

resolveWorkdir()首先取得当前 Agent Session 的header.cwd。如果 Sandbox Policy 已经解析出 workspaceRoot,则以该 root 为准,确保“相对目录基准”和“沙箱允许写入的工作区”使用同一个文件系统身份。模型没有提供 workdir 时直接使用这个 Session 目录;模型给出相对路径时,把它拼接到 Session 目录下;绝对路径则原样交给后续层。

这里没有在 Tool 层对路径做realpath()或目录存在性验证。Tool 层的任务只是建立执行请求,真正的文件系统解释发生在执行环境中。相对路径也不是靠 Bash 里的cd持久化:每个调用都会新建一个 Shell,工具描述明确要求通过 workdir 表达目录。LocalBashExecutor.resolve()会为缺失的 workdir 使用配置项cwd,再退回process.cwd(),但正常 Agent 调用通常已经由 Session 提供目录。

这种设计保留了 Shell 调用的无状态性。一次命令中的cd、变量、函数和 shell option 都只属于当前bash -c,不会进入下一次 Tool Call。需要跨调用保存的事实必须进入文件、Session、Job 或其他明确的持久结构,而不是依赖一个隐含的长期 Shell 进程。

4. Sandbox Policy 的逐调用解析

SandboxPolicyService是文件副作用策略的统一来源。它保存部署默认 mode 与 fallback workspaceRoot,并通过 Session Projection 读取最后一条sandbox/mode事件。每次命令执行时,策略按以下优先级形成:

一次性已批准 mode > Session 中最后记录的 sandbox/mode > 部署默认 mode

workspaceRoot 则优先取 Session Header 中不可变的 cwd,没有 Session 或 Session 没有 cwd 时才使用部署 fallback。解析结果是完整的SandboxExecutionPolicy,包含 mode、绝对 workspaceRoot 和可选 sessionId。Bash Executor 不需要认识 Session;tool-bash在操作边界把当前 Session 交给 Policy Service,再把得到的纯数据 policy 放入 Shell 请求。

当前 mode 包括read-only、workspace-write和danger-full-access。前两个属于 confined mode,SandboxBashExecutor会请求ctx.sandbox生成受限 argv;danger-full-access不再调用 confinement provider,直接走本地 Bash 机械,但仍在前台结果中报告{ mode, denied: false },使调用方能够知道实际选择的策略。

5. 一次性 Sandbox 提权

模型在真实 denial 之后可以用同一条 command 加上sandbox_permissions和 justification 重试。Tool Body 在任何命令执行之前调用approveEscalation(),先判断目标 mode 是否相对当前有效 mode 严格变宽,再检查 Approval Service 和 Agent 是否存在,最后把目标 mode 与 justification 组成可审计理由交给用户审批。非严格变宽的请求不会弹出审批;通道缺失、无人可问、用户拒绝、交互取消和不可用通道全部 fail closed。

批准结果只覆盖这一次 Shell 请求。代码通过{ ...standingPolicy, mode: approvedMode }构造 per-call policy,没有写入sandbox/modeSession Event,也没有修改部署默认值。下一次命令仍从 Session 和部署配置重新解析 standing policy。这与权限预设切换的语义不同:前者是一个 Tool Call 的临时例外,后者是 Session 后续操作共享的状态。

Sandbox denial 也不是 Tool Runtime 层面的isError。命令确实启动并由受限 runner 返回结果,SandboxBashExecutor根据退出状态和 stderr signature 设置sandbox.denied,render 再追加统一 marker。模型因此可以区分普通非零退出与文件策略拒绝,并按工具说明进行一次受审批的原命令重试。Sandbox runner 自身不可用则属于基础设施故障:前台路径抛出SandboxUnavailableError,后台路径在 process sandbox facts 中标记runnerFailed,不能把 runner 故障误报成命令被策略拒绝。

6. Shell Request 的默认值与上限

LocalBashExecutor.resolve()把开放的ShellExecRequest转成完全展开的ShellExecSpec。默认前台 timeout 为 120 秒,配置允许的 per-call timeout 上限为 600 秒;模型给出的值会经过clampTimeout(),不是无限制接受。stdout 默认内存预算为 64,000 bytes,stderr 使用相同 executor 配置上限,每个 stream 的默认 spill 上限为 64 MiB,TERM 到 KILL 的默认 grace 为 3 秒。

resolve 以后,run 和 start 不再重复读取默认值。这一点让同一调用即使跨过异步 sandbox 准备,也不会在中途因热更新配置而拼出前后不一致的 spec。ShellExecSpec还保留 stdin、普通 env、Harness 管理的 dshEnv 和 sandboxPolicy;模型面对的 Bash Tool 不公开 stdin 与任意 env,其他可信插件可以通过 Shell Service 使用这两个能力。

前台 stdout 可以由可信调用方单独申请stdoutMaxBytes,stderr 始终使用 executor 的普通输出上限。模型 Bash Tool 没有暴露这个参数,避免模型任意扩大宿主内存预算。后台 start 明确忽略 timeoutMs,生命周期改由 Job 取消信号和ShellProcess.kill()管理。

7. 子进程环境的组合顺序

Shell 层先加入NO_COLOR=1、TERM=dumb、PAGER=cat和GIT_PAGER=cat,减少颜色控制符、分页器和交互终端行为对 Tool Result 的污染。普通调用方 env 覆盖这些 model-friendly 默认值,当前调用收集到的 dshEnv 最后合并,因此 Harness 管理的DSH_*事实不能被普通 env 冒充。

Subprocess 层不会直接继承完整的process.env。scrubbedParentEnv()删除名称匹配KEY|PASSWORD|SECRET|TOKEN的凭据形环境变量,并删除所有环境中的DSH_*;PATH、HOME、locale 和代理变量保留。显式 spec.env 在 scrub 以后合并,所以可信调用方仍可以有意识地恢复某个值,undefined则作为 tombstone 删除普通 ambient entry。Windows 合并按环境变量名大小写不敏感处理,POSIX 使用通常的区分大小写语义。

这层清理防止 Harness 自身的模型 API Key、Session 身份和旧 DSH 上下文无意进入命令。与此同时,代理设置会按父进程已经解析的策略重新覆盖,并为子 Node 进程带上使用环境代理所需的选项,避免宿主经过代理而子进程悄悄直连。

8. 原始 Bash argv 与受限 argv

未启用 confinement 时,前台run()和后台start()都使用精确 argv:

['bash', '-c', command]

Subprocess Service 接受的是 argv 数组,不会再次把数组拼成 Shell 字符串。真正进行 Shell 语法解析的只有bash -c,因此重定向、管道、变量展开和 heredoc 都发生在 Bash 内部,而不是 Node spawn 或 Subprocess Runtime 中。

SandboxBashExecutor在 confined mode 下调用ctx.sandbox.confine(['bash', '-c', command], policy, signal)。Provider 返回ConfinedArgv,其中既有真正交给 Subprocess 的 runner argv,也有 enforcement 完整度、denial signatures、runner failure rules 等结算分类事实。前台 sandbox 准备与命令执行共享同一个 deadline;准备阶段已经超时会返回尚未 spawn 的 timedOut 结果,外部 Abort 则继续作为取消抛出。

danger-full-access不经过 runner,confined mode 则必须返回能够实际实施限制的 argv或失败。代码不允许 sandbox provider 静默退回未受限的原始命令。具体 Linux backend 可以选择适合宿主的平台机制,但上层只消费统一的 ConfinedArgv 与结算事实。

9. 前台命令的 Deadline 与原因分类

LocalBashExecutor.runArgv()使用deadline(spec.signal, spec.timeoutMs, 'BASH_TIMEOUT')把调用方 AbortSignal 与 executor timeout 合并成一个信号。这个信号同时覆盖异步 sandbox 准备和后续 Subprocess。准备函数与 deadline 竞争;只有本层拥有的BASH_TIMEOUT首先触发时,尚未 spawn 的调用才返回timedOut: true、空输出和 null exit facts,其他取消原因继续向上抛出。

命令已经 spawn 后,Subprocess Runtime 只响应 signal 并开始终止,不解释取消原因。handle.donesettle 后,Bash Executor 再通过timeoutOf(d.signal, 'BASH_TIMEOUT')判断是否是本层 timeout;如果 fused signal 已 abort 但没有本层 timeout code,则记为aborted。两者互斥,竞争发生时以第一个 Abort 原因作为最终分类。

Tool Body 在拿到ShellRunResult后,对普通非零退出、timeout 和信号终止都返回成功的 canonical foreground value;只有result.aborted会重新抛出 code 为ABORTED的 HarnessError,使 Tool Runtime 将本次调用标成 isError。普通 timeout 会由 render 输出 marker,但不是工具协议失败。这样的区分允许模型阅读已产生的 stdout/stderr、退出状态和 timeout 事实,而用户主动取消则沿 Agent 取消路径闭合。

10. Subprocess Spec 与显式 stdio

spawnSpec()把 Shell Spec 转换成无默认值的SubprocessSpawnSpec。argv、cwd、stdin、stdout、stderr、grace、signal 和 env 全部明确指定。模型 Bash 调用没有 stdin 时使用ignore,底层 fd 0 对应空输入;可信调用方提供 stdin 字符串时使用 batch mode,Subprocess 启动后写入并关闭。stdout 与 stderr 分别进入 collect mode,而不是交给exec()一次性积累在内部 Buffer 中。

Subprocess seam 还支持原始pipe、父进程inherit和独立 control channel,但 Bash Tool 使用的是 bounded collect。SubprocessHandle.done只包含 exitCode 和 signal,不包含 timeout、取消与输出;cause classification 属于持有 deadline 的 Bash 层,输出则通过handle.collected的 offset reader读取。这种拆分使批量命令和后台增量读取可以共用同一个 Collector。

Subprocess Spec 在同步入口检查 grace、预取消状态和 argv[0]。预取消会在创建 handle 之前直接拒绝,空程序名与非法 grace 也不会进入系统 spawn。SubprocessRuntime 不对命令设置默认工作目录或输出预算,避免多个消费者共享一套隐藏进程策略。

11. 有界内存输出与 Spill 文件

OutputCollector为 stdout 和 stderr 各维护一个有界内存尾部。新 chunk 到达后,collector 记录完整流的累计 byte offset;超过 maxBytes 时从头部丢弃整 chunk 或精确裁掉头部字节,始终保留最后 maxBytes。保留尾部是有意选择,因为错误、编译结论和最终统计通常靠近命令输出末尾。

配置 spill 时,第一次内存溢出会在 OS 临时目录下的私有目录中创建随机文件,使用 exclusive create 和0600权限,并把此前已收集的 chunk 与后续 chunk 写入。只要完整输出没有超过 maxSpillBytes,这个文件就是完整流;一旦超过 spill 上限,文件会关闭并删除,后续只保留内存尾部,防止“内存有界但磁盘无限”。close 失败也会停止公开 spill path,因为文件尾部可能不可靠。

前台结算通过readFrom(0)读取保留内容,offset 已滑出窗口时把 truncated 设为 true,并在存在可靠 spill 时返回路径。render 会在输出尾部添加[output truncated; full output: ...]。后台读取维护独立 stdoutOffset 与 stderrOffset,每次只返回上次位置之后的 delta;如果调用者轮询太慢导致 offset 滑出内存窗口,read 标记 lossy,并给出可用 spill 文件。offset 属于读取者而不是 Collector 内部游标,因此多个可信消费者可以独立读取,不会互相吃掉输出。

12. stdout、stderr 与退出状态的模型呈现

前台 render 先放 stdout,再在需要时追加[stderr]区段。两者都为空时输出(no output)。Sandbox denial、timeout、signal 和非零 exit code 作为 marker 追加,exit marker保持最后,供 UI presenter 解析为 terminal card 的 exit pill。

非零 exit code 不会把 Tool Result 标成 isError。命令已经按请求正常启动并返回,退出码属于命令领域结果,模型需要依据 stderr 和 exit marker继续判断。基础设施错误才以异常进入 Tool Runtime,例如 spawn 失败、sandbox runner 不可用或调用被 Agent 取消。信号终止也保留在结构化 ShellRunResult 中;timeout 即使被命令 trap 并最终 exit 0,timedOutmarker 仍然存在,避免被表面退出码覆盖。

Sandbox denial同样独立于 exit code。Provider 使用自己返回的 denial signature 和 runner failure rules 对 stderr 进行分类;runner failure 优先于 denial,因为 runner 没有成功执行内部命令。不同 sandbox backend 的诊断文本可以不同,上层不把某个固定 Linux 错误字符串写死成全局判断。

13. Linux Managed Range 的优先路径

LocalSubprocessRuntime在 Linux 上优先探测 native managed range。探测要求私有 runner 可用、libcexecve绑定可加载,并且当前用户的 systemd manager 支持所需的 transient scope 调用。深度探测成功后会缓存正向结果,后续仍用轻量 manager probe确认 user manager可达;条件不满足时才选择 fallback,并只警告一次当前进程使用了更弱的进程树约束。

native 路径通过systemd-run --user --scope --quiet --collect --expand-environment=no创建唯一 transient scope。私有 runner使用文件传递完整 cwd、env 和 control 配置,进入 scope 后消费 launch request,最终以execve替换自己并运行目标 argv。--expand-environment=no与文件化环境避免 systemd 对 argv 或环境做第二次意外展开。

这个 scope 是命令的 managed range。终止时SystemdScopeOwner调用systemctl --user kill --kill-whom=all --signal=... unit,不是只向systemd-run客户端或顶层 Bash 发信号。waitForExit()轮询 unit 的 LoadState、ActiveState 和 TasksCurrent,只有 scope 不再活动或已证明为空才完成。启动尚未完全建立时还保留 direct process/group fallback,处理取消与 bootstrap 建立之间的竞态。

systemd scope 的价值不只在于发送信号范围更大,还在于提供独立的可观察所有权。顶层命令退出后,只要 scope 中仍有成员,Subprocess Runtime 仍能保留 handle 并在 teardown 时终止它们。普通父子 PID 关系会因 double-fork、reparent 或 Shell 退出而改变,scope/cgroup 的成员关系更适合作为“这次命令仍然拥有多少工作”的判断依据。

14. Detached PGID Fallback

Linux native prerequisites 不可用、运行在 macOS、Windows 或测试指定 fallback 时,普通 spawn 走spawnSubprocess()。POSIX 设置detached: true,使子进程成为新 process group leader;signal 使用负 pid 发送给整个 process group,失败时再尝试 direct child。Windows 不使用 detached group,而是通过taskkill /PID <pid> /T /F终止进程树。

fallback owner 的waitForExit()通过 process group 存活探测等待范围为空;Linux 在 direct child 已 settle 后还会检查/proc中是否存在 live group member,避免僵尸 group 表象阻塞。尽管这比只等待 ChildProcess exit 更完整,源码仍明确把它称为 weaker containment:脱离原 process group 或逃出可观察父树的后代不保证被终止,也不保证延迟 waitForExit。文章分析当前 Linux 机制时,不能把 fallback 的process.kill(-pid, ...)当成全部实现。

15. TERM、KILL 与真实收敛

bindManagedProcess()把 platform launch 与公共 SubprocessHandle 生命周期绑定。spec.signal abort 或调用terminate()时,代码只允许第一次进入 termination,立即启动 managed range observation,先发送 SIGTERM,并设置 grace timer;grace 到期仍未证明 range 消失时再发送 SIGKILL。Windows owner 对任意阶段都执行强制 tree termination,但接口仍保持同一语义。

顶层 ChildProcess 的 direct outcome 与 managed range exit 是两条不同 Promise。handle.done在 direct process exit 并且收集管道结束后 resolve;继承 stdout/stderr fd 的幸存后代可能让 pipe 保持打开,因此还设有同一个 graceMs 的 pipe drain边界,避免结果永久不 settle。该边界只销毁 Harness 自己的 collect stream;raw pipe属于调用者,不能被收集器擅自关闭。

即使 direct process 已经 settle,SIGKILL escalation timer也不会立即清除,因为 managed range 中可能仍有后代。只有 owner 的waitForExit()确认范围为空,才取消后续 escalation 并移除 abort listener。这正是“工具 Promise 已返回”“顶层 Bash 已退出”和“本次命令拥有的全部工作已停止”之间的区别。

16. Runtime 卸载与宿主退出

LocalSubprocessRuntime持有所有 live ordinary handles 与 terminal handles。正常 Cordis disposal 时,它先对每个普通 handle调用 terminate,同时等待handle.done和handle.waitForExit();terminal 则调用自身的 awaited terminate。全部结果通过Promise.allSettled()收集,一个对象清理失败不会阻止其他对象开始终止,最后再合并抛出失败。

Node 的同步exit阶段无法 await,因此 Runtime 还注册terminateForHostExit()。该路径直接对每个仍受控的范围执行最终强制终止:systemd owner 同时尝试 direct SIGKILL 和 unit-wide SIGKILL,fallback owner执行 process group或 taskkill。它不声称已经等待 quiescence,只是宿主退出前的最后兜底。正常 disposal才是能够承诺等待 managed range 清空的主路径。

后台进程之所以能穿过 Shell Executor 自身热重载而继续存在,也来自所有权位于 Subprocess Runtime。ShellProcess只是对 SubprocessHandle 的适配,真正 live set 由ctx.subprocess保留;只有 Subprocess Service 的 composition teardown才会统一停止并等待这些进程。

17. 后台命令与 Job 所有权转移

模型设置run_in_background: true时,Bash Tool 不直接用当前 Tool Call signal 启动长期进程。它先确认后台能力和 Jobs Service 可用,并检查 Tool Call尚未取消,然后调用jobs.start()。Job 的 run callback 才获得 Job 自己的 AbortSignal,并以该 signal完成ctx.shell.resolve()和ctx.shell.start()。当 jobs.start返回 id 时,长期工作的取消所有权已经从 Tool Call 转给 Job。

processJob()内部还有一个 controller,用于覆盖“Job 已取消但 ShellProcess 仍在异步准备”的窗口。取消发生在 handle 发布前,controller使准备 reject;发布后则额外调用process.kill()。done Promise会等待准备与底层 process.done完整收敛,然后把 ShellProcess 状态映射为 JobOutcome。非零退出仍属于 completed,detail保存 exit code;signal kill属于 killed;准备阶段真正失败才是 failed。

后台readOutput()是消费式的增量视图,连续job_output不重复返回旧内容。stderr仍用[stderr]分段,lossy读取附带完整 spill path,sandbox runner failure 与 denial marker也在 process settle后进入输出。后台启动 Tool Result本身只包含 job id,不把尚未完成的命令伪装成一次已完成 Bash 结果。

18. Shell、Terminal 与 Persistent Bash 的边界

本文分析的是一次性 Bash Tool。它始终创建新bash -c,stdin默认关闭,TERM=dumb,也不分配 PTY。需要持续会话、交互式程序、前台 process group inspection 或发送控制字符时,应当使用 Terminal/PTY能力,而不是把一次性 Bash Tool扩展成隐式持久 Shell。

Subprocess seam为此另外定义spawnTerminal(),本地实现使用 node-pty并管理完整 OS session、terminal output、resize、foreground group signal和 awaited termination。tool-bash-persistent建立在另一条持久 Shell抽象上。两种路径与本文的 ordinarySubprocessSpawnSpec有共同的环境与 managed range理念,但 stdio、信号目标和所有权契约不同,不能把普通 pipe subprocess 的行为直接套到 PTY。

19. 命令结果的语义分层

当前实现至少区分五层结果,阅读日志和设计新工具时需要保持这种分层:

层级结果形态主要含义
OS processexitCode / signal顶层进程的直接退出事实
Subprocessoutcome + collected readers进程结果、输出与 managed range 生命周期
ShelltimedOut / aborted / sandbox调用方 deadline 与执行策略分类
Bash canonical valueforeground / background unionTool output schema校验的结构化值
Tool Resultcontent / isError / meta模型和 Session 消费的规范结果

非零 exit code 在 OS、Subprocess、Shell 和 Bash 层都是正常可描述结果;Tool Result仍可成功。spawn ENOENT、Sandbox runner failure等基础设施错误会抛出并成为 isError。Bash executor timeout通常形成带 marker的成功 Tool Result,Agent取消则形成 ABORTED error。把这些状态压成一个 boolean success会丢失模型继续诊断所需的信息,也会让回放无法区分命令问题、策略拒绝和运行基础设施问题。

20. Linux 专用工具的实现边界

未来实现 systemd、Docker、进程和网络工具时,可以复用 Subprocess Service,但不应全部退化成bash字符串模板。专用工具应当用结构化参数表达目标和动作,用 canonical output保存 exit facts之外的领域信息,并根据副作用声明并发安全性。需要调用 CLI 时,可以构造明确 argv直接交给 Subprocess,避免再经过bash -c的字符串解释;需要 Shell语法时才使用 Shell Service。

Sandbox Policy、Approval和managed range仍然适用。一次 systemctl restart需要明确的权限与审计;Docker build可能产生大输出,需要 bounded collect和spill;日志跟随属于长期任务,应该转给Job或Terminal所有权;任何启动外部进程的工具都必须传递 AbortSignal,并等待底层工作真实收敛。专用 Tool只是收窄模型协议,不能绕过现有执行边界。

21. 最终结论

DeepSeek Harness 的命令执行机制不是一层 Shell 包装,而是一条逐级收紧的能力链。Bash Tool负责模型参数、Session身份、工作目录、一次性提权和前后台选择;Shell Executor负责默认值、deadline、环境和命令结果;Sandbox Executor把原始bash -cargv替换成受限 runner argv并分类 denial;Subprocess Runtime负责无凭据泄漏的环境、显式 stdio、有界输出、spill文件和可终止的进程范围;Linux本地实现优先用user-systemd transient scope建立可观察所有权,不可用时才退回detached process group。

取消语义贯穿了整条链。Tool Call signal进入Shell deadline,deadline signal进入Subprocess Spec,Subprocess abort触发managed range的TERM到KILL升级,Bash层在进程settle后判断首个取消原因,Tool Runtime再把Agent取消收口成规范结果。任何一层都没有用“先返回一条错误,后台进程以后再说”的方式伪造完成。对于能够修改文件、启动服务和管理系统资源的Linux Agent,这种对真实收敛的坚持比单纯执行成功更重要。

完成命令执行链之后,下一步可以进入第一项Linux专用能力。合适的起点不是另一个自由文本命令工具,而是只读的系统状态采集:通过结构化接口返回操作系统、CPU、内存、磁盘、网络和服务状态,再比较它与通用Bash在参数约束、输出稳定性、权限审计和模型可用性上的差异。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询