Skip to main content
The CLI and the web UI are both clients of this API, so anything either of them does, you can do over HTTP.

Endpoint summary

Sixty-three operations across 59 paths. The set is generated from the running application, so it is complete for the default all role; other roles serve a subset.

Conventions

Base URL

Every path above is relative to the origin XORCISE is serving on, plus /api. Do not hard-code a port: the REST plane starts at 3001 and walks up to 50 ports to find a free one, so a second instance, or a busy port, moves it. xorcise up prints the URL it took, xorcise status prints it on the REST API row, and xorcise ui prints the same origin with /ui appended. The examples on this page take the base URL from xorcise ui and strip the /ui suffix:

Authentication

The operator API has no authentication. No API key, no session, no token, on any of the agent, mission, run, result, configuration, system, or filesystem endpoints. In the default local deployment topology XORCISE binds loopback plus, on Linux, the Docker bridge gateway — so every local process and every container on that bridge can read your runs and traces, change your judge model, start runs, and delete results. Set XORCISE_HOST=0.0.0.0 and the network can do the same. Reachability is the access control. Security and isolation explains why the bind is what it is and what to do about it. The one exception is the eight run-control endpoints your agent calls. Those authenticate per run with a bearer token, and they authorize nothing else — a run’s token opens that run and no other. The liveness probe the join script polls, GET /api/runs/{run_id}/alive, takes a narrower watch token instead.

The run-control token

POST /api/runs mints a 32-character hexadecimal token and returns it as run_control_key in the 201 body. That is the only response that carries it as a field. It also reaches your agent in two places: the prompt from GET /api/runs/{run_id}/prompt carries it inline, and the shell_block from GET /api/runs/{run_id}/launch-profile exports it as XORCISE_RUN_BEARER. GET /api/runs does not carry it.
Two failures, both 401:
The first means the Authorization header was absent or was not a Bearer credential. The second means the token does not belong to that run. Keep the token off command lines. Any user on the machine can read a process’s arguments with ps, so curl -H "Authorization: Bearer …" exposes the token for as long as the request runs. Pass the header to curl on stdin instead:
The examples below use -H for brevity.

Run ids

The REST API matches run ids exactly and takes the full 32 characters. xorcise run status 4c62254b works because the CLI expands a unique prefix before it calls the API; GET /api/runs/4c62254b/result returns 404.

Errors

Every error the API raises on purpose carries FastAPI’s default shape, a single string under detail:
Those strings are written to be read by a person and are what the web UI displays verbatim, so surface them rather than replacing them with your own message. A 422 is the exception and carries a list instead, one entry per invalid field:
Anything that needs a runner — creating a run, and pulling, updating or ingesting a mission — answers 503 on a server whose role has no runner, such as the experimental control role: this server has no runner to <action>: role '<role>' does not run Docker or Headscale itself, and a remote runner is not supported yet. Use role 'all' ('xorcise up'). Only all and runner own Docker and Headscale. Two answers do not follow that shape. A 409 refusing a delete carries reason, run_ids, and forceable beside detail, as Delete a run describes. And an exception the API does not map to a status, such as SQLite reporting database is locked on POST /api/runs, is not JSON at all: it is a 500 whose body is the plain text Internal Server Error, with the cause in the server’s log rather than in the response. Every endpoint that takes a body or a typed parameter can return 422. It is omitted from the per-endpoint status tables below except where a specific value, such as an unsupported report format, is rejected that way.

The 202 grading window

Grading runs after a run ends, not during the request that ends it, so there is a window in which a run is finished but has no score. Four endpoints share one state ladder: GET /result, GET /report, and GET /stats all return 202 this way, and POST /regrade returns 202 on success. Poll on 202 — it is a normal transient state, not a failure. Polling also re-drives grading, de-duplicated, so a grade lost to a restart heals on the next poll instead of hanging at grading forever. A run that ended before its agent started, or was cut short by an environment failure, is never graded, so those three endpoints answer it with a final 200 rather than a 202 that would never resolve. Branch on status before reading a score: "not_graded" is an answer, and a poll loop should stop on it. POST /regrade refuses such a run with 409.
run_status is the run’s status wording, the same status object every run record carries. detail is the reason recorded when the run ended. For a deploy_failed run the readiness gate closed, it is the gate’s verdict and log tails. For a deploy_failed run whose deploy never finished, it is one line naming why — the create failed before this run's deploy finished (…) or the server stopped before this run's deploy finished, each followed by ; its environment is torn down automatically. For crashed, cancelled and never_started, it is null, except on a run the upgrade to 0.2.1 reclassified as cancelled or never_started, where it says what the run ended as before and which grade the upgrade removed.

Agents

GET /api/agents

Synopsis
Description. Lists every registered agent. An agent is a record you declare, not a process XORCISE launches, so this endpoint reflects only what you have told XORCISE. Parameters. None. Request. No body. Response. 200 with an array of agent records. Each carries id, name, kind, created_at, version, and the optional declared fields endpoint, model, otel, launch_command_template, launch_mode, launch_tips, and mission_preamble. version starts at 1 and increments on every update. otel is stored and read by nothing — the OTLP endpoint your agent actually uses comes from GET /api/runs/{run_id}/launch-profile. kind is always a string, never null, here and in the POST and PUT responses. An agent registered without one reads "kind": "generic". This is a breaking change: earlier releases returned null for such an agent, so a client that tests kind for null must test for generic instead. xorcise db upgrade rewrites every stored agent whose kind is missing or blank to generic. Example

POST /api/agents

Synopsis
Description. Registers an agent under a unique name. kind is the one field that changes behaviour: it selects the replay adapter, the telemetry provider, and the launch provider together. The built-in values are openhands, claude-code, and codex. An omitted or blank kind is stored as generic. Every kind is stored without surrounding whitespace. A kind that names no registered harness is still saved, and the response warns about it. Parameters. None. Request. name is required. endpoint, kind, model, and otel are optional strings. launch_command_template, launch_mode, launch_tips, and mission_preamble are optional launch overrides. The four launch fields are optional per-agent overrides of the harness defaults served by GET /api/runs/{run_id}/launch-profile and GET /api/runs/{run_id}/prompt. null inherits the harness default.
Response. Example
warnings lists what you should know about the saved record, and never blocks the save. Today the only warning is a kind no harness answers to:
The warning says “may” because the replay adapter is chosen by the run’s kind first. Failing that, if exactly one harness’s slug appears as a span’s instrumentation scope or name, that harness’s adapter is used, and only then generic. A mislabelled run of a real harness can still replay with that harness’s adapter. GET /api/agents never includes warnings.

PUT /api/agents/{name}

Synopsis
Description. Replaces an agent’s declaration and increments its version. Sending a different name renames the agent, keeping its id, its version history, and its runs. Results already recorded keep the version they were scored under, which is what lets you compare an agent against its earlier self. Parameters. Request. The same shape as POST /api/agents, including the launch overrides. name is required and may differ from the path parameter. The body replaces the whole declaration, so a body without kind sets it to generic and a body without the launch overrides clears them.
Response. Example

