warpmetal 0.8.10 → 0.8.12
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 +40 -10
- package/package.json +1 -1
- package/skills/warpmetal/SKILL.md +20 -13
- package/skills/warpmetal/references/cli-reference.md +26 -2
- package/skills/warpmetal/references/runtime.md +20 -7
- package/skills/warpmetal/references/safety.md +4 -3
- package/src/cli.js +27 -5
- package/src/ssh-alias.js +21 -12
package/README.md
CHANGED
|
@@ -317,9 +317,10 @@ compatible external signer.
|
|
|
317
317
|
Wallet key management and signing remain outside this package.
|
|
318
318
|
- Destructive or state-changing commands require explicit confirmations and
|
|
319
319
|
generate idempotency keys by default.
|
|
320
|
-
- Runtime bootstrap credentials remain memory-only
|
|
321
|
-
the first host key has been pinned and strictly reverified
|
|
322
|
-
|
|
320
|
+
- Runtime bootstrap credentials remain memory-only. Manual installation asks
|
|
321
|
+
for one only after the first host key has been pinned and strictly reverified;
|
|
322
|
+
automatic reload bootstrap is rendered directly into provider-bound
|
|
323
|
+
cloud-init. Signed supervisor bundles are checksum- and signature-verified.
|
|
323
324
|
- Each agent gets a distinct SSH key forced into exactly one sandbox. Token-free
|
|
324
325
|
connection profiles pin the VPS host key and contain no owner credential or
|
|
325
326
|
private-key material.
|
|
@@ -332,11 +333,30 @@ compatible external signer.
|
|
|
332
333
|
external workspace, lifetime, and start time. The wait completes only when
|
|
333
334
|
the observed digest and generation both match the accepted target.
|
|
334
335
|
- Guarded reload powers the server off first. Runtime-enabled reload requires a
|
|
335
|
-
second acknowledgment
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
336
|
+
second acknowledgment because all sandbox workspaces are erased. WarpMetal
|
|
337
|
+
places the approved signed Runtime bootstrap in reload cloud-init
|
|
338
|
+
automatically; wait for Runtime readiness, then refresh pinned sandbox
|
|
339
|
+
connection profiles. Only a locally recorded reload operation that succeeds
|
|
340
|
+
and reports an owner-host-key refresh opens one new managed trust epoch;
|
|
341
|
+
failed or ambiguous reloads retain the old pin and never permit replacement.
|
|
342
|
+
Erased workspaces are never described as recoverable.
|
|
343
|
+
|
|
344
|
+
For a Runtime-enabled reload, no separate installation command is part of the
|
|
345
|
+
successful path:
|
|
346
|
+
|
|
347
|
+
```sh
|
|
348
|
+
warpmetal server reload \
|
|
349
|
+
--server <serverId> --confirm ERASE --power-off-first \
|
|
350
|
+
--acknowledge-agent-runtime-reset --wait --json
|
|
351
|
+
warpmetal runtime get --server <serverId> --wait --json
|
|
352
|
+
warpmetal sandbox access refresh \
|
|
353
|
+
--server <serverId> --sandbox <sandboxId> --grant <grantId> \
|
|
354
|
+
--connection-file <profile-path> --confirm REFRESH --wait --json
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
The successful reload records a new operation-bound owner SSH trust epoch.
|
|
358
|
+
Verify the replacement host key before using owner SSH; sandbox access remains
|
|
359
|
+
strictly pinned through each refreshed connection profile.
|
|
340
360
|
|
|
341
361
|
## Agent Runtime example
|
|
342
362
|
|
|
@@ -386,10 +406,12 @@ ssh -t <alias> claude
|
|
|
386
406
|
ssh <alias> claude -p '<task>'
|
|
387
407
|
ssh -t <alias> agent
|
|
388
408
|
ssh <alias> agent -p '<task>'
|
|
409
|
+
ssh -t <alias> gemini
|
|
410
|
+
ssh <alias> gemini -p '<task>'
|
|
389
411
|
```
|
|
390
412
|
|
|
391
|
-
Install and authenticate Codex, Claude Code, or
|
|
392
|
-
sandbox first. Provider credentials are sandbox-owned and remain in its
|
|
413
|
+
Install and authenticate Codex, Claude Code, Cursor CLI, or Gemini CLI inside
|
|
414
|
+
the sandbox first. Provider credentials are sandbox-owned and remain in its
|
|
393
415
|
persistent home; WarpMetal does not install these tools, perform their login,
|
|
394
416
|
or receive their credentials.
|
|
395
417
|
|
|
@@ -434,6 +456,14 @@ weaken sandbox forwarding controls to make the IDE connect. Use the supported
|
|
|
434
456
|
[headless mode](https://cursor.com/docs/cli/headless) with
|
|
435
457
|
`ssh <alias> agent -p '<task>'` instead.
|
|
436
458
|
|
|
459
|
+
Use the official Gemini CLI [installation guide](https://geminicli.com/docs/get-started/installation/)
|
|
460
|
+
before running `ssh -t <alias> gemini`, or use its documented
|
|
461
|
+
[headless mode](https://geminicli.com/docs/cli/headless/) with
|
|
462
|
+
`ssh <alias> gemini -p '<task>'`. Gemini's optional Docker or Podman sandbox is
|
|
463
|
+
normally unavailable inside the WarpMetal sandbox because no host
|
|
464
|
+
container-engine socket is exposed; run Gemini directly inside the existing
|
|
465
|
+
outer sandbox and choose its approvals yourself.
|
|
466
|
+
|
|
437
467
|
On the first install in a server trust epoch, JSON output includes
|
|
438
468
|
`hostKeyTrust.state: "trusted_first_use"` and the safe Ed25519 fingerprint.
|
|
439
469
|
Later installs report `"matched"`. First-use trust protects continuity after
|
package/package.json
CHANGED
|
@@ -254,8 +254,8 @@ warpmetal server power \
|
|
|
254
254
|
For a destructive reload, explain that every server-disk file is erased and
|
|
255
255
|
obtain explicit approval. If Agent Runtime is enabled, also explain that every
|
|
256
256
|
sandbox workspace is lost, desired sandboxes return as empty workspaces after
|
|
257
|
-
|
|
258
|
-
command:
|
|
257
|
+
automatic Runtime setup, and pinned profiles must be refreshed. Then use only
|
|
258
|
+
the guarded command:
|
|
259
259
|
|
|
260
260
|
```sh
|
|
261
261
|
warpmetal server reload \
|
|
@@ -271,14 +271,18 @@ warpmetal server reload \
|
|
|
271
271
|
The CLI requires the recovery owner credential so it can keep polling after
|
|
272
272
|
reload revokes SSH-derived access tokens. CLI 0.8.8 records a new SSH trust
|
|
273
273
|
epoch only when that exact local reload operation succeeds and reports that the
|
|
274
|
-
owner host key needs refresh.
|
|
275
|
-
observed Ed25519 host key once in that epoch, then must reconnect strictly
|
|
276
|
-
before requesting bootstrap. Failed or ambiguous reloads retain the prior pin;
|
|
274
|
+
owner host key needs refresh. Failed or ambiguous reloads retain the prior pin;
|
|
277
275
|
never bypass a mismatch or delete a pin to force a retry. A provider-console
|
|
278
|
-
pre-seed is the optional higher-assurance path.
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
276
|
+
pre-seed is the optional higher-assurance path.
|
|
277
|
+
|
|
278
|
+
After a successful Runtime-enabled reload, WarpMetal performs signed Runtime
|
|
279
|
+
setup automatically. Do not run a separate installation command. Wait with
|
|
280
|
+
`warpmetal runtime get --server <serverId> --wait --json`, then wait for grants
|
|
281
|
+
to become `applied` and refresh every connection profile with
|
|
282
|
+
`sandbox access refresh --confirm REFRESH` before connecting. The successful
|
|
283
|
+
operation created a new owner host-trust epoch, so verify the replacement host
|
|
284
|
+
key before owner SSH. Do not fall back to raw API calls for deletion,
|
|
285
|
+
networking, or another unsupported mutation.
|
|
282
286
|
|
|
283
287
|
## Use Agent Runtime
|
|
284
288
|
|
|
@@ -286,12 +290,13 @@ Agent Runtime is optional and shares one owner's VPS only among that owner's
|
|
|
286
290
|
agents. Discover live `agentRuntime` capacity and OS support before choosing
|
|
287
291
|
sizes. Use `--runtime-file` to include sandbox intent in an unpaid order, or
|
|
288
292
|
`warpmetal runtime enable` after the VPS is ready. Supervisor installation is
|
|
289
|
-
separate and requires approval plus `--confirm INSTALL
|
|
293
|
+
separate and requires approval plus `--confirm INSTALL` for initial setup or
|
|
294
|
+
explicit repair; Runtime-enabled OS reloads use automatic signed cloud-init.
|
|
290
295
|
|
|
291
296
|
With CLI 0.8.8, that confirmation also authorizes managed trust on first use
|
|
292
297
|
when the exact server trust epoch has no pin. The CLI performs only an owner-
|
|
293
298
|
key-authenticated `ssh true`, pins the first observed Ed25519 key, immediately
|
|
294
|
-
reconnects strictly
|
|
299
|
+
reconnects strictly before requesting bootstrap. Later connections
|
|
295
300
|
must match. Explain that TOFU cannot detect an active attacker on the first
|
|
296
301
|
connection; never use `ssh-keyscan`, accept a mismatch, or expose a generic pin
|
|
297
302
|
reset.
|
|
@@ -322,8 +327,10 @@ ssh <alias>
|
|
|
322
327
|
|
|
323
328
|
The local alias preserves the forced gateway: it cannot open a host shell and
|
|
324
329
|
does not relax forwarding denial. Provider authentication and credentials stay
|
|
325
|
-
inside the sandbox.
|
|
326
|
-
|
|
330
|
+
inside the sandbox. Install and authenticate Codex, Claude Code, Cursor CLI, or
|
|
331
|
+
Gemini CLI inside the sandbox; use the entry points and compatibility guidance
|
|
332
|
+
in [references/runtime.md](references/runtime.md). After an authenticated
|
|
333
|
+
profile refresh, update the alias only with the explicit second confirmation:
|
|
327
334
|
|
|
328
335
|
```sh
|
|
329
336
|
warpmetal sandbox access refresh \
|
|
@@ -201,7 +201,18 @@ Reload requires the recovery owner credential rather than a short-lived
|
|
|
201
201
|
SSH-derived token. `--power-off-first` authorizes shutdown and powered-off
|
|
202
202
|
verification inside the same operation. When Agent Runtime is enabled,
|
|
203
203
|
`--acknowledge-agent-runtime-reset` is required because workspaces are erased,
|
|
204
|
-
the
|
|
204
|
+
the Runtime identity is replaced, empty sandboxes are reconciled automatically,
|
|
205
|
+
and connection profiles must be refreshed. After a successful reload, wait for
|
|
206
|
+
automatic setup before refreshing profiles:
|
|
207
|
+
|
|
208
|
+
```sh
|
|
209
|
+
warpmetal runtime get --server <serverId> --wait --json
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
The successful operation records a new owner SSH trust epoch. Verify the
|
|
213
|
+
replacement host key before owner SSH. Manual Runtime installation remains an
|
|
214
|
+
explicit repair path when automatic setup fails; it is not part of a
|
|
215
|
+
successful reload.
|
|
205
216
|
|
|
206
217
|
Use `--token-file` only for recovery when local state is unavailable. Prefer
|
|
207
218
|
`WARPMETAL_OWNER_TOKEN` or `WARPMETAL_ACCESS_TOKEN` for a single command over a
|
|
@@ -277,7 +288,7 @@ returns the OpenSSH or remote exit status. `--connection-file` on grant
|
|
|
277
288
|
creation requires `--wait`.
|
|
278
289
|
`sandbox access refresh` atomically replaces a stale token-free profile with
|
|
279
290
|
the currently applied grant and API-reported pinned host keys; use it after an
|
|
280
|
-
OS reload
|
|
291
|
+
OS reload once automatic Runtime reconciliation is ready.
|
|
281
292
|
|
|
282
293
|
`sandbox access install-ssh` is local-only and requires CLI 0.8.10 or newer.
|
|
283
294
|
It turns the reviewed profile and sandbox-private identity into a concrete
|
|
@@ -300,8 +311,14 @@ ssh -t <alias> claude
|
|
|
300
311
|
ssh <alias> claude -p '<task>'
|
|
301
312
|
ssh -t <alias> agent
|
|
302
313
|
ssh <alias> agent -p '<task>'
|
|
314
|
+
ssh -t <alias> gemini
|
|
315
|
+
ssh <alias> gemini -p '<task>'
|
|
303
316
|
```
|
|
304
317
|
|
|
318
|
+
Install and authenticate Codex, Claude Code, Cursor CLI, or Gemini CLI inside
|
|
319
|
+
the sandbox first. WarpMetal does not install, authenticate, configure, or
|
|
320
|
+
receive credentials for those tools.
|
|
321
|
+
|
|
305
322
|
[Codex Desktop](https://learn.chatgpt.com/docs/remote-connections) reads
|
|
306
323
|
the concrete alias from `~/.ssh/config` and starts Codex through the sandbox
|
|
307
324
|
login shell, so Codex must be installed and on that login-shell `PATH`. The
|
|
@@ -310,6 +327,13 @@ not compatible with this boundary. Keep forwarding denied and use the
|
|
|
310
327
|
[Cursor CLI](https://cursor.com/docs/cli/overview) interactive or
|
|
311
328
|
[headless](https://cursor.com/docs/cli/headless) commands shown above.
|
|
312
329
|
|
|
330
|
+
Install Gemini CLI from its official [installation guide](https://geminicli.com/docs/get-started/installation/)
|
|
331
|
+
and use its documented [headless mode](https://geminicli.com/docs/cli/headless/)
|
|
332
|
+
for `gemini -p`. Gemini's optional Docker or Podman sandbox is normally
|
|
333
|
+
unavailable inside the WarpMetal sandbox because no host container-engine
|
|
334
|
+
socket is exposed; run Gemini directly inside the existing outer sandbox and
|
|
335
|
+
choose its approvals yourself.
|
|
336
|
+
|
|
313
337
|
## Skill installation and state
|
|
314
338
|
|
|
315
339
|
```sh
|
|
@@ -208,11 +208,12 @@ The token-free profile is written only after the grant is `applied` and the
|
|
|
208
208
|
API supplies verified VPS host keys. Do not print or open that profile in an
|
|
209
209
|
agent conversation.
|
|
210
210
|
|
|
211
|
-
After a destructive OS reload,
|
|
212
|
-
|
|
213
|
-
profile:
|
|
211
|
+
After a successful destructive OS reload, the signed Runtime bootstrap runs
|
|
212
|
+
automatically. Wait for Runtime readiness and for the retained active grant to
|
|
213
|
+
become `applied`, then replace its stale pinned profile:
|
|
214
214
|
|
|
215
215
|
```sh
|
|
216
|
+
warpmetal runtime get --server <serverId> --wait --json
|
|
216
217
|
warpmetal sandbox access refresh \
|
|
217
218
|
--server <serverId> \
|
|
218
219
|
--sandbox <sandboxId> \
|
|
@@ -225,7 +226,8 @@ warpmetal sandbox access refresh \
|
|
|
225
226
|
|
|
226
227
|
The sandbox record is retained, but its old workspace is not; reconciliation
|
|
227
228
|
creates a new empty workspace. Never bypass a host-key mismatch or reuse the
|
|
228
|
-
pre-reload profile.
|
|
229
|
+
pre-reload profile. Use manual Runtime installation only as an explicitly
|
|
230
|
+
approved repair when the backend reports that automatic setup failed.
|
|
229
231
|
|
|
230
232
|
### Install a concrete OpenSSH alias
|
|
231
233
|
|
|
@@ -294,11 +296,14 @@ ssh -t <alias> claude
|
|
|
294
296
|
ssh <alias> claude -p '<task>'
|
|
295
297
|
ssh -t <alias> agent
|
|
296
298
|
ssh <alias> agent -p '<task>'
|
|
299
|
+
ssh -t <alias> gemini
|
|
300
|
+
ssh <alias> gemini -p '<task>'
|
|
297
301
|
```
|
|
298
302
|
|
|
299
|
-
|
|
300
|
-
the sandbox
|
|
301
|
-
|
|
303
|
+
Install and authenticate Codex, Claude Code, Cursor CLI, or Gemini CLI inside
|
|
304
|
+
the sandbox first. Provider authentication and credentials are sandbox-owned
|
|
305
|
+
and persist only in the sandbox home. WarpMetal does not install, authenticate,
|
|
306
|
+
configure, or receive credentials for those tools.
|
|
302
307
|
|
|
303
308
|
[Codex Desktop](https://learn.chatgpt.com/docs/remote-connections)
|
|
304
309
|
discovers a concrete alias through `~/.ssh/config`, requires ordinary
|
|
@@ -314,6 +319,14 @@ restricted alias. Do not relax forwarding controls. Use the official
|
|
|
314
319
|
[headless mode](https://cursor.com/docs/cli/headless) with
|
|
315
320
|
`ssh <alias> agent -p '<task>'` instead.
|
|
316
321
|
|
|
322
|
+
Follow Gemini CLI's official [installation guide](https://geminicli.com/docs/get-started/installation/),
|
|
323
|
+
then use `ssh -t <alias> gemini` interactively or its documented
|
|
324
|
+
[headless mode](https://geminicli.com/docs/cli/headless/) with
|
|
325
|
+
`ssh <alias> gemini -p '<task>'`. Gemini's optional Docker or Podman sandbox is
|
|
326
|
+
normally unavailable inside the WarpMetal sandbox because no host
|
|
327
|
+
container-engine socket is exposed; run Gemini directly inside the existing
|
|
328
|
+
outer sandbox and choose its approvals yourself.
|
|
329
|
+
|
|
317
330
|
Connect without an owner management credential:
|
|
318
331
|
|
|
319
332
|
```sh
|
|
@@ -95,9 +95,10 @@ A reload requires both `confirm: "ERASE"` and `powerOffFirst: true`. Treat
|
|
|
95
95
|
server, wait until it is powered off, and then erase and reinstall it inside
|
|
96
96
|
one lifecycle operation. When Agent Runtime is enabled, also require
|
|
97
97
|
`acknowledgeAgentRuntimeReset`: all sandbox workspaces are permanently erased,
|
|
98
|
-
the supervisor identity is revoked,
|
|
99
|
-
|
|
100
|
-
CLI command
|
|
98
|
+
the supervisor identity is revoked, signed Runtime setup runs automatically,
|
|
99
|
+
desired sandboxes are recreated empty, and all pinned profiles require refresh.
|
|
100
|
+
Use only the guarded CLI command, wait for Runtime readiness, and stop on
|
|
101
|
+
`manual_review`. Reload invalidates the prior
|
|
101
102
|
owner-facing SSH host-key trust decision; the provider may rotate or preserve
|
|
102
103
|
the key. CLI 0.8.8 opens one new managed trust epoch only after the exact local
|
|
103
104
|
reload operation succeeds and reports that refresh is required. The first
|
package/src/cli.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { readFile, stat } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
3
4
|
import { basename, join, resolve } from "node:path";
|
|
4
5
|
|
|
5
6
|
import {
|
|
@@ -147,6 +148,14 @@ SSH host trust (CLI 0.8.8+):
|
|
|
147
148
|
active attacker on the first connection. Provider-console pre-enrollment is
|
|
148
149
|
the optional higher-assurance alternative.
|
|
149
150
|
|
|
151
|
+
Runtime-enabled reloads:
|
|
152
|
+
Reload still requires --acknowledge-agent-runtime-reset because every
|
|
153
|
+
sandbox workspace is erased. After a successful reload, Runtime setup is
|
|
154
|
+
automatic: use warpmetal runtime get --server <serverId> --wait --json,
|
|
155
|
+
then refresh every sandbox connection profile before reconnecting. The
|
|
156
|
+
successful operation also records a new owner SSH trust epoch; verify the
|
|
157
|
+
replacement host key before owner SSH.
|
|
158
|
+
|
|
150
159
|
Sandbox SSH aliases:
|
|
151
160
|
Refresh the authenticated connection profile before refreshing an existing
|
|
152
161
|
alias:
|
|
@@ -165,6 +174,12 @@ Sandbox SSH aliases:
|
|
|
165
174
|
ssh <alias> claude -p "<prompt>"
|
|
166
175
|
ssh -t <alias> agent
|
|
167
176
|
ssh <alias> agent -p "<prompt>"
|
|
177
|
+
ssh -t <alias> gemini
|
|
178
|
+
ssh <alias> gemini -p "<prompt>"
|
|
179
|
+
|
|
180
|
+
Install and authenticate Codex, Claude Code, Cursor CLI, or Gemini CLI inside
|
|
181
|
+
the sandbox first. WarpMetal does not install, authenticate, configure, or
|
|
182
|
+
receive credentials for those tools.
|
|
168
183
|
|
|
169
184
|
Codex Desktop discovers the concrete alias through ~/.ssh/config and opens
|
|
170
185
|
the sandbox login shell, so Codex must be available on the login-shell PATH.
|
|
@@ -172,10 +187,17 @@ Sandbox SSH aliases:
|
|
|
172
187
|
a host shell, and do not relax isolation: ClearAllForwardings yes keeps all
|
|
173
188
|
forwarding disabled.
|
|
174
189
|
|
|
190
|
+
Gemini's optional Docker or Podman sandbox is normally unavailable inside
|
|
191
|
+
the WarpMetal sandbox because no host container-engine socket is exposed;
|
|
192
|
+
run Gemini directly inside the existing outer sandbox and choose its
|
|
193
|
+
approvals yourself.
|
|
194
|
+
|
|
175
195
|
Compatibility references:
|
|
176
196
|
https://learn.chatgpt.com/docs/remote-connections
|
|
177
197
|
https://cursor.com/docs/cli/overview
|
|
178
198
|
https://cursor.com/docs/cli/headless
|
|
199
|
+
https://geminicli.com/docs/get-started/installation/
|
|
200
|
+
https://geminicli.com/docs/cli/headless/
|
|
179
201
|
|
|
180
202
|
Credential environment variables:
|
|
181
203
|
WARPMETAL_OWNER_TOKEN Recovery/bootstrap credential for one explicit command
|
|
@@ -1982,7 +2004,7 @@ async function applyReloadResult(
|
|
|
1982
2004
|
await store.invalidateServerAccess(serverId);
|
|
1983
2005
|
if (operation.result?.reloadImpact?.agentRuntimeAffected) {
|
|
1984
2006
|
await store.saveRuntime(serverId, {
|
|
1985
|
-
state: "
|
|
2007
|
+
state: "pending_install",
|
|
1986
2008
|
desiredRevision: undefined,
|
|
1987
2009
|
appliedRevision: 0,
|
|
1988
2010
|
lastSeenAt: null,
|
|
@@ -2090,8 +2112,8 @@ async function handleServerReload(client, store, options, context) {
|
|
|
2090
2112
|
(operation?.state === "succeeded" &&
|
|
2091
2113
|
operation.result?.reloadImpact?.ownerKnownHostsNeedRefresh
|
|
2092
2114
|
? operation.result?.reloadImpact?.agentRuntimeAffected
|
|
2093
|
-
? "
|
|
2094
|
-
: "
|
|
2115
|
+
? " Agent Runtime setup is automatic. Wait for Agent Runtime to become ready, then refresh every sandbox connection profile. Verify the replacement host key before owner SSH."
|
|
2116
|
+
: " Verify the replacement host key before owner SSH; the successful reload recorded a new operation-bound trust epoch."
|
|
2095
2117
|
: operation?.state === "succeeded"
|
|
2096
2118
|
? " WarpMetal did not report a host-key refresh; the existing managed pin remains required."
|
|
2097
2119
|
: ""),
|
|
@@ -2740,7 +2762,7 @@ async function handleAccessInstallSsh(options, context) {
|
|
|
2740
2762
|
alias: stringOption(options, "alias", { required: true }),
|
|
2741
2763
|
connectionFile: stringOption(options, "connection-file", { required: true }),
|
|
2742
2764
|
identity: stringOption(options, "identity", { required: true }),
|
|
2743
|
-
homeDirectory: context.env.HOME,
|
|
2765
|
+
homeDirectory: context.env.HOME || context.env.USERPROFILE || homedir(),
|
|
2744
2766
|
confirm: stringOption(options, "confirm"),
|
|
2745
2767
|
});
|
|
2746
2768
|
emit(
|
|
@@ -2755,7 +2777,7 @@ async function handleAccessInstallSsh(options, context) {
|
|
|
2755
2777
|
async function handleAccessRemoveSsh(options, context) {
|
|
2756
2778
|
const result = await removeSshAlias({
|
|
2757
2779
|
alias: stringOption(options, "alias", { required: true }),
|
|
2758
|
-
homeDirectory: context.env.HOME,
|
|
2780
|
+
homeDirectory: context.env.HOME || context.env.USERPROFILE || homedir(),
|
|
2759
2781
|
confirm: stringOption(options, "confirm", { required: true }),
|
|
2760
2782
|
});
|
|
2761
2783
|
emit(
|
package/src/ssh-alias.js
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
rmdir,
|
|
11
11
|
unlink,
|
|
12
12
|
} from "node:fs/promises";
|
|
13
|
-
import { isAbsolute, join, parse, resolve } from "node:path";
|
|
13
|
+
import { isAbsolute, join, parse, posix, resolve, win32 } from "node:path";
|
|
14
14
|
import { randomUUID } from "node:crypto";
|
|
15
15
|
|
|
16
16
|
import {
|
|
@@ -21,7 +21,8 @@ import {
|
|
|
21
21
|
import { CliError } from "./errors.js";
|
|
22
22
|
|
|
23
23
|
const ALIAS_PATTERN = /^(?=.{1,63}$)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
24
|
-
const
|
|
24
|
+
const POSIX_UNSAFE_PATH = /[\0\r\n\t%"'`$!&;<>|*?()[\]{}\\#]/;
|
|
25
|
+
const WINDOWS_UNSAFE_PATH = /[\0\r\n\t%"'`$!&;<>|*?()[\]{}#]/;
|
|
25
26
|
const MANAGED_HEADER = "# Managed by WarpMetal. Do not edit.";
|
|
26
27
|
|
|
27
28
|
function invalid(message) {
|
|
@@ -37,17 +38,18 @@ export function validateSshAlias(value) {
|
|
|
37
38
|
return value;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
function validateAbsolutePath(value, label) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
!isAbsolute(value) ||
|
|
44
|
-
resolve(value) !== value ||
|
|
45
|
-
value === parse(value).root ||
|
|
46
|
-
UNSAFE_PATH.test(value)
|
|
47
|
-
) {
|
|
41
|
+
export function validateAbsolutePath(value, label, platform = process.platform) {
|
|
42
|
+
const path = platform === "win32" ? win32 : posix;
|
|
43
|
+
if (typeof value !== "string" || !path.isAbsolute(value)) {
|
|
48
44
|
invalid(`${label} must be an absolute path without unsafe interpolation characters.`);
|
|
49
45
|
}
|
|
50
|
-
|
|
46
|
+
const resolved = path.resolve(value);
|
|
47
|
+
const unsafePath =
|
|
48
|
+
platform === "win32" ? WINDOWS_UNSAFE_PATH : POSIX_UNSAFE_PATH;
|
|
49
|
+
if (resolved === path.parse(resolved).root || unsafePath.test(resolved)) {
|
|
50
|
+
invalid(`${label} must be an absolute path without unsafe interpolation characters.`);
|
|
51
|
+
}
|
|
52
|
+
return resolved;
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
function requireOwner(metadata, label) {
|
|
@@ -420,12 +422,19 @@ async function syncDirectory(path) {
|
|
|
420
422
|
handle = await open(path, "r");
|
|
421
423
|
await handle.sync();
|
|
422
424
|
} catch (error) {
|
|
423
|
-
if (!
|
|
425
|
+
if (!isIgnorableDirectorySyncError(error)) throw error;
|
|
424
426
|
} finally {
|
|
425
427
|
await handle?.close().catch(() => {});
|
|
426
428
|
}
|
|
427
429
|
}
|
|
428
430
|
|
|
431
|
+
export function isIgnorableDirectorySyncError(error, platform = process.platform) {
|
|
432
|
+
return (
|
|
433
|
+
["EINVAL", "ENOTSUP", "EISDIR"].includes(error?.code) ||
|
|
434
|
+
(platform === "win32" && ["EPERM", "EACCES"].includes(error?.code))
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
429
438
|
async function commitStaged(staged) {
|
|
430
439
|
try {
|
|
431
440
|
for (const item of staged) {
|