@danielblomma/cortex-mcp 2.2.5 → 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 (196) hide show
  1. package/CHANGELOG.md +201 -0
  2. package/README.md +73 -3
  3. package/bin/cortex.mjs +339 -242
  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 +336 -212
  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/query.ts +36 -2
  124. package/scaffold/mcp/src/cli/run.ts +53 -19
  125. package/scaffold/mcp/src/cli/stage.ts +8 -3
  126. package/scaffold/mcp/src/core/config.ts +9 -2
  127. package/scaffold/mcp/src/core/enterprise-host-identity.ts +259 -0
  128. package/scaffold/mcp/src/core/enterprise-identity.ts +20 -0
  129. package/scaffold/mcp/src/core/enterprise-rotation.ts +185 -0
  130. package/scaffold/mcp/src/core/govern-paths.ts +30 -0
  131. package/scaffold/mcp/src/core/license.ts +186 -17
  132. package/scaffold/mcp/src/core/secure-endpoint.ts +23 -0
  133. package/scaffold/mcp/src/core/workflow/enforcement.ts +8 -1
  134. package/scaffold/mcp/src/core/workflow/mcp-tools.ts +3 -0
  135. package/scaffold/mcp/src/core/workflow/resolution.ts +9 -1
  136. package/scaffold/mcp/src/core/workflow/synced-capability-registry.ts +15 -0
  137. package/scaffold/mcp/src/core/workflow/synced-registry.ts +15 -0
  138. package/scaffold/mcp/src/daemon/capability-sync-checker.ts +38 -2
  139. package/scaffold/mcp/src/daemon/egress-proxy.ts +14 -8
  140. package/scaffold/mcp/src/daemon/global-host-events.ts +288 -0
  141. package/scaffold/mcp/src/daemon/heartbeat-pusher.ts +4 -0
  142. package/scaffold/mcp/src/daemon/heartbeat-tracker.ts +2 -0
  143. package/scaffold/mcp/src/daemon/host-events-pusher.ts +5 -0
  144. package/scaffold/mcp/src/daemon/main.ts +99 -25
  145. package/scaffold/mcp/src/daemon/project-service-registry.ts +107 -0
  146. package/scaffold/mcp/src/daemon/skill-sync-checker.ts +368 -31
  147. package/scaffold/mcp/src/daemon/sync-checker.ts +4 -0
  148. package/scaffold/mcp/src/daemon/ungoverned-scanner.ts +57 -25
  149. package/scaffold/mcp/src/daemon/workflow-sync-checker.ts +41 -2
  150. package/scaffold/mcp/src/embed.ts +211 -10
  151. package/scaffold/mcp/src/enterprise/audit/push.ts +8 -0
  152. package/scaffold/mcp/src/enterprise/index.ts +1 -1
  153. package/scaffold/mcp/src/enterprise/policy/sync.ts +12 -0
  154. package/scaffold/mcp/src/enterprise/reviews/changed-files.ts +34 -0
  155. package/scaffold/mcp/src/enterprise/reviews/pattern-context.ts +231 -0
  156. package/scaffold/mcp/src/enterprise/reviews/push.ts +9 -0
  157. package/scaffold/mcp/src/enterprise/reviews/trust-state.ts +3 -1
  158. package/scaffold/mcp/src/enterprise/telemetry/sync.ts +9 -0
  159. package/scaffold/mcp/src/enterprise/tools/enterprise.ts +49 -36
  160. package/scaffold/mcp/src/enterprise/violations/push.ts +9 -0
  161. package/scaffold/mcp/src/enterprise/workflow/push.ts +7 -0
  162. package/scaffold/mcp/src/paths.ts +3 -5
  163. package/scaffold/mcp/src/patternEvidence.ts +347 -0
  164. package/scaffold/mcp/src/plugin.ts +20 -0
  165. package/scaffold/mcp/src/search.ts +35 -9
  166. package/scaffold/mcp/src/searchCore.ts +254 -12
  167. package/scaffold/mcp/src/searchResults.ts +96 -9
  168. package/scaffold/mcp/src/types.ts +7 -0
  169. package/scaffold/mcp/tests/changed-files.test.mjs +41 -0
  170. package/scaffold/mcp/tests/copilot-shim.test.mjs +19 -1
  171. package/scaffold/mcp/tests/egress-proxy.test.mjs +37 -0
  172. package/scaffold/mcp/tests/embed-entities.test.mjs +109 -1
  173. package/scaffold/mcp/tests/enterprise-identity-sync.test.mjs +401 -0
  174. package/scaffold/mcp/tests/enterprise-pattern-context.test.mjs +322 -0
  175. package/scaffold/mcp/tests/enterprise-setup.test.mjs +189 -0
  176. package/scaffold/mcp/tests/global-host-events.test.mjs +81 -0
  177. package/scaffold/mcp/tests/govern-install.test.mjs +95 -2
  178. package/scaffold/mcp/tests/govern-repair.test.mjs +20 -3
  179. package/scaffold/mcp/tests/heartbeat-tracker.test.mjs +26 -0
  180. package/scaffold/mcp/tests/license.test.mjs +367 -0
  181. package/scaffold/mcp/tests/paths.test.mjs +11 -3
  182. package/scaffold/mcp/tests/pattern-evidence.test.mjs +321 -0
  183. package/scaffold/mcp/tests/project-service-registry.test.mjs +172 -0
  184. package/scaffold/mcp/tests/query-cli.test.mjs +73 -1
  185. package/scaffold/mcp/tests/review-trust-contract.test.mjs +18 -0
  186. package/scaffold/mcp/tests/search-graph-score.test.mjs +167 -0
  187. package/scaffold/mcp/tests/secure-enterprise-endpoint.test.mjs +46 -0
  188. package/scaffold/mcp/tests/skill-sync-checker.test.mjs +446 -2
  189. package/scaffold/mcp/tests/ungoverned-scanner.test.mjs +104 -22
  190. package/scaffold/mcp/tests/workflow-cli.test.mjs +14 -1
  191. package/scaffold/mcp/tests/workflow-synced-capabilities.test.mjs +37 -0
  192. package/scaffold/mcp/tests/workflow-synced-registry.test.mjs +40 -1
  193. package/scaffold/scripts/dashboard.mjs +29 -18
  194. package/scaffold/scripts/doctor.sh +26 -8
  195. package/scaffold/scripts/ingest.mjs +37 -4
  196. package/scaffold/scripts/status.sh +13 -6
