The Server–Runner contract
What a Runner is on the wire, where the authority for this section lies, and the shape of every call.
This section is for somebody implementing a Runner of their own: a program that registers with an AlgoJudge Server, is approved, claims work, and reports results. It is not a description of the Runner that ships — that is Runner.
The accepted specification prevails
This section describes version 1.1 of the contract. It is written here for a reader, and the accepted specification is what a Server is built to: on any divergence, the specification is right and this section is wrong.
It holds for every page in this section, which is why none of them repeats it.
What a Runner is, on the wire
Three properties, and everything else follows from them.
- The Runner initiates every connection. The Server never calls a Runner. There is no callback URL, no inbound port, and nothing to register but a key.
- The Runner is stateless apart from a cache. One that dies mid-evaluation resumes nothing and nobody comes back for that work. The Server's lease is the whole recovery story, which is why it is a correctness mechanism rather than a safety net.
- The Server never parses a problem type. It is matched by string equality and nothing else. Adding a problem type is not a Server change, and a Runner may implement a type the Server has never heard of.
A Runner holds an Ed25519 key pair it generates once. The Server knows it by the fingerprint of that key.
The lifecycle
register approve challenge + token
anonymous ────────> pendingApproval ────────> approved ──────────────> holding a token
│ │ │
│ │ revoke │ claim
▼ ▼ ▼
(refused a token) revoked ──> forget holding a jobA Runner may register at any time and evaluates nothing until an administrator
approves it — runner:approve is a global permission, and no manager template
carries it. A re-registration — which is what every restart is — asks for a
challenge first and signs the nonce; the challenge endpoint does not itself
require approval, so a Runner still waiting to be approved can restart like any
other. Revocation is permanent: there is no rotation, so a leaked key means a
new configuration, a new key and a new registration.
The whole conversation runs in one order, and two parts of it are requirements rather than habits: the lease is renewed on a timer while work is in progress, and anything the evaluation produced is uploaded and attached before the report, which ends the job.
Every call
Everything is JSON over HTTP, under /api/v1.
| Path | Auth | |
|---|---|---|
| Health | GET /api/v1/health | anonymous |
| Register | POST /api/v1/runner/register | anonymous; signed for a fingerprint the Server already knows — see Registration |
| Challenge | POST /api/v1/runner/auth/challenge | anonymous |
| Token | POST /api/v1/runner/auth/token | anonymous |
| Claim a job | POST /api/v1/runner/jobs/claim | bearer |
| Renew a lease | POST /api/v1/runner/jobs/{jobId}/lease | bearer |
| Report progress | POST /api/v1/runner/jobs/{jobId}/progress | bearer |
| Report a result | POST /api/v1/runner/jobs/{jobId}/report | bearer |
| Give a job back | POST /api/v1/runner/jobs/{jobId}/release | bearer |
| Read a file | GET /api/v1/runner/files/{id} | bearer |
| Upload a file | POST /api/v1/runner/files | bearer |
| Attach to an attempt | POST /api/v1/runner/jobs/{jobId}/files | bearer |
| Attach to the Runner | POST /api/v1/runner/files/attach | bearer |
| Heartbeat | POST /api/v1/runner/heartbeat | bearer |
The token goes in Authorization: Bearer. It has a lifetime, and it authorizes
this Runner's own surface and nothing on the panel a person signs in to.
Two conventions that govern every document
Both come from the Server's serializer rather than from taste, and the
implementation is the clearest statement of them —
crates/aj-protocol/src/wire.rs in AlgoJudge-Runner is the reference
implementation of every shape in this section.
- camelCase, everywhere.
- Absent, never
null. An optional value the Server does not have is simply not in the document. A client must treat a missing member as absent rather than expecting an explicitnull, and must not send one either.
Refusals carry a code
An error answer carries a machine-readable code, and the code is what to switch
on. The HTTP status alone is not enough: a 503 may equally come from an edge
proxy that has never heard of this product.
The codes are listed on the page for the call that produces them.
The rest of this section
| Registration and authentication | the key, approval, and the challenge-response handshake |
| Claiming and the lease | how work is taken, and what makes a late report refusable |
| Reporting | results, idempotency, and the failure that is not a verdict |
| Files | transfer, sha256, and attachments |
| Availability | the heartbeat, and a Server that is up and declining to serve |
| What is not specified | including two documents that are history, not standards |
Conformance
The Server carries a conformance suite that exercises the contract against a real database on every pull request. A second implementation should pass the same sequence: register, be refused a token before approval, be approved, authenticate, be refused a replayed nonce, claim, read only its own package, report, resend the report and receive the same result, and lose a job to lease expiry.
Four cases are worth writing even though nothing obliges an implementation to
pass them, because each is a way of being wrong that looks like working: a
released job is queued again at once with its delivery count where it was; a
Runner waiting on an empty queue is handed a submission made afterwards in a
fraction of that wait, rather than when it runs out; a 204 arrives at the
deadline and not before; and a job reported as an infrastructure failure is
queued again rather than failed, until the deliveries run out.
This project is licensed under MIT. See LICENSE. This documentation is CC BY 4.0.