oh-my-codex 0.16.4 → 0.17.0

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.
Files changed (138) hide show
  1. package/Cargo.lock +5 -5
  2. package/Cargo.toml +1 -1
  3. package/dist/catalog/__tests__/generator.test.js +2 -0
  4. package/dist/catalog/__tests__/generator.test.js.map +1 -1
  5. package/dist/cli/__tests__/doctor-warning-copy.test.js +80 -7
  6. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  7. package/dist/cli/__tests__/index.test.js +17 -11
  8. package/dist/cli/__tests__/index.test.js.map +1 -1
  9. package/dist/cli/__tests__/mcp-serve.test.js +4 -0
  10. package/dist/cli/__tests__/mcp-serve.test.js.map +1 -1
  11. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js +8 -3
  12. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js.map +1 -1
  13. package/dist/cli/__tests__/setup-install-mode.test.js +27 -1
  14. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  15. package/dist/cli/__tests__/ultragoal.test.js +22 -0
  16. package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
  17. package/dist/cli/doctor.d.ts.map +1 -1
  18. package/dist/cli/doctor.js +66 -10
  19. package/dist/cli/doctor.js.map +1 -1
  20. package/dist/cli/index.d.ts +8 -2
  21. package/dist/cli/index.d.ts.map +1 -1
  22. package/dist/cli/index.js +17 -7
  23. package/dist/cli/index.js.map +1 -1
  24. package/dist/cli/mcp-serve.d.ts.map +1 -1
  25. package/dist/cli/mcp-serve.js +4 -0
  26. package/dist/cli/mcp-serve.js.map +1 -1
  27. package/dist/cli/plugin-marketplace.d.ts +20 -0
  28. package/dist/cli/plugin-marketplace.d.ts.map +1 -1
  29. package/dist/cli/plugin-marketplace.js +115 -1
  30. package/dist/cli/plugin-marketplace.js.map +1 -1
  31. package/dist/cli/setup.d.ts.map +1 -1
  32. package/dist/cli/setup.js +29 -10
  33. package/dist/cli/setup.js.map +1 -1
  34. package/dist/cli/ultragoal.d.ts.map +1 -1
  35. package/dist/cli/ultragoal.js +7 -1
  36. package/dist/cli/ultragoal.js.map +1 -1
  37. package/dist/config/__tests__/codex-hooks.test.js +136 -9
  38. package/dist/config/__tests__/codex-hooks.test.js.map +1 -1
  39. package/dist/config/__tests__/generator-idempotent.test.js +15 -0
  40. package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
  41. package/dist/config/codex-hooks.d.ts +13 -14
  42. package/dist/config/codex-hooks.d.ts.map +1 -1
  43. package/dist/config/codex-hooks.js +85 -7
  44. package/dist/config/codex-hooks.js.map +1 -1
  45. package/dist/config/generator.d.ts +4 -1
  46. package/dist/config/generator.d.ts.map +1 -1
  47. package/dist/config/generator.js +15 -9
  48. package/dist/config/generator.js.map +1 -1
  49. package/dist/config/omx-first-party-mcp.d.ts.map +1 -1
  50. package/dist/config/omx-first-party-mcp.js +7 -0
  51. package/dist/config/omx-first-party-mcp.js.map +1 -1
  52. package/dist/hooks/__tests__/design-skill.test.d.ts +2 -0
  53. package/dist/hooks/__tests__/design-skill.test.d.ts.map +1 -0
  54. package/dist/hooks/__tests__/design-skill.test.js +55 -0
  55. package/dist/hooks/__tests__/design-skill.test.js.map +1 -0
  56. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +265 -0
  57. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
  58. package/dist/hooks/__tests__/skill-catalog-hygiene.test.js +1 -1
  59. package/dist/hooks/__tests__/skill-catalog-hygiene.test.js.map +1 -1
  60. package/dist/hooks/__tests__/skill-guidance-contract.test.js +41 -0
  61. package/dist/hooks/__tests__/skill-guidance-contract.test.js.map +1 -1
  62. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  63. package/dist/hooks/keyword-detector.js +5 -1
  64. package/dist/hooks/keyword-detector.js.map +1 -1
  65. package/dist/hooks/keyword-registry.d.ts.map +1 -1
  66. package/dist/hooks/keyword-registry.js +2 -0
  67. package/dist/hooks/keyword-registry.js.map +1 -1
  68. package/dist/hooks/prompt-guidance-contract.d.ts.map +1 -1
  69. package/dist/hooks/prompt-guidance-contract.js +47 -2
  70. package/dist/hooks/prompt-guidance-contract.js.map +1 -1
  71. package/dist/mcp/__tests__/bootstrap.test.js +3 -0
  72. package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
  73. package/dist/mcp/__tests__/hermes-bridge.test.d.ts +2 -0
  74. package/dist/mcp/__tests__/hermes-bridge.test.d.ts.map +1 -0
  75. package/dist/mcp/__tests__/hermes-bridge.test.js +374 -0
  76. package/dist/mcp/__tests__/hermes-bridge.test.js.map +1 -0
  77. package/dist/mcp/__tests__/state-paths.test.js +96 -13
  78. package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
  79. package/dist/mcp/bootstrap.d.ts +1 -1
  80. package/dist/mcp/bootstrap.d.ts.map +1 -1
  81. package/dist/mcp/bootstrap.js +2 -0
  82. package/dist/mcp/bootstrap.js.map +1 -1
  83. package/dist/mcp/hermes-bridge.d.ts +81 -0
  84. package/dist/mcp/hermes-bridge.d.ts.map +1 -0
  85. package/dist/mcp/hermes-bridge.js +400 -0
  86. package/dist/mcp/hermes-bridge.js.map +1 -0
  87. package/dist/mcp/hermes-server.d.ts +269 -0
  88. package/dist/mcp/hermes-server.d.ts.map +1 -0
  89. package/dist/mcp/hermes-server.js +121 -0
  90. package/dist/mcp/hermes-server.js.map +1 -0
  91. package/dist/mcp/state-paths.d.ts.map +1 -1
  92. package/dist/mcp/state-paths.js +41 -9
  93. package/dist/mcp/state-paths.js.map +1 -1
  94. package/dist/modes/__tests__/base-tmux-pane.test.js +31 -1
  95. package/dist/modes/__tests__/base-tmux-pane.test.js.map +1 -1
  96. package/dist/scripts/__tests__/codex-native-hook.test.js +187 -2
  97. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  98. package/dist/scripts/codex-native-hook.d.ts +1 -0
  99. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  100. package/dist/scripts/codex-native-hook.js +44 -17
  101. package/dist/scripts/codex-native-hook.js.map +1 -1
  102. package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
  103. package/dist/scripts/notify-hook/tmux-injection.js +91 -2
  104. package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
  105. package/dist/state/mode-state-context.d.ts +2 -0
  106. package/dist/state/mode-state-context.d.ts.map +1 -1
  107. package/dist/state/mode-state-context.js +21 -0
  108. package/dist/state/mode-state-context.js.map +1 -1
  109. package/dist/ultragoal/__tests__/artifacts.test.js +121 -0
  110. package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
  111. package/dist/ultragoal/artifacts.d.ts +9 -1
  112. package/dist/ultragoal/artifacts.d.ts.map +1 -1
  113. package/dist/ultragoal/artifacts.js +105 -3
  114. package/dist/ultragoal/artifacts.js.map +1 -1
  115. package/dist/utils/__tests__/paths.test.js +31 -1
  116. package/dist/utils/__tests__/paths.test.js.map +1 -1
  117. package/dist/utils/paths.d.ts +6 -0
  118. package/dist/utils/paths.d.ts.map +1 -1
  119. package/dist/utils/paths.js +18 -0
  120. package/dist/utils/paths.js.map +1 -1
  121. package/dist/wiki/lifecycle.js +3 -3
  122. package/dist/wiki/lifecycle.js.map +1 -1
  123. package/package.json +1 -1
  124. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  125. package/plugins/oh-my-codex/.mcp.json +8 -0
  126. package/plugins/oh-my-codex/skills/design/SKILL.md +180 -0
  127. package/plugins/oh-my-codex/skills/skill/SKILL.md +2 -1
  128. package/plugins/oh-my-codex/skills/ultraqa/SKILL.md +161 -47
  129. package/plugins/oh-my-codex/skills/visual-ralph/SKILL.md +2 -2
  130. package/skills/design/SKILL.md +180 -0
  131. package/skills/frontend-ui-ux/SKILL.md +6 -2
  132. package/skills/skill/SKILL.md +2 -1
  133. package/skills/ultraqa/SKILL.md +161 -47
  134. package/skills/visual-ralph/SKILL.md +2 -2
  135. package/src/scripts/__tests__/codex-native-hook.test.ts +206 -1
  136. package/src/scripts/codex-native-hook.ts +45 -18
  137. package/src/scripts/notify-hook/tmux-injection.ts +110 -3
  138. package/templates/catalog-manifest.json +9 -2