@@ -289,7 +289,12 @@ test("uninstall removes managed file and updates state", async () => {
289
289
  });
290
290
  assert.equal(fs.existsSync(target), true);
291
291
 
292
- const result = await runGovernUninstall({ cli: "claude", cwd: root, skipRoot: true });
292
+ const result = await runGovernUninstall({
293
+ cli: "claude",
294
+ cwd: root,
295
+ skipRoot: true,
296
+ pathOverride: { claude: target },
297
+ });
293
298
  assert.equal(result.ok, true, result.message);
294
299
  assert.equal(fs.existsSync(target), false);
295
300
 
@@ -329,7 +334,12 @@ test("uninstall in enforced mode requires --break-glass + --reason", async () =>
329
334
  skipRoot: true,
330
335
  });
331
336
 
332
- const blocked = await runGovernUninstall({ cli: "claude", cwd: root, skipRoot: true });
337
+ const blocked = await runGovernUninstall({
338
+ cli: "claude",
339
+ cwd: root,
340
+ skipRoot: true,
341
+ pathOverride: { claude: target },
342
+ });
333
343
  assert.equal(blocked.ok, false);
334
344
  assert.match(blocked.message, /enforced mode/);
335
345
  assert.equal(fs.existsSync(target), true);
@@ -339,6 +349,7 @@ test("uninstall in enforced mode requires --break-glass + --reason", async () =>
339
349
  cwd: root,
340
350
  breakGlass: true,
341
351
  skipRoot: true,
352
+ pathOverride: { claude: target },
342
353
  });
