DSH / Atlas
2026-07-08implementedfeature

The self-referential cordis toolset

自引用 cordis 工具集

Everything in this harness is a cordis plugin, but the agent running inside that plugin runtime cannot see or touch it: it cannot enumerate the services and events around it, cannot extend itself with a new tool mid-session, and cannot compose capabilities it invents. Handing the model that power is worth exploring — a self-referential agent that inspects and modifies its own runtime — but it raises three correctness

English

Problem

Everything in this harness is a cordis plugin, but the agent running inside that plugin runtime cannot see or touch it: it cannot enumerate the services and events around it, cannot extend itself with a new tool mid-session, and cannot compose capabilities it invents. Handing the model that power is worth exploring — a self-referential agent that inspects and modifies its own runtime — but it raises three correctness problems at once, and the design is about answering them rather than the raw "let the model run code" mechanic.

First, model-written registration must be validated where it happens: a malformed tool schema has to fail at registration, not when a later request tries to assemble it into a prompt. Second, model-written code has to call service APIs whose source it has never seen — guessed method signatures and, worse, guessed return-value shapes cost many steps of blind probing. Third, everything the model mounts must be fully disposable, by the model on demand and by the ordinary plugin lifecycle when the host plugin reloads, or a long session accretes orphaned listeners and tools.

Decision

The toolset ships as @deepseek-ai/dsh-tool-cordis and is demoed by examples/web-cordis. It gives the model three tools over the live Cordis runtime in the current DSH process: inspect it, mount an in-memory temporary Plugin, and unmount that Plugin to quiescence.

The vm isolates accidental global pollution, and the context façade hides framework internals. Neither restricts the authority of exposed services: a temporary Plugin can call ctx.shell with the host executor's privileges and reach the real filesystem and web services. It runs in the shared DSH runtime and may affect other sessions in that process. This is an opt-in development tool with bash-equivalent trust, not a security boundary or product default.

The three tools

ToolContract
cordis_inspectRead-only report over the live current-process runtime, one Markdown section per what value (omit what for all sections). plugins lists every live fiber; temporary lists only the temporary Plugins created by cordis_mount. An exact name with what: "api" or what: "events" narrows to one source-documented target.
cordis_mountEvaluates code now as an async JavaScript-function body in a node:vm sandbox and saves it nowhere. The returned Plugin is mounted under the internal cordis-dynamic group and tracked under a fresh process-local id (dyn-1, dyn-2, …).
cordis_unmountUnmounts one cordis_mount temporary Plugin by id and returns only after every owned tool, listener, service, timer, and effect reaches quiescence. It cannot remove Loader, configured, or installed Plugins.

cordis_inspect sections are services (every provided ctx service and owning fiber), plugins (every live plugin fiber), tools (what the model can call), temporary (the cordis_mount subset with id, running/pending state, provided and awaited services, and lifetime), api (live service signatures and referenced types), and events (harness events with dispatch mode and signature). Temporary Plugins remain active across later turns and disappear after cordis_unmount, toolset unload, or DSH restart; they are never restored automatically. Broad api and events reports omit full JSDoc to stay compact; an exact name returns one service or event with its original method/declaration JSDoc. A name is invalid with other sections, unknown targets fail, and an API target must be live. The model-facing tool descriptions carry the operational rules needed at call time; the generated tool catalog is their exhaustive rendering.

Sandbox semantics

Mount code runs as an async-function body in a fresh vm realm. Its documented API steers file, network, process, and timer access through Cordis services so mounts remain inspectable and disposable. Host-realm helpers still make Node escape possible, consistent with the trusted posture. vmTimeoutMs bounds only synchronous evaluation.

Sandbox globals are deliberately small: a tagged write-through console ([cordis:<id>] … on the host stdout/stderr, so a listener that fires long after the mount call still lands somewhere the user sees), the harness.defineTool / harness.registerTool registration pair, the encoding primitives fresh vm contexts lack (btoa/atob as host closures over Buffer — a sanctioned exception, Buffer itself is never exposed — plus TextEncoder/TextDecoder), and callable traps over the withheld Node APIs (require, setTimeout/setInterval/setImmediate/clearTimeout/clearInterval, fetch) that throw a redirect naming the cordis alternative. Only function-shaped globals are trapped; process and Buffer stay undefined so a typeof feature probe stays inert rather than detonating a throwing accessor.

