@metamynd/agentsafe-guard 0.2.0 → 0.3.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/LICENSE +21 -21
- package/README.md +303 -279
- package/agentsafe-guard.mjs +606 -480
- package/example-openclaw-agent.mjs +47 -47
- package/magp-did.mjs +157 -157
- package/package.json +59 -59
- package/policy-core.mjs +83 -4
- package/x402.mjs +43 -43
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 MetaMynd
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MetaMynd
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,279 +1,303 @@
|
|
|
1
|
-
# AgentSafe Guard — runtime governance for OpenClaw (and any Node agent)
|
|
2
|
-
|
|
3
|
-
Drop-in middleware that gates an agent's actions through the AgentSafe **authorize** endpoint
|
|
4
|
-
before they run. The gate deterministically returns `allow` / `block` / `escalate` after checking
|
|
5
|
-
the agent's **mandate**, its enforced **Standards**, and its assigned **SOPs** — the same rules a
|
|
6
|
-
compliance team edits in the dashboard, changeable live with no redeploy.
|
|
7
|
-
|
|
8
|
-
- **Zero dependencies.** Uses Node's built-in Ed25519 (`node:crypto`) + `fetch` (Node 18+), plus
|
|
9
|
-
`policy-core.mjs` — the deterministic evaluator, itself dependency-free, generated from
|
|
10
|
-
`backend/src/policy-core` (regenerate with `npm run build:guard-core`).
|
|
11
|
-
- **Deterministic + tamper-resistant.** Every call is Ed25519-signed over a canonical message with a
|
|
12
|
-
single-use nonce; the gate is server-authoritative and cannot be bypassed. No LLM at the gate.
|
|
13
|
-
- **Fail-closed.** A network/gate failure returns `block`, so the agent never proceeds blind.
|
|
14
|
-
- **Local evaluation.** The guard can also evaluate a signed policy bundle **locally** with the same
|
|
15
|
-
`policy-core` the gate runs (MAGP §9.2 cooperative mode) — identical inputs give the identical
|
|
16
|
-
verdict, with no network round-trip. See §4.
|
|
17
|
-
|
|
18
|
-
## Install
|
|
19
|
-
|
|
20
|
-
```bash
|
|
21
|
-
npm i @metamynd/agentsafe-guard
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
Requires Node ≥ 18 (built-in `fetch` + Ed25519). The package has **no dependencies**.
|
|
25
|
-
|
|
26
|
-
### Fastest start — scaffold a governed agent in one command
|
|
27
|
-
|
|
28
|
-
If you don't have an agent config yet, let the scaffolder log you in, provision the agent
|
|
29
|
-
(identity + mandate + starter SOP + Standards in one call), write `agent.metamynd.json`, and drop a
|
|
30
|
-
runnable example:
|
|
31
|
-
|
|
32
|
-
```bash
|
|
33
|
-
npm create metamynd-agent@latest # or: npx create-metamynd-agent
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
Then:
|
|
37
|
-
|
|
38
|
-
```js
|
|
39
|
-
import { createGuardFromConfig } from '@metamynd/agentsafe-guard';
|
|
40
|
-
const guard = await createGuardFromConfig('./agent.metamynd.json'); // no env vars
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
The rest of this guide shows the manual path (seed → wire) and the advanced features
|
|
44
|
-
(local eval, handshake, escalation, payments).
|
|
45
|
-
|
|
46
|
-
### Enforcement mode: local-first (default) or remote
|
|
47
|
-
|
|
48
|
-
Since v0.2.0 the guard decides **locally by default**. `guardTool` (and the mode-aware
|
|
49
|
-
`guard.check(...)`) evaluate the rule layer against the agent's cached signed policy
|
|
50
|
-
bundle using the **same `policy-core` bytes the gate runs** — so a **block or escalate
|
|
51
|
-
is decided with no network** (instant, works offline). An **allowed value action**
|
|
52
|
-
(`amount > 0`) is still sealed by the remote gate, because the cumulative-spend cap,
|
|
53
|
-
nonce/replay + atomic cap, and anchored evidence **must** be server-side. If the bundle
|
|
54
|
-
can't be fetched, the guard defers to the authoritative remote gate rather than
|
|
55
|
-
blind-allowing; a value action it can neither evaluate nor seal **fails closed**.
|
|
56
|
-
|
|
57
|
-
```js
|
|
58
|
-
// default — local-first
|
|
59
|
-
const guard = await createGuardFromConfig('./agent.metamynd.json');
|
|
60
|
-
// opt out — every call hits the gate
|
|
61
|
-
const remote = await createGuardFromConfig('./agent.metamynd.json', { mode: 'remote' });
|
|
62
|
-
// pure offline (no remote seal; drops cumulative-cap + evidence — you accept the trade)
|
|
63
|
-
const offline = await createGuardFromConfig('./agent.metamynd.json', { mode: 'local', sealValueActions: false });
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
`guard.authorize(...)` is always the explicit **remote** call (unchanged);
|
|
67
|
-
`guard.authorizeLocal(...)` is the explicit local-first call; `guard.check(...)` follows
|
|
68
|
-
the configured `mode`. All return `{ decision, reasonCode, authorizationId, … }`.
|
|
69
|
-
|
|
70
|
-
### Trustless currency check (`verifyOnChain`)
|
|
71
|
-
|
|
72
|
-
Local eval trusts a bundle fetched over TLS. With `{ verifyOnChain: true }` the guard
|
|
73
|
-
additionally confirms — from a **public Hedera mirror node, with MetaMynd offline** —
|
|
74
|
-
that its local bundle is the **latest one anchored on the agent's own topic**. On each
|
|
75
|
-
recompile MetaMynd publishes `sha256(bundle signature)` to the topic; the guard reads
|
|
76
|
-
the latest `policy-update` op and requires `sha256(bundle.proof.signature)` to match it.
|
|
77
|
-
If it can't confirm (mismatch / not yet anchored / mirror down), it **defers to the
|
|
78
|
-
authoritative remote gate** rather than evaluate a bundle it can't prove is current.
|
|
79
|
-
Append-only Hedera consensus makes the latest op authoritative and a **rollback**
|
|
80
|
-
(serving an older signed bundle) detectable; a monotonic sequence number defeats a mirror
|
|
81
|
-
that hides recent updates.
|
|
82
|
-
|
|
83
|
-
```js
|
|
84
|
-
const guard = await createGuardFromConfig('./agent.metamynd.json', { verifyOnChain: true });
|
|
85
|
-
await guard.policyAnchor(); // → { sigDigest, seq } read from Hedera (or null)
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
### Push invalidation (`watchPolicy`)
|
|
89
|
-
|
|
90
|
-
By default a rule change is picked up within the bundle's `maxStaleness` (or on the
|
|
91
|
-
next on-chain check). `guard.watchPolicy()` subscribes to a Server-Sent-Events stream
|
|
92
|
-
(`GET /policy/events/:did`, zero-dep — plain `fetch`) so a change **invalidates the
|
|
93
|
-
guard's cache in ~1s** and the next call re-fetches (and re-verifies) the new rules.
|
|
94
|
-
It auto-reconnects; a dropped stream still leaves staleness + the on-chain check as the
|
|
95
|
-
floor, so a missed push degrades latency, never safety.
|
|
96
|
-
|
|
97
|
-
```js
|
|
98
|
-
const stop = guard.watchPolicy((change) => console.log('rules changed', change));
|
|
99
|
-
// … later: stop.close();
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
### Bring your own key (BYOK)
|
|
103
|
-
|
|
104
|
-
Provision the agent with your **own** public key so MetaMynd never sees the private key. The identity
|
|
105
|
-
is issued unverified with a one-time `challenge`; the gate blocks it (`AGENT_KEY_UNVERIFIED`) until you
|
|
106
|
-
prove control. The guard signs the challenge with your key and submits it:
|
|
107
|
-
|
|
108
|
-
```js
|
|
109
|
-
const guard = await createGuardFromConfig('./agent.metamynd.json', { agentKey: myPrivateKey });
|
|
110
|
-
await guard.verifyKey({ ref: config.identityId, challenge: config.challenge, token: ownerToken }); // one-time
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
`guard.signChallenge(challenge)` returns just the hex signature if you'd rather submit verify-key
|
|
114
|
-
yourself. (Fastest path: `npm create metamynd-agent@latest -- --byok` does all of this for you.)
|
|
115
|
-
|
|
116
|
-
## 1. Seed a bound agent (once)
|
|
117
|
-
|
|
118
|
-
Run the seed against a running stack — it prints the agent's `DID` and `KEY`:
|
|
119
|
-
|
|
120
|
-
```powershell
|
|
121
|
-
cd backend
|
|
122
|
-
$env:API_BASE="http://localhost:9926/api/v1" # or https://metamynd.ai/api/v1
|
|
123
|
-
node_modules\.bin\tsx scripts\demo-seed-governance.ts
|
|
124
|
-
# → AGENT_DID = did:hedera:testnet:...
|
|
125
|
-
# → AGENT_KEY = 302e0201...
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
The agent is now bound to a mandate (`flight-purchase`), an **enforced Standard** (EU AI Act) and an
|
|
129
|
-
**active SOP** (spend cap + approved tools). Manage/toggle these from the dashboard:
|
|
130
|
-
Super Admin → Standards, Legal Entity → SOPs.
|
|
131
|
-
|
|
132
|
-
## 2. Try the example
|
|
133
|
-
|
|
134
|
-
```powershell
|
|
135
|
-
cd integrations\agentsafe-guard
|
|
136
|
-
$env:AGENTSAFE_API="http://localhost:9926/api/v1"
|
|
137
|
-
$env:AGENT_DID="did:hedera:testnet:..."
|
|
138
|
-
$env:AGENT_KEY="302e0201..."
|
|
139
|
-
node example-openclaw-agent.mjs
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
```
|
|
143
|
-
✅ ALLOW $150 book-flight, low risk → booked PNR-DEMO (remaining $200)
|
|
144
|
-
⛔ BLOCK $600 book-flight → SOP_SPEND_CAP
|
|
145
|
-
⛔ BLOCK $100 wire-transfer tool → SOP_TOOL_BLOCKED
|
|
146
|
-
⚠ ESCALATE $100 high-risk decision → RISK_REVIEW
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
Now flip the SOP's **active → inactive** toggle in the UI (or edit a rule) and re-run — the decision
|
|
150
|
-
changes in real time.
|
|
151
|
-
|
|
152
|
-
## 3. Wire it into your OpenClaw agent
|
|
153
|
-
|
|
154
|
-
Wrap each governed tool's handler with `guardTool(...)`. The wrapped handler only runs when the gate
|
|
155
|
-
allows; otherwise it throws a `GovernanceBlocked` error your agent surfaces to the user.
|
|
156
|
-
|
|
157
|
-
```js
|
|
158
|
-
import { createGuard, createGuardFromConfig } from '@metamynd/agentsafe-guard';
|
|
159
|
-
|
|
160
|
-
// Preferred: load the portable config the one-call onboarding endpoint returns (no env vars).
|
|
161
|
-
const guard = await createGuardFromConfig('./agent.metamynd.json');
|
|
162
|
-
|
|
163
|
-
// Or configure explicitly:
|
|
164
|
-
const guardExplicit = createGuard({
|
|
165
|
-
api: process.env.AGENTSAFE_API,
|
|
166
|
-
agentDid: process.env.AGENT_DID,
|
|
167
|
-
agentKey: process.env.AGENT_KEY, // held only by the agent
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
// Your existing OpenClaw tool handler:
|
|
171
|
-
async function bookFlight(args) { /* …call the airline… */ return { pnr: 'ABC123' }; }
|
|
172
|
-
|
|
173
|
-
// Register the GATED version with OpenClaw instead of the raw handler:
|
|
174
|
-
const gatedBookFlight = guard.guardTool(
|
|
175
|
-
'flight-purchase', // the governed action (matches the mandate scope)
|
|
176
|
-
bookFlight,
|
|
177
|
-
(a) => ({ // map tool args → gate inputs
|
|
178
|
-
amount: a.amount,
|
|
179
|
-
currency: 'USD',
|
|
180
|
-
merchant: a.merchant,
|
|
181
|
-
context: { tool: 'book-flight', jurisdiction: a.jurisdiction, riskLevel: a.riskLevel },
|
|
182
|
-
}),
|
|
183
|
-
);
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
- If your OpenClaw build has a **pre-tool hook / middleware** instead of raw handlers, call
|
|
187
|
-
`await guard.authorize({ action, amount, merchant, context })` there and refuse on any non-`allow`.
|
|
188
|
-
- **`context`** is what the Standard/SOP atoms read (jurisdiction, model, tool, PII, risk, …). Each
|
|
189
|
-
atom declares what it needs — fetch the catalog at `GET /api/v1/standards/atoms` to see the exact
|
|
190
|
-
fields (`requiredContext`) for the rules your agent is bound to.
|
|
191
|
-
- For **payment** tools (x402, §7a): after `authorize` allows, the Service returns a 402 bound to
|
|
192
|
-
your `authorizationId`. Call `guard.preparePayment(requirements, authorizationId)` — it refuses an
|
|
193
|
-
unbound or mismatched 402 — pay via x402, then reconcile the hold with
|
|
194
|
-
`await guard.capture(authorizationId, amountCharged, bookingRef, settlementTxHash)`. An
|
|
195
|
-
uncaptured hold auto-voids at its expiry (`POST /policy/mandate/authorize/:id/void` to release early).
|
|
196
|
-
|
|
197
|
-
## 4. Evaluate locally (no network)
|
|
198
|
-
|
|
199
|
-
For low-latency, cooperative-mode governance the guard can evaluate a **policy bundle** locally with
|
|
200
|
-
the same deterministic `policy-core` the gate runs — no round-trip. Given the same rule packs,
|
|
201
|
-
mandate, and request, it returns the **identical** `allow` / `block` / `escalate` verdict.
|
|
202
|
-
|
|
203
|
-
```js
|
|
204
|
-
const verdict = guard.evaluateLocally({
|
|
205
|
-
standards: [{ standardKey: 'eu-ai-act', document: { molecules: [/* … */] } }],
|
|
206
|
-
sops: [{ standardKey: 'sop:travel', document: { molecules: [/* … */] } }],
|
|
207
|
-
mandate: { permission: [/* ODRL constraints … */] },
|
|
208
|
-
request: { action: 'flight-purchase', amount: 600, merchant: 'amadeus',
|
|
209
|
-
context: { riskLevel: 'low' } },
|
|
210
|
-
});
|
|
211
|
-
// → { decision: 'block', reasonCode: 'SOP_SPEND_CAP', authorizationId: null, remaining: null, proofRef: null }
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
Or wrap a tool to gate it against a local bundle (fails closed like `guardTool`):
|
|
215
|
-
|
|
216
|
-
```js
|
|
217
|
-
const gated = guard.guardToolLocal('flight-purchase', bookFlight, mapArgs, { standards, sops, mandate });
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
1
|
+
# AgentSafe Guard — runtime governance for OpenClaw (and any Node agent)
|
|
2
|
+
|
|
3
|
+
Drop-in middleware that gates an agent's actions through the AgentSafe **authorize** endpoint
|
|
4
|
+
before they run. The gate deterministically returns `allow` / `block` / `escalate` after checking
|
|
5
|
+
the agent's **mandate**, its enforced **Standards**, and its assigned **SOPs** — the same rules a
|
|
6
|
+
compliance team edits in the dashboard, changeable live with no redeploy.
|
|
7
|
+
|
|
8
|
+
- **Zero dependencies.** Uses Node's built-in Ed25519 (`node:crypto`) + `fetch` (Node 18+), plus
|
|
9
|
+
`policy-core.mjs` — the deterministic evaluator, itself dependency-free, generated from
|
|
10
|
+
`backend/src/policy-core` (regenerate with `npm run build:guard-core`).
|
|
11
|
+
- **Deterministic + tamper-resistant.** Every call is Ed25519-signed over a canonical message with a
|
|
12
|
+
single-use nonce; the gate is server-authoritative and cannot be bypassed. No LLM at the gate.
|
|
13
|
+
- **Fail-closed.** A network/gate failure returns `block`, so the agent never proceeds blind.
|
|
14
|
+
- **Local evaluation.** The guard can also evaluate a signed policy bundle **locally** with the same
|
|
15
|
+
`policy-core` the gate runs (MAGP §9.2 cooperative mode) — identical inputs give the identical
|
|
16
|
+
verdict, with no network round-trip. See §4.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm i @metamynd/agentsafe-guard
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Requires Node ≥ 18 (built-in `fetch` + Ed25519). The package has **no dependencies**.
|
|
25
|
+
|
|
26
|
+
### Fastest start — scaffold a governed agent in one command
|
|
27
|
+
|
|
28
|
+
If you don't have an agent config yet, let the scaffolder log you in, provision the agent
|
|
29
|
+
(identity + mandate + starter SOP + Standards in one call), write `agent.metamynd.json`, and drop a
|
|
30
|
+
runnable example:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm create metamynd-agent@latest # or: npx create-metamynd-agent
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Then:
|
|
37
|
+
|
|
38
|
+
```js
|
|
39
|
+
import { createGuardFromConfig } from '@metamynd/agentsafe-guard';
|
|
40
|
+
const guard = await createGuardFromConfig('./agent.metamynd.json'); // no env vars
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The rest of this guide shows the manual path (seed → wire) and the advanced features
|
|
44
|
+
(local eval, handshake, escalation, payments).
|
|
45
|
+
|
|
46
|
+
### Enforcement mode: local-first (default) or remote
|
|
47
|
+
|
|
48
|
+
Since v0.2.0 the guard decides **locally by default**. `guardTool` (and the mode-aware
|
|
49
|
+
`guard.check(...)`) evaluate the rule layer against the agent's cached signed policy
|
|
50
|
+
bundle using the **same `policy-core` bytes the gate runs** — so a **block or escalate
|
|
51
|
+
is decided with no network** (instant, works offline). An **allowed value action**
|
|
52
|
+
(`amount > 0`) is still sealed by the remote gate, because the cumulative-spend cap,
|
|
53
|
+
nonce/replay + atomic cap, and anchored evidence **must** be server-side. If the bundle
|
|
54
|
+
can't be fetched, the guard defers to the authoritative remote gate rather than
|
|
55
|
+
blind-allowing; a value action it can neither evaluate nor seal **fails closed**.
|
|
56
|
+
|
|
57
|
+
```js
|
|
58
|
+
// default — local-first
|
|
59
|
+
const guard = await createGuardFromConfig('./agent.metamynd.json');
|
|
60
|
+
// opt out — every call hits the gate
|
|
61
|
+
const remote = await createGuardFromConfig('./agent.metamynd.json', { mode: 'remote' });
|
|
62
|
+
// pure offline (no remote seal; drops cumulative-cap + evidence — you accept the trade)
|
|
63
|
+
const offline = await createGuardFromConfig('./agent.metamynd.json', { mode: 'local', sealValueActions: false });
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`guard.authorize(...)` is always the explicit **remote** call (unchanged);
|
|
67
|
+
`guard.authorizeLocal(...)` is the explicit local-first call; `guard.check(...)` follows
|
|
68
|
+
the configured `mode`. All return `{ decision, reasonCode, authorizationId, … }`.
|
|
69
|
+
|
|
70
|
+
### Trustless currency check (`verifyOnChain`)
|
|
71
|
+
|
|
72
|
+
Local eval trusts a bundle fetched over TLS. With `{ verifyOnChain: true }` the guard
|
|
73
|
+
additionally confirms — from a **public Hedera mirror node, with MetaMynd offline** —
|
|
74
|
+
that its local bundle is the **latest one anchored on the agent's own topic**. On each
|
|
75
|
+
recompile MetaMynd publishes `sha256(bundle signature)` to the topic; the guard reads
|
|
76
|
+
the latest `policy-update` op and requires `sha256(bundle.proof.signature)` to match it.
|
|
77
|
+
If it can't confirm (mismatch / not yet anchored / mirror down), it **defers to the
|
|
78
|
+
authoritative remote gate** rather than evaluate a bundle it can't prove is current.
|
|
79
|
+
Append-only Hedera consensus makes the latest op authoritative and a **rollback**
|
|
80
|
+
(serving an older signed bundle) detectable; a monotonic sequence number defeats a mirror
|
|
81
|
+
that hides recent updates.
|
|
82
|
+
|
|
83
|
+
```js
|
|
84
|
+
const guard = await createGuardFromConfig('./agent.metamynd.json', { verifyOnChain: true });
|
|
85
|
+
await guard.policyAnchor(); // → { sigDigest, seq } read from Hedera (or null)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Push invalidation (`watchPolicy`)
|
|
89
|
+
|
|
90
|
+
By default a rule change is picked up within the bundle's `maxStaleness` (or on the
|
|
91
|
+
next on-chain check). `guard.watchPolicy()` subscribes to a Server-Sent-Events stream
|
|
92
|
+
(`GET /policy/events/:did`, zero-dep — plain `fetch`) so a change **invalidates the
|
|
93
|
+
guard's cache in ~1s** and the next call re-fetches (and re-verifies) the new rules.
|
|
94
|
+
It auto-reconnects; a dropped stream still leaves staleness + the on-chain check as the
|
|
95
|
+
floor, so a missed push degrades latency, never safety.
|
|
96
|
+
|
|
97
|
+
```js
|
|
98
|
+
const stop = guard.watchPolicy((change) => console.log('rules changed', change));
|
|
99
|
+
// … later: stop.close();
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Bring your own key (BYOK)
|
|
103
|
+
|
|
104
|
+
Provision the agent with your **own** public key so MetaMynd never sees the private key. The identity
|
|
105
|
+
is issued unverified with a one-time `challenge`; the gate blocks it (`AGENT_KEY_UNVERIFIED`) until you
|
|
106
|
+
prove control. The guard signs the challenge with your key and submits it:
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
const guard = await createGuardFromConfig('./agent.metamynd.json', { agentKey: myPrivateKey });
|
|
110
|
+
await guard.verifyKey({ ref: config.identityId, challenge: config.challenge, token: ownerToken }); // one-time
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`guard.signChallenge(challenge)` returns just the hex signature if you'd rather submit verify-key
|
|
114
|
+
yourself. (Fastest path: `npm create metamynd-agent@latest -- --byok` does all of this for you.)
|
|
115
|
+
|
|
116
|
+
## 1. Seed a bound agent (once)
|
|
117
|
+
|
|
118
|
+
Run the seed against a running stack — it prints the agent's `DID` and `KEY`:
|
|
119
|
+
|
|
120
|
+
```powershell
|
|
121
|
+
cd backend
|
|
122
|
+
$env:API_BASE="http://localhost:9926/api/v1" # or https://metamynd.ai/api/v1
|
|
123
|
+
node_modules\.bin\tsx scripts\demo-seed-governance.ts
|
|
124
|
+
# → AGENT_DID = did:hedera:testnet:...
|
|
125
|
+
# → AGENT_KEY = 302e0201...
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The agent is now bound to a mandate (`flight-purchase`), an **enforced Standard** (EU AI Act) and an
|
|
129
|
+
**active SOP** (spend cap + approved tools). Manage/toggle these from the dashboard:
|
|
130
|
+
Super Admin → Standards, Legal Entity → SOPs.
|
|
131
|
+
|
|
132
|
+
## 2. Try the example
|
|
133
|
+
|
|
134
|
+
```powershell
|
|
135
|
+
cd integrations\agentsafe-guard
|
|
136
|
+
$env:AGENTSAFE_API="http://localhost:9926/api/v1"
|
|
137
|
+
$env:AGENT_DID="did:hedera:testnet:..."
|
|
138
|
+
$env:AGENT_KEY="302e0201..."
|
|
139
|
+
node example-openclaw-agent.mjs
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
✅ ALLOW $150 book-flight, low risk → booked PNR-DEMO (remaining $200)
|
|
144
|
+
⛔ BLOCK $600 book-flight → SOP_SPEND_CAP
|
|
145
|
+
⛔ BLOCK $100 wire-transfer tool → SOP_TOOL_BLOCKED
|
|
146
|
+
⚠ ESCALATE $100 high-risk decision → RISK_REVIEW
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Now flip the SOP's **active → inactive** toggle in the UI (or edit a rule) and re-run — the decision
|
|
150
|
+
changes in real time.
|
|
151
|
+
|
|
152
|
+
## 3. Wire it into your OpenClaw agent
|
|
153
|
+
|
|
154
|
+
Wrap each governed tool's handler with `guardTool(...)`. The wrapped handler only runs when the gate
|
|
155
|
+
allows; otherwise it throws a `GovernanceBlocked` error your agent surfaces to the user.
|
|
156
|
+
|
|
157
|
+
```js
|
|
158
|
+
import { createGuard, createGuardFromConfig } from '@metamynd/agentsafe-guard';
|
|
159
|
+
|
|
160
|
+
// Preferred: load the portable config the one-call onboarding endpoint returns (no env vars).
|
|
161
|
+
const guard = await createGuardFromConfig('./agent.metamynd.json');
|
|
162
|
+
|
|
163
|
+
// Or configure explicitly:
|
|
164
|
+
const guardExplicit = createGuard({
|
|
165
|
+
api: process.env.AGENTSAFE_API,
|
|
166
|
+
agentDid: process.env.AGENT_DID,
|
|
167
|
+
agentKey: process.env.AGENT_KEY, // held only by the agent
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Your existing OpenClaw tool handler:
|
|
171
|
+
async function bookFlight(args) { /* …call the airline… */ return { pnr: 'ABC123' }; }
|
|
172
|
+
|
|
173
|
+
// Register the GATED version with OpenClaw instead of the raw handler:
|
|
174
|
+
const gatedBookFlight = guard.guardTool(
|
|
175
|
+
'flight-purchase', // the governed action (matches the mandate scope)
|
|
176
|
+
bookFlight,
|
|
177
|
+
(a) => ({ // map tool args → gate inputs
|
|
178
|
+
amount: a.amount,
|
|
179
|
+
currency: 'USD',
|
|
180
|
+
merchant: a.merchant,
|
|
181
|
+
context: { tool: 'book-flight', jurisdiction: a.jurisdiction, riskLevel: a.riskLevel },
|
|
182
|
+
}),
|
|
183
|
+
);
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
- If your OpenClaw build has a **pre-tool hook / middleware** instead of raw handlers, call
|
|
187
|
+
`await guard.authorize({ action, amount, merchant, context })` there and refuse on any non-`allow`.
|
|
188
|
+
- **`context`** is what the Standard/SOP atoms read (jurisdiction, model, tool, PII, risk, …). Each
|
|
189
|
+
atom declares what it needs — fetch the catalog at `GET /api/v1/standards/atoms` to see the exact
|
|
190
|
+
fields (`requiredContext`) for the rules your agent is bound to.
|
|
191
|
+
- For **payment** tools (x402, §7a): after `authorize` allows, the Service returns a 402 bound to
|
|
192
|
+
your `authorizationId`. Call `guard.preparePayment(requirements, authorizationId)` — it refuses an
|
|
193
|
+
unbound or mismatched 402 — pay via x402, then reconcile the hold with
|
|
194
|
+
`await guard.capture(authorizationId, amountCharged, bookingRef, settlementTxHash)`. An
|
|
195
|
+
uncaptured hold auto-voids at its expiry (`POST /policy/mandate/authorize/:id/void` to release early).
|
|
196
|
+
|
|
197
|
+
## 4. Evaluate locally (no network)
|
|
198
|
+
|
|
199
|
+
For low-latency, cooperative-mode governance the guard can evaluate a **policy bundle** locally with
|
|
200
|
+
the same deterministic `policy-core` the gate runs — no round-trip. Given the same rule packs,
|
|
201
|
+
mandate, and request, it returns the **identical** `allow` / `block` / `escalate` verdict.
|
|
202
|
+
|
|
203
|
+
```js
|
|
204
|
+
const verdict = guard.evaluateLocally({
|
|
205
|
+
standards: [{ standardKey: 'eu-ai-act', document: { molecules: [/* … */] } }],
|
|
206
|
+
sops: [{ standardKey: 'sop:travel', document: { molecules: [/* … */] } }],
|
|
207
|
+
mandate: { permission: [/* ODRL constraints … */] },
|
|
208
|
+
request: { action: 'flight-purchase', amount: 600, merchant: 'amadeus',
|
|
209
|
+
context: { riskLevel: 'low' } },
|
|
210
|
+
});
|
|
211
|
+
// → { decision: 'block', reasonCode: 'SOP_SPEND_CAP', authorizationId: null, remaining: null, proofRef: null }
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Or wrap a tool to gate it against a local bundle (fails closed like `guardTool`):
|
|
215
|
+
|
|
216
|
+
```js
|
|
217
|
+
const gated = guard.guardToolLocal('flight-purchase', bookFlight, mapArgs, { standards, sops, mandate });
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Execution adapters (dry-run / sandbox) — SAFR §19
|
|
221
|
+
|
|
222
|
+
By default a permitted (`allow`/`observe`) tool runs its real handler. An **ExecutionAdapter**
|
|
223
|
+
interposes between the verdict and the side-effect, so the *same* governed decision can be run
|
|
224
|
+
live, **simulated (dry-run)**, or routed to a sandbox — without changing the handler or the gate.
|
|
225
|
+
A blocked/escalated action still throws `GovernanceBlocked` before any adapter is consulted.
|
|
226
|
+
|
|
227
|
+
```js
|
|
228
|
+
import { dryRunExecutionAdapter } from '@metamynd/agentsafe-guard';
|
|
229
|
+
|
|
230
|
+
// Guard-wide (or set AGENTSAFE_EXECUTION_MODE=dry-run):
|
|
231
|
+
const guard = createGuard({ api, agentDid, agentKey, executionAdapter: dryRunExecutionAdapter });
|
|
232
|
+
|
|
233
|
+
// …or per tool (overrides the guard default):
|
|
234
|
+
const preview = guard.guardTool('flight-purchase', bookFlight, mapArgs, { executionAdapter: dryRunExecutionAdapter });
|
|
235
|
+
await preview({ amount: 100 }); // → { dryRun: true, action, decision, authorizationId, args } — bookFlight NEVER runs
|
|
236
|
+
|
|
237
|
+
// Custom adapter: run the real handler, or substitute it. `proceed()` invokes handler(args, decision).
|
|
238
|
+
const sandboxed = (ctx) => ctx.action === 'flight-purchase' ? sandboxBook(ctx.args) : ctx.proceed();
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Contract: `async ({ action, args, decision, proceed }) => result`. Call `proceed()` to execute for
|
|
242
|
+
real; return without it to substitute the side-effect. Self-check: `node execution-adapter.smoke.mjs`.
|
|
243
|
+
|
|
244
|
+
Signed request fields (`amount`, `merchant`) are always applied over the unsigned `context`, so a
|
|
245
|
+
forged context key can never shadow them (MAGP §6.4.2). Run the self-check:
|
|
246
|
+
|
|
247
|
+
```powershell
|
|
248
|
+
cd integrations\agentsafe-guard
|
|
249
|
+
node local-eval.smoke.mjs # PASS when every local verdict matches the gate
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Local evaluation is the cooperative-mode pre-check; the gate still owns the stateful parts
|
|
253
|
+
(single-use nonce, atomic spend-cap reservation, evidence anchoring), so value-bearing actions should
|
|
254
|
+
still settle through the gate / `capture` flow.
|
|
255
|
+
|
|
256
|
+
## 5. Mutual handshake with a Service (§8.2)
|
|
257
|
+
|
|
258
|
+
Before transacting with a Service (an MCP), the agent and Service prove control of their DIDs
|
|
259
|
+
to each other — no issuer calls, because the keys are embedded in the DIDs (§4.1.2). The agent
|
|
260
|
+
drives the initiator side:
|
|
261
|
+
|
|
262
|
+
```js
|
|
263
|
+
const hs = guard.handshake();
|
|
264
|
+
const { nonceA, message } = hs.hello(); // → send HELLO to the Service
|
|
265
|
+
// Service replies with CHALLENGE { toDid, nonceB, sigB(nonceA) }
|
|
266
|
+
const { sigA, handshakeId } = hs.prove({ nonceA, challenge }); // verifies the Service, → send PROVE
|
|
267
|
+
// Service replies READY { channelId }
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
`prove()` throws `HandshakeFailed` if the Service's CHALLENGE does not verify against the key in
|
|
271
|
+
its DID. The Service side uses [`agentsafe-mcp-guard`](../agentsafe-mcp-guard), which also
|
|
272
|
+
re-evaluates the agent's signed request trustlessly (§9.6). Discover a Service's endpoint and
|
|
273
|
+
confirm its key via the public resolver `GET /did/:did` (§4.4).
|
|
274
|
+
|
|
275
|
+
## 6. Escalation — human-in-the-loop (§9a)
|
|
276
|
+
|
|
277
|
+
An `escalate` verdict is **not a denial** — the action is *held* pending the Owner's approval. The
|
|
278
|
+
verdict carries an `escalationId`; no budget is reserved and (for payments) nothing settles until it
|
|
279
|
+
is approved. The Owner resolves it in the dashboard / via `POST /policy/escalations/:id/resolve`;
|
|
280
|
+
the agent polls the outcome:
|
|
281
|
+
|
|
282
|
+
```js
|
|
283
|
+
const d = await guard.authorize({ action: 'flight-purchase', amount: 5000, context: { riskLevel: 'high' } });
|
|
284
|
+
if (d.decision === 'escalate') {
|
|
285
|
+
// parked for review — d.escalationId, d.expiresAt
|
|
286
|
+
const outcome = await guard.escalationStatus(d.escalationId);
|
|
287
|
+
// → { status: 'approved' | 'denied' | 'expired' | 'pending', authorizationId, reasonCode }
|
|
288
|
+
// on 'approved', outcome.authorizationId carries into the §7a capture/pay flow.
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
On approval the budget/cap gate re-runs (§9a.3), so an approval still can't overspend; an
|
|
293
|
+
unresolved escalation lapses to denied after its TTL (§9a.4).
|
|
294
|
+
|
|
295
|
+
## Trust model
|
|
296
|
+
|
|
297
|
+
The gate is a **checkpoint** — enforcement is real when it's actually called.
|
|
298
|
+
- **Trustless:** the counterparty the agent transacts with calls the *public* authorize endpoint and
|
|
299
|
+
only proceeds on `allow`. A rogue agent that skips the call can't get the counterparty to act.
|
|
300
|
+
- **Cooperative:** the agent's own tool layer (this guard) calls the gate and refuses on block/escalate.
|
|
301
|
+
|
|
302
|
+
Either way, every decision is Ed25519-authenticated, deterministic, and anchored as evidence
|
|
303
|
+
(visible in the dashboard's Regulator log and on HashScan).
|