@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.
- package/README.md +35 -27
- package/package.json +1 -1
- package/src/admin-capacity-reservations.mjs +2 -12
- package/src/admin-command-handlers.mjs +0 -1
- package/src/admin-credits.mjs +1 -2
- package/src/admin-enrollment.mjs +22 -30
- package/src/admin-mutations.mjs +6 -56
- package/src/admin-node-diagnostics.mjs +2 -36
- package/src/admin-node-lifecycle.mjs +25 -43
- package/src/admin-shell.mjs +39 -32
- package/src/catalog-shell.mjs +4 -18
- package/src/external-bucket-commands.mjs +5 -73
- package/src/interaction-runtime.mjs +2 -37
- package/src/kubernetes-commands.mjs +11 -102
- package/src/launch-mode-handlers.mjs +0 -1
- package/src/mount-commands.mjs +25 -131
- package/src/storage-commands.mjs +2 -87
- package/src/vm-commands.mjs +11 -44
- package/vendor/capabilities/capabilities/admin-capacity-reservations.js +12 -5
- package/vendor/capabilities/capabilities/admin-enrollment.js +18 -17
- package/vendor/capabilities/capabilities/admin-nodes-lifecycle.js +3 -101
- package/vendor/capabilities/capabilities/admin.js +3 -3
- package/vendor/capabilities/capabilities/clusters.js +3 -3
- package/vendor/capabilities/capabilities/commerce-market.js +9 -9
- package/vendor/capabilities/capabilities/compute.js +7 -7
- package/vendor/capabilities/capabilities/credits.js +7 -2
- package/vendor/capabilities/capabilities/external-buckets.js +4 -4
- package/vendor/capabilities/capabilities/fleet.js +109 -29
- package/vendor/capabilities/capabilities/listings.js +44 -16
- package/vendor/capabilities/capabilities/locality.js +42 -0
- package/vendor/capabilities/capabilities/market-shared.js +1 -1
- package/vendor/capabilities/capabilities/markets.js +14 -14
- package/vendor/capabilities/capabilities/mounts.js +4 -4
- package/vendor/capabilities/capabilities/native-listing.js +51 -15
- package/vendor/capabilities/capabilities/networks.js +5 -5
- package/vendor/capabilities/capabilities/observability.js +49 -14
- package/vendor/capabilities/capabilities/tokens.js +5 -9
- package/vendor/capabilities/capabilities/users.js +8 -10
- 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
|
|
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]
|
|
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>
|
|
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
|
|
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>
|
|
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>
|
|
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>
|
|
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 [
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
164
|
-
facility_id: parsed.facility_id
|
|
196
|
+
organization_id: parsed.organization_id,
|
|
197
|
+
facility_id: parsed.facility_id,
|
|
165
198
|
status: parsed.status,
|
|
166
|
-
reservation_id: parsed.reservation_id
|
|
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).
|
|
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 =
|
|
192
|
-
const body = {
|
|
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.
|
|
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 =
|
|
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: "
|
|
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 =
|
|
256
|
-
const timeoutSeconds = parsed.timeout_seconds
|
|
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.
|
|
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 =
|
|
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
|
-
...(
|
|
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
|
|
34
|
+
site_nickname: z
|
|
35
|
+
.string()
|
|
36
|
+
.optional()
|
|
37
|
+
.describe("A state, city, address, or catalog region such as us-west-3."),
|
|
34
38
|
site_supplier: z.string().optional(),
|
|
35
39
|
ram: z.string().optional(),
|
|
36
40
|
storage: z.string().optional(),
|
|
@@ -67,7 +71,10 @@ const slackListingCreateParams = {
|
|
|
67
71
|
fabric_type: { type: "string" },
|
|
68
72
|
node_count: { type: "integer" },
|
|
69
73
|
gpus_per_node: { type: "integer" },
|
|
70
|
-
site_nickname: {
|
|
74
|
+
site_nickname: {
|
|
75
|
+
type: "string",
|
|
76
|
+
description: "Where the capacity is, exactly as the user said it: a state, city, address, or catalog region such as us-west-3. Ornn places it in a catalog region.",
|
|
77
|
+
},
|
|
71
78
|
site_supplier: { type: "string" },
|
|
72
79
|
ram: { type: "string" },
|
|
73
80
|
storage: { type: "string" },
|
|
@@ -242,7 +249,7 @@ export const createListingCapability = defineCapability({
|
|
|
242
249
|
let organizationId = parsed.organizationId ?? parsed.organization_id;
|
|
243
250
|
let facilityId = parsed.facilityId ?? parsed.facility_id;
|
|
244
251
|
const supplierQuery = (parsed.site_supplier ?? "").trim().toLowerCase();
|
|
245
|
-
const
|
|
252
|
+
const place = (parsed.site_nickname ?? parsed.site ?? "").trim();
|
|
246
253
|
if (!organizationId && supplierQuery) {
|
|
247
254
|
const matches = (await listSupplierOrganizations(ctx.fetch)).filter((org) => org.id.toLowerCase() === supplierQuery || org.name.toLowerCase() === supplierQuery);
|
|
248
255
|
if (matches.length !== 1) {
|
|
@@ -250,7 +257,19 @@ export const createListingCapability = defineCapability({
|
|
|
250
257
|
}
|
|
251
258
|
organizationId = matches[0].id;
|
|
252
259
|
}
|
|
253
|
-
|
|
260
|
+
let located = null;
|
|
261
|
+
let lookupUnavailable = false;
|
|
262
|
+
if (!facilityId && place) {
|
|
263
|
+
try {
|
|
264
|
+
located = await locateRegion(ctx.fetch, place);
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
if (ctx.confirmed)
|
|
268
|
+
throw error;
|
|
269
|
+
lookupUnavailable = true;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if ((facilityId && !organizationId) || (!facilityId && !place && organizationId)) {
|
|
254
273
|
const matches = [];
|
|
255
274
|
const seenCursors = new Set();
|
|
256
275
|
let cursor;
|
|
@@ -262,14 +281,7 @@ export const createListingCapability = defineCapability({
|
|
|
262
281
|
throw new Error("invalid facility catalog");
|
|
263
282
|
matches.push(...response.facilities.filter((facility) => (!organizationId ||
|
|
264
283
|
(facility.organizationId ?? facility.organization_id) === organizationId) &&
|
|
265
|
-
(facilityId
|
|
266
|
-
? (facility.facilityId ?? facility.id) === facilityId
|
|
267
|
-
: !facilityQuery ||
|
|
268
|
-
[
|
|
269
|
-
facility.facilityId ?? facility.id,
|
|
270
|
-
facility.region,
|
|
271
|
-
facility.displayName ?? facility.display_name,
|
|
272
|
-
].some((value) => value?.toLowerCase() === facilityQuery))));
|
|
284
|
+
(!facilityId || (facility.facilityId ?? facility.id) === facilityId)));
|
|
273
285
|
cursor = response.nextCursor ?? undefined;
|
|
274
286
|
if (cursor) {
|
|
275
287
|
if (seenCursors.has(cursor) || seenCursors.size >= 49) {
|
|
@@ -302,6 +314,10 @@ export const createListingCapability = defineCapability({
|
|
|
302
314
|
const gpusPerNode = parsed.gpus_per_node ?? 8;
|
|
303
315
|
const boundPayload = bound.payload;
|
|
304
316
|
const draft = Object.fromEntries(Object.entries(boundPayload).filter(([key]) => !LISTING_CREATE_INPUT_ONLY_KEYS.includes(key)));
|
|
317
|
+
if (located)
|
|
318
|
+
draft.site_nickname = located.name;
|
|
319
|
+
else if (place && !facilityId && !lookupUnavailable)
|
|
320
|
+
delete draft.site_nickname;
|
|
305
321
|
const gpuSlug = normalized.gpu_type
|
|
306
322
|
? resolveCatalogTerm(hardware.terms, "gpu", normalized.gpu_type).canonical
|
|
307
323
|
: null;
|
|
@@ -318,11 +334,12 @@ export const createListingCapability = defineCapability({
|
|
|
318
334
|
percent: parsed.deposit_percent,
|
|
319
335
|
})
|
|
320
336
|
: 0);
|
|
321
|
-
const
|
|
337
|
+
const priced = Boolean(organizationId && gpuSlug && buyerMicro != null);
|
|
338
|
+
const listingAt = (facility) => priced && facility
|
|
322
339
|
? primaryListingBody({
|
|
323
|
-
organizationId,
|
|
324
|
-
facilityId,
|
|
325
|
-
gpuSlug,
|
|
340
|
+
organizationId: organizationId,
|
|
341
|
+
facilityId: facility,
|
|
342
|
+
gpuSlug: gpuSlug,
|
|
326
343
|
gpusPerNode,
|
|
327
344
|
provisioningType: parsed.access_mode ?? "virtual-machine",
|
|
328
345
|
availableAt: startsAt,
|
|
@@ -337,13 +354,24 @@ export const createListingCapability = defineCapability({
|
|
|
337
354
|
}, buyerMicro, downPaymentCents)
|
|
338
355
|
: null;
|
|
339
356
|
if (!ctx.confirmed) {
|
|
357
|
+
const listing = listingAt(facilityId);
|
|
340
358
|
return preview("create_listing", {
|
|
341
359
|
...draft,
|
|
342
360
|
node_count: nodeCount,
|
|
343
361
|
gpus_per_node: gpusPerNode,
|
|
362
|
+
...(located ? { facility_region: located.region } : {}),
|
|
363
|
+
...(lookupUnavailable
|
|
364
|
+
? { location_unavailable: place, location_hint: LOCATION_LOOKUP_UNAVAILABLE }
|
|
365
|
+
: place && !facilityId && !located
|
|
366
|
+
? { location_unresolved: place, location_hint: unplacedLocation(place) }
|
|
367
|
+
: {}),
|
|
344
368
|
...(listing ? { listing } : {}),
|
|
345
369
|
});
|
|
346
370
|
}
|
|
371
|
+
if (located && !facilityId && priced) {
|
|
372
|
+
facilityId = (await ensureFacility(ctx.fetch, organizationId, located.region)).facilityId;
|
|
373
|
+
}
|
|
374
|
+
const listing = listingAt(facilityId);
|
|
347
375
|
if (!listing) {
|
|
348
376
|
return {
|
|
349
377
|
ok: false,
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { statusOf } from "./staff-shared.js";
|
|
2
|
+
export const LOCALITY_GEOLOCATE_PATH = "/v1/orchestrator/locality/geolocate";
|
|
3
|
+
export const FACILITY_ENSURE_PATH = "/provisioning/facilities/ensure";
|
|
4
|
+
/**
|
|
5
|
+
* `GET /v1/orchestrator/locality/geolocate?address=`: the catalog region a typed
|
|
6
|
+
* place stands in. Orchestrator answers a region code or name as itself and
|
|
7
|
+
* geocodes anything else (state, city, address) to its nearest region.
|
|
8
|
+
* Null means Orchestrator could not place the text (404); an unavailable
|
|
9
|
+
* geocoder or gateway throws, so callers ask for a retry rather than new text.
|
|
10
|
+
*/
|
|
11
|
+
export async function locateRegion(fetch, place) {
|
|
12
|
+
const address = place.trim();
|
|
13
|
+
if (!address)
|
|
14
|
+
return null;
|
|
15
|
+
let located;
|
|
16
|
+
try {
|
|
17
|
+
located = (await fetch(`${LOCALITY_GEOLOCATE_PATH}?address=${encodeURIComponent(address)}`, {
|
|
18
|
+
method: "GET",
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
if (statusOf(error) === 404)
|
|
23
|
+
return null;
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
const region = typeof located?.facility_slug === "string" ? located.facility_slug.trim() : "";
|
|
27
|
+
if (!region)
|
|
28
|
+
return null;
|
|
29
|
+
const name = typeof located?.region === "string" && located.region.trim() ? located.region.trim() : region;
|
|
30
|
+
return { region, name };
|
|
31
|
+
}
|
|
32
|
+
export function unplacedLocation(place) {
|
|
33
|
+
return `Ornn could not place "${place}". Give a state, city, address, or catalog region such as us-west-3.`;
|
|
34
|
+
}
|
|
35
|
+
export const LOCATION_LOOKUP_UNAVAILABLE = "Ornn's location lookup is unavailable right now. Keep this location and try again in a moment.";
|
|
36
|
+
/** `POST /internal/v1/facilities/ensure`: the organization's facility at a catalog region. */
|
|
37
|
+
export async function ensureFacility(fetch, organizationId, region) {
|
|
38
|
+
return (await fetch(FACILITY_ENSURE_PATH, {
|
|
39
|
+
method: "POST",
|
|
40
|
+
body: { organizationId, region },
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
@@ -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
|
|
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) {
|