@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.
Files changed (59) hide show
  1. package/README.md +55 -24
  2. package/package.json +4 -2
  3. package/src/admin-capacity-reservations.mjs +6 -13
  4. package/src/admin-command-handlers.mjs +2 -1
  5. package/src/admin-credits.mjs +1 -2
  6. package/src/admin-enrollment.mjs +22 -30
  7. package/src/admin-mutations.mjs +6 -56
  8. package/src/admin-node-diagnostics.mjs +3 -41
  9. package/src/admin-node-lifecycle.mjs +40 -160
  10. package/src/admin-node-sanitize.mjs +243 -0
  11. package/src/admin-shell.mjs +39 -32
  12. package/src/catalog-shell.mjs +6 -6
  13. package/src/evidence-commands.mjs +4 -6
  14. package/src/external-bucket-commands.mjs +76 -96
  15. package/src/interaction-runtime.mjs +5 -37
  16. package/src/kubernetes-commands.mjs +25 -183
  17. package/src/launch-mode-handlers.mjs +0 -1
  18. package/src/machine-reference.mjs +33 -56
  19. package/src/mount-commands.mjs +25 -131
  20. package/src/slurm-commands.mjs +13 -2
  21. package/src/storage-commands.mjs +2 -87
  22. package/src/vm-commands.mjs +48 -101
  23. package/vendor/api-client/commerce.js +743 -0
  24. package/vendor/capabilities/cli.js +7688 -7
  25. package/vendor/capabilities/capabilities/admin-capacity-reservations.js +0 -101
  26. package/vendor/capabilities/capabilities/admin-enrollment.js +0 -321
  27. package/vendor/capabilities/capabilities/admin-node-diagnostics.js +0 -88
  28. package/vendor/capabilities/capabilities/admin-nodes-lifecycle.js +0 -235
  29. package/vendor/capabilities/capabilities/admin.js +0 -91
  30. package/vendor/capabilities/capabilities/cli.js +0 -156
  31. package/vendor/capabilities/capabilities/clusters.js +0 -248
  32. package/vendor/capabilities/capabilities/commerce-market.js +0 -405
  33. package/vendor/capabilities/capabilities/compute.js +0 -523
  34. package/vendor/capabilities/capabilities/credits.js +0 -78
  35. package/vendor/capabilities/capabilities/evidence.js +0 -145
  36. package/vendor/capabilities/capabilities/external-buckets.js +0 -191
  37. package/vendor/capabilities/capabilities/fleet.js +0 -306
  38. package/vendor/capabilities/capabilities/identity.js +0 -129
  39. package/vendor/capabilities/capabilities/listings.js +0 -398
  40. package/vendor/capabilities/capabilities/locality.js +0 -42
  41. package/vendor/capabilities/capabilities/market-shared.js +0 -46
  42. package/vendor/capabilities/capabilities/markets.js +0 -482
  43. package/vendor/capabilities/capabilities/mounts.js +0 -176
  44. package/vendor/capabilities/capabilities/native-listing.js +0 -99
  45. package/vendor/capabilities/capabilities/networks.js +0 -1264
  46. package/vendor/capabilities/capabilities/observability.js +0 -218
  47. package/vendor/capabilities/capabilities/organization-invites.js +0 -53
  48. package/vendor/capabilities/capabilities/reservation-list.js +0 -68
  49. package/vendor/capabilities/capabilities/staff-shared.js +0 -64
  50. package/vendor/capabilities/capabilities/tokens.js +0 -165
  51. package/vendor/capabilities/capabilities/users.js +0 -159
  52. package/vendor/capabilities/capabilities/volumes.js +0 -178
  53. package/vendor/capabilities/capability.js +0 -99
  54. package/vendor/capabilities/cli-catalog.js +0 -73
  55. package/vendor/capabilities/cli-migrations.js +0 -264
  56. package/vendor/capabilities/names.js +0 -6
  57. package/vendor/capabilities/software-catalog.js +0 -82
  58. package/vendor/capabilities/spec-catalog.js +0 -151
  59. package/vendor/capabilities/supplier-directory.js +0 -20
