@ornncompute/cli 1.0.67 → 1.0.75

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +35 -27
  2. package/package.json +1 -1
  3. package/src/admin-capacity-reservations.mjs +2 -12
  4. package/src/admin-command-handlers.mjs +0 -1
  5. package/src/admin-credits.mjs +1 -2
  6. package/src/admin-enrollment.mjs +22 -30
  7. package/src/admin-mutations.mjs +6 -56
  8. package/src/admin-node-diagnostics.mjs +2 -36
  9. package/src/admin-node-lifecycle.mjs +25 -43
  10. package/src/admin-shell.mjs +39 -32
  11. package/src/catalog-shell.mjs +4 -18
  12. package/src/external-bucket-commands.mjs +5 -73
  13. package/src/interaction-runtime.mjs +2 -37
  14. package/src/kubernetes-commands.mjs +11 -102
  15. package/src/launch-mode-handlers.mjs +0 -1
  16. package/src/mount-commands.mjs +25 -131
  17. package/src/storage-commands.mjs +2 -87
  18. package/src/vm-commands.mjs +11 -44
  19. package/vendor/capabilities/capabilities/admin-capacity-reservations.js +12 -5
  20. package/vendor/capabilities/capabilities/admin-enrollment.js +18 -17
  21. package/vendor/capabilities/capabilities/admin-nodes-lifecycle.js +3 -101
  22. package/vendor/capabilities/capabilities/admin.js +3 -3
  23. package/vendor/capabilities/capabilities/clusters.js +3 -3
  24. package/vendor/capabilities/capabilities/commerce-market.js +9 -9
  25. package/vendor/capabilities/capabilities/compute.js +7 -7
  26. package/vendor/capabilities/capabilities/credits.js +7 -2
  27. package/vendor/capabilities/capabilities/external-buckets.js +4 -4
  28. package/vendor/capabilities/capabilities/fleet.js +109 -29
  29. package/vendor/capabilities/capabilities/listings.js +44 -16
  30. package/vendor/capabilities/capabilities/locality.js +42 -0
  31. package/vendor/capabilities/capabilities/market-shared.js +1 -1
  32. package/vendor/capabilities/capabilities/markets.js +14 -14
  33. package/vendor/capabilities/capabilities/mounts.js +4 -4
  34. package/vendor/capabilities/capabilities/native-listing.js +51 -15
  35. package/vendor/capabilities/capabilities/networks.js +5 -5
  36. package/vendor/capabilities/capabilities/observability.js +49 -14
  37. package/vendor/capabilities/capabilities/tokens.js +5 -9
  38. package/vendor/capabilities/capabilities/users.js +8 -10
  39. package/vendor/capabilities/capabilities/volumes.js +5 -5