@@ -0,0 +1,374 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdir, mkdtemp, realpath, rm, symlink, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { afterEach, beforeEach, describe, it } from "node:test";
6
+ import { hermesListArtifacts, hermesListSessions, hermesReadArtifact, hermesReadStatus, hermesReadTail, hermesReportStatus, hermesSendPrompt, hermesStartSession, } from "../hermes-bridge.js";
7
+ const originalRoots = process.env.OMX_MCP_WORKDIR_ROOTS;
8
+ const originalOmxRoot = process.env.OMX_ROOT;
9
+ const originalOmxStateRoot = process.env.OMX_STATE_ROOT;
10
+ const originalTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
11
+ const originalSessionId = process.env.OMX_SESSION_ID;
12
+ const originalCodexSessionId = process.env.CODEX_SESSION_ID;
13
+ const originalGenericSessionId = process.env.SESSION_ID;
14
+ beforeEach(() => {
15
+ delete process.env.OMX_MCP_WORKDIR_ROOTS;
16
+ delete process.env.OMX_ROOT;
17
+ delete process.env.OMX_STATE_ROOT;
18
+ delete process.env.OMX_TEAM_STATE_ROOT;
19
+ delete process.env.OMX_SESSION_ID;
20
+ delete process.env.CODEX_SESSION_ID;
21
+ delete process.env.SESSION_ID;
22
+ });
23
+ afterEach(() => {
24
+ if (typeof originalRoots === "string")
25
+ process.env.OMX_MCP_WORKDIR_ROOTS = originalRoots;
26
+ else
27
+ delete process.env.OMX_MCP_WORKDIR_ROOTS;
28
+ if (typeof originalOmxRoot === "string")
29
+ process.env.OMX_ROOT = originalOmxRoot;
30
+ else
31
+ delete process.env.OMX_ROOT;
32
+ if (typeof originalOmxStateRoot === "string")
33
+ process.env.OMX_STATE_ROOT = originalOmxStateRoot;
34
+ else
35
+ delete process.env.OMX_STATE_ROOT;
36
+ if (typeof originalTeamStateRoot === "string")
37
+ process.env.OMX_TEAM_STATE_ROOT = originalTeamStateRoot;
38
+ else
39
+ delete process.env.OMX_TEAM_STATE_ROOT;
40
+ if (typeof originalSessionId === "string")
41
+ process.env.OMX_SESSION_ID = originalSessionId;
42
+ else
43
+ delete process.env.OMX_SESSION_ID;
44
+ if (typeof originalCodexSessionId === "string")
45
+ process.env.CODEX_SESSION_ID = originalCodexSessionId;
46
+ else
47
+ delete process.env.CODEX_SESSION_ID;
48
+ if (typeof originalGenericSessionId === "string")
49
+ process.env.SESSION_ID = originalGenericSessionId;
50
+ else
51
+ delete process.env.SESSION_ID;
52
+ });
53
+ async function tempWorkspace(name) {
54
+ delete process.env.OMX_ROOT;
55
+ delete process.env.OMX_STATE_ROOT;
56
+ delete process.env.OMX_TEAM_STATE_ROOT;
57
+ return await realpath(await mkdtemp(join(await realpath(tmpdir()), name)));
58
+ }
59
+ describe("Hermes MCP bridge core", () => {
60
+ it("lists session-scoped OMX state without exposing terminal internals", async () => {
61
+ const cwd = await tempWorkspace("omx-hermes-list-");
62
+ try {
63
+ await mkdir(join(cwd, ".omx", "state", "sessions", "sess-a"), { recursive: true });
64
+ await writeFile(join(cwd, ".omx", "state", "sessions", "sess-a", "ralph-state.json"), JSON.stringify({ active: true, current_phase: "executing" }));
65
+ const result = await hermesListSessions({ workingDirectory: cwd });
66
+ assert.equal(result.ok, true);
67
+ assert.deepEqual(result.data?.sessions, [
68
+ { session_id: "sess-a", active: false, source: "session_state_dir", modes: ["ralph"] },
69
+ ]);
70
+ }
71
+ finally {
72
+ await rm(cwd, { recursive: true, force: true });
73
+ }
74
+ });
75
+ it("projects status without leaking raw internal mode state", async () => {
76
+ const cwd = await tempWorkspace("omx-hermes-status-");
77
+ try {
78
+ await mkdir(join(cwd, ".omx", "state", "sessions", "sess-a"), { recursive: true });
79
+ await writeFile(join(cwd, ".omx", "state", "sessions", "sess-a", "ralph-state.json"), JSON.stringify({
80
+ active: true,
81
+ current_phase: "verifying",
82
+ run_outcome: "continue",
83
+ lifecycle_outcome: "finished",
84
+ updated_at: "2026-05-11T00:00:00.000Z",
85
+ completed_at: "2026-05-11T00:01:00.000Z",
86
+ private_control_room: { token: "do-not-leak" },
87
+ state: { prompt_to_artifact_checklist: ["internal"] },
88
+ }));
89
+ const result = await hermesReadStatus({ workingDirectory: cwd, session_id: "sess-a" });
90
+ assert.equal(result.ok, true);
91
+ assert.deepEqual(result.data?.modes, [
92
+ {
93
+ mode: "ralph",
94
+ scope: "session",
95
+ active: true,
96
+ phase: "verifying",
97
+ run_outcome: "continue",
98
+ lifecycle_outcome: "finished",
99
+ updated_at: "2026-05-11T00:00:00.000Z",
100
+ completed_at: "2026-05-11T00:01:00.000Z",
101
+ },
102
+ ]);
103
+ assert.equal(JSON.stringify(result).includes("do-not-leak"), false);
104
+ assert.equal(JSON.stringify(result).includes("prompt_to_artifact_checklist"), false);
105
+ }
106
+ finally {
107
+ await rm(cwd, { recursive: true, force: true });
108
+ }
109
+ });
110
+ it("projects current session metadata without leaking process or tmux internals", async () => {
111
+ const cwd = await tempWorkspace("omx-hermes-current-status-");
112
+ try {
113
+ const result = await hermesReadStatus({ workingDirectory: cwd }, {
114
+ readUsableSessionState: async () => ({
115
+ session_id: "sess-current",
116
+ native_session_id: "native-current",
117
+ cwd,
118
+ started_at: "2026-05-11T00:00:00.000Z",
119
+ pid: 12345,
120
+ pid_cmdline: "codex --secret",
121
+ pid_start_ticks: 67890,
122
+ tmux_session_name: "private-tmux",
123
+ }),
124
+ });
125
+ assert.equal(result.ok, true);
126
+ assert.deepEqual(result.data?.session, {
127
+ session_id: "sess-current",
128
+ native_session_id: "native-current",
129
+ cwd,
130
+ started_at: "2026-05-11T00:00:00.000Z",
131
+ });
132
+ assert.equal(JSON.stringify(result).includes("12345"), false);
133
+ assert.equal(JSON.stringify(result).includes("codex --secret"), false);
134
+ assert.equal(JSON.stringify(result).includes("private-tmux"), false);
135
+ }
136
+ finally {
137
+ await rm(cwd, { recursive: true, force: true });
138
+ }
139
+ });
140
+ it("reads a bounded session-history tail without tmux scrollback", async () => {
141
+ const cwd = await tempWorkspace("omx-hermes-tail-");
142
+ try {
143
+ await mkdir(join(cwd, ".omx", "logs"), { recursive: true });
144
+ await writeFile(join(cwd, ".omx", "logs", "session-history.jsonl"), ["one", "two", "three"].map((message) => JSON.stringify({ message })).join("\n") + "\n");
145
+ const result = await hermesReadTail({ workingDirectory: cwd, lines: 2 });
146
+ assert.equal(result.ok, true);
147
+ assert.deepEqual(result.data?.tail, [JSON.stringify({ message: "two" }), JSON.stringify({ message: "three" })]);
148
+ assert.match(result.data?.path ?? "", /session-history\.jsonl$/);
149
+ }
150
+ finally {
151
+ await rm(cwd, { recursive: true, force: true });
152
+ }
153
+ });
154
+ it("requires explicit mutation opt-in before queuing prompts", async () => {
155
+ const result = await hermesSendPrompt({ session_id: "sess-a", prompt: "continue" });
156
+ assert.equal(result.ok, false);
157
+ assert.equal(result.code, "mutation_not_allowed");
158
+ });
159
+ it("queues selected prompts through the audited exec follow-up contract", async () => {
160
+ const result = await hermesSendPrompt({ session_id: "sess-a", prompt: "continue", actor: "hermes-test", allow_mutation: true }, {
161
+ injectExecFollowup: async ({ sessionId, prompt, actor }) => ({
162
+ queued: {
163
+ id: "followup-1",
164
+ session_id: sessionId,
165
+ prompt,
166
+ actor: actor ?? "missing",
167
+ created_at: "2026-05-11T00:00:00.000Z",
168
+ },
169
+ queuePath: "/tmp/queue.json",
170
+ }),
171
+ });
172
+ assert.equal(result.ok, true);
173
+ assert.deepEqual(result.data, {
174
+ followup_id: "followup-1",
175
+ session_id: "sess-a",
176
+ queue_path: "/tmp/queue.json",
177
+ });
178
+ });
179
+ it("starts sessions in tmux worktree mode and requires mutation opt-in", async () => {
180
+ const cwd = await tempWorkspace("omx-hermes-start-");
181
+ try {
182
+ let observed = null;
183
+ const result = await hermesStartSession({ workingDirectory: cwd, prompt: "$ralph fix it", worktreeName: "pkg/demo", allow_mutation: true }, {
184
+ resolveOmxCliEntryPath: () => "/opt/omx/dist/cli/omx.js",
185
+ spawnProcess: ((command, args, options) => {
186
+ observed = { command, args, cwd: options.cwd };
187
+ return { pid: 4242, unref() { } };
188
+ }),
189
+ });
190
+ assert.equal(result.ok, true);
191
+ assert.deepEqual(observed, {
192
+ command: "/opt/omx/dist/cli/omx.js",
193
+ args: ["--tmux", "--worktree=pkg/demo", "$ralph fix it"],
194
+ cwd,
195
+ });
196
+ assert.equal(result.data?.pid, 4242);
197
+ }
198
+ finally {
199
+ await rm(cwd, { recursive: true, force: true });
200
+ }
201
+ });
202
+ it("lists and reads only safe result artifact paths", async () => {
203
+ const cwd = await tempWorkspace("omx-hermes-artifacts-");
204
+ try {
205
+ await mkdir(join(cwd, ".omx", "plans"), { recursive: true });
206
+ await writeFile(join(cwd, ".omx", "plans", "prd-demo.md"), "hello artifact");
207
+ const list = await hermesListArtifacts({ workingDirectory: cwd });
208
+ assert.equal(list.ok, true);
209
+ assert.deepEqual(list.data?.artifacts, [{ path: ".omx/plans/prd-demo.md", bytes: 14 }]);
210
+ const read = await hermesReadArtifact({ workingDirectory: cwd, path: ".omx/plans/prd-demo.md", max_bytes: 5 });
211
+ assert.equal(read.ok, true);
212
+ assert.deepEqual(read.data, { path: ".omx/plans/prd-demo.md", content: "hello", truncated: true });
213
+ const rejected = await hermesReadArtifact({ workingDirectory: cwd, path: "package.json" });
214
+ assert.equal(rejected.ok, false);
215
+ assert.equal(rejected.code, "artifact_outside_safe_roots");
216
+ const traversal = await hermesReadArtifact({ workingDirectory: cwd, path: ".omx/plans/../../package.json" });
217
+ assert.equal(traversal.ok, false);
218
+ assert.equal(traversal.code, "artifact_outside_safe_roots");
219
+ }
220
+ finally {
221
+ await rm(cwd, { recursive: true, force: true });
222
+ }
223
+ });
224
+ it("reads large artifacts with max_bytes truncation and reports stat sizes", async () => {
225
+ const cwd = await tempWorkspace("omx-hermes-large-artifact-");
226
+ try {
227
+ await mkdir(join(cwd, ".omx", "plans"), { recursive: true });
228
+ const content = `${"a".repeat(1024 * 1024)}tail`;
229
+ await writeFile(join(cwd, ".omx", "plans", "large.md"), content);
230
+ const list = await hermesListArtifacts({ workingDirectory: cwd });
231
+ assert.equal(list.ok, true);
232
+ assert.deepEqual(list.data?.artifacts, [{ path: ".omx/plans/large.md", bytes: content.length }]);
233
+ const read = await hermesReadArtifact({ workingDirectory: cwd, path: ".omx/plans/large.md", max_bytes: 8 });
234
+ assert.equal(read.ok, true);
235
+ assert.deepEqual(read.data, { path: ".omx/plans/large.md", content: "aaaaaaaa", truncated: true });
236
+ }
237
+ finally {
238
+ await rm(cwd, { recursive: true, force: true });
239
+ }
240
+ });
241
+ it("reads session-history tails from a bounded suffix", async () => {
242
+ const cwd = await tempWorkspace("omx-hermes-large-tail-");
243
+ try {
244
+ await mkdir(join(cwd, ".omx", "logs"), { recursive: true });
245
+ await writeFile(join(cwd, ".omx", "logs", "session-history.jsonl"), `${"ignored\n".repeat(40_000)}one\ntwo\nthree\n`);
246
+ const result = await hermesReadTail({ workingDirectory: cwd, lines: 2 });
247
+ assert.equal(result.ok, true);
248
+ assert.deepEqual(result.data?.tail, ["two", "three"]);
249
+ }
250
+ finally {
251
+ await rm(cwd, { recursive: true, force: true });
252
+ }
253
+ });
254
+ it("does not list artifacts from safe-root directories symlinked outside the worktree", async () => {
255
+ const cwd = await tempWorkspace("omx-hermes-list-root-symlink-");
256
+ const outside = await tempWorkspace("omx-hermes-list-root-outside-");
257
+ try {
258
+ await mkdir(join(cwd, ".omx"), { recursive: true });
259
+ await mkdir(outside, { recursive: true });
260
+ await writeFile(join(outside, "secret.md"), "outside artifact");
261
+ await symlink(outside, join(cwd, ".omx", "plans"));
262
+ const result = await hermesListArtifacts({ workingDirectory: cwd });
263
+ assert.equal(result.ok, true);
264
+ assert.deepEqual(result.data?.artifacts, []);
265
+ }
266
+ finally {
267
+ await rm(cwd, { recursive: true, force: true });
268
+ await rm(outside, { recursive: true, force: true });
269
+ }
270
+ });
271
+ it("rejects session-history tails symlinked outside the worktree", async () => {
272
+ const cwd = await tempWorkspace("omx-hermes-tail-symlink-");
273
+ const outside = await tempWorkspace("omx-hermes-tail-outside-");
274
+ try {
275
+ await mkdir(join(cwd, ".omx", "logs"), { recursive: true });
276
+ const outsideLog = join(outside, "session-history.jsonl");
277
+ await writeFile(outsideLog, "secret\n");
278
+ await symlink(outsideLog, join(cwd, ".omx", "logs", "session-history.jsonl"));
279
+ const result = await hermesReadTail({ workingDirectory: cwd, lines: 1 });
280
+ assert.equal(result.ok, false);
281
+ assert.equal(result.code, "invalid_input");
282
+ assert.match(result.error ?? "", /outside working directory/);
283
+ }
284
+ finally {
285
+ await rm(cwd, { recursive: true, force: true });
286
+ await rm(outside, { recursive: true, force: true });
287
+ }
288
+ });
289
+ it("rejects safe-root artifact symlinks that resolve outside the worktree", async () => {
290
+ const cwd = await tempWorkspace("omx-hermes-artifact-symlink-");
291
+ const outside = await mkdtemp(join(tmpdir(), "omx-hermes-artifact-outside-"));
292
+ try {
293
+ await mkdir(join(cwd, ".omx", "plans"), { recursive: true });
294
+ const outsideFile = join(outside, "host.md");
295
+ await writeFile(outsideFile, "outside artifact");
296
+ await symlink(outsideFile, join(cwd, ".omx", "plans", "host.md"));
297
+ const result = await hermesReadArtifact({ workingDirectory: cwd, path: ".omx/plans/host.md" });
298
+ assert.equal(result.ok, false);
299
+ assert.equal(result.code, "artifact_outside_safe_roots");
300
+ }
301
+ finally {
302
+ await rm(cwd, { recursive: true, force: true });
303
+ await rm(outside, { recursive: true, force: true });
304
+ }
305
+ });
306
+ it("rejects workdir-root candidate symlinks that would expose outside artifacts", async () => {
307
+ const allowed = await tempWorkspace("omx-hermes-allowed-root-");
308
+ const outside = await tempWorkspace("omx-hermes-outside-root-");
309
+ try {
310
+ await mkdir(join(outside, ".omx", "plans"), { recursive: true });
311
+ await writeFile(join(outside, ".omx", "plans", "secret.md"), "outside via workdir symlink");
312
+ await symlink(outside, join(allowed, "link"));
313
+ process.env.OMX_MCP_WORKDIR_ROOTS = allowed;
314
+ const result = await hermesReadArtifact({
315
+ workingDirectory: join(allowed, "link"),
316
+ path: ".omx/plans/secret.md",
317
+ });
318
+ assert.equal(result.ok, false);
319
+ assert.equal(result.code, "invalid_input");
320
+ assert.match(result.error ?? "", /outside allowed roots/);
321
+ }
322
+ finally {
323
+ await rm(allowed, { recursive: true, force: true });
324
+ await rm(outside, { recursive: true, force: true });
325
+ }
326
+ });
327
+ it("rejects symlinked OMX_MCP_WORKDIR_ROOTS entries before reading artifacts", async () => {
328
+ const intended = await tempWorkspace("omx-hermes-intended-root-");
329
+ const outside = await tempWorkspace("omx-hermes-outside-root-");
330
+ try {
331
+ await mkdir(join(outside, ".omx", "plans"), { recursive: true });
332
+ await writeFile(join(outside, ".omx", "plans", "secret.md"), "outside via symlinked root");
333
+ const symlinkedRoot = join(intended, "allowed-link");
334
+ await symlink(outside, symlinkedRoot);
335
+ process.env.OMX_MCP_WORKDIR_ROOTS = symlinkedRoot;
336
+ const result = await hermesReadArtifact({
337
+ workingDirectory: symlinkedRoot,
338
+ path: ".omx/plans/secret.md",
339
+ });
340
+ assert.equal(result.ok, false);
341
+ assert.equal(result.code, "invalid_input");
342
+ assert.match(result.error ?? "", /resolves through a symlink/);
343
+ }
344
+ finally {
345
+ await rm(intended, { recursive: true, force: true });
346
+ await rm(outside, { recursive: true, force: true });
347
+ }
348
+ });
349
+ it("writes a bounded Hermes coordination report", async () => {
350
+ const cwd = await tempWorkspace("omx-hermes-report-");
351
+ try {
352
+ const result = await hermesReportStatus({
353
+ workingDirectory: cwd,
354
+ session_id: "sess-a",
355
+ status: "complete",
356
+ summary: "PR opened",
357
+ pr_url: "https://github.com/Yeachan-Heo/oh-my-codex/pull/1",
358
+ allow_mutation: true,
359
+ }, { now: () => new Date("2026-05-11T00:00:00.000Z") });
360
+ assert.equal(result.ok, true);
361
+ assert.match(result.data?.path ?? "", /sessions[/\\]sess-a[/\\]hermes-coordination\.json$/);
362
+ assert.deepEqual(result.data?.report, {
363
+ status: "complete",
364
+ updated_at: "2026-05-11T00:00:00.000Z",
365
+ summary: "PR opened",
366
+ pr_url: "https://github.com/Yeachan-Heo/oh-my-codex/pull/1",
367
+ });
368
+ }
369
+ finally {
370
+ await rm(cwd, { recursive: true, force: true });
371
+ }
372
+ });
373
+ });
374
+ //# sourceMappingURL=hermes-bridge.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hermes-bridge.test.js","sourceRoot":"","sources":["../../../src/mcp/__tests__/hermes-bridge.test.ts"],"names":[],"mappings":"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"}
@@ -1,10 +1,36 @@
1
- import { describe, it } from 'node:test';
1
+ import { afterEach, beforeEach, describe, it } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
- import { mkdir, mkdtemp, rm, writeFile } from 'fs/promises';
3
+ import { mkdir, mkdtemp, realpath, rm, symlink, writeFile } from 'fs/promises';
4
4
  import { existsSync } from 'fs';
