@metamynd/agentsafe-mcp-guard 0.3.5 → 0.3.6

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/README.md CHANGED
@@ -99,13 +99,36 @@ response that omits it — the one place a future backend change could quietly r
99
99
  confused-deputy gap this check exists to close, with nothing else here able to notice. The
100
100
  decision is unchanged (still tolerated, not blocked) — this is visibility, not a new refusal.
101
101
 
102
- ### Replay and cumulative spend (`requireAuthorization`)
102
+ **0.3.6 two gaps a security review found in what this guard actually enforces, not just what
103
+ its docs claimed.** First: `requireAuthorization`'s doc only ever named replay and cumulative
104
+ spend as what it closes, but rate limits, circuit breakers, and spend-pattern anomaly detection
105
+ are equally stateful and equally invisible to the stateless bundle re-check — the doc undersold
106
+ its own scope. A value-bearing call permitted with `requireAuthorization` off now logs a warning
107
+ naming all five. Second: `guardIncomingTool`'s capability check only ran when the CALLER chose to
108
+ include `signed.capability` — an agent could simply omit it and the "authorize $150, execute
109
+ $5,000" protection never engaged, verifier configured or not. New `requireCapability: true` makes
110
+ an omitted capability a hard block (`CAPABILITY_REQUIRED`) instead of a silent pass-through. Both
111
+ off by default — existing embeds are unchanged.
112
+
113
+ ### Replay, cumulative spend, rate limits, breakers, spend anomalies (`requireAuthorization`)
103
114
 
104
115
  Re-evaluating policy per request (above) proves the request is well-formed and in-policy — it
105
116
  does **not** stop a captured, still-fresh request from being replayed, and it can't enforce the
106
117
  mandate's TOTAL budget across many separately-legal calls (each is only checked against its own
107
- per-transaction cap). Both are the stateful issuer gate's job, not something a stateless re-check
108
- can do on its own.
118
+ per-transaction cap). Neither is something a stateless re-check can do on its own: both, like
119
+ rate limits, circuit breakers, and spend-pattern anomaly detection, key off the agent's history
120
+ on the issuer's side, which never travels to this guard's stateless bundle re-check. **All of
121
+ these are the stateful issuer gate's job.** `requireAuthorization` is the one setting that closes
122
+ all of them at once, because it forces the exact request back through that gate before this
123
+ Service executes anything. Left off, a value-bearing call permitted here logs a warning saying
124
+ exactly that, so the gap is visible in your own logs rather than silent:
125
+
126
+ ```
127
+ [mcp-guard] "flight-purchase" (amount=100) permitted in trustless mode — rate-limit,
128
+ circuit-breaker, replay, cumulative-spend, and spend-anomaly floors are stateful and were NOT
129
+ re-verified against live issuer state. Set requireAuthorization:true for custodial/value-bearing
130
+ surfaces.
131
+ ```
109
132
 
