@sabaiway/agent-workflow-kit 14.22.0 → 14.24.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 (34) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/README.md +1 -1
  3. package/SKILL.md +1 -1
  4. package/bridges/antigravity-cli-bridge/SKILL.md +16 -10
  5. package/bridges/antigravity-cli-bridge/bin/agy-jev.test.mjs +105 -0
  6. package/bridges/antigravity-cli-bridge/bin/agy-review.sh +20 -6
  7. package/bridges/antigravity-cli-bridge/bin/agy.sh +26 -11
  8. package/bridges/antigravity-cli-bridge/capability.json +3 -3
  9. package/bridges/antigravity-cli-bridge/references/models-and-flags.md +4 -3
  10. package/bridges/antigravity-cli-bridge/setup/README.md +2 -1
  11. package/bridges/codex-cli-bridge/SKILL.md +44 -24
  12. package/bridges/codex-cli-bridge/bin/codex-exec-jev.test.mjs +385 -0
  13. package/bridges/codex-cli-bridge/bin/codex-exec.sh +262 -42
  14. package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +16 -18
  15. package/bridges/codex-cli-bridge/bin/codex-model-posture.test.mjs +16 -7
  16. package/bridges/codex-cli-bridge/bin/codex-review-honesty.test.mjs +1 -0
  17. package/bridges/codex-cli-bridge/bin/codex-review-jev.test.mjs +301 -0
  18. package/bridges/codex-cli-bridge/bin/codex-review.sh +240 -26
  19. package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +9 -9
  20. package/bridges/codex-cli-bridge/capability.json +10 -10
  21. package/bridges/codex-cli-bridge/references/driving-codex.md +18 -14
  22. package/bridges/codex-cli-bridge/references/sandbox-and-flags.md +61 -22
  23. package/bridges/codex-cli-bridge/setup/README.md +8 -5
  24. package/capability.json +1 -1
  25. package/package.json +1 -1
  26. package/references/modes/recommendations.md +5 -4
  27. package/references/modes/velocity.md +1 -1
  28. package/tools/ack-write.mjs +1 -1
  29. package/tools/bridge-sandbox-recipe.mjs +40 -0
  30. package/tools/detect-backends.mjs +3 -3
  31. package/tools/manifest/schema.md +9 -9
  32. package/tools/manifest/validate.mjs +5 -5
  33. package/tools/recommendations.mjs +17 -46
  34. package/tools/velocity-profile.mjs +64 -14
@@ -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: '4.0.0'
5
+ version: '5.0.0'
6
6
  ---
7
7
 
8
8
  # codex-cli-bridge
@@ -18,7 +18,8 @@ or a working-tree diff and only emits findings (`codex-review`).
18
18
 
19
19
  Use this skill when the user wants to:
20
20
 
21
- - Delegate plan or instruction EXECUTION to `codex` in a workspace-write sandbox (network OFF).
21
+ - Delegate plan or instruction EXECUTION to `codex` in a workspace-write sandbox (network to
22
+ `api.typesafe.ai` and `docs.typesafe.ai` only where the permissions profile `jev` runs, off elsewhere).
22
23
  - Get a second-opinion ADVISORY review of an implementation plan or the current diff.
23
24
  - Install, authenticate, smoke-test, or troubleshoot `codex`, or understand its sandbox/flags/models.
24
25
  - Drive codex efficiently from the main agent (exec vs review, `resume`, the commit boundary).
@@ -39,8 +40,9 @@ binary, run `codex login` once under a ChatGPT subscription, then expose this sk
39
40
  print, copy, commit, or package `~/.codex/auth.json` — it is personal and is **never bundled** with
40
41
  this skill. Both wrappers enforce the subscription path before invoking codex:
41
42
 
42
- - they **unset every `*_API_KEY`** (plus `OPENAI_API_KEY` / `CODEX_API_KEY` / `OPENAI_BASE_URL`) so a
43
- stray key can never silently switch you to paid api-key billing;
43
+ - they **unset `OPENAI_BASE_URL` and every `*_API_KEY` except `TYPESAFE_API_KEY`** (`OPENAI_API_KEY`
44
+ and `CODEX_API_KEY` included; `TYPESAFE_API_KEY` passes as given) so a stray key can never silently
45
+ switch you to paid api-key billing;
44
46
  - they pass **`--ignore-user-config`** so a personal `~/.codex/config.toml` cannot change model,
45
47
  sandbox, or approval behaviour (auth still works — codex reads the login from `CODEX_HOME`
46
48
  regardless of that flag);
