@danielblomma/cortex-mcp 1.7.1 → 2.0.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 (79) hide show
  1. package/bin/cortex.mjs +679 -32
  2. package/bin/style.mjs +349 -0
  3. package/package.json +4 -3
  4. package/scaffold/mcp/package-lock.json +834 -671
  5. package/scaffold/mcp/package.json +1 -1
  6. package/scaffold/mcp/src/cli/enterprise-setup.ts +124 -0
  7. package/scaffold/mcp/src/cli/govern.ts +987 -0
  8. package/scaffold/mcp/src/cli/run.ts +306 -0
  9. package/scaffold/mcp/src/cli/telemetry-test.ts +158 -0
  10. package/scaffold/mcp/src/cli/ungoverned-detector.ts +168 -0
  11. package/scaffold/mcp/src/core/audit/query.ts +81 -0
  12. package/scaffold/mcp/src/core/audit/writer.ts +68 -0
  13. package/scaffold/mcp/src/core/config.ts +329 -0
  14. package/scaffold/mcp/src/core/index.ts +34 -0
  15. package/scaffold/mcp/src/core/license.ts +202 -0
  16. package/scaffold/mcp/src/core/policy/enforce.ts +98 -0
  17. package/scaffold/mcp/src/core/policy/injection.ts +229 -0
  18. package/scaffold/mcp/src/core/policy/store.ts +197 -0
  19. package/scaffold/mcp/src/core/rbac/check.ts +40 -0
  20. package/scaffold/mcp/src/core/telemetry/collector.ts +234 -0
  21. package/scaffold/mcp/src/core/validators/builtins.ts +711 -0
  22. package/scaffold/mcp/src/core/validators/config.ts +47 -0
  23. package/scaffold/mcp/src/core/validators/engine.ts +199 -0
  24. package/scaffold/mcp/src/core/validators/evaluators/code_comments.ts +294 -0
  25. package/scaffold/mcp/src/core/validators/evaluators/regex.ts +144 -0
  26. package/scaffold/mcp/src/daemon/client.ts +155 -0
  27. package/scaffold/mcp/src/daemon/egress-proxy.ts +331 -0
  28. package/scaffold/mcp/src/daemon/heartbeat-pusher.ts +147 -0
  29. package/scaffold/mcp/src/daemon/heartbeat-tracker.ts +223 -0
  30. package/scaffold/mcp/src/daemon/host-events-pusher.ts +285 -0
  31. package/scaffold/mcp/src/daemon/main.ts +300 -0
  32. package/scaffold/mcp/src/daemon/paths.ts +41 -0
  33. package/scaffold/mcp/src/daemon/protocol.ts +101 -0
  34. package/scaffold/mcp/src/daemon/server.ts +227 -0
  35. package/scaffold/mcp/src/daemon/sync-checker.ts +213 -0
  36. package/scaffold/mcp/src/daemon/ungoverned-scanner.ts +149 -0
  37. package/scaffold/mcp/src/embed.ts +1 -1
  38. package/scaffold/mcp/src/embeddings.ts +1 -1
  39. package/scaffold/mcp/src/enterprise/audit/push.ts +84 -0
  40. package/scaffold/mcp/src/enterprise/index.ts +415 -0
  41. package/scaffold/mcp/src/enterprise/model/deploy.ts +33 -0
  42. package/scaffold/mcp/src/enterprise/policy/sync.ts +146 -0
  43. package/scaffold/mcp/src/enterprise/privacy/boundary.ts +212 -0
  44. package/scaffold/mcp/src/enterprise/reviews/push.ts +79 -0
  45. package/scaffold/mcp/src/enterprise/telemetry/sync.ts +72 -0
  46. package/scaffold/mcp/src/enterprise/tools/enterprise.ts +1031 -0
  47. package/scaffold/mcp/src/enterprise/tools/walk.ts +79 -0
  48. package/scaffold/mcp/src/enterprise/violations/push.ts +102 -0
  49. package/scaffold/mcp/src/enterprise/workflow/push.ts +60 -0
  50. package/scaffold/mcp/src/enterprise/workflow/state.ts +535 -0
  51. package/scaffold/mcp/src/hooks/pre-compact.ts +54 -0
  52. package/scaffold/mcp/src/hooks/pre-tool-use.ts +96 -0
  53. package/scaffold/mcp/src/hooks/session-end.ts +73 -0
  54. package/scaffold/mcp/src/hooks/session-start.ts +78 -0
  55. package/scaffold/mcp/src/hooks/shared.ts +134 -0
  56. package/scaffold/mcp/src/hooks/stop.ts +60 -0
  57. package/scaffold/mcp/src/hooks/user-prompt-submit.ts +64 -0
  58. package/scaffold/mcp/src/plugin.ts +150 -0
  59. package/scaffold/mcp/src/server.ts +218 -7
  60. package/scaffold/mcp/tests/copilot-shim.test.mjs +146 -0
  61. package/scaffold/mcp/tests/daemon-client.test.mjs +32 -0
  62. package/scaffold/mcp/tests/egress-proxy.test.mjs +239 -0
  63. package/scaffold/mcp/tests/enterprise-config.test.mjs +154 -0
  64. package/scaffold/mcp/tests/govern-install.test.mjs +320 -0
  65. package/scaffold/mcp/tests/govern-repair.test.mjs +157 -0
  66. package/scaffold/mcp/tests/govern-status.test.mjs +538 -0
  67. package/scaffold/mcp/tests/govern.test.mjs +74 -0
  68. package/scaffold/mcp/tests/heartbeat-pusher.test.mjs +154 -0
  69. package/scaffold/mcp/tests/heartbeat-tracker.test.mjs +237 -0
  70. package/scaffold/mcp/tests/host-events-pusher.test.mjs +347 -0
  71. package/scaffold/mcp/tests/policy-check.test.mjs +220 -0
  72. package/scaffold/mcp/tests/repo-name.test.mjs +134 -0
  73. package/scaffold/mcp/tests/run.test.mjs +109 -0
  74. package/scaffold/mcp/tests/sync-checker.test.mjs +188 -0
  75. package/scaffold/mcp/tests/ungoverned-detector.test.mjs +191 -0
  76. package/scaffold/mcp/tests/ungoverned-scanner.test.mjs +198 -0
  77. package/scaffold/scripts/bootstrap.sh +0 -11
  78. package/scaffold/scripts/doctor.sh +24 -4
  79. package/types.js +5 -0
