Skip to content

The API

Andrea de Ruvo edited this page Aug 20, 2026 · 5 revisions

The API

The workspace

GET /api/prefs is the sixty keys the browser used to keep to itself: the desks, where every window sits, the prompt library, the placeholder sets, the shortcuts, which viewer each extension gets. It moved because of two things people ask for — a desk made at the desk should exist on the phone, and something that is not a browser should be able to read the workspace: an agent that has just started three jobs could lay out a desk to watch them in, and a script that sends prompts should be able to read the library they come from.

GET /api/prefs {version, prefs}
PATCH /api/prefs {changes: {key: value}} — merges those keys and leaves the rest; a null removes one
PUT /api/prefs {version, prefs} — the whole document, refused with 409 if the version has moved

Why a merge and not a replacement. Last-write-wins on the whole document loses the desk made on the phone the moment a laptop saves an older copy of everything, which is exactly the case this exists for. The browser therefore sends only the keys it has touched since it loaded. Two devices editing different things both keep their change; the same key still resolves to whoever wrote last, and there is no honest way around that without asking a person which they meant.

Agents may read it and not write it. The prompt library is worth reading; an agent quietly rearranging your windows is a different proposition, and one line away if it is ever wanted.

Swagger, on your own machine

Settings → Go to → **The API, live**, or /api/docs with your token. Every route with its shape, a box to find one among fifty, and Try it out that really calls it — carrying the token the page was opened with, so it answers rather than 401s.

Two decisions in it. It is behind the token, under /api/ like everything else, because the route list of a server that holds a shell is not a thing to publish. And the bundle is vendored, like xterm and pdf.js: FastAPI will serve this in one line and that line fetches its JavaScript from a CDN, which is no use on the machines Argus usually lives on. The token never appears on the page — it travels in a header, not in the spec's address, so a screenshot of this page is not a leak.

For an agent rather than a person, see An agent driving Argus.

Everything Argus does, it does through its API — the browser is one client of it, not a privileged one. That is the extension story: a script, a cron job, an agent hook or another machine can do anything the app can, with no plugin to install and nothing running inside the page.

The routes, described — or openapi.json for Swagger UI, Postman or a client generator. A running server serves its own at /api/openapi.json, behind the token like everything else.

Two rules hold throughout: one token, in an Authorization: Bearer … header or a ?token= query where a header cannot go; and everything under /api, the description included, so a single rule guards the lot.

The page is generated from the routes themselves, and a test fails when it drifts.

A token per device

One token means losing a phone costs you every device: the only remedy is rotating the one secret they all share. So each device can have its own.

SettingsDevicesAdd a device, give it a name, and you get a link and a QR code once. Only a hash is kept — the same bargain GitHub makes for a personal access token, and for the same reason: losing the file must not hand anyone a working key.

  • Revoking one leaves the others alone, and takes effect on that device's next request. Nothing to restart: the list is re-read every time a token is presented, which is the only reason revocation is a feature rather than a paragraph.
  • A device may do the work and may not manage devices. Only the token in the config can add or revoke. A phone that is lost cannot mint itself a second key, and cannot lock you out of your own machine.
  • Renaming does not sign anything out. "phone" becomes "old phone" the day a new one arrives, and a list you cannot correct is one you stop trusting when it matters.
  • The list says when each was last used, written at most once a minute — the useful answer is "yesterday or today", not which second.

There is deliberately no bcrypt or argon2 on those hashes. Slow hashes exist to make guessable secrets expensive to attack; these are 256 bits of randomness, and a slow hash would only make every request slower.

The journal

Journal, in the sidebar. It answers one question — has somebody been in here — and it is built around that rather than around a table.

What goes in: everything that changed something, and everything that was refused, whatever method it used. What does not: successful reads. Listing a folder every four seconds while you scroll is most of the traffic and none of the interesting part, and a file full of it is a file nobody opens.

Each line has the action, which key did it (the config token, a device by name, or a board), the address it came from, the status and the time. Behind a reverse proxy the peer is always loopback and the real client is in a header, so both are kept and shown apart: a header is whatever the sender wrote, and reading a log without knowing which of the two you are looking at is worse than seeing one.

Refusals from one address are collapsed — a scanner can produce thousands a minute, and a journal that rotates its own history away while being flooded is worse than none — but the count is kept, so twelve knocks read as twelve.

Filters at the top: Everything, Refused, Changes, and a box that matches the address, the key and the action at once. Only the token from the config can read it: a record a stolen device could read is a record that tells whoever took it what you can see.

Emptying it is deliberate and says how much went: everything, or only what is older than a day, two days or a week. Nothing empties itself on a schedule — the whole value of the file is that it is still there when you finally think to look.

It is not tamper-proof, and does not pretend to be. Anything holding the master token can delete the file, and a shell on the machine certainly can. It answers "what happened" for a person looking back, which is the question that actually comes up.

Orchestrations

GET /api/runs · POST /api/runs

A noticeboard, and only that: what the machine is orchestrating right now, so a browser can draw it and a board can count it. Held in memory, sixteen at a time, dropping finished ones before running ones — a cap that drops by age alone throws away the run you are watching in favour of one that ended twenty minutes ago.

Nothing here starts, stops or reaches an agent. Posting to it changes nothing about the work; the orchestration goes on unchanged with every browser shut, which is why an agent key may write to it.

A run that claims to be running and has not been heard from in five minutes reads as gone rather than running. The framework beats once a minute while it waits, so the silence means the watcher stopped — not the agents, which nothing here ever reached.

It also appears, as a count rather than a graph, under runs in GET /api/overview, so a board sweeping ten machines learns that one of them is orchestrating without asking twice. The key is absent entirely when nothing is running.

POST /api/tmux/launch takes one more field for the same reason: desk: true tells whoever has the app open that a session started, so it lands on their desk instead of waiting to be found in a list. It reaches only pages open at that moment; nothing is queued.

Where it is

GET /api/network

The hostname, the user, every address this machine answers on with the routed one first, the address the request came from, and the ssh line for reaching a loopback port. Read entirely from this machine and this request: no network call is made, ever.

POST /api/network/outside

The only thing in Argus that tells somebody else anything, and a POST for that reason — it is an action with a consequence rather than a reading. It asks one of two public services what this machine's address looks like from outside, and asking that is telling them. Nothing does it on a timer or on anybody's behalf, the answer is not written down, and ask_outside: false refuses it outright.

Clone this wiki locally