@ornncompute/cli 0.1.7 → 0.1.9

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
@@ -56,10 +56,11 @@ ornn nodes keys list <node-id> [--json]
56
56
  ornn node health <node-id> [--json]
57
57
  ornn node diagnose <node-id> [--json]
58
58
  ornn node deenroll <node-id> [--reason <text>] [--keep-record] [--json]
59
- ornn fleet clean <ip>... --operator <id-or-slug> --ib-island <name> --identity-file <path> --dry-run [--source-user <user>]... [--preserve-user <user>]... [--ssh-user ubuntu|admin|ornn] [--json]
60
- ornn fleet clean <failed-fleet-id> --identity-file <path> --dry-run [--source-user <user>]... [--preserve-user <user>]... [--json]
61
- ornn fleet clean <fleet-id> --identity-file <path> --confirm-clean <plan-hash> [--source-user <user>]... [--preserve-user <user>]... [--json]
59
+ ornn fleet clean <ip>... --operator <id-or-slug> --ib-island <name> --identity-file <path> --dry-run [--policy <path>] [--policy-out <path>] [--ssh-user <non-root-linux-user>] [--json]
60
+ ornn fleet clean <fleet-id> --identity-file <path> --dry-run [--policy <path>] [--policy-out <path>] [--json]
61
+ ornn fleet clean <fleet-id> --identity-file <path> --confirm-clean <plan-hash> [--json]
62
62
  ornn fleet enroll <fleet-id> --identity-file <path> [--confirm-takeover <ip[,ip...]>] [--json]
63
+ ornn fleet delete <fleet-id> --confirm-delete <fleet-id> [--json]
63
64
  ornn reservations commerce list --tenant <email-or-id> [--fleet <fleet-id>] [--json]
64
65
  ornn reservations commerce create --tenant <email> --listing <listing-id> --fleet <fleet-id> [--start-at <timestamp>] [--end-at <timestamp>] [--price-per-gpu-hour <rate>] [--json]
65
66
  ornn fleet deploy <fleet-id> --tenant <contact-email> [--user <email-or-id>] --commerce-reservation <id> [--network public|private] [--json]
@@ -227,28 +228,73 @@ Fleet handoff is an ordered, Compute-backed workflow:
227
228
  - Before first contact, independently verify each host key and add it to the
228
229
  operator's SSH `known_hosts`; fleet SSH uses strict host-key checking and will
229
230
  not trust a key first presented during cleanup.
