Skip to content

How an agent works

The agent calls outwards, opens no port, and runs only what carries a valid Runjet signature.

Runjet never runs your command. It schedules it, writes down that it should happen, and hands it to an agent — a process on your machine, which runs it and reports how it went. The control plane's image does not even have a shell, so this is a property rather than a rule.

Runjetdb-backup 03:00your hostagentsh -c …waiting for worksealed dispatchreport and output
A cron tick becomes a row. The agent comes for it on a held call, starts the process where it is, and sends a report back.

It calls outwards

The agent listens on no port. It opens an HTTPS connection to RUNJET_URL itself and asks whether there is work. That is why it works behind NAT, behind a firewall that only lets traffic out, and on a machine with no public address — and why Runjet does not need to know your host's address. The only thing that has to be open is the route out to api.runjet.dev.

The poll is a held call, not a loop

Runjet holds the request open until something appears or until AGENT_POLL_WAIT runs out (30 s by default). When a cron tick creates a run for this agent, the response leaves immediately — the latency from tick to the process starting is under a second, and yet the agent sends two requests a minute when nothing is happening. After a failed request it waits five seconds and tries again; neither a restart of Runjet nor a network outage stops it, they only slow it down.

Two keys

Each side verifies the other, and neither needs TLS to do it.

  • The workspace key is an Ed25519 pair whose public half the agent was given at enrolment in RUNJET_PUBLIC_KEYS. Every dispatch is sealed with it: the agent verifies the signature before it even unpacks the response, and what it cannot verify it refuses to run — it does not happen and it is not reported as a failure. There is one key for the whole workspace; an agent enrolled in yours refuses a dispatch signed by any other.
  • The agent key is created at enrolment on the host and its private half never leaves it — Runjet is given only the public one. Every request from the agent carries a signature over the method, the path, the body, the time and a nonce, so an intercepted request cannot be replayed and nobody impersonates the agent without its key.

TLS is required all the same, not for authenticity but for privacy: commands and their output carry secrets, and a signature does not hide them.

What is in a dispatch

One dispatch is one run: the identifiers of the run and the job, the job's name, the command, the names of its secrets (never the values — only the agent has those, see secrets), a time limit, when it was issued and when it expires. An expired dispatch is refused. All of it is under the signature, so an agent cannot extend its own limit or change the command.

The same response may also carry a cancellation, sealed the same way — being asked to stop looks innocent beside being asked to start, but an unverified instruction to stop is a way to silence somebody's scheduled work from outside. The agent handles a cancellation before any new start, so that an ending does not wait on the slot it is meant to release.

And ceilings arrive: how many runs the agent may hold at once and how long a request should wait. They are ceilings, not orders — an agent may take fewer, never more — and they are covered in configuration.

Reporting back

Before the agent runs anything it reports the run as running; if Runjet refused that, the run would not start at all. Only then does it fill in the secrets — so a missing value is reported as a failed run rather than left for a sweep that would blame a silent agent.

While the run goes, the agent streams the output in pieces, each first passing through masking, and every few seconds sends a heartbeat that holds its lease on the run. At the end it sends the verdict: success, or failure with an exit status and a sentence saying why.

Shutting the agent down mid-run kills the process and reports the result anyway — the reporting goes through a context detached from the loop that is ending. An agent that dies without reporting loses its lease and Runjet closes the run as orphaned; what that means is among the run states.

The protocol version

The agent says which version of the protocol it speaks in every request, and Runjet serves a window of versions rather than a single number. What that means for upgrades, and what you will see when an agent falls out of the window, is on the upgrading and versions page.