@suveren/gateway 0.29.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/content/integrations/gmail.json +13 -2
  2. package/content/integrations/mail.json +12 -2
  3. package/content/integrations/mollie.json +10 -0
  4. package/dist/control-plane/index.mjs +125 -37
  5. package/dist/mcp-server/http.mjs +772 -420
  6. package/dist/ui/assets/index-C2BnlNib.js +123 -0
  7. package/dist/ui/assets/{index-DUIlMVlP.css → index-DPgewsLG.css} +1 -1
  8. package/dist/ui/index.html +2 -2
  9. package/node_modules/qs/.github/THREAT_MODEL.md +4 -2
  10. package/node_modules/qs/CHANGELOG.md +38 -14
  11. package/node_modules/qs/README.md +31 -3
  12. package/node_modules/qs/dist/qs.js +20 -20
  13. package/node_modules/qs/lib/parse.js +94 -4
  14. package/node_modules/qs/lib/stringify.js +4 -4
  15. package/node_modules/qs/lib/utils.js +130 -6
  16. package/node_modules/qs/package.json +4 -4
  17. package/node_modules/qs/test/parse.js +174 -0
  18. package/node_modules/qs/test/stringify.js +263 -33
  19. package/node_modules/qs/test/utils.js +184 -0
  20. package/node_modules/sanitize-html/index.js +47 -10
  21. package/node_modules/sanitize-html/package.json +2 -3
  22. package/package.json +1 -1
  23. package/dist/ui/assets/index-C3vTF2Ye.js +0 -123
  24. package/node_modules/deepmerge/.editorconfig +0 -7
  25. package/node_modules/deepmerge/.eslintcache +0 -1
  26. package/node_modules/deepmerge/changelog.md +0 -167
  27. package/node_modules/deepmerge/dist/cjs.js +0 -133
  28. package/node_modules/deepmerge/dist/umd.js +0 -139
  29. package/node_modules/deepmerge/index.d.ts +0 -20
  30. package/node_modules/deepmerge/index.js +0 -106
  31. package/node_modules/deepmerge/license.txt +0 -21
  32. package/node_modules/deepmerge/package.json +0 -42
  33. package/node_modules/deepmerge/readme.md +0 -264
  34. package/node_modules/deepmerge/rollup.config.js +0 -22
@@ -1071,6 +1071,25 @@ var SPClient = class {
1071
1071
  if (!res.ok) return null;
1072
1072
  return res.json();
1073
1073
  }
1074
+ /**
1075
+ * One proposal by id, whatever its status — `GET /api/proposals/:id`
1076
+ * (the AS admits the creator, an approver, or the authorization's owner;
1077
+ * this gateway only ever calls it for an id it itself submitted, via
1078
+ * proposalSubmissions, so the creator case always applies). Used by the
1079
+ * `/internal/my-pending-proposals` stand-in (bin/http.ts) — see that
1080
+ * route's doc comment for why it exists at all: the AS has no endpoint
1081
+ * to list "proposals I created" scoped by creator, only `?domain=`,
1082
+ * `?status=committed/rejected`, and `?approver=me`, none of which ever
1083
+ * match an above-cap proposal that has no pendingDomains unless the
1084
+ * caller also happens to be a pendingApprover.
1085
+ */
1086
+ async getProposal(id) {
1087
+ const res = await this.fetch(`/api/proposals/${encodeURIComponent(id)}`);
1088
+ if (res.status === 404) return null;
1089
+ if (!res.ok) throw new Error(`SP proposal request failed: ${res.status}`);
1090
+ const data = await res.json();
1091
+ return data.proposal;
1092
+ }
1074
1093
  /**
1075
1094
  * Get proposals that have been fully committed and are ready for execution.
1076
1095
  */
@@ -1124,7 +1143,7 @@ var AsKeyMismatchError = class extends Error {
1124
1143
  }
1125
1144
  };
1126
1145
  function isCommitmentDowngrade(auth) {
1127
- const requiresReview = auth.signedCommitmentMode === "review" || auth.signedCommitmentMode === "review_above_cap";
1146
+ const requiresReview = auth.signedCommitmentMode === "review";
1128
1147
  return requiresReview && (auth.deferredCommitmentDomains ?? []).length === 0;
1129
1148
  }
