Appearance
Runner protocol v1
Included from
api/runner-v1.md. Edit it there.
The runner initiates every connection to the control plane. The transport is HTTP/2 over TLS 1.3 with a verified client certificate. The runner certificate has a lifetime of at most 24 hours and exactly one runner identity URI SAN: spiffe://forelead/runners/<runner-id>/workspaces/<workspace-id>. The request body cannot choose another runner or workspace. Renewed certificates are read on a new handshake.
All JSON requests and responses carry apiVersion: "v1". An unsupported version is rejected. IDs and lease tokens are opaque to clients. A lease lasts 30 seconds, and its expiry is in control-plane UTC. Runners measure clock offset before registration, so they can compare lease expiry against server time. Claiming requires an active registration. A runner may hold at most its declared number of active leases, and a job is eligible only when its workspace and required labels match. After expiry, the job can be claimed with a new token; old tokens cannot renew, release, or send stream messages. Release returns the job to the queue.
| Method and path | Request | Response |
|---|---|---|
GET /v1/runner/clock | none | apiVersion, serverTime |
POST /v1/runner/register | apiVersion, runnerId, workspaceId, slots, cpu, memoryBytes, architecture, labels, clockOffsetNs | apiVersion |
POST /v1/runner/claim | apiVersion, runnerId | lease or 204 when none is available |
POST /v1/runner/renew | apiVersion, jobId, leaseToken | renewed lease |
POST /v1/runner/release | apiVersion, jobId, leaseToken | 204 |
POST /v1/runner/code/head | apiVersion, jobId, leaseToken | JSON apiVersion, commit |
POST /v1/runner/code/bundle | apiVersion, jobId, leaseToken, kind, commit | application/x-git-bundle bytes |
POST /v1/runner/inventory | apiVersion, runnerId, items (each kind, name, jobId, state, image, createdAt, cpu, memory, orphan) | JSON apiVersion, reap (orders kind, name) |
POST /v1/runner/events | NDJSON messages | 204 |
POST /v1/runner/logs | NDJSON messages | 204 |
inventory is how the control plane sees the runtime without Docker access: the runner lists the containers and networks it created (every one carries forelead.runner, forelead.workspace, forelead.task and forelead.job labels), about every 15 seconds. Only names the launcher generates are accepted. orphan is the runner's own statement that no live job on it owns the resource. The reply carries the orphans an administrator ordered removed; the runner removes one only if it still calls it an orphan. The workspace comes from the certificate, never from the body.
Each lease contains apiVersion, job (id, workspaceId, labels, and, for a checkout job, repository, targetBranch, baseCommit, and baseTree pinned when claimed), token, and expiresAt. Each NDJSON message contains apiVersion, runnerId, jobId, leaseToken, sequence, and data. Events and logs use separate concurrent HTTP/2 requests and independent increasing sequence numbers per lease. A stream is bounded to 1 MiB and 25 seconds client side; the caller opens another request to continue. Messages with an expired or foreign lease, an unsupported version, or a repeated sequence are rejected.
Code routes require the current runner certificate and the exact active job lease. The control plane owns code-host credentials; the runner receives only commit IDs and bundles. code/head is for agent jobs and returns the current target-branch commit. code/bundle accepts kind: "base" only for the lease's baseCommit, kind: "head" only for the current target head of an agent job, and kind: "published" only for the durable published commit of an agent job. The commit request field must equal that permitted commit. The runner requires a valid baseTree in its lease before requesting a base bundle and verifies that the imported base commit has that exact tree. Missing or mismatched code identity, lease, commit, or tree is refused. Bundle responses are capped at 256 MiB and are never a source of code-host credentials.
The v0 broker is process-local. The coordinator can enqueue jobs through the Go Broker.Enqueue boundary. Durable queueing, a task-to-job adapter, and harness execution are outside this protocol increment.
All runner routes in the code
The Go code registers 41 routes under /v1/runner/. This table is generated at build time from the code, so it cannot drift. "Documented" means the route appears in the table above in this contract; "not yet" routes work, but their request and response shapes are only in the source file.
| Route | Documented | Registered in |
|---|---|---|
GET /v1/runner/clock | yes | internal/runnerproto/http.go |
POST /v1/runner/agent/blocked | not yet | internal/runnerproto/agent_blocked.go |
POST /v1/runner/agent/certificate | not yet | internal/runnerproto/task_certificate.go |
POST /v1/runner/agent/context | not yet | internal/runnerproto/agent_context.go |
POST /v1/runner/agent/decision-boards | not yet | internal/runnerproto/decision_proposals.go |
POST /v1/runner/agent/decisions | not yet | internal/runnerproto/decisions.go |
POST /v1/runner/agent/decisions/ack | not yet | internal/runnerproto/decisions.go |
POST /v1/runner/agent/decisions/pending | not yet | internal/runnerproto/decisions_pending.go |
POST /v1/runner/agent/environment-changed | not yet | internal/runnerproto/agent_environment.go |
POST /v1/runner/agent/environment-failure | not yet | internal/runnerproto/agent_environment_failure.go |
POST /v1/runner/agent/gate-failure | not yet | internal/runnerproto/agent_gate_failure.go |
POST /v1/runner/agent/gate-run | not yet | internal/runnerproto/agent_gate_run.go |
POST /v1/runner/agent/gates | not yet | internal/runnerproto/gate_delivery.go |
POST /v1/runner/agent/grant-boards | not yet | internal/runnerproto/decision_proposals.go |
POST /v1/runner/agent/manifest | not yet | internal/runnerproto/agent_manifest.go |
POST /v1/runner/agent/notes | not yet | internal/runnerproto/agent_notes.go |
POST /v1/runner/agent/planning | not yet | internal/runnerproto/planning.go |
POST /v1/runner/agent/publish | not yet | internal/runnerproto/agent_publish.go |
POST /v1/runner/agent/research | not yet | internal/runnerproto/research.go |
POST /v1/runner/agent/review | not yet | internal/runnerproto/agent_review.go |
POST /v1/runner/agent/review-certificate | not yet | internal/runnerproto/review_certificate.go |
POST /v1/runner/agent/scope | not yet | internal/runnerproto/agent_scope.go |
POST /v1/runner/claim | yes | internal/runnerproto/http.go |
POST /v1/runner/code/bundle | yes | internal/runnerproto/code.go |
POST /v1/runner/code/head | yes | internal/runnerproto/code.go |
POST /v1/runner/complete | not yet | internal/runnerproto/http.go |
POST /v1/runner/events | yes | internal/runnerproto/http.go |
POST /v1/runner/inventory | yes | internal/runnerproto/inventory.go |
POST /v1/runner/logs | yes | internal/runnerproto/http.go |
POST /v1/runner/pr-update/result | not yet | internal/runnerproto/pr_update.go |
POST /v1/runner/preflight/evidence | not yet | internal/runnerproto/preflight_delivery.go |
POST /v1/runner/register | yes | internal/runnerproto/http.go |
POST /v1/runner/release | yes | internal/runnerproto/http.go |
POST /v1/runner/renew | yes | internal/runnerproto/http.go |
POST /v1/runner/session/chunks | not yet | internal/runnerproto/session.go |
POST /v1/runner/session/effects/finish | not yet | internal/runnerproto/effects.go |
POST /v1/runner/session/effects/history | not yet | internal/runnerproto/effects.go |
POST /v1/runner/session/effects/pending | not yet | internal/runnerproto/effects.go |
POST /v1/runner/session/effects/prepare | not yet | internal/runnerproto/effects.go |
POST /v1/runner/session/list | not yet | internal/runnerproto/session.go |
POST /v1/runner/session/read | not yet | internal/runnerproto/session.go |