Neutral Dash service API

Neutral Dash service API

The authenticated /v1/dashboard/* surface is the server-to-server protocol for a dedicated DashboardBackend. It is separate from the same-origin /dash/v1/* browser BFF:

The TypeScript controller is DashboardNeutralApiController; ApiServer accepts it through the optional dashboardApi service. SessionApiClient provides the authenticated transport and RemoteDashboardBackend implements the same backend contract as embedded mode without importing in-process services.

Route parsing and dispatch live in src/api-dashboard-routes.ts (routeDashboardRequest), separate from ApiServer. That module never authenticates, never writes to a socket, and never reads an unbounded body: the caller supplies an already-bounded JSON reader and renders the returned status, data, headers, and optional response request id through the single shared envelope. ApiServer therefore remains the only owner of service-bearer admission, response bounds, and WebSocket upgrades, and the ordering guarantee above is structural rather than conventional.

Routes

Method/path Purpose
GET /v1/dashboard/capabilities neutral resources, effective limits, Rich availability, capability-gated TUI status, and optional schedule/diagnostics support for version negotiation
GET /v1/dashboard/diagnostics bounded browser-safe policy status and normalized recent request failures; no raw logs or sensitive content
GET /v1/dashboard/inventory bounded search/filter/page over public inventory rows
GET /v1/dashboard/inventory/{inventoryId} authenticated full info, including source path/ownership diagnostics
GET /v1/dashboard/inventory/{inventoryId}/transcript preview-only normalized projection, or an identity-matched unavailable resource with records: [], with optional exact fingerprint precondition
POST /v1/dashboard/inventory/{inventoryId}/activate durable preview/reuse/direct/fork activation ticket
GET /v1/dashboard/activation/{ticketId} activation ticket
POST /v1/dashboard/session/{sessionRef}/export durable export-as-new or guarded append-back ticket
GET /v1/dashboard/export/{ticketId} export ticket
POST /v1/dashboard/session/{sessionRef}/lease renew the exact cooperative ownership lease
POST /v1/dashboard/session-drafts persist a validated new-session draft without runtime/model/tool work
GET|DELETE /v1/dashboard/session-drafts/{draftId} inspect or revision-cancel a durable unsent draft
POST /v1/dashboard/session-drafts/{draftId}/send admit the exact first message through the injected materializer
GET /v1/dashboard/session-draft-send/{ticketId} inspect queued/running/terminal/indeterminate first-send truth
GET /v1/dashboard/session/{sessionRef}/tui capability-gated pi-daemon-tui.v1 WebSocket

HTTP successes use the normal session API envelope (apiVersion, requestId, hostInstanceId, ok, data). Errors use safe typed ApiErrorBody. Unknown minor fields remain additive.

Admission and idempotency

Activation/export and draft create/cancel/send bodies contain requestId and idempotencyKey. Draft cancel/send additionally require exact If-Match and expectedRevision; every first-send ticket retains the admitted revision and deterministic target session identity. The HTTP X-Request-Id and Idempotency-Key headers must match when present/required; a mismatch fails before mutation. The ownership service retains the durable ticket and enforces semantic key reuse. A running operation interrupted by a host crash is indeterminate and is never blindly resubmitted.

Direct/fork activation and export require exact inventory/managed source fingerprints. The service returns typed conflicts for stale sources, active controllers/mutations/writers, invalid leases, divergent history, and ownership collisions.

Inventory and transcript bounds

SessionInventory keeps its public API in src/session-inventory.ts, while its private critical paths are separated by responsibility: approved-root walking and descriptor-safe JSONL parsing in session-inventory-scanner.ts, authenticated hot-head/full-index codecs and validators in session-inventory-persistence.ts, and bounded filter/search/cursor/ordering logic in session-inventory-query.ts. Shared limits, persisted shapes, version constants, and typed errors live in session-inventory-contract.ts. The entry module re-exports the exact previous runtime/type surface; persisted magic values and byte layouts are unchanged. Request-path list/getInfo remain immutable in-memory operations and never scan the filesystem.

Inventory query parameters are limit, opaque cursor, bounded search, CSV sourceKind/runtime, unread, and modifiedAfter. Transcript parameters are limit, opaque cursor, direction, leafId, and fingerprint. The controller resolves the authenticated inventory information resource and passes only its canonical path plus exact current fingerprint to TranscriptProjector. It never accepts an arbitrary client path. A retained inventory row without a projectable source is still a valid readonly resource: the route returns 200 with the exact inventory and managed generation identity, records: [], availability.state: "unavailable", unavailable freshness, and observerAttachAllowed: false. A quarantined generation also carries only its bounded safe recovery code. It never copies a source path, adapter descriptor, or invented history into that response. Projectable pages report current source freshness and may allow observer attach only when the managed generation is not quarantined.

When owner defaults are configured, neutral capabilities include only the effective browser-safe lazy-draft spec and content-free cwd/model/authority source labels. Pi settings/config paths, package values, explicit resource paths, and host settings never cross the service boundary. Draft create and restart-time materialization independently enforce the current owner policy.

Every effective bound is returned by neutral capabilities. Request bodies use the existing API body limit; response serialization uses the same pre-allocation bound as all other API records. Because one projected transcript may legitimately exceed the per-response HTTP envelope, RemoteDashboardBackend intentionally fetches at most three bounded records per neutral request and combines opaque older/newer pages up to the caller's requested record limit. A single record remains below the published record bound; dedicated mode therefore preserves valid multi-megabyte transcript output without raising an unbounded client response limit.

Rich attach and prompt-free hydration

Dedicated Rich panes use the existing pi-daemon-rpc.v1 attachment. Browser, SDK, Pi Droid, and external-canary clients must require matching identities, availability.state: "available", current freshness, no quarantine, and observerAttachAllowed: true before opening even a readonly observer. An unavailable transcript remains a successful host/session listing and empty readonly view; it is not an attach request. The remote backend sets hydrate=true explicitly: the daemon reopens a retained durable session through its persisted catalog/configuration policy, without submitting a prompt, and holds a renewable residency lease for the lifetime of the shared attachment. Ordinary RPC clients that omit hydrate remain resident-only.

One upstream framed socket is coalesced per managed session/generation. Opaque cursors survive bounded reconnect; a host/generation/retention mismatch is an explicit replay gap followed by a fresh REST-backed transcript plus atomic RPC snapshot. Responses are private and never replayed. Any command, extension UI answer, TUI input, resize, or control operation sent before a connection loss but missing its acknowledgement is indeterminate, not blindly resubmitted.

TUI negotiation

The neutral TUI route uses exactly one WebSocket subprotocol:

pi-daemon-tui.v1

When the server-side interactive view/UI-broker seam is unavailable, capabilities advertise tui.available: false with a safe reason and upgrades fail 501 tui_unavailable. A missing/wrong subprotocol fails 426 and advertises the required protocol. Service-bearer authentication still happens first.

The attachment implementation is injected as DashboardTuiAttachmentManager; this API slice does not spawn a second Pi process or create another session writer.

Client methods

SessionApiClient provides typed methods:

The client retains the existing loopback/plaintext policy, bounded aggregate response size, request timeout, service-bearer header, and safe error mapping.

Machine-readable contracts

Diagnostics are intentionally not an arbitrary log-file API. The service owns a 128-event in-memory ring, normalizes identifier-bearing paths to route templates, replaces unknown error text with a fixed safe message, and publishes only booleans/counts about effective configuration. Raw launch logs, prompts, model output, filesystem paths, credentials, environment values, request bodies, and bearer material never enter the ring. Diagnostics reads are authenticated before route lookup and the browser BFF restricts them to global administrators.