Runner API
Note
Synced from tie/docs/runner-api.md on 2026-11-02. Edit the source, not this copy.
The runner daemon (cmd/tie-runner, and any compatible server) exposes an
HTTP JSON API for submitting batch jobs, sequencing multi-step pipelines,
deploying container images, administering long-running services, and
clustering runners into a fleet. This document is the normative
specification; the machine-readable counterparts are the Route table and
the golden fixtures in testdata/runner/ (see runner/api).
1. Conventions
- Transport. HTTP/1.1; servers normally serve TLS. A plain-HTTP
listener may additionally exist for the static fileserver (see
FileServerconfig). - Bodies. JSON, UTF-8. Request bodies are decoded leniently: unknown
fields are ignored, so servers and clients of different versions can mix.
Field names are wire format and most types have no
jsontags — the Go field name is the JSON name (PascalCase). Some fleet types carry lowercase tags (e.g."join_token","external_port"); each fixture pins the exact spelling. - Errors. Two shapes, per endpoint (pinned by fixtures):
- a zero-valued
JobStatuscarrying onlyErrorMsg(testdata/runner/job-zero.json), used by the job/pipeline endpoints; - plain-text
http.Errorbodies for fleet-registry and validation failures. Prefer shape 1 on/api/*data-plane endpoints.
- a zero-valued
- Auth. HTTP Basic. Three credential kinds:
- DeployKey — per-image secret minted at deploy. Password auth; the
username is checked only if the server config sets
AuthUser(empty = accept any username). Gates: job cancel, pipeline cancel/resume (key of the entry step), image delete, service start/stop, deploy of an existing image. - AdminToken — shared fleet secret (
AdminTokenconfig; empty disables those endpoints entirely). Gates:POST /api/reload,POST /api/cert, fleet host listing/approve/remove, image replicate. - FleetJoinToken — seed-only secret. Gates
POST /api/fleet/register.
- DeployKey — per-image secret minted at deploy. Password auth; the
username is checked only if the server config sets
sincequery parameter.GET /api/jobs,/api/pipelines,/api/statsacceptsinceas a Unix timestamp, a Go duration ("720h"), or a day count ("30d"). Filtersuserandstatusare exact matches.- Tie integration. Content is exchanged by hash through the tie
filehost (
Filehostconfig +Tie-Storeheader).GET /api/filehosttells clients which URL and store to use. Input trees are tie directory manifests; step input selection followsio/compose(Spec): the selection vocabulary is byte-identical on both sides of the wire. - Version reporting.
GET /api/capabilitiescarries the server’sVersion,GitCommit,BuildDate. Clients gate behavior on it (and treat empty values as “predates version reporting”).
2. Routes
The complete table (method, pattern, access) lives in runner/api.Routes
and testdata/runner/routes.json; it is kept equal by
TestRoutesManifest. Sections below document behavior. Fixtures in
testdata/runner/ are exactly what a server marshals for each reply.
run — POST /api/run
Request JobRequest (job-request.json) → reply JobStatus
(job-status.json). Access: public (the User field is self-reported
attribution). The runner may execute locally or forward the request to a
fleet peer whose capabilities fit; the reply always comes from the
responding runner with DeployManager naming the executing host.
ForceAddToQueue queues even when a local slot is free. PipelineID/
StepName are reserved for pipeline stepping.
status — GET /api/status/{jobid}
→ JobStatus. Forwarded to the owning backend when the responding runner
does not hold the job. A not-found is a plain-text 404.
jobs — GET /api/jobs, DELETE /api/jobs/{jobid}
GET → array of JobStatus (jobs-list.json), newest first, with Log
elided (fetch the log via /api/status/{jobid}). DELETE cancels; auth =
DeployKey of the job’s handler image.
pipelines — POST/GET /api/pipelines, GET/DELETE /api/pipelines/{id}, POST /api/pipelines/{id}/resume
A pipeline is a whole step graph submitted at once (PipelineRequest,
pipeline-request.json); the runner sequences it server-side. Steps name
each other via compose.Spec.FromStep; Seeds pre-satisfies a name with a
tree hash the client already uploaded. Status values are the job vocabulary
(pipeline level) and pending/running/done/failed/skipped (step level).
Replies are Pipeline (pipeline.json). DELETE cancels (DeployKey of
the entry step). POST …/resume accepts ResumeRequest
(resume-request.json) or ?from=, with an empty body meaning “retry what
did not finish”.
queue-active-running — GET /api/queue, GET /api/active, GET /api/running
Queued jobs (array), the single active job (a bare JobStatus or null,
back-compat shape), and running jobs (array). Public.
deploy — POST /api/deploymanager
Request DeployRequest (deploy-request.json), reply DeployResponse
(deploy-response.json). Command is "test" (build + test-run a job, no
image published) or "prod" (build, push, deploy). ProjectHash names a
tarball already uploaded to the filehost. Deploying a name that already
exists requires that image’s DeployKey; a first deploy mints one and
returns it in DeployKey. JobType selects batch handler vs service
(WebserviceV1, TCPServiceV1); service fields configure ports and
autostart, and the assigned stable port comes back in
ServiceExternalPort.
filehost — GET /api/filehost
→ DeployResponse with Message = filehost URL, Store = the configured
upload store (filehost-reply.json). Clients upload where the runner’s
jobs will read from.
fleet — GET /api/fleet
→ FleetReply (fleet.json): every runner URL known to the responder
(itself + configured Backends). This is how a client that knows one
runner discovers them all.
fleet-registry — POST /api/fleet/register, GET /api/fleet/hosts, POST /api/fleet/approve, POST /api/fleet/remove
New runners join a fleet by repeatedly POST-ing RegisterRequest
(register-request.json, auth = join_token) to the seed until an admin
approves; the endpoint is idempotent per URL (polling is the join
protocol). RegisterReply (register-reply.json) returns status
(pending/active) and, once active, the seed’s full config echo
(config.json) so the joiner can write its own configuration. Host
listing/approval/removal need the AdminToken; non-seed runners proxy these
to the seed. FleetHost entries (fleet-hosts.json): pending/active/
removed. admin_token is included in a listing only when it is meant to
bootstrap a joiner.
cert — POST /api/cert
AdminToken. Body {"cert": PEM, "key": PEM}. The server installs the
pair and hot-swaps the live TLS listener (no restart); reply reports the
new subject and expiry. GET /api/capabilities’ CertNotAfter is the
fleet-wide expiry monitor.
stats — GET /api/stats
Query since (see conventions) and local=true to skip the fleet merge.
→ JobStats (stats.json). Servers merge peer stats; ActiveUsers maps
month → user list for cross-runner dedup and is omitted when empty.
capabilities — GET-or-POST /api/capabilities
→ CapabilityReply (capabilities.json): job types, live resource pool
(free/total CPU/RAM/GPU, queue length, active count), build info,
CertNotAfter, DeployedImageCount. This is the routing input for
capacity-aware dispatch and the health-diff for admins.
reload — POST /api/reload
AdminToken. Hot-reloads mutable config; {"status":"reloaded"}. Disabled
when AdminToken is empty. Signal-based reload works regardless.
images — GET /api/images, PUT /api/images/replicate, DELETE /api/images/{image}
GET → []ImageInfo (images.json) — never contains DeployKeys. PUT
(AdminToken) pushes one image entry fleet-wide (body = the image record).
DELETE (DeployKey) removes the image and stops its service if any.
services — GET /api/services, GET/POST/DELETE /api/services/{service}
GET → map of TaskInfo keyed by service name (services.json); one
service via the named form. POST starts, DELETE stops (both
DeployKey). Statuses: running / stopped / crashed (crashed = died after
start; logs carries the reason).
proxy — /service/{service}[/{params…}]
The data plane. Requests are forwarded to the service container with the
/service/{name} prefix stripped; responses are rewritten (HTML/XML/CSS/
JS URL rewriting, Set-Cookie path prefixing, Location rewriting) so
apps work under a shared origin. ServiceProxyRewrite (per image) limits
rewriting (all default, html, none) for backends that emit their own
prefixed URLs. WebSocket upgrade requests are tunneled. Clean-URL mode
keys off X-Forwarded-Host.
files — GET /files/…
Static fileserver when enabled (client binaries, certificates). The
entrypoint binary name published here is configurable (default
tie-exec); container images fetch it at build time.
3. Container contract (entrypoint ↔ runner)
The runner executes handler images by invoking the entrypoint binary
(cmd/tie-exec) inside the container. This contract is frozen:
Entrypoint flags:
-filehost URL -inputhash HASH [-store NAME] [-settings /settings.json] [-apphash HASH] [-debug] [-app NAME] [-args A...] [-input /input] [-output /output] [-prefix /tmp] [-usefuse] [-cache-size-gb N] [-version].Fixed container paths: input tree at
/input(FUSE-mounted on demand or pre-downloaded), output collected from/output, settings file at/settings.json, injected app binary at/app(when-apphashset). The host-side tree directory bind-mounted into the container at/extis configurable on the host side (ExtDir).Result protocol on stdout. Lines of the form
#!TIE SetResult <output-hash> #!TIE SetError <message> #!TIE PercentDone <number> (conventionally percent complete; the parser accepts any float, apps define the scale)Servers accept the canonical
#!TIEtoken and (for compatibility with entrypoints deployed before this spec) the legacy spelling of the same protocol name. Entrypoints may emit both; servers ignore unrecognized prefixes. The child application’s exit code propagates through the entrypoint.Progress may also be inferred by the runner sampling container memory; memory peak/current are reported in
JobStatus.
4. Configuration echo and fleet join compatibility
POST /api/fleet/register echoes the seed’s whole configuration
(config.json fixture, key set pinned by test). Rules:
- The echoed key set may grow; joiners must ignore keys they don’t know.
- A key is never renamed or removed.
- Servers older than this spec may echo additional sections (e.g. cloud batch coordinator settings); core joiners ignore them.
5. Compatibility rules (normative)
- Route patterns and methods are frozen. New routes are additive.
- Exported field names in every type of
runner/apiare frozen forever, including values the current server does not populate (BatchJobID,Queue,Regionare kept as the external-handle trio — servers and backends are free to repurpose the values, never the names). - Status vocabulary strings (
queued|running|done|failed|cancelled, steppending|…|skipped, hostspending|active|removed, tasksrunning|stopped|crashed) are frozen. - Job type names
CPUv1,GPUv1,WebserviceV1,TCPServiceV1are frozen; deployments add their own names via config. io/composeSpecfield names are frozen wire format (the struct has no tags; its JSON spelling is its Go spelling).- In-container paths, entrypoint flags, and result-protocol line grammar (§3) are frozen.
- Deprecation = stop emitting + keep parsing. Silent removal is a protocol break and requires a versioned fork, not a field edit.
Anything not pinned here or by a fixture is implementation detail and free to change.