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:
- Resolves the named tool's descriptor from the catalog. A miss fails the build with `ErrToolNotRegistered` (no silent skip — §13 fail-loudly).
- Maps `Approval.Policy` onto a concrete `approval.ApprovalPolicy` instance. An unknown policy fails with `ErrUnknownApprovalPolicy`.
- Maps `OAuth.Provider` + `OAuth.BindingScope` onto an existing `auth.OAuthProvider`. A missing provider fails with `ErrUnknownOAuthProvider`.
- 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 ¶
- Variables
- func WrapWithApproval(d tools.ToolDescriptor, gate *approval.ApprovalGate, ...) tools.ToolDescriptor
- func WrapWithOAuth(d tools.ToolDescriptor, prov auth.OAuthProvider, opts OAuthWrapperOptions) tools.ToolDescriptor
- type ApprovalWrapperOptions
- type Builder
- type Deps
- type OAuthWrapperOptions
Constants ¶
This section is empty.
Variables ¶
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") // ErrAuthorizerRequired — Apply was called with no Authorizer and // at least one entry declares an approval policy. The authorizer // is the gate\'s injected resolve-privilege seam (Phase 111f, // D-203); a gate with no resolve privilege check is a // misconfiguration, not a permissive mode. ErrAuthorizerRequired = errors.New("catalog: approval.ResolveAuthorizer 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 ¶
func WrapWithApproval(d tools.ToolDescriptor, gate *approval.ApprovalGate, opts ApprovalWrapperOptions) tools.ToolDescriptor
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 ¶
func WrapWithOAuth(d tools.ToolDescriptor, prov auth.OAuthProvider, opts OAuthWrapperOptions) tools.ToolDescriptor
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 ¶
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:
- Resolving the underlying descriptor.
- 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
// Authorizer is the resolve-privilege seam threaded into every
// constructed ApprovalGate (Phase 111f, D-203). Mandatory when
// any entry declares approval; ignored otherwise. The runtime
// assembly passes the runtime-vocabulary default
// (`approval.NewIdentityAuthorizer()`) unless the caller injects
// the Protocol-side adapter for wire-driven resolution.
Authorizer approval.ResolveAuthorizer
// 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.