Mount code crosses the vm boundary through three controls. Dual-realm instanceof recognizes both host and vm objects. harness.defineTool rebuilds the output schema/projectors in the host realm, snapshots the body value as host-owned JSON, and lets the registry enforce the canonical tool-output contract before observation. The mounted plugin receives a whitelist context façade, not a raw or pass-through Context; framework plumbing and context-valued returns are rejected. Service reads require a declared inject, preserving Cordis activation and unload semantics. ctx.tools.get exposes only the schema view, so mounted code cannot bypass ToolRuntime.execute by calling a definition directly.

The boundary normalizes unambiguous JSON-Schema forms into ParameterSchemaSpec, preserving integer, raw object openness, and required arrays. Direct DSL object nodes must declare additionalProperties; invalid vocabulary fails with the accepted alternatives. Parse, TypeScript, missing-return, Node-API, and duplicate-tool errors include the relevant source line or corrective contract without narrating implementation internals.

The internal group and temporary-Plugin lifecycle

Every temporary Plugin is a child of one internal cordis-dynamic group beneath the tool plugin, so ordinary fiber disposal handles toolset reload and unload. cordis_mount awaits settlement; startup failure disposes the fiber before returning an error. A settled pending Plugin remains visible with its missing injections. cordis_unmount awaits the Plugin fiber's disposal.

Temporary Plugins exist only in process memory. They create no Plugin file, install no package, change no cordis.yml or personal/project configuration, do not survive restart, and have no automatic save, promote, or install path. Keeping an experiment means asking the Agent to implement a normal project Plugin or installable profile bundle through the regular development workflow.

Cross-mount composition via provide/inject

Mounts relate to each other through ordinary cordis service semantics, with their ids as the lifecycle handles: mount A calls ctx.provide('foo', value), mount B declares inject: ['foo'] and activates the moment foo exists; mounted first, B stays pending and names the missing service; unmounting A sends B back to pending (its registrations unwound) and a later re-provide re-runs B's apply through a fresh sandbox façade; a duplicate provide fails loud with the owning fiber named. One realm caveat: a service value provided by a mount is a vm-realm object — method calls on it work from anywhere, but consumers must not assume host prototypes on it.

The generated API catalog

cordis_inspect serves API and event data from a generated catalog rather than a duplicated table. The generator reuses the Cordis catalog AST scan and emits service summaries, signatures, original service-method and event JSDoc, event modes, referenced type declarations, and the inherited context API. Ambiguous type names are omitted and oversized declarations are marked as truncated.

Freshness is gated like every generated artifact: pnpm run verify-cordis-api (in doc-sync) regenerates in memory and fails on any diff, so a JSDoc or public-signature edit cannot ship without regenerating the catalog the model reads. At runtime the inspect tool intersects the catalog with the live runtime rather than dumping it: broad reports render live catalogued services as summary + signatures, live services without a catalog entry (mount-provided ones) as name + owning fiber, catalogued services with no live provider tersely, and then the referenced type shapes. Exact-name reports render one live service or event with the original JSDoc immediately before each signature; keeping that detail opt-in avoids charging its token cost on exploratory listings.

Configuration, rendering, and observability

The plugin exposes one config field, validated by schemastery and documented in the config catalog: vmTimeoutMs (default 5000), the millisecond bound on the synchronous portion of code evaluation. The current model-facing names are cordis_inspect, cordis_mount, and cordis_unmount; the internal cordis-dynamic group name and dyn- id prefix remain structural vocabulary. All three tools render as generic cards per the tool cookbook: inspect is read, mount is execute carrying code as rawInput, and unmount is delete. Web conversation rows preserve those generic mechanics while giving the tools the action titles Inspect, Mount temporary Plugin, and Unmount temporary Plugin plus one shared Cordis accent; the mount row retains the shared JavaScript expansion and syntax highlighting.

