Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions scripts/validate-live-dogfood-evidence.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,34 @@ function evidence(manifest = validManifest(), doctor: unknown = DOCTOR): string
)}\n\`\`\`\n`;
}

const ADAPTER = '/tmp/dogfood/bin/run-musl-dogfood.sh';

/**
* Reshape every stage observation the way an execution adapter records it:
* the adapter is the executable, the candidate binary moves into adapterArgv,
* and argv stays the candidate's OWN arguments. Only linux-x64-musl runs this
* way, which is why the pre-fix binary-prefixed argv escaped every other leg.
*/
function adapterShaped(manifest: Record<string, any>): void {
for (const stage of manifest.lifecycle.stages) {
for (const command of stage.observation.commands) {
command.adapterArgv = [command.candidateBinary, ...command.argv];
command.executable = ADAPTER;
command.executableSha256 = HEX('9');
}
reprojectStage(stage);
}
}

function reprojectStage(stage: Record<string, any>): void {
stage.command = stage.observation.commands
.map((entry: Record<string, any>) => [entry.executable, ...(entry.adapterArgv ?? entry.argv)].join(' '))
.join(' && ');
stage.observationSha256 = createHash('sha256')
.update(`${JSON.stringify(stage.observation, null, 2)}\n`)
.digest('hex');
}

function errorsAfter(mutator: (manifest: Record<string, any>) => void): string[] {
const manifest = validManifest();
mutator(manifest);
Expand All @@ -265,6 +293,39 @@ describe('validate-live-dogfood-evidence schema v2', () => {
).toContain('must reject retired MCP evidence');
});

test('accepts adapter-shaped observations and rejects binary-prefixed argv', () => {
expect(errorsAfter(adapterShaped)).toEqual([]);
// The linux-x64-musl regression: argv carried [<candidate binary>, 'task',
// 'list', '--json'], so the standalone assertion could never match.
const prefixed = errorsAfter((manifest) => {
adapterShaped(manifest);
for (const stage of manifest.lifecycle.stages) {
for (const command of stage.observation.commands) command.argv = [...command.adapterArgv];
reprojectStage(stage);
}
});
for (const index of [5, 6, 7]) {
expect(prefixed).toContain(
`stages[${index}].standalone must contain standalone task list --json and board --json observations`,
);
}
expect(
errorsAfter((manifest) => {
adapterShaped(manifest);
manifest.lifecycle.stages[0].observation.commands[0].adapterArgv = ['ok', 7];
reprojectStage(manifest.lifecycle.stages[0]);
}).join('\n'),
).toContain('commands[0].adapterArgv must be a string array');
expect(
errorsAfter((manifest) => {
adapterShaped(manifest);
const stage = manifest.lifecycle.stages.find((entry: { id: string }) => entry.id === 'new-thread-sentinel');
stage.observation.commands[0].adapterArgv.push('genie_board@mcp');
reprojectStage(stage);
}).join('\n'),
).toContain('must reject retired MCP evidence');
});

