HTTP API

Loopback REST and SSE endpoints used by the UI, CLI, and agents.

Use the session base URL from diffing url. CLI and MCP clients target loopback sessions. Authenticate with the session cookie (browser) or x-diffing-token header.

Authentication and browser boundaries

Loopback binds reject non-loopback Host headers on HTML and API routes. Any supplied Origin must match the request origin; mismatches return 403. Responses set Cache-Control: no-store, X-Content-Type-Options: nosniff, and Referrer-Policy: no-referrer.

Loopback browsers can still bootstrap from the review page. When authentication is enabled on a non-loopback bind, HTML and deep links also require the session header or cookie; unauthenticated requests return 401 without disclosing the credential in their body or cookies. The browser fetch wrapper attaches its token only to same-origin /api/ requests and preserves existing request headers.

--insecure-no-auth explicitly disables authentication, including on loopback. The CLI requires it for wildcard binds (0.0.0.0/::). It does not disable Host/Origin checks. Prefer loopback; there is no automatic authenticated LAN login flow.

Native file access

Selected local working-tree previews, saves, edits, suggestions and uploaded-image operations use a pinned native directory capability. Descendant symlinks and .git path components are denied; already-decoded API paths are not decoded again. The helper is discovered only in verified installation/source locations, never on PATH. Missing/incompatible helpers fail closed with 503; there is no Node fallback for these operations. Source contributors build it with pnpm build:tui:debug.

This is not project-wide containment: untracked/EditorConfig reads and trusted external Git/editor/LSP/search tools remain outside this guarantee. The continuation checklist is docs/hardening-status.md in the source checkout.

MethodPathBehavior
GET/api/file-content?path&versionRaw bytes; required version=old|new; sandbox CSP prevents active preview content acting as the review app
GET/api/file-text?path&version{content, missing, hash}; hash covers returned bytes; NUL in the first 8192 bytes returns 415
POST/api/save-file{filePath, content, gitAdd?}; native replacement, optional Git stage; no hash precondition
POST/api/edit-save{filePath, content, baseHash?, anchorUpdates?}; optional optimistic pre-write hash check
POST/api/comments/:id/apply-suggestionReads current bytes, applies the suggestion and writes with the just-read hash

Limits: native files 50 MiB, save/edit JSON requests 70 MiB, uploaded images 10 MiB, multipart requests 11 MiB. Save paths are strings of at most 4096 UTF-16 code units without NUL. baseHash, when supplied, is 64 lowercase hex characters. At most 1024 anchor updates are accepted; IDs are nonempty, lines are nonnegative integers and optional positive range starts cannot exceed the end or accompany file-level line zero. Invalid payloads return 400 before writing.

PR/custom comparisons (revisions, pathspecs or show mode) reject these writes with 403. Hash mismatch returns 409 with conflict: true. Hash checks are not cross-process compare-and-swap. A reported metadata failure after a successful write returns 500 with fileSaved: true (edit-save also returns hash); reconcile metadata rather than reapply the file change. Failed optional staging returns 200, ok: true, and gitAddError because the file was saved.

Native failures return {error, code, outcomeUnknown}: invalid-path/invalid-request/not-file 400, denied 403, not-found 404, conflict 409, too-large 413, protocol 502, unavailable/busy 503, timeout 504, io 500. Validation and other endpoints can use simpler error bodies. If outcomeUnknown is true, inspect current file state before retrying; never blindly replay a write. Transport failure stays failed until explicit reset/restart.

Preview limitation: local new reads the current working tree and old reads HEAD; exact staged/revision/commit-series sides are deferred. PR previews use the session base/head SHAs. Working-tree missing results only come from native not-found, but some legacy Git failures still appear as missing. Source completeness is not yet guaranteed by bounded-inspect metadata.

Handoff

MethodPathRole
POST/api/review/sendHuman releases waiters; increments round
GET/api/review/awaitLong-poll (sinceRound, timeoutMs ≤ 50000)
GET/api/review/statusRound / waiters snapshot
GET/api/review/historyIn-memory round history; resets on server restart

