@danielblomma/cortex-mcp 2.4.0 → 2.4.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.
Files changed (173) hide show
  1. package/CHANGELOG.md +201 -0
  2. package/README.md +19 -3
  3. package/bin/cortex.mjs +308 -75
  4. package/bin/daemon-control.mjs +162 -0
  5. package/package.json +7 -3
  6. package/scaffold/mcp/dist/.cortex-build-hash +1 -0
  7. package/scaffold/mcp/dist/cli/enterprise-setup.js +134 -0
  8. package/scaffold/mcp/dist/cli/govern.js +937 -0
  9. package/scaffold/mcp/dist/cli/query.js +409 -0
  10. package/scaffold/mcp/dist/cli/run.js +295 -0
  11. package/scaffold/mcp/dist/cli/stage.js +308 -0
  12. package/scaffold/mcp/dist/cli/telemetry-test.js +141 -0
  13. package/scaffold/mcp/dist/cli/ungoverned-detector.js +133 -0
  14. package/scaffold/mcp/dist/contextEntities.js +282 -0
  15. package/scaffold/mcp/dist/core/audit/query.js +72 -0
  16. package/scaffold/mcp/dist/core/audit/writer.js +28 -0
  17. package/scaffold/mcp/dist/core/config.js +235 -0
  18. package/scaffold/mcp/dist/core/enterprise-host-identity.js +193 -0
  19. package/scaffold/mcp/dist/core/enterprise-identity.js +18 -0
  20. package/scaffold/mcp/dist/core/enterprise-rotation.js +158 -0
  21. package/scaffold/mcp/dist/core/govern-paths.js +21 -0
  22. package/scaffold/mcp/dist/core/index.js +14 -0
  23. package/scaffold/mcp/dist/core/license.js +278 -0
  24. package/scaffold/mcp/dist/core/policy/enforce.js +66 -0
  25. package/scaffold/mcp/dist/core/policy/injection.js +172 -0
  26. package/scaffold/mcp/dist/core/policy/store.js +179 -0
  27. package/scaffold/mcp/dist/core/rbac/check.js +30 -0
  28. package/scaffold/mcp/dist/core/secure-endpoint.js +25 -0
  29. package/scaffold/mcp/dist/core/telemetry/collector.js +285 -0
  30. package/scaffold/mcp/dist/core/telemetry/state-dir.js +31 -0
  31. package/scaffold/mcp/dist/core/validators/builtins.js +632 -0
  32. package/scaffold/mcp/dist/core/validators/config.js +44 -0
  33. package/scaffold/mcp/dist/core/validators/engine.js +120 -0
  34. package/scaffold/mcp/dist/core/validators/evaluators/code_comments.js +236 -0
  35. package/scaffold/mcp/dist/core/validators/evaluators/regex.js +116 -0
  36. package/scaffold/mcp/dist/core/workflow/artifact-io.js +103 -0
  37. package/scaffold/mcp/dist/core/workflow/capabilities.js +88 -0
  38. package/scaffold/mcp/dist/core/workflow/default-workflows.js +102 -0
  39. package/scaffold/mcp/dist/core/workflow/enforcement.js +164 -0
  40. package/scaffold/mcp/dist/core/workflow/envelope.js +132 -0
  41. package/scaffold/mcp/dist/core/workflow/index.js +11 -0
  42. package/scaffold/mcp/dist/core/workflow/mcp-tools.js +175 -0
  43. package/scaffold/mcp/dist/core/workflow/resolution.js +69 -0
  44. package/scaffold/mcp/dist/core/workflow/run-lifecycle.js +123 -0
  45. package/scaffold/mcp/dist/core/workflow/schemas.js +141 -0
  46. package/scaffold/mcp/dist/core/workflow/synced-capability-registry.js +60 -0
  47. package/scaffold/mcp/dist/core/workflow/synced-registry.js +60 -0
  48. package/scaffold/mcp/dist/daemon/capability-sync-checker.js +242 -0
  49. package/scaffold/mcp/dist/daemon/client.js +130 -0
  50. package/scaffold/mcp/dist/daemon/egress-proxy.js +288 -0
  51. package/scaffold/mcp/dist/daemon/global-host-events.js +222 -0
  52. package/scaffold/mcp/dist/daemon/heartbeat-pusher.js +116 -0
  53. package/scaffold/mcp/dist/daemon/heartbeat-tracker.js +165 -0
  54. package/scaffold/mcp/dist/daemon/host-events-pusher.js +249 -0
  55. package/scaffold/mcp/dist/daemon/main.js +449 -0
  56. package/scaffold/mcp/dist/daemon/paths.js +36 -0
  57. package/scaffold/mcp/dist/daemon/project-service-registry.js +78 -0
  58. package/scaffold/mcp/dist/daemon/protocol.js +8 -0
  59. package/scaffold/mcp/dist/daemon/server.js +180 -0
  60. package/scaffold/mcp/dist/daemon/skill-sync-checker.js +557 -0
  61. package/scaffold/mcp/dist/daemon/sync-checker.js +165 -0
  62. package/scaffold/mcp/dist/daemon/ungoverned-scanner.js +136 -0
  63. package/scaffold/mcp/dist/daemon/workflow-sync-checker.js +249 -0
  64. package/scaffold/mcp/dist/defaults.js +6 -0
  65. package/scaffold/mcp/dist/embed.js +627 -0
  66. package/scaffold/mcp/dist/embedScheduler.js +479 -0
  67. package/scaffold/mcp/dist/embeddings.js +167 -0
  68. package/scaffold/mcp/dist/enterprise/audit/push.js +66 -0
  69. package/scaffold/mcp/dist/enterprise/index.js +327 -0
  70. package/scaffold/mcp/dist/enterprise/model/deploy.js +27 -0
  71. package/scaffold/mcp/dist/enterprise/policy/sync.js +129 -0
  72. package/scaffold/mcp/dist/enterprise/privacy/boundary.js +184 -0
  73. package/scaffold/mcp/dist/enterprise/reviews/changed-files.js +33 -0
  74. package/scaffold/mcp/dist/enterprise/reviews/pattern-context.js +202 -0
  75. package/scaffold/mcp/dist/enterprise/reviews/policy-selection.js +46 -0
  76. package/scaffold/mcp/dist/enterprise/reviews/push.js +102 -0
  77. package/scaffold/mcp/dist/enterprise/reviews/trust-state.js +186 -0
  78. package/scaffold/mcp/dist/enterprise/telemetry/sync.js +57 -0
  79. package/scaffold/mcp/dist/enterprise/tools/enterprise.js +826 -0
  80. package/scaffold/mcp/dist/enterprise/tools/harness.js +40 -0
  81. package/scaffold/mcp/dist/enterprise/tools/walk.js +73 -0
  82. package/scaffold/mcp/dist/enterprise/violations/push.js +77 -0
  83. package/scaffold/mcp/dist/enterprise/workflow/push.js +44 -0
  84. package/scaffold/mcp/dist/enterprise/workflow/state.js +329 -0
  85. package/scaffold/mcp/dist/frontmatter.js +33 -0
  86. package/scaffold/mcp/dist/graph.js +769 -0
  87. package/scaffold/mcp/dist/graphCsv.js +55 -0
  88. package/scaffold/mcp/dist/graphMetrics.js +8 -0
  89. package/scaffold/mcp/dist/hooks/permission-request.js +89 -0
  90. package/scaffold/mcp/dist/hooks/post-tool-use.js +105 -0
  91. package/scaffold/mcp/dist/hooks/pre-compact.js +29 -0
  92. package/scaffold/mcp/dist/hooks/pre-tool-use.js +78 -0
  93. package/scaffold/mcp/dist/hooks/session-end.js +43 -0
  94. package/scaffold/mcp/dist/hooks/session-start.js +41 -0
  95. package/scaffold/mcp/dist/hooks/shared.js +194 -0
  96. package/scaffold/mcp/dist/hooks/stop.js +28 -0
  97. package/scaffold/mcp/dist/hooks/user-prompt-submit.js +33 -0
  98. package/scaffold/mcp/dist/impactPresentation.js +137 -0
  99. package/scaffold/mcp/dist/impactRanking.js +191 -0
  100. package/scaffold/mcp/dist/impactResponse.js +30 -0
  101. package/scaffold/mcp/dist/impactResults.js +105 -0
  102. package/scaffold/mcp/dist/impactSeed.js +20 -0
  103. package/scaffold/mcp/dist/impactTraversal.js +64 -0
  104. package/scaffold/mcp/dist/jsonl.js +77 -0
  105. package/scaffold/mcp/dist/loadGraph.js +759 -0
  106. package/scaffold/mcp/dist/lruCache.js +38 -0
  107. package/scaffold/mcp/dist/paths.js +97 -0
  108. package/scaffold/mcp/dist/patternEvidence.js +272 -0
  109. package/scaffold/mcp/dist/plugin.js +81 -0
  110. package/scaffold/mcp/dist/presets.js +78 -0
  111. package/scaffold/mcp/dist/relatedResponse.js +18 -0
  112. package/scaffold/mcp/dist/relatedTraversal.js +78 -0
  113. package/scaffold/mcp/dist/rules.js +23 -0
  114. package/scaffold/mcp/dist/search.js +212 -0
  115. package/scaffold/mcp/dist/searchCore.js +457 -0
  116. package/scaffold/mcp/dist/searchResults.js +230 -0
  117. package/scaffold/mcp/dist/server.js +317 -0
  118. package/scaffold/mcp/dist/types.js +1 -0
  119. package/scaffold/mcp/package-lock.json +321 -200
  120. package/scaffold/mcp/package.json +15 -6
  121. package/scaffold/mcp/src/cli/enterprise-setup.ts +82 -8
  122. package/scaffold/mcp/src/cli/govern.ts +137 -52
  123. package/scaffold/mcp/src/cli/run.ts +53 -19
  124. package/scaffold/mcp/src/cli/stage.ts +8 -3
  125. package/scaffold/mcp/src/core/config.ts +9 -2
  126. package/scaffold/mcp/src/core/enterprise-host-identity.ts +259 -0
  127. package/scaffold/mcp/src/core/enterprise-identity.ts +20 -0
  128. package/scaffold/mcp/src/core/enterprise-rotation.ts +185 -0
  129. package/scaffold/mcp/src/core/govern-paths.ts +30 -0
  130. package/scaffold/mcp/src/core/license.ts +186 -17
  131. package/scaffold/mcp/src/core/secure-endpoint.ts +23 -0
  132. package/scaffold/mcp/src/core/workflow/enforcement.ts +8 -1
  133. package/scaffold/mcp/src/core/workflow/mcp-tools.ts +3 -0
  134. package/scaffold/mcp/src/core/workflow/resolution.ts +9 -1
  135. package/scaffold/mcp/src/core/workflow/synced-capability-registry.ts +15 -0
  136. package/scaffold/mcp/src/core/workflow/synced-registry.ts +15 -0
  137. package/scaffold/mcp/src/daemon/capability-sync-checker.ts +38 -2
  138. package/scaffold/mcp/src/daemon/egress-proxy.ts +14 -8
  139. package/scaffold/mcp/src/daemon/global-host-events.ts +288 -0
  140. package/scaffold/mcp/src/daemon/heartbeat-pusher.ts +4 -0
  141. package/scaffold/mcp/src/daemon/heartbeat-tracker.ts +2 -0
  142. package/scaffold/mcp/src/daemon/host-events-pusher.ts +5 -0
  143. package/scaffold/mcp/src/daemon/main.ts +99 -25
  144. package/scaffold/mcp/src/daemon/project-service-registry.ts +107 -0
  145. package/scaffold/mcp/src/daemon/skill-sync-checker.ts +368 -31
  146. package/scaffold/mcp/src/daemon/sync-checker.ts +4 -0
  147. package/scaffold/mcp/src/daemon/ungoverned-scanner.ts +57 -25
  148. package/scaffold/mcp/src/daemon/workflow-sync-checker.ts +41 -2
  149. package/scaffold/mcp/src/enterprise/audit/push.ts +8 -0
  150. package/scaffold/mcp/src/enterprise/policy/sync.ts +12 -0
  151. package/scaffold/mcp/src/enterprise/reviews/push.ts +9 -0
  152. package/scaffold/mcp/src/enterprise/reviews/trust-state.ts +3 -1
  153. package/scaffold/mcp/src/enterprise/telemetry/sync.ts +9 -0
  154. package/scaffold/mcp/src/enterprise/violations/push.ts +9 -0
  155. package/scaffold/mcp/src/enterprise/workflow/push.ts +7 -0
  156. package/scaffold/mcp/src/plugin.ts +20 -0
  157. package/scaffold/mcp/tests/copilot-shim.test.mjs +19 -1
  158. package/scaffold/mcp/tests/egress-proxy.test.mjs +37 -0
  159. package/scaffold/mcp/tests/enterprise-identity-sync.test.mjs +401 -0
  160. package/scaffold/mcp/tests/enterprise-setup.test.mjs +189 -0
  161. package/scaffold/mcp/tests/global-host-events.test.mjs +81 -0
  162. package/scaffold/mcp/tests/govern-install.test.mjs +95 -2
  163. package/scaffold/mcp/tests/govern-repair.test.mjs +20 -3
  164. package/scaffold/mcp/tests/heartbeat-tracker.test.mjs +26 -0
  165. package/scaffold/mcp/tests/license.test.mjs +367 -0
  166. package/scaffold/mcp/tests/project-service-registry.test.mjs +172 -0
  167. package/scaffold/mcp/tests/review-trust-contract.test.mjs +18 -0
  168. package/scaffold/mcp/tests/secure-enterprise-endpoint.test.mjs +46 -0
  169. package/scaffold/mcp/tests/skill-sync-checker.test.mjs +446 -2
  170. package/scaffold/mcp/tests/ungoverned-scanner.test.mjs +104 -22
  171. package/scaffold/mcp/tests/workflow-cli.test.mjs +14 -1
  172. package/scaffold/mcp/tests/workflow-synced-capabilities.test.mjs +37 -0
  173. package/scaffold/mcp/tests/workflow-synced-registry.test.mjs +40 -1