230
- - `fleet clean ... --dry-run` tries `ubuntu`, `admin`, then `ornn`, records each
231
- host's hardware identity and exact deletion plan, and prints one fleet plan
232
- hash. Repeat `--source-user` for Linux accounts owned by the departing tenant
233
- (including an explicitly reviewed orphaned `/home/<user>` directory) and
234
- `--preserve-user` for reviewed host accounts. Package/service-owned
235
- accounts are protected automatically; any other account stops planning for
236
- explicit review. Re-running with `--confirm-clean <plan-hash>` and the same
237
- account flags removes the approved tenant
238
- users, keys, containers/images/volumes/networks/build cache, workload paths,
239
- mounts, services, and tenant-scoped logs. Apply refuses plans with running
240
- containers, locks the management/root passwords, enforces key-only SSH, trims
241
- supported filesystems, preserves existing root and management keys and host
242
- logs, and stores per-node proof in Compute. A failed
243
- cleanup can be dry-run again with the same fleet ID; only failed members are
244
- replanned. Residual GPU
245
- consumers or tenant/platform listeners fail verification instead of being
246
- silently accepted.
231
+ - `fleet clean ... --dry-run` tries `ubuntu`, `admin`, then `ornn`, or the
232
+ explicit non-root account supplied with `--ssh-user`. It binds the
233
+ inventory to the host's hardware identity, records the plan in Compute, and
234
+ writes an editable mode-`0600` policy beside the local fleet manifest (or to
235
+ `--policy-out`). Inventory covers accounts and orphan homes, authorized SSH
236
+ keys, tenant/log/temp/platform paths, workload and listener processes, GPU
237
+ consumers, custom systemd services, Docker/Podman objects and bind-mount
238
+ dependencies, and every block drive. Each resource advertises its own valid
239
+ actions. Suggestions are never applied silently: unresolved choices and
240
+ conflicts are recorded as plan data, so edit the policy and re-run the same
241
+ fleet with `--policy`; planning is not terminalized just because a choice is
242
+ needed. `--source-user` and `--preserve-user` remain legacy user-only
243
+ shortcuts and cannot be mixed with a policy file.
244
+ Coverage gates also inventory listeners, GPU and container-runtime visibility,
245
+ non-block mounts, advanced SSH sources, filesystem scope, root-owned workloads,
246
+ network state, groups/sudo, IPC, swap/RAM, and active block stacks. A verified
247
+ gate is kept automatically. A present or uninspectable gate must be explicitly
248
+ ignored with a reason, so the receipt exposes the excluded scope.
249
+ - `fleet clean <fleet-id> ... --confirm-clean <plan-hash>` is shown only after
250
+ every choice is resolved. Execution rechecks hardware and resource identity,
251
+ runs an idempotent per-resource journal, preserves the management SSH key,
252
+ enforces key-only SSH, and returns exact deleted, preserved, exception, and
253
+ drive-assurance evidence. `ignore` requires a reason and produces
254
+ `clean_with_exceptions`; it is never reported as residue-free deletion.
255
+ Kept files and directories are content-hashed; process identity, container
256
+ replacement state, and exact drive topology are rechecked. Data-drive actions
257
+ are selectable: `logical_clean`, `discard`, `sanitize`, or
258
+ `reformat`. The system drive and drives without a unique serial/WWN are
259
+ locked to keep/ignore for in-band cleanup. Logical clean is offered only when
260
+ every filesystem in the approved topology is mounted and can be emptied and
261
+ verified; it never substitutes `wipefs` for data deletion. Discard and NVMe
262
+ sanitize require device-reported support, and strong actions refuse active or
263
+ unverified block stacks. `sanitize` requires NVMe plus
264
+ controller-reported completion. Logical cleanup, discard, and reformat are
265
+ not certified physical-media erasure, and the system drive requires an
266
+ out-of-band reimage for that assurance.
267
+ If the local SSH command times out while a long device operation is still in
268
+ progress, the recorded run remains resumable; re-run the same
269
+ `--confirm-clean <plan-hash>` command instead of creating a different plan.
270
+ Plans created by a pre-v7 runner must be replanned with `--dry-run` before
271
+ approval. This keeps newly discovered resources bound to a fresh reviewed
272
+ hash instead of adapting a legacy account-only plan during execution.
273
+
274
+ In the generated policy, change only `action`, plus `reason` for `ignore` or
275
+ `replacement` for `replace`; keep each generated `identity_hash` intact. `keep`
276
+ leaves a resource running, `stop_keep` stops but retains it, `stop` terminates a
277
+ process, `delete` removes it, `restart` restarts a service, and `replace`
278
+ recreates a container from the supplied image/name/network/command/labels/mounts
279
+ specification. The generated `suggested_action` is review guidance, not an
280
+ automatic decision. Do not put secrets in replacement commands or labels; use
281
+ the platform secret manager or a reviewed secret mount.
282
+
247
283
  - `fleet enroll <fleet-id>` refuses nodes without successful cleanup proof,
248
- issues and revokes one cleanup-bound token per host, installs the agent,
284
+ live-revalidates each receipt under the current enrollment attempt, then
285
+ issues and revokes one cleanup- and attempt-bound token per host, installs the
286
+ agent,
249
287
  assigns the IB island, verifies the same management account still has
250
288
  key-based SSH and passwordless sudo, and records the resulting GPU node IDs
