# T10.4 — HTTP API surface | Field | Value | |---|---| | Phase | P10 — Orchestration | | Size | L — over 3 days | | Status | Not started | | Flags | — | | Spec | inlined below | | Blocks | T10.6, T10.7, T10.9 | ## Goal `axum` HTTP service binding one method table — spawn / status / stream / episode / transcript / rewind / cancel / retry / goal / metrics — to `poimen-sdk` ports. One method set: HTTP today, the same table T9.2's stdio JSON-RPC binds to later. ## Facts (inlined — no spec read needed) - **The protocol is a published artifact** — INDEX.md's own rule for the sidecar applies equally here: methods added, never repurposed; fields added, never removed, from the first commit. - HTTP is a transport binding onto `poimen-sdk` (T9.1), not a bespoke access layer with its own business logic. Handlers translate, they do not decide. - `/v1/problems` and `/v1/plans/{id}/approve|reject` route to T10.6's workflow, not implemented here — this task defines the routes and request/response contracts; T10.6 fills in plan-draft semantics. - `/v1/runs/{id}/stream` tails the same committed log the outbox relay reads — SSE, not a second read path. ## Steps 1. Stand up the `axum` service, one route module per resource: runs, plans, goals, metrics. 2. `POST /v1/runs` → spawn_run. Request `{tenant, workflow_ref, input, goal_id?, dispatch_key?}`. Response `{run_id, status}`. 3. `GET /v1/runs/{id}`, `/episode` (T1.7), `/attempts/{step}/{n}/transcript` (T1.6 `BlobRef` dereference), `/stream` (SSE). 4. `POST /v1/runs/{id}/rewind` (T2.3), `/cancel`, `/attempts/{step}/retry` (T1.4). 5. `GET /v1/goals/{id}` (T10.1 `GoalView`). 6. `GET /v1/metrics`, `/v1/runs/{id}/metrics` (T8.2's existing list, scoped). 7. Auth/tenant resolution: reject any request without a resolvable `TenantId` before touching the kernel — no implicit tenant. 8. Version the route set from commit one: additive only, document the rule inline in the router module. ## Acceptance - Full request/response cycle for spawn → poll → episode → transcript against an embedded `redb` backend. - Rewind, cancel, retry each produce the same log-level effect as calling the underlying kernel function directly — no HTTP-layer divergence. ## Verify **Harness:** embedded `redb`, stub model, `axum` test client (`tower::ServiceExt::oneshot`). **Integration test** — `tests/it_http_api_surface.rs`: 1. `POST /v1/runs`; assert 200 with `run_id`; assert kernel shows `Scheduled`. 2. `GET /v1/runs/{id}` until terminal; assert status matches kernel state exactly at each poll. 3. `GET /v1/runs/{id}/episode`; assert shape matches T1.7's `EpisodeView`, includes failed attempts. 4. `GET /v1/runs/{id}/attempts/{step}/{n}/transcript`; assert returned bytes match `BlobStore` content directly (T1.6). 5. `POST .../rewind`; assert new `BranchId` created, parent branch still readable via all-branches. 6. `POST .../retry`; assert new `AttemptNo`, prior attempt record unmodified. 7. `POST .../cancel` mid-run; assert `Cancelled` reached (matches T1.3's cancel matrix). 8. Request with no resolvable tenant; assert rejected before any kernel call — instrument kernel entrypoints, assert zero calls. **Command:** `cargo test -p api http_api_surface` **False pass:** - Asserting HTTP 200 without checking underlying kernel state — a route that accepts and silently drops a request passes this. - Testing transcript retrieval only for a successful attempt — failed-attempt transcripts are the debugging case that matters most. ## Traps - Putting business logic (retry policy, plan validation) in the HTTP handler instead of the kernel/port layer. The handler is a transport, not a second brain. - Resolving `BlobRef`s eagerly inside `/episode` "for convenience" — T4.3's regression, one layer up. --- Background (not required to do this task): [rust-agentic-sys.md](../../../rust-agentic-sys.md) §8.5, §10.1 · [../../INDEX.md](../../INDEX.md) (embedding contract, ~line 60-86) · [T1.7-episode-query-surface.md](T1.7-episode-query-surface.md) · [T1.6-prompt-and-output-blob-capture.md](T1.6-prompt-and-output-blob-capture.md)