@ornncompute/cli 1.0.64 → 1.0.67
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 +7 -3
- package/package.json +1 -1
- package/src/api-client.mjs +2 -7
- package/src/auth-store.mjs +0 -20
- package/src/catalog-shell.mjs +8 -2
- package/src/kubernetes-commands.mjs +6 -36
- package/src/session-commands.mjs +20 -248
- package/src/vm-commands.mjs +25 -44
- package/vendor/capabilities/capabilities/compute.js +1 -2
- package/vendor/capabilities/capabilities/identity.js +5 -11
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. Restricted operations retain their approval requirements.
|
|
62
|
+
|
|
59
63
|
Advanced customer resources are registered under one noun and one verb:
|
|
60
64
|
|
|
61
65
|
```text
|
|
@@ -351,9 +355,9 @@ npm install -g @ornncompute/cli@0.2.10
|
|
|
351
355
|
```
|
|
352
356
|
|
|
353
357
|
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,
|
|
358
|
+
`update-check.json`. Current CLI releases ignore saved organization hints and
|
|
359
|
+
use the gateway's newest membership. Neither direction rewrites credentials or
|
|
360
|
+
configured endpoints. Rollback is unsupported from day 91. From day 91 through day 180,
|
|
357
361
|
legacy compatibility calls return a non-forwarding `426 Upgrade Required`
|
|
358
362
|
tombstone with `cli_version_unsupported`, minimum version `1.0.0`, and the
|
|
359
363
|
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
|
@@ -985,6 +985,7 @@ async function executeCapability(
|
|
|
985
985
|
`${resolveApiBaseUrl({ env: io.env })}/v1/orchestrator`)
|
|
986
986
|
: undefined;
|
|
987
987
|
const parsedInput = parseCapabilityInput(capability, rawInput);
|
|
988
|
+
let ownOrganizationId;
|
|
988
989
|
let data;
|
|
989
990
|
try {
|
|
990
991
|
data = await capability.execute(
|
|
@@ -993,7 +994,7 @@ async function executeCapability(
|
|
|
993
994
|
confirmed,
|
|
994
995
|
installerBaseUrl,
|
|
995
996
|
role,
|
|
996
|
-
fetch: (path, init = {}) => {
|
|
997
|
+
fetch: async (path, init = {}) => {
|
|
997
998
|
if (executionRuntime?.transport) return executionRuntime.transport(path, init);
|
|
998
999
|
if (audience === "admin") {
|
|
999
1000
|
// Shared observability capabilities use their owner namespace;
|
|
@@ -1004,6 +1005,11 @@ async function executeCapability(
|
|
|
1004
1005
|
: path;
|
|
1005
1006
|
return adminFetch(io, endpoint, init);
|
|
1006
1007
|
}
|
|
1008
|
+
if (capability.domain === "listings" && ownOrganizationId === undefined) {
|
|
1009
|
+
// Verify listing owner IDs before the request, never after a committed mutation.
|
|
1010
|
+
const session = await customerCapabilityFetch(io, "/v1/cli/session", {}, authSnapshot);
|
|
1011
|
+
ownOrganizationId = session.selectedOrganization?.id ?? null;
|
|
1012
|
+
}
|
|
1007
1013
|
return customerCapabilityFetch(io, path, init, authSnapshot);
|
|
1008
1014
|
},
|
|
1009
1015
|
cli: {
|
|
@@ -1026,7 +1032,7 @@ async function executeCapability(
|
|
|
1026
1032
|
interaction?.close();
|
|
1027
1033
|
}
|
|
1028
1034
|
if (audience === "customer") {
|
|
1029
|
-
assertCustomerResultSafe(data,
|
|
1035
|
+
assertCustomerResultSafe(data, ownOrganizationId);
|
|
1030
1036
|
}
|
|
1031
1037
|
if (audience === "admin" || sessionCommand || isInteractionExecution(data)) return data;
|
|
1032
1038
|
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
878
|
if (!id || !name || selected.approved !== true) {
|
|
899
|
-
runtime.usage("
|
|
879
|
+
runtime.usage("Your current organization must be approved before launch.", {
|
|
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
418
|
if (!id || !name || selected.approved !== true) {
|
|
439
|
-
interaction.usage("
|
|
419
|
+
interaction.usage("Your current organization must be approved before launch.", {
|
|
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: {
|
|
@@ -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
|
},
|