@suveren/gateway 0.20.2 → 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.
Files changed (27) hide show
  1. package/content/integrations/erp.json +1 -1
  2. package/dist/mcp-server/http.mjs +146 -10
  3. package/dist/ui/assets/index-BBjT8ue7.js +116 -0
  4. package/dist/ui/assets/{index-blnpvM2g.css → index-D5hgQ_8Z.css} +1 -1
  5. package/dist/ui/index.html +2 -2
  6. package/node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition/README.md +105 -35
  7. package/node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition/dist/index.d.ts +72 -0
  8. package/node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition/dist/index.js +388 -0
  9. package/node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition/dist/index.js.map +1 -0
  10. package/node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition/package.json +31 -19
  11. package/node_modules/@modelcontextprotocol/sdk/node_modules/express/Readme.md +15 -9
  12. package/node_modules/@modelcontextprotocol/sdk/node_modules/express/lib/application.js +2 -2
  13. package/node_modules/@modelcontextprotocol/sdk/node_modules/express/lib/request.js +32 -19
  14. package/node_modules/@modelcontextprotocol/sdk/node_modules/express/lib/response.js +27 -30
  15. package/node_modules/@modelcontextprotocol/sdk/node_modules/express/node_modules/content-type/LICENSE +22 -0
  16. package/node_modules/@modelcontextprotocol/sdk/node_modules/express/node_modules/content-type/README.md +71 -0
  17. package/node_modules/@modelcontextprotocol/sdk/node_modules/express/node_modules/content-type/dist/index.d.ts +46 -0
  18. package/node_modules/@modelcontextprotocol/sdk/node_modules/express/node_modules/content-type/dist/index.js +176 -0
  19. package/node_modules/@modelcontextprotocol/sdk/node_modules/express/node_modules/content-type/dist/index.js.map +1 -0
  20. package/node_modules/@modelcontextprotocol/sdk/node_modules/express/node_modules/content-type/package.json +52 -0
  21. package/node_modules/@modelcontextprotocol/sdk/node_modules/express/package.json +10 -10
  22. package/node_modules/@types/node/README.md +1 -1
  23. package/node_modules/@types/node/package.json +2 -2
  24. package/node_modules/@types/node/util.d.ts +6 -3
  25. package/package.json +1 -1
  26. package/dist/ui/assets/index-1s1q-XLL.js +0 -116
  27. package/node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition/index.js +0 -536
@@ -6,7 +6,7 @@
6
6
  "icon": "briefcase",
7
7
  "profile": "sales",
8
8
  "npmPackage": "@humanagencyp/erp-mcp",
9
- "npmVersion": "0.5.0",
9
+ "npmVersion": "0.6.0",
10
10
  "mcp": {
11
11
  "command": "erp-mcp",
12
12
  "args": [],
@@ -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
- content: [{
3578
- type: "text",
3579
- text: `Refused: "${tool.namespacedName}" is not available. No ticket was requested.`
3580
- }],
3581
- isError: true
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 code = err.body?.errors?.[0]?.code ?? "unknown";
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 {