@danielblomma/cortex-mcp 1.7.2 → 2.0.3

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 (79) hide show
  1. package/README.md +4 -24
  2. package/bin/cortex.mjs +679 -32
  3. package/bin/style.mjs +349 -0
  4. package/package.json +4 -3
  5. package/scaffold/mcp/src/cli/enterprise-setup.ts +124 -0
  6. package/scaffold/mcp/src/cli/govern.ts +987 -0
  7. package/scaffold/mcp/src/cli/run.ts +306 -0
  8. package/scaffold/mcp/src/cli/telemetry-test.ts +158 -0
  9. package/scaffold/mcp/src/cli/ungoverned-detector.ts +168 -0
  10. package/scaffold/mcp/src/core/audit/query.ts +81 -0
  11. package/scaffold/mcp/src/core/audit/writer.ts +68 -0
  12. package/scaffold/mcp/src/core/config.ts +329 -0
  13. package/scaffold/mcp/src/core/index.ts +34 -0
  14. package/scaffold/mcp/src/core/license.ts +202 -0
  15. package/scaffold/mcp/src/core/policy/enforce.ts +98 -0
  16. package/scaffold/mcp/src/core/policy/injection.ts +229 -0
  17. package/scaffold/mcp/src/core/policy/store.ts +197 -0
  18. package/scaffold/mcp/src/core/rbac/check.ts +40 -0
  19. package/scaffold/mcp/src/core/telemetry/collector.ts +408 -0
  20. package/scaffold/mcp/src/core/validators/builtins.ts +711 -0
  21. package/scaffold/mcp/src/core/validators/config.ts +47 -0
  22. package/scaffold/mcp/src/core/validators/engine.ts +199 -0
  23. package/scaffold/mcp/src/core/validators/evaluators/code_comments.ts +294 -0
  24. package/scaffold/mcp/src/core/validators/evaluators/regex.ts +144 -0
  25. package/scaffold/mcp/src/daemon/client.ts +155 -0
  26. package/scaffold/mcp/src/daemon/egress-proxy.ts +331 -0
  27. package/scaffold/mcp/src/daemon/heartbeat-pusher.ts +147 -0
  28. package/scaffold/mcp/src/daemon/heartbeat-tracker.ts +223 -0
  29. package/scaffold/mcp/src/daemon/host-events-pusher.ts +285 -0
  30. package/scaffold/mcp/src/daemon/main.ts +435 -0
  31. package/scaffold/mcp/src/daemon/paths.ts +41 -0
  32. package/scaffold/mcp/src/daemon/protocol.ts +101 -0
  33. package/scaffold/mcp/src/daemon/server.ts +227 -0
  34. package/scaffold/mcp/src/daemon/sync-checker.ts +213 -0
  35. package/scaffold/mcp/src/daemon/ungoverned-scanner.ts +149 -0
  36. package/scaffold/mcp/src/enterprise/audit/push.ts +84 -0
  37. package/scaffold/mcp/src/enterprise/index.ts +386 -0
  38. package/scaffold/mcp/src/enterprise/model/deploy.ts +33 -0
  39. package/scaffold/mcp/src/enterprise/policy/sync.ts +146 -0
  40. package/scaffold/mcp/src/enterprise/privacy/boundary.ts +214 -0
  41. package/scaffold/mcp/src/enterprise/reviews/push.ts +79 -0
  42. package/scaffold/mcp/src/enterprise/telemetry/sync.ts +73 -0
  43. package/scaffold/mcp/src/enterprise/tools/enterprise.ts +1031 -0
  44. package/scaffold/mcp/src/enterprise/tools/walk.ts +79 -0
  45. package/scaffold/mcp/src/enterprise/violations/push.ts +102 -0
  46. package/scaffold/mcp/src/enterprise/workflow/push.ts +60 -0
  47. package/scaffold/mcp/src/enterprise/workflow/state.ts +535 -0
  48. package/scaffold/mcp/src/hooks/pre-compact.ts +54 -0
  49. package/scaffold/mcp/src/hooks/pre-tool-use.ts +96 -0
  50. package/scaffold/mcp/src/hooks/session-end.ts +73 -0
  51. package/scaffold/mcp/src/hooks/session-start.ts +78 -0
  52. package/scaffold/mcp/src/hooks/shared.ts +134 -0
  53. package/scaffold/mcp/src/hooks/stop.ts +60 -0
  54. package/scaffold/mcp/src/hooks/user-prompt-submit.ts +64 -0
  55. package/scaffold/mcp/src/loadGraph.ts +2 -0
  56. package/scaffold/mcp/src/plugin.ts +150 -0
  57. package/scaffold/mcp/src/server.ts +218 -7
  58. package/scaffold/mcp/tests/copilot-shim.test.mjs +146 -0
  59. package/scaffold/mcp/tests/daemon-client.test.mjs +32 -0
  60. package/scaffold/mcp/tests/egress-proxy.test.mjs +239 -0
  61. package/scaffold/mcp/tests/enterprise-config.test.mjs +154 -0
  62. package/scaffold/mcp/tests/govern-install.test.mjs +320 -0
  63. package/scaffold/mcp/tests/govern-repair.test.mjs +157 -0
  64. package/scaffold/mcp/tests/govern-status.test.mjs +538 -0
  65. package/scaffold/mcp/tests/govern.test.mjs +74 -0
  66. package/scaffold/mcp/tests/heartbeat-pusher.test.mjs +154 -0
  67. package/scaffold/mcp/tests/heartbeat-tracker.test.mjs +237 -0
  68. package/scaffold/mcp/tests/host-events-pusher.test.mjs +347 -0
  69. package/scaffold/mcp/tests/policy-check.test.mjs +220 -0
  70. package/scaffold/mcp/tests/repo-name.test.mjs +134 -0
  71. package/scaffold/mcp/tests/run.test.mjs +109 -0
  72. package/scaffold/mcp/tests/sync-checker.test.mjs +188 -0
  73. package/scaffold/mcp/tests/telemetry-collector.test.mjs +30 -0
  74. package/scaffold/mcp/tests/ungoverned-detector.test.mjs +191 -0
  75. package/scaffold/mcp/tests/ungoverned-scanner.test.mjs +198 -0
  76. package/scaffold/scripts/bootstrap.sh +0 -11
  77. package/scaffold/scripts/doctor.sh +24 -4
  78. package/types.js +5 -0
  79. package/docs/MCP_MARKETPLACE.md +0 -160
