callx402 1.0.1 → 1.0.3
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/.github/npm-publish.yml.staged +27 -0
- package/.github/workflows/npm-publish.yml +29 -0
- package/.github/workflows/review-ci.yml +24 -0
- package/README.md +171 -10
- package/bin/callx402.js +300 -13
- package/core/index.js +35 -6
- package/core/payer-auth.js +59 -0
- package/core/rail.js +317 -0
- package/core/veyline.js +240 -0
- package/docs/README.md +11 -9
- package/docs/action-reference.md +2 -2
- package/docs/cli-help-verified-2026-10-06.txt +36 -0
- package/docs/problem-map.md +30 -15
- package/docs/signed-redemption.md +84 -0
- package/docs/troubleshooting/cx402-duppay-001-duplicate-payment-risk.md +135 -0
- package/docs/troubleshooting/cx402-paidnores-001-paid-no-result.md +3 -1
- package/docs/troubleshooting/cx402-safretry-001-retry-after-payment.md +13 -1
- package/docs/what-is-callx402.md +5 -4
- package/logo-400.png +0 -0
- package/mcp/README.md +5 -0
- package/mcp/index.js +1 -1
- package/package.json +4 -3
- package/server.json +2 -2
- package/test/cli-dispatch.test.js +38 -19
- package/test/helpers.js +20 -0
- package/test/integrity.test.js +2 -1
- package/test/intent-pipeline.test.js +2 -3
- package/test/payer-auth.test.js +92 -0
- package/test/remote.test.js +2 -3
- package/test/safety.test.js +4 -5
- package/test/sdk.test.js +3 -4
- package/test/server.test.js +4 -5
- package/test/subsystems.test.js +7 -8
- package/test/veyline-commands.test.js +193 -0
- package/mcp-publisher +0 -0
package/bin/callx402.js
CHANGED
|
@@ -17,25 +17,247 @@ const path = require('path');
|
|
|
17
17
|
|
|
18
18
|
const ROOT = path.resolve(__dirname, '..');
|
|
19
19
|
const core = require(path.join(ROOT, 'core'));
|
|
20
|
+
const rail = require(path.join(ROOT, 'core', 'rail'));
|
|
20
21
|
const { loadConfig, getConfig, setConfig, listConfig } = require(path.join(ROOT, 'core', 'config'));
|
|
21
22
|
const { EXIT } = require(path.join(ROOT, 'core', 'envelope'));
|
|
22
23
|
|
|
23
24
|
const PKG = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
|
|
24
25
|
const VERSION = PKG.version || '1.0.0';
|
|
25
26
|
|
|
26
|
-
const COMMANDS = ['diagnose', 'rescue', 'route', 'resolve', 'doctor', 'execute', 'monitor', 'status', 'preflight', 'inspect', 'config'];
|
|
27
|
+
const COMMANDS = ['diagnose', 'rescue', 'route', 'resolve', 'doctor', 'execute', 'monitor', 'status', 'preflight', 'inspect', 'config', 'evidence', 'explain', 'recover'];
|
|
28
|
+
|
|
29
|
+
// Paid on-demand rail actions that have no CLI command (architecture: the CLI
|
|
30
|
+
// is the free local tier; these run on the paid rail via the quote-then-pay
|
|
31
|
+
// invocation in docs/problem-map.md). Named here so a mistyped/rail-action
|
|
32
|
+
// command gets a routing hint instead of a dead-end usage error.
|
|
33
|
+
const RAIL_ONLY_ACTIONS = {
|
|
34
|
+
safe_retry: 'safe_retry', 'safe-retry': 'safe_retry',
|
|
35
|
+
duplicate_payment_risk: 'duplicate_payment_risk', 'duplicate-payment-risk': 'duplicate_payment_risk',
|
|
36
|
+
failure_classification: 'failure_classification', 'failure-classification': 'failure_classification',
|
|
37
|
+
settlement_interpretation: 'settlement_interpretation', 'settlement-interpretation': 'settlement_interpretation',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Per-command help: `callx402 <command> --help` prints the command's own usage
|
|
41
|
+
// instead of the global help. Free local tier; paid rail counterparts (where
|
|
42
|
+
// they exist) are named with their docs pointer.
|
|
43
|
+
const COMMAND_HELP = {
|
|
44
|
+
diagnose: `callx402 diagnose — run the x402 doctor over supplied evidence.
|
|
45
|
+
|
|
46
|
+
Usage:
|
|
47
|
+
callx402 diagnose --target <url> [--evidence <json|@file>]
|
|
48
|
+
callx402 diagnose --evidence '{"txHash":"0x..."}'
|
|
49
|
+
|
|
50
|
+
Read-only: classifies the failure stage from evidence you supply; performs no
|
|
51
|
+
live probing and changes no state. Requires PAYLOAD_MCP_DOCTOR=1 (exit 3 names
|
|
52
|
+
the flag when it is off).
|
|
53
|
+
|
|
54
|
+
Paid one-off counterpart: the 'diagnose' rail action (quote first, then pay;
|
|
55
|
+
see docs/problem-map.md). The MCP tool x402_diagnose covers the same free
|
|
56
|
+
diagnostic.
|
|
57
|
+
|
|
58
|
+
Exit codes: 0 ok · 1 failure · 2 usage · 3 disabled/unreachable ·
|
|
59
|
+
4 budget refused · 5 settlement unknown (never auto-retry, never repay).
|
|
60
|
+
`,
|
|
61
|
+
rescue: `callx402 rescue — incident triage for stuck or failed x402 transactions.
|
|
62
|
+
|
|
63
|
+
Usage:
|
|
64
|
+
callx402 rescue --incident <id> [--network <net>] [--auth]
|
|
65
|
+
|
|
66
|
+
Read-only triage: detects the incident, quotes the rescue, presents the
|
|
67
|
+
free-vs-paid offer. Paid execution is NEVER performed by this command.
|
|
68
|
+
Without --auth (or CALLX402_AUTH_TOKEN / config rescueAuth) it refuses with
|
|
69
|
+
exit 3. Requires PAYLOAD_RESCUE=1.
|
|
70
|
+
|
|
71
|
+
Paid one-off counterpart: the 'rescue' rail action (see docs/problem-map.md).
|
|
72
|
+
|
|
73
|
+
Exit codes: 0 ok · 1 failure · 2 usage · 3 disabled/unreachable or auth_required ·
|
|
74
|
+
4 budget refused · 5 settlement unknown (never auto-retry, never repay).
|
|
75
|
+
`,
|
|
76
|
+
route: `callx402 route — select a tool/route for a goal under a routing policy.
|
|
77
|
+
|
|
78
|
+
Usage:
|
|
79
|
+
callx402 route --goal <text> [--speed fast|balanced|cheap]
|
|
80
|
+
[--networks a,b] [--assets a,b]
|
|
81
|
+
|
|
82
|
+
Selection only: nothing executed, no money moved. Requires PAYLOAD_MCP_ROUTES=1.
|
|
83
|
+
|
|
84
|
+
Exit codes: 0 ok · 1 failure · 2 usage (missing --goal) · 3 disabled/unreachable ·
|
|
85
|
+
4 budget refused · 5 settlement unknown (never auto-retry, never repay).
|
|
86
|
+
`,
|
|
87
|
+
resolve: `callx402 resolve — resolve the settlement state of a payment from evidence.
|
|
88
|
+
|
|
89
|
+
Usage:
|
|
90
|
+
callx402 resolve --evidence '{"txHash":"0x...","network":"base"}'
|
|
91
|
+
callx402 resolve --evidence @evidence.json
|
|
92
|
+
|
|
93
|
+
Read-only analysis: maps evidence to DEFINITELY_PAID, DEFINITELY_NOT_PAID,
|
|
94
|
+
AUTHORIZED_NOT_SETTLED, SETTLEMENT_PENDING, CONFLICT, or UNKNOWN. Never
|
|
95
|
+
queries the chain, never signs, never retries, never repays. Requires
|
|
96
|
+
PAYLOAD_SETTLEMENT_RESOLVER=1.
|
|
97
|
+
|
|
98
|
+
Thin evidence -> settlement UNKNOWN, exit 5. That is the stop sign: do not
|
|
99
|
+
retry, do not repay; gather fresh evidence or escalate.
|
|
100
|
+
|
|
101
|
+
Paid one-off counterpart: the 'resolve' rail action (quote first, then pay;
|
|
102
|
+
see docs/problem-map.md). The MCP tool x402_resolve covers the same free
|
|
103
|
+
diagnostic.
|
|
104
|
+
|
|
105
|
+
Exit codes: 0 ok · 1 failure · 2 usage (malformed evidence JSON) ·
|
|
106
|
+
3 disabled/unreachable · 4 budget refused ·
|
|
107
|
+
5 settlement unknown (never auto-retry, never repay).
|
|
108
|
+
`,
|
|
109
|
+
doctor: `callx402 doctor — alias of diagnose.
|
|
110
|
+
|
|
111
|
+
Usage:
|
|
112
|
+
callx402 doctor --target <url> [--evidence <json|@file>]
|
|
113
|
+
|
|
114
|
+
Identical to diagnose (the envelope's action field reads "diagnose").
|
|
115
|
+
Requires PAYLOAD_MCP_DOCTOR=1.
|
|
116
|
+
|
|
117
|
+
Exit codes: 0 ok · 1 failure · 2 usage · 3 disabled/unreachable ·
|
|
118
|
+
4 budget refused · 5 settlement unknown (never auto-retry, never repay).
|
|
119
|
+
`,
|
|
120
|
+
execute: `callx402 execute — run the intent pipeline under an explicit budget.
|
|
121
|
+
|
|
122
|
+
Usage:
|
|
123
|
+
callx402 execute --intent <text> [--max-budget N] [--dry-run]
|
|
124
|
+
[--idempotency-key K] [--approve] [--force-retry]
|
|
125
|
+
|
|
126
|
+
Budget-gated and idempotent. --dry-run plans only. Over-budget plans refuse
|
|
127
|
+
with exit 4 and zero side effects. --force-retry after a stored run whose
|
|
128
|
+
settlement is UNKNOWN is refused (exit 5): it could double-spend.
|
|
129
|
+
|
|
130
|
+
Exit codes: 0 ok · 1 failure · 2 usage (missing --intent) ·
|
|
131
|
+
3 disabled/unreachable · 4 budget refused ·
|
|
132
|
+
5 settlement unknown (never auto-retry, never repay).
|
|
133
|
+
`,
|
|
134
|
+
monitor: `callx402 monitor — Sentinel snapshot of x402 incident state.
|
|
135
|
+
|
|
136
|
+
Usage:
|
|
137
|
+
callx402 monitor --once single snapshot (default)
|
|
138
|
+
callx402 monitor --watch [--interval <ms>]
|
|
139
|
+
|
|
140
|
+
Read-only. The default sentinel is inert ("no live tracking") unless
|
|
141
|
+
PAYLOAD_SENTINEL=1 enables the live tracker.
|
|
142
|
+
|
|
143
|
+
Exit codes: 0 ok · 1 failure · 2 usage · 3 disabled/unreachable ·
|
|
144
|
+
4 budget refused · 5 settlement unknown (never auto-retry, never repay).
|
|
145
|
+
`,
|
|
146
|
+
status: `callx402 status — per-subsystem reachability report.
|
|
147
|
+
|
|
148
|
+
Usage:
|
|
149
|
+
callx402 status [--json]
|
|
150
|
+
|
|
151
|
+
Requires nothing; works even when every subsystem is disabled. Reports
|
|
152
|
+
reachable/enabled/version/flag for all 15 subsystem handles. Always exits 0
|
|
153
|
+
on success.
|
|
154
|
+
`,
|
|
155
|
+
preflight: `callx402 preflight — preflight checks over a supplied context.
|
|
156
|
+
|
|
157
|
+
Usage:
|
|
158
|
+
callx402 preflight [--json]
|
|
159
|
+
|
|
160
|
+
Read-only: validates setup before a paid run; provisions or repairs nothing.
|
|
161
|
+
Requires PAYLOAD_MCP_PREFLIGHT=1.
|
|
162
|
+
|
|
163
|
+
Paid one-off counterpart: the 'preflight' rail action (see docs/problem-map.md).
|
|
164
|
+
|
|
165
|
+
Exit codes: 0 ok · 1 failure · 2 usage · 3 disabled/unreachable ·
|
|
166
|
+
4 budget refused · 5 settlement unknown (never auto-retry, never repay).
|
|
167
|
+
`,
|
|
168
|
+
inspect: `callx402 inspect — capability-graph lookup or stats.
|
|
169
|
+
|
|
170
|
+
Usage:
|
|
171
|
+
callx402 inspect [--query <text>]
|
|
172
|
+
|
|
173
|
+
Lookups only. Requires PAYLOAD_MCP_FABRIC=1; without registered tools it
|
|
174
|
+
returns zero candidates honestly.
|
|
175
|
+
|
|
176
|
+
Exit codes: 0 ok · 1 failure · 2 usage · 3 disabled/unreachable ·
|
|
177
|
+
4 budget refused · 5 settlement unknown (never auto-retry, never repay).
|
|
178
|
+
`,
|
|
179
|
+
evidence: `callx402 evidence — show recorded evidence for an operation.
|
|
180
|
+
|
|
181
|
+
Usage:
|
|
182
|
+
callx402 evidence <operationId> [--dir <path>]
|
|
183
|
+
|
|
184
|
+
Reads the Veyline operation ledger: events, latest per-plane states (payment,
|
|
185
|
+
execution, delivery), protocols. Unknown operations are honest ("no events
|
|
186
|
+
recorded"), still exit 0. Requires PAYLOAD_VEYLINE_LEDGER=1. First step in any
|
|
187
|
+
incident: returns what is recorded, nothing invented.
|
|
188
|
+
|
|
189
|
+
Paid one-off counterpart: the 'evidence' rail action (quote first, then pay;
|
|
190
|
+
see docs/problem-map.md). The MCP tool x402_evidence covers the same free
|
|
191
|
+
diagnostic.
|
|
192
|
+
|
|
193
|
+
Exit codes: 0 ok · 1 failure · 2 usage (missing operationId) ·
|
|
194
|
+
3 disabled/unreachable · 4 budget refused ·
|
|
195
|
+
5 settlement unknown (never auto-retry, never repay).
|
|
196
|
+
`,
|
|
197
|
+
explain: `callx402 explain — explain an operation's state in plain language.
|
|
198
|
+
|
|
199
|
+
Usage:
|
|
200
|
+
callx402 explain <operationId> [--dir <path>]
|
|
201
|
+
|
|
202
|
+
Assessments from recorded ledger evidence: NO_BASIS, INCOMPLETE (do not retry,
|
|
203
|
+
do not repay), KNOWN_SAFE, RECOVERY_CANDIDATE (run 'recover'), PARTIAL.
|
|
204
|
+
Requires PAYLOAD_VEYLINE_LEDGER=1.
|
|
205
|
+
|
|
206
|
+
Paid one-off counterpart: the 'explain' rail action (see docs/problem-map.md).
|
|
207
|
+
The MCP tool x402_explain covers the same free diagnostic.
|
|
208
|
+
|
|
209
|
+
Exit codes: 0 ok · 1 failure · 2 usage (missing operationId) ·
|
|
210
|
+
3 disabled/unreachable · 4 budget refused ·
|
|
211
|
+
5 settlement unknown (never auto-retry, never repay).
|
|
212
|
+
`,
|
|
213
|
+
recover: `callx402 recover — report the safe recovery decision (read-only).
|
|
214
|
+
|
|
215
|
+
Usage:
|
|
216
|
+
callx402 recover <operationId> [--identity <id> | --evidence <json>]
|
|
217
|
+
|
|
218
|
+
Evaluates recorded history against the operation identity and returns the
|
|
219
|
+
recovery decision: RECOVERABLE, SAFE_RETRY, EXECUTED_BUT_UNRECOVERABLE, or
|
|
220
|
+
SETTLEMENT_UNKNOWN, mapped to KNOWN_SAFE or HUMAN_REVIEW. Never charges,
|
|
221
|
+
never executes, never repays. Requires PAYLOAD_VEYLINE_RECOVERY=1.
|
|
222
|
+
|
|
223
|
+
This is the read-only answer to "is a retry safe?" — a SAFE_RETRY verdict
|
|
224
|
+
here is advisory; performing the safe action stays with your own adapters.
|
|
225
|
+
For the paid one-off verdict on a specific retry plan, use the 'safe_retry'
|
|
226
|
+
rail action (quote first, then pay; see docs/problem-map.md). The MCP tool
|
|
227
|
+
x402_recover covers the same free diagnostic.
|
|
228
|
+
|
|
229
|
+
Exit codes: 0 ok · 1 failure · 2 usage (missing identity/evidence) ·
|
|
230
|
+
3 disabled/unreachable · 4 budget refused ·
|
|
231
|
+
5 settlement unknown (never auto-retry, never repay).
|
|
232
|
+
`,
|
|
233
|
+
config: `callx402 config — manage local configuration.
|
|
234
|
+
|
|
235
|
+
Usage:
|
|
236
|
+
callx402 config list
|
|
237
|
+
callx402 config get <key>
|
|
238
|
+
callx402 config set <key> <value>
|
|
239
|
+
|
|
240
|
+
Valid keys: mode, remoteUrl, authToken, v2Root, defaultNetwork, defaultAsset,
|
|
241
|
+
defaultBudgetUsd, approvalThresholdUsd, rescueAuth. Values persist to
|
|
242
|
+
~/.config/callx402/config.json (CALLX402_CONFIG overrides the path);
|
|
243
|
+
CALLX402_* environment variables override file values at runtime.
|
|
244
|
+
|
|
245
|
+
Exit codes: 0 ok · 2 usage (unknown key or subcommand).
|
|
246
|
+
`,
|
|
247
|
+
};
|
|
27
248
|
|
|
28
249
|
const HELP = `callx402 ${VERSION} — universal action layer for x402 infrastructure.
|
|
29
|
-
|
|
30
|
-
|
|
250
|
+
Install it and it works: subsystem commands route to the hosted Payload Rail
|
|
251
|
+
by default (free quote first, then pay-per-action). Nothing here executes
|
|
252
|
+
payments by itself.
|
|
31
253
|
|
|
32
254
|
Usage:
|
|
33
255
|
callx402 <command> [args] [options]
|
|
34
256
|
callx402 "natural language intent" intent mode (plans, never executes blindly)
|
|
35
257
|
callx402 --help | --version
|
|
36
258
|
|
|
37
|
-
Commands:
|
|
38
|
-
diagnose run the x402 doctor over supplied evidence [--target
|
|
259
|
+
Commands (hosted by default — zero setup):
|
|
260
|
+
diagnose run the x402 doctor over supplied evidence [--target <url|json>] [--evidence <json|@file>]
|
|
39
261
|
rescue incident triage (auth-gated; read-only without auth) --incident <id>
|
|
40
262
|
route select a tool/route for a goal --goal <text>
|
|
41
263
|
resolve resolve settlement state from evidence --evidence <json|@file>
|
|
@@ -45,12 +267,15 @@ Commands:
|
|
|
45
267
|
status per-subsystem reachable/enabled/version report
|
|
46
268
|
preflight run MCP preflight checks
|
|
47
269
|
inspect capability-graph lookup/stats [--query <text>]
|
|
48
|
-
|
|
270
|
+
evidence show recorded evidence for an operation <operationId> [--dir <path>]
|
|
271
|
+
explain explain an operation's state in plain language <operationId> [--dir <path>]
|
|
272
|
+
recover report the safe recovery decision (read-only) <operationId> [--identity <id> | --evidence <json>]
|
|
273
|
+
config list | get <key> | set <key> <v>
|
|
49
274
|
|
|
50
275
|
Options:
|
|
51
276
|
--json print the full result envelope as JSON
|
|
52
277
|
--timeout <ms> fail the action if it exceeds <ms>
|
|
53
|
-
--approve
|
|
278
|
+
--approve approve the quoted price non-interactively
|
|
54
279
|
--force-retry explicit retry flag (REFUSED when prior settlement is UNKNOWN)
|
|
55
280
|
--dry-run plan only; no execution side effects
|
|
56
281
|
--speed fast|balanced|cheap routing policy hint
|
|
@@ -61,6 +286,15 @@ Options:
|
|
|
61
286
|
|
|
62
287
|
Exit codes: 0 ok · 1 failure · 2 usage · 3 disabled/unreachable ·
|
|
63
288
|
4 budget refused · 5 settlement unknown (never auto-retry, never repay).
|
|
289
|
+
|
|
290
|
+
First run (no setup): callx402 diagnose --evidence '{"txHash":"0x..."}'
|
|
291
|
+
free quote first, then pay-per-action on the hosted rail.
|
|
292
|
+
callx402 status honest subsystem reachability report
|
|
293
|
+
callx402 'plain words' plans only; nothing executes, no money moves
|
|
294
|
+
Execution modes: hosted rail by default. Set CALLX402_LOCAL=1 to prefer the
|
|
295
|
+
local v2.0.0 runtime tree instead (advanced self-hosted mode): set
|
|
296
|
+
CALLX402_V2_ROOT to its path and enable each subsystem's flag
|
|
297
|
+
(run 'callx402 status' to see flag names).
|
|
64
298
|
`;
|
|
65
299
|
|
|
66
300
|
function parseArgv(argv) {
|
|
@@ -71,9 +305,11 @@ function parseArgv(argv) {
|
|
|
71
305
|
const KNOWN_FLAGS = new Set([
|
|
72
306
|
'help', 'version', 'json', 'timeout', 'maxBudget', 'approvalThreshold',
|
|
73
307
|
'idempotencyKey', 'evidence', 'incident', 'goal', 'intent', 'query',
|
|
308
|
+
'txHash', 'payerAuth', 'creditId',
|
|
74
309
|
'target', 'speed', 'interval', 'network', 'networks', 'asset', 'assets',
|
|
75
310
|
'providers', 'deadline', 'risk',
|
|
76
311
|
'dryRun', 'approve', 'forceRetry', 'retry', 'auth', 'watch', 'once',
|
|
312
|
+
'dir', 'operationId', 'identity',
|
|
77
313
|
]);
|
|
78
314
|
let i = 0;
|
|
79
315
|
while (i < argv.length) {
|
|
@@ -89,7 +325,8 @@ function parseArgv(argv) {
|
|
|
89
325
|
i += 1;
|
|
90
326
|
continue;
|
|
91
327
|
}
|
|
92
|
-
const NEEDS_VALUE = new Set(['timeout', 'maxBudget', 'approvalThreshold', 'idempotencyKey', 'evidence', 'incident', 'goal', 'intent', 'query', 'target',
|
|
328
|
+
const NEEDS_VALUE = new Set(['timeout', 'maxBudget', 'approvalThreshold', 'idempotencyKey', 'evidence', 'incident', 'goal', 'intent', 'query', 'target',
|
|
329
|
+
'txHash', 'payerAuth', 'creditId', 'speed', 'interval', 'network', 'networks', 'asset', 'assets', 'providers', 'deadline', 'risk', 'dir', 'operationId', 'identity']);
|
|
93
330
|
if (NEEDS_VALUE.has(name)) {
|
|
94
331
|
if (val !== null) out.flags[name] = val;
|
|
95
332
|
else if (i + 1 < argv.length && !argv[i + 1].startsWith('-')) out.flags[name] = argv[++i];
|
|
@@ -111,8 +348,15 @@ function parseArgv(argv) {
|
|
|
111
348
|
return out;
|
|
112
349
|
}
|
|
113
350
|
|
|
114
|
-
function usageError(message) {
|
|
115
|
-
|
|
351
|
+
function usageError(message, first) {
|
|
352
|
+
let msg = message;
|
|
353
|
+
// A rail-only paid action typed as a command is a routing hint, not a
|
|
354
|
+
// dead end: point at the quote-then-pay invocation instead.
|
|
355
|
+
if (first && RAIL_ONLY_ACTIONS[first] && /^unknown command /.test(message)) {
|
|
356
|
+
msg = `${message} — '${RAIL_ONLY_ACTIONS[first]}' is a paid on-demand rail action, not a CLI command. ` +
|
|
357
|
+
`Get the free quote, then pay deliberately: see docs/problem-map.md`;
|
|
358
|
+
}
|
|
359
|
+
console.error(`callx402: ${msg}\nValid commands: ${COMMANDS.join(', ')}\nRun 'callx402 --help' for usage, or 'callx402 <command> --help' for one command.`);
|
|
116
360
|
process.exit(EXIT.USAGE);
|
|
117
361
|
}
|
|
118
362
|
|
|
@@ -132,17 +376,30 @@ function summarize(envelope) {
|
|
|
132
376
|
function statusSummary(envelope) {
|
|
133
377
|
const lines = [envelope.disposition || 'status'];
|
|
134
378
|
const subs = (envelope.data && envelope.data.subsystems) || {};
|
|
379
|
+
let reachable = 0;
|
|
135
380
|
for (const [name, s] of Object.entries(subs)) {
|
|
381
|
+
if (s.reachable) reachable += 1;
|
|
136
382
|
const mark = !s.reachable ? 'UNREACHABLE' : s.enabled ? 'enabled' : `disabled (${s.flag || 'no FLAG export'})`;
|
|
137
383
|
lines.push(` ${name.padEnd(12)} ${mark}${s.version ? ` v${s.version}` : ''}`);
|
|
138
384
|
}
|
|
385
|
+
if (reachable === 0 && Object.keys(subs).length > 0) {
|
|
386
|
+
lines.push('');
|
|
387
|
+
lines.push(' No subsystems reachable: subsystem commands need the v2.0.0 tree.');
|
|
388
|
+
lines.push(' Set CALLX402_V2_ROOT to its path (see README "Full subsystem commands").');
|
|
389
|
+
}
|
|
139
390
|
return lines.join('\n');
|
|
140
391
|
}
|
|
141
392
|
|
|
142
393
|
async function main() {
|
|
143
394
|
const { flags, positionals, errors } = parseArgv(process.argv.slice(2));
|
|
144
395
|
|
|
145
|
-
|
|
396
|
+
// Per-command help: `callx402 <command> --help` prints that command's own
|
|
397
|
+
// usage. Bare `--help` (or --help with an unknown word) prints the global help.
|
|
398
|
+
if (flags.help) {
|
|
399
|
+
const cmd = positionals[0];
|
|
400
|
+
if (cmd && COMMAND_HELP[cmd]) { process.stdout.write(COMMAND_HELP[cmd]); process.exit(EXIT.OK); }
|
|
401
|
+
process.stdout.write(HELP); process.exit(EXIT.OK);
|
|
402
|
+
}
|
|
146
403
|
if (flags.version) { process.stdout.write(`callx402 ${VERSION}\n`); process.exit(EXIT.OK); }
|
|
147
404
|
if (errors.length > 0) usageError(errors[0]);
|
|
148
405
|
|
|
@@ -152,8 +409,8 @@ async function main() {
|
|
|
152
409
|
// like natural language (more than one word). A single unknown word is an
|
|
153
410
|
// unknown command (exit 2), per the CLI contract.
|
|
154
411
|
const isCommand = first && COMMANDS.includes(first);
|
|
155
|
-
if (!isCommand && first && first.startsWith('-')) usageError(`unknown input '${first}'
|
|
156
|
-
if (!isCommand && positionals.length === 1 && !/\s/.test(first)) usageError(`unknown command '${first}'
|
|
412
|
+
if (!isCommand && first && first.startsWith('-')) usageError(`unknown input '${first}'`, first);
|
|
413
|
+
if (!isCommand && positionals.length === 1 && !/\s/.test(first)) usageError(`unknown command '${first}'`, first);
|
|
157
414
|
const command = isCommand ? first : (first === undefined ? null : '__intent__');
|
|
158
415
|
|
|
159
416
|
// config handled directly (not a subsystem dispatch)
|
|
@@ -214,6 +471,9 @@ async function main() {
|
|
|
214
471
|
once: !!flags.once,
|
|
215
472
|
interval: toNum(flags.interval),
|
|
216
473
|
network: flags.network,
|
|
474
|
+
dir: flags.dir,
|
|
475
|
+
operationId: flags.operationId,
|
|
476
|
+
identity: flags.identity,
|
|
217
477
|
deadline: flags.deadline,
|
|
218
478
|
risk: flags.risk,
|
|
219
479
|
networks: toList(flags.networks) || (flags.network ? [flags.network] : undefined),
|
|
@@ -232,6 +492,7 @@ async function main() {
|
|
|
232
492
|
if (command === 'execute' && !args.intent && rest.length > 0) args.intent = rest.join(' ');
|
|
233
493
|
if (command === 'inspect' && !args.query && rest.length > 0) args.query = rest.join(' ');
|
|
234
494
|
if (command === 'rescue' && !args.incident && rest.length > 0) args.incident = rest[0];
|
|
495
|
+
if ((command === 'evidence' || command === 'explain' || command === 'recover') && !args.operationId && rest.length > 0) args.operationId = rest[0];
|
|
235
496
|
}
|
|
236
497
|
|
|
237
498
|
if (Number.isNaN(args.timeoutMs)) usageError('--timeout must be a number (ms)');
|
|
@@ -239,6 +500,32 @@ async function main() {
|
|
|
239
500
|
if (args.approvalThreshold !== undefined && Number.isNaN(args.approvalThreshold)) usageError('--approval-threshold must be a number');
|
|
240
501
|
|
|
241
502
|
try {
|
|
503
|
+
// Hosted rail routing (default): subsystem commands run against the live
|
|
504
|
+
// Payload Rail with zero local setup — invoke -> free quote ->
|
|
505
|
+
// payment/authorization -> result. Set CALLX402_LOCAL=1 to prefer the
|
|
506
|
+
// local v2.0.0 runtime tree instead (advanced self-hosted mode).
|
|
507
|
+
if (rail.isRailCommand(actionName) && !rail.preferLocal()) {
|
|
508
|
+
const railResult = await rail.invokeRail(actionName,
|
|
509
|
+
{ ...args, txHash: flags.txHash, payerAuth: flags.payerAuth, creditId: flags.creditId },
|
|
510
|
+
{ yes: args.approve, json: args.json });
|
|
511
|
+
if (args.json) {
|
|
512
|
+
console.log(JSON.stringify(railResult, null, 2));
|
|
513
|
+
} else if (railResult.ok) {
|
|
514
|
+
console.log(`\nResult (via ${railResult.via}, invocation ${railResult.invocation_id}):`);
|
|
515
|
+
const ex = railResult.execution;
|
|
516
|
+
if (ex && ex.executed) {
|
|
517
|
+
if (ex.disposition) console.log(ex.disposition);
|
|
518
|
+
if (ex.result !== undefined) console.log(JSON.stringify(ex.result, null, 2));
|
|
519
|
+
} else if (ex && ex.error) {
|
|
520
|
+
console.log(`Note: ${ex.error}`);
|
|
521
|
+
}
|
|
522
|
+
if (railResult.notice) console.log(railResult.notice);
|
|
523
|
+
} else {
|
|
524
|
+
console.error(`callx402: ${railResult.error}`);
|
|
525
|
+
if (railResult.quote) console.error(`Quote was: $${railResult.quote.price_usd} (${railResult.quote.quote_id})`);
|
|
526
|
+
}
|
|
527
|
+
process.exit(railResult.ok ? EXIT.OK : EXIT.FAIL);
|
|
528
|
+
}
|
|
242
529
|
const { result, exitCode } = await core.runAction(actionName, args, { version: VERSION });
|
|
243
530
|
if (args.json) {
|
|
244
531
|
console.log(JSON.stringify(result, null, 2));
|
package/core/index.js
CHANGED
|
@@ -14,10 +14,11 @@ const { loadConfig } = require('./config');
|
|
|
14
14
|
const { getSubsystem, getStatus: statusOf, SUBSYSTEMS } = require('./subsystems');
|
|
15
15
|
const { runIntent } = require('./pipeline');
|
|
16
16
|
const { dispatchRemote } = require('./remote');
|
|
17
|
+
const { actEvidence, actExplain, actRecover } = require('./veyline');
|
|
17
18
|
|
|
18
|
-
const ACTIONS = ['diagnose', 'rescue', 'route', 'resolve', 'doctor', 'execute', 'monitor', 'status', 'preflight', 'inspect'];
|
|
19
|
+
const ACTIONS = ['diagnose', 'rescue', 'route', 'resolve', 'doctor', 'execute', 'monitor', 'status', 'preflight', 'inspect', 'evidence', 'explain', 'recover'];
|
|
19
20
|
|
|
20
|
-
/** Parse --evidence style input: JSON string or @file. */
|
|
21
|
+
/** Parse --evidence style input: JSON string or @file. Strict: bad JSON is a usage error. */
|
|
21
22
|
function readEvidenceInput(input, fs, path) {
|
|
22
23
|
if (input === undefined || input === null || input === '') return {};
|
|
23
24
|
if (typeof input !== 'string') return input;
|
|
@@ -30,6 +31,29 @@ function readEvidenceInput(input, fs, path) {
|
|
|
30
31
|
return JSON.parse(s);
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Parse diagnose input. --evidence stays strict JSON (or @file). --target
|
|
36
|
+
* accepts a plain URL or identifier string: when it is not valid JSON it is
|
|
37
|
+
* wrapped as { target } evidence so the documented
|
|
38
|
+
* `diagnose --target <url>` example works as printed.
|
|
39
|
+
*/
|
|
40
|
+
function readDiagnoseInput(args, fs, path) {
|
|
41
|
+
const ev = args.evidence;
|
|
42
|
+
if (ev !== undefined && ev !== null && ev !== '') return readEvidenceInput(ev, fs, path);
|
|
43
|
+
const t = args.target;
|
|
44
|
+
if (t === undefined || t === null || t === '') return {};
|
|
45
|
+
const s = String(t).trim();
|
|
46
|
+
if (s.startsWith('@')) {
|
|
47
|
+
const raw = fs.readFileSync(path.resolve(s.slice(1)), 'utf8');
|
|
48
|
+
return JSON.parse(raw);
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
return JSON.parse(s);
|
|
52
|
+
} catch {
|
|
53
|
+
return { target: s };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
33
57
|
function disabledResult(action, subsystem, handle) {
|
|
34
58
|
return {
|
|
35
59
|
result: fail(action, {
|
|
@@ -48,7 +72,9 @@ function unreachableResult(action, subsystem, handle) {
|
|
|
48
72
|
result: fail(action, {
|
|
49
73
|
subsystem, subsystemStatus: 'unreachable',
|
|
50
74
|
code: 'subsystem_unreachable',
|
|
51
|
-
message: `${subsystem} subsystem could not be loaded (${handle.error || 'unknown error'}). Nothing was executed
|
|
75
|
+
message: `${subsystem} subsystem could not be loaded (${handle.error || 'unknown error'}). Nothing was executed. ` +
|
|
76
|
+
`To enable: set CALLX402_V2_ROOT to the v2.0.0 subsystem tree path (see README "Full subsystem commands"), ` +
|
|
77
|
+
`or run in remote mode (CALLX402_MODE=remote, CALLX402_REMOTE_URL=<your callx402 server>).`,
|
|
52
78
|
disposition: `${subsystem} unreachable: nothing executed.`,
|
|
53
79
|
data: { error: handle.error },
|
|
54
80
|
}),
|
|
@@ -67,7 +93,7 @@ async function actDiagnose(args, ctx) {
|
|
|
67
93
|
const g = requireSubsystem('diagnose', 'doctor', ctx);
|
|
68
94
|
if (!g.handle) return g;
|
|
69
95
|
try {
|
|
70
|
-
const evidence =
|
|
96
|
+
const evidence = readDiagnoseInput(args, require('fs'), require('path'));
|
|
71
97
|
const report = g.handle.module.runMcpDoctor(evidence);
|
|
72
98
|
const whatFailed = report.whatFailed || 'unknown';
|
|
73
99
|
return {
|
|
@@ -269,7 +295,7 @@ async function actStatus(args, ctx) {
|
|
|
269
295
|
result: ok('status', {
|
|
270
296
|
subsystem: null,
|
|
271
297
|
disposition: `callx402 status: ${reachable}/${all.length} subsystems reachable, ${enabledCount}/${all.length} enabled.`,
|
|
272
|
-
data: { version: ctx.version || '
|
|
298
|
+
data: { version: ctx.version || '1.0.0', v2Root: require('./subsystems').getV2Root(ctx.config || {}), subsystems },
|
|
273
299
|
}),
|
|
274
300
|
exitCode: EXIT.OK,
|
|
275
301
|
};
|
|
@@ -314,7 +340,7 @@ async function actInspect(args, ctx) {
|
|
|
314
340
|
*/
|
|
315
341
|
async function runAction(name, args = {}, opts = {}) {
|
|
316
342
|
const config = opts.config || loadConfig();
|
|
317
|
-
const ctx = { deps: opts.deps || {}, config, timeoutMs: opts.timeoutMs, version: opts.version || '
|
|
343
|
+
const ctx = { deps: opts.deps || {}, config, timeoutMs: opts.timeoutMs, version: opts.version || '1.0.0' };
|
|
318
344
|
|
|
319
345
|
// SPEC §5: remote mode dispatches over HTTP to a callx402 server. The deps
|
|
320
346
|
// seam is local-only: an explicit subsystem override means local dispatch.
|
|
@@ -334,6 +360,9 @@ async function runAction(name, args = {}, opts = {}) {
|
|
|
334
360
|
case 'status': return actStatus(args, ctx);
|
|
335
361
|
case 'preflight': return actPreflight(args, ctx);
|
|
336
362
|
case 'inspect': return actInspect(args, ctx);
|
|
363
|
+
case 'evidence': return actEvidence(args, ctx);
|
|
364
|
+
case 'explain': return actExplain(args, ctx);
|
|
365
|
+
case 'recover': return actRecover(args, ctx);
|
|
337
366
|
default:
|
|
338
367
|
return {
|
|
339
368
|
result: fail(name, {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The client never signs messages or accepts wallet private keys.
|
|
5
|
+
* An external wallet signs the EIP-191 message and supplies a 65-byte signature.
|
|
6
|
+
* This canonical message MUST remain in lockstep with Rail payer-auth.ts.
|
|
7
|
+
*/
|
|
8
|
+
const fs = require('node:fs');
|
|
9
|
+
const { createHash } = require('node:crypto');
|
|
10
|
+
const path = require('node:path');
|
|
11
|
+
|
|
12
|
+
function actionRequestHash(action, body) {
|
|
13
|
+
const data = {
|
|
14
|
+
action,
|
|
15
|
+
evidence: body.evidence ?? null,
|
|
16
|
+
invocation_id: body.invocation_id ?? null,
|
|
17
|
+
context: body.context ?? null,
|
|
18
|
+
operation_id: body.operation_id ?? null,
|
|
19
|
+
target: body.target ?? null,
|
|
20
|
+
identity: body.identity ?? null,
|
|
21
|
+
};
|
|
22
|
+
return '0x' + createHash('sha256').update(JSON.stringify(data), 'utf8').digest('hex');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function buildAuthMessage(auth) {
|
|
26
|
+
return [
|
|
27
|
+
'Payload x402 Authorization',
|
|
28
|
+
`wallet:${String(auth.wallet).toLowerCase()}`,
|
|
29
|
+
`action:${auth.action}`,
|
|
30
|
+
`txHash:${String(auth.txHash).toLowerCase()}`,
|
|
31
|
+
`quote_id:${auth.quote_id}`,
|
|
32
|
+
`request_hash:${auth.request_hash}`,
|
|
33
|
+
`network:${auth.network}`,
|
|
34
|
+
`recipient:${String(auth.recipient).toLowerCase()}`,
|
|
35
|
+
`nonce:${auth.nonce}`,
|
|
36
|
+
`expiry:${auth.expiry}`,
|
|
37
|
+
].join('\n');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function readSignedAuthorization(value) {
|
|
41
|
+
if (!value || typeof value !== 'string') throw new Error('payer authorization JSON or @file required');
|
|
42
|
+
const raw = value.startsWith('@') ?
|
|
43
|
+
fs.readFileSync(path.resolve(value.slice(1)), 'utf8') : value;
|
|
44
|
+
const obj = JSON.parse(raw);
|
|
45
|
+
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) throw new Error('expected an object');
|
|
46
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(obj.wallet || '')) throw new Error('invalid wallet');
|
|
47
|
+
if (!/^0x[0-9a-fA-F]{64}$/.test(obj.txHash || '')) throw new Error('invalid txHash');
|
|
48
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(obj.recipient || '')) throw new Error('invalid recipient');
|
|
49
|
+
if (!/^0x[0-9a-fA-F]{64}$/.test(obj.request_hash || '')) throw new Error('invalid action request hash');
|
|
50
|
+
if (!/^0x[0-9a-fA-F]{130}$/.test(obj.signature || '')) throw new Error('invalid EIP-191 signature');
|
|
51
|
+
if (typeof obj.action !== 'string' || typeof obj.network !== 'string' ||
|
|
52
|
+
typeof obj.quote_id !== 'string' || typeof obj.nonce !== 'string' ||
|
|
53
|
+
!Number.isSafeInteger(obj.expiry) || obj.expiry <= Math.floor(Date.now() / 1000)) {
|
|
54
|
+
throw new Error('malformed or expired authorization fields');
|
|
55
|
+
}
|
|
56
|
+
return obj;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { buildAuthMessage, actionRequestHash, readSignedAuthorization };
|