@suveren/gateway 0.7.6 → 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.
Files changed (181) hide show
  1. package/content/integrations/deploy-github.json +10 -1
  2. package/content/integrations/mollie.json +103 -28
  3. package/dist/mcp-server/http.mjs +510 -132
  4. package/dist/ui/assets/{index-CsrM6RQr.js → index-BFnSVa9N.js} +22 -22
  5. package/dist/ui/index.html +1 -1
  6. package/node_modules/@hap/core/dist/index.d.mts +29 -6
  7. package/node_modules/@hap/core/dist/index.d.ts +29 -6
  8. package/node_modules/@hap/core/dist/index.js +54 -8
  9. package/node_modules/@hap/core/dist/index.mjs +53 -8
  10. package/node_modules/@hap/core/package.json +2 -1
  11. package/node_modules/@hap/core/src/frame.ts +115 -14
  12. package/node_modules/@hap/core/src/gatekeeper.ts +26 -0
  13. package/node_modules/@hap/core/src/types.ts +7 -1
  14. package/node_modules/@types/node/README.md +1 -1
  15. package/node_modules/@types/node/buffer.buffer.d.ts +5 -0
  16. package/node_modules/@types/node/crypto.d.ts +0 -2
  17. package/node_modules/@types/node/dgram.d.ts +119 -4
  18. package/node_modules/@types/node/ffi.d.ts +1 -1
  19. package/node_modules/@types/node/fs/promises.d.ts +61 -65
  20. package/node_modules/@types/node/fs.d.ts +86 -76
  21. package/node_modules/@types/node/http.d.ts +1 -1
  22. package/node_modules/@types/node/http2.d.ts +2 -2
  23. package/node_modules/@types/node/index.d.ts +1 -0
  24. package/node_modules/@types/node/inspector.d.ts +2 -1
  25. package/node_modules/@types/node/net.d.ts +108 -16
  26. package/node_modules/@types/node/package.json +2 -2
  27. package/node_modules/@types/node/quic.d.ts +37 -11
  28. package/node_modules/@types/node/stream/iter.d.ts +4 -4
  29. package/node_modules/@types/node/tls.d.ts +24 -0
  30. package/node_modules/@types/node/ts5.6/buffer.buffer.d.ts +5 -0
  31. package/node_modules/@types/node/ts5.6/index.d.ts +1 -0
  32. package/node_modules/@types/node/ts5.7/index.d.ts +1 -0
  33. package/node_modules/@types/node/tty.d.ts +4 -3
  34. package/node_modules/@types/node/vfs.d.ts +210 -0
  35. package/node_modules/@types/node/vm.d.ts +1 -1
  36. package/node_modules/express-rate-limit/dist/index.cjs +6 -3
  37. package/node_modules/express-rate-limit/dist/index.d.cts +11 -0
  38. package/node_modules/express-rate-limit/dist/index.d.mts +11 -0
  39. package/node_modules/express-rate-limit/dist/index.d.ts +11 -0
  40. package/node_modules/express-rate-limit/dist/index.mjs +6 -3
  41. package/node_modules/express-rate-limit/{license.md → license} +7 -6
  42. package/node_modules/express-rate-limit/package.json +5 -5
  43. package/node_modules/express-rate-limit/readme.md +1 -0
  44. package/node_modules/fast-uri/index.js +20 -5
  45. package/node_modules/fast-uri/lib/utils.js +8 -1
  46. package/node_modules/fast-uri/package.json +1 -1
  47. package/node_modules/fast-uri/test/component-safe-serialization.test.js +58 -0
  48. package/node_modules/fast-uri/test/ipv6-validation.test.js +34 -0
  49. package/node_modules/hono/dist/cjs/client/client.js +4 -4
  50. package/node_modules/hono/dist/cjs/hono-base.js +7 -6
  51. package/node_modules/hono/dist/cjs/jsx/base.js +18 -1
  52. package/node_modules/hono/dist/cjs/jsx/components.js +25 -26
  53. package/node_modules/hono/dist/cjs/jsx/context.js +5 -5
  54. package/node_modules/hono/dist/cjs/jsx/dom/server.js +5 -1
  55. package/node_modules/hono/dist/cjs/jsx/intrinsic-element/components.js +2 -2
  56. package/node_modules/hono/dist/cjs/jsx/streaming.js +4 -5
  57. package/node_modules/hono/dist/client/client.js +4 -4
  58. package/node_modules/hono/dist/hono-base.js +7 -6
  59. package/node_modules/hono/dist/jsx/base.js +15 -1
  60. package/node_modules/hono/dist/jsx/components.js +26 -27
  61. package/node_modules/hono/dist/jsx/context.js +6 -6
  62. package/node_modules/hono/dist/jsx/dom/server.js +5 -1
  63. package/node_modules/hono/dist/jsx/intrinsic-element/components.js +3 -3
  64. package/node_modules/hono/dist/jsx/streaming.js +5 -6
  65. package/node_modules/hono/dist/types/context.d.ts +2 -2
  66. package/node_modules/hono/dist/types/jsx/base.d.ts +3 -0
  67. package/node_modules/hono/package.json +2 -2
  68. package/node_modules/ip-address/README.md +92 -82
  69. package/node_modules/ip-address/dist/common.d.ts +20 -0
  70. package/node_modules/ip-address/dist/common.js +36 -0
  71. package/node_modules/ip-address/dist/common.js.map +1 -1
  72. package/node_modules/ip-address/dist/ipv4.d.ts +50 -0
  73. package/node_modules/ip-address/dist/ipv4.js +75 -0
  74. package/node_modules/ip-address/dist/ipv4.js.map +1 -1
  75. package/node_modules/ip-address/dist/ipv6.d.ts +79 -10
  76. package/node_modules/ip-address/dist/ipv6.js +120 -30
  77. package/node_modules/ip-address/dist/ipv6.js.map +1 -1
  78. package/node_modules/ip-address/dist/v4/constants.d.ts +12 -0
  79. package/node_modules/ip-address/dist/v4/constants.js +40 -1
  80. package/node_modules/ip-address/dist/v4/constants.js.map +1 -1
  81. package/node_modules/ip-address/dist/v6/constants.d.ts +14 -0
  82. package/node_modules/ip-address/dist/v6/constants.js +47 -1
  83. package/node_modules/ip-address/dist/v6/constants.js.map +1 -1
  84. package/node_modules/ip-address/package.json +1 -1
  85. package/node_modules/jose/dist/types/jwe/compact/decrypt.d.ts +7 -16
  86. package/node_modules/jose/dist/types/jwe/compact/encrypt.d.ts +6 -17
  87. package/node_modules/jose/dist/types/jwe/flattened/decrypt.d.ts +7 -16
  88. package/node_modules/jose/dist/types/jwe/flattened/encrypt.d.ts +11 -34
  89. package/node_modules/jose/dist/types/jwe/general/decrypt.d.ts +7 -16
  90. package/node_modules/jose/dist/types/jwe/general/encrypt.d.ts +15 -35
  91. package/node_modules/jose/dist/types/jwk/embedded.d.ts +3 -8
  92. package/node_modules/jose/dist/types/jwk/thumbprint.d.ts +5 -5
  93. package/node_modules/jose/dist/types/jwks/local.d.ts +6 -10
  94. package/node_modules/jose/dist/types/jwks/remote.d.ts +13 -24
  95. package/node_modules/jose/dist/types/jws/compact/sign.d.ts +5 -10
  96. package/node_modules/jose/dist/types/jws/compact/verify.d.ts +9 -18
  97. package/node_modules/jose/dist/types/jws/flattened/sign.d.ts +7 -16
  98. package/node_modules/jose/dist/types/jws/flattened/verify.d.ts +9 -18
  99. package/node_modules/jose/dist/types/jws/general/sign.d.ts +11 -21
  100. package/node_modules/jose/dist/types/jws/general/verify.d.ts +9 -18
  101. package/node_modules/jose/dist/types/jwt/decrypt.d.ts +10 -20
  102. package/node_modules/jose/dist/types/jwt/encrypt.d.ts +6 -13
  103. package/node_modules/jose/dist/types/jwt/sign.d.ts +5 -6
  104. package/node_modules/jose/dist/types/jwt/unsecured.d.ts +9 -14
  105. package/node_modules/jose/dist/types/jwt/verify.d.ts +9 -19
  106. package/node_modules/jose/dist/types/key/export.d.ts +4 -4
  107. package/node_modules/jose/dist/types/key/generate_key_pair.d.ts +6 -8
  108. package/node_modules/jose/dist/types/key/generate_secret.d.ts +4 -11
  109. package/node_modules/jose/dist/types/key/import.d.ts +10 -17
  110. package/node_modules/jose/dist/types/types.d.ts +44 -84
  111. package/node_modules/jose/dist/types/util/base64url.d.ts +2 -2
  112. package/node_modules/jose/dist/types/util/decode_jwt.d.ts +3 -10
  113. package/node_modules/jose/dist/types/util/decode_protected_header.d.ts +4 -4
  114. package/node_modules/jose/dist/types/util/errors.d.ts +26 -53
  115. package/node_modules/jose/dist/webapi/index.js +70 -32
  116. package/node_modules/jose/dist/webapi/jwe/compact/decrypt.js +7 -8
  117. package/node_modules/jose/dist/webapi/jwe/compact/encrypt.js +42 -48
  118. package/node_modules/jose/dist/webapi/jwe/flattened/decrypt.js +14 -14
  119. package/node_modules/jose/dist/webapi/jwe/flattened/encrypt.js +53 -64
  120. package/node_modules/jose/dist/webapi/jwe/general/decrypt.js +38 -49
  121. package/node_modules/jose/dist/webapi/jwe/general/encrypt.js +121 -169
  122. package/node_modules/jose/dist/webapi/jwk/embedded.js +30 -32
  123. package/node_modules/jose/dist/webapi/jwk/thumbprint.js +54 -70
  124. package/node_modules/jose/dist/webapi/jwks/local.js +47 -64
  125. package/node_modules/jose/dist/webapi/jwks/remote.js +88 -136
  126. package/node_modules/jose/dist/webapi/jws/compact/sign.js +21 -21
  127. package/node_modules/jose/dist/webapi/jws/compact/verify.js +7 -8
  128. package/node_modules/jose/dist/webapi/jws/flattened/sign.js +29 -31
  129. package/node_modules/jose/dist/webapi/jws/flattened/verify.js +21 -24
  130. package/node_modules/jose/dist/webapi/jws/general/sign.js +61 -72
  131. package/node_modules/jose/dist/webapi/jws/general/verify.js +45 -69
  132. package/node_modules/jose/dist/webapi/jwt/decrypt.js +13 -20
  133. package/node_modules/jose/dist/webapi/jwt/encrypt.js +57 -70
  134. package/node_modules/jose/dist/webapi/jwt/sign.js +17 -16
  135. package/node_modules/jose/dist/webapi/jwt/unsecured.js +32 -40
  136. package/node_modules/jose/dist/webapi/jwt/verify.js +12 -14
  137. package/node_modules/jose/dist/webapi/key/export.js +32 -37
  138. package/node_modules/jose/dist/webapi/key/generate_key_pair.js +40 -48
  139. package/node_modules/jose/dist/webapi/key/generate_secret.js +35 -40
  140. package/node_modules/jose/dist/webapi/key/import.js +52 -56
  141. package/node_modules/jose/dist/webapi/lib/asn1.js +130 -169
  142. package/node_modules/jose/dist/webapi/lib/base64.js +18 -20
  143. package/node_modules/jose/dist/webapi/lib/buffer_utils.js +34 -38
  144. package/node_modules/jose/dist/webapi/lib/content_encryption.js +79 -115
  145. package/node_modules/jose/dist/webapi/lib/crypto_key.js +24 -25
  146. package/node_modules/jose/dist/webapi/lib/deflate.js +45 -47
  147. package/node_modules/jose/dist/webapi/lib/helpers.js +40 -37
  148. package/node_modules/jose/dist/webapi/lib/invalid_key_input.js +10 -24
  149. package/node_modules/jose/dist/webapi/lib/is_key_like.js +17 -15
  150. package/node_modules/jose/dist/webapi/lib/jwe_algorithms.js +106 -93
  151. package/node_modules/jose/dist/webapi/lib/jwe_decrypt.js +207 -230
  152. package/node_modules/jose/dist/webapi/lib/jwe_encrypt.js +104 -138
  153. package/node_modules/jose/dist/webapi/lib/jwk_metadata.js +17 -18
  154. package/node_modules/jose/dist/webapi/lib/jwk_to_key.js +9 -16
  155. package/node_modules/jose/dist/webapi/lib/jws_algorithms.js +62 -61
  156. package/node_modules/jose/dist/webapi/lib/jws_sign.js +50 -75
  157. package/node_modules/jose/dist/webapi/lib/jws_verify.js +80 -125
  158. package/node_modules/jose/dist/webapi/lib/jwt_claims_set.js +123 -209
  159. package/node_modules/jose/dist/webapi/lib/key.js +84 -136
  160. package/node_modules/jose/dist/webapi/lib/key_algorithm.js +13 -8
  161. package/node_modules/jose/dist/webapi/lib/key_descriptor.js +8 -6
  162. package/node_modules/jose/dist/webapi/lib/key_management.js +200 -231
  163. package/node_modules/jose/dist/webapi/lib/key_options.js +7 -5
  164. package/node_modules/jose/dist/webapi/lib/options.js +55 -68
  165. package/node_modules/jose/dist/webapi/lib/signing.js +18 -22
  166. package/node_modules/jose/dist/webapi/lib/type_checks.js +26 -29
  167. package/node_modules/jose/dist/webapi/util/base64url.js +26 -35
  168. package/node_modules/jose/dist/webapi/util/decode_jwt.js +32 -31
  169. package/node_modules/jose/dist/webapi/util/decode_protected_header.js +18 -23
  170. package/node_modules/jose/dist/webapi/util/errors.js +94 -84
  171. package/node_modules/jose/package.json +17 -81
  172. package/package.json +2 -2
  173. package/profiles/calendar/0.5.profile.json +190 -0
  174. package/profiles/charge/0.5.profile.json +210 -0
  175. package/profiles/customers/0.7.profile.json +157 -0
  176. package/profiles/deploy/0.9.profile.json +176 -0
  177. package/profiles/email/0.6.profile.json +208 -0
  178. package/profiles/index.json +9 -1
  179. package/profiles/publish/0.5.profile.json +186 -0
  180. package/profiles/purchase/0.5.profile.json +231 -0
  181. package/profiles/records/0.5.profile.json +117 -0
