@suveren/gateway 0.28.1 → 0.29.1

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.
@@ -5,9 +5,9 @@ import {
5
5
 
6
6
  // src/index.ts
7
7
  import { randomBytes as randomBytes3 } from "crypto";
8
- import { existsSync as existsSync12, readFileSync as readFileSync14, renameSync as renameSync2 } from "fs";
9
- import { dirname as dirname9, join as join14 } from "path";
10
- import { homedir as homedir7 } from "os";
8
+ import { existsSync as existsSync13, readFileSync as readFileSync15, renameSync as renameSync3 } from "fs";
9
+ import { dirname as dirname10, join as join15 } from "path";
10
+ import { homedir as homedir8 } from "os";
11
11
  import express from "express";
12
12
  import { createProxyMiddleware } from "http-proxy-middleware";
13
13
 
@@ -309,15 +309,109 @@ var Vault = class {
309
309
  }
310
310
  };
311
311
 
312
- // src/lib/install-method.ts
313
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
312
+ // src/lib/shown-snapshot.ts
313
+ import { createHash as createHash2 } from "crypto";
314
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, renameSync } from "fs";
315
+ import { homedir as homedir2 } from "os";
314
316
  import { dirname as dirname2, join as join2 } from "path";
317
+ import { canonicalize } from "@hap/core";
318
+ function computeShownHash(snapshot) {
319
+ return `sha256:${createHash2("sha256").update(canonicalize(snapshot)).digest("hex")}`;
320
+ }
321
+ var DEFAULT_DIR = process.env.SUVEREN_DATA_DIR ?? join2(homedir2(), ".suveren");
322
+ var FILE_NAME = "shown-snapshots.enc.json";
323
+ var SHOWN_SNAPSHOT_MAX_AGE_SECONDS = 90 * 24 * 60 * 60;
324
+ var ShownSnapshotStore = class {
325
+ filePath;
326
+ constructor(dataDir) {
327
+ this.filePath = join2(dataDir ?? DEFAULT_DIR, FILE_NAME);
328
+ }
329
+ /** Hashes + stores `snapshot` for `proposalId`, returning the hash to send
330
+ * with the decision. Requires an unlocked vault — callers already require
331
+ * an AS session (and therefore an unlocked vault) to reach this path.
332
+ * Opportunistically prunes expired entries first (doc §8.4's retention —
333
+ * this store has no in-memory cache to prune on every write the way
334
+ * `ExecutionLog` does, so a decision call is the one guaranteed moment
335
+ * the vault key is in hand). */
336
+ save(proposalId, snapshot, vault2) {
337
+ this.pruneWithVault(vault2);
338
+ const shownHash = computeShownHash(snapshot);
339
+ const entry = { shownHash, snapshot, storedAt: Math.floor(Date.now() / 1e3) };
340
+ const file = this.load();
341
+ file.entries[proposalId] = vault2.encrypt(JSON.stringify(entry));
342
+ this.persist(file);
343
+ return shownHash;
344
+ }
345
+ /** Returns the stored entry, or null if none exists (off-device / never
346
+ * approved on this gateway — doc §8.4's "only on the approving gateway"). */
347
+ get(proposalId, vault2) {
348
+ const file = this.load();
349
+ const blob = file.entries[proposalId];
350
+ if (!blob) return null;
351
+ try {
352
+ return JSON.parse(vault2.decrypt(blob));
353
+ } catch {
354
+ return null;
355
+ }
356
+ }
357
+ remove(proposalId) {
358
+ const file = this.load();
359
+ if (!(proposalId in file.entries)) return;
360
+ delete file.entries[proposalId];
361
+ this.persist(file);
362
+ }
363
+ /**
364
+ * Drops entries older than {@link SHOWN_SNAPSHOT_MAX_AGE_SECONDS}. `storedAt`
365
+ * lives INSIDE each encrypted blob, so this needs the vault key — called
366
+ * once per unlock (same trigger as `ExecutionLog`'s own migration-on-
367
+ * unlock), not on a timer while locked.
368
+ */
369
+ pruneWithVault(vault2, now = Math.floor(Date.now() / 1e3)) {
370
+ const file = this.load();
371
+ const cutoff = now - SHOWN_SNAPSHOT_MAX_AGE_SECONDS;
372
+ let changed = false;
373
+ for (const [proposalId, blob] of Object.entries(file.entries)) {
374
+ try {
375
+ const entry = JSON.parse(vault2.decrypt(blob));
376
+ if (entry.storedAt < cutoff) {
377
+ delete file.entries[proposalId];
378
+ changed = true;
379
+ }
380
+ } catch {
381
+ }
382
+ }
383
+ if (changed) this.persist(file);
384
+ }
385
+ // ─── Persistence ────────────────────────────────────────────────────────
386
+ load() {
387
+ if (!existsSync2(this.filePath)) return { version: 1, entries: {} };
388
+ try {
389
+ const parsed = JSON.parse(readFileSync2(this.filePath, "utf8"));
390
+ if (parsed?.version !== 1 || typeof parsed.entries !== "object" || parsed.entries === null) {
391
+ return { version: 1, entries: {} };
392
+ }
393
+ return parsed;
394
+ } catch {
395
+ return { version: 1, entries: {} };
396
+ }
397
+ }
398
+ persist(file) {
399
+ mkdirSync2(dirname2(this.filePath), { recursive: true, mode: 448 });
400
+ const tmp = `${this.filePath}.tmp`;
401
+ writeFileSync2(tmp, JSON.stringify(file, null, 2), { mode: 384 });
402
+ renameSync(tmp, this.filePath);
403
+ }
404
+ };
405
+
406
+ // src/lib/install-method.ts
407
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
408
+ import { dirname as dirname3, join as join3 } from "path";
315
409
  var INSTALL_MARKER = "install-method.json";
