Expose agent session identity to tools and hooks
向工具与钩子公开 agent 会话标识
An agent can identify its workspace through `session.header.cwd`, but a model using bash cannot reliably identify the session that owns the call. Resume, forks, and concurrent parent/child agents make any ambient guess unreliable, while future plugins may need to expose other harness-owned environment facts to shell commands. The boundary must preserve two properties: the owner of a fact decides how to resolve it, an
English
Problem
An agent can identify its workspace through session.header.cwd, but a model using bash cannot reliably identify the session that owns the call. Resume, forks, and concurrent parent/child agents make any ambient guess unreliable, while future plugins may need to expose other harness-owned environment facts to shell commands.
The boundary must preserve two properties: the owner of a fact decides how to resolve it, and every child receives a per-execution snapshot rather than process-global mutable state. In particular, a nested harness must not leak its ambient DSH_* values into a child whose current agent, persistence backend, or configuration differs.
Decision
The model-facing bash package owns a ctx.shellEnv registry. A contributor declares its stable name, every DSH_* key it may return, a description for each key, and resolve(execution: ToolExecution). Duplicate contributor names, duplicate key ownership, reserved keys, malformed declarations, undeclared runtime output, and non-string output fail loudly. Registration is a Cordis effect and is removed with the contributing plugin fiber. list() exposes declarations without running resolvers, keeping the environment API enumerable for diagnostics and future prompt/UI consumers.
The registry rebuilds a trusted overlay for every foreground and background bash ToolExecution:
DSH_HOMEis always the absolute configured Harness home. The standalone@deepseek-ai/dsh-home-pathsutility owns its precedence: explicitdshHome, then ambient$DSH_HOME, then~/.dsh.DSH_SHELL=1is always present and identifies a model bash child managed by DeepSeek Harness.DSH_SESSION_IDis present when the execution has an agent and equalsagent.session.header.id.
A transcript-location fact is deliberately absent. An earlier form of this decision also extended the persistence seam with a locate() path query feeding a DSH_SESSION_JSONL variable and the hook bridges' transcript_path; the persistence export and pre-release trims note owns removing that half — the paths were only readable with compression disabled, and the seam no longer exposes artifact locations.
Plugins that need shell-visible facts depend on the registry and register their own keys; they do not modify process.env.
The bash seam exports DSH_ENV_PREFIX as the single namespace source and derives DshEnvironmentKey from its typeof. Tool-bash derives built-in names and model guidance from that constant, while executors use it for ambient filtering. The seam carries the managed overlay separately as ShellExecRequest.dshEnv / ShellExecSpec.dshEnv: ordinary env remains the general in-process plugin surface used by hooks, while dshEnv is typed to managed keys. The local executor removes every inherited ambient managed key, applies its ordinary scrub/terminal environment/explicit env, and finally merges the trusted dshEnv snapshot, so an env entry can never displace a managed value. This guarantees that a missing value means absent now rather than inherited from an outer or previous harness. The model-facing tool still ignores model-supplied env/stdin arguments.
The bash tool description teaches only the durable convention: current harness environment facts are available through managed $DSH_* variables and may be inspected when needed. It does not enumerate persistence-specific keys or add a permanent system-prompt section. Tool schemas are already logged in request headers and tool output is logged as tool/result, so no new session event is required.
The Claude Code and Codex hook bridges keep transcript_path in their wire payloads for protocol shape but always send '' (Claude Code) / null (Codex); the persistence export and pre-release trims note owns that degradation.
Peer product findings
Peer products separate stable identity from physical storage. Codex injects stable CODEX_THREAD_ID into spawned shells while recorder and hook integrations own transcript paths. Claude Code supplies session_id and transcript_path as structured hook/status input. OpenCode carries identity in structured tool context; Kimi Code expands a session placeholder; Reasonix keeps the active session path on its controller. The portable rule is to inject identity at the invocation boundary, let storage resolve location, and never use a process-global current-session variable in a concurrent harness.
Lifecycle and persistence semantics
A fresh session receives its id before the first turn, so its first bash call can read DSH_SESSION_ID. Resume reuses the loaded header and therefore the same id. Fork and spawn create new session ids. Parent and child calls resolve from their own ToolExecution.agent; each command receives an immutable snapshot even when calls overlap. The registry is effect-scoped and HMR-safe.
dshHome is session-independent deployment context. Agent-core resolves one value through @deepseek-ai/dsh-home-paths and routes it to both tool-bash and local skill discovery; standalone consumers call the same resolver. If top-level dshHome and skills.local.dshHome are both supplied and resolve differently, composition fails instead of exposing contradictory homes. Persistence may change independently without freezing its facts into the session prefix.
Testing
Unit coverage pins registry declaration validation, effect disposal, per-execution collection, the dshHome precedence, and the local executor's DSH_* scrub/rebuild order. Request-recording tests cover foreground/background snapshots, no-agent calls, ignored model env, and parent/child isolation. Both hook bridge suites pin the constant degraded transcript dialects.
A keyless full-loop integration drives the real agent loop, JSONL persistence, tool-bash, and bash-local on the first turn. The child prints DSH_HOME, DSH_SHELL, session id, and an inherited stale sentinel; the test verifies current values, absence of the stale variable, and the eventual persisted header. Snapshot coverage pins the generic bash description in the recorded request header. No with-key test is required because the contract is deterministic local execution rather than model choice.
Alternatives considered
Only an id plus find. Search cannot know a custom root or backend layout and races under multiple sessions.
Global process.env. Concurrent agents would overwrite one another and nested harnesses would inherit stale current-session values.
A typed waterfall event. Listeners cannot declare ownership without running, and later listeners can silently overwrite keys. A registry detects key conflicts at registration and remains enumerable.
Have each persistence backend register bash env directly. That reverses the dependency from storage into one consumer and forces bash into deployments that do not use it.
A model-facing session_info tool. It adds schema and another call while bash already supplies the query API; the registry generalizes to future environment facts without one tool per fact.
Consequences
Every model bash child receives current Harness home and shell identity, and agent calls additionally receive stable session identity. The managed DSH_* facts inside these children come from the harness: ambient values are removed, current trusted values are re-added last, and an ordinary caller's env entry cannot displace them.
The namespace is discoverable but not secret. DSH_HOME can reveal a configured root, and a command can override variables inside its own shell syntax. Consumers treat them as correlation and environment facts and rely on sandbox/filesystem policy rather than variable secrecy for authorization.
中文
问题
agent(智能体)可以通过 session.header.cwd 识别其工作区,但使用 bash 的模型无法可靠识别当前调用所属的会话。恢复、fork 以及并发运行的父子 agent 会让任何来自环境的猜测都不可靠,而未来的插件也可能需要向 shell 命令公开其他由 harness 所有的环境事实。
这项边界必须维持两个属性:事实的所有者决定如何解析该事实;每个子进程接收每次执行的快照,而不是进程级可变全局状态。尤其是嵌套 harness 不能把环境中的 DSH_* 值泄漏给当前 agent、持久化后端或配置均可能不同的子进程。
决策
面向模型的 bash 包拥有一个 ctx.shellEnv 注册表。贡献方声明稳定名称、它可能返回的每个 DSH_* 键、每个键的说明,以及 resolve(execution: ToolExecution)。贡献方名称重复、键所有权重复、使用保留键、声明格式错误、运行时输出未声明或输出不是字符串时,系统都会明确失败。注册属于 Cordis effect,并随贡献插件的 fiber 一同移除。list() 无需运行解析器即可公开声明,从而让环境 API 可供诊断工具和未来的提示词/UI 消费方枚举。
注册表会为每次前台和后台 bash ToolExecution 重新构建受信任的覆盖层:
DSH_HOME始终是配置的 Harness home 绝对路径。独立的@deepseek-ai/dsh-home-paths工具库规定其优先级:显式dshHome,其次是环境中的$DSH_HOME,最后是~/.dsh。DSH_SHELL=1始终存在,用于标识由 DeepSeek Harness 管理、面向模型的 bash 子进程。- 执行具有关联 agent 时,
DSH_SESSION_ID存在并等于agent.session.header.id。
transcript(文本记录)位置事实被有意省略。本决策的早期形式还在持久化 seam 上增加了 locate() 路径查询,为 DSH_SESSION_JSONL 变量和钩子桥接层的 transcript_path 提供来源;持久化导出与预发布精简 Note 负责移除这一半——这些路径只有在禁用压缩时才可读,该 seam 也不再公开产物位置。
需要向 shell 公开事实的插件依赖该注册表,并注册各自的键;它们不修改 process.env。
bash seam 导出 DSH_ENV_PREFIX 作为唯一的命名空间来源,并派生 DshEnvironmentKey,其来源是该常量的 typeof。tool-bash 从该常量派生内置名称与模型指引,执行器则使用该常量过滤环境中已有的值。seam 通过 ShellExecRequest.dshEnv/ShellExecSpec.dshEnv 单独传递受管理的覆盖层:普通 env 仍是钩子所用的通用进程内插件接口,dshEnv 则以类型约束为受管理键。本地执行器移除环境中继承的全部受管理键,依次应用普通清理、终端环境和显式 env,最后合并受信任的 dshEnv 快照,因此 env 条目永远无法顶掉受管理的值。这保证了值缺失表示它当前确实不存在,而不是从外层或先前的 harness 继承而来。面向模型的工具仍忽略模型提供的 env/stdin 参数。
bash 工具说明只讲解持久约定:当前 harness 环境事实通过受管理的 $DSH_* 变量提供,可以在需要时查看。它不会枚举持久化专用键,也不会添加永久的系统提示词章节。工具 schema 已记录在请求 header 中,工具输出则记录为 tool/result,因此无需新增会话事件。
Claude Code 和 Codex 钩子桥接层为保持协议格式,仍在线上 payload 中保留 transcript_path 字段,但始终发送 ''(Claude Code)/null(Codex);这项降级由持久化导出与预发布精简 Note 负责。
同类产品调研
同类产品把稳定标识与物理存储分开处理。Codex 向 spawn 的 shell 注入稳定的 CODEX_THREAD_ID,而 recorder 和钩子接口负责提供 transcript 路径。Claude Code 通过结构化的钩子/状态输入提供 session_id 和 transcript_path。OpenCode 在结构化工具上下文中携带标识;Kimi Code 展开会话占位符;Reasonix 则把活动会话路径保存在控制器上。可移植的规则是:在调用边界注入标识,由存储层解析位置,绝不在并发 harness 中使用进程级的当前会话全局变量。
生命周期与持久化语义
新会话在第一个轮次之前获得 id,因此它的首次 bash 调用即可读取 DSH_SESSION_ID。恢复操作复用已加载的 header,因此 id 不变。fork 和 spawn 会创建新的会话 id。父子调用分别从自己的 ToolExecution.agent 解析事实;即使调用重叠,每条命令也会收到不可变快照。注册表受 effect 作用域约束,并且可安全用于 HMR(热模块替换)。
dshHome 是与会话无关的部署上下文。agent-core 通过 @deepseek-ai/dsh-home-paths 解析出一个值,并将其同时传给 tool-bash 和本地 skill(技能)发现;独立消费方调用同一解析器。如果顶层 dshHome 与 skills.local.dshHome 均已提供但解析结果不同,组合会失败,而不会公开互相矛盾的 home。持久化可以独立变更,无需把其事实冻结到会话前缀中。
测试
单元测试覆盖注册表声明校验、effect 释放、逐次执行收集、dshHome 优先级,以及本地执行器清理并重建 DSH_* 的顺序。请求录制测试覆盖前台/后台快照、无 agent 调用、忽略模型 env,以及父子隔离。两套钩子桥接测试均锁定恒定的降级 transcript 方言。
一项无密钥的完整循环集成测试会在第一个轮次驱动真实的 agent loop、JSONL 持久化、tool-bash 与 bash-local。子进程打印 DSH_HOME、DSH_SHELL、会话 id 和继承的陈旧哨兵值;测试校验当前值、陈旧变量不存在,并最终检查持久化 header。快照测试会固定录制请求 header 中的通用 bash 说明。该约定属于确定性的本地执行,不涉及模型选择,因此无需带密钥测试。
考虑过的替代方案
只提供 id,再用 find。 搜索无法得知自定义根目录或后端布局,并且在多会话环境下存在竞态。
使用全局 process.env。 并发 agent 会互相覆盖,嵌套 harness 也会继承陈旧的当前会话值。
使用类型化 waterfall 事件。 监听器不运行就无法声明所有权,而后续监听器可以无提示地覆盖键。注册表能在注册时检测键冲突,并且保持可枚举。
让每个持久化后端直接注册 bash 环境。 这会反转依赖方向,让存储层依赖某一个消费方,并迫使未使用 bash 的部署也引入它。
增加面向模型的 session_info 工具。 bash 已经提供查询 API,新增工具只会多出 schema 和一次调用;注册表可以扩展至未来的环境事实,无需为每项事实增加一个工具。
影响
每个面向模型的 bash 子进程都会收到当前 Harness home 和 shell 标识,关联 agent 的调用还会收到稳定的会话标识。这些子进程中受管理的 DSH_* 事实来自 harness:系统移除环境中已有的受管理值、在最后重新加入当前受信任的值,普通调用方的 env 条目无法顶掉它们。
该命名空间可被发现,但并非秘密。DSH_HOME 可能泄露配置的根目录,而且命令可以在自己的 shell 语法中覆盖变量。消费方应把这些值视为关联信息和环境事实,并依靠沙箱/文件系统策略而不是变量保密性来完成授权。