fix(core): enforce agent-scoped ownership boundaries
This commit is contained in:
@@ -390,10 +390,12 @@ export function apply(ctx: Context, config: Config = {}): void {
|
||||
'agent/session-prefix': args => args[0],
|
||||
'agent/step-result': args => args[0],
|
||||
'agent/turn-continuation': args => args[0],
|
||||
'agent/turn-stop': args => args[0],
|
||||
'agent/error': args => args[0],
|
||||
'tools/pre-execute': args => (args[0] as ToolExecution).agent,
|
||||
'tools/execute': args => (args[0] as ToolExecution).agent,
|
||||
'tools/post-execute': args => (args[0] as ToolExecution).agent,
|
||||
'tools/result': args => (args[0] as ToolExecution).agent,
|
||||
'system-prompt/assemble': args => (args[1] as AssembleContext).scope,
|
||||
'session/created': null,
|
||||
'session/event': null,
|
||||
@@ -429,11 +431,11 @@ export function apply(ctx: Context, config: Config = {}): void {
|
||||
|
||||
// --- Setup-drives invariant ---------------------------------------------
|
||||
//
|
||||
// CreateAgentOptions.setup REGISTERS the agent's scoped world; it must not
|
||||
// DRIVE the agent — an inject() there opens a turn before
|
||||
// `agent/session-start`, inverting the "session-start fires before the
|
||||
// first turn" contract every bridge keys on. A turn/start appended to a
|
||||
// live agent's session before its agent/session-start fired is therefore a
|
||||
// CreateAgentOptions.setup COMPOSES the agent's scoped world; it must not
|
||||
// DRIVE the agent. ReactLoopAgent rejects every driving verb structurally
|
||||
// until rollback-covered publication reaches the session-start boundary; this event-level invariant remains the
|
||||
// cross-implementation backstop for alternate Agent implementations and raw
|
||||
// session writes. A turn/start appended before agent/session-start is a
|
||||
// creation-time misuse, reported at the appending call site. Sessions of
|
||||
// agents that exist BEFORE this plugin applies are marked started (their
|
||||
// ordering is unknowable after the fact — never a false positive on HMR).
|
||||
@@ -450,7 +452,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
||||
if (owner === undefined) return
|
||||
throw new InvariantError(
|
||||
`agent "${owner.id}": a turn opened before agent/session-start fired — `
|
||||
+ 'CreateAgentOptions.setup registers the scoped world, it must not drive the agent '
|
||||
+ 'CreateAgentOptions.setup composes the scoped world, it must not drive the agent '
|
||||
+ '(send/steer/inject belong after creation returns)')
|
||||
})
|
||||
|
||||
|
||||
@@ -828,11 +828,15 @@ describe('scoped-dispatch invariants', () => {
|
||||
['agent/pre-step', [agent, 1, 1, '', new AbortController().signal]],
|
||||
['agent/prompt-submit', [agent, [], { kind: 'user' }, () => Promise.resolve({ kind: 'allow' })]],
|
||||
['agent/request', [agent, 1, 1, { model: 'm' }, () => Promise.resolve({ model: 'm' })]],
|
||||
['agent/session-prefix', [agent, [], new AbortController().signal, () => Promise.resolve([])]],
|
||||
['agent/step-result', [agent, 1, 1, { role: 'assistant', content: [] }, () => Promise.resolve({ role: 'assistant', content: [] })]],
|
||||
['agent/turn-continuation', [agent, 1, { action: 'stop' }, () => Promise.resolve({ action: 'stop' })]],
|
||||
['agent/turn-stop', [agent, 1]],
|
||||
['agent/error', [agent, 1, 0, new Error('x')]],
|
||||
['tools/pre-execute', [{ callId: 'c', name: 't', arguments: {}, agent }, () => Promise.resolve({ kind: 'allow' })]],
|
||||
['tools/execute', [{ callId: 'c', name: 't', arguments: {}, agent }, () => Promise.resolve({ callId: 'c', content: [], isError: false })]],
|
||||
['tools/post-execute', [{ callId: 'c', name: 't', arguments: {}, agent }, { callId: 'c', content: [], isError: false }, () => Promise.resolve({ kind: 'accept' })]],
|
||||
['tools/result', [{ callId: 'c', name: 't', arguments: {}, agent }, { callId: 'c', content: [], isError: false }]],
|
||||
]
|
||||
for (const [event, args] of rows) {
|
||||
const subject = event.startsWith('tools/') ? agent : agent
|
||||
@@ -870,7 +874,7 @@ describe('scoped-dispatch invariants', () => {
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it('rejects a turn opened before agent/session-start (setup drives the agent)', async () => {
|
||||
it('backstops alternate agents that open a turn before agent/session-start', async () => {
|
||||
const ctx = await scopedCtx()
|
||||
// A live agent whose session is in the store but whose session-start has
|
||||
// not fired: appending turn/start must throw the teaching error.
|
||||
|
||||
Reference in New Issue
Block a user