@ornncompute/cli 0.2.23 → 1.0.25

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.
Files changed (85) hide show
  1. package/README.md +304 -420
  2. package/package.json +12 -6
  3. package/src/admin-capacity-reservations.mjs +280 -0
  4. package/src/admin-command-handlers.mjs +57 -0
  5. package/src/admin-enrollment.mjs +680 -0
  6. package/src/admin-mutations.mjs +187 -0
  7. package/src/admin-node-diagnostics.mjs +412 -0
  8. package/src/admin-node-lifecycle.mjs +908 -0
  9. package/src/admin-shell.mjs +322 -0
  10. package/src/api-client.mjs +81 -28
  11. package/src/atomic-output.mjs +112 -0
  12. package/src/auth-store.mjs +182 -11
  13. package/src/capability-catalog.mjs +7 -0
  14. package/src/catalog-shell.mjs +1350 -0
  15. package/src/cli.mjs +123 -10266
  16. package/src/connect-mode-handlers.mjs +170 -0
  17. package/src/device-auth.mjs +178 -61
  18. package/src/evidence-commands.mjs +1340 -0
  19. package/src/external-bucket-commands.mjs +1374 -0
  20. package/src/interaction-runtime.mjs +310 -0
  21. package/src/kubernetes-commands.mjs +2229 -0
  22. package/src/launch-mode-handlers.mjs +119 -0
  23. package/src/machine-reference.mjs +277 -0
  24. package/src/mount-commands.mjs +1146 -0
  25. package/src/session-commands.mjs +565 -0
  26. package/src/slurm-commands.mjs +263 -0
  27. package/src/startup.mjs +34 -11
  28. package/src/storage-commands.mjs +1144 -0
  29. package/src/update.mjs +172 -24
  30. package/src/vm-commands.mjs +4273 -0
  31. package/src/vpn-secret-output.mjs +150 -0
  32. package/src/wait-runtime.mjs +231 -0
  33. package/vendor/capabilities/capabilities/admin-capacity-reservations.js +126 -0
  34. package/vendor/capabilities/capabilities/admin-enrollment.js +321 -0
  35. package/vendor/capabilities/capabilities/admin-node-diagnostics.js +88 -0
  36. package/vendor/capabilities/capabilities/admin-nodes-lifecycle.js +235 -0
  37. package/vendor/capabilities/capabilities/admin.js +89 -0
  38. package/vendor/capabilities/capabilities/capacity-offers.js +368 -0
  39. package/vendor/capabilities/capabilities/cli.js +156 -0
  40. package/vendor/capabilities/capabilities/clusters.js +150 -1
  41. package/vendor/capabilities/capabilities/compute.js +522 -0
  42. package/vendor/capabilities/capabilities/evidence.js +145 -0
  43. package/vendor/capabilities/capabilities/external-buckets.js +191 -0
  44. package/vendor/capabilities/capabilities/fleet.js +21 -5
  45. package/vendor/capabilities/capabilities/identity.js +114 -3
  46. package/vendor/capabilities/capabilities/listings.js +14 -8
  47. package/vendor/capabilities/capabilities/mounts.js +176 -0
  48. package/vendor/capabilities/capabilities/networks.js +1257 -0
  49. package/vendor/capabilities/capabilities/observability.js +38 -8
  50. package/vendor/capabilities/capabilities/tokens.js +20 -8
  51. package/vendor/capabilities/capabilities/volumes.js +178 -0
  52. package/vendor/capabilities/capability.js +19 -13
  53. package/vendor/capabilities/cli-catalog.js +65 -0
  54. package/vendor/capabilities/cli-migrations.js +288 -0
  55. package/vendor/capabilities/cli.js +7 -0
  56. package/vendor/capabilities/software-catalog.js +36 -0
  57. package/vendor/capabilities/spec-catalog.js +2 -2
  58. package/src/catalog-dispatch.mjs +0 -605
  59. package/src/color.mjs +0 -71
  60. package/vendor/capabilities/capabilities/clusters.d.ts +0 -2
  61. package/vendor/capabilities/capabilities/fleet.d.ts +0 -8
  62. package/vendor/capabilities/capabilities/identity.d.ts +0 -3
  63. package/vendor/capabilities/capabilities/listings.d.ts +0 -51
  64. package/vendor/capabilities/capabilities/observability.d.ts +0 -5
  65. package/vendor/capabilities/capabilities/staff-access.d.ts +0 -29
  66. package/vendor/capabilities/capabilities/staff-access.js +0 -278
  67. package/vendor/capabilities/capabilities/staff-commerce.d.ts +0 -160
  68. package/vendor/capabilities/capabilities/staff-commerce.js +0 -689
  69. package/vendor/capabilities/capabilities/staff-fleet.d.ts +0 -140
  70. package/vendor/capabilities/capabilities/staff-fleet.js +0 -538
  71. package/vendor/capabilities/capabilities/staff-gtm.d.ts +0 -142
  72. package/vendor/capabilities/capabilities/staff-gtm.js +0 -521
  73. package/vendor/capabilities/capabilities/staff-shared.d.ts +0 -47
  74. package/vendor/capabilities/capabilities/staff-shared.js +0 -98
  75. package/vendor/capabilities/capabilities/tokens.d.ts +0 -10
  76. package/vendor/capabilities/capabilities/users.d.ts +0 -2
  77. package/vendor/capabilities/capability.d.ts +0 -93
  78. package/vendor/capabilities/catalog.d.ts +0 -3
  79. package/vendor/capabilities/catalog.js +0 -26
  80. package/vendor/capabilities/index.d.ts +0 -16
  81. package/vendor/capabilities/index.js +0 -16
  82. package/vendor/capabilities/names.d.ts +0 -6
  83. package/vendor/capabilities/role.d.ts +0 -6
  84. package/vendor/capabilities/role.js +0 -17
  85. package/vendor/capabilities/spec-catalog.d.ts +0 -78
