@suveren/gateway 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -20,10 +20,19 @@
20
20
  "label": "GitHub token",
21
21
  "type": "password",
22
22
  "placeholder": "github_pat_..."
23
+ },
24
+ {
25
+ "key": "artifactPath",
26
+ "label": "Artifact directory (optional)",
27
+ "type": "text",
28
+ "optional": true,
29
+ "placeholder": "website/dist",
30
+ "description": "Repository-relative directory the host serves as-is, if the built artifact is committed. Lets get_deployment report a digest of the served bytes and derive the source commit from git, so a private staged build can still be released."
23
31
  }
24
32
  ],
25
33
  "envMapping": {
26
- "GITHUB_TOKEN": "githubToken"
34
+ "GITHUB_TOKEN": "githubToken",
35
+ "HAP_DEPLOY_ARTIFACT_PATH": "artifactPath"
27
36
  }
28
37
  },
29
38
  "oauth": null,
@@ -254,7 +254,10 @@ var SPClient = class {
254
254
  const message = first?.message ?? first?.code ?? respBody.error ?? `SP receipt request failed: ${res.status}`;
255
255
  throw new SPReceiptError(message, res.status, respBody);
256
256
  }
257
- return { receipt: respBody.receipt };
257
+ return {
258
+ receipt: respBody.receipt,
259
+ idempotent: respBody.idempotent === true
260
+ };
258
261
  }
259
262
  throw lastError ?? new Error("postReceipt: retries exhausted");
260
263
  }
@@ -1136,6 +1139,93 @@ var ReceiptArchive = class {
1136
1139
  }
1137
1140
  };
1138
1141
 
1142
+ // src/lib/execution-journal.ts
1143
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync6, renameSync } from "fs";
1144
+ import { homedir as homedir5 } from "os";
1145
+ import { dirname as dirname5, join as join6 } from "path";
1146
+ import { createHash } from "crypto";
1147
+ var DEFAULT_DIR5 = process.env.SUVEREN_DATA_DIR ?? join6(homedir5(), ".suveren");
1148
+ var FILE_NAME = "execution-journal.json";
1149
+ var MAX_AGE_SECONDS2 = 90 * 24 * 60 * 60;
1150
+ function hashToolArgs(args) {
1151
+ return "sha256:" + createHash("sha256").update(stableStringify(args)).digest("hex");
1152
+ }
1153
+ function stableStringify(value) {
1154
+ if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "undefined";
1155
+ if (Array.isArray(value)) return "[" + value.map(stableStringify).join(",") + "]";
1156
+ const obj = value;
1157
+ const keys = Object.keys(obj).sort();
1158
+ return "{" + keys.map((k) => JSON.stringify(k) + ":" + stableStringify(obj[k])).join(",") + "}";
1159
+ }
1160
+ var ExecutionJournal = class {
1161
+ filePath;
1162
+ constructor(filePath) {
1163
+ const baseDir = filePath && filePath.endsWith(".json") ? dirname5(filePath) : filePath ?? DEFAULT_DIR5;
1164
+ this.filePath = join6(baseDir, FILE_NAME);
1165
+ }
1166
+ /** Look a ticket up. Always reads from disk — another process may have written. */
1167
+ get(ticketId) {
1168
+ return this.load().entries.find((e) => e.ticketId === ticketId);
1169
+ }
1170
+ /**
1171
+ * Record intent to execute under `ticketId`. Refuses if any row for that
1172
+ * ticket already exists, whatever its state — deciding what an existing row
1173
+ * means is the caller's job, and it differs by state (see commitments.ts).
1174
+ */
1175
+ begin(entry) {
1176
+ const file = this.load();
1177
+ const existing = file.entries.find((e) => e.ticketId === entry.ticketId);
1178
+ if (existing) return { ok: false, existing };
1179
+ file.entries.push({
1180
+ ...entry,
1181
+ state: "intent",
1182
+ startedAt: nowSec(),
1183
+ pid: process.pid
1184
+ });
1185
+ this.persist(file);
1186
+ return { ok: true };
1187
+ }
1188
+ /** Close an `intent` row. Missing row is a programming error, not a silent no-op. */
1189
+ complete(ticketId, state2) {
1190
+ const file = this.load();
1191
+ const row = file.entries.find((e) => e.ticketId === ticketId);
1192
+ if (!row) throw new Error(`execution journal: no row for ticket ${ticketId} to complete`);
1193
+ row.state = state2;
1194
+ row.finishedAt = nowSec();
1195
+ this.persist(file);
1196
+ }
1197
+ /** All rows, newest last. Diagnostic. */
1198
+ getAll() {
1199
+ return [...this.load().entries];
1200
+ }
1201
+ // ─── Persistence ────────────────────────────────────────────────────────
1202
+ load() {
1203
+ if (!existsSync6(this.filePath)) return { version: 1, entries: [] };
1204
+ try {
1205
+ const parsed = JSON.parse(readFileSync6(this.filePath, "utf8"));
1206
+ if (parsed?.version !== 1 || !Array.isArray(parsed.entries)) {
1207
+ throw new Error("unexpected shape");
1208
+ }
1209
+ return parsed;
1210
+ } catch (err) {
1211
+ throw new Error(
1212
+ `execution journal at ${this.filePath} is unreadable (${err instanceof Error ? err.message : String(err)}); refusing to execute until it is inspected`
1213
+ );
1214
+ }
1215
+ }
1216
+ persist(file) {
1217
+ const cutoff = nowSec() - MAX_AGE_SECONDS2;
1218
+ file.entries = file.entries.filter((e) => e.startedAt >= cutoff);
1219
+ mkdirSync5(dirname5(this.filePath), { recursive: true, mode: 448 });
1220
+ const tmp = this.filePath + ".tmp";
1221
+ writeFileSync5(tmp, JSON.stringify(file, null, 2), { mode: 384 });
1222
+ renameSync(tmp, this.filePath);
1223
+ }
1224
+ };
1225
+ function nowSec() {
1226
+ return Math.floor(Date.now() / 1e3);
1227
+ }
1228
+
1139
1229
  // src/lib/gatekeeper.ts