1130
1149
  var AttestationCache = class {
@@ -1171,23 +1190,37 @@ var AttestationCache = class {
1171
1190
  */
1172
1191
  activeWorkspaceGroupId = null;
1173
1192
  /**
1174
- * Re-resolve {@link activeWorkspaceGroupId} from the Authority Server.
1175
- * Called by the resync-gates flow (http.ts's `/internal/resync-gates`),
1176
- * never on the hot tool-call path — it makes up to two AS calls. Leaves
1177
- * the previous value in place on any failure (network error, unexpected
1178
- * shape): losing a known-good value on a transient blip would turn this
1179
- * defence-in-depth check into a false refusal of every cached mandate,
1180
- * which is worse than leaving it momentarily stale.
1193
+ * TR7 (work-plan "Added 2026-10-14"): the account's own Personal group id
1194
+ * — learned alongside {@link activeWorkspaceGroupId} so {@link isTeamGroup}
1195
+ * can tell a TEAM-owned authorization apart from a Personal one without a
1196
+ * live AS call on the hot read path. `null` = not yet known, same
1197
+ * "skip rather than guess" posture as `activeWorkspaceGroupId`.
1198
+ */
1199
+ personalGroupId = null;
1200
+ /**
1201
+ * Re-resolve {@link activeWorkspaceGroupId} and {@link personalGroupId}
1202
+ * from the Authority Server. Called by the resync-gates flow (http.ts's
1203
+ * `/internal/resync-gates`), never on the hot tool-call path — it makes up
1204
+ * to two AS calls (`getActiveTeamGroupId` + `getPersonalGroupId`, the
1205
+ * second already needed for both values). Leaves the previous value(s) in
1206
+ * place on any failure (network error, unexpected shape): losing a
1207
+ * known-good value on a transient blip would turn this defence-in-depth
1208
+ * check into a false refusal of every cached mandate, which is worse than
1209
+ * leaving it momentarily stale.
1181
1210
  */
1182
1211
  async refreshActiveWorkspace() {
1212
+ let activeTeam = null;
1213
+ try {
1214
+ activeTeam = await this.spClient.getActiveTeamGroupId();
1215
+ if (activeTeam) this.activeWorkspaceGroupId = activeTeam;
1216
+ } catch {
1217
+ }
1183
1218
  try {
1184
- const activeTeam = await this.spClient.getActiveTeamGroupId();
1185
- if (activeTeam) {
1186
- this.activeWorkspaceGroupId = activeTeam;
1187
- return;
1188
- }
1189
1219
  const personalId = await this.spClient.getPersonalGroupId();
1190
- if (personalId) this.activeWorkspaceGroupId = personalId;
1220
+ if (personalId) {
1221
+ this.personalGroupId = personalId;
1222
+ if (!activeTeam) this.activeWorkspaceGroupId = personalId;
1223
+ }
1191
1224
  } catch {
1192
1225
  }
1193
1226
  }
@@ -1200,6 +1233,19 @@ var AttestationCache = class {
1200
1233
  isForeignWorkspace(groupId) {
1201
1234
  return groupId !== void 0 && this.activeWorkspaceGroupId !== null && groupId !== this.activeWorkspaceGroupId;
1202
1235
  }
1236
+ /**
1237
+ * TR7 — is `groupId` a TEAM group (not the account's own Personal
1238
+ * workspace)? `undefined` ("don't know") when either `groupId` itself or
1239
+ * the account's Personal group id hasn't been learned yet — callers MUST
1240
+ * treat that the same as Personal (read enforcement has no Authority
1241
+ * Server backstop to fail closed to, unlike {@link isForeignWorkspace}'s
1242
+ * ticket-time safety net, so an unknown status must never be read as
1243
+ * "team" — that would apply a ceiling nobody has actually confirmed).
1244
+ */
1245
+ isTeamGroup(groupId) {
1246
+ if (groupId === void 0 || this.personalGroupId === null) return void 0;
1247
+ return groupId !== this.personalGroupId;
1248
+ }
1203
1249
  /**
1204
1250
  * Get the SP public key, fetching from SP if not cached or expired, then —
1205
1251
  * when a pin exists for this exact AS URL — verifying the live key still
@@ -1779,6 +1825,7 @@ var DenialLog = class {
1779
1825
  } catch (err) {
1780
1826
  console.error("[DenialLog] failed to record denial:", err);
1781
1827
  }
1828
+ void notifyControlPlane("denial-recorded");
1782
1829
  }
1783
1830
  /** Recent denials, newest first. `sinceMs` and `limit` are optional filters. */
1784
1831
  getRecent(sinceMs, limit = MAX_RECORDS) {
@@ -2190,21 +2237,168 @@ function nowSec() {
2190
2237
  return Math.floor(Date.now() / 1e3);
2191
2238
  }
2192
2239
 
2193
- // src/lib/proposal-submission-store.ts
2194
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync7, existsSync as existsSync9, renameSync as renameSync2 } from "fs";
2240
+ // src/lib/execution-result-store.ts
2241
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync7, existsSync as existsSync9, unlinkSync as unlinkSync5 } from "fs";
2195
2242
  import { homedir as homedir6 } from "os";
2196
2243
  import { dirname as dirname7, join as join9 } from "path";
2244
+ import { createCipheriv as createCipheriv5, createDecipheriv as createDecipheriv5, randomBytes as randomBytes5 } from "crypto";
2197
2245
  var DEFAULT_DIR6 = process.env.SUVEREN_DATA_DIR ?? join9(homedir6(), ".suveren");
2198
- var FILE_NAME2 = "proposal-submissions.json";
2199
2246
  var MAX_AGE_SECONDS3 = 90 * 24 * 60 * 60;
2247
+ var MAX_BYTES = 16 * 1024;
2248
+ function capResult(text, structuredContent) {
2249
+ const structJson = structuredContent ? JSON.stringify(structuredContent) : void 0;
2250
+ const textBytes = Buffer.byteLength(text, "utf8");
2251
+ const structBytes = structJson ? Buffer.byteLength(structJson, "utf8") : 0;
2252
+ if (textBytes + structBytes <= MAX_BYTES) {
2253
+ return { text, ...structuredContent ? { structuredContent } : {}, truncated: false };
2254
+ }
2255
+ if (textBytes <= MAX_BYTES) {
2256
+ return { text, truncated: true };
2257
+ }
2258
+ const cappedText = Buffer.from(text, "utf8").subarray(0, MAX_BYTES).toString("utf8");
2259
+ return { text: cappedText, truncated: true };
2260
+ }
2261
+ var ExecutionResultStore = class {
2262
+ entries = [];
2263
+ baseDir;
2264
+ vaultKey = null;
2265
+ constructor(filePath) {
2266
+ this.baseDir = filePath && filePath.endsWith(".json") ? dirname7(filePath) : filePath ?? DEFAULT_DIR6;
2267
+ this.loadPlaintext();
2268
+ }
2269
+ /** Mirrors OutcomeOutbox.setVaultKey — migrates plaintext -> encrypted once, in place. */
2270
+ setVaultKey(key) {
2271
+ this.vaultKey = key;
2272
+ if (existsSync9(this.encryptedFilePath)) {
2273
+ this.loadEncrypted();
2274
+ } else if (this.entries.length > 0) {
2275
+ this.persistEncrypted();
2276
+ if (existsSync9(this.plaintextFilePath)) {
2277
+ try {
2278
+ unlinkSync5(this.plaintextFilePath);
2279
+ } catch {
2280
+ }
2281
+ }
2282
+ }
2283
+ }
2284
+ /**
2285
+ * True when encrypted evidence exists on disk that this process cannot
2286
+ * read yet (no vault key). Callers should not report "nothing retained" in
2287
+ * this state — that would claim a hollow absence over unreadable content.
2288
+ */
2289
+ isLocked() {
2290
+ return this.vaultKey === null && existsSync9(this.encryptedFilePath);
2291
+ }
2292
+ /** Record the outcome of one execution. Replaces any prior entry for the same proposal. */
2293
+ record(input, now = nowSec2()) {
2294
+ const capped = capResult(input.text, input.structuredContent);
2295
+ this.entries = this.entries.filter((e) => e.proposalId !== input.proposalId);
2296
+ this.entries.push({
2297
+ proposalId: input.proposalId,
2298
+ ticketId: input.ticketId,
2299
+ ok: input.ok,
2300
+ text: capped.text,
2301
+ ...capped.structuredContent ? { structuredContent: capped.structuredContent } : {},
2302
+ ...capped.truncated ? { truncated: true } : {},
2303
+ recordedAt: now
2304
+ });
2305
+ this.prune(now);
2306
+ this.persist();
2307
+ }
2308
+ /** The retained outcome for a proposal, or undefined if never recorded (or pruned). */
2309
+ get(proposalId) {
2310
+ return this.entries.find((e) => e.proposalId === proposalId);
2311
+ }
2312
+ /** Diagnostic / tests. */
2313
+ get size() {
2314
+ return this.entries.length;
2315
+ }
2316
+ // ─── Retention ────────────────────────────────────────────────────────────
2317
+ prune(now) {
2318
+ const cutoff = now - MAX_AGE_SECONDS3;
2319
+ this.entries = this.entries.filter((e) => e.recordedAt >= cutoff);
2320
+ }
2321
+ // ─── Encryption (mirrors OutcomeOutbox / DenialLog) ────────────────────────
2322
+ encrypt(plaintext) {
2323
+ if (!this.vaultKey) throw new Error("No vault key");
2324
+ const iv = randomBytes5(12);
2325
+ const cipher = createCipheriv5("aes-256-gcm", this.vaultKey, iv);
2326
+ const enc = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
2327
+ return { iv: iv.toString("hex"), ciphertext: enc.toString("hex"), tag: cipher.getAuthTag().toString("hex") };
2328
+ }
2329
+ decrypt(blob) {
2330
+ if (!this.vaultKey) throw new Error("No vault key");
2331
+ const decipher = createDecipheriv5("aes-256-gcm", this.vaultKey, Buffer.from(blob.iv, "hex"));
2332
+ decipher.setAuthTag(Buffer.from(blob.tag, "hex"));
2333
+ return Buffer.concat([decipher.update(Buffer.from(blob.ciphertext, "hex")), decipher.final()]).toString("utf8");
2334
+ }
2335
+ get plaintextFilePath() {
2336
+ return join9(this.baseDir, "execution-results.json");
2337
+ }
2338
+ get encryptedFilePath() {
2339
+ return join9(this.baseDir, "execution-results.enc.json");
2340
+ }
2341
+ loadPlaintext() {
2342
+ if (!existsSync9(this.plaintextFilePath)) {
2343
+ mkdirSync7(this.baseDir, { recursive: true });
2344
+ return;
2345
+ }
2346
+ try {
2347
+ const data = JSON.parse(readFileSync10(this.plaintextFilePath, "utf-8"));
2348
+ this.entries = data.entries ?? [];
2349
+ this.prune(nowSec2());
2350
+ } catch {
2351
+ console.error(`[ExecutionResultStore] Could not parse ${this.plaintextFilePath}, starting fresh`);
2352
+ this.entries = [];
2353
+ }
2354
+ }
2355
+ loadEncrypted() {
2356
+ if (!existsSync9(this.encryptedFilePath)) return;
2357
+ try {
2358
+ const data = JSON.parse(readFileSync10(this.encryptedFilePath, "utf-8"));
2359
+ const storeFile = JSON.parse(this.decrypt(data.blob));
2360
+ this.entries = storeFile.entries ?? [];
2361
+ this.prune(nowSec2());
2362
+ } catch (err) {
2363
+ console.error(`[ExecutionResultStore] Could not decrypt ${this.encryptedFilePath}:`, err);
2364
+ this.entries = [];
2365
+ }
2366
+ }
2367
+ persist() {
2368
+ if (this.vaultKey) this.persistEncrypted();
2369
+ else this.persistPlaintext();
2370
+ }
2371
+ persistPlaintext() {
2372
+ const data = { version: 1, entries: this.entries };
2373
+ mkdirSync7(this.baseDir, { recursive: true, mode: 448 });
2374
+ writeFileSync7(this.plaintextFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
2375
+ }
2376
+ persistEncrypted() {
2377
+ const storeFile = { version: 1, entries: this.entries };
2378
+ const data = { version: 1, blob: this.encrypt(JSON.stringify(storeFile)) };
2379
+ mkdirSync7(this.baseDir, { recursive: true, mode: 448 });
2380
+ writeFileSync7(this.encryptedFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
2381
+ }
2382
+ };
2200
2383
  function nowSec2() {
2201
2384
  return Math.floor(Date.now() / 1e3);
2202
2385
  }
2386
+
2387
+ // src/lib/proposal-submission-store.ts
2388
+ import { mkdirSync as mkdirSync8, readFileSync as readFileSync11, writeFileSync as writeFileSync8, existsSync as existsSync10, renameSync as renameSync2 } from "fs";
2389
+ import { homedir as homedir7 } from "os";
2390
+ import { dirname as dirname8, join as join10 } from "path";
2391
+ var DEFAULT_DIR7 = process.env.SUVEREN_DATA_DIR ?? join10(homedir7(), ".suveren");
2392
+ var FILE_NAME2 = "proposal-submissions.json";
2393
+ var MAX_AGE_SECONDS4 = 90 * 24 * 60 * 60;
2394
+ function nowSec3() {
2395
+ return Math.floor(Date.now() / 1e3);
2396
+ }
2203
2397
  var ProposalSubmissionStore = class {
2204
2398
  filePath;
2205
2399
  constructor(filePath) {
2206
- const baseDir = filePath && filePath.endsWith(".json") ? dirname7(filePath) : filePath ?? DEFAULT_DIR6;
2207
- this.filePath = join9(baseDir, FILE_NAME2);
2400
+ const baseDir = filePath && filePath.endsWith(".json") ? dirname8(filePath) : filePath ?? DEFAULT_DIR7;
2401
+ this.filePath = join10(baseDir, FILE_NAME2);
2208
2402
  }
2209
2403
  /** Record what was submitted, right after a successful submitProposal(). */
2210
2404
  record(entry) {
@@ -2217,7 +2411,7 @@ var ProposalSubmissionStore = class {
2217
2411
  executionContextHash: hashToolArgs(entry.executionContext),
2218
2412
  authorizationId: entry.authorizationId,
2219
2413
  profileId: entry.profileId,
2220
- submittedAt: nowSec2(),
2414
+ submittedAt: nowSec3(),
2221
2415
  ...entry.previewHash ? { previewHash: entry.previewHash } : {}
2222
2416
  });
2223
2417
  this.persist(file);
@@ -2227,11 +2421,23 @@ var ProposalSubmissionStore = class {
2227
2421
  get(proposalId) {
2228
2422
  return this.load().entries.find((e) => e.proposalId === proposalId);
2229
2423
  }
2424
+ /**
2425
+ * Every submission still on record (newest-pruned by `MAX_AGE_SECONDS`
2426
+ * — see `load`). "Awaiting others" stand-in (2026-10-15, real-gateway
2427
+ * e2e): until the Authority Server has a creator-scoped proposal query,
2428
+ * this (every id THIS gateway has ever submitted) is the only local
2429
+ * source for "proposals I created" — bin/http.ts's
2430
+ * `/internal/my-pending-proposals` resolves each id's CURRENT state from
2431
+ * the AS and filters to `status: 'pending'`.
2432
+ */
2433
+ list() {
2434
+ return this.load().entries;
2435
+ }
2230
2436
  load() {
2231
- if (!existsSync9(this.filePath)) return { version: 1, entries: [] };
2437
+ if (!existsSync10(this.filePath)) return { version: 1, entries: [] };
2232
2438
  try {
2233
- const parsed = JSON.parse(readFileSync10(this.filePath, "utf-8"));
2234
- const cutoff = nowSec2() - MAX_AGE_SECONDS3;
2439
+ const parsed = JSON.parse(readFileSync11(this.filePath, "utf-8"));
2440
+ const cutoff = nowSec3() - MAX_AGE_SECONDS4;
2235
2441
  parsed.entries = (parsed.entries ?? []).filter((e) => e.submittedAt >= cutoff);
2236
2442
  return parsed;
2237
2443
  } catch {
@@ -2239,26 +2445,26 @@ var ProposalSubmissionStore = class {
2239
2445
  }
2240
2446
  }
2241
2447
  persist(file) {
2242
- mkdirSync7(dirname7(this.filePath), { recursive: true, mode: 448 });
2448
+ mkdirSync8(dirname8(this.filePath), { recursive: true, mode: 448 });
2243
2449
  const tmpPath = `${this.filePath}.tmp`;
2244
- writeFileSync7(tmpPath, JSON.stringify(file, null, 2), { encoding: "utf-8", mode: 384 });
2450
+ writeFileSync8(tmpPath, JSON.stringify(file, null, 2), { encoding: "utf-8", mode: 384 });
2245
2451
  renameSync2(tmpPath, this.filePath);
2246
2452
  }
2247
2453
  };
2248
2454
 
2249
2455
  // src/lib/preview-log.ts
2250
- import { mkdirSync as mkdirSync8, readFileSync as readFileSync11, writeFileSync as writeFileSync8, existsSync as existsSync10, renameSync as renameSync3 } from "fs";
2251
- import { homedir as homedir7 } from "os";
2252
- import { dirname as dirname8, join as join10 } from "path";
2253
- var DEFAULT_DIR7 = process.env.SUVEREN_DATA_DIR ?? join10(homedir7(), ".suveren");
2456
+ import { mkdirSync as mkdirSync9, readFileSync as readFileSync12, writeFileSync as writeFileSync9, existsSync as existsSync11, renameSync as renameSync3 } from "fs";
2457
+ import { homedir as homedir8 } from "os";
2458
+ import { dirname as dirname9, join as join11 } from "path";
2459
+ var DEFAULT_DIR8 = process.env.SUVEREN_DATA_DIR ?? join11(homedir8(), ".suveren");
2254
2460
  var FILE_NAME3 = "preview-log.json";
2255
2461
  var MAX_RECORDS2 = 500;
2256
2462
  var MAX_AGE_MS2 = 30 * 24 * 60 * 60 * 1e3;
2257
2463
  var PreviewLog = class {
2258
2464
  filePath;
2259
2465
  constructor(filePath) {
2260
- const baseDir = filePath && filePath.endsWith(".json") ? dirname8(filePath) : filePath ?? DEFAULT_DIR7;
2261
- this.filePath = join10(baseDir, FILE_NAME3);
2466
+ const baseDir = filePath && filePath.endsWith(".json") ? dirname9(filePath) : filePath ?? DEFAULT_DIR8;
2467
+ this.filePath = join11(baseDir, FILE_NAME3);
2262
2468
  }
2263
2469
  /** Record one preview read. Never throws — recording must not break the read. */
2264
2470
  record(entry) {
@@ -2280,28 +2486,28 @@ var PreviewLog = class {
2280
2486
  return [...this.load().entries];
2281
2487
  }
2282
2488
  load() {
2283
- if (!existsSync10(this.filePath)) return { version: 1, entries: [] };
2489
+ if (!existsSync11(this.filePath)) return { version: 1, entries: [] };
2284
2490
  try {
2285
- const parsed = JSON.parse(readFileSync11(this.filePath, "utf-8"));
2491
+ const parsed = JSON.parse(readFileSync12(this.filePath, "utf-8"));
2286
2492
  return { version: 1, entries: parsed.entries ?? [] };
2287
2493
  } catch {
2288
2494
  return { version: 1, entries: [] };
2289
2495
  }
2290
2496
  }
2291
2497
  persist(file) {
2292
- mkdirSync8(dirname8(this.filePath), { recursive: true, mode: 448 });
2498
+ mkdirSync9(dirname9(this.filePath), { recursive: true, mode: 448 });
2293
2499
  const tmp = `${this.filePath}.tmp`;
2294
- writeFileSync8(tmp, JSON.stringify(file, null, 2), { encoding: "utf-8", mode: 384 });
2500
+ writeFileSync9(tmp, JSON.stringify(file, null, 2), { encoding: "utf-8", mode: 384 });
2295
2501
  renameSync3(tmp, this.filePath);
2296
2502
  }
2297
2503
  };
2298
2504
 
2299
2505
  // src/lib/outcome-outbox.ts
2300
- import { mkdirSync as mkdirSync9, readFileSync as readFileSync12, writeFileSync as writeFileSync9, existsSync as existsSync11, unlinkSync as unlinkSync5 } from "fs";
2301
- import { homedir as homedir8 } from "os";
2302
- import { dirname as dirname9, join as join11 } from "path";
2303
- import { createCipheriv as createCipheriv5, createDecipheriv as createDecipheriv5, randomBytes as randomBytes5 } from "crypto";
2304
- var DEFAULT_DIR8 = process.env.SUVEREN_DATA_DIR ?? join11(homedir8(), ".suveren");
2506
+ import { mkdirSync as mkdirSync10, readFileSync as readFileSync13, writeFileSync as writeFileSync10, existsSync as existsSync12, unlinkSync as unlinkSync6 } from "fs";
2507
+ import { homedir as homedir9 } from "os";
2508
+ import { dirname as dirname10, join as join12 } from "path";
2509
+ import { createCipheriv as createCipheriv6, createDecipheriv as createDecipheriv6, randomBytes as randomBytes6 } from "crypto";
2510
+ var DEFAULT_DIR9 = process.env.SUVEREN_DATA_DIR ?? join12(homedir9(), ".suveren");
2305
2511
  var MAX_AGE_MS3 = 7 * 24 * 60 * 60 * 1e3;
2306
2512
  function outcomeEntryFor(proposal, submittedHere, who, nowMs) {
2307
2513
  if (!submittedHere) return null;
@@ -2316,19 +2522,19 @@ var OutcomeOutbox = class {
2316
2522
  baseDir;
2317
2523
  vaultKey = null;
2318
2524
  constructor(filePath) {
2319
- this.baseDir = filePath && filePath.endsWith(".json") ? dirname9(filePath) : filePath ?? DEFAULT_DIR8;
2525
+ this.baseDir = filePath && filePath.endsWith(".json") ? dirname10(filePath) : filePath ?? DEFAULT_DIR9;
2320
2526
  this.loadPlaintext();
2321
2527
  }
2322
2528
  /** Mirrors DenialLog.setVaultKey — migrates plaintext -> encrypted once, in place. */
2323
2529
  setVaultKey(key) {
2324
2530
  this.vaultKey = key;
2325
- if (existsSync11(this.encryptedFilePath)) {
2531
+ if (existsSync12(this.encryptedFilePath)) {
2326
2532
  this.loadEncrypted();
2327
2533
  } else if (this.entries.length > 0) {
2328
2534
  this.persistEncrypted();
2329
- if (existsSync11(this.plaintextFilePath)) {
2535
+ if (existsSync12(this.plaintextFilePath)) {
2330
2536
  try {
2331
- unlinkSync5(this.plaintextFilePath);
2537
+ unlinkSync6(this.plaintextFilePath);
2332
2538
  } catch {
2333
2539
  }
2334
2540
  }
@@ -2377,30 +2583,30 @@ var OutcomeOutbox = class {
2377
2583
  // ─── Encryption (mirrors DenialLog / ExecutionLog) ─────────────────────────
2378
2584
  encrypt(plaintext) {
2379
2585
  if (!this.vaultKey) throw new Error("No vault key");
2380
- const iv = randomBytes5(12);
2381
- const cipher = createCipheriv5("aes-256-gcm", this.vaultKey, iv);
2586
+ const iv = randomBytes6(12);
2587
+ const cipher = createCipheriv6("aes-256-gcm", this.vaultKey, iv);
2382
2588
  const enc = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
2383
2589
  return { iv: iv.toString("hex"), ciphertext: enc.toString("hex"), tag: cipher.getAuthTag().toString("hex") };
2384
2590
  }
2385
2591
  decrypt(blob) {
2386
2592
  if (!this.vaultKey) throw new Error("No vault key");
2387
- const decipher = createDecipheriv5("aes-256-gcm", this.vaultKey, Buffer.from(blob.iv, "hex"));
2593
+ const decipher = createDecipheriv6("aes-256-gcm", this.vaultKey, Buffer.from(blob.iv, "hex"));
2388
2594
  decipher.setAuthTag(Buffer.from(blob.tag, "hex"));
2389
2595
  return Buffer.concat([decipher.update(Buffer.from(blob.ciphertext, "hex")), decipher.final()]).toString("utf8");
2390
2596
  }
2391
2597
  get plaintextFilePath() {
2392
- return join11(this.baseDir, "outcome-outbox.json");
2598
+ return join12(this.baseDir, "outcome-outbox.json");
2393
2599
  }
2394
2600
  get encryptedFilePath() {
2395
- return join11(this.baseDir, "outcome-outbox.enc.json");
2601
+ return join12(this.baseDir, "outcome-outbox.enc.json");
2396
2602
  }
2397
2603
  loadPlaintext() {
2398
- if (!existsSync11(this.plaintextFilePath)) {
2399
- mkdirSync9(this.baseDir, { recursive: true });
2604
+ if (!existsSync12(this.plaintextFilePath)) {
2605
+ mkdirSync10(this.baseDir, { recursive: true });
2400
2606
  return;
2401
2607
  }
2402
2608
  try {
2403
- const data = JSON.parse(readFileSync12(this.plaintextFilePath, "utf-8"));
2609
+ const data = JSON.parse(readFileSync13(this.plaintextFilePath, "utf-8"));
2404
2610
  this.entries = data.entries ?? [];
2405
2611
  this.prune(Date.now());
2406
2612
  } catch {
@@ -2409,9 +2615,9 @@ var OutcomeOutbox = class {
2409
2615
  }
2410
2616
  }
2411
2617
  loadEncrypted() {
2412
- if (!existsSync11(this.encryptedFilePath)) return;
2618
+ if (!existsSync12(this.encryptedFilePath)) return;
2413
2619
  try {
2414
- const data = JSON.parse(readFileSync12(this.encryptedFilePath, "utf-8"));
2620
+ const data = JSON.parse(readFileSync13(this.encryptedFilePath, "utf-8"));
2415
2621
  const storeFile = JSON.parse(this.decrypt(data.blob));
2416
2622
  this.entries = storeFile.entries ?? [];
2417
2623
  this.prune(Date.now());
@@ -2426,14 +2632,14 @@ var OutcomeOutbox = class {
2426
2632
  }
2427
2633
  persistPlaintext() {
2428
2634
  const data = { version: 1, entries: this.entries };
2429
- mkdirSync9(this.baseDir, { recursive: true, mode: 448 });
2430
- writeFileSync9(this.plaintextFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
2635
+ mkdirSync10(this.baseDir, { recursive: true, mode: 448 });
2636
+ writeFileSync10(this.plaintextFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
2431
2637
  }
2432
2638
  persistEncrypted() {
2433
2639
  const storeFile = { version: 1, entries: this.entries };
2434
2640
  const data = { version: 1, blob: this.encrypt(JSON.stringify(storeFile)) };
2435
- mkdirSync9(this.baseDir, { recursive: true, mode: 448 });
2436
- writeFileSync9(this.encryptedFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
2641
+ mkdirSync10(this.baseDir, { recursive: true, mode: 448 });
2642
+ writeFileSync10(this.encryptedFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
2437
2643
  }
2438
2644
  };
2439
2645
  function formatOutcomeLine(e) {
@@ -2545,10 +2751,10 @@ var MCPGatekeeper = class {
2545
2751
  };
2546
2752
 
2547
2753
  // src/lib/report/report-store.ts
2548
- import { mkdirSync as mkdirSync10, readFileSync as readFileSync13, writeFileSync as writeFileSync10, existsSync as existsSync12, unlinkSync as unlinkSync6 } from "fs";
2549
- import { homedir as homedir9 } from "os";
2550
- import { dirname as dirname10, join as join12 } from "path";
2551
- import { createCipheriv as createCipheriv6, createDecipheriv as createDecipheriv6, randomBytes as randomBytes6 } from "crypto";
2754
+ import { mkdirSync as mkdirSync11, readFileSync as readFileSync14, writeFileSync as writeFileSync11, existsSync as existsSync13, unlinkSync as unlinkSync7 } from "fs";
2755
+ import { homedir as homedir10 } from "os";
2756
+ import { dirname as dirname11, join as join13 } from "path";
2757
+ import { createCipheriv as createCipheriv7, createDecipheriv as createDecipheriv7, randomBytes as randomBytes7 } from "crypto";
2552
2758
 
2553
2759
  // src/lib/report/record-resolvers.ts
2554
2760
  function findById(rows, id) {
@@ -2856,29 +3062,29 @@ async function buildCoverage(caseElements, getExport, archive, referenced, windo
2856
3062
  }
2857
3063
 
2858
3064
  // src/lib/report/report-store.ts
2859
- var DEFAULT_DIR9 = process.env.SUVEREN_DATA_DIR ?? join12(homedir9(), ".suveren");
3065
+ var DEFAULT_DIR10 = process.env.SUVEREN_DATA_DIR ?? join13(homedir10(), ".suveren");
2860
3066
  var ReportStore = class {
2861
3067
  report = null;
2862
3068
  baseDir;
2863
3069
  vaultKey = null;
2864
3070
  constructor(filePath) {
2865
3071
  if (filePath && filePath.endsWith(".json")) {
2866
- this.baseDir = dirname10(filePath);
3072
+ this.baseDir = dirname11(filePath);
2867
3073
  } else {
2868
- this.baseDir = filePath ?? DEFAULT_DIR9;
3074
+ this.baseDir = filePath ?? DEFAULT_DIR10;
2869
3075
  }
2870
3076
  this.loadPlaintext();
2871
3077
  }
2872
3078
  /** Set the vault key for encryption. Triggers migration from plaintext if needed. */
2873
3079
  setVaultKey(key) {
2874
3080
  this.vaultKey = key;
2875
- if (existsSync12(this.encryptedFilePath)) {
3081
+ if (existsSync13(this.encryptedFilePath)) {
2876
3082
  this.loadEncrypted();
2877
3083
  } else if (this.report !== null) {
2878
3084
  this.persistEncrypted();
2879
- if (existsSync12(this.plaintextFilePath)) {
3085
+ if (existsSync13(this.plaintextFilePath)) {
2880
3086
  try {
2881
- unlinkSync6(this.plaintextFilePath);
3087
+ unlinkSync7(this.plaintextFilePath);
2882
3088
  } catch {
2883
3089
  }
2884
3090
  }
@@ -2891,7 +3097,7 @@ var ReportStore = class {
2891
3097
  * "the report exists but is locked".
2892
3098
  */
2893
3099
  isLocked() {
2894
- return this.vaultKey === null && existsSync12(this.encryptedFilePath);
3100
+ return this.vaultKey === null && existsSync13(this.encryptedFilePath);
2895
3101
  }
2896
3102
  /** The stored, already-verified report — or null if none has been saved
2897
3103
  * (and the store is not locked; check `isLocked()` first). */
@@ -2925,49 +3131,49 @@ var ReportStore = class {
2925
3131
  // ─── Encryption helpers ─────────────────────────────────────────────────
2926
3132
  encrypt(plaintext) {
2927
3133
  if (!this.vaultKey) throw new Error("No vault key");
2928
- const iv = randomBytes6(12);
2929
- const cipher = createCipheriv6("aes-256-gcm", this.vaultKey, iv);
3134
+ const iv = randomBytes7(12);
3135
+ const cipher = createCipheriv7("aes-256-gcm", this.vaultKey, iv);
2930
3136
  const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
2931
3137
  const tag = cipher.getAuthTag();
2932
3138
  return { iv: iv.toString("hex"), ciphertext: encrypted.toString("hex"), tag: tag.toString("hex") };
2933
3139
  }
2934
3140
  decrypt(blob) {
2935
3141
  if (!this.vaultKey) throw new Error("No vault key");
2936
- const decipher = createDecipheriv6("aes-256-gcm", this.vaultKey, Buffer.from(blob.iv, "hex"));
3142
+ const decipher = createDecipheriv7("aes-256-gcm", this.vaultKey, Buffer.from(blob.iv, "hex"));
2937
3143
  decipher.setAuthTag(Buffer.from(blob.tag, "hex"));
2938
3144
  const decrypted = Buffer.concat([decipher.update(Buffer.from(blob.ciphertext, "hex")), decipher.final()]);
2939
3145
  return decrypted.toString("utf8");
2940
3146
  }
2941
3147
  // ─── File paths ─────────────────────────────────────────────────────────
2942
3148
  get plaintextFilePath() {
2943
- return join12(this.baseDir, "report.json");
3149
+ return join13(this.baseDir, "report.json");
2944
3150
  }
2945
3151
  get encryptedFilePath() {
2946
- return join12(this.baseDir, "report.enc.json");
3152
+ return join13(this.baseDir, "report.enc.json");
2947
3153
  }
2948
3154
  // ─── Load / Persist ─────────────────────────────────────────────────────
2949
3155
  loadPlaintext() {
2950
- if (!existsSync12(this.plaintextFilePath)) {
2951
- mkdirSync10(this.baseDir, { recursive: true });
3156
+ if (!existsSync13(this.plaintextFilePath)) {
3157
+ mkdirSync11(this.baseDir, { recursive: true });
2952
3158
  return;
2953
3159
  }
2954
3160
  try {
2955
- const raw = readFileSync13(this.plaintextFilePath, "utf-8");
3161
+ const raw = readFileSync14(this.plaintextFilePath, "utf-8");
2956
3162
  const parsed = JSON.parse(raw);
2957
3163
  this.report = parsed.report ?? null;
2958
3164
  } catch {
2959
3165
  console.error(`[ReportStore] Could not parse ${this.plaintextFilePath} \u2014 preserving as .corrupt`);
2960
3166
  try {
2961
- writeFileSync10(`${this.plaintextFilePath}.corrupt-${Date.now()}`, readFileSync13(this.plaintextFilePath));
3167
+ writeFileSync11(`${this.plaintextFilePath}.corrupt-${Date.now()}`, readFileSync14(this.plaintextFilePath));
2962
3168
  } catch {
2963
3169
  }
2964
3170
  this.report = null;
2965
3171
  }
2966
3172
  }
2967
3173
  loadEncrypted() {
2968
- if (!existsSync12(this.encryptedFilePath)) return;
3174
+ if (!existsSync13(this.encryptedFilePath)) return;
2969
3175
  try {
2970
- const raw = readFileSync13(this.encryptedFilePath, "utf-8");
3176
+ const raw = readFileSync14(this.encryptedFilePath, "utf-8");
2971
3177
  const data = JSON.parse(raw);
2972
3178
  const decrypted = this.decrypt(data.blob);
2973
3179
  const parsed = JSON.parse(decrypted);
@@ -2975,7 +3181,7 @@ var ReportStore = class {
2975
3181
  } catch (err) {
2976
3182
  console.error(`[ReportStore] Could not decrypt ${this.encryptedFilePath} \u2014 preserving as .corrupt:`, err);
2977
3183
  try {
2978
- writeFileSync10(`${this.encryptedFilePath}.corrupt-${Date.now()}`, readFileSync13(this.encryptedFilePath));
3184
+ writeFileSync11(`${this.encryptedFilePath}.corrupt-${Date.now()}`, readFileSync14(this.encryptedFilePath));
2979
3185
  } catch {
2980
3186
  }
2981
3187
  this.report = null;
@@ -2990,16 +3196,16 @@ var ReportStore = class {
2990
3196
  }
2991
3197
  persistPlaintext() {
2992
3198
  const data = { version: 1, report: this.report };
2993
- mkdirSync10(this.baseDir, { recursive: true, mode: 448 });
2994
- writeFileSync10(this.plaintextFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
3199
+ mkdirSync11(this.baseDir, { recursive: true, mode: 448 });
3200
+ writeFileSync11(this.plaintextFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
2995
3201
  }
2996
3202
  persistEncrypted() {
2997
3203
  const data = {
2998
3204
  version: 1,
2999
3205
  blob: this.encrypt(JSON.stringify({ version: 1, report: this.report }))
3000
3206
  };
3001
- mkdirSync10(this.baseDir, { recursive: true, mode: 448 });
3002
- writeFileSync10(this.encryptedFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
3207
+ mkdirSync11(this.baseDir, { recursive: true, mode: 448 });
3208
+ writeFileSync11(this.encryptedFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
3003
3209
  }
3004
3210
  };
3005
3211
 
@@ -3017,6 +3223,8 @@ var SharedState = class {
3017
3223
  reportStore;
3018
3224
  /** Which tickets this Gatekeeper has executed — the "one execution per ticket" half of exactly-once. */
3019
3225
  executionJournal;
3226
+ /** The content half of what executionJournal records — see execution-result-store.ts. */
3227
+ executionResults;
3020
3228
  /** What THIS gateway submitted for each review-mode proposal it created —
3021
3229
  * see ticket-verify.ts / commitments.ts. */
3022
3230
  proposalSubmissions;
@@ -3057,6 +3265,7 @@ var SharedState = class {
3057
3265
  this.receiptArchive = new ReceiptArchive(gateStorePath);
3058
3266
  this.reportStore = new ReportStore(gateStorePath);
3059
3267
  this.executionJournal = new ExecutionJournal(gateStorePath);
3268
+ this.executionResults = new ExecutionResultStore(gateStorePath);
3060
3269
  this.proposalSubmissions = new ProposalSubmissionStore(gateStorePath);
3061
3270
  this.previewLog = new PreviewLog(gateStorePath);
3062
3271
  this.outcomeOutbox = new OutcomeOutbox(gateStorePath);
@@ -3180,7 +3389,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3180
3389
  import { z as z2 } from "zod";
3181
3390
 
3182
3391
  // src/lib/mandate-brief.ts
3183
- import { getProfile as getProfile4 } from "@hap/core";
3392
+ import { getProfile as getProfile5 } from "@hap/core";
3184
3393
 
3185
3394
  // src/lib/consumption.ts
3186
3395
  import { boundActionTypes } from "@hap/core";
@@ -3248,29 +3457,29 @@ function formatConsumptionFull(entries) {
3248
3457
  }
3249
3458
 
3250
3459
  // src/lib/context-loader.ts
3251
- import { readFileSync as readFileSync14, existsSync as existsSync13, mkdirSync as mkdirSync11, writeFileSync as writeFileSync11, renameSync as renameSync4 } from "fs";
3252
- import { homedir as homedir10 } from "os";
3253
- import { join as join13, dirname as dirname11 } from "path";
3254
- var DEFAULT_DIR10 = process.env.SUVEREN_DATA_DIR ?? join13(homedir10(), ".suveren");
3460
+ import { readFileSync as readFileSync15, existsSync as existsSync14, mkdirSync as mkdirSync12, writeFileSync as writeFileSync12, renameSync as renameSync4 } from "fs";
3461
+ import { homedir as homedir11 } from "os";
3462
+ import { join as join14, dirname as dirname12 } from "path";
3463
+ var DEFAULT_DIR11 = process.env.SUVEREN_DATA_DIR ?? join14(homedir11(), ".suveren");
3255
3464
  var CONTEXT_MAX_BYTES = 16 * 1024;
3256
3465
  function contextFilePath(dataDir2) {
3257
- return join13(dataDir2 ?? DEFAULT_DIR10, "context.md");
3466
+ return join14(dataDir2 ?? DEFAULT_DIR11, "context.md");
3258
3467
  }
3259
3468
  function writeContextFile(content, dataDir2) {
3260
3469
  const bytes = Buffer.byteLength(content, "utf-8");
3261
3470
  if (bytes > CONTEXT_MAX_BYTES) throw new Error(`the brief is ${bytes} bytes; the limit is ${CONTEXT_MAX_BYTES}.`);
3262
3471
  const filePath = contextFilePath(dataDir2);
3263
- mkdirSync11(dirname11(filePath), { recursive: true });
3472
+ mkdirSync12(dirname12(filePath), { recursive: true });
3264
3473
  const tmpPath = `${filePath}.tmp`;
3265
- writeFileSync11(tmpPath, content, "utf-8");
3474
+ writeFileSync12(tmpPath, content, "utf-8");
3266
3475
  renameSync4(tmpPath, filePath);
3267
3476
  }
3268
3477
  var BRIEF_MAX_CHARS = 16 * 1024;
3269
3478
  function readContextFile(dataDir2) {
3270
3479
  const filePath = contextFilePath(dataDir2);
3271
- if (!existsSync13(filePath)) return null;
3480
+ if (!existsSync14(filePath)) return null;
3272
3481
  try {
3273
- const content = readFileSync14(filePath, "utf-8").trim();
3482
+ const content = readFileSync15(filePath, "utf-8").trim();
3274
3483
  return content || null;
3275
3484
  } catch {
3276
3485
  return null;
@@ -3463,18 +3672,239 @@ function attachTicketId(tool, args, ticketId) {
3463
3672
  return { ...args, ticket_id: ticketId };
3464
3673
  }
3465
3674
 
3466
- // src/lib/preview.ts
3467
- import { computeContentHash as computeContentHash2 } from "@hap/core";
3468
-
3469
- // src/lib/manifest-loader.ts
3470
- import { readFileSync as readFileSync15, existsSync as existsSync14 } from "fs";
3471
- import { join as join14, resolve } from "path";
3472
-
3473
- // src/lib/tool-gating-types.ts
3474
- var MAX_PREVIEW_READS = 10;
3475
- var MAX_FOREACH_ELEMENTS = 50;
3476
- var MAX_RECORD_KEY_STEPS = 2;
3477
-
3675
+ // src/lib/preview.ts
3676
+ import { computeContentHash as computeContentHash2 } from "@hap/core";
3677
+
3678
+ // src/lib/manifest-loader.ts
3679
+ import { readFileSync as readFileSync16, existsSync as existsSync15 } from "fs";
3680
+ import { join as join15, resolve } from "path";
3681
+ import { getProfile as getProfile2 } from "@hap/core";
3682
+
3683
+ // src/lib/tool-gating-types.ts
3684
+ var MAX_PREVIEW_READS = 10;
3685
+ var MAX_FOREACH_ELEMENTS = 50;
3686
+ var MAX_RECORD_KEY_STEPS = 2;
3687
+
3688
+ // src/lib/scope-specificity.ts
3689
+ function tokenSet(value) {
3690
+ if (value === void 0 || value === null) return /* @__PURE__ */ new Set();
3691
+ return new Set(
3692
+ String(value).split(",").map((s) => s.trim().toLowerCase()).filter(Boolean)
3693
+ );
3694
+ }
3695
+ function subsetOrEqual(a, b) {
3696
+ if (b.size === 0) return true;
3697
+ if (a.size === 0) return false;
3698
+ for (const x of a) if (!b.has(x)) return false;
3699
+ return true;
3700
+ }
3701
+ function atLeastAsSpecific(keys, a, b) {
3702
+ for (const k of keys) {
3703
+ if (!subsetOrEqual(tokenSet(a[k]), tokenSet(b[k]))) return false;
3704
+ }
3705
+ return true;
3706
+ }
3707
+ function strictlyMoreSpecific(keys, a, b) {
3708
+ return atLeastAsSpecific(keys, a, b) && !atLeastAsSpecific(keys, b, a);
3709
+ }
3710
+ function selectAuthorization(contextKeys, passers) {
3711
+ if (passers.length === 1) {
3712
+ return { chosen: passers[0], superseded: [], reason: "sole" };
3713
+ }
3714
+ const dominators = passers.filter(
3715
+ (a) => passers.every((b) => b === a || strictlyMoreSpecific(contextKeys, a.context, b.context))
3716
+ );
3717
+ if (dominators.length === 1) {
3718
+ const chosen2 = dominators[0];
3719
+ return { chosen: chosen2, superseded: passers.filter((p) => p !== chosen2), reason: "most-specific" };
3720
+ }
3721
+ const approvers = passers.filter((p) => p.requiresApproval);
3722
+ const pool = approvers.length > 0 ? approvers : passers;
3723
+ const chosen = pool.slice().sort((a, b) => a.id.localeCompare(b.id))[0];
3724
+ return {
3725
+ chosen,
3726
+ superseded: passers.filter((p) => p !== chosen),
3727
+ reason: approvers.length > 0 ? "fail-safe-approval" : "fail-safe-tiebreak"
3728
+ };
3729
+ }
3730
+
3731
+ // src/lib/read-gate.ts
3732
+ function boundsSatisfyReadGate(bounds, gate) {
3733
+ if (!gate.boundField) return true;
3734
+ const actual = bounds?.[gate.boundField];
3735
+ return actual !== void 0 && String(actual) === gate.requiredValue;
3736
+ }
3737
+ function readToolIsGoverned(g) {
3738
+ if (!g) return false;
3739
+ return Boolean(g.boundField) || g.read != null || g.readGovernance === "none";
3740
+ }
3741
+ function allowedResources(contexts, boundField) {
3742
+ const out = /* @__PURE__ */ new Set();
3743
+ for (const ctx of contexts) {
3744
+ for (const v of tokenSet(ctx?.[boundField])) out.add(v);
3745
+ }
3746
+ return out;
3747
+ }
3748
+ function deniedResources(requested, allowed) {
3749
+ return requested.filter((r) => !allowed.has(r));
3750
+ }
3751
+ function filterItemsByResource(items, idPath, allowed) {
3752
+ return items.filter((it) => {
3753
+ const id = getByDottedPath(it, idPath);
3754
+ return typeof id === "string" && allowed.has(id);
3755
+ });
3756
+ }
3757
+ function hasBlockedValue(item, valuesPath, blocked) {
3758
+ const v = getByDottedPath(item, valuesPath);
3759
+ const arr = Array.isArray(v) ? v : v != null ? [v] : [];
3760
+ return arr.some((x) => typeof x === "string" && blocked.has(x));
3761
+ }
3762
+ var MS_PER_DAY = 24 * 60 * 60 * 1e3;
3763
+ function parseMessageTimestamp(value) {
3764
+ if (typeof value === "number" && Number.isFinite(value)) return value;
3765
+ if (typeof value === "string" && value.trim() !== "") {
3766
+ const n = Number(value);
3767
+ if (Number.isFinite(n)) return n;
3768
+ const iso = Date.parse(value);
3769
+ if (Number.isFinite(iso)) return iso;
3770
+ }
3771
+ return null;
3772
+ }
3773
+ function ageInDays(timestampMs, nowMs) {
3774
+ return Math.floor((nowMs - timestampMs) / MS_PER_DAY);
3775
+ }
3776
+ function maxReadAgeDays(boundsList, field) {
3777
+ let max = null;
3778
+ for (const b of boundsList) {
3779
+ const raw = b?.[field];
3780
+ const n = typeof raw === "number" ? raw : raw !== void 0 ? Number(raw) : NaN;
3781
+ if (Number.isFinite(n)) max = max === null ? n : Math.max(max, n);
3782
+ }
3783
+ return max;
3784
+ }
3785
+ function isOlderThanMaxAge(timestampMs, maxAgeDays, nowMs) {
3786
+ if (timestampMs === null) return true;
3787
+ return ageInDays(timestampMs, nowMs) > maxAgeDays;
3788
+ }
3789
+ var TRAILING_OPERATOR = /(?:^|\s)(?:OR|AND|NOT)$|[-+|&:]$/i;
3790
+ function parensBalanced(s) {
3791
+ let depth = 0;
3792
+ let inQuote = false;
3793
+ for (const c of s) {
3794
+ if (c === '"') {
3795
+ inQuote = !inQuote;
3796
+ continue;
3797
+ }
3798
+ if (inQuote) continue;
3799
+ if (c === "(") depth++;
3800
+ else if (c === ")" && --depth < 0) return false;
3801
+ }
3802
+ return !inQuote && depth === 0;
3803
+ }
3804
+ function composeReadQuery(agentQuery, clauses) {
3805
+ const injected = clauses.filter((c) => typeof c === "string" && c.trim() !== "").map((c) => c.trim());
3806
+ const base = (agentQuery ?? "").trim();
3807
+ if (base === "") return { ok: true, query: injected.join(" ") };
3808
+ if (!parensBalanced(base)) {
3809
+ return { ok: false, reason: "the requested search query has unbalanced parentheses" };
3810
+ }
3811
+ if (TRAILING_OPERATOR.test(base)) {
3812
+ return { ok: false, reason: "the requested search query ends in a dangling operator" };
3813
+ }
3814
+ if (injected.length === 0) return { ok: true, query: base };
3815
+ return { ok: true, query: `(${base}) ${injected.join(" ")}` };
3816
+ }
3817
+ function detectAgeConflict(agentQuery, conflictPattern, maxAgeDays) {
3818
+ if (!agentQuery || !conflictPattern || maxAgeDays === null) return null;
3819
+ let re;
3820
+ try {
3821
+ re = new RegExp(conflictPattern, "gi");
3822
+ } catch {
3823
+ return null;
3824
+ }
3825
+ let worst = null;
3826
+ for (const match of agentQuery.matchAll(re)) {
3827
+ const days = Number(match[1]);
3828
+ if (!Number.isFinite(days)) continue;
3829
+ if (days >= maxAgeDays && (worst === null || days > worst)) worst = days;
3830
+ }
3831
+ return worst;
3832
+ }
3833
+ function resolveAgeBoundField(boundsSchema, ageExecField) {
3834
+ const fields = boundsSchema?.fields;
3835
+ if (!fields) return null;
3836
+ for (const [name, def] of Object.entries(fields)) {
3837
+ if (def?.boundType?.kind === "per_transaction" && def.boundType.of === ageExecField) return name;
3838
+ }
3839
+ return null;
3840
+ }
3841
+ function classifyEnumSwitches(boundsSchema) {
3842
+ const readSide = [];
3843
+ const writeSide = [];
3844
+ for (const [name, def] of Object.entries(boundsSchema?.fields ?? {})) {
3845
+ if (def?.boundType?.kind !== "enum") continue;
3846
+ const values = new Set(def.boundType.values ?? []);
3847
+ if (!values.has("none")) continue;
3848
+ if (values.size === 2 && values.has("unlimited")) readSide.push(name);
3849
+ else writeSide.push(name);
3850
+ }
3851
+ return { readSide, writeSide };
3852
+ }
3853
+ function uncoveredReadSwitchReason(overrides, boundsSchema) {
3854
+ const { readSide, writeSide } = classifyEnumSwitches(boundsSchema);
3855
+ const anySwitch = [...readSide, ...writeSide];
3856
+ if (anySwitch.length === 0) return null;
3857
+ const valuesOf = (field) => boundsSchema?.fields?.[field]?.boundType?.values ?? [];
3858
+ for (const [toolName, entry] of Object.entries(overrides)) {
3859
+ if (entry !== null && typeof entry !== "object") continue;
3860
+ const e = entry ?? {};
3861
+ if (entry !== null && e.category !== "read") continue;
3862
+ if (e.readGovernance === "none") {
3863
+ if (!e.readGovernanceReason) {
3864
+ return `override "${toolName}" declares readGovernance:"none" with no readGovernanceReason \u2014 an exemption from this profile's switch(es) [${anySwitch.join(", ")}] needs a written reason`;
3865
+ }
3866
+ continue;
3867
+ }
3868
+ if (!e.boundField || !anySwitch.includes(e.boundField)) {
3869
+ return `override "${toolName}" is category:"read" under a profile that declares enum switch(es) [${anySwitch.join(", ")}] (read-side: [${readSide.join(", ")}], write-side: [${writeSide.join(", ")}]), but does not reference one via boundField (and carries no readGovernance exemption) \u2014 it would silently ignore e.g. "${anySwitch[0]}: none"`;
3870
+ }
3871
+ const positiveValues = valuesOf(e.boundField).filter((v) => v !== "none");
3872
+ if (!e.requiredValue || !positiveValues.includes(e.requiredValue)) {
3873
+ return `override "${toolName}".requiredValue for boundField "${e.boundField}" must be ${positiveValues.map((v) => `"${v}"`).join(" or ")}, not ${JSON.stringify(e.requiredValue)} \u2014 requiring "none" (or an unlisted value) would invert the gate`;
3874
+ }
3875
+ }
3876
+ return null;
3877
+ }
3878
+ function getByDottedPath(obj, path) {
3879
+ return path.split(".").reduce(
3880
+ (o, k) => o == null ? void 0 : o[k],
3881
+ obj
3882
+ );
3883
+ }
3884
+ function firstParsableDate(obj, paths) {
3885
+ for (const path of Array.isArray(paths) ? paths : [paths]) {
3886
+ const parsed = parseMessageTimestamp(getByDottedPath(obj, path));
3887
+ if (parsed !== null) return parsed;
3888
+ }
3889
+ return null;
3890
+ }
3891
+ function renderAgeConstraint(template, maxAgeDays, nowMs) {
3892
+ const floorIso = new Date(nowMs - maxAgeDays * MS_PER_DAY).toISOString();
3893
+ return template.replace(/\{days\}/g, String(maxAgeDays)).replace(/\{date\}/g, floorIso.slice(0, 10));
3894
+ }
3895
+ function ageFloorValue(maxAgeDays, nowMs, format = "iso") {
3896
+ const floorMs = nowMs - maxAgeDays * MS_PER_DAY;
3897
+ if (format === "epoch_ms") return floorMs;
3898
+ if (format === "epoch_s") return String(Math.floor(floorMs / 1e3));
3899
+ return new Date(floorMs).toISOString();
3900
+ }
3901
+ function clampAgeFloor(requested, maxAgeDays, nowMs, format = "iso") {
3902
+ const floorMs = nowMs - maxAgeDays * MS_PER_DAY;
3903
+ const asked = parseMessageTimestamp(requested);
3904
+ if (asked !== null && asked >= floorMs) return requested;
3905
+ return ageFloorValue(maxAgeDays, nowMs, format);
3906
+ }
3907
+
3478
3908
  // src/lib/manifest-loader.ts
3479
3909
  var EXACT_SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
3480
3910
  function isExactSemver(value) {
@@ -3823,28 +4253,34 @@ function invalidManifestContextReason(manifest) {
3823
4253
  }
3824
4254
  return null;
3825
4255
  }
4256
+ function invalidManifestReadGateReason(manifest) {
4257
+ const profileDef = getProfile2(manifest.profile);
4258
+ if (!profileDef) return null;
4259
+ const overrides = manifest.toolGating?.overrides ?? {};
4260
+ return uncoveredReadSwitchReason(overrides, profileDef.boundsSchema);
4261
+ }
3826
4262
  var manifests = /* @__PURE__ */ new Map();
3827
4263
  function loadManifests(integrationsDir) {
3828
4264
  const dir = resolve(
3829
- integrationsDir ?? process.env.SUVEREN_MANIFESTS_DIR ?? join14(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "content", "integrations")
4265
+ integrationsDir ?? process.env.SUVEREN_MANIFESTS_DIR ?? join15(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "content", "integrations")
3830
4266
  );
3831
- const indexPath = join14(dir, "index.json");
3832
- if (!existsSync14(indexPath)) {
4267
+ const indexPath = join15(dir, "index.json");
4268
+ if (!existsSync15(indexPath)) {
3833
4269
  console.error(`[ManifestLoader] No index.json found at ${indexPath}, skipping manifest loading`);
3834
4270
  return 0;
3835
4271
  }
3836
4272
  let index;
3837
4273
  try {
3838
- index = JSON.parse(readFileSync15(indexPath, "utf-8"));
4274
+ index = JSON.parse(readFileSync16(indexPath, "utf-8"));
3839
4275
  } catch (err) {
3840
4276
  console.error(`[ManifestLoader] Failed to parse ${indexPath}:`, err);
3841
4277
  return 0;
3842
4278
  }
3843
4279
  let loaded = 0;
3844
4280
  for (const [id, relativePath] of Object.entries(index.integrations)) {
3845
- const manifestPath = join14(dir, relativePath);
4281
+ const manifestPath = join15(dir, relativePath);
3846
4282
  try {
3847
- const manifest = JSON.parse(readFileSync15(manifestPath, "utf-8"));
4283
+ const manifest = JSON.parse(readFileSync16(manifestPath, "utf-8"));
3848
4284
  const pinReason = invalidNpmPinReason(manifest);
3849
4285
  if (pinReason) {
3850
4286
  console.error(`[ManifestLoader] Refusing manifest ${id} (${manifestPath}): ${pinReason}`);
@@ -3860,6 +4296,11 @@ function loadManifests(integrationsDir) {
3860
4296
  console.error(`[ManifestLoader] Refusing manifest ${id} (${manifestPath}): ${contextReason}`);
3861
4297
  continue;
3862
4298
  }
4299
+ const readGateReason = invalidManifestReadGateReason(manifest);
4300
+ if (readGateReason) {
4301
+ console.error(`[ManifestLoader] Refusing manifest ${id} (${manifestPath}): ${readGateReason}`);
4302
+ continue;
4303
+ }
3863
4304
  manifests.set(id, manifest);
3864
4305
  loaded++;
3865
4306
  } catch (err) {
@@ -4144,189 +4585,6 @@ async function buildInternalPreview(integrationManager2, integrationId, toolName
4144
4585
  });
4145
4586
  }
4146
4587
 
4147
- // src/lib/scope-specificity.ts
4148
- function tokenSet(value) {
4149
- if (value === void 0 || value === null) return /* @__PURE__ */ new Set();
4150
- return new Set(
4151
- String(value).split(",").map((s) => s.trim().toLowerCase()).filter(Boolean)
4152
- );
4153
- }
4154
- function subsetOrEqual(a, b) {
4155
- if (b.size === 0) return true;
4156
- if (a.size === 0) return false;
4157
- for (const x of a) if (!b.has(x)) return false;
4158
- return true;
4159
- }
4160
- function atLeastAsSpecific(keys, a, b) {
4161
- for (const k of keys) {
4162
- if (!subsetOrEqual(tokenSet(a[k]), tokenSet(b[k]))) return false;
4163
- }
4164
- return true;
4165
- }
4166
- function strictlyMoreSpecific(keys, a, b) {
4167
- return atLeastAsSpecific(keys, a, b) && !atLeastAsSpecific(keys, b, a);
4168
- }
4169
- function selectAuthorization(contextKeys, passers) {
4170
- if (passers.length === 1) {
4171
- return { chosen: passers[0], superseded: [], reason: "sole" };
4172
- }
4173
- const dominators = passers.filter(
4174
- (a) => passers.every((b) => b === a || strictlyMoreSpecific(contextKeys, a.context, b.context))
4175
- );
4176
- if (dominators.length === 1) {
4177
- const chosen2 = dominators[0];
4178
- return { chosen: chosen2, superseded: passers.filter((p) => p !== chosen2), reason: "most-specific" };
4179
- }
4180
- const approvers = passers.filter((p) => p.requiresApproval);
4181
- const pool = approvers.length > 0 ? approvers : passers;
4182
- const chosen = pool.slice().sort((a, b) => a.id.localeCompare(b.id))[0];
4183
- return {
4184
- chosen,
4185
- superseded: passers.filter((p) => p !== chosen),
4186
- reason: approvers.length > 0 ? "fail-safe-approval" : "fail-safe-tiebreak"
4187
- };
4188
- }
4189
-
4190
- // src/lib/read-gate.ts
4191
- function boundsSatisfyReadGate(bounds, gate) {
4192
- if (!gate.boundField) return true;
4193
- const actual = bounds?.[gate.boundField];
4194
- return actual !== void 0 && String(actual) === gate.requiredValue;
4195
- }
4196
- function readToolIsGoverned(g) {
4197
- if (!g) return false;
4198
- return Boolean(g.boundField) || g.read != null || g.readGovernance === "none";
4199
- }
4200
- function allowedResources(contexts, boundField) {
4201
- const out = /* @__PURE__ */ new Set();
4202
- for (const ctx of contexts) {
4203
- for (const v of tokenSet(ctx?.[boundField])) out.add(v);
4204
- }
4205
- return out;
4206
- }
4207
- function deniedResources(requested, allowed) {
4208
- return requested.filter((r) => !allowed.has(r));
4209
- }
4210
- function filterItemsByResource(items, idPath, allowed) {
4211
- return items.filter((it) => {
4212
- const id = getByDottedPath(it, idPath);
4213
- return typeof id === "string" && allowed.has(id);
4214
- });
4215
- }
4216
- function hasBlockedValue(item, valuesPath, blocked) {
4217
- const v = getByDottedPath(item, valuesPath);
4218
- const arr = Array.isArray(v) ? v : v != null ? [v] : [];
4219
- return arr.some((x) => typeof x === "string" && blocked.has(x));
4220
- }
4221
- var MS_PER_DAY = 24 * 60 * 60 * 1e3;
4222
- function parseMessageTimestamp(value) {
4223
- if (typeof value === "number" && Number.isFinite(value)) return value;
4224
- if (typeof value === "string" && value.trim() !== "") {
4225
- const n = Number(value);
4226
- if (Number.isFinite(n)) return n;
4227
- const iso = Date.parse(value);
4228
- if (Number.isFinite(iso)) return iso;
4229
- }
4230
- return null;
4231
- }
4232
- function ageInDays(timestampMs, nowMs) {
4233
- return Math.floor((nowMs - timestampMs) / MS_PER_DAY);
4234
- }
4235
- function maxReadAgeDays(boundsList, field) {
4236
- let max = null;
4237
- for (const b of boundsList) {
4238
- const raw = b?.[field];
4239
- const n = typeof raw === "number" ? raw : raw !== void 0 ? Number(raw) : NaN;
4240
- if (Number.isFinite(n)) max = max === null ? n : Math.max(max, n);
4241
- }
4242
- return max;
4243
- }
4244
- function isOlderThanMaxAge(timestampMs, maxAgeDays, nowMs) {
4245
- if (timestampMs === null) return true;
4246
- return ageInDays(timestampMs, nowMs) > maxAgeDays;
4247
- }
4248
- var TRAILING_OPERATOR = /(?:^|\s)(?:OR|AND|NOT)$|[-+|&:]$/i;
4249
- function parensBalanced(s) {
4250
- let depth = 0;
4251
- let inQuote = false;
4252
- for (const c of s) {
4253
- if (c === '"') {
4254
- inQuote = !inQuote;
4255
- continue;
4256
- }
4257
- if (inQuote) continue;
4258
- if (c === "(") depth++;
4259
- else if (c === ")" && --depth < 0) return false;
4260
- }
4261
- return !inQuote && depth === 0;
4262
- }
4263
- function composeReadQuery(agentQuery, clauses) {
4264
- const injected = clauses.filter((c) => typeof c === "string" && c.trim() !== "").map((c) => c.trim());
4265
- const base = (agentQuery ?? "").trim();
4266
- if (base === "") return { ok: true, query: injected.join(" ") };
4267
- if (!parensBalanced(base)) {
4268
- return { ok: false, reason: "the requested search query has unbalanced parentheses" };
4269
- }
4270
- if (TRAILING_OPERATOR.test(base)) {
4271
- return { ok: false, reason: "the requested search query ends in a dangling operator" };
4272
- }
4273
- if (injected.length === 0) return { ok: true, query: base };
4274
- return { ok: true, query: `(${base}) ${injected.join(" ")}` };
4275
- }
4276
- function detectAgeConflict(agentQuery, conflictPattern, maxAgeDays) {
4277
- if (!agentQuery || !conflictPattern || maxAgeDays === null) return null;
4278
- let re;
4279
- try {
4280
- re = new RegExp(conflictPattern, "gi");
4281
- } catch {
4282
- return null;
4283
- }
4284
- let worst = null;
4285
- for (const match of agentQuery.matchAll(re)) {
4286
- const days = Number(match[1]);
4287
- if (!Number.isFinite(days)) continue;
4288
- if (days >= maxAgeDays && (worst === null || days > worst)) worst = days;
4289
- }
4290
- return worst;
4291
- }
4292
- function resolveAgeBoundField(boundsSchema, ageExecField) {
4293
- const fields = boundsSchema?.fields;
4294
- if (!fields) return null;
4295
- for (const [name, def] of Object.entries(fields)) {
4296
- if (def?.boundType?.kind === "per_transaction" && def.boundType.of === ageExecField) return name;
4297
- }
4298
- return null;
4299
- }
4300
- function getByDottedPath(obj, path) {
4301
- return path.split(".").reduce(
4302
- (o, k) => o == null ? void 0 : o[k],
4303
- obj
4304
- );
4305
- }
4306
- function firstParsableDate(obj, paths) {
4307
- for (const path of Array.isArray(paths) ? paths : [paths]) {
4308
- const parsed = parseMessageTimestamp(getByDottedPath(obj, path));
4309
- if (parsed !== null) return parsed;
4310
- }
4311
- return null;
4312
- }
4313
- function renderAgeConstraint(template, maxAgeDays, nowMs) {
4314
- const floorIso = new Date(nowMs - maxAgeDays * MS_PER_DAY).toISOString();
4315
- return template.replace(/\{days\}/g, String(maxAgeDays)).replace(/\{date\}/g, floorIso.slice(0, 10));
4316
- }
4317
- function ageFloorValue(maxAgeDays, nowMs, format = "iso") {
4318
- const floorMs = nowMs - maxAgeDays * MS_PER_DAY;
4319
- if (format === "epoch_ms") return floorMs;
4320
- if (format === "epoch_s") return String(Math.floor(floorMs / 1e3));
4321
- return new Date(floorMs).toISOString();
4322
- }
4323
- function clampAgeFloor(requested, maxAgeDays, nowMs, format = "iso") {
4324
- const floorMs = nowMs - maxAgeDays * MS_PER_DAY;
4325
- const asked = parseMessageTimestamp(requested);
4326
- if (asked !== null && asked >= floorMs) return requested;
4327
- return ageFloorValue(maxAgeDays, nowMs, format);
4328
- }
4329
-
4330
4588
  // src/lib/field-path.ts
4331
4589
  function matchesWhere(element, where) {
4332
4590
  if (element === null || typeof element !== "object" || Array.isArray(element)) return false;
@@ -4811,8 +5069,8 @@ async function verifyTicket(cache, receipt, expected) {
4811
5069
  if (typeof timestamp !== "number" || !Number.isFinite(timestamp)) {
4812
5070
  throw new TicketBindingMismatchError("Ticket carries no valid timestamp \u2014 refusing to execute.");
4813
5071
  }
4814
- const nowSec3 = Math.floor(Date.now() / 1e3);
4815
- const ageSec = nowSec3 - timestamp;
5072
+ const nowSec4 = Math.floor(Date.now() / 1e3);
5073
+ const ageSec = nowSec4 - timestamp;
4816
5074
  if (ageSec > TICKET_MAX_AGE_SECONDS) {
4817
5075
  throw new TicketBindingMismatchError(
4818
5076
  `Ticket is ${ageSec}s old, older than the ${TICKET_MAX_AGE_SECONDS}s freshness window \u2014 refusing to execute.`
@@ -4947,7 +5205,7 @@ function normalizeIncomingArgs(tool, args) {
4947
5205
  }
4948
5206
 
4949
5207
  // src/lib/numeric-execution.ts
4950
- import { getProfile as getProfile2 } from "@hap/core";
5208
+ import { getProfile as getProfile3 } from "@hap/core";
4951
5209
  function parseNumericString(value) {
4952
5210
  if (!/^-?\d+(\.\d+)?$/.test(value)) return void 0;
4953
5211
  const parsed = Number(value);
@@ -4963,7 +5221,7 @@ function declaredNumericFields(profile) {
4963
5221
  return out;
4964
5222
  }
4965
5223
  function coerceDeclaredNumericFields(profileId, execution) {
4966
- const fields = declaredNumericFields(getProfile2(profileId));
5224
+ const fields = declaredNumericFields(getProfile3(profileId));
4967
5225
  for (const field of fields) {
4968
5226
  const value = execution[field];
4969
5227
  if (typeof value !== "string") continue;
@@ -4973,7 +5231,7 @@ function coerceDeclaredNumericFields(profileId, execution) {
4973
5231
  }
4974
5232
 
4975
5233
  // src/lib/tool-proxy.ts
4976
- import { getProfile as getProfile3, ContentBindingError, boundActionTypes as boundActionTypes2 } from "@hap/core";
5234
+ import { getProfile as getProfile4, ContentBindingError, boundActionTypes as boundActionTypes2 } from "@hap/core";
4977
5235
  import { readFile } from "fs/promises";
4978
5236
  import { extname } from "path";
4979
5237
  import { randomUUID } from "crypto";
@@ -5045,7 +5303,7 @@ function profileMatches(profileId, shortName2) {
5045
5303
  return profileId === shortName2 || profileId.includes("/" + shortName2 + "@") || profileId.endsWith("/" + shortName2);
5046
5304
  }
5047
5305
  function wireExecutionContext(profileId, execution) {
5048
- const profile = getProfile3(profileId);
5306
+ const profile = getProfile4(profileId);
5049
5307
  const allowed = /* @__PURE__ */ new Set(["action_type"]);
5050
5308
  const fields = profile?.boundsSchema?.fields;
5051
5309
  if (fields) {
@@ -5063,7 +5321,7 @@ function wireExecutionContext(profileId, execution) {
5063
5321
  return out;
5064
5322
  }
5065
5323
  function countBoundsFor(profileId, actionType) {
5066
- const fields = getProfile3(profileId)?.boundsSchema?.fields;
5324
+ const fields = getProfile4(profileId)?.boundsSchema?.fields;
5067
5325
  if (!fields) return null;
5068
5326
  return Object.entries(fields).filter(([, def]) => def.boundType?.kind === "cumulative_count").filter(([name, def]) => {
5069
5327
  const applies = boundActionTypes2(name, def);
@@ -5138,7 +5396,7 @@ function recordBlocked(state2, tool, rec) {
5138
5396
  function recordGatekeeperRefusal(state2, tool, refusals) {
5139
5397
  if (refusals.length === 0) return;
5140
5398
  const pick = refusals.find((r) => r.bound !== void 0 && r.actual !== void 0) ?? refusals[0];
5141
- const scoped = pick.field ? Boolean(getProfile3(pick.profileId)?.scopeSchema?.fields?.[pick.field]) : false;
5399
+ const scoped = pick.field ? Boolean(getProfile4(pick.profileId)?.scopeSchema?.fields?.[pick.field]) : false;
5142
5400
  const asValueOrString = (v) => typeof v === "number" || typeof v === "string" ? v : void 0;
5143
5401
  recordBlocked(state2, tool, {
5144
5402
  kind: scoped ? "scope" : "bound",
@@ -5238,7 +5496,7 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
5238
5496
  };
5239
5497
  };
5240
5498
  }
5241
- const { profile, executionMapping, staticExecution, category } = tool.gating;
5499
+ const { profile, executionMapping, staticExecution, category, boundField: toolBoundField, requiredValue: toolRequiredValue } = tool.gating;
5242
5500
  const eligibleMandates = () => {
5243
5501
  const refusals = [];
5244
5502
  const matching = state2.getEnrichedAuthorizations().filter((a) => a.complete && profileMatches(a.profileId, profile)).filter((a) => {
@@ -5317,17 +5575,19 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
5317
5575
  );
5318
5576
  }
5319
5577
  }
5320
- const readProfile = readAdapter ? getProfile3(matchingAuths[0].profileId) : void 0;
5578
+ const readProfile = readAdapter ? getProfile4(matchingAuths[0].profileId) : void 0;
5321
5579
  const boundsSchema = readProfile?.boundsSchema;
5322
5580
  const ageBoundField = readAdapter?.ageField ? resolveAgeBoundField(boundsSchema, readAdapter.ageField) : null;
5323
5581
  const hasAgeDimension = Boolean(readAdapter?.ageField);
5324
5582
  let readMaxAge = null;
5325
5583
  if (hasAgeDimension) {
5326
5584
  const localAge = integrationManager2.getReadAgeDays(tool.integrationId);
5327
- readMaxAge = localAge ?? (ageBoundField ? maxReadAgeDays(
5585
+ const signedAge = ageBoundField ? maxReadAgeDays(
5328
5586
  matchingAuths.map((a) => a.bounds ?? a.frame),
5329
5587
  ageBoundField
5330
- ) : null);
5588
+ ) : null;
5589
+ const isTeam = typeof state2.cache?.isTeamGroup === "function" ? state2.cache.isTeamGroup(matchingAuths[0]?.groupId) : void 0;
5590
+ readMaxAge = isTeam === true && signedAge !== null ? localAge !== null ? Math.min(localAge, signedAge) : signedAge : localAge ?? signedAge;
5331
5591
  if (readMaxAge === null) {
5332
5592
  return denyRead(
5333
5593
  state2,
@@ -5481,7 +5741,7 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
5481
5741
  };
5482
5742
  }
5483
5743
  {
5484
- const profileDef = getProfile3(profile);
5744
+ const profileDef = getProfile4(profile);
5485
5745
  const registry = profileDef?.boundsSchema?.actionTypes;
5486
5746
  if (Array.isArray(registry) && registry.length > 0 && !registry.includes(declaredActionType)) {
5487
5747
  recordBlocked(state2, tool, {
@@ -5519,6 +5779,25 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
5519
5779
  const passers = [];
5520
5780
  const structuredRefusals = [];
5521
5781
  for (const candidate of matchingAuths) {
5782
+ if (toolBoundField) {
5783
+ const gateOk = boundsSatisfyReadGate(
5784
+ candidate.bounds ?? candidate.frame,
5785
+ { boundField: toolBoundField, requiredValue: toolRequiredValue }
5786
+ );
5787
+ if (!gateOk) {
5788
+ errors.push(
5789
+ `${candidate.path}: requires "${toolBoundField}: ${toolRequiredValue}", but this authorization does not grant it`
5790
+ );
5791
+ structuredRefusals.push({
5792
+ authorizationId: candidate.authorizationId,
5793
+ profileId: candidate.profileId,
5794
+ code: "BOUND_EXCEEDED",
5795
+ field: toolBoundField,
5796
+ bound: toolRequiredValue
5797
+ });
5798
+ continue;
5799
+ }
5800
+ }
5522
5801
  const { result } = await state2.gatekeeper.verifyExecution(candidate.authorizationId, execution, {
5523
5802
  bounds: candidate.bounds,
5524
5803
  context: candidate.context
@@ -5566,7 +5845,7 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
5566
5845
  isError: true
5567
5846
  };
5568
5847
  }
5569
- const contextKeys = getProfile3(passers[0].profileId)?.scopeSchema?.keyOrder ?? [];
5848
+ const contextKeys = getProfile4(passers[0].profileId)?.scopeSchema?.keyOrder ?? [];
5570
5849
  return attemptWithCandidates(passers);
5571
5850
  async function attemptWithCandidates(candidates) {
5572
5851
  const selection = selectAuthorization(
@@ -5758,29 +6037,33 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
5758
6037
  return attemptWithCandidates(remaining);
5759
6038
  }
5760
6039
  if (err instanceof SPReceiptError && err.statusCode === 409) {
5761
- const spBody = err.body;
5762
- let pendingApprovers = spBody.approvers ?? [];
5763
- if (pendingApprovers.length === 0) {
5764
- try {
5765
- const summary = await state2.spClient.getAuthorizationSummary(authzId);
5766
- if (summary?.approvers_frozen) {
5767
- pendingApprovers = summary.approvers_frozen;
5768
- }
5769
- if (summary?.created_by) {
5770
- pendingApprovers = [summary.created_by, ...pendingApprovers];
5771
- }
5772
- } catch {
5773
- }
5774
- } else {
5775
- try {
5776
- const summary = await state2.spClient.getAuthorizationSummary(authzId);
5777
- if (summary?.created_by) {
5778
- pendingApprovers = [summary.created_by, ...pendingApprovers];
6040
+ const rawBody = err.body;
6041
+ const spBody = rawBody?.errors?.[0] ?? rawBody;
6042
+ let pendingApprovers = spBody?.approvers ?? [];
6043
+ let summary;
6044
+ const authSummary = async () => {
6045
+ if (summary === void 0) {
6046
+ try {
6047
+ summary = await state2.spClient.getAuthorizationSummary(authzId);
6048
+ } catch {
6049
+ summary = null;
5779
6050
  }
5780
- } catch {
5781
6051
  }
6052
+ return summary;
6053
+ };
6054
+ if (pendingApprovers.length === 0) {
6055
+ const s = await authSummary();
6056
+ if (s?.approvers_frozen) pendingApprovers = s.approvers_frozen;
6057
+ }
6058
+ const isTeam = typeof state2.cache?.isTeamGroup === "function" ? state2.cache.isTeamGroup(auth.groupId) : void 0;
6059
+ const ownerId = (await authSummary())?.created_by;
6060
+ if (isTeam === true) {
6061
+ if (ownerId) pendingApprovers = pendingApprovers.filter((uid) => uid !== ownerId);
6062
+ } else if (ownerId) {
6063
+ pendingApprovers = [ownerId, ...pendingApprovers];
5782
6064
  }
5783
6065
  const uniqueApprovers = [...new Set(pendingApprovers)];
6066
+ const recipientUserIds = ownerId ? [.../* @__PURE__ */ new Set([ownerId, ...uniqueApprovers])] : uniqueApprovers;
5784
6067
  try {
5785
6068
  const enrichedArgs = await attachImagePreview(args);
5786
6069
  const wireExecCtx = wireExecutionContext(auth.profileId, execution);
@@ -5793,7 +6076,8 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
5793
6076
  toolArgs: enrichedArgs,
5794
6077
  executionContext: wireExecCtx,
5795
6078
  pendingApprovers: uniqueApprovers,
5796
- recipientUserIds: uniqueApprovers
6079
+ recipientUserIds,
6080
+ ...ownerId ? { createdBy: ownerId } : {}
5797
6081
  });
5798
6082
  const previewHash = await computeSubmissionPreviewHash(
5799
6083
  integrationManager2,
@@ -6026,7 +6310,7 @@ function buildMandateBrief(opts) {
6026
6310
  const parts = [];
6027
6311
  if (boundsDesc) parts.push(boundsDesc);
6028
6312
  if (executionLog) {
6029
- const profile = getProfile4(auth.profileId) ?? getProfile4(shortName2);
6313
+ const profile = getProfile5(auth.profileId) ?? getProfile5(shortName2);
6030
6314
  const consumption = getConsumptionState(auth, executionLog, profile);
6031
6315
  const compact = formatConsumptionCompact(consumption);
6032
6316
  if (compact) parts.push(compact);
@@ -6064,7 +6348,7 @@ function buildMandateBrief(opts) {
6064
6348
  }
6065
6349
 
6066
6350
  // src/tools/authorizations.ts
6067
- import { getProfile as getProfile5 } from "@hap/core";
6351
+ import { getProfile as getProfile6 } from "@hap/core";
6068
6352
  function shortProfileName2(profileId) {
6069
6353
  const withoutVersion = profileId.replace(/@.*$/, "");
6070
6354
  const parts = withoutVersion.split("/");
@@ -6182,7 +6466,7 @@ function listAuthorizationsHandler(state2, integrationManager2, contextDir) {
6182
6466
  } catch {
6183
6467
  }
6184
6468
  const shortName2 = shortProfileName2(auth.profileId);
6185
- const profile = getProfile5(auth.profileId) ?? getProfile5(shortName2);
6469
+ const profile = getProfile6(auth.profileId) ?? getProfile6(shortName2);
6186
6470
  if (profile) {
6187
6471
  const consumption = getConsumptionState(auth, state2.executionLog, profile);
6188
6472
  const consumptionText = formatConsumptionFull(consumption);
@@ -6242,7 +6526,7 @@ function listAuthorizationsHandler(state2, integrationManager2, contextDir) {
6242
6526
  lines.push(" Mode: review (each action requires your approval)");
6243
6527
  }
6244
6528
  const shortName2 = shortProfileName2(auth.profileId);
6245
- const profile = getProfile5(auth.profileId) ?? getProfile5(shortName2);
6529
+ const profile = getProfile6(auth.profileId) ?? getProfile6(shortName2);
6246
6530
  if (profile) {
6247
6531
  const consumption = getConsumptionState(auth, state2.executionLog, profile);
6248
6532
  const compact = formatConsumptionCompact(consumption);
@@ -6435,6 +6719,18 @@ async function decryptRejectionNote(by) {
6435
6719
  }
6436
6720
 
6437
6721
  // src/tools/commitments.ts
6722
+ function recordExecutionOutcome(state2, proposalId, ticketId, ok, text, structuredContent) {
6723
+ try {
6724
+ state2.executionResults?.record({ proposalId, ticketId, ok, text, structuredContent });
6725
+ } catch (err) {
6726
+ console.error(`[Suveren MCP] Could not record the execution result for proposal ${proposalId}:`, err);
6727
+ }
6728
+ }
6729
+ function logExecutionOutcome(proposalId, ok, shortMessage) {
6730
+ console.error(
6731
+ `[Suveren MCP] Proposal ${proposalId}: execution outcome=${ok ? "ok" : `error (${shortMessage ?? "unknown"})`}`
6732
+ );
6733
+ }
6438
6734
  function buildSkippedProposalNote(proposalId) {
6439
6735
  return `Proposal ${proposalId} was approved, but was submitted from another device/installation of this operator (or one whose local record is gone) \u2014 this gateway will not execute it. Re-run the same request here to execute it from this gateway.`;
6440
6736
  }
@@ -6701,12 +6997,19 @@ async function executeCommitted(proposal, state2, integrationManager2) {
6701
6997
  try {
6702
6998
  result = await integrationManager2.callTool(integrationId, toolName, outgoingArgs);
6703
6999
  } catch (err) {
7000
+ const msg = err instanceof Error ? err.message : String(err);
6704
7001
  state2.executionJournal.complete(receiptId, "failed");
7002
+ recordExecutionOutcome(state2, proposal.id, receiptId, false, `The tool call threw: ${msg}`);
7003
+ logExecutionOutcome(proposal.id, false, err instanceof Error ? err.name : "thrown error");
7004
+ void notifyControlPlane("execution-failed");
6705
7005
  throw err;
6706
7006
  }
6707
7007
  const resultText = result.content?.[0]?.text ?? JSON.stringify(result);
6708
7008
  if (result.isError) {
6709
7009
  state2.executionJournal.complete(receiptId, "failed", "refused", resultText);
7010
+ recordExecutionOutcome(state2, proposal.id, receiptId, false, resultText);
7011
+ logExecutionOutcome(proposal.id, false, "the connector returned an error");
7012
+ void notifyControlPlane("execution-failed");
6710
7013
  return {
6711
7014
  text: `Proposal ${proposal.id} was approved, but ${integrationId} refused to run it \u2014 nothing was done.
6712
7015
  Reason: ${resultText}`,
@@ -6714,6 +7017,8 @@ Reason: ${resultText}`,
6714
7017
  };
6715
7018
  }
6716
7019
  state2.executionJournal.complete(receiptId, "done");
7020
+ recordExecutionOutcome(state2, proposal.id, receiptId, true, resultText, result.structuredContent);
7021
+ logExecutionOutcome(proposal.id, true);
6717
7022
  state2.executionLog.record({
6718
7023
  profileId: proposal.profileId,
6719
7024
  path: proposal.path,
@@ -6724,6 +7029,12 @@ Reason: ${resultText}`,
6724
7029
  Result: ${resultText}` };
6725
7030
  } catch (err) {
6726
7031
  const msg = err instanceof Error ? err.message : String(err);
7032
+ if (state2.executionJournal.get(receiptId)?.state === "intent") {
7033
+ state2.executionJournal.complete(receiptId, "failed");
7034
+ recordExecutionOutcome(state2, proposal.id, receiptId, false, `Receipt issued but tool execution failed: ${msg}`);
7035
+ logExecutionOutcome(proposal.id, false, err instanceof Error ? err.name : "error");
7036
+ void notifyControlPlane("execution-failed");
7037
+ }
6727
7038
  return {
6728
7039
  text: `Proposal ${proposal.id} receipt issued but tool execution failed: ${msg}`,
6729
7040
  isError: true
@@ -6732,11 +7043,22 @@ Result: ${resultText}` };
6732
7043
  }
6733
7044
  function localOutcomeOfExecuted(proposal, state2) {
6734
7045
  const entry = state2.executionJournal?.findByProposal?.(proposal.id);
6735
- if (!entry || entry.state === "done") return null;
7046
+ if (!entry) return null;
6736
7047
  const integration = proposal.tool.split("__")[0];
7048
+ if (entry.state === "done") {
7049
+ const stored = state2.executionResults?.get(proposal.id);
7050
+ if (!stored || !stored.ok) return null;
7051
+ return {
7052
+ text: `Proposal ${proposal.id} was approved and executed.
7053
+ Result: ${stored.text}`,
7054
+ isError: false
7055
+ };
7056
+ }
6737
7057
  if (entry.state === "failed") {
6738
- const reasonSuffix = entry.detail ? `
6739
- Reason: ${entry.detail}` : "";
7058
+ const stored = state2.executionResults?.get(proposal.id);
7059
+ const detail = (stored && !stored.ok ? stored.text : void 0) ?? entry.detail;
7060
+ const reasonSuffix = detail ? `
7061
+ Reason: ${detail}` : "";
6740
7062
  return {
6741
7063
  text: entry.outcome === "as_refused" ? `Proposal ${proposal.id} was approved, but the Authority Server refused its ticket \u2014 nothing ran. Do not retry the same values; ask the person (e.g. for a higher limit).${reasonSuffix}` : entry.outcome === "refused" ? `Proposal ${proposal.id} was approved, but ${integration} refused to run it \u2014 nothing was done. Do not call the tool again with the same values; a new attempt needs a new request and approval.${reasonSuffix}` : entry.outcome === "changed" ? `Proposal ${proposal.id} was approved, but the record changed in ${integration} since it was requested \u2014 nothing was done. A new attempt needs a new request and approval.${reasonSuffix}` : `Proposal ${proposal.id} was approved, but running it failed \u2014 whether anything happened in ${integration} is unknown. Check ${integration} before trying again; a new attempt needs a new request and approval.${reasonSuffix}`,
6742
7064
  isError: true
@@ -7083,21 +7405,21 @@ function createMcpServer(state2, integrationManager2) {
7083
7405
  }
7084
7406
 
7085
7407
  // src/lib/agent-contact.ts
7086
- import { existsSync as existsSync15, mkdirSync as mkdirSync12, readFileSync as readFileSync16, renameSync as renameSync5, writeFileSync as writeFileSync12 } from "fs";
7087
- import { dirname as dirname12, join as join15 } from "path";
7408
+ import { existsSync as existsSync16, mkdirSync as mkdirSync13, readFileSync as readFileSync17, renameSync as renameSync5, writeFileSync as writeFileSync13 } from "fs";
7409
+ import { dirname as dirname13, join as join16 } from "path";
7088
7410
  var FILE = "agent-contact.json";
7089
7411
  var REWRITE_AFTER_MS = 6e4;
7090
7412
  var AgentContactStore = class {
7091
7413
  path;
7092
7414
  current;
7093
7415
  constructor(dataDir2) {
7094
- this.path = join15(dataDir2, FILE);
7416
+ this.path = join16(dataDir2, FILE);
7095
7417
  this.current = this.load();
7096
7418
  }
7097
7419
  load() {
7098
- if (!existsSync15(this.path)) return null;
7420
+ if (!existsSync16(this.path)) return null;
7099
7421
  try {
7100
- const v = JSON.parse(readFileSync16(this.path, "utf8"));
7422
+ const v = JSON.parse(readFileSync17(this.path, "utf8"));
7101
7423
  if (typeof v.clientName !== "string" || typeof v.firstSeenAt !== "string" || typeof v.lastSeenAt !== "string") return null;
7102
7424
  return { clientName: v.clientName, clientVersion: v.clientVersion, firstSeenAt: v.firstSeenAt, lastSeenAt: v.lastSeenAt };
7103
7425
  } catch {
@@ -7120,9 +7442,9 @@ var AgentContactStore = class {
7120
7442
  };
7121
7443
  if (!due) return;
7122
7444
  try {
7123
- mkdirSync12(dirname12(this.path), { recursive: true });
7445
+ mkdirSync13(dirname13(this.path), { recursive: true });
7124
7446
  const tmp = `${this.path}.tmp`;
7125
- writeFileSync12(tmp, JSON.stringify(this.current, null, 2));
7447
+ writeFileSync13(tmp, JSON.stringify(this.current, null, 2));
7126
7448
  renameSync5(tmp, this.path);
7127
7449
  } catch (err) {
7128
7450
  console.error("[Suveren MCP] Could not save agent contact:", err);
@@ -7157,16 +7479,16 @@ function verifyGateContentHashes(content, auth) {
7157
7479
  }
7158
7480
 
7159
7481
  // src/lib/integration-registry.ts
7160
- import { mkdirSync as mkdirSync13, readFileSync as readFileSync17, writeFileSync as writeFileSync13, existsSync as existsSync16 } from "fs";
7161
- import { homedir as homedir11 } from "os";
7162
- import { dirname as dirname13, join as join16 } from "path";
7163
- var DEFAULT_DATA_DIR = process.env.SUVEREN_DATA_DIR ?? join16(homedir11(), ".suveren");
7482
+ import { mkdirSync as mkdirSync14, readFileSync as readFileSync18, writeFileSync as writeFileSync14, existsSync as existsSync17 } from "fs";
7483
+ import { homedir as homedir12 } from "os";
7484
+ import { dirname as dirname14, join as join17 } from "path";
7485
+ var DEFAULT_DATA_DIR = process.env.SUVEREN_DATA_DIR ?? join17(homedir12(), ".suveren");
7164
7486
  var IntegrationRegistry = class {
7165
7487
  integrations = /* @__PURE__ */ new Map();
7166
7488
  filePath;
7167
7489
  constructor(dataDir2) {
7168
7490
  const dir = dataDir2 ?? DEFAULT_DATA_DIR;
7169
- this.filePath = join16(dir, "integrations.json");
7491
+ this.filePath = join17(dir, "integrations.json");
7170
7492
  this.load();
7171
7493
  }
7172
7494
  // ─── CRUD ───────────────────────────────────────────────────────────────
@@ -7197,12 +7519,12 @@ var IntegrationRegistry = class {
7197
7519
  }
7198
7520
  // ─── Load / Save ────────────────────────────────────────────────────────
7199
7521
  load() {
7200
- if (!existsSync16(this.filePath)) {
7201
- mkdirSync13(dirname13(this.filePath), { recursive: true });
7522
+ if (!existsSync17(this.filePath)) {
7523
+ mkdirSync14(dirname14(this.filePath), { recursive: true });
7202
7524
  return;
7203
7525
  }
7204
7526
  try {
7205
- const raw = readFileSync17(this.filePath, "utf-8");
7527
+ const raw = readFileSync18(this.filePath, "utf-8");
7206
7528
  const data = JSON.parse(raw);
7207
7529
  const migrated = data.integrations.map((i) => {
7208
7530
  if ("toolGating" in i && !("profile" in i)) {
@@ -7231,15 +7553,15 @@ var IntegrationRegistry = class {
7231
7553
  version: 1,
7232
7554
  integrations: Array.from(this.integrations.values())
7233
7555
  };
7234
- mkdirSync13(dirname13(this.filePath), { recursive: true });
7235
- writeFileSync13(this.filePath, JSON.stringify(data, null, 2), { encoding: "utf-8" });
7556
+ mkdirSync14(dirname14(this.filePath), { recursive: true });
7557
+ writeFileSync14(this.filePath, JSON.stringify(data, null, 2), { encoding: "utf-8" });
7236
7558
  }
7237
7559
  };
7238
7560
 
7239
7561
  // src/lib/integration-manager.ts
7240
- import { homedir as homedir12 } from "os";
7241
- import { join as join17, delimiter } from "path";
7242
- import { existsSync as existsSync17, writeFileSync as writeFileSync14, mkdirSync as mkdirSync14, readFileSync as readFileSync18, rmSync } from "fs";
7562
+ import { homedir as homedir13 } from "os";
7563
+ import { join as join18, delimiter } from "path";
7564
+ import { existsSync as existsSync18, writeFileSync as writeFileSync15, mkdirSync as mkdirSync15, readFileSync as readFileSync19, rmSync } from "fs";
7243
7565
  import { execFile } from "child_process";
7244
7566
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
7245
7567
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
@@ -7369,8 +7691,8 @@ function inheritedConnectorEnv(parent, passEnv = []) {
7369
7691
  }
7370
7692
 
7371
7693
  // src/lib/integration-manager.ts
7372
- var DEFAULT_DATA_DIR2 = process.env.SUVEREN_DATA_DIR ?? join17(homedir12(), ".suveren");
7373
- var INTEGRATIONS_DIR = process.env.SUVEREN_INTEGRATIONS_DIR ?? join17(DEFAULT_DATA_DIR2, "integrations");
7694
+ var DEFAULT_DATA_DIR2 = process.env.SUVEREN_DATA_DIR ?? join18(homedir13(), ".suveren");
7695
+ var INTEGRATIONS_DIR = process.env.SUVEREN_INTEGRATIONS_DIR ?? join18(DEFAULT_DATA_DIR2, "integrations");
7374
7696
  var OFFLINE_MODE = process.env.SUVEREN_OFFLINE === "1";
7375
7697
  var installLock = Promise.resolve();
7376
7698
  function withInstallLock(task) {
@@ -7378,7 +7700,7 @@ function withInstallLock(task) {
7378
7700
  installLock = run.then(() => void 0, () => void 0);
7379
7701
  return run;
7380
7702
  }
7381
- var INTEGRATIONS_BIN = join17(INTEGRATIONS_DIR, "node_modules", ".bin");
7703
+ var INTEGRATIONS_BIN = join18(INTEGRATIONS_DIR, "node_modules", ".bin");
7382
7704
  function getIntegrationsBinDir() {
7383
7705
  return INTEGRATIONS_BIN;
7384
7706
  }
@@ -7387,12 +7709,12 @@ function buildPath() {
7387
7709
  return [INTEGRATIONS_BIN, base].join(delimiter);
7388
7710
  }
7389
7711
  function ensureIntegrationsDir() {
7390
- if (!existsSync17(INTEGRATIONS_DIR)) {
7391
- mkdirSync14(INTEGRATIONS_DIR, { recursive: true });
7712
+ if (!existsSync18(INTEGRATIONS_DIR)) {
7713
+ mkdirSync15(INTEGRATIONS_DIR, { recursive: true });
7392
7714
  }
7393
- const pkgPath = join17(INTEGRATIONS_DIR, "package.json");
7394
- if (!existsSync17(pkgPath)) {
7395
- writeFileSync14(pkgPath, JSON.stringify({ name: "suveren-integrations", version: "1.0.0", private: true }, null, 2));
7715
+ const pkgPath = join18(INTEGRATIONS_DIR, "package.json");
7716
+ if (!existsSync18(pkgPath)) {
7717
+ writeFileSync15(pkgPath, JSON.stringify({ name: "suveren-integrations", version: "1.0.0", private: true }, null, 2));
7396
7718
  }
7397
7719
  }
7398
7720
  function readAgeOf(config) {
@@ -7481,19 +7803,19 @@ var IntegrationManager = class {
7481
7803
  * exist, and treat anything else as not-installed (and reinstallable).
7482
7804
  */
7483
7805
  isUsableInstall(npmPackage) {
7484
- const pkgDir = join17(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
7485
- const pkgJson = join17(pkgDir, "package.json");
7486
- if (!existsSync17(pkgDir) || !existsSync17(pkgJson)) return false;
7806
+ const pkgDir = join18(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
7807
+ const pkgJson = join18(pkgDir, "package.json");
7808
+ if (!existsSync18(pkgDir) || !existsSync18(pkgJson)) return false;
7487
7809
  try {
7488
- const pkg = JSON.parse(readFileSync18(pkgJson, "utf8"));
7810
+ const pkg = JSON.parse(readFileSync19(pkgJson, "utf8"));
7489
7811
  const defaultBinName = (pkg.name ?? npmPackage).split("/").pop() ?? "";
7490
7812
  const binMap = typeof pkg.bin === "string" ? { [defaultBinName]: pkg.bin } : pkg.bin ?? {};
7491
7813
  const binEntries = Object.entries(binMap);
7492
7814
  for (const [binName, target] of binEntries) {
7493
- if (!existsSync17(join17(pkgDir, target))) return false;
7815
+ if (!existsSync18(join18(pkgDir, target))) return false;
7494
7816
  if (!this.binShimExists(binName)) return false;
7495
7817
  }
7496
- if (binEntries.length === 0 && pkg.main && !existsSync17(join17(pkgDir, pkg.main))) return false;
7818
+ if (binEntries.length === 0 && pkg.main && !existsSync18(join18(pkgDir, pkg.main))) return false;
7497
7819
  return true;
7498
7820
  } catch {
7499
7821
  return false;
@@ -7508,9 +7830,9 @@ var IntegrationManager = class {
7508
7830
  */
7509
7831
  readInstalledVersion(npmPackage) {
7510
7832
  if (!this.isUsableInstall(npmPackage)) return null;
7511
- const pkgJson = join17(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"), "package.json");
7833
+ const pkgJson = join18(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"), "package.json");
7512
7834
  try {
7513
- const pkg = JSON.parse(readFileSync18(pkgJson, "utf8"));
7835
+ const pkg = JSON.parse(readFileSync19(pkgJson, "utf8"));
7514
7836
  return typeof pkg.version === "string" ? pkg.version : null;
7515
7837
  } catch {
7516
7838
  return null;
@@ -7524,7 +7846,7 @@ var IntegrationManager = class {
7524
7846
  binShimExists(binName) {
7525
7847
  if (!binName) return false;
7526
7848
  const candidates = process.platform === "win32" ? [binName, `${binName}.cmd`, `${binName}.CMD`, `${binName}.ps1`] : [binName];
7527
- return candidates.some((c) => existsSync17(join17(INTEGRATIONS_BIN, c)));
7849
+ return candidates.some((c) => existsSync18(join18(INTEGRATIONS_BIN, c)));
7528
7850
  }
7529
7851
  /**
7530
7852
  * The exact version THIS gateway's CURRENTLY LOADED manifest pins
@@ -7614,8 +7936,8 @@ var IntegrationManager = class {
7614
7936
  * nothing at all.
7615
7937
  */
7616
7938
  async installNow(npmPackage, exactVersion, cleanFirst = true) {
7617
- const pkgDir = join17(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
7618
- if (cleanFirst && existsSync17(pkgDir)) {
7939
+ const pkgDir = join18(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
7940
+ if (cleanFirst && existsSync18(pkgDir)) {
7619
7941
  console.error(`[IntegrationManager] Removing ${exactVersion ? "outdated" : "unusable"} install of ${npmPackage}`);
7620
7942
  try {
7621
7943
  rmSync(pkgDir, { recursive: true, force: true });
@@ -8204,7 +8526,18 @@ var IntegrationManager = class {
8204
8526
  simulationOnly: ext.simulationOnly,
8205
8527
  approvalView: ext.approvalView,
8206
8528
  contextKeys: ext.contextKeys,
8207
- sourceFromArg: ext.sourceFromArg
8529
+ sourceFromArg: ext.sourceFromArg,
8530
+ // A WRITE override may ALSO declare a capability-gate (enum boundType,
8531
+ // e.g. records.json's archive_access/delete_access) — the same
8532
+ // mechanism the read branch uses, now generalized: hap-core's
8533
+ // Gatekeeper deliberately never compares an `enum` bound against
8534
+ // execution (gatekeeper.ts's 'enum' case: "the gateway tool-proxy
8535
+ // gates tool calls ... before they reach the Gatekeeper"), so if this
8536
+ // branch dropped it, nothing anywhere would ever enforce it. Before
8537
+ // this, boundField/requiredValue on a non-"read" override was silently
8538
+ // discarded here — the config in the manifest existed but did nothing.
8539
+ boundField: ext.boundField,
8540
+ requiredValue: ext.requiredValue
8208
8541
  };
8209
8542
  }
8210
8543
  return {
@@ -8335,7 +8668,7 @@ async function buildTicketDetails(archive, ticketIds) {
8335
8668
  }
8336
8669
 
8337
8670
  // src/lib/report/window.ts
8338
- import { getProfile as getProfile6 } from "@hap/core";
8671
+ import { getProfile as getProfile7 } from "@hap/core";
8339
8672
  var REPORT_AGE_FIELD = "read_age_days";
8340
8673
  var REPORTING_PROFILE = "reporting";
8341
8674
  var END_SKEW_SECONDS = 120;
@@ -8349,7 +8682,7 @@ function windowLabel(start, days, loadedAt) {
8349
8682
  if (loadedAt !== null && loadedAt === start) return `${since} (when the test data was loaded)`;
8350
8683
  return `${since} (the last ${days} ${days === 1 ? "day" : "days"})`;
8351
8684
  }
8352
- var defaultLookup = (id) => getProfile6(id);
8685
+ var defaultLookup = (id) => getProfile7(id);
8353
8686
  function reportingMandateRefusal(m, lookup = defaultLookup) {
8354
8687
  const schema = lookup(m.profileId)?.boundsSchema;
8355
8688
  const field = resolveAgeBoundField(schema, REPORT_AGE_FIELD);
@@ -8559,28 +8892,28 @@ function agentAccess(token, opensSession) {
8559
8892
  }
8560
8893
 
8561
8894
  // src/lib/profile-loader.ts
8562
- import { readFileSync as readFileSync19, existsSync as existsSync18 } from "fs";
8563
- import { join as join18, resolve as resolve2 } from "path";
8895
+ import { readFileSync as readFileSync20, existsSync as existsSync19 } from "fs";
8896
+ import { join as join19, resolve as resolve2 } from "path";
8564
8897
  import { registerProfile } from "@hap/core";
8565
8898
  function loadProfiles(profilesDir) {
8566
- const dir = resolve2(profilesDir ?? process.env.SUVEREN_PROFILES_DIR ?? join18(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "..", "hap-profiles"));
8567
- const indexPath = join18(dir, "index.json");
8568
- if (!existsSync18(indexPath)) {
8899
+ const dir = resolve2(profilesDir ?? process.env.SUVEREN_PROFILES_DIR ?? join19(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "..", "hap-profiles"));
8900
+ const indexPath = join19(dir, "index.json");
8901
+ if (!existsSync19(indexPath)) {
8569
8902
  warnNoProfiles(`No index.json at ${indexPath}`);
8570
8903
  return 0;
8571
8904
  }
8572
8905
  let index;
8573
8906
  try {
8574
- index = JSON.parse(readFileSync19(indexPath, "utf-8"));
8907
+ index = JSON.parse(readFileSync20(indexPath, "utf-8"));
8575
8908
  } catch (err) {
8576
8909
  console.error(`[ProfileLoader] Failed to parse ${indexPath}:`, err);
8577
8910
  return 0;
8578
8911
  }
8579
8912
  let loaded = 0;
8580
8913
  for (const [profileId, relativePath] of Object.entries(index.profiles)) {
8581
- const profilePath = join18(dir, relativePath);
8914
+ const profilePath = join19(dir, relativePath);
8582
8915
  try {
8583
- const profile = JSON.parse(readFileSync19(profilePath, "utf-8"));
8916
+ const profile = JSON.parse(readFileSync20(profilePath, "utf-8"));
8584
8917
  registerProfile(profileId, profile);
8585
8918
  const shortName2 = profileId.split("/").pop()?.replace(/@.*$/, "");
8586
8919
  if (shortName2 && shortName2 !== profileId) {
@@ -8921,20 +9254,20 @@ async function controlPlaneMandate(dryRun, request) {
8921
9254
  }
8922
9255
 
8923
9256
  // src/lib/guides.ts
8924
- import { existsSync as existsSync19, readdirSync, readFileSync as readFileSync20 } from "fs";
8925
- import { dirname as dirname14, join as join19 } from "path";
9257
+ import { existsSync as existsSync20, readdirSync, readFileSync as readFileSync21 } from "fs";
9258
+ import { dirname as dirname15, join as join20 } from "path";
8926
9259
  var FILE_RE = /^(?:(\d+)-)?([a-z0-9][a-z0-9-]*)\.md$/;
8927
9260
  function builtinGuidesDir() {
8928
9261
  if (process.env.SUVEREN_BUILTIN_GUIDES_DIR) return process.env.SUVEREN_BUILTIN_GUIDES_DIR;
8929
9262
  let dir = import.meta.dirname ?? __dirname;
8930
9263
  for (let i = 0; i < 8; i++) {
8931
- const candidate = join19(dir, "content", "guides");
8932
- if (existsSync19(candidate)) return candidate;
8933
- const parent = dirname14(dir);
9264
+ const candidate = join20(dir, "content", "guides");
9265
+ if (existsSync20(candidate)) return candidate;
9266
+ const parent = dirname15(dir);
8934
9267
  if (parent === dir) break;
8935
9268
  dir = parent;
8936
9269
  }
8937
- return join19(import.meta.dirname ?? __dirname, "content", "guides");
9270
+ return join20(import.meta.dirname ?? __dirname, "content", "guides");
8938
9271
  }
8939
9272
  function parse(body) {
8940
9273
  const lines = body.split(/\r?\n/);
@@ -8945,14 +9278,14 @@ function parse(body) {
8945
9278
  return { title, summary };
8946
9279
  }
8947
9280
  function readDir(dir, source) {
8948
- if (!dir || !existsSync19(dir)) return [];
9281
+ if (!dir || !existsSync20(dir)) return [];
8949
9282
  const out = [];
8950
9283
  for (const file of readdirSync(dir)) {
8951
9284
  const m = FILE_RE.exec(file);
8952
9285
  if (!m) continue;
8953
9286
  let body;
8954
9287
  try {
8955
- body = readFileSync20(join19(dir, file), "utf8");
9288
+ body = readFileSync21(join20(dir, file), "utf8");
8956
9289
  } catch {
8957
9290
  continue;
8958
9291
  }
@@ -9087,7 +9420,7 @@ function unifiedLineDiff(before, after) {
9087
9420
  }
9088
9421
 
9089
9422
  // src/lib/builtins/setup.ts
9090
- import { getProfile as getProfile7 } from "@hap/core";
9423
+ import { getProfile as getProfile8 } from "@hap/core";
9091
9424
  var DELEGATION_PROFILE = "delegation";
9092
9425
  var DELEGATION_INTENT_HINT = [
9093
9426
  "Why \u2014 With test data only, I want to find out which routine parts of my work an AI could take over, so people gain time \u2014 before anything touches real systems.",
@@ -9139,7 +9472,7 @@ function delegationItselfRefusal(args) {
9139
9472
  }
9140
9473
  function delegationMandateRefusal(auth) {
9141
9474
  if (isSimulationMode()) return void 0;
9142
- const profile = getProfile7(auth.profileId);
9475
+ const profile = getProfile8(auth.profileId);
9143
9476
  if (profile?.scopeSchema?.fields?.allowed_profiles) return void 0;
9144
9477
  return `your Delegation mandate (${shortName(auth.profileId)} ${versionOf(auth.profileId)}) does not declare which profiles or modes you may propose \u2014 only a Delegation mandate on delegation 0.4 or later works outside simulation mode. Ask the person to give a current Delegation mandate.`;
9145
9478
  }
@@ -9197,7 +9530,7 @@ function delegationChecks(deps, args, targetShortName, targetDisplayName) {
9197
9530
  );
9198
9531
  const first = governing[0];
9199
9532
  if (first) {
9200
- const delegationProfile = getProfile7(first.profileId);
9533
+ const delegationProfile = getProfile8(first.profileId);
9201
9534
  const entry = getConsumptionState(first, deps.state.executionLog, delegationProfile).find((e) => e.field === "mandate_daily_max");
9202
9535
  if (entry && entry.limit !== null) {
9203
9536
  checks.push(`Proposals today: ${entry.current} of ${entry.limit}`);
@@ -9207,7 +9540,7 @@ function delegationChecks(deps, args, targetShortName, targetDisplayName) {
9207
9540
  }
9208
9541
  function resolveTargetProfile(input) {
9209
9542
  if (typeof input !== "string" || !input.trim()) return void 0;
9210
- const profile = getProfile7(input.trim());
9543
+ const profile = getProfile8(input.trim());
9211
9544
  if (!profile) return void 0;
9212
9545
  return { profile, shortName: shortName(profile.id) };
9213
9546
  }
@@ -9358,7 +9691,7 @@ Read each topic with get_guide(topic) before doing that step.`);
9358
9691
  return { ...builtinText(`Unknown topic "${topic}". Topics: ${guides.map((g) => g.topic).join(", ") || "none installed"}.`), isError: true };
9359
9692
  }
9360
9693
  const body = fillGenerated(guide.body.trim(), {
9361
- limits: () => limitsSection(systems.map((s) => ({ id: s.id, profile: getProfile7(s.profile) }))),
9694
+ limits: () => limitsSection(systems.map((s) => ({ id: s.id, profile: getProfile8(s.profile) }))),
9362
9695
  package: () => packageSection(
9363
9696
  deps.integrationManager.getAllTools().find((t) => !deps.integrationManager.isBuiltin(t.integrationId) && t.originalName === "load_simulation")
9364
9697
  )
@@ -9500,8 +9833,8 @@ function registerBuiltins(deps, factories = BUILTIN_FACTORIES) {
9500
9833
  import { decodeMandateBlob as decodeMandateBlob3 } from "@hap/core";
9501
9834
 
9502
9835
  // src/lib/committed-executor.ts
9503
- import { existsSync as existsSync20, mkdirSync as mkdirSync15, readFileSync as readFileSync21, unlinkSync as unlinkSync7, writeFileSync as writeFileSync15 } from "fs";
9504
- import { dirname as dirname15, join as join20 } from "path";
9836
+ import { existsSync as existsSync21, mkdirSync as mkdirSync16, readFileSync as readFileSync22, unlinkSync as unlinkSync8, writeFileSync as writeFileSync16 } from "fs";
9837
+ import { dirname as dirname16, join as join21 } from "path";
9505
9838
  function describeAutoExecutionLog(proposalId, tool, result) {
9506
9839
  if (result.skipped) return null;
9507
9840
  return result.isError ? `[Suveren MCP] Auto-exec proposal ${proposalId}: ${result.text}` : `[Suveren MCP] Auto-executed proposal ${proposalId}: ${tool}`;
@@ -9533,7 +9866,7 @@ var ExecutorLock = class {
9533
9866
  path;
9534
9867
  held = false;
9535
9868
  constructor(dataDir2) {
9536
- this.path = join20(dataDir2, "executor.lock");
9869
+ this.path = join21(dataDir2, "executor.lock");
9537
9870
  }
9538
9871
  /** Try to take the lock. Returns the holder's pid when someone else has it. */
9539
9872
  acquire() {
@@ -9541,9 +9874,9 @@ var ExecutorLock = class {
9541
9874
  if (current && current.pid !== process.pid && isAlive(current.pid)) {
9542
9875
  return { held: false, holderPid: current.pid };
9543
9876
  }
9544
- mkdirSync15(dirname15(this.path), { recursive: true, mode: 448 });
9877
+ mkdirSync16(dirname16(this.path), { recursive: true, mode: 448 });
9545
9878
  const lock = { pid: process.pid, startedAt: Math.floor(Date.now() / 1e3) };
9546
- writeFileSync15(this.path, JSON.stringify(lock), { mode: 384 });
9879
+ writeFileSync16(this.path, JSON.stringify(lock), { mode: 384 });
9547
9880
  this.held = true;
9548
9881
  return { held: true };
9549
9882
  }
@@ -9559,15 +9892,15 @@ var ExecutorLock = class {
9559
9892
  const current = this.read();
9560
9893
  if (current?.pid === process.pid) {
9561
9894
  try {
9562
- unlinkSync7(this.path);
9895
+ unlinkSync8(this.path);
9563
9896
  } catch {
9564
9897
  }
9565
9898
  }
9566
9899
  }
9567
9900
  read() {
9568
- if (!existsSync20(this.path)) return null;
9901
+ if (!existsSync21(this.path)) return null;
9569
9902
  try {
9570
- const parsed = JSON.parse(readFileSync21(this.path, "utf8"));
9903
+ const parsed = JSON.parse(readFileSync22(this.path, "utf8"));
9571
9904
  return typeof parsed?.pid === "number" ? parsed : null;
9572
9905
  } catch {
9573
9906
  return null;
@@ -9584,20 +9917,20 @@ function isAlive(pid) {
9584
9917
  }
9585
9918
 
9586
9919
  // bin/http.ts
9587
- import { homedir as homedir13 } from "os";
9588
- import { join as join22 } from "path";
9920
+ import { homedir as homedir14 } from "os";
9921
+ import { join as join23 } from "path";
9589
9922
 
9590
9923
  // src/lib/mcp-as-tracker.ts
9591
- import { existsSync as existsSync21, mkdirSync as mkdirSync16, readFileSync as readFileSync22, writeFileSync as writeFileSync16 } from "fs";
9592
- import { dirname as dirname16, join as join21 } from "path";
9924
+ import { existsSync as existsSync22, mkdirSync as mkdirSync17, readFileSync as readFileSync23, writeFileSync as writeFileSync17 } from "fs";
9925
+ import { dirname as dirname17, join as join22 } from "path";
9593
9926
  function trackerPath(dataDir2) {
9594
- return join21(dataDir2, "mcp-as-pairing.json");
9927
+ return join22(dataDir2, "mcp-as-pairing.json");
9595
9928
  }
9596
9929
  function readMcpPairedAsUrl(dataDir2) {
9597
9930
  const path = trackerPath(dataDir2);
9598
- if (!existsSync21(path)) return null;
9931
+ if (!existsSync22(path)) return null;
9599
9932
  try {
9600
- const data = JSON.parse(readFileSync22(path, "utf-8"));
9933
+ const data = JSON.parse(readFileSync23(path, "utf-8"));
9601
9934
  return typeof data.asUrl === "string" ? data.asUrl : null;
9602
9935
  } catch {
9603
9936
  return null;
@@ -9605,12 +9938,12 @@ function readMcpPairedAsUrl(dataDir2) {
9605
9938
  }
9606
9939
  function writeMcpPairedAsUrl(dataDir2, asUrl) {
9607
9940
  const path = trackerPath(dataDir2);
9608
- mkdirSync16(dirname16(path), { recursive: true, mode: 448 });
9609
- writeFileSync16(path, JSON.stringify({ asUrl }, null, 2), { encoding: "utf-8", mode: 384 });
9941
+ mkdirSync17(dirname17(path), { recursive: true, mode: 448 });
9942
+ writeFileSync17(path, JSON.stringify({ asUrl }, null, 2), { encoding: "utf-8", mode: 384 });
9610
9943
  }
9611
9944
 
9612
9945
  // bin/http.ts
9613
- var dataDir = process.env.SUVEREN_DATA_DIR ?? join22(homedir13(), ".suveren");
9946
+ var dataDir = process.env.SUVEREN_DATA_DIR ?? join23(homedir14(), ".suveren");
9614
9947
  var spUrl = resolveAsUrl(dataDir);
9615
9948
  validatePinTlsForUrl(resolvePinTls(dataDir), spUrl);
9616
9949
  var port = parseInt(process.env.SUVEREN_MCP_PORT ?? "3430", 10);
@@ -9696,7 +10029,8 @@ app.post("/internal/configure", internalOnly, (req, res) => {
9696
10029
  state.receiptArchive.setVaultKey(key);
9697
10030
  state.reportStore.setVaultKey(key);
9698
10031
  state.outcomeOutbox.setVaultKey(key);
9699
- console.error("[Suveren MCP] Vault key configured \u2014 gate store + denial log + receipt archive + report store + outcome outbox encryption active");
10032
+ state.executionResults.setVaultKey(key);
10033
+ console.error("[Suveren MCP] Vault key configured \u2014 gate store + denial log + receipt archive + report store + outcome outbox + execution results encryption active");
9700
10034
  }
9701
10035
  if (apiKey) {
9702
10036
  state.spClient.setApiKey(apiKey);
@@ -10079,6 +10413,24 @@ app.get("/internal/skipped-commitments", internalOnly, async (_req, res) => {
10079
10413
  res.status(502).json({ error: err instanceof Error ? err.message : "Could not reach the Authority Server" });
10080
10414
  }
10081
10415
  });
10416
+ app.get("/internal/my-pending-proposals", internalOnly, async (_req, res) => {
10417
+ try {
10418
+ const submitted = state.proposalSubmissions.list();
10419
+ const resolved = await Promise.all(
10420
+ submitted.map(async (s) => {
10421
+ try {
10422
+ return await state.spClient.getProposal(s.proposalId);
10423
+ } catch {
10424
+ return null;
10425
+ }
10426
+ })
10427
+ );
10428
+ const pending = resolved.filter((p) => p !== null && p.status === "pending");
10429
+ res.json({ proposals: pending });
10430
+ } catch (err) {
10431
+ res.status(502).json({ error: err instanceof Error ? err.message : "Could not reach the Authority Server" });
10432
+ }
10433
+ });
10082
10434
  app.get("/internal/evidence", internalOnly, (_req, res) => {
10083
10435
  if (state.receiptArchive.isLocked()) {
10084
10436
  res.status(503).json({ error: "Vault locked \u2014 the evidence archive is encrypted until sign-in." });
@@ -10364,7 +10716,7 @@ app.listen(port, bindHost, () => {
10364
10716
  (proposal) => executeCommitted(proposal, state, integrationManager)
10365
10717
  );
10366
10718
  installCommittedExecutor(committedExecutor);
10367
- const dataDir2 = process.env.SUVEREN_DATA_DIR ?? join22(homedir13(), ".suveren");
10719
+ const dataDir2 = process.env.SUVEREN_DATA_DIR ?? join23(homedir14(), ".suveren");
10368
10720
  const executorLock = new ExecutorLock(dataDir2);
10369
10721
  const lock = executorLock.acquire();
10370
10722
  if (!lock.held) {