@metamynd/agentsafe-mcp-guard 0.6.1 → 0.6.2

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.
@@ -152,13 +152,28 @@ export function createMcpGuard({ serviceDid, serviceKey, keyProvider: keyProvide
152
152
 
153
153
  /** Evaluate the agent's bundle against the request via policy-core (signed fields last). */
154
154
  function verdictFromBundle(bundle, req) {
155
- const { agentDid, action, amount = 0, currency = 'USD', merchant = '', itinerary = {}, cumulativeSpend = amount, now } = req;
156
- const mandate = (bundle.mandates ?? []).find((m) => m.action === action)?.document;
155
+ const { agentDid, action, amount = 0, currency = 'USD', merchant = '', resource = null, itinerary = {}, cumulativeSpend = amount, now } = req;
156
+ const mandates = bundle.mandates ?? [];
157
+ const mandate = mandates.find((m) => m.action === action)?.document;
158
+ // No mandate covers this action at all — refuse outright, matching mandate.service.ts's
159
+ // own first check (before signature/standards/anything else). Without this, `evaluate()`
160
+ // treats an omitted `mandate` as "skip the mandate layer" (its own documented, intentional
161
+ // behavior for a caller that never resolves one at all) — found live: an ungranted action
162
+ // with no Standard/SOP molecule happening to also catch it was silently ALLOWED here,
163
+ // while the hosted gate and the agent SDK both correctly refused the identical request.
164
+ if (!mandate) {
165
+ return { decision: 'block', reasonCode: mandates.length > 0 ? 'NO_PERMISSION_FOR_ACTION' : 'NO_MANDATE', authorizationId: null, remaining: null, proofRef: null };
166
+ }
157
167
  return evaluate({
158
168
  standards: (bundle.standards ?? []).map((s) => ({ standardKey: s.key, document: s.document })),
159
169
  sops: (bundle.sops ?? []).map((s) => ({ standardKey: `sop:${s.id}`, document: s.document })),
160
170
  mandate,
161
- context: applySignedLast(itinerary, { action, agentDid, amount }),
171
+ // currency/merchant/resource are signed fields, same as action/agentDid/amount above —
172
+ // omitting them here (found live: they were) means a currency-scoped amount-over/
173
+ // cumulative-over Standards/SOP atom always sees currency as absent and fires closed
174
+ // (SOP_SPEND_CAP on a genuinely in-cap request), and a resource-scope atom never runs
175
+ // at all. Mirrors mandate.service.ts's ruleCtx (PR #588), the parity target for this.
176
+ context: applySignedLast(itinerary, { action, agentDid, amount, currency, merchant, resource }),
162
177
  mandateRequest: mandate
163
178
  ? {
164
179
  target: action,
@@ -172,6 +187,9 @@ export function createMcpGuard({ serviceDid, serviceKey, keyProvider: keyProvide
172
187
  // — omitting this would make EVERY unit-bearing cap fail regardless of amount.
173
188
  // Defaults to 'USD', matching verifyRequest()'s own default for this field.
174
189
  'mm:currency': currency,
190
+ // Unprefixed `resource` (not `mm:resource`) to match the constraint's own
191
+ // leftOperand (ResourceService.scopeConstraint()) — mirrors mandate.service.ts.
192
+ resource,
175
193
  }),
176
194
  }
177
195
  : undefined,
@@ -182,17 +200,19 @@ export function createMcpGuard({ serviceDid, serviceKey, keyProvider: keyProvide
182
200
  * Verify an agent's presented signed authorize request, then re-evaluate policy
183
201
  * locally against the agent's issuer-hosted bundle. Fails CLOSED: any bad
184
202
  * signature, staleness, fetch error, or evaluation error returns a block.
185
- * @param {{agentDid,action,amount?,currency?,merchant?,itinerary?,nonce,issuedAt,signature}} signed
203
+ * @param {{agentDid,action,amount?,currency?,merchant?,resource?,itinerary?,nonce,issuedAt,signature}} signed
186
204
  * @returns {Promise<{decision:'allow'|'observe'|'block'|'escalate'|'suspend'|'quarantine',reasonCode:string|null}>}
187
205
  */
188
206
  async function verifyRequest(signed = {}) {
189
207
  try {
190
- const { agentDid, action, amount = 0, currency = 'USD', merchant = '', nonce, issuedAt, signature } = signed;
208
+ const { agentDid, action, amount = 0, currency = 'USD', merchant = '', resource = null, nonce, issuedAt, signature } = signed;
191
209
  if (!agentDid || !action || !nonce || !issuedAt || !signature) {
192
210
  return { decision: 'block', reasonCode: 'MALFORMED_REQUEST' };
193
211
  }
194
212
  // 1. Signature over the canonical message (§7.3), verified via key-in-DID (§4.1.2).
195
- const message = buildAuthMessage({ agentDid, action, amount, currency, merchant, nonce, issuedAt });
213
+ // `resource` MUST be included it's the 8th signed field (canonical.ts); omitting it
214
+ // here (found live: it was) rejects every genuinely-valid resource-bearing signature.
215
+ const message = buildAuthMessage({ agentDid, action, amount, currency, merchant, resource, nonce, issuedAt });
196
216
  if (!verifyDidSignature(agentDid, message, signature)) {
197
217
  return { decision: 'block', reasonCode: 'SIGNATURE_INVALID' };
198
218
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metamynd/agentsafe-mcp-guard",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
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",