search

package
v1.3.1 Latest Latest
Warning

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

Go to latest
Published: Jun 11, 2026 License: Apache-2.0 Imports: 11 Imported by: 0

Documentation

Overview

Package search owns the Harbor Phase 72c (D-108) cross-cutting search primitive — the four runtime-side per-subsystem indexes (sessions, tasks, events, artifacts) plus the `search.query` palette dispatcher that aggregates them.

Why this package exists

Brief 11 §CC-4 split the Console's cross-cutting global search into two halves: runtime-side for high-cardinality entities (sessions, tasks, events, artifacts) and Console-side for slow-moving catalog data (tools, agents, flows, MCP connections). This package owns the runtime-side half. Console-side adapters do NOT live here — they land in their per-page Stage-2 Console phases (73c/d/e/f/g/i/k).

The §4.4 seam

`Searcher` is the per-index interface; one Searcher instance per canonical index, all registered into a `SearcherRegistry` at boot. The `search.query` palette dispatcher (`aggregate.go::Query`) fans out concurrently to every requested index. There is no driver pluralism per index — the runtime owns one implementation per index; pluralism would be a post-V1 concern (an FTS sidecar swap, etc.).

Identity is mandatory (CLAUDE.md §6, RFC §5.5)

Every search call rejects requests with an incomplete identity triple via `ErrIdentityRequired`. Cross-tenant search requires the `auth.ScopeAdmin` claim per D-079; an unauth'd cross-tenant request is rejected with `ErrCrossTenantRequiresAdmin`. The rejection is loud — there is no silent degradation to an empty result set (CLAUDE.md §13).

Heavy-payload bypass (D-026)

Result rows whose underlying preview payload would exceed the heavy-content threshold ship as a populated `Ref` on the `SearchResultRow`, never inline bytes. Per-index searchers enforce this at the row-construction site.

Concurrent reuse (D-025)

Every Searcher is a compiled artifact: the dependency reads are set once at construction (the SessionRegistry / TaskRegistry / Replayer / ArtifactStore / Redactor); per-call state lives in `ctx` + the `SearchRequest`. One Searcher serves N concurrent goroutines safely.

Index

Constants

View Source
const HeavyPreviewThreshold = config.DefaultHeavyOutputThresholdBytes

HeavyPreviewThreshold is the D-026 bound — a `SearchResultRow` preview whose UTF-8 byte length would exceed this value ships as a `*SearchArtifactRef` instead of inline bytes. Single-sourced on `config.DefaultHeavyOutputThresholdBytes` (the one home of the 32 KiB heavy-output default — the DefaultSpawnDepthCap precedent) so the search bound cannot drift from the runtime LLM-edge safety net; the search package still has no dependency on internal/llm.

View Source
const PerIndexTimeout = 5 * time.Second

PerIndexTimeout caps the per-index fan-out wait inside Query. A per-index Searcher whose Search has not returned within this budget is cancelled and its rows are dropped from the merge. V1 ships a generous 5-second cap; post-V1 may make this configurable per deployment.

View Source
const PreviewMaxRunes = 256

PreviewMaxRunes is the soft cap applied at the row-construction site after redaction — a preview that fits under HeavyPreviewThreshold but exceeds this rune count is truncated with an ellipsis. Keeps the wire response compact even when individual previews are within the hard byte bound.

Variables