@@ -0,0 +1,150 @@
1
+ import { createHash, createPrivateKey, createPublicKey } from "node:crypto";
2
+ import { readFile } from "node:fs/promises";
3
+ import { networkInterfaces } from "node:os";
4
+
5
+ import {
6
+ assertAtomicOutputAvailable,
7
+ AtomicOutputExistsError,
8
+ writeAtomicOutput,
9
+ } from "./atomic-output.mjs";
10
+
11
+ const X25519_PKCS8_PREFIX = Buffer.from("302e020100300506032b656e04220420", "hex");
12
+ const PRIVATE_KEY_PLACEHOLDER = "<your-wireguard-private-key>";
13
+
14
+ function parseWireGuardPrivateKey(content) {
15
+ const source = String(content).trim();
16
+ const assigned = source
17
+ .split(/\r?\n/)
18
+ .map((line) => line.match(/^\s*PrivateKey\s*=\s*(\S+)\s*$/)?.[1])
19
+ .find(Boolean);
20
+ const value = assigned ?? source;
21
+ if (!value || !/^[A-Za-z0-9+/]{42}[AEIMQUYcgkosw048]=$/.test(value)) {
22
+ const error = new Error(
23
+ "The WireGuard private-key file must contain one base64 private key or a PrivateKey = value line."
24
+ );
25
+ error.code = "invalid_wireguard_private_key";
26
+ throw error;
27
+ }
28
+ const raw = Buffer.from(value, "base64");
29
+ if (raw.length !== 32) {
30
+ const error = new Error("The WireGuard private key must decode to exactly 32 bytes.");
31
+ error.code = "invalid_wireguard_private_key";
32
+ throw error;
33
+ }
34
+ const privateObject = createPrivateKey({
35
+ key: Buffer.concat([X25519_PKCS8_PREFIX, raw]),
36
+ format: "der",
37
+ type: "pkcs8",
38
+ });
39
+ const publicDer = createPublicKey(privateObject).export({ format: "der", type: "spki" });
40
+ return {
41
+ privateKey: value,
42
+ publicKey: Buffer.from(publicDer).subarray(-32).toString("base64"),
43
+ };
44
+ }
45
+
46
+ async function wireGuardKeyFromFile(privateKeyPath) {
47
+ if (typeof privateKeyPath !== "string" || !privateKeyPath.trim()) {
48
+ const error = new Error("A WireGuard private-key file is required.");
49
+ error.code = "wireguard_private_key_required";
50
+ throw error;
51
+ }
52
+ return parseWireGuardPrivateKey(await readFile(privateKeyPath, "utf8"));
53
+ }
54
+
55
+ export async function wireGuardPublicKeyFromFile(privateKeyPath) {
56
+ return (await wireGuardKeyFromFile(privateKeyPath)).publicKey;
57
+ }
58
+
59
+ async function completeWireGuardConfig(content, privateKeyPath, expectedPublicKey) {
60
+ const key = await wireGuardKeyFromFile(privateKeyPath);
61
+ if (expectedPublicKey && key.publicKey !== expectedPublicKey) {
62
+ const error = new Error(
63
+ "The selected WireGuard private key no longer matches the reviewed peer."
64
+ );
65
+ error.code = "wireguard_key_changed";
66
+ throw error;
67
+ }
68
+ if (!String(content).includes(`PrivateKey = ${PRIVATE_KEY_PLACEHOLDER}`)) {
69
+ const error = new Error(
70
+ "Ornn returned a VPN configuration without its private-key handoff marker."
71
+ );
72
+ error.code = "vpn_configuration_invalid";
73
+ throw error;
74
+ }
75
+ return String(content).replace(
76
+ `PrivateKey = ${PRIVATE_KEY_PLACEHOLDER}`,
77
+ `PrivateKey = ${key.privateKey}`
78
+ );
79
+ }
80
+
81
+ export function hasLocalNetworkAddress(address) {
82
+ const expected = String(address ?? "")
83
+ .split("/")[0]
84
+ .trim();
85
+ if (!expected) return false;
86
+ return Object.values(networkInterfaces()).some((entries) =>
87
+ (entries ?? []).some((entry) => entry.address === expected)
88
+ );
89
+ }
90
+
91
+ function fingerprint(content) {
92
+ return `sha256:${createHash("sha256").update(content).digest("hex")}`;
93
+ }
94
+
95
+ function modeLabel(mode) {
96
+ if (typeof mode === "string") return mode.startsWith("0") ? mode : `0${mode}`;
97
+ return `0${(Number(mode) & 0o777).toString(8).padStart(3, "0")}`;
98
+ }
99
+
100
+ function outputError(error, outputPath) {
101
+ if (!(error instanceof AtomicOutputExistsError)) return error;
102
+ const refusal = new Error(
103
+ `Refusing to overwrite existing VPN configuration: ${outputPath}. Add --overwrite to replace it deliberately.`
104
+ );
105
+ refusal.code = "output_exists";
106
+ return refusal;
107
+ }
108
+
109
+ export async function prepareVpnSecretExport({ outputPath, overwrite = false, print = false }) {
110
+ if (print) return;
111
+ try {
112
+ await assertAtomicOutputAvailable(outputPath, { overwrite });
113
+ } catch (error) {
114
+ throw outputError(error, outputPath);
115
+ }
116
+ }
117
+
118
+ /**
119
+ * App-owned secret export hook installed on the shared interaction runtime.
120
+ * Secret bytes are consumed here and never returned in the command result.
121
+ */
122
+ export async function exportVpnSecret(
123
+ { content, outputPath, overwrite = false, print = false, privateKeyPath, publicKey },
124
+ { machine, stdout }
125
+ ) {
126
+ const completed = await completeWireGuardConfig(content, privateKeyPath, publicKey);
127
+ const digest = fingerprint(completed);
128
+ if (print) {
129
+ if (machine) {
130
+ const error = new Error(
131
+ "Machine mode cannot print VPN configuration bytes; use --output <path>."
132
+ );
133
+ error.code = "secret_print_unsupported_in_machine_mode";
134
+ throw error;
135
+ }
136
+ stdout.write(completed.endsWith("\n") ? completed : `${completed}\n`);
137
+ return { fingerprint: digest, mode: null, path: null };
138
+ }
139
+
140
+ try {
141
+ const written = await writeAtomicOutput(outputPath, completed, { mode: 0o600, overwrite });
142
+ return {
143
+ fingerprint: digest,
144
+ mode: modeLabel(written.mode),
145
+ path: written.path,
146
+ };
147
+ } catch (error) {
148
+ throw outputError(error, outputPath);
149
+ }
150
+ }
@@ -0,0 +1,231 @@
1
+ function progressRecord(command, sequence, observedAt, outcome, message, data) {
2
+ return {
3
+ schema: "ornn.cli/v1",
4
+ type: "progress",
5
+ command,
6
+ sequence,
7
+ observed_at: observedAt,
8
+ outcome,
9
+ message,
10
+ data: data ?? null,
11
+ };
12
+ }
13
+
14
+ export async function waitForDelay(delay, sleep, signal) {
15
+ if (!signal) {
16
+ if (sleep) {
17
+ await sleep(delay);
18
+ } else {
19
+ await new Promise((resolve) => setTimeout(resolve, delay));
20
+ }
21
+ return "elapsed";
22
+ }
23
+ if (signal.aborted) return "interrupted";
24
+ if (sleep) {
25
+ return new Promise((resolve, reject) => {
26
+ let settled = false;
27
+ const finish = (outcome) => {
28
+ if (settled) return;
29
+ settled = true;
30
+ signal.removeEventListener("abort", interrupted);
31
+ resolve(outcome);
32
+ };
33
+ const interrupted = () => finish("interrupted");
34
+ signal.addEventListener("abort", interrupted, { once: true });
35
+ Promise.resolve(sleep(delay)).then(
36
+ () => finish("elapsed"),
37
+ (error) => {
38
+ signal.removeEventListener("abort", interrupted);
39
+ reject(error);
40
+ }
41
+ );
42
+ });
43
+ }
44
+ return new Promise((resolve) => {
45
+ let settled = false;
46
+ let timer;
47
+ const finish = (outcome) => {
48
+ if (settled) return;
49
+ settled = true;
50
+ clearTimeout(timer);
51
+ signal.removeEventListener("abort", interrupted);
52
+ resolve(outcome);
53
+ };
54
+ const interrupted = () => finish("interrupted");
55
+ signal.addEventListener("abort", interrupted, { once: true });
56
+ timer = setTimeout(() => finish("elapsed"), delay);
57
+ });
58
+ }
59
+
60
+ export async function readUntilDeadline(read, { signal, timeoutMs } = {}) {
61
+ const controller = new AbortController();
62
+ return new Promise((resolve, reject) => {
63
+ let settled = false;
64
+ let timer;
65
+ const cleanup = () => {
66
+ clearTimeout(timer);
67
+ signal?.removeEventListener("abort", interrupted);
68
+ };
69
+ const finish = (result) => {
70
+ if (settled) return;
71
+ settled = true;
72
+ cleanup();
73
+ resolve(result);
74
+ };
75
+ const interrupted = () => {
76
+ controller.abort(signal?.reason);
77
+ finish({ kind: "interrupted" });
78
+ };
79
+ if (Number.isFinite(timeoutMs)) {
80
+ timer = setTimeout(
81
+ () => {
82
+ controller.abort(new DOMException("The wait deadline elapsed.", "TimeoutError"));
83
+ finish({ kind: "timeout" });
84
+ },
85
+ Math.max(0, timeoutMs)
86
+ );
87
+ }
88
+ signal?.addEventListener("abort", interrupted, { once: true });
89
+ Promise.resolve()
90
+ .then(() => read({ signal: controller.signal }))
91
+ .then(
92
+ (value) => finish({ kind: "value", value }),
93
+ (error) => {
94
+ if (settled) return;
95
+ settled = true;
96
+ cleanup();
97
+ reject(error);
98
+ }
99
+ );
100
+ if (signal?.aborted) interrupted();
101
+ });
102
+ }
103
+
104
+ /**
105
+ * Poll a durable remote goal. Poll callbacks must issue only idempotent reads
106
+ * and forward the provided signal so local timeout/interruption can end a
107
+ * stalled read without cancelling the durable remote operation.
108
+ */
109
+ export async function waitForGoal(
110
+ {
111
+ command,
112
+ emitProgress = true,
113
+ initial,
114
+ intervalMs = 5_000,
115
+ isReady,
116
+ isTerminal = () => false,
117
+ message,
118
+ onObserve = () => {},
119
+ poll,
120
+ pollImmediately = false,
121
+ projectProgress = () => null,
122
+ progressIntervalMs = 10_000,
123
+ timeoutMs,
124
+ },
125
+ { format, now = () => Date.now(), signal, sleep, stderr, stdout }
126
+ ) {
127
+ let current = initial;
128
+ let elapsedMs = 0;
129
+ let scheduledElapsedMs = 0;
130
+ let delayBeforePoll = !pollImmediately;
131
+ let lastDescription = "";
132
+ let lastReportMs = -progressIntervalMs;
133
+ let sequence = 0;
134
+ const startedAt = now();
135
+ const localController = signal ? null : new AbortController();
136
+ const localSignal = signal ?? localController.signal;
137
+ const onSigint = localController ? () => localController.abort() : null;
138
+ if (onSigint) process.once("SIGINT", onSigint);
139
+
140
+ function syncElapsed() {
141
+ const wallElapsedMs = Math.max(0, now() - startedAt);
142
+ elapsedMs = Math.min(timeoutMs, Math.max(scheduledElapsedMs, wallElapsedMs));
143
+ }
144
+
145
+ function report(force = false) {
146
+ onObserve(current, elapsedMs);
147
+ if (!emitProgress) return;
148
+ const description = message(current, elapsedMs);
149
+ if (!force && description === lastDescription && elapsedMs - lastReportMs < progressIntervalMs)
150
+ return;
151
+ lastDescription = description;
152
+ lastReportMs = elapsedMs;
153
+ const outcome = isReady(current)
154
+ ? { class: "ready", phase: "goal_ready" }
155
+ : isTerminal(current)
156
+ ? { class: "failed", phase: "terminal" }
157
+ : { class: "working", phase: "provisioning" };
158
+ if (format === "ndjson") {
159
+ sequence += 1;
160
+ stdout.write(
161
+ `${JSON.stringify(
162
+ progressRecord(
163
+ command,
164
+ sequence,
165
+ new Date(startedAt + elapsedMs).toISOString(),
166
+ outcome,
167
+ description,
168
+ projectProgress(current)
169
+ )
170
+ )}\n`
171
+ );
172
+ } else if (format === "human") {
173
+ stderr.write(`${description}\n`);
174
+ }
175
+ }
176
+
177
+ try {
178
+ syncElapsed();
179
+ report(true);
180
+ for (;;) {
181
+ syncElapsed();
182
+ const immediatePoll = !delayBeforePoll;
183
+ if (localSignal.aborted) return { kind: "interrupted", value: current };
184
+ if (isReady(current)) return { kind: "ready", value: current };
185
+ if (isTerminal(current)) return { kind: "terminal", value: current };
186
+ if (elapsedMs >= timeoutMs && !immediatePoll) {
187
+ return { kind: "timeout", value: current };
188
+ }
189
+ if (delayBeforePoll) {
190
+ const untilProgressMs = Math.max(0, progressIntervalMs - (elapsedMs - lastReportMs));
191
+ const delay = Math.min(intervalMs, timeoutMs - elapsedMs, untilProgressMs);
192
+ if (delay > 0) {
193
+ const waited = await waitForDelay(delay, sleep, localSignal);
194
+ if (waited === "interrupted" || localSignal.aborted) {
195
+ return { kind: "interrupted", value: current };
196
+ }
197
+ scheduledElapsedMs += delay;
198
+ }
199
+ }
200
+ delayBeforePoll = true;
201
+ syncElapsed();
202
+ report();
203
+ if (elapsedMs >= timeoutMs && !immediatePoll) {
204
+ return { kind: "timeout", value: current };
205
+ }
206
+ const remainingMs = Math.max(1, timeoutMs - elapsedMs);
207
+ const untilProgressMs = Math.max(1, progressIntervalMs - (elapsedMs - lastReportMs));
208
+ const readWindowMs = Math.min(remainingMs, progressIntervalMs, untilProgressMs);
209
+ const polled = await readUntilDeadline(poll, {
210
+ signal: localSignal,
211
+ timeoutMs: readWindowMs,
212
+ });
213
+ if (polled.kind === "interrupted") {
214
+ return { kind: "interrupted", value: current };
215
+ }
216
+ if (polled.kind === "timeout") {
217
+ scheduledElapsedMs += readWindowMs;
218
+ syncElapsed();
219
+ if (elapsedMs >= timeoutMs) return { kind: "timeout", value: current };
220
+ report();
221
+ delayBeforePoll = false;
222
+ continue;
223
+ }
224
+ current = polled.value;
225
+ syncElapsed();
226
+ report();
227
+ }
228
+ } finally {
229
+ if (onSigint) process.removeListener("SIGINT", onSigint);
230
+ }
231
+ }
@@ -0,0 +1,126 @@
1
+ import { z } from "zod";
2
+ import { defineCapability } from "../capability.js";
3
+ import { createListingCapability } from "./listings.js";
4
+ function adminHelp(command, outcome, human, automation, review) {
5
+ return {
6
+ audience: "admin",
7
+ category: "resource",
8
+ command,
9
+ outcome,
10
+ human,
11
+ automation,
12
+ acceptedReferences: [],
13
+ ...(review ? { review } : {}),
14
+ };
15
+ }
16
+ const capacityOfferCreateInput = z.object({
17
+ gpu_type: z.string().min(1),
18
+ node_count: z.number().int().positive(),
19
+ gpus_per_node: z.number().int().positive(),
20
+ site_nickname: z.string().min(1),
21
+ available_start_at: z.string().min(1),
22
+ available_end_at: z.string().min(1),
23
+ buy_now_price_per_gpu_hour: z.number().nonnegative(),
24
+ });
25
+ export const adminCapacityOffersCreateCapability = {
26
+ ...createListingCapability,
27
+ cli: {
28
+ ...createListingCapability.cli,
29
+ vnext: adminHelp(["admin", "capacity-offers", "create"], "Create one capacity offer 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", "Not supported in machine or redirected execution because offer creation has no durable replay identity.", "GPU, capacity, site, availability window, and price are normalized before default-no consent."),
30
+ },
31
+ input: createListingCapability.input,
32
+ execute: async (ctx, input) => {
33
+ if (!ctx.cli)
34
+ return createListingCapability.execute(ctx, createListingCapability.input.parse(input));
35
+ const result = await createListingCapability.execute(ctx, capacityOfferCreateInput.parse(input));
36
+ if (!ctx.confirmed || object(result).ok === false)
37
+ return result;
38
+ return { capacity_offer: normalizeCapacityOffer(result) };
39
+ },
40
+ };
41
+ const reservationCreateInput = z.object({
42
+ organization_id: z.string().uuid(),
43
+ listing_id: z.string().uuid(),
44
+ node_count: z.number().int().positive(),
45
+ start_at: z.string().min(1),
46
+ end_at: z.string().min(1),
47
+ price_per_gpu_hour: z.string().regex(/^\d+(?:\.\d{1,6})?$/),
48
+ });
49
+ function object(value) {
50
+ return value && typeof value === "object" && !Array.isArray(value)
51
+ ? value
52
+ : {};
53
+ }
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
+ export const adminReservationsCreateCapability = defineCapability({
89
+ id: "reservations.create",
90
+ domain: "reservations",
91
+ roles: ["admin"],
92
+ description: "Create one capacity reservation from explicit, reviewed commercial terms.",
93
+ mutation: "preview-confirm",
94
+ http: { method: "POST", path: "/internal/commerce-reservations" },
95
+ cli: {
96
+ command: ["admin", "reservations", "create"],
97
+ vnext: adminHelp(["admin", "reservations", "create"], "Create one capacity reservation from explicit commercial terms.", "ornn admin reservations create --organization ID --capacity-offer ID --nodes COUNT --starts-at TIME --ends-at TIME --price-per-gpu-hour USD", "Not supported in machine or redirected execution because reservation creation has no durable replay identity.", "Organization, capacity offer, quantity, dates, and price are normalized before default-no consent."),
98
+ },
99
+ mcp: false,
100
+ slack: false,
101
+ input: reservationCreateInput,
102
+ execute: async (ctx, input) => {
103
+ const parsed = reservationCreateInput.parse(input);
104
+ if (!ctx.confirmed)
105
+ return { preview: parsed };
106
+ const payload = await ctx.fetch("/internal/commerce-reservations", {
107
+ method: "POST",
108
+ body: {
109
+ tenant_id: parsed.organization_id,
110
+ listing_id: parsed.listing_id,
111
+ node_count: parsed.node_count,
112
+ start_at: parsed.start_at,
113
+ end_at: parsed.end_at,
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),
120
+ };
121
+ },
122
+ });
123
+ export const adminCapacityReservationCapabilities = [
124
+ adminCapacityOffersCreateCapability,
125
+ adminReservationsCreateCapability,
126
+ ];