@ornncompute/cli 0.2.24 → 1.0.25

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 (85) hide show
  1. package/README.md +298 -426
  2. package/package.json +12 -6
  3. package/src/admin-capacity-reservations.mjs +280 -0
  4. package/src/admin-command-handlers.mjs +57 -0
  5. package/src/admin-enrollment.mjs +680 -0
  6. package/src/admin-mutations.mjs +187 -0
  7. package/src/admin-node-diagnostics.mjs +412 -0
  8. package/src/admin-node-lifecycle.mjs +908 -0
  9. package/src/admin-shell.mjs +322 -0
  10. package/src/api-client.mjs +81 -28
  11. package/src/atomic-output.mjs +112 -0
  12. package/src/auth-store.mjs +182 -11
  13. package/src/capability-catalog.mjs +7 -0
  14. package/src/catalog-shell.mjs +1350 -0
  15. package/src/cli.mjs +123 -10266
  16. package/src/connect-mode-handlers.mjs +170 -0
  17. package/src/device-auth.mjs +178 -61
  18. package/src/evidence-commands.mjs +1340 -0
  19. package/src/external-bucket-commands.mjs +1374 -0
  20. package/src/interaction-runtime.mjs +310 -0
  21. package/src/kubernetes-commands.mjs +2229 -0
  22. package/src/launch-mode-handlers.mjs +119 -0
  23. package/src/machine-reference.mjs +277 -0
  24. package/src/mount-commands.mjs +1146 -0
  25. package/src/session-commands.mjs +565 -0
  26. package/src/slurm-commands.mjs +263 -0
  27. package/src/startup.mjs +34 -11
  28. package/src/storage-commands.mjs +1144 -0
  29. package/src/update.mjs +172 -24
  30. package/src/vm-commands.mjs +4273 -0
  31. package/src/vpn-secret-output.mjs +150 -0
  32. package/src/wait-runtime.mjs +231 -0
  33. package/vendor/capabilities/capabilities/admin-capacity-reservations.js +126 -0
  34. package/vendor/capabilities/capabilities/admin-enrollment.js +321 -0
  35. package/vendor/capabilities/capabilities/admin-node-diagnostics.js +88 -0
  36. package/vendor/capabilities/capabilities/admin-nodes-lifecycle.js +235 -0
  37. package/vendor/capabilities/capabilities/admin.js +89 -0
  38. package/vendor/capabilities/capabilities/capacity-offers.js +368 -0
  39. package/vendor/capabilities/capabilities/cli.js +156 -0
  40. package/vendor/capabilities/capabilities/clusters.js +150 -1
  41. package/vendor/capabilities/capabilities/compute.js +522 -0
  42. package/vendor/capabilities/capabilities/evidence.js +145 -0
  43. package/vendor/capabilities/capabilities/external-buckets.js +191 -0
  44. package/vendor/capabilities/capabilities/fleet.js +21 -5
  45. package/vendor/capabilities/capabilities/identity.js +114 -3
  46. package/vendor/capabilities/capabilities/listings.js +14 -8
  47. package/vendor/capabilities/capabilities/mounts.js +176 -0
  48. package/vendor/capabilities/capabilities/networks.js +1257 -0
  49. package/vendor/capabilities/capabilities/observability.js +38 -8
  50. package/vendor/capabilities/capabilities/tokens.js +20 -8
  51. package/vendor/capabilities/capabilities/volumes.js +178 -0
  52. package/vendor/capabilities/capability.js +19 -13
  53. package/vendor/capabilities/cli-catalog.js +65 -0
  54. package/vendor/capabilities/cli-migrations.js +288 -0
  55. package/vendor/capabilities/cli.js +7 -0
  56. package/vendor/capabilities/software-catalog.js +36 -0
  57. package/vendor/capabilities/spec-catalog.js +2 -2
  58. package/src/catalog-dispatch.mjs +0 -605
  59. package/src/color.mjs +0 -71
  60. package/vendor/capabilities/capabilities/clusters.d.ts +0 -2
  61. package/vendor/capabilities/capabilities/fleet.d.ts +0 -8
  62. package/vendor/capabilities/capabilities/identity.d.ts +0 -3
  63. package/vendor/capabilities/capabilities/listings.d.ts +0 -51
  64. package/vendor/capabilities/capabilities/observability.d.ts +0 -5
  65. package/vendor/capabilities/capabilities/staff-access.d.ts +0 -29
  66. package/vendor/capabilities/capabilities/staff-access.js +0 -278
  67. package/vendor/capabilities/capabilities/staff-commerce.d.ts +0 -160
  68. package/vendor/capabilities/capabilities/staff-commerce.js +0 -689
  69. package/vendor/capabilities/capabilities/staff-fleet.d.ts +0 -140
  70. package/vendor/capabilities/capabilities/staff-fleet.js +0 -538
  71. package/vendor/capabilities/capabilities/staff-gtm.d.ts +0 -142
  72. package/vendor/capabilities/capabilities/staff-gtm.js +0 -521
  73. package/vendor/capabilities/capabilities/staff-shared.d.ts +0 -47
  74. package/vendor/capabilities/capabilities/staff-shared.js +0 -98
  75. package/vendor/capabilities/capabilities/tokens.d.ts +0 -10
  76. package/vendor/capabilities/capabilities/users.d.ts +0 -2
  77. package/vendor/capabilities/capability.d.ts +0 -93
  78. package/vendor/capabilities/catalog.d.ts +0 -3
  79. package/vendor/capabilities/catalog.js +0 -26
  80. package/vendor/capabilities/index.d.ts +0 -16
  81. package/vendor/capabilities/index.js +0 -16
  82. package/vendor/capabilities/names.d.ts +0 -6
  83. package/vendor/capabilities/role.d.ts +0 -6
  84. package/vendor/capabilities/role.js +0 -17
  85. package/vendor/capabilities/spec-catalog.d.ts +0 -78
