Controller HTTP API

Reporting reads use the shared capability policy and the authorized resource's team. This includes run lists, receipts, attempts, steps, events, approvals, debug pauses, pipeline lists and latest runs, trends, runner views, sizing and logs. Unbound machine roots and deployment credentials cannot substitute for team membership. Work and trigger claims receive only the read alternatives declared for their bindings.

Latest pipeline reads select verified default-branch runs by default. Pass from_any_run=true to include other runs. A job or owner capability can use that opt-out only when its plan declares from_any_run: true on the pipeline reference. Direct cross-run job-output reads enforce the same requirement.

Run receipts use the same response on HTTP and the local API socket. Each step identifies its job with job_id. Receipts omit raw invocation metadata and local store locations.

Job reports expose output_digest from the job's committed output record. Receipts include that digest as sha256:<digest> in identity.outputs_hash, including outputs copied from a memo. Collected records omit the digest.

Job logs use /api/v1/runs/{run}/jobs/{job}/logs, with /stream and /completeness beneath it. The old /runs/{run}/logs/{job} paths are removed. Use /api/v1/runners instead of /api/v1/agents, /api/v1/approvals instead of /api/v1/approvals/pending, and /api/v1/sizing or /api/v1/sizing/explain instead of /api/v1/capacity/profiles. Read debug pauses at /runs/{run}/debug-pauses. The paused alias is removed.

Schedule reads and writes use /api/v1/schedules. Repository replacement uses PUT /schedules/repos; repository deletion uses DELETE /schedules/repos. Delete one schedule with DELETE /schedules/{schedule}. The /api/v1/crons family and POST .../disarm are removed. Reads require member runs.read; writes require member runs.control. List queries accept all and next; detail queries accept fires and next, including zero. Overrides preserve explicit empty argument sets, empty zones and zero catch-up.

Repository schedule registration returns 422 plan_failed when source ref resolution finds an unreachable or unknown repository, refused Git credentials, or a missing ref. The message names the repository and ref and asks the caller to check the URL, credentials and ref. These refusals are logged at info and leave schedules unchanged. Server faults, such as a missing Git executable, return 500 internal and are logged at error.

Approval decisions use POST /runs/{run}/approvals/{job} with resolution (approved or denied) and optional comment. The authenticated identity supplies the approver. Success returns {"approval":{...},"replayed":false}. Approval reads and decisions use job_id. The approval-request HTTP route is removed; the run owner creates gates through its execution lifecycle.

Schedule run-now, approval decisions and debug-pause creation require one Idempotency-Key header. Debug-pause creation accepts job_id, reason and optional expires_at, with the run selected by the route. Caller-supplied approvers, release fields, run selectors and legacy node_id fields are refused. Embedded controllers supply WithScheduleWriter and WithRunControlWriter; these adapters recheck authority and commit state, events and replay together. Missing transaction adapters return policy_missing.

Team invitation, build-trust, billing and storage reads use the same capability boundary, as do GitHub App installation, repository, trigger and extra-repository reads. Billing availability is checked after authentication. The API's /metrics route requires deployment operator.limits or a verified console session. A dedicated metrics listener keeps its existing network boundary.

The CLI, runners, fetched engines and dashboard use /api/v1. The local controller exposes the same messages and authorization policy over its private API socket. The HTTP reference is generated from the shared capability table. It includes node-host and external-proof routes with their separate audiences.

Inventory lists and run detail collections accept limit and an opaque cursor, default to 50 rows and accept at most 200 per page. Responses retain their named collection field and include next_cursor and truncated. Pass a nonempty next_cursor back as cursor, preserving the other filters, until truncated is false. Raw array responses now use a named collection envelope: members, invitations, deliveries, pauses, deletions or jobs, as appropriate. The CLI and dashboard follow these cursors to display complete inventories. Existing run, log, metric, sizing and other bounded-query pagination retain their own limits.

The repository's api/openapi.yaml is generated from the same policy and the shared wire types. Each operation retains its complete authorization alternatives under x-sparkwing-policy. JSON schemas preserve omitted fields, nulls and byte encoding. Shared validators enforce constraints beyond those serialization shapes. Operations without a documented success body carry x-sparkwing-wire: undocumented. They do not inherit a body from a removed endpoint.

The shared wire contract and controller policy use the same route declarations. Native deployment and storage composition require acceptance evidence beyond client transport fixtures.

AuthenticationSection anchor link

A route requires one complete access alternative: credential class, every listed grant, resource binding and liveness. Alternatives cannot supply one another's missing checks. Ownership comes from a server lookup. A request cannot choose its actor, granted runner identity, billing provenance or claim fence.

