warpmetal 0.8.2 → 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
@@ -325,6 +325,17 @@ warpmetal sandbox access keygen \
325
325
  --json
326
326
  ```
327
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
+
328
339
  See `skills/warpmetal/references/runtime.md` for the complete lifecycle,
329
340
  cleanup, access-grant, and strict host-key connection workflow.
330
341
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "warpmetal",
3
- "version": "0.8.2",
3
+ "version": "0.8.3",
4
4
  "description": "Agent-safe CLI and skill for purchasing, renewing, and managing WarpMetal VPS servers",
5
5
  "type": "module",
6
6
  "bin": {
@@ -68,6 +68,36 @@ The CLI holds the one-time bootstrap only in memory, verifies the signed
68
68
  artifact, uploads it through OpenSSH without a shell-enabled local spawn, and
69
69
  does not print or store the bootstrap.
70
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
+
71
101
  ## Sizes and lifetime
72
102
 
73
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(message, { exitCode = 1, details = undefined } = {}) {
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), { exitCode: 4 });
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
- { stdin: `${bootstrapToken}\n`, spawnImpl, redact: [bootstrapToken] },
359
+ {
360
+ stdin: `${bootstrapToken}\n`,
361
+ spawnImpl,
362
+ redact: [bootstrapToken],
363
+ mapError: mappedInstallerError,
364
+ },
290
365
  );
291
366
  await spawnChecked(
292
367
  "ssh",