@danielblomma/cortex-mcp 2.4.0 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/CHANGELOG.md +201 -0
  2. package/README.md +19 -3
  3. package/bin/cortex.mjs +308 -75
  4. package/bin/daemon-control.mjs +162 -0
  5. package/package.json +7 -3
  6. package/scaffold/mcp/dist/.cortex-build-hash +1 -0
  7. package/scaffold/mcp/dist/cli/enterprise-setup.js +134 -0
  8. package/scaffold/mcp/dist/cli/govern.js +937 -0
  9. package/scaffold/mcp/dist/cli/query.js +409 -0
  10. package/scaffold/mcp/dist/cli/run.js +295 -0
  11. package/scaffold/mcp/dist/cli/stage.js +308 -0
  12. package/scaffold/mcp/dist/cli/telemetry-test.js +141 -0
  13. package/scaffold/mcp/dist/cli/ungoverned-detector.js +133 -0
  14. package/scaffold/mcp/dist/contextEntities.js +282 -0
  15. package/scaffold/mcp/dist/core/audit/query.js +72 -0
  16. package/scaffold/mcp/dist/core/audit/writer.js +28 -0
  17. package/scaffold/mcp/dist/core/config.js +235 -0
  18. package/scaffold/mcp/dist/core/enterprise-host-identity.js +193 -0
  19. package/scaffold/mcp/dist/core/enterprise-identity.js +18 -0
  20. package/scaffold/mcp/dist/core/enterprise-rotation.js +158 -0
  21. package/scaffold/mcp/dist/core/govern-paths.js +21 -0
  22. package/scaffold/mcp/dist/core/index.js +14 -0
  23. package/scaffold/mcp/dist/core/license.js +278 -0
  24. package/scaffold/mcp/dist/core/policy/enforce.js +66 -0
  25. package/scaffold/mcp/dist/core/policy/injection.js +172 -0
  26. package/scaffold/mcp/dist/core/policy/store.js +179 -0
  27. package/scaffold/mcp/dist/core/rbac/check.js +30 -0
  28. package/scaffold/mcp/dist/core/secure-endpoint.js +25 -0
  29. package/scaffold/mcp/dist/core/telemetry/collector.js +285 -0
  30. package/scaffold/mcp/dist/core/telemetry/state-dir.js +31 -0
  31. package/scaffold/mcp/dist/core/validators/builtins.js +632 -0
  32. package/scaffold/mcp/dist/core/validators/config.js +44 -0
  33. package/scaffold/mcp/dist/core/validators/engine.js +120 -0
  34. package/scaffold/mcp/dist/core/validators/evaluators/code_comments.js +236 -0
  35. package/scaffold/mcp/dist/core/validators/evaluators/regex.js +116 -0
  36. package/scaffold/mcp/dist/core/workflow/artifact-io.js +103 -0
  37. package/scaffold/mcp/dist/core/workflow/capabilities.js +88 -0
  38. package/scaffold/mcp/dist/core/workflow/default-workflows.js +102 -0
  39. package/scaffold/mcp/dist/core/workflow/enforcement.js +164 -0
  40. package/scaffold/mcp/dist/core/workflow/envelope.js +132 -0
  41. package/scaffold/mcp/dist/core/workflow/index.js +11 -0
  42. package/scaffold/mcp/dist/core/workflow/mcp-tools.js +175 -0
  43. package/scaffold/mcp/dist/core/workflow/resolution.js +69 -0
  44. package/scaffold/mcp/dist/core/workflow/run-lifecycle.js +123 -0
  45. package/scaffold/mcp/dist/core/workflow/schemas.js +141 -0
  46. package/scaffold/mcp/dist/core/workflow/synced-capability-registry.js +60 -0
  47. package/scaffold/mcp/dist/core/workflow/synced-registry.js +60 -0
  48. package/scaffold/mcp/dist/daemon/capability-sync-checker.js +242 -0
  49. package/scaffold/mcp/dist/daemon/client.js +130 -0
  50. package/scaffold/mcp/dist/daemon/egress-proxy.js +288 -0
  51. package/scaffold/mcp/dist/daemon/global-host-events.js +222 -0
  52. package/scaffold/mcp/dist/daemon/heartbeat-pusher.js +116 -0
  53. package/scaffold/mcp/dist/daemon/heartbeat-tracker.js +165 -0
  54. package/scaffold/mcp/dist/daemon/host-events-pusher.js +249 -0
  55. package/scaffold/mcp/dist/daemon/main.js +449 -0
  56. package/scaffold/mcp/dist/daemon/paths.js +36 -0
  57. package/scaffold/mcp/dist/daemon/project-service-registry.js +78 -0
  58. package/scaffold/mcp/dist/daemon/protocol.js +8 -0
  59. package/scaffold/mcp/dist/daemon/server.js +180 -0
  60. package/scaffold/mcp/dist/daemon/skill-sync-checker.js +557 -0
  61. package/scaffold/mcp/dist/daemon/sync-checker.js +165 -0
  62. package/scaffold/mcp/dist/daemon/ungoverned-scanner.js +136 -0
  63. package/scaffold/mcp/dist/daemon/workflow-sync-checker.js +249 -0
  64. package/scaffold/mcp/dist/defaults.js +6 -0
  65. package/scaffold/mcp/dist/embed.js +627 -0
  66. package/scaffold/mcp/dist/embedScheduler.js +479 -0
  67. package/scaffold/mcp/dist/embeddings.js +167 -0
  68. package/scaffold/mcp/dist/enterprise/audit/push.js +66 -0
  69. package/scaffold/mcp/dist/enterprise/index.js +327 -0
  70. package/scaffold/mcp/dist/enterprise/model/deploy.js +27 -0
  71. package/scaffold/mcp/dist/enterprise/policy/sync.js +129 -0
  72. package/scaffold/mcp/dist/enterprise/privacy/boundary.js +184 -0
  73. package/scaffold/mcp/dist/enterprise/reviews/changed-files.js +33 -0
  74. package/scaffold/mcp/dist/enterprise/reviews/pattern-context.js +202 -0
  75. package/scaffold/mcp/dist/enterprise/reviews/policy-selection.js +46 -0
  76. package/scaffold/mcp/dist/enterprise/reviews/push.js +102 -0
  77. package/scaffold/mcp/dist/enterprise/reviews/trust-state.js +186 -0
  78. package/scaffold/mcp/dist/enterprise/telemetry/sync.js +57 -0
  79. package/scaffold/mcp/dist/enterprise/tools/enterprise.js +826 -0
  80. package/scaffold/mcp/dist/enterprise/tools/harness.js +40 -0
  81. package/scaffold/mcp/dist/enterprise/tools/walk.js +73 -0
  82. package/scaffold/mcp/dist/enterprise/violations/push.js +77 -0
  83. package/scaffold/mcp/dist/enterprise/workflow/push.js +44 -0
  84. package/scaffold/mcp/dist/enterprise/workflow/state.js +329 -0
  85. package/scaffold/mcp/dist/frontmatter.js +33 -0
  86. package/scaffold/mcp/dist/graph.js +769 -0
  87. package/scaffold/mcp/dist/graphCsv.js +55 -0
  88. package/scaffold/mcp/dist/graphMetrics.js +8 -0
  89. package/scaffold/mcp/dist/hooks/permission-request.js +89 -0
  90. package/scaffold/mcp/dist/hooks/post-tool-use.js +105 -0
  91. package/scaffold/mcp/dist/hooks/pre-compact.js +29 -0
  92. package/scaffold/mcp/dist/hooks/pre-tool-use.js +78 -0
  93. package/scaffold/mcp/dist/hooks/session-end.js +43 -0
  94. package/scaffold/mcp/dist/hooks/session-start.js +41 -0
  95. package/scaffold/mcp/dist/hooks/shared.js +194 -0
  96. package/scaffold/mcp/dist/hooks/stop.js +28 -0
  97. package/scaffold/mcp/dist/hooks/user-prompt-submit.js +33 -0
  98. package/scaffold/mcp/dist/impactPresentation.js +137 -0
  99. package/scaffold/mcp/dist/impactRanking.js +191 -0
  100. package/scaffold/mcp/dist/impactResponse.js +30 -0
  101. package/scaffold/mcp/dist/impactResults.js +105 -0
  102. package/scaffold/mcp/dist/impactSeed.js +20 -0
  103. package/scaffold/mcp/dist/impactTraversal.js +64 -0
  104. package/scaffold/mcp/dist/jsonl.js +77 -0
  105. package/scaffold/mcp/dist/loadGraph.js +759 -0
  106. package/scaffold/mcp/dist/lruCache.js +38 -0
  107. package/scaffold/mcp/dist/paths.js +97 -0
  108. package/scaffold/mcp/dist/patternEvidence.js +272 -0
  109. package/scaffold/mcp/dist/plugin.js +81 -0
  110. package/scaffold/mcp/dist/presets.js +78 -0
  111. package/scaffold/mcp/dist/relatedResponse.js +18 -0
  112. package/scaffold/mcp/dist/relatedTraversal.js +78 -0
  113. package/scaffold/mcp/dist/rules.js +23 -0
  114. package/scaffold/mcp/dist/search.js +212 -0
  115. package/scaffold/mcp/dist/searchCore.js +457 -0
  116. package/scaffold/mcp/dist/searchResults.js +230 -0
  117. package/scaffold/mcp/dist/server.js +317 -0
  118. package/scaffold/mcp/dist/types.js +1 -0
  119. package/scaffold/mcp/package-lock.json +321 -200
  120. package/scaffold/mcp/package.json +15 -6
  121. package/scaffold/mcp/src/cli/enterprise-setup.ts +82 -8
  122. package/scaffold/mcp/src/cli/govern.ts +137 -52
  123. package/scaffold/mcp/src/cli/run.ts +53 -19
  124. package/scaffold/mcp/src/cli/stage.ts +8 -3
  125. package/scaffold/mcp/src/core/config.ts +9 -2
  126. package/scaffold/mcp/src/core/enterprise-host-identity.ts +259 -0
  127. package/scaffold/mcp/src/core/enterprise-identity.ts +20 -0
  128. package/scaffold/mcp/src/core/enterprise-rotation.ts +185 -0
  129. package/scaffold/mcp/src/core/govern-paths.ts +30 -0
  130. package/scaffold/mcp/src/core/license.ts +186 -17
  131. package/scaffold/mcp/src/core/secure-endpoint.ts +23 -0
  132. package/scaffold/mcp/src/core/workflow/enforcement.ts +8 -1
  133. package/scaffold/mcp/src/core/workflow/mcp-tools.ts +3 -0
  134. package/scaffold/mcp/src/core/workflow/resolution.ts +9 -1
  135. package/scaffold/mcp/src/core/workflow/synced-capability-registry.ts +15 -0
  136. package/scaffold/mcp/src/core/workflow/synced-registry.ts +15 -0
  137. package/scaffold/mcp/src/daemon/capability-sync-checker.ts +38 -2
  138. package/scaffold/mcp/src/daemon/egress-proxy.ts +14 -8
  139. package/scaffold/mcp/src/daemon/global-host-events.ts +288 -0
  140. package/scaffold/mcp/src/daemon/heartbeat-pusher.ts +4 -0
  141. package/scaffold/mcp/src/daemon/heartbeat-tracker.ts +2 -0
  142. package/scaffold/mcp/src/daemon/host-events-pusher.ts +5 -0
  143. package/scaffold/mcp/src/daemon/main.ts +99 -25
  144. package/scaffold/mcp/src/daemon/project-service-registry.ts +107 -0
  145. package/scaffold/mcp/src/daemon/skill-sync-checker.ts +368 -31
  146. package/scaffold/mcp/src/daemon/sync-checker.ts +4 -0
  147. package/scaffold/mcp/src/daemon/ungoverned-scanner.ts +57 -25
  148. package/scaffold/mcp/src/daemon/workflow-sync-checker.ts +41 -2
  149. package/scaffold/mcp/src/enterprise/audit/push.ts +8 -0
  150. package/scaffold/mcp/src/enterprise/policy/sync.ts +12 -0
  151. package/scaffold/mcp/src/enterprise/reviews/push.ts +9 -0
  152. package/scaffold/mcp/src/enterprise/reviews/trust-state.ts +3 -1
  153. package/scaffold/mcp/src/enterprise/telemetry/sync.ts +9 -0
  154. package/scaffold/mcp/src/enterprise/violations/push.ts +9 -0
  155. package/scaffold/mcp/src/enterprise/workflow/push.ts +7 -0
  156. package/scaffold/mcp/src/plugin.ts +20 -0
  157. package/scaffold/mcp/tests/copilot-shim.test.mjs +19 -1
  158. package/scaffold/mcp/tests/egress-proxy.test.mjs +37 -0
  159. package/scaffold/mcp/tests/enterprise-identity-sync.test.mjs +401 -0
  160. package/scaffold/mcp/tests/enterprise-setup.test.mjs +189 -0
  161. package/scaffold/mcp/tests/global-host-events.test.mjs +81 -0
  162. package/scaffold/mcp/tests/govern-install.test.mjs +95 -2
  163. package/scaffold/mcp/tests/govern-repair.test.mjs +20 -3
  164. package/scaffold/mcp/tests/heartbeat-tracker.test.mjs +26 -0
  165. package/scaffold/mcp/tests/license.test.mjs +367 -0
  166. package/scaffold/mcp/tests/project-service-registry.test.mjs +172 -0
  167. package/scaffold/mcp/tests/review-trust-contract.test.mjs +18 -0
  168. package/scaffold/mcp/tests/secure-enterprise-endpoint.test.mjs +46 -0
  169. package/scaffold/mcp/tests/skill-sync-checker.test.mjs +446 -2
  170. package/scaffold/mcp/tests/ungoverned-scanner.test.mjs +104 -22
  171. package/scaffold/mcp/tests/workflow-cli.test.mjs +14 -1
  172. package/scaffold/mcp/tests/workflow-synced-capabilities.test.mjs +37 -0
  173. package/scaffold/mcp/tests/workflow-synced-registry.test.mjs +40 -1
