protocol

package
v1.17.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 23, 2026 License: Apache-2.0 Imports: 19 Imported by: 0

Documentation

Overview

Package protocol implements the `sessions.*` Protocol methods the Console Sessions page consumes:

  • sessions.list — paginated, filtered SessionRegistry projection.
  • sessions.inspect — full per-session snapshot for the detail view.
  • sessions.delete — own-session-only data-lifecycle erasure.
  • sessions.set_title — sets/clears a session's human-readable title.

The seam (CLAUDE.md §4.4)

The Service depends on the `Projector` interface, not on a concrete session registry. The V1 production implementation is `ListerProjector` (lister_projector.go) — a thin read-only projection over a `sessions.SessionLister`. A future remote / cross-runtime projector slots in behind the same interface without reshaping the Service.

Identity is mandatory (CLAUDE.md §6 rule 9)

Every method takes the wire request's `IdentityScope`. An incomplete triple fails closed with `ErrIdentityRequired` — there is no identity-downgrading knob. The Service NEVER reads identity from a package-level global; the triple flows in via the request.

Cross-tenant gating

A `sessions.list` whose `Filter.TenantIDs` names a tenant other than the caller's verified tenant requires the verified `auth.ScopeAdmin` claim. The Service receives an `adminScoped bool` the wire handler computes from the verified JWT scope set; a false value on a cross-tenant filter fails closed with `ErrCrossTenantScope`. There is NO `sessions.admin` scope — the closed two-scope set (`admin` + `console:fleet`) is the only admit surface. On a successful admin-scope query the Service emits an `audit.admin_scope_used` event.

Concurrent reuse

A constructed *Service is immutable after NewService and safe to share across N concurrent goroutines: it holds only the Projector reference + an optional bus + redactor + logger; every method's per-call state lives in the call's arguments and locals, never on the Service.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrIdentityRequired — the request carried an incomplete identity
	// triple. RFC §5.5 / CLAUDE.md §6 rule 9 — fails closed.
	ErrIdentityRequired = errors.New("sessions/protocol: identity scope incomplete")
	// ErrCrossTenantScope — a `sessions.list` filter named a tenant
	// outside the caller's verified tenant without the verified
	// `auth.ScopeAdmin` claim.
	ErrCrossTenantScope = errors.New("sessions/protocol: cross-tenant filter requires the admin scope claim")
	// ErrInvalidRequest — the request was structurally invalid (an
	// out-of-range limit, an unknown enum, a malformed cursor).
	ErrInvalidRequest = errors.New("sessions/protocol: invalid request")
	// ErrSessionNotFound — `sessions.inspect` / `sessions.delete` targeted
	// a session id with no record visible to the caller's identity scope.
	ErrSessionNotFound = errors.New("sessions/protocol: session not found")
	// ErrSessionRunning — `sessions.delete` was refused because the target
	// session has a RUNNING task (mirroring the GC never-reap-running
	// invariant). The handler maps it to CodeSessionRunning (409). No
	// store is touched on refusal.
	ErrSessionRunning = errors.New("sessions/protocol: cannot erase a session with a running task")
	// ErrErasureUnsupported — `sessions.delete` reached a Service that was
	// built without an Eraser (the runtime does not advertise the
	// CapSessionLifecycle capability). The handler maps it to
	// CodeUnknownMethod (404) so a client that probed the route detects
	// the unwired surface exactly as it would a missing route.
	ErrErasureUnsupported = errors.New("sessions/protocol: session erasure is not wired on this runtime")
	// ErrErasureRecordFailed — the erasure cascade's destructive steps
	// completed but the durable `session.erased` record-of-fact could not
	// be completed (a redactor refusal or a bus-publish failure).
	// Delete fails the whole call loud rather than reporting success with
	// a missing audit trail; the session's data IS gone, and a re-invoke
	// converges (it re-attempts only the record, no destructive step
	// re-runs). The handler maps it to CodeRuntimeError (500).
	ErrErasureRecordFailed = errors.New("sessions/protocol: erasure record-of-fact could not be durably completed")
	// ErrMisconfigured — NewService was called with a nil Projector.
	ErrMisconfigured = errors.New("sessions/protocol: NewService missing a mandatory dependency")
	// ErrTitleSetUnsupported — `sessions.set_title` reached a Service that
	// was built without a TitleSetter. The handler maps it to
	// CodeUnknownMethod (404), the same posture as ErrErasureUnsupported.
	ErrTitleSetUnsupported = errors.New("sessions/protocol: session title-set is not wired on this runtime")
)

Sentinel errors the Service returns. The wire handler maps each onto a canonical Protocol Code + HTTP status; in-process callers compare with errors.Is.

