@suveren/gateway 0.7.3 → 0.7.4

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 (49) hide show
  1. package/content/integrations/gmail.json +3 -6
  2. package/dist/control-plane/index.mjs +46 -4
  3. package/dist/mcp-server/http.mjs +59 -8
  4. package/dist/ui/assets/{index-DdJeSfV9.js → index-0Q2eNytX.js} +10 -10
  5. package/dist/ui/index.html +1 -1
  6. package/node_modules/@hono/node-server/dist/index.cjs +151 -9
  7. package/node_modules/@hono/node-server/dist/index.mjs +151 -9
  8. package/node_modules/@hono/node-server/package.json +1 -1
  9. package/node_modules/@modelcontextprotocol/sdk/node_modules/body-parser/node_modules/content-type/README.md +2 -0
  10. package/node_modules/@modelcontextprotocol/sdk/node_modules/body-parser/node_modules/content-type/dist/index.d.ts +20 -0
  11. package/node_modules/@modelcontextprotocol/sdk/node_modules/body-parser/node_modules/content-type/dist/index.js +20 -14
  12. package/node_modules/@modelcontextprotocol/sdk/node_modules/body-parser/node_modules/content-type/dist/index.js.map +1 -1
  13. package/node_modules/@modelcontextprotocol/sdk/node_modules/body-parser/node_modules/content-type/package.json +1 -1
  14. package/node_modules/@modelcontextprotocol/sdk/node_modules/type-is/node_modules/content-type/README.md +2 -0
  15. package/node_modules/@modelcontextprotocol/sdk/node_modules/type-is/node_modules/content-type/dist/index.d.ts +20 -0
  16. package/node_modules/@modelcontextprotocol/sdk/node_modules/type-is/node_modules/content-type/dist/index.js +20 -14
  17. package/node_modules/@modelcontextprotocol/sdk/node_modules/type-is/node_modules/content-type/dist/index.js.map +1 -1
  18. package/node_modules/@modelcontextprotocol/sdk/node_modules/type-is/node_modules/content-type/package.json +1 -1
  19. package/node_modules/eventsource-parser/dist/index.cjs +2 -2
  20. package/node_modules/eventsource-parser/dist/index.cjs.map +1 -1
  21. package/node_modules/eventsource-parser/dist/index.js +2 -2
  22. package/node_modules/eventsource-parser/dist/index.js.map +1 -1
  23. package/node_modules/eventsource-parser/package.json +1 -1
  24. package/node_modules/eventsource-parser/src/parse.ts +4 -2
  25. package/node_modules/hono/dist/cjs/client/client.js +3 -3
  26. package/node_modules/hono/dist/cjs/jsx/intrinsic-element/components.js +1 -1
  27. package/node_modules/hono/dist/cjs/middleware/cors/index.js +15 -12
  28. package/node_modules/hono/dist/cjs/middleware/etag/digest.js +47 -1
  29. package/node_modules/hono/dist/cjs/middleware/secure-headers/secure-headers.js +6 -3
  30. package/node_modules/hono/dist/cjs/utils/url.js +2 -2
  31. package/node_modules/hono/dist/client/client.js +3 -3
  32. package/node_modules/hono/dist/jsx/intrinsic-element/components.js +1 -1
  33. package/node_modules/hono/dist/middleware/cors/index.js +15 -12
  34. package/node_modules/hono/dist/middleware/etag/digest.js +47 -1
  35. package/node_modules/hono/dist/middleware/secure-headers/secure-headers.js +6 -3
  36. package/node_modules/hono/dist/types/middleware/secure-headers/permissions-policy.d.ts +3 -3
  37. package/node_modules/hono/dist/utils/url.js +2 -2
  38. package/node_modules/hono/package.json +1 -1
  39. package/node_modules/ip-address/README.md +134 -134
  40. package/node_modules/ip-address/dist/address-error.d.ts +11 -0
  41. package/node_modules/ip-address/dist/address-error.js.map +1 -1
  42. package/node_modules/ip-address/dist/ipv4.d.ts +22 -6
  43. package/node_modules/ip-address/dist/ipv4.js +22 -6
  44. package/node_modules/ip-address/dist/ipv4.js.map +1 -1
  45. package/node_modules/ip-address/dist/ipv6.d.ts +10 -3
  46. package/node_modules/ip-address/dist/ipv6.js +26 -17
  47. package/node_modules/ip-address/dist/ipv6.js.map +1 -1
  48. package/node_modules/ip-address/package.json +1 -1
  49. package/package.json +2 -2
