fix(web): address a session's own services from the host

A preset publishes its services behind `isolate` realms, which is what
makes them per session — and what makes them invisible to every host
context. The api-proxy kept reading the root realm, so requests that are
ABOUT a session but arrive from outside it answered for a singleton that
no longer exists: `goal.pause`/`clear` and `skill.list` returned "this
deployment does not mount @deepseek-ai/dsh-goal / dsh-skill" for sessions
whose composition mounts exactly that. Verified against a running host
before and after.

`agentPresets.serviceFor(agent, name)` addresses the instance instead,
reading the same subtree-ownership relation `leakedServices` already
uses, inverted. It is read addressing for a caller holding the agent: a
host row that `inject`s a service cannot use it, because injection
resolves before any session exists — which is why `tools` and
`subagents` stay host-plane and this is not a way around that.

Tool presenters had the same shape and the same cure: `viewFor` looked
definitions up without a scope while the global layer is empty by
design, so every card degraded to the generic renderer. It now takes the
owning agent.

Cold resume through `agentFor()` mounted no preset at all, so every
generic entry point — prompt, models, commands — rebuilt a restarted
session on host tools and the deployment persona. It composes the
recorded preset now, as the other resume path already did.
This commit is contained in:
Yichen Jiang
2026-08-06 13:39:40 +08:00
parent fedb8a2702
commit c58cc23d45
6 changed files with 234 additions and 28 deletions
+23 -2
View File
@@ -13,11 +13,13 @@
import { Context, Service } from 'cordis'
import z from 'schemastery'
import { discoverPresets } from './discovery.ts'
import { mountPreset } from './mount.ts'
import { mountPreset, serviceForAgent } from './mount.ts'
import { UnknownPresetError, type AgentPreset, type Config } from './types.ts'
export { COMPOSITION_FILE, discoverPresets, scanRoot } from './discovery.ts'
export { inactiveRows, leakedServices, livePresetMounts, mountPreset, type PresetMount } from './mount.ts'
export {
inactiveRows, leakedServices, livePresetMounts, mountPreset, serviceForAgent, type PresetMount,
} from './mount.ts'
export { PresetMountError, UnknownPresetError } from './types.ts'
export type { AgentPreset, Config, PresetRoot, PresetTrust } from './types.ts'
@@ -95,6 +97,25 @@ export class AgentPresets extends Service {
await mountPreset(agentCtx, preset)
return preset
}
/**
* One agent's instance of a service its preset mounted.
*
* A preset publishes services behind `isolate` realms, which are invisible
* outside the group that declares them — including to the host. This is how a
* caller holding the agent reads one anyway: a request that is ABOUT a
* session but arrives from outside it, which is every browser RPC.
*
* Read addressing only. A host row that `inject`s a service cannot use this,
* because injection resolves before any session exists and has no agent to
* key by; such a service belongs on the host plane instead.
* @param agent - the agent whose composition to look inside.
* @param name - the service name as the preset's rows resolve it.
* @returns the agent's instance, or undefined when its preset mounts none.
*/
serviceFor<K extends string & keyof Context>(agent: { ctx: Context }, name: K): Context[K] | undefined {
return serviceForAgent(this.ctx, agent, name)
}
}
export default AgentPresets