Functions

This section is empty.

Types

type CounterEnricher added in v1.14.0

type CounterEnricher struct {
	// contains filtered or unexported fields
}

CounterEnricher is the V1 production Enricher. It aggregates the per-session counters from raw data owned by subsystems one package over — SUMMING, not reading a shadow store:

  • total_cost_cents / total_tokens: summed from `llm.cost.recorded` events scoped to the session (via the event substrate).
  • events_count: the count of the session's events from the durable event substrate (HistoryReplayer.ListWindow, bounded).
  • tasks_count / has_failed_task: the session's tasks from the task registry.
  • has_pending_intervention: a paused pause record from the pause registry scoped to the session.

Concurrent reuse (CLAUDE.md §5)

A constructed *CounterEnricher is immutable after NewCounterEnricher: it holds only the bus / registry / coordinator references (each itself safe for concurrent reuse) plus a logger. Every Counters call's per-run state lives in its arguments and locals; the enricher reads nothing from itself for run-specific data.

func NewCounterEnricher added in v1.14.0

func NewCounterEnricher(deps CounterEnricherDeps) (*CounterEnricher, error)

NewCounterEnricher builds the V1 production Enricher. Every dependency is mandatory — a nil Bus / Tasks / Pauses fails loud with ErrMisconfigured rather than building an enricher that reports believable-but-false zeros on one dimension (CLAUDE.md §5). The returned *CounterEnricher is immutable and safe for concurrent reuse.

func (*CounterEnricher) Counters added in v1.14.0

func (e *CounterEnricher) Counters(ctx context.Context, id identity.Identity, sessionID string) SessionCounters

Counters implements Enricher for the production backend. It reads the session's own full triple (id) to scope every source — no cross-session bleed: the authorisation to see this session already happened at the projector's identity-scope predicate; the enricher re-scopes each read to the ROW's own identity so an admin listing another tenant's session reads exactly that session's tasks / events / pauses.

type CounterEnricherDeps added in v1.14.0

type CounterEnricherDeps struct {
	// Bus is the event substrate the cost / tokens / events counters read
	// from. It MUST implement events.HistoryReplayer to serve the windowed
	// per-session scan; a bus without it yields honest-partial counters
	// (Partial=true, zero bus-derived counts), never a silent zero.
	Bus events.EventBus
	// Tasks is the task registry the tasks_count / has_failed_task counters
	// read from, scoped to the session.
	Tasks tasks.TaskRegistry
	// Pauses is the pause coordinator the has_pending_intervention counter
	// reads from, scoped to the session.
	Pauses pauseresume.Coordinator
	// Logger receives Warn-level diagnostics when a bounded scan truncates
	// or the substrate is unavailable (the degradation is NEVER silent —
	// it is both logged and surfaced as SessionCounters.Partial). Nil
	// routes to slog.Default().
	Logger *slog.Logger
}

CounterEnricherDeps carries the CounterEnricher's mandatory dependencies. All three are required — a nil dependency would silently drop a counter dimension (a value that reads zero on a busy session), the exact silent-absence class this phase closes, so NewCounterEnricher fails loud rather than build a half-blind enricher (CLAUDE.md §5).

type Enricher added in v1.14.0

type Enricher interface {
	// Counters returns the read-time counter rollup for one session,
	// aggregated from the cost-event stream, the task registry, the event
	// substrate, and the pause registry — all identity-scoped to `id` (the
	// session's own full triple). A zero-valued return is honest ("we don't
	// have this data"), never silent degradation. When the bounded
	// per-session scan hits its bound (or a retention gap), the returned
	// SessionCounters.Partial is set and the cost / tokens / events counts
	// are an HONEST LOWER BOUND, never a plausible exact number.
	Counters(ctx context.Context, id identity.Identity, sessionID string) SessionCounters
}

Enricher is the optional read-time counter-rollup backend the ListerProjector overlays onto a SessionRow. It mirrors the shipped tasks.Projector enricher SEAM — only the seam is inherited; the aggregation below is net-new (the tasks serve enricher returns a ZERO cost rollup, deferring cost to the event stream). Production wiring supplies a CounterEnricher backed by the event substrate + task registry + pause registry; tests and partial builds run without one.

A projector with no Enricher wired reports honest ZERO counters — "we don't have this data", not a silent degradation of a known value — and the Service loud-rejects a facet/sort over those counters (WARN-3, see protocol.go) so an unwired build can never reproduce the false-absence defect this phase closes.

type Eraser added in v1.7.0

