@sabaiway/agent-workflow-kit 3.1.0 → 3.3.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 +71 -0
- package/SKILL.md +3 -3
- package/bridges/antigravity-cli-bridge/SKILL.md +6 -4
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +84 -7
- package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +113 -0
- package/bridges/antigravity-cli-bridge/bin/agy.sh +55 -6
- package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +18 -0
- 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 +211 -1
- package/bridges/codex-cli-bridge/bin/codex-review.sh +76 -13
- package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +47 -0
- 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/upgrade.md +9 -4
- package/references/modes/velocity.md +8 -2
- 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/bridge-settings-read.mjs +25 -5
- package/tools/delegation.mjs +2 -2
- package/tools/detect-backends.mjs +10 -0
- package/tools/lens-region.mjs +113 -36
- package/tools/recipes.mjs +5 -2
- package/tools/release-scan.mjs +147 -13
- package/tools/run-gates.mjs +43 -2
- package/tools/velocity-profile.mjs +229 -20
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
# codex-exec <file|-> -- <extra codex flags...> # passthrough codex flags
|
|
37
37
|
# codex-exec --resume-last <file|-> # continue the last session (iterate, no re-send)
|
|
38
38
|
# codex-exec --resume <session-id> <file|-> # continue a specific session
|
|
39
|
-
# CODEX_HARD_TIMEOUT=
|
|
39
|
+
# CODEX_HARD_TIMEOUT=7200 codex-exec <file> # raise the hard wall-clock cap (integer seconds)
|
|
40
40
|
# CODEX_PROBE=1 CODEX_MODEL=<slug> codex-exec <file> # throwaway probe (relaxes the guard)
|
|
41
41
|
set -euo pipefail
|
|
42
42
|
|
|
@@ -72,6 +72,20 @@ Notes:
|
|
|
72
72
|
nested inside a harness sandbox (the FS turns read-only) — route it OUTSIDE the harness sandbox
|
|
73
73
|
(excludedCommands / a per-run consented bypass) on the OBSERVED bwrap/EPERM failure, never a
|
|
74
74
|
preemptive blanket
|
|
75
|
+
exec posture banner: ONE stderr line before dispatch states the ACTUAL run posture —
|
|
76
|
+
exec posture: model=… effort=… tier=… sandbox=workspace-write session=fresh|resume:<id> timeout=… —
|
|
77
|
+
from RESOLVED post-validation values; the resume id is validated pre-spend, and control bytes in
|
|
78
|
+
any banner field refuse pre-spend
|
|
79
|
+
threat model: the sidecar byte and grammar screens detect corrupted input under a trusted parent
|
|
80
|
+
environment. A hostile parent environment — including exported shell functions or PATH
|
|
81
|
+
substitution of core/backend commands — is outside the threat model and can substitute the
|
|
82
|
+
backend itself. Targeted shadow-proof resolution protects banner/dispatch honesty from accidental
|
|
83
|
+
shadowing; it is not an environment security boundary
|
|
84
|
+
the exec posture banner appends a banner-only timeout=<duration|uncapped> field — exactly the
|
|
85
|
+
duration handed to timeout(1), uncapped when no timeout/gtimeout binary caps the run;
|
|
86
|
+
INFORMATIONAL only: it is never persisted in a receipt or session sidecar
|
|
87
|
+
quote the posture banner verbatim when labeling this dispatch — the banner is the machine-stated
|
|
88
|
+
posture; a prose re-type drifts
|
|
75
89
|
|
|
76
90
|
Settings file (KEY=VALUE, parsed never sourced; env wins over file, file wins over built-in default):
|
|
77
91
|
${XDG_CONFIG_HOME:-~/.config}/agent-workflow/bridge-settings.conf
|
|
@@ -193,6 +207,56 @@ aw_apply_settings() {
|
|
|
193
207
|
}
|
|
194
208
|
aw_apply_settings
|
|
195
209
|
|
|
210
|
+
# --- Effective-timeout resolver (D5 banner honesty; AD-061) --------------------
|
|
211
|
+
# ONE rule, both bridges: the posture banner prints EXACTLY the duration handed to timeout(1) —
|
|
212
|
+
# an integer-seconds value rendered with the `s` suffix, a duration string verbatim — and
|
|
213
|
+
# `timeout=uncapped` when no timeout/gtimeout binary can cap the run; never a fabricated number.
|
|
214
|
+
# The EFFECTIVE value (env included — closing the aw_settings_valid env bypass) is validated by
|
|
215
|
+
# the same per-key rule as the settings file, plus a 7-digit integer-part bound (overflow); an
|
|
216
|
+
# invalid value warns + falls back to the built-in default — a typo never silently masquerades
|
|
217
|
+
# as a cap. AGY_TIMEOUT shares AGY_HARD_TIMEOUT's duration rule (it has no settings-file arm).
|
|
218
|
+
aw_effective_timeout() {
|
|
219
|
+
local key="$1" default="$2" value="${!1:-}" rule="$1" intpart
|
|
220
|
+
[[ "$rule" == "AGY_TIMEOUT" ]] && rule="AGY_HARD_TIMEOUT"
|
|
221
|
+
[[ -n "$value" ]] || { printf '%s' "$default"; return 0; }
|
|
222
|
+
intpart="${value%%[!0-9]*}"
|
|
223
|
+
if ! aw_settings_valid "$rule" "$value" || (( ${#intpart} > 7 )); then
|
|
224
|
+
# %q escapes the raw value so a control byte in it can never forge an extra diagnostic line
|
|
225
|
+
# (the direct agy-run lane has no pre-spend screen — the warning itself must be injection-proof).
|
|
226
|
+
printf "warning: invalid value '%q' for %s — using the built-in default %s.\n" "$value" "$key" "$default" >&2
|
|
227
|
+
printf '%s' "$default"
|
|
228
|
+
return 0
|
|
229
|
+
fi
|
|
230
|
+
printf '%s' "$value"
|
|
231
|
+
}
|
|
232
|
+
aw_timeout_label() {
|
|
233
|
+
local bin="$1" value="$2"
|
|
234
|
+
[[ -n "$bin" ]] || { printf 'uncapped'; return 0; }
|
|
235
|
+
case "$value" in
|
|
236
|
+
*[!0-9]*) printf '%s' "$value" ;;
|
|
237
|
+
*) printf '%ss' "$value" ;;
|
|
238
|
+
esac
|
|
239
|
+
}
|
|
240
|
+
aw_resolve_timeout_bin() {
|
|
241
|
+
local bin dir base
|
|
242
|
+
bin="$(builtin type -P timeout 2>/dev/null || true)"
|
|
243
|
+
[[ -n "$bin" ]] || bin="$(builtin type -P gtimeout 2>/dev/null || true)"
|
|
244
|
+
[[ -n "$bin" ]] || { printf ''; return 0; }
|
|
245
|
+
case "$bin" in
|
|
246
|
+
/*) ;;
|
|
247
|
+
*)
|
|
248
|
+
case "$bin" in
|
|
249
|
+
*/*) dir="${bin%/*}"; base="${bin##*/}" ;;
|
|
250
|
+
*) dir="."; base="$bin" ;;
|
|
251
|
+
esac
|
|
252
|
+
dir="$(builtin cd -- "$dir" 2>/dev/null && builtin pwd -P)" || { printf ''; return 0; }
|
|
253
|
+
bin="$dir/$base"
|
|
254
|
+
;;
|
|
255
|
+
esac
|
|
256
|
+
[[ -f "$bin" && -x "$bin" ]] || { printf ''; return 0; }
|
|
257
|
+
printf '%s' "$bin"
|
|
258
|
+
}
|
|
259
|
+
|
|
196
260
|
DEFAULT_CODEX_MODEL="gpt-5.6-sol" # frontier coding model (verified locally) — pinned
|
|
197
261
|
DEFAULT_CODEX_EFFORT="xhigh" # maximum reasoning effort — pinned
|
|
198
262
|
CODEX_MODEL="${CODEX_MODEL:-$DEFAULT_CODEX_MODEL}"
|
|
@@ -209,6 +273,15 @@ CODEX_HARD_TIMEOUT="${CODEX_HARD_TIMEOUT:-3600}"
|
|
|
209
273
|
# the wrapper validates the effective value: an unsupported one warns and runs on the standard
|
|
210
274
|
# tier — a typo can never silently masquerade as Fast.
|
|
211
275
|
CODEX_SERVICE_TIER="${CODEX_SERVICE_TIER:-}"
|
|
276
|
+
# D5 pre-spend control-byte screen — BEFORE tier validation (the codex-review.sh order: a
|
|
277
|
+
# malformed value is not a policy question). Screens EVERY exec-banner field.
|
|
278
|
+
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
|
|
279
|
+
if [[ "${_posture_pair#*=}" == *[$'\x01'-$'\x1f'$'\x7f']* ]]; then
|
|
280
|
+
echo "error: ${_posture_pair%%=*} contains control bytes — fix the setting (env or bridge-settings.conf) and re-run." >&2
|
|
281
|
+
exit 2
|
|
282
|
+
fi
|
|
283
|
+
done
|
|
284
|
+
CODEX_HARD_TIMEOUT="$(aw_effective_timeout CODEX_HARD_TIMEOUT 3600)"
|
|
212
285
|
if [[ -n "$CODEX_SERVICE_TIER" ]] && ! aw_settings_valid CODEX_SERVICE_TIER "$CODEX_SERVICE_TIER"; then
|
|
213
286
|
echo "warning: CODEX_SERVICE_TIER='$CODEX_SERVICE_TIER' is not a supported service tier ('priority') — running on the standard tier." >&2
|
|
214
287
|
CODEX_SERVICE_TIER=""
|
|
@@ -257,14 +330,27 @@ fi
|
|
|
257
330
|
# Resolve the real git to an ABSOLUTE executable, ignoring shell functions/aliases
|
|
258
331
|
# (`type -P` forces a PATH lookup). The shim embeds this path so codex cannot recurse
|
|
259
332
|
# into the shim or delegate to the wrong binary.
|
|
260
|
-
real_git="$(type -P git 2>/dev/null || true)"
|
|
333
|
+
real_git="$(builtin type -P git 2>/dev/null || true)"
|
|
261
334
|
if [[ -z "$real_git" ]]; then
|
|
262
335
|
echo "error: 'git' not found on PATH (needed for the work tree and the git-write boundary shim)." >&2
|
|
263
336
|
exit 127
|
|
264
337
|
fi
|
|
338
|
+
# Normalize a relative-PATH hit to an ABSOLUTE path with the same shadow-proof discipline as
|
|
339
|
+
# aw_resolve_timeout_bin: parameter-expansion split (no external dirname/basename an exported
|
|
340
|
+
# function could shadow), builtin cd/pwd -P, fail-closed to empty (the -x check below STOPs).
|
|
265
341
|
case "$real_git" in
|
|
266
342
|
/*) ;;
|
|
267
|
-
*)
|
|
343
|
+
*)
|
|
344
|
+
case "$real_git" in
|
|
345
|
+
*/*) _git_dir="${real_git%/*}"; _git_base="${real_git##*/}" ;;
|
|
346
|
+
*) _git_dir="."; _git_base="$real_git" ;;
|
|
347
|
+
esac
|
|
348
|
+
if _git_dir="$(builtin cd -- "$_git_dir" 2>/dev/null && builtin pwd -P)"; then
|
|
349
|
+
real_git="$_git_dir/$_git_base"
|
|
350
|
+
else
|
|
351
|
+
real_git=""
|
|
352
|
+
fi
|
|
353
|
+
;;
|
|
268
354
|
esac
|
|
269
355
|
if [[ ! -x "$real_git" ]]; then
|
|
270
356
|
echo "error: resolved git path '$real_git' is not an executable." >&2
|
|
@@ -363,12 +449,37 @@ if [[ -n "$resume_mode" ]]; then
|
|
|
363
449
|
echo " Run a normal 'codex-exec' once (it records the session id there) before resuming." >&2
|
|
364
450
|
exit 2
|
|
365
451
|
fi
|
|
366
|
-
|
|
367
|
-
|
|
452
|
+
# RAW-byte NUL screen BEFORE the shell variable: bash command substitution silently DROPS
|
|
453
|
+
# NUL bytes, so a hostile `sess-\0target` would otherwise be repaired into a valid id.
|
|
454
|
+
nul_count="$(head -n1 -- "$sidecar" | LC_ALL=C tr -cd '\000' | wc -c)"
|
|
455
|
+
if (( nul_count != 0 )); then
|
|
456
|
+
echo "error: the session sidecar '$sidecar' carries NUL bytes — refusing before any spend; delete it or pass --resume <session-id>." >&2
|
|
457
|
+
exit 2
|
|
458
|
+
fi
|
|
459
|
+
# Read the first line WITHOUT content mutation — only a CRLF terminator is stripped. A
|
|
460
|
+
# whitespace-ONLY line is an empty sidecar; inner whitespace beside real content hits the
|
|
461
|
+
# session-id grammar below and REFUSES — it is never silently stripped into a different
|
|
462
|
+
# (accidentally valid) id.
|
|
463
|
+
resume_id="$(head -n1 -- "$sidecar")"
|
|
464
|
+
resume_id="${resume_id%$'\r'}"
|
|
465
|
+
if [[ -z "${resume_id//[[:space:]]/}" ]]; then
|
|
368
466
|
echo "error: the session sidecar '$sidecar' is empty — no session id to resume." >&2
|
|
369
467
|
exit 2
|
|
370
468
|
fi
|
|
371
469
|
fi
|
|
470
|
+
# Resume-id grammar (AD-061, derived from live codex ids — UUID-like and sess-* forms alike):
|
|
471
|
+
# a safe charset + length bound, FIRST char alphanumeric (a leading dash would reach
|
|
472
|
+
# `codex exec resume` as an OPTION, bypassing the guarded passthrough). A hostile/malformed
|
|
473
|
+
# id — explicit OR sidecar-read — refuses BEFORE any spend and before the id can reach the
|
|
474
|
+
# codex argv; the raw value is never echoed (it could carry control bytes).
|
|
475
|
+
aw_session_id_re='^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$'
|
|
476
|
+
if [[ ! "$resume_id" =~ $aw_session_id_re ]]; then
|
|
477
|
+
echo "error: the resolved session id is not a valid codex session id (letters, digits, dot," >&2
|
|
478
|
+
echo " underscore, hyphen; must START with a letter or digit; 1-128 chars) — refusing" >&2
|
|
479
|
+
echo " before any spend. If it came from the sidecar, the file may be corrupted —" >&2
|
|
480
|
+
echo " delete it or pass --resume <session-id>." >&2
|
|
481
|
+
exit 2
|
|
482
|
+
fi
|
|
372
483
|
else
|
|
373
484
|
# Normal mode: split off passthrough codex flags after a literal `--`. Extra args
|
|
374
485
|
# WITHOUT the `--` separator are a mistake — fail loudly rather than drop them.
|
|
@@ -541,17 +652,24 @@ run_env=(env "PATH=$shim_dir:$PATH")
|
|
|
541
652
|
# after the initial TERM if codex ignores it (a live probe confirmed plain
|
|
542
653
|
# `timeout` reaps the whole codex child tree — no --foreground needed). If neither
|
|
543
654
|
# binary exists we warn loudly and run uncapped rather than fail silently.
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
timeout_bin="gtimeout"
|
|
549
|
-
fi
|
|
655
|
+
# aw_resolve_timeout_bin: builtin type -P (an exported function can shadow neither `timeout` nor
|
|
656
|
+
# `type` itself), normalized to an ABSOLUTE path fail-closed — the dispatch invokes the same
|
|
657
|
+
# absolute path the banner rendered from (banner and run never make independent conclusions).
|
|
658
|
+
timeout_bin="$(aw_resolve_timeout_bin)"
|
|
550
659
|
if [[ -z "$timeout_bin" ]]; then
|
|
551
660
|
echo "warning: no 'timeout'/'gtimeout' on PATH — running codex WITHOUT a hard wall-clock cap" >&2
|
|
552
661
|
echo " (install coreutils to enable CODEX_HARD_TIMEOUT=$CODEX_HARD_TIMEOUT)." >&2
|
|
553
662
|
fi
|
|
554
663
|
|
|
664
|
+
# --- D5 exec banner (one line, the ACTUAL run posture; AD-061) -----------------
|
|
665
|
+
# Emitted from RESOLVED post-validation values, AFTER the resume id is resolved and validated,
|
|
666
|
+
# BEFORE the dispatch. The timeout field is banner-only (never a receipt/sidecar field): it
|
|
667
|
+
# prints exactly the duration handed to timeout(1), or `uncapped` without a capping binary.
|
|
668
|
+
aw_timeout_banner="$(aw_timeout_label "$timeout_bin" "$CODEX_HARD_TIMEOUT")"
|
|
669
|
+
aw_session_label="fresh"
|
|
670
|
+
[[ -n "$resume_mode" ]] && aw_session_label="resume:$resume_id"
|
|
671
|
+
echo "exec posture: model=$CODEX_MODEL effort=$CODEX_EFFORT tier=${CODEX_SERVICE_TIER:-standard} sandbox=workspace-write session=$aw_session_label timeout=$aw_timeout_banner" >&2
|
|
672
|
+
|
|
555
673
|
# Normal mode: -o writes $out, the JSON stream + logs go to $trace. Resume mode: the
|
|
556
674
|
# final message is codex's stdout → $out, logs → $trace. Either way the final lands
|
|
557
675
|
# in $out and diagnostics in $trace, so the post-processing below is shared.
|
|
@@ -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=""',
|
|
@@ -1270,3 +1270,213 @@ describe('codex-exec.sh — settings surface ⟷ manifest (D6, manifest-pinned)'
|
|
|
1270
1270
|
}
|
|
1271
1271
|
});
|
|
1272
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
|
+
});
|
|
@@ -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
|
|
@@ -732,6 +732,12 @@ describe('codex-review.sh — --help contract (manifest-pinned)', () => {
|
|
|
732
732
|
assert.equal(norm(helpSection(help, 'Receipt:').join(' ')), norm(REVIEW_CONTRACT.receipt));
|
|
733
733
|
assert.match(REVIEW_CONTRACT.receipt, /sha256 over the canonical uncommitted-state payload/, 'the fingerprint definition lives in the manifest contract');
|
|
734
734
|
});
|
|
735
|
+
|
|
736
|
+
it('Notes renders the manifest review contract.notes verbatim (AD-061 — a typed contract key that MUST surface)', () => {
|
|
737
|
+
const help = runHelp('--help').stdout;
|
|
738
|
+
assert.ok((REVIEW_CONTRACT.notes ?? []).length >= 2, 'the review contract declares the banner-only-timeout + quote-verbatim notes');
|
|
739
|
+
assert.equal(norm(helpSection(help, 'Notes:').join(' ')), norm(REVIEW_CONTRACT.notes.join(' ')));
|
|
740
|
+
});
|
|
735
741
|
});
|
|
736
742
|
|
|
737
743
|
describe('codex-review.sh — source-level reverse guard (parser arms ⟷ manifest)', () => {
|
|
@@ -1325,4 +1331,45 @@ describe('codex-review.sh — dispatch-posture labeling (D5)', () => {
|
|
|
1325
1331
|
assert.equal(receipts.length, 0);
|
|
1326
1332
|
assert.match(r.stderr, /control/i, 'named as the control-byte class, not a policy refusal');
|
|
1327
1333
|
});
|
|
1334
|
+
|
|
1335
|
+
it('the banner appends the RESOLVED hard timeout — banner-only, never in the receipt (AD-061)', () => {
|
|
1336
|
+
const sb = makeSandbox();
|
|
1337
|
+
const r = run(sb, {});
|
|
1338
|
+
const receipts = readReceipts(sb.repo);
|
|
1339
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1340
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1341
|
+
assert.match(r.stderr, /^review posture: model=gpt-5\.6-sol effort=xhigh tier=standard timeout=1800s$/m);
|
|
1342
|
+
assert.deepEqual(Object.keys(receipts[0].posture), ['model', 'effort', 'tier'], 'timeout never enters the receipt posture');
|
|
1343
|
+
});
|
|
1344
|
+
|
|
1345
|
+
it('an INVALID effective CODEX_HARD_TIMEOUT (env — the closed aw_settings_valid bypass) warns and the banner prints the default', () => {
|
|
1346
|
+
const sb = makeSandbox();
|
|
1347
|
+
const r = run(sb, { env: { CODEX_HARD_TIMEOUT: 'nonsense' } });
|
|
1348
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1349
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1350
|
+
assert.match(r.stderr, /invalid value 'nonsense' for CODEX_HARD_TIMEOUT/, 'the fallback is loud');
|
|
1351
|
+
assert.match(r.stderr, /^review posture: .* timeout=1800s$/m, 'the banner prints the built-in default');
|
|
1352
|
+
});
|
|
1353
|
+
|
|
1354
|
+
it('a CODEX_HARD_TIMEOUT carrying CONTROL BYTES refuses pre-spend (the banner-field screen)', () => {
|
|
1355
|
+
const sb = makeSandbox();
|
|
1356
|
+
const r = run(sb, { env: { CODEX_HARD_TIMEOUT: `1800${String.fromCharCode(1)}` } });
|
|
1357
|
+
const receipts = readReceipts(sb.repo);
|
|
1358
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1359
|
+
assert.notEqual(r.status, 0);
|
|
1360
|
+
assert.equal(r.capStdin, '', 'codex is never invoked');
|
|
1361
|
+
assert.equal(receipts.length, 0);
|
|
1362
|
+
assert.match(r.stderr, /control/i);
|
|
1363
|
+
});
|
|
1364
|
+
|
|
1365
|
+
it('a DEL (0x7f) byte in a banner field refuses pre-spend like the C0 range', () => {
|
|
1366
|
+
const sb = makeSandbox();
|
|
1367
|
+
const r = run(sb, { env: { CODEX_MODEL: `gpt-5.6-sol${String.fromCharCode(127)}` } });
|
|
1368
|
+
const receipts = readReceipts(sb.repo);
|
|
1369
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1370
|
+
assert.notEqual(r.status, 0);
|
|
1371
|
+
assert.equal(r.capStdin, '', 'codex is never invoked');
|
|
1372
|
+
assert.equal(receipts.length, 0);
|
|
1373
|
+
assert.match(r.stderr, /control/i);
|
|
1374
|
+
});
|
|
1328
1375
|
});
|