@metamynd/agentsafe-guard 0.5.1 → 0.6.3
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 +44 -1
- package/agentsafe-guard.mjs +10 -4
- package/package.json +1 -1
- package/policy-core.mjs +36 -5
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ re-implement the gate:
|
|
|
25
25
|
([markdown](https://metamynd.ai/specs/magp-v1.0.md))
|
|
26
26
|
|
|
27
27
|
It defines agent identity, the canonical signed message (§8.3), the sixteen-stage order of
|
|
28
|
-
checks (§8.5), all
|
|
28
|
+
checks (§8.5), all 62 reason codes (Appendix A), delegation narrowing (§5.4), and evidence
|
|
29
29
|
you can verify offline without MetaMynd (§13.4). If you are writing a client in a language
|
|
30
30
|
other than JavaScript, read §8.3.3–8.3.5 first: key encoding, number stringification and
|
|
31
31
|
signed-vs-sent field identity each surface only as `SIGNATURE_INVALID`.
|
|
@@ -78,6 +78,19 @@ you state *our agents must carry a merchant allow-list* and find out when one do
|
|
|
78
78
|
`--json` for machine-readable output. Evaluation is local and pure: no holds are minted, no
|
|
79
79
|
nonces spent, no budget consumed, and a run costs one GET.
|
|
80
80
|
|
|
81
|
+
**0.6.0 — `amount-unknown`.** The same discipline applied to the amount itself: a spend cap
|
|
82
|
+
is only as good as the number it's checked against, and a signed-transaction or nested x402
|
|
83
|
+
payload can carry its amount somewhere a naive check never looks. `amount-unknown` is a
|
|
84
|
+
deny-by-default atom for exactly that case — an action whose value the gate can't determine
|
|
85
|
+
is blocked, not silently waved through an untested cap.
|
|
86
|
+
|
|
87
|
+
**0.6.2 — `amount-unknown` closes negative amounts too.** A negative `amount` (e.g. `-5`)
|
|
88
|
+
previously read as "a real, known number" and sailed straight past `amount-unknown` — and
|
|
89
|
+
past a naive `amount-over` cap, since `-5 > 500` is never true. An attacker submitting a
|
|
90
|
+
negative value could clear a spend cap for free, or erode a cumulative-spend tracker that
|
|
91
|
+
sums signed amounts over time. `amount-unknown` now fires on any non-finite, non-numeric,
|
|
92
|
+
**or negative** amount; a genuine `0` still passes.
|
|
93
|
+
|
|
81
94
|
```yaml
|
|
82
95
|
# .github/workflows/governance.yml
|
|
83
96
|
name: Governance
|
|
@@ -98,6 +111,36 @@ jobs:
|
|
|
98
111
|
set `AGENT_KEY` in the environment — `AGENT_KEY`, `AGENT_DID` and `METAMYND_API` all
|
|
99
112
|
override the file when present.
|
|
100
113
|
|
|
114
|
+
### As a packaged Action
|
|
115
|
+
|
|
116
|
+
Same check, packaged so you don't hand-roll the workflow above — and it writes a
|
|
117
|
+
pass/fail table straight into the PR's checks summary instead of a log a reviewer has to
|
|
118
|
+
open:
|
|
119
|
+
|
|
120
|
+
```yaml
|
|
121
|
+
# .github/workflows/governance.yml
|
|
122
|
+
name: Governance
|
|
123
|
+
on: [push, pull_request]
|
|
124
|
+
|
|
125
|
+
jobs:
|
|
126
|
+
mandate:
|
|
127
|
+
runs-on: ubuntu-latest
|
|
128
|
+
steps:
|
|
129
|
+
- uses: actions/checkout@v5
|
|
130
|
+
- uses: Metamynd/agentsafe-guard/packages/agentsafe-guard@v0.6.2
|
|
131
|
+
with:
|
|
132
|
+
config: ./agent.metamynd.json
|
|
133
|
+
require: merchants,perTxn
|
|
134
|
+
env:
|
|
135
|
+
AGENT_KEY: ${{ secrets.AGENT_KEY }}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Pinned to a released tag, the same way you'd pin any third-party action — not `@main`,
|
|
139
|
+
which moves under you every time this repository resyncs. Inputs: `config` (default
|
|
140
|
+
`./agent.metamynd.json`), `require`, `version` (the `@metamynd/agentsafe-guard` npm range
|
|
141
|
+
to run, default `latest`), `working-directory`. Output: `ok` (`"true"`/`"false"`), if a
|
|
142
|
+
later step needs to branch on the result.
|
|
143
|
+
|
|
101
144
|
## Install
|
|
102
145
|
|
|
103
146
|
```bash
|
package/agentsafe-guard.mjs
CHANGED
|
@@ -226,7 +226,7 @@ export function createGuard(opts = {}) {
|
|
|
226
226
|
const reasonCode = contained.status === 'quarantined' ? 'AGENT_QUARANTINED' : 'AGENT_SUSPENDED';
|
|
227
227
|
return { decision, reasonCode, authorizationId: null, remaining: null, proofRef: null };
|
|
228
228
|
}
|
|
229
|
-
const { action, amount = 0, merchant = '', context = {}, cumulativeSpend = amount, now } = request;
|
|
229
|
+
const { action, amount = 0, currency = 'USD', merchant = '', context = {}, cumulativeSpend = amount, now } = request;
|
|
230
230
|
// Operating-mode autonomy ladder (Phase 2.5b): the trust-driven posture rides as a
|
|
231
231
|
// SIBLING (like `contained`) and biases the edge verdict identically to the gate.
|
|
232
232
|
// READ_ONLY denies a value-bearing action up-front; SUPERVISED/RESTRICTED only
|
|
@@ -251,6 +251,12 @@ export function createGuard(opts = {}) {
|
|
|
251
251
|
'mm:payAmount': amount,
|
|
252
252
|
'mm:cumulativeSpend': cumulativeSpend,
|
|
253
253
|
'mm:merchant': merchant,
|
|
254
|
+
// A payAmount/cumulativeSpend constraint issued with a `unit` (currency) is
|
|
255
|
+
// only satisfied in that currency (see mandate-eval.ts's constraintSatisfied)
|
|
256
|
+
// — omitting this here would make EVERY unit-bearing cap fail regardless of
|
|
257
|
+
// amount, since undefined never equals a real unit. Defaults to 'USD' to match
|
|
258
|
+
// the same default this file already uses for authorize()/buildSignedRequest().
|
|
259
|
+
'mm:currency': currency,
|
|
254
260
|
}),
|
|
255
261
|
}
|
|
256
262
|
: undefined,
|
|
@@ -417,7 +423,7 @@ export function createGuard(opts = {}) {
|
|
|
417
423
|
*
|
|
418
424
|
* @param {string} action
|
|
419
425
|
* @param {(args:any, decision:any)=>any} handler
|
|
420
|
-
* @param {(args:any)=>{amount?:number,merchant?:string,context?:object}} mapArgs
|
|
426
|
+
* @param {(args:any)=>{amount?:number,currency?:string,merchant?:string,context?:object}} mapArgs
|
|
421
427
|
* @param {object|((args:any)=>object|Promise<object>)} getBundle { standards, sops, mandate } (or a resolver)
|
|
422
428
|
*/
|
|
423
429
|
function guardToolLocal(action, handler, mapArgs = (a) => a, getBundle = {}, toolOpts = {}) {
|
|
@@ -425,9 +431,9 @@ export function createGuard(opts = {}) {
|
|
|
425
431
|
return async (args) => {
|
|
426
432
|
let decision;
|
|
427
433
|
try {
|
|
428
|
-
const { amount, merchant, context } = mapArgs(args);
|
|
434
|
+
const { amount, currency, merchant, context } = mapArgs(args);
|
|
429
435
|
const bundle = typeof getBundle === 'function' ? await getBundle(args) : getBundle;
|
|
430
|
-
decision = evaluateLocally({ ...bundle, request: { action, amount, merchant, context } });
|
|
436
|
+
decision = evaluateLocally({ ...bundle, request: { action, amount, currency, merchant, context } });
|
|
431
437
|
} catch (err) {
|
|
432
438
|
decision = { decision: 'block', reasonCode: 'LOCAL_EVAL_ERROR', error: String(err?.message ?? err) };
|
|
433
439
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metamynd/agentsafe-guard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.3",
|
|
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",
|
package/policy-core.mjs
CHANGED
|
@@ -11,6 +11,24 @@ var ATOM_REGISTRY = {
|
|
|
11
11
|
return have !== void 0 && need !== void 0 && have >= need;
|
|
12
12
|
},
|
|
13
13
|
"amount-over": (c, cfg) => typeof c.amount === "number" && c.amount > Number(cfg?.limit ?? 0),
|
|
14
|
+
// Deny-by-default primitive for value-moving actions. Fires on ABSENCE (like the
|
|
15
|
+
// evidence atoms below, and unlike `amount-over`) OR on a NEGATIVE amount: true when
|
|
16
|
+
// the context carries no usable amount, or one that cannot be trusted for capping —
|
|
17
|
+
// the gate cannot tell how much value the call would move, so a spend cap authored
|
|
18
|
+
// next to it would silently never fire. `amount-over` only ever fires on `> limit`,
|
|
19
|
+
// so a negative amount clears every positive cap by construction, and on a system
|
|
20
|
+
// that tracks committed spend ADDITIVELY (reserved += amount), a negative claim can
|
|
21
|
+
// net-reduce what's already committed rather than add to it — the same "cap never
|
|
22
|
+
// fires" failure as a missing amount, reached from the other side of zero. Zero
|
|
23
|
+
// itself is NOT covered here: a genuine $0 action (a read, a no-op) is a valid,
|
|
24
|
+
// known amount, not an unknown one. Author this with BLOCK as the FIRST rule of a
|
|
25
|
+
// spend policy; the cap that follows then only ever judges a known, non-negative
|
|
26
|
+
// number. Opt-in: only a rule that keys it runs it, so actions that carry no amount
|
|
27
|
+
// by nature are unaffected. The public authorize endpoint's own schema already
|
|
28
|
+
// rejects a negative amount before it reaches this atom (defense in depth, not the
|
|
29
|
+
// only layer) — this is what closes the same gap for paths that schema doesn't
|
|
30
|
+
// cover: the local/harness evaluator and the platform's own MCP tool policies.
|
|
31
|
+
"amount-unknown": (c) => !(typeof c.amount === "number" && Number.isFinite(c.amount) && c.amount >= 0),
|
|
14
32
|
// Total budget: cumulativeSpend is a SERVER-derived, signed-last context field (never
|
|
15
33
|
// shadowable by the agent's itinerary), so this compares already-spent + this amount.
|
|
16
34
|
"cumulative-over": (c, cfg) => Number(c.cumulativeSpend ?? 0) + Number(c.amount ?? 0) > Number(cfg?.limit ?? 0),
|
|
@@ -58,8 +76,9 @@ ${c.output ?? ""}`.toLowerCase();
|
|
|
58
76
|
};
|
|
59
77
|
function notInAllowList(value, allowList) {
|
|
60
78
|
const v = value != null ? String(value).toLowerCase().trim() : "";
|
|
79
|
+
if (v === "") return false;
|
|
61
80
|
const allowed = (Array.isArray(allowList) ? allowList : []).map((x) => String(x).toLowerCase().trim());
|
|
62
|
-
return
|
|
81
|
+
return !allowed.includes(v);
|
|
63
82
|
}
|
|
64
83
|
|
|
65
84
|
// src/policy-core/atom-catalog.ts
|
|
@@ -71,6 +90,13 @@ var ATOM_SPECS = [
|
|
|
71
90
|
config: [{ key: "limit", type: "number", required: true, description: "Maximum allowed amount for one transaction" }],
|
|
72
91
|
requiredContext: ["amount"]
|
|
73
92
|
},
|
|
93
|
+
{
|
|
94
|
+
predicate: "amount-unknown",
|
|
95
|
+
label: "Amount not determinable",
|
|
96
|
+
description: "Fires when the action carries no usable amount, or a NEGATIVE one \u2014 the gate cannot trust either for capping. A deny-by-default control for value-moving actions: author it with BLOCK ahead of a spend cap, otherwise an amount that is missing, unparseable, or negative passes the cap untested (amount-over only ever fires above the limit, so a negative amount clears every positive cap). A genuine $0 amount does NOT fire this \u2014 only attach it to actions that must always carry a real, non-negative amount.",
|
|
97
|
+
config: [],
|
|
98
|
+
requiredContext: ["amount"]
|
|
99
|
+
},
|
|
74
100
|
{
|
|
75
101
|
predicate: "cumulative-over",
|
|
76
102
|
label: "Total budget over limit",
|
|
@@ -191,7 +217,7 @@ function requiredContextFor(predicates) {
|
|
|
191
217
|
}
|
|
192
218
|
|
|
193
219
|
// src/policy-core/standards-rules.ts
|
|
194
|
-
var PRECEDENCE = { allow: 0, observe: 1, escalate: 2, block: 3, suspend: 4, quarantine: 5 };
|
|
220
|
+
var PRECEDENCE = { allow: 0, observe: 1, escalate: 2, block: 3, suspend: 4, quarantine: 5, decommission: 6 };
|
|
195
221
|
function atomFires(atom, ctx) {
|
|
196
222
|
const pred = ATOM_REGISTRY[atom.predicate];
|
|
197
223
|
if (!pred) return false;
|
|
@@ -329,7 +355,9 @@ function constraintSatisfied(c, req) {
|
|
|
329
355
|
const op = OPERATORS[c.operator];
|
|
330
356
|
if (!op) return false;
|
|
331
357
|
const left = Object.prototype.hasOwnProperty.call(req.values, c.leftOperand) ? req.values[c.leftOperand] : void 0;
|
|
332
|
-
|
|
358
|
+
if (!op(left, c.rightOperand)) return false;
|
|
359
|
+
if (c.unit && req.values["mm:currency"] !== c.unit) return false;
|
|
360
|
+
return true;
|
|
333
361
|
}
|
|
334
362
|
function targetOf(rule, mandate) {
|
|
335
363
|
return rule.target ?? mandate.target;
|
|
@@ -399,7 +427,7 @@ function sumEventField(events, type, field) {
|
|
|
399
427
|
}
|
|
400
428
|
|
|
401
429
|
// src/policy-core/evaluate.ts
|
|
402
|
-
var PRECEDENCE2 = { allow: 0, observe: 1, escalate: 2, block: 3, suspend: 4, quarantine: 5 };
|
|
430
|
+
var PRECEDENCE2 = { allow: 0, observe: 1, escalate: 2, block: 3, suspend: 4, quarantine: 5, decommission: 6 };
|
|
403
431
|
function evaluate(input) {
|
|
404
432
|
let decision = "allow";
|
|
405
433
|
let reasonCode = "AUTHORIZED";
|
|
@@ -421,8 +449,11 @@ function evaluate(input) {
|
|
|
421
449
|
}
|
|
422
450
|
|
|
423
451
|
// src/policy-core/canonical.ts
|
|
452
|
+
function escapeField(v) {
|
|
453
|
+
return v.replace(/\\/g, "\\\\").replace(/\|/g, "\\|");
|
|
454
|
+
}
|
|
424
455
|
function buildAuthMessage(f) {
|
|
425
|
-
return
|
|
456
|
+
return [f.agentDid, f.action, f.amount, f.currency, f.merchant ?? "", f.nonce, f.issuedAt].map((v) => escapeField(String(v))).join("|");
|
|
426
457
|
}
|
|
427
458
|
|
|
428
459
|
// src/policy-core/context.ts
|