@hachej/boring-governance 0.1.71 → 0.1.72

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.
@@ -1,3 +1,6 @@
1
+ // src/server/index.ts
2
+ import path3 from "path";
3
+
1
4
  // src/server/loadPolicy.ts
2
5
  import { readFile, stat } from "fs/promises";
3
6
  import { parse } from "yaml";
@@ -16,25 +19,25 @@ function normalizePolicyEmail(value) {
16
19
  function isRecord(value) {
17
20
  return typeof value === "object" && value !== null && !Array.isArray(value);
18
21
  }
19
- function nonEmptyString(value, path2) {
22
+ function nonEmptyString(value, path4) {
20
23
  if (typeof value !== "string" || value.trim().length === 0) {
21
- fail(`${path2} must be a non-empty string`);
24
+ fail(`${path4} must be a non-empty string`);
22
25
  }
23
26
  return value.trim();
24
27
  }
25
- function uuidString(value, path2) {
26
- const id = nonEmptyString(value, path2);
28
+ function uuidString(value, path4) {
29
+ const id = nonEmptyString(value, path4);
27
30
  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`);
31
+ fail(`${path4} must be a UUID workspace id`);
29
32
  }
30
33
  return id;
31
34
  }
32
- function finiteNumber(value, path2, opts) {
35
+ function finiteNumber(value, path4, opts) {
33
36
  if (typeof value !== "number" || !Number.isFinite(value)) {
34
- fail(`${path2} must be a finite number`);
37
+ fail(`${path4} must be a finite number`);
35
38
  }
36
39
  if (opts.allowZero ? value < opts.min : value <= opts.min) {
37
- fail(`${path2} must be ${opts.allowZero ? ">=" : ">"} ${opts.min}`);
40
+ fail(`${path4} must be ${opts.allowZero ? ">=" : ">"} ${opts.min}`);
38
41
  }
39
42
  return value;
40
43
  }
@@ -96,42 +99,42 @@ function hasTopLevelAlternation(pattern) {
96
99
  }
97
100
  return false;
98
101
  }
99
- function validateSegmentSafePattern(pattern, path2) {
102
+ function validateSegmentSafePattern(pattern, path4) {
100
103
  const { prefix, consumed, unsafeEscape } = literalPathPrefix(pattern);
101
- if (unsafeEscape) fail(`${path2} must use only literal regex escapes before the path-segment boundary`);
104
+ if (unsafeEscape) fail(`${path4} must use only literal regex escapes before the path-segment boundary`);
102
105
  if (!prefix) {
103
106
  const remainder2 = pattern.slice("^/".length);
104
107
  if (remainder2.startsWith(".*") || remainder2.startsWith("$")) return;
105
- fail(`${path2} must start with a literal path segment or ^/.*`);
108
+ fail(`${path4} must start with a literal path segment or ^/.*`);
106
109
  }
107
110
  const remainder = pattern.slice("^/".length + consumed);
108
111
  if (prefix.endsWith("/")) {
109
- if (/^[?*+{]/.test(remainder)) fail(`${path2} must not make the path-segment boundary slash optional`);
112
+ if (/^[?*+{]/.test(remainder)) fail(`${path4} must not make the path-segment boundary slash optional`);
110
113
  return;
111
114
  }
112
115
  if (remainder.startsWith("$")) return;
113
116
  for (const guard of ["(?:/|$)", "(?:$|/)", "(/|$)", "($|/)"]) {
114
117
  if (!remainder.startsWith(guard)) continue;
115
118
  if (/^[?*+{]/.test(remainder.slice(guard.length))) {
116
- fail(`${path2} must not make the path-segment boundary guard optional`);
119
+ fail(`${path4} must not make the path-segment boundary guard optional`);
117
120
  }
118
121
  return;
119
122
  }
120
- fail(`${path2} must make literal path-prefix grants segment-safe with /, $, or (?:/|$)`);
123
+ fail(`${path4} must make literal path-prefix grants segment-safe with /, $, or (?:/|$)`);
121
124
  }
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);
125
+ function validateRegexPattern(value, path4) {
126
+ const pattern = nonEmptyString(value, path4);
127
+ if (pattern.length > MAX_REGEX_LENGTH) fail(`${path4} must be at most ${MAX_REGEX_LENGTH} characters`);
128
+ if (!pattern.startsWith("^/")) fail(`${path4} must start with ^/`);
129
+ if (hasTopLevelAlternation(pattern)) fail(`${path4} must not use top-level regex alternation; add separate allow rules instead`);
130
+ validateSegmentSafePattern(pattern, path4);
128
131
  if (/\([^)]*[+*][^)]*\)[+*{]/.test(pattern)) {
129
- fail(`${path2} contains an unsafe nested quantifier`);
132
+ fail(`${path4} contains an unsafe nested quantifier`);
130
133
  }
131
134
  try {
132
135
  new RegExp(pattern);
133
136
  } catch (error) {
134
- fail(`${path2} must compile as a JavaScript RegExp: ${error instanceof Error ? error.message : String(error)}`);
137
+ fail(`${path4} must compile as a JavaScript RegExp: ${error instanceof Error ? error.message : String(error)}`);
135
138
  }
136
139
  return pattern;
137
140
  }
@@ -139,11 +142,11 @@ function validateModels(value, userPath, defaultMonthlyModelBudgetEur) {
139
142
  if (value === void 0) return [];
140
143
  if (!Array.isArray(value)) fail(`${userPath}.models must be an array`);
141
144
  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`, {
145
+ const path4 = `${userPath}.models[${index}]`;
146
+ if (!isRecord(entry)) fail(`${path4} must be an object`);
147
+ const provider = nonEmptyString(entry.provider, `${path4}.provider`);
148
+ const id = nonEmptyString(entry.id, `${path4}.id`);
149
+ const monthlyBudgetEur = entry.monthlyBudgetEur === void 0 ? defaultMonthlyModelBudgetEur : finiteNumber(entry.monthlyBudgetEur, `${path4}.monthlyBudgetEur`, {
147
150
  min: 0,
148
151
  allowZero: true
149
152
  });
@@ -155,6 +158,15 @@ function validateModels(value, userPath, defaultMonthlyModelBudgetEur) {
155
158
  };
156
159
  });
157
160
  }
161
+ function validateUserBudgets(value, userPath) {
162
+ if (value === void 0) return { monthlyEur: null, monthlyMicros: null };
163
+ if (!isRecord(value)) fail(`${userPath}.budgets must be an object`);
164
+ const monthlyEur = value.monthlyEur === void 0 ? null : finiteNumber(value.monthlyEur, `${userPath}.budgets.monthlyEur`, { min: 0, allowZero: true });
165
+ return {
166
+ monthlyEur,
167
+ monthlyMicros: monthlyEur === null ? null : Math.round(monthlyEur * MICROS_PER_EUR)
168
+ };
169
+ }
158
170
  function validateCompanyContext(value, userPath) {
159
171
  if (value === void 0) return { allow: [] };
160
172
  if (!isRecord(value)) fail(`${userPath}.companyContext must be an object`);
@@ -168,8 +180,8 @@ function validateCompanyContext(value, userPath) {
168
180
  allow: allow.map((pattern, index) => validateRegexPattern(pattern, `${userPath}.companyContext.allow[${index}]`))
169
181
  };
170
182
  }
171
- function validateRole(value, path2) {
172
- if (value !== "admin" && value !== "user") fail(`${path2} must be admin or user`);
183
+ function validateRole(value, path4) {
184
+ if (value !== "admin" && value !== "user") fail(`${path4} must be admin or user`);
173
185
  return value;
174
186
  }
175
187
  function validateGovernancePolicy(input) {
@@ -184,16 +196,17 @@ function validateGovernancePolicy(input) {
184
196
  if (!Array.isArray(candidate.users)) fail("users must be an array");
185
197
  const usersByEmail = /* @__PURE__ */ new Map();
186
198
  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`);
199
+ const path4 = `users[${index}]`;
200
+ if (!isRecord(entry)) fail(`${path4} must be an object`);
201
+ const email = normalizePolicyEmail(nonEmptyString(entry.email, `${path4}.email`));
202
+ if (!email.includes("@")) fail(`${path4}.email must be an email address`);
191
203
  if (usersByEmail.has(email)) fail(`duplicate user email: ${email}`);
192
204
  const user = {
193
205
  email,
194
- role: validateRole(entry.role, `${path2}.role`),
195
- models: validateModels(entry.models, path2, defaultMonthlyModelBudgetEur),
196
- companyContext: validateCompanyContext(entry.companyContext, path2)
206
+ role: validateRole(entry.role, `${path4}.role`),
207
+ budgets: validateUserBudgets(entry.budgets, path4),
208
+ models: validateModels(entry.models, path4, defaultMonthlyModelBudgetEur),
209
+ companyContext: validateCompanyContext(entry.companyContext, path4)
197
210
  };
198
211
  usersByEmail.set(email, user);
199
212
  return user;
@@ -219,13 +232,13 @@ function validateGovernancePolicy(input) {
219
232
  var MAX_POLICY_BYTES = 256 * 1024;
220
233
  var GOVERNANCE_POLICY_PATH_ENV = "BORING_GOVERNANCE_POLICY_PATH";
221
234
  var GOVERNANCE_DEV_EMAIL_VERIFICATION_OVERRIDE_ENV = "BORING_GOVERNANCE_ALLOW_UNVERIFIED_EMAIL_DEV";
222
- function invalidResult(path2, error) {
235
+ function invalidResult(path4, error) {
223
236
  return {
224
237
  enabled: true,
225
238
  policy: null,
226
239
  status: {
227
240
  state: "invalid",
228
- path: path2,
241
+ path: path4,
229
242
  message: error instanceof Error ? error.message : String(error)
230
243
  }
231
244
  };
@@ -348,10 +361,21 @@ var GovernanceService = class {
348
361
  const grant = userPolicy?.models.find((entry) => entry.provider === model.provider && entry.id === model.id);
349
362
  return grant?.monthlyBudgetMicros ?? null;
350
363
  }
364
+ userMonthlyBudgetMicros(user) {
365
+ if (!this.loaded.enabled) return null;
366
+ return this.userPolicy(user)?.budgets.monthlyMicros ?? null;
367
+ }
351
368
  companyContextRules(user) {
352
369
  if (!this.loaded.enabled) return [];
353
370
  return this.userPolicy(user)?.companyContext.allow ?? [];
354
371
  }
372
+ companyContextAccessForUser(user) {
373
+ if (!this.loaded.enabled) return "none";
374
+ const policy = this.userPolicy(user);
375
+ if (!policy) return "none";
376
+ if (policy.role === "admin") return "readwrite";
377
+ return policy.companyContext.allow.length > 0 ? "readonly" : "none";
378
+ }
355
379
  companyContextWorkspaceId() {
356
380
  return this.enabledPolicy()?.tenant.companyContextWorkspaceId ?? null;
357
381
  }
@@ -382,6 +406,7 @@ var GovernanceService = class {
382
406
  email: entry.email,
383
407
  role: entry.role,
384
408
  modelCount: entry.models.length,
409
+ monthlyBudgetEur: entry.budgets.monthlyEur,
385
410
  contextRuleCount: entry.companyContext.allow.length
386
411
  })),
387
412
  models: policy.users.flatMap((entry) => entry.models.map((model) => ({ ...model, email: entry.email }))),
@@ -516,9 +541,7 @@ async function reconcileCompanyContextWorkspace(app, service) {
516
541
 
517
542
  // src/server/metering.ts
518
543
  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;
544
+ import { ModelBudgetExceededError, PostgresBudgetReservationStore, UserBudgetExceededError } from "@hachej/boring-core/server";
522
545
  var DEFAULT_HOLD_TTL_SECONDS = 60 * 60;
523
546
  function authRequiredError() {
524
547
  return Object.assign(new Error("authentication required"), { statusCode: 401, code: ErrorCode.enum.UNAUTHORIZED });
@@ -530,7 +553,7 @@ function policyDeniedError() {
530
553
  return Object.assign(new Error("model is not allowed by governance policy"), { statusCode: 403, code: ErrorCode.enum.TOOL_INVALID_INPUT });
531
554
  }
532
555
  function budgetError(error) {
533
- if (typeof ModelBudgetExceededError2 === "function" && error instanceof ModelBudgetExceededError2) return error;
556
+ if (typeof ModelBudgetExceededError === "function" && error instanceof ModelBudgetExceededError) return error;
534
557
  return error instanceof Error ? error : new Error(String(error));
535
558
  }
536
559
  function runKey(input) {
@@ -538,8 +561,8 @@ function runKey(input) {
538
561
  }
539
562
  function createGovernanceMeteringSink(options) {
540
563
  let store;
541
- const getStore = () => store ??= new PostgresModelBudgetStore2(options.getDb());
542
- const reservationsByRun = /* @__PURE__ */ new Map();
564
+ const getStore = () => store ??= new PostgresBudgetReservationStore(options.getDb(), { eligibleLegacySources: ["pi-chat", "pi-chat-fallback", "pi-chat-expired"] });
565
+ const admissionsByRun = /* @__PURE__ */ new Map();
543
566
  const holdTtlSeconds = options.holdTtlSeconds ?? DEFAULT_HOLD_TTL_SECONDS;
544
567
  async function reserveGovernance(input) {
545
568
  if (!options.service.isEnabled()) return void 0;
@@ -552,35 +575,59 @@ function createGovernanceMeteringSink(options) {
552
575
  } catch {
553
576
  throw policyDeniedError();
554
577
  }
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);
578
+ const modelBudgetMicros = options.service.monthlyBudgetMicros(user, { provider: input.model.provider, id: input.model.id });
579
+ if (modelBudgetMicros === null || modelBudgetMicros <= 0) throw new ModelBudgetExceededError(0, 0, modelBudgetMicros ?? 0, 0);
557
580
  const policy = options.service.policy();
558
581
  const holdMicros = policy?.tenant.perRunHoldMicros ?? 1e6;
559
- const result = await getStore().reserve({
582
+ const now = /* @__PURE__ */ new Date();
583
+ let userReservationInput;
584
+ const userBudgetMicros = options.service.userMonthlyBudgetMicros(user);
585
+ if (userBudgetMicros !== null) {
586
+ if (userBudgetMicros <= 0) throw new (UserBudgetExceededError ?? ModelBudgetExceededError)(0, 0, userBudgetMicros, 0);
587
+ userReservationInput = {
588
+ scope: "user",
589
+ userId: input.userId,
590
+ workspaceId: input.workspaceId,
591
+ sessionId: input.sessionId,
592
+ runId: input.runId,
593
+ budgetMicros: userBudgetMicros,
594
+ holdMicros,
595
+ ttlSeconds: holdTtlSeconds,
596
+ now
597
+ };
598
+ }
599
+ const modelReservationInput = {
600
+ scope: "model",
560
601
  userId: input.userId,
561
602
  workspaceId: input.workspaceId,
562
603
  sessionId: input.sessionId,
563
604
  runId: input.runId,
564
605
  provider: input.model.provider,
565
606
  model: input.model.id,
566
- budgetMicros,
607
+ budgetMicros: modelBudgetMicros,
567
608
  holdMicros,
568
- ttlSeconds: holdTtlSeconds
569
- }).catch((error) => {
609
+ ttlSeconds: holdTtlSeconds,
610
+ now
611
+ };
612
+ const admission = await getStore().reserveAdmission({ user: userReservationInput, model: modelReservationInput }).catch((error) => {
570
613
  throw budgetError(error);
571
614
  });
572
615
  const key = runKey(input);
573
- if (key) reservationsByRun.set(key, result.reservationId);
574
- return result.reservationId;
616
+ if (key) admissionsByRun.set(key, admission);
617
+ return admission;
575
618
  }
576
619
  async function finishGovernance(input, action) {
577
620
  if (!options.service.isEnabled()) return;
578
621
  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);
622
+ const admission = key ? admissionsByRun.get(key) : void 0;
623
+ const status = action === "settle" ? "settled" : "released";
624
+ if (admission) {
625
+ await getStore().finishAdmission(admission, status);
626
+ if (key) admissionsByRun.delete(key);
627
+ return;
628
+ }
629
+ if (!input.userId || !input.runId) return;
630
+ await getStore().finishRun({ runId: input.runId, userId: input.userId }, status);
584
631
  }
585
632
  function releaseConsumesBudget(input) {
586
633
  return input.reason === "usage-write-failed" || input.reason === "fallback-hold-charge";
@@ -588,21 +635,24 @@ function createGovernanceMeteringSink(options) {
588
635
  return {
589
636
  isEnabled: () => options.service.isEnabled() || options.delegate.isEnabled?.() !== false,
590
637
  async reserveRun(input) {
591
- const governanceReservationId = await reserveGovernance(input);
638
+ const admission = await reserveGovernance(input);
592
639
  try {
593
640
  return await options.delegate.reserveRun(input);
594
641
  } catch (error) {
595
- if (governanceReservationId) await getStore().release({ reservationId: governanceReservationId }).catch(() => {
596
- });
597
- const key = runKey(input);
598
- if (key) reservationsByRun.delete(key);
642
+ try {
643
+ if (admission) await getStore().releaseCreated(admission);
644
+ } catch {
645
+ } finally {
646
+ const key = runKey(input);
647
+ if (key) admissionsByRun.delete(key);
648
+ }
599
649
  throw error;
600
650
  }
601
651
  },
602
652
  recordUsage(input) {
603
653
  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);
654
+ const admission = key ? admissionsByRun.get(key) : void 0;
655
+ return options.delegate.recordUsage(admission ? { ...input, metadata: { ...input.metadata ?? {}, ...getStore().metadataForAdmission(admission) } } : input);
606
656
  },
607
657
  async settleRun(input) {
608
658
  try {
@@ -627,10 +677,10 @@ function createGovernanceMeteringSink(options) {
627
677
  }
628
678
 
629
679
  // 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";
680
+ import { constants as constants2 } from "fs";
681
+ import { access, chmod, copyFile, lstat as lstat2, mkdir as mkdir2, mkdtemp, readdir as readdir2, realpath as realpath2, rm as rm2, stat as stat3 } from "fs/promises";
632
682
  import os from "os";
633
- import path from "path";
683
+ import path2 from "path";
634
684
  import { createLogger } from "@hachej/boring-agent/server";
635
685
  import { ErrorCode as ErrorCode2 } from "@hachej/boring-agent/shared";
636
686
  import {
@@ -638,6 +688,260 @@ import {
638
688
  ScopedFilesystemRuntimeBindingManager,
639
689
  createReadonlyProjectionOperations
640
690
  } from "@hachej/boring-bash/server";
691
+
692
+ // src/server/companyContextStore.ts
693
+ import { constants } from "fs";
694
+ import { lstat, mkdir, open, readdir, readFile as readFile2, realpath, rename, rm, stat as stat2 } from "fs/promises";
695
+ import path from "path";
696
+ import { randomUUID } from "crypto";
697
+ import lockfile from "proper-lockfile";
698
+ var COMPANY_CONTEXT_CONFLICT_CODE = "conflict";
699
+ var CompanyContextConflictError = class extends Error {
700
+ constructor(details) {
701
+ super(details.currentMtimeMs === void 0 ? "file no longer exists" : "file has been modified since last read");
702
+ this.details = details;
703
+ this.name = "CompanyContextConflictError";
704
+ }
705
+ details;
706
+ statusCode = 409;
707
+ code = COMPANY_CONTEXT_CONFLICT_CODE;
708
+ };
709
+ var COMPANY_CONTEXT_STATE_DIR = ".boring-governance";
710
+ var mutationQueues = /* @__PURE__ */ new Map();
711
+ function normalizeCompanyPath(value) {
712
+ const normalized = value.replace(/\\/g, "/");
713
+ if (normalized.includes("\0")) throw Object.assign(new Error("invalid company path"), { code: "EPERM" });
714
+ const withRoot = normalized.startsWith("/") ? normalized : `/${normalized}`;
715
+ const parts = withRoot.split("/").filter(Boolean);
716
+ if (parts.some((part) => part === "." || part === ".." || part === COMPANY_CONTEXT_STATE_DIR)) {
717
+ throw Object.assign(new Error("company path traversal is not allowed"), { code: "EPERM" });
718
+ }
719
+ return `/${parts.join("/")}`;
720
+ }
721
+ function assertContained(root, candidate) {
722
+ const relative = path.relative(root, candidate);
723
+ if (relative.startsWith("..") || path.isAbsolute(relative)) {
724
+ throw Object.assign(new Error("path escapes company context root"), { code: "EPERM" });
725
+ }
726
+ }
727
+ async function inspectExistingPath(root, target, allowMissingLeaf) {
728
+ assertContained(root, target);
729
+ const relative = path.relative(root, target);
730
+ let current = root;
731
+ const parts = relative.split(path.sep).filter(Boolean);
732
+ for (let index = 0; index < parts.length; index += 1) {
733
+ current = path.join(current, parts[index]);
734
+ try {
735
+ const entry = await lstat(current);
736
+ if (entry.isSymbolicLink()) throw Object.assign(new Error("company context symlinks are not allowed"), { code: "EPERM" });
737
+ if (index < parts.length - 1 && !entry.isDirectory()) {
738
+ throw Object.assign(new Error("company context parent is not a directory"), { code: "ENOTDIR" });
739
+ }
740
+ } catch (error) {
741
+ if (error.code === "ENOENT" && allowMissingLeaf && index === parts.length - 1) return;
742
+ throw error;
743
+ }
744
+ }
745
+ }
746
+ async function acquireFilesystemLock(root) {
747
+ const stateRoot = path.join(root, COMPANY_CONTEXT_STATE_DIR);
748
+ const lockPath = path.join(stateRoot, "mutation.lock");
749
+ await mkdir(stateRoot, { recursive: true, mode: 448 });
750
+ const stateStat = await lstat(stateRoot);
751
+ if (stateStat.isSymbolicLink() || !stateStat.isDirectory()) {
752
+ throw Object.assign(new Error("unsafe company context state directory"), { code: "EPERM" });
753
+ }
754
+ return await lockfile.lock(root, {
755
+ lockfilePath: lockPath,
756
+ realpath: false,
757
+ stale: 3e4,
758
+ update: 5e3,
759
+ retries: { retries: 100, minTimeout: 25, maxTimeout: 100 }
760
+ });
761
+ }
762
+ async function withMutationLock(key, operation) {
763
+ const previous = mutationQueues.get(key) ?? Promise.resolve();
764
+ let releaseProcessLock;
765
+ const current = new Promise((resolve) => {
766
+ releaseProcessLock = resolve;
767
+ });
768
+ const queued = previous.then(() => current);
769
+ mutationQueues.set(key, queued);
770
+ await previous;
771
+ let releaseFilesystemLock = null;
772
+ try {
773
+ releaseFilesystemLock = await acquireFilesystemLock(key);
774
+ return await operation();
775
+ } finally {
776
+ await releaseFilesystemLock?.().catch(() => {
777
+ });
778
+ releaseProcessLock();
779
+ if (mutationQueues.get(key) === queued) mutationQueues.delete(key);
780
+ }
781
+ }
782
+ var CompanyContextStore = class _CompanyContextStore {
783
+ constructor(root) {
784
+ this.root = root;
785
+ }
786
+ root;
787
+ static async open(root) {
788
+ const canonicalRoot = await realpath(path.resolve(root));
789
+ const rootStat = await stat2(canonicalRoot);
790
+ if (!rootStat.isDirectory()) throw new Error("company context root is not a directory");
791
+ return new _CompanyContextStore(canonicalRoot);
792
+ }
793
+ resolve(companyPath) {
794
+ const normalized = normalizeCompanyPath(companyPath);
795
+ const target = path.resolve(this.root, `.${normalized}`);
796
+ assertContained(this.root, target);
797
+ return { root: this.root, target };
798
+ }
799
+ async read(companyPath) {
800
+ const { target } = this.resolve(companyPath);
801
+ await inspectExistingPath(this.root, target, false);
802
+ for (let attempt = 0; attempt < 2; attempt += 1) {
803
+ const before = await stat2(target);
804
+ if (!before.isFile()) throw Object.assign(new Error("company context path is not a file"), { code: "EISDIR" });
805
+ const content = await readFile2(target, "utf8");
806
+ const after = await stat2(target);
807
+ if (before.mtimeMs === after.mtimeMs && before.size === after.size) return { content, mtimeMs: after.mtimeMs };
808
+ }
809
+ throw Object.assign(new Error("company context changed while reading"), { statusCode: 409, code: COMPANY_CONTEXT_CONFLICT_CODE });
810
+ }
811
+ async list(companyPath) {
812
+ const { target } = this.resolve(companyPath);
813
+ await inspectExistingPath(this.root, target, false);
814
+ const entries = await readdir(target, { withFileTypes: true });
815
+ return {
816
+ entries: entries.filter((entry) => !entry.isSymbolicLink() && entry.name !== COMPANY_CONTEXT_STATE_DIR).map((entry) => entry.name)
817
+ };
818
+ }
819
+ async walk(companyPath) {
820
+ const normalized = normalizeCompanyPath(companyPath);
821
+ const pending = [normalized];
822
+ const files = [];
823
+ while (pending.length > 0) {
824
+ const current = pending.shift();
825
+ const listed = await this.list(current);
826
+ for (const name of listed.entries) {
827
+ const child = current === "/" ? `/${name}` : `${current}/${name}`;
828
+ const childStat = await this.stat(child);
829
+ if (childStat.isDirectory) pending.push(child);
830
+ else files.push(child);
831
+ }
832
+ }
833
+ return files;
834
+ }
835
+ async find(companyPath, pattern, options = {}) {
836
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/\u0000/g, ".*");
837
+ const matcher = new RegExp(`^${escaped}$`);
838
+ const paths = (await this.walk(companyPath)).filter((entry) => matcher.test(path.posix.basename(entry)) || matcher.test(entry));
839
+ const offset = Math.max(0, options.offset ?? 0);
840
+ return { paths: paths.slice(offset, options.limit === void 0 ? void 0 : offset + Math.max(0, options.limit)) };
841
+ }
842
+ async grep(companyPath, pattern, options = {}) {
843
+ const matcher = new RegExp(pattern);
844
+ const matches = [];
845
+ for (const file of await this.walk(companyPath)) {
846
+ const { content } = await this.read(file);
847
+ for (const [index, line] of content.split(/\r?\n/).entries()) {
848
+ matcher.lastIndex = 0;
849
+ if (matcher.test(line)) matches.push({ path: file, line: index + 1, text: line });
850
+ }
851
+ }
852
+ const offset = Math.max(0, options.offset ?? 0);
853
+ return { matches: matches.slice(offset, options.limit === void 0 ? void 0 : offset + Math.max(0, options.limit)) };
854
+ }
855
+ async stat(companyPath) {
856
+ const { target } = this.resolve(companyPath);
857
+ await inspectExistingPath(this.root, target, false);
858
+ const entry = await stat2(target);
859
+ return { isDirectory: entry.isDirectory(), mtimeMs: entry.mtimeMs };
860
+ }
861
+ async write(companyPath, content, expectedMtimeMs) {
862
+ const { target } = this.resolve(companyPath);
863
+ return withMutationLock(this.root, async () => {
864
+ await inspectExistingPath(this.root, path.dirname(target), false);
865
+ let currentMtimeMs;
866
+ try {
867
+ await inspectExistingPath(this.root, target, false);
868
+ currentMtimeMs = (await stat2(target)).mtimeMs;
869
+ } catch (error) {
870
+ if (error.code !== "ENOENT") throw error;
871
+ }
872
+ if (expectedMtimeMs !== void 0 && currentMtimeMs !== expectedMtimeMs) {
873
+ throw new CompanyContextConflictError({ currentMtimeMs, expectedMtimeMs });
874
+ }
875
+ const temporary = path.join(path.dirname(target), `.${path.basename(target)}.${randomUUID()}.tmp`);
876
+ let promoted = false;
877
+ try {
878
+ const handle = await open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 384);
879
+ try {
880
+ await handle.writeFile(content, "utf8");
881
+ await handle.sync();
882
+ } finally {
883
+ await handle.close();
884
+ }
885
+ await rename(temporary, target);
886
+ promoted = true;
887
+ return { mtimeMs: (await stat2(target)).mtimeMs };
888
+ } finally {
889
+ if (!promoted) await rm(temporary, { force: true }).catch(() => {
890
+ });
891
+ }
892
+ });
893
+ }
894
+ async delete(companyPath) {
895
+ const { target } = this.resolve(companyPath);
896
+ await withMutationLock(this.root, async () => {
897
+ await inspectExistingPath(this.root, target, false);
898
+ const entry = await lstat(target);
899
+ if (!entry.isFile()) throw Object.assign(new Error("company context path is not a file"), { code: "EPERM" });
900
+ await rm(target);
901
+ });
902
+ }
903
+ async mkdir(companyPath, recursive = false) {
904
+ const { target } = this.resolve(companyPath);
905
+ await withMutationLock(this.root, async () => {
906
+ if (!recursive) {
907
+ await inspectExistingPath(this.root, path.dirname(target), false);
908
+ await mkdir(target);
909
+ return;
910
+ }
911
+ const relative = path.relative(this.root, target);
912
+ let current = this.root;
913
+ for (const segment of relative.split(path.sep).filter(Boolean)) {
914
+ current = path.join(current, segment);
915
+ try {
916
+ const entry = await lstat(current);
917
+ if (entry.isSymbolicLink() || !entry.isDirectory()) {
918
+ throw Object.assign(new Error("unsafe company context directory"), { code: "EPERM" });
919
+ }
920
+ } catch (error) {
921
+ if (error.code !== "ENOENT") throw error;
922
+ await mkdir(current);
923
+ }
924
+ }
925
+ });
926
+ }
927
+ async move(fromPath, toPath) {
928
+ const from = this.resolve(fromPath).target;
929
+ const to = this.resolve(toPath).target;
930
+ await withMutationLock(this.root, async () => {
931
+ await inspectExistingPath(this.root, from, false);
932
+ await inspectExistingPath(this.root, path.dirname(to), false);
933
+ try {
934
+ await inspectExistingPath(this.root, to, false);
935
+ throw Object.assign(new Error("destination already exists"), { code: "EEXIST" });
936
+ } catch (error) {
937
+ if (error.code !== "ENOENT") throw error;
938
+ }
939
+ await rename(from, to);
940
+ });
941
+ }
942
+ };
943
+
944
+ // src/server/filesystemBindings.ts
641
945
  var COMPANY_CONTEXT_MOUNT_PATH = "/company_context";
642
946
  var AGENT_MODE_ENV = "BORING_AGENT_MODE";
643
947
  var AGENT_WORKSPACE_ROOT_ENV = "BORING_AGENT_WORKSPACE_ROOT";
@@ -648,7 +952,7 @@ var companyContextBindingFallbackLogger = {
648
952
  companyContextBindingCanonicalLogger.error(message, fields);
649
953
  }
650
954
  };
651
- function normalizeCompanyPath(value) {
955
+ function normalizeCompanyPath2(value) {
652
956
  const normalized = value.replace(/\\/g, "/");
653
957
  if (normalized.includes("\0")) throw new Error("invalid company path");
654
958
  const withRoot = normalized.startsWith("/") ? normalized : `/${normalized}`;
@@ -657,17 +961,18 @@ function normalizeCompanyPath(value) {
657
961
  return `/${parts.join("/")}`;
658
962
  }
659
963
  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");
964
+ const realRoot = await realpath2(root);
965
+ const existing = await realpath2(candidate);
966
+ const rel = path2.relative(realRoot, existing);
967
+ if (rel.startsWith("..") || path2.isAbsolute(rel)) throw new Error("path escapes company context root");
664
968
  }
665
969
  async function walkFiles(root, current = root) {
666
- const entries = await readdir(current, { withFileTypes: true });
970
+ const entries = await readdir2(current, { withFileTypes: true });
667
971
  const out = [];
668
972
  for (const entry of entries) {
669
- const absolutePath = path.join(current, entry.name);
670
- const entryStat = await lstat(absolutePath);
973
+ if (current === root && entry.name === COMPANY_CONTEXT_STATE_DIR) continue;
974
+ const absolutePath = path2.join(current, entry.name);
975
+ const entryStat = await lstat2(absolutePath);
671
976
  if (entryStat.isSymbolicLink()) continue;
672
977
  if (entry.isDirectory()) out.push(...await walkFiles(root, absolutePath));
673
978
  else if (entry.isFile()) out.push(absolutePath);
@@ -675,9 +980,9 @@ async function walkFiles(root, current = root) {
675
980
  return out;
676
981
  }
677
982
  async function makeProjectionReadonly(root, current = root) {
678
- const entries = await readdir(current, { withFileTypes: true });
983
+ const entries = await readdir2(current, { withFileTypes: true });
679
984
  for (const entry of entries) {
680
- const absolutePath = path.join(current, entry.name);
985
+ const absolutePath = path2.join(current, entry.name);
681
986
  if (entry.isDirectory()) await makeProjectionReadonly(root, absolutePath);
682
987
  else if (entry.isFile()) await chmod(absolutePath, 256);
683
988
  }
@@ -686,9 +991,9 @@ async function makeProjectionReadonly(root, current = root) {
686
991
  async function makeProjectionRemovable(current) {
687
992
  await chmod(current, 448).catch(() => {
688
993
  });
689
- const entries = await readdir(current, { withFileTypes: true }).catch(() => []);
994
+ const entries = await readdir2(current, { withFileTypes: true }).catch(() => []);
690
995
  for (const entry of entries) {
691
- const absolutePath = path.join(current, entry.name);
996
+ const absolutePath = path2.join(current, entry.name);
692
997
  if (entry.isDirectory()) await makeProjectionRemovable(absolutePath);
693
998
  else await chmod(absolutePath, 384).catch(() => {
694
999
  });
@@ -730,7 +1035,7 @@ var RegexFilteredCompanyContextProvider = class {
730
1035
  if (handle.lifecycle) handle.lifecycle.active = false;
731
1036
  if (handle.projectionRoot) {
732
1037
  await makeProjectionRemovable(handle.projectionRoot);
733
- await rm(handle.projectionRoot, { recursive: true, force: true });
1038
+ await rm2(handle.projectionRoot, { recursive: true, force: true });
734
1039
  }
735
1040
  }
736
1041
  async prepareBinding(_ctx, binding) {
@@ -738,17 +1043,17 @@ var RegexFilteredCompanyContextProvider = class {
738
1043
  if (binding.access !== "readonly" || binding.projection !== "policy-filtered") {
739
1044
  throw new Error("company_context policy mount only supports readonly policy-filtered bindings");
740
1045
  }
741
- await access(this.sourceRoot, constants.R_OK);
742
- const projectionRoot = await mkdtemp(path.join(this.projectionRootParent, "boring-company-context-"));
1046
+ await access(this.sourceRoot, constants2.R_OK);
1047
+ const projectionRoot = await mkdtemp(path2.join(this.projectionRootParent, "boring-company-context-"));
743
1048
  try {
744
1049
  const files = await walkFiles(this.sourceRoot);
745
1050
  for (const sourceFile of files) {
746
1051
  await assertInsideRoot(this.sourceRoot, sourceFile);
747
- const rel = path.relative(this.sourceRoot, sourceFile).split(path.sep).join("/");
748
- const companyPath = normalizeCompanyPath(rel);
1052
+ const rel = path2.relative(this.sourceRoot, sourceFile).split(path2.sep).join("/");
1053
+ const companyPath = normalizeCompanyPath2(rel);
749
1054
  if (!this.allowedRules.some((rule) => rule.test(companyPath))) continue;
750
- const destination = path.join(projectionRoot, ...companyPath.slice(1).split("/"));
751
- await mkdir(path.dirname(destination), { recursive: true });
1055
+ const destination = path2.join(projectionRoot, ...companyPath.slice(1).split("/"));
1056
+ await mkdir2(path2.dirname(destination), { recursive: true });
752
1057
  await copyFile(sourceFile, destination);
753
1058
  }
754
1059
  await makeProjectionReadonly(projectionRoot);
@@ -762,7 +1067,7 @@ var RegexFilteredCompanyContextProvider = class {
762
1067
  };
763
1068
  } catch (error) {
764
1069
  await makeProjectionRemovable(projectionRoot);
765
- await rm(projectionRoot, { recursive: true, force: true });
1070
+ await rm2(projectionRoot, { recursive: true, force: true });
766
1071
  throw error;
767
1072
  }
768
1073
  }
@@ -779,10 +1084,10 @@ function asRuntimeOperations(ops) {
779
1084
  }
780
1085
  function createDefaultCompanyContextRootResolver(env = process.env) {
781
1086
  const explicitSourceRoot = env[GOVERNANCE_COMPANY_CONTEXT_ROOT_ENV]?.trim();
782
- if (explicitSourceRoot) return () => path.resolve(explicitSourceRoot);
1087
+ if (explicitSourceRoot) return () => path2.resolve(explicitSourceRoot);
783
1088
  if (env[AGENT_MODE_ENV]?.trim() === "vercel-sandbox") return void 0;
784
1089
  const workspaceStorageRoot = env[AGENT_WORKSPACE_ROOT_ENV]?.trim() || process.cwd();
785
- return (_ctx, companyContextWorkspaceId) => path.resolve(workspaceStorageRoot, companyContextWorkspaceId);
1090
+ return (_ctx, companyContextWorkspaceId) => path2.resolve(workspaceStorageRoot, companyContextWorkspaceId);
786
1091
  }
787
1092
  function logCompanyContextBindingError(ctx, reason, details = {}) {
788
1093
  const fields = {
@@ -814,16 +1119,16 @@ async function resolveCompanyContextSourceRoot(ctx, companyContextWorkspaceId, r
814
1119
  return null;
815
1120
  }
816
1121
  try {
817
- const sourceRoot = await realpath(path.resolve(candidate));
818
- const sourceStat = await stat2(sourceRoot);
1122
+ const sourceRoot = await realpath2(path2.resolve(candidate));
1123
+ const sourceStat = await stat3(sourceRoot);
819
1124
  if (!sourceStat.isDirectory()) {
820
1125
  logCompanyContextBindingError(ctx, "source_root_not_directory", { companyContextWorkspaceId, sourceRoot });
821
1126
  return null;
822
1127
  }
823
- await access(sourceRoot, constants.R_OK);
1128
+ await access(sourceRoot, constants2.R_OK);
824
1129
  return sourceRoot;
825
1130
  } catch (error) {
826
- logCompanyContextBindingError(ctx, "source_root_unavailable", { companyContextWorkspaceId, sourceRoot: path.resolve(candidate), err: error });
1131
+ logCompanyContextBindingError(ctx, "source_root_unavailable", { companyContextWorkspaceId, sourceRoot: path2.resolve(candidate), err: error });
827
1132
  return null;
828
1133
  }
829
1134
  }
@@ -832,12 +1137,44 @@ function createGovernanceFilesystemBindings(service, options = {}) {
832
1137
  if (!service.isEnabled()) return void 0;
833
1138
  const user = userFromContext(ctx);
834
1139
  if (!user?.id) return [];
835
- const rules = service.companyContextRules(user);
1140
+ const policyAccess = service.companyContextAccessForUser(user);
1141
+ const accessMode = policyAccess === "readwrite" && options.allowAdminMutations !== true ? "readonly" : policyAccess;
836
1142
  const companyContextWorkspaceId = service.companyContextWorkspaceId();
837
- if (!companyContextWorkspaceId || rules.length === 0) return [];
1143
+ if (!companyContextWorkspaceId || accessMode === "none") return [];
838
1144
  if (ctx.workspaceId === companyContextWorkspaceId) return [];
839
1145
  const sourceRoot = await resolveCompanyContextSourceRoot(ctx, companyContextWorkspaceId, options.resolveCompanyContextRoot);
840
1146
  if (!sourceRoot) return [];
1147
+ if (accessMode === "readwrite") {
1148
+ const store = await CompanyContextStore.open(sourceRoot);
1149
+ const operations2 = {
1150
+ read: (descriptor) => store.read(descriptor.path),
1151
+ list: (descriptor) => store.list(descriptor.path),
1152
+ find: (descriptor, pattern, opOptions) => store.find(descriptor.path, pattern, opOptions),
1153
+ grep: (descriptor, pattern, opOptions) => store.grep(descriptor.path, pattern, opOptions),
1154
+ stat: async (descriptor) => {
1155
+ const result = await store.stat(descriptor.path);
1156
+ return { isDirectory: result.isDirectory, metadata: { mtimeMs: result.mtimeMs } };
1157
+ },
1158
+ write: (descriptor) => store.write(descriptor.path, descriptor.content, descriptor.expectedMtimeMs),
1159
+ delete: async (descriptor) => {
1160
+ await store.delete(descriptor.path);
1161
+ return {};
1162
+ },
1163
+ move: async (descriptor) => {
1164
+ await store.move(descriptor.from, descriptor.to);
1165
+ return {};
1166
+ },
1167
+ mkdir: async (descriptor) => {
1168
+ await store.mkdir(descriptor.path, descriptor.recursive);
1169
+ return {};
1170
+ },
1171
+ rejectMutation(operation) {
1172
+ throw new Error(`company_context ${operation} operation is unavailable`);
1173
+ }
1174
+ };
1175
+ return [{ filesystem: COMPANY_CONTEXT_FILESYSTEM_ID, access: "readwrite", operations: operations2 }];
1176
+ }
1177
+ const rules = service.companyContextRules(user);
841
1178
  const projectionRootParent = options.projectionRootParent ?? os.tmpdir();
842
1179
  const binding = {
843
1180
  filesystem: COMPANY_CONTEXT_FILESYSTEM_ID,
@@ -885,6 +1222,15 @@ function createGovernanceFilesystemBindings(service, options = {}) {
885
1222
  async function buildGovernanceService(options = {}) {
886
1223
  return createGovernanceService(await loadGovernancePolicy(options));
887
1224
  }
1225
+ function defaultAdminMutationsAllowed(options) {
1226
+ if (options.resolveCompanyContextRoot) return false;
1227
+ const companyRoot = process.env.BORING_GOVERNANCE_COMPANY_CONTEXT_ROOT?.trim();
1228
+ if (!companyRoot) return false;
1229
+ const workspaceRoot = path3.resolve(process.env.BORING_AGENT_WORKSPACE_ROOT?.trim() || process.cwd());
1230
+ const resolvedCompanyRoot = path3.resolve(companyRoot);
1231
+ const relative = path3.relative(workspaceRoot, resolvedCompanyRoot);
1232
+ return relative.startsWith("..") || path3.isAbsolute(relative);
1233
+ }
888
1234
  async function createGovernance(config) {
889
1235
  const service = await buildGovernanceService({ config });
890
1236
  return {
@@ -894,8 +1240,9 @@ async function createGovernance(config) {
894
1240
  filterModels: createGovernanceModelFilter(service),
895
1241
  createMeteringSink: (delegate, getDb) => createGovernanceMeteringSink({ service, delegate, getDb }),
896
1242
  getFilesystemBindings: (options = {}) => createGovernanceFilesystemBindings(service, {
897
- resolveCompanyContextRoot: createDefaultCompanyContextRootResolver(),
898
- ...options
1243
+ ...options,
1244
+ resolveCompanyContextRoot: options.resolveCompanyContextRoot ?? createDefaultCompanyContextRootResolver(),
1245
+ allowAdminMutations: options.allowAdminMutations ?? defaultAdminMutationsAllowed(options)
899
1246
  }),
900
1247
  pi: { strictModelResolution: service.isEnabled() }
901
1248
  };
@@ -909,6 +1256,8 @@ function createGovernanceModelFilter(service) {
909
1256
  const user = governanceUserFromRequest(request);
910
1257
  if (!service.isEnabled()) return { models, defaultModel };
911
1258
  if (!user) return { models: [] };
1259
+ const aggregateBudgetMicros = service.userMonthlyBudgetMicros(user);
1260
+ if (aggregateBudgetMicros !== null && aggregateBudgetMicros <= 0) return { models: [], defaultModel: void 0 };
912
1261
  const allowedModels = service.allowedModelsForUser(user, models).filter((model) => (service.monthlyBudgetMicros(user, model) ?? 0) > 0);
913
1262
  const allowedDefault = defaultModel && allowedModels.some((model) => model.available && model.provider === defaultModel.provider && model.id === defaultModel.id) ? defaultModel : void 0;
914
1263
  return { models: allowedModels, defaultModel: allowedDefault };