fix(web): keep the subagent registry in the host plane
`dsh web` failed to boot: the aggressive plane split moved `subagents` and its spawn/fork backends into the preset's entry-local realm, but `dsh-host-apiproxy` is a host row that injects `subagents` to answer the browser's cross-session queries, so it waited forever for a service only sessions now provided. A per-session copy is wrong twice over — a provider name registers once, so the second session would have collided anyway. The registry and its backends go back to the host composition; the presets keep the delegation TOOLS, which resolve that host registry. `workflows` stays entry-local: nothing outside an agent reads it. The web real-composition test could not have caught this, because it disabled `api-gateway` — the very row whose pending injection names the break. It now boots with the api-proxy enabled and the browse directory picker substituted, so the boot audit covers the whole host-plane injection graph. Re-introducing the old split makes it fail with the three pending entries, which is how this was verified.
This commit is contained in:
@@ -22,7 +22,7 @@
|
||||
|
||||
You can read and modify the harness you run on. Its composition is Cordis: every capability is a plugin row in a `cordis.yml`, and an agent preset is one such file mounted for a single session.
|
||||
|
||||
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. An AGENT PRESET holds what one session contributes to those registries: its tools, its persona, its delegation backends. A row that publishes a service belongs in the host composition, or inside an `isolate` realm if the preset genuinely owns that service.
|
||||
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.
|
||||
|
||||
Load the `editing-cordis-compositions` skill before writing or changing a composition.
|
||||
|
||||
@@ -157,29 +157,21 @@
|
||||
|
||||
# ── delegation and workflows ────────────────────────────────────────────────
|
||||
|
||||
# Every backend and every tool that reaches `subagents` or `workflows` shares
|
||||
# one realm: a consumer left outside it would resolve the host's registry
|
||||
# instead, which this preset does not populate.
|
||||
# The `subagents` registry and its spawn/fork backends live in the HOST
|
||||
# composition: the registry is a process singleton whose cross-session queries
|
||||
# the api-proxy serves to the browser, and a provider name may only be
|
||||
# registered once. This preset contributes the delegation TOOLS, which resolve
|
||||
# that host registry.
|
||||
#
|
||||
# `workflows` is different — nothing outside an agent reads it — so every row
|
||||
# that reaches it shares one entry-local realm here, and a consumer left
|
||||
# outside would resolve a host registry this preset does not populate.
|
||||
- id: delegation
|
||||
name: cordis:group
|
||||
group: true
|
||||
isolate:
|
||||
subagents: true
|
||||
workflows: true
|
||||
config:
|
||||
- id: subagent
|
||||
name: '@deepseek-ai/dsh-subagent'
|
||||
|
||||
- id: subagent-spawn
|
||||
name: '@deepseek-ai/dsh-subagent-spawn'
|
||||
config:
|
||||
providerName: spawn
|
||||
|
||||
- id: subagent-fork
|
||||
name: '@deepseek-ai/dsh-subagent-fork'
|
||||
config:
|
||||
providerName: fork
|
||||
|
||||
- id: tool-subagent-control
|
||||
name: '@deepseek-ai/dsh-tool-subagent-control'
|
||||
|
||||
|
||||
@@ -11,9 +11,11 @@ Every capability in this harness is a plugin row in a `cordis.yml`. There is no
|
||||
|
||||
Two planes, and the choice is not about how "agent-related" something feels — it is about whether the thing must be shared.
|
||||
|
||||
**Host composition.** The registries themselves (`tools`, `systemPrompt`, `agents`, `agent-loop`, `sessions`), anything crossing sessions (persistence, session query, storage, settings, credentials, telemetry), the sandbox and approval stack, and the model route. One instance for the process.
|
||||
**Host composition.** The registries themselves (`tools`, `systemPrompt`, `agents`, `agent-loop`, `sessions`), anything crossing sessions (persistence, session query, storage, settings, credentials, telemetry), the sandbox and approval stack, the model route, and the subagent registry with its spawn/fork backends. One instance for the process.
|
||||
|
||||
**Agent preset.** What one session contributes to those registries: its tool plugins, its persona, its delegation backends, its compaction policy. One instance per session, mounted under that session's scope and unwound with it.
|
||||
**Agent preset.** What one session contributes to those registries: its tool plugins, its persona and prompt sections, its compaction policy. One instance per session, mounted under that session's scope and unwound with it.
|
||||
|
||||
**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`. The shipped ones live beside the deployment's composition; locally authored ones live under `$DSH_HOME/.agent-presets/<name>/`.
|
||||
|
||||
|
||||
@@ -172,29 +172,21 @@
|
||||
|
||||
# ── delegation and workflows ────────────────────────────────────────────────
|
||||
|
||||
# Every backend and every tool that reaches `subagents` or `workflows` shares
|
||||
# one realm: a consumer left outside it would resolve the host's registry
|
||||
# instead, which this preset does not populate.
|
||||
# The `subagents` registry and its spawn/fork backends live in the HOST
|
||||
# composition: the registry is a process singleton whose cross-session queries
|
||||
# the api-proxy serves to the browser, and a provider name may only be
|
||||
# registered once. This preset contributes the delegation TOOLS, which resolve
|
||||
# that host registry.
|
||||
#
|
||||
# `workflows` is different — nothing outside an agent reads it — so every row
|
||||
# that reaches it shares one entry-local realm here, and a consumer left
|
||||
# outside would resolve a host registry this preset does not populate.
|
||||
- id: delegation
|
||||
name: cordis:group
|
||||
group: true
|
||||
isolate:
|
||||
subagents: true
|
||||
workflows: true
|
||||
config:
|
||||
- id: subagent
|
||||
name: '@deepseek-ai/dsh-subagent'
|
||||
|
||||
- id: subagent-spawn
|
||||
name: '@deepseek-ai/dsh-subagent-spawn'
|
||||
config:
|
||||
providerName: spawn
|
||||
|
||||
- id: subagent-fork
|
||||
name: '@deepseek-ai/dsh-subagent-fork'
|
||||
config:
|
||||
providerName: fork
|
||||
|
||||
- id: tool-subagent-control
|
||||
name: '@deepseek-ai/dsh-tool-subagent-control'
|
||||
|
||||
|
||||
@@ -30,19 +30,22 @@ async function bootWeb(settingsFile: string, extra: PatchOptions[] = []): Promis
|
||||
// outcome. Point it at a temp file for the same reason the roster below
|
||||
// names only the shipped root.
|
||||
{ id: 'settings', config: { path: settingsFile, watch: false } },
|
||||
// Host rows with side effects outside this process: a bound port, a
|
||||
// served asset tree, a telemetry exporter.
|
||||
// Host rows with side effects outside this process: a bound port, a served
|
||||
// asset tree, a telemetry exporter. `api-gateway` and `directory-picker`
|
||||
// stay ENABLED on purpose — the api-proxy is the host row that injects
|
||||
// `subagents`, `workspace`, and the rest of the agent plane, so disabling
|
||||
// it would hide exactly the breakage this file exists to catch: a service
|
||||
// moved into the presets that a host row still waits for. The boot audit
|
||||
// is that assertion.
|
||||
{ id: 'webserver', disabled: true },
|
||||
{ id: 'telemetry-otel', disabled: true },
|
||||
{ id: 'modules', disabled: true },
|
||||
{ id: 'connection', disabled: true },
|
||||
// NOT a side-effect row: the api-proxy cannot mount in THIS layer at all,
|
||||
// because it injects `subagents` and the subagent registry moved into the
|
||||
// presets here. That is the breakage a later layer returns to the host
|
||||
// plane; when it does, this line comes out and the boot audit covers the
|
||||
// whole host-plane injection graph again.
|
||||
{ id: 'api-gateway', disabled: true },
|
||||
// The shipped `-auto` chooser resolves its interaction from a running
|
||||
// host and so waits for the webserver disabled above; the browse variant
|
||||
// supplies `directoryPicker` without one.
|
||||
{ id: 'directory-picker', disabled: true },
|
||||
{ insert: [{ id: 'directory-picker-browse', name: '@deepseek-ai/dsh-host-directory-picker-browse' }] },
|
||||
// The roster AppCLIEntry would patch in; only the shipped root, so a
|
||||
// developer's own `~/.dsh/.preset` cannot change this test's outcome.
|
||||
// `default` here is the COMPOSITION default — the base layer the settings
|
||||
@@ -401,7 +404,7 @@ describe('the default preset as a user setting', () => {
|
||||
})
|
||||
|
||||
describe('a session keeps the preset it was created with', () => {
|
||||
it('records the preset the gateway guard reads', async () => {
|
||||
it('refuses to adopt a live session under a different preset', async () => {
|
||||
const handle = await ctx.agents.create({
|
||||
sessionId: SessionId('preset-locked'),
|
||||
meta: { agentPreset: 'core-web' },
|
||||
|
||||
Reference in New Issue
Block a user