The API - andreaderuvo/argus GitHub Wiki

The API

The browser is one client of this API, not a privileged one: anything it does, a script or an agent can do with a key. Every route โ€” its method, path, parameters, what it is for, and whether an agent's key opens it โ€” is in API reference, generated from the code. This page is about the ideas behind the parts people build on.

The workspace

GET /api/prefs is everything 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 may make a desk or rename one with POST /api/desks (below), and nothing more: it cannot move, close or rearrange what you have.

The Argus plugin

GET /api/plugin โ†’ {version, agents: [{agent, name, present, installed, outdated}], reload: [sessions], behind: [{session, agent, version, at_prompt}]}: the version this checkout offers, what each agent has, the Claude sessions at their prompt, and every session still running an older plugin. GET /api/plugin/version โ†’ {version}, what the plugin's SessionStart hook asks. POST /api/plugin {agent: claude|codex, action: install|update} runs the agent's own plugin commands (this checkout as the marketplace when it has none). POST /api/plugin/reload types /reload-plugins into the Claude sessions whose last word was done โ€” or only {sessions: [...]} of them โ€” โ†’ {reloaded, failed, not_now}. POST /api/plugin/seen {session, version} is how a session says which plugin it runs. Not open to an agent's key.

Desks

POST /api/desks with {name, folder?, show?, session?} finds the desk of that name (any case) or makes an empty one. folder is where its browsers and new sessions start. show (true by default) asks the open pages to switch to it. session also puts a window on that existing session in the desk (404 if there is none). {rename: "Desk 11", name: "Pippo"} renames one instead (show false by default): 404 if there is no such desk, 409 if the new name is taken. The answer is {id, name, made, folder, session?}. Nothing here removes a desk or what is in it, which is why an agent key may call it.

Labels on processes

The note you can write on a process from the System screen.

GET /api/labels {labels: [{pid, label}]} โ€” only processes still running
POST /api/labels {pid, label} โ†’ {pid, label}; an empty label clears it. 404 if no such process

/api/system (its processes) and /api/ports (its ports) carry label on every row โ€” always present, "" when there is none โ€” and each process row also has its pid and full command. A label is keyed by pid and start time, so it never lands on a recycled pid, and it is gone once its process has ended. None of the three is an agent route: an agent's token gets 403 on all of them.

Working or waiting

GET /api/tmux/states โ†’ {states: {session: {agent, state, since, plugin?, plugin_old?}}, plugin_offered}, for every session with an agent in it: state is working or waiting, since when it last changed; plugin the Argus plugin version the session said it runs. The same fields (agent, state, state_since) ride on /api/tmux/sessions, and state/state_since on /api/who. Worked out from the pane when the agent has no hooks; an agent whose hooks tell Argus (start, done, asking) is believed over its pane. A session appears a few seconds after the first look, once there are readings enough to tell. seen is true for a wait set aside.

POST /api/tmux/seen {sessions: [...]} sets waits aside ("Got it"): each agent that is waiting stops asking until it has worked and stopped again. Answers the new states.

POST /api/bell {why, text?, session?} โ€” why is done, asking, failed, note or start (a turn began; rings nothing) โ€” takes an optional plugin (the version the hook belongs to) and an optional conversation (a UUID): the conversation the agent is in, which is how a Claude Code hook tells Argus what to resume after a reboot. A second done or asking about a turn that has already rung is answered with that bell and repeat: true, and rings nothing.

Options when starting an agent

GET /api/launchers?versions=1 gives each agent launcher agent and options: what the box offers, filtered by that agent's own --help. POST /api/tmux/launch takes options, by name, e.g. {"permissions": "skip", "model": "opus"}; the server builds the flags and answers 400 for anything not on offer. The answer's command is the line that was run.

An agent's key gets 403 for a choice marked as dangerous, such as --dangerously-skip-permissions. An agent may start another agent, not one that asks nobody before acting. That choice is yours, from the New session box.

Teams

