docs(openapi): apply Five-Axis review fixes to management spec
gate-check-v3 / gate-check (pull_request) Waiting to run
qa-review / approved (pull_request) Waiting to run
security-review / approved (pull_request) Waiting to run
sop-checklist / review-refire (pull_request) Waiting to run
sop-tier-check / tier-check (pull_request) Waiting to run
sop-tier-check / tier-check (pull_request_review) Successful in 9s
ci-arm64-advisory / fast-checks (pull_request) Waiting to run
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 11s
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (pull_request) Successful in 12s
E2E Chat / detect-changes (pull_request) Successful in 16s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 11s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 12s
Harness Replays / detect-changes (pull_request) Successful in 12s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 12s
Lint no tenant GITEA or GITHUB token write / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 8s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 7s
sop-checklist / review-refire (pull_request_target) Has been skipped
sop-checklist / all-items-acked (pull_request) acked: 7/7
sop-checklist / na-declarations (pull_request) N/A: (none)
qa-review / approved (pull_request_target) Successful in 4s
sop-checklist / all-items-acked (pull_request_target) Successful in 4s
security-review / approved (pull_request_target) Successful in 4s
gate-check-v3 / gate-check (pull_request_target) Successful in 4s
sop-tier-check / tier-check (pull_request_target) Successful in 4s
verify-providers-gen / Regenerate providers artifact and fail on drift (pull_request) Successful in 59s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 0s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m28s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 2s
Harness Replays / Harness Replays (pull_request) Successful in 1s
E2E Chat / E2E Chat (pull_request) Successful in 6s
E2E API Smoke Test / detect-changes (pull_request) Successful in 13s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 1m35s
audit-force-merge / audit (pull_request_target) Successful in 4s
CI / Platform (Go) (pull_request) Has been cancelled
CI / Canvas (Next.js) (pull_request) Has been cancelled
CI / Shellcheck (E2E scripts) (pull_request) Has been cancelled
CI / Canvas Deploy Reminder (pull_request) Has been cancelled
CI / Detect changes (pull_request) Has been cancelled
CI / all-required (pull_request) Failing after 40m22s
CI / Python Lint & Test (pull_request) Has been cancelled
gate-check-v3 / gate-check (pull_request) Waiting to run
qa-review / approved (pull_request) Waiting to run
security-review / approved (pull_request) Waiting to run
sop-checklist / review-refire (pull_request) Waiting to run
sop-tier-check / tier-check (pull_request) Waiting to run
sop-tier-check / tier-check (pull_request_review) Successful in 9s
ci-arm64-advisory / fast-checks (pull_request) Waiting to run
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 11s
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (pull_request) Successful in 12s
E2E Chat / detect-changes (pull_request) Successful in 16s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 11s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 12s
Harness Replays / detect-changes (pull_request) Successful in 12s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 12s
Lint no tenant GITEA or GITHUB token write / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 8s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 7s
sop-checklist / review-refire (pull_request_target) Has been skipped
sop-checklist / all-items-acked (pull_request) acked: 7/7
sop-checklist / na-declarations (pull_request) N/A: (none)
qa-review / approved (pull_request_target) Successful in 4s
sop-checklist / all-items-acked (pull_request_target) Successful in 4s
security-review / approved (pull_request_target) Successful in 4s
gate-check-v3 / gate-check (pull_request_target) Successful in 4s
sop-tier-check / tier-check (pull_request_target) Successful in 4s
verify-providers-gen / Regenerate providers artifact and fail on drift (pull_request) Successful in 59s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 0s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m28s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 2s
Harness Replays / Harness Replays (pull_request) Successful in 1s
E2E Chat / E2E Chat (pull_request) Successful in 6s
E2E API Smoke Test / detect-changes (pull_request) Successful in 13s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 1m35s
audit-force-merge / audit (pull_request_target) Successful in 4s
CI / Platform (Go) (pull_request) Has been cancelled
CI / Canvas (Next.js) (pull_request) Has been cancelled
CI / Shellcheck (E2E scripts) (pull_request) Has been cancelled
CI / Canvas Deploy Reminder (pull_request) Has been cancelled
CI / Detect changes (pull_request) Has been cancelled
CI / all-required (pull_request) Failing after 40m22s
CI / Python Lint & Test (pull_request) Has been cancelled
Verified each against the authoritative handler source (molecule-core
workspace-server + molecule-controlplane) before editing:
1. tenantAdminToken: http/bearer -> apiKey header X-Molecule-Admin-Token.
authenticateTenant (controlplane workspace_provision.go) reads that
header, NOT Authorization, and derives org from the token
(SELECT org_id ... WHERE admin_token=$1). Removed orgRoutingHeaderId
from the DELETE /api/v1/workspaces/{workspace_id} security — no
X-Molecule-Org-Id is read on deprovision.
2. ProvisionStatus.stage: added `failed` (emitted by orgs.go on
failed/deprovisioning/deprovisioned). Existing launching/installing/
starting/configuring_https/ready all confirmed emitted by
orgs_progress.go + estimateBootProgress — none trimmed.
3. GET /workspaces/{id}: set security: [] — router.go registers it
outside every auth group (intentionally open for canvas-node self-
polling). Dropped the now-inapplicable 401.
4. Multi-period budget shape: added `budget_limits` (canonical) + legacy
`budget_limit` to PatchBudgetRequest, and `periods` (+ PeriodBudget)
to BudgetResponse, matching budget.go budgetResponse/PatchBudget.
5. GET tenant llm-billing-mode already modeled (handler serves GET+PUT) —
no change needed; verified.
6. Added prune=true destructive note (only literal "true" permanently
deletes, internal#734) and the CP-admin
/api/v1/admin/workspaces/{id}/llm-billing-mode GET+PUT pair
(cpAdminBearer, requires ?org_slug=).
redocly lint clean under both recommended and recommended-strict.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -125,12 +125,18 @@ components:
|
||||
DELETE /workspaces/:id (deprovision) additionally requires a per-tenant
|
||||
admin_token + X-Molecule-Org-Id (issue #118) — see `tenantAdminToken`.
|
||||
tenantAdminToken:
|
||||
type: http
|
||||
scheme: bearer
|
||||
type: apiKey
|
||||
in: header
|
||||
name: X-Molecule-Admin-Token
|
||||
description: |
|
||||
Per-tenant admin_token, presented to CP deprovision alongside the
|
||||
provision shared secret so a leaked shared secret can't terminate
|
||||
OTHER tenants' workspaces (issue #118). Sent with X-Molecule-Org-Id.
|
||||
OTHER tenants' workspaces (issue #118). Carried in the
|
||||
`X-Molecule-Admin-Token` header (NOT Authorization) — the handler
|
||||
(controlplane internal/handlers/workspace_provision.go
|
||||
authenticateTenant) reads this header and derives the org from the
|
||||
token (SELECT org_id FROM org_instances WHERE admin_token = $1), so no
|
||||
X-Molecule-Org-Id is needed on deprovision.
|
||||
orgApiKey:
|
||||
type: http
|
||||
scheme: bearer
|
||||
@@ -303,7 +309,14 @@ components:
|
||||
description: Org provisioning progress (GET /api/v1/orgs/:slug/provision-status).
|
||||
properties:
|
||||
status: { type: string, enum: [provisioning, running, failed] }
|
||||
stage: { type: string, enum: [launching, installing, starting, configuring_https, ready] }
|
||||
stage:
|
||||
type: string
|
||||
description: |
|
||||
UI checklist step. `failed` is emitted on a failed/deprovisioning/
|
||||
deprovisioned instance; `ready` on a healthy instance; the rest are
|
||||
the boot-progress stages (controlplane internal/handlers/orgs.go +
|
||||
orgs_progress.go bootProgressOrder/estimateBootProgress).
|
||||
enum: [launching, installing, starting, configuring_https, ready, failed]
|
||||
progress: { type: integer, minimum: 0, maximum: 100 }
|
||||
message: { type: string }
|
||||
eta_seconds: { type: integer }
|
||||
@@ -481,19 +494,60 @@ components:
|
||||
parent_id: { type: [string, "null"], format: uuid }
|
||||
additionalProperties: true
|
||||
|
||||
PeriodBudget:
|
||||
type: object
|
||||
description: |
|
||||
Per-period budget view (workspace-server budget.go periodBudget):
|
||||
configured ceiling (null = no limit), rolling-window spend, and
|
||||
remaining headroom (null when no limit; may go negative).
|
||||
properties:
|
||||
limit: { type: [integer, "null"], format: int64, description: "USD cents; null = no limit." }
|
||||
spend: { type: integer, format: int64, description: "Rolling-window spend, USD cents." }
|
||||
remaining: { type: [integer, "null"], format: int64, description: "limit - spend; null when no limit; may be negative." }
|
||||
required: [spend]
|
||||
|
||||
BudgetResponse:
|
||||
type: object
|
||||
description: |
|
||||
Canonical budget view (workspace-server budget.go budgetResponse), same
|
||||
shape for GET and PATCH. `periods` is the multi-period SSOT; the
|
||||
top-level `budget_limit`/`monthly_spend`/`budget_remaining` are
|
||||
back-compat mirrors of the monthly period for pre-multi-period clients.
|
||||
properties:
|
||||
budget_limit: { type: [integer, "null"], format: int64, description: "USD cents; null = no limit." }
|
||||
monthly_spend: { type: integer, format: int64 }
|
||||
budget_remaining: { type: [integer, "null"], format: int64 }
|
||||
periods:
|
||||
type: object
|
||||
description: |
|
||||
Per-period budgets keyed by period name. Canonical multi-period
|
||||
view (internal#734 / budget_periods.go).
|
||||
additionalProperties: { $ref: "#/components/schemas/PeriodBudget" }
|
||||
propertyNames: { enum: [hourly, daily, weekly, monthly] }
|
||||
budget_limit: { type: [integer, "null"], format: int64, description: "Back-compat: monthly limit (USD cents); null = no limit." }
|
||||
monthly_spend: { type: integer, format: int64, description: "Back-compat: monthly rolling spend (USD cents)." }
|
||||
budget_remaining: { type: [integer, "null"], format: int64, description: "Back-compat: monthly remaining (USD cents); null = no limit." }
|
||||
|
||||
PatchBudgetRequest:
|
||||
type: object
|
||||
description: budget_limit required; null clears the limit, integer (USD cents, >=0) sets it.
|
||||
description: |
|
||||
Set workspace budget limits. `budget_limits` is canonical (multi-period
|
||||
map); legacy scalar `budget_limit` is kept for back-compat. Exactly one
|
||||
of the two must be present (an empty body is a 400). A per-period value
|
||||
of null/absent clears that period; a non-negative int (USD cents) sets
|
||||
it. When `budget_limit` is used, it sets the monthly period and is kept
|
||||
synced to the budget_limits monthly key server-side.
|
||||
minProperties: 1
|
||||
properties:
|
||||
budget_limit: { type: [integer, "null"], format: int64 }
|
||||
required: [budget_limit]
|
||||
budget_limits:
|
||||
type: object
|
||||
description: |
|
||||
CANONICAL. Period → USD-cents-or-null. Allowed keys: hourly, daily,
|
||||
weekly, monthly (an unknown key is a 400).
|
||||
additionalProperties: { type: [integer, "null"], format: int64, minimum: 0 }
|
||||
propertyNames: { enum: [hourly, daily, weekly, monthly] }
|
||||
budget_limit:
|
||||
type: [integer, "null"]
|
||||
format: int64
|
||||
minimum: 0
|
||||
description: "Legacy single-monthly limit (USD cents, >=0); null clears it."
|
||||
|
||||
BillingModeResponse:
|
||||
type: object
|
||||
@@ -1119,6 +1173,91 @@ paths:
|
||||
schema: { type: array, items: { type: object, additionalProperties: true } }
|
||||
"401": { $ref: "#/components/responses/Unauthorized" }
|
||||
|
||||
/api/v1/admin/workspaces/{id}/llm-billing-mode:
|
||||
parameters: [{ $ref: "#/components/parameters/WorkspaceId" }]
|
||||
get:
|
||||
tags: [cp-admin]
|
||||
summary: Get a workspace LLM billing mode (CP-admin proxy)
|
||||
description: |
|
||||
CP-side operator proxy to the per-tenant
|
||||
/admin/workspaces/:id/llm-billing-mode. **Requires `?org_slug=`** — CP
|
||||
has no workspace_id→org_id index, so the slug tells CP which tenant to
|
||||
dispatch to (controlplane internal/handlers/admin_workspace_billing_mode.go).
|
||||
operationId: adminGetWorkspaceBillingMode
|
||||
security: [{ cpAdminBearer: [] }]
|
||||
parameters:
|
||||
- name: org_slug
|
||||
in: query
|
||||
required: true
|
||||
description: Org slug — resolves which tenant CP proxies to.
|
||||
schema: { type: string, pattern: "^[a-z][a-z0-9-]{2,31}$" }
|
||||
responses:
|
||||
"200":
|
||||
description: OK.
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/BillingModeResponse" }
|
||||
"400": { $ref: "#/components/responses/BadRequest" }
|
||||
"401": { $ref: "#/components/responses/Unauthorized" }
|
||||
"404":
|
||||
description: Slug doesn't resolve to an org.
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/Error" }
|
||||
"502":
|
||||
description: Per-tenant call failed (auth/network/parse).
|
||||
content:
|
||||
application/json:
|
||||
schema: { type: object, additionalProperties: true }
|
||||
"503":
|
||||
description: Billing-mode client unwired on this CP (dev / no Secrets).
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/Error" }
|
||||
put:
|
||||
tags: [cp-admin]
|
||||
summary: Set/clear a workspace LLM billing mode (CP-admin proxy)
|
||||
description: |
|
||||
CP-side operator proxy. **Requires `?org_slug=`.** Body
|
||||
{"mode": "byok"|"platform_managed"|"disabled"|null}; mode is validated
|
||||
against the credits enum BEFORE the per-tenant call.
|
||||
operationId: adminPutWorkspaceBillingMode
|
||||
security: [{ cpAdminBearer: [] }]
|
||||
parameters:
|
||||
- name: org_slug
|
||||
in: query
|
||||
required: true
|
||||
description: Org slug — resolves which tenant CP proxies to.
|
||||
schema: { type: string, pattern: "^[a-z][a-z0-9-]{2,31}$" }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/PutBillingModeRequest" }
|
||||
responses:
|
||||
"200":
|
||||
description: Updated.
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/BillingModeResponse" }
|
||||
"400": { $ref: "#/components/responses/BadRequest" }
|
||||
"401": { $ref: "#/components/responses/Unauthorized" }
|
||||
"404":
|
||||
description: Slug doesn't resolve to an org.
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/Error" }
|
||||
"502":
|
||||
description: Per-tenant call failed (auth/network/parse).
|
||||
content:
|
||||
application/json:
|
||||
schema: { type: object, additionalProperties: true }
|
||||
"503":
|
||||
description: Billing-mode client unwired on this CP (dev / no Secrets).
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/Error" }
|
||||
|
||||
# ------------------------------------------------------------- CP: PROVISION
|
||||
/api/v1/workspaces/provision:
|
||||
post:
|
||||
@@ -1159,18 +1298,24 @@ paths:
|
||||
tags: [cp-provision]
|
||||
summary: Deprovision (terminate) a workspace EC2 + DNS
|
||||
description: |
|
||||
Requires the provision shared secret AND a per-tenant admin_token +
|
||||
X-Molecule-Org-Id (issue #118) so a leaked shared secret can't kill
|
||||
other tenants' workspaces. `?prune=` controls data-volume pruning.
|
||||
Requires the provision shared secret AND a per-tenant admin_token
|
||||
(X-Molecule-Admin-Token, issue #118) so a leaked shared secret can't
|
||||
kill other tenants' workspaces. The org is derived from the admin token
|
||||
server-side, so no X-Molecule-Org-Id header is read here. `?prune=`
|
||||
controls data-volume pruning.
|
||||
operationId: deprovisionWorkspace
|
||||
security:
|
||||
- provisionSecret: []
|
||||
tenantAdminToken: []
|
||||
orgRoutingHeaderId: []
|
||||
parameters:
|
||||
- name: prune
|
||||
in: query
|
||||
required: false
|
||||
description: |
|
||||
Only the literal string `true` triggers a PERMANENT data delete
|
||||
(the data volume is tagged for immediate erase — destructive,
|
||||
internal#734). Any other value (including absent) terminates the
|
||||
instance but preserves the data volume for the grace-period sweep.
|
||||
schema: { type: boolean }
|
||||
responses:
|
||||
"200":
|
||||
@@ -1263,13 +1408,13 @@ paths:
|
||||
parameters: [{ $ref: "#/components/parameters/WorkspaceId" }]
|
||||
get:
|
||||
tags: [tenant-workspaces]
|
||||
summary: Get a workspace
|
||||
summary: Get a workspace (INTENTIONALLY UNAUTHENTICATED)
|
||||
description: |
|
||||
Intentionally open — registered outside every auth group
|
||||
(workspace-server internal/router/router.go) so canvas nodes can fetch
|
||||
their own state without a token (WorkspaceNode polling + health checks).
|
||||
operationId: tenantGetWorkspace
|
||||
security:
|
||||
- orgApiKey: []
|
||||
orgRoutingHeaderId: []
|
||||
- workspaceToken: []
|
||||
orgRoutingHeaderId: []
|
||||
security: []
|
||||
parameters:
|
||||
- { $ref: "#/components/parameters/OrgIdHeader" }
|
||||
- { $ref: "#/components/parameters/OrgSlugHeader" }
|
||||
@@ -1279,7 +1424,6 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/Workspace" }
|
||||
"401": { $ref: "#/components/responses/Unauthorized" }
|
||||
"404": { $ref: "#/components/responses/NotFound" }
|
||||
delete:
|
||||
tags: [tenant-workspaces]
|
||||
|
||||
Reference in New Issue
Block a user