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
- Variables
- func IsValidationError(err error) bool
- func KnownBuiltInTools() []string
- func KnownCustomToolTypes() []string
- type A2APeerConfig
- type ArtifactsConfig
- type AuditConfig
- type CLIConfig
- type Config
- type CustomToolConfig
- type DevHotReloadConfig
- type DistributedConfig
- type EventsConfig
- type GovernanceConfig
- type GovernanceRateLimitConfig
- type GovernanceTierConfig
- type IdentityConfig
- type LLMConfig
- type LLMCorrectionsConfig
- type LLMCorrectionsProfileConfig
- type LLMCostOverridesConfig
- type LLMCustomProviderConfig
- type LLMModelProfileConfig
- type LLMNetworkDefaults
- type LoadOption
- type MCPServerConfig
- type MemoryConfig
- type PauseResumeConfig
- type PlannerConfig
- type PlannerPlanningHintsCfg
- type ProjectedToolPolicy
- type ProtocolConfig
- type RuntimeConfig
- type ServerConfig
- type SessionsConfig
- type SkillsConfig
- type SkillsDirectoryConfig
- type StateConfig
- type TasksConfig
- type TelemetryConfig
- type ToolApprovalConfig
- type ToolEntryConfig
- type ToolOAuthConfig
- type ToolOAuthProviderConfig
- type ToolPolicyConfig
- type ToolsConfig
Constants ¶
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.
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.
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.
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.
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 ¶
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 ¶
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 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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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
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.