send body

{ "generalComment": "Optional markdown summary" }

await response (released)

{
  "status": "released",
  "payload": {
    "round": 4,
    "sentAt": 1782782782782,
    "commentXml": "<code-review-comments>…</code-review-comments>",
    "openCount": 0,
    "comments": [],
    "mode": "standard"
  }
}

Bounded diff inspect

MethodPathRole
GET/api/diff/summary?excludeTotals, kind counts, top-level directories, complete, and omittedPaths when optional reads were skipped. exclude=lockfiles drops lock/generated basenames from counts only
GET/api/diff/files?path&cursor&limitPaged file metadata. path is a git pathspec-ish glob; nextCursor indexes the filtered list
GET/api/diff/hunks?file|path&cursor&limit&generationHunk metadata. path XOR file; 0 matches → 404, many → 409
GET/api/diff/slice?file|path&start&maxLines&maxBytes&generationBounded logical rows
GET/api/diff/search?q&path&file&row&limit&maxBytes&generationLiteral search; optional path limits files

Comments

MethodPathRole
GET/api/commentsList threads
POST/api/commentsCreate (filePath, side, lineNumber, body, optional startLineNumber, severity)
PUT/api/comments/:idEdit body or { status }
DELETE/api/comments/:idDelete
POST/api/comments/resolve-allResolve all open
POST/api/comments/:id/repliesReply
PUT/api/comments/:id/replies/:replyIdEdit reply
DELETE/api/comments/:id/replies/:replyIdDelete reply
POST/api/comments/:id/apply-suggestionApply suggestion fence

Comment validation

Creates require a nonempty string filePath (at most 4096 UTF-16 code units, no NUL), side: "additions" | "deletions", nonnegative integer lineNumber, and nonblank string body. lineNumber: 0 is file-level. Optional startLineNumber must be positive, no greater than lineNumber, and absent for file-level comments. Missing lineContent defaults to ""; supplied context must be a string. Optional severity is blocking, nit, question, praise, or none.

Body limit: 65,536 UTF-16 code units. Context limit: 262,144. Requests under /api/comments and its child routes are limited to 1,048,576 bytes. Invalid fields or malformed JSON return 400; oversized requests return 413. Rejected requests leave the comment store unchanged.

Comment updates require a valid body or status: "open" | "resolved". Reply creation and editing require a nonblank body within the same limit. Replies accept optional role: "user" | "agent" and a nonempty string model of at most 256 UTF-16 code units. Without an explicit role, a model implies agent; otherwise the role is user.

Agent progress

MethodPath
POST/api/agent/progress
GET/api/agent/progress
{ "message": "Working…", "model": "…", "pct": 40 }

Live SSE

GET /api/live — events: heartbeat, change, comments, plans, mockups, agent-status, plan-review-status, mockup-review-status.

POST /api/search — { scope, query, limit, regex, changedPaths? }
POST /api/search/track — frecency update

Attachments

POST /api/attachments (multipart) · GET /api/attachments/:filename
Stored under per-repo attachments/ through the native storage-root capability. PNG, JPEG, WebP and GIF only; images are limited to 10 MiB and multipart requests to 11 MiB. Magic-byte/type mismatch on upload returns 415. GET only serves regular files with an allowed image signature; missing/invalid images return 404, traversal returns 403, and native denial/unavailability remains 403/503. Codex receives captured image bytes as a data URL rather than a local-image path to reopen.

AI assistance

Loopback-only review assistant used by the web UI. Inference requires trigger: "user" — clients must not call /api/ai/run from lifecycle, hover, selection, refresh, or navigation events.

