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.
Authentication
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 retries
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 display
| Operation | Request | Response |
|---|---|---|
POST /api/v1/runs | CreateRunRequest | CreateRunResponse |
POST /api/v1/runners/claim | ClaimRequest | ClaimResponse |
POST /api/v1/launcher/claim | ClaimRequest | ClaimResponse |
POST /api/v1/runs/{run}/claim | Named trigger ClaimRequest | ClaimResponse |
POST /api/v1/runs/{run}/plan | PlanSubmission | PlanAccepted |
GET /api/v1/runs/{run}/snapshot | Run selector | ScheduleSnapshot |
POST /api/v1/runs/{run}/jobs/{job}/reconcile | ReconcileRequest | ReconcileResponse |
POST /api/v1/runs/{run}/jobs/{job}/transition | JobTransitionRequest | Applied |
POST /api/v1/runs/{run}/jobs/{job}/steps/start | StepReportRequest with state running | StepRecorded |
POST /api/v1/runs/{run}/jobs/{job}/steps/finish | StepReportRequest with state passed, failed or cancelled | StepRecorded |
POST /api/v1/runs/{run}/jobs/{job}/steps/skip | StepReportRequest with state skipped | StepRecorded |
POST /api/v1/runs/{run}/jobs/{job}/expansion | ExpansionSubmission | ExpansionAccepted |
POST /api/v1/runs/{run}/finish | FinishRequest | Applied |
POST /api/v1/runs/{run}/retry | RetryRequest | CreateRunResponse |
GET /api/v1/runs/{run} | Run ID or team number | RunDetail |
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 detail
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 reconciliation
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 data
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 downloads
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 groups
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 policy
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 output
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 controls
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.