warpmetal 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,8 +4,10 @@ The official command-line client and portable Agent Skill for WarpMetal.
4
4
 
5
5
  The CLI uses the public API at `https://api.warpmetal.com`, stores generated
6
6
  WarpMetal credentials in a user-private state file, and never reads or stores
7
- wallet private keys or SSH private-key contents. Version 0.3 adds guarded OS
8
- reload and Agent Runtime recovery to the fixed-size, isolated sandbox workflow.
7
+ wallet private keys or SSH private-key contents. Version 0.4 adds a guarded
8
+ x402api Agent Wallet handoff: WarpMetal writes the exact credential-free payment
9
+ request, explains the next commands to an agent, validates the returned payment
10
+ artifact, and submits it without absorbing wallet custody.
9
11
 
10
12
  ## Distribution
11
13
 
@@ -21,9 +23,16 @@ remains the stable executable API.
21
23
 
22
24
  ## Install
23
25
 
26
+ WarpMetal supports Node.js 20 and 22. The separate x402api Agent Wallet
27
+ requires Node.js 22; it is not a WarpMetal package dependency. Install the
28
+ published wallet CLI with the exact version WarpMetal reports:
29
+
24
30
  ```sh
25
31
  npm install --global warpmetal
26
32
  warpmetal --help
33
+
34
+ npm install --global @x402api/agent-wallet-cli@0.2.1
35
+ x402api help --json
27
36
  ```
28
37
 
29
38
  Install the bundled skill for supported coding agents:
@@ -58,6 +67,44 @@ Pass `--json` for structured, secret-redacted output. Use
58
67
  `WARPMETAL_API_URL` for an alternate API origin and `WARPMETAL_HOME` for an
59
68
  alternate state directory.
60
69
 
70
+ ## Pay through the x402api Agent Wallet
71
+
72
+ After preparing an order, request its live payment challenge:
73
+
74
+ ```sh
75
+ warpmetal checkout challenge --task <taskId> --json
76
+ ```
77
+
78
+ On HTTP 402 the CLI returns exact `paymentTerms`, the pinned
79
+ `@x402api/agent-wallet-cli` package contract, and argv arrays under
80
+ `paymentWorkflow`. It also writes an owner-only request envelope that contains
81
+ the exact checkout URL and body but no WarpMetal credential. The published
82
+ launch wallet accepts sponsored Base USDC and sponsored Solana USDC/USDT only;
83
+ the returned terms identify compatible alternatives and confirm that the buyer
84
+ does not need ETH or SOL. After inspecting the terms and approving payment,
85
+ invoke the returned authorization argv with a dedicated wallet name:
86
+
87
+ ```sh
88
+ x402api payment authorize \
89
+ --wallet <wallet-name> \
90
+ --request-envelope <path-returned-by-warpmetal> \
91
+ --artifact-out <path-returned-by-warpmetal> \
92
+ --json
93
+
94
+ warpmetal checkout submit \
95
+ --task <taskId> \
96
+ --payment-artifact <owner-only-artifact-path> \
97
+ --wait \
98
+ --json
99
+ ```
100
+
101
+ The x402api Agent Wallet is a separate, merchant-neutral executable. Install
102
+ its matching `x402api-pay` skill with `x402api skill install --output <agent-skill-directory>/x402api-pay --json`.
103
+ Do not use `x402api pay`, `payment submit`, or `payment reconcile` for WarpMetal:
104
+ checkout requires the private WarpMetal owner token, so x402api authorizes and
105
+ WarpMetal submits. WarpMetal keeps `--payment-signature-file` for another
106
+ compatible external signer.
107
+
61
108
  ## Security boundary
62
109
 
63
110
  - Order and access tokens are never printed; they are written to
@@ -68,8 +115,9 @@ alternate state directory.
68
115
  - WarpMetal applies key-only OpenSSH configuration on initial provisioning and
69
116
  every OS reload. Password and keyboard-interactive login are disabled; never
70
117
  request, store, or expect a VPS login password.
71
- - The CLI accepts an externally produced x402 `PAYMENT-SIGNATURE` from a file.
72
- Wallet key management and signing remain outside this package.
118
+ - The CLI writes x402api-compatible request envelopes and accepts validated
119
+ x402api payment artifacts or a compatible external `PAYMENT-SIGNATURE` file.
120
+ Wallet key management and signing remain outside this package.
73
121
  - Destructive or state-changing commands require explicit confirmations and
