@metamynd/agentsafe-guard 0.12.0 → 0.12.1
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-guard.mjs +47 -6
- package/package.json +1 -1
package/agentsafe-guard.mjs
CHANGED
|
@@ -297,7 +297,7 @@ export function createGuard(opts = {}) {
|
|
|
297
297
|
* @param {Array<{standardKey:string,document:object}>} [p.standards] enforced Standards bound to the agent
|
|
298
298
|
* @param {Array<{standardKey:string,document:object}>} [p.sops] active SOPs assigned to the agent
|
|
299
299
|
* @param {object} [p.mandate] the ODRL mandate document (omit to skip the mandate layer)
|
|
300
|
-
* @param {{action:string,amount?:number,merchant?:string,context?:object,cumulativeSpend?:number,now?:string}} p.request
|
|
300
|
+
* @param {{action:string,amount?:number,currency?:string,merchant?:string,resource?:string,context?:object,cumulativeSpend?:number,now?:string}} p.request
|
|
301
301
|
* @returns {{decision:'allow'|'block'|'escalate',reasonCode:string|null,authorizationId:null,remaining:null,proofRef:null}}
|
|
302
302
|
*/
|
|
303
303
|
function evaluateLocally({ contained = null, operatingMode = null, standards = [], sops = [], mandate, request }) {
|
|
@@ -310,7 +310,7 @@ export function createGuard(opts = {}) {
|
|
|
310
310
|
const reasonCode = contained.status === 'quarantined' ? 'AGENT_QUARANTINED' : 'AGENT_SUSPENDED';
|
|
311
311
|
return { decision, reasonCode, authorizationId: null, remaining: null, proofRef: null };
|
|
312
312
|
}
|
|
313
|
-
const { action, amount = 0, currency = 'USD', merchant = '', context = {}, cumulativeSpend = amount, now } = request;
|
|
313
|
+
const { action, amount = 0, currency = 'USD', merchant = '', resource = null, context = {}, cumulativeSpend = amount, now } = request;
|
|
314
314
|
// Operating-mode autonomy ladder (Phase 2.5b): the trust-driven posture rides as a
|
|
315
315
|
// SIBLING (like `contained`) and biases the edge verdict identically to the gate.
|
|
316
316
|
// READ_ONLY denies a value-bearing action up-front; SUPERVISED/RESTRICTED only
|
|
@@ -326,7 +326,11 @@ export function createGuard(opts = {}) {
|
|
|
326
326
|
standards,
|
|
327
327
|
sops,
|
|
328
328
|
mandate,
|
|
329
|
-
|
|
329
|
+
// currency/merchant/resource are signed fields too, same as action/agentDid/amount —
|
|
330
|
+
// omitting them here means a currency-scoped amount-over/cumulative-over Standards/SOP
|
|
331
|
+
// atom always sees currency as absent and fires closed. Mirrors mandate.service.ts's
|
|
332
|
+
// ruleCtx (PR #588) and the same fix in agentsafe-mcp-guard.mjs's verdictFromBundle.
|
|
333
|
+
context: applySignedLast(context, { action, agentDid, amount, currency, merchant, resource }),
|
|
330
334
|
mandateRequest: mandate
|
|
331
335
|
? {
|
|
332
336
|
target: action,
|
|
@@ -341,6 +345,9 @@ export function createGuard(opts = {}) {
|
|
|
341
345
|
// amount, since undefined never equals a real unit. Defaults to 'USD' to match
|
|
342
346
|
// the same default this file already uses for authorize()/buildSignedRequest().
|
|
343
347
|
'mm:currency': currency,
|
|
348
|
+
// Unprefixed `resource` (not `mm:resource`) to match the constraint's own
|
|
349
|
+
// leftOperand (ResourceService.scopeConstraint()) — mirrors mandate.service.ts.
|
|
350
|
+
resource,
|
|
344
351
|
}),
|
|
345
352
|
}
|
|
346
353
|
: undefined,
|
|
@@ -372,14 +379,29 @@ export function createGuard(opts = {}) {
|
|
|
372
379
|
return b;
|
|
373
380
|
}
|
|
374
381
|
|
|
375
|
-
/**
|
|
382
|
+
/**
|
|
383
|
+
* Map a fetched bundle into the shape evaluateLocally expects, for one action.
|
|
384
|
+
*
|
|
385
|
+
* `mandateFound` distinguishes "no mandate matches THIS action" from "omit mandate to
|
|
386
|
+
* skip the layer" (evaluateLocally's own documented, intentional behavior for a caller
|
|
387
|
+
* that never resolves one at all, e.g. guardToolLocal's caller-supplied bundle) — found
|
|
388
|
+
* live: this used to fall back to `mandates[0]` (an ARBITRARY, possibly unrelated
|
|
389
|
+
* mandate for a completely different action) rather than correctly reporting no
|
|
390
|
+
* authority for this one, and `authorizeLocal` never distinguished either case from a
|
|
391
|
+
* genuinely-mandate-less bundle, so an ungranted action with no Standard/SOP rule
|
|
392
|
+
* happening to also catch it was silently ALLOWED instead of NO_PERMISSION_FOR_ACTION.
|
|
393
|
+
*/
|
|
376
394
|
function _bundleFor(b, action) {
|
|
395
|
+
const mandates = b.mandates ?? [];
|
|
396
|
+
const match = mandates.find((m) => m.action === action);
|
|
377
397
|
return {
|
|
378
398
|
contained: b.contained ?? null,
|
|
379
399
|
operatingMode: b.operatingMode ?? null,
|
|
380
400
|
standards: (b.standards ?? []).map((s) => ({ standardKey: s.id ?? s.standardKey ?? 'standard', document: s.document })).filter((s) => s.document),
|
|
381
401
|
sops: (b.sops ?? []).map((s) => ({ standardKey: s.id ?? s.sopId ?? 'sop', document: s.document })).filter((s) => s.document),
|
|
382
|
-
mandate:
|
|
402
|
+
mandate: match?.document,
|
|
403
|
+
mandateFound: !!match,
|
|
404
|
+
anyMandates: mandates.length > 0,
|
|
383
405
|
};
|
|
384
406
|
}
|
|
385
407
|
|
|
@@ -470,7 +492,26 @@ export function createGuard(opts = {}) {
|
|
|
470
492
|
const sig = b?.proof?.signature;
|
|
471
493
|
if (!anchor?.sigDigest || !sig || _sha256(sig) !== anchor.sigDigest) return authorize(input);
|
|
472
494
|
}
|
|
473
|
-
const
|
|
495
|
+
const { mandateFound, anyMandates, ...bundleForAction } = _bundleFor(b, action);
|
|
496
|
+
// No mandate covers this action at all — refuse outright rather than let evaluateLocally
|
|
497
|
+
// silently allow (its documented "omit mandate to skip the layer" behavior is for a
|
|
498
|
+
// caller that never intended a mandate check, not for one that looked and found none).
|
|
499
|
+
// Standards/SOP containment still applies first — a suspended/quarantined agent is
|
|
500
|
+
// refused for THAT reason, not misreported as merely lacking this one action.
|
|
501
|
+
if (!mandateFound) {
|
|
502
|
+
const contained = bundleForAction.contained;
|
|
503
|
+
if (contained && contained.status) {
|
|
504
|
+
const decision = contained.status === 'quarantined' ? 'quarantine' : 'suspend';
|
|
505
|
+
const reasonCode = contained.status === 'quarantined' ? 'AGENT_QUARANTINED' : 'AGENT_SUSPENDED';
|
|
506
|
+
const local = { decision, reasonCode, authorizationId: null, remaining: null, proofRef: null };
|
|
507
|
+
reportLocalDecision(action, local.decision, local.reasonCode);
|
|
508
|
+
return local;
|
|
509
|
+
}
|
|
510
|
+
const local = { decision: 'block', reasonCode: anyMandates ? 'NO_PERMISSION_FOR_ACTION' : 'NO_MANDATE', authorizationId: null, remaining: null, proofRef: null };
|
|
511
|
+
reportLocalDecision(action, local.decision, local.reasonCode);
|
|
512
|
+
return local;
|
|
513
|
+
}
|
|
514
|
+
const local = evaluateLocally({ ...bundleForAction, request: input });
|
|
474
515
|
// allow/observe both PERMIT; block/escalate/contain are decided locally with no network.
|
|
475
516
|
const permits = local.decision === 'allow' || local.decision === 'observe';
|
|
476
517
|
if (!permits) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metamynd/agentsafe-guard",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.1",
|
|
4
4
|
"description": "Zero-dependency runtime governance for any Node AI agent \u2014 gate tool calls through MetaMynd/AgentSafe (allow / block / escalate) against the agent's mandate, enforced Standards, and SOPs. Ed25519-signed, deterministic, fail-closed.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./agentsafe-guard.mjs",
|