View Source
var (
	// ErrIdentityRequired — the search request's identity scope is
	// missing one of (tenant, user, session). Identity is mandatory
	// (CLAUDE.md §6 rule 9, RFC §5.5).
	ErrIdentityRequired = errors.New("search: identity required (tenant/user/session)")
	// ErrCrossTenantRequiresAdmin — the request's filter expands the
	// query OUTSIDE the caller's authenticated tenant AND the caller
	// does not hold the `auth.ScopeAdmin` claim (D-079).
	ErrCrossTenantRequiresAdmin = errors.New("search: cross-tenant search requires the auth.ScopeAdmin claim")
	// ErrInvalidRequest — the request fails structural validation
	// (PageSize > MaxSearchPageSize, an unknown SearchIndex in
	// `Indexes`, etc.). Surfaces at the handler boundary as
	// CodeInvalidRequest.
	ErrInvalidRequest = errors.New("search: invalid request")
	// ErrRedactionFailed — a `SearchResultRow.Preview` failed
	// redaction. Per the CLAUDE.md §7 rule, the request fails loud
	// rather than emitting un-redacted bytes.
	ErrRedactionFailed = errors.New("search: redaction failed")
	// ErrUnknownIndex — an `Indexes` entry on a `SearchRequest` for
	// `search.query` is not one of the four canonical runtime-side
	// indexes. Distinct from ErrInvalidRequest so callers can branch.
	ErrUnknownIndex = errors.New("search: unknown index")
)

Sentinel errors. Callers compare via errors.Is.

View Source
var ConcurrentReuseTag = struct{ Note string }{Note: "search: per-index Searchers are D-025 compiled artifacts; per-call state lives in ctx + req"}

ConcurrentReuseTag is a no-op assertion site — kept as a package-level var so a future static analyser can grep it as the canonical D-025 witness across the search subsystem. The actual enforcement is the concurrent_reuse_test.go N≥100 stress.

Functions

func CrossTenantRequested

func CrossTenantRequested(callerTenant string, req types.SearchRequest) bool

CrossTenantRequested reports whether the request's filter targets tenants OTHER than the caller's authenticated tenant. The handler gates this on the admin-scope predicate; CrossTenantRequested itself is a pure read.

The rule:

  • Empty Filter.TenantIDs → caller's own tenant only (no cross-tenant).
  • Filter.TenantIDs contains exactly the caller's tenant → no cross-tenant.
  • Any other shape → cross-tenant requested.

func EffectiveTenantSet

func EffectiveTenantSet(callerTenant string, req types.SearchRequest) []string

EffectiveTenantSet returns the set of tenants the request should be scoped to AFTER admin-gating. When the caller's filter is empty, the effective set is `{callerTenant}` (the default). When the filter names tenants, the effective set is those tenants verbatim — the caller already passed the admin-scope gate (if needed) by the time this is called.

func MatchesAnyField

func MatchesAnyField(needle string, fields ...string) bool

MatchesAnyField is a convenience over MatchesQuery — true when at least one of `fields` contains `needle`. Per-index Searchers call this to OR together the searchable fields (e.g. session ID + agent name + status).

func MatchesQuery

func MatchesQuery(haystack, needle string) bool

MatchesQuery reports whether haystack contains needle, case-folded. Empty needle matches anything (the caller wants every row in scope). Used uniformly by every per-index Searcher so the substring semantics are identical across the cluster.

func Paginate

func Paginate(all []types.SearchResultRow, req types.SearchRequest) (page, pageSize, pageCount int, totalCount int64, hasMore bool, slice []types.SearchResultRow)

Paginate slices rows according to the request's (defaulted) page + size and returns the slice plus the pagination math (page, page_size, page_count, total_count, has_more). Returns a non-nil empty slice when the page is past the end (rather than nil) so the JSON wire form is `[]` consistently.

func Query

Query is the `search.query` palette dispatcher — the runtime-side half of Brief 11 §CC-4's "single search box in the Console header" experience. It concurrently fans out to every requested runtime-side index in the registry, merges the result rows, applies the union pagination, and returns one paginated `SearchResponse`.