Model-visible ⟺ logged holds with no new session event type: mount and unmount are visible through their logged tool/call / tool/result pairs, and any changed tool set is logged by the full changed request header emitted when schemas change between steps. Temporary Plugins are process memory, not session state: session resume rehydrates conversation history but never recreates them.

Alternatives considered

A structured per-capability registration tool instead of cordis_mount. The most tempting alternative is a cordis_register_tool with explicit name / description / parameters / code fields (and siblings cordis_register_listener, cordis_register_service, …) rather than a single "mount a plugin" primitive. It was rejected because its one real win — no plugin boilerplate for the single commonest case — does not pay for its costs, while a single mount primitive answers every capability at once.

DimensionStructured per-capability toolsSingle cordis_mount
Schema correctnessparameters is still model-written JSON needing unified-schema validation, merely one step earlierThe same validation runs at the sandbox boundary, with the same instructive errors
The code fieldAn execute body is still model-written JS in a vm; the realm and service-call correctness problems are unchangedOne sandbox, one normalization path, one guarded registration
Capability coverageTools only; listeners, services, inject relations each need another structured tool — an API that grows without boundOne vocabulary (a cordis plugin) covers every effect, present and future
Cross-mount compositionNot expressible in a tool-registration payloadNative provide/inject, ordinary cordis semantics
InspectabilityRegisters something the plugin list cannot show as a pluginWhat the model mounts is exactly what cordis_inspect renders
Model ergonomicsWins for the single most common case (no plugin boilerplate)Mitigated by the canonical recipe in the mount description plus boundary errors that teach the fix

The correctness investment therefore goes where it pays for every capability at once: the generated API catalog surfaced through cordis_inspect, and sandbox-boundary validation whose error messages teach the correct call. A structured registration tool remains addable later as sugar that synthesizes mount code; nothing here forecloses it.

A hand-maintained service/event reference in the tool. The first cut of the inspect tool carried a hand-written table of service method signatures. It was replaced by the generated api-catalog.ts because a hand table drifts from the JSDoc the moment a signature changes and nothing gates the drift, whereas the generated artifact is freshness-checked against the same AST the docs use.

A new cordis/mount session event. A durable event recording each mount's source and name has clear precedent (hook/invoked, compaction/start). It was declined for v1: mount and unmount are already visible as tool/call / tool/result pairs and the tool-set change is already logged as a full changed request header, so a dedicated event would only duplicate the record. It remains addable if an audit use case needs the mount source and name outside the tool call.

A hardened / capability-restricted sandbox. Trapping Node built-ins and handing mount code a whitelist façade rather than the raw context might suggest an intent to sandbox for safety. It is explicitly not that: the traps and the façade narrow the API mount code sees — steering it onto cordis services and away from leak-prone Node built-ins and framework internals — for correctness and to close the unguarded-context escape, but the capabilities the façade exposes (ctx.shell, ctx.fs, ctx.web) reach the real runtime, so it is not a security boundary. A real one (separate process, permission prompts) was out of scope for a dev/opt-in toolset and would fight the entire point — handing the model the live runtime.

Consequences

The toolset is a deliberate opt-in with a fully-privileged ctx, so a deployment adopts it as consciously as a bash tool. Several facts follow that the tool descriptions warn the model about directly: a waterfall listener (e.g. tools/pre-execute) that returns without calling next() short-circuits the chain, so a mounted listener can stop the agent's own tool dispatch (waterfall semantics); mount code runs inside a tool call of the current turn, so awaiting anything that resolves only after the turn deadlocks; vmTimeoutMs bounds synchronous evaluation only; and mounts do not survive session resume.

中文

问题

本 harness 中的一切都是 cordis 插件,但运行在该插件运行时内部的 agent(智能体)既看不到也碰不到它:它无法枚举周围的服务和事件,无法在会话中途为自己添加新工具,也无法组合自己发明的能力。赋予模型这种能力值得探索——一个能审视并修改自身运行时的自引用 agent——但这同时引发三个正确性问题,本设计的核心正是回答这些问题,而非单纯的「让模型执行代码」机制。