DELETE /api/agents/{name}

Synopsis
Description. Removes an agent together with every version and run belonging to it, and everything recorded for those runs — what DELETE /api/runs/{run_id} removes for one. This cascade is deliberate: a result is meaningless without the agent record that names what produced it. Deleting a mission behaves differently and keeps its runs. Parameters. Request. No body. Response. The cascade applies the same rules as deleting one run to every run the agent has. It is refused while any of them has not finished or is being graded — it no longer deletes live runs — and each finished run whose environment is still up is torn down first, one after another, before anything is removed. The 409 body is the same, with detail prefixed cannot remove agent '<name>': . Example

GET /api/agents/{name}/history

Synopsis
Description. Returns every result recorded for an agent, oldest first, so you can watch a score move as the agent changes. intel_disclosed is filled at read time from what the agent actually requested during each run. Parameters. Request. No body. Response. 200 with an array of history entries carrying run_id, agent_id, created_at, overall, deterministic, judge, judge_status, partial, partial_trigger, trace_ref, a conditions object, and the run’s status wording. 404 when no agent has that name. judge_status is the recorded grade’s, as on the result, so a 0 in judge can be told apart from a judge that never ran (model-not-configured or unavailable). It is null when the stored grade cannot be read back. Example

Missions and the library

GET /api/missions

Synopsis
Description. Returns one merged list of the missions installed on this machine and the missions the XORCISE library offers, distinguished by source. The library needs no account, key, or sign-in; when it is disconnected or unreachable you get the installed set only. Parameters. None. Request. No body. Response. 200 with an array of catalog entries: source (your_own or library), mission_id, name, summary, proficiency, specialty, type (lab or static), skills, technologies, installed, and image. A static mission has no environment and therefore no image. Each entry also carries these fields, which are null (or [] for platforms) wherever XORCISE has nothing to report, such as for a mission you ingested yourself or a catalog that does not serve them: Example

GET /api/missions/{mission_id}/manifest

Synopsis
Description. Returns the complete mission.json for a mission — metadata, environment, attachments, artifacts, checks, rubric, intel, and terrain. XORCISE prefers the installed copy and falls back to the library’s. The rubric and the checks are visible here, so treat this endpoint as spoiler territory for anyone about to attempt the mission. Mission manifest documents every field. Parameters. Request. No body. Response. 200 with the manifest. 404 when the mission is neither installed nor in the library. Example

GET /api/missions/{mission_id}/terrain

Synopsis
Description. The mission’s terrain map projected with no run attached — the same base graph a run of it starts from, every element in its base state and no live updates applied. This is what the mission detail page draws, so the map you study before pulling is the graph a run actually begins from, not a simplification of it. Resolves against your installed copy when you have one, otherwise the library entry. Parameters. Request. No body. Response. Example

POST /api/missions/{mission_id}/update

Synopsis
Description. Updates an installed library mission to the artifact the catalog currently serves, re-pulling it in place. Synchronous: it answers when the swap is done, or immediately when the install already matches. That covers both reasons an artifact moves — a new mission version from its creator, or a re-fuse onto a newer base — because the operation is the same re-pull either way. The swap is atomic. A pull that fails replaces nothing, and the previous install stays byte-for-byte intact. Layers shared with the previous release are already on disk, so an update usually transfers far less than the first pull. updated: false means the install already matched the catalog’s current artifact — compared by index digest first — and nothing was touched. Parameters. Request. No body. Response. Example

DELETE /api/missions/{mission_id}

Synopsis
Description. Uninstalls a mission. Runs and results that reference it survive. This is the opposite of the agent cascade. Without purge, the fused image also stays in Docker, so a re-pull or re-ingest is cheap, and POST /api/missions/prune removes it later. With purge=true, XORCISE also removes the image, under the same rules as prune. The image stays when it is not a XORCISE mission image, or when another install, an unfinished run, or a container still uses it. The mission is uninstalled whatever happens to its image. Parameters. Request. No body. Response. The 200 body has four fields. mission_id is the mission you uninstalled. image is the image its install used, and is null for a static mission. detail is a sentence for a person, such as kept <image>: used by an active run. image_outcome is one of these values: Example

POST /api/missions/prune

Synopsis
Description. Finds the local mission images that nothing uses any more and, with apply: true, removes them. Without a body, or with apply: false, it is a dry run that removes nothing. xorcise mission prune calls this endpoint, and xorcise mission prune documents what counts as a XORCISE mission image. Prune never removes the image of an installed mission, of a run that has not finished, of any container, or of a mission that is being pulled. It never touches an image that is not a XORCISE mission image. Removal is never forced. If Docker refuses to remove an image, prune lists it under failed and still removes the rest. A mission counts as being pulled only while it has a pull job, which is how POST /api/missions/{mission_id}/pull-jobs pulls. POST /api/missions/{mission_id}/pull and the automatic pull inside POST /api/runs run without one, so a prune at that moment can remove the image they just downloaded before their install record names it. Repair that mission with xorcise mission pull <id> --refresh. Parameters. None. Request. Optional. apply defaults to false. image_ids limits the removal to the images you confirmed from a dry run. Prune still plans again, and removes only the confirmed images that are still candidates. Without image_ids, it removes every candidate it finds.
Response. The report has six fields:
  • applied is true when the request set apply: true.
  • candidates lists every image prune found, whether or not it was removed. With image_ids, it lists only the confirmed images that are still candidates. Each entry has image_id, tags, slug, size_bytes, and reason. reason is superseded when the image’s mission is installed under a different image, and not installed when its mission is not installed. An untagged image has tags: [].
  • removed lists the ids of the images prune removed. It is empty on a dry run.
  • failed lists each image prune could not remove, with image_id, tags, and error. It is empty on a dry run.
  • skipped lists the ids in image_ids that are not candidates now, because something uses them, they are gone, or they never were one. Prune left them alone. It is empty without image_ids.
  • reclaimable_bytes is the sum of the candidates’ size_bytes. It is an upper bound, because images built on the same base share layers, and a shared layer stays on disk while any image still uses it.
Example
To remove the candidates you reviewed, send apply: true with their ids:

POST /api/missions/ingest

Synopsis
Description. Starts ingesting a mission bundle from a directory on the machine running XORCISE, not from the machine making the request. Ingest validates the manifest, builds the fused image, and installs the result, which takes minutes, so it returns a job id immediately and does the work in the background. Validation happens only here — there is no separate validate endpoint. Parameters. None. Request. bundle_dir is required and is an absolute path on the XORCISE host.
Response. 202 with {"job_id": "<job_id>"}. Poll GET /api/missions/ingest/{job_id} for the outcome. A malformed body is 422; a bad manifest is not rejected here but surfaces as an error job status. Example

GET /api/missions/ingest/{job_id}