343
354
  assert.equal(noReason.ok, false);
344
355
  assert.match(noReason.message, /requires --reason/);
@@ -349,6 +360,7 @@ test("uninstall in enforced mode requires --break-glass + --reason", async () =>
349
360
  breakGlass: true,
350
361
  reason: "Incident response",
351
362
  skipRoot: true,
363
+ pathOverride: { claude: target },
352
364
  });
353
365
  assert.equal(allowed.ok, true, allowed.message);
354
366
  assert.equal(fs.existsSync(target), false);
@@ -358,6 +370,87 @@ test("uninstall in enforced mode requires --break-glass + --reason", async () =>
358
370
  }
359
371
  });
360
372
 
373
+ test("uninstall ignores a persisted managed path and removes only the derived target", async () => {
374
+ const { root, ctx } = makeProject({
375
+ apiKey: "ent_test_key_12345678",
376
+ baseUrl: "https://example.com",
377
+ });
378
+ const victim = path.join(root, "victim.txt");
379
+ const derived = path.join(root, "derived-managed.json");
380
+ fs.writeFileSync(victim, "do not delete");
381
+ fs.writeFileSync(derived, "managed");
382
+ fs.writeFileSync(
383
+ path.join(ctx, "govern.local.json"),
384
+ JSON.stringify({
385
+ installs: {
386
+ claude: {
387
+ path: victim,
388
+ version: "attacker-controlled",
389
+ frameworks: [],
390
+ installed_at: new Date().toISOString(),
391
+ mode: "advisory",
392
+ },
393
+ },
394
+ }),
395
+ );
396
+ try {
397
+ const result = await runGovernUninstall({
398
+ cli: "claude",
399
+ cwd: root,
400
+ skipRoot: true,
401
+ pathOverride: { claude: derived },
402
+ });
403
+ assert.equal(result.ok, true, result.message);
404
+ assert.equal(fs.readFileSync(victim, "utf8"), "do not delete");
405
+ assert.equal(fs.existsSync(derived), false);
406
+ } finally {
407
+ fs.rmSync(root, { recursive: true, force: true });
408
+ }
409
+ });
410
+
411
+ test("govern state replacement does not follow a dangling project-state symlink", async () => {
412
+ const { server, baseUrl } = await startMockServer({
413
+ "GET /api/v1/govern/config": (_req, res) => {
414
+ res.setHeader("ETag", '"v1"');
415
+ res.end(JSON.stringify({
416
+ cli: "claude",
417
+ managed_settings: { allowManagedHooksOnly: true },
418
+ deny_rules: [],
419
+ tamper_config: {
420
+ heartbeat_interval_seconds: 60,
421
+ missing_threshold_seconds: 300,
422
+ },
423
+ frameworks: [{ id: "iso27001", version: "1" }],
424
+ }));
425
+ },
426
+ "POST /api/v1/govern/applied": (_req, res) =>
427
+ res.end(JSON.stringify({ ok: true })),
428
+ });
429
+ const { root, ctx } = makeProject({
430
+ apiKey: "ent_state_symlink_12345678",
431
+ baseUrl,
432
+ });
433
+ const victim = path.join(root, "must-not-be-created");
434
+ const statePath = path.join(ctx, "govern.local.json");
435
+ const managedPath = path.join(root, "managed.json");
436
+ fs.symlinkSync(victim, statePath);
437
+ try {
438
+ const result = await runGovernInstall({
439
+ cli: "claude",
440
+ cwd: root,
441
+ skipRoot: true,
442
+ pathOverride: { claude: managedPath },
443
+ });
444
+ assert.equal(result.ok, true, result.message);
445
+ assert.equal(fs.existsSync(victim), false);
446
+ assert.equal(fs.lstatSync(statePath).isSymbolicLink(), false);
447
+ assert.equal(fs.statSync(statePath).mode & 0o777, 0o600);
448
+ } finally {
449
+ server.close();
450
+ fs.rmSync(root, { recursive: true, force: true });
451
+ }
452
+ });
453
+
361
454
  test("install with no api_key returns helpful error", async () => {
362
455
  const { root } = makeProject({ apiKey: "", baseUrl: "http://example.com" });
363
456
  try {
@@ -42,8 +42,16 @@ test("repair: errors when managed file is missing", async () => {
42
42
  },
43
43
  },
44
44
  });
45
+ const expectedPath = path.join(
46
+ os.tmpdir(),
47
+ "definitely-not-here-derived-claude-managed.json",
48
+ );
45
49
  try {
46
- const result = await runGovernRepair({ cwd: root, skipRoot: true });
50
+ const result = await runGovernRepair({
51
+ cwd: root,
52
+ skipRoot: true,
53
+ pathOverride: { claude: expectedPath },
54
+ });
47
55
  assert.equal(result.ok, false);
48
56
  assert.match(result.message, /missing/);
49
57
  } finally {
@@ -72,7 +80,11 @@ test("repair: errors when copilot shim has been replaced", async () => {
72
80
  }),
73
81
  );
74
82
  try {
75
- const result = await runGovernRepair({ cwd: root, skipRoot: true });
83
+ const result = await runGovernRepair({
84
+ cwd: root,
85
+ skipRoot: true,
86
+ pathOverride: { copilot: fakeShim },
87
+ });
76
88
  assert.equal(result.ok, false);
77
89
  assert.match(result.message, /no longer a cortex shim/);
78
90
  } finally {
@@ -116,6 +128,7 @@ test("repair: clears tamper lock when managed paths verify clean", async () => {
116
128
  cwd: root,
117
129
  skipRoot: true,
118
130
  reason: "Operator reviewed and cleared",
131
+ pathOverride: { claude: claudeManaged },
119
132
  });
120
133
  assert.equal(result.ok, true, result.message);
121
134
  assert.equal(result.removed_lock, true);
@@ -147,7 +160,11 @@ test("repair: success even when there is no lock — paths still verified", asyn
147
160
  }),
148
161
  );
