projection

package
v1.18.0 Latest Latest
Warning

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

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

Documentation

Overview

Package projection holds the run-start agent-config projection shared by every run-loop driver (the production dev driver and the harbortest devstack twin) and exercised directly by the control-plane integration test. Extracting it here keeps the projection logic in ONE place: the drivers live in separate binaries (cmd/harbor's package main cannot be imported by harbortest/devstack), so an inlined copy in each would drift (CLAUDE.md §17.6). The integration test calls the same function, so the test exercises the real projection rather than a test-local copy (CLAUDE.md §17.4).

Index

Constants

This section is empty.

Variables

View Source
var ErrReconcileRead = errors.New("agentcfg/projection: run-start reconcile active-revision read failed")

ErrReconcileRead wraps an active-revision read failure inside ReconcileConnections so the run-loop caller can distinguish a fail-loud read error (never a silent "detach nothing", CLAUDE.md §13) from a best-effort detach error.

View Source
var ErrSkillBodyMissing = errors.New("agentcfg/projection: agent-config pins a skill whose body is absent from the store")

ErrSkillBodyMissing is returned by ActiveSkillViews when an agent's active config pins an ADMIN skill-membership name whose body is absent from the store (e.g. the skill was hard-deleted, or a rollback landed on a revision referencing a since-deleted skill). An admin-pinned skill whose body is absent from the store is a LOUD failure at run-start projection — never a silent drop (CLAUDE.md §13). Session-personal skill names are exempt (a safe-subset add that may legitimately not be in the directory view).

Functions

func ActiveLLMOverrides

func ActiveLLMOverrides(ctx context.Context, reg agentcfg.Registry, agentID string, id identity.Quadruple) (*planner.LLMOverrides, error)

ActiveLLMOverrides resolves the PER-AGENT LLM-parameter override layer from the agent's active config revision at run start. It returns the per-agent sampling defaults (model / temperature / max-tokens / reasoning-effort) the agent has pinned, as a planner.LLMOverrides carrying ONLY those four dimensions — the layer the run loop folds BETWEEN the session override and the tenant-wide baseline (precedence session › per-agent › tenant-wide baseline › config default).

A nil registry, an empty agentID, an agent with no active revision, or an active revision with no LLM-params section returns (nil, nil) — the backward-compatible "no per-agent override" path. A registry read error is returned so the caller fails the run loudly (CLAUDE.md §13): no silent fall-through to the tenant baseline on a read failure.

The active revision is read ONCE per run; the returned bundle is fresh (its pointers are copies), so concurrent / in-flight runs keep their own snapshot (the concurrent-reuse contract). `id` carries the run's identity; only the triple is used (the registry is identity-scoped, never keyed by run). This is sampling parameters only — ExtraInstructions / prompt layers are resolved elsewhere (the agent-config prompt-layer projection), so the per-agent LLM layer never carries prompt text.

func ActivePlannerCatalogView

func ActivePlannerCatalogView(ctx context.Context, reg agentcfg.Registry, ov sessionoverlay.Store, agentID string, id identity.Quadruple, cat tools.ToolCatalog, filter tools.CatalogFilter) (tools.PlannerCatalogView, error)

ActivePlannerCatalogView builds the run's planner-facing catalog view at run start, applying the agent's active-config tool exposure: a paused MCP server's tools and any individually-disabled tool are excluded from the view (next-turn projection — the live transport stays WARM). The exclusion set is the order-independent union of THREE narrow-only disable tiers — the admin baseline, the durable per-user disable set (which spans that user's sessions for the agent), and the ephemeral session overlay — so it can only ever grow: no tier can re-expose a tool a higher tier disabled. It always returns a usable view: a nil registry, an empty agentID, an agent with no active revision, or an active revision with no tool-exposure section (or an empty one) returns the plain tools.NewPlannerView over cat+filter — the backward-compatible "ungated" path. A registry read error is returned so the caller fails the run loudly (CLAUDE.md §13): no silent fall-through to the unfiltered view on a read failure.