74
122
  generate idempotency keys by default.
75
123
  - Runtime bootstrap credentials remain memory-only. Signed supervisor bundles
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "warpmetal",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Agent-safe CLI and skill for purchasing and managing WarpMetal VPS servers",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,7 +16,7 @@
16
16
  "node": ">=20.0.0"
17
17
  },
18
18
  "scripts": {
19
- "check": "node --check bin/warpmetal.js && node --check src/api.js && node --check src/args.js && node --check src/cli.js && node --check src/connection.js && node --check src/errors.js && node --check src/install-skill.js && node --check src/installer.js && node --check src/runtime.js && node --check src/ssh.js && node --check src/state.js && node --check src/version.js",
19
+ "check": "node --check bin/warpmetal.js && node --check src/api.js && node --check src/args.js && node --check src/cli.js && node --check src/connection.js && node --check src/errors.js && node --check src/install-skill.js && node --check src/installer.js && node --check src/payment.js && node --check src/runtime.js && node --check src/ssh.js && node --check src/state.js && node --check src/version.js",
20
20
  "test": "node --test",
21
21
  "prepack": "npm run check && npm test"
22
22
  },
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: warpmetal
3
- description: Safely purchase and manage WarpMetal VPS servers and Agent Runtime sandboxes with the official warpmetal CLI. Use when a shell-capable agent needs live VPS discovery, x402 ordering, provisioning, server management, optional runtime installation, fixed-size sandbox creation, persistent or temporary lifetime, per-agent SSH access, sandbox connection, access revocation, or workspace deletion.
3
+ description: Safely purchase and manage WarpMetal VPS servers and Agent Runtime sandboxes with the official warpmetal CLI, including x402api Agent Wallet payment handoff. Use when a shell-capable agent needs live VPS discovery, x402 payment authorization, ordering, provisioning, server management, optional runtime installation, fixed-size sandbox creation, persistent or temporary lifetime, per-agent SSH access, sandbox connection, access revocation, or workspace deletion.
4
4
  ---
5
5
 
6
6
  # WarpMetal
@@ -19,12 +19,15 @@ with ad hoc HTTP commands.
19
19
  authorizing payment, using an SSH identity, or changing a server.
20
20
  5. Read [references/cli-reference.md](references/cli-reference.md) when choosing
21
21
  a command or interpreting an exit code.
22
- 6. Read [references/runtime.md](references/runtime.md) before requesting,
22
+ 6. Read [references/payments.md](references/payments.md) before creating or
23
+ funding a wallet, authorizing payment, or resolving an ambiguous attempt.
24
+ 7. Read [references/runtime.md](references/runtime.md) before requesting,
23
25
  installing, accessing, expiring, or deleting Agent Runtime sandboxes.
24
26
 
25
- Never read, print, summarize, upload, or commit the WarpMetal state file. Never
26
- read an SSH private-key file. Pass its path only to a command designed to use
27
- it. Never request or handle a wallet seed phrase or private key.
27
+ Never read, print, summarize, upload, or commit the WarpMetal state file,
28
+ x402api keystore, password file, payment request envelope, or payment artifact.
29
+ Never read an SSH private-key file. Pass private paths only to commands designed
30
+ to use them. Never request or handle a wallet seed phrase or private key.
28
31
 
29
32
  WarpMetal provisions and reloads servers with key-only OpenSSH access. Password
30
33
  and keyboard-interactive login are disabled, including for root. Never request,
@@ -64,26 +67,44 @@ open the state file to retrieve the credential.
64
67
 
65
68
  ## Authorize payment
66
69
 