Synopsis
Description. Reports an ingest job’s status and streams its build log. since is a line cursor: pass the number of lines you have already consumed and you get only what is new, which is how the CLI and the web UI tail a build without re-reading it. Parameters. Request. No body. Response. 200 with job_id, status (building, installed, or error), logs, and, once installed, slug and image. On failure detail carries the reason, and a manifest rejection reads as invalid mission.json — most often an unknown key, since every manifest model forbids extras. 404 when the job id is unknown. Example

POST /api/missions/{mission_id}/pull-jobs

Synopsis
Description. Starts a background pull of a library mission onto this machine. Calling it while a pull for the same mission is already running joins that job rather than starting a second one, so it is safe to retry. This is the pull path both the CLI and the web UI use. An install always asks the registry for the image, even when the image is already local, so the job ends in error when the registry cannot be reached. Docker downloads only the layers it does not already have. Parameters. Request. Optional. refresh defaults to false. Without it, the job installs a mission that is not installed, and leaves an installed one as it is.
With refresh: true, the job repairs an installed mission. It removes the installed image, never forced, then pulls the library’s current image and reinstalls. It does not reuse the image the install recorded, so if the library has moved on, the repair also lands on its current release. On a mission that is not installed, refresh changes nothing. The job ends in error, with nothing removed, when another tag names the same image, when the registry does not answer for the image it will pull, or when a container, running or stopped, still uses the image. It does not check for a run that has been created but has no container yet, so a repair at that moment can remove the image that run is about to start. If the pull fails after the removal, detail says the image is already gone. A repair cancelled after the removal also leaves the mission without an image until a repair succeeds. A refresh never replaces one of your own missions. The job ends in error instead. A refresh that arrives while a pull for the same mission is already running joins that pull, and joined is true. The running job does not become a refresh. Response. 202 with job_id and joined. joined is true when the call joined a pull that was already running instead of starting one. The running job keeps the options it started with, so a joined refresh did not happen. Poll GET /api/missions/pull-jobs/{job_id}. Example

GET /api/missions/pull-jobs/{job_id}

Synopsis
Description. Reports a pull job’s progress. percent and eta_seconds are null until Docker knows the total size, and neither is monotonic, because the daemon discovers layers as it goes. A pull that shows no byte movement is usually not stuck: cached layers and registry negotiation report status without transferring anything, which is what phase is for. Parameters. Request. No body. Response. 200 with job_id, mission_id, status (pulling, installed, error, or cancelled), phase (resolving, pulling_image, downloading_bundle, installing, or done), bytes_current, bytes_total, percent, eta_seconds, detail, cancel_requested, and, once installed, the catalog entry. 404 when the job id is unknown. Example

GET /api/missions/pull-jobs

Synopsis
Description. Finds the pull job currently running for a mission, so a client that reloads mid-pull can rejoin the progress it was already watching instead of starting a second pull. Parameters. Request. No body. Response. 200 with the same job object as GET /api/missions/pull-jobs/{job_id}, or null when nothing is pulling. Omitting mission_id is 422. Example

POST /api/missions/pull-jobs/{job_id}/cancel

Synopsis
Description. Requests cancellation of a pull. The cancel is cooperative and is checked on every progress event, so it lands quickly and does abort the transfer rather than letting it finish quietly. Calling it twice is harmless, and cancelling a job that already finished returns that job unchanged. Parameters. Request. No body. Response. 200 with the job object, cancel_requested set to true and status moving to cancelled. 404 when the job id is unknown. Example

POST /api/missions/{mission_id}/pull

Synopsis
Description. Pulls a library mission and blocks until the pull finishes. It works, but it is superseded: no shipped client calls it, a large image holds the connection open for minutes, and it reports no progress. Prefer the job pair above. Treat this endpoint as internal. Parameters. Request. No body. Response. Example

GET /api/catalog/status

Synopsis
Description. Reports whether the mission library is reachable from this machine. disconnected means you turned the library off; error means it is on but did not answer. Parameters. None. Request. No body. Response. 200 with state (connected, error, or disconnected), last_sync, and message. Example

GET /api/announcements

Synopsis
Description. Returns the announcements XORCISE Remote is publishing, which the web UI shows as banners. The local server fetches them from catalog_url on the caller’s behalf, as GET /v1/announcements/active, and keeps the answer in memory for two minutes per catalog URL. The web UI calls this once per document load and never polls; it retries up to twice only when this local endpoint itself did not answer, for example while the server restarts. The answer is an empty list, never an error, whenever there is nothing to show. With the library disconnected, catalog_url cleared, or use_stubs on, no request leaves the machine. Otherwise an unreachable catalog, a reply over 64 KiB or in the wrong shape, and a catalog too old to serve announcements all give the same empty list. This call waits at most five seconds for the catalog. If the catalog has not answered by then, it returns the last announcements the server received from that catalog URL, even if they are older than two minutes, or an empty list if there are none. The fetch carries on in the background, and its reply is kept for the next call. Only one fetch per catalog URL runs at a time. Parameters. None. Request. No body. Response. 200 with announcements, holding at most one entry per placement. Each entry has id, revision, placement (application for the banner below the header, catalog for the one in the XORCISE Remote tab), tone (information, maintenance, incident, or resolved), body_md, and dismissible. body_md is a restricted Markdown subset of at most 600 characters. revision goes up when the publisher edits an announcement, and the web UI remembers a dismissal per id and revision, so an edited banner comes back. dismissible is always false for an incident. The response carries Cache-Control: no-store. Example

Runs

GET /api/runs

Synopsis
Description. Lists runs, newest first. There is no endpoint for a single run: list and filter client-side, or read the run’s result, environment, or events directly. state is created until the agent starts, xorcising from then until the run ends, and terminal once it is over. Parameters. None. Request. No body. Response. 200 with an array of run records carrying run_id, agent_id, mission, name, state, created_at, started_at, start_trigger, start_trigger_label, budget_seconds, terminal_trigger, completed_at, model, sandbox_ref, agent_version, mission_version, source_agent, intel_policy, last_telemetry_at, outcome, and status. The run_control_key is never included. started_at is when the agent started, and start_trigger is what showed it had: telemetry (its first OTel export was admitted), run_control (its first run-control call with the run’s key), or tailnet_key (it fetched its tailnet join key, on a lab mission). Both are null until the agent starts, and stay null on a run whose agent never did. start_trigger_label is the server’s wording for the trigger — first OTel export, first run-control call, or fetched its tailnet key — and is read-only. What starts a run has the rules. terminal_trigger is null until the run ends, then one of done, timeout, operator, cancelled, never_started, deploy_failed, or crashed. The server derives the last two fields from it on every read; they are never stored:
  • outcome is how the run ended for every rate: completed (done), partial (timeout or operator; graded), not_started (cancelled or never_started; never graded, and left out of every performance view), infra_failed (deploy_failed or crashed; never graded, and in no score or rate), or null for a run that has not ended. Whether a graded run is in the median is its bucket on GET /api/results/runs: a timeout is, an operator stop is not.
  • status is how the run is shown: a label such as Created, Xorcising, Completed, Timed out, Stopped, Cancelled, Never started, Deploy failed, or Crashed; a tone of ok, active, err, or muted; and a one-sentence detail saying why it ended, null for a run that has not ended and for done. The CLI and the web UI both print these words rather than mapping the trigger themselves.
