@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
@@ -1,6 +1,7 @@
1
1
  import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
3
  import { buildSearchResults, buildSearchResultsWithStats } from "../dist/searchResults.js";
4
+ import { expandQueryTokens, semanticScore, structuralSearchBoost, tokenize } from "../dist/searchCore.js";
4
5
 
5
6
  function makeEntity(id, entityType, overrides = {}) {
6
7
  return {
@@ -166,6 +167,172 @@ test("ranking is preserved when score fields are omitted from the response", ()
166
167
  assert.equal("score" in results[0], false);
167
168
  });
168
169
 
170
+ test("tokenization exposes camel-case code identifiers for lexical matching", () => {
171
+ const tokens = tokenize("buildSearchResults parseRankingFromConfig parseJavaScriptCode parseCSharpParser C# VB.NET");
172
+
173
+ assert.ok(tokens.includes("build"));
174
+ assert.ok(tokens.includes("search"));
175
+ assert.ok(tokens.includes("results"));
176
+ assert.ok(tokens.includes("parse"));
177
+ assert.ok(tokens.includes("ranking"));
178
+ assert.ok(tokens.includes("config"));
179
+ assert.ok(tokens.includes("javascript"));
180
+ assert.ok(tokens.includes("csharp"));
181
+ assert.ok(tokens.includes("vbnet"));
182
+ assert.equal(tokens.includes("java"), false);
183
+ });
184
+
185
+ test("query aliases do not dilute lexical semantic scoring", () => {
186
+ assert.equal(semanticScore(expandQueryTokens(["dashboard"]), "", "status"), 0.85);
187
+ assert.equal(semanticScore(expandQueryTokens(["import"]), "", "imports"), 0.85);
188
+ });
189
+
190
+ test("structural search boost favors matching path and symbol fields", () => {
191
+ const query = "How does the JavaScript and TypeScript parser collect calls and imports?";
192
+ const queryTokens = expandQueryTokens(Array.from(new Set(tokenize(query))));
193
+ const jsImports = makeEntity("js-imports", "Chunk", {
194
+ label: "collectImports",
195
+ path: "scaffold/scripts/parsers/javascript/imports.mjs"
196
+ });
197
+ const cppParser = makeEntity("cpp-parser", "Chunk", {
198
+ label: "parseCode",
199
+ path: "scaffold/scripts/parsers/cpp-treesitter.mjs"
200
+ });
201
+ const csharpParser = makeEntity("csharp-parser", "Chunk", {
202
+ label: "CSharpParser",
203
+ path: "scaffold/scripts/parsers/dotnet/CSharpParser/Program.cs"
204
+ });
205
+
206
+ assert.ok(
207
+ structuralSearchBoost(jsImports, queryTokens, query.toLowerCase()) >
208
+ structuralSearchBoost(cppParser, queryTokens, query.toLowerCase())
209
+ );
210
+ assert.ok(
211
+ structuralSearchBoost(jsImports, queryTokens, query.toLowerCase()) >
212
+ structuralSearchBoost(csharpParser, queryTokens, query.toLowerCase())
213
+ );
214
+ });
215
+
216
+ test("low-semantic graph hubs cannot outrank strong semantic code matches on secondary signals alone", () => {
217
+ const candidates = [
218
+ makeEntity("hub", "Chunk", {
219
+ text: "broad weak query",
220
+ trust_level: 100
221
+ }),
222
+ makeEntity("leaf", "Chunk", {
223
+ text: "precise strong query",
224
+ trust_level: 10
225
+ })
226
+ ];
227
+
228
+ const results = buildSearchResults({
229
+ candidates,
230
+ degreeByEntity: new Map([
231
+ ["hub", 100],
232
+ ["leaf", 0]
233
+ ]),
234
+ queryTokens: ["query"],
235
+ queryPhrase: "query",
236
+ ranking: { semantic: 0.4, graph: 0.25, trust: 0.2, recency: 0.15 },
237
+ includeScores: true,
238
+ includeMatchedRules: false,
239
+ includeContent: false,
240
+ queryVector: null,
241
+ embeddingVectors: new Map(),
242
+ topK: 2,
243
+ minLexicalRelevance: 0,
244
+ minVectorRelevance: 0,
245
+ semanticScorer: (_tokens, _phrase, text) => (text.includes("strong") ? 0.7 : 0.2),
246
+ vectorScorer: () => 0,
247
+ recencyScorer: () => 1,
248
+ structuralSearchBooster: () => 0,
249
+ legacyDataAccessBooster: () => 0
250
+ });
251
+
252
+ assert.equal(results[0].id, "leaf");
253
+ });
254
+
255
+ test("diverse top-k selection admits a near-tie from another path", () => {
256
+ const candidates = [
257
+ makeEntity("a1", "Chunk", { path: "src/a.ts", text: "a1 query" }),
258
+ makeEntity("a2", "Chunk", { path: "src/a.ts", text: "a2 query" }),
259
+ makeEntity("a3", "Chunk", { path: "src/a.ts", text: "a3 query" }),
260
+ makeEntity("b1", "Chunk", { path: "src/b.ts", text: "b1 query" })
261
+ ];
262
+ const semanticById = new Map([
263
+ ["a1", 0.9],
264
+ ["a2", 0.89],
265
+ ["a3", 0.88],
266
+ ["b1", 0.86]
267
+ ]);
268
+
269
+ const results = buildSearchResults({
270
+ candidates,
271
+ degreeByEntity: new Map(),
272
+ queryTokens: ["query"],
273
+ queryPhrase: "query",
274
+ ranking: { semantic: 1, graph: 0, trust: 0, recency: 0 },
275
+ includeScores: true,
276
+ includeMatchedRules: false,
277
+ includeContent: false,
278
+ queryVector: null,
279
+ embeddingVectors: new Map(),
280
+ topK: 3,
281
+ minLexicalRelevance: 0,
282
+ minVectorRelevance: 0,
283
+ semanticScorer: (_tokens, _phrase, text) => semanticById.get(text.split(" ")[0]) ?? 0,
284
+ vectorScorer: () => 0,
285
+ recencyScorer: () => 0,
286
+ structuralSearchBooster: () => 0,
287
+ legacyDataAccessBooster: () => 0
288
+ });
289
+
290
+ assert.deepEqual(results.map((result) => result.id), ["a1", "a2", "b1"]);
291
+ assert.equal(results.some((result) => result.id === "a3"), false);
292
+ assert.deepEqual(results.map((result) => result.score), [0.9, 0.89, 0.86]);
293
+ });
294
+
295
+ test("strong test evidence can outrank a slightly weaker graph hub", () => {
296
+ const candidates = [
297
+ makeEntity("hub", "Chunk", {
298
+ path: "src/hub.ts",
299
+ text: "implementation query",
300
+ trust_level: 50
301
+ }),
302
+ makeEntity("test-evidence", "File", {
303
+ path: "tests/context-regressions.test.mjs",
304
+ text: "regression evidence query",
305
+ trust_level: 50
306
+ })
307
+ ];
308
+
309
+ const results = buildSearchResults({
310
+ candidates,
311
+ degreeByEntity: new Map([
312
+ ["hub", 100],
313
+ ["test-evidence", 0]
314
+ ]),
315
+ queryTokens: ["query"],
316
+ queryPhrase: "query",
317
+ ranking: { semantic: 0.4, graph: 0.25, trust: 0.2, recency: 0.15 },
318
+ includeScores: true,
319
+ includeMatchedRules: false,
320
+ includeContent: false,
321
+ queryVector: null,
322
+ embeddingVectors: new Map(),
323
+ topK: 2,
324
+ minLexicalRelevance: 0,
325
+ minVectorRelevance: 0,
326
+ semanticScorer: (_tokens, _phrase, text) => (text.includes("regression") ? 0.5 : 0.49),
327
+ vectorScorer: () => 0,
328
+ recencyScorer: () => 1,
329
+ structuralSearchBooster: () => 0,
330
+ legacyDataAccessBooster: () => 0
331
+ });
332
+
333
+ assert.equal(results[0].id, "test-evidence");
334
+ });
335
+
169
336
  test("search results can consume a repeatable candidate generator with stats", () => {
170
337
  let passes = 0;
171
338
  const candidateSource = function* () {
@@ -0,0 +1,46 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+
4
+ import { isAllowedEnterpriseEndpoint } from "../dist/core/secure-endpoint.js";
5
+ import { pushMetrics } from "../dist/enterprise/telemetry/sync.js";
6
+ import { syncFromCloud } from "../dist/enterprise/policy/sync.js";
7
+
8
+ test("Enterprise endpoint policy allows TLS and loopback HTTP only", () => {
9
+ assert.equal(isAllowedEnterpriseEndpoint("https://enterprise.example.com"), true);
10
+ assert.equal(isAllowedEnterpriseEndpoint("http://127.0.0.1:8787"), true);
11
+ assert.equal(isAllowedEnterpriseEndpoint("http://localhost:8787"), true);
12
+ assert.equal(isAllowedEnterpriseEndpoint("http://[::1]:8787"), true);
13
+ assert.equal(isAllowedEnterpriseEndpoint("http://external.example.com"), false);
14
+ assert.equal(
15
+ isAllowedEnterpriseEndpoint("https://user:pass@enterprise.example.com"),
16
+ false,
17
+ );
18
+ });
19
+
20
+ test("telemetry and policy send primitives reject external HTTP before attaching a bearer key", async () => {
21
+ let fetches = 0;
22
+ globalThis.fetch = async () => {
23
+ fetches += 1;
24
+ throw new Error("fetch must not be called");
25
+ };
26
+ try {
27
+ const telemetry = await pushMetrics(
28
+ {},
29
+ "http://external.example.com/telemetry",
30
+ "ent_secret_12345678",
31
+ );
32
+ assert.equal(telemetry.success, false);
33
+ assert.match(telemetry.error, /insecure or invalid/);
34
+
35
+ const policy = await syncFromCloud(
36
+ "http://external.example.com/policy",
37
+ "ent_secret_12345678",
38
+ {},
39
+ );
40
+ assert.equal(policy.success, false);
41
+ assert.match(policy.error, /insecure or invalid/);
42
+ assert.equal(fetches, 0);
43
+ } finally {
44
+ globalThis.fetch = undefined;
45
+ }
46
+ });
@@ -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
+ });