@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 +72 -24
- package/package.json +1 -1
- package/src/api-client.mjs +33 -21
- package/src/cli.mjs +1597 -492
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 [--
|
|
60
|
-
ornn fleet clean <
|
|
61
|
-
ornn fleet clean <fleet-id> --identity-file <path> --confirm-clean <plan-hash> [--
|
|
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`,
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
`--
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
270
|
-
`~/.config/ornn/fleets/`
|
|
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
package/src/api-client.mjs
CHANGED
|
@@ -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(
|
|
355
|
-
|
|
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
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
}
|