@sabaiway/agent-workflow-kit 14.22.0 → 14.23.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 (28) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/SKILL.md +1 -1
  3. package/bridges/antigravity-cli-bridge/SKILL.md +16 -10
  4. package/bridges/antigravity-cli-bridge/bin/agy-jev.test.mjs +105 -0
  5. package/bridges/antigravity-cli-bridge/bin/agy-review.sh +20 -6
  6. package/bridges/antigravity-cli-bridge/bin/agy.sh +26 -11
  7. package/bridges/antigravity-cli-bridge/capability.json +3 -3
  8. package/bridges/antigravity-cli-bridge/references/models-and-flags.md +4 -3
  9. package/bridges/antigravity-cli-bridge/setup/README.md +2 -1
  10. package/bridges/codex-cli-bridge/SKILL.md +44 -24
  11. package/bridges/codex-cli-bridge/bin/codex-exec-jev.test.mjs +385 -0
  12. package/bridges/codex-cli-bridge/bin/codex-exec.sh +262 -42
  13. package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +16 -18
  14. package/bridges/codex-cli-bridge/bin/codex-model-posture.test.mjs +16 -7
  15. package/bridges/codex-cli-bridge/bin/codex-review-honesty.test.mjs +1 -0
  16. package/bridges/codex-cli-bridge/bin/codex-review-jev.test.mjs +301 -0
  17. package/bridges/codex-cli-bridge/bin/codex-review.sh +240 -26
  18. package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +9 -9
  19. package/bridges/codex-cli-bridge/capability.json +10 -10
  20. package/bridges/codex-cli-bridge/references/driving-codex.md +18 -14
  21. package/bridges/codex-cli-bridge/references/sandbox-and-flags.md +61 -22
  22. package/bridges/codex-cli-bridge/setup/README.md +8 -5
  23. package/capability.json +1 -1
  24. package/package.json +1 -1
  25. package/references/modes/recommendations.md +1 -1
  26. package/references/modes/velocity.md +1 -1
  27. package/tools/detect-backends.mjs +3 -3
  28. package/tools/manifest/schema.md +2 -2
@@ -9,8 +9,11 @@
9
9
  #
10
10
  # Fixed policy (single source of truth — passed via flags + --ignore-user-config,
11
11
  # so behaviour is deterministic regardless of ~/.codex/config.toml):
12
- # - workspace-write sandbox: codex may edit the repo, nothing outside it
13
- # - network access OFF: new dependencies / network installs are done by a human
12
+ # - codex may edit the repo and the temp dirs, nothing else: on Linux with codex-cli 0.160.0 or
13
+ # newer, from a repository outside every temp root and off a codex version and profile digest
14
+ # recorded as refused, the run carries the permissions profile `jev` (workspace write, network to
15
+ # api.typesafe.ai and docs.typesafe.ai only); elsewhere today's workspace-write sandbox with network off. The banner's
16
+ # jev= field says which; new dependencies / network installs are done by a human
14
17
  # - approval_policy=never: there is no TTY in exec; anything needing escalation
15
18
  # is refused and reported, then handled by hand
16
19
  # - the host posture's model and reasoning effort (see below)
@@ -26,9 +29,10 @@
26
29
  # re-sending context), never from a per-call downgrade.
27
30
  #
28
31
  # Auth: SUBSCRIPTION ONLY. Uses the cached ChatGPT login under CODEX_HOME
29
- # (~/.codex). The wrapper unsets every *_API_KEY plus OPENAI_BASE_URL and passes
30
- # --ignore-user-config, so a stray key or a personal ~/.codex/config.toml can
31
- # never silently switch billing or change behaviour. No credentials are bundled.
32
+ # (~/.codex). The wrapper unsets OPENAI_BASE_URL and every *_API_KEY except
33
+ # TYPESAFE_API_KEY (which passes as given) and passes --ignore-user-config, so a stray
34
+ # key or a personal ~/.codex/config.toml can never silently switch billing or change
35
+ # behaviour. No credentials are bundled.
32
36
  #
33
37
  # Usage (installed on PATH as `codex-exec`):
34
38
  # codex-exec docs/plans/<slug>.md # drive a plan file
@@ -47,7 +51,7 @@ set -euo pipefail
47
51
  case "${1:-}" in
48
52
  --help|-h)
49
53
  cat <<'HELP'
50
- codex-exec — delegate plan/instruction EXECUTION to the OpenAI Codex CLI (subscription-only; workspace-write sandbox, network OFF, git writes blocked — the orchestrator commits).
54
+ codex-exec — delegate plan/instruction EXECUTION to the OpenAI Codex CLI (subscription-only; writes to the repository and the temp dirs only, network to api.typesafe.ai and docs.typesafe.ai where the profile runs and off elsewhere, git writes blocked — the orchestrator commits).
51
55
 
52
56
  Usage:
53
57
  codex-exec [--nonce <n>] <plan-file|->
@@ -66,6 +70,7 @@ Round-2 / resume:
66
70
  Guarded passthrough after '--':
67
71
  blocked always: -c* --config* -s* --sandbox* --dangerously-bypass-approvals-and-sandbox --dangerously-bypass-hook-trust --full-auto --oss --local-provider* -p* --profile* -m* --model* -o* --output-last-message* --json* --color* --output-schema* --ephemeral*
68
72
  relaxed only under CODEX_PROBE=1: --add-dir* -C* --cd* --skip-git-repo-check --ignore-rules --enable* --disable*
73
+ (under the permissions profile `jev`, --add-dir -C --cd --enable --disable refuse even then)
69
74
 
70
75
  Receipt:
71
76
  side effect — a NONCED run mints ONE exec receipt beside the delegation store: the dispatch nonce
