@ornncompute/cli 0.2.24 → 1.0.34

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 (90) hide show
  1. package/README.md +351 -426
  2. package/package.json +12 -6
  3. package/src/admin-capacity-reservations.mjs +305 -0
  4. package/src/admin-command-handlers.mjs +57 -0
  5. package/src/admin-enrollment.mjs +681 -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 +905 -0
  9. package/src/admin-shell.mjs +456 -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 +1363 -0
  15. package/src/cli.mjs +123 -10266
  16. package/src/connect-mode-handlers.mjs +170 -0
  17. package/src/device-auth.mjs +179 -62
  18. package/src/evidence-commands.mjs +1354 -0
  19. package/src/external-bucket-commands.mjs +1374 -0
  20. package/src/gpu-catalog.mjs +21 -0
  21. package/src/identity-file.mjs +28 -0
  22. package/src/interaction-runtime.mjs +310 -0
  23. package/src/kubernetes-commands.mjs +2235 -0
  24. package/src/launch-mode-handlers.mjs +119 -0
  25. package/src/machine-reference.mjs +285 -0
  26. package/src/mount-commands.mjs +1148 -0
  27. package/src/session-commands.mjs +565 -0
  28. package/src/slurm-commands.mjs +274 -0
  29. package/src/startup.mjs +34 -11
  30. package/src/storage-commands.mjs +1164 -0
  31. package/src/update.mjs +172 -24
  32. package/src/vm-commands.mjs +4308 -0
  33. package/src/vpn-secret-output.mjs +150 -0
  34. package/src/wait-runtime.mjs +231 -0
  35. package/vendor/capabilities/capabilities/admin-capacity-reservations.js +96 -0
  36. package/vendor/capabilities/capabilities/admin-enrollment.js +321 -0
  37. package/vendor/capabilities/capabilities/admin-node-diagnostics.js +88 -0
  38. package/vendor/capabilities/capabilities/admin-nodes-lifecycle.js +235 -0
  39. package/vendor/capabilities/capabilities/admin.js +89 -0
  40. package/vendor/capabilities/capabilities/cli.js +156 -0
  41. package/vendor/capabilities/capabilities/clusters.js +150 -1
  42. package/vendor/capabilities/capabilities/commerce-market.js +330 -0
  43. package/vendor/capabilities/capabilities/compute.js +522 -0
  44. package/vendor/capabilities/capabilities/evidence.js +145 -0
  45. package/vendor/capabilities/capabilities/external-buckets.js +191 -0
  46. package/vendor/capabilities/capabilities/fleet.js +77 -25
  47. package/vendor/capabilities/capabilities/identity.js +114 -3
  48. package/vendor/capabilities/capabilities/listings.js +141 -144
  49. package/vendor/capabilities/capabilities/market-shared.js +44 -0
  50. package/vendor/capabilities/capabilities/markets.js +449 -0
  51. package/vendor/capabilities/capabilities/mounts.js +176 -0
  52. package/vendor/capabilities/capabilities/networks.js +1264 -0
  53. package/vendor/capabilities/capabilities/observability.js +38 -8
  54. package/vendor/capabilities/capabilities/tokens.js +32 -17
  55. package/vendor/capabilities/capabilities/volumes.js +178 -0
  56. package/vendor/capabilities/capability.js +21 -14
  57. package/vendor/capabilities/cli-catalog.js +67 -0
  58. package/vendor/capabilities/cli-migrations.js +261 -0
  59. package/vendor/capabilities/cli.js +7 -0
  60. package/vendor/capabilities/software-catalog.js +36 -0
  61. package/vendor/capabilities/spec-catalog.js +28 -5
  62. package/vendor/capabilities/supplier-directory.js +20 -0
  63. package/src/catalog-dispatch.mjs +0 -605
  64. package/src/color.mjs +0 -71
  65. package/vendor/capabilities/capabilities/clusters.d.ts +0 -2
  66. package/vendor/capabilities/capabilities/fleet.d.ts +0 -8
  67. package/vendor/capabilities/capabilities/identity.d.ts +0 -3
  68. package/vendor/capabilities/capabilities/listings.d.ts +0 -51
  69. package/vendor/capabilities/capabilities/observability.d.ts +0 -5
  70. package/vendor/capabilities/capabilities/staff-access.d.ts +0 -29
  71. package/vendor/capabilities/capabilities/staff-access.js +0 -278
  72. package/vendor/capabilities/capabilities/staff-commerce.d.ts +0 -160
  73. package/vendor/capabilities/capabilities/staff-commerce.js +0 -689
  74. package/vendor/capabilities/capabilities/staff-fleet.d.ts +0 -140
  75. package/vendor/capabilities/capabilities/staff-fleet.js +0 -538
  76. package/vendor/capabilities/capabilities/staff-gtm.d.ts +0 -142
  77. package/vendor/capabilities/capabilities/staff-gtm.js +0 -521
  78. package/vendor/capabilities/capabilities/staff-shared.d.ts +0 -47
  79. package/vendor/capabilities/capabilities/staff-shared.js +0 -98
  80. package/vendor/capabilities/capabilities/tokens.d.ts +0 -10
  81. package/vendor/capabilities/capabilities/users.d.ts +0 -2
  82. package/vendor/capabilities/capability.d.ts +0 -93
  83. package/vendor/capabilities/catalog.d.ts +0 -3
  84. package/vendor/capabilities/catalog.js +0 -26
  85. package/vendor/capabilities/index.d.ts +0 -16
  86. package/vendor/capabilities/index.js +0 -16
  87. package/vendor/capabilities/names.d.ts +0 -6
  88. package/vendor/capabilities/role.d.ts +0 -6
  89. package/vendor/capabilities/role.js +0 -17
  90. 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,