Example
Every record also carries the provenance of the mission install the run used, copied when the run was created: install_revision (the local install counter), mission_version and mission_base_version (SemVer strings), platform (for example linux/amd64), platform_digest, index_digest, and content_hash. All but install_revision are null for a mission you ingested yourself or one installed before XORCISE recorded provenance. POST /api/runs returns the same fields. Every record also carries terminal_detail, why the run ended when the trigger alone does not say. On a run the readiness gate closed out as deploy_failed, it holds the gate’s verdict followed by the environment’s log tails, captured before the environment was torn down and capped at 8,000 characters; The readiness window says what goes in it. It is null while the run is live, and for a run that ended done, timeout, operator, cancelled, never_started, or crashed. The exception is a run the upgrade to 0.2.1 reclassified as cancelled or never_started: its terminal_detail says what it ended as before, the score an earlier version gave it, and that the upgrade removed that grade.

POST /api/runs

Synopsis
Description. Creates a run and deploys its environment: it reserves a private subnet, starts the mission, joins it to the run’s tailnet, renders the connect prompt, and mints the run-control token. For a lab mission this takes seconds and can fail for real infrastructure reasons, which is why it has more failure codes than any other endpoint. A static mission deploys nothing. Before it reserves anything, a lab run checks that this host can nest containers at the platform the mission’s image was installed for, which on an arm64 host is linux/amd64 for a mission with no arm64 image. When the mission is not installed yet and this endpoint pulls it, the check runs at the platform the pull selects, before the image downloads. Parameters. None. Request. agent and mission are required and are the agent’s name and the mission’s id. budget_seconds bounds the run in wall-clock seconds. name overrides the generated run name. intel_policy is all or empty for every authored intel item, none for no intel, or a comma-separated list of intel ids such as i1,i3; it defaults to all.
Response. Example

DELETE /api/runs/{run_id}

Synopsis
Description. Deletes one run and everything recorded for it: its result, its submissions, its raw traces and logs, its evidence seal, its event stream, its observed facts and terrain updates, the run row, and its runs/<run_id>/ directory under the XORCISE home. A run that has not finished cannot be deleted, because its environment is still deployed — terminate it first. The steps run in an order that keeps a partial failure from doing lasting harm, but they are not one transaction. A delete that fails before the run row goes leaves the run listed with its result, possibly without its telemetry or submissions, and repeating the delete finishes it. Past that point a failure leaves only records with no run behind them — a run directory that cannot be removed is logged and the delete still answers 204 — and the next start removes them. The OTLP receiver does not refuse a deleted run’s id, because the run’s seal is deleted with it, so whatever a harness still exports for that id is stored again until the next start clears it. Parameters. Request. No body. Response. The run record is the only handle XORCISE keeps on a run’s container and tailnet nodes, so by default a delete does not remove it while the environment may still be up. If the run is finished but its environment has not been released yet, the server tears it down first, synchronously, so the call can take as long as that teardown. If the teardown fails because Docker or Headscale is unreachable, the delete is refused. With force=true it goes ahead anyway and logs a warning: the container then stays until XORCISE next stops or starts and reaps it, and nothing is left that can remove its tailnet nodes. A refusal is a 409 whose body carries three fields beside detail: run_ids names the runs the refusal is about. force=true overrides unreleased only; a running or grading refusal is returned whatever force says. Example
The same delete with Headscale stopped:

POST /api/runs/{run_id}/terminate

Synopsis
Description. Stops a run early on your instruction, seals the evidence collected so far, tears the environment down, and schedules grading in the background. The result is flagged partial with a partial_trigger of operator, which is how a deliberately-stopped run is told apart from one that scored badly. A run whose agent has not started is cancelled instead, and is not graded. Parameters. Request. No body. Response. Grading has not finished when this returns. Poll GET /api/runs/{run_id}/result through the 202 window. A run still created, whose agent has not started, is cancelled instead of stopped: the record comes back with terminal_trigger cancelled, outcome not_started and the label Cancelled, and nothing is graded, so its result is the final not-graded answer straight away. Example

GET /api/runs/{run_id}/environment

Synopsis
Description. Reports the live state of the run’s environment. This is what drives the status chip on the live run page, and it is the endpoint to poll after creating a run to know when the mission is actually up. While the run is live, detail is the readiness gate’s latest reading in words: which mission services are still starting, that the Docker daemon inside the run’s container is not answering, or that the run’s subnet router has not joined the tailnet yet. Parameters. Request. No body. Response. 200 with run_id, state, ready, detail, and release_attempts. state is none for a static mission with no environment, then starting, ready, or failed while the run is live. Once the run is over it reads releasing until its container and tailnet nodes are actually gone, then released. 404 when the run id is unknown. Every finished lab run passes through releasing, because the environment is torn down only once grading finishes. releasing with a release_attempts of 0 is that normal wait. A non-zero release_attempts counts failed teardowns, almost always Docker or Headscale being unreachable, and detail then carries the count and the last error. XORCISE retries the teardown automatically until it lands; see release_sweep_interval_seconds. Example
A finished run whose teardown failed while Headscale was stopped:

GET /api/runs/{run_id}/prompt

Synopsis
Description. Returns the connect prompt verbatim — the mission text your agent reads, carrying the objective with target IPs substituted, the tailnet join recipe, the run-control endpoints and token, and the correlation marker. This is the prompt you hand an agent directly, so it carries the token inline. The prompt embedded in the launch command from launch-profile refers to $XORCISE_RUN_BEARER instead. launch_mode rebases the run-control base URL in that text to where your agent actually runs, and getting it wrong is the most common reason an agent cannot check in. Parameters. An unrecognised launch_mode is clamped to container rather than rejected, then narrowed to what the agent’s harness supports. Request. No body. Response. 200 with {"run_id": "...", "prompt": "..."}. 404 when the run id is unknown. Example

GET /api/runs/{run_id}/launch-profile

Synopsis
Description. Returns the telemetry environment for this run and a copy-paste command to start the agent with it. This is the authoritative OTLP endpoint — the otel field on the agent record is not read. correlation reports how strongly traces will bind to this run: resource-attr for a harness with a telemetry provider, prompt-sentinel otherwise, which relies on the marker in the connect prompt surviving. Parameters. Request. No body. Response. 200 with run_id, env, correlation, notes, fallback, launch_mode, launch_modes, command, shell_block, and tips. env is empty when no OTLP collector is configured. When the harness has a launch command, shell_block also exports the run’s token as XORCISE_RUN_BEARER before the command, and notes says to pass it through when the command starts the agent elsewhere. The prompt inside command refers to the variable rather than carrying the token. env never carries the token, because xorcise run launch-profile prints it as a file you might save anywhere. fallback is true when the generic telemetry provider supplied env: for openhands, which has no provider of its own, for generic, and for a kind of your own. 404 when the run id is unknown. Example

