@suveren/gateway 0.20.1 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/content/integrations/erp.json +1 -1
- package/dist/mcp-server/http.mjs +152 -11
- package/dist/ui/assets/index-BBjT8ue7.js +116 -0
- package/dist/ui/assets/{index-blnpvM2g.css → index-D5hgQ_8Z.css} +1 -1
- package/dist/ui/index.html +2 -2
- package/node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition/README.md +105 -35
- package/node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition/dist/index.d.ts +72 -0
- package/node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition/dist/index.js +388 -0
- package/node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition/dist/index.js.map +1 -0
- package/node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition/package.json +31 -19
- package/node_modules/@modelcontextprotocol/sdk/node_modules/express/Readme.md +15 -9
- package/node_modules/@modelcontextprotocol/sdk/node_modules/express/lib/application.js +2 -2
- package/node_modules/@modelcontextprotocol/sdk/node_modules/express/lib/request.js +32 -19
- package/node_modules/@modelcontextprotocol/sdk/node_modules/express/lib/response.js +27 -30
- package/node_modules/@modelcontextprotocol/sdk/node_modules/express/node_modules/content-type/LICENSE +22 -0
- package/node_modules/@modelcontextprotocol/sdk/node_modules/express/node_modules/content-type/README.md +71 -0
- package/node_modules/@modelcontextprotocol/sdk/node_modules/express/node_modules/content-type/dist/index.d.ts +46 -0
- package/node_modules/@modelcontextprotocol/sdk/node_modules/express/node_modules/content-type/dist/index.js +176 -0
- package/node_modules/@modelcontextprotocol/sdk/node_modules/express/node_modules/content-type/dist/index.js.map +1 -0
- package/node_modules/@modelcontextprotocol/sdk/node_modules/express/node_modules/content-type/package.json +52 -0
- package/node_modules/@modelcontextprotocol/sdk/node_modules/express/package.json +10 -10
- package/node_modules/@types/node/README.md +1 -1
- package/node_modules/@types/node/package.json +2 -2
- package/node_modules/@types/node/util.d.ts +6 -3
- package/package.json +1 -1
- package/dist/ui/assets/index-1s1q-XLL.js +0 -116
- package/node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition/index.js +0 -536
package/dist/mcp-server/http.mjs
CHANGED
|
@@ -1970,14 +1970,22 @@ var ExecutionJournal = class {
|
|
|
1970
1970
|
return { ok: true };
|
|
1971
1971
|
}
|
|
1972
1972
|
/** Close an `intent` row. Missing row is a programming error, not a silent no-op. */
|
|
1973
|
-
complete(ticketId, state2) {
|
|
1973
|
+
complete(ticketId, state2, outcome) {
|
|
1974
1974
|
const file = this.load();
|
|
1975
1975
|
const row = file.entries.find((e) => e.ticketId === ticketId);
|
|
1976
1976
|
if (!row) throw new Error(`execution journal: no row for ticket ${ticketId} to complete`);
|
|
1977
1977
|
row.state = state2;
|
|
1978
|
+
if (outcome) row.outcome = outcome;
|
|
1978
1979
|
row.finishedAt = nowSec();
|
|
1979
1980
|
this.persist(file);
|
|
1980
1981
|
}
|
|
1982
|
+
/** The newest row for a review-path proposal — what really happened when it
|
|
1983
|
+
* ran on this gateway (the AS marks a proposal "executed" when it issues the
|
|
1984
|
+
* ticket, before the action runs). Undefined when it never ran here. */
|
|
1985
|
+
findByProposal(proposalId) {
|
|
1986
|
+
const rows = this.load().entries.filter((e) => e.proposalId === proposalId);
|
|
1987
|
+
return rows[rows.length - 1];
|
|
1988
|
+
}
|
|
1981
1989
|
/** All rows, newest last. Diagnostic. */
|
|
1982
1990
|
getAll() {
|
|
1983
1991
|
return [...this.load().entries];
|
|
@@ -3563,6 +3571,7 @@ function denyRead(state2, tool, reason, detail, target) {
|
|
|
3563
3571
|
tool: tool.originalName,
|
|
3564
3572
|
integrationId: tool.integrationId,
|
|
3565
3573
|
profile: tool.gating?.profile ?? null,
|
|
3574
|
+
kind: "read",
|
|
3566
3575
|
reason,
|
|
3567
3576
|
detail,
|
|
3568
3577
|
target
|
|
@@ -3571,15 +3580,62 @@ function denyRead(state2, tool, reason, detail, target) {
|
|
|
3571
3580
|
}
|
|
3572
3581
|
return { content: [{ type: "text", text: `Read blocked by Gatekeeper: ${detail}` }], isError: true };
|
|
3573
3582
|
}
|
|
3583
|
+
function recordBlocked(state2, tool, rec) {
|
|
3584
|
+
state2.denialLog?.record({
|
|
3585
|
+
ts: Date.now(),
|
|
3586
|
+
tool: tool.originalName,
|
|
3587
|
+
integrationId: tool.integrationId,
|
|
3588
|
+
profile: tool.gating?.profile ?? null,
|
|
3589
|
+
...rec
|
|
3590
|
+
});
|
|
3591
|
+
}
|
|
3592
|
+
function recordGatekeeperRefusal(state2, tool, refusals) {
|
|
3593
|
+
if (refusals.length === 0) return;
|
|
3594
|
+
const pick = refusals.find((r) => r.bound !== void 0 && r.actual !== void 0) ?? refusals[0];
|
|
3595
|
+
const scoped = pick.field ? Boolean(getProfile2(pick.profileId)?.scopeSchema?.fields?.[pick.field]) : false;
|
|
3596
|
+
const asValueOrString = (v) => typeof v === "number" || typeof v === "string" ? v : void 0;
|
|
3597
|
+
recordBlocked(state2, tool, {
|
|
3598
|
+
kind: scoped ? "scope" : "bound",
|
|
3599
|
+
detail: "Refused locally by the Gatekeeper.",
|
|
3600
|
+
mandateId: pick.authorizationId,
|
|
3601
|
+
field: pick.field,
|
|
3602
|
+
value: asValueOrString(pick.actual),
|
|
3603
|
+
limit: asValueOrString(pick.bound),
|
|
3604
|
+
who: "gateway",
|
|
3605
|
+
code: pick.code
|
|
3606
|
+
});
|
|
3607
|
+
}
|
|
3608
|
+
function recordAsRefusal(state2, tool, authorizationId, err) {
|
|
3609
|
+
const code = typeof err?.code === "string" ? err.code : void 0;
|
|
3610
|
+
const asValueOrString = (v) => typeof v === "number" || typeof v === "string" ? v : void 0;
|
|
3611
|
+
recordBlocked(state2, tool, {
|
|
3612
|
+
kind: code === "CUMULATIVE_LIMIT_EXCEEDED" ? "cumulative" : code === "BOUND_EXCEEDED" ? "bound" : "not_authorized",
|
|
3613
|
+
detail: "Refused by the Authority Server.",
|
|
3614
|
+
mandateId: authorizationId,
|
|
3615
|
+
field: typeof err?.field === "string" ? err.field : void 0,
|
|
3616
|
+
value: asValueOrString(err?.actual),
|
|
3617
|
+
limit: asValueOrString(err?.expected),
|
|
3618
|
+
who: "authority-server",
|
|
3619
|
+
code
|
|
3620
|
+
});
|
|
3621
|
+
}
|
|
3574
3622
|
function createGatedToolHandler(tool, integrationManager2, state2) {
|
|
3575
3623
|
if (isSimulationMode() && !integrationManager2.isSimulationSafe(tool.integrationId)) {
|
|
3576
|
-
return async () =>
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3624
|
+
return async () => {
|
|
3625
|
+
recordBlocked(state2, tool, {
|
|
3626
|
+
kind: "simulation",
|
|
3627
|
+
detail: "Real systems are blocked while simulation mode is on.",
|
|
3628
|
+
who: "gateway",
|
|
3629
|
+
code: "SIMULATION_MODE"
|
|
3630
|
+
});
|
|
3631
|
+
return {
|
|
3632
|
+
content: [{
|
|
3633
|
+
type: "text",
|
|
3634
|
+
text: `Refused: "${tool.namespacedName}" is not available. No ticket was requested.`
|
|
3635
|
+
}],
|
|
3636
|
+
isError: true
|
|
3637
|
+
};
|
|
3638
|
+
};
|
|
3583
3639
|
}
|
|
3584
3640
|
const gated = createGatedToolHandlerInner(tool, integrationManager2, state2);
|
|
3585
3641
|
const gateable = !!tool.gating?.profile && tool.gating.category !== "disabled";
|
|
@@ -3642,6 +3698,15 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
|
|
|
3642
3698
|
};
|
|
3643
3699
|
const noEligibleMandate = (refusals) => {
|
|
3644
3700
|
const reasons = [...new Set(refusals)].join(" ");
|
|
3701
|
+
recordBlocked(state2, tool, {
|
|
3702
|
+
kind: "not_authorized",
|
|
3703
|
+
// Fixed text only: `reasons` can name resources or correspondents (read
|
|
3704
|
+
// refusals), and a blocked-action record carries no content.
|
|
3705
|
+
detail: `No active authorization matching profile "${profile}".`,
|
|
3706
|
+
field: profile,
|
|
3707
|
+
who: "gateway",
|
|
3708
|
+
code: "NO_MATCHING_MANDATE"
|
|
3709
|
+
});
|
|
3645
3710
|
return {
|
|
3646
3711
|
content: [{
|
|
3647
3712
|
type: "text",
|
|
@@ -3847,6 +3912,13 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
|
|
|
3847
3912
|
}
|
|
3848
3913
|
const declaredActionType = typeof execution.action_type === "string" && execution.action_type.length > 0 ? execution.action_type : void 0;
|
|
3849
3914
|
if (!declaredActionType) {
|
|
3915
|
+
recordBlocked(state2, tool, {
|
|
3916
|
+
kind: "bound",
|
|
3917
|
+
detail: `${tool.namespacedName} declares no action_type in its manifest's staticExecution.`,
|
|
3918
|
+
field: "action_type",
|
|
3919
|
+
who: "gateway",
|
|
3920
|
+
code: "MISSING_ACTION_TYPE"
|
|
3921
|
+
});
|
|
3850
3922
|
return {
|
|
3851
3923
|
content: [{
|
|
3852
3924
|
type: "text",
|
|
@@ -3859,6 +3931,14 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
|
|
|
3859
3931
|
const profileDef = getProfile2(profile);
|
|
3860
3932
|
const registry = profileDef?.boundsSchema?.actionTypes;
|
|
3861
3933
|
if (Array.isArray(registry) && registry.length > 0 && !registry.includes(declaredActionType)) {
|
|
3934
|
+
recordBlocked(state2, tool, {
|
|
3935
|
+
kind: "bound",
|
|
3936
|
+
detail: `action_type "${declaredActionType}" is not in profile ${profile}'s actionTypes registry.`,
|
|
3937
|
+
field: "action_type",
|
|
3938
|
+
value: declaredActionType,
|
|
3939
|
+
who: "gateway",
|
|
3940
|
+
code: "INVALID_ACTION_TYPE"
|
|
3941
|
+
});
|
|
3862
3942
|
return {
|
|
3863
3943
|
content: [{
|
|
3864
3944
|
type: "text",
|
|
@@ -3872,6 +3952,7 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
|
|
|
3872
3952
|
if (matchingAuths.length === 0) return noEligibleMandate(refusals);
|
|
3873
3953
|
const errors = [];
|
|
3874
3954
|
const passers = [];
|
|
3955
|
+
const structuredRefusals = [];
|
|
3875
3956
|
for (const candidate of matchingAuths) {
|
|
3876
3957
|
const { result } = await state2.gatekeeper.verifyExecution(candidate.authorizationId, execution, {
|
|
3877
3958
|
bounds: candidate.bounds,
|
|
@@ -3880,6 +3961,13 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
|
|
|
3880
3961
|
const capped = result.approved ? zeroCappedBound(candidate.profileId, candidate.bounds ?? candidate.frame, typeof execution.action_type === "string" ? execution.action_type : void 0) : null;
|
|
3881
3962
|
if (result.approved && capped) {
|
|
3882
3963
|
errors.push(`${candidate.path}: ${capped} is 0 \u2014 this mandate does not allow "${execution.action_type}"`);
|
|
3964
|
+
structuredRefusals.push({
|
|
3965
|
+
authorizationId: candidate.authorizationId,
|
|
3966
|
+
profileId: candidate.profileId,
|
|
3967
|
+
code: "BOUND_EXCEEDED",
|
|
3968
|
+
field: typeof execution.action_type === "string" ? execution.action_type : void 0,
|
|
3969
|
+
bound: 0
|
|
3970
|
+
});
|
|
3883
3971
|
} else if (result.approved) {
|
|
3884
3972
|
passers.push(candidate);
|
|
3885
3973
|
} else {
|
|
@@ -3890,9 +3978,20 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
|
|
|
3890
3978
|
return `${candidate.path}: ${e.message}`;
|
|
3891
3979
|
});
|
|
3892
3980
|
errors.push(...reasons);
|
|
3981
|
+
for (const e of result.errors) {
|
|
3982
|
+
structuredRefusals.push({
|
|
3983
|
+
authorizationId: candidate.authorizationId,
|
|
3984
|
+
profileId: candidate.profileId,
|
|
3985
|
+
code: e.code,
|
|
3986
|
+
field: e.field,
|
|
3987
|
+
bound: e.bound,
|
|
3988
|
+
actual: e.actual
|
|
3989
|
+
});
|
|
3990
|
+
}
|
|
3893
3991
|
}
|
|
3894
3992
|
}
|
|
3895
3993
|
if (passers.length === 0) {
|
|
3994
|
+
recordGatekeeperRefusal(state2, tool, structuredRefusals);
|
|
3896
3995
|
return {
|
|
3897
3996
|
content: [{
|
|
3898
3997
|
type: "text",
|
|
@@ -4044,6 +4143,7 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
|
|
|
4044
4143
|
state2.cache.markNeedsReapproval(auth.authorizationId);
|
|
4045
4144
|
const remaining = candidates.filter((c) => c.authorizationId !== auth.authorizationId);
|
|
4046
4145
|
if (remaining.length === 0) {
|
|
4146
|
+
recordAsRefusal(state2, tool, auth.authorizationId, { code: "VERSION_UNSUPPORTED" });
|
|
4047
4147
|
return {
|
|
4048
4148
|
content: [{
|
|
4049
4149
|
type: "text",
|
|
@@ -4059,9 +4159,11 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
|
|
|
4059
4159
|
}
|
|
4060
4160
|
if (err instanceof SPReceiptError && isStaleMandateRefusal(err)) {
|
|
4061
4161
|
state2.cache.invalidate(auth.authorizationId);
|
|
4062
|
-
const
|
|
4162
|
+
const staleErr = err.body?.errors?.[0];
|
|
4163
|
+
const code = staleErr?.code ?? "unknown";
|
|
4063
4164
|
const remaining = candidates.filter((c) => c.authorizationId !== auth.authorizationId);
|
|
4064
4165
|
if (remaining.length === 0) {
|
|
4166
|
+
recordAsRefusal(state2, tool, auth.authorizationId, staleErr);
|
|
4065
4167
|
console.error(
|
|
4066
4168
|
`[Suveren MCP] fallback(${tool.namespacedName}): mandate ${auth.authorizationId} invalid (${code}) \u2014 no remaining candidates`
|
|
4067
4169
|
);
|
|
@@ -4141,6 +4243,7 @@ Proposal ID: ${proposal.id}. Use check-pending-commitments to track status.`
|
|
|
4141
4243
|
}
|
|
4142
4244
|
}
|
|
4143
4245
|
if (err instanceof SPReceiptError && err.statusCode === 422) {
|
|
4246
|
+
recordAsRefusal(state2, tool, auth.authorizationId, err.body?.errors?.[0]);
|
|
4144
4247
|
return {
|
|
4145
4248
|
content: [{ type: "text", text: `Action blocked: ${err.message} (hard team ceiling \u2014 contact the team admin)` }],
|
|
4146
4249
|
isError: true
|
|
@@ -4151,6 +4254,7 @@ Proposal ID: ${proposal.id}. Use check-pending-commitments to track status.`
|
|
|
4151
4254
|
if (errors403?.[0]?.code === "MANDATE_REVOKED") {
|
|
4152
4255
|
state2.cache.invalidate(auth.authorizationId);
|
|
4153
4256
|
}
|
|
4257
|
+
recordAsRefusal(state2, tool, auth.authorizationId, errors403?.[0]);
|
|
4154
4258
|
return {
|
|
4155
4259
|
content: [{ type: "text", text: `Blocked by SP: ${err.message}` }],
|
|
4156
4260
|
isError: true
|
|
@@ -4798,6 +4902,8 @@ async function executeCommitted(proposal, state2, integrationManager2) {
|
|
|
4798
4902
|
if (err instanceof SPReceiptError) {
|
|
4799
4903
|
const code = err.body.errors?.[0]?.code;
|
|
4800
4904
|
if (code === "PROPOSAL_ALREADY_EXECUTED") {
|
|
4905
|
+
const local = localOutcomeOfExecuted(proposal, state2);
|
|
4906
|
+
if (local) return local;
|
|
4801
4907
|
return {
|
|
4802
4908
|
text: `Proposal ${proposal.id} has already been executed by another request.`
|
|
4803
4909
|
};
|
|
@@ -4865,6 +4971,15 @@ async function executeCommitted(proposal, state2, integrationManager2) {
|
|
|
4865
4971
|
state2.executionJournal.complete(receiptId, "failed");
|
|
4866
4972
|
throw err;
|
|
4867
4973
|
}
|
|
4974
|
+
const resultText = result.content?.[0]?.text ?? JSON.stringify(result);
|
|
4975
|
+
if (result.isError) {
|
|
4976
|
+
state2.executionJournal.complete(receiptId, "failed", "refused");
|
|
4977
|
+
return {
|
|
4978
|
+
text: `Proposal ${proposal.id} was approved, but ${integrationId} refused to run it \u2014 nothing was done.
|
|
4979
|
+
Reason: ${resultText}`,
|
|
4980
|
+
isError: true
|
|
4981
|
+
};
|
|
4982
|
+
}
|
|
4868
4983
|
state2.executionJournal.complete(receiptId, "done");
|
|
4869
4984
|
state2.executionLog.record({
|
|
4870
4985
|
profileId: proposal.profileId,
|
|
@@ -4872,7 +4987,6 @@ async function executeCommitted(proposal, state2, integrationManager2) {
|
|
|
4872
4987
|
execution: proposal.executionContext,
|
|
4873
4988
|
timestamp: Math.floor(Date.now() / 1e3)
|
|
4874
4989
|
});
|
|
4875
|
-
const resultText = result.content?.[0]?.text ?? JSON.stringify(result);
|
|
4876
4990
|
return { text: `Proposal ${proposal.id} committed and executed.
|
|
4877
4991
|
Result: ${resultText}` };
|
|
4878
4992
|
} catch (err) {
|
|
@@ -4883,6 +4997,21 @@ Result: ${resultText}` };
|
|
|
4883
4997
|
};
|
|
4884
4998
|
}
|
|
4885
4999
|
}
|
|
5000
|
+
function localOutcomeOfExecuted(proposal, state2) {
|
|
5001
|
+
const entry = state2.executionJournal?.findByProposal?.(proposal.id);
|
|
5002
|
+
if (!entry || entry.state === "done") return null;
|
|
5003
|
+
const integration = proposal.tool.split("__")[0];
|
|
5004
|
+
if (entry.state === "failed") {
|
|
5005
|
+
return {
|
|
5006
|
+
text: entry.outcome === "refused" ? `Proposal ${proposal.id} was approved, but ${integration} refused to run it \u2014 nothing was done. Do not call the tool again with the same values; a new attempt needs a new request and approval.` : `Proposal ${proposal.id} was approved, but running it failed \u2014 whether anything happened in ${integration} is unknown. Check ${integration} before trying again; a new attempt needs a new request and approval.`,
|
|
5007
|
+
isError: true
|
|
5008
|
+
};
|
|
5009
|
+
}
|
|
5010
|
+
return {
|
|
5011
|
+
text: `Proposal ${proposal.id} was approved and started running; the outcome is not known yet. Do not call the tool again.`,
|
|
5012
|
+
isError: false
|
|
5013
|
+
};
|
|
5014
|
+
}
|
|
4886
5015
|
function checkPendingCommitmentsHandler(state2, integrationManager2) {
|
|
4887
5016
|
return async (args) => {
|
|
4888
5017
|
if (!state2.spClient.isUnlocked()) {
|
|
@@ -4907,6 +5036,13 @@ function checkPendingCommitmentsHandler(state2, integrationManager2) {
|
|
|
4907
5036
|
};
|
|
4908
5037
|
}
|
|
4909
5038
|
if (match.status === "executed") {
|
|
5039
|
+
const local = localOutcomeOfExecuted(match, state2);
|
|
5040
|
+
if (local) {
|
|
5041
|
+
return {
|
|
5042
|
+
content: [{ type: "text", text: local.text }],
|
|
5043
|
+
...local.isError ? { isError: true } : {}
|
|
5044
|
+
};
|
|
5045
|
+
}
|
|
4910
5046
|
const result = match.executionResult ? `
|
|
4911
5047
|
Result: ${JSON.stringify(match.executionResult, null, 2)}` : " The action ran; the gateway did not retain its output.";
|
|
4912
5048
|
return {
|
|
@@ -7767,6 +7903,7 @@ app.post("/internal/stop-all-running", internalOnly, async (_req, res) => {
|
|
|
7767
7903
|
}
|
|
7768
7904
|
});
|
|
7769
7905
|
var sseSessions = /* @__PURE__ */ new Map();
|
|
7906
|
+
var SESSION_NOT_FOUND = { jsonrpc: "2.0", error: { code: -32001, message: "Session not found" }, id: null };
|
|
7770
7907
|
var guardAgent = agentAccess(
|
|
7771
7908
|
agentToken,
|
|
7772
7909
|
(req) => req.path === "/sse" && req.method === "GET" || req.path === "/mcp" && req.method === "POST" && !req.headers["mcp-session-id"]
|
|
@@ -7790,7 +7927,7 @@ app.post("/messages", guardAgent, async (req, res) => {
|
|
|
7790
7927
|
const sessionId = req.query.sessionId;
|
|
7791
7928
|
const transport = sseSessions.get(sessionId);
|
|
7792
7929
|
if (!transport) {
|
|
7793
|
-
res.status(
|
|
7930
|
+
res.status(404).json(SESSION_NOT_FOUND);
|
|
7794
7931
|
return;
|
|
7795
7932
|
}
|
|
7796
7933
|
await transport.handlePostMessage(req, res, req.body);
|
|
@@ -7826,6 +7963,10 @@ app.all("/mcp", guardAgent, async (req, res) => {
|
|
|
7826
7963
|
}
|
|
7827
7964
|
return;
|
|
7828
7965
|
}
|
|
7966
|
+
if (sessionId) {
|
|
7967
|
+
res.status(404).json(SESSION_NOT_FOUND);
|
|
7968
|
+
return;
|
|
7969
|
+
}
|
|
7829
7970
|
res.status(400).json({ error: "Bad request \u2014 missing or invalid session" });
|
|
7830
7971
|
} else {
|
|
7831
7972
|
res.status(405).json({ error: "Method not allowed" });
|