openapi: 3.1.0 info: title: Simolyot / Polygon Orchestrator API version: "1.1.0" description: | Platform-facing REST + SSE surface of the Polygon orchestrator (the control plane behind Simolyot). Mirrors `proto/polygon/v1` (JobService) and is what robofirst's cloud-sim proxy, the web SPA, the Python SDK, and the `polygon` CLI talk to. **1.1.0** adds the full live surface: realtime streams (list / subscribe / consume / recording), job `rerun` / `share` / `artifacts`, node metrics / self-update / join-tokens, per-org entitlements, job analytics, the audit trail's org scoping, the operator channel-target controls, the artifact-zip and health routes, and outgoing **webhooks** (signed run-completion callbacks). ## Authentication All `/v1` routes accept a bearer token (`Authorization: Bearer `). Enforcement is controlled server-side by `POLYGON_AUTH`: * `off` (default) — token is attached if present, never required. * `shadow` — logs what would be denied, still allows. * `required` — anonymous requests to protected paths get `401`. On the live deploy `POLYGON_AUTH=required`. A handful of routes stay public even in `required` mode: `GET /healthz` (+ `GET /v1/healthz`), `GET /v1/auth/whoami`, `GET /v1/roles`, the artifact/upload routes, and the token-admin routes (which carry their own `POLYGON_ADMIN_TOKEN` gate). Token administration (`/v1/auth/tokens*`) and `/v1/admin/*` are gated by the operator admin token (`POLYGON_ADMIN_TOKEN`), passed as the bearer. ## Authorization tiers Mutating and management routes enforce one of three server-side checks: * **scope** (`requireScope`) — a mutating run route needs the matching RBAC scope on the caller's token (e.g. `runs:submit`, `runs:cancel`, `runs:priority`). An anonymous caller in an open deploy is left alone; a real token that lacks the scope gets `403`. * **manage** (`authorizeManage`) — token administration, webhooks, and the org-scoped audit trail accept EITHER the operator admin token (superuser, any org) OR an org owner/admin role confined to its **own** org. A non-superuser is silently narrowed to its own org (cross-org ids return `404`, never an existence oracle). * **admin** (`requireAdmin` / operator-only) — fleet-wide controls (node join-tokens, node self-update, channel promotion) require the operator admin token. ## Org scoping Reads are tenant-filtered: an authenticated principal sees only its own org's runs / nodes-load / analytics; an anonymous caller in an open (`off`) deploy sees everything. Cross-org resource ids resolve to `404` rather than `403` so an id can't be probed for existence. ## Server-Sent Events `GET /v1/jobs/{id}/events` streams live telemetry as `text/event-stream`. Because browser `EventSource` cannot set headers, this route (and only this route) also accepts the token as a `?token=` query parameter. Realtime data streams (`GET /v1/jobs/{id}/streams/{name}/data`) are SSE too, but gated by a short-lived opaque **ticket** minted from `.../subscribe`, not the bearer. ## Webhooks An org owner/admin (or the operator admin token) can register a signed callback fired when a run finishes. Event types: `run.succeeded`, `run.failed`, `run.cancelled`. Each delivery is an HTTP POST carrying the headers `X-Simolyot-Event`, `X-Simolyot-Delivery`, `X-Simolyot-Timestamp`, and `X-Simolyot-Signature: sha256=`, where the signature is `HMAC-SHA256(secret, ".")` (the GitHub/Stripe convention; the timestamp is bound inside the MAC to defeat replay). The signing `secret` is returned exactly once, at endpoint creation. servers: - url: http://localhost:8080 description: Local orchd - url: https://polygon.robofirst.dev description: Live deploy (POLYGON_AUTH=required) security: - bearerAuth: [] tags: - name: jobs description: Submit, inspect, control, rerun, share, and stream runs. - name: streams description: Realtime run streams — list, subscribe (ticket), consume, and replay. - name: runs description: RunSpec — layered config (platform` — validated against (job, stream). Recent chunks are replayed first, then the live tail; each frame is `event: stream` with the chunk JSON in `data:`. security: - ticketQuery: [] - bearerAuth: [] parameters: - $ref: '#/components/parameters/JobId' - $ref: '#/components/parameters/StreamName' - name: ticket in: query required: false description: The subscribe ticket (alternative to the bearer header). schema: { type: string } responses: '200': description: A stream of chunks. content: text/event-stream: schema: { $ref: '#/components/schemas/StreamChunk' } '401': description: Invalid or expired stream ticket. '503': description: Streams disabled. /v1/jobs/{id}/recording: get: tags: [streams] operationId: getJobRecording summary: Full replay timeline description: | The SYSTEM recording a viewer scrubs post-hoc: the metrics timeseries and log lines (each stamped with its orchd receive time), plus any durably recorded stream-data channels. Metrics/logs are always present; stream points appear only when the run enabled a stream. Same tenancy gate as `/logs`, and whitelisted for share-link reads. parameters: - $ref: '#/components/parameters/JobId' responses: '200': description: The replay timeline. content: application/json: schema: { $ref: '#/components/schemas/JobRecording' } '404': description: Not found (or cross-org). # ── Share links ─────────────────────────────────────────────────────────── /s/{code}: get: tags: [jobs] operationId: shareRedirect summary: Resolve a short share link description: | Resolves a `/s/{code}` short link to its job and `302`-redirects to the SPA run view carrying the code as `?share=`. Unknown/expired codes bounce to the home page with `?share_error=expired`. Public. security: - {} parameters: - name: code in: path required: true description: The share code minted by `POST /v1/jobs/{id}/share`. schema: { type: string } responses: '302': description: Redirect to the run view (or home on an expired code). headers: Location: description: The SPA URL the code resolves to. schema: { type: string } # ── Runs (RunSpec) ──────────────────────────────────────────────────────── /v1/runs: post: tags: [runs] operationId: createRun summary: Create a run from a RunSpec description: | Resolves a RunSpec — the full JobSpec surface PLUS four config `layers` (`platform` < `org` < `project` < `user`, folded low→high) — into an immutable `ResolvedRun`, then submits it through the SAME admission/tenancy/quota pipeline as `POST /v1/jobs`. Returns `202` with the job view; `run.id == job.id` for a single-step run. The flat top-level `config` merges into the `user` layer, so a layerless body behaves exactly like a plain submit. `?embed=1` additionally mints a read-only embed grant for the new run and returns it as an `embed` object alongside the job (the LMS one-shot flow). The `Idempotency-Key` header is honored — a retried create returns the same run. security: - bearerAuth: [] parameters: - name: embed in: query description: Set to `1` to also mint + return an embed grant for the new run. schema: { type: string, enum: ['1'] } requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/RunSpecBody' responses: '202': description: Accepted — the run was queued (job view; plus `embed` when `?embed=1`). content: application/json: schema: $ref: '#/components/schemas/Job' '400': description: Bad JSON or unknown stack. '403': description: Missing the `runs:submit` scope. '422': description: Stack not available, or invalid config value. '429': description: Org / user concurrency limit reached. get: tags: [runs] operationId: listRuns summary: List runs (history) description: | The caller's org-scoped run history, newest first — each accessible job as a run row (the job summary plus `run_id` and `has_runspec`). Filterable by `?stack`/`?project`/`?phase`, with the same opt-in cursor pagination as `GET /v1/jobs` (`?paginate=1` / `?cursor=`). parameters: - { name: phase, in: query, description: 'queued|assigned|pulling|running|succeeded|failed|timeout|cancelled', schema: { type: string } } - { name: stack, in: query, schema: { type: string } } - { name: project, in: query, schema: { type: string } } - { name: paginate, in: query, schema: { type: string, enum: ['1'] } } - { name: cursor, in: query, schema: { type: string } } - { name: limit, in: query, schema: { type: integer, default: 500 } } - { name: offset, in: query, schema: { type: integer, default: 0 } } responses: '200': description: Run rows (bare array, or a cursor envelope under `?paginate=1`). content: application/json: schema: type: array items: { $ref: '#/components/schemas/RunRow' } '400': description: Unknown `phase` filter value. /v1/runs/validate: post: tags: [runs] operationId: validateRunSpec summary: Validate a RunSpec (dry-run) description: | Resolves the layered config, then validates the effective config against the stack manifest — WITHOUT enqueuing. Same auth as submit (`runs:submit`). security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/RunSpecBody' responses: '200': description: Validation report. content: application/json: schema: type: object properties: stack: { type: string } ok: { type: boolean } errors: { type: array, items: { $ref: '#/components/schemas/RunProblem' } } warnings: { type: array, items: { $ref: '#/components/schemas/RunProblem' } } '400': description: Malformed body or unknown stack. '403': description: Missing the `runs:submit` scope. /v1/runs/resolve: post: tags: [runs] operationId: resolveRunSpec summary: Resolve a RunSpec (layer diff, dry-run) description: | Returns the immutable `ResolvedRun` (effective config + per-key provenance + the per-layer diff) plus the manifest validation, WITHOUT enqueuing — the dry-run a UI shows before launch ("what each layer contributed and what will actually run"). security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/RunSpecBody' responses: '200': description: Resolved run + validation. content: application/json: schema: type: object properties: resolved: { $ref: '#/components/schemas/ResolvedRun' } diff: type: array description: Convenience alias of `resolved.layers` (the per-layer contribution). items: { $ref: '#/components/schemas/LayerDiff' } validation: type: object properties: ok: { type: boolean } errors: { type: array, items: { $ref: '#/components/schemas/RunProblem' } } warnings: { type: array, items: { $ref: '#/components/schemas/RunProblem' } } '400': description: Malformed body or unknown stack. '403': description: Missing the `runs:submit` scope. /v1/runs/{id}: get: tags: [runs] operationId: getRun summary: Get one run description: | The job view (phase / metrics / artifacts / submitted spec) PLUS the immutable `run` snapshot (effective config + provenance + layer diff) and a `stages` array — a single-step run is exactly one stage (the job itself). Org-scoped: a cross-org / unknown id is `404`. parameters: - $ref: '#/components/parameters/JobId' responses: '200': description: The run detail. content: application/json: schema: allOf: - $ref: '#/components/schemas/Job' - type: object properties: run_id: { type: string, description: Equals the job id for a single-step run. } run: { $ref: '#/components/schemas/ResolvedRun' } stages: type: array items: { $ref: '#/components/schemas/RunStage' } '404': description: Not found (or cross-org). # ── Pipelines (DAG) ─────────────────────────────────────────────────────── /v1/pipelines: post: tags: [pipelines] operationId: submitPipeline summary: Submit a DAG pipeline description: | Validates the spec (DAG topology + each stage's RunSpec), creates a PipelineRun, and starts it — the initial ready-set (root stages) is submitted as normal admitted Jobs via the same pipeline as a single run, each inheriting the caller's org/scopes/quota. A DAG-topology failure is a `400`; a stage RunSpec failure (unknown stack / bad config) is a `422` — both BEFORE anything is enqueued. security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/PipelineBody' responses: '202': description: Accepted — the pipeline run started. content: application/json: schema: { $ref: '#/components/schemas/PipelineRun' } '400': description: Invalid pipeline DAG (cycle / unknown dep / duplicate name). '403': description: Missing the `runs:submit` scope. '422': description: One or more stages failed validation. '503': description: Pipelines disabled on this deployment. get: tags: [pipelines] operationId: listPipelines summary: List pipeline runs description: The caller's pipeline runs (org-scoped), newest first, with a `?limit`/`?offset` window. parameters: - { name: limit, in: query, schema: { type: integer, default: 500 } } - { name: offset, in: query, schema: { type: integer, default: 0 } } responses: '200': description: Pipeline-run summaries. content: application/json: schema: type: array items: { $ref: '#/components/schemas/PipelineRun' } /v1/pipelines/validate: post: tags: [pipelines] operationId: validatePipeline summary: Validate a pipeline (dry-run) description: | Validates the DAG topology (cycles / unknown deps / duplicate names) AND each stage's RunSpec against its manifest. Always `200` with a structured report; only a malformed body is a `400`. Nothing enqueued. security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/PipelineBody' responses: '200': description: Validation report. content: application/json: schema: type: object properties: ok: { type: boolean } errors: type: array description: DAG-topology problems (empty when the graph is valid). items: { $ref: '#/components/schemas/RunProblem' } stages: type: array items: type: object properties: name: { type: string } stack: { type: string } validation: type: object properties: ok: { type: boolean } errors: { type: array, items: { $ref: '#/components/schemas/RunProblem' } } warnings: { type: array, items: { $ref: '#/components/schemas/RunProblem' } } '400': description: Malformed body. '403': description: Missing the `runs:submit` scope. /v1/pipelines/{id}: get: tags: [pipelines] operationId: getPipeline summary: Get one pipeline run description: | The full DAG + every stage's phase/job_id/final_metrics + the overall status (the StageGraph data source). Org-scoped: a cross-org / unknown id is `404`. parameters: - $ref: '#/components/parameters/PipelineId' responses: '200': description: Pipeline-run detail. content: application/json: schema: { $ref: '#/components/schemas/PipelineRun' } '404': description: Not found (or cross-org). /v1/pipelines/{id}/cancel: post: tags: [pipelines] operationId: cancelPipeline summary: Cancel a pipeline run description: | Kills/leaves every non-terminal stage Job and marks pending stages cancelled. `runs:cancel` scoped; org-scoped (cross-tenant → `404`). security: - bearerAuth: [] parameters: - $ref: '#/components/parameters/PipelineId' responses: '200': description: Cancelled — the updated pipeline run. content: application/json: schema: { $ref: '#/components/schemas/PipelineRun' } '403': description: Missing the `runs:cancel` scope. '404': description: Not found (or cross-org). # ── Nodes ───────────────────────────────────────────────────────────────── /v1/nodes: get: tags: [nodes] operationId: listNodes summary: List fleet nodes description: | Returns every registered node with its capabilities and live committed load (CPU/RAM/GPU reserved by its running jobs, plus what's running). A node's `running_jobs` disclose their `job_id`/`stack` only for the caller's own org; a cross-org running slot is redacted (empty id/stack). Pagination mirrors `GET /v1/jobs`: a bare array by default, or a `{items, next_cursor, has_more}` envelope (keyset on node `id`) when `?paginate=1`/`?cursor=` is set (limit default 50, capped at 200). parameters: - name: paginate in: query description: Set to `1` to switch to the cursor-envelope response. schema: { type: string, enum: ['1'] } - name: cursor in: query description: Opaque keyset cursor from a prior page's `next_cursor`. Presence also selects envelope mode. schema: { type: string } - name: limit in: query description: Envelope mode only — default 50, capped at 200. schema: { type: integer, default: 50 } responses: '200': description: | Node list — a bare array by default, or a `{items, next_cursor, has_more}` envelope when `?paginate=1`/`?cursor=` is set. content: application/json: schema: oneOf: - type: array items: { $ref: '#/components/schemas/Node' } - $ref: '#/components/schemas/NodePage' /v1/nodes/{id}: patch: tags: [nodes] operationId: patchNode summary: Operator overrides for a node description: | Applies persisted operator overrides (rename, enable/disable/drain, resource caps). Partial — only provided fields change. Survives the node's re-registration. parameters: - name: id in: path required: true schema: { type: string } requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/NodePatch' responses: '200': description: The node's effective values after the patch. content: application/json: schema: $ref: '#/components/schemas/NodePatchResult' '400': description: Bad body. '404': description: Node not found. /v1/nodes/{id}/metrics: get: tags: [nodes] operationId: nodeMetrics summary: Node load timeseries description: | A node's committed-load timeseries (`[{ts, cpu, ram, gpu, jobs, load}]`) over the requested window, plus its RAW reported maxima (pre-override) so the operator UI can bound its allocation sliders by the node's true ceiling. Same read exposure as `GET /v1/nodes` (operational numbers, not tenant data). parameters: - name: id in: path required: true schema: { type: string } - name: window in: query description: 'Lookback duration (`15m`, `1h`, `24h`). Default 1h, capped at 24h.' schema: { type: string, default: '1h' } responses: '200': description: The node's load samples. content: application/json: schema: { $ref: '#/components/schemas/NodeMetrics' } '404': description: Node not found. /v1/nodes/{id}/update: post: tags: [nodes] operationId: updateNode summary: Nudge a node to self-update (operator) description: | Operator-only. Pushes the `UpdateTo` frame over the node's stream so the agent triggers its self-update now (the host systemd timer is the steady-state auto-updater). Reports current vs target version and whether the signal was delivered. Requires the operator admin token. security: - adminBearer: [] parameters: - name: id in: path required: true schema: { type: string } responses: '200': description: Update signal delivered (node connected). content: application/json: schema: { $ref: '#/components/schemas/NodeUpdateResult' } '202': description: Accepted but not delivered now (node offline / transport down); it will pick up the target on its next tick. content: application/json: schema: { $ref: '#/components/schemas/NodeUpdateResult' } '403': description: Node management requires the operator admin token. '404': description: Node not found. /v1/nodes/join-tokens: post: tags: [nodes] operationId: mintJoinToken summary: Mint a node join token (operator) description: | Issues a tier-bound node join token — the operator shares its one-line enroll command with a contributor. Operator-admin-gated; the token is shown exactly once. An invalid tier defaults to `community`. security: - adminBearer: [] requestBody: required: false content: application/json: schema: type: object properties: tier: type: string enum: [community, partner, trusted] org_id: { type: string } label: { type: string } responses: '201': description: Token minted — shown once. content: application/json: schema: { $ref: '#/components/schemas/JoinTokenMint' } '401': description: Not an admin. '503': description: Enrollment disabled. get: tags: [nodes] operationId: listJoinTokens summary: List node join tokens (operator) security: - adminBearer: [] responses: '200': description: Secret-free join-token list. content: application/json: schema: type: array items: { $ref: '#/components/schemas/JoinTokenView' } '401': description: Not an admin. /v1/nodes/join-tokens/{id}: delete: tags: [nodes] operationId: revokeJoinToken summary: Revoke a node join token (operator) security: - adminBearer: [] parameters: - name: id in: path required: true schema: { type: string } responses: '200': description: Revoked. content: application/json: schema: type: object properties: revoked: { type: string } '401': description: Not an admin. '404': description: Join token not found. # ── Stacks ──────────────────────────────────────────────────────────────── /v1/stacks: get: tags: [stacks] operationId: listStacks summary: List base stacks description: The catalog of base environments. `view=launch` is the lean Human Launch Wizard projection; `view=summary` is the MCP discovery projection. parameters: - name: view in: query description: Optional lean projection. `launch` keeps config/resource/launch-guide fields but omits OCI image, docs and examples. schema: { type: string, enum: [summary, launch] } responses: '200': description: Stack catalog. content: application/json: schema: type: array items: $ref: '#/components/schemas/Stack' # ── Analytics ───────────────────────────────────────────────────────────── /v1/analytics/jobs: get: tags: [analytics] operationId: jobAnalytics summary: Job launches/outcomes over time description: | Fleet job launches/outcomes bucketed across the window (`[{ts, submitted, succeeded, failed, cancelled}]`), plus a rollup summary (adding live `running`/`queued`). Counts respect tenancy: an authenticated caller sees only its own org's jobs; anon/admin sees all. parameters: - name: window in: query description: 'Lookback duration (`24h`, `7d`). Default 24h, capped at 30d.' schema: { type: string, default: '24h' } - name: buckets in: query description: Number of time buckets (default 48, 1–500). schema: { type: integer, default: 48 } responses: '200': description: Bucketed series + summary. content: application/json: schema: { $ref: '#/components/schemas/JobAnalytics' } # ── Auth ────────────────────────────────────────────────────────────────── /v1/auth/whoami: get: tags: [auth] operationId: whoami summary: The calling principal description: | Reports the caller's resolved identity (kind `anon` if unauthenticated). Public — reachable even in `required` mode. security: - bearerAuth: [] - {} responses: '200': description: Principal. content: application/json: schema: $ref: '#/components/schemas/Principal' /v1/me/entitlements: get: tags: [auth] operationId: entitlements summary: Usage vs quota (budget meter) description: | The caller org's in-flight usage against its concurrency quotas — the budget meter the UI shows and the number a client checks before a big sweep. Quotas come from env (`POLYGON_MAX_CONCURRENT_ORG`, `POLYGON_MAX_CONCURRENT_USER`; a per-user cap of `0` means unlimited and is omitted from `remaining`). responses: '200': description: Entitlements. content: application/json: schema: { $ref: '#/components/schemas/Entitlements' } /v1/policy/preflight: get: tags: [policy] operationId: policyPreflightGet summary: Preflight a submit (dry-run admission) description: | Runs the same admission gate as `POST /v1/jobs` — resource clamps (`POLYGON_MAX_*`) and concurrency quotas (`POLYGON_MAX_CONCURRENT_ORG/USER`) — WITHOUT enqueuing anything, so a client/UI can grey out fields or check before a large sweep. Same auth as submit (`runs:submit`, org from the principal). parameters: - { name: stack, in: query, schema: { type: string } } - { name: cpu_cores, in: query, schema: { type: number } } - { name: ram_gb, in: query, schema: { type: number } } - { name: gpu_count, in: query, schema: { type: integer } } - { name: timeout_sec, in: query, schema: { type: integer } } - { name: isolation, in: query, schema: { type: string } } security: [{ bearerAuth: [] }] responses: '200': description: Admission decision (dry-run). content: application/json: schema: { $ref: '#/components/schemas/PolicyPreflight' } '401': { description: Authentication required. } '403': { description: Missing runs:submit scope. } post: tags: [policy] operationId: policyPreflightPost summary: Preflight a submit (dry-run admission) description: | POST form of the preflight — accepts the submit `limits` shape in the body. Identical decision to the GET form; runs the admission gate without enqueuing. security: [{ bearerAuth: [] }] requestBody: content: application/json: schema: type: object properties: stack: { type: string } limits: type: object properties: cpu_cores: { type: number } ram_gb: { type: number } gpu_count: { type: integer } timeout_sec: { type: integer } isolation: { type: string } responses: '200': description: Admission decision (dry-run). content: application/json: schema: { $ref: '#/components/schemas/PolicyPreflight' } '401': { description: Authentication required. } '403': { description: Missing runs:submit scope. } /v1/policy: get: tags: [policy] operationId: getPolicy summary: Resolved policy for an org description: | The platform seed, the stored org/project/user layers, and the folded `effective` caps (with per-axis provenance). Admin-gated (operator admin token, or an org owner/admin confined to its own org). A superuser targets any org with `?org=`; a cross-tenant `?org=` is `404`. security: [{ bearerAuth: [] }] parameters: - { name: org, in: query, schema: { type: string }, description: 'Target org (superuser only; otherwise ignored/own org).' } responses: '200': description: Resolved policy view. content: application/json: schema: { $ref: '#/components/schemas/PolicyView' } '401': { description: Not authorized. } '404': { description: Cross-tenant org. } '503': { description: Policy engine disabled. } put: tags: [policy] operationId: putPolicy summary: Set an org's policy layers description: | Replaces the org's policy doc — its `org`, per-`projects`, and per-`users` cap layers. The platform layer is not settable (it is the env seed). An empty doc removes all refinements. Admin-gated, org-scoped. security: [{ bearerAuth: [] }] parameters: - { name: org, in: query, schema: { type: string }, description: 'Target org (superuser only).' } requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/PolicyDoc' } responses: '200': description: Stored — returns the doc and the newly-folded effective caps. content: application/json: schema: type: object properties: org_id: { type: string } policy: { $ref: '#/components/schemas/PolicyDoc' } effective: { $ref: '#/components/schemas/EffectivePolicy' } '400': { description: Bad body. } '401': { description: Not authorized. } '503': { description: Policy engine disabled. } /v1/policy/preview: get: tags: [policy] operationId: policyPreviewGet summary: Preview effective policy as a user description: | Resolves the effective caps for a named `subject` (and optional `project` / `stack`) WITHOUT enqueuing — the admin "what would this person actually get?" tool. With a `stack`, also returns the EffectiveManifest (the stack's config surface intersected with the caps). Admin-gated, org-scoped. security: [{ bearerAuth: [] }] parameters: - { name: org, in: query, schema: { type: string } } - { name: subject, in: query, schema: { type: string }, description: 'The user id to preview (alias: user).' } - { name: project, in: query, schema: { type: string } } - { name: stack, in: query, schema: { type: string } } responses: '200': description: Previewed effective policy. content: application/json: schema: { $ref: '#/components/schemas/PolicyPreview' } '401': { description: Not authorized. } '404': { description: Cross-tenant org. } '503': { description: Policy engine disabled. } post: tags: [policy] operationId: policyPreviewPost summary: Preview effective policy as a user description: POST form of the preview — same fields accepted as query or JSON body. security: [{ bearerAuth: [] }] requestBody: content: application/json: schema: type: object properties: subject: { type: string } project: { type: string } stack: { type: string } responses: '200': description: Previewed effective policy. content: application/json: schema: { $ref: '#/components/schemas/PolicyPreview' } '401': { description: Not authorized. } '503': { description: Policy engine disabled. } /v1/sessions: post: tags: [sessions] operationId: submitSession summary: Launch an interactive session (a multi-service pod) description: | Launches a long-lived pod a human drives interactively. The pod is a list of services started in topological order (`needs`) behind optional `health` gates. A session is a `Mode=session` job, so it is tenanted and quota-counted exactly like a run (it reserves against the reservation ledger and releases on stop). keepalive + an idle-TTL reaper bound its lifetime. Needs `runs:submit`. security: [{ bearerAuth: [] }] requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/SessionBody' } responses: '202': description: Session accepted (phase launching). content: application/json: schema: { $ref: '#/components/schemas/SessionView' } '400': { description: Missing pod, or an invalid pod DAG (cycle / unknown needs). } '422': { description: Unknown or unavailable stack for a service. } '429': { description: Over the org/user concurrency quota. } '503': { description: Sessions disabled on this deployment. } get: tags: [sessions] operationId: listSessions summary: List the caller's sessions description: The caller's sessions (org-scoped), newest first. security: [{ bearerAuth: [] }] responses: '200': description: Sessions. content: application/json: schema: type: array items: { $ref: '#/components/schemas/SessionView' } /v1/sessions/{id}: get: tags: [sessions] operationId: getSession summary: Get one session security: [{ bearerAuth: [] }] parameters: - { name: id, in: path, required: true, schema: { type: string } } responses: '200': description: Session detail (includes services + actions). content: application/json: schema: { $ref: '#/components/schemas/SessionView' } '404': { description: Unknown or cross-tenant session. } delete: tags: [sessions] operationId: stopSession summary: Stop a session description: | Cancels the pod job (tearing the pod down on its node) and marks the session stopped; the terminal phase releases the quota reservation. Needs `runs:cancel`. security: [{ bearerAuth: [] }] parameters: - { name: id, in: path, required: true, schema: { type: string } } responses: '200': description: Stopped. content: application/json: schema: { $ref: '#/components/schemas/SessionView' } '404': { description: Unknown or cross-tenant session. } /v1/sessions/{id}/keepalive: post: tags: [sessions] operationId: keepaliveSession summary: Keep a session alive description: | Bumps the session's last-keepalive so the idle reaper leaves it alive. A live client must call this well within `idle_ttl_sec`. Needs `runs:submit`. security: [{ bearerAuth: [] }] parameters: - { name: id, in: path, required: true, schema: { type: string } } responses: '200': description: Kept alive. content: application/json: schema: { $ref: '#/components/schemas/SessionView' } '404': { description: Unknown or cross-tenant session. } '409': { description: Session is not live. } /v1/sessions/{id}/signal: post: tags: [sessions] operationId: sessionSignal summary: WebRTC signaling (DEFERRED — 501) description: | Documented stub for the WebRTC media plane (SFU-lite relay + coturn), which is infra-gated and NOT wired in this environment. Always returns `501` with the signaling seam. Interactive control today is the `/data` teleop channel. security: [{ bearerAuth: [] }] parameters: - { name: id, in: path, required: true, schema: { type: string } } responses: '501': { description: WebRTC media relay not enabled (deferred). } '404': { description: Unknown or cross-tenant session. } /v1/jobs/{id}/streams/{name}/teleop: post: tags: [streams] operationId: streamTeleop summary: Send a teleop control frame (bidirectional /data) description: | The downstream (write) half of the bidirectional `/data` channel: push a control frame to the pod running the job. Authz is the short-lived HMAC stream **ticket** minted from `.../subscribe` (query `?ticket=` or a bearer of the ticket) — a ticketless or cross-org send is rejected. The node writes the frame to `/streams/.teleop` for a service to consume. security: [{ bearerAuth: [] }] parameters: - { name: id, in: path, required: true, schema: { type: string } } - { name: name, in: path, required: true, schema: { type: string } } - { name: ticket, in: query, schema: { type: string }, description: 'The stream ticket (or pass it as the bearer).' } requestBody: content: application/json: schema: { type: object, description: 'Opaque control frame (≤256 KiB), forwarded verbatim to the pod.' } responses: '202': description: Delivered. content: application/json: schema: type: object properties: delivered: { type: boolean } stream: { type: string } seq: { type: integer, format: int64 } '401': { description: Invalid or expired stream ticket. } '404': { description: Job not found. } '409': { description: Run is not live (no node to receive teleop). } '502': { description: Could not deliver to the node. } '503': { description: Teleop transport unavailable. } /v1/nodes/grants: post: tags: [nodes] operationId: createNodeGrant summary: Grant another org access to your nodes description: | Cross-org node sharing: let a `grantee_org` place runs on the owner org's nodes (the scheduler then treats the grantee like an owner for the matched nodes). `owner_org` defaults to the caller's org (a superuser must name it); a `node` selector is validated to actually be owned by the owner org. Needs the `nodes:grant` scope (owner/admin hold it via `*`). With no grants, scheduling is unchanged; an unowned (legacy) node is universal by default. security: [{ bearerAuth: [] }] requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/NodeGrantBody' } responses: '201': description: Grant created. content: application/json: schema: { $ref: '#/components/schemas/NodeGrantView' } '400': { description: Missing grantee_org (or owner_org for a superuser). } '404': { description: Node not found, or cross-tenant owner_org. } '422': { description: Invalid selector, or a node your org does not own. } '503': { description: Node sharing disabled. } get: tags: [nodes] operationId: listNodeGrants summary: List grants my org issued + received security: [{ bearerAuth: [] }] responses: '200': description: Grants, split into issued and received. content: application/json: schema: type: object properties: issued: type: array items: { $ref: '#/components/schemas/NodeGrantView' } received: type: array items: { $ref: '#/components/schemas/NodeGrantView' } /v1/nodes/grants/{id}: delete: tags: [nodes] operationId: revokeNodeGrant summary: Revoke a node-access grant description: | Only the owner org (owner/admin) or a superuser may revoke. A caller that does not own the grant gets `404` (no existence oracle). Revocation is effective on the next scheduler tick. Needs `nodes:grant`. security: [{ bearerAuth: [] }] parameters: - { name: id, in: path, required: true, schema: { type: string } } responses: '204': { description: Revoked. } '404': { description: Unknown grant, or not owned by the caller. } '503': { description: Node sharing disabled. } /v1/roles: get: tags: [auth] operationId: listRoles summary: RBAC role catalog description: The fixed role → scopes bundles. Public. security: - bearerAuth: [] - {} responses: '200': description: Roles. content: application/json: schema: type: array items: $ref: '#/components/schemas/Role' /v1/auth/tokens: post: tags: [auth] operationId: mintToken summary: Mint a login token or service API key description: | Admin-gated (bearer must equal `POLYGON_ADMIN_TOKEN`). The secret is returned exactly once. Supply either a `role` (expands to a scope bundle) or explicit `scopes`. A user with neither defaults to `*`. security: - adminBearer: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/MintBody' responses: '201': description: Token minted — secret shown once. content: application/json: schema: $ref: '#/components/schemas/MintResult' '400': description: Missing subject or unknown role. '401': description: Not an admin. '503': description: Auth disabled. get: tags: [auth] operationId: listTokens summary: List tokens (secret-free) security: - adminBearer: [] responses: '200': description: Token list. content: application/json: schema: type: array items: $ref: '#/components/schemas/TokenView' '401': description: Not an admin. /v1/auth/tokens/{id}: delete: tags: [auth] operationId: revokeToken summary: Revoke a token security: - adminBearer: [] parameters: - name: id in: path required: true schema: { type: string } responses: '200': description: Revoked. content: application/json: schema: type: object properties: revoked: { type: string } '401': description: Not an admin. '404': description: Token not found. # ── Identity / sessions (DB-native, POLYGON_STORE=postgres) ─────────────── /v1/auth/token: post: tags: [auth] operationId: exchangeLoginToken summary: Exchange a login token for a session description: | DB-native identity (wired ONLY when `POLYGON_STORE=postgres`; otherwise `404`). Exchanges an admin-issued login token (`slt_…`) for a server session and returns the session token (`slyt_ses_…`) ONCE — the bearer for every subsequent call. The session token is expiring, revocable, and stored only as a SHA-256 hash at rest. Public path (reachable without a session). A bad / expired / revoked token → `401`. security: - {} requestBody: required: true content: application/json: schema: type: object required: [token] properties: token: { type: string, description: The login token (`slt_…`). } responses: '200': description: Session minted — token shown once. content: application/json: schema: { $ref: '#/components/schemas/SessionResult' } '400': description: Missing token. '401': description: Invalid or expired login token. '404': description: Identity backend not active. /v1/auth/logout: post: tags: [auth] operationId: logout summary: Revoke the current session description: | Revokes the presented session. Idempotent — an absent / unknown token is a silent `204`, so logout never leaks token validity. DB-native (else `404`). responses: '204': description: Session revoked (or nothing to revoke). '404': description: Identity backend not active. /v1/auth/seat: post: tags: [auth] operationId: seatUser summary: Seat a user + issue their first login token (operator) description: | Bootstrap primitive: the operator admin token creates a user (with an auto-provisioned personal org) and issues their first login token. Superuser-only; DB-native (else `404`). Thereafter owners/admins seat users via `POST /v1/orgs/{id}/members`. security: - adminBearer: [] requestBody: required: true content: application/json: schema: type: object properties: name: { type: string } email: { type: string } responses: '201': description: User seated — login token shown once. content: application/json: schema: type: object properties: user: { $ref: '#/components/schemas/IdentityUser' } org_id: { type: string } login_token: { type: string, description: The user exchanges this at POST /v1/auth/token. Not shown again. } note: { type: string } '403': description: Requires the operator admin token. '404': description: Identity backend not active. # ── Orgs (team self-service, DB-native) ─────────────────────────────────── /v1/orgs: post: tags: [orgs] operationId: createOrg summary: Create a team org description: | Creates a team org with the caller as `owner`. Requires a real user session (the operator admin token has no user to own an org → `403`). DB-native (else `404`). requestBody: required: true content: application/json: schema: type: object properties: name: { type: string } slug: { type: string } responses: '201': description: Org created. content: application/json: schema: { $ref: '#/components/schemas/Org' } '403': description: A user session is required. '404': description: Identity backend not active. get: tags: [orgs] operationId: listOrgs summary: The caller's org seats description: The orgs the calling user is a member of, with their role. DB-native (else `404`). responses: '200': description: Org memberships. content: application/json: schema: type: array items: { $ref: '#/components/schemas/OrgMembership' } '404': description: Identity backend not active. /v1/orgs/{id}/members: post: tags: [orgs] operationId: addMember summary: Seat a member description: | Seats a user and grants them a role in the org. Owner/admin (or superuser) only; a member the caller isn't authorized over is `404` (cross-org) / `403` (insufficient role). Seating a NEW user returns a one-time `login_token`; an existing account is added with no credential (anti-impersonation). An admin may not grant owner/admin. DB-native (else `404`). parameters: - $ref: '#/components/parameters/OrgId' requestBody: required: true content: application/json: schema: type: object properties: name: { type: string } email: { type: string } role: { type: string, enum: [owner, admin, member, viewer], default: member } responses: '201': description: Member seated / updated. content: application/json: schema: type: object properties: user: { $ref: '#/components/schemas/IdentityUser' } role: { type: string } login_token: { type: string, description: Present only when a NEW user was created. } note: { type: string } '403': description: Requires the owner or admin role (or only an owner may grant owner/admin). '404': description: Org not found (or cross-org), or identity backend not active. get: tags: [orgs] operationId: listMembers summary: List an org's members description: Owner/admin (or superuser) only; cross-org `404`. DB-native (else `404`). parameters: - $ref: '#/components/parameters/OrgId' responses: '200': description: Members. content: application/json: schema: type: array items: { $ref: '#/components/schemas/OrgMember' } '403': description: Requires the owner or admin role. '404': description: Org not found (or cross-org), or identity backend not active. /v1/orgs/{id}/members/{uid}: delete: tags: [orgs] operationId: removeMember summary: Remove a member description: | Removes a user's membership. Owner/admin (or superuser) only; cross-org `404`; removing the last owner → `409`. DB-native (else `404`). parameters: - $ref: '#/components/parameters/OrgId' - name: uid in: path required: true description: The member's user id. schema: { type: string } responses: '204': description: Membership removed. '403': description: Requires the owner or admin role. '404': description: Org/member not found (or cross-org), or identity backend not active. '409': description: Cannot remove the last owner. # ── Embed (LMS read-only run views) ─────────────────────────────────────── /v1/jobs/{id}/embed: post: tags: [embed] operationId: embedJob summary: Mint an embed grant (service-only) description: | Mints a time-boxed, READ-ONLY, single-job, optionally branded embed grant an external service (an LMS) iframes to show a live view of ONE run without leaking a Симолёт credential to the browser. Service-only: the caller must be able to see the run AND hold a runs management scope (`runs:manage`/`runs:share`/`runs:submit`/`*`); a share/embed view or a `viewer` cannot mint. Returns `{token, url, expires_at}`, where `url` is `/embed/e/`. Requires the auth store (else `503`). security: - bearerAuth: [] parameters: - $ref: '#/components/parameters/JobId' requestBody: required: false content: application/json: schema: { $ref: '#/components/schemas/EmbedMintBody' } responses: '201': description: Grant minted. content: application/json: schema: { $ref: '#/components/schemas/EmbedMintResult' } '403': description: Not allowed to mint (missing scope, or a shared/embedded view). '404': description: Not found (or cross-org). '503': description: Embedding requires the auth store. /v1/embed/grant: get: tags: [embed] operationId: embedGrant summary: Resolve an embed token to its grant description: | Returns the embed token's resolved grant (job + allow-set + expiry + branding) for the stripped SPA view to render its chrome. Self-authenticating via the token (`X-Embed-Token` header or `?embed=` query param) — no principal needed. Fail-closed: `401` on any missing / invalid / expired token. security: - embedHeader: [] - embedQuery: [] parameters: - name: X-Embed-Token in: header required: false description: The embed token (alternative to `?embed=`). schema: { type: string } - name: embed in: query required: false description: The embed token (alternative to the header). schema: { type: string } responses: '200': description: The resolved grant. content: application/json: schema: { $ref: '#/components/schemas/EmbedGrantView' } '401': description: Invalid or expired embed token. '503': description: Embedding requires the auth store. /embed/e/{token}: get: tags: [embed] operationId: embedRedirect summary: Resolve an embed link to the branded view description: | Resolves a `/embed/e/{token}` link to the stripped, branded SPA embed view, `302`-redirecting to `/embed?e=&j=`. An invalid / expired / tampered token bounces to `/embed?embed_error=expired` — fail-closed, never leaks a job. Public. security: - {} parameters: - name: token in: path required: true description: The embed token minted by `POST /v1/jobs/{id}/embed`. schema: { type: string } responses: '302': description: Redirect to the branded embed view (or an error flag on an expired token). headers: Location: schema: { type: string } # ── Git connections (repo-first) ────────────────────────────────────────── /v1/git/connections: post: tags: [git] operationId: createGitConnection summary: Store a git connection description: | Stores an org's git connection (one repo + its credential, sealed with AES-256-GCM at rest). A submit then names `{source:{type:git, connection_id, ref}}` and orchd resolves the ref to an exact commit and clones on your behalf. Two auth kinds: an HTTPS PAT, or an SSH deploy key the platform generates (the public half is returned ONCE in `ssh_pub`). Requires the `git:connect` scope; fails closed (`503`) if the secret store isn't configured. Set `report_status: false` to opt this connection OUT of the commit-status mirror (default on) — the mirror otherwise posts a run's pending→success/failure outcome back to the source commit on GitHub / GitLab / Gitea / Forgejo. security: - bearerAuth: [] requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/GitConnectionBody' } responses: '201': description: Connection stored (secret-free view; `ssh_pub` + note for a generated key). content: application/json: schema: { $ref: '#/components/schemas/GitConnectionView' } '400': description: Bad provider / auth_kind / base_url / repo_url, or missing secret. '403': description: Missing the `git:connect` scope. '422': description: Policy violation (e.g. GitHub is PAT-only). '503': description: Git integration or the secret store is unavailable. get: tags: [git] operationId: listGitConnections summary: List git connections (secret-free) description: The caller org's connections (secret-free). The operator admin token sees all orgs, or one via `?org=`. security: - bearerAuth: [] parameters: - name: org in: query description: 'Filter to one org (superuser only; default: own org).' schema: { type: string } responses: '200': description: Connection list. content: application/json: schema: type: array items: { $ref: '#/components/schemas/GitConnectionView' } '403': description: Missing the `git:read` scope. # ── Admin ───────────────────────────────────────────────────────────────── /v1/admin/audit: get: tags: [admin] operationId: listAudit summary: Audit trail description: | The append-only record of security/state-changing actions (token mint/revoke, node join-token mint/revoke, run submit/cancel/share, webhook create/delete/test), newest first. Populated only when the deploy is configured with a database (`POLYGON_DB_URL`); otherwise returns an empty array. Manage-gated (`authorizeManage`): the operator admin token sees all orgs (or one via `?org=`); an org manager is confined to its own org's trail. security: - adminBearer: [] - bearerAuth: [] parameters: - name: org in: query description: 'Filter to one org (superuser only; default: all orgs).' schema: { type: string } - name: limit in: query schema: { type: integer, default: 100 } responses: '200': description: Audit rows. content: application/json: schema: type: array items: { $ref: '#/components/schemas/AuditRow' } '401': description: Not authenticated. '403': description: Not authorized to manage. /v1/admin/channel-target: post: tags: [admin] operationId: setChannelTarget summary: Set a channel's target agent version (operator) description: | Operator-only. Pins the desired agent version for a release channel; the gRPC layer then drives node self-update toward it. Requires the operator admin token. security: - adminBearer: [] requestBody: required: true content: application/json: schema: type: object required: [channel, version] properties: channel: { type: string, example: stable } version: { type: string, example: '1.4.2' } responses: '200': description: 'The updated channel→version mapping.' content: application/json: schema: type: object additionalProperties: { type: string } '400': description: Missing channel or version. '401': description: Not an admin. get: tags: [admin] operationId: getChannelTargets summary: Current channel → target version map (operator) security: - adminBearer: [] responses: '200': description: The channel → target-version map. content: application/json: schema: type: object additionalProperties: { type: string } '401': description: Not an admin. # ── Webhooks ────────────────────────────────────────────────────────────── /v1/webhooks: post: tags: [webhooks] operationId: createWebhook summary: Register a webhook endpoint description: | Registers an outgoing-webhook endpoint for the caller's org and returns the signing `secret` ONCE. Manage-gated (`authorizeManage`): the operator admin token must name a target `org_id`; an org manager may only create into its own org. The URL is validated through the SSRF egress guard, so an internal target is refused. See the top-level **Webhooks** description for the delivery signature and event types. security: - adminBearer: [] - bearerAuth: [] requestBody: required: true content: application/json: schema: type: object required: [url] properties: url: { type: string, format: uri, example: 'https://example.com/hooks/polygon' } org_id: { type: string, description: Required for the operator admin token; ignored/own-org for an org manager. } events: type: array description: 'Event types to receive (default: all). One of run.succeeded|run.failed|run.cancelled.' items: { type: string, enum: [run.succeeded, run.failed, run.cancelled] } responses: '201': description: Endpoint created — `secret` shown once. content: application/json: schema: { $ref: '#/components/schemas/WebhookCreated' } '400': description: Missing url / invalid url / superuser must set org_id. '401': description: Not authenticated. '403': description: Cannot create a webhook for another org. '503': description: Webhooks disabled. get: tags: [webhooks] operationId: listWebhooks summary: List webhook endpoints (secret-free) description: | Lists the caller org's endpoints as secret-free views. The operator admin token sees all orgs, or one via `?org=`. security: - adminBearer: [] - bearerAuth: [] parameters: - name: org in: query description: 'Filter to one org (superuser only; default: all orgs).' schema: { type: string } responses: '200': description: Endpoint list. content: application/json: schema: type: array items: { $ref: '#/components/schemas/WebhookView' } '401': description: Not authenticated. '403': description: Not authorized to manage. '503': description: Webhooks disabled. /v1/webhooks/{id}: delete: tags: [webhooks] operationId: deleteWebhook summary: Delete a webhook endpoint security: - adminBearer: [] - bearerAuth: [] parameters: - $ref: '#/components/parameters/WebhookId' responses: '200': description: Deleted. content: application/json: schema: type: object properties: deleted: { type: string } '404': description: Not found (or cross-org). '503': description: Webhooks disabled. /v1/webhooks/{id}/test: post: tags: [webhooks] operationId: testWebhook summary: Send a test delivery description: | Enqueues a synthetic ping delivery so the customer can confirm receipt and signature verification end-to-end. Returns the `delivery_id`. security: - adminBearer: [] - bearerAuth: [] parameters: - $ref: '#/components/parameters/WebhookId' responses: '202': description: Test delivery queued. content: application/json: schema: type: object properties: delivery_id: { type: string } status: { type: string, example: queued } '404': description: Not found (or cross-org). '503': description: Webhooks disabled. /v1/webhooks/{id}/deliveries: get: tags: [webhooks] operationId: listWebhookDeliveries summary: Recent delivery attempts description: The recent delivery attempts for an endpoint (status/attempts/response code). security: - adminBearer: [] - bearerAuth: [] parameters: - $ref: '#/components/parameters/WebhookId' responses: '200': description: Delivery records. content: application/json: schema: type: array items: { $ref: '#/components/schemas/WebhookDelivery' } '404': description: Not found (or cross-org). '503': description: Webhooks disabled. # ── Artifacts / uploads ─────────────────────────────────────────────────── /v1/uploads: post: tags: [artifacts] operationId: uploadProject summary: Upload a user project description: | Accepts a single file or a `.zip`/`.tar.gz` of a project as multipart form field `file`. Returns an agent-reachable `url` to set as the job's `workspace.ref` (type `http`); the agent unpacks archives into `/work`. Auth-exempt (grant-style trust). security: - {} requestBody: required: true content: multipart/form-data: schema: type: object required: [file] properties: file: type: string format: binary responses: '200': description: Stored. content: application/json: schema: $ref: '#/components/schemas/UploadResult' '400': description: Missing form field 'file'. '413': description: Body exceeds POLYGON_MAX_UPLOAD_MB. /v1/artifacts/{key}: put: tags: [artifacts] operationId: putArtifact summary: Upload an artifact by key description: | Agents PUT run outputs here (the grant target). The body is stored verbatim and hashed; the response carries `X-Sha256` and `X-Size` headers. Auth-exempt (grant-style trust). security: - {} parameters: - $ref: '#/components/parameters/ArtifactKey' requestBody: required: true content: application/octet-stream: schema: type: string format: binary responses: '201': description: Stored. headers: X-Sha256: description: Hex SHA-256 of the stored bytes. schema: { type: string } X-Size: description: Stored byte count. schema: { type: string } '400': description: Bad key. '413': description: Body exceeds the upload cap. get: tags: [artifacts] operationId: getArtifact summary: Download an artifact description: | Always served as `application/octet-stream` with a download disposition and `X-Content-Type-Options: nosniff`, so an uploaded HTML/SVG artifact can't execute as stored XSS. Auth-exempt. security: - {} parameters: - $ref: '#/components/parameters/ArtifactKey' responses: '200': description: The artifact bytes. content: application/octet-stream: schema: type: string format: binary '400': description: Bad key. '404': description: Not found. /v1/artifacts-zip/{prefix}: get: tags: [artifacts] operationId: getArtifactsZip summary: Download a whole run's outputs as one .zip description: | Streams every file under a prefix (a job id — i.e. all of that run's outputs) as a single `.zip`. Same org-ownership gate as the single-artifact download; refuses anything that isn't a real directory. Auth-exempt (grant-style trust), guarded by the org-ownership `Access` check. security: - {} parameters: - name: prefix in: path required: true description: Slash-delimited prefix (matched greedily), typically a job id. schema: { type: string } responses: '200': description: A zip archive of every file under the prefix. content: application/zip: schema: type: string format: binary '400': description: Bad key. '404': description: Not a directory / not found (or cross-org). # ── Health ──────────────────────────────────────────────────────────────── /healthz: get: tags: [health] operationId: healthz summary: Liveness probe description: Returns `ok`. Public — reachable even in `required` mode. security: - {} responses: '200': description: Alive. content: text/plain: schema: { type: string, example: ok } /v1/healthz: get: tags: [health] operationId: healthzV1 summary: Liveness probe (versioned alias) description: Versioned alias of `GET /healthz`. Public. security: - {} responses: '200': description: Alive. content: text/plain: schema: { type: string, example: ok } # ── MCP (Model Context Protocol) ────────────────────────────────────────── /mcp: post: tags: [mcp] operationId: mcp summary: Native MCP server (JSON-RPC 2.0 facade) description: | Model Context Protocol endpoint (Streamable-HTTP transport, request/response form). Lets an LLM — Claude via the Anthropic MCP connector — drive the platform as a set of tools: launch runs, poll status, read logs, fetch artifacts. Also available at the versioned alias `POST /v1/mcp`. It is a thin JSON-RPC 2.0 facade over this same REST surface: each `tools/call` is dispatched internally through the router carrying the caller's Principal, so RBAC scopes, tenancy and admission all apply exactly as for the underlying route. Authenticate with the same `Authorization: Bearer ` as the rest of the API. JSON-RPC methods: `initialize`, `notifications/initialized` (202, no body), `tools/list`, `tools/call`, `ping`. Tools: `list_stacks`, `submit_job`, `preflight_job`, `get_job`, `list_jobs`, `get_logs`, `list_artifacts`, `cancel_job`, `rerun_job`, `get_nodes`, `entitlements`. requestBody: required: true content: application/json: schema: type: object description: A JSON-RPC 2.0 request object. required: [jsonrpc, method] properties: jsonrpc: { type: string, enum: ['2.0'] } id: { description: 'Request id (omit for a notification).' } method: { type: string, example: tools/call } params: { type: object } responses: '200': description: A single JSON-RPC 2.0 response object. content: application/json: schema: type: object properties: jsonrpc: { type: string, enum: ['2.0'] } id: {} result: { type: object } error: type: object properties: code: { type: integer } message: { type: string } '202': description: A JSON-RPC notification was accepted (no response body). components: securitySchemes: bearerAuth: type: http scheme: bearer description: "Platform token — `Authorization: Bearer `." adminBearer: type: http scheme: bearer description: Operator admin token (`POLYGON_ADMIN_TOKEN`). tokenQuery: type: apiKey in: query name: token description: Token as a query param, accepted only on the events SSE route. ticketQuery: type: apiKey in: query name: ticket description: Short-lived stream ticket from `.../subscribe`, accepted only on the stream-data route. embedHeader: type: apiKey in: header name: X-Embed-Token description: A read-only embed token, self-authenticating on the embed-grant route. embedQuery: type: apiKey in: query name: embed description: A read-only embed token as a query param (browser links can't set a header). parameters: JobId: name: id in: path required: true description: Job id (e.g. `job_ab12cd`). schema: { type: string } StreamName: name: name in: path required: true description: Stream name as listed by `GET /v1/jobs/{id}/streams`. schema: { type: string } WebhookId: name: id in: path required: true description: Webhook endpoint id. schema: { type: string } ArtifactKey: name: key in: path required: true description: Slash-delimited artifact key (matched greedily). schema: { type: string } PipelineId: name: id in: path required: true description: Pipeline-run id (e.g. `plr_ab12cd`). schema: { type: string } OrgId: name: id in: path required: true description: Org id (e.g. `org_ab12cd`). schema: { type: string } schemas: # ── Submit ─────────────────────────────────────────────────────────────── SubmitJobBody: type: object required: [stack] description: The full JobSpec surface. Only `stack` is mandatory. properties: stack: type: string description: Base environment name (must exist in the catalog). command: type: array items: { type: string } description: Run command as argv (falls back to the stack default). env: type: object additionalProperties: { type: string } description: | Environment variables. Special keys `POLYGON_SETUP` (install deps) and `POLYGON_DISPLAY` (headless display) are recognized. config: type: object additionalProperties: true description: Values for the manifest's declared `config` fields (validated). workspace: $ref: '#/components/schemas/Mount' inputs: type: array items: { $ref: '#/components/schemas/Mount' } outputs: type: array items: { $ref: '#/components/schemas/OutputSpec' } requires: $ref: '#/components/schemas/Requires' weights: type: object additionalProperties: { type: number } description: Soft-scoring weights for placement. weights_preset: type: string description: Named weight bundle. enum: [training, interactive, batch] limits: $ref: '#/components/schemas/Limits' isolation: type: string enum: [none, gvisor, kata] priority: type: integer description: Default 100. seed: type: integer format: int64 mode: type: string description: Default "run". enum: [run, session] Mount: type: object description: A workspace/input binding. `ref` is the content ref (`inline://…`, git url, http url). properties: path: { type: string, description: Mount point inside the container. } ref: { type: string } type: type: string enum: [git, archive, file, dir, inline] mode: type: string enum: [ro, rw] OutputSpec: type: object properties: artifact: { type: string, description: Artifact type/label. } glob: { type: string } optional: { type: boolean } Limits: type: object properties: cpu_cores: { type: integer } ram_gb: { type: integer } gpu_count: { type: integer } pids: { type: integer } timeout_sec: { type: integer } no_internet: { type: boolean } egress_allow: type: array items: { type: string } Requires: type: object properties: gpu: type: object properties: required: { type: boolean } min_vram_gb: { type: integer } vendor: { type: string } cuda_min: { type: string } cpu: type: object properties: min_cores: { type: integer } ram: type: object properties: min_gb: { type: integer } disk: type: object properties: min_gb: { type: integer } arch: type: array items: { type: string } trust: type: object properties: min_tier: type: string enum: [community, partner, trusted] capabilities: type: array items: { type: string } # ── Job view ─────────────────────────────────────────────────────────── Job: type: object description: The canonical job view returned by submit/get/list/cancel/priority. required: [job_id, stack, phase] properties: job_id: { type: string } stack: { type: string } image: { type: string, description: Resolved image@digest. } phase: type: string enum: [queued, assigned, pulling, running, succeeded, failed, timeout, cancelled] node_id: { type: string } priority: { type: integer } mode: { type: string } seed: { type: integer, format: int64 } spec_hash: type: string example: 'sha256:0a1b2c3d4e5f' description: | Content-addressed reproducibility fingerprint over the fully-resolved spec (stack, image, command, env, config, workspace, inputs, limits, seed, requires). Two runs with the same `spec_hash` reproduce identically; a re-run inherits it. Filter GET /v1/jobs by `?spec=` to get the cohort. project: { type: string } org_id: { type: string } attempts: { type: integer, description: Infra requeues (node dropped mid-run). } created_at: { type: string, format: date-time } started_at: { type: string, format: date-time } finished_at: { type: string, format: date-time } duration_ms: { type: integer, format: int64 } error: { type: string } spec: { $ref: '#/components/schemas/JobViewSpec' } final_metrics: type: object additionalProperties: { type: number } artifacts: type: array items: { $ref: '#/components/schemas/ArtifactRef' } JobViewSpec: type: object description: '"What was given" — the run spec echoed back (secrets redacted).' properties: command: type: array items: { type: string } setup: { type: string, description: From env POLYGON_SETUP. } display: { type: boolean, description: From env POLYGON_DISPLAY. } env: type: object additionalProperties: { type: string } description: User env with secret-looking values redacted. config: type: object additionalProperties: true workspace: type: object properties: type: { type: string } path: { type: string } code: { type: string, description: Present for inline:// workspaces. } ref: { type: string, description: Present for git/http workspaces. } inputs: type: array items: type: object properties: path: { type: string } ref: { type: string } type: { type: string } outputs: type: array items: type: object properties: artifact: { type: string } glob: { type: string } resources: type: object properties: cpu_cores: { type: integer } ram_gb: { type: integer } gpu_count: { type: integer } timeout_sec: { type: integer } gpu_required: { type: boolean } min_tier: { type: string } ArtifactRef: type: object properties: artifact: { type: string } ref: { type: string } sha256: { type: string } size: { type: integer, format: int64 } CancelResult: allOf: - $ref: '#/components/schemas/Job' - type: object properties: cancelled: { type: boolean } killed_on_node: { type: boolean } # ── Logs ────────────────────────────────────────────────────────────── JobLogs: type: object properties: logs: type: array items: type: object properties: stream: { type: string, description: stdout | stderr | system. } line: { type: string } metrics: type: array items: type: object properties: step: { type: integer, format: int64 } values: type: object additionalProperties: { type: number } phase: { type: string } SSEEvent: type: object description: The JSON payload in an SSE `data:` frame. properties: kind: type: string enum: [log, metric, phase] job_id: { type: string } seq: { type: integer, format: int64 } stream: { type: string } line: { type: string } phase: { type: string } step: { type: integer, format: int64 } values: type: object additionalProperties: { type: number } # ── Nodes ────────────────────────────────────────────────────────────── Node: type: object properties: id: { type: string } name: { type: string } disabled: { type: boolean } tier: type: string enum: [community, partner, trusted] status: { type: string } agent_version: { type: string } channel: { type: string } cpu_cores: { type: integer } ram_gb: { type: integer } gpus: { type: integer } max_vram_gb: { type: integer } gpu_models: type: array items: type: object properties: model: { type: string } vendor: { type: string } vram_gb: { type: integer } cuda: { type: string } arch: { type: string } os: { type: string } max_concurrency: { type: integer } used_cpu: { type: integer } used_ram: { type: integer } used_gpu: { type: integer } running_jobs: type: array items: type: object properties: job_id: { type: string } stack: { type: string } phase: { type: string } dlperf: { type: number } reputation: { type: number } reliability: { type: number } active_jobs: { type: integer } last_heartbeat: { type: string, format: date-time } NodePatch: type: object description: Partial — only provided fields change. properties: name: { type: string } disabled: { type: boolean } cpu_cap: { type: integer } ram_cap: { type: integer } max_concurrency: { type: integer } NodePatchResult: type: object properties: id: { type: string } name: { type: string } disabled: { type: boolean } cpu_cores: { type: integer } ram_gb: { type: integer } max_concurrency: { type: integer } # ── Stacks ───────────────────────────────────────────────────────────── Stack: type: object properties: name: { type: string } version: { type: string } image: { type: string } modes: type: array items: { type: string } display_name: { type: string } description: { type: string } categories: type: array items: { type: string } requires: { $ref: '#/components/schemas/StackRequires' } config: type: array items: { $ref: '#/components/schemas/StackField' } metrics: type: array items: type: object properties: key: { type: string } higher_is_better: { type: boolean } default_command: type: array items: { type: string } default_outputs: type: array items: { $ref: '#/components/schemas/OutputSpec' } docs: { type: string } examples: type: array items: { $ref: '#/components/schemas/StackExample' } launch: { $ref: '#/components/schemas/StackLaunch' } unavailable: { type: boolean } StackLaunch: type: object properties: cover: { type: string } featured: { type: boolean } badge: { type: string } tasks: type: array items: { $ref: '#/components/schemas/LaunchTask' } LaunchTask: type: object required: [id, title] properties: id: { type: string } title: { type: string } description: { type: string } cover: { type: string } command: type: array items: { type: string } config: type: object additionalProperties: true settings: type: array items: { type: string } presets: type: array items: { $ref: '#/components/schemas/LaunchPreset' } LaunchPreset: type: object required: [id, title] properties: id: { type: string } title: { type: string } description: { type: string } cover: { type: string } config: type: object additionalProperties: true StackRequires: type: object properties: gpu: type: object properties: required: { type: boolean } min_vram_gb: { type: integer } cpu: type: object properties: min_cores: { type: integer } ram: type: object properties: min_gb: { type: integer } StackField: type: object properties: key: { type: string } type: { type: string, description: "int | number | bool | enum | select | string" } values: type: array items: {} default: {} min: { type: integer } max: { type: integer } description: { type: string } StackExample: type: object properties: name: { type: string } description: { type: string } setup: { type: string } run: { type: string } code: { type: string } gpu: { type: boolean } display: { type: boolean } # ── Auth ─────────────────────────────────────────────────────────────── Principal: type: object properties: kind: type: string enum: [user, service, node, anon] subject: { type: string } org_id: { type: string } name: { type: string } scopes: type: array items: { type: string } Role: type: object properties: name: { type: string } scopes: type: array items: { type: string } MintBody: type: object required: [subject] properties: kind: type: string enum: [user, service] subject: { type: string } org_id: { type: string } name: { type: string } role: type: string enum: [owner, admin, member, viewer] scopes: type: array items: { type: string } MintResult: type: object properties: id: { type: string } token: { type: string, description: The secret — shown only once. } kind: { type: string } subject: { type: string } org_id: { type: string } role: { type: string } scopes: type: array items: { type: string } note: { type: string } TokenView: type: object properties: id: { type: string } kind: { type: string } subject: { type: string } org_id: { type: string } name: { type: string } scopes: type: array items: { type: string } created_at: { type: string, format: date-time } revoked: { type: boolean } AuditRow: type: object description: One append-only audit-trail entry. properties: id: { type: string } org_id: { type: string } actor_type: { type: string, description: 'user | service | node | system' } actor_id: { type: string } action: { type: string, example: 'runs:submit' } resource_type: { type: string } resource_id: { type: string } after: { description: Optional JSON snapshot of the affected resource. } ip: { type: string } created_at: { type: string, format: date-time } UploadResult: type: object properties: url: { type: string, description: Agent-reachable artifact URL. } path: { type: string } name: { type: string } # ── Pagination envelopes ───────────────────────────────────────────────── JobPage: type: object description: Cursor-envelope page of jobs (returned when `?paginate=1`/`?cursor=`). properties: items: type: array items: { $ref: '#/components/schemas/Job' } next_cursor: type: string description: Opaque cursor for the next page; empty at the end of the list. has_more: { type: boolean } total: type: integer description: Size of the caller's access+query-filtered scope (before seek/limit). NodePage: type: object description: Cursor-envelope page of nodes (returned when `?paginate=1`/`?cursor=`). properties: items: type: array items: { $ref: '#/components/schemas/Node' } next_cursor: { type: string } has_more: { type: boolean } # ── Share ──────────────────────────────────────────────────────────────── ShareResult: type: object properties: token: { type: string, description: The share code (the credential). } code: { type: string, description: Same value as `token`. } url: { type: string, description: 'Short link, e.g. https://…/s/.' } # ── Streams ────────────────────────────────────────────────────────────── StreamView: type: object properties: name: { type: string } kind: { type: string, description: 'e.g. video | data | telemetry.' } enabled: { type: boolean } transport: { type: string, description: 'Delivery transport (e.g. sse, webrtc).' } fps: { type: integer } width: { type: integer } height: { type: integer } subscribable: type: boolean description: True only for an enabled data/telemetry stream while the hub is running. StreamTicket: type: object description: The subscribe response — a short-lived ticket + delivery URL. properties: session_id: { type: string } transport: { type: string } ticket: { type: string, description: 'Opaque ~60s ticket bound to (job, stream, session).' } url: { type: string, description: 'Delivery URL with the ticket embedded as ?ticket=.' } StreamChunk: type: object description: One chunk in a data/telemetry stream (the JSON in an SSE `stream` frame). properties: seq: { type: integer, format: int64 } stream: { type: string } kind: { type: string } encoding: { type: string } ts: { type: integer, format: int64, description: Unix ms. } data: { type: string, description: Chunk payload (opaque; may be base64 per `encoding`). } JobRecording: type: object description: The full post-hoc replay timeline for a run. properties: job_id: { type: string } phase: { type: string } started_at: { type: integer, format: int64, description: Unix ms. } ended_at: { type: integer, format: int64, description: Unix ms. } metrics: type: array items: type: object properties: ts: { type: integer, format: int64 } step: { type: integer, format: int64 } values: type: object additionalProperties: { type: number } logs: type: array items: type: object properties: ts: { type: integer, format: int64 } stream: { type: string } line: { type: string } streams: type: array description: 'Per-stream summary (present only when the run recorded streams).' items: type: object properties: name: { type: string } kind: { type: string } count: { type: integer } stream_points: type: array items: { $ref: '#/components/schemas/StreamChunk' } truncated: type: object properties: stream_points: { type: boolean } # ── Node metrics / update / join-tokens ────────────────────────────────── NodeMetrics: type: object properties: node_id: { type: string } window: { type: string, description: 'Effective window (e.g. "1h0m0s").' } max_cpu: { type: integer, description: RAW reported CPU cores (pre-override ceiling). } max_ram: { type: integer, description: RAW reported RAM GB. } max_gpu: { type: integer, description: RAW reported GPU count. } max_slots: { type: integer, description: RAW reported concurrency slots. } samples: type: array items: { $ref: '#/components/schemas/NodeSample' } NodeSample: type: object description: One committed-load point in a node's timeseries. properties: ts: { type: integer, format: int64, description: Unix ms. } cpu: { type: integer, description: Committed CPU cores in use. } ram: { type: integer, description: Committed RAM GB in use. } gpu: { type: integer, description: Committed GPUs in use. } jobs: { type: integer, description: Active jobs on the node. } load: { type: number, description: 0..1 peak utilization across cpu / ram / slots. } NodeUpdateResult: type: object properties: node_id: { type: string } current_version: { type: string } target_version: { type: string } up_to_date: { type: boolean } auto_update: { type: boolean, description: The host-side systemd auto-update timer is active. } update_sent: { type: boolean, description: Whether the UpdateTo frame was delivered now. } channel: { type: string } note: { type: string } JoinTokenMint: type: object description: A freshly minted node join token — the secret is shown once. properties: id: { type: string } token: { type: string, description: The join secret — not shown again. } tier: { type: string, enum: [community, partner, trusted] } note: { type: string } JoinTokenView: type: object description: Secret-free join-token listing record. properties: id: { type: string } tier: { type: string, enum: [community, partner, trusted] } org_id: { type: string } label: { type: string } created_at: { type: string, format: date-time } revoked: { type: boolean } # ── Analytics ──────────────────────────────────────────────────────────── JobAnalytics: type: object properties: window: { type: string, description: 'Effective window (e.g. "24h0m0s").' } buckets: type: array items: { $ref: '#/components/schemas/JobBucket' } summary: type: object properties: submitted: { type: integer } succeeded: { type: integer } failed: { type: integer, description: failed + timeout. } cancelled: { type: integer } running: { type: integer, description: Live in-flight (assigned/pulling/running). } queued: { type: integer, description: Live queued. } JobBucket: type: object properties: ts: { type: integer, format: int64, description: 'Bucket start, unix ms.' } submitted: { type: integer, description: Jobs created in this bucket. } succeeded: { type: integer } failed: { type: integer, description: failed + timeout. } cancelled: { type: integer } # ── Entitlements ───────────────────────────────────────────────────────── Entitlements: type: object properties: principal: { $ref: '#/components/schemas/Principal' } org: { type: string } usage: type: object properties: running: { type: integer } queued: { type: integer } user_inflight: { type: integer } limits: type: object properties: max_concurrent_org: { type: integer } max_concurrent_user: { type: integer, description: '0 = no per-user cap.' } remaining: type: object description: 'Remaining headroom; `user_concurrent` is present only when a per-user cap is set.' properties: org_concurrent: { type: integer } user_concurrent: { type: integer } # ── Policy preflight (dry-run admission) ───────────────────────────────── PolicyPreflight: type: object description: What a submit WOULD do — the admission decision without enqueuing. properties: org: { type: string } stack: { type: string } allowed: { type: boolean, description: 'false when a quota/limit would reject the submit.' } clamped: type: object description: The post-clamp resource values a submit would actually use. properties: cpu_cores: { type: number } ram_gb: { type: number } gpu_count: { type: integer } timeout_sec: { type: integer } clamp_notes: type: array description: 'One note per ceiling-reduced dimension, e.g. "cpu 200→128 (POLYGON_MAX_CPU)". Never null.' items: { type: string } reason: { type: string, description: 'Human reason when allowed=false.' } http_status: { type: integer, description: '0 when allowed; 429 when over a concurrency cap.' } stack_error: { type: string, description: 'What a submit would reject (unknown/unavailable stack), if any.' } concurrency: type: object properties: org_inflight: { type: integer } org_limit: { type: integer } user_inflight: { type: integer } user_limit: { type: integer, description: '0 = no per-user cap.' } # ── Webhooks ───────────────────────────────────────────────────────────── WebhookCreated: type: object description: The create response — carries the signing secret exactly ONCE. properties: id: { type: string } org_id: { type: string } url: { type: string, format: uri } events: type: array items: { type: string, enum: [run.succeeded, run.failed, run.cancelled] } enabled: { type: boolean } created_at: { type: string, format: date-time } secret: { type: string, description: 'HMAC signing secret — store it now, it is not shown again.' } note: { type: string } WebhookView: type: object description: Secret-free endpoint view. properties: id: { type: string } org_id: { type: string } url: { type: string, format: uri } events: type: array items: { type: string, enum: [run.succeeded, run.failed, run.cancelled] } enabled: { type: boolean } created_at: { type: string, format: date-time } WebhookDelivery: type: object description: One delivery attempt record. properties: id: { type: string } endpoint_id: { type: string } event: { type: string, enum: [run.succeeded, run.failed, run.cancelled] } status: { type: string, enum: [pending, delivered, failed] } attempts: { type: integer } response_code: { type: integer, description: HTTP status of the last attempt (0 if none). } error: { type: string } created_at: { type: string, format: date-time } updated_at: { type: string, format: date-time } WebhookPayload: type: object description: | The JSON body POSTed to a registered endpoint when a run finishes. Signed via the `X-Simolyot-Signature` header (see the top-level Webhooks section). properties: event: { type: string, enum: [run.succeeded, run.failed, run.cancelled] } delivery_id: { type: string } created_at: { type: string, format: date-time } run: type: object properties: id: { type: string } org_id: { type: string } project: { type: string } stack: { type: string } phase: { type: string } created_at: { type: string, format: date-time } started_at: { type: string, format: date-time } finished_at: { type: string, format: date-time } error: { type: string } metrics: type: object additionalProperties: { type: number } artifacts: type: array items: { $ref: '#/components/schemas/ArtifactRef' } # ── Runs (RunSpec) ──────────────────────────────────────────────────────── RunSpecBody: description: | A run request: the full SubmitJobBody surface PLUS the four config `layers`. A strict superset of SubmitJobBody — a body with no `layers` behaves identically to a plain submit (the flat `config` is treated as the `user` layer). allOf: - $ref: '#/components/schemas/SubmitJobBody' - type: object properties: layers: $ref: '#/components/schemas/RunLayers' RunLayers: type: object description: | Config overlays keyed by layer, folded low→high (`platform` < `org` < `project` < `user`). Any subset may be present; unknown layer names are ignored. properties: platform: { type: object, additionalProperties: true, description: Deployment-wide defaults (lowest precedence). } org: { type: object, additionalProperties: true, description: Per-tenant overlay. } project: { type: object, additionalProperties: true, description: Per-project overlay. } user: { type: object, additionalProperties: true, description: The caller's own values (highest precedence). } ResolvedRun: type: object description: The immutable snapshot Resolve produces — deterministic and reproducible. properties: stack: { type: string } command: type: array items: { type: string } resources: { $ref: '#/components/schemas/RunResources' } config: type: object additionalProperties: true description: The effective (merged) config after folding all layers. provenance: type: object additionalProperties: { type: string, enum: [platform, org, project, user] } description: The winning layer per config key. layers: type: array items: { $ref: '#/components/schemas/LayerDiff' } RunResources: type: object description: Resolved runtime asks (0 = unspecified / node's share). properties: cpu_cores: { type: integer } ram_gb: { type: integer } gpu_count: { type: integer } timeout_sec: { type: integer } LayerDiff: type: object description: One layer's contribution to the resolved config. properties: layer: { type: string, enum: [platform, org, project, user] } set: type: object additionalProperties: true description: Keys this layer OWNS in the effective config (it set them; no higher layer overrode). overridden: type: array description: Keys this layer set that a higher layer later shadowed. items: { type: string } RunProblem: type: object description: One structured config/DAG validation problem. properties: key: { type: string } code: { type: string, description: 'e.g. unknown_key, bad_type, out_of_range, unknown_stack, cycle.' } message: { type: string } RunRow: type: object description: One run-history row — the job summary plus run metadata. properties: job_id: { type: string } run_id: { type: string, description: Equals `job_id` for a single-step run. } has_runspec: { type: boolean, description: Whether the run carries a resolved RunSpec snapshot. } stack: { type: string } phase: { type: string } project: { type: string } created_at: { type: string, format: date-time } RunStage: type: object description: One stage of a run's StageGraph (a single-step run is exactly one stage). properties: id: { type: string } name: { type: string } stack: { type: string } phase: { type: string } job_id: { type: string } # ── Pipelines (DAG) ─────────────────────────────────────────────────────── PipelineBody: type: object description: A DAG-pipeline submit/validate request. required: [stages] properties: name: { type: string } project: { type: string } stages: type: array items: { $ref: '#/components/schemas/PipelineStageBody' } PipelineStageBody: description: | One stage: DAG metadata (`name`, `needs`, `allow_failure`) plus the full RunSpec surface (a superset of a /v1/runs body — stack/command/config/layers/limits). allOf: - type: object properties: name: { type: string } needs: type: array description: Names of stages this one depends on (starts once all have succeeded). items: { type: string } allow_failure: type: boolean description: When true, this stage failing does NOT skip its dependents. - $ref: '#/components/schemas/RunSpecBody' PipelineRun: type: object description: A pipeline run — the list summary omits the `stages` array; the detail view includes it. properties: pipeline_run_id: { type: string, example: 'plr_ab12cd' } name: { type: string } project: { type: string } org_id: { type: string } status: { type: string, enum: [pending, running, succeeded, failed, cancelled] } created_at: { type: string, format: date-time } started_at: { type: string, format: date-time } finished_at: { type: string, format: date-time } stage_count: { type: integer } progress: type: object properties: pending: { type: integer } queued: { type: integer } running: { type: integer } succeeded: { type: integer } failed: { type: integer } skipped: { type: integer } cancelled: { type: integer } stages: type: array description: Present in the detail view (GET /v1/pipelines/{id}). items: { $ref: '#/components/schemas/PipelineStageView' } PipelineStageView: type: object properties: name: { type: string } needs: { type: array, items: { type: string } } allow_failure: { type: boolean } phase: { type: string, enum: [pending, queued, running, succeeded, failed, skipped, cancelled] } stack: { type: string } job_id: { type: string, description: Present once the stage's Job is launched. } error: { type: string } final_metrics: type: object additionalProperties: { type: number } # ── Identity / sessions / orgs ──────────────────────────────────────────── SessionResult: type: object description: The token→session exchange response (session token shown ONCE). properties: session_token: { type: string, description: 'The API bearer (`slyt_ses_…`); not shown again.' } expires_at: { type: string, format: date-time } user_id: { type: string } org_id: { type: string } role: { type: string, enum: [owner, admin, member, viewer] } scopes: { type: array, items: { type: string } } note: { type: string } IdentityUser: type: object properties: id: { type: string, example: 'usr_ab12cd' } email: { type: string } name: { type: string } status: { type: string } created_at: { type: string, format: date-time } Org: type: object properties: id: { type: string, example: 'org_ab12cd' } kind: { type: string, enum: [personal, team] } name: { type: string } slug: { type: string } created_at: { type: string, format: date-time } OrgMembership: type: object description: One of a user's org seats. properties: org: { $ref: '#/components/schemas/Org' } role: { type: string, enum: [owner, admin, member, viewer] } OrgMember: type: object description: A row of an org's member list. properties: user_id: { type: string } name: { type: string } email: { type: string } role: { type: string, enum: [owner, admin, member, viewer] } joined_at: { type: string, format: date-time } # ── Embed ───────────────────────────────────────────────────────────────── EmbedMintBody: type: object description: The mint request (all fields optional — a bare POST mints a full-feature, unbranded grant). properties: ttl_hours: { type: integer, description: 'Grant lifetime in hours (default 24; capped at 720 = 30 days).' } allow: type: array description: 'Read categories to permit; empty == all.' items: { type: string, enum: [logs, metrics, streams, artifacts] } branding: type: object properties: title: { type: string } logo_url: { type: string } accent: { type: string } EmbedMintResult: type: object properties: token: { type: string, description: The embed token (the only credential the browser holds). } url: { type: string, description: '`/embed/e/` — iframe this.' } expires_at: { type: string, format: date-time } EmbedGrantView: type: object description: The browser-facing resolved grant (carries no secret). properties: job: { type: string } allow: type: array description: 'Allowed read categories (an empty list reads as "all" on the client).' items: { type: string, enum: [logs, metrics, streams, artifacts] } exp: { type: integer, format: int64, description: Expiry, unix seconds. } branding: type: object properties: title: { type: string } logo_url: { type: string } accent: { type: string } # ── Git connections ─────────────────────────────────────────────────────── GitConnectionBody: type: object required: [provider, base_url, repo_url] properties: provider: { type: string, enum: [forgejo, gitea, gitlab, github] } base_url: { type: string, description: Provider origin (e.g. https://github.com). } repo_url: { type: string, description: Repository URL (e.g. https://github.com/acme/sim-lab.git). } auth_kind: { type: string, enum: [https_pat, ssh_deploy_key], default: https_pat } secret: { type: string, description: 'PAT (https_pat) or private key (ssh_deploy_key); consumed, never echoed. Omit for a keyless ssh_deploy_key to have one generated.' } label: { type: string } default_ref: { type: string } report_status: type: boolean description: | Opt out of the commit-status mirror. Omitted/true = mirror a pending→success/failure back to the source commit; false = suppress it. GitConnectionView: type: object description: The secret-free connection view (the credential is never returned). properties: id: { type: string, example: 'gc_ab12cd' } org_id: { type: string } provider: { type: string, enum: [forgejo, gitea, gitlab, github] } base_url: { type: string } repo_url: { type: string } auth_kind: { type: string, enum: [https_pat, ssh_deploy_key] } label: { type: string } default_ref: { type: string } status: { type: string, enum: [active, degraded] } ssh_pub: { type: string, description: 'The generated deploy key''s public half — shown ONCE at create time for a keyless ssh_deploy_key.' } report_status: { type: boolean, description: Whether the commit-status mirror is enabled for this connection. } created_at: { type: string, format: date-time } note: { type: string, description: One-time create note (e.g. "add this deploy key…"). } # ── Policy & quotas ────────────────────────────────────────────────────── PolicyCaps: type: object description: | One policy layer's caps — a SPARSE object; only the axes it sets appear, the rest fall through to a lower-precedence layer. Concurrency 0 = no cap. properties: max_cpu: { type: integer, description: CPU cores per run. } max_ram: { type: integer, description: RAM GB per run. } max_gpu: { type: integer, description: GPUs per run. } max_timeout_sec: { type: integer, description: Wall-clock cap per run. } max_concurrent_org: { type: integer, description: In-flight runs per org. } max_concurrent_user: { type: integer, description: In-flight runs per user (0 = no per-user cap). } PolicyDoc: type: object description: An org's stored policy — the org-level caps plus per-project and per-user overrides. properties: org_id: { type: string } org: { $ref: '#/components/schemas/PolicyCaps' } projects: type: object additionalProperties: { $ref: '#/components/schemas/PolicyCaps' } description: Per-project cap overrides, keyed by project name. users: type: object additionalProperties: { $ref: '#/components/schemas/PolicyCaps' } description: Per-user cap overrides, keyed by user id. updated_at: { type: string, format: date-time } EffectivePolicy: type: object description: The folded caps — for each axis, the value from the highest-precedence layer that set it, with provenance. properties: max_cpu: { type: integer } max_ram: { type: integer } max_gpu: { type: integer } max_timeout_sec: { type: integer } max_concurrent_org: { type: integer } max_concurrent_user: { type: integer } provenance: type: object additionalProperties: { type: string, enum: [platform, org, project, user] } description: Which layer produced each set axis. PolicyView: type: object description: The resolved policy view for an org (GET /v1/policy). properties: org_id: { type: string } platform: { $ref: '#/components/schemas/PolicyCaps' } policy: { $ref: '#/components/schemas/PolicyDoc' } effective: { $ref: '#/components/schemas/EffectivePolicy' } effective_caps: type: object description: The effective caps after any explicit POLYGON_MAX_* env override (the admission ceilings). PolicyPreview: type: object description: Effective policy previewed for a named subject (GET/POST /v1/policy/preview). properties: org_id: { type: string } subject: { type: string } project: { type: string } effective: { $ref: '#/components/schemas/EffectivePolicy' } effective_caps: { type: object } stack: { type: string, description: Present only when a stack was named. } effective_manifest: type: object description: The stack's config surface intersected with the effective caps (present only with a stack). stack_error: { type: string, description: Set when the named stack is unknown. } # ── Sessions & pods ────────────────────────────────────────────────────── PodHealth: type: object description: A service's health gate — a service its dependents `need` is not "up" until this passes. properties: command: type: array items: { type: string } period_sec: { type: integer } timeout_sec: { type: integer } retries: { type: integer } PodService: type: object description: One service in a pod. Declare an image OR a stack (which resolves to an image). required: [name] properties: name: { type: string } image: { type: string, description: Explicit image (wins over stack). } stack: { type: string, description: A catalog stack to resolve to an image. } command: type: array items: { type: string } env: type: object additionalProperties: { type: string } needs: type: array items: { type: string } description: Service names this one starts after (topological order; validated for cycles / unknown refs). health: { $ref: '#/components/schemas/PodHealth' } cpu_cores: { type: integer } ram_gb: { type: integer } gpu_count: { type: integer } SessionBody: type: object description: The POST /v1/sessions request. required: [pod] properties: name: { type: string } project: { type: string } idle_ttl_sec: { type: integer, description: 'Idle-reap TTL (default 300, or POLYGON_SESSION_IDLE_TTL_SEC).' } actions: type: array items: { type: object } description: Optional client-defined interactive actions surfaced in the session view. pod: type: object required: [services] properties: services: type: array minItems: 1 items: { $ref: '#/components/schemas/PodService' } SessionView: type: object description: A session — its lifecycle phase and (in detail) its services + teleop stream. properties: id: { type: string, example: 'ses_9f21ab' } job_id: { type: string, description: The Mode=session job carrying the pod. } org_id: { type: string } name: { type: string } project: { type: string } phase: { type: string, enum: [launching, ready, idle, stopping, stopped, failed] } idle_ttl_sec: { type: integer } created_at: { type: string, format: date-time } last_keepalive: { type: string, format: date-time } started_at: { type: string, format: date-time } stopped_at: { type: string, format: date-time } error: { type: string } services: type: array description: Present in detail views. items: type: object properties: name: { type: string } stack: { type: string } image: { type: string } needs: { type: array, items: { type: string } } actions: type: array items: { type: object } teleop_stream: { type: string, description: 'The /data stream name to drive the pod (detail views).' } # ── Cross-org node grants ──────────────────────────────────────────────── GrantSelector: type: object description: Which of the owner org's nodes a grant covers. properties: kind: { type: string, enum: [all, node, tier], description: 'Default all.' } node_id: { type: string, description: 'For kind=node.' } tier: { type: string, enum: [community, partner, trusted], description: 'For kind=tier.' } NodeGrantBody: type: object description: The POST /v1/nodes/grants request. required: [grantee_org] properties: owner_org: { type: string, description: 'Whose nodes are shared — defaults to the caller''s org (a superuser must name it).' } grantee_org: { type: string, description: 'The org being granted access.' } selector: { $ref: '#/components/schemas/GrantSelector' } expires_at: { type: string, format: date-time, description: 'Optional expiry; omit for no expiry.' } NodeGrantView: type: object description: A node-access grant (all fields non-secret). properties: id: { type: string, example: 'grant_ab12cd' } owner_org: { type: string } grantee_org: { type: string } selector: { $ref: '#/components/schemas/GrantSelector' } created_by: { type: string } created_at: { type: string, format: date-time } expires_at: { type: string, format: date-time }