@@ -0,0 +1,415 @@
1
+ import path from "node:path";
2
+ import { randomUUID } from "node:crypto";
3
+ import { createRequire } from "node:module";
4
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import {
6
+ loadEnterpriseConfig,
7
+ resolveEnterpriseActivation,
8
+ type EnterpriseConfig,
9
+ } from "../core/config.js";
10
+ import { deployBundledModel } from "./model/deploy.js";
11
+ import { TelemetryCollector } from "../core/telemetry/collector.js";
12
+ import { pushMetrics } from "./telemetry/sync.js";
13
+ import { AuditWriter, type AuditEntry } from "../core/audit/writer.js";
14
+ import { pushAuditEvents, queueAuditEvent, setAuditPushContext } from "./audit/push.js";
15
+ import { PolicyStore } from "../core/policy/store.js";
16
+ import { syncFromCloud, syncFromLocal } from "./policy/sync.js";
17
+ import { registerEnterpriseTools } from "./tools/enterprise.js";
18
+ import { pushViolations, setViolationPushContext } from "./violations/push.js";
19
+ import { pushReviewResults, setReviewPushContext } from "./reviews/push.js";
20
+ import { setWorkflowPushContext } from "./workflow/push.js";
21
+
22
+ const require = createRequire(import.meta.url);
23
+ const pkg = require("../package.json") as { version: string };
24
+
25
+ export const name = "cortex-enterprise";
26
+ export const version: string = pkg.version;
27
+
28
+ const timers: NodeJS.Timeout[] = [];
29
+ let activeCollector: TelemetryCollector | null = null;
30
+ let activeConfig: EnterpriseConfig | null = null;
31
+ let activeAuditWriter: AuditWriter | null = null;
32
+ let activeInstanceId: string | null = null;
33
+ let activeSessionId: string | null = null;
34
+ let activeRepo: string | null = null;
35
+
36
+ async function flushComplianceQueues(
37
+ config: EnterpriseConfig,
38
+ reason: "periodic" | "shutdown",
39
+ ): Promise<void> {
40
+ const baseUrl = (config.enterprise.base_url || config.enterprise.endpoint).trim();
41
+ const apiKey = config.enterprise.api_key.trim();
42
+ if (!baseUrl || !apiKey) return;
43
+
44
+ try {
45
+ const result = await pushAuditEvents(baseUrl, apiKey);
46
+ if (!result.success) {
47
+ process.stderr.write(`[cortex-enterprise] ${reason} audit push failed: ${result.error}\n`);
48
+ }
49
+ } catch (err) {
50
+ process.stderr.write(`[cortex-enterprise] ${reason} audit push error: ${err}\n`);
51
+ }
52
+
53
+ try {
54
+ const result = await pushViolations(baseUrl, apiKey);
55
+ if (!result.success) {
56
+ process.stderr.write(`[cortex-enterprise] ${reason} violations push failed: ${result.error}\n`);
57
+ }
58
+ } catch (err) {
59
+ process.stderr.write(`[cortex-enterprise] ${reason} violations push error: ${err}\n`);
60
+ }
61
+
62
+ try {
63
+ const result = await pushReviewResults(baseUrl, apiKey);
64
+ if (!result.success) {
65
+ process.stderr.write(`[cortex-enterprise] ${reason} reviews push failed: ${result.error}\n`);
66
+ }
67
+ } catch (err) {
68
+ process.stderr.write(`[cortex-enterprise] ${reason} reviews push error: ${err}\n`);
69
+ }
70
+ }
71
+
72
+ type ToolExecutionEvent = {
73
+ phase: "start" | "success" | "error";
74
+ tool: string;
75
+ timestamp: string;
76
+ input: Record<string, unknown>;
77
+ query?: string;
78
+ query_length?: number;
79
+ result_count?: number;
80
+ estimated_tokens_saved?: number;
81
+ entities_returned?: string[];
82
+ rules_applied?: string[];
83
+ duration_ms?: number;
84
+ error?: string;
85
+ };
86
+
87
+ type SessionCallRecord = {
88
+ tool: string;
89
+ query?: string;
90
+ resultCount: number;
91
+ time: string;
92
+ outcome?: "success" | "error";
93
+ duration_ms?: number;
94
+ error?: string;
95
+ };
96
+
97
+ type SessionEvent = {
98
+ phase: "start" | "end";
99
+ timestamp: string;
100
+ duration_ms?: number;
101
+ tool_calls?: number;
102
+ successful_tool_calls?: number;
103
+ failed_tool_calls?: number;
104
+ calls?: SessionCallRecord[];
105
+ };
106
+
107
+ export function shutdown(): void {
108
+ for (const t of timers) clearInterval(t);
109
+ timers.length = 0;
110
+ activeCollector = null;
111
+ activeConfig = null;
112
+ activeAuditWriter = null;
113
+ activeInstanceId = null;
114
+ activeSessionId = null;
115
+ activeRepo = null;
116
+ setAuditPushContext({});
117
+ setViolationPushContext({});
118
+ setReviewPushContext({});
119
+ setWorkflowPushContext({});
120
+ }
121
+
122
+ /**
123
+ * Telemetry hook called by cortex core after each tool execution.
124
+ * Wired up via the CortexPlugin.onToolCall interface.
125
+ */
126
+ export function onToolCall(toolName: string, resultCount: number, tokensSaved: number): void {
127
+ activeCollector?.record(toolName, resultCount, tokensSaved);
128
+ }
129
+
130
+ export function onToolEvent(event: ToolExecutionEvent): void {
131
+ if (event.phase === "success" || event.phase === "error") {
132
+ activeCollector?.recordEvent({
133
+ tool: event.tool,
134
+ phase: event.phase,
135
+ result_count: event.result_count,
136
+ estimated_tokens_saved: event.estimated_tokens_saved,
137
+ duration_ms: event.duration_ms,
138
+ });
139
+ }
140
+
141
+ if ((event.phase === "success" || event.phase === "error") && activeAuditWriter) {
142
+ activeAuditWriter.log({
143
+ timestamp: event.timestamp,
144
+ tool: event.tool,
145
+ input: event.input,
146
+ result_count: event.result_count ?? 0,
147
+ entities_returned: event.entities_returned ?? [],
148
+ rules_applied: event.rules_applied ?? [],
149
+ duration_ms: event.duration_ms ?? 0,
150
+ status: event.phase,
151
+ error: event.error,
152
+ event_type: "tool_call",
153
+ evidence_level: "diagnostic",
154
+ resource_type: "context_tool",
155
+ repo: activeRepo ?? undefined,
156
+ instance_id: activeInstanceId ?? undefined,
157
+ session_id: activeSessionId ?? undefined,
158
+ metadata:
159
+ event.query_length !== undefined
160
+ ? {
161
+ query_present: true,
162
+ query_length: event.query_length,
163
+ }
164
+ : undefined,
165
+ });
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Unified entry point for enterprise-tool activity. Replaces the previous
171
+ * `auditWriter.log({...})` pattern: records the same audit entry AND bumps
172
+ * the telemetry collector so dashboard counters move. Callers pass the
173
+ * existing audit-shape object plus an optional `tokens_saved` (defaults 0).
174
+ */
175
+ export type ToolActivity = AuditEntry & {
176
+ tokens_saved?: number;
177
+ };
178
+
179
+ export function recordToolActivity(activity: ToolActivity): void {
180
+ const status = activity.status ?? "success";
181
+
182
+ activeAuditWriter?.log({
183
+ ...activity,
184
+ status,
185
+ repo: activity.repo ?? activeRepo ?? undefined,
186
+ instance_id: activity.instance_id ?? activeInstanceId ?? undefined,
187
+ session_id: activity.session_id ?? activeSessionId ?? undefined,
188
+ });
189
+
190
+ activeCollector?.recordEvent({
191
+ tool: activity.tool,
192
+ phase: status,
193
+ result_count: activity.result_count,
194
+ estimated_tokens_saved: activity.tokens_saved ?? 0,
195
+ duration_ms: activity.duration_ms,
196
+ });
197
+ }
198
+
199
+ /**
200
+ * Session-end hook called by cortex core on shutdown.
201
+ * Awaited with a timeout — this is the reliable telemetry push path.
202
+ */
203
+ export async function onSessionEnd(): Promise<void> {
204
+ if (!activeConfig) return;
205
+ const config = activeConfig;
206
+ if (config.telemetry.enabled && config.telemetry.endpoint && activeCollector) {
207
+ activeCollector.flush();
208
+ try {
209
+ const result = await pushMetrics(
210
+ activeCollector.getMetrics(),
211
+ config.telemetry.endpoint,
212
+ config.telemetry.api_key,
213
+ {
214
+ repo: activeRepo ?? undefined,
215
+ session_id: activeSessionId ?? undefined,
216
+ },
217
+ );
218
+ if (!result.success) {
219
+ process.stderr.write(`[cortex-enterprise] Shutdown telemetry push failed: ${result.error}\n`);
220
+ }
221
+ } catch (err) {
222
+ process.stderr.write(`[cortex-enterprise] Shutdown telemetry push error: ${err}\n`);
223
+ }
224
+ }
225
+
226
+ await flushComplianceQueues(config, "shutdown");
227
+ }
228
+
229
+ export async function onSessionEvent(event: SessionEvent): Promise<void> {
230
+ if (event.phase === "start") {
231
+ activeCollector?.recordSessionStart();
232
+ return;
233
+ }
234
+
235
+ if (event.phase === "end") {
236
+ activeCollector?.recordSessionEnd(event.duration_ms ?? 0);
237
+ if (activeAuditWriter) {
238
+ activeAuditWriter.log({
239
+ timestamp: event.timestamp,
240
+ tool: "session.summary",
241
+ input: {
242
+ tool_calls: event.tool_calls ?? 0,
243
+ successful_tool_calls: event.successful_tool_calls ?? 0,
244
+ failed_tool_calls: event.failed_tool_calls ?? 0,
245
+ },
246
+ result_count: event.tool_calls ?? 0,
247
+ entities_returned: [],
248
+ rules_applied: [],
249
+ duration_ms: event.duration_ms ?? 0,
250
+ status: "success",
251
+ event_type: "session",
252
+ evidence_level: "diagnostic",
253
+ resource_type: "session",
254
+ repo: activeRepo ?? undefined,
255
+ instance_id: activeInstanceId ?? undefined,
256
+ session_id: activeSessionId ?? undefined,
257
+ });
258
+ }
259
+ }
260
+ }
261
+
262
+ export async function register(server: McpServer): Promise<void> {
263
+ const projectRoot = process.env.CORTEX_PROJECT_ROOT?.trim() || process.cwd();
264
+ const contextDir = path.join(projectRoot, ".context");
265
+
266
+ const config = loadEnterpriseConfig(contextDir);
267
+ const activation = resolveEnterpriseActivation(config);
268
+ if (!activation.active) {
269
+ process.stderr.write(
270
+ `[cortex-enterprise] cloud features inactive: ${activation.reason}\n`
271
+ );
272
+ }
273
+
274
+ activeConfig = config;
275
+
276
+ // Deploy bundled embedding model if not already cached
277
+ const modelDeployed = deployBundledModel(contextDir);
278
+ if (modelDeployed) {
279
+ process.stderr.write(`[cortex-enterprise] Bundled embedding model deployed\n`);
280
+ }
281
+
282
+ // Initialize subsystems
283
+ const collector = new TelemetryCollector(contextDir, version);
284
+ activeCollector = collector;
285
+ activeInstanceId = collector.getMetrics().instance_id;
286
+ activeSessionId = randomUUID();
287
+ activeRepo = path.basename(projectRoot);
288
+ const auditWriter = config.audit.enabled
289
+ ? new AuditWriter(contextDir, {
290
+ onEntry(entry) {
291
+ queueAuditEvent(entry);
292
+ },
293
+ })
294
+ : null;
295
+ activeAuditWriter = auditWriter;
296
+ const policyStore = new PolicyStore(contextDir);
297
+
298
+ setAuditPushContext({
299
+ repo: activeRepo ?? undefined,
300
+ instance_id: activeInstanceId ?? undefined,
301
+ session_id: activeSessionId ?? undefined,
302
+ });
303
+ setViolationPushContext({
304
+ repo: activeRepo ?? undefined,
305
+ instance_id: activeInstanceId ?? undefined,
306
+ session_id: activeSessionId ?? undefined,
307
+ });
308
+ setReviewPushContext({
309
+ repo: activeRepo ?? undefined,
310
+ instance_id: activeInstanceId ?? undefined,
311
+ session_id: activeSessionId ?? undefined,
312
+ });
313
+ setWorkflowPushContext({
314
+ repo: activeRepo ?? undefined,
315
+ instance_id: activeInstanceId ?? undefined,
316
+ session_id: activeSessionId ?? undefined,
317
+ });
318
+
319
+ // Initial policy sync
320
+ if (config.policy.enabled) {
321
+ if (config.policy.endpoint && config.policy.api_key) {
322
+ await syncFromCloud(config.policy.endpoint, config.policy.api_key, policyStore, {
323
+ instance_id: activeInstanceId ?? undefined,
324
+ session_id: activeSessionId ?? undefined,
325
+ });
326
+ process.stderr.write(`[cortex-enterprise] Policy sync: cloud\n`);
327
+ } else {
328
+ syncFromLocal(policyStore);
329
+ const orgCount = policyStore.loadOrgPolicies().length;
330
+ if (orgCount > 0) {
331
+ process.stderr.write(`[cortex-enterprise] Policy sync: ${orgCount} org rules loaded\n`);
332
+ }
333
+ }
334
+ }
335
+
336
+ registerEnterpriseTools(server, collector, auditWriter, config, contextDir, policyStore, version);
337
+
338
+ // v2.0.0: globalThis.__cortexContextToolHook bridge removed.
339
+ // Enterprise is now in-process with cortex-mcp; tool events flow via
340
+ // plugin.ts's onToolEvent hook directly through activation.ts.
341
+
342
+ process.stderr.write(`[cortex-enterprise] v${version}\n`);
343
+
344
+ // Log active features
345
+ const features: string[] = [];
346
+ if (config.telemetry.enabled) features.push("telemetry");
347
+ if (config.audit.enabled) features.push("audit");
348
+ if (config.policy.enabled) features.push("policy");
349
+ if (config.rbac.enabled) features.push(`rbac(${config.rbac.default_role})`);
350
+ if (features.length > 0) {
351
+ process.stderr.write(`[cortex-enterprise] Active: ${features.join(", ")}\n`);
352
+ }
353
+
354
+ // Schedule telemetry flush + push
355
+ if (config.telemetry.enabled) {
356
+ // Push any accumulated metrics from previous sessions on startup
357
+ if (config.telemetry.endpoint) {
358
+ pushMetrics(collector.getMetrics(), config.telemetry.endpoint, config.telemetry.api_key, {
359
+ repo: activeRepo ?? undefined,
360
+ session_id: activeSessionId ?? undefined,
361
+ })
362
+ .then((r) => { if (!r.success) process.stderr.write(`[cortex-enterprise] Startup telemetry push failed: ${r.error}\n`); })
363
+ .catch((err) => { process.stderr.write(`[cortex-enterprise] Startup telemetry push error: ${err}\n`); });
364
+ }
365
+
366
+ const intervalMs = config.telemetry.interval_minutes * 60000;
367
+ const timer = setInterval(async () => {
368
+ try {
369
+ collector.flush();
370
+ if (config.telemetry.endpoint) {
371
+ await pushMetrics(collector.getMetrics(), config.telemetry.endpoint, config.telemetry.api_key, {
372
+ repo: activeRepo ?? undefined,
373
+ session_id: activeSessionId ?? undefined,
374
+ });
375
+ }
376
+ } catch (err) {
377
+ process.stderr.write(`[cortex-enterprise] Telemetry flush error: ${err}\n`);
378
+ }
379
+ }, intervalMs);
380
+ timer.unref();
381
+ timers.push(timer);
382
+ }
383
+
384
+ // Schedule compliance queue flushes independently from telemetry so
385
+ // policy evidence is still delivered when metrics collection is off.
386
+ if (config.policy.enabled && config.policy.endpoint && config.policy.api_key) {
387
+ const intervalMs =
388
+ (config.telemetry.enabled
389
+ ? config.telemetry.interval_minutes
390
+ : config.policy.sync_interval_minutes) * 60000;
391
+ const timer = setInterval(async () => {
392
+ await flushComplianceQueues(config, "periodic");
393
+ }, intervalMs);
394
+ timer.unref();
395
+ timers.push(timer);
396
+ }
397
+
398
+ // Schedule policy sync
399
+ if (config.policy.enabled && config.policy.endpoint && config.policy.api_key) {
400
+ const intervalMs = config.policy.sync_interval_minutes * 60000;
401
+ const timer = setInterval(async () => {
402
+ try {
403
+ await syncFromCloud(config.policy.endpoint, config.policy.api_key, policyStore, {
404
+ instance_id: activeInstanceId ?? undefined,
405
+ session_id: activeSessionId ?? undefined,
406
+ });
407
+ } catch (err) {
408
+ process.stderr.write(`[cortex-enterprise] Policy sync error: ${err}\n`);
409
+ }
410
+ }, intervalMs);
411
+ timer.unref();
412
+ timers.push(timer);
413
+ }
414
+
415
+ }
@@ -0,0 +1,33 @@
1
+ import { cpSync, existsSync, mkdirSync } from "node:fs";
2
+ import { join, dirname } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = dirname(__filename);
7
+
8
+ const BUNDLED_MODEL_DIR = join(__dirname, "..", "..", "models");
9
+ const MODEL_NAME = "Xenova/all-MiniLM-L6-v2";
10
+
11
+ /**
12
+ * If the project doesn't have the embedding model cached,
13
+ * copy it from the bundled models in this package.
14
+ * Returns true if a model was deployed, false if already present or no bundle.
15
+ */
16
+ export function deployBundledModel(contextDir: string): boolean {
17
+ const targetDir = join(contextDir, "embeddings", "models", MODEL_NAME);
18
+ const sourceDir = join(BUNDLED_MODEL_DIR, MODEL_NAME);
19
+
20
+ // Already cached — nothing to do
21
+ if (existsSync(join(targetDir, "onnx", "model_quantized.onnx"))) {
22
+ return false;
23
+ }
24
+
25
+ // No bundled model in this package
26
+ if (!existsSync(join(sourceDir, "onnx", "model_quantized.onnx"))) {
27
+ return false;
28
+ }
29
+
30
+ mkdirSync(targetDir, { recursive: true });
31
+ cpSync(sourceDir, targetDir, { recursive: true });
32
+ return true;
33
+ }
@@ -0,0 +1,146 @@
1
+ import { z } from "zod";
2
+ import type { OrgPolicy, PolicyStore } from "../../core/policy/store.js";
3
+
4
+ const CloudPolicySchema = z.object({
5
+ id: z.string().min(1).max(200),
6
+ title: z.string().min(1).max(200).optional(),
7
+ kind: z.enum(["predefined", "custom"]).optional(),
8
+ status: z.enum(["draft", "active", "disabled", "archived"]).optional(),
9
+ severity: z.enum(["info", "warning", "error", "block"]).optional(),
10
+ description: z.string().max(1000).default(""),
11
+ priority: z.number().int().min(0).max(1000).default(50),
12
+ scope: z.string().max(200).default("global"),
13
+ enforce: z.boolean().default(true),
14
+ type: z.string().min(1).max(100).nullable().optional(),
15
+ config: z.record(z.string(), z.unknown()).nullable().optional(),
16
+ });
17
+
18
+ const CloudResponseSchema = z.object({
19
+ rules: z.array(CloudPolicySchema).default([]),
20
+ });
21
+
22
+ export type SyncResult = {
23
+ success: boolean;
24
+ synced: number;
25
+ source: "cloud" | "local";
26
+ timestamp: string;
27
+ error?: string;
28
+ };
29
+
30
+ let lastSync: SyncResult | null = null;
31
+
32
+ export type SyncContext = {
33
+ instance_id?: string;
34
+ session_id?: string;
35
+ };
36
+
37
+ export function getLastSync(): SyncResult | null {
38
+ return lastSync;
39
+ }
40
+
41
+ /**
42
+ * Pull org-wide policies from the Cortex Cloud API (connected edition).
43
+ * The actual cloud API is built in Phase 4 — this sends a GET
44
+ * and expects a JSON array of policy objects.
45
+ */
46
+ export async function syncFromCloud(
47
+ endpoint: string,
48
+ apiKey: string,
49
+ store: PolicyStore,
50
+ context: SyncContext = {},
51
+ ): Promise<SyncResult> {
52
+ if (!endpoint || !apiKey) {
53
+ const result: SyncResult = {
54
+ success: false,
55
+ synced: 0,
56
+ source: "cloud",
57
+ timestamp: new Date().toISOString(),
58
+ error: "endpoint or api_key not configured",
59
+ };
60
+ lastSync = result;
61
+ return result;
62
+ }
63
+
64
+ try {
65
+ const response = await fetch(endpoint, {
66
+ method: "GET",
67
+ headers: {
68
+ "Authorization": `Bearer ${apiKey}`,
69
+ "Accept": "application/json",
70
+ ...(context.instance_id
71
+ ? { "x-cortex-instance-id": context.instance_id }
72
+ : {}),
73
+ ...(context.session_id
74
+ ? { "x-cortex-session-id": context.session_id }
75
+ : {}),
76
+ },
77
+ signal: AbortSignal.timeout(10000),
78
+ });
79
+
80
+ if (!response.ok) {
81
+ const result: SyncResult = {
82
+ success: false,
83
+ synced: 0,
84
+ source: "cloud",
85
+ timestamp: new Date().toISOString(),
86
+ error: `HTTP ${response.status}`,
87
+ };
88
+ lastSync = result;
89
+ return result;
90
+ }
91
+
92
+ const raw = await response.json();
93
+ const data = CloudResponseSchema.parse(raw);
94
+ const policies: OrgPolicy[] = data.rules.map((r) => ({
95
+ id: r.id,
96
+ title: r.title ?? r.id,
97
+ kind: r.kind ?? null,
98
+ status: r.status ?? "active",
99
+ severity: r.severity ?? "block",
100
+ description: r.description,
101
+ priority: r.priority,
102
+ scope: r.scope,
103
+ enforce: r.enforce,
104
+ type: r.type ?? null,
105
+ config: r.config ?? null,
106
+ source: "org" as const,
107
+ }));
108
+
109
+ store.writeOrgPolicies(policies);
110
+
111
+ const result: SyncResult = {
112
+ success: true,
113
+ synced: policies.length,
114
+ source: "cloud",
115
+ timestamp: new Date().toISOString(),
116
+ };
117
+ lastSync = result;
118
+ return result;
119
+ } catch (err) {
120
+ const result: SyncResult = {
121
+ success: false,
122
+ synced: 0,
123
+ source: "cloud",
124
+ timestamp: new Date().toISOString(),
125
+ error: err instanceof Error ? err.message : "unknown error",
126
+ };
127
+ lastSync = result;
128
+ return result;
129
+ }
130
+ }
131
+
132
+ /**
133
+ * For air-gapped: just reload from local org-rules.yaml file.
134
+ * Returns the count of policies found.
135
+ */
136
+ export function syncFromLocal(store: PolicyStore): SyncResult {
137
+ const policies = store.loadOrgPolicies();
138
+ const result: SyncResult = {
139
+ success: true,
140
+ synced: policies.length,
141
+ source: "local",
142
+ timestamp: new Date().toISOString(),
143
+ };
144
+ lastSync = result;
145
+ return result;
146
+ }