The active revision is read ONCE per run; the returned view is fresh, so concurrent / in-flight runs keep their own snapshot (the concurrent-reuse contract). Only the identity triple is used (the registry is identity-scoped, never keyed by run).

func ActivePromptLayers

func ActivePromptLayers(ctx context.Context, reg agentcfg.Registry, agentID string, id identity.Quadruple) (base string, user string, ok bool, err error)

ActivePromptLayers resolves the agent's active-config layered system prompt at run start. It returns the base + user layer text and whether the active revision carries a prompt-layer section (ok). A nil registry, an empty agentID, an agent with no active revision, or an active revision with no prompt-layer section returns ("", "", false, nil) — the backward-compatible "no durable prompt layers" path (the run keeps its configured base). A registry read error is returned so the caller fails the run loudly (CLAUDE.md §13): no silent fall-through on a read failure.

An unset layer within a present section returns the empty string for that layer (the caller treats empty as "inherit the configured default" for the base, and "no user layer" for the user layer).

The active revision is read ONCE per run; the returned values are plain strings, so concurrent / in-flight runs keep their own snapshot (the concurrent-reuse contract). Only the identity triple is used (the registry is identity-scoped, never keyed by run).

func ActiveRunCompletionHook added in v1.10.0

func ActiveRunCompletionHook(ctx context.Context, reg agentcfg.Registry, agentID string, id identity.Quadruple, yamlDefault *steering.CompletionHookSpec) (*steering.CompletionHookSpec, bool, error)

ActiveRunCompletionHook resolves the run-completion hook for a run at run start with next-run projection semantics. Resolution precedence is pinned here (and by a table test — CLAUDE.md §17.6): the agent-config `hooks` section (when PRESENT) over the static yaml default over no hook. The two run-loop drivers (the production dev driver and the harbortest devstack twin) call this ONE helper, so the precedence cannot drift between binaries.

A PRESENT hooks section is authoritative (mirrors the naming section): a run-completion hook with a non-empty tool pins it, while a present section with no/empty run-completion tool is an explicit per-agent NO-HOOK that WINS over yamlDefault (returns (nil, false)). Only an ABSENT (nil) hooks section falls through to yamlDefault — otherwise a per-agent opt-out of transcript egress would be silently discarded.

yamlDefault is the operator's static `runtime.hooks.run_completion` projection (nil when unset). The returned spec's AgentID is stamped from agentID (registration metadata, never an isolation key — §6). A nil registry, an empty agentID, or an agent with no active revision falls through to yamlDefault. A registry read error is returned so the caller fails the run loudly (CLAUDE.md §13): no silent fall-through to yaml on a read failure.

The active revision is read ONCE per run; the returned spec is fresh, so concurrent / in-flight runs keep their own snapshot (the concurrent-reuse contract). Only the identity triple is used (the registry is identity-scoped, never keyed by run).

func ActiveSkillViews

func ActiveSkillViews(ctx context.Context, reg agentcfg.Registry, ov sessionoverlay.Store, agentID string, id identity.Quadruple, views []skills.SkillView) ([]skills.SkillView, error)

ActiveSkillViews applies an agent's active-config skills membership to the run's skill-directory views at run start. It resolves the agent's active revision once and, when the revision pins a skills section, keeps only the views whose name is in the membership set. A nil registry, an empty agentID, or an agent with no active revision / no skills section returns the views unchanged — the backward-compatible "ungated" path. A registry read error is returned so the caller fails the run loudly (CLAUDE.md §13): no silent fall-through to the unfiltered view on a read failure.

The active revision is read ONCE per run; the returned slice is fresh, so concurrent / in-flight runs keep their own snapshot (the concurrent-reuse contract). `id` carries the run's identity; only the triple is used (the registry is identity-scoped, never keyed by run).

func ApplyPromptLayers

func ApplyPromptLayers(ctx context.Context, reg agentcfg.Registry, overlayStore sessionoverlay.Store, agentID string, id identity.Quadruple, ov *planner.LLMOverrides) (*planner.LLMOverrides, error)

