@hachej/boring-governance 0.1.64
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/LICENSE +21 -0
- package/README.md +33 -0
- package/dist/front/index.d.ts +60 -0
- package/dist/front/index.js +137 -0
- package/dist/server/index.d.ts +176 -0
- package/dist/server/index.js +933 -0
- package/package.json +76 -0
|
@@ -0,0 +1,933 @@
|
|
|
1
|
+
// src/server/loadPolicy.ts
|
|
2
|
+
import { readFile, stat } from "fs/promises";
|
|
3
|
+
import { parse } from "yaml";
|
|
4
|
+
import { isCoreEmailVerificationEnabled } from "@hachej/boring-core/shared";
|
|
5
|
+
|
|
6
|
+
// src/server/validatePolicy.ts
|
|
7
|
+
var MICROS_PER_EUR = 1e6;
|
|
8
|
+
var MAX_REGEX_LENGTH = 512;
|
|
9
|
+
var MAX_CONTEXT_RULES_PER_USER = 128;
|
|
10
|
+
function fail(message) {
|
|
11
|
+
throw new Error(message);
|
|
12
|
+
}
|
|
13
|
+
function normalizePolicyEmail(value) {
|
|
14
|
+
return value.trim().toLowerCase();
|
|
15
|
+
}
|
|
16
|
+
function isRecord(value) {
|
|
17
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
18
|
+
}
|
|
19
|
+
function nonEmptyString(value, path2) {
|
|
20
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
21
|
+
fail(`${path2} must be a non-empty string`);
|
|
22
|
+
}
|
|
23
|
+
return value.trim();
|
|
24
|
+
}
|
|
25
|
+
function uuidString(value, path2) {
|
|
26
|
+
const id = nonEmptyString(value, path2);
|
|
27
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id)) {
|
|
28
|
+
fail(`${path2} must be a UUID workspace id`);
|
|
29
|
+
}
|
|
30
|
+
return id;
|
|
31
|
+
}
|
|
32
|
+
function finiteNumber(value, path2, opts) {
|
|
33
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
34
|
+
fail(`${path2} must be a finite number`);
|
|
35
|
+
}
|
|
36
|
+
if (opts.allowZero ? value < opts.min : value <= opts.min) {
|
|
37
|
+
fail(`${path2} must be ${opts.allowZero ? ">=" : ">"} ${opts.min}`);
|
|
38
|
+
}
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
var LITERAL_REGEX_ESCAPES = /* @__PURE__ */ new Set(["\\", "/", ".", "-", "^", "$", "+", "*", "?", "(", ")", "[", "]", "{", "}", "|"]);
|
|
42
|
+
function literalPathPrefix(pattern) {
|
|
43
|
+
let prefix = "";
|
|
44
|
+
let index = "^/".length;
|
|
45
|
+
let unsafeEscape = false;
|
|
46
|
+
while (index < pattern.length) {
|
|
47
|
+
const char = pattern[index];
|
|
48
|
+
if (!char) break;
|
|
49
|
+
if (char === "\\") {
|
|
50
|
+
const escaped = pattern[index + 1];
|
|
51
|
+
if (!escaped) break;
|
|
52
|
+
if (!LITERAL_REGEX_ESCAPES.has(escaped)) {
|
|
53
|
+
unsafeEscape = true;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
prefix += escaped;
|
|
57
|
+
index += 2;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if ("^$.+*?()[]{}|".includes(char)) break;
|
|
61
|
+
prefix += char;
|
|
62
|
+
index += 1;
|
|
63
|
+
}
|
|
64
|
+
return { prefix, consumed: index - "^/".length, unsafeEscape };
|
|
65
|
+
}
|
|
66
|
+
function hasTopLevelAlternation(pattern) {
|
|
67
|
+
let escaped = false;
|
|
68
|
+
let inCharacterClass = false;
|
|
69
|
+
let groupDepth = 0;
|
|
70
|
+
for (const char of pattern) {
|
|
71
|
+
if (escaped) {
|
|
72
|
+
escaped = false;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (char === "\\") {
|
|
76
|
+
escaped = true;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (inCharacterClass) {
|
|
80
|
+
if (char === "]") inCharacterClass = false;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (char === "[") {
|
|
84
|
+
inCharacterClass = true;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (char === "(") {
|
|
88
|
+
groupDepth += 1;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (char === ")") {
|
|
92
|
+
groupDepth = Math.max(0, groupDepth - 1);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (char === "|" && groupDepth === 0) return true;
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
function validateSegmentSafePattern(pattern, path2) {
|
|
100
|
+
const { prefix, consumed, unsafeEscape } = literalPathPrefix(pattern);
|
|
101
|
+
if (unsafeEscape) fail(`${path2} must use only literal regex escapes before the path-segment boundary`);
|
|
102
|
+
if (!prefix) {
|
|
103
|
+
const remainder2 = pattern.slice("^/".length);
|
|
104
|
+
if (remainder2.startsWith(".*") || remainder2.startsWith("$")) return;
|
|
105
|
+
fail(`${path2} must start with a literal path segment or ^/.*`);
|
|
106
|
+
}
|
|
107
|
+
const remainder = pattern.slice("^/".length + consumed);
|
|
108
|
+
if (prefix.endsWith("/")) {
|
|
109
|
+
if (/^[?*+{]/.test(remainder)) fail(`${path2} must not make the path-segment boundary slash optional`);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (remainder.startsWith("$")) return;
|
|
113
|
+
for (const guard of ["(?:/|$)", "(?:$|/)", "(/|$)", "($|/)"]) {
|
|
114
|
+
if (!remainder.startsWith(guard)) continue;
|
|
115
|
+
if (/^[?*+{]/.test(remainder.slice(guard.length))) {
|
|
116
|
+
fail(`${path2} must not make the path-segment boundary guard optional`);
|
|
117
|
+
}
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
fail(`${path2} must make literal path-prefix grants segment-safe with /, $, or (?:/|$)`);
|
|
121
|
+
}
|
|
122
|
+
function validateRegexPattern(value, path2) {
|
|
123
|
+
const pattern = nonEmptyString(value, path2);
|
|
124
|
+
if (pattern.length > MAX_REGEX_LENGTH) fail(`${path2} must be at most ${MAX_REGEX_LENGTH} characters`);
|
|
125
|
+
if (!pattern.startsWith("^/")) fail(`${path2} must start with ^/`);
|
|
126
|
+
if (hasTopLevelAlternation(pattern)) fail(`${path2} must not use top-level regex alternation; add separate allow rules instead`);
|
|
127
|
+
validateSegmentSafePattern(pattern, path2);
|
|
128
|
+
if (/\([^)]*[+*][^)]*\)[+*{]/.test(pattern)) {
|
|
129
|
+
fail(`${path2} contains an unsafe nested quantifier`);
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
new RegExp(pattern);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
fail(`${path2} must compile as a JavaScript RegExp: ${error instanceof Error ? error.message : String(error)}`);
|
|
135
|
+
}
|
|
136
|
+
return pattern;
|
|
137
|
+
}
|
|
138
|
+
function validateModels(value, userPath, defaultMonthlyModelBudgetEur) {
|
|
139
|
+
if (value === void 0) return [];
|
|
140
|
+
if (!Array.isArray(value)) fail(`${userPath}.models must be an array`);
|
|
141
|
+
return value.map((entry, index) => {
|
|
142
|
+
const path2 = `${userPath}.models[${index}]`;
|
|
143
|
+
if (!isRecord(entry)) fail(`${path2} must be an object`);
|
|
144
|
+
const provider = nonEmptyString(entry.provider, `${path2}.provider`);
|
|
145
|
+
const id = nonEmptyString(entry.id, `${path2}.id`);
|
|
146
|
+
const monthlyBudgetEur = entry.monthlyBudgetEur === void 0 ? defaultMonthlyModelBudgetEur : finiteNumber(entry.monthlyBudgetEur, `${path2}.monthlyBudgetEur`, {
|
|
147
|
+
min: 0,
|
|
148
|
+
allowZero: true
|
|
149
|
+
});
|
|
150
|
+
return {
|
|
151
|
+
provider,
|
|
152
|
+
id,
|
|
153
|
+
monthlyBudgetEur,
|
|
154
|
+
monthlyBudgetMicros: Math.round(monthlyBudgetEur * MICROS_PER_EUR)
|
|
155
|
+
};
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
function validateCompanyContext(value, userPath) {
|
|
159
|
+
if (value === void 0) return { allow: [] };
|
|
160
|
+
if (!isRecord(value)) fail(`${userPath}.companyContext must be an object`);
|
|
161
|
+
const allow = value.allow;
|
|
162
|
+
if (allow === void 0) return { allow: [] };
|
|
163
|
+
if (!Array.isArray(allow)) fail(`${userPath}.companyContext.allow must be an array`);
|
|
164
|
+
if (allow.length > MAX_CONTEXT_RULES_PER_USER) {
|
|
165
|
+
fail(`${userPath}.companyContext.allow must contain at most ${MAX_CONTEXT_RULES_PER_USER} rules`);
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
allow: allow.map((pattern, index) => validateRegexPattern(pattern, `${userPath}.companyContext.allow[${index}]`))
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function validateRole(value, path2) {
|
|
172
|
+
if (value !== "admin" && value !== "user") fail(`${path2} must be admin or user`);
|
|
173
|
+
return value;
|
|
174
|
+
}
|
|
175
|
+
function validateGovernancePolicy(input) {
|
|
176
|
+
if (!isRecord(input)) fail("policy must be an object");
|
|
177
|
+
const candidate = input;
|
|
178
|
+
if (!isRecord(candidate.tenant)) fail("tenant must be an object");
|
|
179
|
+
const tenant = candidate.tenant;
|
|
180
|
+
const tenantId = nonEmptyString(tenant.id, "tenant.id");
|
|
181
|
+
const companyContextWorkspaceId = tenant.companyContextWorkspaceId === void 0 || tenant.companyContextWorkspaceId === null ? null : uuidString(tenant.companyContextWorkspaceId, "tenant.companyContextWorkspaceId");
|
|
182
|
+
const defaultMonthlyModelBudgetEur = tenant.defaultMonthlyModelBudgetEur === void 0 ? 0 : finiteNumber(tenant.defaultMonthlyModelBudgetEur, "tenant.defaultMonthlyModelBudgetEur", { min: 0, allowZero: true });
|
|
183
|
+
const perRunHoldEur = tenant.perRunHoldEur === void 0 ? 1 : finiteNumber(tenant.perRunHoldEur, "tenant.perRunHoldEur", { min: 0, allowZero: false });
|
|
184
|
+
if (!Array.isArray(candidate.users)) fail("users must be an array");
|
|
185
|
+
const usersByEmail = /* @__PURE__ */ new Map();
|
|
186
|
+
const users = candidate.users.map((entry, index) => {
|
|
187
|
+
const path2 = `users[${index}]`;
|
|
188
|
+
if (!isRecord(entry)) fail(`${path2} must be an object`);
|
|
189
|
+
const email = normalizePolicyEmail(nonEmptyString(entry.email, `${path2}.email`));
|
|
190
|
+
if (!email.includes("@")) fail(`${path2}.email must be an email address`);
|
|
191
|
+
if (usersByEmail.has(email)) fail(`duplicate user email: ${email}`);
|
|
192
|
+
const user = {
|
|
193
|
+
email,
|
|
194
|
+
role: validateRole(entry.role, `${path2}.role`),
|
|
195
|
+
models: validateModels(entry.models, path2, defaultMonthlyModelBudgetEur),
|
|
196
|
+
companyContext: validateCompanyContext(entry.companyContext, path2)
|
|
197
|
+
};
|
|
198
|
+
usersByEmail.set(email, user);
|
|
199
|
+
return user;
|
|
200
|
+
});
|
|
201
|
+
const companyContextEnforced = users.some((user) => user.companyContext.allow.length > 0);
|
|
202
|
+
if (companyContextEnforced && !companyContextWorkspaceId) {
|
|
203
|
+
fail("tenant.companyContextWorkspaceId is required when company-context rules are configured");
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
tenant: {
|
|
207
|
+
id: tenantId,
|
|
208
|
+
companyContextWorkspaceId,
|
|
209
|
+
defaultMonthlyModelBudgetEur,
|
|
210
|
+
perRunHoldEur,
|
|
211
|
+
perRunHoldMicros: Math.round(perRunHoldEur * MICROS_PER_EUR)
|
|
212
|
+
},
|
|
213
|
+
users,
|
|
214
|
+
usersByEmail
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// src/server/loadPolicy.ts
|
|
219
|
+
var MAX_POLICY_BYTES = 256 * 1024;
|
|
220
|
+
var GOVERNANCE_POLICY_PATH_ENV = "BORING_GOVERNANCE_POLICY_PATH";
|
|
221
|
+
var GOVERNANCE_DEV_EMAIL_VERIFICATION_OVERRIDE_ENV = "BORING_GOVERNANCE_ALLOW_UNVERIFIED_EMAIL_DEV";
|
|
222
|
+
function invalidResult(path2, error) {
|
|
223
|
+
return {
|
|
224
|
+
enabled: true,
|
|
225
|
+
policy: null,
|
|
226
|
+
status: {
|
|
227
|
+
state: "invalid",
|
|
228
|
+
path: path2,
|
|
229
|
+
message: error instanceof Error ? error.message : String(error)
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function shouldAllowDevEmailVerificationOverride(env, nodeEnv) {
|
|
234
|
+
return nodeEnv !== "production" && env[GOVERNANCE_DEV_EMAIL_VERIFICATION_OVERRIDE_ENV] === "1";
|
|
235
|
+
}
|
|
236
|
+
async function loadGovernancePolicy({
|
|
237
|
+
env = process.env,
|
|
238
|
+
nodeEnv = process.env["NODE_ENV"] ?? "development",
|
|
239
|
+
config
|
|
240
|
+
} = {}) {
|
|
241
|
+
const configuredPath = env[GOVERNANCE_POLICY_PATH_ENV]?.trim();
|
|
242
|
+
if (!configuredPath) {
|
|
243
|
+
return {
|
|
244
|
+
enabled: false,
|
|
245
|
+
policy: null,
|
|
246
|
+
status: { state: "disabled", reason: "missing-env", path: null }
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
let fileStat;
|
|
250
|
+
try {
|
|
251
|
+
fileStat = await stat(configuredPath);
|
|
252
|
+
} catch (error) {
|
|
253
|
+
const code = error.code;
|
|
254
|
+
if (code === "ENOENT") {
|
|
255
|
+
return {
|
|
256
|
+
enabled: false,
|
|
257
|
+
policy: null,
|
|
258
|
+
status: { state: "disabled", reason: "missing-file", path: configuredPath }
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
throw error;
|
|
262
|
+
}
|
|
263
|
+
if (fileStat.size > MAX_POLICY_BYTES) {
|
|
264
|
+
const result = invalidResult(configuredPath, new Error(`policy file exceeds ${MAX_POLICY_BYTES} bytes`));
|
|
265
|
+
if (nodeEnv === "production") throw new Error(result.status.state === "invalid" ? result.status.message : "invalid governance policy");
|
|
266
|
+
return result;
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
if (config && !isCoreEmailVerificationEnabled(config) && !shouldAllowDevEmailVerificationOverride(env, nodeEnv)) {
|
|
270
|
+
throw new Error(
|
|
271
|
+
`governance requires email verification; configure mail or set ${GOVERNANCE_DEV_EMAIL_VERIFICATION_OVERRIDE_ENV}=1 outside production`
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
const raw = await readFile(configuredPath, "utf8");
|
|
275
|
+
const parsed = parse(raw);
|
|
276
|
+
const policy = validateGovernancePolicy(parsed);
|
|
277
|
+
return {
|
|
278
|
+
enabled: true,
|
|
279
|
+
policy,
|
|
280
|
+
status: {
|
|
281
|
+
state: "active",
|
|
282
|
+
path: configuredPath,
|
|
283
|
+
tenantId: policy.tenant.id,
|
|
284
|
+
userCount: policy.users.length
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
} catch (error) {
|
|
288
|
+
const result = invalidResult(configuredPath, error);
|
|
289
|
+
if (nodeEnv === "production") throw new Error(result.status.state === "invalid" ? result.status.message : "invalid governance policy");
|
|
290
|
+
return result;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// src/server/governanceService.ts
|
|
295
|
+
var GovernancePolicyError = class extends Error {
|
|
296
|
+
constructor(message, code) {
|
|
297
|
+
super(message);
|
|
298
|
+
this.code = code;
|
|
299
|
+
this.name = "GovernancePolicyError";
|
|
300
|
+
}
|
|
301
|
+
code;
|
|
302
|
+
};
|
|
303
|
+
var GovernanceService = class {
|
|
304
|
+
constructor(loaded) {
|
|
305
|
+
this.loaded = loaded;
|
|
306
|
+
}
|
|
307
|
+
loaded;
|
|
308
|
+
isEnabled() {
|
|
309
|
+
return this.loaded.enabled;
|
|
310
|
+
}
|
|
311
|
+
policyStatus() {
|
|
312
|
+
return this.loaded.status;
|
|
313
|
+
}
|
|
314
|
+
policy() {
|
|
315
|
+
return this.loaded.policy;
|
|
316
|
+
}
|
|
317
|
+
enabledPolicy() {
|
|
318
|
+
if (!this.loaded.enabled) return null;
|
|
319
|
+
if (!this.loaded.policy) return null;
|
|
320
|
+
return this.loaded.policy;
|
|
321
|
+
}
|
|
322
|
+
userPolicy(user) {
|
|
323
|
+
const policy = this.enabledPolicy();
|
|
324
|
+
if (!policy || !user?.email || user.emailVerified !== true) return null;
|
|
325
|
+
return policy.usersByEmail.get(normalizePolicyEmail(user.email)) ?? null;
|
|
326
|
+
}
|
|
327
|
+
roleForUser(user) {
|
|
328
|
+
return this.userPolicy(user)?.role ?? null;
|
|
329
|
+
}
|
|
330
|
+
isAdmin(user) {
|
|
331
|
+
return this.roleForUser(user) === "admin";
|
|
332
|
+
}
|
|
333
|
+
allowedModelsForUser(user, servedModels) {
|
|
334
|
+
const userPolicy = this.userPolicy(user);
|
|
335
|
+
if (!this.loaded.enabled) return [...servedModels];
|
|
336
|
+
if (!userPolicy) return [];
|
|
337
|
+
const allowed = new Set(userPolicy.models.map((model) => `${model.provider}\0${model.id}`));
|
|
338
|
+
return servedModels.filter((model) => allowed.has(`${model.provider}\0${model.id}`));
|
|
339
|
+
}
|
|
340
|
+
assertModelAllowed(user, model) {
|
|
341
|
+
if (!this.loaded.enabled) return;
|
|
342
|
+
const allowed = this.allowedModelsForUser(user, [model]);
|
|
343
|
+
if (allowed.length === 0) throw new GovernancePolicyError("model is not allowed by governance policy", "not_allowed");
|
|
344
|
+
}
|
|
345
|
+
monthlyBudgetMicros(user, model) {
|
|
346
|
+
if (!this.loaded.enabled) return null;
|
|
347
|
+
const userPolicy = this.userPolicy(user);
|
|
348
|
+
const grant = userPolicy?.models.find((entry) => entry.provider === model.provider && entry.id === model.id);
|
|
349
|
+
return grant?.monthlyBudgetMicros ?? null;
|
|
350
|
+
}
|
|
351
|
+
companyContextRules(user) {
|
|
352
|
+
if (!this.loaded.enabled) return [];
|
|
353
|
+
return this.userPolicy(user)?.companyContext.allow ?? [];
|
|
354
|
+
}
|
|
355
|
+
companyContextWorkspaceId() {
|
|
356
|
+
return this.enabledPolicy()?.tenant.companyContextWorkspaceId ?? null;
|
|
357
|
+
}
|
|
358
|
+
me(user) {
|
|
359
|
+
const role = this.roleForUser(user);
|
|
360
|
+
const admin = role === "admin";
|
|
361
|
+
const base = {
|
|
362
|
+
enabled: this.loaded.enabled,
|
|
363
|
+
role,
|
|
364
|
+
admin
|
|
365
|
+
};
|
|
366
|
+
if (!this.loaded.enabled) return base;
|
|
367
|
+
const policy = this.loaded.policy;
|
|
368
|
+
if (!policy) {
|
|
369
|
+
return { ...base, policyStatus: this.loaded.status };
|
|
370
|
+
}
|
|
371
|
+
if (!admin) return base;
|
|
372
|
+
return {
|
|
373
|
+
...base,
|
|
374
|
+
policyStatus: this.loaded.status,
|
|
375
|
+
tenant: {
|
|
376
|
+
id: policy.tenant.id,
|
|
377
|
+
companyContextWorkspaceId: policy.tenant.companyContextWorkspaceId,
|
|
378
|
+
defaultMonthlyModelBudgetEur: policy.tenant.defaultMonthlyModelBudgetEur,
|
|
379
|
+
perRunHoldEur: policy.tenant.perRunHoldEur
|
|
380
|
+
},
|
|
381
|
+
users: policy.users.map((entry) => ({
|
|
382
|
+
email: entry.email,
|
|
383
|
+
role: entry.role,
|
|
384
|
+
modelCount: entry.models.length,
|
|
385
|
+
contextRuleCount: entry.companyContext.allow.length
|
|
386
|
+
})),
|
|
387
|
+
models: policy.users.flatMap((entry) => entry.models.map((model) => ({ ...model, email: entry.email }))),
|
|
388
|
+
companyContextRules: policy.users.flatMap((entry) => entry.companyContext.allow.map((pattern) => ({ email: entry.email, pattern })))
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
function createGovernanceService(result) {
|
|
393
|
+
return new GovernanceService(result);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// src/server/routes.ts
|
|
397
|
+
function governanceRoutes(service) {
|
|
398
|
+
return async (app) => {
|
|
399
|
+
app.get("/api/v1/governance/me", async (request) => {
|
|
400
|
+
return service.me(request.user ?? null);
|
|
401
|
+
});
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// src/server/companyContextBootstrap.ts
|
|
406
|
+
import { ConfigValidationError } from "@hachej/boring-core/shared";
|
|
407
|
+
var COMPANY_CONTEXT_WORKSPACE_NAME = "Company Context";
|
|
408
|
+
var COMPANY_CONTEXT_WORKSPACE_MANAGED_BY = "company-context";
|
|
409
|
+
var provisionInFlight = /* @__PURE__ */ new Map();
|
|
410
|
+
function adminPolicies(service) {
|
|
411
|
+
const policy = service.policy();
|
|
412
|
+
if (!policy) return [];
|
|
413
|
+
return policy.users.filter((user) => user.role === "admin");
|
|
414
|
+
}
|
|
415
|
+
function assertCompanyContextWorkspace(workspace, workspaceId) {
|
|
416
|
+
if (workspace.managedBy === COMPANY_CONTEXT_WORKSPACE_MANAGED_BY) return;
|
|
417
|
+
throw new ConfigValidationError([
|
|
418
|
+
{
|
|
419
|
+
path: ["tenant", "companyContextWorkspaceId"],
|
|
420
|
+
message: `workspace ${workspaceId} already exists but is not marked as a managed Company Context workspace`
|
|
421
|
+
}
|
|
422
|
+
]);
|
|
423
|
+
}
|
|
424
|
+
async function provisionCompanyContextWorkspace(app, workspaceId, ownerId) {
|
|
425
|
+
if (!app.provisioner) return;
|
|
426
|
+
const runtime = await app.workspaceStore.getWorkspaceRuntime(workspaceId);
|
|
427
|
+
if (runtime?.state === "ready" && runtime.volumePath) return;
|
|
428
|
+
const existingProvision = provisionInFlight.get(workspaceId);
|
|
429
|
+
if (existingProvision) {
|
|
430
|
+
await existingProvision;
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
const provisionPromise = (async () => {
|
|
434
|
+
await app.workspaceStore.putWorkspaceRuntime(workspaceId, { state: "pending" });
|
|
435
|
+
try {
|
|
436
|
+
const result = await app.provisioner.provision({
|
|
437
|
+
workspaceId,
|
|
438
|
+
workspaceName: COMPANY_CONTEXT_WORKSPACE_NAME,
|
|
439
|
+
ownerId,
|
|
440
|
+
appId: app.config.appId
|
|
441
|
+
});
|
|
442
|
+
await app.workspaceStore.putWorkspaceRuntime(workspaceId, {
|
|
443
|
+
state: "ready",
|
|
444
|
+
volumePath: result.volumePath
|
|
445
|
+
});
|
|
446
|
+
} catch (error) {
|
|
447
|
+
await app.workspaceStore.putWorkspaceRuntime(workspaceId, {
|
|
448
|
+
state: "error",
|
|
449
|
+
lastError: error instanceof Error ? error.message : String(error),
|
|
450
|
+
lastErrorOp: "provision"
|
|
451
|
+
});
|
|
452
|
+
throw error;
|
|
453
|
+
}
|
|
454
|
+
})();
|
|
455
|
+
provisionInFlight.set(workspaceId, provisionPromise);
|
|
456
|
+
try {
|
|
457
|
+
await provisionPromise;
|
|
458
|
+
} finally {
|
|
459
|
+
if (provisionInFlight.get(workspaceId) === provisionPromise) provisionInFlight.delete(workspaceId);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
async function reconcileCompanyContextWorkspace(app, service) {
|
|
463
|
+
if (!service.isEnabled()) return;
|
|
464
|
+
const governanceApp = app;
|
|
465
|
+
const workspaceId = service.companyContextWorkspaceId();
|
|
466
|
+
if (!workspaceId) return;
|
|
467
|
+
const adminUsers = [];
|
|
468
|
+
for (const policyUser of adminPolicies(service)) {
|
|
469
|
+
const user = await governanceApp.userStore.getByEmail(policyUser.email);
|
|
470
|
+
if (user?.emailVerified) adminUsers.push(user);
|
|
471
|
+
}
|
|
472
|
+
let workspace = await governanceApp.workspaceStore.get(workspaceId);
|
|
473
|
+
if (workspace) {
|
|
474
|
+
assertCompanyContextWorkspace(workspace, workspaceId);
|
|
475
|
+
} else {
|
|
476
|
+
const deletedWorkspace = await governanceApp.workspaceStore.getIncludingDeleted(workspaceId);
|
|
477
|
+
if (deletedWorkspace) {
|
|
478
|
+
assertCompanyContextWorkspace(deletedWorkspace, workspaceId);
|
|
479
|
+
workspace = await governanceApp.workspaceStore.restore(workspaceId);
|
|
480
|
+
if (!workspace) throw new Error(`Workspace ${workspaceId} could not be restored`);
|
|
481
|
+
app.log.info({ workspaceId }, "governance.company_context.restored");
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
const owner = adminUsers[0];
|
|
485
|
+
if (!workspace) {
|
|
486
|
+
if (!owner) {
|
|
487
|
+
app.log.warn({ workspaceId }, "governance.company_context.no_existing_verified_admins");
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
workspace = await governanceApp.workspaceStore.create(owner.id, COMPANY_CONTEXT_WORKSPACE_NAME, app.config.appId, {
|
|
491
|
+
id: workspaceId,
|
|
492
|
+
isDefault: false,
|
|
493
|
+
managedBy: COMPANY_CONTEXT_WORKSPACE_MANAGED_BY
|
|
494
|
+
});
|
|
495
|
+
assertCompanyContextWorkspace(workspace, workspaceId);
|
|
496
|
+
app.log.info({ workspaceId, ownerId: owner.id }, "governance.company_context.created");
|
|
497
|
+
}
|
|
498
|
+
const adminIds = new Set(adminUsers.map((admin) => admin.id));
|
|
499
|
+
for (const admin of adminUsers) {
|
|
500
|
+
await governanceApp.workspaceStore.upsertMember(workspaceId, admin.id, "owner");
|
|
501
|
+
}
|
|
502
|
+
const existingMembers = await governanceApp.workspaceStore.listMembers(workspaceId);
|
|
503
|
+
for (const member of existingMembers) {
|
|
504
|
+
if (adminIds.has(member.userId)) continue;
|
|
505
|
+
const result = adminIds.size === 0 ? await governanceApp.workspaceStore.removeMember(workspaceId, member.userId, { allowLastOwner: true }) : await governanceApp.workspaceStore.removeMember(workspaceId, member.userId);
|
|
506
|
+
if (!result.removed) {
|
|
507
|
+
app.log.warn({ workspaceId, userId: member.userId, code: result.code }, "governance.company_context.remove_stale_member_failed");
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
if (!owner) {
|
|
511
|
+
app.log.warn({ workspaceId }, "governance.company_context.no_existing_verified_admins");
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
await provisionCompanyContextWorkspace(app, workspaceId, owner.id);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// src/server/metering.ts
|
|
518
|
+
import { ErrorCode } from "@hachej/boring-agent/shared";
|
|
519
|
+
import * as coreServer from "@hachej/boring-core/server";
|
|
520
|
+
var PostgresModelBudgetStore2 = coreServer.PostgresModelBudgetStore;
|
|
521
|
+
var ModelBudgetExceededError2 = coreServer.ModelBudgetExceededError;
|
|
522
|
+
var DEFAULT_HOLD_TTL_SECONDS = 60 * 60;
|
|
523
|
+
function authRequiredError() {
|
|
524
|
+
return Object.assign(new Error("authentication required"), { statusCode: 401, code: ErrorCode.enum.UNAUTHORIZED });
|
|
525
|
+
}
|
|
526
|
+
function modelRequiredError() {
|
|
527
|
+
return Object.assign(new Error("model is required by governance policy"), { statusCode: 400, code: ErrorCode.enum.TOOL_INVALID_INPUT });
|
|
528
|
+
}
|
|
529
|
+
function policyDeniedError() {
|
|
530
|
+
return Object.assign(new Error("model is not allowed by governance policy"), { statusCode: 403, code: ErrorCode.enum.TOOL_INVALID_INPUT });
|
|
531
|
+
}
|
|
532
|
+
function budgetError(error) {
|
|
533
|
+
if (typeof ModelBudgetExceededError2 === "function" && error instanceof ModelBudgetExceededError2) return error;
|
|
534
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
535
|
+
}
|
|
536
|
+
function runKey(input) {
|
|
537
|
+
return input.userId ? `${input.userId}\0${input.runId}` : null;
|
|
538
|
+
}
|
|
539
|
+
function createGovernanceMeteringSink(options) {
|
|
540
|
+
let store;
|
|
541
|
+
const getStore = () => store ??= new PostgresModelBudgetStore2(options.getDb());
|
|
542
|
+
const reservationsByRun = /* @__PURE__ */ new Map();
|
|
543
|
+
const holdTtlSeconds = options.holdTtlSeconds ?? DEFAULT_HOLD_TTL_SECONDS;
|
|
544
|
+
async function reserveGovernance(input) {
|
|
545
|
+
if (!options.service.isEnabled()) return void 0;
|
|
546
|
+
const identity = input;
|
|
547
|
+
if (!input.userId || !identity.userEmail) throw authRequiredError();
|
|
548
|
+
const user = { id: input.userId, email: identity.userEmail, emailVerified: identity.userEmailVerified === true };
|
|
549
|
+
if (!input.model?.provider || !input.model.id) throw modelRequiredError();
|
|
550
|
+
try {
|
|
551
|
+
options.service.assertModelAllowed(user, { provider: input.model.provider, id: input.model.id });
|
|
552
|
+
} catch {
|
|
553
|
+
throw policyDeniedError();
|
|
554
|
+
}
|
|
555
|
+
const budgetMicros = options.service.monthlyBudgetMicros(user, { provider: input.model.provider, id: input.model.id });
|
|
556
|
+
if (budgetMicros === null || budgetMicros <= 0) throw new ModelBudgetExceededError2(0, 0, budgetMicros ?? 0, 0);
|
|
557
|
+
const policy = options.service.policy();
|
|
558
|
+
const holdMicros = policy?.tenant.perRunHoldMicros ?? 1e6;
|
|
559
|
+
const result = await getStore().reserve({
|
|
560
|
+
userId: input.userId,
|
|
561
|
+
workspaceId: input.workspaceId,
|
|
562
|
+
sessionId: input.sessionId,
|
|
563
|
+
runId: input.runId,
|
|
564
|
+
provider: input.model.provider,
|
|
565
|
+
model: input.model.id,
|
|
566
|
+
budgetMicros,
|
|
567
|
+
holdMicros,
|
|
568
|
+
ttlSeconds: holdTtlSeconds
|
|
569
|
+
}).catch((error) => {
|
|
570
|
+
throw budgetError(error);
|
|
571
|
+
});
|
|
572
|
+
const key = runKey(input);
|
|
573
|
+
if (key) reservationsByRun.set(key, result.reservationId);
|
|
574
|
+
return result.reservationId;
|
|
575
|
+
}
|
|
576
|
+
async function finishGovernance(input, action) {
|
|
577
|
+
if (!options.service.isEnabled()) return;
|
|
578
|
+
const key = runKey(input);
|
|
579
|
+
const reservationId = key ? reservationsByRun.get(key) : void 0;
|
|
580
|
+
if (!reservationId && (!input.userId || !input.runId)) return;
|
|
581
|
+
if (action === "settle") await getStore().settle({ reservationId, runId: input.runId, userId: input.userId });
|
|
582
|
+
else await getStore().release({ reservationId, runId: input.runId, userId: input.userId });
|
|
583
|
+
if (key) reservationsByRun.delete(key);
|
|
584
|
+
}
|
|
585
|
+
function releaseConsumesBudget(input) {
|
|
586
|
+
return input.reason === "usage-write-failed" || input.reason === "fallback-hold-charge";
|
|
587
|
+
}
|
|
588
|
+
return {
|
|
589
|
+
isEnabled: () => options.service.isEnabled() || options.delegate.isEnabled?.() !== false,
|
|
590
|
+
async reserveRun(input) {
|
|
591
|
+
const governanceReservationId = await reserveGovernance(input);
|
|
592
|
+
try {
|
|
593
|
+
return await options.delegate.reserveRun(input);
|
|
594
|
+
} catch (error) {
|
|
595
|
+
if (governanceReservationId) await getStore().release({ reservationId: governanceReservationId }).catch(() => {
|
|
596
|
+
});
|
|
597
|
+
const key = runKey(input);
|
|
598
|
+
if (key) reservationsByRun.delete(key);
|
|
599
|
+
throw error;
|
|
600
|
+
}
|
|
601
|
+
},
|
|
602
|
+
recordUsage(input) {
|
|
603
|
+
const key = runKey(input);
|
|
604
|
+
const reservationId = key ? reservationsByRun.get(key) : void 0;
|
|
605
|
+
return options.delegate.recordUsage(reservationId ? { ...input, metadata: { ...input.metadata ?? {}, modelBudgetReservationId: reservationId } } : input);
|
|
606
|
+
},
|
|
607
|
+
async settleRun(input) {
|
|
608
|
+
try {
|
|
609
|
+
await options.delegate.settleRun(input);
|
|
610
|
+
} finally {
|
|
611
|
+
await finishGovernance(input, "release");
|
|
612
|
+
}
|
|
613
|
+
},
|
|
614
|
+
async releaseRun(input) {
|
|
615
|
+
if (releaseConsumesBudget(input)) {
|
|
616
|
+
await finishGovernance(input, "settle");
|
|
617
|
+
await options.delegate.releaseRun(input);
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
try {
|
|
621
|
+
await options.delegate.releaseRun(input);
|
|
622
|
+
} finally {
|
|
623
|
+
await finishGovernance(input, "release");
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// src/server/filesystemBindings.ts
|
|
630
|
+
import { constants } from "fs";
|
|
631
|
+
import { access, chmod, copyFile, lstat, mkdir, mkdtemp, readdir, realpath, rm, stat as stat2 } from "fs/promises";
|
|
632
|
+
import os from "os";
|
|
633
|
+
import path from "path";
|
|
634
|
+
import { createLogger } from "@hachej/boring-agent/server";
|
|
635
|
+
import { ErrorCode as ErrorCode2 } from "@hachej/boring-agent/shared";
|
|
636
|
+
import {
|
|
637
|
+
COMPANY_CONTEXT_FILESYSTEM_ID,
|
|
638
|
+
ScopedFilesystemRuntimeBindingManager,
|
|
639
|
+
createReadonlyProjectionOperations
|
|
640
|
+
} from "@hachej/boring-bash/server";
|
|
641
|
+
var COMPANY_CONTEXT_MOUNT_PATH = "/company_context";
|
|
642
|
+
var AGENT_MODE_ENV = "BORING_AGENT_MODE";
|
|
643
|
+
var AGENT_WORKSPACE_ROOT_ENV = "BORING_AGENT_WORKSPACE_ROOT";
|
|
644
|
+
var GOVERNANCE_COMPANY_CONTEXT_ROOT_ENV = "BORING_GOVERNANCE_COMPANY_CONTEXT_ROOT";
|
|
645
|
+
var companyContextBindingCanonicalLogger = createLogger("boring-governance/company-context");
|
|
646
|
+
var companyContextBindingFallbackLogger = {
|
|
647
|
+
error(fields, message) {
|
|
648
|
+
companyContextBindingCanonicalLogger.error(message, fields);
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
function normalizeCompanyPath(value) {
|
|
652
|
+
const normalized = value.replace(/\\/g, "/");
|
|
653
|
+
if (normalized.includes("\0")) throw new Error("invalid company path");
|
|
654
|
+
const withRoot = normalized.startsWith("/") ? normalized : `/${normalized}`;
|
|
655
|
+
const parts = withRoot.split("/").filter(Boolean);
|
|
656
|
+
if (parts.some((part) => part === "." || part === "..")) throw new Error("company path traversal is not allowed");
|
|
657
|
+
return `/${parts.join("/")}`;
|
|
658
|
+
}
|
|
659
|
+
async function assertInsideRoot(root, candidate) {
|
|
660
|
+
const realRoot = await realpath(root);
|
|
661
|
+
const existing = await realpath(candidate);
|
|
662
|
+
const rel = path.relative(realRoot, existing);
|
|
663
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) throw new Error("path escapes company context root");
|
|
664
|
+
}
|
|
665
|
+
async function walkFiles(root, current = root) {
|
|
666
|
+
const entries = await readdir(current, { withFileTypes: true });
|
|
667
|
+
const out = [];
|
|
668
|
+
for (const entry of entries) {
|
|
669
|
+
const absolutePath = path.join(current, entry.name);
|
|
670
|
+
const entryStat = await lstat(absolutePath);
|
|
671
|
+
if (entryStat.isSymbolicLink()) continue;
|
|
672
|
+
if (entry.isDirectory()) out.push(...await walkFiles(root, absolutePath));
|
|
673
|
+
else if (entry.isFile()) out.push(absolutePath);
|
|
674
|
+
}
|
|
675
|
+
return out;
|
|
676
|
+
}
|
|
677
|
+
async function makeProjectionReadonly(root, current = root) {
|
|
678
|
+
const entries = await readdir(current, { withFileTypes: true });
|
|
679
|
+
for (const entry of entries) {
|
|
680
|
+
const absolutePath = path.join(current, entry.name);
|
|
681
|
+
if (entry.isDirectory()) await makeProjectionReadonly(root, absolutePath);
|
|
682
|
+
else if (entry.isFile()) await chmod(absolutePath, 256);
|
|
683
|
+
}
|
|
684
|
+
await chmod(current, 320);
|
|
685
|
+
}
|
|
686
|
+
async function makeProjectionRemovable(current) {
|
|
687
|
+
await chmod(current, 448).catch(() => {
|
|
688
|
+
});
|
|
689
|
+
const entries = await readdir(current, { withFileTypes: true }).catch(() => []);
|
|
690
|
+
for (const entry of entries) {
|
|
691
|
+
const absolutePath = path.join(current, entry.name);
|
|
692
|
+
if (entry.isDirectory()) await makeProjectionRemovable(absolutePath);
|
|
693
|
+
else await chmod(absolutePath, 384).catch(() => {
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
function compileRules(rules) {
|
|
698
|
+
return rules.map((rule) => new RegExp(rule));
|
|
699
|
+
}
|
|
700
|
+
function userFromContext(ctx) {
|
|
701
|
+
const requestUser = ctx.request?.user;
|
|
702
|
+
if (requestUser?.email) {
|
|
703
|
+
return { id: requestUser.id, email: requestUser.email, emailVerified: requestUser.emailVerified === true };
|
|
704
|
+
}
|
|
705
|
+
if (!ctx.userEmail) return null;
|
|
706
|
+
return { id: ctx.userId, email: ctx.userEmail, emailVerified: ctx.userEmailVerified === true };
|
|
707
|
+
}
|
|
708
|
+
var RegexFilteredCompanyContextProvider = class {
|
|
709
|
+
constructor(sourceRoot, allowedRules, projectionRootParent) {
|
|
710
|
+
this.sourceRoot = sourceRoot;
|
|
711
|
+
this.allowedRules = allowedRules;
|
|
712
|
+
this.projectionRootParent = projectionRootParent;
|
|
713
|
+
}
|
|
714
|
+
sourceRoot;
|
|
715
|
+
allowedRules;
|
|
716
|
+
projectionRootParent;
|
|
717
|
+
async disposeBinding(prepared) {
|
|
718
|
+
const handle = prepared.handle;
|
|
719
|
+
if (handle.lifecycle) handle.lifecycle.active = false;
|
|
720
|
+
if (handle.projectionRoot) {
|
|
721
|
+
await makeProjectionRemovable(handle.projectionRoot);
|
|
722
|
+
await rm(handle.projectionRoot, { recursive: true, force: true });
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
async prepareBinding(_ctx, binding) {
|
|
726
|
+
if (binding.filesystem !== COMPANY_CONTEXT_FILESYSTEM_ID) throw new Error(`unsupported filesystem binding ${binding.filesystem}`);
|
|
727
|
+
if (binding.access !== "readonly" || binding.projection !== "policy-filtered") {
|
|
728
|
+
throw new Error("company_context policy mount only supports readonly policy-filtered bindings");
|
|
729
|
+
}
|
|
730
|
+
await access(this.sourceRoot, constants.R_OK);
|
|
731
|
+
const projectionRoot = await mkdtemp(path.join(this.projectionRootParent, "boring-company-context-"));
|
|
732
|
+
try {
|
|
733
|
+
const files = await walkFiles(this.sourceRoot);
|
|
734
|
+
for (const sourceFile of files) {
|
|
735
|
+
await assertInsideRoot(this.sourceRoot, sourceFile);
|
|
736
|
+
const rel = path.relative(this.sourceRoot, sourceFile).split(path.sep).join("/");
|
|
737
|
+
const companyPath = normalizeCompanyPath(rel);
|
|
738
|
+
if (!this.allowedRules.some((rule) => rule.test(companyPath))) continue;
|
|
739
|
+
const destination = path.join(projectionRoot, ...companyPath.slice(1).split("/"));
|
|
740
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
741
|
+
await copyFile(sourceFile, destination);
|
|
742
|
+
}
|
|
743
|
+
await makeProjectionReadonly(projectionRoot);
|
|
744
|
+
return {
|
|
745
|
+
binding,
|
|
746
|
+
handle: {
|
|
747
|
+
filesystem: binding.filesystem,
|
|
748
|
+
projectionRoot,
|
|
749
|
+
lifecycle: { active: true }
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
} catch (error) {
|
|
753
|
+
await makeProjectionRemovable(projectionRoot);
|
|
754
|
+
await rm(projectionRoot, { recursive: true, force: true });
|
|
755
|
+
throw error;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
function asRuntimeOperations(ops) {
|
|
760
|
+
return {
|
|
761
|
+
read: (descriptor) => ops.read(descriptor),
|
|
762
|
+
list: (descriptor) => ops.list(descriptor),
|
|
763
|
+
find: (descriptor, pattern, options) => ops.find(descriptor, pattern, options),
|
|
764
|
+
grep: (descriptor, pattern, options) => ops.grep(descriptor, pattern, options),
|
|
765
|
+
stat: (descriptor) => ops.stat(descriptor),
|
|
766
|
+
rejectMutation: (operation, descriptor) => ops.rejectMutation(operation, descriptor)
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
function createDefaultCompanyContextRootResolver(env = process.env) {
|
|
770
|
+
const explicitSourceRoot = env[GOVERNANCE_COMPANY_CONTEXT_ROOT_ENV]?.trim();
|
|
771
|
+
if (explicitSourceRoot) return () => path.resolve(explicitSourceRoot);
|
|
772
|
+
if (env[AGENT_MODE_ENV]?.trim() === "vercel-sandbox") return void 0;
|
|
773
|
+
const workspaceStorageRoot = env[AGENT_WORKSPACE_ROOT_ENV]?.trim() || process.cwd();
|
|
774
|
+
return (_ctx, companyContextWorkspaceId) => path.resolve(workspaceStorageRoot, companyContextWorkspaceId);
|
|
775
|
+
}
|
|
776
|
+
function logCompanyContextBindingError(ctx, reason, details = {}) {
|
|
777
|
+
const fields = {
|
|
778
|
+
code: ErrorCode2.enum.CONFIG_INVALID,
|
|
779
|
+
event: "governance.company_context.binding_omitted",
|
|
780
|
+
reason,
|
|
781
|
+
workspaceId: ctx.workspaceId,
|
|
782
|
+
requestId: ctx.requestId ?? ctx.request?.id,
|
|
783
|
+
...details
|
|
784
|
+
};
|
|
785
|
+
const message = "company_context binding omitted";
|
|
786
|
+
if (ctx.request?.log) ctx.request.log.error(fields, message);
|
|
787
|
+
else companyContextBindingFallbackLogger.error(fields, message);
|
|
788
|
+
}
|
|
789
|
+
async function resolveCompanyContextSourceRoot(ctx, companyContextWorkspaceId, resolver) {
|
|
790
|
+
if (!resolver) {
|
|
791
|
+
logCompanyContextBindingError(ctx, "missing_explicit_source_root_resolver", { companyContextWorkspaceId });
|
|
792
|
+
return null;
|
|
793
|
+
}
|
|
794
|
+
let candidate;
|
|
795
|
+
try {
|
|
796
|
+
candidate = await resolver(ctx, companyContextWorkspaceId);
|
|
797
|
+
} catch (error) {
|
|
798
|
+
logCompanyContextBindingError(ctx, "source_root_resolver_failed", { companyContextWorkspaceId, err: error });
|
|
799
|
+
return null;
|
|
800
|
+
}
|
|
801
|
+
if (!candidate?.trim()) {
|
|
802
|
+
logCompanyContextBindingError(ctx, "missing_explicit_source_root", { companyContextWorkspaceId });
|
|
803
|
+
return null;
|
|
804
|
+
}
|
|
805
|
+
try {
|
|
806
|
+
const sourceRoot = await realpath(path.resolve(candidate));
|
|
807
|
+
const sourceStat = await stat2(sourceRoot);
|
|
808
|
+
if (!sourceStat.isDirectory()) {
|
|
809
|
+
logCompanyContextBindingError(ctx, "source_root_not_directory", { companyContextWorkspaceId, sourceRoot });
|
|
810
|
+
return null;
|
|
811
|
+
}
|
|
812
|
+
await access(sourceRoot, constants.R_OK);
|
|
813
|
+
return sourceRoot;
|
|
814
|
+
} catch (error) {
|
|
815
|
+
logCompanyContextBindingError(ctx, "source_root_unavailable", { companyContextWorkspaceId, sourceRoot: path.resolve(candidate), err: error });
|
|
816
|
+
return null;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
function createGovernanceFilesystemBindings(service, options = {}) {
|
|
820
|
+
return async (ctx) => {
|
|
821
|
+
if (!service.isEnabled()) return void 0;
|
|
822
|
+
const user = userFromContext(ctx);
|
|
823
|
+
if (!user?.id) return [];
|
|
824
|
+
const rules = service.companyContextRules(user);
|
|
825
|
+
const companyContextWorkspaceId = service.companyContextWorkspaceId();
|
|
826
|
+
if (!companyContextWorkspaceId || rules.length === 0) return [];
|
|
827
|
+
if (ctx.workspaceId === companyContextWorkspaceId) return [];
|
|
828
|
+
const sourceRoot = await resolveCompanyContextSourceRoot(ctx, companyContextWorkspaceId, options.resolveCompanyContextRoot);
|
|
829
|
+
if (!sourceRoot) return [];
|
|
830
|
+
const projectionRootParent = options.projectionRootParent ?? os.tmpdir();
|
|
831
|
+
const binding = {
|
|
832
|
+
filesystem: COMPANY_CONTEXT_FILESYSTEM_ID,
|
|
833
|
+
access: "readonly",
|
|
834
|
+
mountPath: COMPANY_CONTEXT_MOUNT_PATH,
|
|
835
|
+
projection: "policy-filtered"
|
|
836
|
+
};
|
|
837
|
+
const boundCtx = {
|
|
838
|
+
humanUserId: user.id,
|
|
839
|
+
agentId: `agent:${ctx.sessionId ?? ctx.requestId ?? "request"}`,
|
|
840
|
+
sessionId: ctx.sessionId ?? ctx.requestId ?? "request",
|
|
841
|
+
workspaceId: ctx.workspaceId,
|
|
842
|
+
requestId: ctx.requestId ?? ctx.sessionId ?? "request"
|
|
843
|
+
};
|
|
844
|
+
const provider = new RegexFilteredCompanyContextProvider(sourceRoot, compileRules(rules), projectionRootParent);
|
|
845
|
+
const manager = new ScopedFilesystemRuntimeBindingManager({
|
|
846
|
+
resolver: { resolveBindings: async () => [binding] },
|
|
847
|
+
providers: { [COMPANY_CONTEXT_FILESYSTEM_ID]: provider }
|
|
848
|
+
});
|
|
849
|
+
async function withReadonlyOperations(fn) {
|
|
850
|
+
const plan = await manager.prepareRuntime(boundCtx);
|
|
851
|
+
try {
|
|
852
|
+
const prepared = plan.bindings.find((entry) => entry.binding.filesystem === COMPANY_CONTEXT_FILESYSTEM_ID);
|
|
853
|
+
if (!prepared) throw new Error("company_context binding was not prepared");
|
|
854
|
+
return await fn(createReadonlyProjectionOperations(prepared.handle));
|
|
855
|
+
} finally {
|
|
856
|
+
await manager.disposeRuntime(boundCtx);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
const operations = {
|
|
860
|
+
read: (descriptor) => withReadonlyOperations((ops) => ops.read(descriptor)),
|
|
861
|
+
list: (descriptor) => withReadonlyOperations((ops) => ops.list(descriptor)),
|
|
862
|
+
find: (descriptor, pattern, opOptions) => withReadonlyOperations((ops) => ops.find(descriptor, pattern, opOptions)),
|
|
863
|
+
grep: (descriptor, pattern, opOptions) => withReadonlyOperations((ops) => ops.grep(descriptor, pattern, opOptions)),
|
|
864
|
+
stat: (descriptor) => withReadonlyOperations((ops) => ops.stat(descriptor)),
|
|
865
|
+
rejectMutation(operation, descriptor) {
|
|
866
|
+
return asRuntimeOperations(createReadonlyProjectionOperations({ filesystem: COMPANY_CONTEXT_FILESYSTEM_ID, projectionRoot: sourceRoot })).rejectMutation(operation, descriptor);
|
|
867
|
+
}
|
|
868
|
+
};
|
|
869
|
+
return [{ filesystem: COMPANY_CONTEXT_FILESYSTEM_ID, access: "readonly", operations }];
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// src/server/index.ts
|
|
874
|
+
async function buildGovernanceService(options = {}) {
|
|
875
|
+
return createGovernanceService(await loadGovernancePolicy(options));
|
|
876
|
+
}
|
|
877
|
+
async function createGovernance(config) {
|
|
878
|
+
const service = await buildGovernanceService({ config });
|
|
879
|
+
return {
|
|
880
|
+
service,
|
|
881
|
+
status: service.policyStatus(),
|
|
882
|
+
serverPlugin: createGovernanceServerPlugin(service),
|
|
883
|
+
filterModels: createGovernanceModelFilter(service),
|
|
884
|
+
createMeteringSink: (delegate, getDb) => createGovernanceMeteringSink({ service, delegate, getDb }),
|
|
885
|
+
getFilesystemBindings: (options = {}) => createGovernanceFilesystemBindings(service, {
|
|
886
|
+
resolveCompanyContextRoot: createDefaultCompanyContextRootResolver(),
|
|
887
|
+
...options
|
|
888
|
+
}),
|
|
889
|
+
pi: { strictModelResolution: service.isEnabled() }
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
function governanceUserFromRequest(request) {
|
|
893
|
+
const user = request.user;
|
|
894
|
+
return user ? { id: user.id, email: user.email, emailVerified: user.emailVerified } : null;
|
|
895
|
+
}
|
|
896
|
+
function createGovernanceModelFilter(service) {
|
|
897
|
+
return async ({ request }, models, defaultModel) => {
|
|
898
|
+
const user = governanceUserFromRequest(request);
|
|
899
|
+
if (!service.isEnabled()) return { models, defaultModel };
|
|
900
|
+
if (!user) return { models: [] };
|
|
901
|
+
const allowedModels = service.allowedModelsForUser(user, models).filter((model) => (service.monthlyBudgetMicros(user, model) ?? 0) > 0);
|
|
902
|
+
const allowedDefault = defaultModel && allowedModels.some((model) => model.available && model.provider === defaultModel.provider && model.id === defaultModel.id) ? defaultModel : void 0;
|
|
903
|
+
return { models: allowedModels, defaultModel: allowedDefault };
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
function createGovernanceServerPlugin(service) {
|
|
907
|
+
return {
|
|
908
|
+
id: "full-app-governance",
|
|
909
|
+
label: "Full-app governance",
|
|
910
|
+
routes: async (app) => {
|
|
911
|
+
await app.register(governanceRoutes(service));
|
|
912
|
+
app.addHook("onReady", async () => {
|
|
913
|
+
await reconcileCompanyContextWorkspace(app, service);
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
export {
|
|
919
|
+
GOVERNANCE_DEV_EMAIL_VERIFICATION_OVERRIDE_ENV,
|
|
920
|
+
GOVERNANCE_POLICY_PATH_ENV,
|
|
921
|
+
GovernancePolicyError,
|
|
922
|
+
GovernanceService,
|
|
923
|
+
buildGovernanceService,
|
|
924
|
+
createDefaultCompanyContextRootResolver,
|
|
925
|
+
createGovernance,
|
|
926
|
+
createGovernanceFilesystemBindings,
|
|
927
|
+
createGovernanceMeteringSink,
|
|
928
|
+
createGovernanceModelFilter,
|
|
929
|
+
createGovernanceServerPlugin,
|
|
930
|
+
loadGovernancePolicy,
|
|
931
|
+
normalizePolicyEmail,
|
|
932
|
+
validateGovernancePolicy
|
|
933
|
+
};
|