@trucore/openclaw-atf 0.1.1 → 0.2.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/src/index.mjs CHANGED
@@ -1,13 +1,44 @@
1
1
  /**
2
2
  * @trucore/openclaw-atf — OpenClaw plugin: ATF agent tools
3
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_receiptverify a receipt deterministically
10
- * 6. atf_report_savingsgenerate receipt-backed "ATF saved you" report
4
+ * Registers OPTIONAL tools for OpenClaw agents:
5
+ * 1. atf_health check ATF CLI / API availability
6
+ * 2. atf_discover fetch/summarize ATF manifest
7
+ * 3. atf_bootstrap_plan generate bootstrap steps (plan only, no side effects)
8
+ * 4. atf_bootstrap_execute_safe run safe bootstrap steps locally
9
+ * 5. atf_protect_intentprotect a DeFi intent and return receipt + decision
10
+ * 6. atf_verify_receiptverify a receipt deterministically
11
+ * 7. atf_report_savings — generate receipt-backed "ATF saved you" report
12
+ * 8. atf_integration_doctor — integration readiness / troubleshooting
13
+ * 9. atf_bot_preflight — pre-session readiness check (go/no-go)
14
+ * 10. atf_tx_explain — explain a receipt or deny decision in human terms
15
+ * 11. atf_billing_info — billing/pricing metadata for advanced bot package
16
+ * 12. atf_adoption_advisor — deterministic adoption recommendation for bots
17
+ * 13. atf_billing_claim — verify on-chain payment and process billing claim
18
+ *
19
+ * Exports:
20
+ * - default: atfPlugin(api) — legacy single-function entry
21
+ * - register(api) — OpenClaw lifecycle: register tools
22
+ * - activate(api) — OpenClaw lifecycle: activate plugin (config validation)
23
+ * - deactivate() — OpenClaw lifecycle: deactivate plugin
24
+ * - PLUGIN_ID — canonical plugin identity constant
25
+ * - PLUGIN_VERSION — plugin version constant
26
+ * - buildHumanSummary(reportJson) — human messaging helper
27
+ * - REASON_CODE_CATALOG — deny reason code explanations
28
+ * - ATF_BILLING_MANIFEST — machine-readable billing metadata
29
+ * - evaluateAdoption — adoption advisor evaluation function
30
+ * - TRIGGER_MODEL — frozen trigger definitions
31
+ * - INSTALL_GUIDANCE — frozen install/enable guidance
32
+ * - SAFE_CALL_SEQUENCE — frozen default call sequence
33
+ * - verifyBillingClaim — billing claim verification function
34
+ * - CLAIM_STATUS — claim status enum
35
+ * - CLAIM_DENY_CODES — claim deny code catalog
36
+ * - TREASURY_ADDRESS — canonical treasury address
37
+ * - USDC_MINT — canonical USDC mint address
38
+ * - CONTRACT_FAMILIES — universal contract family definitions
39
+ * - TOOL_FAMILY_MAP — tool → contract family mapping
40
+ * - CANONICAL_TOOLS — frozen list of all tool names
41
+ * - CANONICAL_TOOL_COUNT — number of canonical tools
11
42
  *
12
43
  * Uses ONLY built-in Node modules: child_process, fs, path, os, url.
13
44
  * No network calls unless config.safety.allowNetwork = true.
@@ -17,8 +48,59 @@
17
48
  import { execFile } from "node:child_process";
18
49
  import { promisify } from "node:util";
19
50
 
51
+ // Config validation — used by activate() for non-blocking warnings.
52
+ // Imported as a module reference so activate() can call it synchronously.
53
+ import * as _configModule from "./config.mjs";
54
+
55
+ // Adoption advisor — deterministic trigger evaluation and recommendation.
56
+ import {
57
+ evaluateAdoption,
58
+ TRIGGER_MODEL,
59
+ INSTALL_GUIDANCE,
60
+ SAFE_CALL_SEQUENCE,
61
+ } from "./adoption_advisor.mjs";
62
+
63
+ // Billing claim verification — deterministic on-chain payment verification.
64
+ import {
65
+ verifyBillingClaim,
66
+ CLAIM_STATUS,
67
+ CLAIM_DENY_CODES,
68
+ TREASURY_ADDRESS,
69
+ USDC_MINT,
70
+ KNOWN_PACKAGES,
71
+ checkDuplicate,
72
+ _resetClaimTracking,
73
+ } from "./billing_claim.mjs";
74
+
75
+ // Standardized response helpers — OpenClaw adapter envelope builders.
76
+ import {
77
+ nativeToolSuccess,
78
+ nativeToolError,
79
+ RESPONSE_STATUS,
80
+ } from "./tool_response.mjs";
81
+
82
+ // Universal contract layer — canonical schemas, codes, and result builders.
83
+ // Re-exported so consumers can access the universal layer through the plugin.
84
+ import {
85
+ REASON_CODE_CATALOG as _REASON_CODE_CATALOG,
86
+ CONTRACT_FAMILIES,
87
+ TOOL_FAMILY_MAP,
88
+ CANONICAL_TOOLS,
89
+ CANONICAL_TOOL_COUNT,
90
+ } from "./contracts/index.mjs";
91
+
20
92
  const execFileAsync = promisify(execFile);
21
93
 
94
+ // ---------------------------------------------------------------------------
95
+ // Canonical plugin identity — use ONLY these constants everywhere
96
+ // ---------------------------------------------------------------------------
97
+
98
+ /** Canonical plugin ID. Must match openclaw.plugin.json `id` field. */
99
+ export const PLUGIN_ID = "trucore-atf";
100
+
101
+ /** Plugin version. Must match package.json + openclaw.plugin.json `version`. */
102
+ export const PLUGIN_VERSION = "0.2.0";
103
+
22
104
  // ---------------------------------------------------------------------------
23
105
  // Default config
24
106
  // ---------------------------------------------------------------------------
@@ -89,33 +171,6 @@ function tryParseJson(raw) {
89
171
  }
90
172
  }
91
173
 
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
174
  // ---------------------------------------------------------------------------
120
175
  // Tool: atf_discover
121
176
  // ---------------------------------------------------------------------------