@@ -216,8 +216,7 @@
216
216
  "bounds": {
217
217
  "recipient_max": "0",
218
218
  "send_daily_max": "0",
219
- "read_max_age_days": "30",
220
- "read_daily_max": "100"
219
+ "read_max_age_days": "30"
221
220
  },
222
221
  "context": {
223
222
  "allowed_recipients": "",
@@ -227,8 +226,7 @@
227
226
  "ttl": 2592000,
228
227
  "tags": [
229
228
  "read-only",
230
- "30 days",
231
- "100 reads/day"
229
+ "30 days"
232
230
  ]
233
231
  },
234
232
  {
@@ -239,8 +237,7 @@
239
237
  "bounds": {
240
238
  "recipient_max": "5",
241
239
  "send_daily_max": "20",
242
- "read_max_age_days": "90",
243
- "read_daily_max": "200"
240
+ "read_max_age_days": "90"
244
241
  },
245
242
  "context": {
246
243
  "allowed_recipients": "",
@@ -865,6 +865,7 @@ function createVaultRouter(vault2) {
865
865
  import { Router as Router3 } from "express";
866
866
 
867
867
  // src/lib/ai-client.ts
868
+ var CHAT_TOKEN_BUDGET = 4e3;
868
869
  var PROVIDER_PRESETS = {
869
870
  ollama: {
870
871
  provider: "ollama",
@@ -892,6 +893,30 @@ var PROVIDER_PRESETS = {
892
893
  model: "meta-llama/Llama-3-8b-chat-hf"
893
894
  }
894
895
  };
896
+ function explainEmptyReply(choice, usage, budget) {
897
+ const reasoningTokens = usage?.completion_tokens_details?.reasoning_tokens ?? 0;
898
+ const finish = choice?.finish_reason;
899
+ const hadReasoning = reasoningTokens > 0 || Boolean(choice?.message?.reasoning_content || choice?.message?.reasoning);
900
+ if (finish === "length" && hadReasoning) {
901
+ return `The model is a reasoning model and spent its whole ${budget}-token budget` + (reasoningTokens ? ` (${reasoningTokens} tokens)` : "") + " thinking, leaving nothing for the answer. Raise the token limit or choose a non-reasoning model in Settings.";
902
+ }
903
+ if (finish === "length") {
904
+ return `The model hit the ${budget}-token limit before writing any answer.`;
905
+ }
906
+ if (hadReasoning) {
907
+ return "The model returned only internal reasoning and no answer \u2014 it may not be compatible with this endpoint. Try a different model in Settings.";
908
+ }
909
+ if (finish === "content_filter") {
910
+ return "The provider's content filter blocked the response.";
911
+ }
912
+ if (choice?.message?.tool_calls?.length) {
913
+ return "The model tried to call a tool instead of answering. Choose a model without forced tool use.";
914
+ }
915
+ if (!choice) {
916
+ return "The provider returned no choices. Check the model name in Settings.";
917
+ }
918
+ return `The model returned an empty answer${finish ? ` (finish_reason: ${finish})` : ""}.`;
919
+ }
895
920
  var SYSTEM_PROMPTS = {
896
921
  intent: `You are a reviewing assistant helping a human articulate their intent for an AI agent authorization.
897
922
 
@@ -1062,13 +1087,20 @@ ${request.currentText}
1062
1087
  model: config.model,
1063
1088
  messages,
1064
1089
  stream: false,
1065
- options: { temperature: 0.3, num_predict: 600 }
1090
+ options: { temperature: 0.3, num_predict: CHAT_TOKEN_BUDGET }
1066
1091
  }),
1067
1092
  signal: AbortSignal.timeout(6e4)
1068
1093
  });
1069
1094
  if (!response.ok) throw new Error(`Ollama: ${response.status}`);
1070
1095
  const data = await response.json();
1071
- reply = data.message?.content?.trim() || "No response generated.";
1096
+ const content = data.message?.content?.trim();
1097
+ if (!content) {
1098
+ return {
1099
+ success: false,
1100
+ error: data.message?.thinking ? "The model returned only internal reasoning and no answer. Try a non-thinking model." : `The model returned an empty answer. Check that "${config.model}" is pulled and running in Ollama.`
1101
+ };
1102
+ }
1103
+ reply = content;
1072
1104
  } else {
1073
1105
  const headers = { "Content-Type": "application/json" };
1074
1106
  if (config.apiKey) headers["Authorization"] = `Bearer ${config.apiKey}`;
@@ -1079,7 +1111,10 @@ ${request.currentText}
1079
1111
  model: config.model,
1080
1112
  messages,
1081
1113
  temperature: 0.3,
1082
- max_tokens: 600
1114
+ // Reasoning models charge their thinking against this same budget,
1115
+ // so a small number here produces an empty answer rather than a
1116
+ // short one. 4000 leaves room to think AND reply.
1117
+ max_tokens: CHAT_TOKEN_BUDGET
1083
1118
  }),
1084
1119
  signal: AbortSignal.timeout(6e4)
1085
1120
  });
@@ -1088,7 +1123,14 @@ ${request.currentText}
1088
1123
  throw new Error(`AI provider: ${response.status} - ${errorText}`);
1089
1124
  }
1090
1125
  const data = await response.json();
1091
- reply = data.choices?.[0]?.message?.content?.trim() || "No response generated.";
1126
+ const content = data.choices?.[0]?.message?.content?.trim();
1127
+ if (!content) {
1128
+ return {
1129
+ success: false,
1130
+ error: explainEmptyReply(data.choices?.[0], data.usage, CHAT_TOKEN_BUDGET)
1131
+ };
1132
+ }
1133
+ reply = content;
1092
1134
  }
1093
1135
  return { success: true, reply };
1094
1136
  } catch (error) {
@@ -137,7 +137,7 @@ var SPClient = class {
137
137
  */
138
138
  async postReceipt(data) {
139
139
  const body = JSON.stringify(data);
140
- const maxAttempts = data.idempotencyKey ? this.receiptRetry.maxAttempts : 1;
140
+ const maxAttempts = data.idempotencyKey || data.proposalId ? this.receiptRetry.maxAttempts : 1;
141
141
  let lastError;
142
142
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
143
143
  const isLast = attempt === maxAttempts;
@@ -2403,6 +2403,7 @@ async function executeCommitted(proposal, state2, integrationManager2) {
2403
2403
  `[Suveren MCP] Warning: proposal ${proposal.id} has no action_type in executionContext. Bounds check may be skipped. Fix the integration manifest for ${proposal.tool}.`
2404
2404
  );
2405
2405
  }
2406
+ const cachedAuth = state2.cache.getAllAuthorizations().find((a) => a.authorizationId === proposal.authorizationId);
2406
2407
  let receiptId;
2407
2408
  try {
2408
2409
  const binding = computeContentBinding(
@@ -2413,6 +2414,9 @@ async function executeCommitted(proposal, state2, integrationManager2) {
2413
2414
  );
2414
2415
  const { receipt } = await state2.spClient.postReceipt({
2415
2416
  authorizationId: proposal.authorizationId,
2417
+ // Optional cross-check — the AS fails closed on a mismatch. Parity
2418
+ // with the automatic path (tool-proxy.ts).
2419
+ boundsHash: cachedAuth?.boundsHash,
2416
2420
  profileId: proposal.profileId,
2417
2421
  action: proposal.tool,
2418
2422
  actionType: proposalActionType,
@@ -2420,6 +2424,13 @@ async function executeCommitted(proposal, state2, integrationManager2) {
2420
2424
  amount: typeof proposal.executionContext.amount === "number" ? proposal.executionContext.amount : void 0,
2421
2425
  proposalId: proposal.id,
2422
2426
  toolArgs: proposal.toolArgs,
2427
+ // Deliberately NO idempotencyKey: the spec has review-mode commits carry
2428
+ // a proposalId INSTEAD of a key — the proposal's committed→executed
2429
+ // transition is their replay protection, and the AS replays the ORIGINAL
2430
+ // receipt to the same caller if a retry arrives after it committed.
2431
+ // SPClient therefore treats proposalId as retry-safe on its own; a
2432
+ // PROPOSAL_ALREADY_EXECUTED answer is still a definitive rejection
2433
+ // (never retried) and means a *different* caller consumed the proposal.
2423
2434
  ...binding ?? {}
2424
2435
  });
2425
2436
  receiptId = typeof receipt?.id === "string" ? receipt.id : void 0;
@@ -2451,7 +2462,7 @@ async function executeCommitted(proposal, state2, integrationManager2) {
2451
2462
  let outgoingArgs = proposal.toolArgs;
2452
2463
  if (discovered && receiptId) {
2453
2464
  if (shouldAttachFooter()) {
2454
- outgoingArgs = appendVerificationFooter(discovered, outgoingArgs, receiptId);
2465
+ outgoingArgs = appendVerificationFooter(discovered, outgoingArgs, receiptId, cachedAuth?.subjects?.[0]);
2455
2466
  }
2456
2467
  outgoingArgs = attachReceiptId(discovered, outgoingArgs, receiptId);
2457
2468
  outgoingArgs = encodeOutgoingArgs(discovered, outgoingArgs);
@@ -2672,7 +2683,12 @@ function createMcpServer(state2, integrationManager2) {
2672
2683
  }
2673
2684
  }
2674
2685
  for (const tool of allTools) {
2675
- if (proxiedTools.has(tool.namespacedName)) continue;
2686
+ const existing = proxiedTools.get(tool.namespacedName);
2687
+ if (existing) {
2688
+ if (existing.tool === tool) continue;
2689
+ existing.registered.remove();
2690
+ proxiedTools.delete(tool.namespacedName);
2691
+ }
2676
2692
  const handler = createGatedToolHandler(tool, integrationManager2, state2);
2677
2693
  const zodShape = jsonSchemaToZodShape(tool.inputSchema);
2678
2694
  const description = buildProxiedToolDescription(tool, state2);
@@ -2869,6 +2885,24 @@ var IntegrationManager = class {
2869
2885
  }
2870
2886
  running = /* @__PURE__ */ new Map();
2871
2887
  onToolsChanged = null;
2888
+ /**
2889
+ * Per-integration operation queue. Start/stop for one id are serialized
2890
+ * through here; without it, a second start issued while the first is still
2891
+ * installing/handshaking sees `running.has(id) === false`, skips the stop,
2892
+ * and both children come up — last `running.set` wins, the loser's process
2893
+ * leaks, and the surviving GATING is whichever start happened to finish
2894
+ * last. That last-writer-wins gating is a fail-open: a permissive manifest
2895
+ * config can silently replace a stricter explicit one (observed as the
2896
+ * read-gate value-mismatch flake).
2897
+ */
2898
+ opQueues = /* @__PURE__ */ new Map();
2899
+ /** Run `task` after every previously queued operation for `id` has settled. */
2900
+ runExclusive(id, task) {
2901
+ const prev = this.opQueues.get(id) ?? Promise.resolve();
2902
+ const run = prev.then(task, task);
2903
+ this.opQueues.set(id, run.then(() => void 0, () => void 0));
2904
+ return run;
2905
+ }
2872
2906
  /**
2873
2907
  * Register a callback invoked when the tool set changes
2874
2908
  * (integration started, stopped, or crashed).
@@ -3018,10 +3052,19 @@ var IntegrationManager = class {
3018
3052
  * Start a downstream MCP server integration.
3019
3053
  * Installs npm package on-demand if needed, resolves envKeys,
3020
3054
  * spawns the process, connects as MCP client, and discovers tools.
3055
+ *
3056
+ * Serialized per id: a start issued while another start/stop for the same
3057
+ * id is in flight waits for it, then applies its own semantics (restart, or
3058
+ * keep-if-running with `skipIfRunning`). See `opQueues`.
3021
3059
  */
3022
- async startIntegration(config) {
3023
- if (this.running.has(config.id)) {
3024
- await this.stopIntegration(config.id);
3060
+ async startIntegration(config, opts = {}) {
3061
+ return this.runExclusive(config.id, () => this.startIntegrationLocked(config, opts));
3062
+ }
3063
+ async startIntegrationLocked(config, opts) {
3064
+ const existing = this.running.get(config.id);
3065
+ if (existing) {
3066
+ if (opts.skipIfRunning) return existing.tools;
3067
+ await this.stopIntegrationLocked(config.id);
3025
3068
  }
3026
3069
  if (config.npmPackage) {
3027
3070
  await this.ensureInstalled(config.npmPackage);
@@ -3076,6 +3119,7 @@ var IntegrationManager = class {
3076
3119
  };
3077
3120
  this.running.set(config.id, entry);
3078
3121
  transport.onclose = () => {
3122
+ if (this.running.get(config.id)?.transport !== transport) return;
3079
3123
  console.error(`[IntegrationManager] Transport closed for ${config.id}`);
3080
3124
  this.handleCrash(config.id);
3081
3125
  };
@@ -3084,8 +3128,15 @@ var IntegrationManager = class {
3084
3128
  }
3085
3129
  /**
3086
3130
  * Stop a running integration, closing its transport and removing its tools.
3131
+ *
3132
+ * Serialized per id: a stop issued while a start is in flight waits and
3133
+ * then stops the just-started instance, instead of silently doing nothing
3134
+ * because the entry wasn't in `running` yet.
3087
3135
  */
3088
3136
  async stopIntegration(id) {
3137
+ return this.runExclusive(id, () => this.stopIntegrationLocked(id));
3138
+ }
3139
+ async stopIntegrationLocked(id) {
3089
3140
  const entry = this.running.get(id);
3090
3141
  if (!entry) return;
3091
3142
  entry.transport.onclose = void 0;
@@ -3308,7 +3359,7 @@ var IntegrationManager = class {
3308
3359
  console.error(`[IntegrationManager] ${id} crashed, respawning in ${delay}ms (attempt ${attempts + 1}/${MAX_RESPAWN_ATTEMPTS})`);
3309
3360
  setTimeout(async () => {
3310
3361
  try {
3311
- await this.startIntegration(entry.config);
3362
+ await this.startIntegration(entry.config, { skipIfRunning: true });
3312
3363
  const newEntry = this.running.get(id);
3313
3364
  if (newEntry) {
3314
3365
  newEntry.respawnAttempts = attempts + 1;
@@ -3833,7 +3884,7 @@ async function startOneIntegration(config) {
3833
3884
  const manifest = getManifest(config.id);
3834
3885
  const effectiveConfig = manifest ? { ...config, toolGating: manifest.toolGating, npmPackage: manifest.npmPackage ?? config.npmPackage } : config;
3835
3886
  try {
3836
- await integrationManager.startIntegration(effectiveConfig);
3887
+ await integrationManager.startIntegration(effectiveConfig, { skipIfRunning: true });
3837
3888
  } catch (err) {
3838
3889
  console.error(`[Suveren MCP] Failed to start integration ${config.id}:`, err);
3839
3890
  }