warpmetal 0.8.5 → 0.8.7
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 +42 -0
- package/package.json +1 -1
- package/skills/warpmetal/SKILL.md +22 -0
- package/skills/warpmetal/references/cli-reference.md +21 -1
- package/skills/warpmetal/references/payments.md +10 -0
- package/skills/warpmetal/references/runtime.md +34 -0
- package/src/cli.js +117 -5
- package/src/installer.js +137 -20
package/README.md
CHANGED
|
@@ -141,6 +141,18 @@ confirmation is pending. As soon as `confirmed` is true, stop all payment
|
|
|
141
141
|
submission—even when `finalized` is false—and let WarpMetal continue signed-
|
|
142
142
|
receipt finality while provisioning or renewal proceeds.
|
|
143
143
|
|
|
144
|
+
For an interactive initial purchase, `checkout challenge` may also return a
|
|
145
|
+
short-lived `humanCheckout` object. Its `url` and `qrPayload` are the same
|
|
146
|
+
`https://pay.x402api.com/c/...` bearer capability for the exact charge; encode
|
|
147
|
+
that URL as the QR, never the WarpMetal recipient address. The buyer connects
|
|
148
|
+
their own wallet and needs only the advertised USDC/USDT balance because
|
|
149
|
+
x402api sponsors the native gas. This is an alternative to the agent-wallet
|
|
150
|
+
workflow, not a second payment. After browser payment, run the exact
|
|
151
|
+
`humanCheckout.afterPayment.argv` status command. When the ready result asks
|
|
152
|
+
for `ask_human_for_notification_email`, ask the owner and add the optional
|
|
153
|
+
lifecycle-notification address they provide. Autonomous purchases and renewals
|
|
154
|
+
continue to use the bounded agent-wallet path.
|
|
155
|
+
|
|
144
156
|
- When a human is actively chatting with the agent, show the exact live terms
|
|
145
157
|
and ask for confirmation immediately before authorizing and submitting.
|
|
146
158
|
- In an unattended run, a pre-funded dedicated wallet is standing spend
|
|
@@ -337,6 +349,36 @@ warpmetal sandbox access keygen \
|
|
|
337
349
|
--json
|
|
338
350
|
```
|
|
339
351
|
|
|
352
|
+
WarpMetal CLI v0.8.7 with Agent Runtime v0.1.25 or newer can enable the narrowly
|
|
353
|
+
scoped AppArmor exception required by a verified workload that creates an inner
|
|
354
|
+
Bubblewrap PID namespace and private `/proc`:
|
|
355
|
+
|
|
356
|
+
```sh
|
|
357
|
+
warpmetal runtime install \
|
|
358
|
+
--server <serverId> \
|
|
359
|
+
--ssh-user root \
|
|
360
|
+
--confirm INSTALL \
|
|
361
|
+
--nested-private-procfs enable \
|
|
362
|
+
--wait \
|
|
363
|
+
--json
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
This is an explicit host-scoped opt-in for nested-Bubblewrap hosts, not a
|
|
367
|
+
requirement for ordinary VPS or Runtime workloads that use only the outer
|
|
368
|
+
sandbox. The default `preserve` action leaves the current policy state
|
|
369
|
+
unchanged. Use `disable` during an approved maintenance window to unload
|
|
370
|
+
WarpMetal's policy and restore the pre-install file and loaded-policy state.
|
|
371
|
+
Because Runtime sandboxes share one Unix owner, treat an enabled policy as
|
|
372
|
+
available to every sandbox on that Runtime host whose process matches the
|
|
373
|
+
signed, root-owned bwrap path; it is not a per-sandbox permission.
|
|
374
|
+
|
|
375
|
+
Common inner-boundary designs are planning with an exact read-only checkout,
|
|
376
|
+
coding with only one approved checkout and output directory writable, and QA
|
|
377
|
+
with an exact candidate plus isolated test processes and scratch space. The
|
|
378
|
+
boundary protects a persistent trusted runner from repository-controlled
|
|
379
|
+
commands and sibling attempts. GitHub access, installing Codex or another AI
|
|
380
|
+
CLI, and delegating to subagents do not by themselves require this capability.
|
|
381
|
+
|
|
340
382
|
Installation gives pre-existing Docker containers exact liveness checks and
|
|
341
383
|
tracks common container-runtime processes without collecting application
|
|
342
384
|
configuration. The signed installer uses `crun` for its private rootless Podman
|
package/package.json
CHANGED
|
@@ -88,6 +88,18 @@ returns `paymentTerms` plus exact `paymentWorkflow.authorize.argv` and
|
|
|
88
88
|
`paymentWorkflow.submit.argv` arrays. Do not reconstruct those commands or
|
|
89
89
|
open either file.
|
|
90
90
|
|
|
91
|
+
In an interactive initial purchase, the result may also contain
|
|
92
|
+
`humanCheckout`. Offer it as an alternative to the local agent wallet. Show
|
|
93
|
+
`humanCheckout.url` as clickable text and render only the identical
|
|
94
|
+
`humanCheckout.qrPayload` URL as the purchase QR; never render the recipient
|
|
95
|
+
address as this QR. The URL is an expiring bearer capability, so do not log,
|
|
96
|
+
save, or send it anywhere except to the buyer who requested this purchase. If
|
|
97
|
+
the buyer uses it, do not authorize or submit through the agent wallet. Run the
|
|
98
|
+
exact `humanCheckout.afterPayment.argv` command, wait for the server to become
|
|
99
|
+
ready, then follow `ask_human_for_notification_email`: ask the owner for the
|
|
100
|
+
optional lifecycle-notification address and add only the address they provide.
|
|
101
|
+
Ignore this path in unattended purchasing and renewal automation.
|
|
102
|
+
|
|
91
103
|
Determine payment authority from the current execution context. In an
|
|
92
104
|
interactive conversation, show the human the exact amount, asset, network,
|
|
93
105
|
recipient, profile, and maximum authorization lifetime from `paymentTerms`,
|
|
@@ -271,6 +283,16 @@ sizes. Use `--runtime-file` to include sandbox intent in an unpaid order, or
|
|
|
271
283
|
`warpmetal runtime enable` after the VPS is ready. Supervisor installation is
|
|
272
284
|
separate and requires approval plus `--confirm INSTALL`.
|
|
273
285
|
|
|
286
|
+
For any verified workload that creates an inner Bubblewrap PID namespace and
|
|
287
|
+
private `/proc`, CLI 0.8.7 with Runtime 0.1.25 or newer may add
|
|
288
|
+
`--nested-private-procfs enable` to the approved Runtime install. Planning with
|
|
289
|
+
a read-only checkout, coding in one writable checkout, and independent QA are
|
|
290
|
+
common uses. GitHub access, an AI CLI, or subagent delegation alone does not
|
|
291
|
+
require it. Omission means `preserve`, which leaves policy state unchanged;
|
|
292
|
+
`disable` is an explicit maintenance action. The exception is host-scoped
|
|
293
|
+
rather than sandbox-scoped, so separate workloads onto different VPS hosts when
|
|
294
|
+
they must not share it.
|
|
295
|
+
|
|
274
296
|
Omitted lifetime means persistent. A temporary sandbox requires
|
|
275
297
|
`--confirm TEMPORARY`, expires 15 minutes to 24 hours after first reaching
|
|
276
298
|
running, and permanently deletes its workspace at expiry. Never describe a
|
|
@@ -72,6 +72,15 @@ retire that attempt and return a new `paymentAttemptId`. The CLI replaces the
|
|
|
72
72
|
saved challenge and stale wallet-attempt metadata; use only the newly returned
|
|
73
73
|
workflow.
|
|
74
74
|
|
|
75
|
+
For interactive initial purchases, the response may additionally include
|
|
76
|
+
`humanCheckout.url`, the identical `qrPayload`, `expiresAt`, and an exact
|
|
77
|
+
`afterPayment.argv` command. The URL is a short-lived bearer capability for the
|
|
78
|
+
same charge, not a recipient address. If the buyer pays in the hosted checkout,
|
|
79
|
+
do not submit an agent-wallet artifact; run the returned status command and,
|
|
80
|
+
after ready, follow `ask_human_for_notification_email` to offer lifecycle
|
|
81
|
+
notices. The CLI does not persist the hosted URL. Renewal commands never expose
|
|
82
|
+
or use this interactive option.
|
|
83
|
+
|
|
75
84
|
The current integration targets `@x402api/agent-wallet-cli@0.2.9`. A compatible
|
|
76
85
|
live term is marked `agentWalletSupported: true` and must use the sponsored
|
|
77
86
|
Base USDC or Solana USDC/USDT launch profile with buyer native fees disabled.
|
|
@@ -203,7 +212,9 @@ warpmetal runtime enable --server <serverId> [--idempotency-key <key>] --json
|
|
|
203
212
|
warpmetal runtime get --server <serverId> [--wait] [--timeout-seconds <n>] --json
|
|
204
213
|
warpmetal runtime install \
|
|
205
214
|
--server <serverId> [--identity <owner-key>] --ssh-user root \
|
|
206
|
-
--confirm INSTALL
|
|
215
|
+
--confirm INSTALL \
|
|
216
|
+
[--nested-private-procfs <preserve|enable|disable>] \
|
|
217
|
+
[--wait] [--timeout-seconds <n>] --json
|
|
207
218
|
|
|
208
219
|
warpmetal sandbox create \
|
|
209
220
|
--server <serverId> --name <name> --size <small|medium|large|xlarge> \
|
|
@@ -221,6 +232,15 @@ warpmetal sandbox delete \
|
|
|
221
232
|
--server <serverId> --sandbox <sandboxId> --confirm DELETE [--wait] --json
|
|
222
233
|
```
|
|
223
234
|
|
|
235
|
+
The nested-private-procfs action requires CLI 0.8.7 and Runtime 0.1.25 or
|
|
236
|
+
newer. It defaults to `preserve`. `enable` is a host-level opt-in for any
|
|
237
|
+
verified workload that creates an inner Bubblewrap PID namespace and private
|
|
238
|
+
`/proc`; read-only planning, single-workspace coding, and independent QA are
|
|
239
|
+
common examples. GitHub use, an AI CLI, or subagent delegation alone does not
|
|
240
|
+
require it. `disable` unloads WarpMetal's policy and restores the recorded
|
|
241
|
+
pre-enable state. It is not a per-sandbox capability because Runtime sandboxes
|
|
242
|
+
share one Unix owner.
|
|
243
|
+
|
|
224
244
|
See [runtime.md](runtime.md) for capacity, lifetime, cleanup, polling, and
|
|
225
245
|
installation safety. Exit 8 means accepted or pending, never applied.
|
|
226
246
|
|
|
@@ -123,6 +123,16 @@ warpmetal checkout challenge --task <taskId> --json
|
|
|
123
123
|
Read only the returned safe JSON. Confirm `paymentTerms`, then use the exact
|
|
124
124
|
argv arrays returned under `paymentWorkflow`:
|
|
125
125
|
|
|
126
|
+
For an interactive initial purchase, an optional `humanCheckout` object offers
|
|
127
|
+
a second presentation path for the same charge. Its `url` and `qrPayload` must
|
|
128
|
+
be identical hosted-checkout URLs; render that URL as the QR and copyable link,
|
|
129
|
+
never a recipient address. If the buyer completes this path, skip agent-wallet
|
|
130
|
+
authorization and run `humanCheckout.afterPayment.argv`. Continue bounded
|
|
131
|
+
status polling until ready, then ask for the optional lifecycle-notification
|
|
132
|
+
email when the result returns `ask_human_for_notification_email`. The hosted
|
|
133
|
+
URL expires at `humanCheckout.expiresAt`, is not persisted by the CLI, and is
|
|
134
|
+
never used for unattended purchases or renewals.
|
|
135
|
+
|
|
126
136
|
```text
|
|
127
137
|
paymentWorkflow.authorize.argv
|
|
128
138
|
x402api payment authorize --wallet <wallet-name>
|
|
@@ -58,6 +58,7 @@ warpmetal runtime install \
|
|
|
58
58
|
--identity <owner-private-key-path> \
|
|
59
59
|
--ssh-user root \
|
|
60
60
|
--confirm INSTALL \
|
|
61
|
+
[--nested-private-procfs <preserve|enable|disable>] \
|
|
61
62
|
--wait \
|
|
62
63
|
--json
|
|
63
64
|
warpmetal runtime get --server <serverId> --wait --json
|
|
@@ -68,6 +69,29 @@ The CLI holds the one-time bootstrap only in memory, verifies the signed
|
|
|
68
69
|
artifact, uploads it through OpenSSH without a shell-enabled local spawn, and
|
|
69
70
|
does not print or store the bootstrap.
|
|
70
71
|
|
|
72
|
+
`--nested-private-procfs` is supported by `warpmetal` CLI 0.8.7 with Agent
|
|
73
|
+
Runtime 0.1.25 or newer. Its default is `preserve`, which makes no AppArmor
|
|
74
|
+
policy change. Select `enable` only when this VPS is intentionally dedicated to
|
|
75
|
+
a verified workload that creates an inner Bubblewrap PID namespace and private
|
|
76
|
+
procfs. Ordinary VPS users and Runtime workloads that rely only on the outer
|
|
77
|
+
sandbox do not need it. Select `disable` only during an approved maintenance
|
|
78
|
+
action to unload WarpMetal's policy and restore the exact file and loaded-policy
|
|
79
|
+
state that existed before enablement.
|
|
80
|
+
|
|
81
|
+
Examples include a planner with an exact read-only checkout, a coder with only
|
|
82
|
+
one approved checkout and output directory writable, and QA with an exact
|
|
83
|
+
candidate plus isolated test processes and scratch space. This protects a
|
|
84
|
+
persistent trusted runner from repository-controlled commands and sibling
|
|
85
|
+
attempts. GitHub access, installing an AI CLI, and subagent delegation alone do
|
|
86
|
+
not require nested private procfs.
|
|
87
|
+
|
|
88
|
+
This setting is host-scoped, not sandbox-scoped. Runtime sandboxes share one
|
|
89
|
+
Unix owner, so every sandbox on the host can use the exception only through the
|
|
90
|
+
signed, root-owned, fixed bwrap path matched by the policy. Enabling it does not
|
|
91
|
+
authorize arbitrary bwrap binaries or change the trust boundary for the
|
|
92
|
+
Runtime image selected by WarpMetal's authenticated backend. Isolate workloads
|
|
93
|
+
on separate VPS hosts if they must not share this host capability.
|
|
94
|
+
|
|
71
95
|
The signed installer is designed to preserve container workloads already
|
|
72
96
|
running on a supported host. It selects `crun` for WarpMetal's private rootless
|
|
73
97
|
Podman service, refuses APT removals and DNF erasures, protects installed
|
|
@@ -92,6 +116,16 @@ host automatically:
|
|
|
92
116
|
unexpectedly; stop and review the host package logs;
|
|
93
117
|
- `runtime_legacy_migration_required`: preview Podman state needs a separate,
|
|
94
118
|
explicitly reviewed migration and was not reset.
|
|
119
|
+
- `runtime_nested_private_procfs_architecture_unsupported`: the requested
|
|
120
|
+
coding-host policy is unsupported by this CPU architecture;
|
|
121
|
+
- `runtime_apparmor_state_unverifiable`: the host's current AppArmor state
|
|
122
|
+
cannot be established safely;
|
|
123
|
+
- `runtime_apparmor_policy_conflict`: an existing file or loaded-policy state
|
|
124
|
+
conflicts with WarpMetal's recorded transaction;
|
|
125
|
+
- `runtime_apparmor_policy_rollback_failed`: the installer could not restore
|
|
126
|
+
the exact prior AppArmor state; stop and perform operator recovery.
|
|
127
|
+
- `runtime_apparmor_policy_recovery_failed`: an interrupted prior policy
|
|
128
|
+
transaction could not be recovered; stop and perform operator recovery.
|
|
95
129
|
|
|
96
130
|
There is no force bypass. If `runtime_reboot_required` is returned, schedule
|
|
97
131
|
the reboot as a separate maintenance action and retry only after the host and
|
package/src/cli.js
CHANGED
|
@@ -110,7 +110,7 @@ Usage:
|
|
|
110
110
|
warpmetal operation get --operation <operationId> [--server <serverId>] [--wait]
|
|
111
111
|
warpmetal runtime enable|get --server <serverId> [--wait]
|
|
112
112
|
warpmetal runtime install --server <serverId> [--identity <owner-key>] --ssh-user <user>
|
|
113
|
-
--confirm INSTALL [--wait]
|
|
113
|
+
--confirm INSTALL [--nested-private-procfs <preserve|enable|disable>] [--wait]
|
|
114
114
|
warpmetal sandbox create --server <serverId> --name <name> --size <size>
|
|
115
115
|
[--lifetime temporary] [--expires-in-seconds <n>] [--confirm TEMPORARY] [--wait]
|
|
116
116
|
warpmetal sandbox create --server <serverId> --file <batch.json> [--confirm TEMPORARY]
|
|
@@ -132,6 +132,16 @@ Global options:
|
|
|
132
132
|
--help Show help
|
|
133
133
|
--version Show the CLI version
|
|
134
134
|
|
|
135
|
+
Nested private procfs (CLI 0.8.7+, Runtime 0.1.25+):
|
|
136
|
+
preserve Default; do not inspect or change AppArmor policy state
|
|
137
|
+
enable Enable the exact-path Bubblewrap policy on an amd64 host
|
|
138
|
+
disable Remove it and restore the recorded pre-enable policy state
|
|
139
|
+
|
|
140
|
+
Use enable once per dedicated Runtime host when a verified workload creates an
|
|
141
|
+
inner Bubblewrap PID namespace and private /proc. Planning, coding, and QA are
|
|
142
|
+
common examples. GitHub access, an AI CLI, and subagent delegation alone do not
|
|
143
|
+
require it. The capability is host-scoped, not per-sandbox.
|
|
144
|
+
|
|
135
145
|
Credential environment variables:
|
|
136
146
|
WARPMETAL_OWNER_TOKEN Recovery/bootstrap credential for one explicit command
|
|
137
147
|
WARPMETAL_ACCESS_TOKEN Short-lived SSH-derived credential for one explicit command
|
|
@@ -427,7 +437,11 @@ function challengeResult(
|
|
|
427
437
|
taskId,
|
|
428
438
|
checkoutBody,
|
|
429
439
|
response,
|
|
430
|
-
{
|
|
440
|
+
{
|
|
441
|
+
requireChallenge = true,
|
|
442
|
+
allowHumanCheckout = false,
|
|
443
|
+
humanCheckoutContext = undefined,
|
|
444
|
+
} = {},
|
|
431
445
|
) {
|
|
432
446
|
const status = response.data?.status;
|
|
433
447
|
const rejected = response.status === 402 && status === "payment_rejected";
|
|
@@ -459,6 +473,13 @@ function challengeResult(
|
|
|
459
473
|
"WarpMetal returned PAYMENT-REQUIRED without X-X402API-Challenge-Digest.",
|
|
460
474
|
);
|
|
461
475
|
}
|
|
476
|
+
const humanCheckout = allowHumanCheckout
|
|
477
|
+
? humanCheckoutResult(
|
|
478
|
+
response.data?.humanCheckout,
|
|
479
|
+
taskId,
|
|
480
|
+
humanCheckoutContext,
|
|
481
|
+
)
|
|
482
|
+
: undefined;
|
|
462
483
|
return {
|
|
463
484
|
status,
|
|
464
485
|
taskId,
|
|
@@ -469,6 +490,7 @@ function challengeResult(
|
|
|
469
490
|
challengeHandle,
|
|
470
491
|
challengeDigest,
|
|
471
492
|
checkoutBodySha256: createHash("sha256").update(checkoutBody).digest("hex"),
|
|
493
|
+
...(humanCheckout ? { humanCheckout } : {}),
|
|
472
494
|
...(rejected
|
|
473
495
|
? {
|
|
474
496
|
errorCode: response.data?.errorCode,
|
|
@@ -479,6 +501,74 @@ function challengeResult(
|
|
|
479
501
|
};
|
|
480
502
|
}
|
|
481
503
|
|
|
504
|
+
function humanCheckoutResult(value, taskId, { baseUrl, stateDirectory } = {}) {
|
|
505
|
+
if (value === undefined) return undefined;
|
|
506
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
507
|
+
throw new CliError("WarpMetal returned malformed hosted checkout details.");
|
|
508
|
+
}
|
|
509
|
+
const keys = Object.keys(value).sort();
|
|
510
|
+
if (keys.join(",") !== "expiresAt,qrPayload,url") {
|
|
511
|
+
throw new CliError("WarpMetal returned malformed hosted checkout details.");
|
|
512
|
+
}
|
|
513
|
+
if (
|
|
514
|
+
typeof value.url !== "string" ||
|
|
515
|
+
typeof value.qrPayload !== "string" ||
|
|
516
|
+
value.url !== value.qrPayload ||
|
|
517
|
+
typeof value.expiresAt !== "string"
|
|
518
|
+
) {
|
|
519
|
+
throw new CliError("WarpMetal returned malformed hosted checkout details.");
|
|
520
|
+
}
|
|
521
|
+
let url;
|
|
522
|
+
try {
|
|
523
|
+
url = new URL(value.url);
|
|
524
|
+
} catch {
|
|
525
|
+
throw new CliError("WarpMetal returned an invalid hosted checkout URL.");
|
|
526
|
+
}
|
|
527
|
+
if (
|
|
528
|
+
url.protocol !== "https:" ||
|
|
529
|
+
url.origin !== "https://pay.x402api.com" ||
|
|
530
|
+
url.username ||
|
|
531
|
+
url.password ||
|
|
532
|
+
url.search ||
|
|
533
|
+
url.hash ||
|
|
534
|
+
!/^\/c\/chk_[A-Za-z0-9_-]{32}$/.test(url.pathname) ||
|
|
535
|
+
url.toString() !== value.url
|
|
536
|
+
) {
|
|
537
|
+
throw new CliError("WarpMetal returned an invalid hosted checkout URL.");
|
|
538
|
+
}
|
|
539
|
+
const rfc3339 =
|
|
540
|
+
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
541
|
+
const expiresAt = Date.parse(value.expiresAt);
|
|
542
|
+
if (
|
|
543
|
+
!rfc3339.test(value.expiresAt) ||
|
|
544
|
+
!Number.isFinite(expiresAt) ||
|
|
545
|
+
expiresAt <= Date.now()
|
|
546
|
+
) {
|
|
547
|
+
throw new CliError(
|
|
548
|
+
"WarpMetal returned an invalid or expired hosted checkout expiry.",
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
return {
|
|
552
|
+
url: value.url,
|
|
553
|
+
qrPayload: value.qrPayload,
|
|
554
|
+
expiresAt: value.expiresAt,
|
|
555
|
+
afterPayment: {
|
|
556
|
+
argv: [
|
|
557
|
+
"warpmetal",
|
|
558
|
+
"order",
|
|
559
|
+
"status",
|
|
560
|
+
"--task",
|
|
561
|
+
taskId,
|
|
562
|
+
"--wait",
|
|
563
|
+
...(baseUrl ? ["--base-url", baseUrl] : []),
|
|
564
|
+
...(stateDirectory ? ["--state-dir", stateDirectory] : []),
|
|
565
|
+
"--json",
|
|
566
|
+
],
|
|
567
|
+
notificationNextAction: "ask_human_for_notification_email",
|
|
568
|
+
},
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
|
|
482
572
|
async function attachPaymentWorkflow(
|
|
483
573
|
client,
|
|
484
574
|
store,
|
|
@@ -517,7 +607,8 @@ async function attachPaymentWorkflow(
|
|
|
517
607
|
paymentChallengeDigest: request.challengeDigest,
|
|
518
608
|
paymentWorkflow: workflow,
|
|
519
609
|
});
|
|
520
|
-
|
|
610
|
+
const { humanCheckout: _ephemeralHostedCheckout, ...persisted } = safe;
|
|
611
|
+
await store.savePaymentChallenge(taskId, persisted);
|
|
521
612
|
return safe;
|
|
522
613
|
}
|
|
523
614
|
|
|
@@ -773,9 +864,20 @@ async function handleCheckoutChallenge(client, store, options, context) {
|
|
|
773
864
|
store,
|
|
774
865
|
taskId,
|
|
775
866
|
order,
|
|
776
|
-
challengeResult(taskId, order.checkoutBody, response
|
|
867
|
+
challengeResult(taskId, order.checkoutBody, response, {
|
|
868
|
+
// Initial checkout is the only interactive direct-payment surface.
|
|
869
|
+
// Autonomous renewal continues to use the bounded agent-wallet policy.
|
|
870
|
+
allowHumanCheckout: true,
|
|
871
|
+
humanCheckoutContext: {
|
|
872
|
+
baseUrl: client.baseUrl,
|
|
873
|
+
stateDirectory: store.directory,
|
|
874
|
+
},
|
|
875
|
+
}),
|
|
777
876
|
stringOption(options, "request-envelope-out"),
|
|
778
877
|
);
|
|
878
|
+
const directCheckoutInstructions = safe.humanCheckout
|
|
879
|
+
? `\nDirect wallet checkout (optional, expires ${safe.humanCheckout.expiresAt}): ${safe.humanCheckout.url}\nEncode only that URL as the purchase QR. After payment, continue with: ${shellCommand(safe.humanCheckout.afterPayment.argv)}. When provisioning is ready, ask the owner for the optional lifecycle-notification email before adding it.`
|
|
880
|
+
: "";
|
|
779
881
|
const paymentInstructions = safe.paymentWorkflow
|
|
780
882
|
? `\nWallet package: ${safe.paymentWorkflow.signerPackage.spec} (Node ${safe.paymentWorkflow.signerNodeRequirement})\nInstall: ${shellCommand(safe.paymentWorkflow.signerPackage.install.argv)}\nVerify: ${shellCommand(safe.paymentWorkflow.signerContract.probe.argv)}\n${walletWorkflowInstructions(safe.paymentWorkflow)}\nRequest envelope: ${safe.paymentWorkflow.requestEnvelopePath}\nAuthorize only after selecting/funding one wallet: ${shellCommand(safe.paymentWorkflow.authorize.argv)}\nSubmit with WarpMetal: ${shellCommand(safe.paymentWorkflow.submit.argv)}`
|
|
781
883
|
: "";
|
|
@@ -784,7 +886,7 @@ async function handleCheckoutChallenge(client, store, options, context) {
|
|
|
784
886
|
safe,
|
|
785
887
|
context.json,
|
|
786
888
|
response.status === 402
|
|
787
|
-
? `Payment authorization required for ${taskId}.${paymentInstructions}`
|
|
889
|
+
? `Payment authorization required for ${taskId}.${directCheckoutInstructions}${paymentInstructions}`
|
|
788
890
|
: `Checkout status for ${taskId}: ${safe.status}`,
|
|
789
891
|
);
|
|
790
892
|
return response.status === 409 ? 6 : response.status === 402 ? 7 : 0;
|
|
@@ -2048,6 +2150,14 @@ async function handleRuntimeInstall(client, store, options, context) {
|
|
|
2048
2150
|
stringOption(options, "identity"),
|
|
2049
2151
|
);
|
|
2050
2152
|
const sshUser = stringOption(options, "ssh-user", { required: true });
|
|
2153
|
+
const nestedPrivateProcfs =
|
|
2154
|
+
stringOption(options, "nested-private-procfs") || "preserve";
|
|
2155
|
+
if (!["preserve", "enable", "disable"].includes(nestedPrivateProcfs)) {
|
|
2156
|
+
throw new CliError(
|
|
2157
|
+
"--nested-private-procfs must be preserve, enable, or disable.",
|
|
2158
|
+
{ exitCode: 2 },
|
|
2159
|
+
);
|
|
2160
|
+
}
|
|
2051
2161
|
if (stringOption(options, "confirm", { required: true }) !== "INSTALL") {
|
|
2052
2162
|
throw new CliError(
|
|
2053
2163
|
"Confirm supervisor installation with --confirm INSTALL.",
|
|
@@ -2075,6 +2185,7 @@ async function handleRuntimeInstall(client, store, options, context) {
|
|
|
2075
2185
|
identity,
|
|
2076
2186
|
sshUser,
|
|
2077
2187
|
bootstrap: bootstrap.data,
|
|
2188
|
+
nestedPrivateProcfs,
|
|
2078
2189
|
fetchImpl: context.fetchImpl,
|
|
2079
2190
|
spawnImpl: context.spawnImpl,
|
|
2080
2191
|
});
|
|
@@ -2883,6 +2994,7 @@ async function dispatch(positionals, options, passthrough, context) {
|
|
|
2883
2994
|
"identity",
|
|
2884
2995
|
"ssh-user",
|
|
2885
2996
|
"confirm",
|
|
2997
|
+
"nested-private-procfs",
|
|
2886
2998
|
"idempotency-key",
|
|
2887
2999
|
"wait",
|
|
2888
3000
|
"timeout-seconds",
|
package/src/installer.js
CHANGED
|
@@ -5,14 +5,14 @@ import {
|
|
|
5
5
|
verify as verifySignature,
|
|
6
6
|
} from "node:crypto";
|
|
7
7
|
import { spawn as nodeSpawn } from "node:child_process";
|
|
8
|
-
import {
|
|
8
|
+
import { lstat, mkdtemp, readdir, rm, writeFile } from "node:fs/promises";
|
|
9
9
|
import { tmpdir } from "node:os";
|
|
10
10
|
import { join, resolve } from "node:path";
|
|
11
11
|
|
|
12
12
|
import { CliError } from "./errors.js";
|
|
13
13
|
|
|
14
14
|
const MAX_ARTIFACT_BYTES = 256 * 1024 * 1024;
|
|
15
|
-
const
|
|
15
|
+
const BASE_REQUIRED_FILES = [
|
|
16
16
|
"install.sh",
|
|
17
17
|
"warpmetal-agentctl",
|
|
18
18
|
"warpmetal-sandbox-gateway",
|
|
@@ -24,6 +24,26 @@ const REQUIRED_FILES = [
|
|
|
24
24
|
"warpmetal-sandbox.conf",
|
|
25
25
|
];
|
|
26
26
|
|
|
27
|
+
const NESTED_PRIVATE_PROCFS_FILES = [
|
|
28
|
+
"nested-private-procfs-oracle.sh",
|
|
29
|
+
"warpmetal-agent-runtime-bwrap",
|
|
30
|
+
"warpmetal-apparmor-policy.sh",
|
|
31
|
+
"warpmetal-policy-metadata",
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
function supportsNestedPrivateProcfs(version) {
|
|
35
|
+
const match = /^(\d+)\.(\d+)\.(\d+)/.exec(version);
|
|
36
|
+
if (!match) return false;
|
|
37
|
+
const [, major, minor, patch] = match.map(Number);
|
|
38
|
+
return major > 0 || minor > 1 || (minor === 1 && patch >= 25);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function requiredFiles(version) {
|
|
42
|
+
return supportsNestedPrivateProcfs(version)
|
|
43
|
+
? [...BASE_REQUIRED_FILES, ...NESTED_PRIVATE_PROCFS_FILES]
|
|
44
|
+
: BASE_REQUIRED_FILES;
|
|
45
|
+
}
|
|
46
|
+
|
|
27
47
|
const INSTALLER_ERROR_MESSAGES = new Map([
|
|
28
48
|
[
|
|
29
49
|
"runtime_install_lock_unavailable",
|
|
@@ -77,12 +97,75 @@ const INSTALLER_ERROR_MESSAGES = new Map([
|
|
|
77
97
|
"runtime_legacy_migration_required",
|
|
78
98
|
"Existing preview Agent Runtime state requires a separately reviewed migration; it was not reset.",
|
|
79
99
|
],
|
|
100
|
+
[
|
|
101
|
+
"runtime_nested_private_procfs_architecture_unsupported",
|
|
102
|
+
"Nested private procfs is unavailable on this host architecture.",
|
|
103
|
+
],
|
|
104
|
+
[
|
|
105
|
+
"runtime_nested_private_procfs_mode_invalid",
|
|
106
|
+
"The Agent Runtime installer rejected the nested-private-procfs action.",
|
|
107
|
+
],
|
|
108
|
+
[
|
|
109
|
+
"runtime_apparmor_policy_bundle_invalid",
|
|
110
|
+
"The signed Runtime bundle does not contain a valid nested-private-procfs policy set.",
|
|
111
|
+
],
|
|
112
|
+
[
|
|
113
|
+
"runtime_apparmor_state_unverifiable",
|
|
114
|
+
"The server's AppArmor state could not be verified safely.",
|
|
115
|
+
],
|
|
116
|
+
[
|
|
117
|
+
"runtime_apparmor_policy_unsupported",
|
|
118
|
+
"The server cannot safely enable the nested-private-procfs AppArmor policy.",
|
|
119
|
+
],
|
|
120
|
+
[
|
|
121
|
+
"runtime_apparmor_policy_backup_failed",
|
|
122
|
+
"The existing AppArmor policy state could not be preserved exactly.",
|
|
123
|
+
],
|
|
124
|
+
[
|
|
125
|
+
"runtime_apparmor_policy_conflict",
|
|
126
|
+
"The server has a conflicting nested-private-procfs AppArmor policy state.",
|
|
127
|
+
],
|
|
128
|
+
[
|
|
129
|
+
"runtime_apparmor_policy_parse_failed",
|
|
130
|
+
"The signed nested-private-procfs AppArmor policy did not pass host validation.",
|
|
131
|
+
],
|
|
132
|
+
[
|
|
133
|
+
"runtime_apparmor_policy_install_failed",
|
|
134
|
+
"The nested-private-procfs AppArmor policy could not be installed safely.",
|
|
135
|
+
],
|
|
136
|
+
[
|
|
137
|
+
"runtime_apparmor_policy_load_failed",
|
|
138
|
+
"The nested-private-procfs AppArmor policy could not be loaded and verified.",
|
|
139
|
+
],
|
|
140
|
+
[
|
|
141
|
+
"runtime_apparmor_policy_disable_failed",
|
|
142
|
+
"The nested-private-procfs AppArmor policy could not be disabled and restored safely.",
|
|
143
|
+
],
|
|
144
|
+
[
|
|
145
|
+
"runtime_apparmor_policy_rollback_failed",
|
|
146
|
+
"The AppArmor policy transaction could not restore its prior state; operator recovery is required.",
|
|
147
|
+
],
|
|
148
|
+
[
|
|
149
|
+
"runtime_apparmor_policy_recovery_failed",
|
|
150
|
+
"A prior interrupted AppArmor policy transaction could not be recovered; operator recovery is required.",
|
|
151
|
+
],
|
|
80
152
|
]);
|
|
81
153
|
|
|
154
|
+
const RECOVERY_ERROR_PRIORITY = [
|
|
155
|
+
"runtime_apparmor_policy_rollback_failed",
|
|
156
|
+
"runtime_apparmor_policy_recovery_failed",
|
|
157
|
+
];
|
|
158
|
+
|
|
82
159
|
function mappedInstallerError(result) {
|
|
83
160
|
const lines = `${result.stderr}\n${result.stdout}`
|
|
84
161
|
.split(/\r?\n/)
|
|
85
162
|
.map((line) => line.trim());
|
|
163
|
+
for (const code of RECOVERY_ERROR_PRIORITY) {
|
|
164
|
+
if (lines.includes(code)) {
|
|
165
|
+
const message = INSTALLER_ERROR_MESSAGES.get(code);
|
|
166
|
+
return { code, message: `${message} (${code})` };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
86
169
|
for (const [code, message] of INSTALLER_ERROR_MESSAGES) {
|
|
87
170
|
if (lines.includes(code)) return { code, message: `${message} (${code})` };
|
|
88
171
|
}
|
|
@@ -271,6 +354,7 @@ export async function installRuntime({
|
|
|
271
354
|
identity,
|
|
272
355
|
sshUser,
|
|
273
356
|
bootstrap,
|
|
357
|
+
nestedPrivateProcfs = "preserve",
|
|
274
358
|
fetchImpl = globalThis.fetch,
|
|
275
359
|
spawnImpl = nodeSpawn,
|
|
276
360
|
}) {
|
|
@@ -284,6 +368,20 @@ export async function installRuntime({
|
|
|
284
368
|
});
|
|
285
369
|
}
|
|
286
370
|
const metadata = validateArtifact(bootstrap?.artifact);
|
|
371
|
+
if (!new Set(["preserve", "enable", "disable"]).has(nestedPrivateProcfs)) {
|
|
372
|
+
throw new CliError("The nested private procfs action is invalid.", {
|
|
373
|
+
exitCode: 2,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
if (
|
|
377
|
+
nestedPrivateProcfs !== "preserve" &&
|
|
378
|
+
!supportsNestedPrivateProcfs(metadata.version)
|
|
379
|
+
) {
|
|
380
|
+
throw new CliError(
|
|
381
|
+
`Agent Runtime ${metadata.version} does not support nested private procfs actions.`,
|
|
382
|
+
{ exitCode: 4 },
|
|
383
|
+
);
|
|
384
|
+
}
|
|
287
385
|
const bootstrapToken = bootstrap?.bootstrapToken;
|
|
288
386
|
if (
|
|
289
387
|
typeof bootstrapToken !== "string" ||
|
|
@@ -316,16 +414,26 @@ export async function installRuntime({
|
|
|
316
414
|
spawnImpl,
|
|
317
415
|
},
|
|
318
416
|
);
|
|
319
|
-
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
417
|
+
const expectedFiles = requiredFiles(metadata.version);
|
|
418
|
+
const actualFiles = await readdir(extractPath);
|
|
419
|
+
const exactNames =
|
|
420
|
+
actualFiles.length === expectedFiles.length &&
|
|
421
|
+
expectedFiles.every((name) => actualFiles.includes(name));
|
|
422
|
+
if (!exactNames) {
|
|
324
423
|
throw new CliError(
|
|
325
|
-
"The signed runtime bundle
|
|
424
|
+
"The signed runtime bundle files do not match this CLI version.",
|
|
326
425
|
{ exitCode: 4 },
|
|
327
426
|
);
|
|
328
427
|
}
|
|
428
|
+
for (const file of expectedFiles) {
|
|
429
|
+
const metadata = await lstat(join(extractPath, file));
|
|
430
|
+
if (!metadata.isFile() || metadata.isSymbolicLink()) {
|
|
431
|
+
throw new CliError(
|
|
432
|
+
"The signed runtime bundle files do not match this CLI version.",
|
|
433
|
+
{ exitCode: 4 },
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
329
437
|
remoteTouched = true;
|
|
330
438
|
await spawnChecked(
|
|
331
439
|
"scp",
|
|
@@ -351,19 +459,23 @@ export async function installRuntime({
|
|
|
351
459
|
],
|
|
352
460
|
{ spawnImpl },
|
|
353
461
|
);
|
|
462
|
+
const installerArguments = [
|
|
463
|
+
...ssh,
|
|
464
|
+
"sudo",
|
|
465
|
+
`${remoteBundle}/install.sh`,
|
|
466
|
+
"--api",
|
|
467
|
+
client.baseUrl,
|
|
468
|
+
"--server",
|
|
469
|
+
serverId,
|
|
470
|
+
"--bundle",
|
|
471
|
+
remoteBundle,
|
|
472
|
+
];
|
|
473
|
+
if (nestedPrivateProcfs !== "preserve") {
|
|
474
|
+
installerArguments.push("--nested-private-procfs", nestedPrivateProcfs);
|
|
475
|
+
}
|
|
354
476
|
await spawnChecked(
|
|
355
477
|
"ssh",
|
|
356
|
-
|
|
357
|
-
...ssh,
|
|
358
|
-
"sudo",
|
|
359
|
-
`${remoteBundle}/install.sh`,
|
|
360
|
-
"--api",
|
|
361
|
-
client.baseUrl,
|
|
362
|
-
"--server",
|
|
363
|
-
serverId,
|
|
364
|
-
"--bundle",
|
|
365
|
-
remoteBundle,
|
|
366
|
-
],
|
|
478
|
+
installerArguments,
|
|
367
479
|
{
|
|
368
480
|
stdin: `${bootstrapToken}\n`,
|
|
369
481
|
spawnImpl,
|
|
@@ -378,7 +490,12 @@ export async function installRuntime({
|
|
|
378
490
|
spawnImpl,
|
|
379
491
|
},
|
|
380
492
|
);
|
|
381
|
-
return {
|
|
493
|
+
return {
|
|
494
|
+
serverId,
|
|
495
|
+
supervisorVersion: metadata.version,
|
|
496
|
+
installed: true,
|
|
497
|
+
nestedPrivateProcfsAction: nestedPrivateProcfs,
|
|
498
|
+
};
|
|
382
499
|
} catch (error) {
|
|
383
500
|
operationFailed = true;
|
|
384
501
|
throw error;
|