@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,282 @@
1
+ function isWindowChunkId(id) {
2
+ return id.includes(":window:");
3
+ }
4
+ function baseChunkId(id) {
5
+ const markerIndex = id.indexOf(":window:");
6
+ return markerIndex === -1 ? id : id.slice(0, markerIndex);
7
+ }
8
+ function groupRuleLinks(relations) {
9
+ const links = new Map();
10
+ for (const relation of relations) {
11
+ if (relation.relation !== "CONSTRAINS" && relation.relation !== "IMPLEMENTS") {
12
+ continue;
13
+ }
14
+ if (relation.relation === "CONSTRAINS") {
15
+ const list = links.get(relation.to) ?? [];
16
+ list.push(relation.from);
17
+ links.set(relation.to, list);
18
+ }
19
+ else {
20
+ const list = links.get(relation.from) ?? [];
21
+ list.push(relation.to);
22
+ links.set(relation.from, list);
23
+ }
24
+ }
25
+ return links;
26
+ }
27
+ function buildRelationSearchSignals(data) {
28
+ const labelsById = new Map();
29
+ for (const document of data.documents) {
30
+ labelsById.set(document.id, document.path);
31
+ }
32
+ for (const chunk of data.chunks) {
33
+ labelsById.set(chunk.id, chunk.name || chunk.id);
34
+ }
35
+ for (const module of data.modules) {
36
+ labelsById.set(module.id, module.name || module.path);
37
+ }
38
+ for (const project of data.projects) {
39
+ labelsById.set(project.id, project.name || project.path);
40
+ }
41
+ const supportedRelations = new Set([
42
+ "CALLS_SQL",
43
+ "USES_CONFIG_KEY",
44
+ "USES_RESOURCE_KEY",
45
+ "USES_SETTING_KEY",
46
+ "USES_CONFIG",
47
+ "TRANSFORMS_CONFIG"
48
+ ]);
49
+ const signalsByEntity = new Map();
50
+ for (const relation of data.relations) {
51
+ if (!supportedRelations.has(relation.relation)) {
52
+ continue;
53
+ }
54
+ const sourceLabel = labelsById.get(relation.from) ?? relation.from;
55
+ const targetLabel = labelsById.get(relation.to) ?? relation.to;
56
+ const outgoingSignal = [relation.relation.toLowerCase(), relation.note, targetLabel].filter(Boolean).join(" ");
57
+ const incomingSignal = ["affected_by", relation.relation.toLowerCase(), relation.note, sourceLabel]
58
+ .filter(Boolean)
59
+ .join(" ");
60
+ const outgoing = signalsByEntity.get(relation.from) ?? [];
61
+ outgoing.push(outgoingSignal);
62
+ signalsByEntity.set(relation.from, outgoing);
63
+ const incoming = signalsByEntity.get(relation.to) ?? [];
64
+ incoming.push(incomingSignal);
65
+ signalsByEntity.set(relation.to, incoming);
66
+ }
67
+ return new Map([...signalsByEntity.entries()].map(([id, parts]) => [id, parts.join("\n")]));
68
+ }
69
+ function buildSearchEntityIndexes(data) {
70
+ const fileRuleLinks = groupRuleLinks(data.relations);
71
+ const relationSignals = buildRelationSearchSignals(data);
72
+ const adrPathSet = new Set(data.adrs
73
+ .map((adr) => adr.path.trim().toLowerCase())
74
+ .filter((adrPath) => adrPath.length > 0));
75
+ const filePathById = new Map(data.documents.filter((document) => document.kind === "CODE").map((document) => [document.id, document.path]));
76
+ return { fileRuleLinks, relationSignals, adrPathSet, filePathById };
77
+ }
78
+ export function* iterateSearchEntities(data, includeContent) {
79
+ const { fileRuleLinks, relationSignals, adrPathSet, filePathById } = buildSearchEntityIndexes(data);
80
+ for (const document of data.documents) {
81
+ const normalizedPath = document.path.trim().toLowerCase();
82
+ if (document.kind === "ADR" && adrPathSet.has(normalizedPath)) {
83
+ continue;
84
+ }
85
+ yield {
86
+ id: document.id,
87
+ entity_type: "File",
88
+ kind: document.kind,
89
+ label: document.path,
90
+ path: document.path,
91
+ text: `${document.path}\n${document.excerpt}\n${document.content}\n${relationSignals.get(document.id) ?? ""}`,
92
+ status: document.status,
93
+ source_of_truth: document.source_of_truth,
94
+ trust_level: document.trust_level,
95
+ updated_at: document.updated_at,
96
+ snippet: document.excerpt,
97
+ matched_rules: fileRuleLinks.get(document.id) ?? [],
98
+ content: includeContent ? document.content : undefined
99
+ };
100
+ }
101
+ for (const rule of data.rules) {
102
+ yield {
103
+ id: rule.id,
104
+ entity_type: "Rule",
105
+ kind: "RULE",
106
+ label: rule.title || rule.id,
107
+ path: "",
108
+ text: `${rule.id}\n${rule.title}\n${rule.body}`,
109
+ status: rule.status,
110
+ source_of_truth: rule.source_of_truth,
111
+ trust_level: rule.trust_level,
112
+ updated_at: rule.updated_at,
113
+ snippet: rule.body.slice(0, 500),
114
+ matched_rules: [rule.id],
115
+ content: includeContent ? rule.body : undefined
116
+ };
117
+ }
118
+ for (const adr of data.adrs) {
119
+ yield {
120
+ id: adr.id,
121
+ entity_type: "ADR",
122
+ kind: "ADR",
123
+ label: adr.title || adr.id,
124
+ path: adr.path,
125
+ text: `${adr.path}\n${adr.title}\n${adr.body}`,
126
+ status: adr.status,
127
+ source_of_truth: adr.source_of_truth,
128
+ trust_level: adr.trust_level,
129
+ updated_at: adr.decision_date,
130
+ snippet: adr.body.slice(0, 500),
131
+ matched_rules: [],
132
+ content: includeContent ? adr.body : undefined
133
+ };
134
+ }
135
+ for (const chunk of data.chunks) {
136
+ const filePath = filePathById.get(chunk.file_id) ?? "";
137
+ yield {
138
+ id: chunk.id,
139
+ entity_type: "Chunk",
140
+ kind: chunk.kind || "chunk",
141
+ label: chunk.name || chunk.id,
142
+ path: filePath,
143
+ text: `${filePath}\n${chunk.name}\n${chunk.signature}\n${chunk.description}\n${chunk.body}\n${relationSignals.get(chunk.id) ?? ""}`,
144
+ status: chunk.status,
145
+ source_of_truth: chunk.source_of_truth,
146
+ trust_level: chunk.trust_level,
147
+ updated_at: chunk.updated_at,
148
+ snippet: chunk.description || chunk.body.slice(0, 500),
149
+ matched_rules: fileRuleLinks.get(chunk.file_id) ?? [],
150
+ content: includeContent ? chunk.body : undefined
151
+ };
152
+ }
153
+ for (const module of data.modules) {
154
+ yield {
155
+ id: module.id,
156
+ entity_type: "Module",
157
+ kind: "MODULE",
158
+ label: module.name,
159
+ path: module.path,
160
+ text: `${module.path}\n${module.name}\n${module.summary}\n${module.exported_symbols}`,
161
+ status: module.status,
162
+ source_of_truth: module.source_of_truth,
163
+ trust_level: module.trust_level,
164
+ updated_at: module.updated_at,
165
+ snippet: (module.summary || "").slice(0, 500),
166
+ matched_rules: [],
167
+ content: includeContent ? module.summary : undefined
168
+ };
169
+ }
170
+ for (const project of data.projects) {
171
+ yield {
172
+ id: project.id,
173
+ entity_type: "Project",
174
+ kind: project.kind.toUpperCase() || "PROJECT",
175
+ label: project.name || project.path,
176
+ path: project.path,
177
+ text: `${project.path}\n${project.name}\n${project.kind}\n${project.language}\n${project.target_framework}\n${project.summary}`,
178
+ status: project.status,
179
+ source_of_truth: project.source_of_truth,
180
+ trust_level: project.trust_level,
181
+ updated_at: project.updated_at,
182
+ snippet: (project.summary || "").slice(0, 500),
183
+ matched_rules: [],
184
+ content: includeContent ? project.summary : undefined
185
+ };
186
+ }
187
+ }
188
+ export function buildSearchEntities(data, includeContent) {
189
+ return Array.from(iterateSearchEntities(data, includeContent));
190
+ }
191
+ export function buildChunkPartOfRelations(data) {
192
+ const relations = [];
193
+ for (const chunk of data.chunks) {
194
+ if (isWindowChunkId(chunk.id)) {
195
+ relations.push({
196
+ from: chunk.id,
197
+ to: baseChunkId(chunk.id),
198
+ relation: "PART_OF",
199
+ note: "Overlap window belongs to base chunk"
200
+ });
201
+ }
202
+ if (!chunk.file_id) {
203
+ continue;
204
+ }
205
+ relations.push({
206
+ from: chunk.id,
207
+ to: chunk.file_id,
208
+ relation: "PART_OF",
209
+ note: "Chunk belongs to file"
210
+ });
211
+ }
212
+ return relations;
213
+ }
214
+ export function entityCatalog(data) {
215
+ const catalog = new Map();
216
+ const fileById = new Map(data.documents.map((document) => [document.id, document]));
217
+ for (const file of data.documents) {
218
+ catalog.set(file.id, {
219
+ id: file.id,
220
+ type: "File",
221
+ label: file.path,
222
+ status: file.status,
223
+ source_of_truth: file.source_of_truth
224
+ });
225
+ }
226
+ for (const rule of data.rules) {
227
+ catalog.set(rule.id, {
228
+ id: rule.id,
229
+ type: "Rule",
230
+ label: rule.title,
231
+ status: rule.status,
232
+ source_of_truth: rule.source_of_truth
233
+ });
234
+ }
235
+ for (const adr of data.adrs) {
236
+ catalog.set(adr.id, {
237
+ id: adr.id,
238
+ type: "ADR",
239
+ label: adr.title || adr.id,
240
+ status: adr.status,
241
+ source_of_truth: adr.source_of_truth
242
+ });
243
+ }
244
+ for (const chunk of data.chunks) {
245
+ const filePath = fileById.get(chunk.file_id)?.path ?? "";
246
+ const chunkEntity = {
247
+ id: chunk.id,
248
+ type: "Chunk",
249
+ label: chunk.name || chunk.id,
250
+ status: chunk.status,
251
+ source_of_truth: chunk.source_of_truth
252
+ };
253
+ if (filePath) {
254
+ chunkEntity.path = filePath;
255
+ }
256
+ catalog.set(chunk.id, chunkEntity);
257
+ }
258
+ for (const module of data.modules) {
259
+ catalog.set(module.id, {
260
+ id: module.id,
261
+ type: "Module",
262
+ label: module.name,
263
+ status: module.status,
264
+ source_of_truth: module.source_of_truth,
265
+ path: module.path
266
+ });
267
+ }
268
+ for (const project of data.projects) {
269
+ catalog.set(project.id, {
270
+ id: project.id,
271
+ type: "Project",
272
+ label: project.name || project.path,
273
+ status: project.status,
274
+ source_of_truth: project.source_of_truth,
275
+ path: project.path
276
+ });
277
+ }
278
+ return catalog;
279
+ }
280
+ export function buildEntitySearchMap(data) {
281
+ return new Map(buildSearchEntities(data, false).map((entity) => [entity.id, entity]));
282
+ }
@@ -0,0 +1,72 @@
1
+ import { readFileSync, readdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
4
+ export function queryAuditLog(contextDir, query) {
5
+ const auditDir = join(contextDir, "audit");
6
+ const limit = query.limit ?? 50;
7
+ if (query.from && !DATE_RE.test(query.from))
8
+ return [];
9
+ if (query.to && !DATE_RE.test(query.to))
10
+ return [];
11
+ let files;
12
+ try {
13
+ files = readdirSync(auditDir)
14
+ .filter(f => f.endsWith(".jsonl"))
15
+ .sort();
16
+ }
17
+ catch {
18
+ return [];
19
+ }
20
+ // Filter files by date range
21
+ if (query.from) {
22
+ const fromFile = `${query.from}.jsonl`;
23
+ files = files.filter(f => f >= fromFile);
24
+ }
25
+ if (query.to) {
26
+ const toFile = `${query.to}.jsonl`;
27
+ files = files.filter(f => f <= toFile);
28
+ }
29
+ const results = [];
30
+ // Read newest files first
31
+ for (const file of files.reverse()) {
32
+ if (results.length >= limit)
33
+ break;
34
+ try {
35
+ const raw = readFileSync(join(auditDir, file), "utf8").trim();
36
+ if (!raw)
37
+ continue;
38
+ const entries = raw
39
+ .split("\n")
40
+ .map(line => {
41
+ try {
42
+ return JSON.parse(line);
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ })
48
+ .filter(Boolean)
49
+ .reverse(); // newest first within file
50
+ for (const entry of entries) {
51
+ if (results.length >= limit)
52
+ break;
53
+ // Apply tool filter
54
+ if (query.tool && entry.tool !== query.tool)
55
+ continue;
56
+ if (query.event_type && entry.event_type !== query.event_type)
57
+ continue;
58
+ if (query.evidence_level && entry.evidence_level !== query.evidence_level)
59
+ continue;
60
+ if (query.status && entry.status !== query.status)
61
+ continue;
62
+ if (query.session_id && entry.session_id !== query.session_id)
63
+ continue;
64
+ results.push(entry);
65
+ }
66
+ }
67
+ catch {
68
+ // skip unreadable files
69
+ }
70
+ }
71
+ return results;
72
+ }
@@ -0,0 +1,28 @@
1
+ import { appendFile, mkdir } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ export class AuditWriter {
4
+ auditDir;
5
+ onEntry;
6
+ initialized = false;
7
+ constructor(contextDir, options = {}) {
8
+ this.auditDir = join(contextDir, "audit");
9
+ this.onEntry = options.onEntry ?? null;
10
+ }
11
+ log(entry) {
12
+ const date = entry.timestamp.slice(0, 10); // YYYY-MM-DD
13
+ const filePath = join(this.auditDir, `${date}.jsonl`);
14
+ const line = JSON.stringify(entry) + "\n";
15
+ this.onEntry?.(entry);
16
+ // Fire-and-forget async write — don't block the tool handler
17
+ this.writeAsync(filePath, line).catch(() => {
18
+ process.stderr.write("[cortex-enterprise] Failed to write audit entry\n");
19
+ });
20
+ }
21
+ async writeAsync(filePath, line) {
22
+ if (!this.initialized) {
23
+ await mkdir(this.auditDir, { recursive: true });
24
+ this.initialized = true;
25
+ }
26
+ await appendFile(filePath, line);
27
+ }
28
+ }
@@ -0,0 +1,235 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { parseValidatorsConfig } from "./validators/config.js";
4
+ import { isAllowedEnterpriseEndpoint } from "./secure-endpoint.js";
5
+ const DEFAULT_FRAMEWORKS = ["iso27001", "iso42001", "soc2"];
6
+ const EU_ADDON_FRAMEWORKS = ["gdpr", "ai_act", "nis2"];
7
+ const VALID_FRAMEWORKS = new Set([
8
+ ...DEFAULT_FRAMEWORKS,
9
+ ...EU_ADDON_FRAMEWORKS,
10
+ ]);
11
+ const VALID_MODES = new Set(["off", "advisory", "enforced"]);
12
+ const VALID_TIERS = new Set(["prevent", "wrap", "detect", "off"]);
13
+ const DEFAULTS = {
14
+ enterprise: {
15
+ endpoint: "",
16
+ api_key: "",
17
+ base_url: "",
18
+ },
19
+ telemetry: {
20
+ enabled: false,
21
+ endpoint: "",
22
+ api_key: "",
23
+ interval_minutes: 10,
24
+ },
25
+ audit: {
26
+ enabled: true,
27
+ retention_days: 90,
28
+ },
29
+ policy: {
30
+ enabled: true,
31
+ endpoint: "",
32
+ api_key: "",
33
+ sync_interval_minutes: 240,
34
+ },
35
+ rbac: {
36
+ enabled: false,
37
+ default_role: "developer",
38
+ },
39
+ validators: {},
40
+ compliance: {
41
+ frameworks: [],
42
+ eu_addons: false,
43
+ },
44
+ govern: {
45
+ mode: "off",
46
+ sync_on_startup: true,
47
+ sync_interval_minutes: 60,
48
+ tier_claude: "prevent",
49
+ tier_codex: "prevent",
50
+ tier_copilot: "wrap",
51
+ detect_ungoverned: true,
52
+ govern_endpoint: "",
53
+ },
54
+ };
55
+ const VALID_ROLES = ["admin", "developer", "readonly"];
56
+ function isValidRole(value) {
57
+ return VALID_ROLES.includes(value);
58
+ }
59
+ function stripInlineComment(value) {
60
+ // Strip # comments that aren't inside quotes
61
+ const singleMatch = value.match(/^'([^']*)'(\s*#.*)?$/);
62
+ if (singleMatch)
63
+ return singleMatch[1];
64
+ const doubleMatch = value.match(/^"([^"]*)"(\s*#.*)?$/);
65
+ if (doubleMatch)
66
+ return doubleMatch[1];
67
+ // Unquoted: strip from first # preceded by whitespace
68
+ const commentIdx = value.search(/\s+#/);
69
+ return commentIdx >= 0 ? value.slice(0, commentIdx).trimEnd() : value;
70
+ }
71
+ function parseInlineList(value) {
72
+ const trimmed = value.trim();
73
+ if (!trimmed.startsWith("[") || !trimmed.endsWith("]"))
74
+ return null;
75
+ const inner = trimmed.slice(1, -1).trim();
76
+ if (!inner)
77
+ return [];
78
+ return inner
79
+ .split(",")
80
+ .map((part) => part.trim().replace(/^["']|["']$/g, ""))
81
+ .filter(Boolean);
82
+ }
83
+ function parseSimpleYaml(text) {
84
+ const result = {};
85
+ let section = "";
86
+ for (const line of text.split("\n")) {
87
+ const trimmed = line.trimEnd();
88
+ if (!trimmed || trimmed.startsWith("#"))
89
+ continue;
90
+ const sectionMatch = trimmed.match(/^(\w+):\s*$/);
91
+ if (sectionMatch) {
92
+ section = sectionMatch[1];
93
+ continue;
94
+ }
95
+ const kvMatch = trimmed.match(/^\s+(\w+):\s*(.+?)\s*$/);
96
+ if (kvMatch && section) {
97
+ result[`${section}.${kvMatch[1]}`] = stripInlineComment(kvMatch[2]);
98
+ continue;
99
+ }
100
+ const topMatch = trimmed.match(/^(\w+):\s*(.+?)\s*$/);
101
+ if (topMatch) {
102
+ result[topMatch[1]] = stripInlineComment(topMatch[2]);
103
+ }
104
+ }
105
+ return result;
106
+ }
107
+ function isLikelyApiKey(value) {
108
+ return /^(?:ctx|ent)_[A-Za-z0-9._-]{8,}$/.test(value);
109
+ }
110
+ function isLikelyHttpUrl(value) {
111
+ try {
112
+ const parsed = new URL(value);
113
+ return parsed.protocol === "http:" || parsed.protocol === "https:";
114
+ }
115
+ catch {
116
+ return false;
117
+ }
118
+ }
119
+ export function resolveEnterpriseActivation(config) {
120
+ const apiKey = config.enterprise.api_key.trim();
121
+ const endpoint = (config.enterprise.base_url ||
122
+ config.enterprise.endpoint).trim();
123
+ if (!apiKey) {
124
+ return { active: false, reason: "missing_api_key", api_key: null, endpoint: endpoint || null };
125
+ }
126
+ if (!endpoint) {
127
+ return { active: false, reason: "missing_endpoint", api_key: apiKey, endpoint: null };
128
+ }
129
+ if (!isLikelyApiKey(apiKey)) {
130
+ return { active: false, reason: "invalid_api_key_format", api_key: apiKey, endpoint };
131
+ }
132
+ if (!isLikelyHttpUrl(endpoint) ||
133
+ !isAllowedEnterpriseEndpoint(endpoint)) {
134
+ return { active: false, reason: "invalid_endpoint_format", api_key: apiKey, endpoint };
135
+ }
136
+ return { active: true, reason: "active", api_key: apiKey, endpoint };
137
+ }
138
+ function deriveEndpoint(baseUrl, suffix) {
139
+ if (!baseUrl)
140
+ return "";
141
+ return baseUrl.replace(/\/$/, "") + suffix;
142
+ }
143
+ function isValidTier(value) {
144
+ return value !== undefined && VALID_TIERS.has(value);
145
+ }
146
+ function isValidMode(value) {
147
+ return value !== undefined && VALID_MODES.has(value);
148
+ }
149
+ function resolveFrameworks(rawList, euAddons) {
150
+ const parsed = rawList ? parseInlineList(rawList) : null;
151
+ const base = parsed && parsed.length > 0
152
+ ? parsed.filter((f) => VALID_FRAMEWORKS.has(f))
153
+ : DEFAULT_FRAMEWORKS.slice();
154
+ if (!euAddons)
155
+ return base;
156
+ const merged = new Set(base);
157
+ for (const f of EU_ADDON_FRAMEWORKS)
158
+ merged.add(f);
159
+ return Array.from(merged);
160
+ }
161
+ export function loadEnterpriseConfig(contextDir) {
162
+ let raw;
163
+ try {
164
+ raw = readFileSync(join(contextDir, "enterprise.yml"), "utf8");
165
+ }
166
+ catch {
167
+ try {
168
+ raw = readFileSync(join(contextDir, "enterprise.yaml"), "utf8");
169
+ }
170
+ catch {
171
+ return DEFAULTS;
172
+ }
173
+ }
174
+ const fields = parseSimpleYaml(raw);
175
+ const enterpriseApiKey = fields["enterprise.api_key"] ?? DEFAULTS.enterprise.api_key;
176
+ const baseUrl = (fields["enterprise.base_url"] ?? fields["enterprise.endpoint"] ?? "").replace(/\/$/, "");
177
+ const enterpriseEndpoint = fields["enterprise.endpoint"] ?? baseUrl;
178
+ const telemetryEndpoint = fields["enterprise.endpoint_telemetry"] ??
179
+ fields["telemetry.endpoint"] ??
180
+ deriveEndpoint(baseUrl, "/api/v1/telemetry/push");
181
+ const telemetryApiKey = fields["telemetry.api_key"] ?? enterpriseApiKey;
182
+ const policyEndpoint = fields["enterprise.endpoint_policy"] ??
183
+ fields["policy.endpoint"] ??
184
+ deriveEndpoint(baseUrl, "/api/v1/policies/sync");
185
+ const policyApiKey = fields["policy.api_key"] ?? enterpriseApiKey;
186
+ const governEndpoint = fields["enterprise.endpoint_govern"] ??
187
+ deriveEndpoint(baseUrl, "/api/v1/govern");
188
+ const euAddons = fields["compliance.eu_addons"] === "true";
189
+ const frameworks = resolveFrameworks(fields["compliance.frameworks"], euAddons);
190
+ const governMode = isValidMode(fields["govern.mode"]) ? fields["govern.mode"] : DEFAULTS.govern.mode;
191
+ return {
192
+ enterprise: {
193
+ endpoint: enterpriseEndpoint,
194
+ api_key: enterpriseApiKey,
195
+ base_url: baseUrl,
196
+ },
197
+ telemetry: {
198
+ endpoint: telemetryEndpoint,
199
+ api_key: telemetryApiKey,
200
+ enabled: fields["telemetry.enabled"] !== undefined
201
+ ? fields["telemetry.enabled"] === "true"
202
+ : !!(telemetryEndpoint && telemetryApiKey),
203
+ interval_minutes: parseInt(fields["telemetry.interval_minutes"] ?? "", 10) || DEFAULTS.telemetry.interval_minutes,
204
+ },
205
+ audit: {
206
+ enabled: fields["audit.enabled"] !== "false",
207
+ retention_days: parseInt(fields["audit.retention_days"] ?? "", 10) || DEFAULTS.audit.retention_days,
208
+ },
209
+ policy: {
210
+ enabled: fields["policy.enabled"] !== "false",
211
+ endpoint: policyEndpoint,
212
+ api_key: policyApiKey,
213
+ sync_interval_minutes: parseInt(fields["policy.sync_interval_minutes"] ?? "", 10) || DEFAULTS.policy.sync_interval_minutes,
214
+ },
215
+ rbac: {
216
+ enabled: fields["rbac.enabled"] === "true",
217
+ default_role: isValidRole(fields["rbac.default_role"]) ? fields["rbac.default_role"] : DEFAULTS.rbac.default_role,
218
+ },
219
+ validators: parseValidatorsConfig(fields),
220
+ compliance: {
221
+ frameworks,
222
+ eu_addons: euAddons,
223
+ },
224
+ govern: {
225
+ mode: governMode,
226
+ sync_on_startup: fields["govern.sync_on_startup"] !== "false",
227
+ sync_interval_minutes: parseInt(fields["govern.sync_interval_minutes"] ?? "", 10) || DEFAULTS.govern.sync_interval_minutes,
228
+ tier_claude: isValidTier(fields["govern.tier_claude"]) ? fields["govern.tier_claude"] : DEFAULTS.govern.tier_claude,
229
+ tier_codex: isValidTier(fields["govern.tier_codex"]) ? fields["govern.tier_codex"] : DEFAULTS.govern.tier_codex,
230
+ tier_copilot: isValidTier(fields["govern.tier_copilot"]) ? fields["govern.tier_copilot"] : DEFAULTS.govern.tier_copilot,
231
+ detect_ungoverned: fields["govern.detect_ungoverned"] !== "false",
232
+ govern_endpoint: governEndpoint,
233
+ },
234
+ };
235
+ }