@suveren/gateway 0.20.2 → 0.22.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 (28) hide show
  1. package/content/integrations/erp.json +15 -3
  2. package/dist/control-plane/index.mjs +179 -46
  3. package/dist/mcp-server/http.mjs +623 -181
  4. package/dist/ui/assets/{index-blnpvM2g.css → index-BSAkn_Of.css} +1 -1
  5. package/dist/ui/assets/index-CBoGbefI.js +116 -0
  6. package/dist/ui/index.html +2 -2
  7. package/node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition/README.md +105 -35
  8. package/node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition/dist/index.d.ts +72 -0
  9. package/node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition/dist/index.js +388 -0
  10. package/node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition/dist/index.js.map +1 -0
  11. package/node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition/package.json +31 -19
  12. package/node_modules/@modelcontextprotocol/sdk/node_modules/express/Readme.md +15 -9
  13. package/node_modules/@modelcontextprotocol/sdk/node_modules/express/lib/application.js +2 -2
  14. package/node_modules/@modelcontextprotocol/sdk/node_modules/express/lib/request.js +32 -19
  15. package/node_modules/@modelcontextprotocol/sdk/node_modules/express/lib/response.js +27 -30
  16. package/node_modules/@modelcontextprotocol/sdk/node_modules/express/node_modules/content-type/LICENSE +22 -0
  17. package/node_modules/@modelcontextprotocol/sdk/node_modules/express/node_modules/content-type/README.md +71 -0
  18. package/node_modules/@modelcontextprotocol/sdk/node_modules/express/node_modules/content-type/dist/index.d.ts +46 -0
  19. package/node_modules/@modelcontextprotocol/sdk/node_modules/express/node_modules/content-type/dist/index.js +176 -0
  20. package/node_modules/@modelcontextprotocol/sdk/node_modules/express/node_modules/content-type/dist/index.js.map +1 -0
  21. package/node_modules/@modelcontextprotocol/sdk/node_modules/express/node_modules/content-type/package.json +52 -0
  22. package/node_modules/@modelcontextprotocol/sdk/node_modules/express/package.json +10 -10
  23. package/node_modules/@types/node/README.md +1 -1
  24. package/node_modules/@types/node/package.json +2 -2
  25. package/node_modules/@types/node/util.d.ts +6 -3
  26. package/package.json +1 -1
  27. package/dist/ui/assets/index-1s1q-XLL.js +0 -116
  28. package/node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition/index.js +0 -536
@@ -1931,6 +1931,7 @@ import { createHash as createHash2 } from "crypto";
1931
1931
  var DEFAULT_DIR5 = process.env.SUVEREN_DATA_DIR ?? join8(homedir5(), ".suveren");
1932
1932
  var FILE_NAME = "execution-journal.json";
1933
1933
  var MAX_AGE_SECONDS2 = 90 * 24 * 60 * 60;
1934
+ var DETAIL_CAP = 500;
1934
1935
  function hashToolArgs(args) {
1935
1936
  return "sha256:" + createHash2("sha256").update(stableStringify(args)).digest("hex");
1936
1937
  }
@@ -1969,15 +1970,30 @@ var ExecutionJournal = class {
1969
1970
  this.persist(file);
1970
1971
  return { ok: true };
1971
1972
  }
1972
- /** Close an `intent` row. Missing row is a programming error, not a silent no-op. */
1973
- complete(ticketId, state2) {
1973
+ /**
1974
+ * Close an `intent` row. Missing row is a programming error, not a silent
1975
+ * no-op. `detail` (capped to DETAIL_CAP) is stored only alongside outcome
1976
+ * 'refused' — see JournalEntry's doc comment.
1977
+ */
1978
+ complete(ticketId, state2, outcome, detail) {
1974
1979
  const file = this.load();
1975
1980
  const row = file.entries.find((e) => e.ticketId === ticketId);
1976
1981
  if (!row) throw new Error(`execution journal: no row for ticket ${ticketId} to complete`);
1977
1982
  row.state = state2;
1983
+ if (outcome) row.outcome = outcome;
1984
+ if (detail !== void 0) {
1985
+ row.detail = detail.length > DETAIL_CAP ? detail.slice(0, DETAIL_CAP) : detail;
1986
+ }
1978
1987
  row.finishedAt = nowSec();
1979
1988
  this.persist(file);
1980
1989
  }
1990
+ /** The newest row for a review-path proposal — what really happened when it
1991
+ * ran on this gateway (the AS marks a proposal "executed" when it issues the
1992
+ * ticket, before the action runs). Undefined when it never ran here. */
1993
+ findByProposal(proposalId) {
1994
+ const rows = this.load().entries.filter((e) => e.proposalId === proposalId);
1995
+ return rows[rows.length - 1];
1996
+ }
1981
1997
  /** All rows, newest last. Diagnostic. */
1982
1998
  getAll() {
1983
1999
  return [...this.load().entries];
@@ -2037,7 +2053,8 @@ var ProposalSubmissionStore = class {
2037
2053
  executionContextHash: hashToolArgs(entry.executionContext),
2038
2054
  authorizationId: entry.authorizationId,
2039
2055
  profileId: entry.profileId,
2040
- submittedAt: nowSec2()
2056
+ submittedAt: nowSec2(),
2057
+ ...entry.previewHash ? { previewHash: entry.previewHash } : {}
2041
2058
  });
2042
2059
  this.persist(file);
2043
2060
  }
@@ -2065,6 +2082,56 @@ var ProposalSubmissionStore = class {
2065
2082
  }
2066
2083
  };
2067
2084
 
2085
+ // src/lib/preview-log.ts
2086
+ import { mkdirSync as mkdirSync8, readFileSync as readFileSync11, writeFileSync as writeFileSync8, existsSync as existsSync10, renameSync as renameSync3 } from "fs";
2087
+ import { homedir as homedir7 } from "os";
2088
+ import { dirname as dirname8, join as join10 } from "path";
2089
+ var DEFAULT_DIR7 = process.env.SUVEREN_DATA_DIR ?? join10(homedir7(), ".suveren");
2090
+ var FILE_NAME3 = "preview-log.json";
2091
+ var MAX_RECORDS2 = 500;
2092
+ var MAX_AGE_MS2 = 30 * 24 * 60 * 60 * 1e3;
2093
+ var PreviewLog = class {
2094
+ filePath;
2095
+ constructor(filePath) {
2096
+ const baseDir = filePath && filePath.endsWith(".json") ? dirname8(filePath) : filePath ?? DEFAULT_DIR7;
2097
+ this.filePath = join10(baseDir, FILE_NAME3);
2098
+ }
2099
+ /** Record one preview read. Never throws — recording must not break the read. */
2100
+ record(entry) {
2101
+ try {
2102
+ const file = this.load();
2103
+ file.entries.push(entry);
2104
+ const cutoff = entry.ts - MAX_AGE_MS2;
2105
+ file.entries = file.entries.filter((e) => e.ts >= cutoff);
2106
+ if (file.entries.length > MAX_RECORDS2) {
2107
+ file.entries = file.entries.slice(file.entries.length - MAX_RECORDS2);
2108
+ }
2109
+ this.persist(file);
2110
+ } catch (err) {
2111
+ console.error("[PreviewLog] failed to record preview read:", err);
2112
+ }
2113
+ }
2114
+ /** Diagnostic only. */
2115
+ getAll() {
2116
+ return [...this.load().entries];
2117
+ }
2118
+ load() {
2119
+ if (!existsSync10(this.filePath)) return { version: 1, entries: [] };
2120
+ try {
2121
+ const parsed = JSON.parse(readFileSync11(this.filePath, "utf-8"));
2122
+ return { version: 1, entries: parsed.entries ?? [] };
2123
+ } catch {
2124
+ return { version: 1, entries: [] };
2125
+ }
2126
+ }
2127
+ persist(file) {
2128
+ mkdirSync8(dirname8(this.filePath), { recursive: true, mode: 448 });
2129
+ const tmp = `${this.filePath}.tmp`;
2130
+ writeFileSync8(tmp, JSON.stringify(file, null, 2), { encoding: "utf-8", mode: 384 });
2131
+ renameSync3(tmp, this.filePath);
2132
+ }
2133
+ };
2134
+
2068
2135
  // src/lib/gatekeeper.ts
2069
2136
  import { verify } from "@hap/core";