110
133
  ```js
111
134
  const guard = createMcpGuard({ serviceDid, issuerApi, requireAuthorization: true });
@@ -199,6 +222,19 @@ transaction** — the host reconstructs the tx and verifies MetaMynd's signature
199
222
  `checkCapabilityBinding` from `magp-bind`), so "authorize $150, execute $5,000" is rejected in the
200
223
  prod guard, not just the demo gateway. No verifier configured → opt-in (unchanged).
201
224
 
225
+ Presenting a capability is otherwise the **caller's** choice: an agent can simply omit
226
+ `signed.capability` and the check above never runs, verifier configured or not. Set
227
+ `requireCapability: true` to close that omission — a PERMIT with no capability is then blocked
228
+ (`CAPABILITY_REQUIRED`) instead of silently passing through unbound:
229
+
230
+ ```js
231
+ const guard = createMcpGuard({ serviceDid, verifyCapability, requireCapability: true });
232
+ ```
233
+
234
+ Off by default, so an existing integration whose callers don't yet present a capability keeps
235
+ working unchanged. Turn it on for any Service where the decision-token binding is meant to be
236
+ mandatory, not opt-in.
237
+
202
238
  Holds carry an expiry (§7a.4): if not captured, the reservation auto-voids and the budget returns
203
239
  to the cap; a party can also void explicitly via `POST /policy/mandate/authorize/:id/void`.
204
240
 
@@ -42,8 +42,22 @@ const CLOCK_SKEW_TOLERANCE_MS = 30 * 1000;
42
42
  * SPEND, neither of which the stateless re-check above can enforce on its own. Off by default:
43
43
  * it costs a network round trip per value-bearing call, so it's a deliberate choice, not a
44
44
  * strictly-dominant one — a Service happy with per-request policy re-evaluation alone can skip it.
45
+ *
46
+ * The stateless re-check ALSO cannot see the issuer's other STATEFUL floors — rate limits,
47
+ * circuit breakers, and spend-pattern anomaly detection all key off the agent's server-side
48
+ * history, which never travels to the edge. `requireAuthorization` is the one mechanism that
49
+ * closes all of these at once, because it forces the exact request through the stateful gate
50
+ * before this Service will execute it. A value-bearing call permitted here with
51
+ * `requireAuthorization` OFF logs a warning for exactly this reason — see guardIncomingTool.
52
+ * @param {boolean} [cfg.requireCapability] when true AND `verifyCapability` is configured, a PERMIT
53
+ * verdict for a call with NO `signed.capability` is now BLOCKED (`CAPABILITY_REQUIRED`) rather than
54
+ * silently passing through unbound. Without this, capability binding is opt-in from the CALLER's
55
+ * side — an agent can simply omit `capability` and the "authorize $150, execute $5,000" check below
56
+ * never runs at all, since it only fires when the field is present. Off by default (an existing
57
+ * integrator's un-capability-aware callers must keep working); set true on any Service where
58
+ * capability binding is meant to be mandatory, not opt-in.
45
59
  */
46
- export function createMcpGuard({ serviceDid, serviceKey, issuerApi, fetchBundle, policyPublicKey, settlementStore, verifyCapability, requireAuthorization = false } = {}) {
60
+ export function createMcpGuard({ serviceDid, serviceKey, issuerApi, fetchBundle, policyPublicKey, settlementStore, verifyCapability, requireAuthorization = false, requireCapability = false } = {}) {
47
61
  if (!serviceDid) throw new Error('createMcpGuard requires { serviceDid }');
48
62
  const base = issuerApi ? issuerApi.replace(/\/$/, '') : null;
49
63
  const privateKey = serviceKey
@@ -273,20 +287,40 @@ export function createMcpGuard({ serviceDid, serviceKey, issuerApi, fetchBundle,
273
287
  // exact transaction — the host reconstructs the tx + verifies MetaMynd's signature OFFLINE,
274
288
  // so "authorize $150, execute $5,000" (authorize-A / execute-B) is rejected HERE, in the
275
289
  // prod guard, not just the demo gateway. No verifier configured → unchanged (opt-in).
276
- if (signed?.capability && typeof verifyCapability === 'function') {
277
- let bind;
278
- try { bind = await verifyCapability(signed); }
279
- catch (err) { bind = { ok: false, reasonCode: 'CAPABILITY_CHECK_ERROR', error: String(err?.message ?? err) }; }
280
- if (!bind?.ok) {
281
- const err = new Error(`MCP guard CAPABILITY "${action}": ${bind?.reasonCode ?? 'CAPABILITY_INVALID'}`);
290
+ //
291
+ // Presenting a capability is the CALLER's choice, not this guard's: an agent can simply
292
+ // omit `signed.capability` and this whole check is skipped, verifier or not — that is
293
+ // exactly the omission `requireCapability` closes. Without it, capability binding is
294
+ // opt-in from the wrong side of the trust boundary.
295
+ if (typeof verifyCapability === 'function') {
296
+ if (signed?.capability) {
297
+ let bind;
298
+ try { bind = await verifyCapability(signed); }
299
+ catch (err) { bind = { ok: false, reasonCode: 'CAPABILITY_CHECK_ERROR', error: String(err?.message ?? err) }; }
300
+ if (!bind?.ok) {
301
+ const err = new Error(`MCP guard CAPABILITY "${action}": ${bind?.reasonCode ?? 'CAPABILITY_INVALID'}`);
302
+ err.name = 'GovernanceBlocked';
303
+ err.governance = { decision: 'block', reasonCode: bind?.reasonCode ?? 'CAPABILITY_INVALID' };
304
+ throw err;
305
+ }
306
+ } else if (requireCapability) {
307
+ const err = new Error(`MCP guard CAPABILITY "${action}": CAPABILITY_REQUIRED`);
282
308
  err.name = 'GovernanceBlocked';
283
- err.governance = { decision: 'block', reasonCode: bind?.reasonCode ?? 'CAPABILITY_INVALID' };
309
+ err.governance = { decision: 'block', reasonCode: 'CAPABILITY_REQUIRED' };
284
310
  throw err;
285
311
  }
286
312
  }
287
313
  if (decision.decision === 'observe') {
288
314
  console.warn(`[mcp-guard] OBSERVE "${action}": ${decision.reasonCode} — served under monitoring`);
289
315
  }
316
+ // Trustless mode cannot see the issuer's stateful floors (see requireAuthorization's own
317
+ // doc above) — surface that as a loud, per-call signal rather than a silent gap, so an
318
+ // operator serving real value through this path finds out from their own logs rather
319
+ // than from an incident. Gated on value-bearing (amount > 0): a free/read action has
320
+ // nothing for those floors to protect, so warning on it would just be noise.
321
+ if (!requireAuthorization && Number(signed?.amount) > 0) {
322
+ console.warn(`[mcp-guard] "${action}" (amount=${signed.amount}) permitted in trustless mode — rate-limit, circuit-breaker, replay, cumulative-spend, and spend-anomaly floors are stateful and were NOT re-verified against live issuer state. Set requireAuthorization:true for custodial/value-bearing surfaces.`);
323
+ }
290
324
  return handler(signed, ...rest);
291
325
  };
292
326
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metamynd/agentsafe-mcp-guard",
3
- "version": "0.3.5",
3
+ "version": "0.3.6",
4
4
  "description": "Zero-dependency trustless governance for the SERVICE side. An MCP server or API re-verifies a calling agent's signed request against the agent's own published policy \u00e2\u20ac\u201d so an agent that ignores its own guard still cannot make your service act.",
5
5
  "type": "module",
6
6
  "main": "./agentsafe-mcp-guard.mjs",