# TFBthumb Beta API Reference

Substrate version this reference covers: `v0.2.2` (read live from `GET /api/v1/version`).

This is an evaluation API. Bounded-claim set is what is promised. Out-of-scope items are listed in [`LIMITS.md`](LIMITS.md) and apply by reference to every endpoint below.

---

## Authentication

Every endpoint that touches a run requires an API key in the `X-API-Key` request header.

```
X-API-Key: tfb_beta_<token>
```

Keys are issued per organisation, scoped to one evaluation, time-bounded, with a daily run cap. The token prefix tells you the tier:

| Prefix | Tier | What it allows |
|---|---|---|
| `tfb_playground_*` | playground | Public demo; consequential actions refused at the wrapper |
| `tfb_beta_*` | beta | Paid evaluation; consequential actions surface to your approver |

A revoked or expired key returns `403`. A daily-cap exhaustion returns `429`.

---

## Endpoints

### `GET /healthz`

Liveness probe. No auth.

```
200 {"ok": true, "version": "v0.2.2"}
```

### `GET /api/v1/version`

The substrate version the API is bound to. No auth.

```
200 {"substrate_version": "v0.2.2"}
```

### `POST /api/v1/run`

Start a new run. Returns immediately with a run id; the engine drives the agent in the background. Poll `GET /runs/{id}` for status.

Request:
```json
{
  "url": "https://example.com/contact",
  "task": "<plain English | JSON spec>"
}
```

The `task` field accepts two shapes:

- **Plain English** — for browse-only tasks. The agent observes the page; the deterministic RuleBrain has no fields/submit so the trace records observations, not actions.
- **Structured JSON** — for form-fill tasks. Schema:

  ```json
  {
    "intent": "Fill the contact form and submit it.",
    "fields": {"Email": "user@example.com", "Full name": "Eval User"},
    "submit": "Submit",
    "success_text": "Saved"
  }
  ```

  The keys mirror `RuleBrain.__init__` in the substrate. `fields` maps accessible names to desired values. `submit` is the accessible name of the consequential action. `success_text` is the substring the agent watches for in the page's status/aria-live signal to declare the task complete.

Response:
```json
{
  "run_id": "run_KvBz_3Xa8Q",
  "status": "queued",
  "tier": "beta",
  "substrate_version": "v0.2.2",
  "receipt_url": "/api/v1/runs/run_KvBz_3Xa8Q"
}
```

Errors:
- `401` missing or unknown key
- `403` revoked or expired key
- `429` daily cap exhausted

### `GET /api/v1/runs/{run_id}`

Read the current state of a run. Returns the full receipt — trace, ledger receipts, pending approvals, final summary.

Response (in-flight, awaiting approval):
```json
{
  "run_id": "run_KvBz_3Xa8Q",
  "tier": "beta",
  "status": "awaiting_approval",
  "task": "...",
  "url": "...",
  "substrate_version": "v0.2.2",
  "trace": [
    {"intent": {"action": "type", "target": "Email", "text": "..."}, "outcome": "acted"},
    {"intent": {"action": "type", "target": "Full name", "text": "..."}, "outcome": "acted"}
  ],
  "ledger_receipts": [
    {"seq": 0, "kind": "action", "verb": "click", "name": "Email", "decision": "allow"},
    ...
  ],
  "pending_approvals": [
    {
      "approval_id": "appr_aZ31Wm",
      "fingerprint": "...",
      "tier_name": "CONSEQUENTIAL",
      "verb": "click",
      "target_name": "Submit",
      "requested_at": 1781524800.123
    }
  ],
  "final_summary": "",
  "error": "",
  "finished_at": null
}
```

Errors:
- `401`/`403` auth as above
- `403` "not your run" if a different key opened this run
- `404` unknown run id

### `POST /api/v1/runs/{run_id}/approve`

Approve a pending consequential action. The wrapper mints a single-use HumanAuthority token bound to that fingerprint; the agent retries on its next loop iteration.

Request:
```json
{"approval_id": "appr_aZ31Wm"}
```

Response:
```json
{"ok": true, "approval_id": "appr_aZ31Wm", "decision": "approve"}
```

Errors:
- `401`/`403` auth as above
- `403` "not your run"
- `403` "playground tier does not support consequential approvals"
- `404` unknown run
- `409` approval already decided OR expired (the agent's 10-minute approval window passed)

### `POST /api/v1/runs/{run_id}/deny`

Same shape as `/approve`. The agent records the denial and halts cleanly with `final_summary` naming the refusal.

---

## Run lifecycle states

| Status | Meaning |
|---|---|
| `queued` | accepted, not yet started |
| `running` | agent loop is active |
| `awaiting_approval` | one or more consequential actions pending your decision |
| `done` | agent completed (whether by success or by explicit refusal) |
| `blocked` | agent halted on a no-progress trip or a denial |
| `error` | engine crashed; see `error` field |

Terminal states are `done`, `blocked`, `error`. A run that sits in `awaiting_approval` for more than 10 minutes auto-denies and moves to `blocked`.

---

## Ledger receipts

Every `ledger_receipts[]` entry is one row of the Ceiling's hash-chained ledger. The chain is the audit trail — every action, every effect, every block, every allow, with a sha256 of (prev_hash + canonicalized body). Tamper detection is mechanical: a flipped byte breaks the chain.

The shape of one entry:

```json
{
  "seq": 0,
  "ts": 1781524800.123,
  "kind": "action",        // "action" | "effect" | "authority_init"
  "verb": "click",         // intent verb (action) or HTTP method (effect)
  "name": "Submit",        // accessible name (action) or URL (effect)
  "tier": 2,               // 0=READ, 1=REVERSIBLE, 2=CONSEQUENTIAL
  "decision": "allow",     // "allow" | "block"
  "reason": "",            // "needs-human-token" | "frozen" | ...
  "fingerprint": "abc...", // sha256 fingerprint
  "token_nonce": "xyz",    // null when no token was needed
  "prev_hash": "...",
  "hash": "..."
}
```

You can verify the chain yourself by recomputing the hash of each row's body against its prev_hash. The substrate ships with `Ledger.verify()` that does this, but the receipt is open enough for any tool that speaks sha256.

---

## What this API does not do

See [`LIMITS.md`](LIMITS.md) for the full list. Highlights:

- No multi-tab orchestration. Each run is one page.
- No cross-origin iframe walking.
- No closed shadow root introspection.
- No GET-with-side-effects coverage at the wire gate.
- No LLM-Brain (yet). All runs use the deterministic RuleBrain.
- No unattended autonomy. Every consequential action surfaces.
