@sabaiway/agent-workflow-kit 3.0.0 → 3.2.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 +82 -1
- package/README.md +1 -0
- package/SKILL.md +7 -3
- package/bin/install.mjs +10 -1
- package/bridges/antigravity-cli-bridge/SKILL.md +6 -4
- package/bridges/antigravity-cli-bridge/bin/agy-review-honesty.test.mjs +20 -7
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +84 -7
- package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +189 -18
- package/bridges/antigravity-cli-bridge/bin/agy.sh +55 -6
- package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +77 -41
- package/bridges/antigravity-cli-bridge/capability.json +4 -2
- package/bridges/antigravity-cli-bridge/references/driving-agy.md +5 -0
- package/bridges/codex-cli-bridge/SKILL.md +8 -4
- package/bridges/codex-cli-bridge/bin/codex-exec.sh +129 -11
- package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +278 -23
- package/bridges/codex-cli-bridge/bin/codex-review-honesty.test.mjs +20 -7
- package/bridges/codex-cli-bridge/bin/codex-review.sh +76 -13
- package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +112 -17
- package/bridges/codex-cli-bridge/capability.json +19 -4
- package/bridges/codex-cli-bridge/references/driving-codex.md +11 -0
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/bootstrap.md +3 -2
- package/references/modes/recommendations.md +1 -0
- package/references/modes/upgrade.md +9 -4
- package/references/modes/velocity.md +2 -0
- package/references/modes/worktrees.md +120 -0
- package/references/scripts/archive-decisions.mjs +2 -2
- package/references/scripts/archive-decisions.test.mjs +5 -5
- package/references/scripts/check-docs-size-cli.test.mjs +41 -0
- package/references/scripts/check-docs-size.mjs +46 -29
- package/references/shared/command-shapes.md +22 -0
- package/references/shared/composition-handoff.md +7 -2
- package/references/templates/agent_rules.md +10 -1
- package/tools/autonomy-doctor.mjs +1 -1
- package/tools/bridge-settings-read.mjs +25 -5
- package/tools/changed-surface.mjs +8 -8
- package/tools/commands.mjs +9 -0
- package/tools/delegation.mjs +2 -2
- package/tools/detect-backends.mjs +10 -0
- package/tools/grounding.mjs +13 -13
- package/tools/inject-methodology.mjs +71 -40
- package/tools/lens-region.mjs +113 -36
- package/tools/migrate-adr-store.mjs +3 -3
- package/tools/procedures.mjs +1 -1
- package/tools/recipes.mjs +2 -2
- package/tools/recommendations.mjs +34 -7
- package/tools/release-scan.mjs +12 -2
- package/tools/review-state.mjs +3 -3
- package/tools/sandbox-masks.mjs +13 -13
- package/tools/set-recipe.mjs +1 -1
- package/tools/velocity-profile.mjs +7 -7
- package/tools/worktrees.mjs +2292 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { describe, it } from 'node:test';
|
|
1
|
+
import { describe, it, after } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import {
|
|
4
4
|
mkdtempSync, mkdirSync, writeFileSync, chmodSync, rmSync, readFileSync,
|
|
5
|
-
existsSync, readdirSync, symlinkSync,
|
|
5
|
+
existsSync, readdirSync, symlinkSync, cpSync,
|
|
6
6
|
} from 'node:fs';
|
|
7
7
|
import { tmpdir } from 'node:os';
|
|
8
8
|
import { join, dirname, resolve } from 'node:path';
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
|
-
import { spawnSync } from 'node:child_process';
|
|
10
|
+
import { spawnSync, execFile } from 'node:child_process';
|
|
11
11
|
|
|
12
12
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
13
13
|
const WRAPPER = join(HERE, 'codex-exec.sh');
|
|
@@ -28,7 +28,7 @@ const FAKE_CODEX = [
|
|
|
28
28
|
'{ for a in "$@"; do echo "$a"; done; } >"$CODEX_FAKE_ARGV"',
|
|
29
29
|
'{ 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
30
|
'cat >"$CODEX_FAKE_STDIN"',
|
|
31
|
-
'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 $?)"; } > "${CODEX_FAKE_GIT_RESULT:-/dev/null}" 2>&1; fi',
|
|
31
|
+
'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',
|
|
32
32
|
'if [[ -n "${CODEX_FAKE_SLEEP:-}" ]]; then sleep "${CODEX_FAKE_SLEEP}"; fi',
|
|
33
33
|
'out=""',
|
|
34
34
|
'prev=""',
|
|
@@ -55,13 +55,16 @@ const FAKE_CODEX = [
|
|
|
55
55
|
'',
|
|
56
56
|
].join('\n');
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
// The sandbox base and the PATH farms are READ-ONLY per invocation, so both are built ONCE and
|
|
59
|
+
// shared (a per-test `git init`+commit and a per-call farm rebuild dominate the wall otherwise).
|
|
60
|
+
const SHARED_ROOT = mkdtempSync(join(tmpdir(), 'codex-exec-shared-'));
|
|
61
|
+
after(() => rmSync(SHARED_ROOT, { recursive: true, force: true }));
|
|
62
|
+
|
|
63
|
+
const TEMPLATE_ROOT = (() => {
|
|
64
|
+
const root = join(SHARED_ROOT, 'template-root');
|
|
60
65
|
const bin = join(root, 'bin');
|
|
61
66
|
mkdirSync(bin, { recursive: true });
|
|
62
|
-
|
|
63
|
-
writeFileSync(stub, FAKE_CODEX, { mode: 0o755 });
|
|
64
|
-
chmodSync(stub, 0o755);
|
|
67
|
+
writeFileSync(join(bin, 'codex'), FAKE_CODEX, { mode: 0o755 });
|
|
65
68
|
// A git work tree with a root AGENTS.md — the wrapper preflights both.
|
|
66
69
|
const repo = join(root, 'repo');
|
|
67
70
|
mkdirSync(repo);
|
|
@@ -72,7 +75,15 @@ const makeSandbox = () => {
|
|
|
72
75
|
writeFileSync(join(repo, 'AGENTS.md'), '# AGENTS\n\nHard Constraints: none (test fixture).\n');
|
|
73
76
|
g('add', '-A');
|
|
74
77
|
g('commit', '-qm', 'base');
|
|
75
|
-
return
|
|
78
|
+
return root;
|
|
79
|
+
})();
|
|
80
|
+
|
|
81
|
+
const makeSandbox = () => {
|
|
82
|
+
const root = mkdtempSync(join(tmpdir(), 'codex-exec-test-'));
|
|
83
|
+
cpSync(TEMPLATE_ROOT, root, { recursive: true });
|
|
84
|
+
const bin = join(root, 'bin');
|
|
85
|
+
chmodSync(join(bin, 'codex'), 0o755);
|
|
86
|
+
return { root, bin, repo: join(root, 'repo') };
|
|
76
87
|
};
|
|
77
88
|
|
|
78
89
|
// A PATH dir mirroring the real one MINUS the named binaries, to exercise the
|
|
@@ -96,6 +107,13 @@ const makePathWithout = (root, exclude = []) => {
|
|
|
96
107
|
return dir;
|
|
97
108
|
};
|
|
98
109
|
|
|
110
|
+
const farms = new Map();
|
|
111
|
+
const farmFor = (exclude) => {
|
|
112
|
+
const key = exclude.join('|');
|
|
113
|
+
if (!farms.has(key)) farms.set(key, makePathWithout(SHARED_ROOT, exclude));
|
|
114
|
+
return farms.get(key);
|
|
115
|
+
};
|
|
116
|
+
|
|
99
117
|
const run = ({ repo, bin }, { args = ['-'], input = 'do the thing', env = {}, path, cwd } = {}) => {
|
|
100
118
|
const argvFile = join(repo, '.cap-argv');
|
|
101
119
|
const envFile = join(repo, '.cap-env');
|
|
@@ -121,6 +139,33 @@ const run = ({ repo, bin }, { args = ['-'], input = 'do the thing', env = {}, pa
|
|
|
121
139
|
return { ...r, argv: readIf(argvFile), capEnv: readIf(envFile), capStdin: readIf(stdinFile) };
|
|
122
140
|
};
|
|
123
141
|
|
|
142
|
+
// Async twin of run() for the sleep-bound timeout tests: spawnSync blocks the event loop for
|
|
143
|
+
// the whole deliberate wait, so a concurrent describe could not overlap them. Same contract.
|
|
144
|
+
const runAsync = ({ repo, bin }, { args = ['-'], input = 'do the thing', env = {}, path, cwd } = {}) =>
|
|
145
|
+
new Promise((done) => {
|
|
146
|
+
const argvFile = join(repo, '.cap-argv');
|
|
147
|
+
const envFile = join(repo, '.cap-env');
|
|
148
|
+
const stdinFile = join(repo, '.cap-stdin');
|
|
149
|
+
const child = execFile('bash', [WRAPPER, ...args], {
|
|
150
|
+
cwd: cwd || repo,
|
|
151
|
+
encoding: 'utf8',
|
|
152
|
+
timeout: 30000,
|
|
153
|
+
env: {
|
|
154
|
+
PATH: path || `${bin}:${process.env.PATH}`,
|
|
155
|
+
HOME: repo,
|
|
156
|
+
TMPDIR: process.env.TMPDIR ?? '/tmp',
|
|
157
|
+
CODEX_FAKE_ARGV: argvFile,
|
|
158
|
+
CODEX_FAKE_ENV: envFile,
|
|
159
|
+
CODEX_FAKE_STDIN: stdinFile,
|
|
160
|
+
...env,
|
|
161
|
+
},
|
|
162
|
+
}, (error, stdout, stderr) => {
|
|
163
|
+
const readIf = (p) => (existsSync(p) ? readFileSync(p, 'utf8') : '');
|
|
164
|
+
done({ status: error ? (error.code ?? 1) : 0, stdout, stderr, argv: readIf(argvFile), capEnv: readIf(envFile), capStdin: readIf(stdinFile) });
|
|
165
|
+
});
|
|
166
|
+
child.stdin.end(input);
|
|
167
|
+
});
|
|
168
|
+
|
|
124
169
|
describe('codex-exec.sh — quality-first model/effort guard (1.1)', () => {
|
|
125
170
|
it('refuses a non-default CODEX_MODEL and never spends a run', () => {
|
|
126
171
|
const sb = makeSandbox();
|
|
@@ -344,11 +389,11 @@ describe('codex-exec.sh — leaner prompt (1.4)', () => {
|
|
|
344
389
|
});
|
|
345
390
|
});
|
|
346
391
|
|
|
347
|
-
describe('codex-exec.sh — hard timeout (1.3)', () => {
|
|
348
|
-
it('kills a hung codex at CODEX_HARD_TIMEOUT and reports it', () => {
|
|
392
|
+
describe('codex-exec.sh — hard timeout (1.3)', { concurrency: true }, () => {
|
|
393
|
+
it('kills a hung codex at CODEX_HARD_TIMEOUT and reports it', async () => {
|
|
349
394
|
const sb = makeSandbox();
|
|
350
395
|
const started = Date.now();
|
|
351
|
-
const r =
|
|
396
|
+
const r = await runAsync(sb, { env: { CODEX_FAKE_SLEEP: '30', CODEX_HARD_TIMEOUT: '2' } });
|
|
352
397
|
const elapsed = Date.now() - started;
|
|
353
398
|
rmSync(sb.root, { recursive: true, force: true });
|
|
354
399
|
assert.ok(elapsed < 18000, `must return well under the kill-after window, took ${elapsed}ms`);
|
|
@@ -358,7 +403,7 @@ describe('codex-exec.sh — hard timeout (1.3)', () => {
|
|
|
358
403
|
|
|
359
404
|
it('warns and runs uncapped when neither timeout nor gtimeout is on PATH', () => {
|
|
360
405
|
const sb = makeSandbox();
|
|
361
|
-
const path = `${sb.bin}:${
|
|
406
|
+
const path = `${sb.bin}:${farmFor(['timeout', 'gtimeout'])}`;
|
|
362
407
|
const r = run(sb, { path });
|
|
363
408
|
rmSync(sb.root, { recursive: true, force: true });
|
|
364
409
|
assert.equal(r.status, 0, r.stderr);
|
|
@@ -368,7 +413,7 @@ describe('codex-exec.sh — hard timeout (1.3)', () => {
|
|
|
368
413
|
|
|
369
414
|
it('resume runs uncapped (and warns) when no timeout binary is on PATH', () => {
|
|
370
415
|
const sb = makeSandbox();
|
|
371
|
-
const path = `${sb.bin}:${
|
|
416
|
+
const path = `${sb.bin}:${farmFor(['timeout', 'gtimeout'])}`;
|
|
372
417
|
const r = run(sb, { args: ['--resume', 'sess-1', '-'], input: 'go', path });
|
|
373
418
|
rmSync(sb.root, { recursive: true, force: true });
|
|
374
419
|
assert.equal(r.status, 0, r.stderr);
|
|
@@ -545,7 +590,7 @@ describe('codex-exec.sh — environment preflight (fail fast, before a run)', ()
|
|
|
545
590
|
it('STOPs with 127 when codex is not on PATH', () => {
|
|
546
591
|
const sb = makeSandbox();
|
|
547
592
|
// PATH WITHOUT the fake codex bin and without any real codex.
|
|
548
|
-
const path =
|
|
593
|
+
const path = farmFor(['codex']);
|
|
549
594
|
const r = run(sb, { path });
|
|
550
595
|
rmSync(sb.root, { recursive: true, force: true });
|
|
551
596
|
assert.equal(r.status, 127);
|
|
@@ -556,7 +601,7 @@ describe('codex-exec.sh — environment preflight (fail fast, before a run)', ()
|
|
|
556
601
|
it('STOPs with 127 when git is not on PATH', () => {
|
|
557
602
|
const sb = makeSandbox();
|
|
558
603
|
// codex present (sb.bin) but git stripped — exercises the type -P git guard.
|
|
559
|
-
const path = `${sb.bin}:${
|
|
604
|
+
const path = `${sb.bin}:${farmFor(['git'])}`;
|
|
560
605
|
const r = run(sb, { path });
|
|
561
606
|
rmSync(sb.root, { recursive: true, force: true });
|
|
562
607
|
assert.equal(r.status, 127);
|
|
@@ -681,7 +726,7 @@ const runHelp = (arg) => {
|
|
|
681
726
|
const root = mkdtempSync(join(tmpdir(), 'codex-exec-help-'));
|
|
682
727
|
const nongit = join(root, 'nongit');
|
|
683
728
|
mkdirSync(nongit, { recursive: true });
|
|
684
|
-
const path =
|
|
729
|
+
const path = farmFor(['codex', 'agy', 'git']);
|
|
685
730
|
const r = spawnSync('bash', [WRAPPER, arg], {
|
|
686
731
|
cwd: nongit, encoding: 'utf8', timeout: 15000, env: { HOME: root, PATH: path },
|
|
687
732
|
});
|
|
@@ -1027,20 +1072,20 @@ describe('codex-exec.sh — service tier knob (bridges 2.3.0)', () => {
|
|
|
1027
1072
|
|
|
1028
1073
|
});
|
|
1029
1074
|
|
|
1030
|
-
describe('codex-exec.sh — bridge settings file semantics (bridges 2.3.0)', () => {
|
|
1031
|
-
it('env overrides file: CODEX_HARD_TIMEOUT env=2 file=9999 → killed at the env cap', () => {
|
|
1075
|
+
describe('codex-exec.sh — bridge settings file semantics (bridges 2.3.0)', { concurrency: true }, () => {
|
|
1076
|
+
it('env overrides file: CODEX_HARD_TIMEOUT env=2 file=9999 → killed at the env cap', async () => {
|
|
1032
1077
|
const sb = makeSandbox();
|
|
1033
1078
|
writeSettings(sb, 'CODEX_HARD_TIMEOUT=9999\n');
|
|
1034
|
-
const r =
|
|
1079
|
+
const r = await runAsync(sb, { env: { CODEX_FAKE_SLEEP: '5', CODEX_HARD_TIMEOUT: '2' } });
|
|
1035
1080
|
rmSync(sb.root, { recursive: true, force: true });
|
|
1036
1081
|
assert.notEqual(r.status, 0, 'the env cap (2s) must win over the file cap (9999s)');
|
|
1037
1082
|
assert.match(r.stderr, /exceeded the hard cap CODEX_HARD_TIMEOUT=2s/);
|
|
1038
1083
|
});
|
|
1039
1084
|
|
|
1040
|
-
it('a file-set CODEX_HARD_TIMEOUT is effective (killed at the file cap)', () => {
|
|
1085
|
+
it('a file-set CODEX_HARD_TIMEOUT is effective (killed at the file cap)', async () => {
|
|
1041
1086
|
const sb = makeSandbox();
|
|
1042
1087
|
writeSettings(sb, 'CODEX_HARD_TIMEOUT=2\n');
|
|
1043
|
-
const r =
|
|
1088
|
+
const r = await runAsync(sb, { env: { CODEX_FAKE_SLEEP: '5' } });
|
|
1044
1089
|
rmSync(sb.root, { recursive: true, force: true });
|
|
1045
1090
|
assert.notEqual(r.status, 0, 'the file cap must apply when the env is unset');
|
|
1046
1091
|
assert.match(r.stderr, /exceeded the hard cap CODEX_HARD_TIMEOUT=2s/);
|
|
@@ -1225,3 +1270,213 @@ describe('codex-exec.sh — settings surface ⟷ manifest (D6, manifest-pinned)'
|
|
|
1225
1270
|
}
|
|
1226
1271
|
});
|
|
1227
1272
|
});
|
|
1273
|
+
|
|
1274
|
+
describe('codex-exec.sh — dispatch-posture labeling (D5, AD-061)', () => {
|
|
1275
|
+
const banners = (stderr) => stderr.split('\n').filter((l) => l.startsWith('exec posture: '));
|
|
1276
|
+
|
|
1277
|
+
it('ONE banner line carries the ACTUAL {model, effort, tier, sandbox, session, timeout} on a fresh run', () => {
|
|
1278
|
+
const sb = makeSandbox();
|
|
1279
|
+
const r = run(sb, {});
|
|
1280
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1281
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1282
|
+
const lines = banners(r.stderr);
|
|
1283
|
+
assert.equal(lines.length, 1, 'EXACTLY ONE banner line per run');
|
|
1284
|
+
assert.equal(lines[0],
|
|
1285
|
+
'exec posture: model=gpt-5.6-sol effort=xhigh tier=standard sandbox=workspace-write session=fresh timeout=3600s');
|
|
1286
|
+
});
|
|
1287
|
+
|
|
1288
|
+
it('an ARMED Fast tier rides the banner (tier=priority)', () => {
|
|
1289
|
+
const sb = makeSandbox();
|
|
1290
|
+
const r = run(sb, { env: { CODEX_SERVICE_TIER: 'priority' } });
|
|
1291
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1292
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1293
|
+
assert.match(r.stderr, /^exec posture: .* tier=priority .*$/m);
|
|
1294
|
+
});
|
|
1295
|
+
|
|
1296
|
+
it('a resume banner carries the RESOLVED session id (explicit --resume)', () => {
|
|
1297
|
+
const sb = makeSandbox();
|
|
1298
|
+
const r = run(sb, { args: ['--resume', 'sess-xyz', '-'] });
|
|
1299
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1300
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1301
|
+
const lines = banners(r.stderr);
|
|
1302
|
+
assert.equal(lines.length, 1, 'EXACTLY ONE banner line on resume too');
|
|
1303
|
+
assert.match(lines[0], / session=resume:sess-xyz /, 'the banner names the resolved id');
|
|
1304
|
+
});
|
|
1305
|
+
|
|
1306
|
+
it('--resume-last resolves the sidecar id into the banner', () => {
|
|
1307
|
+
const sb = makeSandbox();
|
|
1308
|
+
writeFileSync(join(sb.repo, '.codex-last-session'), 'sess-from-sidecar\n');
|
|
1309
|
+
const r = run(sb, { args: ['--resume-last', '-'] });
|
|
1310
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1311
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1312
|
+
assert.match(r.stderr, / session=resume:sess-from-sidecar /);
|
|
1313
|
+
});
|
|
1314
|
+
|
|
1315
|
+
it('a HOSTILE/malformed EXPLICIT session id refuses pre-spend (no codex invocation)', () => {
|
|
1316
|
+
for (const hostile of ['evil;rm -rf /', 'a b', `x${String.fromCharCode(1)}y`]) {
|
|
1317
|
+
const sb = makeSandbox();
|
|
1318
|
+
const r = run(sb, { args: ['--resume', hostile, '-'] });
|
|
1319
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1320
|
+
assert.notEqual(r.status, 0, `must refuse: ${JSON.stringify(hostile)}`);
|
|
1321
|
+
assert.equal(r.capStdin, '', 'codex is never invoked');
|
|
1322
|
+
assert.match(r.stderr, /session id/i, 'named as the session-id class');
|
|
1323
|
+
}
|
|
1324
|
+
});
|
|
1325
|
+
|
|
1326
|
+
it('a HOSTILE SIDECAR-READ session id refuses pre-spend the same way', () => {
|
|
1327
|
+
const sb = makeSandbox();
|
|
1328
|
+
writeFileSync(join(sb.repo, '.codex-last-session'), 'evil$(touch pwned)\n');
|
|
1329
|
+
const r = run(sb, { args: ['--resume-last', '-'] });
|
|
1330
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1331
|
+
assert.notEqual(r.status, 0);
|
|
1332
|
+
assert.equal(r.capStdin, '', 'codex is never invoked');
|
|
1333
|
+
assert.match(r.stderr, /session id/i);
|
|
1334
|
+
});
|
|
1335
|
+
|
|
1336
|
+
it('a FLAG-SHAPED sidecar id (leading dash) refuses at the grammar — never reaches codex as an option', () => {
|
|
1337
|
+
for (const bad of ['--last\n', '-x\n']) {
|
|
1338
|
+
const sb = makeSandbox();
|
|
1339
|
+
writeFileSync(join(sb.repo, '.codex-last-session'), bad);
|
|
1340
|
+
const r = run(sb, { args: ['--resume-last', '-'] });
|
|
1341
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1342
|
+
assert.notEqual(r.status, 0, `must refuse: ${JSON.stringify(bad)}`);
|
|
1343
|
+
assert.equal(r.capStdin, '', 'codex is never invoked');
|
|
1344
|
+
assert.match(r.stderr, /session id/i, 'refused at the grammar, never parsed as a codex option');
|
|
1345
|
+
}
|
|
1346
|
+
});
|
|
1347
|
+
|
|
1348
|
+
it('a sidecar carrying a NUL byte refuses pre-spend — bash would silently repair it into a valid id', () => {
|
|
1349
|
+
const sb = makeSandbox();
|
|
1350
|
+
writeFileSync(join(sb.repo, '.codex-last-session'), Buffer.from('sess-\0target\n', 'binary'));
|
|
1351
|
+
const r = run(sb, { args: ['--resume-last', '-'] });
|
|
1352
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1353
|
+
assert.notEqual(r.status, 0);
|
|
1354
|
+
assert.equal(r.capStdin, '', 'codex is never invoked');
|
|
1355
|
+
assert.match(r.stderr, /NUL/i, 'named as the NUL class — the raw bytes are checked before the shell variable');
|
|
1356
|
+
});
|
|
1357
|
+
|
|
1358
|
+
it('a valid id containing the ASCII digit 0 gets no false NUL refusal', () => {
|
|
1359
|
+
const sb = makeSandbox();
|
|
1360
|
+
writeFileSync(join(sb.repo, '.codex-last-session'), 'sess-01\n');
|
|
1361
|
+
const r = run(sb, { args: ['--resume-last', '-'] });
|
|
1362
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1363
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1364
|
+
assert.match(r.stderr, / session=resume:sess-01 /);
|
|
1365
|
+
});
|
|
1366
|
+
|
|
1367
|
+
it('a sidecar id with inner WHITESPACE refuses — never silently repaired into a different id', () => {
|
|
1368
|
+
for (const bad of ['sess bad\n', 'sess\tbad\n']) {
|
|
1369
|
+
const sb = makeSandbox();
|
|
1370
|
+
writeFileSync(join(sb.repo, '.codex-last-session'), bad);
|
|
1371
|
+
const r = run(sb, { args: ['--resume-last', '-'] });
|
|
1372
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1373
|
+
assert.notEqual(r.status, 0, `must refuse: ${JSON.stringify(bad)}`);
|
|
1374
|
+
assert.equal(r.capStdin, '', 'codex is never invoked');
|
|
1375
|
+
assert.match(r.stderr, /session id/i, 'the grammar refusal fires — the id is never whitespace-stripped into validity');
|
|
1376
|
+
}
|
|
1377
|
+
});
|
|
1378
|
+
|
|
1379
|
+
it('a banner field carrying CONTROL BYTES refuses pre-spend (model / effort / tier / timeout / DEL)', () => {
|
|
1380
|
+
const cases = [
|
|
1381
|
+
{ CODEX_MODEL: `gpt-5.6-sol${String.fromCharCode(1)}` },
|
|
1382
|
+
{ CODEX_EFFORT: `xhigh${String.fromCharCode(2)}` },
|
|
1383
|
+
{ CODEX_SERVICE_TIER: `priority${String.fromCharCode(3)}` },
|
|
1384
|
+
{ CODEX_HARD_TIMEOUT: `3600${String.fromCharCode(4)}` },
|
|
1385
|
+
{ CODEX_MODEL: `gpt-5.6-sol${String.fromCharCode(127)}` },
|
|
1386
|
+
];
|
|
1387
|
+
for (const env of cases) {
|
|
1388
|
+
const sb = makeSandbox();
|
|
1389
|
+
const r = run(sb, { env });
|
|
1390
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1391
|
+
assert.notEqual(r.status, 0, `must refuse: ${JSON.stringify(env)}`);
|
|
1392
|
+
assert.equal(r.capStdin, '', 'codex is never invoked');
|
|
1393
|
+
assert.match(r.stderr, /control/i, 'named as the control-byte class');
|
|
1394
|
+
}
|
|
1395
|
+
});
|
|
1396
|
+
|
|
1397
|
+
it('timeout honesty: no timeout/gtimeout on PATH → timeout=uncapped, never a fabricated number', () => {
|
|
1398
|
+
const sb = makeSandbox();
|
|
1399
|
+
const r = run(sb, { path: `${sb.bin}:${farmFor(['timeout', 'gtimeout'])}` });
|
|
1400
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1401
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1402
|
+
assert.match(r.stderr, /^exec posture: .* timeout=uncapped$/m);
|
|
1403
|
+
});
|
|
1404
|
+
|
|
1405
|
+
it('an EXPORTED shell function shadowing timeout never fools the banner (type -P discipline)', () => {
|
|
1406
|
+
const sb = makeSandbox();
|
|
1407
|
+
const r = run(sb, {
|
|
1408
|
+
path: `${sb.bin}:${farmFor(['timeout', 'gtimeout'])}`,
|
|
1409
|
+
env: { 'BASH_FUNC_timeout%%': '() { return 0; }' },
|
|
1410
|
+
});
|
|
1411
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1412
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1413
|
+
assert.match(r.stderr, /^exec posture: .* timeout=uncapped$/m, 'a shell function is not a capping binary');
|
|
1414
|
+
});
|
|
1415
|
+
|
|
1416
|
+
it('an EXPORTED `type` function faking a path never fools the resolver (builtin type discipline)', () => {
|
|
1417
|
+
const sb = makeSandbox();
|
|
1418
|
+
const r = run(sb, {
|
|
1419
|
+
path: `${sb.bin}:${farmFor(['timeout', 'gtimeout'])}`,
|
|
1420
|
+
env: { 'BASH_FUNC_type%%': '() { echo /fake/timeout; }' },
|
|
1421
|
+
});
|
|
1422
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1423
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1424
|
+
assert.match(r.stderr, /^exec posture: .* timeout=uncapped$/m, 'builtin type bypasses an exported type function');
|
|
1425
|
+
});
|
|
1426
|
+
|
|
1427
|
+
it('a RELATIVE first PATH git entry + shadowed dirname/basename still bakes an ABSOLUTE real git into the shim', () => {
|
|
1428
|
+
const sb = makeSandbox();
|
|
1429
|
+
const realGit = (process.env.PATH || '').split(':').filter(Boolean).map((d) => join(d, 'git')).find((p) => existsSync(p));
|
|
1430
|
+
assert.ok(realGit, 'a real git exists on PATH');
|
|
1431
|
+
mkdirSync(join(sb.repo, 'relgit'), { recursive: true });
|
|
1432
|
+
writeFileSync(join(sb.repo, 'relgit', 'git'), `#!/usr/bin/env bash\nexec ${realGit} "$@"\n`, { mode: 0o755 });
|
|
1433
|
+
const gitResult = join(sb.repo, '.cap-git');
|
|
1434
|
+
const r = run(sb, {
|
|
1435
|
+
path: `relgit:${sb.bin}:${process.env.PATH}`,
|
|
1436
|
+
env: {
|
|
1437
|
+
CODEX_FAKE_GIT_PROBE: '1',
|
|
1438
|
+
CODEX_FAKE_GIT_RESULT: gitResult,
|
|
1439
|
+
'BASH_FUNC_dirname%%': '() { echo /shadowed; }',
|
|
1440
|
+
'BASH_FUNC_basename%%': '() { echo shadowed; }',
|
|
1441
|
+
},
|
|
1442
|
+
});
|
|
1443
|
+
const probe = existsSync(gitResult) ? readFileSync(gitResult, 'utf8') : '';
|
|
1444
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1445
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1446
|
+
assert.match(probe, /cdaway=0/, 'the shim git works from a different cwd — the embedded path is absolute, shadow-proof');
|
|
1447
|
+
});
|
|
1448
|
+
|
|
1449
|
+
it('a RELATIVE PATH entry still yields an ABSOLUTE capping binary (the stub sees an absolute $0)', () => {
|
|
1450
|
+
const sb = makeSandbox();
|
|
1451
|
+
mkdirSync(join(sb.repo, 'relbin'), { recursive: true });
|
|
1452
|
+
const cap = join(sb.repo, '.stub-argv0');
|
|
1453
|
+
writeFileSync(join(sb.repo, 'relbin', 'timeout'), [
|
|
1454
|
+
'#!/usr/bin/env bash',
|
|
1455
|
+
'echo "$0" >"$TIMEOUT_STUB_CAP"',
|
|
1456
|
+
'while [[ "$1" == --* ]]; do shift; done',
|
|
1457
|
+
'shift',
|
|
1458
|
+
'exec "$@"',
|
|
1459
|
+
'',
|
|
1460
|
+
].join('\n'), { mode: 0o755 });
|
|
1461
|
+
const r = run(sb, {
|
|
1462
|
+
path: `relbin:${sb.bin}:${farmFor(['timeout', 'gtimeout'])}`,
|
|
1463
|
+
env: { TIMEOUT_STUB_CAP: cap },
|
|
1464
|
+
});
|
|
1465
|
+
const argv0 = existsSync(cap) ? readFileSync(cap, 'utf8').trim() : '';
|
|
1466
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1467
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1468
|
+
assert.match(r.stderr, /^exec posture: .* timeout=3600s$/m, 'the relative-PATH binary still caps the run');
|
|
1469
|
+
assert.ok(argv0.startsWith('/'), `the stub must be invoked by ABSOLUTE path, got: ${JSON.stringify(argv0)}`);
|
|
1470
|
+
});
|
|
1471
|
+
|
|
1472
|
+
it('an INVALID effective CODEX_HARD_TIMEOUT (env — the closed aw_settings_valid bypass) warns and falls back to the default', () => {
|
|
1473
|
+
for (const bad of ['abc', '0', '999999999']) {
|
|
1474
|
+
const sb = makeSandbox();
|
|
1475
|
+
const r = run(sb, { env: { CODEX_HARD_TIMEOUT: bad } });
|
|
1476
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1477
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1478
|
+
assert.match(r.stderr, new RegExp(`invalid value '${bad}' for CODEX_HARD_TIMEOUT`), 'the fallback is loud');
|
|
1479
|
+
assert.match(r.stderr, /^exec posture: .* timeout=3600s$/m, 'the banner prints the built-in default, never the bad value');
|
|
1480
|
+
}
|
|
1481
|
+
});
|
|
1482
|
+
});
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
// CODEX_SERVICE_TIER BEFORE tier validation (no multiline warning echo, no silent standard-tier
|
|
6
6
|
// run). Colocated separately — codex-review.test.mjs is red-proof-frozen; standalone harness.
|
|
7
7
|
|
|
8
|
-
import { describe, it } from 'node:test';
|
|
8
|
+
import { describe, it, after } from 'node:test';
|
|
9
9
|
import assert from 'node:assert/strict';
|
|
10
|
-
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, chmodSync, existsSync, readdirSync, symlinkSync } from 'node:fs';
|
|
10
|
+
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, chmodSync, existsSync, readdirSync, symlinkSync, cpSync } from 'node:fs';
|
|
11
11
|
import { tmpdir } from 'node:os';
|
|
12
12
|
import { join, dirname, resolve } from 'node:path';
|
|
13
13
|
import { fileURLToPath } from 'node:url';
|
|
@@ -46,12 +46,17 @@ const makePathWithout = (root, exclude = []) => {
|
|
|
46
46
|
return dir;
|
|
47
47
|
};
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
// Farm + sandbox base are READ-ONLY per invocation — built ONCE, shared (a per-run farm rebuild
|
|
50
|
+
// plus a per-test `git init`+commit dominate the wall otherwise).
|
|
51
|
+
const SHARED_ROOT = mkdtempSync(join(tmpdir(), 'codex-honesty-shared-'));
|
|
52
|
+
after(() => rmSync(SHARED_ROOT, { recursive: true, force: true }));
|
|
53
|
+
const FARM = makePathWithout(SHARED_ROOT, ['codex']);
|
|
54
|
+
|
|
55
|
+
const TEMPLATE_ROOT = (() => {
|
|
56
|
+
const root = join(SHARED_ROOT, 'template-root');
|
|
51
57
|
const bin = join(root, 'bin');
|
|
52
58
|
mkdirSync(bin, { recursive: true });
|
|
53
59
|
writeFileSync(join(bin, 'codex'), FAKE_CODEX, { mode: 0o755 });
|
|
54
|
-
chmodSync(join(bin, 'codex'), 0o755);
|
|
55
60
|
const repo = join(root, 'repo');
|
|
56
61
|
mkdirSync(repo);
|
|
57
62
|
const g = (...args) => spawnSync('git', args, { cwd: repo, encoding: 'utf8' });
|
|
@@ -62,7 +67,15 @@ const makeSandbox = () => {
|
|
|
62
67
|
g('add', '-A');
|
|
63
68
|
g('commit', '-qm', 'base');
|
|
64
69
|
writeFileSync(join(repo, 'pending.txt'), 'PENDING\n');
|
|
65
|
-
return
|
|
70
|
+
return root;
|
|
71
|
+
})();
|
|
72
|
+
|
|
73
|
+
const makeSandbox = () => {
|
|
74
|
+
const root = mkdtempSync(join(tmpdir(), 'codex-honesty-'));
|
|
75
|
+
cpSync(TEMPLATE_ROOT, root, { recursive: true });
|
|
76
|
+
const bin = join(root, 'bin');
|
|
77
|
+
chmodSync(join(bin, 'codex'), 0o755);
|
|
78
|
+
return { root, bin, repo: join(root, 'repo') };
|
|
66
79
|
};
|
|
67
80
|
|
|
68
81
|
const run = (sb, { args = ['code'], env = {} } = {}) => {
|
|
@@ -73,7 +86,7 @@ const run = (sb, { args = ['code'], env = {} } = {}) => {
|
|
|
73
86
|
timeout: 30000,
|
|
74
87
|
env: {
|
|
75
88
|
HOME: sb.repo,
|
|
76
|
-
PATH: `${sb.bin}:${
|
|
89
|
+
PATH: `${sb.bin}:${FARM}`,
|
|
77
90
|
TMPDIR: process.env.TMPDIR ?? '/tmp',
|
|
78
91
|
CODEX_HOME: join(sb.root, 'codex-home'),
|
|
79
92
|
CODEX_FAKE_SENTINEL: sentinel,
|
|
@@ -79,12 +79,21 @@ Settings file (KEY=VALUE, parsed never sourced; env wins over file, file wins ov
|
|
|
79
79
|
CODEX_HARD_TIMEOUT — hard wall-clock cap, integer seconds 1..86400 (built-in default 1800)
|
|
80
80
|
CODEX_REVIEW_MAX_TOTAL_BYTES — inline-payload cap, integer bytes 1..100000000 (default 1500000); above it the diff rides via a git-dir temp file
|
|
81
81
|
|
|
82
|
+
Notes:
|
|
83
|
+
the review posture banner appends a banner-only timeout=<duration|uncapped> field — exactly the
|
|
84
|
+
duration handed to timeout(1), uncapped when no timeout/gtimeout binary caps the run;
|
|
85
|
+
INFORMATIONAL only: it never enters the receipt posture or the D5 banner↔receipt parity
|
|
86
|
+
quote the posture banner verbatim when labeling this dispatch — the banner is the machine-stated
|
|
87
|
+
posture; a prose re-type drifts
|
|
88
|
+
|
|
82
89
|
Honesty + posture (D4/D5):
|
|
83
90
|
a run whose final message carries NO recognized 'Verdict: <ship|revise|rethink>' line — empty or
|
|
84
91
|
missing output included — exits 4 with NO receipt: a FAILED review to RE-RUN, never a fatal
|
|
85
92
|
session error. One stderr banner line states the ACTUAL run posture (review posture: model=…
|
|
86
|
-
effort=… tier=…) and the receipt records the same posture {model, effort, tier} (tier
|
|
87
|
-
the standard tier
|
|
93
|
+
effort=… tier=… timeout=…) and the receipt records the same posture {model, effort, tier} (tier
|
|
94
|
+
null on the standard tier; the timeout field is banner-only — never a receipt field). Quote the
|
|
95
|
+
posture banner verbatim when labeling this dispatch. A posture value carrying control bytes
|
|
96
|
+
refuses pre-spend in every mode.
|
|
88
97
|
|
|
89
98
|
Environment: CODEX_REVIEW_SCHEMA=1 (structured JSON findings), CODEX_HARD_TIMEOUT (seconds, default 1800), CODEX_PROBE=1 (throwaway probe only), AW_REVIEW_RECEIPTS (receipt file override).
|
|
90
99
|
Requires at run time: the codex CLI on PATH, a ChatGPT-subscription login, a git work tree with a root AGENTS.md (--help needs none of these).
|
|
@@ -201,12 +210,62 @@ aw_apply_settings() {
|
|
|
201
210
|
}
|
|
202
211
|
aw_apply_settings
|
|
203
212
|
|
|
213
|
+
# --- Effective-timeout resolver (D5 banner honesty; AD-061) --------------------
|
|
214
|
+
# ONE rule, both bridges: the posture banner prints EXACTLY the duration handed to timeout(1) —
|
|
215
|
+
# an integer-seconds value rendered with the `s` suffix, a duration string verbatim — and
|
|
216
|
+
# `timeout=uncapped` when no timeout/gtimeout binary can cap the run; never a fabricated number.
|
|
217
|
+
# The EFFECTIVE value (env included — closing the aw_settings_valid env bypass) is validated by
|
|
218
|
+
# the same per-key rule as the settings file, plus a 7-digit integer-part bound (overflow); an
|
|
219
|
+
# invalid value warns + falls back to the built-in default — a typo never silently masquerades
|
|
220
|
+
# as a cap. AGY_TIMEOUT shares AGY_HARD_TIMEOUT's duration rule (it has no settings-file arm).
|
|
221
|
+
aw_effective_timeout() {
|
|
222
|
+
local key="$1" default="$2" value="${!1:-}" rule="$1" intpart
|
|
223
|
+
[[ "$rule" == "AGY_TIMEOUT" ]] && rule="AGY_HARD_TIMEOUT"
|
|
224
|
+
[[ -n "$value" ]] || { printf '%s' "$default"; return 0; }
|
|
225
|
+
intpart="${value%%[!0-9]*}"
|
|
226
|
+
if ! aw_settings_valid "$rule" "$value" || (( ${#intpart} > 7 )); then
|
|
227
|
+
# %q escapes the raw value so a control byte in it can never forge an extra diagnostic line
|
|
228
|
+
# (the direct agy-run lane has no pre-spend screen — the warning itself must be injection-proof).
|
|
229
|
+
printf "warning: invalid value '%q' for %s — using the built-in default %s.\n" "$value" "$key" "$default" >&2
|
|
230
|
+
printf '%s' "$default"
|
|
231
|
+
return 0
|
|
232
|
+
fi
|
|
233
|
+
printf '%s' "$value"
|
|
234
|
+
}
|
|
235
|
+
aw_timeout_label() {
|
|
236
|
+
local bin="$1" value="$2"
|
|
237
|
+
[[ -n "$bin" ]] || { printf 'uncapped'; return 0; }
|
|
238
|
+
case "$value" in
|
|
239
|
+
*[!0-9]*) printf '%s' "$value" ;;
|
|
240
|
+
*) printf '%ss' "$value" ;;
|
|
241
|
+
esac
|
|
242
|
+
}
|
|
243
|
+
aw_resolve_timeout_bin() {
|
|
244
|
+
local bin dir base
|
|
245
|
+
bin="$(builtin type -P timeout 2>/dev/null || true)"
|
|
246
|
+
[[ -n "$bin" ]] || bin="$(builtin type -P gtimeout 2>/dev/null || true)"
|
|
247
|
+
[[ -n "$bin" ]] || { printf ''; return 0; }
|
|
248
|
+
case "$bin" in
|
|
249
|
+
/*) ;;
|
|
250
|
+
*)
|
|
251
|
+
case "$bin" in
|
|
252
|
+
*/*) dir="${bin%/*}"; base="${bin##*/}" ;;
|
|
253
|
+
*) dir="."; base="$bin" ;;
|
|
254
|
+
esac
|
|
255
|
+
dir="$(builtin cd -- "$dir" 2>/dev/null && builtin pwd -P)" || { printf ''; return 0; }
|
|
256
|
+
bin="$dir/$base"
|
|
257
|
+
;;
|
|
258
|
+
esac
|
|
259
|
+
[[ -f "$bin" && -x "$bin" ]] || { printf ''; return 0; }
|
|
260
|
+
printf '%s' "$bin"
|
|
261
|
+
}
|
|
262
|
+
|
|
204
263
|
DEFAULT_CODEX_MODEL="gpt-5.6-sol"
|
|
205
264
|
DEFAULT_CODEX_EFFORT="xhigh"
|
|
206
265
|
# Review-receipt identity (AD-038). AW_BRIDGE_VERSION mirrors this bridge's SKILL.md/capability.json
|
|
207
266
|
# version (drift-guarded by codex-review.test.mjs against capability.json).
|
|
208
267
|
AW_RECEIPT_BACKEND="codex"
|
|
209
|
-
AW_BRIDGE_VERSION="3.
|
|
268
|
+
AW_BRIDGE_VERSION="3.1.0"
|
|
210
269
|
CODEX_MODEL="${CODEX_MODEL:-$DEFAULT_CODEX_MODEL}"
|
|
211
270
|
CODEX_EFFORT="${CODEX_EFFORT:-$DEFAULT_CODEX_EFFORT}"
|
|
212
271
|
# Generous hard cap for a slow xhigh review (subscription latency varies).
|
|
@@ -225,12 +284,13 @@ CODEX_SERVICE_TIER="${CODEX_SERVICE_TIER:-}"
|
|
|
225
284
|
# D5 pre-spend control-byte screen — BEFORE tier validation (a malformed value is not a policy
|
|
226
285
|
# question; validating first would echo the hostile value into a multiline warning and then run
|
|
227
286
|
# on the standard tier, defeating the stated refusal). Screens the RAW model/effort/tier.
|
|
228
|
-
for _posture_pair in "CODEX_MODEL=$CODEX_MODEL" "CODEX_EFFORT=$CODEX_EFFORT" "CODEX_SERVICE_TIER=$CODEX_SERVICE_TIER"; do
|
|
229
|
-
if [[ "${_posture_pair#*=}" == *[$'\x01'-$'\x1f']* ]]; then
|
|
287
|
+
for _posture_pair in "CODEX_MODEL=$CODEX_MODEL" "CODEX_EFFORT=$CODEX_EFFORT" "CODEX_SERVICE_TIER=$CODEX_SERVICE_TIER" "CODEX_HARD_TIMEOUT=$CODEX_HARD_TIMEOUT"; do
|
|
288
|
+
if [[ "${_posture_pair#*=}" == *[$'\x01'-$'\x1f'$'\x7f']* ]]; then
|
|
230
289
|
echo "error: ${_posture_pair%%=*} contains control bytes — fix the setting (env or bridge-settings.conf) and re-run." >&2
|
|
231
290
|
exit 2
|
|
232
291
|
fi
|
|
233
292
|
done
|
|
293
|
+
CODEX_HARD_TIMEOUT="$(aw_effective_timeout CODEX_HARD_TIMEOUT 1800)"
|
|
234
294
|
if [[ -n "$CODEX_SERVICE_TIER" ]] && ! aw_settings_valid CODEX_SERVICE_TIER "$CODEX_SERVICE_TIER"; then
|
|
235
295
|
echo "warning: CODEX_SERVICE_TIER='$CODEX_SERVICE_TIER' is not a supported service tier ('priority') — running on the standard tier." >&2
|
|
236
296
|
CODEX_SERVICE_TIER=""
|
|
@@ -243,8 +303,15 @@ CHATGPT_LOGIN_GUARD="Logged in using ChatGPT"
|
|
|
243
303
|
|
|
244
304
|
# --- D5 banner (one line, the ACTUAL run posture) -------------------------------------------
|
|
245
305
|
# The control-byte screen already ran above (BEFORE tier validation); the banner states the
|
|
246
|
-
# EFFECTIVE posture (post tier validation) and the receipt records the same fields
|
|
247
|
-
|
|
306
|
+
# EFFECTIVE posture (post tier validation) and the receipt records the same fields — except
|
|
307
|
+
# `timeout`, a BANNER-ONLY informational field (AD-061): it prints exactly the duration handed
|
|
308
|
+
# to timeout(1), or `uncapped` without a capping binary, and never enters the receipt.
|
|
309
|
+
# aw_resolve_timeout_bin: builtin type -P (an exported function can shadow neither `timeout` nor
|
|
310
|
+
# `type` itself), normalized to an ABSOLUTE path fail-closed; the dispatch below reuses this SAME
|
|
311
|
+
# resolved path (banner and run never make independent conclusions).
|
|
312
|
+
timeout_bin="$(aw_resolve_timeout_bin)"
|
|
313
|
+
aw_timeout_banner="$(aw_timeout_label "$timeout_bin" "$CODEX_HARD_TIMEOUT")"
|
|
314
|
+
echo "review posture: model=$CODEX_MODEL effort=$CODEX_EFFORT tier=${CODEX_SERVICE_TIER:-standard} timeout=$aw_timeout_banner" >&2
|
|
248
315
|
|
|
249
316
|
# --- Quality-first guard: refuse a silent model/effort downgrade ---------------
|
|
250
317
|
# A relaxed run is RECORDED, not just warned about: the receipt carries probe:true so the kit's
|
|
@@ -663,12 +730,8 @@ fence_env=(env
|
|
|
663
730
|
CODEX_HOME="$real_codex_home")
|
|
664
731
|
|
|
665
732
|
# --- Hard wall-clock cap via timeout(1) (gtimeout on macOS) -------------------
|
|
666
|
-
timeout_bin
|
|
667
|
-
|
|
668
|
-
timeout_bin="timeout"
|
|
669
|
-
elif command -v gtimeout >/dev/null 2>&1; then
|
|
670
|
-
timeout_bin="gtimeout"
|
|
671
|
-
fi
|
|
733
|
+
# $timeout_bin was resolved ONCE (absolute path, type -P) at the banner emit above — the run
|
|
734
|
+
# reuses the exact binary the banner reported.
|
|
672
735
|
if [[ -z "$timeout_bin" ]]; then
|
|
673
736
|
echo "warning: no 'timeout'/'gtimeout' on PATH — running codex WITHOUT a hard wall-clock cap" >&2
|
|
674
737
|
echo " (install coreutils to enable CODEX_HARD_TIMEOUT=$CODEX_HARD_TIMEOUT)." >&2
|