GET /api/runs/{run_id}/events

Synopsis
Description. Returns the normalized replay stream — what the agent did, after the harness’s raw telemetry has been run through its replay adapter. XORCISE ingests both traces and logs, and they advance on separate cursors, because a harness such as Codex carries its entire narrative in logs. Poll by echoing the next_cursor from the previous response. Parameters. Request. No body. Response. 200 with run_id, source_agent, adapter_name, adapter_version, events, counts, warnings, fallback, next_since, and next_cursor. fallback is true exactly when the generic adapter handled the stream, which includes every run of a generic agent. An unknown run id returns an empty stream rather than 404. Each event carries id, run_id, kind, title, body, ts, raw_ref and source_agent, plus optional detail such as subkind, role, severity, status, duration_ms, data and the grouping and actor ids. raw_ref points back at the stored record the event came from. counts and warnings describe the whole run on every page, whatever the cursor. GET /api/runs/{run_id}/telemetry lists their keys and returns them without the events. Example

GET /api/runs/{run_id}/events/{event_id}/raw

Synopsis
Description. Returns the stored OTLP records that one normalized event was derived from. Use it to check what a replay adapter did with a record, or to see attributes the adapter dropped. The raw record is canonical; the normalized event is a view over it. Parameters. Request. No body. Response. 200 with event_id, raw_ref, spans and logs. raw_ref has run_id, signal (trace or log), raw_seq, span_id and trace_id. For a trace-derived event, spans holds the raw OTLP span or spans at that sequence whose spanId matches and logs is empty. For a log-derived event, logs holds the raw log records of the export batch at that sequence and spans is empty. 404 when the event id is not in that run. Example

GET /api/runs/{run_id}/telemetry

Synopsis
Description. Returns the run’s telemetry summary: which replay adapter rendered the run, whether that was the generic fallback, how many spans and log records arrived, how many of them carry content the judge can read, and the normalization warnings. It is the header of GET /api/runs/{run_id}/events without the events, read from one cached row, which is rebuilt from the raw trace first whenever new telemetry has arrived or the renderer has changed. xorcise run status reads it for a graded run, prints it as the telemetry: line, and adds it to --json output as telemetry. The run report reads it too. It is derived from the raw trace and is never a grading input. Parameters. Request. No body. Response. The 200 body has run_id, source_agent, adapter_name, adapter_version, fallback, counts, and warnings. fallback is true exactly when the generic adapter rendered the run. A run with no telemetry yet returns zero counts and no warnings. counts has these keys: Each entry in warnings has code, message, span_id (always null for the codes below), and count. The message is the sentence the replay, the CLI and the report show word for word. The code is one of these values: The warnings cover whatever has arrived so far. On a live run, no_content can appear before the first content-bearing span lands, which is why the web UI withholds it until the run has ended. Example
A harness whose spans carry only an event class, an id and a source, through the generic adapter, reads like this:

GET /api/runs/{run_id}/traces

Synopsis
Description. Returns the raw OTLP records collected for a run, in arrival order, with no adapter applied. since is exclusive, so passing the last seq you saw gives you only what arrived after it. This is the endpoint behind xorcise run traces, and it is the right one for confirming that telemetry is reaching XORCISE at all. Parameters. Request. No body. Response. 200 with {"run_id": "...", "records": [{"seq": 0, "payload": {...}}]}. An empty records array on a run whose agent is working usually means correlation failed rather than that nothing happened — check correlation on the launch profile. Example

GET /api/runs/{run_id}/otlp.jsonl

Synopsis
Description. The run’s raw OpenTelemetry stream as a downloadable JSONL file: one line per export batch, verbatim as the agent streamed it — trace batches (resourceSpans) in sequence order, then log batches (resourceLogs). There is deliberately no XORCISE framing. No header line, no envelope; every line is a plain OTLP/JSON object. The file is the Collector’s own otlpjson format, so it feeds straight into an OpenTelemetry Collector and on to Jaeger, Tempo, or any other backend that reads OTLP. Works mid-run as a partial export of what has been ingested so far. The evidence stores are append-only with whole-batch commits, so a partial export is always a valid prefix of the final stream rather than a torn one. Parameters. Request. No body. Response. The suggested filename is xorcise-run-<id8>-<mission-slug>-otlp.jsonl. Example
The CLI equivalent is xorcise run traces --export; the web UI offers it as OTLP Traces (.jsonl) on the result page.

GET /api/runs/{run_id}/events.jsonl

Synopsis
Description. The run’s normalized event projection as a downloadable JSONL file: a {"type": "header", …} line carrying run and adapter metadata, then one AgentEvent per line with clean bodies rather than raw OTLP. This is the same content the on-seal artifact under ~/.xorcise/runs/<run_id>/ carries, and what xorcise run events export fetches. Works mid-run as a partial projection. For the unnormalized stream, use GET /api/runs/{run_id}/otlp.jsonl. Parameters. Request. No body. Response. The suggested filename is xorcise-run-<id8>-<mission-slug>-events.jsonl. Example

GET /api/runs/{run_id}/terrain2

Synopsis
Description. Returns the resolved terrain map for a run: the nodes the mission author declared, the updates that have fired, and the attribution linking each update to the evidence that triggered it. Terrain conditions are natural-language statements scored by a model, so this endpoint’s contents depend on the terrain model being configured. Parameters. Request. No body. Response. 200 with the resolved terrain: run_id, mission_id, objective_id, summary, groups, nodes, edges, updates, and attribution. 404 when the run id is unknown. A mission that declares no terrain returns an empty map, not an error. Example
A node’s state is defined, discovered or completed. Each update targets a node, group or edge by target_id and sets whichever of state, discovered and active it carries. event_id is null for updates XORCISE derived from its own records, and ts is null when an update has no timestamp to anchor to. attribution.running is true while the terrain model is still working through the run’s events.

GET /api/runs/{run_id}/artifacts

Synopsis
Description. Returns everything the agent submitted during the run, with the payloads intact, in submission order. This is the operator’s view — it needs no run-control token, unlike the endpoint the agent submits through. The flag, if the agent found one, is the artifact named flag. Parameters. Request. No body. Response. 200 with an array of {name, kind, seq, payload}. 404 when the run id is unknown. Example

Results

GET /api/runs/{run_id}/result

