From 6e43224ed500208b2ebb8cd63afeb1d09ad55a75 Mon Sep 17 00:00:00 2001 From: Akua Ship Crewmate Date: Fri, 24 Jul 2026 01:33:07 +0000 Subject: [PATCH 1/7] Add bounded capacity command overlay --- src/commands/capacity.ts | 152 +++++++++++++++++++++++++++++++ src/runtime/public-api-client.ts | 56 ++++++++++++ 2 files changed, 208 insertions(+) create mode 100644 src/commands/capacity.ts create mode 100644 src/runtime/public-api-client.ts diff --git a/src/commands/capacity.ts b/src/commands/capacity.ts new file mode 100644 index 0000000..1d2a472 --- /dev/null +++ b/src/commands/capacity.ts @@ -0,0 +1,152 @@ +import { usageError } from "../runtime/errors"; +import { PublicApiClient, type ApiFetch } from "../runtime/public-api-client"; +import type { RenderEnvelope } from "../runtime/render"; + +export interface CapacityDependencies { + readToken(env: Record): Promise; + fetch: ApiFetch; +} + +interface ParsedCommand { + command: string; + path: string; + workspace?: string; +} + +export async function capacityView( + argv: readonly string[], + env: Record, + dependencies: CapacityDependencies, +): Promise { + const parsed = parseCommand(argv); + const token = await dependencies.readToken(env); + const data = await new PublicApiClient(token, dependencies.fetch).get(parsed.path, parsed.workspace); + return { command: parsed.command, data }; +} + +function parseCommand(argv: readonly string[]): ParsedCommand { + const operation = `${argv[0] ?? ""} ${argv[1] ?? ""}`; + const flags = parseFlags(argv.slice(2)); + + if (operation === "clusters get") { + allowOnly(flags, ["--id", "--workspace"]); + const id = requiredCanonicalId(flags, "--id", "clu"); + const workspace = optionalCanonicalId(flags, "--workspace", "ws"); + return { command: "akua clusters get", path: `/v1/clusters/${id}`, workspace }; + } + + if (operation === "compute-configs list") { + allowOnly(flags, ["--view", "--workspace"]); + requireExactValue(flags, "--view", "full"); + const workspace = optionalCanonicalId(flags, "--workspace", "ws"); + return { command: "akua compute-configs list", path: "/v1/compute_configs?view=full", workspace }; + } + + if (operation === "compute list-instance-types") { + allowOnly(flags, ["--config"]); + const config = requiredCanonicalId(flags, "--config", "cfg"); + return { + command: "akua compute list-instance-types", + path: `/v1/compute/instance_types?config=${config}`, + }; + } + + if (operation === "machines list") { + allowOnly(flags, ["--cluster-id", "--view", "--workspace"]); + const cluster = requiredCanonicalId(flags, "--cluster-id", "clu"); + requireExactValue(flags, "--view", "full"); + const workspace = optionalCanonicalId(flags, "--workspace", "ws"); + return { + command: "akua machines list", + path: `/v1/machines?cluster_id=${cluster}&view=full`, + workspace, + }; + } + + if (operation === "machines create") { + allowOnly(flags, ["--cluster-id", "--compute-config-id", "--instance-type", "--idempotency-key", "--yes"]); + requiredCanonicalId(flags, "--cluster-id", "clu"); + requiredCanonicalId(flags, "--compute-config-id", "cfg"); + requiredValue(flags, "--instance-type"); + requiredValue(flags, "--idempotency-key"); + if (flags.get("--yes") !== true) { + throw usageError("machines create requires explicit --yes confirmation."); + } + throw usageError("machines create execution is not available in this capacity overlay."); + } + + throw usageError("Unknown capacity command."); +} + +function parseFlags(argv: readonly string[]): Map { + const flags = new Map(); + for (let index = 0; index < argv.length; index += 1) { + const raw = argv[index]; + if (!raw.startsWith("--")) { + throw usageError("Unexpected capacity command argument."); + } + const equals = raw.indexOf("="); + const name = equals === -1 ? raw : raw.slice(0, equals); + if (flags.has(name)) { + throw usageError(`Flag ${name} may be specified only once.`); + } + if (name === "--yes") { + if (equals !== -1) { + throw usageError("--yes does not accept a value."); + } + flags.set(name, true); + continue; + } + const value = equals === -1 ? argv[index + 1] : raw.slice(equals + 1); + if (value === undefined || value === "" || value.startsWith("--")) { + throw usageError(`Missing value for ${name}.`); + } + flags.set(name, value); + if (equals === -1) { + index += 1; + } + } + return flags; +} + +function allowOnly(flags: ReadonlyMap, allowed: readonly string[]): void { + for (const name of flags.keys()) { + if (!allowed.includes(name)) { + throw usageError(`Unknown flag: ${name}`); + } + } +} + +function requiredCanonicalId(flags: ReadonlyMap, name: string, prefix: string): string { + const value = requiredValue(flags, name); + if (!new RegExp(`^${prefix}_[a-z0-9]{16}$`).test(value)) { + throw usageError(`${name} must be a canonical ${prefix}_ ID.`); + } + return value; +} + +function optionalCanonicalId( + flags: ReadonlyMap, + name: string, + prefix: string, +): string | undefined { + if (!flags.has(name)) { + return undefined; + } + return requiredCanonicalId(flags, name, prefix); +} + +function requiredValue(flags: ReadonlyMap, name: string): string { + const value = flags.get(name); + if (typeof value !== "string" || value === "") { + throw usageError(`Missing required ${name} flag.`); + } + return value; +} + +function requireExactValue(flags: ReadonlyMap, name: string, expected: string): void { + const value = requiredValue(flags, name); + if (value !== expected) { + throw usageError(`${name} must be ${expected}.`); + } +} diff --git a/src/runtime/public-api-client.ts b/src/runtime/public-api-client.ts new file mode 100644 index 0000000..c4178e1 --- /dev/null +++ b/src/runtime/public-api-client.ts @@ -0,0 +1,56 @@ +import { AkuaCliError } from "./errors"; + +const PublicApiBase = "https://api.akua.dev"; + +export type ApiFetch = (input: string | URL | Request, init?: RequestInit) => Promise; + +export class PublicApiClient { + constructor( + private readonly token: string, + private readonly apiFetch: ApiFetch = fetch, + ) {} + + async get(path: string, workspace?: string): Promise { + const headers: Record = { authorization: `Bearer ${this.token}` }; + if (workspace !== undefined) { + headers["akua-context"] = workspace; + } + + let response: Response; + try { + response = await this.apiFetch(`${PublicApiBase}${path}`, { method: "GET", headers }); + } catch { + throw new AkuaCliError({ + type: "transport_error", + code: "AKUA_PUBLIC_API_TRANSPORT_ERROR", + message: "The Akua API request could not be completed.", + exitCode: 1, + }); + } + + let body: unknown; + try { + body = await response.json(); + } catch { + throw new AkuaCliError({ + type: "invalid_response", + code: "AKUA_PUBLIC_API_INVALID_RESPONSE", + message: "The Akua API returned an invalid JSON response.", + status: response.status, + exitCode: 1, + }); + } + + if (!response.ok) { + throw new AkuaCliError({ + type: "api_error", + code: "AKUA_PUBLIC_API_ERROR", + message: "The Akua API rejected the request.", + status: response.status, + requestId: response.headers.get("x-request-id") ?? undefined, + retryAfter: response.headers.get("retry-after"), + }); + } + return body; + } +} From 620faad297520ed6ae28b873e1929679fc2e4b2d Mon Sep 17 00:00:00 2001 From: Akua Ship Crewmate Date: Fri, 24 Jul 2026 01:37:34 +0000 Subject: [PATCH 2/7] Route bounded capacity command overlays --- src/bin/akua.ts | 21 +++++ src/commands/auth.ts | 17 ++++ src/commands/capacity.ts | 8 +- test/capacity-commands.test.ts | 153 +++++++++++++++++++++++++++++++++ 4 files changed, 198 insertions(+), 1 deletion(-) create mode 100644 test/capacity-commands.test.ts diff --git a/src/bin/akua.ts b/src/bin/akua.ts index faa2931..2c37400 100644 --- a/src/bin/akua.ts +++ b/src/bin/akua.ts @@ -1,6 +1,7 @@ #!/usr/bin/env bun import { authView } from "../commands/auth"; import { agentOsView } from "../commands/agent-os"; +import { capacityView } from "../commands/capacity"; import { buildHomeView } from "../commands/home"; import { commandRegistry } from "../generated/commands.gen"; import { AkuaCliError, commandNotImplemented, usageError } from "../runtime/errors"; @@ -52,6 +53,10 @@ async function route(argv: readonly string[], env: Record arg.startsWith("-")); if (unknownFlag) { throw usageError(`Unknown flag: ${flagName(unknownFlag)}`); @@ -101,6 +106,11 @@ function helpView(): RenderEnvelope { " akua auth status Show local authentication status", " akua auth logout Remove the saved local API token", " akua agent-os load-hcloud-provider --workspace --token-file [--expected-ssh-key-fingerprint [--expected-ssh-key-name ]]", + " akua clusters get --id [--workspace ]", + " akua compute-configs list --view full [--workspace ]", + " akua compute list-instance-types --config ", + " akua machines list --cluster-id --view full [--workspace ]", + " akua machines create ... --yes Validate the bounded create command (submission unavailable)", " akua commands List generated public OpenAPI command registry", " akua --help Show help", " akua --version Show version", @@ -112,6 +122,17 @@ function helpView(): RenderEnvelope { }; } +function isCapacityOverlay(argv: readonly string[]): boolean { + const command = `${argv[0] ?? ""} ${argv[1] ?? ""}`; + return ( + command === "clusters get" || + command === "compute-configs list" || + command === "compute list-instance-types" || + command === "machines list" || + command === "machines create" + ); +} + function stripGlobalFlags(argv: readonly string[]): string[] { const stripped: string[] = []; for (let index = 0; index < argv.length; index += 1) { diff --git a/src/commands/auth.ts b/src/commands/auth.ts index 18a78be..a3782d4 100644 --- a/src/commands/auth.ts +++ b/src/commands/auth.ts @@ -63,6 +63,23 @@ export async function readProtectedCallerToken(env: Record): Promise { + if (hasEnvToken(env)) { + return env.AKUA_API_TOKEN as string; + } + + const token = (await readConfig(resolveConfigPath(env))).token; + if (typeof token !== "string" || token === "") { + throw new AkuaCliError({ + type: "authentication_error", + code: "AKUA_PUBLIC_API_AUTH_REQUIRED", + message: "An Akua API token is required.", + exitCode: 3, + }); + } + return token; +} + async function loginView(argv: readonly string[], env: Record): Promise { const token = parseLoginFlags(argv); const configPath = resolveConfigPath(env); diff --git a/src/commands/capacity.ts b/src/commands/capacity.ts index 1d2a472..c777737 100644 --- a/src/commands/capacity.ts +++ b/src/commands/capacity.ts @@ -1,12 +1,18 @@ import { usageError } from "../runtime/errors"; import { PublicApiClient, type ApiFetch } from "../runtime/public-api-client"; import type { RenderEnvelope } from "../runtime/render"; +import { readPublicApiToken } from "./auth"; export interface CapacityDependencies { readToken(env: Record): Promise; fetch: ApiFetch; } +const productionDependencies: CapacityDependencies = { + readToken: readPublicApiToken, + fetch, +}; + interface ParsedCommand { command: string; path: string; @@ -16,7 +22,7 @@ interface ParsedCommand { export async function capacityView( argv: readonly string[], env: Record, - dependencies: CapacityDependencies, + dependencies: CapacityDependencies = productionDependencies, ): Promise { const parsed = parseCommand(argv); const token = await dependencies.readToken(env); diff --git a/test/capacity-commands.test.ts b/test/capacity-commands.test.ts new file mode 100644 index 0000000..c8f8ad2 --- /dev/null +++ b/test/capacity-commands.test.ts @@ -0,0 +1,153 @@ +import { describe, expect, test } from "bun:test"; +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { join } from "node:path"; + +import { readPublicApiToken } from "../src/commands/auth"; +import { capacityView, type CapacityDependencies } from "../src/commands/capacity"; +import { PublicApiClient, type ApiFetch } from "../src/runtime/public-api-client"; + +const TOKEN = "sentinel-public-api-token"; +const CLUSTER = "clu_j572abc123def456"; +const WORKSPACE = "ws_j572abc123def456"; +const CONFIG = "cfg_j572abc123def456"; + +describe("capacity command overlays", () => { + test("clusters get binds the canonical path and exact workspace header", async () => { + const fixture = apiFixture({ + id: CLUSTER, + html_url: `https://akua.dev/clusters/${CLUSTER}`, + name: "production", + workspace_id: WORKSPACE, + state: "active", + provider: "managed_kaas", + reconciling: false, + created_at: 1, + updated_at: 2, + etag: "3", + }); + const result = await capacityView(["clusters", "get", "--id", CLUSTER, "--workspace", WORKSPACE], {}, deps(fixture.fetch)); + + expect(result.data).toMatchObject({ id: CLUSTER, workspace_id: WORKSPACE }); + expect(fixture.requests).toHaveLength(1); + expect(fixture.requests[0]).toMatchObject({ + url: `https://api.akua.dev/v1/clusters/${CLUSTER}`, + method: "GET", + headers: { authorization: `Bearer ${TOKEN}`, "akua-context": WORKSPACE }, + }); + expect(JSON.stringify(result)).not.toContain(TOKEN); + }); + + test("compute config and machine lists force full view and preserve ownership", async () => { + const configs = apiFixture({ + data: [{ id: CONFIG, name: "hcloud-fsn1", provider: "hcloud", provider_config: { provider: "hcloud", region: "fsn1", image: "ubuntu", machine_type_filter: null }, secret_id: null, created_at: 1 }], + has_more: false, + next_cursor: null, + }); + const configResult = await capacityView(["compute-configs", "list", "--view", "full", "--workspace", WORKSPACE], {}, deps(configs.fetch)); + expect(configResult.data).toMatchObject({ data: [{ id: CONFIG }] }); + expect(configs.requests[0].url).toBe("https://api.akua.dev/v1/compute_configs?view=full"); + + const machines = apiFixture({ data: [], has_more: false, next_cursor: null }); + await capacityView(["machines", "list", "--cluster-id", CLUSTER, "--view", "full", "--workspace", WORKSPACE], {}, deps(machines.fetch)); + expect(machines.requests[0].url).toBe(`https://api.akua.dev/v1/machines?cluster_id=${CLUSTER}&view=full`); + }); + + test("instance types require an explicit config and return full comparison fields", async () => { + const fixture = apiFixture([{ name: "cpx31", arch: "amd64", cpu: 4, memory_mib: 8192, storage_mib: 163840, price_per_hour: 0.0208, available: true, zone: "fsn1", capacity_type: "on-demand" }]); + const result = await capacityView(["compute", "list-instance-types", "--config", CONFIG], {}, deps(fixture.fetch)); + expect(result.data).toEqual([{ name: "cpx31", arch: "amd64", cpu: 4, memory_mib: 8192, storage_mib: 163840, price_per_hour: 0.0208, available: true, zone: "fsn1", capacity_type: "on-demand" }]); + expect(fixture.requests[0].url).toBe(`https://api.akua.dev/v1/compute/instance_types?config=${CONFIG}`); + }); + + test("invalid IDs, unknown flags, and create without yes fail before auth or fetch", async () => { + let authCalls = 0; + let fetchCalls = 0; + const dependencies: CapacityDependencies = { + readToken: async () => { authCalls += 1; return TOKEN; }, + fetch: async () => { fetchCalls += 1; throw new Error("must not fetch"); }, + }; + for (const argv of [ + ["clusters", "get", "--id", "not-a-cluster"], + ["machines", "list", "--cluster-id", CLUSTER, "--bogus"], + ["machines", "create", "--cluster-id", CLUSTER, "--compute-config-id", CONFIG, "--instance-type", "cpx31", "--idempotency-key", "captain-key"], + ]) { + await expect(capacityView(argv, {}, dependencies)).rejects.toMatchObject({ exitCode: 2 }); + } + expect(authCalls).toBe(0); + expect(fetchCalls).toBe(0); + }); + + test("public auth prefers the ephemeral environment token over protected config", async () => { + const home = await mkdtemp(join(process.cwd(), ".tmp-capacity-auth-")); + try { + const configDir = join(home, ".config", "akua"); + await mkdir(configDir, { recursive: true, mode: 0o700 }); + await writeFile(join(configDir, "config.json"), JSON.stringify({ token: "stored-token" }), { mode: 0o600 }); + + await expect(readPublicApiToken({ HOME: home })).resolves.toBe("stored-token"); + await expect(readPublicApiToken({ HOME: home, AKUA_API_TOKEN: TOKEN })).resolves.toBe(TOKEN); + } finally { + await rm(home, { recursive: true, force: true }); + } + }); + + test("transport failures and CLI validation never expose the token sentinel", async () => { + const failingFetch: ApiFetch = async () => { + throw new Error(`transport included ${TOKEN}`); + }; + const error = await capacityView(["clusters", "get", "--id", CLUSTER], {}, deps(failingFetch)).catch((value) => value); + expect(String(error)).not.toContain(TOKEN); + + const cli = await runAkua(["clusters", "get", "--id", "not-a-cluster", "--json"], { + AKUA_API_TOKEN: TOKEN, + }); + expect(cli.exitCode).toBe(2); + expect(cli.stdout).toContain("canonical clu_ ID"); + expect(`${cli.stdout}${cli.stderr}`).not.toContain(TOKEN); + expect(cli.stdout).not.toContain("AKUA_COMMAND_NOT_IMPLEMENTED"); + }); + + test("CLI help distinguishes the executable capacity overlays", async () => { + const cli = await runAkua(["--help", "--json"]); + expect(cli.exitCode).toBe(0); + expect(cli.stdout).toContain("akua clusters get"); + expect(cli.stdout).toContain("akua compute-configs list --view full"); + expect(cli.stdout).toContain("akua compute list-instance-types"); + expect(cli.stdout).toContain("akua machines list"); + }); +}); + +interface CapturedRequest { url: string; method: string; headers: Record; body?: string } + +function apiFixture(body: unknown, status = 200) { + const requests: CapturedRequest[] = []; + const fetch: ApiFetch = async (input, init) => { + requests.push({ url: String(input), method: init?.method ?? "GET", headers: Object.fromEntries(new Headers(init?.headers).entries()), body: typeof init?.body === "string" ? init.body : undefined }); + return new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } }); + }; + return { fetch, requests }; +} + +function deps(fetch: ApiFetch): CapacityDependencies { + return { readToken: async () => TOKEN, fetch }; +} + +async function runAkua(args: readonly string[], env: Record = {}) { + const childEnv = { ...process.env, ...env }; + delete childEnv.AKUA_OUTPUT; + if (!("AKUA_API_TOKEN" in env)) { + delete childEnv.AKUA_API_TOKEN; + } + const proc = Bun.spawn({ + cmd: ["bun", "src/bin/akua.ts", ...args], + stdout: "pipe", + stderr: "pipe", + env: childEnv, + }); + const [stdout, stderr, exitCode] = await Promise.all([ + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + proc.exited, + ]); + return { stdout, stderr, exitCode }; +} From f2787b9c4ad885b90e22bc307465ddfa688e3a23 Mon Sep 17 00:00:00 2001 From: Akua Ship Crewmate Date: Fri, 24 Jul 2026 01:41:50 +0000 Subject: [PATCH 3/7] Document bounded capacity overlays --- README.md | 19 +++++++- docs/architecture.md | 19 +++++--- test/capacity-openapi.test.ts | 82 +++++++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 8 deletions(-) create mode 100644 test/capacity-openapi.test.ts diff --git a/README.md b/README.md index faa3626..e841a0c 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Bun/TypeScript executable for humans, automation, and coding agents. The current MVP implements local token authentication, adaptive structured output, and discovery of the public operationId-driven command registry. Generated API -commands are discoverable but not yet executable unless `akua --help` says so. +commands are discovery-only unless listed as executable by `akua --help`. The canonical executable is `akua`; there is no `cnap` compatibility binary. @@ -139,6 +139,23 @@ akua auth status akua auth logout ``` +## Executable capacity overlays + +Four reviewed public API reads are executable in addition to the discovery-only +generated registry: + +```sh +akua clusters get --id [--workspace ] +akua compute-configs list --view full [--workspace ] +akua compute list-instance-types --config +akua machines list --cluster-id --view full [--workspace ] +``` + +They use the fixed production API, accept only canonical IDs, and authenticate +with `AKUA_API_TOKEN` before the protected local config. `machines create` and +all other generated mutations remain unavailable; registry discovery does not +imply execution support. + `AKUA_API_TOKEN` takes precedence over a stored token. Login writes `~/.config/akua/config.json`; the directory is forced to `0700` and the file to `0600`. Login replaces only `token` and preserves unknown config keys. Logout diff --git a/docs/architecture.md b/docs/architecture.md index dde632b..06ceaa5 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,6 +1,6 @@ # Akua Cloud CLI Architecture -Status: greenfield scaffold with local auth/config MVP. +Status: local auth/config MVP with bounded executable capacity overlays. ## Decisions And Non-Goals @@ -128,12 +128,13 @@ segment becomes the action, and single-segment operationIds fall back to the HTTP method as the action. The generator assumes OpenAPI operationIds are unique; it does not currently enforce uniqueness itself. -The generator deliberately produces a registry, not hand-written API coverage. -Execution is stubbed until the API client and request/body binding layer lands. -The next implementation step should add a small CLI overlay file for exceptions -that cannot be inferred safely from OpenAPI alone, such as preferred aliases, -default list fields, destructive-command confirmation labels, and resource- -specific next steps. +The generator deliberately produces a discovery registry, not generic API +execution. A small hand-written capacity overlay executes only reviewed +contracts: `clusters get`, `compute-configs list --view full`, +`compute list-instance-types --config `, and +`machines list --cluster-id --view full`. All other generated rows are +discovery-only. In particular, `machines create` remains unavailable rather +than enabling the legacy untyped `node_claim` contract. Generation tasks: @@ -239,6 +240,10 @@ akua # registry status home view akua auth login --token # save a local API token akua auth status # show effective auth source akua auth logout # remove the saved local API token +akua clusters get --id # executable reviewed API overlay +akua compute-configs list --view full # executable reviewed API overlay +akua compute list-instance-types --config +akua machines list --cluster-id --view full akua commands # first 20 generated public commands akua commands --resource workspaces # resource filter akua commands --operation-id workspaces.list diff --git a/test/capacity-openapi.test.ts b/test/capacity-openapi.test.ts new file mode 100644 index 0000000..5f35b1a --- /dev/null +++ b/test/capacity-openapi.test.ts @@ -0,0 +1,82 @@ +import { describe, expect, test } from "bun:test"; + +type JsonObject = Record; + +const spec = (await Bun.file(new URL("../openapi/public.json", import.meta.url)).json()) as JsonObject; + +describe("capacity overlay OpenAPI contracts", () => { + test("reviewed read overlays retain their exact operations and bindings", () => { + expect(operation("/v1/clusters/{id}", "get")).toMatchObject({ + operationId: "clusters.get", + "x-platform-visibility": "PUBLIC", + parameters: [ + { name: "id", in: "path", required: true }, + { name: "akua-context", in: "header", required: false }, + ], + }); + const configs = operation("/v1/compute_configs", "get"); + expect(configs.operationId).toBe("computeConfigs.list"); + expect(bindings(configs)).toEqual([ + ["cursor", "query", false], + ["limit", "query", false], + ["view", "query", false], + ["akua-context", "header", false], + ]); + expect(configs.parameters[2].schema.enum).toEqual(["basic", "full"]); + + const instanceTypes = operation("/v1/compute/instance_types", "get"); + expect(instanceTypes.operationId).toBe("compute.listInstanceTypes"); + expect(bindings(instanceTypes)).toEqual([["config", "query", false]]); + + const machines = operation("/v1/machines", "get"); + expect(machines.operationId).toBe("machines.list"); + expect(bindings(machines)).toEqual([ + ["cursor", "query", false], + ["limit", "query", false], + ["cluster_id", "query", false], + ["state", "query", false], + ["view", "query", false], + ["akua-context", "header", false], + ]); + expect(machines.parameters[4].schema.enum).toEqual(["basic", "full"]); + }); + + test("machine creation remains the canonical closed idempotent contract", () => { + const create = operation("/v1/machines", "post"); + expect(create).toMatchObject({ + operationId: "machines.create", + "x-platform-visibility": "PUBLIC", + }); + expect(bindings(create)).toEqual([ + ["akua-context", "header", false], + ["idempotency-key", "header", false], + ]); + expect(create.responses["202"].content["application/json"].schema.$ref).toBe( + "#/components/schemas/OperationEnvelope", + ); + + const body = create.requestBody.content["application/json"].schema; + expect(body).toMatchObject({ + type: "object", + required: ["cluster_id", "instance_type", "compute_config_id"], + additionalProperties: false, + }); + expect(Object.keys(body.properties).sort()).toEqual([ + "cluster_id", + "compute_config_id", + "instance_type", + "name", + "node_claim", + ]); + }); +}); + +function operation(path: string, method: string): JsonObject { + const value = spec.paths?.[path]?.[method]; + expect(value).toBeDefined(); + return value as JsonObject; +} + +function bindings(value: JsonObject): Array<[string, string, boolean]> { + return value.parameters.map((parameter: JsonObject) => [parameter.name, parameter.in, parameter.required]); +} From 3018fb0894c7b3ff863b1e7a92af039c78d21d91 Mon Sep 17 00:00:00 2001 From: Akua Ship Crewmate Date: Fri, 24 Jul 2026 02:10:37 +0000 Subject: [PATCH 4/7] Correct capacity request execution contracts --- src/commands/capacity.ts | 77 +++++++++++++++++++++++++++----- src/runtime/public-api-client.ts | 65 +++++++++++++++++++++++++-- test/capacity-commands.test.ts | 70 ++++++++++++++++++++++++++--- 3 files changed, 190 insertions(+), 22 deletions(-) diff --git a/src/commands/capacity.ts b/src/commands/capacity.ts index c777737..b1aae86 100644 --- a/src/commands/capacity.ts +++ b/src/commands/capacity.ts @@ -1,4 +1,4 @@ -import { usageError } from "../runtime/errors"; +import { AkuaCliError, usageError } from "../runtime/errors"; import { PublicApiClient, type ApiFetch } from "../runtime/public-api-client"; import type { RenderEnvelope } from "../runtime/render"; import { readPublicApiToken } from "./auth"; @@ -15,8 +15,14 @@ const productionDependencies: CapacityDependencies = { interface ParsedCommand { command: string; - path: string; + path?: string; workspace?: string; + create?: { + cluster_id: string; + compute_config_id: string; + instance_type: string; + idempotency_key: string; + }; } export async function capacityView( @@ -26,7 +32,17 @@ export async function capacityView( ): Promise { const parsed = parseCommand(argv); const token = await dependencies.readToken(env); - const data = await new PublicApiClient(token, dependencies.fetch).get(parsed.path, parsed.workspace); + const client = new PublicApiClient(token, dependencies.fetch); + if (parsed.create !== undefined) { + const { idempotency_key, ...body } = parsed.create; + const operation = validateOperationEnvelope(await client.createMachine(body, idempotency_key, parsed.workspace)); + return { + command: parsed.command, + observations: ["Machine creation accepted. No automatic retry was attempted."], + data: { ...operation, idempotency_key }, + }; + } + const data = await client.get(parsed.path as string, parsed.workspace); return { command: parsed.command, data }; } @@ -50,10 +66,10 @@ function parseCommand(argv: readonly string[]): ParsedCommand { if (operation === "compute list-instance-types") { allowOnly(flags, ["--config"]); - const config = requiredCanonicalId(flags, "--config", "cfg"); + const config = requiredBoundedValue(flags, "--config", 54); return { command: "akua compute list-instance-types", - path: `/v1/compute/instance_types?config=${config}`, + path: `/v1/compute/instance_types?config=${encodeURIComponent(config)}`, }; } @@ -70,15 +86,20 @@ function parseCommand(argv: readonly string[]): ParsedCommand { } if (operation === "machines create") { - allowOnly(flags, ["--cluster-id", "--compute-config-id", "--instance-type", "--idempotency-key", "--yes"]); - requiredCanonicalId(flags, "--cluster-id", "clu"); - requiredCanonicalId(flags, "--compute-config-id", "cfg"); - requiredValue(flags, "--instance-type"); - requiredValue(flags, "--idempotency-key"); + allowOnly(flags, ["--cluster-id", "--compute-config-id", "--instance-type", "--idempotency-key", "--workspace", "--yes"]); + const cluster_id = requiredCanonicalId(flags, "--cluster-id", "clu"); + const compute_config_id = requiredBoundedValue(flags, "--compute-config-id", 54); + const instance_type = requiredBoundedValue(flags, "--instance-type", 120); + const idempotency_key = requiredBoundedValue(flags, "--idempotency-key", 64); + const workspace = optionalCanonicalId(flags, "--workspace", "ws"); if (flags.get("--yes") !== true) { throw usageError("machines create requires explicit --yes confirmation."); } - throw usageError("machines create execution is not available in this capacity overlay."); + return { + command: "akua machines create", + workspace, + create: { cluster_id, compute_config_id, instance_type, idempotency_key }, + }; } throw usageError("Unknown capacity command."); @@ -125,7 +146,7 @@ function allowOnly(flags: ReadonlyMap, allowed: readonly function requiredCanonicalId(flags: ReadonlyMap, name: string, prefix: string): string { const value = requiredValue(flags, name); - if (!new RegExp(`^${prefix}_[a-z0-9]{16}$`).test(value)) { + if (!new RegExp(`^${prefix}_[a-z0-9]{32}$`).test(value)) { throw usageError(`${name} must be a canonical ${prefix}_ ID.`); } return value; @@ -150,6 +171,38 @@ function requiredValue(flags: ReadonlyMap, name: string): return value; } +function requiredBoundedValue( + flags: ReadonlyMap, + name: string, + maxLength: number, +): string { + const value = requiredValue(flags, name); + if (value.length > maxLength || /[\u0000-\u001f\u007f]/.test(value)) { + throw usageError(`${name} must be at most ${maxLength} characters and contain no control characters.`); + } + return value; +} + +function validateOperationEnvelope(value: unknown): { operation_id: string } { + if ( + value === null || + typeof value !== "object" || + Array.isArray(value) || + Object.keys(value).length !== 1 || + typeof (value as Record).operation_id !== "string" || + (value as Record).operation_id.length < 1 || + (value as Record).operation_id.length > 53 + ) { + throw new AkuaCliError({ + type: "invalid_response", + code: "AKUA_PUBLIC_API_INVALID_RESPONSE", + message: "The Akua API machine-create response did not match the reviewed contract.", + exitCode: 1, + }); + } + return value as { operation_id: string }; +} + function requireExactValue(flags: ReadonlyMap, name: string, expected: string): void { const value = requiredValue(flags, name); if (value !== expected) { diff --git a/src/runtime/public-api-client.ts b/src/runtime/public-api-client.ts index c4178e1..2da6a25 100644 --- a/src/runtime/public-api-client.ts +++ b/src/runtime/public-api-client.ts @@ -4,6 +4,8 @@ const PublicApiBase = "https://api.akua.dev"; export type ApiFetch = (input: string | URL | Request, init?: RequestInit) => Promise; +class AmbiguousTransportError extends Error {} + export class PublicApiClient { constructor( private readonly token: string, @@ -11,15 +13,63 @@ export class PublicApiClient { ) {} async get(path: string, workspace?: string): Promise { - const headers: Record = { authorization: `Bearer ${this.token}` }; + return this.request(path, { method: "GET" }, workspace); + } + + async createMachine( + body: { cluster_id: string; instance_type: string; compute_config_id: string }, + idempotencyKey: string, + workspace?: string, + ): Promise { + try { + return await this.request( + "/v1/machines", + { + method: "POST", + headers: { "content-type": "application/json", "idempotency-key": idempotencyKey }, + body: JSON.stringify(body), + }, + workspace, + 202, + ); + } catch (error) { + if (error instanceof AmbiguousTransportError) { + throw new AkuaCliError({ + type: "unknown_outcome", + code: "AKUA_MACHINE_CREATE_OUTCOME_UNKNOWN", + message: "Machine creation outcome is unknown. The request was not retried.", + exitCode: 1, + nextSteps: [ + { + command: "akua machines create ... --idempotency-key --yes", + description: "Check operation status first; replay only with the exact caller-supplied idempotency key.", + }, + ], + }); + } + throw error; + } + } + + private async request( + path: string, + init: RequestInit, + workspace?: string, + expectedStatus?: number, + ): Promise { + const headers = new Headers(init.headers); + headers.set("authorization", `Bearer ${this.token}`); if (workspace !== undefined) { - headers["akua-context"] = workspace; + headers.set("akua-context", workspace); } let response: Response; try { - response = await this.apiFetch(`${PublicApiBase}${path}`, { method: "GET", headers }); + response = await this.apiFetch(`${PublicApiBase}${path}`, { ...init, headers }); } catch { + if (init.method === "POST") { + throw new AmbiguousTransportError(); + } throw new AkuaCliError({ type: "transport_error", code: "AKUA_PUBLIC_API_TRANSPORT_ERROR", @@ -51,6 +101,15 @@ export class PublicApiClient { retryAfter: response.headers.get("retry-after"), }); } + if (expectedStatus !== undefined && response.status !== expectedStatus) { + throw new AkuaCliError({ + type: "invalid_response", + code: "AKUA_PUBLIC_API_INVALID_RESPONSE", + message: "The Akua API returned an unexpected success status.", + status: response.status, + exitCode: 1, + }); + } return body; } } diff --git a/test/capacity-commands.test.ts b/test/capacity-commands.test.ts index c8f8ad2..ee65c0e 100644 --- a/test/capacity-commands.test.ts +++ b/test/capacity-commands.test.ts @@ -7,9 +7,9 @@ import { capacityView, type CapacityDependencies } from "../src/commands/capacit import { PublicApiClient, type ApiFetch } from "../src/runtime/public-api-client"; const TOKEN = "sentinel-public-api-token"; -const CLUSTER = "clu_j572abc123def456"; -const WORKSPACE = "ws_j572abc123def456"; -const CONFIG = "cfg_j572abc123def456"; +const CLUSTER = "clu_j572abc123def456j572abc123def456"; +const WORKSPACE = "ws_j572abc123def456j572abc123def456"; +const CONFIG = "j572abc123def456j572abc123def456"; describe("capacity command overlays", () => { test("clusters get binds the canonical path and exact workspace header", async () => { @@ -52,11 +52,67 @@ describe("capacity command overlays", () => { expect(machines.requests[0].url).toBe(`https://api.akua.dev/v1/machines?cluster_id=${CLUSTER}&view=full`); }); - test("instance types require an explicit config and return full comparison fields", async () => { + test("instance types bind the explicit config name and return full comparison fields", async () => { const fixture = apiFixture([{ name: "cpx31", arch: "amd64", cpu: 4, memory_mib: 8192, storage_mib: 163840, price_per_hour: 0.0208, available: true, zone: "fsn1", capacity_type: "on-demand" }]); - const result = await capacityView(["compute", "list-instance-types", "--config", CONFIG], {}, deps(fixture.fetch)); + const configName = "hcloud fsn1/prod"; + const result = await capacityView(["compute", "list-instance-types", "--config", configName], {}, deps(fixture.fetch)); expect(result.data).toEqual([{ name: "cpx31", arch: "amd64", cpu: 4, memory_mib: 8192, storage_mib: 163840, price_per_hour: 0.0208, available: true, zone: "fsn1", capacity_type: "on-demand" }]); - expect(fixture.requests[0].url).toBe(`https://api.akua.dev/v1/compute/instance_types?config=${CONFIG}`); + expect(fixture.requests[0].url).toBe("https://api.akua.dev/v1/compute/instance_types?config=hcloud%20fsn1%2Fprod"); + }); + + test("machine create submits the canonical closed request exactly once", async () => { + const fixture = apiFixture({ operation_id: "op_create_123" }, 202); + const result = await capacityView([ + "machines", "create", + "--cluster-id", CLUSTER, + "--compute-config-id", CONFIG, + "--instance-type", "cpx31", + "--idempotency-key", "captain-stable-key", + "--workspace", WORKSPACE, + "--yes", + ], {}, deps(fixture.fetch)); + + expect(result.data).toEqual({ operation_id: "op_create_123", idempotency_key: "captain-stable-key" }); + expect(fixture.requests).toHaveLength(1); + expect(fixture.requests[0]).toEqual({ + url: "https://api.akua.dev/v1/machines", + method: "POST", + headers: { + "akua-context": WORKSPACE, + authorization: `Bearer ${TOKEN}`, + "content-type": "application/json", + "idempotency-key": "captain-stable-key", + }, + body: JSON.stringify({ cluster_id: CLUSTER, compute_config_id: CONFIG, instance_type: "cpx31" }), + }); + expect(JSON.stringify(result)).not.toContain(TOKEN); + }); + + test("ambiguous machine-create transport outcome is not retried and preserves same-key guidance", async () => { + let attempts = 0; + const fetch: ApiFetch = async () => { + attempts += 1; + throw new Error(`socket closed with ${TOKEN}`); + }; + const error = await capacityView([ + "machines", "create", + "--cluster-id", CLUSTER, + "--compute-config-id", CONFIG, + "--instance-type", "cpx31", + "--idempotency-key", "captain-stable-key", + "--yes", + ], {}, deps(fetch)).catch((value) => value); + + expect(attempts).toBe(1); + expect(error).toMatchObject({ + code: "AKUA_MACHINE_CREATE_OUTCOME_UNKNOWN", + message: expect.stringContaining("unknown"), + nextSteps: [{ + command: expect.stringContaining(""), + description: expect.stringContaining("exact caller-supplied idempotency key"), + }], + }); + expect(JSON.stringify(error.toPayload())).not.toContain(TOKEN); }); test("invalid IDs, unknown flags, and create without yes fail before auth or fetch", async () => { @@ -67,7 +123,7 @@ describe("capacity command overlays", () => { fetch: async () => { fetchCalls += 1; throw new Error("must not fetch"); }, }; for (const argv of [ - ["clusters", "get", "--id", "not-a-cluster"], + ["clusters", "get", "--id", "clu_j572abc123def456"], ["machines", "list", "--cluster-id", CLUSTER, "--bogus"], ["machines", "create", "--cluster-id", CLUSTER, "--compute-config-id", CONFIG, "--instance-type", "cpx31", "--idempotency-key", "captain-key"], ]) { From 7ba74a9f244aa7ba9c2acbd02c4cc2b1fb1fb602 Mon Sep 17 00:00:00 2001 From: Akua Ship Crewmate Date: Fri, 24 Jul 2026 02:27:47 +0000 Subject: [PATCH 5/7] Harden capacity client outcomes --- src/commands/capacity.ts | 32 ++--- src/runtime/public-api-client.ts | 196 +++++++++++++++++++++++++------ test/capacity-commands.test.ts | 120 +++++++++++++++---- 3 files changed, 265 insertions(+), 83 deletions(-) diff --git a/src/commands/capacity.ts b/src/commands/capacity.ts index b1aae86..bafa135 100644 --- a/src/commands/capacity.ts +++ b/src/commands/capacity.ts @@ -1,4 +1,4 @@ -import { AkuaCliError, usageError } from "../runtime/errors"; +import { usageError } from "../runtime/errors"; import { PublicApiClient, type ApiFetch } from "../runtime/public-api-client"; import type { RenderEnvelope } from "../runtime/render"; import { readPublicApiToken } from "./auth"; @@ -35,7 +35,7 @@ export async function capacityView( const client = new PublicApiClient(token, dependencies.fetch); if (parsed.create !== undefined) { const { idempotency_key, ...body } = parsed.create; - const operation = validateOperationEnvelope(await client.createMachine(body, idempotency_key, parsed.workspace)); + const operation = await client.createMachine(body, idempotency_key, parsed.workspace); return { command: parsed.command, observations: ["Machine creation accepted. No automatic retry was attempted."], @@ -66,7 +66,7 @@ function parseCommand(argv: readonly string[]): ParsedCommand { if (operation === "compute list-instance-types") { allowOnly(flags, ["--config"]); - const config = requiredBoundedValue(flags, "--config", 54); + const config = requiredOpaquePublicId(flags, "--config"); return { command: "akua compute list-instance-types", path: `/v1/compute/instance_types?config=${encodeURIComponent(config)}`, @@ -88,7 +88,7 @@ function parseCommand(argv: readonly string[]): ParsedCommand { if (operation === "machines create") { allowOnly(flags, ["--cluster-id", "--compute-config-id", "--instance-type", "--idempotency-key", "--workspace", "--yes"]); const cluster_id = requiredCanonicalId(flags, "--cluster-id", "clu"); - const compute_config_id = requiredBoundedValue(flags, "--compute-config-id", 54); + const compute_config_id = requiredOpaquePublicId(flags, "--compute-config-id"); const instance_type = requiredBoundedValue(flags, "--instance-type", 120); const idempotency_key = requiredBoundedValue(flags, "--idempotency-key", 64); const workspace = optionalCanonicalId(flags, "--workspace", "ws"); @@ -183,24 +183,14 @@ function requiredBoundedValue( return value; } -function validateOperationEnvelope(value: unknown): { operation_id: string } { - if ( - value === null || - typeof value !== "object" || - Array.isArray(value) || - Object.keys(value).length !== 1 || - typeof (value as Record).operation_id !== "string" || - (value as Record).operation_id.length < 1 || - (value as Record).operation_id.length > 53 - ) { - throw new AkuaCliError({ - type: "invalid_response", - code: "AKUA_PUBLIC_API_INVALID_RESPONSE", - message: "The Akua API machine-create response did not match the reviewed contract.", - exitCode: 1, - }); +function requiredOpaquePublicId(flags: ReadonlyMap, name: string): string { + const value = requiredValue(flags, name); + // Public IDs have an opaque 32-character payload and a resource prefix. The + // OpenAPI contract does not assign a literal prefix to compute configs. + if (value.length > 54 || !/^[a-z][a-z0-9]{0,20}_[a-z0-9]{32}$/.test(value)) { + throw usageError(`${name} must be a prefixed opaque public ID.`); } - return value as { operation_id: string }; + return value; } function requireExactValue(flags: ReadonlyMap, name: string, expected: string): void { diff --git a/src/runtime/public-api-client.ts b/src/runtime/public-api-client.ts index 2da6a25..b4dcf0f 100644 --- a/src/runtime/public-api-client.ts +++ b/src/runtime/public-api-client.ts @@ -1,16 +1,30 @@ import { AkuaCliError } from "./errors"; const PublicApiBase = "https://api.akua.dev"; +const DefaultRequestTimeoutMs = 30_000; +const DefaultMaxResponseBytes = 1_048_576; export type ApiFetch = (input: string | URL | Request, init?: RequestInit) => Promise; -class AmbiguousTransportError extends Error {} +export interface PublicApiClientOptions { + requestTimeoutMs?: number; + maxResponseBytes?: number; +} + +class UnknownCreateOutcomeError extends Error {} export class PublicApiClient { + private readonly requestTimeoutMs: number; + private readonly maxResponseBytes: number; + constructor( private readonly token: string, private readonly apiFetch: ApiFetch = fetch, - ) {} + options: PublicApiClientOptions = {}, + ) { + this.requestTimeoutMs = options.requestTimeoutMs ?? DefaultRequestTimeoutMs; + this.maxResponseBytes = options.maxResponseBytes ?? DefaultMaxResponseBytes; + } async get(path: string, workspace?: string): Promise { return this.request(path, { method: "GET" }, workspace); @@ -20,9 +34,9 @@ export class PublicApiClient { body: { cluster_id: string; instance_type: string; compute_config_id: string }, idempotencyKey: string, workspace?: string, - ): Promise { + ): Promise<{ operation_id: string }> { try { - return await this.request( + const value = await this.request( "/v1/machines", { method: "POST", @@ -32,8 +46,9 @@ export class PublicApiClient { workspace, 202, ); + return validateOperationEnvelope(value); } catch (error) { - if (error instanceof AmbiguousTransportError) { + if (error instanceof UnknownCreateOutcomeError) { throw new AkuaCliError({ type: "unknown_outcome", code: "AKUA_MACHINE_CREATE_OUTCOME_UNKNOWN", @@ -63,53 +78,162 @@ export class PublicApiClient { headers.set("akua-context", workspace); } + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), this.requestTimeoutMs); let response: Response; try { - response = await this.apiFetch(`${PublicApiBase}${path}`, { ...init, headers }); + response = await this.apiFetch(`${PublicApiBase}${path}`, { + ...init, + headers, + signal: controller.signal, + }); } catch { + clearTimeout(timeout); if (init.method === "POST") { - throw new AmbiguousTransportError(); + throw new UnknownCreateOutcomeError(); } - throw new AkuaCliError({ - type: "transport_error", - code: "AKUA_PUBLIC_API_TRANSPORT_ERROR", - message: "The Akua API request could not be completed.", - exitCode: 1, - }); + throw transportError(); } let body: unknown; try { - body = await response.json(); + const text = await readBoundedText(response, this.maxResponseBytes); + body = JSON.parse(text); } catch { - throw new AkuaCliError({ - type: "invalid_response", - code: "AKUA_PUBLIC_API_INVALID_RESPONSE", - message: "The Akua API returned an invalid JSON response.", - status: response.status, - exitCode: 1, - }); + clearTimeout(timeout); + if (init.method === "POST" && response.ok) { + throw new UnknownCreateOutcomeError(); + } + if (!response.ok) { + throw apiError(response); + } + throw invalidResponse(response.status, "The Akua API returned an invalid or oversized JSON response."); + } finally { + clearTimeout(timeout); } if (!response.ok) { - throw new AkuaCliError({ - type: "api_error", - code: "AKUA_PUBLIC_API_ERROR", - message: "The Akua API rejected the request.", - status: response.status, - requestId: response.headers.get("x-request-id") ?? undefined, - retryAfter: response.headers.get("retry-after"), - }); + throw apiError(response, body, this.token); } if (expectedStatus !== undefined && response.status !== expectedStatus) { - throw new AkuaCliError({ - type: "invalid_response", - code: "AKUA_PUBLIC_API_INVALID_RESPONSE", - message: "The Akua API returned an unexpected success status.", - status: response.status, - exitCode: 1, - }); + if (init.method === "POST") { + throw new UnknownCreateOutcomeError(); + } + throw invalidResponse(response.status, "The Akua API returned an unexpected success status."); } return body; } } + +async function readBoundedText(response: Response, maxBytes: number): Promise { + const declaredLength = response.headers.get("content-length"); + if (declaredLength !== null && /^\d+$/.test(declaredLength) && Number(declaredLength) > maxBytes) { + throw new Error("response too large"); + } + if (response.body === null) { + return ""; + } + const reader = response.body.getReader(); + const chunks: Uint8Array[] = []; + let size = 0; + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + size += value.byteLength; + if (size > maxBytes) { + await reader.cancel(); + throw new Error("response too large"); + } + chunks.push(value); + } + } finally { + reader.releaseLock(); + } + const bytes = new Uint8Array(size); + let offset = 0; + for (const chunk of chunks) { + bytes.set(chunk, offset); + offset += chunk.byteLength; + } + return new TextDecoder("utf-8", { fatal: true }).decode(bytes); +} + +function apiError(response: Response, body?: unknown, token?: string): AkuaCliError { + const entry = firstApiErrorEntry(body); + return new AkuaCliError({ + type: "api_error", + code: entry === undefined ? "AKUA_PUBLIC_API_ERROR" : `AKUA_PUBLIC_API_${entry.code}`, + message: entry === undefined + ? "The Akua API rejected the request." + : redactToken(entry.message, token), + path: entry?.path, + status: response.status, + requestId: response.headers.get("x-request-id") ?? undefined, + retryAfter: response.headers.get("retry-after"), + }); +} + +function firstApiErrorEntry(value: unknown): { code: number; message: string; path?: string[] } | undefined { + if (!isRecord(value) || value.success !== false || !Array.isArray(value.errors) || !isRecord(value.result)) { + return undefined; + } + const entry = value.errors[0]; + if (!isRecord(entry) || !Number.isSafeInteger(entry.code) || typeof entry.message !== "string") { + return undefined; + } + if (entry.message.length < 1 || entry.message.length > 1_000 || /[\u0000-\u001f\u007f]/.test(entry.message)) { + return undefined; + } + let path: string[] | undefined; + if (entry.path !== undefined) { + if (!Array.isArray(entry.path) || entry.path.length > 32 || entry.path.some((part) => + typeof part !== "string" || part.length < 1 || part.length > 200 || /[\u0000-\u001f\u007f]/.test(part) + )) { + return undefined; + } + path = entry.path as string[]; + } + return { code: entry.code as number, message: entry.message, path }; +} + +function redactToken(message: string, token: string | undefined): string { + return token === undefined || token === "" ? message : message.split(token).join("[REDACTED]"); +} + +function validateOperationEnvelope(value: unknown): { operation_id: string } { + if ( + !isRecord(value) || + Object.keys(value).length !== 1 || + typeof value.operation_id !== "string" || + value.operation_id.length < 1 || + value.operation_id.length > 53 || + /[\u0000-\u001f\u007f]/.test(value.operation_id) + ) { + throw new UnknownCreateOutcomeError(); + } + return { operation_id: value.operation_id }; +} + +function transportError(): AkuaCliError { + return new AkuaCliError({ + type: "transport_error", + code: "AKUA_PUBLIC_API_TRANSPORT_ERROR", + message: "The Akua API request could not be completed.", + exitCode: 1, + }); +} + +function invalidResponse(status: number, message: string): AkuaCliError { + return new AkuaCliError({ + type: "invalid_response", + code: "AKUA_PUBLIC_API_INVALID_RESPONSE", + message, + status, + exitCode: 1, + }); +} + +function isRecord(value: unknown): value is Record { + return value !== null && typeof value === "object" && !Array.isArray(value); +} diff --git a/test/capacity-commands.test.ts b/test/capacity-commands.test.ts index ee65c0e..ceaca17 100644 --- a/test/capacity-commands.test.ts +++ b/test/capacity-commands.test.ts @@ -9,7 +9,8 @@ import { PublicApiClient, type ApiFetch } from "../src/runtime/public-api-client const TOKEN = "sentinel-public-api-token"; const CLUSTER = "clu_j572abc123def456j572abc123def456"; const WORKSPACE = "ws_j572abc123def456j572abc123def456"; -const CONFIG = "j572abc123def456j572abc123def456"; +const CONFIG = "cc_j572abc123def456j572abc123def456"; +const RAW_UUID = "123e4567-e89b-12d3-a456-426614174000"; describe("capacity command overlays", () => { test("clusters get binds the canonical path and exact workspace header", async () => { @@ -52,12 +53,11 @@ describe("capacity command overlays", () => { expect(machines.requests[0].url).toBe(`https://api.akua.dev/v1/machines?cluster_id=${CLUSTER}&view=full`); }); - test("instance types bind the explicit config name and return full comparison fields", async () => { + test("instance types bind the explicit prefixed config ID and return full comparison fields", async () => { const fixture = apiFixture([{ name: "cpx31", arch: "amd64", cpu: 4, memory_mib: 8192, storage_mib: 163840, price_per_hour: 0.0208, available: true, zone: "fsn1", capacity_type: "on-demand" }]); - const configName = "hcloud fsn1/prod"; - const result = await capacityView(["compute", "list-instance-types", "--config", configName], {}, deps(fixture.fetch)); + const result = await capacityView(["compute", "list-instance-types", "--config", CONFIG], {}, deps(fixture.fetch)); expect(result.data).toEqual([{ name: "cpx31", arch: "amd64", cpu: 4, memory_mib: 8192, storage_mib: 163840, price_per_hour: 0.0208, available: true, zone: "fsn1", capacity_type: "on-demand" }]); - expect(fixture.requests[0].url).toBe("https://api.akua.dev/v1/compute/instance_types?config=hcloud%20fsn1%2Fprod"); + expect(fixture.requests[0].url).toBe(`https://api.akua.dev/v1/compute/instance_types?config=${CONFIG}`); }); test("machine create submits the canonical closed request exactly once", async () => { @@ -88,30 +88,85 @@ describe("capacity command overlays", () => { expect(JSON.stringify(result)).not.toContain(TOKEN); }); - test("ambiguous machine-create transport outcome is not retried and preserves same-key guidance", async () => { + test("ambiguous machine-create outcomes are not retried and preserve same-key guidance", async () => { + const cases: ApiFetch[] = [ + async () => { throw new Error(`socket closed with ${TOKEN}`); }, + async () => new Response("{truncated", { status: 202 }), + async () => new Response(JSON.stringify({ wrong: "shape" }), { status: 202 }), + async () => new Response(JSON.stringify({ operation_id: "already-created" }), { status: 200 }), + ]; + for (const fetchCase of cases) { + let attempts = 0; + const fetch: ApiFetch = async (...args) => { + attempts += 1; + return fetchCase(...args); + }; + const error = await createMachine(fetch).catch((value) => value); + expect(attempts).toBe(1); + expect(error).toMatchObject({ + code: "AKUA_MACHINE_CREATE_OUTCOME_UNKNOWN", + message: expect.stringContaining("unknown"), + nextSteps: [{ + command: expect.stringContaining(""), + description: expect.stringContaining("exact caller-supplied idempotency key"), + }], + }); + expect(JSON.stringify(error.toPayload())).not.toContain(TOKEN); + } + }); + + test("create timeout is outcome unknown after exactly one bounded attempt", async () => { let attempts = 0; - const fetch: ApiFetch = async () => { + const fetch: ApiFetch = (_input, init) => new Promise((_resolve, reject) => { attempts += 1; - throw new Error(`socket closed with ${TOKEN}`); - }; - const error = await capacityView([ - "machines", "create", - "--cluster-id", CLUSTER, - "--compute-config-id", CONFIG, - "--instance-type", "cpx31", - "--idempotency-key", "captain-stable-key", - "--yes", - ], {}, deps(fetch)).catch((value) => value); - + init?.signal?.addEventListener("abort", () => reject(new DOMException("timed out", "AbortError")), { once: true }); + }); + const client = new PublicApiClient(TOKEN, fetch, { requestTimeoutMs: 5 }); + const error = await client.createMachine( + { cluster_id: CLUSTER, compute_config_id: CONFIG, instance_type: "cpx31" }, + "captain-stable-key", + ).catch((value) => value); expect(attempts).toBe(1); + expect(error.code).toBe("AKUA_MACHINE_CREATE_OUTCOME_UNKNOWN"); + expect(JSON.stringify(error.toPayload())).not.toContain(TOKEN); + }); + + test("valid provider errors project the first stable entry and redact tokens", async () => { + const fixture = apiFixture({ + success: false, + errors: [{ code: 7002, message: `Resource ${TOKEN} not found`, path: ["body", "compute_config_id"], metadata: { ignored: "value" } }], + result: {}, + }, 404, { "x-request-id": "req_safe", "retry-after": "9" }); + const error = await capacityView(["compute", "list-instance-types", "--config", CONFIG], {}, deps(fixture.fetch)).catch((value) => value); expect(error).toMatchObject({ - code: "AKUA_MACHINE_CREATE_OUTCOME_UNKNOWN", - message: expect.stringContaining("unknown"), - nextSteps: [{ - command: expect.stringContaining(""), - description: expect.stringContaining("exact caller-supplied idempotency key"), - }], + code: "AKUA_PUBLIC_API_7002", + status: 404, + path: ["body", "compute_config_id"], + requestId: "req_safe", + retryAfter: "9", }); + expect(error.message).toBe("Resource [REDACTED] not found"); + expect(JSON.stringify(error.toPayload())).not.toContain(TOKEN); + + let createAttempts = 0; + const createError = await createMachine(async () => { + createAttempts += 1; + return new Response(JSON.stringify({ + success: false, + errors: [{ code: 7009, message: "Capacity conflict", path: ["body", "instance_type"] }], + result: {}, + }), { status: 409 }); + }).catch((value) => value); + expect(createAttempts).toBe(1); + expect(createError).toMatchObject({ code: "AKUA_PUBLIC_API_7009", status: 409 }); + expect(createError.code).not.toBe("AKUA_MACHINE_CREATE_OUTCOME_UNKNOWN"); + }); + + test("responses are byte bounded", async () => { + const fetch: ApiFetch = async () => new Response(JSON.stringify({ data: "x".repeat(33) })); + const client = new PublicApiClient(TOKEN, fetch, { maxResponseBytes: 32 }); + const error = await client.get("/v1/clusters/test").catch((value) => value); + expect(error).toMatchObject({ code: "AKUA_PUBLIC_API_INVALID_RESPONSE" }); expect(JSON.stringify(error.toPayload())).not.toContain(TOKEN); }); @@ -124,6 +179,8 @@ describe("capacity command overlays", () => { }; for (const argv of [ ["clusters", "get", "--id", "clu_j572abc123def456"], + ["compute", "list-instance-types", "--config", RAW_UUID], + ["machines", "create", "--cluster-id", CLUSTER, "--compute-config-id", RAW_UUID, "--instance-type", "cpx31", "--idempotency-key", "captain-key", "--yes"], ["machines", "list", "--cluster-id", CLUSTER, "--bogus"], ["machines", "create", "--cluster-id", CLUSTER, "--compute-config-id", CONFIG, "--instance-type", "cpx31", "--idempotency-key", "captain-key"], ]) { @@ -175,11 +232,11 @@ describe("capacity command overlays", () => { interface CapturedRequest { url: string; method: string; headers: Record; body?: string } -function apiFixture(body: unknown, status = 200) { +function apiFixture(body: unknown, status = 200, responseHeaders: Record = {}) { const requests: CapturedRequest[] = []; const fetch: ApiFetch = async (input, init) => { requests.push({ url: String(input), method: init?.method ?? "GET", headers: Object.fromEntries(new Headers(init?.headers).entries()), body: typeof init?.body === "string" ? init.body : undefined }); - return new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } }); + return new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json", ...responseHeaders } }); }; return { fetch, requests }; } @@ -188,6 +245,17 @@ function deps(fetch: ApiFetch): CapacityDependencies { return { readToken: async () => TOKEN, fetch }; } +function createMachine(fetch: ApiFetch) { + return capacityView([ + "machines", "create", + "--cluster-id", CLUSTER, + "--compute-config-id", CONFIG, + "--instance-type", "cpx31", + "--idempotency-key", "captain-stable-key", + "--yes", + ], {}, deps(fetch)); +} + async function runAkua(args: readonly string[], env: Record = {}) { const childEnv = { ...process.env, ...env }; delete childEnv.AKUA_OUTPUT; From 5eecfb9445d34b2512faba0dd5bef767a672f7e9 Mon Sep 17 00:00:00 2001 From: Akua Ship Crewmate Date: Fri, 24 Jul 2026 02:32:20 +0000 Subject: [PATCH 6/7] Finish capacity overlay delivery contracts --- README.md | 17 ++++++++++------- docs/architecture.md | 14 +++++++++----- src/bin/akua.ts | 4 ++-- src/commands/capacity.ts | 2 +- src/runtime/public-api-client.ts | 2 +- test/capacity-commands.test.ts | 16 ++++++++++------ 6 files changed, 33 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index e841a0c..7a63af2 100644 --- a/README.md +++ b/README.md @@ -141,20 +141,23 @@ akua auth logout ## Executable capacity overlays -Four reviewed public API reads are executable in addition to the discovery-only -generated registry: +Five reviewed public API overlays are executable in addition to the +discovery-only generated registry: ```sh akua clusters get --id [--workspace ] akua compute-configs list --view full [--workspace ] -akua compute list-instance-types --config +akua compute list-instance-types --config akua machines list --cluster-id --view full [--workspace ] +akua machines create --cluster-id --compute-config-id --instance-type --idempotency-key [--workspace ] --yes ``` -They use the fixed production API, accept only canonical IDs, and authenticate -with `AKUA_API_TOKEN` before the protected local config. `machines create` and -all other generated mutations remain unavailable; registry discovery does not -imply execution support. +They use the fixed production API, validate canonical resource IDs and bounded +names locally, and authenticate with `AKUA_API_TOKEN` before the protected local +config. Machine creation targets only canonical `POST /v1/machines`, submits +once, and requires explicit `--yes` plus a caller-visible idempotency key. All +other generated mutations remain unavailable; registry discovery does not imply +execution support. `AKUA_API_TOKEN` takes precedence over a stored token. Login writes `~/.config/akua/config.json`; the directory is forced to `0700` and the file to diff --git a/docs/architecture.md b/docs/architecture.md index 06ceaa5..bda78d3 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -131,10 +131,13 @@ unique; it does not currently enforce uniqueness itself. The generator deliberately produces a discovery registry, not generic API execution. A small hand-written capacity overlay executes only reviewed contracts: `clusters get`, `compute-configs list --view full`, -`compute list-instance-types --config `, and -`machines list --cluster-id --view full`. All other generated rows are -discovery-only. In particular, `machines create` remains unavailable rather -than enabling the legacy untyped `node_claim` contract. +`compute list-instance-types --config `, +`machines list --cluster-id --view full`, and canonical +`machines create --cluster-id --compute-config-id +--instance-type --idempotency-key [--workspace +] --yes`. All other generated rows are discovery-only. The create +overlay binds a closed three-field body to `POST /v1/machines`; it does not +enable the legacy untyped `compute.createMachine` `node_claim` contract. Generation tasks: @@ -242,8 +245,9 @@ akua auth status # show effective auth source akua auth logout # remove the saved local API token akua clusters get --id # executable reviewed API overlay akua compute-configs list --view full # executable reviewed API overlay -akua compute list-instance-types --config +akua compute list-instance-types --config akua machines list --cluster-id --view full +akua machines create --cluster-id --compute-config-id --instance-type --idempotency-key [--workspace ] --yes akua commands # first 20 generated public commands akua commands --resource workspaces # resource filter akua commands --operation-id workspaces.list diff --git a/src/bin/akua.ts b/src/bin/akua.ts index 2c37400..b44cd11 100644 --- a/src/bin/akua.ts +++ b/src/bin/akua.ts @@ -108,9 +108,9 @@ function helpView(): RenderEnvelope { " akua agent-os load-hcloud-provider --workspace --token-file [--expected-ssh-key-fingerprint [--expected-ssh-key-name ]]", " akua clusters get --id [--workspace ]", " akua compute-configs list --view full [--workspace ]", - " akua compute list-instance-types --config ", + " akua compute list-instance-types --config ", " akua machines list --cluster-id --view full [--workspace ]", - " akua machines create ... --yes Validate the bounded create command (submission unavailable)", + " akua machines create --cluster-id --compute-config-id --instance-type --idempotency-key [--workspace ] --yes", " akua commands List generated public OpenAPI command registry", " akua --help Show help", " akua --version Show version", diff --git a/src/commands/capacity.ts b/src/commands/capacity.ts index bafa135..7692d3e 100644 --- a/src/commands/capacity.ts +++ b/src/commands/capacity.ts @@ -66,7 +66,7 @@ function parseCommand(argv: readonly string[]): ParsedCommand { if (operation === "compute list-instance-types") { allowOnly(flags, ["--config"]); - const config = requiredOpaquePublicId(flags, "--config"); + const config = requiredBoundedValue(flags, "--config", 54); return { command: "akua compute list-instance-types", path: `/v1/compute/instance_types?config=${encodeURIComponent(config)}`, diff --git a/src/runtime/public-api-client.ts b/src/runtime/public-api-client.ts index b4dcf0f..8e8b642 100644 --- a/src/runtime/public-api-client.ts +++ b/src/runtime/public-api-client.ts @@ -167,7 +167,7 @@ function apiError(response: Response, body?: unknown, token?: string): AkuaCliEr message: entry === undefined ? "The Akua API rejected the request." : redactToken(entry.message, token), - path: entry?.path, + path: entry?.path?.map((part) => redactToken(part, token)), status: response.status, requestId: response.headers.get("x-request-id") ?? undefined, retryAfter: response.headers.get("retry-after"), diff --git a/test/capacity-commands.test.ts b/test/capacity-commands.test.ts index ceaca17..6dc7a59 100644 --- a/test/capacity-commands.test.ts +++ b/test/capacity-commands.test.ts @@ -53,11 +53,12 @@ describe("capacity command overlays", () => { expect(machines.requests[0].url).toBe(`https://api.akua.dev/v1/machines?cluster_id=${CLUSTER}&view=full`); }); - test("instance types bind the explicit prefixed config ID and return full comparison fields", async () => { + test("instance types bind the explicit config name and return full comparison fields", async () => { const fixture = apiFixture([{ name: "cpx31", arch: "amd64", cpu: 4, memory_mib: 8192, storage_mib: 163840, price_per_hour: 0.0208, available: true, zone: "fsn1", capacity_type: "on-demand" }]); - const result = await capacityView(["compute", "list-instance-types", "--config", CONFIG], {}, deps(fixture.fetch)); + const configName = "hcloud fsn1/prod"; + const result = await capacityView(["compute", "list-instance-types", "--config", configName], {}, deps(fixture.fetch)); expect(result.data).toEqual([{ name: "cpx31", arch: "amd64", cpu: 4, memory_mib: 8192, storage_mib: 163840, price_per_hour: 0.0208, available: true, zone: "fsn1", capacity_type: "on-demand" }]); - expect(fixture.requests[0].url).toBe(`https://api.akua.dev/v1/compute/instance_types?config=${CONFIG}`); + expect(fixture.requests[0].url).toBe("https://api.akua.dev/v1/compute/instance_types?config=hcloud%20fsn1%2Fprod"); }); test("machine create submits the canonical closed request exactly once", async () => { @@ -134,14 +135,14 @@ describe("capacity command overlays", () => { test("valid provider errors project the first stable entry and redact tokens", async () => { const fixture = apiFixture({ success: false, - errors: [{ code: 7002, message: `Resource ${TOKEN} not found`, path: ["body", "compute_config_id"], metadata: { ignored: "value" } }], + errors: [{ code: 7002, message: `Resource ${TOKEN} not found`, path: ["body", `compute_config_id.${TOKEN}`], metadata: { ignored: "value" } }], result: {}, }, 404, { "x-request-id": "req_safe", "retry-after": "9" }); const error = await capacityView(["compute", "list-instance-types", "--config", CONFIG], {}, deps(fixture.fetch)).catch((value) => value); expect(error).toMatchObject({ code: "AKUA_PUBLIC_API_7002", status: 404, - path: ["body", "compute_config_id"], + path: ["body", "compute_config_id.[REDACTED]"], requestId: "req_safe", retryAfter: "9", }); @@ -179,7 +180,8 @@ describe("capacity command overlays", () => { }; for (const argv of [ ["clusters", "get", "--id", "clu_j572abc123def456"], - ["compute", "list-instance-types", "--config", RAW_UUID], + ["compute", "list-instance-types", "--config", "bad\nconfig"], + ["compute", "list-instance-types", "--config", "x".repeat(55)], ["machines", "create", "--cluster-id", CLUSTER, "--compute-config-id", RAW_UUID, "--instance-type", "cpx31", "--idempotency-key", "captain-key", "--yes"], ["machines", "list", "--cluster-id", CLUSTER, "--bogus"], ["machines", "create", "--cluster-id", CLUSTER, "--compute-config-id", CONFIG, "--instance-type", "cpx31", "--idempotency-key", "captain-key"], @@ -227,6 +229,8 @@ describe("capacity command overlays", () => { expect(cli.stdout).toContain("akua compute-configs list --view full"); expect(cli.stdout).toContain("akua compute list-instance-types"); expect(cli.stdout).toContain("akua machines list"); + expect(cli.stdout).toContain("akua machines create --cluster-id --compute-config-id --instance-type --idempotency-key [--workspace ] --yes"); + expect(cli.stdout).not.toContain("submission unavailable"); }); }); From 730e8e347f1f424d8234e9161385b11689883148 Mon Sep 17 00:00:00 2001 From: Akua Ship Crewmate Date: Fri, 24 Jul 2026 02:35:31 +0000 Subject: [PATCH 7/7] Narrow bounded-response test error --- test/capacity-commands.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/capacity-commands.test.ts b/test/capacity-commands.test.ts index 6dc7a59..73e50bc 100644 --- a/test/capacity-commands.test.ts +++ b/test/capacity-commands.test.ts @@ -4,6 +4,7 @@ import { join } from "node:path"; import { readPublicApiToken } from "../src/commands/auth"; import { capacityView, type CapacityDependencies } from "../src/commands/capacity"; +import { AkuaCliError } from "../src/runtime/errors"; import { PublicApiClient, type ApiFetch } from "../src/runtime/public-api-client"; const TOKEN = "sentinel-public-api-token"; @@ -168,6 +169,7 @@ describe("capacity command overlays", () => { const client = new PublicApiClient(TOKEN, fetch, { maxResponseBytes: 32 }); const error = await client.get("/v1/clusters/test").catch((value) => value); expect(error).toMatchObject({ code: "AKUA_PUBLIC_API_INVALID_RESPONSE" }); + if (!(error instanceof AkuaCliError)) throw new Error("Expected a structured CLI error."); expect(JSON.stringify(error.toPayload())).not.toContain(TOKEN); });