@metamynd/agentsafe-guard 0.6.4 → 0.7.0
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 +14 -1
- package/agentsafe-guard.mjs +28 -4
- package/governance-envelope.mjs +69 -0
- package/package.json +3 -2
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 64 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`.
|
|
@@ -99,6 +99,19 @@ so the identical "mismatch → not satisfied" rule let a prohibition like `payAm
|
|
|
99
99
|
unit USD` be silently skipped by declaring any other currency, including a mere case
|
|
100
100
|
difference (`'usd'` vs `'USD'`). The currency comparison is also now case-insensitive.
|
|
101
101
|
|
|
102
|
+
**0.6.5 — `IDENTITY_KEY_MISMATCH`, a new reason code (now 63 total).** The backend gate
|
|
103
|
+
now cross-checks a resolved agent's registered public key against the key embedded in its
|
|
104
|
+
own self-certifying DID (`did:hedera` / `did:key`) before trusting a signature against it —
|
|
105
|
+
free, local, no network round-trip, since the DID already carries its own proof. Nothing in
|
|
106
|
+
this package's own evaluation changes; documented here because the count in this README and
|
|
107
|
+
the linked spec moved.
|
|
108
|
+
|
|
109
|
+
**0.6.6 — `SPEND_PATTERN_ANOMALY`, a new reason code (now 64 total).** The backend gate can
|
|
110
|
+
now escalate an otherwise-permitted decision whose amount deviates sharply from an agent's
|
|
111
|
+
OWN recorded spend history — a baseline the platform's trust-graph engine learns, not a
|
|
112
|
+
threshold a human pre-sets. Opt-in (`SPEND_ANOMALY_MODE=on`), off by default. Nothing in this
|
|
113
|
+
package's own evaluation changes; documented here because the count moved again.
|
|
114
|
+
|
|
102
115
|
```yaml
|
|
103
116
|
# .github/workflows/governance.yml
|
|
104
117
|
name: Governance
|
package/agentsafe-guard.mjs
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import crypto from 'node:crypto';
|
|
12
12
|
import { readFileSync } from 'node:fs';
|
|
13
13
|
import { evaluate, buildAuthMessage, applySignedLast, operatingModeGate } from './policy-core.mjs';
|
|
14
|
+
import { envelopeHashFor } from './governance-envelope.mjs';
|
|
14
15
|
import { verifyDidSignature } from './magp-did.mjs';
|
|
15
16
|
import { checkSettlementBinding } from './x402.mjs';
|
|
16
17
|
|
|
@@ -120,6 +121,20 @@ export function createGuard(opts = {}) {
|
|
|
120
121
|
return crypto.sign(null, Buffer.from(message, 'utf8'), privateKey).toString('hex');
|
|
121
122
|
}
|
|
122
123
|
|
|
124
|
+
// Tier 1 context-claim binding (opt-in, docs/design/context-claim-binding.md): when
|
|
125
|
+
// on, sign the GovernanceEnvelope hash too, so a counterparty/gate can prove the
|
|
126
|
+
// agent's OWN key attested to the context it submitted — not just the signed action
|
|
127
|
+
// subset. Off by default: a bare request stays a valid degenerate envelope, exactly
|
|
128
|
+
// like today, and the wire body carries no envelopeSignature field at all.
|
|
129
|
+
const signContext = opts.signContext ?? cfg?.signContext ?? false;
|
|
130
|
+
function envelopeSignatureFor({ action, amount, currency, merchant, context, trace, materiality, nonce, issuedAt }) {
|
|
131
|
+
if (!signContext) return undefined;
|
|
132
|
+
// The hash is independent of `signature` (excluded from what it commits to — see
|
|
133
|
+
// governance-envelope.ts), so an empty placeholder here is exact, not approximate.
|
|
134
|
+
const hash = envelopeHashFor({ agentDid, action, amount, currency, merchant, itinerary: context, trace, materiality, nonce, issuedAt, signature: '' });
|
|
135
|
+
return sign(hash);
|
|
136
|
+
}
|
|
137
|
+
|
|
123
138
|
// --- Enforcement mode (spec §9.2 + local-first plan) --------------------------------------
|
|
124
139
|
// 'local' (DEFAULT): decide the rule layer LOCALLY against a cached signed bundle — a
|
|
125
140
|
// block/escalate needs no network; an allowed VALUE action is still sealed by the remote
|
|
@@ -158,7 +173,11 @@ export function createGuard(opts = {}) {
|
|
|
158
173
|
const message = buildAuthMessage({ agentDid, action, amount, currency, merchant, nonce, issuedAt });
|
|
159
174
|
// trace/materiality are GovernanceEnvelope fields (SAFR §5) — unsigned metadata; the
|
|
160
175
|
// signed message stays the action subset, so verification is unchanged.
|
|
161
|
-
return {
|
|
176
|
+
return {
|
|
177
|
+
agentDid, action, amount, currency, merchant, itinerary: context, trace, materiality, nonce, issuedAt,
|
|
178
|
+
signature: sign(message),
|
|
179
|
+
envelopeSignature: envelopeSignatureFor({ action, amount, currency, merchant, context, trace, materiality, nonce, issuedAt }),
|
|
180
|
+
};
|
|
162
181
|
}
|
|
163
182
|
|
|
164
183
|
/**
|
|
@@ -176,9 +195,14 @@ export function createGuard(opts = {}) {
|
|
|
176
195
|
const res = await fetch(`${base}/policy/mandate/authorize`, {
|
|
177
196
|
method: 'POST',
|
|
178
197
|
headers: { 'Content-Type': 'application/json' },
|
|
179
|
-
// trace/materiality (SAFR §5 envelope)
|
|
180
|
-
// drops them when undefined, so an
|
|
181
|
-
|
|
198
|
+
// trace/materiality (SAFR §5 envelope) and envelopeSignature (Tier 1, opt-in) ride
|
|
199
|
+
// as unsigned-message metadata; JSON.stringify drops them when undefined, so an
|
|
200
|
+
// agent that omits them (or leaves signContext off) sends the legacy body.
|
|
201
|
+
body: JSON.stringify({
|
|
202
|
+
agentDid, action, amount, currency, merchant, itinerary: context, trace, materiality, nonce, issuedAt,
|
|
203
|
+
signature: sign(message),
|
|
204
|
+
envelopeSignature: envelopeSignatureFor({ action, amount, currency, merchant, context, trace, materiality, nonce, issuedAt }),
|
|
205
|
+
}),
|
|
182
206
|
});
|
|
183
207
|
const body = await res.json().catch(() => null);
|
|
184
208
|
return body?.data ?? { decision: 'block', reasonCode: `GATE_HTTP_${res.status}` };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// GENERATED from backend/src/features/policy/mandate/governance-envelope.ts — do not edit. Regenerate: npm run build:guard-core
|
|
2
|
+
|
|
3
|
+
// src/features/policy/mandate/governance-envelope.ts
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
var ENVELOPE_VERSION = "1.0";
|
|
6
|
+
function stableStringify(value) {
|
|
7
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value ?? null);
|
|
8
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
9
|
+
const obj = value;
|
|
10
|
+
const keys = Object.keys(obj).filter((k) => obj[k] !== void 0).sort();
|
|
11
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(",")}}`;
|
|
12
|
+
}
|
|
13
|
+
function envelopeIntegrityHash(env) {
|
|
14
|
+
const { integrity: _omit, ...rest } = env;
|
|
15
|
+
return createHash("sha256").update(stableStringify(rest)).digest("hex");
|
|
16
|
+
}
|
|
17
|
+
function buildGovernanceEnvelope(input) {
|
|
18
|
+
const base = {
|
|
19
|
+
envelopeId: `env:${input.nonce}`,
|
|
20
|
+
version: ENVELOPE_VERSION,
|
|
21
|
+
createdAt: input.issuedAt,
|
|
22
|
+
agent: { did: input.agentDid },
|
|
23
|
+
action: {
|
|
24
|
+
actionId: `act:${input.nonce}`,
|
|
25
|
+
actionType: input.action,
|
|
26
|
+
amount: input.amount,
|
|
27
|
+
currency: input.currency,
|
|
28
|
+
merchant: input.merchant ?? null,
|
|
29
|
+
...input.materiality ? { materiality: input.materiality } : {}
|
|
30
|
+
},
|
|
31
|
+
...input.trace ? { trace: input.trace } : {},
|
|
32
|
+
...input.itinerary ? { context: input.itinerary } : {}
|
|
33
|
+
};
|
|
34
|
+
return {
|
|
35
|
+
...base,
|
|
36
|
+
integrity: {
|
|
37
|
+
payloadHash: envelopeIntegrityHash(base),
|
|
38
|
+
signature: input.signature,
|
|
39
|
+
signatureType: "Ed25519"
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function envelopeHashFor(input) {
|
|
44
|
+
return buildGovernanceEnvelope(input).integrity.payloadHash;
|
|
45
|
+
}
|
|
46
|
+
function authorizeInputFromEnvelope(env) {
|
|
47
|
+
const nonce = env.envelopeId.startsWith("env:") ? env.envelopeId.slice(4) : env.envelopeId;
|
|
48
|
+
return {
|
|
49
|
+
agentDid: env.agent.did,
|
|
50
|
+
action: env.action.actionType,
|
|
51
|
+
amount: env.action.amount ?? 0,
|
|
52
|
+
currency: env.action.currency ?? "",
|
|
53
|
+
merchant: env.action.merchant ?? void 0,
|
|
54
|
+
itinerary: env.context,
|
|
55
|
+
trace: env.trace,
|
|
56
|
+
materiality: env.action.materiality,
|
|
57
|
+
nonce,
|
|
58
|
+
issuedAt: env.createdAt,
|
|
59
|
+
signature: env.integrity.signature ?? ""
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export {
|
|
63
|
+
ENVELOPE_VERSION,
|
|
64
|
+
authorizeInputFromEnvelope,
|
|
65
|
+
buildGovernanceEnvelope,
|
|
66
|
+
envelopeHashFor,
|
|
67
|
+
envelopeIntegrityHash,
|
|
68
|
+
stableStringify
|
|
69
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metamynd/agentsafe-guard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
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",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"verify.mjs",
|
|
18
18
|
"agentsafe-guard.mjs",
|
|
19
19
|
"policy-core.mjs",
|
|
20
|
+
"governance-envelope.mjs",
|
|
20
21
|
"magp-did.mjs",
|
|
21
22
|
"x402.mjs",
|
|
22
23
|
"example-openclaw-agent.mjs",
|
|
@@ -24,7 +25,7 @@
|
|
|
24
25
|
"LICENSE"
|
|
25
26
|
],
|
|
26
27
|
"scripts": {
|
|
27
|
-
"test": "node demo.mjs && node local-eval.smoke.mjs && node execution-adapter.smoke.mjs && node verify.smoke.mjs",
|
|
28
|
+
"test": "node demo.mjs && node local-eval.smoke.mjs && node execution-adapter.smoke.mjs && node verify.smoke.mjs && node context-signature.smoke.mjs",
|
|
28
29
|
"demo": "node demo.mjs"
|
|
29
30
|
},
|
|
30
31
|
"engines": {
|