catalog

package
v1.2.0 Latest Latest
Warning

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

Go to latest
Published: May 30, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

Documentation

Overview

Package catalog ships Harbor's operator-config-driven tool-catalog wiring (Phase 64a / D-090). Operators declare per-tool middleware in `tools.entries[]` and the `Builder` defined here auto-wraps each registered tool descriptor with the matching `approval.ApprovalGate` and / or OAuth-aware invocation wrapper. No Go wiring code on the operator's side; the runtime composes the middleware stack at boot.

The wiring contract

The builder consumes:

  • The raw `[]config.ToolEntryConfig` from `internal/config.Tools.Entries`.
  • The set of `*approval.ApprovalGate`s the dev cmd opened (one per declared `tools.<name>.approval` entry — the builder allocates fresh gates so each tool has its own pending-resolution map; gates share the SAME Coordinator + Bus + Redactor so the pause/resume primitive remains unified per CLAUDE.md §13).
  • The set of `auth.OAuthProvider`s keyed by provider name (one per declared `tools.<name>.oauth` provider).

For each entry, the builder:

  1. Resolves the named tool's descriptor from the catalog. A miss fails the build with `ErrToolNotRegistered` (no silent skip — §13 fail-loudly).
  2. Maps `Approval.Policy` onto a concrete `approval.ApprovalPolicy` instance. An unknown policy fails with `ErrUnknownApprovalPolicy`.
  3. Maps `OAuth.Provider` + `OAuth.BindingScope` onto an existing `auth.OAuthProvider`. A missing provider fails with `ErrUnknownOAuthProvider`.
  4. Composes the wrapper stack and registers the wrapped descriptor.

Wrapper composition order

When BOTH approval AND OAuth are declared for the same tool, the outer wrapper is **approval**, the inner is **OAuth**. Rationale:

  • Approval is the gate operators expect to fire FIRST: a HITL "Approve call to <tool>?" prompt should pop BEFORE any OAuth flow starts. Otherwise an operator who rejects a write would still have triggered an OAuth dance that consumed user attention.
  • OAuth's `*ErrAuthRequired` propagates UP through the approval wrapper unchanged — the gate's `RunGuarded` returns the inner tool's error verbatim when approval succeeds. So when OAuth is needed, the planner still observes `*ErrAuthRequired` and can pause for OAuth completion.

D-090 pins this order. Reversing it (OAuth outermost) would mean an OAuth pause fires BEFORE the approval gate, which contradicts operator intent and burns the user's OAuth-completion attention on a call that may end up rejected.

Concurrent reuse (D-025)

`*Builder` is a one-shot constructor — built, called once via Apply, then discarded. The wrapped descriptors `Apply` produces ARE the long-lived artifacts; each composed Invoke closure is safe for N concurrent invocations because:

  • The wrapper holds the gate / provider by reference; both are compiled artifacts safe for concurrent reuse (Phase 30 / 31 concurrent_test.go pins).
  • Per-invocation state lives in ctx + the inner descriptor's ToolResult, never on the wrapper.

Fail-loud at boot

Every error path here is fatal at boot. The catalog builder NEVER degrades silently — an unknown policy / provider / tool name is the operator's typo, and they want to know about it BEFORE the runtime starts serving traffic. CLAUDE.md §13 amendment.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrCatalogRequired — Apply was called with a nil Catalog. The
	// builder cannot resolve target descriptors without the catalog.
	ErrCatalogRequired = errors.New("catalog: ToolCatalog required")
	// ErrCoordinatorRequired — Apply was called with no Coordinator and
	// at least one entry declares an approval policy. The Coordinator
	// is the unified pause/resume primitive (Phase 50 / D-067).
	ErrCoordinatorRequired = errors.New("catalog: pauseresume.Coordinator required when entries declare approval")
	// ErrBusRequired — Apply was called with no EventBus and at least
	// one entry declares an approval policy. The bus carries the gate's
	// `tool.approval_requested` / `tool.approved` / `tool.rejected`
	// lifecycle events.
	ErrBusRequired = errors.New("catalog: events.EventBus required when entries declare approval")
	// ErrRedactorRequired — Apply was called with no Redactor and at
	// least one entry declares an approval policy. The redactor
	// processes the approval-request payload before emission
	// (CLAUDE.md §7 rule 6).
	ErrRedactorRequired = errors.New("catalog: audit.Redactor required when entries declare approval")
	// ErrToolNotRegistered — an `entries[].name` did not resolve to a
	// registered tool in the catalog. The error message names the
	// offending tool name + lists currently-registered names so the
	// operator sees the typo.
	ErrToolNotRegistered = errors.New("catalog: entry references a tool name that is not registered")
	// ErrUnknownApprovalPolicy — an `entries[].approval.policy` named
	// a policy the bundled set does not provide. The error message
	// names the offending value.
	ErrUnknownApprovalPolicy = errors.New("catalog: unknown approval policy")
	// ErrUnknownOAuthProvider — an `entries[].oauth.provider` named a
	// provider the supplied OAuth registry does not contain. The
	// error message names the offending value + lists configured
	// providers.
	ErrUnknownOAuthProvider = errors.New("catalog: unknown oauth provider")
	// ErrInvalidBindingScope — an `entries[].oauth.binding_scope` was
	// not one of the canonical values. (Mirrors the config-time check;
	// duplicated here as defence-in-depth in case a programmatic
	// caller builds entries without going through config.Validate.)
	ErrInvalidBindingScope = errors.New("catalog: invalid oauth binding_scope")
	// ErrAlreadyApplied — Apply was called twice on the same Builder.
	// The builder is one-shot.
	ErrAlreadyApplied = errors.New("catalog: builder already applied")
	// ErrInvalidLoadingMode — entries[].loading_mode names a value
	// not in {"", "always", "deferred"}. Phase 107c / D-167. Fired
	// from the Builder as defence-in-depth; the config validator
	// rejects the same shape pre-boot.
	ErrInvalidLoadingMode = errors.New("catalog: invalid loading_mode")
)

Sentinel errors. Callers compare via errors.Is.

Functions

func WrapWithApproval

WrapWithApproval wraps `d` so every Invoke call routes through the gate's `RunGuarded`. On gate REJECT the wrapper returns `*approval.ErrToolRejected`; on gate APPROVE the original args flow into `d.Invoke`. Identity is read from ctx (mandatory — `identity.MustFrom`).

func WrapWithOAuth

WrapWithOAuth wraps `d` so every Invoke call first ensures an access token exists via `prov.Token`. A `*auth.ErrAuthRequired` return short-circuits — the wrapper does NOT call the underlying tool; the runtime catches the typed sentinel and pauses the run via the unified pause/resume primitive (Phase 50).

The Phase 64a wrapper does NOT inject the token into the upstream request — that is a per-driver concern (HTTP / MCP / A2A drivers each compose their own bearer-token injection). The wrapper's job is to PRE-CHECK token availability so the runtime can pause for OAuth completion BEFORE attempting the call.

Types

type ApprovalWrapperOptions

type ApprovalWrapperOptions struct {
	// Tags is the static tag set the wrapper attaches to every
	// approval request originating from this tool. The tagged policy
	// matches against this list.
	Tags []string
}

ApprovalWrapperOptions tunes the approval-gate wrapper's behaviour.

type Builder

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

Builder applies a list of `ToolEntryConfig`s to a `ToolCatalog`, wrapping each named descriptor with the declared middleware. One Builder per Apply — the type is one-shot.

Builder is NOT a long-lived artifact; the wrapped descriptors it installs ARE. The D-025 concurrent-reuse invariant lives on those descriptors, not on Builder.

func New

func New(entries []config.ToolEntryConfig, deps Deps) *Builder

New constructs a Builder. Validation is deferred to Apply so the caller can introspect the entries before applying.

func (*Builder) Apply

func (b *Builder) Apply(ctx context.Context) error

Apply installs every entry's middleware onto the catalog. The catalog MUST already have the underlying tool descriptors registered; Apply REPLACES them with wrapped versions by calling `Resolve` + re-registering via the catalog's `Register` after a `Deregister`-equivalent. Since the canonical ToolCatalog interface has no Deregister method (RegisterMany is one-shot at boot), Apply works around this by:

  1. Resolving the underlying descriptor.
  2. Mutating the catalog through a small `ReplaceForBuilder` shim when the catalog implements it (the in-memory catalog does).

For the in-memory `*catalog` shipped today, Apply uses the `(c *Replaceable).Replace` shim wired in `internal/tools`. Future catalog implementations either provide an equivalent or document "no per-tool wiring at boot."

Apply is idempotent in the failure path: a partial application is rolled back when ANY entry errors (the in-memory catalog snapshots the descriptor set before mutation; on error, the snapshot is restored). A successful Apply is destructive; the original descriptors are GONE from the catalog after return.

Apply is one-shot — a second call returns `ErrAlreadyApplied`.

type Deps

type Deps struct {
	// Catalog is the tool catalog whose descriptors get re-registered
	// with their wrappers. Mandatory.
	Catalog tools.ToolCatalog
	// Coordinator is the unified pause/resume primitive. Mandatory
	// when any entry declares approval; ignored otherwise.
	Coordinator pauseresume.Coordinator
	// Bus is the event bus the approval gate emits on. Mandatory
	// when any entry declares approval; ignored otherwise.
	Bus events.EventBus
	// Redactor processes the gate's approval-request payload before
	// emission. Mandatory when any entry declares approval; ignored
	// otherwise.
	Redactor audit.Redactor
	// OAuthProviders maps the operator-facing provider name (the
	// string under `entries[].oauth.provider`) to a constructed
	// `auth.OAuthProvider`. An entry referencing a name not in this
	// map fails Apply with `ErrUnknownOAuthProvider`. Empty when no
	// entry declares OAuth.
	OAuthProviders map[string]auth.OAuthProvider
	// AppliedGates is an optional out-channel: when set, the builder
	// pushes every constructed `*approval.ApprovalGate` into this map
	// keyed by the tool name. Callers (the dev cmd, the integration
	// test) use this to drive in-process `ResolveApproval` calls.
	// Nil disables the surfacing.
	AppliedGates map[string]*approval.ApprovalGate
}

Deps bundles the collaborators the Builder consumes. When the entry list contains NO approval entries, the Coordinator / Bus / Redactor fields are unused (the builder still validates structurally).

type OAuthWrapperOptions

type OAuthWrapperOptions struct {
	// ProviderName is the operator-facing name of the OAuth source
	// the tool binds to. Surfaced in error messages.
	ProviderName string
	// BindingScope is the resolved auth.BindingScope (`user` or
	// `agent`) the tool's calls should authenticate under.
	BindingScope auth.BindingScope
}

OAuthWrapperOptions tunes the OAuth wrapper's behaviour.

Jump to

Keyboard shortcuts

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