67
- Run `warpmetal checkout challenge --task <taskId> --json` to obtain the live
68
- x402 terms. Before any wallet creation, funding, or signature, show the user
69
- the exact amount, asset, network, recipient, and expiration derived from the
70
- live challenge and obtain explicit approval.
71
-
72
- Use a compatible external wallet signer to produce one `PAYMENT-SIGNATURE`
73
- header value in a file. Do not pass wallet secrets to WarpMetal or the CLI.
74
- After approval, submit it with:
70
+ Run `warpmetal checkout challenge --task <taskId> --json`. The CLI validates
71
+ the live x402 terms, writes an owner-only credential-free request envelope, and
72
+ returns `paymentTerms` plus exact `paymentWorkflow.authorize.argv` and
73
+ `paymentWorkflow.submit.argv` arrays. Do not reconstruct those commands or
74
+ open either file.
75
+
76
+ Show the user the exact amount, asset, network, recipient, profile, and maximum
77
+ authorization lifetime from `paymentTerms`; obtain explicit approval before
78
+ signing or submission. Follow [references/payments.md](references/payments.md)
79
+ to install the exact `paymentWorkflow.signerPackage.spec`, verify the V1
80
+ machine contract, install its matching `x402api-pay` skill, and select or fund
81
+ a dedicated network-specific wallet. Require `agentWalletSupported: true`,
82
+ `sponsoredNetworkFee: true`, and `buyerNativeFeeRequired: false` on the chosen
83
+ live term. Never fall back to a historical buyer-funded or TRON profile.
84
+
85
+ After approval, invoke the returned authorize argv once. The separate
86
+ `x402api` executable owns the wallet, validates the envelope, and writes the
87
+ payment artifact. Then invoke the returned submit argv, equivalent to:
75
88
 
76
89
  ```sh
77
90
  warpmetal checkout submit \
78
91
  --task <taskId> \
79
- --payment-signature-file <path> \
92
+ --payment-artifact <owner-only-artifact-path> \
80
93
  --wait \
81
94
  --json
82
95
  ```
83
96
 
84
- If the command reports a rejected signature, inspect the replacement live
85
- challenge and request new approval where its terms changed. If it reports
86
- `manual_review`, stop immediately and never create another payment.
97
+ Do not use `x402api pay`, `x402api payment submit`, or `x402api payment
98
+ reconcile` for this checkout. Those commands submit credential-free requests,
99
+ but WarpMetal must add its private owner token locally.
100
+
101
+ WarpMetal verifies that the artifact matches the exact saved request and an
102
+ advertised sponsored requirement, gas reservation, resource, extensions, and
103
+ buyer payment identifier before sending its signature. The legacy
104
+ `--payment-signature-file` input remains available for another compatible
105
+ external signer. If a signed request returns a replacement challenge, use the
106
+ new CLI-produced workflow and request approval again when terms changed. On
107
+ `manual_review` or an ambiguous attempt, stop and never create another payment.
87
108
 
88
109
  ## Provision and manage
89
110
 
@@ -1,4 +1,4 @@
1
1
  interface:
2
2
  display_name: "WarpMetal VPS"
3
- short_description: "Manage WarpMetal VPS and agent sandboxes safely"
4
- default_prompt: "Use $warpmetal to provision a VPS and manage isolated agent sandboxes safely."
3
+ short_description: "Purchase and manage WarpMetal compute safely"
4
+ default_prompt: "Use $warpmetal to purchase a VPS through the guarded x402 wallet handoff and manage isolated agent sandboxes safely."
@@ -33,11 +33,14 @@ warpmetal order prepare \
33
33
  [--idempotency-key <key>] \
34
34
  --json
35
35
 
36
- warpmetal checkout challenge --task <taskId> --json
36
+ warpmetal checkout challenge \
37
+ --task <taskId> \
38
+ [--request-envelope-out <path>] \
39
+ --json
37
40
 
38
41
  warpmetal checkout submit \
39
42
  --task <taskId> \
40
- --payment-signature-file <path> \
43
+ (--payment-artifact <path> | --payment-signature-file <path>) \
41
44
  [--wait] [--timeout-seconds <n>] \
42
45
  --json
43
46
 
@@ -47,8 +50,29 @@ warpmetal order status \
47
50
  --json
