@ornncompute/cli 1.0.35 → 1.0.48

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.
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
- import { defineCapability } from "../capability.js";
3
- import { createListingCapability } from "./listings.js";
2
+ import { defineCapability, preview } from "../capability.js";
3
+ import { nativeListingCreateInput } from "./native-listing.js";
4
+ import { idempotencyHeaders } from "./market-shared.js";
4
5
  function adminHelp(command, outcome, human, automation, review) {
5
6
  return {
6
7
  audience: "admin",
@@ -13,78 +14,58 @@ function adminHelp(command, outcome, human, automation, review) {
13
14
  ...(review ? { review } : {}),
14
15
  };
15
16
  }
16
- const capacityOfferCreateInput = z.object({
17
- gpu_type: z.string().min(1),
18
- node_count: z.number().int().positive(),
19
- gpus_per_node: z.number().int().min(1).max(8),
20
- site_nickname: z.string().min(1),
21
- available_start_at: z.string().min(1),
22
- available_end_at: z.string().min(1),
23
- buy_now_price_per_gpu_hour: z.number().positive(),
24
- cost_per_gpu_hour: z.number().positive().optional(),
25
- down_payment_cents: z.number().int().nonnegative().optional(),
26
- access_mode: z.enum(["bare-metal", "virtual-machine"]).optional(),
27
- });
28
- export const adminCapacityOffersCreateCapability = {
29
- ...createListingCapability,
17
+ export const adminCapacityOffersCreateCapability = defineCapability({
18
+ id: "listing.create",
19
+ domain: "listings",
20
+ roles: ["admin"],
21
+ description: "Create native capacity with explicit organization/facility IDs, hardware, availability and advertised terms. Returns {listing, terms}. Requires an idempotencyKey reused on retries and confirm: true.",
22
+ mutation: "preview-confirm",
23
+ http: { method: "POST", path: "/commerce/v2/listings" },
24
+ input: nativeListingCreateInput,
25
+ slack: false,
30
26
  cli: {
31
- ...createListingCapability.cli,
32
- vnext: adminHelp(["admin", "capacity-offers", "create"], "Create one primary listing from explicit commercial terms.", "ornn admin capacity-offers create --gpu GPU --nodes COUNT --gpus-per-node COUNT --site SITE --starts-at TIME --ends-at TIME --price-per-gpu-hour USD [--supplier-price-per-gpu-hour USD] [--down-payment-cents CENTS] [--access-mode bare-metal|virtual-machine]", "Not supported in machine or redirected execution because offer creation has no durable replay identity.", "GPU, site, availability window, supplier price, spread, and down payment are normalized before default-no consent."),
27
+ command: ["admin", "capacity-offers", "create"],
28
+ vnext: adminHelp(["admin", "capacity-offers", "create"], "Create native capacity and its advertised financial terms.", "ornn admin capacity-offers create --gpu-slug GPU --capacity-node-count COUNT --gpus-per-node COUNT --facility-id UUID --provisioning-type bare-metal|virtual-machine --state draft|available --terms JSON --idempotency-key KEY [--organization-id UUID] [--available-at TIME --available-until TIME] [--days-to-rfs DAYS]", "Requires an eligible human terminal; reuse --idempotency-key on retries.", "Review the native parent fields and each financial term before default-no consent."),
33
29
  },
34
- input: createListingCapability.input,
35
30
  execute: async (ctx, input) => {
36
- if (!ctx.cli)
37
- return createListingCapability.execute(ctx, createListingCapability.input.parse(input));
38
- const result = await createListingCapability.execute(ctx, capacityOfferCreateInput.parse(input));
39
- if (!ctx.confirmed || object(result).ok === false)
40
- return result;
41
- return { listing: result };
31
+ const { confirm: _confirm, idempotencyKey, ...body } = nativeListingCreateInput.parse(input);
32
+ if (!ctx.confirmed)
33
+ return preview("listing.create", body);
34
+ return ctx.fetch("/commerce/v2/listings", {
35
+ method: "POST",
36
+ body,
37
+ headers: idempotencyHeaders(ctx, idempotencyKey),
38
+ });
42
39
  },
43
- };
44
- const MICRO_USD = 1_000_000;
40
+ });
45
41
  export const reservationCreateInput = z.object({
46
- organization_id: z.string().uuid(),
47
- listing_id: z.string().uuid(),
48
- start_at: z.string().min(1),
49
- end_at: z.string().min(1),
50
- price_per_gpu_hour: z
42
+ customerId: z.string().uuid(),
43
+ listingTermId: z.string().uuid(),
44
+ reservedNodeCount: z.number().int().positive().max(2_147_483_647),
45
+ reservedGpuCount: z.number().int().positive().max(2_147_483_647),
46
+ startsAt: z.iso.datetime({ offset: true }),
47
+ endsAt: z.iso.datetime({ offset: true }).nullable().optional(),
48
+ transactionId: z
51
49
  .string()
52
- .regex(/^\d+(?:\.\d{1,6})?$/)
53
- .describe("USD per GPU-hour, for example 2.50."),
54
- down_payment_cents: z.number().int().nonnegative().optional(),
55
- notes: z.string().max(2000).optional(),
50
+ .uuid()
51
+ .describe("Reuse this transaction ID when retrying the same reservation."),
52
+ vmDiskId: z.string().uuid().nullable().optional(),
56
53
  });
57
- function object(value) {
58
- return value && typeof value === "object" && !Array.isArray(value)
59
- ? value
60
- : {};
61
- }
62
54
  export const adminReservationsCreateCapability = defineCapability({
63
55
  id: "reservations.create",
64
56
  domain: "reservations",
65
57
  roles: ["admin"],
66
- description: "Create one schedule reservation on a listing from explicit, reviewed commercial terms (admin). The reservation covers the listing's nodes for the window.",
58
+ description: "Create one reservation from a listing term, customer, quantities, dates, and a stable transaction ID (admin).",
67
59
  mutation: "preview-confirm",
68
60
  http: { method: "POST", path: "/commerce/reservations" },
69
61
  cli: {
70
62
  command: ["admin", "reservations", "create"],
71
- vnext: adminHelp(["admin", "reservations", "create"], "Create one schedule reservation from explicit commercial terms.", "ornn admin reservations create --organization ID --capacity-offer ID --starts-at TIME --ends-at TIME --price-per-gpu-hour USD [--down-payment-cents CENTS]", "Not supported in machine or redirected execution because reservation creation has no durable replay identity.", "Organization, listing, dates, price, and down payment are normalized before default-no consent."),
63
+ vnext: adminHelp(["admin", "reservations", "create"], "Create one reservation using the selected listing term and quantities.", "ornn admin reservations create --customer ID --listing-term ID --nodes COUNT --gpus COUNT --starts-at TIME --transaction ID [--ends-at TIME] [--vm-disk ID]", "Requires an eligible human terminal. Reuse --transaction when retrying the same reservation.", "Customer, listing term, reserved quantities, dates, VM disk, and transaction ID are reviewed before default-no consent."),
72
64
  },
73
65
  slack: false,
74
66
  input: reservationCreateInput,
75
67
  execute: async (ctx, input) => {
76
- const parsed = reservationCreateInput.parse(input);
77
- const body = {
78
- listingId: parsed.listing_id,
79
- organizationId: parsed.organization_id,
80
- startsAt: parsed.start_at,
81
- endsAt: parsed.end_at,
82
- pricePerGpuHourMicroUsd: Math.round(Number(parsed.price_per_gpu_hour) * MICRO_USD),
83
- ...(parsed.down_payment_cents !== undefined
84
- ? { downPaymentCents: parsed.down_payment_cents }
85
- : {}),
86
- ...(parsed.notes ? { notes: parsed.notes } : {}),
87
- };
68
+ const body = reservationCreateInput.parse(input);
88
69
  if (!ctx.confirmed)
89
70
  return { preview: body };
90
71
  return { reservation: await ctx.fetch("/commerce/reservations", { method: "POST", body }) };
@@ -3,10 +3,10 @@ import { defineCapability } from "../capability.js";
3
3
  import { listFacilitiesCapability, listNodesCapability, listSuppliersCapability } from "./fleet.js";
4
4
  import { logsLatestCapability, telemetryLatestCapability } from "./observability.js";
5
5
  import { tokensListCapability } from "./tokens.js";
6
- import { usersQueryCapability } from "./users.js";
7
- import { adminCapacityOffersCreateCapability } from "./admin-capacity-reservations.js";
6
+ import { organizationListCapability, usersListCapability } from "./users.js";
8
7
  export const ADMIN_CLI_ROOTS = [
9
8
  { name: "users" },
9
+ { name: "organizations" },
10
10
  { name: "suppliers" },
11
11
  { name: "facilities" },
12
12
  { name: "nodes" },
@@ -36,7 +36,8 @@ function withAdminBinding(capability, help) {
36
36
  },
37
37
  };
38
38
  }
39
- export const adminUsersSearchCapability = withAdminBinding(usersQueryCapability, adminHelp(["admin", "users", "search"], "Search the staff-authorized user directory.", "ornn admin users search [--name NAME] [--email EMAIL] [--organization NAME] [--limit 50] [--cursor CURSOR]", "ornn admin users search --email EMAIL --limit 50 [--cursor CURSOR] --format json"));
39
+ export const adminUsersListCapability = withAdminBinding(usersListCapability, adminHelp(["admin", "users", "list"], "List users belonging to organizations in the Authorization directory.", "ornn admin users list [--query QUERY] [--organization UUID] [--limit 25] [--offset OFFSET] [--cursor CURSOR]", "ornn admin users list --query QUERY --limit 25 [--cursor CURSOR] --format json"));
40
+ export const adminOrganizationsListCapability = withAdminBinding(organizationListCapability, adminHelp(["admin", "organizations", "list"], "List organizations in the Authorization directory.", "ornn admin organizations list [--query QUERY] [--role consumer|supplier] [--approved true|false] [--ids UUID,...] [--limit 25] [--offset OFFSET] [--cursor CURSOR]", "ornn admin organizations list --query QUERY --limit 25 [--cursor CURSOR] --format json"));
40
41
  export const adminSuppliersListCapability = withAdminBinding(listSuppliersCapability, adminHelp(["admin", "suppliers", "list"], "List supplier organizations visible to staff.", "ornn admin suppliers list [--limit 50] [--cursor CURSOR]", "ornn admin suppliers list --limit 50 --format json"));
41
42
  export const adminFacilitiesListCapability = withAdminBinding(listFacilitiesCapability, adminHelp(["admin", "facilities", "list"], "List facilities visible to staff.", "ornn admin facilities list [--supplier UUID] [--limit 50] [--cursor CURSOR]", "ornn admin facilities list --limit 50 --format json"));
42
43
  export const adminNodesListCapability = withAdminBinding(listNodesCapability, adminHelp(["admin", "nodes", "list"], "List enrolled, ghost, and off-grid nodes.", "ornn admin nodes list [--supplier UUID] [--facility FACILITY] [--status STATUS] [--email EMAIL] [--limit 50] [--cursor CURSOR]", "ornn admin nodes list --limit 50 [--cursor CURSOR] --format json"));
@@ -64,7 +65,8 @@ export const adminNodeMetricsCapability = withAdminBinding(telemetryLatestCapabi
64
65
  export const adminNodeLogsCapability = withAdminBinding(logsLatestCapability, adminHelp(["admin", "nodes", "logs"], "Read one bounded node log page.", "ornn admin nodes logs <node-id> [--stream kernel|serial] [--count 200] [--before TIME] [--cursor CURSOR]", "ornn admin nodes logs <node-id> --stream kernel --count 200 [--before TIME] [--cursor CURSOR] --format json", ["canonical node ID"]));
65
66
  export const adminEnrollmentTokensListCapability = withAdminBinding(tokensListCapability, adminHelp(["admin", "enrollment-tokens", "list"], "List enrollment-token metadata without token bytes.", "ornn admin enrollment-tokens list [--limit 50] [--cursor CURSOR]", "ornn admin enrollment-tokens list --limit 50 --format json"));
66
67
  export const adminCapabilities = [
67
- adminUsersSearchCapability,
68
+ adminUsersListCapability,
69
+ adminOrganizationsListCapability,
68
70
  adminSuppliersListCapability,
69
71
  adminFacilitiesListCapability,
70
72
  adminNodesListCapability,
@@ -74,14 +76,14 @@ export const adminCapabilities = [
74
76
  adminEnrollmentTokensListCapability,
75
77
  ];
76
78
  const replacements = {
77
- "listings.create": adminCapacityOffersCreateCapability,
78
79
  "facilities.list": adminFacilitiesListCapability,
79
80
  "logs.latest": adminNodeLogsCapability,
80
81
  "nodes.list": adminNodesListCapability,
81
82
  "supplier.list": adminSuppliersListCapability,
82
83
  "telemetry.latest": adminNodeMetricsCapability,
83
84
  "tokens.list": adminEnrollmentTokensListCapability,
84
- "users.query": adminUsersSearchCapability,
85
+ "users.list": adminUsersListCapability,
86
+ "organization.list": adminOrganizationsListCapability,
85
87
  };
86
88
  /** Applies canonical admin CLI bindings without changing MCP or Slack identities. */
87
89
  export function bindAdminCapabilities(capabilities) {
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
- import { defineCapability } from "../capability.js";
3
- import { consent, flag, idempotencyHeaders, microUsdLabel, positionals, query, seg, } from "./market-shared.js";
2
+ import { nativeTermsInput, nativeTermFields, listingWriteControls } from "./native-listing.js";
3
+ import { defineCapability, preview } from "../capability.js";
4
+ import { consent, idempotencyHeaders, positionals, query, seg } from "./market-shared.js";
4
5
  // Commerce market verbs: one capability per role-aware `/commerce` route.
5
6
  // Commerce decides from the caller's session what each listing or
6
7
  // reservation reveals; these verbs return its JSON unchanged.
@@ -18,7 +19,6 @@ function help(command, outcome, human, automation, related, order, acceptedRefer
18
19
  order,
19
20
  };
20
21
  }
21
- const microUsd = z.coerce.number().int().positive();
22
22
  const pageLimit = z.coerce.number().int().min(1).max(200).optional();
23
23
  const nodeIdList = z.preprocess((value) => typeof value === "string"
24
24
  ? value
@@ -28,29 +28,29 @@ const nodeIdList = z.preprocess((value) => typeof value === "string"
28
28
  : value, z.array(z.string().uuid()).min(1));
29
29
  export const listingsListInput = z.object({
30
30
  $positionals: positionals,
31
- kind: z.enum(["primary", "secondary"]).optional(),
32
- state: z.enum(["available", "internal", "delisted"]).optional(),
31
+ kind: z.enum(["primary", "secondary", "on_demand"]).optional(),
32
+ state: z.enum(["draft", "available", "held", "sold", "delisted"]).optional(),
33
33
  sourceReservationId: z.string().uuid().optional(),
34
34
  sourceListingId: z.string().uuid().optional(),
35
- limit: pageLimit,
35
+ limit: z.coerce.number().int().min(1).max(100).optional(),
36
36
  cursor: z.string().min(1).optional(),
37
37
  });
38
38
  export const listingsListCapability = defineCapability({
39
39
  id: "listings.list",
40
40
  domain: "listings",
41
41
  roles: ["user"],
42
- description: "List Commerce listings. Primary listings are shown to approved organizations; secondary listings (relists) to every signed-in organization. Prices are micro-USD per GPU-hour.",
42
+ description: "List native capacity listings. Primary listings require organization approval. Returns capacityListingId, hardware, capacity and availability; read listings.terms separately for prices and durations.",
43
43
  mutation: "read",
44
- http: { method: "GET", path: "/commerce/listings" },
44
+ http: { method: "GET", path: "/commerce/v2/listings" },
45
45
  input: listingsListInput,
46
46
  cli: {
47
47
  command: ["capacity-offers", "list"],
48
- vnext: help(["capacity-offers", "list"], "List primary and secondary listings with bounded pagination and no supplier provenance.", "ornn capacity-offers list [--kind primary|secondary] [--source-reservation-id <id>] [--limit <1-200>] [--cursor <opaque>]", "ornn capacity-offers list --limit 50 --format json", ["capacity-offers show", "schedules list"], 20, ["opaque page cursor"]),
48
+ vnext: help(["capacity-offers", "list"], "List capacity parents with bounded pagination; read their financial terms separately.", "ornn capacity-offers list [--kind primary|secondary|on_demand] [--source-reservation-id <id>] [--limit <1-100>] [--cursor <opaque>]", "ornn capacity-offers list --limit 50 --format json", ["capacity-offers show", "capacity-offers terms", "schedules list"], 20, ["opaque page cursor"]),
49
49
  },
50
50
  slack: false,
51
51
  execute: async (ctx, input) => {
52
52
  const parsed = listingsListInput.parse(input ?? {});
53
- return ctx.fetch(`/commerce/listings${query({
53
+ return ctx.fetch(`/commerce/v2/listings${query({
54
54
  kind: parsed.kind,
55
55
  state: parsed.state,
56
56
  sourceReservationId: parsed.sourceReservationId,
@@ -68,122 +68,113 @@ export const listingShowCapability = defineCapability({
68
68
  id: "listings.show",
69
69
  domain: "listings",
70
70
  roles: ["user"],
71
- description: "Show one Commerce listing: specs, window, node count and its buyer price.",
71
+ description: "Show one native capacity listing. Returns capacityListingId, hardware, capacity and availability; use listings.terms for financial choices.",
72
72
  mutation: "read",
73
- http: { method: "GET", path: "/commerce/listings/{listingId}" },
73
+ http: { method: "GET", path: "/commerce/v2/listings/{listingId}" },
74
74
  input: listingShowInput,
75
75
  cli: {
76
76
  command: ["capacity-offers", "show"],
77
- vnext: help(["capacity-offers", "show"], "Inspect one listing's hardware, facility, window and price.", "ornn capacity-offers show --listing-id <id>", "ornn capacity-offers show --listing-id <id> --format json", ["capacity-offers list", "schedules show"], 21, ["listing ID"]),
77
+ vnext: help(["capacity-offers", "show"], "Inspect one listing's hardware, capacity and availability.", "ornn capacity-offers show --listing-id <id>", "ornn capacity-offers show --listing-id <id> --format json", ["capacity-offers list", "capacity-offers terms", "schedules show"], 21, ["listing ID"]),
78
78
  },
79
79
  slack: false,
80
80
  execute: async (ctx, input) => {
81
81
  const parsed = listingShowInput.parse(input);
82
- return ctx.fetch(`/commerce/listings/${seg(parsed.listingId)}`, { method: "GET" });
82
+ return ctx.fetch(`/commerce/v2/listings/${seg(parsed.listingId)}`, { method: "GET" });
83
83
  },
84
84
  });
85
- export const listingRelistInput = z
86
- .object({
85
+ export const listingTermsCapability = defineCapability({
86
+ id: "listings.terms",
87
+ domain: "listings",
88
+ roles: ["user"],
89
+ description: "Read a capacity listing's advertised financial terms. Each term returns listingTermsId, capacityListingId, pricePerGpuHourMicroUsd, duration bounds, downPaymentCents and renewable. Use the parent's capacityListingId as listingId. Prices are integer micro-USD per GPU-hour.",
90
+ mutation: "read",
91
+ http: { method: "GET", path: "/commerce/v2/listings/{listingId}/terms" },
92
+ input: listingShowInput,
93
+ cli: {
94
+ command: ["capacity-offers", "terms"],
95
+ vnext: help(["capacity-offers", "terms"], "Read the advertised financial terms for a capacity listing.", "ornn capacity-offers terms --listing-id <id>", "ornn capacity-offers terms --listing-id <id> --format json", ["capacity-offers show", "offers post"], 22, ["capacityListingId from capacity-offers list or show"]),
96
+ },
97
+ slack: false,
98
+ execute: async (ctx, input) => {
99
+ const parsed = listingShowInput.parse(input);
100
+ return ctx.fetch(`/commerce/v2/listings/${seg(parsed.listingId)}/terms`, { method: "GET" });
101
+ },
102
+ });
103
+ export const listingRelistInput = z.object({
87
104
  $positionals: positionals,
88
- sourceReservationId: z
89
- .string()
90
- .uuid()
91
- .optional()
92
- .describe("A confirmed or active schedule reservation you hold."),
93
- sourceListingId: z
94
- .string()
95
- .uuid()
96
- .optional()
97
- .describe("A primary listing you own (suppliers relist their own listings)."),
98
- nodeIds: nodeIdList.describe("The nodes to relist: a subset of the source's nodes (comma-separated on the CLI)."),
99
- startsAt: z.string().min(1).describe("RFC 3339 start, inside the source's window."),
100
- endsAt: z.string().min(1).describe("RFC 3339 end, inside the source's window."),
101
- askPricePerGpuHourMicroUsd: microUsd.describe("Asking price in micro-USD per GPU-hour, the listing's only price: buyers buy any open window at it, and idle time sells as spot at it."),
102
- sellAtMarketFirst: flag
103
- .default(false)
104
- .describe("Sell each node's GPUs into resting spot bids before asking at the asking price."),
105
+ reservationId: z.string().uuid(),
106
+ startsAt: z.iso.datetime({ offset: true }),
107
+ endsAt: z.iso.datetime({ offset: true }),
108
+ nodeCount: z.coerce.number().int().positive(),
109
+ nodeIds: nodeIdList.optional(),
110
+ reservedNodeIds: nodeIdList.optional(),
111
+ terms: nativeTermsInput,
105
112
  idempotencyKey: z.string().min(1).max(200).optional(),
106
113
  confirm: z.boolean().optional(),
107
- })
108
- .refine((relist) => (relist.sourceReservationId === undefined) !== (relist.sourceListingId === undefined), {
109
- message: "Give exactly one of sourceReservationId or sourceListingId.",
110
- path: ["sourceReservationId"],
111
114
  });
112
115
  export const listingRelistCapability = defineCapability({
113
116
  id: "listings.relist",
114
117
  domain: "listings",
115
118
  roles: ["user"],
116
- description: "Relist a time slice and/or node subset of a schedule reservation you hold (or a primary listing you own) as a secondary listing with an asking price. " +
117
- "Buyers buy any open window of it at the asking price, and its unsold time goes on the spot book. Fails with node_window_sold if those nodes are already sold for part of the window, or relist_overlaps if another live relist of those nodes overlaps it. Requires confirm: true.",
119
+ description: "Relist delivered capacity from a reservation using a native availability window, nodeCount and financial terms. Supply nodeIds or reservedNodeIds only when selecting specific nodes. Requires confirm: true.",
118
120
  mutation: "preview-confirm",
119
- http: { method: "POST", path: "/commerce/listings" },
121
+ http: { method: "POST", path: "/commerce/reservations/{reservationId}/relist" },
120
122
  input: listingRelistInput,
121
123
  cli: {
122
124
  command: ["capacity-offers", "relist"],
123
- vnext: help(["capacity-offers", "relist"], "Relist part of a reservation or primary listing you hold.", "ornn capacity-offers relist --source-reservation-id <id> --node-ids <id,...> --starts-at <time> --ends-at <time> --ask-price-per-gpu-hour-micro-usd <n> [--sell-at-market-first true]", "ornn capacity-offers relist --source-reservation-id <id> --node-ids <id> --starts-at <time> --ends-at <time> --ask-price-per-gpu-hour-micro-usd 2500000 --yes --format json", ["capacity-offers update", "capacity-offers delist", "reservations show"], 23, ["reservation ID", "listing ID", "node ID"]),
125
+ vnext: help(["capacity-offers", "relist"], "Relist reserved capacity with explicit financial terms.", "ornn capacity-offers relist --reservation-id UUID --node-count COUNT --starts-at TIME --ends-at TIME --terms JSON [--node-ids UUID,...] [--reserved-node-ids UUID,...]", "ornn capacity-offers relist --reservation-id UUID --node-count 1 --starts-at TIME --ends-at TIME --terms JSON --idempotency-key KEY --yes --format json", ["capacity-offers terms", "capacity-offers delist"], 23, ["reservation ID"]),
124
126
  },
125
127
  slack: false,
126
128
  execute: async (ctx, input) => {
127
- const parsed = listingRelistInput.parse(input);
128
- const body = {
129
- ...(parsed.sourceReservationId ? { sourceReservationId: parsed.sourceReservationId } : {}),
130
- ...(parsed.sourceListingId ? { sourceListingId: parsed.sourceListingId } : {}),
131
- nodeIds: parsed.nodeIds,
132
- startsAt: parsed.startsAt,
133
- endsAt: parsed.endsAt,
134
- askPricePerGpuHourMicroUsd: parsed.askPricePerGpuHourMicroUsd,
135
- sellAtMarketFirst: parsed.sellAtMarketFirst,
136
- };
129
+ const { $positionals: _, reservationId, confirm: _confirm, idempotencyKey, ...body } = listingRelistInput.parse(input);
137
130
  const gate = await consent(ctx, "listings.relist", "Relist capacity", [
138
- {
139
- label: "Source",
140
- value: parsed.sourceReservationId
141
- ? `reservation ${parsed.sourceReservationId}`
142
- : `listing ${parsed.sourceListingId}`,
143
- },
144
- { label: "Nodes", value: parsed.nodeIds.join(", ") },
145
- { label: "Window", value: `${parsed.startsAt} to ${parsed.endsAt}` },
146
- { label: "Asking price", value: microUsdLabel(parsed.askPricePerGpuHourMicroUsd) },
147
- { label: "Sell at market first", value: parsed.sellAtMarketFirst ? "yes" : "no" },
148
- ], body);
131
+ { label: "Reservation", value: reservationId },
132
+ { label: "Capacity and financial terms", value: JSON.stringify(body) },
133
+ ], { reservationId, ...body });
149
134
  if (gate)
150
135
  return gate;
151
- return ctx.fetch("/commerce/listings", {
136
+ return ctx.fetch(`/commerce/reservations/${seg(reservationId)}/relist`, {
152
137
  method: "POST",
153
138
  body,
154
- headers: idempotencyHeaders(ctx, parsed.idempotencyKey),
139
+ headers: idempotencyHeaders(ctx, idempotencyKey),
155
140
  });
156
141
  },
157
142
  });
158
- export const listingUpdateInput = z.object({
143
+ export const listingUpdateInput = z
144
+ .object({
159
145
  $positionals: positionals,
160
146
  listingId: z.string().uuid(),
161
- askPricePerGpuHourMicroUsd: microUsd,
162
- confirm: z.boolean().optional(),
163
- });
147
+ listingTermsId: z.string().uuid(),
148
+ ...nativeTermFields,
149
+ ...listingWriteControls,
150
+ })
151
+ .strict();
164
152
  export const listingUpdateCapability = defineCapability({
165
153
  id: "listings.update",
166
154
  domain: "listings",
167
- roles: ["user"],
168
- description: "Change the asking price of a secondary listing you own, its only price. Windows bought later use the new price, and the listing's spot asks are re-sent at it. Requires confirm: true.",
155
+ roles: ["admin"],
156
+ description: "Staff only: edit one advertised native financial term with its complete financial fields. Reuse idempotencyKey on retries. Requires confirm: true.",
169
157
  mutation: "preview-confirm",
170
- http: { method: "PATCH", path: "/commerce/listings/{listingId}" },
158
+ http: { method: "PATCH", path: "/commerce/v2/listings/{listingId}/terms/{listingTermsId}" },
171
159
  input: listingUpdateInput,
172
160
  cli: {
173
- command: ["capacity-offers", "update"],
174
- vnext: help(["capacity-offers", "update"], "Change the asking price of a listing you relisted.", "ornn capacity-offers update --listing-id <id> --ask-price-per-gpu-hour-micro-usd <n>", "ornn capacity-offers update --listing-id <id> --ask-price-per-gpu-hour-micro-usd 2400000 --yes --format json", ["capacity-offers relist", "capacity-offers delist"], 24, ["listing ID"]),
161
+ command: ["admin", "capacity-offers", "update"],
162
+ vnext: {
163
+ ...help(["admin", "capacity-offers", "update"], "Edit one advertised financial term as staff.", "ornn admin capacity-offers update --listing-id UUID --listing-terms-id UUID --min-duration-hours HOURS --max-duration-hours HOURS --price-per-gpu-hour-micro-usd RATE --renewable true|false --idempotency-key KEY [--type reserved|spot|on-demand] [--down-payment-cents CENTS]", "Requires an eligible human terminal; reuse --idempotency-key on retries.", ["capacity-offers terms"], 24),
164
+ audience: "admin",
165
+ },
175
166
  },
176
167
  slack: false,
168
+ mcp: false,
177
169
  execute: async (ctx, input) => {
178
- const parsed = listingUpdateInput.parse(input);
179
- const body = { askPricePerGpuHourMicroUsd: parsed.askPricePerGpuHourMicroUsd };
180
- const gate = await consent(ctx, "listings.update", "Update listing asking price", [
181
- { label: "Listing", value: parsed.listingId },
182
- { label: "Asking price", value: microUsdLabel(body.askPricePerGpuHourMicroUsd) },
183
- ], { listingId: parsed.listingId, ...body });
184
- if (gate)
185
- return gate;
186
- return ctx.fetch(`/commerce/listings/${seg(parsed.listingId)}`, { method: "PATCH", body });
170
+ const { $positionals: _, listingId, listingTermsId, confirm: _confirm, idempotencyKey, ...body } = listingUpdateInput.parse(input);
171
+ if (!ctx.confirmed)
172
+ return preview("listings.update", { listingId, listingTermsId, ...body });
173
+ return ctx.fetch(`/commerce/v2/listings/${seg(listingId)}/terms/${seg(listingTermsId)}`, {
174
+ method: "PATCH",
175
+ body,
176
+ headers: idempotencyHeaders(ctx, idempotencyKey),
177
+ });
187
178
  },
188
179
  });
189
180
  export const listingDelistInput = z.object({
@@ -197,7 +188,7 @@ export const listingDelistCapability = defineCapability({
197
188
  roles: ["user"],
198
189
  description: "Delist a listing you own. Its unsold time leaves the market, its spot asks are cancelled and running spot reservations on it end. Windows already bought stay sold. Requires confirm: true.",
199
190
  mutation: "preview-confirm",
200
- http: { method: "DELETE", path: "/commerce/listings/{listingId}" },
191
+ http: { method: "POST", path: "/commerce/v2/listings/{listingId}/delist" },
201
192
  input: listingDelistInput,
202
193
  cli: {
203
194
  command: ["capacity-offers", "delist"],
@@ -215,7 +206,7 @@ export const listingDelistCapability = defineCapability({
215
206
  ], { listingId: parsed.listingId });
216
207
  if (gate)
217
208
  return gate;
218
- return ctx.fetch(`/commerce/listings/${seg(parsed.listingId)}`, { method: "DELETE" });
209
+ return ctx.fetch(`/commerce/v2/listings/${seg(parsed.listingId)}/delist`, { method: "POST" });
219
210
  },
220
211
  });
221
212
  export const reservationsListInput = z.object({
@@ -301,30 +292,14 @@ export const reservationInvoicesCapability = defineCapability({
301
292
  });
302
293
  },
303
294
  });
304
- export const feesShowInput = z.object({ $positionals: positionals });
305
- export const feesShowCapability = defineCapability({
306
- id: "fees.show",
307
- domain: "billing",
308
- roles: ["user"],
309
- description: "Show the transaction fee on spot and secondary trades, in parts per million (1000000 = 100%). The side that takes a resting order pays it; primary schedules carry no transaction fee.",
310
- mutation: "read",
311
- http: { method: "GET", path: "/commerce/fees" },
312
- input: feesShowInput,
313
- cli: {
314
- command: ["fees", "show"],
315
- vnext: help(["fees", "show"], "Show the transaction fee on spot and secondary trades.", "ornn fees show", "ornn fees show --format json", ["spot bid", "offers post"], 29),
316
- },
317
- slack: false,
318
- execute: async (ctx) => ctx.fetch("/commerce/fees", { method: "GET" }),
319
- });
320
295
  export const commerceMarketCapabilities = [
321
296
  listingsListCapability,
322
297
  listingShowCapability,
298
+ listingTermsCapability,
323
299
  listingRelistCapability,
324
300
  listingUpdateCapability,
325
301
  listingDelistCapability,
326
302
  reservationsListCapability,
327
303
  reservationsShowCapability,
328
304
  reservationInvoicesCapability,
329
- feesShowCapability,
330
305
  ];
@@ -23,6 +23,7 @@ export const launchInput = z
23
23
  resume: z.union([z.string().min(1), z.literal(true)]).optional(),
24
24
  timeout: z.string().optional(),
25
25
  username: z.string().optional(),
26
+ vms: z.string().optional(),
26
27
  wait: z.boolean().optional(),
27
28
  yes: z.boolean().optional(),
28
29
  volume: z.string().optional(),
@@ -139,11 +140,12 @@ export const launchCapability = cliOnly({
139
140
  category: "journey",
140
141
  order: 2,
141
142
  outcome: "Launch one reviewed VM, Bare Metal machine, Kubernetes cluster, or Slurm cluster from active reserved capacity, including a declared durable Bare Metal workspace.",
142
- human: "ornn launch [<capacity-reservation>] --mode vm|bare-metal|kubernetes|slurm [--key <ssh-key>]; Bare Metal may add --network shared --volume <managed-volume> --mount-access read-only|read-write; Kubernetes/Slurm prompt for missing --network public|private and --nodes <membership-count>",
143
- automation: "ornn launch <canonical-reservation-id> --mode vm|bare-metal|kubernetes|slurm --organization <id> --yes --idempotency-key <key> --format json; VM/Bare Metal/Slurm require --key <canonical-key-id>; Bare Metal may add --network shared --volume <canonical-volume-id> --mount-access read-only|read-write; Kubernetes/Slurm require --network public|private --nodes <membership-count> --machines <canonical-machine-ids>",
143
+ human: "ornn launch [<capacity-reservation>] --mode vm|bare-metal|kubernetes|slurm [--key <ssh-key>]; Bare Metal may add --network shared --volume <managed-volume> --mount-access read-only|read-write; Kubernetes/Slurm prompt for missing --network public|private and --nodes <membership-count>, and may add running VMs with --vms <vm-ids>",
144
+ automation: "ornn launch <canonical-reservation-id> --mode vm|bare-metal|kubernetes|slurm --organization <id> --yes --idempotency-key <key> --format json; VM/Bare Metal/Slurm require --key <canonical-key-id>; Bare Metal may add --network shared --volume <canonical-volume-id> --mount-access read-only|read-write; Kubernetes/Slurm require --network public|private --nodes <membership-count> --machines <canonical-machine-ids>, and may add --vms <canonical-vm-ids> (with --nodes 0 for a cluster of VMs only)",
144
145
  acceptedReferences: [
145
146
  "canonical capacity-reservation ID",
146
147
  "canonical eligible machine IDs",
148
+ "canonical running VM IDs for cluster membership",
147
149
  "invocation-local eligible reservation and machine choices",
148
150
  "exact SSH-key and managed-volume references; organization network shared",
149
151
  ],