@@ -1,218 +0,0 @@
1
- import { z } from "zod";
2
- import { defineCapability } from "../capability.js";
3
- function queryPath(path, params) {
4
- const parts = [];
5
- for (const [key, value] of Object.entries(params)) {
6
- if (value?.trim())
7
- parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(value.trim())}`);
8
- }
9
- return parts.length ? `${path}?${parts.join("&")}` : path;
10
- }
11
- function nodePrefix(role, id) {
12
- const encoded = encodeURIComponent(id);
13
- return role === "user"
14
- ? `/v1/observability/machines/${encoded}`
15
- : `/v1/observability/nodes/${encoded}`;
16
- }
17
- const latestInput = z.object({
18
- node_id: z.string().min(1),
19
- nodeId: z.string().optional(),
20
- start: z.string().optional(),
21
- end: z.string().optional(),
22
- span: z.string().optional(),
23
- max_points: z.number().int().min(1).max(500).optional(),
24
- metrics: z.string().optional(),
25
- });
26
- const logsLatestInput = z.object({
27
- node_id: z.string().min(1),
28
- nodeId: z.string().optional(),
29
- stream: z.enum(["serial", "kernel"]).default("kernel"),
30
- count: z.number().int().min(1).max(1000).optional(),
31
- before: z.string().optional(),
32
- start: z.string().optional(),
33
- cursor: z.string().optional(),
34
- });
35
- const tailInput = z.object({
36
- node_id: z.string().min(1),
37
- nodeId: z.string().optional(),
38
- duration: z.number().int().min(1).max(120).optional(),
39
- max_events: z.number().int().min(1).max(200).optional(),
40
- fields: z.string().optional(),
41
- });
42
- const logsTailInput = tailInput.extend({
43
- stream: z.enum(["serial", "kernel"]).default("kernel"),
44
- });
45
- function nodeId(input) {
46
- return (input.node_id || input.nodeId || "").trim();
47
- }
48
- function expandSpan(span, start, end) {
49
- if (start?.trim() && end?.trim()) {
50
- return { start: start.trim(), end: end.trim() };
51
- }
52
- const now = Date.now();
53
- const match = /^(\d+)(m|h|d)$/.exec((span ?? "15m").trim());
54
- const amount = match ? Number(match[1]) : 15;
55
- const unit = match?.[2] ?? "m";
56
- const ms = unit === "d" ? amount * 86_400_000 : unit === "h" ? amount * 3_600_000 : amount * 60_000;
57
- return { start: new Date(now - ms).toISOString(), end: new Date(now).toISOString() };
58
- }
59
- export const telemetryLatestCapability = defineCapability({
60
- id: "telemetry.latest",
61
- domain: "observability",
62
- roles: ["user", "admin"],
63
- description: "One-shot telemetry window for one node (start/end or span plus max_points). Users may only query a reserved machine. Admins may query any enrolled node. Pass a node UUID. Use this for heartbeat / last-seen / public_ip. Not a live stream.",
64
- mutation: "read",
65
- http: { method: "GET", path: "/v1/observability/nodes/{id}/telemetry/query" },
66
- slack: {
67
- parameters: {
68
- type: "object",
69
- properties: {
70
- node_id: { type: "string", description: "Node UUID from nodes.list." },
71
- span: { type: "string", description: "Lookback like 15m, 1h, 1d. Default 15m." },
72
- start: { type: "string" },
73
- end: { type: "string" },
74
- max_points: { type: "number", description: "Default 240, max 500." },
75
- metrics: { type: "string", description: "Optional comma-separated metric names; omit for all stored metrics." },
76
- },
77
- required: ["node_id"],
78
- },
79
- },
80
- input: latestInput,
81
- execute: async (ctx, input) => {
82
- const parsed = latestInput.parse(input);
83
- const id = nodeId(parsed);
84
- const range = expandSpan(parsed.span, parsed.start, parsed.end);
85
- return ctx.fetch(queryPath(`${nodePrefix(ctx.role, id)}/telemetry/query`, {
86
- start: range.start,
87
- end: range.end,
88
- max_points: String(parsed.max_points ?? 240),
89
- metrics: parsed.metrics,
90
- }), { method: "GET" });
91
- },
92
- });
93
- export const logsLatestCapability = defineCapability({
94
- id: "logs.latest",
95
- domain: "observability",
96
- roles: ["user", "admin"],
97
- description: "Stored kernel or serial logs, all severities, newest first. Optional start/before bounds can query any retained history; default is the seven days before now. First call omits cursor; subsequent pages pass next_cursor with the same start/before. Users only on a reserved machine. Not a live tail.",
98
- mutation: "read",
99
- http: { method: "GET", path: "/v1/observability/nodes/{id}/logs/{stream}/query" },
100
- slack: {
101
- parameters: {
102
- type: "object",
103
- properties: {
104
- node_id: { type: "string", description: "Node UUID from nodes.list." },
105
- stream: { type: "string", enum: ["serial", "kernel"] },
106
- count: { type: "number", description: "Page size. Default 200, max 1000." },
107
- before: { type: "string", description: "RFC3339 upper bound. Default now. Keep fixed when paging." },
108
- start: { type: "string", description: "RFC3339 lower bound. Default seven days before the upper bound." },
109
- cursor: { type: "string", description: "next_cursor from the previous page." },
110
- },
111
- required: ["node_id"],
112
- },
113
- },
114
- input: logsLatestInput,
115
- execute: async (ctx, input) => {
116
- const parsed = logsLatestInput.parse(input);
117
- const id = nodeId(parsed);
118
- const end = parsed.before?.trim() || new Date().toISOString();
119
- const start = parsed.start?.trim() || new Date(Date.parse(end) - 7 * 86_400_000).toISOString();
120
- return ctx.fetch(queryPath(`${nodePrefix(ctx.role, id)}/logs/${parsed.stream}/query`, {
121
- start,
122
- end,
123
- limit: String(parsed.count ?? 200),
124
- order: "desc",
125
- cursor: parsed.cursor,
126
- }), { method: "GET" });
127
- },
128
- });
129
- /** Full stored telemetry rows, including context fields omitted by numeric windows. */
130
- export const telemetrySnapshotCapability = defineCapability({
131
- id: "telemetry.snapshot",
132
- domain: "observability",
133
- roles: ["admin"],
134
- description: "Read the latest stored heartbeat with all staff-visible fields, including version, status, port context, and kernel-log coverage when stored. Works while the node is offline; check observed_at for staleness. limit=1 returns the latest row; larger limits only scan a bounded recent window, at most 24h. Use telemetry.latest for historical numeric windows.",
135
- mutation: "read",
136
- http: { method: "GET", path: "/v1/observability/nodes/{id}/telemetry/latest" },
137
- mcp: false,
138
- slack: {
139
- parameters: {
140
- type: "object",
141
- properties: {
142
- node_id: { type: "string", description: "Node UUID." },
143
- limit: { type: "number", description: "Recent full heartbeat rows. Default 1, max 1000." },
144
- },
145
- required: ["node_id"],
146
- },
147
- },
148
- input: z.object({ node_id: z.string().min(1), limit: z.number().int().min(1).max(1000).optional() }),
149
- execute: async (ctx, input) => ctx.fetch(queryPath(`/v1/observability/nodes/${encodeURIComponent(input.node_id)}/telemetry/latest`, {
150
- limit: String(input.limit ?? 1),
151
- }), { method: "GET" }),
152
- });
153
- export const telemetryTailCapability = defineCapability({
154
- id: "telemetry.tail",
155
- domain: "observability",
156
- roles: ["user", "admin"],
157
- description: "Live telemetry follow on the same Observability subscribe feed the graphs use. Users only on a reserved machine. Slack and MCP attach for duration (default 20s) and return events. Not telemetry.latest.",
158
- mutation: "read",
159
- http: { method: "GET", path: "/v1/observability/nodes/{id}/telemetry/subscribe" },
160
- slack: {
161
- parameters: {
162
- type: "object",
163
- properties: {
164
- node_id: { type: "string", description: "Node UUID from nodes.list." },
165
- duration: { type: "number", description: "Seconds to attach. Default 20, max 120." },
166
- max_events: { type: "number" },
167
- fields: { type: "string" },
168
- },
169
- required: ["node_id"],
170
- },
171
- },
172
- input: tailInput,
173
- execute: async (ctx, input) => {
174
- const parsed = tailInput.parse(input);
175
- const id = nodeId(parsed);
176
- return ctx.fetch(queryPath(`${nodePrefix(ctx.role, id)}/telemetry/subscribe`, {
177
- fields: parsed.fields,
178
- duration: String(parsed.duration ?? 20),
179
- max_events: String(parsed.max_events ?? 40),
180
- }), { method: "GET", headers: { Accept: "text/event-stream" } });
181
- },
182
- });
183
- export const logsTailCapability = defineCapability({
184
- id: "logs.tail",
185
- domain: "observability",
186
- roles: ["user", "admin"],
187
- description: "Live kernel or serial log follow on the same Observability subscribe feed LogViewer uses. Users only on a reserved machine. Slack and MCP attach for duration (default 20s). Not logs.latest.",
188
- mutation: "read",
189
- http: { method: "GET", path: "/v1/observability/nodes/{id}/logs/{stream}/subscribe" },
190
- slack: {
191
- parameters: {
192
- type: "object",
193
- properties: {
194
- node_id: { type: "string", description: "Node UUID from nodes.list." },
195
- stream: { type: "string", enum: ["serial", "kernel"] },
196
- duration: { type: "number", description: "Seconds to attach. Default 20, max 120." },
197
- max_events: { type: "number" },
198
- },
199
- required: ["node_id"],
200
- },
201
- },
202
- input: logsTailInput,
203
- execute: async (ctx, input) => {
204
- const parsed = logsTailInput.parse(input);
205
- const id = nodeId(parsed);
206
- return ctx.fetch(queryPath(`${nodePrefix(ctx.role, id)}/logs/${parsed.stream}/subscribe`, {
207
- duration: String(parsed.duration ?? 20),
208
- max_events: String(parsed.max_events ?? 40),
209
- }), { method: "GET", headers: { Accept: "text/event-stream" } });
210
- },
211
- });
212
- export const observabilityCapabilities = [
213
- telemetrySnapshotCapability,
214
- telemetryLatestCapability,
215
- telemetryTailCapability,
216
- logsLatestCapability,
217
- logsTailCapability,
218
- ];
@@ -1,53 +0,0 @@
1
- import { z } from "zod";
2
- import { defineCapability } from "../capability.js";
3
- export const organizationInviteInput = z.object({
4
- organization_id: z.uuid(),
5
- email: z.string().trim().toLowerCase().pipe(z.email()),
6
- });
7
- export const organizationInviteCapability = defineCapability({
8
- id: "organization.invite",
9
- domain: "users",
10
- roles: ["admin"],
11
- description: "Email an invitation to an existing organization. Resolve its UUID with organization_list first; ask the operator to choose if matches are ambiguous. The recipient may already belong to another organization. Send directly when the admin requests the invitation.",
12
- mutation: "immediate",
13
- http: { method: "POST", path: "/v1/staff/invites" },
14
- cli: {
15
- command: ["admin", "organizations", "invite"],
16
- vnext: {
17
- audience: "admin",
18
- category: "resource",
19
- command: ["admin", "organizations", "invite"],
20
- outcome: "Email an invitation to an existing organization.",
21
- human: "ornn admin organizations invite --organization UUID --email EMAIL",
22
- automation: "ornn admin organizations invite --organization UUID --email EMAIL --format json",
23
- acceptedReferences: ["organization UUID from admin organizations list"],
24
- related: ["admin organizations list"],
25
- },
26
- },
27
- slack: {
28
- parameters: {
29
- type: "object",
30
- properties: {
31
- organization_id: {
32
- type: "string",
33
- format: "uuid",
34
- description: "Exact organization UUID from organization_list.",
35
- },
36
- email: {
37
- type: "string",
38
- format: "email",
39
- description: "Address to receive the invitation.",
40
- },
41
- },
42
- required: ["organization_id", "email"],
43
- },
44
- },
45
- input: organizationInviteInput,
46
- execute: async (ctx, input) => {
47
- const parsed = organizationInviteInput.parse(input);
48
- return ctx.fetch("/v1/staff/invites", {
49
- method: "POST",
50
- body: { ...parsed, send_email: true },
51
- });
52
- },
53
- });
@@ -1,68 +0,0 @@
1
- import { z } from "zod";
2
- import { defineCapability } from "../capability.js";
3
- import { queryString } from "./staff-shared.js";
4
- export const reservationListInput = z.object({
5
- customerId: z.string().uuid().optional(),
6
- listingId: z.string().uuid().optional(),
7
- listingTermId: z.string().uuid().optional(),
8
- status: z.enum(["confirmed", "active", "completed", "cancelled"]).optional(),
9
- cursor: z.string().optional(),
10
- limit: z.number().int().min(1).max(100).optional(),
11
- });
12
- // Staff on every surface: Slack and the CLI resolve a customer's reservations
13
- // with this, then nodes.list reservation_id for that reservation's machines.
14
- export const reservationListCapability = defineCapability({
15
- id: "reservation.list",
16
- domain: "reservations",
17
- roles: ["admin"],
18
- description: "Internal staff only: list Commerce reservations across organizations, optionally filtered by customer organization (customerId), listing, listing term, and derived status. Resolve a customer's organization with users.list or organization.list first; pass each reservationId to nodes.list reservation_id for its machines. Page with nextCursor as cursor.",
19
- mutation: "read",
20
- http: { method: "GET", path: "/commerce/reservations" },
21
- cli: {
22
- command: ["admin", "reservations", "list"],
23
- vnext: {
24
- audience: "admin",
25
- category: "resource",
26
- command: ["admin", "reservations", "list"],
27
- outcome: "List reservations across organizations.",
28
- human: "ornn admin reservations list [--customer UUID] [--listing UUID] [--listing-term UUID] [--status confirmed|active|completed|cancelled] [--limit 50] [--cursor CURSOR]",
29
- automation: "ornn admin reservations list --customer UUID --limit 50 [--cursor CURSOR] --format json",
30
- acceptedReferences: [],
31
- },
32
- },
33
- slack: {
34
- parameters: {
35
- type: "object",
36
- properties: {
37
- customerId: {
38
- type: "string",
39
- description: "Optional customer organization UUID from users_list or organization_list.",
40
- },
41
- listingId: { type: "string", description: "Optional listing UUID." },
42
- listingTermId: { type: "string", description: "Optional listing term UUID." },
43
- status: {
44
- type: "string",
45
- enum: ["confirmed", "active", "completed", "cancelled"],
46
- description: "Optional derived reservation status.",
47
- },
48
- cursor: {
49
- type: "string",
50
- description: "Optional. Pass the previous response's nextCursor to fetch the next page.",
51
- },
52
- limit: { type: "integer", description: "Optional page size (Slack returns at most 10)." },
53
- },
54
- },
55
- },
56
- input: reservationListInput,
57
- execute: async (ctx, input) => {
58
- const parsed = reservationListInput.parse(input ?? {});
59
- return ctx.fetch(`/commerce/reservations${queryString({
60
- customerId: parsed.customerId,
61
- listingId: parsed.listingId,
62
- status: parsed.status,
63
- listingTermId: parsed.listingTermId,
64
- cursor: parsed.cursor,
65
- limit: parsed.limit,
66
- })}`, { method: "GET" });
67
- },
68
- });
@@ -1,64 +0,0 @@
1
- /** Helpers shared by staff capabilities. */
2
- /**
3
- * Build a request body from the fields that were actually supplied.
4
- *
5
- * Staff writes are almost all "PATCH the fields the caller named": `undefined`
6
- * means leave alone, and `null` is a real value that clears the field. So the
7
- * check is `!== undefined`, never truthiness.
8
- */
9
- export function mapBody(input, mapping) {
10
- const body = {};
11
- for (const [field, wire] of Object.entries(mapping)) {
12
- if (input[field] !== undefined)
13
- body[wire] = input[field];
14
- }
15
- return body;
16
- }
17
- /** Path segment escape. */
18
- export function seg(value) {
19
- return encodeURIComponent(value);
20
- }
21
- /**
22
- * A transport failure's HTTP status, when it carries one.
23
- *
24
- * Capabilities cannot import a surface's error class (`apps/mcp`'s
25
- * `WebApiError`, say), so read the status structurally.
26
- */
27
- export function statusOf(error) {
28
- if (error && typeof error === "object" && "status" in error) {
29
- const status = error.status;
30
- if (typeof status === "number")
31
- return status;
32
- }
33
- return undefined;
34
- }
35
- /**
36
- * Read a resource's current optimistic-concurrency version.
37
- *
38
- * Commerce rejects a PATCH or lifecycle action whose `version` is absent or
39
- * non-positive ("missing version" / "version must be positive"), but no staff
40
- * caller has that number to hand. Read it back when it was not supplied, and
41
- * only once confirmed so a preview stays a pure dry run.
42
- */
43
- export async function resolveVersion(ctx, path, provided) {
44
- if (provided && provided > 0)
45
- return provided;
46
- if (!ctx.confirmed)
47
- return 1;
48
- try {
49
- const current = (await ctx.fetch(path, { method: "GET" }));
50
- if (typeof current?.version === "number" && current.version > 0)
51
- return current.version;
52
- }
53
- catch {
54
- // Fall through: send 1 and let commerce report the conflict, which names
55
- // the expected version, rather than failing on a lookup the caller can't see.
56
- }
57
- return 1;
58
- }
59
- export function queryString(params) {
60
- const parts = Object.entries(params)
61
- .filter((entry) => entry[1] !== undefined && entry[1] !== "")
62
- .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
63
- return parts.length > 0 ? `?${parts.join("&")}` : "";
64
- }
@@ -1,165 +0,0 @@
1
- import { z } from "zod";
2
- import { defineCapability, preview } from "../capability.js";
3
- const optionalText = z.string().nullish();
4
- const tokenCreateInput = z.object({
5
- organization_id: z.string().min(1),
6
- organizationId: optionalText,
7
- facility_id: optionalText,
8
- facilityId: optionalText,
9
- expires_in_seconds: z.number().int().positive().nullish(),
10
- expiresInSeconds: z.number().int().positive().nullish(),
11
- filter: z.string().nullish(),
12
- ip: optionalText,
13
- installer_base: optionalText,
14
- host: optionalText,
15
- hosts: z.array(z.string()).nullish(),
16
- username: optionalText,
17
- port: z.number().int().min(1).max(65535).nullish(),
18
- private_key: optionalText,
19
- privateKey: optionalText,
20
- yes: z.boolean().optional(),
21
- });
22
- const emptyInput = z.object({});
23
- export function buildEnrollmentInstallCommand(options) {
24
- const base = (options.base ?? "https://api.ornn.com/v1/orchestrator").replace(/\/+$/, "");
25
- const params = [`token=${encodeURIComponent(options.token)}`];
26
- if (options.facilityId)
27
- params.push(`facility_id=${encodeURIComponent(options.facilityId)}`);
28
- if (options.filter)
29
- params.push(`filter=${encodeURIComponent(options.filter)}`);
30
- if (options.ip)
31
- params.push(`ip=${encodeURIComponent(options.ip)}`);
32
- if (options.restoreNodeId)
33
- params.push(`restore_node_id=${encodeURIComponent(options.restoreNodeId)}`);
34
- if (options.force)
35
- params.push("force=true");
36
- const href = `${base}/enrollment/install?${params.join("&")}`;
37
- // Without pipefail, a failed curl (bad installer_base, network error, non-2xx
38
- // response) pipes empty stdin into `sudo bash`, which then exits 0 having run
39
- // nothing -- the caller sees exit_code: 0 for an install that never happened.
40
- return `set -o pipefail; curl -fsSL '${href.replace(/'/g, "'\\''")}' | sudo bash`;
41
- }
42
- function tokenString(row, keys) {
43
- for (const key of keys) {
44
- const value = row[key];
45
- if (typeof value === "string" && value.trim())
46
- return value.trim();
47
- }
48
- return undefined;
49
- }
50
- export const tokensListCapability = defineCapability({
51
- id: "tokens.list",
52
- domain: "fleet",
53
- roles: ["admin"],
54
- description: "List issued enrollment tokens (id, supplier organization_id, status, expiry). Does not return plaintext secrets.",
55
- mutation: "read",
56
- http: { method: "GET", path: "/enrollment-tokens" },
57
- slack: { parameters: { type: "object", properties: {} } },
58
- input: emptyInput,
59
- execute: async (ctx) => ctx.fetch("/enrollment-tokens", { method: "GET" }),
60
- });
61
- export const tokensCreateCapability = defineCapability({
62
- id: "tokens.create",
63
- domain: "fleet",
64
- roles: ["admin"],
65
- description: "Admin-only. Mint an enrollment token. Without a host this returns the curl installer. With host + private_key, orchestrator SSHs and runs that curl (same as Bulk Enroll). Never invent a command. CLI asks y/N. Slack waits for a human confirm. MCP requires confirm: true.",
66
- mutation: "preview-confirm",
67
- http: { method: "POST", path: "/enrollment-tokens" },
68
- slack: {
69
- parameters: {
70
- type: "object",
71
- properties: {
72
- organization_id: {
73
- type: "string",
74
- description: "Supplier organization UUID from supplier.list.",
75
- },
76
- facility_id: { type: "string", description: "Optional facility UUID." },
77
- expires_in_seconds: { type: "integer", description: "Token TTL in seconds." },
78
- filter: {
79
- type: "string",
80
- description: "Optional comma-separated enrollment compartment ids. Omit to run every step.",
81
- },
82
- ip: { type: "string", description: "Optional IP stamped into the installer URL." },
83
- host: { type: "string", description: "SSH host/IP to enroll now." },
84
- hosts: {
85
- type: "array",
86
- items: { type: "string" },
87
- description: "Optional extra SSH hosts to enroll with the same key.",
88
- },
89
- username: { type: "string", description: "SSH user. Default admin." },
90
- port: { type: "integer", description: "SSH port. Default 22." },
91
- private_key: {
92
- type: "string",
93
- description: "PEM private key. Required to enroll over SSH.",
94
- },
95
- },
96
- required: ["organization_id"],
97
- },
98
- },
99
- input: tokenCreateInput,
100
- execute: async (ctx, input) => {
101
- const parsed = tokenCreateInput.parse(input);
102
- const organizationId = (parsed.organization_id || parsed.organizationId || "").trim();
103
- const hosts = [
104
- ...(parsed.host?.trim() ? [parsed.host.trim()] : []),
105
- ...(parsed.hosts ?? []).map((host) => host.trim()).filter(Boolean),
106
- ];
107
- const privateKey = parsed.private_key?.trim() || parsed.privateKey?.trim() || "";
108
- const payload = {
109
- organization_id: organizationId,
110
- facility_id: parsed.facility_id ?? parsed.facilityId,
111
- expires_in_seconds: parsed.expires_in_seconds ?? parsed.expiresInSeconds,
112
- filter: parsed.filter,
113
- ip: parsed.ip,
114
- hosts,
115
- username: parsed.username?.trim() || "admin",
116
- port: parsed.port ?? 22,
117
- remote: hosts.length > 0 && privateKey.length > 0,
118
- };
119
- if (!ctx.confirmed) {
120
- return preview("tokens.create", { ...payload, private_key: privateKey ? "[redacted]" : "" });
121
- }
122
- const minted = (await ctx.fetch("/enrollment-tokens", {
123
- method: "POST",
124
- body: {
125
- organization_id: organizationId,
126
- ...(payload.facility_id ? { facility_id: payload.facility_id } : {}),
127
- ...(payload.expires_in_seconds ? { expires_in_seconds: payload.expires_in_seconds } : {}),
128
- },
129
- }));
130
- const token = tokenString(minted, ["token", "plaintext"]) ?? "";
131
- const installCommand = token
132
- ? buildEnrollmentInstallCommand({
133
- token,
134
- base: parsed.installer_base ?? undefined,
135
- ip: parsed.ip ?? undefined,
136
- filter: parsed.filter ?? undefined,
137
- force: payload.remote,
138
- })
139
- : null;
140
- if (!payload.remote || !installCommand) {
141
- return { ...minted, install_command: installCommand };
142
- }
143
- const results = [];
144
- for (const host of hosts) {
145
- results.push(await ctx.fetch("/remote", {
146
- method: "POST",
147
- body: {
148
- host,
149
- username: payload.username,
150
- port: payload.port,
151
- private_key: privateKey,
152
- command: installCommand,
153
- timeout_seconds: 600,
154
- },
155
- }));
156
- }
157
- return {
158
- id: minted.id,
159
- organization_id: minted.organization_id ?? organizationId,
160
- expires_at: minted.expires_at,
161
- hosts: results,
162
- };
163
- },
164
- });
165
- export const tokenCapabilities = [tokensListCapability, tokensCreateCapability];