@metamynd/agentsafe-guard 0.1.0 → 0.1.2

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 CHANGED
@@ -1,223 +1,223 @@
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
- ### Bring your own key (BYOK)
47
-
48
- Provision the agent with your **own** public key so MetaMynd never sees the private key. The identity
49
- is issued unverified with a one-time `challenge`; the gate blocks it (`AGENT_KEY_UNVERIFIED`) until you
50
- prove control. The guard signs the challenge with your key and submits it:
51
-
52
- ```js
53
- const guard = await createGuardFromConfig('./agent.metamynd.json', { agentKey: myPrivateKey });
54
- await guard.verifyKey({ ref: config.identityId, challenge: config.challenge, token: ownerToken }); // one-time
55
- ```
56
-
57
- `guard.signChallenge(challenge)` returns just the hex signature if you'd rather submit verify-key
58
- yourself. (Fastest path: `npm create metamynd-agent@latest -- --byok` does all of this for you.)
59
-
60
- ## 1. Seed a bound agent (once)
61
-
62
- Run the seed against a running stack — it prints the agent's `DID` and `KEY`:
63
-
64
- ```powershell
65
- cd backend
66
- $env:API_BASE="http://localhost:9926/api/v1" # or https://metamynd.ai/api/v1
67
- node_modules\.bin\tsx scripts\demo-seed-governance.ts
68
- # → AGENT_DID = did:hedera:testnet:...
69
- # → AGENT_KEY = 302e0201...
70
- ```
71
-
72
- The agent is now bound to a mandate (`flight-purchase`), an **enforced Standard** (EU AI Act) and an
73
- **active SOP** (spend cap + approved tools). Manage/toggle these from the dashboard:
74
- Super Admin → Standards, Legal Entity → SOPs.
75
-
76
- ## 2. Try the example
77
-
78
- ```powershell
79
- cd integrations\agentsafe-guard
80
- $env:AGENTSAFE_API="http://localhost:9926/api/v1"
81
- $env:AGENT_DID="did:hedera:testnet:..."
82
- $env:AGENT_KEY="302e0201..."
83
- node example-openclaw-agent.mjs
84
- ```
85
-
86
- ```
87
- ✅ ALLOW $150 book-flight, low risk → booked PNR-DEMO (remaining $200)
88
- ⛔ BLOCK $600 book-flight → SOP_SPEND_CAP
89
- ⛔ BLOCK $100 wire-transfer tool → SOP_TOOL_BLOCKED
90
- ⚠ ESCALATE $100 high-risk decision → RISK_REVIEW
91
- ```
92
-
93
- Now flip the SOP's **active → inactive** toggle in the UI (or edit a rule) and re-run — the decision
94
- changes in real time.
95
-
96
- ## 3. Wire it into your OpenClaw agent
97
-
98
- Wrap each governed tool's handler with `guardTool(...)`. The wrapped handler only runs when the gate
99
- allows; otherwise it throws a `GovernanceBlocked` error your agent surfaces to the user.
100
-
101
- ```js
102
- import { createGuard, createGuardFromConfig } from '@metamynd/agentsafe-guard';
103
-
104
- // Preferred: load the portable config the one-call onboarding endpoint returns (no env vars).
105
- const guard = await createGuardFromConfig('./agent.metamynd.json');
106
-
107
- // Or configure explicitly:
108
- const guardExplicit = createGuard({
109
- api: process.env.AGENTSAFE_API,
110
- agentDid: process.env.AGENT_DID,
111
- agentKey: process.env.AGENT_KEY, // held only by the agent
112
- });
113
-
114
- // Your existing OpenClaw tool handler:
115
- async function bookFlight(args) { /* …call the airline… */ return { pnr: 'ABC123' }; }
116
-
117
- // Register the GATED version with OpenClaw instead of the raw handler:
118
- const gatedBookFlight = guard.guardTool(
119
- 'flight-purchase', // the governed action (matches the mandate scope)
120
- bookFlight,
121
- (a) => ({ // map tool args → gate inputs
122
- amount: a.amount,
123
- currency: 'USD',
124
- merchant: a.merchant,
125
- context: { tool: 'book-flight', jurisdiction: a.jurisdiction, riskLevel: a.riskLevel },
126
- }),
127
- );
128
- ```
129
-
130
- - If your OpenClaw build has a **pre-tool hook / middleware** instead of raw handlers, call
131
- `await guard.authorize({ action, amount, merchant, context })` there and refuse on any non-`allow`.
132
- - **`context`** is what the Standard/SOP atoms read (jurisdiction, model, tool, PII, risk, …). Each
133
- atom declares what it needs — fetch the catalog at `GET /api/v1/standards/atoms` to see the exact
134
- fields (`requiredContext`) for the rules your agent is bound to.
135
- - For **payment** tools (x402, §7a): after `authorize` allows, the Service returns a 402 bound to
136
- your `authorizationId`. Call `guard.preparePayment(requirements, authorizationId)` — it refuses an
137
- unbound or mismatched 402 — pay via x402, then reconcile the hold with
138
- `await guard.capture(authorizationId, amountCharged, bookingRef, settlementTxHash)`. An
139
- uncaptured hold auto-voids at its expiry (`POST /policy/mandate/authorize/:id/void` to release early).
140
-
141
- ## 4. Evaluate locally (no network)
142
-
143
- For low-latency, cooperative-mode governance the guard can evaluate a **policy bundle** locally with
144
- the same deterministic `policy-core` the gate runs — no round-trip. Given the same rule packs,
145
- mandate, and request, it returns the **identical** `allow` / `block` / `escalate` verdict.
146
-
147
- ```js
148
- const verdict = guard.evaluateLocally({
149
- standards: [{ standardKey: 'eu-ai-act', document: { molecules: [/* … */] } }],
150
- sops: [{ standardKey: 'sop:travel', document: { molecules: [/* … */] } }],
151
- mandate: { permission: [/* ODRL constraints … */] },
152
- request: { action: 'flight-purchase', amount: 600, merchant: 'amadeus',
153
- context: { riskLevel: 'low' } },
154
- });
155
- // → { decision: 'block', reasonCode: 'SOP_SPEND_CAP', authorizationId: null, remaining: null, proofRef: null }
156
- ```
157
-
158
- Or wrap a tool to gate it against a local bundle (fails closed like `guardTool`):
159
-
160
- ```js
161
- const gated = guard.guardToolLocal('flight-purchase', bookFlight, mapArgs, { standards, sops, mandate });
162
- ```
163
-
164
- Signed request fields (`amount`, `merchant`) are always applied over the unsigned `context`, so a
165
- forged context key can never shadow them (MAGP §6.4.2). Run the self-check:
166
-
167
- ```powershell
168
- cd integrations\agentsafe-guard
169
- node local-eval.smoke.mjs # PASS when every local verdict matches the gate
170
- ```
171
-
172
- Local evaluation is the cooperative-mode pre-check; the gate still owns the stateful parts
173
- (single-use nonce, atomic spend-cap reservation, evidence anchoring), so value-bearing actions should
174
- still settle through the gate / `capture` flow.
175
-
176
- ## 5. Mutual handshake with a Service (§8.2)
177
-
178
- Before transacting with a Service (an MCP), the agent and Service prove control of their DIDs
179
- to each other — no issuer calls, because the keys are embedded in the DIDs (§4.1.2). The agent
180
- drives the initiator side:
181
-
182
- ```js
183
- const hs = guard.handshake();
184
- const { nonceA, message } = hs.hello(); // → send HELLO to the Service
185
- // Service replies with CHALLENGE { toDid, nonceB, sigB(nonceA) }
186
- const { sigA, handshakeId } = hs.prove({ nonceA, challenge }); // verifies the Service, → send PROVE
187
- // Service replies READY { channelId }
188
- ```
189
-
190
- `prove()` throws `HandshakeFailed` if the Service's CHALLENGE does not verify against the key in
191
- its DID. The Service side uses [`agentsafe-mcp-guard`](../agentsafe-mcp-guard), which also
192
- re-evaluates the agent's signed request trustlessly (§9.6). Discover a Service's endpoint and
193
- confirm its key via the public resolver `GET /did/:did` (§4.4).
194
-
195
- ## 6. Escalation — human-in-the-loop (§9a)
196
-
197
- An `escalate` verdict is **not a denial** — the action is *held* pending the Owner's approval. The
198
- verdict carries an `escalationId`; no budget is reserved and (for payments) nothing settles until it
199
- is approved. The Owner resolves it in the dashboard / via `POST /policy/escalations/:id/resolve`;
200
- the agent polls the outcome:
201
-
202
- ```js
203
- const d = await guard.authorize({ action: 'flight-purchase', amount: 5000, context: { riskLevel: 'high' } });
204
- if (d.decision === 'escalate') {
205
- // parked for review — d.escalationId, d.expiresAt
206
- const outcome = await guard.escalationStatus(d.escalationId);
207
- // → { status: 'approved' | 'denied' | 'expired' | 'pending', authorizationId, reasonCode }
208
- // on 'approved', outcome.authorizationId carries into the §7a capture/pay flow.
209
- }
210
- ```
211
-
212
- On approval the budget/cap gate re-runs (§9a.3), so an approval still can't overspend; an
213
- unresolved escalation lapses to denied after its TTL (§9a.4).
214
-
215
- ## Trust model
216
-
217
- The gate is a **checkpoint** — enforcement is real when it's actually called.
218
- - **Trustless:** the counterparty the agent transacts with calls the *public* authorize endpoint and
219
- only proceeds on `allow`. A rogue agent that skips the call can't get the counterparty to act.
220
- - **Cooperative:** the agent's own tool layer (this guard) calls the gate and refuses on block/escalate.
221
-
222
- Either way, every decision is Ed25519-authenticated, deterministic, and anchored as evidence
223
- (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
+ ### Bring your own key (BYOK)
47
+
48
+ Provision the agent with your **own** public key so MetaMynd never sees the private key. The identity
49
+ is issued unverified with a one-time `challenge`; the gate blocks it (`AGENT_KEY_UNVERIFIED`) until you
50
+ prove control. The guard signs the challenge with your key and submits it:
51
+
52
+ ```js
53
+ const guard = await createGuardFromConfig('./agent.metamynd.json', { agentKey: myPrivateKey });
54
+ await guard.verifyKey({ ref: config.identityId, challenge: config.challenge, token: ownerToken }); // one-time
55
+ ```
56
+
57
+ `guard.signChallenge(challenge)` returns just the hex signature if you'd rather submit verify-key
58
+ yourself. (Fastest path: `npm create metamynd-agent@latest -- --byok` does all of this for you.)
59
+
60
+ ## 1. Seed a bound agent (once)
61
+
62
+ Run the seed against a running stack — it prints the agent's `DID` and `KEY`:
63
+
64
+ ```powershell
65
+ cd backend
66
+ $env:API_BASE="http://localhost:9926/api/v1" # or https://metamynd.ai/api/v1
67
+ node_modules\.bin\tsx scripts\demo-seed-governance.ts
68
+ # → AGENT_DID = did:hedera:testnet:...
69
+ # → AGENT_KEY = 302e0201...
70
+ ```
71
+
72
+ The agent is now bound to a mandate (`flight-purchase`), an **enforced Standard** (EU AI Act) and an
73
+ **active SOP** (spend cap + approved tools). Manage/toggle these from the dashboard:
74
+ Super Admin → Standards, Legal Entity → SOPs.
75
+
76
+ ## 2. Try the example
77
+
78
+ ```powershell
79
+ cd integrations\agentsafe-guard
80
+ $env:AGENTSAFE_API="http://localhost:9926/api/v1"
81
+ $env:AGENT_DID="did:hedera:testnet:..."
82
+ $env:AGENT_KEY="302e0201..."
83
+ node example-openclaw-agent.mjs
84
+ ```
85
+
86
+ ```
87
+ ✅ ALLOW $150 book-flight, low risk → booked PNR-DEMO (remaining $200)
88
+ ⛔ BLOCK $600 book-flight → SOP_SPEND_CAP
89
+ ⛔ BLOCK $100 wire-transfer tool → SOP_TOOL_BLOCKED
90
+ ⚠ ESCALATE $100 high-risk decision → RISK_REVIEW
91
+ ```
92
+
93
+ Now flip the SOP's **active → inactive** toggle in the UI (or edit a rule) and re-run — the decision
94
+ changes in real time.
95
+
96
+ ## 3. Wire it into your OpenClaw agent
97
+
98
+ Wrap each governed tool's handler with `guardTool(...)`. The wrapped handler only runs when the gate
99
+ allows; otherwise it throws a `GovernanceBlocked` error your agent surfaces to the user.
100
+
101
+ ```js
102
+ import { createGuard, createGuardFromConfig } from '@metamynd/agentsafe-guard';
103
+
104
+ // Preferred: load the portable config the one-call onboarding endpoint returns (no env vars).
105
+ const guard = await createGuardFromConfig('./agent.metamynd.json');
106
+
107
+ // Or configure explicitly:
108
+ const guardExplicit = createGuard({
109
+ api: process.env.AGENTSAFE_API,
110
+ agentDid: process.env.AGENT_DID,
111
+ agentKey: process.env.AGENT_KEY, // held only by the agent
112
+ });
113
+
114
+ // Your existing OpenClaw tool handler:
115
+ async function bookFlight(args) { /* …call the airline… */ return { pnr: 'ABC123' }; }
116
+
117
+ // Register the GATED version with OpenClaw instead of the raw handler:
118
+ const gatedBookFlight = guard.guardTool(
119
+ 'flight-purchase', // the governed action (matches the mandate scope)
120
+ bookFlight,
121
+ (a) => ({ // map tool args → gate inputs
122
+ amount: a.amount,
123
+ currency: 'USD',
124
+ merchant: a.merchant,
125
+ context: { tool: 'book-flight', jurisdiction: a.jurisdiction, riskLevel: a.riskLevel },
126
+ }),
127
+ );
128
+ ```
129
+
130
+ - If your OpenClaw build has a **pre-tool hook / middleware** instead of raw handlers, call
131
+ `await guard.authorize({ action, amount, merchant, context })` there and refuse on any non-`allow`.
132
+ - **`context`** is what the Standard/SOP atoms read (jurisdiction, model, tool, PII, risk, …). Each
133
+ atom declares what it needs — fetch the catalog at `GET /api/v1/standards/atoms` to see the exact
134
+ fields (`requiredContext`) for the rules your agent is bound to.
135
+ - For **payment** tools (x402, §7a): after `authorize` allows, the Service returns a 402 bound to
136
+ your `authorizationId`. Call `guard.preparePayment(requirements, authorizationId)` — it refuses an
137
+ unbound or mismatched 402 — pay via x402, then reconcile the hold with
138
+ `await guard.capture(authorizationId, amountCharged, bookingRef, settlementTxHash)`. An
139
+ uncaptured hold auto-voids at its expiry (`POST /policy/mandate/authorize/:id/void` to release early).
140
+
141
+ ## 4. Evaluate locally (no network)
142
+
143
+ For low-latency, cooperative-mode governance the guard can evaluate a **policy bundle** locally with
144
+ the same deterministic `policy-core` the gate runs — no round-trip. Given the same rule packs,
145
+ mandate, and request, it returns the **identical** `allow` / `block` / `escalate` verdict.
146
+
147
+ ```js
148
+ const verdict = guard.evaluateLocally({
149
+ standards: [{ standardKey: 'eu-ai-act', document: { molecules: [/* … */] } }],
150
+ sops: [{ standardKey: 'sop:travel', document: { molecules: [/* … */] } }],
151
+ mandate: { permission: [/* ODRL constraints … */] },
152
+ request: { action: 'flight-purchase', amount: 600, merchant: 'amadeus',
153
+ context: { riskLevel: 'low' } },
154
+ });
155
+ // → { decision: 'block', reasonCode: 'SOP_SPEND_CAP', authorizationId: null, remaining: null, proofRef: null }
156
+ ```
157
+
158
+ Or wrap a tool to gate it against a local bundle (fails closed like `guardTool`):
159
+
160
+ ```js
161
+ const gated = guard.guardToolLocal('flight-purchase', bookFlight, mapArgs, { standards, sops, mandate });
162
+ ```
163
+
164
+ Signed request fields (`amount`, `merchant`) are always applied over the unsigned `context`, so a
165
+ forged context key can never shadow them (MAGP §6.4.2). Run the self-check:
166
+
167
+ ```powershell
168
+ cd integrations\agentsafe-guard
169
+ node local-eval.smoke.mjs # PASS when every local verdict matches the gate
170
+ ```
171
+
172
+ Local evaluation is the cooperative-mode pre-check; the gate still owns the stateful parts
173
+ (single-use nonce, atomic spend-cap reservation, evidence anchoring), so value-bearing actions should
174
+ still settle through the gate / `capture` flow.
175
+
176
+ ## 5. Mutual handshake with a Service (§8.2)
177
+
178
+ Before transacting with a Service (an MCP), the agent and Service prove control of their DIDs
179
+ to each other — no issuer calls, because the keys are embedded in the DIDs (§4.1.2). The agent
180
+ drives the initiator side:
181
+
182
+ ```js
183
+ const hs = guard.handshake();
184
+ const { nonceA, message } = hs.hello(); // → send HELLO to the Service
185
+ // Service replies with CHALLENGE { toDid, nonceB, sigB(nonceA) }
186
+ const { sigA, handshakeId } = hs.prove({ nonceA, challenge }); // verifies the Service, → send PROVE
187
+ // Service replies READY { channelId }
188
+ ```
189
+
190
+ `prove()` throws `HandshakeFailed` if the Service's CHALLENGE does not verify against the key in
191
+ its DID. The Service side uses [`agentsafe-mcp-guard`](../agentsafe-mcp-guard), which also
192
+ re-evaluates the agent's signed request trustlessly (§9.6). Discover a Service's endpoint and
193
+ confirm its key via the public resolver `GET /did/:did` (§4.4).
194
+
195
+ ## 6. Escalation — human-in-the-loop (§9a)
196
+
197
+ An `escalate` verdict is **not a denial** — the action is *held* pending the Owner's approval. The
198
+ verdict carries an `escalationId`; no budget is reserved and (for payments) nothing settles until it
199
+ is approved. The Owner resolves it in the dashboard / via `POST /policy/escalations/:id/resolve`;
200
+ the agent polls the outcome:
201
+
202
+ ```js
203
+ const d = await guard.authorize({ action: 'flight-purchase', amount: 5000, context: { riskLevel: 'high' } });
204
+ if (d.decision === 'escalate') {
205
+ // parked for review — d.escalationId, d.expiresAt
206
+ const outcome = await guard.escalationStatus(d.escalationId);
207
+ // → { status: 'approved' | 'denied' | 'expired' | 'pending', authorizationId, reasonCode }
208
+ // on 'approved', outcome.authorizationId carries into the §7a capture/pay flow.
209
+ }
210
+ ```
211
+
212
+ On approval the budget/cap gate re-runs (§9a.3), so an approval still can't overspend; an
213
+ unresolved escalation lapses to denied after its TTL (§9a.4).
214
+
215
+ ## Trust model
216
+
217
+ The gate is a **checkpoint** — enforcement is real when it's actually called.
218
+ - **Trustless:** the counterparty the agent transacts with calls the *public* authorize endpoint and
219
+ only proceeds on `allow`. A rogue agent that skips the call can't get the counterparty to act.
220
+ - **Cooperative:** the agent's own tool layer (this guard) calls the gate and refuses on block/escalate.
221
+
222
+ Either way, every decision is Ed25519-authenticated, deterministic, and anchored as evidence
223
+ (visible in the dashboard's Regulator log and on HashScan).