@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.
- package/content/integrations/gmail.json +3 -6
- package/dist/control-plane/index.mjs +46 -4
- package/dist/mcp-server/http.mjs +59 -8
- package/dist/ui/assets/{index-DdJeSfV9.js → index-0Q2eNytX.js} +10 -10
- package/dist/ui/index.html +1 -1
- package/node_modules/@hono/node-server/dist/index.cjs +151 -9
- package/node_modules/@hono/node-server/dist/index.mjs +151 -9
- package/node_modules/@hono/node-server/package.json +1 -1
- package/node_modules/@modelcontextprotocol/sdk/node_modules/body-parser/node_modules/content-type/README.md +2 -0
- package/node_modules/@modelcontextprotocol/sdk/node_modules/body-parser/node_modules/content-type/dist/index.d.ts +20 -0
- package/node_modules/@modelcontextprotocol/sdk/node_modules/body-parser/node_modules/content-type/dist/index.js +20 -14
- package/node_modules/@modelcontextprotocol/sdk/node_modules/body-parser/node_modules/content-type/dist/index.js.map +1 -1
- package/node_modules/@modelcontextprotocol/sdk/node_modules/body-parser/node_modules/content-type/package.json +1 -1
- package/node_modules/@modelcontextprotocol/sdk/node_modules/type-is/node_modules/content-type/README.md +2 -0
- package/node_modules/@modelcontextprotocol/sdk/node_modules/type-is/node_modules/content-type/dist/index.d.ts +20 -0
- package/node_modules/@modelcontextprotocol/sdk/node_modules/type-is/node_modules/content-type/dist/index.js +20 -14
- package/node_modules/@modelcontextprotocol/sdk/node_modules/type-is/node_modules/content-type/dist/index.js.map +1 -1
- package/node_modules/@modelcontextprotocol/sdk/node_modules/type-is/node_modules/content-type/package.json +1 -1
- package/node_modules/eventsource-parser/dist/index.cjs +2 -2
- package/node_modules/eventsource-parser/dist/index.cjs.map +1 -1
- package/node_modules/eventsource-parser/dist/index.js +2 -2
- package/node_modules/eventsource-parser/dist/index.js.map +1 -1
- package/node_modules/eventsource-parser/package.json +1 -1
- package/node_modules/eventsource-parser/src/parse.ts +4 -2
- package/node_modules/hono/dist/cjs/client/client.js +3 -3
- package/node_modules/hono/dist/cjs/jsx/intrinsic-element/components.js +1 -1
- package/node_modules/hono/dist/cjs/middleware/cors/index.js +15 -12
- package/node_modules/hono/dist/cjs/middleware/etag/digest.js +47 -1
- package/node_modules/hono/dist/cjs/middleware/secure-headers/secure-headers.js +6 -3
- package/node_modules/hono/dist/cjs/utils/url.js +2 -2
- package/node_modules/hono/dist/client/client.js +3 -3
- package/node_modules/hono/dist/jsx/intrinsic-element/components.js +1 -1
- package/node_modules/hono/dist/middleware/cors/index.js +15 -12
- package/node_modules/hono/dist/middleware/etag/digest.js +47 -1
- package/node_modules/hono/dist/middleware/secure-headers/secure-headers.js +6 -3
- package/node_modules/hono/dist/types/middleware/secure-headers/permissions-policy.d.ts +3 -3
- package/node_modules/hono/dist/utils/url.js +2 -2
- package/node_modules/hono/package.json +1 -1
- package/node_modules/ip-address/README.md +134 -134
- package/node_modules/ip-address/dist/address-error.d.ts +11 -0
- package/node_modules/ip-address/dist/address-error.js.map +1 -1
- package/node_modules/ip-address/dist/ipv4.d.ts +22 -6
- package/node_modules/ip-address/dist/ipv4.js +22 -6
- package/node_modules/ip-address/dist/ipv4.js.map +1 -1
- package/node_modules/ip-address/dist/ipv6.d.ts +10 -3
- package/node_modules/ip-address/dist/ipv6.js +26 -17
- package/node_modules/ip-address/dist/ipv6.js.map +1 -1
- package/node_modules/ip-address/package.json +1 -1
- 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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
package/dist/mcp-server/http.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
3024
|
-
|
|
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
|
}
|