From 073fc109b99b8914a1bca69944862cfda00322da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robin=20Br=C3=A4mer?= <22003767+robinbraemer@users.noreply.github.com> Date: Fri, 11 Sep 2026 13:38:33 +0200 Subject: [PATCH] fix(cli): adopt required product-quality API contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rationale: The checked-in public OpenAPI snapshot predates CNAP b45c0af’s operations.list Akua-Context header and the explicit system-managed machine-selection contract. Regenerating from those two producer-backed corrections makes the supported CLI accept and forward the workspace context and permit omitted compute_config_id. Rejected: Hand-editing the generated executor would bypass the sole-source OpenAPI contract and fail deterministic generation checks. Fetching the complete latest public snapshot would pull unrelated product evolution into this targeted launch-blocker repair. Risk: This updates only the two snapshot fragments proven against b45c0af; a future full snapshot adoption remains independently auditable. Tested: bun run test -- test/generated-command.test.ts; mise run generate:check; mise run check; git diff --check. --- openapi/public.json | 19 +++++++++++-- src/generated/commands.gen.ts | 8 ++++-- src/generated/openapi-api.gen.ts | 12 ++++---- .../public-operation-executor.gen.ts | 8 ++++-- test/generated-command.test.ts | 28 +++++++++++++++++-- 5 files changed, 60 insertions(+), 15 deletions(-) diff --git a/openapi/public.json b/openapi/public.json index 1e9e4da..3e67ff2 100644 --- a/openapi/public.json +++ b/openapi/public.json @@ -33468,7 +33468,8 @@ "compute_config_id": { "type": "string", "minLength": 1, - "maxLength": 54 + "maxLength": 54, + "description": "Compute config that supplies the provider account for this machine. Omit it to explicitly request an Akua system-managed machine on platform infrastructure; free-tier instance types and machine quotas then apply." }, "name": { "type": "string", @@ -33481,8 +33482,7 @@ }, "required": [ "cluster_id", - "instance_type", - "compute_config_id" + "instance_type" ], "additionalProperties": false } @@ -37341,6 +37341,19 @@ "description": "Required when `owner_type` is set. Ignored otherwise.", "name": "owner_id", "in": "query" + }, + { + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 53, + "description": "Optional workspace/scope context for the request. Carries a single workspace wire id (`ws_…`) today. Only needed for broad tokens — a workspace-owned token implies its workspace.", + "example": "ws_j572abc123def456" + }, + "required": false, + "description": "Optional workspace/scope context for the request. Carries a single workspace wire id (`ws_…`) today. Only needed for broad tokens — a workspace-owned token implies its workspace.", + "name": "akua-context", + "in": "header" } ], "responses": { diff --git a/src/generated/commands.gen.ts b/src/generated/commands.gen.ts index 0caa0f2..fea4005 100644 --- a/src/generated/commands.gen.ts +++ b/src/generated/commands.gen.ts @@ -2947,8 +2947,7 @@ export const commandRegistry: readonly CommandDefinition[] = "required": false, "example": { "cluster_id": "", - "instance_type": "", - "compute_config_id": "" + "instance_type": "" } } }, @@ -3658,6 +3657,11 @@ export const commandRegistry: readonly CommandDefinition[] = "name": "owner_id", "in": "query", "required": false + }, + { + "name": "akua-context", + "in": "header", + "required": false } ] }, diff --git a/src/generated/openapi-api.gen.ts b/src/generated/openapi-api.gen.ts index 0ea3f74..a4083d0 100644 --- a/src/generated/openapi-api.gen.ts +++ b/src/generated/openapi-api.gen.ts @@ -938,8 +938,8 @@ export type MachinesCreateParams = { readonly "akua-context"?: string, readonly export const MachinesCreateParams = Schema.Struct({ "akua-context": Schema.optionalKey(Schema.String.annotate({ "description": "Optional workspace/scope context for the request. Carries a single workspace wire id (`ws_…`) today. Only needed for broad tokens — a workspace-owned token implies its workspace." }).check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" })).check(Schema.isMaxLength(53).annotate({ "expected": "a value with a length of at most 53" }))), "idempotency-key": Schema.optionalKey(Schema.String.annotate({ "description": "Optional caller-supplied idempotency key. Any non-empty key up to 64 characters is accepted. Endpoint-specific documentation describes how the key is used." }).check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" })).check(Schema.isMaxLength(64).annotate({ "expected": "a value with a length of at most 64" }))) }) export type MachinesCreateHeaders = { readonly "akua-context"?: string, readonly "idempotency-key"?: string } export const MachinesCreateHeaders = Schema.Struct({ "akua-context": Schema.optionalKey(Schema.String.annotate({ "description": "Optional workspace/scope context for the request. Carries a single workspace wire id (`ws_…`) today. Only needed for broad tokens — a workspace-owned token implies its workspace." }).check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" })).check(Schema.isMaxLength(53).annotate({ "expected": "a value with a length of at most 53" }))), "idempotency-key": Schema.optionalKey(Schema.String.annotate({ "description": "Optional caller-supplied idempotency key. Any non-empty key up to 64 characters is accepted. Endpoint-specific documentation describes how the key is used." }).check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" })).check(Schema.isMaxLength(64).annotate({ "expected": "a value with a length of at most 64" }))) }) -export type MachinesCreateRequestJson = { readonly "cluster_id": string, readonly "instance_type": string, readonly "compute_config_id": string, readonly "name"?: string, readonly "node_claim"?: { readonly [x: string]: Schema.Json } } -export const MachinesCreateRequestJson = Schema.Struct({ "cluster_id": Schema.String.check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" })).check(Schema.isMaxLength(54).annotate({ "expected": "a value with a length of at most 54" })), "instance_type": Schema.String.check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" })), "compute_config_id": Schema.String.check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" })).check(Schema.isMaxLength(54).annotate({ "expected": "a value with a length of at most 54" })), "name": Schema.optionalKey(Schema.String.check(Schema.isMaxLength(120).annotate({ "expected": "a value with a length of at most 120" }))), "node_claim": Schema.optionalKey(Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))) }) +export type MachinesCreateRequestJson = { readonly "cluster_id": string, readonly "instance_type": string, readonly "compute_config_id"?: string, readonly "name"?: string, readonly "node_claim"?: { readonly [x: string]: Schema.Json } } +export const MachinesCreateRequestJson = Schema.Struct({ "cluster_id": Schema.String.check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" })).check(Schema.isMaxLength(54).annotate({ "expected": "a value with a length of at most 54" })), "instance_type": Schema.String.check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" })), "compute_config_id": Schema.optionalKey(Schema.String.annotate({ "description": "Compute config that supplies the provider account for this machine. Omit it to explicitly request an Akua system-managed machine on platform infrastructure; free-tier instance types and machine quotas then apply." }).check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" })).check(Schema.isMaxLength(54).annotate({ "expected": "a value with a length of at most 54" }))), "name": Schema.optionalKey(Schema.String.check(Schema.isMaxLength(120).annotate({ "expected": "a value with a length of at most 120" }))), "node_claim": Schema.optionalKey(Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))) }) export type MachinesCreate202 = MachineOperation export const MachinesCreate202 = MachineOperation export type MachinesCreate401 = ApiErrorResponse @@ -1460,10 +1460,12 @@ export type EntitlementsList401 = ApiErrorResponse export const EntitlementsList401 = ApiErrorResponse export type EntitlementsList403 = ApiErrorResponse export const EntitlementsList403 = ApiErrorResponse -export type OperationsListParams = { readonly "cursor"?: string, readonly "limit"?: number, readonly "owner_type"?: "install" | "repository_change_request" | "repository" | "workspace" | "machine" | "cluster" | "package", readonly "owner_id"?: string } -export const OperationsListParams = Schema.Struct({ "cursor": Schema.optionalKey(Schema.String.annotate({ "description": "Pagination cursor from a previous response's `next_cursor`." })), "limit": Schema.optionalKey(Schema.Number.annotate({ "description": "Items per page (1-100, default 50)", "default": 50 }).check(Schema.isInt().annotate({ "expected": "an integer" })).check(Schema.isGreaterThanOrEqualTo(1).annotate({ "expected": "a value greater than or equal to 1" })).check(Schema.isLessThanOrEqualTo(100).annotate({ "expected": "a value less than or equal to 100" }))), "owner_type": Schema.optionalKey(Schema.Literals(["install", "repository_change_request", "repository", "workspace", "machine", "cluster", "package"])), "owner_id": Schema.optionalKey(Schema.String.annotate({ "description": "Required when `owner_type` is set. Ignored otherwise." })) }) +export type OperationsListParams = { readonly "cursor"?: string, readonly "limit"?: number, readonly "owner_type"?: "install" | "repository_change_request" | "repository" | "workspace" | "machine" | "cluster" | "package", readonly "owner_id"?: string, readonly "akua-context"?: string } +export const OperationsListParams = Schema.Struct({ "cursor": Schema.optionalKey(Schema.String.annotate({ "description": "Pagination cursor from a previous response's `next_cursor`." })), "limit": Schema.optionalKey(Schema.Number.annotate({ "description": "Items per page (1-100, default 50)", "default": 50 }).check(Schema.isInt().annotate({ "expected": "an integer" })).check(Schema.isGreaterThanOrEqualTo(1).annotate({ "expected": "a value greater than or equal to 1" })).check(Schema.isLessThanOrEqualTo(100).annotate({ "expected": "a value less than or equal to 100" }))), "owner_type": Schema.optionalKey(Schema.Literals(["install", "repository_change_request", "repository", "workspace", "machine", "cluster", "package"])), "owner_id": Schema.optionalKey(Schema.String.annotate({ "description": "Required when `owner_type` is set. Ignored otherwise." })), "akua-context": Schema.optionalKey(Schema.String.annotate({ "description": "Optional workspace/scope context for the request. Carries a single workspace wire id (`ws_…`) today. Only needed for broad tokens — a workspace-owned token implies its workspace." }).check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" })).check(Schema.isMaxLength(53).annotate({ "expected": "a value with a length of at most 53" }))) }) export type OperationsListQuery = { readonly "cursor"?: string, readonly "limit"?: number, readonly "owner_type"?: "install" | "repository_change_request" | "repository" | "workspace" | "machine" | "cluster" | "package", readonly "owner_id"?: string } export const OperationsListQuery = Schema.Struct({ "cursor": Schema.optionalKey(Schema.String.annotate({ "description": "Pagination cursor from a previous response's `next_cursor`." })), "limit": Schema.optionalKey(Schema.Number.annotate({ "description": "Items per page (1-100, default 50)", "default": 50 }).check(Schema.isInt().annotate({ "expected": "an integer" })).check(Schema.isGreaterThanOrEqualTo(1).annotate({ "expected": "a value greater than or equal to 1" })).check(Schema.isLessThanOrEqualTo(100).annotate({ "expected": "a value less than or equal to 100" }))), "owner_type": Schema.optionalKey(Schema.Literals(["install", "repository_change_request", "repository", "workspace", "machine", "cluster", "package"])), "owner_id": Schema.optionalKey(Schema.String.annotate({ "description": "Required when `owner_type` is set. Ignored otherwise." })) }) +export type OperationsListHeaders = { readonly "akua-context"?: string } +export const OperationsListHeaders = Schema.Struct({ "akua-context": Schema.optionalKey(Schema.String.annotate({ "description": "Optional workspace/scope context for the request. Carries a single workspace wire id (`ws_…`) today. Only needed for broad tokens — a workspace-owned token implies its workspace." }).check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" })).check(Schema.isMaxLength(53).annotate({ "expected": "a value with a length of at most 53" }))) }) export type OperationsList200 = OperationList export const OperationsList200 = OperationList export type OperationsList400 = ApiErrorResponse @@ -4179,7 +4181,7 @@ class EntitlementsGroup extends HttpApiGroup.make("Entitlements") .annotate(OpenApi.Description, "Effective workspace capabilities and limits.") {} class OperationsGroup extends HttpApiGroup.make("Operations") - .add(HttpApiEndpoint.get("operationsList", "/operations", { query: OperationsListQuery, success: OperationsList200, error: [OperationsList400.pipe(HttpApiSchema.status(400)), OperationsList401.pipe(HttpApiSchema.status(401)), OperationsList403.pipe(HttpApiSchema.status(403))] }) + .add(HttpApiEndpoint.get("operationsList", "/operations", { query: OperationsListQuery, headers: OperationsListHeaders, success: OperationsList200, error: [OperationsList400.pipe(HttpApiSchema.status(400)), OperationsList401.pipe(HttpApiSchema.status(401)), OperationsList403.pipe(HttpApiSchema.status(403))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "operations.list") .annotate(OpenApi.Summary, "List operations") diff --git a/src/generated/public-operation-executor.gen.ts b/src/generated/public-operation-executor.gen.ts index d0616a8..e7afa3d 100644 --- a/src/generated/public-operation-executor.gen.ts +++ b/src/generated/public-operation-executor.gen.ts @@ -2683,14 +2683,18 @@ function executeOperation( case "operations.list": return Effect.gen(function* () { const input = yield* Schema.decodeUnknownEffect( - Schema.Struct({ query: Schema.optionalKey(Schema.Unknown) }), + Schema.Struct({ query: Schema.optionalKey(Schema.Unknown), headers: Schema.optionalKey(Schema.Unknown) }), strictParseOptions, )(rawInput); const query = yield* atEnvelopeKey("query", Schema.decodeUnknownEffect( Api.OperationsListQuery, strictParseOptions, )(input.query ?? {})); - const value = yield* executeClientOperation(client, client.client["Operations"]["operationsList"]({ query })); + const headers = yield* atEnvelopeKey("headers", Schema.decodeUnknownEffect( + Api.OperationsListHeaders, + strictParseOptions, + )(input.headers ?? {})); + const value = yield* executeClientOperation(client, client.client["Operations"]["operationsList"]({ query, headers })); return mode === "raw" ? value : { _tag: "Value", value }; }); case "operations.wait": diff --git a/test/generated-command.test.ts b/test/generated-command.test.ts index 1b6afc4..bc580c7 100644 --- a/test/generated-command.test.ts +++ b/test/generated-command.test.ts @@ -107,12 +107,11 @@ describe("generated public commands", () => { expect(received?.headers.get("authorization")).toBe("Bearer test-token"); }); - test("machines.create sends exact headers and JSON body and decodes 202", async () => { + test("machines.create permits an omitted optional compute config and decodes 202", async () => { let received: Request | undefined; const body = { cluster_id: "clu_123", instance_type: "cx23", - compute_config_id: "ccfg_123", }; const operation = machineOperation(); const result = await runGenerated( @@ -141,6 +140,29 @@ describe("generated public commands", () => { expect(await received?.json()).toEqual(body); }); + test("operations.list forwards the workspace context header", async () => { + let received: Request | undefined; + const result = await runGenerated( + "operations.list", + ["--input", "-"], + JSON.stringify({ headers: { "akua-context": "ws_123" } }), + (input, init) => { + received = new Request(input, init); + return Promise.resolve( + Response.json({ data: [], has_more: false, next_cursor: null }), + ); + }, + ); + + expect(result.data).toEqual({ + data: [], + has_more: false, + next_cursor: null, + }); + expect(received?.url).toBe("https://api.akua.dev/v1/operations"); + expect(received?.headers.get("akua-context")).toBe("ws_123"); + }); + test("clusters.resume builds the literal :action-suffixed request path", async () => { let received: Request | undefined; const operation = clusterOperation(); @@ -537,7 +559,7 @@ describe("generated public commands", () => { { path: ["body", "secret"], message: "Expected no excess property" }, ], inputExample: - '{"body":{"cluster_id":"","instance_type":"","compute_config_id":""}}', + '{"body":{"cluster_id":"","instance_type":""}}', }); });