@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 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`. `selectedOrganizationId` is an untrusted hint; the gateway
355
- revalidates membership. Neither direction rewrites credentials or configured
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ornncompute/cli",
3
- "version": "1.0.64",
3
+ "version": "1.0.67",
4
4
  "description": "Command-line interface for Ornn compute access workflows.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- const headers = {
83
+ return {
84
84
  Authorization: `Bearer ${session.accessToken}`,
85
- // This marker and the organization selector grant nothing by themselves.
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({
@@ -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
  }
@@ -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, authSnapshot?.current?.selectedOrganizationId);
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, input, runtime) {
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
- let result = record(await session("context", { organization: input.organization }));
875
- let data = record(result.data);
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("Select one approved organization before launch.", {
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, input, runtime);
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",
@@ -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", "--organization", "--poll-interval", "--timeout"],
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
- values: ["--organization"],
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 readServerContext({ env, fetchImpl, session: suppliedSession }) {
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
- let context = await readServerContext({ env, fetchImpl, session: localSession });
301
- await bindAuthUser(context.user, {
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 && context.user?.id) {
158
+ if (sessionSnapshot?.current && data.user?.id) {
306
159
  sessionSnapshot.current = {
307
160
  ...sessionSnapshot.current,
308
- user: { ...(sessionSnapshot.current.user ?? {}), ...context.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
- context,
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 resolveContext(
489
- { organization: input.organization },
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(input, runtime);
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
  }
@@ -408,39 +408,25 @@ function machineMutationPreflight(input, interaction, { requireYes = true } = {}
408
408
  }
409
409
  }
410
410
 
411
- async function selectedOrganization(context, input, interaction) {
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
- let result = record(await session("context", { organization: input.organization }));
415
- let data = record(result.data);
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("Select one approved organization before launch.", {
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("Linux login is always tenant; --username cannot request another name.", {
916
- code: "launch_username_unsupported",
917
- });
900
+ interaction.usage(UNSUPPORTED_USERNAME_MESSAGE, { code: "launch_username_unsupported" });
918
901
  }
919
- return { ...input, username: "tenant" };
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("Linux login is always tenant; --username cannot request another name.", {
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, input, interaction);
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, input, interaction);
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 --organization <id> --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)",
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 [--organization <exact-name>] [--no-browser] [--timeout <seconds>]",
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 and select the current server-verified organization context.",
46
+ description: "Show the current server-verified organization context.",
49
47
  mutation: "read",
50
48
  http: { method: "GET", path: "/v1/cli/session" },
51
- input: contextInput,
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 [--organization <exact-name-or-id>]",
60
- automation: "ornn context --organization <canonical-id> --format json",
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
  },