test('rejects obsolete flat integrationSummary.state', () => {
const flat = { ok: true, checks: [{}], integrationSummary: { schemaVersion: 1, state: 'current' } };
expect(validateLiveDogfoodEvidence(evidence(validManifest(), flat)).join('\n')).toContain(
Expand Down
20 changes: 18 additions & 2 deletions scripts/validate-live-dogfood-evidence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,12 @@ const STAGE_EXPECTATIONS: Record<(typeof REQUIRED_STAGE_IDS)[number], StageExpec
},
};

/** The real spawned vector: the adapter's argv when one wrapped the candidate. */
function spawnArgv(command: JsonRecord): unknown[] {
if (Array.isArray(command.adapterArgv)) return command.adapterArgv;
return Array.isArray(command.argv) ? command.argv : [];
}

function validateCapturedCommand(
errors: string[],
label: string,
Expand All @@ -506,6 +512,15 @@ function validateCapturedCommand(
if (!Array.isArray(command.argv) || command.argv.some((arg) => typeof arg !== 'string')) {
errors.push(`${label}.argv must be a string array`);
}
// Optional: present only when an execution adapter (linux-x64-musl Alpine)
// spawned the candidate. `argv` stays the candidate's own arguments on every
// platform; `adapterArgv` records the real vector handed to `executable`.
if (
command.adapterArgv !== undefined &&
(!Array.isArray(command.adapterArgv) || command.adapterArgv.some((arg) => typeof arg !== 'string'))
) {
errors.push(`${label}.adapterArgv must be a string array`);
Comment on lines +518 to +522

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Bind adapterArgv to the candidate invocation

For adapter-backed evidence, this only verifies that adapterArgv is a string array; it never requires it to equal [candidateBinary, ...argv]. Consequently, evidence can claim standalone task list --json and board --json in argv while the recorded spawn vector invokes different arguments or even a different binary, and the release validator still accepts the supposed standalone proof. Enforce the documented relationship before using argv as candidate-execution evidence.

AGENTS.md reference: AGENTS.md:L55-L57

Useful? React with 👍 / 👎.

}
if (typeof command.pid !== 'number' || !Number.isInteger(command.pid) || command.pid <= 0) {
errors.push(`${label}.pid must be a positive integer`);
}
Expand All @@ -518,7 +533,7 @@ function validateCapturedCommand(
if (typeof command.stderr !== 'string') errors.push(`${label}.stderr must be a string`);
return {
record: command,
summary: [String(command.executable), ...(Array.isArray(command.argv) ? command.argv.map(String) : [])].join(' '),
summary: [String(command.executable), ...spawnArgv(command).map(String)].join(' '),
exit: typeof command.exit === 'number' && Number.isInteger(command.exit) ? command.exit : null,
candidateSha256: command.candidateBinarySha256,
};
Expand All @@ -544,7 +559,8 @@ function validateStandaloneObservation(
const hasBoard = argv.some((args) => Array.isArray(args) && args.join(' ') === 'board --json');
if (!hasTask || !hasBoard)
errors.push(`${label} must contain standalone task list --json and board --json observations`);
if (argv.some((args) => Array.isArray(args) && args.some((arg) => String(arg).toLowerCase().includes('mcp')))) {
const vectors = commands.flatMap((value) => [value.record.argv, value.record.adapterArgv]);
if (vectors.some((args) => Array.isArray(args) && args.some((arg) => String(arg).toLowerCase().includes('mcp')))) {
Comment on lines +562 to +563

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exclude the candidate path from the retired-MCP scan

On adapter-backed runs, adapterArgv[0] is the absolute candidate-binary path rather than a candidate argument. If TMPDIR or an injected dogfood root contains the substring mcp (for example /tmp/mcp-release), every otherwise valid musl standalone observation is rejected as retired MCP evidence. Scan only the actual candidate arguments, or at least omit the adapter vector's first element.

AGENTS.md reference: AGENTS.md:L55-L57

Useful? React with 👍 / 👎.

errors.push(`${label} must reject retired MCP evidence`);
}
}
Expand Down
75 changes: 75 additions & 0 deletions tests/support/codex-dogfood-harness.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { afterEach, describe, expect, test } from 'bun:test';
import { chmodSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { captureCommand, resolveInvocation } from './codex-dogfood-harness.ts';

const roots: string[] = [];

function fixture(): { root: string; binary: string; adapter: string } {
const root = realpathSync(mkdtempSync(join(tmpdir(), 'dogfood-harness-')));
roots.push(root);
// Stands in for the authenticated candidate: it echoes the exact argv it was
// handed, so the evidence record can be checked against reality.
const binary = join(root, 'genie');
writeFileSync(binary, '#!/usr/bin/env bash\nprintf \'%s\\n\' "$*"\n');
chmodSync(binary, 0o755);
// Stands in for scripts/run-musl-dogfood.sh: takes the candidate binary as
// its first positional argument and execs it with the remaining argv.
const adapter = join(root, 'adapter.sh');
writeFileSync(adapter, '#!/usr/bin/env bash\nset -eu\ncandidate=$1\nshift\nexec "$candidate" "$@"\n');
chmodSync(adapter, 0o755);
return { root, binary, adapter };
}

afterEach(() => {
while (roots.length > 0) rmSync(roots.pop() as string, { recursive: true, force: true });
});

describe('candidate invocation evidence', () => {
test('resolveInvocation keeps argv on the candidate and names the adapter vector separately', () => {
expect(resolveInvocation({ binary: '/bin/genie', args: ['board', '--json'] })).toEqual({
command: '/bin/genie',
argv: ['board', '--json'],
spawnArgv: ['board', '--json'],
});
expect(
resolveInvocation({ binary: '/bin/genie', args: ['board', '--json'], executionAdapter: '/bin/adapter' }),
).toEqual({
command: '/bin/adapter',
argv: ['board', '--json'],
adapterArgv: ['/bin/genie', 'board', '--json'],
spawnArgv: ['/bin/genie', 'board', '--json'],
});
});

test('records the candidate argv unchanged with and without an execution adapter', () => {
const { root, binary, adapter } = fixture();
const args = ['task', 'list', '--json'];
const direct = captureCommand({ root, binary, args, cwd: root, env: { PATH: process.env.PATH ?? '' } });
expect(direct.exit).toBe(0);
expect(direct.executable).toBe(binary);
expect(direct.candidateBinary).toBe(binary);
expect(direct.argv).toEqual(args);
expect(direct.adapterArgv).toBeUndefined();
expect(direct.stdout.trim()).toBe('task list --json');

const adapted = captureCommand({
root,
binary,
args,
cwd: root,
env: { PATH: process.env.PATH ?? '' },
executionAdapter: adapter,
});
expect(adapted.exit).toBe(0);
// The evidence argv is the candidate's own arguments on BOTH legs: a
// linux-x64-musl run must be byte-comparable with every other platform.
expect(adapted.argv).toEqual(args);
expect(adapted.adapterArgv).toEqual([binary, ...args]);
expect(adapted.executable).toBe(adapter);
expect(adapted.candidateBinary).toBe(binary);
// The candidate really did receive exactly `args` through the adapter.
expect(adapted.stdout.trim()).toBe('task list --json');
});
});
60 changes: 47 additions & 13 deletions tests/support/codex-dogfood-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,18 @@ interface CapturedCommand {
executableSha256: string;
candidateBinary: string | null;
candidateBinarySha256: string | null;
/**
* The CANDIDATE's own arguments, never the adapter spawn vector. Evidence
* consumers assert on what the genie binary was asked to do, so this stays
* identical on every platform whether or not an execution adapter wraps it.
*/
argv: string[];
/**
* Present only when an execution adapter ran the candidate (linux-x64-musl
* goes through scripts/run-musl-dogfood.sh): the real argv handed to
* `executable`, i.e. [candidateBinary, ...argv].
*/
adapterArgv?: string[];
pid: number;
requestedCwd: string;
cwdIdentity: string;
Expand Down Expand Up @@ -776,7 +787,26 @@ function verifyLegacyReleaseProvenance(input: {
}
}

function captureCommand(input: {
/**
* Split one candidate invocation into the evidence argv (always the
* candidate's own arguments) and the real spawn vector. Without an execution
* adapter the two coincide; with one, the adapter takes the candidate binary
* as its first positional argument.
*/
export function resolveInvocation(input: { binary: string; args: string[]; executionAdapter?: string }): {
command: string;
argv: string[];
adapterArgv?: string[];
spawnArgv: string[];
} {
if (input.executionAdapter === undefined) {
return { command: input.binary, argv: input.args, spawnArgv: input.args };
}
const adapterArgv = [input.binary, ...input.args];
return { command: input.executionAdapter, argv: input.args, adapterArgv, spawnArgv: adapterArgv };
}

export function captureCommand(input: {
root: string;
binary: string;
args: string[];
Expand All @@ -786,13 +816,12 @@ function captureCommand(input: {
stdin?: Uint8Array;
timeoutMs?: number;
}): CapturedCommand {
const command = input.executionAdapter ?? input.binary;
const argv = input.executionAdapter === undefined ? input.args : [input.binary, ...input.args];
const { command, argv, adapterArgv, spawnArgv } = resolveInvocation(input);
const env =
input.executionAdapter === undefined
? input.env
: { ...input.env, DOGFOOD_ROOT: input.root, DOGFOOD_ADAPTER_CWD: realpathSync(input.cwd) };
const result = Bun.spawnSync([command, ...argv], {
const result = Bun.spawnSync([command, ...spawnArgv], {
cwd: input.cwd,
env,
stdin: input.stdin,
Expand All @@ -801,7 +830,7 @@ function captureCommand(input: {
timeout: input.timeoutMs ?? 120_000,
});
if (result.exitedDueToTimeout === true) {
throw new Error(`bounded candidate command timed out: ${[command, ...argv].join(' ')}`);
throw new Error(`bounded candidate command timed out: ${[command, ...spawnArgv].join(' ')}`);
}
const cwdStat = statSync(realpathSync(input.cwd));
return {
Expand All @@ -810,6 +839,7 @@ function captureCommand(input: {
candidateBinary: realpathSync(input.binary),
candidateBinarySha256: sha256File(realpathSync(input.binary)),
argv,
...(adapterArgv === undefined ? {} : { adapterArgv }),
pid: result.pid,
requestedCwd: realpathSync(input.cwd),
cwdIdentity: `${cwdStat.dev}:${cwdStat.ino}`,
Expand All @@ -826,11 +856,12 @@ function capturePtySetup(input: {
env: Record<string, string>;
executionAdapter?: string;
}): CapturedCommand {
const command = input.executionAdapter ?? input.binary;
const commandArgs =
input.executionAdapter === undefined
? [input.binary, 'setup', '--codex']
: [input.executionAdapter, input.binary, 'setup', '--codex'];
const { command, argv, adapterArgv, spawnArgv } = resolveInvocation({
binary: input.binary,
args: ['setup', '--codex'],
executionAdapter: input.executionAdapter,
});
const commandArgs = [command, ...spawnArgv];
const env = {
...input.env,
CI: '',
Expand Down Expand Up @@ -879,7 +910,8 @@ function capturePtySetup(input: {
executableSha256: sha256File(realpathSync(command)),
candidateBinary: realpathSync(input.binary),
candidateBinarySha256: sha256File(realpathSync(input.binary)),
argv: commandArgs.slice(1),
argv,
...(adapterArgv === undefined ? {} : { adapterArgv }),
pid: result.pid,
requestedCwd: realpathSync(input.cwd),
cwdIdentity: `${cwdStat.dev}:${cwdStat.ino}`,
Expand All @@ -898,7 +930,7 @@ function requireExit(observation: CapturedCommand, expected: number, label: stri
throw new Error(
`${label} exited ${observation.exit}, expected ${expected}\n` +
` exe: ${observation.executable}\n` +
` argv: ${JSON.stringify(observation.argv)}\n` +
` argv: ${JSON.stringify(observation.adapterArgv ?? observation.argv)}\n` +
` stderr[${observation.stderr.length}B]: ${observation.stderr.trim() || '<empty>'}\n` +
` stdout[${observation.stdout.length}B]: ${observation.stdout.trim() || '<empty>'}`,
);
Expand Down Expand Up @@ -1119,7 +1151,9 @@ function stage(
const observation = { schemaVersion: 1 as const, commands };
return {
id,
command: commands.map((command) => [command.executable, ...command.argv].join(' ')).join(' && '),
command: commands
.map((command) => [command.executable, ...(command.adapterArgv ?? command.argv)].join(' '))
.join(' && '),
...projection,
observationPath: `observations/${id}.json`,
observationSha256: sha256Bytes(Buffer.from(serializeStageObservation(observation))),
Expand Down