@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 +206 -0
- package/openclaw.plugin.json +51 -0
- package/package.json +55 -0
- package/src/index.mjs +827 -0
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
|
+
}
|