MethodPathRole
GET/api/ai/connectionsConnection status for Codex, Claude, OpenCode, Cursor, Grok
GET/api/ai/modelsModels from connected sources
POST/api/ai/connections/:source/keyStore direct API key (apiKey, optional remember)
POST/api/ai/connections/:source/loginReturn CLI setup command (route: subscription | …)
POST/api/ai/connections/:source/configure-runtime-keyReturn runtime BYOK setup command
DELETE/api/ai/connections/:sourceDisconnect / clear stored key
GET/api/ai/conversations?surface&scopeKeyList conversation summaries
POST/api/ai/conversationsCreate (surface, scopeKey, optional title / modelId)
GET / PUT / DELETE/api/ai/conversations/:idRead / update (title, draft, modelId, turns) / delete
POST/api/ai/runStart inference; streams SSE events (start, text-delta, warning, error, complete)
POST/api/ai/runs/:id/cancelCancel an in-flight run

/api/ai/run body (required fields)

{
  "trigger": "user",
  "conversationId": "…",
  "modelId": "…",
  "surface": "diff",
  "action": "ask",
  "prompt": "optional",
  "context": { "kind": "diff", "patch": "…" }
}

surface is diff | pr-diff | plan. Context attachments: ≤ 8 @ files / 64 KB; ≤ 8 line ranges / 64 KB; ≤ 4 images. Conversations live in per-repo ai-conversations.json.

Plans

Plan CRUD and comments live under /api/plans… (list, get, versions, submit, comments, review status). Prefer CLI/MCP for agents; use HTTP when embedding or debugging.

Mockups

Comment scope = version + screen + viewport (desktop|tablet|mobile); viewport is part of every posted comment and every inspect filter. Prefer CLI/MCP for agents; use HTTP when embedding or debugging.

MethodPathRole
GET/api/mockups[?include=comments|full]Compact summaries by default; include=comments adds threads (single-op lookup helpers); include=full returns raw records (compatibility)
POST/api/mockupsSubmit (html or screens[]; id resubmits → version++)
GET/api/mockups/:idOne mockup (screens + comments)
PUT / DELETE/api/mockups/:idUpdate / delete
GET/api/mockups/:id/versions · /versions/:nVersion metadata / historical body
GET/api/mockups/:id/inspect?view&status&screen&viewport&version&id&cursor&limit&contextBounded reads — view=summary|comments|comment|screen, context=none|anchor|source (default anchor), bodies truncate at 400 chars, nextCursor pagination
GET/api/mockups/:id/screens/:screenId/document?version&viewportServed screen (injected probe; nonce echoed back on comment posts)
PUT/api/mockups/:id/screens/:screenIdOne-screen upsert (html, optional label, expectedVersion)
PATCH/api/mockups/:id/screens/:screenIdExact-text patch (expectedText, replacement) or region replace (region, replacement); expectedVersion optional; 0 matches → 409 exact-text-not-found / region-not-found
DELETE/api/mockups/:id/screens/:screenId[?expectedVersion]One-screen remove (refuses last screen)
POST/api/mockups/:id/threads/batchAtomic thread batch { operations: [{ op: reply|edit|delete|resolve|unresolve, commentId, replyId?, body?, role?, model? }] } — all validated before any applies; thread ops never bump the version
POST/api/mockups/:id/commentsCreate comment (kind, screenId, body, viewport, anchor fields, optional nonce)
PUT / DELETE/api/mockups/:id/comments/:commentIdEdit body/status / delete
POST / PUT / DELETE/api/mockups/:id/comments/:commentId/replies[/:replyId]Reply / edit / delete reply
POST/api/mockups/:id/decisionSubmit review (decision, optional decisionComment, mode, focused screen/viewport); releases waiters
GET/api/mockup-review/await · /statusLong-poll verdict / round snapshot

Screen ops return the updated mockup; expectedVersion mismatch aborts with 409 (version-mismatch, nothing applied). Verdicts release waiters via GET /api/mockup-review/await and GET /api/mockup-review/status.

Other

PathRole
POST /api/open-fileLaunch editor (vscode/zed/vim/neovim/default)
Git/IDE helpersDiff options, settings persistence, etc.

Deep endpoint catalog remains in repository docs/cli.md §11 for rare routes.