@ornncompute/cli 1.0.64 → 1.0.72
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 +17 -9
- package/package.json +1 -1
- package/src/api-client.mjs +2 -7
- package/src/auth-store.mjs +0 -20
- package/src/catalog-shell.mjs +12 -20
- package/src/kubernetes-commands.mjs +7 -37
- package/src/session-commands.mjs +20 -248
- package/src/vm-commands.mjs +26 -45
- package/vendor/capabilities/capabilities/admin-capacity-reservations.js +12 -5
- package/vendor/capabilities/capabilities/compute.js +1 -2
- package/vendor/capabilities/capabilities/identity.js +5 -11
- package/vendor/capabilities/capabilities/listings.js +44 -16
- package/vendor/capabilities/capabilities/locality.js +42 -0
- package/vendor/capabilities/capabilities/markets.js +2 -2
- package/vendor/capabilities/capabilities/native-listing.js +51 -15
package/README.md
CHANGED
|
@@ -56,6 +56,10 @@ ornn completion <bash|fish|zsh> --format human
|
|
|
56
56
|
ornn resources
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
+
The CLI uses Authorization's newest membership on every request, regardless of
|
|
60
|
+
approval. `ornn context` displays that current organization; login and launch
|
|
61
|
+
use the same context. Reserve browsing and purchasing still require approval.
|
|
62
|
+
|
|
59
63
|
Advanced customer resources are registered under one noun and one verb:
|
|
60
64
|
|
|
61
65
|
```text
|
|
@@ -96,7 +100,7 @@ relisted (secondary) listing term with Markets `POST /v1/markets/bookings`
|
|
|
96
100
|
when the window starts. `market` and `offers` call Markets (`/v1/markets`):
|
|
97
101
|
buy `--nodes` at market (Markets also takes GPU counts and limit bids; the
|
|
98
102
|
CLI does not send them yet), end a fill with
|
|
99
|
-
`market end` (Commerce `
|
|
103
|
+
`market end` (Commerce `DELETE /commerce/reservations/{id}`), post an offer
|
|
100
104
|
on a whole primary listing for one of its terms (`--listing-term-id`, optional
|
|
101
105
|
`--duration-seconds`), list the offers your organization made or received, and
|
|
102
106
|
accept or cancel offers by `--offer-id`.
|
|
@@ -119,11 +123,15 @@ customer and takes `--organization-id UUID`, `--listing-id UUID`,
|
|
|
119
123
|
whole seconds, the term is non-renewable Reserved, and the price is the term's.
|
|
120
124
|
Reuse the key and payload on retries. Creation requires an eligible human
|
|
121
125
|
terminal. Native capacity creation takes
|
|
122
|
-
`--gpu-slug`, `--
|
|
123
|
-
`--
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
126
|
+
`--gpu-slug`, `--gpus-per-node`, `--facility-id`, `--provisioning-type`,
|
|
127
|
+
`--state`, `--terms` (a JSON array) and `--idempotency-key`, plus optional
|
|
128
|
+
`--organization-id`. Give `--capacity-node-count`, or `--node-ids` (a JSON array)
|
|
129
|
+
to attach nodes at creation; the count then defaults to the number of nodes.
|
|
130
|
+
Primary capacity takes `--days-to-rfs` (0 when ready now) or both
|
|
131
|
+
`--available-at` and `--available-until`, not both. Reuse the key and the same
|
|
132
|
+
body on retries. Prices are integer micro-USD inside terms, with explicit
|
|
133
|
+
duration bounds, nullable deposit, renewable flag and optional
|
|
134
|
+
`preemptNoticeSeconds`.
|
|
127
135
|
`ornn admin capacity-offers update` edits a selected term using `--listing-id`,
|
|
128
136
|
`--listing-terms-id`, the complete term fields and `--idempotency-key`. Financial
|
|
129
137
|
editing remains staff-only. The old customer `capacity-offers update` is removed.
|
|
@@ -351,9 +359,9 @@ npm install -g @ornncompute/cli@0.2.10
|
|
|
351
359
|
```
|
|
352
360
|
|
|
353
361
|
Both directions reuse valid `auth.json`, `config.json`, and
|
|
354
|
-
`update-check.json`.
|
|
355
|
-
|
|
356
|
-
endpoints. Rollback is unsupported from day 91. From day 91 through day 180,
|
|
362
|
+
`update-check.json`. Current CLI releases ignore saved organization hints and
|
|
363
|
+
use the gateway's newest membership. Neither direction rewrites credentials or
|
|
364
|
+
configured endpoints. Rollback is unsupported from day 91. From day 91 through day 180,
|
|
357
365
|
legacy compatibility calls return a non-forwarding `426 Upgrade Required`
|
|
358
366
|
tombstone with `cli_version_unsupported`, minimum version `1.0.0`, and the
|
|
359
367
|
migration URL. The legacy route surface is absent from day 181
|
package/package.json
CHANGED
package/src/api-client.mjs
CHANGED
|
@@ -80,17 +80,12 @@ export async function loadRequiredSession({ env = process.env } = {}) {
|
|
|
80
80
|
}
|
|
81
81
|
export function userCredentialHeaders(session) {
|
|
82
82
|
if (!session?.accessToken) return {};
|
|
83
|
-
|
|
83
|
+
return {
|
|
84
84
|
Authorization: `Bearer ${session.accessToken}`,
|
|
85
|
-
//
|
|
86
|
-
// The gateway re-resolves the bearer and replaces them with trusted scope.
|
|
85
|
+
// The gateway resolves the bearer to trusted user and organization scope.
|
|
87
86
|
"X-Ornn-Credential-Type": "user",
|
|
88
87
|
"X-Ornn-CLI-Version": CLI_VERSION,
|
|
89
88
|
};
|
|
90
|
-
if (typeof session.selectedOrganizationId === "string" && session.selectedOrganizationId.trim()) {
|
|
91
|
-
headers["X-Organization-Id"] = session.selectedOrganizationId.trim();
|
|
92
|
-
}
|
|
93
|
-
return headers;
|
|
94
89
|
}
|
|
95
90
|
|
|
96
91
|
export async function cliRequest({
|
package/src/auth-store.mjs
CHANGED
|
@@ -134,26 +134,6 @@ export async function bindAuthUser(user, { env = process.env, expectedAccessToke
|
|
|
134
134
|
return true;
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
export async function saveOrganizationHint(
|
|
138
|
-
selectedOrganizationId,
|
|
139
|
-
{ env = process.env, expectedAccessToken } = {}
|
|
140
|
-
) {
|
|
141
|
-
const session = await loadAuthSession({ env });
|
|
142
|
-
if (
|
|
143
|
-
!session?.accessToken ||
|
|
144
|
-
(expectedAccessToken && session.accessToken !== expectedAccessToken)
|
|
145
|
-
) {
|
|
146
|
-
return null;
|
|
147
|
-
}
|
|
148
|
-
const payload = { ...session };
|
|
149
|
-
if (typeof selectedOrganizationId === "string" && selectedOrganizationId.trim()) {
|
|
150
|
-
payload.selectedOrganizationId = selectedOrganizationId.trim();
|
|
151
|
-
} else {
|
|
152
|
-
delete payload.selectedOrganizationId;
|
|
153
|
-
}
|
|
154
|
-
return writeAuthSession(payload, env);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
137
|
function pendingString(value, field) {
|
|
158
138
|
return typeof value?.[field] === "string" ? value[field].trim() : "";
|
|
159
139
|
}
|
package/src/catalog-shell.mjs
CHANGED
|
@@ -236,17 +236,11 @@ function retiredSyntaxError(argv, findLegacyCliMigration, migrationGuideUrl) {
|
|
|
236
236
|
);
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
-
function resolveRequest(catalog,
|
|
240
|
-
|
|
239
|
+
function resolveRequest(catalog, argvIn, findLegacyCliMigration, migrationGuideUrl) {
|
|
240
|
+
const argv = argvIn[0] === "--help" || argvIn[0] === "-h" ? ["help", ...argvIn.slice(1)] : argvIn;
|
|
241
|
+
if (argv.length === 0) {
|
|
241
242
|
return { kind: "root", command: "help" };
|
|
242
243
|
}
|
|
243
|
-
if (argv[0] === "--help" || argv[0] === "-h") {
|
|
244
|
-
return {
|
|
245
|
-
kind: "error",
|
|
246
|
-
command: "help",
|
|
247
|
-
error: new VnextUsageError("Usage: ornn [--format human|json|ndjson]"),
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
244
|
|
|
251
245
|
const retired = retiredSyntaxError(argv, findLegacyCliMigration, migrationGuideUrl);
|
|
252
246
|
if (retired) {
|
|
@@ -311,15 +305,7 @@ function resolveRequest(catalog, argv, findLegacyCliMigration, migrationGuideUrl
|
|
|
311
305
|
if (capability) {
|
|
312
306
|
const command = vnextCommand(capability);
|
|
313
307
|
const rest = argv.slice(command.length);
|
|
314
|
-
|
|
315
|
-
if (helpFlags.length) {
|
|
316
|
-
if (rest.length !== 1) {
|
|
317
|
-
return {
|
|
318
|
-
kind: "error",
|
|
319
|
-
command: command.join(" "),
|
|
320
|
-
error: new VnextUsageError(`Usage: ${capability.cli.vnext.human}`),
|
|
321
|
-
};
|
|
322
|
-
}
|
|
308
|
+
if (rest.some((argument) => argument === "--help" || argument === "-h")) {
|
|
323
309
|
return { kind: "help", command: `help ${command.join(" ")}`, capability };
|
|
324
310
|
}
|
|
325
311
|
if (rest.length && capability.cli.vnext.arguments !== "semantic") {
|
|
@@ -985,6 +971,7 @@ async function executeCapability(
|
|
|
985
971
|
`${resolveApiBaseUrl({ env: io.env })}/v1/orchestrator`)
|
|
986
972
|
: undefined;
|
|
987
973
|
const parsedInput = parseCapabilityInput(capability, rawInput);
|
|
974
|
+
let ownOrganizationId;
|
|
988
975
|
let data;
|
|
989
976
|
try {
|
|
990
977
|
data = await capability.execute(
|
|
@@ -993,7 +980,7 @@ async function executeCapability(
|
|
|
993
980
|
confirmed,
|
|
994
981
|
installerBaseUrl,
|
|
995
982
|
role,
|
|
996
|
-
fetch: (path, init = {}) => {
|
|
983
|
+
fetch: async (path, init = {}) => {
|
|
997
984
|
if (executionRuntime?.transport) return executionRuntime.transport(path, init);
|
|
998
985
|
if (audience === "admin") {
|
|
999
986
|
// Shared observability capabilities use their owner namespace;
|
|
@@ -1004,6 +991,11 @@ async function executeCapability(
|
|
|
1004
991
|
: path;
|
|
1005
992
|
return adminFetch(io, endpoint, init);
|
|
1006
993
|
}
|
|
994
|
+
if (capability.domain === "listings" && ownOrganizationId === undefined) {
|
|
995
|
+
// Verify listing owner IDs before the request, never after a committed mutation.
|
|
996
|
+
const session = await customerCapabilityFetch(io, "/v1/cli/session", {}, authSnapshot);
|
|
997
|
+
ownOrganizationId = session.selectedOrganization?.id ?? null;
|
|
998
|
+
}
|
|
1007
999
|
return customerCapabilityFetch(io, path, init, authSnapshot);
|
|
1008
1000
|
},
|
|
1009
1001
|
cli: {
|
|
@@ -1026,7 +1018,7 @@ async function executeCapability(
|
|
|
1026
1018
|
interaction?.close();
|
|
1027
1019
|
}
|
|
1028
1020
|
if (audience === "customer") {
|
|
1029
|
-
assertCustomerResultSafe(data,
|
|
1021
|
+
assertCustomerResultSafe(data, ownOrganizationId);
|
|
1030
1022
|
}
|
|
1031
1023
|
if (audience === "admin" || sessionCommand || isInteractionExecution(data)) return data;
|
|
1032
1024
|
return {
|
|
@@ -868,35 +868,15 @@ async function resolveLaunchReservation(context, runtime, input, accessMode) {
|
|
|
868
868
|
return fetchReservation(context.fetch, runtime, selected, accessMode);
|
|
869
869
|
}
|
|
870
870
|
|
|
871
|
-
async function selectedOrganization(context,
|
|
871
|
+
async function selectedOrganization(context, runtime) {
|
|
872
872
|
const session = context.cli?.session;
|
|
873
873
|
if (!session) throw new Error("The current organization session is unavailable.");
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
let selected = record(data.selected_organization);
|
|
877
|
-
if (!text(selected.id) && runtime.eligibleHuman) {
|
|
878
|
-
const choices = (Array.isArray(data.organizations) ? data.organizations : [])
|
|
879
|
-
.map(record)
|
|
880
|
-
.filter(
|
|
881
|
-
(organization) =>
|
|
882
|
-
organization.approved === true && text(organization.id) && text(organization.name)
|
|
883
|
-
)
|
|
884
|
-
.map((organization) => ({ label: text(organization.name), value: text(organization.id) }));
|
|
885
|
-
if (choices.length > 1) {
|
|
886
|
-
const answer = await runtime.ask({ label: "Organization", choices });
|
|
887
|
-
if (!answer)
|
|
888
|
-
runtime.usage("Choose an approved organization before launch.", {
|
|
889
|
-
code: "organization_required",
|
|
890
|
-
});
|
|
891
|
-
result = record(await session("context", { organization: answer }));
|
|
892
|
-
data = record(result.data);
|
|
893
|
-
selected = record(data.selected_organization);
|
|
894
|
-
}
|
|
895
|
-
}
|
|
874
|
+
const result = record(await session("context", {}));
|
|
875
|
+
const selected = record(record(result.data).selected_organization);
|
|
896
876
|
const id = text(selected.id);
|
|
897
877
|
const name = text(selected.name);
|
|
898
|
-
if (!id || !name
|
|
899
|
-
runtime.usage("
|
|
878
|
+
if (!id || !name) {
|
|
879
|
+
runtime.usage("Join an organization before launching compute.", {
|
|
900
880
|
code: "organization_required",
|
|
901
881
|
field: "organization",
|
|
902
882
|
});
|
|
@@ -934,12 +914,6 @@ function clusterMachineLaunchPreflight(runtime, input, membershipCount) {
|
|
|
934
914
|
});
|
|
935
915
|
}
|
|
936
916
|
if (runtime.eligibleHuman) return;
|
|
937
|
-
if (!UUID.test(text(input.organization) ?? "")) {
|
|
938
|
-
runtime.usage("Automation requires a canonical organization ID.", {
|
|
939
|
-
code: "canonical_organization_id_required",
|
|
940
|
-
field: "organization",
|
|
941
|
-
});
|
|
942
|
-
}
|
|
943
917
|
if (input.yes !== true) {
|
|
944
918
|
runtime.usage("--yes is required for automation after reviewing the exact intent.", {
|
|
945
919
|
code: "consent_required",
|
|
@@ -1153,7 +1127,7 @@ export async function launchClusterMode(
|
|
|
1153
1127
|
clusterMachineLaunchPreflight(runtime, input, membershipCount);
|
|
1154
1128
|
if (accessMode === "slurm") keyInputShapePreflight(input, runtime);
|
|
1155
1129
|
const idempotencyKey = requireMutationKey(runtime, input);
|
|
1156
|
-
const organization = await selectedOrganization(context,
|
|
1130
|
+
const organization = await selectedOrganization(context, runtime);
|
|
1157
1131
|
const keyPlan = accessMode === "slurm" ? await keyPlanFromInput(context, input, runtime) : null;
|
|
1158
1132
|
const reservation = await resolveLaunchReservation(context, runtime, input, accessMode);
|
|
1159
1133
|
const reservationId = text(reservation.id);
|
|
@@ -1321,11 +1295,7 @@ export async function launchClusterMode(
|
|
|
1321
1295
|
presentation: { lines: ["No cluster was created."] },
|
|
1322
1296
|
});
|
|
1323
1297
|
}
|
|
1324
|
-
const currentOrganization = await selectedOrganization(
|
|
1325
|
-
context,
|
|
1326
|
-
{ organization: organization.id },
|
|
1327
|
-
runtime
|
|
1328
|
-
);
|
|
1298
|
+
const currentOrganization = await selectedOrganization(context, runtime);
|
|
1329
1299
|
if (currentOrganization.id !== organization.id) {
|
|
1330
1300
|
runtime.error("The selected organization changed after review; no cluster was created.", {
|
|
1331
1301
|
code: "organization_changed",
|
package/src/session-commands.mjs
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { createInterface } from "node:readline/promises";
|
|
2
|
-
|
|
3
1
|
import { CliApiError, cliRequest, resolveApiBaseUrl } from "./api-client.mjs";
|
|
4
2
|
import {
|
|
5
3
|
bindAuthUser,
|
|
@@ -7,7 +5,6 @@ import {
|
|
|
7
5
|
getAuthConfigPath,
|
|
8
6
|
loadAuthSession,
|
|
9
7
|
saveAuthSession,
|
|
10
|
-
saveOrganizationHint,
|
|
11
8
|
} from "./auth-store.mjs";
|
|
12
9
|
import {
|
|
13
10
|
DeviceAuthError,
|
|
@@ -17,8 +14,6 @@ import {
|
|
|
17
14
|
waitForDeviceApproval,
|
|
18
15
|
} from "./device-auth.mjs";
|
|
19
16
|
|
|
20
|
-
const CANONICAL_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
21
|
-
|
|
22
17
|
export class SessionCommandError extends Error {
|
|
23
18
|
constructor(
|
|
24
19
|
message,
|
|
@@ -83,25 +78,22 @@ function positiveNumberOption(value, name, usageText) {
|
|
|
83
78
|
|
|
84
79
|
export function parseSessionCommandInput(capabilityId, argv) {
|
|
85
80
|
if (capabilityId === "identity.login") {
|
|
86
|
-
const usageText =
|
|
87
|
-
"Usage: ornn login [--organization <exact-name>] [--no-browser] [--timeout <seconds>]";
|
|
81
|
+
const usageText = "Usage: ornn login [--no-browser] [--timeout <seconds>]";
|
|
88
82
|
const parsed = parseOptions(argv, {
|
|
89
83
|
booleans: ["--no-browser"],
|
|
90
|
-
values: ["--auth-base", "--
|
|
84
|
+
values: ["--auth-base", "--poll-interval", "--timeout"],
|
|
91
85
|
usageText,
|
|
92
86
|
});
|
|
93
87
|
return {
|
|
94
88
|
authBase: parsed["auth-base"],
|
|
95
89
|
noBrowser: parsed["no-browser"],
|
|
96
|
-
organization: parsed.organization,
|
|
97
90
|
pollInterval: positiveNumberOption(parsed["poll-interval"], "--poll-interval", usageText),
|
|
98
91
|
timeout: positiveNumberOption(parsed.timeout, "--timeout", usageText),
|
|
99
92
|
};
|
|
100
93
|
}
|
|
101
94
|
if (capabilityId === "identity.context") {
|
|
102
95
|
return parseOptions(argv, {
|
|
103
|
-
|
|
104
|
-
usageText: "Usage: ornn context [--organization <exact-name-or-id>]",
|
|
96
|
+
usageText: "Usage: ornn context",
|
|
105
97
|
});
|
|
106
98
|
}
|
|
107
99
|
if (capabilityId === "identity.logout") {
|
|
@@ -142,154 +134,7 @@ function sessionContext(value) {
|
|
|
142
134
|
};
|
|
143
135
|
}
|
|
144
136
|
|
|
145
|
-
async function
|
|
146
|
-
const session = suppliedSession === undefined ? await loadAuthSession({ env }) : suppliedSession;
|
|
147
|
-
try {
|
|
148
|
-
return sessionContext(
|
|
149
|
-
await cliRequest({
|
|
150
|
-
endpoint: "/v1/cli/session",
|
|
151
|
-
env,
|
|
152
|
-
fetchImpl,
|
|
153
|
-
method: "GET",
|
|
154
|
-
...(session ? { session } : {}),
|
|
155
|
-
})
|
|
156
|
-
);
|
|
157
|
-
} catch (error) {
|
|
158
|
-
if (
|
|
159
|
-
!(error instanceof CliApiError) ||
|
|
160
|
-
error.status !== 403 ||
|
|
161
|
-
!session?.accessToken ||
|
|
162
|
-
!session.selectedOrganizationId
|
|
163
|
-
) {
|
|
164
|
-
throw error;
|
|
165
|
-
}
|
|
166
|
-
const unscopedSession = { ...session };
|
|
167
|
-
delete unscopedSession.selectedOrganizationId;
|
|
168
|
-
return sessionContext(
|
|
169
|
-
await cliRequest({
|
|
170
|
-
endpoint: "/v1/cli/session",
|
|
171
|
-
env,
|
|
172
|
-
fetchImpl,
|
|
173
|
-
method: "GET",
|
|
174
|
-
session: unscopedSession,
|
|
175
|
-
})
|
|
176
|
-
);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function selectionAction(machine) {
|
|
181
|
-
return {
|
|
182
|
-
type: "command",
|
|
183
|
-
command: machine
|
|
184
|
-
? "ornn context --organization <canonical-id> --format json"
|
|
185
|
-
: "ornn context --organization <exact-name>",
|
|
186
|
-
description: machine
|
|
187
|
-
? "Select one approved organization by canonical identifier."
|
|
188
|
-
: "Select one approved organization by its exact displayed name.",
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function findApprovedOrganization(organizations, selector, machine) {
|
|
193
|
-
if (machine && !CANONICAL_ID.test(selector)) {
|
|
194
|
-
throw new SessionCommandError(
|
|
195
|
-
"Automation must select an organization by canonical identifier.",
|
|
196
|
-
{
|
|
197
|
-
action: selectionAction(true),
|
|
198
|
-
code: "organization_id_required",
|
|
199
|
-
exitCode: 2,
|
|
200
|
-
}
|
|
201
|
-
);
|
|
202
|
-
}
|
|
203
|
-
const matches = organizations.filter((candidate) =>
|
|
204
|
-
machine ? candidate.id === selector : candidate.id === selector || candidate.name === selector
|
|
205
|
-
);
|
|
206
|
-
if (matches.length === 0) {
|
|
207
|
-
throw new SessionCommandError("The requested organization is not a current membership.", {
|
|
208
|
-
action: selectionAction(machine),
|
|
209
|
-
code: "organization_not_available",
|
|
210
|
-
exitCode: 5,
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
if (matches.length > 1) {
|
|
214
|
-
throw new SessionCommandError(
|
|
215
|
-
"More than one organization has that name; select by canonical identifier.",
|
|
216
|
-
{
|
|
217
|
-
action: selectionAction(true),
|
|
218
|
-
code: "organization_ambiguous",
|
|
219
|
-
exitCode: 2,
|
|
220
|
-
}
|
|
221
|
-
);
|
|
222
|
-
}
|
|
223
|
-
if (!matches[0].approved) {
|
|
224
|
-
throw new SessionCommandError(`Organization ${matches[0].name} is not approved.`, {
|
|
225
|
-
code: "organization_not_approved",
|
|
226
|
-
exitCode: 5,
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
return matches[0];
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
async function promptForOrganization(organizations, { stdin, stdout }) {
|
|
233
|
-
stdout.write("Choose an approved organization by typing its exact name:\n");
|
|
234
|
-
for (const candidate of organizations) stdout.write(` ${candidate.name}\n`);
|
|
235
|
-
const readline = createInterface({ input: stdin, output: stdout, terminal: false });
|
|
236
|
-
try {
|
|
237
|
-
return await readline.question("Organization: ");
|
|
238
|
-
} finally {
|
|
239
|
-
readline.close();
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function updateSnapshotOrganization(sessionSnapshot, selectedOrganizationId) {
|
|
244
|
-
if (!sessionSnapshot?.current) return;
|
|
245
|
-
const next = { ...sessionSnapshot.current };
|
|
246
|
-
if (selectedOrganizationId) next.selectedOrganizationId = selectedOrganizationId;
|
|
247
|
-
else delete next.selectedOrganizationId;
|
|
248
|
-
sessionSnapshot.current = next;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
async function selectAndRevalidate(
|
|
252
|
-
selector,
|
|
253
|
-
context,
|
|
254
|
-
{ env, fetchImpl, machine, sessionSnapshot }
|
|
255
|
-
) {
|
|
256
|
-
const selected = findApprovedOrganization(context.organizations, selector, machine);
|
|
257
|
-
const expectedAccessToken = sessionSnapshot?.current?.accessToken;
|
|
258
|
-
await saveOrganizationHint(selected.id, { env, expectedAccessToken });
|
|
259
|
-
updateSnapshotOrganization(sessionSnapshot, selected.id);
|
|
260
|
-
const revalidated = await readServerContext({
|
|
261
|
-
env,
|
|
262
|
-
fetchImpl,
|
|
263
|
-
session: sessionSnapshot?.current,
|
|
264
|
-
});
|
|
265
|
-
if (revalidated.selected_organization?.id !== selected.id) {
|
|
266
|
-
await saveOrganizationHint(null, { env, expectedAccessToken });
|
|
267
|
-
updateSnapshotOrganization(sessionSnapshot, null);
|
|
268
|
-
throw new SessionCommandError(
|
|
269
|
-
"Organization membership changed before selection could be verified.",
|
|
270
|
-
{
|
|
271
|
-
action: selectionAction(machine),
|
|
272
|
-
code: "organization_membership_changed",
|
|
273
|
-
exitCode: 5,
|
|
274
|
-
}
|
|
275
|
-
);
|
|
276
|
-
}
|
|
277
|
-
return revalidated;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
async function resolveContext(
|
|
281
|
-
input,
|
|
282
|
-
{
|
|
283
|
-
eligibleHuman,
|
|
284
|
-
env,
|
|
285
|
-
fetchImpl,
|
|
286
|
-
machine,
|
|
287
|
-
promptIfMultiple = false,
|
|
288
|
-
sessionSnapshot,
|
|
289
|
-
stdin,
|
|
290
|
-
stdout,
|
|
291
|
-
}
|
|
292
|
-
) {
|
|
137
|
+
async function context({ env, fetchImpl, sessionSnapshot }) {
|
|
293
138
|
const localSession =
|
|
294
139
|
sessionSnapshot?.current === undefined
|
|
295
140
|
? await loadAuthSession({ env })
|
|
@@ -297,83 +142,28 @@ async function resolveContext(
|
|
|
297
142
|
if (sessionSnapshot && sessionSnapshot.current === undefined) {
|
|
298
143
|
sessionSnapshot.current = localSession;
|
|
299
144
|
}
|
|
300
|
-
|
|
301
|
-
|
|
145
|
+
const data = sessionContext(
|
|
146
|
+
await cliRequest({
|
|
147
|
+
endpoint: "/v1/cli/session",
|
|
148
|
+
env,
|
|
149
|
+
fetchImpl,
|
|
150
|
+
method: "GET",
|
|
151
|
+
session: localSession,
|
|
152
|
+
})
|
|
153
|
+
);
|
|
154
|
+
await bindAuthUser(data.user, {
|
|
302
155
|
env,
|
|
303
156
|
expectedAccessToken: localSession?.accessToken,
|
|
304
157
|
});
|
|
305
|
-
if (sessionSnapshot?.current &&
|
|
158
|
+
if (sessionSnapshot?.current && data.user?.id) {
|
|
306
159
|
sessionSnapshot.current = {
|
|
307
160
|
...sessionSnapshot.current,
|
|
308
|
-
user: { ...(sessionSnapshot.current.user ?? {}), ...
|
|
309
|
-
};
|
|
310
|
-
}
|
|
311
|
-
const localHint = localSession?.selectedOrganizationId;
|
|
312
|
-
const hintIsCurrent =
|
|
313
|
-
context.selected_organization && context.selected_organization.id === localHint;
|
|
314
|
-
if (localHint && !hintIsCurrent) {
|
|
315
|
-
await saveOrganizationHint(null, {
|
|
316
|
-
env,
|
|
317
|
-
expectedAccessToken: localSession?.accessToken,
|
|
318
|
-
});
|
|
319
|
-
updateSnapshotOrganization(sessionSnapshot, null);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
const selectionRuntime = { env, fetchImpl, machine, sessionSnapshot };
|
|
323
|
-
if (input.organization) {
|
|
324
|
-
return {
|
|
325
|
-
context: await selectAndRevalidate(input.organization, context, selectionRuntime),
|
|
326
|
-
action: null,
|
|
327
|
-
exitCode: 0,
|
|
328
|
-
outcome: { class: "ready", phase: "complete" },
|
|
161
|
+
user: { ...(sessionSnapshot.current.user ?? {}), ...data.user },
|
|
329
162
|
};
|
|
330
163
|
}
|
|
331
164
|
|
|
332
|
-
if (hintIsCurrent) {
|
|
333
|
-
return {
|
|
334
|
-
context,
|
|
335
|
-
action: null,
|
|
336
|
-
exitCode: 0,
|
|
337
|
-
outcome: { class: "ready", phase: "complete" },
|
|
338
|
-
};
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
const approved = context.organizations.filter((candidate) => candidate.approved);
|
|
342
|
-
if (approved.length === 1) {
|
|
343
|
-
context = await selectAndRevalidate(approved[0].id, context, {
|
|
344
|
-
...selectionRuntime,
|
|
345
|
-
machine: true,
|
|
346
|
-
});
|
|
347
|
-
return {
|
|
348
|
-
context,
|
|
349
|
-
action: null,
|
|
350
|
-
exitCode: 0,
|
|
351
|
-
outcome: { class: "ready", phase: "complete" },
|
|
352
|
-
};
|
|
353
|
-
}
|
|
354
|
-
if (approved.length > 1 && promptIfMultiple && eligibleHuman) {
|
|
355
|
-
const answer = await promptForOrganization(approved, { stdin, stdout });
|
|
356
|
-
context = await selectAndRevalidate(answer, context, {
|
|
357
|
-
...selectionRuntime,
|
|
358
|
-
machine: false,
|
|
359
|
-
});
|
|
360
|
-
return {
|
|
361
|
-
context,
|
|
362
|
-
action: null,
|
|
363
|
-
exitCode: 0,
|
|
364
|
-
outcome: { class: "ready", phase: "complete" },
|
|
365
|
-
};
|
|
366
|
-
}
|
|
367
|
-
if (approved.length > 1) {
|
|
368
|
-
return {
|
|
369
|
-
context,
|
|
370
|
-
action: selectionAction(machine),
|
|
371
|
-
exitCode: 3,
|
|
372
|
-
outcome: { class: "action_required", phase: "organization_selection" },
|
|
373
|
-
};
|
|
374
|
-
}
|
|
375
165
|
return {
|
|
376
|
-
|
|
166
|
+
data,
|
|
377
167
|
action: null,
|
|
378
168
|
exitCode: 0,
|
|
379
169
|
outcome: { class: "ready", phase: "complete" },
|
|
@@ -485,26 +275,8 @@ async function login(input, runtime) {
|
|
|
485
275
|
|
|
486
276
|
await saveAuthSession(session, { env: runtime.env });
|
|
487
277
|
runtime.stderr.write(`\nSession saved to ${getAuthConfigPath(runtime.env)}\n`);
|
|
488
|
-
const resolved = await
|
|
489
|
-
|
|
490
|
-
{ ...runtime, machine: false, promptIfMultiple: true }
|
|
491
|
-
);
|
|
492
|
-
return {
|
|
493
|
-
data: { ...resolved.context, session_saved: true },
|
|
494
|
-
action: resolved.action,
|
|
495
|
-
exitCode: resolved.exitCode,
|
|
496
|
-
outcome: resolved.outcome,
|
|
497
|
-
};
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
async function context(input, runtime) {
|
|
501
|
-
const resolved = await resolveContext(input, { ...runtime, promptIfMultiple: false });
|
|
502
|
-
return {
|
|
503
|
-
data: resolved.context,
|
|
504
|
-
action: resolved.action,
|
|
505
|
-
exitCode: resolved.exitCode,
|
|
506
|
-
outcome: resolved.outcome,
|
|
507
|
-
};
|
|
278
|
+
const resolved = await context(runtime);
|
|
279
|
+
return { ...resolved, data: { ...resolved.data, session_saved: true } };
|
|
508
280
|
}
|
|
509
281
|
|
|
510
282
|
async function logout(runtime) {
|
|
@@ -559,7 +331,7 @@ async function logout(runtime) {
|
|
|
559
331
|
|
|
560
332
|
export async function executeSessionCommand(command, input, runtime) {
|
|
561
333
|
if (command === "login") return login(input, runtime);
|
|
562
|
-
if (command === "context") return context(
|
|
334
|
+
if (command === "context") return context(runtime);
|
|
563
335
|
if (command === "logout") return logout(runtime);
|
|
564
336
|
throw new CliApiError(`Unsupported session capability: ${command}`);
|
|
565
337
|
}
|
package/src/vm-commands.mjs
CHANGED
|
@@ -408,39 +408,25 @@ function machineMutationPreflight(input, interaction, { requireYes = true } = {}
|
|
|
408
408
|
}
|
|
409
409
|
}
|
|
410
410
|
|
|
411
|
-
async function selectedOrganization(context,
|
|
411
|
+
async function selectedOrganization(context, interaction, expectedId) {
|
|
412
412
|
const session = context.cli?.session;
|
|
413
413
|
if (!session) throw new Error("The current organization session is unavailable.");
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
let selected = record(data.selected_organization);
|
|
417
|
-
if (!text(selected.id) && interaction.eligibleHuman) {
|
|
418
|
-
const choices = (Array.isArray(data.organizations) ? data.organizations : [])
|
|
419
|
-
.map(record)
|
|
420
|
-
.filter(
|
|
421
|
-
(organization) =>
|
|
422
|
-
organization.approved === true && text(organization.id) && text(organization.name)
|
|
423
|
-
)
|
|
424
|
-
.map((organization) => ({ label: text(organization.name), value: text(organization.id) }));
|
|
425
|
-
if (choices.length > 1) {
|
|
426
|
-
const answer = await interaction.ask({ label: "Organization", choices });
|
|
427
|
-
if (!answer)
|
|
428
|
-
interaction.usage("Choose an approved organization before launch.", {
|
|
429
|
-
code: "organization_required",
|
|
430
|
-
});
|
|
431
|
-
result = record(await session("context", { organization: answer }));
|
|
432
|
-
data = record(result.data);
|
|
433
|
-
selected = record(data.selected_organization);
|
|
434
|
-
}
|
|
435
|
-
}
|
|
414
|
+
const result = record(await session("context", {}));
|
|
415
|
+
const selected = record(record(result.data).selected_organization);
|
|
436
416
|
const id = text(selected.id);
|
|
437
417
|
const name = text(selected.name);
|
|
438
|
-
if (!id || !name
|
|
439
|
-
interaction.usage("
|
|
418
|
+
if (!id || !name) {
|
|
419
|
+
interaction.usage("Join an organization before launching compute.", {
|
|
440
420
|
code: "organization_required",
|
|
441
421
|
field: "organization",
|
|
442
422
|
});
|
|
443
423
|
}
|
|
424
|
+
if (expectedId && id !== expectedId) {
|
|
425
|
+
interaction.error("Your organization changed; review the launch again before continuing.", {
|
|
426
|
+
code: "organization_changed",
|
|
427
|
+
exitCode: 2,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
444
430
|
return { id, name };
|
|
445
431
|
}
|
|
446
432
|
|
|
@@ -820,7 +806,6 @@ function launchInputPreflight(input, interaction) {
|
|
|
820
806
|
const target = text(input.reservation) ?? text(positionals[0]);
|
|
821
807
|
if (interaction.eligibleHuman && !interaction.machine) return;
|
|
822
808
|
requireCanonical(interaction, target, "reservation", "Capacity reservation");
|
|
823
|
-
requireCanonical(interaction, input.organization, "organization", "Organization");
|
|
824
809
|
const keyRef = text(input.keyId) ?? text(input.key);
|
|
825
810
|
const suppliedPublicKey = text(input.publicKey) ?? text(input.publicKeyFile);
|
|
826
811
|
if (!keyRef && !suppliedPublicKey) {
|
|
@@ -912,13 +897,14 @@ function launchOwnerSnapshot(machines) {
|
|
|
912
897
|
function withAuthoritativeUsername(input, _machines, interaction) {
|
|
913
898
|
const requested = text(input.username);
|
|
914
899
|
if (requested && requested !== "tenant") {
|
|
915
|
-
interaction.usage(
|
|
916
|
-
code: "launch_username_unsupported",
|
|
917
|
-
});
|
|
900
|
+
interaction.usage(UNSUPPORTED_USERNAME_MESSAGE, { code: "launch_username_unsupported" });
|
|
918
901
|
}
|
|
919
|
-
return
|
|
902
|
+
return input;
|
|
920
903
|
}
|
|
921
904
|
|
|
905
|
+
const UNSUPPORTED_USERNAME_MESSAGE =
|
|
906
|
+
"--username cannot choose the Linux login; the machine reports its own login.";
|
|
907
|
+
|
|
922
908
|
function validatedMachine(machines, key, input, interaction, expectedMode = "vm") {
|
|
923
909
|
if (!machines.length) return null;
|
|
924
910
|
if (machines.length !== 1) {
|
|
@@ -940,9 +926,7 @@ function validatedMachine(machines, key, input, interaction, expectedMode = "vm"
|
|
|
940
926
|
}
|
|
941
927
|
const username = text(input.username);
|
|
942
928
|
if (username && username !== "tenant") {
|
|
943
|
-
interaction.usage(
|
|
944
|
-
code: "launch_username_unsupported",
|
|
945
|
-
});
|
|
929
|
+
interaction.usage(UNSUPPORTED_USERNAME_MESSAGE, { code: "launch_username_unsupported" });
|
|
946
930
|
}
|
|
947
931
|
const fingerprintValue = text(key.fingerprint);
|
|
948
932
|
const assigned = Array.isArray(machine.authorized_key_fingerprints)
|
|
@@ -1072,7 +1056,7 @@ export const vmLaunchModeHandler = {
|
|
|
1072
1056
|
async launch({ context, input, interaction }) {
|
|
1073
1057
|
launchInputPreflight(input, interaction);
|
|
1074
1058
|
input = await normalizePublicKeyInput(input);
|
|
1075
|
-
const organization = await selectedOrganization(context,
|
|
1059
|
+
const organization = await selectedOrganization(context, interaction);
|
|
1076
1060
|
const reservation = await resolveReservation(context, input, interaction, "vm");
|
|
1077
1061
|
const reservationId = resourceId(reservation);
|
|
1078
1062
|
const keyPlan = await keyPlanFromInput(context, input, interaction);
|
|
@@ -1123,6 +1107,7 @@ export const vmLaunchModeHandler = {
|
|
|
1123
1107
|
});
|
|
1124
1108
|
}
|
|
1125
1109
|
|
|
1110
|
+
await selectedOrganization(context, interaction, organization.id);
|
|
1126
1111
|
const currentReservation = assertActiveReservation(
|
|
1127
1112
|
await context.fetch(
|
|
1128
1113
|
`/v1/workflows/commerce/reservations/${encodeURIComponent(reservationId)}`
|
|
@@ -1214,9 +1199,7 @@ export const vmLaunchModeHandler = {
|
|
|
1214
1199
|
resourceId(key),
|
|
1215
1200
|
"--idempotency-key",
|
|
1216
1201
|
idempotencyKey,
|
|
1217
|
-
...(interaction.machine
|
|
1218
|
-
? ["--organization", resourceId(organization), "--yes", "--no-wait", "--format", "json"]
|
|
1219
|
-
: []),
|
|
1202
|
+
...(interaction.machine ? ["--yes", "--no-wait", "--format", "json"] : []),
|
|
1220
1203
|
].join(" ");
|
|
1221
1204
|
return uncertainMutationFailure(interaction, error, {
|
|
1222
1205
|
data: {
|
|
@@ -1684,8 +1667,6 @@ function workspaceResumeCommand(input, goal, facts, id, interaction, localResume
|
|
|
1684
1667
|
goal.reservationId,
|
|
1685
1668
|
"--mode",
|
|
1686
1669
|
"bare-metal",
|
|
1687
|
-
"--organization",
|
|
1688
|
-
goal.organizationId,
|
|
1689
1670
|
"--key",
|
|
1690
1671
|
goal.keyId,
|
|
1691
1672
|
"--network",
|
|
@@ -1892,6 +1873,7 @@ async function launchDurableBareMetalWorkspace({
|
|
|
1892
1873
|
});
|
|
1893
1874
|
}
|
|
1894
1875
|
|
|
1876
|
+
await selectedOrganization(context, interaction, organization.id);
|
|
1895
1877
|
const [currentReservationValue, currentKeyPlan, currentFacts] = await Promise.all([
|
|
1896
1878
|
context.fetch(`/v1/workflows/commerce/reservations/${encodeURIComponent(reservationId)}`),
|
|
1897
1879
|
keyPlanFromInput(context, input, interaction),
|
|
@@ -2246,6 +2228,7 @@ export const bareMetalLaunchModeHandler = {
|
|
|
2246
2228
|
mode: "bare-metal",
|
|
2247
2229
|
async launch({ context, input, interaction }) {
|
|
2248
2230
|
const durableWorkspace = workspaceIntentRequested(input);
|
|
2231
|
+
let resumeOrganizationId;
|
|
2249
2232
|
if (durableWorkspace && input.resume === true) {
|
|
2250
2233
|
if (!interaction.eligibleHuman || interaction.machine) {
|
|
2251
2234
|
interaction.usage("--resume without a value requires an eligible interactive terminal.", {
|
|
@@ -2256,7 +2239,6 @@ export const bareMetalLaunchModeHandler = {
|
|
|
2256
2239
|
const hasConflictingIntent =
|
|
2257
2240
|
(input.$positionals?.length ?? 0) > 0 ||
|
|
2258
2241
|
[
|
|
2259
|
-
input.organization,
|
|
2260
2242
|
input.reservation,
|
|
2261
2243
|
input.reservationId,
|
|
2262
2244
|
input.key,
|
|
@@ -2295,6 +2277,7 @@ export const bareMetalLaunchModeHandler = {
|
|
|
2295
2277
|
field: "resume",
|
|
2296
2278
|
});
|
|
2297
2279
|
}
|
|
2280
|
+
resumeOrganizationId = remembered.organizationId;
|
|
2298
2281
|
input = {
|
|
2299
2282
|
...input,
|
|
2300
2283
|
$positionals: [remembered.reservationId],
|
|
@@ -2302,7 +2285,6 @@ export const bareMetalLaunchModeHandler = {
|
|
|
2302
2285
|
key: remembered.keyId,
|
|
2303
2286
|
mountAccess: remembered.mountAccess,
|
|
2304
2287
|
network: remembered.networkId,
|
|
2305
|
-
organization: remembered.organizationId,
|
|
2306
2288
|
resume: remembered.id,
|
|
2307
2289
|
username: remembered.username ?? undefined,
|
|
2308
2290
|
volume: remembered.volumeId,
|
|
@@ -2323,7 +2305,7 @@ export const bareMetalLaunchModeHandler = {
|
|
|
2323
2305
|
}
|
|
2324
2306
|
launchInputPreflight(input, interaction);
|
|
2325
2307
|
input = await normalizePublicKeyInput(input);
|
|
2326
|
-
const organization = await selectedOrganization(context,
|
|
2308
|
+
const organization = await selectedOrganization(context, interaction, resumeOrganizationId);
|
|
2327
2309
|
const reservation = await resolveReservation(context, input, interaction, "bare-metal");
|
|
2328
2310
|
const reservationId = resourceId(reservation);
|
|
2329
2311
|
const keyPlan = await keyPlanFromInput(context, input, interaction);
|
|
@@ -2398,6 +2380,7 @@ export const bareMetalLaunchModeHandler = {
|
|
|
2398
2380
|
fetchAssignedKeys(context, reservationId),
|
|
2399
2381
|
fetchMachines(context, reservationId),
|
|
2400
2382
|
]);
|
|
2383
|
+
await selectedOrganization(context, interaction, organization.id);
|
|
2401
2384
|
const currentReservation = assertActiveReservation(
|
|
2402
2385
|
currentReservationValue,
|
|
2403
2386
|
reservationId,
|
|
@@ -2481,9 +2464,7 @@ export const bareMetalLaunchModeHandler = {
|
|
|
2481
2464
|
resourceId(key),
|
|
2482
2465
|
"--idempotency-key",
|
|
2483
2466
|
idempotencyKey,
|
|
2484
|
-
...(interaction.machine
|
|
2485
|
-
? ["--organization", resourceId(organization), "--yes", "--no-wait", "--format", "json"]
|
|
2486
|
-
: []),
|
|
2467
|
+
...(interaction.machine ? ["--yes", "--no-wait", "--format", "json"] : []),
|
|
2487
2468
|
].join(" ");
|
|
2488
2469
|
return uncertainMutationFailure(interaction, error, {
|
|
2489
2470
|
data: {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { defineCapability, preview } from "../capability.js";
|
|
3
|
+
import { ensureFacility, locateRegion, unplacedLocation } from "./locality.js";
|
|
3
4
|
import { nativeListingCreateInput } from "./native-listing.js";
|
|
4
5
|
import { idempotencyHeaders, microUsd } from "./market-shared.js";
|
|
5
6
|
function adminHelp(command, outcome, human, automation, review) {
|
|
@@ -18,22 +19,28 @@ export const adminCapacityOffersCreateCapability = defineCapability({
|
|
|
18
19
|
id: "listing.create",
|
|
19
20
|
domain: "listings",
|
|
20
21
|
roles: ["admin"],
|
|
21
|
-
description: "Create native capacity with
|
|
22
|
+
description: "Create native capacity with an organization, a facilityId or a location, hardware, availability and advertised terms. A location is any state, city, address, or catalog region (us-west-3); Ornn places it in a catalog region and uses the organization's facility there. Primary capacity needs daysToRfs (0 when ready now) or both availableAt and availableUntil. nodeIds attach nodes at creation. Returns {listing, terms}. Requires an idempotencyKey reused on retries and confirm: true.",
|
|
22
23
|
mutation: "preview-confirm",
|
|
23
24
|
http: { method: "POST", path: "/commerce/v2/listings" },
|
|
24
25
|
input: nativeListingCreateInput,
|
|
25
26
|
slack: false,
|
|
26
27
|
cli: {
|
|
27
28
|
command: ["admin", "capacity-offers", "create"],
|
|
28
|
-
vnext: adminHelp(["admin", "capacity-offers", "create"], "Create native capacity and its advertised financial terms.", "ornn admin capacity-offers create --gpu-slug GPU --
|
|
29
|
+
vnext: adminHelp(["admin", "capacity-offers", "create"], "Create native capacity and its advertised financial terms.", "ornn admin capacity-offers create --gpu-slug GPU --gpus-per-node COUNT (--facility-id UUID | --location PLACE) --provisioning-type bare-metal|virtual-machine --state draft|available --terms JSON --idempotency-key KEY (--days-to-rfs DAYS | --available-at TIME --available-until TIME) [--capacity-node-count COUNT] [--node-ids JSON] [--organization-id UUID]", "Requires an eligible human terminal; --location also needs --organization-id; reuse --idempotency-key on retries.", "Review the native parent fields and each financial term before default-no consent."),
|
|
29
30
|
},
|
|
30
31
|
execute: async (ctx, input) => {
|
|
31
|
-
const { confirm: _confirm, idempotencyKey, ...body } = nativeListingCreateInput.parse(input);
|
|
32
|
+
const { confirm: _confirm, idempotencyKey, location, ...body } = nativeListingCreateInput.parse(input);
|
|
33
|
+
const located = location ? await locateRegion(ctx.fetch, location) : null;
|
|
34
|
+
if (location && !located)
|
|
35
|
+
throw new Error(unplacedLocation(location));
|
|
32
36
|
if (!ctx.confirmed)
|
|
33
|
-
return preview("listing.create", body);
|
|
37
|
+
return preview("listing.create", located ? { ...body, located } : body);
|
|
38
|
+
const facilityId = located
|
|
39
|
+
? (await ensureFacility(ctx.fetch, body.organizationId, located.region)).facilityId
|
|
40
|
+
: body.facilityId;
|
|
34
41
|
return ctx.fetch("/commerce/v2/listings", {
|
|
35
42
|
method: "POST",
|
|
36
|
-
body,
|
|
43
|
+
body: { ...body, facilityId },
|
|
37
44
|
headers: idempotencyHeaders(ctx, idempotencyKey),
|
|
38
45
|
});
|
|
39
46
|
},
|
|
@@ -16,7 +16,6 @@ export const launchInput = z
|
|
|
16
16
|
network: z.string().optional(),
|
|
17
17
|
noWait: z.boolean().optional(),
|
|
18
18
|
nodes: z.union([z.string(), z.number()]).optional(),
|
|
19
|
-
organization: z.string().optional(),
|
|
20
19
|
publicKey: z.string().optional(),
|
|
21
20
|
publicKeyFile: z.string().optional(),
|
|
22
21
|
reservation: z.string().optional(),
|
|
@@ -141,7 +140,7 @@ export const launchCapability = cliOnly({
|
|
|
141
140
|
order: 2,
|
|
142
141
|
outcome: "Launch one reviewed VM, Bare Metal machine, Kubernetes cluster, or Slurm cluster from active reserved capacity, including a declared durable Bare Metal workspace.",
|
|
143
142
|
human: "ornn launch [<capacity-reservation>] --mode vm|bare-metal|kubernetes|slurm [--key <ssh-key>]; Bare Metal may add --network shared --volume <managed-volume> --mount-access read-only|read-write; Kubernetes/Slurm prompt for missing --network public|private and --nodes <membership-count>, and may add running VMs with --vms <vm-ids>",
|
|
144
|
-
automation: "ornn launch <canonical-reservation-id> --mode vm|bare-metal|kubernetes|slurm --
|
|
143
|
+
automation: "ornn launch <canonical-reservation-id> --mode vm|bare-metal|kubernetes|slurm --yes --idempotency-key <key> --format json; VM/Bare Metal/Slurm require --key <canonical-key-id>; Bare Metal may add --network shared --volume <canonical-volume-id> --mount-access read-only|read-write; Kubernetes/Slurm require --network public|private --nodes <membership-count> --machines <canonical-machine-ids>, and may add --vms <canonical-vm-ids> (with --nodes 0 for a cluster of VMs only)",
|
|
145
144
|
acceptedReferences: [
|
|
146
145
|
"canonical capacity-reservation ID",
|
|
147
146
|
"canonical eligible machine IDs",
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { defineCapability } from "../capability.js";
|
|
3
3
|
const emptyInput = z.object({});
|
|
4
|
-
const contextInput = z.object({ organization: z.string().min(1).optional() });
|
|
5
4
|
const loginInput = z.object({
|
|
6
5
|
authBase: z.string().min(1).optional(),
|
|
7
6
|
noBrowser: z.boolean().optional(),
|
|
8
|
-
organization: z.string().min(1).optional(),
|
|
9
7
|
pollInterval: z.number().positive().optional(),
|
|
10
8
|
timeout: z.number().positive().optional(),
|
|
11
9
|
});
|
|
@@ -30,7 +28,7 @@ export const loginCapability = defineCapability({
|
|
|
30
28
|
category: "session",
|
|
31
29
|
arguments: "semantic",
|
|
32
30
|
outcome: "Authenticate in a browser and return to the terminal.",
|
|
33
|
-
human: "ornn login [--
|
|
31
|
+
human: "ornn login [--no-browser] [--timeout <seconds>]",
|
|
34
32
|
automation: "ornn login --format json",
|
|
35
33
|
resumableStates: ["browser authorization"],
|
|
36
34
|
related: ["context", "logout"],
|
|
@@ -45,10 +43,10 @@ export const contextCapability = defineCapability({
|
|
|
45
43
|
id: "identity.context",
|
|
46
44
|
domain: "identity",
|
|
47
45
|
roles: ["user"],
|
|
48
|
-
description: "Show
|
|
46
|
+
description: "Show the current server-verified organization context.",
|
|
49
47
|
mutation: "read",
|
|
50
48
|
http: { method: "GET", path: "/v1/cli/session" },
|
|
51
|
-
input:
|
|
49
|
+
input: emptyInput,
|
|
52
50
|
cli: {
|
|
53
51
|
command: ["context"],
|
|
54
52
|
vnext: {
|
|
@@ -56,12 +54,8 @@ export const contextCapability = defineCapability({
|
|
|
56
54
|
category: "session",
|
|
57
55
|
arguments: "semantic",
|
|
58
56
|
outcome: "Show the current user, selected organization, approval, and available organizations.",
|
|
59
|
-
human: "ornn context
|
|
60
|
-
automation: "ornn context --
|
|
61
|
-
acceptedReferences: [
|
|
62
|
-
"exact organization name (human)",
|
|
63
|
-
"canonical organization ID (automation)",
|
|
64
|
-
],
|
|
57
|
+
human: "ornn context",
|
|
58
|
+
automation: "ornn context --format json",
|
|
65
59
|
related: ["login", "logout"],
|
|
66
60
|
order: 2,
|
|
67
61
|
},
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { listSupplierOrganizations } from "../supplier-directory.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { defineCapability, preview } from "../capability.js";
|
|
4
|
+
import { ensureFacility, LOCATION_LOOKUP_UNAVAILABLE, locateRegion, unplacedLocation, } from "./locality.js";
|
|
4
5
|
import { bindHardwareFields, HARDWARE_LISTING_FIELDS, HARDWARE_LISTING_FILTERS, loadHardwareCatalog, projectCatalogTerms, resolveCatalogTerm, } from "../spec-catalog.js";
|
|
5
6
|
function queryPath(path, params) {
|
|
6
7
|
const parts = [];
|
|
@@ -30,7 +31,10 @@ export const listingCreateInput = z.object({
|
|
|
30
31
|
fabric_type: z.string().optional(),
|
|
31
32
|
node_count: z.number().int().optional(),
|
|
32
33
|
gpus_per_node: z.number().int().optional(),
|
|
33
|
-
site_nickname: z
|
|
34
|
+
site_nickname: z
|
|
35
|
+
.string()
|
|
36
|
+
.optional()
|
|
37
|
+
.describe("A state, city, address, or catalog region such as us-west-3."),
|
|
34
38
|
site_supplier: z.string().optional(),
|
|
35
39
|
ram: z.string().optional(),
|
|
36
40
|
storage: z.string().optional(),
|
|
@@ -67,7 +71,10 @@ const slackListingCreateParams = {
|
|
|
67
71
|
fabric_type: { type: "string" },
|
|
68
72
|
node_count: { type: "integer" },
|
|
69
73
|
gpus_per_node: { type: "integer" },
|
|
70
|
-
site_nickname: {
|
|
74
|
+
site_nickname: {
|
|
75
|
+
type: "string",
|
|
76
|
+
description: "Where the capacity is, exactly as the user said it: a state, city, address, or catalog region such as us-west-3. Ornn places it in a catalog region.",
|
|
77
|
+
},
|
|
71
78
|
site_supplier: { type: "string" },
|
|
72
79
|
ram: { type: "string" },
|
|
73
80
|
storage: { type: "string" },
|
|
@@ -242,7 +249,7 @@ export const createListingCapability = defineCapability({
|
|
|
242
249
|
let organizationId = parsed.organizationId ?? parsed.organization_id;
|
|
243
250
|
let facilityId = parsed.facilityId ?? parsed.facility_id;
|
|
244
251
|
const supplierQuery = (parsed.site_supplier ?? "").trim().toLowerCase();
|
|
245
|
-
const
|
|
252
|
+
const place = (parsed.site_nickname ?? parsed.site ?? "").trim();
|
|
246
253
|
if (!organizationId && supplierQuery) {
|
|
247
254
|
const matches = (await listSupplierOrganizations(ctx.fetch)).filter((org) => org.id.toLowerCase() === supplierQuery || org.name.toLowerCase() === supplierQuery);
|
|
248
255
|
if (matches.length !== 1) {
|
|
@@ -250,7 +257,19 @@ export const createListingCapability = defineCapability({
|
|
|
250
257
|
}
|
|
251
258
|
organizationId = matches[0].id;
|
|
252
259
|
}
|
|
253
|
-
|
|
260
|
+
let located = null;
|
|
261
|
+
let lookupUnavailable = false;
|
|
262
|
+
if (!facilityId && place) {
|
|
263
|
+
try {
|
|
264
|
+
located = await locateRegion(ctx.fetch, place);
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
if (ctx.confirmed)
|
|
268
|
+
throw error;
|
|
269
|
+
lookupUnavailable = true;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if ((facilityId && !organizationId) || (!facilityId && !place && organizationId)) {
|
|
254
273
|
const matches = [];
|
|
255
274
|
const seenCursors = new Set();
|
|
256
275
|
let cursor;
|
|
@@ -262,14 +281,7 @@ export const createListingCapability = defineCapability({
|
|
|
262
281
|
throw new Error("invalid facility catalog");
|
|
263
282
|
matches.push(...response.facilities.filter((facility) => (!organizationId ||
|
|
264
283
|
(facility.organizationId ?? facility.organization_id) === organizationId) &&
|
|
265
|
-
(facilityId
|
|
266
|
-
? (facility.facilityId ?? facility.id) === facilityId
|
|
267
|
-
: !facilityQuery ||
|
|
268
|
-
[
|
|
269
|
-
facility.facilityId ?? facility.id,
|
|
270
|
-
facility.region,
|
|
271
|
-
facility.displayName ?? facility.display_name,
|
|
272
|
-
].some((value) => value?.toLowerCase() === facilityQuery))));
|
|
284
|
+
(!facilityId || (facility.facilityId ?? facility.id) === facilityId)));
|
|
273
285
|
cursor = response.nextCursor ?? undefined;
|
|
274
286
|
if (cursor) {
|
|
275
287
|
if (seenCursors.has(cursor) || seenCursors.size >= 49) {
|
|
@@ -302,6 +314,10 @@ export const createListingCapability = defineCapability({
|
|
|
302
314
|
const gpusPerNode = parsed.gpus_per_node ?? 8;
|
|
303
315
|
const boundPayload = bound.payload;
|
|
304
316
|
const draft = Object.fromEntries(Object.entries(boundPayload).filter(([key]) => !LISTING_CREATE_INPUT_ONLY_KEYS.includes(key)));
|
|
317
|
+
if (located)
|
|
318
|
+
draft.site_nickname = located.name;
|
|
319
|
+
else if (place && !facilityId && !lookupUnavailable)
|
|
320
|
+
delete draft.site_nickname;
|
|
305
321
|
const gpuSlug = normalized.gpu_type
|
|
306
322
|
? resolveCatalogTerm(hardware.terms, "gpu", normalized.gpu_type).canonical
|
|
307
323
|
: null;
|
|
@@ -318,11 +334,12 @@ export const createListingCapability = defineCapability({
|
|
|
318
334
|
percent: parsed.deposit_percent,
|
|
319
335
|
})
|
|
320
336
|
: 0);
|
|
321
|
-
const
|
|
337
|
+
const priced = Boolean(organizationId && gpuSlug && buyerMicro != null);
|
|
338
|
+
const listingAt = (facility) => priced && facility
|
|
322
339
|
? primaryListingBody({
|
|
323
|
-
organizationId,
|
|
324
|
-
facilityId,
|
|
325
|
-
gpuSlug,
|
|
340
|
+
organizationId: organizationId,
|
|
341
|
+
facilityId: facility,
|
|
342
|
+
gpuSlug: gpuSlug,
|
|
326
343
|
gpusPerNode,
|
|
327
344
|
provisioningType: parsed.access_mode ?? "virtual-machine",
|
|
328
345
|
availableAt: startsAt,
|
|
@@ -337,13 +354,24 @@ export const createListingCapability = defineCapability({
|
|
|
337
354
|
}, buyerMicro, downPaymentCents)
|
|
338
355
|
: null;
|
|
339
356
|
if (!ctx.confirmed) {
|
|
357
|
+
const listing = listingAt(facilityId);
|
|
340
358
|
return preview("create_listing", {
|
|
341
359
|
...draft,
|
|
342
360
|
node_count: nodeCount,
|
|
343
361
|
gpus_per_node: gpusPerNode,
|
|
362
|
+
...(located ? { facility_region: located.region } : {}),
|
|
363
|
+
...(lookupUnavailable
|
|
364
|
+
? { location_unavailable: place, location_hint: LOCATION_LOOKUP_UNAVAILABLE }
|
|
365
|
+
: place && !facilityId && !located
|
|
366
|
+
? { location_unresolved: place, location_hint: unplacedLocation(place) }
|
|
367
|
+
: {}),
|
|
344
368
|
...(listing ? { listing } : {}),
|
|
345
369
|
});
|
|
346
370
|
}
|
|
371
|
+
if (located && !facilityId && priced) {
|
|
372
|
+
facilityId = (await ensureFacility(ctx.fetch, organizationId, located.region)).facilityId;
|
|
373
|
+
}
|
|
374
|
+
const listing = listingAt(facilityId);
|
|
347
375
|
if (!listing) {
|
|
348
376
|
return {
|
|
349
377
|
ok: false,
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { statusOf } from "./staff-shared.js";
|
|
2
|
+
export const LOCALITY_GEOLOCATE_PATH = "/v1/orchestrator/locality/geolocate";
|
|
3
|
+
export const FACILITY_ENSURE_PATH = "/provisioning/facilities/ensure";
|
|
4
|
+
/**
|
|
5
|
+
* `GET /v1/orchestrator/locality/geolocate?address=`: the catalog region a typed
|
|
6
|
+
* place stands in. Orchestrator answers a region code or name as itself and
|
|
7
|
+
* geocodes anything else (state, city, address) to its nearest region.
|
|
8
|
+
* Null means Orchestrator could not place the text (404); an unavailable
|
|
9
|
+
* geocoder or gateway throws, so callers ask for a retry rather than new text.
|
|
10
|
+
*/
|
|
11
|
+
export async function locateRegion(fetch, place) {
|
|
12
|
+
const address = place.trim();
|
|
13
|
+
if (!address)
|
|
14
|
+
return null;
|
|
15
|
+
let located;
|
|
16
|
+
try {
|
|
17
|
+
located = (await fetch(`${LOCALITY_GEOLOCATE_PATH}?address=${encodeURIComponent(address)}`, {
|
|
18
|
+
method: "GET",
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
if (statusOf(error) === 404)
|
|
23
|
+
return null;
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
const region = typeof located?.facility_slug === "string" ? located.facility_slug.trim() : "";
|
|
27
|
+
if (!region)
|
|
28
|
+
return null;
|
|
29
|
+
const name = typeof located?.region === "string" && located.region.trim() ? located.region.trim() : region;
|
|
30
|
+
return { region, name };
|
|
31
|
+
}
|
|
32
|
+
export function unplacedLocation(place) {
|
|
33
|
+
return `Ornn could not place "${place}". Give a state, city, address, or catalog region such as us-west-3.`;
|
|
34
|
+
}
|
|
35
|
+
export const LOCATION_LOOKUP_UNAVAILABLE = "Ornn's location lookup is unavailable right now. Keep this location and try again in a moment.";
|
|
36
|
+
/** `POST /internal/v1/facilities/ensure`: the organization's facility at a catalog region. */
|
|
37
|
+
export async function ensureFacility(fetch, organizationId, region) {
|
|
38
|
+
return (await fetch(FACILITY_ENSURE_PATH, {
|
|
39
|
+
method: "POST",
|
|
40
|
+
body: { organizationId, region },
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
@@ -293,7 +293,7 @@ export const marketEndCapability = defineCapability({
|
|
|
293
293
|
roles: ["user"],
|
|
294
294
|
description: "End a running fill: its reservation stops, its VMs stop, and its GPUs go back on the book. Requires confirm: true.",
|
|
295
295
|
mutation: "preview-confirm",
|
|
296
|
-
http: { method: "
|
|
296
|
+
http: { method: "DELETE", path: "/commerce/reservations/{fillId}" },
|
|
297
297
|
input: marketEndInput,
|
|
298
298
|
cli: {
|
|
299
299
|
command: ["market", "end"],
|
|
@@ -308,7 +308,7 @@ export const marketEndCapability = defineCapability({
|
|
|
308
308
|
], { fillId: parsed.fillId });
|
|
309
309
|
if (gate)
|
|
310
310
|
return gate;
|
|
311
|
-
return ctx.fetch(`/commerce/reservations/${seg(parsed.fillId)}
|
|
311
|
+
return ctx.fetch(`/commerce/reservations/${seg(parsed.fillId)}`, { method: "DELETE" });
|
|
312
312
|
},
|
|
313
313
|
});
|
|
314
314
|
export const offersListInput = z.object({
|
|
@@ -7,35 +7,62 @@ export const nativeTermFields = {
|
|
|
7
7
|
pricePerGpuHourMicroUsd: z.coerce.number().int().positive(),
|
|
8
8
|
downPaymentCents: z.coerce.number().int().nonnegative().nullable().optional(),
|
|
9
9
|
renewable: flag,
|
|
10
|
+
preemptNoticeSeconds: z.coerce.number().int().nonnegative().optional(),
|
|
10
11
|
};
|
|
11
12
|
export const nativeTermInput = z.object(nativeTermFields).strict();
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
13
|
+
function jsonArray(item) {
|
|
14
|
+
return z.preprocess((value) => {
|
|
15
|
+
if (typeof value !== "string")
|
|
16
|
+
return value;
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(value);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
}, z.array(item));
|
|
24
|
+
}
|
|
25
|
+
export const nativeTermsInput = jsonArray(nativeTermInput);
|
|
22
26
|
export const listingWriteControls = {
|
|
23
27
|
confirm: z.boolean().optional(),
|
|
24
28
|
idempotencyKey: z.string().min(1).max(200),
|
|
25
29
|
};
|
|
30
|
+
const listingLocation = z
|
|
31
|
+
.string()
|
|
32
|
+
.trim()
|
|
33
|
+
.min(1)
|
|
34
|
+
.max(256)
|
|
35
|
+
.optional()
|
|
36
|
+
.describe("Instead of facilityId: a state, city, address, or catalog region such as us-west-3. Ornn places it in a catalog region and uses the supplier's facility there.");
|
|
26
37
|
export const nativeListingCreateInput = z
|
|
27
38
|
.object({
|
|
28
39
|
organizationId: z.string().uuid().nullable().optional(),
|
|
29
40
|
kind: z.enum(["primary", "secondary"]).default("primary"),
|
|
30
41
|
gpuSlug: z.string().min(1),
|
|
31
42
|
gpusPerNode: z.coerce.number().int().positive(),
|
|
32
|
-
capacityNodeCount: z.coerce
|
|
33
|
-
|
|
43
|
+
capacityNodeCount: z.coerce
|
|
44
|
+
.number()
|
|
45
|
+
.int()
|
|
46
|
+
.positive()
|
|
47
|
+
.optional()
|
|
48
|
+
.describe("Defaults to the number of nodeIds."),
|
|
49
|
+
nodeIds: jsonArray(z.string().uuid()).optional(),
|
|
50
|
+
facilityId: z.string().uuid().optional(),
|
|
51
|
+
location: listingLocation,
|
|
34
52
|
provisioningType: z.enum(["bare-metal", "virtual-machine"]),
|
|
35
53
|
state: z.enum(["draft", "available"]),
|
|
36
|
-
availableAt: z.iso
|
|
54
|
+
availableAt: z.iso
|
|
55
|
+
.datetime({ offset: true })
|
|
56
|
+
.nullable()
|
|
57
|
+
.optional()
|
|
58
|
+
.describe("Primary capacity takes availableAt and availableUntil, or daysToRfs, not both."),
|
|
37
59
|
availableUntil: z.iso.datetime({ offset: true }).nullable().optional(),
|
|
38
|
-
daysToRfs: z.coerce
|
|
60
|
+
daysToRfs: z.coerce
|
|
61
|
+
.number()
|
|
62
|
+
.int()
|
|
63
|
+
.nonnegative()
|
|
64
|
+
.optional()
|
|
65
|
+
.describe("Days until ready for service; 0 when ready now. Required unless both dates are set."),
|
|
39
66
|
cpu: z.string().optional(),
|
|
40
67
|
ram: z.string().optional(),
|
|
41
68
|
storage: z.string().optional(),
|
|
@@ -44,13 +71,22 @@ export const nativeListingCreateInput = z
|
|
|
44
71
|
terms: nativeTermsInput,
|
|
45
72
|
...listingWriteControls,
|
|
46
73
|
})
|
|
47
|
-
.strict()
|
|
74
|
+
.strict()
|
|
75
|
+
.refine((value) => Boolean(value.facilityId) !== Boolean(value.location), {
|
|
76
|
+
message: "Give exactly one of facilityId or location.",
|
|
77
|
+
path: ["facilityId"],
|
|
78
|
+
})
|
|
79
|
+
.refine((value) => !value.location || Boolean(value.organizationId), {
|
|
80
|
+
message: "location needs organizationId, the supplier whose facility it is.",
|
|
81
|
+
path: ["organizationId"],
|
|
82
|
+
});
|
|
48
83
|
export const nativeListingUpdateFields = {
|
|
49
84
|
state: z.enum(["draft", "available", "delisted"]).optional(),
|
|
50
85
|
nodeCount: z.coerce.number().int().positive().optional(),
|
|
51
86
|
gpuSlug: z.string().min(1).optional(),
|
|
52
87
|
gpusPerNode: z.coerce.number().int().positive().optional(),
|
|
53
88
|
facilityId: z.string().uuid().optional(),
|
|
89
|
+
location: listingLocation,
|
|
54
90
|
provisioningType: z.enum(["bare-metal", "virtual-machine"]).optional(),
|
|
55
91
|
availableAt: z.iso.datetime({ offset: true }).optional(),
|
|
56
92
|
availableUntil: z.iso.datetime({ offset: true }).nullable().optional(),
|