@danielblomma/cortex-mcp 1.7.2 → 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 (75) 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/src/cli/enterprise-setup.ts +124 -0
  5. package/scaffold/mcp/src/cli/govern.ts +987 -0
  6. package/scaffold/mcp/src/cli/run.ts +306 -0
  7. package/scaffold/mcp/src/cli/telemetry-test.ts +158 -0
  8. package/scaffold/mcp/src/cli/ungoverned-detector.ts +168 -0
  9. package/scaffold/mcp/src/core/audit/query.ts +81 -0
  10. package/scaffold/mcp/src/core/audit/writer.ts +68 -0
  11. package/scaffold/mcp/src/core/config.ts +329 -0
  12. package/scaffold/mcp/src/core/index.ts +34 -0
  13. package/scaffold/mcp/src/core/license.ts +202 -0
  14. package/scaffold/mcp/src/core/policy/enforce.ts +98 -0
  15. package/scaffold/mcp/src/core/policy/injection.ts +229 -0
  16. package/scaffold/mcp/src/core/policy/store.ts +197 -0
  17. package/scaffold/mcp/src/core/rbac/check.ts +40 -0
  18. package/scaffold/mcp/src/core/telemetry/collector.ts +234 -0
  19. package/scaffold/mcp/src/core/validators/builtins.ts +711 -0
  20. package/scaffold/mcp/src/core/validators/config.ts +47 -0
  21. package/scaffold/mcp/src/core/validators/engine.ts +199 -0
  22. package/scaffold/mcp/src/core/validators/evaluators/code_comments.ts +294 -0
  23. package/scaffold/mcp/src/core/validators/evaluators/regex.ts +144 -0
  24. package/scaffold/mcp/src/daemon/client.ts +155 -0
  25. package/scaffold/mcp/src/daemon/egress-proxy.ts +331 -0
  26. package/scaffold/mcp/src/daemon/heartbeat-pusher.ts +147 -0
  27. package/scaffold/mcp/src/daemon/heartbeat-tracker.ts +223 -0
  28. package/scaffold/mcp/src/daemon/host-events-pusher.ts +285 -0
  29. package/scaffold/mcp/src/daemon/main.ts +300 -0
  30. package/scaffold/mcp/src/daemon/paths.ts +41 -0
  31. package/scaffold/mcp/src/daemon/protocol.ts +101 -0
  32. package/scaffold/mcp/src/daemon/server.ts +227 -0
  33. package/scaffold/mcp/src/daemon/sync-checker.ts +213 -0
  34. package/scaffold/mcp/src/daemon/ungoverned-scanner.ts +149 -0
  35. package/scaffold/mcp/src/enterprise/audit/push.ts +84 -0
  36. package/scaffold/mcp/src/enterprise/index.ts +415 -0
  37. package/scaffold/mcp/src/enterprise/model/deploy.ts +33 -0
  38. package/scaffold/mcp/src/enterprise/policy/sync.ts +146 -0
  39. package/scaffold/mcp/src/enterprise/privacy/boundary.ts +212 -0
  40. package/scaffold/mcp/src/enterprise/reviews/push.ts +79 -0
  41. package/scaffold/mcp/src/enterprise/telemetry/sync.ts +72 -0
  42. package/scaffold/mcp/src/enterprise/tools/enterprise.ts +1031 -0
  43. package/scaffold/mcp/src/enterprise/tools/walk.ts +79 -0
  44. package/scaffold/mcp/src/enterprise/violations/push.ts +102 -0
  45. package/scaffold/mcp/src/enterprise/workflow/push.ts +60 -0
  46. package/scaffold/mcp/src/enterprise/workflow/state.ts +535 -0
  47. package/scaffold/mcp/src/hooks/pre-compact.ts +54 -0
  48. package/scaffold/mcp/src/hooks/pre-tool-use.ts +96 -0
  49. package/scaffold/mcp/src/hooks/session-end.ts +73 -0
  50. package/scaffold/mcp/src/hooks/session-start.ts +78 -0
  51. package/scaffold/mcp/src/hooks/shared.ts +134 -0
  52. package/scaffold/mcp/src/hooks/stop.ts +60 -0
  53. package/scaffold/mcp/src/hooks/user-prompt-submit.ts +64 -0
  54. package/scaffold/mcp/src/plugin.ts +150 -0
  55. package/scaffold/mcp/src/server.ts +218 -7
  56. package/scaffold/mcp/tests/copilot-shim.test.mjs +146 -0
  57. package/scaffold/mcp/tests/daemon-client.test.mjs +32 -0
  58. package/scaffold/mcp/tests/egress-proxy.test.mjs +239 -0
  59. package/scaffold/mcp/tests/enterprise-config.test.mjs +154 -0
  60. package/scaffold/mcp/tests/govern-install.test.mjs +320 -0
  61. package/scaffold/mcp/tests/govern-repair.test.mjs +157 -0
  62. package/scaffold/mcp/tests/govern-status.test.mjs +538 -0
  63. package/scaffold/mcp/tests/govern.test.mjs +74 -0
  64. package/scaffold/mcp/tests/heartbeat-pusher.test.mjs +154 -0
  65. package/scaffold/mcp/tests/heartbeat-tracker.test.mjs +237 -0
  66. package/scaffold/mcp/tests/host-events-pusher.test.mjs +347 -0
  67. package/scaffold/mcp/tests/policy-check.test.mjs +220 -0
  68. package/scaffold/mcp/tests/repo-name.test.mjs +134 -0
  69. package/scaffold/mcp/tests/run.test.mjs +109 -0
  70. package/scaffold/mcp/tests/sync-checker.test.mjs +188 -0
  71. package/scaffold/mcp/tests/ungoverned-detector.test.mjs +191 -0
  72. package/scaffold/mcp/tests/ungoverned-scanner.test.mjs +198 -0
  73. package/scaffold/scripts/bootstrap.sh +0 -11
  74. package/scaffold/scripts/doctor.sh +24 -4
  75. package/types.js +5 -0