@@ -97,7 +99,7 @@ waste, never lowering quality. Full knob list: [§ Environment knobs](#environme
97
99
  Drive codex only through the two wrappers (installed on `PATH`), run from the target project root:
98
100
 
99
101
  ```bash
100
- # EXECUTION (workspace-write sandbox, network OFF, never prompts):
102
+ # EXECUTION (workspace writes; network to api.typesafe.ai and docs.typesafe.ai under the profile `jev`, else off; never prompts):
101
103
  codex-exec docs/plans/<slug>.md # drive a plan file
102
104
  echo "apply review fix: ..." | codex-exec - # ad-hoc instruction from stdin
103
105
  codex-exec <file|-> -- <extra codex flags...> # GUARDED passthrough after `--` (policy/model/capture flags rejected; some relaxed only under CODEX_PROBE=1)
@@ -109,7 +111,7 @@ echo "now do step 2 ..." | codex-exec --resume <session-id> -
109
111
  # ACCOUNTED EXECUTION (the delegation ledger's exec lane — see "Dispatch identity" below):
110
112
  codex-exec --nonce <n> docs/plans/<slug>-dispatch.md # mints a fail-closed exec receipt
111
113
 
112
- # REVIEW (read-only sandbox — codex cannot edit anything, only emits findings):
114
+ # REVIEW (read-only: the profile `jev` with no write entry, or --sandbox read-only — codex only emits findings):
113
115
  codex-review plan docs/plans/<slug>.md # critique a plan
114
116
  codex-review code # review the current working-tree diff (precomputed)
115
117
  codex-review code "focus on the new reducer" # review with extra focus
@@ -118,12 +120,20 @@ codex-review code "focus on the new reducer" # review with extra focus
118
120
  **Honesty + posture (D4/D5):** a run whose final message has no recognized
119
121
  `Verdict: <ship|revise|rethink>` line — empty or missing output included — **exits 4 with NO
120
122
  receipt**: treat it as a *failed review to re-run*, never a fatal session error. One stderr banner
121
- states the actual posture (`review posture: model=… effort=… tier=… source=… timeout=…`) and the
123
+ states the actual posture (`review posture: model=… effort=… tier=… jev=… source=… timeout=…`) and the
122
124
  receipt records the same `posture {model, effort, tier}` (tier `null` on the standard tier); control
123
125
  bytes in a posture value refuse pre-spend in every mode. `codex-exec` states its posture the same way —
124
- ONE `exec posture: model=… effort=… tier=… sandbox=workspace-write session=fresh|resume:<id>
125
- source=… timeout=…` stderr line before dispatch (the resume id validated pre-spend). The `source=`
126
- field (`model:<s>,effort:<s>`, each `default`, `setting` or `environment`) and the `timeout=` field
126
+ ONE `exec posture: model=… effort=… tier=… sandbox=jev-profile|workspace-write session=fresh|resume:<id>
127
+ jev=… source=… timeout=…` stderr line before dispatch (the resume id validated pre-spend). The `jev=`
128
+ field says what the run reaches: `jev=api.typesafe.ai,docs.typesafe.ai` under the permissions profile `jev`
129
+ (Linux, codex-cli 0.160.0 or newer, for `codex-exec` a repository outside every temp root, a codex
130
+ version and profile digest not recorded as refused), else `jev=unreachable (<reason>)` on today's flags.
131
+ A run whose codex refuses the profile (a profile error before the turn starts, or `bwrap: execvp` in a
132
+ failed command) **exits 72** with no success receipt (a review writes none); a refused profile key is
133
+ recorded in `${XDG_STATE_HOME:-~/.local/state}/agent-workflow/codex-jev-profile-refused` when that file
134
+ can be written, so the next run of that wrapper on that version and digest takes today's flags; a
135
+ `bwrap: execvp` failure records nothing. The `jev=`, `source=` (`model:<s>,effort:<s>`,
136
+ each `default`, `setting` or `environment`) and `timeout=` fields
127
137
  are **banner-only** (`timeout=` is exactly the duration handed to `timeout(1)`; on exec `uncapped`
128
138
  without a capping binary, while `codex-review` **fails CLOSED pre-spend** there) — informational,
129
139
  never a receipt field. **Quote the posture banner verbatim** when labeling a dispatch.
@@ -172,8 +182,9 @@ anything that would defeat a policy is guarded — see [§ Models](#models-the-h
172
182
  | `CODEX_REVIEW_SCHEMA` | unset | `codex-review`: `=1` returns findings as a validated JSON object (`--output-schema`), with a raw-text fallback. Default off. |
173
183
  | `CODEX_PROBE` | unset | `=1` ⇒ throwaway-probe mode: the one route to a one-off model or effort, and it relaxes the tier-2 passthrough guard (echoed loudly); a probe review's receipt never attests. Never for real work. |
174
184
 
175
- The git-write shim, `--ignore-user-config`, and the `*_API_KEY` scrub are NOT env-tunable — they are
176
- fixed invariants.
185
+ The git-write shim, `--ignore-user-config`, the key rule (`OPENAI_BASE_URL` and every `*_API_KEY` but
186
+ `TYPESAFE_API_KEY` unset) and the choice between the profile and today's flags are NOT env-tunable —
187
+ they are fixed invariants.
177
188
 
178
189
  ### Dispatch identity — the accounted exec lane
179
190
 
@@ -238,11 +249,17 @@ avoided). And the execution contract tells codex: if the project declares **no**
238
249
  set, **STOP and report** rather than invent checks. Pass `--skip-git-repo-check` to codex only when
239
250
  you truly mean it.
240
251
 
241
- **The Jev skill in a delegated run.** A `codex-exec` run loads the vendor's Jev skill from the user root the kit's
242
- `jev-skill` command fills, `~/.agents/skills` — measured 2026-10-02: a run under `--ignore-user-config` listed the
243
- skills of that root. The run never inherits `TYPESAFE_API_KEY`: the wrappers unset every `*_API_KEY` before codex
244
- starts. codex exec has no network, so Jev requests are the orchestrator's own, made in the user's environment, and
245
- their answers are passed into a brief as text.
252
+ **The Jev skill in a delegated run.** A `codex-exec` or `codex-review` run loads the vendor's Jev skill from the
253
+ user root the kit's `jev-skill` command fills, `~/.agents/skills` (a run under `--ignore-user-config` listed its
254
+ skills, measured 2026-10-02; `codex-review` links that root into its fence home), and `TYPESAFE_API_KEY` passes the
255
+ wrappers as given while every other `*_API_KEY` is unset. On Linux with codex-cli 0.160.0 or newer, off a codex
256
+ version and profile digest recorded as refused and, for `codex-exec`, from a repository outside every temp root, the
257
+ run carries the permissions profile `jev`: it reaches `api.typesafe.ai` and `docs.typesafe.ai` and no other host, a review still
258
+ writes nothing, and the banner reads `jev=api.typesafe.ai,docs.typesafe.ai`; anywhere else the run keeps today's
259
+ flags with network off and the banner says `jev=unreachable (<reason>)`. Two residuals stand: codex's server-side
260
+ `web_search` reaches pages outside the profile, and the key now sits in every run's environment, the `web_search`
261
+ channel included. A codex release that stops recognizing a profile key costs one run per version and profile digest
262
+ (exit 72), while the state file can be written, before the recorded fallback takes over.
246
263
 
247
264
  ## How the main agent drives `codex` efficiently
248
265
 
@@ -263,11 +280,12 @@ See [`references/driving-codex.md`](references/driving-codex.md) for the full pl
263
280
  resume entrypoint re-establishes EVERY wrapper invariant (subscription-only, `--ignore-user-config`,
264
281
  the host posture's model/effort) and **restates the full posture via `-c`** — `codex exec resume` resets the
265
282
  sandbox/approval/network posture and rejects the `-s`/`--add-dir`/`-C` posture flags, so the wrapper
266
- sets `sandbox_mode=workspace-write` + `approval_policy=never` +
267
- `sandbox_workspace_write.network_access=false` explicitly. It reads the session id from the sidecar
268
- (`--resume-last`) or takes it as an argument.
269
- - **Network is OFF in exec.** New dependencies and any network step are installed by hand, then codex
270
- is re-dispatched.
283
+ sets `approval_policy=never` and the run's own posture explicitly — the profile `jev`'s four overrides,
284
+ or `sandbox_mode=workspace-write` + `sandbox_workspace_write.network_access=false` — chosen afresh on
285
+ each resume. It reads the session id from the sidecar (`--resume-last`) or takes it as an argument.
286
+ - **Network: two hosts at most in exec.** Under the profile `jev` a run reaches `api.typesafe.ai` and
287
+ `docs.typesafe.ai` only; under today's flags none. New dependencies and any other network step are
288
+ installed by hand, then codex is re-dispatched.
271
289
  - **`codex-review code` precomputes the diff.** The wrapper assembles the change set (repo map, status,
272
290
  staged + unstaged diff, untracked file contents) and feeds it in, so codex does not roam the
273
291
  filesystem rediscovering it; a clean tree exits 0 before a run is spent. Native `codex review` is
@@ -293,8 +311,10 @@ The wrappers work in any git repo where `codex` is installed and authenticated.
293
311
 
294
312
  ## Known limitations
295
313
 
296
- - **Network is OFF** in `codex-exec` (`sandbox_workspace_write.network_access=false`): codex cannot
297
- install dependencies or reach the network — do that by hand, then re-dispatch.
314
+ - **Network: two hosts at most** in `codex-exec` — the profile `jev` reaches `api.typesafe.ai` and
315
+ `docs.typesafe.ai` only, today's flags (`sandbox_workspace_write.network_access=false`) none: codex
316
+ cannot install dependencies — do that by hand, then re-dispatch. The profile runs only on Linux with
317
+ codex-cli 0.160.0 or newer; Windows and macOS keep today's flags until its round-trip is probed there.
298
318
  - **No live approvals** — `codex exec` has no TTY, so `approval_policy=never`; an action that would
299
319
  need escalation is reported, not approved interactively.
300
320
  - **`resume` resets the posture** — `codex exec resume` rejects `-s`/`--add-dir`/`-C` and forgets the
@@ -0,0 +1,385 @@
1
+ // codex-exec-jev.test.mjs — spec jev-every-run on codex-exec: the shared key rule, the posture the wrapper chooses
2
+ // (the jev permissions profile or today's flags), the loud failure, the banner, the directive and the probe
3
+ // passthrough. Standalone harness with its own fake codex and a fake uname.
4
+ import { describe, it, after } from 'node:test';
5
+ import assert from 'node:assert/strict';
6
+ import { mkdirSync, mkdtempSync, writeFileSync, readFileSync, existsSync, rmSync, cpSync, chmodSync, realpathSync, statSync } from 'node:fs';
7
+ import { tmpdir } from 'node:os';
8
+ import { join, dirname } from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+ import { spawnSync } from 'node:child_process';
11
+
12
+ const HERE = dirname(fileURLToPath(import.meta.url));
13
+ const EXEC = join(HERE, 'codex-exec.sh');
14
+ const HOSTS = 'api.typesafe.ai,docs.typesafe.ai';
15
+ // The repositories sit outside every temp root: under the package's node_modules, TMPDIR their sibling, never an ancestor.
16
+ const FIXTURES = join(HERE, '..', 'node_modules', '.aw-fixtures');
17
+ const TMP = join(FIXTURES, 'tmp');
18
+ mkdirSync(TMP, { recursive: true });
19
+ mkdirSync(join(FIXTURES, 'repos'), { recursive: true });
20
+ const ROOT = mkdtempSync(join(FIXTURES, 'repos', 'exec-jev-'));
21
+ after(() => rmSync(ROOT, { recursive: true, force: true }));
22
+ const REAL_UNAME = spawnSync('bash', ['-c', 'type -P uname'], { encoding: 'utf8' }).stdout.trim();
23
+
24
+ // Every spawn of the CLI appends one line: its kind and the state of each key ([value] or unset).
25
+ const FAKE_CODEX = [
26
+ '#!/usr/bin/env bash',
27
+ 'set -u',
28
+ 'st() { if [[ -n "${!1+x}" ]]; then printf "[%s]" "${!1}"; else printf unset; fi; }',
29
+ 'rec() { echo "$1 key=$(st TYPESAFE_API_KEY) openai=$(st OPENAI_API_KEY) codex=$(st CODEX_API_KEY) foo=$(st FOO_API_KEY) base=$(st OPENAI_BASE_URL)" >>"$CODEX_FAKE_LOG"; }',
30
+ 'case "${1:-}" in',
31
+ ' login) rec login; echo "Logged in using ChatGPT"; exit 0 ;;',
32
+ ' --version) rec version; echo "${CODEX_FAKE_VERSION-codex-cli 0.160.0}"; exit 0 ;;',
33
+ ' debug) rec catalog; cat <<EOF',
34
+ '{"models":[{"slug":"gpt-6.1-sol","priority":1,"visibility":"list","default_reasoning_level":"low","supported_reasoning_levels":[{"effort":"low"},{"effort":"medium"},{"effort":"high"}]}]}',
35
+ 'EOF',
36
+ ' exit 0 ;;',
37
+ 'esac',
38
+ 'rec run',
39
+ '{ for a in "$@"; do echo "$a"; done; } >"$CODEX_FAKE_ARGV"',
40
+ 'cat >"$CODEX_FAKE_STDIN"',
41
+ 'out=""; prev=""; for a in "$@"; do if [[ "$prev" == "-o" ]]; then out="$a"; fi; prev="$a"; done',
42
+ 'if [[ -n "$out" ]]; then echo "${CODEX_FAKE_FINAL:-FAKE_FINAL}" >"$out"; fi',
43
+ 'echo "{\\"type\\":\\"thread.started\\",\\"thread_id\\":\\"sess-jev\\"}"',
44
+ 'if [[ -n "${CODEX_FAKE_PRE:-}" ]]; then echo "$CODEX_FAKE_PRE"; fi',
45
+ 'echo "{\\"type\\":\\"turn.started\\"}"',
46
+ 'if [[ -n "${CODEX_FAKE_POST:-}" ]]; then echo "$CODEX_FAKE_POST"; fi',
47
+ 'echo "{\\"type\\":\\"turn.completed\\",\\"usage\\":{}}"',
48
+ 'exit "${CODEX_FAKE_EXIT:-0}"',
49
+ '',
50
+ ].join('\n');
51
+ const FAKE_UNAME = ['#!/usr/bin/env bash', 'if [[ $# -eq 0 || "$1" == "-s" ]]; then echo "${CODEX_FAKE_UNAME:-Linux}"; exit 0; fi',
52
+ `exec "${REAL_UNAME}" "$@"`, ''].join('\n');
53
+ const CONTRACT = '# dispatch\n\n```aw-dispatch-contract\n{"nonce":"n1","scope":"probe"}\n```\n';
54
+
55
+ const initRepo = (repo) => {
56
+ mkdirSync(repo, { recursive: true });
57
+ const git = (...args) => spawnSync('git', args, { cwd: repo, encoding: 'utf8' });
58
+ git('init', '-q');
59
+ git('config', 'user.email', 'probe@example.com');
60
+ git('config', 'user.name', 'probe');
61
+ writeFileSync(join(repo, 'AGENTS.md'), '# AGENTS\n\nHard Constraints: none (test fixture).\n');
62
+ git('add', '-A');
63
+ git('commit', '-qm', 'base');
64
+ writeFileSync(join(repo, 'task.md'), CONTRACT);
65
+ };
66
+ const TEMPLATE = join(ROOT, 'template');
67
+ mkdirSync(join(TEMPLATE, 'bin'), { recursive: true });
68
+ mkdirSync(join(TEMPLATE, 'home'));
69
+ writeFileSync(join(TEMPLATE, 'bin', 'codex'), FAKE_CODEX);
70
+ writeFileSync(join(TEMPLATE, 'bin', 'uname'), FAKE_UNAME);
71
+ initRepo(join(TEMPLATE, 'repo'));
72
+
73
+ let sequence = 0;
74
+ const sandbox = () => {
75
+ const dir = join(ROOT, `sb-${++sequence}`);
76
+ cpSync(TEMPLATE, dir, { recursive: true });
77
+ for (const name of ['codex', 'uname']) chmodSync(join(dir, 'bin', name), 0o755);
78
+ return { dir, bin: join(dir, 'bin'), home: join(dir, 'home'), repo: join(dir, 'repo'),
79
+ state: join(dir, 'home', '.local', 'state', 'agent-workflow', 'codex-jev-profile-refused') };
80
+ };
81
+ const drive = (sb, args, env = {}, cwd = sb.repo) => {
82
+ const cap = (name) => join(sb.dir, name);
83
+ for (const name of ['log', 'argv', 'stdin']) rmSync(cap(name), { force: true });
84
+ const r = spawnSync('bash', [EXEC, ...args], {
85
+ cwd, input: 'do the thing', encoding: 'utf8', timeout: 60000,
86
+ env: { PATH: `${sb.bin}:${process.env.PATH}`, HOME: sb.home, TMPDIR: TMP, CODEX_FAKE_LOG: cap('log'),
87
+ CODEX_FAKE_ARGV: cap('argv'), CODEX_FAKE_STDIN: cap('stdin'), ...env },
88
+ });
89
+ const read = (name) => (existsSync(cap(name)) ? readFileSync(cap(name), 'utf8') : '');
90
+ return { status: r.status, stdout: r.stdout, stderr: r.stderr, log: read('log'), argv: read('argv').split('\n').slice(0, -1),
91
+ prompt: read('stdin').replace(/\s+/g, ' ') };
92
+ };
93
+
94
+ // -c key=value pairs of the run's argv, and a reader for the TOML inline tables a profile override carries.
95
+ const overrides = (argv) => Object.fromEntries(argv.flatMap((arg, i) => (argv[i - 1] === '-c'
96
+ ? [[arg.slice(0, arg.indexOf('=')), arg.slice(arg.indexOf('=') + 1)]] : [])));
97
+ const inline = (text) => {
98
+ let at = 0;
99
+ const eat = (re) => {
100
+ const m = re.exec(text.slice(at));
101
+ assert.ok(m, `an inline table: ${text}`);
102
+ at += m[0].length;
103
+ return m;
104
+ };
105
+ const value = () => {
106
+ if (eat(/^\s*(\{?)/)[1] === '') return eat(/^"([^"]*)"|^([\w.:-]+)/).slice(1).find((part) => part !== undefined);
107
+ const table = {};
108
+ while (eat(/^\s*(\}?)/)[1] === '') {
109
+ const key = value();
110
+ eat(/^\s*=/);
111
+ table[key] = value();
112
+ eat(/^\s*,?/);
113
+ }
114
+ return table;
115
+ };
116
+ const parsed = value();
117
+ assert.equal(text.slice(at).trim(), '', `nothing after the inline table: ${text}`);
118
+ return parsed;
119
+ };
120
+ const EXEC_FS = { ':root': 'read', ':tmpdir': 'write', ':slash_tmp': 'write', ':workspace_roots': { '.': 'write' } };
121
+ const PROFILE_KEYS = ['default_permissions', 'permissions.jev.filesystem', 'permissions.jev.network.domains', 'permissions.jev.network.enabled'];
122
+ const profileKeys = (c) => Object.keys(c).filter((key) => key === 'default_permissions' || key.startsWith('permissions.')).sort();
123
+ const assertProfile = (r) => {
124
+ assert.equal(r.status, 0, r.stderr);
125
+ const c = overrides(r.argv);
126
+ assert.deepEqual(profileKeys(c), PROFILE_KEYS, r.argv.join(' '));
127
+ assert.equal(c.default_permissions.replace(/^"(.*)"$/, '$1'), 'jev');
128
+ assert.equal(c['permissions.jev.network.enabled'], 'true');
129
+ assert.deepEqual(inline(c['permissions.jev.network.domains']), { 'api.typesafe.ai': 'allow', 'docs.typesafe.ai': 'allow' });
130
+ assert.deepEqual(inline(c['permissions.jev.filesystem']), EXEC_FS);
131
+ assert.equal(r.argv.includes('--sandbox') || 'sandbox_mode' in c, false, 'neither --sandbox nor sandbox_mode beside the profile');
132
+ };
133
+ const assertFallback = (r) => {
134
+ assert.equal(r.status, 0, r.stderr);
135
+ const c = overrides(r.argv);
136
+ assert.deepEqual(profileKeys(c), [], r.argv.join(' '));
137
+ assert.ok(r.argv[r.argv.indexOf('--sandbox') + 1] === 'workspace-write' || c.sandbox_mode === 'workspace-write', r.argv.join(' '));
138
+ assert.equal(c['sandbox_workspace_write.network_access'], 'false');
139
+ };
140
+ const bannerOf = (r) => r.stderr.split('\n').find((line) => line.startsWith('exec posture: ')) ?? '';
141
+ const banner = (sandboxValue, jev, session = 'fresh') => 'exec posture: model=gpt-6.1-sol effort=high tier=standard '
142
+ + `sandbox=${sandboxValue} session=${session} jev=${jev} source=model:default,effort:default timeout=3600s`;
143
+ const unreachable = (reason) => banner('workspace-write', `unreachable (${reason})`);
144
+ const KEYS = { TYPESAFE_API_KEY: 'jev-key', OPENAI_API_KEY: 'sk-live', CODEX_API_KEY: 'ck', FOO_API_KEY: 'foo', OPENAI_BASE_URL: 'http://proxy' };
145
+ const PASSED = 'key=[jev-key] openai=unset codex=unset foo=unset base=unset';
146
+ const spawnsOf = (r) => r.log.split('\n').filter(Boolean).map((line) => [line.slice(0, line.indexOf(' ')), line.slice(line.indexOf(' ') + 1)]);
147
+ const runEnv = (r) => (spawnsOf(r).find(([kind]) => kind === 'run') ?? [])[1];
148
+ const item = (body) => JSON.stringify({ type: 'item.completed', item: body });
149
+ const P13_TEXT = 'Configured filesystem path `:slash_tmp` is not recognized by this version of Codex and will be ignored. Upgrade Codex if this path is required.';
150
+ const P13 = item({ id: 'item_0', type: 'error', message: P13_TEXT });
151
+ const P1 = item({ id: 'item_0', type: 'error', message: 'Permissions profile `jev` does not define any recognized filesystem entries for this version of Codex. Filesystem access will remain restricted.' });
152
+ const OTHER_SHAPE = item({ id: 'item_0', type: 'error', message: 'Unknown configuration key `permissions.jev.network.mode`.' });
153
+ const command = (output, exitCode, status) => item({ id: 'item_1', type: 'command_execution', command: 'bash -lc ls', aggregated_output: output, exit_code: exitCode, status });
154
+ const BWRAP = 'bwrap: execvp /usr/lib/node_modules/@openai/codex/vendor/x86_64-unknown-linux-musl/bin/codex: No such file or directory\n';
155
+ const receiptOf = (sb) => JSON.parse(readFileSync(join(sb.repo, '.git', 'agent-workflow-exec-receipt-5-codex-n1.json'), 'utf8'));
156
+ const RECEIPT_KEYS = ['schema', 'kind', 'state', 'backend', 'nonce', 'owner', 'contractDigest', 'wrapperVersion', 'posture', 'capS',
157
+ 'killGraceS', 'sessionId', 'exitStatus', 'outcome', 'reportDigest', 'reportLength', 'timestamp'];
158
+ const stateLines = (sb) => (existsSync(sb.state) ? readFileSync(sb.state, 'utf8').split('\n').filter(Boolean) : []);
159
+
160
+ describe('the key rule on codex-exec — spec:jev-every-run/S1', () => {
161
+ it('a fresh run hands codex only TYPESAFE_API_KEY of the keys, and the login, version and catalog spawns see the same env', () => {
162
+ const r = drive(sandbox(), ['-'], KEYS);
163
+ assert.equal(r.status, 0, r.stderr);
164
+ assert.deepEqual([...new Set(spawnsOf(r).map(([kind]) => kind))].sort(), ['catalog', 'login', 'run', 'version'], r.log);
165
+ for (const [kind, env] of spawnsOf(r)) assert.equal(env, PASSED, `${kind} spawn`);
166
+ });
167
+ });
168
+
169
+ describe('a resumed run — spec:jev-every-run/S2', () => {
170
+ it('keeps the key rule and carries the profile overrides with no sandbox_mode', () => {
171
+ const r = drive(sandbox(), ['--resume', 'sess-1', '-'], KEYS);
172
+ assertProfile(r);
173
+ assert.deepEqual(r.argv.slice(0, 3), ['exec', 'resume', 'sess-1']);
174
+ for (const [kind, env] of spawnsOf(r)) assert.equal(env, PASSED, `${kind} spawn`);
175
+ });
176
+ });
177
+
178
+ describe('an empty key and an unset key — spec:jev-every-run/S5', () => {
179
+ it('an empty TYPESAFE_API_KEY reaches codex empty, an unset one stays unset, and the argv is the same with the key set', () => {
180
+ const runs = ['jev-key', '', undefined].map((value) => drive(sandbox(), ['-'], { TYPESAFE_API_KEY: value }));
181
+ assert.deepEqual(runs.map((r) => runEnv(r)?.split(' ')[0]), ['key=[jev-key]', 'key=[]', 'key=unset']);
182
+ const normalized = runs.map((r) => r.argv.map((arg, i) => (r.argv[i - 1] === '-o' ? '<out>' : arg)).join(' '));
183
+ assert.equal(normalized[1], normalized[0]);
184
+ assert.equal(normalized[2], normalized[0]);
185
+ assertProfile(runs[0]);
186
+ });
187
+ });
188
+
189
+ describe('the exec profile on Linux from codex-cli 0.160.0 — spec:jev-every-run/S7', () => {
190
+ for (const version of ['0.160.0', '0.161.0', '1.0.0']) {
191
+ it(`codex-cli ${version}: the exec profile exactly, neither --sandbox nor sandbox_mode`, () => {
192
+ assertProfile(drive(sandbox(), ['-'], { CODEX_FAKE_VERSION: `codex-cli ${version}` }));
193
+ });
194
+ }
195
+ });
196
+
197
+ describe('the fallback by OS and by temp root — spec:jev-every-run/S9', () => {
198
+ it('a uname other than Linux runs today\'s flags with the key passed and names the reason', () => {
199
+ const r = drive(sandbox(), ['-'], { ...KEYS, CODEX_FAKE_UNAME: 'Darwin' });
200
+ assertFallback(r);
201
+ assert.equal(runEnv(r), PASSED);
202
+ assert.equal(bannerOf(r), unreachable('not linux (Darwin)'));
203
+ });
204
+ it('a repository root under the realpath of TMPDIR falls back; a sibling sharing its name prefix does not', () => {
205
+ const sb = sandbox();
206
+ const root = join(sb.dir, 'pfx');
207
+ cpSync(sb.repo, join(root, 'repo'), { recursive: true });
208
+ const under = drive(sb, ['-'], { ...KEYS, TMPDIR: root }, join(root, 'repo'));
209
+ assertFallback(under);
210
+ assert.equal(runEnv(under), PASSED);
211
+ assert.equal(bannerOf(under), unreachable(`repository under ${realpathSync(root)}`));
212
+ cpSync(sb.repo, join(sb.dir, 'pfx-repo'), { recursive: true });
213
+ assertProfile(drive(sb, ['-'], { TMPDIR: root }, join(sb.dir, 'pfx-repo')));
214
+ });
215
+ it('a worktree whose git common dir is under TMPDIR falls back', () => {
216
+ const sb = sandbox();
217
+ const root = join(sb.dir, 'tmproot');
218
+ cpSync(sb.repo, join(root, 'main'), { recursive: true });
219
+ spawnSync('git', ['worktree', 'add', '-q', join(sb.dir, 'wt')], { cwd: join(root, 'main'), encoding: 'utf8' });
220
+ writeFileSync(join(sb.dir, 'wt', 'task.md'), CONTRACT);
221
+ const r = drive(sb, ['-'], { TMPDIR: root }, join(sb.dir, 'wt'));
222
+ assertFallback(r);
223
+ assert.equal(bannerOf(r), unreachable(`repository under ${realpathSync(root)}`));
224
+ // A git older than 2.31 echoes an unknown --path-format=… as its own line before the answer.
225
+ const realGit = spawnSync('bash', ['-c', 'command -v git'], { encoding: 'utf8' }).stdout.trim();
226
+ writeFileSync(join(sb.bin, 'git'), ['#!/usr/bin/env bash', 'args=()', 'for a in "$@"; do',
227
+ ' if [[ "$a" == --path-format=* ]]; then echo "$a"; else args+=("$a"); fi', 'done', `exec ${realGit} "\${args[@]}"`, ''].join('\n'));
228
+ chmodSync(join(sb.bin, 'git'), 0o755);
229
+ const old = drive(sb, ['-'], { TMPDIR: root }, join(sb.dir, 'wt'));
230
+ assertFallback(old);
231
+ assert.equal(bannerOf(old), unreachable(`repository under ${realpathSync(root)}`));
232
+ });
233
+ it('a repository under /tmp falls back naming /tmp', () => {
234
+ const repo = mkdtempSync(join(tmpdir(), 'exec-jev-tmp-'));
235
+ try {
236
+ cpSync(join(TEMPLATE, 'repo'), repo, { recursive: true });
237
+ const underTmp = realpathSync(repo).startsWith(`${realpathSync('/tmp')}/`);
238
+ const r = drive(sandbox(), ['-'], { TMPDIR: underTmp ? TMP : tmpdir() }, repo);
239
+ assertFallback(r);
240
+ assert.equal(bannerOf(r), unreachable(`repository under ${underTmp ? '/tmp' : realpathSync(tmpdir())}`));
241
+ } finally {
242
+ rmSync(repo, { recursive: true, force: true });
243
+ }
244
+ });
245
+ });
246
+
247
+ describe('the fallback by version and by the refused record — spec:jev-every-run/S10', () => {
248
+ const cases = [['codex-cli 0.159.0', 'codex 0.159.0 below 0.160.0'], ['codex-cli 0.160.0-alpha.1', 'codex 0.160.0-alpha.1 below 0.160.0'],
249
+ ['codex-cli', 'codex version unreadable']];
250
+ for (const [line, reason] of cases) {
251
+ it(`a version line '${line}' runs today's flags with the key passed: ${reason}`, () => {
252
+ const r = drive(sandbox(), ['-'], { ...KEYS, CODEX_FAKE_VERSION: line });
253
+ assertFallback(r);
254
+ assert.equal(runEnv(r), PASSED);
255
+ assert.equal(bannerOf(r), unreachable(reason));
256
+ });
257
+ }
258
+ it('a listed version with this run\'s digest falls back naming the file; another digest or another version runs the profile', () => {
259
+ const sb = sandbox();
260
+ assert.equal(drive(sb, ['-'], { CODEX_FAKE_PRE: P13 }).status, 72);
261
+ const [entry] = stateLines(sb);
262
+ assert.match(entry ?? '', /^0\.160\.0 [0-9a-f]{64}$/);
263
+ const refused = drive(sb, ['-'], KEYS);
264
+ assertFallback(refused);
265
+ assert.equal(runEnv(refused), PASSED);
266
+ assert.ok(bannerOf(refused).includes('jev=unreachable (profile not recognized by codex 0.160.0'), bannerOf(refused));
267
+ assert.ok(bannerOf(refused).includes(sb.state), 'the banner reason names the file');
268
+ for (const other of [`0.160.0 ${'a'.repeat(64)}`, `0.161.0 ${entry.split(' ')[1]}`]) {
269
+ writeFileSync(sb.state, `${other}\n`);
270
+ assertProfile(drive(sb, ['-']));
271
+ }
272
+ });
273
+ });
274
+
275
+ describe('the profile error item before the turn — spec:jev-every-run/S11', () => {
276
+ for (const [label, shape] of [['the P13 shape', P13], ['the P1 shape', P1]]) {
277
+ it(`${label}, printed twice before turn.started: exit 72, one entry appended, a transport-failure receipt`, () => {
278
+ const sb = sandbox();
279
+ const r = drive(sb, ['--nonce', 'n1', 'task.md'], { CODEX_FAKE_PRE: `${shape}\n${shape}` });
280
+ assert.equal(r.status, 72, r.stderr);
281
+ assert.ok(r.stderr.includes(JSON.parse(shape).item.message), 'the matched message is printed');
282
+ assert.doesNotMatch(r.stderr, /update codex/i);
283
+ assert.doesNotMatch(r.stdout, /FAKE_FINAL/, 'no final message is printed');
284
+ assert.equal(stateLines(sb).length, 1);
285
+ assert.match(stateLines(sb)[0], /^0\.160\.0 [0-9a-f]{64}$/);
286
+ assert.equal(statSync(dirname(sb.state)).mode & 0o777, 0o700);
287
+ assert.equal(receiptOf(sb).outcome, 'transport-failure');
288
+ });
289
+ }
290
+ for (const [label, env] of [['after turn.started', { CODEX_FAKE_POST: P13 }], ['in another shape', { CODEX_FAKE_PRE: OTHER_SHAPE }]]) {
291
+ it(`${label}: the run ends as today and nothing is appended`, () => {
292
+ const sb = sandbox();
293
+ const r = drive(sb, ['-'], env);
294
+ assertProfile(r);
295
+ assert.equal(existsSync(sb.state), false);
296
+ });
297
+ }
298
+ it('a failed write warns once naming the path and still exits 72 with its receipt', () => {
299
+ const sb = sandbox();
300
+ mkdirSync(dirname(dirname(sb.state)), { recursive: true });
301
+ writeFileSync(dirname(sb.state), 'a file where the state directory belongs\n');
302
+ const r = drive(sb, ['--nonce', 'n1', 'task.md'], { CODEX_FAKE_PRE: P13 });
303
+ assert.equal(r.status, 72, r.stderr);
304
+ assert.equal(r.stderr.split('\n').filter((line) => line.includes(sb.state)).length, 1, r.stderr);
305
+ assert.equal(receiptOf(sb).outcome, 'transport-failure');
306
+ });
307
+ });
308
+
309
+ describe('bwrap: execvp in a failed command — spec:jev-every-run/S12', () => {
310
+ it('exits 72 without touching the state file', () => {
311
+ const sb = sandbox();
312
+ const r = drive(sb, ['--nonce', 'n1', 'task.md'], { CODEX_FAKE_POST: command(BWRAP, 1, 'failed') });
313
+ assert.equal(r.status, 72, r.stderr);
314
+ assert.ok(r.stderr.includes('bwrap: execvp'), r.stderr);
315
+ assert.equal(existsSync(sb.state), false);
316
+ assert.equal(receiptOf(sb).outcome, 'transport-failure');
317
+ });
318
+ });
319
+
320
+ describe('the same strings where they never fire — spec:jev-every-run/S13', () => {
321
+ it('inside a successful command\'s output and in the final message: exit 0, a success receipt, no state file', () => {
322
+ const sb = sandbox();
323
+ const r = drive(sb, ['--nonce', 'n1', 'task.md'], { CODEX_FAKE_POST: command(`${P13_TEXT}\n${BWRAP}`, 0, 'completed'),
324
+ CODEX_FAKE_FINAL: `${P13_TEXT} ${BWRAP}` });
325
+ assertProfile(r);
326
+ assert.ok(r.stdout.includes(P13_TEXT));
327
+ assert.equal(receiptOf(sb).outcome, 'success');
328
+ assert.equal(existsSync(sb.state), false);
329
+ });
330
+ });
331
+
332
+ describe('the exec banner and the receipt — spec:jev-every-run/S14', () => {
333
+ it('a profile run reads sandbox=jev-profile and the two hosts before source=, the receipt keys unchanged', () => {
334
+ const sb = sandbox();
335
+ const r = drive(sb, ['--nonce', 'n1', 'task.md']);
336
+ assertProfile(r);
337
+ assert.equal(bannerOf(r), banner('jev-profile', HOSTS));
338
+ assert.deepEqual(Object.keys(receiptOf(sb)), RECEIPT_KEYS);
339
+ assert.deepEqual(Object.keys(receiptOf(sb).posture), ['model', 'effort', 'tier']);
340
+ });
341
+ it('a fallback run reads sandbox=workspace-write and jev=unreachable, timeout= last', () => {
342
+ const r = drive(sandbox(), ['--resume', 'sess-1', '-'], { CODEX_FAKE_VERSION: 'codex-cli 0.159.0' });
343
+ assert.equal(bannerOf(r), banner('workspace-write', 'unreachable (codex 0.159.0 below 0.160.0)', 'resume:sess-1'));
344
+ });
345
+ });
346
+
347
+ describe('the execution directive and the resume reminder — spec:jev-every-run/S17', () => {
348
+ const HOST_LINE = 'the hosts api.typesafe.ai and docs.typesafe.ai are reachable; network access to any other host is an escalation: STOP and report';
349
+ it('a fresh profile run names the two hosts and the other-host escalation; a fallback run keeps network access whole', () => {
350
+ const profile = drive(sandbox(), ['-']);
351
+ assertProfile(profile);
352
+ assert.ok(profile.prompt.includes(HOST_LINE), profile.prompt);
353
+ const fallback = drive(sandbox(), ['-'], { CODEX_FAKE_UNAME: 'Darwin' });
354
+ assert.ok(fallback.prompt.includes('escalation (network access, writes outside the repo'), fallback.prompt);
355
+ assert.doesNotMatch(fallback.prompt, /typesafe/);
356
+ });
357
+ it('a resume states its current posture: the hosts on a profile run, network access on the next run of the same session that falls back', () => {
358
+ const sb = sandbox();
359
+ const profile = drive(sb, ['--resume', 'sess-1', '-']);
360
+ assertProfile(profile);
361
+ assert.ok(profile.prompt.includes(HOST_LINE), profile.prompt);
362
+ const fallback = drive(sb, ['--resume', 'sess-1', '-'], { CODEX_FAKE_UNAME: 'Darwin' });
363
+ assertFallback(fallback);
364
+ assert.match(fallback.prompt, /network access/);
365
+ assert.doesNotMatch(fallback.prompt, /typesafe/);
366
+ });
367
+ });
368
+
369
+ describe('the probe passthrough under the profile — spec:jev-every-run/S18', () => {
370
+ for (const flag of [['--add-dir', '/x'], ['-C', '/x'], ['--cd', '/x'], ['--enable', 'foo'], ['--disable', 'foo']]) {
371
+ it(`CODEX_PROBE=1 with ${flag[0]}: refused pre-spend under the profile, admitted under the fallback`, () => {
372
+ const refused = drive(sandbox(), ['-', '--', ...flag], { CODEX_PROBE: '1' });
373
+ assert.equal(refused.status, 2, refused.stderr);
374
+ assert.equal(spawnsOf(refused).some(([kind]) => kind === 'run'), false, 'no run is spent');
375
+ const admitted = drive(sandbox(), ['-', '--', ...flag], { CODEX_PROBE: '1', CODEX_FAKE_UNAME: 'Darwin' });
376
+ assertFallback(admitted);
377
+ assert.ok(admitted.argv.includes(flag[0]), admitted.argv.join(' '));
378
+ });
379
+ }
380
+ it('exactly these five: --ignore-rules stays admitted under the profile', () => {
381
+ const r = drive(sandbox(), ['-', '--', '--ignore-rules'], { CODEX_PROBE: '1' });
382
+ assertProfile(r);
383
+ assert.ok(r.argv.includes('--ignore-rules'));
384
+ });
385
+ });