warpmetal 0.8.11 → 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 +28 -8
- package/package.json +1 -1
- package/skills/warpmetal/SKILL.md +16 -11
- package/skills/warpmetal/references/cli-reference.md +13 -2
- package/skills/warpmetal/references/runtime.md +6 -4
- package/skills/warpmetal/references/safety.md +4 -3
- package/src/cli.js +14 -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
|
|
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.
|
|
@@ -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
|
|
@@ -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
|
|
|
@@ -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:
|
|
@@ -1995,7 +2004,7 @@ async function applyReloadResult(
|
|
|
1995
2004
|
await store.invalidateServerAccess(serverId);
|
|
1996
2005
|
if (operation.result?.reloadImpact?.agentRuntimeAffected) {
|
|
1997
2006
|
await store.saveRuntime(serverId, {
|
|
1998
|
-
state: "
|
|
2007
|
+
state: "pending_install",
|
|
1999
2008
|
desiredRevision: undefined,
|
|
2000
2009
|
appliedRevision: 0,
|
|
2001
2010
|
lastSeenAt: null,
|
|
@@ -2103,8 +2112,8 @@ async function handleServerReload(client, store, options, context) {
|
|
|
2103
2112
|
(operation?.state === "succeeded" &&
|
|
2104
2113
|
operation.result?.reloadImpact?.ownerKnownHostsNeedRefresh
|
|
2105
2114
|
? operation.result?.reloadImpact?.agentRuntimeAffected
|
|
2106
|
-
? "
|
|
2107
|
-
: "
|
|
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."
|
|
2108
2117
|
: operation?.state === "succeeded"
|
|
2109
2118
|
? " WarpMetal did not report a host-key refresh; the existing managed pin remains required."
|
|
2110
2119
|
: ""),
|
|
@@ -2753,7 +2762,7 @@ async function handleAccessInstallSsh(options, context) {
|
|
|
2753
2762
|
alias: stringOption(options, "alias", { required: true }),
|
|
2754
2763
|
connectionFile: stringOption(options, "connection-file", { required: true }),
|
|
2755
2764
|
identity: stringOption(options, "identity", { required: true }),
|
|
2756
|
-
homeDirectory: context.env.HOME,
|
|
2765
|
+
homeDirectory: context.env.HOME || context.env.USERPROFILE || homedir(),
|
|
2757
2766
|
confirm: stringOption(options, "confirm"),
|
|
2758
2767
|
});
|
|
2759
2768
|
emit(
|
|
@@ -2768,7 +2777,7 @@ async function handleAccessInstallSsh(options, context) {
|
|
|
2768
2777
|
async function handleAccessRemoveSsh(options, context) {
|
|
2769
2778
|
const result = await removeSshAlias({
|
|
2770
2779
|
alias: stringOption(options, "alias", { required: true }),
|
|
2771
|
-
homeDirectory: context.env.HOME,
|
|
2780
|
+
homeDirectory: context.env.HOME || context.env.USERPROFILE || homedir(),
|
|
2772
2781
|
confirm: stringOption(options, "confirm", { required: true }),
|
|
2773
2782
|
});
|
|
2774
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) {
|