@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.
@@ -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 parsed = parseHederaDid(did);
79
- if (!parsed) return false;
102
+ const publicKeyBytes = didPublicKeyBytes(did);
103
+ if (!publicKeyBytes) return false;
80
104
  try {
81
- const key = ed25519KeyFromRaw(parsed.publicKeyBytes);
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 parsed = parseHederaDid(did);
89
- if (!parsed) return null;
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: `${did}#did-root-key`,
124
+ id: vmId,
97
125
  type: "Ed25519VerificationKey2020",
98
126
  controller: did,
99
- publicKeyMultibase: parsed.publicKeyMultibase
127
+ publicKeyMultibase
100
128
  }
101
129
  ],
102
- authentication: [`${did}#did-root-key`]
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.0",
3
+ "version": "0.1.2",
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) => {
@@ -27,7 +30,12 @@ ${c.output ?? ""}`.toLowerCase();
27
30
  "model-not-allowed": (c, cfg) => notInAllowList(c.model, cfg?.allowed),
28
31
  "tool-not-allowed": (c, cfg) => notInAllowList(c.tool, cfg?.allowed),
29
32
  "pii-present": (c) => c.piiPresent === true,
30
- "rate-limit-exceeded": (c, cfg) => typeof c.callCount === "number" && c.callCount > Number(cfg?.max ?? 0)
33
+ "rate-limit-exceeded": (c, cfg) => typeof c.callCount === "number" && c.callCount > Number(cfg?.max ?? 0),
34
+ // Trust guidance (MetaMynd Trust Index / HCS-28). Fires when the counterparty's trust score is
35
+ // below a soft REVIEW line — intended to author an ESCALATE (route to a human), NOT a hard block.
36
+ // The score is server-derived (signed-last) so the agent's itinerary can't fake it; when no score
37
+ // is present (e.g. no counterparty resolved) the atom simply does not fire — no guidance.
38
+ "hol-trust-below-review": (c, cfg) => typeof c.holTrustScore === "number" && c.holTrustScore < Number(cfg?.reviewBelow ?? 60)
31
39
  };
32
40
  function notInAllowList(value, allowList) {
33
41
  const v = value != null ? String(value).toLowerCase().trim() : "";
@@ -39,9 +47,16 @@ function notInAllowList(value, allowList) {
39
47
  var ATOM_SPECS = [
40
48
  {
41
49
  predicate: "amount-over",
42
- label: "Amount over limit",
43
- description: "Fires when the action amount exceeds a configured limit.",
44
- config: [{ key: "limit", type: "number", required: true, description: "Maximum allowed amount" }],
50
+ label: "Per-transaction amount over limit",
51
+ description: "Fires when a single action amount exceeds a configured limit (per-transaction cap).",
52
+ config: [{ key: "limit", type: "number", required: true, description: "Maximum allowed amount for one transaction" }],
53
+ requiredContext: ["amount"]
54
+ },
55
+ {
56
+ predicate: "cumulative-over",
57
+ label: "Total budget over limit",
58
+ description: "Fires when cumulative spend (already-spent + this transaction) exceeds a configured total budget.",
59
+ config: [{ key: "limit", type: "number", required: true, description: "Maximum total budget across all transactions" }],
45
60
  requiredContext: ["amount"]
46
61
  },
47
62
  {
@@ -123,6 +138,13 @@ var ATOM_SPECS = [
123
138
  description: "Fires when the rolling call count exceeds a configured maximum.",
124
139
  config: [{ key: "max", type: "number", required: true, description: "Maximum allowed calls" }],
125
140
  requiredContext: ["callCount"]
141
+ },
142
+ {
143
+ predicate: "hol-trust-below-review",
144
+ label: "Counterparty trust below review line",
145
+ description: "Routes to human review when the counterparty's MetaMynd Trust Index (HCS-28) score is below a soft review line. Guidance, not a hard block \u2014 author it with an ESCALATE decision. The score is resolved server-side; no counterparty score \u2192 the atom does not fire.",
146
+ config: [{ key: "reviewBelow", type: "number", required: true, description: "Trust score (0\u2013100) below which a human is asked to decide" }],
147
+ requiredContext: ["holTrustScore"]
126
148
  }
127
149
  ];
128
150
  var CATALOGUED_ATOMS = ATOM_SPECS.filter((s) => !!ATOM_REGISTRY[s.predicate]);