8cea4a30c4
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>
1996 lines
73 KiB
YAML
1996 lines
73 KiB
YAML
openapi: 3.1.0
|
|
|
|
# =============================================================================
|
|
# Molecule Platform — Management API (SSOT)
|
|
#
|
|
# This is the hand-authored, authoritative OpenAPI 3.1 contract for the
|
|
# Molecule platform MANAGEMENT surface. It is the single source of truth from
|
|
# which all management tooling derives: the management MCP server, the
|
|
# management CLI (molecule-cli), and the human-facing API docs (RFC #1706 /
|
|
# the gap closed by PLATFORM-MANAGEMENT-API.md §5c).
|
|
#
|
|
# It deliberately covers TWO services behind ONE spec (see README.md in this
|
|
# directory for the full split):
|
|
#
|
|
# 1. Control plane (CP) — molecule-controlplane @ api.moleculesai.app.
|
|
# Owns orgs, members, billing, provisioning, fleet/admin ops.
|
|
# Routes: /api/v1/* (stable) — the /cp/* mirror is sunset-headed and is
|
|
# NOT modelled here (identical shapes, RFC #61 Deprecation/Sunset headers).
|
|
#
|
|
# 2. Tenant workspace-server — molecule-core/workspace-server, ONE EC2 per
|
|
# org @ <slug>.moleculesai.app. Owns workspaces, secrets, templates,
|
|
# org-tokens, bundles. Reached either directly at the tenant host or via
|
|
# the CP edge reverse-proxy (subdomain or X-Molecule-Org-Slug routing).
|
|
#
|
|
# The existing swaggo-generated swagger.{json,yaml} in this directory only
|
|
# covers /schedules and is OpenAPI 2.0; this file supersedes it for the
|
|
# management surface. Schedules/agent/registry/a2a (the per-workspace RUNTIME
|
|
# surface) are intentionally out of scope here.
|
|
#
|
|
# Grounding: every path, body and security scheme below was read from the
|
|
# router + handler sources (controlplane internal/router/router.go +
|
|
# internal/handlers/*, workspace-server internal/router/router.go +
|
|
# internal/handlers/*). Where a body field is best-effort it is flagged.
|
|
# =============================================================================
|
|
|
|
info:
|
|
title: Molecule Platform Management API
|
|
version: "1.0.0"
|
|
description: |
|
|
Authoritative management contract for the Molecule platform. Two services,
|
|
two auth stacks, one spec. See `docs/openapi/README.md` for the
|
|
service-split + per-tier security matrix. MCP and CLI tooling are
|
|
generated/derived from this document — treat it as SSOT.
|
|
|
|
**Security tiers (summary — full matrix in README):**
|
|
- `workosSession` — WorkOS AuthKit session cookie (`mcp_session`), + org-membership/ownership checks. CP org/member/billing surface.
|
|
- `cpAdminBearer` — `CP_ADMIN_API_TOKEN` operator bearer. CP `/api/v1/admin/*` fleet/tenant/pin/env surface.
|
|
- `provisionSecret` — CP `PROVISION_SHARED_SECRET` bearer (+ tenant admin_token for deprovision). CP `/api/v1/workspaces/provision|deprovision`.
|
|
- `orgApiKey` — tenant Org API Key (`Authorization: Bearer <key>` + `X-Molecule-Org-Id`). Full tenant-admin; CANNOT reach CP.
|
|
- `workspaceToken` — per-workspace bearer, bound to one workspace id (+ tenant routing header).
|
|
|
|
**Guards:** mutating CP admin ops carry confirm/dry-run guards — flagged
|
|
per-operation. The Org API Key is full-tenant-admin and self-minting
|
|
(treat as tenant root; no scope-down yet — TODO in `orgtoken`).
|
|
contact:
|
|
name: Molecule AI Core Platform
|
|
url: https://git.moleculesai.app/molecule-ai/molecule-core
|
|
license:
|
|
name: Proprietary
|
|
identifier: LicenseRef-Molecule-AI-Proprietary
|
|
|
|
servers:
|
|
- url: https://api.moleculesai.app
|
|
description: Control plane (CP). Org/member/billing/provisioning/admin surface under /api/v1.
|
|
- url: https://{slug}.moleculesai.app
|
|
description: |
|
|
Tenant workspace-server, one per org. Workspace/secret/template/org-token/
|
|
bundle surface. Requests may also be sent to the CP host with an
|
|
X-Molecule-Org-Slug header; the CP edge reverse-proxies to this host.
|
|
variables:
|
|
slug:
|
|
default: agents-team
|
|
description: Org slug (subdomain segment).
|
|
|
|
tags:
|
|
- name: cp-orgs
|
|
description: "[CP] Organization lifecycle (session-owned)."
|
|
- name: cp-billing
|
|
description: "[CP] Billing — invoices, checkout, portal, top-up (session-owned)."
|
|
- name: cp-admin
|
|
description: "[CP] Operator admin — org create, tenant teardown, workspace env, ListOrgWorkspaces, pins (admin bearer)."
|
|
- name: cp-provision
|
|
description: "[CP] Workspace EC2 provisioning (provision shared-secret)."
|
|
- name: tenant-workspaces
|
|
description: "[Tenant] Workspace lifecycle + secrets + budget + billing-mode."
|
|
- name: tenant-org
|
|
description: "[Tenant] Org import, templates, Org API Key (org-token) management."
|
|
- name: tenant-secrets
|
|
description: "[Tenant] Org-wide (global) secrets."
|
|
- name: tenant-bundles
|
|
description: "[Tenant] Bundle export/import."
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Security schemes — the five tiers from PLATFORM-MANAGEMENT-API.md §1.
|
|
# Each operation declares the scheme(s) it accepts under its own `security`.
|
|
# -----------------------------------------------------------------------------
|
|
components:
|
|
securitySchemes:
|
|
workosSession:
|
|
type: apiKey
|
|
in: cookie
|
|
name: mcp_session
|
|
description: |
|
|
WorkOS AuthKit session cookie. Set by the CP /api/v1/auth/callback flow.
|
|
Handlers additionally enforce org_members ownership/membership, so a
|
|
generic "logged in" cookie cannot read another org's data. CP org,
|
|
member, and billing surface only — never authorizes CP admin or the
|
|
tenant surface.
|
|
cpAdminBearer:
|
|
type: http
|
|
scheme: bearer
|
|
description: |
|
|
Operator bearer = the CP `CP_ADMIN_API_TOKEN`. Gates every
|
|
/api/v1/admin/* route via AdminGate (constant-time compare). Also
|
|
accepts an admin-allowlisted WorkOS session, but server-to-server
|
|
callers (CI, ops scripts, the CP-admin MCP) use this bearer. Disabled
|
|
(routes still mounted, all 401) when the token is unset in prod.
|
|
provisionSecret:
|
|
type: http
|
|
scheme: bearer
|
|
description: |
|
|
CP `PROVISION_SHARED_SECRET` bearer. Gates /api/v1/workspaces/provision
|
|
(constant-time compare). The routes are not mounted at all when the
|
|
secret is unset (an unauth'd provision endpoint is an unauth RCE).
|
|
DELETE /workspaces/:id (deprovision) additionally requires a per-tenant
|
|
admin_token + X-Molecule-Org-Id (issue #118) — see `tenantAdminToken`.
|
|
tenantAdminToken:
|
|
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). 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
|
|
description: |
|
|
Tenant Org API Key (dashboard "Org API Keys"; `org_api_tokens` —
|
|
sha256-hashed, prefixed, revocable, FULL tenant-admin, self-minting).
|
|
Authorizes the entire tenant-admin surface of its own org via the
|
|
tenant AdminAuth/WorkspaceAuth gates. Present as `Authorization:
|
|
Bearer <key>` to the tenant host. MUST be paired with the
|
|
`orgRoutingHeader` (X-Molecule-Org-Id or X-Molecule-Org-Slug) so the
|
|
platform edge routes to the correct tenant. CANNOT reach any CP route
|
|
(org create/delete, billing, members, provisioning all 401/403 it).
|
|
workspaceToken:
|
|
type: http
|
|
scheme: bearer
|
|
description: |
|
|
Per-workspace bearer token (`workspace_auth_tokens`), bound to a single
|
|
workspace id by the tenant WorkspaceAuth middleware — workspace A's
|
|
token cannot hit workspace B's sub-routes. Sent with the
|
|
`orgRoutingHeader`. Rejected on admin-list/create/delete routes when an
|
|
ADMIN_TOKEN is configured (use orgApiKey there).
|
|
orgRoutingHeaderId:
|
|
type: apiKey
|
|
in: header
|
|
name: X-Molecule-Org-Id
|
|
description: |
|
|
Tenant routing + TenantGuard match (org UUID). Required on tenant-host
|
|
requests so the CP edge / TenantGuard route to and authorize against
|
|
the correct org. Either this or X-Molecule-Org-Slug must be present.
|
|
orgRoutingHeaderSlug:
|
|
type: apiKey
|
|
in: header
|
|
name: X-Molecule-Org-Slug
|
|
description: |
|
|
Tenant routing header (human-readable slug, e.g. "agents-team").
|
|
Alternative to X-Molecule-Org-Id; slug is preferred for client code.
|
|
|
|
parameters:
|
|
OrgSlug:
|
|
name: slug
|
|
in: path
|
|
required: true
|
|
description: Org slug (subdomain segment, ^[a-z][a-z0-9-]{2,31}$).
|
|
schema:
|
|
type: string
|
|
pattern: "^[a-z][a-z0-9-]{2,31}$"
|
|
WorkspaceId:
|
|
name: id
|
|
in: path
|
|
required: true
|
|
description: Workspace UUID.
|
|
schema:
|
|
type: string
|
|
format: uuid
|
|
OrgIdHeader:
|
|
name: X-Molecule-Org-Id
|
|
in: header
|
|
required: false
|
|
description: Tenant routing/TenantGuard header (org UUID). Send this or X-Molecule-Org-Slug.
|
|
schema:
|
|
type: string
|
|
OrgSlugHeader:
|
|
name: X-Molecule-Org-Slug
|
|
in: header
|
|
required: false
|
|
description: Tenant routing header (slug). Send this or X-Molecule-Org-Id.
|
|
schema:
|
|
type: string
|
|
|
|
responses:
|
|
BadRequest:
|
|
description: Invalid request body or parameters.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Error"
|
|
Unauthorized:
|
|
description: Missing or invalid credential for the required tier.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Error"
|
|
Forbidden:
|
|
description: Authenticated but not authorized (wrong tier, or not a member/owner).
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Error"
|
|
NotFound:
|
|
description: Resource not found (also returned to avoid leaking existence).
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Error"
|
|
Conflict:
|
|
description: Slug taken / resource already exists.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Error"
|
|
|
|
schemas:
|
|
Error:
|
|
type: object
|
|
properties:
|
|
error:
|
|
type: string
|
|
description: Human-readable error message.
|
|
required: [error]
|
|
|
|
# ---- CP: orgs ----
|
|
Organization:
|
|
type: object
|
|
description: One paying customer. Slug is immutable after creation.
|
|
properties:
|
|
id: { type: string, format: uuid }
|
|
slug: { type: string }
|
|
name: { type: string }
|
|
plan: { type: string }
|
|
status: { type: string, description: "e.g. active | provisioning | suspended | deleted" }
|
|
stripe_customer_id: { type: string }
|
|
credits_balance: { type: integer, format: int64 }
|
|
plan_monthly_credits: { type: integer, format: int64 }
|
|
overage_used_credits: { type: integer, format: int64 }
|
|
overage_cap_credits: { type: integer, format: int64 }
|
|
created_at: { type: string, format: date-time }
|
|
updated_at: { type: string, format: date-time }
|
|
required: [id, slug, name, plan, status]
|
|
|
|
CreateOrgRequest:
|
|
type: object
|
|
description: Session-authed org creation (POST /api/v1/orgs).
|
|
properties:
|
|
slug: { type: string, pattern: "^[a-z][a-z0-9-]{2,31}$" }
|
|
name: { type: string }
|
|
required: [slug, name]
|
|
|
|
AdminCreateOrgRequest:
|
|
type: object
|
|
description: |
|
|
Admin server-to-server org creation. Strict-decoded — unknown fields
|
|
are a 400 (a stale `is_staging` key would otherwise silently provision
|
|
a prod org). Skips terms/quota/billing gates.
|
|
additionalProperties: false
|
|
properties:
|
|
slug: { type: string, pattern: "^[a-z][a-z0-9-]{2,31}$" }
|
|
name: { type: string }
|
|
owner_user_id:
|
|
type: string
|
|
description: Opaque membership owner. Convention "e2e-runner:<slug>" for CI orgs.
|
|
required: [slug, name, owner_user_id]
|
|
|
|
AdminCreateOrgDryRun:
|
|
type: object
|
|
description: Response when POST /api/v1/admin/orgs?dry_run=true.
|
|
properties:
|
|
dry_run: { type: boolean, const: true }
|
|
slug: { type: string }
|
|
name: { type: string }
|
|
owner_user_id: { type: string }
|
|
|
|
AdminTenantTokenResponse:
|
|
type: object
|
|
properties:
|
|
slug: { type: string }
|
|
admin_token: { type: string, description: "Per-tenant admin token (sensitive)." }
|
|
|
|
ProvisionStatus:
|
|
type: object
|
|
description: Org provisioning progress (GET /api/v1/orgs/:slug/provision-status).
|
|
properties:
|
|
status: { type: string, enum: [provisioning, running, failed] }
|
|
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 }
|
|
url: { type: string }
|
|
required: [status, stage, progress, message, eta_seconds]
|
|
|
|
OrgExport:
|
|
type: object
|
|
description: GDPR data-portability export (best-effort field list).
|
|
properties:
|
|
schema_version: { type: string }
|
|
exported_at: { type: string, format: date-time }
|
|
exported_by: { type: string, description: "Requesting session user id." }
|
|
subject:
|
|
type: object
|
|
properties:
|
|
user_id: { type: string }
|
|
organization:
|
|
$ref: "#/components/schemas/Organization"
|
|
infrastructure:
|
|
type: object
|
|
description: Omitted for failed-provision orgs.
|
|
additionalProperties: true
|
|
members:
|
|
type: array
|
|
items: { type: object, additionalProperties: true }
|
|
notes:
|
|
type: object
|
|
additionalProperties: true
|
|
|
|
DeleteTenantRequest:
|
|
type: object
|
|
description: Confirm-gated admin tenant teardown. `confirm` must equal the URL slug.
|
|
properties:
|
|
confirm:
|
|
type: string
|
|
description: Must exactly equal the path slug or the request 400s before any teardown.
|
|
required: [confirm]
|
|
|
|
UpdateWorkspaceEnvRequest:
|
|
type: object
|
|
description: |
|
|
Admin env mutation for a running workspace (SSM + restart). Keys
|
|
matching the secret-keyword guard (TOKEN/SECRET/KEY/PASSWORD) require
|
|
force=true. Prefer Infisical per-persona paths for real secrets.
|
|
properties:
|
|
env:
|
|
type: object
|
|
additionalProperties: { type: string }
|
|
description: At least one key required.
|
|
merge:
|
|
type: boolean
|
|
default: true
|
|
description: true = merge into existing env; false = replace (destructive).
|
|
force:
|
|
type: boolean
|
|
default: false
|
|
description: Bypass the secret-keyword guard.
|
|
required: [env]
|
|
|
|
UpdateWorkspaceEnvResponse:
|
|
type: object
|
|
properties:
|
|
ok: { type: boolean }
|
|
workspace_id: { type: string }
|
|
instance_id: { type: string }
|
|
ssm_status: { type: string }
|
|
ssm_exit_code: { type: integer, format: int32 }
|
|
container_restarted: { type: boolean }
|
|
applied_keys: { type: array, items: { type: string } }
|
|
env_keys_after: { type: array, items: { type: string } }
|
|
|
|
PinPromoteRequest:
|
|
type: object
|
|
description: |
|
|
Generic pin-table promote body. Shape is per-resource (thin-ami:
|
|
region+ami_id; runtime-image: template+digest). `promoted_by` is filled
|
|
server-side from the admin actor. additionalProperties allowed because
|
|
the dispatcher validates per-resource.
|
|
additionalProperties: true
|
|
|
|
# ---- CP: provisioning ----
|
|
WorkspaceProvisionRequest:
|
|
type: object
|
|
description: Provision a workspace EC2 instance (called by tenant platforms).
|
|
properties:
|
|
org_id: { type: string, format: uuid }
|
|
workspace_id: { type: string, format: uuid }
|
|
runtime: { type: string, description: "claude-code | codex | langgraph | ..." }
|
|
tier: { type: integer }
|
|
instance_type: { type: string }
|
|
disk_gb: { type: integer, format: int32 }
|
|
platform_url: { type: string, format: uri }
|
|
env:
|
|
type: object
|
|
additionalProperties: { type: string }
|
|
config_files:
|
|
type: object
|
|
additionalProperties: { type: string }
|
|
display:
|
|
type: object
|
|
properties:
|
|
mode: { type: string }
|
|
width: { type: integer }
|
|
height: { type: integer }
|
|
protocol: { type: string }
|
|
required: [org_id, workspace_id, runtime, platform_url]
|
|
|
|
WorkspaceProvisionResponse:
|
|
type: object
|
|
properties:
|
|
instance_id: { type: string }
|
|
public_ip: { type: string }
|
|
private_ip: { type: string }
|
|
state: { type: string }
|
|
runtime: { type: string }
|
|
domain: { type: string }
|
|
|
|
WorkspaceProvisionStatus:
|
|
type: object
|
|
properties:
|
|
instance_id: { type: string }
|
|
state: { type: string }
|
|
public_ip: { type: string }
|
|
private_ip: { type: string }
|
|
|
|
RuntimePinMissing:
|
|
type: object
|
|
description: 422 when no runtime image pin exists for (runtime).
|
|
properties:
|
|
error: { type: string, const: RUNTIME_PIN_MISSING }
|
|
required: [error]
|
|
|
|
# ---- Tenant: workspaces ----
|
|
CreateWorkspaceRequest:
|
|
type: object
|
|
description: |
|
|
Tenant workspace creation (POST /workspaces, AdminAuth). Best-effort —
|
|
full struct is models.CreateWorkspacePayload. Only `name` is required.
|
|
properties:
|
|
name: { type: string }
|
|
role: { type: string }
|
|
template: { type: string, description: "workspace-configs-templates folder name." }
|
|
tier: { type: integer }
|
|
model: { type: string }
|
|
llm_provider: { type: string }
|
|
runtime: { type: string, description: "Default claude-code; derived from template if empty." }
|
|
external: { type: boolean, description: "true = registered URL only, no Docker container." }
|
|
url: { type: string, description: "External A2A endpoint (push mode)." }
|
|
delivery_mode: { type: string, enum: [push, poll] }
|
|
workspace_dir: { type: string }
|
|
workspace_access: { type: string, enum: [none, read_only, read_write] }
|
|
parent_id: { type: [string, "null"], format: uuid }
|
|
budget_limit:
|
|
type: [integer, "null"]
|
|
format: int64
|
|
description: Monthly spend ceiling in USD cents; null = no limit.
|
|
secrets:
|
|
type: object
|
|
additionalProperties: { type: string }
|
|
description: Optional key→value secrets persisted (encrypted) at creation.
|
|
max_concurrent_tasks: { type: integer }
|
|
required: [name]
|
|
|
|
Workspace:
|
|
type: object
|
|
description: A workspace row (best-effort — see models.Workspace).
|
|
properties:
|
|
id: { type: string, format: uuid }
|
|
name: { type: string }
|
|
role: { type: [string, "null"] }
|
|
tier: { type: integer }
|
|
runtime: { type: string }
|
|
status: { type: string }
|
|
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:
|
|
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: |
|
|
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_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
|
|
properties:
|
|
mode: { type: string, enum: [platform_managed, byok, disabled] }
|
|
effective_mode: { type: string }
|
|
source: { type: string, description: "workspace | org | default" }
|
|
additionalProperties: true
|
|
|
|
PutBillingModeRequest:
|
|
type: object
|
|
description: |
|
|
{"mode":"byok"} sets the override; {"mode":null} clears it; {} is a 400
|
|
(caller must be explicit).
|
|
properties:
|
|
mode:
|
|
type: [string, "null"]
|
|
enum: [platform_managed, byok, disabled, null]
|
|
required: [mode]
|
|
|
|
# ---- Tenant: secrets ----
|
|
SecretSummary:
|
|
type: object
|
|
description: Secret metadata (keys only — values never returned by List).
|
|
properties:
|
|
key: { type: string }
|
|
created_at: { type: string }
|
|
updated_at: { type: string }
|
|
source: { type: string, description: "workspace | global override info." }
|
|
additionalProperties: true
|
|
|
|
SetSecretRequest:
|
|
type: object
|
|
description: |
|
|
Upserts AES-256-GCM, emits secret.set audit, auto-restarts the
|
|
workspace. platform-managed billing strips vendor-LLM keys unless
|
|
billing-mode is byok; GIT_HTTP_* are never stripped.
|
|
properties:
|
|
key: { type: string }
|
|
value: { type: string }
|
|
required: [key, value]
|
|
|
|
# ---- Tenant: org tokens (Org API Keys) ----
|
|
CreateOrgTokenRequest:
|
|
type: object
|
|
description: Optional body — an empty POST mints an unnamed token.
|
|
properties:
|
|
name: { type: string, maxLength: 100 }
|
|
|
|
CreateOrgTokenResponse:
|
|
type: object
|
|
description: Plaintext token returned exactly ONCE.
|
|
properties:
|
|
id: { type: string }
|
|
prefix: { type: string }
|
|
name: { type: string }
|
|
auth_token: { type: string, description: "Plaintext — shown once; copy now." }
|
|
warning: { type: string }
|
|
required: [id, prefix, auth_token]
|
|
|
|
OrgTokenList:
|
|
type: object
|
|
properties:
|
|
tokens:
|
|
type: array
|
|
items:
|
|
type: object
|
|
properties:
|
|
id: { type: string }
|
|
prefix: { type: string }
|
|
name: { type: string }
|
|
created_at: { type: string }
|
|
additionalProperties: true
|
|
count: { type: integer }
|
|
|
|
# ---- Tenant: org import / templates / bundles ----
|
|
OrgImportRequest:
|
|
type: object
|
|
description: |
|
|
Create workspaces from an org template. Provide `dir` (server-side org
|
|
template dir, traversal-guarded) OR an inline `template`. `mode`
|
|
controls cleanup of pre-existing workspaces.
|
|
properties:
|
|
dir: { type: string, description: "Org template directory name." }
|
|
template:
|
|
type: object
|
|
description: Inline OrgTemplate (best-effort; see OrgTemplate YAML schema).
|
|
additionalProperties: true
|
|
mode: { type: string, enum: ["", merge, reconcile], default: merge }
|
|
|
|
TemplateImportRequest:
|
|
type: object
|
|
description: Import a workspace template (writes config files into configsDir).
|
|
properties:
|
|
name: { type: string }
|
|
files:
|
|
type: object
|
|
additionalProperties: { type: string }
|
|
required: [name, files]
|
|
|
|
BundleImportRequest:
|
|
type: object
|
|
description: Import a workspace bundle (CRITICAL surface — admin-gated). Best-effort body.
|
|
properties:
|
|
name: { type: string }
|
|
additionalProperties: true
|
|
required: [name]
|
|
|
|
# =============================================================================
|
|
# Paths
|
|
# =============================================================================
|
|
paths:
|
|
|
|
# ------------------------------------------------------------------ CP: ORGS
|
|
/api/v1/orgs:
|
|
post:
|
|
tags: [cp-orgs]
|
|
summary: Create an organization (session-owned)
|
|
operationId: createOrg
|
|
security: [{ workosSession: [] }]
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/CreateOrgRequest" }
|
|
responses:
|
|
"201":
|
|
description: Created.
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/Organization" }
|
|
"400": { $ref: "#/components/responses/BadRequest" }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
"402":
|
|
description: Quota/credit gate — payment required.
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/Error" }
|
|
"409": { $ref: "#/components/responses/Conflict" }
|
|
"412":
|
|
description: Terms of Service not yet accepted.
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/Error" }
|
|
get:
|
|
tags: [cp-orgs]
|
|
summary: List the caller's organizations
|
|
operationId: listOrgs
|
|
security: [{ workosSession: [] }]
|
|
responses:
|
|
"200":
|
|
description: OK.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items: { $ref: "#/components/schemas/Organization" }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
|
|
/api/v1/orgs/{slug}:
|
|
parameters: [{ $ref: "#/components/parameters/OrgSlug" }]
|
|
get:
|
|
tags: [cp-orgs]
|
|
summary: Get an organization (membership-checked)
|
|
operationId: getOrg
|
|
security: [{ workosSession: [] }]
|
|
responses:
|
|
"200":
|
|
description: OK.
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/Organization" }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"404": { $ref: "#/components/responses/NotFound" }
|
|
delete:
|
|
tags: [cp-orgs]
|
|
summary: Delete an organization (owner-only GDPR purge cascade)
|
|
description: |
|
|
Owner-only. Cascade purges Stripe + EC2 + Cloudflare + DB rows, writes
|
|
an org_purges audit row. Returns 204 on success. Irreversible.
|
|
operationId: deleteOrg
|
|
security: [{ workosSession: [] }]
|
|
responses:
|
|
"204": { description: Deleted. }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"404": { $ref: "#/components/responses/NotFound" }
|
|
|
|
/api/v1/orgs/{slug}/export:
|
|
parameters: [{ $ref: "#/components/parameters/OrgSlug" }]
|
|
get:
|
|
tags: [cp-orgs]
|
|
summary: Export an organization (GDPR data portability)
|
|
operationId: exportOrg
|
|
security: [{ workosSession: [] }]
|
|
responses:
|
|
"200":
|
|
description: OK.
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/OrgExport" }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
"404": { $ref: "#/components/responses/NotFound" }
|
|
|
|
/api/v1/orgs/{slug}/provision-status:
|
|
parameters: [{ $ref: "#/components/parameters/OrgSlug" }]
|
|
get:
|
|
tags: [cp-orgs]
|
|
summary: Org provisioning progress
|
|
operationId: getProvisionStatus
|
|
security: [{ workosSession: [] }]
|
|
responses:
|
|
"200":
|
|
description: OK.
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/ProvisionStatus" }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
"404": { $ref: "#/components/responses/NotFound" }
|
|
|
|
/api/v1/orgs/{slug}/instance:
|
|
parameters: [{ $ref: "#/components/parameters/OrgSlug" }]
|
|
get:
|
|
tags: [cp-orgs]
|
|
summary: Public tenant-routing lookup (NO AUTH)
|
|
description: |
|
|
Public, unauthenticated routing lookup used by the Cloudflare Worker.
|
|
Returns {slug, status, ip, org_id}. Rate-limited to prevent
|
|
enumeration. (This is the "200 with no key" in the audit — not the Org
|
|
API Key.)
|
|
operationId: getOrgInstance
|
|
security: []
|
|
responses:
|
|
"200":
|
|
description: OK.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
slug: { type: string }
|
|
status: { type: string }
|
|
ip: { type: string }
|
|
org_id: { type: string }
|
|
additionalProperties: true
|
|
"404": { $ref: "#/components/responses/NotFound" }
|
|
|
|
# ---------------------------------------------------------------- CP: BILLING
|
|
/api/v1/billing/invoices:
|
|
get:
|
|
tags: [cp-billing]
|
|
summary: List invoices
|
|
operationId: listInvoices
|
|
security: [{ workosSession: [] }]
|
|
responses:
|
|
"200":
|
|
description: OK.
|
|
content:
|
|
application/json:
|
|
schema: { type: array, items: { type: object, additionalProperties: true } }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
|
|
/api/v1/billing/checkout:
|
|
post:
|
|
tags: [cp-billing]
|
|
summary: Create a Stripe Checkout session
|
|
operationId: billingCheckout
|
|
security: [{ workosSession: [] }]
|
|
requestBody:
|
|
required: false
|
|
content:
|
|
application/json:
|
|
schema: { type: object, additionalProperties: true }
|
|
responses:
|
|
"200":
|
|
description: OK (checkout url).
|
|
content:
|
|
application/json:
|
|
schema: { type: object, additionalProperties: true }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
|
|
/api/v1/billing/portal:
|
|
post:
|
|
tags: [cp-billing]
|
|
summary: Create a Stripe billing-portal session
|
|
operationId: billingPortal
|
|
security: [{ workosSession: [] }]
|
|
responses:
|
|
"200":
|
|
description: OK (portal url).
|
|
content:
|
|
application/json:
|
|
schema: { type: object, additionalProperties: true }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
|
|
/api/v1/billing/topup:
|
|
post:
|
|
tags: [cp-billing]
|
|
summary: Credit top-up
|
|
operationId: billingTopup
|
|
security: [{ workosSession: [] }]
|
|
requestBody:
|
|
required: false
|
|
content:
|
|
application/json:
|
|
schema: { type: object, additionalProperties: true }
|
|
responses:
|
|
"200":
|
|
description: OK.
|
|
content:
|
|
application/json:
|
|
schema: { type: object, additionalProperties: true }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
|
|
# ------------------------------------------------------------------ CP: ADMIN
|
|
/api/v1/admin/orgs:
|
|
get:
|
|
tags: [cp-admin]
|
|
summary: List all organizations (operator)
|
|
operationId: adminListOrgs
|
|
security: [{ cpAdminBearer: [] }]
|
|
responses:
|
|
"200":
|
|
description: OK.
|
|
content:
|
|
application/json:
|
|
schema: { type: array, items: { $ref: "#/components/schemas/Organization" } }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
"403": { $ref: "#/components/responses/Forbidden" }
|
|
post:
|
|
tags: [cp-admin]
|
|
summary: Admin-create an organization (server-to-server)
|
|
description: |
|
|
Skips terms/quota/billing gates. Strict-decoded (unknown fields → 400).
|
|
**Dry-run guard:** `?dry_run=true` validates + echoes without creating.
|
|
operationId: adminCreateOrg
|
|
security: [{ cpAdminBearer: [] }]
|
|
parameters:
|
|
- name: dry_run
|
|
in: query
|
|
required: false
|
|
description: When true, validate + echo only; no org created.
|
|
schema: { type: boolean }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/AdminCreateOrgRequest" }
|
|
responses:
|
|
"200":
|
|
description: Dry-run echo (when ?dry_run=true).
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/AdminCreateOrgDryRun" }
|
|
"201":
|
|
description: Created.
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/Organization" }
|
|
"400": { $ref: "#/components/responses/BadRequest" }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
"409": { $ref: "#/components/responses/Conflict" }
|
|
|
|
/api/v1/admin/orgs/{slug}/admin-token:
|
|
parameters: [{ $ref: "#/components/parameters/OrgSlug" }]
|
|
get:
|
|
tags: [cp-admin]
|
|
summary: Get the per-tenant admin token (operator)
|
|
operationId: adminGetTenantToken
|
|
security: [{ cpAdminBearer: [] }]
|
|
responses:
|
|
"200":
|
|
description: OK.
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/AdminTenantTokenResponse" }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
"404":
|
|
description: Tenant admin token not yet provisioned.
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/Error" }
|
|
|
|
/api/v1/admin/orgs/{slug}/workspaces:
|
|
parameters: [{ $ref: "#/components/parameters/OrgSlug" }]
|
|
get:
|
|
tags: [cp-admin]
|
|
summary: List a tenant's workspaces + health (CP-side proxy)
|
|
description: |
|
|
CP proxies to the tenant's admin GET /workspaces so ops scripts get one
|
|
CP surface without holding per-tenant tokens or setting the WAF Origin
|
|
header. 502 on tenant-layer failure (carries http_status); 503 when the
|
|
WorkspaceLister is unwired.
|
|
operationId: adminListOrgWorkspaces
|
|
security: [{ cpAdminBearer: [] }]
|
|
responses:
|
|
"200":
|
|
description: OK.
|
|
content:
|
|
application/json:
|
|
schema: { type: object, additionalProperties: true }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
"404": { $ref: "#/components/responses/NotFound" }
|
|
"502":
|
|
description: Tenant-layer failure (auth/network/parse) — carries http_status.
|
|
content:
|
|
application/json:
|
|
schema: { type: object, additionalProperties: true }
|
|
"503":
|
|
description: WorkspaceLister not configured on this CP.
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/Error" }
|
|
|
|
/api/v1/admin/tenants/{slug}:
|
|
parameters: [{ $ref: "#/components/parameters/OrgSlug" }]
|
|
delete:
|
|
tags: [cp-admin]
|
|
summary: Admin-delete a tenant (confirm-gated teardown)
|
|
description: |
|
|
**Confirm guard:** body `confirm` MUST equal the URL slug or the
|
|
request 400s before any teardown. Reuses the executeOrgPurge cascade
|
|
(Stripe + EC2 + CF + DB rows + audit). 503 when no provisioner is wired.
|
|
operationId: adminDeleteTenant
|
|
security: [{ cpAdminBearer: [] }]
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/DeleteTenantRequest" }
|
|
responses:
|
|
"200":
|
|
description: Teardown completed.
|
|
content:
|
|
application/json:
|
|
schema: { type: object, additionalProperties: true }
|
|
"400":
|
|
description: Confirm token missing or != slug.
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/Error" }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
"503":
|
|
description: Provisioner not configured in this environment.
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/Error" }
|
|
|
|
/api/v1/admin/tenants/{slug}/scrub-artifacts:
|
|
parameters: [{ $ref: "#/components/parameters/OrgSlug" }]
|
|
post:
|
|
tags: [cp-admin]
|
|
summary: Scrub tenant artifacts (destructive, confirm-gated)
|
|
description: "**Confirm guard:** confirm-token body must equal the URL slug."
|
|
operationId: adminScrubTenantArtifacts
|
|
security: [{ cpAdminBearer: [] }]
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/DeleteTenantRequest" }
|
|
responses:
|
|
"200":
|
|
description: Scrubbed.
|
|
content:
|
|
application/json:
|
|
schema: { type: object, additionalProperties: true }
|
|
"400": { $ref: "#/components/responses/BadRequest" }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
|
|
/api/v1/admin/tenants/{slug}/diagnostics:
|
|
parameters: [{ $ref: "#/components/parameters/OrgSlug" }]
|
|
get:
|
|
tags: [cp-admin]
|
|
summary: Tenant diagnostics (read-only)
|
|
operationId: adminTenantDiagnostics
|
|
security: [{ cpAdminBearer: [] }]
|
|
responses:
|
|
"200":
|
|
description: OK.
|
|
content:
|
|
application/json:
|
|
schema: { type: object, additionalProperties: true }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
|
|
/api/v1/admin/tenants/{slug}/redeploy:
|
|
parameters: [{ $ref: "#/components/parameters/OrgSlug" }]
|
|
post:
|
|
tags: [cp-admin]
|
|
summary: Redeploy a single tenant container
|
|
operationId: adminRedeployTenant
|
|
security: [{ cpAdminBearer: [] }]
|
|
responses:
|
|
"200":
|
|
description: Redeploy dispatched.
|
|
content:
|
|
application/json:
|
|
schema: { type: object, additionalProperties: true }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
"503":
|
|
description: Redeployer not wired (dev CP without AWS SSM).
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/Error" }
|
|
|
|
/api/v1/admin/tenants/redeploy-fleet:
|
|
post:
|
|
tags: [cp-admin]
|
|
summary: Redeploy the whole tenant fleet (post-merge CI)
|
|
operationId: adminRedeployFleet
|
|
security: [{ cpAdminBearer: [] }]
|
|
requestBody:
|
|
required: false
|
|
content:
|
|
application/json:
|
|
schema: { type: object, additionalProperties: true }
|
|
responses:
|
|
"200":
|
|
description: Fleet rollout dispatched.
|
|
content:
|
|
application/json:
|
|
schema: { type: object, additionalProperties: true }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
|
|
/api/v1/admin/workspaces/{id}/env:
|
|
parameters: [{ $ref: "#/components/parameters/WorkspaceId" }]
|
|
post:
|
|
tags: [cp-admin]
|
|
summary: Mutate a running workspace's env (SSM + restart)
|
|
description: |
|
|
Merge (default) or replace env on a running workspace via SSM, then
|
|
restart. **Force guard:** keys matching TOKEN/SECRET/KEY/PASSWORD
|
|
require force=true. 503 when SSM/EC2 wiring is absent.
|
|
operationId: adminUpdateWorkspaceEnv
|
|
security: [{ cpAdminBearer: [] }]
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/UpdateWorkspaceEnvRequest" }
|
|
responses:
|
|
"200":
|
|
description: Applied.
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/UpdateWorkspaceEnvResponse" }
|
|
"400":
|
|
description: Bad body, or secret-keyword guard hit without force=true.
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/Error" }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
"404": { $ref: "#/components/responses/NotFound" }
|
|
"503":
|
|
description: SSM/EC2 wiring not present.
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/Error" }
|
|
|
|
/api/v1/admin/thin-ami/promote:
|
|
post:
|
|
tags: [cp-admin]
|
|
summary: Promote a thin-AMI pin (host AMI per region)
|
|
operationId: adminPromoteThinAmi
|
|
security: [{ cpAdminBearer: [] }]
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/PinPromoteRequest" }
|
|
responses:
|
|
"200":
|
|
description: Promoted.
|
|
content:
|
|
application/json:
|
|
schema: { type: object, additionalProperties: true }
|
|
"400": { $ref: "#/components/responses/BadRequest" }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
get:
|
|
tags: [cp-admin]
|
|
summary: List thin-AMI pins
|
|
operationId: adminListThinAmi
|
|
security: [{ cpAdminBearer: [] }]
|
|
responses:
|
|
"200":
|
|
description: OK.
|
|
content:
|
|
application/json:
|
|
schema: { type: array, items: { type: object, additionalProperties: true } }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
|
|
/api/v1/admin/runtime-image/promote:
|
|
post:
|
|
tags: [cp-admin]
|
|
summary: Promote a runtime-image pin (per-template GHCR/ECR digest)
|
|
operationId: adminPromoteRuntimeImage
|
|
security: [{ cpAdminBearer: [] }]
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/PinPromoteRequest" }
|
|
responses:
|
|
"200":
|
|
description: Promoted.
|
|
content:
|
|
application/json:
|
|
schema: { type: object, additionalProperties: true }
|
|
"400": { $ref: "#/components/responses/BadRequest" }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
get:
|
|
tags: [cp-admin]
|
|
summary: List runtime-image pins
|
|
operationId: adminListRuntimeImage
|
|
security: [{ cpAdminBearer: [] }]
|
|
responses:
|
|
"200":
|
|
description: OK.
|
|
content:
|
|
application/json:
|
|
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:
|
|
tags: [cp-provision]
|
|
summary: Provision a workspace EC2 instance
|
|
description: |
|
|
Called by tenant platforms (server-to-server). SSRF + provider-env +
|
|
runtime-pin gated. **Returns 422 RUNTIME_PIN_MISSING** when no runtime
|
|
image pin exists. Mounted only when PROVISION_SHARED_SECRET is set.
|
|
operationId: provisionWorkspace
|
|
security: [{ provisionSecret: [] }]
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/WorkspaceProvisionRequest" }
|
|
responses:
|
|
"201":
|
|
description: Provisioned.
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/WorkspaceProvisionResponse" }
|
|
"400": { $ref: "#/components/responses/BadRequest" }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
"422":
|
|
description: RUNTIME_PIN_MISSING — no runtime image pin for this runtime.
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/RuntimePinMissing" }
|
|
|
|
/api/v1/workspaces/{workspace_id}:
|
|
parameters:
|
|
- name: workspace_id
|
|
in: path
|
|
required: true
|
|
schema: { type: string, format: uuid }
|
|
delete:
|
|
tags: [cp-provision]
|
|
summary: Deprovision (terminate) a workspace EC2 + DNS
|
|
description: |
|
|
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: []
|
|
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":
|
|
description: Terminated.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
status: { type: string, const: terminated }
|
|
"400": { $ref: "#/components/responses/BadRequest" }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
|
|
/api/v1/workspaces/{workspace_id}/status:
|
|
parameters:
|
|
- name: workspace_id
|
|
in: path
|
|
required: true
|
|
schema: { type: string, format: uuid }
|
|
get:
|
|
tags: [cp-provision]
|
|
summary: Workspace instance status
|
|
operationId: getProvisionWorkspaceStatus
|
|
security: [{ provisionSecret: [] }]
|
|
parameters:
|
|
- name: instance_id
|
|
in: query
|
|
required: true
|
|
schema: { type: string }
|
|
responses:
|
|
"200":
|
|
description: OK.
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/WorkspaceProvisionStatus" }
|
|
"400": { $ref: "#/components/responses/BadRequest" }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
|
|
# ----------------------------------------------------- TENANT: WORKSPACES
|
|
/workspaces:
|
|
get:
|
|
tags: [tenant-workspaces]
|
|
summary: List workspaces in the org (tenant-admin)
|
|
operationId: tenantListWorkspaces
|
|
security:
|
|
- orgApiKey: []
|
|
orgRoutingHeaderId: []
|
|
- workosSession: []
|
|
orgRoutingHeaderSlug: []
|
|
parameters:
|
|
- { $ref: "#/components/parameters/OrgIdHeader" }
|
|
- { $ref: "#/components/parameters/OrgSlugHeader" }
|
|
responses:
|
|
"200":
|
|
description: OK.
|
|
content:
|
|
application/json:
|
|
schema: { type: array, items: { $ref: "#/components/schemas/Workspace" } }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
post:
|
|
tags: [tenant-workspaces]
|
|
summary: Create a workspace (tenant-admin)
|
|
description: |
|
|
AdminAuth. Rejected for a bare per-workspace token when ADMIN_TOKEN is
|
|
set — use the Org API Key. POST/secrets-at-create auto-restart applies.
|
|
operationId: tenantCreateWorkspace
|
|
security:
|
|
- orgApiKey: []
|
|
orgRoutingHeaderId: []
|
|
- workosSession: []
|
|
orgRoutingHeaderSlug: []
|
|
parameters:
|
|
- { $ref: "#/components/parameters/OrgIdHeader" }
|
|
- { $ref: "#/components/parameters/OrgSlugHeader" }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/CreateWorkspaceRequest" }
|
|
responses:
|
|
"201":
|
|
description: Created.
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/Workspace" }
|
|
"400": { $ref: "#/components/responses/BadRequest" }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
|
|
/workspaces/{id}:
|
|
parameters: [{ $ref: "#/components/parameters/WorkspaceId" }]
|
|
get:
|
|
tags: [tenant-workspaces]
|
|
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: []
|
|
parameters:
|
|
- { $ref: "#/components/parameters/OrgIdHeader" }
|
|
- { $ref: "#/components/parameters/OrgSlugHeader" }
|
|
responses:
|
|
"200":
|
|
description: OK.
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/Workspace" }
|
|
"404": { $ref: "#/components/responses/NotFound" }
|
|
delete:
|
|
tags: [tenant-workspaces]
|
|
summary: Delete a workspace (cascade; tenant-admin)
|
|
operationId: tenantDeleteWorkspace
|
|
security:
|
|
- orgApiKey: []
|
|
orgRoutingHeaderId: []
|
|
parameters:
|
|
- { $ref: "#/components/parameters/OrgIdHeader" }
|
|
- { $ref: "#/components/parameters/OrgSlugHeader" }
|
|
responses:
|
|
"200":
|
|
description: Deleted.
|
|
content:
|
|
application/json:
|
|
schema: { type: object, additionalProperties: true }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
"404": { $ref: "#/components/responses/NotFound" }
|
|
|
|
/workspaces/{id}/restart:
|
|
parameters: [{ $ref: "#/components/parameters/WorkspaceId" }]
|
|
post:
|
|
tags: [tenant-workspaces]
|
|
summary: Restart a workspace
|
|
operationId: tenantRestartWorkspace
|
|
security:
|
|
- orgApiKey: []
|
|
orgRoutingHeaderId: []
|
|
- workspaceToken: []
|
|
orgRoutingHeaderId: []
|
|
parameters:
|
|
- { $ref: "#/components/parameters/OrgIdHeader" }
|
|
- { $ref: "#/components/parameters/OrgSlugHeader" }
|
|
responses:
|
|
"200": { description: Restart dispatched. }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
|
|
/workspaces/{id}/pause:
|
|
parameters: [{ $ref: "#/components/parameters/WorkspaceId" }]
|
|
post:
|
|
tags: [tenant-workspaces]
|
|
summary: Pause a workspace (stops container)
|
|
operationId: tenantPauseWorkspace
|
|
security:
|
|
- orgApiKey: []
|
|
orgRoutingHeaderId: []
|
|
- workspaceToken: []
|
|
orgRoutingHeaderId: []
|
|
parameters:
|
|
- { $ref: "#/components/parameters/OrgIdHeader" }
|
|
- { $ref: "#/components/parameters/OrgSlugHeader" }
|
|
responses:
|
|
"200": { description: Paused. }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
|
|
/workspaces/{id}/resume:
|
|
parameters: [{ $ref: "#/components/parameters/WorkspaceId" }]
|
|
post:
|
|
tags: [tenant-workspaces]
|
|
summary: Resume a paused workspace
|
|
operationId: tenantResumeWorkspace
|
|
security:
|
|
- orgApiKey: []
|
|
orgRoutingHeaderId: []
|
|
- workspaceToken: []
|
|
orgRoutingHeaderId: []
|
|
parameters:
|
|
- { $ref: "#/components/parameters/OrgIdHeader" }
|
|
- { $ref: "#/components/parameters/OrgSlugHeader" }
|
|
responses:
|
|
"200": { description: Resumed. }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
|
|
/workspaces/{id}/budget:
|
|
parameters: [{ $ref: "#/components/parameters/WorkspaceId" }]
|
|
get:
|
|
tags: [tenant-workspaces]
|
|
summary: Get workspace budget + spend
|
|
operationId: tenantGetBudget
|
|
security:
|
|
- orgApiKey: []
|
|
orgRoutingHeaderId: []
|
|
- workspaceToken: []
|
|
orgRoutingHeaderId: []
|
|
parameters:
|
|
- { $ref: "#/components/parameters/OrgIdHeader" }
|
|
- { $ref: "#/components/parameters/OrgSlugHeader" }
|
|
responses:
|
|
"200":
|
|
description: OK.
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/BudgetResponse" }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
"404": { $ref: "#/components/responses/NotFound" }
|
|
patch:
|
|
tags: [tenant-workspaces]
|
|
summary: Set workspace budget limit (tenant-admin)
|
|
operationId: tenantPatchBudget
|
|
security:
|
|
- orgApiKey: []
|
|
orgRoutingHeaderId: []
|
|
parameters:
|
|
- { $ref: "#/components/parameters/OrgIdHeader" }
|
|
- { $ref: "#/components/parameters/OrgSlugHeader" }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/PatchBudgetRequest" }
|
|
responses:
|
|
"200":
|
|
description: Updated.
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/BudgetResponse" }
|
|
"400": { $ref: "#/components/responses/BadRequest" }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
|
|
/admin/workspaces/{id}/llm-billing-mode:
|
|
parameters: [{ $ref: "#/components/parameters/WorkspaceId" }]
|
|
get:
|
|
tags: [tenant-workspaces]
|
|
summary: Get workspace LLM billing mode
|
|
operationId: tenantGetBillingMode
|
|
security:
|
|
- orgApiKey: []
|
|
orgRoutingHeaderId: []
|
|
parameters:
|
|
- { $ref: "#/components/parameters/OrgIdHeader" }
|
|
- { $ref: "#/components/parameters/OrgSlugHeader" }
|
|
responses:
|
|
"200":
|
|
description: OK.
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/BillingModeResponse" }
|
|
"400": { $ref: "#/components/responses/BadRequest" }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
put:
|
|
tags: [tenant-workspaces]
|
|
summary: Set/clear workspace LLM billing mode (tenant-admin)
|
|
description: '{"mode":"byok"} sets override; {"mode":null} clears; {} → 400.'
|
|
operationId: tenantPutBillingMode
|
|
security:
|
|
- orgApiKey: []
|
|
orgRoutingHeaderId: []
|
|
parameters:
|
|
- { $ref: "#/components/parameters/OrgIdHeader" }
|
|
- { $ref: "#/components/parameters/OrgSlugHeader" }
|
|
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": { $ref: "#/components/responses/NotFound" }
|
|
|
|
# ------------------------------------------------------ TENANT: WS SECRETS
|
|
/workspaces/{id}/secrets:
|
|
parameters: [{ $ref: "#/components/parameters/WorkspaceId" }]
|
|
get:
|
|
tags: [tenant-workspaces]
|
|
summary: List workspace secrets (keys only)
|
|
operationId: tenantListWorkspaceSecrets
|
|
security:
|
|
- orgApiKey: []
|
|
orgRoutingHeaderId: []
|
|
- workspaceToken: []
|
|
orgRoutingHeaderId: []
|
|
parameters:
|
|
- { $ref: "#/components/parameters/OrgIdHeader" }
|
|
- { $ref: "#/components/parameters/OrgSlugHeader" }
|
|
responses:
|
|
"200":
|
|
description: OK (values masked).
|
|
content:
|
|
application/json:
|
|
schema: { type: array, items: { $ref: "#/components/schemas/SecretSummary" } }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
post:
|
|
tags: [tenant-workspaces]
|
|
summary: Set a workspace secret (auto-restarts the workspace)
|
|
description: |
|
|
THE secret/env lever. Upserts AES-256-GCM, emits secret.set audit,
|
|
auto-restarts the workspace. platform-managed billing strips vendor-LLM
|
|
keys unless billing-mode=byok; GIT_HTTP_* never stripped.
|
|
operationId: tenantSetWorkspaceSecret
|
|
security:
|
|
- orgApiKey: []
|
|
orgRoutingHeaderId: []
|
|
- workspaceToken: []
|
|
orgRoutingHeaderId: []
|
|
parameters:
|
|
- { $ref: "#/components/parameters/OrgIdHeader" }
|
|
- { $ref: "#/components/parameters/OrgSlugHeader" }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/SetSecretRequest" }
|
|
responses:
|
|
"200": { description: Set. }
|
|
"400": { $ref: "#/components/responses/BadRequest" }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
put:
|
|
tags: [tenant-workspaces]
|
|
summary: Upsert a workspace secret (alias of POST)
|
|
operationId: tenantPutWorkspaceSecret
|
|
security:
|
|
- orgApiKey: []
|
|
orgRoutingHeaderId: []
|
|
- workspaceToken: []
|
|
orgRoutingHeaderId: []
|
|
parameters:
|
|
- { $ref: "#/components/parameters/OrgIdHeader" }
|
|
- { $ref: "#/components/parameters/OrgSlugHeader" }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/SetSecretRequest" }
|
|
responses:
|
|
"200": { description: Set. }
|
|
"400": { $ref: "#/components/responses/BadRequest" }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
|
|
/workspaces/{id}/secrets/{key}:
|
|
parameters:
|
|
- { $ref: "#/components/parameters/WorkspaceId" }
|
|
- name: key
|
|
in: path
|
|
required: true
|
|
schema: { type: string }
|
|
delete:
|
|
tags: [tenant-workspaces]
|
|
summary: Delete a workspace secret (auto-restarts)
|
|
operationId: tenantDeleteWorkspaceSecret
|
|
security:
|
|
- orgApiKey: []
|
|
orgRoutingHeaderId: []
|
|
- workspaceToken: []
|
|
orgRoutingHeaderId: []
|
|
parameters:
|
|
- { $ref: "#/components/parameters/OrgIdHeader" }
|
|
- { $ref: "#/components/parameters/OrgSlugHeader" }
|
|
responses:
|
|
"200": { description: Deleted. }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
"404": { $ref: "#/components/responses/NotFound" }
|
|
|
|
# -------------------------------------------------- TENANT: GLOBAL SECRETS
|
|
/settings/secrets:
|
|
get:
|
|
tags: [tenant-secrets]
|
|
summary: List org-wide (global) secrets (keys only)
|
|
operationId: tenantListGlobalSecrets
|
|
security:
|
|
- orgApiKey: []
|
|
orgRoutingHeaderId: []
|
|
parameters:
|
|
- { $ref: "#/components/parameters/OrgIdHeader" }
|
|
- { $ref: "#/components/parameters/OrgSlugHeader" }
|
|
responses:
|
|
"200":
|
|
description: OK (values masked).
|
|
content:
|
|
application/json:
|
|
schema: { type: array, items: { $ref: "#/components/schemas/SecretSummary" } }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
post:
|
|
tags: [tenant-secrets]
|
|
summary: Set an org-wide secret (fan-out auto-restart)
|
|
description: |
|
|
Sets a global secret; auto-restarts every non-paused/non-removed/
|
|
non-external workspace that doesn't shadow the key (issue #15).
|
|
operationId: tenantSetGlobalSecret
|
|
security:
|
|
- orgApiKey: []
|
|
orgRoutingHeaderId: []
|
|
parameters:
|
|
- { $ref: "#/components/parameters/OrgIdHeader" }
|
|
- { $ref: "#/components/parameters/OrgSlugHeader" }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/SetSecretRequest" }
|
|
responses:
|
|
"200": { description: Set. }
|
|
"400": { $ref: "#/components/responses/BadRequest" }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
put:
|
|
tags: [tenant-secrets]
|
|
summary: Upsert an org-wide secret (alias of POST)
|
|
operationId: tenantPutGlobalSecret
|
|
security:
|
|
- orgApiKey: []
|
|
orgRoutingHeaderId: []
|
|
parameters:
|
|
- { $ref: "#/components/parameters/OrgIdHeader" }
|
|
- { $ref: "#/components/parameters/OrgSlugHeader" }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/SetSecretRequest" }
|
|
responses:
|
|
"200": { description: Set. }
|
|
"400": { $ref: "#/components/responses/BadRequest" }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
|
|
/settings/secrets/{key}:
|
|
parameters:
|
|
- name: key
|
|
in: path
|
|
required: true
|
|
schema: { type: string }
|
|
delete:
|
|
tags: [tenant-secrets]
|
|
summary: Delete an org-wide secret (fan-out auto-restart)
|
|
operationId: tenantDeleteGlobalSecret
|
|
security:
|
|
- orgApiKey: []
|
|
orgRoutingHeaderId: []
|
|
parameters:
|
|
- { $ref: "#/components/parameters/OrgIdHeader" }
|
|
- { $ref: "#/components/parameters/OrgSlugHeader" }
|
|
responses:
|
|
"200": { description: Deleted. }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
"404": { $ref: "#/components/responses/NotFound" }
|
|
|
|
# ------------------------------------------------- TENANT: ORG TOKENS / KEYS
|
|
/org/tokens:
|
|
get:
|
|
tags: [tenant-org]
|
|
summary: List Org API Keys (prefix + metadata)
|
|
operationId: tenantListOrgTokens
|
|
security:
|
|
- orgApiKey: []
|
|
orgRoutingHeaderId: []
|
|
parameters:
|
|
- { $ref: "#/components/parameters/OrgIdHeader" }
|
|
- { $ref: "#/components/parameters/OrgSlugHeader" }
|
|
responses:
|
|
"200":
|
|
description: OK.
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/OrgTokenList" }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
post:
|
|
tags: [tenant-org]
|
|
summary: Mint an Org API Key (full tenant-admin; plaintext shown once)
|
|
description: |
|
|
Mints a new Org API Key. Plaintext is returned exactly ONCE. The key is
|
|
full-tenant-admin and can itself mint/revoke more keys (treat as tenant
|
|
root). Empty body mints an unnamed token.
|
|
operationId: tenantCreateOrgToken
|
|
security:
|
|
- orgApiKey: []
|
|
orgRoutingHeaderId: []
|
|
parameters:
|
|
- { $ref: "#/components/parameters/OrgIdHeader" }
|
|
- { $ref: "#/components/parameters/OrgSlugHeader" }
|
|
requestBody:
|
|
required: false
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/CreateOrgTokenRequest" }
|
|
responses:
|
|
"200":
|
|
description: Minted (plaintext shown once).
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/CreateOrgTokenResponse" }
|
|
"400": { $ref: "#/components/responses/BadRequest" }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
|
|
/org/tokens/{id}:
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema: { type: string }
|
|
delete:
|
|
tags: [tenant-org]
|
|
summary: Revoke an Org API Key
|
|
operationId: tenantRevokeOrgToken
|
|
security:
|
|
- orgApiKey: []
|
|
orgRoutingHeaderId: []
|
|
parameters:
|
|
- { $ref: "#/components/parameters/OrgIdHeader" }
|
|
- { $ref: "#/components/parameters/OrgSlugHeader" }
|
|
responses:
|
|
"200":
|
|
description: Revoked.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
revoked: { type: string }
|
|
"400": { $ref: "#/components/responses/BadRequest" }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
"404": { $ref: "#/components/responses/NotFound" }
|
|
|
|
# ------------------------------------------------- TENANT: ORG IMPORT / TMPL
|
|
/org/import:
|
|
post:
|
|
tags: [tenant-org]
|
|
summary: Create workspaces from an org template (tenant-admin)
|
|
description: |
|
|
AdminAuth. Provide `dir` (server-side org template, traversal-guarded)
|
|
OR an inline `template`. `mode=reconcile` cascade-deletes zombie
|
|
workspaces left by a prior import.
|
|
operationId: tenantOrgImport
|
|
security:
|
|
- orgApiKey: []
|
|
orgRoutingHeaderId: []
|
|
parameters:
|
|
- { $ref: "#/components/parameters/OrgIdHeader" }
|
|
- { $ref: "#/components/parameters/OrgSlugHeader" }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/OrgImportRequest" }
|
|
responses:
|
|
"200":
|
|
description: Imported (workspace result set).
|
|
content:
|
|
application/json:
|
|
schema: { type: object, additionalProperties: true }
|
|
"400": { $ref: "#/components/responses/BadRequest" }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
"404": { $ref: "#/components/responses/NotFound" }
|
|
|
|
/org/templates:
|
|
get:
|
|
tags: [tenant-org]
|
|
summary: List available org templates (tenant-admin)
|
|
operationId: tenantListOrgTemplates
|
|
security:
|
|
- orgApiKey: []
|
|
orgRoutingHeaderId: []
|
|
parameters:
|
|
- { $ref: "#/components/parameters/OrgIdHeader" }
|
|
- { $ref: "#/components/parameters/OrgSlugHeader" }
|
|
responses:
|
|
"200":
|
|
description: OK.
|
|
content:
|
|
application/json:
|
|
schema: { type: array, items: { type: object, additionalProperties: true } }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
|
|
/templates:
|
|
get:
|
|
tags: [tenant-org]
|
|
summary: List workspace templates (tenant-admin)
|
|
operationId: tenantListTemplates
|
|
security:
|
|
- orgApiKey: []
|
|
orgRoutingHeaderId: []
|
|
parameters:
|
|
- { $ref: "#/components/parameters/OrgIdHeader" }
|
|
- { $ref: "#/components/parameters/OrgSlugHeader" }
|
|
responses:
|
|
"200":
|
|
description: OK.
|
|
content:
|
|
application/json:
|
|
schema: { type: array, items: { type: object, additionalProperties: true } }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
|
|
/templates/import:
|
|
post:
|
|
tags: [tenant-org]
|
|
summary: Import a workspace template (writes config files; tenant-admin)
|
|
operationId: tenantImportTemplate
|
|
security:
|
|
- orgApiKey: []
|
|
orgRoutingHeaderId: []
|
|
parameters:
|
|
- { $ref: "#/components/parameters/OrgIdHeader" }
|
|
- { $ref: "#/components/parameters/OrgSlugHeader" }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/TemplateImportRequest" }
|
|
responses:
|
|
"201":
|
|
description: Imported.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
status: { type: string }
|
|
id: { type: string }
|
|
name: { type: string }
|
|
"400": { $ref: "#/components/responses/BadRequest" }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
"409": { $ref: "#/components/responses/Conflict" }
|
|
|
|
# ----------------------------------------------------------- TENANT: BUNDLES
|
|
/bundles/export/{id}:
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema: { type: string }
|
|
get:
|
|
tags: [tenant-bundles]
|
|
summary: Export a workspace bundle (tenant-admin)
|
|
description: HIGH-sensitivity — full system prompts + memory. Admin-gated (#165).
|
|
operationId: tenantExportBundle
|
|
security:
|
|
- orgApiKey: []
|
|
orgRoutingHeaderId: []
|
|
parameters:
|
|
- { $ref: "#/components/parameters/OrgIdHeader" }
|
|
- { $ref: "#/components/parameters/OrgSlugHeader" }
|
|
responses:
|
|
"200":
|
|
description: OK (bundle).
|
|
content:
|
|
application/json:
|
|
schema: { type: object, additionalProperties: true }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
"404": { $ref: "#/components/responses/NotFound" }
|
|
|
|
/bundles/import:
|
|
post:
|
|
tags: [tenant-bundles]
|
|
summary: Import a workspace bundle (tenant-admin)
|
|
description: CRITICAL — anon creation of arbitrary workspaces if unguarded. Admin-gated (#164).
|
|
operationId: tenantImportBundle
|
|
security:
|
|
- orgApiKey: []
|
|
orgRoutingHeaderId: []
|
|
parameters:
|
|
- { $ref: "#/components/parameters/OrgIdHeader" }
|
|
- { $ref: "#/components/parameters/OrgSlugHeader" }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/BundleImportRequest" }
|
|
responses:
|
|
"201":
|
|
description: Imported.
|
|
content:
|
|
application/json:
|
|
schema: { type: object, additionalProperties: true }
|
|
"400": { $ref: "#/components/responses/BadRequest" }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|