@ornncompute/cli 1.0.25 → 1.0.34
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 +76 -23
- package/package.json +1 -1
- package/src/admin-capacity-reservations.mjs +68 -43
- package/src/admin-enrollment.mjs +14 -13
- package/src/admin-node-lifecycle.mjs +10 -13
- package/src/admin-shell.mjs +146 -12
- package/src/catalog-shell.mjs +58 -45
- package/src/device-auth.mjs +1 -1
- package/src/evidence-commands.mjs +25 -11
- package/src/gpu-catalog.mjs +21 -0
- package/src/identity-file.mjs +28 -0
- package/src/kubernetes-commands.mjs +12 -6
- package/src/machine-reference.mjs +10 -2
- package/src/mount-commands.mjs +4 -2
- package/src/slurm-commands.mjs +11 -0
- package/src/storage-commands.mjs +27 -7
- package/src/vm-commands.mjs +42 -7
- package/vendor/capabilities/capabilities/admin-capacity-reservations.js +31 -61
- package/vendor/capabilities/capabilities/admin-enrollment.js +9 -9
- package/vendor/capabilities/capabilities/admin-nodes-lifecycle.js +6 -6
- package/vendor/capabilities/capabilities/admin.js +7 -7
- package/vendor/capabilities/capabilities/commerce-market.js +330 -0
- package/vendor/capabilities/capabilities/fleet.js +57 -21
- package/vendor/capabilities/capabilities/listings.js +137 -146
- package/vendor/capabilities/capabilities/market-shared.js +44 -0
- package/vendor/capabilities/capabilities/markets.js +449 -0
- package/vendor/capabilities/capabilities/networks.js +7 -0
- package/vendor/capabilities/capabilities/tokens.js +12 -9
- package/vendor/capabilities/capability.js +2 -1
- package/vendor/capabilities/cli-catalog.js +4 -2
- package/vendor/capabilities/cli-migrations.js +16 -43
- package/vendor/capabilities/cli.js +1 -1
- package/vendor/capabilities/spec-catalog.js +26 -3
- package/vendor/capabilities/supplier-directory.js +20 -0
- package/vendor/capabilities/capabilities/capacity-offers.js +0 -368
package/src/mount-commands.mjs
CHANGED
|
@@ -351,7 +351,7 @@ async function reservationMachineCount(runtime, reservationId, signal) {
|
|
|
351
351
|
let count = 0;
|
|
352
352
|
try {
|
|
353
353
|
const rows = candidateRows(
|
|
354
|
-
await runtime.fetch("/v1/
|
|
354
|
+
await runtime.fetch("/v1/orchestrator/storage-targets", {
|
|
355
355
|
method: "GET",
|
|
356
356
|
...(signal ? { signal } : {}),
|
|
357
357
|
})
|
|
@@ -595,7 +595,9 @@ async function candidates(input, runtime) {
|
|
|
595
595
|
"ornn volume-mounts candidates [--limit <1-500>] [--cursor <cursor>]",
|
|
596
596
|
runtime.interaction
|
|
597
597
|
);
|
|
598
|
-
const rows = candidateRows(
|
|
598
|
+
const rows = candidateRows(
|
|
599
|
+
await runtime.fetch("/v1/orchestrator/storage-targets", { method: "GET" })
|
|
600
|
+
);
|
|
599
601
|
const offset = cursorOffset(input.cursor, runtime.interaction);
|
|
600
602
|
const limit = pageLimit(input.limit, runtime.interaction);
|
|
601
603
|
if (offset > rows.length) {
|
package/src/slurm-commands.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn as spawnProcessDefault } from "node:child_process";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
|
|
4
|
+
import { identityFilePermissionProblem } from "./identity-file.mjs";
|
|
4
5
|
import {
|
|
5
6
|
clusterModeConnectCandidates,
|
|
6
7
|
launchClusterMode,
|
|
@@ -203,6 +204,16 @@ export function createSlurmConnectModeHandler({ spawnProcess = spawnProcessDefau
|
|
|
203
204
|
ssh,
|
|
204
205
|
};
|
|
205
206
|
if (runtime.eligibleHuman && !runtime.machine && state.input.print !== true) {
|
|
207
|
+
const identityFile = text(state.input.identityFile);
|
|
208
|
+
const keyProblem = await identityFilePermissionProblem(
|
|
209
|
+
identityFile ? expandHome(identityFile) : null
|
|
210
|
+
);
|
|
211
|
+
if (keyProblem) {
|
|
212
|
+
runtime.usage(keyProblem, {
|
|
213
|
+
code: "identity_file_permissions",
|
|
214
|
+
field: "identityFile",
|
|
215
|
+
});
|
|
216
|
+
}
|
|
206
217
|
const launched = await spawnSsh(login, spawnProcess);
|
|
207
218
|
if (launched.code === 130) {
|
|
208
219
|
return {
|
package/src/storage-commands.mjs
CHANGED
|
@@ -288,12 +288,17 @@ function safeStorageError(error, fallback, { action } = {}) {
|
|
|
288
288
|
? "service_unavailable"
|
|
289
289
|
: "storage_operation_failed");
|
|
290
290
|
const unavailable = code === "service_unavailable";
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
code
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
291
|
+
const ownerMessage = detailOf(error)?.message;
|
|
292
|
+
return new StorageCommandError(
|
|
293
|
+
SAFE_ERROR_MESSAGES[code] ??
|
|
294
|
+
(typeof ownerMessage === "string" && ownerMessage.trim() ? ownerMessage : fallback),
|
|
295
|
+
{
|
|
296
|
+
action,
|
|
297
|
+
code,
|
|
298
|
+
exitCode: unavailable ? 6 : 1,
|
|
299
|
+
retryable: unavailable,
|
|
300
|
+
}
|
|
301
|
+
);
|
|
297
302
|
}
|
|
298
303
|
|
|
299
304
|
export function idempotencyHeaders(input) {
|
|
@@ -828,12 +833,15 @@ async function uploadFile(input, runtime) {
|
|
|
828
833
|
title: "Upload volume file",
|
|
829
834
|
});
|
|
830
835
|
if (declined) return declined;
|
|
836
|
+
const requestHash = createHash("sha256");
|
|
837
|
+
for await (const chunk of createReadStream(localFile)) requestHash.update(chunk);
|
|
838
|
+
const sha256 = requestHash.digest("hex");
|
|
831
839
|
let session;
|
|
832
840
|
try {
|
|
833
841
|
session = await runtime.fetch(
|
|
834
842
|
computeEndpoint(`${STORAGE_PREFIX}/${encodeURIComponent(String(drive.id))}/file-upload-url`),
|
|
835
843
|
{
|
|
836
|
-
body: { content_type: contentType, path: remotePath, size_bytes: fileInfo.size },
|
|
844
|
+
body: { content_type: contentType, path: remotePath, size_bytes: fileInfo.size, sha256 },
|
|
837
845
|
headers: idempotencyHeaders(input),
|
|
838
846
|
method: "POST",
|
|
839
847
|
}
|
|
@@ -843,6 +851,18 @@ async function uploadFile(input, runtime) {
|
|
|
843
851
|
}
|
|
844
852
|
const uploadId =
|
|
845
853
|
typeof session?.upload_id === "string" ? safeIdentifier(session.upload_id) : null;
|
|
854
|
+
if (uploadId && session.state === "complete") {
|
|
855
|
+
return runtime.interaction.result({
|
|
856
|
+
data: {
|
|
857
|
+
file: safeFile({ path: remotePath, size_bytes: fileInfo.size }),
|
|
858
|
+
integrity: { sha256 },
|
|
859
|
+
upload_id: uploadId,
|
|
860
|
+
volume: safeVolume(drive),
|
|
861
|
+
},
|
|
862
|
+
outcome: { class: "ready", phase: "uploaded" },
|
|
863
|
+
resume: { kind: "volume-upload", ref: uploadId },
|
|
864
|
+
});
|
|
865
|
+
}
|
|
846
866
|
if (!uploadId || typeof session?.upload_url !== "string") {
|
|
847
867
|
if (uploadId) await cancelUpload(runtime, String(drive.id), uploadId);
|
|
848
868
|
throw new StorageCommandError("Ornn returned an invalid upload session.", {
|
package/src/vm-commands.mjs
CHANGED
|
@@ -3,13 +3,14 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
import { readFile } from "node:fs/promises";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { isIP } from "node:net";
|
|
6
|
+
import { identityFilePermissionProblem } from "./identity-file.mjs";
|
|
6
7
|
import { isRecoverableWaitFailure } from "./interaction-runtime.mjs";
|
|
7
8
|
import { resolvedMountPath } from "./mount-commands.mjs";
|
|
8
9
|
import {
|
|
9
10
|
isReferenceMiss,
|
|
10
11
|
listActiveDirectReservations,
|
|
11
12
|
listOwnedActiveMachines,
|
|
12
|
-
|
|
13
|
+
reservationLabeler,
|
|
13
14
|
resolveMachineReference,
|
|
14
15
|
} from "./machine-reference.mjs";
|
|
15
16
|
|
|
@@ -513,6 +514,7 @@ async function resolveReservation(context, input, interaction, expectedMode = nu
|
|
|
513
514
|
}
|
|
514
515
|
let ref = resourceId(candidates[0]);
|
|
515
516
|
if (candidates.length > 1) {
|
|
517
|
+
const reservationLabel = await reservationLabeler(context.fetch);
|
|
516
518
|
ref = await interaction.ask({
|
|
517
519
|
label: "Capacity reservation",
|
|
518
520
|
choices: candidates.map((reservation) => ({
|
|
@@ -534,9 +536,21 @@ async function resolveReservation(context, input, interaction, expectedMode = nu
|
|
|
534
536
|
}
|
|
535
537
|
|
|
536
538
|
async function fetchOrganizationKeys(context) {
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
539
|
+
const keys = [];
|
|
540
|
+
const seen = new Set();
|
|
541
|
+
let cursor = null;
|
|
542
|
+
do {
|
|
543
|
+
const query = new URLSearchParams({ status: "active", limit: "500" });
|
|
544
|
+
if (cursor) query.set("cursor", cursor);
|
|
545
|
+
const page = await context.fetch(
|
|
546
|
+
`/v1/workflows/compute/nodes/organizations/me/ssh-keys?${query}`
|
|
547
|
+
);
|
|
548
|
+
keys.push(...keysFromPayload(page));
|
|
549
|
+
cursor = page.next_cursor ?? null;
|
|
550
|
+
if (cursor && seen.has(cursor)) throw new Error("SSH key pagination did not advance.");
|
|
551
|
+
if (cursor) seen.add(cursor);
|
|
552
|
+
} while (cursor);
|
|
553
|
+
return activeKeys(keys);
|
|
540
554
|
}
|
|
541
555
|
|
|
542
556
|
export function keyInputShapePreflight(input, interaction) {
|
|
@@ -1071,6 +1085,7 @@ export const vmLaunchModeHandler = {
|
|
|
1071
1085
|
const reviewedRelationshipSnapshot = JSON.stringify(reviewedRelationships);
|
|
1072
1086
|
const waitByDefault = interaction.eligibleHuman && !interaction.machine;
|
|
1073
1087
|
const shouldWait = input.noWait ? false : input.wait ? true : waitByDefault;
|
|
1088
|
+
const reservationLabel = await reservationLabeler(context.fetch);
|
|
1074
1089
|
const reviewAccepted = await interaction.review({
|
|
1075
1090
|
title: "VM launch review",
|
|
1076
1091
|
terms: [
|
|
@@ -1828,6 +1843,7 @@ async function launchDurableBareMetalWorkspace({
|
|
|
1828
1843
|
: input.wait
|
|
1829
1844
|
? true
|
|
1830
1845
|
: interaction.eligibleHuman && !interaction.machine;
|
|
1846
|
+
const reservationLabel = await reservationLabeler(context.fetch);
|
|
1831
1847
|
const accepted = await interaction.review({
|
|
1832
1848
|
title: "Durable Bare Metal workspace review",
|
|
1833
1849
|
terms: [
|
|
@@ -2333,6 +2349,7 @@ export const bareMetalLaunchModeHandler = {
|
|
|
2333
2349
|
? true
|
|
2334
2350
|
: interaction.eligibleHuman && !interaction.machine;
|
|
2335
2351
|
const allocation = reservationAllocationContext(reservation);
|
|
2352
|
+
const reservationLabel = await reservationLabeler(context.fetch);
|
|
2336
2353
|
const reviewAccepted = await interaction.review({
|
|
2337
2354
|
title: "Bare Metal launch review",
|
|
2338
2355
|
terms: [
|
|
@@ -2857,6 +2874,15 @@ function createDirectMachineConnectModeHandler({
|
|
|
2857
2874
|
ssh_command: command,
|
|
2858
2875
|
};
|
|
2859
2876
|
if (interaction.eligibleHuman && !interaction.machine && input.print !== true) {
|
|
2877
|
+
const keyProblem = await identityFilePermissionProblem(
|
|
2878
|
+
invocation.raw || !text(input.identityFile) ? null : expandPath(input.identityFile)
|
|
2879
|
+
);
|
|
2880
|
+
if (keyProblem) {
|
|
2881
|
+
interaction.usage(keyProblem, {
|
|
2882
|
+
code: "identity_file_permissions",
|
|
2883
|
+
field: "identityFile",
|
|
2884
|
+
});
|
|
2885
|
+
}
|
|
2860
2886
|
interaction.stderr?.write(
|
|
2861
2887
|
`Opening SSH to ${displayName}.\nRun: ${command}\nWaiting for SSH to exit. Press Ctrl-C to stop only local SSH; the remote machine keeps running.\n`
|
|
2862
2888
|
);
|
|
@@ -3355,6 +3381,7 @@ async function guidedSwitchReservation(context, interaction) {
|
|
|
3355
3381
|
}
|
|
3356
3382
|
let reference = resourceId(reservations[0]);
|
|
3357
3383
|
if (reservations.length > 1) {
|
|
3384
|
+
const reservationLabel = await reservationLabeler(context.fetch);
|
|
3358
3385
|
reference = await interaction.ask({
|
|
3359
3386
|
label: "Capacity reservation",
|
|
3360
3387
|
choices: reservations.map((reservation) => ({
|
|
@@ -3527,6 +3554,7 @@ async function switchMachine(input, context, interaction) {
|
|
|
3527
3554
|
reservationId,
|
|
3528
3555
|
interaction
|
|
3529
3556
|
);
|
|
3557
|
+
const reservationLabel = await reservationLabeler(context.fetch);
|
|
3530
3558
|
if (
|
|
3531
3559
|
reservationLabel(currentReservation) !== reservationLabel(reservation) ||
|
|
3532
3560
|
directMachineSnapshot(currentMachines, currentRelationships, currentKeys) !== snapshot
|
|
@@ -3988,10 +4016,17 @@ function reservationPosition(input, interaction) {
|
|
|
3988
4016
|
export async function executeSshKeyCommand(command, input, context, interaction) {
|
|
3989
4017
|
if (command === "list") {
|
|
3990
4018
|
positional(input, 0, "ornn ssh-keys list [--limit <1-500>] [--cursor <cursor>]", interaction);
|
|
3991
|
-
const
|
|
3992
|
-
|
|
4019
|
+
const limit = input.limit ?? 50;
|
|
4020
|
+
const query = new URLSearchParams({ status: "active", limit: String(limit) });
|
|
4021
|
+
if (text(input.cursor)) query.set("cursor", input.cursor);
|
|
4022
|
+
const payload = await context.fetch(
|
|
4023
|
+
`/v1/workflows/compute/nodes/organizations/me/ssh-keys?${query}`
|
|
3993
4024
|
);
|
|
3994
|
-
const
|
|
4025
|
+
const items = activeKeys(keysFromPayload(payload));
|
|
4026
|
+
const page = {
|
|
4027
|
+
items,
|
|
4028
|
+
page: { limit, returned: items.length, next_cursor: payload.next_cursor ?? null },
|
|
4029
|
+
};
|
|
3995
4030
|
return interaction.result({
|
|
3996
4031
|
data: { ssh_keys: page.items.map(projectKey) },
|
|
3997
4032
|
page: page.page,
|
|
@@ -16,17 +16,20 @@ function adminHelp(command, outcome, human, automation, review) {
|
|
|
16
16
|
const capacityOfferCreateInput = z.object({
|
|
17
17
|
gpu_type: z.string().min(1),
|
|
18
18
|
node_count: z.number().int().positive(),
|
|
19
|
-
gpus_per_node: z.number().int().
|
|
19
|
+
gpus_per_node: z.number().int().min(1).max(8),
|
|
20
20
|
site_nickname: z.string().min(1),
|
|
21
21
|
available_start_at: z.string().min(1),
|
|
22
22
|
available_end_at: z.string().min(1),
|
|
23
|
-
buy_now_price_per_gpu_hour: z.number().
|
|
23
|
+
buy_now_price_per_gpu_hour: z.number().positive(),
|
|
24
|
+
cost_per_gpu_hour: z.number().positive().optional(),
|
|
25
|
+
down_payment_cents: z.number().int().nonnegative().optional(),
|
|
26
|
+
access_mode: z.enum(["bare-metal", "virtual-machine"]).optional(),
|
|
24
27
|
});
|
|
25
28
|
export const adminCapacityOffersCreateCapability = {
|
|
26
29
|
...createListingCapability,
|
|
27
30
|
cli: {
|
|
28
31
|
...createListingCapability.cli,
|
|
29
|
-
vnext: adminHelp(["admin", "capacity-offers", "create"], "Create one
|
|
32
|
+
vnext: adminHelp(["admin", "capacity-offers", "create"], "Create one primary listing from explicit commercial terms.", "ornn admin capacity-offers create --gpu GPU --nodes COUNT --gpus-per-node COUNT --site SITE --starts-at TIME --ends-at TIME --price-per-gpu-hour USD [--supplier-price-per-gpu-hour USD] [--down-payment-cents CENTS] [--access-mode bare-metal|virtual-machine]", "Not supported in machine or redirected execution because offer creation has no durable replay identity.", "GPU, site, availability window, supplier price, spread, and down payment are normalized before default-no consent."),
|
|
30
33
|
},
|
|
31
34
|
input: createListingCapability.input,
|
|
32
35
|
execute: async (ctx, input) => {
|
|
@@ -35,89 +38,56 @@ export const adminCapacityOffersCreateCapability = {
|
|
|
35
38
|
const result = await createListingCapability.execute(ctx, capacityOfferCreateInput.parse(input));
|
|
36
39
|
if (!ctx.confirmed || object(result).ok === false)
|
|
37
40
|
return result;
|
|
38
|
-
return {
|
|
41
|
+
return { listing: result };
|
|
39
42
|
},
|
|
40
43
|
};
|
|
41
|
-
const
|
|
44
|
+
const MICRO_USD = 1_000_000;
|
|
45
|
+
export const reservationCreateInput = z.object({
|
|
42
46
|
organization_id: z.string().uuid(),
|
|
43
47
|
listing_id: z.string().uuid(),
|
|
44
|
-
node_count: z.number().int().positive(),
|
|
45
48
|
start_at: z.string().min(1),
|
|
46
49
|
end_at: z.string().min(1),
|
|
47
|
-
price_per_gpu_hour: z
|
|
50
|
+
price_per_gpu_hour: z
|
|
51
|
+
.string()
|
|
52
|
+
.regex(/^\d+(?:\.\d{1,6})?$/)
|
|
53
|
+
.describe("USD per GPU-hour, for example 2.50."),
|
|
54
|
+
down_payment_cents: z.number().int().nonnegative().optional(),
|
|
55
|
+
notes: z.string().max(2000).optional(),
|
|
48
56
|
});
|
|
49
57
|
function object(value) {
|
|
50
58
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
51
59
|
? value
|
|
52
60
|
: {};
|
|
53
61
|
}
|
|
54
|
-
function field(value, ...keys) {
|
|
55
|
-
for (const key of keys) {
|
|
56
|
-
if (value[key] !== undefined)
|
|
57
|
-
return value[key];
|
|
58
|
-
}
|
|
59
|
-
return undefined;
|
|
60
|
-
}
|
|
61
|
-
function normalizeCapacityOffer(value) {
|
|
62
|
-
const row = object(object(value).capacity_offer ?? object(value).listing ?? value);
|
|
63
|
-
return {
|
|
64
|
-
capacity_offer_id: field(row, "capacity_offer_id", "listing_id", "listingId", "id"),
|
|
65
|
-
gpu: field(row, "gpu", "gpu_type", "gpuType"),
|
|
66
|
-
node_count: field(row, "node_count", "nodeCount", "capacity_nodes", "capacityNodes"),
|
|
67
|
-
gpus_per_node: field(row, "gpus_per_node", "gpusPerNode"),
|
|
68
|
-
site: field(row, "site", "site_nickname", "siteNickname", "facility"),
|
|
69
|
-
starts_at: field(row, "starts_at", "start_at", "startAt", "available_start_at"),
|
|
70
|
-
ends_at: field(row, "ends_at", "end_at", "endAt", "available_end_at"),
|
|
71
|
-
price_per_gpu_hour: field(row, "price_per_gpu_hour", "buy_now_price_per_gpu_hour", "buyNowRatePerGpuHour"),
|
|
72
|
-
status: field(row, "status", "state"),
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
function normalizeReservation(value) {
|
|
76
|
-
const row = object(value);
|
|
77
|
-
return {
|
|
78
|
-
reservation_id: field(row, "reservation_id", "reservationId", "id"),
|
|
79
|
-
listing_id: field(row, "listing_id", "listingId"),
|
|
80
|
-
organization_id: field(row, "organization_id", "organizationId", "tenant_id", "tenantId"),
|
|
81
|
-
node_count: field(row, "node_count", "nodeCount"),
|
|
82
|
-
start_at: field(row, "start_at", "startAt"),
|
|
83
|
-
end_at: field(row, "end_at", "endAt"),
|
|
84
|
-
status: field(row, "status"),
|
|
85
|
-
price_per_gpu_hour: field(row, "price_per_gpu_hour", "pricePerGpuHour", "pricePerGpuHr"),
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
62
|
export const adminReservationsCreateCapability = defineCapability({
|
|
89
63
|
id: "reservations.create",
|
|
90
64
|
domain: "reservations",
|
|
91
65
|
roles: ["admin"],
|
|
92
|
-
description: "Create one
|
|
66
|
+
description: "Create one schedule reservation on a listing from explicit, reviewed commercial terms (admin). The reservation covers the listing's nodes for the window.",
|
|
93
67
|
mutation: "preview-confirm",
|
|
94
|
-
http: { method: "POST", path: "/
|
|
68
|
+
http: { method: "POST", path: "/commerce/reservations" },
|
|
95
69
|
cli: {
|
|
96
70
|
command: ["admin", "reservations", "create"],
|
|
97
|
-
vnext: adminHelp(["admin", "reservations", "create"], "Create one
|
|
71
|
+
vnext: adminHelp(["admin", "reservations", "create"], "Create one schedule reservation from explicit commercial terms.", "ornn admin reservations create --organization ID --capacity-offer ID --starts-at TIME --ends-at TIME --price-per-gpu-hour USD [--down-payment-cents CENTS]", "Not supported in machine or redirected execution because reservation creation has no durable replay identity.", "Organization, listing, dates, price, and down payment are normalized before default-no consent."),
|
|
98
72
|
},
|
|
99
|
-
mcp: false,
|
|
100
73
|
slack: false,
|
|
101
74
|
input: reservationCreateInput,
|
|
102
75
|
execute: async (ctx, input) => {
|
|
103
76
|
const parsed = reservationCreateInput.parse(input);
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
price_per_gpu_hr: parsed.price_per_gpu_hour,
|
|
115
|
-
},
|
|
116
|
-
});
|
|
117
|
-
const body = object(payload);
|
|
118
|
-
return {
|
|
119
|
-
reservation: normalizeReservation(body.reservation ?? body),
|
|
77
|
+
const body = {
|
|
78
|
+
listingId: parsed.listing_id,
|
|
79
|
+
organizationId: parsed.organization_id,
|
|
80
|
+
startsAt: parsed.start_at,
|
|
81
|
+
endsAt: parsed.end_at,
|
|
82
|
+
pricePerGpuHourMicroUsd: Math.round(Number(parsed.price_per_gpu_hour) * MICRO_USD),
|
|
83
|
+
...(parsed.down_payment_cents !== undefined
|
|
84
|
+
? { downPaymentCents: parsed.down_payment_cents }
|
|
85
|
+
: {}),
|
|
86
|
+
...(parsed.notes ? { notes: parsed.notes } : {}),
|
|
120
87
|
};
|
|
88
|
+
if (!ctx.confirmed)
|
|
89
|
+
return { preview: body };
|
|
90
|
+
return { reservation: await ctx.fetch("/commerce/reservations", { method: "POST", body }) };
|
|
121
91
|
},
|
|
122
92
|
});
|
|
123
93
|
export const adminCapacityReservationCapabilities = [
|
|
@@ -44,7 +44,7 @@ export const adminEnrollmentTokensListCapability = {
|
|
|
44
44
|
};
|
|
45
45
|
},
|
|
46
46
|
};
|
|
47
|
-
export const adminEnrollmentTokenCreateCapability = withVnextBinding(tokensCreateCapability, adminHelp(["admin", "enrollment-tokens", "create"], "Create one expiring enrollment token through an explicit secret export.", "ornn admin enrollment-tokens create --
|
|
47
|
+
export const adminEnrollmentTokenCreateCapability = withVnextBinding(tokensCreateCapability, adminHelp(["admin", "enrollment-tokens", "create"], "Create one expiring enrollment token through an explicit secret export.", "ornn admin enrollment-tokens create --supplier UUID --expires-in SECONDS (--print | --output PATH) [--overwrite] [--yes]", "Unsupported: token creation requires an eligible human terminal.", ["canonical supplier organization UUID"], "Review the supplier, expiry, and secret destination before minting the token."));
|
|
48
48
|
const tokenRevokeInput = z.object({
|
|
49
49
|
token_id: UUID,
|
|
50
50
|
expected_created_at: nonEmpty.optional(),
|
|
@@ -70,7 +70,7 @@ function tokenMetadata(value) {
|
|
|
70
70
|
return null;
|
|
71
71
|
return {
|
|
72
72
|
id: row.id,
|
|
73
|
-
|
|
73
|
+
organization_id: typeof row.organization_id === "string" ? row.organization_id : null,
|
|
74
74
|
status: typeof row.status === "string" ? row.status : "unknown",
|
|
75
75
|
expires_at: typeof row.expires_at === "string" ? row.expires_at : null,
|
|
76
76
|
created_at: typeof row.created_at === "string" ? row.created_at : null,
|
|
@@ -145,7 +145,7 @@ export const adminEnrollmentTokenRevokeCapability = defineCapability({
|
|
|
145
145
|
return {
|
|
146
146
|
id: parsed.token_id,
|
|
147
147
|
created_at: current.target.created_at,
|
|
148
|
-
|
|
148
|
+
organization_id: current.target.organization_id,
|
|
149
149
|
expires_at: current.target.expires_at,
|
|
150
150
|
status: "revoked",
|
|
151
151
|
reconciled: false,
|
|
@@ -156,7 +156,7 @@ export const adminEnrollmentTokenRevokeCapability = defineCapability({
|
|
|
156
156
|
const nodeEnrollInput = z.object({
|
|
157
157
|
node_id: UUID,
|
|
158
158
|
host: nonEmpty,
|
|
159
|
-
|
|
159
|
+
organization_id: UUID,
|
|
160
160
|
facility_id: UUID,
|
|
161
161
|
filter: z.string().optional(),
|
|
162
162
|
identity_file: nonEmpty,
|
|
@@ -185,12 +185,12 @@ export const adminNodeEnrollCapability = defineCapability({
|
|
|
185
185
|
id: "nodes.enroll",
|
|
186
186
|
domain: "fleet",
|
|
187
187
|
roles: ["admin"],
|
|
188
|
-
description: "Enroll one explicit node over SSH with an explicit
|
|
188
|
+
description: "Enroll one explicit node over SSH with an explicit supplier and facility assignment.",
|
|
189
189
|
mutation: "preview-confirm",
|
|
190
190
|
http: { method: "POST", path: "/internal/remote" },
|
|
191
191
|
cli: {
|
|
192
192
|
command: ["admin", "nodes", "enroll"],
|
|
193
|
-
vnext: adminHelp(["admin", "nodes", "enroll"], "Install Ornn management on one explicitly assigned node.", "ornn admin nodes enroll <node-id> --host HOST --
|
|
193
|
+
vnext: adminHelp(["admin", "nodes", "enroll"], "Install Ornn management on one explicitly assigned node.", "ornn admin nodes enroll <node-id> --host HOST --supplier UUID --facility UUID [--filter COMPARTMENT_IDS] --identity-file PATH --token-ttl SECONDS --replace-existing-management [--verbose] [--yes]", "Unsupported: remote enrollment requires an eligible human terminal.", ["canonical node UUID", "canonical supplier organization UUID", "canonical facility UUID"], "Review node identity, SSH target, assignment, compartment filter, and replacement of existing management."),
|
|
194
194
|
},
|
|
195
195
|
mcp: false,
|
|
196
196
|
slack: false,
|
|
@@ -201,7 +201,7 @@ export const adminNodeEnrollCapability = defineCapability({
|
|
|
201
201
|
return {
|
|
202
202
|
node_id: parsed.node_id,
|
|
203
203
|
host: parsed.host,
|
|
204
|
-
|
|
204
|
+
organization_id: parsed.organization_id,
|
|
205
205
|
facility_id: parsed.facility_id,
|
|
206
206
|
filter: parsed.filter,
|
|
207
207
|
expires_in_seconds: parsed.expires_in_seconds,
|
|
@@ -214,7 +214,7 @@ export const adminNodeEnrollCapability = defineCapability({
|
|
|
214
214
|
const minted = (await ctx.fetch("/enrollment-tokens", {
|
|
215
215
|
method: "POST",
|
|
216
216
|
body: {
|
|
217
|
-
|
|
217
|
+
organization_id: parsed.organization_id,
|
|
218
218
|
expires_in_seconds: parsed.expires_in_seconds,
|
|
219
219
|
},
|
|
220
220
|
}));
|
|
@@ -248,7 +248,7 @@ export const adminNodeEnrollCapability = defineCapability({
|
|
|
248
248
|
return {
|
|
249
249
|
node_id: parsed.node_id,
|
|
250
250
|
host: parsed.host,
|
|
251
|
-
|
|
251
|
+
organization_id: parsed.organization_id,
|
|
252
252
|
facility_id: parsed.facility_id,
|
|
253
253
|
filter: parsed.filter,
|
|
254
254
|
management: "replaced",
|
|
@@ -83,7 +83,7 @@ export const adminNodeRebootCapability = defineCapability({
|
|
|
83
83
|
http: { method: "POST", path: "/nodes/{id}/reboot" },
|
|
84
84
|
cli: {
|
|
85
85
|
command: ["admin", "nodes", "reboot"],
|
|
86
|
-
vnext: adminLifecycleHelp(["admin", "nodes", "reboot"], "Reboot one exact live machine while preserving local data and access configuration.", "ornn admin nodes reboot <node-ref> [--yes] [--no-wait] [--timeout SECONDS]", "Unsupported: reboot requires an eligible human terminal.", "Node and live-machine identities,
|
|
86
|
+
vnext: adminLifecycleHelp(["admin", "nodes", "reboot"], "Reboot one exact live machine while preserving local data and access configuration.", "ornn admin nodes reboot <node-ref> [--yes] [--no-wait] [--timeout SECONDS]", "Unsupported: reboot requires an eligible human terminal.", "Node and live-machine identities, supplier, facility, capacity assignment, and preserved access."),
|
|
87
87
|
},
|
|
88
88
|
mcp: false,
|
|
89
89
|
slack: false,
|
|
@@ -99,7 +99,7 @@ export const adminNodeHardResetCapability = defineCapability({
|
|
|
99
99
|
http: { method: "POST", path: "/nodes/{id}/reboot" },
|
|
100
100
|
cli: {
|
|
101
101
|
command: ["admin", "nodes", "hard-reset"],
|
|
102
|
-
vnext: adminLifecycleHelp(["admin", "nodes", "hard-reset"], "Erase local tenant data and hard reset one exact live machine.", "ornn admin nodes hard-reset <node-ref> --erase-local-data [--yes] [--no-wait] [--timeout SECONDS]", "Unsupported: hard reset requires an eligible human terminal.", "Node and live-machine identities,
|
|
102
|
+
vnext: adminLifecycleHelp(["admin", "nodes", "hard-reset"], "Erase local tenant data and hard reset one exact live machine.", "ornn admin nodes hard-reset <node-ref> --erase-local-data [--yes] [--no-wait] [--timeout SECONDS]", "Unsupported: hard reset requires an eligible human terminal.", "Node and live-machine identities, supplier, facility, capacity assignment, erased local data, and restored keys."),
|
|
103
103
|
},
|
|
104
104
|
mcp: false,
|
|
105
105
|
slack: false,
|
|
@@ -115,7 +115,7 @@ export const adminNodeSuspendManagementCapability = defineCapability({
|
|
|
115
115
|
http: { method: "POST", path: "/internal/nodes/{id}/off-grid" },
|
|
116
116
|
cli: {
|
|
117
117
|
command: ["admin", "nodes", "suspend-management"],
|
|
118
|
-
vnext: adminLifecycleHelp(["admin", "nodes", "suspend-management"], "Remove Ornn management and managed access from one powered-on node.", "ornn admin nodes suspend-management <node-ref> [--reason TEXT] [--yes] [--no-wait]", "ornn admin nodes suspend-management <node-id> --yes --idempotency-key KEY --format json", "Node,
|
|
118
|
+
vnext: adminLifecycleHelp(["admin", "nodes", "suspend-management"], "Remove Ornn management and managed access from one powered-on node.", "ornn admin nodes suspend-management <node-ref> [--reason TEXT] [--yes] [--no-wait]", "ornn admin nodes suspend-management <node-id> --yes --idempotency-key KEY --format json", "Node, supplier, facility, capacity assignment, management removal, and managed-access loss."),
|
|
119
119
|
},
|
|
120
120
|
mcp: false,
|
|
121
121
|
slack: false,
|
|
@@ -145,7 +145,7 @@ export const adminNodeResumeManagementCapability = defineCapability({
|
|
|
145
145
|
http: { method: "POST", path: "/internal/nodes/{id}/on-grid" },
|
|
146
146
|
cli: {
|
|
147
147
|
command: ["admin", "nodes", "resume-management"],
|
|
148
|
-
vnext: adminLifecycleHelp(["admin", "nodes", "resume-management"], "Restore Ornn management and managed access on one off-grid node.", "ornn admin nodes resume-management <node-ref> [--yes] [--no-wait]", "ornn admin nodes resume-management <node-id> --yes --idempotency-key KEY --format json", "Node,
|
|
148
|
+
vnext: adminLifecycleHelp(["admin", "nodes", "resume-management"], "Restore Ornn management and managed access on one off-grid node.", "ornn admin nodes resume-management <node-ref> [--yes] [--no-wait]", "ornn admin nodes resume-management <node-id> --yes --idempotency-key KEY --format json", "Node, supplier, facility, capacity assignment, remote re-enrollment, and restored managed access."),
|
|
149
149
|
},
|
|
150
150
|
mcp: false,
|
|
151
151
|
slack: false,
|
|
@@ -173,7 +173,7 @@ export const adminNodeTerminateCapability = defineCapability({
|
|
|
173
173
|
http: { method: "POST", path: "/internal/nodes/{id}/terminate" },
|
|
174
174
|
cli: {
|
|
175
175
|
command: ["admin", "nodes", "terminate"],
|
|
176
|
-
vnext: adminLifecycleHelp(["admin", "nodes", "terminate"], "End managed access and permanently delete one exact node record.", "ornn admin nodes terminate <node-ref> --delete-node-record [--skip-cleanup] [--reason TEXT] [--yes]", "ornn admin nodes terminate <node-id> --delete-node-record --yes --idempotency-key KEY --format json", "Node,
|
|
176
|
+
vnext: adminLifecycleHelp(["admin", "nodes", "terminate"], "End managed access and permanently delete one exact node record.", "ornn admin nodes terminate <node-ref> --delete-node-record [--skip-cleanup] [--reason TEXT] [--yes]", "ornn admin nodes terminate <node-id> --delete-node-record --yes --idempotency-key KEY --format json", "Node, supplier, facility, reservation/capacity, host cleanup, node-record deletion, and access loss."),
|
|
177
177
|
},
|
|
178
178
|
mcp: false,
|
|
179
179
|
slack: false,
|
|
@@ -204,7 +204,7 @@ export const adminNodeAssignCapability = defineCapability({
|
|
|
204
204
|
http: { method: "POST", path: "/internal/nodes/{id}/assign" },
|
|
205
205
|
cli: {
|
|
206
206
|
command: ["admin", "nodes", "assign"],
|
|
207
|
-
vnext: adminLifecycleHelp(["admin", "nodes", "assign"], "Assign one exact node to one exact capacity reservation.", "ornn admin nodes assign <node-ref> --reservation <reservation-id> [--yes] [--no-wait]", "ornn admin nodes assign <node-id> --reservation <reservation-id> --yes --idempotency-key KEY --format json", "Node,
|
|
207
|
+
vnext: adminLifecycleHelp(["admin", "nodes", "assign"], "Assign one exact node to one exact capacity reservation.", "ornn admin nodes assign <node-ref> --reservation <reservation-id> [--yes] [--no-wait]", "ornn admin nodes assign <node-id> --reservation <reservation-id> --yes --idempotency-key KEY --format json", "Node, supplier, facility, previous and requested capacity assignments, and tenant access change."),
|
|
208
208
|
},
|
|
209
209
|
mcp: false,
|
|
210
210
|
slack: false,
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { defineCapability } from "../capability.js";
|
|
3
|
-
import { listFacilitiesCapability, listNodesCapability,
|
|
3
|
+
import { listFacilitiesCapability, listNodesCapability, listSuppliersCapability } from "./fleet.js";
|
|
4
4
|
import { logsLatestCapability, telemetryLatestCapability } from "./observability.js";
|
|
5
5
|
import { tokensListCapability } from "./tokens.js";
|
|
6
6
|
import { usersQueryCapability } from "./users.js";
|
|
7
7
|
import { adminCapacityOffersCreateCapability } from "./admin-capacity-reservations.js";
|
|
8
8
|
export const ADMIN_CLI_ROOTS = [
|
|
9
9
|
{ name: "users" },
|
|
10
|
-
{ name: "
|
|
10
|
+
{ name: "suppliers" },
|
|
11
11
|
{ name: "facilities" },
|
|
12
12
|
{ name: "nodes" },
|
|
13
13
|
{ name: "enrollment-tokens" },
|
|
@@ -37,9 +37,9 @@ function withAdminBinding(capability, help) {
|
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
39
|
export const adminUsersSearchCapability = withAdminBinding(usersQueryCapability, adminHelp(["admin", "users", "search"], "Search the staff-authorized user directory.", "ornn admin users search [--name NAME] [--email EMAIL] [--organization NAME] [--limit 50] [--cursor CURSOR]", "ornn admin users search --email EMAIL --limit 50 [--cursor CURSOR] --format json"));
|
|
40
|
-
export const
|
|
41
|
-
export const adminFacilitiesListCapability = withAdminBinding(listFacilitiesCapability, adminHelp(["admin", "facilities", "list"], "List facilities visible to staff.", "ornn admin facilities list [--
|
|
42
|
-
export const adminNodesListCapability = withAdminBinding(listNodesCapability, adminHelp(["admin", "nodes", "list"], "List enrolled, ghost, and off-grid nodes.", "ornn admin nodes list [--
|
|
40
|
+
export const adminSuppliersListCapability = withAdminBinding(listSuppliersCapability, adminHelp(["admin", "suppliers", "list"], "List supplier organizations visible to staff.", "ornn admin suppliers list [--limit 50] [--cursor CURSOR]", "ornn admin suppliers list --limit 50 --format json"));
|
|
41
|
+
export const adminFacilitiesListCapability = withAdminBinding(listFacilitiesCapability, adminHelp(["admin", "facilities", "list"], "List facilities visible to staff.", "ornn admin facilities list [--supplier UUID] [--limit 50] [--cursor CURSOR]", "ornn admin facilities list --limit 50 --format json"));
|
|
42
|
+
export const adminNodesListCapability = withAdminBinding(listNodesCapability, adminHelp(["admin", "nodes", "list"], "List enrolled, ghost, and off-grid nodes.", "ornn admin nodes list [--supplier UUID] [--facility FACILITY] [--status STATUS] [--email EMAIL] [--limit 50] [--cursor CURSOR]", "ornn admin nodes list --limit 50 [--cursor CURSOR] --format json"));
|
|
43
43
|
const nodeInput = z.object({ node_id: z.string().min(1) });
|
|
44
44
|
export const adminNodeHealthCapability = defineCapability({
|
|
45
45
|
id: "nodes.health",
|
|
@@ -65,7 +65,7 @@ export const adminNodeLogsCapability = withAdminBinding(logsLatestCapability, ad
|
|
|
65
65
|
export const adminEnrollmentTokensListCapability = withAdminBinding(tokensListCapability, adminHelp(["admin", "enrollment-tokens", "list"], "List enrollment-token metadata without token bytes.", "ornn admin enrollment-tokens list [--limit 50] [--cursor CURSOR]", "ornn admin enrollment-tokens list --limit 50 --format json"));
|
|
66
66
|
export const adminCapabilities = [
|
|
67
67
|
adminUsersSearchCapability,
|
|
68
|
-
|
|
68
|
+
adminSuppliersListCapability,
|
|
69
69
|
adminFacilitiesListCapability,
|
|
70
70
|
adminNodesListCapability,
|
|
71
71
|
adminNodeHealthCapability,
|
|
@@ -78,7 +78,7 @@ const replacements = {
|
|
|
78
78
|
"facilities.list": adminFacilitiesListCapability,
|
|
79
79
|
"logs.latest": adminNodeLogsCapability,
|
|
80
80
|
"nodes.list": adminNodesListCapability,
|
|
81
|
-
"
|
|
81
|
+
"supplier.list": adminSuppliersListCapability,
|
|
82
82
|
"telemetry.latest": adminNodeMetricsCapability,
|
|
83
83
|
"tokens.list": adminEnrollmentTokensListCapability,
|
|
84
84
|
"users.query": adminUsersSearchCapability,
|