Add `dsh-host-plugin-manager`: the `pluginManager` service and the `plugins` Remote that install, uninstall, enable, disable, and retry bundles over the booted profile, add and remove rows in the profile's or an agent preset's user layer, report what a package's services keep alive, and fold the manifest, the probe record, and the live tree into one view per package. pnpm runs the way the CLI runs it, streaming its output as `plugins/install-log`; every change emits `plugins/changed`, and api-remotes forwards both. Enabling is the Loader's own transaction: a rejected recomposition restores the layer list, and a runtime recomposition records contained rows through `recordContainedStates` because the boot audit does not run again; `ContainedGroup.create` now records a row that resolved pending rather than clearing it. Give every agent preset a user patch layer: `cordis.patch.yml` beside a locally authored composition, or alone in `.agent-presets/<id>/` for a shipped one. Discovery attaches and judges it, the mount applies it as the include's runtime patches, standing generations follow the layer's content and return to a retired generation when it comes back, the composition inventory reports each row's `source` and `disabledBy`, a copy carries the layer, and `removeOverlay` deletes it. `dsh-global-tool-mask` is `tools.restrict()` as a scope-only row for such a layer. Add `dsh-patch-file` as the one home of the patch-list format: the parser moved out of app-boot, plus a comment-preserving key-level writer with lock, atomic replace, and readback. app-boot exports `layerTrust`, `bundleLayerPatches`, and `recordContainedStates`, and `ProfileRuntime` carries the install anchor.
description, kind
| description | kind |
|---|---|
| Read-only projection of the current Cordis Loader plugin state with each agent preset's composition beside it: the pluginInventory service and its pluginInventory/list Remote for web GUI host clients. | package-reference |
@deepseek-ai/dsh-host-plugin-inventory
English | 中文
Summary
Clients and settings pages can show what is currently composed in the host: calling pluginInventory/list returns the current non-group Loader entries in Loader order — entry id, module specifier, effective enablement, and root Fiber phase (pending, loading, active, failed, or unloading, or null when an entry has no live root Fiber). When an agent-preset roster is composed, the snapshot also carries one group per preset — id, trust, display name, default marking, health, and flattened composition rows — because a deployment that mounts the roster runs its model-facing plugins there rather than on the Loader's own entries. The snapshot is point-in-time: the Loader is the sole lifecycle authority, and this package owns no cache, history, provenance model, event stream, or mutation path. Client packages consume the Remote through the explicit api-remotes assembly rather than importing the Host implementation.
Table of Contents
- Use this package
- Understand the implementation
- Further Exploration
- Model Experience
- Known Limitations and Deferred Work
- Dev Note
Use this package
Call pluginInventory/list when a client or settings page needs to show what is currently composed in the host — which plugins are loaded, enabled, and alive, and what each agent preset would give a session. The Remote is the only entry point: the service is Remote-only and deliberately declares no same-process Cordis Context merge.
What a snapshot contains
Each row is one non-group Loader entry: its entry id, the exact module specifier, the effective enablement (including disabled ancestor groups), and the current root Fiber phase. pending means the entry waits to load, loading that it is being read, active that it is running, failed that its fiber rejected, and unloading that it is being torn down; null means no live root Fiber exists at all. Structural group rows are skipped.
When the profile launcher composed the tree, each row also says who supplied it: trust is builtin for a row of the installation's own bundles and external for a row of a bundle the user installed, package names that bundle with its version and, for an external row, the id the bundle's own patch declared before the launcher prefixed it, and a disabled row carries disabledBy — user when a user patch file disabled it with a literal disabled: true, composition for a bundle's own gate or tombstone. A row an isolated external bundle failed to start is gone from the tree; it is still listed, with fiberPhase: 'failed' and a failure naming the stage and message, from the launcher's failure registry. Without the launcher every row reads builtin and none carries a package or failure.
Per-preset compositions
With a roster composed, agentPresets carries one group per preset in roster order: its id, whether the deployment ships it or the user owns it (trust, which clients use to localize shipped names), published display name, whether a session naming no preset composes it, and flattened plugin rows — entry id (null when the file row declares none), module specifier, effective enablement, the row's own !!js disabled expression when it carries one, a root-fiber phase when the composition is live, whether the preset's composition file or its user patch layer supplied the row (source), and for a row that is off whether the composition or the layer switched it off (disabledBy). A preset some session already composed answers from its newest standing generation — even when its file has since broken, because the mount is what those sessions run; one never composed since boot answers from its composition file with disabled gates evaluated against the Loader context, and reading never mounts a preset. conditional enablement marks a gate the Host could not evaluate, and a broken preset nothing composed stays listed with its reason and no rows. Without a roster the field is absent.
What you can and cannot do with it
The inventory is a snapshot for display and diagnostics: a client can render the roster, flag failed entries, and detect changes by comparing snapshots. It cannot enable, disable, add, or remove plugins, and it carries no history — a fiber that already failed and was removed is absent. Because the service reads the Loader on every call, the answer always reflects the current composition rather than a cached view.
Understand the implementation
Implementation internals — click to expand
Design concept
The gateway is a direct projection with no second lifecycle truth: every list() call reads ctx.loader.entries() and maps each non-group entry to its public row. Cordis's internal plugin/status events already maintain Entry.fiber and Fiber.state, so a cache would only add another lifecycle truth to keep synchronized. The agent-preset roster is an optional peer resolved per call through ctx.get('agentPresets'): its compositionInventory() owns every preset read, and this package only maps root-fiber states onto the public phase vocabulary.
The phase mapping
Fiber states map onto the public phase vocabulary, with disposed folding into null — an entry whose fiber is gone has no live root to report. The phase therefore never distinguishes why no live root exists: the entry may never have started, or its fiber may already have been disposed.
Source map
| File | Role |
|---|---|
src/index.ts |
PluginInventoryGateway: the pluginInventory Remote service and the Loader projection |
src/types.ts |
Public payload types: PluginInventoryEntry, PluginInventorySnapshot, PluginFiberPhase |
| — | No runtime invariant companion is published; every snapshot is projected directly from Loader-owned state. |
Typert generates the Host and Client Remote artifacts exposed by ./typert and ./remote.
Further Exploration
Read these when the inventory contract is not enough: how the Remote reaches clients, then the Loader it projects and the surface that renders it.
- Remote assembly — how clients consume
pluginInventory/listwithout importing the Host implementation. - Cordis plugin loader — the Loader whose entries this package projects.
- Plugin inventory settings surface — the browser-side projection that renders the inventory.
Model Experience
None, as the host-side read-only Loader projection registers nothing model-facing.
KV Cache effect
None; this package neither assembles nor sends a provider request.
Known Limitations and Deferred Work
These limits define what a point-in-time inventory cannot tell a client. They are current package constraints, not a task backlog.
- Point-in-time state only — the result contains no durable failure history or subscription; a missing root Fiber is reported as
null, regardless of why no live root exists. - No mutation — the service cannot enable, disable, add, or remove plugins in either plane; provenance stops at the bundle layer, so a row a user patch or a
--patchoverlay inserted carries no package. - Presets appear only with a roster — a deployment without
dsh-agent-presetsserves Loader entries alone; theagentPresetsfield is absent rather than empty.
Dev Note
Working context for maintainers — click to expand
None.