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 (admin) 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 (admin) 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 (admin) 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 (admin) 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 (admin) 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; a stage auto_apply on a gated workspace needs admin) 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; a stage auto_apply on a gated workspace needs admin) 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 (admin) 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 (admin) 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 (admin) 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 (admin) 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 (operator; auto_apply needs admin) 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 (operator; turning auto_apply on or requires_approval off needs admin) 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 on the workspace; deleting the last workspace requiring approval on a repository + working directory needs org 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; an auto-applying source needs admin) 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 (admin) 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 (viewer) description: > Parses the staged config (terragrunt render + variables.tf, or plain tofu) and returns every module variable with its configured state. The `default` field carries the value the config resolves — a terragrunt input or a module default — so it is only populated for callers who clear the variable-management bar (admin, or an admin team grant on this workspace). Below that bar the response is names, types, descriptions and provenance with no values, and the terragrunt render that would resolve them is skipped. 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 (admin) 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 (admin on both workspaces) description: > Upserts each of the source workspace's state outputs as a terraform-category variable on this one. Outputs the source state marks sensitive are skipped: state redacts their values, so there is nothing to carry across. A source whose outputs are all sensitive answers 400 saying so. 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 (admin on both workspaces) 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 (admin) 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 (admin) 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 (admin) 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 (admin) 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; apply/destroy on a workspace that requires approval needs admin) 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." } effective_role: type: string enum: [viewer, operator, admin, owner] description: >- The caller's role on this workspace: their org role, or a higher role one of their teams holds through a workspace team grant. Single-workspace reads only. 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 description: >- Repo-relative path to the leaf portal runs in. Stored canonical: "./envs/production", "envs//production", "envs/./production" and "envs/production/" all name one directory to the executor, so they are all stored as "envs/production". Omit on update to keep the stored value. 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 description: >- Repo-relative path to the leaf portal runs in. Stored canonical: "./envs/production", "envs//production", "envs/./production" and "envs/production/" all name one directory to the executor, so they are all stored as "envs/production". Omit on update to keep the stored value. 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 } requires_approval: type: boolean description: >- Approval gate for the clone. Omit to inherit the source's. Raising it is an operator action — it is how a clone of a config another workspace already gates gets created without an admin. Clearing a gate the source holds needs admin. 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 description: >- Attribute names the instance carries. Values are present only for callers who may manage this workspace's state; below that bar every value is null and attributes_redacted is true, because tofu writes provider secrets into state attributes in cleartext. attributes_redacted: type: boolean description: Attribute values were withheld; the names are complete. 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 } attributes_redacted: type: boolean description: >- before/after values were withheld; the counts and changed_keys are the same ones a state manager sees. 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: [development, staging, production] } cluster_version: { type: string } system_nodes: { $ref: "#/components/schemas/ClusterOrderSystemNodes" } network: { $ref: "#/components/schemas/ClusterOrderNetwork" } endpoint_public_access: type: boolean description: > Clusters vend with a private API endpoint (the eks-fleet default). Setting this true requires a non-empty endpoint_public_access_cidrs allowlist — orders that opt into public access without one are rejected with 400 (mirroring the Cluster XRD's CEL rule). endpoint_public_access_cidrs: type: array items: { type: string } description: > CIDR allowlist scoping who can reach the public endpoint (e.g. 203.0.113.0/24). Required (non-empty) when endpoint_public_access is true; each entry must parse as a CIDR. 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 } allowed_datastore_kinds: description: > Datastore kinds an operator may declare on a tenant from this template. Empty places no restriction. The override allowlist cannot express this on its own: `datastores` is one dotted path carrying the whole list, so allowing it allows every kind. type: array items: type: string enum: [relational, keyValue, objectStore, queue, cache, stream] 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, allowed_datastore_kinds, 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 } allowed_datastore_kinds: type: array items: type: string enum: [relational, keyValue, objectStore, queue, cache, stream] 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 } allowed_datastore_kinds: type: array items: type: string enum: [relational, keyValue, objectStore, queue, cache, stream] 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: []