config

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: 15 Imported by: 0

Documentation

Overview

Package config defines Harbor's strongly-typed configuration surface.

Configuration is loaded once at boot via Load (or LoadFromBytes for tests). After Load returns, the *Config is immutable; subsystems hold a *Config reference and read from it concurrently. This is the concurrent-reuse contract from D-025.

The struct layout uses one sub-struct per RFC §6 subsystem so that future phases can extend their slice without cross-package import cycles. Sub-structs whose owning phase has not yet shipped are reserved as empty zero-valued types — yaml `omitempty` keeps the boot-log readable.

Two struct tag conventions augment the standard yaml tag:

  • `reload:"live"` opts a field into hot-reload. The default is `restart` (whether explicitly tagged or absent). Phase 02 ships the mechanism; no field opts in yet.
  • `secret:"true"` marks a field whose value must be redacted by MarshalForLogging. A name-based fallback also redacts fields whose YAML name matches the canonical secret list.

Index

Constants

View Source
const (
	// DevHotReloadPolicyDrain waits for in-flight RunLoops to drain up
	// to DrainTimeout before forcing a stack restart. The default.
	DevHotReloadPolicyDrain = "drain"
	// DevHotReloadPolicyCancel cancels in-flight RunLoops immediately
	// on a triggered restart — no drain wait.
	DevHotReloadPolicyCancel = "cancel"
	// DevHotReloadPolicyDisabled is equivalent to Enabled: false; the
	// watcher is not started. Listed as a canonical value so an operator
	// can disable hot-reload via a single explicit field rather than
	// flipping the *bool.
	DevHotReloadPolicyDisabled = "disabled"
)

Canonical retain-in-flight policy values for DevHotReloadConfig. Centralised so cmd/harbor and the validator reference one spelling.

View Source
const DefaultHeavyOutputThresholdBytes = 32 * 1024

DefaultHeavyOutputThresholdBytes is the ONE source of the D-026 / D-022 heavy-output threshold default (32 KiB; RFC §6.10): the byte size at which the runtime promotes heavy content to artifact-backed stubs. `Defaults()` seeds `ArtifactsConfig.HeavyOutputThresholdBytes` from it; the dispatch executor's safety floor (`internal/runtime/dispatch`), the LLM-edge snapshot default (`llm.DefaultHeavyOutputThreshold`), and the search preview bound (`search.HeavyPreviewThreshold`) all reference this constant (the `DefaultSpawnDepthCap` precedent — Wave B audit follow-up). No other literal copy of the value is allowed.

View Source
const DefaultMaxRequestBytes = 4 << 20

DefaultMaxRequestBytes is the `ProtocolConfig.MaxRequestBytes` value applied when the operator configured none — 4 MiB, the spec's "moderate-size upload" posture for the Phase 73l artifacts.put pipeline.

View Source
const DefaultSkillsContextMax = 5

DefaultSkillsContextMax is the ONE source of the skills-context cap default: how many skill bodies the run loop fetches from `skills.SkillStore.Search` and hands the planner via `RunContext.SkillsContext` when `skills_context_max` is unset.

View Source
const DefaultSpawnDepthCap = 4

DefaultSpawnDepthCap is the ONE source of the spawn-depth default (Phase 110c — D-196; deduped from the former unexported mirror pair `config.defaultSpawnDepthCap` / `cmd/harbor::defaultMaxSpawnDepth`). The tool executor's constructor clamp references this constant; no other literal copy of the value is allowed.

Variables

View Source
var (
	// ErrConfigInvalid wraps any failure to parse, override, or
	// validate a configuration source. Callers should errors.Is on
	// this sentinel to distinguish "config layer rejected the input"
	// from upstream filesystem / IO errors.
	ErrConfigInvalid = errors.New("config: invalid configuration")
	// ErrConfigNotFound is returned when Load is given a path that
	// does not exist. It wraps the originating fs error so callers
	// can still errors.Is(err, fs.ErrNotExist).
	ErrConfigNotFound = errors.New("config: file not found")
)

Sentinel errors. Callers compare against these via errors.Is.

Functions

func IsValidationError

func IsValidationError(err error) bool

IsValidationError reports whether err originated in validation (vs. a parse or env-override failure). Callers who want to distinguish boot-time misconfiguration from filesystem trouble can errors.Is on ErrConfigInvalid first, then this helper.

func KnownBuiltInTools

func KnownBuiltInTools() []string

KnownBuiltInTools returns the sorted built-in allowlist as a slice. Public so the `internal/tools/builtin` mirror test can reach it without importing internal validator state.

func KnownCustomToolTypes

func KnownCustomToolTypes() []string

KnownCustomToolTypes returns the sorted allowlist of yaml-shorthand types `tools.custom[]` accepts. Public so the scaffold engine + a future drift test can read the same source of truth.

Types

type A2APeerConfig

type A2APeerConfig struct {
	URL                   string        `yaml:"url"`
	TrustTier             int           `yaml:"trust_tier"`
	LatencyTierMS         int           `yaml:"latency_tier_ms"`
	AllowInsecureLoopback bool          `yaml:"allow_insecure_loopback,omitempty"`
	AgentCardTTL          time.Duration `yaml:"agent_card_ttl,omitempty"`
}

A2APeerConfig declares an A2A peer the southbound driver may connect to. URL is required; the driver rejects HTTP schemes unless the host is loopback or `AllowInsecureLoopback` is true (AGENTS.md §7).

`TrustTier` is an operator-set integer in [1, 5] (1 = third-party, 5 = first-party). The route-scoring registry uses this to rank peers when more than one declares the same capability.

`LatencyTierMS` is an operator hint at the peer's expected p50 latency in milliseconds. Smaller values rank higher (latency is the tie-breaker among similarly-trusted peers).

`AllowInsecureLoopback` opts a loopback HTTP peer into the driver. The flag is name-checked against loopback only — a non-loopback HTTP host is still rejected regardless. Restart-required.

`AgentCardTTL` overrides the driver-level AgentCard cache TTL. Zero falls back to the driver default (10 minutes).

type ArtifactsConfig

type ArtifactsConfig struct {
	Driver                    string `yaml:"driver"`
	FSRoot                    string `yaml:"fs_root,omitempty"`
	DSN                       string `yaml:"dsn,omitempty" secret:"true"`
	HeavyOutputThresholdBytes int    `yaml:"heavy_output_threshold_bytes,omitempty"`
	S3Bucket                  string `yaml:"s3_bucket,omitempty"`
	S3Endpoint                string `yaml:"s3_endpoint,omitempty"`
	S3Region                  string `yaml:"s3_region,omitempty"`
	S3Prefix                  string `yaml:"s3_prefix,omitempty"`
	S3AccessKeyID             string `yaml:"s3_access_key_id,omitempty" secret:"true"`
	S3SecretAccessKey         string `yaml:"s3_secret_access_key,omitempty" secret:"true"`
	S3UsePathStyle            bool   `yaml:"s3_use_path_style,omitempty"`
}

ArtifactsConfig configures the ArtifactStore driver, the filesystem-driver root path, the SQL-driver connection string, and the heavy-output threshold above which the runtime mandatorily routes payloads through the store.

`Driver` selects an artifacts driver. V1 ships five drivers: `inmem` (the floor; per-process lifetime, no persistence), `fs` (single-binary production target), `sqlite` (Phase 18 — SQLite- backed, durable across restart), `postgres` (Phase 18 — Postgres-backed, durable across restart, multi-replica safe), and `s3` (Phase 19 — S3-compatible object-store-backed, durable; presigned-URL `GetRef` via the optional `Presigner` capability). Default `inmem`.

`FSRoot` is required when `Driver == "fs"`; it is the root directory under which `<root>/<tenant>/<user>/<session>/<task>/ <namespace>/<id>` blobs land. The directory is created (`os.MkdirAll`) at driver `New` time.

`DSN` is required when `Driver` is `"sqlite"` or `"postgres"`. Format:

  • SQLite: a bare file path (e.g. `/var/lib/harbor/artifacts.sqlite`) or the `:memory:` sentinel (degenerate dev case).
  • Postgres: a standard URL form (`postgres://user:pass@host:5432/db?sslmode=disable`) or pgx key-value form.

`HeavyOutputThresholdBytes` is the byte size at which the runtime mandatorily routes a payload through the ArtifactStore. Default 32 KB (D-022, RFC §6.10). Per-tool overrides land at Phase 26 via the tool catalog; the field is the runtime-wide default. Consumed by the tool dispatcher (Phase 26+) and the LLM-edge catch-all (Phase 32) — validated today so an operator's deployment is rejected for an invalid value even before the consumers land.

S3* fields configure the Phase 19 S3-style driver (AWS S3 / MinIO / Cloudflare R2 / any S3-compat backend). `S3Bucket` is required when `Driver == "s3"`. `S3Region` defaults to "us-east-1" when unset. `S3Endpoint` is the base URL for non-AWS backends (MinIO / R2); leave empty to use AWS's default endpoint resolution. `S3Prefix` is an optional path prefix that lets multiple Harbor deployments share one bucket. `S3AccessKeyID` and `S3SecretAccessKey` are optional — when both are empty the SDK's default credential chain is used (AWS_*, IRSA, instance metadata, etc.). `S3UsePathStyle` defaults to false (AWS native); flip on for MinIO / older R2 endpoints.