@@ -6,9 +6,34 @@ import path from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
 
8
8
  import {
9
- runSkillSyncForCli,
10
- runSkillSyncOnce,
9
+ runSkillSyncForCli as runSkillSyncForCliImpl,
10
+ runSkillSyncOnce as runSkillSyncOnceImpl,
11
11
  } from "../dist/daemon/skill-sync-checker.js";
12
+ import { enterpriseCredentialId } from "../dist/core/license.js";
13
+ import { claimEnterpriseHostIdentity } from "../dist/core/enterprise-host-identity.js";
14
+
15
+ const TEST_ENDPOINT = "https://example.com";
16
+ const TEST_API_KEY = "ent_test_12345678";
17
+
18
+ function enrollTestIdentity() {
19
+ assert.equal(
20
+ claimEnterpriseHostIdentity(
21
+ enterpriseCredentialId(TEST_ENDPOINT, TEST_API_KEY),
22
+ TEST_ENDPOINT,
23
+ ),
24
+ true,
25
+ );
26
+ }
27
+
28
+ async function runSkillSyncForCli(cwd, cli) {
29
+ enrollTestIdentity();
30
+ return runSkillSyncForCliImpl(cwd, cli);
31
+ }
32
+
33
+ async function runSkillSyncOnce(cwd, clis) {
34
+ enrollTestIdentity();
35
+ return runSkillSyncOnceImpl(cwd, clis);
36
+ }
12
37
 
