@metamynd/agentsafe-guard 0.1.0 → 0.1.1
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 +223 -223
- package/agentsafe-guard.mjs +316 -316
- package/example-openclaw-agent.mjs +47 -47
- package/magp-did.mjs +39 -8
- package/package.json +1 -1
- package/policy-core.mjs +13 -3
|
@@ -1,47 +1,47 @@
|
|
|
1
|
-
// example-openclaw-agent.mjs — a runnable demo of the guard against a seeded bound agent.
|
|
2
|
-
//
|
|
3
|
-
// Simulates how an OpenClaw agent's TOOL is wrapped: the raw tool only runs if the
|
|
4
|
-
// AgentSafe gate returns `allow`. Run it after seeding an agent with
|
|
5
|
-
// backend/scripts/demo-seed-governance.ts:
|
|
6
|
-
//
|
|
7
|
-
// $env:AGENTSAFE_API="http://localhost:9926/api/v1"
|
|
8
|
-
// $env:AGENT_DID="did:hedera:testnet:..."
|
|
9
|
-
// $env:AGENT_KEY="302e0201..."
|
|
10
|
-
// node example-openclaw-agent.mjs
|
|
11
|
-
import { createGuard } from './agentsafe-guard.mjs';
|
|
12
|
-
|
|
13
|
-
const guard = createGuard({
|
|
14
|
-
api: process.env.AGENTSAFE_API ?? 'http://localhost:9926/api/v1',
|
|
15
|
-
agentDid: process.env.AGENT_DID,
|
|
16
|
-
agentKey: process.env.AGENT_KEY,
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
// --- The agent's tool. In OpenClaw you register this handler for the tool; here we
|
|
20
|
-
// wrap it with guard.guardTool so every call is gated first. ---
|
|
21
|
-
const bookFlight = guard.guardTool(
|
|
22
|
-
'flight-purchase', // the governed action (matches the mandate scope)
|
|
23
|
-
async (args, decision) => {
|
|
24
|
-
// Only reached when the gate ALLOWED. Real booking would go here.
|
|
25
|
-
return { booked: true, pnr: 'PNR-DEMO', remaining: decision.remaining, ...args };
|
|
26
|
-
},
|
|
27
|
-
// Map the tool args → gate inputs. `context` carries what the Standard/SOP rules need.
|
|
28
|
-
(a) => ({ amount: a.amount, currency: 'USD', merchant: a.merchant, context: { tool: a.tool ?? 'book-flight', riskLevel: a.riskLevel ?? 'low' } }),
|
|
29
|
-
);
|
|
30
|
-
|
|
31
|
-
async function ask(label, args) {
|
|
32
|
-
try {
|
|
33
|
-
const r = await bookFlight(args);
|
|
34
|
-
console.log(` \x1b[32m✅ ALLOW\x1b[0m ${label.padEnd(30)} → booked ${r.pnr} (remaining $${r.remaining})`);
|
|
35
|
-
} catch (e) {
|
|
36
|
-
const g = e.governance ?? {};
|
|
37
|
-
const tag = g.decision === 'escalate' ? '\x1b[33m⚠ ESCALATE\x1b[0m' : '\x1b[31m⛔ BLOCK\x1b[0m';
|
|
38
|
-
console.log(` ${tag} ${label.padEnd(30)} → ${g.reasonCode ?? e.message}`);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
console.log(`\n OpenClaw agent — every booking passes through the AgentSafe gate\n ${'─'.repeat(60)}`);
|
|
43
|
-
await ask('$150 book-flight, low risk', { amount: 150, merchant: 'skyward-air', tool: 'book-flight', riskLevel: 'low' });
|
|
44
|
-
await ask('$600 book-flight', { amount: 600, merchant: 'skyward-air', tool: 'book-flight', riskLevel: 'low' });
|
|
45
|
-
await ask('$100 wire-transfer tool', { amount: 100, merchant: 'skyward-air', tool: 'wire-transfer', riskLevel: 'low' });
|
|
46
|
-
await ask('$100 high-risk decision', { amount: 100, merchant: 'skyward-air', tool: 'book-flight', riskLevel: 'high' });
|
|
47
|
-
console.log(`\n The agent refuses blocked/escalated actions itself — governance decided, not the LLM.\n`);
|
|
1
|
+
// example-openclaw-agent.mjs — a runnable demo of the guard against a seeded bound agent.
|
|
2
|
+
//
|
|
3
|
+
// Simulates how an OpenClaw agent's TOOL is wrapped: the raw tool only runs if the
|
|
4
|
+
// AgentSafe gate returns `allow`. Run it after seeding an agent with
|
|
5
|
+
// backend/scripts/demo-seed-governance.ts:
|
|
6
|
+
//
|
|
7
|
+
// $env:AGENTSAFE_API="http://localhost:9926/api/v1"
|
|
8
|
+
// $env:AGENT_DID="did:hedera:testnet:..."
|
|
9
|
+
// $env:AGENT_KEY="302e0201..."
|
|
10
|
+
// node example-openclaw-agent.mjs
|
|
11
|
+
import { createGuard } from './agentsafe-guard.mjs';
|
|
12
|
+
|
|
13
|
+
const guard = createGuard({
|
|
14
|
+
api: process.env.AGENTSAFE_API ?? 'http://localhost:9926/api/v1',
|
|
15
|
+
agentDid: process.env.AGENT_DID,
|
|
16
|
+
agentKey: process.env.AGENT_KEY,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// --- The agent's tool. In OpenClaw you register this handler for the tool; here we
|
|
20
|
+
// wrap it with guard.guardTool so every call is gated first. ---
|
|
21
|
+
const bookFlight = guard.guardTool(
|
|
22
|
+
'flight-purchase', // the governed action (matches the mandate scope)
|
|
23
|
+
async (args, decision) => {
|
|
24
|
+
// Only reached when the gate ALLOWED. Real booking would go here.
|
|
25
|
+
return { booked: true, pnr: 'PNR-DEMO', remaining: decision.remaining, ...args };
|
|
26
|
+
},
|
|
27
|
+
// Map the tool args → gate inputs. `context` carries what the Standard/SOP rules need.
|
|
28
|
+
(a) => ({ amount: a.amount, currency: 'USD', merchant: a.merchant, context: { tool: a.tool ?? 'book-flight', riskLevel: a.riskLevel ?? 'low' } }),
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
async function ask(label, args) {
|
|
32
|
+
try {
|
|
33
|
+
const r = await bookFlight(args);
|
|
34
|
+
console.log(` \x1b[32m✅ ALLOW\x1b[0m ${label.padEnd(30)} → booked ${r.pnr} (remaining $${r.remaining})`);
|
|
35
|
+
} catch (e) {
|
|
36
|
+
const g = e.governance ?? {};
|
|
37
|
+
const tag = g.decision === 'escalate' ? '\x1b[33m⚠ ESCALATE\x1b[0m' : '\x1b[31m⛔ BLOCK\x1b[0m';
|
|
38
|
+
console.log(` ${tag} ${label.padEnd(30)} → ${g.reasonCode ?? e.message}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
console.log(`\n OpenClaw agent — every booking passes through the AgentSafe gate\n ${'─'.repeat(60)}`);
|
|
43
|
+
await ask('$150 book-flight, low risk', { amount: 150, merchant: 'skyward-air', tool: 'book-flight', riskLevel: 'low' });
|
|
44
|
+
await ask('$600 book-flight', { amount: 600, merchant: 'skyward-air', tool: 'book-flight', riskLevel: 'low' });
|
|
45
|
+
await ask('$100 wire-transfer tool', { amount: 100, merchant: 'skyward-air', tool: 'wire-transfer', riskLevel: 'low' });
|
|
46
|
+
await ask('$100 high-risk decision', { amount: 100, merchant: 'skyward-air', tool: 'book-flight', riskLevel: 'high' });
|
|
47
|
+
console.log(`\n The agent refuses blocked/escalated actions itself — governance decided, not the LLM.\n`);
|
package/magp-did.mjs
CHANGED
|
@@ -67,6 +67,30 @@ function parseHederaDid(did) {
|
|
|
67
67
|
if (publicKeyBytes.length !== 32) return null;
|
|
68
68
|
return { network, publicKeyMultibase, publicKeyBytes, topicId };
|
|
69
69
|
}
|
|
70
|
+
var ED25519_MULTICODEC = Uint8Array.of(237, 1);
|
|
71
|
+
function buildDidKey(publicKeyBytes) {
|
|
72
|
+
const prefixed = new Uint8Array(ED25519_MULTICODEC.length + publicKeyBytes.length);
|
|
73
|
+
prefixed.set(ED25519_MULTICODEC, 0);
|
|
74
|
+
prefixed.set(publicKeyBytes, ED25519_MULTICODEC.length);
|
|
75
|
+
return `did:key:${multibaseBase58btc(prefixed)}`;
|
|
76
|
+
}
|
|
77
|
+
function parseDidKey(did) {
|
|
78
|
+
const m = /^did:key:(z[1-9A-HJ-NP-Za-km-z]+)$/.exec(did ?? "");
|
|
79
|
+
if (!m) return null;
|
|
80
|
+
const multibase = m[1];
|
|
81
|
+
let decoded;
|
|
82
|
+
try {
|
|
83
|
+
decoded = base58Decode(multibase.slice(1));
|
|
84
|
+
} catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
if (decoded.length !== ED25519_MULTICODEC.length + 32) return null;
|
|
88
|
+
if (decoded[0] !== ED25519_MULTICODEC[0] || decoded[1] !== ED25519_MULTICODEC[1]) return null;
|
|
89
|
+
return { method: "key", publicKeyMultibase: multibase, publicKeyBytes: decoded.slice(ED25519_MULTICODEC.length) };
|
|
90
|
+
}
|
|
91
|
+
function didPublicKeyBytes(did) {
|
|
92
|
+
return parseHederaDid(did)?.publicKeyBytes ?? parseDidKey(did)?.publicKeyBytes ?? null;
|
|
93
|
+
}
|
|
70
94
|
|
|
71
95
|
// src/features/magp/did.ts
|
|
72
96
|
var ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
|
|
@@ -75,31 +99,35 @@ function ed25519KeyFromRaw(raw) {
|
|
|
75
99
|
return crypto.createPublicKey({ key: der, format: "der", type: "spki" });
|
|
76
100
|
}
|
|
77
101
|
function verifyDidSignature(did, message, signatureHex) {
|
|
78
|
-
const
|
|
79
|
-
if (!
|
|
102
|
+
const publicKeyBytes = didPublicKeyBytes(did);
|
|
103
|
+
if (!publicKeyBytes) return false;
|
|
80
104
|
try {
|
|
81
|
-
const key = ed25519KeyFromRaw(
|
|
105
|
+
const key = ed25519KeyFromRaw(publicKeyBytes);
|
|
82
106
|
return crypto.verify(null, Buffer.from(message, "utf8"), key, Buffer.from(signatureHex, "hex"));
|
|
83
107
|
} catch {
|
|
84
108
|
return false;
|
|
85
109
|
}
|
|
86
110
|
}
|
|
87
111
|
function buildDidDocument(did, service) {
|
|
88
|
-
const
|
|
89
|
-
|
|
112
|
+
const hedera = parseHederaDid(did);
|
|
113
|
+
const key = hedera ? null : parseDidKey(did);
|
|
114
|
+
if (!hedera && !key) return null;
|
|
115
|
+
const publicKeyMultibase = hedera ? hedera.publicKeyMultibase : key.publicKeyMultibase;
|
|
116
|
+
const fragment = hedera ? "#did-root-key" : `#${key.publicKeyMultibase}`;
|
|
117
|
+
const vmId = `${did}${fragment}`;
|
|
90
118
|
const doc = {
|
|
91
119
|
"@context": ["https://www.w3.org/ns/did/v1"],
|
|
92
120
|
id: did,
|
|
93
121
|
controller: did,
|
|
94
122
|
verificationMethod: [
|
|
95
123
|
{
|
|
96
|
-
id:
|
|
124
|
+
id: vmId,
|
|
97
125
|
type: "Ed25519VerificationKey2020",
|
|
98
126
|
controller: did,
|
|
99
|
-
publicKeyMultibase
|
|
127
|
+
publicKeyMultibase
|
|
100
128
|
}
|
|
101
129
|
],
|
|
102
|
-
authentication: [
|
|
130
|
+
authentication: [vmId]
|
|
103
131
|
};
|
|
104
132
|
if (service) {
|
|
105
133
|
doc.service = [
|
|
@@ -118,9 +146,12 @@ export {
|
|
|
118
146
|
base58,
|
|
119
147
|
base58Decode,
|
|
120
148
|
buildDidDocument,
|
|
149
|
+
buildDidKey,
|
|
121
150
|
buildHederaDid,
|
|
151
|
+
didPublicKeyBytes,
|
|
122
152
|
ed25519KeyFromRaw,
|
|
123
153
|
multibaseBase58btc,
|
|
154
|
+
parseDidKey,
|
|
124
155
|
parseHederaDid,
|
|
125
156
|
verifyDidSignature
|
|
126
157
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metamynd/agentsafe-guard",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Zero-dependency runtime governance for any Node AI agent — gate tool calls through MetaMynd/AgentSafe (allow / block / escalate) against the agent's mandate, enforced Standards, and SOPs. Ed25519-signed, deterministic, fail-closed.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./agentsafe-guard.mjs",
|
package/policy-core.mjs
CHANGED
|
@@ -11,6 +11,9 @@ var ATOM_REGISTRY = {
|
|
|
11
11
|
return have !== void 0 && need !== void 0 && have >= need;
|
|
12
12
|
},
|
|
13
13
|
"amount-over": (c, cfg) => typeof c.amount === "number" && c.amount > Number(cfg?.limit ?? 0),
|
|
14
|
+
// Total budget: cumulativeSpend is a SERVER-derived, signed-last context field (never
|
|
15
|
+
// shadowable by the agent's itinerary), so this compares already-spent + this amount.
|
|
16
|
+
"cumulative-over": (c, cfg) => Number(c.cumulativeSpend ?? 0) + Number(c.amount ?? 0) > Number(cfg?.limit ?? 0),
|
|
14
17
|
// Fires if any configured term appears in the prompt and/or output text.
|
|
15
18
|
// Used to govern agent responses on content (prohibited claims, sensitive advice).
|
|
16
19
|
"text-matches": (c, cfg) => {
|
|
@@ -39,9 +42,16 @@ function notInAllowList(value, allowList) {
|
|
|
39
42
|
var ATOM_SPECS = [
|
|
40
43
|
{
|
|
41
44
|
predicate: "amount-over",
|
|
42
|
-
label: "
|
|
43
|
-
description: "Fires when
|
|
44
|
-
config: [{ key: "limit", type: "number", required: true, description: "Maximum allowed amount" }],
|
|
45
|
+
label: "Per-transaction amount over limit",
|
|
46
|
+
description: "Fires when a single action amount exceeds a configured limit (per-transaction cap).",
|
|
47
|
+
config: [{ key: "limit", type: "number", required: true, description: "Maximum allowed amount for one transaction" }],
|
|
48
|
+
requiredContext: ["amount"]
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
predicate: "cumulative-over",
|
|
52
|
+
label: "Total budget over limit",
|
|
53
|
+
description: "Fires when cumulative spend (already-spent + this transaction) exceeds a configured total budget.",
|
|
54
|
+
config: [{ key: "limit", type: "number", required: true, description: "Maximum total budget across all transactions" }],
|
|
45
55
|
requiredContext: ["amount"]
|
|
46
56
|
},
|
|
47
57
|
{
|