warpmetal 0.8.12 → 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 +83 -5
- package/package.json +1 -1
- package/skills/warpmetal/SKILL.md +1 -1
- package/skills/warpmetal/references/cli-reference.md +71 -4
- package/skills/warpmetal/references/runtime.md +23 -4
- package/src/api.js +30 -0
- package/src/cli.js +245 -6
- package/src/runtime.js +59 -2
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
|
|
|
@@ -340,6 +344,19 @@ compatible external signer.
|
|
|
340
344
|
and reports an owner-host-key refresh opens one new managed trust epoch;
|
|
341
345
|
failed or ambiguous reloads retain the old pin and never permit replacement.
|
|
342
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.
|
|
343
360
|
|
|
344
361
|
For a Runtime-enabled reload, no separate installation command is part of the
|
|
345
362
|
successful path:
|
|
@@ -387,6 +404,61 @@ warpmetal sandbox access keygen \
|
|
|
387
404
|
--json
|
|
388
405
|
```
|
|
389
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
|
+
|
|
390
462
|
### Install a standard sandbox SSH alias
|
|
391
463
|
|
|
392
464
|
Use a separate keypair and access grant for each sandbox. After the grant is
|
|
@@ -410,10 +482,16 @@ ssh -t <alias> gemini
|
|
|
410
482
|
ssh <alias> gemini -p '<task>'
|
|
411
483
|
```
|
|
412
484
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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.
|
|
417
495
|
|
|
418
496
|
The alias is local-only. It prepends a managed include to `~/.ssh/config` and
|
|
419
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`.
|
|
@@ -221,6 +221,13 @@ process listings.
|
|
|
221
221
|
|
|
222
222
|
## Agent Runtime and sandboxes
|
|
223
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
|
+
|
|
224
231
|
```sh
|
|
225
232
|
warpmetal runtime enable --server <serverId> [--idempotency-key <key>] --json
|
|
226
233
|
warpmetal runtime get --server <serverId> [--wait] [--timeout-seconds <n>] --json
|
|
@@ -243,6 +250,11 @@ warpmetal sandbox action \
|
|
|
243
250
|
[--wait] --json
|
|
244
251
|
warpmetal sandbox delete \
|
|
245
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
|
|
246
258
|
```
|
|
247
259
|
|
|
248
260
|
CLI 0.8.8 manages owner-facing VPS host trust during `runtime install`. With no
|
|
@@ -256,7 +268,52 @@ reloads never replace trust. This TOFU step cannot detect an active attacker on
|
|
|
256
268
|
the first connection. Provider-console pre-enrollment is optional and stronger.
|
|
257
269
|
|
|
258
270
|
See [runtime.md](runtime.md) for capacity, lifetime, cleanup, polling, and
|
|
259
|
-
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.
|
|
260
317
|
|
|
261
318
|
## Per-agent access
|
|
262
319
|
|
|
@@ -315,9 +372,16 @@ ssh -t <alias> gemini
|
|
|
315
372
|
ssh <alias> gemini -p '<task>'
|
|
316
373
|
```
|
|
317
374
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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.
|
|
321
385
|
|
|
322
386
|
[Codex Desktop](https://learn.chatgpt.com/docs/remote-connections) reads
|
|
323
387
|
the concrete alias from `~/.ssh/config` and starts Codex through the sandbox
|
|
@@ -341,6 +405,9 @@ warpmetal agent install --target <codex|claude|all> [--scope user|project]
|
|
|
341
405
|
warpmetal state list --json
|
|
342
406
|
```
|
|
343
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
|
+
|
|
344
411
|
`state list` returns identifiers, public runtime metadata, and
|
|
345
412
|
credential-presence booleans only. Never
|
|
346
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
|
|
@@ -300,10 +314,15 @@ ssh -t <alias> gemini
|
|
|
300
314
|
ssh <alias> gemini -p '<task>'
|
|
301
315
|
```
|
|
302
316
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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.
|
|
307
326
|
|
|
308
327
|
[Codex Desktop](https://learn.chatgpt.com/docs/remote-connections)
|
|
309
328
|
discovers a concrete alias through `~/.ssh/config`, requires ordinary
|
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
|
@@ -68,6 +68,7 @@ const SANDBOX_TERMINAL_STATES = new Set([
|
|
|
68
68
|
"failed",
|
|
69
69
|
]);
|
|
70
70
|
const GRANT_TERMINAL_STATES = new Set(["applied", "revoked", "failed"]);
|
|
71
|
+
const TOOL_SETUP_TERMINAL_STATES = new Set(["ready", "failed", "cancelled"]);
|
|
71
72
|
const COMMON_OPTIONS = ["base-url", "json", "state-dir", "help"];
|
|
72
73
|
|
|
73
74
|
const HELP = `WarpMetal CLI ${VERSION}
|
|
@@ -128,6 +129,10 @@ Usage:
|
|
|
128
129
|
--alias <alias> [--confirm REFRESH]
|
|
129
130
|
warpmetal sandbox access remove-ssh --alias <alias> --confirm REMOVE
|
|
130
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>]
|
|
131
136
|
warpmetal state list
|
|
132
137
|
warpmetal agent install --target <codex|claude|all> [--scope <user|project>] [--force]
|
|
133
138
|
|
|
@@ -155,6 +160,26 @@ Runtime-enabled reloads:
|
|
|
155
160
|
then refresh every sandbox connection profile before reconnecting. The
|
|
156
161
|
successful operation also records a new owner SSH trust epoch; verify the
|
|
157
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.
|
|
158
183
|
|
|
159
184
|
Sandbox SSH aliases:
|
|
160
185
|
Refresh the authenticated connection profile before refreshing an existing
|
|
@@ -177,9 +202,16 @@ Sandbox SSH aliases:
|
|
|
177
202
|
ssh -t <alias> gemini
|
|
178
203
|
ssh <alias> gemini -p "<prompt>"
|
|
179
204
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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.
|
|
183
215
|
|
|
184
216
|
Codex Desktop discovers the concrete alias through ~/.ssh/config and opens
|
|
185
217
|
the sandbox login shell, so Codex must be available on the login-shell PATH.
|
|
@@ -379,6 +411,17 @@ async function requireReloadToken(store, serverId, options, env) {
|
|
|
379
411
|
return token;
|
|
380
412
|
}
|
|
381
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
|
+
|
|
382
425
|
function safePreparedOrder(data, stateFile) {
|
|
383
426
|
return {
|
|
384
427
|
task: data.task,
|
|
@@ -441,6 +484,94 @@ async function pollRuntime(client, serverId, token, timeoutSeconds) {
|
|
|
441
484
|
}
|
|
442
485
|
}
|
|
443
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
|
+
|
|
444
575
|
async function pollSandbox(
|
|
445
576
|
client,
|
|
446
577
|
serverId,
|
|
@@ -780,9 +911,12 @@ async function handlePrepareOrder(client, store, options, context) {
|
|
|
780
911
|
}
|
|
781
912
|
const email = stringOption(options, "email");
|
|
782
913
|
const runtimeFile = stringOption(options, "runtime-file");
|
|
783
|
-
const
|
|
914
|
+
const runtimeIntent = runtimeFile
|
|
784
915
|
? await readSandboxFile(runtimeFile)
|
|
785
916
|
: undefined;
|
|
917
|
+
const sandboxes = Array.isArray(runtimeIntent)
|
|
918
|
+
? runtimeIntent
|
|
919
|
+
: runtimeIntent?.sandboxes;
|
|
786
920
|
if (sandboxes) {
|
|
787
921
|
requireTemporaryConfirmation(sandboxes, stringOption(options, "confirm"));
|
|
788
922
|
}
|
|
@@ -828,7 +962,11 @@ async function handlePrepareOrder(client, store, options, context) {
|
|
|
828
962
|
sshKeyLabel: selected.identity.keyName,
|
|
829
963
|
};
|
|
830
964
|
if (email) request.email = email;
|
|
831
|
-
if (
|
|
965
|
+
if (runtimeIntent) {
|
|
966
|
+
request.agentRuntime = Array.isArray(runtimeIntent)
|
|
967
|
+
? { sandboxes: runtimeIntent }
|
|
968
|
+
: runtimeIntent;
|
|
969
|
+
}
|
|
832
970
|
const key =
|
|
833
971
|
stringOption(options, "idempotency-key") || idempotencyKey("order");
|
|
834
972
|
const response = await client.prepareOrder(request, key);
|
|
@@ -839,7 +977,7 @@ async function handlePrepareOrder(client, store, options, context) {
|
|
|
839
977
|
selected.identity.identityId,
|
|
840
978
|
);
|
|
841
979
|
const safe = safePreparedOrder(response.data, store.path);
|
|
842
|
-
if (
|
|
980
|
+
if (runtimeIntent) {
|
|
843
981
|
safe.agentRuntimeIntent = {
|
|
844
982
|
sandboxes: sandboxes.map((sandbox) => ({
|
|
845
983
|
name: sandbox.name,
|
|
@@ -848,6 +986,9 @@ async function handlePrepareOrder(client, store, options, context) {
|
|
|
848
986
|
expiresInSeconds: sandbox.expiresInSeconds,
|
|
849
987
|
})),
|
|
850
988
|
};
|
|
989
|
+
if (!Array.isArray(runtimeIntent)) {
|
|
990
|
+
safe.agentRuntimeIntent.setup = runtimeIntent.setup;
|
|
991
|
+
}
|
|
851
992
|
}
|
|
852
993
|
emit(
|
|
853
994
|
context.stdout,
|
|
@@ -2789,6 +2930,82 @@ async function handleAccessRemoveSsh(options, context) {
|
|
|
2789
2930
|
return 0;
|
|
2790
2931
|
}
|
|
2791
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
|
+
|
|
2792
3009
|
async function dispatch(positionals, options, passthrough, context) {
|
|
2793
3010
|
const command = positionals.join(" ");
|
|
2794
3011
|
if (passthrough.length > 0 && command !== "sandbox connect") {
|
|
@@ -3242,6 +3459,28 @@ async function dispatch(positionals, options, passthrough, context) {
|
|
|
3242
3459
|
case "sandbox connect":
|
|
3243
3460
|
rejectUnknownOptions(options, ["connection-file", "identity", "help"]);
|
|
3244
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);
|
|
3245
3484
|
case "state list": {
|
|
3246
3485
|
rejectUnknownOptions(options, COMMON_OPTIONS);
|
|
3247
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) {
|