1140
1230
  import { verify } from "@hap/core";
1141
1231
  var MCPGatekeeper = class {
@@ -1216,6 +1306,8 @@ var SharedState = class {
1216
1306
  executionLog;
1217
1307
  denialLog;
1218
1308
  receiptArchive;
1309
+ /** Which tickets this Gatekeeper has executed — the "one execution per ticket" half of exactly-once. */
1310
+ executionJournal;
1219
1311
  gatekeeper;
1220
1312
  constructor(spUrl2, gateStorePath) {
1221
1313
  this.spClient = new SPClient(spUrl2);
@@ -1224,6 +1316,7 @@ var SharedState = class {
1224
1316
  this.executionLog = new ExecutionLog(gateStorePath);
1225
1317
  this.denialLog = new DenialLog(gateStorePath);
1226
1318
  this.receiptArchive = new ReceiptArchive(gateStorePath);
1319
+ this.executionJournal = new ExecutionJournal(gateStorePath);
1227
1320
  this.gatekeeper = new MCPGatekeeper(this.cache);
1228
1321
  }
1229
1322
  /**
@@ -1372,17 +1465,17 @@ function formatConsumptionFull(entries) {
1372
1465
  }
1373
1466
 
1374
1467
  // src/lib/context-loader.ts
1375
- import { readFileSync as readFileSync6, existsSync as existsSync6 } from "fs";
1376
- import { homedir as homedir5 } from "os";
1377
- import { join as join6 } from "path";
1378
- var DEFAULT_DIR5 = process.env.SUVEREN_DATA_DIR ?? join6(homedir5(), ".suveren");
1468
+ import { readFileSync as readFileSync7, existsSync as existsSync7 } from "fs";
1469
+ import { homedir as homedir6 } from "os";
1470
+ import { join as join7 } from "path";
1471
+ var DEFAULT_DIR6 = process.env.SUVEREN_DATA_DIR ?? join7(homedir6(), ".suveren");
1379
1472
  var BRIEF_MAX_CHARS = 16 * 1024;
1380
1473
  function readContextFile(dataDir) {
1381
- const dir = dataDir ?? DEFAULT_DIR5;
1382
- const filePath = join6(dir, "context.md");
1383
- if (!existsSync6(filePath)) return null;
1474
+ const dir = dataDir ?? DEFAULT_DIR6;
1475
+ const filePath = join7(dir, "context.md");
1476
+ if (!existsSync7(filePath)) return null;
1384
1477
  try {
1385
- const content = readFileSync6(filePath, "utf-8").trim();
1478
+ const content = readFileSync7(filePath, "utf-8").trim();
1386
1479
  return content || null;
1387
1480
  } catch {
1388
1481
  return null;
@@ -2458,6 +2551,30 @@ Proposal ID: ${proposal.id}. Use check-pending-commitments to track status.`
2458
2551
  let outgoingArgs = shouldAttachFooter() && receiptId ? appendVerificationFooter(tool, args, receiptId, auth.subjects?.[0]) : args;
2459
2552
  if (receiptId) outgoingArgs = attachReceiptId(tool, outgoingArgs, receiptId);
2460
2553
  outgoingArgs = encodeOutgoingArgs(tool, outgoingArgs);
2554
+ if (receiptId) {
2555
+ const begun = state2.executionJournal.begin({
2556
+ ticketId: receiptId,
2557
+ tool: tool.namespacedName,
2558
+ argsHash: hashToolArgs(args)
2559
+ });
2560
+ if (!begun.ok) {
2561
+ return {
2562
+ content: [{
2563
+ type: "text",
2564
+ text: `Blocked: ticket ${receiptId} was already used to execute ${tool.namespacedName} (state: ${begun.existing.state}). Not running it again.`
2565
+ }],
2566
+ isError: true
2567
+ };
2568
+ }
2569
+ try {
2570
+ const result = await integrationManager2.callTool(tool.integrationId, tool.originalName, outgoingArgs);
2571
+ state2.executionJournal.complete(receiptId, "done");
2572
+ return result;
2573
+ } catch (err) {
2574
+ state2.executionJournal.complete(receiptId, "failed");
2575
+ throw err;
2576
+ }
2577
+ }
2461
2578
  return integrationManager2.callTool(tool.integrationId, tool.originalName, outgoingArgs);
2462
2579
  }
2463
2580
  });
@@ -2786,6 +2903,13 @@ function listIntegrationsHandler(state2, integrationManager2) {
2786
2903
 
2787
2904
  // src/tools/commitments.ts
2788
2905
  import { ContentBindingError as ContentBindingError2 } from "@hap/core";
2906
+ var installedExecutor = null;
2907
+ function installCommittedExecutor(executor) {
2908
+ installedExecutor = executor;
2909
+ }
2910
+ function runCommitted(proposal, state2, integrationManager2) {
2911
+ return installedExecutor ? installedExecutor.execute(proposal) : executeCommitted(proposal, state2, integrationManager2);
2912
+ }
2789
2913
  async function executeCommitted(proposal, state2, integrationManager2) {
2790
2914
  if (!integrationManager2) {
2791
2915
  return { text: `Proposal ${proposal.id} committed but integration manager unavailable for execution.`, isError: true };
@@ -2805,6 +2929,7 @@ async function executeCommitted(proposal, state2, integrationManager2) {
2805
2929
  }
2806
2930
  const cachedAuth = state2.cache.getAllAuthorizations().find((a) => a.authorizationId === proposal.authorizationId);
2807
2931
  let receiptId;
2932
+ let replayed = false;
2808
2933
  try {
2809
2934
  const binding = computeContentBinding(
2810
2935
  proposal.profileId,
@@ -2812,7 +2937,7 @@ async function executeCommitted(proposal, state2, integrationManager2) {
2812
2937
  proposal.toolArgs,
2813
2938
  proposalActionType
2814
2939
  );
2815
- const { receipt } = await state2.spClient.postReceipt({
2940
+ const { receipt, idempotent } = await state2.spClient.postReceipt({
2816
2941
  authorizationId: proposal.authorizationId,
2817
2942
  // Optional cross-check — the AS fails closed on a mismatch. Parity
2818
2943
  // with the automatic path (tool-proxy.ts).
@@ -2837,6 +2962,7 @@ async function executeCommitted(proposal, state2, integrationManager2) {
2837
2962
  ...binding ? { contentHash: binding.contentHash, contentBinding: binding.contentBinding } : {}
2838
2963
  });
2839
2964
  receiptId = typeof receipt?.id === "string" ? receipt.id : void 0;
2965
+ replayed = idempotent;
2840
2966
  await state2.archiveReceipt(receipt, {
2841
2967
  authorizationId: proposal.authorizationId,
2842
2968
  profileId: proposal.profileId,
@@ -2874,6 +3000,36 @@ async function executeCommitted(proposal, state2, integrationManager2) {
2874
3000
  isError: true
2875
3001
  };
2876
3002
  }
3003
+ if (!receiptId) {
3004
+ return {
3005
+ text: `Proposal ${proposal.id}: the ticket carries no id \u2014 refusing to execute an action that could not be recorded.`,
3006
+ isError: true
3007
+ };
3008
+ }
3009
+ const begun = state2.executionJournal.begin({
3010
+ ticketId: receiptId,
3011
+ proposalId: proposal.id,
3012
+ tool: proposal.tool,
3013
+ argsHash: hashToolArgs(proposal.toolArgs)
3014
+ });
3015
+ if (!begun.ok) {
3016
+ const prior = begun.existing;
3017
+ const when = new Date(prior.startedAt * 1e3).toISOString();
3018
+ if (prior.state === "done") {
3019
+ return {
3020
+ text: `Proposal ${proposal.id} was already executed under ticket ${receiptId} at ${when}. Not running it again.`
3021
+ };
3022
+ }
3023
+ return {
3024
+ text: `Proposal ${proposal.id}: an execution under ticket ${receiptId} started at ${when} (pid ${prior.pid}) and ${prior.state === "failed" ? "reported failure" : "never reported completion"}. Not re-running: the action may already have taken effect. Check the downstream system before deciding.`,
3025
+ isError: true
3026
+ };
3027
+ }
3028
+ if (replayed) {
3029
+ console.error(
3030
+ `[Suveren MCP] Proposal ${proposal.id}: ticket ${receiptId} was replayed by the AS with no local execution record \u2014 executing once (lost-response recovery).`
3031
+ );
3032
+ }
2877
3033
  try {
2878
3034
  let outgoingArgs = proposal.toolArgs;
2879
3035
  if (discovered && receiptId) {
@@ -2883,7 +3039,14 @@ async function executeCommitted(proposal, state2, integrationManager2) {
2883
3039
  outgoingArgs = attachReceiptId(discovered, outgoingArgs, receiptId);
2884
3040
  outgoingArgs = encodeOutgoingArgs(discovered, outgoingArgs);
2885
3041
  }
2886
- const result = await integrationManager2.callTool(integrationId, toolName, outgoingArgs);
3042
+ let result;
3043
+ try {
3044
+ result = await integrationManager2.callTool(integrationId, toolName, outgoingArgs);
3045
+ } catch (err) {
3046
+ state2.executionJournal.complete(receiptId, "failed");
3047
+ throw err;
3048
+ }
3049
+ state2.executionJournal.complete(receiptId, "done");
2887
3050
  state2.executionLog.record({
2888
3051
  profileId: proposal.profileId,
2889
3052
  path: proposal.path,
@@ -2918,7 +3081,7 @@ function checkPendingCommitmentsHandler(state2, integrationManager2) {
2918
3081
  };
2919
3082
  }
2920
3083
  if (match.status === "committed") {
2921
- const { text, isError } = await executeCommitted(match, state2, integrationManager2);
3084
+ const { text, isError } = await runCommitted(match, state2, integrationManager2);
2922
3085
  return {
2923
3086
  content: [{ type: "text", text }],
2924
3087
  ...isError ? { isError: true } : {}
@@ -3172,16 +3335,16 @@ function verifyGateContentHashes(content, auth) {
3172
3335
  }
3173
3336
 
3174
3337
  // src/lib/integration-registry.ts
3175
- import { mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync5, existsSync as existsSync7 } from "fs";
3176
- import { homedir as homedir6 } from "os";
3177
- import { dirname as dirname5, join as join7 } from "path";
3178
- var DEFAULT_DATA_DIR = process.env.SUVEREN_DATA_DIR ?? join7(homedir6(), ".suveren");
3338
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync8, writeFileSync as writeFileSync6, existsSync as existsSync8 } from "fs";
3339
+ import { homedir as homedir7 } from "os";
3340
+ import { dirname as dirname6, join as join8 } from "path";
3341
+ var DEFAULT_DATA_DIR = process.env.SUVEREN_DATA_DIR ?? join8(homedir7(), ".suveren");
3179
3342
  var IntegrationRegistry = class {
3180
3343
  integrations = /* @__PURE__ */ new Map();
3181
3344
  filePath;
3182
3345
  constructor(dataDir) {
3183
3346
  const dir = dataDir ?? DEFAULT_DATA_DIR;
3184
- this.filePath = join7(dir, "integrations.json");
3347
+ this.filePath = join8(dir, "integrations.json");
3185
3348
  this.load();
3186
3349
  }
3187
3350
  // ─── CRUD ───────────────────────────────────────────────────────────────
@@ -3212,12 +3375,12 @@ var IntegrationRegistry = class {
3212
3375
  }
3213
3376
  // ─── Load / Save ────────────────────────────────────────────────────────
3214
3377
  load() {
3215
- if (!existsSync7(this.filePath)) {
3216
- mkdirSync5(dirname5(this.filePath), { recursive: true });
3378
+ if (!existsSync8(this.filePath)) {
3379
+ mkdirSync6(dirname6(this.filePath), { recursive: true });
3217
3380
  return;
3218
3381
  }
3219
3382
  try {
3220
- const raw = readFileSync7(this.filePath, "utf-8");
3383
+ const raw = readFileSync8(this.filePath, "utf-8");
3221
3384
  const data = JSON.parse(raw);
3222
3385
  const migrated = data.integrations.map((i) => {
3223
3386
  if ("toolGating" in i && !("profile" in i)) {
@@ -3246,39 +3409,39 @@ var IntegrationRegistry = class {
3246
3409
  version: 1,
3247
3410
  integrations: Array.from(this.integrations.values())
3248
3411
  };
3249
- mkdirSync5(dirname5(this.filePath), { recursive: true });
3250
- writeFileSync5(this.filePath, JSON.stringify(data, null, 2), { encoding: "utf-8" });
3412
+ mkdirSync6(dirname6(this.filePath), { recursive: true });
3413
+ writeFileSync6(this.filePath, JSON.stringify(data, null, 2), { encoding: "utf-8" });
3251
3414
  }
3252
3415
  };
3253
3416
 
3254
3417
  // src/lib/integration-manager.ts
3255
- import { homedir as homedir7 } from "os";
3256
- import { join as join8, delimiter } from "path";
3257
- import { existsSync as existsSync8, writeFileSync as writeFileSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync8, rmSync } from "fs";
3418
+ import { homedir as homedir8 } from "os";
3419
+ import { join as join9, delimiter } from "path";
3420
+ import { existsSync as existsSync9, writeFileSync as writeFileSync7, mkdirSync as mkdirSync7, readFileSync as readFileSync9, rmSync } from "fs";
3258
3421
  import { execFile } from "child_process";
3259
3422
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3260
3423
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
3261
3424
  import { getProfile as getProfile5 } from "@hap/core";
3262
- var DEFAULT_DATA_DIR2 = process.env.SUVEREN_DATA_DIR ?? join8(homedir7(), ".suveren");
3263
- var INTEGRATIONS_DIR = process.env.SUVEREN_INTEGRATIONS_DIR ?? join8(DEFAULT_DATA_DIR2, "integrations");
3425
+ var DEFAULT_DATA_DIR2 = process.env.SUVEREN_DATA_DIR ?? join9(homedir8(), ".suveren");
3426
+ var INTEGRATIONS_DIR = process.env.SUVEREN_INTEGRATIONS_DIR ?? join9(DEFAULT_DATA_DIR2, "integrations");
3264
3427
  var installLock = Promise.resolve();
3265
3428
  function withInstallLock(task) {
3266
3429
  const run = installLock.then(task, task);
3267
3430
  installLock = run.then(() => void 0, () => void 0);
3268
3431
  return run;
3269
3432
  }
3270
- var INTEGRATIONS_BIN = join8(INTEGRATIONS_DIR, "node_modules", ".bin");
3433
+ var INTEGRATIONS_BIN = join9(INTEGRATIONS_DIR, "node_modules", ".bin");
3271
3434
  function buildPath() {
3272
3435
  const base = process.env.PATH ?? "";
3273
3436
  return [INTEGRATIONS_BIN, base].join(delimiter);
3274
3437
  }
3275
3438
  function ensureIntegrationsDir() {
3276
- if (!existsSync8(INTEGRATIONS_DIR)) {
3277
- mkdirSync6(INTEGRATIONS_DIR, { recursive: true });
3439
+ if (!existsSync9(INTEGRATIONS_DIR)) {
3440
+ mkdirSync7(INTEGRATIONS_DIR, { recursive: true });
3278
3441
  }
3279
- const pkgPath = join8(INTEGRATIONS_DIR, "package.json");
3280
- if (!existsSync8(pkgPath)) {
3281
- writeFileSync6(pkgPath, JSON.stringify({ name: "suveren-integrations", version: "1.0.0", private: true }, null, 2));
3442
+ const pkgPath = join9(INTEGRATIONS_DIR, "package.json");
3443
+ if (!existsSync9(pkgPath)) {
3444
+ writeFileSync7(pkgPath, JSON.stringify({ name: "suveren-integrations", version: "1.0.0", private: true }, null, 2));
3282
3445
  }
3283
3446
  }
3284
3447
  function readAgeOf(config) {
@@ -3344,19 +3507,19 @@ var IntegrationManager = class {
3344
3507
  * exist, and treat anything else as not-installed (and reinstallable).
3345
3508
  */
3346
3509
  isUsableInstall(npmPackage) {
3347
- const pkgDir = join8(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
3348
- const pkgJson = join8(pkgDir, "package.json");
3349
- if (!existsSync8(pkgDir) || !existsSync8(pkgJson)) return false;
3510
+ const pkgDir = join9(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
3511
+ const pkgJson = join9(pkgDir, "package.json");
3512
+ if (!existsSync9(pkgDir) || !existsSync9(pkgJson)) return false;
3350
3513
  try {
3351
- const pkg = JSON.parse(readFileSync8(pkgJson, "utf8"));
3514
+ const pkg = JSON.parse(readFileSync9(pkgJson, "utf8"));
3352
3515
  const defaultBinName = (pkg.name ?? npmPackage).split("/").pop() ?? "";
3353
3516
  const binMap = typeof pkg.bin === "string" ? { [defaultBinName]: pkg.bin } : pkg.bin ?? {};
3354
3517
  const binEntries = Object.entries(binMap);
3355
3518
  for (const [binName, target] of binEntries) {
3356
- if (!existsSync8(join8(pkgDir, target))) return false;
3519
+ if (!existsSync9(join9(pkgDir, target))) return false;
3357
3520
  if (!this.binShimExists(binName)) return false;
3358
3521
  }
3359
- if (binEntries.length === 0 && pkg.main && !existsSync8(join8(pkgDir, pkg.main))) return false;
3522
+ if (binEntries.length === 0 && pkg.main && !existsSync9(join9(pkgDir, pkg.main))) return false;
3360
3523
  return true;
3361
3524
  } catch {
3362
3525
  return false;
@@ -3370,7 +3533,7 @@ var IntegrationManager = class {
3370
3533
  binShimExists(binName) {
3371
3534
  if (!binName) return false;
3372
3535
  const candidates = process.platform === "win32" ? [binName, `${binName}.cmd`, `${binName}.CMD`, `${binName}.ps1`] : [binName];
3373
- return candidates.some((c) => existsSync8(join8(INTEGRATIONS_BIN, c)));
3536
+ return candidates.some((c) => existsSync9(join9(INTEGRATIONS_BIN, c)));
3374
3537
  }
3375
3538
  /**
3376
3539
  * Install an npm package into the managed integrations directory if not
@@ -3394,8 +3557,8 @@ var IntegrationManager = class {
3394
3557
  }
3395
3558
  /** The actual install. Callers MUST hold the install lock. */
3396
3559
  async installNow(npmPackage) {
3397
- const pkgDir = join8(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
3398
- if (existsSync8(pkgDir)) {
3560
+ const pkgDir = join9(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
3561
+ if (existsSync9(pkgDir)) {
3399
3562
  console.error(`[IntegrationManager] Removing unusable install of ${npmPackage}`);
3400
3563
  try {
3401
3564
  rmSync(pkgDir, { recursive: true, force: true });
@@ -3811,28 +3974,28 @@ var IntegrationManager = class {
3811
3974
  };
3812
3975
 
3813
3976
  // src/lib/profile-loader.ts
3814
- import { readFileSync as readFileSync9, existsSync as existsSync9 } from "fs";
3815
- import { join as join9, resolve } from "path";
3977
+ import { readFileSync as readFileSync10, existsSync as existsSync10 } from "fs";
3978
+ import { join as join10, resolve } from "path";
3816
3979
  import { registerProfile } from "@hap/core";
3817
3980
  function loadProfiles(profilesDir) {
3818
- const dir = resolve(profilesDir ?? process.env.SUVEREN_PROFILES_DIR ?? join9(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "..", "hap-profiles"));
3819
- const indexPath = join9(dir, "index.json");
3820
- if (!existsSync9(indexPath)) {
3981
+ const dir = resolve(profilesDir ?? process.env.SUVEREN_PROFILES_DIR ?? join10(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "..", "hap-profiles"));
3982
+ const indexPath = join10(dir, "index.json");
3983
+ if (!existsSync10(indexPath)) {
3821
3984
  warnNoProfiles(`No index.json at ${indexPath}`);
3822
3985
  return 0;
3823
3986
  }
3824
3987
  let index;
3825
3988
  try {
3826
- index = JSON.parse(readFileSync9(indexPath, "utf-8"));
3989
+ index = JSON.parse(readFileSync10(indexPath, "utf-8"));
3827
3990
  } catch (err) {
3828
3991
  console.error(`[ProfileLoader] Failed to parse ${indexPath}:`, err);
3829
3992
  return 0;
3830
3993
  }
3831
3994
  let loaded = 0;
3832
3995
  for (const [profileId, relativePath] of Object.entries(index.profiles)) {
3833
- const profilePath = join9(dir, relativePath);
3996
+ const profilePath = join10(dir, relativePath);
3834
3997
  try {
3835
- const profile = JSON.parse(readFileSync9(profilePath, "utf-8"));
3998
+ const profile = JSON.parse(readFileSync10(profilePath, "utf-8"));
3836
3999
  registerProfile(profileId, profile);
3837
4000
  const shortName = profileId.split("/").pop()?.replace(/@.*$/, "");
3838
4001
  if (shortName && shortName !== profileId) {
@@ -3865,30 +4028,30 @@ function warnNoProfiles(reason) {
3865
4028
  }
3866
4029
 
3867
4030
  // src/lib/manifest-loader.ts
3868
- import { readFileSync as readFileSync10, existsSync as existsSync10 } from "fs";
3869
- import { join as join10, resolve as resolve2 } from "path";
4031
+ import { readFileSync as readFileSync11, existsSync as existsSync11 } from "fs";
4032
+ import { join as join11, resolve as resolve2 } from "path";
3870
4033
  var manifests = /* @__PURE__ */ new Map();
3871
4034
  function loadManifests(integrationsDir) {
3872
4035
  const dir = resolve2(
3873
- integrationsDir ?? process.env.SUVEREN_MANIFESTS_DIR ?? join10(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "content", "integrations")
4036
+ integrationsDir ?? process.env.SUVEREN_MANIFESTS_DIR ?? join11(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "content", "integrations")
3874
4037
  );
3875
- const indexPath = join10(dir, "index.json");
3876
- if (!existsSync10(indexPath)) {
4038
+ const indexPath = join11(dir, "index.json");
4039
+ if (!existsSync11(indexPath)) {
3877
4040
  console.error(`[ManifestLoader] No index.json found at ${indexPath}, skipping manifest loading`);
3878
4041
  return 0;
3879
4042
  }
3880
4043
  let index;
3881
4044
  try {
3882
- index = JSON.parse(readFileSync10(indexPath, "utf-8"));
4045
+ index = JSON.parse(readFileSync11(indexPath, "utf-8"));
3883
4046
  } catch (err) {
3884
4047
  console.error(`[ManifestLoader] Failed to parse ${indexPath}:`, err);
3885
4048
  return 0;
3886
4049
  }
3887
4050
  let loaded = 0;
3888
4051
  for (const [id, relativePath] of Object.entries(index.integrations)) {
3889
- const manifestPath = join10(dir, relativePath);
4052
+ const manifestPath = join11(dir, relativePath);
3890
4053
  try {
3891
- const manifest = JSON.parse(readFileSync10(manifestPath, "utf-8"));
4054
+ const manifest = JSON.parse(readFileSync11(manifestPath, "utf-8"));
3892
4055
  manifests.set(id, manifest);
3893
4056
  loaded++;
3894
4057
  } catch (err) {
@@ -3907,6 +4070,90 @@ function getAllManifests() {
3907
4070
 
3908
4071
  // bin/http.ts
3909
4072
  import { decodeAttestationBlob as decodeAttestationBlob3 } from "@hap/core";
4073
+
4074
+ // src/lib/committed-executor.ts
4075
+ import { existsSync as existsSync12, mkdirSync as mkdirSync8, readFileSync as readFileSync12, unlinkSync as unlinkSync5, writeFileSync as writeFileSync8 } from "fs";
4076
+ import { dirname as dirname7, join as join12 } from "path";
4077
+ var CommittedExecutor = class {
4078
+ constructor(run) {
4079
+ this.run = run;
4080
+ }
4081
+ inFlight = /* @__PURE__ */ new Map();
4082
+ chain = Promise.resolve();
4083
+ /** Execute `proposal` once, however many callers ask while it is running. */
4084
+ execute(proposal) {
4085
+ const existing = this.inFlight.get(proposal.id);
4086
+ if (existing) return existing;
4087
+ const p = this.chain.then(() => this.run(proposal)).finally(() => this.inFlight.delete(proposal.id));
4088
+ this.chain = p.then(
4089
+ () => void 0,
4090
+ () => void 0
4091
+ );
4092
+ this.inFlight.set(proposal.id, p);
4093
+ return p;
4094
+ }
4095
+ /** Proposal ids currently queued or running. Diagnostic. */
4096
+ get pending() {
4097
+ return [...this.inFlight.keys()];
4098
+ }
4099
+ };
4100
+ var ExecutorLock = class {
4101
+ path;
4102
+ held = false;
4103
+ constructor(dataDir) {
4104
+ this.path = join12(dataDir, "executor.lock");
4105
+ }
4106
+ /** Try to take the lock. Returns the holder's pid when someone else has it. */
4107
+ acquire() {
4108
+ const current = this.read();
4109
+ if (current && current.pid !== process.pid && isAlive(current.pid)) {
4110
+ return { held: false, holderPid: current.pid };
4111
+ }
4112
+ mkdirSync8(dirname7(this.path), { recursive: true, mode: 448 });
4113
+ const lock = { pid: process.pid, startedAt: Math.floor(Date.now() / 1e3) };
4114
+ writeFileSync8(this.path, JSON.stringify(lock), { mode: 384 });
4115
+ this.held = true;
4116
+ return { held: true };
4117
+ }
4118
+ /** True if this process currently holds the lock (re-checks the file). */
4119
+ isHeld() {
4120
+ if (!this.held) return false;
4121
+ const current = this.read();
4122
+ return current?.pid === process.pid;
4123
+ }
4124
+ release() {
4125
+ if (!this.held) return;
4126
+ this.held = false;
4127
+ const current = this.read();
4128
+ if (current?.pid === process.pid) {
4129
+ try {
4130
+ unlinkSync5(this.path);
4131
+ } catch {
4132
+ }
4133
+ }
4134
+ }
4135
+ read() {
4136
+ if (!existsSync12(this.path)) return null;
4137
+ try {
4138
+ const parsed = JSON.parse(readFileSync12(this.path, "utf8"));
4139
+ return typeof parsed?.pid === "number" ? parsed : null;
4140
+ } catch {
4141
+ return null;
4142
+ }
4143
+ }
4144
+ };
4145
+ function isAlive(pid) {
4146
+ try {
4147
+ process.kill(pid, 0);
4148
+ return true;
4149
+ } catch (err) {
4150
+ return err.code === "EPERM";
4151
+ }
4152
+ }
4153
+
4154
+ // bin/http.ts
4155
+ import { homedir as homedir9 } from "os";
4156
+ import { join as join13 } from "path";
3910
4157
  var spUrl = process.env.SUVEREN_AS_URL ?? "https://www.suveren.ai";
3911
4158
  var port = parseInt(process.env.SUVEREN_MCP_PORT ?? "3430", 10);
3912
4159
  var state = new SharedState(spUrl);
@@ -4397,12 +4644,31 @@ app.listen(port, "0.0.0.0", () => {
4397
4644
  }
4398
4645
  });
4399
4646
  const PROPOSAL_POLL_INTERVAL = 5e3;
4647
+ const committedExecutor = new CommittedExecutor(
4648
+ (proposal) => executeCommitted(proposal, state, integrationManager)
4649
+ );
4650
+ installCommittedExecutor(committedExecutor);
4651
+ const dataDir = process.env.SUVEREN_DATA_DIR ?? join13(homedir9(), ".suveren");
4652
+ const executorLock = new ExecutorLock(dataDir);
4653
+ const lock = executorLock.acquire();
4654
+ if (!lock.held) {
4655
+ console.error(
4656
+ `[Suveren MCP] Committed-proposal execution is DISABLED in this process: pid ${lock.holderPid} holds ${dataDir}/executor.lock. Approved proposals will run there. Stop that gateway if this one should execute.`
4657
+ );
4658
+ }
4659
+ for (const sig of ["SIGINT", "SIGTERM", "exit"]) {
4660
+ process.once(sig, () => executorLock.release());
4661
+ }
4662
+ let draining = false;
4400
4663
  async function executeCommittedProposals() {
4664
+ if (!executorLock.isHeld()) return;
4665
+ if (draining) return;
4666
+ draining = true;
4401
4667
  try {
4402
4668
  const committed = await state.spClient.getCommittedProposals();
4403
4669
  for (const proposal of committed) {
4404
4670
  try {
4405
- const { text, isError } = await executeCommitted(proposal, state, integrationManager);
4671
+ const { text, isError } = await committedExecutor.execute(proposal);
4406
4672
  console.error(
4407
4673
  isError ? `[Suveren MCP] Auto-exec proposal ${proposal.id}: ${text}` : `[Suveren MCP] Auto-executed proposal ${proposal.id}: ${proposal.tool}`
4408
4674
  );
@@ -4411,6 +4677,8 @@ app.listen(port, "0.0.0.0", () => {
4411
4677
  }
4412
4678
  }
4413
4679
  } catch {
4680
+ } finally {
4681
+ draining = false;
4414
4682
  }
4415
4683
  }
4416
4684
  setInterval(executeCommittedProposals, PROPOSAL_POLL_INTERVAL);
@@ -157,24 +157,24 @@ const hc = (baseUrl, options) => createProxy(function proxyCallback(opts) {
157
157
  const path = parts.join("/");
158
158
  const url = (0, import_utils.mergePath)(baseUrl, path);
159
159
  if (method === "url" || method === "path") {
160
- let result = url;
160
+ let result = (0, import_utils.removeIndexString)(url);
161
161
  if (opts.args[0]) {
162
162
  if (opts.args[0].param) {
163
- result = (0, import_utils.replaceUrlParam)(url, opts.args[0].param);
163
+ result = (0, import_utils.replaceUrlParam)(result, opts.args[0].param);
164
164
  }
165
165
  if (opts.args[0].query) {
166
166
  result = appendQueryParams(result, buildSearchParamsOption(opts.args[0].query));
167
167
  }
168
168
  }
169
- result = (0, import_utils.removeIndexString)(result);
170
169
  if (method === "url") {
171
170
  return new URL(result);
172
171
  }
173
172
  return result.slice(baseUrl.replace(/\/+$/, "").length).replace(/^\/?/, "/");
174
173
  }
175
174
  if (method === "ws") {
175
+ const normalizedUrl = (0, import_utils.removeIndexString)(url);
176
176
  const webSocketUrl = (0, import_utils.replaceUrlProtocol)(
177
- opts.args[0]?.param ? (0, import_utils.replaceUrlParam)(url, opts.args[0].param) : url,
177
+ opts.args[0]?.param ? (0, import_utils.replaceUrlParam)(normalizedUrl, opts.args[0].param) : normalizedUrl,
178
178
  "ws"
179
179
  );
180
180
  const targetUrl = new URL(webSocketUrl);