@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.
- package/CHANGELOG.md +201 -0
- package/README.md +73 -3
- package/bin/cortex.mjs +339 -242
- package/bin/daemon-control.mjs +162 -0
- package/package.json +7 -3
- package/scaffold/mcp/dist/.cortex-build-hash +1 -0
- package/scaffold/mcp/dist/cli/enterprise-setup.js +134 -0
- package/scaffold/mcp/dist/cli/govern.js +937 -0
- package/scaffold/mcp/dist/cli/query.js +409 -0
- package/scaffold/mcp/dist/cli/run.js +295 -0
- package/scaffold/mcp/dist/cli/stage.js +308 -0
- package/scaffold/mcp/dist/cli/telemetry-test.js +141 -0
- package/scaffold/mcp/dist/cli/ungoverned-detector.js +133 -0
- package/scaffold/mcp/dist/contextEntities.js +282 -0
- package/scaffold/mcp/dist/core/audit/query.js +72 -0
- package/scaffold/mcp/dist/core/audit/writer.js +28 -0
- package/scaffold/mcp/dist/core/config.js +235 -0
- package/scaffold/mcp/dist/core/enterprise-host-identity.js +193 -0
- package/scaffold/mcp/dist/core/enterprise-identity.js +18 -0
- package/scaffold/mcp/dist/core/enterprise-rotation.js +158 -0
- package/scaffold/mcp/dist/core/govern-paths.js +21 -0
- package/scaffold/mcp/dist/core/index.js +14 -0
- package/scaffold/mcp/dist/core/license.js +278 -0
- package/scaffold/mcp/dist/core/policy/enforce.js +66 -0
- package/scaffold/mcp/dist/core/policy/injection.js +172 -0
- package/scaffold/mcp/dist/core/policy/store.js +179 -0
- package/scaffold/mcp/dist/core/rbac/check.js +30 -0
- package/scaffold/mcp/dist/core/secure-endpoint.js +25 -0
- package/scaffold/mcp/dist/core/telemetry/collector.js +285 -0
- package/scaffold/mcp/dist/core/telemetry/state-dir.js +31 -0
- package/scaffold/mcp/dist/core/validators/builtins.js +632 -0
- package/scaffold/mcp/dist/core/validators/config.js +44 -0
- package/scaffold/mcp/dist/core/validators/engine.js +120 -0
- package/scaffold/mcp/dist/core/validators/evaluators/code_comments.js +236 -0
- package/scaffold/mcp/dist/core/validators/evaluators/regex.js +116 -0
- package/scaffold/mcp/dist/core/workflow/artifact-io.js +103 -0
- package/scaffold/mcp/dist/core/workflow/capabilities.js +88 -0
- package/scaffold/mcp/dist/core/workflow/default-workflows.js +102 -0
- package/scaffold/mcp/dist/core/workflow/enforcement.js +164 -0
- package/scaffold/mcp/dist/core/workflow/envelope.js +132 -0
- package/scaffold/mcp/dist/core/workflow/index.js +11 -0
- package/scaffold/mcp/dist/core/workflow/mcp-tools.js +175 -0
- package/scaffold/mcp/dist/core/workflow/resolution.js +69 -0
- package/scaffold/mcp/dist/core/workflow/run-lifecycle.js +123 -0
- package/scaffold/mcp/dist/core/workflow/schemas.js +141 -0
- package/scaffold/mcp/dist/core/workflow/synced-capability-registry.js +60 -0
- package/scaffold/mcp/dist/core/workflow/synced-registry.js +60 -0
- package/scaffold/mcp/dist/daemon/capability-sync-checker.js +242 -0
- package/scaffold/mcp/dist/daemon/client.js +130 -0
- package/scaffold/mcp/dist/daemon/egress-proxy.js +288 -0
- package/scaffold/mcp/dist/daemon/global-host-events.js +222 -0
- package/scaffold/mcp/dist/daemon/heartbeat-pusher.js +116 -0
- package/scaffold/mcp/dist/daemon/heartbeat-tracker.js +165 -0
- package/scaffold/mcp/dist/daemon/host-events-pusher.js +249 -0
- package/scaffold/mcp/dist/daemon/main.js +449 -0
- package/scaffold/mcp/dist/daemon/paths.js +36 -0
- package/scaffold/mcp/dist/daemon/project-service-registry.js +78 -0
- package/scaffold/mcp/dist/daemon/protocol.js +8 -0
- package/scaffold/mcp/dist/daemon/server.js +180 -0
- package/scaffold/mcp/dist/daemon/skill-sync-checker.js +557 -0
- package/scaffold/mcp/dist/daemon/sync-checker.js +165 -0
- package/scaffold/mcp/dist/daemon/ungoverned-scanner.js +136 -0
- package/scaffold/mcp/dist/daemon/workflow-sync-checker.js +249 -0
- package/scaffold/mcp/dist/defaults.js +6 -0
- package/scaffold/mcp/dist/embed.js +627 -0
- package/scaffold/mcp/dist/embedScheduler.js +479 -0
- package/scaffold/mcp/dist/embeddings.js +167 -0
- package/scaffold/mcp/dist/enterprise/audit/push.js +66 -0
- package/scaffold/mcp/dist/enterprise/index.js +327 -0
- package/scaffold/mcp/dist/enterprise/model/deploy.js +27 -0
- package/scaffold/mcp/dist/enterprise/policy/sync.js +129 -0
- package/scaffold/mcp/dist/enterprise/privacy/boundary.js +184 -0
- package/scaffold/mcp/dist/enterprise/reviews/changed-files.js +33 -0
- package/scaffold/mcp/dist/enterprise/reviews/pattern-context.js +202 -0
- package/scaffold/mcp/dist/enterprise/reviews/policy-selection.js +46 -0
- package/scaffold/mcp/dist/enterprise/reviews/push.js +102 -0
- package/scaffold/mcp/dist/enterprise/reviews/trust-state.js +186 -0
- package/scaffold/mcp/dist/enterprise/telemetry/sync.js +57 -0
- package/scaffold/mcp/dist/enterprise/tools/enterprise.js +826 -0
- package/scaffold/mcp/dist/enterprise/tools/harness.js +40 -0
- package/scaffold/mcp/dist/enterprise/tools/walk.js +73 -0
- package/scaffold/mcp/dist/enterprise/violations/push.js +77 -0
- package/scaffold/mcp/dist/enterprise/workflow/push.js +44 -0
- package/scaffold/mcp/dist/enterprise/workflow/state.js +329 -0
- package/scaffold/mcp/dist/frontmatter.js +33 -0
- package/scaffold/mcp/dist/graph.js +769 -0
- package/scaffold/mcp/dist/graphCsv.js +55 -0
- package/scaffold/mcp/dist/graphMetrics.js +8 -0
- package/scaffold/mcp/dist/hooks/permission-request.js +89 -0
- package/scaffold/mcp/dist/hooks/post-tool-use.js +105 -0
- package/scaffold/mcp/dist/hooks/pre-compact.js +29 -0
- package/scaffold/mcp/dist/hooks/pre-tool-use.js +78 -0
- package/scaffold/mcp/dist/hooks/session-end.js +43 -0
- package/scaffold/mcp/dist/hooks/session-start.js +41 -0
- package/scaffold/mcp/dist/hooks/shared.js +194 -0
- package/scaffold/mcp/dist/hooks/stop.js +28 -0
- package/scaffold/mcp/dist/hooks/user-prompt-submit.js +33 -0
- package/scaffold/mcp/dist/impactPresentation.js +137 -0
- package/scaffold/mcp/dist/impactRanking.js +191 -0
- package/scaffold/mcp/dist/impactResponse.js +30 -0
- package/scaffold/mcp/dist/impactResults.js +105 -0
- package/scaffold/mcp/dist/impactSeed.js +20 -0
- package/scaffold/mcp/dist/impactTraversal.js +64 -0
- package/scaffold/mcp/dist/jsonl.js +77 -0
- package/scaffold/mcp/dist/loadGraph.js +759 -0
- package/scaffold/mcp/dist/lruCache.js +38 -0
- package/scaffold/mcp/dist/paths.js +97 -0
- package/scaffold/mcp/dist/patternEvidence.js +272 -0
- package/scaffold/mcp/dist/plugin.js +81 -0
- package/scaffold/mcp/dist/presets.js +78 -0
- package/scaffold/mcp/dist/relatedResponse.js +18 -0
- package/scaffold/mcp/dist/relatedTraversal.js +78 -0
- package/scaffold/mcp/dist/rules.js +23 -0
- package/scaffold/mcp/dist/search.js +212 -0
- package/scaffold/mcp/dist/searchCore.js +457 -0
- package/scaffold/mcp/dist/searchResults.js +230 -0
- package/scaffold/mcp/dist/server.js +317 -0
- package/scaffold/mcp/dist/types.js +1 -0
- package/scaffold/mcp/package-lock.json +336 -212
- package/scaffold/mcp/package.json +15 -6
- package/scaffold/mcp/src/cli/enterprise-setup.ts +82 -8
- package/scaffold/mcp/src/cli/govern.ts +137 -52
- package/scaffold/mcp/src/cli/query.ts +36 -2
- package/scaffold/mcp/src/cli/run.ts +53 -19
- package/scaffold/mcp/src/cli/stage.ts +8 -3
- package/scaffold/mcp/src/core/config.ts +9 -2
- package/scaffold/mcp/src/core/enterprise-host-identity.ts +259 -0
- package/scaffold/mcp/src/core/enterprise-identity.ts +20 -0
- package/scaffold/mcp/src/core/enterprise-rotation.ts +185 -0
- package/scaffold/mcp/src/core/govern-paths.ts +30 -0
- package/scaffold/mcp/src/core/license.ts +186 -17
- package/scaffold/mcp/src/core/secure-endpoint.ts +23 -0
- package/scaffold/mcp/src/core/workflow/enforcement.ts +8 -1
- package/scaffold/mcp/src/core/workflow/mcp-tools.ts +3 -0
- package/scaffold/mcp/src/core/workflow/resolution.ts +9 -1
- package/scaffold/mcp/src/core/workflow/synced-capability-registry.ts +15 -0
- package/scaffold/mcp/src/core/workflow/synced-registry.ts +15 -0
- package/scaffold/mcp/src/daemon/capability-sync-checker.ts +38 -2
- package/scaffold/mcp/src/daemon/egress-proxy.ts +14 -8
- package/scaffold/mcp/src/daemon/global-host-events.ts +288 -0
- package/scaffold/mcp/src/daemon/heartbeat-pusher.ts +4 -0
- package/scaffold/mcp/src/daemon/heartbeat-tracker.ts +2 -0
- package/scaffold/mcp/src/daemon/host-events-pusher.ts +5 -0
- package/scaffold/mcp/src/daemon/main.ts +99 -25
- package/scaffold/mcp/src/daemon/project-service-registry.ts +107 -0
- package/scaffold/mcp/src/daemon/skill-sync-checker.ts +368 -31
- package/scaffold/mcp/src/daemon/sync-checker.ts +4 -0
- package/scaffold/mcp/src/daemon/ungoverned-scanner.ts +57 -25
- package/scaffold/mcp/src/daemon/workflow-sync-checker.ts +41 -2
- package/scaffold/mcp/src/embed.ts +211 -10
- package/scaffold/mcp/src/enterprise/audit/push.ts +8 -0
- package/scaffold/mcp/src/enterprise/index.ts +1 -1
- package/scaffold/mcp/src/enterprise/policy/sync.ts +12 -0
- package/scaffold/mcp/src/enterprise/reviews/changed-files.ts +34 -0
- package/scaffold/mcp/src/enterprise/reviews/pattern-context.ts +231 -0
- package/scaffold/mcp/src/enterprise/reviews/push.ts +9 -0
- package/scaffold/mcp/src/enterprise/reviews/trust-state.ts +3 -1
- package/scaffold/mcp/src/enterprise/telemetry/sync.ts +9 -0
- package/scaffold/mcp/src/enterprise/tools/enterprise.ts +49 -36
- package/scaffold/mcp/src/enterprise/violations/push.ts +9 -0
- package/scaffold/mcp/src/enterprise/workflow/push.ts +7 -0
- package/scaffold/mcp/src/paths.ts +3 -5
- package/scaffold/mcp/src/patternEvidence.ts +347 -0
- package/scaffold/mcp/src/plugin.ts +20 -0
- package/scaffold/mcp/src/search.ts +35 -9
- package/scaffold/mcp/src/searchCore.ts +254 -12
- package/scaffold/mcp/src/searchResults.ts +96 -9
- package/scaffold/mcp/src/types.ts +7 -0
- package/scaffold/mcp/tests/changed-files.test.mjs +41 -0
- package/scaffold/mcp/tests/copilot-shim.test.mjs +19 -1
- package/scaffold/mcp/tests/egress-proxy.test.mjs +37 -0
- package/scaffold/mcp/tests/embed-entities.test.mjs +109 -1
- package/scaffold/mcp/tests/enterprise-identity-sync.test.mjs +401 -0
- package/scaffold/mcp/tests/enterprise-pattern-context.test.mjs +322 -0
- package/scaffold/mcp/tests/enterprise-setup.test.mjs +189 -0
- package/scaffold/mcp/tests/global-host-events.test.mjs +81 -0
- package/scaffold/mcp/tests/govern-install.test.mjs +95 -2
- package/scaffold/mcp/tests/govern-repair.test.mjs +20 -3
- package/scaffold/mcp/tests/heartbeat-tracker.test.mjs +26 -0
- package/scaffold/mcp/tests/license.test.mjs +367 -0
- package/scaffold/mcp/tests/paths.test.mjs +11 -3
- package/scaffold/mcp/tests/pattern-evidence.test.mjs +321 -0
- package/scaffold/mcp/tests/project-service-registry.test.mjs +172 -0
- package/scaffold/mcp/tests/query-cli.test.mjs +73 -1
- package/scaffold/mcp/tests/review-trust-contract.test.mjs +18 -0
- package/scaffold/mcp/tests/search-graph-score.test.mjs +167 -0
- package/scaffold/mcp/tests/secure-enterprise-endpoint.test.mjs +46 -0
- package/scaffold/mcp/tests/skill-sync-checker.test.mjs +446 -2
- package/scaffold/mcp/tests/ungoverned-scanner.test.mjs +104 -22
- package/scaffold/mcp/tests/workflow-cli.test.mjs +14 -1
- package/scaffold/mcp/tests/workflow-synced-capabilities.test.mjs +37 -0
- package/scaffold/mcp/tests/workflow-synced-registry.test.mjs +40 -1
- package/scaffold/scripts/dashboard.mjs +29 -18
- package/scaffold/scripts/doctor.sh +26 -8
- package/scaffold/scripts/ingest.mjs +37 -4
- 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({
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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
|
|
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
|
});
|