Plugin Registry
dsh has two "plugin registration" paths:
- Static registration:
cordis.patch.yml→ Loader →ctx.plugin→ RegistryService. Repo-bundled plugins, plugins installed by profile, and bundle-layered plugins all go this way. - Dynamic registration: the agent injects code into the process at runtime via the
cordis_definetool. Session-scoped, not persisted, lost on restart.
Static Registration: PluginInventoryGateway
class PluginInventoryGateway at packages/host/plugin-inventory/src/index.ts:43-70:
inject=['loader']@Remote('list')directly iteratesloader.entries()- Maps
entry.fiber.stateto the stringspending/loading/active/failed/disposed/unloading - No caching on reads; consistency is maintained via Cordis's own events
PluginInventoryGateway.list() exposes only entryId / moduleName / enabled / fiberPhase on non-group entries — it deliberately omits config, because config may contain secrets.
Dynamic Registration: DynamicCordisRegistry
Registry Structure
class DynamicCordisRegistry at packages/extensions/cordis-host-runner/src/registry.ts:141-184:
plugins: Map<…, DynamicCordisPlugin>
nextPlugin / nextPackage / nextRun / nextApproval // four countersThe four counters mint prefix-N / pkg-N / run-N / approval-N IDs respectively.
DynamicCordisPlugin Structure
DynamicCordisPlugin at packages/extensions/cordis-host-runner/src/registry.ts:36-70:
packages: Map<…, DynamicCordisDefinition>— version historyapprovedClientPackages/clientVersionUpdatesApproved— user authorization gatescurrentPackageId/nextPackageId/run/latestRun— current and target versions
Runner Service
class DynamicCordisRunnerService extends TypertRemoteService at packages/extensions/cordis-host-runner/src/index.ts:124-145:
static inject = ['tools']- Holds a
DynamicCordisRegistry+CordisInspectRegistryService+ astartingMap (to prevent concurrent activations)
define: Define Without Executing
define(request) at packages/extensions/cordis-host-runner/src/index.ts:151-202:
- Validates name / purpose.
precheckCodesandbox pre-parse.- Mints plugin / package IDs.
- Writes
definition.hostCode/clientCode.
Define does not execute — this is the safety boundary of dynamic registration. Code is stored first; activation goes through a separate run.
Immutable Package Versions for Dynamic Plugins
Editing code = cordis_define appending a new packageId; activation = cordis_run switching versions:
currentPackageIdonly advances after success.- On failure it does not roll back to the old version (the caller must explicitly
runto roll back).
This gives dynamic-plugin version evolution an audit trail, and failure states are observable via the inspect tools.
Two Identity Models
| Static Plugin | Dynamic Plugin | |
|---|---|---|
| Identity | Loader entry id | plugin-{prefix}-{N} |
| Persistence | cordis.patch.yml / profile | minted in-process, lost on restart |
| Source | npm package / local path | generated by the agent at runtime |
Docs and Tools Share One Source
class CordisCatalogProjector at packages/typert/generator/src/cordis-catalog.ts:136-200 projects a Service / Event catalog out of the Typert model.
collectEvents / collectServices at packages/typert/generator/src/cordis-catalog.ts:403-413: scans interface Events / interface Context blocks, parsing @mode tags + JSDoc.
cordis-catalog.ts is both the doc generator and the data source for the cordis_inspect_list tool (tool-cordis/src/api-catalog.ts is generated by scripts/gen-cordis-api.ts using the same AST walk), ensuring that "docs" and "the API the model sees" share one origin.
Approval and Cancellation
The pendingRequests index of DynamicCordisRegistry supports cancellation / claiming (claimRequest is first-answer-wins) for an activation request "awaiting user approval". This is the human-in-the-loop gate for dynamic plugins — the agent defines the code, the user approves it in the UI, and only then does the system activate it.
What to read next
- Loading and Discovery — how the Loader turns patch.yml into a running fiber
- Plugin Context API — which tools the agent uses to manipulate dynamic plugins