@@ -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
+ });
@@ -0,0 +1,189 @@
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 {
8
+ bindEnterpriseIdentity,
9
+ runEnterpriseSetup,
10
+ } from "../dist/cli/enterprise-setup.js";
11
+
12
+ function makeWorkspace() {
13
+ const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-enterprise-setup-"));
14
+ fs.mkdirSync(path.join(cwd, ".context"), { recursive: true });
15
+ const homeDir = path.join(cwd, "home");
16
+ fs.mkdirSync(homeDir);
17
+ process.env.HOME = homeDir;
18
+ return cwd;
19
+ }
20
+
21
+ function stubValidLicense() {
22
+ globalThis.fetch = async () => ({
23
+ ok: true,
24
+ status: 200,
25
+ json: async () => ({
26
+ valid: true,
27
+ edition: "enterprise",
28
+ features: ["govern"],
29
+ expires_at: "2099-01-01T00:00:00.000Z",
30
+ max_repos: 10,
31
+ }),
32
+ });
33
+ }
34
+
35
+ test.afterEach(() => {
36
+ delete process.env.HOME;
37
+ globalThis.fetch = undefined;
38
+ });
39
+
40
+ test("runEnterpriseSetup: atomically creates enterprise.yml with mode 0600", async () => {
41
+ const cwd = makeWorkspace();
42
+ stubValidLicense();
43
+
44
+ try {
45
+ const result = await runEnterpriseSetup({
46
+ cwd,
47
+ endpoint: "https://licenses.example.com",
48
+ apiKey: "ent_secure_12345678",
49
+ });
50
+ assert.equal(result.ok, true);
51
+ assert.ok(result.configPath);
52
+ assert.equal(fs.statSync(result.configPath).mode & 0o777, 0o600);
53
+ assert.match(fs.readFileSync(result.configPath, "utf8"), /ent_secure_12345678/);
54
+ assert.equal(
55
+ fs.existsSync(
56
+ path.join(process.env.HOME, ".cortex", "enterprise-host-identity.json"),
57
+ ),
58
+ false,
59
+ "license/config validation must not implicitly enroll host-global state",
60
+ );
61
+ assert.deepEqual(
62
+ fs.readdirSync(path.join(cwd, ".context")).filter((name) => name.endsWith(".tmp")),
63
+ [],
64
+ );
65
+ } finally {
66
+ fs.rmSync(cwd, { recursive: true, force: true });
67
+ }
68
+ });
69
+
70
+ test("bindEnterpriseIdentity creates a user-owned marker and permits explicit same-endpoint rotation", () => {
71
+ const cwd = makeWorkspace();
72
+ const homeDir = process.env.HOME;
73
+ const endpoint = "https://licenses.example.com";
74
+ try {
75
+ assert.equal(
76
+ bindEnterpriseIdentity({
77
+ endpoint,
78
+ apiKey: "ent_first_rotation_12345678",
79
+ }),
80
+ true,
81
+ );
82
+ const markerPath = path.join(
83
+ homeDir,
84
+ ".cortex",
85
+ "enterprise-host-identity.json",
86
+ );
87
+ assert.equal(fs.statSync(markerPath).mode & 0o777, 0o600);
88
+ assert.equal(fs.statSync(markerPath).uid, process.getuid());
89
+ const first = JSON.parse(fs.readFileSync(markerPath, "utf8"));
90
+
91
+ assert.equal(
92
+ bindEnterpriseIdentity({
93
+ endpoint,
94
+ apiKey: "ent_second_rotation_12345678",
95
+ }),
96
+ true,
97
+ );
98
+ const second = JSON.parse(fs.readFileSync(markerPath, "utf8"));
99
+ assert.notEqual(second.credential_id, first.credential_id);
100
+ assert.equal(second.endpoint_sha256, first.endpoint_sha256);
101
+
102
+ assert.equal(
103
+ bindEnterpriseIdentity({
104
+ endpoint: "https://another-org.example.com",
105
+ apiKey: "ent_other_endpoint_12345678",
106
+ }),
107
+ false,
108
+ );
109
+ assert.deepEqual(JSON.parse(fs.readFileSync(markerPath, "utf8")), second);
110
+ } finally {
111
+ fs.rmSync(cwd, { recursive: true, force: true });
112
+ }
113
+ });
114
+
115
+ test("bindEnterpriseIdentity refuses a symlinked user-global Cortex directory", () => {
116
+ const cwd = makeWorkspace();
117
+ const homeDir = process.env.HOME;
118
+ const outside = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-host-outside-"));
119
+ fs.symlinkSync(outside, path.join(homeDir, ".cortex"), "dir");
120
+ try {
121
+ assert.equal(
122
+ bindEnterpriseIdentity({
123
+ endpoint: "https://licenses.example.com",
124
+ apiKey: "ent_symlink_guard_12345678",
125
+ }),
126
+ false,
127
+ );
128
+ assert.equal(
129
+ fs.existsSync(path.join(outside, "enterprise-host-identity.json")),
130
+ false,
131
+ );
132
+ } finally {
133
+ fs.rmSync(cwd, { recursive: true, force: true });
134
+ fs.rmSync(outside, { recursive: true, force: true });
135
+ }
136
+ });
137
+
138
+ test("runEnterpriseSetup: replacing a permissive config leaves mode 0600", async () => {
139
+ const cwd = makeWorkspace();
140
+ const configPath = path.join(cwd, ".context", "enterprise.yml");
141
+ fs.writeFileSync(configPath, "old", { encoding: "utf8", mode: 0o644 });
142
+ fs.chmodSync(configPath, 0o644);
143
+ stubValidLicense();
144
+
145
+ try {
146
+ const result = await runEnterpriseSetup({
147
+ cwd,
148
+ endpoint: "https://licenses.example.com",
149
+ apiKey: "ent_replaced_12345678",
150
+ });
151
+ assert.equal(result.ok, true);
152
+ assert.equal(fs.statSync(configPath).mode & 0o777, 0o600);
153
+ assert.doesNotMatch(fs.readFileSync(configPath, "utf8"), /^old$/);
154
+ } finally {
155
+ fs.rmSync(cwd, { recursive: true, force: true });
156
+ }
157
+ });
158
+
159
+ test("runEnterpriseSetup: rejects non-loopback HTTP before transmitting the API key", async () => {
160
+ const cwd = makeWorkspace();
161
+ let calls = 0;
162
+ globalThis.fetch = async () => {
163
+ calls += 1;
164
+ return {
165
+ ok: true,
166
+ status: 200,
167
+ json: async () => ({
168
+ valid: true,
169
+ edition: "enterprise",
170
+ features: ["govern"],
171
+ expires_at: "2099-01-01T00:00:00.000Z",
172
+ max_repos: 10,
173
+ }),
174
+ };
175
+ };
176
+
177
+ try {
178
+ const result = await runEnterpriseSetup({
179
+ cwd,
180
+ endpoint: "http://licenses.example.com",
181
+ apiKey: "ent_secure_12345678",
182
+ });
183
+ assert.equal(result.ok, false);
184
+ assert.match(result.message, /must use HTTPS/);
185
+ assert.equal(calls, 0);
186
+ } finally {
187
+ fs.rmSync(cwd, { recursive: true, force: true });
188
+ }
189
+ });
@@ -0,0 +1,81 @@
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 {
8
+ pushGlobalUngovernedEvents,
9
+ writeGlobalUngovernedEvent,
10
+ } from "../dist/daemon/global-host-events.js";
11
+ import { claimEnterpriseHostIdentity } from "../dist/core/enterprise-host-identity.js";
12
+ import { enterpriseCredentialId } from "../dist/core/license.js";
13
+
14
+ test("global host events push only under their enrolled credential", async () => {
15
+ const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-global-push-"));
16
+ const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-global-home-"));
17
+ const endpoint = "https://govern.example.com";
18
+ const apiKey = "ent_global_push_12345678";
19
+ const credentialId = enterpriseCredentialId(endpoint, apiKey);
20
+ fs.mkdirSync(path.join(cwd, ".context"));
21
+ fs.writeFileSync(
22
+ path.join(cwd, ".context", "enterprise.yml"),
23
+ [
24
+ "enterprise:",
25
+ ` api_key: ${apiKey}`,
26
+ ` endpoint: ${endpoint}`,
27
+ "",
28
+ ].join("\n"),
29
+ );
30
+ process.env.HOME = homeDir;
31
+ const requests = [];
32
+ globalThis.fetch = async (input, init) => {
33
+ requests.push({ input: String(input), init });
34
+ return { ok: true, status: 200, statusText: "OK" };
35
+ };
36
+
37
+ try {
38
+ assert.equal(claimEnterpriseHostIdentity(credentialId, endpoint), true);
39
+ assert.equal(
40
+ writeGlobalUngovernedEvent(credentialId, {
41
+ event_type: "ungoverned_ai_session_detected",
42
+ timestamp: "2026-07-28T10:00:00.000Z",
43
+ host_id: "host-1",
44
+ cli: "claude",
45
+ binary: "/usr/local/bin/claude",
46
+ pid: 42,
47
+ ppid: 1,
48
+ user: "alice",
49
+ args: "claude --prompt secret",
50
+ parent_chain: [],
51
+ mode: "advisory",
52
+ action: "logged",
53
+ }),
54
+ true,
55
+ );
56
+
57
+ assert.deepEqual(
58
+ await pushGlobalUngovernedEvents(cwd, credentialId),
59
+ { pushed: 1 },
60
+ );
61
+ assert.equal(requests.length, 1);
62
+ assert.equal(
63
+ requests[0].init.headers.Authorization,
64
+ `Bearer ${apiKey}`,
65
+ );
66
+ const body = JSON.parse(requests[0].init.body);
67
+ assert.equal(body.events[0].pid, 42);
68
+ assert.equal(body.events[0].args, "claude --prompt secret");
69
+
70
+ assert.deepEqual(
71
+ await pushGlobalUngovernedEvents(cwd, credentialId),
72
+ { pushed: 0 },
73
+ );
74
+ assert.equal(requests.length, 1);
75
+ } finally {
76
+ delete process.env.HOME;
77
+ globalThis.fetch = undefined;
78
+ fs.rmSync(cwd, { recursive: true, force: true });
79
+ fs.rmSync(homeDir, { recursive: true, force: true });
80
+ }
81
+ });