@sabaiway/agent-workflow-kit 5.3.0 → 5.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +138 -0
- package/README.md +2 -1
- package/SKILL.md +5 -1
- package/bridges/antigravity-cli-bridge/SKILL.md +1 -1
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +1 -1
- package/bridges/antigravity-cli-bridge/capability.json +1 -1
- package/bridges/codex-cli-bridge/SKILL.md +53 -5
- package/bridges/codex-cli-bridge/bin/codex-exec.sh +622 -30
- package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +731 -3
- package/bridges/codex-cli-bridge/bin/codex-review.sh +1 -1
- package/bridges/codex-cli-bridge/capability.json +15 -10
- package/bridges/codex-cli-bridge/references/sandbox-and-flags.md +16 -12
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/core-evidence.md +1 -1
- package/references/modes/coverage-check.md +1 -1
- package/references/modes/dispatch.md +29 -0
- package/references/modes/gates.md +7 -2
- package/references/modes/receipt-deadline.md +3 -3
- package/references/modes/recommendations.md +3 -1
- package/references/modes/upgrade.md +1 -1
- package/references/modes/velocity.md +5 -1
- package/references/scripts/migrate-gates.mjs +102 -10
- package/references/scripts/migrate-gates.test.mjs +37 -0
- package/tools/commands.mjs +7 -0
- package/tools/core-evidence.mjs +79 -5
- package/tools/coverage-check.mjs +23 -7
- package/tools/coverage-producer.mjs +68 -0
- package/tools/coverage-state.mjs +24 -0
- package/tools/declared-paths.mjs +32 -0
- package/tools/detect-backends.mjs +5 -4
- package/tools/dispatch-record.mjs +10 -3
- package/tools/dispatch-store.mjs +392 -0
- package/tools/dispatch.mjs +1779 -0
- package/tools/doc-parity.mjs +27 -4
- package/tools/exec-producer.mjs +483 -0
- package/tools/exec-receipt.mjs +263 -0
- package/tools/flow-store.mjs +111 -462
- package/tools/gates-declaration.mjs +49 -0
- package/tools/gates-init.mjs +83 -6
- package/tools/receipt-deadline.mjs +25 -3
- package/tools/recommendations.mjs +63 -19
- package/tools/release-scan.mjs +33 -0
- package/tools/run-gates.mjs +111 -32
- package/tools/store-append.mjs +444 -0
- package/tools/velocity-profile.mjs +102 -23
|
@@ -8,6 +8,7 @@ import { tmpdir } from 'node:os';
|
|
|
8
8
|
import { join, dirname, resolve } from 'node:path';
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
10
|
import { spawnSync, execFile } from 'node:child_process';
|
|
11
|
+
import { createHash } from 'node:crypto';
|
|
11
12
|
|
|
12
13
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
13
14
|
const WRAPPER = join(HERE, 'codex-exec.sh');
|
|
@@ -29,6 +30,21 @@ const FAKE_CODEX = [
|
|
|
29
30
|
'{ echo "HOME=${HOME:-}"; echo "CODEX_HOME=${CODEX_HOME:-}"; echo "XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-}"; echo "OPENAI_API_KEY=${OPENAI_API_KEY:-<unset>}"; echo "OPENAI_BASE_URL=${OPENAI_BASE_URL:-<unset>}"; echo "FOO_API_KEY=${FOO_API_KEY:-<unset>}"; echo "CODEX_REAL_GIT=${CODEX_REAL_GIT:-<unset>}"; } >"$CODEX_FAKE_ENV"',
|
|
30
31
|
'cat >"$CODEX_FAKE_STDIN"',
|
|
31
32
|
'if [[ "${CODEX_FAKE_GIT_PROBE:-}" == "1" ]]; then { echo "realgit_env=${CODEX_REAL_GIT:-unset}"; echo "status=$(git status --short >/dev/null 2>&1; echo $?)"; echo "diff=$(git --no-pager diff >/dev/null 2>&1; echo $?)"; echo "dashC_read=$(git -C . status --short >/dev/null 2>&1; echo $?)"; echo "dashc_read=$(git -c core.pager=cat status --short >/dev/null 2>&1; echo $?)"; echo "bare=$(git >/dev/null 2>&1; echo $?)"; echo "commit=$(git commit -m x >/dev/null 2>&1; echo $?)"; echo "add=$(git add -A >/dev/null 2>&1; echo $?)"; echo "checkout=$(git checkout -- . >/dev/null 2>&1; echo $?)"; echo "unknown=$(git frobnicate >/dev/null 2>&1; echo $?)"; echo "config_read=$(git config user.name >/dev/null 2>&1; echo $?)"; echo "config_list=$(git config --list >/dev/null 2>&1; echo $?)"; echo "config_bare=$(git config >/dev/null 2>&1; echo $?)"; echo "config_write=$(git config user.name HACKED >/dev/null 2>&1; echo $?)"; echo "config_bypass=$(git config --get --add a.b v >/dev/null 2>&1; echo $?)"; echo "symref_write=$(git symbolic-ref HEAD refs/heads/x >/dev/null 2>&1; echo $?)"; echo "reflog_write=$(git reflog expire --all >/dev/null 2>&1; echo $?)"; echo "cdaway=$(cd / && git --version >/dev/null 2>&1; echo $?)"; } > "${CODEX_FAKE_GIT_RESULT:-/dev/null}" 2>&1; fi',
|
|
33
|
+
// Delegation seams: the fake runs BETWEEN the pre-spend reservation and the terminal publication,
|
|
34
|
+
// which is the only moment a fixture can disturb either. TAMPER forges a foreign owner into the
|
|
35
|
+
// reservation; MKDIR plants a directory where an artifact must land; RO_DIR turns the store
|
|
36
|
+
// directory read-only. Each is inert when its variable is unset.
|
|
37
|
+
'if [[ -n "${CODEX_FAKE_TAMPER:-}" ]]; then',
|
|
38
|
+
' cat >"$CODEX_FAKE_TAMPER" <<EOF',
|
|
39
|
+
'{"schema":1,"kind":"exec-receipt","state":"reserved","backend":"codex","nonce":"${CODEX_FAKE_TAMPER_NONCE:-n1}","owner":"a-foreign-run","contractDigest":"${CODEX_FAKE_TAMPER_DIGEST:-0000000000000000000000000000000000000000000000000000000000000000}","wrapperVersion":"0.0.0","posture":{"model":"m","effort":"e","tier":null},"capS":1,"killGraceS":0,"sessionId":null,"exitStatus":null,"outcome":null,"reportDigest":null,"reportLength":null,"timestamp":"2026-01-01T00:00:00.000Z"}',
|
|
40
|
+
'EOF',
|
|
41
|
+
'fi',
|
|
42
|
+
'if [[ -n "${CODEX_FAKE_MKDIR:-}" ]]; then mkdir -p "$CODEX_FAKE_MKDIR"; fi',
|
|
43
|
+
'if [[ -n "${CODEX_FAKE_RO_DIR:-}" ]]; then chmod 500 "$CODEX_FAKE_RO_DIR"; fi',
|
|
44
|
+
// SNAPSHOT copies a file WHILE the CLI runs — the only way to observe what existed mid-flight.
|
|
45
|
+
// ABSENT is written rather than nothing, so "the file was not there" and "the seam never fired"
|
|
46
|
+
// stay distinguishable.
|
|
47
|
+
'if [[ -n "${CODEX_FAKE_SNAPSHOT_SRC:-}" ]]; then cp "$CODEX_FAKE_SNAPSHOT_SRC" "$CODEX_FAKE_SNAPSHOT_DST" || echo ABSENT >"$CODEX_FAKE_SNAPSHOT_DST"; fi',
|
|
32
48
|
'if [[ -n "${CODEX_FAKE_SLEEP:-}" ]]; then sleep "${CODEX_FAKE_SLEEP}"; fi',
|
|
33
49
|
'out=""',
|
|
34
50
|
'prev=""',
|
|
@@ -37,7 +53,9 @@ const FAKE_CODEX = [
|
|
|
37
53
|
' prev="$a"',
|
|
38
54
|
'done',
|
|
39
55
|
'if [[ -n "$out" ]]; then',
|
|
40
|
-
|
|
56
|
+
// EMPTY_OUT creates the capture file and writes NOTHING into it — the case that must stay
|
|
57
|
+
// distinguishable from NO_OUT, where the file is never created at all.
|
|
58
|
+
' if [[ "${CODEX_FAKE_EMPTY_OUT:-}" == "1" ]]; then : >"$out"; elif [[ "${CODEX_FAKE_NO_OUT:-}" != "1" ]]; then echo "${CODEX_FAKE_FINAL:-FAKE_FINAL_MESSAGE}" >"$out"; fi',
|
|
41
59
|
' if [[ "${CODEX_FAKE_NO_THREAD:-}" != "1" ]]; then',
|
|
42
60
|
' cat <<EOF',
|
|
43
61
|
'{"type":"thread.started","thread_id":"${CODEX_FAKE_THREAD_ID:-fake-thread-123}"}',
|
|
@@ -644,6 +662,33 @@ describe('codex-exec.sh — resume entrypoint restates every invariant (3.1)', (
|
|
|
644
662
|
/approval_policy=never/, /sandbox_workspace_write\.network_access=false/,
|
|
645
663
|
];
|
|
646
664
|
|
|
665
|
+
// `codex exec resume` accepts a NARROWER flag set than `codex exec`, and the difference is not
|
|
666
|
+
// guessable: the hermetic fake accepts any argv, so a flag the real CLI rejects passes every unit
|
|
667
|
+
// test and then fails pre-spend on the first live resume. That happened — `--color never` was
|
|
668
|
+
// added to this lane unprobed and broke it outright. This list is transcribed from
|
|
669
|
+
// `codex exec resume --help` on codex-cli 0.147.0 (probed 2026-08-08); anything the wrapper sends
|
|
670
|
+
// that is not on it fails HERE instead of in front of a user.
|
|
671
|
+
const RESUME_ACCEPTED_FLAGS = new Set([
|
|
672
|
+
'--last', '--all', '-c', '--config', '--enable', '-i', '--image', '--strict-config', '--disable',
|
|
673
|
+
'-m', '--model', '--dangerously-bypass-approvals-and-sandbox', '--dangerously-bypass-hook-trust',
|
|
674
|
+
'--skip-git-repo-check', '--ephemeral', '--ignore-user-config', '--ignore-rules',
|
|
675
|
+
'--output-schema', '--json', '-o', '--output-last-message', '-h', '--help',
|
|
676
|
+
]);
|
|
677
|
+
|
|
678
|
+
it('every flag the resume lane sends is one the REAL `codex exec resume` accepts', () => {
|
|
679
|
+
const sb = makeSandbox();
|
|
680
|
+
const r = run(sb, { args: ['--resume', 'sess-flags', '-'], input: 'go' });
|
|
681
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
682
|
+
assert.equal(r.status, 0, r.stderr);
|
|
683
|
+
const sent = r.argv.split('\n').filter((a) => a.startsWith('-') && a !== '-');
|
|
684
|
+
assert.ok(sent.length > 0, 'the resume argv must carry flags at all');
|
|
685
|
+
for (const flag of sent) {
|
|
686
|
+
assert.ok(RESUME_ACCEPTED_FLAGS.has(flag),
|
|
687
|
+
`resume sends ${flag}, which \`codex exec resume --help\` does not list — it would exit 2 pre-spend`);
|
|
688
|
+
}
|
|
689
|
+
assert.equal(sent.includes('--color'), false, 'the regression this list exists to prevent');
|
|
690
|
+
});
|
|
691
|
+
|
|
647
692
|
it('--resume <id>: composes `exec resume <id>` with the full restated policy', () => {
|
|
648
693
|
const sb = makeSandbox();
|
|
649
694
|
const r = run(sb, { args: ['--resume', 'sess-xyz', '-'], input: 'continue please' });
|
|
@@ -657,14 +702,16 @@ describe('codex-exec.sh — resume entrypoint restates every invariant (3.1)', (
|
|
|
657
702
|
// The capture unification: resume used to be the odd mode out — no -o, no --json, its event
|
|
658
703
|
// stream nowhere — which is precisely why the lane the nested-sandbox incident fired on had no
|
|
659
704
|
// evidence surface. `codex exec resume` accepts both (live-probed, codex-cli 0.147.0).
|
|
660
|
-
it('resume composes the unified capture —
|
|
705
|
+
it('resume composes the unified capture — -o and --json, and NOT --color (which it rejects)', () => {
|
|
661
706
|
const sb = makeSandbox();
|
|
662
707
|
const r = run(sb, { args: ['--resume', 'sess-unified', '-'], input: 'continue please' });
|
|
663
708
|
rmSync(sb.root, { recursive: true, force: true });
|
|
664
709
|
assert.equal(r.status, 0, r.stderr);
|
|
665
710
|
assert.match(r.argv, /(^|\n)-o(\n|$)/, 'resume writes the final message through -o');
|
|
666
711
|
assert.match(r.argv, /(^|\n)--json(\n|$)/, 'resume streams the structured events');
|
|
667
|
-
|
|
712
|
+
// The evidence surface is shared with a fresh run; the COLOUR flag is not, because
|
|
713
|
+
// `codex exec resume` does not accept it. Sending it exits 2 before the run starts.
|
|
714
|
+
assert.equal(/(^|\n)--color(\n|$)/.test(r.argv), false, 'resume rejects --color — probed on codex-cli 0.147.0');
|
|
668
715
|
assert.match(r.stdout, /FAKE_FINAL_MESSAGE/, 'resume stdout is still the final message');
|
|
669
716
|
});
|
|
670
717
|
|
|
@@ -1704,3 +1751,684 @@ describe('codex-exec.sh — dispatch-posture labeling (D5, AD-061)', () => {
|
|
|
1704
1751
|
}
|
|
1705
1752
|
});
|
|
1706
1753
|
});
|
|
1754
|
+
|
|
1755
|
+
// ── the delegation dispatch identity: nonce seam, reservation, fail-closed receipt ────────────────
|
|
1756
|
+
// The exec lane's arrival identity (delegation Plan 2, Phase 3). Two halves are load-bearing and
|
|
1757
|
+
// tested apart: the PRE-SPEND reservation, which is what makes one nonce mean one dispatch, and the
|
|
1758
|
+
// TERMINAL publication, which is fail-closed where the review lane's receipt only warns.
|
|
1759
|
+
// A nonce-LESS run is the no-regression pin — every suite above drives one, and none of them may
|
|
1760
|
+
// see an artifact.
|
|
1761
|
+
|
|
1762
|
+
const STORE_BASENAME = 'agent-workflow-delegation.jsonl';
|
|
1763
|
+
const receiptName = (nonce, backend = 'codex') => `agent-workflow-exec-receipt-${backend.length}-${backend}-${nonce}.json`;
|
|
1764
|
+
const reportName = (nonce, backend = 'codex') => `agent-workflow-exec-report-${backend.length}-${backend}-${nonce}.txt`;
|
|
1765
|
+
// The store directory a nonce-less-by-default sandbox resolves to: the git common dir of the repo.
|
|
1766
|
+
const storeDirOf = (sb) => join(sb.repo, '.git');
|
|
1767
|
+
const execArtifacts = (dir) => {
|
|
1768
|
+
let names;
|
|
1769
|
+
try { names = readdirSync(dir); } catch { return []; }
|
|
1770
|
+
return names.filter((n) => n.startsWith('agent-workflow-exec-')).sort();
|
|
1771
|
+
};
|
|
1772
|
+
const readReceipt = (dir, nonce) => JSON.parse(readFileSync(join(dir, receiptName(nonce)), 'utf8'));
|
|
1773
|
+
|
|
1774
|
+
// A dispatch file carrying exactly ONE top-level contract block — the same shape `dispatch open
|
|
1775
|
+
// --contract` parses, so the digest the wrapper computes here is the digest the ledger recorded.
|
|
1776
|
+
const CONTRACT_FENCE = '```';
|
|
1777
|
+
const writeContract = (sb, nonce, over = {}) => {
|
|
1778
|
+
const contract = {
|
|
1779
|
+
schema: 1,
|
|
1780
|
+
nonce,
|
|
1781
|
+
stepClass: 'code',
|
|
1782
|
+
vehicle: { requested: 'codex-exec', selected: 'codex-exec' },
|
|
1783
|
+
scope: 'the bounded sub-task', inputs: 'the files it may touch', acceptance: 'the named tests',
|
|
1784
|
+
returnShape: 'a diff plus a report', producerContract: 'wrapper-git',
|
|
1785
|
+
deadlineS: 3700,
|
|
1786
|
+
retry: { cap: 1, index: 0 },
|
|
1787
|
+
...over,
|
|
1788
|
+
};
|
|
1789
|
+
const rel = `${nonce}-dispatch.md`;
|
|
1790
|
+
writeFileSync(join(sb.repo, rel),
|
|
1791
|
+
`# sub-task\n\n${CONTRACT_FENCE}aw-dispatch-contract\n${JSON.stringify(contract, null, 2)}\n${CONTRACT_FENCE}\n`);
|
|
1792
|
+
return rel;
|
|
1793
|
+
};
|
|
1794
|
+
|
|
1795
|
+
// run() reads the capture files the PREVIOUS run left behind, so a test proving "the CLI was never
|
|
1796
|
+
// invoked" has to clear them first — otherwise a stale argv reads as a fresh invocation.
|
|
1797
|
+
const clearCaptures = (sb) => {
|
|
1798
|
+
for (const name of ['.cap-argv', '.cap-env', '.cap-stdin']) rmSync(join(sb.repo, name), { force: true });
|
|
1799
|
+
};
|
|
1800
|
+
|
|
1801
|
+
describe('codex-exec.sh — the nonce seam (D11)', () => {
|
|
1802
|
+
it('a nonce-LESS run writes NO artifact into the store directory', () => {
|
|
1803
|
+
const sb = makeSandbox();
|
|
1804
|
+
const file = writeContract(sb, 'unused');
|
|
1805
|
+
const r = run(sb, { args: [file] });
|
|
1806
|
+
const artifacts = execArtifacts(storeDirOf(sb));
|
|
1807
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1808
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1809
|
+
assert.deepEqual(artifacts, [], 'a nonce-less invocation is byte-unchanged — no reservation, no receipt');
|
|
1810
|
+
assert.doesNotMatch(r.stderr, /exec receipt:/, 'and it says nothing about a receipt either');
|
|
1811
|
+
});
|
|
1812
|
+
|
|
1813
|
+
it('each of the three D11 argv forms is accepted and reaches the right mode', () => {
|
|
1814
|
+
const cases = [
|
|
1815
|
+
{ label: 'fresh', args: (f) => ['--nonce', 'nf', f], nonce: 'nf', wantSession: 'fake-thread-123', resume: false },
|
|
1816
|
+
{ label: 'resume-last', args: (f) => ['--resume-last', '--nonce', 'nl', f], nonce: 'nl', wantSession: 'sess-from-sidecar', resume: true },
|
|
1817
|
+
{ label: 'resume', args: (f) => ['--resume', 'sess-xyz', '--nonce', 'nr', f], nonce: 'nr', wantSession: 'sess-xyz', resume: true },
|
|
1818
|
+
];
|
|
1819
|
+
for (const c of cases) {
|
|
1820
|
+
const sb = makeSandbox();
|
|
1821
|
+
writeFileSync(join(sb.repo, '.codex-last-session'), 'sess-from-sidecar\n');
|
|
1822
|
+
const file = writeContract(sb, c.nonce);
|
|
1823
|
+
const r = run(sb, { args: c.args(file) });
|
|
1824
|
+
const receipt = r.status === 0 ? readReceipt(storeDirOf(sb), c.nonce) : null;
|
|
1825
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1826
|
+
assert.equal(r.status, 0, `${c.label}: ${r.stderr}`);
|
|
1827
|
+
assert.match(r.argv, /(^|\n)exec(\n|$)/, `${c.label}: codex exec was really reached`);
|
|
1828
|
+
assert.equal(/(^|\n)resume(\n|$)/.test(r.argv), c.resume, `${c.label}: the mode selector survived the nonce strip`);
|
|
1829
|
+
assert.equal(receipt.state, 'terminal', `${c.label}: the run published its terminal receipt`);
|
|
1830
|
+
assert.equal(receipt.nonce, c.nonce);
|
|
1831
|
+
assert.equal(receipt.sessionId, c.wantSession, `${c.label}: the session id is the run's own`);
|
|
1832
|
+
}
|
|
1833
|
+
});
|
|
1834
|
+
|
|
1835
|
+
it('a --nonce AFTER the prompt operand or after -- is payload, never a flag', () => {
|
|
1836
|
+
const sb = makeSandbox();
|
|
1837
|
+
const file = writeContract(sb, 'late');
|
|
1838
|
+
const passthrough = run(sb, { args: [file, '--', '--nonce', 'late'] });
|
|
1839
|
+
const afterOperand = run(sb, { args: [file, '--nonce', 'late'] });
|
|
1840
|
+
const artifacts = execArtifacts(storeDirOf(sb));
|
|
1841
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1842
|
+
assert.equal(passthrough.status, 0, passthrough.stderr);
|
|
1843
|
+
assert.match(passthrough.argv, /(^|\n)--nonce(\n|$)/, 'after -- it reaches codex argv as payload');
|
|
1844
|
+
assert.deepEqual(artifacts, [], 'and it never minted a dispatch identity');
|
|
1845
|
+
assert.equal(afterOperand.status, 2, 'without -- an extra argument is the existing loud refusal');
|
|
1846
|
+
assert.match(afterOperand.stderr, /unexpected argument '--nonce'/);
|
|
1847
|
+
});
|
|
1848
|
+
|
|
1849
|
+
it('the flag and the environment value are ONE seam: agreeing runs, disagreeing refuses pre-spend', () => {
|
|
1850
|
+
const agree = makeSandbox();
|
|
1851
|
+
const agreeFile = writeContract(agree, 'same');
|
|
1852
|
+
const ok = run(agree, { args: ['--nonce', 'same', agreeFile], env: { AW_DISPATCH_NONCE: 'same' } });
|
|
1853
|
+
const okState = ok.status === 0 ? readReceipt(storeDirOf(agree), 'same').state : null;
|
|
1854
|
+
rmSync(agree.root, { recursive: true, force: true });
|
|
1855
|
+
assert.equal(ok.status, 0, ok.stderr);
|
|
1856
|
+
assert.equal(okState, 'terminal');
|
|
1857
|
+
|
|
1858
|
+
const clash = makeSandbox();
|
|
1859
|
+
const clashFile = writeContract(clash, 'flagnonce');
|
|
1860
|
+
const bad = run(clash, { args: ['--nonce', 'flagnonce', clashFile], env: { AW_DISPATCH_NONCE: 'envnonce' } });
|
|
1861
|
+
const artifacts = execArtifacts(storeDirOf(clash));
|
|
1862
|
+
rmSync(clash.root, { recursive: true, force: true });
|
|
1863
|
+
assert.equal(bad.status, 2);
|
|
1864
|
+
assert.match(bad.stderr, /--nonce disagrees with the AW_DISPATCH_NONCE environment value/);
|
|
1865
|
+
assert.equal(bad.argv, '', 'the refusal precedes any spend');
|
|
1866
|
+
assert.deepEqual(artifacts, [], 'and it reserves nothing');
|
|
1867
|
+
});
|
|
1868
|
+
|
|
1869
|
+
it('a nonce outside the safe grammar refuses pre-spend, from either source', () => {
|
|
1870
|
+
for (const bad of ['a/b', '../x', 'a b', 'x'.repeat(65), '']) {
|
|
1871
|
+
const viaFlag = makeSandbox();
|
|
1872
|
+
const file = writeContract(viaFlag, 'grammar');
|
|
1873
|
+
const f = run(viaFlag, { args: ['--nonce', bad, file] });
|
|
1874
|
+
const flagArtifacts = execArtifacts(storeDirOf(viaFlag));
|
|
1875
|
+
rmSync(viaFlag.root, { recursive: true, force: true });
|
|
1876
|
+
assert.equal(f.status, 2, `--nonce "${bad}" must refuse`);
|
|
1877
|
+
assert.match(f.stderr, /fails the safe nonce grammar/);
|
|
1878
|
+
assert.equal(f.argv, '', 'nothing was spent');
|
|
1879
|
+
assert.deepEqual(flagArtifacts, [], 'nothing was reserved');
|
|
1880
|
+
|
|
1881
|
+
if (bad === '') continue; // an EMPTY env value is "unset" to the seam, not a bad nonce
|
|
1882
|
+
const viaEnv = makeSandbox();
|
|
1883
|
+
const e = run(viaEnv, { args: [writeContract(viaEnv, 'grammar')], env: { AW_DISPATCH_NONCE: bad } });
|
|
1884
|
+
rmSync(viaEnv.root, { recursive: true, force: true });
|
|
1885
|
+
assert.equal(e.status, 2, `AW_DISPATCH_NONCE "${bad}" must refuse`);
|
|
1886
|
+
assert.match(e.stderr, /AW_DISPATCH_NONCE fails the safe nonce grammar/);
|
|
1887
|
+
}
|
|
1888
|
+
});
|
|
1889
|
+
|
|
1890
|
+
it('a duplicate --nonce refuses, and a --nonce with no value refuses', () => {
|
|
1891
|
+
const sb = makeSandbox();
|
|
1892
|
+
const file = writeContract(sb, 'dup');
|
|
1893
|
+
const dup = run(sb, { args: ['--nonce', 'dup', '--nonce', 'other', file] });
|
|
1894
|
+
const bare = run(sb, { args: ['--nonce'] });
|
|
1895
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1896
|
+
assert.equal(dup.status, 2);
|
|
1897
|
+
assert.match(dup.stderr, /duplicate --nonce — one dispatch carries one nonce/);
|
|
1898
|
+
assert.equal(bare.status, 2);
|
|
1899
|
+
assert.match(bare.stderr, /--nonce needs a value/);
|
|
1900
|
+
});
|
|
1901
|
+
|
|
1902
|
+
it('an accounted dispatch needs a contract FILE: stdin and a header-less file both refuse pre-spend', () => {
|
|
1903
|
+
const sb = makeSandbox();
|
|
1904
|
+
const stdin = run(sb, { args: ['--nonce', 'n1', '-'], input: 'do the thing' });
|
|
1905
|
+
writeFileSync(join(sb.repo, 'plain.md'), '# just a plan\n\nno contract block here\n');
|
|
1906
|
+
clearCaptures(sb);
|
|
1907
|
+
const headerless = run(sb, { args: ['--nonce', 'n1', 'plain.md'] });
|
|
1908
|
+
const artifacts = execArtifacts(storeDirOf(sb));
|
|
1909
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1910
|
+
assert.equal(stdin.status, 2);
|
|
1911
|
+
assert.match(stdin.stderr, /a nonced dispatch runs a contract FILE, not stdin/);
|
|
1912
|
+
assert.equal(headerless.status, 2);
|
|
1913
|
+
assert.match(headerless.stderr, /no top-level ```aw-dispatch-contract block found/);
|
|
1914
|
+
assert.equal(headerless.argv, '', 'neither refusal spent a run');
|
|
1915
|
+
assert.deepEqual(artifacts, [], 'neither refusal reserved a nonce');
|
|
1916
|
+
});
|
|
1917
|
+
});
|
|
1918
|
+
|
|
1919
|
+
describe('codex-exec.sh — the pre-spend reservation (D1/D8)', () => {
|
|
1920
|
+
it('the reservation EXISTS, in state reserved, while the CLI is still running', () => {
|
|
1921
|
+
// The ordering claim needs a mid-flight observation. Asserting it from a SECOND run would prove
|
|
1922
|
+
// nothing: by then the first run has finished and left a TERMINAL artifact, so moving the
|
|
1923
|
+
// reservation to after the CLI would keep such a test green. The fake copies the receipt while
|
|
1924
|
+
// it runs; only a genuinely pre-spend reservation can be `reserved` at that moment.
|
|
1925
|
+
const sb = makeSandbox();
|
|
1926
|
+
const file = writeContract(sb, 'midflight');
|
|
1927
|
+
const snapshot = join(sb.root, 'receipt-during-the-run.json');
|
|
1928
|
+
const r = run(sb, {
|
|
1929
|
+
args: ['--nonce', 'midflight', file],
|
|
1930
|
+
env: { CODEX_FAKE_SNAPSHOT_SRC: join(storeDirOf(sb), receiptName('midflight')), CODEX_FAKE_SNAPSHOT_DST: snapshot },
|
|
1931
|
+
});
|
|
1932
|
+
const seen = readFileSync(snapshot, 'utf8');
|
|
1933
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1934
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1935
|
+
assert.notEqual(seen.trim(), 'ABSENT', 'the receipt must already exist when the CLI starts');
|
|
1936
|
+
assert.equal(JSON.parse(seen).state, 'reserved', 'and it is the RESERVATION — the terminal receipt comes later');
|
|
1937
|
+
});
|
|
1938
|
+
|
|
1939
|
+
it('a second dispatch on the same nonce refuses unspent, leaving the first run\'s evidence untouched', () => {
|
|
1940
|
+
const sb = makeSandbox();
|
|
1941
|
+
const file = writeContract(sb, 'once');
|
|
1942
|
+
const first = run(sb, { args: ['--nonce', 'once', file] });
|
|
1943
|
+
const receiptPath = join(storeDirOf(sb), receiptName('once'));
|
|
1944
|
+
const afterFirst = readFileSync(receiptPath, 'utf8');
|
|
1945
|
+
clearCaptures(sb);
|
|
1946
|
+
const second = run(sb, { args: ['--nonce', 'once', file] });
|
|
1947
|
+
const afterSecond = readFileSync(receiptPath, 'utf8');
|
|
1948
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1949
|
+
assert.equal(first.status, 0, first.stderr);
|
|
1950
|
+
assert.equal(second.status, 2, 'one nonce, one dispatch');
|
|
1951
|
+
assert.match(second.stderr, /already exists at .* one nonce, one dispatch: refusing PRE-SPEND/);
|
|
1952
|
+
assert.equal(second.argv, '', 'the CLI was never invoked — the refusal is PRE-spend, proven by the fake recording nothing');
|
|
1953
|
+
assert.equal(afterSecond, afterFirst, 'the first dispatch\'s evidence is byte-untouched');
|
|
1954
|
+
});
|
|
1955
|
+
|
|
1956
|
+
it('the dispatch nonce must equal the contract header\'s nonce — a disagreement refuses unspent', () => {
|
|
1957
|
+
// `dispatch open` COPIES the nonce from the header, so a disagreeing --nonce could only reserve
|
|
1958
|
+
// an identity no return would ever absorb — after paying for the run.
|
|
1959
|
+
const sb = makeSandbox();
|
|
1960
|
+
const file = writeContract(sb, 'header');
|
|
1961
|
+
const r = run(sb, { args: ['--nonce', 'other', file] });
|
|
1962
|
+
const artifacts = execArtifacts(storeDirOf(sb));
|
|
1963
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1964
|
+
assert.equal(r.status, 2);
|
|
1965
|
+
assert.match(r.stderr, /the dispatch nonce 'other' does not match the contract header's nonce 'header'/);
|
|
1966
|
+
assert.equal(r.argv, '', 'the refusal precedes the spend');
|
|
1967
|
+
assert.deepEqual(artifacts, [], 'and precedes the reservation');
|
|
1968
|
+
});
|
|
1969
|
+
|
|
1970
|
+
it('a nonce-LESS run of a contract-bearing file never compares nonces — the accounted lane owns that rule', () => {
|
|
1971
|
+
const sb = makeSandbox();
|
|
1972
|
+
const file = writeContract(sb, 'header');
|
|
1973
|
+
const r = run(sb, { args: [file] });
|
|
1974
|
+
const artifacts = execArtifacts(storeDirOf(sb));
|
|
1975
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1976
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1977
|
+
assert.deepEqual(artifacts, [], 'an ordinary plan file that happens to carry a contract block still runs unaccounted');
|
|
1978
|
+
});
|
|
1979
|
+
|
|
1980
|
+
it('a store directory whose parent name ends in a NEWLINE resolves as the kit resolves it', () => {
|
|
1981
|
+
// `$( )` strips every trailing newline; the kit's own reader strips exactly ONE (git's
|
|
1982
|
+
// terminator). Without a sentinel the two sides would resolve different directories here, and the
|
|
1983
|
+
// wrapper would write beside a ledger the kit never reads.
|
|
1984
|
+
const sb = makeSandbox();
|
|
1985
|
+
const weird = join(sb.root, 'ledger\n');
|
|
1986
|
+
mkdirSync(weird, { recursive: true });
|
|
1987
|
+
const file = writeContract(sb, 'nlstore');
|
|
1988
|
+
const r = run(sb, { args: ['--nonce', 'nlstore', file], env: { AW_DELEGATION_STORE: join(weird, STORE_BASENAME) } });
|
|
1989
|
+
const landed = execArtifacts(weird);
|
|
1990
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1991
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1992
|
+
assert.deepEqual(landed, [receiptName('nlstore'), reportName('nlstore')].sort(),
|
|
1993
|
+
'the artifacts land in the directory the kit would compute, newline and all');
|
|
1994
|
+
});
|
|
1995
|
+
|
|
1996
|
+
it('an already-taken REPORT name refuses the reservation too — the kit refuses on either name', () => {
|
|
1997
|
+
const sb = makeSandbox();
|
|
1998
|
+
const file = writeContract(sb, 'leftover');
|
|
1999
|
+
writeFileSync(join(storeDirOf(sb), reportName('leftover')), 'a report from something else\n');
|
|
2000
|
+
const r = run(sb, { args: ['--nonce', 'leftover', file] });
|
|
2001
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
2002
|
+
assert.equal(r.status, 2);
|
|
2003
|
+
assert.match(r.stderr, /already exists at .*agent-workflow-exec-report-5-codex-leftover\.txt/);
|
|
2004
|
+
assert.equal(r.argv, '', 'nothing was spent');
|
|
2005
|
+
});
|
|
2006
|
+
|
|
2007
|
+
it('a NONCED run with no capping binary refuses pre-spend, while a nonce-less one still warns and runs', () => {
|
|
2008
|
+
const sb = makeSandbox();
|
|
2009
|
+
const file = writeContract(sb, 'uncapped');
|
|
2010
|
+
const path = `${sb.bin}:${farmFor(['timeout', 'gtimeout'])}`;
|
|
2011
|
+
const nonced = run(sb, { args: ['--nonce', 'uncapped', file], path });
|
|
2012
|
+
const artifacts = execArtifacts(storeDirOf(sb));
|
|
2013
|
+
clearCaptures(sb);
|
|
2014
|
+
const plain = run(sb, { args: [file], path });
|
|
2015
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
2016
|
+
assert.equal(nonced.status, 2, nonced.stderr);
|
|
2017
|
+
assert.match(nonced.stderr, /a nonced dispatch refuses to run uncapped/);
|
|
2018
|
+
assert.equal(nonced.argv, '', 'the refusal precedes the spend');
|
|
2019
|
+
assert.deepEqual(artifacts, [], 'and precedes the reservation');
|
|
2020
|
+
assert.equal(plain.status, 0, plain.stderr);
|
|
2021
|
+
assert.match(plain.stderr, /running codex WITHOUT a hard wall-clock cap/, 'the nonce-less lane is unchanged');
|
|
2022
|
+
});
|
|
2023
|
+
|
|
2024
|
+
it('a PREFLIGHT refusal leaves NO reservation (login guard, missing AGENTS.md, off-pin model)', () => {
|
|
2025
|
+
const cases = [
|
|
2026
|
+
{ label: 'login', env: { CODEX_FAKE_LOGIN: 'Not logged in' } },
|
|
2027
|
+
{ label: 'model', env: { CODEX_MODEL: 'gpt-5.4-mini' } },
|
|
2028
|
+
{ label: 'agents', env: {}, drop: true },
|
|
2029
|
+
];
|
|
2030
|
+
for (const c of cases) {
|
|
2031
|
+
const sb = makeSandbox();
|
|
2032
|
+
const file = writeContract(sb, 'preflight');
|
|
2033
|
+
if (c.drop) rmSync(join(sb.repo, 'AGENTS.md'), { force: true });
|
|
2034
|
+
const r = run(sb, { args: ['--nonce', 'preflight', file], env: c.env });
|
|
2035
|
+
const artifacts = execArtifacts(storeDirOf(sb));
|
|
2036
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
2037
|
+
assert.notEqual(r.status, 0, `${c.label}: the preflight must refuse`);
|
|
2038
|
+
assert.deepEqual(artifacts, [], `${c.label}: a refused run leaves no reservation`);
|
|
2039
|
+
}
|
|
2040
|
+
});
|
|
2041
|
+
|
|
2042
|
+
it('the reservation carries everything knowable PRE-SPEND and nulls every terminal-only field', () => {
|
|
2043
|
+
// Proven on the artifact the CLI-blocking refusal leaves behind: a second dispatch is refused
|
|
2044
|
+
// pre-spend, so the FIRST run's reservation is the only shape a fixture can observe mid-flight —
|
|
2045
|
+
// instead, block the terminal publication and read the surviving reservation.
|
|
2046
|
+
const sb = makeSandbox();
|
|
2047
|
+
const file = writeContract(sb, 'resv');
|
|
2048
|
+
const r = run(sb, {
|
|
2049
|
+
args: ['--nonce', 'resv', file],
|
|
2050
|
+
env: { CODEX_FAKE_MKDIR: join(storeDirOf(sb), reportName('resv')) },
|
|
2051
|
+
});
|
|
2052
|
+
const held = readReceipt(storeDirOf(sb), 'resv');
|
|
2053
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
2054
|
+
assert.equal(r.status, 71, r.stderr);
|
|
2055
|
+
assert.equal(held.state, 'reserved');
|
|
2056
|
+
assert.equal(held.wrapperVersion, MANIFEST.version, 'the receipt stamps the bridge version version-sync bumps');
|
|
2057
|
+
assert.deepEqual(held.posture, { model: 'gpt-5.6-sol', effort: 'xhigh', tier: null });
|
|
2058
|
+
assert.equal(held.capS, 3600);
|
|
2059
|
+
assert.equal(held.killGraceS, 15);
|
|
2060
|
+
assert.match(held.contractDigest, /^[0-9a-f]{64}$/);
|
|
2061
|
+
for (const field of ['sessionId', 'exitStatus', 'outcome', 'reportDigest', 'reportLength']) {
|
|
2062
|
+
assert.equal(held[field], null, `a reservation proves nothing about ${field}`);
|
|
2063
|
+
}
|
|
2064
|
+
});
|
|
2065
|
+
|
|
2066
|
+
it('the store directory resolves as the kit resolves it: absolute override wins, relative and trailing-separator refuse', () => {
|
|
2067
|
+
const sb = makeSandbox();
|
|
2068
|
+
const file = writeContract(sb, 'store');
|
|
2069
|
+
const elsewhere = join(sb.root, 'ledger');
|
|
2070
|
+
mkdirSync(elsewhere, { recursive: true });
|
|
2071
|
+
const ok = run(sb, { args: ['--nonce', 'store', file], env: { AW_DELEGATION_STORE: join(elsewhere, STORE_BASENAME) } });
|
|
2072
|
+
const landed = execArtifacts(elsewhere);
|
|
2073
|
+
clearCaptures(sb);
|
|
2074
|
+
const rel = run(sb, { args: ['--nonce', 'store2', writeContract(sb, 'store2')], env: { AW_DELEGATION_STORE: `ledger/${STORE_BASENAME}` } });
|
|
2075
|
+
clearCaptures(sb);
|
|
2076
|
+
const trailing = run(sb, { args: ['--nonce', 'store3', writeContract(sb, 'store3')], env: { AW_DELEGATION_STORE: `${elsewhere}/` } });
|
|
2077
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
2078
|
+
assert.equal(ok.status, 0, ok.stderr);
|
|
2079
|
+
assert.deepEqual(landed, [receiptName('store'), reportName('store')].sort(), 'both artifacts land in the override\'s dirname');
|
|
2080
|
+
assert.equal(rel.status, 2);
|
|
2081
|
+
assert.match(rel.stderr, /AW_DELEGATION_STORE must be an ABSOLUTE path/);
|
|
2082
|
+
assert.equal(trailing.status, 2);
|
|
2083
|
+
assert.match(trailing.stderr, /must not end with a path separator/);
|
|
2084
|
+
});
|
|
2085
|
+
});
|
|
2086
|
+
|
|
2087
|
+
describe('codex-exec.sh — the fail-closed terminal receipt (D1/D3/3.1.d)', () => {
|
|
2088
|
+
it('a SUCCESSFUL run publishes a complete report and a terminal receipt that describes it', () => {
|
|
2089
|
+
const sb = makeSandbox();
|
|
2090
|
+
const file = writeContract(sb, 'ok');
|
|
2091
|
+
const r = run(sb, { args: ['--nonce', 'ok', file] });
|
|
2092
|
+
const dir = storeDirOf(sb);
|
|
2093
|
+
const receipt = readReceipt(dir, 'ok');
|
|
2094
|
+
const report = readFileSync(join(dir, reportName('ok')));
|
|
2095
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
2096
|
+
assert.equal(r.status, 0, r.stderr);
|
|
2097
|
+
assert.equal(receipt.state, 'terminal');
|
|
2098
|
+
assert.equal(receipt.outcome, 'success');
|
|
2099
|
+
assert.equal(receipt.exitStatus, 0);
|
|
2100
|
+
assert.equal(receipt.reportLength, report.length, 'the receipt describes the bytes on disk');
|
|
2101
|
+
assert.equal(receipt.reportDigest, createHash('sha256').update(report).digest('hex'));
|
|
2102
|
+
assert.equal(report.toString(), 'FAKE_FINAL_MESSAGE\n', 'the report IS the delegate\'s final message');
|
|
2103
|
+
assert.match(r.stderr, /exec receipt: nonce=ok outcome=success exit=0 session=fake-thread-123/);
|
|
2104
|
+
});
|
|
2105
|
+
|
|
2106
|
+
it('a FAILED run still publishes a terminal receipt — with its exit status, outcome and session id', () => {
|
|
2107
|
+
const sb = makeSandbox();
|
|
2108
|
+
const file = writeContract(sb, 'boom');
|
|
2109
|
+
const r = run(sb, { args: ['--nonce', 'boom', file], env: { CODEX_FAKE_EXIT: '5' } });
|
|
2110
|
+
const receipt = readReceipt(storeDirOf(sb), 'boom');
|
|
2111
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
2112
|
+
assert.equal(r.status, 5, 'the wrapper still exits with the run\'s own status');
|
|
2113
|
+
assert.equal(receipt.state, 'terminal');
|
|
2114
|
+
assert.equal(receipt.exitStatus, 5);
|
|
2115
|
+
assert.equal(receipt.outcome, 'transport-failure');
|
|
2116
|
+
assert.equal(receipt.sessionId, 'fake-thread-123', 'the session id is captured BEFORE outcome branching — a failed run has one too');
|
|
2117
|
+
assert.match(r.stderr, /error: codex exec failed \(exit 5\)/, 'the failure diagnostics still print, and BEFORE the receipt line');
|
|
2118
|
+
assert.ok(r.stderr.indexOf('codex exec failed') < r.stderr.indexOf('exec receipt:'), 'a publication never swallows the trace tail');
|
|
2119
|
+
});
|
|
2120
|
+
|
|
2121
|
+
it('an ABSENT capture and an EXISTING EMPTY one both record zero bytes — and neither is a failed probe', () => {
|
|
2122
|
+
// `mktemp` used to pre-create the capture file, so ENOENT could never occur and "absent" was not
|
|
2123
|
+
// a case the publisher could even see. It matters because of what it collides with: once an
|
|
2124
|
+
// absent capture is possible, the fail-closed read-error branch has to tell it apart from a
|
|
2125
|
+
// genuinely UNREADABLE one (the test below) instead of treating every errno alike.
|
|
2126
|
+
// What the two cases here do NOT get is a difference in the RECEIPT: the D2 key set is frozen and
|
|
2127
|
+
// has no field for "the delegate wrote nothing at all", so both record the sha256 of no bytes —
|
|
2128
|
+
// stated rather than papered over, and the outcome stays D3's (rc 0 + a session id is success).
|
|
2129
|
+
const absent = makeSandbox();
|
|
2130
|
+
const absentFile = writeContract(absent, 'noout');
|
|
2131
|
+
const a = run(absent, { args: ['--nonce', 'noout', absentFile], env: { CODEX_FAKE_NO_OUT: '1' } });
|
|
2132
|
+
const absentReceipt = readReceipt(storeDirOf(absent), 'noout');
|
|
2133
|
+
const absentReport = readFileSync(join(storeDirOf(absent), reportName('noout')));
|
|
2134
|
+
rmSync(absent.root, { recursive: true, force: true });
|
|
2135
|
+
assert.equal(a.status, 0, a.stderr);
|
|
2136
|
+
assert.match(a.stderr, /codex produced no final-message file/, 'the run says it has no answer rather than printing an empty one');
|
|
2137
|
+
assert.equal(absentReport.length, 0);
|
|
2138
|
+
assert.equal(absentReceipt.reportLength, 0);
|
|
2139
|
+
assert.equal(absentReceipt.reportDigest, createHash('sha256').update(Buffer.alloc(0)).digest('hex'),
|
|
2140
|
+
'an absent final message is recorded as the sha256 of NO bytes, never as a failed probe');
|
|
2141
|
+
|
|
2142
|
+
const empty = makeSandbox();
|
|
2143
|
+
const emptyFile = writeContract(empty, 'emptyout');
|
|
2144
|
+
const e = run(empty, { args: ['--nonce', 'emptyout', emptyFile], env: { CODEX_FAKE_EMPTY_OUT: '1' } });
|
|
2145
|
+
const emptyReceipt = readReceipt(storeDirOf(empty), 'emptyout');
|
|
2146
|
+
rmSync(empty.root, { recursive: true, force: true });
|
|
2147
|
+
assert.equal(e.status, 0, e.stderr);
|
|
2148
|
+
assert.equal(emptyReceipt.reportLength, 0, 'an EXISTING empty capture records zero bytes too');
|
|
2149
|
+
assert.equal(emptyReceipt.reportDigest, absentReceipt.reportDigest,
|
|
2150
|
+
'DELIBERATE: the frozen D2 key set has no field that separates them, so the receipt does not pretend to');
|
|
2151
|
+
assert.match(e.stderr, /codex produced no final-message file/,
|
|
2152
|
+
'the existing-but-EMPTY capture takes the same -s fallback — it too has no answer to print');
|
|
2153
|
+
});
|
|
2154
|
+
|
|
2155
|
+
it('an UNREADABLE final message is a failed probe — exit 71, never a silent empty report', () => {
|
|
2156
|
+
const sb = makeSandbox();
|
|
2157
|
+
const file = writeContract(sb, 'unread');
|
|
2158
|
+
// A DIRECTORY at the capture path: readable-as-a-path, unreadable as a file (EISDIR), and
|
|
2159
|
+
// distinct from ENOENT — which is exactly the errno split the fold turns on.
|
|
2160
|
+
nodeShimWith(sb, 'eisdir.cjs', `
|
|
2161
|
+
const fs = require('node:fs');
|
|
2162
|
+
const real = fs.readFileSync;
|
|
2163
|
+
fs.readFileSync = (target, ...rest) => {
|
|
2164
|
+
if (typeof target === 'string' && target.endsWith('final-message.txt')) {
|
|
2165
|
+
const err = new Error('EISDIR: injected unreadable capture');
|
|
2166
|
+
err.code = 'EISDIR';
|
|
2167
|
+
throw err;
|
|
2168
|
+
}
|
|
2169
|
+
return real(target, ...rest);
|
|
2170
|
+
};
|
|
2171
|
+
`);
|
|
2172
|
+
const r = run(sb, { args: ['--nonce', 'unread', file] });
|
|
2173
|
+
const receipt = readReceipt(storeDirOf(sb), 'unread');
|
|
2174
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
2175
|
+
assert.equal(r.status, 71, r.stderr);
|
|
2176
|
+
assert.match(r.stderr, /is a FAILED probe, not an absent one/);
|
|
2177
|
+
assert.equal(receipt.state, 'reserved', 'nothing terminal was published over a probe that failed');
|
|
2178
|
+
});
|
|
2179
|
+
|
|
2180
|
+
it('a DANGLING SYMLINK at the capture path is a failed probe, never a clean empty report', () => {
|
|
2181
|
+
// The trap the ENOENT split set for itself: readFileSync FOLLOWS the link, so a dangling one
|
|
2182
|
+
// reports ENOENT — indistinguishable from "the delegate wrote nothing" — and a corrupt capture
|
|
2183
|
+
// would be published as an empty report on a `success` receipt. lstat decides first.
|
|
2184
|
+
const sb = makeSandbox();
|
|
2185
|
+
const file = writeContract(sb, 'dangle');
|
|
2186
|
+
nodeShimWith(sb, 'dangling.cjs', `
|
|
2187
|
+
const fs = require('node:fs');
|
|
2188
|
+
const realLstat = fs.lstatSync;
|
|
2189
|
+
fs.lstatSync = (target, ...rest) => {
|
|
2190
|
+
if (typeof target === 'string' && target.endsWith('final-message.txt')) {
|
|
2191
|
+
return { isFile: () => false, isSymbolicLink: () => true, isDirectory: () => false };
|
|
2192
|
+
}
|
|
2193
|
+
return realLstat(target, ...rest);
|
|
2194
|
+
};
|
|
2195
|
+
`);
|
|
2196
|
+
const r = run(sb, { args: ['--nonce', 'dangle', file] });
|
|
2197
|
+
const receipt = readReceipt(storeDirOf(sb), 'dangle');
|
|
2198
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
2199
|
+
assert.equal(r.status, 71, r.stderr);
|
|
2200
|
+
assert.match(r.stderr, /is a symlink, not a regular file — a CORRUPT capture is a FAILED probe/);
|
|
2201
|
+
assert.equal(receipt.state, 'reserved', 'nothing terminal was published over a corrupt capture');
|
|
2202
|
+
});
|
|
2203
|
+
|
|
2204
|
+
it('a run that identified no session records sessionId null with outcome missing-identity', () => {
|
|
2205
|
+
const sb = makeSandbox();
|
|
2206
|
+
const file = writeContract(sb, 'anon');
|
|
2207
|
+
const r = run(sb, { args: ['--nonce', 'anon', file], env: { CODEX_FAKE_NO_THREAD: '1' } });
|
|
2208
|
+
const receipt = readReceipt(storeDirOf(sb), 'anon');
|
|
2209
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
2210
|
+
assert.equal(r.status, 0, r.stderr);
|
|
2211
|
+
assert.equal(receipt.sessionId, null);
|
|
2212
|
+
assert.equal(receipt.outcome, 'missing-identity', 'exit 0 without an identity is never "success"');
|
|
2213
|
+
});
|
|
2214
|
+
|
|
2215
|
+
it('a TIMEOUT publishes exitStatus 124 with outcome transport-failure', async () => {
|
|
2216
|
+
const sb = makeSandbox();
|
|
2217
|
+
const file = writeContract(sb, 'slow');
|
|
2218
|
+
const r = await runAsync(sb, {
|
|
2219
|
+
args: ['--nonce', 'slow', file],
|
|
2220
|
+
env: { CODEX_FAKE_SLEEP: '3', CODEX_HARD_TIMEOUT: '1' },
|
|
2221
|
+
});
|
|
2222
|
+
const receipt = readReceipt(storeDirOf(sb), 'slow');
|
|
2223
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
2224
|
+
assert.equal(r.status, 124, r.stderr);
|
|
2225
|
+
assert.equal(receipt.exitStatus, 124);
|
|
2226
|
+
assert.equal(receipt.outcome, 'transport-failure', 'the timeout codes are transport-failure like any other nonzero exit');
|
|
2227
|
+
assert.match(r.stderr, /exceeded the hard cap/);
|
|
2228
|
+
});
|
|
2229
|
+
|
|
2230
|
+
it('a FOREIGN owner refuses with NOTHING published — both artifacts stay byte-unchanged', () => {
|
|
2231
|
+
const sb = makeSandbox();
|
|
2232
|
+
const file = writeContract(sb, 'tamper');
|
|
2233
|
+
const dir = storeDirOf(sb);
|
|
2234
|
+
const r = run(sb, {
|
|
2235
|
+
args: ['--nonce', 'tamper', file],
|
|
2236
|
+
env: { CODEX_FAKE_TAMPER: join(dir, receiptName('tamper')), CODEX_FAKE_TAMPER_NONCE: 'tamper' },
|
|
2237
|
+
});
|
|
2238
|
+
const receipt = readFileSync(join(dir, receiptName('tamper')), 'utf8');
|
|
2239
|
+
const reportExists = existsSync(join(dir, reportName('tamper')));
|
|
2240
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
2241
|
+
assert.equal(r.status, 70, r.stderr);
|
|
2242
|
+
assert.match(r.stderr, /is not this run’s reservation before any publication/);
|
|
2243
|
+
assert.match(JSON.parse(receipt).owner, /^a-foreign-run$/, 'the foreign reservation is left exactly as it was found');
|
|
2244
|
+
assert.equal(reportExists, false, 'a foreign owner publishes NOTHING — not even the report');
|
|
2245
|
+
assert.match(r.stderr, /PARTIALLY EDITED/, 'and the tree is named dirtied, never silently accepted');
|
|
2246
|
+
});
|
|
2247
|
+
|
|
2248
|
+
it('a failed REPORT write exits 71 naming the report, and the reservation survives for --no-receipt', () => {
|
|
2249
|
+
const sb = makeSandbox();
|
|
2250
|
+
const file = writeContract(sb, 'noreport');
|
|
2251
|
+
const dir = storeDirOf(sb);
|
|
2252
|
+
const r = run(sb, {
|
|
2253
|
+
args: ['--nonce', 'noreport', file],
|
|
2254
|
+
env: { CODEX_FAKE_MKDIR: join(dir, reportName('noreport')) },
|
|
2255
|
+
});
|
|
2256
|
+
const receipt = readReceipt(dir, 'noreport');
|
|
2257
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
2258
|
+
assert.equal(r.status, 71, r.stderr);
|
|
2259
|
+
assert.match(r.stderr, /the delegate’s report could not be published/);
|
|
2260
|
+
assert.equal(receipt.state, 'reserved', 'no terminal receipt EVER appears without a complete report behind it');
|
|
2261
|
+
assert.match(r.stderr, /dispatch return --nonce noreport --no-receipt --exit-status 0 --outcome <o>/);
|
|
2262
|
+
});
|
|
2263
|
+
|
|
2264
|
+
it('an UNWRITABLE store directory stops the REPORT lane nonzero and says the tree is dirtied — NOT the review lane\'s warn-only receipt', () => {
|
|
2265
|
+
// Named for the branch it really reaches. Both artifacts live in one directory, so a directory
|
|
2266
|
+
// turned read-only stops the FIRST write — the report. The two POST-report branches need a
|
|
2267
|
+
// failure injected between the writes, which the two tests below do.
|
|
2268
|
+
const sb = makeSandbox();
|
|
2269
|
+
const file = writeContract(sb, 'rodir');
|
|
2270
|
+
const store = join(sb.root, 'ledger');
|
|
2271
|
+
mkdirSync(store, { recursive: true });
|
|
2272
|
+
const r = run(sb, {
|
|
2273
|
+
args: ['--nonce', 'rodir', file],
|
|
2274
|
+
env: { AW_DELEGATION_STORE: join(store, STORE_BASENAME), CODEX_FAKE_RO_DIR: store },
|
|
2275
|
+
});
|
|
2276
|
+
chmodSync(store, 0o700);
|
|
2277
|
+
const surviving = readReceipt(store, 'rodir');
|
|
2278
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
2279
|
+
assert.notEqual(r.status, 0, 'codex-review.sh warns and returns 0 here; the exec lane must not');
|
|
2280
|
+
assert.equal(r.status, 71, r.stderr);
|
|
2281
|
+
assert.match(r.stderr, /the delegate’s report could not be published/);
|
|
2282
|
+
assert.match(r.stderr, /Nothing beyond the reservation was published/);
|
|
2283
|
+
assert.match(r.stderr, /the working tree may be PARTIALLY EDITED/);
|
|
2284
|
+
assert.equal(surviving.state, 'reserved', 'the reservation survives, so --no-receipt can still absorb the thread');
|
|
2285
|
+
});
|
|
2286
|
+
});
|
|
2287
|
+
|
|
2288
|
+
// ── the two POST-report branches ─────────────────────────────────────────────────────────────────
|
|
2289
|
+
// Reaching them needs a failure BETWEEN the report write and the terminal replace, and no filesystem
|
|
2290
|
+
// state a fixture can set up does that: both artifacts share one directory, so every such condition
|
|
2291
|
+
// stops the report first. The failure is injected instead — through a `node` shim on the sandbox
|
|
2292
|
+
// PATH, the SAME mechanism this suite already uses to exercise missing binaries. It is deliberately
|
|
2293
|
+
// NOT NODE_OPTIONS: the wrapper CLEARS that for every mint core precisely so an inherited
|
|
2294
|
+
// `--require` cannot rewrite `fs` under the owner checks and the publication, and a test that leaned
|
|
2295
|
+
// on it would be testing a hole rather than the behaviour.
|
|
2296
|
+
const nodeShimWith = (sb, name, body) => {
|
|
2297
|
+
const hook = join(sb.root, name);
|
|
2298
|
+
writeFileSync(hook, body);
|
|
2299
|
+
writeFileSync(join(sb.bin, 'node'), [
|
|
2300
|
+
'#!/usr/bin/env bash',
|
|
2301
|
+
'set -u',
|
|
2302
|
+
`exec ${process.execPath} --require ${hook} "$@"`,
|
|
2303
|
+
'',
|
|
2304
|
+
].join('\n'), { mode: 0o755 });
|
|
2305
|
+
};
|
|
2306
|
+
|
|
2307
|
+
// Let the report rename through, then break the NEXT one — the terminal receipt is the only other
|
|
2308
|
+
// `.json` rename the publisher performs.
|
|
2309
|
+
const BREAK_RECEIPT_RENAME = `
|
|
2310
|
+
const fs = require('node:fs');
|
|
2311
|
+
const real = fs.renameSync;
|
|
2312
|
+
fs.renameSync = (from, to) => {
|
|
2313
|
+
if (String(to).endsWith('.json')) {
|
|
2314
|
+
const err = new Error('EACCES: injected terminal-receipt failure');
|
|
2315
|
+
err.code = 'EACCES';
|
|
2316
|
+
throw err;
|
|
2317
|
+
}
|
|
2318
|
+
return real(from, to);
|
|
2319
|
+
};
|
|
2320
|
+
`;
|
|
2321
|
+
|
|
2322
|
+
// Let the report rename through, then hand the SECOND owner check a foreign reservation.
|
|
2323
|
+
const FORGE_SECOND_CLAIM = `
|
|
2324
|
+
const fs = require('node:fs');
|
|
2325
|
+
const realRead = fs.readFileSync;
|
|
2326
|
+
const realRename = fs.renameSync;
|
|
2327
|
+
let reportPublished = false;
|
|
2328
|
+
fs.renameSync = (from, to) => {
|
|
2329
|
+
const out = realRename(from, to);
|
|
2330
|
+
if (String(to).endsWith('.txt')) reportPublished = true;
|
|
2331
|
+
return out;
|
|
2332
|
+
};
|
|
2333
|
+
fs.readFileSync = (target, ...rest) => {
|
|
2334
|
+
const bytes = realRead(target, ...rest);
|
|
2335
|
+
if (reportPublished && typeof target === 'string' && target.endsWith('.json')) {
|
|
2336
|
+
const held = JSON.parse(String(bytes));
|
|
2337
|
+
held.owner = 'a-foreign-run';
|
|
2338
|
+
return JSON.stringify(held);
|
|
2339
|
+
}
|
|
2340
|
+
return bytes;
|
|
2341
|
+
};
|
|
2342
|
+
`;
|
|
2343
|
+
|
|
2344
|
+
describe('codex-exec.sh — the POST-report failure lanes never claim an untouched tree', () => {
|
|
2345
|
+
it('a terminal-receipt write that fails AFTER the report exits 71 and says the report IS published', () => {
|
|
2346
|
+
const sb = makeSandbox();
|
|
2347
|
+
const file = writeContract(sb, 'postr');
|
|
2348
|
+
nodeShimWith(sb, 'break-receipt.cjs', BREAK_RECEIPT_RENAME);
|
|
2349
|
+
const dir = storeDirOf(sb);
|
|
2350
|
+
const r = run(sb, { args: ['--nonce', 'postr', file] });
|
|
2351
|
+
const receipt = readReceipt(dir, 'postr');
|
|
2352
|
+
const report = readFileSync(join(dir, reportName('postr')), 'utf8');
|
|
2353
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
2354
|
+
assert.equal(r.status, 71, r.stderr);
|
|
2355
|
+
assert.match(r.stderr, /the terminal exec receipt could not be published/);
|
|
2356
|
+
assert.match(r.stderr, /The REPORT is published; the terminal receipt was NOT completed/);
|
|
2357
|
+
assert.doesNotMatch(r.stderr, /NOTHING was published/, 'the message must not deny bytes that are on disk');
|
|
2358
|
+
assert.equal(report, 'FAKE_FINAL_MESSAGE\n', 'the published report is complete and readable');
|
|
2359
|
+
assert.equal(receipt.state, 'reserved', 'the replace never happened, so the reservation is what survives here');
|
|
2360
|
+
});
|
|
2361
|
+
|
|
2362
|
+
it('a SECOND owner check that fails after the report exits 71 without claiming nothing was published', () => {
|
|
2363
|
+
const sb = makeSandbox();
|
|
2364
|
+
const file = writeContract(sb, 'forge');
|
|
2365
|
+
nodeShimWith(sb, 'forge-owner.cjs', FORGE_SECOND_CLAIM);
|
|
2366
|
+
const dir = storeDirOf(sb);
|
|
2367
|
+
const r = run(sb, { args: ['--nonce', 'forge', file] });
|
|
2368
|
+
const reportExists = existsSync(join(dir, reportName('forge')));
|
|
2369
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
2370
|
+
assert.equal(r.status, 71, r.stderr);
|
|
2371
|
+
assert.match(r.stderr, /is not this run’s reservation immediately before the terminal replace: its owner token belongs to ANOTHER run/);
|
|
2372
|
+
assert.match(r.stderr, /The REPORT is published; the terminal receipt was NOT completed/);
|
|
2373
|
+
assert.doesNotMatch(r.stderr, /NOTHING was published/, 'after the report is on disk that claim is false');
|
|
2374
|
+
assert.equal(reportExists, true, 'and the report really is there — which is what the message says');
|
|
2375
|
+
});
|
|
2376
|
+
|
|
2377
|
+
it('an inherited NODE_OPTIONS cannot reach the mint cores — the wrapper clears it', () => {
|
|
2378
|
+
// The regression guard for the hole the two tests above used to lean on: a --require inherited
|
|
2379
|
+
// from the caller's environment could rewrite `fs` inside the owner checks and the publication.
|
|
2380
|
+
// Here the SAME preload that breaks the receipt rename is handed to the wrapper as NODE_OPTIONS
|
|
2381
|
+
// rather than through the PATH shim; the run must be entirely unaffected.
|
|
2382
|
+
const sb = makeSandbox();
|
|
2383
|
+
const file = writeContract(sb, 'envopt');
|
|
2384
|
+
const hook = join(sb.root, 'break-receipt.cjs');
|
|
2385
|
+
writeFileSync(hook, BREAK_RECEIPT_RENAME);
|
|
2386
|
+
const r = run(sb, { args: ['--nonce', 'envopt', file], env: { NODE_OPTIONS: `--require=${hook}` } });
|
|
2387
|
+
const receipt = readReceipt(storeDirOf(sb), 'envopt');
|
|
2388
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
2389
|
+
assert.equal(r.status, 0, r.stderr);
|
|
2390
|
+
assert.equal(receipt.state, 'terminal', 'the publication completed — the injected failure never reached it');
|
|
2391
|
+
assert.equal(receipt.outcome, 'success');
|
|
2392
|
+
});
|
|
2393
|
+
|
|
2394
|
+
it('a FIRST owner check that fails is the only lane that may claim NOTHING was published', () => {
|
|
2395
|
+
const sb = makeSandbox();
|
|
2396
|
+
const file = writeContract(sb, 'first');
|
|
2397
|
+
const dir = storeDirOf(sb);
|
|
2398
|
+
const r = run(sb, {
|
|
2399
|
+
args: ['--nonce', 'first', file],
|
|
2400
|
+
env: { CODEX_FAKE_TAMPER: join(dir, receiptName('first')), CODEX_FAKE_TAMPER_NONCE: 'first' },
|
|
2401
|
+
});
|
|
2402
|
+
const reportExists = existsSync(join(dir, reportName('first')));
|
|
2403
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
2404
|
+
assert.equal(r.status, 70, r.stderr);
|
|
2405
|
+
assert.match(r.stderr, /NOTHING was published — not the report, not the receipt/);
|
|
2406
|
+
assert.doesNotMatch(r.stderr, /--no-receipt --exit-status/, 'a foreign reservation is never a --no-receipt source: its posture belongs to another run');
|
|
2407
|
+
assert.equal(reportExists, false);
|
|
2408
|
+
});
|
|
2409
|
+
});
|
|
2410
|
+
|
|
2411
|
+
describe('codex-exec.sh — the inline node mint cores stay intact', () => {
|
|
2412
|
+
it('the wrapper parses', () => {
|
|
2413
|
+
// The apostrophe SCANNER this replaced is gone with the class it policed: every mint core now
|
|
2414
|
+
// rides a QUOTED heredoc, where an apostrophe is ordinary text. A scanner that had to model
|
|
2415
|
+
// shell quoting was a second parser for a problem the quoting choice created — and it had its own
|
|
2416
|
+
// blind spot (a JS line beginning `' ` read as a closing quote and ended the scan early).
|
|
2417
|
+
const syntax = spawnSync('bash', ['-n', WRAPPER], { encoding: 'utf8' });
|
|
2418
|
+
assert.equal(syntax.status, 0, syntax.stderr);
|
|
2419
|
+
});
|
|
2420
|
+
|
|
2421
|
+
it('every mint core is read by a BUILTIN, run with NODE_OPTIONS cleared, and asserted non-empty', () => {
|
|
2422
|
+
const source = readFileSync(WRAPPER, 'utf8');
|
|
2423
|
+
assert.equal((source.match(/^ *IFS= read -r -d '' aw_js <<'AW_JS' \|\| true$/gm) ?? []).length, 4,
|
|
2424
|
+
'the four cores: the store-dir resolver, the contract header, the reservation, the terminal publication');
|
|
2425
|
+
assert.equal((source.match(/^AW_JS$/gm) ?? []).length, 4, 'each heredoc is terminated');
|
|
2426
|
+
assert.equal((source.match(/NODE_OPTIONS= node -e "\$aw_js"/g) ?? []).length, 4, 'every core runs with NODE_OPTIONS cleared');
|
|
2427
|
+
assert.equal((source.match(/aw_require_core "\$aw_js"/g) ?? []).length, 4, 'and none runs before it is proven non-empty');
|
|
2428
|
+
// The regressions this replaces, both live: a single-quoted -e string truncates on an apostrophe,
|
|
2429
|
+
// and a `$(cat …)` substitution turns a missing `cat` into `node -e ""` exiting 0 — a run that
|
|
2430
|
+
// publishes nothing and reports success.
|
|
2431
|
+
assert.equal((source.match(/node -e '/g) ?? []).length, 0, 'no core may go back to a single-quoted -e string');
|
|
2432
|
+
assert.equal((source.match(/node -e "\$\(cat/g) ?? []).length, 0, 'no core may go back to a cat substitution');
|
|
2433
|
+
});
|
|
2434
|
+
});
|