Synopsis
Description. Returns the recorded score and the conditions it was produced under. The score is half deterministic checks and half model judgement, each weighted 0.5, so a run graded with no judge configured tops out at 50% and that is not a failure. judge_status is the diagnostic to read first when the judge half is zero. grade.transcript_items says how many distilled transcript items the judge was handed: 0 means it saw no telemetry content and graded from the artifacts alone, and it is null on a result graded by an earlier release. Parameters. Request. No body. Response. models_reported is the model or models the agent’s harness named in the run’s telemetry, in the order it first named them, and [] when it named none. It is not conditions.model, which is only what you declared with --model: both are self-reported, XORCISE verifies neither, and Conditions shows how the two are displayed together. The list holds at most eight names, and a name longer than 120 characters is cut short and ends in …. models_reported_truncated counts the further distinct names past the eighth. It stops growing once the telemetry has named 512 distinct models, so it is never more than 504. The same values are models and models_truncated on GET /stats. agent_model and telemetry_window are the run report’s Agent model and Telemetry window cells, word for word, computed by the same code, so a client can show them without re-deriving the report’s rules. agent_model weighs the declared model against the names the telemetry carried, as Conditions tabulates, and is null where the report would say not disclosed. telemetry_window is the stretch the telemetry covers, or why it is withheld when the harness’s clock contradicts the server’s, and is null when the report omits the row: no telemetry, or a span nobody measured. Both are reported by the agent’s harness and say so in their text. Each check_breakdown entry is one check’s verdict: id, source, ref, op, the resolved value, passed, the effective weight, error, and blocked_by. blocked_by lists the checks named in the check’s requires that did not pass. A check with a non-empty blocked_by reports passed: false even if its own op succeeded. Each judge_breakdown entry carries criterion_id, text, weight, score, reason, and status. grade.judge_status is ok, partial, model-not-configured, or unavailable, with the reason in judge_detail. grade.grading_failed is true when grading itself raised and the 0.0 result is XORCISE’s stand-in: nothing graded the run, and re-grading it is the fix. grade also declares hard_fails, key_evidence, and major_deductions; no code path fills them and they are always empty. How grading works explains the arithmetic. evidence_digest is the 64-character hex SHA-256 recorded when the run was sealed. evidence_verified is true or false only when this request hashed the evidence again, and null otherwise. Branch on evidence_status: The digest covers the raw trace and log batches, the run-control record, and the observed facts. It does not cover the result itself, and it is not a signature: Security and isolation sets out what it can and cannot show. Example
Ask for a verdict with verify=true. The seal fields of the same run then read:

GET /api/runs/{run_id}/report

Synopsis
Description. Returns the run’s full report as a single self-contained document — the same scores, check table, rubric, evidence, artifacts, and conditions as the result, rendered for sharing. The HTML carries its own styling and loads no external asset, so it survives being emailed. Every request hashes the run’s evidence again and checks it against the seal, so a report takes longer on a bigger run. The outcome is the Evidence seal row of the Conditions table, with the first 16 characters of the digest; a run sealed before XORCISE recorded digests has no row. A mismatch also puts a warning at the top of the document, above the scores. Parameters. Request. No body. Response. Example
A partial judge result shows each of Overall and Judge as a range from the score to its upper bound, such as 0.76 (76%)–0.91 (91%), followed by a line giving the share of rubric weight the judge scored.

GET /api/runs/{run_id}/stats

Synopsis
Description. Returns the token, count, model, and timing snapshot recorded when the run was graded. The snapshot names what produced it in projection: the event projection it was folded from and the version of the fold. When that does not match the current one, because the run was graded by an earlier release or before the snapshot existed, the snapshot is folded again from the run’s events on this read and stored in place of the old one, so this endpoint, GET /result and the report agree. The grade itself is never rewritten. Every token figure is 0 on a run that emitted no telemetry, which is the normal reading rather than a fault. Parameters. Request. No body. Response. models and models_truncated are the names and the overflow count that GET /result returns as models_reported and models_reported_truncated. In timing, elapsed_seconds is the server’s wall clock from the run’s creation to its close-out, which the report shows as Duration. first_event_ts and last_event_ts are the earliest and latest event timestamps as the harness dated them, and a span’s timestamp is its start. last_event_end_ts is the latest point any event reaches, its start plus the duration it reported, and equals last_event_ts when no event reported a duration. The report’s Telemetry window runs from first_event_ts to last_event_end_ts; Budgets says when it is withheld. projection reads like claude-code@8+normalizer.3+stats.3: the adapter and its version, the normalizer version, and the fold version, so it changes whenever any of the three does. Example

POST /api/runs/{run_id}/regrade

Synopsis
Description. Re-grades a finished run’s already-sealed evidence against your current settings, without re-running the agent. This is the fix for a run whose judge half failed for a configuration reason — the transcript exceeded the judge’s token budget, or the key was rejected. Raise the cap or fix the key, call this, and the judge scores the same preserved evidence. Re-grading is cheap next to a re-run, and it is the only way to recover a misconfigured judge on a run you cannot reproduce. The CLI equivalent is xorcise run regrade; the web UI exposes it as Re-evaluate on the Results page. Parameters. Request. No body. Response. The new score arrives through the same 202 poll as the first grade, so a client needs no extra state. Calls are de-duplicated: pressing the button twice grades once. Before grading, XORCISE checks the run’s evidence against its seal. If it no longer matches, the server logs a warning and grades the run anyway; the response is the same 202. Read evidence_status with verify=true on the result to see it. Example

GET /api/results/runs

Synopsis
Description. Returns every finished run, oldest first, with its scores and how it counts, in one request. The web UI’s run strip, grader agreement plot and run ledger draw from it. A run still in progress is not listed, and neither is one that ended before its agent started (cancelled or never_started). A run still being graded is, with the bucket grading, and reading this route re-drives its grading as polling its result does, so a grade lost to a restart heals here too. Parameters. Request. No body. Response. 200 with an array of run scores carrying run_id, agent_id, mission, mission_version, current_version, created_at, completed_at, outcome, terminal_trigger, bucket, timed_out, deterministic, judge, judge_upper, judge_status, overall, overall_upper, judge_model, and intel_disclosed. bucket is how the run counts, and the one rule every view reads: mission_version is the version label the web UI shows after a v: the mission’s own version, or its install revision for a mission that declares none. current_version is false for a run on a version that is no longer installed; for a mission no longer installed at all, its newest run’s version counts as current. It is decided over every run, so a filter never changes it. The scores are null for a run with no grade. Example

GET /api/results/summary

Synopsis
Description. Summarises the finished runs GET /api/results/runs lists: as one group, per agent, per mission version, or per agent on each mission version. This is the summary the web UI’s Results page and dashboard, and xorcise leaderboard, all show. A run that ended before its agent started is not in it at all. The median is over the runs in the scored bucket; every other run is counted on its own and never in it. Like the runs route, reading it re-drives grading for a run still grading. Parameters. Request. No body. Response. 200 with an array of summaries; [] with no finished runs. 422 for any other by. Each summary carries: Every score is a fraction between 0 and 1, and null when the group has no scored run. Groups with an agent come back ranked: best median first, a group with no median last, then more scored runs first, then the name. A mission_agent summary keeps the mission versions in order and ranks the agents within each. Mission groups come back in the order of their oldest run. Example

