warpmetal 0.8.11 → 0.8.13
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 +111 -13
- package/package.json +1 -1
- package/skills/warpmetal/SKILL.md +17 -12
- package/skills/warpmetal/references/cli-reference.md +84 -6
- package/skills/warpmetal/references/runtime.md +29 -8
- package/skills/warpmetal/references/safety.md +4 -3
- package/src/api.js +30 -0
- package/src/cli.js +259 -11
- package/src/runtime.js +59 -2
- package/src/ssh-alias.js +21 -12
package/README.md
CHANGED
|
@@ -41,11 +41,15 @@ x402api help --json
|
|
|
41
41
|
Install the bundled skill for supported coding agents:
|
|
42
42
|
|
|
43
43
|
```sh
|
|
44
|
+
warpmetal agent install --target <codex|claude|all>
|
|
44
45
|
warpmetal agent install --target codex
|
|
45
46
|
warpmetal agent install --target claude
|
|
46
47
|
warpmetal agent install --target all
|
|
47
48
|
```
|
|
48
49
|
|
|
50
|
+
`warpmetal agent install` installs only the bundled WarpMetal skill; it does
|
|
51
|
+
not install sandbox tools or select a sandbox tool profile.
|
|
52
|
+
|
|
49
53
|
Use `--scope project` to install into the current repository instead of the
|
|
50
54
|
user-level agent directory.
|
|
51
55
|
|
|
@@ -58,7 +62,7 @@ integration.
|
|
|
58
62
|
|
|
59
63
|
The repository also contains a skills-only WarpMetal plugin for the public
|
|
60
64
|
Plugins Directory shared by Codex and ChatGPT. The plugin remains a separate
|
|
61
|
-
artifact from the npm CLI and requires `warpmetal` CLI version 0.8.
|
|
65
|
+
artifact from the npm CLI and requires `warpmetal` CLI version 0.8.13 or newer.
|
|
62
66
|
|
|
63
67
|
To test the repository marketplace after the plugin lands on `main`:
|
|
64
68
|
|
|
@@ -317,9 +321,10 @@ compatible external signer.
|
|
|
317
321
|
Wallet key management and signing remain outside this package.
|
|
318
322
|
- Destructive or state-changing commands require explicit confirmations and
|
|
319
323
|
generate idempotency keys by default.
|
|
320
|
-
- Runtime bootstrap credentials remain memory-only
|
|
321
|
-
the first host key has been pinned and strictly reverified
|
|
322
|
-
|
|
324
|
+
- Runtime bootstrap credentials remain memory-only. Manual installation asks
|
|
325
|
+
for one only after the first host key has been pinned and strictly reverified;
|
|
326
|
+
automatic reload bootstrap is rendered directly into provider-bound
|
|
327
|
+
cloud-init. Signed supervisor bundles are checksum- and signature-verified.
|
|
323
328
|
- Each agent gets a distinct SSH key forced into exactly one sandbox. Token-free
|
|
324
329
|
connection profiles pin the VPS host key and contain no owner credential or
|
|
325
330
|
private-key material.
|
|
@@ -332,11 +337,43 @@ compatible external signer.
|
|
|
332
337
|
external workspace, lifetime, and start time. The wait completes only when
|
|
333
338
|
the observed digest and generation both match the accepted target.
|
|
334
339
|
- Guarded reload powers the server off first. Runtime-enabled reload requires a
|
|
335
|
-
second acknowledgment
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
+
second acknowledgment because all sandbox workspaces are erased. WarpMetal
|
|
341
|
+
places the approved signed Runtime bootstrap in reload cloud-init
|
|
342
|
+
automatically; wait for Runtime readiness, then refresh pinned sandbox
|
|
343
|
+
connection profiles. Only a locally recorded reload operation that succeeds
|
|
344
|
+
and reports an owner-host-key refresh opens one new managed trust epoch;
|
|
345
|
+
failed or ambiguous reloads retain the old pin and never permit replacement.
|
|
346
|
+
Erased workspaces are never described as recoverable.
|
|
347
|
+
- New agent-enabled candidate orders and Runtime-enabled reloads pass the
|
|
348
|
+
closed nested-sandbox opt-in inside verified provider cloud-init. The signed
|
|
349
|
+
Runtime bundle installs the exact-path AppArmor policy for the neutral
|
|
350
|
+
image's immutable Bubblewrap helper; this needs no follow-up customer SSH
|
|
351
|
+
key and adds no public order field or CLI flag. VPS-only cloud-init is
|
|
352
|
+
unchanged. Existing enrolled hosts use reload/reprovision; the CLI does not
|
|
353
|
+
claim a silent in-place policy repair command.
|
|
354
|
+
- Runtime reports a new or replaced sandbox generation as running only after a
|
|
355
|
+
fixed, credential-free inner-Bubblewrap oracle proves nested namespaces, an
|
|
356
|
+
inherited read-only view of the outer Agent Box's already isolated procfs,
|
|
357
|
+
an allowed workspace write, empty Linux capability sets,
|
|
358
|
+
`no-new-privileges`, and protected-root write denial. Failure stops the
|
|
359
|
+
container before access grants or tool setup advance.
|
|
360
|
+
|
|
361
|
+
For a Runtime-enabled reload, no separate installation command is part of the
|
|
362
|
+
successful path:
|
|
363
|
+
|
|
364
|
+
```sh
|
|
365
|
+
warpmetal server reload \
|
|
366
|
+
--server <serverId> --confirm ERASE --power-off-first \
|
|
367
|
+
--acknowledge-agent-runtime-reset --wait --json
|
|
368
|
+
warpmetal runtime get --server <serverId> --wait --json
|
|
369
|
+
warpmetal sandbox access refresh \
|
|
370
|
+
--server <serverId> --sandbox <sandboxId> --grant <grantId> \
|
|
371
|
+
--connection-file <profile-path> --confirm REFRESH --wait --json
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
The successful reload records a new operation-bound owner SSH trust epoch.
|
|
375
|
+
Verify the replacement host key before using owner SSH; sandbox access remains
|
|
376
|
+
strictly pinned through each refreshed connection profile.
|
|
340
377
|
|
|
341
378
|
## Agent Runtime example
|
|
342
379
|
|
|
@@ -367,6 +404,61 @@ warpmetal sandbox access keygen \
|
|
|
367
404
|
--json
|
|
368
405
|
```
|
|
369
406
|
|
|
407
|
+
### Registered sandbox tools
|
|
408
|
+
|
|
409
|
+
WarpMetal can apply only registered, immutable tool profiles. These commands use
|
|
410
|
+
the owner credential already stored in the private WarpMetal state file; they do
|
|
411
|
+
not accept a bearer token on the command line or permit a caller-supplied URL,
|
|
412
|
+
shell command, environment, or artifact override.
|
|
413
|
+
|
|
414
|
+
```sh
|
|
415
|
+
warpmetal tools list --server <serverId> --json
|
|
416
|
+
warpmetal tools install --server <serverId> --sandbox <sandboxId> --profile <profileId> \
|
|
417
|
+
[--idempotency-key <key>] [--wait] [--timeout-seconds <n>] --json
|
|
418
|
+
warpmetal tools status --server <serverId> [--wait] [--timeout-seconds <n>] --json
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
`tools install` posts only the selected registered `profileId`. Waiting is
|
|
422
|
+
bounded: `ready` exits 0, `failed` or `cancelled` exits 5, and a timeout exits 8.
|
|
423
|
+
JSON output contains public profile, operation, and receipt metadata only.
|
|
424
|
+
Successful non-wait installs and status inspections exit 0 while reporting the
|
|
425
|
+
accepted or current state; exit 8 is reserved for an actual `--wait` deadline
|
|
426
|
+
timeout.
|
|
427
|
+
|
|
428
|
+
Codex is a released automatic tool profile when the public
|
|
429
|
+
`/agent-tool-profiles` endpoint advertises it as `available`. WarpMetal installs
|
|
430
|
+
the exact registered artifacts when selected at order time or through
|
|
431
|
+
`tools install`. Claude Code uses the released `claude-code` automatic tool
|
|
432
|
+
profile under the same public availability gate. Claude Managed Agents are separate:
|
|
433
|
+
`claude-managed-ant` is an install-only CLI profile. Installing `ant` does not
|
|
434
|
+
authenticate a worker and does not activate Managed Agents. Cursor CLI remains
|
|
435
|
+
manual and unavailable as an automatic profile until separately qualified later.
|
|
436
|
+
Gemini CLI remains manual.
|
|
437
|
+
|
|
438
|
+
An order `--runtime-file` may add the closed optional setup intent beside its
|
|
439
|
+
`sandboxes` array:
|
|
440
|
+
|
|
441
|
+
```json
|
|
442
|
+
{
|
|
443
|
+
"sandboxes": [
|
|
444
|
+
{ "name": "codex-worker", "size": "small" },
|
|
445
|
+
{ "name": "claude-worker", "size": "small" },
|
|
446
|
+
{ "name": "managed-worker", "size": "small" }
|
|
447
|
+
],
|
|
448
|
+
"setup": {
|
|
449
|
+
"version": 1,
|
|
450
|
+
"sandboxProfiles": [
|
|
451
|
+
{ "sandboxName": "codex-worker", "profileId": "codex" },
|
|
452
|
+
{ "sandboxName": "claude-worker", "profileId": "claude-code" },
|
|
453
|
+
{ "sandboxName": "managed-worker", "profileId": "claude-managed-ant" }
|
|
454
|
+
]
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
Only those fields are accepted. Each setup entry must name a sandbox in the
|
|
460
|
+
same file and a registered profile ID.
|
|
461
|
+
|
|
370
462
|
### Install a standard sandbox SSH alias
|
|
371
463
|
|
|
372
464
|
Use a separate keypair and access grant for each sandbox. After the grant is
|
|
@@ -390,10 +482,16 @@ ssh -t <alias> gemini
|
|
|
390
482
|
ssh <alias> gemini -p '<task>'
|
|
391
483
|
```
|
|
392
484
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
485
|
+
Authenticate Codex or Claude Code inside the sandbox after its selected profile
|
|
486
|
+
is ready. The public `/agent-tool-profiles` response is authoritative for
|
|
487
|
+
availability and checkout selection. Claude Code uses the `claude-code`
|
|
488
|
+
automatic tool profile. Claude Managed Agents are
|
|
489
|
+
separate: `claude-managed-ant` is an install-only CLI profile. Installing `ant`
|
|
490
|
+
does not authenticate a worker and does not activate Managed Agents. Cursor CLI
|
|
491
|
+
remains manual and unavailable as an automatic profile until separately
|
|
492
|
+
qualified later. Install and authenticate Cursor CLI or Gemini CLI inside the
|
|
493
|
+
sandbox. Provider credentials are sandbox-owned and remain in its
|
|
494
|
+
persistent home; WarpMetal does not perform provider login or receive them.
|
|
397
495
|
|
|
398
496
|
The alias is local-only. It prepends a managed include to `~/.ssh/config` and
|
|
399
497
|
uses the profile's pinned host key plus the sandbox identity. It never uses or
|
package/package.json
CHANGED
|
@@ -11,7 +11,7 @@ with ad hoc HTTP commands.
|
|
|
11
11
|
|
|
12
12
|
## Start safely
|
|
13
13
|
|
|
14
|
-
1. Run `warpmetal --version` and require version `0.8.
|
|
14
|
+
1. Run `warpmetal --version` and require version `0.8.13` or newer for this
|
|
15
15
|
plugin. If it is missing or older, explain the compatibility requirement,
|
|
16
16
|
ask before installing or upgrading software, and use only the official npm
|
|
17
17
|
package from `https://www.npmjs.com/package/warpmetal`.
|
|
@@ -254,8 +254,8 @@ warpmetal server power \
|
|
|
254
254
|
For a destructive reload, explain that every server-disk file is erased and
|
|
255
255
|
obtain explicit approval. If Agent Runtime is enabled, also explain that every
|
|
256
256
|
sandbox workspace is lost, desired sandboxes return as empty workspaces after
|
|
257
|
-
|
|
258
|
-
command:
|
|
257
|
+
automatic Runtime setup, and pinned profiles must be refreshed. Then use only
|
|
258
|
+
the guarded command:
|
|
259
259
|
|
|
260
260
|
```sh
|
|
261
261
|
warpmetal server reload \
|
|
@@ -271,14 +271,18 @@ warpmetal server reload \
|
|
|
271
271
|
The CLI requires the recovery owner credential so it can keep polling after
|
|
272
272
|
reload revokes SSH-derived access tokens. CLI 0.8.8 records a new SSH trust
|
|
273
273
|
epoch only when that exact local reload operation succeeds and reports that the
|
|
274
|
-
owner host key needs refresh.
|
|
275
|
-
observed Ed25519 host key once in that epoch, then must reconnect strictly
|
|
276
|
-
before requesting bootstrap. Failed or ambiguous reloads retain the prior pin;
|
|
274
|
+
owner host key needs refresh. Failed or ambiguous reloads retain the prior pin;
|
|
277
275
|
never bypass a mismatch or delete a pin to force a retry. A provider-console
|
|
278
|
-
pre-seed is the optional higher-assurance path.
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
276
|
+
pre-seed is the optional higher-assurance path.
|
|
277
|
+
|
|
278
|
+
After a successful Runtime-enabled reload, WarpMetal performs signed Runtime
|
|
279
|
+
setup automatically. Do not run a separate installation command. Wait with
|
|
280
|
+
`warpmetal runtime get --server <serverId> --wait --json`, then wait for grants
|
|
281
|
+
to become `applied` and refresh every connection profile with
|
|
282
|
+
`sandbox access refresh --confirm REFRESH` before connecting. The successful
|
|
283
|
+
operation created a new owner host-trust epoch, so verify the replacement host
|
|
284
|
+
key before owner SSH. Do not fall back to raw API calls for deletion,
|
|
285
|
+
networking, or another unsupported mutation.
|
|
282
286
|
|
|
283
287
|
## Use Agent Runtime
|
|
284
288
|
|
|
@@ -286,12 +290,13 @@ Agent Runtime is optional and shares one owner's VPS only among that owner's
|
|
|
286
290
|
agents. Discover live `agentRuntime` capacity and OS support before choosing
|
|
287
291
|
sizes. Use `--runtime-file` to include sandbox intent in an unpaid order, or
|
|
288
292
|
`warpmetal runtime enable` after the VPS is ready. Supervisor installation is
|
|
289
|
-
separate and requires approval plus `--confirm INSTALL
|
|
293
|
+
separate and requires approval plus `--confirm INSTALL` for initial setup or
|
|
294
|
+
explicit repair; Runtime-enabled OS reloads use automatic signed cloud-init.
|
|
290
295
|
|
|
291
296
|
With CLI 0.8.8, that confirmation also authorizes managed trust on first use
|
|
292
297
|
when the exact server trust epoch has no pin. The CLI performs only an owner-
|
|
293
298
|
key-authenticated `ssh true`, pins the first observed Ed25519 key, immediately
|
|
294
|
-
reconnects strictly
|
|
299
|
+
reconnects strictly before requesting bootstrap. Later connections
|
|
295
300
|
must match. Explain that TOFU cannot detect an active attacker on the first
|
|
296
301
|
connection; never use `ssh-keyscan`, accept a mismatch, or expose a generic pin
|
|
297
302
|
reset.
|
|
@@ -201,7 +201,18 @@ Reload requires the recovery owner credential rather than a short-lived
|
|
|
201
201
|
SSH-derived token. `--power-off-first` authorizes shutdown and powered-off
|
|
202
202
|
verification inside the same operation. When Agent Runtime is enabled,
|
|
203
203
|
`--acknowledge-agent-runtime-reset` is required because workspaces are erased,
|
|
204
|
-
the
|
|
204
|
+
the Runtime identity is replaced, empty sandboxes are reconciled automatically,
|
|
205
|
+
and connection profiles must be refreshed. After a successful reload, wait for
|
|
206
|
+
automatic setup before refreshing profiles:
|
|
207
|
+
|
|
208
|
+
```sh
|
|
209
|
+
warpmetal runtime get --server <serverId> --wait --json
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
The successful operation records a new owner SSH trust epoch. Verify the
|
|
213
|
+
replacement host key before owner SSH. Manual Runtime installation remains an
|
|
214
|
+
explicit repair path when automatic setup fails; it is not part of a
|
|
215
|
+
successful reload.
|
|
205
216
|
|
|
206
217
|
Use `--token-file` only for recovery when local state is unavailable. Prefer
|
|
207
218
|
`WARPMETAL_OWNER_TOKEN` or `WARPMETAL_ACCESS_TOKEN` for a single command over a
|
|
@@ -210,6 +221,13 @@ process listings.
|
|
|
210
221
|
|
|
211
222
|
## Agent Runtime and sandboxes
|
|
212
223
|
|
|
224
|
+
Agent-enabled first boot and Runtime-enabled reload automatically carry the
|
|
225
|
+
closed nested-sandbox opt-in inside verified provider cloud-init. The signed
|
|
226
|
+
Runtime bundle installs the exact-path policy for the image's immutable
|
|
227
|
+
Bubblewrap helper without a later customer SSH key. This is not a public order
|
|
228
|
+
field or CLI flag; VPS-only cloud-init remains unchanged, and existing enrolled
|
|
229
|
+
hosts use reload/reprovision. There is no silent in-place policy repair.
|
|
230
|
+
|
|
213
231
|
```sh
|
|
214
232
|
warpmetal runtime enable --server <serverId> [--idempotency-key <key>] --json
|
|
215
233
|
warpmetal runtime get --server <serverId> [--wait] [--timeout-seconds <n>] --json
|
|
@@ -232,6 +250,11 @@ warpmetal sandbox action \
|
|
|
232
250
|
[--wait] --json
|
|
233
251
|
warpmetal sandbox delete \
|
|
234
252
|
--server <serverId> --sandbox <sandboxId> --confirm DELETE [--wait] --json
|
|
253
|
+
|
|
254
|
+
warpmetal tools list --server <serverId> --json
|
|
255
|
+
warpmetal tools install --server <serverId> --sandbox <sandboxId> --profile <profileId> \
|
|
256
|
+
[--idempotency-key <key>] [--wait] [--timeout-seconds <n>] --json
|
|
257
|
+
warpmetal tools status --server <serverId> [--wait] [--timeout-seconds <n>] --json
|
|
235
258
|
```
|
|
236
259
|
|
|
237
260
|
CLI 0.8.8 manages owner-facing VPS host trust during `runtime install`. With no
|
|
@@ -245,7 +268,52 @@ reloads never replace trust. This TOFU step cannot detect an active attacker on
|
|
|
245
268
|
the first connection. Provider-console pre-enrollment is optional and stronger.
|
|
246
269
|
|
|
247
270
|
See [runtime.md](runtime.md) for capacity, lifetime, cleanup, polling, and
|
|
248
|
-
installation safety. Exit 8
|
|
271
|
+
installation safety. Exit 8 is reserved for an actual bounded `--wait`
|
|
272
|
+
deadline timeout; a successful non-wait inspection exits 0 while reporting an
|
|
273
|
+
accepted, pending, applying, ready, or empty current state.
|
|
274
|
+
|
|
275
|
+
The `tools` commands use only the owner token already stored for the named
|
|
276
|
+
server. They do not accept token argv or `--token-file`. `tools list` returns
|
|
277
|
+
the registered immutable profiles. `tools install` sends only `{profileId}`
|
|
278
|
+
with an idempotency key; callers cannot supply download URLs, shell commands,
|
|
279
|
+
argv, environment variables, or artifacts. With `--wait`, `ready` exits 0,
|
|
280
|
+
`failed` or `cancelled` exits 5, and a bounded timeout exits 8. JSON contains
|
|
281
|
+
only the public profile/setup-operation fields returned by WarpMetal.
|
|
282
|
+
|
|
283
|
+
Codex is a released automatic tool profile when the public
|
|
284
|
+
`/agent-tool-profiles` endpoint advertises it as `available`. WarpMetal installs
|
|
285
|
+
the exact registered artifacts when selected at order time or through
|
|
286
|
+
`tools install`. Claude Code uses the released `claude-code` automatic tool
|
|
287
|
+
profile under the same public availability gate. Claude Managed Agents are separate:
|
|
288
|
+
`claude-managed-ant` is an install-only CLI profile. Installing `ant` does not
|
|
289
|
+
authenticate a worker and does not activate Managed Agents. Cursor CLI remains
|
|
290
|
+
manual and unavailable as an automatic profile until separately qualified later.
|
|
291
|
+
Gemini CLI remains manual.
|
|
292
|
+
|
|
293
|
+
For order-time setup, the runtime JSON file may contain the exact optional
|
|
294
|
+
shape below in addition to `sandboxes`:
|
|
295
|
+
|
|
296
|
+
```json
|
|
297
|
+
{
|
|
298
|
+
"sandboxes": [
|
|
299
|
+
{ "name": "codex-worker", "size": "small" },
|
|
300
|
+
{ "name": "claude-worker", "size": "small" },
|
|
301
|
+
{ "name": "managed-worker", "size": "small" }
|
|
302
|
+
],
|
|
303
|
+
"setup": {
|
|
304
|
+
"version": 1,
|
|
305
|
+
"sandboxProfiles": [
|
|
306
|
+
{ "sandboxName": "codex-worker", "profileId": "codex" },
|
|
307
|
+
{ "sandboxName": "claude-worker", "profileId": "claude-code" },
|
|
308
|
+
{ "sandboxName": "managed-worker", "profileId": "claude-managed-ant" }
|
|
309
|
+
]
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Every selection must reference a sandbox name in the same file. Unknown fields,
|
|
315
|
+
including URL, shell, command, argv, environment, or artifact overrides, are
|
|
316
|
+
rejected before an API request.
|
|
249
317
|
|
|
250
318
|
## Per-agent access
|
|
251
319
|
|
|
@@ -277,7 +345,7 @@ returns the OpenSSH or remote exit status. `--connection-file` on grant
|
|
|
277
345
|
creation requires `--wait`.
|
|
278
346
|
`sandbox access refresh` atomically replaces a stale token-free profile with
|
|
279
347
|
the currently applied grant and API-reported pinned host keys; use it after an
|
|
280
|
-
OS reload
|
|
348
|
+
OS reload once automatic Runtime reconciliation is ready.
|
|
281
349
|
|
|
282
350
|
`sandbox access install-ssh` is local-only and requires CLI 0.8.10 or newer.
|
|
283
351
|
It turns the reviewed profile and sandbox-private identity into a concrete
|
|
@@ -304,9 +372,16 @@ ssh -t <alias> gemini
|
|
|
304
372
|
ssh <alias> gemini -p '<task>'
|
|
305
373
|
```
|
|
306
374
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
375
|
+
Authenticate each selected provider tool inside the sandbox after setup is
|
|
376
|
+
ready. The public `/agent-tool-profiles` response is authoritative for
|
|
377
|
+
availability and checkout selection. Claude Code uses the `claude-code`
|
|
378
|
+
automatic tool profile. Claude Managed Agents are
|
|
379
|
+
separate: `claude-managed-ant` is an install-only CLI profile. Installing `ant`
|
|
380
|
+
does not authenticate a worker and does not activate Managed Agents. Cursor CLI
|
|
381
|
+
remains manual and unavailable as an automatic profile until separately
|
|
382
|
+
qualified later. Install and authenticate Cursor CLI or Gemini CLI inside the
|
|
383
|
+
sandbox. WarpMetal does not perform provider login or receive provider
|
|
384
|
+
credentials.
|
|
310
385
|
|
|
311
386
|
[Codex Desktop](https://learn.chatgpt.com/docs/remote-connections) reads
|
|
312
387
|
the concrete alias from `~/.ssh/config` and starts Codex through the sandbox
|
|
@@ -330,6 +405,9 @@ warpmetal agent install --target <codex|claude|all> [--scope user|project]
|
|
|
330
405
|
warpmetal state list --json
|
|
331
406
|
```
|
|
332
407
|
|
|
408
|
+
`warpmetal agent install` installs only the bundled WarpMetal skill; it does
|
|
409
|
+
not install sandbox tools or select a sandbox tool profile.
|
|
410
|
+
|
|
333
411
|
`state list` returns identifiers, public runtime metadata, and
|
|
334
412
|
credential-presence booleans only. Never
|
|
335
413
|
open the underlying state file from an agent session.
|
|
@@ -46,6 +46,20 @@ Pass it to `warpmetal order prepare --runtime-file <path>`. If it contains a
|
|
|
46
46
|
temporary sandbox, pass `--confirm TEMPORARY`. Preparing remains unpaid; keep
|
|
47
47
|
the existing separate interactive-or-autonomous payment authority check.
|
|
48
48
|
|
|
49
|
+
For released Codex and Claude Code automatic profiles, an agent-enabled first
|
|
50
|
+
boot passes a closed nested-sandbox opt-in through verified
|
|
51
|
+
provider cloud-init. The signed Runtime bundle owns the exact-path AppArmor
|
|
52
|
+
policy for the neutral image's immutable Bubblewrap helper. No later customer
|
|
53
|
+
SSH key is used. No public order field or CLI flag exists; VPS-only cloud-init
|
|
54
|
+
remains unchanged. Existing enrolled hosts require reload/reprovision for this policy
|
|
55
|
+
because no silent in-place repair command is claimed.
|
|
56
|
+
Runtime does not report a new or replaced sandbox generation as running until a
|
|
57
|
+
fixed, credential-free inner-Bubblewrap oracle proves nested namespaces, an
|
|
58
|
+
inherited read-only view of the outer Agent Box's already isolated procfs, an
|
|
59
|
+
allowed workspace write, empty Linux capability sets, `no-new-privileges`, and
|
|
60
|
+
protected-root write denial; failure stops the container before access grants
|
|
61
|
+
or tool setup advance.
|
|
62
|
+
|
|
49
63
|
For an existing ready server:
|
|
50
64
|
|
|
51
65
|
Use the WarpMetal owner account `root` for every supported host image; do not
|
|
@@ -208,11 +222,12 @@ The token-free profile is written only after the grant is `applied` and the
|
|
|
208
222
|
API supplies verified VPS host keys. Do not print or open that profile in an
|
|
209
223
|
agent conversation.
|
|
210
224
|
|
|
211
|
-
After a destructive OS reload,
|
|
212
|
-
|
|
213
|
-
profile:
|
|
225
|
+
After a successful destructive OS reload, the signed Runtime bootstrap runs
|
|
226
|
+
automatically. Wait for Runtime readiness and for the retained active grant to
|
|
227
|
+
become `applied`, then replace its stale pinned profile:
|
|
214
228
|
|
|
215
229
|
```sh
|
|
230
|
+
warpmetal runtime get --server <serverId> --wait --json
|
|
216
231
|
warpmetal sandbox access refresh \
|
|
217
232
|
--server <serverId> \
|
|
218
233
|
--sandbox <sandboxId> \
|
|
@@ -225,7 +240,8 @@ warpmetal sandbox access refresh \
|
|
|
225
240
|
|
|
226
241
|
The sandbox record is retained, but its old workspace is not; reconciliation
|
|
227
242
|
creates a new empty workspace. Never bypass a host-key mismatch or reuse the
|
|
228
|
-
pre-reload profile.
|
|
243
|
+
pre-reload profile. Use manual Runtime installation only as an explicitly
|
|
244
|
+
approved repair when the backend reports that automatic setup failed.
|
|
229
245
|
|
|
230
246
|
### Install a concrete OpenSSH alias
|
|
231
247
|
|
|
@@ -298,10 +314,15 @@ ssh -t <alias> gemini
|
|
|
298
314
|
ssh <alias> gemini -p '<task>'
|
|
299
315
|
```
|
|
300
316
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
317
|
+
Authenticate each selected provider tool inside the sandbox after setup is
|
|
318
|
+
ready. The public `/agent-tool-profiles` endpoint is authoritative for profile
|
|
319
|
+
availability. Claude Code uses the released `claude-code` automatic tool
|
|
320
|
+
profile. Claude Managed Agents are separate: `claude-managed-ant` is an
|
|
321
|
+
install-only CLI profile. Installing `ant` does not authenticate a worker and
|
|
322
|
+
does not activate Managed Agents. Cursor CLI remains manual and unavailable as
|
|
323
|
+
an automatic profile until separately qualified later. Provider authentication
|
|
324
|
+
and credentials are sandbox-owned and persist only in the sandbox home;
|
|
325
|
+
WarpMetal does not perform provider login or receive those credentials.
|
|
305
326
|
|
|
306
327
|
[Codex Desktop](https://learn.chatgpt.com/docs/remote-connections)
|
|
307
328
|
discovers a concrete alias through `~/.ssh/config`, requires ordinary
|
|
@@ -95,9 +95,10 @@ A reload requires both `confirm: "ERASE"` and `powerOffFirst: true`. Treat
|
|
|
95
95
|
server, wait until it is powered off, and then erase and reinstall it inside
|
|
96
96
|
one lifecycle operation. When Agent Runtime is enabled, also require
|
|
97
97
|
`acknowledgeAgentRuntimeReset`: all sandbox workspaces are permanently erased,
|
|
98
|
-
the supervisor identity is revoked,
|
|
99
|
-
|
|
100
|
-
CLI command
|
|
98
|
+
the supervisor identity is revoked, signed Runtime setup runs automatically,
|
|
99
|
+
desired sandboxes are recreated empty, and all pinned profiles require refresh.
|
|
100
|
+
Use only the guarded CLI command, wait for Runtime readiness, and stop on
|
|
101
|
+
`manual_review`. Reload invalidates the prior
|
|
101
102
|
owner-facing SSH host-key trust decision; the provider may rotate or preserve
|
|
102
103
|
the key. CLI 0.8.8 opens one new managed trust epoch only after the exact local
|
|
103
104
|
reload operation succeeds and reports that refresh is required. The first
|
package/src/api.js
CHANGED
|
@@ -346,6 +346,36 @@ export class WarpMetalClient {
|
|
|
346
346
|
);
|
|
347
347
|
}
|
|
348
348
|
|
|
349
|
+
listAgentToolProfiles(token) {
|
|
350
|
+
return this.request("GET", "/agent-tool-profiles", { token });
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
installAgentToolProfile(
|
|
354
|
+
serverId,
|
|
355
|
+
sandboxId,
|
|
356
|
+
profileId,
|
|
357
|
+
token,
|
|
358
|
+
idempotencyKey,
|
|
359
|
+
) {
|
|
360
|
+
return this.request(
|
|
361
|
+
"POST",
|
|
362
|
+
`/servers/${encodeURIComponent(serverId)}/sandboxes/${encodeURIComponent(sandboxId)}/tool-setup-operations`,
|
|
363
|
+
{
|
|
364
|
+
body: { profileId },
|
|
365
|
+
token,
|
|
366
|
+
idempotencyKey,
|
|
367
|
+
},
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
getAgentSetup(serverId, token) {
|
|
372
|
+
return this.request(
|
|
373
|
+
"GET",
|
|
374
|
+
`/servers/${encodeURIComponent(serverId)}/agent-setup`,
|
|
375
|
+
{ token },
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
349
379
|
sandboxAction(serverId, sandboxId, action, token, idempotencyKey) {
|
|
350
380
|
const body =
|
|
351
381
|
action === "refresh_image"
|
package/src/cli.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { readFile, stat } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
3
4
|
import { basename, join, resolve } from "node:path";
|
|
4
5
|
|
|
5
6
|
import {
|
|
@@ -67,6 +68,7 @@ const SANDBOX_TERMINAL_STATES = new Set([
|
|
|
67
68
|
"failed",
|
|
68
69
|
]);
|
|
69
70
|
const GRANT_TERMINAL_STATES = new Set(["applied", "revoked", "failed"]);
|
|
71
|
+
const TOOL_SETUP_TERMINAL_STATES = new Set(["ready", "failed", "cancelled"]);
|
|
70
72
|
const COMMON_OPTIONS = ["base-url", "json", "state-dir", "help"];
|
|
71
73
|
|
|
72
74
|
const HELP = `WarpMetal CLI ${VERSION}
|
|
@@ -127,6 +129,10 @@ Usage:
|
|
|
127
129
|
--alias <alias> [--confirm REFRESH]
|
|
128
130
|
warpmetal sandbox access remove-ssh --alias <alias> --confirm REMOVE
|
|
129
131
|
warpmetal sandbox connect --connection-file <path> --identity <sandbox-key> [-- <command>]
|
|
132
|
+
warpmetal tools list --server <serverId>
|
|
133
|
+
warpmetal tools install --server <serverId> --sandbox <sandboxId> --profile <profileId>
|
|
134
|
+
[--idempotency-key <key>] [--wait] [--timeout-seconds <n>]
|
|
135
|
+
warpmetal tools status --server <serverId> [--wait] [--timeout-seconds <n>]
|
|
130
136
|
warpmetal state list
|
|
131
137
|
warpmetal agent install --target <codex|claude|all> [--scope <user|project>] [--force]
|
|
132
138
|
|
|
@@ -147,6 +153,34 @@ SSH host trust (CLI 0.8.8+):
|
|
|
147
153
|
active attacker on the first connection. Provider-console pre-enrollment is
|
|
148
154
|
the optional higher-assurance alternative.
|
|
149
155
|
|
|
156
|
+
Runtime-enabled reloads:
|
|
157
|
+
Reload still requires --acknowledge-agent-runtime-reset because every
|
|
158
|
+
sandbox workspace is erased. After a successful reload, Runtime setup is
|
|
159
|
+
automatic: use warpmetal runtime get --server <serverId> --wait --json,
|
|
160
|
+
then refresh every sandbox connection profile before reconnecting. The
|
|
161
|
+
successful operation also records a new owner SSH trust epoch; verify the
|
|
162
|
+
replacement host key before owner SSH.
|
|
163
|
+
Agent-enabled first boot and reload also activate the signed Runtime's exact
|
|
164
|
+
immutable-Bubblewrap host policy through verified cloud-init. There is no
|
|
165
|
+
public nested-sandbox order field or runtime-install flag, and VPS-only
|
|
166
|
+
cloud-init is unchanged. Existing enrolled hosts use reload/reprovision;
|
|
167
|
+
no silent in-place policy repair is claimed.
|
|
168
|
+
|
|
169
|
+
Sandbox tool profiles:
|
|
170
|
+
Codex is a released automatic tool profile when the public
|
|
171
|
+
/agent-tool-profiles endpoint advertises it as available. WarpMetal installs
|
|
172
|
+
the exact registered artifacts when selected at order time or with tools
|
|
173
|
+
install. Claude Code uses the released claude-code automatic tool profile
|
|
174
|
+
under the same public availability gate. Claude Managed Agents are separate: claude-managed-ant
|
|
175
|
+
is an install-only CLI profile. Installing ant does not authenticate a worker
|
|
176
|
+
and does not activate Managed Agents. Cursor CLI remains manual and unavailable
|
|
177
|
+
as an automatic profile until separately qualified later. Gemini CLI remains manual. Successful
|
|
178
|
+
non-wait installs and status inspections exit 0; exit 8 is reserved for an
|
|
179
|
+
actual --wait deadline timeout.
|
|
180
|
+
|
|
181
|
+
warpmetal agent install installs only the bundled WarpMetal skill; it does not
|
|
182
|
+
install sandbox tools or change a sandbox tool profile.
|
|
183
|
+
|
|
150
184
|
Sandbox SSH aliases:
|
|
151
185
|
Refresh the authenticated connection profile before refreshing an existing
|
|
152
186
|
alias:
|
|
@@ -168,9 +202,16 @@ Sandbox SSH aliases:
|
|
|
168
202
|
ssh -t <alias> gemini
|
|
169
203
|
ssh <alias> gemini -p "<prompt>"
|
|
170
204
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
205
|
+
Authenticate Codex or Claude Code inside the sandbox after the selected
|
|
206
|
+
profile is ready. The public /agent-tool-profiles response is authoritative
|
|
207
|
+
for availability and checkout selection. The Claude Code automatic profile
|
|
208
|
+
ID is claude-code. Claude Managed Agents are separate:
|
|
209
|
+
claude-managed-ant is an install-only CLI profile. Installing ant does not
|
|
210
|
+
authenticate a worker and does not activate Managed Agents. Cursor CLI remains
|
|
211
|
+
manual and unavailable as an automatic profile until separately qualified later.
|
|
212
|
+
Install and authenticate Cursor CLI or Gemini CLI inside the sandbox.
|
|
213
|
+
WarpMetal never receives provider credentials; authentication remains inside
|
|
214
|
+
the sandbox.
|
|
174
215
|
|
|
175
216
|
Codex Desktop discovers the concrete alias through ~/.ssh/config and opens
|
|
176
217
|
the sandbox login shell, so Codex must be available on the login-shell PATH.
|
|
@@ -370,6 +411,17 @@ async function requireReloadToken(store, serverId, options, env) {
|
|
|
370
411
|
return token;
|
|
371
412
|
}
|
|
372
413
|
|
|
414
|
+
async function requireStoredOwnerToken(store, serverId) {
|
|
415
|
+
const token = (await store.server(serverId))?.ownerToken;
|
|
416
|
+
if (!token) {
|
|
417
|
+
throw new CliError(
|
|
418
|
+
`No stored owner credential is available for ${serverId}. Restore the private state file created for that server.`,
|
|
419
|
+
{ exitCode: 4 },
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
return token;
|
|
423
|
+
}
|
|
424
|
+
|
|
373
425
|
function safePreparedOrder(data, stateFile) {
|
|
374
426
|
return {
|
|
375
427
|
task: data.task,
|
|
@@ -432,6 +484,94 @@ async function pollRuntime(client, serverId, token, timeoutSeconds) {
|
|
|
432
484
|
}
|
|
433
485
|
}
|
|
434
486
|
|
|
487
|
+
function toolSetupOutcome(result) {
|
|
488
|
+
const operations = result.data?.setupOperations || [];
|
|
489
|
+
if (operations.some((operation) => ["failed", "cancelled"].includes(operation.state))) {
|
|
490
|
+
return 5;
|
|
491
|
+
}
|
|
492
|
+
return 0;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function publicToolSetupOperation(operation) {
|
|
496
|
+
if (!operation || typeof operation !== "object" || Array.isArray(operation)) {
|
|
497
|
+
return {};
|
|
498
|
+
}
|
|
499
|
+
return Object.fromEntries(
|
|
500
|
+
[
|
|
501
|
+
"id",
|
|
502
|
+
"sandboxId",
|
|
503
|
+
"sandboxGeneration",
|
|
504
|
+
"profileId",
|
|
505
|
+
"profileRevision",
|
|
506
|
+
"profileDigest",
|
|
507
|
+
"state",
|
|
508
|
+
"receiptDigest",
|
|
509
|
+
"errorCode",
|
|
510
|
+
]
|
|
511
|
+
.filter((field) => Object.hasOwn(operation, field))
|
|
512
|
+
.map((field) => [field, operation[field]]),
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function publicToolSetupData(data) {
|
|
517
|
+
if (Array.isArray(data?.setupOperations)) {
|
|
518
|
+
return {
|
|
519
|
+
setupOperations: data.setupOperations.map(publicToolSetupOperation),
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
return { setupOperation: publicToolSetupOperation(data?.setupOperation) };
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function publicAgentToolProfiles(data) {
|
|
526
|
+
return {
|
|
527
|
+
profiles: Array.isArray(data?.profiles)
|
|
528
|
+
? data.profiles.map((profile) =>
|
|
529
|
+
profile && typeof profile === "object" && !Array.isArray(profile)
|
|
530
|
+
? Object.fromEntries(
|
|
531
|
+
[
|
|
532
|
+
"id",
|
|
533
|
+
"displayName",
|
|
534
|
+
"revision",
|
|
535
|
+
"digest",
|
|
536
|
+
"platform",
|
|
537
|
+
"mode",
|
|
538
|
+
"availability",
|
|
539
|
+
"checkoutSelectable",
|
|
540
|
+
]
|
|
541
|
+
.filter((field) => Object.hasOwn(profile, field))
|
|
542
|
+
.map((field) => [field, profile[field]]),
|
|
543
|
+
)
|
|
544
|
+
: {},
|
|
545
|
+
)
|
|
546
|
+
: [],
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
async function pollToolSetup(client, serverId, token, timeoutSeconds, operationId) {
|
|
551
|
+
const deadline = timeoutDeadline(timeoutSeconds);
|
|
552
|
+
while (true) {
|
|
553
|
+
const result = await client.getAgentSetup(serverId, token);
|
|
554
|
+
const operations = result.data?.setupOperations || [];
|
|
555
|
+
const selected = operationId
|
|
556
|
+
? operations.filter((operation) => operation.id === operationId)
|
|
557
|
+
: operations;
|
|
558
|
+
const terminal = selected.every((operation) =>
|
|
559
|
+
TOOL_SETUP_TERMINAL_STATES.has(operation.state),
|
|
560
|
+
);
|
|
561
|
+
if ((operationId ? selected.length > 0 : true) && terminal) {
|
|
562
|
+
return operationId
|
|
563
|
+
? { ...result, data: { setupOperation: selected[0] } }
|
|
564
|
+
: result;
|
|
565
|
+
}
|
|
566
|
+
ensureBeforeDeadline(deadline, `Tool setup for ${serverId}`);
|
|
567
|
+
const seconds = suggestedDelay(result);
|
|
568
|
+
const remainingMs = deadline - Date.now();
|
|
569
|
+
await new Promise((resolveDelay) =>
|
|
570
|
+
setTimeout(resolveDelay, Math.min(seconds * 1_000, Math.max(0, remainingMs))),
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
435
575
|
async function pollSandbox(
|
|
436
576
|
client,
|
|
437
577
|
serverId,
|
|
@@ -771,9 +911,12 @@ async function handlePrepareOrder(client, store, options, context) {
|
|
|
771
911
|
}
|
|
772
912
|
const email = stringOption(options, "email");
|
|
773
913
|
const runtimeFile = stringOption(options, "runtime-file");
|
|
774
|
-
const
|
|
914
|
+
const runtimeIntent = runtimeFile
|
|
775
915
|
? await readSandboxFile(runtimeFile)
|
|
776
916
|
: undefined;
|
|
917
|
+
const sandboxes = Array.isArray(runtimeIntent)
|
|
918
|
+
? runtimeIntent
|
|
919
|
+
: runtimeIntent?.sandboxes;
|
|
777
920
|
if (sandboxes) {
|
|
778
921
|
requireTemporaryConfirmation(sandboxes, stringOption(options, "confirm"));
|
|
779
922
|
}
|
|
@@ -819,7 +962,11 @@ async function handlePrepareOrder(client, store, options, context) {
|
|
|
819
962
|
sshKeyLabel: selected.identity.keyName,
|
|
820
963
|
};
|
|
821
964
|
if (email) request.email = email;
|
|
822
|
-
if (
|
|
965
|
+
if (runtimeIntent) {
|
|
966
|
+
request.agentRuntime = Array.isArray(runtimeIntent)
|
|
967
|
+
? { sandboxes: runtimeIntent }
|
|
968
|
+
: runtimeIntent;
|
|
969
|
+
}
|
|
823
970
|
const key =
|
|
824
971
|
stringOption(options, "idempotency-key") || idempotencyKey("order");
|
|
825
972
|
const response = await client.prepareOrder(request, key);
|
|
@@ -830,7 +977,7 @@ async function handlePrepareOrder(client, store, options, context) {
|
|
|
830
977
|
selected.identity.identityId,
|
|
831
978
|
);
|
|
832
979
|
const safe = safePreparedOrder(response.data, store.path);
|
|
833
|
-
if (
|
|
980
|
+
if (runtimeIntent) {
|
|
834
981
|
safe.agentRuntimeIntent = {
|
|
835
982
|
sandboxes: sandboxes.map((sandbox) => ({
|
|
836
983
|
name: sandbox.name,
|
|
@@ -839,6 +986,9 @@ async function handlePrepareOrder(client, store, options, context) {
|
|
|
839
986
|
expiresInSeconds: sandbox.expiresInSeconds,
|
|
840
987
|
})),
|
|
841
988
|
};
|
|
989
|
+
if (!Array.isArray(runtimeIntent)) {
|
|
990
|
+
safe.agentRuntimeIntent.setup = runtimeIntent.setup;
|
|
991
|
+
}
|
|
842
992
|
}
|
|
843
993
|
emit(
|
|
844
994
|
context.stdout,
|
|
@@ -1995,7 +2145,7 @@ async function applyReloadResult(
|
|
|
1995
2145
|
await store.invalidateServerAccess(serverId);
|
|
1996
2146
|
if (operation.result?.reloadImpact?.agentRuntimeAffected) {
|
|
1997
2147
|
await store.saveRuntime(serverId, {
|
|
1998
|
-
state: "
|
|
2148
|
+
state: "pending_install",
|
|
1999
2149
|
desiredRevision: undefined,
|
|
2000
2150
|
appliedRevision: 0,
|
|
2001
2151
|
lastSeenAt: null,
|
|
@@ -2103,8 +2253,8 @@ async function handleServerReload(client, store, options, context) {
|
|
|
2103
2253
|
(operation?.state === "succeeded" &&
|
|
2104
2254
|
operation.result?.reloadImpact?.ownerKnownHostsNeedRefresh
|
|
2105
2255
|
? operation.result?.reloadImpact?.agentRuntimeAffected
|
|
2106
|
-
? "
|
|
2107
|
-
: "
|
|
2256
|
+
? " Agent Runtime setup is automatic. Wait for Agent Runtime to become ready, then refresh every sandbox connection profile. Verify the replacement host key before owner SSH."
|
|
2257
|
+
: " Verify the replacement host key before owner SSH; the successful reload recorded a new operation-bound trust epoch."
|
|
2108
2258
|
: operation?.state === "succeeded"
|
|
2109
2259
|
? " WarpMetal did not report a host-key refresh; the existing managed pin remains required."
|
|
2110
2260
|
: ""),
|
|
@@ -2753,7 +2903,7 @@ async function handleAccessInstallSsh(options, context) {
|
|
|
2753
2903
|
alias: stringOption(options, "alias", { required: true }),
|
|
2754
2904
|
connectionFile: stringOption(options, "connection-file", { required: true }),
|
|
2755
2905
|
identity: stringOption(options, "identity", { required: true }),
|
|
2756
|
-
homeDirectory: context.env.HOME,
|
|
2906
|
+
homeDirectory: context.env.HOME || context.env.USERPROFILE || homedir(),
|
|
2757
2907
|
confirm: stringOption(options, "confirm"),
|
|
2758
2908
|
});
|
|
2759
2909
|
emit(
|
|
@@ -2768,7 +2918,7 @@ async function handleAccessInstallSsh(options, context) {
|
|
|
2768
2918
|
async function handleAccessRemoveSsh(options, context) {
|
|
2769
2919
|
const result = await removeSshAlias({
|
|
2770
2920
|
alias: stringOption(options, "alias", { required: true }),
|
|
2771
|
-
homeDirectory: context.env.HOME,
|
|
2921
|
+
homeDirectory: context.env.HOME || context.env.USERPROFILE || homedir(),
|
|
2772
2922
|
confirm: stringOption(options, "confirm", { required: true }),
|
|
2773
2923
|
});
|
|
2774
2924
|
emit(
|
|
@@ -2780,6 +2930,82 @@ async function handleAccessRemoveSsh(options, context) {
|
|
|
2780
2930
|
return 0;
|
|
2781
2931
|
}
|
|
2782
2932
|
|
|
2933
|
+
async function handleToolsList(client, store, options, context) {
|
|
2934
|
+
const serverId = stringOption(options, "server", { required: true });
|
|
2935
|
+
const token = await requireStoredOwnerToken(store, serverId);
|
|
2936
|
+
const result = await client.listAgentToolProfiles(token);
|
|
2937
|
+
const output = publicAgentToolProfiles(result.data);
|
|
2938
|
+
emit(
|
|
2939
|
+
context.stdout,
|
|
2940
|
+
output,
|
|
2941
|
+
context.json,
|
|
2942
|
+
output.profiles
|
|
2943
|
+
.map((profile) => `${profile.id} revision ${profile.revision}`)
|
|
2944
|
+
.join("\n"),
|
|
2945
|
+
);
|
|
2946
|
+
return 0;
|
|
2947
|
+
}
|
|
2948
|
+
|
|
2949
|
+
async function handleToolsInstall(client, store, options, context) {
|
|
2950
|
+
const serverId = stringOption(options, "server", { required: true });
|
|
2951
|
+
const sandboxId = stringOption(options, "sandbox", { required: true });
|
|
2952
|
+
const profileId = stringOption(options, "profile", { required: true });
|
|
2953
|
+
const token = await requireStoredOwnerToken(store, serverId);
|
|
2954
|
+
const key =
|
|
2955
|
+
stringOption(options, "idempotency-key") || idempotencyKey("tools-install");
|
|
2956
|
+
let result = await client.installAgentToolProfile(
|
|
2957
|
+
serverId,
|
|
2958
|
+
sandboxId,
|
|
2959
|
+
profileId,
|
|
2960
|
+
token,
|
|
2961
|
+
key,
|
|
2962
|
+
);
|
|
2963
|
+
const wait = booleanOption(options, "wait");
|
|
2964
|
+
if (wait) {
|
|
2965
|
+
result = await pollToolSetup(
|
|
2966
|
+
client,
|
|
2967
|
+
serverId,
|
|
2968
|
+
token,
|
|
2969
|
+
integerOption(options, "timeout-seconds", 900),
|
|
2970
|
+
result.data?.setupOperation?.id,
|
|
2971
|
+
);
|
|
2972
|
+
}
|
|
2973
|
+
const output = publicToolSetupData(result.data);
|
|
2974
|
+
const operation = output.setupOperation;
|
|
2975
|
+
emit(
|
|
2976
|
+
context.stdout,
|
|
2977
|
+
output,
|
|
2978
|
+
context.json,
|
|
2979
|
+
`Tool setup ${operation?.id || "requested"}: ${operation?.state || "pending"}.`,
|
|
2980
|
+
);
|
|
2981
|
+
if (["failed", "cancelled"].includes(operation?.state)) return 5;
|
|
2982
|
+
if (!wait) return 0;
|
|
2983
|
+
return operation?.state === "ready" ? 0 : 5;
|
|
2984
|
+
}
|
|
2985
|
+
|
|
2986
|
+
async function handleToolsStatus(client, store, options, context) {
|
|
2987
|
+
const serverId = stringOption(options, "server", { required: true });
|
|
2988
|
+
const token = await requireStoredOwnerToken(store, serverId);
|
|
2989
|
+
const result = booleanOption(options, "wait")
|
|
2990
|
+
? await pollToolSetup(
|
|
2991
|
+
client,
|
|
2992
|
+
serverId,
|
|
2993
|
+
token,
|
|
2994
|
+
integerOption(options, "timeout-seconds", 900),
|
|
2995
|
+
)
|
|
2996
|
+
: await client.getAgentSetup(serverId, token);
|
|
2997
|
+
const output = publicToolSetupData(result.data);
|
|
2998
|
+
emit(
|
|
2999
|
+
context.stdout,
|
|
3000
|
+
output,
|
|
3001
|
+
context.json,
|
|
3002
|
+
output.setupOperations
|
|
3003
|
+
.map((operation) => `${operation.id}: ${operation.state}`)
|
|
3004
|
+
.join("\n"),
|
|
3005
|
+
);
|
|
3006
|
+
return toolSetupOutcome({ ...result, data: output });
|
|
3007
|
+
}
|
|
3008
|
+
|
|
2783
3009
|
async function dispatch(positionals, options, passthrough, context) {
|
|
2784
3010
|
const command = positionals.join(" ");
|
|
2785
3011
|
if (passthrough.length > 0 && command !== "sandbox connect") {
|
|
@@ -3233,6 +3459,28 @@ async function dispatch(positionals, options, passthrough, context) {
|
|
|
3233
3459
|
case "sandbox connect":
|
|
3234
3460
|
rejectUnknownOptions(options, ["connection-file", "identity", "help"]);
|
|
3235
3461
|
return handleSandboxConnect(options, passthrough, context);
|
|
3462
|
+
case "tools list":
|
|
3463
|
+
rejectUnknownOptions(options, [...COMMON_OPTIONS, "server"]);
|
|
3464
|
+
return handleToolsList(client, store, options, context);
|
|
3465
|
+
case "tools install":
|
|
3466
|
+
rejectUnknownOptions(options, [
|
|
3467
|
+
...COMMON_OPTIONS,
|
|
3468
|
+
"server",
|
|
3469
|
+
"sandbox",
|
|
3470
|
+
"profile",
|
|
3471
|
+
"idempotency-key",
|
|
3472
|
+
"wait",
|
|
3473
|
+
"timeout-seconds",
|
|
3474
|
+
]);
|
|
3475
|
+
return handleToolsInstall(client, store, options, context);
|
|
3476
|
+
case "tools status":
|
|
3477
|
+
rejectUnknownOptions(options, [
|
|
3478
|
+
...COMMON_OPTIONS,
|
|
3479
|
+
"server",
|
|
3480
|
+
"wait",
|
|
3481
|
+
"timeout-seconds",
|
|
3482
|
+
]);
|
|
3483
|
+
return handleToolsStatus(client, store, options, context);
|
|
3236
3484
|
case "state list": {
|
|
3237
3485
|
rejectUnknownOptions(options, COMMON_OPTIONS);
|
|
3238
3486
|
const summary = await store.summary();
|
package/src/runtime.js
CHANGED
|
@@ -15,6 +15,9 @@ const SANDBOX_FIELDS = new Set([
|
|
|
15
15
|
"lifetime",
|
|
16
16
|
"expiresInSeconds",
|
|
17
17
|
]);
|
|
18
|
+
const RUNTIME_FIELDS = new Set(["sandboxes", "setup"]);
|
|
19
|
+
const SETUP_FIELDS = new Set(["version", "sandboxProfiles"]);
|
|
20
|
+
const SANDBOX_PROFILE_FIELDS = new Set(["sandboxName", "profileId"]);
|
|
18
21
|
|
|
19
22
|
function exactFields(value, allowed, label) {
|
|
20
23
|
const unknown = Object.keys(value).find((field) => !allowed.has(field));
|
|
@@ -113,8 +116,62 @@ export async function readSandboxFile(path) {
|
|
|
113
116
|
exitCode: 2,
|
|
114
117
|
});
|
|
115
118
|
}
|
|
116
|
-
exactFields(document,
|
|
117
|
-
|
|
119
|
+
exactFields(document, RUNTIME_FIELDS, "The sandbox JSON file");
|
|
120
|
+
const sandboxes = validateSandboxBatch(document.sandboxes);
|
|
121
|
+
if (document.setup === undefined) return sandboxes;
|
|
122
|
+
const setup = document.setup;
|
|
123
|
+
if (!setup || typeof setup !== "object" || Array.isArray(setup)) {
|
|
124
|
+
throw new CliError("setup must be a JSON object.", { exitCode: 2 });
|
|
125
|
+
}
|
|
126
|
+
exactFields(setup, SETUP_FIELDS, "setup");
|
|
127
|
+
if (setup.version !== 1) {
|
|
128
|
+
throw new CliError("setup.version must be 1.", { exitCode: 2 });
|
|
129
|
+
}
|
|
130
|
+
if (
|
|
131
|
+
!Array.isArray(setup.sandboxProfiles) ||
|
|
132
|
+
setup.sandboxProfiles.length < 1 ||
|
|
133
|
+
setup.sandboxProfiles.length > 32
|
|
134
|
+
) {
|
|
135
|
+
throw new CliError(
|
|
136
|
+
"setup.sandboxProfiles must contain between 1 and 32 entries.",
|
|
137
|
+
{ exitCode: 2 },
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
const sandboxNames = new Set(sandboxes.map((sandbox) => sandbox.name));
|
|
141
|
+
const selectedNames = new Set();
|
|
142
|
+
const sandboxProfiles = setup.sandboxProfiles.map((input) => {
|
|
143
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
144
|
+
throw new CliError("Every setup profile selection must be a JSON object.", {
|
|
145
|
+
exitCode: 2,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
exactFields(input, SANDBOX_PROFILE_FIELDS, "A setup profile selection");
|
|
149
|
+
if (typeof input.sandboxName !== "string" || !NAME.test(input.sandboxName)) {
|
|
150
|
+
throw new CliError("setup sandbox names must use lowercase identifiers.", {
|
|
151
|
+
exitCode: 2,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
if (typeof input.profileId !== "string" || !NAME.test(input.profileId)) {
|
|
155
|
+
throw new CliError("setup profile IDs must use lowercase identifiers.", {
|
|
156
|
+
exitCode: 2,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
if (!sandboxNames.has(input.sandboxName)) {
|
|
160
|
+
throw new CliError(
|
|
161
|
+
`setup references an unknown sandbox: ${input.sandboxName}`,
|
|
162
|
+
{ exitCode: 2 },
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
if (selectedNames.has(input.sandboxName)) {
|
|
166
|
+
throw new CliError(
|
|
167
|
+
`setup selects the sandbox more than once: ${input.sandboxName}`,
|
|
168
|
+
{ exitCode: 2 },
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
selectedNames.add(input.sandboxName);
|
|
172
|
+
return { sandboxName: input.sandboxName, profileId: input.profileId };
|
|
173
|
+
});
|
|
174
|
+
return { sandboxes, setup: { version: 1, sandboxProfiles } };
|
|
118
175
|
}
|
|
119
176
|
|
|
120
177
|
export function containsTemporary(sandboxes) {
|
package/src/ssh-alias.js
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
rmdir,
|
|
11
11
|
unlink,
|
|
12
12
|
} from "node:fs/promises";
|
|
13
|
-
import { isAbsolute, join, parse, resolve } from "node:path";
|
|
13
|
+
import { isAbsolute, join, parse, posix, resolve, win32 } from "node:path";
|
|
14
14
|
import { randomUUID } from "node:crypto";
|
|
15
15
|
|
|
16
16
|
import {
|
|
@@ -21,7 +21,8 @@ import {
|
|
|
21
21
|
import { CliError } from "./errors.js";
|
|
22
22
|
|
|
23
23
|
const ALIAS_PATTERN = /^(?=.{1,63}$)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
24
|
-
const
|
|
24
|
+
const POSIX_UNSAFE_PATH = /[\0\r\n\t%"'`$!&;<>|*?()[\]{}\\#]/;
|
|
25
|
+
const WINDOWS_UNSAFE_PATH = /[\0\r\n\t%"'`$!&;<>|*?()[\]{}#]/;
|
|
25
26
|
const MANAGED_HEADER = "# Managed by WarpMetal. Do not edit.";
|
|
26
27
|
|
|
27
28
|
function invalid(message) {
|
|
@@ -37,17 +38,18 @@ export function validateSshAlias(value) {
|
|
|
37
38
|
return value;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
function validateAbsolutePath(value, label) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
!isAbsolute(value) ||
|
|
44
|
-
resolve(value) !== value ||
|
|
45
|
-
value === parse(value).root ||
|
|
46
|
-
UNSAFE_PATH.test(value)
|
|
47
|
-
) {
|
|
41
|
+
export function validateAbsolutePath(value, label, platform = process.platform) {
|
|
42
|
+
const path = platform === "win32" ? win32 : posix;
|
|
43
|
+
if (typeof value !== "string" || !path.isAbsolute(value)) {
|
|
48
44
|
invalid(`${label} must be an absolute path without unsafe interpolation characters.`);
|
|
49
45
|
}
|
|
50
|
-
|
|
46
|
+
const resolved = path.resolve(value);
|
|
47
|
+
const unsafePath =
|
|
48
|
+
platform === "win32" ? WINDOWS_UNSAFE_PATH : POSIX_UNSAFE_PATH;
|
|
49
|
+
if (resolved === path.parse(resolved).root || unsafePath.test(resolved)) {
|
|
50
|
+
invalid(`${label} must be an absolute path without unsafe interpolation characters.`);
|
|
51
|
+
}
|
|
52
|
+
return resolved;
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
function requireOwner(metadata, label) {
|
|
@@ -420,12 +422,19 @@ async function syncDirectory(path) {
|
|
|
420
422
|
handle = await open(path, "r");
|
|
421
423
|
await handle.sync();
|
|
422
424
|
} catch (error) {
|
|
423
|
-
if (!
|
|
425
|
+
if (!isIgnorableDirectorySyncError(error)) throw error;
|
|
424
426
|
} finally {
|
|
425
427
|
await handle?.close().catch(() => {});
|
|
426
428
|
}
|
|
427
429
|
}
|
|
428
430
|
|
|
431
|
+
export function isIgnorableDirectorySyncError(error, platform = process.platform) {
|
|
432
|
+
return (
|
|
433
|
+
["EINVAL", "ENOTSUP", "EISDIR"].includes(error?.code) ||
|
|
434
|
+
(platform === "win32" && ["EPERM", "EACCES"].includes(error?.code))
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
429
438
|
async function commitStaged(staged) {
|
|
430
439
|
try {
|
|
431
440
|
for (const item of staged) {
|