Capabilities, browser sessions and local peers pass the same applicable resource checks. Operator-console sessions and deployment-admin capabilities have separate powers. See Authentication and Security.

External routes name their own proof: GitHub webhook HMAC, payment signature, GitHub Actions OIDC, browser CSRF/nonce or exact signed-object URL. A missing or invalid credential returns 401. A live credential without the route's authority returns 403. Resource conflicts use their canonical code, including lock_held.

Machine enrollment at POST /api/v1/runners requires a direct account session with runs.write for a personal runner or team.admin for a shared runner. The issuing transaction rechecks that authority, repository patterns, reserved pools, the existing runner limit and active name uniqueness. Service issuance at POST /api/v1/operator/tokens rechecks deployment authority and active service names. Registration facts persist with the capability root. Machine claims retain every ancestor's repository restriction, including after delegation and rotation.

Errors and retriesSection anchor link

HTTP errors use one envelope:

{"error":{"code":"lock_held","message":"another holder owns this claim","hint":"wait for its lease"}}

The inner object requires a nonempty code and a string message. Optional hint and details retain their declared string and object types. Clients accept unknown additive fields and future codes. They reject duplicate keys, null required fields, malformed bodies and nested envelopes without echoing the body.

A client-canceled request returns HTTP 499 (Client Closed Request) when its response headers have not been sent. A named claim for a missing execution unit returns HTTP 404. Neither case is a server fault.

The route's retry policy governs the shared HTTP/socket transport. Exact-body retries preserve the body. Keyed intake keeps its idempotency key and body. Claims, named claims and reconciliation can return a fresh bearer once, so a lost response is never automatically replayed. Here intake and retry also suppress replay. Lease recovery resolves an uncertain claim. A lost initial here bearer requires a fresh trigger request. See Here runners.

Intake, execution and displaySection anchor link

OperationRequestResponse
POST /api/v1/runsCreateRunRequestCreateRunResponse
POST /api/v1/runners/claimClaimRequestClaimResponse
POST /api/v1/launcher/claimClaimRequestClaimResponse
POST /api/v1/runs/{run}/claimNamed trigger ClaimRequestClaimResponse
POST /api/v1/runs/{run}/planPlanSubmissionPlanAccepted
GET /api/v1/runs/{run}/snapshotRun selectorScheduleSnapshot
POST /api/v1/runs/{run}/jobs/{job}/reconcileReconcileRequestReconcileResponse
POST /api/v1/runs/{run}/jobs/{job}/transitionJobTransitionRequestApplied
POST /api/v1/runs/{run}/jobs/{job}/steps/startStepReportRequest with state runningStepRecorded
POST /api/v1/runs/{run}/jobs/{job}/steps/finishStepReportRequest with state passed, failed or cancelledStepRecorded
POST /api/v1/runs/{run}/jobs/{job}/steps/skipStepReportRequest with state skippedStepRecorded
POST /api/v1/runs/{run}/jobs/{job}/expansionExpansionSubmissionExpansionAccepted
POST /api/v1/runs/{run}/finishFinishRequestApplied
POST /api/v1/runs/{run}/retryRetryRequestCreateRunResponse
GET /api/v1/runs/{run}Run ID or team numberRunDetail

The wire types live in internal/controllerwire. Intake seals requested inputs once. Acceptance seals checkout declarations, resolved settings and the validated plan together. Retry retains the original request, source authority, SHA and accepted seals, with the retrier recorded separately. It never rewrites the original seal's identity to match the new run ID.

Retry requires mode=failed|all and a nonempty idempotency_key in the request body. The server rejects a missing or unknown selection before creating a run.

Direct store consumers use Store.ClaimUnit for launcher and runner awards. Store.ClaimLaunch, LaunchClaim, LaunchClaimRequest, MinLaunchLifetime and LaunchDeadlineSlack are removed. HTTP consumers call Claim on the controller returned by client.NewEngineClient with EngineOptions.Launcher. Client.ClaimLaunch is removed. The granted service root supplies identity and metering. Claims persist their fences, billing reservations and bootstrap credentials in one transaction.

Owner handoff keeps the awarded execution run ID separate from that original seal ID. The host receives the inner request document and its canonical seal hash, plus the accepted spec and plan when resuming. Local, here and runner adapters preserve those inputs before host binding. Initial intake receipts still require the returned run ID to match their newly created seal. Local, standalone and here hosting derive the owner fence from the award and preserve the reason when authority cancellation ends hosting.

