/** @fileoverview Agent inspect/stage/submit transport, refusal and file boundaries. */ import { expect, it } from 'vitest'; import { mkdtemp, writeFile, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { agentInspect, agentStage, agentSubmit, registerAgentCommands, type AgentDeps, type RequestOptions, } from '../src/cli-agent.js'; import { Command } from 'commander'; const SELF = '11111111-1111-4111-8111-111111111111'; const OTHER = '22222222-2222-4222-8222-222222222222'; function deps() { const calls: RequestOptions[] = [], output: string[] = []; const deps: AgentDeps = { ctx: { apiUrl: 'http://localhost:1', selfId: SELF }, json: true, now: () => 1, io: { out: (line) => output.push(line), err: () => {} }, request: async (_ctx, options) => { calls.push(options); return { status: 200, text: '', json: { success: true, data: { evidence: 'hello', stageId: OTHER, sha256: 'a'.repeat(64) } }, }; }, }; return { deps, calls, output }; } it('inspect uses the ownership-checked versioned endpoint with exact bounds and cursor', async () => { const d = deps(); expect(await agentInspect(d.deps, { id: OTHER, maxBytes: 42, since: 'cursor' })).toBe(0); expect(d.calls[0]).toMatchObject({ method: 'GET', path: `/api/v1/sessions/${OTHER}/inspect`, query: { maxBytes: 42, since: 'cursor' }, }); expect(JSON.parse(d.output[0]).evidence).toBe('hello'); expect(await agentInspect(d.deps, { id: OTHER, maxBytes: 65537 })).toBe(4); expect(d.calls).toHaveLength(1); }); it('stage reads a bounded UTF8 file and never adds Enter; submit sends only receipt and hash', async () => { const dir = await mkdtemp(join(tmpdir(), 'agent-stage-cli-')); try { const path = join(dir, 'message'); await writeFile(path, 'hello'); const d = deps(); expect(await agentStage(d.deps, { id: OTHER, messageFile: path })).toBe(0); expect(d.calls[0]).toMatchObject({ method: 'POST', path: `/api/v1/sessions/${OTHER}/stage`, body: { message: 'hello' }, }); expect(await agentSubmit(d.deps, { id: OTHER, stageId: OTHER, confirmSha: 'a'.repeat(64) })).toBe(0); expect(d.calls[1].body).toEqual({ stageId: OTHER, confirmSha: 'a'.repeat(64) }); await writeFile(path, 'hello\n'); expect(await agentStage(d.deps, { id: OTHER, messageFile: path })).toBe(4); await writeFile(path, Buffer.from([0xff])); expect(await agentStage(d.deps, { id: OTHER, messageFile: path })).toBe(4); await writeFile(path, 'x'.repeat(8193)); expect(await agentStage(d.deps, { id: OTHER, messageFile: path })).toBe(4); expect(d.calls).toHaveLength(2); } finally { await rm(dir, { recursive: true, force: true }); } }); it('refuses self-target and malformed submission before transport', async () => { const d = deps(); expect(await agentStage(d.deps, { id: SELF, messageFile: '/nonexistent' })).toBe(4); expect(await agentSubmit(d.deps, { id: SELF, stageId: OTHER, confirmSha: 'a'.repeat(64) })).toBe(4); expect(await agentSubmit(d.deps, { id: OTHER, stageId: OTHER, confirmSha: 'bad' })).toBe(4); expect(d.calls).toHaveLength(0); }); it('registers required file and confirmation options and bounded inspect flags', () => { const program = new Command(); registerAgentCommands(program); const commands = program.commands.find((command) => command.name() === 'agent')!.commands; for (const name of ['inspect', 'stage', 'submit']) expect(commands.some((command) => command.name() === name)).toBe(true); expect( commands.find((command) => command.name() === 'stage')!.options.find((option) => option.long === '--message-file') ?.mandatory ).toBe(true); expect( commands.find((command) => command.name() === 'submit')!.options.find((option) => option.long === '--confirm-sha') ?.mandatory ).toBe(true); }); it('shows the opt-in 403 verbatim on inspect, stage and submit while the setting is off', async () => { const dir = await mkdtemp(join(tmpdir(), 'agent-stage-cli-')); try { const path = join(dir, 'message'); await writeFile(path, 'hello'); const message = 'Experimental agent coordination (inspect, stage, submit) is disabled. Turn on "Experimental agent coordination" in Settings (agentCoordinationEnabled) and save first.'; for (const json of [false, true]) { const lines: string[] = []; const refusing: AgentDeps = { ctx: { apiUrl: 'http://localhost:1', selfId: SELF }, json, now: () => 1, io: { out: (line) => lines.push(line), err: (line) => lines.push(line) }, request: async () => ({ status: 403, text: '', json: { success: false, error: message, errorCode: 'FORBIDDEN' }, }), }; expect(await agentInspect(refusing, { id: OTHER, maxBytes: 10 })).toBe(1); expect(await agentStage(refusing, { id: OTHER, messageFile: path })).toBe(1); expect(await agentSubmit(refusing, { id: OTHER, stageId: OTHER, confirmSha: 'a'.repeat(64) })).toBe(1); expect(lines).toHaveLength(3); for (const line of lines) { const shown = json ? (JSON.parse(line) as { success: boolean; error: string }).error : line; expect(shown).toContain(`FORBIDDEN: ${message} (HTTP 403)`); } } } finally { await rm(dir, { recursive: true, force: true }); } });