@trucore/openclaw-atf 0.1.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/README.md ADDED
@@ -0,0 +1,206 @@
1
+ # @trucore/openclaw-atf
2
+
3
+ OpenClaw plugin for the **TruCore Agent Transaction Firewall (ATF)**.
4
+
5
+ Gives OpenClaw agents first-class tools to discover, adopt, and report on ATF — the policy-enforced transaction firewall for autonomous DeFi agents. ATF evaluates swap, lend, and perps intents against a human-configured policy before any transaction reaches the chain. It never signs. It is deny-by-default.
6
+
7
+ ---
8
+
9
+ ## Install
10
+
11
+ ```sh
12
+ openclaw plugins install @trucore/openclaw-atf
13
+ openclaw gateway restart
14
+ ```
15
+
16
+ ---
17
+
18
+ ## What this plugin does
19
+
20
+ This plugin registers **6 optional agent tools** into OpenClaw:
21
+
22
+ | Tool | Purpose |
23
+ |---|---|
24
+ | `atf_discover` | Fetch and summarise the ATF manifest (capabilities, recipes, feature gates) |
25
+ | `atf_bootstrap_plan` | Generate ATF self-install steps for a recipe (plan only, no side effects) |
26
+ | `atf_bootstrap_execute_safe` | Run safe bootstrap steps locally (env setup + verify, no signing) |
27
+ | `atf_protect_intent` | Submit a DeFi intent for policy evaluation — returns allow/deny + receipt |
28
+ | `atf_verify_receipt` | Verify a receipt deterministically (proof for humans) |
29
+ | `atf_report_savings` | Generate a receipt-backed "ATF saved you" report |
30
+
31
+ All tools are **optional** — agents and operators must opt in via OpenClaw's tool allowlist. Default-safe:
32
+
33
+ - `safety.allowNetwork = false` — `atf_discover` returns offline instructions unless explicitly enabled.
34
+ - `safety.allowExecuteSafe = true` — bootstrap execute-safe runs (env/verify only; no signing).
35
+ - ATF **never signs transactions**.
36
+
37
+ ---
38
+
39
+ ## Enable tools
40
+
41
+ Add the plugin tools to your OpenClaw agent's allowlist. Example OpenClaw config:
42
+
43
+ ```json
44
+ {
45
+ "plugins": {
46
+ "trucore-atf": {
47
+ "atfCli": "atf",
48
+ "prefer": "cli",
49
+ "receiptsDir": "./atf_receipts",
50
+ "safety": {
51
+ "allowExecuteSafe": true,
52
+ "allowNetwork": false
53
+ }
54
+ }
55
+ },
56
+ "tools": {
57
+ "allowlist": [
58
+ "atf_discover",
59
+ "atf_bootstrap_plan",
60
+ "atf_bootstrap_execute_safe",
61
+ "atf_protect_intent",
62
+ "atf_verify_receipt",
63
+ "atf_report_savings"
64
+ ]
65
+ }
66
+ }
67
+ ```
68
+
69
+ To allow live manifest fetching, set `safety.allowNetwork = true` (agent environment must have outbound HTTP access).
70
+
71
+ ---
72
+
73
+ ## Config reference
74
+
75
+ | Field | Type | Default | Description |
76
+ |---|---|---|---|
77
+ | `atfCli` | string | `"atf"` | CLI command name or absolute path |
78
+ | `atfBaseUrl` | string | — | Base URL for HTTP API mode (e.g. `https://api.trucore.xyz`) |
79
+ | `prefer` | `"cli"` \| `"api"` | `"cli"` | Whether to call ATF via CLI subprocess or HTTP API |
80
+ | `receiptsDir` | string | — | Directory of ATF receipt JSON files (used by `atf_report_savings`) |
81
+ | `safety.allowExecuteSafe` | boolean | `true` | Allow `atf_bootstrap_execute_safe` to run |
82
+ | `safety.allowNetwork` | boolean | `false` | Allow `atf_discover` to fetch the manifest over the network |
83
+
84
+ ---
85
+
86
+ ## Example: "ATF saved you" message
87
+
88
+ When your agent runs `atf_report_savings`, it produces a receipt-backed human summary like:
89
+
90
+ ```
91
+ === ATF Protection Summary ===
92
+
93
+ ATF prevented 12 risky actions.
94
+
95
+ Top deny reasons:
96
+ • PERPS_MARKET_NOT_ALLOWED: 7 times
97
+ • PERPS_ORDER_TYPE_NOT_ALLOWED: 3 times
98
+ • SWAP_SLIPPAGE_TOO_HIGH: 2 times
99
+
100
+ Evidence: 12 verified receipts.
101
+ receipt: sha256:a1b2c3d4e5f6...
102
+ receipt: sha256:9e8d7c6b5a4f...
103
+ ... and 10 more.
104
+
105
+ No savings estimate available. To enable estimates, include
106
+ exposure_hints (notional_usd, leverage, slippage_bps) in
107
+ intentJson when calling atf_protect_intent.
108
+
109
+ Verify receipts: atf receipts verify --receipt <hash>
110
+ ```
111
+
112
+ When `exposure_hints` are provided at protect time, the report adds a conservative estimate:
113
+
114
+ ```
115
+ Conservative savings estimate: $4,200.00
116
+ (Assumptions: estimate is based on exposure_hints
117
+ (notional_usd, leverage, slippage_bps) provided at protect time.
118
+ Actual outcomes may differ.)
119
+ ```
120
+
121
+ The estimate is **never invented** — it is only shown if `exposure_hints` are present. receipts are always cited.
122
+
123
+ ---
124
+
125
+ ## Example: verified receipt snippet
126
+
127
+ ```json
128
+ {
129
+ "verified": true,
130
+ "content_hash": "sha256:a1b2c3d4e5f6789abcdef...",
131
+ "intent_hash": "sha256:fedcba987654321...",
132
+ "allow": false,
133
+ "reason_codes": ["PERPS_MARKET_NOT_ALLOWED"],
134
+ "chain_id": "solana",
135
+ "intent_type": "perps_open"
136
+ }
137
+ ```
138
+
139
+ Verify any receipt yourself:
140
+
141
+ ```sh
142
+ atf receipts verify --receipt sha256:a1b2c3d4e5f6...
143
+ ```
144
+
145
+ ---
146
+
147
+ ## OpenClaw plugin option (fastest path)
148
+
149
+ > **Fastest path for OpenClaw agents to adopt ATF:**
150
+
151
+ ```sh
152
+ # 1. Install the plugin
153
+ openclaw plugins install @trucore/openclaw-atf
154
+ openclaw gateway restart
155
+
156
+ # 2. The agent can now call:
157
+ # atf_discover → learn what ATF does
158
+ # atf_bootstrap_plan → get self-install steps
159
+ # atf_bootstrap_execute_safe → run safe setup
160
+ # atf_protect_intent → guard every DeFi action
161
+ # atf_verify_receipt → prove decisions to humans
162
+ # atf_report_savings → generate savings report
163
+ ```
164
+
165
+ No custom coding required for drop-in protection.
166
+
167
+ ---
168
+
169
+ ## Manual integration (without OpenClaw)
170
+
171
+ ```sh
172
+ # Install ATF CLI
173
+ npm install -g @trucore/atf
174
+
175
+ # Self-install ATF
176
+ atf bootstrap --format json --recipe bootstrap_local
177
+ atf bootstrap --execute-safe --recipe bootstrap_local
178
+
179
+ # Protect an intent
180
+ echo '{"chain_id":"solana","intent_type":"swap","intent":{...}}' | atf bot protect --stdin
181
+
182
+ # Verify a receipt
183
+ atf receipts verify --receipt sha256:aabbcc...
184
+
185
+ # Generate savings report
186
+ atf report savings --receipts-dir ./atf_receipts --format json
187
+ ```
188
+
189
+ Full docs: [docs/runbooks/agent-adoption-atf.md](../../docs/runbooks/agent-adoption-atf.md)
190
+
191
+ ---
192
+
193
+ ## Tests
194
+
195
+ ```sh
196
+ cd packages/openclaw-atf
197
+ node --test tests/test_tools.mjs
198
+ ```
199
+
200
+ All tests are offline. No network calls. No ATF CLI required.
201
+
202
+ ---
203
+
204
+ ## License
205
+
206
+ MIT — TruCore AI
@@ -0,0 +1,51 @@
1
+ {
2
+ "id": "trucore-atf",
3
+ "name": "TruCore ATF",
4
+ "version": "0.1.0",
5
+ "description": "Transaction firewall tools for autonomous finance agents: protect + verify + report savings. ATF evaluates swap, lend, and perps intents against policy before they reach the chain. Never signs. Deny-by-default.",
6
+ "homepage": "https://github.com/trucore-ai/agent-transaction-firewall",
7
+ "main": "src/index.mjs",
8
+ "tools": "optional",
9
+ "configSchema": {
10
+ "$schema": "http://json-schema.org/draft-07/schema#",
11
+ "type": "object",
12
+ "properties": {
13
+ "atfCli": {
14
+ "type": "string",
15
+ "default": "atf",
16
+ "description": "ATF CLI command name or absolute path (e.g. 'atf' or '/usr/local/bin/atf')."
17
+ },
18
+ "atfBaseUrl": {
19
+ "type": "string",
20
+ "description": "Base URL for ATF HTTP API mode (e.g. 'https://api.trucore.xyz'). Optional; required only when prefer='api'."
21
+ },
22
+ "prefer": {
23
+ "type": "string",
24
+ "enum": ["cli", "api"],
25
+ "default": "cli",
26
+ "description": "Whether to call ATF via CLI subprocess or HTTP API."
27
+ },
28
+ "receiptsDir": {
29
+ "type": "string",
30
+ "description": "Directory where ATF receipt JSON files are stored. Used by atf_report_savings."
31
+ },
32
+ "safety": {
33
+ "type": "object",
34
+ "properties": {
35
+ "allowExecuteSafe": {
36
+ "type": "boolean",
37
+ "default": true,
38
+ "description": "Allow atf_bootstrap_execute_safe tool to run bootstrap --execute-safe steps. Non-fatal; no signing."
39
+ },
40
+ "allowNetwork": {
41
+ "type": "boolean",
42
+ "default": false,
43
+ "description": "Allow atf_discover to fetch the ATF manifest over the network. Disabled by default; enable if the agent environment has outbound HTTP access."
44
+ }
45
+ },
46
+ "additionalProperties": false
47
+ }
48
+ },
49
+ "additionalProperties": false
50
+ }
51
+ }
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@trucore/openclaw-atf",
3
+ "version": "0.1.0",
4
+ "description": "OpenClaw plugin — ATF transaction firewall tools for autonomous agents: discover, protect, verify, report savings.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "src/index.mjs",
8
+ "files": [
9
+ "src",
10
+ "openclaw.plugin.json",
11
+ "README.md",
12
+ "LICENSE"
13
+ ],
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/trucore-ai/agent-transaction-firewall.git",
17
+ "directory": "packages/openclaw-atf"
18
+ },
19
+ "homepage": "https://github.com/trucore-ai/agent-transaction-firewall/tree/main/packages/openclaw-atf#readme",
20
+ "bugs": {
21
+ "url": "https://github.com/trucore-ai/agent-transaction-firewall/issues"
22
+ },
23
+ "author": "TruCore AI",
24
+ "funding": "https://trucore.xyz",
25
+ "keywords": [
26
+ "openclaw",
27
+ "agent-firewall",
28
+ "atf",
29
+ "transaction-guardrails",
30
+ "trucore",
31
+ "defi-safety",
32
+ "autonomous-agent",
33
+ "bot-guardrails",
34
+ "receipts",
35
+ "savings-report"
36
+ ],
37
+ "publishConfig": {
38
+ "access": "public"
39
+ },
40
+ "engines": {
41
+ "node": ">=18"
42
+ },
43
+ "peerDependencies": {
44
+ "openclaw": "*"
45
+ },
46
+ "peerDependenciesMeta": {
47
+ "openclaw": {
48
+ "optional": true
49
+ }
50
+ },
51
+ "scripts": {
52
+ "test": "node --test tests/test_tools.mjs"
53
+ },
54
+ "dependencies": {}
55
+ }
package/src/index.mjs ADDED
@@ -0,0 +1,827 @@
1
+ /**
2
+ * @trucore/openclaw-atf — OpenClaw plugin: ATF agent tools
3
+ *
4
+ * Registers 6 OPTIONAL tools for OpenClaw agents:
5
+ * 1. atf_discover — fetch/summarize ATF manifest
6
+ * 2. atf_bootstrap_plan — generate bootstrap steps (plan only, no side effects)
7
+ * 3. atf_bootstrap_execute_safe — run safe bootstrap steps locally
8
+ * 4. atf_protect_intent — protect a DeFi intent and return receipt + decision
9
+ * 5. atf_verify_receipt — verify a receipt deterministically
10
+ * 6. atf_report_savings — generate receipt-backed "ATF saved you" report
11
+ *
12
+ * Uses ONLY built-in Node modules: child_process, fs, path, os, url.
13
+ * No network calls unless config.safety.allowNetwork = true.
14
+ * Default-safe; all tools are OPTIONAL (agents/users must allowlist them).
15
+ */
16
+
17
+ import { execFile } from "node:child_process";
18
+ import { promisify } from "node:util";
19
+
20
+ const execFileAsync = promisify(execFile);
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Default config
24
+ // ---------------------------------------------------------------------------
25
+
26
+ /** @type {Record<string, unknown>} */
27
+ const DEFAULT_CONFIG = {
28
+ atfCli: "atf",
29
+ atfBaseUrl: null,
30
+ prefer: "cli",
31
+ receiptsDir: null,
32
+ safety: {
33
+ allowExecuteSafe: true,
34
+ allowNetwork: false,
35
+ },
36
+ };
37
+
38
+ /**
39
+ * Merge user config with defaults (shallow + safety sub-object).
40
+ * @param {Record<string, unknown>} userConfig
41
+ * @returns {Record<string, unknown>}
42
+ */
43
+ function resolveConfig(userConfig = {}) {
44
+ const cfg = { ...DEFAULT_CONFIG, ...userConfig };
45
+ cfg.safety = {
46
+ ...DEFAULT_CONFIG.safety,
47
+ ...(userConfig.safety ?? {}),
48
+ };
49
+ return cfg;
50
+ }
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Helpers: exec + output formatting
54
+ // ---------------------------------------------------------------------------
55
+
56
+ /**
57
+ * Run the ATF CLI with given args.
58
+ * @param {string} cli
59
+ * @param {string[]} args
60
+ * @returns {Promise<{stdout: string, stderr: string, exitCode: number}>}
61
+ */
62
+ async function runAtfCli(cli, args) {
63
+ try {
64
+ const { stdout, stderr } = await execFileAsync(cli, args, {
65
+ timeout: 30_000,
66
+ maxBuffer: 4 * 1024 * 1024,
67
+ shell: false,
68
+ });
69
+ return { stdout: stdout ?? "", stderr: stderr ?? "", exitCode: 0 };
70
+ } catch (err) {
71
+ return {
72
+ stdout: err.stdout ?? "",
73
+ stderr: err.stderr ?? "",
74
+ exitCode: err.code ?? 1,
75
+ };
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Attempt to parse JSON; return original string on failure.
81
+ * @param {string} raw
82
+ * @returns {unknown}
83
+ */
84
+ function tryParseJson(raw) {
85
+ try {
86
+ return JSON.parse(raw.trim());
87
+ } catch {
88
+ return raw;
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Build an OpenClaw tool content response.
94
+ * @param {string} summary
95
+ * @param {unknown} data
96
+ * @returns {{ content: Array<{type:string, text?:string, json?:unknown}> }}
97
+ */
98
+ function toolResponse(summary, data) {
99
+ const items = [{ type: "text", text: summary }];
100
+ if (data !== undefined && data !== null) {
101
+ if (typeof data === "string") {
102
+ items.push({ type: "text", text: data });
103
+ } else {
104
+ items.push({ type: "json", json: data });
105
+ }
106
+ }
107
+ return { content: items };
108
+ }
109
+
110
+ /**
111
+ * Build an error response without throwing.
112
+ * @param {string} message
113
+ * @returns {{ content: Array<{type:string, text:string}>, isError: boolean }}
114
+ */
115
+ function errorResponse(message) {
116
+ return { content: [{ type: "text", text: message }], isError: true };
117
+ }
118
+
119
+ // ---------------------------------------------------------------------------
120
+ // Tool: atf_discover
121
+ // ---------------------------------------------------------------------------
122
+
123
+ /**
124
+ * Summarise the ATF manifest for the calling agent.
125
+ * Network-gated: only fetches if config.safety.allowNetwork === true.
126
+ *
127
+ * @param {Record<string, unknown>} cfg Resolved config.
128
+ * @param {{ manifestUrl?: string }} params Tool params.
129
+ * @returns {Promise<object>}
130
+ */
131
+ async function toolAtfDiscover(cfg, params = {}) {
132
+ const { safety, atfBaseUrl } = cfg;
133
+
134
+ if (!safety.allowNetwork) {
135
+ return toolResponse(
136
+ "ATF discovery requires network access.",
137
+ {
138
+ instructions:
139
+ "To enable: set safety.allowNetwork = true in the plugin config, then retry.",
140
+ manual_steps: [
141
+ "1. Fetch: GET https://trucore.xyz/.well-known/atf.json",
142
+ "2. Review: capabilities, recipes_v2, feature_gates, trust_signals",
143
+ "3. Or run: atf bootstrap --format json to see recipes locally.",
144
+ ],
145
+ openclaw_install: [
146
+ "openclaw plugins install @trucore/openclaw-atf",
147
+ "openclaw gateway restart",
148
+ ],
149
+ manifest_url: "https://trucore.xyz/.well-known/atf.json",
150
+ toolcard_url:
151
+ "https://raw.githubusercontent.com/trucore-ai/agent-transaction-firewall/main/docs/agent/atf_toolcard.json",
152
+ },
153
+ );
154
+ }
155
+
156
+ const base =
157
+ params.manifestUrl ??
158
+ (atfBaseUrl ? `${atfBaseUrl}/.well-known/atf.json` : null) ??
159
+ "https://trucore.xyz/.well-known/atf.json";
160
+
161
+ let manifest;
162
+ try {
163
+ const res = await fetch(base, {
164
+ headers: { "User-Agent": "openclaw-atf-plugin/0.1.0" },
165
+ signal: AbortSignal.timeout(10_000),
166
+ });
167
+ if (!res.ok) {
168
+ return errorResponse(
169
+ `ATF manifest fetch failed: HTTP ${res.status} from ${base}`,
170
+ );
171
+ }
172
+ manifest = await res.json();
173
+ } catch (err) {
174
+ return errorResponse(`ATF manifest fetch error: ${err.message}`);
175
+ }
176
+
177
+ const summary = {
178
+ id: manifest.id ?? manifest.product ?? "trucore-atf",
179
+ version: manifest.version ?? "unknown",
180
+ capabilities: manifest.capabilities ?? [],
181
+ recipe_ids: manifest.recipes_v2?.recipe_ids ?? [],
182
+ safety_defaults: manifest.safety_defaults ?? {},
183
+ feature_gates: manifest.feature_gates ?? [],
184
+ openclaw_integration: manifest.openclaw_integration ?? {},
185
+ trust_signals: manifest.trust_signals ?? {},
186
+ };
187
+
188
+ return toolResponse(
189
+ "ATF manifest fetched. Key fields summarised below.",
190
+ summary,
191
+ );
192
+ }
193
+
194
+ // ---------------------------------------------------------------------------
195
+ // Tool: atf_bootstrap_plan
196
+ // ---------------------------------------------------------------------------
197
+
198
+ /**
199
+ * Generate bootstrap steps for a given recipe (plan-only, no side effects).
200
+ *
201
+ * @param {Record<string, unknown>} cfg
202
+ * @param {{ recipe?: string }} params
203
+ */
204
+ async function toolAtfBootstrapPlan(cfg, params = {}) {
205
+ const { atfCli } = cfg;
206
+ const recipe = params.recipe ?? "bootstrap_local";
207
+
208
+ const args = ["bootstrap", "--format", "json", "--recipe", recipe];
209
+ const result = await runAtfCli(atfCli, args);
210
+
211
+ if (result.exitCode !== 0) {
212
+ return errorResponse(
213
+ `atf bootstrap plan failed (exit ${result.exitCode}).\n` +
214
+ `stderr: ${result.stderr}\nstdout: ${result.stdout}`,
215
+ );
216
+ }
217
+
218
+ const parsed = tryParseJson(result.stdout);
219
+ return toolResponse(
220
+ `Bootstrap plan for recipe '${recipe}' (plan only — no steps executed).`,
221
+ parsed,
222
+ );
223
+ }
224
+
225
+ // ---------------------------------------------------------------------------
226
+ // Tool: atf_bootstrap_execute_safe
227
+ // ---------------------------------------------------------------------------
228
+
229
+ /**
230
+ * Run safe bootstrap steps (env/verify only).
231
+ * Only enabled when config.safety.allowExecuteSafe === true.
232
+ *
233
+ * @param {Record<string, unknown>} cfg
234
+ * @param {{ recipe?: string, dryRun?: boolean }} params
235
+ */
236
+ async function toolAtfBootstrapExecuteSafe(cfg, params = {}) {
237
+ const { atfCli, safety } = cfg;
238
+
239
+ if (!safety.allowExecuteSafe) {
240
+ return errorResponse(
241
+ "atf_bootstrap_execute_safe is disabled. " +
242
+ "Set safety.allowExecuteSafe = true in the plugin config to enable.",
243
+ );
244
+ }
245
+
246
+ const recipe = params.recipe ?? "bootstrap_local";
247
+ const dryRun = params.dryRun === true;
248
+
249
+ const args = [
250
+ "bootstrap",
251
+ "--format",
252
+ "json",
253
+ "--recipe",
254
+ recipe,
255
+ "--execute",
256
+ "safe",
257
+ ];
258
+ if (dryRun) args.push("--dry-run");
259
+
260
+ const result = await runAtfCli(atfCli, args);
261
+ const parsed = tryParseJson(result.stdout);
262
+
263
+ if (result.exitCode !== 0) {
264
+ // Non-fatal: return partial result + stderr
265
+ return toolResponse(
266
+ `Bootstrap execute-safe completed with non-zero exit (${result.exitCode}). ` +
267
+ `Check executed_steps for partial results.`,
268
+ {
269
+ exitCode: result.exitCode,
270
+ stderr: result.stderr,
271
+ result: parsed,
272
+ },
273
+ );
274
+ }
275
+
276
+ return toolResponse(
277
+ `Bootstrap execute-safe complete for recipe '${recipe}'.${dryRun ? " (dry-run)" : ""}`,
278
+ parsed,
279
+ );
280
+ }
281
+
282
+ // ---------------------------------------------------------------------------
283
+ // Tool: atf_protect_intent
284
+ // ---------------------------------------------------------------------------
285
+
286
+ /**
287
+ * Protect a DeFi intent and return receipt + decision.
288
+ *
289
+ * @param {Record<string, unknown>} cfg
290
+ * @param {{ intentJson: object, exposureHints?: object }} params
291
+ */
292
+ async function toolAtfProtectIntent(cfg, params = {}) {
293
+ const { atfCli, prefer, atfBaseUrl } = cfg;
294
+ const { intentJson, exposureHints } = params;
295
+
296
+ if (!intentJson || typeof intentJson !== "object") {
297
+ return errorResponse(
298
+ "atf_protect_intent requires 'intentJson' (object) input.",
299
+ );
300
+ }
301
+
302
+ // Merge exposure hints into metadata if provided
303
+ const payload = { ...intentJson };
304
+ if (
305
+ exposureHints &&
306
+ typeof exposureHints === "object" &&
307
+ Object.keys(exposureHints).length > 0
308
+ ) {
309
+ payload.metadata = {
310
+ ...(payload.metadata ?? {}),
311
+ exposure_hints: exposureHints,
312
+ };
313
+ }
314
+
315
+ const payloadStr = JSON.stringify(payload);
316
+
317
+ if (prefer === "api" && atfBaseUrl) {
318
+ // HTTP API mode
319
+ let res;
320
+ try {
321
+ res = await fetch(`${atfBaseUrl}/v1/bot/protect`, {
322
+ method: "POST",
323
+ headers: {
324
+ "Content-Type": "application/json",
325
+ "User-Agent": "openclaw-atf-plugin/0.1.0",
326
+ },
327
+ body: payloadStr,
328
+ signal: AbortSignal.timeout(15_000),
329
+ });
330
+ } catch (err) {
331
+ return errorResponse(`ATF API protect call failed: ${err.message}`);
332
+ }
333
+
334
+ let data;
335
+ try {
336
+ data = await res.json();
337
+ } catch {
338
+ return errorResponse(
339
+ `ATF API protect response not JSON (HTTP ${res.status}).`,
340
+ );
341
+ }
342
+
343
+ if (!res.ok) {
344
+ return errorResponse(
345
+ `ATF API protect HTTP ${res.status}: ${JSON.stringify(data)}`,
346
+ );
347
+ }
348
+
349
+ return toolResponse(
350
+ `ATF protect decision: ${data.allow ? "ALLOW" : "DENY"}.` +
351
+ (data.reason_codes?.length
352
+ ? ` Reason codes: ${data.reason_codes.join(", ")}.`
353
+ : ""),
354
+ data,
355
+ );
356
+ }
357
+
358
+ // CLI mode: pass JSON via stdin
359
+ const { execFile: execFileRaw } = await import("node:child_process");
360
+ const decision = await new Promise((resolve) => {
361
+ const child = execFileRaw(
362
+ atfCli,
363
+ ["bot", "protect", "--format", "json", "--stdin"],
364
+ { timeout: 30_000, maxBuffer: 4 * 1024 * 1024, shell: false },
365
+ (err, stdout, stderr) => {
366
+ resolve({
367
+ stdout: stdout ?? "",
368
+ stderr: stderr ?? "",
369
+ exitCode: err ? (err.code ?? 1) : 0,
370
+ });
371
+ },
372
+ );
373
+ child.stdin.write(payloadStr);
374
+ child.stdin.end();
375
+ });
376
+
377
+ const parsed = tryParseJson(decision.stdout);
378
+ const allow =
379
+ typeof parsed === "object" && parsed !== null ? parsed.allow : null;
380
+
381
+ if (decision.exitCode !== 0 && typeof parsed !== "object") {
382
+ return errorResponse(
383
+ `ATF protect failed (exit ${decision.exitCode}).\n` +
384
+ `stderr: ${decision.stderr}\nstdout: ${decision.stdout}`,
385
+ );
386
+ }
387
+
388
+ return toolResponse(
389
+ `ATF protect decision: ${allow === true ? "ALLOW" : allow === false ? "DENY" : "see json"}.` +
390
+ (typeof parsed === "object" && parsed?.reason_codes?.length
391
+ ? ` Reason codes: ${parsed.reason_codes.join(", ")}.`
392
+ : ""),
393
+ parsed,
394
+ );
395
+ }
396
+
397
+ // ---------------------------------------------------------------------------
398
+ // Tool: atf_verify_receipt
399
+ // ---------------------------------------------------------------------------
400
+
401
+ /**
402
+ * Verify a receipt deterministically.
403
+ *
404
+ * @param {Record<string, unknown>} cfg
405
+ * @param {{ receipt: string|object }} params
406
+ */
407
+ async function toolAtfVerifyReceipt(cfg, params = {}) {
408
+ const { atfCli } = cfg;
409
+ const { receipt } = params;
410
+
411
+ if (!receipt) {
412
+ return errorResponse(
413
+ "atf_verify_receipt requires 'receipt' param " +
414
+ "(receipt JSON string, object, or file path).",
415
+ );
416
+ }
417
+
418
+ // If receipt is an object or looks like JSON, pass via stdin
419
+ const receiptStr =
420
+ typeof receipt === "object"
421
+ ? JSON.stringify(receipt)
422
+ : String(receipt).trim();
423
+
424
+ const isFilePath =
425
+ typeof receipt === "string" &&
426
+ !receiptStr.startsWith("{") &&
427
+ !receiptStr.startsWith("[");
428
+
429
+ let result;
430
+ if (isFilePath) {
431
+ // File path mode
432
+ result = await runAtfCli(atfCli, [
433
+ "receipts",
434
+ "verify",
435
+ "--format",
436
+ "json",
437
+ "--receipt",
438
+ receiptStr,
439
+ ]);
440
+ } else {
441
+ // Stdin mode
442
+ const { execFile: execFileRaw } = await import("node:child_process");
443
+ result = await new Promise((resolve) => {
444
+ const child = execFileRaw(
445
+ atfCli,
446
+ ["receipts", "verify", "--format", "json", "--stdin"],
447
+ { timeout: 30_000, maxBuffer: 4 * 1024 * 1024, shell: false },
448
+ (err, stdout, stderr) => {
449
+ resolve({
450
+ stdout: stdout ?? "",
451
+ stderr: stderr ?? "",
452
+ exitCode: err ? (err.code ?? 1) : 0,
453
+ });
454
+ },
455
+ );
456
+ child.stdin.write(receiptStr);
457
+ child.stdin.end();
458
+ });
459
+ }
460
+
461
+ const parsed = tryParseJson(result.stdout);
462
+
463
+ if (result.exitCode !== 0) {
464
+ return errorResponse(
465
+ `ATF verify failed (exit ${result.exitCode}).\n` +
466
+ `stderr: ${result.stderr}\nstdout: ${result.stdout}`,
467
+ );
468
+ }
469
+
470
+ const verified =
471
+ typeof parsed === "object" && parsed !== null ? parsed.verified : null;
472
+
473
+ return toolResponse(
474
+ `Receipt verification: ${verified === true ? "VERIFIED" : verified === false ? "FAILED" : "see json"}.`,
475
+ parsed,
476
+ );
477
+ }
478
+
479
+ // ---------------------------------------------------------------------------
480
+ // Tool: atf_report_savings
481
+ // ---------------------------------------------------------------------------
482
+
483
+ /**
484
+ * Generate a receipt-backed "ATF saved you" report.
485
+ *
486
+ * @param {Record<string, unknown>} cfg
487
+ * @param {{ last?: number, receiptsDir?: string }} params
488
+ */
489
+ async function toolAtfReportSavings(cfg, params = {}) {
490
+ const { atfCli } = cfg;
491
+ const receiptsDir = params.receiptsDir ?? cfg.receiptsDir;
492
+ const last = params.last ?? 50;
493
+
494
+ const args = ["report", "savings", "--format", "json", "--last", String(last)];
495
+ if (receiptsDir) {
496
+ args.push("--receipts-dir", receiptsDir);
497
+ }
498
+
499
+ const result = await runAtfCli(atfCli, args);
500
+ const parsed = tryParseJson(result.stdout);
501
+
502
+ if (result.exitCode !== 0) {
503
+ return errorResponse(
504
+ `ATF report savings failed (exit ${result.exitCode}).\n` +
505
+ `stderr: ${result.stderr}\nstdout: ${result.stdout}`,
506
+ );
507
+ }
508
+
509
+ const summary = buildHumanSummary(
510
+ typeof parsed === "object" ? parsed : {},
511
+ );
512
+
513
+ return toolResponse(summary, parsed);
514
+ }
515
+
516
+ // ---------------------------------------------------------------------------
517
+ // Human messaging helper
518
+ // ---------------------------------------------------------------------------
519
+
520
+ /**
521
+ * Build a human-readable "ATF saved you" summary from a savings report JSON.
522
+ * Conservative: only includes $ estimate if hints are present.
523
+ *
524
+ * @param {Record<string, unknown>} reportJson - Output from `atf report savings --format json`
525
+ * @returns {string}
526
+ */
527
+ export function buildHumanSummary(reportJson) {
528
+ if (!reportJson || typeof reportJson !== "object") {
529
+ return "ATF savings report: no data available.";
530
+ }
531
+
532
+ const lines = ["=== ATF Protection Summary ===", ""];
533
+
534
+ // Denied actions
535
+ const totalDenied =
536
+ reportJson.total_denied ??
537
+ reportJson.deny_count ??
538
+ reportJson.denied_count ??
539
+ null;
540
+ if (totalDenied !== null) {
541
+ lines.push(
542
+ `ATF prevented ${totalDenied} risky action${totalDenied !== 1 ? "s" : ""}.`,
543
+ );
544
+ } else {
545
+ lines.push("ATF protection active.");
546
+ }
547
+
548
+ // Top reason codes
549
+ const reasons =
550
+ reportJson.by_reason_code ??
551
+ reportJson.reason_code_breakdown ??
552
+ reportJson.reasons ??
553
+ null;
554
+ if (reasons && typeof reasons === "object" && !Array.isArray(reasons)) {
555
+ const sorted = Object.entries(reasons)
556
+ .sort((a, b) => Number(b[1]) - Number(a[1]))
557
+ .slice(0, 5);
558
+ if (sorted.length > 0) {
559
+ lines.push("");
560
+ lines.push("Top deny reasons:");
561
+ for (const [code, count] of sorted) {
562
+ lines.push(` • ${code}: ${count} time${count !== 1 ? "s" : ""}`);
563
+ }
564
+ }
565
+ } else if (Array.isArray(reasons) && reasons.length > 0) {
566
+ lines.push("");
567
+ lines.push("Top deny reasons:");
568
+ for (const entry of reasons.slice(0, 5)) {
569
+ const code = entry.code ?? entry.reason_code ?? entry.name ?? "UNKNOWN";
570
+ const count = entry.count ?? entry.occurrences ?? 1;
571
+ lines.push(` • ${code}: ${count} time${count !== 1 ? "s" : ""}`);
572
+ }
573
+ }
574
+
575
+ // Evidence: receipt hashes
576
+ const evidence =
577
+ reportJson.evidence ??
578
+ reportJson.receipts ??
579
+ reportJson.receipt_hashes ??
580
+ null;
581
+ if (Array.isArray(evidence) && evidence.length > 0) {
582
+ lines.push("");
583
+ lines.push(
584
+ `Evidence: ${evidence.length} verified receipt${evidence.length !== 1 ? "s" : ""}.`,
585
+ );
586
+ const hashes = evidence
587
+ .map((e) => {
588
+ if (typeof e === "string") return e;
589
+ return (
590
+ e.content_hash ??
591
+ e.receipt_hash ??
592
+ e.hash ??
593
+ e.receipt_id ??
594
+ null
595
+ );
596
+ })
597
+ .filter(Boolean)
598
+ .slice(0, 10);
599
+ if (hashes.length > 0) {
600
+ for (const h of hashes) {
601
+ lines.push(` receipt: ${h}`);
602
+ }
603
+ if (evidence.length > 10) {
604
+ lines.push(` ... and ${evidence.length - 10} more.`);
605
+ }
606
+ }
607
+ }
608
+
609
+ // Conservative $ savings estimate (ONLY if hints present in report)
610
+ const savingsEstimate =
611
+ reportJson.savings_estimate_usd ??
612
+ reportJson.estimated_savings_usd ??
613
+ reportJson.conservative_savings_usd ??
614
+ null;
615
+ const hasHints =
616
+ reportJson.has_exposure_hints === true ||
617
+ reportJson.notional_hints_present === true ||
618
+ savingsEstimate !== null;
619
+
620
+ if (savingsEstimate !== null && hasHints) {
621
+ lines.push("");
622
+ const formatted =
623
+ typeof savingsEstimate === "number"
624
+ ? `$${savingsEstimate.toLocaleString("en-US", { maximumFractionDigits: 2 })}`
625
+ : String(savingsEstimate);
626
+ lines.push(
627
+ `Conservative savings estimate: ${formatted}`,
628
+ );
629
+ lines.push(
630
+ " (Assumptions: estimate is based on exposure_hints " +
631
+ "(notional_usd, leverage, slippage_bps) provided at protect time. " +
632
+ "Actual outcomes may differ.)",
633
+ );
634
+ } else if (!hasHints && totalDenied && Number(totalDenied) > 0) {
635
+ lines.push("");
636
+ lines.push(
637
+ "No savings estimate available. " +
638
+ "To enable estimates, include exposure_hints " +
639
+ "(notional_usd, leverage, slippage_bps) " +
640
+ "in the intentJson when calling atf_protect_intent.",
641
+ );
642
+ }
643
+
644
+ lines.push("");
645
+ lines.push(
646
+ "Verify receipts: atf receipts verify --receipt <hash>",
647
+ );
648
+
649
+ return lines.join("\n");
650
+ }
651
+
652
+ // ---------------------------------------------------------------------------
653
+ // Plugin entry-point
654
+ // ---------------------------------------------------------------------------
655
+
656
+ /**
657
+ * OpenClaw plugin entry-point.
658
+ *
659
+ * Called by the OpenClaw runtime when the plugin is loaded.
660
+ * All tools are registered as OPTIONAL — agents / operators must allowlist
661
+ * them before they can be used.
662
+ *
663
+ * @param {{ registerTool: Function, config: Record<string, unknown> }} api
664
+ */
665
+ export default function atfPlugin(api) {
666
+ const cfg = resolveConfig(api?.config ?? {});
667
+
668
+ // ---- Tool 1: atf_discover -----------------------------------------------
669
+ api.registerTool({
670
+ name: "atf_discover",
671
+ optional: true,
672
+ description:
673
+ "Fetch and summarise the ATF manifest and toolcard so you know ATF's " +
674
+ "capabilities, recipe IDs, feature gates, and integration steps. " +
675
+ "Requires safety.allowNetwork=true to fetch live; otherwise returns " +
676
+ "offline instructions.",
677
+ inputSchema: {
678
+ type: "object",
679
+ properties: {
680
+ manifestUrl: {
681
+ type: "string",
682
+ description:
683
+ "Override manifest URL (default: https://trucore.xyz/.well-known/atf.json).",
684
+ },
685
+ },
686
+ additionalProperties: false,
687
+ },
688
+ handler: (params) => toolAtfDiscover(cfg, params),
689
+ });
690
+
691
+ // ---- Tool 2: atf_bootstrap_plan -----------------------------------------
692
+ api.registerTool({
693
+ name: "atf_bootstrap_plan",
694
+ optional: true,
695
+ description:
696
+ "Generate ATF bootstrap steps for a given recipe. " +
697
+ "Plan-only — no side effects. " +
698
+ "Returns the steps, env flags, and verification commands the agent should follow.",
699
+ inputSchema: {
700
+ type: "object",
701
+ properties: {
702
+ recipe: {
703
+ type: "string",
704
+ default: "bootstrap_local",
705
+ description:
706
+ "Recipe ID to plan. One of: bootstrap_local, enable_perps_drift, " +
707
+ "enable_perps_mango, enable_perps_hyperliquid.",
708
+ },
709
+ },
710
+ additionalProperties: false,
711
+ },
712
+ handler: (params) => toolAtfBootstrapPlan(cfg, params),
713
+ });
714
+
715
+ // ---- Tool 3: atf_bootstrap_execute_safe ---------------------------------
716
+ api.registerTool({
717
+ name: "atf_bootstrap_execute_safe",
718
+ optional: true,
719
+ description:
720
+ "Run ATF bootstrap --execute-safe steps (env setup and verification only). " +
721
+ "Never requires signing or network access. " +
722
+ "Disabled if safety.allowExecuteSafe=false. Errors are non-fatal.",
723
+ inputSchema: {
724
+ type: "object",
725
+ properties: {
726
+ recipe: {
727
+ type: "string",
728
+ default: "bootstrap_local",
729
+ description: "Recipe ID to execute.",
730
+ },
731
+ dryRun: {
732
+ type: "boolean",
733
+ default: false,
734
+ description: "Preview steps without executing.",
735
+ },
736
+ },
737
+ additionalProperties: false,
738
+ },
739
+ handler: (params) => toolAtfBootstrapExecuteSafe(cfg, params),
740
+ });
741
+
742
+ // ---- Tool 4: atf_protect_intent -----------------------------------------
743
+ api.registerTool({
744
+ name: "atf_protect_intent",
745
+ optional: true,
746
+ description:
747
+ "Submit a DeFi intent (swap, lend, perps) to ATF for policy evaluation. " +
748
+ "Returns allow/deny decision, reason_codes, and a tamper-evident receipt. " +
749
+ "ATF never signs transactions.",
750
+ inputSchema: {
751
+ type: "object",
752
+ required: ["intentJson"],
753
+ properties: {
754
+ intentJson: {
755
+ type: "object",
756
+ description:
757
+ "Full ATF ExecutionRequest or intent object. " +
758
+ "Fields: chain_id, intent_type, raw_tx (optional), intent (object), metadata (optional).",
759
+ },
760
+ exposureHints: {
761
+ type: "object",
762
+ description:
763
+ "Optional exposure hints for savings reporting. " +
764
+ "Include notional_usd, leverage, and/or slippage_bps to enable conservative savings estimates.",
765
+ properties: {
766
+ notional_usd: { type: "number" },
767
+ leverage: { type: "number" },
768
+ slippage_bps: { type: "integer" },
769
+ },
770
+ additionalProperties: true,
771
+ },
772
+ },
773
+ additionalProperties: false,
774
+ },
775
+ handler: (params) => toolAtfProtectIntent(cfg, params),
776
+ });
777
+
778
+ // ---- Tool 5: atf_verify_receipt -----------------------------------------
779
+ api.registerTool({
780
+ name: "atf_verify_receipt",
781
+ optional: true,
782
+ description:
783
+ "Verify an ATF receipt deterministically. " +
784
+ "Returns verified=true/false, content_hash, and intent_hash. " +
785
+ "Use to prove to humans that a transaction was evaluated.",
786
+ inputSchema: {
787
+ type: "object",
788
+ required: ["receipt"],
789
+ properties: {
790
+ receipt: {
791
+ description:
792
+ "Receipt to verify: JSON object, JSON string, or file path.",
793
+ },
794
+ },
795
+ additionalProperties: false,
796
+ },
797
+ handler: (params) => toolAtfVerifyReceipt(cfg, params),
798
+ });
799
+
800
+ // ---- Tool 6: atf_report_savings -----------------------------------------
801
+ api.registerTool({
802
+ name: "atf_report_savings",
803
+ optional: true,
804
+ description:
805
+ "Generate a receipt-backed 'ATF saved you money / prevented losses' report. " +
806
+ "Includes deny counts by reason code, evidence receipt hashes, " +
807
+ "and a conservative $ estimate only when exposure hints are present.",
808
+ inputSchema: {
809
+ type: "object",
810
+ properties: {
811
+ last: {
812
+ type: "integer",
813
+ default: 50,
814
+ description: "Number of most recent receipts to include.",
815
+ },
816
+ receiptsDir: {
817
+ type: "string",
818
+ description:
819
+ "Directory containing ATF receipt JSON files. " +
820
+ "Defaults to config.receiptsDir.",
821
+ },
822
+ },
823
+ additionalProperties: false,
824
+ },
825
+ handler: (params) => toolAtfReportSavings(cfg, params),
826
+ });
827
+ }