type Eraser interface {
	// Erase performs the full erasure cascade for the verified identity
	// (own-session-only — the scope contract is enforced at the Service
	// edge before Erase is reached). Returns the deletion telemetry, or a
	// refusal: sessions.ErrSessionRunning (a RUNNING task) /
	// sessions.ErrSessionNotFound (absent under the caller's identity).
	Erase(ctx context.Context, id identity.Identity) (prototypes.SessionsDeleteResponse, error)
}

Eraser is the seam the Service depends on for `sessions.delete`. The V1 production implementation is the in-runtime cascade orchestrator (`sessions.CascadeEraser`), which performs the real three-store cascade + session-record delete for the verified identity, refusing fail-loud on a running task. The Service depends ONLY on this interface (CLAUDE.md §4.4) — a future remote / cross-runtime eraser slots in behind it without reshaping the Service.

type ListerProjector

type ListerProjector struct {
	// contains filtered or unexported fields
}

ListerProjector is the V1 production Projector — a thin read-only projection over a `sessions.SessionLister` (the Registry's `ListSnapshots` surface). It maps the runtime `sessions.SessionSnapshot` onto the flat Protocol `SessionRow` wire shape (RFC §5.1 single-source rule: the Console never reads `sessions.Session`).

Identity scoping (CLAUDE.md §6)

ListSessions builds the `sessions.SessionListFilter` so the registry scopes by tenant: a non-admin caller is restricted to its own `(tenant, user)`; an admin caller MAY widen via the request's `TenantIDs`. The registry's `ListSnapshots` does NOT re-check scope — the gate is the Service's `ErrCrossTenantScope` check; this projector only translates the gate decision into the filter shape.

Enrichment seam (CLAUDE.md §4.4)

The SessionLister owns the lifecycle fields (status, timestamps, title, identity); it does NOT model the per-session cost / token / task / event counters or a session→agent binding. ListerProjector reads the counters through the optional Enricher seam (enricher.go), mirroring the shipped tasks.Projector enricher. When no Enricher is wired, the counters stay ZERO — honest ("we don't have this data"), not silent degradation — and the Service loud-rejects a facet/sort over them (WARN-3) so an unwired build never returns a false-empty counter page.

Concurrent reuse

A constructed *ListerProjector is immutable after NewListerProjector and safe to share across N concurrent goroutines — it holds only the SessionLister + optional Enricher references (both themselves safe for concurrent reuse); every method's per-call state lives in the call's arguments and locals.

func NewListerProjector

func NewListerProjector(lister sessions.SessionLister, opts ...ListerProjectorOption) (*ListerProjector, error)

NewListerProjector builds the V1 Projector over a SessionLister. The lister is mandatory — a nil fails loud rather than building a projector that nil-panics on the first request (CLAUDE.md §5).

func (*ListerProjector) CountersAvailable added in v1.14.0

func (p *ListerProjector) CountersAvailable() bool

CountersAvailable reports whether the projector populates the numeric / boolean counters (cost / tokens / tasks / events / intervention / failed-task) — i.e. whether an Enricher is wired. False on a partial build: the Service then loud-rejects a facet / sort over those counters rather than returning a false-empty page (WARN-3).

func (*ListerProjector) InspectSession

func (p *ListerProjector) InspectSession(ctx context.Context, id identity.Identity, sessionID string, adminScoped bool) (prototypes.SessionsInspectResponse, error)

InspectSession implements Projector.InspectSession. It lists the one session id and projects the snapshot plus the (currently empty) recent-interventions / recent-artifacts slices.

V1 scope note: the recent-interventions / recent-artifacts cards are fed by the Console's own event-stream subscription on the detail route (the page consumes `pause.*` / `artifacts.*` events filtered to the session — page spec §5). `sessions.inspect` ships the Row projection + empty capped slices; the cards populate from the live event stream client-side. A future StateStore-backed enrichment can pre-fill the slices without a wire-shape break (the fields are already on the response).

func (*ListerProjector) ListSessions

func (p *ListerProjector) ListSessions(ctx context.Context, id identity.Identity, f prototypes.SessionFilter, adminScoped bool) ([]prototypes.SessionRow, error)

ListSessions implements Projector.ListSessions. It builds the identity-scoped registry filter, lists the snapshots, and projects each onto a SessionRow.

type ListerProjectorOption added in v1.14.0

type ListerProjectorOption func(*ListerProjector)

ListerProjectorOption configures NewListerProjector.

func WithEnricher added in v1.14.0

func WithEnricher(e Enricher) ListerProjectorOption

WithEnricher wires the read-time counter-rollup backend. A nil enricher is treated as "WithEnricher not supplied" — the projector ships honest ZERO counters and reports CountersAvailable()==false, so the Service loud-rejects a facet/sort over the counters rather than returning a false-empty page (WARN-3).

type Option

type Option func(*Service)

Option configures NewService.

func WithBus

func WithBus(b events.EventBus) Option

WithBus wires the canonical events.EventBus the Service publishes the `audit.admin_scope_used` event onto when an admin-scope query succeeds. A nil bus is treated as "WithBus not supplied" — the admin path still works, but the audit observation is logged at Info instead of published (the admin action is NEVER fully silent — CLAUDE.md §13).

func WithEraser added in v1.7.0

func WithEraser(e Eraser) Option

WithEraser wires the Eraser the Service dispatches `sessions.delete` to. When unsupplied (or nil) the Service answers `sessions.delete` with ErrErasureUnsupported (the handler maps it to a 404) and the runtime does NOT advertise the CapSessionLifecycle capability — capability gating: a runtime that did not wire an eraser is honestly read-only on the Sessions surface. A non-nil eraser enables the erasure path.

func WithLogger

func WithLogger(l *slog.Logger) Option

WithLogger sets the slog.Logger the Service logs admin actions and audit-emit failures to. A nil logger routes to slog.Default().

func WithRedactor

func WithRedactor(r audit.Redactor) Option

WithRedactor wires the audit.Redactor the Service runs the `audit.admin_scope_used` payload through before publishing. A nil redactor is treated as "WithRedactor not supplied".

func WithTitleSetter added in v1.12.0

func WithTitleSetter(ts TitleSetter) Option

WithTitleSetter wires the TitleSetter the Service dispatches `sessions.set_title` to. When unsupplied (or nil) the Service answers `sessions.set_title` with ErrTitleSetUnsupported (the handler maps it to a 404) — a runtime that did not wire a title setter is honestly read-only on session titles. A non-nil setter enables the write path.

type Projector

type Projector interface {
	// ListSessions returns every session row visible to the caller,
	// already identity-scoped: when adminScoped is false the
	// implementation MUST restrict to the caller's own (tenant, user);
	// when true it MAY honour a cross-tenant TenantIDs filter. The
	// Service applies the facet filter + sort + pagination on top.
	ListSessions(ctx context.Context, id identity.Identity, f prototypes.SessionFilter, adminScoped bool) ([]prototypes.SessionRow, error)
	// InspectSession returns the full snapshot for sessionID, or
	// ErrSessionNotFound. adminScoped widens the lookup across tenants.
	InspectSession(ctx context.Context, id identity.Identity, sessionID string, adminScoped bool) (prototypes.SessionsInspectResponse, error)
	// CountersAvailable reports whether the projector populates the
	// numeric / boolean counters (cost / tokens / tasks / events /
	// intervention / failed-task). False when no counter Enricher is wired:
	// the Service then loud-rejects a facet / sort over those counters
	// rather than returning a false-empty page (WARN-3).
	CountersAvailable() bool
}

Projector is the read seam the Service depends on. The V1 production implementation is ListerProjector. Every method takes the verified identity triple plus the resolved admin-scope flag so the implementation scopes its reads.

type Service

type Service struct {
	// contains filtered or unexported fields
}

Service implements the `sessions.*` Protocol methods. It is a safe for concurrent reuse compiled artifact — immutable after NewService.

func NewService

func NewService(projector Projector, opts ...Option) (*Service, error)

NewService builds the Sessions Protocol service over a Projector. The projector is mandatory — a nil fails loud with ErrMisconfigured rather than building a Service that would nil-panic on the first request (CLAUDE.md §5). The returned *Service is immutable after construction and safe for concurrent use by N goroutines.

func (*Service) Delete added in v1.7.0

Delete implements the `sessions.delete` method — the identity-scoped, own-session-only data-lifecycle erasure of a whole session and its scoped State, Memory, and Artifacts.

The scope contract is own-session-only: the request identity IS the caller's verified identity (the wire handler overlays the verified triple and rejects any body-identity mismatch as identity_required before this method is reached), so there is no admin / cross-tenant path. A nil eraser (the capability was not wired) is reported as ErrErasureUnsupported. A refusal on a RUNNING task surfaces as ErrSessionRunning (409); an absent session as ErrSessionNotFound (404).

func (*Service) HasEraser added in v1.7.0

func (s *Service) HasEraser() bool

HasEraser reports whether the Service was wired with an Eraser — i.e. whether `sessions.delete` is supported and the runtime should advertise the CapSessionLifecycle capability. The wiring layer uses it to gate the capability advertisement so a read-only runtime stays honest.

func (*Service) HasTitleSetter added in v1.12.0

func (s *Service) HasTitleSetter() bool

HasTitleSetter reports whether the Service was wired with a TitleSetter — i.e. whether `sessions.set_title` is supported. The wiring layer can use it exactly like HasEraser to gate any future capability advertisement.

func (*Service) Inspect

Inspect implements the `sessions.inspect` method — the full per-session snapshot the Console Sessions detail view renders.

func (*Service) List

List implements the `sessions.list` method. It validates identity, enforces the cross-tenant gate, resolves the identity-scoped rows from the Projector, applies the facet filter + sort + cursor pagination, and emits an `audit.admin_scope_used` event on a successful admin-scope query.

func (*Service) SetTitle added in v1.12.0

SetTitle implements the `sessions.set_title` method — sets or clears a session's human-readable title.

The write scope is the owning `(tenant, user)`: req.Identity's tenant/user MUST equal the caller's verified identity (the wire handler overlays the verified triple and rejects any body-identity mismatch as identity_required before this method is reached, mirroring `sessions.delete`). req.SessionID is a DEDICATED field and MAY name a sibling session of the caller's own `(tenant, user)` — the SessionID component of req.Identity itself is unused for targeting (only its tenant/user matter). A nil TitleSetter (the capability was not wired) is reported as ErrTitleSetUnsupported.

type SessionCounters added in v1.14.0

type SessionCounters struct {
	// TasksCount is the number of tasks the session has spawned.
	TasksCount int
	// EventsCount is the number of events the session has emitted (a lower
	// bound when Partial is set).
	EventsCount int
	// TotalCostCents is the session's accumulated LLM cost in US cents (a
	// lower bound when Partial is set).
	TotalCostCents int64
	// TotalTokens is the session's accumulated LLM token count (a lower
	// bound when Partial is set).
	TotalTokens int64
	// HasPendingIntervention reports whether the session has a pause
	// awaiting resume / approval.
	HasPendingIntervention bool
	// HasFailedTask reports whether the session has at least one failed
	// task.
	HasFailedTask bool
	// Partial is true when the bounded per-session scan that produced the
	// cost / tokens / events counts hit its bound (ListWindow HasMore /
	// Truncated) or the windowed-read substrate was unavailable. The counts
	// are then an HONEST LOWER BOUND, not exact — the facet/sort layer MUST
	// NOT treat a Partial key as authoritative. TasksCount / HasFailedTask /
	// HasPendingIntervention (from the registries, not the bounded scan)
	// stay exact.
	Partial bool
}

SessionCounters is the read-time counter rollup for one session — the six false-absence SessionRow counters plus the honest-partial marker.

type SessionsAdminQueryPayload

type SessionsAdminQueryPayload struct {
	events.SafeSealed
	// Actor is the verified admin identity at the Protocol edge — the
	// (tenant, user, session) triple the JWT carried.
	Actor identity.Identity
	// Method is the Protocol method that carried the cross-tenant query
	// (`sessions.list` or `sessions.inspect`).
	Method string
}

SessionsAdminQueryPayload is the typed SafePayload published on the canonical `audit.admin_scope_used` event when an operator runs a cross-tenant `sessions.list` / `sessions.inspect` query under the verified `auth.ScopeAdmin` claim.

SafePayload by construction: every field is a bounded identity component or a Protocol method name — no caller-supplied bytes reach the bus. The Sessions wire surface rejects malformed requests at the Protocol edge before the emit.

The payload is distinct from `auth.AdminScopeUsedPayload` ( impersonation), `events.AdminScopeUsedPayload` (Subscribe), and `toolsprotocol.ToolsAdminActionPayload` — all ride the same canonical `audit.admin_scope_used` event type, but each emit source declares its own typed payload. A subscriber type-switches.

type TitleSetter added in v1.12.0

type TitleSetter interface {
	// SetTitle sets or clears the title of session `id` for the CALLER's
	// verified `(tenant, user)` (`ident`). See sessions.SessionRegistry's
	// SetTitle for the full semantics (trim, validate, empty clears,
	// (tenant, user) write scope). Returns sessions.ErrInvalidTitle,
	// sessions.ErrSessionNotFound, or sessions.ErrIdentityMismatch on
	// refusal.
	SetTitle(ctx context.Context, id string, ident identity.Identity, title string) error
}

TitleSetter is the write seam the Service dispatches `sessions.set_title` to. The V1 production implementation is `*sessions.Registry` — its SetTitle method satisfies this interface directly (Registry already implements the wider SessionRegistry, of which SetTitle is one method). The Service depends ONLY on this narrow interface (CLAUDE.md §4.4) — a future remote / cross-runtime title-set slots in behind it without reshaping the Service.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL