@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
package/README.md
CHANGED
|
@@ -58,7 +58,7 @@ ornn resources
|
|
|
58
58
|
|
|
59
59
|
The CLI uses Authorization's newest membership on every request, regardless of
|
|
60
60
|
approval. `ornn context` displays that current organization; login and launch
|
|
61
|
-
use the same context.
|
|
61
|
+
use the same context. Reserve browsing and purchasing still require approval.
|
|
62
62
|
|
|
63
63
|
Advanced customer resources are registered under one noun and one verb:
|
|
64
64
|
|
|
@@ -100,13 +100,13 @@ relisted (secondary) listing term with Markets `POST /v1/markets/bookings`
|
|
|
100
100
|
when the window starts. `market` and `offers` call Markets (`/v1/markets`):
|
|
101
101
|
buy `--nodes` at market (Markets also takes GPU counts and limit bids; the
|
|
102
102
|
CLI does not send them yet), end a fill with
|
|
103
|
-
`market end` (Commerce `
|
|
103
|
+
`market end` (Commerce `DELETE /commerce/reservations/{id}`), post an offer
|
|
104
104
|
on a whole primary listing for one of its terms (`--listing-term-id`, optional
|
|
105
105
|
`--duration-seconds`), list the offers your organization made or received, and
|
|
106
106
|
accept or cancel offers by `--offer-id`.
|
|
107
|
-
Prices are integer micro-USD per GPU-hour (`1000000` is $1.00).
|
|
108
|
-
|
|
109
|
-
`--
|
|
107
|
+
Prices are integer micro-USD per GPU-hour (`1000000` is $1.00). Writes execute
|
|
108
|
+
immediately; use `--format json` for machine-readable output and
|
|
109
|
+
`--idempotency-key` for safe retries. Customer payment methods and
|
|
110
110
|
billing stay in the web app. Launch consumes already allocated
|
|
111
111
|
capacity; internal reservation lookups remain necessary for access and
|
|
112
112
|
ownership.
|
|
@@ -116,18 +116,22 @@ Only admins can create primary listings or reservations through
|
|
|
116
116
|
They post to the role-aware `POST /commerce/v2/listings` and Markets
|
|
117
117
|
`POST /v1/markets/bookings` with the admin's own session; the owners enforce
|
|
118
118
|
the admin role. Both commands retain explicit commercial-term validation and
|
|
119
|
-
|
|
119
|
+
execute directly. Reservation creation books a window for a
|
|
120
120
|
customer and takes `--organization-id UUID`, `--listing-id UUID`,
|
|
121
121
|
`--listing-term-id UUID`, `--nodes COUNT`, `--starts-at TIME`, `--ends-at TIME`,
|
|
122
122
|
`--price-per-gpu-hour-micro-usd N`, and `--idempotency-key KEY`. The window is
|
|
123
123
|
whole seconds, the term is non-renewable Reserved, and the price is the term's.
|
|
124
124
|
Reuse the key and payload on retries. Creation requires an eligible human
|
|
125
125
|
terminal. Native capacity creation takes
|
|
126
|
-
`--gpu-slug`, `--
|
|
127
|
-
`--
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
126
|
+
`--gpu-slug`, `--gpus-per-node`, `--facility-id`, `--provisioning-type`,
|
|
127
|
+
`--state`, `--terms` (a JSON array) and `--idempotency-key`, plus optional
|
|
128
|
+
`--organization-id`. Give `--capacity-node-count`, or `--node-ids` (a JSON array)
|
|
129
|
+
to attach nodes at creation; the count then defaults to the number of nodes.
|
|
130
|
+
Primary capacity takes `--days-to-rfs` (0 when ready now) or both
|
|
131
|
+
`--available-at` and `--available-until`, not both. Reuse the key and the same
|
|
132
|
+
body on retries. Prices are integer micro-USD inside terms, with explicit
|
|
133
|
+
duration bounds, nullable deposit, renewable flag and optional
|
|
134
|
+
`preemptNoticeSeconds`.
|
|
131
135
|
`ornn admin capacity-offers update` edits a selected term using `--listing-id`,
|
|
132
136
|
`--listing-terms-id`, the complete term fields and `--idempotency-key`. Financial
|
|
133
137
|
editing remains staff-only. The old customer `capacity-offers update` is removed.
|
|
@@ -151,6 +155,8 @@ Every `ornn admin` command, read or mutation, requires an admin session.
|
|
|
151
155
|
Authorization directory by person name, email, or company name, with bounded `--limit` and
|
|
152
156
|
`--offset` pagination. Users can be restricted by `--organization`; organization
|
|
153
157
|
results can be filtered by `--role`, `--approved`, or `--ids`.
|
|
158
|
+
User, organization, and supplier directory lists return `next_offset`; pass it
|
|
159
|
+
unchanged as the next `--offset`.
|
|
154
160
|
|
|
155
161
|
`ornn admin organizations invite --organization UUID --email EMAIL` emails an
|
|
156
162
|
invitation immediately using the selected organization UUID from the directory.
|
|
@@ -162,8 +168,9 @@ delivery or share the sign-in link before retrying. Use `--format json` for auto
|
|
|
162
168
|
`ornn admin organizations grant-credit --organization UUID --amount-cents 10000
|
|
163
169
|
--note "Customer goodwill credit" --idempotency-key UUID` grants $100.00 in Ornn
|
|
164
170
|
credits to the organization. Find the organization with the directory commands
|
|
165
|
-
above. Like admin reservation creation, it requires
|
|
166
|
-
|
|
171
|
+
above. Like admin reservation creation, it requires an eligible human terminal
|
|
172
|
+
and executes immediately after validating the organization, USD amount, note,
|
|
173
|
+
and UUID.
|
|
167
174
|
The key supplies the existing API's required `adjustmentId`; repeating it with
|
|
168
175
|
the same details returns the same grant. The admin's session supplies audit
|
|
169
176
|
identity. See the [credit-adjustment contract](../../services/commerce/README.md#staff-credit-adjustments).
|
|
@@ -208,15 +215,15 @@ never contains admin children. There is no second parser or command registry.
|
|
|
208
215
|
## Interaction contract
|
|
209
216
|
|
|
210
217
|
An eligible human context requires TTY stdin and TTY stdout and no JSON/NDJSON
|
|
211
|
-
format. Human commands may guide selection
|
|
212
|
-
|
|
218
|
+
format. Human commands may guide selection and render a normalized summary;
|
|
219
|
+
mutations execute immediately without a confirmation prompt.
|
|
213
220
|
|
|
214
221
|
Automation supplies complete semantic inputs and an explicit format:
|
|
215
222
|
|
|
216
223
|
```bash
|
|
217
224
|
ornn status --format json
|
|
218
225
|
ornn machines list --limit 100 --format ndjson
|
|
219
|
-
ornn launch RESERVATION_ID --mode vm
|
|
226
|
+
ornn launch RESERVATION_ID --mode vm \
|
|
220
227
|
--idempotency-key caller-stable-key --format json
|
|
221
228
|
```
|
|
222
229
|
|
|
@@ -225,19 +232,20 @@ Redirected commands require `--format human`, `--format json`, or
|
|
|
225
232
|
prompts or launches a browser. Inputs missing for a safe decision fail before
|
|
226
233
|
network, browser, or process I/O.
|
|
227
234
|
|
|
228
|
-
Global interaction modifiers are `--format`, `--guided`, `--
|
|
235
|
+
Global interaction modifiers are `--format`, `--guided`, `--wait`,
|
|
229
236
|
`--no-wait`, `--timeout`, `--no-browser`, `--overwrite`, and
|
|
230
|
-
`--idempotency-key`.
|
|
231
|
-
|
|
237
|
+
`--idempotency-key`. Destructive operations require explicit intent flags such
|
|
238
|
+
as `--erase-local-data` or `--delete-node-record`. Machine retries require a
|
|
232
239
|
caller-stable idempotency key and reconcile against durable owner state.
|
|
233
240
|
|
|
234
|
-
Permanent deletion and teardown requests
|
|
235
|
-
networks, detached volumes, filesystem deployments, and
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
241
|
+
Permanent deletion and teardown requests execute directly when explicitly
|
|
242
|
+
requested. Private networks, detached volumes, filesystem deployments, and
|
|
243
|
+
Kubernetes or Slurm clusters use their command arguments to express the exact
|
|
244
|
+
operation. Clearing a volume or deleting nested content is live-terminal only
|
|
245
|
+
and does not prompt for consent.
|
|
239
246
|
|
|
240
|
-
|
|
247
|
+
Other paged list commands use bounded `--limit` values and opaque `--cursor`
|
|
248
|
+
values. Pass the returned cursor unchanged as the next `--cursor`. Waits
|
|
241
249
|
poll only retryable transitional states, honor the caller deadline and
|
|
242
250
|
cancellation, and return the last observed state on timeout. Durable Bare Metal
|
|
243
251
|
workspace flows persist local recovery pointers for `launch --resume`.
|
|
@@ -330,8 +338,8 @@ with one replacement and <https://docs.ornn.com/cli#migrating-to-vnext>.
|
|
|
330
338
|
| `--json` | `--format json` |
|
|
331
339
|
| `--interactive` | `--guided` |
|
|
332
340
|
|
|
333
|
-
Changed behavior is intentional: waits are deadline-bound,
|
|
334
|
-
|
|
341
|
+
Changed behavior is intentional: waits are deadline-bound, mutations execute
|
|
342
|
+
directly, pagination is bounded and cursor-based, machine output is always
|
|
335
343
|
enveloped, and exit statuses are semantic. Old help, completion entries,
|
|
336
344
|
fixtures, staff roots, and owner-route spellings are not shipped.
|
|
337
345
|
|
package/package.json
CHANGED
|
@@ -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
|
}
|
|
@@ -50,7 +50,6 @@ export function preflightAdminCommand(catalog, argv, runtime) {
|
|
|
50
50
|
...runtime,
|
|
51
51
|
idempotencyKey: parsed.mutation?.idempotencyKey,
|
|
52
52
|
safety: handler.safety,
|
|
53
|
-
yes: parsed.mutation?.yes,
|
|
54
53
|
});
|
|
55
54
|
}
|
|
56
55
|
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,
|
|
@@ -36,11 +36,6 @@ function parseOptions(argv, allowed, usage) {
|
|
|
36
36
|
if (options[name] !== undefined) {
|
|
37
37
|
throw usageError(`${usage}\n--${name} may be supplied only once.`, name);
|
|
38
38
|
}
|
|
39
|
-
if (name === "yes") {
|
|
40
|
-
if (separator !== -1) throw usageError(`${usage}\n--yes does not take a value.`, "yes");
|
|
41
|
-
options.yes = true;
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
39
|
const parsed = optionValue(argv, index, name, usage);
|
|
45
40
|
options[name] = parsed.value;
|
|
46
41
|
index += parsed.consumed;
|
|
@@ -64,18 +59,13 @@ function timeoutSeconds(value) {
|
|
|
64
59
|
}
|
|
65
60
|
|
|
66
61
|
export function parseAdminNodeDiagnoseArgs(argv) {
|
|
67
|
-
const { options, positionals } = parseOptions(
|
|
68
|
-
argv,
|
|
69
|
-
new Set(["yes", "idempotency-key"]),
|
|
70
|
-
DIAGNOSE_USAGE
|
|
71
|
-
);
|
|
62
|
+
const { options, positionals } = parseOptions(argv, new Set(["idempotency-key"]), DIAGNOSE_USAGE);
|
|
72
63
|
if (positionals.length !== 1 || !positionals[0]) throw usageError(DIAGNOSE_USAGE, "node-id");
|
|
73
64
|
return {
|
|
74
65
|
input: { node_id: positionals[0] },
|
|
75
66
|
mutation: {
|
|
76
67
|
idempotencyKey: options["idempotency-key"],
|
|
77
68
|
safety: ADMIN_MUTATION_SAFETY.eligibleHumanOnly,
|
|
78
|
-
yes: options.yes === true,
|
|
79
69
|
},
|
|
80
70
|
};
|
|
81
71
|
}
|
|
@@ -83,7 +73,7 @@ export function parseAdminNodeDiagnoseArgs(argv) {
|
|
|
83
73
|
export function parseAdminNodeExecArgs(argv) {
|
|
84
74
|
const { options, positionals } = parseOptions(
|
|
85
75
|
argv,
|
|
86
|
-
new Set(["command", "timeout", "
|
|
76
|
+
new Set(["command", "timeout", "idempotency-key"]),
|
|
87
77
|
EXEC_USAGE
|
|
88
78
|
);
|
|
89
79
|
if (positionals.length !== 1 || !positionals[0]) throw usageError(EXEC_USAGE, "node-id");
|
|
@@ -99,7 +89,6 @@ export function parseAdminNodeExecArgs(argv) {
|
|
|
99
89
|
mutation: {
|
|
100
90
|
idempotencyKey: options["idempotency-key"],
|
|
101
91
|
safety: ADMIN_MUTATION_SAFETY.eligibleHumanOnly,
|
|
102
|
-
yes: options.yes === true,
|
|
103
92
|
},
|
|
104
93
|
};
|
|
105
94
|
}
|
|
@@ -272,27 +261,6 @@ function executionResult(raw, input, target) {
|
|
|
272
261
|
};
|
|
273
262
|
}
|
|
274
263
|
|
|
275
|
-
function declinedResult(input) {
|
|
276
|
-
return {
|
|
277
|
-
data: {
|
|
278
|
-
declined: true,
|
|
279
|
-
node: { node_id: input.node_id },
|
|
280
|
-
...(input.command ? { request: requestCorrelation(input) } : {}),
|
|
281
|
-
},
|
|
282
|
-
exitCode: 1,
|
|
283
|
-
outcome: { class: "failed", phase: "consent_declined" },
|
|
284
|
-
action: null,
|
|
285
|
-
resume: null,
|
|
286
|
-
errors: [
|
|
287
|
-
{
|
|
288
|
-
code: "consent_declined",
|
|
289
|
-
message: "The action was declined; no mutation was submitted.",
|
|
290
|
-
retryable: false,
|
|
291
|
-
},
|
|
292
|
-
],
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
|
-
|
|
296
264
|
function interruptedResult(input) {
|
|
297
265
|
return {
|
|
298
266
|
data: {
|
|
@@ -343,7 +311,6 @@ async function runDiagnosis({ input, mutation, runtime, execute, fetch }) {
|
|
|
343
311
|
},
|
|
344
312
|
execute: async () => execute({ node_id: input.node_id }, { confirmed: true }),
|
|
345
313
|
});
|
|
346
|
-
if (result.status === "declined") return declinedResult(input);
|
|
347
314
|
return diagnosisResult(result.data, target);
|
|
348
315
|
} catch (error) {
|
|
349
316
|
if (wasInterrupted(error, runtime)) return interruptedResult(input);
|
|
@@ -388,7 +355,6 @@ async function runExecution({ input, mutation, runtime, execute, fetch }) {
|
|
|
388
355
|
{ confirmed: true }
|
|
389
356
|
),
|
|
390
357
|
});
|
|
391
|
-
if (result.status === "declined") return declinedResult(input);
|
|
392
358
|
return executionResult(result.data, input, target);
|
|
393
359
|
} catch (error) {
|
|
394
360
|
if (wasInterrupted(error, runtime)) return interruptedResult(input);
|