48
51
  ```
49
52
 
50
- The payment signature file must contain one HTTP header value. The CLI does
51
- not create or store wallet keys and does not sign x402 challenges.
53
+ On HTTP 402, `checkout challenge` validates and displays exact payment terms,
54
+ writes a credential-free x402api V1 request envelope with owner-only
55
+ permissions, and returns the exact pinned wallet package, V1 contract probe,
56
+ matching wallet-skill install, authorization, and WarpMetal submission argv
57
+ arrays. The default envelope and suggested artifact paths live under the
58
+ private WarpMetal state directory. An explicit output path must not already
59
+ contain different content.
60
+
61
+ The current integration targets `@x402api/agent-wallet-cli@0.2.1`. A compatible
62
+ live term is marked `agentWalletSupported: true` and must use the sponsored
63
+ Base USDC or Solana USDC/USDT launch profile with buyer native fees disabled.
64
+ WarpMetal rejects a challenge with no compatible sponsored term. Because the
65
+ checkout is authenticated, do not substitute `x402api pay`, `payment submit`,
66
+ or `payment reconcile` for the returned WarpMetal submission command.
67
+
68
+ `--payment-artifact` accepts the owner-only JSON artifact produced by a
69
+ compatible pinned x402api Agent Wallet release. WarpMetal validates its request
70
+ and payment-requirement digests, resource, extensions, buyer payment identifier,
71
+ signature, sponsorship expiry, file type, and permissions before submission.
72
+ It reports only safe attempt metadata. The compatibility
73
+ `--payment-signature-file` input must contain one HTTP header value. WarpMetal
74
+ never creates, imports, reads, or stores wallet keys and does not sign x402
75
+ challenges itself. See [payments.md](payments.md).
52
76
 
53
77
  ## Server management
54
78
 
@@ -0,0 +1,110 @@
1
+ # WarpMetal payment workflow
2
+
3
+ ## Contents
4
+
5
+ - Trust boundary
6
+ - Wallet setup and funding
7
+ - Exact purchase workflow
8
+ - Retries and recovery
9
+
10
+ ## Trust boundary
11
+
12
+ WarpMetal is the merchant tool. It owns product selection, the private owner
13
+ credential, exact checkout bytes, submission, provisioning, and fulfillment.
14
+ The separate x402api Agent Wallet owns encrypted network keys, balance checks,
15
+ payment authorization, and durable payment attempts. Never merge their state
16
+ files or pass a WarpMetal credential to `x402api`.
17
+
18
+ WarpMetal runs on Node.js 20 or 22. The x402api Agent Wallet currently requires
19
+ Node.js 22. Use the exact published package reported by
20
+ `paymentWorkflow.signerPackage.spec`; the current contract is
21
+ `@x402api/agent-wallet-cli@0.2.1`. Do not add it as a WarpMetal dependency,
22
+ install executable wallet code from an unpinned repository URL, or substitute
23
+ a similarly named package.
24
+
25
+ The wallet package ships its own version-matched `x402api-pay` skill. Install
26
+ that skill into the directory used by Codex, Claude Code, or another portable
27
+ Agent Skills runtime with the returned `paymentWorkflow.walletSkill.install.argv`.
28
+ The command refuses to overwrite an existing directory; do not delete an old
29
+ skill automatically.
30
+
31
+ ## Wallet setup and funding
32
+
33
+ 1. Install the exact `paymentWorkflow.signerPackage.install.argv` under Node
34
+ 22. Run `command -v x402api` and the returned
35
+ `paymentWorkflow.signerContract.probe.argv`. Require contract version 1.
36
+ 2. Ensure an operator configured `X402API_WALLET_PASSWORD_FILE` as an owner-only
37
+ file, or is supervising `--password-stdin`. Never read either value.
38
+ 3. Use `x402api wallet list --json` and reuse a dedicated persistent wallet for
39
+ the exact challenge network.
40
+ 4. Create a wallet only with explicit approval. Keep Base and Solana wallets
41
+ separate; never import the owner's primary seed or key. TRON wallet
42
+ management exists, but the published launch payer cannot authorize TRON.
43
+ 5. Use `x402api wallet balance --wallet <name> --json`. If funding is short,
44
+ show the public address, exact network, token, and atomic deficit. The owner
45
+ funds the token address from a wallet they control. Sponsored launch
46
+ payments never ask the buyer to fund ETH or SOL.
47
+
48
+ Treat the dedicated wallet's funded balance as spend authority available to
49
+ the agent, bounded by any wallet-local maximum payment policy. WarpMetal still
50
+ requires explicit approval immediately before its payment authorization and
51
+ submission workflow.
52
+
53
+ ## Exact purchase workflow
54
+
55
+ ```sh
56
+ warpmetal checkout challenge --task <taskId> --json
57
+ ```
58
+
59
+ Read only the returned safe JSON. Confirm `paymentTerms`, then use the exact
60
+ argv arrays returned under `paymentWorkflow`:
61
+
62
+ ```text
63
+ paymentWorkflow.authorize.argv
64
+ x402api payment authorize --wallet <wallet-name>
65
+ --request-envelope <owner-only-request-path>
66
+ --artifact-out <owner-only-artifact-path> --json
67
+
68
+ paymentWorkflow.submit.argv
69
+ warpmetal checkout submit --task <taskId>
70
+ --payment-artifact <owner-only-artifact-path> --wait --json
71
+ ```
72
+
73
+ Replace only `<wallet-name>` after selecting the wallet for the advertised
74
+ network. Do not parse, rewrite, copy, or display the request envelope or payment
75
+ artifact. The envelope excludes the WarpMetal owner token; the artifact holds
76
+ the complete payment signature and remains owner-only.
77
+
78
+ Choose only a term marked `agentWalletSupported: true`,
79
+ `sponsoredNetworkFee: true`, and `buyerNativeFeeRequired: false`. The supported
80
+ launch profiles are sponsored Base USDC and sponsored Solana USDC/USDT, bound
81
+ by the strict `com.x402api.gas-sponsorship` extension. Stop on an expired gas
82
+ reservation or any buyer-funded, unsupported, or unbound alternative.
83
+
84
+ Do not replace the two-stage workflow with `x402api pay`, `payment submit`, or
85
+ `payment reconcile`. Those wallet commands can submit an exact credential-free
86
+ endpoint; WarpMetal checkout requires a private owner token that must never
87
+ enter the wallet envelope. Use x402api only for authorization and WarpMetal for
88
+ submission and merchant reconciliation.
89
+
90
+ WarpMetal rejects an artifact whose request digest, selected payment
91
+ requirement, resource, extension set, buyer payment identifier, signature,
92
+ expiry, sponsorship lifetime, file type, or permissions do not match the saved
93
+ challenge. Keep `--payment-signature-file` only as a compatibility path for
94
+ another external signer.
95
+
96
+ ## Retries and recovery
97
+
98
+ - `payment_pending` or `payment_finalizing`: keep the same checkout bytes and
99
+ artifact. `--wait` performs bounded retries with that exact authorization.
100
+ - Timeout or process restart after authorization: use the saved x402api attempt
101
+ ID and artifact. Reconcile with WarpMetal and reuse its submit argv. Never
102
+ authorize again merely because submission is unknown.
103
+ - Signed HTTP 402: the artifact was definitively rejected. Run the WarpMetal
104
+ challenge flow again, inspect the replacement terms, and authorize one new
105
+ artifact only when the protocol and approval allow it.
106
+ - `manual_review`: payment or fulfillment may be final. Stop all payment and
107
+ mutation retries.
108
+ - Expired or corrupt artifact, changed request digest, unexpected recipient,
109
+ unsupported network, asset, profile, sponsorship error, or request-binding
110
+ mismatch: stop instead of falling back or asking the buyer to fund ETH/SOL.
@@ -34,8 +34,15 @@
34
34
  - Give each agent a distinct sandbox-specific SSH key and grant. Never reuse
35
35
  the owner host key or one agent key across multiple sandboxes.
36
36
  - Never request, read, transmit, or store a wallet seed phrase or private key.
37
- - Never put a token, payment signature, private key, or state-file content in
38
- a prompt, URL, log, screenshot, source file, or shell argument.
37
+ - Install only the exact `@x402api/agent-wallet-cli` version and matching
38
+ bundled `x402api-pay` skill reported by WarpMetal. Never substitute an
39
+ unpinned repository checkout or similarly named wallet package.
40
+ - Never read or display an x402api password file, encrypted keystore, request
41
+ envelope, payment artifact, or complete payment signature. Pass only their
42
+ paths to the documented commands.
43
+ - Never put a token, payment signature, payment artifact, wallet password,
44
+ private key, or state-file content in a prompt, URL, log, screenshot, source
45
+ file, or shell argument.
39
46
 
40
47
  ## Required confirmations
41
48
 
@@ -71,9 +78,13 @@ Confirm the plan, hostname, OS, and public key before preparing it.
71
78
 
72
79
  - Reuse the same idempotency key only for the exact same logical request.
73
80
  - For `payment_pending`, retry the exact checkout body and the exact same
74
- payment signature. Do not create a replacement payment.
81
+ payment artifact. Do not create a replacement payment.
75
82
  - A signed HTTP 402 rejects that signature. Use the new live challenge for one
76
- replacement authorization.
83
+ replacement authorization after checking the new terms and approval.
84
+ - On timeout or restart after wallet authorization, reuse the saved x402api
85
+ attempt and artifact through WarpMetal. Never authorize again merely because
86
+ submission or fulfillment is uncertain, and never send the private owner
87
+ token through `x402api pay`, `payment submit`, or `payment reconcile`.
77
88
  - Treat `manual_review` as terminal. Do not pay again and do not repeat the
78
89
  mutation.
79
90
  - Treat HTTP 202 as accepted or pending, never as proof of success. Poll the
package/src/cli.js CHANGED
@@ -14,6 +14,13 @@ import { connectionProfile, writeConnectionProfile } from "./connection.js";
14
14
  import { CliError, toErrorMessage } from "./errors.js";
15
15
  import { installSkill } from "./install-skill.js";
16
16
  import { installRuntime } from "./installer.js";
17
+ import {
18
+ createPaymentRequestEnvelope,
19
+ defaultPaymentPaths,
20
+ paymentWorkflow,
21
+ readPaymentArtifact,
22
+ writePaymentRequestEnvelope,
23
+ } from "./payment.js";
17
24
  import {
18
25
  readSandboxFile,
19
26
  requireTemporaryConfirmation,
@@ -66,8 +73,9 @@ Usage:
66
73
  --ssh-public-key-file <path> [--runtime-file <path>] [--confirm TEMPORARY]
67
74
  [--email <address>] [--idempotency-key <key>]
68
75
  warpmetal order status --task <taskId> [--wait] [--timeout-seconds <n>]
69
- warpmetal checkout challenge --task <taskId>
70
- warpmetal checkout submit --task <taskId> --payment-signature-file <path>
76
+ warpmetal checkout challenge --task <taskId> [--request-envelope-out <path>]
77
+ warpmetal checkout submit --task <taskId>
78
+ (--payment-artifact <path> | --payment-signature-file <path>)
71
79
  [--wait] [--timeout-seconds <n>]
72
80
  warpmetal server login --server <serverId> --identity <private-key-path>
73
81
  warpmetal server get --server <serverId>
@@ -112,6 +120,16 @@ function writeLine(stream, value = "") {
112
120
  stream.write(`${value}\n`);
113
121
  }
114
122
 
123
+ function shellCommand(argv) {
124
+ return argv
125
+ .map((value) =>
126
+ /^[A-Za-z0-9_./:=+@-]+$/.test(value)
127
+ ? value
128
+ : `'${value.replaceAll("'", `'\\''`)}'`,
129
+ )
130
+ .join(" ");
131
+ }
132
+
115
133
  function emit(stream, value, json, human) {
116
134
  if (json) writeLine(stream, JSON.stringify(value, null, 2));
117
135
  else writeLine(stream, human);
@@ -344,6 +362,46 @@ function challengeResult(taskId, checkoutBody, response) {
344
362
  };
345
363
  }