Contract:

  • Identity is mandatory; missing-triple returns `ErrIdentityRequired`.
  • Cross-tenant gating runs at the aggregate edge — a cross-tenant request without `auth.ScopeAdmin` is rejected with `ErrCrossTenantRequiresAdmin` (NEVER silently downgraded).
  • Empty `req.Indexes` means "all four registered indexes."
  • Unknown indexes in `req.Indexes` are rejected at validation (`ErrUnknownIndex`); they NEVER fall through to a silent skip.
  • Per-index failures fan-in: a single index's failure does NOT fail the whole `search.query` — the dispatcher emits a best-effort union (per Brief 11 §CC-4's "graceful degradation on backend stutter"). The failure mode is logged via the redactor's logger contract elsewhere; for the wire response the other indexes' rows still ship. This is the ONE exception to the fail-loud rule §13 carves out for aggregators — and only after identity + scope checks have already passed (those failures stay loud).
  • Carries NO index of its own; emits NO events.

`Query` is a pure function over the registry, ctx, callerID, and req — no per-call state lives on `*SearcherRegistry` (D-025).

func RedactAndCapPreview

func RedactAndCapPreview(ctx context.Context, redactor audit.Redactor, preview string) (out string, heavy bool, err error)

RedactAndCapPreview is the standard preview-emission helper every Searcher uses: it (a) runs the raw preview through the audit Redactor; (b) checks the byte-length against HeavyPreviewThreshold — when over, returns the empty preview + a true bool signalling the caller MUST populate a Ref instead; (c) caps to PreviewMaxRunes with an ellipsis.

On redaction failure: returns wrapped ErrRedactionFailed. The caller (typically inside Search) MUST NOT emit a row when this errors.

func SortRowsByOccurredAtDesc

func SortRowsByOccurredAtDesc(rows []types.SearchResultRow)

SortRowsByOccurredAtDesc orders rows newest-first. V1's ordering contract per the Phase 72c plan: lexicographic match + time-order; post-V1 may add relevance scoring.

func TimeInWindow

func TimeInWindow(t time.Time, req types.SearchRequest) bool

TimeInWindow reports whether t falls within the request's [Since, Until] window. Zero Since means "no lower bound"; zero Until means "no upper bound." When both are zero, every time passes.

func ValidateRequest

func ValidateRequest(callerID identity.Identity, req types.SearchRequest) error

ValidateRequest is the request-edge structural check shared by every `search.*` method. It enforces:

  • Identity triple present (`ErrIdentityRequired`).
  • PageSize within `[0, types.MaxSearchPageSize]` (`ErrInvalidRequest`).
  • Page non-negative (`ErrInvalidRequest`).
  • Every value in `Indexes` (when populated, for `search.query`) is a canonical index (`ErrUnknownIndex`).

The caller is the per-method handler (or the aggregate dispatcher); the result is normalised `(page, size)` defaults for the caller to apply.

Types

type Deps

type Deps struct {
	// Redactor is the audit redactor every Preview goes through before
	// emission. Required.
	Redactor audit.Redactor
	// AdminScope is the predicate the searcher consults to gate
	// cross-tenant requests. Required.
	AdminScope ScopeChecker
}

Deps bundles the construction-time dependencies every per-index searcher needs. Fields documented per searcher; missing required dependencies fail loud at construction (CLAUDE.md §5).

func (Deps) Validate

func (d Deps) Validate() error

Validate returns wrapped ErrInvalidRequest when a required Dep is missing.

type ScopeChecker

type ScopeChecker func(ctx context.Context) bool

ScopeChecker is the narrow predicate the Searchers consult to decide whether a cross-tenant request is allowed. The production implementation is `server.SearchAdminScopeFromAuth` — owned by the Runtime's network surface per the D-203 direction rule (runtime packages import protocol TYPES only, never protocol auth / behaviour), so this package never sees the Protocol's auth vocabulary; tests inject a deterministic predicate.

The signature deliberately takes `ctx` (not a verified-identity struct) so the implementation can read the verified scope set from the auth context attached by the Phase 61 middleware.

type Searcher

