Files
Codeman/test/cli-agent-inspection.test.ts
T
Aamer Akhter c78635a3d3 feat(agent): make coordination routes opt-in and address review
Gate inspect, stage and submit behind the synced agentCoordinationEnabled
setting (default OFF, App Settings > Cross-session features, zh-CN
strings). While off, all three routes answer 403 FORBIDDEN naming the
setting, checked before session lookup; the CLI verbs print it verbatim.

Remember the attributed Codex rollout per session and re-verify only its
head on later calls, rescanning only date directories not older than it
for a /new conflict. The first walk reads 1 KiB prefixes first, so a
store of 2,100+ unrelated rollouts still attributes.

Answer unexpected inspection errors with 422 OPERATION_FAILED and log
the session id and error code only.

Read the guarded composer glyph and footer from the CLI registry: codex
workDetect.promptGlyph and a new workDetect.composerFooterLine
capability, compiled through compileVersionRegex. Codex footer effort
words are one shared list with modelDetect.screenLine.

Fix the wiki API reference link, frame the versioning policy entry as a
case of the experimental rule, join the CLAUDE.md Agent CLI paragraph,
and update the changeset, skill reference and architecture notes.
2026-10-24 22:54:17 -04:00

128 lines
5.3 KiB
TypeScript

/** @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 });
}
});