Help us shape the future of Go! Take the 2026 Go Developer Survey

protocol

package
v1.33.5 Latest Latest
Warning

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

Go to latest
Published: Oct 27, 2026 License: Apache-2.0 Imports: 15 Imported by: 0

Documentation

Overview

Package protocol implements the two `tasks.*` read methods the Console Tasks page consumes:

  • tasks.list — paginated, faceted task-row projection + per-status aggregates + cursor pagination.
  • tasks.get — enriched single-task detail: parent-session ref, parent-task ref, cost rollup, planner-snapshot ref.

The Console Tasks page consumes the EXISTING task-control verbs (`cancel` / `pause` / `resume` / `prioritize` / `approve` / `reject`) for mutation — there is NO `tasks.*` mutating method (CLAUDE.md §13 "no parallel implementations"). Both methods here are pure reads.

The seam (CLAUDE.md §4.4)

The Service depends on the `Projector` interface, not on a concrete task registry. The V1 production implementation is `RegistryProjector` (registry_projector.go) — a thin read-only projection over a `tasks.TaskRegistry`. A future remote / aggregated-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 `tasks.list` whose `Filter.Identities` names more than one distinct tenant is a cross-tenant fan-in. The Service receives an `adminScoped bool` the wire handler computes from the verified JWT scope set; a false value on a cross-tenant request fails closed with `ErrScopeMismatch`. There is NO `tasks.admin` scope — the closed two-scope set (`admin` + `console:fleet`) is the only admit surface, and the cross-tenant gate is `ScopeAdmin`. On an accepted cross-tenant request the Service emits an `audit.admin_scope_used` event through the shipped audit.Redactor.

A `tasks.get` for a TaskID outside the caller's tenant returns `ErrTaskNotFound` — existence is never revealed across tenants.

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("tasks/protocol: identity scope incomplete")
	// ErrScopeMismatch — a cross-tenant `tasks.list` fan-in was issued
	// without the verified `auth.ScopeAdmin` claim.
	ErrScopeMismatch = errors.New("tasks/protocol: cross-tenant query requires the admin scope claim")
	// ErrTaskNotFound — the requested TaskID is not visible to the
	// caller's identity scope (covers both genuine absence and a
	// cross-tenant lookup — existence is never revealed across tenants).
	ErrTaskNotFound = errors.New("tasks/protocol: task not found")
	// ErrInvalidRequest — the request was structurally invalid (an empty
	// task ID, an out-of-range page size, an unknown enum value).
	ErrInvalidRequest = errors.New("tasks/protocol: invalid request")
	// ErrMisconfigured — NewService was called with a nil Projector.
	ErrMisconfigured = errors.New("tasks/protocol: NewService missing a mandatory dependency")
)

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 ApprovalChecker added in v1.14.0

type ApprovalChecker interface {
	// HasPendingApproval reports whether the task's run (scoped to the
	// task's own identity triple + run id) holds at least one open HITL /
	// tool-approval gate. Identity-scoped: session A's open gate never
	// bleeds into session B's row.
	HasPendingApproval(ctx context.Context, taskIdentity identity.Identity, runID string) bool
}

ApprovalChecker is the optional list-time seam RegistryProjector reads the per-row `HasPendingApproval` facet from. The task-registry record is a lifecycle record — it does NOT itself model "has an open approval gate"; that derived signal is owned by the pause/approval registry one package over (the tasks lifecycle-record model). Production wiring supplies an implementation backed by the pause coordinator scoped to the task's run; tests and partial builds run without one.

A projector with no ApprovalChecker wired leaves `HasPendingApproval` at its honest false — the projection-completeness gate's Half-B prod-wiring test proves production ALWAYS wires it (a forgotten `WithApprovalChecker` in mux.go would ship a permanently-false facet, the exact never-wired variant the gate closes).

type Enricher

type Enricher interface {
	// ParentSession returns the parent-session reference card for the
	// task. A zero-valued ref is acceptable ("we don't have this data").
	ParentSession(ctx context.Context, id identity.Identity, taskID string) prototypes.TaskParentSessionRef
	// Cost returns the per-task cost rollup aggregated from
	// `llm.cost.recorded` events scoped to the task.
	Cost(ctx context.Context, id identity.Identity, taskID string) prototypes.TaskCostRollup
	// PlannerSnapshot returns the planner-checkpoint reference at task
	// spawn time, or nil when no checkpoint exists.
	PlannerSnapshot(ctx context.Context, id identity.Identity, taskID string) *prototypes.TaskPlannerSnapshotRef
	// Trajectory returns the projected reasoning-trace snapshot for the
	// task, or nil when the trajectory is unavailable (evicted, not
	// captured, or the run-loop has not wired a trajectory source).
	Trajectory(ctx context.Context, id identity.Identity, taskID string) *prototypes.TaskTrajectoryRef
}