@@ -2,14 +2,13 @@ import { z } from "zod";
2
2
  import { defineCapability, preview } from "../capability.js";
3
3
  const optionalText = z.string().nullish();
4
4
  const tokenCreateInput = z.object({
5
- operator_id: z.string().min(1),
6
- operatorId: optionalText,
5
+ organization_id: z.string().min(1),
6
+ organizationId: optionalText,
7
7
  facility_id: optionalText,
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,8 +50,8 @@ function tokenString(row, keys) {
44
50
  export const tokensListCapability = defineCapability({
45
51
  id: "tokens.list",
46
52
  domain: "fleet",
47
- roles: ["reviewer", "admin"],
48
- description: "List issued enrollment tokens (id, operator, status, expiry). Does not return plaintext secrets.",
53
+ roles: ["admin"],
54
+ description: "List issued enrollment tokens (id, supplier organization_id, status, expiry). Does not return plaintext secrets.",
49
55
  mutation: "read",
50
56
  http: { method: "GET", path: "/enrollment-tokens" },
51
57
  slack: { parameters: { type: "object", properties: {} } },
@@ -63,10 +69,16 @@ export const tokensCreateCapability = defineCapability({
63
69
  parameters: {
64
70
  type: "object",
65
71
  properties: {
66
- operator_id: { type: "string", description: "Commerce operator UUID from operator.list." },
72
+ organization_id: {
73
+ type: "string",
74
+ description: "Supplier organization UUID from supplier.list.",
75
+ },
67
76
  facility_id: { type: "string", description: "Optional facility UUID." },
68
77
  expires_in_seconds: { type: "integer", description: "Token TTL in seconds." },
69
- node_mode: { type: "string", enum: ["bare-metal", "virtual-machine"] },
78
+ filter: {
79
+ type: "string",
80
+ description: "Optional comma-separated enrollment compartment ids. Omit to run every step.",
81
+ },
70
82
  ip: { type: "string", description: "Optional IP stamped into the installer URL." },
71
83
  host: { type: "string", description: "SSH host/IP to enroll now." },
72
84
  hosts: {
@@ -76,26 +88,28 @@ export const tokensCreateCapability = defineCapability({
76
88
  },
77
89
  username: { type: "string", description: "SSH user. Default admin." },
78
90
  port: { type: "integer", description: "SSH port. Default 22." },
79
- private_key: { type: "string", description: "PEM private key. Required to enroll over SSH." },
91
+ private_key: {
92
+ type: "string",
93
+ description: "PEM private key. Required to enroll over SSH.",
94
+ },
80
95
  },
81
- required: ["operator_id"],
96
+ required: ["organization_id"],
82
97
  },
83
98
  },
84
99
  input: tokenCreateInput,
85
100
  execute: async (ctx, input) => {
86
101
  const parsed = tokenCreateInput.parse(input);
87
- const operatorId = (parsed.operator_id || parsed.operatorId || "").trim();
102
+ const organizationId = (parsed.organization_id || parsed.organizationId || "").trim();
88
103
  const hosts = [
89
104
  ...(parsed.host?.trim() ? [parsed.host.trim()] : []),
90
105
  ...(parsed.hosts ?? []).map((host) => host.trim()).filter(Boolean),
91
106
  ];
92
107
  const privateKey = parsed.private_key?.trim() || parsed.privateKey?.trim() || "";
93
- const nodeMode = parsed.node_mode ?? parsed.nodeMode;
94
108
  const payload = {
95
- operator_id: operatorId,
109
+ organization_id: organizationId,
96
110
  facility_id: parsed.facility_id ?? parsed.facilityId,
97
111
  expires_in_seconds: parsed.expires_in_seconds ?? parsed.expiresInSeconds,
98
- node_mode: nodeMode,
112
+ filter: parsed.filter,
99
113
  ip: parsed.ip,
100
114
  hosts,
101
115
  username: parsed.username?.trim() || "admin",
@@ -108,7 +122,7 @@ export const tokensCreateCapability = defineCapability({
108
122
  const minted = (await ctx.fetch("/enrollment-tokens", {
109
123
  method: "POST",
110
124
  body: {
111
- operator_id: operatorId,
125
+ organization_id: organizationId,
112
126
  ...(payload.facility_id ? { facility_id: payload.facility_id } : {}),
113
127
  ...(payload.expires_in_seconds ? { expires_in_seconds: payload.expires_in_seconds } : {}),
114
128
  },
@@ -119,6 +133,7 @@ export const tokensCreateCapability = defineCapability({
119
133
  token,
120
134
  base: parsed.installer_base ?? undefined,
121
135
  ip: parsed.ip ?? undefined,
136
+ filter: parsed.filter ?? undefined,
122
137
  force: payload.remote,
123
138
  })
124
139
  : null;
@@ -141,7 +156,7 @@ export const tokensCreateCapability = defineCapability({
141
156
  }
142
157
  return {
143
158
  id: minted.id,
144
- operator_id: minted.operator_id ?? operatorId,
159
+ organization_id: minted.organization_id ?? organizationId,
145
160
  expires_at: minted.expires_at,
146
161
  hosts: results,
147
162
  };
@@ -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,11 +1,12 @@
1
1
  import { namesFromId } from "./names.js";
2
2
  export const DOMAINS = [
3
+ "cli",
3
4
  "identity",
4
5
  "listings",
5
6
  "fleet",
6
7
  "users",
7
8
  "reservations",
8
- "bids",
9
+ "markets",
9
10
  "access",
10
11
  "clusters",
11
12
  "slurm",
@@ -14,6 +15,7 @@ export const DOMAINS = [
14
15
  "storage",
15
16
  "vpn",
16
17
  "billing",
18
+ "downtime",
17
19
  "observability",
18
20
  "gtm",
19
21
  ];
@@ -29,19 +31,26 @@ export const DOMAINS = [
29
31
  export function preview(action, payload) {
30
32
  return { status: "preview", requiresConfirm: true, confirmed: false, action, payload };
31
33
  }
32
- /** Fill CLI/MCP/Slack names from `id`. Slack parameters stay optional. */
34
+ /** CLI is opt-in; MCP and Slack retain derived names unless explicitly unbound. */
33
35
  export function defineCapability(spec) {
34
36
  const names = namesFromId(spec.id);
35
- const { slack, ...rest } = spec;
37
+ const { cli, mcp, slack, ...rest } = spec;
36
38
  return {
37
39
  ...rest,
38
- cli: { command: names.cli },
39
- mcp: { name: names.mcp },
40
+ ...(cli !== false && cli !== undefined
41
+ ? {
42
+ cli: {
43
+ command: cli.command ?? names.cli,
44
+ ...(cli.vnext ? { vnext: cli.vnext } : {}),
45
+ },
46
+ }
47
+ : {}),
48
+ ...(mcp === false ? {} : { mcp: { name: mcp?.name ?? names.mcp } }),
40
49
  ...(slack === false
41
50
  ? {}
42
51
  : {
43
52
  slack: {
44
- name: names.slack,
53
+ name: slack?.name ?? names.slack,
45
54
  ...(slack?.parameters ? { parameters: slack.parameters } : {}),
46
55
  },
47
56
  }),
@@ -61,19 +70,17 @@ export function defineCapability(spec) {
61
70
  export function defineStaffMcpCapability(spec) {
62
71
  return { ...spec, mcp: { name: namesFromId(spec.id).mcp } };
63
72
  }
64
- export function slackStaffVisible(capability) {
65
- return capability.roles.some((role) => role === "reviewer" || role === "admin");
66
- }
67
73
  export function visibleToClient(capability, client, role) {
68
74
  if (!capability.roles.includes(role))
69
75
  return false;
70
- if (client === "slack") {
71
- if (role === "user")
76
+ if (client === "slack")
77
+ return capability.slack != null;
78
+ if (client === "cli") {
79
+ const help = capability.cli?.vnext;
80
+ if (!help || !capability.cli)
72
81
  return false;
73
- return slackStaffVisible(capability) && capability.slack != null;
82
+ return help.audience === "customer" ? role === "user" : role === "admin";
74
83
  }
75
- if (client === "cli")
76
- return capability.cli != null;
77
84
  return capability.mcp != null;
78
85
  }
79
86
  export function forRole(catalog, role, client) {
@@ -0,0 +1,67 @@
1
+ import { adminCapabilities, bindAdminCapabilities } from "./capabilities/admin.js";
2
+ import { adminCapacityReservationCapabilities } from "./capabilities/admin-capacity-reservations.js";
3
+ import { commerceMarketCapabilities } from "./capabilities/commerce-market.js";
4
+ import { marketCapabilities } from "./capabilities/markets.js";
5
+ import { adminEnrollmentCapabilities, bindAdminEnrollmentCapabilities, } from "./capabilities/admin-enrollment.js";
6
+ import { adminNodeDiagnosticCapabilities } from "./capabilities/admin-node-diagnostics.js";
7
+ import { adminNodeLifecycleCapabilities } from "./capabilities/admin-nodes-lifecycle.js";
8
+ import { computeCapabilities } from "./capabilities/compute.js";
9
+ import { cliShellCapabilities } from "./capabilities/cli.js";
10
+ import { clusterCliCapabilities, kubernetesCapabilities, slurmCapabilities, } from "./capabilities/clusters.js";
11
+ import { externalBucketCapabilities } from "./capabilities/external-buckets.js";
12
+ import { evidenceCapabilities } from "./capabilities/evidence.js";
13
+ import { fleetCapabilities } from "./capabilities/fleet.js";
14
+ import { tokenCapabilities } from "./capabilities/tokens.js";
15
+ import { mountCapabilities } from "./capabilities/mounts.js";
16
+ import { identityCapabilities } from "./capabilities/identity.js";
17
+ import { listingCapabilities } from "./capabilities/listings.js";
18
+ import { observabilityCapabilities } from "./capabilities/observability.js";
19
+ import { networkCapabilities } from "./capabilities/networks.js";
20
+ import { volumeCapabilities } from "./capabilities/volumes.js";
21
+ import { userCapabilities } from "./capabilities/users.js";
22
+ function canonicalCliCatalog(capabilities) {
23
+ return capabilities.map((capability) => {
24
+ const help = capability.cli?.vnext;
25
+ if (!help) {
26
+ const { cli: _retiredCliBinding, ...withoutCli } = capability;
27
+ return withoutCli;
28
+ }
29
+ return {
30
+ ...capability,
31
+ cli: {
32
+ command: [...(help.command ?? capability.cli?.command ?? [])],
33
+ vnext: help,
34
+ },
35
+ };
36
+ });
37
+ }
38
+ /** Canonical operational catalog, without staff-only commerce or CRM modules. */
39
+ export const catalog = [
40
+ ...canonicalCliCatalog([
41
+ ...bindAdminEnrollmentCapabilities(bindAdminCapabilities([
42
+ ...cliShellCapabilities,
43
+ ...identityCapabilities,
44
+ ...listingCapabilities,
45
+ ...commerceMarketCapabilities,
46
+ ...marketCapabilities,
47
+ ...computeCapabilities,
48
+ ...fleetCapabilities.filter((capability) => capability.id !== "node.exec"),
49
+ ...networkCapabilities,
50
+ ...clusterCliCapabilities,
51
+ ...tokenCapabilities,
52
+ ...userCapabilities,
53
+ ...volumeCapabilities,
54
+ ...mountCapabilities,
55
+ ...externalBucketCapabilities,
56
+ ...evidenceCapabilities,
57
+ ...observabilityCapabilities,
58
+ ...slurmCapabilities,
59
+ ...kubernetesCapabilities,
60
+ ...adminNodeLifecycleCapabilities,
61
+ ])),
62
+ ...adminCapabilities.filter((capability) => capability.id === "nodes.health"),
63
+ ...adminCapacityReservationCapabilities.filter((capability) => capability.id !== "listings.create"),
64
+ ...adminNodeDiagnosticCapabilities,
65
+ ...adminEnrollmentCapabilities,
66
+ ]),
67
+ ];