@@ -21,6 +21,7 @@ const latestInput = z.object({
21
21
  end: z.string().optional(),
22
22
  span: z.string().optional(),
23
23
  max_points: z.number().int().min(1).max(500).optional(),
24
+ metrics: z.string().optional(),
24
25
  });
25
26
  const logsLatestInput = z.object({
26
27
  node_id: z.string().min(1),
@@ -28,6 +29,7 @@ const logsLatestInput = z.object({
28
29
  stream: z.enum(["serial", "kernel"]).default("kernel"),
29
30
  count: z.number().int().min(1).max(1000).optional(),
30
31
  before: z.string().optional(),
32
+ start: z.string().optional(),
31
33
  cursor: z.string().optional(),
32
34
  });
33
35
  const tailInput = z.object({
@@ -57,8 +59,8 @@ function expandSpan(span, start, end) {
57
59
  export const telemetryLatestCapability = defineCapability({
58
60
  id: "telemetry.latest",
59
61
  domain: "observability",
60
- roles: ["user", "reviewer", "admin"],
61
- description: "One-shot telemetry window for one node (start/end or span plus max_points). Users may only query a reserved machine. Reviewers and admins may query any enrolled node. Pass a node UUID. Use this for heartbeat / last-seen / public_ip. Not a live stream.",
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.",
62
64
  mutation: "read",
63
65
  http: { method: "GET", path: "/v1/observability/nodes/{id}/telemetry/query" },
64
66
  slack: {
@@ -70,6 +72,7 @@ export const telemetryLatestCapability = defineCapability({
70
72
  start: { type: "string" },
71
73
  end: { type: "string" },
72
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." },
73
76
  },
74
77
  required: ["node_id"],
75
78
  },
@@ -83,14 +86,15 @@ export const telemetryLatestCapability = defineCapability({
83
86
  start: range.start,
84
87
  end: range.end,
85
88
  max_points: String(parsed.max_points ?? 240),
89
+ metrics: parsed.metrics,
86
90
  }), { method: "GET" });
87
91
  },
88
92
  });
89
93
  export const logsLatestCapability = defineCapability({
90
94
  id: "logs.latest",
91
95
  domain: "observability",
92
- roles: ["user", "reviewer", "admin"],
93
- description: "One-shot newest kernel or serial log page (before + count + cursor). Users only on a reserved machine. Not a live tail. First call omits cursor; older pages pass next_cursor.",
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.",
94
98
  mutation: "read",
95
99
  http: { method: "GET", path: "/v1/observability/nodes/{id}/logs/{stream}/query" },
96
100
  slack: {
@@ -100,7 +104,8 @@ export const logsLatestCapability = defineCapability({
100
104
  node_id: { type: "string", description: "Node UUID from nodes.list." },
101
105
  stream: { type: "string", enum: ["serial", "kernel"] },
102
106
  count: { type: "number", description: "Page size. Default 200, max 1000." },
103
- before: { type: "string", description: "RFC3339 upper bound. Default now." },
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." },
104
109
  cursor: { type: "string", description: "next_cursor from the previous page." },
105
110
  },
106
111
  required: ["node_id"],
@@ -111,7 +116,7 @@ export const logsLatestCapability = defineCapability({
111
116
  const parsed = logsLatestInput.parse(input);
112
117
  const id = nodeId(parsed);
113
118
  const end = parsed.before?.trim() || new Date().toISOString();
114
- const start = new Date(Date.parse(end) - 7 * 86_400_000).toISOString();
119
+ const start = parsed.start?.trim() || new Date(Date.parse(end) - 7 * 86_400_000).toISOString();
115
120
  return ctx.fetch(queryPath(`${nodePrefix(ctx.role, id)}/logs/${parsed.stream}/query`, {
116
121
  start,
117
122
  end,
@@ -121,10 +126,34 @@ export const logsLatestCapability = defineCapability({
121
126
  }), { method: "GET" });
122
127
  },
123
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
+ });
124
153
  export const telemetryTailCapability = defineCapability({
125
154
  id: "telemetry.tail",
126
155
  domain: "observability",
127
- roles: ["user", "reviewer", "admin"],
156
+ roles: ["user", "admin"],
128
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.",
129
158
  mutation: "read",
130
159
  http: { method: "GET", path: "/v1/observability/nodes/{id}/telemetry/subscribe" },
@@ -154,7 +183,7 @@ export const telemetryTailCapability = defineCapability({
154
183
  export const logsTailCapability = defineCapability({
155
184
  id: "logs.tail",
156
185
  domain: "observability",
157
- roles: ["user", "reviewer", "admin"],
186
+ roles: ["user", "admin"],
158
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.",
159
188
  mutation: "read",
160
189
  http: { method: "GET", path: "/v1/observability/nodes/{id}/logs/{stream}/subscribe" },
@@ -181,6 +210,7 @@ export const logsTailCapability = defineCapability({
181
210
  },
182
211
  });
183
212
  export const observabilityCapabilities = [
213
+ telemetrySnapshotCapability,
184
214
  telemetryLatestCapability,
185
215
  telemetryTailCapability,
186
216
  logsLatestCapability,
@@ -8,8 +8,7 @@ const tokenCreateInput = z.object({
8
8
  facilityId: optionalText,
9
9
  expires_in_seconds: z.number().int().positive().nullish(),
10
10
  expiresInSeconds: z.number().int().positive().nullish(),
11
- node_mode: z.enum(["bare-metal", "virtual-machine", "vm"]).nullish(),
12
- nodeMode: z.enum(["bare-metal", "virtual-machine", "vm"]).nullish(),
11
+ filter: z.string().nullish(),
13
12
  ip: optionalText,
14
13
  installer_base: optionalText,
15
14
  host: optionalText,
@@ -24,6 +23,10 @@ const emptyInput = z.object({});
24
23
  export function buildEnrollmentInstallCommand(options) {
25
24
  const base = (options.base ?? "https://api.ornn.com/v1/orchestrator").replace(/\/+$/, "");
26
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)}`);
27
30
  if (options.ip)
28
31
  params.push(`ip=${encodeURIComponent(options.ip)}`);
29
32
  if (options.restoreNodeId)
@@ -31,7 +34,10 @@ export function buildEnrollmentInstallCommand(options) {
31
34
  if (options.force)
32
35
  params.push("force=true");
33
36
  const href = `${base}/enrollment/install?${params.join("&")}`;
34
- return `curl -fsSL '${href.replace(/'/g, "'\\''")}' | sudo bash`;
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`;
35
41
  }
36
42
  function tokenString(row, keys) {
37
43
  for (const key of keys) {
@@ -44,7 +50,7 @@ function tokenString(row, keys) {
44
50
  export const tokensListCapability = defineCapability({
45
51
  id: "tokens.list",
46
52
  domain: "fleet",
47
- roles: ["reviewer", "admin"],
53
+ roles: ["admin"],
48
54
  description: "List issued enrollment tokens (id, operator, status, expiry). Does not return plaintext secrets.",
49
55
  mutation: "read",
50
56
  http: { method: "GET", path: "/enrollment-tokens" },
@@ -66,7 +72,10 @@ export const tokensCreateCapability = defineCapability({
66
72
  operator_id: { type: "string", description: "Commerce operator UUID from operator.list." },
67
73
  facility_id: { type: "string", description: "Optional facility UUID." },
68
74
  expires_in_seconds: { type: "integer", description: "Token TTL in seconds." },
69
- node_mode: { type: "string", enum: ["bare-metal", "virtual-machine"] },
75
+ filter: {
76
+ type: "string",
77
+ description: "Optional comma-separated enrollment compartment ids. Omit to run every step.",
78
+ },
70
79
  ip: { type: "string", description: "Optional IP stamped into the installer URL." },
71
80
  host: { type: "string", description: "SSH host/IP to enroll now." },
72
81
  hosts: {
@@ -76,7 +85,10 @@ export const tokensCreateCapability = defineCapability({
76
85
  },
77
86
  username: { type: "string", description: "SSH user. Default admin." },
78
87
  port: { type: "integer", description: "SSH port. Default 22." },
79
- private_key: { type: "string", description: "PEM private key. Required to enroll over SSH." },
88
+ private_key: {
89
+ type: "string",
90
+ description: "PEM private key. Required to enroll over SSH.",
91
+ },
80
92
  },
81
93
  required: ["operator_id"],
82
94
  },
@@ -90,12 +102,11 @@ export const tokensCreateCapability = defineCapability({
90
102
  ...(parsed.hosts ?? []).map((host) => host.trim()).filter(Boolean),
91
103
  ];
92
104
  const privateKey = parsed.private_key?.trim() || parsed.privateKey?.trim() || "";
93
- const nodeMode = parsed.node_mode ?? parsed.nodeMode;
94
105
  const payload = {
95
106
  operator_id: operatorId,
96
107
  facility_id: parsed.facility_id ?? parsed.facilityId,
97
108
  expires_in_seconds: parsed.expires_in_seconds ?? parsed.expiresInSeconds,
98
- node_mode: nodeMode,
109
+ filter: parsed.filter,
99
110
  ip: parsed.ip,
100
111
  hosts,
101
112
  username: parsed.username?.trim() || "admin",
@@ -119,6 +130,7 @@ export const tokensCreateCapability = defineCapability({
119
130
  token,
120
131
  base: parsed.installer_base ?? undefined,
121
132
  ip: parsed.ip ?? undefined,
133
+ filter: parsed.filter ?? undefined,
122
134
  force: payload.remote,
123
135
  })
124
136
  : null;
@@ -0,0 +1,178 @@
1
+ import { z } from "zod";
2
+ import { defineCapability, } from "../capability.js";
3
+ const storageCliInput = z
4
+ .object({
5
+ $positionals: z.array(z.string()).optional(),
6
+ contentType: z.string().optional(),
7
+ cursor: z.string().optional(),
8
+ idempotencyKey: z.string().optional(),
9
+ limit: z.union([z.string(), z.number()]).optional(),
10
+ name: z.string().optional(),
11
+ output: z.string().optional(),
12
+ overwrite: z.boolean().optional(),
13
+ path: z.string().optional(),
14
+ prefix: z.string().optional(),
15
+ source: z.string().optional(),
16
+ yes: z.boolean().optional(),
17
+ })
18
+ .strict();
19
+ function storageExecutor(context) {
20
+ const interaction = context.cli?.interaction;
21
+ if (!interaction?.storage) {
22
+ throw new Error("This capability must execute through the Ornn CLI storage runtime.");
23
+ }
24
+ return interaction.storage.bind(interaction);
25
+ }
26
+ function storageCapability({ id, command, description, mutation, outcome, human, automation, acceptedReferences = [], review, related = [], }) {
27
+ return defineCapability({
28
+ id,
29
+ domain: "storage",
30
+ roles: ["user"],
31
+ description,
32
+ mutation,
33
+ input: storageCliInput,
34
+ cli: {
35
+ command,
36
+ vnext: {
37
+ arguments: "semantic",
38
+ audience: "customer",
39
+ category: "resource",
40
+ outcome,
41
+ human,
42
+ automation,
43
+ acceptedReferences,
44
+ ...(review ? { review } : {}),
45
+ related,
46
+ },
47
+ },
48
+ mcp: false,
49
+ slack: false,
50
+ execute: async (context, input) => storageExecutor(context)(id, input),
51
+ });
52
+ }
53
+ export const volumeCapabilities = [
54
+ storageCapability({
55
+ id: "storage.volumes.list",
56
+ command: ["volumes", "list"],
57
+ description: "List managed volumes in one bounded page.",
58
+ mutation: "read",
59
+ outcome: "List durable managed volumes independently from their mounts.",
60
+ human: "ornn volumes list [--limit <1-100>] [--cursor <cursor>]",
61
+ automation: "ornn volumes list --limit 50 --format json",
62
+ related: ["volumes show", "volumes create"],
63
+ }),
64
+ storageCapability({
65
+ id: "storage.volumes.show",
66
+ command: ["volumes", "show"],
67
+ description: "Inspect one managed volume by canonical ID or exact unique name.",
68
+ mutation: "read",
69
+ outcome: "Inspect one durable volume without changing its lifecycle.",
70
+ human: "ornn volumes show <volume>",
71
+ automation: "ornn volumes show <volume-id> --format json",
72
+ acceptedReferences: ["canonical volume ID", "exact unique volume name"],
73
+ related: ["volumes files list", "volume-mounts show"],
74
+ }),
75
+ storageCapability({
76
+ id: "storage.volumes.create",
77
+ command: ["volumes", "create"],
78
+ description: "Create or rediscover a uniquely named managed volume.",
79
+ mutation: "preview-confirm",
80
+ outcome: "Create one durable volume or safely replay its normalized intent.",
81
+ human: "ornn volumes create --name <name> [--source <volume>] [--yes]",
82
+ automation: "ornn volumes create --name <name> [--source <volume-id>] --yes --idempotency-key <key> --format json",
83
+ acceptedReferences: ["canonical source volume ID", "exact unique source volume name"],
84
+ related: ["volumes show", "volumes files upload"],
85
+ }),
86
+ storageCapability({
87
+ id: "storage.volumes.refresh",
88
+ command: ["volumes", "refresh"],
89
+ description: "Refresh the owner-backed view of one managed volume.",
90
+ mutation: "read",
91
+ outcome: "Refresh and inspect the current durable volume state.",
92
+ human: "ornn volumes refresh <volume>",
93
+ automation: "ornn volumes refresh <volume-id> --format json",
94
+ acceptedReferences: ["canonical volume ID", "exact unique volume name"],
95
+ related: ["volumes show"],
96
+ }),
97
+ storageCapability({
98
+ id: "storage.volumes.clear",
99
+ command: ["volumes", "clear"],
100
+ description: "Erase every file in one volume after a live default-no confirmation.",
101
+ mutation: "preview-confirm",
102
+ outcome: "Permanently clear one volume only from a live interactive terminal.",
103
+ human: "ornn volumes clear <volume>",
104
+ automation: "Unsupported: this command requires a live interactive terminal.",
105
+ acceptedReferences: ["canonical volume ID", "exact unique volume name"],
106
+ review: "Every file and folder in the volume is permanently deleted.",
107
+ related: ["volumes files list", "volume-mounts detach"],
108
+ }),
109
+ storageCapability({
110
+ id: "storage.volumes.delete",
111
+ command: ["volumes", "delete"],
112
+ description: "Delete an explicitly selected, relationship-free managed volume.",
113
+ mutation: "preview-confirm",
114
+ outcome: "Delete one detached volume after explicit consent.",
115
+ human: "ornn volumes delete <volume>",
116
+ automation: "ornn volumes delete <volume-id> --yes --idempotency-key <key> --format json",
117
+ acceptedReferences: ["canonical volume ID", "exact unique volume name"],
118
+ review: "The volume and all of its data are permanently deleted.",
119
+ related: ["volume-mounts detach", "volumes show"],
120
+ }),
121
+ storageCapability({
122
+ id: "storage.volumes.files.list",
123
+ command: ["volumes", "files", "list"],
124
+ description: "List nested files in one volume in a bounded page.",
125
+ mutation: "read",
126
+ outcome: "List nested volume files by semantic remote path.",
127
+ human: "ornn volumes files list <volume> [--prefix <remote-path>] [--limit <1-500>] [--cursor <cursor>]",
128
+ automation: "ornn volumes files list <volume-id> --limit 50 --format json",
129
+ acceptedReferences: ["canonical volume ID", "exact unique volume name"],
130
+ related: ["volumes files upload", "volumes files download"],
131
+ }),
132
+ storageCapability({
133
+ id: "storage.volumes.files.upload",
134
+ command: ["volumes", "files", "upload"],
135
+ description: "Stage, upload, and finalize one local file at a nested volume path.",
136
+ mutation: "preview-confirm",
137
+ outcome: "Upload one file through a reconciled staged transfer.",
138
+ human: "ornn volumes files upload <volume> <local-file> [--path <remote-path>] [--content-type <type>] [--yes]",
139
+ automation: "ornn volumes files upload <volume-id> <local-file> --path <remote-path> --yes --idempotency-key <key> --format json",
140
+ acceptedReferences: ["canonical volume ID", "exact unique volume name"],
141
+ related: ["volumes files list", "volumes refresh"],
142
+ }),
143
+ storageCapability({
144
+ id: "storage.volumes.files.download",
145
+ command: ["volumes", "files", "download"],
146
+ description: "Download one nested volume file to an explicit local path atomically.",
147
+ mutation: "local",
148
+ outcome: "Download one file without partially replacing local data.",
149
+ human: "ornn volumes files download <volume> <remote-path> --output <local-path> [--overwrite]",
150
+ automation: "ornn volumes files download <volume-id> <remote-path> --output <local-path> --format json",
151
+ acceptedReferences: ["canonical volume ID", "exact unique volume name"],
152
+ related: ["volumes files list"],
153
+ }),
154
+ storageCapability({
155
+ id: "storage.volumes.files.delete",
156
+ command: ["volumes", "files", "delete"],
157
+ description: "Delete one nested file after a live default-no confirmation.",
158
+ mutation: "preview-confirm",
159
+ outcome: "Permanently delete one file only from a live interactive terminal.",
160
+ human: "ornn volumes files delete <volume> <remote-path>",
161
+ automation: "Unsupported: this command requires a live interactive terminal.",
162
+ acceptedReferences: ["canonical volume ID", "exact unique volume name"],
163
+ review: "The selected file is permanently deleted.",
164
+ related: ["volumes files list"],
165
+ }),
166
+ storageCapability({
167
+ id: "storage.volumes.folders.delete",
168
+ command: ["volumes", "folders", "delete"],
169
+ description: "Delete one nested folder after a live default-no confirmation.",
170
+ mutation: "preview-confirm",
171
+ outcome: "Permanently delete one folder tree only from a live interactive terminal.",
172
+ human: "ornn volumes folders delete <volume> <remote-path>",
173
+ automation: "Unsupported: this command requires a live interactive terminal.",
174
+ acceptedReferences: ["canonical volume ID", "exact unique volume name"],
175
+ review: "The selected folder and every nested file are permanently deleted.",
176
+ related: ["volumes files list"],
177
+ }),
178
+ ];
@@ -1,5 +1,6 @@
1
1
  import { namesFromId } from "./names.js";
2
2
  export const DOMAINS = [
3
+ "cli",
3
4
  "identity",
4
5
  "listings",
5
6
  "fleet",
@@ -29,19 +30,26 @@ export const DOMAINS = [
29
30
  export function preview(action, payload) {
30
31
  return { status: "preview", requiresConfirm: true, confirmed: false, action, payload };
31
32
  }
32
- /** Fill CLI/MCP/Slack names from `id`. Slack parameters stay optional. */
33
+ /** CLI is opt-in; MCP and Slack retain derived names unless explicitly unbound. */
33
34
  export function defineCapability(spec) {
34
35
  const names = namesFromId(spec.id);
35
- const { slack, ...rest } = spec;
36
+ const { cli, mcp, slack, ...rest } = spec;
36
37
  return {
37
38
  ...rest,
38
- cli: { command: names.cli },
39
- mcp: { name: names.mcp },
39
+ ...(cli !== false && cli !== undefined
40
+ ? {
41
+ cli: {
42
+ command: cli.command ?? names.cli,
43
+ ...(cli.vnext ? { vnext: cli.vnext } : {}),
44
+ },
45
+ }
46
+ : {}),
47
+ ...(mcp === false ? {} : { mcp: { name: mcp?.name ?? names.mcp } }),
40
48
  ...(slack === false
41
49
  ? {}
42
50
  : {
43
51
  slack: {
44
- name: names.slack,
52
+ name: slack?.name ?? names.slack,
45
53
  ...(slack?.parameters ? { parameters: slack.parameters } : {}),
46
54
  },
47
55
  }),
@@ -61,19 +69,17 @@ export function defineCapability(spec) {
61
69
  export function defineStaffMcpCapability(spec) {
62
70
  return { ...spec, mcp: { name: namesFromId(spec.id).mcp } };
63
71
  }
64
- export function slackStaffVisible(capability) {
65
- return capability.roles.some((role) => role === "reviewer" || role === "admin");
66
- }
67
72
  export function visibleToClient(capability, client, role) {
68
73
  if (!capability.roles.includes(role))
69
74
  return false;
70
- if (client === "slack") {
71
- if (role === "user")
75
+ if (client === "slack")
76
+ return capability.slack != null;
77
+ if (client === "cli") {
78
+ const help = capability.cli?.vnext;
79
+ if (!help || !capability.cli)
72
80
  return false;
73
- return slackStaffVisible(capability) && capability.slack != null;
81
+ return help.audience === "customer" ? role === "user" : role === "admin";
74
82
  }
75
- if (client === "cli")
76
- return capability.cli != null;
77
83
  return capability.mcp != null;
78
84
  }
79
85
  export function forRole(catalog, role, client) {
@@ -0,0 +1,65 @@
1
+ import { adminCapabilities, bindAdminCapabilities } from "./capabilities/admin.js";
2
+ import { capacityOfferCapabilities } from "./capabilities/capacity-offers.js";
3
+ import { adminCapacityReservationCapabilities } from "./capabilities/admin-capacity-reservations.js";
4
+ import { adminEnrollmentCapabilities, bindAdminEnrollmentCapabilities, } from "./capabilities/admin-enrollment.js";
5
+ import { adminNodeDiagnosticCapabilities } from "./capabilities/admin-node-diagnostics.js";
6
+ import { adminNodeLifecycleCapabilities } from "./capabilities/admin-nodes-lifecycle.js";
7
+ import { computeCapabilities } from "./capabilities/compute.js";
8
+ import { cliShellCapabilities } from "./capabilities/cli.js";
9
+ import { clusterCliCapabilities, kubernetesCapabilities, slurmCapabilities, } from "./capabilities/clusters.js";
10
+ import { externalBucketCapabilities } from "./capabilities/external-buckets.js";
11
+ import { evidenceCapabilities } from "./capabilities/evidence.js";
12
+ import { fleetCapabilities } from "./capabilities/fleet.js";
13
+ import { tokenCapabilities } from "./capabilities/tokens.js";
14
+ import { mountCapabilities } from "./capabilities/mounts.js";
15
+ import { identityCapabilities } from "./capabilities/identity.js";
16
+ import { listingCapabilities } from "./capabilities/listings.js";
17
+ import { observabilityCapabilities } from "./capabilities/observability.js";
18
+ import { networkCapabilities } from "./capabilities/networks.js";
19
+ import { volumeCapabilities } from "./capabilities/volumes.js";
20
+ import { userCapabilities } from "./capabilities/users.js";
21
+ function canonicalCliCatalog(capabilities) {
22
+ return capabilities.map((capability) => {
23
+ const help = capability.cli?.vnext;
24
+ if (!help) {
25
+ const { cli: _retiredCliBinding, ...withoutCli } = capability;
26
+ return withoutCli;
27
+ }
28
+ return {
29
+ ...capability,
30
+ cli: {
31
+ command: [...(help.command ?? capability.cli?.command ?? [])],
32
+ vnext: help,
33
+ },
34
+ };
35
+ });
36
+ }
37
+ /** Canonical operational catalog, without staff-only commerce or CRM modules. */
38
+ export const catalog = [
39
+ ...canonicalCliCatalog([
40
+ ...bindAdminEnrollmentCapabilities(bindAdminCapabilities([
41
+ ...cliShellCapabilities,
42
+ ...identityCapabilities,
43
+ ...listingCapabilities,
44
+ ...capacityOfferCapabilities,
45
+ ...computeCapabilities,
46
+ ...fleetCapabilities.filter((capability) => capability.id !== "node.exec"),
47
+ ...networkCapabilities,
48
+ ...clusterCliCapabilities,
49
+ ...tokenCapabilities,
50
+ ...userCapabilities,
51
+ ...volumeCapabilities,
52
+ ...mountCapabilities,
53
+ ...externalBucketCapabilities,
54
+ ...evidenceCapabilities,
55
+ ...observabilityCapabilities,
56
+ ...slurmCapabilities,
57
+ ...kubernetesCapabilities,
58
+ ...adminNodeLifecycleCapabilities,
59
+ ])),
60
+ ...adminCapabilities.filter((capability) => capability.id === "nodes.health"),
61
+ ...adminCapacityReservationCapabilities.filter((capability) => capability.id !== "listings.create"),
62
+ ...adminNodeDiagnosticCapabilities,
63
+ ...adminEnrollmentCapabilities,
64
+ ]),
65
+ ];