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 CHANGED
@@ -367,42 +367,89 @@ warpmetal sandbox access keygen \
367
367
  --json
368
368
  ```
369
369
 
370
- On the first install in a server trust epoch, JSON output includes
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
- WarpMetal CLI v0.8.7 with Agent Runtime v0.1.25 or newer can enable the narrowly
378
- scoped AppArmor exception required by a verified workload that creates an inner
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 runtime install \
383
- --server <serverId> \
384
- --ssh-user root \
385
- --confirm INSTALL \
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
- This is an explicit host-scoped opt-in for nested-Bubblewrap hosts, not a
392
- requirement for ordinary VPS or Runtime workloads that use only the outer
393
- sandbox. The default `preserve` action leaves the current policy state
394
- unchanged. Use `disable` during an approved maintenance window to unload
395
- WarpMetal's policy and restore the pre-install file and loaded-policy state.
396
- Because Runtime sandboxes share one Unix owner, treat an enabled policy as
397
- available to every sandbox on that Runtime host whose process matches the
398
- signed, root-owned bwrap path; it is not a per-sandbox permission.
399
-
400
- Common inner-boundary designs are planning with an exact read-only checkout,
401
- coding with only one approved checkout and output directory writable, and QA
402
- with an exact candidate plus isolated test processes and scratch space. The
403
- boundary protects a persistent trusted runner from repository-controlled
404
- commands and sibling attempts. GitHub access, installing Codex or another AI
405
- CLI, and delegating to subagents do not by themselves require this capability.
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.9",
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 [--nested-private-procfs <preserve|enable|disable>] [--wait]
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",