Harbor configuration reference
This document is the full operator-facing knob reference for Harbor's harbor.yaml. Every leaf field on internal/config.Config has a heading here; internal/config/doc_drift_test.go fails the build when a field lands without documentation.
Conventions used throughout:
- Default — the value applied when the key is absent (sourced from
internal/config/loader.go::Defaults()). - Validation — what the validator (
internal/config/validate.go) rejects. - Restart-required — restart-required unless explicitly tagged
reload:"live"on the struct field (CLAUDE.md §10).
For the tiered yaml template used by harbor init, see cmd/harbor/init/templates/default/harbor.yaml.tmpl.
Runtime-wide PostgreSQL budget (v1.29.1)
These restart-required fields are one aggregate budget for every compatible PostgreSQL projection in one Harbor runtime: state, memory, artifacts, skills, sessions/turns, and observability/rollups. They are not six per-store allowances. Equal canonical DSNs share one runtime-owned *sql.DB; distinct DSNs remain supported and charge every acquisition to the same runtime-wide budget.
postgres:
pool:
max_open: 3
max_idle: 1
conn_max_lifetime: 5m
conn_max_idle_time: 30spostgres.pool.max_open must be positive and is the aggregate open-permit ceiling. postgres.pool.max_idle must be non-negative and is the aggregate idle target. conn_max_lifetime and conn_max_idle_time are finite; the production default idle lifetime is 30 seconds. The six direct migration sessions in the budget below are an operator/orchestrator rollout ceiling, not a Harbor runtime configuration key.
The Basic-4GB connection proof is nine runtimes × two generations × three open permits = 54, plus six direct migration sessions, 12 reserved Pengui/capabilities connections, and a 25-connection operator reserve = 97 of the live max_connections=103, leaving six below the hard cap. Steady state is 70. No plan upgrade is required by this posture.
Migration apply must use a direct/session-affine PostgreSQL endpoint, normally port 5432, because the runner takes a session advisory lock. Migration verify is read-only and may use transaction-pooled PgBouncer 6432 after direct apply. An unproven transaction-pooled apply endpoint fails closed.
For the staged split-to-unified procedure and machine-readable row/hash manifests, see Consolidate split PostgreSQL projections.
Offline legacy durable-head repair
Legacy durable-head repair is deliberately not a runtime configuration field. It is an offline administrative command because a corrupt head can prevent a normal runtime from booting. Select the StateStore driver and DSN directly; the command does not assemble the Runtime:
harbor events repair-legacy-heads \
--driver postgres \
--dsn "$HARBOR_STATE_DSN" \
--mode inspect \
--json > /var/lib/harbor/legacy-heads.inspect.jsoninspect is the default and performs no writes. Apply requires the event writer to be stopped and an explicit freeze/drain acknowledgement:
harbor events repair-legacy-heads \
--driver postgres \
--dsn "$HARBOR_STATE_DIRECT_DSN" \
--mode apply \
--freeze-ack \
--json > /var/lib/harbor/legacy-heads.apply.jsonThe command reports and stores only content-free evidence: counts, positions, stable identifiers or hashes, generations, immutable entry hashes, tool version, and outcome. It never logs or writes payload bytes or identity values. Use --max-heads and --max-duplicates to set explicit operator bounds; the StateStore scan reads at most one item beyond each bound before refusing an oversized inventory. It refuses every detectable PgBouncer/transaction-pooled or 6432 mutation DSN before opening a write handle; PostgreSQL apply requires a direct, session-affine 5432 endpoint. See Repair legacy durable heads for the stop-before-repair, backup/rollback, inspect/apply/verify procedure.
Server
server.bind_addr
Listen address (host:port) for Harbor's network surface (Protocol + health endpoint). Default: 127.0.0.1:8080. Validation: must parse via net.SplitHostPort.
server.shutdown_grace_period
Max time harbor dev waits for in-flight requests to drain on SIGTERM before forcing close. Default: 30s. Validation: > 0.
server.allowed_origins
CORS allowlist for the D-091 multi-process Console+Runtime posture (Phase 83v / D-162). Each entry is an exact origin (scheme://host[:port], no path / query / fragment) the Runtime accepts cross-origin requests from. Empty list (the default) = no CORS headers = same-origin only.
On a matching origin the middleware echoes the request's Origin header verbatim into Access-Control-Allow-Origin and sets Access-Control-Allow-Credentials: true so the browser sends the Authorization bearer on subsequent requests. The middleware NEVER emits Access-Control-Allow-Origin: * in production — * is incompatible with credentialed requests and the browser refuses the combination. CLAUDE.md §7: declare exact origins in production.
The validator rejects * (and any wildcard shape) unless server.cors_dev_allow_any: true is also set. Validation: each entry must parse as a URL with http or https scheme and a non-empty host; paths / queries / fragments are rejected.
Example:
server:
allowed_origins:
- https://console.example.com
- https://console.example.com:8443
- http://127.0.0.1:18790server.cors_dev_allow_any
Explicit, dev-only escape hatch that opens the CORS surface to ANY origin (Phase 83v / D-162). NEVER set in production: a harbor dev boot with this flag set prints a stderr banner so the posture is visibly dev-only. Provided for first-clone Console iteration against a harbor dev loop where the Console origin (Vite, :5173) varies during development.
Default: false. Set explicitly:
server:
cors_dev_allow_any: trueThe middleware still emits the per-origin Access-Control-Allow- Origin echo (never *) so credentialed responses keep working.
server.debug_addr
Optional address for the pprof debug HTTP listener. Empty (the default) disables it entirely. When set it MUST be a loopback host:port (127.0.0.0/8 or ::1) — the validator rejects any other host so the profiler is never exposed off-box. The listener runs on its OWN http.Server with a private mux serving only the net/http/pprof handlers; it is never mounted on the Protocol mux. Enabling it prints a stderr banner so the dev-only posture is visible. It is a diagnostic escape hatch, not a production surface.
Default: "" (disabled). Set explicitly:
server:
debug_addr: "127.0.0.1:6060"Then go tool pprof http://127.0.0.1:6060/debug/pprof/heap (or /goroutine, /profile) against the running runtime.
Identity
identity.jwt_algorithms
Asymmetric-only JWT-algorithm allowlist (CLAUDE.md §7 rule 1). Default: none — operator MUST set at least one. Validation: each entry must be one of RS256 / RS384 / RS512 / ES256 / ES384 / ES512. HS* and none are forbidden.
identity.issuer
OIDC issuer URL. Default: none. Validation: non-empty.
identity.audience
OIDC audience claim. Default: none. Validation: non-empty.
identity.jwks_url
URL the JWT verifier fetches the JWKS document from. Default: empty. Validation: one of jwks_url or jwks_file MUST be set.
identity.jwks_file
Filesystem path to a static JWKS document (offline / air-gapped scenarios). Default: empty. Validation: see jwks_url.
identity.jwks_max_stale
Max-stale ceiling: the longest a cached JWKS key snapshot is honored without a successful refresh before the validator fails closed (rejects tokens with a distinct jwks_stale reason) rather than serving a possibly-revoked key during a prolonged IdP outage. Default: 0, which applies the safe built-in ceiling (1h). Validation: a negative value is rejected; a positive value below the 1m floor is rejected (a ceiling below the minimum refresh window can never be satisfied). There is no "disable" path — Harbor's posture is fail-closed. This bounds — it does not make instantaneous — key revocation; pair a tight ceiling with overlapping IdP signing keys and short token TTLs.
Telemetry
telemetry.log_format
Slog handler format. Default: json. Validation: json or text.
telemetry.log_level
Slog level threshold. Default: info. Validation: debug / info / warn / error.
telemetry.otel_endpoint
OTLP/gRPC endpoint URL for span + metric export. Default: empty (noop exporter — spans / metrics collected but not shipped).
telemetry.service_name
Service identifier on emitted spans + metrics. Default: harbor. Validation: non-empty.
Observability
observability.rollups.driver
Driver for the durable observability rollup projection. Default: empty — the observability.rollups.* block is left unwired (the rollup query/projector is not built). Validation: when set, inmem / sqlite / postgres. Restart-required.
observability.rollups.dsn
Driver connection string for the rollup projection. Default: empty. Validation: required when driver is sqlite or postgres; unused for inmem. SQLite: file path. Postgres: libpq URL. Secret: redacted in audit logs. Restart-required.
observability.rollups.migration_mode
Postgres migration posture at store open. Default: apply when empty or omitted. Validation: apply or verify, and only valid when driver is postgres. apply takes Harbor's session advisory lock and applies missing migrations, so it requires a direct/session-capable Postgres endpoint. verify performs read-only checks of the subsystem's namespaced harbor_schema_migrations rows, harbor_store_identity, and required physical schema, takes no advisory lock, begins no transaction, executes no DDL/write, and fails loud if identity, checksums, schema, or an embedded migration is missing or malformed. It is safe for transaction-pooled connections after a separate apply step has completed. Restart-required.
PostgreSQL runtime budget
postgres.pool.max_open
Aggregate maximum open backend connections for one Harbor runtime across all six PostgreSQL stores. Equal DSNs reuse one pool; distinct DSNs share this same budget. Default: 3 (the Basic-4GB rollout budget).
The supported steady-state topology is one logical PostgreSQL database per Harbor runtime with state, memory, artifacts, skills, sessions.turns, and observability.rollups pointed at that database. A distinct DSN per subsystem remains supported for the first hotfix rollout, but all such pools still consume this same aggregate budget; consolidate one runtime at a time.
postgres.pool.max_idle
Aggregate idle allowance distributed across the runtime's physical pools. Default: 1.
postgres.pool.conn_max_lifetime
Maximum connection generation lifetime. Default: 5m; zero selects the default. Restart-required.
postgres.pool.conn_max_idle_time
Maximum idle connection lifetime. Default: 30s; zero selects the default, never an unbounded idle lifetime. Restart-required.
The Basic-4GB acceptance math is 18 overlapping runtimes × 3 open = 54, plus 6 direct migration sessions reserved by the operator/orchestrator, 12 Pengui/capabilities connections, and a 25-connection operator reserve = 97, below the observed max_connections=103. The six migration sessions are rollout accounting, not a Harbor runtime configuration key.
State
state.driver
StateStore driver. Default: inmem. Validation: inmem / sqlite / postgres.
state.dsn
Driver connection string. Default: empty. Validation: required when driver != "inmem". SQLite: file path. Postgres: libpq URL. Secret: redacted in audit logs.
state.migration_mode
Postgres migration posture at store open. Default: apply when empty or omitted. Validation: apply or verify, and only valid when driver is postgres. apply takes Harbor's session advisory lock and applies missing migrations, so it requires a direct/session-capable Postgres endpoint. verify performs read-only checks of the subsystem's namespaced harbor_schema_migrations rows, harbor_store_identity, and required physical schema, takes no advisory lock, begins no transaction, executes no DDL/write, and fails loud if identity, checksums, schema, or an embedded migration is missing or malformed. It is safe for transaction-pooled connections after a separate apply step has completed. Restart-required.
Separate configuration persistence
configuration_state.driver
Optional restart-required store for agent registrations, versioned agent/user configuration, lifecycle/retirement records, configuration operation journals and session-specific settings. Omitted: reuse state exactly as before. When present, driver must be inmem, sqlite or postgres; a persistent driver requires dsn. DSNs are secrets. PostgreSQL migration_mode follows the same apply/verify contract as state.migration_mode and uses the existing StateStore schema and runtime-managed pool.
This does not redirect messages, cumulative summaries, tool evidence, runs, tasks, event history or artifacts. For ephemeral conversations, keep state, events, artifacts and task storage ephemeral; configuring this store alone does not turn off any independently enabled durable capability. Explicitly saved user instructions and personal configuration are settings, not chat history, and do persist here. Session deletion fences and removes its settings from both stores without deleting agent-wide settings.
state:
driver: inmem
configuration_state:
driver: postgres
# Supply HARBOR_CONFIGURATION_STATE_DSN through your secret environment.
migration_mode: verify # Only after a direct-endpoint apply has succeeded.All three fields accept HARBOR_CONFIGURATION_STATE_* environment overrides. Changing this store does not copy old configuration or recover already lost in-memory revisions. Preserve/export existing settings before switching stores and explicitly restore them through the authenticated configuration API.
configuration_state.dsn
Connection string for the separate configuration store. Required for sqlite and postgres; omitted with inmem. Secret and redacted like state.dsn. Restart-required. Environment: HARBOR_CONFIGURATION_STATE_DSN.
configuration_state.migration_mode
PostgreSQL only: apply (default) or verify, with exactly the same endpoint, schema and migration guarantees as state.migration_mode. Restart-required. Environment: HARBOR_CONFIGURATION_STATE_MIGRATION_MODE.
LLM
llm.driver
LLM driver. Default: bifrost (Phase 64 / D-089). Validation: bifrost (production) or mock (tests only). Empty resolves to bifrost.
llm.provider
Provider name routed through bifrost. Default: empty. Validation: required when driver != "mock". May reference a custom_providers entry (NIM / vLLM / ollama / any OpenAI-compatible endpoint) instead of a native bifrost provider.
llm.model
Canonical model identifier. Default: empty. Validation: required when driver != "mock". Must have a matching model_profiles[name] entry for the safety-net token-budget guard.
llm.api_key
Provider API key, typically given as env.NAME so the driver reads os.Getenv("NAME") at boot. Default: empty. Validation: required when driver != "mock" AND the provider is not a custom-provider name. Secret: redacted in audit logs.
llm.base_url
Override base URL for the provider's API. Default: empty (provider's hardcoded default).
llm.timeout
Per-request timeout. Default: 60s. Validation: > 0 (unless provider is a custom-provider name).
llm.context_window_reserve
Safety margin (fraction) the token-budget guard reserves above the model's hard cap. Default: 0.05 (5%). Validation: [0.0, 1.0).
llm.model_profiles
Per-model knobs (context-window cap, JSON-schema mode, default-max- tokens, reasoning-effort, cost overrides, max-retries, correction- layer overrides). Each entry's context_window_tokens MUST be > 0. See the LLMModelProfileConfig godoc for the full surface.
image_input is optional: true declares image input support; false withholds attached image content from that model and gives it a clear notice; omission means support is unknown and preserves the existing image route. An external provider-route resolver can supply the same request-local fact as model_profile.image_input only when Harbor advertises image_input_supported: true in its select/resolve request. Deploy a trusted resolver that honors this negotiation before upgrading a runtime, so older runtimes never receive the additive field.
llm.corrections.enabled
Top-level toggle for the Phase 34 per-provider correction layer. Default: true. Set false only for safety-pass isolation tests.
llm.custom_providers
Operator-declared OpenAI-compatible providers (Phase 33a — NIM / vLLM / ollama / lm-studio / in-house gateways). Each entry needs name / base_url / api_key_env_var / models. See the LLMCustomProviderConfig godoc for the full surface.
llm.provider_route.resolver_url
Boot-pinned URL for the optional external provider-route resolver. HTTPS is required for remote endpoints; loopback HTTP is allowed for local development and tests. The resolver is called only for an explicitly routed request; an empty provider_route block does no environment lookup, network work, or goroutine setup. Userinfo, queries, and fragments are rejected. Restart-required.
llm.provider_route.auth_token_env
Name of the environment variable containing the resolver's authentication token. Required when provider_route is configured. The value is read once at boot, never serialized, logged, or returned in an error, and is sent only to the boot-pinned resolver endpoint. Restart-required.
The runtime is identified by the resolver authentication credential. Harbor sends no runtime ID; route calls use the runtime identity from the verified coordinator-minted token of the run. Removed provider_route.runtime_id and HARBOR_LLM_PROVIDER_ROUTE_RUNTIME_ID settings are ignored with a deprecation warning. Deploy the resolver that ignores the legacy request field first, then upgrade Harbor, then remove obsolete fleet settings.
llm.provider_route.timeout
Maximum duration for each provider-route resolver exchange. Default: 10s when zero. Validation: non-negative. Runtime-default requests never consult the resolver. Restart-required.
llm.credential_source
Where the PRIMARY provider's API key is sourced: "" / "local" (default — resolved from api_key once at boot) or "remote" (broker-pull from the named inference_broker at connect + refresh). Brokered XOR local — a remote source requires a resolvable inference_broker and an empty api_key (both set, or neither, is a boot error). Restart-required; NOT a Protocol surface.
llm.inference_broker
Names the inference_brokers[] entry the primary provider's key is pulled from when credential_source is "remote". Referenced by non-secret NAME (the pull endpoint / audience / scope ceiling live on the named broker). Required when credential_source: remote, rejected otherwise. Restart-required.
llm.inference_brokers
Boot-declared list of NAMED inference-plane credential brokers — the pinned credential SINK for a runtime's LLM provider key. Each entry needs name / credential_url (https or loopback) / auth_token_env, with optional audience / scope_ceiling / cache_ttl / timeout. No URL or secret ever crosses the wire (the credential-plane invariant); referenced by name from llm.credential_source: remote and from the agent_config.set_llm_provider Protocol write. See the InferenceBrokerConfig godoc. Config/file-only, restart-required.
llm.external_grant.mode
Runtime-side external inference-grant posture. Default: disabled. Validation: disabled, optional, or required. optional verifies a supplied signed grant while preserving legacy calls; required rejects calls without one. Restart-required.
llm.external_grant.route_mode
Optional route shape accepted by the runtime-side grant verifier: coordinator_bound requires a coordinator-selected provider route and credential binding, while runtime_default uses the runtime's configured provider/model and carries only the signed limits and lease. Empty accepts either explicit shape. In required mode, explicit runtime_default requires the configured native/custom provider key at boot; explicit coordinator_bound does not load it. Legacy empty mode may boot without a local key for coordinator-bound compatibility and uses a configured key when one is available. Restart-required.
llm.external_grant.audience
The exact audience claim accepted by the runtime-side grant verifier. Required when llm.external_grant.mode is optional or required. Non-secret.
llm.external_grant.runtime_id
The runtime identity bound into accepted grants. Required for an enabled external-grant posture. It is a boot fence, not a caller-supplied authority.
llm.external_grant.authorized_organizations
Optional explicit organization allowlist for accepted signed grants. When it is empty, the configured coordinator public-key issuer is the authority and the runtime accepts grants for every organization that issuer signs. When present, each signed grant organization must be listed. This is a set, not a single boot-pinned organization, so one runtime can serve multiple organizations concurrently without trusting an organization value from the dispatch caller.
llm.external_grant.public_keys
Named Ed25519 public keys used to verify signed grants. Values are standard or URL-safe base64-encoded public keys; private signing material never belongs in the runtime configuration. Credential resolution remains a boot-host seam; receipt delivery may use that seam or the optional stock coordinator transport below. Restart-required.
llm.external_grant.coordinator.receipt_url
Optional stock harbor serve endpoint that accepts canonical, content-free usage-receipt batches and returns exact receipt-ID/body-hash acknowledgements. Setting it opts the runtime into coordinator delivery; leaving it empty starts no coordinator client, timer, scan, or network work. HTTPS is required except for loopback HTTP. User info, queries, and fragments are rejected. Restart-required.
llm.external_grant.coordinator.credential_url
Optional stock harbor serve endpoint for resolving the opaque credential binding of an already-verified coordinator_bound grant. The authenticated request carries only that exact signed grant; it has no separate organization, runtime, identity, run, provider, connection, route, handle, or generation override. Harbor accepts a response only when its provider, opaque handle, connection generation, and credential-asset generation exactly match the grant. HTTPS is required except for loopback HTTP. User info, queries, and fragments are rejected. A small in-memory cache is keyed by the digest of the full verified content-free binding, capped at 256 entries and 30 seconds, never past the response or grant expiry; it is cleared when the runtime closes. runtime_default never calls this endpoint. Restart-required.
llm.external_grant.coordinator.auth_token_env
Name of the environment variable containing the runtime's coordinator service credential. Required when receipt_url or credential_url is set. The value is read once at boot, never serialized, and never included in logs or errors. Restart-required.
llm.external_grant.coordinator.timeout
Bound for each receipt HTTP exchange. Default: 10s when zero. Validation: non-negative. Restart-required.
llm.external_grant.coordinator.max_batch
Maximum canonical receipts in one delivery request. Default: 64 when zero. Validation: 1 through 1000 when explicitly set. The durable outbox retains unacknowledged receipts for bounded replay. Startup legacy recovery requests one extra row and fails boot rather than silently skipping later records when retained legacy receipts exceed this value. Increase the approved bound or perform offline recovery before restarting. Restart-required.
llm.external_grant.coordinator.reconcile_interval
Slow crash-recovery reconciliation cadence for durable receipts that predate the due index or lost its wake-up. Default: 5m when zero. Normal receipt delivery is enqueue-driven and does not poll this interval. Validation: non-negative. Retry deadlines do not accelerate this maintenance scan. Restart-required.
llm.external_grant.coordinator.top_up_url
Optional stock harbor serve endpoint for authenticated bounded lease successors. It is called only after an otherwise valid grant needs more capacity or its signed deadline elapsed; idle runtimes make no top-up calls. Requests carry the exact canonical predecessor, requested units, renewal reason, and a stable idempotency key covering all three. Responses must return the same key and one exact canonical successor, which Harbor verifies and durably applies before provider work. Harbor retains that signed successor content-free and reuses it across later calls and restarts; it does not contact the coordinator while current durable capacity remains sufficient. HTTPS is required except for loopback HTTP. User info, queries, and fragments are rejected. Restart-required.
llm.network_defaults.timeout
Default per-provider timeout. Default: 0 → bifrost's package default. Restart-required.
llm.network_defaults.max_retries
Default per-provider retry count. Default: 0 → bifrost default.
llm.network_defaults.retry_backoff_initial
Default initial backoff before retry. Default: 0.
llm.network_defaults.retry_backoff_max
Default cap on backoff growth. Default: 0.
llm.network_defaults.concurrency
Default in-flight request limit per provider. Default: 0.
llm.network_defaults.buffer_size
Default request-queue buffer per provider. Default: 0.
Embeddings
The embedding-client block (Phase 84d — D-191): the model/provider pair Harbor turns text into vectors with, configured separately from the chat llm block. Fully optional — but REQUIRED the moment an embedding-consuming mode is enabled (skills.retrieval: semantic); the validator names the missing key so the boot failure is actionable, and a semantic mode never silently degrades to non-semantic retrieval.
embeddings.driver
Embeddings driver. Default: empty (resolves to bifrost). Validation: when set, bifrost (V1; there is deliberately no mock / stub embeddings driver).
embeddings.provider
Embedding provider (e.g. openai). Validation: required when the block is set.
embeddings.model
Embedding model (e.g. text-embedding-3-small). Its own operator choice — nothing falls back to llm.model. Validation: required when the block is set.
embeddings.api_key
Literal key or env.NAME reference, matching the llm.api_key convention. Validation: required when the block is set. Secret: redacted.
embeddings.base_url
Optional endpoint override forwarded to the gateway. Default: empty.
embeddings.timeout
Optional per-request timeout. Default: 0 (gateway default). Validation: >= 0.
embeddings.dimensions
Optional reduced output dimension for providers that support it. Default: 0 (the model's native dimension). Validation: >= 0.
Governance
governance.repair_attempts
Per-LLM-call schema-repair budget. Default: 3. Validation: >= 0.
governance.default_tier
Tier applied to an identity not matched by a custom resolver. Default: empty (no default → no enforcement for unmatched identities). Validation: when set, MUST reference a key in identity_tiers.
governance.identity_tiers
Per-tier policy bundle (cost ceiling, rate-limit token bucket, max-tokens cap). Default: empty (latent — no enforcement, D-044). Populated tiers are enforced (Phase 111a, D-198): the runtime composes the enforcement subsystem at boot, so the cost ceiling fails calls with ErrBudgetExceeded, the token bucket with ErrRateLimited, and the per-call cap with ErrMaxTokensExceeded (each with a matching governance.* event). The same tiers also feed the read-only governance.posture surface. See GovernanceTierConfig godoc for the full surface.
Distributed
distributed.bus_driver
MessageBus driver. Default: loopback. Validation: loopback / durable.
loopback(default) — in-process; projects eachBusEnvelopeonto the local event bus. No durability, no cross-instance fan-out.durable— persists everyBusEnvelopethrough theStateStoreand projects it onto the local event bus, with a background poller that also projects envelopes published by OTHER instances sharing the store (or left by a crash) — at-least-once cross-instance fan-out + restart-replay. StateStore-backed (Postgres-as-queue on a shared Postgres store; single-instance restart-replay on SQLite). Reuses the runtimeStateStore; fails loud at construction when no store is wired. Consumers dedupe on(TaskID, Edge, EventID). NATS / Redis Streams remain future drivers.
distributed.bus_poll_interval
How often the durable bus driver scans the shared StateStore for envelopes published by other instances (or left by a crash). Optional; the durable driver applies a built-in default (1s) when unset. Ignored by loopback.
distributed.remote_driver
RemoteTransport driver. Default: loopback. Validation: loopback or a2a.
Runtime
Runtime run-lifecycle configuration. Two populated bodies: the run-completion hook and the runtime.naming session auto-naming fleet default.
runtime.hooks.run_completion.tool
The catalog tool the run's transcript is dispatched to at the run loop's terminal boundary (the run-completion hook). Empty (the default) disables the static hook — the per-agent, versioned agent-config hooks section can still enable it. The tool need not be exposed to the planner: the executor resolves it against the full catalog. Resolution at run start is agent-config over this yaml over no hook — a PRESENT agent-config hooks section is authoritative either way, so a section with no/empty run-completion tool (a bare hooks: {}) is an explicit per-agent NO-HOOK that overrides this yaml fleet hook (section presence is the signal; it is never dropped as inert). A hook failure never alters the run outcome (it emits run.hook_failed + a Warn log).
runtime.hooks.run_completion.timeout
Bounds the detached hook dispatch (Go duration, e.g. 10s). The dispatch runs under a context detached from the run's cancellation but bounded by this timeout, so a wedged sink cannot leak the hook goroutine. Non-positive (the default) falls back to the 10s runtime default. Setting a timeout without runtime.hooks.run_completion.tool is rejected (a timeout for a hook that will never fire). A negative value is rejected.
runtime.naming.auto
Enables session auto-naming fleet-wide (the runtime.naming block). Opt-in, default false (off): with the block absent or auto: false the runtime writes no naming counters, makes no naming LLM calls, and emits no naming events. A per-agent, versioned agent-config naming section overrides this default at run start (agent-config over this yaml over off) — a PRESENT section is authoritative either way, so a bare {auto: false} revision is an explicit per-agent opt-out that wins over a yaml-on fleet default (section presence is the signal; it is never dropped as inert). When on, the runtime titles a session itself at each run's terminal boundary via ONE governed Complete call over a bounded transcript digest; a naming failure never alters the run outcome (it emits session.naming_failed + a Warn log). The request keeps a fixed 64-token visible-output ceiling. Its naming-only reasoning_mode defaults to off, so a model profile's reasoning default cannot consume that allowance. Providers that reject reasoning controls can use provider_default, which omits the provider control while still preventing inheritance from the selected model profile.
The naming call is bounded by a FIXED runtime timeout (10s) — unlike the run-completion hook, whose timeout is per-section configurable, the naming timeout is not an operator knob. The trigger runs synchronously at the run's terminal boundary, AFTER the completion hook, so the worst-case post-run latency is hook timeout + naming timeout, serialized. A naming FAILURE does not consume the max_repetitions cap: as long as a title is due, the runtime retries on every subsequent completed run until one succeeds — so on a naming-on fleet whose naming LLM is DOWN, every completed run pays one failing (≤ 10s) naming attempt and emits one session.naming_failed until the LLM recovers or the policy is switched off.
runtime.naming.after_turns
The number of completed runs after which the FIRST auto-name fires (fire on the Nth completed run). Zero (the default) resolves to 1 at run start. A negative value is rejected.
runtime.naming.repeat_every
When greater than 0, re-names the session every N completed turns after the first. Zero (the default) names once only. A negative value is rejected.
runtime.naming.max_repetitions
Caps the TOTAL number of auto-namings (including the first). It is REQUIRED >= 1 whenever runtime.naming.repeat_every > 0 — no unlimited value exists, so unbounded periodic re-naming is unrepresentable. Ignored when repeat_every is 0 (naming happens once). A negative value is rejected. For a repeating policy constructed programmatically (an embedder building a naming spec by hand, bypassing the yaml/wire validators), the policy-level default of 5 applies when the cap is unset — the no-unlimited invariant holds on every path. The cap is PER-CYCLE, not per-session-lifetime: a manual clear (an empty sessions.set_title) re-arms auto-naming by zeroing the naming counters, so each clear opens a fresh arming cycle with its own cap budget.
runtime.naming.max_title_len
Bounds the auto-generated title in runes. Zero (the default) resolves to 80 at run start; a set value must be within [8, 200]. The auto title is deterministically clamped to this bound (unlike the manual sessions.set_title verb, which rejects oversize input — the trusted-internal vs untrusted-boundary asymmetry is intentional).
runtime.naming.model
The model the auto-naming Complete call requests. Empty (the default) uses the run's effective model. A set value is validated against llm.model_profiles at boot; point it at a cheap profile to keep naming inexpensive (the naming call consumes the session identity's governance budget by design).
runtime.naming.reasoning_mode
Controls reasoning only for the session auto-naming call. Empty (the default) and off explicitly disable reasoning so a model profile's planner reasoning default cannot consume the fixed title allowance. provider_default omits the provider reasoning control while still suppressing model-profile inheritance; use it only for a naming provider that rejects or does not implement an explicit disable. A durable per-agent naming section replaces the whole yaml section, so an omitted reasoning_mode there resolves to the default off.
Memory
Cumulative session-memory migration — PR #779
The accepted D-477 / RFC 002 target replaces the separate retained execution window and pair-only summary pipeline. It is not implemented by the historical RC1–RC4 tags. The current implementation uses one execution-memory owner; release acceptance remains tracked separately in the implementation tracker.
The target standard configuration is:
memory:
strategy: rolling_summary
recent_turns: 20The checkpoint carries context across windows; recent_turns bounds detailed history only. Set memory.strategy: none for stateless operation. Compaction uses memory.budget_tokens for the complete assembled request (zero derives a safe target from the effective model), independently of model output limits. sessions continues to own lifetime and deletion. Remove sessions.retained_context_turns, the SDK WithRetainedContext switch and planner.token_budget; removed keys must fail with migration guidance. No compatibility engine or silent old-record reinterpretation is planned. Test the new version with fresh isolated sessions before deployment; do not rewrite old RC tags or delete uncertain-operation evidence to make a session start.
Landed budget consolidation: planner.token_budget and HARBOR_PLANNER_TOKEN_BUDGET are removed and fail with migration guidance. Use memory.budget_tokens / HARBOR_MEMORY_BUDGET_TOKENS. Rolling-summary execution builds the compactor even with a zero explicit target. The separate session/SDK activation switches are also removed: choose memory.strategy: rolling_summary and memory.recent_turns for serving and embedding. Omitted YAML memory settings and config.Defaults() now select rolling_summary with recent_turns: 20. This retains short-term session context and may make governed compaction calls. Set memory.strategy: none explicitly for stateless execution. The legacy pair-summary engine and its background recovery loop are removed. Administrative Inspect/Put/Delete use the same cumulative owner as runtime execution. Put accepts a conversational note, not caller-asserted tool receipts or execution metadata.
Large memory.get values return source-bound references. Read them through bounded artifacts.get, which checks the current memory source on every read. They are not separate stored artifacts and cannot be presigned. Deletion, expiry or a changed source invalidates the old reference; re-read memory.list to inspect the current representation. Use memory.delete to delete the source.
memory.driver
MemoryStore driver. Default: inmem. Validation: inmem / sqlite / postgres.
memory.dsn
Persistent-driver connection string. Default: empty. Validation: required when driver != "inmem". Secret: redacted.
Note (D-174): conversation memory durability rides on the configured state.driver, not memory.dsn. All memory drivers share the cumulative owner backed by the StateStore, so a SQL memory driver with a SQL memory.dsn but an inmem state.driver is NOT durable across a restart. To make memory durable, set a SQL state.driver; inmem memory + a SQL StateStore is already durable.
memory.migration_mode
Postgres migration posture at store open. Default: apply when empty or omitted. Validation: apply or verify, and only valid when driver is postgres. apply takes Harbor's session advisory lock and applies missing migrations, so it requires a direct/session-capable Postgres endpoint. verify performs read-only checks of the subsystem's namespaced harbor_schema_migrations rows, harbor_store_identity, and required physical schema, takes no advisory lock, begins no transaction, executes no DDL/write, and fails loud if identity, checksums, schema, or an embedded migration is missing or malformed. It is safe for transaction-pooled connections after a separate apply step has completed. Restart-required.
memory.strategy
Memory shape. Default: rolling_summary. Validation: none / rolling_summary. The removed truncation strategy is rejected: cumulative memory never discards unsummarized history to fit the recent window. Every memory driver uses the same StateStore-backed execution owner. A SQL state.driver makes this history durable across restart. rolling_summary requires the governed LLM client; the runtime constructs its trajectory compactor through that client. Missing dependencies fail at boot rather than selecting a stub or a second summary pipeline.
memory.budget_tokens
Working-input compaction target, in estimated tokens. Validation: >= 0. With rolling_summary, zero derives the target from the effective model's input capacity, context reserve and requested output reservation. A positive value also builds the within-run compactor for stateless execution. It is a soft target: preserve fresh tool results; the final model admission guard remains mandatory. It never sets or lowers a model's output-token allowance. Set the deployment default in harbor.yaml (or HARBOR_MEMORY_BUDGET_TOKENS); changing that default requires a restart. Served runtimes advertising agent_config_memory_v1 also accept a per-agent memory.budget_tokens override through a versioned agent_config.set_revision. Omit that revision section to inherit YAML again. Overrides apply to the next run and require an already wired compactor; they cannot enable a missing LLM dependency.
There is no second planner budget or pair-only summary engine. The former memory.recovery_backlog_max setting and its environment override are removed and rejected. Cumulative memory fails explicitly when unsummarized evidence cannot fit; it never drops a recovery backlog to create space.
memory.recent_turns
Number of recent root executions the cumulative rolling_summary strategy keeps in detail. Default: 20; explicit 0 also selects twenty turns. Validation: non-negative, with no separate 32-turn maximum. Larger windows retain more detail and can increase storage, projection and compaction work. The checkpoint carries earlier meaning after covered detail leaves the window; this number is not a checkpoint-history limit. Ignored by none.
memory.summarizer.max_tokens
Completion-token allowance per compaction call, including provider reasoning where applicable. Validation: >= 0. Omitted or zero keeps the existing 2048-token default; a positive value selects a deployment-specific allowance. For example, 8192 gives a reasoning model more room to finish its structured summary. This is separate from memory.budget_tokens (working input) and the driving model's output allowance. Governed model-capacity admission still applies; independently routed compaction clamps the allowance to its selected profile's output maximum. Increasing this allowance reserves more output space and can reduce input space per compaction chunk. Truncated or otherwise incomplete summaries remain errors; they never replace a valid checkpoint. Valid narratives have no separate fixed 16 KiB output or persisted-checkpoint ceiling. Strict structured-summary validation remains, and each subsequent maintenance request must still fit the configured heavy-content threshold and its selected model's input budget.
Set it in YAML or HARBOR_MEMORY_SUMMARIZER_MAX_TOKENS. Restart-required; not an agent-config Protocol field. It applies to both within-run and cross-turn compaction through the same governed client, with no change to prompts, reasoning controls or route authority.
memory.summarizer.max_calls
Maximum chronological completion calls in one compaction. Omitted or zero selects 16; positive values set the deployment's allowance. Negative values are rejected. Configure through YAML or HARBOR_MEMORY_SUMMARIZER_MAX_CALLS; restart-required. This is a maintenance-work allowance, not a retry count or session-lifetime limit. Every call still follows model admission, cancellation, rate/cost governance and the existing retry policy. Exhaustion fails explicitly without installing a partial summary or dropping retained evidence.
Increasing this value can increase maintenance time and spend. Receipt consumers must accept the positive-ordinal maintenance identity grammar before using more than sixteen calls; older versions may reject those receipts. The default remains compatible. Identity, nonce and route validation are unchanged.
memory.summarizer.model
Pins the model the rolling_summary compaction summariser requests, independent of the planner's model — set it to a cheaper/faster model to keep compaction cheap (D-243). Default: empty → the main LLM's default model (today's behavior). A model with no matching model_profiles entry fails at runtime like any unsupported model; it is not rejected at load time. It selects the maintenance model whenever a within-run or cross-turn compactor is configured. An externally selected provider route or execution grant remains authoritative: it currently binds maintenance to its admitted model too. A different static summarizer model is rejected on that path, not silently authorized or replaced with the runtime's local key. Separately authorized maintenance routing is not implemented by this YAML field. This setting is restart-required.
memory.summarizer.provider_route
Optional, restart-required opaque selector for a separately authorized compaction model on externally routed runs (D-482). Mutually exclusive with model. Uses the existing llm.provider_route resolver and the same governed Bifrost client; it does not contain a credential, endpoint, or identity override. Supply all fields:
memory:
budget_tokens: 64000 # example deployment target, not a framework default
summarizer:
provider_route:
route_id: your-maintenance-route
route_generation: 1
provider_connection_id: your-provider-connection
provider_connection_generation: 1
credential_asset_generation: 1
model_selector: your-compaction-aliasObtain current selectors/generations from the configured resolver's control plane; the example identifiers are placeholders. The resolver must authorize that model for the actual runtime, agent and user and return its model profile. Missing admission, revocation, stale generations, missing profiles and signed execution grants fail closed. There is no fallback to a local key or the driving model. Plain static-model runs continue to use summarizer.model instead.
Each chunk is packed against the selected compaction model's own input capacity, output allowance and configured context reserve. It does not inherit the driving model's reasoning effort or large output reservation. The existing summary output allowance is clamped to the selected model's maximum; reasoning controls are omitted. Selection is rechecked by the normal client, and credentials are resolved afresh by the Bifrost leaf for each actual attempt. This route selection is boot configuration; agent_config_memory_v1 edits only the working-input budget.
Each selector leaf also accepts its normal environment override, prefixed with HARBOR_MEMORY_SUMMARIZER_PROVIDER_ROUTE_: ROUTE_ID, ROUTE_GENERATION, PROVIDER_CONNECTION_ID, PROVIDER_CONNECTION_GENERATION, CREDENTIAL_ASSET_GENERATION, and MODEL_SELECTOR. Supply all six when YAML omits the section. Environment values override individual YAML leaves; omitting both leaves the route unselected. Partial or empty selectors fail validation, and generation values must be positive unsigned 64-bit integers. This lets an immutable image keep its working-input budget in YAML while selecting a route through deployment configuration, without embedding environment-specific IDs.
memory.summarizer.prompt
Operator guidance APPENDED to the baseline rolling_summary summariser system prompt behind an explicit "extend, do not override" separator (D-243) — it never replaces the baseline role framing or conciseness/preserve-goals guarantees. Default: empty → baseline prompt only. It applies whenever a within-run or cross-turn compactor is configured.
Removed semantic-memory settings
memory.retrieval, memory.retrieval_top_k and memory.retrieval_min_score are removed under D-477. Their corresponding HARBOR_MEMORY_RETRIEVAL* overrides are also rejected, including empty or zero values. Remove these settings; cumulative session memory uses checkpoints and recent execution evidence, not a separate vector index. External long-term memory remains an integration responsibility. Semantic skill retrieval and the embedding-client seam are unchanged.
Skills
skills.driver
SkillStore driver. Default: empty (block fully optional; an empty block disables the subsystem). Validation: when set, localdb / postgres.
skills.dsn
Driver connection string. Default: empty. Validation: required when driver is localdb or postgres. Secret: redacted.
skills.migration_mode
Postgres migration posture at store open. Default: apply when empty or omitted. Validation: apply or verify, and only valid when driver is postgres. apply takes Harbor's session advisory lock and applies missing migrations, so it requires a direct/session-capable Postgres endpoint. verify performs read-only checks of the subsystem's namespaced harbor_schema_migrations rows, harbor_store_identity, and required physical schema, takes no advisory lock, begins no transaction, executes no DDL/write, and fails loud if identity, checksums, schema, or an embedded migration is missing or malformed. It is safe for transaction-pooled connections after a separate apply step has completed. Restart-required.
skills.directory.pinned
Skill names anchored at the top of every <skills_context> view, in declaration order (Phase 111d — D-201). Pinning is an ordering preference only — pinned skills are never exempt from the capability filter. Default: empty. Validation: entries non-empty and unique; requires skills.driver to be set.
skills.directory.max_entries
Cap on the injected directory view's length. Default: 0 — falls back to planner.skills_context_max's resolved value (default 5) so the pre-111d injection-budget knob keeps its meaning. Validation: 0 or in [1, 200].
skills.directory.selection
Ordering of the unpinned remainder. Default: pinned_then_recent (UpdatedAt DESC). Validation: when set, must be pinned_then_recent. pinned_then_top (UseCount DESC) is recognised but rejected as not yet wired — no production path increments skill usage counters, so the ordering would silently degrade to alphabetical; the validator fails loud instead (the tools.http_manifests precedent). It becomes accepted when a usage-tracking path lands.
skills.retrieval
Opt-in Search / skill_search ranking mode (Phase 84d — D-191). Empty (the default) keeps the token-savvy FTS5 → regex → exact ladder; semantic ranks by embedding similarity over the identity-scoped catalog (result path semantic). Capability filtering, redaction, and the budgeter apply unchanged on top. Default: empty. Validation: empty or semantic; semantic requires the embeddings block and skills.driver to be set.
skills.session_personal_cutover.tenants
Optional, restart-required declarations for the durable session-personal-skill cutover. This is a finite operator allowlist, not runtime tenant or writer discovery. Supplying one or more declarations requires a configured skills.driver and skills.dsn; a declaration that cannot reach the required SkillStore fails boot rather than being silently ignored.
Each entry carries tenant_id, epoch, roster_digest, and legacy_writers_drained:
tenant_idis an exact, case-sensitive opaque tenant key.epochidentifies one intentional migration rollout. Use a new value for a new rollout; it is not a clock supplied by Harbor.roster_digestis the operator-attested digest of the drained legacy writer roster. It is an integrity label, not a secret.legacy_writers_draineddefaults tofalse. Set ittrueonly after all older writers are drained and the roster has been independently attested.
The list is bounded to 256 entries. tenant_id, epoch, and roster_digest must each be non-empty printable ASCII tokens with no leading or trailing whitespace (maximum lengths: 128, 128, and 256 bytes respectively). Tenant IDs must be unique exactly; empty, malformed, duplicate, or over-bound declarations fail boot loud.
With this block omitted, with a tenant absent from it, or with legacy_writers_drained: false, the tenant stays in read-only dual_read. Existing eligible legacy session-personal skills remain readable, but session-personal mutations are deliberately refused with session_skill_cutover_pending (HTTP 409); Harbor does not create another legacy shared-skill body. A drained declaration authorizes only Harbor's bounded durable checkpointed migration. It does not make writes safe immediately: mutations remain refused until a fresh verification pass durably authorizes state_only. Harbor discovers no tenants or writers at runtime, and a malformed or declaration-mismatched durable checkpoint remains mutation-refusing dual_read with a loud diagnostic.
skills:
driver: localdb
dsn: /var/lib/harbor/skills.sqlite
session_personal_cutover:
tenants:
- tenant_id: tenant-acme
epoch: 2026-08-cutover-01
roster_digest: sha256-... # attested label, not a secret
legacy_writers_drained: falseskills.boot_agent_packs
Restart-required operator declarations that load node-local boot agent packs at boot (Phase 248 — D-427): a bounded list of per-agent skill packages read from the filesystem. Default: empty — no packs are loaded. Validation: requires a configured skills.driver and skills.dsn; a declaration on an unconfigured skills block fails boot loud.
Each entry names exactly ONE (tenant_id, agent_id) pair — an exact, case-sensitive opaque tenant key and the resolved boot agent it belongs to. There are no wildcards, no tenant-wide application, and no runtime discovery: a declaration is honored only for that exact tenant + resolved boot agent, and only when the effective agent actually resolves to the declared agent_id.
The driver/DSN pair provisions the store that persists revision and personal skill state — the boot packs themselves are never stored there. With driver: postgres that state is durable and shared across replicas, but the boot packs remain node-local: each node reloads them from its own files at boot, so a file change on one node never converges the others. Treat pack files as per-node configuration, not a shared catalog.
Each entry carries tenant_id, agent_id, directory, and include:
tenant_id— the exact, case-sensitive opaque tenant key (above).agent_id— the exact resolved boot agent the pack belongs to.directory— the pack root, resolved relative to the directory containing this config file, never the process working directory.include— one or more package directories, each RELATIVE todirectory. Oneincludeentry is exactly one package directory containing exactly one case-sensitive top-level regular UTF-8SKILL.md. The v1.28 contract is resource-free: no auxiliary resource payloads ride alongside the skill body.
Loading is strict and fails loud. The loader rejects symlinks, hardlinks, special files (FIFO / socket / device), path traversal outside the pack root, duplicate declarations (the same tenant + agent, or the same package included twice), and any out-of-bounds shape. Packs load eagerly and immutably BEFORE the runtime is ready; the loaded set is fixed for the process lifetime, and file changes take effect only on restart — never hot-reloaded.
The loader is read-only by construction: it performs no writes, exposes no admin verbs, has no lifecycle, and never creates agents or widens reach. It is separate from the existing EnsureBootAgentLifecycle boot maintenance, which MAY still write revisions; the two coexist.
Boot-pack items and revision (agent_packs) items form ONE operator tier, resolved last in the run-visible composition. A skill present in both a boot pack and the revision with the same content hash is accepted (deduplicated); the same skill at DIFFERENT hashes is a conflict that fails loud. The combined item count (boot + revision) is capped at 256.
Boot-owned items are read-only at every mutation surface, and the refusal is precise. A new write / upsert / proposal commit / rollback / activation that targets a boot-owned canonical name fails with a typed boot-owned conflict even when the payload carries the same content hash — the NAME is boot-owned, not the bytes. Removal may delete a real legacy active-revision shadow (a revision agent_packs record at that name), but a boot-only removal — a name whose only contributor is the boot pack — returns a typed read-only refusal and never removes the boot contribution.
Preview surfaces report the effective-composition provenance of each item as the exact closed set boot / revision / both, alongside the per-package semantic content hash of each loaded package. The loaded boot pack set as a whole carries the deterministic boot_pack_set_hash — a node-local digest over the set, distinct from any per-package hash, and never shared through the store: boot packs stay per-node, so Postgres never converges them. Config removal on the next runtime removes only the node-local boot contribution: an independently persisted active revision remains visible in the shared preview as revision-only provenance — no tombstone and no erasure of the durable revision — while a name with BOTH contributions absent stays non-oracularly unavailable (typed not-found/denied), never a fabricated empty or a cross-principal row.
Headless RunOnce against a boot-pack agent is unsupported and fails loud — it never silently runs without the packs. Production harbor serve and the devstack resolve the SAME loader/composer path: the devstack's synthetic boot agent opens the same eager index before its run-loop driver and composes the baseline exactly like a production boot agent, and the run-start skill snapshot binds the frozen entries through the exact (tenant, effective-boot-agent) membership lookup with the boot set hash, the combined hash, and per-item boot|revision|both provenance.
skills:
driver: postgres
dsn: ${SKILLS_DSN}
boot_agent_packs:
- tenant_id: acme
agent_id: harbor-dev-agent
directory: /etc/harbor/skills
include:
- workbench-foundation
- workbench-prototype-authoring
- workbench-brand-identity
- workbench-deck-authoringTasks
tasks.driver
TaskRegistry driver. Default: inprocess. Validation: inprocess / durable.
inprocess(default) — keeps task/group/patch state in memory. Live state is observable, but a runtime restart starts with an empty registry.durable— persists task/group/patch records through the configuredStateStore, so they survive a runtime restart. On open it replays every record and runs a recovery sweep: a task leftrunningby a crash is transitioned tofailedwith the reserved error coderuntime_restarted(the record is recovered; execution is not re-driven). It reuses the runtime'sStateStore— no extra config beyondtasks.driver: durable. For survival across a real process restart, pair it with a durablestate.driver(sqliteorpostgres); withstate.driver: inmemrecords survive only an in-process driver reopen. Selectingdurablewith noStateStorewired fails loudly at boot.
tasks.retain_turn_timeout
Max time the engine blocks a foreground turn waiting for retain- turn groups to resolve. Default: 5m. Validation: > 0.
tasks.continuation_hop_limit
Max background-continuation hops a planner runtime may take before requiring user confirmation. Default: 8. Validation: > 0.
Sessions
Session execution memory
Configure root conversation memory under memory, not sessions. rolling_summary selects cumulative execution-context memory; recent_turns accepts non-negative values, with zero selecting twenty detailed turns. Restart-required. It persists recent terminal root runs in the configured StateStore and projects permitted historical evidence into the next root request. It replaces legacy pair-only memory projection for this mode; external long-term memory and trusted completion hooks are unchanged. Child tasks use their explicit task context, not the root window, and their private transcripts are not added to it.
memory:
strategy: rolling_summary
recent_turns: 20RunOnce uses the same memory configuration; no per-call activation override exists. sessions.retained_context_turns, its environment override and the SDK WithRetainedContext option are removed. Use memory.strategy: none for a stateless stack. In-memory StateStore retention ends with the process; SQLite/Postgres preserve committed content across restarts. The configured recent-turn window and session idle TTL govern retention; interrupted settlement can temporarily overflow the detail target until successful compaction. Expiry and erasure apply. The old 32-turn, 256-step, 256-older-turn, 64-reference and 16 KiB reference-metadata ceilings are removed, as is the former 512 KiB evidence/journal ceiling: memory.budget_tokens governs model-input compaction, not stored byte size. Run execution follows its existing configured step/tranche budget rather than a second journal cap. All retained references remain scope/lifetime-validated; their model-facing metadata passes ordinary assembled-request admission. Successful compaction replaces model-facing covered detail with cumulative checkpoint meaning while retaining exact evidence; window rollover is not forgetting. This is private execution evidence, not additional content in sessions.turns.*.
Admission and the query are committed before work. Each dispatch requires an intent checkpoint before execution and a settlement checkpoint before dependent inference. A failed persistence write stops the run, never asks the planner to retry the action. Terminal publication seals the journal atomically with the session window, then removes transient frames by exact generation. An interrupted run can still have an unknown outcome between external execution and settlement. Reconcile with the owning service rather than automatically repeating it. Automatic cold-run continuation remains unsupported.
Private windows use format version 4 with cumulative coverage and fenced admissions. Source strings and identifier/completeness values remain exact, while JSON envelope formatting may canonicalize. Incompatible older records are refused explicitly; there is no compatibility layer or implicit source conversion. No stored historical action is executed by restoration.
sessions.idle_ttl
Time before an idle session is swept. Default: 24h. Validation:
0 AND <=
hard_cap.
sessions.hard_cap
Absolute max session lifetime. Default: 720h (30 days). Validation: > 0.
sessions.sweep_interval
Background sweeper period. Default: 15m. Validation: > 0 AND <= idle_ttl.
sessions.turns.driver
Driver for the durable conversation-turn projection. Default: empty — the sessions.turns.* block is left unwired (the projection is not built). Validation: when set, inmem / sqlite / postgres. Restart-required.
sessions.turns.dsn
Driver connection string for the conversation-turn projection. Default: empty. Validation: required when driver is sqlite or postgres; unused for inmem. SQLite: file path. Postgres: libpq URL. Secret: redacted in audit logs. Restart-required.
sessions.turns.migration_mode
Postgres migration posture at store open. Default: apply when empty or omitted. Validation: apply or verify, and only valid when driver is postgres. apply takes Harbor's session advisory lock and applies missing migrations, so it requires a direct/session-capable Postgres endpoint. verify performs read-only checks of the subsystem's namespaced harbor_schema_migrations rows, harbor_store_identity, and required physical schema, takes no advisory lock, begins no transaction, executes no DDL/write, and fails loud if identity, checksums, schema, or an embedded migration is missing or malformed. It is safe for transaction-pooled connections after a separate apply step has completed. Restart-required.
sessions.turns.retention
Number of newest-turn rows the projection retains per session. Default: 0 — zero or negative selects the projection's documented default. Restart-required.
Pause/Resume
pauseresume.max_park_duration
Ceiling on how long a pause may stay parked before the pause sweeper resumes it with the typed timeout Decision (pause.resumed, D-096) and the waiting run terminates as a constraints-conflict (Phase 111c / D-200). Default: 0 — pauses never expire and the sweeper is not started. Validation: >= 0.
pauseresume.sweep_interval
Pause-sweeper scan period (consumed only when max_park_duration > 0). Default: 1m (0 = the default applies). Validation: >= 0 AND <= max_park_duration when both are set.
Artifacts
artifacts.driver
ArtifactStore driver. Default: inmem. Validation: inmem / fs / sqlite / postgres / s3.
artifacts.fs_root
Root directory for the fs driver. Default: empty. Validation: required when driver = "fs". Auto-created at driver New.
artifacts.dsn
SQL-driver connection string. Default: empty. Validation: required when driver is sqlite or postgres. Secret: redacted.
artifacts.migration_mode
Postgres migration posture at store open. Default: apply when empty or omitted. Validation: apply or verify, and only valid when driver is postgres. apply takes Harbor's session advisory lock and applies missing migrations, so it requires a direct/session-capable Postgres endpoint. verify performs read-only checks of the subsystem's namespaced harbor_schema_migrations rows, harbor_store_identity, and required physical schema, takes no advisory lock, begins no transaction, executes no DDL/write, and fails loud if identity, checksums, schema, or an embedded migration is missing or malformed. It is safe for transaction-pooled connections after a separate apply step has completed. Restart-required.
artifacts.heavy_output_threshold_bytes
Byte size at which the runtime mandatorily routes a payload bound for a model's context window through the ArtifactStore (D-022 / D-026 / D-358). Default: 131072 (128 KiB). Validation: >= 0.
It governs the LLM-context arm only — the tool dispatcher's promote-to-stub boundary, the LLM-edge leak guard and auto-materialization, the trajectory-compaction payload budget, and the tools.content_stats.heavy_threshold_bytes figure that reports it. The routing is a runtime-wide invariant; there are no per-tool overrides.
It does not govern Console-facing Protocol replies. pause.list, memory.get / memory.list, the flow catalog and the mcp.servers.read_resource / mcp.apps.call_tool / mcp.apps.tool_context reads select inline-versus-reference at a pinned 32 KiB bound that deliberately does not track this key: those payloads are rendered by a browser, never placed in a prompt, and their selected reply arm is Protocol-visible. Raising this key widens what a planner sees inline; it leaves every browser-facing reply unchanged.
Not hot-reloadable — restart required.
artifacts.fetch_default_max_bytes
Window served by an artifact read-back when the caller names no bound of its own — artifacts.get on the Protocol surface, and the artifact_fetch builtin the model calls. Default: 65536 (64 KiB). Validation: >= 0 (0 selects the built-in default), and the resolved value must not exceed the resolved artifacts.fetch_hard_max_bytes.
heavy_output_threshold_bytes above governs what GOES OUT to the store; this and the ceiling below govern what COMES BACK, and the pair is meant to be tuned together. Not hot-reloadable — restart required.
artifacts.fetch_hard_max_bytes
Ceiling a caller's own read-back bound is clamped to. Default: 1048576 (1 MiB). Validation: >= 0 (0 selects the built-in default).
A request above the ceiling is served at the ceiling, not refused, and the response says so through its truncated / total_size_bytes / returned_bytes fields — a caller cannot know a deployment's ceiling before asking, so a refusal would cost a round trip and teach nothing. Silent truncation is what the fail-loud rule forbids; truthful truncation is the posture.
The guarantee is bounded and stated as such: this ceiling bounds ONE read. It is not a budget over repeated reads — aggregate consumption stays the governance layer's concern (cost ceilings and rate limits). Not hot-reloadable — restart required.
artifacts.s3_bucket
S3 bucket name (Phase 19). Default: empty. Validation: required when driver = "s3".
artifacts.s3_endpoint
Base URL for non-AWS S3-compatible backends (MinIO / R2). Default: empty (AWS default endpoint resolution).
artifacts.s3_region
AWS region. Default: us-east-1.
artifacts.s3_prefix
Path prefix inside the bucket. Lets multiple Harbor deployments share one bucket. Default: empty.
artifacts.s3_access_key_id
S3 access key. Default: empty (SDK default credential chain). Secret: redacted.
artifacts.s3_secret_access_key
S3 secret key. Default: empty. Secret: redacted.
artifacts.s3_use_path_style
Use path-style addressing instead of virtual-host (MinIO / older R2 endpoints). Default: false.
Events
events.driver
EventBus driver. Default: inmem. Validation: inmem or durable (Phase 57 — StateStore-backed).
events.max_subscribers_per_session
Cap on concurrent subscribers per session. Default: 16. Validation: > 0.
events.subscriber_buffer_size
Per-subscriber channel buffer. Default: 256. Validation: > 0.
events.idle_timeout
Max idle time before a subscriber is reaped. Default: 60s. Validation: > 0.
events.drop_window
Backpressure drop-policy window. Default: 1s. Validation: > 0.
events.replay_buffer_size
In-memory ring-buffer depth for replay. Default: 10000. Validation: >= 0 (zero disables replay on the inmem driver).
events.state_driver
StateStore driver the durable event driver persists through. Default: empty (degrades to best-effort in-memory ring with a loud warning per D-074).
events.state_dsn
DSN for the durable event log's StateStore. Default: empty. Validation: required when state_driver is non-empty AND non-inmem. Secret: redacted.
events.legacy_writers_drained
Explicit durable-event writer-drain acknowledgement. Default: false. A v1.29.1 Runtime finding no shared sequence authority fails boot until this is true, including an apparently empty store: emptiness cannot prove that a silent legacy writer is absent. Set it only after every v1.29.0 process that can publish events to the same StateStore has stopped. An ordinary zero-downtime or rolling deploy is not compliant because it starts the new writer before the old writer stops; use a true stop-before-start, suspend-then-resume, or an equivalent platform guarantee. Migration-only processes that never publish events are not event writers. Once the v1.29.1 authority exists, later restarts do not require the acknowledgement.
Audit (reserved)
Reserved block — populated as audit phases land. No leaf fields today.
Protocol
protocol.max_request_bytes
Upper bound on artifacts.put upload body size (Phase 73l / D-120). Default: 4 MiB (DefaultMaxRequestBytes). Bodies above this fail with HTTP 413.
CLI
cli.dev_hot_reload.enabled
harbor dev hot-reload watcher toggle (Phase 65 / D-099). Default: true. The --no-hot-reload flag is the operator-facing escape hatch.
cli.dev_hot_reload.policy
Retain-in-flight policy on a triggered restart. Default: drain. Validation: drain / cancel / disabled.
cli.dev_hot_reload.drain_timeout
Cap on the drain policy's wait for in-flight RunLoops. Default: 5s. Validation: > 0.
cli.dev_hot_reload.watch_roots
Paths the fsnotify watcher monitors. Default: [".harbor/agents"] (the Phase 66 drafts directory). The dev cmd unions this with the loaded config file's directory.
Tools
tools.http_manifests
Paths to UTCP-style YAML manifests for the Phase 27 HTTP driver. Default: empty list. Loaded at boot by the HTTP-manifest boot loader (D-279): assembly calls http.LoadManifest on each entry and http.RegisterManifest to register its tools on the runtime catalog by name — AFTER built-in tools and BEFORE tools.entries[] applies its middleware, so an entries[] block naming a manifest tool (approval / OAuth / loading-mode) resolves cleanly. Once registered, a manifest tool is indistinguishable from one registered inline via RegisterHTTPTool — same descriptor, same ToolPolicy shell, same catalog wiring — so tools.entries[].oauth (see below) binds an OAuth provider to it with zero new machinery.
Path resolution. A relative entry resolves against the loaded config file's directory (filepath.Clean(filepath.Join(configDir, entry))); one that escapes the directory is rejected at Load time with a fieldError naming tools.http_manifests[i] (§7 rule 5) — lexically (a ../ that walks outside it) always, and via a symlink-containment re-check when the joined path exists on disk (a symlink inside the directory pointing outside it is rejected; a not-yet-existing manifest gets the lexical check only, since boot is the existence-enforcement home). An absolute entry is filepath.Cleaned and accepted as-is — the same trust posture as artifacts.fs_root. A hand-built *Config constructed without config.Load (a headless Go embedder) skips this resolution step, as do entries injected post-Load via config.WithOverrides (no config directory is retained on *Config); pass absolute paths in both cases.
Validation vs. boot. Validate checks the list structurally only — each entry non-empty after trim, unique after filepath.Clean — so harbor validate accepts a config whose manifest file does not exist yet (existence/parsing is boot's job, matching the tools.mcp_servers precedent of not probing URLs at validate time). A listed manifest that is missing, unreadable, unparseable, or fails the driver's own validation (http.ErrManifestInvalid: literal secrets, .Auth template leaks, unknown fields, missing env refs) fails Assemble loudly, naming both the file and tools.http_manifests[i]. A manifest tool whose name collides with an already-registered catalog tool fails the boot the same way (tools.ErrToolDuplicateName) — never a silent skip.
Restart-required: manifests are boot-only and do not hot-reload. See examples/tools/http-weather.yaml for a worked manifest and examples/harbor.yaml / examples/dev.yaml for the paired tools.entries[].oauth binding that exercises catalog OAuth wrapping end to end.
tools.mcp_servers
MCP southbound attachments (Phase 28). Each entry needs name, transport_mode, and either url (HTTP transports) or command (stdio). See MCPServerConfig godoc for the full surface.
tools.mcp_servers[].oauth_provider (and .meta_annotations)
Southbound per-identity OAuth binding (D-278). oauth_provider names a declared tools.oauth_providers[] entry (a NON-SECRET provider name); a bound http(s) connection resolves a fresh bearer for the calling identity on every identity-stamped per-call RPC and injects Authorization: Bearer <tok> on that request only (the connect-time headers stay for the initialize/discovery handshake). A bound provider whose token fetch fails aborts the call loud — never an unauthenticated fallback; a consent_required refusal parks the run on the unified pause/resume primitive. Validation rejects an unknown provider name (the error lists the declared names), a binding on any connection without an http(s) url — explicit stdio AND an omitted/auto transport with only a command, which would auto-select stdio at connect and silently never inject — and a static Authorization header alongside the binding (one auth mode per connection).
meta_annotations is a static, NON-SECRET map[string]string merged into the MCP _meta on every identity-stamped call — the deployment's own attribution vocabulary, passed to the server alongside the (tenant, user, session) triple and the provenance agent_id. See examples/dev.yaml for a worked stanza.
Each KEY is an annotation path: a key with no . sets a top-level _meta key, and a DOTTED key NESTS. vendor.account_id: acct-42 lands at _meta.vendor.account_id, not as the literal flat key _meta["vendor.account_id"]. This is the same interpretation injection.meta_key has always had, so one _meta namespace has one meaning regardless of which mechanism wrote into it — which is what lets a receiver-style server read ONE nested namespace and find both an injected credential and its non-secret companion value.
Behaviour change (v1.24). A dotted annotation key was legal before and landed FLAT; it now nests. No config rewrite is required — the new shape is the shape a dotted key was already asking for — but a connection that declared one sends a different
_metaon its next call after upgrade. Flat keys are unaffected.
Validation rejects, loudly, at boot and at every runtime door (agent_config.add_mcp_connection, agent_config.set_revision, and attach):
- an empty key or an empty path segment (
a..b); - a key whose WHOLE value or ANY dot-segment is reserved —
tenant,user,session,agent_id,traceparent,tracestate, and anyio.modelcontextprotocol/-prefixed key. Both arms are checked: the whole-key arm is the only one that sees the spec-reserved namespace (its segments carry no prefix), and the per-segment arm is the only one that sees a reserved key used as a path component.tenant.foois refused; - a path deeper than 16 segments (see
meta_keybelow for why); - a path that COLLIDES with another declared path on the same connection — equal to it, or a prefix of it — including the
injection.meta_keypath.{vendor: x, vendor.id: y}is refused, and so is a flatvendorannotation alongsideinjection.meta_key: vendor.api_key. Refusing collisions is also what makes the merge order-independent: distinct non-prefixing paths write disjoint leaves, so map-iteration order cannot change the bytes on the wire. A connection persisted before this rule shipped that carries a colliding pair fails its calls loudly rather than silently picking a winner.
Audit consequence — over-redaction, not a leak. The audit redactor matches on a key and replaces the WHOLE value without recursing, and its receiver-injection rule matches on the LAST -/_/.-separated segment. A flat token.env key therefore was NOT redacted (last segment env); nested, the node key is token, which matches, so the ENTIRE token subtree collapses to *** — non-secret siblings included. Redaction COVERAGE is preserved (nothing that was redacted stops being redacted); the cost is reduced audit usefulness under a credential-named namespace. If you want a namespace's non-secret annotations to stay readable in audit payloads, do not name the node with a credential token.
tools.mcp_servers[].injection
Per-user credential INJECTION for a RECEIVER-STYLE server (D-341) — one that authenticates by RECEIVING its credential directly on each request (an arbitrary header, an Authorization: Basic value, or a _meta key) instead of PULLING it via RFC 8693. The runtime SOURCES the acting user's credential from the named broker on every identity-stamped call (the SAME per-user broker-pull as oauth_provider — never held, never client-pushed) and INJECTS it in the declared, NON-SECRET form. Only the pulled value is secret; the mapping is config. Fields:
provider— a declaredtools.oauth_providers[]broker name (required).form—header/basic/meta(required).header— forform: header, the target request header name (e.g.x-vendor-api-key). Must be a redaction-covered credential key and must not beAuthorization(useform: basic).basic_username— forform: basic, the (optional, non-secret) username half; the pulled credential is the password half ofAuthorization: Basic base64(username ":" credential).meta_key— forform: meta, the target_metakey PATH, dot-separated for nesting (e.g.vendor.api_key). No path segment may be a reserved_metakey and the leaf must be a redaction-covered credential key. The path is capped at 16 segments — the SAME capmeta_annotationspaths carry, applied at every door (before v1.24 it was enforced only on the wire door, so a boot-declared over-deep path was accepted where the identical wire-declared one was refused). The cap exists so a declared path can never push an audit payload past the redactor's deep-walk ceiling and turn every audit emit for the connection into a hard redaction failure. It may not collide with a declaredmeta_annotationspath (see above).
Injection is mutually exclusive with oauth_provider / tool_oauth_providers / a static Authorization header (one auth mode per connection). The connection needs an http(s) url and its host must be in the broker's allowed_downstream_hosts (fail-closed — the credential leaves to that host). A broker error fails the call loud (never an unauthenticated send). The audit redactor is extended so the Basic scheme, the declared header key, and the _meta credential leaf are all held to ***; validation rejects a target key the redactor would not cover, so an injected value can never reach an audit payload uncredacted. See examples/harbor.yaml › tools.mcp_servers[].injection.
tools.mcp_servers[].policy (and .tool_policies)
Optional retry/timeout policy for the tools a server registers (Phase 26b / D-175). Without it, every tool uses the runtime default (30 s per-attempt deadline, 4 total attempts). policy: sets the per-server default; tool_policies: { <tool-name>: { … } } overrides individual tools (keyed by the server-side MCP tool name). Fields, all optional:
max_attempts— TOTAL attempts including the first (e.g.1= one attempt, no retry;4= the default). Projected toMaxRetries = max_attempts - 1.timeout_ms— per-attempt deadline in milliseconds.retry_on— error-class allowlist:transient/timeout/5xx/permanent.backoff_base_ms/backoff_mult/backoff_max_ms— exponential backoff between retries.
Per-field fall-through: a field you omit inherits the runtime default — setting only timeout_ms keeps the default 4 attempts. Example: a slow, reliably-throttled tool can be set to max_attempts: 1, timeout_ms: 90000 so it makes one long attempt instead of four 30 s ones. Validation: max_attempts >= 1, timeout_ms >= 0, retry_on in the allowlist, override keys non-empty + unique. See examples/harbor.yaml.
tools.a2a_peers
A2A southbound peers (Phase 29). Each entry needs url, trust_tier in [1, 5], latency_tier_ms >= 0, agent_card_ttl >= 0. HTTPS-only unless allow_insecure_loopback is set on a loopback host.
tools.entries
Per-tool catalog wiring (Phase 64a / D-090). Attaches approval gates, OAuth bindings, and / or a loading mode to a tool name without writing Go wiring code. See ToolEntryConfig godoc. Each entry carries:
approval— an approval-gate binding (optional).oauth— an OAuth binding (optional).loading_mode— when the tool appears in the planner's prompt-time catalog.""(default) oralwaysmeans every turn;deferredhides the tool by default and lets the LLM discover it via meta-tools. This boot value is the base the runtime loading-mode overrides layer on top of (D-281): the effective mode resolves per-toolagent_config.set_tool_exposureoverride > per-server override > this bootloading_mode> driver default, applied next-turn.
Validation: an entry must set at least one of approval, oauth, or loading_mode (an entry with no fields is a configuration typo).
tools.oauth_providers
Operator-configured OAuth providers (D-095). Each entry needs name, driver, client_id_env, client_secret_env. See ToolOAuthProviderConfig godoc. Two drivers ship:
oauth2— the generic OAuth2/PKCE Authorization Code flow. Needsauth_url,token_url, andredirect_url.tokenexchange(D-271) — pull-based external-credential provisioning. Instead of the interactive authorization-code flow, at token-miss time the runtime obtains a downstream tool credential from an external credential broker (a fleet orchestrator, an enterprise token vault, an STS) via an RFC-8693 token exchange keyed on the verified ctx identity triple, so one central grant serves N runtimes without N consents. Fields:token_url— the broker's token-exchange endpoint. Mandatory.auth_url/redirect_url— interactive-flow fields; not used by this driver (waived, not required).client_id_env/client_secret_env— name the env vars holding the runtime's OWN credential for authenticating to the broker.extra.audience— the RFC-8693 audience. Optional; defaults to the provider name. Superseded byaudience(the ceiling) when that is set.extra.cache_ttl_cap— max in-memory cache lifetime (a Go duration, default5m; values below30sare rejected at boot).audience— the boot-declared token-audience ceiling (D-300). When set it is the authority for the exchanged token's audience — decoupled from the caller-chosen provider name. Optional; empty preserves the legacy audience-from-name behaviour.scope_ceiling— the boot-declared scope ceiling (D-300). When set, the requestedscopesare INTERSECTED against it — a requested scope outside the ceiling is dropped. Optional; empty preserves the legacy pass-through.allow_private_token_url— DEV-ONLY, fail-closed opt-in (D-338). The credential-bearing exchange POST is hardened with a post-DNS dial backstop that refuses private-range / link-local resolved addresses (the DNS-rebinding defence). Set thistrueto permit a private-IPtoken_urlfor THIS provider — the containerized local-dev topology where the broker sits behind a private-IP TLS sidecar. It relaxes ONLY the private / link-local / ULA branch; the unspecified address (0.0.0.0/::) stays refused, loopback stays allowed, and the token-endpoint redirect refusal is untouched. Defaultfalse; boot-declared and config/file-only — never Protocol-writable. The globalHARBOR_DEV_ALLOW_PRIVATE_EXCHANGE=1boot env is the equivalent opt-in for all providers (effective = this flag OR that env); when it fires, every boot prints a[DEV-ONLY PRIVATE-IP TOKEN EXCHANGE — DO NOT USE IN PRODUCTION]stderr banner. Meaningful only for thetokenexchangedriver (rejected fail-loud onoauth2). Do NOT enable in production.
Credential-sink allow-list — allowed_downstream_hosts (D-300). Every provider a tools.mcp_servers[] connection binds MUST declare allowed_downstream_hosts — the boot-declared set of downstream connection hosts (host[:port]) the provider's credential may be injected into. The credential-plane invariant is that no admin-writable field determines where a credential is sent, so the sink set is boot-declared, config/file-only. A binding whose MCP connection host is not listed is refused at boot (and at runtime-add) fail-closed — never a silent unauthenticated dial. Host comparison folds the well-known default port (https://host ≡ https://host:443) and is case-insensitive. This is mandatory for a bindable provider (an empty allow-list on a bound provider is a boot error); it applies to BOTH the oauth2 and tokenexchange drivers.
Brokered tokens are TTL-cached in memory only and never persisted (TokenStore.Put is never called — the broker stays the single source of truth); a cached token is served until the EARLIER of the broker-advertised expiry and the cap. Broker failure fails the run loudly (ErrExchangeFailed, no silent interactive fallback); a consent_required refusal parks on the unified pause primitive. See examples/dev.yaml for a fully-commented block.
Each provider's OWN client credential resolves through the credential_source seam (D-285):
env(the default; omit the field) —client_id/client_secretare read fromclient_id_env/client_secret_envonce at boot, fail-loud when either env var is unset. Existing configs are byte-compatible.remote— the runtime pullsclient_id/client_secretfrom a coordinator-served endpoint at first need (not at boot), so a credential a coordinator mints AFTER the runtime booted reaches it with zero touch. The pulled credential is held in memory only, TTL-capped, single-flighted, and refetched on expiry — nothing is persisted. Withremotethe broker secret never enters the runtime's environment (defense-in-depth). Valid only for thetokenexchangedriver; declaringremotealongsideclient_id_env/client_secret_envis a validation error (one source per entry — §13). Requires aremote:block:url— the coordinator credential endpoint. Required, validated well-formed at boot. Must behttps— the fetch sends the runtime's service bearer token, so TLS is mandatory; the one carve-out is a loopback host (127.0.0.1/::1/localhost, the local fixture / dev case), where plaintexthttpis accepted. Redirects from the endpoint are refused (never followed) — a credential endpoint that redirects is treated as a fault.auth_token_env— names the env var holding the runtime's own service token, sent asAuthorization: Bearer. Required; read lazily at fetch time so a rotated token is picked up without restart.cache_ttl— optional; caps the in-memory serve horizon (a Go duration; default 5m). The effective horizon ismin(response expires_in, cache_ttl).credential_scope—tenantby default;deploymentexplicitly selects the legacy runtime-wide contract. Signed capability bindings always use tenant scope.timeout— optional; bounds a single fetch (default 30s).
The fetch contract (Harbor-defined, versioned) — a single authenticated GET:
httpGET <url> Authorization: Bearer <token from auth_token_env> Accept: application/json X-Harbor-Credential-Scope-Version: 1 X-Harbor-Credential-Tenant: <verified tenant>The coordinator responds with a strict JSON object:
json{ "format_version": 2, "tenant_id": "<verified tenant>", "client_id": "...", "client_secret": "...", "expires_in": 3600 }format_version(required) is the contract version — the runtime requires version2with the matching tenant echo (deploymentexplicitly uses the old header-free version1); unknown top-level fields are rejected (strict parse).expires_in(optional, seconds) drives the cache TTL. A fetch that is unreachable, non-200, or malformed fails the tool call loud with a typed sentinel and emits atool.provider_credential_fetch_failedaudit event — never a fallback to env, to an unauthenticated call, or to the interactive flow. A successful fetch emitstool.provider_credential_fetched(both events carry zero secret bytes). A push of the credential over the Protocol stays rejected as credential passthrough (D-271). Seeexamples/dev.yamlfor a fully-commentedremoteblock.
tools.oauth_credential_brokers
Boot-declared list of NAMED credential sinks (D-300) — the config home for the pinned token endpoints + allowed downstream hosts a Protocol-installed, zero-URL provider descriptor references by name. The credential-plane invariant keeps every sink-determining value boot-declared, config/file-only; this list is where those pinned sinks live. It is additive — the inline oauth_providers[].remote block stays valid — and restart-required (NOT a Protocol surface). Each entry:
name— operator-facing broker identifier. Required, unique within the list; referenced by name from a provider descriptor.token_url— the broker's RFC-8693 token-exchange endpoint (the pinned credential sink the org'sclient_id/client_secretare POSTed to). Required; must behttps(or a loopback host for the dev/fixture case). Boot-pinned — never wire-writable.allowed_downstream_hosts— the sink allow-list for the exchanged bearer. Required, non-empty (a bearer-minting broker must declare where its tokens may be injected — fail-closed).auth_token_env— names the env var holding the runtime's own broker credential (§7 rule 2 — never hardcoded). Required, non-empty.cache_ttl— optional; caps the in-memory serve horizon (Go duration).timeout— optional; bounds a single broker exchange (Go duration).
tools.oauth_credential_brokers[].signed_oauth_mcp_capability_authority
Optional boot-only D-401 trust-anchor block for the production agent_config.register_oauth_mcp_capability path. Omitting it leaves that broker unable to authorize signed capability registration. It is not a Protocol field and never exposes verifier material, an endpoint, credential source, host list, or the lifetime ceiling to an administrator.
enabled— requiredtruewhen the block is present; the explicit production opt-in.falseis rejected so a partially filled block cannot be mistaken for an enabled authority.issuer— required exact issuer for authority envelopes.- exactly one of
jwks_url(HTTPS except localhost fixtures) orjwks_file— required fixed public-key verifier source. It is fetched during boot; an opted-in but unavailable or invalid keyset fails boot loud. max_authority_lifetime— required positive Go duration. It caps signedexp - iat; an envelope exactly on the ceiling is accepted, one over it is refused. This is an operator-selected boot ceiling, not a hidden global default, and does not ride the wire.
tools.oauth_token_kek_env
Env-var name holding the 32-byte hex-encoded key-encryption key (KEK) used for AES-256-GCM token encryption at rest. Default: empty. Validation: required when oauth_providers is non-empty.
tools.allow_wire_oauth_descriptor
DEV-ONLY, fail-closed opt-in permitting agent_config.set_oauth_provider / add_mcp_connection to carry a FULL OAuth-provider binding over the wire (token_url / audience / scopes, still naming a boot-declared credential_broker) instead of only a boot-declared provider NAME — so a coordinator can stand up a new OAuth-fronted MCP server at runtime without a static oauth_providers[] block + redeploy. Default: false. With it unset (all of production) a wire descriptor carrying any credential-sink field is REJECTED, exactly as the zero-URL name-only binding rejects it today. When set, the relaxation stays bounded — allowed_downstream_hosts is DERIVED from the connected server's own URL (never a wire field) and the wire token_url dials face the identical token-exchange SSRF backstop. The same opt-in is also available globally via the HARBOR_ALLOW_WIRE_OAUTH_DESCRIPTOR boot env; the effective posture is this flag OR that env. Restart-required. Do NOT enable in production.
tools.allow_wire_injection
DEV-ONLY, fail-closed opt-in permitting agent_config.add_mcp_connection to carry a per-user credential-INJECTION mapping for a RECEIVER-STYLE MCP server over the wire (the injection object: which boot-declared oauth_providers[] broker + which target header / basic / _meta key), so a coordinator can ATTACH such a server at runtime and wire per-user credential delivery to it without a static mcp_servers[].injection block + redeploy. INDEPENDENT of allow_wire_oauth_descriptor — enable one without the other. Default: false. With it unset (all of production) a connection carrying any injection field is REJECTED. The mapping is NON-secret (the credential is still pulled per-user from the named broker at call time; nothing secret rides the wire); when set the relaxation stays bounded — the pulled credential's reachable host is DERIVED from the connection's own URL and validated against the named broker's boot-declared allowed_downstream_hosts (never a wire field), and every declared target key must be redaction-covered. The same opt-in is also available globally via the HARBOR_ALLOW_WIRE_INJECTION boot env; the effective posture is this flag OR that env. Restart-required. Do NOT enable in production.
tools.built_in
Opt-in list of Harbor-shipped built-in tools to register against the catalog at boot (Phase 83n / D-153). Default: empty (no built-ins registered). Validation: each name MUST be in internal/tools/builtin.KnownNames(). V1.1 ships:
clock.now— return current UTC time as RFC 3339 + epoch milliseconds.text.echo— echo input text verbatim.
tools.custom
Operator-declared custom tools whose Go shell is generated by harbor scaffold (Phase 83o / D-154). Each entry takes:
name— catalog tool name. Required. Unique withincustom; no collision withbuilt_in.description— one-line summary. Required.input— flat map offield: type. Type allowlist (V1.1):string/integer/number/boolean/[]string.output— same shape asinput.
The scaffolder materialises one tools/<name>.go stub + matching tools/<name>_test.go per entry. The runtime does NOT auto-discover these tools — the operator imports the generated tools/ package and calls RegisterTools from the agent's bootstrap path.
Example:
tools:
custom:
- name: get_weather
description: Look up current weather by city.
input:
city: string
units: string
output:
temp_c: number
summary: stringtools.granted_scopes
Operator-declared list of authorization scopes the dev runtime's planner-facing catalog view treats as granted (Phase 83m / D-156). 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. Tools with no AuthScopes are always visible.
Default: empty (no scopes granted — tools that declare AuthScopes are invisible). Validation: each entry MUST be a non-empty string; scope names are operator-defined per their tool sources (no allowlist). Restart-required.
Example:
tools:
granted_scopes:
- read:repo
- write:issues
- admintools.search_cache_dsn
SQLite DSN backing the Phase 107c / D-167 tool SearchCache (FTS5 + regex fallback). The discovery meta-tools (tool_search, tool_get) delegate to this index; an empty value selects the in-memory default, which is suitable for development (discovery state lives for the process lifetime).
Operators that want the cache to persist across reboots set a file: URI; the driver layers journal_mode(WAL) + busy_timeout(5000) automatically when the URI does not already declare them.
Default: empty (in-memory cache). Restart-required.
Example:
tools:
search_cache_dsn: file:./harbor-tools.dbtools.mcp_app_host
Deployment-wide MCP App (io.modelcontextprotocol/ui) host capability the runtime advertises to every MCP server during the initialize handshake. The host's rendering ability does not vary per server, so this is a single deployment-level block, not a per-server field.
display_modes lists the MCP App display modes this host can render, from the closed set inline / fullscreen / pip (the ext-apps McpUiDisplayMode values). A spec-conformant server reads these to tailor the app references it returns. Each entry must be in the closed set and unique; a typo fails at config load.
Omitting the block (or leaving display_modes empty) resolves to the inline-only baseline ([inline]) — the mode the Console renders out of the box. Set the full set once the deployment's Console serves the fullscreen-tab and side-by-side (pip) layouts.
Default: nil (resolves to [inline]). Restart-required.
Fresh render-admission consequence (D-412 amendment, Shipped v1.28). The durable-reopen MCP App admission surface mints stateless, integrity-protected render admissions sealed with the deployment's existing shared KEK authority — the tools.oauth_token_kek_env-backed AES-256-GCM sealer the OAuth token path already uses (D-095). Production and devstack use ONE constructor/composition path and ONE immutable shared sealer instance. The surface is enabled by tools.mcp_app_render_admission.enabled (default false; omitted stays compatible with existing deployments — there is no second authority field and no literal key). When the surface is enabled, an empty tools.oauth_token_kek_env name, a missing/unset/invalid KEK, or a sealer construction failure fails readiness loud, naming the problem, even when no OAuth provider or credential broker is configured. Restart and multi-replica deployments verify admissions against the shared KEK AND the same current provider/catalog generation — a per-process ephemeral key or a process-local discovery counter would make one replica's admission unverifiable on another, and a replica holding a different current catalog fails closed as a generation mismatch. The admission claim binds schema/time/(tenant,user,session)/effective-agent/server/resource/current provider/catalog generation and never carries raw arguments, secrets, provider output, callback names, or general capabilities; ordinary resource reads never mint an admission, and unavailable/expired admissions answer typed unavailable/expired with fresh checks on refresh. There is no persisted callback authority, no hot registry, and no transcript impersonation.
Example:
tools:
mcp_app_host:
display_modes: [inline, fullscreen, pip]
mcp_app_render_admission:
enabled: true # default false; when enabled, tools.oauth_token_kek_env
# must name a valid 32-byte hex KEK or readiness fails loudtools.mcp_app_render_admission.enabled
Opt-in toggle for the MCP App render-admission surface. Default: false (the surface is off; existing deployments are unaffected). Restart-required.
When true, the runtime mints stateless, integrity-protected render admissions sealed with the deployment's EXISTING shared tools.oauth_token_kek_env AES-256-GCM key-encryption authority — the same sealer the OAuth token path already uses. There is no second secret or config key.
Enabling the surface makes readiness fail LOUD — naming the problem — when tools.oauth_token_kek_env names an empty env var, when the KEK is missing, unset, or invalid, or when the sealer cannot be constructed, even when no OAuth provider or credential broker is configured.
Only the explicit current ui:// admission-requesting read, after a successful identity-scoped tool-context replay, mints an admission. Ordinary resource reads never mint; AppBridge secondary reads and failed / unavailable / foreign replays mint no authority.
tools.mcp_artifact_egress_max_bytes
Bounds ONE substituted artifact value on one outbound MCP tool call — the ceiling for egress substitution, where the runtime resolves an artifact id the model authored and places the resolved bytes into the outbound tool-call body, so a large document reaches a remote tool without transiting the model's context.
A value above the ceiling is REFUSED loud, never truncated. The artifact read path truncates truthfully — a model asking for a window gets a window and is told it is one — but a truncated document delivered to a remote ingester is a corruption rather than a bounded read, and a tool argument has nowhere to carry the truthful-truncation signal.
It is deliberately independent of the heavy-output threshold. Substituted bytes never enter a model's context, so this is a network and memory budget, not a token budget; tying it to a context-window constant would make one number answer two unrelated questions.
Sizing arithmetic. The transient footprint is ceiling x in-flight mapped calls. It is not multiplied by the retry budget: resolution happens once per dispatched call rather than once per attempt, so a default policy's four attempts do not quadruple it. Base64 inflates the wire form by roughly a third, so 8 MiB of content is around 11 MB on the wire — one reason the mapping is refused on stdio, where a frame that large is least appropriate.
Zero / omitted resolves to the default. A negative value is refused at config load rather than read as "unbounded".
Default: 8388608 (8 MiB). Restart-required.
Example:
tools:
mcp_artifact_egress_max_bytes: 4194304 # 4 MiBtools.mcp_servers[].artifact_byte_eligible (and .artifact_params)
The per-connection half of egress substitution. Both fields are http-only and both are refused on stdio — explicit, or an auto transport with only a command, which auto-selects stdio at connect and would otherwise leave an operator believing egress was on while it silently never fired.
artifact_byte_eligible declares that this connection may receive artifact bytes. It is the containment boundary for the feature: with it unset (every connection by default) an artifact_params mapping is refused at the door and no outbound call ever carries resolved content.
artifact_params maps this server's tool names to the parameter names on those tools which carry artifact bytes — keyed per tool name in the same shape tool_policies and tool_oauth_providers use (the server-side MCP tool name, not the <source>_<tool> Harbor-facing one). It requires artifact_byte_eligible on the same connection.
A parameter name ending in one trailing ? is optional. Harbor strips the marker before matching the server schema; an optional value that is missing, null, or an empty / whitespace-only string is treated as absent and is not substituted. This empty-string case accommodates MCP/schema adapters that materialize omitted optional string properties as "". Non-empty optional values and all required values retain strict type, non-empty-id, identity-scoped resolution, and byte-ceiling checks.
The mapping is canonicalized and bounded at config validation: at most 32 methods, at most 8 parameters per method, at most 128 UTF-8 bytes per method or parameter name, and at most 8 KiB of canonical JSON. Values above a boundary fail validation; Harbor never truncates the declaration.
Each mapped parameter is validated at attach against the server's own discovered inputSchema: it must be declared there, and declared string-typed. Harbor declaring "this parameter takes artifact bytes" against a server whose schema never declared such a parameter is the same defect shape as advertising a capability nothing services, so the mapping is checked rather than trusted. The check is point-in-time: a server that mutates its schema without a tools/list_changed notification can drift out from under a validated mapping, and the drift then surfaces as a server-side argument-validation error on the wire — a loud failure, never a silent wrong-shape send.
The substituted value is emitted as RFC 4648 §4 standard base64. Never a raw Go string: encoding/json rewrites every invalid-UTF-8 byte in a string to U+FFFD, which corrupts any binary document.
What this widens, and what it does not. The reachable artifact SET is unchanged — the dispatching run's own (tenant, user, session), enforced by the same run-scoped resolver the in-process arm uses. Only the RECIPIENT widens. Note the boundary this does not claim: both fields are admin-writable over the control plane, so a tenant admin can attach a server they control, map a parameter, declare it eligible, and receive a user's artifact bytes on the next run that names an id. That sits inside the trust boundary a shared runtime already accepts — a shared runtime trusts its co-tenant admins for runtime-added connections — and a deployment needing hard isolation runs one runtime per tenant. Artifact bytes are stored as authored (unredacted), so a byte-eligible connection can move a secret.
The compensating control is mandatory and not optional: every substitution emits mcp.artifact_egressed fail-closed, before the wire request — artifact id, server, tool, parameter, byte count and a sha256: digest, never the bytes. A substitution that could not be recorded does not happen.
Known limit. A byte-mapped parameter is not reachable from an MCP App's tool callback. That path is a browser-driven invocation with no run, so no resolver is seated and the call fails loud rather than being given a second, differently-scoped definition of what the feature can reach.
Default: unset (no connection is byte-eligible; every outbound call is byte-identical to a build without the feature). Restart-required.
Example:
tools:
mcp_servers:
- name: docstore
transport_mode: streamable_http
url: https://docs.internal.example.com/mcp
artifact_byte_eligible: true
artifact_params:
ingest_document: [content]
summarize: [document]tools.mcp_add_connection
Gates the admin-driven runtime add of a NEW MCP server connection over the Protocol (agent_config.add_mcp_connection). Adding a stdio MCP server spawns an operator-supplied command — an RCE surface — so the add path is fail-closed: a stdio add is permitted ONLY when its command argv[0] is listed exactly in stdio_allowlist. An empty / omitted block rejects every stdio add (the secure default). http adds are admin-scoped but are not gated by this allowlist.
The command is always launched argv-form (never via a shell). Secret auth material (bearer headers, OAuth tokens) supplied with an add flows to the live transport only and is NEVER persisted in the recorded config revision, the diff, or any emitted event.
Default: nil (every stdio add rejected). Restart-required.
Example:
tools:
mcp_add_connection:
stdio_allowlist:
- /usr/local/bin/harbor-mcptest-stdio
- /opt/mcp/servers/my-trusted-serverPlanner
planner.driver
Planner driver. Default: react (V1 reference). Validation: react (V1.1).
planner.max_steps
The planner step tranche and the runtime's continuation authority (HA-57, D-417/D-418): the number of planner steps a run may consume in one cycle before it is parked for continuation. When a tranche of planner steps is consumed without a terminal Finish, the run is parked through the unified pause primitive — a typed constraints_conflict pause carrying {cause: max_steps_exceeded, max_steps, steps_observed} — instead of terminating; it is never forced to finalise. An authorised RESUME continues the SAME run with a fresh tranche (the tranche counter resets; the cumulative trajectory is untouched), so long-running work spans repeated cycles as ONE run. A fresh process cannot resume a parked run — it answers the typed pauseresume.ErrRestartUnavailable (D-417). The planner-side per-tranche breaker (react.DefaultMaxSteps = 12) ends the cycle with the typed NoPath Finish carrying max_steps_exceeded; the runtime's outer ErrMaxStepsExceeded guard (default 64) remains the runaway backstop when tranche pausing is unavailable. Default: 0 → the driver default (12); it never means unbounded and never selects the legacy terminal-only loop. Validation: >= 0 (a negative value is rejected).
planner.extra_guidance
Operator-supplied domain-specific guidance injected into the planner's <additional_guidance> system-prompt section (Phase 83a / RFC §6.2). Default: empty.
The built-in ReAct prompt treats developer/operator guidance as instructions for scope, prohibitions, approvals, and output rules, not just tone. It directs the model not to perform conflicting actions, including via tools or delegation, and to help with permitted parts of the request. An ambiguous conflict between trusted instructions stops the affected action rather than inventing permission. With no extra guidance the agent still helps normally; this is not a deny-all or a domain allowlist inferred from the agent's name.
For multi-step work, the default asks for brief, factual progress updates as assistant prose alongside native tool calls in the same response. A prose-only response ends the run, so a standalone promise to keep working is not a progress mechanism. Updates cover the next action, meaningful evidence, or a blocker; they are not private reasoning or raw tool dumps. Simple requests do not need narration, and the final answer must stand on its own. To suppress updates for a silent or machine-readable consumer, say so in trusted guidance. An output schema alone governs the terminal answer, not intermediate prose.
planner.extra_guidance, operator-authored extra_system_blocks, and resolved tenant LLMOverrides.ExtraInstructions compose in <additional_guidance> (in that order, followed by runtime repair guidance). In contrast, the ordinary caller's RunOverrides.extra_instructions becomes escaped UserPersonalization, not trusted guidance. User preferences, tool results, retrieved documents, skills, memory, and prior assistant text must not override operator restrictions merely by claiming a higher role (D-387).
These are model-behavior defaults, not a security boundary or a guarantee of prompt-injection resistance. Developers still define the domain rules and configure runtime identity, permissions, tool exposure, approval policies, and governance. Test representative benign and adversarial requests against each chosen model. Prompt/golden tests verify assembly; scripted-provider tests verify transport behavior, not whether a live model obeys the policy.
An explicit custom system prompt, durable base replacement, one-shot SystemPromptOverride, or custom prompt builder retains its existing replacement semantics. It does not automatically inherit these built-in sections. Additive guidance still survives the default builder's base replacement; authors of custom prompts/builders must supply equivalent behavioral instructions themselves. No event schema or client rendering changes are implied: clients must consume assistant-content updates and keep provider reasoning separate. A provider that only emits a different status channel needs an adapter; never display private reasoning as a substitute for a missing progress update.
planner.reasoning_replay
Whether the ReAct planner re-injects a prior step's captured provider reasoning trace (Phase 83e / D-148). Default: never. Validation: never / text (no provider_native in V1.1).
planner.max_tool_examples_per_tool
Cap on curated examples rendered per tool in the planner's <available_tools> section (Phase 83b / D-144). Default: 0 → driver default of 3. Validation: >= 0.
planner.parallel_tool_calls
Toggles native parallel tool-call emission (Phase 107d / D-169). When the LLM returns N>1 tool-calls in one response, true makes the React planner emit a native CallParallel and the dev ToolExecutor dispatch the branches concurrently; false selects the Phase 107c serialization fallback (one CallTool per step via RunContext.PendingToolCalls). Pointer-bool: an omitted key resolves to true (the native-parallel default). Validation: none (both states are correct).
planner.absolute_max_spawn_depth
Caps the ParentTaskID-chain depth of planner-spawned background tasks (Phase 107e / D-170). When a planner emits _spawn_task, the dev ToolExecutor reads the parent chain depth and rejects loudly — an error observation the planner re-plans against, never a silent drop — any spawn whose child would exceed this depth, so a background sub-agent that itself emits _spawn_task cannot recurse without bound. The cap bounds depth, not breadth. Default: 0 → dev-runtime default of 4. Validation: >= 0.
planner.max_batch_spawns
Caps how many task spawns a single Batch decision may carry — the BREADTH ceiling (Phase 186 / D-323). A Batch is the shape a projector builds when a model batches _spawn_task calls alongside other calls in one response; the dev ToolExecutor rejects the WHOLE batch (no tool branch dispatches, no spawn registers) — never a silent truncation to the first N — when its spawn count exceeds this cap. Distinct from absolute_max_spawn_depth, which bounds spawn-chain DEPTH, not the breadth of one response's spawns. Default: 0 → dev-runtime default of 5 (conservative, operator-revisable). Validation: >= 0.
planner.skills_context_max
Cap on skill bodies the dev run loop fetches from SkillStore.Search and hands the planner via RunContext.SkillsContext (Phase 83f / D-149). Default: 0 → dev-runtime default of 5. Validation: >= 0.
planner.planning_hints.constraints
Free-form text rendered verbatim into the planner's <planning_constraints> system-prompt section (Phase 83c). Default: empty.
planner.planning_hints.preferred_tools
Tool names the planner should prefer when multiple satisfy the same goal. Default: empty.
planner.extra
Per-driver opaque extras map. Reserved for future drivers' per-flow knobs. Default: empty. V1.1 react driver ignores it.
Multimodal
multimodal.disposition
Per-agent attachment disposition policy (Phase 84b / D-189): a map from a MIME key — an exact media type (application/pdf), a family wildcard (image/*), or the literal * (the agent-wide default) — to a disposition value: ref (emit an ArtifactStub + Fetch.Tool hint; the developer processes the bytes via a tool), inline (DataURL inline; image/* only at V1.1), provider_native (opt-in provider-side understanding — the LLM driver uploads the attachment to the provider's file surface and the model sees it via an opaque file_id (Phase 84c / D-190); a provider without support for the modality degrades to ref with a logged notice), or tool:<name> (force the named catalog tool via Fetch.Tool). Precedence: per-attachment Protocol disposition hint > this map > the runtime default (image/* → inline, everything else → ref — byte-for-byte the pre-84b behaviour). Default: empty (runtime default applies). Validation: keys/values must satisfy the grammar above. Restart-required.
Virtual agents
virtual_agents.owner
The configured top-level agent that owns the optional virtual-agent profiles (virtual_agents: block). Required when the block is present; must equal the runtime's configured default agent id (checked at the run-loop boundary). Virtual agents are NOT registered agents — they never appear in agents.list, are never control.start targets, and never join the isolation tuple. Task.AgentID stays the top-level agent; a profile is a per-run CONFIGURATION projection only.
virtual_agents.profiles
The ordered declaration list of virtual-agent profiles owned by virtual_agents.owner. Each profile carries a key (the selector the planner's _spawn_task virtual_agent argument uses), an optional label, an optional parent (defaults to the owner; must equal it), and a bounded narrow-only overlay: llm (model / temperature / max_tokens / reasoning_effort), skills (intersection), tools (disabled_tools / paused_servers — exclusions only), limits (max_steps / token_budget), and instructions (trusted additive specialist guidance, ≤ 16 KiB). The overlay can never widen resources or guardrails, attach capabilities, own providers / hooks / memory, recurse into another profile, or target A2A — those dimensions have no fields, and strict YAML decoding rejects an unknown key at boot. The effective child run is the parent's frozen config plus the selected profile's overlay, pinned by profile hash + config revision/digest so a restart reproduces the exact profile. Omission (no block) is byte-compatible. Restart-required.
virtual_agents.max_profiles
The maximum number of virtual-agent profiles the owning agent may expose. The value is carried with the virtual_agents: section and enforced at the run-loop boundary. Restart-required.
Tenant-scoped broker credential pulls
Remote tool tokenexchange credentials default to the verified execution tenant. Signed capability providers instead bind the same selection to their authenticated, immutable connection tenant and validate the caller before resolving any credential. The runtime service bearer authenticates the runtime; the tenant selector grants no authority by itself. The coordinator must validate that the authenticated runtime is mapped to that exact tenant before returning its credential.
The request remains an authenticated GET to the boot-pinned URL and adds X-Harbor-Credential-Scope-Version: 1 and X-Harbor-Credential-Tenant: <verified tenant>. A successful response is a strict JSON object with format_version: 2, tenant_id echoing the selected tenant, client_id, client_secret, and optional expires_in seconds. Missing or different tenants, legacy responses, unknown fields, malformed JSON and permanent HTTP refusals fail closed without automatic tool retries. HTTP 408, 429 and server outages remain transient. The runtime never retries a scoped request as legacy.
Upgrade the coordinator endpoint before adopting the runtime default. For an intentionally deployment-wide legacy tool broker, explicitly set credential_scope: deployment on tools.oauth_credential_brokers[] or the tools.oauth_providers[].remote block. This uses the original header-free format_version: 1 contract. A legacy coordinator must reject ambiguous runtime mappings rather than select an arbitrary tenant. Signed capability providers always use scoped pulls regardless of this compatibility setting. Env/static credentials and inference-plane boot credentials retain their existing behavior.
Cache and single-flight entries are isolated by tenant and runtime bearer generation. They are memory-only, expiry/TTL bounded, limited to 256 entries per source, and invalidated for the caller tenant on provider revocation. A revoked in-flight fetch cannot repopulate the cache. Cancellation of one waiter does not cancel its peers; HTTP work remains timeout bounded. Providers are reconstructed on recovery, so no broker credential survives restart. Coordinator revocation is observed on the next uncached pull, within the configured TTL; this is not an instantaneous push channel.
runtime.info.capabilities advertises tenant_scoped_broker_credentials_v1 when the agent-config surface is wired. Consumers require this capability before relying on shared-runtime tenant isolation; a build-version guess is not equivalent evidence.
Broker-bound App operation requests
The candidate App operation v1 contract extends existing Protocol requests, not provider configuration. It requires a signed capability broker and existing fresh render admission for callbacks. Older brokers or credential providers fail closed. Keep external host activation off until its policy, document binding and real-service acceptance are complete; retire generic provider bindings/caches during that activation.