第一,模型编写的注册必须在注册发生时就完成校验:格式错误的工具 schema 必须在注册时失败,而不是等到后续请求尝试将其组装进提示词时才报错。第二,模型编写的代码需要调用它从未见过源码的服务 API——靠猜测方法签名、更糟糕的是猜测返回值结构,会消耗大量盲目试探的步骤。第三,模型挂载的一切都必须完全可释放:模型可以按需释放,普通的插件生命周期在宿主插件重载时也会释放,否则长会话会积累遗留的监听器和工具。

决策

该工具集以 @deepseek-ai/dsh-tool-cordis 发布,并由 examples/web-cordis 演示。它为模型提供三个工具,用于操作当前 DSH 进程中的活跃 Cordis 运行时:检查该运行时、挂载一个仅存于内存的临时插件,再将该插件卸载至完全停稳。

vm 隔离了意外的全局污染,上下文门面隐藏了框架内部细节。但二者都不限制已暴露服务的权限:临时插件可以调用 ctx.shell 以宿主执行器的权限运行命令,也能访问真实的文件系统和网络服务。它运行在共享 DSH 运行时中,可能影响同一进程的其他会话。这是一个需要显式启用的开发工具,信任等级与 bash 相当,不是安全边界,也不是产品默认配置。

三个工具

工具约定
cordis_inspect当前进程活跃运行时的只读报告,每个 what 值对应一个 Markdown 小节(省略 what 则输出全部小节)。plugins 列出全部存活 fiber,temporary 只列 cordis_mount 创建的临时插件。精确 name 搭配 what: "api"what: "events" 可收窄到一个带源码文档的目标。
cordis_mount立即在 node:vm 沙箱中把 code 作为异步 JavaScript 函数体求值,且不保存到任何位置。返回的插件挂在内部 cordis-dynamic 分组下,并用新的进程内 id(dyn-1dyn-2……)跟踪。
cordis_unmount按 id 卸载一个 cordis_mount 临时插件,并只在其自有工具、监听器、服务、定时器和其他 effect 完全停稳后返回。它不能删除 Loader、已配置或已安装的插件。

cordis_inspect 的小节是 services(每个已提供的 ctx 服务及所属 fiber)、plugins(全部存活插件 fiber)、tools(模型可调用的工具)、temporarycordis_mount 子集,包含 id、running/pending 状态、提供与等待的服务和生命周期)、api(活跃服务签名及其引用类型)和 events(harness 事件及分发模式和签名)。临时插件可跨后续轮次保持活跃,并在 cordis_unmount、工具集卸载或 DSH 重启后消失;系统绝不会自动恢复它们。宽泛的 apievents 报告省略完整 JSDoc;精确 name 返回一个服务或事件及其原始 JSDoc。其他小节不能搭配 name,未知目标会失败,而 API 目标必须处于活跃状态。面向模型的工具描述包含调用时所需的操作规则;生成的工具目录是这些规则的完整呈现。

沙箱语义

挂载代码以异步函数体的形式在一个新的 vm realm 中运行。其文档化的 API 将文件、网络、进程和定时器访问引导至 Cordis 服务,使挂载保持可审视和可释放。宿主 realm 的辅助手段仍然使 Node 逃逸成为可能,这与信任姿态一致。vmTimeoutMs 仅约束同步执行部分。

沙箱全局变量刻意精简:一个带标签的直写 console(在宿主 stdout/stderr 上输出 [cordis:<id>] …,这样在挂载调用之后很久才触发的监听器输出仍能落到用户可见的地方)、harness.defineToolharness.registerTool 注册对、新 vm 上下文缺少的编码原语(btoaatob 作为基于 Buffer 的宿主闭包——这是一个明确允许的例外,Buffer 本身从不暴露——加上 TextEncoderTextDecoder),以及对未暴露的 Node API 设置的可调用陷阱(requiresetTimeoutsetIntervalsetImmediateclearTimeoutclearIntervalfetch),这些陷阱会抛出一条重定向消息指明 cordis 替代方案。只有函数形态的全局变量才设陷阱;processBuffer 保持 undefined,这样 typeof 特性探测仍然无害,而不会触发会抛出异常的访问器。