ApplyPromptLayers overlays the agent's active-config durable prompt layers onto the run's resolved per-run override bundle at run start. It is the ONE shared seam both run-loop drivers (the production dev driver and the harbortest devstack twin) call after resolving the LLM-parameter overrides, so the two binaries cannot drift (CLAUDE.md §17.6).

A non-empty base layer is set as ov.BasePromptLayer (overriding the run's configured base at the prompt builder); a non-empty user layer is set as ov.UserPromptLayer (composed below the base in the lower-trust position). An empty layer is treated as unset (the base inherits the configured default; no user layer is added) — so a run with no durable prompt layers is unchanged. When ov is nil but a layer is present, a fresh bundle is allocated. A registry read error is returned so the caller fails the run loudly. The returned bundle is fresh-per-run (no shared mutable state).

func EffectiveLoadingMode added in v1.10.0

func EffectiveLoadingMode(ctx context.Context, reg agentcfg.Registry, agentID string, id identity.Quadruple, t tools.Tool, boot tools.LoadingMode) (tools.LoadingMode, error)

EffectiveLoadingMode resolves tool t's projected EFFECTIVE LoadingMode under agentID's ADMIN-tier active config revision — the SAME precedence ActivePlannerCatalogView applies at run start, exposed for the `tools.describe` read surface's optional `agent_id` path. boot is the boot-effective mode (already reflecting the driver default + any boot `tools.entries[].loading_mode`); a nil registry, an empty agentID, or an agent with no active revision / no tool-exposure section returns boot unchanged — the backward-compatible path byte-identical to `tools.describe` behaviour before this projection existed. A registry read error is returned so the caller fails the request loudly (CLAUDE.md §13).

func FilterSkillViewsByMembership

func FilterSkillViewsByMembership(views []skills.SkillView, names []string) []skills.SkillView

FilterSkillViewsByMembership keeps only the views whose Name is in the membership set. An empty membership keeps nothing (the rollback-to-empty case — an explicit empty skills section disables every skill for the next run). The returned slice is always freshly allocated.

func ReconcileConnections added in v1.11.0

func ReconcileConnections(ctx context.Context, reg agentcfg.Registry, agentID string, id identity.Quadruple, detacher ConnectionDetacher, bootDeclared map[string]struct{}) (int, error)

ReconcileConnections is the DETACH leg of run-start reconciliation. It compares the agent's DECLARED runtime-added connections (the active config revision's connections section) against the reconciling OWNER's ATTACHED set (the owner-scoped reconcile VIEW over the live MCP registry) and detaches every server that owner has attached but no longer declares — a connection removed via `agent_config.remove_mcp_connection`, or a server re-declared away by a rollback past an add. Detaching deregisters the server's tools from the catalog + MCP registry and closes its transport, so the NEXT run's projected catalog excludes them, the registry no longer lists it, and the subprocess drains.

Owner-scoped reconcile view (the fix for the process-global over-detach)

The attached set the reconcile diffs against is the reconciling OWNER's runtime-added entries only — the (tenant, agent) owner stamped at attach. Boot-declared servers (untagged) and every OTHER owner's runtime-adds are NOT in the view, so a run for owner A can never detach a boot server or a tenant-B runtime-added connection, even though the underlying registry + catalog stay process-global and deployment-shared (resolution + dispatch stay bare-name; boot servers remain visible to every session). This is the per-owner reconcile VIEW the design intends — NOT a store re-key, and NOT an isolation key (agent_id is not an isolation principal). The owner is derived from the run's verified triple (tenant) + agentID; a zero owner yields an empty view (nothing to reconcile), so a reconcile without an owner detaches nothing rather than falling back to the whole registry. The bootDeclared set is retained as belt-and-suspenders (an owner's view already excludes boot servers by construction, but the explicit skip documents the invariant).

Honest in-flight semantics (read this before assuming isolation)

