feat(settings): serve every registered namespace and key plugin cards on it

A plugin that registered a settings namespace could not reach the browser
configuration page: the api-proxy filtered every read and gated every
write through two hardcoded namespace lists, and the plugin configuration
section rendered an unordered list of cards carrying an opaque id rather
than the namespace they edit. Both gates lived in this repository, so a
user-authored plugin was configurable only by hand-editing settings.yaml.

The proxy now serves whatever ctx.settings.describe() returns and adds no
boundary of its own; a name no registration answers folds into the seam's
own settings-rejected, and the settings-not-exposed code retires. The
settings seam is untouched: which client may read a namespace, and which
page renders it, are facts about consumers.

settings.plugin.item becomes a keyed slot whose key is the namespace a
card edits, following tool.call.toolview. The section reads describe once
and dispatches the intersection of the slot ledger and the served set, so
a namespace another surface owns renders nothing without declaring
anything, and a card for an uncomposed plugin is never dispatched.
This commit is contained in:
Yichen Jiang
2026-08-12 21:07:57 +08:00
parent 4bc526f40b
commit 4366528a38
36 changed files with 745 additions and 197 deletions

View File

@@ -6,13 +6,13 @@ The **Plugins** settings section: one expandable card per Host plugin whose conf
## What appears here
A card renders only when its namespace is both registered by a live Host plugin and served to the browser. A deployment that does not compose the owning plugin — or serves the namespace to no client — renders nothing for it rather than an empty or disabled card, so the section reflects what this deployment actually runs.
The section reads which settings namespaces the Host serves and dispatches one slot key per namespace, so what renders is the intersection of two ledgers: the namespaces a live Host plugin registered, and the cards registered under those keys. A served namespace no card claims renders nothing — another surface owns it, or this deployment ships no browser half for it — and a card whose namespace this deployment does not serve is never dispatched, so an uncomposed plugin leaves no trace and does not hold the section back from its empty line. Cards appear in the order they registered, not the order the Host describes their namespaces — plugin activation can reorder the description between boots. The empty line waits for the Host's first answer, so an unanswered read never reads as "this deployment configures no plugin".
The first batch covers the shell executor (`bash`), the agent loop's tool-call parallelism (`agent-loop`), and the DeepSeek search provider (`web-search-deepseek`).
The cards this package ships cover the shell executor (`bash`), the agent loop's tool-call parallelism (`agent-loop`), and the DeepSeek search provider (`web-search-deepseek`).
## Extension point
The section declares `settings.plugin.item`, a root list slot. A plugin that ships a browser half registers its own card into that slot and owns its controls; this package neither enumerates namespaces nor renders a form it was not given. Ordering follows the slot's `order`.
The section declares `settings.plugin.item`, a root keyed slot whose key is the settings namespace a card edits. A plugin that ships a browser half registers its own card under its own namespace and owns every part of it — chrome, controls, and copy; this package supplies no form it was not given and never learns what a namespace means. Keying on the namespace is what lets a plugin distributed outside this repository appear here: it registers the namespace on the Host and the card in the browser, and the section pairs the two.
## Writes
@@ -35,6 +35,6 @@ None; this package neither assembles nor sends a provider request.
## Known Limitations and Deferred Work
- **Only host-plane plugins appear** — a plugin an agent preset mounts carries its configuration inline in that preset's `agent.cordis.yml` and cannot register a settings namespace at all (a second session mounting the same preset would fail on a duplicate registration), so this section lists nothing for it. Editing those values remains the preset editor's job.
- **Exposure is a Host allowlist, not a plugin declaration** — a namespace absent from the api-proxy's allowlist answers `settings-not-exposed` even when its owner registered it, so a plugin distributed outside this repository cannot surface its own configuration here without a change in `packages/host/apiproxy`.
- **A card still needs a browser bundle** — the browser half must be a `dsh.client` package built in the client module system's lazy-CJS factory format, and the `clientBundle` preset that emits it lives in `packages/client/tsdown.client.ts` rather than a published package, so a plugin outside this repository has to reproduce that build itself. The bundle-purity gate also forbids importing this package's card chrome or form model as values, so such a card owns its own staging and revision fencing.
- **The served namespaces re-read on two signals only** — the wire announces settings-document commits and connection resets, not registrations, so a namespace whose owner registers after the section's read joins the list on the next document commit or reconnect.
- **The shell card follows the composed executor** — the POSIX and PowerShell executor families share the `bash` namespace because a host composes exactly one of them, so the served schema differs by platform (PowerShell adds `pwshPath`) even though the card edits the same two fields on both, and a deployment composing neither shows no card.
- **The empty line counts registered cards, not visible ones** — a card whose namespace this deployment does not expose renders nothing, but still counts, so a deployment that exposes none shows an empty list rather than the empty line. The count is also read once, because the renderer caches a root entry's inject face; a card registered later does not raise it.