Run control

These are the endpoints your agent calls during a run. Every one requires Authorization: Bearer <run_control_key> and returns 401 without it, except /alive, which the join script calls with its watch token. Two shared mappings apply to the calls that read or write the run record: once the run is over, GET /mission, POST /artifacts, GET /intel, POST /complete, /alive, and the call that mints an attachment link return 409, and a mission that cannot be read returns 404. /connect, /join.sh, /tailscale.tgz and the signed attachment download do not check the run state. The first call that passes the bearer check moves a run that is still created to xorcising. Its start_trigger is tailnet_key for connect, join.sh and tailscale.tgz, and run_control for the rest. The join script’s /alive check takes a different token and does not start a run. Connect any agent walks through the contract these endpoints form.

GET /api/runs/{run_id}/mission

Synopsis
Description. Returns the agent’s brief: the mission id, the objective with target IPs already substituted, and the names of the attachments available. It deliberately does not include the rubric or the checks. Your agent can re-read this at any time while the run is open. Parameters. Request. No body. Response. Example

POST /api/runs/{run_id}/artifacts

Synopsis
Description. Submits one named finding. This is how the agent reports what it recovered, and the flag is the artifact named flag — there is no separate flag endpoint. Content is inline text or JSON; binary upload is out of scope. Submitting a name twice records both, in order. The name matters: deterministic checks look the artifact up by the name the agent used, so a misspelled name fails the check silently rather than raising an error here. Parameters. Request. name and content are both required strings.
Response. Example

GET /api/runs/{run_id}/intel

Synopsis
Description. Requests the next intel item the run’s policy allows. Each call discloses one more, in the order the mission authored them, and the count is recorded on the result as intel_disclosed, so disclosed intel is visible in the conditions a score is compared under. remaining tells the agent how many intel items are left after this call. When the policy allows no intel, or all of it is spent, intel is null and remaining is 0. Parameters. Request. No body. Response. Example

POST /api/runs/{run_id}/complete

Synopsis
Description. Ends the run. XORCISE seals the evidence, tears the environment down, and schedules grading in the background. Your agent should call this when it is finished, whether or not it solved the mission — a run left open scores nothing until its budget expires. Telemetry arriving after this call is dropped once the drain window closes — telemetry_drain_seconds, 5 seconds by default — so flush your exporter before calling it. Parameters. Request. No body. Response. Example

GET /api/runs/{run_id}/connect

Synopsis
Description. Returns the credentials your agent needs to join the run’s private network, as JSON. login_server is already rewritten to an address the agent can reach, and ca_cert carries the local certificate authority when one is needed and is an empty string otherwise. Most agents use the script form below instead of assembling the join themselves. Parameters. Request. No body. Response. 200 with login_server, join_key, and ca_cert. 401 without a valid bearer. Example

GET /api/runs/{run_id}/join.sh

Synopsis
Description. Returns the same join bundle as a runnable shell script, which is the join path the connect prompt tells your agent to use. It needs no root and configures the tunnel in userspace, printing the SOCKS5 address the agent then routes target traffic through. The script bakes a self-reaper that tears the tunnel down as soon as GET /api/runs/{run_id}/alive reports the run over, and at the latest at the run’s budget plus 600 seconds, or after 86,400 seconds when the run is unbudgeted. The script keeps the join key and the token off every command line it runs; Connect any agent lists the variables it reads. Parameters. Request. No body. Response. 200 with text/x-shellscript. 401 without a valid bearer. Example

GET /api/runs/{run_id}/alive

Synopsis
Description. Answers whether the run is still live. The join script’s self-reaper polls it every 15 seconds and tears the tunnel down on the first 409. It authenticates with the run’s watch token, not the run-control token, and the two never substitute for each other: the watch token is refused everywhere else, and the run-control token is refused here. The join script carries the watch token where the run-control token must not go — BusyBox wget in the Docker sidecar can only send a header as a command-line argument — so a leaked watch token reveals only whether the run is still going. Parameters. Request. No body. Response. A deleted run answers 409 rather than 401, so a reaper polling for it stops.

GET /api/runs/{run_id}/tailscale.tgz

Synopsis
Description. Serves the pinned static tailnet client the join script uses, so an agent in a minimal container does not need a package manager or its own network access to get one. The join script fetches this for you; call it directly only when you are assembling the join by hand. Parameters. Request. No body. Response. Example

GET /api/runs/{run_id}/attachments/{name}

Synopsis
Description. Downloads a mission attachment in two calls. Call it with the bearer and no query parameters and you get a signed URL back; call that URL with the X-Run-Key header and you get the bytes. The split keeps file bytes off the bearer-authenticated call so a download can be streamed or handed to another process without carrying the run token in a shell history. Parameters. Request. No body. Response. The mint response carries name, url, expires_at, media_type, and sha256. Example
Then fetch the bytes:

Harnesses

GET /api/harnesses

Synopsis
Description. Lists the built-in harnesses that ship both a replay adapter and a launch provider — the values worth registering as an agent’s kind. Each descriptor carries a registration-time preview of how that harness is launched. The preview is a template, not a runnable command: run credentials, endpoints, and the mission text only exist once a run is created, so GET /api/runs/{run_id}/launch-profile remains authoritative for a real launch. Parameters. None. Request. No body. Response. 200 with an array of descriptors carrying kind, display_name, description, model_hints, capabilities (the same profile the capabilities endpoint serves), and launch — launch_modes (host, container), command_template, model_flag, model_flag_anchor, tips, and mission_preamble. Example

GET /api/harnesses/capabilities

Synopsis
Description. Returns every registered replay adapter’s declared telemetry capability profile, sorted by adapter name — the honest matrix of which event kinds a harness actually exports. notes carry the user-facing gap sentences for partially-supported kinds, rendered verbatim in the UI and the judge disclosure. verified is false only for the generic fallback adapter, whose profile is not audited against a real harness. Parameters. None. Request. No body. Response. 200 with an array of profiles carrying adapter_name, adapter_version, verified, kinds (a mapping over every event kind to supported, partial, or unsupported), message_roles, and notes. Example

System and configuration

GET /api/health

Synopsis
Description. Liveness only. It answers as soon as the REST plane is serving and says nothing about Docker, Headscale, or the database — use the system endpoint for those. Parameters. None. Request. No body. Response. 200 with {"status": "ok", "service": "rest"}. Example

GET /api/system