This is a projection-boundary act: it fires at run START, never in the middle of the calling run, and EXPOSURE correctness is next-turn — a removed server never appears in any catalog view projected after the removal revision. Physical TEARDOWN is a separate, process-global effect: the catalog and MCP registry are shared across sessions, so a reconcile triggered by one session's run-start can deregister a source and close its transport while ANOTHER session's run is mid-flight and about to call it. That in-flight run's prompt snapshot is unchanged, but its NEXT call to the detached server fails LOUDLY — a typed catalog not-found at dispatch, or a closed-transport error from the driver — never a hang, a panic, or a silent success. This is the same failure class as an operator stopping a boot-declared server mid-run, and it is the deliberate trade: a refcount/drain protocol was rejected as complexity the removal semantics do not need.

Serialisation and idempotency

Reconcile holds NO cross-run lock of its own: safety comes from the registry read being atomic, the detacher being idempotent (an already-detached source is a no-op), and each underlying primitive (catalog deregister, registry deregister, transport close) being internally synchronised. N concurrent reconciles converge to the same state; the concurrent tests pin this.

It is DETACH-ONLY by design: attaching a declared-but-absent server (the restart-survival reconcile) is a separate, deferred concern; the live add verb is the attach path, and re-add after remove reuses the persisted agent-bound token through that verb (no second consent). One window is accepted: a reconcile racing a concurrent re-add of the same name by the SAME owner can detach the freshly re-added server (a stale declared-set read — heals at the next add or restart). The earlier process-global over-detach — where one owner's reconcile enumerated the whole registry and could detach boot servers or another owner's runtime-adds — is CLOSED by the owner-scoped reconcile view above: AttachedSources returns only the reconciling owner's runtime-added entries.

A nil registry, an empty agentID, or a nil detacher returns (0, nil) — the backward-compatible "no reconcile" path. A registry read error is returned wrapped in ErrReconcileRead (fail loud, never swallowed). Detach errors are joined and returned (the caller logs them loud) but do not abort the diff: one server that refuses to close must not strand the others. The identity triple + agentID form the owner-scoped reconcile view; the tenant + agent are the owner tag (agent_id is registration metadata here, not an isolation key — isolation stays the triple). Returns the number of servers detached.

func ReconcileDiscoveryOrigins added in v1.14.0

func ReconcileDiscoveryOrigins(ctx context.Context, reg agentcfg.Registry, agentID string, id identity.Quadruple, reconciler DiscoveryOriginReconciler) (int, error)

ReconcileDiscoveryOrigins is the ALLOWANCE-RECONCILE leg of run-start reconciliation — the rollback / set_revision path for the OAuth-discovery cross-origin allow-list. For each of the reconciling OWNER's runtime-added connections that the active revision still declares, it re-derives the connection's declared allow-list from the CURRENT revision and re-applies it to the live registry (a FULL IDEMPOTENT re-prune, not a rollback-delta). So a rollback past a grant REVOKES the origin live (the current revision no longer declares it), and any stale requirement record (e.g. from a revoke that landed mid-Discover) is corrected at the next run start — a bounded self-heal.

It is owner-scoped exactly like ReconcileConnections: AttachedSources returns only the reconciling owner's runtime-adds, so a run for owner A never touches a boot server's or tenant-B's allow-list. A detach handled by ReconcileConnections removes the source from the owner view, so a no-longer-declared connection is not re-applied here (it is torn down there).

A nil registry, an empty agentID, or a nil reconciler returns (0, nil) — the backward-compatible "no reconcile" path. A registry read error is returned wrapped in ErrReconcileRead (fail loud). Apply errors are joined and returned (logged loud by the caller) but do not abort the sweep. Returns the number of connections whose allow-list was re-applied.

func ReconcileOAuthProviders added in v1.14.0

func ReconcileOAuthProviders(ctx context.Context, reg agentcfg.Registry, agentID string, id identity.Quadruple, reconciler OAuthProviderReconciler) (int, error)

ReconcileOAuthProviders is the PROVIDER-RECONCILE leg of run-start reconciliation — the rollback / set_revision path for the Protocol-installed OAuth providers. For the reconciling OWNER it makes the live owner-tagged provider set match the CURRENT active revision's installed-provider section: a declared provider not currently installed is installed; an installed provider the current revision no longer declares is UNINSTALLED (and CLOSED). So a rollback past an install REVOKES the provider live, and a rollback of a removal re-installs it — one mechanism, N triggers (a full idempotent reconcile, not a rollback-delta).