挂载代码通过三道控制跨越 vm 边界。双 realm instanceof 同时识别宿主和 vm 对象。harness.defineTool 在宿主 realm 中重建输出 schema/投影器,将工具体返回值快照为宿主自有的 JSON,并让注册表在观测前强制执行规范工具输出约定。挂载的插件接收的是一个白名单上下文门面,而非原始或透传的 Context;框架内部机制和以上下文为值的返回会被拒绝。服务读取需要声明 inject,保留 Cordis 的激活与卸载语义。ctx.tools.get 仅暴露 schema 视图,因此挂载代码无法绕过 ToolRuntime.execute 直接调用定义。

边界将无歧义的 JSON Schema 形式规范化为 ParameterSchemaSpec,同时保留 integer、原始对象开放性和 required 数组。直接使用 DSL 的对象节点必须声明 additionalProperties;无效词汇会报错并给出可接受的替代方案。解析错误、TypeScript 错误、缺少 return、Node API 误用和重复工具名等错误信息包含相关源码行或纠正性约定,不叙述实现内部细节。

内部分组与临时插件生命周期

每个临时插件都是工具插件下方内部 cordis-dynamic 分组的子节点,因此普通的 fiber 释放即可处理工具集重载和卸载。cordis_mount 会等待 settlement;启动失败时在返回错误前释放 fiber。已 settle 但处于 pending 状态的插件仍然可见,并列出其缺失的注入。cordis_unmount 等待插件 fiber 的释放完成。

临时 Plugin 只存在于进程内存中。它不会创建 Plugin 文件、安装 package、修改 cordis.yml 或个人/项目配置、跨重启存续,也不存在自动保存、转正式或安装路径。若要保留实验结果,应让 Agent 通过常规开发流程实现普通的项目 Plugin 或可安装的 profile 组合包。

通过 provide/inject 实现跨挂载组合

挂载之间通过普通的 cordis 服务语义相互关联,以各自的 id 作为生命周期句柄:挂载 A 调用 ctx.provide('foo', value),挂载 B 声明 inject: ['foo'] 并在 foo 存在的瞬间激活;如果 B 先挂载,它保持 pending 状态并列出缺失的服务;卸载 A 使 B 回到 pending(其注册被撤销),之后重新 provide 会通过一个新的沙箱门面重新运行 B 的 apply;重复 provide 会明确报错并指出拥有该服务的 fiber。一个 realm 注意事项:由挂载 provide 的服务值是 vm realm 对象——从任何地方调用其方法都能工作,但消费方不得假设它具有宿主原型。

生成的 API 目录

cordis_inspect 从生成的目录提供 API 和事件数据,而非维护一份重复的表格。生成器复用 Cordis 目录的 AST 扫描,输出服务摘要、签名、原始服务方法与事件 JSDoc、事件模式、引用的类型声明以及继承的上下文 API。有歧义的类型名被省略,过大的声明被标记为截断。

新鲜度像所有生成产物一样受门禁约束:pnpm run verify-cordis-api(在 doc-sync 中)在内存中重新生成并在有任何 diff 时失败,因此 JSDoc 或公开签名变更如果不重新生成模型读取的目录就无法合入。运行时 inspect 工具将目录与活跃运行时取交集而非直接转储:宽泛报告把有目录条目的活跃服务渲染为摘要 + 签名,把没有目录条目的活跃服务(挂载提供的)渲染为名称 + 所属 fiber,简要列出有目录条目但无活跃提供方的服务,再附上引用的类型结构。精确名称报告渲染一个活跃服务或事件,并把原始 JSDoc 紧靠在每个签名之前;让该细节按需出现,避免探索性列表承担其 token 成本。

配置、渲染与可观测性

该插件暴露一个配置字段,由 schemastery 校验并记录在配置目录中:vmTimeoutMs(默认 5000),代码同步求值部分的毫秒上限。当前面向模型的名称是 cordis_inspectcordis_mountcordis_unmount;内部 cordis-dynamic 分组名和 dyn- id 前缀仍是结构性词汇。三个工具均按工具实操手册渲染为 generic 卡片:inspect 为 read,mount 为携带代码 rawInputexecute,unmount 为 delete。Web 对话行保留这些通用机制,同时为各工具设置操作标题 InspectMount temporary PluginUnmount temporary Plugin 以及统一的 Cordis 强调色;mount 行仍使用共用的 JavaScript 展开视图和语法高亮。

