Documentation
¶
Overview ¶
Package protocol implements the two `tasks.*` read methods the Console Tasks page (Phase 73d / D-123) 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 Phase 54 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 (D-079) ¶
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 (D-025) ¶
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 ¶
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 (D-079). 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 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 ¶
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 ¶
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 ¶
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)
}
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 brief 11 §CC-4's high-cardinality runtime-side posture. A cross-tenant fan-in is gated by the Service (admin scope, D-079); 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 (D-025) ¶
RegistryProjector is immutable after NewRegistryProjector: it holds the registry + enricher references. The registry is itself D-025-safe; 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 D-025-safe.
func (*RegistryProjector) GetTask ¶
func (p *RegistryProjector) GetTask(ctx context.Context, id identity.Identity, taskID string) (prototypes.TaskDetail, error)
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 ¶
func (p *RegistryProjector) ListTasks(ctx context.Context, id identity.Identity) ([]prototypes.TaskRow, error)
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.
type RegistryProjectorOption ¶
type RegistryProjectorOption func(*RegistryProjector)
RegistryProjectorOption configures NewRegistryProjector.
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 D-025-safe compiled artifact — immutable after NewService.
func NewService ¶
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 (D-025) and safe for concurrent use by N goroutines.
func (*Service) Get ¶
func (s *Service) Get(ctx context.Context, req prototypes.TaskGetRequest) (prototypes.TaskDetail, error)
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 (D-026) — the Projector never inlines bytes above the heavy-content threshold.
func (*Service) List ¶
func (s *Service) List(ctx context.Context, req prototypes.TaskListRequest, adminScoped bool) (prototypes.TaskListResponse, error)
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). Phase 73d / D-123.
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 Phase 72b impersonation shape), `events.AdminScopeUsedPayload` (the Phase 05 Subscribe shape), and `ToolsAdminActionPayload` (Phase 73f): 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.