From 0b3ac6356bcecd3455e8cbf7aff6acddd6ffa92d Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Tue, 11 Aug 2026 17:43:09 +0800 Subject: [PATCH 1/4] fix(preset): correct the composition-authoring skill and give it a real check The `cordis` preset's `editing-cordis-compositions` skill is the only guidance an agent has when it authors a preset, and four of its statements were false. `tool-bash` was named as the worked example of a row that hides a service; it provides nothing and injects `bashEnv` from the host's own `bash-env` row, so following that advice strands the row behind its realm and the preset fails to mount. The `isolate` example composed `tasks-local` with `tool-tasks`, which the shipped compositions' own comments say breaks `run_in_background`. A string realm label was described as pooling one instance; labels join realms and `provide()` still throws on the second registration. Rows were to be checked against a package README, which no harness package publishes. Verification is now the agent's own: `standingKeyFor(id)` runs the same mount a session start performs and rejects an unresolvable package, an invalid config, a service in the root realm, and a row that never activated. The skill states that `list()`'s `broken` field is a shape check that every one of those passes, ships the `cordis_mount` plugin that reaches the roster service, and names `copy()` as the authoring write. The prohibition on touching the shipped install is promoted to its own section and extended to the host composition. Fixes #2266 --- ...-08-09-broken-preset-roster-rows.i18n.yaml | 4 +- .../2026-08-09-broken-preset-roster-rows.md | 2 +- ...2026-08-09-broken-preset-roster-rows.zh.md | 2 +- ...nt-validates-its-own-composition.i18n.yaml | 6 + ...ing-agent-validates-its-own-composition.md | 68 +++++++++ ...-agent-validates-its-own-composition.zh.md | 68 +++++++++ .../editing-cordis-compositions/SKILL.md | 144 ++++++++++++------ 7 files changed, 246 insertions(+), 48 deletions(-) create mode 100644 .agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.i18n.yaml create mode 100644 .agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.md create mode 100644 .agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.zh.md diff --git a/.agents/notes/implemented/bug-fix/2026-08-09-broken-preset-roster-rows.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-08-09-broken-preset-roster-rows.i18n.yaml index 9ad1682b62..2a62045ef1 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-09-broken-preset-roster-rows.i18n.yaml +++ b/.agents/notes/implemented/bug-fix/2026-08-09-broken-preset-roster-rows.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-09-broken-preset-roster-rows.md -2026-08-09-broken-preset-roster-rows.md: fef6a183b10f98b8ae9d2b42701380c69bc83462 -2026-08-09-broken-preset-roster-rows.zh.md: 196bcf4ef16325a1d7692d2ea13d9fa683d500f4 +2026-08-09-broken-preset-roster-rows.md: 069585957d4d99598cc38e4a7c6bc8c8d82490ca +2026-08-09-broken-preset-roster-rows.zh.md: d541292b59496464eb91bc03278e0800af16c4a3 diff --git a/.agents/notes/implemented/bug-fix/2026-08-09-broken-preset-roster-rows.md b/.agents/notes/implemented/bug-fix/2026-08-09-broken-preset-roster-rows.md index fef6a183b1..069585957d 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-09-broken-preset-roster-rows.md +++ b/.agents/notes/implemented/bug-fix/2026-08-09-broken-preset-roster-rows.md @@ -26,7 +26,7 @@ Surfaces split by their job: the management section renders broken rows as marke - **`PRESET_ID` moved to `types.ts`** so discovery and authoring share one containment vocabulary; authoring re-exports it unchanged. - **The reason is one line.** js-yaml appends a multi-line code-frame snippet; the roster card is not a terminal, so `compositionProblem` keeps the first line. - **Two mount.spec races were left untouched deliberately**: `ensureStanding` is still reachable with a preset resolved just before deletion (the private-path tests), and its stamp/unstampable semantics are unchanged — the health check happens before, in the public route. -- **Creator-mode guidance rides the same PR**: the `cordis` preset's persona now forbids editing the shipped install (corrupting `cordis` would disable the mode itself) and points authoring at `${DSH_HOME:-$HOME/.dsh}/.agent-presets//`; its skill teaches `preset.yml` metadata, the copy-first workflow, the one-escalation sandbox reality (the preset root lies outside the session workspace), and honest verification (the agent cannot start sessions; the settings page's red marking is the user's check). Verified live: asked to edit the shipped `cordis` composition directly, the composed agent refuses citing both rules and offers the copy path; asked for a real preset, it lands it under `$DSH_HOME`, batches writes into one escalation, self-checks with the loader dialect, and hands verification to the user. +- **Creator-mode guidance rides the same PR**: the `cordis` preset's persona forbids editing the shipped install (corrupting `cordis` would disable the mode itself) and points authoring at `${DSH_HOME:-$HOME/.dsh}/.agent-presets//`; its skill teaches `preset.yml` metadata, the copy-first workflow, and the one-escalation sandbox reality (the preset root lies outside the session workspace). Verified live: asked to edit the shipped `cordis` composition directly, the composed agent refuses citing both rules and offers the copy path; asked for a real preset, it lands it under `$DSH_HOME` and batches writes into one escalation. The verification half of that guidance — that the agent cannot start sessions, so the settings page's red marking is the user's check — is superseded by [the authoring agent mount-validates its own composition](2026-08-11-preset-authoring-agent-validates-its-own-composition.md): the shape check below is not validation, and `standingKeyFor` gives the agent the real one. The health decision in this note is unchanged. ## Alternatives considered diff --git a/.agents/notes/implemented/bug-fix/2026-08-09-broken-preset-roster-rows.zh.md b/.agents/notes/implemented/bug-fix/2026-08-09-broken-preset-roster-rows.zh.md index 196bcf4ef1..d541292b59 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-09-broken-preset-roster-rows.zh.md +++ b/.agents/notes/implemented/bug-fix/2026-08-09-broken-preset-roster-rows.zh.md @@ -26,7 +26,7 @@ Status: implemented - **`PRESET_ID` 移到 `types.ts`**,让发现与创作共享同一份包含边界词汇;authoring 原样转发导出。 - **原因只留一行。** js-yaml 会附上多行代码框摘录;名单卡片不是终端,`compositionProblem` 只保留首行。 - **mount.spec 的两个竞态用例特意不动**:`ensureStanding` 仍可能拿到删除前一刻解析出的 preset(私有路径测试),其 stamp/unstampable 语义不变——健康检查发生在此之前的公开路径上。 -- **创造模式的引导随同一 PR 落地**:`cordis` preset 的 persona 现在禁止编辑随附安装(损坏 `cordis` 会禁用这一模式本身),并把创作指向 `${DSH_HOME:-$HOME/.dsh}/.agent-presets//`;其技能新教了 `preset.yml` 元信息、先复制再改的流程、一次升级的沙箱现实(preset 根目录在会话工作区之外)与诚实的验证方式(agent 无法自己启动会话;设置页的红色标记是用户的检查项)。已实测:被要求直接改随附 `cordis` 组装时,组装出的 agent 援引两条规则拒绝并给出复制路径;被要求真正创建 preset 时,它落在 `$DSH_HOME` 下、把写入合并为一次升级、用加载器方言自查、并把验证交还用户。 +- **创造模式的引导随同一 PR 落地**:`cordis` preset 的 persona 禁止编辑随附安装(损坏 `cordis` 会禁用这一模式本身),并把创作指向 `${DSH_HOME:-$HOME/.dsh}/.agent-presets//`;其技能教了 `preset.yml` 元信息、先复制再改的流程与一次升级的沙箱现实(preset 根目录在会话工作区之外)。已实测:被要求直接改随附 `cordis` 组装时,组装出的 agent 援引两条规则拒绝并给出复制路径;被要求真正创建 preset 时,它落在 `$DSH_HOME` 下并把写入合并为一次升级。该引导中关于验证的那一半——agent 无法自己启动会话,因而设置页的红色标记是用户的检查项——已由[创作 preset 的 agent 自行挂载校验其组装](2026-08-11-preset-authoring-agent-validates-its-own-composition.md)取代:下文的结构检查不是校验,而 `standingKeyFor` 才给了 agent 真正的校验手段。本篇的健康检查决策不变。 ## 曾考虑的替代方案 diff --git a/.agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.i18n.yaml new file mode 100644 index 0000000000..d932d7f6f4 --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.md +2026-08-11-preset-authoring-agent-validates-its-own-composition.md: 77bd30d4c8f6599ccde50b4d814f55d065266763 +2026-08-11-preset-authoring-agent-validates-its-own-composition.zh.md: cdc0d30bcd769e1a17ddcce364002f0136a26242 diff --git a/.agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.md b/.agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.md new file mode 100644 index 0000000000..77bd30d4c8 --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.md @@ -0,0 +1,68 @@ +# Agent Note: The preset-authoring agent mount-validates its own composition + +Status: implemented + +English | [中文](2026-08-11-preset-authoring-agent-validates-its-own-composition.zh.md) + +## 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 `bash-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 `tasks-local` with `tool-tasks`. `tasks-local` is host-plane, and the shipped compositions say in their own comments that an entry-local realm around `tool-tasks` makes `run_in_background` answer "background tasks 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. Outside `apps/cli` (`files: ["lib/*.js", "config"]`), every harness package publishes only `lib/index.js`, `lib/invariant.js`, and `lib/types/**/*.d.ts` — no README, no `src/`, no `docs/`. In an installed deployment 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 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 | empty | `service "tasks" has been registered at ` | +| 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](../architecture/2026-08-08-per-preset-standing-mounts.md) 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 for `agentPresets`; `verify-cordis-api` in `doc-sync` is what holds that. +- Two examples are now quotations of `standard`'s composition. They drift if that file's `delegation` group changes, which the `web-agent-presets` e2e 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](2026-08-09-broken-preset-roster-rows.md), 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](../simplification/2026-08-08-copy-only-preset-authoring.md). diff --git a/.agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.zh.md b/.agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.zh.md new file mode 100644 index 0000000000..cdc0d30bcd --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.zh.md @@ -0,0 +1,68 @@ +# Agent Note: 创作 preset 的 agent 自行挂载校验其组装 + +Status: implemented + +[English](2026-08-11-preset-authoring-agent-validates-its-own-composition.md) | 中文 + +## Problem + +`cordis` preset 随包发布 `editing-cordis-compositions`,它是 agent 创作 preset 时唯一的指导来源。其中四条陈述与事实不符,而分量最重的两条恰好指向该 skill 自称「最容易让人栽跟头的规则」。 + +它把 `tool-bash` 当作「行名看不出发布服务」的示例——「看着像工具,其实 provides `bashEnv`」。`tool-bash` 不发布任何服务,它声明 `inject: ['tools', 'bash', 'systemPrompt', 'bashEnv']`,`bashEnv` 来自宿主组装自己的 `bash-env` 行。agent 照此给 `tool-bash` 套上 `isolate` realm,该行会永远等待被自己的 realm 挡住的服务,整个 preset 挂载失败。 + +它的 `isolate` 示例把 `tasks-local` 与 `tool-tasks` 组在一起。`tasks-local` 位于宿主平面,而已发布组装在自己的注释里写明:给 `tool-tasks` 套 entry-local realm 会让 `run_in_background` 回答「background tasks unavailable」。示例与紧挨着它的文件互相矛盾。 + +它把字符串 realm label 描述为跨子树共享一个实例。label 只是加入同一 realm,`provide()` 在同一 realm symbol 下第二次注册仍然抛错——`standard` 的头部注释早已如此说明。 + +它让 agent 去读包的 README 判断某行是否发布服务。除 `apps/cli`(`files: ["lib/*.js", "config"]`)外,所有 harness 包只发布 `lib/index.js`、`lib/invariant.js` 与 `lib/types/**/*.d.ts`,没有 README、没有 `src/`、没有 `docs/`。在装机部署中该指令根本无法执行。 + +四条之下还压着一个能力断言:agent「自己起不了会话」,于是校验退化成肉眼核对 YAML 字段,再把结果经设置页的红色标记交给用户。那个标记是发现阶段的结构检查,远弱于这句话给人的印象。 + +## Decision + +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。 + +禁止改动随发布安装的约束,从创作步骤中的一段提升为顶部的 `## 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 ` | +| 同一行置于 `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 回答挂载拒绝原因。 + +## Alternatives considered + +**把校验留给用户,只修四处错误。** 这些错误与那句能力断言同源——指导是按 preset 层的公开面写的,而不是按被组装出的 agent 实际够得到的东西写的——而无法自查的 agent 交出的组装,其缺陷设置页同样看不见。 + +**把 `list()` 的 `broken` 字段教成校验手段。** 它正是设置页展示的字段,看起来像是预期答案。它对所有要紧的失败一律放行,而把它当成校验,正是原指导显得完整的原因。 + +**给 preset 加一个一等的 preset 校验工具。** 组合出的路径已经存在,且由 `cordis_mount` 自己的 schema 记载;专用工具会给一个「无需专用工具即可够到运行时」的 preset 再添一个面向模型的行。 + +## Consequences + +- 校验成功会留下一个永不回收的常驻代际,这是 roster 按代际本就承担的[常驻挂载](../architecture/2026-08-08-per-preset-standing-mounts.md)代价——由 agent 在编辑收尾时付一次,而不是由用户在首次会话时付。 +- skill 现在依赖 `cordis_inspect` 生成的 API 目录对 `agentPresets` 保持最新;`doc-sync` 中的 `verify-cordis-api` 是守住这一点的门禁。 +- 有两个示例现在是对 `standard` 组装的引用。若该文件的 `delegation` 组发生变化它们会漂移,而 `web-agent-presets` e2e 捕捉不到。 +- 被修正的四条陈述原本是该 skill 对 realm 规则仅有的具体图示。选择替换而非删除,规则才仍然可教;替换后的示例读一个已发布文件即可核验。 + +## Related + +取代[破损 preset 是 roster 行](2026-08-09-broken-preset-roster-rows.md)中关于创作模式指导的那一条,其健康检查决策依然有效——本篇只推翻它「agent 起不了会话;设置页的红色标记是用户的检查手段」这一结论。创作的 copy-only 形态由[copy-only preset 创作](../simplification/2026-08-08-copy-only-preset-authoring.md)负责。 diff --git a/apps/cli/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md b/apps/cli/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md index ce751f039d..7a37a61e0c 100644 --- a/apps/cli/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md +++ b/apps/cli/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md @@ -1,12 +1,18 @@ --- name: editing-cordis-compositions -description: Use when creating or changing a Cordis composition for this harness — writing or editing an agent preset, adding or removing a plugin row, deciding whether something belongs to the host composition or to one session, or diagnosing a row that mounted but contributed nothing. +description: Use when creating, changing, or validating a Cordis composition for this harness — writing or editing an agent preset, adding or removing a plugin row, deciding whether something belongs to the host composition or to one session, checking whether a preset you authored actually mounts, or diagnosing a row that mounted but contributed nothing. --- # Editing Cordis compositions Every capability in this harness is a plugin row in a `cordis.yml`. There is no separate configuration language: changing what an agent can do means changing which rows are composed for it. +## Off-limits + +**Never edit, delete, or overwrite a preset that ships with the deployment** — the `agent-presets` directory beside the deployment's own config, which supplies `standard`, `code`, `minimal`, and `cordis`. Never escalate the sandbox to reach it, even when a change there looks quicker. An upgrade overwrites that install, and corrupting `cordis` disables preset authoring itself. Reading a shipped composition is the intended way to start; writing to one is not, and neither is editing the host composition to work around a preset limitation. + +To change what a shipped preset does, copy it and edit the copy. Locally authored presets under the user root are yours to create, edit, and delete. + ## Decide the plane first Two planes, and the choice is not about how "agent-related" something feels — it is about whether the thing must be shared. @@ -17,16 +23,103 @@ Two planes, and the choice is not about how "agent-related" something feels — **A service with a consumer outside the agent plane cannot move into a preset.** `subagents` is the worked example: the registry answers cross-session queries for the host api-proxy, so a per-session copy both starves that host row — it waits forever for a service nothing provides — and collides on the second session, since a provider name registers once. The preset contributes the delegation *tools*; the registry and its backends stay host-side. -A preset is a directory holding one `agent.cordis.yml`, optionally beside a `preset.yml` carrying display metadata — `name` and `description` (and, for shipped presets, a roster `order`). Write the metadata too: a preset without it shows up in every picker as its bare directory name. The shipped presets live beside the deployment's composition; locally authored ones live under `${DSH_HOME:-$HOME/.dsh}/.agent-presets//`. +A preset is a directory holding one `agent.cordis.yml`, optionally beside a `preset.yml` carrying display metadata — `name` and `description` (and, for shipped presets, a roster `order`). Write the metadata too: a preset without it shows up in every picker as its bare directory name. Locally authored presets live under `${DSH_HOME:-$HOME/.dsh}/.agent-presets//`. + +## The roster service + +`ctx.agentPresets` owns discovery, authoring, and mounting. You reach it by mounting a temporary plugin that injects it and registers a tool for yourself — `cordis_mount` returns only the mount acknowledgement, so a registered tool is how a service answer gets back to you, and it becomes callable on your next step. + +Read `cordis_inspect what:"api" name:"agentPresets"` for the current signatures before writing the code. The four calls this skill relies on: + +- `list()` — every preset with its `id`, `trust`, and absolute `path`. This is how you locate the shipped compositions without knowing the install layout. +- `read(id)` — one preset's composition text. +- `copy(from, id, name?)` — the only authoring write (see below). +- `standingKeyFor(id)` — mount-validate one preset (see below). + +```js +return { + name: 'preset-tools', + inject: ['agentPresets', 'tools'], + apply(ctx) { + harness.registerTool(ctx, harness.defineTool({ + name: 'preset_check', + description: 'Mount-validate one preset by id.', + parameters: { id: { type: 'string', required: true } }, + output: { schema: { type: 'string' }, render(_a, v) { return [{ type: 'text', text: v }] } }, + async execute(args) { + try { + await ctx.agentPresets.standingKeyFor(args.id) + return 'mounted OK' + } catch (error) { + return error.message + } + }, + })) + }, +} +``` + +Unmount the plugin with `cordis_unmount` when you are done; it is a probe, not a capability to leave behind. ## Authoring a preset -1. **Start from a copy.** Read a shipped composition close to what you want (the `standard` preset is the full coding agent) and copy its whole directory into `${DSH_HOME:-$HOME/.dsh}/.agent-presets//` — the id must be lowercase letters, digits, and hyphens, because it becomes the directory name. A composition written from scratch usually forgets a group realm or a consumer row; a copy starts loadable. -2. **Expect the file sandbox.** The preset root lies outside the session workspace, so under the default `workspace-write` policy the first write is denied. Retry that exact command once with `sandbox_permissions` escalation and a short justification — the user sees and approves it. Batch your writes (one heredoc per file) rather than escalating many small commands. -3. **Rewrite `preset.yml`**: give the copy its own `name` and `description`, and drop any `order` the source declared — that field sorts the shipped roster. -4. **Edit `agent.cordis.yml`** row by row, keeping the plane rule and realm rule above. +1. **Start from a copy.** `copy(from, id, name)` copies a whole preset directory into the user root — composition, metadata, skill directories, assets. It validates the id (lowercase letters, digits, and hyphens, because it becomes the directory name), refuses an id any root already supplies, rolls a failed copy back, and rewrites the copy's `preset.yml` to keep the source's description while dropping its name and roster `order`. Prefer it over a shell copy: it needs no sandbox escalation, and the copy is exactly as loadable as its source. `standard` is the full coding agent and the usual source. +2. **Expect the file sandbox on every edit after the copy.** The user preset root lies outside the session workspace, so under the default `workspace-write` policy the first write there is denied. Retry that exact command once with `sandbox_permissions` escalation and a short justification — the user sees and approves it. Batch your writes (one heredoc per file) rather than escalating many small commands. `copy()` itself runs host-side and needs none of this; the edits do. +3. **Give the copy its own `name` and `description`** in `preset.yml`. +4. **Edit `agent.cordis.yml`** row by row, keeping the plane rule and the realm rule. +5. **Mount-validate the result**, then hand off to the user for a real session — both under *Verifying a change*. -### Native product subagents +A composition written from scratch usually forgets a group realm or a consumer row; a copy starts loadable. + +## The rule that catches people + +**A row that publishes a service may not sit loose in a preset.** Registering a service without an isolate realm puts it in the process-global realm, so the second session mounting that preset collides with the first. The mount rejects it rather than letting the collision surface later. + +Whether a row publishes a service is not visible from its name, and package READMEs are absent from an installed deployment. Read it off the live runtime instead: `cordis_inspect what:"services"` lists every service with the fiber that owns it, so a service attributed to a fiber other than the row you are adding is one that row consumes rather than provides. For a row not in your current composition, mount-validate and read the rejection — it names the offending service. + +When a preset genuinely owns a service, wrap the provider **and every consumer that reaches it** in one group carrying an `isolate` realm. The shipped `standard` composition does this for `workflows`, which nothing outside an agent reads — its `delegation` group, with the delegation tools omitted here: + +```yaml +- id: delegation + name: cordis:group + group: true + isolate: + workflows: true + config: + - id: workflow-workerthread + name: '@deepseek-ai/dsh-workflow-workerthread' + config: + provider: spawn + - id: tool-workflow + name: '@deepseek-ai/dsh-tool-workflow' +``` + +`true` means a realm private to each mounting session. A string label instead joins subtrees into one shared realm; `provide()` still throws on the second registration under that symbol, so a label does not pool instances and is not what a preset needs. + +A consumer left outside the group resolves the host's registry, which the preset did not populate, and then contributes nothing. Mount-validation catches that as a row that never activated. + +Realms are for services a preset owns, not for every group. A host capability the preset only consumes must stay outside a realm, or the row cannot resolve it: `tool-bash`, `tool-tasks`, and `tool-goal` publish nothing and sit loose in `standard`, which explains in comments which host instance each one resolves and why a realm would break it. Wrapping a consumer row in a realm of its own is the same error as leaving one outside its provider's realm. + +## Verifying a change + +**`standingKeyFor(id)` is the check.** It composes the preset's plugin subtree for real — the same mount a session start performs, minus the agent — and rejects the four ways a composition fails: + +- a row whose package does not resolve (`Cannot find package …`); +- a row whose config is invalid (`invalid config: $. missing required value`); +- a service published into the root realm (`service "" has been registered at `); +- a row that never activated (`N row(s) did not activate: : waiting for `). + +It returns normally when the composition mounts. Run it as the final check on a finished edit rather than after every line: a successful mount installs a standing generation that lives until the process exits, while a failed one disposes its subtree and leaves nothing behind. + +**Do not treat the roster's `broken` field as validation.** `list()` reports `broken` from a shape check — the file parses in the loader's YAML dialect and holds named rows — which every failure above passes. It catches a damaged file, not an unusable composition. + +`cordis_inspect` reports THIS session's composition, so it confirms what a row does in the runtime you are already in, never what your new preset will do. + +After a clean mount-validation, ask the user to start a session on the new preset and confirm the tool list; the preset decides tool schemas and prompt sections, and only a real session shows the agent that composition produces. + +`cordis_mount` evaluates JavaScript against the live runtime and disappears on restart. It is for probing, not for shipping a capability: a capability belongs in a composition file. + +## Native product subagents Codex and Claude Code providers already live in the host composition. A preset chooses either product by contributing the same ordinary delegation-tool row used for spawn and fork; never move a product provider into the preset and never add a product-specific settings field. @@ -54,43 +147,6 @@ Copy these disabled templates from a shipped full preset and remove `disabled` o The two rows are independent. Leaving both disabled preserves the copied preset, enabling one exposes only that product tool, and enabling both exposes both. The host must provide `codex` or `claude` on `PATH`; the preset does not install, authenticate, select a model for, or probe either product. -The shipped preset directories are off-limits: never edit or delete them, and never escalate the sandbox to reach them, even when a change there looks quicker — an upgrade overwrites the install, and corrupting the `cordis` preset disables preset authoring itself. Locally authored presets under the user root are yours to create, edit, and delete. - -## The rule that catches people - -**A row that publishes a service may not sit loose in a preset.** Registering a service without an isolate realm puts it in the process-global realm, so the second session mounting that preset collides with the first. The mount rejects it rather than letting the collision surface later. - -Whether a row publishes a service is not visible from its name. `tool-bash` reads like a tool but provides `bashEnv`. Check the package's README, or mount the preset and read the rejection — it names the offending service. - -When a preset genuinely owns a service, wrap the provider **and every consumer that reaches it** in one group carrying an `isolate` realm: - -```yaml -- id: tasks - name: cordis:group - group: true - isolate: - tasks: true - config: - - id: tasks-local - name: '@deepseek-ai/dsh-tasks-local' - - id: tool-tasks - name: '@deepseek-ai/dsh-tool-tasks' -``` - -`true` means a realm private to each mounting session. A string label instead pools one instance across every subtree naming that label — use it only for something genuinely expensive to duplicate. - -A consumer left outside the group resolves the host's registry, which the preset did not populate, and then contributes nothing. That is the quietest failure here: the mount succeeds and a tool is simply missing. - -Host capabilities exposed through registries need no realm: the host `tools` and `skills` registries are layered per scope, so rows like `skill-local` and `tool-skill` sit loose in the preset and their registrations file into this preset's layer automatically — the agent's catalog merges them with whatever the deployment registered globally. - -## Verifying a change - -Read the live runtime with `cordis_inspect` — it reports the services, the plugin fibers, and the registered tools as they actually are, which is the only reliable check that a row did what its name suggests. Note it shows THIS session's composition: a preset you just wrote is not mounted anywhere until a session starts on it. - -To check a preset you authored, re-read the files and validate these fields: the top level is a YAML list, every row is a map with a `name`, every group carries its own list, and service-publishing rows sit behind an `isolate` realm. The settings page's preset roster validates the same fields and marks an unloadable preset broken in red — point the user there, and ask them to start a session on the new preset to confirm the tool list; you cannot start one yourself. - -`cordis_mount` evaluates JavaScript against the live runtime and disappears on restart. It is for probing, not for shipping a capability: a capability belongs in a composition file. - ## What not to move into a preset `agent-loop` registers the one agent factory and throws on a second. The registries own the per-session layering and cannot themselves be per-session. Session persistence must stay host-side or the session list fragments. The sandbox, approval, and permission rows are a deliberate boundary: a preset is exactly as privileged as the plugins it names, so letting one relax its own confinement would defeat the confinement. From 63b564bb4b82d6336fe8d4053a935e6b431e5ae6 Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Tue, 11 Aug 2026 19:49:47 +0800 Subject: [PATCH 2/4] fix(preset): cover both root-realm rejections and narrow the packaging claim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review found two errors in the first pass. The skill quoted `service "" has been registered at ` as the rejection for a service published into the root realm. That message only appears when the loose name collides with one the host already supplies. A preset publishing its OWN new service name reaches the root realm successfully and is rejected by the mount audit instead, with `row(s) published process-global service(s) [...]` — which is the shape a forgotten realm actually takes. Both messages are now listed, verified by mounting one preset of each kind. The Agent Note claimed every harness package publishes exactly three file patterns. 139 do; 28 also publish `lib/client.js`, 22 publish `lib/types/**/*.js`, and `dsh-message-feedback` publishes `src`. The note now states only the fact it needs: no package's `files` includes its README. Also from review: the copy step no longer repeats the display name it already set, the id rule spells out `[a-z0-9][a-z0-9-]*`, and the sandbox step says reads need no escalation. --- ...oring-agent-validates-its-own-composition.i18n.yaml | 4 ++-- ...et-authoring-agent-validates-its-own-composition.md | 5 +++-- ...authoring-agent-validates-its-own-composition.zh.md | 5 +++-- .../cordis/skills/editing-cordis-compositions/SKILL.md | 10 +++++----- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/.agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.i18n.yaml index d932d7f6f4..9c173b1cd7 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.i18n.yaml +++ b/.agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.md -2026-08-11-preset-authoring-agent-validates-its-own-composition.md: 77bd30d4c8f6599ccde50b4d814f55d065266763 -2026-08-11-preset-authoring-agent-validates-its-own-composition.zh.md: cdc0d30bcd769e1a17ddcce364002f0136a26242 +2026-08-11-preset-authoring-agent-validates-its-own-composition.md: b7d20ef20c611a0ea0c694bac6bd3948bd194b2b +2026-08-11-preset-authoring-agent-validates-its-own-composition.zh.md: 512ac5ea0f47f4a796170b070c140f14b0072879 diff --git a/.agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.md b/.agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.md index 77bd30d4c8..b7d20ef20c 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.md +++ b/.agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.md @@ -14,7 +14,7 @@ Its `isolate` example composed `tasks-local` with `tool-tasks`. `tasks-local` is 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. Outside `apps/cli` (`files: ["lib/*.js", "config"]`), every harness package publishes only `lib/index.js`, `lib/invariant.js`, and `lib/types/**/*.d.ts` — no README, no `src/`, no `docs/`. In an installed deployment that instruction cannot be followed at all. +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. @@ -41,7 +41,8 @@ Each row was produced by booting the shipped Web composition and calling the too | 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 | empty | `service "tasks" has been registered at ` | +| service row with no realm, name the host supplies | empty | `service "tasks" has been registered at ` | +| 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` | diff --git a/.agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.zh.md b/.agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.zh.md index cdc0d30bcd..512ac5ea0f 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.zh.md +++ b/.agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.zh.md @@ -14,7 +14,7 @@ Status: implemented 它把字符串 realm label 描述为跨子树共享一个实例。label 只是加入同一 realm,`provide()` 在同一 realm symbol 下第二次注册仍然抛错——`standard` 的头部注释早已如此说明。 -它让 agent 去读包的 README 判断某行是否发布服务。除 `apps/cli`(`files: ["lib/*.js", "config"]`)外,所有 harness 包只发布 `lib/index.js`、`lib/invariant.js` 与 `lib/types/**/*.d.ts`,没有 README、没有 `src/`、没有 `docs/`。在装机部署中该指令根本无法执行。 +它让 agent 去读包的 README 判断某行是否发布服务。每个 harness 包都声明了 `files`,且没有任何一份声明包含自己的 README,因此装机部署中一份也没有。在那里该指令根本无法执行。 四条之下还压着一个能力断言:agent「自己起不了会话」,于是校验退化成肉眼核对 YAML 字段,再把结果经设置页的红色标记交给用户。那个标记是发现阶段的结构检查,远弱于这句话给人的印象。 @@ -41,7 +41,8 @@ agent 按 `cordis_mount` 自身文档所述的方式够到 roster 服务:挂 | 被测组装 | `list()` 的 `broken` | `standingKeyFor()` | |---|---|---| | 行指向不存在的包 | 空 | `Cannot find package '@deepseek-ai/dsh-does-not-exist'` | -| 服务行未套 realm | 空 | `service "tasks" has been registered at ` | +| 服务行未套 realm,名字宿主已提供 | 空 | `service "tasks" has been registered at ` | +| 服务行未套 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` | diff --git a/apps/cli/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md b/apps/cli/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md index 7a37a61e0c..32a59dbb1b 100644 --- a/apps/cli/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md +++ b/apps/cli/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md @@ -63,9 +63,9 @@ Unmount the plugin with `cordis_unmount` when you are done; it is a probe, not a ## Authoring a preset -1. **Start from a copy.** `copy(from, id, name)` copies a whole preset directory into the user root — composition, metadata, skill directories, assets. It validates the id (lowercase letters, digits, and hyphens, because it becomes the directory name), refuses an id any root already supplies, rolls a failed copy back, and rewrites the copy's `preset.yml` to keep the source's description while dropping its name and roster `order`. Prefer it over a shell copy: it needs no sandbox escalation, and the copy is exactly as loadable as its source. `standard` is the full coding agent and the usual source. -2. **Expect the file sandbox on every edit after the copy.** The user preset root lies outside the session workspace, so under the default `workspace-write` policy the first write there is denied. Retry that exact command once with `sandbox_permissions` escalation and a short justification — the user sees and approves it. Batch your writes (one heredoc per file) rather than escalating many small commands. `copy()` itself runs host-side and needs none of this; the edits do. -3. **Give the copy its own `name` and `description`** in `preset.yml`. +1. **Start from a copy.** `copy(from, id, name)` copies a whole preset directory into the user root — composition, metadata, skill directories, assets. It validates the id against `[a-z0-9][a-z0-9-]*` (it becomes the directory name, so no leading hyphen), refuses an id any root already supplies, rolls a failed copy back, and rewrites the copy's `preset.yml` to keep the source's description while dropping its name and roster `order`. Prefer it over a shell copy: it needs no sandbox escalation, and the copy is exactly as loadable as its source. `standard` is the full coding agent and the usual source. +2. **Expect the file sandbox on every edit after the copy.** The user preset root lies outside the session workspace, so under the default `workspace-write` policy the first write there is denied. Only writes are: reading any composition by absolute path needs no escalation. Retry that exact command once with `sandbox_permissions` escalation and a short justification — the user sees and approves it. Batch your writes (one heredoc per file) rather than escalating many small commands. `copy()` itself runs host-side and needs none of this; the edits do. +3. **Write the copy's `description`** in `preset.yml`, and its `name` if you passed none to `copy()`. 4. **Edit `agent.cordis.yml`** row by row, keeping the plane rule and the realm rule. 5. **Mount-validate the result**, then hand off to the user for a real session — both under *Verifying a change*. @@ -106,8 +106,8 @@ Realms are for services a preset owns, not for every group. A host capability th - a row whose package does not resolve (`Cannot find package …`); - a row whose config is invalid (`invalid config: $. missing required value`); -- a service published into the root realm (`service "" has been registered at `); -- a row that never activated (`N row(s) did not activate: : waiting for `). +- a row that never activated (`N row(s) did not activate: : waiting for `); +- a service published into the root realm, which arrives as one of two messages. A name the host does not supply lands in the root realm and the mount audit rejects it: `row(s) published process-global service(s) []; a preset service must sit behind an isolate realm or move to the host composition` — this is the shape a preset's own forgotten realm takes. A name the host already supplies collides before the audit: `service "" has been registered at `. Both name the offending service. It returns normally when the composition mounts. Run it as the final check on a finished edit rather than after every line: a successful mount installs a standing generation that lives until the process exits, while a failed one disposes its subtree and leaves nothing behind. From 782d670f24161ff41c0fb8de4f83aeb063a5e6c9 Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Tue, 11 Aug 2026 19:56:45 +0800 Subject: [PATCH 3/4] fix(preset): stop presenting the preset roots as constants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Neither root is a property of the preset layer. `Config.roots` defaults to `[]`; the two the shipped agent sees are patched in by the dsh CLI, whose user root is `dshHomePath('.agent-presets')` and whose system root ships beside the install. `writableRoot()` then takes the first `user` root from whatever configuration supplied it. The skill and the persona stated `${DSH_HOME:-$HOME/.dsh}/.agent-presets//` as where authored presets live, which is only the default install's answer, and no call reports either root: `authorable` says whether a writable one exists, and `list()` cannot reveal a user root that is still empty. Both now keep that path as what to tell a user asking where to look, and direct every path an agent acts on to `list()` or `resolve()` — which is also what `copy()` already relies on, since it chooses the writable root itself and reports the file it created. --- ...oring-agent-validates-its-own-composition.i18n.yaml | 4 ++-- ...et-authoring-agent-validates-its-own-composition.md | 2 ++ ...authoring-agent-validates-its-own-composition.zh.md | 2 ++ apps/cli/config/agent-presets/cordis/agent.cordis.yml | 2 +- .../cordis/skills/editing-cordis-compositions/SKILL.md | 10 ++++++---- 5 files changed, 13 insertions(+), 7 deletions(-) diff --git a/.agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.i18n.yaml index 9c173b1cd7..2b373ff1d9 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.i18n.yaml +++ b/.agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.md -2026-08-11-preset-authoring-agent-validates-its-own-composition.md: b7d20ef20c611a0ea0c694bac6bd3948bd194b2b -2026-08-11-preset-authoring-agent-validates-its-own-composition.zh.md: 512ac5ea0f47f4a796170b070c140f14b0072879 +2026-08-11-preset-authoring-agent-validates-its-own-composition.md: c513cd169f5e837bfe425ace882f0715b3107c7a +2026-08-11-preset-authoring-agent-validates-its-own-composition.zh.md: 92966ca90e036bddbbd9b8aa9534ba8e7ec5d488 diff --git a/.agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.md b/.agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.md index b7d20ef20c..c513cd169f 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.md +++ b/.agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.md @@ -32,6 +32,8 @@ The agent reaches the roster service the way `cordis_mount` documents: a tempora "Whether a row publishes a service" resolves through `cordis_inspect what:"services"`, which names the owning fiber of every live service. +Neither preset root is a constant the guidance may state as fact. `writableRoot()` takes the first `user` root from configuration, and no call reports either root's path — `authorable` answers only whether a writable one exists, and `list()` cannot reveal a user root that holds nothing yet. The skill and the persona therefore name `${DSH_HOME:-$HOME/.dsh}/.agent-presets/` as the default install's answer for a user asking where to look, and tell the agent to read every path it acts on back from `list()` or `resolve()`. + 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 diff --git a/.agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.zh.md b/.agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.zh.md index 512ac5ea0f..92966ca90e 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.zh.md +++ b/.agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.zh.md @@ -32,6 +32,8 @@ agent 按 `cordis_mount` 自身文档所述的方式够到 roster 服务:挂 「某行是否发布服务」改由 `cordis_inspect what:"services"` 回答,它会给出每个存活服务的持有 fiber。 +两个 preset 根都不是指导可以当作事实陈述的常量。`writableRoot()` 取配置中第一个 `user` 根,且没有任何调用会报告任一根的路径——`authorable` 只回答是否存在可写根,而 `list()` 无法揭示一个尚且为空的用户根。因此 skill 与 persona 只把 `${DSH_HOME:-$HOME/.dsh}/.agent-presets/` 作为默认安装下「用户问它在哪」的答案,并要求 agent 把每一个将要操作的路径都从 `list()` 或 `resolve()` 读回。 + 禁止改动随发布安装的约束,从创作步骤中的一段提升为顶部的 `## Off-limits` 一节,并扩展到禁止改宿主组装绕行。新增的自校验调用不削弱它:`copy()` 拒绝任何根已提供的 id,`remove()` 拒绝随部署发布的 preset。 ## Measured behavior diff --git a/apps/cli/config/agent-presets/cordis/agent.cordis.yml b/apps/cli/config/agent-presets/cordis/agent.cordis.yml index 6fa6030c97..2d08a8998d 100644 --- a/apps/cli/config/agent-presets/cordis/agent.cordis.yml +++ b/apps/cli/config/agent-presets/cordis/agent.cordis.yml @@ -24,7 +24,7 @@ Two planes decide where an edit belongs. The HOST composition holds the registries and anything shared across sessions — persistence, the sandbox and approval stack, the model route, the subagent registry and its backends. An AGENT PRESET holds what one session contributes to those registries: its tools, its persona, its prompt sections. A row that publishes a service belongs in the host composition, or inside an `isolate` realm if the preset genuinely owns that service and nothing outside one agent reads it. - Presets you author live under `${DSH_HOME:-$HOME/.dsh}/.agent-presets//`, one directory per preset. NEVER edit or delete the shipped preset install (the `agent-presets` directory beside the deployment's own config): it belongs to the deployment, an upgrade overwrites it, and corrupting the `cordis` preset would disable this very mode. To change what a shipped preset does, copy its composition into a new preset directory and edit the copy. + Presets you author live one directory per preset under this deployment's writable preset root — `${DSH_HOME:-$HOME/.dsh}/.agent-presets//` in a default install, but the roster reports every real path, so read one back rather than assuming it. NEVER edit or delete the shipped preset install (the `agent-presets` directory beside the deployment's own config): it belongs to the deployment, an upgrade overwrites it, and corrupting the `cordis` preset would disable this very mode. To change what a shipped preset does, copy its composition into a new preset directory and edit the copy. Load the `editing-cordis-compositions` skill before writing or changing a composition. diff --git a/apps/cli/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md b/apps/cli/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md index 32a59dbb1b..3a80c67af4 100644 --- a/apps/cli/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md +++ b/apps/cli/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md @@ -23,7 +23,9 @@ Two planes, and the choice is not about how "agent-related" something feels — **A service with a consumer outside the agent plane cannot move into a preset.** `subagents` is the worked example: the registry answers cross-session queries for the host api-proxy, so a per-session copy both starves that host row — it waits forever for a service nothing provides — and collides on the second session, since a provider name registers once. The preset contributes the delegation *tools*; the registry and its backends stay host-side. -A preset is a directory holding one `agent.cordis.yml`, optionally beside a `preset.yml` carrying display metadata — `name` and `description` (and, for shipped presets, a roster `order`). Write the metadata too: a preset without it shows up in every picker as its bare directory name. Locally authored presets live under `${DSH_HOME:-$HOME/.dsh}/.agent-presets//`. +A preset is a directory holding one `agent.cordis.yml`, optionally beside a `preset.yml` carrying display metadata — `name` and `description` (and, for shipped presets, a roster `order`). Write the metadata too: a preset without it shows up in every picker as its bare directory name. + +**Do not assume where presets live.** Both roots come from this deployment's configuration, and no call reports them directly: `authorable` says only whether a writable root exists. `copy()` picks that root itself, and every preset's absolute path comes back from `list()` and `resolve()` — read it from there. A default install puts locally authored presets under `${DSH_HOME:-$HOME/.dsh}/.agent-presets//` and the shipped set beside the deployment's own config, which is what to tell the user when they ask where to look, but never what to hand a file tool. ## The roster service @@ -31,8 +33,8 @@ A preset is a directory holding one `agent.cordis.yml`, optionally beside a `pre Read `cordis_inspect what:"api" name:"agentPresets"` for the current signatures before writing the code. The four calls this skill relies on: -- `list()` — every preset with its `id`, `trust`, and absolute `path`. This is how you locate the shipped compositions without knowing the install layout. -- `read(id)` — one preset's composition text. +- `list()` — every preset with its `id`, `trust` (`system` for the shipped set, `user` for authored ones), and the absolute `path` of its composition file. This is how you locate any composition without knowing the install layout; the directory is that path's parent. +- `read(id)` — one preset's composition text, without a file tool or a path. - `copy(from, id, name?)` — the only authoring write (see below). - `standingKeyFor(id)` — mount-validate one preset (see below). @@ -63,7 +65,7 @@ Unmount the plugin with `cordis_unmount` when you are done; it is a probe, not a ## Authoring a preset -1. **Start from a copy.** `copy(from, id, name)` copies a whole preset directory into the user root — composition, metadata, skill directories, assets. It validates the id against `[a-z0-9][a-z0-9-]*` (it becomes the directory name, so no leading hyphen), refuses an id any root already supplies, rolls a failed copy back, and rewrites the copy's `preset.yml` to keep the source's description while dropping its name and roster `order`. Prefer it over a shell copy: it needs no sandbox escalation, and the copy is exactly as loadable as its source. `standard` is the full coding agent and the usual source. +1. **Start from a copy.** `copy(from, id, name)` copies a whole preset directory into the user root — composition, metadata, skill directories, assets. It validates the id against `[a-z0-9][a-z0-9-]*` (it becomes the directory name, so no leading hyphen), refuses an id any root already supplies, rolls a failed copy back, and rewrites the copy's `preset.yml` to keep the source's description while dropping its name and roster `order`. Prefer it over a shell copy: it needs no sandbox escalation, it lands the copy in whichever root this deployment made writable, and the copy is exactly as loadable as its source. `resolve(id)` then names the file it created — that path, not a guessed one, is what the following edits target. `standard` is the full coding agent and the usual source. 2. **Expect the file sandbox on every edit after the copy.** The user preset root lies outside the session workspace, so under the default `workspace-write` policy the first write there is denied. Only writes are: reading any composition by absolute path needs no escalation. Retry that exact command once with `sandbox_permissions` escalation and a short justification — the user sees and approves it. Batch your writes (one heredoc per file) rather than escalating many small commands. `copy()` itself runs host-side and needs none of this; the edits do. 3. **Write the copy's `description`** in `preset.yml`, and its `name` if you passed none to `copy()`. 4. **Edit `agent.cordis.yml`** row by row, keeping the plane rule and the realm rule. From 3fb325cfeccf475853638603eafe81c5e3cf2693 Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Tue, 11 Aug 2026 20:04:32 +0800 Subject: [PATCH 4/4] fix(preset): state the user preset root plainly, resolve the acting path The previous pass over-corrected: presenting both roots as unknowable made the guidance vaguer without making it truer. Every `dsh` launcher puts authored presets under `$DSH_HOME/.agent-presets`, so the skill and persona state that as the answer to "where do my presets live" and reserve the read-back for the path an agent actually opens or edits, which `copy()` already reports. --- ...et-authoring-agent-validates-its-own-composition.i18n.yaml | 4 ++-- ...11-preset-authoring-agent-validates-its-own-composition.md | 2 +- ...preset-authoring-agent-validates-its-own-composition.zh.md | 2 +- apps/cli/config/agent-presets/cordis/agent.cordis.yml | 2 +- .../cordis/skills/editing-cordis-compositions/SKILL.md | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.i18n.yaml index 2b373ff1d9..67d2b330b1 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.i18n.yaml +++ b/.agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.md -2026-08-11-preset-authoring-agent-validates-its-own-composition.md: c513cd169f5e837bfe425ace882f0715b3107c7a -2026-08-11-preset-authoring-agent-validates-its-own-composition.zh.md: 92966ca90e036bddbbd9b8aa9534ba8e7ec5d488 +2026-08-11-preset-authoring-agent-validates-its-own-composition.md: 6b9cdf32b70e3ab4adc9f3b0e20bb3d2245486c7 +2026-08-11-preset-authoring-agent-validates-its-own-composition.zh.md: e6e8dabcd886a6331d294744b667552caa01e7b4 diff --git a/.agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.md b/.agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.md index c513cd169f..6b9cdf32b7 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.md +++ b/.agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.md @@ -32,7 +32,7 @@ The agent reaches the roster service the way `cordis_mount` documents: a tempora "Whether a row publishes a service" resolves through `cordis_inspect what:"services"`, which names the owning fiber of every live service. -Neither preset root is a constant the guidance may state as fact. `writableRoot()` takes the first `user` root from configuration, and no call reports either root's path — `authorable` answers only whether a writable one exists, and `list()` cannot reveal a user root that holds nothing yet. The skill and the persona therefore name `${DSH_HOME:-$HOME/.dsh}/.agent-presets/` as the default install's answer for a user asking where to look, and tell the agent to read every path it acts on back from `list()` or `resolve()`. +The guidance keeps `${DSH_HOME:-$HOME/.dsh}/.agent-presets/` as the answer to "where do my presets live" — it is where every `dsh` launcher puts them — while routing the path an agent actually reads or edits through `list()` or `resolve()`. `Config.roots` defaults to `[]` and `apps/cli` patches both roots in, `writableRoot()` takes the first `user` one, and no call reports either path; `authorable` answers only whether a writable root exists, and `list()` cannot reveal a user root that holds nothing yet. Stating the path is therefore right for talking to a person and wrong for feeding a file tool. 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. diff --git a/.agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.zh.md b/.agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.zh.md index 92966ca90e..e6e8dabcd8 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.zh.md +++ b/.agents/notes/implemented/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.zh.md @@ -32,7 +32,7 @@ agent 按 `cordis_mount` 自身文档所述的方式够到 roster 服务:挂 「某行是否发布服务」改由 `cordis_inspect what:"services"` 回答,它会给出每个存活服务的持有 fiber。 -两个 preset 根都不是指导可以当作事实陈述的常量。`writableRoot()` 取配置中第一个 `user` 根,且没有任何调用会报告任一根的路径——`authorable` 只回答是否存在可写根,而 `list()` 无法揭示一个尚且为空的用户根。因此 skill 与 persona 只把 `${DSH_HOME:-$HOME/.dsh}/.agent-presets/` 作为默认安装下「用户问它在哪」的答案,并要求 agent 把每一个将要操作的路径都从 `list()` 或 `resolve()` 读回。 +指导保留 `${DSH_HOME:-$HOME/.dsh}/.agent-presets/` 作为「我的 preset 在哪」的答案——每个 `dsh` 启动器都把它们放在那里——同时把 agent 实际读取或编辑的路径改走 `list()` 或 `resolve()`。`Config.roots` 默认为 `[]`,两个根均由 `apps/cli` 补入,`writableRoot()` 取其中第一个 `user` 根,且没有任何调用会报告任一路径;`authorable` 只回答是否存在可写根,而 `list()` 无法揭示一个尚且为空的用户根。因此写出该路径对人讲是对的,喂给文件工具是错的。 禁止改动随发布安装的约束,从创作步骤中的一段提升为顶部的 `## Off-limits` 一节,并扩展到禁止改宿主组装绕行。新增的自校验调用不削弱它:`copy()` 拒绝任何根已提供的 id,`remove()` 拒绝随部署发布的 preset。 diff --git a/apps/cli/config/agent-presets/cordis/agent.cordis.yml b/apps/cli/config/agent-presets/cordis/agent.cordis.yml index 2d08a8998d..2cfb9edf28 100644 --- a/apps/cli/config/agent-presets/cordis/agent.cordis.yml +++ b/apps/cli/config/agent-presets/cordis/agent.cordis.yml @@ -24,7 +24,7 @@ Two planes decide where an edit belongs. The HOST composition holds the registries and anything shared across sessions — persistence, the sandbox and approval stack, the model route, the subagent registry and its backends. An AGENT PRESET holds what one session contributes to those registries: its tools, its persona, its prompt sections. A row that publishes a service belongs in the host composition, or inside an `isolate` realm if the preset genuinely owns that service and nothing outside one agent reads it. - Presets you author live one directory per preset under this deployment's writable preset root — `${DSH_HOME:-$HOME/.dsh}/.agent-presets//` in a default install, but the roster reports every real path, so read one back rather than assuming it. NEVER edit or delete the shipped preset install (the `agent-presets` directory beside the deployment's own config): it belongs to the deployment, an upgrade overwrites it, and corrupting the `cordis` preset would disable this very mode. To change what a shipped preset does, copy its composition into a new preset directory and edit the copy. + Presets you author live one directory per preset under `${DSH_HOME:-$HOME/.dsh}/.agent-presets//`; the roster reports each preset's real path, so take the one you edit from there. NEVER edit or delete the shipped preset install (the `agent-presets` directory beside the deployment's own config): it belongs to the deployment, an upgrade overwrites it, and corrupting the `cordis` preset would disable this very mode. To change what a shipped preset does, copy its composition into a new preset directory and edit the copy. Load the `editing-cordis-compositions` skill before writing or changing a composition. diff --git a/apps/cli/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md b/apps/cli/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md index 3a80c67af4..744d2f13c1 100644 --- a/apps/cli/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md +++ b/apps/cli/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md @@ -25,7 +25,7 @@ Two planes, and the choice is not about how "agent-related" something feels — A preset is a directory holding one `agent.cordis.yml`, optionally beside a `preset.yml` carrying display metadata — `name` and `description` (and, for shipped presets, a roster `order`). Write the metadata too: a preset without it shows up in every picker as its bare directory name. -**Do not assume where presets live.** Both roots come from this deployment's configuration, and no call reports them directly: `authorable` says only whether a writable root exists. `copy()` picks that root itself, and every preset's absolute path comes back from `list()` and `resolve()` — read it from there. A default install puts locally authored presets under `${DSH_HOME:-$HOME/.dsh}/.agent-presets//` and the shipped set beside the deployment's own config, which is what to tell the user when they ask where to look, but never what to hand a file tool. +Locally authored presets live one directory per preset under `${DSH_HOME:-$HOME/.dsh}/.agent-presets/`, and the shipped set sits beside the deployment's own config. Use those when the user asks where to look. Both roots are configuration rather than fixed locations, though, and no call reports them — `authorable` says only whether a writable one exists — so take the path you actually read or edit from `list()` or `resolve()`, which is also where `copy()` reports what it just created. ## The roster service