149
162
  try {
150
- const result = await runGovernRepair({ cwd: root, skipRoot: true });
163
+ const result = await runGovernRepair({
164
+ cwd: root,
165
+ skipRoot: true,
166
+ pathOverride: { claude: claudeManaged },
167
+ });
151
168
  assert.equal(result.ok, true, result.message);
152
169
  assert.equal(result.removed_lock, false);
153
170
  assert.match(result.message, /No tamper lock present/);
@@ -148,6 +148,32 @@ test("detectTamper: ended session is not flagged", () => {
148
148
  assert.equal(findings.length, 0);
149
149
  });
150
150
 
151
+ test("detectTamper: only evaluates project roots registered by the daemon", () => {
152
+ const tracker = new HeartbeatTracker();
153
+ for (const cwd of ["/first", "/second"]) {
154
+ tracker.recordHeartbeat({
155
+ cli: "claude",
156
+ hook: "SessionStart",
157
+ session_id: `session-${cwd}`,
158
+ cwd,
159
+ ts: ts(-10 * 60 * 1000),
160
+ });
161
+ tracker.recordHeartbeat({
162
+ cli: "claude",
163
+ hook: "PreToolUse",
164
+ session_id: `session-${cwd}`,
165
+ cwd,
166
+ ts: ts(-9 * 60 * 1000),
167
+ });
168
+ }
169
+
170
+ const findings = tracker.detectTamper({
171
+ cwds: ["/second"],
172
+ missingThresholdSeconds: 60,
173
+ });
174
+ assert.deepEqual(findings.map((finding) => finding.cwd), ["/second"]);
175
+ });
176
+
151
177
  test("detectTamper: stale session beyond cleanupAfterMs is auto-removed", () => {
152
178
  const tracker = new HeartbeatTracker({ cleanupAfterMs: 1000 });
153
179
  tracker.recordHeartbeat({
@@ -0,0 +1,367 @@
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 { verifyLicense } from "../dist/core/license.js";
8
+
9
+ function makeContextDir() {
10
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-license-"));
11
+ const contextDir = path.join(root, ".context");
12
+ fs.mkdirSync(contextDir, { recursive: true });
13
+ return { root, contextDir };
14
+ }
15
+
16
+ function validResponse() {
17
+ return {
18
+ ok: true,
19
+ status: 200,
20
+ json: async () => ({
21
+ valid: true,
22
+ edition: "enterprise",
23
+ features: ["govern"],
24
+ expires_at: "2099-01-01T00:00:00.000Z",
25
+ max_repos: 10,
26
+ }),
27
+ };
28
+ }
29
+
30
+ function cachePath(contextDir) {
31
+ return path.join(contextDir, "telemetry", "license_cache.json");
32
+ }
33
+
34
+ test.afterEach(() => {
35
+ globalThis.fetch = undefined;
36
+ });
37
+
38
+ test("verifyLicense: reuses a fresh cache only for the same endpoint and API key", async () => {
39
+ const { root, contextDir } = makeContextDir();
40
+ let calls = 0;
41
+ globalThis.fetch = async () => {
42
+ calls += 1;
43
+ return validResponse();
44
+ };
45
+
46
+ try {
47
+ const first = await verifyLicense(
48
+ contextDir,
49
+ "https://licenses.example.com/",
50
+ "ent_first_12345678",
51
+ );
52
+ const second = await verifyLicense(
53
+ contextDir,
54
+ "https://licenses.example.com",
55
+ "ent_first_12345678",
56
+ );
57
+
58
+ assert.equal(first.valid, true);
59
+ assert.equal(first.source, "remote");
60
+ assert.equal(second.valid, true);
61
+ assert.equal(second.source, "cache");
62
+ assert.equal(calls, 1);
63
+
64
+ const rawCache = fs.readFileSync(cachePath(contextDir), "utf8");
65
+ assert.doesNotMatch(rawCache, /ent_first_12345678/);
66
+ const parsedCache = JSON.parse(rawCache);
67
+ assert.equal(parsedCache.version, 2);
68
+ assert.equal(parsedCache.endpoint, "https://licenses.example.com");
69
+ assert.match(parsedCache.api_key_sha256, /^[a-f0-9]{64}$/);
70
+ } finally {
71
+ fs.rmSync(root, { recursive: true, force: true });
72
+ }
73
+ });
74
+
75
+ test("verifyLicense: atomic cache replacement does not follow a dangling cache symlink", async () => {
76
+ const { root, contextDir } = makeContextDir();
77
+ const victim = path.join(root, "must-not-be-created");
78
+ fs.mkdirSync(path.dirname(cachePath(contextDir)), { recursive: true });
79
+ fs.symlinkSync(victim, cachePath(contextDir));
80
+ globalThis.fetch = async () => validResponse();
81
+
82
+ try {
83
+ const result = await verifyLicense(
84
+ contextDir,
85
+ "https://licenses.example.com",
86
+ "ent_symlink_cache_12345678",
87
+ );
88
+ assert.equal(result.valid, true);
89
+ assert.equal(fs.existsSync(victim), false);
90
+ assert.equal(fs.lstatSync(cachePath(contextDir)).isSymbolicLink(), false);
91
+ assert.equal(fs.statSync(cachePath(contextDir)).mode & 0o777, 0o600);
92
+ } finally {
93
+ fs.rmSync(root, { recursive: true, force: true });
94
+ }
95
+ });
96
+
97
+ test("verifyLicense: does not reuse a cache for another key or endpoint", async () => {
98
+ const { root, contextDir } = makeContextDir();
99
+ let calls = 0;
100
+ globalThis.fetch = async () => {
101
+ calls += 1;
102
+ if (calls === 1) return validResponse();
103
+ throw new Error("offline");
104
+ };
105
+
106
+ try {
107
+ const first = await verifyLicense(
108
+ contextDir,
109
+ "https://licenses.example.com",
110
+ "ent_first_12345678",
111
+ );
112
+ assert.equal(first.valid, true);
113
+
114
+ const otherKey = await verifyLicense(
115
+ contextDir,
116
+ "https://licenses.example.com",
117
+ "ent_second_12345678",
118
+ );
119
+ assert.deepEqual(
120
+ {
121
+ valid: otherKey.valid,
122
+ source: otherKey.source,
123
+ reason: otherKey.valid ? undefined : otherKey.reason,
124
+ },
125
+ {
126
+ valid: false,
127
+ source: "grace_expired",
128
+ reason: "endpoint_unreachable_grace_expired",
129
+ },
130
+ );
131
+
132
+ const otherEndpoint = await verifyLicense(
133
+ contextDir,
134
+ "https://other.example.com",
135
+ "ent_first_12345678",
136
+ );
137
+ assert.equal(otherEndpoint.valid, false);
138
+ assert.equal(otherEndpoint.source, "grace_expired");
139
+ assert.equal(calls, 3);
140
+ } finally {
141
+ fs.rmSync(root, { recursive: true, force: true });
142
+ }
143
+ });
144
+
145
+ test("verifyLicense: 401 and 403 are authoritative and never use grace", async () => {
146
+ for (const status of [401, 403]) {
147
+ const { root, contextDir } = makeContextDir();
148
+ let calls = 0;
149
+ globalThis.fetch = async () => {
150
+ calls += 1;
151
+ if (calls === 1) return validResponse();
152
+ return {
153
+ ok: false,
154
+ status,
155
+ statusText: "Rejected",
156
+ };
157
+ };
158
+
159
+ try {
160
+ await verifyLicense(
161
+ contextDir,
162
+ "https://licenses.example.com",
163
+ "ent_first_12345678",
164
+ );
165
+ const cache = JSON.parse(fs.readFileSync(cachePath(contextDir), "utf8"));
166
+ cache.cached_at = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString();
167
+ fs.writeFileSync(cachePath(contextDir), JSON.stringify(cache), "utf8");
168
+
169
+ const rejected = await verifyLicense(
170
+ contextDir,
171
+ "https://licenses.example.com",
172
+ "ent_first_12345678",
173
+ );
174
+ assert.equal(rejected.valid, false);
175
+ assert.equal(rejected.source, "remote");
176
+ assert.equal(rejected.reason, "authentication_rejected");
177
+ assert.equal(fs.existsSync(cachePath(contextDir)), false);
178
+ } finally {
179
+ fs.rmSync(root, { recursive: true, force: true });
180
+ }
181
+ }
182
+ });
183
+
184
+ test("verifyLicense: an identity-matched positive cache remains available for transient grace", async () => {
185
+ const { root, contextDir } = makeContextDir();
186
+ let calls = 0;
187
+ globalThis.fetch = async () => {
188
+ calls += 1;
189
+ if (calls === 1) return validResponse();
190
+ throw new Error("offline");
191
+ };
192
+
193
+ try {
194
+ await verifyLicense(
195
+ contextDir,
196
+ "https://licenses.example.com",
197
+ "ent_first_12345678",
198
+ );
199
+ const cache = JSON.parse(fs.readFileSync(cachePath(contextDir), "utf8"));
200
+ cache.cached_at = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString();
201
+ fs.writeFileSync(cachePath(contextDir), JSON.stringify(cache), "utf8");
202
+
203
+ const grace = await verifyLicense(
204
+ contextDir,
205
+ "https://licenses.example.com",
206
+ "ent_first_12345678",
207
+ );
208
+ assert.equal(grace.valid, true);
209
+ assert.equal(grace.source, "cache");
210
+ assert.equal(calls, 2);
211
+ } finally {
212
+ fs.rmSync(root, { recursive: true, force: true });
213
+ }
214
+ });
215
+
216
+ test("verifyLicense: legacy unbound cache entries are never trusted", async () => {
217
+ const { root, contextDir } = makeContextDir();
218
+ fs.mkdirSync(path.dirname(cachePath(contextDir)), { recursive: true });
219
+ fs.writeFileSync(
220
+ cachePath(contextDir),
221
+ JSON.stringify({
222
+ cached_at: new Date().toISOString(),
223
+ result: {
224
+ valid: true,
225
+ edition: "enterprise",
226
+ features: [],
227
+ expires_at: "2099-01-01T00:00:00.000Z",
228
+ max_repos: 10,
229
+ verified_at: new Date().toISOString(),
230
+ source: "remote",
231
+ },
232
+ }),
233
+ "utf8",
234
+ );
235
+ globalThis.fetch = async () => {
236
+ throw new Error("offline");
237
+ };
238
+
239
+ try {
240
+ const result = await verifyLicense(
241
+ contextDir,
242
+ "https://licenses.example.com",
243
+ "ent_first_12345678",
244
+ );
245
+ assert.equal(result.valid, false);
246
+ assert.equal(result.source, "grace_expired");
247
+ } finally {
248
+ fs.rmSync(root, { recursive: true, force: true });
249
+ }
250
+ });
251
+
252
+ test("verifyLicense: malformed cache shapes fail closed instead of throwing", async () => {
253
+ const { root, contextDir } = makeContextDir();
254
+ fs.mkdirSync(path.dirname(cachePath(contextDir)), { recursive: true });
255
+ fs.writeFileSync(
256
+ cachePath(contextDir),
257
+ JSON.stringify({ cached_at: new Date().toISOString() }),
258
+ "utf8",
259
+ );
260
+ globalThis.fetch = async () => {
261
+ throw new Error("offline");
262
+ };
263
+
264
+ try {
265
+ const result = await verifyLicense(
266
+ contextDir,
267
+ "https://licenses.example.com",
268
+ "ent_first_12345678",
269
+ );
270
+ assert.equal(result.valid, false);
271
+ assert.equal(result.source, "grace_expired");
272
+ } finally {
273
+ fs.rmSync(root, { recursive: true, force: true });
274
+ }
275
+ });
276
+
277
+ test("verifyLicense: structurally partial v2 cache entries are never trusted", async () => {
278
+ const { root, contextDir } = makeContextDir();
279
+ fs.mkdirSync(path.dirname(cachePath(contextDir)), { recursive: true });
280
+ fs.writeFileSync(
281
+ cachePath(contextDir),
282
+ JSON.stringify({
283
+ version: 2,
284
+ endpoint: "https://licenses.example.com",
285
+ api_key_sha256: "a".repeat(64),
286
+ cached_at: new Date(Date.now() + 60_000).toISOString(),
287
+ result: {
288
+ valid: true,
289
+ expires_at: null,
290
+ },
291
+ }),
292
+ "utf8",
293
+ );
294
+ let calls = 0;
295
+ globalThis.fetch = async () => {
296
+ calls += 1;
297
+ throw new Error("offline");
298
+ };
299
+
300
+ try {
301
+ const result = await verifyLicense(
302
+ contextDir,
303
+ "https://licenses.example.com",
304
+ "ent_first_12345678",
305
+ );
306
+ assert.equal(result.valid, false);
307
+ assert.equal(result.source, "grace_expired");
308
+ assert.equal(calls, 1);
309
+ assert.equal(fs.existsSync(cachePath(contextDir)), false);
310
+ } finally {
311
+ fs.rmSync(root, { recursive: true, force: true });
312
+ }
313
+ });
314
+
315
+ test("verifyLicense: a future-dated otherwise valid cache fails closed and is removed", async () => {
316
+ const { root, contextDir } = makeContextDir();
317
+ let calls = 0;
318
+ globalThis.fetch = async () => {
319
+ calls += 1;
320
+ if (calls === 1) return validResponse();
321
+ throw new Error("offline");
322
+ };
323
+
324
+ try {
325
+ await verifyLicense(
326
+ contextDir,
327
+ "https://licenses.example.com",
328
+ "ent_first_12345678",
329
+ );
330
+ const cache = JSON.parse(fs.readFileSync(cachePath(contextDir), "utf8"));
331
+ cache.cached_at = new Date(Date.now() + 60_000).toISOString();
332
+ fs.writeFileSync(cachePath(contextDir), JSON.stringify(cache), "utf8");
333
+
334
+ const result = await verifyLicense(
335
+ contextDir,
336
+ "https://licenses.example.com",
337
+ "ent_first_12345678",
338
+ );
339
+ assert.equal(result.valid, false);
340
+ assert.equal(result.source, "grace_expired");
341
+ assert.equal(calls, 2);
342
+ } finally {
343
+ fs.rmSync(root, { recursive: true, force: true });
344
+ }
345
+ });
346
+
347
+ test("verifyLicense: refuses cleartext non-loopback endpoints before sending a bearer key", async () => {
348
+ const { root, contextDir } = makeContextDir();
349
+ let calls = 0;
350
+ globalThis.fetch = async () => {
351
+ calls += 1;
352
+ return validResponse();
353
+ };
354
+
355
+ try {
356
+ const result = await verifyLicense(
357
+ contextDir,
358
+ "http://licenses.example.com",
359
+ "ent_first_12345678",
360
+ );
361
+ assert.equal(result.valid, false);
362
+ assert.equal(result.reason, "insecure_or_invalid_endpoint");
363
+ assert.equal(calls, 0);
364
+ } finally {
365
+ fs.rmSync(root, { recursive: true, force: true });
366
+ }
367
+ });
@@ -4,13 +4,15 @@ import { mkdtempSync, mkdirSync, realpathSync, writeFileSync } from "node:fs";
4
4
  import { tmpdir } from "node:os";
5
5
  import path from "node:path";
6
6
  import { spawnSync } from "node:child_process";
7
- import { pathToFileURL } from "node:url";
7
+ import { fileURLToPath, pathToFileURL } from "node:url";
8
+
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
10
 
9
11
  test("paths resolve the project root from cwd without duplicating .context", () => {
10
12
  const projectRoot = mkdtempSync(path.join(tmpdir(), "cortex-paths-"));
11
13
  const contextDir = path.join(projectRoot, ".context");
12
14
  const mcpDir = path.join(contextDir, "mcp");
13
- const pathsModuleUrl = pathToFileURL(path.resolve("dist/paths.js")).href;
15
+ const pathsModuleUrl = pathToFileURL(path.resolve(__dirname, "..", "dist", "paths.js")).href;
14
16
 
15
17
  mkdirSync(mcpDir, { recursive: true });
16
18
  writeFileSync(path.join(contextDir, "config.yaml"), "source_paths:\n - src\n");
@@ -20,7 +22,7 @@ test("paths resolve the project root from cwd without duplicating .context", ()
20
22
  [
21
23
  "--input-type=module",
22
24
  "-e",
23
- `import { REPO_ROOT, CONTEXT_DIR, CACHE_DIR } from ${JSON.stringify(pathsModuleUrl)}; console.log(JSON.stringify({ REPO_ROOT, CONTEXT_DIR, CACHE_DIR }));`
25
+ `import { REPO_ROOT, CONTEXT_DIR, CACHE_DIR, DEFAULT_RANKING } from ${JSON.stringify(pathsModuleUrl)}; console.log(JSON.stringify({ REPO_ROOT, CONTEXT_DIR, CACHE_DIR, DEFAULT_RANKING }));`
24
26
  ],
25
27
  {
26
28
  cwd: mcpDir,
@@ -35,4 +37,10 @@ test("paths resolve the project root from cwd without duplicating .context", ()
35
37
  assert.equal(parsed.REPO_ROOT, resolvedProjectRoot);
36
38
  assert.equal(parsed.CONTEXT_DIR, path.join(resolvedProjectRoot, ".context"));
37
39
  assert.equal(parsed.CACHE_DIR, path.join(resolvedProjectRoot, ".context", "cache"));
40
+ assert.deepEqual(parsed.DEFAULT_RANKING, {
41
+ semantic: 0.4,
42
+ graph: 0.25,
43
+ trust: 0.2,
44
+ recency: 0.15
45
+ });
38
46
  });