type Searcher interface {
	// Index returns the canonical index this Searcher serves.
	Index() types.SearchIndex
	// Search runs the query against the index, applies the identity +
	// scope filter, redacts each preview, paginates, and returns.
	Search(ctx context.Context, req types.SearchRequest) (types.SearchResponse, error)
}

Searcher is the §4.4 seam interface — one implementation per canonical runtime-side index. Each implementation is a D-025 compiled artifact: every dependency is set once at construction; per-call state lives in `ctx` + `SearchRequest`.

Implementations MUST:

  • Reject incomplete identity (`ErrIdentityRequired`).
  • Gate cross-tenant requests on `auth.ScopeAdmin` via the `ScopeChecker` passed at construction (`ErrCrossTenantRequiresAdmin`).
  • Redact every emitted `Preview` via the supplied `audit.Redactor` before returning (`ErrRedactionFailed` on failure).
  • Ship a `*SearchArtifactRef` instead of inline bytes when a preview would exceed `HeavyPreviewThreshold` (D-026).
  • Honor `ctx.Err()` between long phases of work.

type SearcherRegistry

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

SearcherRegistry is the registered set of per-index Searchers — one per canonical index. The aggregate dispatcher (`Query`) reads from it; the per-method protocol handlers route directly to the named Searcher via `Get`.

A registry is built once at boot (`NewRegistry`) and shared across every search call. Registration is closed after construction — there is no Register-at-runtime seam (the canonical index set is closed).

func NewRegistry

func NewRegistry(searchers ...Searcher) (*SearcherRegistry, error)

NewRegistry builds a SearcherRegistry from the given Searchers. Each supplied Searcher MUST report a canonical Index; duplicate indexes fail loud (the wiring is wrong). A registry MAY be incomplete (a subset of the four indexes) — `Query` skips unregistered indexes gracefully rather than failing the whole request, so a partial deployment is acceptable; the missing index simply contributes zero rows.

func (*SearcherRegistry) Get

Get returns the Searcher registered for the given index, or false when no Searcher is registered for that index.

func (*SearcherRegistry) Indexes

func (r *SearcherRegistry) Indexes() []types.SearchIndex

Indexes returns the sorted list of indexes the registry knows about. Deterministic — sorted lexicographically.

Directories

Path Synopsis
Package artifacts implements the Phase 72c `search.artifacts` runtime-side index — a server-enforced search over the artifact store's catalog, scoped to the caller's identity triple unless the `auth.ScopeAdmin` claim is present (D-079).
Package artifacts implements the Phase 72c `search.artifacts` runtime-side index — a server-enforced search over the artifact store's catalog, scoped to the caller's identity triple unless the `auth.ScopeAdmin` claim is present (D-079).
Package events implements the Phase 72c `search.events` runtime-side index — a server-enforced search over the event bus's replay ring, scoped to the caller's identity triple unless the `auth.ScopeAdmin` claim is present (D-079).
Package events implements the Phase 72c `search.events` runtime-side index — a server-enforced search over the event bus's replay ring, scoped to the caller's identity triple unless the `auth.ScopeAdmin` claim is present (D-079).
Package sessions implements the Phase 72c `search.sessions` runtime-side index — a server-enforced search over session lifecycle records, scoped to the caller's identity triple unless the `auth.ScopeAdmin` claim is present (D-079).
Package sessions implements the Phase 72c `search.sessions` runtime-side index — a server-enforced search over session lifecycle records, scoped to the caller's identity triple unless the `auth.ScopeAdmin` claim is present (D-079).
Package tasks implements the Phase 72c `search.tasks` runtime-side index — a server-enforced search over task lifecycle records, scoped to the caller's identity triple unless the `auth.ScopeAdmin` claim is present (D-079).
Package tasks implements the Phase 72c `search.tasks` runtime-side index — a server-enforced search over task lifecycle records, scoped to the caller's identity triple unless the `auth.ScopeAdmin` claim is present (D-079).

Jump to

Keyboard shortcuts

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