feat(web): expose the agent-preset roster over the API

`agentPreset.list` gives a browser the deployment's roster so it can offer a
choice when starting a session. Each row carries the id, its `trust`, and
whether it is the current default.

`trust` is on the wire deliberately: a `user` preset is exactly as privileged
as the plugins it names, so a surface that offers one alongside a shipped
preset can say which is which rather than presenting both as vetted.

The domain is read-only. A preset is a composition on disk, so authoring one
is a filesystem act rather than an RPC; and a deployment composing no presets
answers with an empty roster rather than an error, because sharing the host
composition is a valid deployment.

The RPC map made every registration site a type error, so the route, the
response-schema table, the service delegate, and the browser fixture are all
wired rather than only the ones I remembered.
This commit is contained in:
Yichen Jiang
2026-08-04 00:15:37 +08:00
parent 8d06b2d576
commit e6fe32b3c3
16 changed files with 159 additions and 2 deletions

View File

@@ -2312,6 +2312,17 @@ export function createFixtureApi(options: FixtureOptions = {}): ApiProxy {
return ok(request, { matched: true as const, commandId })
},
},
agentPresets: {
// Two rows so a picker has something to choose between, and so the
// trust distinction a surface must present is visible in the fixture.
list: request => ok(request, {
presets: [
{ id: 'standard', trust: 'system' as const, isDefault: true },
{ id: 'core-web', trust: 'system' as const, isDefault: false },
],
}),
},
skills: {
list: (request) => {
const missing = requireSession(request)
@@ -2609,6 +2620,7 @@ export class FixtureApiClient extends AbstractApiClient {
case 'command.list': return this.api.commands.list(request)
case 'command.execute': return this.api.commands.execute(request, signal)
case 'skill.list': return this.api.skills.list(request)
case 'agentPreset.list': return this.api.agentPresets.list(request)
case 'goal.create': return this.api.goals.create(request)
case 'goal.edit': return this.api.goals.edit(request)
case 'goal.pause': return this.api.goals.pause(request)