@@ -140,7 +140,7 @@ export const launchCapability = cliOnly({
140
140
  order: 2,
141
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.",
142
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>",
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)",
143
+ automation: "ornn launch <canonical-reservation-id> --mode vm|bare-metal|kubernetes|slurm --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)",
144
144
  acceptedReferences: [
145
145
  "canonical capacity-reservation ID",
146
146
  "canonical eligible machine IDs",
@@ -262,7 +262,7 @@ export const machineSwitchCapability = cliOnly({
262
262
  order: 463,
263
263
  outcome: "Change only the explicitly requested VM, Bare Metal, or network goal while retaining SSH-key intent.",
264
264
  human: "ornn machines switch <canonical-reservation-id> [--mode vm|bare-metal] [--network public|private] [--erase-local-data]",
265
- automation: "ornn machines switch <canonical-reservation-id> [--mode vm|bare-metal] [--network public|private] [--erase-local-data] --yes --idempotency-key <key> --format json",
265
+ automation: "ornn machines switch <canonical-reservation-id> [--mode vm|bare-metal] [--network public|private] [--erase-local-data] --idempotency-key <key> --format json",
266
266
  acceptedReferences: ["canonical capacity-reservation ID"],
267
267
  review: "exact reservation, current and requested access mode, network intent, retained SSH keys, access interruption, and permanent local-data erasure for cross-mode changes",
268
268
  resumableStates: ["switch accepted", "SSH ready"],
@@ -287,7 +287,7 @@ export const machineRebootCapability = cliOnly({
287
287
  order: 464,
288
288
  outcome: "Reboot one exact machine and normally wait for restored SSH readiness.",
289
289
  human: "ornn machines reboot <canonical-machine-id|exact-hostname> [--no-wait]",
290
- automation: "ornn machines reboot <canonical-machine-id> --yes --idempotency-key <key> --format json",
290
+ automation: "ornn machines reboot <canonical-machine-id> --idempotency-key <key> --format json",
291
291
  acceptedReferences: [
292
292
  "canonical machine ID",
293
293
  "exact unique hostname",
@@ -316,7 +316,7 @@ export const machineResetCapability = cliOnly({
316
316
  order: 465,
317
317
  outcome: "Hard reset one exact machine only with explicit local-data erasure scope and final consent.",
318
318
  human: "ornn machines reset <canonical-machine-id|exact-hostname> --erase-local-data [--no-wait]",
319
- automation: "ornn machines reset <canonical-machine-id> --erase-local-data --yes --idempotency-key <key> --format json",
319
+ automation: "ornn machines reset <canonical-machine-id> --erase-local-data --idempotency-key <key> --format json",
320
320
  acceptedReferences: [
321
321
  "canonical machine ID",
322
322
  "exact unique hostname",
@@ -392,7 +392,7 @@ export const sshKeyDeleteCapability = cliOnly({
392
392
  order: 452,
393
393
  outcome: "Delete one exact organization SSH key without implying machine delivery.",
394
394
  human: "ornn ssh-keys delete <exact-key-id-or-label>",
395
- automation: "ornn ssh-keys delete <canonical-key-id> --yes --idempotency-key <key> --format json",
395
+ automation: "ornn ssh-keys delete <canonical-key-id> --idempotency-key <key> --format json",
396
396
  acceptedReferences: [
397
397
  "canonical SSH-key ID",
398
398
  "exact unique key label in an eligible terminal",
@@ -443,7 +443,7 @@ export const sshKeyAssignmentAssignCapability = cliOnly({
443
443
  order: 454,
444
444
  outcome: "Assign or replay one exact SSH-key relationship.",
445
445
  human: "ornn ssh-keys assignments assign <canonical-reservation-id> --key <exact-key-id-or-label>",
446
- automation: "ornn ssh-keys assignments assign <canonical-reservation-id> --key <canonical-key-id> --yes --idempotency-key <key> --format json",
446
+ automation: "ornn ssh-keys assignments assign <canonical-reservation-id> --key <canonical-key-id> --idempotency-key <key> --format json",
447
447
  acceptedReferences: [
448
448
  "canonical capacity-reservation ID",
449
449
  "canonical SSH-key ID",
@@ -472,7 +472,7 @@ export const sshKeyAssignmentSyncCapability = cliOnly({
472
472
  order: 455,
473
473
  outcome: "Queue idempotent SSH-key synchronization and report delayed delivery honestly.",
474
474
  human: "ornn ssh-keys assignments sync <canonical-reservation-id>",
475
- automation: "ornn ssh-keys assignments sync <canonical-reservation-id> --yes --idempotency-key <key> --format json",
475
+ automation: "ornn ssh-keys assignments sync <canonical-reservation-id> --idempotency-key <key> --format json",
476
476
  acceptedReferences: ["canonical capacity-reservation ID"],
477
477
  review: "reservation, assigned keys, and target machines",
478
478
  resumableStates: ["queued", "installed", "failed"],
@@ -37,7 +37,7 @@ export const creditGrantCapability = defineCapability({
37
37
  category: "resource",
38
38
  command: ["admin", "organizations", "grant-credit"],
39
39
  outcome: "Grant Ornn credits to a customer organization.",
40
- human: "ornn admin organizations grant-credit --organization UUID --amount-cents N --note TEXT --idempotency-key UUID [--yes]",
40
+ human: "ornn admin organizations grant-credit --organization UUID --amount-cents N --note TEXT --idempotency-key UUID []",
41
41
  automation: "Requires an eligible human terminal.",
42
42
  acceptedReferences: ["organization UUID from admin organizations list"],
43
43
  review: "Review the organization, USD amount, note, and grant UUID before default-no consent.",
@@ -53,6 +53,11 @@ export const creditGrantCapability = defineCapability({
53
53
  format: "uuid",
54
54
  description: "Exact customer organization UUID from organization_list.",
55
55
  },
56
+ adjustmentId: {
57
+ type: "string",
58
+ format: "uuid",
59
+ description: "a new UUID per grant, reused on retries",
60
+ },
56
61
  amountCents: {
57
62
  type: "integer",
58
63
  minimum: 1,
@@ -64,7 +69,7 @@ export const creditGrantCapability = defineCapability({
64
69
  description: "Reason for the credit grant, at most 500 UTF-8 bytes.",
65
70
  },
66
71
  },
67
- required: ["organizationId", "amountCents", "note"],
72
+ required: ["organizationId", "adjustmentId", "amountCents", "note"],
68
73
  },
69
74
  },
70
75
  input: creditGrantInput,
@@ -131,7 +131,7 @@ export const externalBucketCapabilities = [
131
131
  input: connectInput,
132
132
  outcome: "Create or rediscover one external bucket connection with verification kept separate.",
133
133
  human: "ornn external-buckets connect <gcs|s3|r2> --bucket <bucket> [--name <name>] [--prefix <prefix>] [--region <region>] [--account-id <id>|--endpoint-url <url>] [--access-key-id <id> --secret-access-key-file <path>] [--read-only|--read-write]",
134
- automation: "ornn external-buckets connect <gcs|s3|r2> --bucket <bucket> <provider-inputs> --yes --idempotency-key <key> --format json",
134
+ automation: "ornn external-buckets connect <gcs|s3|r2> --bucket <bucket> <provider-inputs> --idempotency-key <key> --format json",
135
135
  review: `Review the exact source, access mode, and credential presence before connecting it. ${gcsImportGuidance}`,
136
136
  related: ["external-buckets verify", "external-buckets import"],
137
137
  }),
@@ -143,7 +143,7 @@ export const externalBucketCapabilities = [
143
143
  input: credentialInput,
144
144
  outcome: "Update one connection's credentials and require a separate verification.",
145
145
  human: "ornn external-buckets update-credentials <external-bucket> --access-key-id <id> --secret-access-key-file <path>",
146
- automation: "ornn external-buckets update-credentials <external-bucket-id> --access-key-id <id> --secret-access-key-file <path> --yes --idempotency-key <key> --format json",
146
+ automation: "ornn external-buckets update-credentials <external-bucket-id> --access-key-id <id> --secret-access-key-file <path> --idempotency-key <key> --format json",
147
147
  acceptedReferences: ["canonical external bucket ID", "exact unique external bucket name"],
148
148
  review: "Credential changes invalidate the prior verification and require renewed review.",
149
149
  related: ["external-buckets verify"],
@@ -169,7 +169,7 @@ export const externalBucketCapabilities = [
169
169
  input: importInput,
170
170
  outcome: "Create or replay exactly one managed working copy with source writeback disabled.",
171
171
  human: "ornn external-buckets import <gcs|s3|r2> <external-bucket> --reservation <canonical-reservation-id> [--no-wait] [--timeout <duration>]",
172
- automation: "ornn external-buckets import <gcs|s3> <external-bucket-id> --reservation <canonical-reservation-id> --yes --idempotency-key <key> [--wait --timeout <duration>] --format json",
172
+ automation: "ornn external-buckets import <gcs|s3> <external-bucket-id> --reservation <canonical-reservation-id> --idempotency-key <key> [--wait --timeout <duration>] --format json",
173
173
  acceptedReferences: ["canonical external bucket ID", "exact unique external bucket name"],
174
174
  review: `Review the isolated destination and explicit no-writeback guarantee before importing. ${gcsImportGuidance}`,
175
175
  resumableStates: ["pending", "working", "partial", "failed", "interrupted"],
@@ -183,7 +183,7 @@ export const externalBucketCapabilities = [
183
183
  input: disconnectInput,
184
184
  outcome: "Remove one external source connection without writing to its source bucket.",
185
185
  human: "ornn external-buckets disconnect <external-bucket>",
186
- automation: "ornn external-buckets disconnect <external-bucket-id> --yes --idempotency-key <key> --format json",
186
+ automation: "ornn external-buckets disconnect <external-bucket-id> --idempotency-key <key> --format json",
187
187
  acceptedReferences: ["canonical external bucket ID", "exact unique external bucket name"],
188
188
  review: "Disconnecting reduces access to the source and is blocked while mounts remain.",
189
189
  related: ["storage detach", "external-buckets show"],
@@ -17,44 +17,41 @@ const suppliersInput = z.object({
17
17
  });
18
18
  const facilitiesInput = z.object({
19
19
  organization_id: z.string().optional(),
20
- organizationId: z.string().optional(),
21
20
  cursor: z.string().optional(),
22
- limit: z.number().int().min(1).max(500).optional(),
21
+ limit: z.number().int().min(1).max(100).optional(),
23
22
  });
24
23
  const nodesInput = z.object({
25
24
  organization_id: z.string().optional(),
26
- organizationId: z.string().optional(),
27
25
  facility_id: z.string().optional(),
28
- facilityId: z.string().optional(),
29
26
  status: z.string().optional(),
30
27
  reservation_id: z.string().optional(),
31
- reservationId: z.string().optional(),
32
28
  cursor: z.string().optional(),
33
29
  max: z.number().int().positive().max(5000).optional(),
34
30
  limit: z.number().int().positive().max(5000).optional(),
35
31
  });
36
32
  const nodeLifecycleInput = z.object({
37
33
  node_id: z.string().min(1),
38
- nodeId: z.string().optional(),
34
+ node_ref: z.string().optional(),
39
35
  reason: z.string().optional(),
40
36
  force: z.boolean().optional(),
37
+ skip_cleanup: z.boolean().optional(),
38
+ delete_node_record: z.literal(true).optional(),
39
+ idempotency_key: z.string().optional(),
40
+ yes: z.boolean().optional(),
41
+ wait: z.boolean().optional(),
42
+ timeout_seconds: z.number().positive().optional(),
41
43
  });
42
44
  const nodeExecInput = z.object({
43
45
  node_id: z.string().min(1),
44
- nodeId: z.string().optional(),
45
46
  command: z.string().min(1),
46
47
  timeout_seconds: z.number().int().positive(),
47
- timeoutSeconds: z.number().int().positive().optional(),
48
48
  yes: z.boolean().optional(),
49
49
  });
50
- function nodeIdOf(input) {
51
- return (input.node_id || input.nodeId || "").trim();
52
- }
53
50
  export const listSuppliersCapability = defineCapability({
54
51
  id: "supplier.list",
55
52
  domain: "fleet",
56
53
  roles: ["admin"],
57
- description: "List supplier organizations (id, name). Resolve supplier names to organization_id with this before nodes.list or facilities.list.",
54
+ description: "List supplier organizations (id, name). Resolve supplier names to organization_id with this before nodes.list or facilities.list. When next_offset is present, pass it back as offset for the next page.",
58
55
  mutation: "read",
59
56
  http: { method: "GET", path: "/v1/organizations" },
60
57
  slack: {
@@ -86,7 +83,7 @@ export const listFacilitiesCapability = defineCapability({
86
83
  id: "facilities.list",
87
84
  domain: "fleet",
88
85
  roles: ["admin"],
89
- description: "List facilities (id, display name, region, supplier organization id). Optional organization_id filter. Resolve facility names with this before nodes.list.",
86
+ description: "List facilities (id, display name, region, supplier organization id). Optional organization_id filter. When nextCursor is present, pass it back as cursor for the next page. Use facilities.show to resolve one facility id.",
90
87
  mutation: "read",
91
88
  http: { method: "GET", path: "/provisioning/facilities" },
92
89
  slack: {
@@ -97,6 +94,16 @@ export const listFacilitiesCapability = defineCapability({
97
94
  type: "string",
98
95
  description: "Optional supplier organization UUID from supplier.list.",
99
96
  },
97
+ cursor: {
98
+ type: "string",
99
+ description: "Pass the previous response's nextCursor to fetch the next page.",
100
+ },
101
+ limit: {
102
+ type: "integer",
103
+ minimum: 1,
104
+ maximum: 100,
105
+ description: "Optional page size (maximum 100).",
106
+ },
100
107
  },
101
108
  },
102
109
  },
@@ -104,12 +111,38 @@ export const listFacilitiesCapability = defineCapability({
104
111
  execute: async (ctx, input) => {
105
112
  const parsed = facilitiesInput.parse(input);
106
113
  return ctx.fetch(queryPath("/provisioning/facilities", {
107
- organizationId: parsed.organization_id ?? parsed.organizationId,
114
+ organizationId: parsed.organization_id,
108
115
  cursor: parsed.cursor,
109
116
  limit: parsed.limit === undefined ? undefined : String(parsed.limit),
110
117
  }), { method: "GET" });
111
118
  },
112
119
  });
120
+ export const facilityShowInput = z.object({ facilityId: z.uuid() });
121
+ export const facilityShowCapability = defineCapability({
122
+ id: "facilities.show",
123
+ domain: "fleet",
124
+ roles: ["admin"],
125
+ description: "Resolve one facility id (e.g. a listing's facilityId) to its display name, region and status.",
126
+ mutation: "read",
127
+ http: { method: "GET", path: "/provisioning/facilities/{facilityId}" },
128
+ cli: false,
129
+ slack: {
130
+ parameters: {
131
+ type: "object",
132
+ properties: {
133
+ facilityId: { type: "string", format: "uuid" },
134
+ },
135
+ required: ["facilityId"],
136
+ },
137
+ },
138
+ input: facilityShowInput,
139
+ execute: async (ctx, input) => {
140
+ const parsed = facilityShowInput.parse(input);
141
+ return ctx.fetch(`/provisioning/facilities/${encodeURIComponent(parsed.facilityId)}`, {
142
+ method: "GET",
143
+ });
144
+ },
145
+ });
113
146
  export const listNodesCapability = defineCapability({
114
147
  id: "nodes.list",
115
148
  domain: "fleet",
@@ -160,10 +193,10 @@ export const listNodesCapability = defineCapability({
160
193
  return ctx.fetch(queryPath("/nodes", pagination), { method: "GET" });
161
194
  }
162
195
  return ctx.fetch(queryPath("/nodes", {
163
- organization_id: parsed.organization_id ?? parsed.organizationId,
164
- facility_id: parsed.facility_id ?? parsed.facilityId,
196
+ organization_id: parsed.organization_id,
197
+ facility_id: parsed.facility_id,
165
198
  status: parsed.status,
166
- reservation_id: parsed.reservation_id ?? parsed.reservationId,
199
+ reservation_id: parsed.reservation_id,
167
200
  ...pagination,
168
201
  }), { method: "GET" });
169
202
  },
@@ -172,9 +205,21 @@ export const nodeOffGridCapability = defineCapability({
172
205
  id: "node.off-grid",
173
206
  domain: "fleet",
174
207
  roles: ["admin"],
175
- description: "Admin-only. Take a node off-grid (remove Ornn agent management, leave the host powered on). CLI asks y/N. Slack waits for a human confirm. MCP requires confirm: true.",
208
+ description: "Admin-only. Take a node off-grid (remove Ornn agent management, leave the host powered on). Slack waits for a human confirm. ",
176
209
  mutation: "preview-confirm",
177
210
  http: { method: "POST", path: "/nodes/{id}/off-grid" },
211
+ cli: {
212
+ command: ["admin", "nodes", "suspend-management"],
213
+ vnext: {
214
+ audience: "admin",
215
+ category: "resource",
216
+ command: ["admin", "nodes", "suspend-management"],
217
+ outcome: "Remove Ornn management and managed access from one powered-on node.",
218
+ human: "ornn admin nodes suspend-management <node-ref> [--reason TEXT] [] [--no-wait]",
219
+ automation: "ornn admin nodes suspend-management <node-id> --idempotency-key KEY --format json",
220
+ review: "Node, supplier, facility, capacity assignment, management removal, and managed-access loss.",
221
+ },
222
+ },
178
223
  slack: {
179
224
  parameters: {
180
225
  type: "object",
@@ -188,14 +233,18 @@ export const nodeOffGridCapability = defineCapability({
188
233
  input: nodeLifecycleInput,
189
234
  execute: async (ctx, input) => {
190
235
  const parsed = nodeLifecycleInput.parse(input);
191
- const id = nodeIdOf(parsed);
192
- const body = { ...(parsed.reason ? { reason: parsed.reason } : {}) };
236
+ const id = parsed.node_id.trim();
237
+ const body = {
238
+ ...(parsed.idempotency_key ? { request_id: parsed.idempotency_key } : {}),
239
+ ...(parsed.reason ? { reason: parsed.reason } : {}),
240
+ };
193
241
  if (!ctx.confirmed) {
194
242
  return preview("node.off-grid", { node_id: id, ...body });
195
243
  }
196
244
  return ctx.fetch(`/nodes/${encodeURIComponent(id)}/off-grid`, {
197
245
  method: "POST",
198
246
  body,
247
+ ...(parsed.idempotency_key ? { headers: { "Idempotency-Key": parsed.idempotency_key } } : {}),
199
248
  });
200
249
  },
201
250
  });
@@ -203,9 +252,21 @@ export const nodeOnGridCapability = defineCapability({
203
252
  id: "node.on-grid",
204
253
  domain: "fleet",
205
254
  roles: ["admin"],
206
- description: "Admin-only. Re-enroll an off-grid node: orchestrator SSHs with the stored admin key and runs the installer with restore_node_id. No caller key. CLI asks y/N. Slack waits for a human confirm. MCP requires confirm: true.",
255
+ description: "Admin-only. Re-enroll an off-grid node: orchestrator SSHs with the stored admin key and runs the installer with restore_node_id. No caller key. Slack waits for a human confirm. ",
207
256
  mutation: "preview-confirm",
208
257
  http: { method: "POST", path: "/nodes/{id}/on-grid" },
258
+ cli: {
259
+ command: ["admin", "nodes", "resume-management"],
260
+ vnext: {
261
+ audience: "admin",
262
+ category: "resource",
263
+ command: ["admin", "nodes", "resume-management"],
264
+ outcome: "Restore Ornn management and managed access on one off-grid node.",
265
+ human: "ornn admin nodes resume-management <node-ref> [] [--no-wait]",
266
+ automation: "ornn admin nodes resume-management <node-id> --idempotency-key KEY --format json",
267
+ review: "Node, supplier, facility, capacity assignment, remote re-enrollment, and restored managed access.",
268
+ },
269
+ },
209
270
  slack: {
210
271
  parameters: {
211
272
  type: "object",
@@ -218,13 +279,16 @@ export const nodeOnGridCapability = defineCapability({
218
279
  input: nodeLifecycleInput,
219
280
  execute: async (ctx, input) => {
220
281
  const parsed = nodeLifecycleInput.parse(input);
221
- const id = nodeIdOf(parsed);
282
+ const id = parsed.node_id.trim();
222
283
  if (!ctx.confirmed) {
223
284
  return preview("node.on-grid", { node_id: id });
224
285
  }
225
286
  return ctx.fetch(`/nodes/${encodeURIComponent(id)}/on-grid`, {
226
287
  method: "POST",
227
- body: {},
288
+ body: {
289
+ ...(parsed.idempotency_key ? { request_id: parsed.idempotency_key } : {}),
290
+ },
291
+ ...(parsed.idempotency_key ? { headers: { "Idempotency-Key": parsed.idempotency_key } } : {}),
228
292
  });
229
293
  },
230
294
  });
@@ -233,7 +297,7 @@ export const nodeExecCapability = defineCapability({
233
297
  domain: "fleet",
234
298
  roles: ["admin"],
235
299
  description: "Admin-only. Run one unrestricted command on an enrolled node over orchestrator SSH with the stored admin key. Always pass timeout_seconds. Not an interactive shell. On NVIDIA hosts use nvidia-smi; on AMD Instinct use amd-smi. Runs on the tool call — no confirm, no y/N.",
236
- mutation: "read",
300
+ mutation: "immediate",
237
301
  http: { method: "POST", path: "/remote" },
238
302
  slack: {
239
303
  parameters: {
@@ -252,8 +316,8 @@ export const nodeExecCapability = defineCapability({
252
316
  input: nodeExecInput,
253
317
  execute: async (ctx, input) => {
254
318
  const parsed = nodeExecInput.parse(input);
255
- const id = nodeIdOf(parsed);
256
- const timeoutSeconds = parsed.timeout_seconds ?? parsed.timeoutSeconds;
319
+ const id = parsed.node_id.trim();
320
+ const timeoutSeconds = parsed.timeout_seconds;
257
321
  return ctx.fetch("/remote", {
258
322
  method: "POST",
259
323
  body: { node_id: id, command: parsed.command, timeout_seconds: timeoutSeconds },
@@ -264,9 +328,21 @@ export const nodeTerminateCapability = defineCapability({
264
328
  id: "node.terminate",
265
329
  domain: "fleet",
266
330
  roles: ["admin"],
267
- description: "Admin-only. Terminate a node record (best-effort clean + delete). Off-grid nodes need force: true. There is no deenroll verb. CLI asks y/N. Slack waits for a human confirm. MCP requires confirm: true.",
331
+ description: "Admin-only. Terminate a node record (best-effort clean + delete). Off-grid nodes need force: true. There is no deenroll verb. Slack waits for a human confirm. ",
268
332
  mutation: "preview-confirm",
269
333
  http: { method: "POST", path: "/nodes/{id}/terminate" },
334
+ cli: {
335
+ command: ["admin", "nodes", "terminate"],
336
+ vnext: {
337
+ audience: "admin",
338
+ category: "resource",
339
+ command: ["admin", "nodes", "terminate"],
340
+ outcome: "End managed access and permanently delete one exact node record.",
341
+ human: "ornn admin nodes terminate <node-ref> --delete-node-record [--skip-cleanup] [--reason TEXT] []",
342
+ automation: "ornn admin nodes terminate <node-id> --delete-node-record --idempotency-key KEY --format json",
343
+ review: "Node, supplier, facility, reservation/capacity, host cleanup, node-record deletion, and access loss.",
344
+ },
345
+ },
270
346
  slack: {
271
347
  parameters: {
272
348
  type: "object",
@@ -281,10 +357,12 @@ export const nodeTerminateCapability = defineCapability({
281
357
  input: nodeLifecycleInput,
282
358
  execute: async (ctx, input) => {
283
359
  const parsed = nodeLifecycleInput.parse(input);
284
- const id = nodeIdOf(parsed);
360
+ const id = parsed.node_id.trim();
361
+ const force = parsed.force ?? parsed.skip_cleanup;
285
362
  const body = {
286
363
  ...(parsed.reason ? { reason: parsed.reason } : {}),
287
- ...(parsed.force !== undefined ? { force: parsed.force } : {}),
364
+ ...(force !== undefined ? { force } : {}),
365
+ ...(parsed.idempotency_key ? { request_id: parsed.idempotency_key } : {}),
288
366
  };
289
367
  if (!ctx.confirmed) {
290
368
  return preview("node.terminate", { node_id: id, ...body });
@@ -292,12 +370,14 @@ export const nodeTerminateCapability = defineCapability({
292
370
  return ctx.fetch(`/nodes/${encodeURIComponent(id)}/terminate`, {
293
371
  method: "POST",
294
372
  body,
373
+ ...(parsed.idempotency_key ? { headers: { "Idempotency-Key": parsed.idempotency_key } } : {}),
295
374
  });
296
375
  },
297
376
  });
298
377
  export const fleetCapabilities = [
299
378
  listSuppliersCapability,
300
379
  listFacilitiesCapability,
380
+ facilityShowCapability,
301
381
  listNodesCapability,
302
382
  nodeOffGridCapability,
303
383
  nodeOnGridCapability,
@@ -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.string().optional(),
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: { type: "string" },
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 facilityQuery = (parsed.site_nickname ?? parsed.site ?? "").trim().toLowerCase();
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
- if ((facilityId && !organizationId) || (!facilityId && (facilityQuery || organizationId))) {
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 listing = organizationId && facilityId && gpuSlug && buyerMicro != null
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
+ }
@@ -21,7 +21,7 @@ export function idempotencyHeaders(ctx, supplied) {
21
21
  }
22
22
  /**
23
23
  * One consent gate for every surface: the CLI reviews the terms in the
24
- * terminal (or takes `--yes`), MCP and Slack return a preview until the
24
+ * terminal (or takes ``), MCP and Slack return a preview until the
25
25
  * caller confirms. `null` means go ahead.
26
26
  */
27
27
  export async function consent(ctx, action, title, terms, payload) {