type AuditConfig

type AuditConfig struct{}

AuditConfig is owned by Phase 03 + later audit phases.

type CLIConfig

type CLIConfig struct {
	DevHotReload DevHotReloadConfig `yaml:"dev_hot_reload,omitempty"`
}

CLIConfig is owned by the CLI phases.

`DevHotReload` configures the Phase 65 (D-099) `harbor dev` hot-reload watcher: fsnotify-driven graceful-drain restart when a watched file changes. The block is opt-out (`Enabled: true` is the default applied by the loader). Restart-required at the harbor-dev level (a change to the block takes effect on the next `harbor dev` boot).

type Config

type Config struct {
	Server      ServerConfig      `yaml:"server"`
	Identity    IdentityConfig    `yaml:"identity"`
	Telemetry   TelemetryConfig   `yaml:"telemetry"`
	State       StateConfig       `yaml:"state"`
	LLM         LLMConfig         `yaml:"llm"`
	Governance  GovernanceConfig  `yaml:"governance"`
	Distributed DistributedConfig `yaml:"distributed,omitempty"`

	// Reserved slots for future phases — owning phase fills the body.
	Runtime   RuntimeConfig   `yaml:"runtime,omitempty"`   // owned by runtime/* phases
	Memory    MemoryConfig    `yaml:"memory,omitempty"`    // owned by memory phases
	Skills    SkillsConfig    `yaml:"skills,omitempty"`    // owned by skills phases
	Tasks     TasksConfig     `yaml:"tasks,omitempty"`     // owned by tasks phases
	Sessions  SessionsConfig  `yaml:"sessions,omitempty"`  // owned by sessions phases
	Artifacts ArtifactsConfig `yaml:"artifacts,omitempty"` // owned by artifacts phases
	Events    EventsConfig    `yaml:"events,omitempty"`    // owned by events phases
	Audit     AuditConfig     `yaml:"audit,omitempty"`     // owned by Phase 03 + audit phases
	Protocol  ProtocolConfig  `yaml:"protocol,omitempty"`  // owned by protocol phases
	CLI       CLIConfig       `yaml:"cli,omitempty"`       // owned by CLI phases
	Tools     ToolsConfig     `yaml:"tools,omitempty"`     // owned by tools subsystem phases (26 / 27 / 28 / 29)
	Planner   PlannerConfig   `yaml:"planner,omitempty"`   // owned by planner phases (D-103)

	PauseResume PauseResumeConfig `yaml:"pauseresume,omitempty"` // owned by pause/resume phases (50 / 51 / 111c — D-200)
	// contains filtered or unexported fields
}

Config is the root configuration. It is immutable after Load.

func Defaults added in v1.3.0

func Defaults() *Config

Defaults returns a *Config pre-populated with the documented non-security defaults. Security-relevant fields (JWT algorithms, audit redaction patterns) are intentionally absent so Validate fails loudly when an operator omits them.

Exported in Phase 110c (D-196): before then this baseline was loader-private, so a YAML-loaded config and a hand-built config got DIFFERENT baselines and factories compensated inconsistently (events fails loud on zero values; sessions self-defaults). `Load` starts from this same function; a headless Go consumer building a config programmatically starts here too:

cfg := config.Defaults()
cfg.LLM.Provider = "openrouter"   // required-for-core
cfg.LLM.Model = "anthropic/claude-sonnet-4"
cfg.LLM.APIKey = "env.OPENROUTER_API_KEY"
if err := cfg.ValidateCore(); err != nil { ... }

Required-for-core fields a zero-config consumer must set before `ValidateCore` passes: `LLM.Provider`, `LLM.Model`, `LLM.APIKey` (the production `bifrost` driver demands a real provider — CLAUDE.md §13 "no test stubs as production defaults"). Everything else carries a working default. The full-binary `Validate()` additionally demands the Identity (JWT) block — see ValidateCore's godoc for the split.

func Load

func Load(ctx context.Context, path string, opts ...LoadOption) (*Config, error)

Load reads a YAML configuration file at path, applies HARBOR_-prefixed environment overrides, runs Validate, and returns an immutable *Config. The returned error is wrapped under either ErrConfigNotFound (if the file is missing) or ErrConfigInvalid (parse / override / validate failure).

Options customise the load (e.g. WithLogger to redirect the deprecation-warning surface). No-option calls log via slog.Default().

func LoadFromBytes

func LoadFromBytes(ctx context.Context, data []byte, opts ...LoadOption) (*Config, error)

LoadFromBytes parses raw YAML bytes (typically from tests). It applies the same env-var overrides and validation pipeline as Load, but does not record a filesystem source — error messages will include "(source: <bytes>)" instead of a path. Options mirror Load.

func WithOverrides

func WithOverrides(c *Config, overrides map[string]string) (*Config, error)

WithOverrides applies a flat key->string override map to a previously-loaded *Config and re-validates. Keys are dotted paths matching the YAML field names ("server.bind_addr", "llm.model"). This is the seam for CLI flag layering (Phase 64) and Console pushed config (post-V1); Phase 02 ships only the mechanism.

func (*Config) LiveReloadable

func (c *Config) LiveReloadable() []string

LiveReloadable returns dotted YAML paths for every field tagged `reload:"live"`. Phase 02 ships zero live fields so this returns an empty slice; later phases that opt in extend it automatically.

func (*Config) MarshalForLogging

func (c *Config) MarshalForLogging() ([]byte, error)

MarshalForLogging produces YAML bytes with secret-shaped fields replaced by "***". Field detection prefers `secret:"true"` struct tags; falls back to the canonical name list. The result is safe to emit to slog at boot and is intended only for logging — never feed it back through Load.

func (*Config) Validate

func (c *Config) Validate() error

Validate runs every section validator and returns the first error, formatted with the offending YAML path and the source filename (when known). Nil on success.

This is the full-binary profile: it includes the Protocol-server identity ceremony (JWT algorithms / issuer / audience / JWKS) that a Runtime serving the Protocol edge MUST carry. Headless library consumers that never serve the Protocol validate with ValidateCore instead. `Load` always runs the full Validate — a YAML-loaded config is binary-shaped by definition.

func (*Config) ValidateCore added in v1.3.0

func (c *Config) ValidateCore() error

ValidateCore runs every section validator EXCEPT the Protocol-server identity ceremony (`identity.jwt_algorithms` / `issuer` / `audience` / JWKS source — the `validateIdentity` section). Phase 110c (D-196): a Go consumer embedding the Runtime headless — never serving the Protocol — is not forced to configure a JWT surface it never serves.

The profile is subtractive and minimal: ONLY the identity section is skipped. Everything a headless embedder can meaningfully configure (state / llm / events / sessions / artifacts / tasks / memory / skills / tools / planner / governance / telemetry / server / CLI) stays validated — anything ambiguous stays in core (fail-closed bias). Full `Validate()` semantics are unchanged; a config that passes Validate always passes ValidateCore.

type CustomToolConfig

type CustomToolConfig struct {
	Name        string            `yaml:"name"`
	Description string            `yaml:"description"`
	Input       map[string]string `yaml:"input,omitempty"`
	Output      map[string]string `yaml:"output,omitempty"`
}

CustomToolConfig declares one operator-defined custom tool whose Go shell is generated by `harbor scaffold` (Phase 83o / D-154). Each entry yields one `tools/<name>.go` stub + matching test file in the scaffolded project.

Fields:

  • `Name` — the catalog tool name (also drives the generated file name + Go function name). Required. Unique within `Custom`; no collision with `BuiltIn`. Operator-friendly form: lowercase + `.` separators are allowed (e.g. `weather.lookup`).
  • `Description` — one-line summary, surfaced in the generated Go comment + the planner-facing tool catalog. Required.
  • `Input` — flat map of field name → type. Generates the typed Go input struct. Type allowlist (V1.1): `string` / `integer` / `number` / `boolean` / `[]string`. Required (may be empty when the tool takes no arguments).
  • `Output` — flat map of field name → type. Same allowlist as `Input`. Required (may be empty when the tool returns nothing observable beyond success).

Restart-required.

type DevHotReloadConfig

type DevHotReloadConfig struct {
	Enabled      *bool         `yaml:"enabled,omitempty"`
	Policy       string        `yaml:"policy,omitempty"`
	DrainTimeout time.Duration `yaml:"drain_timeout,omitempty"`
	WatchRoots   []string      `yaml:"watch_roots,omitempty"`
}

DevHotReloadConfig configures the Phase 65 (D-099) `harbor dev` hot-reload watcher. The block is opt-out — the loader populates defaults that match the §13 "test stubs as production defaults" amendment for dev seams: on by default, fail-loud at boot when the configured watch root is unreadable, never silently disabled.

Fields:

  • `Enabled` — `*bool` so the loader can distinguish "operator didn't set the field" (nil → defaults to true) from "operator explicitly disabled" (&false). The CLI flag `harbor dev --no-hot-reload` is the operator-facing escape hatch — it overrides this value at boot. Default: true.
  • `Policy` — retain-in-flight policy on a triggered restart: `"drain"` (wait for in-flight RunLoops up to `DrainTimeout` before forcing close; the default), `"cancel"` (immediately cancel in-flight; no drain), or `"disabled"` (no hot-reload — equivalent to `Enabled: false`). Unknown values fail validation.
  • `DrainTimeout` — bounds the `drain` policy's wait for in-flight RunLoops. Zero / negative rejected. Default: 5s.
  • `WatchRoots` — list of paths (absolute or relative to the working directory) the fsnotify watcher monitors. The dev cmd unions this with the loaded config file's directory so a config edit also triggers a reload. Default: `[".harbor/agents"]` — the project-local Phase 66 draft-save directory.