251
- in Compute. Generic enrollment tokens retain the legacy enrollment sanitizer.
289
+ in Compute. A private executor credential prevents two CLIs from sharing an
290
+ active lease. Retries reconcile nodes already registered by the same attempt
291
+ before rerunning cleanup or installation. Generic enrollment tokens retain
292
+ the legacy enrollment sanitizer.
293
+ - `fleet delete <fleet-id> --confirm-delete <fleet-id>` hard-deletes only an
294
+ abandoned cleanup plan or an unassigned legacy enrollment record. It saves a
295
+ local audit manifest first and refuses any Commerce/tenant/deployment link,
296
+ active enrollment, assigned GPU node, or completed/running cleanup evidence.
297
+ It never deletes the linked GPU-node records.
252
298
  - `reservations commerce create --tenant <email> --listing <listing-id>
253
299
  --fleet <fleet-id>` creates and activates the tenant's Commerce reservation.
254
300
  GPU count comes from the enrolled fleet; start, end, and hourly rate default
@@ -266,8 +312,10 @@ Fleet handoff is an ordered, Compute-backed workflow:
266
312
  node; Commerce is stamped only after the entire fleet reports deployed. A
267
313
  Commerce reservation can be linked to only one fleet.
268
314
 
269
- Compute Postgres is authoritative; the mode-0600 files under
270
- `~/.config/ornn/fleets/` are only a secret-free cache. Cleanup proves “no
315
+ Compute Postgres is authoritative. The mode-0600 files under
316
+ `~/.config/ornn/fleets/` include a short-lived enrollment resume credential;
317
+ treat them as sensitive and do not copy them into tickets or source control.
318
+ Cleanup proves “no
271
319
  observable residue under the verification scope,” not certified physical-media
272
320
  erasure. Root is never the preserved management account. `--confirm-takeover`
273
321
  names only fleet IPs whose existing node claims may be replaced. The IB-island
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ornncompute/cli",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Command-line interface for Ornn compute access workflows.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -107,7 +107,7 @@ export function resolveComputeBaseUrl({ env = process.env } = {}) {
107
107
  const base = env.ORNN_COMPUTE_BASE_URL?.trim();
108
108
  if (!base) {
109
109
  throw new CliApiError(
110
- "Set ORNN_COMPUTE_BASE_URL to the internal compute service origin to run operator commands.",
110
+ "Set ORNN_COMPUTE_BASE_URL to the internal compute service origin to run operator commands."
111
111
  );
112
112
  }
113
113
  return requireValidBaseUrl(base, "ORNN_COMPUTE_BASE_URL");
