@sabaiway/agent-workflow-kit 5.1.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/SKILL.md +13 -1
  3. package/bridges/antigravity-cli-bridge/SKILL.md +14 -3
  4. package/bridges/antigravity-cli-bridge/bin/agy-review.sh +220 -30
  5. package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +264 -8
  6. package/bridges/antigravity-cli-bridge/bin/agy.sh +12 -2
  7. package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +18 -0
  8. package/bridges/antigravity-cli-bridge/capability.json +19 -13
  9. package/bridges/antigravity-cli-bridge/references/driving-agy.md +3 -2
  10. package/bridges/codex-cli-bridge/SKILL.md +17 -7
  11. package/bridges/codex-cli-bridge/bin/codex-exec.sh +156 -36
  12. package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +228 -4
  13. package/bridges/codex-cli-bridge/bin/codex-review.sh +205 -34
  14. package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +276 -5
  15. package/bridges/codex-cli-bridge/capability.json +10 -7
  16. package/bridges/codex-cli-bridge/references/driving-codex.md +7 -5
  17. package/bridges/codex-cli-bridge/references/sandbox-and-flags.md +26 -12
  18. package/capability.json +1 -1
  19. package/package.json +1 -1
  20. package/references/modes/flow-writer.md +37 -0
  21. package/references/modes/gates.md +4 -4
  22. package/references/modes/procedures.md +4 -2
  23. package/references/modes/receipt-deadline.md +16 -0
  24. package/references/modes/review-state.md +1 -1
  25. package/references/modes/set-flow.md +22 -0
  26. package/references/scripts/archive-decisions.mjs +340 -15
  27. package/references/scripts/archive-decisions.test.mjs +522 -2
  28. package/tools/cheap-agents.mjs +8 -2
  29. package/tools/commands.mjs +24 -2
  30. package/tools/commit-guard.mjs +44 -9
  31. package/tools/core-evidence.mjs +25 -22
  32. package/tools/detect-backends.mjs +33 -11
  33. package/tools/dispatch-record.mjs +926 -0
  34. package/tools/doc-parity.mjs +21 -6
  35. package/tools/flow-check.mjs +842 -0
  36. package/tools/flow-record.mjs +795 -0
  37. package/tools/flow-store-read.mjs +114 -0
  38. package/tools/flow-store.mjs +1178 -0
  39. package/tools/flow-writer.mjs +1265 -0
  40. package/tools/fs-read-nofollow.mjs +128 -0
  41. package/tools/gates-declaration.mjs +184 -0
  42. package/tools/gates-init.mjs +59 -17
  43. package/tools/orchestration-config.mjs +87 -10
  44. package/tools/orchestration-write.mjs +3 -3
  45. package/tools/plan-files.mjs +35 -0
  46. package/tools/procedures.mjs +75 -11
  47. package/tools/receipt-deadline.mjs +242 -0
  48. package/tools/recipes.mjs +21 -0
  49. package/tools/repo-lex.mjs +22 -0
  50. package/tools/review-state.mjs +240 -80
  51. package/tools/run-gates.mjs +361 -139
  52. package/tools/set-flow.mjs +465 -0
  53. package/tools/velocity-profile.mjs +8 -2
@@ -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
@@ -210,8 +222,9 @@ aw_apply_settings
210
222
 
211
223
  # --- Effective-timeout resolver (D5 banner honesty; AD-061) --------------------
212
224
  # ONE rule, both bridges: the posture banner prints EXACTLY the duration handed to timeout(1) —
213
- # an integer-seconds value rendered with the `s` suffix, a duration string verbatim and
214
- # `timeout=uncapped` when no timeout/gtimeout binary can cap the run; never a fabricated number.
225
+ # an integer-seconds value rendered with the `s` suffix, a duration string verbatim; without a
226
+ # capping binary the EXEC wrappers print `timeout=uncapped` and run, while the REVIEW wrappers
227
+ # refuse pre-spend (fail-closed preflight) — never a fabricated number.
215
228
  # The EFFECTIVE value (env included — closing the aw_settings_valid env bypass) is validated by
216
229
  # the same per-key rule as the settings file, plus a 7-digit integer-part bound (overflow); an
217
230
  # invalid value warns + falls back to the built-in default — a typo never silently masquerades
@@ -411,9 +424,9 @@ fi
411
424
 
412
425
  # --- Resume detection (must be the FIRST argument) ---------------------------
413
426
  # A dedicated entrypoint for iterating on a session without re-sending context.
414
- # `codex exec resume` RESETS posture and rejects the -s/--add-dir/-C posture flags
415
- # (it DOES accept -o/--json on 0.142.3, but we capture stdout directly), so we
416
- # restate the FULL policy via -c.
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).
417
430
  resume_mode=""
418
431
  resume_id=""
419
432
  case "${1:-}" in
@@ -607,8 +620,10 @@ chmod 755 "$shim_dir/git"
607
620
  # --- Build the codex invocation + the prompt ---------------------------------
608
621
  if [[ -n "$resume_mode" ]]; then