Private claim and reconciliation responses carry execution settings, fences, accepted inputs and fresh capabilities. Public RunDetail and JobDetail carry safe display projections. Status clients never substitute a raw execution seal for a display record.

A snapshot is one consistent scheduling read. Generator replay uses the exact accepted body and source fence, including empty expansions. The persisted topology stays authoritative. Held jobs receive a cancellation request while their current holder retains result rights. A result replay compares the actor, operation, fence and body digest. Holder start, retry, report and unstarted release include source_attempt. Initial start uses zero. Later reports use the captured ordinal returned by Applied.attempt. Omission and explicit zero remain distinct. Other actors and causes omit the field. The store refuses a mismatched ordinal and replays an identical transition without consuming another attempt.

Run completion returns the committed verdict in Applied.outcome and its reason in Applied.reason. Local and remote hosts use that result for completion output and exit status because cancellation can commit after their last scheduling snapshot.

Step reports carry a positive source_attempt, step ID, state, optional masked error and optional start/finish timestamps. They carry no actor or claim fence. The controller derives those from authorization and current state, compares the source ordinal, and commits through the store's attempt-fenced step writer. The host waits for {"recorded":true} before emitting its step diagnostic. The controller commits these reports through the shared attempt-fenced writer.

A reported job result carries failure: {job, stage, error} when it has a structured failure. The inner error retains its code, message, hint and details. Recovery reads that same persisted failure. An outcome or reason cannot replace its stage. The error-only result field is removed.

Run detailSection anchor link

GET /api/v1/runs/{run}/attempts returns retry history as a cursor-paginated RunPage, oldest first. Stable IDs and team-local numbers select the same history. Each entry uses the shared public RunDetail projection, including redacted inputs and current inputs.lineage. Invocation metadata, execution seals, environment digests and checkout paths are omitted. Every returned run must pass the caller's binding. A denied or invalid member fails the response before any history is sent. Equal creation timestamps sort by run number. Following cursors reads long chains. Cycles cannot repeat entries or prevent traversal from finishing.

RunPruner.DeleteRun uses the authorized DELETE /api/v1/runs/{run} route. It succeeds after an empty HTTP 204 acknowledgement, including log cleanup. The client returns only an error. Deleting another member's run requires the credential to carry team.admin. A credential with runs.control may delete its own account's runs. Prune output identifies the selected run and any structured failure. It does not report a removed-row count or replay state. Cancel, retry and prune preserve numeric run selectors and stable IDs from flags or stdin. The authenticated controller resolves them within the current team.

GET /api/v1/runs/{run} returns the shared run status object with inputs, jobs, attempts, logs and notices. Job and attempt status excludes raw declarations, holder identities and capability prefixes. Log subjects preserve the distinction between job attempts and trigger, plan or eval work.

Inputs always use the display projection. Secret argument values are redacted. Missing or invalid classification redacts every value, including on finished runs. The include query cannot reveal secrets or select an execution view. Members need runs.read. Bound claims need source.read or trigger.host for that run. Missing or dead credentials receive 401; live credentials without the required authority receive 403.

The controller requires its shared verifier and a complete status-read adapter. An absent adapter returns policy_missing. It never fills missing history with invented attempts or returns a partial status response after a read error.

Launcher reconciliationSection anchor link

POST /api/v1/launcher/sync accepts LauncherSyncRequest. Each observation contains award: {kind, id, holder, generation}, job: {namespace, name, uid}, and the reserved node_uid. The award generation identifies the unit award, including an eval's own lease. It does not replace that eval's source generation. The server compares observations with this launcher's stored awards.

The default store adapter reads ancestry, current claims and execution inputs in one snapshot. It checks the root and service identity, holder, generation, expiry and cancellation. Eval awards also require unresolved source expansion, and for a controller-dispatched run, that the controller still owns the run. Exchange preserves the original award shown by synchronization. Sync never changes a claim or returns its bearer.

The response contains held awards without bearer material and a remove list of exact Job instances. Each removal carries namespace, name and UID. The launcher must use that UID as a Kubernetes deletion precondition because a new Job can reuse the name. A failed transport, decode or validation returns no partial sync result. Native Kubernetes adoption and teardown still require execution evidence.

Logs and stored dataSection anchor link

Run logs use GET /api/v1/runs/{run}/logs. Job reads, streams and completeness use /api/v1/runs/{run}/jobs/{job}/logs, /logs/stream and /logs/completeness. Job segment and manifest writes use the matching /log-segments and /log-manifest routes. A commit verifies the stored objects it records; the final commit's manifest names the manifest's stored size and digest, and a mismatch answers integrity_failure. Log subjects distinguish job attempts from trigger, plan and eval units. A final log-tail exception cannot reopen work or write another attempt's logs.

