@mrciphersmith/keryx 0.3.41 → 0.3.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -487,7 +487,7 @@ What is in it today:
487
487
  bounded parallel scheduling, and an offline fleet report over a recorded event
488
488
  log (`keryx agents monitor <events-file>`).
489
489
  - **Vendor CLIs as child agents — off by default.** keryx can hand a bounded,
490
- **read-only** task (or, for `claude-cli` only, a reviewed write — see below)
490
+ **read-only** task (or, for `claude-cli` and `codex-cli`, a reviewed write — see below)
491
491
  to a coding CLI you already have installed (`codex exec`,
492
492
  `claude -p`, or Google's Antigravity CLI, `agy -p --output-format
493
493
  stream-json`) and host it as a child of the same harness: a disposable git
@@ -509,16 +509,20 @@ What is in it today:
509
509
  - **A write from an external agent you review before it lands.**
510
510
  `keryx agents external run claude-cli --task "…" --write` runs `claude` in a
511
511
  throwaway worktree with only `Read Grep Glob Edit Write` (no shell, no network,
512
- no MCP). Its diff is captured, secret-redacted, hashed and stored as a pending
512
+ no MCP); `codex-cli --write` runs `codex` in the same kind of worktree under an
513
+ OS sandbox (as measured: writes outside the worktree fail, a DNS lookup fails, `.git` read-only)
514
+ that still leaves it a shell able to read files your account can read, so review
515
+ its output too. Its diff is captured, secret-redacted, hashed and stored as a pending
513
516
  review — nothing reaches your checkout. `keryx agents external review <run-id>`
514
517
  shows it; `apply <run-id>` needs a real terminal, shows the diff and asks you to
515
518
  type the first 12 hex digits of the patch hash, then creates a NEW local branch
516
519
  `external/<run-id>` with one commit. Your current branch and working tree are
517
520
  never touched, nothing is pushed and no pull request is opened;
518
521
  `discard <run-id>` drops it. There is no flag that skips the confirmation. Only
519
- `claude-cli` can write (`codex-cli` and `antigravity-cli` refuse `--write`), the
520
- review is a human reading the diff (a model review is not built), and a full run
521
- through the installed CLI is not recorded yet. In the TUI: `/external-diff` and a
522
+ `claude-cli` and `codex-cli` can write (`antigravity-cli` refuses `--write`), the
523
+ review is a human reading the diff (a model review is not built), and neither
524
+ has a long live history (`codex` was verified in a scratch repository on
525
+ `codex` 0.159.2; versions outside 0.159.2 up to 0.160.0 are refused). In the TUI: `/external-diff` and a
522
526
  sidebar row while a diff waits. See the [external agent write
523
527
  guide](docs/docs/guides/external-agent-write.md).
524
528
  - **Completion you can audit.** The completion gate blocks on missing evidence: a
@@ -948,7 +952,7 @@ untouched.
948
952
  | No bundled embedding runtime | No semantic ranking in memory search | Lexical memory search remains fully available |
949
953
  | ripgrep is external | `keryx ctx rg` needs `rg` on `PATH` | Install ripgrep, or let the agent read files directly |
950
954
  | Model commands need a credential | Four of the five commands above exit non-zero without one; `wiki enrich` exits `0` and marks the affected pages skipped | Everything else runs deterministically offline |
951
- | External agents are read-only, except `claude-cli` write mode, which has no full live run recorded | A delegated CLI can read and search; only `claude-cli` can write, and only as a stored diff a human reviews (`keryx agents external review\|apply\|discard`) that lands as a new local branch — never on your checkout, never pushed. `codex-cli` and `antigravity-cli` refuse `--write` ("write mode is claude-only in this release"). A model review is not built. The parent gets the child's result and nothing before it — supervision of a running external child is not implemented. `claude` and `agy` read-only runs are live-verified; a write run through the installed CLI is not yet | Use keryx's own child agents for work that must mutate the tree, or the [external agent write guide](docs/docs/guides/external-agent-write.md) for a reviewed `claude-cli` diff |
955
+ | External agents are read-only, except `claude-cli` and `codex-cli` write mode, which have no long live history | A delegated CLI can read and search; only `claude-cli` and `codex-cli` can write, and only as a stored diff a human reviews (`keryx agents external review\|apply\|discard`) that lands as a new local branch — never on your checkout, never pushed. `claude` is confined by a tool allow-list (no shell, no network); `codex` by an OS sandbox that still leaves it a shell that can read files your user can read (it cannot send them, but they can show up in the run output), and is refused when older than 0.159.0 — live-verified in a scratch repository on `codex` 0.159.0. `antigravity-cli` refuses `--write`. A model review is not built. The parent gets the child's result and nothing before it — supervision of a running external child is not implemented. `claude` and `agy` read-only runs are live-verified | Use keryx's own child agents for work that must mutate the tree, or the [external agent write guide](docs/docs/guides/external-agent-write.md) for a reviewed `claude-cli` or `codex-cli` diff |
952
956
  | A dispatched `flow-next` task is "done" by checks, not review | `task done` means normal end + a commit on the trigger branch + `keryx health gate` passing in the worktree; nobody has read the diff | Review and merge the `trigger/<flow>-<task>` branch yourself |
953
957
  | `trust` dispatch needs Linux + a working bubblewrap | The hardened unattended sandbox (network off, home hidden, allow-listed env) is bwrap-only; without it — or on macOS — a `trust` dispatch refuses before starting | Use `permissionMode: "ask"` (read-only), or run triggers on a Linux host with bwrap |
954
958
  | `dispatch.network: true` is the host's full network | The agent's commands then reach the internet and every host loopback service; the model call never needs it (it is made outside the sandbox) | Leave `network` off; review the `trigger/*` branch before installing or building it |
package/dist/cli.js CHANGED
@@ -14119,7 +14119,7 @@ var package_default;
14119
14119
  var init_package = __esm(() => {
14120
14120
  package_default = {
14121
14121
  name: "@mrciphersmith/keryx",
14122
- version: "0.3.41",
14122
+ version: "0.3.43",
14123
14123
  description: "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
14124
14124
  private: false,
14125
14125
  publishConfig: {
@@ -51368,13 +51368,15 @@ permission bridge.
51368
51368
  A line-stream agent (\`claude-cli\`, \`codex-cli\`) is a DIFFERENT thing:
51369
51369
  \`run\` refuses it outright ("speaks the one-way line stream. Delegate to it
51370
51370
  from \`keryx shell\` with /delegate"). There is no ACP permission bridge and
51371
- no patch capture for it \u2014 this release implements \`worktree-write\` for the
51372
- ACP transport only; a dispatch that asks for \`worktree-write\` on a
51373
- line-stream agent is REFUSED fail-closed with a distinguishable
51374
- \`not-implemented\` reason everywhere keryx dispatches one (the internal
51375
- \`runtime\` block, and \`keryx shell\`'s /delegate) \u2014 never silently accepted
51376
- and its writes discarded. It stays \`read-only\`, contained by the same
51377
- disposable worktree as every external run.
51371
+ no patch capture for it in those paths \u2014 \`worktree-write\` is implemented for
51372
+ the ACP transport, and for \`claude-cli\` and \`codex-cli\` only through the
51373
+ operator's own \`keryx agents external run <id> --write\`, which captures the
51374
+ diff for human review. A dispatch that asks for \`worktree-write\` on a
51375
+ line-stream agent anywhere else (the internal \`runtime\` block, and
51376
+ \`keryx shell\`'s /delegate) is REFUSED fail-closed with a distinguishable
51377
+ \`not-implemented\` reason \u2014 never silently accepted and its writes
51378
+ discarded. There it stays \`read-only\`, contained by the same disposable
51379
+ worktree as every external run.
51378
51380
 
51379
51381
  See \`docs/docs/guides/acp-client.md\` for the full ACP contract, including the
51380
51382
  output size bounds (a stderr-only flood and an output flood both end the run
@@ -85139,14 +85141,14 @@ var init_registry3 = __esm(() => {
85139
85141
  label: "Codex",
85140
85142
  binary: "codex",
85141
85143
  detect: ["--version"],
85142
- versionPattern: "codex-cli (\\d+\\.\\d+\\.\\d+)",
85144
+ versionPattern: "codex-cli (\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.]+)?)",
85143
85145
  knownGoodRange: { min: "0.147.0" },
85144
85146
  sandboxModes: ["read-only", "worktree-write"],
85145
85147
  streamingInput: false,
85146
85148
  resumable: true,
85147
85149
  reportsCost: false,
85148
85150
  budgetFlag: false,
85149
- notes: "Resume requires NOT passing `--ephemeral`: an ephemeral thread fails resume with " + "`no rollout found for thread id \u2026 (code -32600)`. `codex exec resume` also takes a " + "narrower flag set than `codex exec` \u2014 no `-s`, no `-C` \u2014 so its sandbox level cannot " + "be re-asserted and it must be spawned with cwd already set to the worktree."
85151
+ notes: "Resume requires NOT passing `--ephemeral`: an ephemeral thread fails resume with " + "`no rollout found for thread id \u2026 (code -32600)`. `codex exec resume` also takes a " + "narrower flag set than `codex exec` \u2014 no `-s`, no `-C` \u2014 so a read-only resume inherits " + "its sandbox, a write run re-asserts its confinement as `-c` overrides, and it must be " + "spawned with cwd already set to the worktree. Write mode needs codex 0.159.2 up to, but not including, 0.160.0."
85150
85152
  },
85151
85153
  {
85152
85154
  id: "claude-cli",
@@ -85196,10 +85198,27 @@ var init_registry3 = __esm(() => {
85196
85198
  });
85197
85199
 
85198
85200
  // src/harness/external/dispatch.ts
85201
+ function codecWriteVersionRefusal(entry, version) {
85202
+ const verified = CODEC_WRITE_VERIFIED_VERSIONS[entry.id];
85203
+ if (verified === undefined)
85204
+ return;
85205
+ const range = `\`${entry.binary}\` ${verified.min} up to, but not including, ${verified.before}`;
85206
+ const prefix = `${entry.label} write mode needs ${range}, the versions whose sandbox confinement was measured; `;
85207
+ if (version === undefined)
85208
+ return `${prefix}the installed version could not be read`;
85209
+ if (!/^\d+\.\d+\.\d+$/.test(version))
85210
+ return `${prefix}found ${version}, a pre-release`;
85211
+ if (compareVersions(version, verified.min) < 0)
85212
+ return `${prefix}found ${version}, which is older`;
85213
+ if (compareVersions(version, verified.before) >= 0) {
85214
+ return `${prefix}found ${version}, which is newer and not yet verified: update keryx, or use \`--task\` without \`--write\``;
85215
+ }
85216
+ return;
85217
+ }
85199
85218
  function implementedSandboxModesFor(entry, options = {}) {
85200
85219
  if (transportOf(entry) === "acp")
85201
85220
  return IMPLEMENTED_ACP_SANDBOX_MODES;
85202
- return entry.id === CODEC_WRITE_AGENT_ID && options.ownsWriteCapture === true ? IMPLEMENTED_ACP_SANDBOX_MODES : IMPLEMENTED_SANDBOX_MODES;
85221
+ return CODEC_WRITE_AGENT_IDS.includes(entry.id) && options.ownsWriteCapture === true ? IMPLEMENTED_ACP_SANDBOX_MODES : IMPLEMENTED_SANDBOX_MODES;
85203
85222
  }
85204
85223
  function validateRuntimeBlock(runtime, allowedActions, options = {}) {
85205
85224
  if (runtime === undefined || runtime.kind === "keryx") {
@@ -85241,7 +85260,7 @@ function validateRuntimeBlock(runtime, allowedActions, options = {}) {
85241
85260
  return {
85242
85261
  ok: false,
85243
85262
  code: "not-implemented",
85244
- reason: `sandbox "${sandbox}" is not implemented in this release; only ${implemented.join(", ")} is available` + (sandbox === "worktree-write" && transportOf(entry) !== "acp" ? `; write mode is claude-only in this release (${CODEC_WRITE_AGENT_ID})` : "")
85263
+ reason: `sandbox "${sandbox}" is not implemented in this release; only ${implemented.join(", ")} is available` + (sandbox === "worktree-write" && transportOf(entry) !== "acp" ? `; write mode is available for ${CODEC_WRITE_AGENT_IDS.join(", ")} only` + (entry.id === "antigravity-cli" ? ": antigravity-cli's edit tool writes outside the worktree" : "") : "")
85245
85264
  };
85246
85265
  }
85247
85266
  if (sandbox === "read-only") {
@@ -85267,12 +85286,16 @@ function readRuntimeBlock(dispatch) {
85267
85286
  return;
85268
85287
  return raw;
85269
85288
  }
85270
- var READ_ONLY_FORBIDDEN_ACTIONS, IMPLEMENTED_SANDBOX_MODES, IMPLEMENTED_ACP_SANDBOX_MODES, CODEC_WRITE_AGENT_ID = "claude-cli";
85289
+ var READ_ONLY_FORBIDDEN_ACTIONS, IMPLEMENTED_SANDBOX_MODES, IMPLEMENTED_ACP_SANDBOX_MODES, CODEC_WRITE_AGENT_IDS, CODEC_WRITE_VERIFIED_VERSIONS;
85271
85290
  var init_dispatch = __esm(() => {
85272
85291
  init_registry3();
85273
85292
  READ_ONLY_FORBIDDEN_ACTIONS = ["write", "network", "spawn-subagent"];
85274
85293
  IMPLEMENTED_SANDBOX_MODES = ["read-only"];
85275
85294
  IMPLEMENTED_ACP_SANDBOX_MODES = ["read-only", "worktree-write"];
85295
+ CODEC_WRITE_AGENT_IDS = ["claude-cli", "codex-cli"];
85296
+ CODEC_WRITE_VERIFIED_VERSIONS = {
85297
+ "codex-cli": { min: "0.159.2", before: "0.160.0" }
85298
+ };
85276
85299
  });
85277
85300
 
85278
85301
  // src/harness/external/env-deny.ts
@@ -85691,6 +85714,127 @@ var init_contracts = __esm(() => {
85691
85714
  ];
85692
85715
  });
85693
85716
 
85717
+ // src/harness/external/strict-schema.ts
85718
+ function isSchema(value) {
85719
+ return typeof value === "object" && value !== null && !Array.isArray(value);
85720
+ }
85721
+ function mapSchemas(value) {
85722
+ const out = {};
85723
+ if (!isSchema(value))
85724
+ return out;
85725
+ for (const [key, child] of Object.entries(value))
85726
+ out[key] = toStrictNode(child);
85727
+ return out;
85728
+ }
85729
+ function makeNullable(schema) {
85730
+ const type = schema.type;
85731
+ if (typeof type === "string") {
85732
+ if (type === "null")
85733
+ return schema;
85734
+ const out = { ...schema, type: [type, "null"] };
85735
+ if (Array.isArray(schema.enum) && !schema.enum.includes(null))
85736
+ out.enum = [...schema.enum, null];
85737
+ return out;
85738
+ }
85739
+ if (Array.isArray(type)) {
85740
+ if (type.includes("null")) {
85741
+ const out = { ...schema };
85742
+ if (Array.isArray(schema.enum) && !schema.enum.includes(null))
85743
+ out.enum = [...schema.enum, null];
85744
+ return out;
85745
+ }
85746
+ const out = { ...schema, type: [...type, "null"] };
85747
+ if (Array.isArray(schema.enum) && !schema.enum.includes(null))
85748
+ out.enum = [...schema.enum, null];
85749
+ return out;
85750
+ }
85751
+ if (Array.isArray(schema.enum) && schema.anyOf === undefined) {
85752
+ return schema.enum.includes(null) ? schema : { ...schema, enum: [...schema.enum, null] };
85753
+ }
85754
+ if (Array.isArray(schema.anyOf)) {
85755
+ return { ...schema, anyOf: [...schema.anyOf, NULL_SCHEMA] };
85756
+ }
85757
+ return { anyOf: [schema, NULL_SCHEMA] };
85758
+ }
85759
+ function toStrictNode(node) {
85760
+ if (!isSchema(node))
85761
+ return {};
85762
+ const out = {};
85763
+ if (node.type !== undefined)
85764
+ out.type = node.type;
85765
+ if (Array.isArray(node.enum))
85766
+ out.enum = [...node.enum];
85767
+ else if ("const" in node)
85768
+ out.enum = [node.const];
85769
+ if (typeof node.$ref === "string")
85770
+ out.$ref = node.$ref;
85771
+ const union = node.anyOf ?? node.oneOf;
85772
+ if (Array.isArray(union))
85773
+ out.anyOf = union.map(toStrictNode);
85774
+ if (isSchema(node.items))
85775
+ out.items = toStrictNode(node.items);
85776
+ if (isSchema(node.$defs))
85777
+ out.$defs = mapSchemas(node.$defs);
85778
+ const isObject = node.type === "object" || isSchema(node.properties);
85779
+ if (isObject) {
85780
+ const required = new Set(Array.isArray(node.required) ? node.required : []);
85781
+ const properties = {};
85782
+ for (const [key, child] of Object.entries(isSchema(node.properties) ? node.properties : {})) {
85783
+ const strict = toStrictNode(child);
85784
+ properties[key] = required.has(key) ? strict : makeNullable(strict);
85785
+ }
85786
+ out.type ??= "object";
85787
+ out.properties = properties;
85788
+ out.required = Object.keys(properties);
85789
+ out.additionalProperties = false;
85790
+ }
85791
+ return out;
85792
+ }
85793
+ function toStrictOutputSchema(bundled) {
85794
+ return toStrictNode(bundled);
85795
+ }
85796
+ function resolveRef3(node, root) {
85797
+ let current = node;
85798
+ for (let hops = 0;typeof current.$ref === "string" && hops < 16; hops += 1) {
85799
+ const ref = current.$ref;
85800
+ if (!ref.startsWith("#/"))
85801
+ return current;
85802
+ let target = root;
85803
+ for (const part of ref.slice(2).split("/")) {
85804
+ target = isSchema(target) ? target[decodeURIComponent(part)] : undefined;
85805
+ }
85806
+ if (!isSchema(target))
85807
+ return current;
85808
+ current = target;
85809
+ }
85810
+ return current;
85811
+ }
85812
+ function dropNulls(value, node, root) {
85813
+ if (!isSchema(node))
85814
+ return value;
85815
+ const schema = resolveRef3(node, root);
85816
+ if (Array.isArray(value)) {
85817
+ return isSchema(schema.items) ? value.map((item) => dropNulls(item, schema.items, root)) : value;
85818
+ }
85819
+ if (!isSchema(value) || !isSchema(schema.properties))
85820
+ return value;
85821
+ const required = new Set(Array.isArray(schema.required) ? schema.required : []);
85822
+ const out = {};
85823
+ for (const [key, child] of Object.entries(value)) {
85824
+ if (child === null && !required.has(key) && key in schema.properties)
85825
+ continue;
85826
+ out[key] = dropNulls(child, schema.properties[key], root);
85827
+ }
85828
+ return out;
85829
+ }
85830
+ function dropOptionalNulls(value, bundled) {
85831
+ return isSchema(bundled) ? dropNulls(value, bundled, bundled) : value;
85832
+ }
85833
+ var NULL_SCHEMA;
85834
+ var init_strict_schema = __esm(() => {
85835
+ NULL_SCHEMA = { type: "null" };
85836
+ });
85837
+
85694
85838
  // src/harness/external/prompt.ts
85695
85839
  function byteLength(text) {
85696
85840
  return Buffer.byteLength(text, "utf8");
@@ -85806,7 +85950,7 @@ var init_prompt2 = __esm(() => {
85806
85950
  EXTERNAL_WRITE_RUNTIME_DIRECTIVE = [
85807
85951
  "# Runtime directive",
85808
85952
  "",
85809
- "You are running non-interactively as a bounded child agent. Produce the work itself as " + "your final message, in the requested output schema. Do not ask questions, do not offer a " + "choice of modes, do not route to another skill, orchestrator, or flow, do not delegate to " + "another agent. You may create and modify files, only inside your current working " + "directory, using the Read, Grep, Glob, Edit and Write tools; no shell is available. Do not " + "touch .git, .github, .claude or .metaproject. Your changes are captured as a patch that a " + "human reviews before anything lands, so summarise what you changed in the final message."
85953
+ "You are running non-interactively as a bounded child agent. Produce the work itself as " + "your final message, in the requested output schema. Do not ask questions, do not offer a " + "choice of modes, do not route to another skill, orchestrator, or flow, do not delegate to " + "another agent. You may create and modify files, only inside your current working " + "directory, using your file reading, searching and editing tools. Do not use a shell to reach " + "outside it. Do not touch .git, .github, .claude or .metaproject. Your changes are captured as a patch that a " + "human reviews before anything lands, so summarise what you changed in the final message."
85810
85954
  ].join(`
85811
85955
  `);
85812
85956
  DIFF_INTRO_COMPLETE = "The operator's uncommitted changes are below. Your worktree is checked out at HEAD and " + "does not contain them, so this diff is the only place they exist for you.";
@@ -87638,6 +87782,7 @@ async function superviseExternalRun(input, deps) {
87638
87782
  if (verdict === "drained") {
87639
87783
  return build(reportedExitCode ?? exitCodeFromEvents(events), false);
87640
87784
  }
87785
+ const exitedBeforeKill = reportedExitCode;
87641
87786
  handle.kill();
87642
87787
  const grace = afterMs2(input.killGraceMs ?? DEFAULT_KILL_GRACE_MS, undefined);
87643
87788
  const code = await Promise.race([exited, grace.promise]);
@@ -87645,7 +87790,7 @@ async function superviseExternalRun(input, deps) {
87645
87790
  if (verdict === "timeout") {
87646
87791
  return build(code ?? EXTERNAL_TIMEOUT_EXIT_CODE, true);
87647
87792
  }
87648
- return build(code ?? exitCodeFromEvents(events), false);
87793
+ return build(exitedBeforeKill ?? exitCodeFromEvents(events), false);
87649
87794
  }
87650
87795
  function deferred() {
87651
87796
  let resolve = () => {
@@ -87714,7 +87859,7 @@ function describeError(error) {
87714
87859
  return "unknown error";
87715
87860
  }
87716
87861
  }
87717
- var EXTERNAL_TIMEOUT_EXIT_CODE = 124, STDOUT_HEAD_BYTES, STDOUT_TAIL_BYTES, DEFAULT_KILL_GRACE_MS = 2000, DEFAULT_TERMINAL_SETTLE_MS = 2000, SUPERVISION_TICK_DIVISOR = 4, SUPERVISION_TICK_FLOOR_MS = 5;
87862
+ var EXTERNAL_TIMEOUT_EXIT_CODE = 124, STDOUT_HEAD_BYTES, STDOUT_TAIL_BYTES, DEFAULT_KILL_GRACE_MS = 2000, DEFAULT_TERMINAL_SETTLE_MS = 2000, CODEX_TERMINAL_SETTLE_MS = 1e4, SUPERVISION_TICK_DIVISOR = 4, SUPERVISION_TICK_FLOOR_MS = 5;
87718
87863
  var init_supervise = __esm(() => {
87719
87864
  init_bounded();
87720
87865
  init_bun_spawn_port();
@@ -88338,6 +88483,8 @@ function buildCodexArgv(input) {
88338
88483
  "--ignore-user-config",
88339
88484
  "--skip-git-repo-check"
88340
88485
  ];
88486
+ if (input.sandbox === "worktree-write")
88487
+ argv.push(...CODEX_WRITE_CONFINEMENT_FLAGS);
88341
88488
  if (input.resultSchemaPath !== undefined)
88342
88489
  argv.push("--output-schema", input.resultSchemaPath);
88343
88490
  if (input.model !== undefined)
@@ -88345,8 +88492,9 @@ function buildCodexArgv(input) {
88345
88492
  argv.push(input.prompt);
88346
88493
  return argv;
88347
88494
  }
88348
- function buildCodexResumeArgv(sessionRef, message) {
88349
- return ["codex", "exec", "resume", sessionRef, "--json", "--ignore-user-config", "--skip-git-repo-check", message];
88495
+ function buildCodexResumeArgv(sessionRef, message, input) {
88496
+ const confinement = input?.sandbox === "worktree-write" ? ["-c", `sandbox_mode="${CODEX_SANDBOX_MODES["worktree-write"]}"`, ...CODEX_WRITE_CONFINEMENT_FLAGS] : [];
88497
+ return ["codex", "exec", "resume", sessionRef, "--json", "--ignore-user-config", "--skip-git-repo-check", ...confinement, message];
88350
88498
  }
88351
88499
  function parseCodexEvents(line) {
88352
88500
  const record = readJsonObject2(line);
@@ -88501,12 +88649,21 @@ function brief2(text, max = 200) {
88501
88649
  const flat = text.replace(/\s+/g, " ").trim();
88502
88650
  return flat.length <= max ? flat : `${flat.slice(0, max - 1)}\u2026`;
88503
88651
  }
88504
- var CODEX_SANDBOX_MODES, RECOGNISED_TYPES, NARRATED_FAILURE_LINE, CLI_USAGE_PATTERNS, AUTH_PATTERNS2, LIMIT_PATTERNS2, codexCliCodec;
88652
+ var CODEX_SANDBOX_MODES, CODEX_WRITE_CONFINEMENT_FLAGS, RECOGNISED_TYPES, NARRATED_FAILURE_LINE, CLI_USAGE_PATTERNS, AUTH_PATTERNS2, LIMIT_PATTERNS2, codexCliCodec;
88505
88653
  var init_codex_cli = __esm(() => {
88506
88654
  CODEX_SANDBOX_MODES = {
88507
88655
  "read-only": "read-only",
88508
88656
  "worktree-write": "workspace-write"
88509
88657
  };
88658
+ CODEX_WRITE_CONFINEMENT_FLAGS = [
88659
+ "--ignore-rules",
88660
+ "-c",
88661
+ "sandbox_workspace_write.exclude_slash_tmp=true",
88662
+ "-c",
88663
+ "sandbox_workspace_write.exclude_tmpdir_env_var=true",
88664
+ "-c",
88665
+ "sandbox_workspace_write.network_access=false"
88666
+ ];
88510
88667
  RECOGNISED_TYPES = new Set([
88511
88668
  "thread.started",
88512
88669
  "turn.started",
@@ -88536,7 +88693,7 @@ var init_codex_cli = __esm(() => {
88536
88693
  parseLine: parseCodexLine,
88537
88694
  parseEvents: parseCodexEvents,
88538
88695
  classifyFailure: classifyCodexFailure,
88539
- buildResumeArgv: (sessionRef, message) => buildCodexResumeArgv(sessionRef, message),
88696
+ buildResumeArgv: buildCodexResumeArgv,
88540
88697
  isRecognisedLine: isRecognisedCodexLine
88541
88698
  };
88542
88699
  });
@@ -88650,20 +88807,42 @@ async function runExternalChild(input, deps) {
88650
88807
  if (!isAcp && codec === undefined) {
88651
88808
  return refuse2("Error", `no codec is registered for external agent "${entry.id}"`);
88652
88809
  }
88810
+ let detectedVersion;
88653
88811
  if (deps.detect !== undefined) {
88654
88812
  const availability = resolveAvailability(entry, await deps.detect(entry.binary, entry.detect));
88655
88813
  if (availability.state === "binary-missing") {
88656
88814
  return refuse2("Denied", `${entry.label} is not installed (\`${entry.binary}\` not on PATH)`);
88657
88815
  }
88816
+ if (availability.state === "available")
88817
+ detectedVersion = availability.version;
88658
88818
  if (availability.state === "available" && availability.verdict.state !== "in-range") {
88659
88819
  deps.onWarning?.(`${entry.label} version ${availability.version ?? "unknown"} is outside the range this build's fixtures were recorded against`);
88660
88820
  }
88661
88821
  }
88822
+ const versionRefusal = sandbox === "worktree-write" ? codecWriteVersionRefusal(entry, detectedVersion) : undefined;
88823
+ if (versionRefusal !== undefined)
88824
+ return refuse2("Denied", versionRefusal);
88662
88825
  const schemaPrep = await prepareResultSchema();
88663
88826
  if (!schemaPrep.ok)
88664
88827
  return refuse2("Error", schemaPrep.reason);
88665
88828
  const { schema: resultSchema, schemaText: resultSchemaText, schemaPath: resultSchemaPath, schemaDir } = schemaPrep;
88666
88829
  const resultSchemaInline = await prepareInlineSchema(resultSchema);
88830
+ const bundledResultSchema = JSON.parse(resultSchemaInline);
88831
+ const codexStrict = entry.id === "codex-cli";
88832
+ let resultSchemaPathForCodec = resultSchemaPath;
88833
+ if (codexStrict) {
88834
+ resultSchemaPathForCodec = path208.join(schemaDir, STRICT_SCHEMA_FILE);
88835
+ try {
88836
+ await writeFile32(resultSchemaPathForCodec, JSON.stringify(toStrictOutputSchema(bundledResultSchema)), "utf8");
88837
+ } catch (error) {
88838
+ await rm15(schemaDir, { recursive: true, force: true }).catch(() => {
88839
+ return;
88840
+ });
88841
+ const message = error instanceof Error ? error.message : String(error);
88842
+ return refuse2("Error", `failed to prepare the required result schema for validation: ${message}`);
88843
+ }
88844
+ }
88845
+ const normalize = codexStrict ? (value) => dropOptionalNulls(value, bundledResultSchema) : undefined;
88667
88846
  try {
88668
88847
  const assembled = buildExternalPrompt({
88669
88848
  taskTitle: input.taskTitle,
@@ -88690,7 +88869,7 @@ async function runExternalChild(input, deps) {
88690
88869
  write: sandbox === "worktree-write",
88691
88870
  worktreePath: created.path,
88692
88871
  prompt: assembled.prompt,
88693
- validate: (built) => validateStructuredResult(built, resultSchema)
88872
+ validate: (built) => validateStructuredResult(built, resultSchema, normalize)
88694
88873
  });
88695
88874
  }
88696
88875
  const runInput = {
@@ -88700,7 +88879,7 @@ async function runExternalChild(input, deps) {
88700
88879
  ...input.runtime.model === undefined || input.runtime.model === null ? {} : { model: input.runtime.model },
88701
88880
  ...input.sessionId === undefined ? {} : { sessionId: input.sessionId },
88702
88881
  ...input.runtime.maxCostUnits === undefined || input.runtime.maxCostUnits === null ? {} : { maxCostUnits: input.runtime.maxCostUnits },
88703
- resultSchemaPath,
88882
+ resultSchemaPath: resultSchemaPathForCodec,
88704
88883
  resultSchema: resultSchemaInline,
88705
88884
  printTimeoutSeconds: Math.ceil(input.timeoutMs / 1000)
88706
88885
  };
@@ -88721,6 +88900,7 @@ async function runExternalChild(input, deps) {
88721
88900
  timeoutMs: input.timeoutMs,
88722
88901
  ...entry.maxStderrBytes === undefined ? {} : { maxStderrBytes: entry.maxStderrBytes },
88723
88902
  stdin: streaming ? "pipe" : "ignore",
88903
+ ...entry.id === "codex-cli" ? { terminalSettleMs: CODEX_TERMINAL_SETTLE_MS } : {},
88724
88904
  ...streaming ? {
88725
88905
  initialStdin: [
88726
88906
  codec.encodeStdinMessage(assembled.prompt)
@@ -88736,8 +88916,14 @@ async function runExternalChild(input, deps) {
88736
88916
  ...deps.onSupervisionTrigger === undefined ? {} : { onSupervisionTrigger: deps.onSupervisionTrigger }
88737
88917
  });
88738
88918
  const cause = outcome.overflow ?? codec.classifyFailure(outcome);
88739
- const built = buildOutcome({ cause, outcome, argv, worktreePath: created.path });
88740
- return await validateStructuredResult(built, resultSchema);
88919
+ const built = buildOutcome({
88920
+ cause,
88921
+ outcome,
88922
+ argv,
88923
+ worktreePath: created.path,
88924
+ finalMessageOnly: entry.id === "codex-cli"
88925
+ });
88926
+ return await validateStructuredResult(built, resultSchema, normalize);
88741
88927
  } finally {
88742
88928
  await deps.worktree.remove(input.worktreeId).catch(() => {
88743
88929
  return;
@@ -88749,7 +88935,7 @@ async function runExternalChild(input, deps) {
88749
88935
  });
88750
88936
  }
88751
88937
  }
88752
- async function validateStructuredResult(built, resultSchema) {
88938
+ async function validateStructuredResult(built, resultSchema, normalize) {
88753
88939
  if (built.status !== "Completed")
88754
88940
  return built;
88755
88941
  let parsed;
@@ -88759,12 +88945,13 @@ async function validateStructuredResult(built, resultSchema) {
88759
88945
  const message = error instanceof Error ? error.message : String(error);
88760
88946
  return structuredResultError(`structured result is not valid JSON: ${message}`, built);
88761
88947
  }
88762
- const validationErrors = await validateJson(parsed, resultSchema);
88948
+ const normalized = normalize === undefined ? parsed : normalize(parsed);
88949
+ const validationErrors = await validateJson(normalized, resultSchema);
88763
88950
  if (validationErrors.length > 0) {
88764
88951
  const detail = validationErrors.slice(0, 3).map((e) => `${e.path}: ${e.message}`).join("; ");
88765
88952
  return structuredResultError(`structured result failed subagent-result schema validation: ${detail}`, built);
88766
88953
  }
88767
- return built;
88954
+ return normalized === parsed ? built : { ...built, output: JSON.stringify(normalized) };
88768
88955
  }
88769
88956
  async function runAcpBranch(args) {
88770
88957
  const { input, deps, entry, worktreePath } = args;
@@ -88824,10 +89011,10 @@ async function runAcpBranch(args) {
88824
89011
  return { ...final, acp: record };
88825
89012
  }
88826
89013
  function buildOutcome(args) {
88827
- const { cause, outcome, argv, worktreePath } = args;
89014
+ const { cause, outcome, argv, worktreePath, finalMessageOnly } = args;
88828
89015
  const sessionRef = findSessionRef(outcome.events);
88829
89016
  const costUnits = findCostUnits(outcome.events);
88830
- const text = collectAssistantText(outcome.events);
89017
+ const text = collectAssistantText(outcome.events, finalMessageOnly);
88831
89018
  const base = {
88832
89019
  argv,
88833
89020
  worktreePath,
@@ -88862,13 +89049,21 @@ function findCostUnits(events) {
88862
89049
  }
88863
89050
  return cost;
88864
89051
  }
88865
- function collectAssistantText(events) {
89052
+ function collectAssistantText(events, finalMessageOnly) {
88866
89053
  for (let i = events.length - 1;i >= 0; i -= 1) {
88867
89054
  const event = events[i];
88868
89055
  if (event?.kind === "child_finished" && event.text !== undefined && event.text.trim().length > 0) {
88869
89056
  return event.text.trim();
88870
89057
  }
88871
89058
  }
89059
+ if (finalMessageOnly) {
89060
+ for (let i = events.length - 1;i >= 0; i -= 1) {
89061
+ const event = events[i];
89062
+ if (event?.kind === "assistant_text" && event.text.trim().length > 0)
89063
+ return event.text.trim();
89064
+ }
89065
+ return "";
89066
+ }
88872
89067
  const parts = [];
88873
89068
  for (const event of events) {
88874
89069
  if (event.kind === "assistant_text")
@@ -88877,10 +89072,11 @@ function collectAssistantText(events) {
88877
89072
  return parts.join(`
88878
89073
  `).trim();
88879
89074
  }
88880
- var DEFAULT_BUDGET_THRESHOLD_FRACTION = 0.8, DEFAULT_NO_PROGRESS_INTERVAL_MS = 30000, DENIED_CAUSE_MARKERS;
89075
+ var DEFAULT_BUDGET_THRESHOLD_FRACTION = 0.8, DEFAULT_NO_PROGRESS_INTERVAL_MS = 30000, DENIED_CAUSE_MARKERS, STRICT_SCHEMA_FILE = "subagent-result.strict.schema.json";
88881
89076
  var init_runtime2 = __esm(() => {
88882
89077
  init_contracts();
88883
89078
  init_dispatch();
89079
+ init_strict_schema();
88884
89080
  init_env();
88885
89081
  init_prompt2();
88886
89082
  init_registry3();
@@ -92163,7 +92359,7 @@ function describeValue4(value) {
92163
92359
  return "null";
92164
92360
  return typeof value;
92165
92361
  }
92166
- function resolveRef3(ref, rootSchema) {
92362
+ function resolveRef4(ref, rootSchema) {
92167
92363
  if (ref.startsWith("#/$defs/")) {
92168
92364
  const name = ref.replace("#/$defs/", "");
92169
92365
  const schema = rootSchema.$defs?.[name];
@@ -92185,7 +92381,7 @@ function validateAgainstSchema3(value, schema, rootSchema) {
92185
92381
  }
92186
92382
  function walk4(value, schema, valuePath, rootSchema, errors) {
92187
92383
  if (schema.$ref) {
92188
- const resolved = resolveRef3(schema.$ref, rootSchema);
92384
+ const resolved = resolveRef4(schema.$ref, rootSchema);
92189
92385
  walk4(value, resolved.schema, valuePath, resolved.root, errors);
92190
92386
  return;
92191
92387
  }
@@ -179443,7 +179639,7 @@ async function runCommand4(args, deps, log) {
179443
179639
  maxExternalDepth: DEFAULT_MAX_EXTERNAL_DEPTH,
179444
179640
  onWarning: (warning) => console.error(`warning: ${plain(warning)}`)
179445
179641
  };
179446
- if (write && id === CODEC_WRITE_AGENT_ID) {
179642
+ if (write && CODEC_WRITE_AGENT_IDS.includes(id)) {
179447
179643
  let handle;
179448
179644
  let interrupted = false;
179449
179645
  const onSigint = () => {
@@ -179717,7 +179913,7 @@ function printExternalHelp() {
179717
179913
  { flag: "--no-probe", desc: "Skip detection entirely; every entry reports `not-probed`." },
179718
179914
  { flag: "--task", desc: "run: what the agent should do. It runs in a disposable worktree; your tree is never touched." },
179719
179915
  { flag: "--unattended", desc: "run: refuse every permission that would need a human (also implied without a TTY)." },
179720
- { flag: "--write", desc: "run: writes land in the disposable worktree and leave as a never-applied patch. An ACP agent advertises fs.writeTextFile; for a line-stream agent only claude is supported (Edit and Write inside the worktree)." },
179916
+ { flag: "--write", desc: "run: writes land in the disposable worktree and leave as a never-applied patch. An ACP agent advertises fs.writeTextFile; for a line-stream agent only claude-cli and codex-cli are supported. claude gets Edit and Write inside the worktree and no shell; codex runs under its OS sandbox (writes confined to the worktree, network closed as far as measured) and keeps a shell that can read any file you can read. codex needs version 0.159.2 or newer but older than 0.160.0. antigravity-cli is refused: its edit tool writes outside the worktree." },
179721
179917
  { flag: "--timeout", desc: "run: wall-clock ceiling in ms (default: externalAgents.defaultTimeoutMs)." }
179722
179918
  ]);
179723
179919
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrciphersmith/keryx",
3
- "version": "0.3.41",
3
+ "version": "0.3.43",
4
4
  "description": "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
5
5
  "private": false,
6
6
  "publishConfig": {
@@ -165,7 +165,7 @@
165
165
  "minLength": 1
166
166
  },
167
167
  "sandbox": {
168
- "description": "Permission level for the external child. `worktree-write` is schema-valid but refused at runtime in the read-only release, with a reason distinguishable from an unsupported-by-this-agent refusal.",
168
+ "description": "Permission level for the external child. `worktree-write` is schema-valid but refused at runtime except for an ACP agent and, through the operator's `--write` command only, `claude-cli` and `codex-cli`, with a reason distinguishable from an unsupported-by-this-agent refusal.",
169
169
  "type": "string",
170
170
  "enum": ["read-only", "worktree-write"]
171
171
  },