fix(tasks): validate owner identity and brand session ids

This commit is contained in:
Yichen Jiang
2026-07-12 17:10:02 +08:00
parent 2688df99cd
commit 8f5ea04c3f
11 changed files with 77 additions and 29 deletions

View File

@@ -1,4 +1,4 @@
import { describe, expect, it, vi } from 'vitest'
import { describe, expect, expectTypeOf, it, vi } from 'vitest'
import { Context } from 'cordis'
import { Session, SessionId } from '@deepseek-ai/dsh-session'
import AgentRegistry, { AgentId } from '@deepseek-ai/dsh-agent'
@@ -6,12 +6,12 @@ import type { Agent } from '@deepseek-ai/dsh-agent'
import TaskService, { TaskId } from '@deepseek-ai/dsh-tasks'
import type { TaskHooks, TaskOutcome, TaskSnapshot, TaskStart } from '@deepseek-ai/dsh-tasks'
function stubAgent(rawId: string): Agent {
function stubAgent(rawId: string, rawSessionId = `${rawId}-session`): Agent {
const id = AgentId(rawId)
return {
id,
options: {},
session: new Session(SessionId(`${id}-session`)),
session: new Session(SessionId(rawSessionId)),
status: 'idle',
send() {},
steer() {},
@@ -48,6 +48,10 @@ async function harness() {
const tick = () => new Promise<void>(r => setTimeout(r, 0))
describe('TaskService.start', () => {
it('preserves the SessionId brand on public owner snapshots', () => {
expectTypeOf<TaskSnapshot['ownerSession']>().toEqualTypeOf<SessionId | undefined>()
})
it('refuses to register while no control surface is attached', async () => {
const ctx = new Context()
await ctx.plugin(TaskService)
@@ -365,9 +369,10 @@ describe('TaskService owner isolation', () => {
const ctx = await harness()
const ghost = stubAgent('ghost') // never registered in ctx.agents
// onCleanup rejects the unregistered agent BEFORE any registry mutation.
// Exact-instance preflight rejects the unregistered agent BEFORE any
// registry mutation or owner-cleanup attachment.
expect(() => ctx.tasks.start(producer({ owner: ghost }).spec))
.toThrow('is not registered')
.toThrow('is not the registered agent instance')
expect(ctx.tasks.list(ghost)).toEqual([])
// Once the agent actually exists, the same owner gets a WORKING cleanup —
@@ -389,6 +394,30 @@ describe('TaskService owner isolation', () => {
expect(cancels).toEqual(['owner disposed'])
expect(ctx.tasks.list(ghost)).toEqual([])
})
it('rejects a stale owner instance after another agent reuses its id', async () => {
const ctx = await harness()
const staleOwner = stubAgent('owner', 'stale-session')
const unregisterStale = ctx.agents.register(staleOwner)
unregisterStale()
const currentOwner = stubAgent('owner', 'current-session')
ctx.agents.register(currentOwner)
const current = producer({ owner: currentOwner })
ctx.tasks.start(current.spec) // Attach the current owner's cleanup first.
const stale = producer({ owner: staleOwner })
const staleRun = vi.fn(() => stale.spec.run())
expect(() => ctx.tasks.start({ ...stale.spec, run: staleRun }))
.toThrow('is not the registered agent instance')
expect(staleRun).not.toHaveBeenCalled()
expect(ctx.tasks.list(staleOwner)).toEqual([])
expect(ctx.tasks.list(currentOwner)).toHaveLength(1)
current.settle({ status: 'completed' })
await tick()
await ctx.agents.drainCleanups(currentOwner.id)
})
})
describe('TaskService owner cleanup', () => {