# HTTP API

The CLI and the web app use this API; so can anything that speaks HTTP. Every
route is under `https://rrun.041.io/api/v1` and takes and returns JSON.

## Authentication

Send a credential as a Bearer token:

```bash
curl -H "Authorization: Bearer $O41_RRUN_API_KEY" https://rrun.041.io/api/v1/jobs
```

- An organization API key (`ak_…`) acts in its organization. It can use the job
  routes below, not the ones marked **person**.
- A signed-in person's token (as the CLI's `login` gets) names its organization
  with the `x-o41-organization` header.

Errors are a 4xx or 5xx status with `{"error": "<code>", "message": "…"}`.

## Jobs

| Route                                     | Does                                                                              |
| ----------------------------------------- | --------------------------------------------------------------------------------- |
| `HEAD /bundles/:sha256`                   | 200 when the bundle is stored, 404 when not                                       |
| `PUT /bundles/:sha256`                    | Upload a bundle: the `.tar.gz` body, at most 95 MiB                               |
| `POST /offers`                            | `{resources, limit?}` → `{offers, errors, skipped}`, cheapest first               |
| `POST /jobs`                              | Create a job → 201 and the job                                                    |
| `GET /jobs?limit&before&active`           | Jobs, newest first → `{jobs, next}`; `active=1` hides ended jobs                  |
| `GET /jobs/:id?wait=<s>`                  | The job; `wait` (up to 25) holds the answer until it ends                         |
| `GET /jobs/:id/logs?after&limit&wait=<s>` | Log lines after `after` → `{lines, next, status, done, expired}`                  |
| `GET /jobs/:id/events?after&limit`        | Events after `after`, oldest first → `{events, next, more}`                       |
| `GET /jobs/:id/telemetry?from&to&points`  | Samples over a range → `{samples, from, to, expired}`                             |
| `POST /jobs/:id/cancel`                   | Cancel → the job                                                                  |
| `POST /jobs/:id/bad-host`                 | The job's cloud machine is bad: delete it, skip its host, run again → the job     |
| `GET /machines?all=1`                     | Machines; `all=1` adds terminated ones                                            |
| `PATCH /machines/:id`                     | `{enabled}`: turn a reserved machine on or off → the machine                      |
| `PATCH /clouds/:cloud[/:name]`            | `{gpu?, cpu?}`: an account's switches, or every account's of a cloud → `{clouds}` |
| `GET /hosts`                              | Hosts remembered for trouble → `{hosts}`, the latest first                        |
| `DELETE /hosts/:host`                     | Forget a host, as `vast%3A133104` → 204; 404 when none is remembered              |
| `GET /whoami`                             | `{orgId, userId, credential}`                                                     |

A job is created from a bundle uploaded first, by its sha256:

```json
{
  "name": "train",
  "command": "./train.sh",
  "bundleSha256": "<64 hex characters>",
  "resources": {
    "gpus": [
      { "model": "RTX5090", "count": 1 },
      { "model": "RTX5080", "count": 4 },
      { "model": "H200", "count": 8 }
    ],
    "minRamGb": 64,
    "minCpus": 0,
    "minDiskGb": 50,
    "minVramGb": 0,
    "spot": true,
    "clouds": [],
    "maxPricePerHour": 0,
    "minCuda": ""
  },
  "image": "",
  "env": { "DATASET": "small" },
  "secrets": { "HF_TOKEN": "hf_…" },
  "timeoutSeconds": 0,
  "keepSeconds": 300,
  "queueSeconds": 1800,
  "restarts": 10,
  "placement": "any"
}
```

`gpus` lists the acceptable shapes, any one of which will do; `gpus: []` asks
for a machine without a GPU. `clouds` takes clouds (`"vast"`, every account of
it) and cloud accounts (`"vast/lab-b"`); `[]` means every connected account.
`maxPricePerHour: 0` means no limit, `timeoutSeconds: 0` never. `restarts` may
be left out for 10; `0` never restarts. `placement` may be left out for `any` (a
free reserved machine first, else a cloud); `reserved` takes only reserved
machines and refuses `image` and `clouds`, `cloud` never takes one. The job
carries `restarts`, `placement` and its `attempt`, and once placed its `cloud`
and `account` (`vast/default`, or `reserved`). The bundle is a gzipped tar of
the folder.

