@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,16 @@
1
1
  import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
- import { parseFileEntities, parseChunkEntities, resolveModelId, DEFAULT_MODEL_ID } from "../dist/embed.js";
3
+ import {
4
+ COMPACT_FILE_TEXT_STRATEGY,
5
+ COMPACT_FILE_TEXT_TARGET_CHARS,
6
+ COMPACT_FILE_TEXT_THRESHOLD_CHARS,
7
+ DEFAULT_MODEL_ID,
8
+ parseChunkEntities,
9
+ parseFileEntities,
10
+ resolveEmbedTextProfile,
11
+ resolveModelId,
12
+ resolveSignatureProfile
13
+ } from "../dist/embed.js";
4
14
 
5
15
  function fileRecord(content) {
6
16
  return {
@@ -38,6 +48,68 @@ test("file embedding text is uncapped beyond the old 7000-char limit", () => {
38
48
  assert.notEqual(a.signature, b.signature);
39
49
  });
40
50
 
51
+ test("compact-files leaves small file text unchanged", () => {
52
+ const content = "export function smallApi() { return true; }";
53
+
54
+ const [full] = parseFileEntities([fileRecord(content)]);
55
+ const [compact] = parseFileEntities([fileRecord(content)], { textProfile: "compact-files" });
56
+
57
+ assert.equal(compact.text, full.text);
58
+ assert.equal(compact.signature, full.signature);
59
+ assert.equal(compact.text_profile, "compact-files");
60
+ assert.equal(compact.text_compacted, false);
61
+ assert.equal(compact.text_omitted_chars, 0);
62
+ });
63
+
64
+ test("compact-files compacts only large file entities and preserves semantic anchors", () => {
65
+ const head = `HEAD_MARKER\n${"h".repeat(15000)}`;
66
+ const signalLines = [
67
+ "import { Owner } from './owner';",
68
+ "export interface UserRecord { owner: Owner; }",
69
+ "export function createUserRecord(owner: Owner) { return { owner }; }"
70
+ ].join("\n");
71
+ const tail = `${"t".repeat(3000)}\nTAIL_MARKER`;
72
+ const content = `${head}\n${"m".repeat(COMPACT_FILE_TEXT_THRESHOLD_CHARS)}\n${signalLines}\n${tail}`;
73
+
74
+ const [full] = parseFileEntities([fileRecord(content)]);
75
+ const [compact] = parseFileEntities([fileRecord(content)], { textProfile: "compact-files" });
76
+
77
+ assert.equal(compact.text_profile, "compact-files");
78
+ assert.equal(compact.text_compacted, true);
79
+ assert.ok(full.text.length > COMPACT_FILE_TEXT_THRESHOLD_CHARS);
80
+ assert.ok(compact.text.length < full.text.length);
81
+ assert.ok(compact.text.length <= COMPACT_FILE_TEXT_TARGET_CHARS + 5120);
82
+ assert.notEqual(compact.signature, full.signature);
83
+ assert.match(compact.text, new RegExp(COMPACT_FILE_TEXT_STRATEGY));
84
+ assert.match(compact.text, /omitted_chars=/);
85
+ assert.ok(compact.text.includes("src/big.ts"));
86
+ assert.ok(compact.text.includes("excerpt"));
87
+ assert.ok(compact.text.includes("HEAD_MARKER"));
88
+ assert.ok(compact.text.includes("TAIL_MARKER"));
89
+ assert.ok(compact.text.includes("import { Owner } from './owner';"));
90
+ assert.ok(compact.text.includes("export interface UserRecord"));
91
+ assert.ok(compact.text.includes("export function createUserRecord"));
92
+ });
93
+
94
+ test("compact-files keeps collecting signal anchors after an oversized signal line", () => {
95
+ const hugeSignal = `export const generatedRoutes = "${"x".repeat(COMPACT_FILE_TEXT_THRESHOLD_CHARS)}";`;
96
+ const laterSignal = "export function conciseAnchor() { return true; }";
97
+ const content = [
98
+ `HEAD_MARKER\n${"h".repeat(15000)}`,
99
+ hugeSignal,
100
+ laterSignal,
101
+ `${"m".repeat(COMPACT_FILE_TEXT_THRESHOLD_CHARS)}\nTAIL_MARKER`
102
+ ].join("\n");
103
+
104
+ const [compact] = parseFileEntities([fileRecord(content)], { textProfile: "compact-files" });
105
+
106
+ assert.equal(compact.text_compacted, true);
107
+ assert.ok(compact.text.includes("export const generatedRoutes"));
108
+ assert.match(compact.text, /signal_line_truncated_chars=/);
109
+ assert.ok(!compact.text.includes("x".repeat(1024)));
110
+ assert.ok(compact.text.includes(laterSignal));
111
+ });
112
+
41
113
  test("chunk embedding text includes the full body beyond the old 2000-char preview", () => {
42
114
  const filePathById = new Map([["file-1", "src/big.ts"]]);
43
115
  const base = "y".repeat(3000);
@@ -50,6 +122,16 @@ test("chunk embedding text includes the full body beyond the old 2000-char previ
50
122
  assert.notEqual(a.signature, b.signature);
51
123
  });
52
124
 
125
+ test("chunk embedding text keeps full body while file compact profile is enabled", () => {
126
+ const filePathById = new Map([["file-1", "src/big.ts"]]);
127
+ const body = `${"z".repeat(COMPACT_FILE_TEXT_THRESHOLD_CHARS)}CHUNK_TAIL`;
128
+
129
+ const [chunk] = parseChunkEntities([chunkRecord(body)], filePathById);
130
+
131
+ assert.ok(chunk.text.includes(body));
132
+ assert.ok(chunk.text.includes("CHUNK_TAIL"));
133
+ });
134
+
53
135
  test("default embedding model is jina code model and env override still works", () => {
54
136
  const saved = process.env.CORTEX_EMBED_MODEL;
55
137
  try {
@@ -70,3 +152,29 @@ test("default embedding model is jina code model and env override still works",
70
152
  }
71
153
  }
72
154
  });
155
+
156
+ test("embedding text profile resolver accepts only known profiles", () => {
157
+ assert.equal(resolveEmbedTextProfile(undefined), "full");
158
+ assert.equal(resolveEmbedTextProfile(""), "full");
159
+ assert.equal(resolveEmbedTextProfile(" full "), "full");
160
+ assert.equal(resolveEmbedTextProfile(" compact-files "), "compact-files");
161
+ assert.throws(() => resolveEmbedTextProfile("compact"), /Unsupported CORTEX_EMBED_TEXT_PROFILE/);
162
+ });
163
+
164
+ test("signature profile preserves default cache and separates compact-files", () => {
165
+ assert.equal(resolveSignatureProfile(null, "full"), "");
166
+ assert.equal(resolveSignatureProfile(2048, "full"), "embed|max_tokens=2048");
167
+
168
+ const compact = resolveSignatureProfile(null, "compact-files");
169
+ assert.match(compact, /^embed\|/);
170
+ assert.match(compact, /text_profile=compact-files/);
171
+ assert.match(compact, new RegExp(COMPACT_FILE_TEXT_STRATEGY));
172
+ assert.match(compact, new RegExp(`threshold_chars=${COMPACT_FILE_TEXT_THRESHOLD_CHARS}`));
173
+
174
+ const cappedCompact = resolveSignatureProfile(2048, "compact-files");
175
+ assert.match(cappedCompact, /max_tokens=2048/);
176
+ assert.notEqual(cappedCompact, resolveSignatureProfile(2048, "full"));
177
+ assert.equal(resolveSignatureProfile(null, "compact-files", "Chunk"), "");
178
+ assert.equal(resolveSignatureProfile(2048, "compact-files", "Chunk"), "embed|max_tokens=2048");
179
+ assert.equal(resolveSignatureProfile(2048, "compact-files", "File"), cappedCompact);
180
+ });
@@ -0,0 +1,401 @@
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 { runWorkflowSyncOnce } from "../dist/daemon/workflow-sync-checker.js";
8
+ import { runCapabilitySyncOnce } from "../dist/daemon/capability-sync-checker.js";
9
+ import {
10
+ runSkillSyncForCli,
11
+ runSkillSyncOnce,
12
+ } from "../dist/daemon/skill-sync-checker.js";
13
+ import { enterpriseCredentialId } from "../dist/core/license.js";
14
+ import { claimEnterpriseHostIdentity } from "../dist/core/enterprise-host-identity.js";
15
+ import { bindEnterpriseIdentity } from "../dist/cli/enterprise-setup.js";
16
+
17
+ function makeProject(endpoint, apiKey) {
18
+ const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-identity-sync-"));
19
+ fs.mkdirSync(path.join(cwd, ".context"));
20
+ fs.writeFileSync(
21
+ path.join(cwd, ".context", "enterprise.yml"),
22
+ [
23
+ "enterprise:",
24
+ ` api_key: ${apiKey}`,
25
+ ` endpoint: ${endpoint}`,
26
+ "",
27
+ ].join("\n"),
28
+ "utf8",
29
+ );
30
+ return cwd;
31
+ }
32
+
33
+ test("a merely opened repository cannot claim the user-global Enterprise identity", async () => {
34
+ const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-identity-home-"));
35
+ const project = makeProject(
36
+ "https://untrusted-repo.example.com",
37
+ "ent_untrusted_repo_12345678",
38
+ );
39
+ process.env.HOME = homeDir;
40
+ let fetches = 0;
41
+ globalThis.fetch = async () => {
42
+ fetches += 1;
43
+ throw new Error("fetch must not occur");
44
+ };
45
+ try {
46
+ const outcome = await runSkillSyncForCli(project, "codex");
47
+ assert.equal(outcome.kind, "failed");
48
+ assert.match(outcome.error, /identity conflict/);
49
+ assert.equal(fetches, 0);
50
+ assert.equal(
51
+ fs.existsSync(
52
+ path.join(homeDir, ".cortex", "enterprise-host-identity.json"),
53
+ ),
54
+ false,
55
+ );
56
+ } finally {
57
+ delete process.env.HOME;
58
+ globalThis.fetch = undefined;
59
+ fs.rmSync(homeDir, { recursive: true, force: true });
60
+ fs.rmSync(project, { recursive: true, force: true });
61
+ }
62
+ });
63
+
64
+ test("organization workflow and capability caches reject a second Enterprise credential before fetch", async () => {
65
+ const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-identity-home-"));
66
+ const first = makeProject(
67
+ "https://identity-a.example.com",
68
+ "ent_identity_a_12345678",
69
+ );
70
+ const second = makeProject(
71
+ "https://identity-b.example.com",
72
+ "ent_identity_b_12345678",
73
+ );
74
+ process.env.HOME = homeDir;
75
+ assert.equal(
76
+ claimEnterpriseHostIdentity(
77
+ enterpriseCredentialId(
78
+ "https://identity-a.example.com",
79
+ "ent_identity_a_12345678",
80
+ ),
81
+ "https://identity-a.example.com",
82
+ ),
83
+ true,
84
+ );
85
+ let fetches = 0;
86
+ globalThis.fetch = async (input) => {
87
+ fetches += 1;
88
+ const url = input instanceof URL ? input : new URL(input);
89
+ if (url.pathname.endsWith("/workflows/manifest")) {
90
+ return {
91
+ ok: true,
92
+ status: 200,
93
+ statusText: "OK",
94
+ json: async () => ({ workflows: [] }),
95
+ };
96
+ }
97
+ if (url.pathname.endsWith("/capabilities/manifest")) {
98
+ return {
99
+ ok: true,
100
+ status: 200,
101
+ statusText: "OK",
102
+ json: async () => ({ capabilities: [] }),
103
+ };
104
+ }
105
+ throw new Error(`Unexpected fetch: ${url}`);
106
+ };
107
+
108
+ try {
109
+ assert.equal((await runWorkflowSyncOnce(first)).kind, "unchanged");
110
+ assert.equal((await runCapabilitySyncOnce(first)).kind, "unchanged");
111
+ assert.equal(fetches, 2);
112
+
113
+ const workflowStatePath = path.join(
114
+ homeDir,
115
+ ".cortex",
116
+ "workflows.local.json",
117
+ );
118
+ const capabilityStatePath = path.join(
119
+ homeDir,
120
+ ".cortex",
121
+ "capabilities.local.json",
122
+ );
123
+ const workflowStateBefore = fs.readFileSync(workflowStatePath, "utf8");
124
+ const capabilityStateBefore = fs.readFileSync(capabilityStatePath, "utf8");
125
+
126
+ const workflowConflict = await runWorkflowSyncOnce(second);
127
+ const capabilityConflict = await runCapabilitySyncOnce(second);
128
+ const skillConflict = await runSkillSyncForCli(second, "codex");
129
+ assert.equal(workflowConflict.kind, "failed");
130
+ assert.match(workflowConflict.error, /identity conflict/);
131
+ assert.equal(capabilityConflict.kind, "failed");
132
+ assert.match(capabilityConflict.error, /identity conflict/);
133
+ assert.equal(skillConflict.kind, "failed");
134
+ assert.match(skillConflict.error, /identity conflict/);
135
+ assert.equal(fetches, 2);
136
+ assert.equal(fs.readFileSync(workflowStatePath, "utf8"), workflowStateBefore);
137
+ assert.equal(
138
+ fs.readFileSync(capabilityStatePath, "utf8"),
139
+ capabilityStateBefore,
140
+ );
141
+ } finally {
142
+ delete process.env.HOME;
143
+ globalThis.fetch = undefined;
144
+ fs.rmSync(homeDir, { recursive: true, force: true });
145
+ fs.rmSync(first, { recursive: true, force: true });
146
+ fs.rmSync(second, { recursive: true, force: true });
147
+ }
148
+ });
149
+
150
+ test("an explicit same-endpoint credential rotation rebinds organization caches", async () => {
151
+ const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-identity-home-"));
152
+ const endpoint = "https://identity-rotation.example.com";
153
+ const firstKey = "ent_identity_first_12345678";
154
+ const secondKey = "ent_identity_second_12345678";
155
+ const first = makeProject(endpoint, firstKey);
156
+ const second = makeProject(endpoint, secondKey);
157
+ process.env.HOME = homeDir;
158
+ const seenAuthorization = [];
159
+ const updatedAt = "2026-07-28T10:00:00.000Z";
160
+ globalThis.fetch = async (input, init = {}) => {
161
+ const authorization = init.headers?.Authorization;
162
+ seenAuthorization.push(authorization);
163
+ const organization =
164
+ authorization === `Bearer ${secondKey}` ? "Organization B" : "Organization A";
165
+ const url = input instanceof URL ? input : new URL(input);
166
+ if (url.pathname.endsWith("/skills/manifest")) {
167
+ const cli = url.searchParams.get("cli");
168
+ return {
169
+ ok: true,
170
+ status: 200,
171
+ statusText: "OK",
172
+ json: async () => ({
173
+ skills: [{
174
+ name: `rotation-${cli}`,
175
+ scope: `cli:${cli}`,
176
+ updated_at: updatedAt,
177
+ }],
178
+ }),
179
+ };
180
+ }
181
+ if (
182
+ url.pathname.endsWith("/skills/rotation-claude") ||
183
+ url.pathname.endsWith("/skills/rotation-codex")
184
+ ) {
185
+ const cli = url.pathname.endsWith("rotation-claude")
186
+ ? "claude"
187
+ : "codex";
188
+ return {
189
+ ok: true,
190
+ status: 200,
191
+ statusText: "OK",
192
+ text: async () => `# ${organization} ${cli} skill\n`,
193
+ };
194
+ }
195
+ if (url.pathname.endsWith("/workflows/manifest")) {
196
+ return {
197
+ ok: true,
198
+ status: 200,
199
+ statusText: "OK",
200
+ json: async () => ({
201
+ workflows: [{
202
+ workflow_id: "rotation-flow",
203
+ version: 1,
204
+ updated_at: updatedAt,
205
+ }],
206
+ }),
207
+ };
208
+ }
209
+ if (url.pathname.endsWith("/workflows/rotation-flow")) {
210
+ return {
211
+ ok: true,
212
+ status: 200,
213
+ statusText: "OK",
214
+ json: async () => ({
215
+ workflow: {
216
+ workflow_id: "rotation-flow",
217
+ description: organization,
218
+ version: 1,
219
+ updated_at: updatedAt,
220
+ definition: {
221
+ id: "rotation-flow",
222
+ description: organization,
223
+ version: 1,
224
+ stages: [{
225
+ name: "review",
226
+ artifact: "review.md",
227
+ reads: [],
228
+ required_fields: [],
229
+ capability: "reviewer",
230
+ description: organization,
231
+ }],
232
+ },
233
+ },
234
+ }),
235
+ };
236
+ }
237
+ if (url.pathname.endsWith("/capabilities/manifest")) {
238
+ return {
239
+ ok: true,
240
+ status: 200,
241
+ statusText: "OK",
242
+ json: async () => ({
243
+ capabilities: [{
244
+ capability_name: "rotation-capability",
245
+ updated_at: updatedAt,
246
+ }],
247
+ }),
248
+ };
249
+ }
250
+ if (url.pathname.endsWith("/capabilities/rotation-capability")) {
251
+ return {
252
+ ok: true,
253
+ status: 200,
254
+ statusText: "OK",
255
+ json: async () => ({
256
+ capability: {
257
+ capability_name: "rotation-capability",
258
+ description: organization,
259
+ updated_at: updatedAt,
260
+ definition: {
261
+ name: "rotation-capability",
262
+ description: organization,
263
+ read_globs: ["**"],
264
+ write_globs: [],
265
+ tools_allowed: [],
266
+ },
267
+ },
268
+ }),
269
+ };
270
+ }
271
+ throw new Error(`Unexpected fetch: ${url}`);
272
+ };
273
+
274
+ try {
275
+ assert.equal(
276
+ claimEnterpriseHostIdentity(
277
+ enterpriseCredentialId(endpoint, firstKey),
278
+ endpoint,
279
+ ),
280
+ true,
281
+ );
282
+ assert.deepEqual(
283
+ (await runSkillSyncOnce(first, ["claude", "codex"])).map(
284
+ (outcome) => outcome.kind,
285
+ ),
286
+ ["synced", "synced"],
287
+ );
288
+ assert.equal((await runWorkflowSyncOnce(first)).kind, "synced");
289
+ assert.equal((await runCapabilitySyncOnce(first)).kind, "synced");
290
+
291
+ assert.equal(
292
+ bindEnterpriseIdentity({ apiKey: secondKey, endpoint }),
293
+ true,
294
+ );
295
+ for (const cli of ["claude", "codex"]) {
296
+ assert.equal(
297
+ fs.existsSync(
298
+ path.join(
299
+ homeDir,
300
+ cli === "codex" ? ".codex" : ".claude",
301
+ "skills",
302
+ `rotation-${cli}`,
303
+ ),
304
+ ),
305
+ false,
306
+ `old ${cli} skill must be purged before identity replacement`,
307
+ );
308
+ }
309
+ for (const file of [
310
+ "skills.local.json",
311
+ "workflows.local.json",
312
+ "capabilities.local.json",
313
+ ]) {
314
+ assert.equal(
315
+ fs.existsSync(path.join(homeDir, ".cortex", file)),
316
+ false,
317
+ `${file} must be invalidated before identity replacement`,
318
+ );
319
+ }
320
+ const skillRotation = await runSkillSyncOnce(
321
+ second,
322
+ ["claude", "codex"],
323
+ );
324
+ const workflowRotation = await runWorkflowSyncOnce(second);
325
+ const capabilityRotation = await runCapabilitySyncOnce(second);
326
+ assert.deepEqual(
327
+ skillRotation.map((outcome) => ({
328
+ kind: outcome.kind,
329
+ changed: outcome.kind === "synced" ? outcome.changed : [],
330
+ })),
331
+ [
332
+ { kind: "synced", changed: [] },
333
+ { kind: "synced", changed: [] },
334
+ ],
335
+ );
336
+ assert.deepEqual(skillRotation[0].added, ["rotation-claude"]);
337
+ assert.deepEqual(skillRotation[1].added, ["rotation-codex"]);
338
+ assert.equal(workflowRotation.kind, "synced");
339
+ assert.deepEqual(workflowRotation.added, ["rotation-flow"]);
340
+ assert.equal(capabilityRotation.kind, "synced");
341
+ assert.deepEqual(capabilityRotation.added, ["rotation-capability"]);
342
+ assert.deepEqual(
343
+ seenAuthorization.slice(-8),
344
+ Array(8).fill(`Bearer ${secondKey}`),
345
+ );
346
+
347
+ for (const file of [
348
+ "skills.local.json",
349
+ "workflows.local.json",
350
+ "capabilities.local.json",
351
+ ]) {
352
+ const state = JSON.parse(
353
+ fs.readFileSync(path.join(homeDir, ".cortex", file), "utf8"),
354
+ );
355
+ assert.equal(
356
+ state.credential_id,
357
+ enterpriseCredentialId(endpoint, secondKey),
358
+ );
359
+ }
360
+ const workflowState = JSON.parse(
361
+ fs.readFileSync(
362
+ path.join(homeDir, ".cortex", "workflows.local.json"),
363
+ "utf8",
364
+ ),
365
+ );
366
+ assert.equal(
367
+ workflowState.workflows["rotation-flow"].definition.description,
368
+ "Organization B",
369
+ );
370
+ const capabilityState = JSON.parse(
371
+ fs.readFileSync(
372
+ path.join(homeDir, ".cortex", "capabilities.local.json"),
373
+ "utf8",
374
+ ),
375
+ );
376
+ assert.equal(
377
+ capabilityState.capabilities["rotation-capability"].definition.description,
378
+ "Organization B",
379
+ );
380
+ for (const cli of ["claude", "codex"]) {
381
+ const skillBody = fs.readFileSync(
382
+ path.join(
383
+ homeDir,
384
+ cli === "codex" ? ".codex" : ".claude",
385
+ "skills",
386
+ `rotation-${cli}`,
387
+ "SKILL.md",
388
+ ),
389
+ "utf8",
390
+ );
391
+ assert.match(skillBody, /Organization B/);
392
+ assert.doesNotMatch(skillBody, /Organization A/);
393
+ }
394
+ } finally {
395
+ delete process.env.HOME;
396
+ globalThis.fetch = undefined;
397
+ fs.rmSync(homeDir, { recursive: true, force: true });
398
+ fs.rmSync(first, { recursive: true, force: true });
399
+ fs.rmSync(second, { recursive: true, force: true });
400
+ }
401
+ });