346
364
 
365
+ async function attachPaymentWorkflow(
366
+ client,
367
+ store,
368
+ taskId,
369
+ order,
370
+ safe,
371
+ requestedEnvelopePath,
372
+ ) {
373
+ if (!safe.paymentRequired) return safe;
374
+ const request = createPaymentRequestEnvelope({
375
+ baseUrl: client.baseUrl,
376
+ checkoutPath: order.checkoutPath,
377
+ checkoutBody: order.checkoutBody,
378
+ paymentRequired: safe.paymentRequired,
379
+ merchantReference: taskId,
380
+ });
381
+ const defaults = defaultPaymentPaths(
382
+ store.directory,
383
+ taskId,
384
+ request.challengeDigest,
385
+ );
386
+ const requestEnvelopePath = await writePaymentRequestEnvelope(
387
+ requestedEnvelopePath || defaults.requestEnvelopePath,
388
+ request.envelope,
389
+ );
390
+ const workflow = paymentWorkflow({
391
+ taskId,
392
+ requestEnvelopePath,
393
+ paymentArtifactPath: defaults.paymentArtifactPath,
394
+ });
395
+ Object.assign(safe, {
396
+ paymentTerms: request.terms,
397
+ paymentRequestDigest: request.requestDigest,
398
+ paymentChallengeDigest: request.challengeDigest,
399
+ paymentWorkflow: workflow,
400
+ });
401
+ await store.savePaymentChallenge(taskId, safe);
402
+ return safe;
403
+ }
404
+
347
405
  async function handleHealth(client, { json, stdout }) {
348
406
  const result = await client.health();
349
407
  emit(
@@ -470,16 +528,23 @@ async function handleCheckoutChallenge(client, store, options, context) {
470
528
  bodyText: order.checkoutBody,
471
529
  token,
472
530
  });
473
- const safe = challengeResult(taskId, order.checkoutBody, response);
474
- if (safe.paymentRequired) {
475
- await store.savePaymentChallenge(taskId, safe);
476
- }
531
+ const safe = await attachPaymentWorkflow(
532
+ client,
533
+ store,
534
+ taskId,
535
+ order,
536
+ challengeResult(taskId, order.checkoutBody, response),
537
+ stringOption(options, "request-envelope-out"),
538
+ );
539
+ const paymentInstructions = safe.paymentWorkflow
540
+ ? `\nWallet package: ${safe.paymentWorkflow.signerPackage.spec} (Node ${safe.paymentWorkflow.signerNodeRequirement})\nInstall: ${shellCommand(safe.paymentWorkflow.signerPackage.install.argv)}\nVerify: ${shellCommand(safe.paymentWorkflow.signerContract.probe.argv)}\nRequest envelope: ${safe.paymentWorkflow.requestEnvelopePath}\nAuthorize: ${shellCommand(safe.paymentWorkflow.authorize.argv)}\nSubmit with WarpMetal: ${shellCommand(safe.paymentWorkflow.submit.argv)}`
541
+ : "";
477
542
  emit(
478
543
  context.stdout,
479
544
  safe,
480
545
  context.json,
481
546
  response.status === 402
482
- ? `Payment authorization required for ${taskId}.\nPAYMENT-REQUIRED: ${safe.paymentRequired}`
547
+ ? `Payment authorization required for ${taskId}.${paymentInstructions}`
483
548
  : `Checkout status for ${taskId}: ${safe.status}`,
484
549
  );
485
550
  return response.status === 409 ? 6 : response.status === 402 ? 7 : 0;
@@ -487,19 +552,45 @@ async function handleCheckoutChallenge(client, store, options, context) {
487
552
 
488
553
  async function handleCheckoutSubmit(client, store, options, context) {
489
554
  const taskId = stringOption(options, "task", { required: true });
490
- const signatureFile = stringOption(options, "payment-signature-file", {
491
- required: true,
492
- });
493
- const paymentSignature = await readHeaderValueFile(
494
- signatureFile,
495
- "The payment signature file",
496
- );
555
+ const signatureFile = stringOption(options, "payment-signature-file");
556
+ const artifactFile = stringOption(options, "payment-artifact");
557
+ if (Boolean(signatureFile) === Boolean(artifactFile)) {
558
+ throw new CliError(
559
+ "Use exactly one of --payment-artifact or --payment-signature-file.",
560
+ { exitCode: 2 },
561
+ );
562
+ }
497
563
  const order = await store.order(taskId);
498
564
  if (!order?.checkoutPath || !order?.checkoutBody) {
499
565
  throw new CliError(`No exact checkout state exists for ${taskId}.`, {
500
566
  exitCode: 2,
501
567
  });
502
568
  }
569
+ let walletPayment;
570
+ let paymentSignature;
571
+ if (artifactFile) {
572
+ if (!order.paymentRequired) {
573
+ throw new CliError(
574
+ `No saved PAYMENT-REQUIRED challenge exists for ${taskId}. Run warpmetal checkout challenge first.`,
575
+ { exitCode: 2 },
576
+ );
577
+ }
578
+ const expected = createPaymentRequestEnvelope({
579
+ baseUrl: client.baseUrl,
580
+ checkoutPath: order.checkoutPath,
581
+ checkoutBody: order.checkoutBody,
582
+ paymentRequired: order.paymentRequired,
583
+ merchantReference: taskId,
584
+ });
585
+ walletPayment = await readPaymentArtifact(artifactFile, expected);
586
+ paymentSignature = walletPayment.paymentSignature;
587
+ await store.saveWalletPaymentAttempt(taskId, walletPayment);
588
+ } else {
589
+ paymentSignature = await readHeaderValueFile(
590
+ signatureFile,
591
+ "The payment signature file",
592
+ );
593
+ }
503
594
  const token = await requireTaskToken(store, taskId, options, context.env);
504
595
  const wait = booleanOption(options, "wait");
505
596
  const deadline = timeoutDeadline(
@@ -512,8 +603,13 @@ async function handleCheckoutSubmit(client, store, options, context) {
512
603
  token,
513
604
  paymentSignature,
514
605
  });
515
- const safe = challengeResult(taskId, order.checkoutBody, response);
516
- if (safe.paymentRequired) await store.savePaymentChallenge(taskId, safe);
606
+ const safe = await attachPaymentWorkflow(
607
+ client,
608
+ store,
609
+ taskId,
610
+ order,
611
+ challengeResult(taskId, order.checkoutBody, response),
612
+ );
517
613
  const retryable =
518
614
  response.status === 202 &&
519
615
  ["payment_pending", "payment_finalizing"].includes(response.data?.status);
@@ -527,6 +623,19 @@ async function handleCheckoutSubmit(client, store, options, context) {
527
623
  ...safe,
528
624
  task: response.data?.task,
529
625
  message: response.data?.message,
626
+ ...(walletPayment
627
+ ? {
628
+ walletPayment: {
629
+ attemptId: walletPayment.attemptId,
630
+ requestDigest: walletPayment.requestDigest,
631
+ buyerPaymentIdentifier: walletPayment.buyerPaymentIdentifier,
632
+ wallet: walletPayment.wallet,
633
+ payerAddress: walletPayment.payerAddress,
634
+ artifactPath: walletPayment.path,
635
+ expiresAt: walletPayment.expiresAt,
636
+ },
637
+ }
638
+ : {}),
530
639
  };
531
640
  emit(
532
641
  context.stdout,
@@ -662,12 +771,7 @@ async function handleServerReload(client, store, options, context) {
662
771
  options,
663
772
  "acknowledge-agent-runtime-reset",
664
773
  );
665
- const token = await requireReloadToken(
666
- store,
667
- serverId,
668
- options,
669
- context.env,
670
- );
774
+ const token = await requireReloadToken(store, serverId, options, context.env);
671
775
  const server = (await client.getServer(serverId, token)).data?.task;
672
776
  if (server?.agentRuntime && !acknowledgeRuntimeReset) {
673
777
  throw new CliError(
@@ -717,7 +821,7 @@ async function handleServerReload(client, store, options, context) {
717
821
  ? " Verify and refresh the owner SSH host key, reinstall Agent Runtime, then refresh every sandbox connection profile."
718
822
  : operation?.state === "succeeded"
719
823
  ? " Verify and refresh the owner SSH host key before reconnecting."
720
- : ""),
824
+ : ""),
721
825
  );
722
826
  if (operation?.state === "manual_review") return 6;
723
827
  if (operation?.state === "failed") return 5;
@@ -1399,13 +1503,19 @@ async function dispatch(positionals, options, passthrough, context) {
1399
1503
  ]);
1400
1504
  return handleTaskStatus(client, store, options, context);
1401
1505
  case "checkout challenge":
1402
- rejectUnknownOptions(options, [...COMMON_OPTIONS, "task", "token-file"]);
1506
+ rejectUnknownOptions(options, [
1507
+ ...COMMON_OPTIONS,
1508
+ "task",
1509
+ "token-file",
1510
+ "request-envelope-out",
1511
+ ]);
1403
1512
  return handleCheckoutChallenge(client, store, options, context);
1404
1513
  case "checkout submit":
1405
1514
  rejectUnknownOptions(options, [
1406
1515
  ...COMMON_OPTIONS,
1407
1516
  "task",
1408
1517
  "token-file",
1518
+ "payment-artifact",
1409
1519
  "payment-signature-file",
1410
1520
  "wait",
1411
1521
  "timeout-seconds",