2070
2137
  var MCPGatekeeper = class {
@@ -2157,9 +2224,9 @@ var MCPGatekeeper = class {
2157
2224
  };
2158
2225
 
2159
2226
  // src/lib/report/report-store.ts
2160
- import { mkdirSync as mkdirSync8, readFileSync as readFileSync11, writeFileSync as writeFileSync8, existsSync as existsSync10, unlinkSync as unlinkSync5 } from "fs";
2161
- import { homedir as homedir7 } from "os";
2162
- import { dirname as dirname8, join as join10 } from "path";
2227
+ import { mkdirSync as mkdirSync9, readFileSync as readFileSync12, writeFileSync as writeFileSync9, existsSync as existsSync11, unlinkSync as unlinkSync5 } from "fs";
2228
+ import { homedir as homedir8 } from "os";
2229
+ import { dirname as dirname9, join as join11 } from "path";
2163
2230
  import { createCipheriv as createCipheriv5, createDecipheriv as createDecipheriv5, randomBytes as randomBytes5 } from "crypto";
2164
2231
 
2165
2232
  // src/lib/report/record-resolvers.ts
@@ -2468,27 +2535,27 @@ async function buildCoverage(caseElements, getExport, archive, referenced, windo
2468
2535
  }
2469
2536
 
2470
2537
  // src/lib/report/report-store.ts
2471
- var DEFAULT_DIR7 = process.env.SUVEREN_DATA_DIR ?? join10(homedir7(), ".suveren");
2538
+ var DEFAULT_DIR8 = process.env.SUVEREN_DATA_DIR ?? join11(homedir8(), ".suveren");
2472
2539
  var ReportStore = class {
2473
2540
  report = null;
2474
2541
  baseDir;
2475
2542
  vaultKey = null;
2476
2543
  constructor(filePath) {
2477
2544
  if (filePath && filePath.endsWith(".json")) {
2478
- this.baseDir = dirname8(filePath);
2545
+ this.baseDir = dirname9(filePath);
2479
2546
  } else {
2480
- this.baseDir = filePath ?? DEFAULT_DIR7;
2547
+ this.baseDir = filePath ?? DEFAULT_DIR8;
2481
2548
  }
2482
2549
  this.loadPlaintext();
2483
2550
  }
2484
2551
  /** Set the vault key for encryption. Triggers migration from plaintext if needed. */
2485
2552
  setVaultKey(key) {
2486
2553
  this.vaultKey = key;
2487
- if (existsSync10(this.encryptedFilePath)) {
2554
+ if (existsSync11(this.encryptedFilePath)) {
2488
2555
  this.loadEncrypted();
2489
2556
  } else if (this.report !== null) {
2490
2557
  this.persistEncrypted();
2491
- if (existsSync10(this.plaintextFilePath)) {
2558
+ if (existsSync11(this.plaintextFilePath)) {
2492
2559
  try {
2493
2560
  unlinkSync5(this.plaintextFilePath);
2494
2561
  } catch {
@@ -2503,7 +2570,7 @@ var ReportStore = class {
2503
2570
  * "the report exists but is locked".
2504
2571
  */
2505
2572
  isLocked() {
2506
- return this.vaultKey === null && existsSync10(this.encryptedFilePath);
2573
+ return this.vaultKey === null && existsSync11(this.encryptedFilePath);
2507
2574
  }
2508
2575
  /** The stored, already-verified report — or null if none has been saved
2509
2576
  * (and the store is not locked; check `isLocked()` first). */
@@ -2552,34 +2619,34 @@ var ReportStore = class {
2552
2619
  }
2553
2620
  // ─── File paths ─────────────────────────────────────────────────────────
2554
2621
  get plaintextFilePath() {
2555
- return join10(this.baseDir, "report.json");
2622
+ return join11(this.baseDir, "report.json");
2556
2623
  }
2557
2624
  get encryptedFilePath() {
2558
- return join10(this.baseDir, "report.enc.json");
2625
+ return join11(this.baseDir, "report.enc.json");
2559
2626
  }
2560
2627
  // ─── Load / Persist ─────────────────────────────────────────────────────
2561
2628
  loadPlaintext() {
2562
- if (!existsSync10(this.plaintextFilePath)) {
2563
- mkdirSync8(this.baseDir, { recursive: true });
2629
+ if (!existsSync11(this.plaintextFilePath)) {
2630
+ mkdirSync9(this.baseDir, { recursive: true });
2564
2631
  return;
2565
2632
  }
2566
2633
  try {
2567
- const raw = readFileSync11(this.plaintextFilePath, "utf-8");
2634
+ const raw = readFileSync12(this.plaintextFilePath, "utf-8");
2568
2635
  const parsed = JSON.parse(raw);
2569
2636
  this.report = parsed.report ?? null;
2570
2637
  } catch {
2571
2638
  console.error(`[ReportStore] Could not parse ${this.plaintextFilePath} \u2014 preserving as .corrupt`);
2572
2639
  try {
2573
- writeFileSync8(`${this.plaintextFilePath}.corrupt-${Date.now()}`, readFileSync11(this.plaintextFilePath));
2640
+ writeFileSync9(`${this.plaintextFilePath}.corrupt-${Date.now()}`, readFileSync12(this.plaintextFilePath));
2574
2641
  } catch {
2575
2642
  }
2576
2643
  this.report = null;
2577
2644
  }
2578
2645
  }
2579
2646
  loadEncrypted() {
2580
- if (!existsSync10(this.encryptedFilePath)) return;
2647
+ if (!existsSync11(this.encryptedFilePath)) return;
2581
2648
  try {
2582
- const raw = readFileSync11(this.encryptedFilePath, "utf-8");
2649
+ const raw = readFileSync12(this.encryptedFilePath, "utf-8");
2583
2650
  const data = JSON.parse(raw);
2584
2651
  const decrypted = this.decrypt(data.blob);
2585
2652
  const parsed = JSON.parse(decrypted);
@@ -2587,7 +2654,7 @@ var ReportStore = class {
2587
2654
  } catch (err) {
2588
2655
  console.error(`[ReportStore] Could not decrypt ${this.encryptedFilePath} \u2014 preserving as .corrupt:`, err);
2589
2656
  try {
2590
- writeFileSync8(`${this.encryptedFilePath}.corrupt-${Date.now()}`, readFileSync11(this.encryptedFilePath));
2657
+ writeFileSync9(`${this.encryptedFilePath}.corrupt-${Date.now()}`, readFileSync12(this.encryptedFilePath));
2591
2658
  } catch {
2592
2659
  }
2593
2660
  this.report = null;
@@ -2602,16 +2669,16 @@ var ReportStore = class {
2602
2669
  }
2603
2670
  persistPlaintext() {
2604
2671
  const data = { version: 1, report: this.report };
2605
- mkdirSync8(this.baseDir, { recursive: true, mode: 448 });
2606
- writeFileSync8(this.plaintextFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
2672
+ mkdirSync9(this.baseDir, { recursive: true, mode: 448 });
2673
+ writeFileSync9(this.plaintextFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
2607
2674
  }
2608
2675
  persistEncrypted() {
2609
2676
  const data = {
2610
2677
  version: 1,
2611
2678
  blob: this.encrypt(JSON.stringify({ version: 1, report: this.report }))
2612
2679
  };
2613
- mkdirSync8(this.baseDir, { recursive: true, mode: 448 });
2614
- writeFileSync8(this.encryptedFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
2680
+ mkdirSync9(this.baseDir, { recursive: true, mode: 448 });
2681
+ writeFileSync9(this.encryptedFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
2615
2682
  }
2616
2683
  };
2617
2684
 
@@ -2632,6 +2699,8 @@ var SharedState = class {
2632
2699
  /** What THIS gateway submitted for each review-mode proposal it created —
2633
2700
  * see ticket-verify.ts / commitments.ts. */
2634
2701
  proposalSubmissions;
2702
+ /** AU3 — local, content-free record of preview reads. See preview-log.ts. */
2703
+ previewLog;
2635
2704
  gatekeeper;
2636
2705
  /**
2637
2706
  * V7 — set by {@link checkAsCompat} (called once at process start, and
@@ -2665,6 +2734,7 @@ var SharedState = class {
2665
2734
  this.reportStore = new ReportStore(gateStorePath);
2666
2735
  this.executionJournal = new ExecutionJournal(gateStorePath);
2667
2736
  this.proposalSubmissions = new ProposalSubmissionStore(gateStorePath);
2737
+ this.previewLog = new PreviewLog(gateStorePath);
2668
2738
  this.gatekeeper = new MCPGatekeeper(this.cache);
2669
2739
  }
2670
2740
  /**
@@ -2853,29 +2923,29 @@ function formatConsumptionFull(entries) {
2853
2923
  }
2854
2924
 
2855
2925
  // src/lib/context-loader.ts
2856
- import { readFileSync as readFileSync12, existsSync as existsSync11, mkdirSync as mkdirSync9, writeFileSync as writeFileSync9, renameSync as renameSync3 } from "fs";
2857
- import { homedir as homedir8 } from "os";
2858
- import { join as join11, dirname as dirname9 } from "path";
2859
- var DEFAULT_DIR8 = process.env.SUVEREN_DATA_DIR ?? join11(homedir8(), ".suveren");
2926
+ import { readFileSync as readFileSync13, existsSync as existsSync12, mkdirSync as mkdirSync10, writeFileSync as writeFileSync10, renameSync as renameSync4 } from "fs";
2927
+ import { homedir as homedir9 } from "os";
2928
+ import { join as join12, dirname as dirname10 } from "path";
2929
+ var DEFAULT_DIR9 = process.env.SUVEREN_DATA_DIR ?? join12(homedir9(), ".suveren");
2860
2930
  var CONTEXT_MAX_BYTES = 16 * 1024;
2861
2931
  function contextFilePath(dataDir2) {
2862
- return join11(dataDir2 ?? DEFAULT_DIR8, "context.md");
2932
+ return join12(dataDir2 ?? DEFAULT_DIR9, "context.md");
2863
2933
  }
2864
2934
  function writeContextFile(content, dataDir2) {
2865
2935
  const bytes = Buffer.byteLength(content, "utf-8");
2866
2936
  if (bytes > CONTEXT_MAX_BYTES) throw new Error(`the brief is ${bytes} bytes; the limit is ${CONTEXT_MAX_BYTES}.`);
2867
2937
  const filePath = contextFilePath(dataDir2);
2868
- mkdirSync9(dirname9(filePath), { recursive: true });
2938
+ mkdirSync10(dirname10(filePath), { recursive: true });
2869
2939
  const tmpPath = `${filePath}.tmp`;
2870
- writeFileSync9(tmpPath, content, "utf-8");
2871
- renameSync3(tmpPath, filePath);
2940
+ writeFileSync10(tmpPath, content, "utf-8");
2941
+ renameSync4(tmpPath, filePath);
2872
2942
  }
2873
2943
  var BRIEF_MAX_CHARS = 16 * 1024;
2874
2944
  function readContextFile(dataDir2) {
2875
2945
  const filePath = contextFilePath(dataDir2);
2876
- if (!existsSync11(filePath)) return null;
2946
+ if (!existsSync12(filePath)) return null;
2877
2947
  try {
2878
- const content = readFileSync12(filePath, "utf-8").trim();
2948
+ const content = readFileSync13(filePath, "utf-8").trim();
2879
2949
  return content || null;
2880
2950
  } catch {
2881
2951
  return null;
@@ -3061,6 +3131,252 @@ function attachTicketId(tool, args, ticketId) {
3061
3131
  return { ...args, ticket_id: ticketId };
3062
3132
  }
3063
3133
 
3134
+ // src/lib/preview.ts
3135
+ import { computeContentHash as computeContentHash2 } from "@hap/core";
3136
+
3137
+ // src/lib/manifest-loader.ts
3138
+ import { readFileSync as readFileSync14, existsSync as existsSync13 } from "fs";
3139
+ import { join as join13, resolve } from "path";
3140
+ var EXACT_SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
3141
+ function isExactSemver(value) {
3142
+ return typeof value === "string" && EXACT_SEMVER_RE.test(value);
3143
+ }
3144
+ function invalidNpmPinReason(manifest) {
3145
+ if (!manifest.npmPackage) return null;
3146
+ if (manifest.npmVersion === void 0) {
3147
+ return `npmPackage "${manifest.npmPackage}" is set but npmVersion is missing \u2014 every connector manifest must pin an exact tested version`;
3148
+ }
3149
+ if (!isExactSemver(manifest.npmVersion)) {
3150
+ return `npmVersion ${JSON.stringify(manifest.npmVersion)} for "${manifest.npmPackage}" is not an exact semver (e.g. "1.2.3") \u2014 ranges, dist-tags like "latest", git specs, URLs, and file paths are not allowed`;
3151
+ }
3152
+ return null;
3153
+ }
3154
+ function invalidPreviewReason(toolName, entry) {
3155
+ if (entry === null || typeof entry !== "object") return null;
3156
+ const preview = entry.preview;
3157
+ if (preview === void 0) return null;
3158
+ if (preview === null || typeof preview !== "object" || Array.isArray(preview)) {
3159
+ return `override "${toolName}".preview must be an object`;
3160
+ }
3161
+ const p = preview;
3162
+ if (typeof p.tool !== "string" || p.tool.length === 0) {
3163
+ return `override "${toolName}".preview.tool must be a non-empty string`;
3164
+ }
3165
+ if (p.args === void 0 || p.args === null || typeof p.args !== "object" || Array.isArray(p.args)) {
3166
+ return `override "${toolName}".preview.args must be an object mapping preview-tool arg names to action arg names`;
3167
+ }
3168
+ for (const [argName, source] of Object.entries(p.args)) {
3169
+ if (typeof source !== "string" || source.length === 0) {
3170
+ return `override "${toolName}".preview.args.${argName} must name a non-empty source argument (string)`;
3171
+ }
3172
+ }
3173
+ if (p.version !== void 0) {
3174
+ if (p.version === null || typeof p.version !== "object" || Array.isArray(p.version)) {
3175
+ return `override "${toolName}".preview.version must be an object`;
3176
+ }
3177
+ const v = p.version;
3178
+ if (typeof v.arg !== "string" || v.arg.length === 0) {
3179
+ return `override "${toolName}".preview.version.arg must be a non-empty string`;
3180
+ }
3181
+ if (typeof v.field !== "string" || v.field.length === 0) {
3182
+ return `override "${toolName}".preview.version.field must be a non-empty string`;
3183
+ }
3184
+ }
3185
+ if (p.fields !== void 0) {
3186
+ if (!Array.isArray(p.fields)) {
3187
+ return `override "${toolName}".preview.fields must be an array of field names`;
3188
+ }
3189
+ for (const [i, f] of p.fields.entries()) {
3190
+ if (typeof f !== "string" || f.length === 0) {
3191
+ return `override "${toolName}".preview.fields[${i}] must be a non-empty string`;
3192
+ }
3193
+ }
3194
+ }
3195
+ return null;
3196
+ }
3197
+ function invalidManifestPreviewReason(manifest) {
3198
+ const overrides = manifest.toolGating?.overrides ?? {};
3199
+ for (const [toolName, entry] of Object.entries(overrides)) {
3200
+ const reason = invalidPreviewReason(toolName, entry);
3201
+ if (reason) return reason;
3202
+ }
3203
+ return null;
3204
+ }
3205
+ var manifests = /* @__PURE__ */ new Map();
3206
+ function loadManifests(integrationsDir) {
3207
+ const dir = resolve(
3208
+ integrationsDir ?? process.env.SUVEREN_MANIFESTS_DIR ?? join13(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "content", "integrations")
3209
+ );
3210
+ const indexPath = join13(dir, "index.json");
3211
+ if (!existsSync13(indexPath)) {
3212
+ console.error(`[ManifestLoader] No index.json found at ${indexPath}, skipping manifest loading`);
3213
+ return 0;
3214
+ }
3215
+ let index;
3216
+ try {
3217
+ index = JSON.parse(readFileSync14(indexPath, "utf-8"));
3218
+ } catch (err) {
3219
+ console.error(`[ManifestLoader] Failed to parse ${indexPath}:`, err);
3220
+ return 0;
3221
+ }
3222
+ let loaded = 0;
3223
+ for (const [id, relativePath] of Object.entries(index.integrations)) {
3224
+ const manifestPath = join13(dir, relativePath);
3225
+ try {
3226
+ const manifest = JSON.parse(readFileSync14(manifestPath, "utf-8"));
3227
+ const pinReason = invalidNpmPinReason(manifest);
3228
+ if (pinReason) {
3229
+ console.error(`[ManifestLoader] Refusing manifest ${id} (${manifestPath}): ${pinReason}`);
3230
+ continue;
3231
+ }
3232
+ const previewReason = invalidManifestPreviewReason(manifest);
3233
+ if (previewReason) {
3234
+ console.error(`[ManifestLoader] Refusing manifest ${id} (${manifestPath}): ${previewReason}`);
3235
+ continue;
3236
+ }
3237
+ manifests.set(id, manifest);
3238
+ loaded++;
3239
+ } catch (err) {
3240
+ console.error(`[ManifestLoader] Failed to load manifest ${id} from ${manifestPath}:`, err);
3241
+ }
3242
+ }
3243
+ console.error(`[ManifestLoader] Loaded ${loaded} integration manifest(s) from ${dir}`);
3244
+ return loaded;
3245
+ }
3246
+ function getManifest(id) {
3247
+ return manifests.get(id);
3248
+ }
3249
+ function getAllManifests() {
3250
+ return Array.from(manifests.values());
3251
+ }
3252
+
3253
+ // src/lib/preview.ts
3254
+ var PREVIEW_READ_TIMEOUT_MS = 5e3;
3255
+ var PREVIEW_MESSAGE_CAP = 500;
3256
+ function capText(text, max = PREVIEW_MESSAGE_CAP) {
3257
+ if (text === void 0) return void 0;
3258
+ return text.length > max ? text.slice(0, max) : text;
3259
+ }
3260
+ function getToolPreviewConfig(integrationId, toolName) {
3261
+ const manifest = getManifest(integrationId);
3262
+ const overrides = manifest?.toolGating?.overrides ?? {};
3263
+ const entry = overrides[toolName];
3264
+ return entry?.preview;
3265
+ }
3266
+ function mapPreviewArgs(preview, actionArgs) {
3267
+ const mapped = {};
3268
+ for (const [targetArg, sourceArg] of Object.entries(preview.args)) {
3269
+ if (sourceArg in actionArgs) mapped[targetArg] = actionArgs[sourceArg];
3270
+ }
3271
+ return mapped;
3272
+ }
3273
+ var PreviewTimeoutError = class extends Error {
3274
+ };
3275
+ function withTimeout(p, ms) {
3276
+ let timer;
3277
+ const timeout = new Promise((_, reject) => {
3278
+ timer = setTimeout(() => reject(new PreviewTimeoutError("preview read timed out")), ms);
3279
+ });
3280
+ return Promise.race([p, timeout]).finally(() => clearTimeout(timer));
3281
+ }
3282
+ async function readPreview(integrationManager2, integrationId, previewTool, args) {
3283
+ const discovered = integrationManager2.getAllTools().find((t) => t.integrationId === integrationId && t.originalName === previewTool);
3284
+ if (!discovered) {
3285
+ return { status: "unavailable", reason: "no_connector" };
3286
+ }
3287
+ try {
3288
+ const result = await withTimeout(
3289
+ integrationManager2.callTool(integrationId, previewTool, args),
3290
+ PREVIEW_READ_TIMEOUT_MS
3291
+ );
3292
+ const text = result.content?.[0]?.text;
3293
+ if (result.isError) {
3294
+ return { status: "not_found", message: capText(text) };
3295
+ }
3296
+ return {
3297
+ status: "ok",
3298
+ body: {
3299
+ ...result.structuredContent !== void 0 ? { structured: result.structuredContent } : {},
3300
+ ...discovered.outputSchema ? { outputSchema: discovered.outputSchema } : {},
3301
+ ...text !== void 0 ? { text } : {}
3302
+ }
3303
+ };
3304
+ } catch (err) {
3305
+ if (err instanceof PreviewTimeoutError) {
3306
+ return { status: "unavailable", reason: "timeout" };
3307
+ }
3308
+ return {
3309
+ status: "unavailable",
3310
+ reason: "connector_error",
3311
+ message: capText(err instanceof Error ? err.message : String(err))
3312
+ };
3313
+ }
3314
+ }
3315
+ function hashPreviewBody(body) {
3316
+ if (body.structured !== void 0) {
3317
+ return computeContentHash2({ kind: "jcs", version: "1" }, body.structured);
3318
+ }
3319
+ return computeContentHash2({ kind: "text", version: "1" }, body.text ?? "");
3320
+ }
3321
+ async function computeSubmissionPreviewHash(integrationManager2, integrationId, toolName, actionArgs) {
3322
+ const preview = getToolPreviewConfig(integrationId, toolName);
3323
+ if (!preview || preview.version) return void 0;
3324
+ const mapped = mapPreviewArgs(preview, actionArgs);
3325
+ const read = await readPreview(integrationManager2, integrationId, preview.tool, mapped);
3326
+ if (read.status !== "ok") return void 0;
3327
+ return hashPreviewBody(read.body);
3328
+ }
3329
+ async function previewChangedSinceSubmission(integrationManager2, integrationId, toolName, actionArgs, previousHash) {
3330
+ const preview = getToolPreviewConfig(integrationId, toolName);
3331
+ if (!preview) return true;
3332
+ const mapped = mapPreviewArgs(preview, actionArgs);
3333
+ const read = await readPreview(integrationManager2, integrationId, preview.tool, mapped);
3334
+ if (read.status !== "ok") return true;
3335
+ return hashPreviewBody(read.body) !== previousHash;
3336
+ }
3337
+ async function buildInternalPreview(integrationManager2, integrationId, toolName, actionArgs) {
3338
+ const preview = getToolPreviewConfig(integrationId, toolName);
3339
+ if (!preview) return { status: "none" };
3340
+ const mappedArgs = mapPreviewArgs(preview, actionArgs);
3341
+ const read1 = await readPreview(integrationManager2, integrationId, preview.tool, mappedArgs);
3342
+ if (read1.status !== "ok") return read1;
3343
+ const ok = (extra = {}) => ({
3344
+ status: "ok",
3345
+ integration: integrationId,
3346
+ tool: preview.tool,
3347
+ body: read1.body,
3348
+ ...preview.fields ? { fields: preview.fields } : {},
3349
+ ...extra
3350
+ });
3351
+ if (!preview.version) {
3352
+ return ok();
3353
+ }
3354
+ const approved = actionArgs[preview.version.arg];
3355
+ if (typeof approved !== "string" && typeof approved !== "number") {
3356
+ return ok();
3357
+ }
3358
+ const { [preview.version.arg]: _omit, ...withoutVersion } = actionArgs;
3359
+ const mappedCurrentArgs = mapPreviewArgs(preview, withoutVersion);
3360
+ const read2 = await readPreview(integrationManager2, integrationId, preview.tool, mappedCurrentArgs);
3361
+ if (read2.status !== "ok" || read2.body.structured === void 0) {
3362
+ return ok();
3363
+ }
3364
+ const current = read2.body.structured[preview.version.field];
3365
+ if (typeof current !== "string" && typeof current !== "number") {
3366
+ return ok();
3367
+ }
3368
+ const stale = String(current) !== String(approved);
3369
+ return ok({
3370
+ version: {
3371
+ field: preview.version.field,
3372
+ approved,
3373
+ current,
3374
+ stale,
3375
+ ...stale ? { currentBody: read2.body } : {}
3376
+ }
3377
+ });
3378
+ }
3379
+
3064
3380
  // src/lib/ticket-verify.ts
3065
3381
  import { verifyTicketSignature } from "@hap/core";
3066
3382
  var TicketBindingMismatchError = class extends Error {
@@ -3563,6 +3879,7 @@ function denyRead(state2, tool, reason, detail, target) {
3563
3879
  tool: tool.originalName,
3564
3880
  integrationId: tool.integrationId,
3565
3881
  profile: tool.gating?.profile ?? null,
3882
+ kind: "read",
3566
3883
  reason,
3567
3884
  detail,
3568
3885
  target
@@ -3571,15 +3888,62 @@ function denyRead(state2, tool, reason, detail, target) {
3571
3888
  }
3572
3889
  return { content: [{ type: "text", text: `Read blocked by Gatekeeper: ${detail}` }], isError: true };
3573
3890
  }
3891
+ function recordBlocked(state2, tool, rec) {
3892
+ state2.denialLog?.record({
3893
+ ts: Date.now(),
3894
+ tool: tool.originalName,
3895
+ integrationId: tool.integrationId,
3896
+ profile: tool.gating?.profile ?? null,
3897
+ ...rec
3898
+ });
3899
+ }
3900
+ function recordGatekeeperRefusal(state2, tool, refusals) {
3901
+ if (refusals.length === 0) return;
3902
+ const pick = refusals.find((r) => r.bound !== void 0 && r.actual !== void 0) ?? refusals[0];
3903
+ const scoped = pick.field ? Boolean(getProfile2(pick.profileId)?.scopeSchema?.fields?.[pick.field]) : false;
3904
+ const asValueOrString = (v) => typeof v === "number" || typeof v === "string" ? v : void 0;
3905
+ recordBlocked(state2, tool, {
3906
+ kind: scoped ? "scope" : "bound",
3907
+ detail: "Refused locally by the Gatekeeper.",
3908
+ mandateId: pick.authorizationId,
3909
+ field: pick.field,
3910
+ value: asValueOrString(pick.actual),
3911
+ limit: asValueOrString(pick.bound),
3912
+ who: "gateway",
3913
+ code: pick.code
3914
+ });
3915
+ }
3916
+ function recordAsRefusal(state2, tool, authorizationId, err) {
3917
+ const code = typeof err?.code === "string" ? err.code : void 0;
3918
+ const asValueOrString = (v) => typeof v === "number" || typeof v === "string" ? v : void 0;
3919
+ recordBlocked(state2, tool, {
3920
+ kind: code === "CUMULATIVE_LIMIT_EXCEEDED" ? "cumulative" : code === "BOUND_EXCEEDED" ? "bound" : "not_authorized",
3921
+ detail: "Refused by the Authority Server.",
3922
+ mandateId: authorizationId,
3923
+ field: typeof err?.field === "string" ? err.field : void 0,
3924
+ value: asValueOrString(err?.actual),
3925
+ limit: asValueOrString(err?.expected),
3926
+ who: "authority-server",
3927
+ code
3928
+ });
3929
+ }
3574
3930
  function createGatedToolHandler(tool, integrationManager2, state2) {
3575
3931
  if (isSimulationMode() && !integrationManager2.isSimulationSafe(tool.integrationId)) {
3576
- return async () => ({
3577
- content: [{
3578
- type: "text",
3579
- text: `Refused: "${tool.namespacedName}" is not available. No ticket was requested.`
3580
- }],
3581
- isError: true
3582
- });
3932
+ return async () => {
3933
+ recordBlocked(state2, tool, {
3934
+ kind: "simulation",
3935
+ detail: "Real systems are blocked while simulation mode is on.",
3936
+ who: "gateway",
3937
+ code: "SIMULATION_MODE"
3938
+ });
3939
+ return {
3940
+ content: [{
3941
+ type: "text",
3942
+ text: `Refused: "${tool.namespacedName}" is not available. No ticket was requested.`
3943
+ }],
3944
+ isError: true
3945
+ };
3946
+ };
3583
3947
  }
3584
3948
  const gated = createGatedToolHandlerInner(tool, integrationManager2, state2);
3585
3949
  const gateable = !!tool.gating?.profile && tool.gating.category !== "disabled";
@@ -3642,6 +4006,15 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
3642
4006
  };
3643
4007
  const noEligibleMandate = (refusals) => {
3644
4008
  const reasons = [...new Set(refusals)].join(" ");
4009
+ recordBlocked(state2, tool, {
4010
+ kind: "not_authorized",
4011
+ // Fixed text only: `reasons` can name resources or correspondents (read
4012
+ // refusals), and a blocked-action record carries no content.
4013
+ detail: `No active authorization matching profile "${profile}".`,
4014
+ field: profile,
4015
+ who: "gateway",
4016
+ code: "NO_MATCHING_MANDATE"
4017
+ });
3645
4018
  return {
3646
4019
  content: [{
3647
4020
  type: "text",
@@ -3847,6 +4220,13 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
3847
4220
  }
3848
4221
  const declaredActionType = typeof execution.action_type === "string" && execution.action_type.length > 0 ? execution.action_type : void 0;
3849
4222
  if (!declaredActionType) {
4223
+ recordBlocked(state2, tool, {
4224
+ kind: "bound",
4225
+ detail: `${tool.namespacedName} declares no action_type in its manifest's staticExecution.`,
4226
+ field: "action_type",
4227
+ who: "gateway",
4228
+ code: "MISSING_ACTION_TYPE"
4229
+ });
3850
4230
  return {
3851
4231
  content: [{
3852
4232
  type: "text",
@@ -3859,6 +4239,14 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
3859
4239
  const profileDef = getProfile2(profile);
3860
4240
  const registry = profileDef?.boundsSchema?.actionTypes;
3861
4241
  if (Array.isArray(registry) && registry.length > 0 && !registry.includes(declaredActionType)) {
4242
+ recordBlocked(state2, tool, {
4243
+ kind: "bound",
4244
+ detail: `action_type "${declaredActionType}" is not in profile ${profile}'s actionTypes registry.`,
4245
+ field: "action_type",
4246
+ value: declaredActionType,
4247
+ who: "gateway",
4248
+ code: "INVALID_ACTION_TYPE"
4249
+ });
3862
4250
  return {
3863
4251
  content: [{
3864
4252
  type: "text",
@@ -3872,6 +4260,7 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
3872
4260
  if (matchingAuths.length === 0) return noEligibleMandate(refusals);
3873
4261
  const errors = [];
3874
4262
  const passers = [];
4263
+ const structuredRefusals = [];
3875
4264
  for (const candidate of matchingAuths) {
3876
4265
  const { result } = await state2.gatekeeper.verifyExecution(candidate.authorizationId, execution, {
3877
4266
  bounds: candidate.bounds,
@@ -3880,6 +4269,13 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
3880
4269
  const capped = result.approved ? zeroCappedBound(candidate.profileId, candidate.bounds ?? candidate.frame, typeof execution.action_type === "string" ? execution.action_type : void 0) : null;
3881
4270
  if (result.approved && capped) {
3882
4271
  errors.push(`${candidate.path}: ${capped} is 0 \u2014 this mandate does not allow "${execution.action_type}"`);
4272
+ structuredRefusals.push({
4273
+ authorizationId: candidate.authorizationId,
4274
+ profileId: candidate.profileId,
4275
+ code: "BOUND_EXCEEDED",
4276
+ field: typeof execution.action_type === "string" ? execution.action_type : void 0,
4277
+ bound: 0
4278
+ });
3883
4279
  } else if (result.approved) {
3884
4280
  passers.push(candidate);
3885
4281
  } else {
@@ -3890,9 +4286,20 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
3890
4286
  return `${candidate.path}: ${e.message}`;
3891
4287
  });
3892
4288
  errors.push(...reasons);
4289
+ for (const e of result.errors) {
4290
+ structuredRefusals.push({
4291
+ authorizationId: candidate.authorizationId,
4292
+ profileId: candidate.profileId,
4293
+ code: e.code,
4294
+ field: e.field,
4295
+ bound: e.bound,
4296
+ actual: e.actual
4297
+ });
4298
+ }
3893
4299
  }
3894
4300
  }
3895
4301
  if (passers.length === 0) {
4302
+ recordGatekeeperRefusal(state2, tool, structuredRefusals);
3896
4303
  return {
3897
4304
  content: [{
3898
4305
  type: "text",
@@ -3943,13 +4350,20 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
3943
4350
  toolArgs: enrichedArgs,
3944
4351
  executionContext: wireExecCtx
3945
4352
  });
4353
+ const previewHash = await computeSubmissionPreviewHash(
4354
+ integrationManager2,
4355
+ tool.integrationId,
4356
+ tool.originalName,
4357
+ enrichedArgs
4358
+ );
3946
4359
  state2.proposalSubmissions.record({
3947
4360
  proposalId: proposal.id,
3948
4361
  tool: tool.namespacedName,
3949
4362
  toolArgs: enrichedArgs,
3950
4363
  executionContext: wireExecCtx,
3951
4364
  authorizationId: authzId,
3952
- profileId: auth.profileId
4365
+ profileId: auth.profileId,
4366
+ previewHash
3953
4367
  });
3954
4368
  return {
3955
4369
  content: [{
@@ -4044,6 +4458,7 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
4044
4458
  state2.cache.markNeedsReapproval(auth.authorizationId);
4045
4459
  const remaining = candidates.filter((c) => c.authorizationId !== auth.authorizationId);
4046
4460
  if (remaining.length === 0) {
4461
+ recordAsRefusal(state2, tool, auth.authorizationId, { code: "VERSION_UNSUPPORTED" });
4047
4462
  return {
4048
4463
  content: [{
4049
4464
  type: "text",
@@ -4059,9 +4474,11 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
4059
4474
  }
4060
4475
  if (err instanceof SPReceiptError && isStaleMandateRefusal(err)) {
4061
4476
  state2.cache.invalidate(auth.authorizationId);
4062
- const code = err.body?.errors?.[0]?.code ?? "unknown";
4477
+ const staleErr = err.body?.errors?.[0];
4478
+ const code = staleErr?.code ?? "unknown";
4063
4479
  const remaining = candidates.filter((c) => c.authorizationId !== auth.authorizationId);
4064
4480
  if (remaining.length === 0) {
4481
+ recordAsRefusal(state2, tool, auth.authorizationId, staleErr);
4065
4482
  console.error(
4066
4483
  `[Suveren MCP] fallback(${tool.namespacedName}): mandate ${auth.authorizationId} invalid (${code}) \u2014 no remaining candidates`
4067
4484
  );
@@ -4112,13 +4529,20 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
4112
4529
  executionContext: wireExecCtx,
4113
4530
  pendingApprovers: uniqueApprovers
4114
4531
  });
4532
+ const previewHash = await computeSubmissionPreviewHash(
4533
+ integrationManager2,
4534
+ tool.integrationId,
4535
+ tool.originalName,
4536
+ enrichedArgs
4537
+ );
4115
4538
  state2.proposalSubmissions.record({
4116
4539
  proposalId: proposal.id,
4117
4540
  tool: tool.namespacedName,
4118
4541
  toolArgs: enrichedArgs,
4119
4542
  executionContext: wireExecCtx,
4120
4543
  authorizationId: authzId,
4121
- profileId: auth.profileId
4544
+ profileId: auth.profileId,
4545
+ previewHash
4122
4546
  });
4123
4547
  return {
4124
4548
  content: [{
@@ -4141,6 +4565,7 @@ Proposal ID: ${proposal.id}. Use check-pending-commitments to track status.`
4141
4565
  }
4142
4566
  }
4143
4567
  if (err instanceof SPReceiptError && err.statusCode === 422) {
4568
+ recordAsRefusal(state2, tool, auth.authorizationId, err.body?.errors?.[0]);
4144
4569
  return {
4145
4570
  content: [{ type: "text", text: `Action blocked: ${err.message} (hard team ceiling \u2014 contact the team admin)` }],
4146
4571
  isError: true
@@ -4151,6 +4576,7 @@ Proposal ID: ${proposal.id}. Use check-pending-commitments to track status.`
4151
4576
  if (errors403?.[0]?.code === "MANDATE_REVOKED") {
4152
4577
  state2.cache.invalidate(auth.authorizationId);
4153
4578
  }
4579
+ recordAsRefusal(state2, tool, auth.authorizationId, errors403?.[0]);
4154
4580
  return {
4155
4581
  content: [{ type: "text", text: `Blocked by SP: ${err.message}` }],
4156
4582
  isError: true
@@ -4798,6 +5224,8 @@ async function executeCommitted(proposal, state2, integrationManager2) {
4798
5224
  if (err instanceof SPReceiptError) {
4799
5225
  const code = err.body.errors?.[0]?.code;
4800
5226
  if (code === "PROPOSAL_ALREADY_EXECUTED") {
5227
+ const local = localOutcomeOfExecuted(proposal, state2);
5228
+ if (local) return local;
4801
5229
  return {
4802
5230
  text: `Proposal ${proposal.id} has already been executed by another request.`
4803
5231
  };
@@ -4849,6 +5277,22 @@ async function executeCommitted(proposal, state2, integrationManager2) {
4849
5277
  `[Suveren MCP] Proposal ${proposal.id}: ticket ${receiptId} was replayed by the AS with no local execution record \u2014 executing once (lost-response recovery).`
4850
5278
  );
4851
5279
  }
5280
+ if (submitted.previewHash) {
5281
+ const changed = await previewChangedSinceSubmission(
5282
+ integrationManager2,
5283
+ integrationId,
5284
+ toolName,
5285
+ proposal.toolArgs,
5286
+ submitted.previewHash
5287
+ );
5288
+ if (changed) {
5289
+ state2.executionJournal.complete(receiptId, "failed", "changed");
5290
+ return {
5291
+ text: `Proposal ${proposal.id} was approved, but the record changed in ${integrationId} since it was requested \u2014 nothing was done. A new attempt needs a new request and approval.`,
5292
+ isError: true
5293
+ };
5294
+ }
5295
+ }
4852
5296
  try {
4853
5297
  let outgoingArgs = proposal.toolArgs;
4854
5298
  if (discovered && receiptId) {
@@ -4865,6 +5309,15 @@ async function executeCommitted(proposal, state2, integrationManager2) {
4865
5309
  state2.executionJournal.complete(receiptId, "failed");
4866
5310
  throw err;
4867
5311
  }
5312
+ const resultText = result.content?.[0]?.text ?? JSON.stringify(result);
5313
+ if (result.isError) {
5314
+ state2.executionJournal.complete(receiptId, "failed", "refused", resultText);
5315
+ return {
5316
+ text: `Proposal ${proposal.id} was approved, but ${integrationId} refused to run it \u2014 nothing was done.
5317
+ Reason: ${resultText}`,
5318
+ isError: true
5319
+ };
5320
+ }
4868
5321
  state2.executionJournal.complete(receiptId, "done");
4869
5322
  state2.executionLog.record({
4870
5323
  profileId: proposal.profileId,
@@ -4872,7 +5325,6 @@ async function executeCommitted(proposal, state2, integrationManager2) {
4872
5325
  execution: proposal.executionContext,
4873
5326
  timestamp: Math.floor(Date.now() / 1e3)
4874
5327
  });
4875
- const resultText = result.content?.[0]?.text ?? JSON.stringify(result);
4876
5328
  return { text: `Proposal ${proposal.id} committed and executed.
4877
5329
  Result: ${resultText}` };
4878
5330
  } catch (err) {
@@ -4883,6 +5335,21 @@ Result: ${resultText}` };
4883
5335
  };
4884
5336
  }
4885
5337
  }
5338
+ function localOutcomeOfExecuted(proposal, state2) {
5339
+ const entry = state2.executionJournal?.findByProposal?.(proposal.id);
5340
+ if (!entry || entry.state === "done") return null;
5341
+ const integration = proposal.tool.split("__")[0];
5342
+ if (entry.state === "failed") {
5343
+ return {
5344
+ text: 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.` : 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.` : `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.`,
5345
+ isError: true
5346
+ };
5347
+ }
5348
+ return {
5349
+ text: `Proposal ${proposal.id} was approved and started running; the outcome is not known yet. Do not call the tool again.`,
5350
+ isError: false
5351
+ };
5352
+ }
4886
5353
  function checkPendingCommitmentsHandler(state2, integrationManager2) {
4887
5354
  return async (args) => {
4888
5355
  if (!state2.spClient.isUnlocked()) {
@@ -4907,6 +5374,13 @@ function checkPendingCommitmentsHandler(state2, integrationManager2) {
4907
5374
  };
4908
5375
  }
4909
5376
  if (match.status === "executed") {
5377
+ const local = localOutcomeOfExecuted(match, state2);
5378
+ if (local) {
5379
+ return {
5380
+ content: [{ type: "text", text: local.text }],
5381
+ ...local.isError ? { isError: true } : {}
5382
+ };
5383
+ }
4910
5384
  const result = match.executionResult ? `
4911
5385
  Result: ${JSON.stringify(match.executionResult, null, 2)}` : " The action ran; the gateway did not retain its output.";
4912
5386
  return {
@@ -5172,21 +5646,21 @@ function createMcpServer(state2, integrationManager2) {
5172
5646
  }
5173
5647
 
5174
5648
  // src/lib/agent-contact.ts
5175
- import { existsSync as existsSync12, mkdirSync as mkdirSync10, readFileSync as readFileSync13, renameSync as renameSync4, writeFileSync as writeFileSync10 } from "fs";
5176
- import { dirname as dirname10, join as join12 } from "path";
5649
+ import { existsSync as existsSync14, mkdirSync as mkdirSync11, readFileSync as readFileSync15, renameSync as renameSync5, writeFileSync as writeFileSync11 } from "fs";
5650
+ import { dirname as dirname11, join as join14 } from "path";
5177
5651
  var FILE = "agent-contact.json";
5178
5652
  var REWRITE_AFTER_MS = 6e4;
5179
5653
  var AgentContactStore = class {
5180
5654
  path;
5181
5655
  current;
5182
5656
  constructor(dataDir2) {
5183
- this.path = join12(dataDir2, FILE);
5657
+ this.path = join14(dataDir2, FILE);
5184
5658
  this.current = this.load();
5185
5659
  }
5186
5660
  load() {
5187
- if (!existsSync12(this.path)) return null;
5661
+ if (!existsSync14(this.path)) return null;
5188
5662
  try {
5189
- const v = JSON.parse(readFileSync13(this.path, "utf8"));
5663
+ const v = JSON.parse(readFileSync15(this.path, "utf8"));
5190
5664
  if (typeof v.clientName !== "string" || typeof v.firstSeenAt !== "string" || typeof v.lastSeenAt !== "string") return null;
5191
5665
  return { clientName: v.clientName, clientVersion: v.clientVersion, firstSeenAt: v.firstSeenAt, lastSeenAt: v.lastSeenAt };
5192
5666
  } catch {
@@ -5209,10 +5683,10 @@ var AgentContactStore = class {
5209
5683
  };
5210
5684
  if (!due) return;
5211
5685
  try {
5212
- mkdirSync10(dirname10(this.path), { recursive: true });
5686
+ mkdirSync11(dirname11(this.path), { recursive: true });
5213
5687
  const tmp = `${this.path}.tmp`;
5214
- writeFileSync10(tmp, JSON.stringify(this.current, null, 2));
5215
- renameSync4(tmp, this.path);
5688
+ writeFileSync11(tmp, JSON.stringify(this.current, null, 2));
5689
+ renameSync5(tmp, this.path);
5216
5690
  } catch (err) {
5217
5691
  console.error("[Suveren MCP] Could not save agent contact:", err);
5218
5692
  }
@@ -5246,16 +5720,16 @@ function verifyGateContentHashes(content, auth) {
5246
5720
  }
5247
5721
 
5248
5722
  // src/lib/integration-registry.ts
5249
- import { mkdirSync as mkdirSync11, readFileSync as readFileSync14, writeFileSync as writeFileSync11, existsSync as existsSync13 } from "fs";
5250
- import { homedir as homedir9 } from "os";
5251
- import { dirname as dirname11, join as join13 } from "path";
5252
- var DEFAULT_DATA_DIR = process.env.SUVEREN_DATA_DIR ?? join13(homedir9(), ".suveren");
5723
+ import { mkdirSync as mkdirSync12, readFileSync as readFileSync16, writeFileSync as writeFileSync12, existsSync as existsSync15 } from "fs";
5724
+ import { homedir as homedir10 } from "os";
5725
+ import { dirname as dirname12, join as join15 } from "path";
5726
+ var DEFAULT_DATA_DIR = process.env.SUVEREN_DATA_DIR ?? join15(homedir10(), ".suveren");
5253
5727
  var IntegrationRegistry = class {
5254
5728
  integrations = /* @__PURE__ */ new Map();
5255
5729
  filePath;
5256
5730
  constructor(dataDir2) {
5257
5731
  const dir = dataDir2 ?? DEFAULT_DATA_DIR;
5258
- this.filePath = join13(dir, "integrations.json");
5732
+ this.filePath = join15(dir, "integrations.json");
5259
5733
  this.load();
5260
5734
  }
5261
5735
  // ─── CRUD ───────────────────────────────────────────────────────────────
@@ -5286,12 +5760,12 @@ var IntegrationRegistry = class {
5286
5760
  }
5287
5761
  // ─── Load / Save ────────────────────────────────────────────────────────
5288
5762
  load() {
5289
- if (!existsSync13(this.filePath)) {
5290
- mkdirSync11(dirname11(this.filePath), { recursive: true });
5763
+ if (!existsSync15(this.filePath)) {
5764
+ mkdirSync12(dirname12(this.filePath), { recursive: true });
5291
5765
  return;
5292
5766
  }
5293
5767
  try {
5294
- const raw = readFileSync14(this.filePath, "utf-8");
5768
+ const raw = readFileSync16(this.filePath, "utf-8");
5295
5769
  const data = JSON.parse(raw);
5296
5770
  const migrated = data.integrations.map((i) => {
5297
5771
  if ("toolGating" in i && !("profile" in i)) {
@@ -5320,79 +5794,19 @@ var IntegrationRegistry = class {
5320
5794
  version: 1,
5321
5795
  integrations: Array.from(this.integrations.values())
5322
5796
  };
5323
- mkdirSync11(dirname11(this.filePath), { recursive: true });
5324
- writeFileSync11(this.filePath, JSON.stringify(data, null, 2), { encoding: "utf-8" });
5797
+ mkdirSync12(dirname12(this.filePath), { recursive: true });
5798
+ writeFileSync12(this.filePath, JSON.stringify(data, null, 2), { encoding: "utf-8" });
5325
5799
  }
5326
5800
  };
5327
5801
 
5328
5802
  // src/lib/integration-manager.ts
5329
- import { homedir as homedir10 } from "os";
5330
- import { join as join15, delimiter } from "path";
5331
- import { existsSync as existsSync15, writeFileSync as writeFileSync12, mkdirSync as mkdirSync12, readFileSync as readFileSync16, rmSync } from "fs";
5803
+ import { homedir as homedir11 } from "os";
5804
+ import { join as join16, delimiter } from "path";
5805
+ import { existsSync as existsSync16, writeFileSync as writeFileSync13, mkdirSync as mkdirSync13, readFileSync as readFileSync17, rmSync } from "fs";
5332
5806
  import { execFile } from "child_process";
5333
5807
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
5334
5808
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
5335
5809
 
5336
- // src/lib/manifest-loader.ts
5337
- import { readFileSync as readFileSync15, existsSync as existsSync14 } from "fs";
5338
- import { join as join14, resolve } from "path";
5339
- var EXACT_SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
5340
- function isExactSemver(value) {
5341
- return typeof value === "string" && EXACT_SEMVER_RE.test(value);
5342
- }
5343
- function invalidNpmPinReason(manifest) {
5344
- if (!manifest.npmPackage) return null;
5345
- if (manifest.npmVersion === void 0) {
5346
- return `npmPackage "${manifest.npmPackage}" is set but npmVersion is missing \u2014 every connector manifest must pin an exact tested version`;
5347
- }
5348
- if (!isExactSemver(manifest.npmVersion)) {
5349
- return `npmVersion ${JSON.stringify(manifest.npmVersion)} for "${manifest.npmPackage}" is not an exact semver (e.g. "1.2.3") \u2014 ranges, dist-tags like "latest", git specs, URLs, and file paths are not allowed`;
5350
- }
5351
- return null;
5352
- }
5353
- var manifests = /* @__PURE__ */ new Map();
5354
- function loadManifests(integrationsDir) {
5355
- const dir = resolve(
5356
- integrationsDir ?? process.env.SUVEREN_MANIFESTS_DIR ?? join14(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "content", "integrations")
5357
- );
5358
- const indexPath = join14(dir, "index.json");
5359
- if (!existsSync14(indexPath)) {
5360
- console.error(`[ManifestLoader] No index.json found at ${indexPath}, skipping manifest loading`);
5361
- return 0;
5362
- }
5363
- let index;
5364
- try {
5365
- index = JSON.parse(readFileSync15(indexPath, "utf-8"));
5366
- } catch (err) {
5367
- console.error(`[ManifestLoader] Failed to parse ${indexPath}:`, err);
5368
- return 0;
5369
- }
5370
- let loaded = 0;
5371
- for (const [id, relativePath] of Object.entries(index.integrations)) {
5372
- const manifestPath = join14(dir, relativePath);
5373
- try {
5374
- const manifest = JSON.parse(readFileSync15(manifestPath, "utf-8"));
5375
- const pinReason = invalidNpmPinReason(manifest);
5376
- if (pinReason) {
5377
- console.error(`[ManifestLoader] Refusing manifest ${id} (${manifestPath}): ${pinReason}`);
5378
- continue;
5379
- }
5380
- manifests.set(id, manifest);
5381
- loaded++;
5382
- } catch (err) {
5383
- console.error(`[ManifestLoader] Failed to load manifest ${id} from ${manifestPath}:`, err);
5384
- }
5385
- }
5386
- console.error(`[ManifestLoader] Loaded ${loaded} integration manifest(s) from ${dir}`);
5387
- return loaded;
5388
- }
5389
- function getManifest(id) {
5390
- return manifests.get(id);
5391
- }
5392
- function getAllManifests() {
5393
- return Array.from(manifests.values());
5394
- }
5395
-
5396
5810
  // src/lib/remote-auth-preflight.ts
5397
5811
  var AUTH_HEADER_RE = /^authorization:\s*(.+)$/i;
5398
5812
  function remotePreflightTarget(command, args) {
@@ -5518,8 +5932,8 @@ function inheritedConnectorEnv(parent, passEnv = []) {
5518
5932
  }
5519
5933
 
5520
5934
  // src/lib/integration-manager.ts
5521
- var DEFAULT_DATA_DIR2 = process.env.SUVEREN_DATA_DIR ?? join15(homedir10(), ".suveren");
5522
- var INTEGRATIONS_DIR = process.env.SUVEREN_INTEGRATIONS_DIR ?? join15(DEFAULT_DATA_DIR2, "integrations");
5935
+ var DEFAULT_DATA_DIR2 = process.env.SUVEREN_DATA_DIR ?? join16(homedir11(), ".suveren");
5936
+ var INTEGRATIONS_DIR = process.env.SUVEREN_INTEGRATIONS_DIR ?? join16(DEFAULT_DATA_DIR2, "integrations");
5523
5937
  var OFFLINE_MODE = process.env.SUVEREN_OFFLINE === "1";
5524
5938
  var installLock = Promise.resolve();
5525
5939
  function withInstallLock(task) {
@@ -5527,7 +5941,7 @@ function withInstallLock(task) {
5527
5941
  installLock = run.then(() => void 0, () => void 0);
5528
5942
  return run;
5529
5943
  }
5530
- var INTEGRATIONS_BIN = join15(INTEGRATIONS_DIR, "node_modules", ".bin");
5944
+ var INTEGRATIONS_BIN = join16(INTEGRATIONS_DIR, "node_modules", ".bin");
5531
5945
  function getIntegrationsBinDir() {
5532
5946
  return INTEGRATIONS_BIN;
5533
5947
  }
@@ -5536,12 +5950,12 @@ function buildPath() {
5536
5950
  return [INTEGRATIONS_BIN, base].join(delimiter);
5537
5951
  }
5538
5952
  function ensureIntegrationsDir() {
5539
- if (!existsSync15(INTEGRATIONS_DIR)) {
5540
- mkdirSync12(INTEGRATIONS_DIR, { recursive: true });
5953
+ if (!existsSync16(INTEGRATIONS_DIR)) {
5954
+ mkdirSync13(INTEGRATIONS_DIR, { recursive: true });
5541
5955
  }
5542
- const pkgPath = join15(INTEGRATIONS_DIR, "package.json");
5543
- if (!existsSync15(pkgPath)) {
5544
- writeFileSync12(pkgPath, JSON.stringify({ name: "suveren-integrations", version: "1.0.0", private: true }, null, 2));
5956
+ const pkgPath = join16(INTEGRATIONS_DIR, "package.json");
5957
+ if (!existsSync16(pkgPath)) {
5958
+ writeFileSync13(pkgPath, JSON.stringify({ name: "suveren-integrations", version: "1.0.0", private: true }, null, 2));
5545
5959
  }
5546
5960
  }
5547
5961
  function readAgeOf(config) {
@@ -5630,19 +6044,19 @@ var IntegrationManager = class {
5630
6044
  * exist, and treat anything else as not-installed (and reinstallable).
5631
6045
  */
5632
6046
  isUsableInstall(npmPackage) {
5633
- const pkgDir = join15(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
5634
- const pkgJson = join15(pkgDir, "package.json");
5635
- if (!existsSync15(pkgDir) || !existsSync15(pkgJson)) return false;
6047
+ const pkgDir = join16(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
6048
+ const pkgJson = join16(pkgDir, "package.json");
6049
+ if (!existsSync16(pkgDir) || !existsSync16(pkgJson)) return false;
5636
6050
  try {
5637
- const pkg = JSON.parse(readFileSync16(pkgJson, "utf8"));
6051
+ const pkg = JSON.parse(readFileSync17(pkgJson, "utf8"));
5638
6052
  const defaultBinName = (pkg.name ?? npmPackage).split("/").pop() ?? "";
5639
6053
  const binMap = typeof pkg.bin === "string" ? { [defaultBinName]: pkg.bin } : pkg.bin ?? {};
5640
6054
  const binEntries = Object.entries(binMap);
5641
6055
  for (const [binName, target] of binEntries) {
5642
- if (!existsSync15(join15(pkgDir, target))) return false;
6056
+ if (!existsSync16(join16(pkgDir, target))) return false;
5643
6057
  if (!this.binShimExists(binName)) return false;
5644
6058
  }
5645
- if (binEntries.length === 0 && pkg.main && !existsSync15(join15(pkgDir, pkg.main))) return false;
6059
+ if (binEntries.length === 0 && pkg.main && !existsSync16(join16(pkgDir, pkg.main))) return false;
5646
6060
  return true;
5647
6061
  } catch {
5648
6062
  return false;
@@ -5657,9 +6071,9 @@ var IntegrationManager = class {
5657
6071
  */
5658
6072
  readInstalledVersion(npmPackage) {
5659
6073
  if (!this.isUsableInstall(npmPackage)) return null;
5660
- const pkgJson = join15(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"), "package.json");
6074
+ const pkgJson = join16(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"), "package.json");
5661
6075
  try {
5662
- const pkg = JSON.parse(readFileSync16(pkgJson, "utf8"));
6076
+ const pkg = JSON.parse(readFileSync17(pkgJson, "utf8"));
5663
6077
  return typeof pkg.version === "string" ? pkg.version : null;
5664
6078
  } catch {
5665
6079
  return null;
@@ -5673,7 +6087,7 @@ var IntegrationManager = class {
5673
6087
  binShimExists(binName) {
5674
6088
  if (!binName) return false;
5675
6089
  const candidates = process.platform === "win32" ? [binName, `${binName}.cmd`, `${binName}.CMD`, `${binName}.ps1`] : [binName];
5676
- return candidates.some((c) => existsSync15(join15(INTEGRATIONS_BIN, c)));
6090
+ return candidates.some((c) => existsSync16(join16(INTEGRATIONS_BIN, c)));
5677
6091
  }
5678
6092
  /**
5679
6093
  * The exact version THIS gateway's CURRENTLY LOADED manifest pins
@@ -5763,8 +6177,8 @@ var IntegrationManager = class {
5763
6177
  * nothing at all.
5764
6178
  */
5765
6179
  async installNow(npmPackage, exactVersion, cleanFirst = true) {
5766
- const pkgDir = join15(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
5767
- if (cleanFirst && existsSync15(pkgDir)) {
6180
+ const pkgDir = join16(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
6181
+ if (cleanFirst && existsSync16(pkgDir)) {
5768
6182
  console.error(`[IntegrationManager] Removing ${exactVersion ? "outdated" : "unusable"} install of ${npmPackage}`);
5769
6183
  try {
5770
6184
  rmSync(pkgDir, { recursive: true, force: true });
@@ -5957,6 +6371,7 @@ var IntegrationManager = class {
5957
6371
  tool.inputSchema ?? {},
5958
6372
  gating?.blockedArgs
5959
6373
  ),
6374
+ outputSchema: tool.outputSchema,
5960
6375
  gating
5961
6376
  };
5962
6377
  });
@@ -6121,7 +6536,11 @@ var IntegrationManager = class {
6121
6536
  type: item.type,
6122
6537
  text: item.text ?? JSON.stringify(item)
6123
6538
  }));
6124
- return { content, isError: result.isError };
6539
+ return {
6540
+ content,
6541
+ isError: result.isError,
6542
+ structuredContent: result.structuredContent
6543
+ };
6125
6544
  } catch (err) {
6126
6545
  const message = err instanceof Error ? err.message : String(err);
6127
6546
  return {
@@ -6670,28 +7089,28 @@ function agentAccess(token, opensSession) {
6670
7089
  }
6671
7090
 
6672
7091
  // src/lib/profile-loader.ts
6673
- import { readFileSync as readFileSync17, existsSync as existsSync16 } from "fs";
6674
- import { join as join16, resolve as resolve2 } from "path";
7092
+ import { readFileSync as readFileSync18, existsSync as existsSync17 } from "fs";
7093
+ import { join as join17, resolve as resolve2 } from "path";
6675
7094
  import { registerProfile } from "@hap/core";
6676
7095
  function loadProfiles(profilesDir) {
6677
- const dir = resolve2(profilesDir ?? process.env.SUVEREN_PROFILES_DIR ?? join16(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "..", "hap-profiles"));
6678
- const indexPath = join16(dir, "index.json");
6679
- if (!existsSync16(indexPath)) {
7096
+ const dir = resolve2(profilesDir ?? process.env.SUVEREN_PROFILES_DIR ?? join17(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "..", "hap-profiles"));
7097
+ const indexPath = join17(dir, "index.json");
7098
+ if (!existsSync17(indexPath)) {
6680
7099
  warnNoProfiles(`No index.json at ${indexPath}`);
6681
7100
  return 0;
6682
7101
  }
6683
7102
  let index;
6684
7103
  try {
6685
- index = JSON.parse(readFileSync17(indexPath, "utf-8"));
7104
+ index = JSON.parse(readFileSync18(indexPath, "utf-8"));
6686
7105
  } catch (err) {
6687
7106
  console.error(`[ProfileLoader] Failed to parse ${indexPath}:`, err);
6688
7107
  return 0;
6689
7108
  }
6690
7109
  let loaded = 0;
6691
7110
  for (const [profileId, relativePath] of Object.entries(index.profiles)) {
6692
- const profilePath = join16(dir, relativePath);
7111
+ const profilePath = join17(dir, relativePath);
6693
7112
  try {
6694
- const profile = JSON.parse(readFileSync17(profilePath, "utf-8"));
7113
+ const profile = JSON.parse(readFileSync18(profilePath, "utf-8"));
6695
7114
  registerProfile(profileId, profile);
6696
7115
  const shortName = profileId.split("/").pop()?.replace(/@.*$/, "");
6697
7116
  if (shortName && shortName !== profileId) {
@@ -7029,20 +7448,20 @@ async function controlPlaneMandate(dryRun, request) {
7029
7448
  }
7030
7449
 
7031
7450
  // src/lib/guides.ts
7032
- import { existsSync as existsSync17, readdirSync, readFileSync as readFileSync18 } from "fs";
7033
- import { dirname as dirname12, join as join17 } from "path";
7451
+ import { existsSync as existsSync18, readdirSync, readFileSync as readFileSync19 } from "fs";
7452
+ import { dirname as dirname13, join as join18 } from "path";
7034
7453
  var FILE_RE = /^(?:(\d+)-)?([a-z0-9][a-z0-9-]*)\.md$/;
7035
7454
  function builtinGuidesDir() {
7036
7455
  if (process.env.SUVEREN_BUILTIN_GUIDES_DIR) return process.env.SUVEREN_BUILTIN_GUIDES_DIR;
7037
7456
  let dir = import.meta.dirname ?? __dirname;
7038
7457
  for (let i = 0; i < 8; i++) {
7039
- const candidate = join17(dir, "content", "guides");
7040
- if (existsSync17(candidate)) return candidate;
7041
- const parent = dirname12(dir);
7458
+ const candidate = join18(dir, "content", "guides");
7459
+ if (existsSync18(candidate)) return candidate;
7460
+ const parent = dirname13(dir);
7042
7461
  if (parent === dir) break;
7043
7462
  dir = parent;
7044
7463
  }
7045
- return join17(import.meta.dirname ?? __dirname, "content", "guides");
7464
+ return join18(import.meta.dirname ?? __dirname, "content", "guides");
7046
7465
  }
7047
7466
  function parse(body) {
7048
7467
  const lines = body.split(/\r?\n/);
@@ -7053,14 +7472,14 @@ function parse(body) {
7053
7472
  return { title, summary };
7054
7473
  }
7055
7474
  function readDir(dir, source) {
7056
- if (!dir || !existsSync17(dir)) return [];
7475
+ if (!dir || !existsSync18(dir)) return [];
7057
7476
  const out = [];
7058
7477
  for (const file of readdirSync(dir)) {
7059
7478
  const m = FILE_RE.exec(file);
7060
7479
  if (!m) continue;
7061
7480
  let body;
7062
7481
  try {
7063
- body = readFileSync18(join17(dir, file), "utf8");
7482
+ body = readFileSync19(join18(dir, file), "utf8");
7064
7483
  } catch {
7065
7484
  continue;
7066
7485
  }
@@ -7370,8 +7789,8 @@ function registerBuiltins(deps, factories = BUILTIN_FACTORIES) {
7370
7789
  import { decodeMandateBlob as decodeMandateBlob3 } from "@hap/core";
7371
7790
 
7372
7791
  // src/lib/committed-executor.ts
7373
- import { existsSync as existsSync18, mkdirSync as mkdirSync13, readFileSync as readFileSync19, unlinkSync as unlinkSync6, writeFileSync as writeFileSync13 } from "fs";
7374
- import { dirname as dirname13, join as join18 } from "path";
7792
+ import { existsSync as existsSync19, mkdirSync as mkdirSync14, readFileSync as readFileSync20, unlinkSync as unlinkSync6, writeFileSync as writeFileSync14 } from "fs";
7793
+ import { dirname as dirname14, join as join19 } from "path";
7375
7794
  var CommittedExecutor = class {
7376
7795
  constructor(run) {
7377
7796
  this.run = run;
@@ -7399,7 +7818,7 @@ var ExecutorLock = class {
7399
7818
  path;
7400
7819
  held = false;
7401
7820
  constructor(dataDir2) {
7402
- this.path = join18(dataDir2, "executor.lock");
7821
+ this.path = join19(dataDir2, "executor.lock");
7403
7822
  }
7404
7823
  /** Try to take the lock. Returns the holder's pid when someone else has it. */
7405
7824
  acquire() {
@@ -7407,9 +7826,9 @@ var ExecutorLock = class {
7407
7826
  if (current && current.pid !== process.pid && isAlive(current.pid)) {
7408
7827
  return { held: false, holderPid: current.pid };
7409
7828
  }
7410
- mkdirSync13(dirname13(this.path), { recursive: true, mode: 448 });
7829
+ mkdirSync14(dirname14(this.path), { recursive: true, mode: 448 });
7411
7830
  const lock = { pid: process.pid, startedAt: Math.floor(Date.now() / 1e3) };
7412
- writeFileSync13(this.path, JSON.stringify(lock), { mode: 384 });
7831
+ writeFileSync14(this.path, JSON.stringify(lock), { mode: 384 });
7413
7832
  this.held = true;
7414
7833
  return { held: true };
7415
7834
  }
@@ -7431,9 +7850,9 @@ var ExecutorLock = class {
7431
7850
  }
7432
7851
  }
7433
7852
  read() {
7434
- if (!existsSync18(this.path)) return null;
7853
+ if (!existsSync19(this.path)) return null;
7435
7854
  try {
7436
- const parsed = JSON.parse(readFileSync19(this.path, "utf8"));
7855
+ const parsed = JSON.parse(readFileSync20(this.path, "utf8"));
7437
7856
  return typeof parsed?.pid === "number" ? parsed : null;
7438
7857
  } catch {
7439
7858
  return null;
@@ -7450,20 +7869,20 @@ function isAlive(pid) {
7450
7869
  }
7451
7870
 
7452
7871
  // bin/http.ts
7453
- import { homedir as homedir11 } from "os";
7454
- import { join as join20 } from "path";
7872
+ import { homedir as homedir12 } from "os";
7873
+ import { join as join21 } from "path";
7455
7874
 
7456
7875
  // src/lib/mcp-as-tracker.ts
7457
- import { existsSync as existsSync19, mkdirSync as mkdirSync14, readFileSync as readFileSync20, writeFileSync as writeFileSync14 } from "fs";
7458
- import { dirname as dirname14, join as join19 } from "path";
7876
+ import { existsSync as existsSync20, mkdirSync as mkdirSync15, readFileSync as readFileSync21, writeFileSync as writeFileSync15 } from "fs";
7877
+ import { dirname as dirname15, join as join20 } from "path";
7459
7878
  function trackerPath(dataDir2) {
7460
- return join19(dataDir2, "mcp-as-pairing.json");
7879
+ return join20(dataDir2, "mcp-as-pairing.json");
7461
7880
  }
7462
7881
  function readMcpPairedAsUrl(dataDir2) {
7463
7882
  const path = trackerPath(dataDir2);
7464
- if (!existsSync19(path)) return null;
7883
+ if (!existsSync20(path)) return null;
7465
7884
  try {
7466
- const data = JSON.parse(readFileSync20(path, "utf-8"));
7885
+ const data = JSON.parse(readFileSync21(path, "utf-8"));
7467
7886
  return typeof data.asUrl === "string" ? data.asUrl : null;
7468
7887
  } catch {
7469
7888
  return null;
@@ -7471,12 +7890,12 @@ function readMcpPairedAsUrl(dataDir2) {
7471
7890
  }
7472
7891
  function writeMcpPairedAsUrl(dataDir2, asUrl) {
7473
7892
  const path = trackerPath(dataDir2);
7474
- mkdirSync14(dirname14(path), { recursive: true, mode: 448 });
7475
- writeFileSync14(path, JSON.stringify({ asUrl }, null, 2), { encoding: "utf-8", mode: 384 });
7893
+ mkdirSync15(dirname15(path), { recursive: true, mode: 448 });
7894
+ writeFileSync15(path, JSON.stringify({ asUrl }, null, 2), { encoding: "utf-8", mode: 384 });
7476
7895
  }
7477
7896
 
7478
7897
  // bin/http.ts
7479
- var dataDir = process.env.SUVEREN_DATA_DIR ?? join20(homedir11(), ".suveren");
7898
+ var dataDir = process.env.SUVEREN_DATA_DIR ?? join21(homedir12(), ".suveren");
7480
7899
  var spUrl = resolveAsUrl(dataDir);
7481
7900
  validatePinTlsForUrl(resolvePinTls(dataDir), spUrl);
7482
7901
  var port = parseInt(process.env.SUVEREN_MCP_PORT ?? "3430", 10);
@@ -7686,6 +8105,29 @@ app.post("/internal/resync-gates", internalOnly, async (_req, res) => {
7686
8105
  }
7687
8106
  res.json({ ok: true, synced, orphaned });
7688
8107
  });
8108
+ app.post("/internal/preview", internalOnly, async (req, res) => {
8109
+ const { proposalId, tool, toolArgs } = req.body;
8110
+ if (!proposalId || !tool || typeof toolArgs !== "object" || toolArgs === null) {
8111
+ res.status(400).json({ error: "Missing proposalId, tool, or toolArgs" });
8112
+ return;
8113
+ }
8114
+ const sep = tool.indexOf("__");
8115
+ if (sep < 0) {
8116
+ res.status(400).json({ error: `Invalid tool name: ${tool}` });
8117
+ return;
8118
+ }
8119
+ const integrationId = tool.slice(0, sep);
8120
+ const toolName = tool.slice(sep + 2);
8121
+ const result = await buildInternalPreview(integrationManager, integrationId, toolName, toolArgs);
8122
+ state.previewLog.record({
8123
+ ts: Date.now(),
8124
+ proposalId,
8125
+ integrationId,
8126
+ tool: toolName,
8127
+ status: result.status
8128
+ });
8129
+ res.json(result);
8130
+ });
7689
8131
  app.post("/internal/add-integration", internalOnly, async (req, res) => {
7690
8132
  try {
7691
8133
  const config = req.body;
@@ -8179,7 +8621,7 @@ app.listen(port, bindHost, () => {
8179
8621
  (proposal) => executeCommitted(proposal, state, integrationManager)
8180
8622
  );
8181
8623
  installCommittedExecutor(committedExecutor);
8182
- const dataDir2 = process.env.SUVEREN_DATA_DIR ?? join20(homedir11(), ".suveren");
8624
+ const dataDir2 = process.env.SUVEREN_DATA_DIR ?? join21(homedir12(), ".suveren");
8183
8625
  const executorLock = new ExecutorLock(dataDir2);
8184
8626
  const lock = executorLock.acquire();
8185
8627
  if (!lock.held) {