feat(agent-presets): ship a cordis agent that can author compositions

A third built-in preset: the standard coding agent plus the self-referential
Cordis toolset, a persona that explains the two-plane split, and a skill
teaching composition authoring. It exists so a person can ask an agent to
author another agent.

The skill ships INSIDE the preset directory rather than in the user's skill
root, and the root is derived from the preset's own `baseUrl` — the loader
evaluates `!!js` with `with (ctx)`, so a composition can locate itself. A
preset is the unit that gets copied and edited, so its documentation should
travel with it.

The skill leads with the rule that actually bites: a row publishing a service
may not sit loose in a preset, whether a row publishes one is not visible from
its name (`tool-bash` provides `bashEnv`), and a consumer left outside its
provider's isolate group resolves the host registry and then contributes
nothing — the quietest failure this design has.

Writing the test surfaced a consequence worth stating: an entry-local realm
makes the service invisible to the agent's own scope too, not just to the host.
Only rows inside that group resolve it, which is precisely what makes
`tool-skill` this agent's own rather than a shared one. The test asserts what
is actually observable from outside instead of reaching for the isolated
service.

TRUST: `cordis_mount` evaluates model-written JavaScript against the live
runtime, and a composition this agent writes becomes a preset other sessions
mount. Both the preset header and the toolset's own documentation say to treat
this as shell access. The tools stay opt-in per session — a test pins that they
are absent from every other preset.
This commit is contained in:
Yichen Jiang
2026-08-04 00:36:39 +08:00
parent 98fbe0ee94
commit 3ae22b3835
6 changed files with 369 additions and 3 deletions

View File

@@ -76,7 +76,7 @@ describe('the shipped Web composition', () => {
it('supplies both shipped presets, and only those, from the system root', async () => {
const listed = await ctx.agentPresets.list()
expect(listed.map(preset => preset.id).sort()).toEqual(['core-web', 'standard'])
expect(listed.map(preset => preset.id).sort()).toEqual(['cordis', 'core-web', 'standard'])
expect(listed.every(preset => preset.trust === 'system')).toBe(true)
expect(ctx.agentPresets.defaultId).toBe('standard')
})
@@ -139,6 +139,51 @@ describe('the shipped Web composition', () => {
}
})
it('composes the cordis agent with its own toolset', async () => {
const handle = await ctx.agents.create({
sessionId: SessionId('preset-cordis'),
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'cordis').then(() => undefined),
})
try {
const tools = toolNames(ctx, handle.agent)
// The self-referential toolset is what distinguishes this preset.
expect(tools).toEqual(expect.arrayContaining(['cordis_inspect', 'cordis_mount', 'cordis_unmount']))
// And it keeps the standard agent's own tools rather than replacing them.
expect(tools).toEqual(expect.arrayContaining(['bash', 'read', 'edit', 'skill']))
// The skill registry sits in this preset's entry-local realm, so it is
// invisible to the host AND to the agent's own scope — only the rows
// inside that group resolve it, which is what makes `tool-skill` the
// agent's own rather than a shared one.
expect(ctx.get('skills')).toBeUndefined()
} finally {
await handle.dispose()
}
})
it('keeps the self-referential toolset out of every other preset', async () => {
const handle = await ctx.agents.create({
sessionId: SessionId('preset-no-cordis'),
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'standard').then(() => undefined),
})
try {
// Editing the live runtime is opt-in per session, not ambient.
expect(toolNames(ctx, handle.agent)).not.toContain('cordis_mount')
} finally {
await handle.dispose()
}
})
it('ships the composition-authoring skill inside the preset directory', async () => {
// The preset's skill root is derived from its own `baseUrl`, so the skill
// travels with the directory wherever the preset is installed.
const skill = join(
CONFIG_DIR, 'agent-presets', 'cordis', 'skills', 'editing-cordis-compositions', 'SKILL.md',
)
expect((await readFile(skill, 'utf8')).startsWith('---\nname: editing-cordis-compositions')).toBe(true)
})
it('never rewrites the preset file it composed from', async () => {
// The Loader persists a tree whose plugin self-disposed, and tearing an
// agent down disposes its whole subtree. Inherited, that rewrote the