609
622
  # Resume RESETS posture and rejects the -s/--add-dir/-C posture flags, so restate
610
- # the entire policy via -c. We deliberately pass no -o/--json (resume DOES accept
611
- # them) codex prints the final message to stdout, which we capture into $out.
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).
612
627
  codex_cmd=(codex exec resume "$resume_id"
613
628
  --ignore-user-config
614
629
  -m "$CODEX_MODEL"
@@ -619,6 +634,9 @@ if [[ -n "$resume_mode" ]]; then
619
634
  -c hide_agent_reasoning=true
620
635
  -c model_reasoning_summary=none
621
636
  "${tier_flags[@]+"${tier_flags[@]}"}"
637
+ --color never
638
+ -o "$out"
639
+ --json
622
640
  -)
623
641
  full_prompt="$RESUME_REMINDER"$'\n\n'"$task"
624
642
  else
@@ -671,25 +689,136 @@ aw_session_label="fresh"
671
689
  [[ -n "$resume_mode" ]] && aw_session_label="resume:$resume_id"
672
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
673
691
 
674
- # Normal mode: -o writes $out, the JSON stream + logs go to $trace. Resume mode: the
675
- # final message is codex's stdout $out, logs $trace. Either way the final lands
676
- # in $out and diagnostics in $trace, so the post-processing below is shared.
677
- set +e
678
- if [[ -n "$resume_mode" ]]; then
679
- if [[ -n "$timeout_bin" ]]; then
680
- printf '%s' "$full_prompt" | "${run_env[@]}" "$timeout_bin" --kill-after=15s "$CODEX_HARD_TIMEOUT" "${codex_cmd[@]}" >"$out" 2>"$trace"
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
+ :
681
778
  else
682
- printf '%s' "$full_prompt" | "${run_env[@]}" "${codex_cmd[@]}" >"$out" 2>"$trace"
779
+ return 1
683
780
  fi
684
- rc=$?
685
- else
686
- if [[ -n "$timeout_bin" ]]; then
687
- printf '%s' "$full_prompt" | "${run_env[@]}" "$timeout_bin" --kill-after=15s "$CODEX_HARD_TIMEOUT" "${codex_cmd[@]}" >"$trace" 2>&1
688
- else
689
- printf '%s' "$full_prompt" | "${run_env[@]}" "${codex_cmd[@]}" >"$trace" 2>&1
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
690
794
  fi
691
- rc=$?
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
692
820
  fi
821
+ rc=$?
693
822
  set -e
694
823
 
695
824
  if [[ $rc -eq 124 || $rc -eq 137 ]]; then
@@ -700,20 +829,7 @@ fi
700
829
  if [[ $rc -ne 0 ]]; then
701
830
  echo "error: codex exec failed (exit $rc). Last lines of the run trace:" >&2
702
831
  tail -n 40 "$trace" >&2
703
- # Nested-sandbox detection: codex ships its OWN OS sandbox (bwrap); run nested inside a harness
704
- # sandbox the FS is read-only and codex's own sandbox setup fails. Fire the STATED recovery hint
705
- # only on a COMBINATION — a sandbox MECHANISM token AND a permission/read-only FAILURE token in the
706
- # trace — so a lone 'bwrap' banner, or a lone 'permission denied' from unrelated code, is NOT enough
707
- # (never a preemptive blanket). grep is line-oriented, so a plain alternation stays within a line —
708
- # the old `[^\n]*` between the two halves was wrong twice over (it excluded the letter 'n', and grep
709
- # never spans lines anyway), so the split into two `-q` passes both fixes it and states the intent.
710
- if grep -qiE 'bwrap|landlock|user namespace|pivot_root|unshare|seccomp' "$trace" 2>/dev/null \
711
- && grep -qiE 'read-only file system|erofs|operation not permitted|permission denied|eperm' "$trace" 2>/dev/null; then
712
- echo "hint: this looks like a NESTED-SANDBOX failure — codex-exec ships its own OS sandbox (bwrap)," >&2
713
- echo " which cannot run nested inside a harness sandbox (the FS is read-only). Route codex-exec" >&2
714
- echo " OUTSIDE the harness sandbox: add it to the harness sandbox excludedCommands, or dispatch" >&2
715
- echo " this one run via a per-run consented bypass. Do NOT blanket-disable the sandbox." >&2
716
- fi
832
+ aw_scan_nested_sandbox "$rc" "$trace"
717
833
  exit $rc
718
834
  fi
719
835
 
@@ -738,3 +854,7 @@ else
738
854
  echo "warning: codex produced no final-message file — printing the run-trace tail instead." >&2
739
855
  tail -n 40 "$trace"
740
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.doesNotMatch(r.argv, /(^|\n)-o(\n|$)/, 'resume rejects -o');
451
- assert.doesNotMatch(r.argv, /(^|\n)--json(\n|$)/, 'resume rejects --json');
452
- assert.doesNotMatch(r.argv, /(^|\n)--color(\n|$)/, 'resume rejects --color');
453
- assert.match(r.stdout, /FAKE_FINAL_MESSAGE/, 'resume prints codex stdout');
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', () => {