「模型可见 ⟺ 已记录」成立,且无需新的会话事件类型:mount 与 unmount 通过已记录的 tool/calltool/result 对可见,当步骤之间的 schema 发生变化时,系统发出的完整 request header 会记录工具集的任何变化。临时插件属于进程内存,而非会话状态:恢复持久化会话只会重建对话历史,绝不会重新创建它们。

曾考虑的替代方案

用结构化的逐能力注册工具替代 cordis_mount 最具吸引力的替代方案是一个带有显式 namedescriptionparameterscode 字段的 cordis_register_tool(以及配套工具 cordis_register_listenercordis_register_service……),而非单一的「挂载一个插件」原语。否决原因:它唯一的真正优势——对最常见的单一场景免去插件样板代码——不足以抵偿其代价,而单一的 mount 原语能一次性覆盖所有能力。

维度结构化逐能力工具单一 cordis_mount
schema 正确性parameters 仍然是模型编写的 JSON,需要统一 schema 校验,只是提前了一步同样的校验在沙箱边界运行,同样的指导性错误信息
代码字段execute 函数体仍然是 vm 中模型编写的 JS;realm 和服务调用的正确性问题不变一个沙箱、一条规范化路径、一处受保护的注册
能力覆盖面仅限工具;监听器、服务、inject 关系各需另一个结构化工具——API 无限增长一套词汇(cordis 插件)覆盖当前和未来的所有效果
跨挂载组合在工具注册载荷中无法表达原生 provideinject,普通的 cordis 语义
可审视性注册的东西无法在插件列表中显示为插件模型挂载的正是 cordis_inspect 渲染的
模型易用性对最常见的单一场景有优势(无插件样板)通过 mount 描述中的规范示例加边界错误信息教会正确调用来缓解

因此正确性投入放在能一次性为所有能力带来回报的地方:通过 cordis_inspect 呈现的生成 API 目录,以及沙箱边界校验(其错误信息教会正确的调用方式)。结构化注册工具日后仍可作为语法糖添加,由它合成 mount 代码;本设计不排斥这一可能。

在工具中手工维护服务/事件参考。 inspect 工具的第一版携带了一份手写的服务方法签名表。它被生成的 api-catalog.ts 取代,因为手写表在签名变化的瞬间就会与 JSDoc 脱节且没有门禁约束这种漂移,而生成产物的新鲜度由文档使用的同一套 AST 检查。

新增 cordis/mount 会话事件。 一个持久事件记录每次挂载的源码和名称,有明确先例(hook/invokedcompaction/start)。v1 中予以否决:挂载和卸载已经作为 tool/calltool/result 对可见,工具集变化已经作为完整的变更 request header 被记录,因此专用事件只会重复记录。如果审计用例需要在工具调用之外取得挂载的源码和名称,日后仍可添加。

加固的/能力受限的沙箱。 对 Node 内置模块设陷阱并向挂载代码提供白名单门面而非原始上下文,可能暗示意图是为安全而沙箱化。这里明确不是:陷阱和门面收窄的是挂载代码所见的 API——将其引导至 cordis 服务、远离易泄漏的 Node 内置模块和框架内部——目的是正确性和封堵未受保护的上下文逃逸,但门面暴露的能力(ctx.shellctx.fsctx.web)触及真实运行时,因此它不是安全边界。真正的安全边界(独立进程、权限提示)超出了一个开发/显式启用工具集的范围,且会与其核心目的——将活跃运行时交给模型——相冲突。

后果

该工具集是刻意的显式启用设计,具有完整权限的 ctx,因此部署方采用它的意识程度应与 bash 工具相当。以下几个事实由工具描述直接告知模型:一个 waterfall(瀑布式事件)监听器(如 tools/pre-execute)如果不调用 next() 就返回,会短路整条链,因此一个挂载的监听器可以阻止 agent 自身的工具分发(waterfall 语义);挂载代码在当前轮次的工具调用内运行,因此 await 任何只在该轮次结束后才 resolve 的东西会导致死锁;vmTimeoutMs 仅约束同步执行;挂载不会在会话恢复后存活。