@@ -121,18 +126,24 @@ Notes:
121
126
  (excludedCommands / a per-run consented bypass) on the OBSERVED bwrap/EPERM failure, never a
122
127
  preemptive blanket
123
128
  exec posture banner: ONE stderr line before dispatch states the ACTUAL run posture —
124
- exec posture: model=… effort=… tier=… sandbox=workspace-write session=fresh|resume:<id> source=… timeout=… —
125
- from RESOLVED post-validation values; the resume id is validated pre-spend, and control bytes in
126
- any banner field refuse pre-spend
129
+ exec posture: model=… effort=… tier=… sandbox=jev-profile|workspace-write session=fresh|resume:<id> jev=… source=… timeout=… —
130
+ from RESOLVED post-validation values; the resume id is validated pre-spend, control bytes in
131
+ a model, effort, tier or timeout value refuse pre-spend, and a jev= reason shows a control byte as ?
127
132
  threat model: the sidecar byte and grammar screens detect corrupted input under a trusted parent
128
133
  environment. A hostile parent environment — including exported shell functions or PATH
129
134
  substitution of core/backend commands — is outside the threat model and can substitute the
130
135
  backend itself. Targeted shadow-proof resolution protects banner/dispatch honesty from accidental
131
136
  shadowing; it is not an environment security boundary
132
- the exec posture banner appends a banner-only source=model:<s>,effort:<s> field — where each value
133
- came from: default, setting or environment — then a banner-only timeout=<duration|uncapped> field
134
- — exactly the duration handed to timeout(1), uncapped when no timeout/gtimeout binary caps the
135
- run; INFORMATIONAL only: neither is persisted in a receipt or session sidecar
137
+ the exec posture banner states what the run reaches in a jev= field: jev=api.typesafe.ai,docs.typesafe.ai
138
+ when the run carries the permissions profile `jev` (Linux, codex-cli 0.160.0 or newer, a repository
139
+ outside every temp root, a version and profile digest not recorded as refused), else
140
+ jev=unreachable (<reason>) on today's workspace-write flags with network off; a run whose codex
141
+ refuses the profile exits 72 with no success receipt and, for a refused profile key, records its
142
+ version and digest when the state file can be written, so the next run on them takes today's flags; then a banner-only
143
+ source=model:<s>,effort:<s> field — where each value came from: default, setting or environment —
144
+ then a banner-only timeout=<duration|uncapped> field — exactly the duration handed to timeout(1),
145
+ uncapped when no timeout/gtimeout binary caps the run; INFORMATIONAL only: none of the three is
146
+ persisted in a receipt or session sidecar
136
147
  quote the posture banner verbatim when labeling this dispatch — the banner is the machine-stated
137
148
  posture; a prose re-type drifts
138
149
  every-run nested-sandbox scan (DUAL policy, deliberately two different rules): the scan runs on
@@ -164,6 +175,9 @@ esac
164
175
  AW_SETTINGS_APPLIED="CODEX_SERVICE_TIER CODEX_HARD_TIMEOUT"
165
176
  DEFAULT_CODEX_MODEL="gpt-6.1-sol"
166
177
  DEFAULT_CODEX_EFFORT="high"
178
+ # This wrapper's filesystem part of the permissions profile `jev` (the shared block below): the whole disk read,
179
+ # writes to the temp dirs and the repository root the wrapper preflights and runs from (P3).
180
+ AW_PROFILE_FILESYSTEM='{":root"="read",":tmpdir"="write",":slash_tmp"="write",":workspace_roots"={"."="write"}}'
167
181
 
168
182
  # --- Bridge settings file (host-level, kit-independent) — byte-identical across the four wrappers ---
169
183
  # ${XDG_CONFIG_HOME:-$HOME/.config}/agent-workflow/bridge-settings.conf holds KEY=VALUE lines,
@@ -352,6 +366,19 @@ aw_resolve_timeout_bin() {
352
366
  [[ -f "$bin" && -x "$bin" ]] || { printf ''; return 0; }
353
367
  printf '%s' "$bin"
354
368
  }
369
+ aw_scrub_billing_keys() {
370
+ # The one key rule (spec jev-every-run): OPENAI_BASE_URL and every *_API_KEY are unset, so a stray key can never
371
+ # switch a run to paid billing; TYPESAFE_API_KEY is neither read nor set, so it reaches the CLI exactly as given.
372
+ local name
373
+ unset OPENAI_BASE_URL 2>/dev/null || true
374
+ for name in $(compgen -v); do
375
+ case "$name" in
376
+ TYPESAFE_API_KEY) ;;
377
+ *_API_KEY) unset "$name" 2>/dev/null || true ;;
378
+ esac
379
+ done
380
+ return 0
381
+ }
355
382
 
356
383
  # Generous hard wall-clock cap, sized for a slow run at the host posture's effort (subscription latency
357
384
  # varies — a trivial reply was observed taking minutes). Raise for a known-healthy
@@ -403,7 +430,7 @@ CHATGPT_LOGIN_GUARD="Logged in using ChatGPT"
403
430
  # receipt this wrapper mints; scripts/release/version-sync.mjs bumps it under the one-anchor-per-file
404
431
  # rule, so a release can never leave it behind (the AD-053 drift class).
405
432
  AW_RECEIPT_BACKEND="codex"
406
- AW_BRIDGE_VERSION="4.0.0" # aw-version-anchor
433
+ AW_BRIDGE_VERSION="5.0.0" # aw-version-anchor
407
434
  # The kill grace handed to timeout(1) as --kill-after, and recorded in the receipt as killGraceS:
408
435
  # ONE constant, so the number the ledger checks against the dispatch deadline is the number the run
409
436
  # actually applied.
@@ -416,12 +443,8 @@ fi
416
443
 
417
444
  # --- Subscription-only guard -------------------------------------------------