Restart-required (no `reload:"live"` tag): the watcher itself is the reload mechanism; reconfiguring the watcher at runtime would race the watcher's own goroutine.

type DistributedConfig

type DistributedConfig struct {
	BusDriver    string `yaml:"bus_driver"`
	RemoteDriver string `yaml:"remote_driver"`
}

DistributedConfig configures Harbor's distributed contracts (Phase 22). `BusDriver` selects the MessageBus driver; `RemoteDriver` selects the RemoteTransport driver. V1 ships only `"loopback"` for both. Post-V1 phase 86 adds durable bus drivers; Phase 29 adds the A2A wire RemoteTransport driver. Restart-required (no `reload:"live"`).

type EventsConfig

type EventsConfig struct {
	Driver                   string        `yaml:"driver"`
	MaxSubscribersPerSession int           `yaml:"max_subscribers_per_session"`
	SubscriberBufferSize     int           `yaml:"subscriber_buffer_size"`
	IdleTimeout              time.Duration `yaml:"idle_timeout"`
	DropWindow               time.Duration `yaml:"drop_window"`
	ReplayBufferSize         int           `yaml:"replay_buffer_size"`
	StateDriver              string        `yaml:"state_driver,omitempty"`
	StateDSN                 string        `yaml:"state_dsn,omitempty" secret:"true"`
}

EventsConfig configures the event bus driver and its in-process limits. Phase 05 filled the previously-reserved slot with the inmem driver's defaults; Phase 06 adds ReplayBufferSize for the in-memory ring buffer that backs Replayer. Phase 57 registers the `durable` driver and adds the two optional StateStore-selection fields below without changing the existing field shape.

ReplayBufferSize=0 disables replay entirely on the inmem driver (Replay returns ErrReplayUnavailable immediately). The default applied by Load is 10000. On the `durable` driver ReplayBufferSize sizes the best-effort fallback ring used ONLY when no StateStore is configured.

StateDriver / StateDSN select the StateStore the `durable` driver (Phase 57) persists events through. They are OPTIONAL and ignored by every other driver: when Driver=="durable" and StateDriver is empty, the durable driver auto-degrades to a best-effort in-memory ring buffer and emits a loud runtime.warning (D-074) — replay is then NOT durable across restarts. StateDSN is required whenever StateDriver is a non-inmem driver (sqlite / postgres), mirroring StateConfig's driver/DSN pairing.

type GovernanceConfig

type GovernanceConfig struct {
	RepairAttempts int `yaml:"repair_attempts"`

	// DefaultTier is the tier name applied to an identity that does
	// not match a custom resolver mapping. Empty = no default tier =
	// no enforcement for unmatched identities (latent default).
	DefaultTier string `yaml:"default_tier,omitempty"`

	// IdentityTiers maps tier name to its policy bundle. Empty is the
	// latent default (no enforcement, D-044); populated tiers are
	// enforced at the LLM edge AND projected on the read-only posture
	// surface (see the type godoc above). Each entry's fields are
	// independently declared — `budget_ceiling_usd` for cost ceilings,
	// plus rate-limit + MaxTokens fields.
	IdentityTiers map[string]GovernanceTierConfig `yaml:"identity_tiers,omitempty"`
}

GovernanceConfig holds the V1 governance policy surface — per-tier cost ceilings, rate limits, and MaxTokens caps, declared through `IdentityTiers` (Phase 36a + 36b). Hot-reload is not yet wired; every field is restart-required.

**Enforcement is wired** (Phase 111a, D-198): a populated `IdentityTiers` map is composed into the enforcement Subsystem (MaxTokens → rate limit → cost ceiling) by the production assembly (`assemble.Assemble` → `governance.SetFactory` → the `llm.Open` wrapper chain) — requests ARE throttled, budgeted, and token-capped, and the same map also drives the read-only `governance.posture` Protocol surface.

**Latent default (Wave 7b scoping, D-044):** an empty `IdentityTiers` map + empty `DefaultTier` keeps the surface fully latent — no enforcement, no wrapper in the LLM chain.

**Removed in D-081 (chore/governance-config-consolidation):** the pre-Phase-36a single-knob fields `default_max_tokens`, `cost_ceiling_usd`, and `rate_limit_tps` are no longer accepted on `GovernanceConfig`. They were validated-but-ignored stubs: the loader took them, the enforcement engine never consumed them, and an operator setting `cost_ceiling_usd: 100` in YAML saw silent no-op behaviour. The loader now emits a structured `config.deprecated_field` slog warning when any of those keys appears in YAML, drops the value, and proceeds. Operators migrating from a pre-Phase-36a config build a `default` tier with the equivalent values under `IdentityTiers`.

type GovernanceRateLimitConfig

type GovernanceRateLimitConfig struct {
	Capacity       int           `yaml:"capacity,omitempty"`
	RefillTokens   int           `yaml:"refill_tokens,omitempty"`
	RefillInterval time.Duration `yaml:"refill_interval,omitempty"`
}

GovernanceRateLimitConfig is the token-bucket shape (Phase 36b). `Capacity` is the bucket ceiling (max reservable tokens). `RefillTokens` are added every `RefillInterval`. A zero `Capacity` disables the rate limit entirely.

type GovernanceTierConfig

type GovernanceTierConfig struct {
	BudgetCeilingUSD float64                   `yaml:"budget_ceiling_usd,omitempty"`
	RateLimit        GovernanceRateLimitConfig `yaml:"rate_limit,omitempty"`
	MaxTokens        int                       `yaml:"max_tokens,omitempty"`
}

GovernanceTierConfig is one tier's policy bundle (Phase 36a + 36b). Each field is independently opt-in: set the cost field only to enforce the cost ceiling, leave the rest zero-valued for latent behaviour.

`BudgetCeilingUSD` — Phase 36a. Per-identity cost ceiling. PreCall blocks when the (identity, tier) accumulator total ≥ this. 0 = no ceiling.

`RateLimit` — Phase 36b. Per-(identity, model) token bucket. Zero- valued (Capacity == 0) = no rate limit.

`MaxTokens` — Phase 36b. Per-call cap. Requests whose `MaxTokens` exceed this fail loudly with `ErrMaxTokensExceeded`. 0 = no cap.

type IdentityConfig

type IdentityConfig struct {
	JWTAlgorithms []string `yaml:"jwt_algorithms"`
	Issuer        string   `yaml:"issuer"`
	Audience      string   `yaml:"audience"`
	JWKSURL       string   `yaml:"jwks_url,omitempty"`
	JWKSFile      string   `yaml:"jwks_file,omitempty"`
}

IdentityConfig configures JWT validation. Per AGENTS.md §7 the algorithm allowlist must contain only asymmetric algorithms.

type LLMConfig

type LLMConfig struct {
	Driver               string                           `yaml:"driver"`
	Provider             string                           `yaml:"provider"`
	Model                string                           `yaml:"model"`
	APIKey               string                           `yaml:"api_key" secret:"true"`
	BaseURL              string                           `yaml:"base_url,omitempty"`
	Timeout              time.Duration                    `yaml:"timeout"`
	ContextWindowReserve float64                          `yaml:"context_window_reserve,omitempty"`
	ModelProfiles        map[string]LLMModelProfileConfig `yaml:"model_profiles,omitempty"`
	// Corrections toggles + per-model-profile-override the Phase 34
	// provider-correction layer. Omitted = enabled (production
	// default). See `LLMCorrectionsConfig` for the wire shape.
	Corrections LLMCorrectionsConfig `yaml:"corrections,omitempty"`

	// CustomProviders is the registry of operator-declared
	// OpenAI-compatible providers (Phase 33a). Each entry adds a new
	// `ModelProvider` to the bifrost account so operators can wire
	// NIM, vLLM, ollama, lm-studio, in-house gateways, or any other
	// OpenAI-compatible endpoint via yaml without per-provider Go
	// code. When `LLMConfig.Provider` matches a custom-provider
	// `Name`, the entry's `BaseURL` / `APIKeyEnvVar` / `Models` /
	// network knobs apply (the legacy single-provider fields
	// `APIKey` / `BaseURL` / `Timeout` are ignored for that case).
	CustomProviders []LLMCustomProviderConfig `yaml:"custom_providers,omitempty"`

	// NetworkDefaults applies to every provider (native + custom)
	// when the per-provider override is absent. Zero-valued fields
	// fall through to bifrost's package-level defaults (Phase 33a
	// unification of timeout/retry knobs that were previously
	// scattered). Restart-required.
	NetworkDefaults LLMNetworkDefaults `yaml:"network_defaults,omitempty"`
}

LLMConfig is the default LLM client surface for the runtime (Phase 32+).

`Driver` selects the §4.4 LLM driver. Phase 32 ships `"mock"`; Phase 33 registers `"bifrost"`. Empty defaults to `"mock"` so a missing configuration value does NOT silently route real LLM traffic — operators opt in to bifrost explicitly.

