@ornncompute/cli 0.1.8 → 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,7 +56,7 @@ 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 [--policy <path>] [--policy-out <path>] [--ssh-user ubuntu|admin|ornn] [--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
60
  ornn fleet clean <fleet-id> --identity-file <path> --dry-run [--policy <path>] [--policy-out <path>] [--json]
61
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]
@@ -228,7 +228,8 @@ Fleet handoff is an ordered, Compute-backed workflow:
228
228
  - Before first contact, independently verify each host key and add it to the
229
229
  operator's SSH `known_hosts`; fleet SSH uses strict host-key checking and will
230
230
  not trust a key first presented during cleanup.
231
- - `fleet clean ... --dry-run` tries `ubuntu`, `admin`, then `ornn`, binds the
231
+ - `fleet clean ... --dry-run` tries `ubuntu`, `admin`, then `ornn`, or the
232
+ explicit non-root account supplied with `--ssh-user`. It binds the
232
233
  inventory to the host's hardware identity, records the plan in Compute, and
233
234
  writes an editable mode-`0600` policy beside the local fleet manifest (or to
234
235
  `--policy-out`). Inventory covers accounts and orphan homes, authorized SSH
@@ -240,22 +241,33 @@ Fleet handoff is an ordered, Compute-backed workflow:
240
241
  fleet with `--policy`; planning is not terminalized just because a choice is
241
242
  needed. `--source-user` and `--preserve-user` remain legacy user-only
242
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.
243
249
  - `fleet clean <fleet-id> ... --confirm-clean <plan-hash>` is shown only after
244
250
  every choice is resolved. Execution rechecks hardware and resource identity,
245
251
  runs an idempotent per-resource journal, preserves the management SSH key,
246
252
  enforces key-only SSH, and returns exact deleted, preserved, exception, and
247
253
  drive-assurance evidence. `ignore` requires a reason and produces
248
254
  `clean_with_exceptions`; it is never reported as residue-free deletion.
249
- Data-drive actions are selectable: `logical_clean`, `discard`, `sanitize`, or
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
250
258
  `reformat`. The system drive and drives without a unique serial/WWN are
251
- locked to keep/ignore for in-band cleanup. `sanitize` requires NVMe plus
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
252
264
  controller-reported completion. Logical cleanup, discard, and reformat are
253
265
  not certified physical-media erasure, and the system drive requires an
254
266
  out-of-band reimage for that assurance.
255
267
  If the local SSH command times out while a long device operation is still in
256
268
  progress, the recorded run remains resumable; re-run the same
257
269
  `--confirm-clean <plan-hash>` command instead of creating a different plan.
258
- Plans created by a pre-v6 runner must be replanned with `--dry-run` before
270
+ Plans created by a pre-v7 runner must be replanned with `--dry-run` before
259
271
  approval. This keeps newly discovered resources bound to a fresh reviewed
260
272
  hash instead of adapting a legacy account-only plan during execution.
261
273
 
@@ -269,10 +281,15 @@ automatic decision. Do not put secrets in replacement commands or labels; use
269
281
  the platform secret manager or a reviewed secret mount.
270
282
 
271
283
  - `fleet enroll <fleet-id>` refuses nodes without successful cleanup proof,
272
- 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,
273
287
  assigns the IB island, verifies the same management account still has
274
288
  key-based SSH and passwordless sudo, and records the resulting GPU node IDs
275
- 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.
276
293
  - `fleet delete <fleet-id> --confirm-delete <fleet-id>` hard-deletes only an
277
294
  abandoned cleanup plan or an unassigned legacy enrollment record. It saves a
278
295
  local audit manifest first and refuses any Commerce/tenant/deployment link,
@@ -295,8 +312,10 @@ the platform secret manager or a reviewed secret mount.
295
312
  node; Commerce is stamped only after the entire fleet reports deployed. A
296
313
  Commerce reservation can be linked to only one fleet.
297
314
 
298
- Compute Postgres is authoritative; the mode-0600 files under
299
- `~/.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
300
319
  observable residue under the verification scope,” not certified physical-media
301
320
  erasure. Root is never the preserved management account. `--confirm-takeover`
302
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.8",
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
  }