fix(core): enforce agent-scoped ownership boundaries

This commit is contained in:
Tianyi Cui
2026-07-11 22:55:26 +08:00
parent 850796bb35
commit 3263dab822
62 changed files with 3982 additions and 857 deletions

View File

@@ -119,37 +119,30 @@ describe('bash tool through the agent loop', () => {
it('background: start → poll → completion notice lands as context/message', async () => {
const adapter = new MockAdapter([
toolCallResponse('call-1', 'bash', { command: 'echo bg-ok', description: 'test command', run_in_background: true }),
toolCallResponse('call-2', 'bash_output', {}, undefined),
// Each harness owns a fresh BashLocal service, whose first task id is
// deterministically bash-1. Keep the scripted call faithful to what the
// model sent; tool arguments are immutable once execution policy begins.
toolCallResponse('call-2', 'bash_output', { task_id: 'bash-1' }, undefined),
textResponse('Background task finished.'),
])
// The second tool call needs the REAL task id from the first result;
// a tools/pre-execute listener rewrites the scripted arguments. (This uses
// the low-level capability to mutate `exec` before dispatch — the
// unadvertised mechanism behind a future first-class input-rewrite decision;
// here it is a test shim to thread the generated id, not a product feature.)
let taskId = ''
const ctx = await harness(adapter)
const agent = ctx.agentLoop.create(AgentId('it-bg'), { model: 'mock' })
// Intercept the first tool result to capture the generated task id, then
// rewrite the second scripted call's arguments to use it.
// Capture the generated id so the deterministic fixture is checked against
// the real executor instead of silently assuming it.
ctx.on('session/event', (_session, event) => {
if (event.type === 'tool/result' && taskId === '') {
const match = /task (bash-\d+)/.exec(resultText(event))
if (match) taskId = match[1]!
}
})
ctx.on('tools/pre-execute', async (exec, next) => {
if (exec.name === 'bash_output') {
exec.arguments = { task_id: taskId }
}
return next()
})
agent.send([{ type: 'text', text: 'run echo bg-ok in the background' }])
await waitForIdle(ctx, agent)
expect(taskId).toBe('bash-1')
// Wait for the background task itself (completion may race turn end).
const task = ctx.bash.get(BashTaskId(taskId))
if (!task) throw new Error(`task ${taskId} not registered`)

View File

@@ -242,7 +242,6 @@ describe('bash tool', () => {
[{ command: ' ', description: 'd' }, /invalid command/],
[{ command: 'x', description: ' ' }, /invalid description/],
[{ command: 'x', description: 'd', timeoutMs: -1 }, /invalid timeoutMs/],
[{ command: 'x', description: 'd', timeoutMs: Number.NaN }, /invalid timeoutMs/],
])('rejects value-invalid args %j', async (args, pattern) => {
const ctx = await setup()
const result = await call(ctx, 'bash', args)
@@ -250,6 +249,15 @@ describe('bash tool', () => {
expect(text(result)).toMatch(pattern)
})
it('rejects a non-JSON numeric argument before tool-specific validation', async () => {
const ctx = await setup()
const result = await call(ctx, 'bash', {
command: 'x', description: 'd', timeoutMs: Number.NaN,
})
expect(result.isError).toBe(true)
expect(text(result)).toContain('tool execution arguments must be losslessly JSON-serializable')
})
it('registers all three schemas in the system prompt assembly', async () => {
const ctx = await setup()
const names = ctx.tools.schemas().map(schema => schema.name)