418
445
  # Never let an API key (or a user config) silently switch codex to paid api-key
419
- # billing or alternate behaviour. Clear the explicit vars first, then any other
420
- # *_API_KEY that may have been added later (`compgen` is a bash builtin).
421
- unset OPENAI_API_KEY CODEX_API_KEY OPENAI_BASE_URL 2>/dev/null || true
422
- while IFS= read -r _api_key_var; do
423
- unset "$_api_key_var" 2>/dev/null || true
424
- done < <(compgen -v 2>/dev/null | grep '_API_KEY$' || true)
446
+ # billing or alternate behaviour: the shared key rule, before the first spawn of codex.
447
+ aw_scrub_billing_keys
425
448
 
426
449
  # --- Environment preflight (fail fast, before spending a subscription run) ----
427
450
  if ! command -v codex >/dev/null 2>&1; then
@@ -472,6 +495,179 @@ if [[ ! -f AGENTS.md ]]; then
472
495
  exit 2
473
496
  fi
474
497
 
498
+ # --- The permissions profile `jev` (spec jev-every-run) — byte-identical in codex-exec.sh and codex-review.sh ---
499
+ # On Linux with codex-cli 0.160.0 or newer a run carries the profile `jev` by four -c overrides, with neither
500
+ # --sandbox nor sandbox_mode beside them: its filesystem part is this wrapper's AW_PROFILE_FILESYSTEM, and its
501
+ # network reaches api.typesafe.ai and docs.typesafe.ai and no other host (a literal of this bridge, which never
502
+ # imports the kit). Elsewhere, and on a codex version and profile digest recorded as refused, the run keeps
503
+ # today's flags and its banner says jev=unreachable (<reason>). Each run, a resume included, chooses afresh.
504
+ AW_PROFILE_FLOOR="0.160.0"
505
+ AW_PROFILE_HOSTS="api.typesafe.ai,docs.typesafe.ai"
506
+ AW_PROFILE_FLAGS=(-c default_permissions=jev -c permissions.jev.network.enabled=true
507
+ -c 'permissions.jev.network.domains={"api.typesafe.ai"="allow","docs.typesafe.ai"="allow"}'
508
+ -c "permissions.jev.filesystem=$AW_PROFILE_FILESYSTEM")
509
+ # The version on the first line of `codex --version` ("codex-cli X.Y.Z[-pre]"); nothing when unreadable.
510
+ aw_codex_version() { # $1 = the timeout binary or ""
511
+ local raw="" line="" re='^codex-cli[[:space:]]+([0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?)[[:space:]]*$'
512
+ if [[ -n "$1" ]]; then
513
+ raw="$("$1" --kill-after=5s 15 codex --version </dev/null 2>/dev/null)" || raw=""
514
+ else
515
+ raw="$(codex --version </dev/null 2>/dev/null)" || raw=""
516
+ fi
517
+ line="${raw%%$'\n'*}"
518
+ line="${line%$'\r'}"
519
+ if [[ "$line" =~ $re ]]; then printf '%s' "${BASH_REMATCH[1]}"; fi
520
+ return 0
521
+ }
522
+ # 0 when version $1 is at or above $2, compared as major.minor.patch; a prerelease sorts below its release.
523
+ aw_version_at_least() {
524
+ local core="${1%%-*}" have=() want=() i
525
+ IFS=. read -r -a have <<<"$core"
526
+ IFS=. read -r -a want <<<"$2"
527
+ for i in 0 1 2; do
528
+ if (( 10#${have[i]} != 10#${want[i]} )); then
529
+ (( 10#${have[i]} > 10#${want[i]} ))
530
+ return
531
+ fi
532
+ done
533
+ [[ "$1" == "$core" ]]
534
+ }
535
+ aw_profile_state_file() {
536
+ printf '%s/agent-workflow/codex-jev-profile-refused' "${XDG_STATE_HOME:-$HOME/.local/state}"
537
+ }
538
+ # sha256 of this run's profile overrides, one per line; nothing without a sha256 tool.
539
+ aw_profile_digest() {
540
+ local sum=""
541
+ if command -v sha256sum >/dev/null 2>&1; then
542
+ sum="$(printf '%s\n' "${AW_PROFILE_FLAGS[@]}" | sha256sum 2>/dev/null)" || sum=""
543
+ elif command -v shasum >/dev/null 2>&1; then
544
+ sum="$(printf '%s\n' "${AW_PROFILE_FLAGS[@]}" | shasum -a 256 2>/dev/null)" || sum=""
545
+ fi
546
+ printf '%s' "${sum%% *}"
547
+ }
548
+ # 0 when the state file $1 lists "<version $2> <digest $3>"; an unreadable file reads as empty.
549
+ aw_profile_refused() {
550
+ local line=""
551
+ [[ -n "$3" && -f "$1" && -r "$1" ]] || return 1
552
+ while IFS= read -r line || [[ -n "$line" ]]; do
553
+ if [[ "${line%$'\r'}" == "$2 $3" ]]; then return 0; fi
554
+ done <"$1"
555
+ return 1
556
+ }
557
+ # Appends "<version $2> <digest $3>" to the state file $1, its directory made 0700; one stderr line when it cannot.
558
+ aw_profile_record() {
559
+ local dir="${1%/*}"
560
+ if [[ -n "$3" ]] && { ( umask 077 && mkdir -p -- "$dir" ) && chmod 700 -- "$dir" && printf '%s %s\n' "$2" "$3" >>"$1"; } 2>/dev/null; then
561
+ return 0
562
+ fi
563
+ echo "warning: could not append the refused profile to $1 — until it can be written, every run on this codex version spends one run on the refusal." >&2
564
+ return 1
565
+ }
566
+ # Chooses this run's posture: profile_on (1 or 0), profile_reason, and the banner's jev= value in profile_label.
567
+ aw_profile_choose() { # $1 = the timeout binary or "", $2 = the repository's temp root or ""
568
+ local os=""
569
+ profile_on=0
570
+ profile_reason=""
571
+ profile_version="$(aw_codex_version "$1")"
572
+ profile_digest="$(aw_profile_digest)"
573
+ os="$(uname -s 2>/dev/null)" || os=""
574
+ if [[ "$os" != "Linux" ]]; then
575
+ profile_reason="not linux (${os:-unreadable})"
576
+ elif [[ -z "$profile_version" ]]; then
577
+ profile_reason="codex version unreadable"
578
+ elif ! aw_version_at_least "$profile_version" "$AW_PROFILE_FLOOR"; then
579
+ profile_reason="codex $profile_version below $AW_PROFILE_FLOOR"
580
+ elif [[ -n "$2" ]]; then
581
+ profile_reason="repository under $2"
582
+ elif aw_profile_refused "$(aw_profile_state_file)" "$profile_version" "$profile_digest"; then
583
+ profile_reason="profile not recognized by codex $profile_version, recorded in $(aw_profile_state_file)"
584
+ else
585
+ profile_on=1
586
+ fi
587
+ if (( profile_on )); then profile_label="$AW_PROFILE_HOSTS"; else profile_label="unreachable ($profile_reason)"; fi
588
+ profile_label="${profile_label//[$'\x01'-$'\x1f'$'\x7f']/?}"
589
+ return 0
590
+ }
591
+ # The loud failure, judged on ONE attempt's trace: 0 on a match, its kind in profile_refusal (1 = a P1/P13-shaped
592
+ # error item before the first turn.started, 2 = `bwrap: execvp` in a command item whose failure is proven) and
593
+ # the matched text in profile_refusal_text. The same strings in a successful command or the final message never fire.
594
+ aw_profile_scan() { # $1 = the trace
595
+ local line="" started=0
596
+ local item_re='^\{"type":"item\.completed","item":\{("id":"[^"]*",)?"type":"error","message":"((Permissions profile `jev` |Configured filesystem path `[^`"]*` is not recognized )[^"\\]*)"'
597
+ profile_refusal=0
598
+ profile_refusal_text=""
599
+ [[ -r "$1" ]] || return 1
600
+ while IFS= read -r line || [[ -n "$line" ]]; do
601
+ if [[ "$line" == '{"type":"turn.started"'* ]]; then started=1; fi
602
+ if (( ! started )) && [[ "$line" =~ $item_re ]]; then
603
+ profile_refusal=1
604
+ profile_refusal_text="${BASH_REMATCH[2]}"
605
+ return 0
606
+ fi
607
+ if aw_ns_item_evidence "$line" && [[ "$AW_NS_OUTPUT" == *'bwrap: execvp'* ]]; then
608
+ profile_refusal=2
609
+ profile_refusal_text="${AW_NS_OUTPUT%%\\n*}"
610
+ return 0
611
+ fi
612
+ done <"$1"
613
+ return 1
614
+ }
615
+ # Prints the matched refusal (no "update codex" remedy: none is known) and records a refused profile key.
616
+ aw_profile_refusal_report() {
617
+ local file=""
618
+ echo "error: codex refused this run's permissions profile \`jev\` (exit 72): $profile_refusal_text" >&2
619
+ if (( profile_refusal == 1 )); then
620
+ file="$(aw_profile_state_file)"
621
+ if aw_profile_record "$file" "$profile_version" "$profile_digest"; then
622
+ echo " recorded in $file: the next run on codex $profile_version with this profile runs today's flags (jev=unreachable)." >&2
623
+ fi
624
+ fi
625
+ return 0
626
+ }
627
+
628
+ # The temp root this repository sits under, printed; nothing when none. The exec profile leaves a .git under /tmp
629
+ # or $TMPDIR writable (P12a) while today's flags keep it read-only (P16), so such a run keeps today's flags.
630
+ aw_repo_temp_root() {
631
+ local root="" real_root="" path="" real=""
632
+ for root in /tmp "${TMPDIR:-}"; do
633
+ [[ -n "$root" ]] || continue
634
+ real_root="$(builtin cd -P -- "$root" 2>/dev/null && builtin pwd -P)" || continue
635
+ # --git-common-dir may answer relative to this directory, where cd -P resolves it; no --path-format (git 2.31+).
636
+ for path in "$(git rev-parse --show-toplevel 2>/dev/null)" "$(git rev-parse --git-common-dir 2>/dev/null)"; do
637
+ [[ -n "$path" ]] || continue
638
+ real="$(builtin cd -P -- "$path" 2>/dev/null && builtin pwd -P)" || continue
639
+ if [[ "$real" == "$real_root" || "$real" == "${real_root%/}/"* ]]; then
640
+ if [[ "$root" == /tmp ]]; then printf '/tmp'; else printf '%s' "$real_root"; fi
641
+ return 0
642
+ fi
643
+ done
644
+ done
645
+ return 0
646
+ }
647
+ # The version read is capped by the same timeout binary as the run (resolved once; the banner reuses it).
648
+ timeout_bin="$(aw_resolve_timeout_bin)"
649
+ aw_profile_choose "$timeout_bin" "$(aw_repo_temp_root)"
650
+ network_flags=()
651
+ if (( profile_on )); then
652
+ aw_sandbox_label="jev-profile"
653
+ read -r -d '' aw_network_item <<'ITEM' || true
654
+ 5. Do NOT commit. Network: the hosts api.typesafe.ai and docs.typesafe.ai are
655
+ reachable; network access to any other host is an escalation: STOP and report.
656
+ If you hit another blocker needing escalation (writes outside the repo, a live
657
+ approval, or an ambiguous decision), STOP and report it clearly — never guess.
658
+ ITEM
659
+ aw_network_line="Network, as of this run (it replaces any earlier statement): the hosts api.typesafe.ai and docs.typesafe.ai are reachable; network access to any other host is an escalation: STOP and report."
660
+ else
661
+ aw_sandbox_label="workspace-write"
662
+ network_flags=(-c sandbox_workspace_write.network_access=false)
663
+ read -r -d '' aw_network_item <<'ITEM' || true
664
+ 5. Do NOT commit. If you hit a blocker needing escalation (network access, writes
665
+ outside the repo, a live approval, or an ambiguous decision), STOP and report
666
+ it clearly — never guess.
667
+ ITEM
668
+ aw_network_line="Network, as of this run (it replaces any earlier statement): network access is an escalation: STOP and report."
669
+ fi
670
+
475
671
  read -r -d '' ORCHESTRATOR_DIRECTIVE <<'DIRECTIVE' || true
476
672
  ORCHESTRATOR EXECUTION CONTRACT — read before the task, follow it exactly:
477
673
  1. Work directly in the current working tree on the current git branch. NEVER run
@@ -486,21 +682,17 @@ ORCHESTRATOR EXECUTION CONTRACT — read before the task, follow it exactly:
486
682
  4. Run the verification / gate set the project declares (in AGENTS.md or the
487
683
  task). If the project declares NO gate set, STOP and report — do NOT invent
488
684
  checks. Fix every failure before finishing.
489
- 5. Do NOT commit. If you hit a blocker needing escalation (network access, writes
490
- outside the repo, a live approval, or an ambiguous decision), STOP and report
491
- it clearly — never guess.
492
-
493
- TASK:
494
685
  DIRECTIVE
686
+ # Item 5 states this run's posture (spec jev-every-run): the two hosts on a profile run, network access whole on a fallback run.
687
+ ORCHESTRATOR_DIRECTIVE="$ORCHESTRATOR_DIRECTIVE"$'\n'"$aw_network_item"$'\n\nTASK:'
495
688
 
496
689
  read -r -d '' RESUME_REMINDER <<'REMINDER' || true
497
690
  CONTINUE the existing task in the same working tree under the SAME contract: never
498
691
  run a git write command (the orchestrator commits), obey the project's root AGENTS.md
499
692
  Hard Constraints, run the project's declared gates, do NOT commit, and STOP + report
500
693
  any blocker — never guess.
501
-
502
- NEW INSTRUCTION:
503
694
  REMINDER
695
+ RESUME_REMINDER="$RESUME_REMINDER"$'\n'"$aw_network_line"$'\n\nNEW INSTRUCTION:'
504
696
 
505
697
  if [[ $# -lt 1 ]]; then
506
698
  echo "usage: $0 <plan-file|-> [-- extra codex args...]" >&2
@@ -659,6 +851,12 @@ else
659
851
  echo " Set CODEX_PROBE=1 for a throwaway probe, or invoke 'codex' directly." >&2
660
852
  exit 2
661
853
  fi
854
+ # Under the permissions profile `jev` these five would move its workspace root or its features.
855
+ if (( profile_on )) && [[ "$_arg" == --add-dir* || "$_arg" == -C* || "$_arg" == --cd* || "$_arg" == --enable* || "$_arg" == --disable* ]]; then
856
+ echo "error: passthrough flag '$_arg' is refused while the run carries the permissions profile \`jev\` — it would move the" >&2
857
+ echo " profile's workspace root or features; nothing was spent. A run on today's flags (jev=unreachable) admits it." >&2
858
+ exit 2
859
+ fi
662
860
  ;;
663
861
  esac
664
862
  done
@@ -930,13 +1128,19 @@ if [[ -n "$resume_mode" ]]; then
930
1128
  # so the capture posture is shared with a fresh run MINUS that flag. Shipping --color here once
931
1129
  # broke every resume invocation with a pre-spend exit 2 — the fake CLI in the suite accepts any
932
1130
  # argv, so only the real one can answer this question. See RESUME_ACCEPTED_FLAGS in the test.
1131
+ # The posture rides -c on a resume too: the profile's overrides (P7), else sandbox_mode with network off.
1132
+ if (( profile_on )); then
1133
+ posture_flags=("${AW_PROFILE_FLAGS[@]}")
1134
+ else
1135
+ posture_flags=(-c sandbox_mode=workspace-write)
1136
+ fi
933
1137
  codex_cmd=(codex exec resume "$resume_id"
934
1138
  --ignore-user-config
935
1139
  -m "$CODEX_MODEL"
936
1140
  -c model_reasoning_effort="$CODEX_EFFORT"
937
- -c sandbox_mode=workspace-write
1141
+ "${posture_flags[@]}"
938
1142
  -c approval_policy=never
939
- -c sandbox_workspace_write.network_access=false
1143
+ "${network_flags[@]+"${network_flags[@]}"}"
940
1144
  -c hide_agent_reasoning=true
941
1145
  -c model_reasoning_summary=none
942
1146
  "${tier_flags[@]+"${tier_flags[@]}"}"
@@ -949,11 +1153,16 @@ else
949
1153
  # (thread.started carries the session id). CoT is dropped and colour disabled, so
950
1154
  # the captured surfaces stay clean. Reasoning still runs at the run's effort — quality is
951
1155
  # unchanged; we only stop printing the noise.
1156
+ if (( profile_on )); then
1157
+ posture_flags=("${AW_PROFILE_FLAGS[@]}")
1158
+ else
1159
+ posture_flags=(--sandbox workspace-write)
1160
+ fi
952
1161
  codex_cmd=(codex exec
953
1162
  --ignore-user-config
954
- --sandbox workspace-write
1163
+ "${posture_flags[@]}"
955
1164
  -c approval_policy="never"
956
- -c sandbox_workspace_write.network_access=false
1165
+ "${network_flags[@]+"${network_flags[@]}"}"
957
1166
  -c model_reasoning_effort="$CODEX_EFFORT"
958
1167
  -c hide_agent_reasoning=true
959
1168
  -c model_reasoning_summary=none
@@ -976,10 +1185,9 @@ run_env=(env "PATH=$shim_dir:$PATH")
976
1185
  # after the initial TERM if codex ignores it (a live probe confirmed plain
977
1186
  # `timeout` reaps the whole codex child tree — no --foreground needed). If neither
978
1187
  # binary exists we warn loudly and run uncapped rather than fail silently.
979
- # aw_resolve_timeout_bin: builtin type -P (an exported function can shadow neither `timeout` nor
980
- # `type` itself), normalized to an ABSOLUTE path fail-closed — the dispatch invokes the same
981
- # absolute path the banner rendered from (banner and run never make independent conclusions).
982
- timeout_bin="$(aw_resolve_timeout_bin)"
1188
+ # aw_resolve_timeout_bin (resolved once, before the version read): builtin type -P (an exported function
1189
+ # can shadow neither `timeout` nor `type` itself), normalized to an ABSOLUTE path fail-closed — the dispatch
1190
+ # invokes the same absolute path the banner rendered from (banner and run never make independent conclusions).
983
1191
  if [[ -z "$timeout_bin" ]]; then
984
1192
  # D8 — an ACCOUNTED dispatch that cannot be capped can never honour the terminal-exit rule: the
985
1193
  # ledger records capS + killGraceS and refuses a dispatch whose deadline is below their sum, so a
@@ -1087,7 +1295,7 @@ fi
1087
1295
  aw_timeout_banner="$(aw_timeout_label "$timeout_bin" "$CODEX_HARD_TIMEOUT")"
1088
1296
  aw_session_label="fresh"
1089
1297
  [[ -n "$resume_mode" ]] && aw_session_label="resume:$resume_id"
1090
- echo "exec posture: model=$CODEX_MODEL effort=$CODEX_EFFORT tier=${CODEX_SERVICE_TIER:-standard} sandbox=workspace-write session=$aw_session_label source=model:$CODEX_MODEL_SOURCE,effort:$CODEX_EFFORT_SOURCE timeout=$aw_timeout_banner" >&2
1298
+ echo "exec posture: model=$CODEX_MODEL effort=$CODEX_EFFORT tier=${CODEX_SERVICE_TIER:-standard} sandbox=$aw_sandbox_label session=$aw_session_label jev=$profile_label source=model:$CODEX_MODEL_SOURCE,effort:$CODEX_EFFORT_SOURCE timeout=$aw_timeout_banner" >&2
1091
1299
 
1092
1300
  # --- The PRE-SPEND reservation (delegation Plan 2 / D1) ------------------------
1093
1301
  # Written immediately before the CLI runs — after EVERY preflight and after the posture banner, so a
@@ -1227,9 +1435,11 @@ aw_ns_is_string_content() {
1227
1435
  return 0
1228
1436
  }
1229
1437
 
1230
- # One trace line → 0 only when THAT line is a command_execution item with a PROVEN failure whose
1231
- # aggregated_output carries both token classes. The wrapper stays dependency-free, so this is not a
1232
- # JSON parse — it is ONE anchored walk over the CLI's observed serialization in which every SKIPPED
1438
+ # One trace line → 0 only when THAT line is a command_execution item with a PROVEN failure; its
1439
+ # aggregated_output (still JSON-escaped) lands in AW_NS_OUTPUT for the caller's token test — both
1440
+ # token classes for the nested-sandbox scan, `bwrap: execvp` for the profile scan. codex-review.sh
1441
+ # carries this walk and aw_ns_is_string_content byte-identical. The wrapper stays dependency-free,
1442
+ # so this is not a JSON parse — it is ONE anchored walk over the CLI's observed serialization in which every SKIPPED
1233
1443
  # gap is PROVEN to be a single JSON string's content (aw_ns_is_string_content). Both halves are
1234
1444
  # load-bearing: testing the fields as independent substrings lets a decoy object supply the anchor
1235
1445
  # while the failure fields belong to another item, and skipping a gap without validating it lets the
@@ -1276,7 +1486,8 @@ aw_ns_item_evidence() {
1276
1486
  else
1277
1487
  return 1
1278
1488
  fi
1279
- aw_ns_both_tokens "$agg"
1489
+ AW_NS_OUTPUT="$agg"
1490
+ return 0
1280
1491
  }
1281
1492
 
1282
1493
  aw_scan_nested_sandbox() { # $1 = rc, $2 = trace path
@@ -1292,7 +1503,7 @@ aw_scan_nested_sandbox() { # $1 = rc, $2 = trace path
1292
1503
  fi
1293
1504
  local line
1294
1505
  while IFS= read -r line || [[ -n "$line" ]]; do
1295
- if aw_ns_item_evidence "$line"; then
1506
+ if aw_ns_item_evidence "$line" && aw_ns_both_tokens "$AW_NS_OUTPUT"; then
1296
1507
  echo "warning: NESTED-SANDBOX — this run COMPLETED, but a tool call inside it FAILED with a sandbox-setup" >&2
1297
1508
  echo " signature. codex-exec ships its own OS sandbox (bwrap), which cannot run nested inside a" >&2
1298
1509
  echo " harness sandbox (the FS turns read-only), so the backend most likely could not read what" >&2
@@ -1319,6 +1530,15 @@ fi
1319
1530
  rc=$?
1320
1531
  set -e
1321
1532
 
1533
+ # --- The loud failure (spec jev-every-run): codex refusing the profile, before any receipt or final message ---
1534
+ # Exit 72 is a status no other arm uses; a nonced run's terminal receipt then records transport-failure.
1535
+ profile_refused=0
1536
+ if (( profile_on )) && aw_profile_scan "$trace"; then
1537
+ aw_profile_refusal_report
1538
+ profile_refused=1
1539
+ rc=72
1540
+ fi
1541
+
1322
1542
  # --- The session id, captured BEFORE any outcome branching (D3) ----------------
1323
1543
  # It used to be extracted only on the SUCCESS path, after the timeout and failure exits, so a run
1324
1544
  # that failed or timed out recorded no identity at all — and the session id is exactly the handle an
@@ -1484,7 +1704,7 @@ AW_JS
1484
1704
  if [[ $rc -eq 124 || $rc -eq 137 ]]; then
1485
1705
  echo "error: codex exec exceeded the hard cap CODEX_HARD_TIMEOUT=${CODEX_HARD_TIMEOUT}s and was terminated." >&2
1486
1706
  echo " Raise CODEX_HARD_TIMEOUT for a known-healthy slow run, or narrow the task, then re-dispatch." >&2
1487
- elif [[ $rc -ne 0 ]]; then
1707
+ elif [[ $rc -ne 0 && $profile_refused -eq 0 ]]; then
1488
1708
  echo "error: codex exec failed (exit $rc). Last lines of the run trace:" >&2
1489
1709
  tail -n 40 "$trace" >&2 || true
1490
1710
  aw_scan_nested_sandbox "$rc" "$trace" || true
@@ -2,7 +2,7 @@ import { describe, it, after } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
3
  import {
4
4
  mkdtempSync, mkdirSync, writeFileSync, chmodSync, rmSync, readFileSync,
5
- existsSync, readdirSync, symlinkSync, cpSync,
5
+ existsSync, readdirSync, symlinkSync, cpSync, realpathSync,
6
6
  } from 'node:fs';
7
7
  import { tmpdir } from 'node:os';
8
8
  import { join, dirname, resolve } from 'node:path';
@@ -22,6 +22,7 @@ const FAKE_CODEX = [
22
22
  '#!/usr/bin/env bash',
23
23
  'set -u',
24
24
  'if [[ "${1:-}" == "login" ]]; then echo "${CODEX_FAKE_LOGIN:-Logged in using ChatGPT}"; exit 0; fi',
25
+ 'if [[ "${1:-}" == "--version" ]]; then echo "codex-cli ${CODEX_FAKE_VERSION:-0.160.0}"; exit 0; fi',
25
26
  'if [[ "${1:-}" == "debug" ]]; then cat <<EOF',
26
27
  '{"models":[{"slug":"gpt-6.1-sol","priority":1,"visibility":"list","default_reasoning_level":"low","supported_reasoning_levels":[{"effort":"low"},{"effort":"medium"},{"effort":"high"}]},{"slug":"gpt-6-astra","priority":2,"visibility":"list","default_reasoning_level":"medium","supported_reasoning_levels":[{"effort":"medium"},{"effort":"high"}]}]}',
27
28
  'EOF',
@@ -83,6 +84,7 @@ const FAKE_CODEX = [
83
84
  'exit "${CODEX_FAKE_EXIT:-0}"',
84
85
  '',
85
86
  ].join('\n');
87
+ const FAKE_UNAME = `#!/usr/bin/env bash\nif [[ $# -eq 0 || "$1" == "-s" ]]; then echo Linux; exit 0; fi\nexec "${spawnSync('bash', ['-c', 'type -P uname'], { encoding: 'utf8' }).stdout.trim()}" "$@"\n`; // Linux on any host
86
88
 
87
89
  // The sandbox base and the PATH farms are READ-ONLY per invocation, so both are built ONCE and
88
90
  // shared (a per-test `git init`+commit and a per-call farm rebuild dominate the wall otherwise).
@@ -94,6 +96,7 @@ const TEMPLATE_ROOT = (() => {
94
96
  const bin = join(root, 'bin');
95
97
  mkdirSync(bin, { recursive: true });
96
98
  writeFileSync(join(bin, 'codex'), FAKE_CODEX, { mode: 0o755 });
99
+ writeFileSync(join(bin, 'uname'), FAKE_UNAME, { mode: 0o755 });
97
100
  // A git work tree with a root AGENTS.md — the wrapper preflights both.
98
101
  const repo = join(root, 'repo');
99
102
  mkdirSync(repo);
@@ -111,9 +114,10 @@ const makeSandbox = () => {
111
114
  const root = mkdtempSync(join(tmpdir(), 'codex-exec-test-'));
112
115
  cpSync(TEMPLATE_ROOT, root, { recursive: true });
113
116
  const bin = join(root, 'bin');
114
- chmodSync(join(bin, 'codex'), 0o755);
117
+ for (const name of ['codex', 'uname']) chmodSync(join(bin, name), 0o755);
115
118
  return { root, bin, repo: join(root, 'repo') };
116
119
  };
120
+ const tempRootOf = (sb) => (realpathSync(sb.repo).startsWith(`${realpathSync('/tmp')}/`) ? '/tmp' : realpathSync(process.env.TMPDIR ?? '/tmp')); // spec jev-every-run: a sandbox sits under a temp root, so codex-exec takes the fallback naming it
117
121
 
118
122
  // A PATH dir mirroring the real one MINUS the named binaries, to exercise the
119
123
  // missing-binary fallbacks (no-cap when timeout is gone; the codex/git preflight
@@ -669,13 +673,16 @@ describe('codex-exec.sh — resume entrypoint restates every invariant (3.1)', {
669
673
  assert.equal(sent.includes('--color'), false, 'the regression this list exists to prevent');
670
674
  });
671
675
 
672
- it('--resume <id>: composes `exec resume <id>` with the full restated policy', async () => {
676
+ // The profile half (spec jev-every-run S2) is codex-exec-jev.test.mjs's: this sandbox sits under a temp root.
677
+ it('--resume <id> on a fallback run: composes `exec resume <id>` with the full restated policy', async () => {
673
678
  const sb = makeSandbox();
679
+ const root = tempRootOf(sb);
674
680
  const r = await run(sb, { args: ['--resume', 'sess-xyz', '-'], input: 'continue please' });
675
681
  rmSync(sb.root, { recursive: true, force: true });
676
682
  assert.equal(r.status, 0, r.stderr);
677
683
  assert.match(r.argv, /(^|\n)sess-xyz(\n|$)/, 'the session id is passed positionally');
678
684
  for (const inv of RESUME_INVARIANTS) assert.match(r.argv, inv, `resume argv must include ${inv}`);
685
+ assert.ok(r.stderr.includes(` jev=unreachable (repository under ${root}) source=`), r.stderr);
679
686
  assert.match(r.stdout, /FAKE_FINAL_MESSAGE/, 'resume prints the final message');
680
687
  });
681
688
 
@@ -1511,15 +1518,15 @@ describe('codex-exec.sh — settings surface ⟷ manifest (D6, manifest-pinned)'
1511
1518
  describe('codex-exec.sh — dispatch-posture labeling (D5, AD-061)', { concurrency: 2 }, () => {
1512
1519
  const banners = (stderr) => stderr.split('\n').filter((l) => l.startsWith('exec posture: '));
1513
1520
 
1514
- it('ONE banner line carries the ACTUAL {model, effort, tier, sandbox, session, timeout} on a fresh run', async () => {
1521
+ it('ONE banner line carries the ACTUAL {model, effort, tier, sandbox, session, jev, timeout} on a fresh fallback run', async () => {
1515
1522
  const sb = makeSandbox();
1523
+ const root = tempRootOf(sb);
1516
1524
  const r = await run(sb, {});
1517
1525
  rmSync(sb.root, { recursive: true, force: true });
1518
1526
  assert.equal(r.status, 0, r.stderr);
1519
1527
  const lines = banners(r.stderr);
1520
1528
  assert.equal(lines.length, 1, 'EXACTLY ONE banner line per run');
1521
- assert.equal(lines[0],
1522
- 'exec posture: model=gpt-6.1-sol effort=high tier=standard sandbox=workspace-write session=fresh source=model:default,effort:default timeout=3600s');
1529
+ assert.equal(lines[0], `exec posture: model=gpt-6.1-sol effort=high tier=standard sandbox=workspace-write session=fresh jev=unreachable (repository under ${root}) source=model:default,effort:default timeout=3600s`);
1523
1530
  });
1524
1531
 
1525
1532
  it('an ARMED Fast tier rides the banner (tier=priority)', async () => {
@@ -1695,10 +1702,7 @@ describe('codex-exec.sh — dispatch-posture labeling (D5, AD-061)', { concurren
1695
1702
  'exec "$@"',
1696
1703
  '',
1697
1704
  ].join('\n'), { mode: 0o755 });
1698
- const r = await run(sb, {
1699
- path: `relbin:${sb.bin}:${farmFor(['timeout', 'gtimeout'])}`,
1700
- env: { TIMEOUT_STUB_CAP: cap },
1701
- });
1705
+ const r = await run(sb, { path: `relbin:${sb.bin}:${farmFor(['timeout', 'gtimeout'])}`, env: { TIMEOUT_STUB_CAP: cap } });
1702
1706
  const argv0 = existsSync(cap) ? readFileSync(cap, 'utf8').trim() : '';
1703
1707
  rmSync(sb.root, { recursive: true, force: true });
1704
1708
  assert.equal(r.status, 0, r.stderr);
@@ -2181,10 +2185,7 @@ fs.lstatSync = (target, ...rest) => {
2181
2185
  it('a TIMEOUT publishes exitStatus 124 with outcome transport-failure', async () => {
2182
2186
  const sb = makeSandbox();
2183
2187
  const file = writeContract(sb, 'slow');
2184
- const r = await runAsync(sb, {
2185
- args: ['--nonce', 'slow', file],
2186
- env: { CODEX_FAKE_SLEEP: '3', CODEX_HARD_TIMEOUT: '1' },
2187
- });
2188
+ const r = await runAsync(sb, { args: ['--nonce', 'slow', file], env: { CODEX_FAKE_SLEEP: '3', CODEX_HARD_TIMEOUT: '1' } });
2188
2189
  const receipt = readReceipt(storeDirOf(sb), 'slow');
2189
2190
  rmSync(sb.root, { recursive: true, force: true });
2190
2191
  assert.equal(r.status, 124, r.stderr);
@@ -2215,10 +2216,7 @@ fs.lstatSync = (target, ...rest) => {
2215
2216
  const sb = makeSandbox();
2216
2217
  const file = writeContract(sb, 'noreport');
2217
2218
  const dir = storeDirOf(sb);
2218
- const r = await run(sb, {
2219
- args: ['--nonce', 'noreport', file],
2220
- env: { CODEX_FAKE_MKDIR: join(dir, reportName('noreport')) },
2221
- });
2219
+ const r = await run(sb, { args: ['--nonce', 'noreport', file], env: { CODEX_FAKE_MKDIR: join(dir, reportName('noreport')) } });
2222
2220
  const receipt = readReceipt(dir, 'noreport');
2223
2221
  rmSync(sb.root, { recursive: true, force: true });
2224
2222
  assert.equal(r.status, 71, r.stderr);