@danielblomma/cortex-mcp 2.4.0 → 2.4.2

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 (214) hide show
  1. package/CHANGELOG.md +226 -0
  2. package/README.md +24 -3
  3. package/bin/cli/arguments.mjs +60 -0
  4. package/bin/cli/context-passthrough.mjs +129 -0
  5. package/bin/cli/daemon.mjs +157 -0
  6. package/bin/cli/enterprise.mjs +516 -0
  7. package/bin/cli/help.mjs +88 -0
  8. package/bin/cli/hooks.mjs +199 -0
  9. package/bin/cli/mcp-command.mjs +87 -0
  10. package/bin/cli/paths.mjs +14 -0
  11. package/bin/cli/process.mjs +49 -0
  12. package/bin/cli/project-commands.mjs +237 -0
  13. package/bin/cli/project-runtime.mjs +34 -0
  14. package/bin/cli/query-command.mjs +18 -0
  15. package/bin/cli/router.mjs +66 -0
  16. package/bin/cli/run-command.mjs +44 -0
  17. package/bin/cli/scaffold-ownership.mjs +936 -0
  18. package/bin/cli/scaffold.mjs +687 -0
  19. package/bin/cli/stage-command.mjs +9 -0
  20. package/bin/cli/telemetry-command.mjs +18 -0
  21. package/bin/cli/trusted-runtime.mjs +18 -0
  22. package/bin/cortex.mjs +12 -1607
  23. package/bin/daemon-control.mjs +162 -0
  24. package/mcp-registry-submission.json +1 -1
  25. package/package.json +8 -3
  26. package/scaffold/mcp/dist/.cortex-build-hash +1 -0
  27. package/scaffold/mcp/dist/cli/enterprise-setup.js +134 -0
  28. package/scaffold/mcp/dist/cli/govern.js +937 -0
  29. package/scaffold/mcp/dist/cli/query.js +409 -0
  30. package/scaffold/mcp/dist/cli/run.js +295 -0
  31. package/scaffold/mcp/dist/cli/stage.js +308 -0
  32. package/scaffold/mcp/dist/cli/telemetry-test.js +141 -0
  33. package/scaffold/mcp/dist/cli/ungoverned-detector.js +133 -0
  34. package/scaffold/mcp/dist/contextEntities.js +282 -0
  35. package/scaffold/mcp/dist/core/audit/query.js +72 -0
  36. package/scaffold/mcp/dist/core/audit/writer.js +28 -0
  37. package/scaffold/mcp/dist/core/config.js +235 -0
  38. package/scaffold/mcp/dist/core/enterprise-host-identity.js +193 -0
  39. package/scaffold/mcp/dist/core/enterprise-identity.js +18 -0
  40. package/scaffold/mcp/dist/core/enterprise-rotation.js +158 -0
  41. package/scaffold/mcp/dist/core/govern-paths.js +21 -0
  42. package/scaffold/mcp/dist/core/index.js +14 -0
  43. package/scaffold/mcp/dist/core/license.js +278 -0
  44. package/scaffold/mcp/dist/core/policy/enforce.js +66 -0
  45. package/scaffold/mcp/dist/core/policy/injection.js +172 -0
  46. package/scaffold/mcp/dist/core/policy/store.js +179 -0
  47. package/scaffold/mcp/dist/core/rbac/check.js +30 -0
  48. package/scaffold/mcp/dist/core/secure-endpoint.js +25 -0
  49. package/scaffold/mcp/dist/core/telemetry/collector.js +285 -0
  50. package/scaffold/mcp/dist/core/telemetry/state-dir.js +31 -0
  51. package/scaffold/mcp/dist/core/validators/builtins.js +632 -0
  52. package/scaffold/mcp/dist/core/validators/config.js +44 -0
  53. package/scaffold/mcp/dist/core/validators/engine.js +120 -0
  54. package/scaffold/mcp/dist/core/validators/evaluators/code_comments.js +236 -0
  55. package/scaffold/mcp/dist/core/validators/evaluators/regex.js +116 -0
  56. package/scaffold/mcp/dist/core/workflow/artifact-io.js +103 -0
  57. package/scaffold/mcp/dist/core/workflow/capabilities.js +88 -0
  58. package/scaffold/mcp/dist/core/workflow/default-workflows.js +102 -0
  59. package/scaffold/mcp/dist/core/workflow/enforcement.js +164 -0
  60. package/scaffold/mcp/dist/core/workflow/envelope.js +132 -0
  61. package/scaffold/mcp/dist/core/workflow/index.js +11 -0
  62. package/scaffold/mcp/dist/core/workflow/mcp-tools.js +175 -0
  63. package/scaffold/mcp/dist/core/workflow/resolution.js +69 -0
  64. package/scaffold/mcp/dist/core/workflow/run-lifecycle.js +123 -0
  65. package/scaffold/mcp/dist/core/workflow/schemas.js +141 -0
  66. package/scaffold/mcp/dist/core/workflow/synced-capability-registry.js +60 -0
  67. package/scaffold/mcp/dist/core/workflow/synced-registry.js +60 -0
  68. package/scaffold/mcp/dist/daemon/capability-sync-checker.js +242 -0
  69. package/scaffold/mcp/dist/daemon/client.js +130 -0
  70. package/scaffold/mcp/dist/daemon/egress-proxy.js +288 -0
  71. package/scaffold/mcp/dist/daemon/global-host-events.js +222 -0
  72. package/scaffold/mcp/dist/daemon/heartbeat-pusher.js +116 -0
  73. package/scaffold/mcp/dist/daemon/heartbeat-tracker.js +165 -0
  74. package/scaffold/mcp/dist/daemon/host-events-pusher.js +249 -0
  75. package/scaffold/mcp/dist/daemon/main.js +449 -0
  76. package/scaffold/mcp/dist/daemon/paths.js +36 -0
  77. package/scaffold/mcp/dist/daemon/project-service-registry.js +78 -0
  78. package/scaffold/mcp/dist/daemon/protocol.js +8 -0
  79. package/scaffold/mcp/dist/daemon/server.js +180 -0
  80. package/scaffold/mcp/dist/daemon/skill-sync-checker.js +557 -0
  81. package/scaffold/mcp/dist/daemon/sync-checker.js +165 -0
  82. package/scaffold/mcp/dist/daemon/ungoverned-scanner.js +136 -0
  83. package/scaffold/mcp/dist/daemon/workflow-sync-checker.js +249 -0
  84. package/scaffold/mcp/dist/defaults.js +6 -0
  85. package/scaffold/mcp/dist/embed.js +627 -0
  86. package/scaffold/mcp/dist/embedScheduler.js +479 -0
  87. package/scaffold/mcp/dist/embeddings.js +167 -0
  88. package/scaffold/mcp/dist/enterprise/audit/push.js +66 -0
  89. package/scaffold/mcp/dist/enterprise/index.js +327 -0
  90. package/scaffold/mcp/dist/enterprise/model/deploy.js +27 -0
  91. package/scaffold/mcp/dist/enterprise/policy/sync.js +129 -0
  92. package/scaffold/mcp/dist/enterprise/privacy/boundary.js +184 -0
  93. package/scaffold/mcp/dist/enterprise/reviews/changed-files.js +33 -0
  94. package/scaffold/mcp/dist/enterprise/reviews/pattern-context.js +202 -0
  95. package/scaffold/mcp/dist/enterprise/reviews/policy-selection.js +46 -0
  96. package/scaffold/mcp/dist/enterprise/reviews/push.js +102 -0
  97. package/scaffold/mcp/dist/enterprise/reviews/trust-state.js +186 -0
  98. package/scaffold/mcp/dist/enterprise/telemetry/sync.js +57 -0
  99. package/scaffold/mcp/dist/enterprise/tools/enterprise.js +826 -0
  100. package/scaffold/mcp/dist/enterprise/tools/harness.js +40 -0
  101. package/scaffold/mcp/dist/enterprise/tools/walk.js +73 -0
  102. package/scaffold/mcp/dist/enterprise/violations/push.js +77 -0
  103. package/scaffold/mcp/dist/enterprise/workflow/push.js +44 -0
  104. package/scaffold/mcp/dist/enterprise/workflow/state.js +329 -0
  105. package/scaffold/mcp/dist/frontmatter.js +33 -0
  106. package/scaffold/mcp/dist/graph.js +769 -0
  107. package/scaffold/mcp/dist/graphCsv.js +55 -0
  108. package/scaffold/mcp/dist/graphMetrics.js +8 -0
  109. package/scaffold/mcp/dist/hooks/permission-request.js +89 -0
  110. package/scaffold/mcp/dist/hooks/post-tool-use.js +105 -0
  111. package/scaffold/mcp/dist/hooks/pre-compact.js +29 -0
  112. package/scaffold/mcp/dist/hooks/pre-tool-use.js +78 -0
  113. package/scaffold/mcp/dist/hooks/session-end.js +43 -0
  114. package/scaffold/mcp/dist/hooks/session-start.js +41 -0
  115. package/scaffold/mcp/dist/hooks/shared.js +194 -0
  116. package/scaffold/mcp/dist/hooks/stop.js +28 -0
  117. package/scaffold/mcp/dist/hooks/user-prompt-submit.js +33 -0
  118. package/scaffold/mcp/dist/impactPresentation.js +137 -0
  119. package/scaffold/mcp/dist/impactRanking.js +191 -0
  120. package/scaffold/mcp/dist/impactResponse.js +30 -0
  121. package/scaffold/mcp/dist/impactResults.js +105 -0
  122. package/scaffold/mcp/dist/impactSeed.js +20 -0
  123. package/scaffold/mcp/dist/impactTraversal.js +64 -0
  124. package/scaffold/mcp/dist/jsonl.js +77 -0
  125. package/scaffold/mcp/dist/loadGraph.js +759 -0
  126. package/scaffold/mcp/dist/lruCache.js +38 -0
  127. package/scaffold/mcp/dist/paths.js +97 -0
  128. package/scaffold/mcp/dist/patternEvidence.js +272 -0
  129. package/scaffold/mcp/dist/plugin.js +81 -0
  130. package/scaffold/mcp/dist/presets.js +78 -0
  131. package/scaffold/mcp/dist/relatedResponse.js +18 -0
  132. package/scaffold/mcp/dist/relatedTraversal.js +78 -0
  133. package/scaffold/mcp/dist/rules.js +23 -0
  134. package/scaffold/mcp/dist/search.js +212 -0
  135. package/scaffold/mcp/dist/searchCore.js +457 -0
  136. package/scaffold/mcp/dist/searchResults.js +230 -0
  137. package/scaffold/mcp/dist/server.js +317 -0
  138. package/scaffold/mcp/dist/types.js +1 -0
  139. package/scaffold/mcp/package-lock.json +321 -200
  140. package/scaffold/mcp/package.json +15 -6
  141. package/scaffold/mcp/src/cli/enterprise-setup.ts +82 -8
  142. package/scaffold/mcp/src/cli/govern.ts +137 -52
  143. package/scaffold/mcp/src/cli/run.ts +53 -19
  144. package/scaffold/mcp/src/cli/stage.ts +8 -3
  145. package/scaffold/mcp/src/core/config.ts +9 -2
  146. package/scaffold/mcp/src/core/enterprise-host-identity.ts +259 -0
  147. package/scaffold/mcp/src/core/enterprise-identity.ts +20 -0
  148. package/scaffold/mcp/src/core/enterprise-rotation.ts +185 -0
  149. package/scaffold/mcp/src/core/govern-paths.ts +30 -0
  150. package/scaffold/mcp/src/core/license.ts +186 -17
  151. package/scaffold/mcp/src/core/secure-endpoint.ts +23 -0
  152. package/scaffold/mcp/src/core/workflow/enforcement.ts +8 -1
  153. package/scaffold/mcp/src/core/workflow/mcp-tools.ts +3 -0
  154. package/scaffold/mcp/src/core/workflow/resolution.ts +9 -1
  155. package/scaffold/mcp/src/core/workflow/synced-capability-registry.ts +15 -0
  156. package/scaffold/mcp/src/core/workflow/synced-registry.ts +15 -0
  157. package/scaffold/mcp/src/daemon/capability-sync-checker.ts +38 -2
  158. package/scaffold/mcp/src/daemon/egress-proxy.ts +14 -8
  159. package/scaffold/mcp/src/daemon/global-host-events.ts +288 -0
  160. package/scaffold/mcp/src/daemon/heartbeat-pusher.ts +4 -0
  161. package/scaffold/mcp/src/daemon/heartbeat-tracker.ts +2 -0
  162. package/scaffold/mcp/src/daemon/host-events-pusher.ts +5 -0
  163. package/scaffold/mcp/src/daemon/main.ts +99 -25
  164. package/scaffold/mcp/src/daemon/project-service-registry.ts +107 -0
  165. package/scaffold/mcp/src/daemon/skill-sync-checker.ts +368 -31
  166. package/scaffold/mcp/src/daemon/sync-checker.ts +4 -0
  167. package/scaffold/mcp/src/daemon/ungoverned-scanner.ts +57 -25
  168. package/scaffold/mcp/src/daemon/workflow-sync-checker.ts +41 -2
  169. package/scaffold/mcp/src/enterprise/audit/push.ts +8 -0
  170. package/scaffold/mcp/src/enterprise/policy/sync.ts +12 -0
  171. package/scaffold/mcp/src/enterprise/reviews/push.ts +9 -0
  172. package/scaffold/mcp/src/enterprise/reviews/trust-state.ts +3 -1
  173. package/scaffold/mcp/src/enterprise/telemetry/sync.ts +9 -0
  174. package/scaffold/mcp/src/enterprise/violations/push.ts +9 -0
  175. package/scaffold/mcp/src/enterprise/workflow/push.ts +7 -0
  176. package/scaffold/mcp/src/plugin.ts +20 -0
  177. package/scaffold/mcp/tests/copilot-shim.test.mjs +19 -1
  178. package/scaffold/mcp/tests/egress-proxy.test.mjs +37 -0
  179. package/scaffold/mcp/tests/enterprise-identity-sync.test.mjs +401 -0
  180. package/scaffold/mcp/tests/enterprise-setup.test.mjs +189 -0
  181. package/scaffold/mcp/tests/global-host-events.test.mjs +81 -0
  182. package/scaffold/mcp/tests/govern-install.test.mjs +95 -2
  183. package/scaffold/mcp/tests/govern-repair.test.mjs +20 -3
  184. package/scaffold/mcp/tests/heartbeat-tracker.test.mjs +26 -0
  185. package/scaffold/mcp/tests/license.test.mjs +367 -0
  186. package/scaffold/mcp/tests/project-service-registry.test.mjs +172 -0
  187. package/scaffold/mcp/tests/review-trust-contract.test.mjs +18 -0
  188. package/scaffold/mcp/tests/secure-enterprise-endpoint.test.mjs +46 -0
  189. package/scaffold/mcp/tests/skill-sync-checker.test.mjs +446 -2
  190. package/scaffold/mcp/tests/ungoverned-scanner.test.mjs +104 -22
  191. package/scaffold/mcp/tests/workflow-cli.test.mjs +14 -1
  192. package/scaffold/mcp/tests/workflow-synced-capabilities.test.mjs +37 -0
  193. package/scaffold/mcp/tests/workflow-synced-registry.test.mjs +40 -1
  194. package/scaffold/ownership/baseline-v2.4.1.json +22 -0
  195. package/scaffold/ownership/current.json +4 -0
  196. package/scaffold/ownership/v1.json +456 -0
  197. package/scaffold/scripts/ingest-parsers.mjs +1 -387
  198. package/scaffold/scripts/ingest-worker.mjs +4 -1
  199. package/scaffold/scripts/ingest.mjs +5 -3899
  200. package/scaffold/scripts/lib/ingest/arguments.mjs +78 -0
  201. package/scaffold/scripts/lib/ingest/chunks.mjs +212 -0
  202. package/scaffold/scripts/lib/ingest/config.mjs +120 -0
  203. package/scaffold/scripts/lib/ingest/constants.mjs +222 -0
  204. package/scaffold/scripts/lib/ingest/files.mjs +387 -0
  205. package/scaffold/scripts/lib/ingest/incremental-state.mjs +131 -0
  206. package/scaffold/scripts/lib/ingest/io.mjs +78 -0
  207. package/scaffold/scripts/lib/ingest/main.mjs +76 -0
  208. package/scaffold/scripts/lib/ingest/parser-composition.mjs +140 -0
  209. package/scaffold/scripts/lib/ingest/parser-registry.mjs +387 -0
  210. package/scaffold/scripts/lib/ingest/pipeline-stages.mjs +1625 -0
  211. package/scaffold/scripts/lib/ingest/projects.mjs +272 -0
  212. package/scaffold/scripts/lib/ingest/relations.mjs +860 -0
  213. package/scaffold/scripts/lib/ingest/runtime-paths.mjs +11 -0
  214. package/scaffold/scripts/lib/ingest/workers.mjs +264 -0
@@ -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
+ });