warpmetal 0.8.1 → 0.8.3
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
|
@@ -98,6 +98,11 @@ checkout returns `serverId`, the CLI binds that ID to the identity so
|
|
|
98
98
|
an `--identity` flag. Use `--ssh-public-key-file` instead when supplying a
|
|
99
99
|
user-managed public key.
|
|
100
100
|
|
|
101
|
+
WarpMetal installs the owner key for `root` on every supported image. The SSH
|
|
102
|
+
key does not encode a login username: use `root@<server-ip>` for an owner shell
|
|
103
|
+
and `--ssh-user root` for runtime installation, not a distribution convention
|
|
104
|
+
such as `ubuntu`.
|
|
105
|
+
|
|
101
106
|
Pass `--json` for structured, secret-redacted output. Use
|
|
102
107
|
`WARPMETAL_API_URL` for an alternate API origin and `WARPMETAL_HOME` for an
|
|
103
108
|
alternate state directory.
|
|
@@ -304,7 +309,7 @@ compatible external signer.
|
|
|
304
309
|
warpmetal runtime enable --server <serverId> --json
|
|
305
310
|
warpmetal runtime install \
|
|
306
311
|
--server <serverId> \
|
|
307
|
-
--ssh-user
|
|
312
|
+
--ssh-user root \
|
|
308
313
|
--confirm INSTALL \
|
|
309
314
|
--wait \
|
|
310
315
|
--json
|
|
@@ -320,6 +325,17 @@ warpmetal sandbox access keygen \
|
|
|
320
325
|
--json
|
|
321
326
|
```
|
|
322
327
|
|
|
328
|
+
Installation gives pre-existing Docker containers exact liveness checks and
|
|
329
|
+
tracks common container-runtime processes without collecting application
|
|
330
|
+
configuration. The signed installer uses `crun` for its private rootless Podman
|
|
331
|
+
engine, refuses package removals or changes to an installed container stack,
|
|
332
|
+
and checks minimal liveness metadata after package installation and again
|
|
333
|
+
immediately before registration. Safety refusals have no force bypass; review
|
|
334
|
+
the reported `runtime_*` code instead of changing or restarting a third-party
|
|
335
|
+
runtime automatically. With `--json`, that code is returned as structured
|
|
336
|
+
`error.code`. A pending reboot and legacy preview Podman state are separate
|
|
337
|
+
maintenance operations, not implicit install steps.
|
|
338
|
+
|
|
323
339
|
See `skills/warpmetal/references/runtime.md` for the complete lifecycle,
|
|
324
340
|
cleanup, access-grant, and strict host-key connection workflow.
|
|
325
341
|
|
package/package.json
CHANGED
|
@@ -209,6 +209,13 @@ key-only policy: `PasswordAuthentication no`,
|
|
|
209
209
|
`KbdInteractiveAuthentication no`, `PermitRootLogin prohibit-password`, and
|
|
210
210
|
`AuthenticationMethods publickey`.
|
|
211
211
|
|
|
212
|
+
The WarpMetal owner SSH account is `root` on every supported image. SSH public
|
|
213
|
+
keys do not bind a login username. Use `root@<server-ip>` for an owner shell and
|
|
214
|
+
pass `--ssh-user root` whenever a CLI command asks for the owner SSH account;
|
|
215
|
+
never infer `ubuntu` or another distribution-default username. This does not
|
|
216
|
+
apply to `sandbox connect`, which uses the gateway username from its connection
|
|
217
|
+
profile.
|
|
218
|
+
|
|
212
219
|
For routine management, prove possession of the installed SSH key without
|
|
213
220
|
reading it:
|
|
214
221
|
|
|
@@ -202,7 +202,7 @@ process listings.
|
|
|
202
202
|
warpmetal runtime enable --server <serverId> [--idempotency-key <key>] --json
|
|
203
203
|
warpmetal runtime get --server <serverId> [--wait] [--timeout-seconds <n>] --json
|
|
204
204
|
warpmetal runtime install \
|
|
205
|
-
--server <serverId> [--identity <owner-key>] --ssh-user
|
|
205
|
+
--server <serverId> [--identity <owner-key>] --ssh-user root \
|
|
206
206
|
--confirm INSTALL [--wait] [--timeout-seconds <n>] --json
|
|
207
207
|
|
|
208
208
|
warpmetal sandbox create \
|
|
@@ -48,12 +48,15 @@ the existing separate interactive-or-autonomous payment authority check.
|
|
|
48
48
|
|
|
49
49
|
For an existing ready server:
|
|
50
50
|
|
|
51
|
+
Use the WarpMetal owner account `root` for every supported host image; do not
|
|
52
|
+
substitute a distribution-default account such as `ubuntu`.
|
|
53
|
+
|
|
51
54
|
```sh
|
|
52
55
|
warpmetal runtime enable --server <serverId> --json
|
|
53
56
|
warpmetal runtime install \
|
|
54
57
|
--server <serverId> \
|
|
55
58
|
--identity <owner-private-key-path> \
|
|
56
|
-
--ssh-user
|
|
59
|
+
--ssh-user root \
|
|
57
60
|
--confirm INSTALL \
|
|
58
61
|
--wait \
|
|
59
62
|
--json
|
|
@@ -65,6 +68,36 @@ The CLI holds the one-time bootstrap only in memory, verifies the signed
|
|
|
65
68
|
artifact, uploads it through OpenSSH without a shell-enabled local spawn, and
|
|
66
69
|
does not print or store the bootstrap.
|
|
67
70
|
|
|
71
|
+
The signed installer is designed to preserve container workloads already
|
|
72
|
+
running on a supported host. It selects `crun` for WarpMetal's private rootless
|
|
73
|
+
Podman service, refuses APT removals and DNF erasures, protects installed
|
|
74
|
+
Docker/containerd/Podman and package-manager versions, and compares a minimal
|
|
75
|
+
root-only liveness snapshot after the guarded package action and immediately
|
|
76
|
+
before registration. The snapshot contains recognized runtime-process start
|
|
77
|
+
metadata and, when Docker is active, container IDs, init PIDs, and start
|
|
78
|
+
timestamps. Docker receives container-level checks; other recognized engines
|
|
79
|
+
receive process-level checks and need separate certification for broader
|
|
80
|
+
coexistence claims. The snapshot never collects names, images, environment,
|
|
81
|
+
mounts, or logs.
|
|
82
|
+
|
|
83
|
+
Treat these refusal codes as safety gates, not prompts to force or repair the
|
|
84
|
+
host automatically:
|
|
85
|
+
|
|
86
|
+
- `runtime_package_plan_unsafe`: the proposed package transaction was refused;
|
|
87
|
+
- `runtime_workload_state_unverifiable`: an active Docker workload could not be
|
|
88
|
+
inspected safely;
|
|
89
|
+
- `runtime_workload_drift_detected`: a pre-existing runtime process or Docker
|
|
90
|
+
container changed during the installation window;
|
|
91
|
+
- `runtime_package_postcondition_failed`: a protected container package changed
|
|
92
|
+
unexpectedly; stop and review the host package logs;
|
|
93
|
+
- `runtime_legacy_migration_required`: preview Podman state needs a separate,
|
|
94
|
+
explicitly reviewed migration and was not reset.
|
|
95
|
+
|
|
96
|
+
There is no force bypass. If `runtime_reboot_required` is returned, schedule
|
|
97
|
+
the reboot as a separate maintenance action and retry only after the host and
|
|
98
|
+
its existing workloads are healthy. The ordinary installer never installs a
|
|
99
|
+
kernel or runs `podman system reset`.
|
|
100
|
+
|
|
68
101
|
## Sizes and lifetime
|
|
69
102
|
|
|
70
103
|
Use `small`, `medium`, `large`, or `xlarge` exactly as the live catalog
|
package/src/errors.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
export class CliError extends Error {
|
|
2
|
-
constructor(
|
|
2
|
+
constructor(
|
|
3
|
+
message,
|
|
4
|
+
{ exitCode = 1, details = undefined, code = undefined } = {},
|
|
5
|
+
) {
|
|
3
6
|
super(message);
|
|
4
7
|
this.name = "CliError";
|
|
5
8
|
this.exitCode = exitCode;
|
|
6
9
|
this.details = details;
|
|
10
|
+
this.code = code ?? details?.code;
|
|
7
11
|
}
|
|
8
12
|
}
|
|
9
13
|
|
package/src/installer.js
CHANGED
|
@@ -24,6 +24,71 @@ const REQUIRED_FILES = [
|
|
|
24
24
|
"warpmetal-sandbox.conf",
|
|
25
25
|
];
|
|
26
26
|
|
|
27
|
+
const INSTALLER_ERROR_MESSAGES = new Map([
|
|
28
|
+
[
|
|
29
|
+
"runtime_install_lock_unavailable",
|
|
30
|
+
"The server cannot create the Agent Runtime installation lock.",
|
|
31
|
+
],
|
|
32
|
+
[
|
|
33
|
+
"runtime_install_state_unavailable",
|
|
34
|
+
"The server cannot create private temporary state for the installation checks.",
|
|
35
|
+
],
|
|
36
|
+
[
|
|
37
|
+
"runtime_install_in_progress",
|
|
38
|
+
"Another Agent Runtime installation is already running on this server.",
|
|
39
|
+
],
|
|
40
|
+
[
|
|
41
|
+
"runtime_reboot_required",
|
|
42
|
+
"The server already requires a reboot. Reboot it as a separate maintenance action, then retry.",
|
|
43
|
+
],
|
|
44
|
+
[
|
|
45
|
+
"runtime_package_index_failed",
|
|
46
|
+
"The server package index could not be refreshed; no Agent Runtime services were changed.",
|
|
47
|
+
],
|
|
48
|
+
[
|
|
49
|
+
"runtime_package_plan_unsafe",
|
|
50
|
+
"Installation refused a package transaction that could change the existing container stack.",
|
|
51
|
+
],
|
|
52
|
+
[
|
|
53
|
+
"runtime_package_apply_failed",
|
|
54
|
+
"The guarded dependency transaction failed; the Agent Runtime supervisor was not registered.",
|
|
55
|
+
],
|
|
56
|
+
[
|
|
57
|
+
"runtime_package_postcondition_failed",
|
|
58
|
+
"An installed container package changed unexpectedly. Do not retry until the host package state is reviewed.",
|
|
59
|
+
],
|
|
60
|
+
[
|
|
61
|
+
"runtime_workload_state_unverifiable",
|
|
62
|
+
"Installation could not safely inspect the existing container workload state.",
|
|
63
|
+
],
|
|
64
|
+
[
|
|
65
|
+
"runtime_workload_drift_detected",
|
|
66
|
+
"An existing container workload changed during installation. Do not retry until the host runtime is reviewed.",
|
|
67
|
+
],
|
|
68
|
+
[
|
|
69
|
+
"runtime_oci_runtime_unavailable",
|
|
70
|
+
"The certified crun OCI runtime is unavailable on this server.",
|
|
71
|
+
],
|
|
72
|
+
[
|
|
73
|
+
"runtime_podman_unavailable",
|
|
74
|
+
"The private Podman service did not become available.",
|
|
75
|
+
],
|
|
76
|
+
[
|
|
77
|
+
"runtime_legacy_migration_required",
|
|
78
|
+
"Existing preview Agent Runtime state requires a separately reviewed migration; it was not reset.",
|
|
79
|
+
],
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
function mappedInstallerError(result) {
|
|
83
|
+
const lines = `${result.stderr}\n${result.stdout}`
|
|
84
|
+
.split(/\r?\n/)
|
|
85
|
+
.map((line) => line.trim());
|
|
86
|
+
for (const [code, message] of INSTALLER_ERROR_MESSAGES) {
|
|
87
|
+
if (lines.includes(code)) return { code, message: `${message} (${code})` };
|
|
88
|
+
}
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
27
92
|
function validateArtifact(artifact) {
|
|
28
93
|
if (
|
|
29
94
|
!artifact ||
|
|
@@ -130,7 +195,7 @@ function appendBounded(current, chunk) {
|
|
|
130
195
|
async function spawnChecked(
|
|
131
196
|
command,
|
|
132
197
|
args,
|
|
133
|
-
{ stdin, spawnImpl = nodeSpawn, redact = [] } = {},
|
|
198
|
+
{ stdin, spawnImpl = nodeSpawn, redact = [], mapError } = {},
|
|
134
199
|
) {
|
|
135
200
|
const result = await new Promise((resolvePromise, reject) => {
|
|
136
201
|
const child = spawnImpl(command, args, {
|
|
@@ -151,13 +216,18 @@ async function spawnChecked(
|
|
|
151
216
|
else child.stdin?.end();
|
|
152
217
|
});
|
|
153
218
|
if (result.status !== 0) {
|
|
219
|
+
const mappedError = mapError?.(result);
|
|
154
220
|
let message =
|
|
221
|
+
mappedError?.message ||
|
|
155
222
|
result.stderr.trim() ||
|
|
156
223
|
result.stdout.trim() ||
|
|
157
224
|
`${command} exited unsuccessfully`;
|
|
158
225
|
for (const secret of redact)
|
|
159
226
|
if (secret) message = message.split(secret).join("[redacted]");
|
|
160
|
-
throw new CliError(message.slice(0, 500), {
|
|
227
|
+
throw new CliError(message.slice(0, 500), {
|
|
228
|
+
exitCode: 4,
|
|
229
|
+
code: mappedError?.code,
|
|
230
|
+
});
|
|
161
231
|
}
|
|
162
232
|
return result;
|
|
163
233
|
}
|
|
@@ -286,7 +356,12 @@ export async function installRuntime({
|
|
|
286
356
|
"--bundle",
|
|
287
357
|
remoteBundle,
|
|
288
358
|
],
|
|
289
|
-
{
|
|
359
|
+
{
|
|
360
|
+
stdin: `${bootstrapToken}\n`,
|
|
361
|
+
spawnImpl,
|
|
362
|
+
redact: [bootstrapToken],
|
|
363
|
+
mapError: mappedInstallerError,
|
|
364
|
+
},
|
|
290
365
|
);
|
|
291
366
|
await spawnChecked(
|
|
292
367
|
"ssh",
|