@metamynd/agentsafe-mcp-guard 0.3.3 → 0.3.5
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 +19 -0
- package/agentsafe-mcp-guard.mjs +13 -1
- package/package.json +1 -1
- package/policy-core.mjs +8 -7
package/README.md
CHANGED
|
@@ -80,6 +80,25 @@ server time, a pre-signing window rather than ordinary clock skew. `issuedAt` ma
|
|
|
80
80
|
to the freshness window (network/processing delay) but lead by no more than 30 seconds (clock
|
|
81
81
|
skew only).
|
|
82
82
|
|
|
83
|
+
**0.3.4 — a mandate's currency check no longer lets a PROHIBITION be dodged by relabeling
|
|
84
|
+
the currency.** A payAmount/cumulativeSpend constraint issued with a `unit` (currency) is
|
|
85
|
+
only satisfied in that currency — correct for a PERMISSION (fail closed to deny on a
|
|
86
|
+
mismatch), but a prohibition only fires when every one of its own constraints is satisfied,
|
|
87
|
+
so the identical "mismatch → not satisfied" rule let a prohibition like `payAmount gteq 1000
|
|
88
|
+
unit USD` be silently skipped by declaring any other currency, including a mere case
|
|
89
|
+
difference (`'usd'` vs `'USD'`). The currency comparison is also now case-insensitive.
|
|
90
|
+
|
|
91
|
+
**0.3.5 — a degraded claim now warns instead of only being silently tolerated.**
|
|
92
|
+
`claimAuthorization()`'s per-field cross-checks each skip when the issuer's claim response
|
|
93
|
+
omits that field — a deliberate, documented rolling-upgrade tolerance for a Service pinned
|
|
94
|
+
against an older backend whose response predates one of these fields existing at all. That
|
|
95
|
+
tolerance was never meant to also mask a REGRESSION on an otherwise-current backend: this
|
|
96
|
+
version logs (`console.warn`) whenever a value-bearing request's claim response omits
|
|
97
|
+
`agentDid`/`amount`/`currency`, or a request that signed a real `merchant` gets a claim
|
|
98
|
+
response that omits it — the one place a future backend change could quietly re-open the
|
|
99
|
+
confused-deputy gap this check exists to close, with nothing else here able to notice. The
|
|
100
|
+
decision is unchanged (still tolerated, not blocked) — this is visibility, not a new refusal.
|
|
101
|
+
|
|
83
102
|
### Replay and cumulative spend (`requireAuthorization`)
|
|
84
103
|
|
|
85
104
|
Re-evaluating policy per request (above) proves the request is well-formed and in-policy — it
|
package/agentsafe-mcp-guard.mjs
CHANGED
|
@@ -229,7 +229,19 @@ export function createMcpGuard({ serviceDid, serviceKey, issuerApi, fetchBundle,
|
|
|
229
229
|
if (!claim.claimed) return { decision: 'block', reasonCode: claim.reasonCode };
|
|
230
230
|
// The claim alone only proves SOME real, unclaimed authorization exists — it must also
|
|
231
231
|
// be FOR this agent and these exact values, or a cheap legitimate hold's id could be
|
|
232
|
-
// presented to unlock a completely different, more expensive execution.
|
|
232
|
+
// presented to unlock a completely different, more expensive execution. Each check is
|
|
233
|
+
// skipped when the claim response omits that field — tolerated for a Service pinned
|
|
234
|
+
// against an older, not-yet-migrated issuer whose response predates the field (see
|
|
235
|
+
// backend markEffect()'s own note) — but a value-bearing request with a real signed
|
|
236
|
+
// amount/merchant omitted from the claim is exactly the "field genuinely absent vs.
|
|
237
|
+
// issuer regressed" ambiguity that note warns about, so it's surfaced rather than
|
|
238
|
+
// silently trusted: this is the ONE place a future backend change could quietly
|
|
239
|
+
// re-open the confused-deputy gap this claim exists to close, and nothing else here
|
|
240
|
+
// would notice.
|
|
241
|
+
if (claim.agentDid === undefined) console.warn('[mcp-guard] claim response omitted agentDid — binding degraded to "some valid unclaimed authorization exists"');
|
|
242
|
+
if (Number(amount) > 0 && claim.amount === undefined) console.warn('[mcp-guard] claim response omitted amount for a value-bearing request — amount binding degraded');
|
|
243
|
+
if (Number(amount) > 0 && claim.currency === undefined) console.warn('[mcp-guard] claim response omitted currency for a value-bearing request — currency binding degraded');
|
|
244
|
+
if (merchant && claim.merchant === undefined) console.warn('[mcp-guard] claim response omitted merchant for a request that signed one — merchant binding degraded');
|
|
233
245
|
if (claim.agentDid !== undefined && claim.agentDid !== agentDid) return { decision: 'block', reasonCode: 'AUTHORIZATION_AGENT_MISMATCH' };
|
|
234
246
|
if (claim.amount !== undefined && Number(claim.amount) !== Number(amount)) return { decision: 'block', reasonCode: 'AUTHORIZATION_AMOUNT_MISMATCH' };
|
|
235
247
|
if (claim.currency !== undefined && claim.currency !== currency) return { decision: 'block', reasonCode: 'AUTHORIZATION_CURRENCY_MISMATCH' };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metamynd/agentsafe-mcp-guard",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
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",
|
package/policy-core.mjs
CHANGED
|
@@ -351,13 +351,14 @@ function reasonFor(constraint) {
|
|
|
351
351
|
if (!constraint) return "CONSTRAINT_FAILED";
|
|
352
352
|
return REASON_BY_OPERAND[constraint.leftOperand] ?? `CONSTRAINT_FAILED:${constraint.leftOperand}`;
|
|
353
353
|
}
|
|
354
|
-
function constraintSatisfied(c, req) {
|
|
354
|
+
function constraintSatisfied(c, req, strict) {
|
|
355
355
|
const op = OPERATORS[c.operator];
|
|
356
356
|
if (!op) return false;
|
|
357
357
|
const left = Object.prototype.hasOwnProperty.call(req.values, c.leftOperand) ? req.values[c.leftOperand] : void 0;
|
|
358
|
-
if (!op(left, c.rightOperand)
|
|
359
|
-
|
|
360
|
-
|
|
358
|
+
if (!c.unit) return op(left, c.rightOperand);
|
|
359
|
+
const currency = req.values["mm:currency"];
|
|
360
|
+
const unitMatches = typeof currency === "string" && currency.toUpperCase() === c.unit.toUpperCase();
|
|
361
|
+
return unitMatches ? op(left, c.rightOperand) : !strict;
|
|
361
362
|
}
|
|
362
363
|
function targetOf(rule, mandate) {
|
|
363
364
|
return rule.target ?? mandate.target;
|
|
@@ -379,7 +380,7 @@ function evaluateMandate(mandate, req) {
|
|
|
379
380
|
}
|
|
380
381
|
for (const p of mandate.prohibition ?? []) {
|
|
381
382
|
if (targetOf(p, mandate) !== req.target) continue;
|
|
382
|
-
const fires = (p.constraint ?? []).every((c) => constraintSatisfied(c, req));
|
|
383
|
+
const fires = (p.constraint ?? []).every((c) => constraintSatisfied(c, req, false));
|
|
383
384
|
if (fires) {
|
|
384
385
|
return {
|
|
385
386
|
decision: p.enforcement ?? "block",
|
|
@@ -397,10 +398,10 @@ function evaluateMandate(mandate, req) {
|
|
|
397
398
|
};
|
|
398
399
|
}
|
|
399
400
|
for (const p of perms) {
|
|
400
|
-
const failing = (p.constraint ?? []).find((c) => !constraintSatisfied(c, req));
|
|
401
|
+
const failing = (p.constraint ?? []).find((c) => !constraintSatisfied(c, req, true));
|
|
401
402
|
if (!failing) return { decision: "allow", reasonCode: "AUTHORIZED" };
|
|
402
403
|
}
|
|
403
|
-
const firstFail = (perms[0].constraint ?? []).find((c) => !constraintSatisfied(c, req));
|
|
404
|
+
const firstFail = (perms[0].constraint ?? []).find((c) => !constraintSatisfied(c, req, true));
|
|
404
405
|
return {
|
|
405
406
|
decision: firstFail?.onFail ?? "block",
|
|
406
407
|
reasonCode: reasonFor(firstFail),
|