AlgoJudge documentation0.1

Registration and authentication

Announcing a key, waiting for approval, and proving the key on every token.

Registration

POST /api/v1/runner/register

Anonymous.

{
  "name": "runner-01",
  "product": "AlgoJudge-Runner",
  "version": "1.0.0",
  "publicKey": "<32 raw bytes, base64>",
  "problemTypes": ["standard-io@1"],
  "external": false,
  "tags": ["lab-a"],
  "machine": { "os": "Debian 12", "cores": 16 }
}

Answers { runnerId, fingerprint, state }, where state is pendingApproval, approved or revoked.

Member
namewhat the manager panel shows
product, versionwhich implementation this is, and which release
publicKeysee below
problemTypesmatched by string equality, never parsed
externalwhether this Runner sends submissions outside the installation. Optional, defaults to false
tagswhich pools it belongs to. Optional, and a seed — see below
machinehost facts. Stored opaquely and only ever shown; the Server does not read inside it

publicKey is the raw 32 bytes, not an SPKI wrapper

The implementation is the clearer statement here. The Server takes the bytes straight into an Ed25519 public-key parameter object; an SPKI blob is 44 bytes and is refused.

RefusalCode
The key is not 32 bytesrunner.key.length
The key is not base64runner.key.malformed
The key has been revokedrunner.revoked, 409

A useful check on arrival: the Server answers with the fingerprint it computed. Both sides hash the same 32 bytes, so a disagreement means something re-encoded the key in transit — and every signature after that would fail with nothing to explain it.

Registering again is how a restart is reported

A Runner registers on every start. The identity is the key, so this does not produce a second Runner and does not need a second approval; every field above is refreshed — except one.

A registration for a key the Server already knows must be signed

It carries two more fields, nonce and signature, obtained and computed exactly as the handshake below does it: a nonce from auth/challenge, Ed25519 over its UTF-8 bytes, base64, spent once.

A first registration cannot be signed and does not carry them — auth/challenge answers 404 for a fingerprint it has never seen, which is how an implementation tells the two apart. A Runner that has registered and is still waiting to be approved can ask for a nonce; the challenge endpoint does not require approval, and such a Runner registers again on every restart.

Without this, the endpoint's anonymity extended to keys the Server already knew. A public key is public by construction and the manager's surface hands it to anybody who may list Runners, so whoever had one could rewrite the row it names — problemTypes and external, two of the filters the claim pairs work on, and the name, product, version and machine a person reads when deciding whether to approve it — with the approval left exactly where it was.

The nonce is signed, not the request. What is being established is only that the private key is present on the machine that is asking. Signing the body would need a canonical serialization every implementation agrees on for ever, and the first day two of them disagreed no Runner could restart.

tags is read on the first registration and never again. From then on it belongs to the operator, who sets it on the manager's surface. It exists so a room of machines can be deployed from one configuration rather than tagged one at a time, and it stops there: a Runner that could re-declare its tags on restart would move itself into an examination's pool with nobody having approved that. Approval — the trust gate — happened before the tags existed.

tags is normalised: trimmed, lowercased, de-duplicated. Absent means the default pool. A Server holds a Runner to sixteen tags of sixty-four characters.

The Runner does not report its own address

The Server reads it from the connection. A machine is a poor witness to how it is reached — it sees a private interface or a container's address, not what the Server actually talked to.

This makes forwarded-header configuration load-bearing rather than tidy: behind a reverse proxy without it, every Runner is recorded as coming from the proxy.

Approval

Approval happens on the ordinary session-authenticated surface:

POST /api/v1/runners/{id}/approve

It is an administrator's act. runner:approve is one of four global runner:* permissions and none of them is in the shipped manager template, so running an activity does not admit a machine to the installation.

That is not a Runner call and needs no place in a Runner implementation. What a Runner needs to handle is the waiting.

Waiting is not a fault, and there is no timeout. A Runner that has registered and not been approved is refused a token, and the right response is to back off and ask again — for as long as it takes somebody to reach the panel. A process that exited because nobody had got to it yet would need something else watching it.

There is no key rotation, and revocation is permanent

A revoked key is refused at registration with runner.revoked and never comes back. An implementation should treat that refusal as terminal rather than retrying: coming back means a new key, a new registration and a new approval, which are decisions for a person.

Authentication

Two calls, both anonymous, because a Runner has no session.

POST /api/v1/runner/auth/challenge      { "fingerprint": "…" }
   → { "nonce": "…", "expiresAt": "…" }

POST /api/v1/runner/auth/token          { "fingerprint": "…", "nonce": "…", "signature": "…" }
   → { "token": "…", "expiresAt": "…" }

The signature is Ed25519 over the UTF-8 bytes of the nonce, base64.

The nonce is single-use and short-lived — two minutes in this Server — and both properties matter: without single use, a captured exchange is replayable for ever; without expiry, one captured today is usable next year.

RefusalCode
No such nonce, or already spentrunner.nonce.unknown
The nonce has expiredrunner.nonce.expired
The nonce belongs to another Runnerrunner.nonce.mismatch
The signature does not verifyrunner.signature
Registered but not yet approved, or revokedrunner.notApproved

Note the last row. Revocation is refused at registration, not here, so a Runner revoked while it held a token sees runner.notApproved on its next call — approval is checked on every call, not only at the handshake, which is what makes revoking a Runner stop the one that is already working.

Losing a token is ordinary

Tokens live in the Server's memory and last twelve hours. So a 401 reaches a Runner that was working perfectly two ways — an ordinary Server restart, and simply running for a day — and the correct response to both is to forget the token and shake hands again, not to exit and not to re-register as something new.

An implementation that treats a 401 as fatal will restart itself every time the Server is updated.

On this page