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

View File

@@ -12,6 +12,13 @@ import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { beforeEach, describe, expect, it } from 'vitest'
import AgentPresets, { leakedServices, livePresetMounts } from '@deepseek-ai/dsh-agent-presets'
declare module 'cordis' {
interface Context {
/** Published by the `isolated` fixture preset behind an entry-local realm. */
fixtureIsolatedSvc: { label: string }
}
}
const FIXTURES = join(dirname(fileURLToPath(import.meta.url)), 'fixtures')
const ROOTS = [
{ path: join(FIXTURES, 'system'), trust: 'system' as const },
@@ -155,6 +162,29 @@ describe('rejecting a composition that cannot be used', () => {
expect(rootResolves(ctx, 'fixtureIsolatedSvc')).toBe(false)
})
it('addresses one agent\'s instance of a realm-private service', async () => {
const first = await agentOn(ctx, 'sess-reach-a', 'isolated')
const second = await agentOn(ctx, 'sess-reach-b', 'isolated')
// The realm keeps the service out of every host context — that is what
// makes it per session — so a caller holding the agent is the only way a
// request from OUTSIDE the session can read the instance it is about.
expect(rootResolves(ctx, 'fixtureIsolatedSvc')).toBe(false)
const mine = ctx.agentPresets.serviceFor(first, 'fixtureIsolatedSvc')
const theirs = ctx.agentPresets.serviceFor(second, 'fixtureIsolatedSvc')
expect(mine).toBeDefined()
expect(theirs).toBeDefined()
// Each agent gets ITS own: the addressing is per subtree, not a lookup
// that happens to find the first match.
expect(mine).not.toBe(theirs)
})
it('answers undefined for a service the agent\'s preset does not mount', async () => {
const agent = await agentOn(ctx, 'sess-reach-none', 'standard')
expect(ctx.agentPresets.serviceFor(agent, 'fixtureIsolatedSvc')).toBeUndefined()
})
it('reports the known ids when a preset is unknown', async () => {
await expect(ctx.agentPresets.resolve('nope'))
.rejects.toThrow(/preset "nope" not found \(available: .*standard/)