Log reads continue from `next`: pass it as `after` on the next call. With
`wait`, an empty read is held up to that many seconds (at most 25) for new
lines. `done` is true once the job is over and every line has been read. A page
holds up to `limit` lines (default 1000, at most 5000) and stops early at about
4 MiB of text, so keep reading from `next` until `done` or an empty page.

Each line is `{seq, at, stream, text, attempt, machineId}`: one line the command
wrote, its `attempt` (1, 2 after the first restart, …) and the machine it ran on
(`null` for rrun's own lines without one). A line over 1 MiB of UTF-8 keeps its
first 1 MiB, ends with `… [rrun: line cut at 1.00 MiB of <size>]` and carries
`cut: {bytes}`, its whole length; a line without `cut` is whole.

Event reads page the same way, without `wait`: each event's `seq` counts up from
1, a page holds up to `limit` events (default 1000, at most 5000), and `more` is
true while more follow `next`. The job itself carries only its latest 50 events.

## Settings (person)

| Route                         | Does                                                                                             |
| ----------------------------- | ------------------------------------------------------------------------------------------------ |
| `POST /machines`              | `{name}`: register a reserved machine → 201 `{id, key, command, machine}`; the key is shown once |
| `DELETE /machines/:id`        | Delete a machine now, or remove a reserved one and revoke its key                                |
| `GET /clouds`                 | Connected cloud accounts with their `gpu` and `cpu` switches                                     |
| `PUT /clouds/:cloud/:name`    | Connect the account `<cloud>/<name>`; the body is its credentials                                |
| `DELETE /clouds/:cloud/:name` | Remove an account's credentials → 204                                                            |
| `GET /storage`                | Where logs go                                                                                    |
| `PUT /storage`                | `{endpoint, bucket, region, prefix, accessKeyId, secretAccessKey}`                               |
| `DELETE /storage`             | Back to rrun's own bucket                                                                        |
| `GET /api-keys`               | `{apiKeys}`                                                                                      |
| `POST /api-keys`              | `{name}` → the key with its `secret`, shown once                                                 |
| `DELETE /api-keys/:id`        | Revoke → 204                                                                                     |
| `GET /audit`                  | Audit entries, newest first → `{entries, next}`; see below                                       |

`GET /audit?limit&before&since&until&action` takes up to 1000 entries (default
100). Pass `next` as `before` for the next, older page; it is null on the last.
`since` and `until` are epoch ms, `since` included and `until` not. `action` is
a prefix: `machine.` takes every machine entry, `host.outbid` just that one.

## Feedback

`POST /feedback` needs no credential, so a report gets through when signing in
is what broke. Send
`{"kind": "bug" | "feedback", "message", "replyTo"?, "context"?}`; `replyTo` is
an email address or an http(s) URL. It answers 201 with
`{id, surface, receivedAt}`, and 429 after ten reports a minute from one
address.

## Outside `/api/v1`

| Route                                   | Does                                                                   |
| --------------------------------------- | ---------------------------------------------------------------------- |
| `GET /install.sh`                       | The installer for `o41-rrun`                                           |
| `GET /install-server.sh`                | Installs the agent on a reserved machine; `sh -s uninstall` removes it |
| `GET /cli/manifest.json`                | The current release: its version and each platform's SHA-256           |
| `GET /cli/:platform`                    | The current `o41-rrun` binary, gzipped                                 |
| `GET /cli/:version/:platform`           | One release's binary, gzipped                                          |
| `GET /.well-known/openid-configuration` | rrun's OIDC issuer, for keyless Google Cloud                           |
| `GET /.well-known/jwks.json`            | Its signing keys                                                       |

Platforms are `linux-x64`, `linux-arm64`, `darwin-x64` and `darwin-arm64`.

---

rrun by 041 documentation. Every page: https://rrun.041.io/llms.txt