`Provider` / `Model` / `APIKey` / `BaseURL` / `Timeout` are the per-bifrost-driver knobs. They are REQUIRED when `Driver != "mock"` — `validateLLM` enforces. The mock driver ignores them.

`ContextWindowReserve` is the safety-net token-budget margin (default 0.05 / 5%). The safety pass fails with `ErrContextWindowExceeded` when the estimated token count is within this fraction of a model's configured cap. Range [0.0, 1.0).

`ModelProfiles` carries per-model knobs (context-window cap, estimator, JSON-schema mode, default max tokens, reasoning effort, cost overrides). The safety net's token-budget guard REQUIRES a profile entry for every model the request mentions; missing profiles surface at request time as `ErrUnsupportedModel`.

type LLMCorrectionsConfig

type LLMCorrectionsConfig struct {
	Enabled *bool `yaml:"enabled,omitempty"`
}

LLMCorrectionsConfig is the top-level toggle for the Phase 34 per-provider correction layer. The layer is enabled by default; operators set `enabled: false` only for testing scenarios that need to exercise the safety pass in isolation.

`Enabled` is `*bool` so the loader can distinguish "operator didn't set the field" (nil → defaults to true) from "operator explicitly disabled" (&false). Restart-required.

type LLMCorrectionsProfileConfig

type LLMCorrectionsProfileConfig struct {
	MessageOrdering        string `yaml:"message_ordering,omitempty"`
	SchemaMode             string `yaml:"schema_mode,omitempty"`
	ReasoningEffortRouting string `yaml:"reasoning_effort_routing,omitempty"`
	ResponseFormatShape    string `yaml:"response_format_shape,omitempty"`
	UsageBackfillEnabled   bool   `yaml:"usage_backfill_enabled,omitempty"`
}

LLMCorrectionsProfileConfig is one per-model profile override for the Phase 34 correction layer. Each field is the operator-facing string form of the equivalent `internal/llm` enum (`MessageOrderingPolicy`, `SchemaSanitizationMode`, `ReasoningRouting`, `ResponseFormatProfile`). Empty string → use the per-provider default keyed by model-name prefix.

Valid enum values:

  • `message_ordering`: "" / "system_first_strict"
  • `schema_mode`: "" / "openai_strict" / "permissive"
  • `reasoning_effort_routing`: "" / "thinking_model"
  • `response_format_shape`: "" / "json_only" / "anthropic"
  • `usage_backfill_enabled`: bool

type LLMCostOverridesConfig

type LLMCostOverridesConfig struct {
	InputPer1M     float64 `yaml:"input_per_1m_tokens"`
	OutputPer1M    float64 `yaml:"output_per_1m_tokens"`
	ReasoningPer1M float64 `yaml:"reasoning_per_1m_tokens,omitempty"`
	Currency       string  `yaml:"currency,omitempty"`
}

