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
|
@@ -782,6 +782,72 @@ describe("codex native hook dispatch", () => {
|
|
|
782
782
|
}
|
|
783
783
|
});
|
|
784
784
|
|
|
785
|
+
it("allows Ralplan Markdown draft-only apply_patch on the live CLI path while denying mixed targets", async () => {
|
|
786
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-ralplan-draft-boundary-"));
|
|
787
|
+
const sessionId = "sess-cli-ralplan-draft-boundary";
|
|
788
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
789
|
+
try {
|
|
790
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId, cwd });
|
|
791
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
792
|
+
active: true,
|
|
793
|
+
skill: "ralplan",
|
|
794
|
+
phase: "planning",
|
|
795
|
+
session_id: sessionId,
|
|
796
|
+
active_skills: [{ skill: "ralplan", phase: "planning", active: true, session_id: sessionId }],
|
|
797
|
+
});
|
|
798
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
799
|
+
active: true,
|
|
800
|
+
mode: "ralplan",
|
|
801
|
+
current_phase: "planning",
|
|
802
|
+
session_id: sessionId,
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
for (const [name, target] of [
|
|
806
|
+
["relative", ".omx/drafts/issue-3105.md"],
|
|
807
|
+
["repository-absolute", join(cwd, ".omx", "drafts", "issue-3105.md")],
|
|
808
|
+
] as const) {
|
|
809
|
+
const result = runNativeHookCliResult({
|
|
810
|
+
hook_event_name: "PreToolUse",
|
|
811
|
+
cwd,
|
|
812
|
+
session_id: sessionId,
|
|
813
|
+
thread_id: `thread-cli-ralplan-draft-${name}`,
|
|
814
|
+
tool_name: "apply_patch",
|
|
815
|
+
tool_input: {
|
|
816
|
+
input: `*** Begin Patch\n*** Add File: ${target}\n+# Draft\n*** End Patch\n`,
|
|
817
|
+
},
|
|
818
|
+
}, { cwd });
|
|
819
|
+
|
|
820
|
+
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
821
|
+
assert.deepEqual(parseSingleJsonStdout(result.stdout), {});
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
const mixedResult = runNativeHookCliResult({
|
|
825
|
+
hook_event_name: "PreToolUse",
|
|
826
|
+
cwd,
|
|
827
|
+
session_id: sessionId,
|
|
828
|
+
thread_id: "thread-cli-ralplan-draft-mixed",
|
|
829
|
+
tool_name: "apply_patch",
|
|
830
|
+
tool_input: {
|
|
831
|
+
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",
|
|
832
|
+
},
|
|
833
|
+
}, { cwd });
|
|
834
|
+
|
|
835
|
+
assert.equal(mixedResult.status, 0, mixedResult.stderr || mixedResult.stdout);
|
|
836
|
+
const mixedOutput = parseSingleJsonStdout(mixedResult.stdout);
|
|
837
|
+
const hookSpecificOutput = mixedOutput.hookSpecificOutput as Record<string, unknown>;
|
|
838
|
+
assert.deepEqual(Object.keys(mixedOutput).sort(), ["hookSpecificOutput"]);
|
|
839
|
+
assert.equal(hookSpecificOutput.hookEventName, "PreToolUse");
|
|
840
|
+
assert.equal(hookSpecificOutput.permissionDecision, "deny");
|
|
841
|
+
assert.match(String(hookSpecificOutput.permissionDecisionReason ?? ""), /src\/leak\.ts/);
|
|
842
|
+
assert.match(String(hookSpecificOutput.permissionDecisionReason ?? ""), /implementation\/write tools are blocked/);
|
|
843
|
+
assert.equal(mixedOutput.decision, undefined);
|
|
844
|
+
assert.equal(mixedOutput.reason, undefined);
|
|
845
|
+
assert.equal(mixedOutput.systemMessage, undefined);
|
|
846
|
+
} finally {
|
|
847
|
+
await rm(cwd, { recursive: true, force: true });
|
|
848
|
+
}
|
|
849
|
+
});
|
|
850
|
+
|
|
785
851
|
it("preserves team-worker typed subagent PreToolUse exemption without thread spawn provenance", async () => {
|
|
786
852
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-team-worker-typed-pretool-exempt-"));
|
|
787
853
|
const sessionId = "sess-team-worker-typed-pretool-exempt";
|
|
@@ -1518,13 +1584,56 @@ PY`,
|
|
|
1518
1584
|
cwd,
|
|
1519
1585
|
session_id: sessionId,
|
|
1520
1586
|
thread_id: "thread-autopilot-diagnostic",
|
|
1521
|
-
}, { cwd }))
|
|
1587
|
+
}, { cwd }));
|
|
1522
1588
|
|
|
1523
1589
|
assert.equal(output.decision, "block");
|
|
1590
|
+
assert.equal(output.stopReason, "autopilot_ultragoal");
|
|
1524
1591
|
assert.match(String(output.reason ?? ""), /state: \.omx\/state\/sessions\/sess-autopilot-diagnostic\/autopilot-state\.json/);
|
|
1525
1592
|
assert.match(String(output.reason ?? ""), /canonical: canonical_agrees/);
|
|
1526
|
-
assert.
|
|
1527
|
-
assert.
|
|
1593
|
+
assert.match(String(output.systemMessage ?? ""), /state: \.omx\/state\/sessions\/sess-autopilot-diagnostic\/autopilot-state\.json/);
|
|
1594
|
+
assert.match(String(output.systemMessage ?? ""), /canonical: canonical_agrees/);
|
|
1595
|
+
assert.deepEqual(Object.keys(output).sort(), ["decision", "reason", "stopReason", "systemMessage"]);
|
|
1596
|
+
assert.equal("statePath" in output, false);
|
|
1597
|
+
assert.equal("canonicalDisagreement" in output, false);
|
|
1598
|
+
} finally {
|
|
1599
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1600
|
+
}
|
|
1601
|
+
});
|
|
1602
|
+
|
|
1603
|
+
it("keeps canonical phase disagreements in active Autopilot Stop diagnostics", async () => {
|
|
1604
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-canonical-phase-"));
|
|
1605
|
+
try {
|
|
1606
|
+
const sessionId = "sess-autopilot-canonical-phase";
|
|
1607
|
+
await writeJson(join(cwd, ".omx", "state", "session.json"), { session_id: sessionId, cwd });
|
|
1608
|
+
await writeJson(join(cwd, ".omx", "state", "skill-active-state.json"), {
|
|
1609
|
+
version: 1,
|
|
1610
|
+
active: true,
|
|
1611
|
+
skill: "autopilot",
|
|
1612
|
+
phase: "execution",
|
|
1613
|
+
session_id: sessionId,
|
|
1614
|
+
active_skills: [{ skill: "autopilot", phase: "execution", active: true, session_id: sessionId }],
|
|
1615
|
+
});
|
|
1616
|
+
await writeJson(join(cwd, ".omx", "state", "sessions", sessionId, "autopilot-state.json"), {
|
|
1617
|
+
active: true,
|
|
1618
|
+
mode: "autopilot",
|
|
1619
|
+
current_phase: "ultragoal",
|
|
1620
|
+
session_id: sessionId,
|
|
1621
|
+
});
|
|
1622
|
+
|
|
1623
|
+
const output = parseSingleJsonStdout(runNativeHookCli({
|
|
1624
|
+
hook_event_name: "Stop",
|
|
1625
|
+
cwd,
|
|
1626
|
+
session_id: sessionId,
|
|
1627
|
+
thread_id: "thread-autopilot-canonical-phase",
|
|
1628
|
+
}, { cwd }));
|
|
1629
|
+
|
|
1630
|
+
assert.equal(output.decision, "block");
|
|
1631
|
+
assert.equal(output.stopReason, "autopilot_ultragoal");
|
|
1632
|
+
assert.match(String(output.reason ?? ""), /canonical: canonical_phase:execution/);
|
|
1633
|
+
assert.match(String(output.systemMessage ?? ""), /canonical: canonical_phase:execution/);
|
|
1634
|
+
assert.deepEqual(Object.keys(output).sort(), ["decision", "reason", "stopReason", "systemMessage"]);
|
|
1635
|
+
assert.equal("statePath" in output, false);
|
|
1636
|
+
assert.equal("canonicalDisagreement" in output, false);
|
|
1528
1637
|
} finally {
|
|
1529
1638
|
await rm(cwd, { recursive: true, force: true });
|
|
1530
1639
|
}
|
|
@@ -1819,6 +1928,193 @@ PY`,
|
|
|
1819
1928
|
}
|
|
1820
1929
|
});
|
|
1821
1930
|
|
|
1931
|
+
it("adds resume-by-id instructions for persisted subagents on SessionStart resume", async () => {
|
|
1932
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-reopen-"));
|
|
1933
|
+
try {
|
|
1934
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
1935
|
+
const sessionId = "omx-reopen-session";
|
|
1936
|
+
await writeSessionStart(cwd, sessionId, { nativeSessionId: "codex-leader-reopen", pid: process.pid });
|
|
1937
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
1938
|
+
schemaVersion: 1,
|
|
1939
|
+
sessions: {
|
|
1940
|
+
[sessionId]: {
|
|
1941
|
+
session_id: sessionId,
|
|
1942
|
+
leader_thread_id: "codex-leader-reopen",
|
|
1943
|
+
updated_at: "2026-07-09T00:00:00.000Z",
|
|
1944
|
+
threads: {
|
|
1945
|
+
"codex-leader-reopen": {
|
|
1946
|
+
thread_id: "codex-leader-reopen",
|
|
1947
|
+
kind: "leader",
|
|
1948
|
+
first_seen_at: "2026-07-09T00:00:00.000Z",
|
|
1949
|
+
last_seen_at: "2026-07-09T00:00:00.000Z",
|
|
1950
|
+
turn_count: 1,
|
|
1951
|
+
},
|
|
1952
|
+
"thread-architect-reopen": {
|
|
1953
|
+
thread_id: "thread-architect-reopen",
|
|
1954
|
+
kind: "subagent",
|
|
1955
|
+
first_seen_at: "2026-07-09T00:01:00.000Z",
|
|
1956
|
+
last_seen_at: "2026-07-09T00:01:00.000Z",
|
|
1957
|
+
turn_count: 2,
|
|
1958
|
+
role: "architect",
|
|
1959
|
+
lane_id: "plan-review",
|
|
1960
|
+
scope: "SessionStart reopen",
|
|
1961
|
+
status: "available",
|
|
1962
|
+
last_handoff_summary: "reviewed the restart plan",
|
|
1963
|
+
},
|
|
1964
|
+
},
|
|
1965
|
+
},
|
|
1966
|
+
},
|
|
1967
|
+
});
|
|
1968
|
+
|
|
1969
|
+
const result = await dispatchCodexNativeHook(
|
|
1970
|
+
{
|
|
1971
|
+
hook_event_name: "SessionStart",
|
|
1972
|
+
cwd,
|
|
1973
|
+
session_id: "codex-leader-reopen",
|
|
1974
|
+
source: "resume",
|
|
1975
|
+
},
|
|
1976
|
+
{ cwd, sessionOwnerPid: process.pid },
|
|
1977
|
+
);
|
|
1978
|
+
|
|
1979
|
+
const additionalContext = String(
|
|
1980
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
1981
|
+
);
|
|
1982
|
+
assert.match(additionalContext, /\[Persisted subagent reopen\]/);
|
|
1983
|
+
assert.match(additionalContext, /resume_agent\("thread-architect-reopen"\)/);
|
|
1984
|
+
assert.match(additionalContext, /role: architect; lane: plan-review; scope: SessionStart reopen; status: available/);
|
|
1985
|
+
assert.match(additionalContext, /saved subagent ids found: 1/);
|
|
1986
|
+
|
|
1987
|
+
const tracking = JSON.parse(await readFile(join(stateDir, "subagent-tracking.json"), "utf-8")) as {
|
|
1988
|
+
sessions?: Record<string, { threads?: Record<string, { resume_requested_at?: string }> }>;
|
|
1989
|
+
};
|
|
1990
|
+
assert.match(
|
|
1991
|
+
tracking.sessions?.[sessionId]?.threads?.["thread-architect-reopen"]?.resume_requested_at ?? "",
|
|
1992
|
+
/^\d{4}-\d{2}-\d{2}T/,
|
|
1993
|
+
);
|
|
1994
|
+
} finally {
|
|
1995
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1996
|
+
}
|
|
1997
|
+
});
|
|
1998
|
+
|
|
1999
|
+
it("does not suggest duplicate same-role subagent spawns when reopen ids exist", async () => {
|
|
2000
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-no-duplicates-"));
|
|
2001
|
+
try {
|
|
2002
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
2003
|
+
const sessionId = "omx-reuse-session";
|
|
2004
|
+
await writeSessionStart(cwd, sessionId, { nativeSessionId: "codex-leader-reuse", pid: process.pid });
|
|
2005
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
2006
|
+
schemaVersion: 1,
|
|
2007
|
+
sessions: {
|
|
2008
|
+
[sessionId]: {
|
|
2009
|
+
session_id: sessionId,
|
|
2010
|
+
leader_thread_id: "codex-leader-reuse",
|
|
2011
|
+
updated_at: "2026-07-09T00:00:00.000Z",
|
|
2012
|
+
threads: {
|
|
2013
|
+
"codex-leader-reuse": {
|
|
2014
|
+
thread_id: "codex-leader-reuse",
|
|
2015
|
+
kind: "leader",
|
|
2016
|
+
first_seen_at: "2026-07-09T00:00:00.000Z",
|
|
2017
|
+
last_seen_at: "2026-07-09T00:00:00.000Z",
|
|
2018
|
+
turn_count: 1,
|
|
2019
|
+
},
|
|
2020
|
+
"thread-critic-reuse": {
|
|
2021
|
+
thread_id: "thread-critic-reuse",
|
|
2022
|
+
kind: "subagent",
|
|
2023
|
+
first_seen_at: "2026-07-09T00:01:00.000Z",
|
|
2024
|
+
last_seen_at: "2026-07-09T00:01:00.000Z",
|
|
2025
|
+
turn_count: 1,
|
|
2026
|
+
role: "critic",
|
|
2027
|
+
lane_id: "risk-review",
|
|
2028
|
+
status: "closed",
|
|
2029
|
+
},
|
|
2030
|
+
},
|
|
2031
|
+
},
|
|
2032
|
+
},
|
|
2033
|
+
});
|
|
2034
|
+
|
|
2035
|
+
const result = await dispatchCodexNativeHook(
|
|
2036
|
+
{
|
|
2037
|
+
hook_event_name: "SessionStart",
|
|
2038
|
+
cwd,
|
|
2039
|
+
session_id: "codex-leader-reuse",
|
|
2040
|
+
source: "startup",
|
|
2041
|
+
},
|
|
2042
|
+
{ cwd, sessionOwnerPid: process.pid },
|
|
2043
|
+
);
|
|
2044
|
+
|
|
2045
|
+
const additionalContext = String(
|
|
2046
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
2047
|
+
);
|
|
2048
|
+
assert.match(additionalContext, /resume_agent\("thread-critic-reuse"\)/);
|
|
2049
|
+
assert.match(additionalContext, /avoid duplicate same-type subagent spawns/);
|
|
2050
|
+
assert.match(additionalContext, /do not spawn a new agent solely because reopen failed/);
|
|
2051
|
+
assert.doesNotMatch(additionalContext, /spawn.*critic.*replacement/i);
|
|
2052
|
+
} finally {
|
|
2053
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2054
|
+
}
|
|
2055
|
+
});
|
|
2056
|
+
|
|
2057
|
+
it("surfaces clear warnings for unavailable persisted subagents without spawning replacements", async () => {
|
|
2058
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-warning-"));
|
|
2059
|
+
try {
|
|
2060
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
2061
|
+
const sessionId = "omx-warning-session";
|
|
2062
|
+
await writeSessionStart(cwd, sessionId, { nativeSessionId: "codex-leader-warning", pid: process.pid });
|
|
2063
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
2064
|
+
schemaVersion: 1,
|
|
2065
|
+
sessions: {
|
|
2066
|
+
[sessionId]: {
|
|
2067
|
+
session_id: sessionId,
|
|
2068
|
+
leader_thread_id: "codex-leader-warning",
|
|
2069
|
+
updated_at: "2026-07-09T00:00:00.000Z",
|
|
2070
|
+
threads: {
|
|
2071
|
+
"codex-leader-warning": {
|
|
2072
|
+
thread_id: "codex-leader-warning",
|
|
2073
|
+
kind: "leader",
|
|
2074
|
+
first_seen_at: "2026-07-09T00:00:00.000Z",
|
|
2075
|
+
last_seen_at: "2026-07-09T00:00:00.000Z",
|
|
2076
|
+
turn_count: 1,
|
|
2077
|
+
},
|
|
2078
|
+
"thread-executor-unavailable": {
|
|
2079
|
+
thread_id: "thread-executor-unavailable",
|
|
2080
|
+
kind: "subagent",
|
|
2081
|
+
first_seen_at: "2026-07-09T00:01:00.000Z",
|
|
2082
|
+
last_seen_at: "2026-07-09T00:01:00.000Z",
|
|
2083
|
+
turn_count: 1,
|
|
2084
|
+
role: "executor",
|
|
2085
|
+
lane_id: "implementation",
|
|
2086
|
+
status: "unavailable",
|
|
2087
|
+
resume_failed_at: "2026-07-09T00:02:00.000Z",
|
|
2088
|
+
resume_failure_reason: "Codex reported missing thread id",
|
|
2089
|
+
},
|
|
2090
|
+
},
|
|
2091
|
+
},
|
|
2092
|
+
},
|
|
2093
|
+
});
|
|
2094
|
+
|
|
2095
|
+
const result = await dispatchCodexNativeHook(
|
|
2096
|
+
{
|
|
2097
|
+
hook_event_name: "SessionStart",
|
|
2098
|
+
cwd,
|
|
2099
|
+
session_id: "codex-leader-warning",
|
|
2100
|
+
source: "resume",
|
|
2101
|
+
},
|
|
2102
|
+
{ cwd, sessionOwnerPid: process.pid },
|
|
2103
|
+
);
|
|
2104
|
+
|
|
2105
|
+
const additionalContext = String(
|
|
2106
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
|
|
2107
|
+
);
|
|
2108
|
+
assert.match(additionalContext, /No compatible saved subagent id is currently marked reopenable/);
|
|
2109
|
+
assert.match(additionalContext, /Reopen warnings:/);
|
|
2110
|
+
assert.match(additionalContext, /thread-executor-unavailable/);
|
|
2111
|
+
assert.match(additionalContext, /last failure: Codex reported missing thread id/);
|
|
2112
|
+
assert.doesNotMatch(additionalContext, /resume_agent\("thread-executor-unavailable"\)/);
|
|
2113
|
+
} finally {
|
|
2114
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2115
|
+
}
|
|
2116
|
+
});
|
|
2117
|
+
|
|
1822
2118
|
it("preserves canonical OMX session scope when native SessionStart arrives with a different id", async () => {
|
|
1823
2119
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-session-reconcile-"));
|
|
1824
2120
|
try {
|
|
@@ -5039,6 +5335,82 @@ standardMaxRounds = 15
|
|
|
5039
5335
|
}
|
|
5040
5336
|
});
|
|
5041
5337
|
|
|
5338
|
+
it("keeps conductor guidance on autopilot activation after capacity-only native subagent evidence", async () => {
|
|
5339
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-capacity-native-"));
|
|
5340
|
+
try {
|
|
5341
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
5342
|
+
await dispatchCodexNativeHook(
|
|
5343
|
+
{
|
|
5344
|
+
hook_event_name: "PostToolUse",
|
|
5345
|
+
cwd,
|
|
5346
|
+
session_id: "sess-autopilot-capacity-native",
|
|
5347
|
+
thread_id: "thread-autopilot-capacity-native",
|
|
5348
|
+
turn_id: "turn-autopilot-capacity-native-spawn",
|
|
5349
|
+
tool_name: "multi_agent_v1.spawn_agent",
|
|
5350
|
+
tool_response: { error: "collab spawn failed: agent thread limit reached" },
|
|
5351
|
+
},
|
|
5352
|
+
{ cwd },
|
|
5353
|
+
);
|
|
5354
|
+
|
|
5355
|
+
const result = await dispatchCodexNativeHook(
|
|
5356
|
+
{
|
|
5357
|
+
hook_event_name: "UserPromptSubmit",
|
|
5358
|
+
cwd,
|
|
5359
|
+
session_id: "sess-autopilot-capacity-native",
|
|
5360
|
+
thread_id: "thread-autopilot-capacity-native",
|
|
5361
|
+
turn_id: "turn-autopilot-capacity-native-prompt",
|
|
5362
|
+
prompt: "$autopilot implement issue #3078",
|
|
5363
|
+
},
|
|
5364
|
+
{ cwd },
|
|
5365
|
+
);
|
|
5366
|
+
|
|
5367
|
+
assert.equal(result.omxEventName, "keyword-detector");
|
|
5368
|
+
assert.equal(result.skillState?.skill, "autopilot");
|
|
5369
|
+
const message = String(
|
|
5370
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext || "",
|
|
5371
|
+
);
|
|
5372
|
+
assert.match(message, /Autopilot protocol:/);
|
|
5373
|
+
assert.match(message, /Conductor mode contract:/);
|
|
5374
|
+
assert.match(message, /Golden Rule: When the Main agent is acting in Conductor mode/);
|
|
5375
|
+
assert.match(message, /Conductor reuse and ledger guidance:/);
|
|
5376
|
+
assert.doesNotMatch(message, /Native subagent support is unavailable in this environment/);
|
|
5377
|
+
assert.doesNotMatch(message, /Reason: agent_thread_limit_reached/);
|
|
5378
|
+
} finally {
|
|
5379
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5380
|
+
}
|
|
5381
|
+
});
|
|
5382
|
+
|
|
5383
|
+
it("omits conductor block and emits unsupported native guidance for unsupported autopilot first-run payload", async () => {
|
|
5384
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-unsupported-native-"));
|
|
5385
|
+
try {
|
|
5386
|
+
await mkdir(join(cwd, ".omx", "state"), { recursive: true });
|
|
5387
|
+
const result = await dispatchCodexNativeHook(
|
|
5388
|
+
{
|
|
5389
|
+
hook_event_name: "UserPromptSubmit",
|
|
5390
|
+
cwd,
|
|
5391
|
+
session_id: "sess-autopilot-unsupported-native",
|
|
5392
|
+
thread_id: "thread-autopilot-unsupported-native",
|
|
5393
|
+
turn_id: "turn-autopilot-unsupported-native",
|
|
5394
|
+
prompt: "$autopilot implement issue #3078",
|
|
5395
|
+
capabilities: { native_subagents: false, multi_agent_v1: false },
|
|
5396
|
+
},
|
|
5397
|
+
{ cwd },
|
|
5398
|
+
);
|
|
5399
|
+
|
|
5400
|
+
const message = String(
|
|
5401
|
+
(result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext || "",
|
|
5402
|
+
);
|
|
5403
|
+
assert.match(message, /Autopilot protocol:/);
|
|
5404
|
+
assert.doesNotMatch(message, /Conductor mode contract:/);
|
|
5405
|
+
assert.doesNotMatch(message, /Conductor reuse and ledger guidance:/);
|
|
5406
|
+
assert.match(message, /Native subagent support is unavailable in this environment/);
|
|
5407
|
+
assert.match(message, /Reason: native_subagents_unsupported/);
|
|
5408
|
+
assert.match(message, /Do not call multi_agent_v1\.close_agent/);
|
|
5409
|
+
} finally {
|
|
5410
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5411
|
+
}
|
|
5412
|
+
});
|
|
5413
|
+
|
|
5042
5414
|
it("records ultragoal prompt skill activation with goal-tool handoff guidance", async () => {
|
|
5043
5415
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-"));
|
|
5044
5416
|
try {
|
|
@@ -7841,15 +8213,79 @@ exit 0
|
|
|
7841
8213
|
);
|
|
7842
8214
|
}
|
|
7843
8215
|
|
|
7844
|
-
|
|
7845
|
-
|
|
7846
|
-
const
|
|
7847
|
-
|
|
8216
|
+
const runtimeRoot = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-deep-interview-runtime-root-"));
|
|
8217
|
+
const previousOmxRoot = process.env.OMX_ROOT;
|
|
8218
|
+
const runtimeSessionId = "sess-di-runtime-artifact";
|
|
8219
|
+
const runtimeStateDir = join(runtimeRoot, ".omx", "state");
|
|
8220
|
+
const runtimeSessionDir = join(runtimeStateDir, "sessions", runtimeSessionId);
|
|
8221
|
+
try {
|
|
8222
|
+
process.env.OMX_ROOT = runtimeRoot;
|
|
8223
|
+
await mkdir(runtimeSessionDir, { recursive: true });
|
|
8224
|
+
await writeJson(join(runtimeStateDir, "session.json"), { session_id: runtimeSessionId, cwd });
|
|
8225
|
+
await writeJson(join(runtimeSessionDir, "skill-active-state.json"), {
|
|
8226
|
+
version: 1,
|
|
8227
|
+
active: true,
|
|
8228
|
+
skill: "deep-interview",
|
|
8229
|
+
phase: "planning",
|
|
8230
|
+
session_id: runtimeSessionId,
|
|
8231
|
+
thread_id: threadId,
|
|
8232
|
+
active_skills: [{ skill: "deep-interview", phase: "planning", active: true, session_id: runtimeSessionId, thread_id: threadId }],
|
|
8233
|
+
});
|
|
8234
|
+
await writeJson(join(runtimeSessionDir, "deep-interview-state.json"), {
|
|
8235
|
+
active: true,
|
|
8236
|
+
mode: "deep-interview",
|
|
8237
|
+
current_phase: "intent-first",
|
|
8238
|
+
session_id: runtimeSessionId,
|
|
8239
|
+
thread_id: threadId,
|
|
8240
|
+
});
|
|
8241
|
+
|
|
8242
|
+
const allowedRuntimeStateWrite = await dispatchCodexNativeHook({
|
|
7848
8243
|
hook_event_name: "PreToolUse",
|
|
7849
8244
|
cwd,
|
|
7850
|
-
session_id:
|
|
7851
|
-
|
|
7852
|
-
|
|
8245
|
+
session_id: runtimeSessionId,
|
|
8246
|
+
thread_id: threadId,
|
|
8247
|
+
tool_name: "Write",
|
|
8248
|
+
tool_use_id: "tool-di-runtime-state-write",
|
|
8249
|
+
tool_input: { file_path: join(runtimeSessionDir, "deep-interview-state.json"), content: "{}\n" },
|
|
8250
|
+
}, { cwd });
|
|
8251
|
+
assert.equal(allowedRuntimeStateWrite.outputJson, null);
|
|
8252
|
+
|
|
8253
|
+
const blockedRuntimeStatePeerWrite = await dispatchCodexNativeHook({
|
|
8254
|
+
hook_event_name: "PreToolUse",
|
|
8255
|
+
cwd,
|
|
8256
|
+
session_id: runtimeSessionId,
|
|
8257
|
+
thread_id: threadId,
|
|
8258
|
+
tool_name: "Write",
|
|
8259
|
+
tool_use_id: "tool-di-runtime-peer-state-write",
|
|
8260
|
+
tool_input: { file_path: join(runtimeRoot, ".omx", "state", "sessions", "../outside", "deep-interview-state.json"), content: "{}\n" },
|
|
8261
|
+
}, { cwd });
|
|
8262
|
+
assert.equal((blockedRuntimeStatePeerWrite.outputJson as { decision?: string } | null)?.decision, "block");
|
|
8263
|
+
|
|
8264
|
+
const blockedRuntimeUnrelatedWrite = await dispatchCodexNativeHook({
|
|
8265
|
+
hook_event_name: "PreToolUse",
|
|
8266
|
+
cwd,
|
|
8267
|
+
session_id: runtimeSessionId,
|
|
8268
|
+
thread_id: threadId,
|
|
8269
|
+
tool_name: "Write",
|
|
8270
|
+
tool_use_id: "tool-di-runtime-unrelated-write",
|
|
8271
|
+
tool_input: { file_path: join(runtimeRoot, "unrelated", "deep-interview-state.json"), content: "{}\n" },
|
|
8272
|
+
}, { cwd });
|
|
8273
|
+
assert.equal((blockedRuntimeUnrelatedWrite.outputJson as { decision?: string } | null)?.decision, "block");
|
|
8274
|
+
} finally {
|
|
8275
|
+
if (typeof previousOmxRoot === "string") process.env.OMX_ROOT = previousOmxRoot;
|
|
8276
|
+
else delete process.env.OMX_ROOT;
|
|
8277
|
+
await rm(runtimeRoot, { recursive: true, force: true });
|
|
8278
|
+
}
|
|
8279
|
+
|
|
8280
|
+
// Cross-mode non-terminal `omx state write` payloads are activations,
|
|
8281
|
+
// because state_write normalizes them to active=true after the hook.
|
|
8282
|
+
const blockedStateCliMutation = await preToolUse(
|
|
8283
|
+
{
|
|
8284
|
+
hook_event_name: "PreToolUse",
|
|
8285
|
+
cwd,
|
|
8286
|
+
session_id: "sess-di-artifact",
|
|
8287
|
+
tool_name: "Bash",
|
|
8288
|
+
tool_use_id: "tool-di-state-cli-write",
|
|
7853
8289
|
tool_input: { command: "omx state write --input '{\"mode\":\"ralph\",\"current_phase\":\"executing\"}' --json" },
|
|
7854
8290
|
},
|
|
7855
8291
|
{ cwd },
|
|
@@ -10499,6 +10935,7 @@ exit 0
|
|
|
10499
10935
|
for (const path of [
|
|
10500
10936
|
".omx/context/findings.md",
|
|
10501
10937
|
".omx/plans/issue-2863.md",
|
|
10938
|
+
".omx/drafts/issue-3105.md",
|
|
10502
10939
|
".omx/specs/issue-2863.md",
|
|
10503
10940
|
".omx/state/required-planning-state.json",
|
|
10504
10941
|
]) {
|
|
@@ -10506,6 +10943,24 @@ exit 0
|
|
|
10506
10943
|
assert.equal(writeResult.outputJson, null, `Write should be allowed for ${path}`);
|
|
10507
10944
|
}
|
|
10508
10945
|
|
|
10946
|
+
const allowedDraftEdit = await preToolUse("Edit", "tool-ralplan-draft-edit", {
|
|
10947
|
+
file_path: ".omx/drafts/issue-3105.md",
|
|
10948
|
+
old_string: "old",
|
|
10949
|
+
new_string: "new",
|
|
10950
|
+
});
|
|
10951
|
+
assert.equal(allowedDraftEdit.outputJson, null);
|
|
10952
|
+
|
|
10953
|
+
for (const path of [
|
|
10954
|
+
join(cwd, ".omx", "drafts", "issue-3105.md"),
|
|
10955
|
+
".omx/drafts/subdir/../issue-3105.md",
|
|
10956
|
+
]) {
|
|
10957
|
+
const normalizedDraftWrite = await preToolUse("Write", `tool-ralplan-normalized-draft-${path}`, {
|
|
10958
|
+
file_path: path,
|
|
10959
|
+
content: "# Draft",
|
|
10960
|
+
});
|
|
10961
|
+
assert.equal(normalizedDraftWrite.outputJson, null, `normalized draft path should be allowed: ${path}`);
|
|
10962
|
+
}
|
|
10963
|
+
|
|
10509
10964
|
for (const protectedPath of [
|
|
10510
10965
|
".omx/state/sessions/sess-ralplan-guard/ralplan-state.json",
|
|
10511
10966
|
".omx/state/sessions/sess-ralplan-guard/autopilot-state.json",
|
|
@@ -10547,11 +11002,26 @@ exit 0
|
|
|
10547
11002
|
});
|
|
10548
11003
|
assert.equal(allowedPatchUpdate.outputJson, null);
|
|
10549
11004
|
|
|
11005
|
+
const allowedDraftPatchAdd = await preToolUse("apply_patch", "tool-ralplan-draft-patch-add", {
|
|
11006
|
+
input: "*** Begin Patch\n*** Add File: .omx/drafts/issue-3105.md\n+# Draft\n*** End Patch\n",
|
|
11007
|
+
});
|
|
11008
|
+
assert.equal(allowedDraftPatchAdd.outputJson, null);
|
|
11009
|
+
|
|
11010
|
+
const allowedDraftPatchUpdate = await preToolUse("ApplyPatch", "tool-ralplan-draft-patch-update", {
|
|
11011
|
+
input: "*** Begin Patch\n*** Update File: .omx/drafts/issue-3105.md\n@@\n-old\n+new\n*** End Patch\n",
|
|
11012
|
+
});
|
|
11013
|
+
assert.equal(allowedDraftPatchUpdate.outputJson, null);
|
|
11014
|
+
|
|
10550
11015
|
const allowedRedirect = await preToolUse("Bash", "tool-ralplan-redirect-allow", {
|
|
10551
11016
|
command: "printf '\\nmore\\n' >> .omx/plans/issue-2863.md",
|
|
10552
11017
|
});
|
|
10553
11018
|
assert.equal(allowedRedirect.outputJson, null);
|
|
10554
11019
|
|
|
11020
|
+
const allowedDraftRedirect = await preToolUse("Bash", "tool-ralplan-draft-redirect-allow", {
|
|
11021
|
+
command: "printf '# Draft\\n' > .omx/drafts/issue-3105.md",
|
|
11022
|
+
});
|
|
11023
|
+
assert.equal(allowedDraftRedirect.outputJson, null);
|
|
11024
|
+
|
|
10555
11025
|
const allowedTee = await preToolUse("Bash", "tool-ralplan-tee-allow", {
|
|
10556
11026
|
command: "printf '\\nmore\\n' | tee -a .omx/specs/issue-2863.md",
|
|
10557
11027
|
});
|
|
@@ -10613,6 +11083,41 @@ exit 0
|
|
|
10613
11083
|
const editReason = String((blockedEdit.outputJson as { reason?: string } | null)?.reason ?? "");
|
|
10614
11084
|
assert.match(editReason, /Edit path/);
|
|
10615
11085
|
assert.match(editReason, /src\/implementation\.ts/);
|
|
11086
|
+
const blockedEditContext = String(
|
|
11087
|
+
(blockedEdit.outputJson as { hookSpecificOutput?: { additionalContext?: string } } | null)
|
|
11088
|
+
?.hookSpecificOutput?.additionalContext ?? "",
|
|
11089
|
+
);
|
|
11090
|
+
assert.match(blockedEditContext, /Markdown drafts under `\.omx\/drafts\/\*\.md`/);
|
|
11091
|
+
for (const allowedCategory of [
|
|
11092
|
+
".omx/context/",
|
|
11093
|
+
".omx/plans/",
|
|
11094
|
+
".omx/specs/",
|
|
11095
|
+
".omx/tmp/",
|
|
11096
|
+
".omx/state/",
|
|
11097
|
+
".beads/",
|
|
11098
|
+
]) {
|
|
11099
|
+
assert.match(blockedEditContext, new RegExp(allowedCategory.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
|
|
11100
|
+
}
|
|
11101
|
+
|
|
11102
|
+
for (const blockedDraftPath of [
|
|
11103
|
+
".omx/drafts/issue-3105.MD",
|
|
11104
|
+
".omx/Drafts/issue-3105.md",
|
|
11105
|
+
".omx/drafts/issue-3105",
|
|
11106
|
+
".omx/drafts/run.sh",
|
|
11107
|
+
".omx/drafts/subdir/issue-3105.md",
|
|
11108
|
+
".omx/drafts/../../src/leak.ts",
|
|
11109
|
+
"/tmp/issue-3105.md",
|
|
11110
|
+
]) {
|
|
11111
|
+
const blockedDraft = await preToolUse("Write", `tool-ralplan-draft-block-${blockedDraftPath}`, {
|
|
11112
|
+
file_path: blockedDraftPath,
|
|
11113
|
+
content: "bad",
|
|
11114
|
+
});
|
|
11115
|
+
assert.equal(
|
|
11116
|
+
(blockedDraft.outputJson as { decision?: string } | null)?.decision,
|
|
11117
|
+
"block",
|
|
11118
|
+
`draft path should be blocked: ${blockedDraftPath}`,
|
|
11119
|
+
);
|
|
11120
|
+
}
|
|
10616
11121
|
|
|
10617
11122
|
const blockedMixedPatch = await preToolUse("apply_patch", "tool-ralplan-patch-mixed", {
|
|
10618
11123
|
input: "*** Begin Patch\n*** Add File: .omx/plans/ok.md\n+ok\n*** Add File: src/leak.ts\n+leak\n*** End Patch\n",
|
|
@@ -10622,6 +11127,18 @@ exit 0
|
|
|
10622
11127
|
assert.match(mixedReason, /apply_patch target/);
|
|
10623
11128
|
assert.match(mixedReason, /src\/leak\.ts/);
|
|
10624
11129
|
|
|
11130
|
+
const blockedMixedDraftPatch = await preToolUse("apply_patch", "tool-ralplan-draft-patch-mixed", {
|
|
11131
|
+
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",
|
|
11132
|
+
});
|
|
11133
|
+
assert.equal((blockedMixedDraftPatch.outputJson as { decision?: string } | null)?.decision, "block");
|
|
11134
|
+
assert.match(String((blockedMixedDraftPatch.outputJson as { reason?: string } | null)?.reason ?? ""), /src\/leak\.ts/);
|
|
11135
|
+
|
|
11136
|
+
const blockedMixedDraftBash = await preToolUse("Bash", "tool-ralplan-draft-bash-mixed", {
|
|
11137
|
+
command: "printf '# Draft\\n' > .omx/drafts/issue-3105.md; printf 'bad\\n' > src/leak.ts",
|
|
11138
|
+
});
|
|
11139
|
+
assert.equal((blockedMixedDraftBash.outputJson as { decision?: string } | null)?.decision, "block");
|
|
11140
|
+
assert.match(String((blockedMixedDraftBash.outputJson as { reason?: string } | null)?.reason ?? ""), /src\/leak\.ts/);
|
|
11141
|
+
|
|
10625
11142
|
const blockedUnparseablePatch = await preToolUse("apply_patch", "tool-ralplan-patch-unparseable", {
|
|
10626
11143
|
input: "not a recognizable patch",
|
|
10627
11144
|
});
|
|
@@ -10636,6 +11153,12 @@ exit 0
|
|
|
10636
11153
|
assert.match(unresolvedReason, /unresolved Bash write target/);
|
|
10637
11154
|
assert.match(unresolvedReason, /\$PLAN_PATH/);
|
|
10638
11155
|
|
|
11156
|
+
const blockedUnresolvedDraftRedirect = await preToolUse("Bash", "tool-ralplan-draft-redirect-unresolved", {
|
|
11157
|
+
command: "cat > \"$DRAFT_PATH\" <<'EOF'\ncontent\nEOF",
|
|
11158
|
+
});
|
|
11159
|
+
assert.equal((blockedUnresolvedDraftRedirect.outputJson as { decision?: string } | null)?.decision, "block");
|
|
11160
|
+
assert.match(String((blockedUnresolvedDraftRedirect.outputJson as { reason?: string } | null)?.reason ?? ""), /\$DRAFT_PATH/);
|
|
11161
|
+
|
|
10639
11162
|
const blockedTraversal = await preToolUse("Write", "tool-ralplan-traversal-block", {
|
|
10640
11163
|
file_path: ".omx/plans/../../src/leak.ts",
|
|
10641
11164
|
content: "bad",
|
|
@@ -11291,6 +11814,175 @@ PY`,
|
|
|
11291
11814
|
}
|
|
11292
11815
|
});
|
|
11293
11816
|
|
|
11817
|
+
it("keeps untyped collaboration child implementation writes blocked under active ralplan planning while typed trusted agents pass (#3116)", async () => {
|
|
11818
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3116-ralplan-untyped-"));
|
|
11819
|
+
process.env.OMX_ROOT = cwd;
|
|
11820
|
+
try {
|
|
11821
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
11822
|
+
const sessionId = "sess-3116-ralplan-untyped";
|
|
11823
|
+
const leaderThreadId = "thread-3116-ralplan-untyped-leader";
|
|
11824
|
+
const childThreadId = "thread-3116-ralplan-untyped-child";
|
|
11825
|
+
const sessionDir = join(stateDir, "sessions", sessionId);
|
|
11826
|
+
const nowIso = new Date().toISOString();
|
|
11827
|
+
await mkdir(sessionDir, { recursive: true });
|
|
11828
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId, cwd });
|
|
11829
|
+
await writeJson(join(sessionDir, "skill-active-state.json"), {
|
|
11830
|
+
version: 1,
|
|
11831
|
+
active: true,
|
|
11832
|
+
skill: "autopilot",
|
|
11833
|
+
phase: "ralplan",
|
|
11834
|
+
session_id: sessionId,
|
|
11835
|
+
thread_id: leaderThreadId,
|
|
11836
|
+
active_skills: [
|
|
11837
|
+
{ skill: "autopilot", phase: "ralplan", active: true, session_id: sessionId, thread_id: leaderThreadId },
|
|
11838
|
+
],
|
|
11839
|
+
});
|
|
11840
|
+
await writeJson(join(sessionDir, "autopilot-state.json"), {
|
|
11841
|
+
active: true,
|
|
11842
|
+
mode: "autopilot",
|
|
11843
|
+
current_phase: "ralplan",
|
|
11844
|
+
session_id: sessionId,
|
|
11845
|
+
thread_id: leaderThreadId,
|
|
11846
|
+
});
|
|
11847
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
11848
|
+
schemaVersion: 1,
|
|
11849
|
+
sessions: {
|
|
11850
|
+
[sessionId]: {
|
|
11851
|
+
session_id: sessionId,
|
|
11852
|
+
leader_thread_id: leaderThreadId,
|
|
11853
|
+
updated_at: nowIso,
|
|
11854
|
+
threads: {
|
|
11855
|
+
[leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
|
|
11856
|
+
[childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1, leader_thread_id: leaderThreadId },
|
|
11857
|
+
},
|
|
11858
|
+
},
|
|
11859
|
+
},
|
|
11860
|
+
});
|
|
11861
|
+
|
|
11862
|
+
// Untyped tracked/spawn-provenance child must NOT bypass the ralplan planning guard.
|
|
11863
|
+
const untypedChild = await dispatchCodexNativeHook(
|
|
11864
|
+
{
|
|
11865
|
+
hook_event_name: "PreToolUse",
|
|
11866
|
+
cwd,
|
|
11867
|
+
session_id: sessionId,
|
|
11868
|
+
thread_id: childThreadId,
|
|
11869
|
+
agent_role: "collaboration-child",
|
|
11870
|
+
source: {
|
|
11871
|
+
subagent: { thread_spawn: { parent_thread_id: leaderThreadId } },
|
|
11872
|
+
},
|
|
11873
|
+
tool_name: "Edit",
|
|
11874
|
+
tool_input: { file_path: "src/implementation.ts", old_string: "a", new_string: "b" },
|
|
11875
|
+
},
|
|
11876
|
+
{ cwd },
|
|
11877
|
+
);
|
|
11878
|
+
assert.equal(untypedChild.outputJson?.decision, "block");
|
|
11879
|
+
|
|
11880
|
+
// A typed trusted agent retains its existing planning exemption.
|
|
11881
|
+
const typedChild = await dispatchCodexNativeHook(
|
|
11882
|
+
{
|
|
11883
|
+
hook_event_name: "PreToolUse",
|
|
11884
|
+
cwd,
|
|
11885
|
+
session_id: sessionId,
|
|
11886
|
+
thread_id: childThreadId,
|
|
11887
|
+
agent_role: "executor",
|
|
11888
|
+
source: {
|
|
11889
|
+
subagent: { thread_spawn: { parent_thread_id: leaderThreadId } },
|
|
11890
|
+
},
|
|
11891
|
+
tool_name: "Edit",
|
|
11892
|
+
tool_input: { file_path: "src/implementation.ts", old_string: "a", new_string: "b" },
|
|
11893
|
+
},
|
|
11894
|
+
{ cwd },
|
|
11895
|
+
);
|
|
11896
|
+
assert.equal(typedChild.outputJson, null);
|
|
11897
|
+
} finally {
|
|
11898
|
+
await rm(cwd, { recursive: true, force: true });
|
|
11899
|
+
}
|
|
11900
|
+
});
|
|
11901
|
+
|
|
11902
|
+
it("keeps untyped collaboration child implementation writes blocked under active deep-interview planning while typed trusted agents pass (#3116)", async () => {
|
|
11903
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3116-di-untyped-"));
|
|
11904
|
+
process.env.OMX_ROOT = cwd;
|
|
11905
|
+
try {
|
|
11906
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
11907
|
+
const sessionId = "sess-3116-di-untyped";
|
|
11908
|
+
const leaderThreadId = "thread-3116-di-untyped-leader";
|
|
11909
|
+
const childThreadId = "thread-3116-di-untyped-child";
|
|
11910
|
+
const sessionDir = join(stateDir, "sessions", sessionId);
|
|
11911
|
+
const nowIso = new Date().toISOString();
|
|
11912
|
+
await mkdir(sessionDir, { recursive: true });
|
|
11913
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId, cwd });
|
|
11914
|
+
await writeJson(join(sessionDir, "skill-active-state.json"), {
|
|
11915
|
+
version: 1,
|
|
11916
|
+
active: true,
|
|
11917
|
+
skill: "deep-interview",
|
|
11918
|
+
phase: "planning",
|
|
11919
|
+
session_id: sessionId,
|
|
11920
|
+
thread_id: leaderThreadId,
|
|
11921
|
+
active_skills: [
|
|
11922
|
+
{ skill: "deep-interview", phase: "planning", active: true, session_id: sessionId, thread_id: leaderThreadId },
|
|
11923
|
+
],
|
|
11924
|
+
});
|
|
11925
|
+
await writeJson(join(sessionDir, "deep-interview-state.json"), {
|
|
11926
|
+
active: true,
|
|
11927
|
+
mode: "deep-interview",
|
|
11928
|
+
current_phase: "intent-first",
|
|
11929
|
+
session_id: sessionId,
|
|
11930
|
+
});
|
|
11931
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
11932
|
+
schemaVersion: 1,
|
|
11933
|
+
sessions: {
|
|
11934
|
+
[sessionId]: {
|
|
11935
|
+
session_id: sessionId,
|
|
11936
|
+
leader_thread_id: leaderThreadId,
|
|
11937
|
+
updated_at: nowIso,
|
|
11938
|
+
threads: {
|
|
11939
|
+
[leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
|
|
11940
|
+
[childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1, leader_thread_id: leaderThreadId },
|
|
11941
|
+
},
|
|
11942
|
+
},
|
|
11943
|
+
},
|
|
11944
|
+
});
|
|
11945
|
+
|
|
11946
|
+
// Untyped tracked/spawn-provenance child must NOT bypass the deep-interview planning guard.
|
|
11947
|
+
const untypedChild = await dispatchCodexNativeHook(
|
|
11948
|
+
{
|
|
11949
|
+
hook_event_name: "PreToolUse",
|
|
11950
|
+
cwd,
|
|
11951
|
+
session_id: sessionId,
|
|
11952
|
+
thread_id: childThreadId,
|
|
11953
|
+
agent_role: "collaboration-child",
|
|
11954
|
+
source: {
|
|
11955
|
+
subagent: { thread_spawn: { parent_thread_id: leaderThreadId } },
|
|
11956
|
+
},
|
|
11957
|
+
tool_name: "Edit",
|
|
11958
|
+
tool_input: { file_path: "src/implementation.ts", old_string: "a", new_string: "b" },
|
|
11959
|
+
},
|
|
11960
|
+
{ cwd },
|
|
11961
|
+
);
|
|
11962
|
+
assert.equal(untypedChild.outputJson?.decision, "block");
|
|
11963
|
+
|
|
11964
|
+
// A typed trusted agent retains its existing planning exemption.
|
|
11965
|
+
const typedChild = await dispatchCodexNativeHook(
|
|
11966
|
+
{
|
|
11967
|
+
hook_event_name: "PreToolUse",
|
|
11968
|
+
cwd,
|
|
11969
|
+
session_id: sessionId,
|
|
11970
|
+
thread_id: childThreadId,
|
|
11971
|
+
agent_role: "executor",
|
|
11972
|
+
source: {
|
|
11973
|
+
subagent: { thread_spawn: { parent_thread_id: leaderThreadId } },
|
|
11974
|
+
},
|
|
11975
|
+
tool_name: "Edit",
|
|
11976
|
+
tool_input: { file_path: "src/implementation.ts", old_string: "a", new_string: "b" },
|
|
11977
|
+
},
|
|
11978
|
+
{ cwd },
|
|
11979
|
+
);
|
|
11980
|
+
assert.equal(typedChild.outputJson, null);
|
|
11981
|
+
} finally {
|
|
11982
|
+
await rm(cwd, { recursive: true, force: true });
|
|
11983
|
+
}
|
|
11984
|
+
});
|
|
11985
|
+
|
|
11294
11986
|
it("allows null-device fd redirects while deep-interview blocks real Bash writes", async () => {
|
|
11295
11987
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-deep-interview-null-redirect-"));
|
|
11296
11988
|
try {
|
|
@@ -11490,6 +12182,38 @@ PY`,
|
|
|
11490
12182
|
assert.equal(blocker.tool_name, "multi_agent_v1.spawn_agent");
|
|
11491
12183
|
assert.match(String(blocker.error_summary), /agent thread limit reached/);
|
|
11492
12184
|
assert.ok(Date.parse(String(blocker.expires_at)) > Date.parse(String(blocker.observed_at)));
|
|
12185
|
+
assert.equal(existsSync(join(cwd, ".omx", "state", "native-subagent-support.json")), false);
|
|
12186
|
+
} finally {
|
|
12187
|
+
await rm(cwd, { recursive: true, force: true });
|
|
12188
|
+
}
|
|
12189
|
+
});
|
|
12190
|
+
|
|
12191
|
+
it("records unsupported native subagent support blocker from spawn_agent PostToolUse output", async () => {
|
|
12192
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-support-record-"));
|
|
12193
|
+
try {
|
|
12194
|
+
const result = await dispatchCodexNativeHook(
|
|
12195
|
+
{
|
|
12196
|
+
hook_event_name: "PostToolUse",
|
|
12197
|
+
cwd,
|
|
12198
|
+
session_id: "sess-subagent-support-record",
|
|
12199
|
+
thread_id: "thread-subagent-support-record",
|
|
12200
|
+
turn_id: "turn-subagent-support-record",
|
|
12201
|
+
tool_name: "multi_agent_v1.spawn_agent",
|
|
12202
|
+
tool_response: { error: "unknown tool: multi_agent_v1.spawn_agent is unavailable" },
|
|
12203
|
+
},
|
|
12204
|
+
{ cwd },
|
|
12205
|
+
);
|
|
12206
|
+
|
|
12207
|
+
assert.equal(result.omxEventName, "post-tool-use");
|
|
12208
|
+
const blocker = JSON.parse(
|
|
12209
|
+
await readFile(join(cwd, ".omx", "state", "native-subagent-support.json"), "utf-8"),
|
|
12210
|
+
) as Record<string, unknown>;
|
|
12211
|
+
assert.equal(blocker.status, "unsupported");
|
|
12212
|
+
assert.equal(blocker.reason, "multi_agent_v1_unavailable");
|
|
12213
|
+
assert.equal(blocker.session_id, "sess-subagent-support-record");
|
|
12214
|
+
assert.equal(blocker.thread_id, "thread-subagent-support-record");
|
|
12215
|
+
assert.equal(blocker.tool_name, "multi_agent_v1.spawn_agent");
|
|
12216
|
+
assert.match(String(blocker.evidence), /unknown tool/);
|
|
11493
12217
|
} finally {
|
|
11494
12218
|
await rm(cwd, { recursive: true, force: true });
|
|
11495
12219
|
}
|
|
@@ -17875,21 +18599,20 @@ PY`,
|
|
|
17875
18599
|
execFileSync("git", ["commit", "-m", "init"], { cwd, stdio: "ignore" });
|
|
17876
18600
|
await writeFile(join(cwd, "src", "scripts", "codex-native-hook.ts"), "export const hook = 2;\n", "utf-8");
|
|
17877
18601
|
|
|
17878
|
-
const
|
|
17879
|
-
|
|
17880
|
-
|
|
17881
|
-
|
|
17882
|
-
|
|
17883
|
-
|
|
17884
|
-
},
|
|
17885
|
-
{ cwd },
|
|
17886
|
-
);
|
|
18602
|
+
const output = parseSingleJsonStdout(runNativeHookCli({
|
|
18603
|
+
hook_event_name: "Stop",
|
|
18604
|
+
cwd,
|
|
18605
|
+
session_id: "sess-stop-doc-refresh",
|
|
18606
|
+
last_assistant_message: "Launch-ready: yes",
|
|
18607
|
+
}, { cwd }));
|
|
17887
18608
|
|
|
17888
|
-
assert.
|
|
17889
|
-
assert.equal(
|
|
17890
|
-
assert.equal(
|
|
17891
|
-
assert.
|
|
17892
|
-
assert.
|
|
18609
|
+
assert.deepEqual(Object.keys(output).sort(), ["systemMessage"]);
|
|
18610
|
+
assert.equal("hookSpecificOutput" in output, false);
|
|
18611
|
+
assert.equal("decision" in output, false);
|
|
18612
|
+
assert.equal("reason" in output, false);
|
|
18613
|
+
assert.equal("stopReason" in output, false);
|
|
18614
|
+
assert.match(String(output.systemMessage ?? ""), /Document-refresh warning/);
|
|
18615
|
+
assert.match(String(output.systemMessage ?? ""), /staged \+ unstaged changes/);
|
|
17893
18616
|
} finally {
|
|
17894
18617
|
await rm(cwd, { recursive: true, force: true });
|
|
17895
18618
|
}
|
|
@@ -20888,6 +21611,21 @@ PY`,
|
|
|
20888
21611
|
|
|
20889
21612
|
assert.equal(result.omxEventName, "pre-tool-use");
|
|
20890
21613
|
assert.equal(result.outputJson, null);
|
|
21614
|
+
|
|
21615
|
+
const allowedDraftPatch = await dispatchCodexNativeHook(
|
|
21616
|
+
{
|
|
21617
|
+
hook_event_name: "PreToolUse",
|
|
21618
|
+
cwd,
|
|
21619
|
+
session_id: nativeSessionId,
|
|
21620
|
+
thread_id: "thread-ralplan-native-map-draft-artifact",
|
|
21621
|
+
tool_name: "apply_patch",
|
|
21622
|
+
tool_input: {
|
|
21623
|
+
input: `*** Begin Patch\n*** Add File: ${join(cwd, ".omx", "drafts", "issue-3105.md")}\n+# Draft\n*** End Patch\n`,
|
|
21624
|
+
},
|
|
21625
|
+
},
|
|
21626
|
+
{ cwd },
|
|
21627
|
+
);
|
|
21628
|
+
assert.equal(allowedDraftPatch.outputJson, null);
|
|
20891
21629
|
} finally {
|
|
20892
21630
|
await rm(cwd, { recursive: true, force: true });
|
|
20893
21631
|
}
|
|
@@ -21006,6 +21744,21 @@ PY`,
|
|
|
21006
21744
|
cwd: ownerCwd,
|
|
21007
21745
|
});
|
|
21008
21746
|
|
|
21747
|
+
const allowedForeignDraft = await dispatchCodexNativeHook(
|
|
21748
|
+
{
|
|
21749
|
+
hook_event_name: "PreToolUse",
|
|
21750
|
+
cwd,
|
|
21751
|
+
session_id: "019e-ralplan-live-root-unresolved-current",
|
|
21752
|
+
thread_id: "thread-ralplan-live-root-conflict",
|
|
21753
|
+
tool_name: "apply_patch",
|
|
21754
|
+
tool_input: {
|
|
21755
|
+
input: `*** Begin Patch\n*** Add File: ${join(cwd, ".omx", "drafts", "issue-3105.md")}\n+# Draft\n*** End Patch\n`,
|
|
21756
|
+
},
|
|
21757
|
+
},
|
|
21758
|
+
{ cwd },
|
|
21759
|
+
);
|
|
21760
|
+
assert.equal(allowedForeignDraft.outputJson, null);
|
|
21761
|
+
|
|
21009
21762
|
const result = await dispatchCodexNativeHook(
|
|
21010
21763
|
{
|
|
21011
21764
|
hook_event_name: "PreToolUse",
|
|
@@ -21099,14 +21852,29 @@ PY`,
|
|
|
21099
21852
|
|
|
21100
21853
|
assert.equal(result.omxEventName, "pre-tool-use");
|
|
21101
21854
|
assert.equal(result.outputJson, null);
|
|
21102
|
-
} finally {
|
|
21103
|
-
await rm(cwd, { recursive: true, force: true });
|
|
21104
|
-
}
|
|
21105
|
-
});
|
|
21106
21855
|
|
|
21107
|
-
|
|
21108
|
-
|
|
21109
|
-
|
|
21856
|
+
const allowedDraftPatch = await dispatchCodexNativeHook(
|
|
21857
|
+
{
|
|
21858
|
+
hook_event_name: "PreToolUse",
|
|
21859
|
+
cwd,
|
|
21860
|
+
session_id: nativeSessionId,
|
|
21861
|
+
thread_id: "thread-ralplan-live-root-owner-draft-pass",
|
|
21862
|
+
tool_name: "apply_patch",
|
|
21863
|
+
tool_input: {
|
|
21864
|
+
input: `*** Begin Patch\n*** Add File: ${join(cwd, ".omx", "drafts", "live-root-owner.md")}\n+# Draft\n*** End Patch\n`,
|
|
21865
|
+
},
|
|
21866
|
+
},
|
|
21867
|
+
{ cwd },
|
|
21868
|
+
);
|
|
21869
|
+
assert.equal(allowedDraftPatch.outputJson, null);
|
|
21870
|
+
} finally {
|
|
21871
|
+
await rm(cwd, { recursive: true, force: true });
|
|
21872
|
+
}
|
|
21873
|
+
});
|
|
21874
|
+
|
|
21875
|
+
it("does not block unrelated native Codex ids when current OMX session mapping does not match", async () => {
|
|
21876
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-native-map-unrelated-"));
|
|
21877
|
+
try {
|
|
21110
21878
|
const stateDir = join(cwd, ".omx", "state");
|
|
21111
21879
|
const sessionId = "sess-ralplan-native-map-owner";
|
|
21112
21880
|
const ownerNativeSessionId = "019e-ralplan-native-owner";
|
|
@@ -21459,6 +22227,189 @@ PY`,
|
|
|
21459
22227
|
}
|
|
21460
22228
|
});
|
|
21461
22229
|
|
|
22230
|
+
it("blocks unsupported active conductor source edits with native delegation recovery guidance", async () => {
|
|
22231
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-unsupported-conductor-source-"));
|
|
22232
|
+
try {
|
|
22233
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
22234
|
+
const sessionId = "sess-unsupported-conductor-source";
|
|
22235
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
22236
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
22237
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "ultragoal", "planning");
|
|
22238
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ultragoal-state.json"), {
|
|
22239
|
+
active: true,
|
|
22240
|
+
mode: "ultragoal",
|
|
22241
|
+
current_phase: "planning",
|
|
22242
|
+
session_id: sessionId,
|
|
22243
|
+
});
|
|
22244
|
+
await writeJson(join(stateDir, "native-subagent-support.json"), {
|
|
22245
|
+
schema_version: 1,
|
|
22246
|
+
status: "unsupported",
|
|
22247
|
+
reason: "multi_agent_v1_unavailable",
|
|
22248
|
+
session_id: sessionId,
|
|
22249
|
+
evidence: "unknown tool: multi_agent_v1.spawn_agent",
|
|
22250
|
+
observed_at: new Date().toISOString(),
|
|
22251
|
+
cwd,
|
|
22252
|
+
});
|
|
22253
|
+
|
|
22254
|
+
const result = await dispatchCodexNativeHook(
|
|
22255
|
+
{
|
|
22256
|
+
hook_event_name: "PreToolUse",
|
|
22257
|
+
cwd,
|
|
22258
|
+
session_id: sessionId,
|
|
22259
|
+
thread_id: "thread-unsupported-conductor-source",
|
|
22260
|
+
tool_name: "Write",
|
|
22261
|
+
tool_input: { file_path: "src/runtime.ts", content: "export const value = 1;\n" },
|
|
22262
|
+
},
|
|
22263
|
+
{ cwd },
|
|
22264
|
+
);
|
|
22265
|
+
|
|
22266
|
+
const output = result.outputJson as { decision?: string; hookSpecificOutput?: { additionalContext?: string } } | null;
|
|
22267
|
+
const context = String(output?.hookSpecificOutput?.additionalContext ?? "");
|
|
22268
|
+
assert.equal(output?.decision, "block");
|
|
22269
|
+
assert.match(String(result.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ultragoal phase: planning\)/);
|
|
22270
|
+
assert.match(context, /Native subagent support is unavailable in this environment/);
|
|
22271
|
+
assert.match(context, /Reason: multi_agent_v1_unavailable/);
|
|
22272
|
+
assert.match(context, /blocked\/cancelled/);
|
|
22273
|
+
assert.match(context, /do not call multi_agent_v1\.close_agent/i);
|
|
22274
|
+
} finally {
|
|
22275
|
+
await rm(cwd, { recursive: true, force: true });
|
|
22276
|
+
}
|
|
22277
|
+
});
|
|
22278
|
+
|
|
22279
|
+
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 () => {
|
|
22280
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-conductor-3119-quote-deadlock-"));
|
|
22281
|
+
try {
|
|
22282
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
22283
|
+
const sessionId = "sess-conductor-3119-quote-deadlock";
|
|
22284
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
22285
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
22286
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "ultragoal", "planning");
|
|
22287
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ultragoal-state.json"), {
|
|
22288
|
+
active: true,
|
|
22289
|
+
mode: "ultragoal",
|
|
22290
|
+
current_phase: "planning",
|
|
22291
|
+
session_id: sessionId,
|
|
22292
|
+
});
|
|
22293
|
+
// Native delegation is genuinely unsupported (explicit negative evidence).
|
|
22294
|
+
await writeJson(join(stateDir, "native-subagent-support.json"), {
|
|
22295
|
+
schema_version: 1,
|
|
22296
|
+
status: "unsupported",
|
|
22297
|
+
reason: "multi_agent_v1_unavailable",
|
|
22298
|
+
session_id: sessionId,
|
|
22299
|
+
evidence: "unknown tool: multi_agent_v1.spawn_agent",
|
|
22300
|
+
observed_at: new Date().toISOString(),
|
|
22301
|
+
cwd,
|
|
22302
|
+
});
|
|
22303
|
+
|
|
22304
|
+
const dispatch = (command: string) =>
|
|
22305
|
+
dispatchCodexNativeHook(
|
|
22306
|
+
{
|
|
22307
|
+
hook_event_name: "PreToolUse",
|
|
22308
|
+
cwd,
|
|
22309
|
+
session_id: sessionId,
|
|
22310
|
+
thread_id: "thread-conductor-3119-quote-deadlock",
|
|
22311
|
+
tool_name: "Bash",
|
|
22312
|
+
tool_input: { command },
|
|
22313
|
+
},
|
|
22314
|
+
{ cwd },
|
|
22315
|
+
);
|
|
22316
|
+
|
|
22317
|
+
// Deadlock prevention (defect B): the Conductor must still terminalize its
|
|
22318
|
+
// own workflow state even when delegation is genuinely unsupported, even
|
|
22319
|
+
// when the JSON payload contains a `>` character.
|
|
22320
|
+
const terminalBlockedWrite = await dispatch(
|
|
22321
|
+
"omx state write --mode ultragoal --input '{\"active\":true,\"current_phase\":\"blocked\",\"reason\":\"native delegation unavailable -> terminalized\"}' --json",
|
|
22322
|
+
);
|
|
22323
|
+
assert.notEqual((terminalBlockedWrite.outputJson as { decision?: string } | null)?.decision, "block");
|
|
22324
|
+
|
|
22325
|
+
// Defect C: quoted regex/source text with redirect metacharacters is not a
|
|
22326
|
+
// write target, so issue creation is not falsely blocked.
|
|
22327
|
+
const issueCreate = await dispatch(
|
|
22328
|
+
"gh issue create --title x --body 'Guard misparsed regex /[^>]+>{1,2}/ as a redirect target'",
|
|
22329
|
+
);
|
|
22330
|
+
assert.notEqual((issueCreate.outputJson as { decision?: string } | null)?.decision, "block");
|
|
22331
|
+
|
|
22332
|
+
// Fail-closed preserved: a REAL unquoted redirect to a non-metadata path is
|
|
22333
|
+
// still blocked.
|
|
22334
|
+
const realRedirect = await dispatch("printf pwn > src/runtime.ts");
|
|
22335
|
+
assert.equal((realRedirect.outputJson as { decision?: string } | null)?.decision, "block");
|
|
22336
|
+
assert.match(
|
|
22337
|
+
String((realRedirect.outputJson as { reason?: string } | null)?.reason ?? ""),
|
|
22338
|
+
/not workflow state\/ledger\/mailbox\/handoff metadata/,
|
|
22339
|
+
);
|
|
22340
|
+
|
|
22341
|
+
// A real unquoted redirect to allowed workflow metadata still passes.
|
|
22342
|
+
const metadataRedirect = await dispatch("printf blocked > .omx/state/conductor.log");
|
|
22343
|
+
assert.notEqual((metadataRedirect.outputJson as { decision?: string } | null)?.decision, "block");
|
|
22344
|
+
} finally {
|
|
22345
|
+
await rm(cwd, { recursive: true, force: true });
|
|
22346
|
+
}
|
|
22347
|
+
});
|
|
22348
|
+
|
|
22349
|
+
it("fail-closed: escaped-quote and ANSI-C quoted redirects to source stay blocked while legit quoted data and nested shells behave (#3119)", async () => {
|
|
22350
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-conductor-3119-escaped-quote-"));
|
|
22351
|
+
try {
|
|
22352
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
22353
|
+
const sessionId = "sess-conductor-3119-escaped-quote";
|
|
22354
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
22355
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
22356
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "ultragoal", "planning");
|
|
22357
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ultragoal-state.json"), {
|
|
22358
|
+
active: true,
|
|
22359
|
+
mode: "ultragoal",
|
|
22360
|
+
current_phase: "planning",
|
|
22361
|
+
session_id: sessionId,
|
|
22362
|
+
});
|
|
22363
|
+
|
|
22364
|
+
const dispatch = (command: string) =>
|
|
22365
|
+
dispatchCodexNativeHook(
|
|
22366
|
+
{
|
|
22367
|
+
hook_event_name: "PreToolUse",
|
|
22368
|
+
cwd,
|
|
22369
|
+
session_id: sessionId,
|
|
22370
|
+
thread_id: "thread-conductor-3119-escaped-quote",
|
|
22371
|
+
tool_name: "Bash",
|
|
22372
|
+
tool_input: { command },
|
|
22373
|
+
},
|
|
22374
|
+
{ cwd },
|
|
22375
|
+
);
|
|
22376
|
+
|
|
22377
|
+
// A top-level escaped quote (\' \") is a LITERAL char in bash, not a quote
|
|
22378
|
+
// opener, and $'...' is ANSI-C quoting. None of these may hide the real
|
|
22379
|
+
// `>` redirect \u2014 all of these genuinely write src/runtime.ts and MUST block.
|
|
22380
|
+
const mustBlock = [
|
|
22381
|
+
"printf pwn > src/runtime.ts", // plain control
|
|
22382
|
+
"printf pwn \\' > src/runtime.ts \\'", // escaped single quote
|
|
22383
|
+
"printf pwn \\\" > src/runtime.ts \\\"", // escaped double quote
|
|
22384
|
+
"printf pwn $'\\'' > src/runtime.ts $'\\''", // ANSI-C escaped quote
|
|
22385
|
+
"bash -c 'printf pwn > src/runtime.ts'", // nested shell redirect
|
|
22386
|
+
];
|
|
22387
|
+
for (const command of mustBlock) {
|
|
22388
|
+
const result = await dispatch(command);
|
|
22389
|
+
assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block", command);
|
|
22390
|
+
assert.match(
|
|
22391
|
+
String((result.outputJson as { reason?: string } | null)?.reason ?? ""),
|
|
22392
|
+
/not workflow state\/ledger\/mailbox\/handoff metadata/,
|
|
22393
|
+
command,
|
|
22394
|
+
);
|
|
22395
|
+
}
|
|
22396
|
+
|
|
22397
|
+
// Legitimate quoted DATA metacharacters (single quotes, double quotes,
|
|
22398
|
+
// ANSI-C) are not redirects and must not be falsely blocked.
|
|
22399
|
+
const mustNotBlock = [
|
|
22400
|
+
"gh issue create --title x --body 'a > b regex /[^>]+>{1,2}/'",
|
|
22401
|
+
"echo \"value > threshold\"",
|
|
22402
|
+
"printf $'a>b'",
|
|
22403
|
+
];
|
|
22404
|
+
for (const command of mustNotBlock) {
|
|
22405
|
+
const result = await dispatch(command);
|
|
22406
|
+
assert.notEqual((result.outputJson as { decision?: string } | null)?.decision, "block", command);
|
|
22407
|
+
}
|
|
22408
|
+
} finally {
|
|
22409
|
+
await rm(cwd, { recursive: true, force: true });
|
|
22410
|
+
}
|
|
22411
|
+
});
|
|
22412
|
+
|
|
21462
22413
|
it("blocks Main-root ralplan writes even when payload has only a typed agent_role", async () => {
|
|
21463
22414
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-agent-role-main-"));
|
|
21464
22415
|
try {
|
|
@@ -21724,25 +22675,22 @@ PY`,
|
|
|
21724
22675
|
}
|
|
21725
22676
|
});
|
|
21726
22677
|
|
|
21727
|
-
it("blocks
|
|
21728
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-
|
|
21729
|
-
|
|
21730
|
-
const originalOmxStateRoot = process.env.OMX_STATE_ROOT;
|
|
21731
|
-
const originalOmxTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
|
|
22678
|
+
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 () => {
|
|
22679
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3117-corrupt-leader-no-lead-"));
|
|
22680
|
+
process.env.OMX_ROOT = cwd;
|
|
21732
22681
|
try {
|
|
21733
|
-
process.env.OMX_ROOT = cwd;
|
|
21734
|
-
delete process.env.OMX_STATE_ROOT;
|
|
21735
|
-
delete process.env.OMX_TEAM_STATE_ROOT;
|
|
21736
22682
|
const stateDir = join(cwd, ".omx", "state");
|
|
21737
|
-
const sessionId = "sess-
|
|
21738
|
-
const leaderThreadId = "thread-
|
|
21739
|
-
const
|
|
22683
|
+
const sessionId = "sess-3117-corrupt-leader-no-lead";
|
|
22684
|
+
const leaderThreadId = "thread-3117-corrupt-leader-no-lead-leader";
|
|
22685
|
+
const childThreadId = "thread-3117-corrupt-leader-no-lead-child";
|
|
21740
22686
|
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
22687
|
+
// native_session_id anchors the leader identity even though the tracker omits
|
|
22688
|
+
// leader_thread_id and corruptly labels the leader kind:"subagent".
|
|
21741
22689
|
await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId });
|
|
21742
|
-
await writeSessionSkillActiveState(stateDir, sessionId, "
|
|
21743
|
-
await writeJson(join(stateDir, "sessions", sessionId, "
|
|
22690
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
|
|
22691
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
|
|
21744
22692
|
active: true,
|
|
21745
|
-
mode: "
|
|
22693
|
+
mode: "ralph",
|
|
21746
22694
|
current_phase: "executing",
|
|
21747
22695
|
session_id: sessionId,
|
|
21748
22696
|
});
|
|
@@ -21751,54 +22699,59 @@ PY`,
|
|
|
21751
22699
|
sessions: {
|
|
21752
22700
|
[sessionId]: {
|
|
21753
22701
|
session_id: sessionId,
|
|
21754
|
-
leader_thread_id: leaderThreadId,
|
|
21755
|
-
updated_at: nowIso,
|
|
21756
22702
|
threads: {
|
|
21757
|
-
[leaderThreadId]: { thread_id: leaderThreadId, kind: "
|
|
22703
|
+
[leaderThreadId]: { thread_id: leaderThreadId, kind: "subagent", mode: "collaboration-child" },
|
|
22704
|
+
[childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child" },
|
|
21758
22705
|
},
|
|
21759
22706
|
},
|
|
21760
22707
|
},
|
|
21761
22708
|
});
|
|
21762
22709
|
|
|
21763
|
-
|
|
22710
|
+
// Untyped leader payload must stay blocked: leader identity is anchored to
|
|
22711
|
+
// native_session_id, not the corrupt tracker kind.
|
|
22712
|
+
const untypedLeader = await dispatchCodexNativeHook(
|
|
21764
22713
|
{
|
|
21765
22714
|
hook_event_name: "PreToolUse",
|
|
21766
22715
|
cwd,
|
|
21767
22716
|
session_id: sessionId,
|
|
21768
22717
|
thread_id: leaderThreadId,
|
|
21769
|
-
agent_role: "executor",
|
|
21770
|
-
source: {
|
|
21771
|
-
subagent: {
|
|
21772
|
-
thread_spawn: {
|
|
21773
|
-
parent_thread_id: leaderThreadId,
|
|
21774
|
-
},
|
|
21775
|
-
},
|
|
21776
|
-
},
|
|
21777
22718
|
tool_name: "apply_patch",
|
|
21778
|
-
tool_input: { file_path: "
|
|
22719
|
+
tool_input: { file_path: "src/feature.ts" },
|
|
21779
22720
|
},
|
|
21780
22721
|
{ cwd },
|
|
21781
22722
|
);
|
|
22723
|
+
assert.equal(untypedLeader.outputJson?.decision, "block");
|
|
22724
|
+
assert.match(String(untypedLeader.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
|
|
21782
22725
|
|
|
21783
|
-
|
|
21784
|
-
|
|
22726
|
+
// Control: a genuine non-leader child stays trusted (P1 preserved).
|
|
22727
|
+
const untypedChild = await dispatchCodexNativeHook(
|
|
22728
|
+
{
|
|
22729
|
+
hook_event_name: "PreToolUse",
|
|
22730
|
+
cwd,
|
|
22731
|
+
session_id: sessionId,
|
|
22732
|
+
thread_id: childThreadId,
|
|
22733
|
+
tool_name: "apply_patch",
|
|
22734
|
+
tool_input: { file_path: "src/feature.ts" },
|
|
22735
|
+
},
|
|
22736
|
+
{ cwd },
|
|
22737
|
+
);
|
|
22738
|
+
assert.equal(untypedChild.outputJson, null);
|
|
21785
22739
|
} finally {
|
|
21786
|
-
if (originalOmxRoot === undefined) delete process.env.OMX_ROOT;
|
|
21787
|
-
else process.env.OMX_ROOT = originalOmxRoot;
|
|
21788
|
-
if (originalOmxStateRoot === undefined) delete process.env.OMX_STATE_ROOT;
|
|
21789
|
-
else process.env.OMX_STATE_ROOT = originalOmxStateRoot;
|
|
21790
|
-
if (originalOmxTeamStateRoot === undefined) delete process.env.OMX_TEAM_STATE_ROOT;
|
|
21791
|
-
else process.env.OMX_TEAM_STATE_ROOT = originalOmxTeamStateRoot;
|
|
21792
22740
|
await rm(cwd, { recursive: true, force: true });
|
|
21793
22741
|
}
|
|
21794
22742
|
});
|
|
21795
22743
|
|
|
21796
|
-
it("
|
|
21797
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-
|
|
22744
|
+
it("fails closed for untyped provenance when no authoritative leader anchor exists (#3117 P2)", async () => {
|
|
22745
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3117-no-leader-anchor-"));
|
|
22746
|
+
process.env.OMX_ROOT = cwd;
|
|
21798
22747
|
try {
|
|
21799
22748
|
const stateDir = join(cwd, ".omx", "state");
|
|
21800
|
-
const sessionId = "sess-
|
|
22749
|
+
const sessionId = "sess-3117-no-leader-anchor";
|
|
22750
|
+
const leaderThreadId = "thread-3117-no-leader-anchor-leader";
|
|
22751
|
+
const childThreadId = "thread-3117-no-leader-anchor-child";
|
|
21801
22752
|
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
22753
|
+
// No native_session_id / owner ids and no tracker leader_thread_id: the leader is
|
|
22754
|
+
// unidentifiable, so untyped provenance must not be inferred from kind:"subagent".
|
|
21802
22755
|
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
21803
22756
|
await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
|
|
21804
22757
|
await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
|
|
@@ -21807,71 +22760,754 @@ PY`,
|
|
|
21807
22760
|
current_phase: "executing",
|
|
21808
22761
|
session_id: sessionId,
|
|
21809
22762
|
});
|
|
21810
|
-
|
|
21811
|
-
|
|
21812
|
-
{
|
|
21813
|
-
|
|
21814
|
-
|
|
21815
|
-
|
|
21816
|
-
|
|
21817
|
-
|
|
21818
|
-
|
|
22763
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
22764
|
+
schemaVersion: 1,
|
|
22765
|
+
sessions: {
|
|
22766
|
+
[sessionId]: {
|
|
22767
|
+
session_id: sessionId,
|
|
22768
|
+
threads: {
|
|
22769
|
+
[leaderThreadId]: { thread_id: leaderThreadId, kind: "subagent", mode: "collaboration-child" },
|
|
22770
|
+
[childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child" },
|
|
22771
|
+
},
|
|
22772
|
+
},
|
|
21819
22773
|
},
|
|
21820
|
-
|
|
21821
|
-
);
|
|
21822
|
-
assert.equal(blocked.outputJson?.decision, "block");
|
|
21823
|
-
assert.match(String(blocked.outputJson?.reason ?? ""), /ralph phase: executing/);
|
|
22774
|
+
});
|
|
21824
22775
|
|
|
21825
|
-
for (const [
|
|
21826
|
-
|
|
21827
|
-
["Edit", ".omx/specs/conductor-owned-spec.md"],
|
|
21828
|
-
] as const) {
|
|
21829
|
-
const planningArtifactWrite = await dispatchCodexNativeHook(
|
|
22776
|
+
for (const threadId of [leaderThreadId, childThreadId]) {
|
|
22777
|
+
const result = await dispatchCodexNativeHook(
|
|
21830
22778
|
{
|
|
21831
22779
|
hook_event_name: "PreToolUse",
|
|
21832
22780
|
cwd,
|
|
21833
22781
|
session_id: sessionId,
|
|
21834
|
-
thread_id:
|
|
21835
|
-
tool_name:
|
|
21836
|
-
tool_input: { file_path:
|
|
22782
|
+
thread_id: threadId,
|
|
22783
|
+
tool_name: "apply_patch",
|
|
22784
|
+
tool_input: { file_path: "src/feature.ts" },
|
|
21837
22785
|
},
|
|
21838
22786
|
{ cwd },
|
|
21839
22787
|
);
|
|
21840
|
-
assert.equal(
|
|
21841
|
-
assert.match(String(
|
|
22788
|
+
assert.equal(result.outputJson?.decision, "block", threadId);
|
|
22789
|
+
assert.match(String(result.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
|
|
21842
22790
|
}
|
|
22791
|
+
} finally {
|
|
22792
|
+
await rm(cwd, { recursive: true, force: true });
|
|
22793
|
+
}
|
|
22794
|
+
});
|
|
21843
22795
|
|
|
21844
|
-
|
|
21845
|
-
|
|
21846
|
-
|
|
21847
|
-
|
|
21848
|
-
|
|
21849
|
-
|
|
21850
|
-
|
|
21851
|
-
|
|
22796
|
+
it("does not borrow leader anchors from a foreign root session.json when evaluating another session (#3117 P3)", async () => {
|
|
22797
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3117-cross-session-anchor-"));
|
|
22798
|
+
process.env.OMX_ROOT = cwd;
|
|
22799
|
+
try {
|
|
22800
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
22801
|
+
const foreignSessionId = "sess-3117-p3-foreign-A";
|
|
22802
|
+
const foreignLeaderThreadId = "thread-3117-p3-A-leader";
|
|
22803
|
+
const sessionId = "sess-3117-p3-checked-B";
|
|
22804
|
+
const leaderThreadId = "thread-3117-p3-B-leader";
|
|
22805
|
+
const childThreadId = "thread-3117-p3-B-child";
|
|
22806
|
+
const sessionPath = join(stateDir, "session.json");
|
|
22807
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
22808
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
|
|
22809
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
|
|
22810
|
+
active: true,
|
|
22811
|
+
mode: "ralph",
|
|
22812
|
+
current_phase: "executing",
|
|
22813
|
+
session_id: sessionId,
|
|
22814
|
+
});
|
|
22815
|
+
// Evaluated session B has a corrupt tracker: no leader_thread_id, leader mislabeled kind:"subagent".
|
|
22816
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
22817
|
+
schemaVersion: 1,
|
|
22818
|
+
sessions: {
|
|
22819
|
+
[sessionId]: {
|
|
22820
|
+
session_id: sessionId,
|
|
22821
|
+
threads: {
|
|
22822
|
+
[leaderThreadId]: { thread_id: leaderThreadId, kind: "subagent", mode: "collaboration-child" },
|
|
22823
|
+
[childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child" },
|
|
22824
|
+
},
|
|
22825
|
+
},
|
|
21852
22826
|
},
|
|
21853
|
-
|
|
21854
|
-
);
|
|
21855
|
-
assert.equal(allowed.outputJson, null);
|
|
22827
|
+
});
|
|
21856
22828
|
|
|
21857
|
-
const
|
|
22829
|
+
const dispatchThread = async (threadId: string) => dispatchCodexNativeHook(
|
|
21858
22830
|
{
|
|
21859
22831
|
hook_event_name: "PreToolUse",
|
|
21860
22832
|
cwd,
|
|
21861
22833
|
session_id: sessionId,
|
|
21862
|
-
thread_id:
|
|
21863
|
-
tool_name: "
|
|
21864
|
-
tool_input: { file_path: "
|
|
22834
|
+
thread_id: threadId,
|
|
22835
|
+
tool_name: "apply_patch",
|
|
22836
|
+
tool_input: { file_path: "src/feature.ts" },
|
|
21865
22837
|
},
|
|
21866
22838
|
{ cwd },
|
|
21867
22839
|
);
|
|
21868
|
-
assert.equal(protectedRawState.outputJson?.decision, "block");
|
|
21869
22840
|
|
|
21870
|
-
|
|
21871
|
-
|
|
21872
|
-
|
|
21873
|
-
|
|
21874
|
-
|
|
22841
|
+
// Root session.json owns a DIFFERENT session A: its native/owner ids must not
|
|
22842
|
+
// anchor session B, so B's mislabeled leader stays blocked (fail closed).
|
|
22843
|
+
await writeJson(sessionPath, { session_id: foreignSessionId, native_session_id: foreignLeaderThreadId });
|
|
22844
|
+
const foreignLeader = await dispatchThread(leaderThreadId);
|
|
22845
|
+
assert.equal(foreignLeader.outputJson?.decision, "block");
|
|
22846
|
+
assert.match(String(foreignLeader.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
|
|
22847
|
+
const foreignChild = await dispatchThread(childThreadId);
|
|
22848
|
+
assert.equal(foreignChild.outputJson?.decision, "block");
|
|
22849
|
+
|
|
22850
|
+
// Control: when the root session.json owns the evaluated session B, its
|
|
22851
|
+
// native_session_id anchors the leader (blocked) while a genuine child is trusted.
|
|
22852
|
+
await writeJson(sessionPath, { session_id: sessionId, native_session_id: leaderThreadId });
|
|
22853
|
+
const ownedLeader = await dispatchThread(leaderThreadId);
|
|
22854
|
+
assert.equal(ownedLeader.outputJson?.decision, "block");
|
|
22855
|
+
assert.match(String(ownedLeader.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
|
|
22856
|
+
const ownedChild = await dispatchThread(childThreadId);
|
|
22857
|
+
assert.equal(ownedChild.outputJson, null);
|
|
22858
|
+
} finally {
|
|
22859
|
+
await rm(cwd, { recursive: true, force: true });
|
|
22860
|
+
}
|
|
22861
|
+
});
|
|
22862
|
+
|
|
22863
|
+
it("fails closed when session.json carries only owner session ids without a leader thread anchor (#3117 P4)", async () => {
|
|
22864
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3117-owner-only-anchor-"));
|
|
22865
|
+
process.env.OMX_ROOT = cwd;
|
|
22866
|
+
try {
|
|
22867
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
22868
|
+
const sessionId = "sess-3117-p4-owner-only";
|
|
22869
|
+
const leaderThreadId = "thread-3117-p4-leader";
|
|
22870
|
+
const childThreadId = "thread-3117-p4-child";
|
|
22871
|
+
const untrackedThreadId = "thread-3117-p4-untracked";
|
|
22872
|
+
const sessionPath = join(stateDir, "session.json");
|
|
22873
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
22874
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
|
|
22875
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
|
|
22876
|
+
active: true,
|
|
22877
|
+
mode: "ralph",
|
|
22878
|
+
current_phase: "executing",
|
|
22879
|
+
session_id: sessionId,
|
|
22880
|
+
});
|
|
22881
|
+
// Corrupt tracker for the evaluated session: no leader_thread_id, leader mislabeled kind:"subagent".
|
|
22882
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
22883
|
+
schemaVersion: 1,
|
|
22884
|
+
sessions: {
|
|
22885
|
+
[sessionId]: {
|
|
22886
|
+
session_id: sessionId,
|
|
22887
|
+
threads: {
|
|
22888
|
+
[leaderThreadId]: { thread_id: leaderThreadId, kind: "subagent", mode: "collaboration-child" },
|
|
22889
|
+
[childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child" },
|
|
22890
|
+
},
|
|
22891
|
+
},
|
|
22892
|
+
},
|
|
22893
|
+
});
|
|
22894
|
+
|
|
22895
|
+
const dispatchThread = async (threadId: string) => dispatchCodexNativeHook(
|
|
22896
|
+
{
|
|
22897
|
+
hook_event_name: "PreToolUse",
|
|
22898
|
+
cwd,
|
|
22899
|
+
session_id: sessionId,
|
|
22900
|
+
thread_id: threadId,
|
|
22901
|
+
tool_name: "apply_patch",
|
|
22902
|
+
tool_input: { file_path: "src/feature.ts" },
|
|
22903
|
+
},
|
|
22904
|
+
{ cwd },
|
|
22905
|
+
);
|
|
22906
|
+
|
|
22907
|
+
// session.json maps to the evaluated session via owner ids but has NO
|
|
22908
|
+
// native_session_id: owner session ids are not leader thread anchors, so trust
|
|
22909
|
+
// must fail closed rather than treat their presence as an anchor (#3117 P4).
|
|
22910
|
+
await writeJson(sessionPath, {
|
|
22911
|
+
session_id: sessionId,
|
|
22912
|
+
owner_omx_session_id: "owner-omx-3117-p4",
|
|
22913
|
+
owner_codex_session_id: "owner-codex-3117-p4",
|
|
22914
|
+
});
|
|
22915
|
+
const ownerOnlyLeader = await dispatchThread(leaderThreadId);
|
|
22916
|
+
assert.equal(ownerOnlyLeader.outputJson?.decision, "block");
|
|
22917
|
+
assert.match(String(ownerOnlyLeader.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
|
|
22918
|
+
// Control: an untracked thread also stays blocked under the same state.
|
|
22919
|
+
const ownerOnlyUntracked = await dispatchThread(untrackedThreadId);
|
|
22920
|
+
assert.equal(ownerOnlyUntracked.outputJson?.decision, "block");
|
|
22921
|
+
|
|
22922
|
+
// Control: once a genuine native_session_id leader thread anchor exists for this
|
|
22923
|
+
// session, the leader is blocked via that anchor while a real child is trusted.
|
|
22924
|
+
await writeJson(sessionPath, { session_id: sessionId, native_session_id: leaderThreadId });
|
|
22925
|
+
const anchoredLeader = await dispatchThread(leaderThreadId);
|
|
22926
|
+
assert.equal(anchoredLeader.outputJson?.decision, "block");
|
|
22927
|
+
assert.match(String(anchoredLeader.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
|
|
22928
|
+
const anchoredChild = await dispatchThread(childThreadId);
|
|
22929
|
+
assert.equal(anchoredChild.outputJson, null);
|
|
22930
|
+
} finally {
|
|
22931
|
+
await rm(cwd, { recursive: true, force: true });
|
|
22932
|
+
}
|
|
22933
|
+
});
|
|
22934
|
+
|
|
22935
|
+
it("blocks conductor writes when thread_spawn provenance is attached to the leader thread", async () => {
|
|
22936
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-conductor-thread-spawn-leader-"));
|
|
22937
|
+
const originalOmxRoot = process.env.OMX_ROOT;
|
|
22938
|
+
const originalOmxStateRoot = process.env.OMX_STATE_ROOT;
|
|
22939
|
+
const originalOmxTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
|
|
22940
|
+
try {
|
|
22941
|
+
process.env.OMX_ROOT = cwd;
|
|
22942
|
+
delete process.env.OMX_STATE_ROOT;
|
|
22943
|
+
delete process.env.OMX_TEAM_STATE_ROOT;
|
|
22944
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
22945
|
+
const sessionId = "sess-conductor-thread-spawn-leader";
|
|
22946
|
+
const leaderThreadId = "thread-conductor-thread-spawn-leader";
|
|
22947
|
+
const nowIso = new Date().toISOString();
|
|
22948
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
22949
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId });
|
|
22950
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "ultragoal", "executing");
|
|
22951
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ultragoal-state.json"), {
|
|
22952
|
+
active: true,
|
|
22953
|
+
mode: "ultragoal",
|
|
22954
|
+
current_phase: "executing",
|
|
22955
|
+
session_id: sessionId,
|
|
22956
|
+
});
|
|
22957
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
22958
|
+
schemaVersion: 1,
|
|
22959
|
+
sessions: {
|
|
22960
|
+
[sessionId]: {
|
|
22961
|
+
session_id: sessionId,
|
|
22962
|
+
leader_thread_id: leaderThreadId,
|
|
22963
|
+
updated_at: nowIso,
|
|
22964
|
+
threads: {
|
|
22965
|
+
[leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
|
|
22966
|
+
},
|
|
22967
|
+
},
|
|
22968
|
+
},
|
|
22969
|
+
});
|
|
22970
|
+
|
|
22971
|
+
const result = await dispatchCodexNativeHook(
|
|
22972
|
+
{
|
|
22973
|
+
hook_event_name: "PreToolUse",
|
|
22974
|
+
cwd,
|
|
22975
|
+
session_id: sessionId,
|
|
22976
|
+
thread_id: leaderThreadId,
|
|
22977
|
+
agent_role: "executor",
|
|
22978
|
+
source: {
|
|
22979
|
+
subagent: {
|
|
22980
|
+
thread_spawn: {
|
|
22981
|
+
parent_thread_id: leaderThreadId,
|
|
22982
|
+
},
|
|
22983
|
+
},
|
|
22984
|
+
},
|
|
22985
|
+
tool_name: "apply_patch",
|
|
22986
|
+
tool_input: { file_path: "configs/ws/kalshi_ws_auth_evidence.json" },
|
|
22987
|
+
},
|
|
22988
|
+
{ cwd },
|
|
22989
|
+
);
|
|
22990
|
+
|
|
22991
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
22992
|
+
assert.match(String(result.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ultragoal phase: executing\)/);
|
|
22993
|
+
} finally {
|
|
22994
|
+
if (originalOmxRoot === undefined) delete process.env.OMX_ROOT;
|
|
22995
|
+
else process.env.OMX_ROOT = originalOmxRoot;
|
|
22996
|
+
if (originalOmxStateRoot === undefined) delete process.env.OMX_STATE_ROOT;
|
|
22997
|
+
else process.env.OMX_STATE_ROOT = originalOmxStateRoot;
|
|
22998
|
+
if (originalOmxTeamStateRoot === undefined) delete process.env.OMX_TEAM_STATE_ROOT;
|
|
22999
|
+
else process.env.OMX_TEAM_STATE_ROOT = originalOmxTeamStateRoot;
|
|
23000
|
+
await rm(cwd, { recursive: true, force: true });
|
|
23001
|
+
}
|
|
23002
|
+
});
|
|
23003
|
+
|
|
23004
|
+
it("denies Main-root conductor apply_patch from the session leader thread during active Ralph (#3116)", async () => {
|
|
23005
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3116-root-denial-"));
|
|
23006
|
+
process.env.OMX_ROOT = cwd;
|
|
23007
|
+
try {
|
|
23008
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
23009
|
+
const sessionId = "sess-3116-root-denial";
|
|
23010
|
+
const leaderThreadId = "thread-3116-root-denial-leader";
|
|
23011
|
+
const nowIso = new Date().toISOString();
|
|
23012
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
23013
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId });
|
|
23014
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
|
|
23015
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
|
|
23016
|
+
active: true,
|
|
23017
|
+
mode: "ralph",
|
|
23018
|
+
current_phase: "executing",
|
|
23019
|
+
session_id: sessionId,
|
|
23020
|
+
});
|
|
23021
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
23022
|
+
schemaVersion: 1,
|
|
23023
|
+
sessions: {
|
|
23024
|
+
[sessionId]: {
|
|
23025
|
+
session_id: sessionId,
|
|
23026
|
+
leader_thread_id: leaderThreadId,
|
|
23027
|
+
updated_at: nowIso,
|
|
23028
|
+
threads: {
|
|
23029
|
+
[leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
|
|
23030
|
+
},
|
|
23031
|
+
},
|
|
23032
|
+
},
|
|
23033
|
+
});
|
|
23034
|
+
|
|
23035
|
+
const result = await dispatchCodexNativeHook(
|
|
23036
|
+
{
|
|
23037
|
+
hook_event_name: "PreToolUse",
|
|
23038
|
+
cwd,
|
|
23039
|
+
session_id: sessionId,
|
|
23040
|
+
thread_id: leaderThreadId,
|
|
23041
|
+
tool_name: "apply_patch",
|
|
23042
|
+
tool_input: { file_path: "src/feature.ts" },
|
|
23043
|
+
},
|
|
23044
|
+
{ cwd },
|
|
23045
|
+
);
|
|
23046
|
+
|
|
23047
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
23048
|
+
assert.match(String(result.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
|
|
23049
|
+
} finally {
|
|
23050
|
+
await rm(cwd, { recursive: true, force: true });
|
|
23051
|
+
}
|
|
23052
|
+
});
|
|
23053
|
+
|
|
23054
|
+
it("allows apply_patch from an untyped collaboration.spawn_agent child tracked as a subagent during active Ralph (#3116)", async () => {
|
|
23055
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3116-trusted-child-"));
|
|
23056
|
+
process.env.OMX_ROOT = cwd;
|
|
23057
|
+
try {
|
|
23058
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
23059
|
+
const sessionId = "sess-3116-trusted-child";
|
|
23060
|
+
const leaderThreadId = "thread-3116-trusted-child-leader";
|
|
23061
|
+
const childThreadId = "thread-3116-trusted-child-worker";
|
|
23062
|
+
const nowIso = new Date().toISOString();
|
|
23063
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
23064
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId });
|
|
23065
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
|
|
23066
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
|
|
23067
|
+
active: true,
|
|
23068
|
+
mode: "ralph",
|
|
23069
|
+
current_phase: "executing",
|
|
23070
|
+
session_id: sessionId,
|
|
23071
|
+
});
|
|
23072
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
23073
|
+
schemaVersion: 1,
|
|
23074
|
+
sessions: {
|
|
23075
|
+
[sessionId]: {
|
|
23076
|
+
session_id: sessionId,
|
|
23077
|
+
leader_thread_id: leaderThreadId,
|
|
23078
|
+
updated_at: nowIso,
|
|
23079
|
+
threads: {
|
|
23080
|
+
[leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
|
|
23081
|
+
[childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1, leader_thread_id: leaderThreadId },
|
|
23082
|
+
},
|
|
23083
|
+
},
|
|
23084
|
+
},
|
|
23085
|
+
});
|
|
23086
|
+
|
|
23087
|
+
// Untyped role (not in the typed-agent catalog) must not defeat tracker-backed trust.
|
|
23088
|
+
const result = await dispatchCodexNativeHook(
|
|
23089
|
+
{
|
|
23090
|
+
hook_event_name: "PreToolUse",
|
|
23091
|
+
cwd,
|
|
23092
|
+
session_id: sessionId,
|
|
23093
|
+
thread_id: childThreadId,
|
|
23094
|
+
agent_role: "collaboration-child",
|
|
23095
|
+
source: {
|
|
23096
|
+
subagent: {
|
|
23097
|
+
thread_spawn: {
|
|
23098
|
+
parent_thread_id: leaderThreadId,
|
|
23099
|
+
depth: 1,
|
|
23100
|
+
agent_nickname: "Helper",
|
|
23101
|
+
},
|
|
23102
|
+
},
|
|
23103
|
+
},
|
|
23104
|
+
tool_name: "apply_patch",
|
|
23105
|
+
tool_input: { file_path: "src/feature.ts" },
|
|
23106
|
+
},
|
|
23107
|
+
{ cwd },
|
|
23108
|
+
);
|
|
23109
|
+
|
|
23110
|
+
assert.equal(result.outputJson, null);
|
|
23111
|
+
} finally {
|
|
23112
|
+
await rm(cwd, { recursive: true, force: true });
|
|
23113
|
+
}
|
|
23114
|
+
});
|
|
23115
|
+
|
|
23116
|
+
it("allows apply_patch from untyped collaboration descendants via tracked thread and tracked parent chain (#3116)", async () => {
|
|
23117
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3116-descendant-"));
|
|
23118
|
+
process.env.OMX_ROOT = cwd;
|
|
23119
|
+
try {
|
|
23120
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
23121
|
+
const sessionId = "sess-3116-descendant";
|
|
23122
|
+
const leaderThreadId = "thread-3116-descendant-leader";
|
|
23123
|
+
const childThreadId = "thread-3116-descendant-child";
|
|
23124
|
+
const grandchildThreadId = "thread-3116-descendant-grandchild";
|
|
23125
|
+
const greatGrandchildThreadId = "thread-3116-descendant-great-grandchild";
|
|
23126
|
+
const nowIso = new Date().toISOString();
|
|
23127
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
23128
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId });
|
|
23129
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
|
|
23130
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
|
|
23131
|
+
active: true,
|
|
23132
|
+
mode: "ralph",
|
|
23133
|
+
current_phase: "executing",
|
|
23134
|
+
session_id: sessionId,
|
|
23135
|
+
});
|
|
23136
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
23137
|
+
schemaVersion: 1,
|
|
23138
|
+
sessions: {
|
|
23139
|
+
[sessionId]: {
|
|
23140
|
+
session_id: sessionId,
|
|
23141
|
+
leader_thread_id: leaderThreadId,
|
|
23142
|
+
updated_at: nowIso,
|
|
23143
|
+
threads: {
|
|
23144
|
+
[leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
|
|
23145
|
+
[childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1, leader_thread_id: leaderThreadId },
|
|
23146
|
+
[grandchildThreadId]: { thread_id: grandchildThreadId, kind: "subagent", mode: "collaboration-child", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1, leader_thread_id: childThreadId },
|
|
23147
|
+
},
|
|
23148
|
+
},
|
|
23149
|
+
},
|
|
23150
|
+
});
|
|
23151
|
+
|
|
23152
|
+
// (a) A descendant whose own thread is tracked as a subagent is trusted directly.
|
|
23153
|
+
const trackedDescendant = await dispatchCodexNativeHook(
|
|
23154
|
+
{
|
|
23155
|
+
hook_event_name: "PreToolUse",
|
|
23156
|
+
cwd,
|
|
23157
|
+
session_id: sessionId,
|
|
23158
|
+
thread_id: grandchildThreadId,
|
|
23159
|
+
tool_name: "apply_patch",
|
|
23160
|
+
tool_input: { file_path: "src/feature.ts" },
|
|
23161
|
+
},
|
|
23162
|
+
{ cwd },
|
|
23163
|
+
);
|
|
23164
|
+
assert.equal(trackedDescendant.outputJson, null);
|
|
23165
|
+
|
|
23166
|
+
// (b) A not-yet-tracked descendant is trusted when its runtime spawn parent is a tracked thread.
|
|
23167
|
+
const chainedDescendant = await dispatchCodexNativeHook(
|
|
23168
|
+
{
|
|
23169
|
+
hook_event_name: "PreToolUse",
|
|
23170
|
+
cwd,
|
|
23171
|
+
session_id: sessionId,
|
|
23172
|
+
thread_id: greatGrandchildThreadId,
|
|
23173
|
+
agent_role: "collaboration-child",
|
|
23174
|
+
source: {
|
|
23175
|
+
subagent: {
|
|
23176
|
+
thread_spawn: {
|
|
23177
|
+
parent_thread_id: grandchildThreadId,
|
|
23178
|
+
depth: 3,
|
|
23179
|
+
},
|
|
23180
|
+
},
|
|
23181
|
+
},
|
|
23182
|
+
tool_name: "apply_patch",
|
|
23183
|
+
tool_input: { file_path: "src/feature.ts" },
|
|
23184
|
+
},
|
|
23185
|
+
{ cwd },
|
|
23186
|
+
);
|
|
23187
|
+
assert.equal(chainedDescendant.outputJson, null);
|
|
23188
|
+
} finally {
|
|
23189
|
+
await rm(cwd, { recursive: true, force: true });
|
|
23190
|
+
}
|
|
23191
|
+
});
|
|
23192
|
+
|
|
23193
|
+
it("resolves the session-pinned Ralph state so collaboration children edit while the leader stays blocked (#3116)", async () => {
|
|
23194
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3116-pinned-state-"));
|
|
23195
|
+
process.env.OMX_ROOT = cwd;
|
|
23196
|
+
try {
|
|
23197
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
23198
|
+
const sessionId = "sess-3116-pinned-state";
|
|
23199
|
+
const leaderThreadId = "thread-3116-pinned-state-leader";
|
|
23200
|
+
const childThreadId = "thread-3116-pinned-state-child";
|
|
23201
|
+
const nowIso = new Date().toISOString();
|
|
23202
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
23203
|
+
// Native session id differs from the canonical session id: the guard must resolve it.
|
|
23204
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId });
|
|
23205
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
|
|
23206
|
+
const ralphStatePath = join(stateDir, "sessions", sessionId, "ralph-state.json");
|
|
23207
|
+
await writeJson(ralphStatePath, {
|
|
23208
|
+
active: true,
|
|
23209
|
+
mode: "ralph",
|
|
23210
|
+
current_phase: "executing",
|
|
23211
|
+
session_id: sessionId,
|
|
23212
|
+
});
|
|
23213
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
23214
|
+
schemaVersion: 1,
|
|
23215
|
+
sessions: {
|
|
23216
|
+
[sessionId]: {
|
|
23217
|
+
session_id: sessionId,
|
|
23218
|
+
leader_thread_id: leaderThreadId,
|
|
23219
|
+
updated_at: nowIso,
|
|
23220
|
+
threads: {
|
|
23221
|
+
[leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
|
|
23222
|
+
[childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1, leader_thread_id: leaderThreadId },
|
|
23223
|
+
},
|
|
23224
|
+
},
|
|
23225
|
+
},
|
|
23226
|
+
});
|
|
23227
|
+
|
|
23228
|
+
// Payload carries the native session id, which must map to the canonical session.
|
|
23229
|
+
const childEdit = await dispatchCodexNativeHook(
|
|
23230
|
+
{
|
|
23231
|
+
hook_event_name: "PreToolUse",
|
|
23232
|
+
cwd,
|
|
23233
|
+
session_id: leaderThreadId,
|
|
23234
|
+
thread_id: childThreadId,
|
|
23235
|
+
tool_name: "apply_patch",
|
|
23236
|
+
tool_input: { file_path: "src/feature.ts" },
|
|
23237
|
+
},
|
|
23238
|
+
{ cwd },
|
|
23239
|
+
);
|
|
23240
|
+
assert.equal(childEdit.outputJson, null);
|
|
23241
|
+
|
|
23242
|
+
const leaderEdit = await dispatchCodexNativeHook(
|
|
23243
|
+
{
|
|
23244
|
+
hook_event_name: "PreToolUse",
|
|
23245
|
+
cwd,
|
|
23246
|
+
session_id: leaderThreadId,
|
|
23247
|
+
thread_id: leaderThreadId,
|
|
23248
|
+
tool_name: "apply_patch",
|
|
23249
|
+
tool_input: { file_path: "src/feature.ts" },
|
|
23250
|
+
},
|
|
23251
|
+
{ cwd },
|
|
23252
|
+
);
|
|
23253
|
+
assert.equal(leaderEdit.outputJson?.decision, "block");
|
|
23254
|
+
assert.match(String(leaderEdit.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
|
|
23255
|
+
|
|
23256
|
+
// The guard reads the session-pinned phase: flipping it to "starting" releases the leader too.
|
|
23257
|
+
await writeJson(ralphStatePath, {
|
|
23258
|
+
active: true,
|
|
23259
|
+
mode: "ralph",
|
|
23260
|
+
current_phase: "starting",
|
|
23261
|
+
session_id: sessionId,
|
|
23262
|
+
});
|
|
23263
|
+
const leaderEditAfterStarting = await dispatchCodexNativeHook(
|
|
23264
|
+
{
|
|
23265
|
+
hook_event_name: "PreToolUse",
|
|
23266
|
+
cwd,
|
|
23267
|
+
session_id: leaderThreadId,
|
|
23268
|
+
thread_id: leaderThreadId,
|
|
23269
|
+
tool_name: "apply_patch",
|
|
23270
|
+
tool_input: { file_path: "src/feature.ts" },
|
|
23271
|
+
},
|
|
23272
|
+
{ cwd },
|
|
23273
|
+
);
|
|
23274
|
+
assert.equal(leaderEditAfterStarting.outputJson, null);
|
|
23275
|
+
} finally {
|
|
23276
|
+
await rm(cwd, { recursive: true, force: true });
|
|
23277
|
+
}
|
|
23278
|
+
});
|
|
23279
|
+
|
|
23280
|
+
it("trusts the native collaboration.spawn_agent child surface via runtime spawn provenance during active Ralph (#3116)", async () => {
|
|
23281
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3116-collab-surface-"));
|
|
23282
|
+
process.env.OMX_ROOT = cwd;
|
|
23283
|
+
try {
|
|
23284
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
23285
|
+
const sessionId = "sess-3116-collab-surface";
|
|
23286
|
+
const leaderThreadId = "thread-3116-collab-surface-leader";
|
|
23287
|
+
const childThreadId = "thread-3116-collab-surface-child";
|
|
23288
|
+
const nowIso = new Date().toISOString();
|
|
23289
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
23290
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId });
|
|
23291
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
|
|
23292
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
|
|
23293
|
+
active: true,
|
|
23294
|
+
mode: "ralph",
|
|
23295
|
+
current_phase: "executing",
|
|
23296
|
+
session_id: sessionId,
|
|
23297
|
+
});
|
|
23298
|
+
// Child SessionStart has not been recorded yet: only the leader is tracked.
|
|
23299
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
23300
|
+
schemaVersion: 1,
|
|
23301
|
+
sessions: {
|
|
23302
|
+
[sessionId]: {
|
|
23303
|
+
session_id: sessionId,
|
|
23304
|
+
leader_thread_id: leaderThreadId,
|
|
23305
|
+
updated_at: nowIso,
|
|
23306
|
+
threads: {
|
|
23307
|
+
[leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
|
|
23308
|
+
},
|
|
23309
|
+
},
|
|
23310
|
+
},
|
|
23311
|
+
});
|
|
23312
|
+
|
|
23313
|
+
// No typed agent_role at all: a raw collaboration.spawn_agent child payload shape.
|
|
23314
|
+
const result = await dispatchCodexNativeHook(
|
|
23315
|
+
{
|
|
23316
|
+
hook_event_name: "PreToolUse",
|
|
23317
|
+
cwd,
|
|
23318
|
+
session_id: sessionId,
|
|
23319
|
+
thread_id: childThreadId,
|
|
23320
|
+
source: {
|
|
23321
|
+
subagent: {
|
|
23322
|
+
thread_spawn: {
|
|
23323
|
+
parent_thread_id: leaderThreadId,
|
|
23324
|
+
depth: 1,
|
|
23325
|
+
agent_nickname: "Implementer",
|
|
23326
|
+
},
|
|
23327
|
+
},
|
|
23328
|
+
},
|
|
23329
|
+
tool_name: "apply_patch",
|
|
23330
|
+
tool_input: { file_path: "src/feature.ts" },
|
|
23331
|
+
},
|
|
23332
|
+
{ cwd },
|
|
23333
|
+
);
|
|
23334
|
+
|
|
23335
|
+
assert.equal(result.outputJson, null);
|
|
23336
|
+
} finally {
|
|
23337
|
+
await rm(cwd, { recursive: true, force: true });
|
|
23338
|
+
}
|
|
23339
|
+
});
|
|
23340
|
+
|
|
23341
|
+
it("denies spoofed or untrusted collaboration provenance while the main root stays protected during active Ralph (#3116)", async () => {
|
|
23342
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3116-spoof-denial-"));
|
|
23343
|
+
process.env.OMX_ROOT = cwd;
|
|
23344
|
+
try {
|
|
23345
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
23346
|
+
const sessionId = "sess-3116-spoof-denial";
|
|
23347
|
+
const leaderThreadId = "thread-3116-spoof-denial-leader";
|
|
23348
|
+
const nowIso = new Date().toISOString();
|
|
23349
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
23350
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId });
|
|
23351
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
|
|
23352
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
|
|
23353
|
+
active: true,
|
|
23354
|
+
mode: "ralph",
|
|
23355
|
+
current_phase: "executing",
|
|
23356
|
+
session_id: sessionId,
|
|
23357
|
+
});
|
|
23358
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
23359
|
+
schemaVersion: 1,
|
|
23360
|
+
sessions: {
|
|
23361
|
+
[sessionId]: {
|
|
23362
|
+
session_id: sessionId,
|
|
23363
|
+
leader_thread_id: leaderThreadId,
|
|
23364
|
+
updated_at: nowIso,
|
|
23365
|
+
threads: {
|
|
23366
|
+
[leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
|
|
23367
|
+
},
|
|
23368
|
+
},
|
|
23369
|
+
},
|
|
23370
|
+
});
|
|
23371
|
+
|
|
23372
|
+
// (a) Untracked thread whose declared parent is neither the leader nor a tracked thread.
|
|
23373
|
+
const orphanParent = await dispatchCodexNativeHook(
|
|
23374
|
+
{
|
|
23375
|
+
hook_event_name: "PreToolUse",
|
|
23376
|
+
cwd,
|
|
23377
|
+
session_id: sessionId,
|
|
23378
|
+
thread_id: "thread-3116-spoof-orphan",
|
|
23379
|
+
agent_role: "collaboration-child",
|
|
23380
|
+
source: {
|
|
23381
|
+
subagent: {
|
|
23382
|
+
thread_spawn: { parent_thread_id: "thread-3116-spoof-outsider" },
|
|
23383
|
+
},
|
|
23384
|
+
},
|
|
23385
|
+
tool_name: "apply_patch",
|
|
23386
|
+
tool_input: { file_path: "src/feature.ts" },
|
|
23387
|
+
},
|
|
23388
|
+
{ cwd },
|
|
23389
|
+
);
|
|
23390
|
+
assert.equal(orphanParent.outputJson?.decision, "block");
|
|
23391
|
+
assert.match(String(orphanParent.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
|
|
23392
|
+
|
|
23393
|
+
// (b) Untyped child with no provenance at all (no tracker thread, no spawn source).
|
|
23394
|
+
const noProvenance = await dispatchCodexNativeHook(
|
|
23395
|
+
{
|
|
23396
|
+
hook_event_name: "PreToolUse",
|
|
23397
|
+
cwd,
|
|
23398
|
+
session_id: sessionId,
|
|
23399
|
+
thread_id: "thread-3116-spoof-bare",
|
|
23400
|
+
agent_role: "collaboration-child",
|
|
23401
|
+
tool_name: "apply_patch",
|
|
23402
|
+
tool_input: { file_path: "src/feature.ts" },
|
|
23403
|
+
},
|
|
23404
|
+
{ cwd },
|
|
23405
|
+
);
|
|
23406
|
+
assert.equal(noProvenance.outputJson?.decision, "block");
|
|
23407
|
+
|
|
23408
|
+
// (c) Spawn provenance attached to the leader thread cannot promote the main root.
|
|
23409
|
+
const leaderSelfSpawn = await dispatchCodexNativeHook(
|
|
23410
|
+
{
|
|
23411
|
+
hook_event_name: "PreToolUse",
|
|
23412
|
+
cwd,
|
|
23413
|
+
session_id: sessionId,
|
|
23414
|
+
thread_id: leaderThreadId,
|
|
23415
|
+
agent_role: "collaboration-child",
|
|
23416
|
+
source: {
|
|
23417
|
+
subagent: {
|
|
23418
|
+
thread_spawn: { parent_thread_id: leaderThreadId },
|
|
23419
|
+
},
|
|
23420
|
+
},
|
|
23421
|
+
tool_name: "apply_patch",
|
|
23422
|
+
tool_input: { file_path: "src/feature.ts" },
|
|
23423
|
+
},
|
|
23424
|
+
{ cwd },
|
|
23425
|
+
);
|
|
23426
|
+
assert.equal(leaderSelfSpawn.outputJson?.decision, "block");
|
|
23427
|
+
} finally {
|
|
23428
|
+
await rm(cwd, { recursive: true, force: true });
|
|
23429
|
+
}
|
|
23430
|
+
});
|
|
23431
|
+
|
|
23432
|
+
it("blocks Main-root ralph conductor source and planning artifact writes while allowing .omx workflow state writes", async () => {
|
|
23433
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralph-conductor-write-"));
|
|
23434
|
+
try {
|
|
23435
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
23436
|
+
const sessionId = "sess-ralph-conductor-write";
|
|
23437
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
23438
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
23439
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
|
|
23440
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
|
|
23441
|
+
active: true,
|
|
23442
|
+
mode: "ralph",
|
|
23443
|
+
current_phase: "executing",
|
|
23444
|
+
session_id: sessionId,
|
|
23445
|
+
});
|
|
23446
|
+
|
|
23447
|
+
const blocked = await dispatchCodexNativeHook(
|
|
23448
|
+
{
|
|
23449
|
+
hook_event_name: "PreToolUse",
|
|
23450
|
+
cwd,
|
|
23451
|
+
session_id: sessionId,
|
|
23452
|
+
thread_id: "thread-ralph-conductor-write",
|
|
23453
|
+
tool_name: "Edit",
|
|
23454
|
+
tool_input: { file_path: "src/runtime.ts" },
|
|
23455
|
+
},
|
|
23456
|
+
{ cwd },
|
|
23457
|
+
);
|
|
23458
|
+
assert.equal(blocked.outputJson?.decision, "block");
|
|
23459
|
+
assert.match(String(blocked.outputJson?.reason ?? ""), /ralph phase: executing/);
|
|
23460
|
+
|
|
23461
|
+
for (const [toolName, filePath] of [
|
|
23462
|
+
["Write", ".omx/plans/conductor-owned-plan.md"],
|
|
23463
|
+
["Edit", ".omx/specs/conductor-owned-spec.md"],
|
|
23464
|
+
] as const) {
|
|
23465
|
+
const planningArtifactWrite = await dispatchCodexNativeHook(
|
|
23466
|
+
{
|
|
23467
|
+
hook_event_name: "PreToolUse",
|
|
23468
|
+
cwd,
|
|
23469
|
+
session_id: sessionId,
|
|
23470
|
+
thread_id: "thread-ralph-conductor-write",
|
|
23471
|
+
tool_name: toolName,
|
|
23472
|
+
tool_input: { file_path: filePath },
|
|
23473
|
+
},
|
|
23474
|
+
{ cwd },
|
|
23475
|
+
);
|
|
23476
|
+
assert.equal(planningArtifactWrite.outputJson?.decision, "block", `${toolName} ${filePath}`);
|
|
23477
|
+
assert.match(String(planningArtifactWrite.outputJson?.reason ?? ""), /plan\/code writes are blocked/);
|
|
23478
|
+
}
|
|
23479
|
+
|
|
23480
|
+
const allowed = await dispatchCodexNativeHook(
|
|
23481
|
+
{
|
|
23482
|
+
hook_event_name: "PreToolUse",
|
|
23483
|
+
cwd,
|
|
23484
|
+
session_id: sessionId,
|
|
23485
|
+
thread_id: "thread-ralph-conductor-write",
|
|
23486
|
+
tool_name: "Write",
|
|
23487
|
+
tool_input: { file_path: ".omx/state/sessions/sess-ralph-conductor-write/ralph-state.json" },
|
|
23488
|
+
},
|
|
23489
|
+
{ cwd },
|
|
23490
|
+
);
|
|
23491
|
+
assert.equal(allowed.outputJson, null);
|
|
23492
|
+
|
|
23493
|
+
const protectedRawState = await dispatchCodexNativeHook(
|
|
23494
|
+
{
|
|
23495
|
+
hook_event_name: "PreToolUse",
|
|
23496
|
+
cwd,
|
|
23497
|
+
session_id: sessionId,
|
|
23498
|
+
thread_id: "thread-ralph-conductor-write",
|
|
23499
|
+
tool_name: "Write",
|
|
23500
|
+
tool_input: { file_path: ".omx/state/sessions/sess-ralph-conductor-write/autopilot-state.json" },
|
|
23501
|
+
},
|
|
23502
|
+
{ cwd },
|
|
23503
|
+
);
|
|
23504
|
+
assert.equal(protectedRawState.outputJson?.decision, "block");
|
|
23505
|
+
|
|
23506
|
+
const safeTransport = await dispatchCodexNativeHook(
|
|
23507
|
+
{
|
|
23508
|
+
hook_event_name: "PreToolUse",
|
|
23509
|
+
cwd,
|
|
23510
|
+
session_id: sessionId,
|
|
21875
23511
|
thread_id: "thread-ralph-conductor-write",
|
|
21876
23512
|
tool_name: "Bash",
|
|
21877
23513
|
tool_input: { command: "omx state write --input '{\"mode\":\"ralph\",\"current_phase\":\"executing\",\"active\":true}' --json" },
|
|
@@ -21884,6 +23520,75 @@ PY`,
|
|
|
21884
23520
|
}
|
|
21885
23521
|
});
|
|
21886
23522
|
|
|
23523
|
+
it("allows structured ultragoal steering cleanup through dynamic nested shell loops", async () => {
|
|
23524
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-steer-cleanup-"));
|
|
23525
|
+
try {
|
|
23526
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
23527
|
+
const sessionId = "sess-ultragoal-steer-cleanup";
|
|
23528
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
23529
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
23530
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "ultragoal", "planning");
|
|
23531
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ultragoal-state.json"), {
|
|
23532
|
+
active: true,
|
|
23533
|
+
mode: "ultragoal",
|
|
23534
|
+
current_phase: "planning",
|
|
23535
|
+
session_id: sessionId,
|
|
23536
|
+
});
|
|
23537
|
+
|
|
23538
|
+
const result = await dispatchCodexNativeHook(
|
|
23539
|
+
{
|
|
23540
|
+
hook_event_name: "PreToolUse",
|
|
23541
|
+
cwd,
|
|
23542
|
+
session_id: sessionId,
|
|
23543
|
+
thread_id: "thread-ultragoal-steer-cleanup",
|
|
23544
|
+
tool_name: "Bash",
|
|
23545
|
+
tool_input: {
|
|
23546
|
+
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'`,
|
|
23547
|
+
},
|
|
23548
|
+
},
|
|
23549
|
+
{ cwd },
|
|
23550
|
+
);
|
|
23551
|
+
|
|
23552
|
+
assert.equal(result.outputJson, null);
|
|
23553
|
+
} finally {
|
|
23554
|
+
await rm(cwd, { recursive: true, force: true });
|
|
23555
|
+
}
|
|
23556
|
+
});
|
|
23557
|
+
|
|
23558
|
+
it("blocks unsafe dynamic nested shell writes in conductor mode", async () => {
|
|
23559
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-dynamic-shell-write-block-"));
|
|
23560
|
+
try {
|
|
23561
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
23562
|
+
const sessionId = "sess-dynamic-shell-write-block";
|
|
23563
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
23564
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
23565
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "ultragoal", "planning");
|
|
23566
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ultragoal-state.json"), {
|
|
23567
|
+
active: true,
|
|
23568
|
+
mode: "ultragoal",
|
|
23569
|
+
current_phase: "planning",
|
|
23570
|
+
session_id: sessionId,
|
|
23571
|
+
});
|
|
23572
|
+
|
|
23573
|
+
const result = await dispatchCodexNativeHook(
|
|
23574
|
+
{
|
|
23575
|
+
hook_event_name: "PreToolUse",
|
|
23576
|
+
cwd,
|
|
23577
|
+
session_id: sessionId,
|
|
23578
|
+
thread_id: "thread-dynamic-shell-write-block",
|
|
23579
|
+
tool_name: "Bash",
|
|
23580
|
+
tool_input: { command: `bash -lc 'cp "$SOURCE_FILE" src/runtime.ts'` },
|
|
23581
|
+
},
|
|
23582
|
+
{ cwd },
|
|
23583
|
+
);
|
|
23584
|
+
|
|
23585
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
23586
|
+
assert.match(String(result.outputJson?.reason ?? ""), /Bash nested shell execution is dynamic and cannot be validated/);
|
|
23587
|
+
} finally {
|
|
23588
|
+
await rm(cwd, { recursive: true, force: true });
|
|
23589
|
+
}
|
|
23590
|
+
});
|
|
23591
|
+
|
|
21887
23592
|
it("blocks common Bash file mutations in Main-root conductor states unless they target workflow metadata", async () => {
|
|
21888
23593
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-conductor-bash-mutations-"));
|
|
21889
23594
|
try {
|