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
|
@@ -581,6 +581,68 @@ describe("codex native hook dispatch", () => {
|
|
|
581
581
|
await rm(cwd, { recursive: true, force: true });
|
|
582
582
|
}
|
|
583
583
|
});
|
|
584
|
+
it("allows Ralplan Markdown draft-only apply_patch on the live CLI path while denying mixed targets", async () => {
|
|
585
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-ralplan-draft-boundary-"));
|
|
586
|
+
const sessionId = "sess-cli-ralplan-draft-boundary";
|
|
587
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
588
|
+
try {
|
|
589
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId, cwd });
|
|
590
|
+
await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
|
|
591
|
+
active: true,
|
|
592
|
+
skill: "ralplan",
|
|
593
|
+
phase: "planning",
|
|
594
|
+
session_id: sessionId,
|
|
595
|
+
active_skills: [{ skill: "ralplan", phase: "planning", active: true, session_id: sessionId }],
|
|
596
|
+
});
|
|
597
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
598
|
+
active: true,
|
|
599
|
+
mode: "ralplan",
|
|
600
|
+
current_phase: "planning",
|
|
601
|
+
session_id: sessionId,
|
|
602
|
+
});
|
|
603
|
+
for (const [name, target] of [
|
|
604
|
+
["relative", ".omx/drafts/issue-3105.md"],
|
|
605
|
+
["repository-absolute", join(cwd, ".omx", "drafts", "issue-3105.md")],
|
|
606
|
+
]) {
|
|
607
|
+
const result = runNativeHookCliResult({
|
|
608
|
+
hook_event_name: "PreToolUse",
|
|
609
|
+
cwd,
|
|
610
|
+
session_id: sessionId,
|
|
611
|
+
thread_id: `thread-cli-ralplan-draft-${name}`,
|
|
612
|
+
tool_name: "apply_patch",
|
|
613
|
+
tool_input: {
|
|
614
|
+
input: `*** Begin Patch\n*** Add File: ${target}\n+# Draft\n*** End Patch\n`,
|
|
615
|
+
},
|
|
616
|
+
}, { cwd });
|
|
617
|
+
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
618
|
+
assert.deepEqual(parseSingleJsonStdout(result.stdout), {});
|
|
619
|
+
}
|
|
620
|
+
const mixedResult = runNativeHookCliResult({
|
|
621
|
+
hook_event_name: "PreToolUse",
|
|
622
|
+
cwd,
|
|
623
|
+
session_id: sessionId,
|
|
624
|
+
thread_id: "thread-cli-ralplan-draft-mixed",
|
|
625
|
+
tool_name: "apply_patch",
|
|
626
|
+
tool_input: {
|
|
627
|
+
input: "*** Begin Patch\n*** Add File: .omx/drafts/issue-3105.md\n+# Draft\n*** Add File: src/leak.ts\n+leak\n*** End Patch\n",
|
|
628
|
+
},
|
|
629
|
+
}, { cwd });
|
|
630
|
+
assert.equal(mixedResult.status, 0, mixedResult.stderr || mixedResult.stdout);
|
|
631
|
+
const mixedOutput = parseSingleJsonStdout(mixedResult.stdout);
|
|
632
|
+
const hookSpecificOutput = mixedOutput.hookSpecificOutput;
|
|
633
|
+
assert.deepEqual(Object.keys(mixedOutput).sort(), ["hookSpecificOutput"]);
|
|
634
|
+
assert.equal(hookSpecificOutput.hookEventName, "PreToolUse");
|
|
635
|
+
assert.equal(hookSpecificOutput.permissionDecision, "deny");
|
|
636
|
+
assert.match(String(hookSpecificOutput.permissionDecisionReason ?? ""), /src\/leak\.ts/);
|
|
637
|
+
assert.match(String(hookSpecificOutput.permissionDecisionReason ?? ""), /implementation\/write tools are blocked/);
|
|
638
|
+
assert.equal(mixedOutput.decision, undefined);
|
|
639
|
+
assert.equal(mixedOutput.reason, undefined);
|
|
640
|
+
assert.equal(mixedOutput.systemMessage, undefined);
|
|
641
|
+
}
|
|
642
|
+
finally {
|
|
643
|
+
await rm(cwd, { recursive: true, force: true });
|
|
644
|
+
}
|
|
645
|
+
});
|
|
584
646
|
it("preserves team-worker typed subagent PreToolUse exemption without thread spawn provenance", async () => {
|
|
585
647
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-team-worker-typed-pretool-exempt-"));
|
|
586
648
|
const sessionId = "sess-team-worker-typed-pretool-exempt";
|
|
@@ -1211,10 +1273,51 @@ PY`,
|
|
|
1211
1273
|
thread_id: "thread-autopilot-diagnostic",
|
|
1212
1274
|
}, { cwd }));
|
|
1213
1275
|
assert.equal(output.decision, "block");
|
|
1276
|
+
assert.equal(output.stopReason, "autopilot_ultragoal");
|
|
1214
1277
|
assert.match(String(output.reason ?? ""), /state: \.omx\/state\/sessions\/sess-autopilot-diagnostic\/autopilot-state\.json/);
|
|
1215
1278
|
assert.match(String(output.reason ?? ""), /canonical: canonical_agrees/);
|
|
1216
|
-
assert.
|
|
1217
|
-
assert.
|
|
1279
|
+
assert.match(String(output.systemMessage ?? ""), /state: \.omx\/state\/sessions\/sess-autopilot-diagnostic\/autopilot-state\.json/);
|
|
1280
|
+
assert.match(String(output.systemMessage ?? ""), /canonical: canonical_agrees/);
|
|
1281
|
+
assert.deepEqual(Object.keys(output).sort(), ["decision", "reason", "stopReason", "systemMessage"]);
|
|
1282
|
+
assert.equal("statePath" in output, false);
|
|
1283
|
+
assert.equal("canonicalDisagreement" in output, false);
|
|
1284
|
+
}
|
|
1285
|
+
finally {
|
|
1286
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1287
|
+
}
|
|
1288
|
+
});
|
|
1289
|
+
it("keeps canonical phase disagreements in active Autopilot Stop diagnostics", async () => {
|
|
1290
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-canonical-phase-"));
|
|
1291
|
+
try {
|
|
1292
|
+
const sessionId = "sess-autopilot-canonical-phase";
|
|
1293
|
+
await writeJson(join(cwd, ".omx", "state", "session.json"), { session_id: sessionId, cwd });
|
|
1294
|
+
await writeJson(join(cwd, ".omx", "state", "skill-active-state.json"), {
|
|
1295
|
+
version: 1,
|
|
1296
|
+
active: true,
|
|
1297
|
+
skill: "autopilot",
|
|
1298
|
+
phase: "execution",
|
|
1299
|
+
session_id: sessionId,
|
|
1300
|
+
active_skills: [{ skill: "autopilot", phase: "execution", active: true, session_id: sessionId }],
|
|
1301
|
+
});
|
|
1302
|
+
await writeJson(join(cwd, ".omx", "state", "sessions", sessionId, "autopilot-state.json"), {
|
|
1303
|
+
active: true,
|
|
1304
|
+
mode: "autopilot",
|
|
1305
|
+
current_phase: "ultragoal",
|
|
1306
|
+
session_id: sessionId,
|
|
1307
|
+
});
|
|
1308
|
+
const output = parseSingleJsonStdout(runNativeHookCli({
|
|
1309
|
+
hook_event_name: "Stop",
|
|
1310
|
+
cwd,
|
|
1311
|
+
session_id: sessionId,
|
|
1312
|
+
thread_id: "thread-autopilot-canonical-phase",
|
|
1313
|
+
}, { cwd }));
|
|
1314
|
+
assert.equal(output.decision, "block");
|
|
1315
|
+
assert.equal(output.stopReason, "autopilot_ultragoal");
|
|
1316
|
+
assert.match(String(output.reason ?? ""), /canonical: canonical_phase:execution/);
|
|
1317
|
+
assert.match(String(output.systemMessage ?? ""), /canonical: canonical_phase:execution/);
|
|
1318
|
+
assert.deepEqual(Object.keys(output).sort(), ["decision", "reason", "stopReason", "systemMessage"]);
|
|
1319
|
+
assert.equal("statePath" in output, false);
|
|
1320
|
+
assert.equal("canonicalDisagreement" in output, false);
|
|
1218
1321
|
}
|
|
1219
1322
|
finally {
|
|
1220
1323
|
await rm(cwd, { recursive: true, force: true });
|
|
@@ -8784,12 +8887,29 @@ exit 0
|
|
|
8784
8887
|
for (const path of [
|
|
8785
8888
|
".omx/context/findings.md",
|
|
8786
8889
|
".omx/plans/issue-2863.md",
|
|
8890
|
+
".omx/drafts/issue-3105.md",
|
|
8787
8891
|
".omx/specs/issue-2863.md",
|
|
8788
8892
|
".omx/state/required-planning-state.json",
|
|
8789
8893
|
]) {
|
|
8790
8894
|
const writeResult = await preToolUse("Write", `tool-ralplan-write-${path}`, { file_path: path, content: "ok" });
|
|
8791
8895
|
assert.equal(writeResult.outputJson, null, `Write should be allowed for ${path}`);
|
|
8792
8896
|
}
|
|
8897
|
+
const allowedDraftEdit = await preToolUse("Edit", "tool-ralplan-draft-edit", {
|
|
8898
|
+
file_path: ".omx/drafts/issue-3105.md",
|
|
8899
|
+
old_string: "old",
|
|
8900
|
+
new_string: "new",
|
|
8901
|
+
});
|
|
8902
|
+
assert.equal(allowedDraftEdit.outputJson, null);
|
|
8903
|
+
for (const path of [
|
|
8904
|
+
join(cwd, ".omx", "drafts", "issue-3105.md"),
|
|
8905
|
+
".omx/drafts/subdir/../issue-3105.md",
|
|
8906
|
+
]) {
|
|
8907
|
+
const normalizedDraftWrite = await preToolUse("Write", `tool-ralplan-normalized-draft-${path}`, {
|
|
8908
|
+
file_path: path,
|
|
8909
|
+
content: "# Draft",
|
|
8910
|
+
});
|
|
8911
|
+
assert.equal(normalizedDraftWrite.outputJson, null, `normalized draft path should be allowed: ${path}`);
|
|
8912
|
+
}
|
|
8793
8913
|
for (const protectedPath of [
|
|
8794
8914
|
".omx/state/sessions/sess-ralplan-guard/ralplan-state.json",
|
|
8795
8915
|
".omx/state/sessions/sess-ralplan-guard/autopilot-state.json",
|
|
@@ -8822,10 +8942,22 @@ exit 0
|
|
|
8822
8942
|
input: "*** Begin Patch\n*** Update File: .omx/plans/issue-2863.md\n@@\n-old\n+new\n*** End Patch\n",
|
|
8823
8943
|
});
|
|
8824
8944
|
assert.equal(allowedPatchUpdate.outputJson, null);
|
|
8945
|
+
const allowedDraftPatchAdd = await preToolUse("apply_patch", "tool-ralplan-draft-patch-add", {
|
|
8946
|
+
input: "*** Begin Patch\n*** Add File: .omx/drafts/issue-3105.md\n+# Draft\n*** End Patch\n",
|
|
8947
|
+
});
|
|
8948
|
+
assert.equal(allowedDraftPatchAdd.outputJson, null);
|
|
8949
|
+
const allowedDraftPatchUpdate = await preToolUse("ApplyPatch", "tool-ralplan-draft-patch-update", {
|
|
8950
|
+
input: "*** Begin Patch\n*** Update File: .omx/drafts/issue-3105.md\n@@\n-old\n+new\n*** End Patch\n",
|
|
8951
|
+
});
|
|
8952
|
+
assert.equal(allowedDraftPatchUpdate.outputJson, null);
|
|
8825
8953
|
const allowedRedirect = await preToolUse("Bash", "tool-ralplan-redirect-allow", {
|
|
8826
8954
|
command: "printf '\\nmore\\n' >> .omx/plans/issue-2863.md",
|
|
8827
8955
|
});
|
|
8828
8956
|
assert.equal(allowedRedirect.outputJson, null);
|
|
8957
|
+
const allowedDraftRedirect = await preToolUse("Bash", "tool-ralplan-draft-redirect-allow", {
|
|
8958
|
+
command: "printf '# Draft\\n' > .omx/drafts/issue-3105.md",
|
|
8959
|
+
});
|
|
8960
|
+
assert.equal(allowedDraftRedirect.outputJson, null);
|
|
8829
8961
|
const allowedTee = await preToolUse("Bash", "tool-ralplan-tee-allow", {
|
|
8830
8962
|
command: "printf '\\nmore\\n' | tee -a .omx/specs/issue-2863.md",
|
|
8831
8963
|
});
|
|
@@ -8881,6 +9013,34 @@ exit 0
|
|
|
8881
9013
|
const editReason = String(blockedEdit.outputJson?.reason ?? "");
|
|
8882
9014
|
assert.match(editReason, /Edit path/);
|
|
8883
9015
|
assert.match(editReason, /src\/implementation\.ts/);
|
|
9016
|
+
const blockedEditContext = String(blockedEdit.outputJson
|
|
9017
|
+
?.hookSpecificOutput?.additionalContext ?? "");
|
|
9018
|
+
assert.match(blockedEditContext, /Markdown drafts under `\.omx\/drafts\/\*\.md`/);
|
|
9019
|
+
for (const allowedCategory of [
|
|
9020
|
+
".omx/context/",
|
|
9021
|
+
".omx/plans/",
|
|
9022
|
+
".omx/specs/",
|
|
9023
|
+
".omx/tmp/",
|
|
9024
|
+
".omx/state/",
|
|
9025
|
+
".beads/",
|
|
9026
|
+
]) {
|
|
9027
|
+
assert.match(blockedEditContext, new RegExp(allowedCategory.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
|
|
9028
|
+
}
|
|
9029
|
+
for (const blockedDraftPath of [
|
|
9030
|
+
".omx/drafts/issue-3105.MD",
|
|
9031
|
+
".omx/Drafts/issue-3105.md",
|
|
9032
|
+
".omx/drafts/issue-3105",
|
|
9033
|
+
".omx/drafts/run.sh",
|
|
9034
|
+
".omx/drafts/subdir/issue-3105.md",
|
|
9035
|
+
".omx/drafts/../../src/leak.ts",
|
|
9036
|
+
"/tmp/issue-3105.md",
|
|
9037
|
+
]) {
|
|
9038
|
+
const blockedDraft = await preToolUse("Write", `tool-ralplan-draft-block-${blockedDraftPath}`, {
|
|
9039
|
+
file_path: blockedDraftPath,
|
|
9040
|
+
content: "bad",
|
|
9041
|
+
});
|
|
9042
|
+
assert.equal(blockedDraft.outputJson?.decision, "block", `draft path should be blocked: ${blockedDraftPath}`);
|
|
9043
|
+
}
|
|
8884
9044
|
const blockedMixedPatch = await preToolUse("apply_patch", "tool-ralplan-patch-mixed", {
|
|
8885
9045
|
input: "*** Begin Patch\n*** Add File: .omx/plans/ok.md\n+ok\n*** Add File: src/leak.ts\n+leak\n*** End Patch\n",
|
|
8886
9046
|
});
|
|
@@ -8888,6 +9048,16 @@ exit 0
|
|
|
8888
9048
|
const mixedReason = String(blockedMixedPatch.outputJson?.reason ?? "");
|
|
8889
9049
|
assert.match(mixedReason, /apply_patch target/);
|
|
8890
9050
|
assert.match(mixedReason, /src\/leak\.ts/);
|
|
9051
|
+
const blockedMixedDraftPatch = await preToolUse("apply_patch", "tool-ralplan-draft-patch-mixed", {
|
|
9052
|
+
input: "*** Begin Patch\n*** Add File: .omx/drafts/issue-3105.md\n+ok\n*** Add File: src/leak.ts\n+leak\n*** End Patch\n",
|
|
9053
|
+
});
|
|
9054
|
+
assert.equal(blockedMixedDraftPatch.outputJson?.decision, "block");
|
|
9055
|
+
assert.match(String(blockedMixedDraftPatch.outputJson?.reason ?? ""), /src\/leak\.ts/);
|
|
9056
|
+
const blockedMixedDraftBash = await preToolUse("Bash", "tool-ralplan-draft-bash-mixed", {
|
|
9057
|
+
command: "printf '# Draft\\n' > .omx/drafts/issue-3105.md; printf 'bad\\n' > src/leak.ts",
|
|
9058
|
+
});
|
|
9059
|
+
assert.equal(blockedMixedDraftBash.outputJson?.decision, "block");
|
|
9060
|
+
assert.match(String(blockedMixedDraftBash.outputJson?.reason ?? ""), /src\/leak\.ts/);
|
|
8891
9061
|
const blockedUnparseablePatch = await preToolUse("apply_patch", "tool-ralplan-patch-unparseable", {
|
|
8892
9062
|
input: "not a recognizable patch",
|
|
8893
9063
|
});
|
|
@@ -8900,6 +9070,11 @@ exit 0
|
|
|
8900
9070
|
const unresolvedReason = String(blockedUnresolvedRedirect.outputJson?.reason ?? "");
|
|
8901
9071
|
assert.match(unresolvedReason, /unresolved Bash write target/);
|
|
8902
9072
|
assert.match(unresolvedReason, /\$PLAN_PATH/);
|
|
9073
|
+
const blockedUnresolvedDraftRedirect = await preToolUse("Bash", "tool-ralplan-draft-redirect-unresolved", {
|
|
9074
|
+
command: "cat > \"$DRAFT_PATH\" <<'EOF'\ncontent\nEOF",
|
|
9075
|
+
});
|
|
9076
|
+
assert.equal(blockedUnresolvedDraftRedirect.outputJson?.decision, "block");
|
|
9077
|
+
assert.match(String(blockedUnresolvedDraftRedirect.outputJson?.reason ?? ""), /\$DRAFT_PATH/);
|
|
8903
9078
|
const blockedTraversal = await preToolUse("Write", "tool-ralplan-traversal-block", {
|
|
8904
9079
|
file_path: ".omx/plans/../../src/leak.ts",
|
|
8905
9080
|
content: "bad",
|
|
@@ -9490,6 +9665,159 @@ PY`,
|
|
|
9490
9665
|
await rm(cwd, { recursive: true, force: true });
|
|
9491
9666
|
}
|
|
9492
9667
|
});
|
|
9668
|
+
it("keeps untyped collaboration child implementation writes blocked under active ralplan planning while typed trusted agents pass (#3116)", async () => {
|
|
9669
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3116-ralplan-untyped-"));
|
|
9670
|
+
process.env.OMX_ROOT = cwd;
|
|
9671
|
+
try {
|
|
9672
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
9673
|
+
const sessionId = "sess-3116-ralplan-untyped";
|
|
9674
|
+
const leaderThreadId = "thread-3116-ralplan-untyped-leader";
|
|
9675
|
+
const childThreadId = "thread-3116-ralplan-untyped-child";
|
|
9676
|
+
const sessionDir = join(stateDir, "sessions", sessionId);
|
|
9677
|
+
const nowIso = new Date().toISOString();
|
|
9678
|
+
await mkdir(sessionDir, { recursive: true });
|
|
9679
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId, cwd });
|
|
9680
|
+
await writeJson(join(sessionDir, "skill-active-state.json"), {
|
|
9681
|
+
version: 1,
|
|
9682
|
+
active: true,
|
|
9683
|
+
skill: "autopilot",
|
|
9684
|
+
phase: "ralplan",
|
|
9685
|
+
session_id: sessionId,
|
|
9686
|
+
thread_id: leaderThreadId,
|
|
9687
|
+
active_skills: [
|
|
9688
|
+
{ skill: "autopilot", phase: "ralplan", active: true, session_id: sessionId, thread_id: leaderThreadId },
|
|
9689
|
+
],
|
|
9690
|
+
});
|
|
9691
|
+
await writeJson(join(sessionDir, "autopilot-state.json"), {
|
|
9692
|
+
active: true,
|
|
9693
|
+
mode: "autopilot",
|
|
9694
|
+
current_phase: "ralplan",
|
|
9695
|
+
session_id: sessionId,
|
|
9696
|
+
thread_id: leaderThreadId,
|
|
9697
|
+
});
|
|
9698
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
9699
|
+
schemaVersion: 1,
|
|
9700
|
+
sessions: {
|
|
9701
|
+
[sessionId]: {
|
|
9702
|
+
session_id: sessionId,
|
|
9703
|
+
leader_thread_id: leaderThreadId,
|
|
9704
|
+
updated_at: nowIso,
|
|
9705
|
+
threads: {
|
|
9706
|
+
[leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
|
|
9707
|
+
[childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1, leader_thread_id: leaderThreadId },
|
|
9708
|
+
},
|
|
9709
|
+
},
|
|
9710
|
+
},
|
|
9711
|
+
});
|
|
9712
|
+
// Untyped tracked/spawn-provenance child must NOT bypass the ralplan planning guard.
|
|
9713
|
+
const untypedChild = await dispatchCodexNativeHook({
|
|
9714
|
+
hook_event_name: "PreToolUse",
|
|
9715
|
+
cwd,
|
|
9716
|
+
session_id: sessionId,
|
|
9717
|
+
thread_id: childThreadId,
|
|
9718
|
+
agent_role: "collaboration-child",
|
|
9719
|
+
source: {
|
|
9720
|
+
subagent: { thread_spawn: { parent_thread_id: leaderThreadId } },
|
|
9721
|
+
},
|
|
9722
|
+
tool_name: "Edit",
|
|
9723
|
+
tool_input: { file_path: "src/implementation.ts", old_string: "a", new_string: "b" },
|
|
9724
|
+
}, { cwd });
|
|
9725
|
+
assert.equal(untypedChild.outputJson?.decision, "block");
|
|
9726
|
+
// A typed trusted agent retains its existing planning exemption.
|
|
9727
|
+
const typedChild = await dispatchCodexNativeHook({
|
|
9728
|
+
hook_event_name: "PreToolUse",
|
|
9729
|
+
cwd,
|
|
9730
|
+
session_id: sessionId,
|
|
9731
|
+
thread_id: childThreadId,
|
|
9732
|
+
agent_role: "executor",
|
|
9733
|
+
source: {
|
|
9734
|
+
subagent: { thread_spawn: { parent_thread_id: leaderThreadId } },
|
|
9735
|
+
},
|
|
9736
|
+
tool_name: "Edit",
|
|
9737
|
+
tool_input: { file_path: "src/implementation.ts", old_string: "a", new_string: "b" },
|
|
9738
|
+
}, { cwd });
|
|
9739
|
+
assert.equal(typedChild.outputJson, null);
|
|
9740
|
+
}
|
|
9741
|
+
finally {
|
|
9742
|
+
await rm(cwd, { recursive: true, force: true });
|
|
9743
|
+
}
|
|
9744
|
+
});
|
|
9745
|
+
it("keeps untyped collaboration child implementation writes blocked under active deep-interview planning while typed trusted agents pass (#3116)", async () => {
|
|
9746
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3116-di-untyped-"));
|
|
9747
|
+
process.env.OMX_ROOT = cwd;
|
|
9748
|
+
try {
|
|
9749
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
9750
|
+
const sessionId = "sess-3116-di-untyped";
|
|
9751
|
+
const leaderThreadId = "thread-3116-di-untyped-leader";
|
|
9752
|
+
const childThreadId = "thread-3116-di-untyped-child";
|
|
9753
|
+
const sessionDir = join(stateDir, "sessions", sessionId);
|
|
9754
|
+
const nowIso = new Date().toISOString();
|
|
9755
|
+
await mkdir(sessionDir, { recursive: true });
|
|
9756
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId, cwd });
|
|
9757
|
+
await writeJson(join(sessionDir, "skill-active-state.json"), {
|
|
9758
|
+
version: 1,
|
|
9759
|
+
active: true,
|
|
9760
|
+
skill: "deep-interview",
|
|
9761
|
+
phase: "planning",
|
|
9762
|
+
session_id: sessionId,
|
|
9763
|
+
thread_id: leaderThreadId,
|
|
9764
|
+
active_skills: [
|
|
9765
|
+
{ skill: "deep-interview", phase: "planning", active: true, session_id: sessionId, thread_id: leaderThreadId },
|
|
9766
|
+
],
|
|
9767
|
+
});
|
|
9768
|
+
await writeJson(join(sessionDir, "deep-interview-state.json"), {
|
|
9769
|
+
active: true,
|
|
9770
|
+
mode: "deep-interview",
|
|
9771
|
+
current_phase: "intent-first",
|
|
9772
|
+
session_id: sessionId,
|
|
9773
|
+
});
|
|
9774
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
9775
|
+
schemaVersion: 1,
|
|
9776
|
+
sessions: {
|
|
9777
|
+
[sessionId]: {
|
|
9778
|
+
session_id: sessionId,
|
|
9779
|
+
leader_thread_id: leaderThreadId,
|
|
9780
|
+
updated_at: nowIso,
|
|
9781
|
+
threads: {
|
|
9782
|
+
[leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
|
|
9783
|
+
[childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1, leader_thread_id: leaderThreadId },
|
|
9784
|
+
},
|
|
9785
|
+
},
|
|
9786
|
+
},
|
|
9787
|
+
});
|
|
9788
|
+
// Untyped tracked/spawn-provenance child must NOT bypass the deep-interview planning guard.
|
|
9789
|
+
const untypedChild = await dispatchCodexNativeHook({
|
|
9790
|
+
hook_event_name: "PreToolUse",
|
|
9791
|
+
cwd,
|
|
9792
|
+
session_id: sessionId,
|
|
9793
|
+
thread_id: childThreadId,
|
|
9794
|
+
agent_role: "collaboration-child",
|
|
9795
|
+
source: {
|
|
9796
|
+
subagent: { thread_spawn: { parent_thread_id: leaderThreadId } },
|
|
9797
|
+
},
|
|
9798
|
+
tool_name: "Edit",
|
|
9799
|
+
tool_input: { file_path: "src/implementation.ts", old_string: "a", new_string: "b" },
|
|
9800
|
+
}, { cwd });
|
|
9801
|
+
assert.equal(untypedChild.outputJson?.decision, "block");
|
|
9802
|
+
// A typed trusted agent retains its existing planning exemption.
|
|
9803
|
+
const typedChild = await dispatchCodexNativeHook({
|
|
9804
|
+
hook_event_name: "PreToolUse",
|
|
9805
|
+
cwd,
|
|
9806
|
+
session_id: sessionId,
|
|
9807
|
+
thread_id: childThreadId,
|
|
9808
|
+
agent_role: "executor",
|
|
9809
|
+
source: {
|
|
9810
|
+
subagent: { thread_spawn: { parent_thread_id: leaderThreadId } },
|
|
9811
|
+
},
|
|
9812
|
+
tool_name: "Edit",
|
|
9813
|
+
tool_input: { file_path: "src/implementation.ts", old_string: "a", new_string: "b" },
|
|
9814
|
+
}, { cwd });
|
|
9815
|
+
assert.equal(typedChild.outputJson, null);
|
|
9816
|
+
}
|
|
9817
|
+
finally {
|
|
9818
|
+
await rm(cwd, { recursive: true, force: true });
|
|
9819
|
+
}
|
|
9820
|
+
});
|
|
9493
9821
|
it("allows null-device fd redirects while deep-interview blocks real Bash writes", async () => {
|
|
9494
9822
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-deep-interview-null-redirect-"));
|
|
9495
9823
|
try {
|
|
@@ -15060,17 +15388,19 @@ PY`,
|
|
|
15060
15388
|
execFileSync("git", ["add", "src/scripts/codex-native-hook.ts"], { cwd, stdio: "ignore" });
|
|
15061
15389
|
execFileSync("git", ["commit", "-m", "init"], { cwd, stdio: "ignore" });
|
|
15062
15390
|
await writeFile(join(cwd, "src", "scripts", "codex-native-hook.ts"), "export const hook = 2;\n", "utf-8");
|
|
15063
|
-
const
|
|
15391
|
+
const output = parseSingleJsonStdout(runNativeHookCli({
|
|
15064
15392
|
hook_event_name: "Stop",
|
|
15065
15393
|
cwd,
|
|
15066
15394
|
session_id: "sess-stop-doc-refresh",
|
|
15067
15395
|
last_assistant_message: "Launch-ready: yes",
|
|
15068
|
-
}, { cwd });
|
|
15069
|
-
assert.
|
|
15070
|
-
assert.equal(
|
|
15071
|
-
assert.equal(
|
|
15072
|
-
assert.
|
|
15073
|
-
assert.
|
|
15396
|
+
}, { cwd }));
|
|
15397
|
+
assert.deepEqual(Object.keys(output).sort(), ["systemMessage"]);
|
|
15398
|
+
assert.equal("hookSpecificOutput" in output, false);
|
|
15399
|
+
assert.equal("decision" in output, false);
|
|
15400
|
+
assert.equal("reason" in output, false);
|
|
15401
|
+
assert.equal("stopReason" in output, false);
|
|
15402
|
+
assert.match(String(output.systemMessage ?? ""), /Document-refresh warning/);
|
|
15403
|
+
assert.match(String(output.systemMessage ?? ""), /staged \+ unstaged changes/);
|
|
15074
15404
|
}
|
|
15075
15405
|
finally {
|
|
15076
15406
|
await rm(cwd, { recursive: true, force: true });
|
|
@@ -17579,6 +17909,17 @@ PY`,
|
|
|
17579
17909
|
}, { cwd });
|
|
17580
17910
|
assert.equal(result.omxEventName, "pre-tool-use");
|
|
17581
17911
|
assert.equal(result.outputJson, null);
|
|
17912
|
+
const allowedDraftPatch = await dispatchCodexNativeHook({
|
|
17913
|
+
hook_event_name: "PreToolUse",
|
|
17914
|
+
cwd,
|
|
17915
|
+
session_id: nativeSessionId,
|
|
17916
|
+
thread_id: "thread-ralplan-native-map-draft-artifact",
|
|
17917
|
+
tool_name: "apply_patch",
|
|
17918
|
+
tool_input: {
|
|
17919
|
+
input: `*** Begin Patch\n*** Add File: ${join(cwd, ".omx", "drafts", "issue-3105.md")}\n+# Draft\n*** End Patch\n`,
|
|
17920
|
+
},
|
|
17921
|
+
}, { cwd });
|
|
17922
|
+
assert.equal(allowedDraftPatch.outputJson, null);
|
|
17582
17923
|
}
|
|
17583
17924
|
finally {
|
|
17584
17925
|
await rm(cwd, { recursive: true, force: true });
|
|
@@ -17686,6 +18027,17 @@ PY`,
|
|
|
17686
18027
|
session_id: ownerSessionId,
|
|
17687
18028
|
cwd: ownerCwd,
|
|
17688
18029
|
});
|
|
18030
|
+
const allowedForeignDraft = await dispatchCodexNativeHook({
|
|
18031
|
+
hook_event_name: "PreToolUse",
|
|
18032
|
+
cwd,
|
|
18033
|
+
session_id: "019e-ralplan-live-root-unresolved-current",
|
|
18034
|
+
thread_id: "thread-ralplan-live-root-conflict",
|
|
18035
|
+
tool_name: "apply_patch",
|
|
18036
|
+
tool_input: {
|
|
18037
|
+
input: `*** Begin Patch\n*** Add File: ${join(cwd, ".omx", "drafts", "issue-3105.md")}\n+# Draft\n*** End Patch\n`,
|
|
18038
|
+
},
|
|
18039
|
+
}, { cwd });
|
|
18040
|
+
assert.equal(allowedForeignDraft.outputJson, null);
|
|
17689
18041
|
const result = await dispatchCodexNativeHook({
|
|
17690
18042
|
hook_event_name: "PreToolUse",
|
|
17691
18043
|
cwd,
|
|
@@ -17758,6 +18110,17 @@ PY`,
|
|
|
17758
18110
|
}, { cwd });
|
|
17759
18111
|
assert.equal(result.omxEventName, "pre-tool-use");
|
|
17760
18112
|
assert.equal(result.outputJson, null);
|
|
18113
|
+
const allowedDraftPatch = await dispatchCodexNativeHook({
|
|
18114
|
+
hook_event_name: "PreToolUse",
|
|
18115
|
+
cwd,
|
|
18116
|
+
session_id: nativeSessionId,
|
|
18117
|
+
thread_id: "thread-ralplan-live-root-owner-draft-pass",
|
|
18118
|
+
tool_name: "apply_patch",
|
|
18119
|
+
tool_input: {
|
|
18120
|
+
input: `*** Begin Patch\n*** Add File: ${join(cwd, ".omx", "drafts", "live-root-owner.md")}\n+# Draft\n*** End Patch\n`,
|
|
18121
|
+
},
|
|
18122
|
+
}, { cwd });
|
|
18123
|
+
assert.equal(allowedDraftPatch.outputJson, null);
|
|
17761
18124
|
}
|
|
17762
18125
|
finally {
|
|
17763
18126
|
await rm(cwd, { recursive: true, force: true });
|
|
@@ -18121,28 +18484,135 @@ PY`,
|
|
|
18121
18484
|
await rm(cwd, { recursive: true, force: true });
|
|
18122
18485
|
}
|
|
18123
18486
|
});
|
|
18124
|
-
it("
|
|
18125
|
-
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-
|
|
18487
|
+
it("keeps the Conductor unblocked: quoted redirect/source text is not a write target and terminal blocked-state writes survive genuinely unsupported native delegation (#3119)", async () => {
|
|
18488
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-conductor-3119-quote-deadlock-"));
|
|
18126
18489
|
try {
|
|
18127
18490
|
const stateDir = join(cwd, ".omx", "state");
|
|
18128
|
-
const sessionId = "sess-
|
|
18491
|
+
const sessionId = "sess-conductor-3119-quote-deadlock";
|
|
18129
18492
|
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
18130
18493
|
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
18131
|
-
await writeSessionSkillActiveState(stateDir, sessionId, "
|
|
18132
|
-
await writeJson(join(stateDir, "sessions", sessionId, "
|
|
18494
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "ultragoal", "planning");
|
|
18495
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ultragoal-state.json"), {
|
|
18133
18496
|
active: true,
|
|
18134
|
-
mode: "
|
|
18497
|
+
mode: "ultragoal",
|
|
18135
18498
|
current_phase: "planning",
|
|
18136
18499
|
session_id: sessionId,
|
|
18137
18500
|
});
|
|
18138
|
-
|
|
18501
|
+
// Native delegation is genuinely unsupported (explicit negative evidence).
|
|
18502
|
+
await writeJson(join(stateDir, "native-subagent-support.json"), {
|
|
18503
|
+
schema_version: 1,
|
|
18504
|
+
status: "unsupported",
|
|
18505
|
+
reason: "multi_agent_v1_unavailable",
|
|
18506
|
+
session_id: sessionId,
|
|
18507
|
+
evidence: "unknown tool: multi_agent_v1.spawn_agent",
|
|
18508
|
+
observed_at: new Date().toISOString(),
|
|
18509
|
+
cwd,
|
|
18510
|
+
});
|
|
18511
|
+
const dispatch = (command) => dispatchCodexNativeHook({
|
|
18139
18512
|
hook_event_name: "PreToolUse",
|
|
18140
18513
|
cwd,
|
|
18141
18514
|
session_id: sessionId,
|
|
18142
|
-
thread_id: "thread-
|
|
18143
|
-
|
|
18144
|
-
|
|
18145
|
-
|
|
18515
|
+
thread_id: "thread-conductor-3119-quote-deadlock",
|
|
18516
|
+
tool_name: "Bash",
|
|
18517
|
+
tool_input: { command },
|
|
18518
|
+
}, { cwd });
|
|
18519
|
+
// Deadlock prevention (defect B): the Conductor must still terminalize its
|
|
18520
|
+
// own workflow state even when delegation is genuinely unsupported, even
|
|
18521
|
+
// when the JSON payload contains a `>` character.
|
|
18522
|
+
const terminalBlockedWrite = await dispatch("omx state write --mode ultragoal --input '{\"active\":true,\"current_phase\":\"blocked\",\"reason\":\"native delegation unavailable -> terminalized\"}' --json");
|
|
18523
|
+
assert.notEqual(terminalBlockedWrite.outputJson?.decision, "block");
|
|
18524
|
+
// Defect C: quoted regex/source text with redirect metacharacters is not a
|
|
18525
|
+
// write target, so issue creation is not falsely blocked.
|
|
18526
|
+
const issueCreate = await dispatch("gh issue create --title x --body 'Guard misparsed regex /[^>]+>{1,2}/ as a redirect target'");
|
|
18527
|
+
assert.notEqual(issueCreate.outputJson?.decision, "block");
|
|
18528
|
+
// Fail-closed preserved: a REAL unquoted redirect to a non-metadata path is
|
|
18529
|
+
// still blocked.
|
|
18530
|
+
const realRedirect = await dispatch("printf pwn > src/runtime.ts");
|
|
18531
|
+
assert.equal(realRedirect.outputJson?.decision, "block");
|
|
18532
|
+
assert.match(String(realRedirect.outputJson?.reason ?? ""), /not workflow state\/ledger\/mailbox\/handoff metadata/);
|
|
18533
|
+
// A real unquoted redirect to allowed workflow metadata still passes.
|
|
18534
|
+
const metadataRedirect = await dispatch("printf blocked > .omx/state/conductor.log");
|
|
18535
|
+
assert.notEqual(metadataRedirect.outputJson?.decision, "block");
|
|
18536
|
+
}
|
|
18537
|
+
finally {
|
|
18538
|
+
await rm(cwd, { recursive: true, force: true });
|
|
18539
|
+
}
|
|
18540
|
+
});
|
|
18541
|
+
it("fail-closed: escaped-quote and ANSI-C quoted redirects to source stay blocked while legit quoted data and nested shells behave (#3119)", async () => {
|
|
18542
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-conductor-3119-escaped-quote-"));
|
|
18543
|
+
try {
|
|
18544
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
18545
|
+
const sessionId = "sess-conductor-3119-escaped-quote";
|
|
18546
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
18547
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
18548
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "ultragoal", "planning");
|
|
18549
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ultragoal-state.json"), {
|
|
18550
|
+
active: true,
|
|
18551
|
+
mode: "ultragoal",
|
|
18552
|
+
current_phase: "planning",
|
|
18553
|
+
session_id: sessionId,
|
|
18554
|
+
});
|
|
18555
|
+
const dispatch = (command) => dispatchCodexNativeHook({
|
|
18556
|
+
hook_event_name: "PreToolUse",
|
|
18557
|
+
cwd,
|
|
18558
|
+
session_id: sessionId,
|
|
18559
|
+
thread_id: "thread-conductor-3119-escaped-quote",
|
|
18560
|
+
tool_name: "Bash",
|
|
18561
|
+
tool_input: { command },
|
|
18562
|
+
}, { cwd });
|
|
18563
|
+
// A top-level escaped quote (\' \") is a LITERAL char in bash, not a quote
|
|
18564
|
+
// opener, and $'...' is ANSI-C quoting. None of these may hide the real
|
|
18565
|
+
// `>` redirect \u2014 all of these genuinely write src/runtime.ts and MUST block.
|
|
18566
|
+
const mustBlock = [
|
|
18567
|
+
"printf pwn > src/runtime.ts", // plain control
|
|
18568
|
+
"printf pwn \\' > src/runtime.ts \\'", // escaped single quote
|
|
18569
|
+
"printf pwn \\\" > src/runtime.ts \\\"", // escaped double quote
|
|
18570
|
+
"printf pwn $'\\'' > src/runtime.ts $'\\''", // ANSI-C escaped quote
|
|
18571
|
+
"bash -c 'printf pwn > src/runtime.ts'", // nested shell redirect
|
|
18572
|
+
];
|
|
18573
|
+
for (const command of mustBlock) {
|
|
18574
|
+
const result = await dispatch(command);
|
|
18575
|
+
assert.equal(result.outputJson?.decision, "block", command);
|
|
18576
|
+
assert.match(String(result.outputJson?.reason ?? ""), /not workflow state\/ledger\/mailbox\/handoff metadata/, command);
|
|
18577
|
+
}
|
|
18578
|
+
// Legitimate quoted DATA metacharacters (single quotes, double quotes,
|
|
18579
|
+
// ANSI-C) are not redirects and must not be falsely blocked.
|
|
18580
|
+
const mustNotBlock = [
|
|
18581
|
+
"gh issue create --title x --body 'a > b regex /[^>]+>{1,2}/'",
|
|
18582
|
+
"echo \"value > threshold\"",
|
|
18583
|
+
"printf $'a>b'",
|
|
18584
|
+
];
|
|
18585
|
+
for (const command of mustNotBlock) {
|
|
18586
|
+
const result = await dispatch(command);
|
|
18587
|
+
assert.notEqual(result.outputJson?.decision, "block", command);
|
|
18588
|
+
}
|
|
18589
|
+
}
|
|
18590
|
+
finally {
|
|
18591
|
+
await rm(cwd, { recursive: true, force: true });
|
|
18592
|
+
}
|
|
18593
|
+
});
|
|
18594
|
+
it("blocks Main-root ralplan writes even when payload has only a typed agent_role", async () => {
|
|
18595
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-agent-role-main-"));
|
|
18596
|
+
try {
|
|
18597
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
18598
|
+
const sessionId = "sess-ralplan-agent-role-main";
|
|
18599
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
18600
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
18601
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "ralplan", "planning");
|
|
18602
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
|
|
18603
|
+
active: true,
|
|
18604
|
+
mode: "ralplan",
|
|
18605
|
+
current_phase: "planning",
|
|
18606
|
+
session_id: sessionId,
|
|
18607
|
+
});
|
|
18608
|
+
const result = await dispatchCodexNativeHook({
|
|
18609
|
+
hook_event_name: "PreToolUse",
|
|
18610
|
+
cwd,
|
|
18611
|
+
session_id: sessionId,
|
|
18612
|
+
thread_id: "thread-ralplan-agent-role-main",
|
|
18613
|
+
agent_role: "executor",
|
|
18614
|
+
tool_name: "Edit",
|
|
18615
|
+
tool_input: { file_path: "src/runtime.ts" },
|
|
18146
18616
|
}, { cwd });
|
|
18147
18617
|
assert.equal(result.outputJson?.decision, "block");
|
|
18148
18618
|
assert.match(String(result.outputJson?.reason ?? ""), /Ralplan is active \(phase: planning\)/);
|
|
@@ -18379,6 +18849,239 @@ PY`,
|
|
|
18379
18849
|
await rm(cwd, { recursive: true, force: true });
|
|
18380
18850
|
}
|
|
18381
18851
|
});
|
|
18852
|
+
it("blocks a corrupt kind:subagent leader that omits leader_thread_id via native session identity while still trusting a real non-leader child (#3117 P2)", async () => {
|
|
18853
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3117-corrupt-leader-no-lead-"));
|
|
18854
|
+
process.env.OMX_ROOT = cwd;
|
|
18855
|
+
try {
|
|
18856
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
18857
|
+
const sessionId = "sess-3117-corrupt-leader-no-lead";
|
|
18858
|
+
const leaderThreadId = "thread-3117-corrupt-leader-no-lead-leader";
|
|
18859
|
+
const childThreadId = "thread-3117-corrupt-leader-no-lead-child";
|
|
18860
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
18861
|
+
// native_session_id anchors the leader identity even though the tracker omits
|
|
18862
|
+
// leader_thread_id and corruptly labels the leader kind:"subagent".
|
|
18863
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId });
|
|
18864
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
|
|
18865
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
|
|
18866
|
+
active: true,
|
|
18867
|
+
mode: "ralph",
|
|
18868
|
+
current_phase: "executing",
|
|
18869
|
+
session_id: sessionId,
|
|
18870
|
+
});
|
|
18871
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
18872
|
+
schemaVersion: 1,
|
|
18873
|
+
sessions: {
|
|
18874
|
+
[sessionId]: {
|
|
18875
|
+
session_id: sessionId,
|
|
18876
|
+
threads: {
|
|
18877
|
+
[leaderThreadId]: { thread_id: leaderThreadId, kind: "subagent", mode: "collaboration-child" },
|
|
18878
|
+
[childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child" },
|
|
18879
|
+
},
|
|
18880
|
+
},
|
|
18881
|
+
},
|
|
18882
|
+
});
|
|
18883
|
+
// Untyped leader payload must stay blocked: leader identity is anchored to
|
|
18884
|
+
// native_session_id, not the corrupt tracker kind.
|
|
18885
|
+
const untypedLeader = await dispatchCodexNativeHook({
|
|
18886
|
+
hook_event_name: "PreToolUse",
|
|
18887
|
+
cwd,
|
|
18888
|
+
session_id: sessionId,
|
|
18889
|
+
thread_id: leaderThreadId,
|
|
18890
|
+
tool_name: "apply_patch",
|
|
18891
|
+
tool_input: { file_path: "src/feature.ts" },
|
|
18892
|
+
}, { cwd });
|
|
18893
|
+
assert.equal(untypedLeader.outputJson?.decision, "block");
|
|
18894
|
+
assert.match(String(untypedLeader.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
|
|
18895
|
+
// Control: a genuine non-leader child stays trusted (P1 preserved).
|
|
18896
|
+
const untypedChild = await dispatchCodexNativeHook({
|
|
18897
|
+
hook_event_name: "PreToolUse",
|
|
18898
|
+
cwd,
|
|
18899
|
+
session_id: sessionId,
|
|
18900
|
+
thread_id: childThreadId,
|
|
18901
|
+
tool_name: "apply_patch",
|
|
18902
|
+
tool_input: { file_path: "src/feature.ts" },
|
|
18903
|
+
}, { cwd });
|
|
18904
|
+
assert.equal(untypedChild.outputJson, null);
|
|
18905
|
+
}
|
|
18906
|
+
finally {
|
|
18907
|
+
await rm(cwd, { recursive: true, force: true });
|
|
18908
|
+
}
|
|
18909
|
+
});
|
|
18910
|
+
it("fails closed for untyped provenance when no authoritative leader anchor exists (#3117 P2)", async () => {
|
|
18911
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3117-no-leader-anchor-"));
|
|
18912
|
+
process.env.OMX_ROOT = cwd;
|
|
18913
|
+
try {
|
|
18914
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
18915
|
+
const sessionId = "sess-3117-no-leader-anchor";
|
|
18916
|
+
const leaderThreadId = "thread-3117-no-leader-anchor-leader";
|
|
18917
|
+
const childThreadId = "thread-3117-no-leader-anchor-child";
|
|
18918
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
18919
|
+
// No native_session_id / owner ids and no tracker leader_thread_id: the leader is
|
|
18920
|
+
// unidentifiable, so untyped provenance must not be inferred from kind:"subagent".
|
|
18921
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
|
|
18922
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
|
|
18923
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
|
|
18924
|
+
active: true,
|
|
18925
|
+
mode: "ralph",
|
|
18926
|
+
current_phase: "executing",
|
|
18927
|
+
session_id: sessionId,
|
|
18928
|
+
});
|
|
18929
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
18930
|
+
schemaVersion: 1,
|
|
18931
|
+
sessions: {
|
|
18932
|
+
[sessionId]: {
|
|
18933
|
+
session_id: sessionId,
|
|
18934
|
+
threads: {
|
|
18935
|
+
[leaderThreadId]: { thread_id: leaderThreadId, kind: "subagent", mode: "collaboration-child" },
|
|
18936
|
+
[childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child" },
|
|
18937
|
+
},
|
|
18938
|
+
},
|
|
18939
|
+
},
|
|
18940
|
+
});
|
|
18941
|
+
for (const threadId of [leaderThreadId, childThreadId]) {
|
|
18942
|
+
const result = await dispatchCodexNativeHook({
|
|
18943
|
+
hook_event_name: "PreToolUse",
|
|
18944
|
+
cwd,
|
|
18945
|
+
session_id: sessionId,
|
|
18946
|
+
thread_id: threadId,
|
|
18947
|
+
tool_name: "apply_patch",
|
|
18948
|
+
tool_input: { file_path: "src/feature.ts" },
|
|
18949
|
+
}, { cwd });
|
|
18950
|
+
assert.equal(result.outputJson?.decision, "block", threadId);
|
|
18951
|
+
assert.match(String(result.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
|
|
18952
|
+
}
|
|
18953
|
+
}
|
|
18954
|
+
finally {
|
|
18955
|
+
await rm(cwd, { recursive: true, force: true });
|
|
18956
|
+
}
|
|
18957
|
+
});
|
|
18958
|
+
it("does not borrow leader anchors from a foreign root session.json when evaluating another session (#3117 P3)", async () => {
|
|
18959
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3117-cross-session-anchor-"));
|
|
18960
|
+
process.env.OMX_ROOT = cwd;
|
|
18961
|
+
try {
|
|
18962
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
18963
|
+
const foreignSessionId = "sess-3117-p3-foreign-A";
|
|
18964
|
+
const foreignLeaderThreadId = "thread-3117-p3-A-leader";
|
|
18965
|
+
const sessionId = "sess-3117-p3-checked-B";
|
|
18966
|
+
const leaderThreadId = "thread-3117-p3-B-leader";
|
|
18967
|
+
const childThreadId = "thread-3117-p3-B-child";
|
|
18968
|
+
const sessionPath = join(stateDir, "session.json");
|
|
18969
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
18970
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
|
|
18971
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
|
|
18972
|
+
active: true,
|
|
18973
|
+
mode: "ralph",
|
|
18974
|
+
current_phase: "executing",
|
|
18975
|
+
session_id: sessionId,
|
|
18976
|
+
});
|
|
18977
|
+
// Evaluated session B has a corrupt tracker: no leader_thread_id, leader mislabeled kind:"subagent".
|
|
18978
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
18979
|
+
schemaVersion: 1,
|
|
18980
|
+
sessions: {
|
|
18981
|
+
[sessionId]: {
|
|
18982
|
+
session_id: sessionId,
|
|
18983
|
+
threads: {
|
|
18984
|
+
[leaderThreadId]: { thread_id: leaderThreadId, kind: "subagent", mode: "collaboration-child" },
|
|
18985
|
+
[childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child" },
|
|
18986
|
+
},
|
|
18987
|
+
},
|
|
18988
|
+
},
|
|
18989
|
+
});
|
|
18990
|
+
const dispatchThread = async (threadId) => dispatchCodexNativeHook({
|
|
18991
|
+
hook_event_name: "PreToolUse",
|
|
18992
|
+
cwd,
|
|
18993
|
+
session_id: sessionId,
|
|
18994
|
+
thread_id: threadId,
|
|
18995
|
+
tool_name: "apply_patch",
|
|
18996
|
+
tool_input: { file_path: "src/feature.ts" },
|
|
18997
|
+
}, { cwd });
|
|
18998
|
+
// Root session.json owns a DIFFERENT session A: its native/owner ids must not
|
|
18999
|
+
// anchor session B, so B's mislabeled leader stays blocked (fail closed).
|
|
19000
|
+
await writeJson(sessionPath, { session_id: foreignSessionId, native_session_id: foreignLeaderThreadId });
|
|
19001
|
+
const foreignLeader = await dispatchThread(leaderThreadId);
|
|
19002
|
+
assert.equal(foreignLeader.outputJson?.decision, "block");
|
|
19003
|
+
assert.match(String(foreignLeader.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
|
|
19004
|
+
const foreignChild = await dispatchThread(childThreadId);
|
|
19005
|
+
assert.equal(foreignChild.outputJson?.decision, "block");
|
|
19006
|
+
// Control: when the root session.json owns the evaluated session B, its
|
|
19007
|
+
// native_session_id anchors the leader (blocked) while a genuine child is trusted.
|
|
19008
|
+
await writeJson(sessionPath, { session_id: sessionId, native_session_id: leaderThreadId });
|
|
19009
|
+
const ownedLeader = await dispatchThread(leaderThreadId);
|
|
19010
|
+
assert.equal(ownedLeader.outputJson?.decision, "block");
|
|
19011
|
+
assert.match(String(ownedLeader.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
|
|
19012
|
+
const ownedChild = await dispatchThread(childThreadId);
|
|
19013
|
+
assert.equal(ownedChild.outputJson, null);
|
|
19014
|
+
}
|
|
19015
|
+
finally {
|
|
19016
|
+
await rm(cwd, { recursive: true, force: true });
|
|
19017
|
+
}
|
|
19018
|
+
});
|
|
19019
|
+
it("fails closed when session.json carries only owner session ids without a leader thread anchor (#3117 P4)", async () => {
|
|
19020
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3117-owner-only-anchor-"));
|
|
19021
|
+
process.env.OMX_ROOT = cwd;
|
|
19022
|
+
try {
|
|
19023
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
19024
|
+
const sessionId = "sess-3117-p4-owner-only";
|
|
19025
|
+
const leaderThreadId = "thread-3117-p4-leader";
|
|
19026
|
+
const childThreadId = "thread-3117-p4-child";
|
|
19027
|
+
const untrackedThreadId = "thread-3117-p4-untracked";
|
|
19028
|
+
const sessionPath = join(stateDir, "session.json");
|
|
19029
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
19030
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
|
|
19031
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
|
|
19032
|
+
active: true,
|
|
19033
|
+
mode: "ralph",
|
|
19034
|
+
current_phase: "executing",
|
|
19035
|
+
session_id: sessionId,
|
|
19036
|
+
});
|
|
19037
|
+
// Corrupt tracker for the evaluated session: no leader_thread_id, leader mislabeled kind:"subagent".
|
|
19038
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
19039
|
+
schemaVersion: 1,
|
|
19040
|
+
sessions: {
|
|
19041
|
+
[sessionId]: {
|
|
19042
|
+
session_id: sessionId,
|
|
19043
|
+
threads: {
|
|
19044
|
+
[leaderThreadId]: { thread_id: leaderThreadId, kind: "subagent", mode: "collaboration-child" },
|
|
19045
|
+
[childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child" },
|
|
19046
|
+
},
|
|
19047
|
+
},
|
|
19048
|
+
},
|
|
19049
|
+
});
|
|
19050
|
+
const dispatchThread = async (threadId) => dispatchCodexNativeHook({
|
|
19051
|
+
hook_event_name: "PreToolUse",
|
|
19052
|
+
cwd,
|
|
19053
|
+
session_id: sessionId,
|
|
19054
|
+
thread_id: threadId,
|
|
19055
|
+
tool_name: "apply_patch",
|
|
19056
|
+
tool_input: { file_path: "src/feature.ts" },
|
|
19057
|
+
}, { cwd });
|
|
19058
|
+
// session.json maps to the evaluated session via owner ids but has NO
|
|
19059
|
+
// native_session_id: owner session ids are not leader thread anchors, so trust
|
|
19060
|
+
// must fail closed rather than treat their presence as an anchor (#3117 P4).
|
|
19061
|
+
await writeJson(sessionPath, {
|
|
19062
|
+
session_id: sessionId,
|
|
19063
|
+
owner_omx_session_id: "owner-omx-3117-p4",
|
|
19064
|
+
owner_codex_session_id: "owner-codex-3117-p4",
|
|
19065
|
+
});
|
|
19066
|
+
const ownerOnlyLeader = await dispatchThread(leaderThreadId);
|
|
19067
|
+
assert.equal(ownerOnlyLeader.outputJson?.decision, "block");
|
|
19068
|
+
assert.match(String(ownerOnlyLeader.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
|
|
19069
|
+
// Control: an untracked thread also stays blocked under the same state.
|
|
19070
|
+
const ownerOnlyUntracked = await dispatchThread(untrackedThreadId);
|
|
19071
|
+
assert.equal(ownerOnlyUntracked.outputJson?.decision, "block");
|
|
19072
|
+
// Control: once a genuine native_session_id leader thread anchor exists for this
|
|
19073
|
+
// session, the leader is blocked via that anchor while a real child is trusted.
|
|
19074
|
+
await writeJson(sessionPath, { session_id: sessionId, native_session_id: leaderThreadId });
|
|
19075
|
+
const anchoredLeader = await dispatchThread(leaderThreadId);
|
|
19076
|
+
assert.equal(anchoredLeader.outputJson?.decision, "block");
|
|
19077
|
+
assert.match(String(anchoredLeader.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
|
|
19078
|
+
const anchoredChild = await dispatchThread(childThreadId);
|
|
19079
|
+
assert.equal(anchoredChild.outputJson, null);
|
|
19080
|
+
}
|
|
19081
|
+
finally {
|
|
19082
|
+
await rm(cwd, { recursive: true, force: true });
|
|
19083
|
+
}
|
|
19084
|
+
});
|
|
18382
19085
|
it("blocks conductor writes when thread_spawn provenance is attached to the leader thread", async () => {
|
|
18383
19086
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-conductor-thread-spawn-leader-"));
|
|
18384
19087
|
const originalOmxRoot = process.env.OMX_ROOT;
|
|
@@ -18449,6 +19152,387 @@ PY`,
|
|
|
18449
19152
|
await rm(cwd, { recursive: true, force: true });
|
|
18450
19153
|
}
|
|
18451
19154
|
});
|
|
19155
|
+
it("denies Main-root conductor apply_patch from the session leader thread during active Ralph (#3116)", async () => {
|
|
19156
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3116-root-denial-"));
|
|
19157
|
+
process.env.OMX_ROOT = cwd;
|
|
19158
|
+
try {
|
|
19159
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
19160
|
+
const sessionId = "sess-3116-root-denial";
|
|
19161
|
+
const leaderThreadId = "thread-3116-root-denial-leader";
|
|
19162
|
+
const nowIso = new Date().toISOString();
|
|
19163
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
19164
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId });
|
|
19165
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
|
|
19166
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
|
|
19167
|
+
active: true,
|
|
19168
|
+
mode: "ralph",
|
|
19169
|
+
current_phase: "executing",
|
|
19170
|
+
session_id: sessionId,
|
|
19171
|
+
});
|
|
19172
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
19173
|
+
schemaVersion: 1,
|
|
19174
|
+
sessions: {
|
|
19175
|
+
[sessionId]: {
|
|
19176
|
+
session_id: sessionId,
|
|
19177
|
+
leader_thread_id: leaderThreadId,
|
|
19178
|
+
updated_at: nowIso,
|
|
19179
|
+
threads: {
|
|
19180
|
+
[leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
|
|
19181
|
+
},
|
|
19182
|
+
},
|
|
19183
|
+
},
|
|
19184
|
+
});
|
|
19185
|
+
const result = await dispatchCodexNativeHook({
|
|
19186
|
+
hook_event_name: "PreToolUse",
|
|
19187
|
+
cwd,
|
|
19188
|
+
session_id: sessionId,
|
|
19189
|
+
thread_id: leaderThreadId,
|
|
19190
|
+
tool_name: "apply_patch",
|
|
19191
|
+
tool_input: { file_path: "src/feature.ts" },
|
|
19192
|
+
}, { cwd });
|
|
19193
|
+
assert.equal(result.outputJson?.decision, "block");
|
|
19194
|
+
assert.match(String(result.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
|
|
19195
|
+
}
|
|
19196
|
+
finally {
|
|
19197
|
+
await rm(cwd, { recursive: true, force: true });
|
|
19198
|
+
}
|
|
19199
|
+
});
|
|
19200
|
+
it("allows apply_patch from an untyped collaboration.spawn_agent child tracked as a subagent during active Ralph (#3116)", async () => {
|
|
19201
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3116-trusted-child-"));
|
|
19202
|
+
process.env.OMX_ROOT = cwd;
|
|
19203
|
+
try {
|
|
19204
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
19205
|
+
const sessionId = "sess-3116-trusted-child";
|
|
19206
|
+
const leaderThreadId = "thread-3116-trusted-child-leader";
|
|
19207
|
+
const childThreadId = "thread-3116-trusted-child-worker";
|
|
19208
|
+
const nowIso = new Date().toISOString();
|
|
19209
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
19210
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId });
|
|
19211
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
|
|
19212
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
|
|
19213
|
+
active: true,
|
|
19214
|
+
mode: "ralph",
|
|
19215
|
+
current_phase: "executing",
|
|
19216
|
+
session_id: sessionId,
|
|
19217
|
+
});
|
|
19218
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
19219
|
+
schemaVersion: 1,
|
|
19220
|
+
sessions: {
|
|
19221
|
+
[sessionId]: {
|
|
19222
|
+
session_id: sessionId,
|
|
19223
|
+
leader_thread_id: leaderThreadId,
|
|
19224
|
+
updated_at: nowIso,
|
|
19225
|
+
threads: {
|
|
19226
|
+
[leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
|
|
19227
|
+
[childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1, leader_thread_id: leaderThreadId },
|
|
19228
|
+
},
|
|
19229
|
+
},
|
|
19230
|
+
},
|
|
19231
|
+
});
|
|
19232
|
+
// Untyped role (not in the typed-agent catalog) must not defeat tracker-backed trust.
|
|
19233
|
+
const result = await dispatchCodexNativeHook({
|
|
19234
|
+
hook_event_name: "PreToolUse",
|
|
19235
|
+
cwd,
|
|
19236
|
+
session_id: sessionId,
|
|
19237
|
+
thread_id: childThreadId,
|
|
19238
|
+
agent_role: "collaboration-child",
|
|
19239
|
+
source: {
|
|
19240
|
+
subagent: {
|
|
19241
|
+
thread_spawn: {
|
|
19242
|
+
parent_thread_id: leaderThreadId,
|
|
19243
|
+
depth: 1,
|
|
19244
|
+
agent_nickname: "Helper",
|
|
19245
|
+
},
|
|
19246
|
+
},
|
|
19247
|
+
},
|
|
19248
|
+
tool_name: "apply_patch",
|
|
19249
|
+
tool_input: { file_path: "src/feature.ts" },
|
|
19250
|
+
}, { cwd });
|
|
19251
|
+
assert.equal(result.outputJson, null);
|
|
19252
|
+
}
|
|
19253
|
+
finally {
|
|
19254
|
+
await rm(cwd, { recursive: true, force: true });
|
|
19255
|
+
}
|
|
19256
|
+
});
|
|
19257
|
+
it("allows apply_patch from untyped collaboration descendants via tracked thread and tracked parent chain (#3116)", async () => {
|
|
19258
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3116-descendant-"));
|
|
19259
|
+
process.env.OMX_ROOT = cwd;
|
|
19260
|
+
try {
|
|
19261
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
19262
|
+
const sessionId = "sess-3116-descendant";
|
|
19263
|
+
const leaderThreadId = "thread-3116-descendant-leader";
|
|
19264
|
+
const childThreadId = "thread-3116-descendant-child";
|
|
19265
|
+
const grandchildThreadId = "thread-3116-descendant-grandchild";
|
|
19266
|
+
const greatGrandchildThreadId = "thread-3116-descendant-great-grandchild";
|
|
19267
|
+
const nowIso = new Date().toISOString();
|
|
19268
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
19269
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId });
|
|
19270
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
|
|
19271
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
|
|
19272
|
+
active: true,
|
|
19273
|
+
mode: "ralph",
|
|
19274
|
+
current_phase: "executing",
|
|
19275
|
+
session_id: sessionId,
|
|
19276
|
+
});
|
|
19277
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
19278
|
+
schemaVersion: 1,
|
|
19279
|
+
sessions: {
|
|
19280
|
+
[sessionId]: {
|
|
19281
|
+
session_id: sessionId,
|
|
19282
|
+
leader_thread_id: leaderThreadId,
|
|
19283
|
+
updated_at: nowIso,
|
|
19284
|
+
threads: {
|
|
19285
|
+
[leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
|
|
19286
|
+
[childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1, leader_thread_id: leaderThreadId },
|
|
19287
|
+
[grandchildThreadId]: { thread_id: grandchildThreadId, kind: "subagent", mode: "collaboration-child", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1, leader_thread_id: childThreadId },
|
|
19288
|
+
},
|
|
19289
|
+
},
|
|
19290
|
+
},
|
|
19291
|
+
});
|
|
19292
|
+
// (a) A descendant whose own thread is tracked as a subagent is trusted directly.
|
|
19293
|
+
const trackedDescendant = await dispatchCodexNativeHook({
|
|
19294
|
+
hook_event_name: "PreToolUse",
|
|
19295
|
+
cwd,
|
|
19296
|
+
session_id: sessionId,
|
|
19297
|
+
thread_id: grandchildThreadId,
|
|
19298
|
+
tool_name: "apply_patch",
|
|
19299
|
+
tool_input: { file_path: "src/feature.ts" },
|
|
19300
|
+
}, { cwd });
|
|
19301
|
+
assert.equal(trackedDescendant.outputJson, null);
|
|
19302
|
+
// (b) A not-yet-tracked descendant is trusted when its runtime spawn parent is a tracked thread.
|
|
19303
|
+
const chainedDescendant = await dispatchCodexNativeHook({
|
|
19304
|
+
hook_event_name: "PreToolUse",
|
|
19305
|
+
cwd,
|
|
19306
|
+
session_id: sessionId,
|
|
19307
|
+
thread_id: greatGrandchildThreadId,
|
|
19308
|
+
agent_role: "collaboration-child",
|
|
19309
|
+
source: {
|
|
19310
|
+
subagent: {
|
|
19311
|
+
thread_spawn: {
|
|
19312
|
+
parent_thread_id: grandchildThreadId,
|
|
19313
|
+
depth: 3,
|
|
19314
|
+
},
|
|
19315
|
+
},
|
|
19316
|
+
},
|
|
19317
|
+
tool_name: "apply_patch",
|
|
19318
|
+
tool_input: { file_path: "src/feature.ts" },
|
|
19319
|
+
}, { cwd });
|
|
19320
|
+
assert.equal(chainedDescendant.outputJson, null);
|
|
19321
|
+
}
|
|
19322
|
+
finally {
|
|
19323
|
+
await rm(cwd, { recursive: true, force: true });
|
|
19324
|
+
}
|
|
19325
|
+
});
|
|
19326
|
+
it("resolves the session-pinned Ralph state so collaboration children edit while the leader stays blocked (#3116)", async () => {
|
|
19327
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3116-pinned-state-"));
|
|
19328
|
+
process.env.OMX_ROOT = cwd;
|
|
19329
|
+
try {
|
|
19330
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
19331
|
+
const sessionId = "sess-3116-pinned-state";
|
|
19332
|
+
const leaderThreadId = "thread-3116-pinned-state-leader";
|
|
19333
|
+
const childThreadId = "thread-3116-pinned-state-child";
|
|
19334
|
+
const nowIso = new Date().toISOString();
|
|
19335
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
19336
|
+
// Native session id differs from the canonical session id: the guard must resolve it.
|
|
19337
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId });
|
|
19338
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
|
|
19339
|
+
const ralphStatePath = join(stateDir, "sessions", sessionId, "ralph-state.json");
|
|
19340
|
+
await writeJson(ralphStatePath, {
|
|
19341
|
+
active: true,
|
|
19342
|
+
mode: "ralph",
|
|
19343
|
+
current_phase: "executing",
|
|
19344
|
+
session_id: sessionId,
|
|
19345
|
+
});
|
|
19346
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
19347
|
+
schemaVersion: 1,
|
|
19348
|
+
sessions: {
|
|
19349
|
+
[sessionId]: {
|
|
19350
|
+
session_id: sessionId,
|
|
19351
|
+
leader_thread_id: leaderThreadId,
|
|
19352
|
+
updated_at: nowIso,
|
|
19353
|
+
threads: {
|
|
19354
|
+
[leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
|
|
19355
|
+
[childThreadId]: { thread_id: childThreadId, kind: "subagent", mode: "collaboration-child", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1, leader_thread_id: leaderThreadId },
|
|
19356
|
+
},
|
|
19357
|
+
},
|
|
19358
|
+
},
|
|
19359
|
+
});
|
|
19360
|
+
// Payload carries the native session id, which must map to the canonical session.
|
|
19361
|
+
const childEdit = await dispatchCodexNativeHook({
|
|
19362
|
+
hook_event_name: "PreToolUse",
|
|
19363
|
+
cwd,
|
|
19364
|
+
session_id: leaderThreadId,
|
|
19365
|
+
thread_id: childThreadId,
|
|
19366
|
+
tool_name: "apply_patch",
|
|
19367
|
+
tool_input: { file_path: "src/feature.ts" },
|
|
19368
|
+
}, { cwd });
|
|
19369
|
+
assert.equal(childEdit.outputJson, null);
|
|
19370
|
+
const leaderEdit = await dispatchCodexNativeHook({
|
|
19371
|
+
hook_event_name: "PreToolUse",
|
|
19372
|
+
cwd,
|
|
19373
|
+
session_id: leaderThreadId,
|
|
19374
|
+
thread_id: leaderThreadId,
|
|
19375
|
+
tool_name: "apply_patch",
|
|
19376
|
+
tool_input: { file_path: "src/feature.ts" },
|
|
19377
|
+
}, { cwd });
|
|
19378
|
+
assert.equal(leaderEdit.outputJson?.decision, "block");
|
|
19379
|
+
assert.match(String(leaderEdit.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
|
|
19380
|
+
// The guard reads the session-pinned phase: flipping it to "starting" releases the leader too.
|
|
19381
|
+
await writeJson(ralphStatePath, {
|
|
19382
|
+
active: true,
|
|
19383
|
+
mode: "ralph",
|
|
19384
|
+
current_phase: "starting",
|
|
19385
|
+
session_id: sessionId,
|
|
19386
|
+
});
|
|
19387
|
+
const leaderEditAfterStarting = await dispatchCodexNativeHook({
|
|
19388
|
+
hook_event_name: "PreToolUse",
|
|
19389
|
+
cwd,
|
|
19390
|
+
session_id: leaderThreadId,
|
|
19391
|
+
thread_id: leaderThreadId,
|
|
19392
|
+
tool_name: "apply_patch",
|
|
19393
|
+
tool_input: { file_path: "src/feature.ts" },
|
|
19394
|
+
}, { cwd });
|
|
19395
|
+
assert.equal(leaderEditAfterStarting.outputJson, null);
|
|
19396
|
+
}
|
|
19397
|
+
finally {
|
|
19398
|
+
await rm(cwd, { recursive: true, force: true });
|
|
19399
|
+
}
|
|
19400
|
+
});
|
|
19401
|
+
it("trusts the native collaboration.spawn_agent child surface via runtime spawn provenance during active Ralph (#3116)", async () => {
|
|
19402
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3116-collab-surface-"));
|
|
19403
|
+
process.env.OMX_ROOT = cwd;
|
|
19404
|
+
try {
|
|
19405
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
19406
|
+
const sessionId = "sess-3116-collab-surface";
|
|
19407
|
+
const leaderThreadId = "thread-3116-collab-surface-leader";
|
|
19408
|
+
const childThreadId = "thread-3116-collab-surface-child";
|
|
19409
|
+
const nowIso = new Date().toISOString();
|
|
19410
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
19411
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId });
|
|
19412
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
|
|
19413
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
|
|
19414
|
+
active: true,
|
|
19415
|
+
mode: "ralph",
|
|
19416
|
+
current_phase: "executing",
|
|
19417
|
+
session_id: sessionId,
|
|
19418
|
+
});
|
|
19419
|
+
// Child SessionStart has not been recorded yet: only the leader is tracked.
|
|
19420
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
19421
|
+
schemaVersion: 1,
|
|
19422
|
+
sessions: {
|
|
19423
|
+
[sessionId]: {
|
|
19424
|
+
session_id: sessionId,
|
|
19425
|
+
leader_thread_id: leaderThreadId,
|
|
19426
|
+
updated_at: nowIso,
|
|
19427
|
+
threads: {
|
|
19428
|
+
[leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
|
|
19429
|
+
},
|
|
19430
|
+
},
|
|
19431
|
+
},
|
|
19432
|
+
});
|
|
19433
|
+
// No typed agent_role at all: a raw collaboration.spawn_agent child payload shape.
|
|
19434
|
+
const result = await dispatchCodexNativeHook({
|
|
19435
|
+
hook_event_name: "PreToolUse",
|
|
19436
|
+
cwd,
|
|
19437
|
+
session_id: sessionId,
|
|
19438
|
+
thread_id: childThreadId,
|
|
19439
|
+
source: {
|
|
19440
|
+
subagent: {
|
|
19441
|
+
thread_spawn: {
|
|
19442
|
+
parent_thread_id: leaderThreadId,
|
|
19443
|
+
depth: 1,
|
|
19444
|
+
agent_nickname: "Implementer",
|
|
19445
|
+
},
|
|
19446
|
+
},
|
|
19447
|
+
},
|
|
19448
|
+
tool_name: "apply_patch",
|
|
19449
|
+
tool_input: { file_path: "src/feature.ts" },
|
|
19450
|
+
}, { cwd });
|
|
19451
|
+
assert.equal(result.outputJson, null);
|
|
19452
|
+
}
|
|
19453
|
+
finally {
|
|
19454
|
+
await rm(cwd, { recursive: true, force: true });
|
|
19455
|
+
}
|
|
19456
|
+
});
|
|
19457
|
+
it("denies spoofed or untrusted collaboration provenance while the main root stays protected during active Ralph (#3116)", async () => {
|
|
19458
|
+
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-3116-spoof-denial-"));
|
|
19459
|
+
process.env.OMX_ROOT = cwd;
|
|
19460
|
+
try {
|
|
19461
|
+
const stateDir = join(cwd, ".omx", "state");
|
|
19462
|
+
const sessionId = "sess-3116-spoof-denial";
|
|
19463
|
+
const leaderThreadId = "thread-3116-spoof-denial-leader";
|
|
19464
|
+
const nowIso = new Date().toISOString();
|
|
19465
|
+
await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
|
|
19466
|
+
await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: leaderThreadId });
|
|
19467
|
+
await writeSessionSkillActiveState(stateDir, sessionId, "ralph", "executing");
|
|
19468
|
+
await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
|
|
19469
|
+
active: true,
|
|
19470
|
+
mode: "ralph",
|
|
19471
|
+
current_phase: "executing",
|
|
19472
|
+
session_id: sessionId,
|
|
19473
|
+
});
|
|
19474
|
+
await writeJson(join(stateDir, "subagent-tracking.json"), {
|
|
19475
|
+
schemaVersion: 1,
|
|
19476
|
+
sessions: {
|
|
19477
|
+
[sessionId]: {
|
|
19478
|
+
session_id: sessionId,
|
|
19479
|
+
leader_thread_id: leaderThreadId,
|
|
19480
|
+
updated_at: nowIso,
|
|
19481
|
+
threads: {
|
|
19482
|
+
[leaderThreadId]: { thread_id: leaderThreadId, kind: "leader", first_seen_at: nowIso, last_seen_at: nowIso, turn_count: 1 },
|
|
19483
|
+
},
|
|
19484
|
+
},
|
|
19485
|
+
},
|
|
19486
|
+
});
|
|
19487
|
+
// (a) Untracked thread whose declared parent is neither the leader nor a tracked thread.
|
|
19488
|
+
const orphanParent = await dispatchCodexNativeHook({
|
|
19489
|
+
hook_event_name: "PreToolUse",
|
|
19490
|
+
cwd,
|
|
19491
|
+
session_id: sessionId,
|
|
19492
|
+
thread_id: "thread-3116-spoof-orphan",
|
|
19493
|
+
agent_role: "collaboration-child",
|
|
19494
|
+
source: {
|
|
19495
|
+
subagent: {
|
|
19496
|
+
thread_spawn: { parent_thread_id: "thread-3116-spoof-outsider" },
|
|
19497
|
+
},
|
|
19498
|
+
},
|
|
19499
|
+
tool_name: "apply_patch",
|
|
19500
|
+
tool_input: { file_path: "src/feature.ts" },
|
|
19501
|
+
}, { cwd });
|
|
19502
|
+
assert.equal(orphanParent.outputJson?.decision, "block");
|
|
19503
|
+
assert.match(String(orphanParent.outputJson?.reason ?? ""), /Main-root Conductor mode is active \(ralph phase: executing\)/);
|
|
19504
|
+
// (b) Untyped child with no provenance at all (no tracker thread, no spawn source).
|
|
19505
|
+
const noProvenance = await dispatchCodexNativeHook({
|
|
19506
|
+
hook_event_name: "PreToolUse",
|
|
19507
|
+
cwd,
|
|
19508
|
+
session_id: sessionId,
|
|
19509
|
+
thread_id: "thread-3116-spoof-bare",
|
|
19510
|
+
agent_role: "collaboration-child",
|
|
19511
|
+
tool_name: "apply_patch",
|
|
19512
|
+
tool_input: { file_path: "src/feature.ts" },
|
|
19513
|
+
}, { cwd });
|
|
19514
|
+
assert.equal(noProvenance.outputJson?.decision, "block");
|
|
19515
|
+
// (c) Spawn provenance attached to the leader thread cannot promote the main root.
|
|
19516
|
+
const leaderSelfSpawn = await dispatchCodexNativeHook({
|
|
19517
|
+
hook_event_name: "PreToolUse",
|
|
19518
|
+
cwd,
|
|
19519
|
+
session_id: sessionId,
|
|
19520
|
+
thread_id: leaderThreadId,
|
|
19521
|
+
agent_role: "collaboration-child",
|
|
19522
|
+
source: {
|
|
19523
|
+
subagent: {
|
|
19524
|
+
thread_spawn: { parent_thread_id: leaderThreadId },
|
|
19525
|
+
},
|
|
19526
|
+
},
|
|
19527
|
+
tool_name: "apply_patch",
|
|
19528
|
+
tool_input: { file_path: "src/feature.ts" },
|
|
19529
|
+
}, { cwd });
|
|
19530
|
+
assert.equal(leaderSelfSpawn.outputJson?.decision, "block");
|
|
19531
|
+
}
|
|
19532
|
+
finally {
|
|
19533
|
+
await rm(cwd, { recursive: true, force: true });
|
|
19534
|
+
}
|
|
19535
|
+
});
|
|
18452
19536
|
it("blocks Main-root ralph conductor source and planning artifact writes while allowing .omx workflow state writes", async () => {
|
|
18453
19537
|
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralph-conductor-write-"));
|
|
18454
19538
|
try {
|