warpmetal 0.8.9 → 0.8.10

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,79 @@ 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
+ ```
390
+
391
+ Install and authenticate Codex, Claude Code, or Cursor CLI inside the
392
+ sandbox first. Provider credentials are sandbox-owned and remain in its
393
+ persistent home; WarpMetal does not install these tools, perform their login,
394
+ or receive their credentials.
395
+
396
+ The alias is local-only. It prepends a managed include to `~/.ssh/config` and
397
+ uses the profile's pinned host key plus the sandbox identity. It never uses or
398
+ exposes the VPS owner management key. The server-side forced gateway maps that
399
+ identity to only its assigned sandbox, cannot open a host shell, and retains
400
+ `ClearAllForwardings yes` together with agent and X11 forwarding denial.
401
+
402
+ An exact reinstall is safe and unchanged. If an authenticated access refresh
403
+ produces a new profile, refresh the profile first and then explicitly replace
404
+ the local alias:
405
+
406
+ ```sh
407
+ warpmetal sandbox access refresh \
408
+ --server <serverId> --sandbox <sandboxId> --grant <grantId> \
409
+ --connection-file <profile-path> --confirm REFRESH --wait --json
410
+ warpmetal sandbox access install-ssh \
411
+ --connection-file <profile-path> \
412
+ --identity <sandbox-private-key-path> \
413
+ --alias <alias> --confirm REFRESH --json
414
+ ```
415
+
416
+ Remove only that managed alias and pin with explicit confirmation; unrelated
417
+ SSH configuration is preserved:
418
+
419
+ ```sh
420
+ warpmetal sandbox access remove-ssh --alias <alias> --confirm REMOVE --json
389
421
  ```
390
422
 
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.
423
+ [Codex Desktop's remote-connections contract](https://learn.chatgpt.com/docs/remote-connections)
424
+ discovers concrete aliases from `~/.ssh/config`, requires `ssh <alias>` to
425
+ work, and starts the remote app server through the login shell. Install Codex
426
+ inside the sandbox and ensure Codex is on the login-shell `PATH` before choosing
427
+ the alias in Codex Desktop.
428
+
429
+ The tested Cursor Remote SSH path is incompatible with this boundary because
430
+ it requests dynamic forwarding, which WarpMetal deliberately denies. Do not
431
+ weaken sandbox forwarding controls to make the IDE connect. Use the supported
432
+ [Cursor CLI](https://cursor.com/docs/cli/overview) interactively with
433
+ `ssh -t <alias> agent` or in
434
+ [headless mode](https://cursor.com/docs/cli/headless) with
435
+ `ssh <alias> agent -p '<task>'` instead.
436
+
437
+ On the first install in a server trust epoch, JSON output includes
438
+ `hostKeyTrust.state: "trusted_first_use"` and the safe Ed25519 fingerprint.
439
+ Later installs report `"matched"`. First-use trust protects continuity after
440
+ that observation, but it cannot detect an active attacker on the first
441
+ connection. Provider-console host-key pre-enrollment remains an optional
442
+ higher-assurance alternative when it is available.
406
443
 
407
444
  Installation gives pre-existing Docker containers exact liveness checks and
408
445
  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.10",
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,38 @@ 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. After an authenticated profile refresh, update the alias
326
+ only with the explicit second confirmation:
327
+
328
+ ```sh
329
+ warpmetal sandbox access refresh \
330
+ --server <serverId> --sandbox <sandboxId> --grant <grantId> \
331
+ --connection-file <profile-path> --confirm REFRESH --wait --json
332
+ warpmetal sandbox access install-ssh \
333
+ --connection-file <profile-path> \
334
+ --identity <sandbox-private-key-path> \
335
+ --alias <alias> --confirm REFRESH --json
336
+ ```
337
+
338
+ Remove it only with
339
+ `warpmetal sandbox access remove-ssh --alias <alias> --confirm REMOVE --json`.
340
+ Do not edit the generated fragment or host pin manually.
341
+
320
342
  If the installed CLI lacks a required runtime command, stop, explain the
321
343
  version limitation, and ask before upgrading the official npm package. Do not
322
344
  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,37 @@ 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
+ ```
304
+
305
+ [Codex Desktop](https://learn.chatgpt.com/docs/remote-connections) reads
306
+ the concrete alias from `~/.ssh/config` and starts Codex through the sandbox
307
+ login shell, so Codex must be installed and on that login-shell `PATH`. The
308
+ tested Cursor Remote SSH route requests prohibited dynamic forwarding and is
309
+ not compatible with this boundary. Keep forwarding denied and use the
310
+ [Cursor CLI](https://cursor.com/docs/cli/overview) interactive or
311
+ [headless](https://cursor.com/docs/cli/headless) commands shown above.
312
+
288
313
  ## Skill installation and state
289
314
 
290
315
  ```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,93 @@ 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
+ ```
298
+
299
+ Provider authentication and credentials are sandbox-owned and persist only in
300
+ the sandbox home. WarpMetal does not install, authenticate, configure, or
301
+ receive credentials for Codex, Claude Code, or Cursor CLI.
302
+
303
+ [Codex Desktop](https://learn.chatgpt.com/docs/remote-connections)
304
+ discovers a concrete alias through `~/.ssh/config`, requires ordinary
305
+ `ssh <alias>` connectivity, and launches the remote app server through the
306
+ login shell. Codex must therefore be installed inside the sandbox and on the
307
+ login-shell `PATH` before selecting the alias in Codex Desktop.
308
+
309
+ The tested Cursor Remote SSH path requests dynamic forwarding, which the
310
+ Runtime correctly denies, so Cursor IDE remote access is not supported by this
311
+ restricted alias. Do not relax forwarding controls. Use the official
312
+ [Cursor CLI](https://cursor.com/docs/cli/overview) interactively with
313
+ `ssh -t <alias> agent` or in
314
+ [headless mode](https://cursor.com/docs/cli/headless) with
315
+ `ssh <alias> agent -p '<task>'` instead.
316
+
265
317
  Connect without an owner management credential:
266
318
 
267
319
  ```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,36 @@ 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
+
169
+ Codex Desktop discovers the concrete alias through ~/.ssh/config and opens
170
+ the sandbox login shell, so Codex must be available on the login-shell PATH.
171
+ Sandbox aliases never use or expose the VPS owner management key, cannot open
172
+ a host shell, and do not relax isolation: ClearAllForwardings yes keeps all
173
+ forwarding disabled.
174
+
175
+ Compatibility references:
176
+ https://learn.chatgpt.com/docs/remote-connections
177
+ https://cursor.com/docs/cli/overview
178
+ https://cursor.com/docs/cli/headless
179
+
156
180
  Credential environment variables:
157
181
  WARPMETAL_OWNER_TOKEN Recovery/bootstrap credential for one explicit command
158
182
  WARPMETAL_ACCESS_TOKEN Short-lived SSH-derived credential for one explicit command
@@ -2164,14 +2188,6 @@ async function handleRuntimeInstall(client, store, options, context) {
2164
2188
  stringOption(options, "identity"),
2165
2189
  );
2166
2190
  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
2191
  if (stringOption(options, "confirm", { required: true }) !== "INSTALL") {
2176
2192
  throw new CliError(
2177
2193
  "Confirm supervisor installation with --confirm INSTALL.",
@@ -2215,7 +2231,6 @@ async function handleRuntimeInstall(client, store, options, context) {
2215
2231
  knownHostsFile: hostKeyTrust.knownHostsFile,
2216
2232
  trustedPublicIp: hostKeyTrust.publicIp,
2217
2233
  bootstrap: bootstrap.data,
2218
- nestedPrivateProcfs,
2219
2234
  fetchImpl: context.fetchImpl,
2220
2235
  spawnImpl: context.spawnImpl,
2221
2236
  });
@@ -2720,6 +2735,38 @@ async function handleSandboxConnect(options, passthrough, context) {
2720
2735
  );
2721
2736
  }
2722
2737
 
2738
+ async function handleAccessInstallSsh(options, context) {
2739
+ const result = await installSshAlias({
2740
+ alias: stringOption(options, "alias", { required: true }),
2741
+ connectionFile: stringOption(options, "connection-file", { required: true }),
2742
+ identity: stringOption(options, "identity", { required: true }),
2743
+ homeDirectory: context.env.HOME,
2744
+ confirm: stringOption(options, "confirm"),
2745
+ });
2746
+ emit(
2747
+ context.stdout,
2748
+ result,
2749
+ context.json,
2750
+ `SSH alias ${result.alias}: ${result.operation}.`,
2751
+ );
2752
+ return 0;
2753
+ }
2754
+
2755
+ async function handleAccessRemoveSsh(options, context) {
2756
+ const result = await removeSshAlias({
2757
+ alias: stringOption(options, "alias", { required: true }),
2758
+ homeDirectory: context.env.HOME,
2759
+ confirm: stringOption(options, "confirm", { required: true }),
2760
+ });
2761
+ emit(
2762
+ context.stdout,
2763
+ result,
2764
+ context.json,
2765
+ `SSH alias ${result.alias}: ${result.operation}.`,
2766
+ );
2767
+ return 0;
2768
+ }
2769
+
2723
2770
  async function dispatch(positionals, options, passthrough, context) {
2724
2771
  const command = positionals.join(" ");
2725
2772
  if (passthrough.length > 0 && command !== "sandbox connect") {
@@ -2766,6 +2813,21 @@ async function dispatch(positionals, options, passthrough, context) {
2766
2813
  );
2767
2814
  return 0;
2768
2815
  }
2816
+ if (command === "sandbox access install-ssh") {
2817
+ rejectUnknownOptions(options, [
2818
+ "json",
2819
+ "help",
2820
+ "connection-file",
2821
+ "identity",
2822
+ "alias",
2823
+ "confirm",
2824
+ ]);
2825
+ return handleAccessInstallSsh(options, context);
2826
+ }
2827
+ if (command === "sandbox access remove-ssh") {
2828
+ rejectUnknownOptions(options, ["json", "help", "alias", "confirm"]);
2829
+ return handleAccessRemoveSsh(options, context);
2830
+ }
2769
2831
 
2770
2832
  const baseUrl = stringOption(options, "base-url");
2771
2833
  const stateDir =
@@ -3030,7 +3092,6 @@ async function dispatch(positionals, options, passthrough, context) {
3030
3092
  "identity",
3031
3093
  "ssh-user",
3032
3094
  "confirm",
3033
- "nested-private-procfs",
3034
3095
  "idempotency-key",
3035
3096
  "wait",
3036
3097
  "timeout-seconds",