@@ -173,7 +173,7 @@ export function resolveInternalReviewSecret({ env = process.env } = {}) {
173
173
  const secret = env.ORNN_INTERNAL_REVIEW_SECRET?.trim();
174
174
  if (!secret) {
175
175
  throw new CliApiError(
176
- "Set ORNN_INTERNAL_REVIEW_SECRET to authenticate operator commands, or run `ornn login` as Ornn staff.",
176
+ "Set ORNN_INTERNAL_REVIEW_SECRET to authenticate operator commands, or run `ornn login` as Ornn staff."
177
177
  );
178
178
  }
179
179
  return secret;
@@ -267,7 +267,7 @@ export async function operatorRequest({
267
267
  throw new CliApiError(
268
268
  "Operator commands need Ornn staff auth: run `ornn login` as an internal user " +
269
269
  "(ORNN_AUTH_BASE_URL), or set ORNN_INTERNAL_REVIEW_SECRET (+ ORNN_COMPUTE_BASE_URL) for direct compute access.",
270
- { status: 401 },
270
+ { status: 401 }
271
271
  );
272
272
  }
273
273
 
@@ -287,7 +287,7 @@ export async function webOperatorRequest({
287
287
  if (!session?.accessToken) {
288
288
  throw new CliApiError(
289
289
  "Commerce-backed deployment requires Ornn staff auth through Web. Run `ornn login` first.",
290
- { status: 401 },
290
+ { status: 401 }
291
291
  );
292
292
  }
293
293
  return operatorRequestViaStaffSession({ body, endpoint, env, fetchImpl, method });
@@ -313,7 +313,7 @@ function requireValidBaseUrl(raw, sourceName) {
313
313
  const stripped = raw.replace(/\/+$/, "");
314
314
  if (!stripped) {
315
315
  throw new CliApiError(
316
- `${sourceName} must be a valid origin (got empty value after removing trailing slashes).`,
316
+ `${sourceName} must be a valid origin (got empty value after removing trailing slashes).`
317
317
  );
318
318
  }
319
319
  let parsed;
@@ -325,7 +325,7 @@ function requireValidBaseUrl(raw, sourceName) {
325
325
  // Path/search/hash are dropped when composing absolute endpoints against this base.
326
326
  if (parsed.pathname !== "/" || parsed.search || parsed.hash) {
327
327
  throw new CliApiError(
328
- `${sourceName} must be a valid origin with no path (got ${JSON.stringify(raw)}).`,
328
+ `${sourceName} must be a valid origin with no path (got ${JSON.stringify(raw)}).`
329
329
  );
330
330
  }
331
331
  return stripped;
@@ -337,9 +337,7 @@ function parseJson(text, url, response) {
337
337
  } catch {
338
338
  const contentType = response?.headers?.get?.("content-type") || "";
339
339
  const isHtml = /html/i.test(contentType) || /^\s*</.test(text);
340
- const hint = isHtml
341
- ? " The Ornn host may not have this CLI endpoint deployed yet."
342
- : "";
340
+ const hint = isHtml ? " The Ornn host may not have this CLI endpoint deployed yet." : "";
343
341
  throw new CliApiError(`Ornn returned a non-JSON response from ${url}.${hint}`, {
344
342
  status: response?.status,
345
343
  });
@@ -351,26 +349,40 @@ async function handleAuthExpiry({ env, response, hadToken }) {
351
349
  return;
352
350
  }
353
351
  await clearAuthSession({ env });
354
- throw new CliApiError("Your Ornn session has expired or was revoked. Run `ornn login` to sign in again.", {
355
- status: 401,
356
- });
352
+ throw new CliApiError(
353
+ "Your Ornn session has expired or was revoked. Run `ornn login` to sign in again.",
354
+ {
355
+ status: 401,
356
+ }
357
+ );
357
358
  }
358
359
 
359
360
  function detailMessage(detail) {
360
361
  if (typeof detail === "string") {
361
362
  return detail;
362
363
  }
364
+ if (Array.isArray(detail)) {
365
+ return detail
366
+ .map((item) => detailMessage(item))
367
+ .filter(Boolean)
368
+ .join("; ");
369
+ }
363
370
  if (!detail || typeof detail !== "object") {
364
371
  return "";
365
372
  }
366
- if (typeof detail.message === "string") {
367
- return detail.message;
368
- }
369
- if (typeof detail.summary === "string") {
370
- return detail.summary;
371
- }
372
- if (typeof detail.code === "string") {
373
- return detail.code;
373
+ const message = [detail.msg, detail.message, detail.summary, detail.code].find(
374
+ (value) => typeof value === "string" && value.trim()
375
+ );
376
+ const location = Array.isArray(detail.loc)
377
+ ? detail.loc
378
+ .filter((value) => typeof value === "string" || typeof value === "number")
379
+ .map(String)
380
+ .join(".")
381
+ : typeof detail.loc === "string"
382
+ ? detail.loc.trim()
383
+ : "";
384
+ if (message) {
385
+ return location ? `${location}: ${message.trim()}` : message.trim();
374
386
  }
375
- return "";
387
+ return detailMessage(detail.detail ?? detail.errors);
376
388
  }