@@ -33,6 +33,85 @@ async function notifyControlPlane(type) {
33
33
  }
34
34
  }
35
35
 
36
+ // src/lib/client-version.ts
37
+ import { createRequire } from "module";
38
+ import { existsSync, readFileSync } from "fs";
39
+ import { join } from "path";
40
+ var UNKNOWN = "unknown";
41
+ function readJson(path) {
42
+ try {
43
+ return JSON.parse(readFileSync(path, "utf8"));
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+ function detectGatewayVersion() {
49
+ const override = process.env.SUVEREN_GATEWAY_VERSION;
50
+ if (override) return override;
51
+ const start = import.meta.dirname ?? process.cwd();
52
+ let dir = start;
53
+ for (let up = 0; up < 6; up++) {
54
+ const pkg = readJson(join(dir, "package.json"));
55
+ const name = pkg?.name;
56
+ const version = pkg?.version;
57
+ if (typeof version === "string" && (name === "@suveren/gateway" || name === "suveren-gateway")) {
58
+ return version;
59
+ }
60
+ const parent = join(dir, "..");
61
+ if (parent === dir) break;
62
+ dir = parent;
63
+ }
64
+ return process.env.HAP_BUILD_SHA ?? UNKNOWN;
65
+ }
66
+ function findPackageVersion(entry, wantName) {
67
+ let dir = join(entry, "..");
68
+ for (let up = 0; up < 6; up++) {
69
+ const candidate = join(dir, "package.json");
70
+ if (existsSync(candidate)) {
71
+ const pkg = readJson(candidate);
72
+ if (typeof pkg?.version === "string" && (pkg.name === wantName || pkg.name === "@hap/core")) {
73
+ return pkg.name === wantName ? pkg.version : null;
74
+ }
75
+ }
76
+ const parent = join(dir, "..");
77
+ if (parent === dir) break;
78
+ dir = parent;
79
+ }
80
+ return null;
81
+ }
82
+ function detectHapCoreVersion() {
83
+ const CORE = "@humanagencyp/hap-core";
84
+ let req;
85
+ try {
86
+ req = createRequire(import.meta.url);
87
+ } catch {
88
+ return UNKNOWN;
89
+ }
90
+ try {
91
+ const v = findPackageVersion(req.resolve(CORE), CORE);
92
+ if (v) return v;
93
+ } catch {
94
+ }
95
+ try {
96
+ const wrapperEntry = req.resolve("@hap/core");
97
+ const v = findPackageVersion(wrapperEntry, CORE);
98
+ if (v) return v;
99
+ const wrapperReq = createRequire(wrapperEntry);
100
+ const v2 = findPackageVersion(wrapperReq.resolve(CORE), CORE);
101
+ if (v2) return v2;
102
+ } catch {
103
+ }
104
+ return UNKNOWN;
105
+ }
106
+ var GATEWAY_VERSION = detectGatewayVersion();
107
+ var HAP_CORE_VERSION = detectHapCoreVersion();
108
+ function clientVersionHeaders() {
109
+ return {
110
+ "x-suveren-gateway-version": GATEWAY_VERSION,
111
+ "x-hap-core-version": HAP_CORE_VERSION
112
+ };
113
+ }
114
+
36
115
  // src/lib/sp-client.ts
37
116
  var SPReceiptError = class extends Error {
38
117
  constructor(message, statusCode, body) {
@@ -82,6 +161,11 @@ var SPClient = class {
82
161
  const url = `${this.baseUrl}${path}`;
83
162
  const headers = {
84
163
  "Content-Type": "application/json",
164
+ // Identify the caller so the AS can answer "what is deployed against
165
+ // me?" and name a version in an upgrade instruction. Advisory only —
166
+ // never used for authorization. Sent on every call, not just receipts,
167
+ // so the picture is not skewed toward one endpoint.
168
+ ...clientVersionHeaders(),
85
169
  ...init?.headers
86
170
  };
87
171
  if (this.sessionCookie) {
@@ -170,7 +254,10 @@ var SPClient = class {
170
254
  const message = first?.message ?? first?.code ?? respBody.error ?? `SP receipt request failed: ${res.status}`;
171
255
  throw new SPReceiptError(message, res.status, respBody);
172
256
  }
173
- return { receipt: respBody.receipt };
257
+ return {
258
+ receipt: respBody.receipt,
259
+ idempotent: respBody.idempotent === true
260
+ };
174
261
  }
175
262
  throw lastError ?? new Error("postReceipt: retries exhausted");
176
263
  }
@@ -378,11 +465,11 @@ var AttestationCache = class {
378
465
  };
379
466
 
380
467
  // src/lib/gate-store.ts
381
- import { mkdirSync, readFileSync, writeFileSync, existsSync, unlinkSync } from "fs";
468
+ import { mkdirSync, readFileSync as readFileSync2, writeFileSync, existsSync as existsSync2, unlinkSync } from "fs";
382
469
  import { homedir } from "os";
383
- import { dirname, join } from "path";
470
+ import { dirname, join as join2 } from "path";
384
471
  import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
385
- var DEFAULT_DIR = process.env.SUVEREN_DATA_DIR ?? join(homedir(), ".suveren");
472
+ var DEFAULT_DIR = process.env.SUVEREN_DATA_DIR ?? join2(homedir(), ".suveren");
386
473
  var GateStore = class {
387
474
  entries = /* @__PURE__ */ new Map();
388
475
  baseDir;
@@ -400,11 +487,11 @@ var GateStore = class {
400
487
  */
401
488
  setVaultKey(key) {
402
489
  this.vaultKey = key;
403
- if (existsSync(this.encryptedFilePath)) {
490
+ if (existsSync2(this.encryptedFilePath)) {
404
491
  this.loadEncrypted();
405
492
  } else if (this.entries.size > 0) {
406
493
  this.persistEncrypted();
407
- if (existsSync(this.plaintextFilePath)) {
494
+ if (existsSync2(this.plaintextFilePath)) {
408
495
  try {
409
496
  unlinkSync(this.plaintextFilePath);
410
497
  } catch {
@@ -455,19 +542,19 @@ var GateStore = class {
455
542
  }
456
543
  // ─── File paths ─────────────────────────────────────────────────────────
457
544
  get plaintextFilePath() {
458
- return join(this.baseDir, "gates.json");
545
+ return join2(this.baseDir, "gates.json");
459
546
  }
460
547
  get encryptedFilePath() {
461
- return join(this.baseDir, "gates.enc.json");
548
+ return join2(this.baseDir, "gates.enc.json");
462
549
  }
463
550
  // ─── Load / Persist ─────────────────────────────────────────────────────
464
551
  loadPlaintext() {
465
- if (!existsSync(this.plaintextFilePath)) {
552
+ if (!existsSync2(this.plaintextFilePath)) {
466
553
  mkdirSync(this.baseDir, { recursive: true });
467
554
  return;
468
555
  }
469
556
  try {
470
- const raw = readFileSync(this.plaintextFilePath, "utf-8");
557
+ const raw = readFileSync2(this.plaintextFilePath, "utf-8");
471
558
  const data = JSON.parse(raw);
472
559
  this.entries = new Map(Object.entries(data.entries));
473
560
  } catch {
@@ -476,9 +563,9 @@ var GateStore = class {
476
563
  }
477
564
  }
478
565
  loadEncrypted() {
479
- if (!existsSync(this.encryptedFilePath)) return;
566
+ if (!existsSync2(this.encryptedFilePath)) return;
480
567
  try {
481
- const raw = readFileSync(this.encryptedFilePath, "utf-8");
568
+ const raw = readFileSync2(this.encryptedFilePath, "utf-8");
482
569
  const data = JSON.parse(raw);
483
570
  this.entries = /* @__PURE__ */ new Map();
484
571
  for (const [key, blob] of Object.entries(data.entries)) {
@@ -519,11 +606,11 @@ var GateStore = class {
519
606
  };
520
607
 
521
608
  // src/lib/execution-log.ts
522
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, unlinkSync as unlinkSync2 } from "fs";
609
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync3, unlinkSync as unlinkSync2 } from "fs";
523
610
  import { homedir as homedir2 } from "os";
524
- import { dirname as dirname2, join as join2 } from "path";
611
+ import { dirname as dirname2, join as join3 } from "path";
525
612
  import { createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv2, randomBytes as randomBytes2 } from "crypto";
526
- var DEFAULT_DIR2 = process.env.SUVEREN_DATA_DIR ?? join2(homedir2(), ".suveren");
613
+ var DEFAULT_DIR2 = process.env.SUVEREN_DATA_DIR ?? join3(homedir2(), ".suveren");
527
614
  var MAX_AGE_SECONDS = 31 * 24 * 60 * 60;
528
615
  var ExecutionLog = class {
529
616
  entries = [];
@@ -542,11 +629,11 @@ var ExecutionLog = class {
542
629
  */
543
630
  setVaultKey(key) {
544
631
  this.vaultKey = key;
545
- if (existsSync2(this.encryptedFilePath)) {
632
+ if (existsSync3(this.encryptedFilePath)) {
546
633
  this.loadEncrypted();
547
634
  } else if (this.entries.length > 0) {
548
635
  this.persistEncrypted();
549
- if (existsSync2(this.plaintextFilePath)) {
636
+ if (existsSync3(this.plaintextFilePath)) {
550
637
  try {
551
638
  unlinkSync2(this.plaintextFilePath);
552
639
  } catch {
@@ -641,19 +728,19 @@ var ExecutionLog = class {
641
728
  }
642
729
  // ─── File paths ─────────────────────────────────────────────────────────
643
730
  get plaintextFilePath() {
644
- return join2(this.baseDir, "execution-log.json");
731
+ return join3(this.baseDir, "execution-log.json");
645
732
  }
646
733
  get encryptedFilePath() {
647
- return join2(this.baseDir, "execution-log.enc.json");
734
+ return join3(this.baseDir, "execution-log.enc.json");
648
735
  }
649
736
  // ─── Load / Persist ─────────────────────────────────────────────────────
650
737
  loadPlaintext() {
651
- if (!existsSync2(this.plaintextFilePath)) {
738
+ if (!existsSync3(this.plaintextFilePath)) {
652
739
  mkdirSync2(this.baseDir, { recursive: true });
653
740
  return;
654
741
  }
655
742
  try {
656
- const raw = readFileSync2(this.plaintextFilePath, "utf-8");
743
+ const raw = readFileSync3(this.plaintextFilePath, "utf-8");
657
744
  const data = JSON.parse(raw);
658
745
  this.entries = data.entries;
659
746
  this.prune();
@@ -663,9 +750,9 @@ var ExecutionLog = class {
663
750
  }
664
751
  }
665
752
  loadEncrypted() {
666
- if (!existsSync2(this.encryptedFilePath)) return;
753
+ if (!existsSync3(this.encryptedFilePath)) return;
667
754
  try {
668
- const raw = readFileSync2(this.encryptedFilePath, "utf-8");
755
+ const raw = readFileSync3(this.encryptedFilePath, "utf-8");
669
756
  const data = JSON.parse(raw);
670
757
  const decrypted = this.decrypt(data.blob);
671
758
  const logFile = JSON.parse(decrypted);
@@ -712,11 +799,11 @@ function windowCutoff(window, now) {
712
799
  }
713
800
 
714
801
  // src/lib/denial-log.ts
715
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync3, unlinkSync as unlinkSync3 } from "fs";
802
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync4, unlinkSync as unlinkSync3 } from "fs";
716
803
  import { homedir as homedir3 } from "os";
717
- import { dirname as dirname3, join as join3 } from "path";
804
+ import { dirname as dirname3, join as join4 } from "path";
718
805
  import { createCipheriv as createCipheriv3, createDecipheriv as createDecipheriv3, randomBytes as randomBytes3 } from "crypto";
719
- var DEFAULT_DIR3 = process.env.SUVEREN_DATA_DIR ?? join3(homedir3(), ".suveren");
806
+ var DEFAULT_DIR3 = process.env.SUVEREN_DATA_DIR ?? join4(homedir3(), ".suveren");
720
807
  var MAX_RECORDS = 200;
721
808
  var MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
722
809
  var DenialLog = class {
@@ -729,11 +816,11 @@ var DenialLog = class {
729
816
  }
730
817
  setVaultKey(key) {
731
818
  this.vaultKey = key;
732
- if (existsSync3(this.encryptedFilePath)) {
819
+ if (existsSync4(this.encryptedFilePath)) {
733
820
  this.loadEncrypted();
734
821
  } else if (this.records.length > 0) {
735
822
  this.persistEncrypted();
736
- if (existsSync3(this.plaintextFilePath)) {
823
+ if (existsSync4(this.plaintextFilePath)) {
737
824
  try {
738
825
  unlinkSync3(this.plaintextFilePath);
739
826
  } catch {
@@ -783,18 +870,18 @@ var DenialLog = class {
783
870
  return Buffer.concat([decipher.update(Buffer.from(blob.ciphertext, "hex")), decipher.final()]).toString("utf8");
784
871
  }
785
872
  get plaintextFilePath() {
786
- return join3(this.baseDir, "denials.json");
873
+ return join4(this.baseDir, "denials.json");
787
874
  }
788
875
  get encryptedFilePath() {
789
- return join3(this.baseDir, "denials.enc.json");
876
+ return join4(this.baseDir, "denials.enc.json");
790
877
  }
791
878
  loadPlaintext() {
792
- if (!existsSync3(this.plaintextFilePath)) {
879
+ if (!existsSync4(this.plaintextFilePath)) {
793
880
  mkdirSync3(this.baseDir, { recursive: true });
794
881
  return;
795
882
  }
796
883
  try {
797
- const data = JSON.parse(readFileSync3(this.plaintextFilePath, "utf-8"));
884
+ const data = JSON.parse(readFileSync4(this.plaintextFilePath, "utf-8"));
798
885
  this.records = data.records ?? [];
799
886
  } catch {
800
887
  console.error(`[DenialLog] Could not parse ${this.plaintextFilePath}, starting fresh`);
@@ -802,9 +889,9 @@ var DenialLog = class {
802
889
  }
803
890
  }
804
891
  loadEncrypted() {
805
- if (!existsSync3(this.encryptedFilePath)) return;
892
+ if (!existsSync4(this.encryptedFilePath)) return;
806
893
  try {
807
- const data = JSON.parse(readFileSync3(this.encryptedFilePath, "utf-8"));
894
+ const data = JSON.parse(readFileSync4(this.encryptedFilePath, "utf-8"));
808
895
  const logFile = JSON.parse(this.decrypt(data.blob));
809
896
  this.records = logFile.records ?? [];
810
897
  } catch (err) {
@@ -830,11 +917,11 @@ var DenialLog = class {
830
917
  };
831
918
 
832
919
  // src/lib/receipt-archive.ts
833
- import { mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync4, unlinkSync as unlinkSync4 } from "fs";
920
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4, existsSync as existsSync5, unlinkSync as unlinkSync4 } from "fs";
834
921
  import { homedir as homedir4 } from "os";
835
- import { dirname as dirname4, join as join4 } from "path";
922
+ import { dirname as dirname4, join as join5 } from "path";
836
923
  import { createCipheriv as createCipheriv4, createDecipheriv as createDecipheriv4, randomBytes as randomBytes4 } from "crypto";
837
- var DEFAULT_DIR4 = process.env.SUVEREN_DATA_DIR ?? join4(homedir4(), ".suveren");
924
+ var DEFAULT_DIR4 = process.env.SUVEREN_DATA_DIR ?? join5(homedir4(), ".suveren");
838
925
  function stripPreviews(proposal) {
839
926
  const args = proposal.toolArgs;
840
927
  if (!args || typeof args !== "object") return proposal;
@@ -859,11 +946,11 @@ var ReceiptArchive = class {
859
946
  */
860
947
  setVaultKey(key) {
861
948
  this.vaultKey = key;
862
- if (existsSync4(this.encryptedFilePath)) {
949
+ if (existsSync5(this.encryptedFilePath)) {
863
950
  this.loadEncrypted();
864
951
  } else if (this.data.receipts.length > 0 || Object.keys(this.data.authorizations).length > 0) {
865
952
  this.persistEncrypted();
866
- if (existsSync4(this.plaintextFilePath)) {
953
+ if (existsSync5(this.plaintextFilePath)) {
867
954
  try {
868
955
  unlinkSync4(this.plaintextFilePath);
869
956
  } catch {
@@ -877,7 +964,7 @@ var ReceiptArchive = class {
877
964
  * this state — that would report a hollow success over unreadable evidence.
878
965
  */
879
966
  isLocked() {
880
- return this.vaultKey === null && existsSync4(this.encryptedFilePath);
967
+ return this.vaultKey === null && existsSync5(this.encryptedFilePath);
881
968
  }
882
969
  /**
883
970
  * Archive a signed receipt (and the grant it was issued under).
@@ -978,19 +1065,19 @@ var ReceiptArchive = class {
978
1065
  }
979
1066
  // ─── File paths ─────────────────────────────────────────────────────────
980
1067
  get plaintextFilePath() {
981
- return join4(this.baseDir, "receipt-archive.json");
1068
+ return join5(this.baseDir, "receipt-archive.json");
982
1069
  }
983
1070
  get encryptedFilePath() {
984
- return join4(this.baseDir, "receipt-archive.enc.json");
1071
+ return join5(this.baseDir, "receipt-archive.enc.json");
985
1072
  }
986
1073
  // ─── Load / Persist ─────────────────────────────────────────────────────
987
1074
  loadPlaintext() {
988
- if (!existsSync4(this.plaintextFilePath)) {
1075
+ if (!existsSync5(this.plaintextFilePath)) {
989
1076
  mkdirSync4(this.baseDir, { recursive: true });
990
1077
  return;
991
1078
  }
992
1079
  try {
993
- const raw = readFileSync4(this.plaintextFilePath, "utf-8");
1080
+ const raw = readFileSync5(this.plaintextFilePath, "utf-8");
994
1081
  const parsed = JSON.parse(raw);
995
1082
  this.data = {
996
1083
  version: 1,
@@ -1002,16 +1089,16 @@ var ReceiptArchive = class {
1002
1089
  try {
1003
1090
  writeFileSync4(
1004
1091
  `${this.plaintextFilePath}.corrupt-${Date.now()}`,
1005
- readFileSync4(this.plaintextFilePath)
1092
+ readFileSync5(this.plaintextFilePath)
1006
1093
  );
1007
1094
  } catch {
1008
1095
  }
1009
1096
  }
1010
1097
  }
1011
1098
  loadEncrypted() {
1012
- if (!existsSync4(this.encryptedFilePath)) return;
1099
+ if (!existsSync5(this.encryptedFilePath)) return;
1013
1100
  try {
1014
- const raw = readFileSync4(this.encryptedFilePath, "utf-8");
1101
+ const raw = readFileSync5(this.encryptedFilePath, "utf-8");
1015
1102
  const data = JSON.parse(raw);
1016
1103
  const decrypted = this.decrypt(data.blob);
1017
1104
  const parsed = JSON.parse(decrypted);
@@ -1025,7 +1112,7 @@ var ReceiptArchive = class {
1025
1112
  try {
1026
1113
  writeFileSync4(
1027
1114
  `${this.encryptedFilePath}.corrupt-${Date.now()}`,
1028
- readFileSync4(this.encryptedFilePath)
1115
+ readFileSync5(this.encryptedFilePath)
1029
1116
  );
1030
1117
  } catch {
1031
1118
  }
@@ -1052,12 +1139,106 @@ var ReceiptArchive = class {
1052
1139
  }
1053
1140
  };
1054
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
+
1055
1229
  // src/lib/gatekeeper.ts
1056
1230
  import { verify } from "@hap/core";
1057
1231
  var MCPGatekeeper = class {
1058
- constructor(cache, executionLog) {
1232
+ /**
1233
+ * No ExecutionLog dependency by construction. The local record is
1234
+ * display-only (protocol.md → "Executor Gating, Context vs Bounds,
1235
+ * Display-Only Logs"), so the Gatekeeper must not be able to read it: with
1236
+ * no log in scope, a local cumulative check cannot be reintroduced by
1237
+ * accident. `SharedState.executionLog` stays — it feeds the UI's consumption
1238
+ * display and nothing else.
1239
+ */
1240
+ constructor(cache) {
1059
1241
  this.cache = cache;
1060
- this.executionLog = executionLog;
1061
1242
  }
1062
1243
  /**
1063
1244
  * Verify an execution request against a cached authorization.
@@ -1104,16 +1285,15 @@ var MCPGatekeeper = class {
1104
1285
  attestations: auth.attestations.map((a) => a.blob),
1105
1286
  execution,
1106
1287
  context: resolvedContext,
1107
- // Scopes the local cumulative check to the same (profileId, path) key the
1108
- // ExecutionLog records under (see tool-proxy's record call). It cannot go
1109
- // inside `frame`: that is validated against the profile's boundsSchema,
1110
- // which declares no `path` field in any shipped profile, so an extra key
1111
- // there fails verification with "Unknown field". Omitting it entirely —
1112
- // the previous behaviour — made every cumulative lookup return zero, so
1113
- // the local gate never fired and the AS was doing all the enforcing.
1288
+ // Identifies the grant whose bounds are being checked. hap-core reads it
1289
+ // only to scope a cumulative lookup, which no longer happens here (see
1290
+ // below); it is kept because it belongs to the request, not to the
1291
+ // dropped check. It cannot go inside `frame`: that is validated against
1292
+ // the profile's boundsSchema, which declares no `path` field in any
1293
+ // shipped profile, so an extra key there fails with "Unknown field".
1114
1294
  path: auth.path
1115
1295
  };
1116
- const result = await verify(request, publicKeyHex, void 0, this.executionLog);
1296
+ const result = await verify(request, publicKeyHex);
1117
1297
  return { result, authorization: auth };
1118
1298
  }
1119
1299
  };
@@ -1126,6 +1306,8 @@ var SharedState = class {
1126
1306
  executionLog;
1127
1307
  denialLog;
1128
1308
  receiptArchive;
1309
+ /** Which tickets this Gatekeeper has executed — the "one execution per ticket" half of exactly-once. */
1310
+ executionJournal;
1129
1311
  gatekeeper;
1130
1312
  constructor(spUrl2, gateStorePath) {
1131
1313
  this.spClient = new SPClient(spUrl2);
@@ -1134,7 +1316,8 @@ var SharedState = class {
1134
1316
  this.executionLog = new ExecutionLog(gateStorePath);
1135
1317
  this.denialLog = new DenialLog(gateStorePath);
1136
1318
  this.receiptArchive = new ReceiptArchive(gateStorePath);
1137
- this.gatekeeper = new MCPGatekeeper(this.cache, this.executionLog);
1319
+ this.executionJournal = new ExecutionJournal(gateStorePath);
1320
+ this.gatekeeper = new MCPGatekeeper(this.cache);
1138
1321
  }
1139
1322
  /**
1140
1323
  * Archive a signed receipt into the local receipt archive — the subject's
@@ -1282,17 +1465,17 @@ function formatConsumptionFull(entries) {
1282
1465
  }
1283
1466
 
1284
1467
  // src/lib/context-loader.ts
1285
- import { readFileSync as readFileSync5, existsSync as existsSync5 } from "fs";
1286
- import { homedir as homedir5 } from "os";
1287
- import { join as join5 } from "path";
1288
- var DEFAULT_DIR5 = process.env.SUVEREN_DATA_DIR ?? join5(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");
1289
1472
  var BRIEF_MAX_CHARS = 16 * 1024;
1290
1473
  function readContextFile(dataDir) {
1291
- const dir = dataDir ?? DEFAULT_DIR5;
1292
- const filePath = join5(dir, "context.md");
1293
- if (!existsSync5(filePath)) return null;
1474
+ const dir = dataDir ?? DEFAULT_DIR6;
1475
+ const filePath = join7(dir, "context.md");
1476
+ if (!existsSync7(filePath)) return null;
1294
1477
  try {
1295
- const content = readFileSync5(filePath, "utf-8").trim();
1478
+ const content = readFileSync7(filePath, "utf-8").trim();
1296
1479
  return content || null;
1297
1480
  } catch {
1298
1481
  return null;
@@ -1933,10 +2116,11 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
1933
2116
  return inner(args);
1934
2117
  };
1935
2118
  if (category === "disabled") {
2119
+ const reason = tool.gating.disabledReason;
1936
2120
  return async () => ({
1937
2121
  content: [{
1938
2122
  type: "text",
1939
- text: `Tool "${tool.namespacedName}" is disabled by the integration manifest and cannot be used.`
2123
+ text: reason ? `Tool "${tool.namespacedName}" is refused: ${reason}. A tool that no manifest entry describes cannot be gated \u2014 there is nothing declaring which action type it performs or how its arguments map to the bounds. Add an entry for "${tool.originalName}" under this integration's toolGating.overrides (with "category": "read" for a read-only tool) before it can be used.` : `Tool "${tool.namespacedName}" is disabled by the integration manifest and cannot be used.`
1940
2124
  }],
1941
2125
  isError: true
1942
2126
  });
@@ -2257,7 +2441,12 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
2257
2441
  executionContext: { ...execution },
2258
2442
  amount: typeof execution.amount === "number" ? execution.amount : void 0,
2259
2443
  idempotencyKey: randomUUID(),
2260
- ...binding ?? {}
2444
+ // Privacy: send the hash and how to reproduce it — never the
2445
+ // preimage. `binding` also carries `boundContent`, the plaintext
2446
+ // that was hashed (an email's to/cc/subject/body); it stays on
2447
+ // this machine for the local archive, so it must be picked out
2448
+ // field by field rather than spread.
2449
+ ...binding ? { contentHash: binding.contentHash, contentBinding: binding.contentBinding } : {}
2261
2450
  });
2262
2451
  receiptId = typeof receipt?.id === "string" ? receipt.id : void 0;
2263
2452
  await state2.archiveReceipt(receipt, {
@@ -2362,6 +2551,30 @@ Proposal ID: ${proposal.id}. Use check-pending-commitments to track status.`
2362
2551
  let outgoingArgs = shouldAttachFooter() && receiptId ? appendVerificationFooter(tool, args, receiptId, auth.subjects?.[0]) : args;
2363
2552
  if (receiptId) outgoingArgs = attachReceiptId(tool, outgoingArgs, receiptId);
2364
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
+ }
2365
2578
  return integrationManager2.callTool(tool.integrationId, tool.originalName, outgoingArgs);
2366
2579
  }
2367
2580
  });
@@ -2407,7 +2620,7 @@ function buildCapabilityMap(profileId, toolGating, integrationManager2) {
2407
2620
  const shortName = shortProfileName2(profileId);
2408
2621
  const gated = [];
2409
2622
  const readOnly = [];
2410
- const defaultGated = [];
2623
+ const undescribed = [];
2411
2624
  for (const tool of allTools) {
2412
2625
  if (!tool.gating || !tool.gating.profile) {
2413
2626
  continue;
@@ -2429,12 +2642,7 @@ function buildCapabilityMap(profileId, toolGating, integrationManager2) {
2429
2642
  const actionType = override.staticExecution?.action_type ?? "unknown";
2430
2643
  gated.push(` - ${tool.originalName}: ${actionType}${mappingDesc ? `, ${mappingDesc}` : ""}`);
2431
2644
  } else {
2432
- const defaultAction = toolGating.default.staticExecution?.action_type;
2433
- if (defaultAction === "read" && Object.keys(toolGating.default.executionMapping ?? {}).length === 0) {
2434
- defaultGated.push(tool.originalName);
2435
- } else {
2436
- gated.push(` - ${tool.originalName}: ${defaultAction ?? "default"} (default gating)`);
2437
- }
2645
+ undescribed.push(tool.originalName);
2438
2646
  }
2439
2647
  }
2440
2648
  const lines = [];
@@ -2446,10 +2654,12 @@ function buildCapabilityMap(profileId, toolGating, integrationManager2) {
2446
2654
  if (readOnly.length > 0) {
2447
2655
  lines.push(` Read-only (no authorization needed): ${readOnly.join(", ")}`);
2448
2656
  }
2449
- if (defaultGated.length > 0) {
2450
- lines.push(` Default-gated (action_type: ${toolGating.default.staticExecution?.action_type ?? "read"}): ${defaultGated.join(", ")}`);
2657
+ if (undescribed.length > 0) {
2658
+ lines.push(
2659
+ ` Refused (no manifest entry \u2014 do not call): ${undescribed.join(", ")}`
2660
+ );
2451
2661
  }
2452
- if (gated.length === 0 && readOnly.length === 0 && defaultGated.length === 0) {
2662
+ if (gated.length === 0 && readOnly.length === 0 && undescribed.length === 0) {
2453
2663
  lines.push(" No tools discovered for this profile.");
2454
2664
  }
2455
2665
  return lines.join("\n");
@@ -2693,6 +2903,13 @@ function listIntegrationsHandler(state2, integrationManager2) {
2693
2903
 
2694
2904
  // src/tools/commitments.ts
2695
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
+ }
2696
2913
  async function executeCommitted(proposal, state2, integrationManager2) {
2697
2914
  if (!integrationManager2) {
2698
2915
  return { text: `Proposal ${proposal.id} committed but integration manager unavailable for execution.`, isError: true };
@@ -2712,6 +2929,7 @@ async function executeCommitted(proposal, state2, integrationManager2) {
2712
2929
  }
2713
2930
  const cachedAuth = state2.cache.getAllAuthorizations().find((a) => a.authorizationId === proposal.authorizationId);
2714
2931
  let receiptId;
2932
+ let replayed = false;
2715
2933
  try {
2716
2934
  const binding = computeContentBinding(
2717
2935
  proposal.profileId,
@@ -2719,7 +2937,7 @@ async function executeCommitted(proposal, state2, integrationManager2) {
2719
2937
  proposal.toolArgs,
2720
2938
  proposalActionType
2721
2939
  );
2722
- const { receipt } = await state2.spClient.postReceipt({
2940
+ const { receipt, idempotent } = await state2.spClient.postReceipt({
2723
2941
  authorizationId: proposal.authorizationId,
2724
2942
  // Optional cross-check — the AS fails closed on a mismatch. Parity
2725
2943
  // with the automatic path (tool-proxy.ts).
@@ -2738,9 +2956,13 @@ async function executeCommitted(proposal, state2, integrationManager2) {
2738
2956
  // SPClient therefore treats proposalId as retry-safe on its own; a
2739
2957
  // PROPOSAL_ALREADY_EXECUTED answer is still a definitive rejection
2740
2958
  // (never retried) and means a *different* caller consumed the proposal.
2741
- ...binding ?? {}
2959
+ // Privacy: hash + how to reproduce it only. `binding.boundContent` is
2960
+ // the plaintext preimage (what the human approved) and never leaves
2961
+ // this machine — see the archive call below.
2962
+ ...binding ? { contentHash: binding.contentHash, contentBinding: binding.contentBinding } : {}
2742
2963
  });
2743
2964
  receiptId = typeof receipt?.id === "string" ? receipt.id : void 0;
2965
+ replayed = idempotent;
2744
2966
  await state2.archiveReceipt(receipt, {
2745
2967
  authorizationId: proposal.authorizationId,
2746
2968
  profileId: proposal.profileId,
@@ -2778,6 +3000,36 @@ async function executeCommitted(proposal, state2, integrationManager2) {
2778
3000
  isError: true
2779
3001
  };
2780
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
+ }
2781
3033
  try {
2782
3034
  let outgoingArgs = proposal.toolArgs;
2783
3035
  if (discovered && receiptId) {
@@ -2787,7 +3039,14 @@ async function executeCommitted(proposal, state2, integrationManager2) {
2787
3039
  outgoingArgs = attachReceiptId(discovered, outgoingArgs, receiptId);
2788
3040
  outgoingArgs = encodeOutgoingArgs(discovered, outgoingArgs);
2789
3041
  }
2790
- 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");
2791
3050
  state2.executionLog.record({
2792
3051
  profileId: proposal.profileId,
2793
3052
  path: proposal.path,
@@ -2822,7 +3081,7 @@ function checkPendingCommitmentsHandler(state2, integrationManager2) {
2822
3081
  };
2823
3082
  }
2824
3083
  if (match.status === "committed") {
2825
- const { text, isError } = await executeCommitted(match, state2, integrationManager2);
3084
+ const { text, isError } = await runCommitted(match, state2, integrationManager2);
2826
3085
  return {
2827
3086
  content: [{ type: "text", text }],
2828
3087
  ...isError ? { isError: true } : {}
@@ -3076,16 +3335,16 @@ function verifyGateContentHashes(content, auth) {
3076
3335
  }
3077
3336
 
3078
3337
  // src/lib/integration-registry.ts
3079
- import { mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync6 } from "fs";
3080
- import { homedir as homedir6 } from "os";
3081
- import { dirname as dirname5, join as join6 } from "path";
3082
- var DEFAULT_DATA_DIR = process.env.SUVEREN_DATA_DIR ?? join6(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");
3083
3342
  var IntegrationRegistry = class {
3084
3343
  integrations = /* @__PURE__ */ new Map();
3085
3344
  filePath;
3086
3345
  constructor(dataDir) {
3087
3346
  const dir = dataDir ?? DEFAULT_DATA_DIR;
3088
- this.filePath = join6(dir, "integrations.json");
3347
+ this.filePath = join8(dir, "integrations.json");
3089
3348
  this.load();
3090
3349
  }
3091
3350
  // ─── CRUD ───────────────────────────────────────────────────────────────
@@ -3116,12 +3375,12 @@ var IntegrationRegistry = class {
3116
3375
  }
3117
3376
  // ─── Load / Save ────────────────────────────────────────────────────────
3118
3377
  load() {
3119
- if (!existsSync6(this.filePath)) {
3120
- mkdirSync5(dirname5(this.filePath), { recursive: true });
3378
+ if (!existsSync8(this.filePath)) {
3379
+ mkdirSync6(dirname6(this.filePath), { recursive: true });
3121
3380
  return;
3122
3381
  }
3123
3382
  try {
3124
- const raw = readFileSync6(this.filePath, "utf-8");
3383
+ const raw = readFileSync8(this.filePath, "utf-8");
3125
3384
  const data = JSON.parse(raw);
3126
3385
  const migrated = data.integrations.map((i) => {
3127
3386
  if ("toolGating" in i && !("profile" in i)) {
@@ -3150,33 +3409,39 @@ var IntegrationRegistry = class {
3150
3409
  version: 1,
3151
3410
  integrations: Array.from(this.integrations.values())
3152
3411
  };
3153
- mkdirSync5(dirname5(this.filePath), { recursive: true });
3154
- 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" });
3155
3414
  }
3156
3415
  };
3157
3416
 
3158
3417
  // src/lib/integration-manager.ts
3159
- import { homedir as homedir7 } from "os";
3160
- import { join as join7, delimiter } from "path";
3161
- import { existsSync as existsSync7, writeFileSync as writeFileSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync7, 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";
3162
3421
  import { execFile } from "child_process";
3163
3422
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3164
3423
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
3165
3424
  import { getProfile as getProfile5 } from "@hap/core";
3166
- var DEFAULT_DATA_DIR2 = process.env.SUVEREN_DATA_DIR ?? join7(homedir7(), ".suveren");
3167
- var INTEGRATIONS_DIR = process.env.SUVEREN_INTEGRATIONS_DIR ?? join7(DEFAULT_DATA_DIR2, "integrations");
3168
- var INTEGRATIONS_BIN = join7(INTEGRATIONS_DIR, "node_modules", ".bin");
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");
3427
+ var installLock = Promise.resolve();
3428
+ function withInstallLock(task) {
3429
+ const run = installLock.then(task, task);
3430
+ installLock = run.then(() => void 0, () => void 0);
3431
+ return run;
3432
+ }
3433
+ var INTEGRATIONS_BIN = join9(INTEGRATIONS_DIR, "node_modules", ".bin");
3169
3434
  function buildPath() {
3170
3435
  const base = process.env.PATH ?? "";
3171
3436
  return [INTEGRATIONS_BIN, base].join(delimiter);
3172
3437
  }
3173
3438
  function ensureIntegrationsDir() {
3174
- if (!existsSync7(INTEGRATIONS_DIR)) {
3175
- mkdirSync6(INTEGRATIONS_DIR, { recursive: true });
3439
+ if (!existsSync9(INTEGRATIONS_DIR)) {
3440
+ mkdirSync7(INTEGRATIONS_DIR, { recursive: true });
3176
3441
  }
3177
- const pkgPath = join7(INTEGRATIONS_DIR, "package.json");
3178
- if (!existsSync7(pkgPath)) {
3179
- 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));
3180
3445
  }
3181
3446
  }
3182
3447
  function readAgeOf(config) {
@@ -3242,19 +3507,19 @@ var IntegrationManager = class {
3242
3507
  * exist, and treat anything else as not-installed (and reinstallable).
3243
3508
  */
3244
3509
  isUsableInstall(npmPackage) {
3245
- const pkgDir = join7(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
3246
- const pkgJson = join7(pkgDir, "package.json");
3247
- if (!existsSync7(pkgDir) || !existsSync7(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;
3248
3513
  try {
3249
- const pkg = JSON.parse(readFileSync7(pkgJson, "utf8"));
3514
+ const pkg = JSON.parse(readFileSync9(pkgJson, "utf8"));
3250
3515
  const defaultBinName = (pkg.name ?? npmPackage).split("/").pop() ?? "";
3251
3516
  const binMap = typeof pkg.bin === "string" ? { [defaultBinName]: pkg.bin } : pkg.bin ?? {};
3252
3517
  const binEntries = Object.entries(binMap);
3253
3518
  for (const [binName, target] of binEntries) {
3254
- if (!existsSync7(join7(pkgDir, target))) return false;
3519
+ if (!existsSync9(join9(pkgDir, target))) return false;
3255
3520
  if (!this.binShimExists(binName)) return false;
3256
3521
  }
3257
- if (binEntries.length === 0 && pkg.main && !existsSync7(join7(pkgDir, pkg.main))) return false;
3522
+ if (binEntries.length === 0 && pkg.main && !existsSync9(join9(pkgDir, pkg.main))) return false;
3258
3523
  return true;
3259
3524
  } catch {
3260
3525
  return false;
@@ -3268,7 +3533,7 @@ var IntegrationManager = class {
3268
3533
  binShimExists(binName) {
3269
3534
  if (!binName) return false;
3270
3535
  const candidates = process.platform === "win32" ? [binName, `${binName}.cmd`, `${binName}.CMD`, `${binName}.ps1`] : [binName];
3271
- return candidates.some((c) => existsSync7(join7(INTEGRATIONS_BIN, c)));
3536
+ return candidates.some((c) => existsSync9(join9(INTEGRATIONS_BIN, c)));
3272
3537
  }
3273
3538
  /**
3274
3539
  * Install an npm package into the managed integrations directory if not
@@ -3285,8 +3550,15 @@ var IntegrationManager = class {
3285
3550
  async ensureInstalled(npmPackage) {
3286
3551
  ensureIntegrationsDir();
3287
3552
  if (this.isUsableInstall(npmPackage)) return;
3288
- const pkgDir = join7(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
3289
- if (existsSync7(pkgDir)) {
3553
+ return withInstallLock(async () => {
3554
+ if (this.isUsableInstall(npmPackage)) return;
3555
+ await this.installNow(npmPackage);
3556
+ });
3557
+ }
3558
+ /** The actual install. Callers MUST hold the install lock. */
3559
+ async installNow(npmPackage) {
3560
+ const pkgDir = join9(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
3561
+ if (existsSync9(pkgDir)) {
3290
3562
  console.error(`[IntegrationManager] Removing unusable install of ${npmPackage}`);
3291
3563
  try {
3292
3564
  rmSync(pkgDir, { recursive: true, force: true });
@@ -3658,8 +3930,9 @@ var IntegrationManager = class {
3658
3930
  }
3659
3931
  return {
3660
3932
  profile: profileId,
3661
- executionMapping: profileGating.default.executionMapping,
3662
- staticExecution: profileGating.default.staticExecution
3933
+ executionMapping: {},
3934
+ category: "disabled",
3935
+ disabledReason: "not described in manifest"
3663
3936
  };
3664
3937
  }
3665
3938
  /**
@@ -3701,28 +3974,28 @@ var IntegrationManager = class {
3701
3974
  };
3702
3975
 
3703
3976
  // src/lib/profile-loader.ts
3704
- import { readFileSync as readFileSync8, existsSync as existsSync8 } from "fs";
3705
- import { join as join8, resolve } from "path";
3977
+ import { readFileSync as readFileSync10, existsSync as existsSync10 } from "fs";
3978
+ import { join as join10, resolve } from "path";
3706
3979
  import { registerProfile } from "@hap/core";
3707
3980
  function loadProfiles(profilesDir) {
3708
- const dir = resolve(profilesDir ?? process.env.SUVEREN_PROFILES_DIR ?? join8(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "..", "hap-profiles"));
3709
- const indexPath = join8(dir, "index.json");
3710
- if (!existsSync8(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)) {
3711
3984
  warnNoProfiles(`No index.json at ${indexPath}`);
3712
3985
  return 0;
3713
3986
  }
3714
3987
  let index;
3715
3988
  try {
3716
- index = JSON.parse(readFileSync8(indexPath, "utf-8"));
3989
+ index = JSON.parse(readFileSync10(indexPath, "utf-8"));
3717
3990
  } catch (err) {
3718
3991
  console.error(`[ProfileLoader] Failed to parse ${indexPath}:`, err);
3719
3992
  return 0;
3720
3993
  }
3721
3994
  let loaded = 0;
3722
3995
  for (const [profileId, relativePath] of Object.entries(index.profiles)) {
3723
- const profilePath = join8(dir, relativePath);
3996
+ const profilePath = join10(dir, relativePath);
3724
3997
  try {
3725
- const profile = JSON.parse(readFileSync8(profilePath, "utf-8"));
3998
+ const profile = JSON.parse(readFileSync10(profilePath, "utf-8"));
3726
3999
  registerProfile(profileId, profile);
3727
4000
  const shortName = profileId.split("/").pop()?.replace(/@.*$/, "");
3728
4001
  if (shortName && shortName !== profileId) {
@@ -3755,30 +4028,30 @@ function warnNoProfiles(reason) {
3755
4028
  }
3756
4029
 
3757
4030
  // src/lib/manifest-loader.ts
3758
- import { readFileSync as readFileSync9, existsSync as existsSync9 } from "fs";
3759
- import { join as join9, 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";
3760
4033
  var manifests = /* @__PURE__ */ new Map();
3761
4034
  function loadManifests(integrationsDir) {
3762
4035
  const dir = resolve2(
3763
- integrationsDir ?? process.env.SUVEREN_MANIFESTS_DIR ?? join9(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "content", "integrations")
4036
+ integrationsDir ?? process.env.SUVEREN_MANIFESTS_DIR ?? join11(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "content", "integrations")
3764
4037
  );
3765
- const indexPath = join9(dir, "index.json");
3766
- if (!existsSync9(indexPath)) {
4038
+ const indexPath = join11(dir, "index.json");
4039
+ if (!existsSync11(indexPath)) {
3767
4040
  console.error(`[ManifestLoader] No index.json found at ${indexPath}, skipping manifest loading`);
3768
4041
  return 0;
3769
4042
  }
3770
4043
  let index;
3771
4044
  try {
3772
- index = JSON.parse(readFileSync9(indexPath, "utf-8"));
4045
+ index = JSON.parse(readFileSync11(indexPath, "utf-8"));
3773
4046
  } catch (err) {
3774
4047
  console.error(`[ManifestLoader] Failed to parse ${indexPath}:`, err);
3775
4048
  return 0;
3776
4049
  }
3777
4050
  let loaded = 0;
3778
4051
  for (const [id, relativePath] of Object.entries(index.integrations)) {
3779
- const manifestPath = join9(dir, relativePath);
4052
+ const manifestPath = join11(dir, relativePath);
3780
4053
  try {
3781
- const manifest = JSON.parse(readFileSync9(manifestPath, "utf-8"));
4054
+ const manifest = JSON.parse(readFileSync11(manifestPath, "utf-8"));
3782
4055
  manifests.set(id, manifest);
3783
4056
  loaded++;
3784
4057
  } catch (err) {
@@ -3797,6 +4070,90 @@ function getAllManifests() {
3797
4070
 
3798
4071
  // bin/http.ts
3799
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";
3800
4157
  var spUrl = process.env.SUVEREN_AS_URL ?? "https://www.suveren.ai";
3801
4158
  var port = parseInt(process.env.SUVEREN_MCP_PORT ?? "3430", 10);
3802
4159
  var state = new SharedState(spUrl);
@@ -4287,12 +4644,31 @@ app.listen(port, "0.0.0.0", () => {
4287
4644
  }
4288
4645
  });
4289
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;
4290
4663
  async function executeCommittedProposals() {
4664
+ if (!executorLock.isHeld()) return;
4665
+ if (draining) return;
4666
+ draining = true;
4291
4667
  try {
4292
4668
  const committed = await state.spClient.getCommittedProposals();
4293
4669
  for (const proposal of committed) {
4294
4670
  try {
4295
- const { text, isError } = await executeCommitted(proposal, state, integrationManager);
4671
+ const { text, isError } = await committedExecutor.execute(proposal);
4296
4672
  console.error(
4297
4673
  isError ? `[Suveren MCP] Auto-exec proposal ${proposal.id}: ${text}` : `[Suveren MCP] Auto-executed proposal ${proposal.id}: ${proposal.tool}`
4298
4674
  );
@@ -4301,6 +4677,8 @@ app.listen(port, "0.0.0.0", () => {
4301
4677
  }
4302
4678
  }
4303
4679
  } catch {
4680
+ } finally {
4681
+ draining = false;
4304
4682
  }
4305
4683
  }
4306
4684
  setInterval(executeCommittedProposals, PROPOSAL_POLL_INTERVAL);