warpmetal 0.8.9 → 0.8.11
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 +77 -30
- package/package.json +2 -2
- package/skills/warpmetal/SKILL.md +34 -10
- package/skills/warpmetal/references/cli-reference.md +48 -10
- package/skills/warpmetal/references/runtime.md +98 -35
- package/src/cli.js +95 -21
- package/src/installer.js +5 -90
- package/src/ssh-alias.js +726 -0
package/README.md
CHANGED
|
@@ -367,42 +367,89 @@ warpmetal sandbox access keygen \
|
|
|
367
367
|
--json
|
|
368
368
|
```
|
|
369
369
|
|
|
370
|
-
|
|
371
|
-
`hostKeyTrust.state: "trusted_first_use"` and the safe Ed25519 fingerprint.
|
|
372
|
-
Later installs report `"matched"`. First-use trust protects continuity after
|
|
373
|
-
that observation, but it cannot detect an active attacker on the first
|
|
374
|
-
connection. Provider-console host-key pre-enrollment remains an optional
|
|
375
|
-
higher-assurance alternative when it is available.
|
|
370
|
+
### Install a standard sandbox SSH alias
|
|
376
371
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
Bubblewrap PID namespace and private `/proc`:
|
|
372
|
+
Use a separate keypair and access grant for each sandbox. After the grant is
|
|
373
|
+
`applied`, install its token-free profile as a concrete OpenSSH alias:
|
|
380
374
|
|
|
381
375
|
```sh
|
|
382
|
-
warpmetal
|
|
383
|
-
--
|
|
384
|
-
--
|
|
385
|
-
--
|
|
386
|
-
--nested-private-procfs enable \
|
|
387
|
-
--wait \
|
|
376
|
+
warpmetal sandbox access install-ssh \
|
|
377
|
+
--connection-file <profile-path> \
|
|
378
|
+
--identity <sandbox-private-key-path> \
|
|
379
|
+
--alias <alias> \
|
|
388
380
|
--json
|
|
381
|
+
|
|
382
|
+
ssh <alias>
|
|
383
|
+
ssh -t <alias> codex
|
|
384
|
+
ssh <alias> codex exec '<task>'
|
|
385
|
+
ssh -t <alias> claude
|
|
386
|
+
ssh <alias> claude -p '<task>'
|
|
387
|
+
ssh -t <alias> agent
|
|
388
|
+
ssh <alias> agent -p '<task>'
|
|
389
|
+
ssh -t <alias> gemini
|
|
390
|
+
ssh <alias> gemini -p '<task>'
|
|
389
391
|
```
|
|
390
392
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
393
|
+
Install and authenticate Codex, Claude Code, Cursor CLI, or Gemini CLI inside
|
|
394
|
+
the sandbox first. Provider credentials are sandbox-owned and remain in its
|
|
395
|
+
persistent home; WarpMetal does not install these tools, perform their login,
|
|
396
|
+
or receive their credentials.
|
|
397
|
+
|
|
398
|
+
The alias is local-only. It prepends a managed include to `~/.ssh/config` and
|
|
399
|
+
uses the profile's pinned host key plus the sandbox identity. It never uses or
|
|
400
|
+
exposes the VPS owner management key. The server-side forced gateway maps that
|
|
401
|
+
identity to only its assigned sandbox, cannot open a host shell, and retains
|
|
402
|
+
`ClearAllForwardings yes` together with agent and X11 forwarding denial.
|
|
403
|
+
|
|
404
|
+
An exact reinstall is safe and unchanged. If an authenticated access refresh
|
|
405
|
+
produces a new profile, refresh the profile first and then explicitly replace
|
|
406
|
+
the local alias:
|
|
407
|
+
|
|
408
|
+
```sh
|
|
409
|
+
warpmetal sandbox access refresh \
|
|
410
|
+
--server <serverId> --sandbox <sandboxId> --grant <grantId> \
|
|
411
|
+
--connection-file <profile-path> --confirm REFRESH --wait --json
|
|
412
|
+
warpmetal sandbox access install-ssh \
|
|
413
|
+
--connection-file <profile-path> \
|
|
414
|
+
--identity <sandbox-private-key-path> \
|
|
415
|
+
--alias <alias> --confirm REFRESH --json
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
Remove only that managed alias and pin with explicit confirmation; unrelated
|
|
419
|
+
SSH configuration is preserved:
|
|
420
|
+
|
|
421
|
+
```sh
|
|
422
|
+
warpmetal sandbox access remove-ssh --alias <alias> --confirm REMOVE --json
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
[Codex Desktop's remote-connections contract](https://learn.chatgpt.com/docs/remote-connections)
|
|
426
|
+
discovers concrete aliases from `~/.ssh/config`, requires `ssh <alias>` to
|
|
427
|
+
work, and starts the remote app server through the login shell. Install Codex
|
|
428
|
+
inside the sandbox and ensure Codex is on the login-shell `PATH` before choosing
|
|
429
|
+
the alias in Codex Desktop.
|
|
430
|
+
|
|
431
|
+
The tested Cursor Remote SSH path is incompatible with this boundary because
|
|
432
|
+
it requests dynamic forwarding, which WarpMetal deliberately denies. Do not
|
|
433
|
+
weaken sandbox forwarding controls to make the IDE connect. Use the supported
|
|
434
|
+
[Cursor CLI](https://cursor.com/docs/cli/overview) interactively with
|
|
435
|
+
`ssh -t <alias> agent` or in
|
|
436
|
+
[headless mode](https://cursor.com/docs/cli/headless) with
|
|
437
|
+
`ssh <alias> agent -p '<task>'` instead.
|
|
438
|
+
|
|
439
|
+
Use the official Gemini CLI [installation guide](https://geminicli.com/docs/get-started/installation/)
|
|
440
|
+
before running `ssh -t <alias> gemini`, or use its documented
|
|
441
|
+
[headless mode](https://geminicli.com/docs/cli/headless/) with
|
|
442
|
+
`ssh <alias> gemini -p '<task>'`. Gemini's optional Docker or Podman sandbox is
|
|
443
|
+
normally unavailable inside the WarpMetal sandbox because no host
|
|
444
|
+
container-engine socket is exposed; run Gemini directly inside the existing
|
|
445
|
+
outer sandbox and choose its approvals yourself.
|
|
446
|
+
|
|
447
|
+
On the first install in a server trust epoch, JSON output includes
|
|
448
|
+
`hostKeyTrust.state: "trusted_first_use"` and the safe Ed25519 fingerprint.
|
|
449
|
+
Later installs report `"matched"`. First-use trust protects continuity after
|
|
450
|
+
that observation, but it cannot detect an active attacker on the first
|
|
451
|
+
connection. Provider-console host-key pre-enrollment remains an optional
|
|
452
|
+
higher-assurance alternative when it is available.
|
|
406
453
|
|
|
407
454
|
Installation gives pre-existing Docker containers exact liveness checks and
|
|
408
455
|
tracks common container-runtime processes without collecting application
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "warpmetal",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.11",
|
|
4
4
|
"description": "Agent-safe CLI and skill for purchasing, renewing, and managing WarpMetal VPS servers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"node": ">=20.0.0"
|
|
17
17
|
},
|
|
18
18
|
"scripts": {
|
|
19
|
-
"check": "node --check bin/warpmetal.js && node --check src/api.js && node --check src/args.js && node --check src/cli.js && node --check src/connection.js && node --check src/errors.js && node --check src/host-trust.js && node --check src/install-skill.js && node --check src/installer.js && node --check src/payment.js && node --check src/runtime.js && node --check src/ssh.js && node --check src/state.js && node --check src/version.js",
|
|
19
|
+
"check": "node --check bin/warpmetal.js && node --check src/api.js && node --check src/args.js && node --check src/cli.js && node --check src/connection.js && node --check src/errors.js && node --check src/host-trust.js && node --check src/install-skill.js && node --check src/installer.js && node --check src/payment.js && node --check src/runtime.js && node --check src/ssh-alias.js && node --check src/ssh.js && node --check src/state.js && node --check src/version.js",
|
|
20
20
|
"plugin:check": "node --test test/plugin.test.js",
|
|
21
21
|
"test": "node --test",
|
|
22
22
|
"prepack": "npm run check && npm test"
|
|
@@ -296,16 +296,6 @@ must match. Explain that TOFU cannot detect an active attacker on the first
|
|
|
296
296
|
connection; never use `ssh-keyscan`, accept a mismatch, or expose a generic pin
|
|
297
297
|
reset.
|
|
298
298
|
|
|
299
|
-
For any verified workload that creates an inner Bubblewrap PID namespace and
|
|
300
|
-
private `/proc`, CLI 0.8.7 with Runtime 0.1.25 or newer may add
|
|
301
|
-
`--nested-private-procfs enable` to the approved Runtime install. Planning with
|
|
302
|
-
a read-only checkout, coding in one writable checkout, and independent QA are
|
|
303
|
-
common uses. GitHub access, an AI CLI, or subagent delegation alone does not
|
|
304
|
-
require it. Omission means `preserve`, which leaves policy state unchanged;
|
|
305
|
-
`disable` is an explicit maintenance action. The exception is host-scoped
|
|
306
|
-
rather than sandbox-scoped, so separate workloads onto different VPS hosts when
|
|
307
|
-
they must not share it.
|
|
308
|
-
|
|
309
299
|
Omitted lifetime means persistent. A temporary sandbox requires
|
|
310
300
|
`--confirm TEMPORARY`, expires 15 minutes to 24 hours after first reaching
|
|
311
301
|
running, and permanently deletes its workspace at expiry. Never describe a
|
|
@@ -317,6 +307,40 @@ pinned host keys, then connect only through `warpmetal sandbox connect`.
|
|
|
317
307
|
Never give an agent the owner host key, owner token, SSH-derived management
|
|
318
308
|
token, runtime bootstrap, or node token.
|
|
319
309
|
|
|
310
|
+
CLI 0.8.10 or newer can turn the already-applied token-free profile into a
|
|
311
|
+
standard concrete OpenSSH alias. Read [references/runtime.md](references/runtime.md)
|
|
312
|
+
before installing one. Use a separate keypair and grant for each sandbox, and
|
|
313
|
+
pass the sandbox identity—not the VPS owner management key:
|
|
314
|
+
|
|
315
|
+
```sh
|
|
316
|
+
warpmetal sandbox access install-ssh \
|
|
317
|
+
--connection-file <profile-path> \
|
|
318
|
+
--identity <sandbox-private-key-path> \
|
|
319
|
+
--alias <alias> --json
|
|
320
|
+
ssh <alias>
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
The local alias preserves the forced gateway: it cannot open a host shell and
|
|
324
|
+
does not relax forwarding denial. Provider authentication and credentials stay
|
|
325
|
+
inside the sandbox. Install and authenticate Codex, Claude Code, Cursor CLI, or
|
|
326
|
+
Gemini CLI inside the sandbox; use the entry points and compatibility guidance
|
|
327
|
+
in [references/runtime.md](references/runtime.md). After an authenticated
|
|
328
|
+
profile refresh, update the alias only with the explicit second confirmation:
|
|
329
|
+
|
|
330
|
+
```sh
|
|
331
|
+
warpmetal sandbox access refresh \
|
|
332
|
+
--server <serverId> --sandbox <sandboxId> --grant <grantId> \
|
|
333
|
+
--connection-file <profile-path> --confirm REFRESH --wait --json
|
|
334
|
+
warpmetal sandbox access install-ssh \
|
|
335
|
+
--connection-file <profile-path> \
|
|
336
|
+
--identity <sandbox-private-key-path> \
|
|
337
|
+
--alias <alias> --confirm REFRESH --json
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
Remove it only with
|
|
341
|
+
`warpmetal sandbox access remove-ssh --alias <alias> --confirm REMOVE --json`.
|
|
342
|
+
Do not edit the generated fragment or host pin manually.
|
|
343
|
+
|
|
320
344
|
If the installed CLI lacks a required runtime command, stop, explain the
|
|
321
345
|
version limitation, and ask before upgrading the official npm package. Do not
|
|
322
346
|
reconstruct runtime changes with raw HTTP, ad hoc SSH, Podman, Docker, or host
|
|
@@ -216,7 +216,6 @@ warpmetal runtime get --server <serverId> [--wait] [--timeout-seconds <n>] --jso
|
|
|
216
216
|
warpmetal runtime install \
|
|
217
217
|
--server <serverId> [--identity <owner-key>] --ssh-user root \
|
|
218
218
|
--confirm INSTALL \
|
|
219
|
-
[--nested-private-procfs <preserve|enable|disable>] \
|
|
220
219
|
[--wait] [--timeout-seconds <n>] --json
|
|
221
220
|
|
|
222
221
|
warpmetal sandbox create \
|
|
@@ -245,15 +244,6 @@ SCP operation is strict; changed keys, malformed pins, and failed or ambiguous
|
|
|
245
244
|
reloads never replace trust. This TOFU step cannot detect an active attacker on
|
|
246
245
|
the first connection. Provider-console pre-enrollment is optional and stronger.
|
|
247
246
|
|
|
248
|
-
The nested-private-procfs action requires CLI 0.8.7 and Runtime 0.1.25 or
|
|
249
|
-
newer. It defaults to `preserve`. `enable` is a host-level opt-in for any
|
|
250
|
-
verified workload that creates an inner Bubblewrap PID namespace and private
|
|
251
|
-
`/proc`; read-only planning, single-workspace coding, and independent QA are
|
|
252
|
-
common examples. GitHub use, an AI CLI, or subagent delegation alone does not
|
|
253
|
-
require it. `disable` unloads WarpMetal's policy and restores the recorded
|
|
254
|
-
pre-enable state. It is not a per-sandbox capability because Runtime sandboxes
|
|
255
|
-
share one Unix owner.
|
|
256
|
-
|
|
257
247
|
See [runtime.md](runtime.md) for capacity, lifetime, cleanup, polling, and
|
|
258
248
|
installation safety. Exit 8 means accepted or pending, never applied.
|
|
259
249
|
|
|
@@ -271,6 +261,10 @@ warpmetal sandbox access get \
|
|
|
271
261
|
warpmetal sandbox access refresh \
|
|
272
262
|
--server <serverId> --sandbox <sandboxId> --grant <grantId> \
|
|
273
263
|
--connection-file <profile-path> --confirm REFRESH [--wait] --json
|
|
264
|
+
warpmetal sandbox access install-ssh \
|
|
265
|
+
--connection-file <profile-path> --identity <sandbox-private-key-path> \
|
|
266
|
+
--alias <alias> [--confirm REFRESH] --json
|
|
267
|
+
warpmetal sandbox access remove-ssh --alias <alias> --confirm REMOVE --json
|
|
274
268
|
warpmetal sandbox access revoke \
|
|
275
269
|
--server <serverId> --sandbox <sandboxId> --grant <grantId> \
|
|
276
270
|
--confirm REVOKE [--wait] --json
|
|
@@ -285,6 +279,50 @@ creation requires `--wait`.
|
|
|
285
279
|
the currently applied grant and API-reported pinned host keys; use it after an
|
|
286
280
|
OS reload and supervisor reinstall.
|
|
287
281
|
|
|
282
|
+
`sandbox access install-ssh` is local-only and requires CLI 0.8.10 or newer.
|
|
283
|
+
It turns the reviewed profile and sandbox-private identity into a concrete
|
|
284
|
+
alias in `~/.ssh/config`. Exact replay is unchanged. If the profile, endpoint,
|
|
285
|
+
pin, or identity changes, first run the authenticated `sandbox access refresh`
|
|
286
|
+
command above, then repeat `sandbox access install-ssh ... --confirm REFRESH`.
|
|
287
|
+
Removal requires exact `--confirm REMOVE` and preserves unrelated SSH config.
|
|
288
|
+
|
|
289
|
+
Use a separate keypair and grant for each sandbox. The alias never uses or
|
|
290
|
+
exposes the VPS owner management key; the forced gateway cannot open a host
|
|
291
|
+
shell, and forwarding remains disabled with `ClearAllForwardings yes`.
|
|
292
|
+
Authentication for user-installed tools happens inside the sandbox. Common
|
|
293
|
+
interactive and one-shot entry points are:
|
|
294
|
+
|
|
295
|
+
```sh
|
|
296
|
+
ssh <alias>
|
|
297
|
+
ssh -t <alias> codex
|
|
298
|
+
ssh <alias> codex exec '<task>'
|
|
299
|
+
ssh -t <alias> claude
|
|
300
|
+
ssh <alias> claude -p '<task>'
|
|
301
|
+
ssh -t <alias> agent
|
|
302
|
+
ssh <alias> agent -p '<task>'
|
|
303
|
+
ssh -t <alias> gemini
|
|
304
|
+
ssh <alias> gemini -p '<task>'
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Install and authenticate Codex, Claude Code, Cursor CLI, or Gemini CLI inside
|
|
308
|
+
the sandbox first. WarpMetal does not install, authenticate, configure, or
|
|
309
|
+
receive credentials for those tools.
|
|
310
|
+
|
|
311
|
+
[Codex Desktop](https://learn.chatgpt.com/docs/remote-connections) reads
|
|
312
|
+
the concrete alias from `~/.ssh/config` and starts Codex through the sandbox
|
|
313
|
+
login shell, so Codex must be installed and on that login-shell `PATH`. The
|
|
314
|
+
tested Cursor Remote SSH route requests prohibited dynamic forwarding and is
|
|
315
|
+
not compatible with this boundary. Keep forwarding denied and use the
|
|
316
|
+
[Cursor CLI](https://cursor.com/docs/cli/overview) interactive or
|
|
317
|
+
[headless](https://cursor.com/docs/cli/headless) commands shown above.
|
|
318
|
+
|
|
319
|
+
Install Gemini CLI from its official [installation guide](https://geminicli.com/docs/get-started/installation/)
|
|
320
|
+
and use its documented [headless mode](https://geminicli.com/docs/cli/headless/)
|
|
321
|
+
for `gemini -p`. Gemini's optional Docker or Podman sandbox is normally
|
|
322
|
+
unavailable inside the WarpMetal sandbox because no host container-engine
|
|
323
|
+
socket is exposed; run Gemini directly inside the existing outer sandbox and
|
|
324
|
+
choose its approvals yourself.
|
|
325
|
+
|
|
288
326
|
## Skill installation and state
|
|
289
327
|
|
|
290
328
|
```sh
|
|
@@ -58,7 +58,6 @@ 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>] \
|
|
62
61
|
--wait \
|
|
63
62
|
--json
|
|
64
63
|
warpmetal runtime get --server <serverId> --wait --json
|
|
@@ -81,29 +80,6 @@ is an optional higher-assurance alternative, not a requirement. Only a locally
|
|
|
81
80
|
recorded successful reload that reports an owner-host-key refresh creates one
|
|
82
81
|
new operation-bound trust epoch; failed or ambiguous reloads do not.
|
|
83
82
|
|
|
84
|
-
`--nested-private-procfs` is supported by `warpmetal` CLI 0.8.7 with Agent
|
|
85
|
-
Runtime 0.1.25 or newer. Its default is `preserve`, which makes no AppArmor
|
|
86
|
-
policy change. Select `enable` only when this VPS is intentionally dedicated to
|
|
87
|
-
a verified workload that creates an inner Bubblewrap PID namespace and private
|
|
88
|
-
procfs. Ordinary VPS users and Runtime workloads that rely only on the outer
|
|
89
|
-
sandbox do not need it. Select `disable` only during an approved maintenance
|
|
90
|
-
action to unload WarpMetal's policy and restore the exact file and loaded-policy
|
|
91
|
-
state that existed before enablement.
|
|
92
|
-
|
|
93
|
-
Examples include a planner with an exact read-only checkout, a coder with only
|
|
94
|
-
one approved checkout and output directory writable, and QA with an exact
|
|
95
|
-
candidate plus isolated test processes and scratch space. This protects a
|
|
96
|
-
persistent trusted runner from repository-controlled commands and sibling
|
|
97
|
-
attempts. GitHub access, installing an AI CLI, and subagent delegation alone do
|
|
98
|
-
not require nested private procfs.
|
|
99
|
-
|
|
100
|
-
This setting is host-scoped, not sandbox-scoped. Runtime sandboxes share one
|
|
101
|
-
Unix owner, so every sandbox on the host can use the exception only through the
|
|
102
|
-
signed, root-owned, fixed bwrap path matched by the policy. Enabling it does not
|
|
103
|
-
authorize arbitrary bwrap binaries or change the trust boundary for the
|
|
104
|
-
Runtime image selected by WarpMetal's authenticated backend. Isolate workloads
|
|
105
|
-
on separate VPS hosts if they must not share this host capability.
|
|
106
|
-
|
|
107
83
|
The signed installer is designed to preserve container workloads already
|
|
108
84
|
running on a supported host. It selects `crun` for WarpMetal's private rootless
|
|
109
85
|
Podman service, refuses APT removals and DNF erasures, protects installed
|
|
@@ -128,17 +104,6 @@ host automatically:
|
|
|
128
104
|
unexpectedly; stop and review the host package logs;
|
|
129
105
|
- `runtime_legacy_migration_required`: preview Podman state needs a separate,
|
|
130
106
|
explicitly reviewed migration and was not reset.
|
|
131
|
-
- `runtime_nested_private_procfs_architecture_unsupported`: the requested
|
|
132
|
-
coding-host policy is unsupported by this CPU architecture;
|
|
133
|
-
- `runtime_apparmor_state_unverifiable`: the host's current AppArmor state
|
|
134
|
-
cannot be established safely;
|
|
135
|
-
- `runtime_apparmor_policy_conflict`: an existing file or loaded-policy state
|
|
136
|
-
conflicts with WarpMetal's recorded transaction;
|
|
137
|
-
- `runtime_apparmor_policy_rollback_failed`: the installer could not restore
|
|
138
|
-
the exact prior AppArmor state; stop and perform operator recovery.
|
|
139
|
-
- `runtime_apparmor_policy_recovery_failed`: an interrupted prior policy
|
|
140
|
-
transaction could not be recovered; stop and perform operator recovery.
|
|
141
|
-
|
|
142
107
|
There is no force bypass. If `runtime_reboot_required` is returned, schedule
|
|
143
108
|
the reboot as a separate maintenance action and retry only after the host and
|
|
144
109
|
its existing workloads are healthy. The ordinary installer never installs a
|
|
@@ -262,6 +227,104 @@ The sandbox record is retained, but its old workspace is not; reconciliation
|
|
|
262
227
|
creates a new empty workspace. Never bypass a host-key mismatch or reuse the
|
|
263
228
|
pre-reload profile.
|
|
264
229
|
|
|
230
|
+
### Install a concrete OpenSSH alias
|
|
231
|
+
|
|
232
|
+
CLI 0.8.10 or newer can install the reviewed profile as a standard local alias
|
|
233
|
+
without another API request:
|
|
234
|
+
|
|
235
|
+
```sh
|
|
236
|
+
warpmetal sandbox access install-ssh \
|
|
237
|
+
--connection-file <profile-path> \
|
|
238
|
+
--identity <sandbox-private-key-path> \
|
|
239
|
+
--alias <alias> \
|
|
240
|
+
--json
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Use a separate keypair and grant for each sandbox. The identity must be the
|
|
244
|
+
private half of that sandbox grant, never the VPS owner management key. The
|
|
245
|
+
command validates the closed token-free profile and its fingerprints, then
|
|
246
|
+
prepends a managed include to `~/.ssh/config`. It writes a private alias
|
|
247
|
+
fragment and dedicated pinned known-hosts file. Exact reinstall is idempotent.
|
|
248
|
+
|
|
249
|
+
If an authenticated access refresh changes the profile, endpoint, host pin, or
|
|
250
|
+
identity, run the server-backed profile refresh first and then explicitly
|
|
251
|
+
replace the local alias:
|
|
252
|
+
|
|
253
|
+
```sh
|
|
254
|
+
warpmetal sandbox access refresh \
|
|
255
|
+
--server <serverId> \
|
|
256
|
+
--sandbox <sandboxId> \
|
|
257
|
+
--grant <grantId> \
|
|
258
|
+
--connection-file <profile-path> \
|
|
259
|
+
--confirm REFRESH \
|
|
260
|
+
--wait \
|
|
261
|
+
--json
|
|
262
|
+
warpmetal sandbox access install-ssh \
|
|
263
|
+
--connection-file <profile-path> \
|
|
264
|
+
--identity <sandbox-private-key-path> \
|
|
265
|
+
--alias <alias> \
|
|
266
|
+
--confirm REFRESH \
|
|
267
|
+
--json
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Never replace a pin merely because an SSH connection reports a mismatch. The
|
|
271
|
+
alias installer consumes only the already-refreshed API profile and does not
|
|
272
|
+
scan or trust a network key. Remove one alias without changing unrelated SSH
|
|
273
|
+
configuration:
|
|
274
|
+
|
|
275
|
+
```sh
|
|
276
|
+
warpmetal sandbox access remove-ssh --alias <alias> --confirm REMOVE --json
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
The generated host block pins the exact host keys, selects only the sandbox
|
|
280
|
+
identity, disables password and keyboard-interactive authentication, retains
|
|
281
|
+
`ClearAllForwardings yes`, disables agent/X11 forwarding and local commands,
|
|
282
|
+
and contains neither `RemoteCommand` nor `RequestTTY`. The existing server-side
|
|
283
|
+
forced gateway still maps the key to exactly one sandbox. The alias cannot open
|
|
284
|
+
a host shell and never uses or exposes the owner management key.
|
|
285
|
+
|
|
286
|
+
After installing and authenticating each provider tool inside the sandbox, use
|
|
287
|
+
the alias for interactive or one-shot work:
|
|
288
|
+
|
|
289
|
+
```sh
|
|
290
|
+
ssh <alias>
|
|
291
|
+
ssh -t <alias> codex
|
|
292
|
+
ssh <alias> codex exec '<task>'
|
|
293
|
+
ssh -t <alias> claude
|
|
294
|
+
ssh <alias> claude -p '<task>'
|
|
295
|
+
ssh -t <alias> agent
|
|
296
|
+
ssh <alias> agent -p '<task>'
|
|
297
|
+
ssh -t <alias> gemini
|
|
298
|
+
ssh <alias> gemini -p '<task>'
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Install and authenticate Codex, Claude Code, Cursor CLI, or Gemini CLI inside
|
|
302
|
+
the sandbox first. Provider authentication and credentials are sandbox-owned
|
|
303
|
+
and persist only in the sandbox home. WarpMetal does not install, authenticate,
|
|
304
|
+
configure, or receive credentials for those tools.
|
|
305
|
+
|
|
306
|
+
[Codex Desktop](https://learn.chatgpt.com/docs/remote-connections)
|
|
307
|
+
discovers a concrete alias through `~/.ssh/config`, requires ordinary
|
|
308
|
+
`ssh <alias>` connectivity, and launches the remote app server through the
|
|
309
|
+
login shell. Codex must therefore be installed inside the sandbox and on the
|
|
310
|
+
login-shell `PATH` before selecting the alias in Codex Desktop.
|
|
311
|
+
|
|
312
|
+
The tested Cursor Remote SSH path requests dynamic forwarding, which the
|
|
313
|
+
Runtime correctly denies, so Cursor IDE remote access is not supported by this
|
|
314
|
+
restricted alias. Do not relax forwarding controls. Use the official
|
|
315
|
+
[Cursor CLI](https://cursor.com/docs/cli/overview) interactively with
|
|
316
|
+
`ssh -t <alias> agent` or in
|
|
317
|
+
[headless mode](https://cursor.com/docs/cli/headless) with
|
|
318
|
+
`ssh <alias> agent -p '<task>'` instead.
|
|
319
|
+
|
|
320
|
+
Follow Gemini CLI's official [installation guide](https://geminicli.com/docs/get-started/installation/),
|
|
321
|
+
then use `ssh -t <alias> gemini` interactively or its documented
|
|
322
|
+
[headless mode](https://geminicli.com/docs/cli/headless/) with
|
|
323
|
+
`ssh <alias> gemini -p '<task>'`. Gemini's optional Docker or Podman sandbox is
|
|
324
|
+
normally unavailable inside the WarpMetal sandbox because no host
|
|
325
|
+
container-engine socket is exposed; run Gemini directly inside the existing
|
|
326
|
+
outer sandbox and choose its approvals yourself.
|
|
327
|
+
|
|
265
328
|
Connect without an owner management credential:
|
|
266
329
|
|
|
267
330
|
```sh
|
package/src/cli.js
CHANGED
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
signSshChallenge,
|
|
38
38
|
sshFingerprint,
|
|
39
39
|
} from "./ssh.js";
|
|
40
|
+
import { installSshAlias, removeSshAlias } from "./ssh-alias.js";
|
|
40
41
|
import { resolveStateDirectory, StateStore } from "./state.js";
|
|
41
42
|
import { VERSION } from "./version.js";
|
|
42
43
|
|
|
@@ -111,7 +112,7 @@ Usage:
|
|
|
111
112
|
warpmetal operation get --operation <operationId> [--server <serverId>] [--wait]
|
|
112
113
|
warpmetal runtime enable|get --server <serverId> [--wait]
|
|
113
114
|
warpmetal runtime install --server <serverId> [--identity <owner-key>] --ssh-user <user>
|
|
114
|
-
--confirm INSTALL [--
|
|
115
|
+
--confirm INSTALL [--wait]
|
|
115
116
|
warpmetal sandbox create --server <serverId> --name <name> --size <size>
|
|
116
117
|
[--lifetime temporary] [--expires-in-seconds <n>] [--confirm TEMPORARY] [--wait]
|
|
117
118
|
warpmetal sandbox create --server <serverId> --file <batch.json> [--confirm TEMPORARY]
|
|
@@ -122,6 +123,9 @@ Usage:
|
|
|
122
123
|
warpmetal sandbox access grant|list|get|revoke ...
|
|
123
124
|
warpmetal sandbox access refresh --server <serverId> --sandbox <sandboxId>
|
|
124
125
|
--grant <grantId> --connection-file <path> --confirm REFRESH [--wait]
|
|
126
|
+
warpmetal sandbox access install-ssh --connection-file <profile> --identity <sandbox-private-key-path>
|
|
127
|
+
--alias <alias> [--confirm REFRESH]
|
|
128
|
+
warpmetal sandbox access remove-ssh --alias <alias> --confirm REMOVE
|
|
125
129
|
warpmetal sandbox connect --connection-file <path> --identity <sandbox-key> [-- <command>]
|
|
126
130
|
warpmetal state list
|
|
127
131
|
warpmetal agent install --target <codex|claude|all> [--scope <user|project>] [--force]
|
|
@@ -133,16 +137,6 @@ Global options:
|
|
|
133
137
|
--help Show help
|
|
134
138
|
--version Show the CLI version
|
|
135
139
|
|
|
136
|
-
Nested private procfs (CLI 0.8.7+, Runtime 0.1.25+):
|
|
137
|
-
preserve Default; do not inspect or change AppArmor policy state
|
|
138
|
-
enable Enable the exact-path Bubblewrap policy on an amd64 host
|
|
139
|
-
disable Remove it and restore the recorded pre-enable policy state
|
|
140
|
-
|
|
141
|
-
Use enable once per dedicated Runtime host when a verified workload creates an
|
|
142
|
-
inner Bubblewrap PID namespace and private /proc. Planning, coding, and QA are
|
|
143
|
-
common examples. GitHub access, an AI CLI, and subagent delegation alone do not
|
|
144
|
-
require it. The capability is host-scoped, not per-sandbox.
|
|
145
|
-
|
|
146
140
|
SSH host trust (CLI 0.8.8+):
|
|
147
141
|
runtime install authenticates with the exact owner key and trusts the first
|
|
148
142
|
observed Ed25519 host key once for that server trust epoch. It pins the key
|
|
@@ -153,6 +147,49 @@ SSH host trust (CLI 0.8.8+):
|
|
|
153
147
|
active attacker on the first connection. Provider-console pre-enrollment is
|
|
154
148
|
the optional higher-assurance alternative.
|
|
155
149
|
|
|
150
|
+
Sandbox SSH aliases:
|
|
151
|
+
Refresh the authenticated connection profile before refreshing an existing
|
|
152
|
+
alias:
|
|
153
|
+
warpmetal sandbox access refresh --server <serverId> --sandbox <sandboxId> --grant <grantId> --connection-file <profile> --confirm REFRESH
|
|
154
|
+
warpmetal sandbox access install-ssh --connection-file <profile> --identity <sandbox-private-key-path> --alias <alias> --confirm REFRESH
|
|
155
|
+
Remove only the managed alias and host pins explicitly:
|
|
156
|
+
warpmetal sandbox access remove-ssh --alias <alias> --confirm REMOVE
|
|
157
|
+
|
|
158
|
+
Use a separate keypair and grant for each sandbox. Tool and provider
|
|
159
|
+
authentication stays inside the sandbox and uses sandbox-owned credentials.
|
|
160
|
+
After installation, the concrete alias supports interactive and one-shot use:
|
|
161
|
+
ssh <alias>
|
|
162
|
+
ssh -t <alias> codex
|
|
163
|
+
ssh <alias> codex exec "<prompt>"
|
|
164
|
+
ssh -t <alias> claude
|
|
165
|
+
ssh <alias> claude -p "<prompt>"
|
|
166
|
+
ssh -t <alias> agent
|
|
167
|
+
ssh <alias> agent -p "<prompt>"
|
|
168
|
+
ssh -t <alias> gemini
|
|
169
|
+
ssh <alias> gemini -p "<prompt>"
|
|
170
|
+
|
|
171
|
+
Install and authenticate Codex, Claude Code, Cursor CLI, or Gemini CLI inside
|
|
172
|
+
the sandbox first. WarpMetal does not install, authenticate, configure, or
|
|
173
|
+
receive credentials for those tools.
|
|
174
|
+
|
|
175
|
+
Codex Desktop discovers the concrete alias through ~/.ssh/config and opens
|
|
176
|
+
the sandbox login shell, so Codex must be available on the login-shell PATH.
|
|
177
|
+
Sandbox aliases never use or expose the VPS owner management key, cannot open
|
|
178
|
+
a host shell, and do not relax isolation: ClearAllForwardings yes keeps all
|
|
179
|
+
forwarding disabled.
|
|
180
|
+
|
|
181
|
+
Gemini's optional Docker or Podman sandbox is normally unavailable inside
|
|
182
|
+
the WarpMetal sandbox because no host container-engine socket is exposed;
|
|
183
|
+
run Gemini directly inside the existing outer sandbox and choose its
|
|
184
|
+
approvals yourself.
|
|
185
|
+
|
|
186
|
+
Compatibility references:
|
|
187
|
+
https://learn.chatgpt.com/docs/remote-connections
|
|
188
|
+
https://cursor.com/docs/cli/overview
|
|
189
|
+
https://cursor.com/docs/cli/headless
|
|
190
|
+
https://geminicli.com/docs/get-started/installation/
|
|
191
|
+
https://geminicli.com/docs/cli/headless/
|
|
192
|
+
|
|
156
193
|
Credential environment variables:
|
|
157
194
|
WARPMETAL_OWNER_TOKEN Recovery/bootstrap credential for one explicit command
|
|
158
195
|
WARPMETAL_ACCESS_TOKEN Short-lived SSH-derived credential for one explicit command
|
|
@@ -2164,14 +2201,6 @@ async function handleRuntimeInstall(client, store, options, context) {
|
|
|
2164
2201
|
stringOption(options, "identity"),
|
|
2165
2202
|
);
|
|
2166
2203
|
const sshUser = stringOption(options, "ssh-user", { required: true });
|
|
2167
|
-
const nestedPrivateProcfs =
|
|
2168
|
-
stringOption(options, "nested-private-procfs") || "preserve";
|
|
2169
|
-
if (!["preserve", "enable", "disable"].includes(nestedPrivateProcfs)) {
|
|
2170
|
-
throw new CliError(
|
|
2171
|
-
"--nested-private-procfs must be preserve, enable, or disable.",
|
|
2172
|
-
{ exitCode: 2 },
|
|
2173
|
-
);
|
|
2174
|
-
}
|
|
2175
2204
|
if (stringOption(options, "confirm", { required: true }) !== "INSTALL") {
|
|
2176
2205
|
throw new CliError(
|
|
2177
2206
|
"Confirm supervisor installation with --confirm INSTALL.",
|
|
@@ -2215,7 +2244,6 @@ async function handleRuntimeInstall(client, store, options, context) {
|
|
|
2215
2244
|
knownHostsFile: hostKeyTrust.knownHostsFile,
|
|
2216
2245
|
trustedPublicIp: hostKeyTrust.publicIp,
|
|
2217
2246
|
bootstrap: bootstrap.data,
|
|
2218
|
-
nestedPrivateProcfs,
|
|
2219
2247
|
fetchImpl: context.fetchImpl,
|
|
2220
2248
|
spawnImpl: context.spawnImpl,
|
|
2221
2249
|
});
|
|
@@ -2720,6 +2748,38 @@ async function handleSandboxConnect(options, passthrough, context) {
|
|
|
2720
2748
|
);
|
|
2721
2749
|
}
|
|
2722
2750
|
|
|
2751
|
+
async function handleAccessInstallSsh(options, context) {
|
|
2752
|
+
const result = await installSshAlias({
|
|
2753
|
+
alias: stringOption(options, "alias", { required: true }),
|
|
2754
|
+
connectionFile: stringOption(options, "connection-file", { required: true }),
|
|
2755
|
+
identity: stringOption(options, "identity", { required: true }),
|
|
2756
|
+
homeDirectory: context.env.HOME,
|
|
2757
|
+
confirm: stringOption(options, "confirm"),
|
|
2758
|
+
});
|
|
2759
|
+
emit(
|
|
2760
|
+
context.stdout,
|
|
2761
|
+
result,
|
|
2762
|
+
context.json,
|
|
2763
|
+
`SSH alias ${result.alias}: ${result.operation}.`,
|
|
2764
|
+
);
|
|
2765
|
+
return 0;
|
|
2766
|
+
}
|
|
2767
|
+
|
|
2768
|
+
async function handleAccessRemoveSsh(options, context) {
|
|
2769
|
+
const result = await removeSshAlias({
|
|
2770
|
+
alias: stringOption(options, "alias", { required: true }),
|
|
2771
|
+
homeDirectory: context.env.HOME,
|
|
2772
|
+
confirm: stringOption(options, "confirm", { required: true }),
|
|
2773
|
+
});
|
|
2774
|
+
emit(
|
|
2775
|
+
context.stdout,
|
|
2776
|
+
result,
|
|
2777
|
+
context.json,
|
|
2778
|
+
`SSH alias ${result.alias}: ${result.operation}.`,
|
|
2779
|
+
);
|
|
2780
|
+
return 0;
|
|
2781
|
+
}
|
|
2782
|
+
|
|
2723
2783
|
async function dispatch(positionals, options, passthrough, context) {
|
|
2724
2784
|
const command = positionals.join(" ");
|
|
2725
2785
|
if (passthrough.length > 0 && command !== "sandbox connect") {
|
|
@@ -2766,6 +2826,21 @@ async function dispatch(positionals, options, passthrough, context) {
|
|
|
2766
2826
|
);
|
|
2767
2827
|
return 0;
|
|
2768
2828
|
}
|
|
2829
|
+
if (command === "sandbox access install-ssh") {
|
|
2830
|
+
rejectUnknownOptions(options, [
|
|
2831
|
+
"json",
|
|
2832
|
+
"help",
|
|
2833
|
+
"connection-file",
|
|
2834
|
+
"identity",
|
|
2835
|
+
"alias",
|
|
2836
|
+
"confirm",
|
|
2837
|
+
]);
|
|
2838
|
+
return handleAccessInstallSsh(options, context);
|
|
2839
|
+
}
|
|
2840
|
+
if (command === "sandbox access remove-ssh") {
|
|
2841
|
+
rejectUnknownOptions(options, ["json", "help", "alias", "confirm"]);
|
|
2842
|
+
return handleAccessRemoveSsh(options, context);
|
|
2843
|
+
}
|
|
2769
2844
|
|
|
2770
2845
|
const baseUrl = stringOption(options, "base-url");
|
|
2771
2846
|
const stateDir =
|
|
@@ -3030,7 +3105,6 @@ async function dispatch(positionals, options, passthrough, context) {
|
|
|
3030
3105
|
"identity",
|
|
3031
3106
|
"ssh-user",
|
|
3032
3107
|
"confirm",
|
|
3033
|
-
"nested-private-procfs",
|
|
3034
3108
|
"idempotency-key",
|
|
3035
3109
|
"wait",
|
|
3036
3110
|
"timeout-seconds",
|