Enricher is the optional per-task enrichment backend RegistryProjector reads parent-session / cost / planner-snapshot data through. Production wiring supplies an implementation backed by the sessions subsystem + the `llm.cost.recorded` event stream + the planner-checkpoint store; tests and partial-builds run without one.

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 a cross-tenant `tasks.list` fan-in succeeds. A nil bus is treated as "WithBus not supplied" — the cross-tenant path still works, but the audit observation is logged at Info instead of published.

func WithLogger

func WithLogger(l *slog.Logger) Option

WithLogger sets the slog.Logger the Service logs cross-tenant fan-ins 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".

type Projector

type Projector interface {
	// ListTasks returns every task-row projection visible to id. The
	// Service applies the facet filter + pagination + aggregate
	// computation on top; the Projector returns the full identity-scoped
	// set, newest-first by StartedAt.
	ListTasks(ctx context.Context, id identity.Identity) ([]prototypes.TaskRow, error)
	// GetTask returns the enriched detail for taskID, or ErrTaskNotFound
	// when the task is not visible to id (cross-tenant lookups return
	// ErrTaskNotFound — existence is never revealed).
	GetTask(ctx context.Context, id identity.Identity, taskID string) (prototypes.TaskDetail, error)

	// ListTenantTasks returns task rows across ALL sessions of the named
	// tenants — the admin-widened FLEET read. It is invoked by Service.List
	// ONLY after the admin-scope gate passes; the implementation reads the
	// registry's explicit tenant-scoped enumeration seam. Every returned
	// row carries full per-(tenant, user, session) identity attribution so
	// a coordinator can attribute each row to its owning source. This is
	// the "future cross-runtime aggregating projector" the RegistryProjector
	// godoc reserved, landed behind the unchanged Projector interface.
	ListTenantTasks(ctx context.Context, tenantIDs []string) ([]prototypes.TaskRow, error)
}

Projector is the read seam the Service depends on. The V1 production implementation is RegistryProjector. Every method takes the verified identity triple so the implementation scopes its reads — the Service never trusts a Projector to apply identity itself; it passes the triple so the implementation scopes a per-tenant view.

type RegistryProjector

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

RegistryProjector is the V1 production Projector — a read-only projection over a `tasks.TaskRegistry`. It maps the runtime-internal `tasks.Task` record onto the flat Protocol wire shapes the Console Tasks page renders.

Scope

`tasks.TaskRegistry.List` is session-scoped: it returns the task summaries for one `(tenant, user, session)` identity. RegistryProjector projects the caller's own session — the realistic V1 surface, matching the high-cardinality runtime-side posture. A cross-tenant fan-in is gated by the Service (admin scope); the projector honours whatever identity the Service passes it. A future cross-runtime aggregating projector slots in behind the Projector interface without reshaping the Service.

Enrichment seam

The TaskRegistry record carries lifecycle + identity + parent-task data, but NOT the parent-session metadata, the per-step cost rollup, or the planner-checkpoint reference. RegistryProjector reads those through the optional Enricher interface. When no Enricher is wired, `tasks.get` returns conservative zero-valued enrichment cards (an empty parent-session ref, a zero cost rollup, a nil planner snapshot) so a partial-build Console still renders the detail rather than failing — the zeros are honest ("we don't have this data"), not silent degradation of a known value.

Concurrent reuse

RegistryProjector is immutable after NewRegistryProjector: it holds the registry + enricher references. The registry is itself safe for concurrent reuse; the projector adds no mutable state.

func NewRegistryProjector

func NewRegistryProjector(registry tasks.TaskRegistry, opts ...RegistryProjectorOption) (*RegistryProjector, error)

NewRegistryProjector builds the V1 production Projector over a `tasks.TaskRegistry`. The registry is mandatory — a nil fails loud with ErrMisconfigured. The returned *RegistryProjector is safe for concurrent reuse.

func (*RegistryProjector) GetTask

GetTask returns the enriched detail for taskID. A task not visible to id (genuine absence or a cross-tenant lookup) returns ErrTaskNotFound — existence is never revealed across tenants.

func (*RegistryProjector) ListTasks

ListTasks returns every task-row projection visible to id — the tasks in id's session, newest-first. The Service applies the facet filter + pagination on top.

