Skip to documentation
Documentation navigation

Documentation navigation

Documentation / guides

Overview

Expose Harness Session reads and controls through the built-in HTTP server.

developer

github.com/looprig/harness/pkg/serve is the HTTP composition seam for a live Harness session. It does not import pkg/session, a store, or an inference implementation. The composition root supplies a Rig for live sessions and a Reader for durable reads, then chooses the complete or read-only surface.

Request planes

The complete handler has three deliberately separate planes:

PlaneRoutesDependencyMeaning
DiscoveryGET /v1/capabilitiesread planeProtocol version and feature list.
Live/controlcreate, restore, input, interrupt, gates, eventsRig plus an in-process registryOperates only on sessions live in this process.
Durable readslist, status, journalReaderReads catalog or journal state without consulting the registry.

ReadHandler registers only discovery and durable reads. A control path is not “disabled” by a runtime check in that mode: it is absent from the mux and therefore returns the standard net/http 404.

%%{init: {"theme":"dark"}}%%
flowchart LR
    C[HTTP client] --> M[method plus path mux]
    M --> A[auth middleware]
    A --> B[body cap middleware]
    B --> L[Live handlers]
    B --> R[Read handlers]
    L --> G[LiveSession registry]
    L --> F[Rig NewSession or RestoreSession]
    R --> D[Reader durable projection or journal]
    L --> S[SSE subscription]

Authentication is applied before the body cap and before a route handler. A live lookup releases the registry lock before calling the session. A read never needs a live session on the current process.

Route contract

These are the ten patterns registered by Handler. The method is part of each Go 1.22 pattern. A matching path with the wrong method is a 405 from the mux; an unknown path is a plain 404 before a serve handler runs.

Method and pathRequest body or querySuccess
GET /v1/capabilitiesnone200 application/json, protocol document
POST /v1/sessionsoptional {"blocks":[...]} and optional Idempotency-Key201, {"session_id": "...", "command_id": "..."}; command_id is omitted for idle create
GET /v1/sessionsskip default 0, limit default 100200, SessionList
POST /v1/sessions/{sid}/restorenone200, {"session_id":"..."}
POST /v1/sessions/{sid}/inputrequired non-empty {"blocks":[...]}200, {"command_id":"..."}
POST /v1/sessions/{sid}/interruptnone200, {"interrupted":true|false}
POST /v1/sessions/{sid}/gates/{gid}{"action":"...","values":{...}}202, {} after durable acceptance
GET /v1/sessions/{sid}/eventsnone200 text/event-stream
GET /v1/sessions/{sid}/statusnone200, SessionStatus
GET /v1/sessions/{sid}/journalfrom_journal_seq default 0, limit default 100200, EventJournalPage

The complete capability response is stable and ordered:

{"protocol":"looprig.serve","version":1,"features":["journal","live_sse","ephemeral_sse","gate_response"]}

The read-only response advertises features: ["journal"]. Capability discovery is not health, tenancy, or authentication status.

Compose the handler

The public contracts are intentionally narrow:

type LiveSession interface {
	SessionID() uuid.UUID
	Submit(context.Context, []content.Block) (uuid.UUID, error)
	SubscribeEvents(event.EventFilter) (event.Subscription, error)
	RespondGate(context.Context, gate.GateResponse) error
	Interrupt(context.Context) (bool, error)
}

type Rig[S LiveSession, O any] interface {
	NewSession(context.Context, ...O) (S, error)
	RestoreSession(context.Context, uuid.UUID) (S, error)
}

type Reader interface {
	ListSessions(context.Context, Page) (SessionList, error)
	ReadStatus(context.Context, uuid.UUID) (SessionStatus, error)
	ReadJournal(context.Context, uuid.UUID, JournalPage) (EventJournalPage, error)
}

func Handler[S LiveSession, O any](rig Rig[S, O], reads Reader, opts ...Option) http.Handler
func ReadHandler(reads Reader, opts ...Option) http.Handler
func Server(addr string, h http.Handler, opts ...ServerOption) (*http.Server, error)

An application can keep its concrete session type while serve remains independent of the session package:

func buildHTTP(rig serve.Rig[*mySession, mySessionOption], reader serve.Reader) (*http.Server, error) {
	// WithAuth is the request trust boundary. The callback may inspect headers,
	// a mTLS identity, or a proxy assertion and returns a non-nil error to deny.
	h := serve.Handler(rig, reader, serve.WithAuth(authenticate), serve.WithMaxBodyBytes(2<<20))
	// Server validates the address and applies the SSE-safe timeout defaults.
	return serve.Server("127.0.0.1:8080", h)
}

The returned handler carries an internal proof of whether WithAuth was installed. Pass it directly to Server; wrapping it in ordinary middleware removes that proof and makes a public bind fail closed.

Source and runnable proof

Run the package tests from the harness repository:

go test ./pkg/serve

← back to documentation