{ "openapi": "3.0.2", "info": { "title": "Flox Factory Service", "summary": "Build scheduling and orchestration", "description": "Flox Factory Service API", "version": "unknown" }, "paths": { "/api/v1/factory/builds": { "get": { "tags": [ "builds" ], "summary": "List Builds", "description": "Return a paginated list of builds, newest first.\n\nThe ``status`` filter matches builds by an effective status derived\nfrom the build's lifecycle (never stored), drawn from a six-value\nvocabulary:\n\n- ``pending``: accepted but not yet dispatched to a builder.\n- ``running``: dispatched and building.\n- ``completed``: finished successfully.\n- ``failed``: finished unsuccessfully. Excludes timed-out builds,\n which match ``timed_out``.\n- ``timed_out``: terminated for exceeding its time budget.\n- ``cancelled``: cancelled, whether before or after dispatch.\n\nThese six values are both the filter vocabulary and the response\nvocabulary: a timed-out build matches ``?status=timed_out`` and\nreports ``status: \"timed_out\"`` in the response body.\n\nFilters:\n\n- ``status``: match one or more effective-status values.\n- ``system``: match system names exactly. A name outside the\n server's system vocabulary returns 422 naming the accepted\n values, which are the systems this deployment's catalog holds\n and so may differ between deployments.\n- ``attr_path``: match attr_path by prefix.\n- ``source_commit_sha``: match source commit SHA by prefix.\n- ``since``: return builds created at or after this time. An ISO\n 8601 timestamp carrying a UTC offset, e.g.\n ``2026-07-17T08:30:00Z``.\n\nFilters combine with AND across parameters and OR within a repeated\none: ``?status=running&status=failed`` matches either status, and\nadding ``&system=x86_64-linux`` further requires that system. Each\nrepeated parameter accepts at most 50 values.\n\n``cursor`` and ``sort`` are reserved for future use and have no\neffect in v1; results are always ordered newest-first.", "operationId": "list_builds_api_v1_factory_builds_get", "security": [ { "HTTPBearer": [] }, { "HTTPBasic": [] } ], "parameters": [ { "name": "status", "in": "query", "required": false, "schema": { "description": "Filter by effective status; repeatable, matched as OR. One of pending, running, completed, failed, timed_out, cancelled.", "title": "Status", "nullable": true, "type": "array", "items": { "$ref": "#/components/schemas/EffectiveBuildStatus" }, "maxItems": 50 }, "description": "Filter by effective status; repeatable, matched as OR. One of pending, running, completed, failed, timed_out, cancelled." }, { "name": "system", "in": "query", "required": false, "schema": { "description": "Filter by system, matched exactly; repeatable, matched as OR. Values must be non-empty, and a value outside the server's system vocabulary returns 422 naming the accepted values.", "title": "System", "nullable": true, "type": "array", "items": { "type": "string", "minLength": 1 }, "maxItems": 50 }, "description": "Filter by system, matched exactly; repeatable, matched as OR. Values must be non-empty, and a value outside the server's system vocabulary returns 422 naming the accepted values." }, { "name": "attr_path", "in": "query", "required": false, "schema": { "description": "Filter by attr_path prefix; repeatable, matched as OR. Values must be non-empty.", "title": "Attr Path", "nullable": true, "type": "array", "items": { "type": "string", "minLength": 1 }, "maxItems": 50 }, "description": "Filter by attr_path prefix; repeatable, matched as OR. Values must be non-empty." }, { "name": "source_commit_sha", "in": "query", "required": false, "schema": { "description": "Filter by source commit SHA prefix; repeatable, matched as OR. Values must be non-empty.", "title": "Source Commit Sha", "nullable": true, "type": "array", "items": { "type": "string", "minLength": 1 }, "maxItems": 50 }, "description": "Filter by source commit SHA prefix; repeatable, matched as OR. Values must be non-empty." }, { "name": "since", "in": "query", "required": false, "schema": { "description": "Return builds created at or after this time (inclusive). An ISO 8601 timestamp carrying a UTC offset, e.g. '2026-07-17T08:30:00Z'.", "title": "Since", "nullable": true, "type": "string", "format": "date-time" }, "description": "Return builds created at or after this time (inclusive). An ISO 8601 timestamp carrying a UTC offset, e.g. '2026-07-17T08:30:00Z'." }, { "name": "page", "in": "query", "required": false, "schema": { "type": "integer", "maximum": 10737418, "minimum": 0, "default": 0, "title": "Page" } }, { "name": "page_size", "in": "query", "required": false, "schema": { "type": "integer", "maximum": 200, "minimum": 1, "default": 50, "title": "Page Size" } }, { "name": "cursor", "in": "query", "required": false, "schema": { "description": "Reserved for future use; not implemented in v1 (results are always ordered newest-first).", "title": "Cursor", "nullable": true, "type": "string" }, "description": "Reserved for future use; not implemented in v1 (results are always ordered newest-first)." }, { "name": "sort", "in": "query", "required": false, "schema": { "description": "Reserved for future use; not implemented in v1 (results are always ordered newest-first).", "title": "Sort", "nullable": true, "type": "string" }, "description": "Reserved for future use; not implemented in v1 (results are always ordered newest-first)." } ], "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BuildListResponse" } } } }, "422": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }, "description": "Unprocessable Entity" } } } }, "/api/v1/factory/builds/{build_id}": { "get": { "tags": [ "builds" ], "summary": "Get Build", "description": "Return a single build by ID with its task sub-object.", "operationId": "get_build_api_v1_factory_builds__build_id__get", "security": [ { "HTTPBearer": [] }, { "HTTPBasic": [] } ], "parameters": [ { "name": "build_id", "in": "path", "required": true, "schema": { "type": "integer", "title": "Build Id" } } ], "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BuildResponse" } } } }, "404": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }, "description": "Not Found" }, "422": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }, "description": "Unprocessable Entity" } } }, "delete": { "tags": [ "builds" ], "summary": "Cancel Build", "description": "Cancel a build.\n\nHandles both active builds (delegated to Build Coordinator) and\npre-dispatch builds (cancelled FS-only via an atomic row lock).\n\nThe operation is idempotent with respect to terminal state. When\nthe build has already reached a terminal state (``cancelled``,\n``completed``, ``failed``, or ``timed_out``) \u2014 whether Build\nCoordinator reports it or the local task row records it \u2014 the\nresponse is still 200 and the ``status`` field carries that\nterminal state. Coordinator statuses are normalized into the\neffective vocabulary before they are surfaced: BC's ``timed_out``\nsurfaces as ``timed_out``, the same word a subsequent ``GET``\nreconstructs from the footprint the callback path persists\n(status='failed' + error_class='timeout'), so the two surfaces\nalways agree.\n\nCancelling a pre-dispatch build permanently retires its identity\ntuple: ``uq_factory_build_identity`` dedup treats the cancelled\nrow like any other terminal build, so a later event expanding to\nthe same identity inserts nothing. Recovery is manual by design \u2014\na cancel that ambient event traffic could overturn would not be a\ncancel.\n\nOutcomes:\n 200 \u2014 Build cancelled, or already terminal. ``BuildResponse.status``\n reflects the effective state.\n 404 \u2014 No build with the given ID.\n 502 \u2014 Build Coordinator unreachable or returned an unexpected\n error; or the coordinator does not know the build yet\n because its dispatch is in flight (the worker commits\n its claim before the HTTP submit), or no longer knows it\n (coordinator restart or purge). In every 502 case the\n correct client action is retry with backoff.\n\nAn audit log line is emitted on every path, including unhandled\nexceptions (``outcome=internal_error``).", "operationId": "cancel_build_api_v1_factory_builds__build_id__delete", "security": [ { "HTTPBearer": [] }, { "HTTPBasic": [] } ], "parameters": [ { "name": "build_id", "in": "path", "required": true, "schema": { "type": "integer", "title": "Build Id" } } ], "responses": { "200": { "description": "Build cancelled or already terminal; status field carries the outcome", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BuildResponse" } } } }, "404": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }, "description": "Not Found" }, "422": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }, "description": "Unprocessable Entity" }, "502": { "description": "Build Coordinator unreachable or returned an error, or the build's registration with the coordinator is still in flight; retry with backoff", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } } }, "/api/v1/factory/builds/{build_id}/logs": { "get": { "tags": [ "builds" ], "summary": "Get Build Logs", "description": "Proxy the log body for a build, source-faithfully.\n\nFetches the log content from Build Coordinator and returns it as\n``text/plain; charset=utf-8`` without modification. No escape\nstripping, no control-byte filtering, no timestamp injection.\nTerminal-escape handling is the CLI consumer's responsibility.\nThe only hygiene applied is at the coordinator client layer: valid\nUTF-8 output and a size-capped read. No JSON envelope; the body\n*is* the log.\n\nReturns:\n 200 with the log body.\n 404 if the build does not exist, has no Build Coordinator\n counterpart (task_id IS NULL), or the coordinator itself\n returns 404.\n 500 if an unexpected error occurs (e.g. get_build_by_id raises).\n 502 if Build Coordinator is unreachable or returns a 5xx error.", "operationId": "get_build_logs_api_v1_factory_builds__build_id__logs_get", "security": [ { "HTTPBearer": [] }, { "HTTPBasic": [] } ], "parameters": [ { "name": "build_id", "in": "path", "required": true, "schema": { "type": "integer", "title": "Build Id" } } ], "responses": { "200": { "description": "Log body", "content": { "text/plain": { "schema": { "type": "string" } } } }, "404": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }, "description": "Build not found or no logs available" }, "422": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }, "description": "Unprocessable Entity" }, "502": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }, "description": "Build Coordinator unreachable or returned an error" } } } }, "/api/v1/factory/tasks/{task_id}": { "get": { "tags": [ "tasks" ], "summary": "Get Task", "description": "Return a single task by ID.\n\nReturns 404 if no task with the given ID exists.", "operationId": "get_task_api_v1_factory_tasks__task_id__get", "security": [ { "HTTPBearer": [] }, { "HTTPBasic": [] } ], "parameters": [ { "name": "task_id", "in": "path", "required": true, "schema": { "type": "integer", "title": "Task Id" } } ], "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TaskResponse" } } } }, "404": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }, "description": "Not Found" }, "422": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }, "description": "Unprocessable Entity" } } } } }, "components": { "schemas": { "BuildListResponse": { "properties": { "builds": { "items": { "$ref": "#/components/schemas/BuildResponse" }, "type": "array", "title": "Builds" }, "total": { "type": "integer", "title": "Total" }, "page": { "type": "integer", "title": "Page" }, "page_size": { "type": "integer", "title": "Page Size" } }, "type": "object", "required": [ "builds", "total", "page", "page_size" ], "title": "BuildListResponse", "description": "Paginated list of builds." }, "BuildResponse": { "properties": { "build_id": { "type": "integer", "title": "Build Id" }, "status": { "$ref": "#/components/schemas/EffectiveBuildStatus" }, "source_repo_url": { "type": "string", "title": "Source Repo Url" }, "source_commit_sha": { "type": "string", "title": "Source Commit Sha" }, "nixpkgs_revision": { "type": "string", "title": "Nixpkgs Revision" }, "system": { "type": "string", "title": "System" }, "attr_path": { "type": "string", "title": "Attr Path" }, "build_type": { "type": "string", "title": "Build Type" }, "catalog_name": { "type": "string", "title": "Catalog Name" }, "exit_code": { "title": "Exit Code", "nullable": true, "type": "integer" }, "created_at": { "type": "string", "format": "date-time", "title": "Created At" }, "task": { "nullable": true, "$ref": "#/components/schemas/TaskResponse" } }, "type": "object", "required": [ "build_id", "status", "source_repo_url", "source_commit_sha", "nixpkgs_revision", "system", "attr_path", "build_type", "catalog_name", "created_at" ], "title": "BuildResponse", "description": "Build-specific details with optional task sub-object.\n\nThe task field is None for undispatched builds (task_id IS NULL\nin factory_builds).\n\nThe status field is the build's effective current status \u2014 the\nEffectiveBuildStatus vocabulary \u2014 computed server-side from the\nfreshest authoritative source:\n\n- Pre-dispatch (task_id IS NULL): computed from\n factory_builds.cancelled_at \u2014 ``\"cancelled\"`` when set, else\n ``\"pending\"``. Neither word is stored; the timestamp is the only\n persisted pre-dispatch state.\n- Dispatched: tasks.status (``\"running\"``, ``\"completed\"``,\n ``\"failed\"``, ``\"cancelled\"``), with ``\"timed_out\"``\n reconstructed from the persisted footprint of an execution\n timeout (status='failed' + error_class='timeout'). A submit-time\n ``dispatch_timeout`` is a different failure \u2014 the build never\n observably started \u2014 and reads as ``\"failed\"``.\n- On cancel responses: Build Coordinator's returned status, which\n is fresher than the local row (which lags until BC's callback\n lands), put through the same derivation \u2014 BC's ``timed_out``\n surfaces as ``\"timed_out\"``, agreeing with what a subsequent GET\n reconstructs once the callback persists the footprint. The field\n never carries a word outside the effective vocabulary.\n\nStaleness: after handoff the field tracks tasks.status, which only\nadvances when Build Coordinator's terminal callback lands. When\ncallbacks are disabled (no callback base URL configured \u2014 a\nsupported worker configuration), the post-handoff status stays at\nthe last persisted value (typically ``\"running\"``) indefinitely;\na cancel response is then the only place a fresher\ncoordinator-reported status appears." }, "EffectiveBuildStatus": { "type": "string", "enum": [ "pending", "running", "completed", "failed", "timed_out", "cancelled" ], "title": "EffectiveBuildStatus", "description": "Effective status of a build, derived server-side and never stored.\n\nA dispatched build's task lifecycle is authoritative, with\ntimed_out reconstructed from a failed task whose error class is\n'timeout'; a build cancelled before dispatch is cancelled; an\nundispatched, uncancelled build is pending. These six values are\nexactly what the derivation can emit; emittable, filterable, and\nthe whole vocabulary are the same set. The member order here is\nthe documentation order." }, "ErrorResponse": { "properties": { "detail": { "type": "string", "title": "Detail" } }, "type": "object", "required": [ "detail" ], "title": "ErrorResponse", "description": "JSON body returned for all error responses on the builds endpoints." }, "TaskErrorClass": { "type": "string", "enum": [ "transient", "permanent", "timeout", "dispatch_timeout" ], "title": "TaskErrorClass", "description": "Diagnostic class of a failed task.\n\nMirrors the ``ck_task_error_class`` CHECK as of migration 1.8.2:\nerror_class IN ('transient', 'permanent', 'timeout',\n'dispatch_timeout'); the column is NULL for non-failure\nterminals.\n\n- TRANSIENT: retry-able by the sweeper.\n- PERMANENT: will not improve on retry.\n- TIMEOUT: the coordinator reported an execution timeout \u2014 a\n build that ran and overran its limit.\n- DISPATCH_TIMEOUT: the submit HTTP call to the coordinator\n itself timed out, so the build's handoff never observably\n started.\n\nThe last two are kept distinct so the read surface shows an\nexecution timeout as timed_out while a submit timeout reads as\nfailed." }, "TaskResponse": { "properties": { "task_id": { "type": "integer", "title": "Task Id" }, "task_type": { "type": "string", "title": "Task Type" }, "status": { "$ref": "#/components/schemas/TaskStatus" }, "error_message": { "title": "Error Message", "nullable": true, "type": "string" }, "error_class": { "nullable": true, "$ref": "#/components/schemas/TaskErrorClass" }, "created_at": { "type": "string", "format": "date-time", "title": "Created At" }, "updated_at": { "type": "string", "format": "date-time", "title": "Updated At" }, "started_at": { "title": "Started At", "nullable": true, "type": "string", "format": "date-time" }, "completed_at": { "title": "Completed At", "nullable": true, "type": "string", "format": "date-time" } }, "type": "object", "required": [ "task_id", "task_type", "status", "created_at", "updated_at" ], "title": "TaskResponse", "description": "Generic task lifecycle \u2014 same shape for all operation types.\n\nThe status field carries the task vocabulary \u2014 the persisted\nlifecycle words, distinct from the derived effective vocabulary\non BuildResponse.status. The task sub-object reports the stored\nfootprint the top-level status is derived from: a timed-out\nbuild's task still reads status='failed' + error_class='timeout'\nwhile the build-level status reads 'timed_out'." }, "TaskStatus": { "type": "string", "enum": [ "running", "completed", "failed", "cancelled" ], "title": "TaskStatus", "description": "Valid status values for tasks.\n\nMirrors the CHECK constraint as of migration 1.8.0:\nstatus IN ('running', 'completed', 'failed', 'cancelled').\n\nThe claim CTE inserts tasks directly as 'running' (the claim is\nthe dispatch; there is no separate queued phase). The three\nterminal values are written by ``process_callback`` and\n``mark_task_failed``." } }, "securitySchemes": { "HTTPBearer": { "type": "http", "scheme": "bearer" }, "HTTPBasic": { "type": "http", "scheme": "basic" } } } }