The TaskRegistry reads the identity triple from the request context (CLAUDE.md §6 rule 3 — identity flows through ctx). The projector folds the verified identity into the context before every registry call so a registry built from an identity-free context (the wire handler's `r.Context()` once auth is satisfied) still scopes its reads. A folding failure is an incomplete triple — fail loud.

func (*RegistryProjector) ListTenantTasks added in v1.11.0

func (p *RegistryProjector) ListTenantTasks(ctx context.Context, tenantIDs []string) ([]prototypes.TaskRow, error)

ListTenantTasks implements Projector.ListTenantTasks — the admin-widened FLEET read. It enumerates every task across ALL sessions of the named tenants through the registry's explicit tenant-scoped enumeration seam (tasks.TaskRegistry.ListTenant) and projects each onto the flat TaskRow wire shape, carrying full per-(tenant, user, session) identity attribution.

Scope + gate

This method is the widened counterpart to ListTasks. It is invoked by Service.List ONLY after the cross-tenant admin-scope gate passes; the RegistryProjector honours whatever tenant set the Service hands it. The registry's ListTenant reads the whole per-runtime task set for a tenant (all users, all sessions) — cross-RUNTIME federation stays the coordinator's job over per-runtime reads (the same division as sessions / events).

Group enrichment

The narrow ListTasks path enriches each row's GroupID from the caller's identity-scoped ListGroups. The fleet read deliberately does NOT resolve GroupID across every session of the tenant: the per-job "Related Sessions" group drill-in is a per-session Background Jobs feature, not a fleet-board column, and a tenant-wide ListGroups fan-out is not worth the cost at V1 fleet cardinality. A widened row therefore carries an empty GroupID — the honest "group membership not resolved in the fleet view", never a silent degradation of a known value.

type RegistryProjectorOption

type RegistryProjectorOption func(*RegistryProjector)

RegistryProjectorOption configures NewRegistryProjector.

func WithApprovalChecker added in v1.14.0

func WithApprovalChecker(c ApprovalChecker) RegistryProjectorOption

WithApprovalChecker wires the list-time approval-gate seam the `HasPendingApproval` facet reads from. A nil checker is treated as "WithApprovalChecker not supplied" — the row's `HasPendingApproval` stays at its honest false (the gate's Half-B prod-wiring test proves production wires it).

func WithEnricher

func WithEnricher(e Enricher) RegistryProjectorOption

WithEnricher wires the per-task enrichment backend. A nil enricher is treated as "WithEnricher not supplied" — `tasks.get` returns conservative zero-valued enrichment cards.

type Service

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

Service implements the two `tasks.*` read methods. It is a concurrency-safe compiled artifact — immutable after NewService.

func NewService

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

NewService builds the Tasks 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) Get

Get implements the `tasks.get` Protocol method. It validates identity, then resolves the enriched single-task detail from the Projector. A TaskID outside the caller's tenant returns ErrTaskNotFound — existence is never revealed across tenants (CLAUDE.md §6; same posture `tasks.TaskRegistry.Get` already enforces). Heavy result content is referenced via ArtifactRef the Projector never inlines bytes above the heavy-content threshold.

func (*Service) List

List implements the `tasks.list` Protocol method. It validates identity, gates a cross-tenant fan-in on the admin scope claim, resolves the identity-scoped task set from the Projector, applies the facet filter + free-text search, computes the per-status aggregates, and returns one cursor-paginated page.

adminScoped is the verified-JWT scope decision the wire handler computes from `auth.HasScope(ctx, auth.ScopeAdmin)`. It is consulted ONLY when the request is a cross-tenant fan-in (Filter.Identities naming more than one distinct tenant); a single-tenant request never requires it.

type TasksAdminActionPayload

type TasksAdminActionPayload 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
	// fan-in (`tasks.list`).
	Method string
	// TenantCount is the number of distinct tenants the cross-tenant
	// `tasks.list` query named.
	TenantCount int
}

TasksAdminActionPayload is the typed SafePayload published on the canonical `audit.admin_scope_used` event when an operator issues a cross-tenant `tasks.list` fan-in (a query whose Filter.Identities names more than one distinct tenant).

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

The payload is distinct from `auth.AdminScopeUsedPayload` (the impersonation shape), `events.AdminScopeUsedPayload` (the Subscribe shape), and `ToolsAdminActionPayload`: all ride the same canonical `audit.admin_scope_used` event type, but each emit source declares its own typed payload (events.go §"Other emit sites ... MAY add new payload types"). A subscriber type-switches on the payload.

Jump to

Keyboard shortcuts

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