@trucore/openclaw-atf 0.1.0 → 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/config.mjs ADDED
@@ -0,0 +1,317 @@
1
+ /**
2
+ * config.mjs — Config validation + sanity checking for ATF OpenClaw plugin
3
+ *
4
+ * Validates user-supplied config against the canonical schema, reports
5
+ * unsupported keys, detects common misconfigurations, and provides
6
+ * operator-language remediation.
7
+ *
8
+ * Exports:
9
+ * - validateConfig(userConfig) → ConfigValidationResult
10
+ * - SUPPORTED_KEYS → canonical top-level keys
11
+ * - SUPPORTED_SAFETY_KEYS → canonical safety sub-keys
12
+ * - CANONICAL_PLUGIN_ID → "trucore-atf"
13
+ * - MINIMAL_ENABLE_CONFIG → smallest working config object
14
+ *
15
+ * Uses ONLY built-in Node modules. No external deps.
16
+ */
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Constants
20
+ // ---------------------------------------------------------------------------
21
+
22
+ /** Canonical plugin ID — single source of truth for config validation. */
23
+ export const CANONICAL_PLUGIN_ID = "trucore-atf";
24
+
25
+ /** Supported top-level config keys (matches openclaw.plugin.json configSchema). */
26
+ export const SUPPORTED_KEYS = Object.freeze([
27
+ "atfCli",
28
+ "atfBaseUrl",
29
+ "prefer",
30
+ "receiptsDir",
31
+ "safety",
32
+ ]);
33
+
34
+ /** Supported safety sub-keys. */
35
+ export const SUPPORTED_SAFETY_KEYS = Object.freeze([
36
+ "allowExecuteSafe",
37
+ "allowNetwork",
38
+ ]);
39
+
40
+ /** Valid values for the `prefer` key. */
41
+ const VALID_PREFER_VALUES = Object.freeze(["cli", "api"]);
42
+
43
+ /**
44
+ * Smallest config that enables the plugin with all defaults.
45
+ * Zero required keys — the plugin works with an empty object.
46
+ */
47
+ export const MINIMAL_ENABLE_CONFIG = Object.freeze({});
48
+
49
+ /**
50
+ * Config for CLI-preferred mode (explicit, copy-paste-safe).
51
+ */
52
+ export const CLI_PREFER_CONFIG = Object.freeze({
53
+ prefer: "cli",
54
+ });
55
+
56
+ /**
57
+ * Config for API-preferred mode (requires atfBaseUrl).
58
+ */
59
+ export const API_PREFER_CONFIG = Object.freeze({
60
+ prefer: "api",
61
+ atfBaseUrl: "https://api.trucore.xyz",
62
+ });
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // Validation result
66
+ // ---------------------------------------------------------------------------
67
+
68
+ /**
69
+ * @typedef {object} ConfigValidationResult
70
+ * @property {boolean} valid True if no errors found.
71
+ * @property {string[]} errors Hard errors (config will not work).
72
+ * @property {string[]} warnings Soft warnings (config works but suboptimal).
73
+ * @property {string[]} remediation Operator-facing fix instructions.
74
+ * @property {string[]} unsupported_keys Keys present but not in schema.
75
+ */
76
+
77
+ // ---------------------------------------------------------------------------
78
+ // Validation logic
79
+ // ---------------------------------------------------------------------------
80
+
81
+ /**
82
+ * Validate a user-supplied plugin config object.
83
+ *
84
+ * Does NOT throw. Returns a structured result with errors, warnings,
85
+ * and remediation steps in operator language.
86
+ *
87
+ * @param {unknown} userConfig The config object from OpenClaw.
88
+ * @returns {ConfigValidationResult}
89
+ */
90
+ export function validateConfig(userConfig) {
91
+ /** @type {string[]} */
92
+ const errors = [];
93
+ /** @type {string[]} */
94
+ const warnings = [];
95
+ /** @type {string[]} */
96
+ const remediation = [];
97
+ /** @type {string[]} */
98
+ const unsupportedKeys = [];
99
+
100
+ // ---- Null / non-object config ----
101
+ if (userConfig === null || userConfig === undefined) {
102
+ // Perfectly fine — zero-config enable
103
+ return {
104
+ valid: true,
105
+ errors: [],
106
+ warnings: [],
107
+ remediation: [],
108
+ unsupported_keys: [],
109
+ };
110
+ }
111
+
112
+ if (typeof userConfig !== "object" || Array.isArray(userConfig)) {
113
+ return {
114
+ valid: false,
115
+ errors: ["Plugin config must be an object (or omitted for defaults)."],
116
+ warnings: [],
117
+ remediation: [
118
+ 'Use: { "plugins": { "trucore-atf": {} } } for zero-config enable.',
119
+ ],
120
+ unsupported_keys: [],
121
+ };
122
+ }
123
+
124
+ const cfg = /** @type {Record<string, unknown>} */ (userConfig);
125
+
126
+ // ---- Detect unsupported top-level keys ----
127
+ const supportedSet = new Set(SUPPORTED_KEYS);
128
+ for (const key of Object.keys(cfg)) {
129
+ if (!supportedSet.has(key)) {
130
+ unsupportedKeys.push(key);
131
+ }
132
+ }
133
+ if (unsupportedKeys.length > 0) {
134
+ errors.push(
135
+ `Unsupported config keys: ${unsupportedKeys.join(", ")}. ` +
136
+ `Supported keys: ${SUPPORTED_KEYS.join(", ")}.`,
137
+ );
138
+ remediation.push(
139
+ `Remove unsupported keys from your plugin config: ${unsupportedKeys.join(", ")}.`,
140
+ );
141
+ }
142
+
143
+ // ---- Validate `prefer` value ----
144
+ if ("prefer" in cfg) {
145
+ if (!VALID_PREFER_VALUES.includes(/** @type {string} */ (cfg.prefer))) {
146
+ errors.push(
147
+ `Invalid prefer value: "${cfg.prefer}". Must be "cli" or "api".`,
148
+ );
149
+ remediation.push('Set prefer to "cli" or "api".');
150
+ }
151
+ }
152
+
153
+ // ---- Validate prefer=api requires atfBaseUrl ----
154
+ if (cfg.prefer === "api" && !cfg.atfBaseUrl) {
155
+ errors.push(
156
+ 'prefer="api" requires atfBaseUrl to be set.',
157
+ );
158
+ remediation.push(
159
+ "Set atfBaseUrl in plugin config " +
160
+ '(e.g. "https://api.trucore.xyz").',
161
+ );
162
+ }
163
+
164
+ // ---- Validate atfBaseUrl format ----
165
+ if (cfg.atfBaseUrl !== undefined && cfg.atfBaseUrl !== null) {
166
+ if (typeof cfg.atfBaseUrl !== "string" || cfg.atfBaseUrl.length === 0) {
167
+ errors.push("atfBaseUrl must be a non-empty string URL.");
168
+ remediation.push(
169
+ 'Set atfBaseUrl to a valid URL (e.g. "https://api.trucore.xyz").',
170
+ );
171
+ } else if (
172
+ !cfg.atfBaseUrl.startsWith("http://") &&
173
+ !cfg.atfBaseUrl.startsWith("https://")
174
+ ) {
175
+ warnings.push(
176
+ `atfBaseUrl "${cfg.atfBaseUrl}" does not start with http:// or https://.`,
177
+ );
178
+ }
179
+ }
180
+
181
+ // ---- Validate atfCli type ----
182
+ if (cfg.atfCli !== undefined && cfg.atfCli !== null) {
183
+ if (typeof cfg.atfCli !== "string" || cfg.atfCli.length === 0) {
184
+ errors.push("atfCli must be a non-empty string.");
185
+ remediation.push('Set atfCli to a CLI command name (e.g. "atf").');
186
+ }
187
+ }
188
+
189
+ // ---- Validate receiptsDir type ----
190
+ if (cfg.receiptsDir !== undefined && cfg.receiptsDir !== null) {
191
+ if (typeof cfg.receiptsDir !== "string") {
192
+ errors.push("receiptsDir must be a string path.");
193
+ }
194
+ }
195
+
196
+ // ---- Validate safety sub-object ----
197
+ if ("safety" in cfg) {
198
+ if (
199
+ cfg.safety === null ||
200
+ typeof cfg.safety !== "object" ||
201
+ Array.isArray(cfg.safety)
202
+ ) {
203
+ errors.push("safety must be an object.");
204
+ remediation.push(
205
+ 'Use: "safety": { "allowExecuteSafe": true, "allowNetwork": false }',
206
+ );
207
+ } else {
208
+ const safetyObj = /** @type {Record<string, unknown>} */ (cfg.safety);
209
+ const safetySupportedSet = new Set(SUPPORTED_SAFETY_KEYS);
210
+ for (const key of Object.keys(safetyObj)) {
211
+ if (!safetySupportedSet.has(key)) {
212
+ unsupportedKeys.push(`safety.${key}`);
213
+ }
214
+ }
215
+ if (
216
+ "allowExecuteSafe" in safetyObj &&
217
+ typeof safetyObj.allowExecuteSafe !== "boolean"
218
+ ) {
219
+ errors.push("safety.allowExecuteSafe must be a boolean.");
220
+ }
221
+ if (
222
+ "allowNetwork" in safetyObj &&
223
+ typeof safetyObj.allowNetwork !== "boolean"
224
+ ) {
225
+ errors.push("safety.allowNetwork must be a boolean.");
226
+ }
227
+ }
228
+ }
229
+
230
+ // ---- Warn about atfBaseUrl without prefer=api ----
231
+ if (
232
+ cfg.atfBaseUrl &&
233
+ typeof cfg.atfBaseUrl === "string" &&
234
+ cfg.prefer !== "api"
235
+ ) {
236
+ warnings.push(
237
+ "atfBaseUrl is set but prefer is not 'api'. " +
238
+ "The URL will only be used as a fallback. " +
239
+ "Set prefer='api' to use the API as the primary backend.",
240
+ );
241
+ }
242
+
243
+ const valid = errors.length === 0;
244
+ return {
245
+ valid,
246
+ errors,
247
+ warnings,
248
+ remediation,
249
+ unsupported_keys: unsupportedKeys,
250
+ };
251
+ }
252
+
253
+ // ---------------------------------------------------------------------------
254
+ // Activation-time warning formatter
255
+ // ---------------------------------------------------------------------------
256
+
257
+ /**
258
+ * Format a ConfigValidationResult into concise, deterministic log lines
259
+ * suitable for activation-time console output.
260
+ *
261
+ * Returns an empty array when config is fully valid with no warnings.
262
+ * Never throws.
263
+ *
264
+ * @param {ConfigValidationResult} result Output from validateConfig().
265
+ * @returns {string[]} Log lines (each prefixed with [trucore-atf]).
266
+ */
267
+ export function formatActivationWarnings(result) {
268
+ if (!result || typeof result !== "object") {
269
+ return [];
270
+ }
271
+
272
+ const hasErrors = result.errors && result.errors.length > 0;
273
+ const hasWarnings = result.warnings && result.warnings.length > 0;
274
+ const hasUnsupported =
275
+ result.unsupported_keys && result.unsupported_keys.length > 0;
276
+
277
+ if (!hasErrors && !hasWarnings && !hasUnsupported) {
278
+ return [];
279
+ }
280
+
281
+ /** @type {string[]} */
282
+ const lines = [];
283
+
284
+ lines.push(
285
+ `[trucore-atf] Plugin activated with config issues (plugin still operational):`,
286
+ );
287
+
288
+ if (hasErrors) {
289
+ for (const err of result.errors) {
290
+ lines.push(`[trucore-atf] ERROR: ${err}`);
291
+ }
292
+ }
293
+
294
+ if (hasWarnings) {
295
+ for (const w of result.warnings) {
296
+ lines.push(`[trucore-atf] WARNING: ${w}`);
297
+ }
298
+ }
299
+
300
+ if (hasUnsupported) {
301
+ lines.push(
302
+ `[trucore-atf] Unsupported keys: ${result.unsupported_keys.join(", ")}`,
303
+ );
304
+ }
305
+
306
+ if (result.remediation && result.remediation.length > 0) {
307
+ lines.push(
308
+ `[trucore-atf] Run atf_integration_doctor for full details and remediation.`,
309
+ );
310
+ } else {
311
+ lines.push(
312
+ `[trucore-atf] Run atf_integration_doctor for full details.`,
313
+ );
314
+ }
315
+
316
+ return lines;
317
+ }
@@ -0,0 +1,217 @@
1
+ /**
2
+ * deny_codes.mjs — Universal ATF deny / reason / error code vocabularies
3
+ *
4
+ * Platform-agnostic code catalogs used across all ATF surfaces.
5
+ * These are the CANONICAL source of truth for deny and reason codes.
6
+ *
7
+ * Exports:
8
+ * - REASON_CODE_CATALOG — deny reason code explanations (swap/perps/lending/general)
9
+ * - CLAIM_DENY_CODES — billing claim deny/error codes
10
+ * - REASON_CATEGORIES — canonical categories for reason codes
11
+ *
12
+ * No dependencies. No side effects. Pure constants.
13
+ */
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Reason categories
17
+ // ---------------------------------------------------------------------------
18
+
19
+ /**
20
+ * Canonical categories for ATF reason codes.
21
+ */
22
+ export const REASON_CATEGORIES = Object.freeze({
23
+ SWAP: "swap",
24
+ PERPS: "perps",
25
+ LENDING: "lending",
26
+ GENERAL: "general",
27
+ UNKNOWN: "unknown",
28
+ });
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Reason code catalog — maps deny codes to human explanations
32
+ // ---------------------------------------------------------------------------
33
+
34
+ /**
35
+ * Canonical reason code catalog: maps ATF deny reason codes to structured
36
+ * explanations. Used by tx_explain and any future surface that needs to
37
+ * explain a deny decision.
38
+ *
39
+ * Each entry has:
40
+ * - category: one of REASON_CATEGORIES values
41
+ * - explanation: human-readable description
42
+ * - remediation: actionable fix instruction
43
+ *
44
+ * @type {Readonly<Record<string, {category: string, explanation: string, remediation: string}>>}
45
+ */
46
+ export const REASON_CODE_CATALOG = Object.freeze({
47
+ // --- DEX / Swap ---
48
+ DEX_VENUE_NOT_ALLOWED: {
49
+ category: REASON_CATEGORIES.SWAP,
50
+ explanation:
51
+ "The swap venue (DEX) is not in the policy's allowed_venues list.",
52
+ remediation:
53
+ "Add the venue to swap_policy.allowed_venues in your policy YAML.",
54
+ },
55
+ DEX_SLIPPAGE_TOO_HIGH: {
56
+ category: REASON_CATEGORIES.SWAP,
57
+ explanation:
58
+ "The requested slippage exceeds the policy's max_slippage_bps limit.",
59
+ remediation:
60
+ "Lower slippage_bps in your intent or raise max_slippage_bps in policy.",
61
+ },
62
+ SWAP_VENUE_NOT_ALLOWED: {
63
+ category: REASON_CATEGORIES.SWAP,
64
+ explanation:
65
+ "The swap venue is not in the policy's allowed_venues list.",
66
+ remediation:
67
+ "Add the venue to swap_policy.allowed_venues in your policy YAML.",
68
+ },
69
+ SWAP_SLIPPAGE_TOO_HIGH: {
70
+ category: REASON_CATEGORIES.SWAP,
71
+ explanation:
72
+ "The slippage exceeds the policy's maximum allowed slippage.",
73
+ remediation:
74
+ "Lower slippage_bps or raise max_slippage_bps in policy.",
75
+ },
76
+ // --- Perps ---
77
+ PERPS_MARKET_NOT_ALLOWED: {
78
+ category: REASON_CATEGORIES.PERPS,
79
+ explanation:
80
+ "The perpetuals market is not in the policy's allowed_markets list.",
81
+ remediation:
82
+ "Add the market to perps_policy.allowed_markets in your policy YAML.",
83
+ },
84
+ PERPS_ORDER_TYPE_NOT_ALLOWED: {
85
+ category: REASON_CATEGORIES.PERPS,
86
+ explanation:
87
+ "The order type is not in the policy's allowed_order_types list.",
88
+ remediation:
89
+ "Add the order type to perps_policy.allowed_order_types.",
90
+ },
91
+ PERPS_LEVERAGE_TOO_HIGH: {
92
+ category: REASON_CATEGORIES.PERPS,
93
+ explanation:
94
+ "The leverage exceeds the policy's max_leverage limit.",
95
+ remediation:
96
+ "Lower leverage or raise max_leverage_x10 in perps_policy.",
97
+ },
98
+ PERPS_VENUE_NOT_ALLOWED: {
99
+ category: REASON_CATEGORIES.PERPS,
100
+ explanation:
101
+ "The perps venue is not in the policy's allowed_venues list.",
102
+ remediation:
103
+ "Add the venue to perps_policy.allowed_venues.",
104
+ },
105
+ PERPS_POSITION_SIZE_TOO_LARGE: {
106
+ category: REASON_CATEGORIES.PERPS,
107
+ explanation:
108
+ "The position size exceeds the policy's maximum.",
109
+ remediation:
110
+ "Lower position size or raise the limit in perps_policy.",
111
+ },
112
+ // --- Lending ---
113
+ LEND_VENUE_NOT_ALLOWED: {
114
+ category: REASON_CATEGORIES.LENDING,
115
+ explanation:
116
+ "The lending venue is not in the policy's allowed_venues list.",
117
+ remediation:
118
+ "Add the venue to lending_policy.allowed_venues.",
119
+ },
120
+ LEND_MARKET_NOT_ALLOWED: {
121
+ category: REASON_CATEGORIES.LENDING,
122
+ explanation:
123
+ "The lending market is not in the policy's allowed_markets list.",
124
+ remediation:
125
+ "Add the market to lending_policy.allowed_markets.",
126
+ },
127
+ LEND_AMOUNT_TOO_LARGE: {
128
+ category: REASON_CATEGORIES.LENDING,
129
+ explanation:
130
+ "The deposit/withdrawal amount exceeds the policy's maximum.",
131
+ remediation:
132
+ "Lower the amount or raise the limit in lending_policy.",
133
+ },
134
+ // --- General ---
135
+ CHAIN_NOT_SUPPORTED: {
136
+ category: REASON_CATEGORIES.GENERAL,
137
+ explanation:
138
+ "The chain_id is not supported or recognized by ATF.",
139
+ remediation:
140
+ "Verify chain_id is correct (e.g. 'solana').",
141
+ },
142
+ INTENT_TYPE_UNKNOWN: {
143
+ category: REASON_CATEGORIES.GENERAL,
144
+ explanation:
145
+ "The intent_type is not recognized by ATF.",
146
+ remediation:
147
+ "Use a supported intent_type: swap, lend_deposit, lend_withdraw, " +
148
+ "perps_open, perps_close.",
149
+ },
150
+ POLICY_NOT_CONFIGURED: {
151
+ category: REASON_CATEGORIES.GENERAL,
152
+ explanation:
153
+ "No policy is configured for this intent type.",
154
+ remediation:
155
+ "Create and load a policy YAML with the relevant section " +
156
+ "(swap_policy, perps_policy, lending_policy).",
157
+ },
158
+ FEATURE_GATE_DISABLED: {
159
+ category: REASON_CATEGORIES.GENERAL,
160
+ explanation:
161
+ "A required feature gate is not enabled.",
162
+ remediation:
163
+ "Set the relevant ATF_ENABLE_* environment variable to 1.",
164
+ },
165
+ });
166
+
167
+ // ---------------------------------------------------------------------------
168
+ // Claim deny codes — billing claim verification
169
+ // ---------------------------------------------------------------------------
170
+
171
+ /**
172
+ * Structured deny/error codes for billing claim verification results.
173
+ */
174
+ export const CLAIM_DENY_CODES = Object.freeze({
175
+ /** Transaction signature is malformed or empty. */
176
+ INVALID_SIGNATURE: "INVALID_SIGNATURE",
177
+ /** Transaction not found on-chain. */
178
+ TX_NOT_FOUND: "TX_NOT_FOUND",
179
+ /** Transaction exists but is not finalized/confirmed. */
180
+ TX_NOT_FINALIZED: "TX_NOT_FINALIZED",
181
+ /** Transaction exists but execution failed. */
182
+ TX_FAILED: "TX_FAILED",
183
+ /** Payment recipient does not match ATF treasury. */
184
+ WRONG_RECIPIENT: "WRONG_RECIPIENT",
185
+ /** Token mint does not match accepted payment rails. */
186
+ WRONG_MINT: "WRONG_MINT",
187
+ /** Asset type is not supported for payment. */
188
+ UNSUPPORTED_ASSET: "UNSUPPORTED_ASSET",
189
+ /** Payment amount is below minimum threshold. */
190
+ INSUFFICIENT_AMOUNT: "INSUFFICIENT_AMOUNT",
191
+ /** Package ID is not recognized. */
192
+ UNKNOWN_PACKAGE: "UNKNOWN_PACKAGE",
193
+ /** Transaction signature has already been claimed. */
194
+ DUPLICATE_CLAIM: "DUPLICATE_CLAIM",
195
+ /** Required claim parameters are missing. */
196
+ MISSING_PARAMS: "MISSING_PARAMS",
197
+ /** RPC infrastructure error during verification. */
198
+ RPC_ERROR: "RPC_ERROR",
199
+ });
200
+
201
+ // ---------------------------------------------------------------------------
202
+ // Convenience: all deny code keys as a frozen array
203
+ // ---------------------------------------------------------------------------
204
+
205
+ /**
206
+ * All reason code keys from the catalog, as a frozen array.
207
+ */
208
+ export const REASON_CODE_KEYS = Object.freeze(
209
+ Object.keys(REASON_CODE_CATALOG),
210
+ );
211
+
212
+ /**
213
+ * All claim deny code keys, as a frozen array.
214
+ */
215
+ export const CLAIM_DENY_CODE_KEYS = Object.freeze(
216
+ Object.keys(CLAIM_DENY_CODES),
217
+ );
@@ -0,0 +1,52 @@
1
+ /**
2
+ * contracts/index.mjs — Universal ATF contract layer barrel export
3
+ *
4
+ * Re-exports all universal contract primitives from a single entry point.
5
+ *
6
+ * Usage:
7
+ * import { RESPONSE_STATUS, REASON_CODE_CATALOG, buildResult } from "./contracts/index.mjs";
8
+ *
9
+ * This module is the canonical import point for any code that needs
10
+ * ATF's platform-agnostic contract definitions.
11
+ *
12
+ * Adapter-specific code (OpenClaw, CLI, API) should import from here
13
+ * rather than duplicating constants.
14
+ */
15
+
16
+ // Status code vocabularies
17
+ export {
18
+ RESPONSE_STATUS,
19
+ DOCTOR_STATUS,
20
+ CLAIM_STATUS,
21
+ BACKEND_STATUS,
22
+ PREFLIGHT_STATUS,
23
+ ADOPTION_TIER,
24
+ SEVERITY,
25
+ SEVERITY_RANK,
26
+ } from "./status_codes.mjs";
27
+
28
+ // Deny / reason code vocabularies
29
+ export {
30
+ REASON_CODE_CATALOG,
31
+ CLAIM_DENY_CODES,
32
+ REASON_CATEGORIES,
33
+ REASON_CODE_KEYS,
34
+ CLAIM_DENY_CODE_KEYS,
35
+ } from "./deny_codes.mjs";
36
+
37
+ // Contract family schemas
38
+ export {
39
+ CONTRACT_FAMILIES,
40
+ UNIVERSAL_RESULT_FIELDS,
41
+ ADAPTER_ENVELOPE_FIELDS,
42
+ TOOL_FAMILY_MAP,
43
+ CANONICAL_TOOLS,
44
+ CANONICAL_TOOL_COUNT,
45
+ } from "./schemas.mjs";
46
+
47
+ // Universal result builder
48
+ export {
49
+ buildResult,
50
+ buildErrorResult,
51
+ buildMeta,
52
+ } from "./result_builder.mjs";