@@ -132,7 +187,8 @@ async function toolAtfDiscover(cfg, params = {}) {
132
187
  const { safety, atfBaseUrl } = cfg;
133
188
 
134
189
  if (!safety.allowNetwork) {
135
- return toolResponse(
190
+ return nativeToolSuccess(
191
+ "atf_discover",
136
192
  "ATF discovery requires network access.",
137
193
  {
138
194
  instructions:
@@ -150,6 +206,7 @@ async function toolAtfDiscover(cfg, params = {}) {
150
206
  toolcard_url:
151
207
  "https://raw.githubusercontent.com/trucore-ai/agent-transaction-firewall/main/docs/agent/atf_toolcard.json",
152
208
  },
209
+ { status: RESPONSE_STATUS.UNAVAILABLE },
153
210
  );
154
211
  }
155
212
 
@@ -161,20 +218,24 @@ async function toolAtfDiscover(cfg, params = {}) {
161
218
  let manifest;
162
219
  try {
163
220
  const res = await fetch(base, {
164
- headers: { "User-Agent": "openclaw-atf-plugin/0.1.0" },
221
+ headers: { "User-Agent": "openclaw-atf-plugin/0.2.0" },
165
222
  signal: AbortSignal.timeout(10_000),
166
223
  });
167
224
  if (!res.ok) {
168
- return errorResponse(
225
+ return nativeToolError(
226
+ "atf_discover",
169
227
  `ATF manifest fetch failed: HTTP ${res.status} from ${base}`,
170
228
  );
171
229
  }
172
230
  manifest = await res.json();
173
231
  } catch (err) {
174
- return errorResponse(`ATF manifest fetch error: ${err.message}`);
232
+ return nativeToolError(
233
+ "atf_discover",
234
+ `ATF manifest fetch error: ${err.message}`,
235
+ );
175
236
  }
176
237
 
177
- const summary = {
238
+ const manifestSummary = {
178
239
  id: manifest.id ?? manifest.product ?? "trucore-atf",
179
240
  version: manifest.version ?? "unknown",
180
241
  capabilities: manifest.capabilities ?? [],
@@ -185,9 +246,10 @@ async function toolAtfDiscover(cfg, params = {}) {
185
246
  trust_signals: manifest.trust_signals ?? {},
186
247
  };
187
248
 
188
- return toolResponse(
249
+ return nativeToolSuccess(
250
+ "atf_discover",
189
251
  "ATF manifest fetched. Key fields summarised below.",
190
- summary,
252
+ manifestSummary,
191
253
  );
192
254
  }
193
255
 
@@ -209,16 +271,18 @@ async function toolAtfBootstrapPlan(cfg, params = {}) {
209
271
  const result = await runAtfCli(atfCli, args);
210
272
 
211
273
  if (result.exitCode !== 0) {
212
- return errorResponse(
274
+ return nativeToolError(
275
+ "atf_bootstrap_plan",
213
276
  `atf bootstrap plan failed (exit ${result.exitCode}).\n` +
214
277
  `stderr: ${result.stderr}\nstdout: ${result.stdout}`,
215
278
  );
216
279
  }
217
280
 
218
281
  const parsed = tryParseJson(result.stdout);
219
- return toolResponse(
282
+ return nativeToolSuccess(
283
+ "atf_bootstrap_plan",
220
284
  `Bootstrap plan for recipe '${recipe}' (plan only — no steps executed).`,
221
- parsed,
285
+ typeof parsed === "object" && parsed !== null ? parsed : { raw: parsed },
222
286
  );
223
287
  }
224
288
 
@@ -237,9 +301,11 @@ async function toolAtfBootstrapExecuteSafe(cfg, params = {}) {
237
301
  const { atfCli, safety } = cfg;
238
302
 
239
303
  if (!safety.allowExecuteSafe) {
240
- return errorResponse(
304
+ return nativeToolError(
305
+ "atf_bootstrap_execute_safe",
241
306
  "atf_bootstrap_execute_safe is disabled. " +
242
307
  "Set safety.allowExecuteSafe = true in the plugin config to enable.",
308
+ { remediation: ["Set safety.allowExecuteSafe = true in plugin config."] },
243
309
  );
244
310
  }
245
311
 
@@ -261,8 +327,9 @@ async function toolAtfBootstrapExecuteSafe(cfg, params = {}) {
261
327
  const parsed = tryParseJson(result.stdout);
262
328
 
263
329
  if (result.exitCode !== 0) {
264
- // Non-fatal: return partial result + stderr
265
- return toolResponse(
330
+ // Non-fatal: return partial result + stderr with degraded status
331
+ return nativeToolSuccess(
332
+ "atf_bootstrap_execute_safe",
266
333
  `Bootstrap execute-safe completed with non-zero exit (${result.exitCode}). ` +
267
334
  `Check executed_steps for partial results.`,
268
335
  {
@@ -270,12 +337,14 @@ async function toolAtfBootstrapExecuteSafe(cfg, params = {}) {
270
337
  stderr: result.stderr,
271
338
  result: parsed,
272
339
  },
340
+ { status: RESPONSE_STATUS.DEGRADED },
273
341
  );
274
342
  }
275
343
 
276
- return toolResponse(
344
+ return nativeToolSuccess(
345
+ "atf_bootstrap_execute_safe",
277
346
  `Bootstrap execute-safe complete for recipe '${recipe}'.${dryRun ? " (dry-run)" : ""}`,
278
- parsed,
347
+ typeof parsed === "object" && parsed !== null ? parsed : { raw: parsed },
279
348
  );
280
349
  }
281
350
 
@@ -294,8 +363,10 @@ async function toolAtfProtectIntent(cfg, params = {}) {
294
363
  const { intentJson, exposureHints } = params;
295
364
 
296
365
  if (!intentJson || typeof intentJson !== "object") {
297
- return errorResponse(
366
+ return nativeToolError(
367
+ "atf_protect_intent",
298
368
  "atf_protect_intent requires 'intentJson' (object) input.",
369
+ { remediation: ["Provide an intentJson object with chain_id, intent_type, and intent fields."] },
299
370
  );
300
371
  }
301
372
 
@@ -322,31 +393,37 @@ async function toolAtfProtectIntent(cfg, params = {}) {
322
393
  method: "POST",
323
394
  headers: {
324
395
  "Content-Type": "application/json",
325
- "User-Agent": "openclaw-atf-plugin/0.1.0",
396
+ "User-Agent": "openclaw-atf-plugin/0.2.0",
326
397
  },
327
398
  body: payloadStr,
328
399
  signal: AbortSignal.timeout(15_000),
329
400
  });
330
401
  } catch (err) {
331
- return errorResponse(`ATF API protect call failed: ${err.message}`);
402
+ return nativeToolError(
403
+ "atf_protect_intent",
404
+ `ATF API protect call failed: ${err.message}`,
405
+ );
332
406
  }
333
407
 
334
408
  let data;
335
409
  try {
336
410
  data = await res.json();
337
411
  } catch {
338
- return errorResponse(
412
+ return nativeToolError(
413
+ "atf_protect_intent",
339
414
  `ATF API protect response not JSON (HTTP ${res.status}).`,
340
415
  );
341
416
  }
342
417
 
343
418
  if (!res.ok) {
344
- return errorResponse(
419
+ return nativeToolError(
420
+ "atf_protect_intent",
345
421
  `ATF API protect HTTP ${res.status}: ${JSON.stringify(data)}`,
346
422
  );
347
423
  }
348
424
 
349
- return toolResponse(
425
+ return nativeToolSuccess(
426
+ "atf_protect_intent",
350
427
  `ATF protect decision: ${data.allow ? "ALLOW" : "DENY"}.` +
351
428
  (data.reason_codes?.length
352
429
  ? ` Reason codes: ${data.reason_codes.join(", ")}.`
@@ -379,18 +456,20 @@ async function toolAtfProtectIntent(cfg, params = {}) {
379
456
  typeof parsed === "object" && parsed !== null ? parsed.allow : null;
380
457
 
381
458
  if (decision.exitCode !== 0 && typeof parsed !== "object") {
382
- return errorResponse(
459
+ return nativeToolError(
460
+ "atf_protect_intent",
383
461
  `ATF protect failed (exit ${decision.exitCode}).\n` +
384
462
  `stderr: ${decision.stderr}\nstdout: ${decision.stdout}`,
385
463
  );
386
464
  }
387
465
 
388
- return toolResponse(
466
+ return nativeToolSuccess(
467
+ "atf_protect_intent",
389
468
  `ATF protect decision: ${allow === true ? "ALLOW" : allow === false ? "DENY" : "see json"}.` +
390
469
  (typeof parsed === "object" && parsed?.reason_codes?.length
391
470
  ? ` Reason codes: ${parsed.reason_codes.join(", ")}.`
392
471
  : ""),
393
- parsed,
472
+ typeof parsed === "object" && parsed !== null ? parsed : { raw: parsed },
394
473
  );
395
474
  }
396
475
 
@@ -409,9 +488,11 @@ async function toolAtfVerifyReceipt(cfg, params = {}) {
409
488
  const { receipt } = params;
410
489
 
411
490
  if (!receipt) {
412
- return errorResponse(
491
+ return nativeToolError(
492
+ "atf_verify_receipt",
413
493
  "atf_verify_receipt requires 'receipt' param " +
414
494
  "(receipt JSON string, object, or file path).",
495
+ { remediation: ["Provide a receipt JSON object, JSON string, or file path."] },
415
496
  );
416
497
  }
417
498
 
@@ -461,7 +542,8 @@ async function toolAtfVerifyReceipt(cfg, params = {}) {
461
542
  const parsed = tryParseJson(result.stdout);
462
543
 
463
544
  if (result.exitCode !== 0) {
464
- return errorResponse(
545
+ return nativeToolError(
546
+ "atf_verify_receipt",
465
547
  `ATF verify failed (exit ${result.exitCode}).\n` +
466
548
  `stderr: ${result.stderr}\nstdout: ${result.stdout}`,
467
549
  );
@@ -470,9 +552,10 @@ async function toolAtfVerifyReceipt(cfg, params = {}) {
470
552
  const verified =
471
553
  typeof parsed === "object" && parsed !== null ? parsed.verified : null;
472
554
 
473
- return toolResponse(
555
+ return nativeToolSuccess(
556
+ "atf_verify_receipt",
474
557
  `Receipt verification: ${verified === true ? "VERIFIED" : verified === false ? "FAILED" : "see json"}.`,
475
- parsed,
558
+ typeof parsed === "object" && parsed !== null ? parsed : { raw: parsed },
476
559
  );
477
560
  }
478
561
 
@@ -500,7 +583,8 @@ async function toolAtfReportSavings(cfg, params = {}) {
500
583
  const parsed = tryParseJson(result.stdout);
501
584
 
502
585
  if (result.exitCode !== 0) {
503
- return errorResponse(
586
+ return nativeToolError(
587
+ "atf_report_savings",
504
588
  `ATF report savings failed (exit ${result.exitCode}).\n` +
505
589
  `stderr: ${result.stderr}\nstdout: ${result.stdout}`,
506
590
  );
@@ -510,7 +594,11 @@ async function toolAtfReportSavings(cfg, params = {}) {
510
594
  typeof parsed === "object" ? parsed : {},
511
595
  );
512
596
 
513
- return toolResponse(summary, parsed);
597
+ return nativeToolSuccess(
598
+ "atf_report_savings",
599
+ summary,
600
+ typeof parsed === "object" && parsed !== null ? parsed : { raw: parsed },
601
+ );
514
602
  }
515
603
 
516
604
  // ---------------------------------------------------------------------------
@@ -649,6 +737,678 @@ export function buildHumanSummary(reportJson) {
649
737
  return lines.join("\n");
650
738
  }
651
739
 
740
+ // ---------------------------------------------------------------------------
741
+ // Tool: atf_health — lightweight availability check
742
+ // ---------------------------------------------------------------------------
743
+
744
+ /**
745
+ * Check whether the ATF CLI or API backend is reachable.
746
+ * Never throws. Returns structured health status with backend resolution.
747
+ * Uses standardized nativeToolSuccess/nativeToolError response shapes.
748
+ *
749
+ * @param {Record<string, unknown>} cfg Resolved config.
750
+ * @returns {Promise<object>}
751
+ */
752
+ async function toolAtfHealth(cfg) {
753
+ const { atfCli, prefer, atfBaseUrl } = cfg;
754
+
755
+ // Use shared backend probes
756
+ const { probeCliAvailable, probeApiAvailable, resolveBackend } =
757
+ await import("./backend.mjs");
758
+
759
+ const cli = await probeCliAvailable(atfCli);
760
+ const api = await probeApiAvailable(atfBaseUrl);
761
+ const backend = await resolveBackend(cfg, { cli, api });
762
+
763
+ const anyAvailable = cli.available === true || api.available === true;
764
+
765
+ const summary = anyAvailable
766
+ ? `ATF backend available (prefer=${prefer}, effective=${backend.effective_backend}).` +
767
+ (cli.available ? ` CLI: ${cli.version}.` : "") +
768
+ (api.available ? ` API: ${atfBaseUrl}.` : "") +
769
+ (backend.fallback_occurred ? ` [FALLBACK: ${backend.fallback_reason}]` : "")
770
+ : "ATF backend unavailable. CLI and API both unreachable. " +
771
+ "Tools will return errors until a backend is available.";
772
+
773
+ const status = anyAvailable
774
+ ? (backend.fallback_occurred ? RESPONSE_STATUS.DEGRADED : RESPONSE_STATUS.OK)
775
+ : RESPONSE_STATUS.UNAVAILABLE;
776
+
777
+ return nativeToolSuccess("atf_health", summary, {
778
+ healthy: anyAvailable,
779
+ cli,
780
+ api,
781
+ }, {
782
+ status,
783
+ backend,
784
+ warnings: backend.warnings,
785
+ });
786
+ }
787
+
788
+ // ---------------------------------------------------------------------------
789
+ // Tool: atf_integration_doctor — readiness / troubleshooting
790
+ // ---------------------------------------------------------------------------
791
+
792
+ /**
793
+ * Run the integration doctor and return a structured readiness report.
794
+ * Answers: Is the plugin loaded? CLI/API available? Tools registered?
795
+ * What should the operator do next?
796
+ * Uses standardized nativeToolSuccess response shape.
797
+ *
798
+ * @param {Record<string, unknown>} cfg Resolved config.
799
+ * @param {string[]} registeredToolNames Names of tools registered with this plugin.
800
+ * @returns {Promise<object>}
801
+ */
802
+ async function toolAtfIntegrationDoctor(cfg, registeredToolNames) {
803
+ const { runDoctor } = await import("./doctor.mjs");
804
+ const { resolveBackend, probeCliAvailable, probeApiAvailable } =
805
+ await import("./backend.mjs");
806
+
807
+ const report = await runDoctor(cfg, registeredToolNames);
808
+
809
+ // Resolve backend for meta
810
+ const cli = await probeCliAvailable(cfg?.atfCli ?? "atf");
811
+ const api = await probeApiAvailable(cfg?.atfBaseUrl);
812
+ const backend = await resolveBackend(cfg, { cli, api });
813
+
814
+ const statusLine = {
815
+ ok: "Integration healthy. All systems operational.",
816
+ degraded: "Integration degraded. Some capabilities limited — see warnings.",
817
+ misconfigured: "Integration misconfigured. Check warnings and remediation steps.",
818
+ unavailable: "Integration unavailable. No ATF backend reachable.",
819
+ }[report.status] ?? `Integration status: ${report.status}`;
820
+
821
+ const responseStatus = {
822
+ ok: RESPONSE_STATUS.OK,
823
+ degraded: RESPONSE_STATUS.DEGRADED,
824
+ misconfigured: RESPONSE_STATUS.ERROR,
825
+ unavailable: RESPONSE_STATUS.UNAVAILABLE,
826
+ }[report.status] ?? RESPONSE_STATUS.ERROR;
827
+
828
+ return nativeToolSuccess("atf_integration_doctor", statusLine, report, {
829
+ status: responseStatus,
830
+ backend,
831
+ warnings: report.warnings,
832
+ });
833
+ }
834
+
835
+ // ---------------------------------------------------------------------------
836
+ // Reason code catalog — sourced from universal contract layer
837
+ // ---------------------------------------------------------------------------
838
+
839
+ /**
840
+ * Canonical reason code catalog: re-exported from the universal contract
841
+ * layer (contracts/deny_codes.mjs). Used by atf_tx_explain and any future
842
+ * surface that needs to explain a deny decision.
843
+ *
844
+ * @type {Readonly<Record<string, {category: string, explanation: string, remediation: string}>>}
845
+ */
846
+ export const REASON_CODE_CATALOG = _REASON_CODE_CATALOG;
847
+
848
+
849
+ // ---------------------------------------------------------------------------
850
+ // Tool: atf_bot_preflight — pre-session readiness check
851
+ // ---------------------------------------------------------------------------
852
+
853
+ /**
854
+ * Lightweight pre-session readiness check. Returns a structured go/no-go
855
+ * signal based on backend availability and config validity. Thinner than
856
+ * the full integration doctor — designed for agents to call before starting
857
+ * a bot session.
858
+ *
859
+ * Uses shared backend resolution + config validation. Never throws.
860
+ *
861
+ * @param {Record<string, unknown>} cfg Resolved config.
862
+ * @param {Record<string, unknown>} [rawUserConfig] Original user config for validation.
863
+ * @returns {Promise<object>}
864
+ */
865
+ async function toolAtfBotPreflight(cfg, rawUserConfig) {
866
+ const { probeCliAvailable, probeApiAvailable, resolveBackend } =
867
+ await import("./backend.mjs");
868
+ const { validateConfig } = await import("./config.mjs");
869
+
870
+ const cli = await probeCliAvailable(cfg?.atfCli ?? "atf");
871
+ const api = await probeApiAvailable(cfg?.atfBaseUrl);
872
+ const backend = await resolveBackend(cfg, { cli, api });
873
+ const configResult = validateConfig(rawUserConfig ?? cfg);
874
+
875
+ const backendReady = backend.effective_backend !== "none";
876
+ const configValid = configResult.valid;
877
+ const ready = backendReady && configValid;
878
+
879
+ /** @type {string[]} */
880
+ const warnings = [...backend.warnings];
881
+ /** @type {string[]} */
882
+ const remediation = [];
883
+
884
+ if (!configValid) {
885
+ for (const err of configResult.errors) {
886
+ warnings.push(`Config: ${err}`);
887
+ }
888
+ for (const rem of configResult.remediation) {
889
+ remediation.push(rem);
890
+ }
891
+ }
892
+ for (const w of configResult.warnings) {
893
+ warnings.push(`Config: ${w}`);
894
+ }
895
+
896
+ if (!backendReady) {
897
+ remediation.push("Install ATF CLI: npm install -g @trucore/atf");
898
+ if (cfg?.prefer === "api") {
899
+ remediation.push("Or set atfBaseUrl to a reachable ATF API.");
900
+ }
901
+ }
902
+
903
+ const status = ready
904
+ ? (backend.fallback_occurred ? RESPONSE_STATUS.DEGRADED : RESPONSE_STATUS.OK)
905
+ : RESPONSE_STATUS.UNAVAILABLE;
906
+
907
+ const summary = ready
908
+ ? `Preflight OK. Backend: ${backend.effective_backend}.` +
909
+ (backend.fallback_occurred ? ` [FALLBACK: ${backend.fallback_reason}]` : "") +
910
+ " Ready to protect intents."
911
+ : "Preflight FAILED. " +
912
+ (!backendReady ? "No ATF backend available. " : "") +
913
+ (!configValid ? "Config has errors. " : "") +
914
+ "Run atf_integration_doctor for full details.";
915
+
916
+ return nativeToolSuccess("atf_bot_preflight", summary, {
917
+ ready,
918
+ backend_ready: backendReady,
919
+ config_valid: configValid,
920
+ config_errors: configResult.errors,
921
+ config_warnings: configResult.warnings,
922
+ cli_available: cli.available,
923
+ api_available: api.available,
924
+ remediation,
925
+ }, {
926
+ status,
927
+ backend,
928
+ warnings,
929
+ });
930
+ }
931
+
932
+ // ---------------------------------------------------------------------------
933
+ // Tool: atf_tx_explain — explain a receipt or deny decision
934
+ // ---------------------------------------------------------------------------
935
+
936
+ /**
937
+ * Explain a receipt or deny decision in human-readable terms.
938
+ * Maps reason codes to explanations from the REASON_CODE_CATALOG.
939
+ * Thin adapter: does not re-evaluate policy, only explains.
940
+ *
941
+ * Accepts either:
942
+ * - { reasonCodes: ["CODE1", "CODE2"] }
943
+ * - { receipt: { reason_codes: [...], ... } }
944
+ *
945
+ * @param {Record<string, unknown>} _cfg Resolved config (unused, for consistency).
946
+ * @param {{ reasonCodes?: string[], receipt?: object }} params
947
+ * @returns {object}
948
+ */
949
+ function toolAtfTxExplain(_cfg, params = {}) {
950
+ const { reasonCodes: rawCodes, receipt } = params;
951
+
952
+ // Extract reason codes from receipt or params
953
+ let codes = [];
954
+ let decision = null;
955
+ let receiptMeta = null;
956
+
957
+ if (receipt && typeof receipt === "object") {
958
+ codes = receipt.reason_codes ?? receipt.reasonCodes ?? [];
959
+ decision = receipt.decision?.status ?? receipt.status ?? null;
960
+ receiptMeta = {
961
+ content_hash: receipt.content_hash ?? null,
962
+ intent_hash: receipt.intent_hash ?? null,
963
+ chain_id: receipt.chain_id ?? null,
964
+ intent_type: receipt.intent_type ?? null,
965
+ venue: receipt.venue ?? null,
966
+ };
967
+ }
968
+
969
+ if (Array.isArray(rawCodes) && rawCodes.length > 0) {
970
+ codes = rawCodes;
971
+ }
972
+
973
+ if (!Array.isArray(codes) || codes.length === 0) {
974
+ return nativeToolError(
975
+ "atf_tx_explain",
976
+ "atf_tx_explain requires 'reasonCodes' (string[]) or 'receipt' (object with reason_codes).",
977
+ { remediation: ["Provide reasonCodes array or a receipt object with reason_codes field."] },
978
+ );
979
+ }
980
+
981
+ // Map each code to explanation
982
+ const explanations = codes.map((code) => {
983
+ const entry = REASON_CODE_CATALOG[code];
984
+ if (entry) {
985
+ return {
986
+ code,
987
+ category: entry.category,
988
+ explanation: entry.explanation,
989
+ remediation: entry.remediation,
990
+ };
991
+ }
992
+ return {
993
+ code,
994
+ category: "unknown",
995
+ explanation: `Reason code '${code}' is not in the known catalog. Check docs/DENY_CODES.md for the full list.`,
996
+ remediation: "Consult ATF documentation for this deny code.",
997
+ };
998
+ });
999
+
1000
+ // Build human summary
1001
+ const lines = [];
1002
+ if (decision) {
1003
+ lines.push(`Decision: ${decision}`);
1004
+ }
1005
+ if (receiptMeta?.venue) {
1006
+ lines.push(`Venue: ${receiptMeta.venue}`);
1007
+ }
1008
+ if (receiptMeta?.intent_type) {
1009
+ lines.push(`Intent type: ${receiptMeta.intent_type}`);
1010
+ }
1011
+ lines.push("");
1012
+ lines.push(`${codes.length} reason code${codes.length !== 1 ? "s" : ""} explained:`);
1013
+ for (const exp of explanations) {
1014
+ lines.push(` • ${exp.code}: ${exp.explanation}`);
1015
+ }
1016
+
1017
+ const summary = lines.join("\n");
1018
+
1019
+ return nativeToolSuccess("atf_tx_explain", summary, {
1020
+ decision,
1021
+ reason_codes: codes,
1022
+ explanations,
1023
+ receipt_meta: receiptMeta,
1024
+ });
1025
+ }
1026
+
1027
+ // ---------------------------------------------------------------------------
1028
+ // Billing manifest — machine-readable pricing/package metadata
1029
+ // ---------------------------------------------------------------------------
1030
+
1031
+ /**
1032
+ * ATF billing manifest — machine-readable metadata for the Advanced Bot Package.
1033
+ *
1034
+ * Bots use this to discover pricing, accepted payment rails, and activation
1035
+ * guidance without needing to parse docs or reverse-engineer billing logic.
1036
+ *
1037
+ * Exported so tests can validate structural integrity.
1038
+ *
1039
+ * @type {Readonly<Record<string, unknown>>}
1040
+ */
1041
+ export const ATF_BILLING_MANIFEST = Object.freeze({
1042
+ schema_version: "1",
1043
+ product: "TruCore ATF",
1044
+ id: "trucore-atf",
1045
+ billing_manifest_version: "1.0.0",
1046
+ packages: Object.freeze([
1047
+ Object.freeze({
1048
+ package_id: "atf_free_core",
1049
+ package_name: "ATF Free Core",
1050
+ package_tier: "free",
1051
+ summary:
1052
+ "Core transaction firewall capabilities — always free, no payment required.",
1053
+ billing_model: "free",
1054
+ price: null,
1055
+ }),
1056
+ Object.freeze({
1057
+ package_id: "atf_advanced_bot",
1058
+ package_name: "ATF Advanced Bot Package",
1059
+ package_tier: "advanced",
1060
+ summary:
1061
+ "Premium capabilities for professional bot operators — " +
1062
+ "priority support, advanced analytics, and premium integrations.",
1063
+ billing_model: "subscription",
1064
+ billing_interval: "monthly",
1065
+ price: Object.freeze({
1066
+ amount: "25.00",
1067
+ currency: "USDC",
1068
+ chain: "solana",
1069
+ }),
1070
+ price_alternatives: Object.freeze([
1071
+ Object.freeze({
1072
+ amount: "0.1",
1073
+ currency: "SOL",
1074
+ chain: "solana",
1075
+ note: "Approximate SOL equivalent (~0.1 SOL/month) — confirm current rate before sending.",
1076
+ }),
1077
+ ]),
1078
+ status: "accepting_payments",
1079
+ }),
1080
+ ]),
1081
+ payment_rails: Object.freeze({
1082
+ chain: "solana",
1083
+ network: "mainnet-beta",
1084
+ accepted_assets: Object.freeze([
1085
+ Object.freeze({
1086
+ asset: "USDC",
1087
+ mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
1088
+ decimals: 6,
1089
+ preferred: true,
1090
+ }),
1091
+ Object.freeze({
1092
+ asset: "SOL",
1093
+ mint: "native",
1094
+ decimals: 9,
1095
+ preferred: false,
1096
+ }),
1097
+ ]),
1098
+ recipient: Object.freeze({
1099
+ address: "GFHVjxUiPWLs214QZToMvJ8PcacBkkzpiQugE5gXGSmx",
1100
+ label: "TruCore Treasury",
1101
+ note: "TruCore treasury on Solana mainnet-beta. " +
1102
+ "Verify this address before sending funds.",
1103
+ }),
1104
+ memo_requirements: Object.freeze({
1105
+ required: true,
1106
+ format: "atf:<package_id>:<your_identifier>",
1107
+ example: "atf:atf_advanced_bot:my-bot-v1",
1108
+ }),
1109
+ confirmation_expectations: Object.freeze({
1110
+ confirmations: "confirmed",
1111
+ estimated_time_seconds: 5,
1112
+ }),
1113
+ }),
1114
+ free_vs_advanced: Object.freeze({
1115
+ free_core_tools: Object.freeze([
1116
+ "atf_health",
1117
+ "atf_discover",
1118
+ "atf_bootstrap_plan",
1119
+ "atf_bootstrap_execute_safe",
1120
+ "atf_protect_intent",
1121
+ "atf_verify_receipt",
1122
+ "atf_report_savings",
1123
+ "atf_integration_doctor",
1124
+ "atf_bot_preflight",
1125
+ "atf_tx_explain",
1126
+ "atf_billing_info",
1127
+ "atf_adoption_advisor",
1128
+ "atf_billing_claim",
1129
+ ]),
1130
+ advanced_package_features: Object.freeze([
1131
+ "priority_support",
1132
+ "advanced_analytics",
1133
+ "custom_policy_review",
1134
+ "early_access_modules",
1135
+ "priority_bug_fixes",
1136
+ "multi_bot_oversight",
1137
+ "bot_governor",
1138
+ "escalation_controls",
1139
+ "circuit_breaker",
1140
+ ]),
1141
+ note: "All core ATF enforcement tools remain free. " +
1142
+ "The advanced package adds premium support surfaces and " +
1143
+ "future operator controls. No existing free tool will be " +
1144
+ "moved behind a paywall.",
1145
+ }),
1146
+ activation: Object.freeze({
1147
+ method: "pay_then_claim",
1148
+ current_reality:
1149
+ "Payment is accepted on-chain. Claim verification is automated via " +
1150
+ "atf_billing_claim — submit your transaction signature and receive a " +
1151
+ "deterministic verification result. Entitlement provisioning after " +
1152
+ "verification is currently semi-automated.",
1153
+ automated_entitlement: "claim_verification",
1154
+ support_contact: "support@trucore.xyz",
1155
+ }),
1156
+ warnings: Object.freeze([
1157
+ "Early-stage billing — pricing and payment flow may change. Always check this manifest for current rates.",
1158
+ "SOL pricing is approximate — confirm current SOL/USD exchange rate before sending.",
1159
+ "Claim verification is automated via atf_billing_claim. Entitlement provisioning after verification is semi-automated.",
1160
+ "Anti-replay duplicate detection is in-memory and resets on process restart.",
1161
+ "Pricing is subject to change — always check this manifest for current rates.",
1162
+ "This billing manifest is informational — it does not execute payments.",
1163
+ ]),
1164
+ });
1165
+
1166
+ // ---------------------------------------------------------------------------
1167
+ // Tool: atf_adoption_advisor — deterministic adoption recommendation
1168
+ // ---------------------------------------------------------------------------
1169
+
1170
+ /**
1171
+ * Evaluate bot capability signals and return a deterministic adoption
1172
+ * recommendation including install_needed, recommended_tier, reason_codes,
1173
+ * call sequence, and install guidance.
1174
+ *
1175
+ * Read-only, pure metadata. No side effects, no network calls.
1176
+ *
1177
+ * @param {Record<string, unknown>} _cfg Resolved config (unused).
1178
+ * @param {{ signals?: Record<string, boolean> }} params
1179
+ * @returns {object}
1180
+ */
1181
+ function toolAtfAdoptionAdvisor(_cfg, params = {}) {
1182
+ const { signals } = params;
1183
+
1184
+ // Validate signals shape
1185
+ if (signals !== undefined && signals !== null) {
1186
+ if (typeof signals !== "object" || Array.isArray(signals)) {
1187
+ return nativeToolError(
1188
+ "atf_adoption_advisor",
1189
+ "Invalid 'signals' parameter: must be an object mapping " +
1190
+ "trigger_id keys to boolean values. Example: " +
1191
+ '{"bot_handles_real_capital": true, "bot_retries_automatically": false}',
1192
+ {
1193
+ remediation: [
1194
+ "Provide signals as an object: {trigger_id: true/false, ...}",
1195
+ "Omit signals entirely to evaluate with no triggers matched.",
1196
+ "Valid trigger_ids: " +
1197
+ TRIGGER_MODEL.map((t) => t.trigger_id).join(", "),
1198
+ ],
1199
+ },
1200
+ );
1201
+ }
1202
+ }
1203
+
1204
+ const result = evaluateAdoption(signals ?? {});
1205
+
1206
+ const summary = result.install_needed
1207
+ ? `ATF adoption recommended (${result.recommended_tier}). ` +
1208
+ `${result.matched_triggers.length} trigger(s) matched. ` +
1209
+ `Highest severity: ${result.highest_severity}.`
1210
+ : "ATF adoption not currently needed. No triggers matched. " +
1211
+ "Monitor for future trigger activation.";
1212
+
1213
+ return nativeToolSuccess("atf_adoption_advisor", summary, result);
1214
+ }
1215
+
1216
+ // ---------------------------------------------------------------------------
1217
+ // Tool: atf_billing_info — billing / pricing metadata discovery
1218
+ // ---------------------------------------------------------------------------
1219
+
1220
+ /**
1221
+ * Return machine-readable billing and package metadata for ATF.
1222
+ *
1223
+ * Read-only metadata tool — does NOT execute payments or check entitlements.
1224
+ * Bots use this to discover:
1225
+ * - which packages/tiers exist (free core vs advanced)
1226
+ * - pricing and accepted payment assets
1227
+ * - payment rail details (chain, recipient, memo format)
1228
+ * - activation/claim flow guidance
1229
+ * - free-vs-advanced capability split
1230
+ *
1231
+ * Always succeeds (pure metadata, no backend dependency).
1232
+ *
1233
+ * @param {Record<string, unknown>} _cfg Resolved config (unused, for consistency).
1234
+ * @param {{ packageId?: string }} params
1235
+ * @returns {object}
1236
+ */
1237
+ function toolAtfBillingInfo(_cfg, params = {}) {
1238
+ const { packageId } = params;
1239
+
1240
+ // If a specific package is requested, filter
1241
+ if (packageId && typeof packageId === "string") {
1242
+ const pkg = ATF_BILLING_MANIFEST.packages.find(
1243
+ (p) => p.package_id === packageId,
1244
+ );
1245
+ if (!pkg) {
1246
+ return nativeToolError(
1247
+ "atf_billing_info",
1248
+ `Unknown package_id: '${packageId}'. ` +
1249
+ `Available: ${ATF_BILLING_MANIFEST.packages.map((p) => p.package_id).join(", ")}.`,
1250
+ {
1251
+ remediation: [
1252
+ "Use one of the available package IDs, or omit packageId to see all packages.",
1253
+ ],
1254
+ },
1255
+ );
1256
+ }
1257
+
1258
+ return nativeToolSuccess(
1259
+ "atf_billing_info",
1260
+ `Billing info for package '${pkg.package_name}' (${pkg.package_tier} tier).`,
1261
+ {
1262
+ package: pkg,
1263
+ payment_rails: ATF_BILLING_MANIFEST.payment_rails,
1264
+ activation: ATF_BILLING_MANIFEST.activation,
1265
+ warnings: ATF_BILLING_MANIFEST.warnings,
1266
+ },
1267
+ );
1268
+ }
1269
+
1270
+ // Full manifest
1271
+ return nativeToolSuccess(
1272
+ "atf_billing_info",
1273
+ "ATF billing metadata: 2 packages (free core + advanced bot). " +
1274
+ "Payment accepted on Solana (USDC preferred). " +
1275
+ "See result for full pricing, payment rails, and activation guidance.",
1276
+ {
1277
+ packages: ATF_BILLING_MANIFEST.packages,
1278
+ payment_rails: ATF_BILLING_MANIFEST.payment_rails,
1279
+ free_vs_advanced: ATF_BILLING_MANIFEST.free_vs_advanced,
1280
+ activation: ATF_BILLING_MANIFEST.activation,
1281
+ warnings: ATF_BILLING_MANIFEST.warnings,
1282
+ },
1283
+ );
1284
+ }
1285
+
1286
+ // ---------------------------------------------------------------------------
1287
+ // Tool: atf_billing_claim — verify on-chain payment and process claim
1288
+ // ---------------------------------------------------------------------------
1289
+
1290
+ /**
1291
+ * Verify an on-chain Solana payment and process a billing claim for
1292
+ * the ATF Advanced Bot Package.
1293
+ *
1294
+ * Checks: tx exists, finalized, recipient = treasury, asset + amount match
1295
+ * package pricing, no duplicate claim.
1296
+ *
1297
+ * @param {Record<string, unknown>} cfg Resolved config.
1298
+ * @param {object} params
1299
+ * @param {string} params.tx_signature Solana transaction signature.
1300
+ * @param {string} params.wallet Claimant wallet address.
1301
+ * @param {string} params.package_id Package being claimed.
1302
+ * @param {string} [params.asset_symbol] Optional asset hint: "USDC" or "SOL".
1303
+ * @param {string} [params.cluster] Optional cluster: "mainnet-beta" (default).
1304
+ * @returns {Promise<object>}
1305
+ */
1306
+ async function toolAtfBillingClaim(cfg, params = {}) {
1307
+ const { tx_signature, wallet, package_id, asset_symbol, cluster } = params;
1308
+
1309
+ // Build fetchTransaction from config if RPC access is available
1310
+ let fetchTransaction = null;
1311
+ const rpcUrl = cfg?.solanaRpcUrl;
1312
+ if (rpcUrl && typeof rpcUrl === "string") {
1313
+ fetchTransaction = async (sig, _cluster) => {
1314
+ const body = JSON.stringify({
1315
+ jsonrpc: "2.0",
1316
+ id: 1,
1317
+ method: "getTransaction",
1318
+ params: [
1319
+ sig,
1320
+ {
1321
+ encoding: "jsonParsed",
1322
+ maxSupportedTransactionVersion: 0,
1323
+ commitment: "confirmed",
1324
+ },
1325
+ ],
1326
+ });
1327
+
1328
+ const res = await fetch(rpcUrl, {
1329
+ method: "POST",
1330
+ headers: { "Content-Type": "application/json" },
1331
+ body,
1332
+ signal: AbortSignal.timeout(15_000),
1333
+ });
1334
+
1335
+ if (!res.ok) {
1336
+ throw new Error(`RPC HTTP ${res.status}`);
1337
+ }
1338
+
1339
+ const json = await res.json();
1340
+ if (json.error) {
1341
+ throw new Error(json.error.message ?? JSON.stringify(json.error));
1342
+ }
1343
+
1344
+ const tx = json.result;
1345
+ if (!tx) return null;
1346
+
1347
+ // Attach confirmationStatus from RPC response context if available
1348
+ if (!tx.confirmationStatus && json.result?.slot) {
1349
+ tx.confirmationStatus = "confirmed";
1350
+ }
1351
+ return tx;
1352
+ };
1353
+ }
1354
+
1355
+ const claimResult = await verifyBillingClaim(
1356
+ { tx_signature, wallet, package_id, asset_symbol, cluster },
1357
+ { fetchTransaction },
1358
+ );
1359
+
1360
+ // Map claim result to native response envelope
1361
+ if (claimResult.claim_status === CLAIM_STATUS.VERIFIED) {
1362
+ return nativeToolSuccess(
1363
+ "atf_billing_claim",
1364
+ `Billing claim verified: ${claimResult.amount_received} ` +
1365
+ `${claimResult.asset_symbol} received for package ` +
1366
+ `'${claimResult.package_id}'. Entitlement: ${claimResult.entitlement_status}.`,
1367
+ claimResult,
1368
+ { warnings: claimResult.warnings },
1369
+ );
1370
+ }
1371
+
1372
+ // Denied or error
1373
+ const summary =
1374
+ `Billing claim ${claimResult.claim_status}: ` +
1375
+ `${claimResult.deny_code ?? "unknown error"}. ` +
1376
+ (claimResult.remediation.length > 0
1377
+ ? claimResult.remediation[0]
1378
+ : "See result for details.");
1379
+
1380
+ return nativeToolError("atf_billing_claim", summary, {
1381
+ remediation: claimResult.remediation,
1382
+ warnings: claimResult.warnings,
1383
+ });
1384
+ }
1385
+
1386
+ // ---------------------------------------------------------------------------
1387
+ // Safe handler wrapper — graceful degradation for all tool handlers
1388
+ // ---------------------------------------------------------------------------
1389
+
1390
+ /**
1391
+ * Wrap a tool handler so runtime errors never crash the gateway.
1392
+ * Returns an errorResponse on any uncaught exception.
1393
+ *
1394
+ * @param {string} toolName
1395
+ * @param {Function} handler
1396
+ * @returns {Function}
1397
+ */
1398
+ function safeHandler(toolName, handler) {
1399
+ return async (params) => {
1400
+ try {
1401
+ return await handler(params);
1402
+ } catch (err) {
1403
+ return nativeToolError(
1404
+ toolName,
1405
+ `[${PLUGIN_ID}] ${toolName} failed: ${err?.message ?? String(err)}. ` +
1406
+ "ATF core CLI fallback is still available.",
1407
+ );
1408
+ }
1409
+ };
1410
+ }
1411
+
652
1412
  // ---------------------------------------------------------------------------
653
1413
  // Plugin entry-point
654
1414
  // ---------------------------------------------------------------------------
@@ -660,11 +1420,42 @@ export function buildHumanSummary(reportJson) {
660
1420
  * All tools are registered as OPTIONAL — agents / operators must allowlist
661
1421
  * them before they can be used.
662
1422
  *
1423
+ * Safe: catches all registration errors, logs warnings, never throws.
1424
+ *
663
1425
  * @param {{ registerTool: Function, config: Record<string, unknown> }} api
664
1426
  */
665
1427
  export default function atfPlugin(api) {
1428
+ if (!api || typeof api.registerTool !== "function") {
1429
+ // Defensive: if api is missing or malformed, log and bail out silently.
1430
+ // This prevents crashing the OpenClaw gateway on bad plugin load.
1431
+ if (typeof console !== "undefined") {
1432
+ console.warn(
1433
+ `[${PLUGIN_ID}] Plugin loaded with invalid api object. ` +
1434
+ "Skipping tool registration. ATF CLI fallback is still available.",
1435
+ );
1436
+ }
1437
+ return;
1438
+ }
1439
+
666
1440
  const cfg = resolveConfig(api?.config ?? {});
667
1441
 
1442
+ // ---- Tool 0: atf_health -------------------------------------------------
1443
+ api.registerTool({
1444
+ name: "atf_health",
1445
+ optional: true,
1446
+ description:
1447
+ "Check ATF CLI and API backend availability. " +
1448
+ "Returns structured health status including version, reachability, " +
1449
+ "and preferred mode. Use this to verify ATF is ready before calling " +
1450
+ "other ATF tools.",
1451
+ inputSchema: {
1452
+ type: "object",
1453
+ properties: {},
1454
+ additionalProperties: false,
1455
+ },
1456
+ handler: safeHandler("atf_health", () => toolAtfHealth(cfg)),
1457
+ });
1458
+
668
1459
  // ---- Tool 1: atf_discover -----------------------------------------------
669
1460
  api.registerTool({
670
1461
  name: "atf_discover",
@@ -685,7 +1476,7 @@ export default function atfPlugin(api) {
685
1476
  },
686
1477
  additionalProperties: false,
687
1478
  },
688
- handler: (params) => toolAtfDiscover(cfg, params),
1479
+ handler: safeHandler("atf_discover", (params) => toolAtfDiscover(cfg, params)),
689
1480
  });
690
1481
 
691
1482
  // ---- Tool 2: atf_bootstrap_plan -----------------------------------------
@@ -709,7 +1500,7 @@ export default function atfPlugin(api) {
709
1500
  },
710
1501
  additionalProperties: false,
711
1502
  },
712
- handler: (params) => toolAtfBootstrapPlan(cfg, params),
1503
+ handler: safeHandler("atf_bootstrap_plan", (params) => toolAtfBootstrapPlan(cfg, params)),
713
1504
  });
714
1505
 
715
1506
  // ---- Tool 3: atf_bootstrap_execute_safe ---------------------------------
@@ -736,7 +1527,7 @@ export default function atfPlugin(api) {
736
1527
  },
737
1528
  additionalProperties: false,
738
1529
  },
739
- handler: (params) => toolAtfBootstrapExecuteSafe(cfg, params),
1530
+ handler: safeHandler("atf_bootstrap_execute_safe", (params) => toolAtfBootstrapExecuteSafe(cfg, params)),
740
1531
  });
741
1532
 
742
1533
  // ---- Tool 4: atf_protect_intent -----------------------------------------
@@ -772,7 +1563,7 @@ export default function atfPlugin(api) {
772
1563
  },
773
1564
  additionalProperties: false,
774
1565
  },
775
- handler: (params) => toolAtfProtectIntent(cfg, params),
1566
+ handler: safeHandler("atf_protect_intent", (params) => toolAtfProtectIntent(cfg, params)),
776
1567
  });
777
1568
 
778
1569
  // ---- Tool 5: atf_verify_receipt -----------------------------------------
@@ -794,7 +1585,7 @@ export default function atfPlugin(api) {
794
1585
  },
795
1586
  additionalProperties: false,
796
1587
  },
797
- handler: (params) => toolAtfVerifyReceipt(cfg, params),
1588
+ handler: safeHandler("atf_verify_receipt", (params) => toolAtfVerifyReceipt(cfg, params)),
798
1589
  });
799
1590
 
800
1591
  // ---- Tool 6: atf_report_savings -----------------------------------------
@@ -822,6 +1613,313 @@ export default function atfPlugin(api) {
822
1613
  },
823
1614
  additionalProperties: false,
824
1615
  },
825
- handler: (params) => toolAtfReportSavings(cfg, params),
1616
+ handler: safeHandler("atf_report_savings", (params) => toolAtfReportSavings(cfg, params)),
826
1617
  });
1618
+
1619
+ // ---- Tool 7: atf_integration_doctor ------------------------------------
1620
+ // Collect registered tool names for the doctor report.
1621
+ // We need to register it after all other tools so it can see them.
1622
+ const registeredToolNames = [...api.tools?.keys?.() ?? []];
1623
+ // The doctor + remaining tools are about to be added
1624
+ registeredToolNames.push(
1625
+ "atf_integration_doctor",
1626
+ "atf_bot_preflight",
1627
+ "atf_tx_explain",
1628
+ "atf_billing_info",
1629
+ "atf_adoption_advisor",
1630
+ "atf_billing_claim",
1631
+ );
1632
+
1633
+ api.registerTool({
1634
+ name: "atf_integration_doctor",
1635
+ optional: true,
1636
+ description:
1637
+ "Run ATF integration readiness check. Reports plugin loading status, " +
1638
+ "CLI/API availability, backend preference vs effective backend, " +
1639
+ "registered tools, configuration warnings, and remediation steps. " +
1640
+ "Use this to validate and troubleshoot the ATF OpenClaw integration.",
1641
+ inputSchema: {
1642
+ type: "object",
1643
+ properties: {},
1644
+ additionalProperties: false,
1645
+ },
1646
+ handler: safeHandler(
1647
+ "atf_integration_doctor",
1648
+ () => toolAtfIntegrationDoctor(cfg, registeredToolNames),
1649
+ ),
1650
+ });
1651
+
1652
+ // ---- Tool 8: atf_bot_preflight ------------------------------------------
1653
+ api.registerTool({
1654
+ name: "atf_bot_preflight",
1655
+ optional: true,
1656
+ description:
1657
+ "Pre-session readiness check: is ATF ready to protect intents right now? " +
1658
+ "Returns a structured go/no-go signal with backend availability, " +
1659
+ "config validity, and remediation steps. Lighter than atf_integration_doctor — " +
1660
+ "use this before starting a bot session to confirm ATF is operational.",
1661
+ inputSchema: {
1662
+ type: "object",
1663
+ properties: {},
1664
+ additionalProperties: false,
1665
+ },
1666
+ handler: safeHandler(
1667
+ "atf_bot_preflight",
1668
+ () => toolAtfBotPreflight(cfg, api?.config),
1669
+ ),
1670
+ });
1671
+
1672
+ // ---- Tool 9: atf_tx_explain ---------------------------------------------
1673
+ api.registerTool({
1674
+ name: "atf_tx_explain",
1675
+ optional: true,
1676
+ description:
1677
+ "Explain an ATF deny decision or receipt in human terms. " +
1678
+ "Maps each reason code to a structured explanation with category, " +
1679
+ "plain-language description, and remediation guidance. " +
1680
+ "Does not re-evaluate policy — only explains existing decisions. " +
1681
+ "Accepts either 'reasonCodes' (string[]) or 'receipt' (object with reason_codes).",
1682
+ inputSchema: {
1683
+ type: "object",
1684
+ properties: {
1685
+ reasonCodes: {
1686
+ type: "array",
1687
+ items: { type: "string" },
1688
+ description:
1689
+ "Array of ATF reason codes to explain (e.g. ['DEX_VENUE_NOT_ALLOWED']).",
1690
+ },
1691
+ receipt: {
1692
+ type: "object",
1693
+ description:
1694
+ "An ATF receipt or decision object with a reason_codes field. " +
1695
+ "If provided, reason codes are extracted from this object.",
1696
+ },
1697
+ },
1698
+ additionalProperties: false,
1699
+ },
1700
+ handler: safeHandler(
1701
+ "atf_tx_explain",
1702
+ (params) => toolAtfTxExplain(cfg, params),
1703
+ ),
1704
+ });
1705
+
1706
+ // ---- Tool 10: atf_billing_info ------------------------------------------
1707
+ api.registerTool({
1708
+ name: "atf_billing_info",
1709
+ optional: true,
1710
+ description:
1711
+ "Discover ATF billing, pricing, and package metadata. " +
1712
+ "Returns machine-readable info about free-core vs advanced-package tiers, " +
1713
+ "accepted payment assets (USDC, SOL on Solana), payment rail details, " +
1714
+ "and activation/claim flow guidance. " +
1715
+ "Read-only metadata — does NOT execute payments.",
1716
+ inputSchema: {
1717
+ type: "object",
1718
+ properties: {
1719
+ packageId: {
1720
+ type: "string",
1721
+ description:
1722
+ "Optional package ID to filter (e.g. 'atf_free_core' or 'atf_advanced_bot'). " +
1723
+ "Omit to see all packages.",
1724
+ },
1725
+ },
1726
+ additionalProperties: false,
1727
+ },
1728
+ handler: safeHandler(
1729
+ "atf_billing_info",
1730
+ (params) => toolAtfBillingInfo(cfg, params),
1731
+ ),
1732
+ });
1733
+
1734
+ // ---- Tool 11: atf_adoption_advisor --------------------------------------
1735
+ api.registerTool({
1736
+ name: "atf_adoption_advisor",
1737
+ optional: true,
1738
+ description:
1739
+ "Evaluate bot capability signals and return a deterministic ATF " +
1740
+ "adoption recommendation. Returns install_needed, recommended_tier " +
1741
+ "(free_core / advanced_bot / none), matched triggers with severity, " +
1742
+ "safe default call sequence, install/enable guidance, and next actions. " +
1743
+ "Read-only, pure metadata — no side effects.",
1744
+ inputSchema: {
1745
+ type: "object",
1746
+ properties: {
1747
+ signals: {
1748
+ type: "object",
1749
+ description:
1750
+ "Map of trigger_id to boolean. Example: " +
1751
+ '{"bot_handles_real_capital": true, "bot_retries_automatically": false}. ' +
1752
+ "Valid trigger_ids: bot_handles_real_capital, " +
1753
+ "bot_signs_or_submits_transactions, bot_retries_automatically, " +
1754
+ "bot_lacks_audit_trail, bot_operates_without_spending_limits, " +
1755
+ "operator_cannot_explain_failures, " +
1756
+ "bot_uses_multiple_dexes_or_protocols, " +
1757
+ "bot_runs_unattended, bot_needs_operator_controls. " +
1758
+ "Omit to evaluate with no triggers (returns install_needed=false).",
1759
+ additionalProperties: { type: "boolean" },
1760
+ },
1761
+ },
1762
+ additionalProperties: false,
1763
+ },
1764
+ handler: safeHandler(
1765
+ "atf_adoption_advisor",
1766
+ (params) => toolAtfAdoptionAdvisor(cfg, params),
1767
+ ),
1768
+ });
1769
+
1770
+ // ---- Tool 12: atf_billing_claim ----------------------------------------
1771
+ api.registerTool({
1772
+ name: "atf_billing_claim",
1773
+ optional: true,
1774
+ description:
1775
+ "Verify an on-chain Solana payment and process a billing claim for " +
1776
+ "the ATF Advanced Bot Package. Checks transaction existence, finality, " +
1777
+ "treasury recipient, asset type (USDC or SOL), and amount against " +
1778
+ "published pricing. Returns a deterministic claim result with " +
1779
+ "entitlement status. Requires tx_signature, wallet, and package_id.",
1780
+ inputSchema: {
1781
+ type: "object",
1782
+ properties: {
1783
+ tx_signature: {
1784
+ type: "string",
1785
+ description:
1786
+ "Solana transaction signature (base58). " +
1787
+ "The payment transaction to verify.",
1788
+ },
1789
+ wallet: {
1790
+ type: "string",
1791
+ description:
1792
+ "Claimant wallet address (Solana public key). " +
1793
+ "The wallet that made the payment.",
1794
+ },
1795
+ package_id: {
1796
+ type: "string",
1797
+ description:
1798
+ "Package to claim. Currently: 'atf_advanced_bot'. " +
1799
+ "Use atf_billing_info to discover available packages.",
1800
+ },
1801
+ asset_symbol: {
1802
+ type: "string",
1803
+ description:
1804
+ "Optional: asset hint — 'USDC' or 'SOL'. " +
1805
+ "If omitted, both are checked automatically.",
1806
+ },
1807
+ cluster: {
1808
+ type: "string",
1809
+ description:
1810
+ "Optional: Solana cluster — 'mainnet-beta' (default). " +
1811
+ "Production entitlements require mainnet-beta.",
1812
+ },
1813
+ },
1814
+ required: ["tx_signature", "wallet", "package_id"],
1815
+ additionalProperties: false,
1816
+ },
1817
+ handler: safeHandler(
1818
+ "atf_billing_claim",
1819
+ (params) => toolAtfBillingClaim(cfg, params),
1820
+ ),
1821
+ });
1822
+ }
1823
+
1824
+ // ---------------------------------------------------------------------------
1825
+ // OpenClaw lifecycle exports: register / activate / deactivate
1826
+ // ---------------------------------------------------------------------------
1827
+
1828
+ /**
1829
+ * OpenClaw lifecycle: register tools.
1830
+ * Equivalent to the default export (atfPlugin). Provided as a named export
1831
+ * to match the expected OpenClaw plugin contract.
1832
+ *
1833
+ * Safe: never throws. If api is invalid, logs a warning and returns.
1834
+ *
1835
+ * @param {{ registerTool: Function, config: Record<string, unknown> }} api
1836
+ */
1837
+ export function register(api) {
1838
+ try {
1839
+ atfPlugin(api);
1840
+ } catch (err) {
1841
+ if (typeof console !== "undefined") {
1842
+ console.warn(
1843
+ `[${PLUGIN_ID}] register() failed: ${err?.message ?? String(err)}. ` +
1844
+ "ATF CLI fallback is still available.",
1845
+ );
1846
+ }
1847
+ }
1848
+ }
1849
+
1850
+ /**
1851
+ * OpenClaw lifecycle: activate plugin.
1852
+ * Called after register() succeeds. Runs non-blocking config validation
1853
+ * and surfaces warnings/errors to the operator via console.warn.
1854
+ *
1855
+ * CRITICAL RULE: Config validation NEVER blocks activation.
1856
+ * Invalid config → warn + degrade gracefully. Never throw.
1857
+ *
1858
+ * @param {{ config?: Record<string, unknown> }} [ctx]
1859
+ */
1860
+ export function activate(ctx) {
1861
+ try {
1862
+ // Import config validation lazily to avoid circular dependency
1863
+ // at module-evaluation time (config.mjs is side-effect-free).
1864
+ const { validateConfig, formatActivationWarnings } = /** @type {any} */ (
1865
+ // Dynamic import would be async—config.mjs is already loaded at this
1866
+ // point (register imports it transitively via doctor.mjs), so we use
1867
+ // a synchronous re-export pattern instead. The helper is pure &
1868
+ // deterministic, so calling it synchronously is safe.
1869
+ _configModule
1870
+ );
1871
+
1872
+ const rawConfig = ctx?.config ?? undefined;
1873
+ const result = validateConfig(rawConfig);
1874
+ const lines = formatActivationWarnings(result);
1875
+
1876
+ if (lines.length > 0 && typeof console !== "undefined") {
1877
+ for (const line of lines) {
1878
+ console.warn(line);
1879
+ }
1880
+ }
1881
+ } catch {
1882
+ // Activation must NEVER fail. Swallow any unexpected error.
1883
+ if (typeof console !== "undefined") {
1884
+ console.warn(
1885
+ `[${PLUGIN_ID}] activate() config validation failed unexpectedly. ` +
1886
+ "Plugin is still operational. Run atf_integration_doctor for details.",
1887
+ );
1888
+ }
1889
+ }
1890
+ }
1891
+
1892
+ /**
1893
+ * OpenClaw lifecycle: deactivate plugin.
1894
+ * Called when the plugin is disabled or the gateway shuts down.
1895
+ * Currently a no-op. Provided for forward compatibility.
1896
+ */
1897
+ export function deactivate() {
1898
+ // No-op: ATF holds no persistent connections or state.
827
1899
  }
1900
+
1901
+ // REASON_CODE_CATALOG is exported at definition site (above).
1902
+
1903
+ // Re-export adoption advisor primitives for external consumers.
1904
+ export { evaluateAdoption, TRIGGER_MODEL, INSTALL_GUIDANCE, SAFE_CALL_SEQUENCE };
1905
+
1906
+ // Re-export billing claim primitives for external consumers and tests.
1907
+ export {
1908
+ verifyBillingClaim,
1909
+ CLAIM_STATUS,
1910
+ CLAIM_DENY_CODES,
1911
+ TREASURY_ADDRESS,
1912
+ USDC_MINT,
1913
+ KNOWN_PACKAGES,
1914
+ checkDuplicate,
1915
+ _resetClaimTracking,
1916
+ };
1917
+
1918
+ // Re-export universal contract layer for external consumers,
1919
+ // future adapters, and SDK/CLI surfaces.
1920
+ export {
1921
+ CONTRACT_FAMILIES,
1922
+ TOOL_FAMILY_MAP,
1923
+ CANONICAL_TOOLS,
1924
+ CANONICAL_TOOL_COUNT,
1925
+ };