@suveren/gateway 0.21.0 → 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.
@@ -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,252 @@ 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
+ 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
+
3072
3380
  // src/lib/ticket-verify.ts
3073
3381
  import { verifyTicketSignature } from "@hap/core";
3074
3382
  var TicketBindingMismatchError = class extends Error {
@@ -4042,13 +4350,20 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
4042
4350
  toolArgs: enrichedArgs,
4043
4351
  executionContext: wireExecCtx
4044
4352
  });
4353
+ const previewHash = await computeSubmissionPreviewHash(
4354
+ integrationManager2,
4355
+ tool.integrationId,
4356
+ tool.originalName,
4357
+ enrichedArgs
4358
+ );
4045
4359
  state2.proposalSubmissions.record({
4046
4360
  proposalId: proposal.id,
4047
4361
  tool: tool.namespacedName,
4048
4362
  toolArgs: enrichedArgs,
4049
4363
  executionContext: wireExecCtx,
4050
4364
  authorizationId: authzId,
4051
- profileId: auth.profileId
4365
+ profileId: auth.profileId,
4366
+ previewHash
4052
4367
  });
4053
4368
  return {
4054
4369
  content: [{
@@ -4214,13 +4529,20 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
4214
4529
  executionContext: wireExecCtx,
4215
4530
  pendingApprovers: uniqueApprovers
4216
4531
  });
4532
+ const previewHash = await computeSubmissionPreviewHash(
4533
+ integrationManager2,
4534
+ tool.integrationId,
4535
+ tool.originalName,
4536
+ enrichedArgs
4537
+ );
4217
4538
  state2.proposalSubmissions.record({
4218
4539
  proposalId: proposal.id,
4219
4540
  tool: tool.namespacedName,
4220
4541
  toolArgs: enrichedArgs,
4221
4542
  executionContext: wireExecCtx,
4222
4543
  authorizationId: authzId,
4223
- profileId: auth.profileId
4544
+ profileId: auth.profileId,
4545
+ previewHash
4224
4546
  });
4225
4547
  return {
4226
4548
  content: [{
@@ -4955,6 +5277,22 @@ async function executeCommitted(proposal, state2, integrationManager2) {
4955
5277
  `[Suveren MCP] Proposal ${proposal.id}: ticket ${receiptId} was replayed by the AS with no local execution record \u2014 executing once (lost-response recovery).`
4956
5278
  );
4957
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
+ }
4958
5296
  try {
4959
5297
  let outgoingArgs = proposal.toolArgs;
4960
5298
  if (discovered && receiptId) {
@@ -4973,7 +5311,7 @@ async function executeCommitted(proposal, state2, integrationManager2) {
4973
5311
  }
4974
5312
  const resultText = result.content?.[0]?.text ?? JSON.stringify(result);
4975
5313
  if (result.isError) {
4976
- state2.executionJournal.complete(receiptId, "failed", "refused");
5314
+ state2.executionJournal.complete(receiptId, "failed", "refused", resultText);
4977
5315
  return {
4978
5316
  text: `Proposal ${proposal.id} was approved, but ${integrationId} refused to run it \u2014 nothing was done.
4979
5317
  Reason: ${resultText}`,
@@ -5003,7 +5341,7 @@ function localOutcomeOfExecuted(proposal, state2) {
5003
5341
  const integration = proposal.tool.split("__")[0];
5004
5342
  if (entry.state === "failed") {
5005
5343
  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.`,
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.`,
5007
5345
  isError: true
5008
5346
  };
5009
5347
  }
@@ -5308,21 +5646,21 @@ function createMcpServer(state2, integrationManager2) {
5308
5646
  }
5309
5647
 
5310
5648
  // 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";
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";
5313
5651
  var FILE = "agent-contact.json";
5314
5652
  var REWRITE_AFTER_MS = 6e4;
5315
5653
  var AgentContactStore = class {
5316
5654
  path;
5317
5655
  current;
5318
5656
  constructor(dataDir2) {
5319
- this.path = join12(dataDir2, FILE);
5657
+ this.path = join14(dataDir2, FILE);
5320
5658
  this.current = this.load();
5321
5659
  }
5322
5660
  load() {
5323
- if (!existsSync12(this.path)) return null;
5661
+ if (!existsSync14(this.path)) return null;
5324
5662
  try {
5325
- const v = JSON.parse(readFileSync13(this.path, "utf8"));
5663
+ const v = JSON.parse(readFileSync15(this.path, "utf8"));
5326
5664
  if (typeof v.clientName !== "string" || typeof v.firstSeenAt !== "string" || typeof v.lastSeenAt !== "string") return null;
5327
5665
  return { clientName: v.clientName, clientVersion: v.clientVersion, firstSeenAt: v.firstSeenAt, lastSeenAt: v.lastSeenAt };
5328
5666
  } catch {
@@ -5345,10 +5683,10 @@ var AgentContactStore = class {
5345
5683
  };
5346
5684
  if (!due) return;
5347
5685
  try {
5348
- mkdirSync10(dirname10(this.path), { recursive: true });
5686
+ mkdirSync11(dirname11(this.path), { recursive: true });
5349
5687
  const tmp = `${this.path}.tmp`;
5350
- writeFileSync10(tmp, JSON.stringify(this.current, null, 2));
5351
- renameSync4(tmp, this.path);
5688
+ writeFileSync11(tmp, JSON.stringify(this.current, null, 2));
5689
+ renameSync5(tmp, this.path);
5352
5690
  } catch (err) {
5353
5691
  console.error("[Suveren MCP] Could not save agent contact:", err);
5354
5692
  }
@@ -5382,16 +5720,16 @@ function verifyGateContentHashes(content, auth) {
5382
5720
  }
5383
5721
 
5384
5722
  // 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");
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");
5389
5727
  var IntegrationRegistry = class {
5390
5728
  integrations = /* @__PURE__ */ new Map();
5391
5729
  filePath;
5392
5730
  constructor(dataDir2) {
5393
5731
  const dir = dataDir2 ?? DEFAULT_DATA_DIR;
5394
- this.filePath = join13(dir, "integrations.json");
5732
+ this.filePath = join15(dir, "integrations.json");
5395
5733
  this.load();
5396
5734
  }
5397
5735
  // ─── CRUD ───────────────────────────────────────────────────────────────
@@ -5422,12 +5760,12 @@ var IntegrationRegistry = class {
5422
5760
  }
5423
5761
  // ─── Load / Save ────────────────────────────────────────────────────────
5424
5762
  load() {
5425
- if (!existsSync13(this.filePath)) {
5426
- mkdirSync11(dirname11(this.filePath), { recursive: true });
5763
+ if (!existsSync15(this.filePath)) {
5764
+ mkdirSync12(dirname12(this.filePath), { recursive: true });
5427
5765
  return;
5428
5766
  }
5429
5767
  try {
5430
- const raw = readFileSync14(this.filePath, "utf-8");
5768
+ const raw = readFileSync16(this.filePath, "utf-8");
5431
5769
  const data = JSON.parse(raw);
5432
5770
  const migrated = data.integrations.map((i) => {
5433
5771
  if ("toolGating" in i && !("profile" in i)) {
@@ -5456,79 +5794,19 @@ var IntegrationRegistry = class {
5456
5794
  version: 1,
5457
5795
  integrations: Array.from(this.integrations.values())
5458
5796
  };
5459
- mkdirSync11(dirname11(this.filePath), { recursive: true });
5460
- 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" });
5461
5799
  }
5462
5800
  };
5463
5801
 
5464
5802
  // 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";
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";
5468
5806
  import { execFile } from "child_process";
5469
5807
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
5470
5808
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
5471
5809
 
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
5810
  // src/lib/remote-auth-preflight.ts
5533
5811
  var AUTH_HEADER_RE = /^authorization:\s*(.+)$/i;
5534
5812
  function remotePreflightTarget(command, args) {
@@ -5654,8 +5932,8 @@ function inheritedConnectorEnv(parent, passEnv = []) {
5654
5932
  }
5655
5933
 
5656
5934
  // 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");
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");
5659
5937
  var OFFLINE_MODE = process.env.SUVEREN_OFFLINE === "1";
5660
5938
  var installLock = Promise.resolve();
5661
5939
  function withInstallLock(task) {
@@ -5663,7 +5941,7 @@ function withInstallLock(task) {
5663
5941
  installLock = run.then(() => void 0, () => void 0);
5664
5942
  return run;
5665
5943
  }
5666
- var INTEGRATIONS_BIN = join15(INTEGRATIONS_DIR, "node_modules", ".bin");
5944
+ var INTEGRATIONS_BIN = join16(INTEGRATIONS_DIR, "node_modules", ".bin");
5667
5945
  function getIntegrationsBinDir() {
5668
5946
  return INTEGRATIONS_BIN;
5669
5947
  }
@@ -5672,12 +5950,12 @@ function buildPath() {
5672
5950
  return [INTEGRATIONS_BIN, base].join(delimiter);
5673
5951
  }
5674
5952
  function ensureIntegrationsDir() {
5675
- if (!existsSync15(INTEGRATIONS_DIR)) {
5676
- mkdirSync12(INTEGRATIONS_DIR, { recursive: true });
5953
+ if (!existsSync16(INTEGRATIONS_DIR)) {
5954
+ mkdirSync13(INTEGRATIONS_DIR, { recursive: true });
5677
5955
  }
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));
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));
5681
5959
  }
5682
5960
  }
5683
5961
  function readAgeOf(config) {
@@ -5766,19 +6044,19 @@ var IntegrationManager = class {
5766
6044
  * exist, and treat anything else as not-installed (and reinstallable).
5767
6045
  */
5768
6046
  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;
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;
5772
6050
  try {
5773
- const pkg = JSON.parse(readFileSync16(pkgJson, "utf8"));
6051
+ const pkg = JSON.parse(readFileSync17(pkgJson, "utf8"));
5774
6052
  const defaultBinName = (pkg.name ?? npmPackage).split("/").pop() ?? "";
5775
6053
  const binMap = typeof pkg.bin === "string" ? { [defaultBinName]: pkg.bin } : pkg.bin ?? {};
5776
6054
  const binEntries = Object.entries(binMap);
5777
6055
  for (const [binName, target] of binEntries) {
5778
- if (!existsSync15(join15(pkgDir, target))) return false;
6056
+ if (!existsSync16(join16(pkgDir, target))) return false;
5779
6057
  if (!this.binShimExists(binName)) return false;
5780
6058
  }
5781
- 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;
5782
6060
  return true;
5783
6061
  } catch {
5784
6062
  return false;
@@ -5793,9 +6071,9 @@ var IntegrationManager = class {
5793
6071
  */
5794
6072
  readInstalledVersion(npmPackage) {
5795
6073
  if (!this.isUsableInstall(npmPackage)) return null;
5796
- const pkgJson = join15(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"), "package.json");
6074
+ const pkgJson = join16(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"), "package.json");
5797
6075
  try {
5798
- const pkg = JSON.parse(readFileSync16(pkgJson, "utf8"));
6076
+ const pkg = JSON.parse(readFileSync17(pkgJson, "utf8"));
5799
6077
  return typeof pkg.version === "string" ? pkg.version : null;
5800
6078
  } catch {
5801
6079
  return null;
@@ -5809,7 +6087,7 @@ var IntegrationManager = class {
5809
6087
  binShimExists(binName) {
5810
6088
  if (!binName) return false;
5811
6089
  const candidates = process.platform === "win32" ? [binName, `${binName}.cmd`, `${binName}.CMD`, `${binName}.ps1`] : [binName];
5812
- return candidates.some((c) => existsSync15(join15(INTEGRATIONS_BIN, c)));
6090
+ return candidates.some((c) => existsSync16(join16(INTEGRATIONS_BIN, c)));
5813
6091
  }
5814
6092
  /**
5815
6093
  * The exact version THIS gateway's CURRENTLY LOADED manifest pins
@@ -5899,8 +6177,8 @@ var IntegrationManager = class {
5899
6177
  * nothing at all.
5900
6178
  */
5901
6179
  async installNow(npmPackage, exactVersion, cleanFirst = true) {
5902
- const pkgDir = join15(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
5903
- if (cleanFirst && existsSync15(pkgDir)) {
6180
+ const pkgDir = join16(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
6181
+ if (cleanFirst && existsSync16(pkgDir)) {
5904
6182
  console.error(`[IntegrationManager] Removing ${exactVersion ? "outdated" : "unusable"} install of ${npmPackage}`);
5905
6183
  try {
5906
6184
  rmSync(pkgDir, { recursive: true, force: true });
@@ -6093,6 +6371,7 @@ var IntegrationManager = class {
6093
6371
  tool.inputSchema ?? {},
6094
6372
  gating?.blockedArgs
6095
6373
  ),
6374
+ outputSchema: tool.outputSchema,
6096
6375
  gating
6097
6376
  };
6098
6377
  });
@@ -6257,7 +6536,11 @@ var IntegrationManager = class {
6257
6536
  type: item.type,
6258
6537
  text: item.text ?? JSON.stringify(item)
6259
6538
  }));
6260
- return { content, isError: result.isError };
6539
+ return {
6540
+ content,
6541
+ isError: result.isError,
6542
+ structuredContent: result.structuredContent
6543
+ };
6261
6544
  } catch (err) {
6262
6545
  const message = err instanceof Error ? err.message : String(err);
6263
6546
  return {
@@ -6806,28 +7089,28 @@ function agentAccess(token, opensSession) {
6806
7089
  }
6807
7090
 
6808
7091
  // 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";
7092
+ import { readFileSync as readFileSync18, existsSync as existsSync17 } from "fs";
7093
+ import { join as join17, resolve as resolve2 } from "path";
6811
7094
  import { registerProfile } from "@hap/core";
6812
7095
  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)) {
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)) {
6816
7099
  warnNoProfiles(`No index.json at ${indexPath}`);
6817
7100
  return 0;
6818
7101
  }
6819
7102
  let index;
6820
7103
  try {
6821
- index = JSON.parse(readFileSync17(indexPath, "utf-8"));
7104
+ index = JSON.parse(readFileSync18(indexPath, "utf-8"));
6822
7105
  } catch (err) {
6823
7106
  console.error(`[ProfileLoader] Failed to parse ${indexPath}:`, err);
6824
7107
  return 0;
6825
7108
  }
6826
7109
  let loaded = 0;
6827
7110
  for (const [profileId, relativePath] of Object.entries(index.profiles)) {
6828
- const profilePath = join16(dir, relativePath);
7111
+ const profilePath = join17(dir, relativePath);
6829
7112
  try {
6830
- const profile = JSON.parse(readFileSync17(profilePath, "utf-8"));
7113
+ const profile = JSON.parse(readFileSync18(profilePath, "utf-8"));
6831
7114
  registerProfile(profileId, profile);
6832
7115
  const shortName = profileId.split("/").pop()?.replace(/@.*$/, "");
6833
7116
  if (shortName && shortName !== profileId) {
@@ -7165,20 +7448,20 @@ async function controlPlaneMandate(dryRun, request) {
7165
7448
  }
7166
7449
 
7167
7450
  // 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";
7451
+ import { existsSync as existsSync18, readdirSync, readFileSync as readFileSync19 } from "fs";
7452
+ import { dirname as dirname13, join as join18 } from "path";
7170
7453
  var FILE_RE = /^(?:(\d+)-)?([a-z0-9][a-z0-9-]*)\.md$/;
7171
7454
  function builtinGuidesDir() {
7172
7455
  if (process.env.SUVEREN_BUILTIN_GUIDES_DIR) return process.env.SUVEREN_BUILTIN_GUIDES_DIR;
7173
7456
  let dir = import.meta.dirname ?? __dirname;
7174
7457
  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);
7458
+ const candidate = join18(dir, "content", "guides");
7459
+ if (existsSync18(candidate)) return candidate;
7460
+ const parent = dirname13(dir);
7178
7461
  if (parent === dir) break;
7179
7462
  dir = parent;
7180
7463
  }
7181
- return join17(import.meta.dirname ?? __dirname, "content", "guides");
7464
+ return join18(import.meta.dirname ?? __dirname, "content", "guides");
7182
7465
  }
7183
7466
  function parse(body) {
7184
7467
  const lines = body.split(/\r?\n/);
@@ -7189,14 +7472,14 @@ function parse(body) {
7189
7472
  return { title, summary };
7190
7473
  }
7191
7474
  function readDir(dir, source) {
7192
- if (!dir || !existsSync17(dir)) return [];
7475
+ if (!dir || !existsSync18(dir)) return [];
7193
7476
  const out = [];
7194
7477
  for (const file of readdirSync(dir)) {
7195
7478
  const m = FILE_RE.exec(file);
7196
7479
  if (!m) continue;
7197
7480
  let body;
7198
7481
  try {
7199
- body = readFileSync18(join17(dir, file), "utf8");
7482
+ body = readFileSync19(join18(dir, file), "utf8");
7200
7483
  } catch {
7201
7484
  continue;
7202
7485
  }
@@ -7506,8 +7789,8 @@ function registerBuiltins(deps, factories = BUILTIN_FACTORIES) {
7506
7789
  import { decodeMandateBlob as decodeMandateBlob3 } from "@hap/core";
7507
7790
 
7508
7791
  // 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";
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";
7511
7794
  var CommittedExecutor = class {
7512
7795
  constructor(run) {
7513
7796
  this.run = run;
@@ -7535,7 +7818,7 @@ var ExecutorLock = class {
7535
7818
  path;
7536
7819
  held = false;
7537
7820
  constructor(dataDir2) {
7538
- this.path = join18(dataDir2, "executor.lock");
7821
+ this.path = join19(dataDir2, "executor.lock");
7539
7822
  }
7540
7823
  /** Try to take the lock. Returns the holder's pid when someone else has it. */
7541
7824
  acquire() {
@@ -7543,9 +7826,9 @@ var ExecutorLock = class {
7543
7826
  if (current && current.pid !== process.pid && isAlive(current.pid)) {
7544
7827
  return { held: false, holderPid: current.pid };
7545
7828
  }
7546
- mkdirSync13(dirname13(this.path), { recursive: true, mode: 448 });
7829
+ mkdirSync14(dirname14(this.path), { recursive: true, mode: 448 });
7547
7830
  const lock = { pid: process.pid, startedAt: Math.floor(Date.now() / 1e3) };
7548
- writeFileSync13(this.path, JSON.stringify(lock), { mode: 384 });
7831
+ writeFileSync14(this.path, JSON.stringify(lock), { mode: 384 });
7549
7832
  this.held = true;
7550
7833
  return { held: true };
7551
7834
  }
@@ -7567,9 +7850,9 @@ var ExecutorLock = class {
7567
7850
  }
7568
7851
  }
7569
7852
  read() {
7570
- if (!existsSync18(this.path)) return null;
7853
+ if (!existsSync19(this.path)) return null;
7571
7854
  try {
7572
- const parsed = JSON.parse(readFileSync19(this.path, "utf8"));
7855
+ const parsed = JSON.parse(readFileSync20(this.path, "utf8"));
7573
7856
  return typeof parsed?.pid === "number" ? parsed : null;
7574
7857
  } catch {
7575
7858
  return null;
@@ -7586,20 +7869,20 @@ function isAlive(pid) {
7586
7869
  }
7587
7870
 
7588
7871
  // bin/http.ts
7589
- import { homedir as homedir11 } from "os";
7590
- import { join as join20 } from "path";
7872
+ import { homedir as homedir12 } from "os";
7873
+ import { join as join21 } from "path";
7591
7874
 
7592
7875
  // 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";
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";
7595
7878
  function trackerPath(dataDir2) {
7596
- return join19(dataDir2, "mcp-as-pairing.json");
7879
+ return join20(dataDir2, "mcp-as-pairing.json");
7597
7880
  }
7598
7881
  function readMcpPairedAsUrl(dataDir2) {
7599
7882
  const path = trackerPath(dataDir2);
7600
- if (!existsSync19(path)) return null;
7883
+ if (!existsSync20(path)) return null;
7601
7884
  try {
7602
- const data = JSON.parse(readFileSync20(path, "utf-8"));
7885
+ const data = JSON.parse(readFileSync21(path, "utf-8"));
7603
7886
  return typeof data.asUrl === "string" ? data.asUrl : null;
7604
7887
  } catch {
7605
7888
  return null;
@@ -7607,12 +7890,12 @@ function readMcpPairedAsUrl(dataDir2) {
7607
7890
  }
7608
7891
  function writeMcpPairedAsUrl(dataDir2, asUrl) {
7609
7892
  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 });
7893
+ mkdirSync15(dirname15(path), { recursive: true, mode: 448 });
7894
+ writeFileSync15(path, JSON.stringify({ asUrl }, null, 2), { encoding: "utf-8", mode: 384 });
7612
7895
  }
7613
7896
 
7614
7897
  // bin/http.ts
7615
- var dataDir = process.env.SUVEREN_DATA_DIR ?? join20(homedir11(), ".suveren");
7898
+ var dataDir = process.env.SUVEREN_DATA_DIR ?? join21(homedir12(), ".suveren");
7616
7899
  var spUrl = resolveAsUrl(dataDir);
7617
7900
  validatePinTlsForUrl(resolvePinTls(dataDir), spUrl);
7618
7901
  var port = parseInt(process.env.SUVEREN_MCP_PORT ?? "3430", 10);
@@ -7822,6 +8105,29 @@ app.post("/internal/resync-gates", internalOnly, async (_req, res) => {
7822
8105
  }
7823
8106
  res.json({ ok: true, synced, orphaned });
7824
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
+ });
7825
8131
  app.post("/internal/add-integration", internalOnly, async (req, res) => {
7826
8132
  try {
7827
8133
  const config = req.body;
@@ -8315,7 +8621,7 @@ app.listen(port, bindHost, () => {
8315
8621
  (proposal) => executeCommitted(proposal, state, integrationManager)
8316
8622
  );
8317
8623
  installCommittedExecutor(committedExecutor);
8318
- const dataDir2 = process.env.SUVEREN_DATA_DIR ?? join20(homedir11(), ".suveren");
8624
+ const dataDir2 = process.env.SUVEREN_DATA_DIR ?? join21(homedir12(), ".suveren");
8319
8625
  const executorLock = new ExecutorLock(dataDir2);
8320
8626
  const lock = executorLock.acquire();
8321
8627
  if (!lock.held) {