@ornncompute/cli 1.0.72 → 1.0.97
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 +55 -24
- package/package.json +4 -2
- package/src/admin-capacity-reservations.mjs +6 -13
- package/src/admin-command-handlers.mjs +2 -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 +3 -41
- package/src/admin-node-lifecycle.mjs +40 -160
- package/src/admin-node-sanitize.mjs +243 -0
- package/src/admin-shell.mjs +39 -32
- package/src/catalog-shell.mjs +6 -6
- package/src/evidence-commands.mjs +4 -6
- package/src/external-bucket-commands.mjs +76 -96
- package/src/interaction-runtime.mjs +5 -37
- package/src/kubernetes-commands.mjs +25 -183
- package/src/launch-mode-handlers.mjs +0 -1
- package/src/machine-reference.mjs +33 -56
- package/src/mount-commands.mjs +25 -131
- package/src/slurm-commands.mjs +13 -2
- package/src/storage-commands.mjs +2 -87
- package/src/vm-commands.mjs +48 -101
- package/vendor/api-client/commerce.js +743 -0
- package/vendor/capabilities/cli.js +7688 -7
- package/vendor/capabilities/capabilities/admin-capacity-reservations.js +0 -101
- package/vendor/capabilities/capabilities/admin-enrollment.js +0 -321
- package/vendor/capabilities/capabilities/admin-node-diagnostics.js +0 -88
- package/vendor/capabilities/capabilities/admin-nodes-lifecycle.js +0 -235
- package/vendor/capabilities/capabilities/admin.js +0 -91
- package/vendor/capabilities/capabilities/cli.js +0 -156
- package/vendor/capabilities/capabilities/clusters.js +0 -248
- package/vendor/capabilities/capabilities/commerce-market.js +0 -405
- package/vendor/capabilities/capabilities/compute.js +0 -523
- package/vendor/capabilities/capabilities/credits.js +0 -78
- package/vendor/capabilities/capabilities/evidence.js +0 -145
- package/vendor/capabilities/capabilities/external-buckets.js +0 -191
- package/vendor/capabilities/capabilities/fleet.js +0 -306
- package/vendor/capabilities/capabilities/identity.js +0 -129
- package/vendor/capabilities/capabilities/listings.js +0 -398
- package/vendor/capabilities/capabilities/locality.js +0 -42
- package/vendor/capabilities/capabilities/market-shared.js +0 -46
- package/vendor/capabilities/capabilities/markets.js +0 -482
- package/vendor/capabilities/capabilities/mounts.js +0 -176
- package/vendor/capabilities/capabilities/native-listing.js +0 -99
- package/vendor/capabilities/capabilities/networks.js +0 -1264
- package/vendor/capabilities/capabilities/observability.js +0 -218
- package/vendor/capabilities/capabilities/organization-invites.js +0 -53
- package/vendor/capabilities/capabilities/reservation-list.js +0 -68
- package/vendor/capabilities/capabilities/staff-shared.js +0 -64
- package/vendor/capabilities/capabilities/tokens.js +0 -165
- package/vendor/capabilities/capabilities/users.js +0 -159
- package/vendor/capabilities/capabilities/volumes.js +0 -178
- package/vendor/capabilities/capability.js +0 -99
- package/vendor/capabilities/cli-catalog.js +0 -73
- package/vendor/capabilities/cli-migrations.js +0 -264
- package/vendor/capabilities/names.js +0 -6
- package/vendor/capabilities/software-catalog.js +0 -82
- package/vendor/capabilities/spec-catalog.js +0 -151
- package/vendor/capabilities/supplier-directory.js +0 -20
package/README.md
CHANGED
|
@@ -7,8 +7,17 @@ sends browser and CLI traffic only through the API gateway.
|
|
|
7
7
|
For the customer install and usage guide, see [`docs/cli.md`](../../docs/cli.md).
|
|
8
8
|
The shared capability catalog in `packages/capabilities` is the authority for
|
|
9
9
|
command identity, role visibility, input schemas, help, and resource discovery.
|
|
10
|
-
The CLI imports `@ornn/capabilities/cli`; packaging includes
|
|
11
|
-
|
|
10
|
+
The CLI imports `@ornn/capabilities/cli`; packaging includes its dependencies,
|
|
11
|
+
including `@ornn/api-client`, and excludes MCP-only catalog handlers. These
|
|
12
|
+
packages run without workspace links or React.
|
|
13
|
+
|
|
14
|
+
Native Commerce requests use the caller’s bearer directly through the gateway:
|
|
15
|
+
`/commerce/*` for public APIs and `/internal/v1/*` for staff APIs.
|
|
16
|
+
The gateway and Commerce enforce the caller’s organization and staff permissions.
|
|
17
|
+
All Commerce operations use `@ornn/api-client/commerce`, including the reservation
|
|
18
|
+
reads inside launch, connect, and evidence flows. Customer reservation pickers
|
|
19
|
+
explicitly scope every page to the selected organization, including for staff.
|
|
20
|
+
The gateway retains its Commerce workflow adapters for already-published CLI versions.
|
|
12
21
|
|
|
13
22
|
For GCS imports, start with the [source-bucket permissions and version-specific CLI guide](https://docs.ornn.com/cli#gcs-imports-and-storage-transfer-service).
|
|
14
23
|
It documents the current CLI 1.x GCS verification limitation. Scoped
|
|
@@ -82,6 +91,9 @@ filesystems deploy | show | wait | delete
|
|
|
82
91
|
```
|
|
83
92
|
|
|
84
93
|
`ssh-keys list` uses backend cursors; see [pagination](../../docs/cli.md#advanced-customer-resources).
|
|
94
|
+
Organization key deletion saves removal on reservation lists before revoking the
|
|
95
|
+
account key. If preparation fails, retry the deletion; re-registering the key
|
|
96
|
+
does not restore its removed reservation access.
|
|
85
97
|
|
|
86
98
|
Market commands call one gateway route each and print that route's JSON.
|
|
87
99
|
`capacity-offers list|show|terms` reads native Commerce capacity
|
|
@@ -104,9 +116,11 @@ CLI does not send them yet), end a fill with
|
|
|
104
116
|
on a whole primary listing for one of its terms (`--listing-term-id`, optional
|
|
105
117
|
`--duration-seconds`), list the offers your organization made or received, and
|
|
106
118
|
accept or cancel offers by `--offer-id`.
|
|
107
|
-
Prices are integer micro-USD per GPU-hour (`1000000` is $1.00)
|
|
108
|
-
|
|
109
|
-
|
|
119
|
+
Prices are integer micro-USD per GPU-hour (`1000000` is $1.00); CPU capacity
|
|
120
|
+
(a null GPU type, traded on the `cpu` book) is priced and counted per vCPU in
|
|
121
|
+
the same fields. Writes execute
|
|
122
|
+
immediately; use `--format json` for machine-readable output and
|
|
123
|
+
`--idempotency-key` for safe retries. Customer payment methods and
|
|
110
124
|
billing stay in the web app. Launch consumes already allocated
|
|
111
125
|
capacity; internal reservation lookups remain necessary for access and
|
|
112
126
|
ownership.
|
|
@@ -116,7 +130,7 @@ Only admins can create primary listings or reservations through
|
|
|
116
130
|
They post to the role-aware `POST /commerce/v2/listings` and Markets
|
|
117
131
|
`POST /v1/markets/bookings` with the admin's own session; the owners enforce
|
|
118
132
|
the admin role. Both commands retain explicit commercial-term validation and
|
|
119
|
-
|
|
133
|
+
execute directly. Reservation creation books a window for a
|
|
120
134
|
customer and takes `--organization-id UUID`, `--listing-id UUID`,
|
|
121
135
|
`--listing-term-id UUID`, `--nodes COUNT`, `--starts-at TIME`, `--ends-at TIME`,
|
|
122
136
|
`--price-per-gpu-hour-micro-usd N`, and `--idempotency-key KEY`. The window is
|
|
@@ -125,8 +139,10 @@ Reuse the key and payload on retries. Creation requires an eligible human
|
|
|
125
139
|
terminal. Native capacity creation takes
|
|
126
140
|
`--gpu-slug`, `--gpus-per-node`, `--facility-id`, `--provisioning-type`,
|
|
127
141
|
`--state`, `--terms` (a JSON array) and `--idempotency-key`, plus optional
|
|
128
|
-
`--organization-id`.
|
|
129
|
-
|
|
142
|
+
`--organization-id`. CPU capacity takes `--gpu-slug cpu` and
|
|
143
|
+
`--cores-per-node` (a whole number of at least 1; each core is 2 vCPUs) instead
|
|
144
|
+
of `--gpus-per-node`; its term prices are per vCPU-hour. Give
|
|
145
|
+
`--capacity-node-count`, or `--node-ids` (a JSON array) to attach nodes at creation; the count then defaults to the number of nodes.
|
|
130
146
|
Primary capacity takes `--days-to-rfs` (0 when ready now) or both
|
|
131
147
|
`--available-at` and `--available-until`, not both. Reuse the key and the same
|
|
132
148
|
body on retries. Prices are integer micro-USD inside terms, with explicit
|
|
@@ -155,6 +171,8 @@ Every `ornn admin` command, read or mutation, requires an admin session.
|
|
|
155
171
|
Authorization directory by person name, email, or company name, with bounded `--limit` and
|
|
156
172
|
`--offset` pagination. Users can be restricted by `--organization`; organization
|
|
157
173
|
results can be filtered by `--role`, `--approved`, or `--ids`.
|
|
174
|
+
User, organization, and supplier directory lists return `next_offset`; pass it
|
|
175
|
+
unchanged as the next `--offset`.
|
|
158
176
|
|
|
159
177
|
`ornn admin organizations invite --organization UUID --email EMAIL` emails an
|
|
160
178
|
invitation immediately using the selected organization UUID from the directory.
|
|
@@ -166,8 +184,9 @@ delivery or share the sign-in link before retrying. Use `--format json` for auto
|
|
|
166
184
|
`ornn admin organizations grant-credit --organization UUID --amount-cents 10000
|
|
167
185
|
--note "Customer goodwill credit" --idempotency-key UUID` grants $100.00 in Ornn
|
|
168
186
|
credits to the organization. Find the organization with the directory commands
|
|
169
|
-
above. Like admin reservation creation, it requires
|
|
170
|
-
|
|
187
|
+
above. Like admin reservation creation, it requires an eligible human terminal
|
|
188
|
+
and executes immediately after validating the organization, USD amount, note,
|
|
189
|
+
and UUID.
|
|
171
190
|
The key supplies the existing API's required `adjustmentId`; repeating it with
|
|
172
191
|
the same details returns the same grant. The admin's session supplies audit
|
|
173
192
|
identity. See the [credit-adjustment contract](../../services/commerce/README.md#staff-credit-adjustments).
|
|
@@ -184,6 +203,17 @@ include the final newline required by OpenSSH, both in files and with `--print`.
|
|
|
184
203
|
`ornn admin nodes enroll` takes optional `--filter COMPARTMENT_IDS`
|
|
185
204
|
(omit to run every compartment step); the retired `--mode` flag is gone. The
|
|
186
205
|
gateway, not CLI metadata, authoritatively enforces the admin role.
|
|
206
|
+
`ornn admin nodes sanitize`, `sanitize-status`, and `sanitize-decide` drive one
|
|
207
|
+
node's held sanitize job, sharing the capability with MCP
|
|
208
|
+
`admin_node_sanitize_*` and Slack `node_sanitize_*`. `sanitize` needs
|
|
209
|
+
`--operation`, its exact `--confirmation` phrase, and `--recovery-uuid` for
|
|
210
|
+
`ram_roundtrip` (refused for `data_wipe`); the job then waits for staff approval.
|
|
211
|
+
`sanitize-decide --confirmation "ACCEPT MANUAL CLEANUP"` also needs
|
|
212
|
+
`--reservation`, `--release-operation` (the Orchestrator release operation),
|
|
213
|
+
`--reason`, `--evidence-file` (only its SHA-256 is sent), and each attestation
|
|
214
|
+
flag: `--tenant-access-revoked`, `--data-drives-sanitized`, and
|
|
215
|
+
`--acknowledge-retained-os`. Sanitize writes run only in an eligible human
|
|
216
|
+
terminal. See [manual cleanup with the boot OS retained](../../docs/provisioning/dedicated-machine-runbook.md#manual-cleanup-with-the-boot-os-retained).
|
|
187
217
|
Suppliers are Authorization supplier organizations: `ornn admin suppliers list`
|
|
188
218
|
returns their `id` and `name`, and `--supplier UUID` on `facilities list`,
|
|
189
219
|
`nodes list`, `enrollment-tokens create`, and `nodes enroll` takes that
|
|
@@ -212,15 +242,15 @@ never contains admin children. There is no second parser or command registry.
|
|
|
212
242
|
## Interaction contract
|
|
213
243
|
|
|
214
244
|
An eligible human context requires TTY stdin and TTY stdout and no JSON/NDJSON
|
|
215
|
-
format. Human commands may guide selection
|
|
216
|
-
|
|
245
|
+
format. Human commands may guide selection and render a normalized summary;
|
|
246
|
+
mutations execute immediately without a confirmation prompt.
|
|
217
247
|
|
|
218
248
|
Automation supplies complete semantic inputs and an explicit format:
|
|
219
249
|
|
|
220
250
|
```bash
|
|
221
251
|
ornn status --format json
|
|
222
252
|
ornn machines list --limit 100 --format ndjson
|
|
223
|
-
ornn launch RESERVATION_ID --mode vm
|
|
253
|
+
ornn launch RESERVATION_ID --mode vm \
|
|
224
254
|
--idempotency-key caller-stable-key --format json
|
|
225
255
|
```
|
|
226
256
|
|
|
@@ -229,19 +259,20 @@ Redirected commands require `--format human`, `--format json`, or
|
|
|
229
259
|
prompts or launches a browser. Inputs missing for a safe decision fail before
|
|
230
260
|
network, browser, or process I/O.
|
|
231
261
|
|
|
232
|
-
Global interaction modifiers are `--format`, `--guided`, `--
|
|
262
|
+
Global interaction modifiers are `--format`, `--guided`, `--wait`,
|
|
233
263
|
`--no-wait`, `--timeout`, `--no-browser`, `--overwrite`, and
|
|
234
|
-
`--idempotency-key`.
|
|
235
|
-
|
|
264
|
+
`--idempotency-key`. Destructive operations require explicit intent flags such
|
|
265
|
+
as `--erase-local-data` or `--delete-node-record`. Machine retries require a
|
|
236
266
|
caller-stable idempotency key and reconcile against durable owner state.
|
|
237
267
|
|
|
238
|
-
Permanent deletion and teardown requests
|
|
239
|
-
networks, detached volumes, filesystem deployments, and
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
268
|
+
Permanent deletion and teardown requests execute directly when explicitly
|
|
269
|
+
requested. Private networks, detached volumes, filesystem deployments, and
|
|
270
|
+
Kubernetes or Slurm clusters use their command arguments to express the exact
|
|
271
|
+
operation. Clearing a volume or deleting nested content is live-terminal only
|
|
272
|
+
and does not prompt for consent.
|
|
243
273
|
|
|
244
|
-
|
|
274
|
+
Other paged list commands use bounded `--limit` values and opaque `--cursor`
|
|
275
|
+
values. Pass the returned cursor unchanged as the next `--cursor`. Waits
|
|
245
276
|
poll only retryable transitional states, honor the caller deadline and
|
|
246
277
|
cancellation, and return the last observed state on timeout. Durable Bare Metal
|
|
247
278
|
workspace flows persist local recovery pointers for `launch --resume`.
|
|
@@ -334,8 +365,8 @@ with one replacement and <https://docs.ornn.com/cli#migrating-to-vnext>.
|
|
|
334
365
|
| `--json` | `--format json` |
|
|
335
366
|
| `--interactive` | `--guided` |
|
|
336
367
|
|
|
337
|
-
Changed behavior is intentional: waits are deadline-bound,
|
|
338
|
-
|
|
368
|
+
Changed behavior is intentional: waits are deadline-bound, mutations execute
|
|
369
|
+
directly, pagination is bounded and cursor-based, machine output is always
|
|
339
370
|
enveloped, and exit statuses are semantic. Old help, completion entries,
|
|
340
371
|
fixtures, staff roots, and owner-route spellings are not shipped.
|
|
341
372
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ornncompute/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.97",
|
|
4
4
|
"description": "Command-line interface for Ornn compute access workflows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -35,7 +35,9 @@
|
|
|
35
35
|
"test": "node --test 'test/*.test.mjs'"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@ornn/
|
|
38
|
+
"@ornn/api-client": "0.1.0",
|
|
39
|
+
"@ornn/capabilities": "0.1.0",
|
|
40
|
+
"esbuild": "^0.28.2"
|
|
39
41
|
},
|
|
40
42
|
"engines": {
|
|
41
43
|
"node": ">=20"
|
|
@@ -5,7 +5,7 @@ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-
|
|
|
5
5
|
|
|
6
6
|
const USAGE = {
|
|
7
7
|
"reservations.create":
|
|
8
|
-
"Usage: ornn admin reservations create --organization-id ID --listing-id ID --listing-term-id ID --nodes COUNT --starts-at TIME --ends-at TIME --price-per-gpu-hour-micro-usd N --idempotency-key KEY
|
|
8
|
+
"Usage: ornn admin reservations create --organization-id ID --listing-id ID --listing-term-id ID --nodes COUNT --starts-at TIME --ends-at TIME --price-per-gpu-hour-micro-usd N --idempotency-key KEY",
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
function parseOptions(argv, { values, positionals }, usage) {
|
|
@@ -17,14 +17,6 @@ function parseOptions(argv, { values, positionals }, usage) {
|
|
|
17
17
|
positionalValues.push(argument);
|
|
18
18
|
continue;
|
|
19
19
|
}
|
|
20
|
-
if (argument === "--yes") {
|
|
21
|
-
if (options.yes !== undefined)
|
|
22
|
-
throw new AdminMutationError(`${usage}\n--yes may be supplied only once.`, {
|
|
23
|
-
field: "yes",
|
|
24
|
-
});
|
|
25
|
-
options.yes = true;
|
|
26
|
-
continue;
|
|
27
|
-
}
|
|
28
20
|
const separator = argument.indexOf("=");
|
|
29
21
|
const name = argument.slice(2, separator === -1 ? undefined : separator);
|
|
30
22
|
if (!values.includes(name)) throw new AdminMutationError(`${usage}\nUnknown option: --${name}`);
|
|
@@ -84,14 +76,13 @@ function timestamp(options, name, usage) {
|
|
|
84
76
|
}
|
|
85
77
|
|
|
86
78
|
function parseCapacityOffer(argv) {
|
|
87
|
-
const { $positionals,
|
|
79
|
+
const { $positionals, ...input } = parseSemanticArguments(argv);
|
|
88
80
|
if ($positionals.length) throw new AdminMutationError("Use named options for listing writes.");
|
|
89
81
|
return {
|
|
90
82
|
input,
|
|
91
83
|
mutation: {
|
|
92
84
|
idempotencyKey: input.idempotencyKey,
|
|
93
85
|
safety: ADMIN_MUTATION_SAFETY.eligibleHumanOnly,
|
|
94
|
-
yes: yes === true,
|
|
95
86
|
},
|
|
96
87
|
};
|
|
97
88
|
}
|
|
@@ -135,7 +126,6 @@ function parseReservationCreate(argv) {
|
|
|
135
126
|
mutation: {
|
|
136
127
|
idempotencyKey,
|
|
137
128
|
safety: ADMIN_MUTATION_SAFETY.eligibleHumanOnly,
|
|
138
|
-
yes: options.yes === true,
|
|
139
129
|
},
|
|
140
130
|
};
|
|
141
131
|
}
|
|
@@ -163,7 +153,10 @@ function reservationCreateReview(input) {
|
|
|
163
153
|
{ label: "Nodes", value: input.nodes },
|
|
164
154
|
{ label: "Starts at", value: input.startsAt },
|
|
165
155
|
{ label: "Ends at", value: input.endsAt },
|
|
166
|
-
{
|
|
156
|
+
{
|
|
157
|
+
label: "Price per unit-hour (micro-USD; per vCPU on CPU capacity)",
|
|
158
|
+
value: input.pricePerGpuHourMicroUsd,
|
|
159
|
+
},
|
|
167
160
|
{ label: "Idempotency key", value: input.idempotencyKey },
|
|
168
161
|
],
|
|
169
162
|
consequence:
|
|
@@ -3,6 +3,7 @@ import { adminCreditHandlers } from "./admin-credits.mjs";
|
|
|
3
3
|
import { adminEnrollmentHandlers } from "./admin-enrollment.mjs";
|
|
4
4
|
import { adminNodeDiagnosticsHandlers } from "./admin-node-diagnostics.mjs";
|
|
5
5
|
import { adminNodeLifecycleHandlers } from "./admin-node-lifecycle.mjs";
|
|
6
|
+
import { adminNodeSanitizeHandlers } from "./admin-node-sanitize.mjs";
|
|
6
7
|
import { ADMIN_MUTATION_SAFETY, assertAdminMutationContext } from "./admin-mutations.mjs";
|
|
7
8
|
|
|
8
9
|
const handlers = [
|
|
@@ -11,6 +12,7 @@ const handlers = [
|
|
|
11
12
|
...adminEnrollmentHandlers,
|
|
12
13
|
...adminNodeDiagnosticsHandlers,
|
|
13
14
|
...adminNodeLifecycleHandlers,
|
|
15
|
+
...adminNodeSanitizeHandlers,
|
|
14
16
|
];
|
|
15
17
|
|
|
16
18
|
export function adminCommandHandler(capability) {
|
|
@@ -50,7 +52,6 @@ export function preflightAdminCommand(catalog, argv, runtime) {
|
|
|
50
52
|
...runtime,
|
|
51
53
|
idempotencyKey: parsed.mutation?.idempotencyKey,
|
|
52
54
|
safety: handler.safety,
|
|
53
|
-
yes: parsed.mutation?.yes,
|
|
54
55
|
});
|
|
55
56
|
}
|
|
56
57
|
return { capabilityId: capability.id, command: command.join(" "), parsed };
|
package/src/admin-credits.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { ADMIN_MUTATION_SAFETY, AdminMutationError, runAdminMutation } from "./a
|
|
|
2
2
|
import { parseSemanticArguments } from "./interaction-runtime.mjs";
|
|
3
3
|
|
|
4
4
|
function parseGrant(argv, capability) {
|
|
5
|
-
const { $positionals,
|
|
5
|
+
const { $positionals, organization, amountCents, note, idempotencyKey, ...extra } =
|
|
6
6
|
parseSemanticArguments(argv);
|
|
7
7
|
if ($positionals.length || Object.keys(extra).length) {
|
|
8
8
|
throw new AdminMutationError(`Usage: ${capability.cli.vnext.human}`);
|
|
@@ -21,7 +21,6 @@ function parseGrant(argv, capability) {
|
|
|
21
21
|
mutation: {
|
|
22
22
|
idempotencyKey: parsed.data.adjustmentId,
|
|
23
23
|
safety: ADMIN_MUTATION_SAFETY.eligibleHumanOnly,
|
|
24
|
-
yes: yes === true,
|
|
25
24
|
},
|
|
26
25
|
};
|
|
27
26
|
}
|
package/src/admin-enrollment.mjs
CHANGED
|
@@ -11,15 +11,15 @@ const ENROLLMENT_LOG_READ_TIMEOUT_MS = 5_000;
|
|
|
11
11
|
|
|
12
12
|
const USAGE = {
|
|
13
13
|
"tokens.create":
|
|
14
|
-
"Usage: ornn admin enrollment-tokens create --supplier UUID --expires-in SECONDS (--print | --output PATH) [--overwrite]
|
|
15
|
-
"
|
|
16
|
-
"Usage: ornn admin enrollment-tokens revoke <token-id> [--expected-created-at TIME] [--
|
|
14
|
+
"Usage: ornn admin enrollment-tokens create --supplier UUID --expires-in SECONDS (--print | --output PATH) [--overwrite]",
|
|
15
|
+
"enrollment.token.revoke":
|
|
16
|
+
"Usage: ornn admin enrollment-tokens revoke <token-id> [--expected-created-at TIME] [--idempotency-key KEY]",
|
|
17
17
|
"nodes.enroll":
|
|
18
|
-
"Usage: ornn admin nodes enroll <node-id> --host HOST --supplier UUID --facility UUID [--filter COMPARTMENT_IDS] --identity-file PATH --token-ttl SECONDS --replace-existing-management [--verbose]
|
|
18
|
+
"Usage: ornn admin nodes enroll <node-id> --host HOST --supplier UUID --facility UUID [--filter COMPARTMENT_IDS] --identity-file PATH --token-ttl SECONDS --replace-existing-management [--verbose]",
|
|
19
19
|
"nodes.export-key":
|
|
20
20
|
"Usage: ornn admin nodes export-key <node-id> (--print | --output PATH) [--overwrite]",
|
|
21
21
|
"nodes.rotate-key":
|
|
22
|
-
"Usage: ornn admin nodes rotate-key <node-id> (--print | --output PATH) [--overwrite]
|
|
22
|
+
"Usage: ornn admin nodes rotate-key <node-id> (--print | --output PATH) [--overwrite]",
|
|
23
23
|
};
|
|
24
24
|
|
|
25
25
|
function parseOptions(argv, { booleans = [], positionals, values }, usage) {
|
|
@@ -108,7 +108,7 @@ function parseTokenCreate(argv) {
|
|
|
108
108
|
const { options } = parseOptions(
|
|
109
109
|
argv,
|
|
110
110
|
{
|
|
111
|
-
booleans: ["overwrite", "print"
|
|
111
|
+
booleans: ["overwrite", "print"],
|
|
112
112
|
positionals: 0,
|
|
113
113
|
values: ["supplier", "expires-in", "output"],
|
|
114
114
|
},
|
|
@@ -127,21 +127,22 @@ function parseTokenCreate(argv) {
|
|
|
127
127
|
},
|
|
128
128
|
mutation: {
|
|
129
129
|
safety: ADMIN_MUTATION_SAFETY.eligibleHumanOnly,
|
|
130
|
-
yes: options.yes === true,
|
|
131
130
|
},
|
|
132
131
|
};
|
|
133
132
|
}
|
|
134
133
|
|
|
135
134
|
function parseTokenRevoke(argv) {
|
|
136
|
-
const usage = USAGE["
|
|
135
|
+
const usage = USAGE["enrollment.token.revoke"];
|
|
137
136
|
const { options, positionals } = parseOptions(
|
|
138
137
|
argv,
|
|
139
|
-
{ booleans: [
|
|
138
|
+
{ booleans: [], positionals: 1, values: ["expected-created-at", "idempotency-key"] },
|
|
140
139
|
usage
|
|
141
140
|
);
|
|
141
|
+
const tokenId = uuid(positionals[0], "token-id");
|
|
142
142
|
return {
|
|
143
143
|
input: {
|
|
144
|
-
|
|
144
|
+
tokenId,
|
|
145
|
+
token_id: tokenId,
|
|
145
146
|
...(options["expected-created-at"]?.trim()
|
|
146
147
|
? { expected_created_at: options["expected-created-at"].trim() }
|
|
147
148
|
: {}),
|
|
@@ -152,7 +153,6 @@ function parseTokenRevoke(argv) {
|
|
|
152
153
|
mutation: {
|
|
153
154
|
idempotencyKey: options["idempotency-key"]?.trim(),
|
|
154
155
|
safety: ADMIN_MUTATION_SAFETY.reconcile,
|
|
155
|
-
yes: options.yes === true,
|
|
156
156
|
},
|
|
157
157
|
};
|
|
158
158
|
}
|
|
@@ -162,7 +162,7 @@ function parseNodeEnroll(argv) {
|
|
|
162
162
|
const { options, positionals } = parseOptions(
|
|
163
163
|
argv,
|
|
164
164
|
{
|
|
165
|
-
booleans: ["replace-existing-management", "verbose"
|
|
165
|
+
booleans: ["replace-existing-management", "verbose"],
|
|
166
166
|
positionals: 1,
|
|
167
167
|
values: [
|
|
168
168
|
"facility",
|
|
@@ -179,12 +179,9 @@ function parseNodeEnroll(argv) {
|
|
|
179
179
|
);
|
|
180
180
|
const filter = options.filter?.trim();
|
|
181
181
|
if (!options["replace-existing-management"]) {
|
|
182
|
-
throw new AdminMutationError(
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
field: "replace-existing-management",
|
|
186
|
-
}
|
|
187
|
-
);
|
|
182
|
+
throw new AdminMutationError(`${usage}\n--replace-existing-management is required.`, {
|
|
183
|
+
field: "replace-existing-management",
|
|
184
|
+
});
|
|
188
185
|
}
|
|
189
186
|
return {
|
|
190
187
|
input: {
|
|
@@ -207,7 +204,6 @@ function parseNodeEnroll(argv) {
|
|
|
207
204
|
},
|
|
208
205
|
mutation: {
|
|
209
206
|
safety: ADMIN_MUTATION_SAFETY.eligibleHumanOnly,
|
|
210
|
-
yes: options.yes === true,
|
|
211
207
|
},
|
|
212
208
|
};
|
|
213
209
|
}
|
|
@@ -218,7 +214,7 @@ function parseNodeKey(argv, rotate) {
|
|
|
218
214
|
const { options, positionals } = parseOptions(
|
|
219
215
|
argv,
|
|
220
216
|
{
|
|
221
|
-
booleans:
|
|
217
|
+
booleans: ["overwrite", "print"],
|
|
222
218
|
positionals: 1,
|
|
223
219
|
values: ["output"],
|
|
224
220
|
},
|
|
@@ -233,7 +229,6 @@ function parseNodeKey(argv, rotate) {
|
|
|
233
229
|
? {
|
|
234
230
|
mutation: {
|
|
235
231
|
safety: ADMIN_MUTATION_SAFETY.eligibleHumanOnly,
|
|
236
|
-
yes: options.yes === true,
|
|
237
232
|
},
|
|
238
233
|
}
|
|
239
234
|
: {}),
|
|
@@ -353,7 +348,7 @@ function reviewFor(capabilityId, input, prepared) {
|
|
|
353
348
|
consequence: "Creates a bearer credential that can enroll a node until it expires.",
|
|
354
349
|
};
|
|
355
350
|
}
|
|
356
|
-
if (capabilityId === "
|
|
351
|
+
if (capabilityId === "enrollment.token.revoke") {
|
|
357
352
|
const target = prepared?.target;
|
|
358
353
|
return {
|
|
359
354
|
title: "Revoke enrollment token",
|
|
@@ -374,9 +369,7 @@ function reviewFor(capabilityId, input, prepared) {
|
|
|
374
369
|
{ label: "SSH target", value: `${input.username}@${input.host}:${input.port}` },
|
|
375
370
|
{ label: "Supplier", value: input.organization_id },
|
|
376
371
|
{ label: "Facility", value: input.facility_id },
|
|
377
|
-
...(input.filter
|
|
378
|
-
? [{ label: "Compartment filter", value: input.filter }]
|
|
379
|
-
: []),
|
|
372
|
+
...(input.filter ? [{ label: "Compartment filter", value: input.filter }] : []),
|
|
380
373
|
{ label: "Token lifetime", value: `${input.expires_in_seconds} seconds` },
|
|
381
374
|
],
|
|
382
375
|
consequence:
|
|
@@ -573,7 +566,7 @@ async function runEnrollmentMutation({ capability, execute, fetch, input, mutati
|
|
|
573
566
|
assertSecretOutputContext(input, runtime);
|
|
574
567
|
await refuseExistingOutput(input.export);
|
|
575
568
|
}
|
|
576
|
-
if (capabilityId === "
|
|
569
|
+
if (capabilityId === "enrollment.token.revoke" && runtime.machine && !input.expected_created_at) {
|
|
577
570
|
throw new AdminMutationError(
|
|
578
571
|
"Machine revocation requires --expected-created-at from the listed token generation.",
|
|
579
572
|
{ code: "exact_token_identity_required", field: "expected-created-at" }
|
|
@@ -585,7 +578,7 @@ async function runEnrollmentMutation({ capability, execute, fetch, input, mutati
|
|
|
585
578
|
...mutation,
|
|
586
579
|
stdout: runtime.stderr ?? runtime.stdout,
|
|
587
580
|
prepare: async () => {
|
|
588
|
-
if (capabilityId === "
|
|
581
|
+
if (capabilityId === "enrollment.token.revoke") {
|
|
589
582
|
const prepared = await execute(input);
|
|
590
583
|
return {
|
|
591
584
|
review: reviewFor(capabilityId, input, prepared),
|
|
@@ -599,7 +592,7 @@ async function runEnrollmentMutation({ capability, execute, fetch, input, mutati
|
|
|
599
592
|
return { review: reviewFor(capabilityId, input), value: input };
|
|
600
593
|
},
|
|
601
594
|
reconcile:
|
|
602
|
-
capabilityId === "
|
|
595
|
+
capabilityId === "enrollment.token.revoke"
|
|
603
596
|
? async (value) => {
|
|
604
597
|
if (value.prepared.disposition === "satisfied") {
|
|
605
598
|
return {
|
|
@@ -635,7 +628,6 @@ async function runEnrollmentMutation({ capability, execute, fetch, input, mutati
|
|
|
635
628
|
},
|
|
636
629
|
});
|
|
637
630
|
|
|
638
|
-
if (execution.status === "declined") return { ...execution, suppressResult: true };
|
|
639
631
|
if (!SECRET_OUTPUT.has(capabilityId)) return execution;
|
|
640
632
|
const exported = await exportSecret({
|
|
641
633
|
capabilityId,
|
|
@@ -655,7 +647,7 @@ export const adminEnrollmentHandlers = [
|
|
|
655
647
|
safety: ADMIN_MUTATION_SAFETY.eligibleHumanOnly,
|
|
656
648
|
},
|
|
657
649
|
{
|
|
658
|
-
capabilityId: "
|
|
650
|
+
capabilityId: "enrollment.token.revoke",
|
|
659
651
|
parse: parseTokenRevoke,
|
|
660
652
|
run: runEnrollmentMutation,
|
|
661
653
|
safety: ADMIN_MUTATION_SAFETY.reconcile,
|
package/src/admin-mutations.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { createInterface } from "node:readline/promises";
|
|
2
|
-
|
|
3
1
|
export const ADMIN_MUTATION_SAFETY = Object.freeze({
|
|
2
|
+
direct: "direct",
|
|
4
3
|
eligibleHumanOnly: "eligible-human-only",
|
|
5
4
|
reconcile: "reconcile",
|
|
6
5
|
});
|
|
@@ -46,32 +45,8 @@ export function writeAdminMutationReview(stdout, review) {
|
|
|
46
45
|
}
|
|
47
46
|
}
|
|
48
47
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
try {
|
|
52
|
-
const answer = await readline.question("Proceed? [y/N] ");
|
|
53
|
-
return /^(y|yes)$/i.test(answer.trim());
|
|
54
|
-
} catch (error) {
|
|
55
|
-
if (
|
|
56
|
-
error?.code === "ERR_USE_AFTER_CLOSE" ||
|
|
57
|
-
error?.code === "ABORT_ERR" ||
|
|
58
|
-
error?.name === "AbortError"
|
|
59
|
-
) {
|
|
60
|
-
return false;
|
|
61
|
-
}
|
|
62
|
-
throw error;
|
|
63
|
-
} finally {
|
|
64
|
-
readline.close();
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export function assertAdminMutationContext({
|
|
69
|
-
eligibleHuman,
|
|
70
|
-
idempotencyKey,
|
|
71
|
-
machine,
|
|
72
|
-
safety,
|
|
73
|
-
yes,
|
|
74
|
-
}) {
|
|
48
|
+
export function assertAdminMutationContext({ eligibleHuman, idempotencyKey, machine, safety }) {
|
|
49
|
+
if (safety === ADMIN_MUTATION_SAFETY.direct) return;
|
|
75
50
|
if (safety === ADMIN_MUTATION_SAFETY.eligibleHumanOnly) {
|
|
76
51
|
if (!eligibleHuman || machine) {
|
|
77
52
|
throw new AdminMutationError(
|
|
@@ -86,12 +61,6 @@ export function assertAdminMutationContext({
|
|
|
86
61
|
code: "admin_mutation_policy_missing",
|
|
87
62
|
});
|
|
88
63
|
}
|
|
89
|
-
if (!eligibleHuman && yes !== true) {
|
|
90
|
-
throw new AdminMutationError("Noninteractive admin mutations require --yes.", {
|
|
91
|
-
code: "consent_required",
|
|
92
|
-
field: "yes",
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
64
|
if (!eligibleHuman && !idempotencyKey?.trim()) {
|
|
96
65
|
throw new AdminMutationError(
|
|
97
66
|
"Noninteractive admin mutations require --idempotency-key with a caller-stable value.",
|
|
@@ -100,12 +69,6 @@ export function assertAdminMutationContext({
|
|
|
100
69
|
}
|
|
101
70
|
}
|
|
102
71
|
|
|
103
|
-
/**
|
|
104
|
-
* Shared privileged-mutation gate. All noninteractive prerequisites are checked
|
|
105
|
-
* before prepare/reconcile/execute can perform I/O. Domain handlers own semantic
|
|
106
|
-
* validation and exact goal comparison; this function owns context safety,
|
|
107
|
-
* normalized review, default-no consent, and the apply/satisfied/conflict flow.
|
|
108
|
-
*/
|
|
109
72
|
export async function runAdminMutation({
|
|
110
73
|
eligibleHuman,
|
|
111
74
|
execute,
|
|
@@ -114,11 +77,9 @@ export async function runAdminMutation({
|
|
|
114
77
|
prepare,
|
|
115
78
|
reconcile,
|
|
116
79
|
safety,
|
|
117
|
-
stdin,
|
|
118
80
|
stdout,
|
|
119
|
-
yes = false,
|
|
120
81
|
}) {
|
|
121
|
-
assertAdminMutationContext({ eligibleHuman, idempotencyKey, machine, safety
|
|
82
|
+
assertAdminMutationContext({ eligibleHuman, idempotencyKey, machine, safety });
|
|
122
83
|
if (typeof prepare !== "function" || typeof execute !== "function") {
|
|
123
84
|
throw new AdminMutationError("Admin mutation handler is incomplete.", {
|
|
124
85
|
code: "admin_mutation_handler_incomplete",
|
|
@@ -132,8 +93,8 @@ export async function runAdminMutation({
|
|
|
132
93
|
|
|
133
94
|
let prepared = await prepare();
|
|
134
95
|
if (!prepared || typeof prepared !== "object") {
|
|
135
|
-
throw new AdminMutationError("Admin mutation preparation returned no
|
|
136
|
-
code: "
|
|
96
|
+
throw new AdminMutationError("Admin mutation preparation returned no operation.", {
|
|
97
|
+
code: "admin_preparation_invalid",
|
|
137
98
|
});
|
|
138
99
|
}
|
|
139
100
|
|
|
@@ -161,22 +122,11 @@ export async function runAdminMutation({
|
|
|
161
122
|
}
|
|
162
123
|
prepared = {
|
|
163
124
|
...prepared,
|
|
164
|
-
...(reconciliation.review ? { review: reconciliation.review } : {}),
|
|
165
125
|
...(reconciliation.value !== undefined ? { value: reconciliation.value } : {}),
|
|
166
126
|
};
|
|
167
127
|
}
|
|
168
128
|
|
|
169
129
|
if (!machine) writeAdminMutationReview(stdout, prepared.review);
|
|
170
|
-
if (eligibleHuman && yes !== true && !(await defaultNoConsent(stdin, stdout))) {
|
|
171
|
-
return {
|
|
172
|
-
data: { submitted: false },
|
|
173
|
-
exitCode: 1,
|
|
174
|
-
outcome: { class: "ended", phase: "declined" },
|
|
175
|
-
reconciled: false,
|
|
176
|
-
status: "declined",
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
|
|
180
130
|
return {
|
|
181
131
|
data: await execute(prepared.value),
|
|
182
132
|
exitCode: 0,
|