@sabaiway/agent-workflow-kit 5.2.0 → 5.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 +40 -0
- package/SKILL.md +1 -1
- package/bridges/codex-cli-bridge/SKILL.md +10 -3
- package/bridges/codex-cli-bridge/bin/codex-exec.sh +153 -34
- package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +228 -4
- package/bridges/codex-cli-bridge/bin/codex-review.sh +1 -1
- package/bridges/codex-cli-bridge/capability.json +3 -2
- package/bridges/codex-cli-bridge/references/driving-codex.md +5 -3
- package/bridges/codex-cli-bridge/references/sandbox-and-flags.md +24 -10
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/scripts/archive-decisions.mjs +340 -15
- package/references/scripts/archive-decisions.test.mjs +522 -2
- package/tools/detect-backends.mjs +1 -0
- package/tools/dispatch-record.mjs +926 -0
- package/tools/flow-check.mjs +48 -12
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,46 @@ Semantically versioned ([semver](https://semver.org)), newest first. The `versio
|
|
|
4
4
|
is the current release. `upgrade` mode reads a project's `docs/ai/.workflow-version` and applies
|
|
5
5
|
every `migrations/<version>-<slug>.md` newer than it, in semver order.
|
|
6
6
|
|
|
7
|
+
## 5.3.0 — a check must speak where it is built to speak: the two silent checks (AD-088)
|
|
8
|
+
|
|
9
|
+
**Two shipped checks stayed SILENT at exactly the point they exist to speak** — one a GATE that
|
|
10
|
+
refuses and blocks a commit, one a WARNING that never changes an exit status. They were one
|
|
11
|
+
sentence, so they ship together.
|
|
12
|
+
|
|
13
|
+
- **The `flow-check` gate stops demanding the receipt its own run will write.** A red final on the
|
|
14
|
+
current base was cleared only through a LATER completed retry — but `run-gates` appends the final
|
|
15
|
+
receipt only after every gate has run, so the in-matrix `flow-check` could never see the receipt
|
|
16
|
+
its OWN run would write, and each `--final` on an unchanged base minted red N+1. No number of
|
|
17
|
+
rerun-causes converged; the only exit was a hook bypass. The `#65` rung is now consumer-aware, the
|
|
18
|
+
same lane split the flow→final comparison already applies one arm away: on the `gate` lane a
|
|
19
|
+
current-base red is
|
|
20
|
+
ALSO answered by a provable IN-PROGRESS retry — an authoritative `rerun-cause` naming its attempt
|
|
21
|
+
and binding the current fingerprint, a `final-start` at that fingerprint ordered strictly after
|
|
22
|
+
that red whose attempt has no completed final, and that fingerprint correlating to exactly ONE
|
|
23
|
+
base, the current one. Inside a real final run the conjunction holds by construction; a standalone
|
|
24
|
+
check on a quiet tree still refuses.
|
|
25
|
+
- **`commit-guard` is unchanged.** It keeps the strict completed-retry demand, so the commit
|
|
26
|
+
boundary still sees a real receipt. The rung's own `consumer` defaults to that strict lane, the
|
|
27
|
+
relaxation is opt-in by EXACT match, and a tree whose fingerprint is unresolvable or ambiguously
|
|
28
|
+
base-correlated never relaxes.
|
|
29
|
+
- **The interrupted-run residual is stated, not papered over.** An interrupted final run leaves the
|
|
30
|
+
same record shape with no live run behind it, so a standalone `flow-check --check` reads PASS in
|
|
31
|
+
that window. It authorizes nothing — the commit boundary refuses that shape twice over,
|
|
32
|
+
independently — and the rung's own source comment says so, pinned end to end by a fixture that
|
|
33
|
+
asserts all three conditions at once.
|
|
34
|
+
- **The bundled `codex-cli-bridge` mirror moves to 3.4.0** — its nested-sandbox scan now runs on
|
|
35
|
+
every completed run, not only a failed one, so a delegated run that SURVIVES the failure and exits
|
|
36
|
+
0 is no longer silent about it. On a successful run, it warns when the scanner recognises the
|
|
37
|
+
expected per-item shape — both tokens inside the `aggregated_output` of ONE `command_execution`
|
|
38
|
+
with a proven failure. The heuristic is biased toward under-firing on ambiguous or schema-drifted
|
|
39
|
+
input: field reordering stops the match, and only the first matching item on a line is judged.
|
|
40
|
+
The exit status deliberately stays 0 there — a heuristic scan must never gain the power to refuse
|
|
41
|
+
real work.
|
|
42
|
+
|
|
43
|
+
Evidence: an end-to-end fixture drives a caused red at the current tree through ONE real
|
|
44
|
+
`run-gates --final` to a GREEN newest-authoritative final and a passing `commit-guard --check` — the
|
|
45
|
+
test that would have failed before this release.
|
|
46
|
+
|
|
7
47
|
## 5.2.0 — the flow machinery ships whole: recorded subset budgets, flow-bound finals, the round arms, and the dogfooded pipeline (AD-086)
|
|
8
48
|
|
|
9
49
|
**Everything the flow series built after 5.1.0 lands as one wave** — the record vocabulary, the
|
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: agent-workflow-kit
|
|
|
3
3
|
description: Deploy or upgrade a portable AI-agent memory-and-workflow system in any project. Use when the user wants to bootstrap `docs/ai/` + an entry-point `AGENTS.md` (+ `CLAUDE.md` alias) + cap/archive/index enforcement in a new or existing repo, set up the Memory Map and session protocols, install the docs-rotation pre-commit hook, or run `/agent-workflow-kit` / `/agent-workflow-kit upgrade`. Triggers on phrases like "set up the memory system", "deploy the AI workflow here", "bootstrap docs/ai", "upgrade the workflow".
|
|
4
4
|
disable-model-invocation: true
|
|
5
5
|
metadata:
|
|
6
|
-
version: '5.
|
|
6
|
+
version: '5.3.0'
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# agent-workflow-kit
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: codex-cli-bridge
|
|
3
3
|
description: Delegate work to the OpenAI Codex CLI (`codex`) under a ChatGPT subscription — run plan/instruction EXECUTION in a sandboxed workspace, or get a read-only ADVISORY review of a plan or working-tree diff — as a second delegated-execution backend beside Antigravity. Use when the user wants to hand a bounded coding task or plan to `codex exec`, get a second-opinion review from codex, install or authenticate Codex CLI, understand its sandbox/network/approval policy, drive codex efficiently from the main agent (exec vs review, resume, the commit boundary), bridge project context (`AGENTS.md`) into codex, or troubleshoot codex flags, models, auth, or its no-TTY headless behaviour.
|
|
4
4
|
metadata:
|
|
5
|
-
version: '3.
|
|
5
|
+
version: '3.4.0'
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# codex-cli-bridge
|
|
@@ -110,8 +110,15 @@ receipt field. **Quote the posture banner verbatim** when labeling a dispatch.
|
|
|
110
110
|
|
|
111
111
|
`codex exec` is headless: there is **no TTY**, so `approval_policy=never` — anything needing
|
|
112
112
|
escalation is refused and reported, never interactively approved. The wrappers capture only codex's
|
|
113
|
-
**final message** (`-o
|
|
114
|
-
|
|
113
|
+
**final message** (`-o`), so output is clean; the JSON event stream + reasoning go to a run trace
|
|
114
|
+
that is **read before it is discarded**. Fresh and resumed runs share **ONE** capture posture
|
|
115
|
+
(`-o` + `--json` + `--color never`, everything redirected into the trace), so both modes have the
|
|
116
|
+
same evidence surface — and the wrapper scans that surface on **every completed run**: a run that
|
|
117
|
+
SURVIVES a nested-sandbox failure exits 0 with an ungrounded answer, so on `rc == 0` it prints the
|
|
118
|
+
answer first and then warns loudly on stderr when one `command_execution` item with a **proven**
|
|
119
|
+
failure carries both a sandbox-mechanism and a permission/read-only token in its `aggregated_output`.
|
|
120
|
+
The exit status stays 0 there (warning, not gate) — read the stderr line. A successful **non-resume**
|
|
121
|
+
`codex-exec` also records the session id to a sidecar
|
|
115
122
|
(`${CODEX_SESSION_FILE:-./.codex-last-session}`) so `--resume-last` can find it. Extra `codex` flags
|
|
116
123
|
go after a literal `--`; the wrapper rejects any that would defeat the policy or the pinned model (see
|
|
117
124
|
[§ Environment knobs](#environment-knobs) and the flag tiers in
|
|
@@ -86,6 +86,18 @@ Notes:
|
|
|
86
86
|
INFORMATIONAL only: it is never persisted in a receipt or session sidecar
|
|
87
87
|
quote the posture banner verbatim when labeling this dispatch — the banner is the machine-stated
|
|
88
88
|
posture; a prose re-type drifts
|
|
89
|
+
every-run nested-sandbox scan (DUAL policy, deliberately two different rules): the scan runs on
|
|
90
|
+
EVERY completed run, not only a failed one, because a run that SURVIVES the nested-sandbox failure
|
|
91
|
+
exits 0 with an ungrounded answer and nothing said so. Failed run (rc != 0): the existing loose
|
|
92
|
+
whole-trace combination rule prints the recovery hint. Successful run (rc == 0): a warning fires
|
|
93
|
+
ONLY on precise per-item evidence — both a sandbox-mechanism token AND a permission/read-only
|
|
94
|
+
failure token inside the aggregated_output of ONE command_execution item whose failure is PROVEN
|
|
95
|
+
(a nonzero exit_code, or the serialized status "failed"); a null exit_code is never failure by
|
|
96
|
+
itself, tokens split across two items never fire, and a successful command's output never fires.
|
|
97
|
+
The answer is printed FIRST on stdout, then the warning on stderr. HONEST RESIDUAL: the exit
|
|
98
|
+
status does NOT change on that lane (a distinct nonzero exit would give a heuristic scan DENY
|
|
99
|
+
polarity, refusing real work whenever the scan over-warns), so an orchestrator keying on exit
|
|
100
|
+
status alone can still bank an ungrounded answer — the stderr warning is the signal
|
|
89
101
|
|
|
90
102
|
Settings file (KEY=VALUE, parsed never sourced; env wins over file, file wins over built-in default):
|
|
91
103
|
${XDG_CONFIG_HOME:-~/.config}/agent-workflow/bridge-settings.conf
|
|
@@ -412,9 +424,9 @@ fi
|
|
|
412
424
|
|
|
413
425
|
# --- Resume detection (must be the FIRST argument) ---------------------------
|
|
414
426
|
# A dedicated entrypoint for iterating on a session without re-sending context.
|
|
415
|
-
# `codex exec resume` RESETS posture and rejects the -s/--add-dir/-C posture flags
|
|
416
|
-
#
|
|
417
|
-
#
|
|
427
|
+
# `codex exec resume` RESETS posture and rejects the -s/--add-dir/-C posture flags,
|
|
428
|
+
# so we restate the FULL policy via -c. It DOES accept -o/--json/--color, and it now
|
|
429
|
+
# carries the same capture posture as a fresh run (one evidence surface, both modes).
|
|
418
430
|
resume_mode=""
|
|
419
431
|
resume_id=""
|
|
420
432
|
case "${1:-}" in
|
|
@@ -608,8 +620,10 @@ chmod 755 "$shim_dir/git"
|
|
|
608
620
|
# --- Build the codex invocation + the prompt ---------------------------------
|
|
609
621
|
if [[ -n "$resume_mode" ]]; then
|
|
610
622
|
# Resume RESETS posture and rejects the -s/--add-dir/-C posture flags, so restate
|
|
611
|
-
# the entire policy via -c.
|
|
612
|
-
#
|
|
623
|
+
# the entire policy via -c. It DOES accept -o/--json/--color, and it now carries the
|
|
624
|
+
# SAME capture posture as a fresh run: the mode asymmetry was a convenience, and it
|
|
625
|
+
# left resume — the lane the survived-nested-sandbox incident fired on — with no
|
|
626
|
+
# evidence surface at all (its event stream went nowhere).
|
|
613
627
|
codex_cmd=(codex exec resume "$resume_id"
|
|
614
628
|
--ignore-user-config
|
|
615
629
|
-m "$CODEX_MODEL"
|
|
@@ -620,6 +634,9 @@ if [[ -n "$resume_mode" ]]; then
|
|
|
620
634
|
-c hide_agent_reasoning=true
|
|
621
635
|
-c model_reasoning_summary=none
|
|
622
636
|
"${tier_flags[@]+"${tier_flags[@]}"}"
|
|
637
|
+
--color never
|
|
638
|
+
-o "$out"
|
|
639
|
+
--json
|
|
623
640
|
-)
|
|
624
641
|
full_prompt="$RESUME_REMINDER"$'\n\n'"$task"
|
|
625
642
|
else
|
|
@@ -672,25 +689,136 @@ aw_session_label="fresh"
|
|
|
672
689
|
[[ -n "$resume_mode" ]] && aw_session_label="resume:$resume_id"
|
|
673
690
|
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
|
|
674
691
|
|
|
675
|
-
#
|
|
676
|
-
#
|
|
677
|
-
#
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
692
|
+
# --- Nested-sandbox evidence scan: ONE entry point, TWO policies ---------------
|
|
693
|
+
# The class: codex ships its OWN OS sandbox (bwrap); run nested inside a harness sandbox the FS is
|
|
694
|
+
# read-only and codex's sandbox setup fails. The scan runs on EVERY completed run, not only a failed
|
|
695
|
+
# one — when the backend SURVIVES the failure (degrades to "I cannot check" and exits 0) a paid run
|
|
696
|
+
# is spent on an ungrounded answer, and nothing said so.
|
|
697
|
+
#
|
|
698
|
+
# The two arms have DIFFERENT rules, deliberately:
|
|
699
|
+
# FAILED run (rc != 0) — the loose whole-trace COMBINATION rule, unchanged: a sandbox MECHANISM
|
|
700
|
+
# token AND a permission/read-only FAILURE token anywhere in the trace. The run already failed and
|
|
701
|
+
# the operator is already reading the tail, so an extra hint costs nothing. grep is line-oriented,
|
|
702
|
+
# so a plain alternation stays within a line — hence two `-q` passes rather than one pattern.
|
|
703
|
+
# SUCCESSFUL run (rc == 0) — per-item evidence ONLY: both tokens inside the aggregated_output of
|
|
704
|
+
# ONE command_execution item whose FAILURE is proven. Here a false positive would libel a good
|
|
705
|
+
# answer, so nothing loose is allowed near it.
|
|
706
|
+
# The successful-run scan is LINE-ORIENTED and TOLERANT: after the unified 2>&1 the trace
|
|
707
|
+
# legitimately mixes plain log lines with JSONL, so every line is judged alone, a line that is not a
|
|
708
|
+
# well-formed command_execution item is simply not evidence (never a parse error, never a stop), and
|
|
709
|
+
# no line can mask a later one.
|
|
710
|
+
AW_NS_MECHANISM='bwrap|landlock|user namespace|pivot_root|unshare|seccomp'
|
|
711
|
+
AW_NS_FAILURE='read-only file system|erofs|operation not permitted|permission denied|eperm'
|
|
712
|
+
|
|
713
|
+
# BOTH token classes present in one piece of text. Fed by here-string rather than `printf | grep`:
|
|
714
|
+
# with `pipefail` on, a producer that takes EPIPE when `grep -q` exits early on a match would make
|
|
715
|
+
# the pipeline non-zero and silently DROP a real signature — an under-fire invisible by
|
|
716
|
+
# construction. Not reproduced on this host, folded as portability hardening: with no explicit
|
|
717
|
+
# pipeline there is no producer left to fail.
|
|
718
|
+
aw_ns_both_tokens() {
|
|
719
|
+
grep -qiE "$AW_NS_MECHANISM" <<<"$1" || return 1
|
|
720
|
+
grep -qiE "$AW_NS_FAILURE" <<<"$1" || return 1
|
|
721
|
+
return 0
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
# Valid JSON string CONTENT — no UNESCAPED quote. This is what proves a delimiter slice stayed
|
|
725
|
+
# inside ONE string instead of crossing an object boundary: without it, a decoy object carrying the
|
|
726
|
+
# anchor lets the walk leave its own string and land in another item's fields.
|
|
727
|
+
# The predicate is the parity rule — a quote is escaped iff an ODD number of backslashes precedes
|
|
728
|
+
# it — expressed as an ERE and evaluated by grep: `(^|[^\])` then an EVEN run `(\\)*` then the
|
|
729
|
+
# quote. It is measured, not assumed: the obvious bash spelling (delete the `\\` and `\"` pairs,
|
|
730
|
+
# then look for a survivor) is the SAME predicate but bash's `${var//…}` is quadratic, and it hung
|
|
731
|
+
# the wrapper outright on a 200KB aggregated_output — a real tool call's output reaches that size.
|
|
732
|
+
aw_ns_is_string_content() {
|
|
733
|
+
if grep -qE '(^|[^\\])(\\\\)*"' <<<"$1"; then return 1; fi
|
|
734
|
+
return 0
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
# One trace line → 0 only when THAT line is a command_execution item with a PROVEN failure whose
|
|
738
|
+
# aggregated_output carries both token classes. The wrapper stays dependency-free, so this is not a
|
|
739
|
+
# JSON parse — it is ONE anchored walk over the CLI's observed serialization in which every SKIPPED
|
|
740
|
+
# gap is PROVEN to be a single JSON string's content (aw_ns_is_string_content). Both halves are
|
|
741
|
+
# load-bearing: testing the fields as independent substrings lets a decoy object supply the anchor
|
|
742
|
+
# while the failure fields belong to another item, and skipping a gap without validating it lets the
|
|
743
|
+
# walk leave its own string and land in that other item anyway.
|
|
744
|
+
# The observed shape (codex-cli 0.147.0, live-probed):
|
|
745
|
+
# {"id":…,"type":"command_execution","command":"…","aggregated_output":"…","exit_code":2,"status":"failed"}
|
|
746
|
+
# Failure proofs: A = a nonzero exit_code, B = the status "failed" immediately after it. A null
|
|
747
|
+
# exit_code is never failure by itself (an in-flight item carries "exit_code":null,"status":"in_progress").
|
|
748
|
+
# Two stated consequences, both deliberate:
|
|
749
|
+
# - a future CLI that REORDERS these fields makes the scan stop firing rather than misfire.
|
|
750
|
+
# Under-firing is the right direction here: over-firing would libel a correct answer.
|
|
751
|
+
# - only the FIRST matching item on a line is judged; a second item's evidence is missed.
|
|
752
|
+
# A hand-crafted trace line is outside the threat model (the same boundary the wrapper declares for
|
|
753
|
+
# a hostile parent environment): the trace's only non-CLI content is plain stderr, which does not
|
|
754
|
+
# start with `{`, and a tool call's own output is JSON-escaped into a string and cannot inject
|
|
755
|
+
# structure. Anything not matching the walk is "not evidence" — never an error, never a stop.
|
|
756
|
+
# Every slice is taken by a SHORT-pattern `#*` cut plus length arithmetic. That is not a style
|
|
757
|
+
# choice: `${var%%<long>*}` and a prefix removal whose PATTERN is a huge variable are both
|
|
758
|
+
# quadratic in bash, and either one hangs the wrapper outright on a 200KB aggregated_output
|
|
759
|
+
# (measured, not assumed — a real tool call's output reaches that size).
|
|
760
|
+
aw_ns_item_evidence() {
|
|
761
|
+
local line="$1" d1='","aggregated_output":"' d2='","exit_code":' rest tail cmd agg code after
|
|
762
|
+
case "$line" in '{'*) ;; *) return 1 ;; esac
|
|
763
|
+
rest="${line#*'"type":"command_execution","command":"'}"
|
|
764
|
+
if [[ "$rest" == "$line" ]]; then return 1; fi
|
|
765
|
+
tail="${rest#*"$d1"}"
|
|
766
|
+
if [[ "$tail" == "$rest" ]]; then return 1; fi
|
|
767
|
+
cmd="${rest:0:$(( ${#rest} - ${#tail} - ${#d1} ))}"
|
|
768
|
+
aw_ns_is_string_content "$cmd" || return 1
|
|
769
|
+
after="${tail#*"$d2"}"
|
|
770
|
+
if [[ "$after" == "$tail" ]]; then return 1; fi
|
|
771
|
+
agg="${tail:0:$(( ${#tail} - ${#after} - ${#d2} ))}"
|
|
772
|
+
aw_ns_is_string_content "$agg" || return 1
|
|
773
|
+
code="${after%%,*}"
|
|
774
|
+
if [[ "$code" =~ ^-?[0-9]+$ && "$code" != "0" ]]; then
|
|
775
|
+
:
|
|
776
|
+
elif [[ "${after#"$code",}" == '"status":"failed"'* ]]; then
|
|
777
|
+
:
|
|
682
778
|
else
|
|
683
|
-
|
|
779
|
+
return 1
|
|
684
780
|
fi
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
781
|
+
aw_ns_both_tokens "$agg"
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
aw_scan_nested_sandbox() { # $1 = rc, $2 = trace path
|
|
785
|
+
[[ -r "$2" ]] || return 0
|
|
786
|
+
if [[ "$1" -ne 0 ]]; then
|
|
787
|
+
if grep -qiE "$AW_NS_MECHANISM" "$2" 2>/dev/null && grep -qiE "$AW_NS_FAILURE" "$2" 2>/dev/null; then
|
|
788
|
+
echo "hint: this looks like a NESTED-SANDBOX failure — codex-exec ships its own OS sandbox (bwrap)," >&2
|
|
789
|
+
echo " which cannot run nested inside a harness sandbox (the FS is read-only). Route codex-exec" >&2
|
|
790
|
+
echo " OUTSIDE the harness sandbox: add it to the harness sandbox excludedCommands, or dispatch" >&2
|
|
791
|
+
echo " this one run via a per-run consented bypass. Do NOT blanket-disable the sandbox." >&2
|
|
792
|
+
fi
|
|
793
|
+
return 0
|
|
691
794
|
fi
|
|
692
|
-
|
|
795
|
+
local line
|
|
796
|
+
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
797
|
+
if aw_ns_item_evidence "$line"; then
|
|
798
|
+
echo "warning: NESTED-SANDBOX — this run COMPLETED, but a tool call inside it FAILED with a sandbox-setup" >&2
|
|
799
|
+
echo " signature. codex-exec ships its own OS sandbox (bwrap), which cannot run nested inside a" >&2
|
|
800
|
+
echo " harness sandbox (the FS turns read-only), so the backend most likely could not read what" >&2
|
|
801
|
+
echo " it was asked to check: the answer above may be UNGROUNDED — treat it as such rather than" >&2
|
|
802
|
+
echo " banking it. Re-dispatch OUTSIDE the harness sandbox: add codex-exec to the harness" >&2
|
|
803
|
+
echo " sandbox excludedCommands, or use a per-run consented bypass. Do NOT blanket-disable the" >&2
|
|
804
|
+
echo " sandbox. The exit status stays 0 on purpose — this is a warning, never a gate." >&2
|
|
805
|
+
return 0
|
|
806
|
+
fi
|
|
807
|
+
done <"$2"
|
|
808
|
+
return 0
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
# ONE capture posture for BOTH modes: -o writes the final message to $out, the JSON
|
|
812
|
+
# event stream and every log line go to $trace (stderr merged in). The final always
|
|
813
|
+
# lands in $out and the diagnostics always in $trace, so the post-processing below —
|
|
814
|
+
# including the evidence scan — is genuinely shared instead of mode-dependent.
|
|
815
|
+
set +e
|
|
816
|
+
if [[ -n "$timeout_bin" ]]; then
|
|
817
|
+
printf '%s' "$full_prompt" | "${run_env[@]}" "$timeout_bin" --kill-after=15s "$CODEX_HARD_TIMEOUT" "${codex_cmd[@]}" >"$trace" 2>&1
|
|
818
|
+
else
|
|
819
|
+
printf '%s' "$full_prompt" | "${run_env[@]}" "${codex_cmd[@]}" >"$trace" 2>&1
|
|
693
820
|
fi
|
|
821
|
+
rc=$?
|
|
694
822
|
set -e
|
|
695
823
|
|
|
696
824
|
if [[ $rc -eq 124 || $rc -eq 137 ]]; then
|
|
@@ -701,20 +829,7 @@ fi
|
|
|
701
829
|
if [[ $rc -ne 0 ]]; then
|
|
702
830
|
echo "error: codex exec failed (exit $rc). Last lines of the run trace:" >&2
|
|
703
831
|
tail -n 40 "$trace" >&2
|
|
704
|
-
|
|
705
|
-
# sandbox the FS is read-only and codex's own sandbox setup fails. Fire the STATED recovery hint
|
|
706
|
-
# only on a COMBINATION — a sandbox MECHANISM token AND a permission/read-only FAILURE token in the
|
|
707
|
-
# trace — so a lone 'bwrap' banner, or a lone 'permission denied' from unrelated code, is NOT enough
|
|
708
|
-
# (never a preemptive blanket). grep is line-oriented, so a plain alternation stays within a line —
|
|
709
|
-
# the old `[^\n]*` between the two halves was wrong twice over (it excluded the letter 'n', and grep
|
|
710
|
-
# never spans lines anyway), so the split into two `-q` passes both fixes it and states the intent.
|
|
711
|
-
if grep -qiE 'bwrap|landlock|user namespace|pivot_root|unshare|seccomp' "$trace" 2>/dev/null \
|
|
712
|
-
&& grep -qiE 'read-only file system|erofs|operation not permitted|permission denied|eperm' "$trace" 2>/dev/null; then
|
|
713
|
-
echo "hint: this looks like a NESTED-SANDBOX failure — codex-exec ships its own OS sandbox (bwrap)," >&2
|
|
714
|
-
echo " which cannot run nested inside a harness sandbox (the FS is read-only). Route codex-exec" >&2
|
|
715
|
-
echo " OUTSIDE the harness sandbox: add it to the harness sandbox excludedCommands, or dispatch" >&2
|
|
716
|
-
echo " this one run via a per-run consented bypass. Do NOT blanket-disable the sandbox." >&2
|
|
717
|
-
fi
|
|
832
|
+
aw_scan_nested_sandbox "$rc" "$trace"
|
|
718
833
|
exit $rc
|
|
719
834
|
fi
|
|
720
835
|
|
|
@@ -739,3 +854,7 @@ else
|
|
|
739
854
|
echo "warning: codex produced no final-message file — printing the run-trace tail instead." >&2
|
|
740
855
|
tail -n 40 "$trace"
|
|
741
856
|
fi
|
|
857
|
+
|
|
858
|
+
# The answer is printed FIRST, then the evidence speaks: a run that COMPLETED can still have been
|
|
859
|
+
# ungrounded, and saying so after the answer keeps stdout byte-identical for every caller.
|
|
860
|
+
aw_scan_nested_sandbox "$rc" "$trace"
|
|
@@ -46,6 +46,13 @@ const FAKE_CODEX = [
|
|
|
46
46
|
' cat <<EOF',
|
|
47
47
|
'{"type":"turn.started"}',
|
|
48
48
|
'{"type":"item.completed","item":{"id":"item_0","type":"agent_message","text":"FAKE_FINAL_MESSAGE"}}',
|
|
49
|
+
'EOF',
|
|
50
|
+
// The event seam: verbatim extra stream lines (JSONL items, plain noise, or both) between the
|
|
51
|
+
// opening events and turn.completed — a multi-line value emits multiple lines.
|
|
52
|
+
' if [[ -n "${CODEX_FAKE_EVENT:-}" ]]; then echo "$CODEX_FAKE_EVENT"; fi',
|
|
53
|
+
// A file-borne twin: a payload too large for the environment (E2BIG) still has to be emittable.
|
|
54
|
+
' if [[ -n "${CODEX_FAKE_EVENT_FILE:-}" ]]; then cat "$CODEX_FAKE_EVENT_FILE"; fi',
|
|
55
|
+
' cat <<EOF',
|
|
49
56
|
'{"type":"turn.completed","usage":{}}',
|
|
50
57
|
'EOF',
|
|
51
58
|
'else',
|
|
@@ -365,6 +372,203 @@ describe('codex-exec.sh — clean output + session capture (1.2)', () => {
|
|
|
365
372
|
assert.doesNotMatch(r.stderr, /NESTED-SANDBOX/, 'a permission failure from unrelated code is not nested-sandbox proof');
|
|
366
373
|
});
|
|
367
374
|
|
|
375
|
+
// ── the rc == 0 arm: the SURVIVED nested-sandbox failure ──
|
|
376
|
+
// The class the failed-run arm cannot see: the backend hits the nested sandbox, degrades to "I
|
|
377
|
+
// cannot check", and exits 0 — a paid run spent on an ungrounded answer with nothing saying so.
|
|
378
|
+
// Both serialized shapes below were observed on the INSTALLED codex-cli 0.147.0: a finished item
|
|
379
|
+
// carries {"exit_code":2,"status":"failed"}, an in-flight one {"exit_code":null,"status":"in_progress"}.
|
|
380
|
+
const cmdItem = ({ command = '/bin/bash -lc probe', output = '', exitCode = null, status = 'completed', id = 'item_1' }) =>
|
|
381
|
+
JSON.stringify({ type: 'item.completed', item: { id, type: 'command_execution', command, aggregated_output: output, exit_code: exitCode, status } });
|
|
382
|
+
const MECHANISM = 'bwrap: setting up sandbox';
|
|
383
|
+
const FAILURE = 'mkdir /newroot: Read-only file system';
|
|
384
|
+
const SIGNATURE = `${MECHANISM}: ${FAILURE}\n`;
|
|
385
|
+
|
|
386
|
+
it('an rc == 0 run whose trace carries a command_execution with a NONZERO exit_code and the signature warns loudly and still prints the answer', () => {
|
|
387
|
+
const sb = makeSandbox();
|
|
388
|
+
const r = run(sb, { env: { CODEX_FAKE_EVENT: cmdItem({ output: SIGNATURE, exitCode: 1, status: 'completed' }) } });
|
|
389
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
390
|
+
assert.equal(r.status, 0, 'the warning lane never changes the exit status');
|
|
391
|
+
assert.match(r.stdout, /FAKE_FINAL_MESSAGE/, 'the answer is printed FIRST, on stdout, unchanged');
|
|
392
|
+
assert.match(r.stderr, /NESTED-SANDBOX/, 'names the class');
|
|
393
|
+
assert.match(r.stderr, /UNGROUNDED/, 'names the consequence for the answer above');
|
|
394
|
+
assert.match(r.stderr, /excludedCommands|per-run consented bypass/, 'names the reroute');
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('an rc == 0 run whose trace carries a command_execution with a null exit_code and an explicitly FAILED status warns', () => {
|
|
398
|
+
const sb = makeSandbox();
|
|
399
|
+
const r = run(sb, { env: { CODEX_FAKE_EVENT: cmdItem({ output: SIGNATURE, exitCode: null, status: 'failed' }) } });
|
|
400
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
401
|
+
assert.equal(r.status, 0);
|
|
402
|
+
assert.match(r.stderr, /NESTED-SANDBOX/, 'the serialized failed status is the second failure proof');
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it('plain non-JSON stderr lines before and after a matching failed command_execution do not suppress the warning', () => {
|
|
406
|
+
const sb = makeSandbox();
|
|
407
|
+
const r = run(sb, {
|
|
408
|
+
env: {
|
|
409
|
+
CODEX_FAKE_STDERR: 'ERROR codex_core::session: failed to load skill /x/SKILL.md: missing field description',
|
|
410
|
+
CODEX_FAKE_EVENT: `not json at all\n${cmdItem({ output: SIGNATURE, exitCode: 2, status: 'failed' })}\nstill not json`,
|
|
411
|
+
},
|
|
412
|
+
});
|
|
413
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
414
|
+
assert.equal(r.status, 0);
|
|
415
|
+
assert.match(r.stderr, /NESTED-SANDBOX/, 'the merged stream is judged line by line — noise is not evidence and never a stop');
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it('the resume lane warns on an rc == 0 nested-sandbox signature — the lane the incident fired on', () => {
|
|
419
|
+
const sb = makeSandbox();
|
|
420
|
+
const r = run(sb, {
|
|
421
|
+
args: ['--resume', 'sess-nested', '-'], input: 'continue',
|
|
422
|
+
env: { CODEX_FAKE_EVENT: cmdItem({ output: SIGNATURE, exitCode: 1, status: 'failed' }) },
|
|
423
|
+
});
|
|
424
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
425
|
+
assert.equal(r.status, 0);
|
|
426
|
+
assert.match(r.stdout, /FAKE_FINAL_MESSAGE/);
|
|
427
|
+
assert.match(r.stderr, /NESTED-SANDBOX/, 'the whole point of unifying the capture');
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('an rc == 0 run with a clean trace warns nothing', () => {
|
|
431
|
+
const sb = makeSandbox();
|
|
432
|
+
const r = run(sb);
|
|
433
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
434
|
+
assert.equal(r.status, 0, r.stderr);
|
|
435
|
+
assert.doesNotMatch(r.stderr, /NESTED-SANDBOX/, 'a clean run must stay silent');
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it('a lone mechanism token and a lone failure token each warn nothing on the rc == 0 lane', () => {
|
|
439
|
+
for (const output of [`${MECHANISM} version 0.11.0\n`, `curl: (7) ${FAILURE}\n`]) {
|
|
440
|
+
const sb = makeSandbox();
|
|
441
|
+
const r = run(sb, { env: { CODEX_FAKE_EVENT: cmdItem({ output, exitCode: 1, status: 'failed' }) } });
|
|
442
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
443
|
+
assert.doesNotMatch(r.stderr, /NESTED-SANDBOX/, `a lone token class is not proof: ${output}`);
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it('nested-sandbox text appearing ONLY inside an agent_message item never warns', () => {
|
|
448
|
+
const sb = makeSandbox();
|
|
449
|
+
const event = JSON.stringify({ type: 'item.completed', item: { id: 'item_9', type: 'agent_message', text: `I hit ${SIGNATURE}` } });
|
|
450
|
+
const r = run(sb, { env: { CODEX_FAKE_EVENT: event } });
|
|
451
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
452
|
+
assert.doesNotMatch(r.stderr, /NESTED-SANDBOX/, 'the model TALKING about a sandbox is not a failed tool call');
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it('a SUCCESSFUL command_execution whose output merely QUOTES both tokens never warns', () => {
|
|
456
|
+
const sb = makeSandbox();
|
|
457
|
+
// The concrete false positive: codex-exec.sh itself carries both token classes, so any
|
|
458
|
+
// successful grep over it would trip a loose whole-trace rule.
|
|
459
|
+
const r = run(sb, {
|
|
460
|
+
env: { CODEX_FAKE_EVENT: cmdItem({ command: '/bin/bash -lc grep -n bwrap codex-exec.sh', output: SIGNATURE, exitCode: 0, status: 'completed' }) },
|
|
461
|
+
});
|
|
462
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
463
|
+
assert.equal(r.status, 0, r.stderr);
|
|
464
|
+
assert.doesNotMatch(r.stderr, /NESTED-SANDBOX/, 'a command that SUCCEEDED proves nothing failed');
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it('a command_execution with a null exit_code and no proven failed status never warns', () => {
|
|
468
|
+
const sb = makeSandbox();
|
|
469
|
+
const r = run(sb, { env: { CODEX_FAKE_EVENT: cmdItem({ output: SIGNATURE, exitCode: null, status: 'in_progress' }) } });
|
|
470
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
471
|
+
assert.doesNotMatch(r.stderr, /NESTED-SANDBOX/, 'a null exit_code is never failure by itself');
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it('tokens split across two different items never warn', () => {
|
|
475
|
+
const sb = makeSandbox();
|
|
476
|
+
const split = [
|
|
477
|
+
cmdItem({ id: 'item_1', output: `${MECHANISM} version 0.11.0\n`, exitCode: 1, status: 'failed' }),
|
|
478
|
+
cmdItem({ id: 'item_2', output: `curl: (7) ${FAILURE}\n`, exitCode: 1, status: 'failed' }),
|
|
479
|
+
].join('\n');
|
|
480
|
+
const r = run(sb, { env: { CODEX_FAKE_EVENT: split } });
|
|
481
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
482
|
+
assert.doesNotMatch(r.stderr, /NESTED-SANDBOX/, 'the combination must sit in ONE item — two failures are not one nested sandbox');
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
// ── object membership, not substring co-occurrence ──
|
|
486
|
+
// Testing the four fields independently is not enough: position in the line is not membership in
|
|
487
|
+
// the item. The scan walks ONE contiguous chain of raw delimiters instead, and every gap in that
|
|
488
|
+
// chain is inside a JSON string, where a quote is escaped and cannot forge the next delimiter.
|
|
489
|
+
it('a decoy object carrying the type, with the failure fields on a DIFFERENT item, never warns', () => {
|
|
490
|
+
const sb = makeSandbox();
|
|
491
|
+
const decoy = '{"type":"item.completed","decoy":{"type":"command_execution"},"item":{"type":"agent_message","aggregated_output":"bwrap: operation not permitted","exit_code":0,"status":"failed"}}';
|
|
492
|
+
const r = run(sb, { env: { CODEX_FAKE_EVENT: decoy } });
|
|
493
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
494
|
+
assert.equal(r.status, 0, r.stderr);
|
|
495
|
+
assert.doesNotMatch(r.stderr, /NESTED-SANDBOX/, 'the type belongs to the decoy; the failure fields belong to an agent_message');
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it('a decoy carrying BOTH the type and a command, with the failure fields on a DIFFERENT item, never warns', () => {
|
|
499
|
+
const sb = makeSandbox();
|
|
500
|
+
// Anchoring on a longer literal is not enough: the skip between fields must itself be PROVEN to
|
|
501
|
+
// be one JSON string's content, or the walk leaves the decoy's command and lands in the
|
|
502
|
+
// agent_message's fields.
|
|
503
|
+
const decoy = '{"type":"item.completed","decoy":{"type":"command_execution","command":"x"},"item":{"type":"agent_message","aggregated_output":"bwrap: setting up sandbox: operation not permitted","exit_code":1,"status":"failed"}}';
|
|
504
|
+
const r = run(sb, { env: { CODEX_FAKE_EVENT: decoy } });
|
|
505
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
506
|
+
assert.equal(r.status, 0, r.stderr);
|
|
507
|
+
assert.doesNotMatch(r.stderr, /NESTED-SANDBOX/, 'an unvalidated gap lets the walk cross an object boundary');
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it('a genuinely failed item with a ~200KB aggregated_output still warns — and the scan does not hang', () => {
|
|
511
|
+
const sb = makeSandbox();
|
|
512
|
+
// Two edges at once: the signature sits FIRST, so any early-exit consumer must not lose it, and
|
|
513
|
+
// the field is far larger than a pipe buffer. It also pins the cost: the quadratic bash string
|
|
514
|
+
// spellings of this walk hang the wrapper outright at this size.
|
|
515
|
+
const big = `${SIGNATURE}${'x'.repeat(200000)}`;
|
|
516
|
+
// The payload rides a FILE: 200KB in the environment is E2BIG on a normal host.
|
|
517
|
+
const payload = join(sb.repo, 'big-event.jsonl');
|
|
518
|
+
writeFileSync(payload, `${cmdItem({ output: big, exitCode: 1, status: 'failed' })}\n`);
|
|
519
|
+
const r = run(sb, { env: { CODEX_FAKE_EVENT_FILE: payload } });
|
|
520
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
521
|
+
assert.equal(r.status, 0, r.stderr);
|
|
522
|
+
assert.match(r.stderr, /NESTED-SANDBOX/, 'a large output must not silently drop a real signature');
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it('a plain non-JSON log line carrying the same substrings never warns', () => {
|
|
526
|
+
const sb = makeSandbox();
|
|
527
|
+
const lookalike = `ERROR codex_core: replaying "type":"command_execution","command":"x","aggregated_output":"${SIGNATURE.trim()}","exit_code":1,"status":"failed"`;
|
|
528
|
+
const r = run(sb, { env: { CODEX_FAKE_EVENT: lookalike } });
|
|
529
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
530
|
+
assert.doesNotMatch(r.stderr, /NESTED-SANDBOX/, 'prose ABOUT an event is not an event — an event line starts with {');
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
it('a FOREIGN "status":"failed" elsewhere on the line never proves a SUCCESSFUL item failed', () => {
|
|
534
|
+
const sb = makeSandbox();
|
|
535
|
+
const event = `${cmdItem({ output: SIGNATURE, exitCode: 0, status: 'completed' })}{"type":"turn.failed","status":"failed"}`;
|
|
536
|
+
const r = run(sb, { env: { CODEX_FAKE_EVENT: event } });
|
|
537
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
538
|
+
assert.doesNotMatch(r.stderr, /NESTED-SANDBOX/, 'the failed status must sit immediately after THIS item exit_code');
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
it('an escaped delimiter inside aggregated_output never fools the slice', () => {
|
|
542
|
+
const sb = makeSandbox();
|
|
543
|
+
const r = run(sb, {
|
|
544
|
+
env: { CODEX_FAKE_EVENT: cmdItem({ output: `${SIGNATURE}","exit_code":1,"status":"failed"`, exitCode: 0, status: 'completed' }) },
|
|
545
|
+
});
|
|
546
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
547
|
+
assert.doesNotMatch(r.stderr, /NESTED-SANDBOX/, 'a quote inside a JSON string is escaped, so the raw delimiter cannot occur there');
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
it('only the FIRST command_execution item of a line is judged — a second item on the same line is missed (a STATED false negative)', () => {
|
|
551
|
+
const sb = makeSandbox();
|
|
552
|
+
const glued = `${cmdItem({ id: 'item_1', output: 'all good\n', exitCode: 0, status: 'completed' })}${cmdItem({ id: 'item_2', output: SIGNATURE, exitCode: 1, status: 'failed' })}`;
|
|
553
|
+
const r = run(sb, { env: { CODEX_FAKE_EVENT: glued } });
|
|
554
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
555
|
+
assert.doesNotMatch(r.stderr, /NESTED-SANDBOX/, 'under-firing is the deliberate direction on a warning lane; this pins it so it cannot change silently');
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
it('a trace of plain non-JSON lines alone carrying both tokens never warns on the rc == 0 arm — while the FAILED arm warns on exactly those bytes', () => {
|
|
559
|
+
const bytes = `${MECHANISM}: ${FAILURE}`;
|
|
560
|
+
const clean = makeSandbox();
|
|
561
|
+
const ok = run(clean, { env: { CODEX_FAKE_STDERR: bytes } });
|
|
562
|
+
rmSync(clean.root, { recursive: true, force: true });
|
|
563
|
+
assert.equal(ok.status, 0, ok.stderr);
|
|
564
|
+
assert.doesNotMatch(ok.stderr, /NESTED-SANDBOX/, 'on a COMPLETED run only per-item evidence speaks');
|
|
565
|
+
const failed = makeSandbox();
|
|
566
|
+
const bad = run(failed, { env: { CODEX_FAKE_STDERR: bytes, CODEX_FAKE_EXIT: '1' } });
|
|
567
|
+
rmSync(failed.root, { recursive: true, force: true });
|
|
568
|
+
assert.equal(bad.status, 1);
|
|
569
|
+
assert.match(bad.stderr, /NESTED-SANDBOX/, 'the failed-run arm keeps its loose whole-trace rule — that is what makes the dual policy visible');
|
|
570
|
+
});
|
|
571
|
+
|
|
368
572
|
it('warns (never silently) when the session sidecar cannot be written', () => {
|
|
369
573
|
const sb = makeSandbox();
|
|
370
574
|
const blocker = join(sb.repo, 'blocker');
|
|
@@ -447,10 +651,30 @@ describe('codex-exec.sh — resume entrypoint restates every invariant (3.1)', (
|
|
|
447
651
|
assert.equal(r.status, 0, r.stderr);
|
|
448
652
|
assert.match(r.argv, /(^|\n)sess-xyz(\n|$)/, 'the session id is passed positionally');
|
|
449
653
|
for (const inv of RESUME_INVARIANTS) assert.match(r.argv, inv, `resume argv must include ${inv}`);
|
|
450
|
-
assert.
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
654
|
+
assert.match(r.stdout, /FAKE_FINAL_MESSAGE/, 'resume prints the final message');
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
// The capture unification: resume used to be the odd mode out — no -o, no --json, its event
|
|
658
|
+
// stream nowhere — which is precisely why the lane the nested-sandbox incident fired on had no
|
|
659
|
+
// evidence surface. `codex exec resume` accepts both (live-probed, codex-cli 0.147.0).
|
|
660
|
+
it('resume composes the unified capture — the same -o/--json/--color posture as a fresh run', () => {
|
|
661
|
+
const sb = makeSandbox();
|
|
662
|
+
const r = run(sb, { args: ['--resume', 'sess-unified', '-'], input: 'continue please' });
|
|
663
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
664
|
+
assert.equal(r.status, 0, r.stderr);
|
|
665
|
+
assert.match(r.argv, /(^|\n)-o(\n|$)/, 'resume writes the final message through -o');
|
|
666
|
+
assert.match(r.argv, /(^|\n)--json(\n|$)/, 'resume streams the structured events');
|
|
667
|
+
assert.match(r.argv, /(^|\n)--color(\n|$)/, 'resume disables colour like a fresh run');
|
|
668
|
+
assert.match(r.stdout, /FAKE_FINAL_MESSAGE/, 'resume stdout is still the final message');
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
it('resume falls back to the trace tail when the final-message file is missing', () => {
|
|
672
|
+
const sb = makeSandbox();
|
|
673
|
+
const r = run(sb, { args: ['--resume', 'sess-noout', '-'], input: 'go', env: { CODEX_FAKE_NO_OUT: '1' } });
|
|
674
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
675
|
+
assert.equal(r.status, 0, r.stderr);
|
|
676
|
+
assert.match(r.stderr, /no final-message file/, 'the fallback is loud, never silent');
|
|
677
|
+
assert.match(r.stdout, /turn\.completed/, 'the trace tail carries the event stream resume now captures');
|
|
454
678
|
});
|
|
455
679
|
|
|
456
680
|
it('--resume-last reads the session id from the sidecar', () => {
|
|
@@ -278,7 +278,7 @@ DEFAULT_CODEX_EFFORT="xhigh"
|
|
|
278
278
|
# Review-receipt identity (AD-038). AW_BRIDGE_VERSION mirrors this bridge's SKILL.md/capability.json
|
|
279
279
|
# version (drift-guarded by codex-review.test.mjs against capability.json).
|
|
280
280
|
AW_RECEIPT_BACKEND="codex"
|
|
281
|
-
AW_BRIDGE_VERSION="3.
|
|
281
|
+
AW_BRIDGE_VERSION="3.4.0"
|
|
282
282
|
CODEX_MODEL="${CODEX_MODEL:-$DEFAULT_CODEX_MODEL}"
|
|
283
283
|
CODEX_EFFORT="${CODEX_EFFORT:-$DEFAULT_CODEX_EFFORT}"
|
|
284
284
|
# Generous hard cap for a slow xhigh review (subscription latency varies).
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"schema": 1,
|
|
4
4
|
"name": "codex-cli-bridge",
|
|
5
5
|
"kind": "execution-backend",
|
|
6
|
-
"version": "3.
|
|
6
|
+
"version": "3.4.0",
|
|
7
7
|
"posture": { "model": "gpt-5.6-sol", "effort": "xhigh", "tier": null },
|
|
8
8
|
"provides": ["execute", "review"],
|
|
9
9
|
"roles": {
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
"exec posture banner: ONE stderr line before dispatch states the ACTUAL run posture — exec posture: model=… effort=… tier=… sandbox=workspace-write session=fresh|resume:<id> timeout=… — from RESOLVED post-validation values; the resume id is validated pre-spend, and control bytes in any banner field refuse pre-spend",
|
|
32
32
|
"threat model: the sidecar byte and grammar screens detect corrupted input under a trusted parent environment. A hostile parent environment — including exported shell functions or PATH substitution of core/backend commands — is outside the threat model and can substitute the backend itself. Targeted shadow-proof resolution protects banner/dispatch honesty from accidental shadowing; it is not an environment security boundary",
|
|
33
33
|
"the exec posture banner appends a banner-only timeout=<duration|uncapped> field — exactly the duration handed to timeout(1), uncapped when no timeout/gtimeout binary caps the run; INFORMATIONAL only: it is never persisted in a receipt or session sidecar",
|
|
34
|
-
"quote the posture banner verbatim when labeling this dispatch — the banner is the machine-stated posture; a prose re-type drifts"
|
|
34
|
+
"quote the posture banner verbatim when labeling this dispatch — the banner is the machine-stated posture; a prose re-type drifts",
|
|
35
|
+
"every-run nested-sandbox scan (DUAL policy, deliberately two different rules): the scan runs on EVERY completed run, not only a failed one, because a run that SURVIVES the nested-sandbox failure exits 0 with an ungrounded answer and nothing said so. Failed run (rc != 0): the existing loose whole-trace combination rule prints the recovery hint. Successful run (rc == 0): a warning fires ONLY on precise per-item evidence — both a sandbox-mechanism token AND a permission/read-only failure token inside the aggregated_output of ONE command_execution item whose failure is PROVEN (a nonzero exit_code, or the serialized status \"failed\"); a null exit_code is never failure by itself, tokens split across two items never fire, and a successful command's output never fires. The answer is printed FIRST on stdout, then the warning on stderr. HONEST RESIDUAL: the exit status does NOT change on that lane (a distinct nonzero exit would give a heuristic scan DENY polarity, refusing real work whenever the scan over-warns), so an orchestrator keying on exit status alone can still bank an ungrounded answer — the stderr warning is the signal"
|
|
35
36
|
]
|
|
36
37
|
}
|
|
37
38
|
},
|
|
@@ -75,9 +75,11 @@ codex-review code "focus on the reducer and its tests"
|
|
|
75
75
|
`codex-exec` prepends an **orchestrator execution contract**: work in the current tree, never
|
|
76
76
|
git-write, *obey* the already-merged `AGENTS.md` (Hard Constraints + declared gates), self-review the
|
|
77
77
|
diff (incl. untracked files), run the project's declared gates (STOP if none are declared), don't
|
|
78
|
-
commit, report blockers. It captures only codex's **final message** (`-o
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
commit, report blockers. It captures only codex's **final message** (`-o`) and, on a **non-resume**
|
|
79
|
+
run, records the session id to `${CODEX_SESSION_FILE:-./.codex-last-session}`. The JSON event stream
|
|
80
|
+
+ reasoning go to a run trace that is **read before it is discarded** — fresh runs and resumes share
|
|
81
|
+
ONE capture posture, and the wrapper scans that trace on every completed run for a nested-sandbox
|
|
82
|
+
failure the run SURVIVED (see [`sandbox-and-flags.md`](sandbox-and-flags.md#clean-output-capture)).
|
|
81
83
|
|
|
82
84
|
## Resume — iterate without re-sending context
|
|
83
85
|
|