@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 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
- Signed request fields (`amount`, `merchant`) are always applied over the unsigned `context`, so a
221
- forged context key can never shadow them (MAGP §6.4.2). Run the self-check:
222
-
223
- ```powershell
224
- cd integrations\agentsafe-guard
225
- node local-eval.smoke.mjs # PASS when every local verdict matches the gate
226
- ```
227
-
228
- Local evaluation is the cooperative-mode pre-check; the gate still owns the stateful parts
229
- (single-use nonce, atomic spend-cap reservation, evidence anchoring), so value-bearing actions should
230
- still settle through the gate / `capture` flow.
231
-
232
- ## 5. Mutual handshake with a Service (§8.2)
233
-
234
- Before transacting with a Service (an MCP), the agent and Service prove control of their DIDs
235
- to each other no issuer calls, because the keys are embedded in the DIDs (§4.1.2). The agent
236
- drives the initiator side:
237
-
238
- ```js
239
- const hs = guard.handshake();
240
- const { nonceA, message } = hs.hello(); // → send HELLO to the Service
241
- // Service replies with CHALLENGE { toDid, nonceB, sigB(nonceA) }
242
- const { sigA, handshakeId } = hs.prove({ nonceA, challenge }); // verifies the Service, send PROVE
243
- // Service replies READY { channelId }
244
- ```
245
-
246
- `prove()` throws `HandshakeFailed` if the Service's CHALLENGE does not verify against the key in
247
- its DID. The Service side uses [`agentsafe-mcp-guard`](../agentsafe-mcp-guard), which also
248
- re-evaluates the agent's signed request trustlessly (§9.6). Discover a Service's endpoint and
249
- confirm its key via the public resolver `GET /did/:did` (§4.4).
250
-
251
- ## 6. Escalation — human-in-the-loop (§9a)
252
-
253
- An `escalate` verdict is **not a denial** the action is *held* pending the Owner's approval. The
254
- verdict carries an `escalationId`; no budget is reserved and (for payments) nothing settles until it
255
- is approved. The Owner resolves it in the dashboard / via `POST /policy/escalations/:id/resolve`;
256
- the agent polls the outcome:
257
-
258
- ```js
259
- const d = await guard.authorize({ action: 'flight-purchase', amount: 5000, context: { riskLevel: 'high' } });
260
- if (d.decision === 'escalate') {
261
- // parked for review — d.escalationId, d.expiresAt
262
- const outcome = await guard.escalationStatus(d.escalationId);
263
- // { status: 'approved' | 'denied' | 'expired' | 'pending', authorizationId, reasonCode }
264
- // on 'approved', outcome.authorizationId carries into the §7a capture/pay flow.
265
- }
266
- ```
267
-
268
- On approval the budget/cap gate re-runs (§9a.3), so an approval still can't overspend; an
269
- unresolved escalation lapses to denied after its TTL (§9a.4).
270
-
271
- ## Trust model
272
-
273
- The gate is a **checkpoint** enforcement is real when it's actually called.
274
- - **Trustless:** the counterparty the agent transacts with calls the *public* authorize endpoint and
275
- only proceeds on `allow`. A rogue agent that skips the call can't get the counterparty to act.
276
- - **Cooperative:** the agent's own tool layer (this guard) calls the gate and refuses on block/escalate.
277
-
278
- Either way, every decision is Ed25519-authenticated, deterministic, and anchored as evidence
279
- (visible in the dashboard's Regulator log and on HashScan).
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).