Files
waggle-os/scripts/verify-codex-tool-denial.mjs
Oleg Maslov b20b138fe4 moving
2026-09-02 10:14:22 +02:00

2883 lines
105 KiB
JavaScript

#!/usr/bin/env node
import { spawn, spawnSync } from 'node:child_process';
import { createHash, randomBytes } from 'node:crypto';
import { createServer } from 'node:http';
import {
existsSync,
lstatSync,
readFileSync,
realpathSync,
} from 'node:fs';
import {
mkdir,
rm,
stat,
writeFile,
} from 'node:fs/promises';
import { homedir, tmpdir } from 'node:os';
import {
basename,
dirname,
isAbsolute,
join,
relative,
resolve,
win32,
} from 'node:path';
import { fileURLToPath } from 'node:url';
const SCRIPT_PATH = fileURLToPath(import.meta.url);
const MAX_CAPTURE_BYTES = 2 * 1024 * 1024;
const MAX_REQUEST_BYTES = 2 * 1024 * 1024;
const TURN_TIMEOUT_MS = 180_000;
const PAID_ACK = 'I_ACKNOWLEDGE_1_CODEX_OFFICIAL_AUTH_CALL';
const DENIAL_REASON = 'Waggle official-auth canary denies every model tool call.';
const WAGGLE_HOOK_EVENTS = [
'preCompact',
'sessionStart',
'stop',
'userPromptSubmit',
];
const DISABLED_FEATURES = [
'shell_tool',
'unified_exec',
'apps',
'browser_use',
'browser_use_external',
'browser_use_full_cdp_access',
'computer_use',
'image_generation',
'in_app_browser',
'multi_agent',
'multi_agent_v2',
'goals',
'skill_search',
'tool_suggest',
'workspace_dependencies',
'skill_mcp_dependency_install',
'plugins',
'plugin_sharing',
'remote_plugin',
'mentions_v2',
];
const CONFIG_OVERRIDES = [
'approval_policy="never"',
'sandbox_mode="read-only"',
'web_search="disabled"',
'orchestrator.skills.enabled=false',
'orchestrator.mcp.enabled=false',
'tools.update_plan.enabled=false',
'tools.experimental_request_user_input.enabled=false',
'project_doc_max_bytes=0',
'history.persistence="none"',
'analytics.enabled=false',
];
const DENIAL_RESPONSE = {
hookSpecificOutput: {
hookEventName: 'PreToolUse',
permissionDecision: 'deny',
permissionDecisionReason: DENIAL_REASON,
},
};
const CANARY_STAGES = new Set([
'app-server-spawn',
'initialize',
'hooks-list',
'pre-turn-boundary',
'thread-start',
'turn-start',
'turn-completed',
'event-audit',
'app-server-close',
'post-turn-invariants',
]);
const EVENT_AUDIT_FAILURE_INVARIANTS = new Set([
'failure-notification',
'wrong-scope',
'model-reroute',
'unknown-notification-method',
'unknown-item-type',
'forbidden-tool-item',
'agent-message-cardinality',
]);
const EVENT_AUDIT_FAILURE_KEYS = [
'invariant',
'notificationCount',
'failureEventsObserved',
'wrongScopeEventsObserved',
'rerouteEventsObserved',
'unknownNotificationEventsObserved',
'unknownItemEventsObserved',
'toolEventsObserved',
'notificationGraphSha256',
'rejectedMethodSha256',
'rejectedItemTypeSha256',
];
const FAIL_CLOSED_PROJECT_TRUST_WARNING =
'Project-local config, hooks, and exec policies are disabled in the following folders until the project is trusted, but skills still load.';
const FAILURE_NOTIFICATION_METHODS = new Set([
'error',
'warning',
'guardianWarning',
'configWarning',
'deprecationNotice',
]);
const ALLOWED_NOTIFICATION_METHODS = new Set([
'account/updated',
'account/rateLimits/updated',
'hook/completed',
'hook/started',
'item/agentMessage/delta',
'item/completed',
'item/plan/delta',
'item/reasoning/summaryPartAdded',
'item/reasoning/summaryTextDelta',
'item/reasoning/textDelta',
'item/started',
'item/updated',
'mcpServer/startupStatus/updated',
'model/safetyBuffering/updated',
'model/verification',
'remoteControl/status/changed',
'serverRequest/resolved',
'thread/name/updated',
'thread/settings/updated',
'thread/started',
'thread/status/changed',
'thread/tokenUsage/updated',
'turn/completed',
'turn/diff/updated',
'turn/moderationMetadata',
'turn/plan/updated',
'turn/started',
]);
const ALLOWED_ITEM_TYPES = new Set([
'agentMessage',
'reasoning',
'userMessage',
]);
const TERMINAL_TURN_FAILURE_STATUSES = new Set(['failed', 'interrupted']);
const SIMPLE_CODEX_ERROR_CODES = new Set([
'badRequest',
'contextWindowExceeded',
'cyberPolicy',
'internalServerError',
'other',
'sandboxError',
'serverOverloaded',
'sessionBudgetExceeded',
'threadRollbackFailed',
'unauthorized',
'usageLimitExceeded',
]);
const HTTP_CODEX_ERROR_CODES = new Set([
'httpConnectionFailed',
'responseStreamConnectionFailed',
'responseStreamDisconnected',
'responseTooManyFailedAttempts',
]);
function assert(condition, message) {
if (!condition) throw new Error(message);
}
function sha256(value) {
return createHash('sha256').update(value).digest('hex');
}
function sha256File(path) {
return sha256(readFileSync(path));
}
function canonicalPath(path) {
return realpathSync.native(path).toLowerCase();
}
function stableValue(value) {
if (Array.isArray(value)) return value.map(stableValue);
if (value && typeof value === 'object') {
return Object.fromEntries(
Object.entries(value)
.sort(([left], [right]) => left.localeCompare(right))
.map(([key, child]) => [key, stableValue(child)]),
);
}
return value;
}
function stableJson(value) {
return JSON.stringify(stableValue(value));
}
function normalizeCodexErrorInfo(value) {
if (value === null || value === undefined) {
return { code: null, httpStatusCode: null };
}
if (typeof value === 'string') {
return SIMPLE_CODEX_ERROR_CODES.has(value)
? { code: value, httpStatusCode: null }
: { code: null, httpStatusCode: null };
}
if (typeof value !== 'object' || Array.isArray(value)) {
return { code: null, httpStatusCode: null };
}
const keys = Object.keys(value);
if (keys.length !== 1) return { code: null, httpStatusCode: null };
const code = keys[0];
const details = value[code];
if (code === 'activeTurnNotSteerable') {
return details !== null
&& typeof details === 'object'
&& !Array.isArray(details)
&& Object.keys(details).length === 1
&& ['review', 'compact'].includes(details.turnKind)
? { code, httpStatusCode: null }
: { code: null, httpStatusCode: null };
}
if (!HTTP_CODEX_ERROR_CODES.has(code)
|| details === null
|| typeof details !== 'object'
|| Array.isArray(details)
|| !Object.keys(details).every((key) => key === 'httpStatusCode')) {
return { code: null, httpStatusCode: null };
}
const httpStatusCode = details.httpStatusCode ?? null;
if (httpStatusCode !== null
&& (!Number.isInteger(httpStatusCode) || httpStatusCode < 0 || httpStatusCode > 65_535)) {
return { code: null, httpStatusCode: null };
}
return { code, httpStatusCode };
}
function sanitizeTurnFailure(turn, errorNotification = null) {
const status = TERMINAL_TURN_FAILURE_STATUSES.has(turn?.status) ? turn.status : null;
const notificationParams = errorNotification?.message?.params ?? null;
const turnInfo = normalizeCodexErrorInfo(turn?.error?.codexErrorInfo);
const notificationInfo = errorNotification?.message?.method === 'error'
? normalizeCodexErrorInfo(notificationParams?.error?.codexErrorInfo)
: { code: null, httpStatusCode: null };
// The final turn is authoritative. A disagreeing or malformed notification is
// reduced to the already-sanitized turn result rather than becoming a new error.
const selected = turnInfo.code === null ? notificationInfo : turnInfo;
const willRetry = notificationParams?.willRetry ?? null;
return {
status,
code: selected.code,
httpStatusCode: selected.httpStatusCode,
willRetry: typeof willRetry === 'boolean' ? willRetry : null,
};
}
function sanitizeProtocolCode(value) {
return Number.isInteger(value)
&& value >= -2_147_483_648
&& value <= 2_147_483_647
? value
: null;
}
function sanitizeCanaryStage(value) {
return CANARY_STAGES.has(value) ? value : null;
}
function groupSha256(values) {
const counts = new Map();
for (const value of values) {
if (typeof value !== 'string') continue;
const digest = sha256(value);
counts.set(digest, (counts.get(digest) ?? 0) + 1);
}
return [...counts.entries()]
.map(([digest, count]) => ({ sha256: digest, count }))
.sort((left, right) => left.sha256.localeCompare(right.sha256));
}
function sanitizeSha256Counts(value) {
if (!Array.isArray(value) || value.length > 256) return null;
let previous = null;
const sanitized = [];
for (const entry of value) {
if (entry === null || typeof entry !== 'object' || Array.isArray(entry)) return null;
if (Object.keys(entry).sort().join(',') !== 'count,sha256') return null;
if (!/^[0-9a-f]{64}$/.test(entry.sha256)) return null;
if (!Number.isInteger(entry.count) || entry.count < 1 || entry.count > 2_147_483_647) return null;
if (previous !== null && previous >= entry.sha256) return null;
previous = entry.sha256;
sanitized.push({ sha256: entry.sha256, count: entry.count });
}
return sanitized;
}
function sanitizeEventAuditFailure(value) {
if (value === null || typeof value !== 'object' || Array.isArray(value)) return null;
if (Object.keys(value).sort().join(',') !== [...EVENT_AUDIT_FAILURE_KEYS].sort().join(',')) return null;
if (!EVENT_AUDIT_FAILURE_INVARIANTS.has(value.invariant)) return null;
const countKeys = [
'notificationCount',
'failureEventsObserved',
'wrongScopeEventsObserved',
'rerouteEventsObserved',
'unknownNotificationEventsObserved',
'unknownItemEventsObserved',
'toolEventsObserved',
];
for (const key of countKeys) {
if (!Number.isInteger(value[key]) || value[key] < 0 || value[key] > 2_147_483_647) return null;
}
if (!/^[0-9a-f]{64}$/.test(value.notificationGraphSha256)) return null;
const rejectedMethodSha256 = sanitizeSha256Counts(value.rejectedMethodSha256);
const rejectedItemTypeSha256 = sanitizeSha256Counts(value.rejectedItemTypeSha256);
if (rejectedMethodSha256 === null || rejectedItemTypeSha256 === null) return null;
return {
invariant: value.invariant,
notificationCount: value.notificationCount,
failureEventsObserved: value.failureEventsObserved,
wrongScopeEventsObserved: value.wrongScopeEventsObserved,
rerouteEventsObserved: value.rerouteEventsObserved,
unknownNotificationEventsObserved: value.unknownNotificationEventsObserved,
unknownItemEventsObserved: value.unknownItemEventsObserved,
toolEventsObserved: value.toolEventsObserved,
notificationGraphSha256: value.notificationGraphSha256,
rejectedMethodSha256,
rejectedItemTypeSha256,
};
}
function selectLifecycleFailure(primaryError, closeError) {
return primaryError ?? closeError ?? null;
}
function proofStateSatisfied(proof, setupOnly) {
if (proof === null || typeof proof !== 'object' || Array.isArray(proof)) return false;
return setupOnly
? proof.executed === false && proof.paidCalls === 0 && proof.pass === null
: proof.executed === true && proof.paidCalls === 0 && proof.pass === true;
}
function parseArgs(argv) {
const values = {};
const booleanFlags = new Set(['--execute-paid', '--self-test', '--setup-only']);
const valueFlags = new Set([
'--ack',
'--codex-exe',
'--expected-head',
'--hive-mind-cli',
'--marker',
'--model',
'--receipt-dir',
'--windows-powershell',
'--workspace',
]);
for (let index = 0; index < argv.length; index += 1) {
const flag = argv[index];
if (booleanFlags.has(flag)) {
assert(values[flag.slice(2)] === undefined, `duplicate argument: ${flag}`);
values[flag.slice(2)] = true;
continue;
}
assert(valueFlags.has(flag), `unsupported argument: ${flag}`);
assert(values[flag.slice(2)] === undefined, `duplicate argument: ${flag}`);
const value = argv[index + 1];
assert(value && !value.startsWith('--'), `missing value for ${flag}`);
values[flag.slice(2)] = value;
index += 1;
}
if (values['self-test']) {
assert(Object.keys(values).length === 1, '--self-test cannot be combined with other arguments');
return values;
}
for (const name of [
'codex-exe',
'expected-head',
'hive-mind-cli',
'model',
'receipt-dir',
'windows-powershell',
'workspace',
]) {
assert(typeof values[name] === 'string', `--${name} is required`);
}
assert(/^[0-9a-f]{40}$/.test(values['expected-head']), '--expected-head must be a lowercase 40-character Git object id');
assert(values.model.length <= 128 && !/[\x00-\x1f\x7f]/.test(values.model), '--model is invalid');
assert(!(values['execute-paid'] && values['setup-only']), '--setup-only and --execute-paid are mutually exclusive');
if (values['execute-paid']) {
assert(typeof values.marker === 'string', '--marker is required with --execute-paid');
assert(values.ack === PAID_ACK, `--ack must be exactly ${PAID_ACK}`);
assert(values.marker.length <= 256 && !/[\x00-\x1f\x7f]/.test(values.marker), '--marker is invalid');
} else {
assert(values.marker === undefined && values.ack === undefined, '--marker/--ack require --execute-paid');
}
return values;
}
function requireRegularFile(path, label) {
assert(existsSync(path), `${label} is missing`);
const posture = lstatSync(path);
assert(posture.isFile() && !posture.isSymbolicLink(), `${label} must be a regular file`);
}
function requireDirectory(path, label) {
assert(existsSync(path), `${label} is missing`);
const posture = lstatSync(path);
assert(posture.isDirectory() && !posture.isSymbolicLink(), `${label} must be a regular directory`);
}
function assertNoExistingReparsePoint(path) {
let cursor = resolve(path);
while (cursor) {
if (existsSync(cursor)) {
const posture = lstatSync(cursor);
assert(!posture.isSymbolicLink(), 'receipt ancestor must not be a symbolic link');
}
const parent = dirname(cursor);
if (parent === cursor) break;
cursor = parent;
}
}
function runChecked(command, args, options = {}) {
const result = spawnSync(command, args, {
cwd: options.cwd,
env: options.env ?? process.env,
encoding: 'utf8',
timeout: options.timeoutMs ?? 30_000,
maxBuffer: MAX_CAPTURE_BYTES,
windowsHide: true,
shell: false,
});
if (result.status !== 0 || result.signal !== null || result.error) {
const code = typeof result.error?.code === 'string' ? result.error.code : null;
throw new Error(`${options.label ?? basename(command)} failed (status=${String(result.status)}, code=${String(code)})`);
}
return {
stdout: String(result.stdout ?? ''),
stderr: String(result.stderr ?? ''),
};
}
function verifySourceSnapshot(expectedHead) {
const repositoryRoot = resolve(dirname(SCRIPT_PATH), '..');
const relativePath = relative(repositoryRoot, SCRIPT_PATH).replaceAll('\\', '/');
assert(relativePath.length > 0 && !relativePath.startsWith('../') && !isAbsolute(relativePath), 'helper must be inside the repository');
const git = (args, label) => runChecked('git.exe', ['-C', repositoryRoot, ...args], {
label,
timeoutMs: 30_000,
}).stdout.trim();
const observedHead = git(['rev-parse', 'HEAD'], 'repository HEAD');
const tree = git(['rev-parse', 'HEAD^{tree}'], 'repository tree');
const trackedStatus = git(['status', '--porcelain=v1', '--untracked-files=no'], 'tracked repository status');
assert(observedHead === expectedHead, `expected HEAD ${expectedHead} but observed ${observedHead}`);
assert(trackedStatus.length === 0, 'tracked repository state must be clean');
const trackedPath = git(['ls-files', '--error-unmatch', '--', relativePath], 'helper tracked-file lookup');
assert(trackedPath === relativePath, 'helper must be tracked at expected HEAD');
const scriptBlob = git(['rev-parse', `${expectedHead}:${relativePath}`], 'helper expected blob');
const workingBlob = git(['hash-object', `--path=${relativePath}`, SCRIPT_PATH], 'helper working blob');
assert(/^[0-9a-f]{40}$/.test(tree), 'repository tree object id is invalid');
assert(/^[0-9a-f]{40}$/.test(scriptBlob), 'helper expected blob object id is invalid');
assert(workingBlob === scriptBlob, 'helper working content differs from expected HEAD');
return {
expectedHead,
observedHead,
tree,
scriptBlob,
trackedClean: true,
};
}
function appendBounded(chunks, chunk) {
const used = chunks.reduce((sum, item) => sum + item.length, 0);
if (used >= MAX_CAPTURE_BYTES) return;
chunks.push(Buffer.from(chunk).subarray(0, MAX_CAPTURE_BYTES - used));
}
function safeError(error, replacements = []) {
let value = error instanceof Error ? error.message : String(error);
for (const replacement of replacements) {
if (replacement) value = value.replaceAll(replacement, '<REDACTED_PATH>');
}
return value
.replace(/\b(?:sk|sess)-[A-Za-z0-9_-]{12,}\b/g, '<REDACTED_CREDENTIAL>')
.slice(0, 500);
}
function waitForValue(read, timeoutMs, label) {
const deadline = Date.now() + timeoutMs;
return new Promise((resolveWait, rejectWait) => {
const poll = () => {
const value = read();
if (value !== undefined) {
resolveWait(value);
return;
}
if (Date.now() >= deadline) {
rejectWait(new Error(`${label} timed out`));
return;
}
setTimeout(poll, 20);
};
poll();
});
}
function tomlString(value) {
return JSON.stringify(value);
}
function powershellLiteral(value) {
return `'${value.replaceAll("'", "''")}'`;
}
function verifyWindowsPowerShell(powerShell) {
requireRegularFile(powerShell, 'Windows PowerShell');
const systemDirectory = runChecked(powerShell, [
'-NoLogo',
'-NoProfile',
'-NonInteractive',
'-Command',
'[Console]::Out.Write([Environment]::SystemDirectory)',
], {
label: 'Windows system-directory probe',
timeoutMs: 20_000,
}).stdout.trim();
assert(isAbsolute(systemDirectory), 'Windows system-directory probe returned a non-absolute path');
assert(basename(systemDirectory).toLowerCase() === 'system32', 'Windows system-directory probe did not return System32');
const probed = join(systemDirectory, 'WindowsPowerShell', 'v1.0', 'powershell.exe');
const packageCommandPath = join(
dirname(systemDirectory),
'System32',
'WindowsPowerShell',
'v1.0',
'powershell.exe',
);
requireRegularFile(probed, 'OS Windows PowerShell');
requireRegularFile(packageCommandPath, 'package-command Windows PowerShell');
assert(canonicalPath(powerShell) === canonicalPath(probed), 'Windows PowerShell is not under the OS system directory');
assert(canonicalPath(packageCommandPath) === canonicalPath(probed), 'package-command Windows PowerShell is not the probed OS binary');
return {
path: packageCommandPath,
sha256: sha256File(packageCommandPath),
systemDirectorySha256: sha256(canonicalPath(systemDirectory)),
};
}
function buildExpectedWaggleHookCommand(powerShell, scriptPath, cliPath) {
const script = [
"$ErrorActionPreference = 'Stop'",
'try {',
` & ${[
process.execPath,
scriptPath,
'--cli-path',
cliPath,
].map(powershellLiteral).join(' ')}`,
' if ($null -eq $LASTEXITCODE) { exit 1 }',
' exit $LASTEXITCODE',
'} catch {',
' [Console]::Error.WriteLine($_.Exception.Message)',
' exit 1',
'}',
].join('\r\n');
const encoded = Buffer.from(script, 'utf16le').toString('base64');
return `${powerShell} -NoLogo -NoProfile -NonInteractive -EncodedCommand ${encoded}`;
}
function buildExpectedWaggleHooks(powerShell, cliPath) {
const repoRoot = resolve(dirname(SCRIPT_PATH), '..');
const packageRoot = join(repoRoot, 'packages', 'hive-mind-hooks-codex', 'dist', 'hooks');
const scripts = new Map([
['preCompact', join(packageRoot, 'pre-compact.js')],
['sessionStart', join(packageRoot, 'session-start.js')],
['stop', join(packageRoot, 'stop.js')],
['userPromptSubmit', join(packageRoot, 'user-prompt-submit.js')],
]);
const commands = new Map();
const artifacts = [];
requireRegularFile(cliPath, 'hive-mind CLI');
artifacts.push({ kind: 'hive-mind-cli', sha256: sha256File(cliPath) });
for (const [eventName, scriptPath] of scripts) {
requireRegularFile(scriptPath, `${eventName} packaged hook`);
const command = buildExpectedWaggleHookCommand(powerShell, scriptPath, cliPath);
commands.set(eventName, sha256(command));
artifacts.push({ eventName, sha256: sha256File(scriptPath), commandSha256: sha256(command) });
}
return {
commands,
artifactsSha256: sha256(stableJson(artifacts)),
cliSha256: sha256File(cliPath),
};
}
function buildDenyHookConfig(powerShell) {
const payload = JSON.stringify(DENIAL_RESPONSE);
const encoded = Buffer.from(`[Console]::Out.Write('${payload.replaceAll("'", "''")}')`, 'utf16le')
.toString('base64');
const command = `${powerShell} -NoLogo -NoProfile -NonInteractive -EncodedCommand ${encoded}`;
assert(!command.includes("'"), 'deny hook command cannot contain a TOML literal quote');
return {
command,
config: `hooks.PreToolUse=[{matcher='*',hooks=[{type='command',command='false',command_windows='${command}',timeout=5}]}]`,
commandSha256: sha256(command),
};
}
function buildChildEnvironment(tempRoot) {
const environment = { ...process.env };
for (const name of [
'ANTHROPIC_API_KEY',
'CODEX_ACCESS_TOKEN',
'OPENAI_ACCESS_TOKEN',
'OPENAI_API_BASE',
'OPENAI_API_KEY',
'OPENAI_BASE_URL',
'OPENROUTER_API_KEY',
]) {
environment[name] = '';
}
environment.NO_COLOR = '1';
environment.HIVE_MIND_DATA_DIR = join(tempRoot, 'mind');
environment.HIVE_MIND_EMBEDDING_PROVIDER = 'mock';
environment.HIVE_MIND_NO_SYNTH = '1';
environment.HIVE_MIND_SCOPES = 'memory:read,memory:write';
environment.HIVE_MIND_SHIM_LOG_LEVEL = 'error';
environment.WAGGLE_SIGNAL_EMIT = '0';
environment.WAGGLE_WORKSPACE_ID = `codex-denial-${randomBytes(12).toString('hex')}`;
environment.TEMP = join(tempRoot, 'process-temp');
environment.TMP = environment.TEMP;
return environment;
}
function mutateModel(base, sealed) {
const model = structuredClone(base);
model.apply_patch_tool_type = null;
model.input_modalities = sealed ? ['text'] : ['text', 'image'];
model.supports_image_detail_original = !sealed;
model.supports_search_tool = false;
model.experimental_supported_tools = [];
model.use_responses_lite = false;
return model;
}
function readBundledModel(codexExecutable, model) {
const raw = runChecked(codexExecutable, ['debug', 'models', '--bundled'], {
label: 'Codex bundled-model catalog',
timeoutMs: 30_000,
}).stdout;
let catalog;
try {
catalog = JSON.parse(raw);
} catch {
throw new Error('Codex bundled-model catalog was not valid JSON');
}
assert(Array.isArray(catalog?.models), 'Codex bundled-model catalog omitted models');
const selected = catalog.models.find((entry) => entry?.slug === model);
assert(selected && typeof selected === 'object', `Codex bundled catalog omitted model ${model}`);
return { selected, bundledSha256: sha256(raw) };
}
function writeCatalog(path, model) {
const value = `${JSON.stringify({ models: [model] })}\n`;
return writeFile(path, value, 'utf8').then(() => sha256(value));
}
function buildMcpInventoryArguments() {
const args = [];
for (const feature of DISABLED_FEATURES) args.push('--disable', feature);
return [...args, 'mcp', 'list', '--json'];
}
function readConfiguredMcpServerNames(codexExecutable, env) {
const raw = runChecked(codexExecutable, buildMcpInventoryArguments(), {
env,
label: 'Codex MCP server inventory',
timeoutMs: 30_000,
}).stdout;
let value;
try {
value = JSON.parse(raw);
} catch {
throw new Error('Codex MCP server inventory was not valid JSON');
}
assert(Array.isArray(value), 'Codex MCP server inventory was not an array');
const names = value.map((entry) => entry?.name);
assert(names.every((name) => typeof name === 'string' && name.length > 0), 'Codex MCP server inventory contained an invalid name');
assert(names.every((name) => !/[\u0000\r\n']/.test(name)), 'Codex MCP server name contains an unsupported character');
assert(new Set(names).size === names.length, 'Codex MCP server inventory contained duplicate names');
return names.sort((left, right) => left.localeCompare(right));
}
function captureMcpBoundary(codexExecutable, env, configPath) {
requireRegularFile(configPath, 'user Codex config.toml');
const names = readConfiguredMcpServerNames(codexExecutable, env);
const namesSha256 = sha256(stableJson(names));
const configSha256 = sha256File(configPath);
return {
count: names.length,
namesSha256,
configSha256,
boundarySha256: sha256(stableJson({ count: names.length, namesSha256, configSha256 })),
};
}
function assertSameMcpBoundary(expected, actual, label) {
assert(
actual.count === expected.count
&& actual.namesSha256 === expected.namesSha256
&& actual.configSha256 === expected.configSha256
&& actual.boundarySha256 === expected.boundarySha256,
`Codex MCP/config boundary changed ${label}`,
);
}
function responseEvent(event) {
return `event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`;
}
function completedUsage() {
return {
input_tokens: 0,
input_tokens_details: null,
output_tokens: 0,
output_tokens_details: null,
total_tokens: 0,
};
}
function messageResponse(text) {
const responseId = `resp_${randomBytes(8).toString('hex')}`;
const itemId = `msg_${randomBytes(8).toString('hex')}`;
const item = {
type: 'message',
role: 'assistant',
id: itemId,
status: 'completed',
content: [{ type: 'output_text', text }],
};
return [
responseEvent({ type: 'response.created', response: { id: responseId, status: 'in_progress' } }),
responseEvent({ type: 'response.output_item.done', output_index: 0, item }),
responseEvent({
type: 'response.completed',
response: { id: responseId, status: 'completed', output: [item], usage: completedUsage() },
}),
].join('');
}
function functionCallResponse(name, argumentsJson) {
const responseId = `resp_${randomBytes(8).toString('hex')}`;
const itemId = `fc_${randomBytes(8).toString('hex')}`;
const callId = `call_${randomBytes(8).toString('hex')}`;
const item = {
type: 'function_call',
id: itemId,
call_id: callId,
name,
arguments: argumentsJson,
status: 'completed',
};
return [
responseEvent({ type: 'response.created', response: { id: responseId, status: 'in_progress' } }),
responseEvent({ type: 'response.output_item.added', output_index: 0, item: { ...item, arguments: '', status: 'in_progress' } }),
responseEvent({ type: 'response.function_call_arguments.done', item_id: itemId, output_index: 0, arguments: argumentsJson }),
responseEvent({ type: 'response.output_item.done', output_index: 0, item }),
responseEvent({
type: 'response.completed',
response: { id: responseId, status: 'completed', output: [item], usage: completedUsage() },
}),
].join('');
}
function toolNamesFromRequest(body) {
if (!Array.isArray(body?.tools)) return [];
return body.tools.map((tool) => tool?.name ?? tool?.function?.name ?? null);
}
function additionalToolsFromRequest(body) {
const values = [];
const visit = (value, insideInput = false) => {
if (Array.isArray(value)) {
value.forEach((child) => visit(child, insideInput));
return;
}
if (!value || typeof value !== 'object') return;
for (const [key, child] of Object.entries(value)) {
if (insideInput && key === 'additional_tools') {
values.push(...(Array.isArray(child) ? child : [child]));
}
visit(child, insideInput || key === 'input');
}
};
visit(body, false);
return values;
}
function requestContainsImage(body) {
let found = false;
const visit = (value) => {
if (found) return;
if (typeof value === 'string') {
if (value.includes('data:image')) found = true;
return;
}
if (Array.isArray(value)) {
value.forEach(visit);
return;
}
if (!value || typeof value !== 'object') return;
if (value.type === 'input_image') {
found = true;
return;
}
Object.values(value).forEach(visit);
};
visit(body);
return found;
}
async function startLoopbackProvider(mode, expectedAcknowledgement, sentinelPath) {
const records = [];
const server = createServer((request, response) => {
const chunks = [];
let bytes = 0;
request.on('data', (chunk) => {
bytes += chunk.length;
if (bytes <= MAX_REQUEST_BYTES) chunks.push(Buffer.from(chunk));
});
request.on('end', () => {
const raw = Buffer.concat(chunks).toString('utf8');
let body = null;
try { body = JSON.parse(raw); } catch { /* rejected below */ }
const record = {
index: records.length,
method: request.method ?? null,
endpointOk: request.url === '/v1/responses',
bytes,
sha256: sha256(raw),
topLevelToolsIsArray: Array.isArray(body?.tools),
toolNames: body ? toolNamesFromRequest(body) : [],
additionalToolCount: body ? additionalToolsFromRequest(body).length : -1,
containsImage: body ? requestContainsImage(body) : false,
};
records.push(record);
if (
request.method !== 'POST'
|| !record.endpointOk
|| body === null
|| bytes > MAX_REQUEST_BYTES
) {
response.writeHead(400, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: { message: 'Waggle loopback policy rejected request' } }));
return;
}
let payload;
if (mode === 'sealed') {
payload = records.length === 1
? messageResponse(expectedAcknowledgement)
: null;
} else if (records.length === 1) {
payload = functionCallResponse('view_image', JSON.stringify({ path: sentinelPath }));
} else if (records.length === 2) {
payload = messageResponse(expectedAcknowledgement);
}
if (payload === null || payload === undefined) {
response.writeHead(400, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: { message: 'Waggle loopback request count exceeded policy' } }));
return;
}
response.writeHead(200, {
'cache-control': 'no-cache',
'content-type': 'text/event-stream; charset=utf-8',
});
response.end(payload);
});
});
await new Promise((resolveListen, rejectListen) => {
server.once('error', rejectListen);
server.listen(0, '127.0.0.1', resolveListen);
});
const address = server.address();
assert(address && typeof address !== 'string', 'loopback provider did not expose an address');
return {
baseUrl: `http://127.0.0.1:${address.port}/v1`,
records,
close: () => new Promise((resolveClose, rejectClose) => {
server.close((error) => error ? rejectClose(error) : resolveClose());
}),
};
}
function buildInvocationArguments(options) {
const args = ['app-server', '--stdio', '--strict-config'];
for (const feature of DISABLED_FEATURES) args.push('--disable', feature);
assert(Array.isArray(options.mcpServerNames), 'MCP server inventory is missing');
const mcpServerEntries = options.mcpServerNames.map((name) => {
assert(!/[\u0000\r\n']/.test(name), 'MCP server name contains an unsupported character');
return `'${name}'={enabled=false}`;
});
const hookStateEntries = [];
const hookStateKeys = new Set();
for (const key of options.hookDisables ?? []) {
assert(!/[\u0000\r\n']/.test(key), 'disabled hook key contains an unsupported character');
assert(!hookStateKeys.has(key), 'duplicate hook-state key');
hookStateKeys.add(key);
hookStateEntries.push(`'${key}'={enabled=false}`);
}
for (const { key, currentHash } of options.hookPins) {
assert(!/[\u0000\r\n']/.test(key), 'hook key contains an unsupported character');
assert(!hookStateKeys.has(key), 'duplicate hook-state key');
assert(/^sha256:[0-9a-f]{64}$/.test(currentHash), 'hook currentHash is invalid');
hookStateKeys.add(key);
hookStateEntries.push(`'${key}'={enabled=true,trusted_hash='${currentHash}'}`);
}
const overrides = [
...CONFIG_OVERRIDES,
'model_reasoning_effort="low"',
`mcp_servers={${mcpServerEntries.join(',')}}`,
`model_catalog_json=${tomlString(options.catalogPath)}`,
...(options.providerBaseUrl ? [
'model_provider="waggle_loopback"',
`model_providers.waggle_loopback.name="Waggle zero-cost loopback"`,
`model_providers.waggle_loopback.base_url=${tomlString(options.providerBaseUrl)}`,
'model_providers.waggle_loopback.wire_api="responses"',
'model_providers.waggle_loopback.request_max_retries=0',
'model_providers.waggle_loopback.stream_max_retries=0',
'model_providers.waggle_loopback.requires_openai_auth=false',
'model_providers.waggle_loopback.supports_websockets=false',
] : [
'model_provider="openai"',
]),
...(options.includeDeny ? [options.denyConfig] : []),
...(hookStateEntries.length > 0 ? [`hooks.state={${hookStateEntries.join(',')}}`] : []),
];
for (const override of overrides) args.push('-c', override);
return args;
}
function normalizedArguments(args, sensitiveValues) {
return args.map((argument) => {
if (argument.startsWith('hooks.state={')) {
return `hooks.state=<sha256:${sha256(argument)}>`;
}
if (argument.startsWith('mcp_servers={')) {
return `mcp_servers=<sha256:${sha256(argument)}>`;
}
let normalized = argument;
for (const [value, label] of sensitiveValues) {
if (value) normalized = normalized.replaceAll(value, label);
}
return normalized;
});
}
class AppServerClient {
constructor(executable, args, cwd, env, replacements) {
this.responses = new Map();
this.notifications = [];
this.stdoutChunks = [];
this.stderrChunks = [];
this.buffer = '';
this.sequence = 0;
this.closed = false;
this.error = null;
this.replacements = replacements;
this.child = spawn(executable, args, {
cwd,
env,
stdio: ['pipe', 'pipe', 'pipe'],
windowsHide: true,
shell: false,
});
this.child.stdout.on('data', (chunk) => {
appendBounded(this.stdoutChunks, chunk);
this.buffer += chunk.toString('utf8');
while (this.buffer.includes('\n')) {
const newline = this.buffer.indexOf('\n');
const line = this.buffer.slice(0, newline).trim();
this.buffer = this.buffer.slice(newline + 1);
this.processLine(line);
}
});
this.child.stderr.on('data', (chunk) => appendBounded(this.stderrChunks, chunk));
this.child.on('error', (error) => {
this.error = safeError(error, replacements);
});
this.child.on('close', () => {
this.closed = true;
this.processLine(this.buffer.trim());
this.buffer = '';
});
}
processLine(line) {
if (!line) return;
let message;
try { message = JSON.parse(line); } catch {
this.error = this.error ?? 'Codex app-server emitted malformed protocol output';
return;
}
this.sequence += 1;
if (message?.id !== undefined && typeof message?.method !== 'string') {
this.responses.set(String(message.id), message);
} else if (typeof message?.method === 'string') {
this.notifications.push({ sequence: this.sequence, message });
}
}
send(message) {
assert(this.child.stdin.writable, 'Codex app-server stdin is closed');
this.child.stdin.write(`${JSON.stringify(message)}\n`);
}
response(id, timeoutMs = 20_000) {
return waitForValue(
() => this.responses.get(String(id)),
timeoutMs,
`Codex app-server response ${id}`,
);
}
notification(method, predicate = () => true, timeoutMs = TURN_TIMEOUT_MS) {
return waitForValue(
() => this.notifications.find((entry) => (
entry.message?.method === method && predicate(entry)
)),
timeoutMs,
`Codex app-server notification ${method}`,
);
}
async initialize(clientName) {
this.send({
method: 'initialize',
id: 1,
params: {
clientInfo: { name: clientName, title: clientName, version: '1.0.0' },
capabilities: { experimentalApi: true },
},
});
const response = await this.response(1);
assertProtocolSuccess(response, 'Codex app-server initialize failed');
this.send({ method: 'initialized', params: {} });
}
async close() {
try { this.child.stdin.end(); } catch { /* already closed */ }
try {
await waitForValue(() => this.closed ? true : undefined, 5_000, 'Codex app-server close');
} catch {
if (this.child.pid) {
spawnSync('taskkill.exe', ['/PID', String(this.child.pid), '/T', '/F'], {
encoding: 'utf8',
timeout: 10_000,
windowsHide: true,
});
}
try { this.child.kill('SIGKILL'); } catch { /* already closed */ }
await waitForValue(() => this.closed ? true : undefined, 5_000, 'forced Codex app-server close');
}
}
transcriptDigest() {
return {
stdoutBytes: Buffer.concat(this.stdoutChunks).length,
stdoutSha256: sha256(Buffer.concat(this.stdoutChunks)),
stderrBytes: Buffer.concat(this.stderrChunks).length,
stderrSha256: sha256(Buffer.concat(this.stderrChunks)),
};
}
}
function assertProtocolSuccess(response, message) {
if (!response?.error) return;
const error = new Error(message);
error.protocolCode = sanitizeProtocolCode(response.error?.code);
throw error;
}
function hookGraphFromEntry(entry) {
const hooks = Array.isArray(entry?.hooks) ? entry.hooks : [];
return hooks.map((hook) => ({
eventName: hook?.eventName ?? null,
sourcePath: hook?.sourcePath ?? null,
sourceCanonical: typeof hook?.sourcePath === 'string' && existsSync(hook.sourcePath)
? canonicalPath(hook.sourcePath)
: String(hook?.sourcePath ?? '').toLowerCase(),
key: hook?.key ?? null,
currentHash: hook?.currentHash ?? null,
commandSha256: typeof hook?.command === 'string' ? sha256(hook.command) : null,
enabled: hook?.enabled === true,
trustStatus: hook?.trustStatus ?? null,
}));
}
function assertExactHookGraph(
entry,
codexHome,
includeDeny,
requireTrusted,
expectedDenyCommandSha256,
expectedWaggleCommands,
allowActiveExtras,
) {
const graph = hookGraphFromEntry(entry);
const expectedCount = WAGGLE_HOOK_EVENTS.length + (includeDeny ? 1 : 0);
const warnings = Array.isArray(entry?.warnings) ? entry.warnings.length : -1;
const errors = Array.isArray(entry?.errors) ? entry.errors.length : -1;
assert(warnings === 0, `hooks/list reported ${warnings} warnings`);
assert(errors === 0, `hooks/list reported ${errors} errors`);
const waggleSource = canonicalPath(join(codexHome, 'hooks.json'));
assert(expectedWaggleCommands instanceof Map, 'expected Waggle hook commands are missing');
const waggle = graph.filter((hook) => (
hook.sourceCanonical === waggleSource
&& hook.commandSha256 === expectedWaggleCommands.get(hook.eventName)
));
const deny = graph.filter((hook) => (
hook.eventName === 'preToolUse'
&& hook.sourceCanonical === 'c:\\<session-flags>\\config.toml'
&& hook.commandSha256 === expectedDenyCommandSha256
));
const waggleSourceMatches = graph.filter((hook) => hook.sourceCanonical === waggleSource).length;
const waggleCommandMatches = graph.filter((hook) => (
hook.commandSha256 === expectedWaggleCommands.get(hook.eventName)
)).length;
assert(
waggle.length === 4,
`hooks/list returned ${waggle.length} Waggle hooks, expected 4 (sourceMatches=${waggleSourceMatches}, commandMatches=${waggleCommandMatches})`,
);
assert(
JSON.stringify(waggle.map((hook) => hook.eventName).sort())
=== JSON.stringify([...WAGGLE_HOOK_EVENTS].sort()),
'hooks/list returned an unexpected Waggle event set',
);
assert(deny.length === (includeDeny ? 1 : 0), 'hooks/list returned an unexpected deny-hook count');
const expected = [...waggle, ...deny];
const expectedKeys = new Set(expected.map((hook) => hook.key));
const extras = graph.filter((hook) => !expectedKeys.has(hook.key));
assert(graph.every((hook) => (
typeof hook.key === 'string'
&& /^sha256:[0-9a-f]{64}$/.test(hook.currentHash ?? '')
&& typeof hook.commandSha256 === 'string'
)), 'hooks/list returned a malformed hook');
assert(expected.every((hook) => hook.enabled), 'an expected hook is disabled');
if (!allowActiveExtras) {
assert(extras.every((hook) => !hook.enabled), 'an unapproved hook remains enabled');
}
if (requireTrusted) {
assert(expected.every((hook) => hook.trustStatus === 'trusted'), 'an expected hook is not hash-trusted');
}
const sanitized = graph.map((hook) => ({
eventName: hook.eventName,
sourceKind: waggle.includes(hook)
? 'waggle-hooks-json'
: deny.includes(hook) ? 'session-flags' : 'disabled-external',
sourcePathSha256: sha256(hook.sourceCanonical),
keySha256: sha256(hook.key),
currentHash: hook.currentHash,
commandSha256: hook.commandSha256,
enabled: hook.enabled,
trustStatus: requireTrusted ? hook.trustStatus : 'pre-pin',
})).sort((left, right) => stableJson(left).localeCompare(stableJson(right)));
const identity = sanitized.map(({ enabled: _enabled, trustStatus: _trustStatus, ...value }) => value);
return {
graph,
expected,
extras,
sanitized,
graphSha256: sha256(stableJson(sanitized)),
identitySha256: sha256(stableJson(identity)),
warnings,
errors,
activeCount: graph.filter((hook) => hook.enabled).length,
extraCount: extras.filter((hook) => hook.enabled).length,
disabledExtraCount: extras.filter((hook) => !hook.enabled).length,
denyHookSha256: includeDeny ? deny[0].currentHash.slice('sha256:'.length) : null,
};
}
async function listHooks(
executable,
args,
cwd,
env,
codexHome,
includeDeny,
requireTrusted,
expectedDenyCommandSha256,
expectedWaggleCommands,
allowActiveExtras,
replacements,
) {
const client = new AppServerClient(executable, args, cwd, env, replacements);
try {
await client.initialize('waggle_codex_tool_denial_discovery');
client.send({ method: 'hooks/list', id: 2, params: { cwds: [cwd] } });
const response = await client.response(2);
assert(!response.error, 'Codex app-server hooks/list failed');
const entry = response.result?.data?.[0];
const result = assertExactHookGraph(
entry,
codexHome,
includeDeny,
requireTrusted,
expectedDenyCommandSha256,
expectedWaggleCommands,
allowActiveExtras,
);
assert(client.error === null, client.error ?? 'Codex hook discovery failed');
return { ...result, transcript: client.transcriptDigest() };
} finally {
await client.close();
}
}
function itemType(notification) {
return notification?.message?.params?.item?.type ?? null;
}
function treeContains(value, needle) {
if (typeof value === 'string') return value.includes(needle);
if (Array.isArray(value)) return value.some((child) => treeContains(child, needle));
if (value && typeof value === 'object') return Object.values(value).some((child) => treeContains(child, needle));
return false;
}
function notificationScope(notification) {
const params = notification?.message?.params ?? {};
return {
threadIds: [params.threadId, params.thread?.id, params.turn?.threadId]
.filter((value) => typeof value === 'string'),
turnIds: [params.turnId, params.turn?.id, params.item?.turnId]
.filter((value) => typeof value === 'string'),
};
}
function isFailClosedProjectTrustWarning(notification) {
if (notification?.message?.method !== 'configWarning') return false;
const params = notification.message?.params;
if (!params || typeof params !== 'object' || Array.isArray(params)) return false;
const allowedKeys = new Set(['details', 'path', 'range', 'summary']);
const keys = Object.keys(params);
if (!keys.includes('summary')) return false;
if (!keys.every((key) => allowedKeys.has(key))) return false;
if (keys.includes('details') && params.details !== null) return false;
if (keys.includes('path') && params.path !== null) return false;
if (keys.includes('range') && params.range !== null) return false;
if (typeof params.summary !== 'string') return false;
const lines = params.summary.split('\n');
if (lines.at(-1) !== '') return false;
if (lines[0] !== FAIL_CLOSED_PROJECT_TRUST_WARNING) return false;
const warningLines = lines.slice(1, -1);
if (warningLines.length < 2 || warningLines.length % 2 !== 0) return false;
const folderPattern = /^ ([1-9][0-9]*)\. ([A-Za-z]:[\\/].+[\\/]\.codex)$/;
const instructionPattern = /^ To load project-local config, hooks, and exec policies, add ([A-Za-z]:[\\/].+) as a trusted project in ([A-Za-z]:[\\/].+[\\/]\.codex[\\/]config\.toml)\.$/;
const normalizePath = (value) => value.replaceAll('/', '\\').replace(/\\+$/, '').toLowerCase();
for (let index = 0; index < warningLines.length; index += 2) {
const folderMatch = folderPattern.exec(warningLines[index]);
const instructionMatch = instructionPattern.exec(warningLines[index + 1]);
if (!folderMatch || !instructionMatch) return false;
if (Number(folderMatch[1]) !== (index / 2) + 1) return false;
if (normalizePath(folderMatch[2]) !== `${normalizePath(instructionMatch[1])}\\.codex`) {
return false;
}
}
return true;
}
function collectEventAudit(notifications, expectedAcknowledgement, threadId, turnId) {
const failureNotifications = notifications.filter((entry) => (
FAILURE_NOTIFICATION_METHODS.has(entry.message?.method)
&& !isFailClosedProjectTrustWarning(entry)
));
const unknownNotifications = notifications.filter((entry) => (
!ALLOWED_NOTIFICATION_METHODS.has(entry.message?.method)
&& !FAILURE_NOTIFICATION_METHODS.has(entry.message?.method)
));
const wrongScopeNotifications = notifications.filter((entry) => {
const scope = notificationScope(entry);
return scope.threadIds.some((value) => value !== threadId)
|| scope.turnIds.some((value) => value !== turnId);
});
const rerouteNotifications = notifications.filter((entry) => (
entry.message?.method === 'model/rerouted'
));
const itemNotifications = notifications.filter((entry) => (
entry.message?.method === 'item/started'
|| entry.message?.method === 'item/updated'
|| entry.message?.method === 'item/completed'
));
const unknownItems = itemNotifications.filter((entry) => !ALLOWED_ITEM_TYPES.has(itemType(entry)));
const completedMessages = notifications.filter((entry) => (
entry.message?.method === 'item/completed'
&& itemType(entry) === 'agentMessage'
));
const exactMessages = completedMessages.filter((entry) => (
entry.message?.params?.item?.text?.trim() === expectedAcknowledgement
));
const imageItems = itemNotifications.filter((entry) => (
String(itemType(entry)).toLowerCase().includes('image')
|| treeContains(entry.message?.params?.item, 'view_image')
));
const denyHooks = notifications.filter((entry) => (
entry.message?.method === 'hook/completed'
&& entry.message?.params?.run?.eventName === 'preToolUse'
&& treeContains(entry.message?.params?.run, DENIAL_REASON)
));
const forbiddenToolItems = itemNotifications.filter((entry) => {
const type = String(itemType(entry) ?? '').toLowerCase();
return [
'command',
'computer',
'dynamictool',
'filechange',
'imagegeneration',
'mcptool',
'websearch',
].some((family) => type.includes(family));
});
const notificationGraph = notifications.map((entry, index) => {
const method = entry.message?.method;
const type = itemType(entry);
return {
ordinal: index + 1,
methodSha256: typeof method === 'string' ? sha256(method) : null,
itemTypeSha256: typeof type === 'string' ? sha256(type) : null,
};
});
return {
notificationCount: notifications.length,
exactMessageCount: exactMessages.length,
normalTextCompleted: completedMessages.length === 1 && exactMessages.length === 1,
imageItemCount: imageItems.length,
firstImageSequence: imageItems[0]?.sequence ?? null,
denyHookCount: denyHooks.length,
firstDenySequence: denyHooks[0]?.sequence ?? null,
toolEventsObserved: forbiddenToolItems.length,
unknownEventsObserved: unknownNotifications.length + unknownItems.length,
unknownNotificationEventsObserved: unknownNotifications.length,
unknownItemEventsObserved: unknownItems.length,
failureEventsObserved: failureNotifications.length,
wrongScopeEventsObserved: wrongScopeNotifications.length,
rerouteEventsObserved: rerouteNotifications.length,
notificationGraphSha256: sha256(stableJson(notificationGraph)),
rejectionEvidence: {
failureMethodSha256: groupSha256(failureNotifications.map((entry) => entry.message?.method)),
wrongScopeMethodSha256: groupSha256(wrongScopeNotifications.map((entry) => entry.message?.method)),
rerouteMethodSha256: groupSha256(rerouteNotifications.map((entry) => entry.message?.method)),
unknownMethodSha256: groupSha256(unknownNotifications.map((entry) => entry.message?.method)),
unknownItemTypeSha256: groupSha256(unknownItems.map((entry) => itemType(entry))),
toolMethodSha256: groupSha256(forbiddenToolItems.map((entry) => entry.message?.method)),
toolItemTypeSha256: groupSha256(forbiddenToolItems.map((entry) => itemType(entry))),
agentMessageMethodSha256: groupSha256(completedMessages.map((entry) => entry.message?.method)),
agentMessageItemTypeSha256: groupSha256(completedMessages.map((entry) => itemType(entry))),
},
};
}
function throwEventAuditFailure(invariant, audit) {
const evidence = audit.rejectionEvidence;
let rejectedMethodSha256 = [];
let rejectedItemTypeSha256 = [];
if (invariant === 'failure-notification') rejectedMethodSha256 = evidence.failureMethodSha256;
if (invariant === 'wrong-scope') rejectedMethodSha256 = evidence.wrongScopeMethodSha256;
if (invariant === 'model-reroute') rejectedMethodSha256 = evidence.rerouteMethodSha256;
if (invariant === 'unknown-notification-method') rejectedMethodSha256 = evidence.unknownMethodSha256;
if (invariant === 'unknown-item-type') rejectedItemTypeSha256 = evidence.unknownItemTypeSha256;
if (invariant === 'forbidden-tool-item') {
rejectedMethodSha256 = evidence.toolMethodSha256;
rejectedItemTypeSha256 = evidence.toolItemTypeSha256;
}
if (invariant === 'agent-message-cardinality') {
rejectedMethodSha256 = evidence.agentMessageMethodSha256;
rejectedItemTypeSha256 = evidence.agentMessageItemTypeSha256;
}
const error = new Error('Codex event audit rejected a sanitized invariant');
error.eventAuditFailure = sanitizeEventAuditFailure({
invariant,
notificationCount: audit.notificationCount,
failureEventsObserved: audit.failureEventsObserved,
wrongScopeEventsObserved: audit.wrongScopeEventsObserved,
rerouteEventsObserved: audit.rerouteEventsObserved,
unknownNotificationEventsObserved: audit.unknownNotificationEventsObserved,
unknownItemEventsObserved: audit.unknownItemEventsObserved,
toolEventsObserved: audit.toolEventsObserved,
notificationGraphSha256: audit.notificationGraphSha256,
rejectedMethodSha256,
rejectedItemTypeSha256,
});
throw error;
}
function assertEventAudit(audit, strictAllowlist, requireAcknowledgement = true) {
if (audit.failureEventsObserved !== 0) throwEventAuditFailure('failure-notification', audit);
if (audit.wrongScopeEventsObserved !== 0) throwEventAuditFailure('wrong-scope', audit);
if (audit.rerouteEventsObserved !== 0) throwEventAuditFailure('model-reroute', audit);
if (strictAllowlist) {
if (audit.unknownNotificationEventsObserved !== 0) {
throwEventAuditFailure('unknown-notification-method', audit);
}
if (audit.unknownItemEventsObserved !== 0) throwEventAuditFailure('unknown-item-type', audit);
if (audit.toolEventsObserved !== 0) throwEventAuditFailure('forbidden-tool-item', audit);
}
if (requireAcknowledgement) {
if (!audit.normalTextCompleted) throwEventAuditFailure('agent-message-cardinality', audit);
}
}
function eventAudit(
notifications,
expectedAcknowledgement,
strictAllowlist,
threadId = null,
turnId = null,
requireAcknowledgement = true,
) {
const audit = collectEventAudit(
notifications,
expectedAcknowledgement,
threadId,
turnId,
);
assertEventAudit(audit, strictAllowlist, requireAcknowledgement);
return audit;
}
async function runTurn(options) {
const client = new AppServerClient(
options.executable,
options.args,
options.workspace,
options.env,
options.replacements,
);
let threadId = null;
let turnId = null;
let result = null;
let primaryError = null;
let closeError = null;
let failureStage = 'app-server-spawn';
let completedStage = 'app-server-spawn';
let modelCalls = 0;
let turnStartCalls = 0;
let closeClean = false;
try {
failureStage = 'initialize';
await client.initialize(`waggle_codex_tool_denial_${options.lane}`);
completedStage = 'initialize';
failureStage = 'hooks-list';
client.send({ method: 'hooks/list', id: 2, params: { cwds: [options.workspace] } });
const hookResponse = await client.response(2);
assertProtocolSuccess(hookResponse, 'Codex app-server hooks/list failed');
const hooks = assertExactHookGraph(
hookResponse.result?.data?.[0],
options.codexHome,
options.includeDeny,
true,
options.expectedDenyCommandSha256,
options.expectedWaggleCommands,
false,
);
assert(hooks.graphSha256 === options.expectedHookGraphSha256, 'hook graph changed after hash pinning');
completedStage = 'hooks-list';
failureStage = 'pre-turn-boundary';
if (options.beforeTurn) await options.beforeTurn();
completedStage = 'pre-turn-boundary';
const threadParams = {
model: options.model,
cwd: options.workspace,
approvalPolicy: 'never',
sandbox: 'read-only',
ephemeral: true,
allowProviderModelFallback: false,
...(options.useDefaultEnvironmentForControl ? {} : { environments: [] }),
selectedCapabilityRoots: [],
dynamicTools: [],
};
failureStage = 'thread-start';
client.send({ method: 'thread/start', id: 3, params: threadParams });
const threadResponse = await client.response(3);
assertProtocolSuccess(threadResponse, 'Codex app-server thread/start failed');
threadId = threadResponse.result?.thread?.id ?? null;
assert(typeof threadId === 'string' && threadId.length > 0, 'Codex app-server omitted thread id');
completedStage = 'thread-start';
if (options.setupOnly) {
result = {
hooks,
events: null,
threadParamsSha256: sha256(stableJson({ ...threadParams, cwd: '<WORKSPACE>' })),
turnParamsSha256: null,
threadId,
threadIdSha256: sha256(threadId),
turnIdSha256: null,
transcript: client.transcriptDigest(),
modelCalls: 0,
turnStartCalls: 0,
completedStage: 'thread-start',
};
} else {
const turnParams = {
threadId,
input: [{ type: 'text', text: options.prompt, text_elements: [] }],
model: options.model,
approvalPolicy: 'never',
sandboxPolicy: { type: 'readOnly', networkAccess: false },
...(options.useDefaultEnvironmentForControl ? {} : { environments: [] }),
};
failureStage = 'turn-start';
turnStartCalls += 1;
client.send({ method: 'turn/start', id: 4, params: turnParams });
const turnResponse = await client.response(4);
assertProtocolSuccess(turnResponse, 'Codex app-server turn/start failed');
modelCalls += 1;
turnId = turnResponse.result?.turn?.id ?? null;
assert(typeof turnId === 'string' && turnId.length > 0, 'Codex app-server omitted turn id');
completedStage = 'turn-start';
failureStage = 'turn-completed';
const completed = await client.notification('turn/completed', (entry) => (
entry.message?.params?.threadId === threadId
&& entry.message?.params?.turn?.id === turnId
));
await new Promise((resolveWait) => setTimeout(resolveWait, 100));
const completedTurn = completed.message?.params?.turn;
if (completedTurn?.status !== 'completed') {
const errorNotification = [...client.notifications].reverse().find((entry) => (
entry.message?.method === 'error'
&& entry.message?.params?.threadId === threadId
&& entry.message?.params?.turnId === turnId
)) ?? null;
const terminalError = new Error('Codex app-server turn did not complete');
terminalError.turnFailure = sanitizeTurnFailure(completedTurn, errorNotification);
throw terminalError;
}
completedStage = 'turn-completed';
failureStage = 'event-audit';
const events = eventAudit(
client.notifications,
options.expectedAcknowledgement,
options.strictAllowlist,
threadId,
turnId,
);
completedStage = 'event-audit';
failureStage = 'post-turn-invariants';
assert(client.error === null, client.error ?? 'Codex app-server protocol failed');
result = {
hooks,
events,
threadParamsSha256: sha256(stableJson({ ...threadParams, cwd: '<WORKSPACE>' })),
turnParamsSha256: sha256(stableJson({ ...turnParams, threadId: '<THREAD>', input: '<PROMPT>' })),
threadId,
threadIdSha256: sha256(threadId),
turnIdSha256: sha256(turnId),
transcript: client.transcriptDigest(),
modelCalls,
turnStartCalls,
completedStage: 'post-turn-invariants',
};
completedStage = 'post-turn-invariants';
}
} catch (error) {
primaryError = error;
}
try {
await client.close();
closeClean = true;
} catch (error) {
closeError = error;
}
if (primaryError === null && closeError === null) {
try {
failureStage = 'event-audit';
const finalEvents = eventAudit(
client.notifications,
options.setupOnly ? null : options.expectedAcknowledgement,
options.strictAllowlist,
threadId,
options.setupOnly ? null : turnId,
!options.setupOnly,
);
assert(
client.error === null,
client.error ?? 'Codex app-server protocol failed after close',
);
result.events = finalEvents;
} catch (error) {
primaryError = error;
}
}
const lifecycleFailure = selectLifecycleFailure(primaryError, closeError);
if (lifecycleFailure !== null) {
const wrapped = new Error(`Codex app-server canary failed at ${sanitizeCanaryStage(
primaryError === null ? 'app-server-close' : failureStage,
) ?? 'app-server-spawn'}`);
wrapped.turnFailure = lifecycleFailure?.turnFailure ?? null;
wrapped.eventAuditFailure = sanitizeEventAuditFailure(lifecycleFailure?.eventAuditFailure);
wrapped.canaryDiagnostic = {
failureStage: sanitizeCanaryStage(primaryError === null ? 'app-server-close' : failureStage),
protocolCode: sanitizeProtocolCode(lifecycleFailure?.protocolCode),
completedStage: sanitizeCanaryStage(completedStage),
modelCalls,
turnStartCalls,
closeClean,
};
throw wrapped;
}
return {
...result,
diagnostic: {
failureStage: null,
protocolCode: null,
completedStage: result.completedStage,
modelCalls,
turnStartCalls,
closeClean,
},
};
}
async function createSentinelPng(path) {
const png = Buffer.from(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=',
'base64',
);
await writeFile(path, png);
requireRegularFile(path, 'owned sentinel PNG');
return sha256(png);
}
async function runValidationSelfTest() {
const root = join(tmpdir(), `waggle-codex-denial-self-test-${randomBytes(12).toString('hex')}`);
const codexHome = join(root, 'codex-home');
const hooksPath = join(codexHome, 'hooks.json');
let cases = 0;
const expectThrow = (run, label) => {
let threw = false;
try { run(); } catch { threw = true; }
assert(threw, `negative fixture was accepted: ${label}`);
cases += 1;
};
const captureEventAuditFailure = (run, label) => {
try {
run();
} catch (error) {
const diagnostic = sanitizeEventAuditFailure(error?.eventAuditFailure);
assert(diagnostic !== null, `event-audit fixture omitted sanitized diagnostics: ${label}`);
return diagnostic;
}
throw new Error(`event-audit fixture was accepted: ${label}`);
};
try {
assert(
stableJson([...CANARY_STAGES]) === stableJson([
'app-server-spawn',
'initialize',
'hooks-list',
'pre-turn-boundary',
'thread-start',
'turn-start',
'turn-completed',
'event-audit',
'app-server-close',
'post-turn-invariants',
]),
'canary stage fixture changed',
);
cases += 1;
assert(
sanitizeProtocolCode(-2_147_483_648) === -2_147_483_648
&& sanitizeProtocolCode(2_147_483_647) === 2_147_483_647,
'signed int32 protocol-code fixture failed',
);
cases += 1;
assert(
sanitizeProtocolCode(-2_147_483_649) === null
&& sanitizeProtocolCode(2_147_483_648) === null
&& sanitizeProtocolCode(1.5) === null
&& sanitizeProtocolCode('1') === null,
'invalid protocol-code fixture was accepted',
);
cases += 1;
const inventoryArguments = buildMcpInventoryArguments();
assert(
inventoryArguments.length === (DISABLED_FEATURES.length * 2) + 3
&& JSON.stringify(inventoryArguments.slice(-3)) === JSON.stringify(['mcp', 'list', '--json'])
&& DISABLED_FEATURES.every((feature, index) => (
inventoryArguments[index * 2] === '--disable'
&& inventoryArguments[(index * 2) + 1] === feature
)),
'MCP inventory did not inherit the app-server feature-disable boundary',
);
cases += 1;
assert(
win32.join('C:\\Windows', 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe')
=== 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe',
'package-command Windows PowerShell spelling fixture failed',
);
await mkdir(codexHome, { recursive: true });
await writeFile(hooksPath, '{}\n', 'utf8');
const expectedCommands = new Map();
const hooks = WAGGLE_HOOK_EVENTS.map((eventName, index) => {
const command = `fixture-waggle-command-${index}`;
expectedCommands.set(eventName, sha256(command));
return {
eventName,
sourcePath: hooksPath,
key: `fixture-waggle-key-${index}`,
currentHash: `sha256:${String(index + 1).padStart(64, '0')}`,
command,
enabled: true,
trustStatus: 'trusted',
};
});
const denyCommand = 'fixture-deny-command';
hooks.push({
eventName: 'preToolUse',
sourcePath: 'C:\\<session-flags>\\config.toml',
key: 'fixture-deny-key',
currentHash: `sha256:${'f'.repeat(64)}`,
command: denyCommand,
enabled: true,
trustStatus: 'trusted',
});
const validEntry = { hooks, warnings: [], errors: [] };
const valid = assertExactHookGraph(
validEntry,
codexHome,
true,
true,
sha256(denyCommand),
expectedCommands,
false,
);
assert(valid.graph.length === 5, 'valid hook fixture did not pass');
cases += 1;
const withExtra = structuredClone(validEntry);
withExtra.hooks.push({ ...withExtra.hooks[0], key: 'extra-key' });
expectThrow(() => assertExactHookGraph(
withExtra, codexHome, true, true, sha256(denyCommand), expectedCommands, false,
), 'extra hook');
const activeExternal = structuredClone(validEntry);
activeExternal.hooks.push({
...activeExternal.hooks[0],
sourcePath: 'C:\\external\\hooks.json',
key: 'external-key',
command: 'external-command',
});
expectThrow(() => assertExactHookGraph(
activeExternal, codexHome, true, true, sha256(denyCommand), expectedCommands, false,
), 'active external hook');
const disabledExternal = structuredClone(activeExternal);
disabledExternal.hooks[5].enabled = false;
const disabledResult = assertExactHookGraph(
disabledExternal, codexHome, true, true, sha256(denyCommand), expectedCommands, false,
);
assert(disabledResult.extraCount === 0 && disabledResult.disabledExtraCount === 1,
'disabled external hook fixture did not pass');
cases += 1;
const wrongDenySource = structuredClone(validEntry);
wrongDenySource.hooks[4].sourcePath = 'C:\\temp\\config.toml';
expectThrow(() => assertExactHookGraph(
wrongDenySource, codexHome, true, true, sha256(denyCommand), expectedCommands, false,
), 'deny source');
const wrongDenyCommand = structuredClone(validEntry);
wrongDenyCommand.hooks[4].command = 'different-deny-command';
expectThrow(() => assertExactHookGraph(
wrongDenyCommand, codexHome, true, true, sha256(denyCommand), expectedCommands, false,
), 'deny command');
const wrongWaggleCommand = structuredClone(validEntry);
wrongWaggleCommand.hooks[0].command = 'different-waggle-command';
expectThrow(() => assertExactHookGraph(
wrongWaggleCommand, codexHome, true, true, sha256(denyCommand), expectedCommands, false,
), 'packaged Waggle command');
const untrusted = structuredClone(validEntry);
untrusted.hooks[0].trustStatus = 'untrusted';
expectThrow(() => assertExactHookGraph(
untrusted, codexHome, true, true, sha256(denyCommand), expectedCommands, false,
), 'untrusted hook');
const acknowledgement = 'SELF_TEST_ACK';
const safeNotification = {
sequence: 1,
message: {
method: 'item/completed',
params: { item: { type: 'agentMessage', text: acknowledgement } },
},
};
assert(eventAudit([safeNotification], acknowledgement, true).normalTextCompleted, 'safe event fixture did not pass');
cases += 1;
for (const itemTypeValue of [
'commandExecution',
'computerUse',
'dynamicToolCall',
'fileChange',
'imageGeneration',
'imageView',
'mcpToolCall',
'toolSearch',
'webSearch',
]) {
const bad = {
sequence: 2,
message: { method: 'item/completed', params: { item: { type: itemTypeValue } } },
};
expectThrow(() => eventAudit([safeNotification, bad], acknowledgement, true), itemTypeValue);
}
const unknownNotification = { sequence: 2, message: { method: 'future/tool/event', params: {} } };
expectThrow(
() => eventAudit([safeNotification, unknownNotification], acknowledgement, true),
'unknown notification',
);
const failureNotification = { sequence: 2, message: { method: 'warning', params: {} } };
expectThrow(
() => eventAudit([safeNotification, failureNotification], acknowledgement, true),
'failure notification',
);
const projectTrustWarning = {
sequence: 2,
message: {
method: 'configWarning',
params: {
summary: `${FAIL_CLOSED_PROJECT_TRUST_WARNING}\n`
+ ' 1. C:\\fixture\\.codex\n'
+ ' To load project-local config, hooks, and exec policies, add c:\\fixture as a trusted project in C:\\Users\\fixture\\.codex\\config.toml.\n',
details: null,
path: null,
range: null,
},
},
};
const remoteControlStatus = {
sequence: 3,
message: { method: 'remoteControl/status/changed', params: { status: 'disconnected' } },
};
const failClosedTrustAudit = eventAudit(
[safeNotification, projectTrustWarning, remoteControlStatus],
acknowledgement,
true,
);
assert(
failClosedTrustAudit.failureEventsObserved === 0
&& failClosedTrustAudit.unknownEventsObserved === 0,
'fail-closed project trust warning or schema-backed remote-control status was rejected',
);
cases += 1;
const unsafeTrustWarning = {
...projectTrustWarning,
message: {
...projectTrustWarning.message,
params: { ...projectTrustWarning.message.params, details: 'unexpected detail' },
},
};
const unsafeTrustDiagnostic = captureEventAuditFailure(
() => eventAudit([safeNotification, unsafeTrustWarning], acknowledgement, true),
'unsafe project trust warning',
);
assert(
unsafeTrustDiagnostic.invariant === 'failure-notification'
&& stableJson(unsafeTrustDiagnostic.rejectedMethodSha256) === stableJson([
{ sha256: sha256('configWarning'), count: 1 },
]),
'non-canonical project trust warning was not rejected',
);
cases += 1;
const appendedTrustWarning = {
...projectTrustWarning,
message: {
...projectTrustWarning.message,
params: {
...projectTrustWarning.message.params,
summary: `${projectTrustWarning.message.params.summary}unexpected suffix\n`,
},
},
};
expectThrow(
() => eventAudit([safeNotification, appendedTrustWarning], acknowledgement, true),
'appended project trust warning',
);
const extraParamTrustWarning = {
...projectTrustWarning,
message: {
...projectTrustWarning.message,
params: { ...projectTrustWarning.message.params, extra: null },
},
};
expectThrow(
() => eventAudit([safeNotification, extraParamTrustWarning], acknowledgement, true),
'extra-param project trust warning',
);
const nearMatchTrustWarning = {
...projectTrustWarning,
message: {
...projectTrustWarning.message,
params: {
...projectTrustWarning.message.params,
summary: projectTrustWarning.message.params.summary.replace(
FAIL_CLOSED_PROJECT_TRUST_WARNING,
`${FAIL_CLOSED_PROJECT_TRUST_WARNING} Additional guidance follows.`,
),
},
},
};
expectThrow(
() => eventAudit([safeNotification, nearMatchTrustWarning], acknowledgement, true),
'near-match project trust warning',
);
const unknownDiagnostic = captureEventAuditFailure(
() => eventAudit([safeNotification, unknownNotification], acknowledgement, true),
'unknown diagnostic',
);
assert(
unknownDiagnostic.invariant === 'unknown-notification-method'
&& unknownDiagnostic.notificationCount === 2
&& unknownDiagnostic.unknownNotificationEventsObserved === 1
&& stableJson(unknownDiagnostic.rejectedMethodSha256) === stableJson([
{ sha256: sha256('future/tool/event'), count: 1 },
])
&& Object.keys(unknownDiagnostic).join(',') === EVENT_AUDIT_FAILURE_KEYS.join(',')
&& !stableJson(unknownDiagnostic).includes('future/tool/event'),
'unknown notification diagnostic was not fixed, hashed, and path-free',
);
cases += 1;
const secretVariant = {
sequence: 2,
message: {
method: 'future/tool/event',
params: { text: 'sk-private-value', path: 'C:\\private\\auth.json', sessionId: 'sess-private' },
},
};
const secretDiagnostic = captureEventAuditFailure(
() => eventAudit([safeNotification, secretVariant], acknowledgement, true),
'secret-independent diagnostic',
);
assert(
stableJson(secretDiagnostic) === stableJson(unknownDiagnostic),
'event-audit diagnostic changed when only untrusted params changed',
);
cases += 1;
const precedenceDiagnostic = captureEventAuditFailure(
() => eventAudit([safeNotification, unknownNotification, failureNotification], acknowledgement, true),
'failure precedence',
);
assert(
precedenceDiagnostic.invariant === 'failure-notification'
&& stableJson(precedenceDiagnostic.rejectedMethodSha256) === stableJson([
{ sha256: sha256('warning'), count: 1 },
]),
'failure notification did not retain precedence and hashed evidence',
);
cases += 1;
const unknownItem = {
sequence: 2,
message: { method: 'item/completed', params: { item: { type: 'futureItem' } } },
};
const unknownItemDiagnostic = captureEventAuditFailure(
() => eventAudit([safeNotification, unknownItem], acknowledgement, true),
'unknown item type',
);
assert(
unknownItemDiagnostic.invariant === 'unknown-item-type'
&& stableJson(unknownItemDiagnostic.rejectedItemTypeSha256) === stableJson([
{ sha256: sha256('futureItem'), count: 1 },
]),
'unknown item diagnostic did not retain only hashed type evidence',
);
cases += 1;
assert(
sanitizeEventAuditFailure({ ...unknownDiagnostic, notificationCount: '2' }) === null,
'event-audit diagnostic accepted a non-integer count',
);
cases += 1;
assert(
sanitizeEventAuditFailure({ ...unknownDiagnostic, params: { text: 'private' } }) === null,
'event-audit diagnostic accepted an unexpected raw property',
);
cases += 1;
const benignNotifications = [
'account/updated',
'mcpServer/startupStatus/updated',
'model/safetyBuffering/updated',
'model/verification',
'thread/name/updated',
'thread/settings/updated',
'turn/moderationMetadata',
].map((method, index) => ({ sequence: index + 2, message: { method, params: {} } }));
assert(
eventAudit([safeNotification, ...benignNotifications], acknowledgement, true).unknownEventsObserved === 0,
'schema-backed benign notification fixture failed',
);
cases += 1;
const wrongScope = {
sequence: 2,
message: {
method: 'item/updated',
params: { threadId: 'wrong-thread', turnId: 'turn-1', item: { type: 'reasoning' } },
},
};
expectThrow(
() => eventAudit([safeNotification, wrongScope], acknowledgement, true, 'thread-1', 'turn-1'),
'cross-turn notification',
);
const reroute = { sequence: 2, message: { method: 'model/rerouted', params: {} } };
expectThrow(
() => eventAudit([safeNotification, reroute], acknowledgement, false),
'model reroute',
);
const mcpBoundary = {
count: 3,
namesSha256: 'a'.repeat(64),
configSha256: 'b'.repeat(64),
boundarySha256: 'c'.repeat(64),
};
assertSameMcpBoundary(mcpBoundary, { ...mcpBoundary }, 'self-test');
cases += 1;
expectThrow(
() => assertSameMcpBoundary(mcpBoundary, { ...mcpBoundary, configSha256: 'd'.repeat(64) }, 'self-test'),
'MCP boundary drift',
);
const simpleFailure = sanitizeTurnFailure({
status: 'failed',
error: { codexErrorInfo: 'unauthorized' },
});
assert(
stableJson(simpleFailure) === stableJson({
status: 'failed', code: 'unauthorized', httpStatusCode: null, willRetry: null,
}),
'simple terminal failure was not normalized',
);
cases += 1;
const httpFailure = sanitizeTurnFailure({ status: 'interrupted' }, {
message: {
method: 'error',
params: {
willRetry: false,
error: {
codexErrorInfo: { responseStreamDisconnected: { httpStatusCode: 429 } },
},
},
},
});
assert(
stableJson(httpFailure) === stableJson({
status: 'interrupted',
code: 'responseStreamDisconnected',
httpStatusCode: 429,
willRetry: false,
}),
'HTTP terminal failure was not normalized',
);
cases += 1;
const maliciousFailure = sanitizeTurnFailure({
status: 'failed',
error: {
message: 'C:\\secret\\auth.json sk-fixture',
additionalDetails: 'file:///private/detail',
codexErrorInfo: { activeTurnNotSteerable: { turnKind: 'review' } },
},
});
assert(
Object.keys(maliciousFailure).sort().join(',') === 'code,httpStatusCode,status,willRetry'
&& !JSON.stringify(maliciousFailure).includes('secret'),
'terminal failure retained untrusted text',
);
cases += 1;
assert(sanitizeTurnFailure({ status: 'completed' }).status === null, 'completed turn was not safely normalized');
cases += 1;
assert(sanitizeTurnFailure({ status: 'inProgress' }).status === null, 'in-progress turn was not safely normalized');
cases += 1;
assert(sanitizeTurnFailure({
status: 'failed', error: { codexErrorInfo: 'futureUnknownCode' },
}).code === null, 'unknown terminal error code was not safely normalized');
cases += 1;
assert(sanitizeTurnFailure({
status: 'failed',
error: {
codexErrorInfo: {
responseStreamDisconnected: { httpStatusCode: 429 },
httpConnectionFailed: { httpStatusCode: 503 },
},
},
}).code === null, 'multi-key terminal error was not safely normalized');
cases += 1;
assert(sanitizeTurnFailure({
status: 'failed',
error: {
codexErrorInfo: { httpConnectionFailed: { httpStatusCode: 70_000 } },
},
}).code === null, 'invalid terminal HTTP status was not safely normalized');
cases += 1;
const disagreeingFailure = sanitizeTurnFailure({
status: 'failed',
error: { codexErrorInfo: 'unauthorized' },
}, {
message: {
method: 'error',
params: { error: { codexErrorInfo: 'usageLimitExceeded' }, willRetry: false },
},
});
assert(
disagreeingFailure.code === 'unauthorized' && disagreeingFailure.willRetry === false,
'final turn was not authoritative over a disagreeing notification',
);
cases += 1;
const malformedFailure = sanitizeTurnFailure(null, {
message: { method: 'futureError', params: { willRetry: 'yes', error: { codexErrorInfo: 7 } } },
});
assert(
stableJson(malformedFailure) === stableJson({
status: null, code: null, httpStatusCode: null, willRetry: null,
}),
'malformed terminal failure was not totally normalized',
);
cases += 1;
const primaryFixture = new Error('primary');
const closeFixture = new Error('close');
assert(
selectLifecycleFailure(primaryFixture, closeFixture) === primaryFixture,
'close failure replaced the primary lifecycle failure',
);
cases += 1;
const requiredArgs = [
'--codex-exe', 'C:\\codex.exe',
'--expected-head', 'a'.repeat(40),
'--hive-mind-cli', 'C:\\hive-mind.exe',
'--model', 'fixture-model',
'--receipt-dir', 'C:\\receipt',
'--windows-powershell', 'C:\\powershell.exe',
'--workspace', 'C:\\workspace',
];
assert(parseArgs([...requiredArgs, '--setup-only'])['setup-only'] === true, 'setup-only fixture failed');
cases += 1;
expectThrow(
() => parseArgs([
...requiredArgs,
'--setup-only',
'--execute-paid',
'--marker', 'fixture',
'--ack', PAID_ACK,
]),
'setup-only paid conflict',
);
const officialProviderArguments = buildInvocationArguments({
mcpServerNames: [],
catalogPath: 'C:\\models.json',
providerBaseUrl: null,
includeDeny: false,
hookDisables: [],
hookPins: [],
});
const officialProviderOverrides = officialProviderArguments.filter((argument) => (
argument.startsWith('model_provider=') || argument.startsWith('model_providers.')
));
assert(
stableJson(officialProviderOverrides) === stableJson(['model_provider="openai"']),
'official provider arguments redefined the reserved built-in OpenAI provider',
);
cases += 1;
assert(cases === 54, `expected exactly 54 self-test cases, observed ${cases}`);
return { pass: true, paidCalls: 0, cases };
} finally {
await rm(root, { recursive: true, force: true });
}
}
const flags = parseArgs(process.argv.slice(2));
if (flags['self-test']) {
process.stdout.write(`${JSON.stringify(await runValidationSelfTest())}\n`);
process.exit(0);
}
const codexExecutable = resolve(flags['codex-exe']);
const hiveMindCli = resolve(flags['hive-mind-cli']);
const receiptDir = resolve(flags['receipt-dir']);
const windowsPowerShell = resolve(flags['windows-powershell']);
const workspace = resolve(flags.workspace);
const model = flags.model;
const expectedHead = flags['expected-head'];
const executePaid = flags['execute-paid'] === true;
const setupOnly = flags['setup-only'] === true;
const initialExecutableSha256 = existsSync(codexExecutable) ? sha256File(codexExecutable) : null;
const initialScriptSha256 = sha256File(SCRIPT_PATH);
const report = {
schemaVersion: 1,
kind: 'codex-tool-denial-and-official-auth',
pass: false,
paidCalls: 0,
proof: { executed: false, paidCalls: 0, pass: null },
source: {
expectedHead,
observedHead: null,
tree: null,
scriptBlob: null,
trackedClean: false,
unchanged: false,
},
executable: {
version: null,
sha256: initialExecutableSha256,
unchanged: false,
},
model: {
idSha256: sha256(model),
bundledCatalogSha256: null,
},
modelCatalog: {
sealedSha256: null,
controlSha256: null,
},
invocation: {
argumentsSha256: null,
configSha256: sha256(stableJson(CONFIG_OVERRIDES)),
threadParamsSha256: null,
turnParamsSha256: null,
mcpServerCount: null,
mcpServerNamesSha256: null,
},
hooks: {
expectedCount: 5,
extraCount: null,
artifactsSha256: null,
cliSha256: null,
graphSha256: null,
denyHookSha256: null,
warnings: null,
errors: null,
allTrusted: false,
unchangedAfterPin: false,
artifactsUnchanged: false,
},
mcpBoundary: {
count: null,
namesSha256: null,
initialSha256: null,
prePaidSha256: null,
postPaidSha256: null,
initialConfigSha256: null,
prePaidConfigSha256: null,
postPaidConfigSha256: null,
unchanged: false,
},
sealed: null,
red: null,
green: null,
paidInvocation: {
attempted: false,
executed: false,
arguments: [],
argumentsSha256: null,
modelCatalogSha256: null,
hookGraphSha256: null,
markerSha256: null,
sessionIdSha256: null,
threadParamsSha256: null,
turnParamsSha256: null,
normalTextCompleted: false,
toolEventsObserved: 0,
unknownEventsObserved: 0,
},
artifacts: {
scriptSha256: initialScriptSha256,
scriptUnchanged: false,
windowsPowerShellSha256: null,
windowsSystemDirectorySha256: null,
workspaceSha256: sha256(canonicalPath(workspace)),
reportPathHash: sha256(join(receiptDir, 'report.json').toLowerCase()),
tempRemoved: false,
},
diagnostic: {
failureStage: null,
protocolCode: null,
completedStage: null,
modelCalls: 0,
turnStartCalls: 0,
closeClean: false,
},
turnFailure: null,
eventAuditFailure: null,
error: null,
};
let rawPaidSessionId = null;
let markerMatched = !executePaid;
let tempRoot = null;
let initialSourceSnapshot = null;
let initialMcpBoundary = null;
const providers = [];
let receiptCreated = false;
try {
assert(process.platform === 'win32', 'this proof is Windows-only');
assert(isAbsolute(flags['codex-exe']), '--codex-exe must be absolute');
assert(isAbsolute(flags['hive-mind-cli']), '--hive-mind-cli must be absolute');
assert(isAbsolute(flags['receipt-dir']), '--receipt-dir must be absolute');
assert(isAbsolute(flags['windows-powershell']), '--windows-powershell must be absolute');
assert(isAbsolute(flags.workspace), '--workspace must be absolute');
requireRegularFile(codexExecutable, 'Codex executable');
requireRegularFile(hiveMindCli, 'hive-mind CLI');
requireDirectory(workspace, 'workspace');
initialSourceSnapshot = verifySourceSnapshot(expectedHead);
Object.assign(report.source, initialSourceSnapshot);
assert(!existsSync(receiptDir), '--receipt-dir must be fresh');
assertNoExistingReparsePoint(dirname(receiptDir));
await mkdir(receiptDir, { recursive: false });
receiptCreated = true;
const codexHome = resolve(process.env.CODEX_HOME || join(homedir(), '.codex'));
requireDirectory(codexHome, 'user Codex profile');
requireRegularFile(join(codexHome, 'hooks.json'), 'user Codex hooks.json');
const codexConfigPath = join(codexHome, 'config.toml');
requireRegularFile(codexConfigPath, 'user Codex config.toml');
const version = runChecked(codexExecutable, ['--version'], {
label: 'Codex version',
timeoutMs: 20_000,
}).stdout.trim();
assert(/^codex(?:-cli)?\s/.test(version), 'Codex returned an unexpected version');
report.executable.version = version;
tempRoot = join(tmpdir(), `waggle-codex-denial-${randomBytes(12).toString('hex')}`);
await mkdir(tempRoot, { recursive: false });
await mkdir(join(tempRoot, 'mind'), { recursive: true });
await mkdir(join(tempRoot, 'process-temp'), { recursive: true });
const env = buildChildEnvironment(tempRoot);
initialMcpBoundary = captureMcpBoundary(codexExecutable, env, codexConfigPath);
const mcpServerNames = readConfiguredMcpServerNames(codexExecutable, env);
assert(
initialMcpBoundary.namesSha256 === sha256(stableJson(mcpServerNames)),
'initial MCP inventory changed while being captured',
);
report.invocation.mcpServerCount = mcpServerNames.length;
report.invocation.mcpServerNamesSha256 = sha256(stableJson(mcpServerNames));
report.mcpBoundary.count = initialMcpBoundary.count;
report.mcpBoundary.namesSha256 = initialMcpBoundary.namesSha256;
report.mcpBoundary.initialSha256 = initialMcpBoundary.boundarySha256;
report.mcpBoundary.initialConfigSha256 = initialMcpBoundary.configSha256;
const sentinelPath = join(tempRoot, 'owned-sentinel.png');
const sentinelSha256 = await createSentinelPng(sentinelPath);
const { selected, bundledSha256 } = readBundledModel(codexExecutable, model);
report.model.bundledCatalogSha256 = bundledSha256;
const sealedCatalogPath = join(tempRoot, 'sealed-models.json');
const controlCatalogPath = join(tempRoot, 'control-models.json');
report.modelCatalog.sealedSha256 = await writeCatalog(sealedCatalogPath, mutateModel(selected, true));
const controlModel = `waggle-control-${sha256(model).slice(0, 16)}`;
const controlCatalogModel = mutateModel(selected, false);
controlCatalogModel.slug = controlModel;
report.modelCatalog.controlSha256 = await writeCatalog(controlCatalogPath, controlCatalogModel);
const trustedPowerShell = verifyWindowsPowerShell(windowsPowerShell);
report.artifacts.windowsPowerShellSha256 = trustedPowerShell.sha256;
report.artifacts.windowsSystemDirectorySha256 = trustedPowerShell.systemDirectorySha256;
const expectedWaggleHooks = buildExpectedWaggleHooks(trustedPowerShell.path, hiveMindCli);
const denial = buildDenyHookConfig(trustedPowerShell.path);
const replacements = [
codexExecutable,
hiveMindCli,
receiptDir,
workspace,
codexHome,
tempRoot,
sentinelPath,
trustedPowerShell.path,
flags.marker,
];
const discoveryArgs = buildInvocationArguments({
mcpServerNames,
catalogPath: sealedCatalogPath,
providerBaseUrl: 'http://127.0.0.1:9/v1',
includeDeny: true,
denyConfig: denial.config,
hookPins: [],
});
const discovered = await listHooks(
codexExecutable,
discoveryArgs,
workspace,
env,
codexHome,
true,
false,
denial.commandSha256,
expectedWaggleHooks.commands,
true,
replacements,
);
const allPins = discovered.expected.map(({ key, currentHash }) => ({ key, currentHash }));
const wagglePins = discovered.expected
.filter(({ eventName }) => eventName !== 'preToolUse')
.map(({ key, currentHash }) => ({ key, currentHash }));
const externalHookDisables = discovered.extras.map(({ key }) => key);
const pinnedDiscoveryArgs = buildInvocationArguments({
mcpServerNames,
catalogPath: sealedCatalogPath,
providerBaseUrl: 'http://127.0.0.1:9/v1',
includeDeny: true,
denyConfig: denial.config,
hookDisables: externalHookDisables,
hookPins: allPins,
});
const pinned = await listHooks(
codexExecutable,
pinnedDiscoveryArgs,
workspace,
env,
codexHome,
true,
true,
denial.commandSha256,
expectedWaggleHooks.commands,
false,
replacements,
);
assert(discovered.identitySha256 === pinned.identitySha256, 'hook graph identity changed while hash-pinning');
assert(pinned.denyHookSha256 !== null, 'deny hook hash is missing');
report.hooks = {
expectedCount: 5,
extraCount: pinned.extraCount,
disabledExtraCount: pinned.disabledExtraCount,
artifactsSha256: expectedWaggleHooks.artifactsSha256,
cliSha256: expectedWaggleHooks.cliSha256,
graphSha256: pinned.graphSha256,
denyHookSha256: pinned.denyHookSha256,
warnings: pinned.warnings,
errors: pinned.errors,
allTrusted: pinned.expected.every((hook) => hook.trustStatus === 'trusted'),
unchangedAfterPin: discovered.identitySha256 === pinned.identitySha256,
artifactsUnchanged: false,
};
if (!setupOnly) {
const sealedAck = `WAGGLE_SEALED_ACK_${randomBytes(24).toString('hex')}`;
const sealedProvider = await startLoopbackProvider('sealed', sealedAck, sentinelPath);
providers.push(sealedProvider);
const sealedArgs = buildInvocationArguments({
mcpServerNames,
catalogPath: sealedCatalogPath,
providerBaseUrl: sealedProvider.baseUrl,
includeDeny: true,
denyConfig: denial.config,
hookDisables: externalHookDisables,
hookPins: allPins,
});
const sealedRun = await runTurn({
executable: codexExecutable,
args: sealedArgs,
workspace,
env,
replacements,
codexHome,
includeDeny: true,
expectedDenyCommandSha256: denial.commandSha256,
expectedWaggleCommands: expectedWaggleHooks.commands,
expectedHookGraphSha256: pinned.graphSha256,
lane: 'sealed',
model,
prompt: `Return exactly ${sealedAck} and nothing else. Do not call tools.`,
expectedAcknowledgement: sealedAck,
strictAllowlist: true,
});
await sealedProvider.close();
providers.splice(providers.indexOf(sealedProvider), 1);
assert(sealedProvider.records.length === 1, `sealed provider saw ${sealedProvider.records.length} requests`);
const sealedRequest = sealedProvider.records[0];
assert(sealedRequest.topLevelToolsIsArray, 'sealed provider request omitted the top-level tools array');
assert(
sealedRequest.toolNames.length === 0,
`sealed provider request exposed top-level tools: ${JSON.stringify(sealedRequest.toolNames)}`,
);
assert(sealedRequest.additionalToolCount === 0, 'sealed provider request exposed input additional_tools');
report.sealed = {
topLevelToolCount: sealedRequest.toolNames.length,
additionalToolCount: sealedRequest.additionalToolCount,
emptySchema: true,
normalTextCompleted: sealedRun.events.normalTextCompleted,
providerRequestSha256: sealedRequest.sha256,
};
const controlAck = `WAGGLE_CONTROL_ACK_${randomBytes(24).toString('hex')}`;
const redProvider = await startLoopbackProvider('red', controlAck, sentinelPath);
providers.push(redProvider);
const redArgs = buildInvocationArguments({
mcpServerNames,
catalogPath: controlCatalogPath,
providerBaseUrl: redProvider.baseUrl,
includeDeny: false,
denyConfig: denial.config,
hookDisables: externalHookDisables,
hookPins: wagglePins,
});
const redDiscovery = await listHooks(
codexExecutable,
redArgs,
workspace,
env,
codexHome,
false,
true,
null,
expectedWaggleHooks.commands,
false,
replacements,
);
const redRun = await runTurn({
executable: codexExecutable,
args: redArgs,
workspace,
env,
replacements,
codexHome,
includeDeny: false,
expectedDenyCommandSha256: null,
expectedWaggleCommands: expectedWaggleHooks.commands,
expectedHookGraphSha256: redDiscovery.graphSha256,
useDefaultEnvironmentForControl: true,
lane: 'red',
model: controlModel,
prompt: 'Follow the provider tool call, then return only the provider acknowledgement.',
expectedAcknowledgement: controlAck,
strictAllowlist: false,
});
await redProvider.close();
providers.splice(providers.indexOf(redProvider), 1);
assert(redProvider.records.length === 2, `RED provider saw ${redProvider.records.length} requests`);
const redTools = redProvider.records[0].toolNames;
assert(
JSON.stringify(redTools) === JSON.stringify(['view_image']),
`RED control catalog did not expose only view_image: ${JSON.stringify(redTools)}`,
);
const redSensitiveDataObserved = redProvider.records[1]?.containsImage === true;
const redReadObserved = redRun.events.imageItemCount > 0 || redSensitiveDataObserved;
const redToolLoopObserved = redProvider.records.length === 2 && redRun.events.normalTextCompleted;
report.red = {
toolNames: redTools,
recordCount: redProvider.records.length,
toolLoopObserved: redToolLoopObserved,
readObserved: redReadObserved,
sensitiveDataObserved: redSensitiveDataObserved,
imageItemCount: redRun.events.imageItemCount,
normalTextCompleted: redRun.events.normalTextCompleted,
sentinelSha256,
};
assert(redToolLoopObserved, 'RED control did not prove the provider-routed view_image tool loop');
const greenProvider = await startLoopbackProvider('green', controlAck, sentinelPath);
providers.push(greenProvider);
const greenArgs = buildInvocationArguments({
mcpServerNames,
catalogPath: controlCatalogPath,
providerBaseUrl: greenProvider.baseUrl,
includeDeny: true,
denyConfig: denial.config,
hookDisables: externalHookDisables,
hookPins: allPins,
});
const greenRun = await runTurn({
executable: codexExecutable,
args: greenArgs,
workspace,
env,
replacements,
codexHome,
includeDeny: true,
expectedDenyCommandSha256: denial.commandSha256,
expectedWaggleCommands: expectedWaggleHooks.commands,
expectedHookGraphSha256: pinned.graphSha256,
useDefaultEnvironmentForControl: true,
lane: 'green',
model: controlModel,
prompt: 'Follow the provider tool call, then return only the provider acknowledgement.',
expectedAcknowledgement: controlAck,
strictAllowlist: false,
});
await greenProvider.close();
providers.splice(providers.indexOf(greenProvider), 1);
assert(greenProvider.records.length === 2, `GREEN provider saw ${greenProvider.records.length} requests`);
assert(
JSON.stringify(greenProvider.records[0].toolNames) === JSON.stringify(['view_image']),
`GREEN control catalog did not expose only view_image: ${JSON.stringify(greenProvider.records[0].toolNames)}`,
);
const greenImageObserved = greenProvider.records.slice(1).some((record) => record.containsImage);
const deniedBeforeRead = greenRun.events.denyHookCount === 1
&& !greenImageObserved
&& (
greenRun.events.firstImageSequence === null
|| greenRun.events.firstDenySequence < greenRun.events.firstImageSequence
);
assert(deniedBeforeRead, 'GREEN hook did not deny view_image before sentinel access');
report.green = {
deniedBeforeRead,
sensitiveDataObserved: false,
normalTextCompleted: greenRun.events.normalTextCompleted,
denialReasonSha256: sha256(DENIAL_REASON),
denyHookReceipts: greenRun.events.denyHookCount,
};
report.invocation.argumentsSha256 = sha256(stableJson(normalizedArguments(greenArgs, [
[sealedCatalogPath, '<MODEL_CATALOG>'],
[controlCatalogPath, '<MODEL_CATALOG>'],
[greenProvider.baseUrl, '<LOOPBACK>'],
[denial.command, '<DENY_COMMAND>'],
])));
report.invocation.threadParamsSha256 = sealedRun.threadParamsSha256;
report.invocation.turnParamsSha256 = sealedRun.turnParamsSha256;
report.proof = { executed: true, paidCalls: 0, pass: true };
}
if (setupOnly) {
const setupArgs = buildInvocationArguments({
mcpServerNames,
catalogPath: sealedCatalogPath,
providerBaseUrl: null,
includeDeny: true,
denyConfig: denial.config,
hookDisables: externalHookDisables,
hookPins: allPins,
});
let preSetupMcpBoundary = null;
const setupRun = await runTurn({
executable: codexExecutable,
args: setupArgs,
workspace,
env,
replacements,
codexHome,
includeDeny: true,
expectedDenyCommandSha256: denial.commandSha256,
expectedWaggleCommands: expectedWaggleHooks.commands,
expectedHookGraphSha256: pinned.graphSha256,
beforeTurn: async () => {
preSetupMcpBoundary = captureMcpBoundary(codexExecutable, env, codexConfigPath);
assertSameMcpBoundary(initialMcpBoundary, preSetupMcpBoundary, 'before setup-only thread/start');
report.mcpBoundary.prePaidSha256 = preSetupMcpBoundary.boundarySha256;
report.mcpBoundary.prePaidConfigSha256 = preSetupMcpBoundary.configSha256;
},
lane: 'setup',
model,
setupOnly: true,
strictAllowlist: true,
});
assert(preSetupMcpBoundary !== null, 'setup-only MCP/config boundary was not captured');
const postSetupMcpBoundary = captureMcpBoundary(codexExecutable, env, codexConfigPath);
assertSameMcpBoundary(initialMcpBoundary, postSetupMcpBoundary, 'after setup-only thread/start');
report.mcpBoundary.postPaidSha256 = postSetupMcpBoundary.boundarySha256;
report.mcpBoundary.postPaidConfigSha256 = postSetupMcpBoundary.configSha256;
report.mcpBoundary.unchanged = true;
report.diagnostic = setupRun.diagnostic;
report.invocation.argumentsSha256 = sha256(stableJson(normalizedArguments(setupArgs, [
[sealedCatalogPath, '<MODEL_CATALOG>'],
[denial.command, '<DENY_COMMAND>'],
])));
report.invocation.threadParamsSha256 = setupRun.threadParamsSha256;
report.invocation.turnParamsSha256 = null;
} else if (executePaid) {
const captureMindRoot = process.env.HIVE_MIND_DATA_DIR;
const captureWorkspaceId = process.env.WAGGLE_WORKSPACE_ID;
assert(
typeof captureMindRoot === 'string' && isAbsolute(captureMindRoot),
'paid lane requires an absolute HIVE_MIND_DATA_DIR from the parent harness',
);
requireDirectory(captureMindRoot, 'paid capture mind root');
assert(
typeof captureWorkspaceId === 'string'
&& /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(captureWorkspaceId),
'paid lane requires a safe WAGGLE_WORKSPACE_ID from the parent harness',
);
const paidEnv = {
...env,
HIVE_MIND_DATA_DIR: captureMindRoot,
WAGGLE_WORKSPACE_ID: captureWorkspaceId,
};
const paidArgs = buildInvocationArguments({
mcpServerNames,
catalogPath: sealedCatalogPath,
providerBaseUrl: null,
includeDeny: true,
denyConfig: denial.config,
hookDisables: externalHookDisables,
hookPins: allPins,
});
report.paidCalls = 1;
report.paidInvocation.attempted = true;
let prePaidMcpBoundary = null;
const paidRun = await runTurn({
executable: codexExecutable,
args: paidArgs,
workspace,
env: paidEnv,
replacements,
codexHome,
includeDeny: true,
expectedDenyCommandSha256: denial.commandSha256,
expectedWaggleCommands: expectedWaggleHooks.commands,
expectedHookGraphSha256: pinned.graphSha256,
beforeTurn: async () => {
prePaidMcpBoundary = captureMcpBoundary(codexExecutable, paidEnv, codexConfigPath);
assertSameMcpBoundary(initialMcpBoundary, prePaidMcpBoundary, 'before the paid turn');
report.mcpBoundary.prePaidSha256 = prePaidMcpBoundary.boundarySha256;
report.mcpBoundary.prePaidConfigSha256 = prePaidMcpBoundary.configSha256;
},
lane: 'paid',
model,
prompt: `Return exactly ${flags.marker} and nothing else. Do not call tools.`,
expectedAcknowledgement: flags.marker,
strictAllowlist: true,
});
report.diagnostic = paidRun.diagnostic;
assert(prePaidMcpBoundary !== null, 'paid MCP/config boundary was not captured');
const postPaidMcpBoundary = captureMcpBoundary(codexExecutable, paidEnv, codexConfigPath);
assertSameMcpBoundary(initialMcpBoundary, postPaidMcpBoundary, 'after the paid turn');
report.mcpBoundary.postPaidSha256 = postPaidMcpBoundary.boundarySha256;
report.mcpBoundary.postPaidConfigSha256 = postPaidMcpBoundary.configSha256;
report.mcpBoundary.unchanged = true;
rawPaidSessionId = paidRun.threadId;
markerMatched = paidRun.events.normalTextCompleted;
const paidNormalizedArguments = normalizedArguments(paidArgs, [
[sealedCatalogPath, '<MODEL_CATALOG>'],
[denial.command, '<DENY_COMMAND>'],
]);
report.paidInvocation = {
attempted: true,
executed: true,
arguments: paidNormalizedArguments,
argumentsSha256: sha256(stableJson(paidNormalizedArguments)),
modelCatalogSha256: report.modelCatalog.sealedSha256,
hookGraphSha256: paidRun.hooks.graphSha256,
markerSha256: sha256(flags.marker),
sessionIdSha256: paidRun.threadIdSha256,
threadParamsSha256: paidRun.threadParamsSha256,
turnParamsSha256: paidRun.turnParamsSha256,
normalTextCompleted: paidRun.events.normalTextCompleted,
toolEventsObserved: paidRun.events.toolEventsObserved,
unknownEventsObserved: paidRun.events.unknownEventsObserved,
};
assert(markerMatched, 'paid Codex lane did not return the exact marker');
assert(report.paidInvocation.toolEventsObserved === 0, 'paid Codex lane emitted a tool event');
assert(report.paidInvocation.unknownEventsObserved === 0, 'paid Codex lane emitted an unknown event');
assert(
report.paidInvocation.threadParamsSha256 === report.invocation.threadParamsSha256,
'paid Codex thread parameters did not match the sealed proof',
);
assert(
report.paidInvocation.turnParamsSha256 === report.invocation.turnParamsSha256,
'paid Codex turn parameters did not match the sealed proof',
);
}
assert(sha256File(codexExecutable) === initialExecutableSha256, 'Codex executable changed during proof');
report.executable.unchanged = true;
assert(sha256File(SCRIPT_PATH) === initialScriptSha256, 'Codex tool-denial helper changed during proof');
report.artifacts.scriptUnchanged = true;
const finalExpectedWaggleHooks = buildExpectedWaggleHooks(trustedPowerShell.path, hiveMindCli);
assert(
finalExpectedWaggleHooks.artifactsSha256 === expectedWaggleHooks.artifactsSha256
&& finalExpectedWaggleHooks.cliSha256 === expectedWaggleHooks.cliSha256,
'packaged Codex hook or hive-mind CLI artifact changed during proof',
);
report.hooks.artifactsUnchanged = true;
const finalSourceSnapshot = verifySourceSnapshot(expectedHead);
assert(
finalSourceSnapshot.observedHead === initialSourceSnapshot.observedHead
&& finalSourceSnapshot.tree === initialSourceSnapshot.tree
&& finalSourceSnapshot.scriptBlob === initialSourceSnapshot.scriptBlob,
'repository source snapshot changed during proof',
);
report.source.unchanged = true;
report.pass = proofStateSatisfied(report.proof, setupOnly)
&& report.source.unchanged
&& report.artifacts.scriptUnchanged
&& report.hooks.artifactsUnchanged
&& report.eventAuditFailure === null
&& (!setupOnly || (
report.diagnostic.failureStage === null
&& report.diagnostic.protocolCode === null
&& report.diagnostic.completedStage === 'thread-start'
&& report.diagnostic.modelCalls === 0
&& report.diagnostic.turnStartCalls === 0
&& report.diagnostic.closeClean
&& report.mcpBoundary.unchanged
))
&& (!executePaid || (
report.paidCalls === 1
&& report.mcpBoundary.unchanged
&& report.paidInvocation.normalTextCompleted
&& report.paidInvocation.toolEventsObserved === 0
&& report.paidInvocation.unknownEventsObserved === 0
));
} catch (error) {
report.turnFailure = error?.turnFailure ?? null;
report.eventAuditFailure = sanitizeEventAuditFailure(error?.eventAuditFailure);
if (error?.canaryDiagnostic) {
report.diagnostic = {
failureStage: sanitizeCanaryStage(error.canaryDiagnostic.failureStage),
protocolCode: sanitizeProtocolCode(error.canaryDiagnostic.protocolCode),
completedStage: sanitizeCanaryStage(error.canaryDiagnostic.completedStage),
modelCalls: Number.isInteger(error.canaryDiagnostic.modelCalls)
&& error.canaryDiagnostic.modelCalls >= 0
? error.canaryDiagnostic.modelCalls
: 0,
turnStartCalls: Number.isInteger(error.canaryDiagnostic.turnStartCalls)
&& error.canaryDiagnostic.turnStartCalls >= 0
? error.canaryDiagnostic.turnStartCalls
: 0,
closeClean: error.canaryDiagnostic.closeClean === true,
};
}
report.error = safeError(error, [
codexExecutable,
hiveMindCli,
receiptDir,
workspace,
windowsPowerShell,
tempRoot,
flags.marker,
]);
}
for (const provider of providers.splice(0)) {
try { await provider.close(); } catch { /* best-effort loopback cleanup */ }
}
if (tempRoot !== null) {
try {
await rm(tempRoot, { recursive: true, force: true });
report.artifacts.tempRemoved = !existsSync(tempRoot);
} catch (error) {
report.error = report.error ?? safeError(error, [tempRoot]);
report.pass = false;
}
}
if (!report.executable.unchanged && existsSync(codexExecutable)) {
report.executable.unchanged = sha256File(codexExecutable) === initialExecutableSha256;
}
if (!report.pass && report.error === null) report.error = 'proof did not satisfy all invariants';
let reportPath = null;
let reportSha256 = null;
if (receiptCreated) {
reportPath = join(receiptDir, 'report.json');
await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
const posture = await stat(reportPath);
assert(posture.isFile(), 'report.json is not a regular file');
reportSha256 = sha256File(reportPath);
}
const summary = {
pass: report.pass,
paidCalls: report.paidCalls,
markerMatched,
reportPath,
reportSha256,
sessionId: executePaid ? rawPaidSessionId : null,
turnFailure: report.turnFailure,
failureStage: report.diagnostic.failureStage,
protocolCode: report.diagnostic.protocolCode,
completedStage: report.diagnostic.completedStage,
modelCalls: report.diagnostic.modelCalls,
turnStartCalls: report.diagnostic.turnStartCalls,
closeClean: report.diagnostic.closeClean,
};
process.stdout.write(`${JSON.stringify(summary)}\n`);
if (!report.pass) process.exitCode = 1;