@@ -0,0 +1,538 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+
7
+ import {
8
+ buildGovernStatus,
9
+ runGovernStatus,
10
+ runGovernSync,
11
+ } from "../dist/cli/govern.js";
12
+ import { writeTamperLock } from "../dist/daemon/heartbeat-tracker.js";
13
+
14
+ function makeProject({ frameworks = ["iso27001"], apiKey = "ent_test_12345678" } = {}) {
15
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-status-"));
16
+ const ctx = path.join(root, ".context");
17
+ fs.mkdirSync(ctx, { recursive: true });
18
+ fs.writeFileSync(
19
+ path.join(ctx, "enterprise.yml"),
20
+ [
21
+ "enterprise:",
22
+ ` api_key: ${apiKey}`,
23
+ " base_url: https://example.com",
24
+ "compliance:",
25
+ ` frameworks: [${frameworks.join(", ")}]`,
26
+ "",
27
+ ].join("\n"),
28
+ );
29
+ return { root, ctx };
30
+ }
31
+
32
+ function writeInstalls(ctx, installs) {
33
+ fs.writeFileSync(path.join(ctx, "govern.local.json"), JSON.stringify({ installs }));
34
+ }
35
+
36
+ test("buildGovernStatus: empty workspace yields off mode and empty installs", () => {
37
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-status-bare-"));
38
+ fs.mkdirSync(path.join(root, ".context"));
39
+ try {
40
+ const report = buildGovernStatus({ cwd: root });
41
+ assert.equal(report.mode_effective, "off");
42
+ assert.equal(report.installs.length, 0);
43
+ assert.equal(report.tamper_lock, null);
44
+ assert.equal(report.update_notification, null);
45
+ assert.equal(report.recent_events_24h.ungoverned_ai_session_detected, 0);
46
+ } finally {
47
+ fs.rmSync(root, { recursive: true, force: true });
48
+ }
49
+ });
50
+
51
+ test("buildGovernStatus: counts deny rules from claude managed-settings JSON", () => {
52
+ const { root, ctx } = makeProject();
53
+ const managedPath = path.join(ctx, "managed-settings.json");
54
+ fs.writeFileSync(
55
+ managedPath,
56
+ JSON.stringify({
57
+ allowManagedHooksOnly: true,
58
+ permissions: {
59
+ deny: ["Bash(rm)", "Edit(~/.x)", "Bash(curl *)"],
60
+ },
61
+ }),
62
+ );
63
+ writeInstalls(ctx, {
64
+ claude: {
65
+ path: managedPath,
66
+ version: "v1",
67
+ frameworks: [{ id: "iso27001", version: "0.1" }],
68
+ installed_at: new Date().toISOString(),
69
+ mode: "advisory",
70
+ },
71
+ });
72
+ try {
73
+ const report = buildGovernStatus({ cwd: root });
74
+ assert.equal(report.installs.length, 1);
75
+ const claudeEntry = report.installs[0];
76
+ assert.equal(claudeEntry.cli, "claude");
77
+ assert.equal(claudeEntry.tier, "Tier 1 (Prevent)");
78
+ assert.equal(claudeEntry.deny_rules_count, 3);
79
+ assert.equal(claudeEntry.managed_path_present, true);
80
+ assert.equal(claudeEntry.managed_path_kind, "managed-settings.json");
81
+ } finally {
82
+ fs.rmSync(root, { recursive: true, force: true });
83
+ }
84
+ });
85
+
86
+ test("buildGovernStatus: counts deny_read entries from codex requirements.toml", () => {
87
+ const { root, ctx } = makeProject();
88
+ const reqPath = path.join(ctx, "requirements.toml");
89
+ fs.writeFileSync(
90
+ reqPath,
91
+ [
92
+ 'allowed_sandbox_modes = ["read-only", "workspace-write"]',
93
+ "[permissions.filesystem]",
94
+ 'deny_read = ["~/.codex/config.toml", "/etc/secret"]',
95
+ "",
96
+ ].join("\n"),
97
+ );
98
+ writeInstalls(ctx, {
99
+ codex: {
100
+ path: reqPath,
101
+ version: "v1",
102
+ frameworks: [{ id: "iso27001", version: "0.1" }],
103
+ installed_at: new Date().toISOString(),
104
+ mode: "enforced",
105
+ },
106
+ });
107
+ try {
108
+ const report = buildGovernStatus({ cwd: root });
109
+ assert.equal(report.installs[0].deny_rules_count, 2);
110
+ assert.equal(report.installs[0].managed_path_kind, "requirements.toml");
111
+ assert.equal(report.mode_effective, "enforced");
112
+ } finally {
113
+ fs.rmSync(root, { recursive: true, force: true });
114
+ }
115
+ });
116
+
117
+ test("buildGovernStatus: extracts shim real-binary path from copilot shim", () => {
118
+ const { root, ctx } = makeProject();
119
+ const shimPath = path.join(ctx, "fake-shim");
120
+ fs.writeFileSync(
121
+ shimPath,
122
+ [
123
+ "#!/bin/sh",
124
+ "# cortex-shim-v1",
125
+ "# Real binary captured at install time: /opt/homebrew/bin/copilot",
126
+ 'exec "$CORTEX" run copilot "$@"',
127
+ "",
128
+ ].join("\n"),
129
+ );
130
+ writeInstalls(ctx, {
131
+ copilot: {
132
+ path: shimPath,
133
+ version: "shim-v1",
134
+ frameworks: [],
135
+ installed_at: new Date().toISOString(),
136
+ mode: "advisory",
137
+ },
138
+ });
139
+ try {
140
+ const report = buildGovernStatus({ cwd: root });
141
+ const c = report.installs[0];
142
+ assert.equal(c.cli, "copilot");
143
+ assert.equal(c.tier, "Tier 2 (Wrap)");
144
+ assert.equal(c.shim_real_binary, "/opt/homebrew/bin/copilot");
145
+ assert.equal(c.managed_path_kind, "shim");
146
+ } finally {
147
+ fs.rmSync(root, { recursive: true, force: true });
148
+ }
149
+ });
150
+
151
+ test("buildGovernStatus: surfaces active tamper-lock", () => {
152
+ const { root, ctx } = makeProject();
153
+ writeInstalls(ctx, {
154
+ claude: {
155
+ path: "/missing",
156
+ version: "v1",
157
+ frameworks: [],
158
+ installed_at: new Date().toISOString(),
159
+ mode: "enforced",
160
+ },
161
+ });
162
+ writeTamperLock(root, {
163
+ version: 1,
164
+ detected_at: new Date().toISOString(),
165
+ cli: "claude",
166
+ session_id: "sess-x",
167
+ hook_name: "any",
168
+ last_seen: new Date(Date.now() - 60_000).toISOString(),
169
+ missing_seconds: 60,
170
+ host_id: "h",
171
+ cwd: root,
172
+ });
173
+ try {
174
+ const report = buildGovernStatus({ cwd: root });
175
+ assert.notEqual(report.tamper_lock, null);
176
+ assert.equal(report.tamper_lock.cli, "claude");
177
+ assert.equal(report.tamper_lock.session_id, "sess-x");
178
+ } finally {
179
+ fs.rmSync(root, { recursive: true, force: true });
180
+ }
181
+ });
182
+
183
+ test("buildGovernStatus: surfaces update notification", () => {
184
+ const { root, ctx } = makeProject({});
185
+ fs.writeFileSync(
186
+ path.join(ctx, ".govern-update-available.json"),
187
+ JSON.stringify({
188
+ cli: "claude",
189
+ latest_version: "newer123",
190
+ current_version: "older",
191
+ detected_at: new Date().toISOString(),
192
+ }),
193
+ );
194
+ try {
195
+ const report = buildGovernStatus({ cwd: root });
196
+ assert.notEqual(report.update_notification, null);
197
+ assert.equal(report.update_notification.latest_version, "newer123");
198
+ } finally {
199
+ fs.rmSync(root, { recursive: true, force: true });
200
+ }
201
+ });
202
+
203
+ test("buildGovernStatus: counts events from host-events JSONL within last 24h", () => {
204
+ const { root, ctx } = makeProject({});
205
+ const auditDir = path.join(ctx, "audit");
206
+ fs.mkdirSync(auditDir);
207
+ const date = new Date().toISOString().slice(0, 10);
208
+ const file = path.join(auditDir, `host-events-${date}.jsonl`);
209
+ const now = Date.now();
210
+ const recent = new Date(now - 60_000).toISOString();
211
+ const old = new Date(now - 48 * 60 * 60 * 1000).toISOString();
212
+ fs.writeFileSync(
213
+ file,
214
+ [
215
+ JSON.stringify({ event_type: "ungoverned_ai_session_detected", timestamp: recent }),
216
+ JSON.stringify({ event_type: "ungoverned_ai_session_detected", timestamp: recent }),
217
+ JSON.stringify({ event_type: "hook_tamper_detected", timestamp: recent }),
218
+ JSON.stringify({ event_type: "govern_config_unchanged", timestamp: recent }),
219
+ // Outside 24h window — should not count.
220
+ JSON.stringify({ event_type: "ungoverned_ai_session_detected", timestamp: old }),
221
+ ].join("\n"),
222
+ );
223
+ try {
224
+ const report = buildGovernStatus({ cwd: root });
225
+ assert.equal(report.recent_events_24h.ungoverned_ai_session_detected, 2);
226
+ assert.equal(report.recent_events_24h.hook_tamper_detected, 1);
227
+ assert.equal(report.recent_events_24h.govern_config_unchanged, 1);
228
+ assert.ok(report.recent_events_sample.length >= 4);
229
+ } finally {
230
+ fs.rmSync(root, { recursive: true, force: true });
231
+ }
232
+ });
233
+
234
+ test("runGovernStatus: --json emits valid JSON containing the report", () => {
235
+ const { root, ctx } = makeProject();
236
+ writeInstalls(ctx, {
237
+ claude: {
238
+ path: "/some-managed",
239
+ version: "v1",
240
+ frameworks: [],
241
+ installed_at: new Date().toISOString(),
242
+ mode: "advisory",
243
+ },
244
+ });
245
+ const lines = [];
246
+ const orig = console.log;
247
+ console.log = (...args) => lines.push(args.join(" "));
248
+ try {
249
+ runGovernStatus({ cwd: root, json: true });
250
+ } finally {
251
+ console.log = orig;
252
+ fs.rmSync(root, { recursive: true, force: true });
253
+ }
254
+ const parsed = JSON.parse(lines.join("\n"));
255
+ assert.equal(parsed.installs.length, 1);
256
+ assert.equal(parsed.installs[0].cli, "claude");
257
+ assert.equal(parsed.mode_effective, "advisory");
258
+ });
259
+
260
+ test("runGovernStatus: compact format mentions tamper lock when present", () => {
261
+ const { root, ctx } = makeProject();
262
+ writeInstalls(ctx, {
263
+ claude: {
264
+ path: "/some-managed",
265
+ version: "v1",
266
+ frameworks: [],
267
+ installed_at: new Date().toISOString(),
268
+ mode: "enforced",
269
+ },
270
+ });
271
+ writeTamperLock(root, {
272
+ version: 1,
273
+ detected_at: new Date().toISOString(),
274
+ cli: "claude",
275
+ session_id: "x",
276
+ hook_name: "any",
277
+ last_seen: new Date().toISOString(),
278
+ missing_seconds: 60,
279
+ host_id: "h",
280
+ cwd: root,
281
+ });
282
+ const lines = [];
283
+ const orig = console.log;
284
+ console.log = (...args) => lines.push(args.join(" "));
285
+ try {
286
+ runGovernStatus({ cwd: root });
287
+ } finally {
288
+ console.log = orig;
289
+ fs.rmSync(root, { recursive: true, force: true });
290
+ }
291
+ const out = lines.join("\n");
292
+ assert.match(out, /TAMPER LOCK ACTIVE/);
293
+ assert.match(out, /sudo cortex enterprise repair/);
294
+ });
295
+
296
+ test("runGovernStatus: --verbose includes per-CLI detail block", () => {
297
+ const { root, ctx } = makeProject({});
298
+ const managedPath = path.join(ctx, "managed-settings.json");
299
+ fs.writeFileSync(managedPath, JSON.stringify({ permissions: { deny: ["Bash(rm)"] } }));
300
+ fs.writeFileSync(
301
+ path.join(ctx, "govern.local.json"),
302
+ JSON.stringify({
303
+ installs: {
304
+ claude: {
305
+ path: managedPath,
306
+ version: "v1",
307
+ frameworks: [{ id: "iso27001", version: "0.1" }],
308
+ installed_at: new Date().toISOString(),
309
+ mode: "advisory",
310
+ },
311
+ },
312
+ }),
313
+ );
314
+ const lines = [];
315
+ const orig = console.log;
316
+ console.log = (...args) => lines.push(args.join(" "));
317
+ try {
318
+ runGovernStatus({ cwd: root, verbose: true });
319
+ } finally {
320
+ console.log = orig;
321
+ fs.rmSync(root, { recursive: true, force: true });
322
+ }
323
+ const out = lines.join("\n");
324
+ assert.match(out, /Per-CLI managed-config detail/);
325
+ assert.match(out, /\[claude\]/);
326
+ assert.match(out, /deny_rules:\s+1/);
327
+ });
328
+
329
+ // --- Regression: M2 — readRecentEvents 24h window must read across files ---
330
+
331
+ test("buildGovernStatus: 24h window finds events across multiple daily files (Fas 8 M2)", () => {
332
+ const { root, ctx } = makeProject({});
333
+ const auditDir = path.join(ctx, "audit");
334
+ fs.mkdirSync(auditDir);
335
+
336
+ const now = new Date();
337
+ const todayStamp = now.toISOString().slice(0, 10);
338
+ const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
339
+ const yesterdayStamp = yesterday.toISOString().slice(0, 10);
340
+ const dayBeforeYesterday = new Date(now.getTime() - 48 * 60 * 60 * 1000);
341
+ const dayBeforeStamp = dayBeforeYesterday.toISOString().slice(0, 10);
342
+
343
+ // Day before yesterday — out of window, must NOT count.
344
+ fs.writeFileSync(
345
+ path.join(auditDir, `host-events-${dayBeforeStamp}.jsonl`),
346
+ JSON.stringify({
347
+ event_type: "ungoverned_ai_session_detected",
348
+ timestamp: new Date(now.getTime() - 47 * 60 * 60 * 1000).toISOString(),
349
+ }) + "\n",
350
+ );
351
+ // Yesterday at exactly ~23h ago — INSIDE the 24h window. The old
352
+ // slice(-2) heuristic happened to read this when files=[d-2, d-1, d],
353
+ // but only because of the daily file granularity; with three files the
354
+ // d-1 file would have been dropped. Three files force the issue.
355
+ fs.writeFileSync(
356
+ path.join(auditDir, `host-events-${yesterdayStamp}.jsonl`),
357
+ [
358
+ JSON.stringify({
359
+ event_type: "hook_tamper_detected",
360
+ timestamp: new Date(now.getTime() - 23 * 60 * 60 * 1000).toISOString(),
361
+ }),
362
+ JSON.stringify({
363
+ event_type: "tamper_repaired",
364
+ timestamp: new Date(now.getTime() - 22 * 60 * 60 * 1000).toISOString(),
365
+ }),
366
+ ].join("\n") + "\n",
367
+ );
368
+ // Today — recent, must count.
369
+ fs.writeFileSync(
370
+ path.join(auditDir, `host-events-${todayStamp}.jsonl`),
371
+ JSON.stringify({
372
+ event_type: "ungoverned_ai_session_detected",
373
+ timestamp: new Date(now.getTime() - 60_000).toISOString(),
374
+ }) + "\n",
375
+ );
376
+ // Force a fourth (older) file too — the OLD code would call slice(-2)
377
+ // on a 4-file list and silently miss yesterday's events. The fix walks
378
+ // every file but breaks early once it hits an entirely-out-of-window
379
+ // file.
380
+ const evenOlderStamp = new Date(now.getTime() - 72 * 60 * 60 * 1000)
381
+ .toISOString()
382
+ .slice(0, 10);
383
+ fs.writeFileSync(
384
+ path.join(auditDir, `host-events-${evenOlderStamp}.jsonl`),
385
+ JSON.stringify({
386
+ event_type: "hook_tamper_detected",
387
+ timestamp: new Date(now.getTime() - 71 * 60 * 60 * 1000).toISOString(),
388
+ }) + "\n",
389
+ );
390
+
391
+ try {
392
+ const report = buildGovernStatus({ cwd: root });
393
+ // Today's ungoverned event.
394
+ assert.equal(report.recent_events_24h.ungoverned_ai_session_detected, 1);
395
+ // Yesterday's tamper events — these are what the slice(-2) bug
396
+ // dropped when there are 4 daily files.
397
+ assert.equal(report.recent_events_24h.hook_tamper_detected, 1);
398
+ assert.equal(report.recent_events_24h.tamper_repaired, 1);
399
+ } finally {
400
+ fs.rmSync(root, { recursive: true, force: true });
401
+ }
402
+ });
403
+
404
+ // --- Regression: M7 — unknown CLI keys must not crash the install walk ---
405
+
406
+ test("buildGovernStatus: tolerates unknown CLI keys in govern.local.json (Fas 8 M7)", () => {
407
+ const { root, ctx } = makeProject({});
408
+ const managedPath = path.join(ctx, "managed-settings.json");
409
+ fs.writeFileSync(managedPath, JSON.stringify({ permissions: { deny: ["Bash(rm)"] } }));
410
+ fs.writeFileSync(
411
+ path.join(ctx, "govern.local.json"),
412
+ JSON.stringify({
413
+ installs: {
414
+ // Known CLI: should still be processed.
415
+ claude: {
416
+ path: managedPath,
417
+ version: "v1",
418
+ frameworks: [],
419
+ installed_at: new Date().toISOString(),
420
+ mode: "advisory",
421
+ },
422
+ // Unknown forward-compatible CLI: must be ignored without crashing.
423
+ gemini: {
424
+ path: "/some/path",
425
+ version: "v1",
426
+ frameworks: [],
427
+ installed_at: new Date().toISOString(),
428
+ mode: "enforced",
429
+ },
430
+ // Garbage key: must also be ignored.
431
+ "": {
432
+ path: "/x",
433
+ version: "v1",
434
+ frameworks: [],
435
+ installed_at: new Date().toISOString(),
436
+ mode: "advisory",
437
+ },
438
+ },
439
+ }),
440
+ );
441
+ try {
442
+ const report = buildGovernStatus({ cwd: root });
443
+ assert.equal(report.installs.length, 1);
444
+ assert.equal(report.installs[0].cli, "claude");
445
+ // The unknown 'enforced' CLI must NOT bump mode_effective to enforced.
446
+ assert.equal(report.mode_effective, "advisory");
447
+ } finally {
448
+ fs.rmSync(root, { recursive: true, force: true });
449
+ }
450
+ });
451
+
452
+ // --- L2: failure-path tests for buildGovernStatus ---
453
+
454
+ test("buildGovernStatus: corrupt govern.local.json yields off mode without throwing (Fas 8 L2)", () => {
455
+ const { root, ctx } = makeProject({});
456
+ fs.writeFileSync(path.join(ctx, "govern.local.json"), "{ this is: not, json:: ::: ");
457
+ try {
458
+ const report = buildGovernStatus({ cwd: root });
459
+ assert.equal(report.installs.length, 0);
460
+ assert.equal(report.mode_effective, "off");
461
+ } finally {
462
+ fs.rmSync(root, { recursive: true, force: true });
463
+ }
464
+ });
465
+
466
+ test("buildGovernStatus: corrupt managed-settings.json yields zero deny rules (Fas 8 L2)", () => {
467
+ const { root, ctx } = makeProject({});
468
+ const managedPath = path.join(ctx, "managed-settings.json");
469
+ fs.writeFileSync(managedPath, "{ not valid json");
470
+ fs.writeFileSync(
471
+ path.join(ctx, "govern.local.json"),
472
+ JSON.stringify({
473
+ installs: {
474
+ claude: {
475
+ path: managedPath,
476
+ version: "v1",
477
+ frameworks: [],
478
+ installed_at: new Date().toISOString(),
479
+ mode: "advisory",
480
+ },
481
+ },
482
+ }),
483
+ );
484
+ try {
485
+ const report = buildGovernStatus({ cwd: root });
486
+ assert.equal(report.installs.length, 1);
487
+ // Corrupt JSON → deny_rules_count is null (couldn't parse), but the
488
+ // status build itself must succeed.
489
+ assert.equal(report.installs[0].deny_rules_count, null);
490
+ assert.equal(report.installs[0].managed_path_present, true);
491
+ } finally {
492
+ fs.rmSync(root, { recursive: true, force: true });
493
+ }
494
+ });
495
+
496
+ test("buildGovernStatus: missing audit/ dir yields zero counts (Fas 8 L2)", () => {
497
+ const { root, ctx } = makeProject({});
498
+ // No audit/ directory at all.
499
+ assert.equal(fs.existsSync(path.join(ctx, "audit")), false);
500
+ try {
501
+ const report = buildGovernStatus({ cwd: root });
502
+ assert.equal(report.recent_events_24h.ungoverned_ai_session_detected, 0);
503
+ assert.equal(report.recent_events_24h.hook_tamper_detected, 0);
504
+ assert.equal(report.recent_events_sample.length, 0);
505
+ } finally {
506
+ fs.rmSync(root, { recursive: true, force: true });
507
+ }
508
+ });
509
+
510
+ test("runGovernSync: silently skips unknown CLI keys (Fas 8 M7)", async () => {
511
+ const { root, ctx } = makeProject({});
512
+ fs.writeFileSync(
513
+ path.join(ctx, "govern.local.json"),
514
+ JSON.stringify({
515
+ installs: {
516
+ gemini: {
517
+ path: "/x",
518
+ version: "v1",
519
+ frameworks: [],
520
+ installed_at: new Date().toISOString(),
521
+ mode: "advisory",
522
+ },
523
+ },
524
+ }),
525
+ );
526
+ const lines = [];
527
+ const orig = console.log;
528
+ console.log = (...args) => lines.push(args.join(" "));
529
+ try {
530
+ await runGovernSync({ cwd: root });
531
+ } finally {
532
+ console.log = orig;
533
+ fs.rmSync(root, { recursive: true, force: true });
534
+ }
535
+ // No targets remain after filtering; expect the empty-state message.
536
+ const out = lines.join("\n");
537
+ assert.match(out, /Nothing to sync/);
538
+ });
@@ -0,0 +1,74 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+
4
+ import { getManagedSettingsPath, buildCodexRequirementsToml } from "../dist/cli/govern.js";
5
+
6
+ test("getManagedSettingsPath: claude on darwin returns macOS managed-settings path", () => {
7
+ const p = getManagedSettingsPath("claude", "darwin");
8
+ assert.equal(p, "/Library/Application Support/ClaudeCode/managed-settings.json");
9
+ });
10
+
11
+ test("getManagedSettingsPath: claude on linux returns /etc path", () => {
12
+ const p = getManagedSettingsPath("claude", "linux");
13
+ assert.equal(p, "/etc/claude-code/managed-settings.json");
14
+ });
15
+
16
+ test("getManagedSettingsPath: codex on darwin returns macOS requirements.toml path", () => {
17
+ const p = getManagedSettingsPath("codex", "darwin");
18
+ assert.equal(p, "/Library/Application Support/Codex/requirements.toml");
19
+ });
20
+
21
+ test("getManagedSettingsPath: codex on linux returns /etc/codex path", () => {
22
+ const p = getManagedSettingsPath("codex", "linux");
23
+ assert.equal(p, "/etc/codex/requirements.toml");
24
+ });
25
+
26
+ test("getManagedSettingsPath: throws on unsupported OS", () => {
27
+ assert.throws(() => getManagedSettingsPath("claude", "win32"), /not yet supported/);
28
+ });
29
+
30
+ test("getManagedSettingsPath: throws on unsupported cli (copilot has no managed file)", () => {
31
+ assert.throws(() => getManagedSettingsPath("copilot", "darwin"), /not yet supported/);
32
+ });
33
+
34
+ test("buildCodexRequirementsToml: emits sandbox + approval upper bounds", () => {
35
+ const config = {
36
+ cli: "codex",
37
+ managed_settings: {},
38
+ deny_rules: [
39
+ { pattern: "Edit(~/.codex/config.toml)", source_frameworks: ["iso27001"] },
40
+ { pattern: "Bash(curl *)", source_frameworks: ["iso27001"] },
41
+ ],
42
+ tamper_config: { heartbeat_interval_seconds: 60, missing_threshold_seconds: 300 },
43
+ frameworks: [{ id: "iso27001", version: "0.1.0" }],
44
+ };
45
+ const toml = buildCodexRequirementsToml(config);
46
+ assert.match(toml, /allowed_sandbox_modes = \["read-only", "workspace-write"\]/);
47
+ assert.match(toml, /allowed_approval_policies = \["untrusted", "on-request"\]/);
48
+ assert.match(toml, /\[permissions\.filesystem\]/);
49
+ assert.match(toml, /deny_read = \["~\/.codex\/config\.toml"\]/);
50
+ // Bash(...) patterns should not appear in deny_read (filesystem only)
51
+ assert.doesNotMatch(toml, /curl/);
52
+ });
53
+
54
+ test("buildCodexRequirementsToml: empty deny_rules emit empty deny_read", () => {
55
+ const toml = buildCodexRequirementsToml({
56
+ cli: "codex",
57
+ managed_settings: {},
58
+ deny_rules: [],
59
+ tamper_config: { heartbeat_interval_seconds: 60, missing_threshold_seconds: 300 },
60
+ frameworks: [],
61
+ });
62
+ assert.match(toml, /deny_read = \[\]/);
63
+ });
64
+
65
+ test("buildCodexRequirementsToml: escapes quotes in patterns", () => {
66
+ const toml = buildCodexRequirementsToml({
67
+ cli: "codex",
68
+ managed_settings: {},
69
+ deny_rules: [{ pattern: 'Edit(~/.codex/file with "quote".toml)', source_frameworks: ["iso27001"] }],
70
+ tamper_config: { heartbeat_interval_seconds: 60, missing_threshold_seconds: 300 },
71
+ frameworks: [],
72
+ });
73
+ assert.match(toml, /\\"quote\\"/);
74
+ });