Synopsis
Description. Reports what the running instance sees about itself: which role it booted, which planes are healthy, where its data lives, and whether its database schema matches the build. xorcise status probes from your machine; this asks the instance, and the two disagreeing is itself the diagnosis. Parameters. None. Request. No body. Response. 200 with role, topology (local or distributed), home, db_url, db_schema (head, behind, fresh, or unknown), planes, catalog, remotes, pid, host_platform, and mission_base. On the default all role, planes carries four rows — rest, docker, headscale, and otlp — each with name, ok, detail, location, role, label, and state. A db_schema of behind means xorcise db upgrade is due. pid is the serving process’s id, which lets xorcise up, xorcise down, and xorcise db upgrade find the running instance through its REST port when the xorcise.pid file is missing or stale. A server older than 0.2.0 reports no pid, so against one of those xorcise up can restore only the port record, not the pid file. host_platform is the platform this host runs missions on, or null when it is unknown. mission_base is null or an object with required_major (the mission base generation this XORCISE needs), client_version, promoted_version, and promoted_index_digest; the last two are null when no promoted base is known. Two more objects report on runs rather than planes:
  • releases counts finished runs whose environment a teardown failed to release, so their container or tailnet nodes are still up: pending, oldest_ended_at, and oldest_error, the most recent teardown error of the longest-waiting run (not the newest error across all of them). A run that is only waiting for its grade to finish is not counted. XORCISE keeps retrying each one, and pending falls back to 0 once Docker and Headscale are reachable again. null when the count cannot be read.
  • boot_reconcile is what the start-up reconcile did with the runs that were live when the server last stopped. state is pending, running, done, or failed, with finished_at. adopted counts runs whose container survived the restart and were left running, aborted runs whose container was gone and were closed out as crashed (not graded), and released unfinished deploys that were torn down and dropped. release_pending lists unfinished deploys whose teardown failed: they are closed as deploy_failed, not graded, and their teardown is retried with the others. failed lists runs the reconcile could not converge, left as they were for the next start. state is failed only when a whole step failed, with the reason in error; the steps after it still ran.
Example

POST /api/system/nested-support/refresh

Synopsis
Description. Probes again whether this host can run a container inside a container, in the server’s own process, and replaces the verdicts run creation reads. The server keeps each platform’s verdict, refusals included, so without this a host you have fixed — Rosetta enabled, say — keeps being refused until XORCISE restarts. It probes the Docker daemon’s own platform and every other platform a run has asked about. Each probe starts a throwaway privileged container, and pulls its image on a cold host, so the call can take minutes. Run creation keeps reading the old verdicts until the new ones are in. xorcise doctor calls this whenever XORCISE is running. With nested_container_check = "skip" nothing is probed: native is ok with the detail nested-container check skipped by configuration, and foreign is empty. Parameters. None. Request. No body. Response. 200 with native, the verdict for the daemon’s own platform, and foreign, one verdict per other platform. Each verdict has platform ("" for the native one), ok, detail, and remediation ("" when there is nothing to fix). Example

GET /api/config

Synopsis
Description. Returns the effective configuration with secrets masked. Model keys are never returned — key_hint shows the last four characters so you can tell which key is loaded. judge.configured is a presence check only and does not prove the key works; the test endpoints do that. Parameters. None. Request. No body. Response. 200 with judge, terrain, catalog, network, and default_budget_seconds. terrain.uses_judge_default is true when no terrain override is set, which means terrain attribution calls the judge model. Configuration keys documents every field. Example

PUT /api/config/model

Synopsis
Description. Sets the judge model and its token caps. Every field is optional and only what you send is changed; an empty string clears a field. The two caps are the controls behind judge failures on long runs — span_max_tokens truncates each span, and transcript_max_tokens caps the whole prompt before it is sent. Writes land in ~/.xorcise/.env as XORCISE_* variables, never in config.toml, so a value set here survives a restart but does not appear in your config file. Parameters. None. Request. All fields optional: model_name, base_url, key, transcript_max_tokens, span_max_tokens, tokenizer, timeout_seconds.
Response. 200 with the updated masked configuration, the same shape as GET /api/config. 422 when a value is out of range — the caps must be non-negative and timeout_seconds must be greater than zero — or when a text field, such as the key, contains a line break: <field> must be a single line — it contains a line break, which would corrupt ~/.xorcise/.env. Nothing was changed. Unlike other 422s, that detail is a string. Example

POST /api/config/model/test

Synopsis
Description. Calls the saved judge model once and reports what happened. Along with the terrain-model test, a re-grade and the grading that follows a run, it spends tokens on your provider account, and it is the difference between a key that is present and a key that works. The call carries the judge’s own order of message roles — system, user, assistant, user — with trivial content, so a server whose chat template would reject a grading call’s role order rejects this one too. It does not test context size or the judge’s reply format. Parameters. None. Request. No body. Response. 200 with ok, status (ok, not_configured, or error), model_name, and message. A failed call is still 200 — read ok, not the status code. Example

PUT /api/config/terrain-model

Synopsis
Description. Sets a separate model for terrain attribution, or clears it so terrain falls back to the judge model. Terrain attribution calls a model on every run that declares terrain, so pointing it at a cheaper model than the judge is the usual reason to set it. Parameters. None. Request. All fields optional: model_name, base_url, key, transcript_max_tokens. Send empty strings to clear the override.
Response. 200 with the updated masked configuration. terrain.uses_judge_default becomes false once an override is set. 422 when transcript_max_tokens is not greater than zero, or when a text field contains a line break, as for PUT /api/config/model. Example

POST /api/config/terrain-model/test

Synopsis
Description. Calls the effective terrain model once — the override when one is set, the judge model otherwise — and reports what happened. Like the judge test, it spends tokens. Parameters. None. Request. No body. Response. 200 with the same shape as the judge test: ok, status, model_name, message. Example

PUT /api/config/catalog

Synopsis
Description. Turns the mission library on or off. Disconnecting hides library missions from browse and leaves your installed missions untouched. No credential is involved in either direction — the library needs no account or key. Parameters. None. Request. connected is required.
Response. 200 with the updated masked configuration. 422 when connected is missing or is not a boolean. Example

PUT /api/config/network

Synopsis
Description. Sets the addresses used when XORCISE runs across more than one machine. This backs the distributed topology, which is experimental: the CLI labels it so, the web UI shows these values read-only, and values written here apply at the next start rather than immediately. Parameters. None. Request. Both fields optional: headscale_url, advertise_host.
Response. 200 with the updated masked configuration. 422 when a field contains a line break, as for PUT /api/config/model. Example

GET /api/fs/list

Synopsis
Description. Lists one directory on the machine running XORCISE. It exists so the web UI’s bundle picker can browse to an ingest directory, and it is read-only. Unlike every other endpoint here, this one is gated on where the caller connects from. It exposes the server host’s own directory tree, so it answers the local operator alone: a client that is not on loopback gets 403. That includes LAN peers under an explicit XORCISE_HOST=0.0.0.0 bind and agent containers reaching the API over the Docker bridge. A caller whose address cannot be parsed is refused too — it fails closed. Security and isolation covers the boundary. Parameters. Request. No body. Response. 200 with path, parent, and entries, each entry carrying name, path, and is_dir. 400 when the path does not exist or is not a directory. 403 when the caller is not on loopback. Example
  • CLI reference — the same operations as commands, with exit codes and the web UI equivalent for each task.
  • Security and isolation — what the missing authentication means in practice, and how to place a boundary around it.