openapi: 3.1.0 info: title: portal API version: 1.0.0 description: | The portal HTTP API. Every route chi serves under /api/v1 (internal/server/server.go) is described here; the schemas mirror the handler-layer *Response types in internal/handler (the wire boundary — repository rows never serialize directly). web/src/api/types.ts is generated from this file with openapi-typescript (`npm run generate:api` in web/); openapi-fetch consumes the generated `paths` interface. Edit this spec, regenerate, and the frontend contract follows — CI fails on drift. Two surfaces sit outside the JSON request/response model: - GET /workspaces/{workspaceId}/runs/{runId}/logs/ws upgrades to a WebSocket (documented on the operation; the stream itself is newline-delimited log text, authenticated via the `bearer` + token subprotocol pair). - GET /auth/github and /auth/github/callback are browser redirect hops of the OAuth flow, not XHR endpoints. Errors use one envelope everywhere (ErrorResponse) via respond.Error / respond.FromError; operations declare it as their default response. servers: - url: /api/v1 paths: /health: get: operationId: getHealth summary: App-level health surface the UI reads description: > Per-service status plus the dev_login flag in development. Returns 503 with the same body shape when degraded. (Liveness/readiness probes live at /healthz and /readyz outside /api/v1.) security: [] responses: "200": description: Healthy content: application/json: schema: { $ref: "#/components/schemas/HealthStatus" } "503": description: Degraded content: application/json: schema: { $ref: "#/components/schemas/HealthStatus" } /auth/github: get: operationId: githubLogin summary: Start the GitHub OAuth flow (browser redirect) security: [] responses: "307": description: Redirect to GitHub's authorize URL; sets the signed oauth_state cookie. default: { $ref: "#/components/responses/Error" } /auth/github/callback: get: operationId: githubCallback summary: GitHub OAuth callback (browser redirect) description: > Exchanges the code, provisions the user, sets the short-lived HttpOnly handoff cookie (scoped to /auth/handoff), and redirects to the SPA callback page, which completes login via POST /auth/handoff. security: [] parameters: - name: code in: query schema: { type: string } - name: state in: query schema: { type: string } responses: "307": description: Redirect to the SPA /auth/callback page. default: { $ref: "#/components/responses/Error" } /auth/handoff: post: operationId: authHandoff summary: Exchange the HttpOnly login-handoff cookie for the JWT (one-time) description: > Completes the login handoff. The OAuth callback (and dev login) set a short-lived HttpOnly cookie scoped to this path; this endpoint returns the JWT once and expires the cookie in the same response (delete-on-read), so a second call gets 401. POST keeps the exchange non-cacheable, and SameSite=Lax blocks cross-site POSTs from carrying the cookie. Unauthenticated by design — the caller has no bearer token yet. security: [] responses: "200": description: The JWT, exactly once; the handoff cookie is expired in the same response. content: application/json: schema: type: object properties: token: { type: string } required: [token] "401": description: Missing, expired, or malformed handoff cookie content: application/json: schema: { $ref: "#/components/schemas/ErrorResponse" } default: { $ref: "#/components/responses/Error" } /auth/me: get: operationId: getMe summary: The authenticated user responses: "200": description: Current user content: application/json: schema: { $ref: "#/components/schemas/User" } "401": description: Not authenticated content: application/json: schema: { $ref: "#/components/schemas/ErrorResponse" } default: { $ref: "#/components/responses/Error" } /webhooks/github: post: operationId: githubWebhook summary: GitHub push webhook (public, HMAC-verified) security: [] requestBody: required: true content: application/json: schema: type: object additionalProperties: true description: GitHub webhook payload (ping or push event). responses: "200": description: Webhook handled (pong / ignored / runs triggered). content: application/json: schema: type: object additionalProperties: true properties: status: { type: string } triggered: { type: integer } required: [status] default: { $ref: "#/components/responses/Error" } /users: get: operationId: listUsers summary: List org users (admin) responses: "200": description: Users content: application/json: schema: { $ref: "#/components/schemas/UserList" } default: { $ref: "#/components/responses/Error" } /users/{userId}/role: put: operationId: updateUserRole summary: Change a user's role (owner) parameters: - { name: userId, in: path, required: true, schema: { type: string } } requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/UpdateRoleRequest" } responses: "200": description: Updated user content: application/json: schema: { $ref: "#/components/schemas/User" } default: { $ref: "#/components/responses/Error" } /audit-logs: get: operationId: listAuditLogs summary: List audit logs (admin) parameters: - { name: page, in: query, schema: { type: integer } } - { name: per_page, in: query, schema: { type: integer } } responses: "200": description: Audit logs content: application/json: schema: { $ref: "#/components/schemas/AuditLogList" } default: { $ref: "#/components/responses/Error" } /variables: get: operationId: listOrgVariables summary: List org variables responses: "200": description: Org variables (sensitive values redacted) content: application/json: schema: { $ref: "#/components/schemas/OrgVariableList" } default: { $ref: "#/components/responses/Error" } post: operationId: createOrgVariable summary: Create an org variable (admin) requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/CreateVariableRequest" } responses: "201": description: Created variable content: application/json: schema: { $ref: "#/components/schemas/OrgVariable" } default: { $ref: "#/components/responses/Error" } /variables/{variableId}: put: operationId: updateOrgVariable summary: Update an org variable (admin) parameters: - { name: variableId, in: path, required: true, schema: { type: string } } requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/CreateVariableRequest" } responses: "200": description: Updated variable content: application/json: schema: { $ref: "#/components/schemas/OrgVariable" } default: { $ref: "#/components/responses/Error" } delete: operationId: deleteOrgVariable summary: Delete an org variable (admin) parameters: - { name: variableId, in: path, required: true, schema: { type: string } } responses: "204": { description: Deleted } default: { $ref: "#/components/responses/Error" } /variables/{variableId}/value: get: operationId: revealOrgVariableValue summary: Reveal an org variable's decrypted value (operator) parameters: - { name: variableId, in: path, required: true, schema: { type: string } } responses: "200": description: Decrypted value content: application/json: schema: { $ref: "#/components/schemas/VariableValue" } default: { $ref: "#/components/responses/Error" } /teams: get: operationId: listTeams summary: List teams parameters: - name: member_of in: query description: "`me` scopes the result to teams the caller belongs to." schema: { type: string, enum: [me] } responses: "200": description: Teams content: application/json: schema: { $ref: "#/components/schemas/TeamList" } default: { $ref: "#/components/responses/Error" } post: operationId: createTeam summary: Create a team (admin) requestBody: required: true content: application/json: schema: type: object properties: name: { type: string } required: [name] responses: "201": description: Created team content: application/json: schema: { $ref: "#/components/schemas/Team" } default: { $ref: "#/components/responses/Error" } /teams/{teamId}: get: operationId: getTeam summary: Get a team parameters: - { name: teamId, in: path, required: true, schema: { type: string } } responses: "200": description: Team content: application/json: schema: { $ref: "#/components/schemas/Team" } default: { $ref: "#/components/responses/Error" } delete: operationId: deleteTeam summary: Delete a team (admin) parameters: - { name: teamId, in: path, required: true, schema: { type: string } } responses: "204": { description: Deleted } default: { $ref: "#/components/responses/Error" } /teams/{teamId}/members: get: operationId: listTeamMembers summary: List team members parameters: - { name: teamId, in: path, required: true, schema: { type: string } } responses: "200": description: Members content: application/json: schema: { $ref: "#/components/schemas/TeamMemberList" } default: { $ref: "#/components/responses/Error" } post: operationId: addTeamMember summary: Add a team member (admin) parameters: - { name: teamId, in: path, required: true, schema: { type: string } } requestBody: required: true content: application/json: schema: type: object properties: user_id: { type: string } role: { type: string } cloud_identity: { type: string } required: [user_id, role] responses: "201": description: Added member content: application/json: schema: { $ref: "#/components/schemas/TeamMember" } default: { $ref: "#/components/responses/Error" } /teams/{teamId}/members/{userId}: put: operationId: updateTeamMember summary: Update a team member (admin) parameters: - { name: teamId, in: path, required: true, schema: { type: string } } - { name: userId, in: path, required: true, schema: { type: string } } requestBody: required: true content: application/json: schema: type: object properties: role: { type: string } cloud_identity: { type: string } required: [role] responses: "200": description: Updated member content: application/json: schema: { $ref: "#/components/schemas/TeamMember" } default: { $ref: "#/components/responses/Error" } delete: operationId: removeTeamMember summary: Remove a team member (admin) parameters: - { name: teamId, in: path, required: true, schema: { type: string } } - { name: userId, in: path, required: true, schema: { type: string } } responses: "204": { description: Removed } default: { $ref: "#/components/responses/Error" } /accounts: get: operationId: listAccounts summary: List AWS accounts parameters: - { name: page, in: query, schema: { type: integer } } - { name: per_page, in: query, schema: { type: integer } } responses: "200": description: Accounts content: application/json: schema: { $ref: "#/components/schemas/AccountList" } default: { $ref: "#/components/responses/Error" } post: operationId: createAccount summary: Register an AWS account (admin) requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/CreateAccountRequest" } responses: "201": description: Created account content: application/json: schema: { $ref: "#/components/schemas/Account" } default: { $ref: "#/components/responses/Error" } /accounts/{accountId}: get: operationId: getAccount summary: Get an account parameters: - { name: accountId, in: path, required: true, schema: { type: string } } responses: "200": description: Account content: application/json: schema: { $ref: "#/components/schemas/Account" } default: { $ref: "#/components/responses/Error" } put: operationId: updateAccount summary: Update an account (admin) parameters: - { name: accountId, in: path, required: true, schema: { type: string } } requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/UpdateAccountRequest" } responses: "200": description: Updated account content: application/json: schema: { $ref: "#/components/schemas/Account" } default: { $ref: "#/components/responses/Error" } delete: operationId: deleteAccount summary: Delete an account (admin) parameters: - { name: accountId, in: path, required: true, schema: { type: string } } responses: "204": { description: Deleted } default: { $ref: "#/components/responses/Error" } /clusters: get: operationId: listClusters summary: List clusters parameters: - { name: page, in: query, schema: { type: integer } } - { name: per_page, in: query, schema: { type: integer } } - { name: account_id, in: query, schema: { type: string } } responses: "200": description: Clusters content: application/json: schema: { $ref: "#/components/schemas/ClusterList" } default: { $ref: "#/components/responses/Error" } post: operationId: createCluster summary: Register a cluster (admin) requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/CreateClusterRequest" } responses: "201": description: Created cluster content: application/json: schema: { $ref: "#/components/schemas/Cluster" } default: { $ref: "#/components/responses/Error" } /clusters/{clusterId}: get: operationId: getCluster summary: Get a cluster parameters: - { name: clusterId, in: path, required: true, schema: { type: string } } responses: "200": description: Cluster content: application/json: schema: { $ref: "#/components/schemas/Cluster" } default: { $ref: "#/components/responses/Error" } put: operationId: updateCluster summary: Update a cluster (admin) parameters: - { name: clusterId, in: path, required: true, schema: { type: string } } requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/UpdateClusterRequest" } responses: "200": description: Updated cluster content: application/json: schema: { $ref: "#/components/schemas/Cluster" } default: { $ref: "#/components/responses/Error" } delete: operationId: deleteCluster summary: Delete a cluster (admin) parameters: - { name: clusterId, in: path, required: true, schema: { type: string } } responses: "204": { description: Deleted } default: { $ref: "#/components/responses/Error" } /clusters/{clusterId}/test-connection: post: operationId: testClusterConnection summary: Re-run the async connection test (admin) parameters: - { name: clusterId, in: path, required: true, schema: { type: string } } responses: "202": description: Connection test enqueued content: application/json: schema: type: object properties: status: { type: string } required: [status] default: { $ref: "#/components/responses/Error" } /cluster-orders: get: operationId: listClusterOrders summary: Recent cluster operations across the org responses: "200": description: Cluster operations, newest first content: application/json: schema: { $ref: "#/components/schemas/ClusterOperationList" } default: { $ref: "#/components/responses/Error" } post: operationId: provisionCluster summary: Order a cluster vend (admin) description: > Templates an eks-fleet Cluster CR from the input and commits it to the clusters GitOps repo; the vended cluster auto-registers when it comes up. requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/ClusterOrderInput" } responses: "202": description: Provision enqueued content: application/json: schema: { $ref: "#/components/schemas/ClusterOperation" } default: { $ref: "#/components/responses/Error" } /cluster-orders/{environment}/{name}: delete: operationId: deprovisionCluster summary: Order a cluster teardown (admin) parameters: - { name: environment, in: path, required: true, schema: { type: string } } - { name: name, in: path, required: true, schema: { type: string } } - { name: team, in: query, required: true, schema: { type: string } } responses: "202": description: Deprovision enqueued content: application/json: schema: { $ref: "#/components/schemas/ClusterOperation" } default: { $ref: "#/components/responses/Error" } /cluster-orders/{environment}/{name}/operations: get: operationId: listClusterOrderOperations summary: Vend log for one cluster, newest first parameters: - { name: environment, in: path, required: true, schema: { type: string } } - { name: name, in: path, required: true, schema: { type: string } } responses: "200": description: Cluster operations content: application/json: schema: { $ref: "#/components/schemas/ClusterOperationList" } default: { $ref: "#/components/responses/Error" } /cluster-orders/{environment}/{name}/unwedge: post: operationId: unwedgeCluster summary: Break-glass teardown for a wedged spoke (owner) description: > Tears the spoke's tagged AWS resources down directly and drops the Workspace finalizers. No-op unless the Workspace is genuinely wedged and already marked for deletion. parameters: - { name: environment, in: path, required: true, schema: { type: string } } - { name: name, in: path, required: true, schema: { type: string } } - { name: team, in: query, required: true, schema: { type: string } } responses: "202": description: Unwedge enqueued content: application/json: schema: { $ref: "#/components/schemas/ClusterOperation" } default: { $ref: "#/components/responses/Error" } /ops/feed: get: operationId: getOpsFeed summary: Org-wide operations feed (admin) description: Cluster vends and tenant deploys merged into one activity stream, newest first. responses: "200": description: Feed items content: application/json: schema: { $ref: "#/components/schemas/OpsFeedItemList" } default: { $ref: "#/components/responses/Error" } /templates: get: operationId: listTemplates summary: List tenant templates responses: "200": description: Templates (team-filtered for non-admins) content: application/json: schema: { $ref: "#/components/schemas/TemplateList" } default: { $ref: "#/components/responses/Error" } post: operationId: createTemplate summary: Create a template (admin) requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/CreateTemplateRequest" } responses: "201": description: Created template content: application/json: schema: { $ref: "#/components/schemas/Template" } default: { $ref: "#/components/responses/Error" } /templates/{templateID}: get: operationId: getTemplate summary: Get a template parameters: - { name: templateID, in: path, required: true, schema: { type: string } } responses: "200": description: Template content: application/json: schema: { $ref: "#/components/schemas/Template" } default: { $ref: "#/components/responses/Error" } put: operationId: updateTemplate summary: Update a template (admin) parameters: - { name: templateID, in: path, required: true, schema: { type: string } } requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/UpdateTemplateRequest" } responses: "200": description: Updated template content: application/json: schema: { $ref: "#/components/schemas/Template" } default: { $ref: "#/components/responses/Error" } delete: operationId: deleteTemplate summary: Delete a template (admin) parameters: - { name: templateID, in: path, required: true, schema: { type: string } } responses: "204": { description: Deleted } default: { $ref: "#/components/responses/Error" } /templates/{templateID}/access: get: operationId: listTemplateAccess summary: List team-access grants on a template parameters: - { name: templateID, in: path, required: true, schema: { type: string } } responses: "200": description: Grants content: application/json: schema: { $ref: "#/components/schemas/TemplateTeamAccessList" } default: { $ref: "#/components/responses/Error" } post: operationId: grantTemplateAccess summary: Grant a team access to a template (admin) parameters: - { name: templateID, in: path, required: true, schema: { type: string } } requestBody: required: true content: application/json: schema: type: object properties: team_id: { type: string } required: [team_id] responses: "201": description: Grant content: application/json: schema: { $ref: "#/components/schemas/TemplateTeamAccess" } default: { $ref: "#/components/responses/Error" } /templates/{templateID}/access/{teamId}: delete: operationId: revokeTemplateAccess summary: Revoke a team's template access (admin) parameters: - { name: templateID, in: path, required: true, schema: { type: string } } - { name: teamId, in: path, required: true, schema: { type: string } } responses: "204": { description: Revoked } default: { $ref: "#/components/responses/Error" } /tenants: get: operationId: listTenants summary: List tenants (watcher-observed inventory) parameters: - { name: page, in: query, schema: { type: integer } } - { name: per_page, in: query, schema: { type: integer } } - { name: cluster_id, in: query, schema: { type: string } } responses: "200": description: Tenants (team-filtered for non-admins) content: application/json: schema: { $ref: "#/components/schemas/TenantList" } default: { $ref: "#/components/responses/Error" } post: operationId: createTenant summary: Request a tenant deploy (operator) description: > Enqueues a tenant_operation of kind=create; the tenant row appears once ArgoCD applies the commit and the watcher observes the CR. requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/CreateTenantRequest" } responses: "202": description: Create enqueued content: application/json: schema: { $ref: "#/components/schemas/TenantOperation" } default: { $ref: "#/components/responses/Error" } /tenants/{tenantId}: get: operationId: getTenant summary: Get a tenant parameters: - { name: tenantId, in: path, required: true, schema: { type: string } } responses: "200": description: Tenant content: application/json: schema: { $ref: "#/components/schemas/Tenant" } default: { $ref: "#/components/responses/Error" } delete: operationId: deleteTenant summary: Request a tenant delete (operator) parameters: - { name: tenantId, in: path, required: true, schema: { type: string } } responses: "202": description: Delete enqueued content: application/json: schema: { $ref: "#/components/schemas/TenantOperation" } default: { $ref: "#/components/responses/Error" } /tenants/{tenantId}/operations: get: operationId: listTenantOperations summary: Per-tenant operation log parameters: - { name: tenantId, in: path, required: true, schema: { type: string } } responses: "200": description: Operations content: application/json: schema: { $ref: "#/components/schemas/TenantOperationList" } default: { $ref: "#/components/responses/Error" } /tenants/{tenantId}/access: get: operationId: listTenantAccess summary: List team-access grants on a tenant parameters: - { name: tenantId, in: path, required: true, schema: { type: string } } responses: "200": description: Grants content: application/json: schema: { $ref: "#/components/schemas/TenantTeamAccessList" } default: { $ref: "#/components/responses/Error" } post: operationId: grantTenantAccess summary: Grant a team access to a tenant (admin) parameters: - { name: tenantId, in: path, required: true, schema: { type: string } } requestBody: required: true content: application/json: schema: type: object properties: team_id: { type: string } required: [team_id] responses: "201": description: Grant content: application/json: schema: { $ref: "#/components/schemas/TenantTeamAccess" } default: { $ref: "#/components/responses/Error" } /tenants/{tenantId}/access/{teamId}: delete: operationId: revokeTenantAccess summary: Revoke a team's tenant access (admin) parameters: - { name: tenantId, in: path, required: true, schema: { type: string } } - { name: teamId, in: path, required: true, schema: { type: string } } responses: "204": { description: Revoked } default: { $ref: "#/components/responses/Error" } /pipelines: get: operationId: listPipelines summary: List pipelines responses: "200": description: Pipelines content: application/json: schema: { $ref: "#/components/schemas/PipelineList" } default: { $ref: "#/components/responses/Error" } post: operationId: createPipeline summary: Create a pipeline (operator) requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/CreatePipelineRequest" } responses: "201": description: Created pipeline content: application/json: schema: { $ref: "#/components/schemas/Pipeline" } default: { $ref: "#/components/responses/Error" } /pipelines/{pipelineId}: get: operationId: getPipeline summary: Get a pipeline with its stages parameters: - { name: pipelineId, in: path, required: true, schema: { type: string } } responses: "200": description: Pipeline detail content: application/json: schema: { $ref: "#/components/schemas/PipelineDetailResponse" } default: { $ref: "#/components/responses/Error" } put: operationId: updatePipeline summary: Update a pipeline (operator) parameters: - { name: pipelineId, in: path, required: true, schema: { type: string } } requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/UpdatePipelineRequest" } responses: "200": description: Updated pipeline content: application/json: schema: { $ref: "#/components/schemas/Pipeline" } default: { $ref: "#/components/responses/Error" } delete: operationId: deletePipeline summary: Delete a pipeline (admin) parameters: - { name: pipelineId, in: path, required: true, schema: { type: string } } responses: "204": { description: Deleted } default: { $ref: "#/components/responses/Error" } /pipelines/{pipelineId}/runs: get: operationId: listPipelineRuns summary: List pipeline runs parameters: - { name: pipelineId, in: path, required: true, schema: { type: string } } - { name: page, in: query, schema: { type: integer } } - { name: per_page, in: query, schema: { type: integer } } responses: "200": description: Pipeline runs content: application/json: schema: { $ref: "#/components/schemas/PipelineRunList" } default: { $ref: "#/components/responses/Error" } post: operationId: startPipelineRun summary: Start a pipeline run (operator) parameters: - { name: pipelineId, in: path, required: true, schema: { type: string } } responses: "201": description: Started run content: application/json: schema: { $ref: "#/components/schemas/PipelineRun" } default: { $ref: "#/components/responses/Error" } /pipelines/{pipelineId}/runs/{runId}: get: operationId: getPipelineRun summary: Get a pipeline run with its stage progress parameters: - { name: pipelineId, in: path, required: true, schema: { type: string } } - { name: runId, in: path, required: true, schema: { type: string } } responses: "200": description: Pipeline run detail content: application/json: schema: { $ref: "#/components/schemas/PipelineRunDetailResponse" } default: { $ref: "#/components/responses/Error" } /pipelines/{pipelineId}/runs/{runId}/cancel: post: operationId: cancelPipelineRun summary: Cancel a pipeline run (operator) parameters: - { name: pipelineId, in: path, required: true, schema: { type: string } } - { name: runId, in: path, required: true, schema: { type: string } } responses: "200": description: Cancelled run content: application/json: schema: { $ref: "#/components/schemas/PipelineRun" } default: { $ref: "#/components/responses/Error" } /pipelines/{pipelineId}/variables: get: operationId: listPipelineVariables summary: List pipeline variables parameters: - { name: pipelineId, in: path, required: true, schema: { type: string } } responses: "200": description: Pipeline variables (sensitive values redacted) content: application/json: schema: { $ref: "#/components/schemas/PipelineVariableList" } default: { $ref: "#/components/responses/Error" } post: operationId: createPipelineVariable summary: Create a pipeline variable (operator) parameters: - { name: pipelineId, in: path, required: true, schema: { type: string } } requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/CreateVariableRequest" } responses: "201": description: Created variable content: application/json: schema: { $ref: "#/components/schemas/PipelineVariable" } default: { $ref: "#/components/responses/Error" } /pipelines/{pipelineId}/variables/{variableId}: put: operationId: updatePipelineVariable summary: Update a pipeline variable (operator) parameters: - { name: pipelineId, in: path, required: true, schema: { type: string } } - { name: variableId, in: path, required: true, schema: { type: string } } requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/CreateVariableRequest" } responses: "200": description: Updated variable content: application/json: schema: { $ref: "#/components/schemas/PipelineVariable" } default: { $ref: "#/components/responses/Error" } delete: operationId: deletePipelineVariable summary: Delete a pipeline variable (operator) parameters: - { name: pipelineId, in: path, required: true, schema: { type: string } } - { name: variableId, in: path, required: true, schema: { type: string } } responses: "204": { description: Deleted } default: { $ref: "#/components/responses/Error" } /pipelines/{pipelineId}/variables/{variableId}/value: get: operationId: revealPipelineVariableValue summary: Reveal a pipeline variable's decrypted value (operator) parameters: - { name: pipelineId, in: path, required: true, schema: { type: string } } - { name: variableId, in: path, required: true, schema: { type: string } } responses: "200": description: Decrypted value content: application/json: schema: { $ref: "#/components/schemas/VariableValue" } default: { $ref: "#/components/responses/Error" } /workspaces: get: operationId: listWorkspaces summary: List workspaces parameters: - { name: page, in: query, schema: { type: integer } } - { name: per_page, in: query, schema: { type: integer } } - { name: search, in: query, schema: { type: string } } - { name: environment, in: query, schema: { type: string } } responses: "200": description: Workspaces with last-run rollups content: application/json: schema: { $ref: "#/components/schemas/WorkspaceList" } default: { $ref: "#/components/responses/Error" } post: operationId: createWorkspace summary: Create a workspace requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/CreateWorkspaceRequest" } responses: "201": description: Created workspace content: application/json: schema: { $ref: "#/components/schemas/Workspace" } default: { $ref: "#/components/responses/Error" } /workspaces/{workspaceId}: get: operationId: getWorkspace summary: Get a workspace parameters: - { name: workspaceId, in: path, required: true, schema: { type: string } } responses: "200": description: Workspace content: application/json: schema: { $ref: "#/components/schemas/Workspace" } "404": description: Not found content: application/json: schema: { $ref: "#/components/schemas/ErrorResponse" } default: { $ref: "#/components/responses/Error" } put: operationId: updateWorkspace summary: Update a workspace parameters: - { name: workspaceId, in: path, required: true, schema: { type: string } } requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/UpdateWorkspaceRequest" } responses: "200": description: Updated workspace content: application/json: schema: { $ref: "#/components/schemas/Workspace" } default: { $ref: "#/components/responses/Error" } delete: operationId: deleteWorkspace summary: Delete a workspace (admin) parameters: - { name: workspaceId, in: path, required: true, schema: { type: string } } responses: "204": { description: Deleted } default: { $ref: "#/components/responses/Error" } /workspaces/{workspaceId}/upload: post: operationId: uploadWorkspaceConfig summary: Upload a configuration archive (operator) description: > Multipart upload (50 MB limit) for source=upload workspaces; the UI sends it with a raw fetch rather than the typed client. parameters: - { name: workspaceId, in: path, required: true, schema: { type: string } } requestBody: required: true content: multipart/form-data: schema: type: object properties: file: type: string format: binary required: [file] responses: "200": description: Workspace with the new config version content: application/json: schema: { $ref: "#/components/schemas/Workspace" } default: { $ref: "#/components/responses/Error" } /workspaces/{workspaceId}/clone: post: operationId: cloneWorkspace summary: Clone a workspace with its variables (operator) parameters: - { name: workspaceId, in: path, required: true, schema: { type: string } } requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/CloneWorkspaceRequest" } responses: "201": description: Cloned workspace content: application/json: schema: { $ref: "#/components/schemas/Workspace" } default: { $ref: "#/components/responses/Error" } /workspaces/{workspaceId}/lock: post: operationId: lockWorkspace summary: Lock a workspace (operator) parameters: - { name: workspaceId, in: path, required: true, schema: { type: string } } responses: "200": description: Locked workspace content: application/json: schema: { $ref: "#/components/schemas/Workspace" } default: { $ref: "#/components/responses/Error" } /workspaces/{workspaceId}/unlock: post: operationId: unlockWorkspace summary: Unlock a workspace (operator) parameters: - { name: workspaceId, in: path, required: true, schema: { type: string } } responses: "200": description: Unlocked workspace content: application/json: schema: { $ref: "#/components/schemas/Workspace" } default: { $ref: "#/components/responses/Error" } /workspaces/{workspaceId}/variables: get: operationId: listWorkspaceVariables summary: List workspace variables parameters: - { name: workspaceId, in: path, required: true, schema: { type: string } } responses: "200": description: Variables (sensitive values redacted) content: application/json: schema: { $ref: "#/components/schemas/WorkspaceVariableList" } default: { $ref: "#/components/responses/Error" } post: operationId: createWorkspaceVariable summary: Create a workspace variable parameters: - { name: workspaceId, in: path, required: true, schema: { type: string } } requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/CreateVariableRequest" } responses: "201": description: Created variable content: application/json: schema: { $ref: "#/components/schemas/WorkspaceVariable" } default: { $ref: "#/components/responses/Error" } /workspaces/{workspaceId}/variables/effective: get: operationId: listEffectiveVariables summary: Merged org → pipeline → workspace variable view parameters: - { name: workspaceId, in: path, required: true, schema: { type: string } } - { name: pipeline_id, in: query, schema: { type: string } } responses: "200": description: Effective variables content: application/json: schema: type: array items: { $ref: "#/components/schemas/EffectiveVariable" } default: { $ref: "#/components/responses/Error" } /workspaces/{workspaceId}/variables/discover: post: operationId: discoverWorkspaceVariables summary: Discover the config's variable surface description: > Parses the staged config (terragrunt render + variables.tf, or plain tofu) and returns every module variable with its configured state. Synchronous; intentionally not list-enveloped. parameters: - { name: workspaceId, in: path, required: true, schema: { type: string } } responses: "200": description: Discovered variables content: application/json: schema: type: array items: { $ref: "#/components/schemas/DiscoveredVariable" } default: { $ref: "#/components/responses/Error" } /workspaces/{workspaceId}/variables/bulk: post: operationId: bulkCreateWorkspaceVariables summary: Create up to 50 variables in one call parameters: - { name: workspaceId, in: path, required: true, schema: { type: string } } requestBody: required: true content: application/json: schema: type: object properties: variables: type: array items: { $ref: "#/components/schemas/CreateVariableRequest" } required: [variables] responses: "201": description: Created variables content: application/json: schema: type: array items: { $ref: "#/components/schemas/WorkspaceVariable" } default: { $ref: "#/components/responses/Error" } /workspaces/{workspaceId}/variables/import-outputs: post: operationId: importWorkspaceOutputs summary: Import another workspace's outputs as variables parameters: - { name: workspaceId, in: path, required: true, schema: { type: string } } requestBody: required: true content: application/json: schema: type: object properties: source_workspace_id: { type: string } required: [source_workspace_id] responses: "201": description: Imported variables content: application/json: schema: type: array items: { $ref: "#/components/schemas/WorkspaceVariable" } default: { $ref: "#/components/responses/Error" } /workspaces/{workspaceId}/variables/copy: post: operationId: copyWorkspaceVariables summary: Copy another workspace's variables into this one parameters: - { name: workspaceId, in: path, required: true, schema: { type: string } } requestBody: required: true content: application/json: schema: type: object properties: source_workspace_id: { type: string } required: [source_workspace_id] responses: "201": description: Copied variables content: application/json: schema: type: array items: { $ref: "#/components/schemas/WorkspaceVariable" } default: { $ref: "#/components/responses/Error" } /workspaces/{workspaceId}/variables/{variableId}: put: operationId: updateWorkspaceVariable summary: Update a workspace variable parameters: - { name: workspaceId, in: path, required: true, schema: { type: string } } - { name: variableId, in: path, required: true, schema: { type: string } } requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/CreateVariableRequest" } responses: "200": description: Updated variable content: application/json: schema: { $ref: "#/components/schemas/WorkspaceVariable" } default: { $ref: "#/components/responses/Error" } delete: operationId: deleteWorkspaceVariable summary: Delete a workspace variable parameters: - { name: workspaceId, in: path, required: true, schema: { type: string } } - { name: variableId, in: path, required: true, schema: { type: string } } responses: "204": { description: Deleted } default: { $ref: "#/components/responses/Error" } /workspaces/{workspaceId}/variables/{variableId}/value: get: operationId: revealWorkspaceVariableValue summary: Reveal a workspace variable's decrypted value (operator) parameters: - { name: workspaceId, in: path, required: true, schema: { type: string } } - { name: variableId, in: path, required: true, schema: { type: string } } responses: "200": description: Decrypted value content: application/json: schema: { $ref: "#/components/schemas/VariableValue" } default: { $ref: "#/components/responses/Error" } /workspaces/{workspaceId}/state: get: operationId: listStateVersions summary: List state versions parameters: - { name: workspaceId, in: path, required: true, schema: { type: string } } - { name: page, in: query, schema: { type: integer } } - { name: per_page, in: query, schema: { type: integer } } responses: "200": description: State versions content: application/json: schema: { $ref: "#/components/schemas/StateVersionList" } default: { $ref: "#/components/responses/Error" } /workspaces/{workspaceId}/state/current: get: operationId: getCurrentStateVersion summary: The latest state version parameters: - { name: workspaceId, in: path, required: true, schema: { type: string } } responses: "200": description: Latest state version content: application/json: schema: { $ref: "#/components/schemas/StateVersion" } "404": description: No state yet content: application/json: schema: { $ref: "#/components/schemas/ErrorResponse" } default: { $ref: "#/components/responses/Error" } /workspaces/{workspaceId}/state/current/resources: get: operationId: listCurrentStateResources summary: Parsed resources from the latest state parameters: - { name: workspaceId, in: path, required: true, schema: { type: string } } responses: "200": description: Resources content: application/json: schema: type: array items: { $ref: "#/components/schemas/StateResource" } default: { $ref: "#/components/responses/Error" } /workspaces/{workspaceId}/state/current/outputs: get: operationId: listCurrentStateOutputs summary: Parsed outputs from the latest state parameters: - { name: workspaceId, in: path, required: true, schema: { type: string } } responses: "200": description: Outputs content: application/json: schema: type: array items: { $ref: "#/components/schemas/StateOutput" } default: { $ref: "#/components/responses/Error" } /workspaces/{workspaceId}/state/diff: get: operationId: diffStateVersions summary: Diff two state versions by serial parameters: - { name: workspaceId, in: path, required: true, schema: { type: string } } - { name: from, in: query, required: true, schema: { type: integer } } - { name: to, in: query, required: true, schema: { type: integer } } responses: "200": description: State diff content: application/json: schema: { $ref: "#/components/schemas/StateDiff" } default: { $ref: "#/components/responses/Error" } /workspaces/{workspaceId}/state/{stateId}: get: operationId: getStateVersion summary: Get a state version parameters: - { name: workspaceId, in: path, required: true, schema: { type: string } } - { name: stateId, in: path, required: true, schema: { type: string } } responses: "200": description: State version content: application/json: schema: { $ref: "#/components/schemas/StateVersion" } default: { $ref: "#/components/responses/Error" } /workspaces/{workspaceId}/state/{stateId}/download: get: operationId: downloadStateVersion summary: Download the raw tfstate blob parameters: - { name: workspaceId, in: path, required: true, schema: { type: string } } - { name: stateId, in: path, required: true, schema: { type: string } } responses: "200": description: The tfstate document (attachment). content: application/json: schema: type: object additionalProperties: true default: { $ref: "#/components/responses/Error" } /workspaces/{workspaceId}/state/serial/{serial}: delete: operationId: deleteStateVersion summary: Drop one state version + its S3 objects (admin) parameters: - { name: workspaceId, in: path, required: true, schema: { type: string } } - { name: serial, in: path, required: true, schema: { type: integer } } responses: "204": { description: Deleted (row + storage) } "200": description: Row deleted, storage cleanup failed (detail attached) content: application/json: schema: type: object properties: deleted: { $ref: "#/components/schemas/StateVersion" } storage_error: { type: string } required: [deleted, storage_error] default: { $ref: "#/components/responses/Error" } /workspaces/{workspaceId}/access: get: operationId: listWorkspaceAccess summary: List team-access grants on a workspace parameters: - { name: workspaceId, in: path, required: true, schema: { type: string } } responses: "200": description: Grants content: application/json: schema: { $ref: "#/components/schemas/WorkspaceTeamAccessList" } default: { $ref: "#/components/responses/Error" } post: operationId: setWorkspaceAccess summary: Grant/replace a team's workspace access (admin) parameters: - { name: workspaceId, in: path, required: true, schema: { type: string } } requestBody: required: true content: application/json: schema: type: object properties: team_id: { type: string } role: { type: string } required: [team_id, role] responses: "201": description: Grant content: application/json: schema: { $ref: "#/components/schemas/WorkspaceTeamAccess" } default: { $ref: "#/components/responses/Error" } /workspaces/{workspaceId}/access/{teamId}: delete: operationId: removeWorkspaceAccess summary: Revoke a team's workspace access (admin) parameters: - { name: workspaceId, in: path, required: true, schema: { type: string } } - { name: teamId, in: path, required: true, schema: { type: string } } responses: "204": { description: Revoked } default: { $ref: "#/components/responses/Error" } /workspaces/{workspaceId}/runs: get: operationId: listRuns summary: List a workspace's runs parameters: - { name: workspaceId, in: path, required: true, schema: { type: string } } - { name: page, in: query, schema: { type: integer } } - { name: per_page, in: query, schema: { type: integer } } responses: "200": description: Runs content: application/json: schema: { $ref: "#/components/schemas/RunList" } default: { $ref: "#/components/responses/Error" } post: operationId: createRun summary: Create a run (role elevates per operation) parameters: - { name: workspaceId, in: path, required: true, schema: { type: string } } requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/CreateRunRequest" } responses: "201": description: Created run content: application/json: schema: { $ref: "#/components/schemas/Run" } default: { $ref: "#/components/responses/Error" } /workspaces/{workspaceId}/runs/{runId}: get: operationId: getRun summary: Get a run parameters: - { name: workspaceId, in: path, required: true, schema: { type: string } } - { name: runId, in: path, required: true, schema: { type: string } } responses: "200": description: Run content: application/json: schema: { $ref: "#/components/schemas/Run" } default: { $ref: "#/components/responses/Error" } /workspaces/{workspaceId}/runs/{runId}/plan-json: get: operationId: getRunPlanJSON summary: The run's machine-readable plan parameters: - { name: workspaceId, in: path, required: true, schema: { type: string } } - { name: runId, in: path, required: true, schema: { type: string } } responses: "200": description: tofu show -json output content: application/json: schema: { $ref: "#/components/schemas/TofuPlanJSON" } default: { $ref: "#/components/responses/Error" } /workspaces/{workspaceId}/runs/{runId}/logs/ws: get: operationId: streamRunLogs summary: Live run log stream (WebSocket upgrade) description: > Not a JSON operation: the connection upgrades to a WebSocket after the org-scope check. Clients authenticate by requesting the subprotocol pair ["bearer", ]; messages are text log lines. Documented here for completeness — the typed client does not call it. parameters: - { name: workspaceId, in: path, required: true, schema: { type: string } } - { name: runId, in: path, required: true, schema: { type: string } } responses: "101": { description: Switching to the WebSocket protocol. } default: { $ref: "#/components/responses/Error" } /workspaces/{workspaceId}/runs/{runId}/cancel: post: operationId: cancelRun summary: Cancel a run (operator) parameters: - { name: workspaceId, in: path, required: true, schema: { type: string } } - { name: runId, in: path, required: true, schema: { type: string } } responses: "200": description: Cancelled run content: application/json: schema: { $ref: "#/components/schemas/Run" } default: { $ref: "#/components/responses/Error" } /workspaces/{workspaceId}/runs/{runId}/approvals: get: operationId: listApprovals summary: List a run's approvals parameters: - { name: workspaceId, in: path, required: true, schema: { type: string } } - { name: runId, in: path, required: true, schema: { type: string } } responses: "200": description: Approvals content: application/json: schema: { $ref: "#/components/schemas/ApprovalList" } default: { $ref: "#/components/responses/Error" } post: operationId: createApproval summary: Approve or reject a gated apply parameters: - { name: workspaceId, in: path, required: true, schema: { type: string } } - { name: runId, in: path, required: true, schema: { type: string } } requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/ApprovalRequest" } responses: "201": description: Recorded decision content: application/json: schema: { $ref: "#/components/schemas/Approval" } default: { $ref: "#/components/responses/Error" } components: securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: JWT responses: Error: description: The standard error envelope (respond.Error / respond.FromError). content: application/json: schema: { $ref: "#/components/schemas/ErrorResponse" } schemas: ErrorResponse: type: object properties: error: { type: string, description: "HTTP status text." } message: { type: string } request_id: { type: string, description: "Set on 5xx responses so logs can be correlated." } required: [error] HealthStatus: type: object properties: status: { type: string } services: type: object additionalProperties: { type: string } dev_login: type: boolean description: Present (true) only in development, where the dev-login route exists. required: [status] VariableValue: type: object properties: value: { type: string } required: [value] User: type: object properties: id: { type: string } org_id: { type: string } email: { type: string } name: { type: string } avatar_url: { type: string } github_id: type: [integer, "null"] description: GitHub account id for OAuth-provisioned users; null for dev users. role: { type: string, enum: [owner, admin, operator, viewer] } last_login_at: { type: string, format: date-time } created_at: { type: string, format: date-time } updated_at: { type: string, format: date-time } required: [id, org_id, email, name, role, created_at, updated_at] UpdateRoleRequest: type: object properties: role: { type: string, enum: [owner, admin, operator, viewer] } required: [role] Workspace: type: object properties: id: { type: string } org_id: { type: string } name: { type: string } description: { type: string } source: { type: string, enum: [vcs, upload] } repo_url: { type: string } repo_branch: { type: string } working_dir: { type: string } tofu_version: { type: string } environment: { type: string, enum: [development, staging, production] } auto_apply: { type: boolean } requires_approval: { type: boolean } vcs_trigger_enabled: { type: boolean } locked: { type: boolean } locked_by: { type: [string, "null"] } current_run_id: { type: [string, "null"] } current_config_version_id: { type: string } created_by: { type: string } created_at: { type: string, format: date-time } updated_at: { type: string, format: date-time } last_run_status: { type: [string, "null"], description: "List-view rollup." } last_run_at: { type: [string, "null"], format: date-time, description: "List-view rollup." } resource_count: { type: integer, description: "List-view rollup." } required: [id, org_id, name, source, repo_url, repo_branch, working_dir, tofu_version, environment, auto_apply, requires_approval, vcs_trigger_enabled, locked, created_by, created_at, updated_at] CreateWorkspaceRequest: type: object properties: name: { type: string } description: { type: string } source: { type: string, enum: [vcs, upload] } repo_url: { type: string } repo_branch: { type: string } working_dir: { type: string } tofu_version: { type: string } environment: { type: string, enum: [development, staging, production] } auto_apply: { type: boolean } requires_approval: { type: boolean } vcs_trigger_enabled: { type: boolean } required: [name] UpdateWorkspaceRequest: type: object properties: name: { type: string } description: { type: string } repo_url: { type: string } repo_branch: { type: string } working_dir: { type: string } tofu_version: { type: string } environment: { type: string, enum: [development, staging, production] } auto_apply: { type: boolean } requires_approval: { type: boolean } vcs_trigger_enabled: { type: boolean } CloneWorkspaceRequest: type: object properties: name: { type: string } description: { type: string } environment: { type: string } required: [name] RunStatus: type: string enum: - pending - queued - planning - planned - awaiting_approval - applying - applied - errored - cancelled - discarded RunOperation: type: string enum: [plan, apply, destroy, import, test] Run: type: object properties: id: { type: string } workspace_id: { type: string } org_id: { type: string } operation: { $ref: "#/components/schemas/RunOperation" } status: { $ref: "#/components/schemas/RunStatus" } plan_output: { type: string } plan_log_url: { type: string } apply_log_url: { type: string } resources_added: { type: integer } resources_changed: { type: integer } resources_deleted: { type: integer } error_message: { type: string } commit_sha: { type: string } plan_json_url: { type: string } created_by: { type: string } started_at: { type: [string, "null"], format: date-time } finished_at: { type: [string, "null"], format: date-time } created_at: { type: string, format: date-time } updated_at: { type: string, format: date-time } required: [id, workspace_id, org_id, operation, status, created_by, created_at, updated_at] CreateRunRequest: type: object properties: operation: { $ref: "#/components/schemas/RunOperation" } imports: type: array items: type: object properties: address: { type: string } id: { type: string } required: [address, id] TofuResourceChange: type: object properties: address: { type: string } module_address: { type: string } mode: { type: string } type: { type: string } name: { type: string } provider_name: { type: string } change: type: object properties: actions: type: array items: { type: string } before: type: [object, "null"] additionalProperties: true after: type: [object, "null"] additionalProperties: true required: [actions, before, after] required: [address, mode, type, name, provider_name, change] TofuPlanJSON: type: object properties: format_version: { type: string } resource_changes: type: array items: { $ref: "#/components/schemas/TofuResourceChange" } StateVersion: type: object properties: id: { type: string } workspace_id: { type: string } org_id: { type: string } run_id: { type: string } serial: { type: integer } state_url: { type: string } resource_count: { type: integer } resource_summary: { type: string } created_at: { type: string, format: date-time } required: [id, workspace_id, org_id, run_id, serial, state_url, resource_count, resource_summary, created_at] StateResource: type: object properties: type: { type: string } name: { type: string } module: { type: string } provider: { type: string } mode: { type: string } attributes: type: object additionalProperties: true required: [type, name, module, provider, mode, attributes] StateOutput: type: object properties: name: { type: string } value: {} type: { type: string } sensitive: { type: boolean } required: [name, value, type, sensitive] ResourceDiff: type: object properties: type: { type: string } name: { type: string } module: { type: string } action: { type: string, enum: [added, removed, changed, unchanged] } before: type: object additionalProperties: true after: type: object additionalProperties: true changed_keys: type: array items: { type: string } required: [type, name, module, action] StateDiff: type: object properties: added: { type: integer } removed: { type: integer } changed: { type: integer } unchanged: { type: integer } diffs: type: array items: { $ref: "#/components/schemas/ResourceDiff" } required: [added, removed, changed, unchanged, diffs] VariableCategory: type: string enum: [terraform, env] WorkspaceVariable: type: object properties: id: { type: string } workspace_id: { type: string } org_id: { type: string } key: { type: string } value: { type: string } sensitive: { type: boolean } category: { $ref: "#/components/schemas/VariableCategory" } description: { type: string } created_at: { type: string, format: date-time } updated_at: { type: string, format: date-time } required: [id, workspace_id, org_id, key, value, sensitive, category, description, created_at, updated_at] OrgVariable: type: object properties: id: { type: string } org_id: { type: string } key: { type: string } value: { type: string } sensitive: { type: boolean } category: { $ref: "#/components/schemas/VariableCategory" } description: { type: string } created_at: { type: string, format: date-time } updated_at: { type: string, format: date-time } required: [id, org_id, key, value, sensitive, category, description, created_at, updated_at] PipelineVariable: type: object properties: id: { type: string } pipeline_id: { type: string } org_id: { type: string } key: { type: string } value: { type: string } sensitive: { type: boolean } category: { $ref: "#/components/schemas/VariableCategory" } description: { type: string } created_at: { type: string, format: date-time } updated_at: { type: string, format: date-time } required: [id, pipeline_id, org_id, key, value, sensitive, category, description, created_at, updated_at] CreateVariableRequest: type: object properties: key: { type: string } value: { type: string } sensitive: { type: boolean } category: { $ref: "#/components/schemas/VariableCategory" } description: { type: string } required: [key, value, sensitive, category] EffectiveVariable: type: object properties: key: { type: string } value: { type: string } sensitive: { type: boolean } category: { $ref: "#/components/schemas/VariableCategory" } description: { type: string } source: { type: string, enum: [org, pipeline, workspace] } source_id: { type: string } required: [key, value, sensitive, category, description, source, source_id] DiscoveredVariable: type: object properties: name: { type: string } type: { type: string } description: { type: string } default: { type: string } required: { type: boolean } configured: { type: boolean } configured_by: { type: string, enum: [terragrunt, portal] } required: [name, required, configured] Team: type: object properties: id: { type: string } org_id: { type: string } name: { type: string } slug: { type: string } created_at: { type: string, format: date-time } updated_at: { type: string, format: date-time } required: [id, org_id, name, slug, created_at, updated_at] TeamMember: type: object properties: id: { type: string } team_id: { type: string } user_id: { type: string } role: { type: string } cloud_identity: { type: string } created_at: { type: string, format: date-time } email: { type: string } user_name: { type: string } avatar_url: { type: string } required: [id, team_id, user_id, role, cloud_identity, created_at, email, user_name, avatar_url] WorkspaceTeamAccess: type: object properties: id: { type: string } workspace_id: { type: string } team_id: { type: string } role: { type: string } created_at: { type: string, format: date-time } team_name: { type: string } team_slug: { type: string } required: [id, workspace_id, team_id, role, created_at, team_name, team_slug] Approval: type: object properties: id: { type: string } run_id: { type: string } org_id: { type: string } user_id: { type: string } status: { type: string, enum: [approved, rejected] } comment: { type: string } created_at: { type: string, format: date-time } user_name: { type: string } avatar_url: { type: string } required: [id, run_id, org_id, user_id, status, comment, created_at] ApprovalRequest: type: object properties: status: { type: string, enum: [approved, rejected] } comment: { type: string } required: [status] AuditLog: type: object properties: id: { type: string } org_id: { type: string } user_id: { type: string } action: { type: string } entity_type: { type: string } entity_id: { type: string } before_data: {} after_data: {} ip_address: { type: string } user_agent: { type: string } created_at: { type: string, format: date-time } required: [id, org_id, user_id, action, entity_type, entity_id, before_data, after_data, ip_address, user_agent, created_at] Pipeline: type: object properties: id: { type: string } org_id: { type: string } name: { type: string } description: { type: string } created_by: { type: string } created_at: { type: string, format: date-time } updated_at: { type: string, format: date-time } required: [id, org_id, name, description, created_by, created_at, updated_at] OnFailure: type: string enum: [stop, continue] PipelineStage: type: object properties: id: { type: string } pipeline_id: { type: string } workspace_id: { type: string } stage_order: { type: integer } auto_apply: { type: boolean } on_failure: { $ref: "#/components/schemas/OnFailure" } created_at: { type: string, format: date-time } workspace_name: { type: string } required: [id, pipeline_id, workspace_id, stage_order, auto_apply, on_failure, created_at, workspace_name] PipelineRunStatus: type: string enum: [idle, running, completed, errored, cancelled] PipelineRun: type: object properties: id: { type: string } pipeline_id: { type: string } org_id: { type: string } status: { $ref: "#/components/schemas/PipelineRunStatus" } current_stage: { type: integer } total_stages: { type: integer } created_by: { type: string } started_at: { type: string, format: date-time } finished_at: { type: [string, "null"], format: date-time } created_at: { type: string, format: date-time } updated_at: { type: string, format: date-time } required: [id, pipeline_id, org_id, status, current_stage, total_stages, created_by, started_at, created_at, updated_at] PipelineStageStatus: type: string enum: - pending - importing_outputs - running - awaiting_approval - completed - errored - skipped - cancelled PipelineRunStage: type: object properties: id: { type: string } pipeline_run_id: { type: string } stage_id: { type: string } workspace_id: { type: string } run_id: { type: [string, "null"] } stage_order: { type: integer } status: { $ref: "#/components/schemas/PipelineStageStatus" } auto_apply: { type: boolean } on_failure: { $ref: "#/components/schemas/OnFailure" } started_at: { type: [string, "null"], format: date-time } finished_at: { type: [string, "null"], format: date-time } created_at: { type: string, format: date-time } updated_at: { type: string, format: date-time } workspace_name: { type: string } required: [id, pipeline_run_id, stage_id, workspace_id, stage_order, status, auto_apply, on_failure, created_at, updated_at, workspace_name] CreatePipelineStageInput: type: object properties: workspace_id: { type: string } auto_apply: { type: boolean } on_failure: { $ref: "#/components/schemas/OnFailure" } required: [workspace_id, auto_apply, on_failure] CreatePipelineRequest: type: object properties: name: { type: string } description: { type: string } stages: type: array items: { $ref: "#/components/schemas/CreatePipelineStageInput" } required: [name, stages] UpdatePipelineRequest: type: object properties: name: { type: string } description: { type: string } stages: type: array items: { $ref: "#/components/schemas/CreatePipelineStageInput" } PipelineDetailResponse: type: object properties: pipeline: { $ref: "#/components/schemas/Pipeline" } stages: type: array items: { $ref: "#/components/schemas/PipelineStage" } required: [pipeline, stages] PipelineRunDetailResponse: type: object properties: pipeline_run: { $ref: "#/components/schemas/PipelineRun" } stages: type: array items: { $ref: "#/components/schemas/PipelineRunStage" } required: [pipeline_run, stages] Account: type: object properties: id: { type: string } org_id: { type: string } name: { type: string } description: { type: string } aws_account_id: { type: string } assume_role_arn: { type: string } default_region: { type: string } created_by: { type: string } created_at: { type: string, format: date-time } updated_at: { type: string, format: date-time } external_id_set: type: boolean description: Whether an assume-role external id is configured; the value itself never leaves the server. required: [id, org_id, name, description, aws_account_id, assume_role_arn, default_region, created_by, created_at, updated_at, external_id_set] CreateAccountRequest: type: object properties: name: { type: string } description: { type: string } aws_account_id: { type: string } assume_role_arn: { type: string } external_id: { type: string } default_region: { type: string } required: [name, aws_account_id, assume_role_arn, default_region] UpdateAccountRequest: type: object properties: name: { type: string } description: { type: string } assume_role_arn: { type: string } external_id: { type: string } default_region: { type: string } ClusterConnectionStatus: type: string enum: [pending, connecting, connected, failed] Cluster: type: object properties: id: { type: string } org_id: { type: string } account_id: { type: string } name: { type: string } description: { type: string } environment: { type: string, enum: [development, staging, production] } api_endpoint: { type: string } region: { type: string } auth_mode: type: string description: sa_token (stored bearer token) or eks_iam (minted from the account role). eks_cluster_name: { type: string } connection_status: { $ref: "#/components/schemas/ClusterConnectionStatus" } last_connected_at: { type: [string, "null"], format: date-time } connection_error: { type: string } node_count: { type: integer } k8s_version: { type: string } argocd_sync_status: type: string description: Synced | OutOfSync | Unknown | "" (not yet observed). argocd_health_status: type: string description: Healthy | Progressing | Degraded | Missing | ... | "" (not yet observed). control_plane_status: type: string description: ACTIVE | UPDATING | CREATING | DEGRADED | ... | "" (not yet observed). platform_version: type: string description: EKS platform version, e.g. eks.7. last_health_observed_at: { type: [string, "null"], format: date-time } credentials_set: type: boolean description: Whether CA bundle + SA token are stored; the ciphertext never leaves the server. created_by: { type: string } created_at: { type: string, format: date-time } updated_at: { type: string, format: date-time } required: [id, org_id, account_id, name, description, environment, api_endpoint, region, connection_status, last_connected_at, connection_error, node_count, k8s_version, argocd_sync_status, argocd_health_status, control_plane_status, platform_version, last_health_observed_at, credentials_set, created_by, created_at, updated_at] CreateClusterRequest: type: object properties: account_id: { type: string } name: { type: string } description: { type: string } environment: { type: string, enum: [development, staging, production] } api_endpoint: { type: string } ca_bundle: { type: string } sa_token: { type: string } region: { type: string } auth_mode: type: string enum: [sa_token, eks_iam] description: Defaults to sa_token. eks_iam requires eks_cluster_name and an account assume-role. eks_cluster_name: { type: string } required: [account_id, name, api_endpoint, ca_bundle, sa_token] UpdateClusterRequest: type: object properties: name: { type: string } description: { type: string } environment: { type: string } api_endpoint: { type: string } ca_bundle: { type: string } sa_token: { type: string } region: { type: string } ClusterOrderInput: type: object description: > The vend form: portal templates an eks-fleet Cluster CR from it and commits it to the clusters GitOps repo. account is the 12-digit AWS account number (not a portal account id); the eks-fleet defaults cover everything optional. properties: name: { type: string } account: { type: string } region: { type: string } team: { type: string } environment: { type: string, enum: [dev, staging, production] } cluster_version: { type: string } system_nodes: { $ref: "#/components/schemas/ClusterOrderSystemNodes" } network: { $ref: "#/components/schemas/ClusterOrderNetwork" } endpoint_public_access: { type: boolean } endpoint_public_access_cidrs: type: array items: { type: string } required: [name, account, region, team] ClusterOrderSystemNodes: type: object properties: instance_types: type: array items: { type: string } min_size: { type: integer } max_size: { type: integer } desired_size: { type: integer } disk_size: { type: integer } ClusterOrderNetwork: type: object properties: vpc_cidr: { type: string } max_azs: { type: integer } nat_gateways: { type: integer } ClusterOperationKind: type: string enum: [provision, deprovision, unwedge] ClusterOperationStatus: type: string enum: [pending, committed, failed, active, deprovisioned] VendPhaseEntry: type: object properties: at: { type: string, format: date-time } detail: { type: string } required: [at] ClusterOperation: type: object properties: id: { type: string } org_id: { type: string } name: { type: string } environment: { type: string } team: { type: string } operation: { $ref: "#/components/schemas/ClusterOperationKind" } status: { $ref: "#/components/schemas/ClusterOperationStatus" } git_commit_sha: { type: string } error: { type: string } spec_json: type: object additionalProperties: true description: The clusterspec input the CR was templated from. cluster_id: { type: [string, "null"] } created_by: { type: string } created_at: { type: string, format: date-time } completed_at: { type: [string, "null"], format: date-time } vend_phases: type: object additionalProperties: { $ref: "#/components/schemas/VendPhaseEntry" } description: > Regressible map keyed by phase (committed, tofu_running, active, ...). Only portal-side phases advance locally; substrate phases need the in-cluster watcher. required: [id, org_id, name, environment, team, operation, status, git_commit_sha, error, spec_json, cluster_id, created_by, created_at, completed_at, vend_phases] Tenant: type: object properties: id: { type: string } org_id: { type: string } cluster_id: { type: string } name: { type: string } phase: { type: string } spec: type: object additionalProperties: true status: type: object additionalProperties: true last_observed_at: { type: string, format: date-time } created_at: { type: string, format: date-time } updated_at: { type: string, format: date-time } required: [id, org_id, cluster_id, name, phase, spec, status, last_observed_at, created_at, updated_at] CreateTenantRequest: type: object properties: cluster_id: { type: string } name: { type: string } values: type: object additionalProperties: true template_id: type: string description: When set, values are operator overrides merged with the template's defaults. owning_team_id: { type: string } required: [cluster_id, name, values] TenantOperationKind: type: string enum: [create, delete] TenantOperationStatus: type: string enum: [pending, committed, failed] TenantOperation: type: object properties: id: { type: string } org_id: { type: string } cluster_id: { type: string } tenant_name: { type: string } operation: { $ref: "#/components/schemas/TenantOperationKind" } status: { $ref: "#/components/schemas/TenantOperationStatus" } git_commit_sha: { type: string } error: { type: string } values_json: type: object additionalProperties: true template_id: { type: [string, "null"] } created_by: { type: string } created_at: { type: string, format: date-time } completed_at: { type: [string, "null"], format: date-time } required: [id, org_id, cluster_id, tenant_name, operation, status, git_commit_sha, error, values_json, template_id, created_by, created_at, completed_at] TenantTeamAccess: type: object properties: id: { type: string } org_id: { type: string } cluster_id: { type: string } tenant_name: { type: string } team_id: { type: string } granted_by: { type: string } granted_at: { type: string, format: date-time } required: [id, org_id, cluster_id, tenant_name, team_id, granted_by, granted_at] TemplateTeamAccess: type: object properties: id: { type: string } org_id: { type: string } template_id: { type: string } team_id: { type: string } granted_by: { type: string } granted_at: { type: string, format: date-time } required: [id, org_id, template_id, team_id, granted_by, granted_at] Template: type: object properties: id: { type: string } org_id: { type: string } name: { type: string } description: { type: string } persona: { type: string } default_values: type: object additionalProperties: true allowed_overrides: type: array items: { type: string } max_budget_usd: { type: integer } allowed_model_families: type: array items: { type: string } required_compliance: type: array items: { type: string } created_by: { type: string } created_at: { type: string, format: date-time } updated_at: { type: string, format: date-time } required: [id, org_id, name, description, persona, default_values, allowed_overrides, max_budget_usd, allowed_model_families, required_compliance, created_by, created_at, updated_at] CreateTemplateRequest: type: object properties: name: { type: string } description: { type: string } persona: { type: string } default_values: type: object additionalProperties: true allowed_overrides: type: array items: { type: string } max_budget_usd: { type: integer } allowed_model_families: type: array items: { type: string } required_compliance: type: array items: { type: string } required: [name, persona] UpdateTemplateRequest: type: object properties: name: { type: string } description: { type: string } persona: { type: string } default_values: type: object additionalProperties: true allowed_overrides: type: array items: { type: string } max_budget_usd: { type: integer } allowed_model_families: type: array items: { type: string } required_compliance: type: array items: { type: string } OpsFeedItem: type: object description: > One entry in the org-wide operations feed: exactly one of cluster/tenant is set, discriminated by kind. `at` is the activity time the feed is sorted on (completion if finished, else creation). properties: kind: { type: string, enum: [cluster, tenant] } at: { type: string, format: date-time } cluster: { $ref: "#/components/schemas/ClusterOperation" } tenant: { $ref: "#/components/schemas/TenantOperation" } required: [kind, at] UserList: type: object properties: data: type: array items: { $ref: "#/components/schemas/User" } total: { type: integer } page: { type: integer } per_page: { type: integer } required: [data, total, page, per_page] AuditLogList: type: object properties: data: type: array items: { $ref: "#/components/schemas/AuditLog" } total: { type: integer } page: { type: integer } per_page: { type: integer } required: [data, total, page, per_page] OrgVariableList: type: object properties: data: type: array items: { $ref: "#/components/schemas/OrgVariable" } total: { type: integer } page: { type: integer } per_page: { type: integer } required: [data, total, page, per_page] PipelineVariableList: type: object properties: data: type: array items: { $ref: "#/components/schemas/PipelineVariable" } total: { type: integer } page: { type: integer } per_page: { type: integer } required: [data, total, page, per_page] WorkspaceVariableList: type: object properties: data: type: array items: { $ref: "#/components/schemas/WorkspaceVariable" } total: { type: integer } page: { type: integer } per_page: { type: integer } required: [data, total, page, per_page] TeamList: type: object properties: data: type: array items: { $ref: "#/components/schemas/Team" } total: { type: integer } page: { type: integer } per_page: { type: integer } required: [data, total, page, per_page] TeamMemberList: type: object properties: data: type: array items: { $ref: "#/components/schemas/TeamMember" } total: { type: integer } page: { type: integer } per_page: { type: integer } required: [data, total, page, per_page] WorkspaceTeamAccessList: type: object properties: data: type: array items: { $ref: "#/components/schemas/WorkspaceTeamAccess" } total: { type: integer } page: { type: integer } per_page: { type: integer } required: [data, total, page, per_page] AccountList: type: object properties: data: type: array items: { $ref: "#/components/schemas/Account" } total: { type: integer } page: { type: integer } per_page: { type: integer } required: [data, total, page, per_page] ClusterList: type: object properties: data: type: array items: { $ref: "#/components/schemas/Cluster" } total: { type: integer } page: { type: integer } per_page: { type: integer } required: [data, total, page, per_page] ClusterOperationList: type: object properties: data: type: array items: { $ref: "#/components/schemas/ClusterOperation" } total: { type: integer } page: { type: integer } per_page: { type: integer } required: [data, total, page, per_page] OpsFeedItemList: type: object properties: data: type: array items: { $ref: "#/components/schemas/OpsFeedItem" } total: { type: integer } page: { type: integer } per_page: { type: integer } required: [data, total, page, per_page] TemplateList: type: object properties: data: type: array items: { $ref: "#/components/schemas/Template" } total: { type: integer } page: { type: integer } per_page: { type: integer } required: [data, total, page, per_page] TemplateTeamAccessList: type: object properties: data: type: array items: { $ref: "#/components/schemas/TemplateTeamAccess" } total: { type: integer } page: { type: integer } per_page: { type: integer } required: [data, total, page, per_page] TenantList: type: object properties: data: type: array items: { $ref: "#/components/schemas/Tenant" } total: { type: integer } page: { type: integer } per_page: { type: integer } required: [data, total, page, per_page] TenantOperationList: type: object properties: data: type: array items: { $ref: "#/components/schemas/TenantOperation" } total: { type: integer } page: { type: integer } per_page: { type: integer } required: [data, total, page, per_page] TenantTeamAccessList: type: object properties: data: type: array items: { $ref: "#/components/schemas/TenantTeamAccess" } total: { type: integer } page: { type: integer } per_page: { type: integer } required: [data, total, page, per_page] PipelineList: type: object properties: data: type: array items: { $ref: "#/components/schemas/Pipeline" } total: { type: integer } page: { type: integer } per_page: { type: integer } required: [data, total, page, per_page] PipelineRunList: type: object properties: data: type: array items: { $ref: "#/components/schemas/PipelineRun" } total: { type: integer } page: { type: integer } per_page: { type: integer } required: [data, total, page, per_page] WorkspaceList: type: object properties: data: type: array items: { $ref: "#/components/schemas/Workspace" } total: { type: integer } page: { type: integer } per_page: { type: integer } required: [data, total, page, per_page] RunList: type: object properties: data: type: array items: { $ref: "#/components/schemas/Run" } total: { type: integer } page: { type: integer } per_page: { type: integer } required: [data, total, page, per_page] StateVersionList: type: object properties: data: type: array items: { $ref: "#/components/schemas/StateVersion" } total: { type: integer } page: { type: integer } per_page: { type: integer } required: [data, total, page, per_page] ApprovalList: type: object properties: data: type: array items: { $ref: "#/components/schemas/Approval" } total: { type: integer } page: { type: integer } per_page: { type: integer } required: [data, total, page, per_page] security: - bearerAuth: []