GET /api/teams โ†’ {teams, templates, roles, conditions}; each team carries its nodes (with state and outcome), edges and start. GET /api/teams/suggest?path=&goal= โ†’ the template a goal reads like and the checks a folder offers; POST /api/teams with {name, goal, template, graph?, path, agents: {step: {launcher, options, worktree}}, check, checks: {step: command}, gate, max_rounds, ws} starts the agents and the team. graph is {nodes: [{id, kind: agent|check|join|end, role, judge, duty, of, command}], edges: [{from, to, when}], start: [ids]}; left out, the template's is used; POST /api/teams/{id}/go|pause|stop (stop with {kill: true} also ends the team's sessions โ†’ {team, ended}); GET /api/teams/{id}/reset โ†’ {folder, files, run, log, declared, file}, what a reset would do, resolved; POST /api/teams/{id}/reset (a stopped team; 409 otherwise) โ†’ {team, deleted, log, ran}; POST /api/teams/{id}/restart โ†’ the new start, with reset, ended and tidied ({desk: [windows closed]} โ€” its desk's windows of ended sessions) โ€” same id, the team file read again; DELETE /api/teams/{id} forgets a finished one. The config's or a device's token, and --allow-write; never an agent's key by itself (it may request it).

A team as text, both ways: POST /api/teams/yaml and POST /api/teams/mermaid take {text} and answer {graph, โ€ฆ} (or 400 naming the line; with preview: true, 200 {error} โ€” for checking while typing), and take {graph} to answer {text}. The Mermaid one also takes base, the team being edited, whose duties and worktrees a flowchart cannot say and are kept.

Every problem, on its line (what the team editor asks while you type): POST /api/teams/lint {text, format?, base?} โ†’ always 200, {format, ok, problems, where, graph?, name?, goal?, gate?, rounds?, permissions?, summary?}. A problem is {line, col, end_line, end_col, level: error|warning, message, fix?} โ€” lines from 1, columns from 0 โ€” and a fix {label, line, col, end_line, end_col, text}: put text over that range. where is each step's line. GET /api/teams/vocab โ†’ the keys, values, roles and conditions a team file may use, from the schema, for completion. POST /api/teams/convert {text, to: yaml|mermaid, base?} โ†’ {text}: one written form to the other, base (the YAML being edited) supplying what a flowchart cannot say. POST /api/teams/save {text, folder, replace?} writes the team as <folder>/team.yaml (--allow-write), keeping the duties and settings of the one it replaces; with one there and no replace, 200 {exists: true, file}.

For the agents in a team (the agent key opens these): GET /api/teams/task?session= โ†’ {team, goal, you, role, judge, duty, your_turn, round, max_rounds, folder, log, before_you, reads, last_check, statuses}, or {none, why} outside a team; POST /api/teams/done {session, summary, status?, details?} reports a turn and answers {recorded, team, you, round, status} โ€” or 409 with the reason (not its turn, already reported, a judge without a verdict); GET /api/teams/expecting?session= โ†’ {expecting, reason}, true while the agent in that session has a turn it has not reported, at most twice per turn: the plugin's Stop guard.

What an agent requests, and you approve (agent key): GET /api/agent/actions โ†’ {actions: [{action, what, asks, danger}]}; POST /api/agent/request {action, args, why?, session?, wait?} โ†’ {id, state: asked|doing|done|refused|failed|unanswered, text, question?, result?, error?} โ€” 400 with the reason when it cannot be done as asked (nothing is asked then); otherwise an ask with Do it / No (question is its id), and on Do it the action through the app's own routes with the full key. GET /api/agent/request/{id}?wait= comes back for it. Settings โ†’ Agents (agentsWithoutAsking in the preferences) and agents_without_asking in the config list actions done at once (a team started with everything needs start_agent listed too). POST /api/desks also takes session: a window on that existing session in the desk.

For an agent designing a team (agent key too): POST /api/teams/check {text} (Mermaid or YAML) โ†’ always 200, {ok: true, format, name, goal?, summary, graph, problems} or {ok: false, error, problems} (the lint's problems); POST /api/teams/propose {text, folder, session?} writes <folder>/team.yaml (first line # proposed by <session> through Argus) and rings with team_proposal: {folder, name, file} on the bell โ€” 400 not a team, 403 outside the roots or a read-only server, 409 when the folder's team.yaml is the person's own. Neither starts anything. A proposal is listed in GET /api/teams as proposals: [{folder, file, name, by, at, summary, graph, goal?}] until a team starts in that folder, DELETE /api/teams/proposals?folder= dismisses it, or its file stops being a proposal. See Teams.

After a reboot

GET /api/resume โ†’ {lost: [{name, agent, cwd, conversation, how, command, lost_at}]}: the sessions that held an agent when their tmux server went away. how is resume or fresh, and command the line it will run. POST /api/resume {names: [...]} brings them back (only names on that list, with the command the server recorded), and POST /api/resume/forget {names} stops offering them.

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 them all, 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: a token on every request, 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 token in the config opens everything. A device token, a watcher token or an agent key opens less: see Security.

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.

Settings โ†’ Devices โ†’ Add 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. desk can also be a desk's name, such as "desk": "pippo". Then it lands on that desk, which is made if it is not there yet. It reaches only pages open at that moment; nothing is queued.

Remembering a browser

/api/remember keeps a browser signed in when its own storage is lost. It is answered by the token gate itself, so it is not in openapi.json.

POST /api/remember with a working token: sets an HttpOnly cookie holding it. 401 for a token that does not work
GET /api/remember {token} if the cookie still opens this server, else {token: null} (200, not 401)
DELETE /api/remember forgets it, as signing out does

Only the config token or a device token is kept, never a watcher's or an agent's. See Security.

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.

Old copies of the commands. GET /api/version carries old_tools: [{name, path, why, fix}], each copy of argus-say & co. found on the PATH that is not today's (a link into the checkout, the installer's launcher and an identical copy are fine). POST /api/tools/link replaces exactly those with links (--allow-write), keeping the old files beside them โ†’ {linked}.

โš ๏ธ **GitHub.com Fallback** โš ๏ธ