@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.
- package/agentsafe-mcp-guard.mjs +26 -6
- package/package.json +1 -1
package/agentsafe-mcp-guard.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|