@danielblomma/cortex-mcp 1.7.2 → 2.0.3
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.
- package/README.md +4 -24
- package/bin/cortex.mjs +679 -32
- package/bin/style.mjs +349 -0
- package/package.json +4 -3
- package/scaffold/mcp/src/cli/enterprise-setup.ts +124 -0
- package/scaffold/mcp/src/cli/govern.ts +987 -0
- package/scaffold/mcp/src/cli/run.ts +306 -0
- package/scaffold/mcp/src/cli/telemetry-test.ts +158 -0
- package/scaffold/mcp/src/cli/ungoverned-detector.ts +168 -0
- package/scaffold/mcp/src/core/audit/query.ts +81 -0
- package/scaffold/mcp/src/core/audit/writer.ts +68 -0
- package/scaffold/mcp/src/core/config.ts +329 -0
- package/scaffold/mcp/src/core/index.ts +34 -0
- package/scaffold/mcp/src/core/license.ts +202 -0
- package/scaffold/mcp/src/core/policy/enforce.ts +98 -0
- package/scaffold/mcp/src/core/policy/injection.ts +229 -0
- package/scaffold/mcp/src/core/policy/store.ts +197 -0
- package/scaffold/mcp/src/core/rbac/check.ts +40 -0
- package/scaffold/mcp/src/core/telemetry/collector.ts +408 -0
- package/scaffold/mcp/src/core/validators/builtins.ts +711 -0
- package/scaffold/mcp/src/core/validators/config.ts +47 -0
- package/scaffold/mcp/src/core/validators/engine.ts +199 -0
- package/scaffold/mcp/src/core/validators/evaluators/code_comments.ts +294 -0
- package/scaffold/mcp/src/core/validators/evaluators/regex.ts +144 -0
- package/scaffold/mcp/src/daemon/client.ts +155 -0
- package/scaffold/mcp/src/daemon/egress-proxy.ts +331 -0
- package/scaffold/mcp/src/daemon/heartbeat-pusher.ts +147 -0
- package/scaffold/mcp/src/daemon/heartbeat-tracker.ts +223 -0
- package/scaffold/mcp/src/daemon/host-events-pusher.ts +285 -0
- package/scaffold/mcp/src/daemon/main.ts +435 -0
- package/scaffold/mcp/src/daemon/paths.ts +41 -0
- package/scaffold/mcp/src/daemon/protocol.ts +101 -0
- package/scaffold/mcp/src/daemon/server.ts +227 -0
- package/scaffold/mcp/src/daemon/sync-checker.ts +213 -0
- package/scaffold/mcp/src/daemon/ungoverned-scanner.ts +149 -0
- package/scaffold/mcp/src/enterprise/audit/push.ts +84 -0
- package/scaffold/mcp/src/enterprise/index.ts +386 -0
- package/scaffold/mcp/src/enterprise/model/deploy.ts +33 -0
- package/scaffold/mcp/src/enterprise/policy/sync.ts +146 -0
- package/scaffold/mcp/src/enterprise/privacy/boundary.ts +214 -0
- package/scaffold/mcp/src/enterprise/reviews/push.ts +79 -0
- package/scaffold/mcp/src/enterprise/telemetry/sync.ts +73 -0
- package/scaffold/mcp/src/enterprise/tools/enterprise.ts +1031 -0
- package/scaffold/mcp/src/enterprise/tools/walk.ts +79 -0
- package/scaffold/mcp/src/enterprise/violations/push.ts +102 -0
- package/scaffold/mcp/src/enterprise/workflow/push.ts +60 -0
- package/scaffold/mcp/src/enterprise/workflow/state.ts +535 -0
- package/scaffold/mcp/src/hooks/pre-compact.ts +54 -0
- package/scaffold/mcp/src/hooks/pre-tool-use.ts +96 -0
- package/scaffold/mcp/src/hooks/session-end.ts +73 -0
- package/scaffold/mcp/src/hooks/session-start.ts +78 -0
- package/scaffold/mcp/src/hooks/shared.ts +134 -0
- package/scaffold/mcp/src/hooks/stop.ts +60 -0
- package/scaffold/mcp/src/hooks/user-prompt-submit.ts +64 -0
- package/scaffold/mcp/src/loadGraph.ts +2 -0
- package/scaffold/mcp/src/plugin.ts +150 -0
- package/scaffold/mcp/src/server.ts +218 -7
- package/scaffold/mcp/tests/copilot-shim.test.mjs +146 -0
- package/scaffold/mcp/tests/daemon-client.test.mjs +32 -0
- package/scaffold/mcp/tests/egress-proxy.test.mjs +239 -0
- package/scaffold/mcp/tests/enterprise-config.test.mjs +154 -0
- package/scaffold/mcp/tests/govern-install.test.mjs +320 -0
- package/scaffold/mcp/tests/govern-repair.test.mjs +157 -0
- package/scaffold/mcp/tests/govern-status.test.mjs +538 -0
- package/scaffold/mcp/tests/govern.test.mjs +74 -0
- package/scaffold/mcp/tests/heartbeat-pusher.test.mjs +154 -0
- package/scaffold/mcp/tests/heartbeat-tracker.test.mjs +237 -0
- package/scaffold/mcp/tests/host-events-pusher.test.mjs +347 -0
- package/scaffold/mcp/tests/policy-check.test.mjs +220 -0
- package/scaffold/mcp/tests/repo-name.test.mjs +134 -0
- package/scaffold/mcp/tests/run.test.mjs +109 -0
- package/scaffold/mcp/tests/sync-checker.test.mjs +188 -0
- package/scaffold/mcp/tests/telemetry-collector.test.mjs +30 -0
- package/scaffold/mcp/tests/ungoverned-detector.test.mjs +191 -0
- package/scaffold/mcp/tests/ungoverned-scanner.test.mjs +198 -0
- package/scaffold/scripts/bootstrap.sh +0 -11
- package/scaffold/scripts/doctor.sh +24 -4
- package/types.js +5 -0
- package/docs/MCP_MARKETPLACE.md +0 -160
|
@@ -0,0 +1,386 @@
|
|
|
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 { AuditWriter, type AuditEntry } from "../core/audit/writer.js";
|
|
13
|
+
import { pushAuditEvents, queueAuditEvent, setAuditPushContext } from "./audit/push.js";
|
|
14
|
+
import { PolicyStore } from "../core/policy/store.js";
|
|
15
|
+
import { syncFromCloud, syncFromLocal } from "./policy/sync.js";
|
|
16
|
+
import { registerEnterpriseTools } from "./tools/enterprise.js";
|
|
17
|
+
import { pushViolations, setViolationPushContext } from "./violations/push.js";
|
|
18
|
+
import { pushReviewResults, setReviewPushContext } from "./reviews/push.js";
|
|
19
|
+
import { setWorkflowPushContext } from "./workflow/push.js";
|
|
20
|
+
|
|
21
|
+
const require = createRequire(import.meta.url);
|
|
22
|
+
const pkg = require("../package.json") as { version: string };
|
|
23
|
+
|
|
24
|
+
export const name = "cortex-enterprise";
|
|
25
|
+
export const version: string = pkg.version;
|
|
26
|
+
|
|
27
|
+
const timers: NodeJS.Timeout[] = [];
|
|
28
|
+
let activeCollector: TelemetryCollector | null = null;
|
|
29
|
+
let activeConfig: EnterpriseConfig | null = null;
|
|
30
|
+
let activeAuditWriter: AuditWriter | null = null;
|
|
31
|
+
let activeInstanceId: string | null = null;
|
|
32
|
+
let activeSessionId: string | null = null;
|
|
33
|
+
let activeRepo: string | null = null;
|
|
34
|
+
|
|
35
|
+
async function flushComplianceQueues(
|
|
36
|
+
config: EnterpriseConfig,
|
|
37
|
+
reason: "periodic" | "shutdown",
|
|
38
|
+
): Promise<void> {
|
|
39
|
+
const baseUrl = (config.enterprise.base_url || config.enterprise.endpoint).trim();
|
|
40
|
+
const apiKey = config.enterprise.api_key.trim();
|
|
41
|
+
if (!baseUrl || !apiKey) return;
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const result = await pushAuditEvents(baseUrl, apiKey);
|
|
45
|
+
if (!result.success) {
|
|
46
|
+
process.stderr.write(`[cortex-enterprise] ${reason} audit push failed: ${result.error}\n`);
|
|
47
|
+
}
|
|
48
|
+
} catch (err) {
|
|
49
|
+
process.stderr.write(`[cortex-enterprise] ${reason} audit push error: ${err}\n`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const result = await pushViolations(baseUrl, apiKey);
|
|
54
|
+
if (!result.success) {
|
|
55
|
+
process.stderr.write(`[cortex-enterprise] ${reason} violations push failed: ${result.error}\n`);
|
|
56
|
+
}
|
|
57
|
+
} catch (err) {
|
|
58
|
+
process.stderr.write(`[cortex-enterprise] ${reason} violations push error: ${err}\n`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const result = await pushReviewResults(baseUrl, apiKey);
|
|
63
|
+
if (!result.success) {
|
|
64
|
+
process.stderr.write(`[cortex-enterprise] ${reason} reviews push failed: ${result.error}\n`);
|
|
65
|
+
}
|
|
66
|
+
} catch (err) {
|
|
67
|
+
process.stderr.write(`[cortex-enterprise] ${reason} reviews push error: ${err}\n`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
type ToolExecutionEvent = {
|
|
72
|
+
phase: "start" | "success" | "error";
|
|
73
|
+
tool: string;
|
|
74
|
+
timestamp: string;
|
|
75
|
+
input: Record<string, unknown>;
|
|
76
|
+
query?: string;
|
|
77
|
+
query_length?: number;
|
|
78
|
+
result_count?: number;
|
|
79
|
+
estimated_tokens_saved?: number;
|
|
80
|
+
entities_returned?: string[];
|
|
81
|
+
rules_applied?: string[];
|
|
82
|
+
duration_ms?: number;
|
|
83
|
+
error?: string;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
type SessionCallRecord = {
|
|
87
|
+
tool: string;
|
|
88
|
+
query?: string;
|
|
89
|
+
resultCount: number;
|
|
90
|
+
time: string;
|
|
91
|
+
outcome?: "success" | "error";
|
|
92
|
+
duration_ms?: number;
|
|
93
|
+
error?: string;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
type SessionEvent = {
|
|
97
|
+
phase: "start" | "end";
|
|
98
|
+
timestamp: string;
|
|
99
|
+
duration_ms?: number;
|
|
100
|
+
tool_calls?: number;
|
|
101
|
+
successful_tool_calls?: number;
|
|
102
|
+
failed_tool_calls?: number;
|
|
103
|
+
calls?: SessionCallRecord[];
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export function shutdown(): void {
|
|
107
|
+
for (const t of timers) clearInterval(t);
|
|
108
|
+
timers.length = 0;
|
|
109
|
+
activeCollector = null;
|
|
110
|
+
activeConfig = null;
|
|
111
|
+
activeAuditWriter = null;
|
|
112
|
+
activeInstanceId = null;
|
|
113
|
+
activeSessionId = null;
|
|
114
|
+
activeRepo = null;
|
|
115
|
+
setAuditPushContext({});
|
|
116
|
+
setViolationPushContext({});
|
|
117
|
+
setReviewPushContext({});
|
|
118
|
+
setWorkflowPushContext({});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Telemetry hook called by cortex core after each tool execution.
|
|
123
|
+
* Wired up via the CortexPlugin.onToolCall interface.
|
|
124
|
+
*/
|
|
125
|
+
export function onToolCall(toolName: string, resultCount: number, tokensSaved: number): void {
|
|
126
|
+
activeCollector?.record(toolName, resultCount, tokensSaved);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function onToolEvent(event: ToolExecutionEvent): void {
|
|
130
|
+
if (event.phase === "success" || event.phase === "error") {
|
|
131
|
+
activeCollector?.recordEvent({
|
|
132
|
+
tool: event.tool,
|
|
133
|
+
phase: event.phase,
|
|
134
|
+
result_count: event.result_count,
|
|
135
|
+
estimated_tokens_saved: event.estimated_tokens_saved,
|
|
136
|
+
duration_ms: event.duration_ms,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if ((event.phase === "success" || event.phase === "error") && activeAuditWriter) {
|
|
141
|
+
activeAuditWriter.log({
|
|
142
|
+
timestamp: event.timestamp,
|
|
143
|
+
tool: event.tool,
|
|
144
|
+
input: event.input,
|
|
145
|
+
result_count: event.result_count ?? 0,
|
|
146
|
+
entities_returned: event.entities_returned ?? [],
|
|
147
|
+
rules_applied: event.rules_applied ?? [],
|
|
148
|
+
duration_ms: event.duration_ms ?? 0,
|
|
149
|
+
status: event.phase,
|
|
150
|
+
error: event.error,
|
|
151
|
+
event_type: "tool_call",
|
|
152
|
+
evidence_level: "diagnostic",
|
|
153
|
+
resource_type: "context_tool",
|
|
154
|
+
repo: activeRepo ?? undefined,
|
|
155
|
+
instance_id: activeInstanceId ?? undefined,
|
|
156
|
+
session_id: activeSessionId ?? undefined,
|
|
157
|
+
metadata:
|
|
158
|
+
event.query_length !== undefined
|
|
159
|
+
? {
|
|
160
|
+
query_present: true,
|
|
161
|
+
query_length: event.query_length,
|
|
162
|
+
}
|
|
163
|
+
: undefined,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Unified entry point for enterprise-tool activity. Replaces the previous
|
|
170
|
+
* `auditWriter.log({...})` pattern: records the same audit entry AND bumps
|
|
171
|
+
* the telemetry collector so dashboard counters move. Callers pass the
|
|
172
|
+
* existing audit-shape object plus an optional `tokens_saved` (defaults 0).
|
|
173
|
+
*/
|
|
174
|
+
export type ToolActivity = AuditEntry & {
|
|
175
|
+
tokens_saved?: number;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
export function recordToolActivity(activity: ToolActivity): void {
|
|
179
|
+
const status = activity.status ?? "success";
|
|
180
|
+
|
|
181
|
+
activeAuditWriter?.log({
|
|
182
|
+
...activity,
|
|
183
|
+
status,
|
|
184
|
+
repo: activity.repo ?? activeRepo ?? undefined,
|
|
185
|
+
instance_id: activity.instance_id ?? activeInstanceId ?? undefined,
|
|
186
|
+
session_id: activity.session_id ?? activeSessionId ?? undefined,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
activeCollector?.recordEvent({
|
|
190
|
+
tool: activity.tool,
|
|
191
|
+
phase: status,
|
|
192
|
+
result_count: activity.result_count,
|
|
193
|
+
estimated_tokens_saved: activity.tokens_saved ?? 0,
|
|
194
|
+
duration_ms: activity.duration_ms,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Session-end hook called by cortex core on shutdown.
|
|
200
|
+
* Awaited with a timeout — this is the reliable telemetry push path.
|
|
201
|
+
*/
|
|
202
|
+
export async function onSessionEnd(): Promise<void> {
|
|
203
|
+
if (!activeConfig) return;
|
|
204
|
+
const config = activeConfig;
|
|
205
|
+
// Telemetry push is owned by the daemon. MCP only persists in-memory
|
|
206
|
+
// metrics to disk so the daemon can pick them up on its next push tick.
|
|
207
|
+
if (config.telemetry.enabled && activeCollector) {
|
|
208
|
+
activeCollector.flush();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
await flushComplianceQueues(config, "shutdown");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export async function onSessionEvent(event: SessionEvent): Promise<void> {
|
|
215
|
+
if (event.phase === "start") {
|
|
216
|
+
activeCollector?.recordSessionStart();
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (event.phase === "end") {
|
|
221
|
+
activeCollector?.recordSessionEnd(event.duration_ms ?? 0);
|
|
222
|
+
if (activeAuditWriter) {
|
|
223
|
+
activeAuditWriter.log({
|
|
224
|
+
timestamp: event.timestamp,
|
|
225
|
+
tool: "session.summary",
|
|
226
|
+
input: {
|
|
227
|
+
tool_calls: event.tool_calls ?? 0,
|
|
228
|
+
successful_tool_calls: event.successful_tool_calls ?? 0,
|
|
229
|
+
failed_tool_calls: event.failed_tool_calls ?? 0,
|
|
230
|
+
},
|
|
231
|
+
result_count: event.tool_calls ?? 0,
|
|
232
|
+
entities_returned: [],
|
|
233
|
+
rules_applied: [],
|
|
234
|
+
duration_ms: event.duration_ms ?? 0,
|
|
235
|
+
status: "success",
|
|
236
|
+
event_type: "session",
|
|
237
|
+
evidence_level: "diagnostic",
|
|
238
|
+
resource_type: "session",
|
|
239
|
+
repo: activeRepo ?? undefined,
|
|
240
|
+
instance_id: activeInstanceId ?? undefined,
|
|
241
|
+
session_id: activeSessionId ?? undefined,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export async function register(server: McpServer): Promise<void> {
|
|
248
|
+
const projectRoot = process.env.CORTEX_PROJECT_ROOT?.trim() || process.cwd();
|
|
249
|
+
const contextDir = path.join(projectRoot, ".context");
|
|
250
|
+
|
|
251
|
+
const config = loadEnterpriseConfig(contextDir);
|
|
252
|
+
const activation = resolveEnterpriseActivation(config);
|
|
253
|
+
if (!activation.active) {
|
|
254
|
+
process.stderr.write(
|
|
255
|
+
`[cortex-enterprise] cloud features inactive: ${activation.reason}\n`
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
activeConfig = config;
|
|
260
|
+
|
|
261
|
+
// Deploy bundled embedding model if not already cached
|
|
262
|
+
const modelDeployed = deployBundledModel(contextDir);
|
|
263
|
+
if (modelDeployed) {
|
|
264
|
+
process.stderr.write(`[cortex-enterprise] Bundled embedding model deployed\n`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Initialize subsystems
|
|
268
|
+
const collector = new TelemetryCollector(contextDir, version);
|
|
269
|
+
activeCollector = collector;
|
|
270
|
+
activeInstanceId = collector.getMetrics().instance_id;
|
|
271
|
+
activeSessionId = randomUUID();
|
|
272
|
+
activeRepo = path.basename(projectRoot);
|
|
273
|
+
const auditWriter = config.audit.enabled
|
|
274
|
+
? new AuditWriter(contextDir, {
|
|
275
|
+
onEntry(entry) {
|
|
276
|
+
queueAuditEvent(entry);
|
|
277
|
+
},
|
|
278
|
+
})
|
|
279
|
+
: null;
|
|
280
|
+
activeAuditWriter = auditWriter;
|
|
281
|
+
const policyStore = new PolicyStore(contextDir);
|
|
282
|
+
|
|
283
|
+
setAuditPushContext({
|
|
284
|
+
repo: activeRepo ?? undefined,
|
|
285
|
+
instance_id: activeInstanceId ?? undefined,
|
|
286
|
+
session_id: activeSessionId ?? undefined,
|
|
287
|
+
});
|
|
288
|
+
setViolationPushContext({
|
|
289
|
+
repo: activeRepo ?? undefined,
|
|
290
|
+
instance_id: activeInstanceId ?? undefined,
|
|
291
|
+
session_id: activeSessionId ?? undefined,
|
|
292
|
+
});
|
|
293
|
+
setReviewPushContext({
|
|
294
|
+
repo: activeRepo ?? undefined,
|
|
295
|
+
instance_id: activeInstanceId ?? undefined,
|
|
296
|
+
session_id: activeSessionId ?? undefined,
|
|
297
|
+
});
|
|
298
|
+
setWorkflowPushContext({
|
|
299
|
+
repo: activeRepo ?? undefined,
|
|
300
|
+
instance_id: activeInstanceId ?? undefined,
|
|
301
|
+
session_id: activeSessionId ?? undefined,
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// Initial policy sync
|
|
305
|
+
if (config.policy.enabled) {
|
|
306
|
+
if (config.policy.endpoint && config.policy.api_key) {
|
|
307
|
+
await syncFromCloud(config.policy.endpoint, config.policy.api_key, policyStore, {
|
|
308
|
+
instance_id: activeInstanceId ?? undefined,
|
|
309
|
+
session_id: activeSessionId ?? undefined,
|
|
310
|
+
});
|
|
311
|
+
process.stderr.write(`[cortex-enterprise] Policy sync: cloud\n`);
|
|
312
|
+
} else {
|
|
313
|
+
syncFromLocal(policyStore);
|
|
314
|
+
const orgCount = policyStore.loadOrgPolicies().length;
|
|
315
|
+
if (orgCount > 0) {
|
|
316
|
+
process.stderr.write(`[cortex-enterprise] Policy sync: ${orgCount} org rules loaded\n`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
registerEnterpriseTools(server, collector, auditWriter, config, contextDir, policyStore, version);
|
|
322
|
+
|
|
323
|
+
// v2.0.0: globalThis.__cortexContextToolHook bridge removed.
|
|
324
|
+
// Enterprise is now in-process with cortex-mcp; tool events flow via
|
|
325
|
+
// plugin.ts's onToolEvent hook directly through activation.ts.
|
|
326
|
+
|
|
327
|
+
process.stderr.write(`[cortex-enterprise] v${version}\n`);
|
|
328
|
+
|
|
329
|
+
// Log active features
|
|
330
|
+
const features: string[] = [];
|
|
331
|
+
if (config.telemetry.enabled) features.push("telemetry");
|
|
332
|
+
if (config.audit.enabled) features.push("audit");
|
|
333
|
+
if (config.policy.enabled) features.push("policy");
|
|
334
|
+
if (config.rbac.enabled) features.push(`rbac(${config.rbac.default_role})`);
|
|
335
|
+
if (features.length > 0) {
|
|
336
|
+
process.stderr.write(`[cortex-enterprise] Active: ${features.join(", ")}\n`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Telemetry push is owned by the daemon (single network writer).
|
|
340
|
+
// MCP only persists in-memory metrics to disk on a tick so the daemon
|
|
341
|
+
// can read and push them.
|
|
342
|
+
if (config.telemetry.enabled) {
|
|
343
|
+
const intervalMs = config.telemetry.interval_minutes * 60000;
|
|
344
|
+
const timer = setInterval(() => {
|
|
345
|
+
try {
|
|
346
|
+
collector.flush();
|
|
347
|
+
} catch (err) {
|
|
348
|
+
process.stderr.write(`[cortex-enterprise] Telemetry flush error: ${err}\n`);
|
|
349
|
+
}
|
|
350
|
+
}, intervalMs);
|
|
351
|
+
timer.unref();
|
|
352
|
+
timers.push(timer);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Schedule compliance queue flushes independently from telemetry so
|
|
356
|
+
// policy evidence is still delivered when metrics collection is off.
|
|
357
|
+
if (config.policy.enabled && config.policy.endpoint && config.policy.api_key) {
|
|
358
|
+
const intervalMs =
|
|
359
|
+
(config.telemetry.enabled
|
|
360
|
+
? config.telemetry.interval_minutes
|
|
361
|
+
: config.policy.sync_interval_minutes) * 60000;
|
|
362
|
+
const timer = setInterval(async () => {
|
|
363
|
+
await flushComplianceQueues(config, "periodic");
|
|
364
|
+
}, intervalMs);
|
|
365
|
+
timer.unref();
|
|
366
|
+
timers.push(timer);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Schedule policy sync
|
|
370
|
+
if (config.policy.enabled && config.policy.endpoint && config.policy.api_key) {
|
|
371
|
+
const intervalMs = config.policy.sync_interval_minutes * 60000;
|
|
372
|
+
const timer = setInterval(async () => {
|
|
373
|
+
try {
|
|
374
|
+
await syncFromCloud(config.policy.endpoint, config.policy.api_key, policyStore, {
|
|
375
|
+
instance_id: activeInstanceId ?? undefined,
|
|
376
|
+
session_id: activeSessionId ?? undefined,
|
|
377
|
+
});
|
|
378
|
+
} catch (err) {
|
|
379
|
+
process.stderr.write(`[cortex-enterprise] Policy sync error: ${err}\n`);
|
|
380
|
+
}
|
|
381
|
+
}, intervalMs);
|
|
382
|
+
timer.unref();
|
|
383
|
+
timers.push(timer);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
}
|
|
@@ -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
|
+
}
|