13
38
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
39
  const skillFixture = JSON.parse(
@@ -76,6 +101,7 @@ function createFetchStub(state) {
76
101
  }
77
102
 
78
103
  if (url.pathname.startsWith("/api/v1/govern/skills/")) {
104
+ if (typeof state.bodyFetches === "number") state.bodyFetches += 1;
79
105
  const name = decodeURIComponent(url.pathname.split("/").pop() ?? "");
80
106
  if (!(name in state.bodies)) {
81
107
  return {
@@ -275,3 +301,421 @@ test("runSkillSyncOnce: consumes the shared org-skillz contract fixture end-to-e
275
301
  );
276
302
  assert.ok(!fs.existsSync(skillPath(homeDir, "claude", "alpha-codex")));
277
303
  });
304
+
305
+ test("runSkillSyncForCli: rejects unsafe remote names before fetching or writing skill bodies", async () => {
306
+ const { cwd } = makeWorkspace();
307
+ const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-skill-home-"));
308
+ process.env.HOME = homeDir;
309
+ const victimDir = path.join(homeDir, "Documents");
310
+ fs.mkdirSync(victimDir);
311
+ fs.writeFileSync(path.join(victimDir, "sentinel.txt"), "keep", "utf8");
312
+ let bodyFetches = 0;
313
+
314
+ globalThis.fetch = async (input) => {
315
+ const url = input instanceof URL ? input : new URL(input);
316
+ if (url.pathname.endsWith("/manifest")) {
317
+ return {
318
+ ok: true,
319
+ json: async () => ({
320
+ skills: [{
321
+ name: "../../Documents",
322
+ scope: "global",
323
+ updated_at: "2026-07-28T00:00:00.000Z",
324
+ }],
325
+ }),
326
+ };
327
+ }
328
+ bodyFetches += 1;
329
+ throw new Error("body fetch must not occur");
330
+ };
331
+
332
+ const outcome = await runSkillSyncForCli(cwd, "codex");
333
+ assert.equal(outcome.kind, "failed");
334
+ assert.match(outcome.error, /invalid skill name/);
335
+ assert.equal(bodyFetches, 0);
336
+ assert.equal(
337
+ fs.readFileSync(path.join(victimDir, "sentinel.txt"), "utf8"),
338
+ "keep",
339
+ );
340
+ });
341
+
342
+ test("runSkillSyncForCli: ignores persisted absolute paths when removing legacy state", async () => {
343
+ const { cwd } = makeWorkspace();
344
+ const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-skill-home-"));
345
+ process.env.HOME = homeDir;
346
+ const victimDir = path.join(homeDir, "Documents");
347
+ fs.mkdirSync(victimDir);
348
+ fs.writeFileSync(path.join(victimDir, "SKILL.md"), "not managed", "utf8");
349
+ fs.writeFileSync(path.join(victimDir, "sentinel.txt"), "keep", "utf8");
350
+ fs.mkdirSync(path.dirname(daemonStatePath(homeDir)), { recursive: true });
351
+ fs.writeFileSync(
352
+ daemonStatePath(homeDir),
353
+ JSON.stringify({
354
+ skills: {
355
+ "codex:safe-skill": {
356
+ cli: "codex",
357
+ scope: "global",
358
+ updated_at: "2026-07-27T00:00:00.000Z",
359
+ path: path.join(victimDir, "SKILL.md"),
360
+ },
361
+ },
362
+ }),
363
+ "utf8",
364
+ );
365
+ globalThis.fetch = createFetchStub({
366
+ manifests: { codex: [] },
367
+ bodies: {},
368
+ });
369
+
370
+ const outcome = await runSkillSyncForCli(cwd, "codex");
371
+ assert.equal(outcome.kind, "synced");
372
+ assert.deepEqual(outcome.removed, ["safe-skill"]);
373
+ assert.equal(
374
+ fs.readFileSync(path.join(victimDir, "sentinel.txt"), "utf8"),
375
+ "keep",
376
+ );
377
+ assert.equal(fs.existsSync(path.join(victimDir, "SKILL.md")), true);
378
+ });
379
+
380
+ test("runSkillSyncForCli: refuses to write through a managed skill directory symlink", {
381
+ skip: process.platform === "win32",
382
+ }, async () => {
383
+ const { cwd } = makeWorkspace();
384
+ const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-skill-home-"));
385
+ process.env.HOME = homeDir;
386
+ const victimDir = path.join(homeDir, "victim");
387
+ const skillsRoot = path.join(homeDir, ".codex", "skills");
388
+ fs.mkdirSync(victimDir);
389
+ fs.mkdirSync(skillsRoot, { recursive: true });
390
+ fs.writeFileSync(path.join(victimDir, "SKILL.md"), "keep", "utf8");
391
+ fs.symlinkSync(victimDir, path.join(skillsRoot, "global-skill"), "dir");
392
+ const remote = {
393
+ bodyFetches: 0,
394
+ manifests: {
395
+ codex: [{
396
+ name: "global-skill",
397
+ scope: "global",
398
+ updated_at: "2026-07-28T00:00:00.000Z",
399
+ }],
400
+ },
401
+ bodies: {
402
+ "global-skill": "remote replacement",
403
+ },
404
+ };
405
+ globalThis.fetch = createFetchStub(remote);
406
+
407
+ const outcome = await runSkillSyncForCli(cwd, "codex");
408
+ assert.equal(outcome.kind, "failed");
409
+ assert.match(outcome.error, /symbolic link/);
410
+ assert.equal(remote.bodyFetches, 0);
411
+ assert.equal(
412
+ fs.readFileSync(path.join(victimDir, "SKILL.md"), "utf8"),
413
+ "keep",
414
+ );
415
+ });
416
+
417
+ test("runSkillSyncForCli: refuses to overwrite an unmanaged personal skill collision", async () => {
418
+ const { cwd } = makeWorkspace();
419
+ const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-skill-home-"));
420
+ process.env.HOME = homeDir;
421
+ const personalDir = path.dirname(
422
+ skillPath(homeDir, "codex", "global-skill"),
423
+ );
424
+ fs.mkdirSync(personalDir, { recursive: true });
425
+ fs.writeFileSync(
426
+ path.join(personalDir, "SKILL.md"),
427
+ "personal content",
428
+ "utf8",
429
+ );
430
+ fs.writeFileSync(path.join(personalDir, "notes.txt"), "keep", "utf8");
431
+ const remote = {
432
+ bodyFetches: 0,
433
+ manifests: {
434
+ codex: [{
435
+ name: "global-skill",
436
+ scope: "global",
437
+ updated_at: "2026-07-28T00:00:00.000Z",
438
+ }],
439
+ },
440
+ bodies: { "global-skill": "remote replacement" },
441
+ };
442
+ globalThis.fetch = createFetchStub(remote);
443
+
444
+ const outcome = await runSkillSyncForCli(cwd, "codex");
445
+ assert.equal(outcome.kind, "failed");
446
+ assert.match(outcome.error, /not owned by Cortex/);
447
+ assert.equal(remote.bodyFetches, 0);
448
+ assert.equal(
449
+ fs.readFileSync(path.join(personalDir, "SKILL.md"), "utf8"),
450
+ "personal content",
451
+ );
452
+ assert.equal(
453
+ fs.readFileSync(path.join(personalDir, "notes.txt"), "utf8"),
454
+ "keep",
455
+ );
456
+ });
457
+
458
+ test("runSkillSyncForCli: refuses a symlinked CLI skills root", {
459
+ skip: process.platform === "win32",
460
+ }, async () => {
461
+ const { cwd } = makeWorkspace();
462
+ const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-skill-home-"));
463
+ process.env.HOME = homeDir;
464
+ const victimDir = path.join(homeDir, "outside");
465
+ const cliDir = path.join(homeDir, ".codex");
466
+ fs.mkdirSync(victimDir);
467
+ fs.mkdirSync(cliDir);
468
+ fs.symlinkSync(victimDir, path.join(cliDir, "skills"), "dir");
469
+ const remote = {
470
+ bodyFetches: 0,
471
+ manifests: {
472
+ codex: [{
473
+ name: "global-skill",
474
+ scope: "global",
475
+ updated_at: "2026-07-28T00:00:00.000Z",
476
+ }],
477
+ },
478
+ bodies: { "global-skill": "remote replacement" },
479
+ };
480
+ globalThis.fetch = createFetchStub(remote);
481
+
482
+ const outcome = await runSkillSyncForCli(cwd, "codex");
483
+ assert.equal(outcome.kind, "failed");
484
+ assert.match(outcome.error, /skills root must not be a symbolic link/);
485
+ assert.equal(remote.bodyFetches, 0);
486
+ assert.deepEqual(fs.readdirSync(victimDir), []);
487
+ });
488
+
489
+ test("runSkillSyncForCli: a second credential cannot mutate the first credential's managed skills", async () => {
490
+ const { cwd, contextDir } = makeWorkspace();
491
+ const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-skill-home-"));
492
+ process.env.HOME = homeDir;
493
+ const remote = {
494
+ manifests: {
495
+ codex: [{
496
+ name: "global-skill",
497
+ scope: "global",
498
+ updated_at: "2026-07-28T00:00:00.000Z",
499
+ }],
500
+ },
501
+ bodies: { "global-skill": "identity A" },
502
+ };
503
+ globalThis.fetch = createFetchStub(remote);
504
+
505
+ const first = await runSkillSyncForCli(cwd, "codex");
506
+ assert.equal(first.kind, "synced");
507
+ const installed = skillPath(homeDir, "codex", "global-skill");
508
+ assert.equal(fs.readFileSync(installed, "utf8"), "identity A");
509
+ assert.ok(
510
+ fs.existsSync(path.join(path.dirname(installed), ".cortex-managed.json")),
511
+ );
512
+
513
+ fs.writeFileSync(
514
+ path.join(contextDir, "enterprise.yml"),
515
+ [
516
+ "enterprise:",
517
+ " api_key: ent_other_12345678",
518
+ " endpoint: https://other.example.com",
519
+ "",
520
+ ].join("\n"),
521
+ "utf8",
522
+ );
523
+ remote.manifests.codex = [];
524
+
525
+ const second = await runSkillSyncForCli(cwd, "codex");
526
+ assert.equal(second.kind, "failed");
527
+ assert.match(second.error, /identity conflict/);
528
+ assert.equal(fs.readFileSync(installed, "utf8"), "identity A");
529
+ });
530
+
531
+ test("runSkillSyncForCli: removal refuses unowned files added to a managed directory", async () => {
532
+ const { cwd } = makeWorkspace();
533
+ const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-skill-home-"));
534
+ process.env.HOME = homeDir;
535
+ const remote = {
536
+ manifests: {
537
+ codex: [{
538
+ name: "global-skill",
539
+ scope: "global",
540
+ updated_at: "2026-07-28T00:00:00.000Z",
541
+ }],
542
+ },
543
+ bodies: { "global-skill": "managed" },
544
+ };
545
+ globalThis.fetch = createFetchStub(remote);
546
+ assert.equal((await runSkillSyncForCli(cwd, "codex")).kind, "synced");
547
+
548
+ const installedDir = path.dirname(
549
+ skillPath(homeDir, "codex", "global-skill"),
550
+ );
551
+ fs.writeFileSync(path.join(installedDir, "personal-note.txt"), "keep", "utf8");
552
+ remote.manifests.codex = [];
553
+
554
+ const outcome = await runSkillSyncForCli(cwd, "codex");
555
+ assert.equal(outcome.kind, "failed");
556
+ assert.match(outcome.error, /contains unowned files/);
557
+ assert.equal(
558
+ fs.readFileSync(path.join(installedDir, "personal-note.txt"), "utf8"),
559
+ "keep",
560
+ );
561
+ });
562
+
563
+ test("runSkillSyncForCli: refuses a managed SKILL.md symlink before fetching replacement content", {
564
+ skip: process.platform === "win32",
565
+ }, async () => {
566
+ const { cwd } = makeWorkspace();
567
+ const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-skill-home-"));
568
+ process.env.HOME = homeDir;
569
+ const remote = {
570
+ bodyFetches: 0,
571
+ manifests: {
572
+ codex: [{
573
+ name: "global-skill",
574
+ scope: "global",
575
+ updated_at: "2026-07-28T00:00:00.000Z",
576
+ }],
577
+ },
578
+ bodies: { "global-skill": "managed v1" },
579
+ };
580
+ globalThis.fetch = createFetchStub(remote);
581
+ assert.equal((await runSkillSyncForCli(cwd, "codex")).kind, "synced");
582
+ assert.equal(remote.bodyFetches, 1);
583
+
584
+ const installed = skillPath(homeDir, "codex", "global-skill");
585
+ const victim = path.join(homeDir, "victim.txt");
586
+ fs.writeFileSync(victim, "keep", "utf8");
587
+ fs.unlinkSync(installed);
588
+ fs.symlinkSync(victim, installed);
589
+ remote.manifests.codex[0].updated_at = "2026-07-28T01:00:00.000Z";
590
+ remote.bodies["global-skill"] = "managed v2";
591
+
592
+ const outcome = await runSkillSyncForCli(cwd, "codex");
593
+ assert.equal(outcome.kind, "failed");
594
+ assert.match(outcome.error, /skill file must not be a symbolic link/);
595
+ assert.equal(remote.bodyFetches, 1);
596
+ assert.equal(fs.readFileSync(victim, "utf8"), "keep");
597
+ });
598
+
599
+ test("runSkillSyncForCli: rejects malformed manifest variants before any body fetch", async () => {
600
+ const variants = [
601
+ {
602
+ label: "absolute name",
603
+ manifest: [{
604
+ name: "/tmp/owned",
605
+ scope: "global",
606
+ updated_at: "2026-07-28T00:00:00.000Z",
607
+ }],
608
+ },
609
+ {
610
+ label: "separator-only name",
611
+ manifest: [{
612
+ name: "../",
613
+ scope: "global",
614
+ updated_at: "2026-07-28T00:00:00.000Z",
615
+ }],
616
+ },
617
+ {
618
+ label: "dot name",
619
+ manifest: [{
620
+ name: ".",
621
+ scope: "global",
622
+ updated_at: "2026-07-28T00:00:00.000Z",
623
+ }],
624
+ },
625
+ {
626
+ label: "malformed entry",
627
+ manifest: [null],
628
+ },
629
+ {
630
+ label: "invalid scope",
631
+ manifest: [{
632
+ name: "safe-name",
633
+ scope: "project",
634
+ updated_at: "2026-07-28T00:00:00.000Z",
635
+ }],
636
+ },
637
+ {
638
+ label: "missing timestamp",
639
+ manifest: [{
640
+ name: "safe-name",
641
+ scope: "global",
642
+ updated_at: "",
643
+ }],
644
+ },
645
+ {
646
+ label: "duplicate name",
647
+ manifest: [
648
+ {
649
+ name: "safe-name",
650
+ scope: "global",
651
+ updated_at: "2026-07-28T00:00:00.000Z",
652
+ },
653
+ {
654
+ name: "safe-name",
655
+ scope: "global",
656
+ updated_at: "2026-07-28T00:00:00.000Z",
657
+ },
658
+ ],
659
+ },
660
+ ];
661
+
662
+ for (const variant of variants) {
663
+ const { cwd } = makeWorkspace();
664
+ const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-skill-home-"));
665
+ process.env.HOME = homeDir;
666
+ let bodyFetches = 0;
667
+ globalThis.fetch = async (input) => {
668
+ const url = input instanceof URL ? input : new URL(input);
669
+ if (url.pathname.endsWith("/manifest")) {
670
+ return {
671
+ ok: true,
672
+ status: 200,
673
+ statusText: "OK",
674
+ json: async () => ({ skills: variant.manifest }),
675
+ };
676
+ }
677
+ bodyFetches += 1;
678
+ throw new Error("body fetch must not occur");
679
+ };
680
+ const outcome = await runSkillSyncForCli(cwd, "codex");
681
+ assert.equal(outcome.kind, "failed", variant.label);
682
+ assert.equal(bodyFetches, 0, variant.label);
683
+ }
684
+ });
685
+
686
+ test("runSkillSyncForCli: sanitizes an unsafe legacy state key without filesystem mutation", async () => {
687
+ const { cwd } = makeWorkspace();
688
+ const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-skill-home-"));
689
+ process.env.HOME = homeDir;
690
+ const victimDir = path.join(homeDir, "Documents");
691
+ fs.mkdirSync(victimDir);
692
+ fs.writeFileSync(path.join(victimDir, "sentinel.txt"), "keep", "utf8");
693
+ fs.mkdirSync(path.dirname(daemonStatePath(homeDir)), { recursive: true });
694
+ fs.writeFileSync(
695
+ daemonStatePath(homeDir),
696
+ JSON.stringify({
697
+ skills: {
698
+ "codex:../../Documents": {
699
+ cli: "codex",
700
+ scope: "global",
701
+ updated_at: "2026-07-27T00:00:00.000Z",
702
+ path: victimDir,
703
+ },
704
+ },
705
+ }),
706
+ "utf8",
707
+ );
708
+ globalThis.fetch = createFetchStub({
709
+ manifests: { codex: [] },
710
+ bodies: {},
711
+ });
712
+
713
+ const outcome = await runSkillSyncForCli(cwd, "codex");
714
+ assert.equal(outcome.kind, "unchanged");
715
+ assert.equal(
716
+ fs.readFileSync(path.join(victimDir, "sentinel.txt"), "utf8"),
717
+ "keep",
718
+ );
719
+ const state = JSON.parse(fs.readFileSync(daemonStatePath(homeDir), "utf8"));
720
+ assert.deepEqual(state.skills, {});
721
+ });
@@ -5,6 +5,8 @@ import os from "node:os";
5
5
  import path from "node:path";
6
6
 
7
7
  import { runScanOnce, writeHostAuditEvent, startUngovernedScanner } from "../dist/daemon/ungoverned-scanner.js";
8
+ import { claimEnterpriseHostIdentity } from "../dist/core/enterprise-host-identity.js";
9
+ import { enterpriseCredentialId } from "../dist/core/license.js";
8
10
 
9
11
  function makeWorkspace(governMode, installs = null) {
10
12
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-ungoverned-"));
@@ -41,7 +43,7 @@ test("writeHostAuditEvent appends one JSONL line per call", async () => {
41
43
  }
42
44
  });
43
45
 
44
- test("runScanOnce: writes audit event with action=logged in advisory mode", async () => {
46
+ test("runScanOnce: does not write unattributed host process metadata into project audit", async () => {
45
47
  const { root } = makeWorkspace("advisory");
46
48
  try {
47
49
  const fakeProcs = [
@@ -54,20 +56,64 @@ test("runScanOnce: writes audit event with action=logged in advisory mode", asyn
54
56
  });
55
57
  assert.equal(findings.length, 1);
56
58
 
57
- const date = new Date().toISOString().slice(0, 10);
58
- const file = path.join(root, ".context", "audit", `host-events-${date}.jsonl`);
59
- const events = fs.readFileSync(file, "utf8").trim().split("\n").map(JSON.parse);
60
- assert.equal(events.length, 1);
61
- assert.equal(events[0].event_type, "ungoverned_ai_session_detected");
62
- assert.equal(events[0].cli, "claude");
63
- assert.equal(events[0].mode, "advisory");
64
- assert.equal(events[0].action, "logged");
65
- assert.equal(events[0].host_id, "test-host");
59
+ assert.equal(
60
+ fs.existsSync(path.join(root, ".context", "audit")),
61
+ false,
62
+ );
66
63
  } finally {
67
64
  fs.rmSync(root, { recursive: true, force: true });
68
65
  }
69
66
  });
70
67
 
68
+ test("runScanOnce: writes detections only to the credential-bound user-global queue", async () => {
69
+ const { root } = makeWorkspace("advisory");
70
+ const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-global-events-"));
71
+ const endpoint = "https://events.example.com";
72
+ const apiKey = "ent_global_events_12345678";
73
+ const credentialId = enterpriseCredentialId(endpoint, apiKey);
74
+ process.env.HOME = homeDir;
75
+ try {
76
+ assert.equal(
77
+ claimEnterpriseHostIdentity(credentialId, endpoint),
78
+ true,
79
+ );
80
+ await runScanOnce({
81
+ cwd: root,
82
+ credentialId,
83
+ detectorOptions: {
84
+ hostId: "host-global",
85
+ processes: [
86
+ {
87
+ pid: 4321,
88
+ ppid: 1,
89
+ user: os.userInfo().username,
90
+ comm: "claude",
91
+ args: "claude --prompt private",
92
+ },
93
+ ],
94
+ },
95
+ });
96
+
97
+ assert.equal(fs.existsSync(path.join(root, ".context", "audit")), false);
98
+ const queueDir = path.join(homeDir, ".cortex", "host-events");
99
+ const queueFile = fs.readdirSync(queueDir).find((name) =>
100
+ name.startsWith("ungoverned-")
101
+ );
102
+ assert.ok(queueFile);
103
+ assert.equal(fs.statSync(path.join(queueDir, queueFile)).mode & 0o777, 0o600);
104
+ const event = JSON.parse(
105
+ fs.readFileSync(path.join(queueDir, queueFile), "utf8").trim(),
106
+ );
107
+ assert.equal(event.credential_id, credentialId);
108
+ assert.equal(event.pid, 4321);
109
+ assert.match(event.args, /private/);
110
+ } finally {
111
+ delete process.env.HOME;
112
+ fs.rmSync(root, { recursive: true, force: true });
113
+ fs.rmSync(homeDir, { recursive: true, force: true });
114
+ }
115
+ });
116
+
71
117
  test("runScanOnce: enforced mode marks action=sigterm but our mock doesn't actually signal real procs", async () => {
72
118
  const { root } = makeWorkspace("enforced");
73
119
  try {
@@ -91,12 +137,11 @@ test("runScanOnce: enforced mode marks action=sigterm but our mock doesn't actua
91
137
  process.kill = origKill;
92
138
  }
93
139
 
94
- const date = new Date().toISOString().slice(0, 10);
95
- const file = path.join(root, ".context", "audit", `host-events-${date}.jsonl`);
96
- const events = fs.readFileSync(file, "utf8").trim().split("\n").map(JSON.parse);
97
- assert.equal(events[0].mode, "enforced");
98
- assert.equal(events[0].action, "sigterm");
99
140
  assert.deepEqual(killed, [99999, "SIGTERM"]);
141
+ assert.equal(
142
+ fs.existsSync(path.join(root, ".context", "audit")),
143
+ false,
144
+ );
100
145
  } finally {
101
146
  fs.rmSync(root, { recursive: true, force: true });
102
147
  }
@@ -127,9 +172,15 @@ test("runScanOnce: emits onFinding callback per detection", async () => {
127
172
  test("runScanOnce: skips Tier 1 CLIs that already have managed installs", async () => {
128
173
  const { root } = makeWorkspace("enforced");
129
174
  const managedClaudePath = path.join(root, "managed-settings.json");
130
- fs.writeFileSync(managedClaudePath, "{}\n");
175
+ fs.writeFileSync(
176
+ managedClaudePath,
177
+ JSON.stringify({ allowManagedHooksOnly: true }) + "\n",
178
+ );
131
179
  const managedCodexPath = path.join(root, "requirements.toml");
132
- fs.writeFileSync(managedCodexPath, "# managed\n");
180
+ fs.writeFileSync(
181
+ managedCodexPath,
182
+ "# Cortex govern — codex requirements (test).\n",
183
+ );
133
184
  fs.writeFileSync(
134
185
  path.join(root, ".context", "govern.local.json"),
135
186
  JSON.stringify({
@@ -160,16 +211,47 @@ test("runScanOnce: skips Tier 1 CLIs that already have managed installs", async
160
211
  const findings = await runScanOnce({
161
212
  cwd: root,
162
213
  mode: "enforced",
214
+ managedPathOverrides: {
215
+ claude: managedClaudePath,
216
+ codex: managedCodexPath,
217
+ },
163
218
  detectorOptions: { processes: fakeProcs, hostId: "test-host" },
164
219
  });
165
220
  assert.equal(findings.length, 1);
166
221
  assert.equal(findings[0].cli, "copilot");
167
222
 
168
- const date = new Date().toISOString().slice(0, 10);
169
- const file = path.join(root, ".context", "audit", `host-events-${date}.jsonl`);
170
- const events = fs.readFileSync(file, "utf8").trim().split("\n").map(JSON.parse);
171
- assert.equal(events.length, 1);
172
- assert.equal(events[0].cli, "copilot");
223
+ assert.equal(fs.existsSync(path.join(root, ".context", "audit")), false);
224
+ } finally {
225
+ fs.rmSync(root, { recursive: true, force: true });
226
+ }
227
+ });
228
+
229
+ test("runScanOnce: forged persisted paths cannot suppress Tier 1 findings", async () => {
230
+ const { root } = makeWorkspace("advisory", {
231
+ claude: {
232
+ mode: "advisory",
233
+ path: "/bin/sh",
234
+ version: "forged",
235
+ frameworks: [],
236
+ installed_at: "now",
237
+ },
238
+ });
239
+ try {
240
+ const findings = await runScanOnce({
241
+ cwd: root,
242
+ mode: "advisory",
243
+ detectorOptions: {
244
+ processes: [{
245
+ pid: 7654,
246
+ ppid: 1,
247
+ user: os.userInfo().username,
248
+ comm: "claude",
249
+ args: "claude --prompt hi",
250
+ }],
251
+ },
252
+ });
253
+ assert.equal(findings.length, 1);
254
+ assert.equal(findings[0].cli, "claude");
173
255
  } finally {
174
256
  fs.rmSync(root, { recursive: true, force: true });
175
257
  }
@@ -5,6 +5,10 @@ import os from "node:os";
5
5
  import path from "node:path";
6
6
 
7
7
  import { runStageCommand } from "../dist/cli/stage.js";
8
+ import { enterpriseCredentialId } from "../dist/core/license.js";
9
+
10
+ const ENTERPRISE_ENDPOINT = "https://licenses.example.com";
11
+ const ENTERPRISE_KEY = "ent_workflow_cli_12345678";
8
12
 
9
13
  const TINY_WORKFLOW = {
10
14
  id: "tiny",
@@ -41,7 +45,12 @@ function makeWorkspace() {
41
45
  fs.mkdirSync(path.join(dir, ".context"), { recursive: true });
42
46
  fs.writeFileSync(
43
47
  path.join(dir, ".context", "enterprise.yml"),
44
- "enterprise:\n api_key: test-key-for-cli-tests\n",
48
+ [
49
+ "enterprise:",
50
+ ` api_key: ${ENTERPRISE_KEY}`,
51
+ ` endpoint: ${ENTERPRISE_ENDPOINT}`,
52
+ "",
53
+ ].join("\n"),
45
54
  "utf8",
46
55
  );
47
56
  return dir;
@@ -53,6 +62,10 @@ function writeSyncedWorkflow(homeDir, workflowId, definition) {
53
62
  fs.writeFileSync(
54
63
  path.join(cortexDir, "workflows.local.json"),
55
64
  JSON.stringify({
65
+ credential_id: enterpriseCredentialId(
66
+ ENTERPRISE_ENDPOINT,
67
+ ENTERPRISE_KEY,
68
+ ),
56
69
  workflows: {
57
70
  [workflowId]: {
58
71
  workflow_id: workflowId,