LLMCostOverridesConfig is a per-model cost-table override (used when the provider response doesn't include cost). Phase 36a reads.

type LLMCustomProviderConfig

type LLMCustomProviderConfig struct {
	Name                 string            `yaml:"name"`
	BaseURL              string            `yaml:"base_url"`
	APIKeyEnvVar         string            `yaml:"api_key_env_var"`
	Models               []string          `yaml:"models"`
	BaseProviderType     string            `yaml:"base_provider_type,omitempty"`
	Timeout              time.Duration     `yaml:"timeout,omitempty"`
	MaxRetries           int               `yaml:"max_retries,omitempty"`
	RetryBackoffInitial  time.Duration     `yaml:"retry_backoff_initial,omitempty"`
	RetryBackoffMax      time.Duration     `yaml:"retry_backoff_max,omitempty"`
	Concurrency          int               `yaml:"concurrency,omitempty"`
	BufferSize           int               `yaml:"buffer_size,omitempty"`
	RequestPathOverrides map[string]string `yaml:"request_path_overrides,omitempty"`
}

LLMCustomProviderConfig declares one operator-configured OpenAI-compatible LLM endpoint (Phase 33a). At least one entry is required when `LLMConfig.Provider` names a non-native provider.

Fields:

  • `Name` — the `ModelProvider` identifier the operator picks (e.g. `"nim"`, `"vllm"`). Must be unique across the list AND must not collide with bifrost's native provider names.
  • `BaseURL` — the OpenAI-compatible endpoint root (e.g. `"https://integrate.api.nvidia.com/v1"`). Required.
  • `APIKeyEnvVar` — the environment variable name (no `env.` prefix; operator writes `"NVIDIA_API_KEY"`, driver resolves `os.Getenv(...)` at construction time). Missing → fail-closed at `New` (`ErrMissingAPIKey`).
  • `Models` — the model-name allowlist bifrost forwards to this provider. At least one entry required.
  • `BaseProviderType` — wire family. Phase 33a accepts only `""` (default to `"openai"`) and `"openai"`. Future phases widen.
  • `Timeout` / `MaxRetries` / `RetryBackoff*` / `Concurrency` / `BufferSize` — per-provider overrides. Zero-valued → fall back to `LLMConfig.NetworkDefaults`, which itself falls back to bifrost's package-level defaults.
  • `RequestPathOverrides` — optional `RequestType` → custom URL path map (forwarded to bifrost's `CustomProviderConfig`). Used when an OpenAI-compatible endpoint hosts e.g. `/chat/completions` at the root instead of `/v1/chat/completions`.

type LLMModelProfileConfig

type LLMModelProfileConfig struct {
	ContextWindowTokens int                     `yaml:"context_window_tokens"`
	TokenEstimator      string                  `yaml:"token_estimator,omitempty"`
	JSONSchemaMode      string                  `yaml:"json_schema_mode,omitempty"`
	DefaultMaxTokens    *int                    `yaml:"default_max_tokens,omitempty"`
	ReasoningEffort     string                  `yaml:"reasoning_effort,omitempty"`
	CostOverrides       *LLMCostOverridesConfig `yaml:"cost_overrides,omitempty"`
	// Corrections — per-model overrides for the Phase 34 correction
	// layer. nil → use the per-provider defaults baked into
	// `internal/llm/corrections.defaultProfileFor`. See
	// `LLMCorrectionsProfileConfig` for the wire shape.
	Corrections *LLMCorrectionsProfileConfig `yaml:"corrections,omitempty"`
	// MaxRetries (Phase 36) — caps the validator-driven retry loop the
	// `internal/llm/retry` wrapper runs against this model. Zero (the
	// default) maps to `llm.DefaultMaxRetries` (1). Negative is
	// rejected by `validateLLM`.
	MaxRetries int `yaml:"max_retries,omitempty"`
}

LLMModelProfileConfig is one entry in `LLMConfig.ModelProfiles`. Keyed by canonical model name (e.g. `"anthropic/claude-sonnet-4"`, `"google/gemini-3.1-flash-lite"`). Phase 32 ships the shape; Phase 33+ consume the fields.

  • `ContextWindowTokens` is the model's hard input-token cap. REQUIRED (> 0); the safety net's token-budget guard uses it.
  • `TokenEstimator` selects the estimator algorithm. "" / "chars_div_4" — default chars/4 + per-message overhead. Phase 33+ may register tiktoken-equivalent estimators by name.
  • `JSONSchemaMode` — Phase 35 reads ("native" / "tools" / "prompted"). Phase 32 stores opaque.
  • `DefaultMaxTokens` — Phase 36b's identity-tier override target. nil → use the runtime/governance default.
  • `ReasoningEffort` — request-level default. Empty string → "use provider default."
  • `CostOverrides` — fallback per-1M-token rates when the provider doesn't include cost in its response. Phase 36a reads when accumulating identity-scoped cost.

type LLMNetworkDefaults

type LLMNetworkDefaults struct {
	Timeout             time.Duration `yaml:"timeout,omitempty"`
	MaxRetries          int           `yaml:"max_retries,omitempty"`
	RetryBackoffInitial time.Duration `yaml:"retry_backoff_initial,omitempty"`
	RetryBackoffMax     time.Duration `yaml:"retry_backoff_max,omitempty"`
	Concurrency         int           `yaml:"concurrency,omitempty"`
	BufferSize          int           `yaml:"buffer_size,omitempty"`
}

LLMNetworkDefaults are the operator-tunable defaults that apply to every provider (native + custom) when the per-provider override is absent (Phase 33a). Zero-valued fields fall through to bifrost's package-level defaults so an operator who omits the block sees today's Phase 33 behaviour unchanged.

type LoadOption

type LoadOption func(*loadConfig)

LoadOption customises a Load / LoadFromBytes call. Options compose in declaration order; later options override earlier ones for the same setting.

func WithLogger

func WithLogger(l *slog.Logger) LoadOption

WithLogger overrides the slog.Logger the config loader emits structured warnings on (e.g. the `config.deprecated_field` warning surfaced when a removed YAML key appears in a config — D-081). A nil logger keeps the default (`slog.Default()`); callers that want to capture the warnings in a test build a logger over a bytes buffer and pass it here.

type MCPServerConfig

type MCPServerConfig struct {
	Name          string            `yaml:"name"`
	TransportMode string            `yaml:"transport_mode"`
	URL           string            `yaml:"url,omitempty"`
	Command       []string          `yaml:"command,omitempty"`
	Headers       map[string]string `yaml:"headers,omitempty" secret:"true"`
	KeepAlive     time.Duration     `yaml:"keep_alive,omitempty"`
	// Policy is the per-server default tool reliability policy
	// (Phase 26b). Optional; nil preserves today's behaviour exactly
	// (every tool inherits `tools.DefaultPolicy()` — 30 s per-attempt
	// deadline / 4 total attempts). When set, it projects to the
	// `mcpdrv.Config.DefaultPolicy` applied to every tool this server
	// registers. Per-field zero values fall through to
	// `tools.DefaultPolicy()` (see `ToolPolicyConfig`). Restart-required.
	Policy *ToolPolicyConfig `yaml:"policy,omitempty"`
	// ToolPolicies are per-tool overrides keyed by the MCP tool's name
	// (the server-side name, NOT the `<source>_<tool>` Harbor-facing
	// name). A tool named here uses the override instead of `Policy`
	// (or the package default). Optional; empty preserves today's
	// behaviour. Restart-required.
	ToolPolicies map[string]ToolPolicyConfig `yaml:"tool_policies,omitempty"`
}

MCPServerConfig is one MCP southbound attachment. `Name` is the source-id prefix (must be unique across servers); `TransportMode` selects the wire transport (`auto` / `sse` / `streamable_http` / `stdio`); `URL` is required for HTTP-flavoured transports; `Command` is required for stdio (argv form ONLY — see `internal/tools/drivers/mcp/transport_stdio.go` for the §7 security rule). `Headers` are operator-supplied auth headers (treated as secrets for redaction). `KeepAlive` is the session-ping interval; zero disables.

Restart-required.

type MemoryConfig

type MemoryConfig struct {
	Driver             string `yaml:"driver"`
	DSN                string `yaml:"dsn,omitempty" secret:"true"`
	Strategy           string `yaml:"strategy,omitempty"`
	BudgetTokens       int    `yaml:"budget_tokens,omitempty"`
	RecoveryBacklogMax int    `yaml:"recovery_backlog_max,omitempty"`
}

MemoryConfig is owned by the memory subsystem phases.

`Driver` selects a `memory.MemoryStore` driver. Phase 23 ships only `"inmem"`; Phase 25 adds `"sqlite"` and `"postgres"`. Default `inmem`.

`DSN` is required when `Driver` is `"sqlite"` or `"postgres"`. The format mirrors the StateStore + ArtifactStore drivers (bare file path or `file:` URI for SQLite; libpq-compatible connection string for Postgres). `secret:"true"` redacts the value in audit-redacted logs.

`Strategy` selects the memory shape: `"none"` (Phase 23), or `"truncation"` / `"rolling_summary"` (Phase 24). Default `none`. `memory.Open` rejects strategies the configured driver does not implement with `ErrStrategyNotImplemented`.

`BudgetTokens` is the truncation / rolling-summary budget cap (token estimate). Zero means "no budget" — appending is unbounded.

`RecoveryBacklogMax` is the bounded queue size for the `rolling_summary` strategy's recovery loop (D-035). Default 16 (applied by the loader when the section is omitted). Overflow drops oldest and emits `memory.recovery_dropped` on the bus. Ignored by the `none` and `truncation` strategies.

Restart-required (no `reload:"live"`).

type PauseResumeConfig added in v1.3.0

type PauseResumeConfig struct {
	MaxParkDuration time.Duration `yaml:"max_park_duration"`
	SweepInterval   time.Duration `yaml:"sweep_interval"`
}

PauseResumeConfig configures the pause lifecycle (Phase 111c / D-200; RFC §3.3 + §6.3).

`MaxParkDuration` is the ceiling on how long a pause may stay parked before the runtime's pause sweeper resumes it with the typed `timeout` Decision (`pause.resumed`, D-096) and the waiting run terminates as a constraints-conflict. Zero (the default) means pauses never expire and the sweeper is not started — the pre-111c behaviour. Negative values are rejected by validation.

`SweepInterval` is the sweeper's scan cadence. Default 1m; 0 means the default applies (the block is off-by-default, so a hand-built Config without it stays valid); negative values are rejected. When expiry is enabled it must not exceed `MaxParkDuration` (a pause must not overstay its deadline by more than one sweep).

Fields are not hot-reloadable in V1 (changing the sweep cadence at runtime would race with the sweeper goroutine — same posture as SessionsConfig).

type PlannerConfig

type PlannerConfig struct {
	Driver                 string                  `yaml:"driver,omitempty"`
	MaxSteps               int                     `yaml:"max_steps,omitempty"`
	ExtraGuidance          string                  `yaml:"extra_guidance,omitempty"`
	ReasoningReplay        string                  `yaml:"reasoning_replay,omitempty"`
	MaxToolExamplesPerTool int                     `yaml:"max_tool_examples_per_tool,omitempty"`
	ParallelToolCalls      *bool                   `yaml:"parallel_tool_calls,omitempty"`
	SkillsContextMax       int                     `yaml:"skills_context_max,omitempty"`
	AbsoluteMaxSpawnDepth  int                     `yaml:"absolute_max_spawn_depth,omitempty"`
	TokenBudget            int                     `yaml:"token_budget,omitempty"`
	PlanningHints          PlannerPlanningHintsCfg `yaml:"planning_hints,omitempty"`
	Extra                  map[string]string       `yaml:"extra,omitempty"`
}

PlannerConfig selects the planner concrete the runtime constructs at boot. The §4.4 seam pattern applied to the planner — closes D-097's "future phases will read cfg.Planner" note and CLAUDE.md §1.3's swappable-planner property gap (D-103, closes issue #126). Mirrors D-095's `ToolOAuthProviderConfig` structurally — same shape, same validator pattern, same registry pattern (`internal/planner` driver registry; `cmd/harbor/main.go` blank-imports each driver).

`Driver` selects a self-registered planner driver in `internal/planner/<name>/`. V1 ships only the `react` driver (the reference LLM-driven ReAct planner — Phase 45 / D-051). Empty defaults to `react` so a missing configuration value boots with the V1 reference planner unchanged; operators opt into alternates explicitly when later phases land them (Plan-Execute, Workflow, Graph, Deterministic, Supervisor, MultiAgent, HumanApproval per RFC §6.2).

`MaxSteps` overrides the driver-side circuit-breaker step cap. Zero (the default) means "use the driver's internal default" — e.g. the react driver's `react.DefaultMaxSteps` (12). The validator rejects negative values pre-boot.

`ExtraGuidance` is operator-supplied domain-specific guidance injected into the rendered ReAct system prompt's `<additional_guidance>` section (Phase 83a, RFC §6.2). Empty (the default) omits the section entirely. The string is rendered verbatim — the operator owns its content hygiene. It flows to the react driver's `WithSystemPromptExtra` Option at construction; other drivers ignore it. The validator imposes no rule beyond "string" — operator copy is operator copy.

`ReasoningReplay` controls whether the ReAct planner re-injects a prior step's captured provider-side reasoning trace into the next turn's prompt (Phase 83e — D-148). The enum has two values: `never` (the default for ALL models — a prior step's captured reasoning is never re-injected) and `text` (prepend the captured trace as a text block above the prior `{tool, args}` action JSON). Empty defaults to `never`; the validator rejects any other value loudly pre-boot. There is no `provider_native` mode in V1.

`MaxToolExamplesPerTool` caps how many curated examples the ReAct prompt's `<available_tools>` section renders per tool (Phase 83b — D-144). Examples are ranked `minimal` > `common` > `edge-case` > untagged; the renderer keeps the top N. Zero (the default) resolves to 3 inside the react driver. The validator rejects negative values loudly pre-boot. Other drivers ignore it.

`ParallelToolCalls` toggles native parallel tool-call emission (Phase 107d — D-169). Pointer-bool so an omitted key (nil) resolves to `true` — the React planner emits a native `CallParallel` when the LLM returns N>1 tool-calls in one response, and the dev `ToolExecutor` dispatches the branches concurrently. An explicit `false` selects the Phase 107c serialization fallback (one `CallTool` per step via `RunContext.PendingToolCalls`). It flows to the react driver's `WithParallelToolCalls` Option only when non-nil; other drivers ignore it. No validator rule beyond "bool" — both states are correct.

`SkillsContextMax` caps how many skill bodies the run loop fetches from `skills.SkillStore.Search` and hands the planner via `RunContext.SkillsContext` (Phase 83f — D-149). Zero (the default) resolves to `DefaultSkillsContextMax` (5) via `SkillsContextMaxResolved()` — the one source of the default since Phase 110c (D-196); the validator rejects negatives loudly pre-boot. Consumed by the per-task run loop (cmd + devstack + headless assemblies); library consumers that build their own RunContext resolve through the same method.

`PlanningHints` is the operator-supplied per-run planning steering the dev run loop projects onto `RunContext.PlanningHints` (Phase 83f — D-149). Renders into the ReAct prompt's `<planning_constraints>` via 83c. V1.1 ships only `Constraints` and `PreferredTools`; the richer Go-struct fields (`ParallelGroups`, `DisallowTools`, `Budget`) remain reachable via a custom planner Option, not via `harbor.yaml`. The block is omitted entirely when empty.

`TokenBudget` is the trajectory-compression threshold (Phase 111e — D-202). When > 0 the per-task run loop projects it onto `RunSpec.Base.Budget.TokenBudget` and the runtime assembly constructs the trajectory compression runner (the LLM-backed `TrajectorySummariser` over the configured LLM client); the steering RunLoop then invokes `MaybeCompress` at each step boundary, compacting an over-budget trajectory into `Trajectory.Summary` (one compression per run at V1.1.x). Zero (the default) disables compression entirely — today's behaviour. The validator rejects negative values loudly pre-boot. Requires a configured `llm` block when non-zero (the summariser needs a real client; fail-loud at assembly).

`Extra` is the per-driver opaque extras map. Reserved for future drivers' per-flow knobs (e.g. a deterministic planner's scripted step sequence, a supervisor planner's sub-agent list). The V1 `react` driver ignores it.

Restart-required (no `reload:"live"` tag): swapping a planner at runtime would race with in-flight RunLoop goroutines holding the old concrete.

func (PlannerConfig) ParallelToolCallsEnabled added in v1.2.0

func (p PlannerConfig) ParallelToolCallsEnabled() bool

ParallelToolCallsEnabled resolves the optional `parallel_tool_calls` knob (Phase 107d — D-169). Nil (unset) resolves to `true` — native parallel tool-call emission is the default. A non-nil value is honoured verbatim. The dev stack reads this when no driver-boundary passthrough is wanted; the `*bool` itself flows through the planner boundary so the react factory can distinguish "unset" from an explicit `false`.

func (PlannerConfig) SkillsContextMaxResolved added in v1.3.0

func (p PlannerConfig) SkillsContextMaxResolved() int

SkillsContextMaxResolved resolves the optional `skills_context_max` knob (Phase 83f — D-149). A non-positive value (unset or zero) resolves to `DefaultSkillsContextMax`. Phase 110c (D-196) re-homed the zero→default resolution here from run-loop literals (the cmd/devstack pair each carried its own `= 5` constant — the D-155-class duplicate-default mechanism).

func (PlannerConfig) SpawnDepthCap added in v1.2.0

func (p PlannerConfig) SpawnDepthCap() int

SpawnDepthCap resolves the optional `absolute_max_spawn_depth` knob (Phase 107e — D-170). A non-positive value (unset or zero) resolves to `DefaultSpawnDepthCap`: a SpawnTask whose child would exceed this ParentTaskID-chain depth is rejected loudly so a background sub-agent cannot recurse without bound. The cap bounds depth, not breadth.

type PlannerPlanningHintsCfg

type PlannerPlanningHintsCfg struct {
	// Constraints is free-form text rendered verbatim into the
	// `<planning_constraints>` section. Voice/tone rules, hard
	// negative-space guidance ("never call X without prior approval"),
	// or domain-specific constraints. Empty omits the line.
	Constraints string `yaml:"constraints,omitempty"`
	// PreferredTools lists tool names the planner should prefer when
	// multiple satisfy the same goal. Empty omits the line.
	PreferredTools []string `yaml:"preferred_tools,omitempty"`
}

PlannerPlanningHintsCfg is the YAML-facing subset of the planner's `PlanningHints` (Phase 83f — D-149). V1.1 ships two fields; the richer Go-struct fields stay reachable through a custom planner Option, not the YAML surface. An empty struct projects to nil on `RunContext.PlanningHints`, so the `<planning_constraints>` prompt section is omitted.

func (PlannerPlanningHintsCfg) IsZero

func (p PlannerPlanningHintsCfg) IsZero() bool

IsZero reports whether every field on the YAML config is empty — the dev run loop reads this to decide whether to project onto `RunContext.PlanningHints` (nil) or to allocate a populated struct.

type ProjectedToolPolicy added in v1.2.0

type ProjectedToolPolicy struct {
	// TimeoutMS is the per-attempt deadline in milliseconds; 0 means
	// "inherit the default" at the runtime layer.
	TimeoutMS int
	// MaxRetries is `max_attempts - 1` when `max_attempts >= 1`, else
	// 0 (inherit the default total attempt count).
	MaxRetries int
	// BackoffBase / BackoffMax are the projected backoff base / cap; a
	// zero Duration inherits the default.
	BackoffBase time.Duration
	BackoffMax  time.Duration
	// BackoffMult is the per-retry multiplier; 0 inherits the default.
	BackoffMult float64
	// RetryOn carries the validated error-class strings (`transient` /
	// `timeout` / `5xx` / `permanent`). nil/empty inherits the default
	// allowlist. The strings are validated against the allowlist by
	// ToToolPolicy; the runtime copy turns them into `tools.ErrorClass`.
	RetryOn []string
	// RetryOnEmpty, when true, instructs the runtime copy to build an
	// EXPLICIT empty (non-nil) `RetryOn` slice — the policy shell reads
	// that as "retry on nothing" (exactly one attempt). Set by
	// ToToolPolicy when `max_attempts == 1` and the operator named no
	// `retry_on` allowlist; see the comment in ToToolPolicy for why the
	// MaxRetries:0 fall-through alone is insufficient. Mutually
	// exclusive with a populated RetryOn.
	RetryOnEmpty bool
}

ProjectedToolPolicy is the cycle-free, primitive-only image of a `ToolPolicyConfig` after projection. Its fields map 1:1 onto `tools.ToolPolicy`; the binary entry point (`cmd/harbor`) performs the trivial final copy into the runtime type. Splitting the projection this way keeps `internal/config` free of an import cycle (`tools` → `events` → `config`) while preserving CLAUDE.md §13's single-source rule: there is exactly ONE place that interprets the operator-facing `ToolPolicyConfig` fields — `ToToolPolicy` below — and the `tools.ToolPolicy` STRUCT remains the single definition of the policy shape.

Per-field zero-value semantics are preserved: a field that the operator omitted (its YAML value is the Go zero value) is left at the zero value here, so `tools.ToolPolicy`'s own per-field `resolved()` fall-through fills it with the package default at dispatch time. The projection NEVER substitutes a default itself — the single exception is the `max_attempts → MaxRetries` arithmetic, which only fires when `max_attempts >= 1` (an omitted `max_attempts` leaves `MaxRetries` zero, inheriting the default attempt count).

type ProtocolConfig

type ProtocolConfig struct {
	MaxRequestBytes int `yaml:"max_request_bytes,omitempty"`
}

ProtocolConfig is owned by the protocol-server phases.

`MaxRequestBytes` is the Phase 73l (D-120) upper bound on an `artifacts.put` upload body. A body above this is rejected with the canonical `CodeRequestTooLarge` / HTTP 413 — fail loud, never silently truncate. Optional; a zero value resolves to `DefaultMaxRequestBytes` (4 MiB) at load time. Operators with a larger upload need adjust this key and the Console's browser-side size hint accordingly.

func (ProtocolConfig) ResolvedMaxRequestBytes

func (c ProtocolConfig) ResolvedMaxRequestBytes() int

ResolvedMaxRequestBytes returns the configured `MaxRequestBytes`, substituting `DefaultMaxRequestBytes` when the field is zero or negative. The protocol-server boot path calls this so a fresh config produces a working upload bound with zero ceremony.

type RuntimeConfig

type RuntimeConfig struct{}

RuntimeConfig is owned by runtime/* phases (engine, streaming, cancellation, backpressure).

type ServerConfig

type ServerConfig struct {
	BindAddr            string        `yaml:"bind_addr"`
	ShutdownGracePeriod time.Duration `yaml:"shutdown_grace_period"`
	AllowedOrigins      []string      `yaml:"allowed_origins,omitempty"`
	CORSDevAllowAny     bool          `yaml:"cors_dev_allow_any,omitempty"`
}

ServerConfig is the network surface for the Harbor binary.

AllowedOrigins is the Phase 83v CORS allowlist for the D-091 multi-process Console+Runtime posture. Each entry is an exact origin (`scheme://host[:port]`) the Runtime accepts cross-origin requests from. Empty list (the default) = no CORS headers = same- origin only (the pre-83v posture). The validator rejects `*` (or any wildcard shape) unless CORSDevAllowAny is true. See D-162 + CLAUDE.md §7 — never wildcard in production.

CORSDevAllowAny is the explicit, dev-only escape hatch that opens the CORS surface to ANY origin. NEVER set in production: a `harbor dev` boot with this flag set prints a stderr banner so the posture is visibly dev-only, and the validator refuses `*` shapes without it. Provided for first-clone Console iteration against a `harbor dev` loop where the Console origin (Vite, `:5173`) varies during development.

type SessionsConfig

type SessionsConfig struct {
	IdleTTL       time.Duration `yaml:"idle_ttl"`
	HardCap       time.Duration `yaml:"hard_cap"`
	SweepInterval time.Duration `yaml:"sweep_interval"`
}

SessionsConfig configures the SessionRegistry's GC sweeper. Defaults match RFC §6.9: idle TTL 24h, hard cap 30 days, sweep every 15 min. Fields are not hot-reloadable in V1 (changing GC cadence at runtime would race with the sweeper goroutine).

type SkillsConfig

type SkillsConfig struct {
	Driver    string                `yaml:"driver,omitempty"`
	DSN       string                `yaml:"dsn,omitempty" secret:"true"`
	Directory SkillsDirectoryConfig `yaml:"directory,omitempty"`
}

SkillsConfig is owned by the skills subsystem phases.

`Driver` selects a `skills.SkillStore` driver. Phase 37 ships the `"localdb"` driver only; Phase 49 (Portico) adds `"portico"`. Default `localdb`.

`DSN` is required when `Driver` is `"localdb"`. The format mirrors the StateStore + MemoryStore drivers (bare file path or `file:` URI for SQLite; `:memory:` honoured for tests). `secret:"true"` redacts the value in audit-redacted logs.

`Directory` shapes the Phase-39 virtual directory the run loop injects as the per-turn `<skills_context>` prompt block (Phase 111d — D-201). All fields optional; restart-required.

type SkillsDirectoryConfig added in v1.3.0

type SkillsDirectoryConfig struct {
	Pinned     []string `yaml:"pinned,omitempty"`
	MaxEntries int      `yaml:"max_entries,omitempty"`
	Selection  string   `yaml:"selection,omitempty"`
}

SkillsDirectoryConfig configures the skills virtual directory (Phase 39 / D-052) the run loop consumes as the `<skills_context>` producer (Phase 111d — D-201). The injected block is a bounded, stable, pinned-then-recent browse window — identity-scoped, capability-filtered, redacted; per-query relevance retrieval stays the LLM's job via the `skill_search` meta-tool.

  • `Pinned` anchors the named skills at the top of every view, in declaration order. Pinning is an ORDERING preference — pinned skills are never exempt from the capability filter.
  • `MaxEntries` caps the view length. 0 (the default) falls back to `planner.skills_context_max`'s resolved value (default 5) so the pre-111d injection-budget knob keeps its meaning; explicit values must sit in [1, 200].
  • `Selection` orders the unpinned remainder: `pinned_then_recent` (default — UpdatedAt DESC). `pinned_then_top` (UseCount DESC) is a canonical library-level Selection but is REJECTED by the operator validator until a production usage-bump path lands — no shipped path increments UseCount, so the ordering would silently degrade to alphabetical (Wave C checkpoint audit; D-201 addendum).

type StateConfig

type StateConfig struct {
	Driver string `yaml:"driver"`
	DSN    string `yaml:"dsn,omitempty" secret:"true"`
}

StateConfig selects the StateStore driver and its connection.

type TasksConfig

type TasksConfig struct {
	Driver               string        `yaml:"driver"`
	RetainTurnTimeout    time.Duration `yaml:"retain_turn_timeout"`
	ContinuationHopLimit int           `yaml:"continuation_hop_limit"`
}

TasksConfig configures the TaskRegistry driver and the Phase 21 backgroundtasks-config knobs. `Driver` selects the registered driver name; Phase 20 ships only `"inprocess"`.

`RetainTurnTimeout` is the maximum time the runtime engine will block a foreground turn waiting for retain-turn groups to resolve. Defaults to 5 minutes (RFC §6.8); zero or negative values are rejected by validation. Consumed by the engine wiring scheduled for Phase 60+ (runtime↔tasks integration); validated today so an operator's deployment is rejected for an invalid value even before the consumer lands.

`ContinuationHopLimit` caps the number of background-continuation hops a planner runtime may take before requiring user confirmation. Defaults to 8 (RFC §6.8); zero or negative values are rejected by validation. Consumed by the planner concretes (Phase 42+); same "validate today, consume later" pattern as `RetainTurnTimeout`.

Restart-required (no `reload:"live"` tag).

type TelemetryConfig

type TelemetryConfig struct {
	LogFormat    string `yaml:"log_format"`
	LogLevel     string `yaml:"log_level"`
	OTelEndpoint string `yaml:"otel_endpoint,omitempty"`
	ServiceName  string `yaml:"service_name"`
}

TelemetryConfig configures slog and OpenTelemetry export.

type ToolApprovalConfig

type ToolApprovalConfig struct {
	// Policy names which bundled approval policy to apply. The
	// catalog builder maps this name onto a concrete
	// `approval.ApprovalPolicy` instance. Allowed values:
	//   - "deny-all"      → `approval.AlwaysDenyPolicy`
	//   - "approve-all"   → `approval.AlwaysApprovePolicy` (dev only)
	//   - "tagged"        → `approval.TaggedPolicy` (consults
	//                       `RequireTags` below)
	// An unknown policy value fails the catalog build with a wrapped
	// error naming the offending value.
	Policy string `yaml:"policy"`
	// Reason is the operator-facing classification carried on
	// `tool.approval_requested`. Optional — the bundled policies
	// supply a sensible default.
	Reason string `yaml:"reason,omitempty"`
	// RequireTags is consulted by the `tagged` policy. An entry whose
	// `Policy: tagged` AND `RequireTags: []` is rejected — the
	// tagged policy with no tags is a no-op (configuration smell).
	RequireTags []string `yaml:"require_tags,omitempty"`
}

ToolApprovalConfig declares an approval-gate wiring for one tool. Phase 64a / D-090.

type ToolEntryConfig

type ToolEntryConfig struct {
	// Name is the catalog tool name the entry applies to. Required.
	// The catalog builder fails closed when no tool registered with
	// this name resolves at boot.
	Name string `yaml:"name"`
	// LoadingMode controls when this tool appears in the planner's
	// prompt-time catalog. "" or "always" = every turn (default).
	// "deferred" = hidden by default; the LLM discovers via meta-tools.
	// Phase 107c / D-167.
	LoadingMode string `yaml:"loading_mode,omitempty"`
	// Approval declares an approval-gate wiring for this tool. Omit
	// for tools that need no gating. When present, `Approval.Policy`
	// MUST be one of the canonical policy names; an unknown value
	// fails closed.
	Approval *ToolApprovalConfig `yaml:"approval,omitempty"`
	// OAuth declares an OAuth binding for this tool. Omit for tools
	// that need no OAuth. When present, `OAuth.Provider` MUST name a
	// configured OAuth source and `OAuth.BindingScope` MUST be one of
	// "user" / "agent" (Phase 30 D-083).
	OAuth *ToolOAuthConfig `yaml:"oauth,omitempty"`
}

ToolEntryConfig is one per-tool catalog wiring declaration. Phase 64a / D-090. The shape is intentionally small: the catalog builder looks up the registered tool by `Name`, then applies whichever of `Approval` and / or `OAuth` are populated.

Layout in YAML:

tools:
  entries:
    - name: delete_doc
      approval:
        policy: deny-all
        reason: "deletion requires human review"
    - name: github.repo.read
      oauth:
        provider: github
        binding_scope: user
    - name: write_to_prod
      approval:
        policy: tagged
        require_tags: ["sensitive", "write:prod"]
      oauth:
        provider: prod-api
        binding_scope: agent

An empty `Approval` AND `OAuth` block is rejected at validation time (an entry with no middleware to apply is a configuration typo).

Restart-required.

type ToolOAuthConfig

type ToolOAuthConfig struct {
	// Provider names the OAuth source the tool binds to. The catalog
	// builder consults the configured OAuth registry; a name that
	// resolves to no source fails the catalog build loud.
	Provider string `yaml:"provider"`
	// BindingScope is "user" or "agent" (Phase 30 / D-083). An
	// invalid value fails the catalog build with a wrapped error
	// naming the offending value.
	BindingScope string `yaml:"binding_scope"`
}

ToolOAuthConfig declares an OAuth binding for one tool. Phase 64a / D-090.

type ToolOAuthProviderConfig

type ToolOAuthProviderConfig struct {
	Name            string            `yaml:"name"`
	Driver          string            `yaml:"driver"`
	ClientIDEnv     string            `yaml:"client_id_env"`
	ClientSecretEnv string            `yaml:"client_secret_env"`
	Scopes          []string          `yaml:"scopes,omitempty"`
	AuthURL         string            `yaml:"auth_url,omitempty"`
	TokenURL        string            `yaml:"token_url,omitempty"`
	RedirectURL     string            `yaml:"redirect_url,omitempty"`
	Extra           map[string]string `yaml:"extra,omitempty"`
}

ToolOAuthProviderConfig declares one operator-configured OAuth provider (D-095, closes issue #116 and D-090's deferred construction gap). Each entry resolves to a self-registered driver in `internal/tools/auth/drivers/<name>/` via the §4.4 seam pattern. The constructed `auth.OAuthProvider` is keyed by `Name` in the catalog builder's `Deps.OAuthProviders` map; `tools.entries[].oauth.provider` references the same `Name`.

The V1 default driver is `oauth2` — generic OAuth2/PKCE Authorization Code flow. Future flow types (device-code, client-credentials, per-vendor extensions) add a new driver under `internal/tools/auth/drivers/<name>/` without changing this shape.

Credentials enter via env-var indirection (§7 rule 2 — never hardcoded, never logged). `ClientIDEnv` / `ClientSecretEnv` name the env vars; the driver resolves `os.Getenv` at construction and fails closed when either is empty.

Layout in YAML:

tools:
  oauth_token_kek_env: HARBOR_OAUTH_TOKEN_KEK
  oauth_providers:
    - name: github
      driver: oauth2
      client_id_env: GITHUB_OAUTH_CLIENT_ID
      client_secret_env: GITHUB_OAUTH_CLIENT_SECRET
      scopes: ["repo", "read:user"]
      auth_url: https://github.com/login/oauth/authorize
      token_url: https://github.com/login/oauth/access_token
      redirect_url: https://example.com/oauth/callback

Fields:

  • `Name` — operator-facing identifier (must be unique across the slice; referenced by `tools.entries[].oauth.provider`).
  • `Driver` — names a self-registered driver under `internal/tools/auth/drivers/<name>/`. Required. Unknown driver names fail validation with the registered-driver list in the error message.
  • `ClientIDEnv` / `ClientSecretEnv` — env-var names the driver reads at construction. Both required.
  • `Scopes` — requested OAuth scopes. Optional.
  • `AuthURL` / `TokenURL` — authorization-server endpoints. Used by the generic `oauth2` driver; driver-specific drivers may ignore.
  • `RedirectURL` — the redirect URI the operator hosts (the Harbor Protocol callback handler). Required for the `oauth2` driver.
  • `Extra` — driver-specific extras map. Reserved for future drivers' per-flow knobs (e.g. device-code's verification URI, vendor-specific tenant ID). Unused by the V1 `oauth2` driver.

Restart-required.

type ToolPolicyConfig added in v1.2.0

type ToolPolicyConfig struct {
	MaxAttempts   int      `yaml:"max_attempts,omitempty"`
	TimeoutMS     int      `yaml:"timeout_ms,omitempty"`
	RetryOn       []string `yaml:"retry_on,omitempty"`
	BackoffBaseMS int      `yaml:"backoff_base_ms,omitempty"`
	BackoffMult   float64  `yaml:"backoff_mult,omitempty"`
	BackoffMaxMS  int      `yaml:"backoff_max_ms,omitempty"`
}

ToolPolicyConfig is the operator-facing YAML projection of `tools.ToolPolicy` (Phase 26b). It is the single config→policy translation surface (CLAUDE.md §13): the projection helper in `policy_projection.go` is the only code that maps these fields onto a `tools.ToolPolicy`. There is no second policy definition.

Field semantics (all optional; a zero/omitted field falls through to `tools.DefaultPolicy()` per-field via `tools.ToolPolicy`'s own zero-value resolution):

  • MaxAttempts is the TOTAL number of attempts including the first (operators think in total attempts, not retries). It projects to `tools.ToolPolicy.MaxRetries = MaxAttempts - 1`. `1` means a single attempt with no retry; `0`/omitted falls through to the default 4 total attempts (3 retries).
  • TimeoutMS is the per-attempt deadline in milliseconds. `0`/ omitted falls through to the default 30000.
  • RetryOn is the retryable error-class allowlist. Each value must be a known `tools.ErrorClass` string (`transient` / `timeout` / `5xx` / `permanent`); validation rejects unknown classes. Empty/ omitted falls through to the default `[transient timeout 5xx]`.
  • BackoffBaseMS / BackoffMaxMS are the backoff base / cap in milliseconds; `0`/omitted fall through to the defaults (100 ms / 30 s). BackoffMult is the per-retry multiplier; `0`/omitted falls through to the default 2.

func (ToolPolicyConfig) ToToolPolicy added in v1.2.0

func (c ToolPolicyConfig) ToToolPolicy() (ProjectedToolPolicy, error)

ToToolPolicy projects the operator-facing `ToolPolicyConfig` onto the cycle-free `ProjectedToolPolicy` image. It is the single config→policy translation seam (CLAUDE.md §13).

Mapping:

  • `max_attempts` (TOTAL attempts incl. the first) → `MaxRetries = max_attempts - 1` when `max_attempts >= 1`; an omitted/zero `max_attempts` leaves `MaxRetries` zero so the runtime inherits the default total attempt count (per-field fall-through).
  • `timeout_ms` → `TimeoutMS` directly.
  • `backoff_base_ms` / `backoff_max_ms` → `time.Duration`.
  • `backoff_mult` → `BackoffMult` directly.
  • `retry_on` strings are validated against the error-class allowlist; an unknown value is a hard error (fail loud — no silent drop, CLAUDE.md §5).

A negative `max_attempts` or `timeout_ms` is rejected here as a belt-and-braces guard; config validation rejects them earlier with a field-scoped error.

type ToolsConfig

type ToolsConfig struct {
	HTTPManifests    []string                  `yaml:"http_manifests,omitempty"`
	MCPServers       []MCPServerConfig         `yaml:"mcp_servers,omitempty"`
	A2APeers         []A2APeerConfig           `yaml:"a2a_peers,omitempty"`
	Entries          []ToolEntryConfig         `yaml:"entries,omitempty"`
	OAuthProviders   []ToolOAuthProviderConfig `yaml:"oauth_providers,omitempty"`
	OAuthTokenKEKEnv string                    `yaml:"oauth_token_kek_env,omitempty"`

	// BuiltIn lists opt-in tools shipped in the Harbor binary that the
	// runtime should register against the catalog at boot. V1.1 ships
	// two names: `clock.now` (current UTC time) and `text.echo` (echo
	// input verbatim). The names are mirrored from
	// `internal/tools/builtin.KnownNames()` via `KnownBuiltInTools()`
	// below; a typo fails validation pre-boot rather than at runtime.
	// Empty list = no built-ins registered. Restart-required (no
	// `reload:"live"` tag).
	BuiltIn []string `yaml:"built_in,omitempty"`

	// Custom lists operator-declared custom tools whose Go shell is
	// generated by `harbor scaffold` (Phase 83o / D-154). Each entry
	// names the tool, gives a one-line description, and declares its
	// input + output shape as a flat map of `field: type` (V1.1
	// supports `string`, `integer`, `number`, `boolean`, `[]string`).
	// The scaffolder materialises one `tools/<name>.go` stub + matching
	// `tools/<name>_test.go` per entry; the rendered Go contains the
	// typed Input/Output structs and a `Handle` stub the operator fills
	// in. The runtime does NOT auto-discover these tools — the operator
	// imports the generated `tools/` package + calls `RegisterTools`
	// from the agent's bootstrap. Restart-required (no `reload:"live"`).
	Custom []CustomToolConfig `yaml:"custom,omitempty"`

	// GrantedScopes is the operator-declared list of authorization
	// scopes the dev binary's planner-facing catalog view treats as
	// granted. The runtime catalog projects only tools whose
	// `AuthScopes` are entirely contained in this set; tools that
	// require a missing scope are invisible to the planner.
	//
	// Phase 83m (Item 6, D-156): the field closes the runloop's
	// `nil /* TODO Phase 83m */` hard-code that left the catalog
	// unfiltered. Empty list = no scopes granted (the existing latent
	// default — tools that declare AuthScopes are invisible). The
	// scopes are operator-defined per their tool sources; the
	// validator only asserts that each entry is a non-empty string,
	// with no allowlist.
	//
	// Restart-required (no `reload:"live"` tag — scope changes
	// trigger a re-evaluation of every catalog descriptor).
	GrantedScopes []string `yaml:"granted_scopes,omitempty"`

	// SearchCacheDSN is the SQLite DSN backing the Phase 107c tool
	// SearchCache (FTS5 + regex fallback). Empty means the dev binary
	// uses an in-memory cache (the V1 default — discovery state lives
	// for the process lifetime). Operators can point at an on-disk
	// path (`file:./harbor-tools.db?_pragma=...`) to persist the
	// cache across reboots. Restart-required.
	SearchCacheDSN string `yaml:"search_cache_dsn,omitempty"`
}

ToolsConfig is owned by the tools subsystem phases (Phase 26+). The block is optional — operators who don't attach external tool sources omit it entirely.

`HTTPManifests` lists paths to UTCP-style YAML manifests for Phase 27's HTTP driver. The boot-path loader is NOT yet wired — no production path calls `LoadManifest` / `RegisterManifest` — so `Validate` REJECTS a non-empty list rather than letting a populated knob silently do nothing (§13; SDK friction audit, docs/notes/sdk-friction-audit.md §1). An empty list is valid and is what the shipped examples carry. HTTP tools are registered programmatically via the Phase 27 driver until the loader lands.

`MCPServers` lists Phase 28's MCP southbound attachments. Each entry boots a `*mcp.Provider` whose discovered tools / resources / prompts are merged into the runtime catalog.

`A2APeers` lists Phase 29's A2A peers. The wire driver (`internal/distributed/drivers/a2a`) reads this slice at construction.

`Entries` lists per-tool catalog wiring declarations: operators attach approval policies and / or OAuth bindings to a tool name without writing Go wiring code. Phase 64a (D-090) — the catalog builder reads this list at boot and auto-wraps each named tool's descriptor with the declared middleware. An entry whose Name does not resolve to a registered tool fails the catalog build loud (§13 "fail loudly at boot"); an entry that names an unknown approval policy or OAuth provider also fails loud.

Restart-required (no `reload:"live"` tag): adding / removing tool providers at runtime is a Phase 91+ Protocol surface concern.

`OAuthProviders` closes D-090's deferred "OAuth provider construction" gap (issue #116 / D-095). Each entry declares one named OAuth provider resolved at boot through the `internal/tools/auth` driver registry (§4.4 seam). The V1 default driver is `oauth2` (generic OAuth2/PKCE Authorization Code flow). When any `tools.entries[].oauth.provider` references a name, that name MUST appear in `OAuthProviders` — validateTools enforces.

`OAuthTokenKEKEnv` names the env var holding the 32-byte hex-encoded key-encryption key (KEK) the OAuth token store consumes for AES-256-GCM encryption at rest (§7 + Phase 30). Required whenever `OAuthProviders` is non-empty; the dev-stack reads the env at boot and fails closed when the env value is empty or wrong-length.

Jump to

Keyboard shortcuts

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