@@ -0,0 +1,212 @@
1
+ import type { AuditEntry } from "../../core/audit/writer.js";
2
+ import type { TelemetryMetrics } from "../../core/telemetry/collector.js";
3
+
4
+ const REPO_METADATA = [
5
+ "repo",
6
+ "instance_id",
7
+ "session_id",
8
+ ] as const;
9
+
10
+ export const OUTBOUND_DATA_BOUNDARY = {
11
+ version: 3,
12
+ excludes: [
13
+ "source_code",
14
+ "raw_prompts",
15
+ "raw_queries",
16
+ "embeddings",
17
+ "graph_data",
18
+ "full_file_contents",
19
+ ],
20
+ telemetry: {
21
+ retention_days: 30,
22
+ payload_type: "counts_and_metadata_only",
23
+ allowed_fields: [
24
+ "period_start",
25
+ "period_end",
26
+ "total_tool_calls",
27
+ "successful_tool_calls",
28
+ "failed_tool_calls",
29
+ "total_duration_ms",
30
+ "session_starts",
31
+ "session_ends",
32
+ "session_duration_ms_total",
33
+ "searches",
34
+ "related_lookups",
35
+ "caller_lookups",
36
+ "trace_lookups",
37
+ "impact_analyses",
38
+ "rule_lookups",
39
+ "reloads",
40
+ "total_results_returned",
41
+ "estimated_tokens_saved",
42
+ "estimated_tokens_total",
43
+ "client_version",
44
+ "repo",
45
+ "instance_id",
46
+ "session_id",
47
+ "tool_metrics",
48
+ ],
49
+ },
50
+ audit: {
51
+ required_retention_days: 365,
52
+ diagnostic_retention_days: 30,
53
+ redaction: "string values are summarized to counts/lengths before outbound push",
54
+ metadata_fields: REPO_METADATA,
55
+ },
56
+ reviews: {
57
+ metadata_fields: REPO_METADATA,
58
+ },
59
+ violations: {
60
+ metadata_fields: REPO_METADATA,
61
+ },
62
+ workflow: {
63
+ metadata_fields: REPO_METADATA,
64
+ },
65
+ } as const;
66
+
67
+ type TelemetryPushContext = {
68
+ repo?: string;
69
+ session_id?: string;
70
+ };
71
+
72
+ const MAX_OBJECT_KEYS = 12;
73
+ const MAX_ARRAY_ITEMS = 12;
74
+ const SENSITIVE_KEY_RE =
75
+ /^(?:query|prompt|content|code|diff|patch|body|text|embedding|embeddings|graph|raw_query|raw_prompt|raw_code|raw_content)$/i;
76
+
77
+ function summarizeString(value: string) {
78
+ return {
79
+ type: "string",
80
+ length: value.length,
81
+ redacted: true,
82
+ };
83
+ }
84
+
85
+ function summarizeArray(value: unknown[], depth: number): Record<string, unknown> {
86
+ if (depth >= 2) {
87
+ return {
88
+ type: "array",
89
+ count: value.length,
90
+ redacted: true,
91
+ };
92
+ }
93
+
94
+ return {
95
+ type: "array",
96
+ count: value.length,
97
+ sample: value.slice(0, MAX_ARRAY_ITEMS).map((item) => summarizeValue(item, depth + 1)),
98
+ };
99
+ }
100
+
101
+ function summarizeObject(
102
+ value: Record<string, unknown>,
103
+ depth: number,
104
+ ): Record<string, unknown> {
105
+ const entries = Object.entries(value).slice(0, MAX_OBJECT_KEYS);
106
+ if (depth >= 2) {
107
+ return {
108
+ type: "object",
109
+ keys: entries.map(([key]) => key),
110
+ key_count: Object.keys(value).length,
111
+ redacted: true,
112
+ };
113
+ }
114
+
115
+ return Object.fromEntries(
116
+ entries.map(([key, item]) => [
117
+ key,
118
+ SENSITIVE_KEY_RE.test(key)
119
+ ? summarizeSensitiveValue(item)
120
+ : summarizeValue(item, depth + 1),
121
+ ]),
122
+ );
123
+ }
124
+
125
+ function summarizeSensitiveValue(value: unknown): Record<string, unknown> {
126
+ if (typeof value === "string") return summarizeString(value);
127
+ if (Array.isArray(value)) {
128
+ return {
129
+ type: "array",
130
+ count: value.length,
131
+ redacted: true,
132
+ };
133
+ }
134
+ if (value && typeof value === "object") {
135
+ return {
136
+ type: "object",
137
+ key_count: Object.keys(value as Record<string, unknown>).length,
138
+ redacted: true,
139
+ };
140
+ }
141
+ return {
142
+ type: typeof value,
143
+ redacted: true,
144
+ };
145
+ }
146
+
147
+ function summarizeValue(value: unknown, depth = 0): unknown {
148
+ if (value === null || typeof value === "number" || typeof value === "boolean") {
149
+ return value;
150
+ }
151
+ if (typeof value === "string") {
152
+ return summarizeString(value);
153
+ }
154
+ if (Array.isArray(value)) {
155
+ return summarizeArray(value, depth);
156
+ }
157
+ if (value && typeof value === "object") {
158
+ return summarizeObject(value as Record<string, unknown>, depth);
159
+ }
160
+ return {
161
+ type: typeof value,
162
+ redacted: true,
163
+ };
164
+ }
165
+
166
+ export function sanitizeOutboundRecord(
167
+ record: Record<string, unknown> | undefined,
168
+ ): Record<string, unknown> {
169
+ if (!record) return {};
170
+ return summarizeObject(record, 0);
171
+ }
172
+
173
+ export function sanitizeAuditEntryForPush(entry: AuditEntry): AuditEntry {
174
+ return {
175
+ ...entry,
176
+ input: sanitizeOutboundRecord(entry.input),
177
+ error: entry.error ? `[redacted:${entry.error.length}]` : undefined,
178
+ metadata: entry.metadata ? sanitizeOutboundRecord(entry.metadata) : undefined,
179
+ };
180
+ }
181
+
182
+ export function buildTelemetryPushPayload(
183
+ metrics: TelemetryMetrics,
184
+ context: TelemetryPushContext = {},
185
+ ) {
186
+ return {
187
+ period_start: metrics.period_start,
188
+ period_end: metrics.period_end,
189
+ total_tool_calls: metrics.total_tool_calls,
190
+ successful_tool_calls: metrics.successful_tool_calls,
191
+ failed_tool_calls: metrics.failed_tool_calls,
192
+ total_duration_ms: metrics.total_duration_ms,
193
+ session_starts: metrics.session_starts,
194
+ session_ends: metrics.session_ends,
195
+ session_duration_ms_total: metrics.session_duration_ms_total,
196
+ searches: metrics.searches,
197
+ related_lookups: metrics.related_lookups,
198
+ caller_lookups: metrics.caller_lookups,
199
+ trace_lookups: metrics.trace_lookups,
200
+ impact_analyses: metrics.impact_analyses,
201
+ rule_lookups: metrics.rule_lookups,
202
+ reloads: metrics.reloads,
203
+ total_results_returned: metrics.total_results_returned,
204
+ estimated_tokens_saved: metrics.estimated_tokens_saved,
205
+ estimated_tokens_total: metrics.estimated_tokens_total,
206
+ client_version: metrics.client_version,
207
+ repo: context.repo,
208
+ instance_id: metrics.instance_id,
209
+ session_id: context.session_id,
210
+ tool_metrics: metrics.tool_metrics,
211
+ };
212
+ }
@@ -0,0 +1,79 @@
1
+ export type ReviewPushItem = {
2
+ policy_id: string;
3
+ pass: boolean;
4
+ severity: "error" | "warning" | "info";
5
+ message: string;
6
+ detail?: string;
7
+ reviewed_at: string;
8
+ };
9
+
10
+ export type ReviewPushContext = {
11
+ repo?: string;
12
+ instance_id?: string;
13
+ session_id?: string;
14
+ };
15
+
16
+ export type ReviewPushResult = {
17
+ success: boolean;
18
+ count: number;
19
+ error?: string;
20
+ };
21
+
22
+ const pending: ReviewPushItem[] = [];
23
+ let activeContext: ReviewPushContext = {};
24
+
25
+ export function setReviewPushContext(context: ReviewPushContext): void {
26
+ activeContext = { ...context };
27
+ }
28
+
29
+ export function queueReviewResult(item: ReviewPushItem): void {
30
+ pending.push(item);
31
+ }
32
+
33
+ export function pendingCount(): number {
34
+ return pending.length;
35
+ }
36
+
37
+ export async function pushReviewResults(
38
+ baseUrl: string,
39
+ apiKey: string,
40
+ ): Promise<ReviewPushResult> {
41
+ if (pending.length === 0) {
42
+ return { success: true, count: 0 };
43
+ }
44
+
45
+ const reviewsUrl = `${baseUrl.replace(/\/$/, "")}/api/v1/reviews/push`;
46
+ const batch = pending.splice(0, 100);
47
+
48
+ try {
49
+ const response = await fetch(reviewsUrl, {
50
+ method: "POST",
51
+ headers: {
52
+ "Authorization": `Bearer ${apiKey}`,
53
+ "Content-Type": "application/json",
54
+ "Accept": "application/json",
55
+ },
56
+ body: JSON.stringify({
57
+ repo: activeContext.repo,
58
+ instance_id: activeContext.instance_id,
59
+ session_id: activeContext.session_id,
60
+ reviews: batch,
61
+ }),
62
+ signal: AbortSignal.timeout(10_000),
63
+ });
64
+
65
+ if (!response.ok) {
66
+ pending.unshift(...batch);
67
+ return { success: false, count: 0, error: `HTTP ${response.status}` };
68
+ }
69
+
70
+ return { success: true, count: batch.length };
71
+ } catch (err) {
72
+ pending.unshift(...batch);
73
+ return {
74
+ success: false,
75
+ count: 0,
76
+ error: err instanceof Error ? err.message : "unknown error",
77
+ };
78
+ }
79
+ }
@@ -0,0 +1,72 @@
1
+ import type { TelemetryMetrics } from "../../core/telemetry/collector.js";
2
+ import { buildTelemetryPushPayload } from "../privacy/boundary.js";
3
+
4
+ export type PushResult = {
5
+ success: boolean;
6
+ status?: number;
7
+ error?: string;
8
+ pushed_at?: string;
9
+ };
10
+
11
+ export type PushContext = {
12
+ repo?: string;
13
+ session_id?: string;
14
+ };
15
+
16
+ let lastPush: PushResult | null = null;
17
+
18
+ export function getLastPush(): PushResult | null {
19
+ return lastPush;
20
+ }
21
+
22
+ /**
23
+ * Push aggregated metrics to the Cortex Cloud API.
24
+ * Connected edition only. Returns success/failure.
25
+ *
26
+ * The actual cloud API is built in Phase 4 — this sends a POST
27
+ * with JSON body and expects a 2xx response.
28
+ */
29
+ export async function pushMetrics(
30
+ metrics: TelemetryMetrics,
31
+ endpoint: string,
32
+ apiKey: string,
33
+ context: PushContext = {},
34
+ ): Promise<PushResult> {
35
+ if (!endpoint || !apiKey) {
36
+ const result: PushResult = { success: false, error: "endpoint or api_key not configured" };
37
+ lastPush = result;
38
+ return result;
39
+ }
40
+
41
+ try {
42
+ const response = await fetch(endpoint, {
43
+ method: "POST",
44
+ headers: {
45
+ "Content-Type": "application/json",
46
+ "Authorization": `Bearer ${apiKey}`,
47
+ },
48
+ body: JSON.stringify(buildTelemetryPushPayload(metrics, context)),
49
+ signal: AbortSignal.timeout(10000),
50
+ });
51
+
52
+ const result: PushResult = {
53
+ success: response.ok,
54
+ status: response.status,
55
+ pushed_at: new Date().toISOString(),
56
+ };
57
+
58
+ if (!response.ok) {
59
+ result.error = `HTTP ${response.status}`;
60
+ }
61
+
62
+ lastPush = result;
63
+ return result;
64
+ } catch (err) {
65
+ const result: PushResult = {
66
+ success: false,
67
+ error: err instanceof Error ? err.message : "unknown error",
68
+ };
69
+ lastPush = result;
70
+ return result;
71
+ }
72
+ }