JSON log pages set complete=true once terminal execution and all available segments have been observed. Missing output is reported separately in the optional incomplete list, with subject and reason for each affected stream. An abandoned writer reports writer_lost after the final-tail grace, even before maintenance persists its incomplete seal. Retained bytes and continuation cursors remain readable; known incompleteness does not return HTTP 409 or 500. Pages with truncated=true still require immediate continuation reads.

An interrupted or incomplete observation remains visible independently of the recorded run verdict. The CLI returns exit 3 when it cannot establish a complete observation, and exit 4 when the controller refuses the read (not_found, unauthenticated, forbidden), which retrying cannot change. See Observability.

Blob transfers and logical memo/depcache records use the authenticated /data routes. Storage authorization rechecks the live capability, bound uploader and persisted read edges. Object requests receive only signed transfer headers. See Direct data uploads.

Data downloadsSection anchor link

POST /api/v1/data/download takes {"digest":"<sha256>"}. It requires a live data capability and a digest in that caller's read set. The controller resolves the team, repository and ref from authenticated authority. A digest, team query or object path grants no access by itself. Ordinary member and machine tokens cannot download bytes without the required data authority.

The response contains url, headers, sha256, size and expires. Send only the returned headers to the storage URL. The controller charges the stored object size against the team's daily download cap before signing. The URL names one immutable object and expires after 60 seconds. Revocation stops new signing requests. Issued URLs remain valid until their expiry. S3 and filesystem backends use the same authorization boundary.

POST /api/v1/data/has-many takes {"digests":["<sha256>"]} and returns {"present":["<sha256>"]}. Missing and unreadable digests are omitted. An empty array returns an empty array. Duplicate digests appear once. An optional deadline is an RFC 3339 timestamp that bounds this query. Presence checks transfer no object bytes and consume no download allowance.

Both routes use the common error envelope. Missing or dead authority returns 401. Live authority without the required grant or read edge returns 403. An authorized missing blob returns 404 with blob_missing. A spent download allowance returns 429 with Retry-After. Private backend errors return internal without object keys or diagnostic text.

GET /api/v1/services announces the download route only when its authority, read-set, metadata, signing and charging adapters are configured. Signed URLs work through the backend selected by the controller. Cache grants and {kind,key} download requests are removed. There is no cache-service fallback. Logs use controller-proxied reads, separate from byte-storage downloads.

Concurrency groupsSection anchor link

GET /api/v1/queue/groups/{group} reads one concurrency group as QueueGroup, with one-based waiter positions, under a live claim. No route acquires, heartbeats, releases or cancels a group directly: every acquisition goes through the admission owner.

Version policySection anchor link

A repository's exact SDK pin selects its verified engine. From launch freeze, the controller supports requests emitted by each engine for twelve calendar months after its release. Older engines continue until a signed security revocation or minimum-version policy takes effect. Additive fields and codes remain readable. Each selected engine owns its matching node protocol. See Versioning.

An applicable Go SDK replacement uses the installed CLI's engine and emits an engine_pin_disabled notice. The pin retains the declared SDK version and the replacement module selector. The accepted identity records the installed executable's digest in manifest_digest, rather than a signed release manifest.

Job outputSection anchor link

GET /api/v1/runs/{run}/jobs/{job}/output returns {"output": VALUE} from the job's committed output record. Members need runs.read. Execution credentials need a current declared read relation. The child output path uses the creating job's child relation. Missing or corrupt bytes fail the read.

Refunds are made in the Stripe dashboard. The controller learns of them only through Stripe's refund webhooks, which reverse the refunded credits once.

Cloud overflow controlsSection anchor link

GET /api/v1/overflow returns the authenticated account's monthly_minutes, used_seconds and UTC period_start. The default limit is null. PUT replaces the saved limit with {"monthly_minutes":N} or removes it with {"monthly_minutes":null}. A sealed request.burst.overflow_minutes can only narrow that account limit. Team billing admission still applies. Self-hosted and local capacity remains eligible when Cloud claims wait for budget.

CreateRunRequest.local_secrets carries requester-resolved values during intake. RenewalRequest.local_secrets re-supplies them only through the live here owner, including after controller restart. Other claimants cannot supply or replace values. The sealed request retains local_secret_names. Responses omit the values. Secret release follows the existing live claim and declared-secret checks and returns masked:true. Values remain in the intake controller's memory and are removed on terminal completion, team deletion and owner lease expiry. Reads recheck the sealed burst preference set. Values remain unavailable until the here host restores them. Retry requires a fresh requester submission.