fix(agent-presets,web): broken presets are roster rows, not gaps

A hand-damaged preset was silent until the worst moment. An unparsable
composition listed as an ordinary selectable row and failed only at the
next session start — set as default, every new session failed. A
directory whose composition file was deleted vanished from the roster
while still occupying its id: copy answered "delete the existing preset
first" while remove answered "not found", a dead end.

Discovery now owns health: every id-shaped directory is a roster slot,
broken when its composition is missing or unloadable, checked with the
loader's own entryListSchema dialect (!!js included) so health never
rejects what the loader accepts. `broken` rides AgentPreset, the
agentPreset.list entry, and the UI row; mount/recompose/standingKeyFor
refuse broken up front with the discovery-reported reason, while
resolve/read/remove still answer. The section renders marked red cards —
unselectable, uncopyable, deletable, location kept on custom rows — and
both pickers drop broken rows entirely.

The cordis preset's persona now forbids editing the shipped install
(corrupting cordis would disable the mode itself) and points authoring
at $DSH_HOME/.agent-presets; its skill teaches preset.yml metadata, the
copy-first workflow, the one-escalation sandbox reality, and honest
verification. Exercised live: asked to edit the shipped composition the
composed agent refuses citing both rules; asked for real presets (simple
and complex) it lands them under the user root with one approved
escalation each and self-checks with the loader dialect.
This commit is contained in:
Yichen Jiang
2026-08-09 02:17:56 +08:00
parent bf31797e95
commit c56f23ae1f
36 changed files with 720 additions and 87 deletions

View File

@@ -42,6 +42,8 @@ The dialog mirrors the host's own containment rule (`[a-z0-9][a-z0-9-]*`) and re
Deleting removes the preset directory. Sessions already composed from it keep running — a composition is mounted once at session creation and nothing re-reads the file.
A roster row carrying `broken` (the host's shape check found the composition missing or unloadable) renders as a marked card: red border, a Broken badge, the reason verbatim, the body disabled — it cannot become the default — and duplication disabled, since a copy of a broken preset is another broken preset. A broken custom row keeps its location and delete actions, because the files are where it gets fixed and deleting is how a ghost directory (composition deleted by hand, directory still blocking the id) is cleared; a broken shipped row withholds the viewer too — there is no readable composition to show. The two pickers (the General row and the new-session chip) drop broken presets entirely: they choose the NEXT session's composition, and offering one that cannot compose would only defer the failure to the session start.
Setting the default writes the `agent-presets` settings namespace, which the host exposes to configuration clients ([`dsh-apiproxy`](../../host/apiproxy/README.md) keeps an explicit allowlist — a namespace outside it makes a picker move and then silently forget).
`agentPreset.read`, `copy`, `openDocument`, and `remove` are loopback-pinned ([`dsh-client-connection`](../connection/README.md)): a composition names the plugins a session runs, so reading one is reconnaissance, and the rest manage the roster and drive the host desktop. `agentPreset.list` is not — it carries ids, trust, and the two path-free capability flags, and a LAN client's picker needs it.