@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.
- package/bin/cortex.mjs +679 -32
- package/bin/style.mjs +349 -0
- package/package.json +4 -3
- package/scaffold/mcp/package-lock.json +834 -671
- package/scaffold/mcp/package.json +1 -1
- 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 +234 -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 +300 -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/embed.ts +1 -1
- package/scaffold/mcp/src/embeddings.ts +1 -1
- package/scaffold/mcp/src/enterprise/audit/push.ts +84 -0
- package/scaffold/mcp/src/enterprise/index.ts +415 -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 +212 -0
- package/scaffold/mcp/src/enterprise/reviews/push.ts +79 -0
- package/scaffold/mcp/src/enterprise/telemetry/sync.ts +72 -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/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/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
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { appendFile, mkdir } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
export type AuditEvidenceLevel = "required" | "diagnostic";
|
|
5
|
+
export type AuditEventType =
|
|
6
|
+
| "tool_call"
|
|
7
|
+
| "workflow_transition"
|
|
8
|
+
| "review_result"
|
|
9
|
+
| "policy_sync"
|
|
10
|
+
| "approval"
|
|
11
|
+
| "session"
|
|
12
|
+
| "security_scan";
|
|
13
|
+
|
|
14
|
+
export type AuditEntry = {
|
|
15
|
+
timestamp: string;
|
|
16
|
+
tool: string;
|
|
17
|
+
input: Record<string, unknown>;
|
|
18
|
+
result_count: number;
|
|
19
|
+
entities_returned: string[];
|
|
20
|
+
rules_applied: string[];
|
|
21
|
+
duration_ms: number;
|
|
22
|
+
status?: "success" | "error";
|
|
23
|
+
error?: string;
|
|
24
|
+
event_type?: AuditEventType;
|
|
25
|
+
evidence_level?: AuditEvidenceLevel;
|
|
26
|
+
resource_type?: string;
|
|
27
|
+
resource_id?: string;
|
|
28
|
+
repo?: string;
|
|
29
|
+
instance_id?: string;
|
|
30
|
+
session_id?: string;
|
|
31
|
+
metadata?: Record<string, unknown>;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type AuditWriterOptions = {
|
|
35
|
+
onEntry?: (entry: AuditEntry) => void;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export class AuditWriter {
|
|
39
|
+
private readonly auditDir: string;
|
|
40
|
+
private readonly onEntry: ((entry: AuditEntry) => void) | null;
|
|
41
|
+
private initialized = false;
|
|
42
|
+
|
|
43
|
+
constructor(contextDir: string, options: AuditWriterOptions = {}) {
|
|
44
|
+
this.auditDir = join(contextDir, "audit");
|
|
45
|
+
this.onEntry = options.onEntry ?? null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
log(entry: AuditEntry): void {
|
|
49
|
+
const date = entry.timestamp.slice(0, 10); // YYYY-MM-DD
|
|
50
|
+
const filePath = join(this.auditDir, `${date}.jsonl`);
|
|
51
|
+
const line = JSON.stringify(entry) + "\n";
|
|
52
|
+
|
|
53
|
+
this.onEntry?.(entry);
|
|
54
|
+
|
|
55
|
+
// Fire-and-forget async write — don't block the tool handler
|
|
56
|
+
this.writeAsync(filePath, line).catch(() => {
|
|
57
|
+
process.stderr.write("[cortex-enterprise] Failed to write audit entry\n");
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private async writeAsync(filePath: string, line: string): Promise<void> {
|
|
62
|
+
if (!this.initialized) {
|
|
63
|
+
await mkdir(this.auditDir, { recursive: true });
|
|
64
|
+
this.initialized = true;
|
|
65
|
+
}
|
|
66
|
+
await appendFile(filePath, line);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { Role, RBACConfig } from "./rbac/check.js";
|
|
4
|
+
import { parseValidatorsConfig, type ValidatorsConfig } from "./validators/config.js";
|
|
5
|
+
|
|
6
|
+
export type TelemetryConfig = {
|
|
7
|
+
enabled: boolean;
|
|
8
|
+
endpoint: string;
|
|
9
|
+
api_key: string;
|
|
10
|
+
interval_minutes: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type EnterpriseServiceConfig = {
|
|
14
|
+
endpoint: string;
|
|
15
|
+
api_key: string;
|
|
16
|
+
base_url: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type EnterpriseActivation =
|
|
20
|
+
| { active: true; reason: "active"; endpoint: string; api_key: string }
|
|
21
|
+
| {
|
|
22
|
+
active: false;
|
|
23
|
+
reason:
|
|
24
|
+
| "missing_api_key"
|
|
25
|
+
| "missing_endpoint"
|
|
26
|
+
| "invalid_api_key_format"
|
|
27
|
+
| "invalid_endpoint_format";
|
|
28
|
+
endpoint: string | null;
|
|
29
|
+
api_key: string | null;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type AuditConfig = {
|
|
33
|
+
enabled: boolean;
|
|
34
|
+
retention_days: number;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type PolicyConfig = {
|
|
38
|
+
enabled: boolean;
|
|
39
|
+
endpoint: string;
|
|
40
|
+
api_key: string;
|
|
41
|
+
sync_interval_minutes: number;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type ComplianceFramework =
|
|
45
|
+
| "iso27001"
|
|
46
|
+
| "iso42001"
|
|
47
|
+
| "soc2"
|
|
48
|
+
| "gdpr"
|
|
49
|
+
| "ai_act"
|
|
50
|
+
| "nis2";
|
|
51
|
+
|
|
52
|
+
export type ComplianceConfig = {
|
|
53
|
+
frameworks: ComplianceFramework[];
|
|
54
|
+
eu_addons: boolean;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export type GovernMode = "off" | "advisory" | "enforced";
|
|
58
|
+
export type GovernTier = "prevent" | "wrap" | "detect" | "off";
|
|
59
|
+
|
|
60
|
+
export type GovernConfig = {
|
|
61
|
+
mode: GovernMode;
|
|
62
|
+
sync_on_startup: boolean;
|
|
63
|
+
sync_interval_minutes: number;
|
|
64
|
+
tier_claude: GovernTier;
|
|
65
|
+
tier_codex: GovernTier;
|
|
66
|
+
tier_copilot: GovernTier;
|
|
67
|
+
detect_ungoverned: boolean;
|
|
68
|
+
govern_endpoint: string;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export type EnterpriseConfig = {
|
|
72
|
+
enterprise: EnterpriseServiceConfig;
|
|
73
|
+
telemetry: TelemetryConfig;
|
|
74
|
+
audit: AuditConfig;
|
|
75
|
+
policy: PolicyConfig;
|
|
76
|
+
rbac: RBACConfig;
|
|
77
|
+
validators: ValidatorsConfig;
|
|
78
|
+
compliance: ComplianceConfig;
|
|
79
|
+
govern: GovernConfig;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const DEFAULT_FRAMEWORKS: ComplianceFramework[] = ["iso27001", "iso42001", "soc2"];
|
|
83
|
+
const EU_ADDON_FRAMEWORKS: ComplianceFramework[] = ["gdpr", "ai_act", "nis2"];
|
|
84
|
+
const VALID_FRAMEWORKS = new Set<ComplianceFramework>([
|
|
85
|
+
...DEFAULT_FRAMEWORKS,
|
|
86
|
+
...EU_ADDON_FRAMEWORKS,
|
|
87
|
+
]);
|
|
88
|
+
const VALID_MODES = new Set<GovernMode>(["off", "advisory", "enforced"]);
|
|
89
|
+
const VALID_TIERS = new Set<GovernTier>(["prevent", "wrap", "detect", "off"]);
|
|
90
|
+
|
|
91
|
+
const DEFAULTS: EnterpriseConfig = {
|
|
92
|
+
enterprise: {
|
|
93
|
+
endpoint: "",
|
|
94
|
+
api_key: "",
|
|
95
|
+
base_url: "",
|
|
96
|
+
},
|
|
97
|
+
telemetry: {
|
|
98
|
+
enabled: false,
|
|
99
|
+
endpoint: "",
|
|
100
|
+
api_key: "",
|
|
101
|
+
interval_minutes: 10,
|
|
102
|
+
},
|
|
103
|
+
audit: {
|
|
104
|
+
enabled: true,
|
|
105
|
+
retention_days: 90,
|
|
106
|
+
},
|
|
107
|
+
policy: {
|
|
108
|
+
enabled: true,
|
|
109
|
+
endpoint: "",
|
|
110
|
+
api_key: "",
|
|
111
|
+
sync_interval_minutes: 240,
|
|
112
|
+
},
|
|
113
|
+
rbac: {
|
|
114
|
+
enabled: false,
|
|
115
|
+
default_role: "developer",
|
|
116
|
+
},
|
|
117
|
+
validators: {},
|
|
118
|
+
compliance: {
|
|
119
|
+
frameworks: [],
|
|
120
|
+
eu_addons: false,
|
|
121
|
+
},
|
|
122
|
+
govern: {
|
|
123
|
+
mode: "off",
|
|
124
|
+
sync_on_startup: true,
|
|
125
|
+
sync_interval_minutes: 60,
|
|
126
|
+
tier_claude: "prevent",
|
|
127
|
+
tier_codex: "prevent",
|
|
128
|
+
tier_copilot: "wrap",
|
|
129
|
+
detect_ungoverned: true,
|
|
130
|
+
govern_endpoint: "",
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const VALID_ROLES: Role[] = ["admin", "developer", "readonly"];
|
|
135
|
+
|
|
136
|
+
function isValidRole(value: string | undefined): value is Role {
|
|
137
|
+
return VALID_ROLES.includes(value as Role);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function stripInlineComment(value: string): string {
|
|
141
|
+
// Strip # comments that aren't inside quotes
|
|
142
|
+
const singleMatch = value.match(/^'([^']*)'(\s*#.*)?$/);
|
|
143
|
+
if (singleMatch) return singleMatch[1];
|
|
144
|
+
const doubleMatch = value.match(/^"([^"]*)"(\s*#.*)?$/);
|
|
145
|
+
if (doubleMatch) return doubleMatch[1];
|
|
146
|
+
// Unquoted: strip from first # preceded by whitespace
|
|
147
|
+
const commentIdx = value.search(/\s+#/);
|
|
148
|
+
return commentIdx >= 0 ? value.slice(0, commentIdx).trimEnd() : value;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function parseInlineList(value: string): string[] | null {
|
|
152
|
+
const trimmed = value.trim();
|
|
153
|
+
if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) return null;
|
|
154
|
+
const inner = trimmed.slice(1, -1).trim();
|
|
155
|
+
if (!inner) return [];
|
|
156
|
+
return inner
|
|
157
|
+
.split(",")
|
|
158
|
+
.map((part) => part.trim().replace(/^["']|["']$/g, ""))
|
|
159
|
+
.filter(Boolean);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function parseSimpleYaml(text: string): Record<string, string> {
|
|
163
|
+
const result: Record<string, string> = {};
|
|
164
|
+
let section = "";
|
|
165
|
+
for (const line of text.split("\n")) {
|
|
166
|
+
const trimmed = line.trimEnd();
|
|
167
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
168
|
+
|
|
169
|
+
const sectionMatch = trimmed.match(/^(\w+):\s*$/);
|
|
170
|
+
if (sectionMatch) {
|
|
171
|
+
section = sectionMatch[1];
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const kvMatch = trimmed.match(/^\s+(\w+):\s*(.+?)\s*$/);
|
|
176
|
+
if (kvMatch && section) {
|
|
177
|
+
result[`${section}.${kvMatch[1]}`] = stripInlineComment(kvMatch[2]);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const topMatch = trimmed.match(/^(\w+):\s*(.+?)\s*$/);
|
|
182
|
+
if (topMatch) {
|
|
183
|
+
result[topMatch[1]] = stripInlineComment(topMatch[2]);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return result;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function isLikelyApiKey(value: string): boolean {
|
|
190
|
+
return /^(?:ctx|ent)_[A-Za-z0-9._-]{8,}$/.test(value);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function isLikelyHttpUrl(value: string): boolean {
|
|
194
|
+
try {
|
|
195
|
+
const parsed = new URL(value);
|
|
196
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
197
|
+
} catch {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function resolveEnterpriseActivation(config: EnterpriseConfig): EnterpriseActivation {
|
|
203
|
+
const apiKey = config.enterprise.api_key.trim();
|
|
204
|
+
const endpoint = config.enterprise.endpoint.trim();
|
|
205
|
+
|
|
206
|
+
if (!apiKey) {
|
|
207
|
+
return { active: false, reason: "missing_api_key", api_key: null, endpoint: endpoint || null };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!endpoint) {
|
|
211
|
+
return { active: false, reason: "missing_endpoint", api_key: apiKey, endpoint: null };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (!isLikelyApiKey(apiKey)) {
|
|
215
|
+
return { active: false, reason: "invalid_api_key_format", api_key: apiKey, endpoint };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (!isLikelyHttpUrl(endpoint)) {
|
|
219
|
+
return { active: false, reason: "invalid_endpoint_format", api_key: apiKey, endpoint };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return { active: true, reason: "active", api_key: apiKey, endpoint };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function deriveEndpoint(baseUrl: string, suffix: string): string {
|
|
226
|
+
if (!baseUrl) return "";
|
|
227
|
+
return baseUrl.replace(/\/$/, "") + suffix;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function isValidTier(value: string | undefined): value is GovernTier {
|
|
231
|
+
return value !== undefined && VALID_TIERS.has(value as GovernTier);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function isValidMode(value: string | undefined): value is GovernMode {
|
|
235
|
+
return value !== undefined && VALID_MODES.has(value as GovernMode);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function resolveFrameworks(rawList: string | undefined, euAddons: boolean): ComplianceFramework[] {
|
|
239
|
+
const parsed = rawList ? parseInlineList(rawList) : null;
|
|
240
|
+
const base = parsed && parsed.length > 0
|
|
241
|
+
? parsed.filter((f): f is ComplianceFramework => VALID_FRAMEWORKS.has(f as ComplianceFramework))
|
|
242
|
+
: DEFAULT_FRAMEWORKS.slice();
|
|
243
|
+
if (!euAddons) return base;
|
|
244
|
+
const merged = new Set<ComplianceFramework>(base);
|
|
245
|
+
for (const f of EU_ADDON_FRAMEWORKS) merged.add(f);
|
|
246
|
+
return Array.from(merged);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function loadEnterpriseConfig(contextDir: string): EnterpriseConfig {
|
|
250
|
+
let raw: string;
|
|
251
|
+
try {
|
|
252
|
+
raw = readFileSync(join(contextDir, "enterprise.yml"), "utf8");
|
|
253
|
+
} catch {
|
|
254
|
+
try {
|
|
255
|
+
raw = readFileSync(join(contextDir, "enterprise.yaml"), "utf8");
|
|
256
|
+
} catch {
|
|
257
|
+
return DEFAULTS;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const fields = parseSimpleYaml(raw);
|
|
262
|
+
const enterpriseApiKey = fields["enterprise.api_key"] ?? DEFAULTS.enterprise.api_key;
|
|
263
|
+
const baseUrl = (fields["enterprise.base_url"] ?? fields["enterprise.endpoint"] ?? "").replace(/\/$/, "");
|
|
264
|
+
const enterpriseEndpoint = fields["enterprise.endpoint"] ?? baseUrl;
|
|
265
|
+
|
|
266
|
+
const telemetryEndpoint =
|
|
267
|
+
fields["enterprise.endpoint_telemetry"] ??
|
|
268
|
+
fields["telemetry.endpoint"] ??
|
|
269
|
+
deriveEndpoint(baseUrl, "/api/v1/telemetry/push");
|
|
270
|
+
const telemetryApiKey = fields["telemetry.api_key"] ?? enterpriseApiKey;
|
|
271
|
+
const policyEndpoint =
|
|
272
|
+
fields["enterprise.endpoint_policy"] ??
|
|
273
|
+
fields["policy.endpoint"] ??
|
|
274
|
+
deriveEndpoint(baseUrl, "/api/v1/policies/sync");
|
|
275
|
+
const policyApiKey = fields["policy.api_key"] ?? enterpriseApiKey;
|
|
276
|
+
const governEndpoint =
|
|
277
|
+
fields["enterprise.endpoint_govern"] ??
|
|
278
|
+
deriveEndpoint(baseUrl, "/api/v1/govern");
|
|
279
|
+
|
|
280
|
+
const euAddons = fields["compliance.eu_addons"] === "true";
|
|
281
|
+
const frameworks = resolveFrameworks(fields["compliance.frameworks"], euAddons);
|
|
282
|
+
|
|
283
|
+
const governMode = isValidMode(fields["govern.mode"]) ? fields["govern.mode"] : DEFAULTS.govern.mode;
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
enterprise: {
|
|
287
|
+
endpoint: enterpriseEndpoint,
|
|
288
|
+
api_key: enterpriseApiKey,
|
|
289
|
+
base_url: baseUrl,
|
|
290
|
+
},
|
|
291
|
+
telemetry: {
|
|
292
|
+
endpoint: telemetryEndpoint,
|
|
293
|
+
api_key: telemetryApiKey,
|
|
294
|
+
enabled: fields["telemetry.enabled"] !== undefined
|
|
295
|
+
? fields["telemetry.enabled"] === "true"
|
|
296
|
+
: !!(telemetryEndpoint && telemetryApiKey),
|
|
297
|
+
interval_minutes: parseInt(fields["telemetry.interval_minutes"] ?? "", 10) || DEFAULTS.telemetry.interval_minutes,
|
|
298
|
+
},
|
|
299
|
+
audit: {
|
|
300
|
+
enabled: fields["audit.enabled"] !== "false",
|
|
301
|
+
retention_days: parseInt(fields["audit.retention_days"] ?? "", 10) || DEFAULTS.audit.retention_days,
|
|
302
|
+
},
|
|
303
|
+
policy: {
|
|
304
|
+
enabled: fields["policy.enabled"] !== "false",
|
|
305
|
+
endpoint: policyEndpoint,
|
|
306
|
+
api_key: policyApiKey,
|
|
307
|
+
sync_interval_minutes: parseInt(fields["policy.sync_interval_minutes"] ?? "", 10) || DEFAULTS.policy.sync_interval_minutes,
|
|
308
|
+
},
|
|
309
|
+
rbac: {
|
|
310
|
+
enabled: fields["rbac.enabled"] === "true",
|
|
311
|
+
default_role: isValidRole(fields["rbac.default_role"]) ? fields["rbac.default_role"] : DEFAULTS.rbac.default_role,
|
|
312
|
+
},
|
|
313
|
+
validators: parseValidatorsConfig(fields),
|
|
314
|
+
compliance: {
|
|
315
|
+
frameworks,
|
|
316
|
+
eu_addons: euAddons,
|
|
317
|
+
},
|
|
318
|
+
govern: {
|
|
319
|
+
mode: governMode,
|
|
320
|
+
sync_on_startup: fields["govern.sync_on_startup"] !== "false",
|
|
321
|
+
sync_interval_minutes: parseInt(fields["govern.sync_interval_minutes"] ?? "", 10) || DEFAULTS.govern.sync_interval_minutes,
|
|
322
|
+
tier_claude: isValidTier(fields["govern.tier_claude"]) ? fields["govern.tier_claude"] : DEFAULTS.govern.tier_claude,
|
|
323
|
+
tier_codex: isValidTier(fields["govern.tier_codex"]) ? fields["govern.tier_codex"] : DEFAULTS.govern.tier_codex,
|
|
324
|
+
tier_copilot: isValidTier(fields["govern.tier_copilot"]) ? fields["govern.tier_copilot"] : DEFAULTS.govern.tier_copilot,
|
|
325
|
+
detect_ungoverned: fields["govern.detect_ungoverned"] !== "false",
|
|
326
|
+
govern_endpoint: governEndpoint,
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Config
|
|
2
|
+
export { loadEnterpriseConfig, resolveEnterpriseActivation } from "./config.js";
|
|
3
|
+
export type {
|
|
4
|
+
TelemetryConfig,
|
|
5
|
+
AuditConfig,
|
|
6
|
+
PolicyConfig,
|
|
7
|
+
EnterpriseConfig,
|
|
8
|
+
EnterpriseServiceConfig,
|
|
9
|
+
EnterpriseActivation
|
|
10
|
+
} from "./config.js";
|
|
11
|
+
|
|
12
|
+
// Telemetry
|
|
13
|
+
export { TelemetryCollector } from "./telemetry/collector.js";
|
|
14
|
+
export type { TelemetryMetrics } from "./telemetry/collector.js";
|
|
15
|
+
|
|
16
|
+
// Audit
|
|
17
|
+
export { AuditWriter } from "./audit/writer.js";
|
|
18
|
+
export type { AuditEntry } from "./audit/writer.js";
|
|
19
|
+
export { queryAuditLog } from "./audit/query.js";
|
|
20
|
+
export type { AuditQuery } from "./audit/query.js";
|
|
21
|
+
|
|
22
|
+
// RBAC
|
|
23
|
+
export { checkAccess, getAccessDeniedMessage } from "./rbac/check.js";
|
|
24
|
+
export type { Role, RBACConfig } from "./rbac/check.js";
|
|
25
|
+
|
|
26
|
+
// Policy
|
|
27
|
+
export { PolicyStore } from "./policy/store.js";
|
|
28
|
+
export type { OrgPolicy } from "./policy/store.js";
|
|
29
|
+
|
|
30
|
+
// Prompt injection
|
|
31
|
+
export { scanForInjection, sanitizeContent } from "./policy/injection.js";
|
|
32
|
+
export type { ScanResult, InjectionMatch, InjectionCategory } from "./policy/injection.js";
|
|
33
|
+
export { enforceInjectionPolicy, isInjectionDefenseActive, buildViolationPayload } from "./policy/enforce.js";
|
|
34
|
+
export type { EnforcementResult } from "./policy/enforce.js";
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
export type LicenseVerification =
|
|
5
|
+
| {
|
|
6
|
+
valid: true;
|
|
7
|
+
edition: string;
|
|
8
|
+
features: string[];
|
|
9
|
+
expires_at: string;
|
|
10
|
+
max_repos: number;
|
|
11
|
+
verified_at: string;
|
|
12
|
+
source: "remote" | "cache";
|
|
13
|
+
}
|
|
14
|
+
| {
|
|
15
|
+
valid: false;
|
|
16
|
+
reason: string;
|
|
17
|
+
verified_at: string;
|
|
18
|
+
source: "remote" | "cache" | "grace_expired";
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type CacheEntry = {
|
|
22
|
+
result: LicenseVerification;
|
|
23
|
+
// ISO timestamp for cache freshness window
|
|
24
|
+
cached_at: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const CACHE_FILE = "license_cache.json";
|
|
28
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24h fresh
|
|
29
|
+
const GRACE_PERIOD_MS = 7 * 24 * 60 * 60 * 1000; // 7d grace if endpoint unreachable
|
|
30
|
+
const REQUEST_TIMEOUT_MS = 5000;
|
|
31
|
+
|
|
32
|
+
function cachePath(contextDir: string): string {
|
|
33
|
+
return join(contextDir, "telemetry", CACHE_FILE);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function readCache(contextDir: string): CacheEntry | null {
|
|
37
|
+
const path = cachePath(contextDir);
|
|
38
|
+
if (!existsSync(path)) return null;
|
|
39
|
+
try {
|
|
40
|
+
const raw = readFileSync(path, "utf8");
|
|
41
|
+
return JSON.parse(raw) as CacheEntry;
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function writeCache(contextDir: string, entry: CacheEntry): void {
|
|
48
|
+
const path = cachePath(contextDir);
|
|
49
|
+
try {
|
|
50
|
+
mkdirSync(join(contextDir, "telemetry"), { recursive: true });
|
|
51
|
+
writeFileSync(path, JSON.stringify(entry, null, 2), "utf8");
|
|
52
|
+
} catch {
|
|
53
|
+
// Cache failures are non-fatal — license check just won't be cached.
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function deleteCache(contextDir: string): void {
|
|
58
|
+
const path = cachePath(contextDir);
|
|
59
|
+
if (!existsSync(path)) return;
|
|
60
|
+
try {
|
|
61
|
+
unlinkSync(path);
|
|
62
|
+
} catch {
|
|
63
|
+
// ignore — best-effort
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function ageMs(isoTimestamp: string): number {
|
|
68
|
+
return Date.now() - new Date(isoTimestamp).getTime();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function fetchLicense(
|
|
72
|
+
endpoint: string,
|
|
73
|
+
apiKey: string,
|
|
74
|
+
instanceId: string | undefined,
|
|
75
|
+
clientVersion: string | undefined,
|
|
76
|
+
): Promise<LicenseVerification | null> {
|
|
77
|
+
const url = `${endpoint.replace(/\/$/, "")}/api/v1/license/verify`;
|
|
78
|
+
const controller = new AbortController();
|
|
79
|
+
const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const res = await fetch(url, {
|
|
83
|
+
method: "POST",
|
|
84
|
+
headers: {
|
|
85
|
+
"content-type": "application/json",
|
|
86
|
+
authorization: `Bearer ${apiKey}`,
|
|
87
|
+
},
|
|
88
|
+
body: JSON.stringify({
|
|
89
|
+
instance_id: instanceId,
|
|
90
|
+
client_version: clientVersion,
|
|
91
|
+
}),
|
|
92
|
+
signal: controller.signal,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (!res.ok) {
|
|
96
|
+
// 401/429/5xx — treat as transient, surface null so caller can fall back.
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const json = (await res.json()) as Record<string, unknown>;
|
|
101
|
+
const verifiedAt = new Date().toISOString();
|
|
102
|
+
|
|
103
|
+
if (json.valid === true) {
|
|
104
|
+
return {
|
|
105
|
+
valid: true,
|
|
106
|
+
edition: String(json.edition ?? "unknown"),
|
|
107
|
+
features: Array.isArray(json.features) ? json.features.map(String) : [],
|
|
108
|
+
expires_at: String(json.expires_at ?? ""),
|
|
109
|
+
max_repos: typeof json.max_repos === "number" ? json.max_repos : 0,
|
|
110
|
+
verified_at: verifiedAt,
|
|
111
|
+
source: "remote",
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
valid: false,
|
|
117
|
+
reason: String(json.reason ?? "unknown"),
|
|
118
|
+
verified_at: verifiedAt,
|
|
119
|
+
source: "remote",
|
|
120
|
+
};
|
|
121
|
+
} catch {
|
|
122
|
+
// Network error, timeout, JSON parse error — treat as transient.
|
|
123
|
+
return null;
|
|
124
|
+
} finally {
|
|
125
|
+
clearTimeout(timeout);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Verify the license for the given api_key. Layered fallback:
|
|
131
|
+
* 1. If a positive cache (valid:true) is fresh (<24h) → use cache
|
|
132
|
+
* Negative cache entries are never trusted; if one is encountered
|
|
133
|
+
* it's deleted on the spot so a since-fixed remote can heal.
|
|
134
|
+
* 2. Otherwise try remote endpoint
|
|
135
|
+
* - On valid:true → write positive cache, return result
|
|
136
|
+
* - On valid:false (authoritative fail) → DELETE any positive
|
|
137
|
+
* cache (so a revoked/expired key doesn't keep masquerading as
|
|
138
|
+
* valid past its remote-side fail), return result, do NOT
|
|
139
|
+
* cache the negative.
|
|
140
|
+
* - On transient failure → fall back to positive cache if within
|
|
141
|
+
* grace period (7d). If only a negative cache exists, ignore it.
|
|
142
|
+
* 3. If no usable cache and endpoint unreachable → return invalid
|
|
143
|
+
* (grace_expired).
|
|
144
|
+
*
|
|
145
|
+
* The caller decides what to do based on the result. Typically:
|
|
146
|
+
* - valid:true → activate enterprise hooks
|
|
147
|
+
* - valid:false → community mode (no enterprise)
|
|
148
|
+
*/
|
|
149
|
+
export async function verifyLicense(
|
|
150
|
+
contextDir: string,
|
|
151
|
+
endpoint: string,
|
|
152
|
+
apiKey: string,
|
|
153
|
+
options: { instance_id?: string; client_version?: string } = {},
|
|
154
|
+
): Promise<LicenseVerification> {
|
|
155
|
+
let cached = readCache(contextDir);
|
|
156
|
+
|
|
157
|
+
// Defensive: a previous version of this code wrote negative results
|
|
158
|
+
// into the cache. Refuse to honour them and clean them up so a
|
|
159
|
+
// since-deployed fix on the remote can be observed.
|
|
160
|
+
if (cached && cached.result.valid === false) {
|
|
161
|
+
deleteCache(contextDir);
|
|
162
|
+
cached = null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Fresh positive cache: skip remote.
|
|
166
|
+
if (cached && cached.result.valid === true && ageMs(cached.cached_at) < CACHE_TTL_MS) {
|
|
167
|
+
return { ...cached.result, source: "cache" };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const remote = await fetchLicense(
|
|
171
|
+
endpoint,
|
|
172
|
+
apiKey,
|
|
173
|
+
options.instance_id,
|
|
174
|
+
options.client_version,
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
if (remote) {
|
|
178
|
+
if (remote.valid) {
|
|
179
|
+
writeCache(contextDir, {
|
|
180
|
+
result: remote,
|
|
181
|
+
cached_at: new Date().toISOString(),
|
|
182
|
+
});
|
|
183
|
+
} else {
|
|
184
|
+
// Authoritative fail from remote — drop any stale positive cache
|
|
185
|
+
// so we don't bounce back to "valid" on the next call.
|
|
186
|
+
deleteCache(contextDir);
|
|
187
|
+
}
|
|
188
|
+
return remote;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Remote unreachable. Fall back to positive cache if within grace.
|
|
192
|
+
if (cached && cached.result.valid === true && ageMs(cached.cached_at) < GRACE_PERIOD_MS) {
|
|
193
|
+
return { ...cached.result, source: "cache" };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
valid: false,
|
|
198
|
+
reason: "endpoint_unreachable_grace_expired",
|
|
199
|
+
verified_at: new Date().toISOString(),
|
|
200
|
+
source: "grace_expired",
|
|
201
|
+
};
|
|
202
|
+
}
|