316
410
  function readInstallMarker(bundleRoot) {
317
- const file = join2(bundleRoot, INSTALL_MARKER);
318
- if (!existsSync2(file)) return null;
411
+ const file = join3(bundleRoot, INSTALL_MARKER);
412
+ if (!existsSync3(file)) return null;
319
413
  try {
320
- const parsed = JSON.parse(readFileSync2(file, "utf8"));
414
+ const parsed = JSON.parse(readFileSync3(file, "utf8"));
321
415
  return typeof parsed.method === "string" ? parsed.method : null;
322
416
  } catch {
323
417
  return null;
@@ -325,13 +419,13 @@ function readInstallMarker(bundleRoot) {
325
419
  }
326
420
  function detectInstallMethod({ dir, dockerEnv, managed }) {
327
421
  if (managed) return "managed";
328
- if (readInstallMarker(dirname2(dirname2(dir))) === "msi") return "msi";
422
+ if (readInstallMarker(dirname3(dirname3(dir))) === "msi") return "msi";
329
423
  if (dockerEnv) return "docker";
330
424
  if (dir.includes("/node_modules/@suveren/gateway/")) return "npm";
331
425
  let cursor = dir;
332
426
  for (let i = 0; i < 8; i++) {
333
- if (existsSync2(join2(cursor, ".git"))) return "dev";
334
- const parent = dirname2(cursor);
427
+ if (existsSync3(join3(cursor, ".git"))) return "dev";
428
+ const parent = dirname3(cursor);
335
429
  if (parent === cursor) break;
336
430
  cursor = parent;
337
431
  }
@@ -625,6 +719,25 @@ async function getToolPreview(data) {
625
719
  clearTimeout(timeout);
626
720
  }
627
721
  }
722
+ async function getToolRequestContext(data) {
723
+ const controller = new AbortController();
724
+ const timeout = setTimeout(() => controller.abort(), 12e3);
725
+ try {
726
+ const res = await fetch(`${MCP_BASE}/internal/request-context`, {
727
+ method: "POST",
728
+ headers: internalHeaders(),
729
+ body: JSON.stringify(data),
730
+ signal: controller.signal
731
+ });
732
+ if (!res.ok) {
733
+ const err = await res.json().catch(() => ({ error: "Unknown error" }));
734
+ throw new Error(`MCP request-context failed: ${err.error ?? res.status}`);
735
+ }
736
+ return res.json();
737
+ } finally {
738
+ clearTimeout(timeout);
739
+ }
740
+ }
628
741
  async function getAgentContact() {
629
742
  const res = await fetch(`${MCP_BASE}/internal/agent-contact`, {
630
743
  headers: internalHeaders()
@@ -759,12 +872,12 @@ async function loadOrGenerateKeyPair(vault2) {
759
872
  }
760
873
 
761
874
  // src/lib/client-version.ts
762
- import { readFileSync as readFileSync3 } from "fs";
763
- import { join as join3 } from "path";
875
+ import { readFileSync as readFileSync4 } from "fs";
876
+ import { join as join4 } from "path";
764
877
  var UNKNOWN = "unknown";
765
878
  function readJson(path) {
766
879
  try {
767
- return JSON.parse(readFileSync3(path, "utf8"));
880
+ return JSON.parse(readFileSync4(path, "utf8"));
768
881
  } catch {
769
882
  return null;
770
883
  }
@@ -775,13 +888,13 @@ function detectGatewayVersion() {
775
888
  const start = import.meta.dirname ?? process.cwd();
776
889
  let dir = start;
777
890
  for (let up = 0; up < 6; up++) {
778
- const pkg = readJson(join3(dir, "package.json"));
891
+ const pkg = readJson(join4(dir, "package.json"));
779
892
  const name = pkg?.name;
780
893
  const version = pkg?.version;
781
894
  if (typeof version === "string" && (name === "@suveren/gateway" || name === "suveren-gateway")) {
782
895
  return version;
783
896
  }
784
- const parent = join3(dir, "..");
897
+ const parent = join4(dir, "..");
785
898
  if (parent === dir) break;
786
899
  dir = parent;
787
900
  }
@@ -795,17 +908,17 @@ function clientVersionHeaders() {
795
908
  }
796
909
 
797
910
  // src/lib/as-pairing.ts
798
- import { createHash as createHash2 } from "crypto";
799
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync4, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
800
- import { dirname as dirname3, join as join4 } from "path";
911
+ import { createHash as createHash3 } from "crypto";
912
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync5, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
913
+ import { dirname as dirname4, join as join5 } from "path";
801
914
  function pairingPath(dataDir) {
802
- return join4(dataDir, "as-pairing.json");
915
+ return join5(dataDir, "as-pairing.json");
803
916
  }
804
917
  function readPairing(dataDir) {
805
918
  const path = pairingPath(dataDir);
806
- if (!existsSync3(path)) return null;
919
+ if (!existsSync4(path)) return null;
807
920
  try {
808
- const data = JSON.parse(readFileSync4(path, "utf-8"));
921
+ const data = JSON.parse(readFileSync5(path, "utf-8"));
809
922
  if (typeof data.asUrl !== "string" || typeof data.publicKeyHex !== "string") return null;
810
923
  return {
811
924
  asUrl: data.asUrl,
@@ -825,15 +938,15 @@ function writePairing(dataDir, asUrl, publicKeyHex, opts = {}) {
825
938
  ...opts.tlsSpkiPinHex ? { tlsSpkiPinHex: opts.tlsSpkiPinHex } : {}
826
939
  };
827
940
  const path = pairingPath(dataDir);
828
- mkdirSync2(dirname3(path), { recursive: true, mode: 448 });
829
- writeFileSync2(path, JSON.stringify(pairing, null, 2), { encoding: "utf-8", mode: 384 });
941
+ mkdirSync3(dirname4(path), { recursive: true, mode: 448 });
942
+ writeFileSync3(path, JSON.stringify(pairing, null, 2), { encoding: "utf-8", mode: 384 });
830
943
  return pairing;
831
944
  }
832
945
  function recordTlsPin(dataDir, asUrl, tlsSpkiPinHex) {
833
946
  const existing = readPairing(dataDir);
834
947
  if (!existing || existing.asUrl !== asUrl) return;
835
948
  const path = pairingPath(dataDir);
836
- writeFileSync2(
949
+ writeFileSync3(
837
950
  path,
838
951
  JSON.stringify({ ...existing, tlsSpkiPinHex }, null, 2),
839
952
  { encoding: "utf-8", mode: 384 }
@@ -842,25 +955,25 @@ function recordTlsPin(dataDir, asUrl, tlsSpkiPinHex) {
842
955
  function clearPairing(dataDir) {
843
956
  const path = pairingPath(dataDir);
844
957
  try {
845
- if (existsSync3(path)) unlinkSync(path);
958
+ if (existsSync4(path)) unlinkSync(path);
846
959
  } catch {
847
960
  }
848
961
  }
849
962
  function fingerprintOf(publicKeyHex) {
850
963
  const raw = Buffer.from(publicKeyHex, "hex");
851
- const digest = createHash2("sha256").update(raw).digest("hex").toUpperCase();
964
+ const digest = createHash3("sha256").update(raw).digest("hex").toUpperCase();
852
965
  return digest.match(/.{1,4}/g)?.join(":") ?? digest;
853
966
  }
854
967
 
855
968
  // src/lib/as-challenge.ts
856
969
  import { createPublicKey, randomBytes as randomBytes2, verify as cryptoVerify } from "crypto";
857
- import { canonicalize } from "@hap/core";
970
+ import { canonicalize as canonicalize2 } from "@hap/core";
858
971
 
859
972
  // src/lib/as-tls-pin.ts
860
- import { createHash as createHash3 } from "crypto";
973
+ import { createHash as createHash4 } from "crypto";
861
974
  import { checkServerIdentity as defaultCheckServerIdentity, rootCertificates } from "tls";
862
975
  import { Agent as HttpsAgent2 } from "https";
863
- import { readFileSync as readFileSync5 } from "fs";
976
+ import { readFileSync as readFileSync6 } from "fs";
864
977
  import { Agent, ProxyAgent, fetch as undiciFetch } from "undici";
865
978
 
866
979
  // src/lib/proxy-env.ts
@@ -995,7 +1108,7 @@ function effectiveCa() {
995
1108
  const path = process.env.NODE_EXTRA_CA_CERTS;
996
1109
  if (!path) return void 0;
997
1110
  try {
998
- return [...rootCertificates, readFileSync5(path, "utf-8")];
1111
+ return [...rootCertificates, readFileSync6(path, "utf-8")];
999
1112
  } catch {
1000
1113
  return void 0;
1001
1114
  }
@@ -1023,7 +1136,7 @@ function spkiSha256Hex(cert) {
1023
1136
  if (!cert.pubkey) {
1024
1137
  throw new Error("peer certificate carries no pubkey (expected the detailed certificate object)");
1025
1138
  }
1026
- return createHash3("sha256").update(cert.pubkey).digest("hex");
1139
+ return createHash4("sha256").update(cert.pubkey).digest("hex");
1027
1140
  }
1028
1141
  function formatPinFingerprint(hex) {
1029
1142
  const upper = hex.toUpperCase();
@@ -1216,7 +1329,7 @@ var CHALLENGE_DOMAIN_PREFIX = "hap-as-challenge\0";
1216
1329
  function challengeSigningBytes(unsigned) {
1217
1330
  return Buffer.concat([
1218
1331
  Buffer.from(CHALLENGE_DOMAIN_PREFIX, "utf-8"),
1219
- Buffer.from(canonicalize({ typ: unsigned.typ, nonce: unsigned.nonce, issuedAt: unsigned.issuedAt }), "utf-8")
1332
+ Buffer.from(canonicalize2({ typ: unsigned.typ, nonce: unsigned.nonce, issuedAt: unsigned.issuedAt }), "utf-8")
1220
1333
  ]);
1221
1334
  }
1222
1335
  function publicKeyFromHex(publicKeyHex) {
@@ -1299,12 +1412,12 @@ async function verifyAsHoldsKey(asUrl, publicKeyHex, pinning = { enforce: false,
1299
1412
  }
1300
1413
 
1301
1414
  // src/lib/as-config.ts
1302
- import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "fs";
1303
- import { dirname as dirname4, join as join6 } from "path";
1415
+ import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
1416
+ import { dirname as dirname5, join as join7 } from "path";
1304
1417
 
1305
1418
  // src/lib/policy.ts
1306
- import { existsSync as existsSync4, readFileSync as readFileSync6 } from "fs";
1307
- import { join as join5 } from "path";
1419
+ import { existsSync as existsSync5, readFileSync as readFileSync7 } from "fs";
1420
+ import { join as join6 } from "path";
1308
1421
  import { spawnSync } from "child_process";
1309
1422
  var POLICY_KEYS = ["AsUrl", "CaFile", "PinTls", "Proxy", "NoProxy", "Simulation", "InstallMethod", "Team"];
1310
1423
  var DEFAULT_REGISTRY_BASE_KEY = "SOFTWARE\\Policies\\Suveren\\Gateway";
@@ -1339,7 +1452,7 @@ function validateAsUrl(candidate) {
1339
1452
  function validateCaFile(candidate) {
1340
1453
  const trimmed = candidate.trim();
1341
1454
  if (!trimmed) return { ok: false, error: "The CA file path is empty." };
1342
- if (!existsSync4(trimmed)) return { ok: false, error: `No file at "${trimmed}".` };
1455
+ if (!existsSync5(trimmed)) return { ok: false, error: `No file at "${trimmed}".` };
1343
1456
  return { ok: true, path: trimmed };
1344
1457
  }
1345
1458
  function validateTeamId(candidate) {
@@ -1410,7 +1523,7 @@ function policyFilePath() {
1410
1523
  if (process.env.SUVEREN_POLICY_FILE) return process.env.SUVEREN_POLICY_FILE;
1411
1524
  if (process.platform === "win32") {
1412
1525
  const programData = process.env.ProgramData || process.env.PROGRAMDATA || "C:\\ProgramData";
1413
- return join5(programData, "Suveren", "gateway-policy.json");
1526
+ return join6(programData, "Suveren", "gateway-policy.json");
1414
1527
  }
1415
1528
  if (process.platform === "darwin") {
1416
1529
  return "/Library/Application Support/Suveren/gateway-policy.json";
@@ -1420,10 +1533,10 @@ function policyFilePath() {
1420
1533
  function readPolicyFile() {
1421
1534
  const path = policyFilePath();
1422
1535
  const source = `the policy file (${path})`;
1423
- if (!existsSync4(path)) return { values: {}, source };
1536
+ if (!existsSync5(path)) return { values: {}, source };
1424
1537
  let data;
1425
1538
  try {
1426
- data = JSON.parse(readFileSync6(path, "utf8"));
1539
+ data = JSON.parse(readFileSync7(path, "utf8"));
1427
1540
  } catch (err) {
1428
1541
  throw new Error(`Could not parse policy file ${path}: ${err instanceof Error ? err.message : String(err)}`);
1429
1542
  }
@@ -1535,14 +1648,14 @@ function readPolicy() {
1535
1648
  // src/lib/as-config.ts
1536
1649
  var DEFAULT_AS_URL = "https://www.suveren.ai";
1537
1650
  function configPath(dataDir) {
1538
- return join6(dataDir, "config.json");
1651
+ return join7(dataDir, "config.json");
1539
1652
  }
1540
1653
  function readAsConfigStrict(dataDir) {
1541
1654
  const path = configPath(dataDir);
1542
- if (!existsSync5(path)) return {};
1655
+ if (!existsSync6(path)) return {};
1543
1656
  let data;
1544
1657
  try {
1545
- data = JSON.parse(readFileSync7(path, "utf-8"));
1658
+ data = JSON.parse(readFileSync8(path, "utf-8"));
1546
1659
  } catch (err) {
1547
1660
  throw new Error(
1548
1661
  `Could not parse ${path}: ${err instanceof Error ? err.message : String(err)}`
@@ -1557,9 +1670,9 @@ function readAsConfigStrict(dataDir) {
1557
1670
  }
1558
1671
  function readAsConfig(dataDir) {
1559
1672
  const path = configPath(dataDir);
1560
- if (!existsSync5(path)) return {};
1673
+ if (!existsSync6(path)) return {};
1561
1674
  try {
1562
- const data = JSON.parse(readFileSync7(path, "utf-8"));
1675
+ const data = JSON.parse(readFileSync8(path, "utf-8"));
1563
1676
  const out = {};
1564
1677
  if (typeof data.asUrl === "string") out.asUrl = data.asUrl;
1565
1678
  if (typeof data.caFile === "string") out.caFile = data.caFile;
@@ -1573,8 +1686,8 @@ function readAsConfig(dataDir) {
1573
1686
  function writeAsConfig(dataDir, patch) {
1574
1687
  const merged = { ...readAsConfig(dataDir), ...patch };
1575
1688
  const path = configPath(dataDir);
1576
- mkdirSync3(dirname4(path), { recursive: true, mode: 448 });
1577
- writeFileSync3(path, JSON.stringify(merged, null, 2), { encoding: "utf-8", mode: 384 });
1689
+ mkdirSync4(dirname5(path), { recursive: true, mode: 448 });
1690
+ writeFileSync4(path, JSON.stringify(merged, null, 2), { encoding: "utf-8", mode: 384 });
1578
1691
  return merged;
1579
1692
  }
1580
1693
  function validateAsUrl2(candidate) {
@@ -1717,17 +1830,17 @@ var eventBus = new EventBus();
1717
1830
  import { spawn, spawnSync as spawnSync2 } from "child_process";
1718
1831
 
1719
1832
  // src/lib/notifier-icon.ts
1720
- import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, existsSync as existsSync6 } from "fs";
1721
- import { homedir as homedir2 } from "os";
1722
- import { join as join7, dirname as dirname5 } from "path";
1833
+ import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, existsSync as existsSync7 } from "fs";
1834
+ import { homedir as homedir3 } from "os";
1835
+ import { join as join8, dirname as dirname6 } from "path";
1723
1836
  var ICON_BASE64 = "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";
1724
1837
  function ensureNotifierIcon(dataDir) {
1725
1838
  try {
1726
- const dir = dataDir ?? process.env.SUVEREN_DATA_DIR ?? join7(homedir2(), ".suveren");
1727
- const path = join7(dir, "notifier-icon.png");
1728
- if (!existsSync6(path)) {
1729
- mkdirSync4(dirname5(path), { recursive: true });
1730
- writeFileSync4(path, Buffer.from(ICON_BASE64, "base64"));
1839
+ const dir = dataDir ?? process.env.SUVEREN_DATA_DIR ?? join8(homedir3(), ".suveren");
1840
+ const path = join8(dir, "notifier-icon.png");
1841
+ if (!existsSync7(path)) {
1842
+ mkdirSync5(dirname6(path), { recursive: true });
1843
+ writeFileSync5(path, Buffer.from(ICON_BASE64, "base64"));
1731
1844
  }
1732
1845
  return path;
1733
1846
  } catch {
@@ -2892,18 +3005,18 @@ async function testAIConnectivity(config) {
2892
3005
  }
2893
3006
 
2894
3007
  // src/lib/agent-brief-store.ts
2895
- import { existsSync as existsSync7, readFileSync as readFileSync8 } from "fs";
2896
- import { homedir as homedir3 } from "os";
2897
- import { join as join8 } from "path";
3008
+ import { existsSync as existsSync8, readFileSync as readFileSync9 } from "fs";
3009
+ import { homedir as homedir4 } from "os";
3010
+ import { join as join9 } from "path";
2898
3011
  var AGENT_CONTEXT_MAX_BYTES = 16 * 1024;
2899
3012
  function agentBriefPath() {
2900
- const dataDir = process.env.SUVEREN_DATA_DIR ?? join8(homedir3(), ".suveren");
2901
- return join8(dataDir, "context.md");
3013
+ const dataDir = process.env.SUVEREN_DATA_DIR ?? join9(homedir4(), ".suveren");
3014
+ return join9(dataDir, "context.md");
2902
3015
  }
2903
3016
  function readAgentBrief() {
2904
3017
  const filePath = agentBriefPath();
2905
- if (!existsSync7(filePath)) return "";
2906
- return readFileSync8(filePath, "utf-8");
3018
+ if (!existsSync8(filePath)) return "";
3019
+ return readFileSync9(filePath, "utf-8");
2907
3020
  }
2908
3021
 
2909
3022
  // src/routes/ai.ts
@@ -3256,28 +3369,28 @@ function createMCPRouter() {
3256
3369
  // src/routes/autostart.ts
3257
3370
  import { Router as Router6 } from "express";
3258
3371
  import { execFile } from "child_process";
3259
- import { existsSync as existsSync8 } from "fs";
3372
+ import { existsSync as existsSync9 } from "fs";
3260
3373
  import { dirname as pathDirname, resolve } from "path";
3261
3374
  import { fileURLToPath } from "url";
3262
3375
  import { promisify } from "util";
3263
3376
  var run = promisify(execFile);
3264
3377
  var SUPPORTED = /* @__PURE__ */ new Set(["darwin", "win32", "linux"]);
3265
3378
  var MODULE_DIR = pathDirname(fileURLToPath(import.meta.url));
3266
- function findCli(dirname10 = MODULE_DIR) {
3379
+ function findCli(dirname11 = MODULE_DIR) {
3267
3380
  const candidates = [
3268
3381
  // Bundled (the shipped layout): dist/control-plane → dist → root → bin
3269
- resolve(dirname10, "..", "..", "bin", "suveren-gateway.js"),
3382
+ resolve(dirname11, "..", "..", "bin", "suveren-gateway.js"),
3270
3383
  // Bundled, were the routes/ directory ever preserved.
3271
- resolve(dirname10, "..", "..", "..", "bin", "suveren-gateway.js"),
3384
+ resolve(dirname11, "..", "..", "..", "bin", "suveren-gateway.js"),
3272
3385
  // Dev, running from source: the repo's own bundle directory.
3273
- resolve(dirname10, "..", "..", "..", "..", "bundle", "bin", "suveren-gateway.js"),
3274
- resolve(dirname10, "..", "..", "..", "..", "..", "bundle", "bin", "suveren-gateway.js")
3386
+ resolve(dirname11, "..", "..", "..", "..", "bundle", "bin", "suveren-gateway.js"),
3387
+ resolve(dirname11, "..", "..", "..", "..", "..", "bundle", "bin", "suveren-gateway.js")
3275
3388
  // Deliberately NO process.cwd() fallback: it makes the result depend on
3276
3389
  // where the process happened to be launched from, so the same install
3277
3390
  // resolves differently between a shell and a login service — and it would
3278
3391
  // mask a genuinely missing CLI whenever the cwd happened to be the repo.
3279
3392
  ];
3280
- return candidates.find(existsSync8) ?? null;
3393
+ return candidates.find(existsSync9) ?? null;
3281
3394
  }
3282
3395
  async function cli(args) {
3283
3396
  const bin = findCli();
@@ -3430,24 +3543,24 @@ import { Router as Router9 } from "express";
3430
3543
 
3431
3544
  // src/routes/approved-intents.ts
3432
3545
  import { Router as Router8 } from "express";
3433
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync5, existsSync as existsSync9, mkdirSync as mkdirSync5 } from "fs";
3434
- import { homedir as homedir4 } from "os";
3435
- import { join as join9 } from "path";
3436
- var SUVEREN_DATA_DIR = process.env.SUVEREN_DATA_DIR ?? join9(homedir4(), ".suveren");
3437
- var FILE_PATH = join9(SUVEREN_DATA_DIR, "approved-intents.enc.json");
3546
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync6, existsSync as existsSync10, mkdirSync as mkdirSync6 } from "fs";
3547
+ import { homedir as homedir5 } from "os";
3548
+ import { join as join10 } from "path";
3549
+ var SUVEREN_DATA_DIR = process.env.SUVEREN_DATA_DIR ?? join10(homedir5(), ".suveren");
3550
+ var FILE_PATH = join10(SUVEREN_DATA_DIR, "approved-intents.enc.json");
3438
3551
  function readFile() {
3439
- if (!existsSync9(FILE_PATH)) {
3552
+ if (!existsSync10(FILE_PATH)) {
3440
3553
  return { version: 1, entries: {} };
3441
3554
  }
3442
3555
  try {
3443
- return JSON.parse(readFileSync9(FILE_PATH, "utf-8"));
3556
+ return JSON.parse(readFileSync10(FILE_PATH, "utf-8"));
3444
3557
  } catch {
3445
3558
  return { version: 1, entries: {} };
3446
3559
  }
3447
3560
  }
3448
3561
  function writeFile(data) {
3449
- mkdirSync5(SUVEREN_DATA_DIR, { recursive: true });
3450
- writeFileSync5(FILE_PATH, JSON.stringify(data, null, 2), "utf-8");
3562
+ mkdirSync6(SUVEREN_DATA_DIR, { recursive: true });
3563
+ writeFileSync6(FILE_PATH, JSON.stringify(data, null, 2), "utf-8");
3451
3564
  }
3452
3565
  function readApprovedIntents(vault2) {
3453
3566
  const file = readFile();
@@ -3579,11 +3692,11 @@ function createEvidenceExportRouter(spUrl, vault2, dataDir, lockAsTlsMismatch2)
3579
3692
  import { Router as Router10 } from "express";
3580
3693
 
3581
3694
  // src/lib/content-crypto.ts
3582
- import { createHash as createHash4 } from "crypto";
3583
- import { canonicalize as canonicalize2 } from "@hap/core";
3695
+ import { createHash as createHash5 } from "crypto";
3696
+ import { canonicalize as canonicalize3 } from "@hap/core";
3584
3697
  var CONTENT_ALG = "HPKE-X25519-HKDF-SHA256-AES256GCM";
3585
3698
  function computeProposalContentHash(toolArgs) {
3586
- return `sha256:${createHash4("sha256").update(canonicalize2(toolArgs)).digest("hex")}`;
3699
+ return `sha256:${createHash5("sha256").update(canonicalize3(toolArgs)).digest("hex")}`;
3587
3700
  }
3588
3701
  function toB64(bytes) {
3589
3702
  return Buffer.from(bytes).toString("base64");
@@ -3638,7 +3751,7 @@ async function resolveRecipientPubkeys(recipientUserIds, deps) {
3638
3751
  return { resolved, unreadableFor };
3639
3752
  }
3640
3753
  async function encryptProposalContent(toolArgs, recipientUserIds, deps) {
3641
- const canonical = canonicalize2(toolArgs);
3754
+ const canonical = canonicalize3(toolArgs);
3642
3755
  const hash = computeProposalContentHash(toolArgs);
3643
3756
  const { resolved, unreadableFor } = await resolveRecipientPubkeys(recipientUserIds, deps);
3644
3757
  if (resolved.length === 0) {
@@ -3679,13 +3792,13 @@ async function decryptProposalContent(wire, myUserId, myPrivateKey) {
3679
3792
  }
3680
3793
 
3681
3794
  // src/lib/execution-journal-reader.ts
3682
- import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
3683
- import { join as join10 } from "path";
3795
+ import { existsSync as existsSync11, readFileSync as readFileSync11 } from "fs";
3796
+ import { join as join11 } from "path";
3684
3797
  function loadJournalEntries(dataDir) {
3685
- const path = join10(dataDir, "execution-journal.json");
3686
- if (!existsSync10(path)) return [];
3798
+ const path = join11(dataDir, "execution-journal.json");
3799
+ if (!existsSync11(path)) return [];
3687
3800
  try {
3688
- const parsed = JSON.parse(readFileSync10(path, "utf-8"));
3801
+ const parsed = JSON.parse(readFileSync11(path, "utf-8"));
3689
3802
  return parsed.entries ?? [];
3690
3803
  } catch {
3691
3804
  return [];
@@ -3698,71 +3811,74 @@ function findLatestByProposal(dataDir, proposalId) {
3698
3811
 
3699
3812
  // src/routes/proposal-preview.ts
3700
3813
  var AS_FETCH_TIMEOUT_MS2 = 1e4;
3701
- function createProposalStatusRouter(spUrl, vault2, dataDir, lockAsTlsMismatch2) {
3702
- const router = Router10();
3703
- router.get("/:id/preview", async (req, res) => {
3704
- const proposalId = req.params.id;
3705
- const cookie = vault2.getSpCookie();
3706
- if (!cookie) {
3707
- res.status(401).json({ error: "No Authority Server session \u2014 sign in to preview this proposal." });
3708
- return;
3814
+ async function resolveProposalToolArgsForRoute(spUrl, vault2, dataDir, lockAsTlsMismatch2, proposalId, res) {
3815
+ const cookie = vault2.getSpCookie();
3816
+ if (!cookie) {
3817
+ res.status(401).json({ error: "No Authority Server session \u2014 sign in to preview this proposal." });
3818
+ return void 0;
3819
+ }
3820
+ let proposal;
3821
+ try {
3822
+ const controller = new AbortController();
3823
+ const timeout = setTimeout(() => controller.abort(), AS_FETCH_TIMEOUT_MS2);
3824
+ const pin = readPairing(dataDir);
3825
+ let asStatus;
3826
+ let asBody;
3827
+ try {
3828
+ const { res: asRes } = await fetchAs(
3829
+ `${spUrl}/api/proposals/${encodeURIComponent(proposalId)}`,
3830
+ { headers: { Cookie: cookie }, signal: controller.signal },
3831
+ {
3832
+ enforce: resolvePinTls(dataDir),
3833
+ pinnedSpkiHex: pin && pin.asUrl === spUrl ? pin.tlsSpkiPinHex : void 0
3834
+ }
3835
+ );
3836
+ asStatus = asRes.status;
3837
+ asBody = await asRes.json().catch(() => ({}));
3838
+ } finally {
3839
+ clearTimeout(timeout);
3840
+ }
3841
+ if (asStatus < 200 || asStatus >= 300 || !asBody.proposal) {
3842
+ res.status(asStatus >= 400 ? asStatus : 502).json({
3843
+ error: asBody.error ?? `Could not fetch proposal ${proposalId} (${asStatus})`
3844
+ });
3845
+ return void 0;
3846
+ }
3847
+ proposal = asBody.proposal;
3848
+ } catch (err) {
3849
+ if (err instanceof AsTlsMismatchError) {
3850
+ lockAsTlsMismatch2();
3851
+ res.status(502).json({ error: err.message });
3852
+ return void 0;
3709
3853
  }
3710
- let proposal;
3854
+ res.status(502).json({ error: `Authority Server unreachable: ${err instanceof Error ? err.message : String(err)}` });
3855
+ return void 0;
3856
+ }
3857
+ if (proposal.toolArgs) {
3858
+ return { tool: proposal.tool, toolArgs: proposal.toolArgs };
3859
+ }
3860
+ if (proposal.content) {
3711
3861
  try {
3712
- const controller = new AbortController();
3713
- const timeout = setTimeout(() => controller.abort(), AS_FETCH_TIMEOUT_MS2);
3714
- const pin = readPairing(dataDir);
3715
- let asStatus;
3716
- let asBody;
3717
- try {
3718
- const { res: asRes } = await fetchAs(
3719
- `${spUrl}/api/proposals/${encodeURIComponent(proposalId)}`,
3720
- { headers: { Cookie: cookie }, signal: controller.signal },
3721
- {
3722
- enforce: resolvePinTls(dataDir),
3723
- pinnedSpkiHex: pin && pin.asUrl === spUrl ? pin.tlsSpkiPinHex : void 0
3724
- }
3725
- );
3726
- asStatus = asRes.status;
3727
- asBody = await asRes.json().catch(() => ({}));
3728
- } finally {
3729
- clearTimeout(timeout);
3730
- }
3731
- if (asStatus < 200 || asStatus >= 300 || !asBody.proposal) {
3732
- res.status(asStatus >= 400 ? asStatus : 502).json({
3733
- error: asBody.error ?? `Could not fetch proposal ${proposalId} (${asStatus})`
3734
- });
3735
- return;
3736
- }
3737
- proposal = asBody.proposal;
3862
+ const kp = await loadOrGenerateKeyPair(vault2);
3863
+ const me = vault2.getSessionUser();
3864
+ const plaintext = await decryptProposalContent(proposal.content, me?.id ?? "", kp.privateKey);
3865
+ return { tool: proposal.tool, toolArgs: JSON.parse(plaintext) };
3738
3866
  } catch (err) {
3739
- if (err instanceof AsTlsMismatchError) {
3740
- lockAsTlsMismatch2();
3741
- res.status(502).json({ error: err.message });
3742
- return;
3743
- }
3744
- res.status(502).json({ error: `Authority Server unreachable: ${err instanceof Error ? err.message : String(err)}` });
3745
- return;
3746
- }
3747
- let toolArgs;
3748
- if (proposal.toolArgs) {
3749
- toolArgs = proposal.toolArgs;
3750
- } else if (proposal.content) {
3751
- try {
3752
- const kp = await loadOrGenerateKeyPair(vault2);
3753
- const me = vault2.getSessionUser();
3754
- const plaintext = await decryptProposalContent(proposal.content, me?.id ?? "", kp.privateKey);
3755
- toolArgs = JSON.parse(plaintext);
3756
- } catch (err) {
3757
- res.json({ status: "unavailable", reason: "connector_error", message: `Could not decrypt this proposal's content on this device: ${err instanceof Error ? err.message : String(err)}` });
3758
- return;
3759
- }
3760
- } else {
3761
- res.status(502).json({ error: `Proposal ${proposalId} carries neither tool_args nor content.` });
3762
- return;
3867
+ res.json({ status: "unavailable", reason: "connector_error", message: `Could not decrypt this proposal's content on this device: ${err instanceof Error ? err.message : String(err)}` });
3868
+ return void 0;
3763
3869
  }
3870
+ }
3871
+ res.status(502).json({ error: `Proposal ${proposalId} carries neither tool_args nor content.` });
3872
+ return void 0;
3873
+ }
3874
+ function createProposalStatusRouter(spUrl, vault2, dataDir, lockAsTlsMismatch2, shownSnapshotStore2) {
3875
+ const router = Router10();
3876
+ router.get("/:id/preview", async (req, res) => {
3877
+ const proposalId = req.params.id;
3878
+ const resolved = await resolveProposalToolArgsForRoute(spUrl, vault2, dataDir, lockAsTlsMismatch2, proposalId, res);
3879
+ if (!resolved) return;
3764
3880
  try {
3765
- const result = await getToolPreview({ proposalId, tool: proposal.tool, toolArgs });
3881
+ const result = await getToolPreview({ proposalId, tool: resolved.tool, toolArgs: resolved.toolArgs });
3766
3882
  if (result.status !== "ok") {
3767
3883
  res.json(result);
3768
3884
  return;
@@ -3773,6 +3889,17 @@ function createProposalStatusRouter(spUrl, vault2, dataDir, lockAsTlsMismatch2)
3773
3889
  res.status(502).json({ error: `MCP server unavailable: ${err instanceof Error ? err.message : String(err)}` });
3774
3890
  }
3775
3891
  });
3892
+ router.get("/:id/request-context", async (req, res) => {
3893
+ const proposalId = req.params.id;
3894
+ const resolved = await resolveProposalToolArgsForRoute(spUrl, vault2, dataDir, lockAsTlsMismatch2, proposalId, res);
3895
+ if (!resolved) return;
3896
+ try {
3897
+ const result = await getToolRequestContext({ tool: resolved.tool, toolArgs: resolved.toolArgs });
3898
+ res.json(result);
3899
+ } catch (err) {
3900
+ res.status(502).json({ error: `MCP server unavailable: ${err instanceof Error ? err.message : String(err)}` });
3901
+ }
3902
+ });
3776
3903
  router.get("/:id/outcome", (req, res) => {
3777
3904
  try {
3778
3905
  const entry = findLatestByProposal(dataDir, req.params.id);
@@ -3802,13 +3929,53 @@ function createProposalStatusRouter(spUrl, vault2, dataDir, lockAsTlsMismatch2)
3802
3929
  res.status(500).json({ error: `Could not read the execution journal: ${err instanceof Error ? err.message : String(err)}` });
3803
3930
  }
3804
3931
  });
3932
+ router.post("/:id/shown-snapshot", (req, res) => {
3933
+ if (!vault2.isUnlocked()) {
3934
+ res.status(401).json({ error: "Vault is locked." });
3935
+ return;
3936
+ }
3937
+ const snapshot = req.body;
3938
+ if (snapshot === void 0 || snapshot === null || typeof snapshot !== "object") {
3939
+ res.status(400).json({ error: "Request body must be the rendered snapshot (an object)." });
3940
+ return;
3941
+ }
3942
+ try {
3943
+ const shownHash = shownSnapshotStore2.save(req.params.id, snapshot, vault2);
3944
+ res.json({ shownHash });
3945
+ } catch (err) {
3946
+ res.status(500).json({ error: `Could not store the snapshot: ${err instanceof Error ? err.message : String(err)}` });
3947
+ }
3948
+ });
3949
+ router.get("/:id/shown-snapshot", (req, res) => {
3950
+ if (!vault2.isUnlocked()) {
3951
+ res.status(401).json({ error: "Vault is locked." });
3952
+ return;
3953
+ }
3954
+ try {
3955
+ const entry = shownSnapshotStore2.get(req.params.id, vault2);
3956
+ if (!entry) {
3957
+ res.json({ status: "off_device" });
3958
+ return;
3959
+ }
3960
+ const recomputed = computeShownHash(entry.snapshot);
3961
+ res.json({
3962
+ status: "ok",
3963
+ snapshot: entry.snapshot,
3964
+ shownHash: entry.shownHash,
3965
+ storedAt: entry.storedAt,
3966
+ verifiedLocally: recomputed === entry.shownHash
3967
+ });
3968
+ } catch (err) {
3969
+ res.status(500).json({ error: `Could not read the snapshot: ${err instanceof Error ? err.message : String(err)}` });
3970
+ }
3971
+ });
3805
3972
  return router;
3806
3973
  }
3807
3974
 
3808
3975
  // src/routes/evidence.ts
3809
3976
  import { Router as Router11 } from "express";
3810
3977
  import { createPublicKey as createPublicKey2, verify as verifyEd25519 } from "crypto";
3811
- import { verifyTicketSignature, encodeDidKey, canonicalize as canonicalize3 } from "@hap/core";
3978
+ import { verifyTicketSignature, encodeDidKey, canonicalize as canonicalize4 } from "@hap/core";
3812
3979
  function issuerFromPublicKeyHex(publicKeyHex) {
3813
3980
  return encodeDidKey(Buffer.from(publicKeyHex, "hex"));
3814
3981
  }
@@ -3827,7 +3994,7 @@ function verifyPreV07TicketSignature(receipt, asPublicKeyHex) {
3827
3994
  });
3828
3995
  const sig = Buffer.from(signature, "base64");
3829
3996
  if (sig.length !== 64) return false;
3830
- return verifyEd25519(null, Buffer.from(canonicalize3(signed), "utf8"), key, sig);
3997
+ return verifyEd25519(null, Buffer.from(canonicalize4(signed), "utf8"), key, sig);
3831
3998
  } catch {
3832
3999
  return false;
3833
4000
  }
@@ -4196,7 +4363,7 @@ function createInternalContentCryptoRouter(getSecret, vault2, spUrl, dataDir) {
4196
4363
 
4197
4364
  // src/lib/update-checker.ts
4198
4365
  import { execSync } from "child_process";
4199
- import { dirname as dirname6 } from "path";
4366
+ import { dirname as dirname7 } from "path";
4200
4367
  import { fileURLToPath as fileURLToPath2 } from "url";
4201
4368
  var GHCR_IMAGE = "suverenai/suveren-gateway";
4202
4369
  var NPM_PACKAGE = "@suveren/gateway";
@@ -4262,7 +4429,7 @@ function compareSemver(a, b) {
4262
4429
  return 0;
4263
4430
  }
4264
4431
  function checkDev() {
4265
- const cwd = dirname6(fileURLToPath2(import.meta.url));
4432
+ const cwd = dirname7(fileURLToPath2(import.meta.url));
4266
4433
  try {
4267
4434
  execSync("git fetch origin --quiet", { cwd, stdio: "ignore", timeout: 15e3 });
4268
4435
  const out = execSync("git rev-list HEAD..origin/main --count", {
@@ -4335,19 +4502,19 @@ data: ${data}
4335
4502
  import { Router as Router17 } from "express";
4336
4503
 
4337
4504
  // src/lib/gateway-settings.ts
4338
- import { readFileSync as readFileSync11, writeFileSync as writeFileSync6, mkdirSync as mkdirSync6 } from "fs";
4339
- import { homedir as homedir5 } from "os";
4340
- import { join as join11, dirname as dirname7 } from "path";
4505
+ import { readFileSync as readFileSync12, writeFileSync as writeFileSync7, mkdirSync as mkdirSync7 } from "fs";
4506
+ import { homedir as homedir6 } from "os";
4507
+ import { join as join12, dirname as dirname8 } from "path";
4341
4508
  var DEFAULT_SETTINGS = {
4342
4509
  desktopNotifications: true
4343
4510
  };
4344
4511
  function settingsPath(dataDir) {
4345
- const dir = dataDir ?? process.env.SUVEREN_DATA_DIR ?? join11(homedir5(), ".suveren");
4346
- return join11(dir, "settings.json");
4512
+ const dir = dataDir ?? process.env.SUVEREN_DATA_DIR ?? join12(homedir6(), ".suveren");
4513
+ return join12(dir, "settings.json");
4347
4514
  }
4348
4515
  function readSettings(dataDir) {
4349
4516
  try {
4350
- const raw = JSON.parse(readFileSync11(settingsPath(dataDir), "utf8"));
4517
+ const raw = JSON.parse(readFileSync12(settingsPath(dataDir), "utf8"));
4351
4518
  return {
4352
4519
  desktopNotifications: typeof raw.desktopNotifications === "boolean" ? raw.desktopNotifications : DEFAULT_SETTINGS.desktopNotifications
4353
4520
  };
@@ -4361,8 +4528,8 @@ function writeSettings(patch, dataDir) {
4361
4528
  ...typeof patch.desktopNotifications === "boolean" ? { desktopNotifications: patch.desktopNotifications } : {}
4362
4529
  };
4363
4530
  const path = settingsPath(dataDir);
4364
- mkdirSync6(dirname7(path), { recursive: true });
4365
- writeFileSync6(path, JSON.stringify(next, null, 2) + "\n", "utf8");
4531
+ mkdirSync7(dirname8(path), { recursive: true });
4532
+ writeFileSync7(path, JSON.stringify(next, null, 2) + "\n", "utf8");
4366
4533
  return next;
4367
4534
  }
4368
4535
 
@@ -4394,20 +4561,20 @@ function createGatewaySettingsRouter() {
4394
4561
  import { Router as Router18 } from "express";
4395
4562
 
4396
4563
  // src/lib/archived-mandates-store.ts
4397
- import { readFileSync as readFileSync12, writeFileSync as writeFileSync7, mkdirSync as mkdirSync7, renameSync } from "fs";
4398
- import { homedir as homedir6 } from "os";
4399
- import { join as join12, dirname as dirname8 } from "path";
4564
+ import { readFileSync as readFileSync13, writeFileSync as writeFileSync8, mkdirSync as mkdirSync8, renameSync as renameSync2 } from "fs";
4565
+ import { homedir as homedir7 } from "os";
4566
+ import { join as join13, dirname as dirname9 } from "path";
4400
4567
  var ID_PATTERN = /^[A-Za-z0-9_.:-]{1,200}$/;
4401
4568
  function isValidMandateId(id) {
4402
4569
  return typeof id === "string" && ID_PATTERN.test(id);
4403
4570
  }
4404
4571
  function storePath(dataDir) {
4405
- const dir = dataDir ?? process.env.SUVEREN_DATA_DIR ?? join12(homedir6(), ".suveren");
4406
- return join12(dir, "archived-mandates.json");
4572
+ const dir = dataDir ?? process.env.SUVEREN_DATA_DIR ?? join13(homedir7(), ".suveren");
4573
+ return join13(dir, "archived-mandates.json");
4407
4574
  }
4408
4575
  function readArchived(dataDir) {
4409
4576
  try {
4410
- const raw = JSON.parse(readFileSync12(storePath(dataDir), "utf8"));
4577
+ const raw = JSON.parse(readFileSync13(storePath(dataDir), "utf8"));
4411
4578
  if (!Array.isArray(raw.archived)) return [];
4412
4579
  return [...new Set(raw.archived.filter(isValidMandateId))];
4413
4580
  } catch {
@@ -4417,10 +4584,10 @@ function readArchived(dataDir) {
4417
4584
  function writeArchived(ids, dataDir) {
4418
4585
  const next = [...new Set(ids.filter(isValidMandateId))].sort();
4419
4586
  const path = storePath(dataDir);
4420
- mkdirSync7(dirname8(path), { recursive: true });
4587
+ mkdirSync8(dirname9(path), { recursive: true });
4421
4588
  const tmp = `${path}.tmp`;
4422
- writeFileSync7(tmp, JSON.stringify({ archived: next }, null, 2) + "\n", "utf8");
4423
- renameSync(tmp, path);
4589
+ writeFileSync8(tmp, JSON.stringify({ archived: next }, null, 2) + "\n", "utf8");
4590
+ renameSync2(tmp, path);
4424
4591
  return next;
4425
4592
  }
4426
4593
  function archiveMandate(id, dataDir) {
@@ -4458,7 +4625,7 @@ function createArchivedMandatesRouter(dataDir) {
4458
4625
  // src/routes/internal-events.ts
4459
4626
  import { Router as Router19 } from "express";
4460
4627
  import { timingSafeEqual as timingSafeEqual3 } from "crypto";
4461
- var ALLOWED = ["proposal-added", "action-approval-needed"];
4628
+ var ALLOWED = ["proposal-added", "action-approval-needed", "execution-failed"];
4462
4629
  var SESSION_EXPIRED = "session-expired";
4463
4630
  var AS_KEY_MISMATCH = "as-key-mismatch";
4464
4631
  var AS_TLS_MISMATCH = "as-tls-mismatch";
@@ -4720,9 +4887,13 @@ function createInternalMandateRouter(getSecret, deps) {
4720
4887
  }
4721
4888
 
4722
4889
  // src/lib/notification-dispatcher.ts
4723
- var TRIGGER_EVENTS = ["proposal-added", "action-approval-needed"];
4890
+ var REVIEW_EVENTS = ["proposal-added", "action-approval-needed"];
4891
+ var FAILURE_EVENTS = ["execution-failed"];
4892
+ var TRIGGER_EVENTS = [...REVIEW_EVENTS, ...FAILURE_EVENTS];
4724
4893
  var TITLE = "Suveren";
4725
4894
  var WAITING_MESSAGE = "Something is waiting for your review.";
4895
+ var FAILURE_MESSAGE = "An approved action failed.";
4896
+ var MIXED_MESSAGE = "Something needs your attention.";
4726
4897
  var FLOOD_THRESHOLD = 10;
4727
4898
  var floodMessage = (n) => `Unusual volume: ${n} requests in the last minute.`;
4728
4899
  var DEBOUNCE_MS = 5e3;
@@ -4733,6 +4904,9 @@ var NotificationDispatcher = class {
4733
4904
  pendingWhileCoolingDown = false;
4734
4905
  /** Events observed since the current window opened — the flood signal only. */
4735
4906
  observedInWindow = 0;
4907
+ /** Which kinds were observed since the current window opened — selects the message. */
4908
+ reviewSeenInWindow = false;
4909
+ failureSeenInWindow = false;
4736
4910
  unsubscribe = null;
4737
4911
  now;
4738
4912
  setTimeoutFn;
@@ -4750,7 +4924,8 @@ var NotificationDispatcher = class {
4750
4924
  start() {
4751
4925
  if (this.unsubscribe) return this.unsubscribe;
4752
4926
  const off = eventBus.subscribe((event) => {
4753
- if (TRIGGER_EVENTS.includes(event.type)) this.onTrigger();
4927
+ if (FAILURE_EVENTS.includes(event.type)) this.onTrigger("failure");
4928
+ else if (REVIEW_EVENTS.includes(event.type)) this.onTrigger("review");
4754
4929
  });
4755
4930
  this.unsubscribe = () => {
4756
4931
  off();
@@ -4762,9 +4937,15 @@ var NotificationDispatcher = class {
4762
4937
  };
4763
4938
  return this.unsubscribe;
4764
4939
  }
4765
- /** Exposed for tests; the bus subscription calls this. */
4766
- onTrigger() {
4940
+ /**
4941
+ * Exposed for tests; the bus subscription calls this. `kind` defaults to
4942
+ * 'review' so every existing call site/test (written before FAILURE_EVENTS
4943
+ * existed) keeps its exact behaviour.
4944
+ */
4945
+ onTrigger(kind = "review") {
4767
4946
  this.observedInWindow += 1;
4947
+ if (kind === "failure") this.failureSeenInWindow = true;
4948
+ else this.reviewSeenInWindow = true;
4768
4949
  if (this.cooldownTimer) {
4769
4950
  this.pendingWhileCoolingDown = true;
4770
4951
  return;
@@ -4777,7 +4958,11 @@ var NotificationDispatcher = class {
4777
4958
  }
4778
4959
  fire() {
4779
4960
  const count = this.observedInWindow;
4961
+ const sawReview = this.reviewSeenInWindow;
4962
+ const sawFailure = this.failureSeenInWindow;
4780
4963
  this.observedInWindow = 0;
4964
+ this.reviewSeenInWindow = false;
4965
+ this.failureSeenInWindow = false;
4781
4966
  let enabled = true;
4782
4967
  try {
4783
4968
  enabled = this.settingsFn().desktopNotifications;
@@ -4788,7 +4973,7 @@ var NotificationDispatcher = class {
4788
4973
  `[Control Plane] Review pending \u2014 desktop notification ${enabled ? "sent" : "suppressed (turned off)"}`
4789
4974
  );
4790
4975
  if (enabled) {
4791
- const message = count >= FLOOD_THRESHOLD ? floodMessage(count) : WAITING_MESSAGE;
4976
+ const message = count >= FLOOD_THRESHOLD ? floodMessage(count) : sawReview && sawFailure ? MIXED_MESSAGE : sawFailure ? FAILURE_MESSAGE : WAITING_MESSAGE;
4792
4977
  this.notifyFn(TITLE, message, process.platform, this.url);
4793
4978
  }
4794
4979
  this.cooldownTimer = this.setTimeoutFn(() => {
@@ -4872,17 +5057,17 @@ function buildSessionHealth(vault2) {
4872
5057
  }
4873
5058
 
4874
5059
  // src/lib/denials-reader.ts
4875
- import { existsSync as existsSync11, readFileSync as readFileSync13 } from "fs";
4876
- import { join as join13 } from "path";
5060
+ import { existsSync as existsSync12, readFileSync as readFileSync14 } from "fs";
5061
+ import { join as join14 } from "path";
4877
5062
  function loadDenials(dataDir, decrypt) {
4878
- const encPath = join13(dataDir, "denials.enc.json");
4879
- const plainPath = join13(dataDir, "denials.json");
4880
- if (existsSync11(encPath)) {
4881
- const { blob } = JSON.parse(readFileSync13(encPath, "utf-8"));
5063
+ const encPath = join14(dataDir, "denials.enc.json");
5064
+ const plainPath = join14(dataDir, "denials.json");
5065
+ if (existsSync12(encPath)) {
5066
+ const { blob } = JSON.parse(readFileSync14(encPath, "utf-8"));
4882
5067
  return JSON.parse(decrypt(blob)).records ?? [];
4883
5068
  }
4884
- if (existsSync11(plainPath)) {
4885
- return JSON.parse(readFileSync13(plainPath, "utf-8")).records ?? [];
5069
+ if (existsSync12(plainPath)) {
5070
+ return JSON.parse(readFileSync14(plainPath, "utf-8")).records ?? [];
4886
5071
  }
4887
5072
  return [];
4888
5073
  }
@@ -4906,11 +5091,11 @@ function isSimulationMode() {
4906
5091
  // src/index.ts
4907
5092
  (function migrateDataDir() {
4908
5093
  if (process.env.SUVEREN_DATA_DIR) return;
4909
- const oldDir = join14(homedir7(), ".hap");
4910
- const newDir = join14(homedir7(), ".suveren");
4911
- if (!existsSync12(newDir) && existsSync12(oldDir)) {
5094
+ const oldDir = join15(homedir8(), ".hap");
5095
+ const newDir = join15(homedir8(), ".suveren");
5096
+ if (!existsSync13(newDir) && existsSync13(oldDir)) {
4912
5097
  try {
4913
- renameSync2(oldDir, newDir);
5098
+ renameSync3(oldDir, newDir);
4914
5099
  console.error(`[Control Plane] Migrated data directory: ${oldDir} \u2192 ${newDir}`);
4915
5100
  } catch (err) {
4916
5101
  console.error(`[Control Plane] FATAL: could not migrate data directory ${oldDir} \u2192 ${newDir}:`, err);
@@ -4918,7 +5103,7 @@ function isSimulationMode() {
4918
5103
  }
4919
5104
  }
4920
5105
  })();
4921
- var DATA_DIR = process.env.SUVEREN_DATA_DIR ?? join14(homedir7(), ".suveren");
5106
+ var DATA_DIR = process.env.SUVEREN_DATA_DIR ?? join15(homedir8(), ".suveren");
4922
5107
  var SP_URL = resolveAsUrl(DATA_DIR);
4923
5108
  validatePinTlsForUrl(resolvePinTls(DATA_DIR), SP_URL);
4924
5109
  var POLICY_LOCKED = [...readPolicy().locked];
@@ -4926,8 +5111,9 @@ var POLICY_LOCKED_INSTALL_METHOD = readPolicy().policy.installMethod;
4926
5111
  var POLICY_LOCKED_TEAM = readPolicy().policy.team;
4927
5112
  var port = parseInt(process.env.SUVEREN_CP_PORT ?? "3402", 10);
4928
5113
  var HAP_MODE = process.env.HAP_MODE ?? "personal";
4929
- var UI_DIST = process.env.HAP_UI_DIST ?? join14(import.meta.dirname ?? __dirname, "../../ui/dist");
5114
+ var UI_DIST = process.env.HAP_UI_DIST ?? join15(import.meta.dirname ?? __dirname, "../../ui/dist");
4930
5115
  var vault = new Vault(DATA_DIR);
5116
+ var shownSnapshotStore = new ShownSnapshotStore(DATA_DIR);
4931
5117
  (function repairIfAsUrlChanged() {
4932
5118
  const pairing = readPairing(DATA_DIR);
4933
5119
  if (pairing && pairing.asUrl !== SP_URL) {
@@ -5236,7 +5422,7 @@ app.use("/api/gateway-settings", jsonParser, authGuard, createGatewaySettingsRou
5236
5422
  app.use("/api/archived-mandates", jsonParser, authGuard, createArchivedMandatesRouter());
5237
5423
  app.use("/api/encrypt-intent", jsonParser, authGuard, createEncryptIntentRouter());
5238
5424
  app.use("/api/evidence-export", authGuard, createEvidenceExportRouter(SP_URL, vault, DATA_DIR, lockAsTlsMismatch));
5239
- app.use("/proposal-status", authGuard, createProposalStatusRouter(SP_URL, vault, DATA_DIR, lockAsTlsMismatch));
5425
+ app.use("/proposal-status", jsonParser, authGuard, createProposalStatusRouter(SP_URL, vault, DATA_DIR, lockAsTlsMismatch, shownSnapshotStore));
5240
5426
  app.use("/api/evidence", authGuard, createEvidenceRouter());
5241
5427
  app.use("/api/report", jsonParser, authGuard, createReportRouter(() => RUNNING_VERSION));
5242
5428
  app.use("/api/decrypt-intent", jsonParser, authGuard, createDecryptIntentRouter(vault));
@@ -5401,7 +5587,7 @@ app.get("/as-pairing", authGuard, (_req, res) => {
5401
5587
  });
5402
5588
  app.get("/denials", authGuard, (req, res) => {
5403
5589
  try {
5404
- const dataDir = process.env.SUVEREN_DATA_DIR ?? join14(homedir7(), ".suveren");
5590
+ const dataDir = process.env.SUVEREN_DATA_DIR ?? join15(homedir8(), ".suveren");
5405
5591
  const all = loadDenials(dataDir, (blob) => vault.decrypt(blob));
5406
5592
  const since = Number(req.query.since);
5407
5593
  const limit = Number(req.query.limit);
@@ -5460,12 +5646,12 @@ app.put("/agent-brief/context", jsonParser, authGuard, async (req, res) => {
5460
5646
  res.status(413).json({ error: `Context exceeds ${AGENT_CONTEXT_MAX_BYTES}-byte cap` });
5461
5647
  return;
5462
5648
  }
5463
- const { mkdirSync: mkdirSync8, writeFileSync: writeFileSync8, renameSync: renameSync3 } = await import("fs");
5649
+ const { mkdirSync: mkdirSync9, writeFileSync: writeFileSync9, renameSync: renameSync4 } = await import("fs");
5464
5650
  const filePath = agentBriefPath();
5465
- mkdirSync8(dirname9(filePath), { recursive: true });
5651
+ mkdirSync9(dirname10(filePath), { recursive: true });
5466
5652
  const tmpPath = `${filePath}.tmp`;
5467
- writeFileSync8(tmpPath, content, "utf-8");
5468
- renameSync3(tmpPath, filePath);
5653
+ writeFileSync9(tmpPath, content, "utf-8");
5654
+ renameSync4(tmpPath, filePath);
5469
5655
  res.json({ ok: true });
5470
5656
  } catch (err) {
5471
5657
  console.error("[Control Plane] agent-brief/context PUT failed:", err);
@@ -5547,7 +5733,7 @@ app.use(
5547
5733
  function detectInstallMethod2() {
5548
5734
  return detectInstallMethod({
5549
5735
  dir: import.meta.dirname ?? __dirname,
5550
- dockerEnv: existsSync12("/.dockerenv"),
5736
+ dockerEnv: existsSync13("/.dockerenv"),
5551
5737
  managed: process.env.SUVEREN_INSTALL_METHOD === "managed" || POLICY_LOCKED_INSTALL_METHOD === "managed"
5552
5738
  });
5553
5739
  }
@@ -5557,10 +5743,10 @@ function detectRunningVersion() {
5557
5743
  return process.env.HAP_BUILD_SHA ?? "dev";
5558
5744
  }
5559
5745
  const dir = import.meta.dirname ?? __dirname;
5560
- const bundlePkg = join14(dir, "..", "..", "package.json");
5561
- if (existsSync12(bundlePkg)) {
5746
+ const bundlePkg = join15(dir, "..", "..", "package.json");
5747
+ if (existsSync13(bundlePkg)) {
5562
5748
  try {
5563
- const pkg = JSON.parse(readFileSync14(bundlePkg, "utf8"));
5749
+ const pkg = JSON.parse(readFileSync15(bundlePkg, "utf8"));
5564
5750
  if (pkg.name === "@suveren/gateway" && typeof pkg.version === "string") {
5565
5751
  return pkg.version;
5566
5752
  }
@@ -5602,7 +5788,7 @@ app.get("/health", async (req, res) => {
5602
5788
  }
5603
5789
  });
5604
5790
  });
5605
- if (existsSync12(UI_DIST)) {
5791
+ if (existsSync13(UI_DIST)) {
5606
5792
  app.use(express.static(UI_DIST, {
5607
5793
  setHeaders: (res, filePath) => {
5608
5794
  if (filePath.endsWith("index.html")) {
@@ -5612,7 +5798,7 @@ if (existsSync12(UI_DIST)) {
5612
5798
  }));
5613
5799
  app.get("*", (_req, res) => {
5614
5800
  res.setHeader("Cache-Control", "no-store, must-revalidate");
5615
- res.sendFile(join14(UI_DIST, "index.html"));
5801
+ res.sendFile(join15(UI_DIST, "index.html"));
5616
5802
  });
5617
5803
  } else {
5618
5804
  app.get("/", (_req, res) => {