5
5
  import { tmpdir } from 'os';
6
6
  import { join, resolve as resolvePath } from 'path';
7
7
  import { getAllScopedStateDirs, getAllScopedStatePaths, getBaseStateDir, getAllSessionScopedStateDirs, getAllSessionScopedStatePaths, getReadScopedStateFilePaths, readCurrentSessionId, resolveWorkingDirectoryForState, getStateDir, getStateFilePath, getStatePath, validateStateFileName, validateStateModeSegment, validateSessionId, } from '../state-paths.js';
8
+ const isolatedEnvKeys = [
9
+ 'OMX_MCP_WORKDIR_ROOTS',
10
+ 'OMX_ROOT',
11
+ 'OMX_STATE_ROOT',
12
+ 'OMX_TEAM_STATE_ROOT',
13
+ 'OMX_SESSION_ID',
14
+ 'CODEX_SESSION_ID',
15
+ 'SESSION_ID',
16
+ ];
17
+ const originalEnv = Object.fromEntries(isolatedEnvKeys.map((key) => [key, process.env[key]]));
18
+ beforeEach(() => {
19
+ for (const key of isolatedEnvKeys)
20
+ delete process.env[key];
21
+ });
22
+ afterEach(() => {
23
+ for (const key of isolatedEnvKeys) {
24
+ const value = originalEnv[key];
25
+ if (typeof value === 'string')
26
+ process.env[key] = value;
27
+ else
28
+ delete process.env[key];
29
+ }
30
+ });
31
+ async function mkRealTemp(prefix) {
32
+ return await realpath(await mkdtemp(join(await realpath(tmpdir()), prefix)));
33
+ }
8
34
  describe('validateSessionId', () => {
9
35
  it('accepts undefined and valid ids', () => {
10
36
  assert.equal(validateSessionId(undefined), undefined);
@@ -118,8 +144,8 @@ describe('state paths', () => {
118
144
  assert.throws(() => resolveWorkingDirectoryForState('bad\0path'), /NUL byte/);
119
145
  });
120
146
  it('enforces OMX_MCP_WORKDIR_ROOTS allowlist when configured', async () => {
121
- const allowedRoot = await mkdtemp(join(tmpdir(), 'omx-allowed-root-'));
122
- const disallowedRoot = await mkdtemp(join(tmpdir(), 'omx-disallowed-root-'));
147
+ const allowedRoot = await mkRealTemp('omx-allowed-root-');
148
+ const disallowedRoot = await mkRealTemp('omx-disallowed-root-');
123
149
  const prev = process.env.OMX_MCP_WORKDIR_ROOTS;
124
150
  process.env.OMX_MCP_WORKDIR_ROOTS = allowedRoot;
125
151
  try {
@@ -135,6 +161,63 @@ describe('state paths', () => {
135
161
  await rm(disallowedRoot, { recursive: true, force: true });
136
162
  }
137
163
  });
164
+ it('preserves symlinked workingDirectory spelling when no allowlist is configured', async () => {
165
+ const realRoot = await mkRealTemp('omx-real-root-');
166
+ const linkParent = await mkRealTemp('omx-link-parent-');
167
+ const link = join(linkParent, 'workspace-link');
168
+ const prev = process.env.OMX_MCP_WORKDIR_ROOTS;
169
+ delete process.env.OMX_MCP_WORKDIR_ROOTS;
170
+ try {
171
+ await symlink(realRoot, link);
172
+ assert.equal(resolveWorkingDirectoryForState(link), link);
173
+ }
174
+ finally {
175
+ if (typeof prev === 'string')
176
+ process.env.OMX_MCP_WORKDIR_ROOTS = prev;
177
+ else
178
+ delete process.env.OMX_MCP_WORKDIR_ROOTS;
179
+ await rm(realRoot, { recursive: true, force: true });
180
+ await rm(linkParent, { recursive: true, force: true });
181
+ }
182
+ });
183
+ it('rejects symlinked workingDirectory candidates that escape OMX_MCP_WORKDIR_ROOTS', async () => {
184
+ const allowedRoot = await mkRealTemp('omx-allowed-root-');
185
+ const outsideRoot = await mkRealTemp('omx-outside-root-');
186
+ const prev = process.env.OMX_MCP_WORKDIR_ROOTS;
187
+ process.env.OMX_MCP_WORKDIR_ROOTS = allowedRoot;
188
+ try {
189
+ const link = join(allowedRoot, 'link');
190
+ await symlink(outsideRoot, link);
191
+ assert.throws(() => resolveWorkingDirectoryForState(link), /outside allowed roots \(OMX_MCP_WORKDIR_ROOTS\)/);
192
+ }
193
+ finally {
194
+ if (typeof prev === 'string')
195
+ process.env.OMX_MCP_WORKDIR_ROOTS = prev;
196
+ else
197
+ delete process.env.OMX_MCP_WORKDIR_ROOTS;
198
+ await rm(allowedRoot, { recursive: true, force: true });
199
+ await rm(outsideRoot, { recursive: true, force: true });
200
+ }
201
+ });
202
+ it('rejects symlinked OMX_MCP_WORKDIR_ROOTS entries instead of treating their targets as allowed roots', async () => {
203
+ const intendedRoot = await mkRealTemp('omx-intended-root-');
204
+ const outsideRoot = await mkRealTemp('omx-outside-root-');
205
+ const prev = process.env.OMX_MCP_WORKDIR_ROOTS;
206
+ const symlinkedRoot = join(intendedRoot, 'allowed-link');
207
+ process.env.OMX_MCP_WORKDIR_ROOTS = symlinkedRoot;
208
+ try {
209
+ await symlink(outsideRoot, symlinkedRoot);
210
+ assert.throws(() => resolveWorkingDirectoryForState(symlinkedRoot), /OMX_MCP_WORKDIR_ROOTS root .* resolves through a symlink/);
211
+ }
212
+ finally {
213
+ if (typeof prev === 'string')
214
+ process.env.OMX_MCP_WORKDIR_ROOTS = prev;
215
+ else
216
+ delete process.env.OMX_MCP_WORKDIR_ROOTS;
217
+ await rm(intendedRoot, { recursive: true, force: true });
218
+ await rm(outsideRoot, { recursive: true, force: true });
219
+ }
220
+ });
138
221
  it('builds global state paths', () => {
139
222
  const base = getBaseStateDir('/repo');
140
223
  assert.equal(base, '/repo/.omx/state');
@@ -150,7 +233,7 @@ describe('state paths', () => {
150
233
  assert.throws(() => getStatePath('../../etc/passwd', '/repo'), /must not contain "\.\."/);
151
234
  });
152
235
  it('enumerates global-only path', async () => {
153
- const wd = await mkdtemp(join(tmpdir(), 'omx-state-paths-'));
236
+ const wd = await mkRealTemp('omx-state-paths-');
154
237
  try {
155
238
  const paths = await getAllScopedStatePaths('team', wd);
156
239
  assert.deepEqual(paths, [getStatePath('team', wd)]);
@@ -160,7 +243,7 @@ describe('state paths', () => {
160
243
  }
161
244
  });
162
245
  it('enumerates session-scoped paths', async () => {
163
- const wd = await mkdtemp(join(tmpdir(), 'omx-state-paths-'));
246
+ const wd = await mkRealTemp('omx-state-paths-');
164
247
  try {
165
248
  const sessionsRoot = join(getBaseStateDir(wd), 'sessions');
166
249
  await mkdir(join(sessionsRoot, 'sess1'), { recursive: true });
@@ -176,7 +259,7 @@ describe('state paths', () => {
176
259
  }
177
260
  });
178
261
  it('enumerates state directories across all scopes', async () => {
179
- const wd = await mkdtemp(join(tmpdir(), 'omx-state-paths-'));
262
+ const wd = await mkRealTemp('omx-state-paths-');
180
263
  try {
181
264
  const sessionsRoot = join(getBaseStateDir(wd), 'sessions');
182
265
  await mkdir(join(sessionsRoot, 'sess1'), { recursive: true });
@@ -191,7 +274,7 @@ describe('state paths', () => {
191
274
  }
192
275
  });
193
276
  it('enumerates global and session-scoped paths together', async () => {
194
- const wd = await mkdtemp(join(tmpdir(), 'omx-state-paths-'));
277
+ const wd = await mkRealTemp('omx-state-paths-');
195
278
  try {
196
279
  const sessionsRoot = join(getBaseStateDir(wd), 'sessions');
197
280
  await mkdir(join(sessionsRoot, 'sess1'), { recursive: true });
@@ -208,7 +291,7 @@ describe('state paths', () => {
208
291
  }
209
292
  });
210
293
  it('ignores invalid session directory names', async () => {
211
- const wd = await mkdtemp(join(tmpdir(), 'omx-state-paths-'));
294
+ const wd = await mkRealTemp('omx-state-paths-');
212
295
  try {
213
296
  const sessionsRoot = join(getBaseStateDir(wd), 'sessions');
214
297
  await mkdir(join(sessionsRoot, 'valid-session'), { recursive: true });
@@ -222,7 +305,7 @@ describe('state paths', () => {
222
305
  }
223
306
  });
224
307
  it('reads session-sensitive runtime files from the current session without root fallback when requested', async () => {
225
- const wd = await mkdtemp(join(tmpdir(), 'omx-state-paths-'));
308
+ const wd = await mkRealTemp('omx-state-paths-');
226
309
  try {
227
310
  const stateDir = getBaseStateDir(wd);
228
311
  await mkdir(join(stateDir, 'sessions', 'sess-current'), { recursive: true });
@@ -238,7 +321,7 @@ describe('state paths', () => {
238
321
  }
239
322
  });
240
323
  it('prefers OMX_SESSION_ID over stale session.json when resolving current session id', async () => {
241
- const wd = await mkdtemp(join(tmpdir(), 'omx-state-paths-'));
324
+ const wd = await mkRealTemp('omx-state-paths-');
242
325
  const previousSessionId = process.env.OMX_SESSION_ID;
243
326
  try {
244
327
  const stateDir = getBaseStateDir(wd);
@@ -260,7 +343,7 @@ describe('state paths', () => {
260
343
  }
261
344
  });
262
345
  it('resolves current session from authoritative team state root without OMX_SESSION_ID', async () => {
263
- const wd = await mkdtemp(join(tmpdir(), 'omx-state-paths-team-root-session-'));
346
+ const wd = await mkRealTemp('omx-state-paths-team-root-session-');
264
347
  const teamStateRoot = join(wd, 'team-state-root');
265
348
  const previousTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
266
349
  const previousSessionId = process.env.OMX_SESSION_ID;
@@ -292,7 +375,7 @@ describe('state paths', () => {
292
375
  }
293
376
  });
294
377
  it('does not resolve current session from source root when a team state root is authoritative', async () => {
295
- const wd = await mkdtemp(join(tmpdir(), 'omx-state-paths-ignore-source-session-'));
378
+ const wd = await mkRealTemp('omx-state-paths-ignore-source-session-');
296
379
  const teamStateRoot = join(wd, 'team-state-root');
297
380
  const previousTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
298
381
  const previousSessionId = process.env.OMX_SESSION_ID;