oh-my-codex 0.20.0 → 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/dist/cli/__tests__/codex-plugin-layout.test.js +27 -0
- 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 +9 -0
- package/dist/cli/__tests__/doctor-invalid-config.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__/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 +3 -3
- package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
- package/dist/cli/__tests__/setup-refresh.test.js +137 -5
- 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 +297 -3
- package/dist/cli/__tests__/uninstall.test.js.map +1 -1
- package/dist/cli/doctor.d.ts +2 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +42 -31
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/setup.js +2 -2
- 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 +200 -104
- package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
- package/dist/config/__tests__/generator-notify.test.js +22 -48
- package/dist/config/__tests__/generator-notify.test.js.map +1 -1
- package/dist/config/generator.d.ts +19 -10
- 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/leader/__tests__/contract.test.js +43 -1
- package/dist/leader/__tests__/contract.test.js.map +1 -1
- package/dist/leader/contract.d.ts.map +1 -1
- package/dist/leader/contract.js +23 -5
- package/dist/leader/contract.js.map +1 -1
- package/dist/scripts/__tests__/codex-native-hook.test.js +1104 -20
- 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 +139 -19
- package/dist/scripts/codex-native-hook.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/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/package.json +1 -1
- package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
- package/src/scripts/__tests__/codex-native-hook.test.ts +1281 -53
- package/src/scripts/codex-native-hook.ts +142 -16
|
@@ -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
|
}
|
|
@@ -10826,6 +10935,7 @@ exit 0
|
|
|
10826
10935
|
for (const path of [
|
|
10827
10936
|
".omx/context/findings.md",
|
|
10828
10937
|
".omx/plans/issue-2863.md",
|
|
10938
|
+
".omx/drafts/issue-3105.md",
|
|
10829
10939
|
".omx/specs/issue-2863.md",
|
|
10830
10940
|
".omx/state/required-planning-state.json",
|
|
10831
10941
|
]) {
|
|
@@ -10833,6 +10943,24 @@ exit 0
|
|
|
10833
10943
|
assert.equal(writeResult.outputJson, null, `Write should be allowed for ${path}`);
|
|
10834
10944
|
}
|
|
10835
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
|
+
|
|
10836
10964
|
for (const protectedPath of [
|
|
10837
10965
|
".omx/state/sessions/sess-ralplan-guard/ralplan-state.json",
|
|
10838
10966
|
".omx/state/sessions/sess-ralplan-guard/autopilot-state.json",
|
|
@@ -10874,11 +11002,26 @@ exit 0
|
|
|
10874
11002
|
});
|
|
10875
11003
|
assert.equal(allowedPatchUpdate.outputJson, null);
|
|
10876
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
|
+
|
|
10877
11015
|
const allowedRedirect = await preToolUse("Bash", "tool-ralplan-redirect-allow", {
|
|
10878
11016
|
command: "printf '\\nmore\\n' >> .omx/plans/issue-2863.md",
|
|
10879
11017
|
});
|
|
10880
11018
|
assert.equal(allowedRedirect.outputJson, null);
|
|
10881
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
|
+
|
|
10882
11025
|
const allowedTee = await preToolUse("Bash", "tool-ralplan-tee-allow", {
|
|
10883
11026
|
command: "printf '\\nmore\\n' | tee -a .omx/specs/issue-2863.md",
|
|
10884
11027
|
});
|
|
@@ -10940,6 +11083,41 @@ exit 0
|
|
|
10940
11083
|
const editReason = String((blockedEdit.outputJson as { reason?: string } | null)?.reason ?? "");
|
|
10941
11084
|
assert.match(editReason, /Edit path/);
|
|
10942
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
|
+
}
|
|
10943
11121
|
|
|
10944
11122
|
const blockedMixedPatch = await preToolUse("apply_patch", "tool-ralplan-patch-mixed", {
|
|
10945
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",
|
|
@@ -10949,6 +11127,18 @@ exit 0
|
|
|
10949
11127
|
assert.match(mixedReason, /apply_patch target/);
|
|
10950
11128
|
assert.match(mixedReason, /src\/leak\.ts/);
|
|
10951
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
|
+
|
|
10952
11142
|
const blockedUnparseablePatch = await preToolUse("apply_patch", "tool-ralplan-patch-unparseable", {
|
|
10953
11143
|
input: "not a recognizable patch",
|
|
10954
11144
|
});
|
|
@@ -10963,6 +11153,12 @@ exit 0
|
|
|
10963
11153
|
assert.match(unresolvedReason, /unresolved Bash write target/);
|
|
10964
11154
|
assert.match(unresolvedReason, /\$PLAN_PATH/);
|
|
10965
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
|
+
|
|
10966
11162
|
const blockedTraversal = await preToolUse("Write", "tool-ralplan-traversal-block", {
|
|
10967
11163
|
file_path: ".omx/plans/../../src/leak.ts",
|
|
10968
11164
|
content: "bad",
|
|
@@ -11618,6 +11814,175 @@ PY`,
|
|
|
11618
11814
|
}
|
|
11619
11815
|
});
|
|
11620
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
|
+
|
|
11621
11986
|
it("allows null-device fd redirects while deep-interview blocks real Bash writes", async () => {
|
|
11622
11987
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-deep-interview-null-redirect-"));
|
|
11623
11988
|
try {
|
|
@@ -18234,21 +18599,20 @@ PY`,
|
|
|
18234
18599
|
execFileSync("git", ["commit", "-m", "init"], { cwd, stdio: "ignore" });
|
|
18235
18600
|
await writeFile(join(cwd, "src", "scripts", "codex-native-hook.ts"), "export const hook = 2;\n", "utf-8");
|
|
18236
18601
|
|
|
18237
|
-
const
|
|
18238
|
-
|
|
18239
|
-
|
|
18240
|
-
|
|
18241
|
-
|
|
18242
|
-
|
|
18243
|
-
},
|
|
18244
|
-
{ cwd },
|
|
18245
|
-
);
|
|
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 }));
|
|
18246
18608
|
|
|
18247
|
-
assert.
|
|
18248
|
-
assert.equal(
|
|
18249
|
-
assert.equal(
|
|
18250
|
-
assert.
|
|
18251
|
-
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/);
|
|
18252
18616
|
} finally {
|
|
18253
18617
|
await rm(cwd, { recursive: true, force: true });
|
|
18254
18618
|
}
|
|
@@ -21247,9 +21611,24 @@ PY`,
|
|
|
21247
21611
|
|
|
21248
21612
|
assert.equal(result.omxEventName, "pre-tool-use");
|
|
21249
21613
|
assert.equal(result.outputJson, null);
|
|
21250
|
-
|
|
21251
|
-
|
|
21252
|
-
|
|
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);
|
|
21629
|
+
} finally {
|
|
21630
|
+
await rm(cwd, { recursive: true, force: true });
|
|
21631
|
+
}
|
|
21253
21632
|
});
|
|
21254
21633
|
|
|
21255
21634
|
it("blocks mapped implementation writes when explicit ultragoal conductor handoff is active", async () => {
|
|
@@ -21365,6 +21744,21 @@ PY`,
|
|
|
21365
21744
|
cwd: ownerCwd,
|
|
21366
21745
|
});
|
|
21367
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
|
+
|
|
21368
21762
|
const result = await dispatchCodexNativeHook(
|
|
21369
21763
|
{
|
|
21370
21764
|
hook_event_name: "PreToolUse",
|
|
@@ -21458,6 +21852,21 @@ PY`,
|
|
|
21458
21852
|
|
|
21459
21853
|
assert.equal(result.omxEventName, "pre-tool-use");
|
|
21460
21854
|
assert.equal(result.outputJson, null);
|
|
21855
|
+
|
|
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);
|
|
21461
21870
|
} finally {
|
|
21462
21871
|
await rm(cwd, { recursive: true, force: true });
|
|
21463
21872
|
}
|
|
@@ -21867,6 +22276,140 @@ PY`,
|
|
|
21867
22276
|
}
|
|
21868
22277
|
});
|
|
21869
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
|
+
|
|
21870
22413
|
it("blocks Main-root ralplan writes even when payload has only a typed agent_role", async () => {
|
|
21871
22414
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-agent-role-main-"));
|
|
21872
22415
|
try {
|
|
@@ -22132,25 +22675,22 @@ PY`,
|
|
|
22132
22675
|
}
|
|
22133
22676
|
});
|
|
22134
22677
|
|
|
22135
|
-
it("blocks
|
|
22136
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-
|
|
22137
|
-
|
|
22138
|
-
const originalOmxStateRoot = process.env.OMX_STATE_ROOT;
|
|
22139
|
-
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;
|
|
22140
22681
|
try {
|
|
22141
|
-
process.env.OMX_ROOT = cwd;
|
|
22142
|
-
delete process.env.OMX_STATE_ROOT;
|
|
22143
|
-
delete process.env.OMX_TEAM_STATE_ROOT;
|
|
22144
22682
|
const stateDir = join(cwd, ".omx", "state");
|
|
22145
|
-
const sessionId = "sess-
|
|
22146
|
-
const leaderThreadId = "thread-
|
|
22147
|
-
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";
|
|
22148
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".
|
|
22149
22689
|
await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId });
|
|
22150
|
-
await writeSessionSkillActiveState(stateDir, sessionId, "
|
|
22151
|
-
await writeJson(join(stateDir, "sessions", sessionId, "
|
|
22690
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
|
|
22691
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
|
|
22152
22692
|
active: true,
|
|
22153
|
-
mode: "
|
|
22693
|
+
mode: "ralph",
|
|
22154
22694
|
current_phase: "executing",
|
|
22155
22695
|
session_id: sessionId,
|
|
22156
22696
|
});
|
|
@@ -22159,44 +22699,732 @@ PY`,
|
|
|
22159
22699
|
sessions: {
|
|
22160
22700
|
[sessionId]: {
|
|
22161
22701
|
session_id: sessionId,
|
|
22162
|
-
leader_thread_id: leaderThreadId,
|
|
22163
|
-
updated_at: nowIso,
|
|
22164
22702
|
threads: {
|
|
22165
|
-
[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" },
|
|
22166
22705
|
},
|
|
22167
22706
|
},
|
|
22168
22707
|
},
|
|
22169
22708
|
});
|
|
22170
22709
|
|
|
22171
|
-
|
|
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(
|
|
22172
22713
|
{
|
|
22173
22714
|
hook_event_name: "PreToolUse",
|
|
22174
22715
|
cwd,
|
|
22175
22716
|
session_id: sessionId,
|
|
22176
22717
|
thread_id: leaderThreadId,
|
|
22177
|
-
|
|
22178
|
-
|
|
22179
|
-
|
|
22180
|
-
|
|
22181
|
-
|
|
22182
|
-
|
|
22718
|
+
tool_name: "apply_patch",
|
|
22719
|
+
tool_input: { file_path: "src/feature.ts" },
|
|
22720
|
+
},
|
|
22721
|
+
{ cwd },
|
|
22722
|
+
);
|
|
22723
|
+
assert.equal(untypedLeader.outputJson?.decision, "block");
|
|
22724
|
+
assert.match(String(untypedLeader.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
|
|
22725
|
+
|
|
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);
|
|
22739
|
+
} finally {
|
|
22740
|
+
await rm(cwd, { recursive: true, force: true });
|
|
22741
|
+
}
|
|
22742
|
+
});
|
|
22743
|
+
|
|
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;
|
|
22747
|
+
try {
|
|
22748
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
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";
|
|
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".
|
|
22755
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
22756
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
|
|
22757
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
|
|
22758
|
+
active: true,
|
|
22759
|
+
mode: "ralph",
|
|
22760
|
+
current_phase: "executing",
|
|
22761
|
+
session_id: sessionId,
|
|
22762
|
+
});
|
|
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
|
+
},
|
|
22773
|
+
},
|
|
22774
|
+
});
|
|
22775
|
+
|
|
22776
|
+
for (const threadId of [leaderThreadId, childThreadId]) {
|
|
22777
|
+
const result = await dispatchCodexNativeHook(
|
|
22778
|
+
{
|
|
22779
|
+
hook_event_name: "PreToolUse",
|
|
22780
|
+
cwd,
|
|
22781
|
+
session_id: sessionId,
|
|
22782
|
+
thread_id: threadId,
|
|
22783
|
+
tool_name: "apply_patch",
|
|
22784
|
+
tool_input: { file_path: "src/feature.ts" },
|
|
22785
|
+
},
|
|
22786
|
+
{ cwd },
|
|
22787
|
+
);
|
|
22788
|
+
assert.equal(result.outputJson?.decision, "block", threadId);
|
|
22789
|
+
assert.match(String(result.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
|
|
22790
|
+
}
|
|
22791
|
+
} finally {
|
|
22792
|
+
await rm(cwd, { recursive: true, force: true });
|
|
22793
|
+
}
|
|
22794
|
+
});
|
|
22795
|
+
|
|
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" },
|
|
22183
22824
|
},
|
|
22184
22825
|
},
|
|
22826
|
+
},
|
|
22827
|
+
});
|
|
22828
|
+
|
|
22829
|
+
const dispatchThread = async (threadId: string) => dispatchCodexNativeHook(
|
|
22830
|
+
{
|
|
22831
|
+
hook_event_name: "PreToolUse",
|
|
22832
|
+
cwd,
|
|
22833
|
+
session_id: sessionId,
|
|
22834
|
+
thread_id: threadId,
|
|
22185
22835
|
tool_name: "apply_patch",
|
|
22186
|
-
tool_input: { file_path: "
|
|
22836
|
+
tool_input: { file_path: "src/feature.ts" },
|
|
22187
22837
|
},
|
|
22188
22838
|
{ cwd },
|
|
22189
22839
|
);
|
|
22190
22840
|
|
|
22191
|
-
|
|
22192
|
-
|
|
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");
|
|
22193
23427
|
} finally {
|
|
22194
|
-
if (originalOmxRoot === undefined) delete process.env.OMX_ROOT;
|
|
22195
|
-
else process.env.OMX_ROOT = originalOmxRoot;
|
|
22196
|
-
if (originalOmxStateRoot === undefined) delete process.env.OMX_STATE_ROOT;
|
|
22197
|
-
else process.env.OMX_STATE_ROOT = originalOmxStateRoot;
|
|
22198
|
-
if (originalOmxTeamStateRoot === undefined) delete process.env.OMX_TEAM_STATE_ROOT;
|
|
22199
|
-
else process.env.OMX_TEAM_STATE_ROOT = originalOmxTeamStateRoot;
|
|
22200
23428
|
await rm(cwd, { recursive: true, force: true });
|
|
22201
23429
|
}
|
|
22202
23430
|
});
|