@suveren/gateway 0.21.0 → 0.23.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.
@@ -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,13 +1970,20 @@ 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, outcome) {
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;
1978
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
+ }
1979
1987
  row.finishedAt = nowSec();
1980
1988
  this.persist(file);
1981
1989
  }
@@ -2045,7 +2053,8 @@ var ProposalSubmissionStore = class {
2045
2053
  executionContextHash: hashToolArgs(entry.executionContext),
2046
2054
  authorizationId: entry.authorizationId,
2047
2055
  profileId: entry.profileId,
2048
- submittedAt: nowSec2()
2056
+ submittedAt: nowSec2(),
2057
+ ...entry.previewHash ? { previewHash: entry.previewHash } : {}
2049
2058
  });
2050
2059
  this.persist(file);
2051
2060
  }
@@ -2073,6 +2082,56 @@ var ProposalSubmissionStore = class {
2073
2082
  }
2074
2083
  };
2075
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
+
2076
2135
  // src/lib/gatekeeper.ts
2077
2136
  import { verify } from "@hap/core";
2078
2137
  var MCPGatekeeper = class {
@@ -2165,9 +2224,9 @@ var MCPGatekeeper = class {
2165
2224
  };
2166
2225
 
2167
2226
  // src/lib/report/report-store.ts
2168
- import { mkdirSync as mkdirSync8, readFileSync as readFileSync11, writeFileSync as writeFileSync8, existsSync as existsSync10, unlinkSync as unlinkSync5 } from "fs";
2169
- import { homedir as homedir7 } from "os";
2170
- 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";
2171
2230
  import { createCipheriv as createCipheriv5, createDecipheriv as createDecipheriv5, randomBytes as randomBytes5 } from "crypto";
2172
2231
 
2173
2232
  // src/lib/report/record-resolvers.ts
@@ -2476,27 +2535,27 @@ async function buildCoverage(caseElements, getExport, archive, referenced, windo
2476
2535
  }
2477
2536
 
2478
2537
  // src/lib/report/report-store.ts
2479
- var DEFAULT_DIR7 = process.env.SUVEREN_DATA_DIR ?? join10(homedir7(), ".suveren");
2538
+ var DEFAULT_DIR8 = process.env.SUVEREN_DATA_DIR ?? join11(homedir8(), ".suveren");
2480
2539
  var ReportStore = class {
2481
2540
  report = null;
2482
2541
  baseDir;
2483
2542
  vaultKey = null;
2484
2543
  constructor(filePath) {
2485
2544
  if (filePath && filePath.endsWith(".json")) {
2486
- this.baseDir = dirname8(filePath);
2545
+ this.baseDir = dirname9(filePath);
2487
2546
  } else {
2488
- this.baseDir = filePath ?? DEFAULT_DIR7;
2547
+ this.baseDir = filePath ?? DEFAULT_DIR8;
2489
2548
  }
2490
2549
  this.loadPlaintext();
2491
2550
  }
2492
2551
  /** Set the vault key for encryption. Triggers migration from plaintext if needed. */
2493
2552
  setVaultKey(key) {
2494
2553
  this.vaultKey = key;
2495
- if (existsSync10(this.encryptedFilePath)) {
2554
+ if (existsSync11(this.encryptedFilePath)) {
2496
2555
  this.loadEncrypted();
2497
2556
  } else if (this.report !== null) {
2498
2557
  this.persistEncrypted();
2499
- if (existsSync10(this.plaintextFilePath)) {
2558
+ if (existsSync11(this.plaintextFilePath)) {
2500
2559
  try {
2501
2560
  unlinkSync5(this.plaintextFilePath);
2502
2561
  } catch {
@@ -2511,7 +2570,7 @@ var ReportStore = class {
2511
2570
  * "the report exists but is locked".
2512
2571
  */
2513
2572
  isLocked() {
2514
- return this.vaultKey === null && existsSync10(this.encryptedFilePath);
2573
+ return this.vaultKey === null && existsSync11(this.encryptedFilePath);
2515
2574
  }
2516
2575
  /** The stored, already-verified report — or null if none has been saved
2517
2576
  * (and the store is not locked; check `isLocked()` first). */
@@ -2560,34 +2619,34 @@ var ReportStore = class {
2560
2619
  }
2561
2620
  // ─── File paths ─────────────────────────────────────────────────────────
2562
2621
  get plaintextFilePath() {
2563
- return join10(this.baseDir, "report.json");
2622
+ return join11(this.baseDir, "report.json");
2564
2623
  }
2565
2624
  get encryptedFilePath() {
2566
- return join10(this.baseDir, "report.enc.json");
2625
+ return join11(this.baseDir, "report.enc.json");
2567
2626
  }
2568
2627
  // ─── Load / Persist ─────────────────────────────────────────────────────
2569
2628
  loadPlaintext() {
2570
- if (!existsSync10(this.plaintextFilePath)) {
2571
- mkdirSync8(this.baseDir, { recursive: true });
2629
+ if (!existsSync11(this.plaintextFilePath)) {
2630
+ mkdirSync9(this.baseDir, { recursive: true });
2572
2631
  return;
2573
2632
  }
2574
2633
  try {
2575
- const raw = readFileSync11(this.plaintextFilePath, "utf-8");
2634
+ const raw = readFileSync12(this.plaintextFilePath, "utf-8");
2576
2635
  const parsed = JSON.parse(raw);
2577
2636
  this.report = parsed.report ?? null;
2578
2637
  } catch {
2579
2638
  console.error(`[ReportStore] Could not parse ${this.plaintextFilePath} \u2014 preserving as .corrupt`);
2580
2639
  try {
2581
- writeFileSync8(`${this.plaintextFilePath}.corrupt-${Date.now()}`, readFileSync11(this.plaintextFilePath));
2640
+ writeFileSync9(`${this.plaintextFilePath}.corrupt-${Date.now()}`, readFileSync12(this.plaintextFilePath));
2582
2641
  } catch {
2583
2642
  }
2584
2643
  this.report = null;
2585
2644
  }
2586
2645
  }
2587
2646
  loadEncrypted() {
2588
- if (!existsSync10(this.encryptedFilePath)) return;
2647
+ if (!existsSync11(this.encryptedFilePath)) return;
2589
2648
  try {
2590
- const raw = readFileSync11(this.encryptedFilePath, "utf-8");
2649
+ const raw = readFileSync12(this.encryptedFilePath, "utf-8");
2591
2650
  const data = JSON.parse(raw);
2592
2651
  const decrypted = this.decrypt(data.blob);
2593
2652
  const parsed = JSON.parse(decrypted);
@@ -2595,7 +2654,7 @@ var ReportStore = class {
2595
2654
  } catch (err) {
2596
2655
  console.error(`[ReportStore] Could not decrypt ${this.encryptedFilePath} \u2014 preserving as .corrupt:`, err);
2597
2656
  try {
2598
- writeFileSync8(`${this.encryptedFilePath}.corrupt-${Date.now()}`, readFileSync11(this.encryptedFilePath));
2657
+ writeFileSync9(`${this.encryptedFilePath}.corrupt-${Date.now()}`, readFileSync12(this.encryptedFilePath));
2599
2658
  } catch {
2600
2659
  }
2601
2660
  this.report = null;
@@ -2610,16 +2669,16 @@ var ReportStore = class {
2610
2669
  }
2611
2670
  persistPlaintext() {
2612
2671
  const data = { version: 1, report: this.report };
2613
- mkdirSync8(this.baseDir, { recursive: true, mode: 448 });
2614
- 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 });
2615
2674
  }
2616
2675
  persistEncrypted() {
2617
2676
  const data = {
2618
2677
  version: 1,
2619
2678
  blob: this.encrypt(JSON.stringify({ version: 1, report: this.report }))
2620
2679
  };
2621
- mkdirSync8(this.baseDir, { recursive: true, mode: 448 });
2622
- 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 });
2623
2682
  }
2624
2683
  };
2625
2684
 
@@ -2640,6 +2699,8 @@ var SharedState = class {
2640
2699
  /** What THIS gateway submitted for each review-mode proposal it created —
2641
2700
  * see ticket-verify.ts / commitments.ts. */
2642
2701
  proposalSubmissions;
2702
+ /** AU3 — local, content-free record of preview reads. See preview-log.ts. */
2703
+ previewLog;
2643
2704
  gatekeeper;
2644
2705
  /**
2645
2706
  * V7 — set by {@link checkAsCompat} (called once at process start, and
@@ -2673,6 +2734,7 @@ var SharedState = class {
2673
2734
  this.reportStore = new ReportStore(gateStorePath);
2674
2735
  this.executionJournal = new ExecutionJournal(gateStorePath);
2675
2736
  this.proposalSubmissions = new ProposalSubmissionStore(gateStorePath);
2737
+ this.previewLog = new PreviewLog(gateStorePath);
2676
2738
  this.gatekeeper = new MCPGatekeeper(this.cache);
2677
2739
  }
2678
2740
  /**
@@ -2861,29 +2923,29 @@ function formatConsumptionFull(entries) {
2861
2923
  }
2862
2924
 
2863
2925
  // src/lib/context-loader.ts
2864
- import { readFileSync as readFileSync12, existsSync as existsSync11, mkdirSync as mkdirSync9, writeFileSync as writeFileSync9, renameSync as renameSync3 } from "fs";
2865
- import { homedir as homedir8 } from "os";
2866
- import { join as join11, dirname as dirname9 } from "path";
2867
- 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");
2868
2930
  var CONTEXT_MAX_BYTES = 16 * 1024;
2869
2931
  function contextFilePath(dataDir2) {
2870
- return join11(dataDir2 ?? DEFAULT_DIR8, "context.md");
2932
+ return join12(dataDir2 ?? DEFAULT_DIR9, "context.md");
2871
2933
  }
2872
2934
  function writeContextFile(content, dataDir2) {
2873
2935
  const bytes = Buffer.byteLength(content, "utf-8");
2874
2936
  if (bytes > CONTEXT_MAX_BYTES) throw new Error(`the brief is ${bytes} bytes; the limit is ${CONTEXT_MAX_BYTES}.`);
2875
2937
  const filePath = contextFilePath(dataDir2);
2876
- mkdirSync9(dirname9(filePath), { recursive: true });
2938
+ mkdirSync10(dirname10(filePath), { recursive: true });
2877
2939
  const tmpPath = `${filePath}.tmp`;
2878
- writeFileSync9(tmpPath, content, "utf-8");
2879
- renameSync3(tmpPath, filePath);
2940
+ writeFileSync10(tmpPath, content, "utf-8");
2941
+ renameSync4(tmpPath, filePath);
2880
2942
  }
2881
2943
  var BRIEF_MAX_CHARS = 16 * 1024;
2882
2944
  function readContextFile(dataDir2) {
2883
2945
  const filePath = contextFilePath(dataDir2);
2884
- if (!existsSync11(filePath)) return null;
2946
+ if (!existsSync12(filePath)) return null;
2885
2947
  try {
2886
- const content = readFileSync12(filePath, "utf-8").trim();
2948
+ const content = readFileSync13(filePath, "utf-8").trim();
2887
2949
  return content || null;
2888
2950
  } catch {
2889
2951
  return null;
@@ -3069,6 +3131,257 @@ function attachTicketId(tool, args, ticketId) {
3069
3131
  return { ...args, ticket_id: ticketId };
3070
3132
  }
3071
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
+ function previewHasNoTarget(preview, mapped) {
3274
+ return Object.keys(preview.args).length > 0 && Object.keys(mapped).length === 0;
3275
+ }
3276
+ var PreviewTimeoutError = class extends Error {
3277
+ };
3278
+ function withTimeout(p, ms) {
3279
+ let timer;
3280
+ const timeout = new Promise((_, reject) => {
3281
+ timer = setTimeout(() => reject(new PreviewTimeoutError("preview read timed out")), ms);
3282
+ });
3283
+ return Promise.race([p, timeout]).finally(() => clearTimeout(timer));
3284
+ }
3285
+ async function readPreview(integrationManager2, integrationId, previewTool, args) {
3286
+ const discovered = integrationManager2.getAllTools().find((t) => t.integrationId === integrationId && t.originalName === previewTool);
3287
+ if (!discovered) {
3288
+ return { status: "unavailable", reason: "no_connector" };
3289
+ }
3290
+ try {
3291
+ const result = await withTimeout(
3292
+ integrationManager2.callTool(integrationId, previewTool, args),
3293
+ PREVIEW_READ_TIMEOUT_MS
3294
+ );
3295
+ const text = result.content?.[0]?.text;
3296
+ if (result.isError) {
3297
+ return { status: "not_found", message: capText(text) };
3298
+ }
3299
+ return {
3300
+ status: "ok",
3301
+ body: {
3302
+ ...result.structuredContent !== void 0 ? { structured: result.structuredContent } : {},
3303
+ ...discovered.outputSchema ? { outputSchema: discovered.outputSchema } : {},
3304
+ ...text !== void 0 ? { text } : {}
3305
+ }
3306
+ };
3307
+ } catch (err) {
3308
+ if (err instanceof PreviewTimeoutError) {
3309
+ return { status: "unavailable", reason: "timeout" };
3310
+ }
3311
+ return {
3312
+ status: "unavailable",
3313
+ reason: "connector_error",
3314
+ message: capText(err instanceof Error ? err.message : String(err))
3315
+ };
3316
+ }
3317
+ }
3318
+ function hashPreviewBody(body) {
3319
+ if (body.structured !== void 0) {
3320
+ return computeContentHash2({ kind: "jcs", version: "1" }, body.structured);
3321
+ }
3322
+ return computeContentHash2({ kind: "text", version: "1" }, body.text ?? "");
3323
+ }
3324
+ async function computeSubmissionPreviewHash(integrationManager2, integrationId, toolName, actionArgs) {
3325
+ const preview = getToolPreviewConfig(integrationId, toolName);
3326
+ if (!preview || preview.version) return void 0;
3327
+ const mapped = mapPreviewArgs(preview, actionArgs);
3328
+ if (previewHasNoTarget(preview, mapped)) return void 0;
3329
+ const read = await readPreview(integrationManager2, integrationId, preview.tool, mapped);
3330
+ if (read.status !== "ok") return void 0;
3331
+ return hashPreviewBody(read.body);
3332
+ }
3333
+ async function previewChangedSinceSubmission(integrationManager2, integrationId, toolName, actionArgs, previousHash) {
3334
+ const preview = getToolPreviewConfig(integrationId, toolName);
3335
+ if (!preview) return true;
3336
+ const mapped = mapPreviewArgs(preview, actionArgs);
3337
+ const read = await readPreview(integrationManager2, integrationId, preview.tool, mapped);
3338
+ if (read.status !== "ok") return true;
3339
+ return hashPreviewBody(read.body) !== previousHash;
3340
+ }
3341
+ async function buildInternalPreview(integrationManager2, integrationId, toolName, actionArgs) {
3342
+ const preview = getToolPreviewConfig(integrationId, toolName);
3343
+ if (!preview) return { status: "none" };
3344
+ const mappedArgs = mapPreviewArgs(preview, actionArgs);
3345
+ if (previewHasNoTarget(preview, mappedArgs)) return { status: "none", reason: "no_target" };
3346
+ const read1 = await readPreview(integrationManager2, integrationId, preview.tool, mappedArgs);
3347
+ if (read1.status !== "ok") return read1;
3348
+ const ok = (extra = {}) => ({
3349
+ status: "ok",
3350
+ integration: integrationId,
3351
+ tool: preview.tool,
3352
+ body: read1.body,
3353
+ ...preview.fields ? { fields: preview.fields } : {},
3354
+ ...extra
3355
+ });
3356
+ if (!preview.version) {
3357
+ return ok();
3358
+ }
3359
+ const approved = actionArgs[preview.version.arg];
3360
+ if (typeof approved !== "string" && typeof approved !== "number") {
3361
+ return ok();
3362
+ }
3363
+ const { [preview.version.arg]: _omit, ...withoutVersion } = actionArgs;
3364
+ const mappedCurrentArgs = mapPreviewArgs(preview, withoutVersion);
3365
+ const read2 = await readPreview(integrationManager2, integrationId, preview.tool, mappedCurrentArgs);
3366
+ if (read2.status !== "ok" || read2.body.structured === void 0) {
3367
+ return ok();
3368
+ }
3369
+ const current = read2.body.structured[preview.version.field];
3370
+ if (typeof current !== "string" && typeof current !== "number") {
3371
+ return ok();
3372
+ }
3373
+ const stale = String(current) !== String(approved);
3374
+ return ok({
3375
+ version: {
3376
+ field: preview.version.field,
3377
+ approved,
3378
+ current,
3379
+ stale,
3380
+ ...stale ? { currentBody: read2.body } : {}
3381
+ }
3382
+ });
3383
+ }
3384
+
3072
3385
  // src/lib/ticket-verify.ts
3073
3386
  import { verifyTicketSignature } from "@hap/core";
3074
3387
  var TicketBindingMismatchError = class extends Error {
@@ -4042,13 +4355,20 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
4042
4355
  toolArgs: enrichedArgs,
4043
4356
  executionContext: wireExecCtx
4044
4357
  });
4358
+ const previewHash = await computeSubmissionPreviewHash(
4359
+ integrationManager2,
4360
+ tool.integrationId,
4361
+ tool.originalName,
4362
+ enrichedArgs
4363
+ );
4045
4364
  state2.proposalSubmissions.record({
4046
4365
  proposalId: proposal.id,
4047
4366
  tool: tool.namespacedName,
4048
4367
  toolArgs: enrichedArgs,
4049
4368
  executionContext: wireExecCtx,
4050
4369
  authorizationId: authzId,
4051
- profileId: auth.profileId
4370
+ profileId: auth.profileId,
4371
+ previewHash
4052
4372
  });
4053
4373
  return {
4054
4374
  content: [{
@@ -4214,13 +4534,20 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
4214
4534
  executionContext: wireExecCtx,
4215
4535
  pendingApprovers: uniqueApprovers
4216
4536
  });
4537
+ const previewHash = await computeSubmissionPreviewHash(
4538
+ integrationManager2,
4539
+ tool.integrationId,
4540
+ tool.originalName,
4541
+ enrichedArgs
4542
+ );
4217
4543
  state2.proposalSubmissions.record({
4218
4544
  proposalId: proposal.id,
4219
4545
  tool: tool.namespacedName,
4220
4546
  toolArgs: enrichedArgs,
4221
4547
  executionContext: wireExecCtx,
4222
4548
  authorizationId: authzId,
4223
- profileId: auth.profileId
4549
+ profileId: auth.profileId,
4550
+ previewHash
4224
4551
  });
4225
4552
  return {
4226
4553
  content: [{
@@ -4955,6 +5282,22 @@ async function executeCommitted(proposal, state2, integrationManager2) {
4955
5282
  `[Suveren MCP] Proposal ${proposal.id}: ticket ${receiptId} was replayed by the AS with no local execution record \u2014 executing once (lost-response recovery).`
4956
5283
  );
4957
5284
  }
5285
+ if (submitted.previewHash) {
5286
+ const changed = await previewChangedSinceSubmission(
5287
+ integrationManager2,
5288
+ integrationId,
5289
+ toolName,
5290
+ proposal.toolArgs,
5291
+ submitted.previewHash
5292
+ );
5293
+ if (changed) {
5294
+ state2.executionJournal.complete(receiptId, "failed", "changed");
5295
+ return {
5296
+ 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.`,
5297
+ isError: true
5298
+ };
5299
+ }
5300
+ }
4958
5301
  try {
4959
5302
  let outgoingArgs = proposal.toolArgs;
4960
5303
  if (discovered && receiptId) {
@@ -4973,7 +5316,7 @@ async function executeCommitted(proposal, state2, integrationManager2) {
4973
5316
  }
4974
5317
  const resultText = result.content?.[0]?.text ?? JSON.stringify(result);
4975
5318
  if (result.isError) {
4976
- state2.executionJournal.complete(receiptId, "failed", "refused");
5319
+ state2.executionJournal.complete(receiptId, "failed", "refused", resultText);
4977
5320
  return {
4978
5321
  text: `Proposal ${proposal.id} was approved, but ${integrationId} refused to run it \u2014 nothing was done.
4979
5322
  Reason: ${resultText}`,
@@ -5003,7 +5346,7 @@ function localOutcomeOfExecuted(proposal, state2) {
5003
5346
  const integration = proposal.tool.split("__")[0];
5004
5347
  if (entry.state === "failed") {
5005
5348
  return {
5006
- 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.` : `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.`,
5349
+ 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.`,
5007
5350
  isError: true
5008
5351
  };
5009
5352
  }
@@ -5308,21 +5651,21 @@ function createMcpServer(state2, integrationManager2) {
5308
5651
  }
5309
5652
 
5310
5653
  // src/lib/agent-contact.ts
5311
- import { existsSync as existsSync12, mkdirSync as mkdirSync10, readFileSync as readFileSync13, renameSync as renameSync4, writeFileSync as writeFileSync10 } from "fs";
5312
- import { dirname as dirname10, join as join12 } from "path";
5654
+ import { existsSync as existsSync14, mkdirSync as mkdirSync11, readFileSync as readFileSync15, renameSync as renameSync5, writeFileSync as writeFileSync11 } from "fs";
5655
+ import { dirname as dirname11, join as join14 } from "path";
5313
5656
  var FILE = "agent-contact.json";
5314
5657
  var REWRITE_AFTER_MS = 6e4;
5315
5658
  var AgentContactStore = class {
5316
5659
  path;
5317
5660
  current;
5318
5661
  constructor(dataDir2) {
5319
- this.path = join12(dataDir2, FILE);
5662
+ this.path = join14(dataDir2, FILE);
5320
5663
  this.current = this.load();
5321
5664
  }
5322
5665
  load() {
5323
- if (!existsSync12(this.path)) return null;
5666
+ if (!existsSync14(this.path)) return null;
5324
5667
  try {
5325
- const v = JSON.parse(readFileSync13(this.path, "utf8"));
5668
+ const v = JSON.parse(readFileSync15(this.path, "utf8"));
5326
5669
  if (typeof v.clientName !== "string" || typeof v.firstSeenAt !== "string" || typeof v.lastSeenAt !== "string") return null;
5327
5670
  return { clientName: v.clientName, clientVersion: v.clientVersion, firstSeenAt: v.firstSeenAt, lastSeenAt: v.lastSeenAt };
5328
5671
  } catch {
@@ -5345,10 +5688,10 @@ var AgentContactStore = class {
5345
5688
  };
5346
5689
  if (!due) return;
5347
5690
  try {
5348
- mkdirSync10(dirname10(this.path), { recursive: true });
5691
+ mkdirSync11(dirname11(this.path), { recursive: true });
5349
5692
  const tmp = `${this.path}.tmp`;
5350
- writeFileSync10(tmp, JSON.stringify(this.current, null, 2));
5351
- renameSync4(tmp, this.path);
5693
+ writeFileSync11(tmp, JSON.stringify(this.current, null, 2));
5694
+ renameSync5(tmp, this.path);
5352
5695
  } catch (err) {
5353
5696
  console.error("[Suveren MCP] Could not save agent contact:", err);
5354
5697
  }
@@ -5382,16 +5725,16 @@ function verifyGateContentHashes(content, auth) {
5382
5725
  }
5383
5726
 
5384
5727
  // src/lib/integration-registry.ts
5385
- import { mkdirSync as mkdirSync11, readFileSync as readFileSync14, writeFileSync as writeFileSync11, existsSync as existsSync13 } from "fs";
5386
- import { homedir as homedir9 } from "os";
5387
- import { dirname as dirname11, join as join13 } from "path";
5388
- var DEFAULT_DATA_DIR = process.env.SUVEREN_DATA_DIR ?? join13(homedir9(), ".suveren");
5728
+ import { mkdirSync as mkdirSync12, readFileSync as readFileSync16, writeFileSync as writeFileSync12, existsSync as existsSync15 } from "fs";
5729
+ import { homedir as homedir10 } from "os";
5730
+ import { dirname as dirname12, join as join15 } from "path";
5731
+ var DEFAULT_DATA_DIR = process.env.SUVEREN_DATA_DIR ?? join15(homedir10(), ".suveren");
5389
5732
  var IntegrationRegistry = class {
5390
5733
  integrations = /* @__PURE__ */ new Map();
5391
5734
  filePath;
5392
5735
  constructor(dataDir2) {
5393
5736
  const dir = dataDir2 ?? DEFAULT_DATA_DIR;
5394
- this.filePath = join13(dir, "integrations.json");
5737
+ this.filePath = join15(dir, "integrations.json");
5395
5738
  this.load();
5396
5739
  }
5397
5740
  // ─── CRUD ───────────────────────────────────────────────────────────────
@@ -5422,12 +5765,12 @@ var IntegrationRegistry = class {
5422
5765
  }
5423
5766
  // ─── Load / Save ────────────────────────────────────────────────────────
5424
5767
  load() {
5425
- if (!existsSync13(this.filePath)) {
5426
- mkdirSync11(dirname11(this.filePath), { recursive: true });
5768
+ if (!existsSync15(this.filePath)) {
5769
+ mkdirSync12(dirname12(this.filePath), { recursive: true });
5427
5770
  return;
5428
5771
  }
5429
5772
  try {
5430
- const raw = readFileSync14(this.filePath, "utf-8");
5773
+ const raw = readFileSync16(this.filePath, "utf-8");
5431
5774
  const data = JSON.parse(raw);
5432
5775
  const migrated = data.integrations.map((i) => {
5433
5776
  if ("toolGating" in i && !("profile" in i)) {
@@ -5456,79 +5799,19 @@ var IntegrationRegistry = class {
5456
5799
  version: 1,
5457
5800
  integrations: Array.from(this.integrations.values())
5458
5801
  };
5459
- mkdirSync11(dirname11(this.filePath), { recursive: true });
5460
- writeFileSync11(this.filePath, JSON.stringify(data, null, 2), { encoding: "utf-8" });
5802
+ mkdirSync12(dirname12(this.filePath), { recursive: true });
5803
+ writeFileSync12(this.filePath, JSON.stringify(data, null, 2), { encoding: "utf-8" });
5461
5804
  }
5462
5805
  };
5463
5806
 
5464
5807
  // src/lib/integration-manager.ts
5465
- import { homedir as homedir10 } from "os";
5466
- import { join as join15, delimiter } from "path";
5467
- import { existsSync as existsSync15, writeFileSync as writeFileSync12, mkdirSync as mkdirSync12, readFileSync as readFileSync16, rmSync } from "fs";
5808
+ import { homedir as homedir11 } from "os";
5809
+ import { join as join16, delimiter } from "path";
5810
+ import { existsSync as existsSync16, writeFileSync as writeFileSync13, mkdirSync as mkdirSync13, readFileSync as readFileSync17, rmSync } from "fs";
5468
5811
  import { execFile } from "child_process";
5469
5812
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
5470
5813
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
5471
5814
 
5472
- // src/lib/manifest-loader.ts
5473
- import { readFileSync as readFileSync15, existsSync as existsSync14 } from "fs";
5474
- import { join as join14, resolve } from "path";
5475
- var EXACT_SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
5476
- function isExactSemver(value) {
5477
- return typeof value === "string" && EXACT_SEMVER_RE.test(value);
5478
- }
5479
- function invalidNpmPinReason(manifest) {
5480
- if (!manifest.npmPackage) return null;
5481
- if (manifest.npmVersion === void 0) {
5482
- return `npmPackage "${manifest.npmPackage}" is set but npmVersion is missing \u2014 every connector manifest must pin an exact tested version`;
5483
- }
5484
- if (!isExactSemver(manifest.npmVersion)) {
5485
- 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`;
5486
- }
5487
- return null;
5488
- }
5489
- var manifests = /* @__PURE__ */ new Map();
5490
- function loadManifests(integrationsDir) {
5491
- const dir = resolve(
5492
- integrationsDir ?? process.env.SUVEREN_MANIFESTS_DIR ?? join14(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "content", "integrations")
5493
- );
5494
- const indexPath = join14(dir, "index.json");
5495
- if (!existsSync14(indexPath)) {
5496
- console.error(`[ManifestLoader] No index.json found at ${indexPath}, skipping manifest loading`);
5497
- return 0;
5498
- }
5499
- let index;
5500
- try {
5501
- index = JSON.parse(readFileSync15(indexPath, "utf-8"));
5502
- } catch (err) {
5503
- console.error(`[ManifestLoader] Failed to parse ${indexPath}:`, err);
5504
- return 0;
5505
- }
5506
- let loaded = 0;
5507
- for (const [id, relativePath] of Object.entries(index.integrations)) {
5508
- const manifestPath = join14(dir, relativePath);
5509
- try {
5510
- const manifest = JSON.parse(readFileSync15(manifestPath, "utf-8"));
5511
- const pinReason = invalidNpmPinReason(manifest);
5512
- if (pinReason) {
5513
- console.error(`[ManifestLoader] Refusing manifest ${id} (${manifestPath}): ${pinReason}`);
5514
- continue;
5515
- }
5516
- manifests.set(id, manifest);
5517
- loaded++;
5518
- } catch (err) {
5519
- console.error(`[ManifestLoader] Failed to load manifest ${id} from ${manifestPath}:`, err);
5520
- }
5521
- }
5522
- console.error(`[ManifestLoader] Loaded ${loaded} integration manifest(s) from ${dir}`);
5523
- return loaded;
5524
- }
5525
- function getManifest(id) {
5526
- return manifests.get(id);
5527
- }
5528
- function getAllManifests() {
5529
- return Array.from(manifests.values());
5530
- }
5531
-
5532
5815
  // src/lib/remote-auth-preflight.ts
5533
5816
  var AUTH_HEADER_RE = /^authorization:\s*(.+)$/i;
5534
5817
  function remotePreflightTarget(command, args) {
@@ -5654,8 +5937,8 @@ function inheritedConnectorEnv(parent, passEnv = []) {
5654
5937
  }
5655
5938
 
5656
5939
  // src/lib/integration-manager.ts
5657
- var DEFAULT_DATA_DIR2 = process.env.SUVEREN_DATA_DIR ?? join15(homedir10(), ".suveren");
5658
- var INTEGRATIONS_DIR = process.env.SUVEREN_INTEGRATIONS_DIR ?? join15(DEFAULT_DATA_DIR2, "integrations");
5940
+ var DEFAULT_DATA_DIR2 = process.env.SUVEREN_DATA_DIR ?? join16(homedir11(), ".suveren");
5941
+ var INTEGRATIONS_DIR = process.env.SUVEREN_INTEGRATIONS_DIR ?? join16(DEFAULT_DATA_DIR2, "integrations");
5659
5942
  var OFFLINE_MODE = process.env.SUVEREN_OFFLINE === "1";
5660
5943
  var installLock = Promise.resolve();
5661
5944
  function withInstallLock(task) {
@@ -5663,7 +5946,7 @@ function withInstallLock(task) {
5663
5946
  installLock = run.then(() => void 0, () => void 0);
5664
5947
  return run;
5665
5948
  }
5666
- var INTEGRATIONS_BIN = join15(INTEGRATIONS_DIR, "node_modules", ".bin");
5949
+ var INTEGRATIONS_BIN = join16(INTEGRATIONS_DIR, "node_modules", ".bin");
5667
5950
  function getIntegrationsBinDir() {
5668
5951
  return INTEGRATIONS_BIN;
5669
5952
  }
@@ -5672,12 +5955,12 @@ function buildPath() {
5672
5955
  return [INTEGRATIONS_BIN, base].join(delimiter);
5673
5956
  }
5674
5957
  function ensureIntegrationsDir() {
5675
- if (!existsSync15(INTEGRATIONS_DIR)) {
5676
- mkdirSync12(INTEGRATIONS_DIR, { recursive: true });
5958
+ if (!existsSync16(INTEGRATIONS_DIR)) {
5959
+ mkdirSync13(INTEGRATIONS_DIR, { recursive: true });
5677
5960
  }
5678
- const pkgPath = join15(INTEGRATIONS_DIR, "package.json");
5679
- if (!existsSync15(pkgPath)) {
5680
- writeFileSync12(pkgPath, JSON.stringify({ name: "suveren-integrations", version: "1.0.0", private: true }, null, 2));
5961
+ const pkgPath = join16(INTEGRATIONS_DIR, "package.json");
5962
+ if (!existsSync16(pkgPath)) {
5963
+ writeFileSync13(pkgPath, JSON.stringify({ name: "suveren-integrations", version: "1.0.0", private: true }, null, 2));
5681
5964
  }
5682
5965
  }
5683
5966
  function readAgeOf(config) {
@@ -5766,19 +6049,19 @@ var IntegrationManager = class {
5766
6049
  * exist, and treat anything else as not-installed (and reinstallable).
5767
6050
  */
5768
6051
  isUsableInstall(npmPackage) {
5769
- const pkgDir = join15(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
5770
- const pkgJson = join15(pkgDir, "package.json");
5771
- if (!existsSync15(pkgDir) || !existsSync15(pkgJson)) return false;
6052
+ const pkgDir = join16(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
6053
+ const pkgJson = join16(pkgDir, "package.json");
6054
+ if (!existsSync16(pkgDir) || !existsSync16(pkgJson)) return false;
5772
6055
  try {
5773
- const pkg = JSON.parse(readFileSync16(pkgJson, "utf8"));
6056
+ const pkg = JSON.parse(readFileSync17(pkgJson, "utf8"));
5774
6057
  const defaultBinName = (pkg.name ?? npmPackage).split("/").pop() ?? "";
5775
6058
  const binMap = typeof pkg.bin === "string" ? { [defaultBinName]: pkg.bin } : pkg.bin ?? {};
5776
6059
  const binEntries = Object.entries(binMap);
5777
6060
  for (const [binName, target] of binEntries) {
5778
- if (!existsSync15(join15(pkgDir, target))) return false;
6061
+ if (!existsSync16(join16(pkgDir, target))) return false;
5779
6062
  if (!this.binShimExists(binName)) return false;
5780
6063
  }
5781
- if (binEntries.length === 0 && pkg.main && !existsSync15(join15(pkgDir, pkg.main))) return false;
6064
+ if (binEntries.length === 0 && pkg.main && !existsSync16(join16(pkgDir, pkg.main))) return false;
5782
6065
  return true;
5783
6066
  } catch {
5784
6067
  return false;
@@ -5793,9 +6076,9 @@ var IntegrationManager = class {
5793
6076
  */
5794
6077
  readInstalledVersion(npmPackage) {
5795
6078
  if (!this.isUsableInstall(npmPackage)) return null;
5796
- const pkgJson = join15(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"), "package.json");
6079
+ const pkgJson = join16(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"), "package.json");
5797
6080
  try {
5798
- const pkg = JSON.parse(readFileSync16(pkgJson, "utf8"));
6081
+ const pkg = JSON.parse(readFileSync17(pkgJson, "utf8"));
5799
6082
  return typeof pkg.version === "string" ? pkg.version : null;
5800
6083
  } catch {
5801
6084
  return null;
@@ -5809,7 +6092,7 @@ var IntegrationManager = class {
5809
6092
  binShimExists(binName) {
5810
6093
  if (!binName) return false;
5811
6094
  const candidates = process.platform === "win32" ? [binName, `${binName}.cmd`, `${binName}.CMD`, `${binName}.ps1`] : [binName];
5812
- return candidates.some((c) => existsSync15(join15(INTEGRATIONS_BIN, c)));
6095
+ return candidates.some((c) => existsSync16(join16(INTEGRATIONS_BIN, c)));
5813
6096
  }
5814
6097
  /**
5815
6098
  * The exact version THIS gateway's CURRENTLY LOADED manifest pins
@@ -5899,8 +6182,8 @@ var IntegrationManager = class {
5899
6182
  * nothing at all.
5900
6183
  */
5901
6184
  async installNow(npmPackage, exactVersion, cleanFirst = true) {
5902
- const pkgDir = join15(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
5903
- if (cleanFirst && existsSync15(pkgDir)) {
6185
+ const pkgDir = join16(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
6186
+ if (cleanFirst && existsSync16(pkgDir)) {
5904
6187
  console.error(`[IntegrationManager] Removing ${exactVersion ? "outdated" : "unusable"} install of ${npmPackage}`);
5905
6188
  try {
5906
6189
  rmSync(pkgDir, { recursive: true, force: true });
@@ -6093,6 +6376,7 @@ var IntegrationManager = class {
6093
6376
  tool.inputSchema ?? {},
6094
6377
  gating?.blockedArgs
6095
6378
  ),
6379
+ outputSchema: tool.outputSchema,
6096
6380
  gating
6097
6381
  };
6098
6382
  });
@@ -6257,7 +6541,11 @@ var IntegrationManager = class {
6257
6541
  type: item.type,
6258
6542
  text: item.text ?? JSON.stringify(item)
6259
6543
  }));
6260
- return { content, isError: result.isError };
6544
+ return {
6545
+ content,
6546
+ isError: result.isError,
6547
+ structuredContent: result.structuredContent
6548
+ };
6261
6549
  } catch (err) {
6262
6550
  const message = err instanceof Error ? err.message : String(err);
6263
6551
  return {
@@ -6806,28 +7094,28 @@ function agentAccess(token, opensSession) {
6806
7094
  }
6807
7095
 
6808
7096
  // src/lib/profile-loader.ts
6809
- import { readFileSync as readFileSync17, existsSync as existsSync16 } from "fs";
6810
- import { join as join16, resolve as resolve2 } from "path";
7097
+ import { readFileSync as readFileSync18, existsSync as existsSync17 } from "fs";
7098
+ import { join as join17, resolve as resolve2 } from "path";
6811
7099
  import { registerProfile } from "@hap/core";
6812
7100
  function loadProfiles(profilesDir) {
6813
- const dir = resolve2(profilesDir ?? process.env.SUVEREN_PROFILES_DIR ?? join16(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "..", "hap-profiles"));
6814
- const indexPath = join16(dir, "index.json");
6815
- if (!existsSync16(indexPath)) {
7101
+ const dir = resolve2(profilesDir ?? process.env.SUVEREN_PROFILES_DIR ?? join17(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "..", "hap-profiles"));
7102
+ const indexPath = join17(dir, "index.json");
7103
+ if (!existsSync17(indexPath)) {
6816
7104
  warnNoProfiles(`No index.json at ${indexPath}`);
6817
7105
  return 0;
6818
7106
  }
6819
7107
  let index;
6820
7108
  try {
6821
- index = JSON.parse(readFileSync17(indexPath, "utf-8"));
7109
+ index = JSON.parse(readFileSync18(indexPath, "utf-8"));
6822
7110
  } catch (err) {
6823
7111
  console.error(`[ProfileLoader] Failed to parse ${indexPath}:`, err);
6824
7112
  return 0;
6825
7113
  }
6826
7114
  let loaded = 0;
6827
7115
  for (const [profileId, relativePath] of Object.entries(index.profiles)) {
6828
- const profilePath = join16(dir, relativePath);
7116
+ const profilePath = join17(dir, relativePath);
6829
7117
  try {
6830
- const profile = JSON.parse(readFileSync17(profilePath, "utf-8"));
7118
+ const profile = JSON.parse(readFileSync18(profilePath, "utf-8"));
6831
7119
  registerProfile(profileId, profile);
6832
7120
  const shortName = profileId.split("/").pop()?.replace(/@.*$/, "");
6833
7121
  if (shortName && shortName !== profileId) {
@@ -7165,20 +7453,20 @@ async function controlPlaneMandate(dryRun, request) {
7165
7453
  }
7166
7454
 
7167
7455
  // src/lib/guides.ts
7168
- import { existsSync as existsSync17, readdirSync, readFileSync as readFileSync18 } from "fs";
7169
- import { dirname as dirname12, join as join17 } from "path";
7456
+ import { existsSync as existsSync18, readdirSync, readFileSync as readFileSync19 } from "fs";
7457
+ import { dirname as dirname13, join as join18 } from "path";
7170
7458
  var FILE_RE = /^(?:(\d+)-)?([a-z0-9][a-z0-9-]*)\.md$/;
7171
7459
  function builtinGuidesDir() {
7172
7460
  if (process.env.SUVEREN_BUILTIN_GUIDES_DIR) return process.env.SUVEREN_BUILTIN_GUIDES_DIR;
7173
7461
  let dir = import.meta.dirname ?? __dirname;
7174
7462
  for (let i = 0; i < 8; i++) {
7175
- const candidate = join17(dir, "content", "guides");
7176
- if (existsSync17(candidate)) return candidate;
7177
- const parent = dirname12(dir);
7463
+ const candidate = join18(dir, "content", "guides");
7464
+ if (existsSync18(candidate)) return candidate;
7465
+ const parent = dirname13(dir);
7178
7466
  if (parent === dir) break;
7179
7467
  dir = parent;
7180
7468
  }
7181
- return join17(import.meta.dirname ?? __dirname, "content", "guides");
7469
+ return join18(import.meta.dirname ?? __dirname, "content", "guides");
7182
7470
  }
7183
7471
  function parse(body) {
7184
7472
  const lines = body.split(/\r?\n/);
@@ -7189,14 +7477,14 @@ function parse(body) {
7189
7477
  return { title, summary };
7190
7478
  }
7191
7479
  function readDir(dir, source) {
7192
- if (!dir || !existsSync17(dir)) return [];
7480
+ if (!dir || !existsSync18(dir)) return [];
7193
7481
  const out = [];
7194
7482
  for (const file of readdirSync(dir)) {
7195
7483
  const m = FILE_RE.exec(file);
7196
7484
  if (!m) continue;
7197
7485
  let body;
7198
7486
  try {
7199
- body = readFileSync18(join17(dir, file), "utf8");
7487
+ body = readFileSync19(join18(dir, file), "utf8");
7200
7488
  } catch {
7201
7489
  continue;
7202
7490
  }
@@ -7506,8 +7794,8 @@ function registerBuiltins(deps, factories = BUILTIN_FACTORIES) {
7506
7794
  import { decodeMandateBlob as decodeMandateBlob3 } from "@hap/core";
7507
7795
 
7508
7796
  // src/lib/committed-executor.ts
7509
- import { existsSync as existsSync18, mkdirSync as mkdirSync13, readFileSync as readFileSync19, unlinkSync as unlinkSync6, writeFileSync as writeFileSync13 } from "fs";
7510
- import { dirname as dirname13, join as join18 } from "path";
7797
+ import { existsSync as existsSync19, mkdirSync as mkdirSync14, readFileSync as readFileSync20, unlinkSync as unlinkSync6, writeFileSync as writeFileSync14 } from "fs";
7798
+ import { dirname as dirname14, join as join19 } from "path";
7511
7799
  var CommittedExecutor = class {
7512
7800
  constructor(run) {
7513
7801
  this.run = run;
@@ -7535,7 +7823,7 @@ var ExecutorLock = class {
7535
7823
  path;
7536
7824
  held = false;
7537
7825
  constructor(dataDir2) {
7538
- this.path = join18(dataDir2, "executor.lock");
7826
+ this.path = join19(dataDir2, "executor.lock");
7539
7827
  }
7540
7828
  /** Try to take the lock. Returns the holder's pid when someone else has it. */
7541
7829
  acquire() {
@@ -7543,9 +7831,9 @@ var ExecutorLock = class {
7543
7831
  if (current && current.pid !== process.pid && isAlive(current.pid)) {
7544
7832
  return { held: false, holderPid: current.pid };
7545
7833
  }
7546
- mkdirSync13(dirname13(this.path), { recursive: true, mode: 448 });
7834
+ mkdirSync14(dirname14(this.path), { recursive: true, mode: 448 });
7547
7835
  const lock = { pid: process.pid, startedAt: Math.floor(Date.now() / 1e3) };
7548
- writeFileSync13(this.path, JSON.stringify(lock), { mode: 384 });
7836
+ writeFileSync14(this.path, JSON.stringify(lock), { mode: 384 });
7549
7837
  this.held = true;
7550
7838
  return { held: true };
7551
7839
  }
@@ -7567,9 +7855,9 @@ var ExecutorLock = class {
7567
7855
  }
7568
7856
  }
7569
7857
  read() {
7570
- if (!existsSync18(this.path)) return null;
7858
+ if (!existsSync19(this.path)) return null;
7571
7859
  try {
7572
- const parsed = JSON.parse(readFileSync19(this.path, "utf8"));
7860
+ const parsed = JSON.parse(readFileSync20(this.path, "utf8"));
7573
7861
  return typeof parsed?.pid === "number" ? parsed : null;
7574
7862
  } catch {
7575
7863
  return null;
@@ -7586,20 +7874,20 @@ function isAlive(pid) {
7586
7874
  }
7587
7875
 
7588
7876
  // bin/http.ts
7589
- import { homedir as homedir11 } from "os";
7590
- import { join as join20 } from "path";
7877
+ import { homedir as homedir12 } from "os";
7878
+ import { join as join21 } from "path";
7591
7879
 
7592
7880
  // src/lib/mcp-as-tracker.ts
7593
- import { existsSync as existsSync19, mkdirSync as mkdirSync14, readFileSync as readFileSync20, writeFileSync as writeFileSync14 } from "fs";
7594
- import { dirname as dirname14, join as join19 } from "path";
7881
+ import { existsSync as existsSync20, mkdirSync as mkdirSync15, readFileSync as readFileSync21, writeFileSync as writeFileSync15 } from "fs";
7882
+ import { dirname as dirname15, join as join20 } from "path";
7595
7883
  function trackerPath(dataDir2) {
7596
- return join19(dataDir2, "mcp-as-pairing.json");
7884
+ return join20(dataDir2, "mcp-as-pairing.json");
7597
7885
  }
7598
7886
  function readMcpPairedAsUrl(dataDir2) {
7599
7887
  const path = trackerPath(dataDir2);
7600
- if (!existsSync19(path)) return null;
7888
+ if (!existsSync20(path)) return null;
7601
7889
  try {
7602
- const data = JSON.parse(readFileSync20(path, "utf-8"));
7890
+ const data = JSON.parse(readFileSync21(path, "utf-8"));
7603
7891
  return typeof data.asUrl === "string" ? data.asUrl : null;
7604
7892
  } catch {
7605
7893
  return null;
@@ -7607,12 +7895,12 @@ function readMcpPairedAsUrl(dataDir2) {
7607
7895
  }
7608
7896
  function writeMcpPairedAsUrl(dataDir2, asUrl) {
7609
7897
  const path = trackerPath(dataDir2);
7610
- mkdirSync14(dirname14(path), { recursive: true, mode: 448 });
7611
- writeFileSync14(path, JSON.stringify({ asUrl }, null, 2), { encoding: "utf-8", mode: 384 });
7898
+ mkdirSync15(dirname15(path), { recursive: true, mode: 448 });
7899
+ writeFileSync15(path, JSON.stringify({ asUrl }, null, 2), { encoding: "utf-8", mode: 384 });
7612
7900
  }
7613
7901
 
7614
7902
  // bin/http.ts
7615
- var dataDir = process.env.SUVEREN_DATA_DIR ?? join20(homedir11(), ".suveren");
7903
+ var dataDir = process.env.SUVEREN_DATA_DIR ?? join21(homedir12(), ".suveren");
7616
7904
  var spUrl = resolveAsUrl(dataDir);
7617
7905
  validatePinTlsForUrl(resolvePinTls(dataDir), spUrl);
7618
7906
  var port = parseInt(process.env.SUVEREN_MCP_PORT ?? "3430", 10);
@@ -7822,6 +8110,29 @@ app.post("/internal/resync-gates", internalOnly, async (_req, res) => {
7822
8110
  }
7823
8111
  res.json({ ok: true, synced, orphaned });
7824
8112
  });
8113
+ app.post("/internal/preview", internalOnly, async (req, res) => {
8114
+ const { proposalId, tool, toolArgs } = req.body;
8115
+ if (!proposalId || !tool || typeof toolArgs !== "object" || toolArgs === null) {
8116
+ res.status(400).json({ error: "Missing proposalId, tool, or toolArgs" });
8117
+ return;
8118
+ }
8119
+ const sep = tool.indexOf("__");
8120
+ if (sep < 0) {
8121
+ res.status(400).json({ error: `Invalid tool name: ${tool}` });
8122
+ return;
8123
+ }
8124
+ const integrationId = tool.slice(0, sep);
8125
+ const toolName = tool.slice(sep + 2);
8126
+ const result = await buildInternalPreview(integrationManager, integrationId, toolName, toolArgs);
8127
+ state.previewLog.record({
8128
+ ts: Date.now(),
8129
+ proposalId,
8130
+ integrationId,
8131
+ tool: toolName,
8132
+ status: result.status
8133
+ });
8134
+ res.json(result);
8135
+ });
7825
8136
  app.post("/internal/add-integration", internalOnly, async (req, res) => {
7826
8137
  try {
7827
8138
  const config = req.body;
@@ -8315,7 +8626,7 @@ app.listen(port, bindHost, () => {
8315
8626
  (proposal) => executeCommitted(proposal, state, integrationManager)
8316
8627
  );
8317
8628
  installCommittedExecutor(committedExecutor);
8318
- const dataDir2 = process.env.SUVEREN_DATA_DIR ?? join20(homedir11(), ".suveren");
8629
+ const dataDir2 = process.env.SUVEREN_DATA_DIR ?? join21(homedir12(), ".suveren");
8319
8630
  const executorLock = new ExecutorLock(dataDir2);
8320
8631
  const lock = executorLock.acquire();
8321
8632
  if (!lock.held) {