oh-my-codex 0.19.1 → 0.20.1
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/Cargo.lock +6 -6
- package/Cargo.toml +1 -1
- package/README.md +3 -1
- package/crates/omx-api/src/lib.rs +3 -3
- package/crates/omx-api/tests/cli.rs +4 -4
- package/crates/omx-explore/src/main.rs +4 -4
- package/crates/omx-sparkshell/src/codex_bridge.rs +2 -2
- package/dist/agents/__tests__/definitions.test.js +3 -3
- package/dist/agents/__tests__/definitions.test.js.map +1 -1
- package/dist/agents/__tests__/native-config.test.js +71 -42
- package/dist/agents/__tests__/native-config.test.js.map +1 -1
- package/dist/agents/definitions.d.ts +1 -1
- package/dist/agents/definitions.d.ts.map +1 -1
- package/dist/agents/definitions.js +3 -3
- package/dist/agents/definitions.js.map +1 -1
- package/dist/agents/native-config.d.ts +2 -2
- package/dist/agents/native-config.d.ts.map +1 -1
- package/dist/agents/native-config.js +7 -5
- package/dist/agents/native-config.js.map +1 -1
- package/dist/autopilot/__tests__/planner-routing.test.js +19 -12
- package/dist/autopilot/__tests__/planner-routing.test.js.map +1 -1
- package/dist/autopilot/__tests__/ralplan-gate.test.js +151 -0
- package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -1
- package/dist/autopilot/planner-routing.d.ts.map +1 -1
- package/dist/autopilot/planner-routing.js +4 -2
- package/dist/autopilot/planner-routing.js.map +1 -1
- package/dist/autopilot/ralplan-gate.d.ts +1 -0
- package/dist/autopilot/ralplan-gate.d.ts.map +1 -1
- package/dist/autopilot/ralplan-gate.js +38 -2
- package/dist/autopilot/ralplan-gate.js.map +1 -1
- package/dist/autoresearch/runtime.d.ts +2 -0
- package/dist/autoresearch/runtime.d.ts.map +1 -1
- package/dist/autoresearch/runtime.js +8 -0
- package/dist/autoresearch/runtime.js.map +1 -1
- package/dist/capabilities/__tests__/lockfile.test.d.ts +2 -0
- package/dist/capabilities/__tests__/lockfile.test.d.ts.map +1 -0
- package/dist/capabilities/__tests__/lockfile.test.js +115 -0
- package/dist/capabilities/__tests__/lockfile.test.js.map +1 -0
- package/dist/capabilities/lockfile.d.ts +102 -0
- package/dist/capabilities/lockfile.d.ts.map +1 -0
- package/dist/capabilities/lockfile.js +334 -0
- package/dist/capabilities/lockfile.js.map +1 -0
- package/dist/cli/__tests__/agents.test.js +5 -5
- package/dist/cli/__tests__/agents.test.js.map +1 -1
- package/dist/cli/__tests__/capabilities.test.d.ts +2 -0
- package/dist/cli/__tests__/capabilities.test.d.ts.map +1 -0
- package/dist/cli/__tests__/capabilities.test.js +135 -0
- package/dist/cli/__tests__/capabilities.test.js.map +1 -0
- package/dist/cli/__tests__/codex-plugin-layout.test.js +264 -6
- package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-context-window-warning.test.js +83 -78
- package/dist/cli/__tests__/doctor-context-window-warning.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-invalid-config.test.js +10 -1
- package/dist/cli/__tests__/doctor-invalid-config.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-spark-routing.test.js +22 -6
- package/dist/cli/__tests__/doctor-spark-routing.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-warning-copy.test.js +87 -1
- package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +53 -18
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/plugin-marketplace-idempotent.test.js +3 -3
- package/dist/cli/__tests__/plugin-marketplace-idempotent.test.js.map +1 -1
- package/dist/cli/__tests__/resume.test.js +95 -6
- package/dist/cli/__tests__/resume.test.js.map +1 -1
- package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js +54 -2
- package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js.map +1 -1
- package/dist/cli/__tests__/setup-install-mode.test.js +49 -21
- package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
- package/dist/cli/__tests__/setup-prompts-overwrite.test.js +3 -3
- package/dist/cli/__tests__/setup-refresh.test.js +166 -12
- package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
- package/dist/cli/__tests__/setup-scope.test.js +27 -3
- package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
- package/dist/cli/__tests__/uninstall.test.js +301 -7
- package/dist/cli/__tests__/uninstall.test.js.map +1 -1
- package/dist/cli/agents.js +1 -1
- package/dist/cli/agents.js.map +1 -1
- package/dist/cli/capabilities.d.ts +3 -0
- package/dist/cli/capabilities.d.ts.map +1 -0
- package/dist/cli/capabilities.js +118 -0
- package/dist/cli/capabilities.js.map +1 -0
- package/dist/cli/doctor.d.ts +3 -1
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +50 -34
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.d.ts +7 -4
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +77 -19
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/plugin-marketplace.d.ts +1 -0
- package/dist/cli/plugin-marketplace.d.ts.map +1 -1
- package/dist/cli/plugin-marketplace.js +31 -0
- package/dist/cli/plugin-marketplace.js.map +1 -1
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +9 -6
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/uninstall.d.ts.map +1 -1
- package/dist/cli/uninstall.js +3 -4
- package/dist/cli/uninstall.js.map +1 -1
- package/dist/config/__tests__/generator-idempotent.test.js +210 -114
- package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
- package/dist/config/__tests__/generator-notify.test.js +26 -52
- package/dist/config/__tests__/generator-notify.test.js.map +1 -1
- package/dist/config/__tests__/models.test.js +51 -26
- package/dist/config/__tests__/models.test.js.map +1 -1
- package/dist/config/generator.d.ts +20 -11
- package/dist/config/generator.d.ts.map +1 -1
- package/dist/config/generator.js +169 -152
- package/dist/config/generator.js.map +1 -1
- package/dist/config/models.d.ts +16 -6
- package/dist/config/models.d.ts.map +1 -1
- package/dist/config/models.js +19 -8
- package/dist/config/models.js.map +1 -1
- package/dist/hooks/__tests__/keyword-detector.test.js +12 -4
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +1 -1
- package/dist/hooks/__tests__/prometheus-strict-contract.test.js +2 -2
- package/dist/hooks/__tests__/prometheus-strict-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/tmux-hook-engine.test.js +2 -2
- package/dist/leader/__tests__/contract.test.js +150 -1
- package/dist/leader/__tests__/contract.test.js.map +1 -1
- package/dist/leader/contract.d.ts +26 -0
- package/dist/leader/contract.d.ts.map +1 -1
- package/dist/leader/contract.js +188 -0
- package/dist/leader/contract.js.map +1 -1
- package/dist/modes/base.d.ts +1 -1
- package/dist/modes/base.d.ts.map +1 -1
- package/dist/pipeline/__tests__/stages.test.js +27 -1
- package/dist/pipeline/__tests__/stages.test.js.map +1 -1
- package/dist/pipeline/stages/ralplan.d.ts +2 -1
- package/dist/pipeline/stages/ralplan.d.ts.map +1 -1
- package/dist/pipeline/stages/ralplan.js +1 -0
- package/dist/pipeline/stages/ralplan.js.map +1 -1
- package/dist/pipeline/stages/ultragoal.d.ts +5 -1
- package/dist/pipeline/stages/ultragoal.d.ts.map +1 -1
- package/dist/pipeline/stages/ultragoal.js +14 -4
- package/dist/pipeline/stages/ultragoal.js.map +1 -1
- package/dist/ralplan/__tests__/runtime.test.js +132 -0
- package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
- package/dist/ralplan/runtime.d.ts +17 -0
- package/dist/ralplan/runtime.d.ts.map +1 -1
- package/dist/ralplan/runtime.js +97 -1
- package/dist/ralplan/runtime.js.map +1 -1
- package/dist/scripts/__tests__/codex-native-hook.test.js +1528 -32
- package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
- package/dist/scripts/codex-native-hook.d.ts.map +1 -1
- package/dist/scripts/codex-native-hook.js +377 -31
- package/dist/scripts/codex-native-hook.js.map +1 -1
- package/dist/state/__tests__/operations.test.js +450 -0
- package/dist/state/__tests__/operations.test.js.map +1 -1
- package/dist/state/__tests__/workflow-transition.test.js +14 -0
- package/dist/state/__tests__/workflow-transition.test.js.map +1 -1
- package/dist/state/operations.d.ts.map +1 -1
- package/dist/state/operations.js +72 -11
- package/dist/state/operations.js.map +1 -1
- package/dist/state/terminal-normalization.d.ts +8 -0
- package/dist/state/terminal-normalization.d.ts.map +1 -0
- package/dist/state/terminal-normalization.js +127 -0
- package/dist/state/terminal-normalization.js.map +1 -0
- package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
- package/dist/state/workflow-transition-reconcile.js +3 -1
- package/dist/state/workflow-transition-reconcile.js.map +1 -1
- package/dist/team/__tests__/delegation-policy.test.js +2 -2
- package/dist/team/__tests__/delegation-policy.test.js.map +1 -1
- package/dist/team/__tests__/model-contract.test.js +25 -25
- package/dist/team/__tests__/model-contract.test.js.map +1 -1
- package/dist/team/__tests__/runtime.test.js +28 -28
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/__tests__/scaling.test.js +7 -7
- package/dist/team/__tests__/scaling.test.js.map +1 -1
- package/dist/team/__tests__/tmux-claude-workers-demo.test.js +5 -5
- package/dist/team/__tests__/tmux-claude-workers-demo.test.js.map +1 -1
- package/dist/team/__tests__/tmux-session.test.js +12 -12
- package/dist/team/__tests__/tmux-session.test.js.map +1 -1
- package/dist/team/__tests__/worker-bootstrap.test.js +57 -9
- package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
- package/dist/team/__tests__/worker-runtime-identity.test.js +3 -3
- package/dist/team/__tests__/worker-runtime-identity.test.js.map +1 -1
- package/dist/team/model-contract.d.ts +1 -1
- package/dist/team/model-contract.d.ts.map +1 -1
- package/dist/team/runtime.d.ts.map +1 -1
- package/dist/team/runtime.js +11 -0
- package/dist/team/runtime.js.map +1 -1
- package/dist/team/worker-bootstrap.d.ts +2 -0
- package/dist/team/worker-bootstrap.d.ts.map +1 -1
- package/dist/team/worker-bootstrap.js +6 -1
- package/dist/team/worker-bootstrap.js.map +1 -1
- package/dist/ultragoal/__tests__/artifacts.test.js +68 -1
- package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
- package/dist/ultragoal/artifacts.d.ts +5 -1
- package/dist/ultragoal/artifacts.d.ts.map +1 -1
- package/dist/ultragoal/artifacts.js +41 -9
- package/dist/ultragoal/artifacts.js.map +1 -1
- package/dist/utils/__tests__/agents-md.test.js +16 -0
- package/dist/utils/__tests__/agents-md.test.js.map +1 -1
- package/dist/utils/__tests__/agents-model-table.test.js +7 -7
- package/dist/utils/__tests__/agents-model-table.test.js.map +1 -1
- package/dist/utils/__tests__/worktree-tool-context.test.d.ts +2 -0
- package/dist/utils/__tests__/worktree-tool-context.test.d.ts.map +1 -0
- package/dist/utils/__tests__/worktree-tool-context.test.js +118 -0
- package/dist/utils/__tests__/worktree-tool-context.test.js.map +1 -0
- package/dist/utils/agents-md.d.ts.map +1 -1
- package/dist/utils/agents-md.js +4 -3
- package/dist/utils/agents-md.js.map +1 -1
- package/dist/utils/worktree-tool-context.d.ts +25 -0
- package/dist/utils/worktree-tool-context.d.ts.map +1 -0
- package/dist/utils/worktree-tool-context.js +116 -0
- package/dist/utils/worktree-tool-context.js.map +1 -0
- package/package.json +1 -1
- package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +96 -10
- package/plugins/oh-my-codex/skills/ai-slop-cleaner/SKILL.md +1 -1
- package/plugins/oh-my-codex/skills/code-review/SKILL.md +1 -1
- package/plugins/oh-my-codex/skills/omx-setup/SKILL.md +5 -5
- package/plugins/oh-my-codex/skills/prometheus-strict/SKILL.md +3 -3
- package/plugins/oh-my-codex/skills/ralplan/SKILL.md +1 -1
- package/plugins/oh-my-codex/skills/team/SKILL.md +1 -1
- package/plugins/oh-my-codex/skills/ultrawork/references/agent-tiers.md +2 -2
- package/prompts/prometheus-strict-metis.md +1 -1
- package/skills/ai-slop-cleaner/SKILL.md +1 -1
- package/skills/code-review/SKILL.md +1 -1
- package/skills/ecomode/references/agent-tiers.md +1 -1
- package/skills/omx-setup/SKILL.md +5 -5
- package/skills/prometheus-strict/SKILL.md +3 -3
- package/skills/ralplan/SKILL.md +1 -1
- package/skills/team/SKILL.md +1 -1
- package/skills/ultrawork/references/agent-tiers.md +2 -2
- package/src/scripts/__tests__/codex-native-hook.test.ts +1818 -113
- package/src/scripts/codex-native-hook.ts +414 -27
|
@@ -581,6 +581,68 @@ describe("codex native hook dispatch", () => {
|
|
|
581
581
|
await rm(cwd, { recursive: true, force: true });
|
|
582
582
|
}
|
|
583
583
|
});
|
|
584
|
+
it("allows Ralplan Markdown draft-only apply_patch on the live CLI path while denying mixed targets", async () => {
|
|
585
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-ralplan-draft-boundary-"));
|
|
586
|
+
const sessionId = "sess-cli-ralplan-draft-boundary";
|
|
587
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
588
|
+
try {
|
|
589
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId, cwd });
|
|
590
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
591
|
+
active: true,
|
|
592
|
+
skill: "ralplan",
|
|
593
|
+
phase: "planning",
|
|
594
|
+
session_id: sessionId,
|
|
595
|
+
active_skills: [{ skill: "ralplan", phase: "planning", active: true, session_id: sessionId }],
|
|
596
|
+
});
|
|
597
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
598
|
+
active: true,
|
|
599
|
+
mode: "ralplan",
|
|
600
|
+
current_phase: "planning",
|
|
601
|
+
session_id: sessionId,
|
|
602
|
+
});
|
|
603
|
+
for (const [name, target] of [
|
|
604
|
+
["relative", ".omx/drafts/issue-3105.md"],
|
|
605
|
+
["repository-absolute", join(cwd, ".omx", "drafts", "issue-3105.md")],
|
|
606
|
+
]) {
|
|
607
|
+
const result = runNativeHookCliResult({
|
|
608
|
+
hook_event_name: "PreToolUse",
|
|
609
|
+
cwd,
|
|
610
|
+
session_id: sessionId,
|
|
611
|
+
thread_id: `thread-cli-ralplan-draft-${name}`,
|
|
612
|
+
tool_name: "apply_patch",
|
|
613
|
+
tool_input: {
|
|
614
|
+
input: `*** Begin Patch\n*** Add File: ${target}\n+# Draft\n*** End Patch\n`,
|
|
615
|
+
},
|
|
616
|
+
}, { cwd });
|
|
617
|
+
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
618
|
+
assert.deepEqual(parseSingleJsonStdout(result.stdout), {});
|
|
619
|
+
}
|
|
620
|
+
const mixedResult = runNativeHookCliResult({
|
|
621
|
+
hook_event_name: "PreToolUse",
|
|
622
|
+
cwd,
|
|
623
|
+
session_id: sessionId,
|
|
624
|
+
thread_id: "thread-cli-ralplan-draft-mixed",
|
|
625
|
+
tool_name: "apply_patch",
|
|
626
|
+
tool_input: {
|
|
627
|
+
input: "*** Begin Patch\n*** Add File: .omx/drafts/issue-3105.md\n+# Draft\n*** Add File: src/leak.ts\n+leak\n*** End Patch\n",
|
|
628
|
+
},
|
|
629
|
+
}, { cwd });
|
|
630
|
+
assert.equal(mixedResult.status, 0, mixedResult.stderr || mixedResult.stdout);
|
|
631
|
+
const mixedOutput = parseSingleJsonStdout(mixedResult.stdout);
|
|
632
|
+
const hookSpecificOutput = mixedOutput.hookSpecificOutput;
|
|
633
|
+
assert.deepEqual(Object.keys(mixedOutput).sort(), ["hookSpecificOutput"]);
|
|
634
|
+
assert.equal(hookSpecificOutput.hookEventName, "PreToolUse");
|
|
635
|
+
assert.equal(hookSpecificOutput.permissionDecision, "deny");
|
|
636
|
+
assert.match(String(hookSpecificOutput.permissionDecisionReason ?? ""), /src\/leak\.ts/);
|
|
637
|
+
assert.match(String(hookSpecificOutput.permissionDecisionReason ?? ""), /implementation\/write tools are blocked/);
|
|
638
|
+
assert.equal(mixedOutput.decision, undefined);
|
|
639
|
+
assert.equal(mixedOutput.reason, undefined);
|
|
640
|
+
assert.equal(mixedOutput.systemMessage, undefined);
|
|
641
|
+
}
|
|
642
|
+
finally {
|
|
643
|
+
await rm(cwd, { recursive: true, force: true });
|
|
644
|
+
}
|
|
645
|
+
});
|
|
584
646
|
it("preserves team-worker typed subagent PreToolUse exemption without thread spawn provenance", async () => {
|
|
585
647
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-team-worker-typed-pretool-exempt-"));
|
|
586
648
|
const sessionId = "sess-team-worker-typed-pretool-exempt";
|
|
@@ -1211,10 +1273,51 @@ PY`,
|
|
|
1211
1273
|
thread_id: "thread-autopilot-diagnostic",
|
|
1212
1274
|
}, { cwd }));
|
|
1213
1275
|
assert.equal(output.decision, "block");
|
|
1276
|
+
assert.equal(output.stopReason, "autopilot_ultragoal");
|
|
1214
1277
|
assert.match(String(output.reason ?? ""), /state: \.omx\/state\/sessions\/sess-autopilot-diagnostic\/autopilot-state\.json/);
|
|
1215
1278
|
assert.match(String(output.reason ?? ""), /canonical: canonical_agrees/);
|
|
1216
|
-
assert.
|
|
1217
|
-
assert.
|
|
1279
|
+
assert.match(String(output.systemMessage ?? ""), /state: \.omx\/state\/sessions\/sess-autopilot-diagnostic\/autopilot-state\.json/);
|
|
1280
|
+
assert.match(String(output.systemMessage ?? ""), /canonical: canonical_agrees/);
|
|
1281
|
+
assert.deepEqual(Object.keys(output).sort(), ["decision", "reason", "stopReason", "systemMessage"]);
|
|
1282
|
+
assert.equal("statePath" in output, false);
|
|
1283
|
+
assert.equal("canonicalDisagreement" in output, false);
|
|
1284
|
+
}
|
|
1285
|
+
finally {
|
|
1286
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1287
|
+
}
|
|
1288
|
+
});
|
|
1289
|
+
it("keeps canonical phase disagreements in active Autopilot Stop diagnostics", async () => {
|
|
1290
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-canonical-phase-"));
|
|
1291
|
+
try {
|
|
1292
|
+
const sessionId = "sess-autopilot-canonical-phase";
|
|
1293
|
+
await writeJson(join(cwd, ".omx", "state", "session.json"), { session_id: sessionId, cwd });
|
|
1294
|
+
await writeJson(join(cwd, ".omx", "state", "skill-active-state.json"), {
|
|
1295
|
+
version: 1,
|
|
1296
|
+
active: true,
|
|
1297
|
+
skill: "autopilot",
|
|
1298
|
+
phase: "execution",
|
|
1299
|
+
session_id: sessionId,
|
|
1300
|
+
active_skills: [{ skill: "autopilot", phase: "execution", active: true, session_id: sessionId }],
|
|
1301
|
+
});
|
|
1302
|
+
await writeJson(join(cwd, ".omx", "state", "sessions", sessionId, "autopilot-state.json"), {
|
|
1303
|
+
active: true,
|
|
1304
|
+
mode: "autopilot",
|
|
1305
|
+
current_phase: "ultragoal",
|
|
1306
|
+
session_id: sessionId,
|
|
1307
|
+
});
|
|
1308
|
+
const output = parseSingleJsonStdout(runNativeHookCli({
|
|
1309
|
+
hook_event_name: "Stop",
|
|
1310
|
+
cwd,
|
|
1311
|
+
session_id: sessionId,
|
|
1312
|
+
thread_id: "thread-autopilot-canonical-phase",
|
|
1313
|
+
}, { cwd }));
|
|
1314
|
+
assert.equal(output.decision, "block");
|
|
1315
|
+
assert.equal(output.stopReason, "autopilot_ultragoal");
|
|
1316
|
+
assert.match(String(output.reason ?? ""), /canonical: canonical_phase:execution/);
|
|
1317
|
+
assert.match(String(output.systemMessage ?? ""), /canonical: canonical_phase:execution/);
|
|
1318
|
+
assert.deepEqual(Object.keys(output).sort(), ["decision", "reason", "stopReason", "systemMessage"]);
|
|
1319
|
+
assert.equal("statePath" in output, false);
|
|
1320
|
+
assert.equal("canonicalDisagreement" in output, false);
|
|
1218
1321
|
}
|
|
1219
1322
|
finally {
|
|
1220
1323
|
await rm(cwd, { recursive: true, force: true });
|
|
@@ -1479,6 +1582,166 @@ PY`,
|
|
|
1479
1582
|
await rm(cwd, { recursive: true, force: true });
|
|
1480
1583
|
}
|
|
1481
1584
|
});
|
|
1585
|
+
it("adds resume-by-id instructions for persisted subagents on SessionStart resume", async () => {
|
|
1586
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-reopen-"));
|
|
1587
|
+
try {
|
|
1588
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
1589
|
+
const sessionId = "omx-reopen-session";
|
|
1590
|
+
await writeSessionStart(cwd, sessionId, { nativeSessionId: "codex-leader-reopen", pid: process.pid });
|
|
1591
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
1592
|
+
schemaVersion: 1,
|
|
1593
|
+
sessions: {
|
|
1594
|
+
[sessionId]: {
|
|
1595
|
+
session_id: sessionId,
|
|
1596
|
+
leader_thread_id: "codex-leader-reopen",
|
|
1597
|
+
updated_at: "2026-07-09T00:00:00.000Z",
|
|
1598
|
+
threads: {
|
|
1599
|
+
"codex-leader-reopen": {
|
|
1600
|
+
thread_id: "codex-leader-reopen",
|
|
1601
|
+
kind: "leader",
|
|
1602
|
+
first_seen_at: "2026-07-09T00:00:00.000Z",
|
|
1603
|
+
last_seen_at: "2026-07-09T00:00:00.000Z",
|
|
1604
|
+
turn_count: 1,
|
|
1605
|
+
},
|
|
1606
|
+
"thread-architect-reopen": {
|
|
1607
|
+
thread_id: "thread-architect-reopen",
|
|
1608
|
+
kind: "subagent",
|
|
1609
|
+
first_seen_at: "2026-07-09T00:01:00.000Z",
|
|
1610
|
+
last_seen_at: "2026-07-09T00:01:00.000Z",
|
|
1611
|
+
turn_count: 2,
|
|
1612
|
+
role: "architect",
|
|
1613
|
+
lane_id: "plan-review",
|
|
1614
|
+
scope: "SessionStart reopen",
|
|
1615
|
+
status: "available",
|
|
1616
|
+
last_handoff_summary: "reviewed the restart plan",
|
|
1617
|
+
},
|
|
1618
|
+
},
|
|
1619
|
+
},
|
|
1620
|
+
},
|
|
1621
|
+
});
|
|
1622
|
+
const result = await dispatchCodexNativeHook({
|
|
1623
|
+
hook_event_name: "SessionStart",
|
|
1624
|
+
cwd,
|
|
1625
|
+
session_id: "codex-leader-reopen",
|
|
1626
|
+
source: "resume",
|
|
1627
|
+
}, { cwd, sessionOwnerPid: process.pid });
|
|
1628
|
+
const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
|
|
1629
|
+
assert.match(additionalContext, /\[Persisted subagent reopen\]/);
|
|
1630
|
+
assert.match(additionalContext, /resume_agent\("thread-architect-reopen"\)/);
|
|
1631
|
+
assert.match(additionalContext, /role: architect; lane: plan-review; scope: SessionStart reopen; status: available/);
|
|
1632
|
+
assert.match(additionalContext, /saved subagent ids found: 1/);
|
|
1633
|
+
const tracking = JSON.parse(await readFile(join(stateDir, "subagent-tracking.json"), "utf-8"));
|
|
1634
|
+
assert.match(tracking.sessions?.[sessionId]?.threads?.["thread-architect-reopen"]?.resume_requested_at ?? "", /^\d{4}-\d{2}-\d{2}T/);
|
|
1635
|
+
}
|
|
1636
|
+
finally {
|
|
1637
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1638
|
+
}
|
|
1639
|
+
});
|
|
1640
|
+
it("does not suggest duplicate same-role subagent spawns when reopen ids exist", async () => {
|
|
1641
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-no-duplicates-"));
|
|
1642
|
+
try {
|
|
1643
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
1644
|
+
const sessionId = "omx-reuse-session";
|
|
1645
|
+
await writeSessionStart(cwd, sessionId, { nativeSessionId: "codex-leader-reuse", pid: process.pid });
|
|
1646
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
1647
|
+
schemaVersion: 1,
|
|
1648
|
+
sessions: {
|
|
1649
|
+
[sessionId]: {
|
|
1650
|
+
session_id: sessionId,
|
|
1651
|
+
leader_thread_id: "codex-leader-reuse",
|
|
1652
|
+
updated_at: "2026-07-09T00:00:00.000Z",
|
|
1653
|
+
threads: {
|
|
1654
|
+
"codex-leader-reuse": {
|
|
1655
|
+
thread_id: "codex-leader-reuse",
|
|
1656
|
+
kind: "leader",
|
|
1657
|
+
first_seen_at: "2026-07-09T00:00:00.000Z",
|
|
1658
|
+
last_seen_at: "2026-07-09T00:00:00.000Z",
|
|
1659
|
+
turn_count: 1,
|
|
1660
|
+
},
|
|
1661
|
+
"thread-critic-reuse": {
|
|
1662
|
+
thread_id: "thread-critic-reuse",
|
|
1663
|
+
kind: "subagent",
|
|
1664
|
+
first_seen_at: "2026-07-09T00:01:00.000Z",
|
|
1665
|
+
last_seen_at: "2026-07-09T00:01:00.000Z",
|
|
1666
|
+
turn_count: 1,
|
|
1667
|
+
role: "critic",
|
|
1668
|
+
lane_id: "risk-review",
|
|
1669
|
+
status: "closed",
|
|
1670
|
+
},
|
|
1671
|
+
},
|
|
1672
|
+
},
|
|
1673
|
+
},
|
|
1674
|
+
});
|
|
1675
|
+
const result = await dispatchCodexNativeHook({
|
|
1676
|
+
hook_event_name: "SessionStart",
|
|
1677
|
+
cwd,
|
|
1678
|
+
session_id: "codex-leader-reuse",
|
|
1679
|
+
source: "startup",
|
|
1680
|
+
}, { cwd, sessionOwnerPid: process.pid });
|
|
1681
|
+
const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
|
|
1682
|
+
assert.match(additionalContext, /resume_agent\("thread-critic-reuse"\)/);
|
|
1683
|
+
assert.match(additionalContext, /avoid duplicate same-type subagent spawns/);
|
|
1684
|
+
assert.match(additionalContext, /do not spawn a new agent solely because reopen failed/);
|
|
1685
|
+
assert.doesNotMatch(additionalContext, /spawn.*critic.*replacement/i);
|
|
1686
|
+
}
|
|
1687
|
+
finally {
|
|
1688
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1689
|
+
}
|
|
1690
|
+
});
|
|
1691
|
+
it("surfaces clear warnings for unavailable persisted subagents without spawning replacements", async () => {
|
|
1692
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-warning-"));
|
|
1693
|
+
try {
|
|
1694
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
1695
|
+
const sessionId = "omx-warning-session";
|
|
1696
|
+
await writeSessionStart(cwd, sessionId, { nativeSessionId: "codex-leader-warning", pid: process.pid });
|
|
1697
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
1698
|
+
schemaVersion: 1,
|
|
1699
|
+
sessions: {
|
|
1700
|
+
[sessionId]: {
|
|
1701
|
+
session_id: sessionId,
|
|
1702
|
+
leader_thread_id: "codex-leader-warning",
|
|
1703
|
+
updated_at: "2026-07-09T00:00:00.000Z",
|
|
1704
|
+
threads: {
|
|
1705
|
+
"codex-leader-warning": {
|
|
1706
|
+
thread_id: "codex-leader-warning",
|
|
1707
|
+
kind: "leader",
|
|
1708
|
+
first_seen_at: "2026-07-09T00:00:00.000Z",
|
|
1709
|
+
last_seen_at: "2026-07-09T00:00:00.000Z",
|
|
1710
|
+
turn_count: 1,
|
|
1711
|
+
},
|
|
1712
|
+
"thread-executor-unavailable": {
|
|
1713
|
+
thread_id: "thread-executor-unavailable",
|
|
1714
|
+
kind: "subagent",
|
|
1715
|
+
first_seen_at: "2026-07-09T00:01:00.000Z",
|
|
1716
|
+
last_seen_at: "2026-07-09T00:01:00.000Z",
|
|
1717
|
+
turn_count: 1,
|
|
1718
|
+
role: "executor",
|
|
1719
|
+
lane_id: "implementation",
|
|
1720
|
+
status: "unavailable",
|
|
1721
|
+
resume_failed_at: "2026-07-09T00:02:00.000Z",
|
|
1722
|
+
resume_failure_reason: "Codex reported missing thread id",
|
|
1723
|
+
},
|
|
1724
|
+
},
|
|
1725
|
+
},
|
|
1726
|
+
},
|
|
1727
|
+
});
|
|
1728
|
+
const result = await dispatchCodexNativeHook({
|
|
1729
|
+
hook_event_name: "SessionStart",
|
|
1730
|
+
cwd,
|
|
1731
|
+
session_id: "codex-leader-warning",
|
|
1732
|
+
source: "resume",
|
|
1733
|
+
}, { cwd, sessionOwnerPid: process.pid });
|
|
1734
|
+
const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
|
|
1735
|
+
assert.match(additionalContext, /No compatible saved subagent id is currently marked reopenable/);
|
|
1736
|
+
assert.match(additionalContext, /Reopen warnings:/);
|
|
1737
|
+
assert.match(additionalContext, /thread-executor-unavailable/);
|
|
1738
|
+
assert.match(additionalContext, /last failure: Codex reported missing thread id/);
|
|
1739
|
+
assert.doesNotMatch(additionalContext, /resume_agent\("thread-executor-unavailable"\)/);
|
|
1740
|
+
}
|
|
1741
|
+
finally {
|
|
1742
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1743
|
+
}
|
|
1744
|
+
});
|
|
1482
1745
|
it("preserves canonical OMX session scope when native SessionStart arrives with a different id", async () => {
|
|
1483
1746
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-session-reconcile-"));
|
|
1484
1747
|
try {
|
|
@@ -4150,6 +4413,66 @@ standardMaxRounds = 15
|
|
|
4150
4413
|
await rm(cwd, { recursive: true, force: true });
|
|
4151
4414
|
}
|
|
4152
4415
|
});
|
|
4416
|
+
it("keeps conductor guidance on autopilot activation after capacity-only native subagent evidence", async () => {
|
|
4417
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-capacity-native-"));
|
|
4418
|
+
try {
|
|
4419
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
4420
|
+
await dispatchCodexNativeHook({
|
|
4421
|
+
hook_event_name: "PostToolUse",
|
|
4422
|
+
cwd,
|
|
4423
|
+
session_id: "sess-autopilot-capacity-native",
|
|
4424
|
+
thread_id: "thread-autopilot-capacity-native",
|
|
4425
|
+
turn_id: "turn-autopilot-capacity-native-spawn",
|
|
4426
|
+
tool_name: "multi_agent_v1.spawn_agent",
|
|
4427
|
+
tool_response: { error: "collab spawn failed: agent thread limit reached" },
|
|
4428
|
+
}, { cwd });
|
|
4429
|
+
const result = await dispatchCodexNativeHook({
|
|
4430
|
+
hook_event_name: "UserPromptSubmit",
|
|
4431
|
+
cwd,
|
|
4432
|
+
session_id: "sess-autopilot-capacity-native",
|
|
4433
|
+
thread_id: "thread-autopilot-capacity-native",
|
|
4434
|
+
turn_id: "turn-autopilot-capacity-native-prompt",
|
|
4435
|
+
prompt: "$autopilot implement issue #3078",
|
|
4436
|
+
}, { cwd });
|
|
4437
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
4438
|
+
assert.equal(result.skillState?.skill, "autopilot");
|
|
4439
|
+
const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
|
|
4440
|
+
assert.match(message, /Autopilot protocol:/);
|
|
4441
|
+
assert.match(message, /Conductor mode contract:/);
|
|
4442
|
+
assert.match(message, /Golden Rule: When the Main agent is acting in Conductor mode/);
|
|
4443
|
+
assert.match(message, /Conductor reuse and ledger guidance:/);
|
|
4444
|
+
assert.doesNotMatch(message, /Native subagent support is unavailable in this environment/);
|
|
4445
|
+
assert.doesNotMatch(message, /Reason: agent_thread_limit_reached/);
|
|
4446
|
+
}
|
|
4447
|
+
finally {
|
|
4448
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4449
|
+
}
|
|
4450
|
+
});
|
|
4451
|
+
it("omits conductor block and emits unsupported native guidance for unsupported autopilot first-run payload", async () => {
|
|
4452
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-unsupported-native-"));
|
|
4453
|
+
try {
|
|
4454
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
4455
|
+
const result = await dispatchCodexNativeHook({
|
|
4456
|
+
hook_event_name: "UserPromptSubmit",
|
|
4457
|
+
cwd,
|
|
4458
|
+
session_id: "sess-autopilot-unsupported-native",
|
|
4459
|
+
thread_id: "thread-autopilot-unsupported-native",
|
|
4460
|
+
turn_id: "turn-autopilot-unsupported-native",
|
|
4461
|
+
prompt: "$autopilot implement issue #3078",
|
|
4462
|
+
capabilities: { native_subagents: false, multi_agent_v1: false },
|
|
4463
|
+
}, { cwd });
|
|
4464
|
+
const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
|
|
4465
|
+
assert.match(message, /Autopilot protocol:/);
|
|
4466
|
+
assert.doesNotMatch(message, /Conductor mode contract:/);
|
|
4467
|
+
assert.doesNotMatch(message, /Conductor reuse and ledger guidance:/);
|
|
4468
|
+
assert.match(message, /Native subagent support is unavailable in this environment/);
|
|
4469
|
+
assert.match(message, /Reason: native_subagents_unsupported/);
|
|
4470
|
+
assert.match(message, /Do not call multi_agent_v1\.close_agent/);
|
|
4471
|
+
}
|
|
4472
|
+
finally {
|
|
4473
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4474
|
+
}
|
|
4475
|
+
});
|
|
4153
4476
|
it("records ultragoal prompt skill activation with goal-tool handoff guidance", async () => {
|
|
4154
4477
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-"));
|
|
4155
4478
|
try {
|
|
@@ -6522,6 +6845,69 @@ exit 0
|
|
|
6522
6845
|
}, { cwd });
|
|
6523
6846
|
assert.equal(protectedWrite.outputJson?.decision, "block", `${filePath} should not be model-writable during deep-interview`);
|
|
6524
6847
|
}
|
|
6848
|
+
const runtimeRoot = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-deep-interview-runtime-root-"));
|
|
6849
|
+
const previousOmxRoot = process.env.OMX_ROOT;
|
|
6850
|
+
const runtimeSessionId = "sess-di-runtime-artifact";
|
|
6851
|
+
const runtimeStateDir = join(runtimeRoot, ".omx", "state");
|
|
6852
|
+
const runtimeSessionDir = join(runtimeStateDir, "sessions", runtimeSessionId);
|
|
6853
|
+
try {
|
|
6854
|
+
process.env.OMX_ROOT = runtimeRoot;
|
|
6855
|
+
await mkdir(runtimeSessionDir, { recursive: true });
|
|
6856
|
+
await writeJson(join(runtimeStateDir, "session.json"), { session_id: runtimeSessionId, cwd });
|
|
6857
|
+
await writeJson(join(runtimeSessionDir, "skill-active-state.json"), {
|
|
6858
|
+
version: 1,
|
|
6859
|
+
active: true,
|
|
6860
|
+
skill: "deep-interview",
|
|
6861
|
+
phase: "planning",
|
|
6862
|
+
session_id: runtimeSessionId,
|
|
6863
|
+
thread_id: threadId,
|
|
6864
|
+
active_skills: [{ skill: "deep-interview", phase: "planning", active: true, session_id: runtimeSessionId, thread_id: threadId }],
|
|
6865
|
+
});
|
|
6866
|
+
await writeJson(join(runtimeSessionDir, "deep-interview-state.json"), {
|
|
6867
|
+
active: true,
|
|
6868
|
+
mode: "deep-interview",
|
|
6869
|
+
current_phase: "intent-first",
|
|
6870
|
+
session_id: runtimeSessionId,
|
|
6871
|
+
thread_id: threadId,
|
|
6872
|
+
});
|
|
6873
|
+
const allowedRuntimeStateWrite = await dispatchCodexNativeHook({
|
|
6874
|
+
hook_event_name: "PreToolUse",
|
|
6875
|
+
cwd,
|
|
6876
|
+
session_id: runtimeSessionId,
|
|
6877
|
+
thread_id: threadId,
|
|
6878
|
+
tool_name: "Write",
|
|
6879
|
+
tool_use_id: "tool-di-runtime-state-write",
|
|
6880
|
+
tool_input: { file_path: join(runtimeSessionDir, "deep-interview-state.json"), content: "{}\n" },
|
|
6881
|
+
}, { cwd });
|
|
6882
|
+
assert.equal(allowedRuntimeStateWrite.outputJson, null);
|
|
6883
|
+
const blockedRuntimeStatePeerWrite = await dispatchCodexNativeHook({
|
|
6884
|
+
hook_event_name: "PreToolUse",
|
|
6885
|
+
cwd,
|
|
6886
|
+
session_id: runtimeSessionId,
|
|
6887
|
+
thread_id: threadId,
|
|
6888
|
+
tool_name: "Write",
|
|
6889
|
+
tool_use_id: "tool-di-runtime-peer-state-write",
|
|
6890
|
+
tool_input: { file_path: join(runtimeRoot, ".omx", "state", "sessions", "../outside", "deep-interview-state.json"), content: "{}\n" },
|
|
6891
|
+
}, { cwd });
|
|
6892
|
+
assert.equal(blockedRuntimeStatePeerWrite.outputJson?.decision, "block");
|
|
6893
|
+
const blockedRuntimeUnrelatedWrite = await dispatchCodexNativeHook({
|
|
6894
|
+
hook_event_name: "PreToolUse",
|
|
6895
|
+
cwd,
|
|
6896
|
+
session_id: runtimeSessionId,
|
|
6897
|
+
thread_id: threadId,
|
|
6898
|
+
tool_name: "Write",
|
|
6899
|
+
tool_use_id: "tool-di-runtime-unrelated-write",
|
|
6900
|
+
tool_input: { file_path: join(runtimeRoot, "unrelated", "deep-interview-state.json"), content: "{}\n" },
|
|
6901
|
+
}, { cwd });
|
|
6902
|
+
assert.equal(blockedRuntimeUnrelatedWrite.outputJson?.decision, "block");
|
|
6903
|
+
}
|
|
6904
|
+
finally {
|
|
6905
|
+
if (typeof previousOmxRoot === "string")
|
|
6906
|
+
process.env.OMX_ROOT = previousOmxRoot;
|
|
6907
|
+
else
|
|
6908
|
+
delete process.env.OMX_ROOT;
|
|
6909
|
+
await rm(runtimeRoot, { recursive: true, force: true });
|
|
6910
|
+
}
|
|
6525
6911
|
// Cross-mode non-terminal `omx state write` payloads are activations,
|
|
6526
6912
|
// because state_write normalizes them to active=true after the hook.
|
|
6527
6913
|
const blockedStateCliMutation = await preToolUse({
|
|
@@ -8501,12 +8887,29 @@ exit 0
|
|
|
8501
8887
|
for (const path of [
|
|
8502
8888
|
".omx/context/findings.md",
|
|
8503
8889
|
".omx/plans/issue-2863.md",
|
|
8890
|
+
".omx/drafts/issue-3105.md",
|
|
8504
8891
|
".omx/specs/issue-2863.md",
|
|
8505
8892
|
".omx/state/required-planning-state.json",
|
|
8506
8893
|
]) {
|
|
8507
8894
|
const writeResult = await preToolUse("Write", `tool-ralplan-write-${path}`, { file_path: path, content: "ok" });
|
|
8508
8895
|
assert.equal(writeResult.outputJson, null, `Write should be allowed for ${path}`);
|
|
8509
8896
|
}
|
|
8897
|
+
const allowedDraftEdit = await preToolUse("Edit", "tool-ralplan-draft-edit", {
|
|
8898
|
+
file_path: ".omx/drafts/issue-3105.md",
|
|
8899
|
+
old_string: "old",
|
|
8900
|
+
new_string: "new",
|
|
8901
|
+
});
|
|
8902
|
+
assert.equal(allowedDraftEdit.outputJson, null);
|
|
8903
|
+
for (const path of [
|
|
8904
|
+
join(cwd, ".omx", "drafts", "issue-3105.md"),
|
|
8905
|
+
".omx/drafts/subdir/../issue-3105.md",
|
|
8906
|
+
]) {
|
|
8907
|
+
const normalizedDraftWrite = await preToolUse("Write", `tool-ralplan-normalized-draft-${path}`, {
|
|
8908
|
+
file_path: path,
|
|
8909
|
+
content: "# Draft",
|
|
8910
|
+
});
|
|
8911
|
+
assert.equal(normalizedDraftWrite.outputJson, null, `normalized draft path should be allowed: ${path}`);
|
|
8912
|
+
}
|
|
8510
8913
|
for (const protectedPath of [
|
|
8511
8914
|
".omx/state/sessions/sess-ralplan-guard/ralplan-state.json",
|
|
8512
8915
|
".omx/state/sessions/sess-ralplan-guard/autopilot-state.json",
|
|
@@ -8539,10 +8942,22 @@ exit 0
|
|
|
8539
8942
|
input: "*** Begin Patch\n*** Update File: .omx/plans/issue-2863.md\n@@\n-old\n+new\n*** End Patch\n",
|
|
8540
8943
|
});
|
|
8541
8944
|
assert.equal(allowedPatchUpdate.outputJson, null);
|
|
8945
|
+
const allowedDraftPatchAdd = await preToolUse("apply_patch", "tool-ralplan-draft-patch-add", {
|
|
8946
|
+
input: "*** Begin Patch\n*** Add File: .omx/drafts/issue-3105.md\n+# Draft\n*** End Patch\n",
|
|
8947
|
+
});
|
|
8948
|
+
assert.equal(allowedDraftPatchAdd.outputJson, null);
|
|
8949
|
+
const allowedDraftPatchUpdate = await preToolUse("ApplyPatch", "tool-ralplan-draft-patch-update", {
|
|
8950
|
+
input: "*** Begin Patch\n*** Update File: .omx/drafts/issue-3105.md\n@@\n-old\n+new\n*** End Patch\n",
|
|
8951
|
+
});
|
|
8952
|
+
assert.equal(allowedDraftPatchUpdate.outputJson, null);
|
|
8542
8953
|
const allowedRedirect = await preToolUse("Bash", "tool-ralplan-redirect-allow", {
|
|
8543
8954
|
command: "printf '\\nmore\\n' >> .omx/plans/issue-2863.md",
|
|
8544
8955
|
});
|
|
8545
8956
|
assert.equal(allowedRedirect.outputJson, null);
|
|
8957
|
+
const allowedDraftRedirect = await preToolUse("Bash", "tool-ralplan-draft-redirect-allow", {
|
|
8958
|
+
command: "printf '# Draft\\n' > .omx/drafts/issue-3105.md",
|
|
8959
|
+
});
|
|
8960
|
+
assert.equal(allowedDraftRedirect.outputJson, null);
|
|
8546
8961
|
const allowedTee = await preToolUse("Bash", "tool-ralplan-tee-allow", {
|
|
8547
8962
|
command: "printf '\\nmore\\n' | tee -a .omx/specs/issue-2863.md",
|
|
8548
8963
|
});
|
|
@@ -8598,6 +9013,34 @@ exit 0
|
|
|
8598
9013
|
const editReason = String(blockedEdit.outputJson?.reason ?? "");
|
|
8599
9014
|
assert.match(editReason, /Edit path/);
|
|
8600
9015
|
assert.match(editReason, /src\/implementation\.ts/);
|
|
9016
|
+
const blockedEditContext = String(blockedEdit.outputJson
|
|
9017
|
+
?.hookSpecificOutput?.additionalContext ?? "");
|
|
9018
|
+
assert.match(blockedEditContext, /Markdown drafts under `\.omx\/drafts\/\*\.md`/);
|
|
9019
|
+
for (const allowedCategory of [
|
|
9020
|
+
".omx/context/",
|
|
9021
|
+
".omx/plans/",
|
|
9022
|
+
".omx/specs/",
|
|
9023
|
+
".omx/tmp/",
|
|
9024
|
+
".omx/state/",
|
|
9025
|
+
".beads/",
|
|
9026
|
+
]) {
|
|
9027
|
+
assert.match(blockedEditContext, new RegExp(allowedCategory.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
|
|
9028
|
+
}
|
|
9029
|
+
for (const blockedDraftPath of [
|
|
9030
|
+
".omx/drafts/issue-3105.MD",
|
|
9031
|
+
".omx/Drafts/issue-3105.md",
|
|
9032
|
+
".omx/drafts/issue-3105",
|
|
9033
|
+
".omx/drafts/run.sh",
|
|
9034
|
+
".omx/drafts/subdir/issue-3105.md",
|
|
9035
|
+
".omx/drafts/../../src/leak.ts",
|
|
9036
|
+
"/tmp/issue-3105.md",
|
|
9037
|
+
]) {
|
|
9038
|
+
const blockedDraft = await preToolUse("Write", `tool-ralplan-draft-block-${blockedDraftPath}`, {
|
|
9039
|
+
file_path: blockedDraftPath,
|
|
9040
|
+
content: "bad",
|
|
9041
|
+
});
|
|
9042
|
+
assert.equal(blockedDraft.outputJson?.decision, "block", `draft path should be blocked: ${blockedDraftPath}`);
|
|
9043
|
+
}
|
|
8601
9044
|
const blockedMixedPatch = await preToolUse("apply_patch", "tool-ralplan-patch-mixed", {
|
|
8602
9045
|
input: "*** Begin Patch\n*** Add File: .omx/plans/ok.md\n+ok\n*** Add File: src/leak.ts\n+leak\n*** End Patch\n",
|
|
8603
9046
|
});
|
|
@@ -8605,6 +9048,16 @@ exit 0
|
|
|
8605
9048
|
const mixedReason = String(blockedMixedPatch.outputJson?.reason ?? "");
|
|
8606
9049
|
assert.match(mixedReason, /apply_patch target/);
|
|
8607
9050
|
assert.match(mixedReason, /src\/leak\.ts/);
|
|
9051
|
+
const blockedMixedDraftPatch = await preToolUse("apply_patch", "tool-ralplan-draft-patch-mixed", {
|
|
9052
|
+
input: "*** Begin Patch\n*** Add File: .omx/drafts/issue-3105.md\n+ok\n*** Add File: src/leak.ts\n+leak\n*** End Patch\n",
|
|
9053
|
+
});
|
|
9054
|
+
assert.equal(blockedMixedDraftPatch.outputJson?.decision, "block");
|
|
9055
|
+
assert.match(String(blockedMixedDraftPatch.outputJson?.reason ?? ""), /src\/leak\.ts/);
|
|
9056
|
+
const blockedMixedDraftBash = await preToolUse("Bash", "tool-ralplan-draft-bash-mixed", {
|
|
9057
|
+
command: "printf '# Draft\\n' > .omx/drafts/issue-3105.md; printf 'bad\\n' > src/leak.ts",
|
|
9058
|
+
});
|
|
9059
|
+
assert.equal(blockedMixedDraftBash.outputJson?.decision, "block");
|
|
9060
|
+
assert.match(String(blockedMixedDraftBash.outputJson?.reason ?? ""), /src\/leak\.ts/);
|
|
8608
9061
|
const blockedUnparseablePatch = await preToolUse("apply_patch", "tool-ralplan-patch-unparseable", {
|
|
8609
9062
|
input: "not a recognizable patch",
|
|
8610
9063
|
});
|
|
@@ -8617,6 +9070,11 @@ exit 0
|
|
|
8617
9070
|
const unresolvedReason = String(blockedUnresolvedRedirect.outputJson?.reason ?? "");
|
|
8618
9071
|
assert.match(unresolvedReason, /unresolved Bash write target/);
|
|
8619
9072
|
assert.match(unresolvedReason, /\$PLAN_PATH/);
|
|
9073
|
+
const blockedUnresolvedDraftRedirect = await preToolUse("Bash", "tool-ralplan-draft-redirect-unresolved", {
|
|
9074
|
+
command: "cat > \"$DRAFT_PATH\" <<'EOF'\ncontent\nEOF",
|
|
9075
|
+
});
|
|
9076
|
+
assert.equal(blockedUnresolvedDraftRedirect.outputJson?.decision, "block");
|
|
9077
|
+
assert.match(String(blockedUnresolvedDraftRedirect.outputJson?.reason ?? ""), /\$DRAFT_PATH/);
|
|
8620
9078
|
const blockedTraversal = await preToolUse("Write", "tool-ralplan-traversal-block", {
|
|
8621
9079
|
file_path: ".omx/plans/../../src/leak.ts",
|
|
8622
9080
|
content: "bad",
|
|
@@ -9207,29 +9665,182 @@ PY`,
|
|
|
9207
9665
|
await rm(cwd, { recursive: true, force: true });
|
|
9208
9666
|
}
|
|
9209
9667
|
});
|
|
9210
|
-
it("
|
|
9211
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-
|
|
9668
|
+
it("keeps untyped collaboration child implementation writes blocked under active ralplan planning while typed trusted agents pass (#3116)", async () => {
|
|
9669
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3116-ralplan-untyped-"));
|
|
9670
|
+
process.env.OMX_ROOT = cwd;
|
|
9212
9671
|
try {
|
|
9213
9672
|
const stateDir = join(cwd, ".omx", "state");
|
|
9214
|
-
const
|
|
9673
|
+
const sessionId = "sess-3116-ralplan-untyped";
|
|
9674
|
+
const leaderThreadId = "thread-3116-ralplan-untyped-leader";
|
|
9675
|
+
const childThreadId = "thread-3116-ralplan-untyped-child";
|
|
9676
|
+
const sessionDir = join(stateDir, "sessions", sessionId);
|
|
9677
|
+
const nowIso = new Date().toISOString();
|
|
9215
9678
|
await mkdir(sessionDir, { recursive: true });
|
|
9216
|
-
await writeJson(join(stateDir, "session.json"), { session_id:
|
|
9679
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId, cwd });
|
|
9217
9680
|
await writeJson(join(sessionDir, "skill-active-state.json"), {
|
|
9218
9681
|
version: 1,
|
|
9219
9682
|
active: true,
|
|
9220
|
-
skill: "
|
|
9221
|
-
phase: "
|
|
9222
|
-
session_id:
|
|
9223
|
-
|
|
9683
|
+
skill: "autopilot",
|
|
9684
|
+
phase: "ralplan",
|
|
9685
|
+
session_id: sessionId,
|
|
9686
|
+
thread_id: leaderThreadId,
|
|
9687
|
+
active_skills: [
|
|
9688
|
+
{ skill: "autopilot", phase: "ralplan", active: true, session_id: sessionId, thread_id: leaderThreadId },
|
|
9689
|
+
],
|
|
9224
9690
|
});
|
|
9225
|
-
await writeJson(join(sessionDir, "
|
|
9691
|
+
await writeJson(join(sessionDir, "autopilot-state.json"), {
|
|
9226
9692
|
active: true,
|
|
9227
|
-
mode: "
|
|
9228
|
-
current_phase: "
|
|
9229
|
-
session_id:
|
|
9230
|
-
|
|
9231
|
-
|
|
9232
|
-
|
|
9693
|
+
mode: "autopilot",
|
|
9694
|
+
current_phase: "ralplan",
|
|
9695
|
+
session_id: sessionId,
|
|
9696
|
+
thread_id: leaderThreadId,
|
|
9697
|
+
});
|
|
9698
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
9699
|
+
schemaVersion: 1,
|
|
9700
|
+
sessions: {
|
|
9701
|
+
[sessionId]: {
|
|
9702
|
+
session_id: sessionId,
|
|
9703
|
+
leader_thread_id: leaderThreadId,
|
|
9704
|
+
updated_at: nowIso,
|
|
9705
|
+
threads: {
|
|
9706
|
+
[leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
|
|
9707
|
+
[childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1, leader_thread_id: leaderThreadId },
|
|
9708
|
+
},
|
|
9709
|
+
},
|
|
9710
|
+
},
|
|
9711
|
+
});
|
|
9712
|
+
// Untyped tracked/spawn-provenance child must NOT bypass the ralplan planning guard.
|
|
9713
|
+
const untypedChild = await dispatchCodexNativeHook({
|
|
9714
|
+
hook_event_name: "PreToolUse",
|
|
9715
|
+
cwd,
|
|
9716
|
+
session_id: sessionId,
|
|
9717
|
+
thread_id: childThreadId,
|
|
9718
|
+
agent_role: "collaboration-child",
|
|
9719
|
+
source: {
|
|
9720
|
+
subagent: { thread_spawn: { parent_thread_id: leaderThreadId } },
|
|
9721
|
+
},
|
|
9722
|
+
tool_name: "Edit",
|
|
9723
|
+
tool_input: { file_path: "src/implementation.ts", old_string: "a", new_string: "b" },
|
|
9724
|
+
}, { cwd });
|
|
9725
|
+
assert.equal(untypedChild.outputJson?.decision, "block");
|
|
9726
|
+
// A typed trusted agent retains its existing planning exemption.
|
|
9727
|
+
const typedChild = await dispatchCodexNativeHook({
|
|
9728
|
+
hook_event_name: "PreToolUse",
|
|
9729
|
+
cwd,
|
|
9730
|
+
session_id: sessionId,
|
|
9731
|
+
thread_id: childThreadId,
|
|
9732
|
+
agent_role: "executor",
|
|
9733
|
+
source: {
|
|
9734
|
+
subagent: { thread_spawn: { parent_thread_id: leaderThreadId } },
|
|
9735
|
+
},
|
|
9736
|
+
tool_name: "Edit",
|
|
9737
|
+
tool_input: { file_path: "src/implementation.ts", old_string: "a", new_string: "b" },
|
|
9738
|
+
}, { cwd });
|
|
9739
|
+
assert.equal(typedChild.outputJson, null);
|
|
9740
|
+
}
|
|
9741
|
+
finally {
|
|
9742
|
+
await rm(cwd, { recursive: true, force: true });
|
|
9743
|
+
}
|
|
9744
|
+
});
|
|
9745
|
+
it("keeps untyped collaboration child implementation writes blocked under active deep-interview planning while typed trusted agents pass (#3116)", async () => {
|
|
9746
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3116-di-untyped-"));
|
|
9747
|
+
process.env.OMX_ROOT = cwd;
|
|
9748
|
+
try {
|
|
9749
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
9750
|
+
const sessionId = "sess-3116-di-untyped";
|
|
9751
|
+
const leaderThreadId = "thread-3116-di-untyped-leader";
|
|
9752
|
+
const childThreadId = "thread-3116-di-untyped-child";
|
|
9753
|
+
const sessionDir = join(stateDir, "sessions", sessionId);
|
|
9754
|
+
const nowIso = new Date().toISOString();
|
|
9755
|
+
await mkdir(sessionDir, { recursive: true });
|
|
9756
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId, cwd });
|
|
9757
|
+
await writeJson(join(sessionDir, "skill-active-state.json"), {
|
|
9758
|
+
version: 1,
|
|
9759
|
+
active: true,
|
|
9760
|
+
skill: "deep-interview",
|
|
9761
|
+
phase: "planning",
|
|
9762
|
+
session_id: sessionId,
|
|
9763
|
+
thread_id: leaderThreadId,
|
|
9764
|
+
active_skills: [
|
|
9765
|
+
{ skill: "deep-interview", phase: "planning", active: true, session_id: sessionId, thread_id: leaderThreadId },
|
|
9766
|
+
],
|
|
9767
|
+
});
|
|
9768
|
+
await writeJson(join(sessionDir, "deep-interview-state.json"), {
|
|
9769
|
+
active: true,
|
|
9770
|
+
mode: "deep-interview",
|
|
9771
|
+
current_phase: "intent-first",
|
|
9772
|
+
session_id: sessionId,
|
|
9773
|
+
});
|
|
9774
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
9775
|
+
schemaVersion: 1,
|
|
9776
|
+
sessions: {
|
|
9777
|
+
[sessionId]: {
|
|
9778
|
+
session_id: sessionId,
|
|
9779
|
+
leader_thread_id: leaderThreadId,
|
|
9780
|
+
updated_at: nowIso,
|
|
9781
|
+
threads: {
|
|
9782
|
+
[leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
|
|
9783
|
+
[childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1, leader_thread_id: leaderThreadId },
|
|
9784
|
+
},
|
|
9785
|
+
},
|
|
9786
|
+
},
|
|
9787
|
+
});
|
|
9788
|
+
// Untyped tracked/spawn-provenance child must NOT bypass the deep-interview planning guard.
|
|
9789
|
+
const untypedChild = await dispatchCodexNativeHook({
|
|
9790
|
+
hook_event_name: "PreToolUse",
|
|
9791
|
+
cwd,
|
|
9792
|
+
session_id: sessionId,
|
|
9793
|
+
thread_id: childThreadId,
|
|
9794
|
+
agent_role: "collaboration-child",
|
|
9795
|
+
source: {
|
|
9796
|
+
subagent: { thread_spawn: { parent_thread_id: leaderThreadId } },
|
|
9797
|
+
},
|
|
9798
|
+
tool_name: "Edit",
|
|
9799
|
+
tool_input: { file_path: "src/implementation.ts", old_string: "a", new_string: "b" },
|
|
9800
|
+
}, { cwd });
|
|
9801
|
+
assert.equal(untypedChild.outputJson?.decision, "block");
|
|
9802
|
+
// A typed trusted agent retains its existing planning exemption.
|
|
9803
|
+
const typedChild = await dispatchCodexNativeHook({
|
|
9804
|
+
hook_event_name: "PreToolUse",
|
|
9805
|
+
cwd,
|
|
9806
|
+
session_id: sessionId,
|
|
9807
|
+
thread_id: childThreadId,
|
|
9808
|
+
agent_role: "executor",
|
|
9809
|
+
source: {
|
|
9810
|
+
subagent: { thread_spawn: { parent_thread_id: leaderThreadId } },
|
|
9811
|
+
},
|
|
9812
|
+
tool_name: "Edit",
|
|
9813
|
+
tool_input: { file_path: "src/implementation.ts", old_string: "a", new_string: "b" },
|
|
9814
|
+
}, { cwd });
|
|
9815
|
+
assert.equal(typedChild.outputJson, null);
|
|
9816
|
+
}
|
|
9817
|
+
finally {
|
|
9818
|
+
await rm(cwd, { recursive: true, force: true });
|
|
9819
|
+
}
|
|
9820
|
+
});
|
|
9821
|
+
it("allows null-device fd redirects while deep-interview blocks real Bash writes", async () => {
|
|
9822
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-deep-interview-null-redirect-"));
|
|
9823
|
+
try {
|
|
9824
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
9825
|
+
const sessionDir = join(stateDir, "sessions", "sess-di-null-redirect");
|
|
9826
|
+
await mkdir(sessionDir, { recursive: true });
|
|
9827
|
+
await writeJson(join(stateDir, "session.json"), { session_id: "sess-di-null-redirect", cwd });
|
|
9828
|
+
await writeJson(join(sessionDir, "skill-active-state.json"), {
|
|
9829
|
+
version: 1,
|
|
9830
|
+
active: true,
|
|
9831
|
+
skill: "deep-interview",
|
|
9832
|
+
phase: "planning",
|
|
9833
|
+
session_id: "sess-di-null-redirect",
|
|
9834
|
+
active_skills: [{ skill: "deep-interview", phase: "planning", active: true, session_id: "sess-di-null-redirect" }],
|
|
9835
|
+
});
|
|
9836
|
+
await writeJson(join(sessionDir, "deep-interview-state.json"), {
|
|
9837
|
+
active: true,
|
|
9838
|
+
mode: "deep-interview",
|
|
9839
|
+
current_phase: "intent-first",
|
|
9840
|
+
session_id: "sess-di-null-redirect",
|
|
9841
|
+
});
|
|
9842
|
+
const allowedCommands = [
|
|
9843
|
+
"find application -type d -name 'bug-tracking*' 2>/dev/null | head -20",
|
|
9233
9844
|
"find application -type d -name 'bug-tracking*' 2> /dev/null | head -20",
|
|
9234
9845
|
"find application -type d -name 'bug-tracking*' 2>NUL | head -20",
|
|
9235
9846
|
"find application -type d -name 'bug-tracking*' 1>/dev/null",
|
|
@@ -9371,6 +9982,32 @@ PY`,
|
|
|
9371
9982
|
assert.equal(blocker.tool_name, "multi_agent_v1.spawn_agent");
|
|
9372
9983
|
assert.match(String(blocker.error_summary), /agent thread limit reached/);
|
|
9373
9984
|
assert.ok(Date.parse(String(blocker.expires_at)) > Date.parse(String(blocker.observed_at)));
|
|
9985
|
+
assert.equal(existsSync(join(cwd, ".omx", "state", "native-subagent-support.json")), false);
|
|
9986
|
+
}
|
|
9987
|
+
finally {
|
|
9988
|
+
await rm(cwd, { recursive: true, force: true });
|
|
9989
|
+
}
|
|
9990
|
+
});
|
|
9991
|
+
it("records unsupported native subagent support blocker from spawn_agent PostToolUse output", async () => {
|
|
9992
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-support-record-"));
|
|
9993
|
+
try {
|
|
9994
|
+
const result = await dispatchCodexNativeHook({
|
|
9995
|
+
hook_event_name: "PostToolUse",
|
|
9996
|
+
cwd,
|
|
9997
|
+
session_id: "sess-subagent-support-record",
|
|
9998
|
+
thread_id: "thread-subagent-support-record",
|
|
9999
|
+
turn_id: "turn-subagent-support-record",
|
|
10000
|
+
tool_name: "multi_agent_v1.spawn_agent",
|
|
10001
|
+
tool_response: { error: "unknown tool: multi_agent_v1.spawn_agent is unavailable" },
|
|
10002
|
+
}, { cwd });
|
|
10003
|
+
assert.equal(result.omxEventName, "post-tool-use");
|
|
10004
|
+
const blocker = JSON.parse(await readFile(join(cwd, ".omx", "state", "native-subagent-support.json"), "utf-8"));
|
|
10005
|
+
assert.equal(blocker.status, "unsupported");
|
|
10006
|
+
assert.equal(blocker.reason, "multi_agent_v1_unavailable");
|
|
10007
|
+
assert.equal(blocker.session_id, "sess-subagent-support-record");
|
|
10008
|
+
assert.equal(blocker.thread_id, "thread-subagent-support-record");
|
|
10009
|
+
assert.equal(blocker.tool_name, "multi_agent_v1.spawn_agent");
|
|
10010
|
+
assert.match(String(blocker.evidence), /unknown tool/);
|
|
9374
10011
|
}
|
|
9375
10012
|
finally {
|
|
9376
10013
|
await rm(cwd, { recursive: true, force: true });
|
|
@@ -14751,17 +15388,19 @@ PY`,
|
|
|
14751
15388
|
execFileSync("git", ["add", "src/scripts/codex-native-hook.ts"], { cwd, stdio: "ignore" });
|
|
14752
15389
|
execFileSync("git", ["commit", "-m", "init"], { cwd, stdio: "ignore" });
|
|
14753
15390
|
await writeFile(join(cwd, "src", "scripts", "codex-native-hook.ts"), "export const hook = 2;\n", "utf-8");
|
|
14754
|
-
const
|
|
15391
|
+
const output = parseSingleJsonStdout(runNativeHookCli({
|
|
14755
15392
|
hook_event_name: "Stop",
|
|
14756
15393
|
cwd,
|
|
14757
15394
|
session_id: "sess-stop-doc-refresh",
|
|
14758
15395
|
last_assistant_message: "Launch-ready: yes",
|
|
14759
|
-
}, { cwd });
|
|
14760
|
-
assert.
|
|
14761
|
-
assert.equal(
|
|
14762
|
-
assert.equal(
|
|
14763
|
-
assert.
|
|
14764
|
-
assert.
|
|
15396
|
+
}, { cwd }));
|
|
15397
|
+
assert.deepEqual(Object.keys(output).sort(), ["systemMessage"]);
|
|
15398
|
+
assert.equal("hookSpecificOutput" in output, false);
|
|
15399
|
+
assert.equal("decision" in output, false);
|
|
15400
|
+
assert.equal("reason" in output, false);
|
|
15401
|
+
assert.equal("stopReason" in output, false);
|
|
15402
|
+
assert.match(String(output.systemMessage ?? ""), /Document-refresh warning/);
|
|
15403
|
+
assert.match(String(output.systemMessage ?? ""), /staged \+ unstaged changes/);
|
|
14765
15404
|
}
|
|
14766
15405
|
finally {
|
|
14767
15406
|
await rm(cwd, { recursive: true, force: true });
|
|
@@ -17270,6 +17909,17 @@ PY`,
|
|
|
17270
17909
|
}, { cwd });
|
|
17271
17910
|
assert.equal(result.omxEventName, "pre-tool-use");
|
|
17272
17911
|
assert.equal(result.outputJson, null);
|
|
17912
|
+
const allowedDraftPatch = await dispatchCodexNativeHook({
|
|
17913
|
+
hook_event_name: "PreToolUse",
|
|
17914
|
+
cwd,
|
|
17915
|
+
session_id: nativeSessionId,
|
|
17916
|
+
thread_id: "thread-ralplan-native-map-draft-artifact",
|
|
17917
|
+
tool_name: "apply_patch",
|
|
17918
|
+
tool_input: {
|
|
17919
|
+
input: `*** Begin Patch\n*** Add File: ${join(cwd, ".omx", "drafts", "issue-3105.md")}\n+# Draft\n*** End Patch\n`,
|
|
17920
|
+
},
|
|
17921
|
+
}, { cwd });
|
|
17922
|
+
assert.equal(allowedDraftPatch.outputJson, null);
|
|
17273
17923
|
}
|
|
17274
17924
|
finally {
|
|
17275
17925
|
await rm(cwd, { recursive: true, force: true });
|
|
@@ -17377,6 +18027,17 @@ PY`,
|
|
|
17377
18027
|
session_id: ownerSessionId,
|
|
17378
18028
|
cwd: ownerCwd,
|
|
17379
18029
|
});
|
|
18030
|
+
const allowedForeignDraft = await dispatchCodexNativeHook({
|
|
18031
|
+
hook_event_name: "PreToolUse",
|
|
18032
|
+
cwd,
|
|
18033
|
+
session_id: "019e-ralplan-live-root-unresolved-current",
|
|
18034
|
+
thread_id: "thread-ralplan-live-root-conflict",
|
|
18035
|
+
tool_name: "apply_patch",
|
|
18036
|
+
tool_input: {
|
|
18037
|
+
input: `*** Begin Patch\n*** Add File: ${join(cwd, ".omx", "drafts", "issue-3105.md")}\n+# Draft\n*** End Patch\n`,
|
|
18038
|
+
},
|
|
18039
|
+
}, { cwd });
|
|
18040
|
+
assert.equal(allowedForeignDraft.outputJson, null);
|
|
17380
18041
|
const result = await dispatchCodexNativeHook({
|
|
17381
18042
|
hook_event_name: "PreToolUse",
|
|
17382
18043
|
cwd,
|
|
@@ -17449,6 +18110,17 @@ PY`,
|
|
|
17449
18110
|
}, { cwd });
|
|
17450
18111
|
assert.equal(result.omxEventName, "pre-tool-use");
|
|
17451
18112
|
assert.equal(result.outputJson, null);
|
|
18113
|
+
const allowedDraftPatch = await dispatchCodexNativeHook({
|
|
18114
|
+
hook_event_name: "PreToolUse",
|
|
18115
|
+
cwd,
|
|
18116
|
+
session_id: nativeSessionId,
|
|
18117
|
+
thread_id: "thread-ralplan-live-root-owner-draft-pass",
|
|
18118
|
+
tool_name: "apply_patch",
|
|
18119
|
+
tool_input: {
|
|
18120
|
+
input: `*** Begin Patch\n*** Add File: ${join(cwd, ".omx", "drafts", "live-root-owner.md")}\n+# Draft\n*** End Patch\n`,
|
|
18121
|
+
},
|
|
18122
|
+
}, { cwd });
|
|
18123
|
+
assert.equal(allowedDraftPatch.outputJson, null);
|
|
17452
18124
|
}
|
|
17453
18125
|
finally {
|
|
17454
18126
|
await rm(cwd, { recursive: true, force: true });
|
|
@@ -17768,6 +18440,157 @@ PY`,
|
|
|
17768
18440
|
await rm(cwd, { recursive: true, force: true });
|
|
17769
18441
|
}
|
|
17770
18442
|
});
|
|
18443
|
+
it("blocks unsupported active conductor source edits with native delegation recovery guidance", async () => {
|
|
18444
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-unsupported-conductor-source-"));
|
|
18445
|
+
try {
|
|
18446
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
18447
|
+
const sessionId = "sess-unsupported-conductor-source";
|
|
18448
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
18449
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
18450
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "ultragoal", "planning");
|
|
18451
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ultragoal-state.json"), {
|
|
18452
|
+
active: true,
|
|
18453
|
+
mode: "ultragoal",
|
|
18454
|
+
current_phase: "planning",
|
|
18455
|
+
session_id: sessionId,
|
|
18456
|
+
});
|
|
18457
|
+
await writeJson(join(stateDir, "native-subagent-support.json"), {
|
|
18458
|
+
schema_version: 1,
|
|
18459
|
+
status: "unsupported",
|
|
18460
|
+
reason: "multi_agent_v1_unavailable",
|
|
18461
|
+
session_id: sessionId,
|
|
18462
|
+
evidence: "unknown tool: multi_agent_v1.spawn_agent",
|
|
18463
|
+
observed_at: new Date().toISOString(),
|
|
18464
|
+
cwd,
|
|
18465
|
+
});
|
|
18466
|
+
const result = await dispatchCodexNativeHook({
|
|
18467
|
+
hook_event_name: "PreToolUse",
|
|
18468
|
+
cwd,
|
|
18469
|
+
session_id: sessionId,
|
|
18470
|
+
thread_id: "thread-unsupported-conductor-source",
|
|
18471
|
+
tool_name: "Write",
|
|
18472
|
+
tool_input: { file_path: "src/runtime.ts", content: "export const value = 1;\n" },
|
|
18473
|
+
}, { cwd });
|
|
18474
|
+
const output = result.outputJson;
|
|
18475
|
+
const context = String(output?.hookSpecificOutput?.additionalContext ?? "");
|
|
18476
|
+
assert.equal(output?.decision, "block");
|
|
18477
|
+
assert.match(String(result.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ultragoal phase: planning\)/);
|
|
18478
|
+
assert.match(context, /Native subagent support is unavailable in this environment/);
|
|
18479
|
+
assert.match(context, /Reason: multi_agent_v1_unavailable/);
|
|
18480
|
+
assert.match(context, /blocked\/cancelled/);
|
|
18481
|
+
assert.match(context, /do not call multi_agent_v1\.close_agent/i);
|
|
18482
|
+
}
|
|
18483
|
+
finally {
|
|
18484
|
+
await rm(cwd, { recursive: true, force: true });
|
|
18485
|
+
}
|
|
18486
|
+
});
|
|
18487
|
+
it("keeps the Conductor unblocked: quoted redirect/source text is not a write target and terminal blocked-state writes survive genuinely unsupported native delegation (#3119)", async () => {
|
|
18488
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-conductor-3119-quote-deadlock-"));
|
|
18489
|
+
try {
|
|
18490
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
18491
|
+
const sessionId = "sess-conductor-3119-quote-deadlock";
|
|
18492
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
18493
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
18494
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "ultragoal", "planning");
|
|
18495
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ultragoal-state.json"), {
|
|
18496
|
+
active: true,
|
|
18497
|
+
mode: "ultragoal",
|
|
18498
|
+
current_phase: "planning",
|
|
18499
|
+
session_id: sessionId,
|
|
18500
|
+
});
|
|
18501
|
+
// Native delegation is genuinely unsupported (explicit negative evidence).
|
|
18502
|
+
await writeJson(join(stateDir, "native-subagent-support.json"), {
|
|
18503
|
+
schema_version: 1,
|
|
18504
|
+
status: "unsupported",
|
|
18505
|
+
reason: "multi_agent_v1_unavailable",
|
|
18506
|
+
session_id: sessionId,
|
|
18507
|
+
evidence: "unknown tool: multi_agent_v1.spawn_agent",
|
|
18508
|
+
observed_at: new Date().toISOString(),
|
|
18509
|
+
cwd,
|
|
18510
|
+
});
|
|
18511
|
+
const dispatch = (command) => dispatchCodexNativeHook({
|
|
18512
|
+
hook_event_name: "PreToolUse",
|
|
18513
|
+
cwd,
|
|
18514
|
+
session_id: sessionId,
|
|
18515
|
+
thread_id: "thread-conductor-3119-quote-deadlock",
|
|
18516
|
+
tool_name: "Bash",
|
|
18517
|
+
tool_input: { command },
|
|
18518
|
+
}, { cwd });
|
|
18519
|
+
// Deadlock prevention (defect B): the Conductor must still terminalize its
|
|
18520
|
+
// own workflow state even when delegation is genuinely unsupported, even
|
|
18521
|
+
// when the JSON payload contains a `>` character.
|
|
18522
|
+
const terminalBlockedWrite = await dispatch("omx state write --mode ultragoal --input '{\"active\":true,\"current_phase\":\"blocked\",\"reason\":\"native delegation unavailable -> terminalized\"}' --json");
|
|
18523
|
+
assert.notEqual(terminalBlockedWrite.outputJson?.decision, "block");
|
|
18524
|
+
// Defect C: quoted regex/source text with redirect metacharacters is not a
|
|
18525
|
+
// write target, so issue creation is not falsely blocked.
|
|
18526
|
+
const issueCreate = await dispatch("gh issue create --title x --body 'Guard misparsed regex /[^>]+>{1,2}/ as a redirect target'");
|
|
18527
|
+
assert.notEqual(issueCreate.outputJson?.decision, "block");
|
|
18528
|
+
// Fail-closed preserved: a REAL unquoted redirect to a non-metadata path is
|
|
18529
|
+
// still blocked.
|
|
18530
|
+
const realRedirect = await dispatch("printf pwn > src/runtime.ts");
|
|
18531
|
+
assert.equal(realRedirect.outputJson?.decision, "block");
|
|
18532
|
+
assert.match(String(realRedirect.outputJson?.reason ?? ""), /not workflow state\/ledger\/mailbox\/handoff metadata/);
|
|
18533
|
+
// A real unquoted redirect to allowed workflow metadata still passes.
|
|
18534
|
+
const metadataRedirect = await dispatch("printf blocked > .omx/state/conductor.log");
|
|
18535
|
+
assert.notEqual(metadataRedirect.outputJson?.decision, "block");
|
|
18536
|
+
}
|
|
18537
|
+
finally {
|
|
18538
|
+
await rm(cwd, { recursive: true, force: true });
|
|
18539
|
+
}
|
|
18540
|
+
});
|
|
18541
|
+
it("fail-closed: escaped-quote and ANSI-C quoted redirects to source stay blocked while legit quoted data and nested shells behave (#3119)", async () => {
|
|
18542
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-conductor-3119-escaped-quote-"));
|
|
18543
|
+
try {
|
|
18544
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
18545
|
+
const sessionId = "sess-conductor-3119-escaped-quote";
|
|
18546
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
18547
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
18548
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "ultragoal", "planning");
|
|
18549
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ultragoal-state.json"), {
|
|
18550
|
+
active: true,
|
|
18551
|
+
mode: "ultragoal",
|
|
18552
|
+
current_phase: "planning",
|
|
18553
|
+
session_id: sessionId,
|
|
18554
|
+
});
|
|
18555
|
+
const dispatch = (command) => dispatchCodexNativeHook({
|
|
18556
|
+
hook_event_name: "PreToolUse",
|
|
18557
|
+
cwd,
|
|
18558
|
+
session_id: sessionId,
|
|
18559
|
+
thread_id: "thread-conductor-3119-escaped-quote",
|
|
18560
|
+
tool_name: "Bash",
|
|
18561
|
+
tool_input: { command },
|
|
18562
|
+
}, { cwd });
|
|
18563
|
+
// A top-level escaped quote (\' \") is a LITERAL char in bash, not a quote
|
|
18564
|
+
// opener, and $'...' is ANSI-C quoting. None of these may hide the real
|
|
18565
|
+
// `>` redirect \u2014 all of these genuinely write src/runtime.ts and MUST block.
|
|
18566
|
+
const mustBlock = [
|
|
18567
|
+
"printf pwn > src/runtime.ts", // plain control
|
|
18568
|
+
"printf pwn \\' > src/runtime.ts \\'", // escaped single quote
|
|
18569
|
+
"printf pwn \\\" > src/runtime.ts \\\"", // escaped double quote
|
|
18570
|
+
"printf pwn $'\\'' > src/runtime.ts $'\\''", // ANSI-C escaped quote
|
|
18571
|
+
"bash -c 'printf pwn > src/runtime.ts'", // nested shell redirect
|
|
18572
|
+
];
|
|
18573
|
+
for (const command of mustBlock) {
|
|
18574
|
+
const result = await dispatch(command);
|
|
18575
|
+
assert.equal(result.outputJson?.decision, "block", command);
|
|
18576
|
+
assert.match(String(result.outputJson?.reason ?? ""), /not workflow state\/ledger\/mailbox\/handoff metadata/, command);
|
|
18577
|
+
}
|
|
18578
|
+
// Legitimate quoted DATA metacharacters (single quotes, double quotes,
|
|
18579
|
+
// ANSI-C) are not redirects and must not be falsely blocked.
|
|
18580
|
+
const mustNotBlock = [
|
|
18581
|
+
"gh issue create --title x --body 'a > b regex /[^>]+>{1,2}/'",
|
|
18582
|
+
"echo \"value > threshold\"",
|
|
18583
|
+
"printf $'a>b'",
|
|
18584
|
+
];
|
|
18585
|
+
for (const command of mustNotBlock) {
|
|
18586
|
+
const result = await dispatch(command);
|
|
18587
|
+
assert.notEqual(result.outputJson?.decision, "block", command);
|
|
18588
|
+
}
|
|
18589
|
+
}
|
|
18590
|
+
finally {
|
|
18591
|
+
await rm(cwd, { recursive: true, force: true });
|
|
18592
|
+
}
|
|
18593
|
+
});
|
|
17771
18594
|
it("blocks Main-root ralplan writes even when payload has only a typed agent_role", async () => {
|
|
17772
18595
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-agent-role-main-"));
|
|
17773
18596
|
try {
|
|
@@ -18026,15 +18849,248 @@ PY`,
|
|
|
18026
18849
|
await rm(cwd, { recursive: true, force: true });
|
|
18027
18850
|
}
|
|
18028
18851
|
});
|
|
18029
|
-
it("blocks
|
|
18030
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-
|
|
18031
|
-
|
|
18032
|
-
const originalOmxStateRoot = process.env.OMX_STATE_ROOT;
|
|
18033
|
-
const originalOmxTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
|
|
18852
|
+
it("blocks a corrupt kind:subagent leader that omits leader_thread_id via native session identity while still trusting a real non-leader child (#3117 P2)", async () => {
|
|
18853
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3117-corrupt-leader-no-lead-"));
|
|
18854
|
+
process.env.OMX_ROOT = cwd;
|
|
18034
18855
|
try {
|
|
18035
|
-
|
|
18036
|
-
|
|
18037
|
-
|
|
18856
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
18857
|
+
const sessionId = "sess-3117-corrupt-leader-no-lead";
|
|
18858
|
+
const leaderThreadId = "thread-3117-corrupt-leader-no-lead-leader";
|
|
18859
|
+
const childThreadId = "thread-3117-corrupt-leader-no-lead-child";
|
|
18860
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
18861
|
+
// native_session_id anchors the leader identity even though the tracker omits
|
|
18862
|
+
// leader_thread_id and corruptly labels the leader kind:"subagent".
|
|
18863
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId });
|
|
18864
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
|
|
18865
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
|
|
18866
|
+
active: true,
|
|
18867
|
+
mode: "ralph",
|
|
18868
|
+
current_phase: "executing",
|
|
18869
|
+
session_id: sessionId,
|
|
18870
|
+
});
|
|
18871
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
18872
|
+
schemaVersion: 1,
|
|
18873
|
+
sessions: {
|
|
18874
|
+
[sessionId]: {
|
|
18875
|
+
session_id: sessionId,
|
|
18876
|
+
threads: {
|
|
18877
|
+
[leaderThreadId]: { thread_id: leaderThreadId, kind: "subagent", mode: "collaboration-child" },
|
|
18878
|
+
[childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child" },
|
|
18879
|
+
},
|
|
18880
|
+
},
|
|
18881
|
+
},
|
|
18882
|
+
});
|
|
18883
|
+
// Untyped leader payload must stay blocked: leader identity is anchored to
|
|
18884
|
+
// native_session_id, not the corrupt tracker kind.
|
|
18885
|
+
const untypedLeader = await dispatchCodexNativeHook({
|
|
18886
|
+
hook_event_name: "PreToolUse",
|
|
18887
|
+
cwd,
|
|
18888
|
+
session_id: sessionId,
|
|
18889
|
+
thread_id: leaderThreadId,
|
|
18890
|
+
tool_name: "apply_patch",
|
|
18891
|
+
tool_input: { file_path: "src/feature.ts" },
|
|
18892
|
+
}, { cwd });
|
|
18893
|
+
assert.equal(untypedLeader.outputJson?.decision, "block");
|
|
18894
|
+
assert.match(String(untypedLeader.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
|
|
18895
|
+
// Control: a genuine non-leader child stays trusted (P1 preserved).
|
|
18896
|
+
const untypedChild = await dispatchCodexNativeHook({
|
|
18897
|
+
hook_event_name: "PreToolUse",
|
|
18898
|
+
cwd,
|
|
18899
|
+
session_id: sessionId,
|
|
18900
|
+
thread_id: childThreadId,
|
|
18901
|
+
tool_name: "apply_patch",
|
|
18902
|
+
tool_input: { file_path: "src/feature.ts" },
|
|
18903
|
+
}, { cwd });
|
|
18904
|
+
assert.equal(untypedChild.outputJson, null);
|
|
18905
|
+
}
|
|
18906
|
+
finally {
|
|
18907
|
+
await rm(cwd, { recursive: true, force: true });
|
|
18908
|
+
}
|
|
18909
|
+
});
|
|
18910
|
+
it("fails closed for untyped provenance when no authoritative leader anchor exists (#3117 P2)", async () => {
|
|
18911
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3117-no-leader-anchor-"));
|
|
18912
|
+
process.env.OMX_ROOT = cwd;
|
|
18913
|
+
try {
|
|
18914
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
18915
|
+
const sessionId = "sess-3117-no-leader-anchor";
|
|
18916
|
+
const leaderThreadId = "thread-3117-no-leader-anchor-leader";
|
|
18917
|
+
const childThreadId = "thread-3117-no-leader-anchor-child";
|
|
18918
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
18919
|
+
// No native_session_id / owner ids and no tracker leader_thread_id: the leader is
|
|
18920
|
+
// unidentifiable, so untyped provenance must not be inferred from kind:"subagent".
|
|
18921
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
18922
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
|
|
18923
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
|
|
18924
|
+
active: true,
|
|
18925
|
+
mode: "ralph",
|
|
18926
|
+
current_phase: "executing",
|
|
18927
|
+
session_id: sessionId,
|
|
18928
|
+
});
|
|
18929
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
18930
|
+
schemaVersion: 1,
|
|
18931
|
+
sessions: {
|
|
18932
|
+
[sessionId]: {
|
|
18933
|
+
session_id: sessionId,
|
|
18934
|
+
threads: {
|
|
18935
|
+
[leaderThreadId]: { thread_id: leaderThreadId, kind: "subagent", mode: "collaboration-child" },
|
|
18936
|
+
[childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child" },
|
|
18937
|
+
},
|
|
18938
|
+
},
|
|
18939
|
+
},
|
|
18940
|
+
});
|
|
18941
|
+
for (const threadId of [leaderThreadId, childThreadId]) {
|
|
18942
|
+
const result = await dispatchCodexNativeHook({
|
|
18943
|
+
hook_event_name: "PreToolUse",
|
|
18944
|
+
cwd,
|
|
18945
|
+
session_id: sessionId,
|
|
18946
|
+
thread_id: threadId,
|
|
18947
|
+
tool_name: "apply_patch",
|
|
18948
|
+
tool_input: { file_path: "src/feature.ts" },
|
|
18949
|
+
}, { cwd });
|
|
18950
|
+
assert.equal(result.outputJson?.decision, "block", threadId);
|
|
18951
|
+
assert.match(String(result.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
|
|
18952
|
+
}
|
|
18953
|
+
}
|
|
18954
|
+
finally {
|
|
18955
|
+
await rm(cwd, { recursive: true, force: true });
|
|
18956
|
+
}
|
|
18957
|
+
});
|
|
18958
|
+
it("does not borrow leader anchors from a foreign root session.json when evaluating another session (#3117 P3)", async () => {
|
|
18959
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3117-cross-session-anchor-"));
|
|
18960
|
+
process.env.OMX_ROOT = cwd;
|
|
18961
|
+
try {
|
|
18962
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
18963
|
+
const foreignSessionId = "sess-3117-p3-foreign-A";
|
|
18964
|
+
const foreignLeaderThreadId = "thread-3117-p3-A-leader";
|
|
18965
|
+
const sessionId = "sess-3117-p3-checked-B";
|
|
18966
|
+
const leaderThreadId = "thread-3117-p3-B-leader";
|
|
18967
|
+
const childThreadId = "thread-3117-p3-B-child";
|
|
18968
|
+
const sessionPath = join(stateDir, "session.json");
|
|
18969
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
18970
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
|
|
18971
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
|
|
18972
|
+
active: true,
|
|
18973
|
+
mode: "ralph",
|
|
18974
|
+
current_phase: "executing",
|
|
18975
|
+
session_id: sessionId,
|
|
18976
|
+
});
|
|
18977
|
+
// Evaluated session B has a corrupt tracker: no leader_thread_id, leader mislabeled kind:"subagent".
|
|
18978
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
18979
|
+
schemaVersion: 1,
|
|
18980
|
+
sessions: {
|
|
18981
|
+
[sessionId]: {
|
|
18982
|
+
session_id: sessionId,
|
|
18983
|
+
threads: {
|
|
18984
|
+
[leaderThreadId]: { thread_id: leaderThreadId, kind: "subagent", mode: "collaboration-child" },
|
|
18985
|
+
[childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child" },
|
|
18986
|
+
},
|
|
18987
|
+
},
|
|
18988
|
+
},
|
|
18989
|
+
});
|
|
18990
|
+
const dispatchThread = async (threadId) => dispatchCodexNativeHook({
|
|
18991
|
+
hook_event_name: "PreToolUse",
|
|
18992
|
+
cwd,
|
|
18993
|
+
session_id: sessionId,
|
|
18994
|
+
thread_id: threadId,
|
|
18995
|
+
tool_name: "apply_patch",
|
|
18996
|
+
tool_input: { file_path: "src/feature.ts" },
|
|
18997
|
+
}, { cwd });
|
|
18998
|
+
// Root session.json owns a DIFFERENT session A: its native/owner ids must not
|
|
18999
|
+
// anchor session B, so B's mislabeled leader stays blocked (fail closed).
|
|
19000
|
+
await writeJson(sessionPath, { session_id: foreignSessionId, native_session_id: foreignLeaderThreadId });
|
|
19001
|
+
const foreignLeader = await dispatchThread(leaderThreadId);
|
|
19002
|
+
assert.equal(foreignLeader.outputJson?.decision, "block");
|
|
19003
|
+
assert.match(String(foreignLeader.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
|
|
19004
|
+
const foreignChild = await dispatchThread(childThreadId);
|
|
19005
|
+
assert.equal(foreignChild.outputJson?.decision, "block");
|
|
19006
|
+
// Control: when the root session.json owns the evaluated session B, its
|
|
19007
|
+
// native_session_id anchors the leader (blocked) while a genuine child is trusted.
|
|
19008
|
+
await writeJson(sessionPath, { session_id: sessionId, native_session_id: leaderThreadId });
|
|
19009
|
+
const ownedLeader = await dispatchThread(leaderThreadId);
|
|
19010
|
+
assert.equal(ownedLeader.outputJson?.decision, "block");
|
|
19011
|
+
assert.match(String(ownedLeader.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
|
|
19012
|
+
const ownedChild = await dispatchThread(childThreadId);
|
|
19013
|
+
assert.equal(ownedChild.outputJson, null);
|
|
19014
|
+
}
|
|
19015
|
+
finally {
|
|
19016
|
+
await rm(cwd, { recursive: true, force: true });
|
|
19017
|
+
}
|
|
19018
|
+
});
|
|
19019
|
+
it("fails closed when session.json carries only owner session ids without a leader thread anchor (#3117 P4)", async () => {
|
|
19020
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3117-owner-only-anchor-"));
|
|
19021
|
+
process.env.OMX_ROOT = cwd;
|
|
19022
|
+
try {
|
|
19023
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
19024
|
+
const sessionId = "sess-3117-p4-owner-only";
|
|
19025
|
+
const leaderThreadId = "thread-3117-p4-leader";
|
|
19026
|
+
const childThreadId = "thread-3117-p4-child";
|
|
19027
|
+
const untrackedThreadId = "thread-3117-p4-untracked";
|
|
19028
|
+
const sessionPath = join(stateDir, "session.json");
|
|
19029
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
19030
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
|
|
19031
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
|
|
19032
|
+
active: true,
|
|
19033
|
+
mode: "ralph",
|
|
19034
|
+
current_phase: "executing",
|
|
19035
|
+
session_id: sessionId,
|
|
19036
|
+
});
|
|
19037
|
+
// Corrupt tracker for the evaluated session: no leader_thread_id, leader mislabeled kind:"subagent".
|
|
19038
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
19039
|
+
schemaVersion: 1,
|
|
19040
|
+
sessions: {
|
|
19041
|
+
[sessionId]: {
|
|
19042
|
+
session_id: sessionId,
|
|
19043
|
+
threads: {
|
|
19044
|
+
[leaderThreadId]: { thread_id: leaderThreadId, kind: "subagent", mode: "collaboration-child" },
|
|
19045
|
+
[childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child" },
|
|
19046
|
+
},
|
|
19047
|
+
},
|
|
19048
|
+
},
|
|
19049
|
+
});
|
|
19050
|
+
const dispatchThread = async (threadId) => dispatchCodexNativeHook({
|
|
19051
|
+
hook_event_name: "PreToolUse",
|
|
19052
|
+
cwd,
|
|
19053
|
+
session_id: sessionId,
|
|
19054
|
+
thread_id: threadId,
|
|
19055
|
+
tool_name: "apply_patch",
|
|
19056
|
+
tool_input: { file_path: "src/feature.ts" },
|
|
19057
|
+
}, { cwd });
|
|
19058
|
+
// session.json maps to the evaluated session via owner ids but has NO
|
|
19059
|
+
// native_session_id: owner session ids are not leader thread anchors, so trust
|
|
19060
|
+
// must fail closed rather than treat their presence as an anchor (#3117 P4).
|
|
19061
|
+
await writeJson(sessionPath, {
|
|
19062
|
+
session_id: sessionId,
|
|
19063
|
+
owner_omx_session_id: "owner-omx-3117-p4",
|
|
19064
|
+
owner_codex_session_id: "owner-codex-3117-p4",
|
|
19065
|
+
});
|
|
19066
|
+
const ownerOnlyLeader = await dispatchThread(leaderThreadId);
|
|
19067
|
+
assert.equal(ownerOnlyLeader.outputJson?.decision, "block");
|
|
19068
|
+
assert.match(String(ownerOnlyLeader.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
|
|
19069
|
+
// Control: an untracked thread also stays blocked under the same state.
|
|
19070
|
+
const ownerOnlyUntracked = await dispatchThread(untrackedThreadId);
|
|
19071
|
+
assert.equal(ownerOnlyUntracked.outputJson?.decision, "block");
|
|
19072
|
+
// Control: once a genuine native_session_id leader thread anchor exists for this
|
|
19073
|
+
// session, the leader is blocked via that anchor while a real child is trusted.
|
|
19074
|
+
await writeJson(sessionPath, { session_id: sessionId, native_session_id: leaderThreadId });
|
|
19075
|
+
const anchoredLeader = await dispatchThread(leaderThreadId);
|
|
19076
|
+
assert.equal(anchoredLeader.outputJson?.decision, "block");
|
|
19077
|
+
assert.match(String(anchoredLeader.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
|
|
19078
|
+
const anchoredChild = await dispatchThread(childThreadId);
|
|
19079
|
+
assert.equal(anchoredChild.outputJson, null);
|
|
19080
|
+
}
|
|
19081
|
+
finally {
|
|
19082
|
+
await rm(cwd, { recursive: true, force: true });
|
|
19083
|
+
}
|
|
19084
|
+
});
|
|
19085
|
+
it("blocks conductor writes when thread_spawn provenance is attached to the leader thread", async () => {
|
|
19086
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-conductor-thread-spawn-leader-"));
|
|
19087
|
+
const originalOmxRoot = process.env.OMX_ROOT;
|
|
19088
|
+
const originalOmxStateRoot = process.env.OMX_STATE_ROOT;
|
|
19089
|
+
const originalOmxTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
|
|
19090
|
+
try {
|
|
19091
|
+
process.env.OMX_ROOT = cwd;
|
|
19092
|
+
delete process.env.OMX_STATE_ROOT;
|
|
19093
|
+
delete process.env.OMX_TEAM_STATE_ROOT;
|
|
18038
19094
|
const stateDir = join(cwd, ".omx", "state");
|
|
18039
19095
|
const sessionId = "sess-conductor-thread-spawn-leader";
|
|
18040
19096
|
const leaderThreadId = "thread-conductor-thread-spawn-leader";
|
|
@@ -18096,6 +19152,387 @@ PY`,
|
|
|
18096
19152
|
await rm(cwd, { recursive: true, force: true });
|
|
18097
19153
|
}
|
|
18098
19154
|
});
|
|
19155
|
+
it("denies Main-root conductor apply_patch from the session leader thread during active Ralph (#3116)", async () => {
|
|
19156
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3116-root-denial-"));
|
|
19157
|
+
process.env.OMX_ROOT = cwd;
|
|
19158
|
+
try {
|
|
19159
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
19160
|
+
const sessionId = "sess-3116-root-denial";
|
|
19161
|
+
const leaderThreadId = "thread-3116-root-denial-leader";
|
|
19162
|
+
const nowIso = new Date().toISOString();
|
|
19163
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
19164
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId });
|
|
19165
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
|
|
19166
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
|
|
19167
|
+
active: true,
|
|
19168
|
+
mode: "ralph",
|
|
19169
|
+
current_phase: "executing",
|
|
19170
|
+
session_id: sessionId,
|
|
19171
|
+
});
|
|
19172
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
19173
|
+
schemaVersion: 1,
|
|
19174
|
+
sessions: {
|
|
19175
|
+
[sessionId]: {
|
|
19176
|
+
session_id: sessionId,
|
|
19177
|
+
leader_thread_id: leaderThreadId,
|
|
19178
|
+
updated_at: nowIso,
|
|
19179
|
+
threads: {
|
|
19180
|
+
[leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
|
|
19181
|
+
},
|
|
19182
|
+
},
|
|
19183
|
+
},
|
|
19184
|
+
});
|
|
19185
|
+
const result = await dispatchCodexNativeHook({
|
|
19186
|
+
hook_event_name: "PreToolUse",
|
|
19187
|
+
cwd,
|
|
19188
|
+
session_id: sessionId,
|
|
19189
|
+
thread_id: leaderThreadId,
|
|
19190
|
+
tool_name: "apply_patch",
|
|
19191
|
+
tool_input: { file_path: "src/feature.ts" },
|
|
19192
|
+
}, { cwd });
|
|
19193
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
19194
|
+
assert.match(String(result.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
|
|
19195
|
+
}
|
|
19196
|
+
finally {
|
|
19197
|
+
await rm(cwd, { recursive: true, force: true });
|
|
19198
|
+
}
|
|
19199
|
+
});
|
|
19200
|
+
it("allows apply_patch from an untyped collaboration.spawn_agent child tracked as a subagent during active Ralph (#3116)", async () => {
|
|
19201
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3116-trusted-child-"));
|
|
19202
|
+
process.env.OMX_ROOT = cwd;
|
|
19203
|
+
try {
|
|
19204
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
19205
|
+
const sessionId = "sess-3116-trusted-child";
|
|
19206
|
+
const leaderThreadId = "thread-3116-trusted-child-leader";
|
|
19207
|
+
const childThreadId = "thread-3116-trusted-child-worker";
|
|
19208
|
+
const nowIso = new Date().toISOString();
|
|
19209
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
19210
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId });
|
|
19211
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
|
|
19212
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
|
|
19213
|
+
active: true,
|
|
19214
|
+
mode: "ralph",
|
|
19215
|
+
current_phase: "executing",
|
|
19216
|
+
session_id: sessionId,
|
|
19217
|
+
});
|
|
19218
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
19219
|
+
schemaVersion: 1,
|
|
19220
|
+
sessions: {
|
|
19221
|
+
[sessionId]: {
|
|
19222
|
+
session_id: sessionId,
|
|
19223
|
+
leader_thread_id: leaderThreadId,
|
|
19224
|
+
updated_at: nowIso,
|
|
19225
|
+
threads: {
|
|
19226
|
+
[leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
|
|
19227
|
+
[childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1, leader_thread_id: leaderThreadId },
|
|
19228
|
+
},
|
|
19229
|
+
},
|
|
19230
|
+
},
|
|
19231
|
+
});
|
|
19232
|
+
// Untyped role (not in the typed-agent catalog) must not defeat tracker-backed trust.
|
|
19233
|
+
const result = await dispatchCodexNativeHook({
|
|
19234
|
+
hook_event_name: "PreToolUse",
|
|
19235
|
+
cwd,
|
|
19236
|
+
session_id: sessionId,
|
|
19237
|
+
thread_id: childThreadId,
|
|
19238
|
+
agent_role: "collaboration-child",
|
|
19239
|
+
source: {
|
|
19240
|
+
subagent: {
|
|
19241
|
+
thread_spawn: {
|
|
19242
|
+
parent_thread_id: leaderThreadId,
|
|
19243
|
+
depth: 1,
|
|
19244
|
+
agent_nickname: "Helper",
|
|
19245
|
+
},
|
|
19246
|
+
},
|
|
19247
|
+
},
|
|
19248
|
+
tool_name: "apply_patch",
|
|
19249
|
+
tool_input: { file_path: "src/feature.ts" },
|
|
19250
|
+
}, { cwd });
|
|
19251
|
+
assert.equal(result.outputJson, null);
|
|
19252
|
+
}
|
|
19253
|
+
finally {
|
|
19254
|
+
await rm(cwd, { recursive: true, force: true });
|
|
19255
|
+
}
|
|
19256
|
+
});
|
|
19257
|
+
it("allows apply_patch from untyped collaboration descendants via tracked thread and tracked parent chain (#3116)", async () => {
|
|
19258
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3116-descendant-"));
|
|
19259
|
+
process.env.OMX_ROOT = cwd;
|
|
19260
|
+
try {
|
|
19261
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
19262
|
+
const sessionId = "sess-3116-descendant";
|
|
19263
|
+
const leaderThreadId = "thread-3116-descendant-leader";
|
|
19264
|
+
const childThreadId = "thread-3116-descendant-child";
|
|
19265
|
+
const grandchildThreadId = "thread-3116-descendant-grandchild";
|
|
19266
|
+
const greatGrandchildThreadId = "thread-3116-descendant-great-grandchild";
|
|
19267
|
+
const nowIso = new Date().toISOString();
|
|
19268
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
19269
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId });
|
|
19270
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
|
|
19271
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
|
|
19272
|
+
active: true,
|
|
19273
|
+
mode: "ralph",
|
|
19274
|
+
current_phase: "executing",
|
|
19275
|
+
session_id: sessionId,
|
|
19276
|
+
});
|
|
19277
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
19278
|
+
schemaVersion: 1,
|
|
19279
|
+
sessions: {
|
|
19280
|
+
[sessionId]: {
|
|
19281
|
+
session_id: sessionId,
|
|
19282
|
+
leader_thread_id: leaderThreadId,
|
|
19283
|
+
updated_at: nowIso,
|
|
19284
|
+
threads: {
|
|
19285
|
+
[leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
|
|
19286
|
+
[childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1, leader_thread_id: leaderThreadId },
|
|
19287
|
+
[grandchildThreadId]: { thread_id: grandchildThreadId, kind: "subagent", mode: "collaboration-child", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1, leader_thread_id: childThreadId },
|
|
19288
|
+
},
|
|
19289
|
+
},
|
|
19290
|
+
},
|
|
19291
|
+
});
|
|
19292
|
+
// (a) A descendant whose own thread is tracked as a subagent is trusted directly.
|
|
19293
|
+
const trackedDescendant = await dispatchCodexNativeHook({
|
|
19294
|
+
hook_event_name: "PreToolUse",
|
|
19295
|
+
cwd,
|
|
19296
|
+
session_id: sessionId,
|
|
19297
|
+
thread_id: grandchildThreadId,
|
|
19298
|
+
tool_name: "apply_patch",
|
|
19299
|
+
tool_input: { file_path: "src/feature.ts" },
|
|
19300
|
+
}, { cwd });
|
|
19301
|
+
assert.equal(trackedDescendant.outputJson, null);
|
|
19302
|
+
// (b) A not-yet-tracked descendant is trusted when its runtime spawn parent is a tracked thread.
|
|
19303
|
+
const chainedDescendant = await dispatchCodexNativeHook({
|
|
19304
|
+
hook_event_name: "PreToolUse",
|
|
19305
|
+
cwd,
|
|
19306
|
+
session_id: sessionId,
|
|
19307
|
+
thread_id: greatGrandchildThreadId,
|
|
19308
|
+
agent_role: "collaboration-child",
|
|
19309
|
+
source: {
|
|
19310
|
+
subagent: {
|
|
19311
|
+
thread_spawn: {
|
|
19312
|
+
parent_thread_id: grandchildThreadId,
|
|
19313
|
+
depth: 3,
|
|
19314
|
+
},
|
|
19315
|
+
},
|
|
19316
|
+
},
|
|
19317
|
+
tool_name: "apply_patch",
|
|
19318
|
+
tool_input: { file_path: "src/feature.ts" },
|
|
19319
|
+
}, { cwd });
|
|
19320
|
+
assert.equal(chainedDescendant.outputJson, null);
|
|
19321
|
+
}
|
|
19322
|
+
finally {
|
|
19323
|
+
await rm(cwd, { recursive: true, force: true });
|
|
19324
|
+
}
|
|
19325
|
+
});
|
|
19326
|
+
it("resolves the session-pinned Ralph state so collaboration children edit while the leader stays blocked (#3116)", async () => {
|
|
19327
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3116-pinned-state-"));
|
|
19328
|
+
process.env.OMX_ROOT = cwd;
|
|
19329
|
+
try {
|
|
19330
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
19331
|
+
const sessionId = "sess-3116-pinned-state";
|
|
19332
|
+
const leaderThreadId = "thread-3116-pinned-state-leader";
|
|
19333
|
+
const childThreadId = "thread-3116-pinned-state-child";
|
|
19334
|
+
const nowIso = new Date().toISOString();
|
|
19335
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
19336
|
+
// Native session id differs from the canonical session id: the guard must resolve it.
|
|
19337
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId });
|
|
19338
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
|
|
19339
|
+
const ralphStatePath = join(stateDir, "sessions", sessionId, "ralph-state.json");
|
|
19340
|
+
await writeJson(ralphStatePath, {
|
|
19341
|
+
active: true,
|
|
19342
|
+
mode: "ralph",
|
|
19343
|
+
current_phase: "executing",
|
|
19344
|
+
session_id: sessionId,
|
|
19345
|
+
});
|
|
19346
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
19347
|
+
schemaVersion: 1,
|
|
19348
|
+
sessions: {
|
|
19349
|
+
[sessionId]: {
|
|
19350
|
+
session_id: sessionId,
|
|
19351
|
+
leader_thread_id: leaderThreadId,
|
|
19352
|
+
updated_at: nowIso,
|
|
19353
|
+
threads: {
|
|
19354
|
+
[leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
|
|
19355
|
+
[childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1, leader_thread_id: leaderThreadId },
|
|
19356
|
+
},
|
|
19357
|
+
},
|
|
19358
|
+
},
|
|
19359
|
+
});
|
|
19360
|
+
// Payload carries the native session id, which must map to the canonical session.
|
|
19361
|
+
const childEdit = await dispatchCodexNativeHook({
|
|
19362
|
+
hook_event_name: "PreToolUse",
|
|
19363
|
+
cwd,
|
|
19364
|
+
session_id: leaderThreadId,
|
|
19365
|
+
thread_id: childThreadId,
|
|
19366
|
+
tool_name: "apply_patch",
|
|
19367
|
+
tool_input: { file_path: "src/feature.ts" },
|
|
19368
|
+
}, { cwd });
|
|
19369
|
+
assert.equal(childEdit.outputJson, null);
|
|
19370
|
+
const leaderEdit = await dispatchCodexNativeHook({
|
|
19371
|
+
hook_event_name: "PreToolUse",
|
|
19372
|
+
cwd,
|
|
19373
|
+
session_id: leaderThreadId,
|
|
19374
|
+
thread_id: leaderThreadId,
|
|
19375
|
+
tool_name: "apply_patch",
|
|
19376
|
+
tool_input: { file_path: "src/feature.ts" },
|
|
19377
|
+
}, { cwd });
|
|
19378
|
+
assert.equal(leaderEdit.outputJson?.decision, "block");
|
|
19379
|
+
assert.match(String(leaderEdit.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
|
|
19380
|
+
// The guard reads the session-pinned phase: flipping it to "starting" releases the leader too.
|
|
19381
|
+
await writeJson(ralphStatePath, {
|
|
19382
|
+
active: true,
|
|
19383
|
+
mode: "ralph",
|
|
19384
|
+
current_phase: "starting",
|
|
19385
|
+
session_id: sessionId,
|
|
19386
|
+
});
|
|
19387
|
+
const leaderEditAfterStarting = await dispatchCodexNativeHook({
|
|
19388
|
+
hook_event_name: "PreToolUse",
|
|
19389
|
+
cwd,
|
|
19390
|
+
session_id: leaderThreadId,
|
|
19391
|
+
thread_id: leaderThreadId,
|
|
19392
|
+
tool_name: "apply_patch",
|
|
19393
|
+
tool_input: { file_path: "src/feature.ts" },
|
|
19394
|
+
}, { cwd });
|
|
19395
|
+
assert.equal(leaderEditAfterStarting.outputJson, null);
|
|
19396
|
+
}
|
|
19397
|
+
finally {
|
|
19398
|
+
await rm(cwd, { recursive: true, force: true });
|
|
19399
|
+
}
|
|
19400
|
+
});
|
|
19401
|
+
it("trusts the native collaboration.spawn_agent child surface via runtime spawn provenance during active Ralph (#3116)", async () => {
|
|
19402
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3116-collab-surface-"));
|
|
19403
|
+
process.env.OMX_ROOT = cwd;
|
|
19404
|
+
try {
|
|
19405
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
19406
|
+
const sessionId = "sess-3116-collab-surface";
|
|
19407
|
+
const leaderThreadId = "thread-3116-collab-surface-leader";
|
|
19408
|
+
const childThreadId = "thread-3116-collab-surface-child";
|
|
19409
|
+
const nowIso = new Date().toISOString();
|
|
19410
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
19411
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId });
|
|
19412
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
|
|
19413
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
|
|
19414
|
+
active: true,
|
|
19415
|
+
mode: "ralph",
|
|
19416
|
+
current_phase: "executing",
|
|
19417
|
+
session_id: sessionId,
|
|
19418
|
+
});
|
|
19419
|
+
// Child SessionStart has not been recorded yet: only the leader is tracked.
|
|
19420
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
19421
|
+
schemaVersion: 1,
|
|
19422
|
+
sessions: {
|
|
19423
|
+
[sessionId]: {
|
|
19424
|
+
session_id: sessionId,
|
|
19425
|
+
leader_thread_id: leaderThreadId,
|
|
19426
|
+
updated_at: nowIso,
|
|
19427
|
+
threads: {
|
|
19428
|
+
[leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
|
|
19429
|
+
},
|
|
19430
|
+
},
|
|
19431
|
+
},
|
|
19432
|
+
});
|
|
19433
|
+
// No typed agent_role at all: a raw collaboration.spawn_agent child payload shape.
|
|
19434
|
+
const result = await dispatchCodexNativeHook({
|
|
19435
|
+
hook_event_name: "PreToolUse",
|
|
19436
|
+
cwd,
|
|
19437
|
+
session_id: sessionId,
|
|
19438
|
+
thread_id: childThreadId,
|
|
19439
|
+
source: {
|
|
19440
|
+
subagent: {
|
|
19441
|
+
thread_spawn: {
|
|
19442
|
+
parent_thread_id: leaderThreadId,
|
|
19443
|
+
depth: 1,
|
|
19444
|
+
agent_nickname: "Implementer",
|
|
19445
|
+
},
|
|
19446
|
+
},
|
|
19447
|
+
},
|
|
19448
|
+
tool_name: "apply_patch",
|
|
19449
|
+
tool_input: { file_path: "src/feature.ts" },
|
|
19450
|
+
}, { cwd });
|
|
19451
|
+
assert.equal(result.outputJson, null);
|
|
19452
|
+
}
|
|
19453
|
+
finally {
|
|
19454
|
+
await rm(cwd, { recursive: true, force: true });
|
|
19455
|
+
}
|
|
19456
|
+
});
|
|
19457
|
+
it("denies spoofed or untrusted collaboration provenance while the main root stays protected during active Ralph (#3116)", async () => {
|
|
19458
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3116-spoof-denial-"));
|
|
19459
|
+
process.env.OMX_ROOT = cwd;
|
|
19460
|
+
try {
|
|
19461
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
19462
|
+
const sessionId = "sess-3116-spoof-denial";
|
|
19463
|
+
const leaderThreadId = "thread-3116-spoof-denial-leader";
|
|
19464
|
+
const nowIso = new Date().toISOString();
|
|
19465
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
19466
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId });
|
|
19467
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
|
|
19468
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
|
|
19469
|
+
active: true,
|
|
19470
|
+
mode: "ralph",
|
|
19471
|
+
current_phase: "executing",
|
|
19472
|
+
session_id: sessionId,
|
|
19473
|
+
});
|
|
19474
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
19475
|
+
schemaVersion: 1,
|
|
19476
|
+
sessions: {
|
|
19477
|
+
[sessionId]: {
|
|
19478
|
+
session_id: sessionId,
|
|
19479
|
+
leader_thread_id: leaderThreadId,
|
|
19480
|
+
updated_at: nowIso,
|
|
19481
|
+
threads: {
|
|
19482
|
+
[leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
|
|
19483
|
+
},
|
|
19484
|
+
},
|
|
19485
|
+
},
|
|
19486
|
+
});
|
|
19487
|
+
// (a) Untracked thread whose declared parent is neither the leader nor a tracked thread.
|
|
19488
|
+
const orphanParent = await dispatchCodexNativeHook({
|
|
19489
|
+
hook_event_name: "PreToolUse",
|
|
19490
|
+
cwd,
|
|
19491
|
+
session_id: sessionId,
|
|
19492
|
+
thread_id: "thread-3116-spoof-orphan",
|
|
19493
|
+
agent_role: "collaboration-child",
|
|
19494
|
+
source: {
|
|
19495
|
+
subagent: {
|
|
19496
|
+
thread_spawn: { parent_thread_id: "thread-3116-spoof-outsider" },
|
|
19497
|
+
},
|
|
19498
|
+
},
|
|
19499
|
+
tool_name: "apply_patch",
|
|
19500
|
+
tool_input: { file_path: "src/feature.ts" },
|
|
19501
|
+
}, { cwd });
|
|
19502
|
+
assert.equal(orphanParent.outputJson?.decision, "block");
|
|
19503
|
+
assert.match(String(orphanParent.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
|
|
19504
|
+
// (b) Untyped child with no provenance at all (no tracker thread, no spawn source).
|
|
19505
|
+
const noProvenance = await dispatchCodexNativeHook({
|
|
19506
|
+
hook_event_name: "PreToolUse",
|
|
19507
|
+
cwd,
|
|
19508
|
+
session_id: sessionId,
|
|
19509
|
+
thread_id: "thread-3116-spoof-bare",
|
|
19510
|
+
agent_role: "collaboration-child",
|
|
19511
|
+
tool_name: "apply_patch",
|
|
19512
|
+
tool_input: { file_path: "src/feature.ts" },
|
|
19513
|
+
}, { cwd });
|
|
19514
|
+
assert.equal(noProvenance.outputJson?.decision, "block");
|
|
19515
|
+
// (c) Spawn provenance attached to the leader thread cannot promote the main root.
|
|
19516
|
+
const leaderSelfSpawn = await dispatchCodexNativeHook({
|
|
19517
|
+
hook_event_name: "PreToolUse",
|
|
19518
|
+
cwd,
|
|
19519
|
+
session_id: sessionId,
|
|
19520
|
+
thread_id: leaderThreadId,
|
|
19521
|
+
agent_role: "collaboration-child",
|
|
19522
|
+
source: {
|
|
19523
|
+
subagent: {
|
|
19524
|
+
thread_spawn: { parent_thread_id: leaderThreadId },
|
|
19525
|
+
},
|
|
19526
|
+
},
|
|
19527
|
+
tool_name: "apply_patch",
|
|
19528
|
+
tool_input: { file_path: "src/feature.ts" },
|
|
19529
|
+
}, { cwd });
|
|
19530
|
+
assert.equal(leaderSelfSpawn.outputJson?.decision, "block");
|
|
19531
|
+
}
|
|
19532
|
+
finally {
|
|
19533
|
+
await rm(cwd, { recursive: true, force: true });
|
|
19534
|
+
}
|
|
19535
|
+
});
|
|
18099
19536
|
it("blocks Main-root ralph conductor source and planning artifact writes while allowing .omx workflow state writes", async () => {
|
|
18100
19537
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralph-conductor-write-"));
|
|
18101
19538
|
try {
|
|
@@ -18167,6 +19604,65 @@ PY`,
|
|
|
18167
19604
|
await rm(cwd, { recursive: true, force: true });
|
|
18168
19605
|
}
|
|
18169
19606
|
});
|
|
19607
|
+
it("allows structured ultragoal steering cleanup through dynamic nested shell loops", async () => {
|
|
19608
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-steer-cleanup-"));
|
|
19609
|
+
try {
|
|
19610
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
19611
|
+
const sessionId = "sess-ultragoal-steer-cleanup";
|
|
19612
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
19613
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
19614
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "ultragoal", "planning");
|
|
19615
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ultragoal-state.json"), {
|
|
19616
|
+
active: true,
|
|
19617
|
+
mode: "ultragoal",
|
|
19618
|
+
current_phase: "planning",
|
|
19619
|
+
session_id: sessionId,
|
|
19620
|
+
});
|
|
19621
|
+
const result = await dispatchCodexNativeHook({
|
|
19622
|
+
hook_event_name: "PreToolUse",
|
|
19623
|
+
cwd,
|
|
19624
|
+
session_id: sessionId,
|
|
19625
|
+
thread_id: "thread-ultragoal-steer-cleanup",
|
|
19626
|
+
tool_name: "Bash",
|
|
19627
|
+
tool_input: {
|
|
19628
|
+
command: `bash -lc 'for goal_id in G001-atomized G002-atomized; do omx ultragoal steer --kind mark_blocked_superseded --target-goal-id "$goal_id" --evidence ".omx/ultragoal cleanup supersedes atomized pseudo-goals." --rationale "Structured steering cleanup keeps durable Ultragoal metadata auditable." --json; done'`,
|
|
19629
|
+
},
|
|
19630
|
+
}, { cwd });
|
|
19631
|
+
assert.equal(result.outputJson, null);
|
|
19632
|
+
}
|
|
19633
|
+
finally {
|
|
19634
|
+
await rm(cwd, { recursive: true, force: true });
|
|
19635
|
+
}
|
|
19636
|
+
});
|
|
19637
|
+
it("blocks unsafe dynamic nested shell writes in conductor mode", async () => {
|
|
19638
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-dynamic-shell-write-block-"));
|
|
19639
|
+
try {
|
|
19640
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
19641
|
+
const sessionId = "sess-dynamic-shell-write-block";
|
|
19642
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
19643
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
19644
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "ultragoal", "planning");
|
|
19645
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ultragoal-state.json"), {
|
|
19646
|
+
active: true,
|
|
19647
|
+
mode: "ultragoal",
|
|
19648
|
+
current_phase: "planning",
|
|
19649
|
+
session_id: sessionId,
|
|
19650
|
+
});
|
|
19651
|
+
const result = await dispatchCodexNativeHook({
|
|
19652
|
+
hook_event_name: "PreToolUse",
|
|
19653
|
+
cwd,
|
|
19654
|
+
session_id: sessionId,
|
|
19655
|
+
thread_id: "thread-dynamic-shell-write-block",
|
|
19656
|
+
tool_name: "Bash",
|
|
19657
|
+
tool_input: { command: `bash -lc 'cp "$SOURCE_FILE" src/runtime.ts'` },
|
|
19658
|
+
}, { cwd });
|
|
19659
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
19660
|
+
assert.match(String(result.outputJson?.reason ?? ""), /Bash nested shell execution is dynamic and cannot be validated/);
|
|
19661
|
+
}
|
|
19662
|
+
finally {
|
|
19663
|
+
await rm(cwd, { recursive: true, force: true });
|
|
19664
|
+
}
|
|
19665
|
+
});
|
|
18170
19666
|
it("blocks common Bash file mutations in Main-root conductor states unless they target workflow metadata", async () => {
|
|
18171
19667
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-conductor-bash-mutations-"));
|
|
18172
19668
|
try {
|