It is owner-scoped exactly like ReconcileConnections: InstalledFor returns only the reconciling owner's installed providers, so a run for owner A never touches a boot-seeded provider's or tenant-B's install — the cross-tenant uninstall/outage is closed by owner-scoping.

A nil registry, an empty agentID, or a nil reconciler returns (0, nil). A registry read error is returned wrapped in ErrReconcileRead (fail loud). Apply errors are joined and returned (logged loud by the caller) but do not abort the sweep. Returns the number of providers whose install state changed.

func RunCompletionHookFromConfig added in v1.10.0

func RunCompletionHookFromConfig(rc config.RunCompletionHookConfig) *steering.CompletionHookSpec

RunCompletionHookFromConfig projects the static `runtime.hooks.run_completion` yaml block onto a steering.CompletionHookSpec, or nil when no static hook is configured (an empty / whitespace-only tool). It is the ONE yaml half of the hook resolution, shared by every run-loop driver (the production dev binary, the devstack twin, and the embed RunOnce path) so the yaml projection cannot drift between binaries. AgentID is left empty — the run-start resolution stamps the acting agent id; timeout defaulting happens at fire time.

Types

type ConnectionDetacher added in v1.11.0

type ConnectionDetacher interface {
	// AttachedSources returns the reconciling owner's RUNTIME-ADDED source ids
	// — the owner-scoped reconcile VIEW the reconcile diffs against the
	// declared set. It deliberately does NOT return boot-declared servers or
	// another owner's runtime-adds: the registry stays process-global and
	// deployment-shared (boot servers resolve + dispatch by bare name across
	// every session), but the reconcile VIEW is owner-scoped so one owner's
	// run-start reconcile can never detach a boot server or another owner's
	// connection. A fresh slice; the caller may retain it.
	AttachedSources(ctx context.Context, owner auth.Owner) []string
	// Detach deregisters the named source's tools from the catalog + MCP
	// registry and closes its transport gracefully. Idempotent — a source
	// already gone is a no-op the concrete swallows.
	Detach(ctx context.Context, source string) error
}

ConnectionDetacher is the driver-agnostic seam the run-start reconciliation uses to detach a no-longer-declared runtime-added MCP server at the next-turn projection boundary. The concrete (wired at the cmd/harbor + devstack boundary) deregisters the source's tools from the planner catalog view + the MCP registry and closes its transport gracefully — the physical inverse of the add path's attach. Keeping it an injected interface preserves this package's §4.4 boundary: the projection imports no concrete MCP driver (auth carries only the plain reconcile-view owner tag, not a driver).

type DiscoveryOriginReconciler added in v1.14.0

type DiscoveryOriginReconciler interface {
	// AttachedSources returns the reconciling owner's runtime-added source ids —
	// the owner-scoped reconcile VIEW (identical to ConnectionDetacher's).
	AttachedSources(ctx context.Context, owner auth.Owner) []string
	// SetOAuthDiscoveryOrigins FULL-REPLACES the named connection's allow-list on
	// the live registry (and prunes a now-unallowed recorded requirement),
	// returning the prior set. Identity-mandatory for authorization.
	SetOAuthDiscoveryOrigins(ctx context.Context, name string, origins []string) (prev []string, err error)
}

DiscoveryOriginReconciler is the driver-agnostic seam the run-start allowance-reconcile leg uses to re-apply each of the reconciling owner's runtime-added connections' OAuth-discovery allow-list to the live MCP registry. Like ConnectionDetacher it is owner-scoped: AttachedSources returns ONLY the reconciling owner's runtime-adds (never boot servers, never another owner's). The concrete (wired at the cmd/harbor + devstack boundary) delegates to the process-global bare-name registry.

type LoadingResolverAdapter added in v1.10.0

type LoadingResolverAdapter struct {
	Registry agentcfg.Registry
}

