The preset-authoring agent mount-validates its own composition
创作 preset 的 agent 自行挂载校验其组装
The `cordis` preset ships `editing-cordis-compositions`, the only guidance an agent has when it authors a preset. Four of its statements were false, and the two that carried the most weight pointed at the rule the skill itself calls "the rule that catches people". It named `tool-bash` as the worked example of a row whose name hides a service — "reads like a tool but provides `bashEnv`". `tool-bash` provides nothing;
English
Problem
The cordis preset ships editing-cordis-compositions, the only guidance an agent has when it authors a preset. Four of its statements were false, and the two that carried the most weight pointed at the rule the skill itself calls "the rule that catches people".
It named tool-bash as the worked example of a row whose name hides a service — "reads like a tool but provides bashEnv". tool-bash provides nothing; it declares inject: ['tools', 'bash', 'systemPrompt', 'bashEnv'], and bashEnv comes from the host composition's own shell-env row. An agent wrapping tool-bash in an isolate realm on that advice strands the row waiting for a service its realm hides, and the whole preset fails to mount.
Its isolate example composed jobs-local with tool-jobs. jobs-local is host-plane, and the shipped compositions say in their own comments that an entry-local realm around tool-jobs makes run_in_background answer "background jobs unavailable". The example contradicted the file next to it.
It described a string realm label as pooling one instance across subtrees. Labels join realms; provide() still throws on the second registration under the same realm symbol, which standard's header comment already stated.
It sent the agent to a package's README to learn whether a row publishes a service. Every harness package declares files, and no declaration includes its README, so an installed deployment carries none. There that instruction cannot be followed at all.
Underneath all four sat a capability claim: the agent "cannot start one [a session] yourself", so verification was hand-reading YAML fields and handing the result to the user through the settings page's red marking. That marking is discovery's shape check, which is far weaker than the sentence implied.
Decision
The skill teaches the agent to mount-validate its own composition through ctx.agentPresets, and every remaining example is taken from a shipped composition in the same repository.
standingKeyFor(id) is the check. It runs ensureStanding() — the same real mount a session start performs, minus the agent — so it rejects a row whose package does not resolve, a row whose config is invalid, a service published into the root realm, and a row that never activated. A failed mount deletes the standing entry and disposes its scope, leaving nothing behind; a successful one installs the standing generation the first real session would have installed anyway. The skill therefore places it as the final check on a finished edit rather than a per-line loop.
The skill states plainly that list()'s broken field is not validation. Discovery's health check proves the file parses in the loader's dialect and holds named rows, and every one of the four failures above passes it.
The agent reaches the roster service the way cordis_mount documents: a temporary plugin declaring inject: ['agentPresets', 'tools'] that registers a tool for itself, because a mount returns only its own acknowledgement and a registered tool is how a service answer reaches the model on the next step. The skill ships that plugin verbatim. agentPresets is in the generated cordis_inspect what:"api" catalog with full JSDoc, and the sandbox façade gates services on fiber.inject alone rather than an allowlist, so nothing about this path is special-cased for the skill.
copy(from, id, name) is named as the authoring write, in place of a shell copy: it validates the id, refuses one any root supplies, rolls a failed copy back, rewrites the copy's preset.yml, and runs host-side without sandbox escalation. The escalation guidance stays, moved to where it applies — editing agent.cordis.yml afterwards still writes outside the session workspace.
"Whether a row publishes a service" resolves through cordis_inspect what:"services", which names the owning fiber of every live service.
The guidance keeps ${DSH_HOME:-$HOME/.dsh}/.agent-presets/ as the answer to "where do my presets live" while routing the path an agent actually reads or edits through list() or resolve(). Stating the path is right for talking to a person and wrong for feeding a file tool: a deployment may configure other roots, and list() cannot reveal a user root that holds nothing yet.
That path is now a property of the package rather than of one launcher. AgentPresets derives <dshHome>/.agent-presets as a user root unless includeUserRoot is false, the way dsh-skill-filesystem derives <dshHome>/skills, and apps/cli supplies only the SHIPPED root — the one path an installed app alone can resolve. The asymmetry it replaces cost a bug: with both roots patched in by one launcher, dsh run booted a roster with no roots at all and failed resolving standard (fixed then by teaching every launcher the patch). The derived root is appended after every configured root, so a shipped id still shadows a home directory claiming it, and writableRoot() still prefers an explicitly configured user root. It is resolved once at construction: a root set that changed between a list() and the copy() acting on its answer would author into a directory the caller never saw.
The prohibition on touching the shipped install is promoted from a paragraph inside the authoring steps to a top ## Off-limits section, extended to cover editing the host composition as a workaround. The new self-validation calls do not weaken it: copy() refuses an id any root supplies, and remove() refuses a preset that ships with the deployment.
Measured behavior
Each row was produced by booting the shipped Web composition and calling the tools through ctx.tools.execute on an agent composed from cordis — no model in the loop.
| Composition under test | list() broken | standingKeyFor() |
|---|---|---|
| row names an absent package | empty | Cannot find package '@deepseek-ai/dsh-does-not-exist' |
| service row with no realm, name the host supplies | empty | service "tasks" has been registered at <LocalJobRegistry> |
| service row with no realm, name the host does not supply | empty | row(s) published process-global service(s) [workflows]; … |
same row inside isolate | empty | mounts |
| consumer row with no provider | empty | 1 row(s) did not activate: … waiting for workflows |
| row missing a required config field | empty | invalid config: $.allowParallelInProgress missing required value |
The skill's own cordis_mount snippet was executed verbatim through the tool registry: it mounts, its preset_check tool appears in the composing agent's catalog on the next read, and it answers mounted OK for a valid preset and the mount rejection for an invalid one.
Alternatives considered
Leaving verification with the user and only fixing the four errors. The errors and the capability claim share a cause — the guidance was written from the preset layer's public surface rather than from what the composed agent can reach — and an agent that cannot check its work hands over compositions whose defects the settings page cannot see either.
Teaching list()'s broken field as the check. It is the one the settings page shows, so it reads like the intended answer. It passes every failure that matters, and presenting it as validation is what made the original guidance feel complete.
Adding a first-class preset-validation tool to the preset. The composed path already exists and is documented by cordis_mount's own schema; a dedicated tool would add a model-facing row to a preset whose point is that the runtime is reachable without one.
Consequences
- A successful validation leaves a standing generation that is never reclaimed, which is the standing-mount cost the roster already carries per generation — the agent pays it once at the end of an edit instead of the user paying it at the first session.
- The skill now depends on
cordis_inspect's generated API catalog staying current foragentPresets;verify-cordis-apiindoc-syncis what holds that. - Two examples are now quotations of
standard's composition. They drift if that file'sdelegationgroup changes, which theweb-agent-presetse2e does not catch. - The four corrected statements were the skill's only concrete illustrations of the realm rule. Replacing rather than deleting them keeps the rule teachable; the replacements are verifiable by reading one shipped file.
Related
Supersedes the creator-guidance bullet in broken presets are roster rows, whose health-check decision remains current — this note reverses only its "the agent cannot start sessions; the settings page's red marking is the user's check" conclusion. Authoring's copy-only shape is owned by copy-only preset authoring.
中文
问题
cordis preset 随包发布 editing-cordis-compositions,它是 agent 创作 preset 时唯一的指导来源。其中四条陈述与事实不符,而分量最重的两条恰好指向该 skill 自称「最容易让人栽跟头的规则」。
它把 tool-bash 当作「行名看不出发布服务」的示例——「看着像工具,其实 provides bashEnv」。tool-bash 不发布任何服务,它声明 inject: ['tools', 'bash', 'systemPrompt', 'bashEnv'],bashEnv 来自宿主组装自己的 shell-env 行。agent 照此给 tool-bash 套上 isolate realm,该行会永远等待被自己的 realm 挡住的服务,整个 preset 挂载失败。
它的 isolate 示例把 jobs-local 与 tool-jobs 组在一起。jobs-local 位于宿主平面,而已发布组装在自己的注释里写明:给 tool-jobs 套 entry-local realm 会让 run_in_background 回答「background jobs unavailable」。示例与紧挨着它的文件互相矛盾。
它把字符串 realm label 描述为跨子树共享一个实例。label 只是加入同一 realm,provide() 在同一 realm symbol 下第二次注册仍然抛错——standard 的头部注释早已如此说明。
它让 agent 去读包的 README 判断某行是否发布服务。每个 harness 包都声明了 files,且没有任何一份声明包含自己的 README,因此装机部署中一份也没有。在那里该指令根本无法执行。
四条之下还压着一个能力断言:agent「自己起不了会话」,于是校验退化成肉眼核对 YAML 字段,再把结果经设置页的红色标记交给用户。那个标记是发现阶段的结构检查,远弱于这句话给人的印象。
决策
skill 教 agent 通过 ctx.agentPresets 自行挂载校验其组装,其余每个示例都取自同一仓库中已发布的组装。
standingKeyFor(id) 是校验手段。它走 ensureStanding()——与会话启动完全相同的真实挂载,只是不创建 agent——因此能拒绝包无法解析的行、配置非法的行、把服务发布进根 realm 的行,以及始终未激活的行。挂载失败会删除常驻条目并 dispose 其 scope,不留残留;挂载成功则装上首次真实会话本来也会装上的那个常驻代际。因此 skill 把它安排为完成编辑后的最终检查,而不是逐行循环。
skill 明确写出:list() 的 broken 字段不是校验。发现阶段的健康检查只证明文件能被 Loader 的方言解析且行带 name,上述四类失败全部能通过它。
agent 按 cordis_mount 自身文档所述的方式够到 roster 服务:挂一个声明 inject: ['agentPresets', 'tools'] 的临时插件,并为自己注册一个工具——因为挂载只返回自身的确认信息,而已注册的工具才是服务结果在下一步抵达模型的途径。skill 逐字附上该插件。agentPresets 位于生成的 cordis_inspect what:"api" 目录中并带完整 JSDoc,沙箱 façade 仅凭 fiber.inject 而非白名单放行服务,因此这条路径没有为该 skill 做任何特例。
copy(from, id, name) 被指定为创作写入手段,取代 shell 复制:它校验 id、拒绝任何根已提供的 id、失败时回滚、重写副本的 preset.yml,并在宿主侧运行而无需沙箱升级。沙箱升级的说明保留,移到真正适用之处——其后编辑 agent.cordis.yml 仍然写在会话工作区之外。
「某行是否发布服务」改由 cordis_inspect what:"services" 回答,它会给出每个存活服务的持有 fiber。
指导保留 ${DSH_HOME:-$HOME/.dsh}/.agent-presets/ 作为「我的 preset 在哪」的答案,同时把 agent 实际读取或编辑的路径改走 list() 或 resolve()。写出该路径对人讲是对的,喂给文件工具是错的:部署可以配置其他根目录,而 list() 无法揭示一个尚且为空的用户根。
该路径如今是本包的属性,而非某个启动器的属性。除非 includeUserRoot 为 false,AgentPresets 自行推导 <dshHome>/.agent-presets 作为 user 根,正如 dsh-skill-filesystem 推导 <dshHome>/skills;apps/cli 只提供随附根——那是唯有已安装 app 才能解析的路径。它取代的那种不对称曾付出过代价:两个根都由单一启动器补入时,dsh run 启动的 roster 一个根都没有,解析 standard 直接失败(当时的修法是让每个启动器都执行该 patch)。推导出的根追加在全部已配置根之后,因此随附 id 仍会遮蔽占用它的家目录目录,而 writableRoot() 仍优先选择显式配置的 user 根。它在构造时解析一次:若根目录集合在一次 list() 与依据其答案执行的 copy() 之间发生变化,写入的将是调用方从未见过的目录。
禁止改动随发布安装的约束,从创作步骤中的一段提升为顶部的 ## Off-limits 一节,并扩展到禁止改宿主组装绕行。新增的自校验调用不削弱它:copy() 拒绝任何根已提供的 id,remove() 拒绝随部署发布的 preset。
Measured behavior
下表每一行都由启动已发布的 Web 组装、并在由 cordis 组装出的 agent 上经 ctx.tools.execute 调用工具得出——全程无模型参与。
| 被测组装 | list() 的 broken | standingKeyFor() |
|---|---|---|
| 行指向不存在的包 | 空 | Cannot find package '@deepseek-ai/dsh-does-not-exist' |
| 服务行未套 realm,名字宿主已提供 | 空 | service "tasks" has been registered at <LocalJobRegistry> |
| 服务行未套 realm,名字宿主未提供 | 空 | row(s) published process-global service(s) [workflows]; … |
同一行置于 isolate 内 | 空 | 挂载成功 |
| 消费者行无人提供服务 | 空 | 1 row(s) did not activate: … waiting for workflows |
| 行缺少必填配置字段 | 空 | invalid config: $.allowParallelInProgress missing required value |
skill 自带的 cordis_mount 代码片段经工具注册表逐字执行:它成功挂载,其 preset_check 工具在下一次读取时出现在组装该 agent 的目录中,对有效 preset 回答 mounted OK,对无效 preset 回答挂载拒绝原因。
考虑过的替代方案
把校验留给用户,只修四处错误。 这些错误与那句能力断言同源——指导是按 preset 层的公开面写的,而不是按被组装出的 agent 实际够得到的东西写的——而无法自查的 agent 交出的组装,其缺陷设置页同样看不见。
把 list() 的 broken 字段教成校验手段。 它正是设置页展示的字段,看起来像是预期答案。它对所有要紧的失败一律放行,而把它当成校验,正是原指导显得完整的原因。
给 preset 加一个一等的 preset 校验工具。 组合出的路径已经存在,且由 cordis_mount 自己的 schema 记载;专用工具会给一个「无需专用工具即可够到运行时」的 preset 再添一个面向模型的行。
后果
- 校验成功会留下一个永不回收的常驻代际,这是 roster 按代际本就承担的常驻挂载代价——由 agent 在编辑收尾时付一次,而不是由用户在首次会话时付。
- skill 现在依赖
cordis_inspect生成的 API 目录对agentPresets保持最新;doc-sync中的verify-cordis-api是守住这一点的门禁。 - 有两个示例现在是对
standard组装的引用。若该文件的delegation组发生变化它们会漂移,而web-agent-presetse2e 捕捉不到。 - 被修正的四条陈述原本是该 skill 对 realm 规则仅有的具体图示。选择替换而非删除,规则才仍然可教;替换后的示例读一个已发布文件即可核验。
Related
取代破损 preset 是 roster 行中关于创作模式指导的那一条,其健康检查决策依然有效——本篇只推翻它「agent 起不了会话;设置页的红色标记是用户的检查手段」这一结论。创作的 copy-only 形态由copy-only preset 创作负责。