Documentation
¶
Overview ¶
Package transports is the Harbor Protocol wire-transport seam — the Phase 60 binding of RFC §5.4's resolved transport choice (SSE for the event stream + REST/JSON for the control surface) onto net/http.
The seam (CLAUDE.md §3, §4.4) ¶
Each wire transport is its own sub-package:
- internal/protocol/transports/control — REST/JSON over the transport-agnostic protocol.ControlSurface (Phase 54).
- internal/protocol/transports/stream — SSE over the events.EventBus (Phase 05).
This package composes them: NewMux wires both handlers into one *http.ServeMux a future server (the `harbor dev` subcommand — Phase 64) mounts. The layout is the §4.4-style seam read for transports rather than drivers: RFC §5.4 explicitly leaves WebSocket as an additive alternate transport. Adding it is a third sub-package (internal/protocol/transports/websocket) plus one more mux entry here — neither `control` nor `stream` is reshaped, and no caller outside this package changes. There is NO driver-registry / factory ceremony: the transport set is small, closed, and mounted in code at boot, not resolved by name from config — the same posture Phase 54 took for the ControlSurface (D-072).
Why no http.Server here ¶
Phase 60 ships the transport HANDLERS, not the server that listens. `harbor dev` (Phase 64) owns the net.Listener, the graceful-shutdown lifecycle, and the /healthz + /readyz endpoints; it calls NewMux and serves the result. Keeping the listen/shutdown lifecycle out of this package means the transports are exercised end-to-end today via httptest (the package + integration tests) without waiting on the server phase — the same decoupling Phase 54 used to stay testable ahead of its wire binding.
Concurrent reuse (D-025) ¶
The *http.ServeMux NewMux returns is immutable after construction and both mounted handlers are themselves D-025-safe compiled artifacts (control.Handler, stream.Handler). One mux serves N concurrent requests safely; concurrent_test.go pins it under -race.
Index ¶
- Variables
- func NewMux(cs *protocol.ControlSurface, bus events.EventBus, opts ...Option) (*http.ServeMux, error)
- type Option
- func WithAgentsService(s *agentsprotocol.Service) Option
- func WithAggregateClock(c events.AggregatorClock) Option
- func WithArtifactsSurface(s control.ArtifactsSurface) Option
- func WithAuthSurface(surface *auth.RotateSurface) Option
- func WithFlows(surface *flowprotocol.Surface) Option
- func WithKeepalive(d time.Duration) Option
- func WithLogger(l *slog.Logger) Option
- func WithMCPSurface(s control.MCPSurface) Option
- func WithMemory(store memory.MemoryStore, driverName string) Option
- func WithPauseList(coord pauseresume.Coordinator, store artifacts.ArtifactStore, ...) Option
- func WithPostureSurface(s control.PostureSurface) Option
- func WithRedactor(r audit.Redactor) Option
- func WithRunsService(s *runsprotocol.Service) Option
- func WithSearch(s control.SearchSurface) Option
- func WithSessionsService(s *sessionsprotocol.Service) Option
- func WithTasksService(s *tasksprotocol.Service) Option
- func WithToolsService(s *toolsprotocol.Service) Option
- func WithValidator(v auth.Validator) Option
- func WithoutValidator() Option
Constants ¶
This section is empty.
Variables ¶
var ErrMisconfigured = errors.New("transports: NewMux missing a mandatory dependency")
ErrMisconfigured — NewMux was called with a nil ControlSurface or a nil EventBus. Both are mandatory: the former feeds the REST control transport, the latter feeds the SSE event transport. Fails closed (CLAUDE.md §5) rather than mounting a half-built mux.
Functions ¶
func NewMux ¶
func NewMux(cs *protocol.ControlSurface, bus events.EventBus, opts ...Option) (*http.ServeMux, error)
NewMux composes the Protocol wire transports into a single *http.ServeMux:
- POST /v1/control/{method} — the REST/JSON control surface.
- GET /v1/events — the SSE event stream.
All three configuration choices are mandatory:
- a non-nil ControlSurface,
- a non-nil EventBus,
- and EITHER `WithValidator(v)` (the production posture — JWT bearer auth at the edge) OR `WithoutValidator()` (the explicit, test-only escape hatch for the Phase 60 trust-based posture).
A missing dependency — including the auth choice — fails loud with ErrMisconfigured rather than mounting a half-built mux or an unauthenticated production surface (CLAUDE.md §13 "Test stubs as production defaults on operator-facing seams"; PR #91).
The returned mux is immutable after construction (D-025) and safe to share across N concurrent requests — a future server (`harbor dev`, Phase 64) mounts it and owns the listen / shutdown lifecycle.
Types ¶
type Option ¶
type Option func(*muxConfig)
Option configures NewMux.
func WithAgentsService ¶
func WithAgentsService(s *agentsprotocol.Service) Option
WithAgentsService wires the Phase 73e (D-124) `agents.*` handler into NewMux — the eight Console Agents-page methods (`agents.list` / `agents.get` / `agents.tools` / `agents.memory` / `agents.governance` / `agents.skills` / `agents.permissions` / `agents.metrics`).
The service is OPTIONAL so existing call-sites compile unchanged. When unsupplied, the `POST /v1/agents/{method}` route is NOT mounted — the smoke script's `skip_if_404` keeps preflight green on a partial build. Production wiring (`harbor dev`) supplies it so the Console Agents page (Phase 73e) has a live surface. When supplied AND WithValidator is set, the route is wrapped in auth.Middleware like every other transport.
All eight `agents.*` methods are READ-ONLY projections of the Agent Registry — the five agent-control verbs (Pause / Drain / Restart / Force-Stop / Deregister) are the EXISTING shipped `registry.*` control verbs (D-066), not new methods (CLAUDE.md §13). A nil service is treated as "WithAgentsService not supplied".
func WithAggregateClock ¶
func WithAggregateClock(c events.AggregatorClock) Option
WithAggregateClock injects a deterministic clock into the events.aggregate handler's underlying *events.Aggregator. Production callers do not use this; the default real clock (UTC) is correct. Tests that exercise the aggregate path with backdated events use this to anchor the aggregator's "now" deterministically — the same posture WithKeepalive takes for the SSE keepalive interval.
func WithArtifactsSurface ¶
func WithArtifactsSurface(s control.ArtifactsSurface) Option
WithArtifactsSurface wires the Phase 73l (D-120) artifacts dispatcher into the control transport. When supplied, the control handler routes the three artifacts methods — `artifacts.list`, `artifacts.put`, `artifacts.get_ref` — to the artifacts surface instead of falling through to the task-control ControlSurface.
The option is OPTIONAL so existing call-sites compile unchanged. When not supplied, the control transport rejects artifacts calls with CodeUnknownMethod (HTTP 404) — the 404 → SKIP path the smoke script relies on. Production wiring (`harbor dev`) supplies it so the Console Artifacts page (Phase 73l) has a live surface. A nil surface is treated as "WithArtifactsSurface not supplied".
func WithAuthSurface ¶
func WithAuthSurface(surface *auth.RotateSurface) Option
WithAuthSurface wires the Phase 73m (D-129) `auth.*` handler into NewMux — the single net-new Console Settings-page method (`auth.rotate_token`). surface is the *auth.RotateSurface the `POST /v1/auth/rotate_token` route dispatches through.
The option is OPTIONAL so existing call-sites compile unchanged. When not supplied (or supplied a nil surface), the `auth.*` route is NOT mounted — the smoke script's `skip_if_404` keeps preflight green on a partial build. Production wiring (`harbor dev` / `harbor console`) supplies it so the Console Settings page "Rotate token" action has a live surface. When supplied AND WithValidator is set, the route is wrapped in auth.Middleware like every other transport — `auth.rotate_token` then gates on the verified `auth.ScopeAdmin` claim (D-079). A nil surface is treated as "WithAuthSurface not supplied".
func WithFlows ¶
func WithFlows(surface *flowprotocol.Surface) Option
WithFlows wires the Phase 73i (D-117) Console Flows-page handler into NewMux. surface is the transport-agnostic flowprotocol.Surface the six `POST /v1/flows/*` routes dispatch through.
The option is OPTIONAL so existing call-sites compile unchanged. When not supplied (or supplied a nil surface), the Flows routes are NOT mounted — the smoke script's `skip_if_404` keeps preflight green on a partial build. Production wiring (`harbor dev`) supplies it so the Console Flows page (Phase 73i) has a live surface.
Five of the six routes are read-only; `POST /v1/flows/run` mutates and the Surface gates it on the verified `auth.ScopeAdmin` claim (D-079 closed two-scope set — no new scope is minted). When WithValidator is also set, the handler is wrapped in auth.Middleware like every other transport.
func WithKeepalive ¶
WithKeepalive overrides the SSE keepalive-comment interval on the stream transport. A non-positive value is ignored.
func WithLogger ¶
WithLogger sets the slog.Logger both transport handlers log to. A nil logger is ignored; the handlers fall back to slog.Default().
func WithMCPSurface ¶
func WithMCPSurface(s control.MCPSurface) Option
WithMCPSurface wires the Phase 73k (D-119) MCP-Connections dispatcher into the control transport. When supplied, the control handler routes the twelve `mcp.servers.*` methods to the MCP surface instead of falling through to the task-control ControlSurface.
The option is OPTIONAL so existing call-sites compile unchanged. When not supplied, the control transport rejects MCP calls with CodeUnknownMethod (HTTP 404) — the 404 → SKIP path the smoke script relies on. Production wiring (`harbor dev`) supplies it so the Console MCP Connections page (Phase 73k) has a live surface. A nil surface is treated as "WithMCPSurface not supplied".
func WithMemory ¶
func WithMemory(store memory.MemoryStore, driverName string) Option
WithMemory wires the Phase 73j (D-118) three `memory.*` read routes into NewMux. store is the memory.MemoryStore the Console Memory page projects from (Phases 23–25); driverName is the configured memory- driver name surfaced on each row. The memory handler reuses the ArtifactStore + heavy-content threshold supplied via WithPauseList for the D-026 heavy-value bypass — so WithPauseList must also be set for the three `memory.*` routes to mount.
store is OPTIONAL — when it is nil (or the ArtifactStore / heavyThreshold from WithPauseList are unset), the three `memory.*` routes are left UN-mounted (the route's smoke `skip_if_404` keeps preflight green on a partial build). When supplied correctly the routes `POST /v1/memory/list` / `/get` / `/health` are mounted and, when WithValidator is also set, wrapped in auth.Middleware like every other transport.
The three methods are READ-ONLY (CLAUDE.md §13) — they project the shipped MemoryStore surface; no mutation path is mounted.
func WithPauseList ¶
func WithPauseList(coord pauseresume.Coordinator, store artifacts.ArtifactStore, heavyThreshold int) Option
WithPauseList wires the Phase 72e `pause.list` snapshot handler into NewMux. coord is the unified pause/resume Coordinator (Phase 50) the snapshot projects from; store is the ArtifactStore the D-026 heavy-content bypass routes oversized pause payloads through; heavyThreshold is the configured heavy-content byte size (cfg.Artifacts.HeavyOutputThresholdBytes).
All three are required together — supplying the option with a nil coord, a nil store, or a non-positive threshold leaves the `pause.list` route UN-mounted (the route's smoke `skip_if_404` keeps preflight green on a partial build). When supplied correctly the route `POST /v1/pause/list` is mounted and, when WithValidator is also set, wrapped in auth.Middleware like every other transport.
pause.list is READ-ONLY against the Coordinator (CLAUDE.md §7 rule 4 / §13) — it reads the shipped pause-coordinator state, it does not reinvent pause coordination.
func WithPostureSurface ¶
func WithPostureSurface(s control.PostureSurface) Option
WithPostureSurface wires the Phase 72f / 72g (D-111 / D-112) posture dispatcher into the control transport. When supplied, the control handler routes the seven posture methods — the five `runtime.*` / `metrics.*` reads plus `governance.posture` / `llm.posture` — to the posture surface instead of falling through to the task-control ControlSurface.
The option is OPTIONAL so existing call-sites compile unchanged. When not supplied, the control transport rejects posture calls with CodeUnknownMethod (HTTP 404) — the 404 → SKIP path the smoke script relies on. Production wiring (`harbor dev`) supplies it so the Console Settings page (Phase 73m) has a live surface. A nil surface is treated as "WithPostureSurface not supplied".
func WithRedactor ¶
WithRedactor wires the audit.Redactor into the control transport so the Phase 72b admin-impersonation gate can publish a redacted `audit.admin_scope_used` event onto the bus on every accepted impersonation. The bus is already mandatory at NewMux (it feeds the SSE event transport); the redactor is the second half of the pair the control transport needs to enable impersonation.
The option is OPTIONAL at the type level so existing call-sites compile unchanged. When the redactor is not supplied, the control transport refuses impersonation requests fail-closed with CodeRuntimeError (CLAUDE.md §13 "Silent degradation"). Production wiring (the `harbor dev` boot path) SHOULD supply it.
A nil redactor is treated as "WithRedactor not supplied".
func WithRunsService ¶
func WithRunsService(s *runsprotocol.Service) Option
WithRunsService wires the Phase 73n (D-130) Console Playground-page handler into NewMux — the `POST /v1/runs/set_overrides` route.
The service is OPTIONAL so existing call-sites compile unchanged. When unsupplied, the `POST /v1/runs/{method}` route is NOT mounted — the smoke script's `skip_if_404` keeps preflight green on a partial build. Production wiring (`harbor dev`) supplies it so the Console Playground page (Phase 73n) can record next-message overrides. When supplied AND WithValidator is set, the route is wrapped in auth.Middleware like every other transport. A nil service is treated as "WithRunsService not supplied".
func WithSearch ¶
func WithSearch(s control.SearchSurface) Option
WithSearch wires the Phase 72c (D-108) search dispatcher into the control transport. When supplied, the handler routes the five `search.*` methods to s.Dispatch instead of falling through to the task-control ControlSurface.
The surface is OPTIONAL so existing call-sites compile unchanged. When unsupplied (or supplied a nil surface), the control transport rejects search calls with CodeUnknownMethod — the smoke script's `skip_if_404` keeps preflight green on a partial build. Production wiring (`harbor dev` / `harbor console`) supplies it so the five `search.*` methods serve live results. A nil surface is treated as "WithSearch not supplied".
func WithSessionsService ¶
func WithSessionsService(s *sessionsprotocol.Service) Option
WithSessionsService wires the Phase 73c (D-122) Console Sessions-page handler into NewMux — the two `POST /v1/sessions/*` routes (`sessions.list` / `sessions.inspect`).
The service is OPTIONAL so existing call-sites compile unchanged. When unsupplied, the `POST /v1/sessions/{method}` routes are NOT mounted — the smoke script's `skip_if_404` keeps preflight green on a partial build. Production wiring (`harbor dev`) supplies it so the Console Sessions page (Phase 73c) has a live surface. When supplied AND WithValidator is set, the route is wrapped in auth.Middleware like every other transport — a cross-tenant `sessions.list` filter then gates on the verified `auth.ScopeAdmin` claim (D-079). A nil service is treated as "WithSessionsService not supplied".
func WithTasksService ¶
func WithTasksService(s *tasksprotocol.Service) Option
WithTasksService wires the Phase 73d (D-123) `tasks.*` handler into NewMux — the two Console Tasks-page read methods (`tasks.list` / `tasks.get`).
The service is OPTIONAL so existing call-sites compile unchanged. When unsupplied, the `POST /v1/tasks/{method}` route is NOT mounted — the smoke script's `skip_if_404` keeps preflight green on a partial build. Production wiring (`harbor dev`) supplies it so the Console Tasks page (Phase 73d) has a live surface. When supplied AND WithValidator is set, the route is wrapped in auth.Middleware like every other transport — a cross-tenant `tasks.list` fan-in then gates on the verified `auth.ScopeAdmin` claim (D-079). A nil service is treated as "WithTasksService not supplied".
Both `tasks.*` methods are READ-ONLY (CLAUDE.md §13) — the Console Tasks page consumes the shipped Phase 54 task-control verbs for mutation; no `tasks.*` mutation path is mounted.
func WithToolsService ¶
func WithToolsService(s *toolsprotocol.Service) Option
WithToolsService wires the Phase 73f `tools.*` handler into NewMux — the seven Console Tools-page methods (`tools.list` / `tools.get` / `tools.describe` / `tools.metrics` / `tools.content_stats` / `tools.set_approval_policy` / `tools.revoke_oauth`).
The service is OPTIONAL so existing call-sites compile unchanged. When unsupplied, the `POST /v1/tools/{method}` route is NOT mounted — the smoke script's `skip_if_404` keeps preflight green on a partial build. Production wiring (`harbor dev`) supplies it so the Console Tools page (Phase 73f) has a live surface. When supplied AND WithValidator is set, the route is wrapped in auth.Middleware like every other transport — the two admin methods then gate on the verified `auth.ScopeAdmin` claim (D-079). A nil service is treated as "WithToolsService not supplied".
func WithValidator ¶
WithValidator wires the Phase 61 JWT auth.Validator into NewMux. BOTH transport handlers (REST control + SSE stream) are wrapped in auth.Middleware: every request must carry a verified `Authorization: Bearer <jwt>`; the middleware injects the verified identity + scopes into the request context.Context before the underlying handler runs.
A validator is **mandatory** — `NewMux` returns `ErrMisconfigured` when neither `WithValidator` nor `WithoutValidator` is supplied (PR #91 amendment to D-078 / CLAUDE.md §13 "Test stubs as production defaults on operator-facing seams"). A nil validator is treated as "WithValidator not supplied"; tests that legitimately need the unauthenticated path use `WithoutValidator()` explicitly.
func WithoutValidator ¶
func WithoutValidator() Option
WithoutValidator is the explicit, test-only escape hatch for cases that legitimately need the Phase 60 trust-based posture (the REST handler inherits `ControlSurface.Dispatch`'s identity-from-body gate, the SSE handler resolves identity from the `X-Harbor-*` carrier headers via `resolveIdentity`). It is used by Phase 60's own package tests + `test/integration/phase60_wire_transport_test.go` to assert the pre-auth transport surface still works.
PRODUCTION CODE MUST NEVER USE THIS OPTION. A Runtime that boots with `WithoutValidator` exposes an unauthenticated Protocol surface, which violates CLAUDE.md §13's "Test stubs as production defaults" rule. The option is named for grepability: an audit can find every production-side WithoutValidator call site at compile time.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package control is the Harbor Protocol REST/JSON control transport — the client→server half of the wire binding RFC §5.4 resolves to (SSE for events + REST/JSON for control).
|
Package control is the Harbor Protocol REST/JSON control transport — the client→server half of the wire binding RFC §5.4 resolves to (SSE for events + REST/JSON for control). |
|
Package cors is the Harbor Protocol CORS middleware — the Phase 83v security primitive that unlocks the D-091 multi-process Console+Runtime posture (Console on one origin attaches to a Runtime on a different origin) without weakening the browser-side enforcement contract.
|
Package cors is the Harbor Protocol CORS middleware — the Phase 83v security primitive that unlocks the D-091 multi-process Console+Runtime posture (Console on one origin attaches to a Runtime on a different origin) without weakening the browser-side enforcement contract. |
|
Package stream — Wave 13 additions (Phase 73e): the `agents.*` HTTP handler.
|
Package stream — Wave 13 additions (Phase 73e): the `agents.*` HTTP handler. |