LoadingResolverAdapter adapts a Registry into the `internal/tools/protocol` package's narrow LoadingResolver seam (the `tools.describe` optional `agent_id` path) via EffectiveLoadingMode, so the SAME projection precedence backs both the run-start prompt-time projection and the read surface — one shared helper, no binary-local reimplementation (CLAUDE.md §17.6). Satisfies `tools/protocol.LoadingResolver` structurally; this package does not import `tools/protocol` to avoid a needless dependency (Go interfaces are satisfied structurally).

func (LoadingResolverAdapter) EffectiveLoading added in v1.10.0

func (a LoadingResolverAdapter) EffectiveLoading(ctx context.Context, id identity.Identity, agentID string, t tools.Tool, boot tools.LoadingMode) (tools.LoadingMode, error)

EffectiveLoading implements the `tools/protocol.LoadingResolver` seam.

type NamingResolution added in v1.12.0

type NamingResolution struct {
	Policy steering.NamingPolicy
	Model  string
}

NamingResolution is the resolved session auto-naming policy for a run: the defaulted steering.NamingPolicy plus the model the naming call should request (empty = the run's effective model / the client default). Returned by ActiveNamingPolicy when a policy is active for the run.

func ActiveNamingPolicy added in v1.12.0

func ActiveNamingPolicy(ctx context.Context, reg agentcfg.Registry, agentID string, id identity.Quadruple, yamlDefault config.RuntimeNamingConfig) (NamingResolution, bool, error)

ActiveNamingPolicy resolves the session auto-naming policy for a run at run start with next-run projection semantics. Resolution precedence is pinned here (and by a table test — CLAUDE.md §17.6): the agent-config `naming` section (when present) over the static yaml `runtime.naming` fleet default over off. The two run-loop drivers (production dev + devstack twin) call this ONE helper, so the precedence cannot drift between binaries.

A section is "active" only when its Auto is true — an agent-config section present with Auto false is an explicit per-agent off that WINS over the yaml default (returns active=false), and a config-free runtime resolves to off, so the opt-in invariant holds (no counters, no LLM calls, no events). The returned policy is defaulted (WithDefaults) so the trigger consumes concrete values.

A nil registry, an empty agentID, or an agent with no active revision / no naming section falls through to the yaml default. A registry read error is returned so the caller fails the run loudly (CLAUDE.md §13): no silent fall-through on a read failure. The active revision is read ONCE per run; the returned value is fresh (concurrent-reuse clean). Only the identity triple is used (the registry is identity-scoped, never keyed by run).

type OAuthProviderReconciler added in v1.14.0

type OAuthProviderReconciler interface {
	// InstalledFor returns the reconciling owner's installed provider names —
	// the owner-scoped reconcile VIEW.
	InstalledFor(ctx context.Context, owner auth.Owner) []string
	// InstallProvider installs (upserts) the descriptor owner-tagged (used when
	// a rollback FORWARD re-declares a provider that is not currently installed).
	// The (tenant, agentID) pair is the owner tuple (shared shape with the
	// agent-config service's ProviderInstaller so one concrete satisfies both).
	InstallProvider(ctx context.Context, tenant, agentID string, desc agentcfg.OAuthProviderDescriptor) error
	// UninstallProvider removes the named provider from the owner-tagged set and
	// CLOSES it (used when a rollback past an install no longer declares it). The
	// (tenant, agentID) pair is the reconciling owner; the set refuses a
	// cross-owner drop at its own boundary (defense in depth). The shared shape
	// with the agent-config service's ProviderInstaller lets one concrete satisfy
	// both.
	UninstallProvider(ctx context.Context, tenant, agentID, name string) error
}

OAuthProviderReconciler is the driver-agnostic seam the run-start provider-reconcile leg uses to make the reconciling owner's installed OAuth providers match its current active revision on the live owner-tagged provider set. Like ConnectionDetacher it is owner-scoped: InstalledFor returns ONLY the reconciling owner's installed providers (never boot-seeded, never another owner's). The concrete (wired at the cmd/harbor + devstack boundary) delegates to the process-global bare-name provider set.

Jump to

Keyboard shortcuts

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