config

package
v2.9.0 Latest Latest
Warning

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

Go to latest
Published: Sep 10, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

Documentation

Overview

Package config defines the on-disk schema for `.agents/config.json` and the rules for discovering, parsing, and merging it with built-in defaults.

A minimal config.json only needs to set what the consumer wants to override; all other fields fall back to DefaultConfig().

Index

Constants

View Source
const (
	AlertTemplateGeneric           = "generic"
	AlertTemplateSwitchboard       = "switchboard"
	AlertTemplateSlack             = "slack"
	AlertTemplateDiscord           = "discord"
	AlertTemplatePagerDutyEventsV2 = "pagerduty_events_v2"
)

Alert template kinds, all of them implemented in pkg/tools/alert.

"generic" is a raw JSON pass-through for a bespoke receiver. "slack", "discord" and "pagerduty_events_v2" are service formats: one platform each, posted straight at that platform's webhook.

"switchboard" is the odd one out — a destination CLASS, not a service format. The go-steer/switchboard chat gateway bridges Slack and Google Chat today and more later, translating one markdown body per platform, so one template covers all of them and the message lands in a thread a human can reply in. The "slack" template is the different thing: Block Kit fired into a channel, fire-and-forget, with nothing coming back.

View Source
const (
	ThemeAuto  = "auto"
	ThemeDark  = "dark"
	ThemeLight = "light"
)

Theme constants for UIConfig.Theme. Reserved buckets; any other lowercase identifier accepted by validateUI is passed through to core-tui as a named-theme lookup.

View Source
const (
	PromptCacheTTL5m = "5m"
	PromptCacheTTL1h = "1h"
)

Prompt-cache breakpoint TTLs accepted by PromptCacheConfig.TTL.

View Source
const (
	PermissionModeAsk         = "ask"
	PermissionModeAllow       = "allow"
	PermissionModeYolo        = "yolo"
	PermissionModePlan        = "plan"
	PermissionModeAcceptEdits = "acceptEdits"
)

Permission modes.

View Source
const (
	ProviderGemini          = "gemini"
	ProviderVertex          = "vertex"
	ProviderAnthropic       = "anthropic"
	ProviderAnthropicVertex = "anthropic-vertex"
	ProviderEcho            = "echo"
	ProviderScripted        = "scripted"
)

Provider names recognized by the resolver.

View Source
const (
	SmallTierParentWarn   = "warn"
	SmallTierParentRefuse = "refuse"
	SmallTierParentAllow  = "allow"
)

Small-tier-parent mode constants. See SafetyConfig.SmallTierParent for behavior. Exported so consumers (CLI, library) can reference the canonical strings.

View Source
const (
	WatchdogOff      = "off"
	WatchdogWarn     = "warn"
	WatchdogFeedback = "feedback"
	WatchdogEnforce  = "enforce"
)

Watchdog mode constants. See SafetyConfig.Watchdog for behavior. Exported so consumers (CLI, library, recipes) can reference the canonical strings rather than re-spelling them. Listed weakest to strongest; each mode includes the behavior of the one before it.

View Source
const (
	CheckpointModeModel    = "model"
	CheckpointModeOperator = "operator"
	CheckpointModeOff      = "off"
)

Checkpoint mode constants. See CheckpointConfig.Mode for behavior. Listed strongest to weakest; each mode is a subset of the one before it.

View Source
const (
	BashSearchGateEnforce = "enforce"
	BashSearchGateWarn    = "warn"
	BashSearchGateAllow   = "allow"
)

Bash search-gate mode constants. See SafetyConfig.BashSearchGate.

View Source
const (
	PlanModeOff      = "off"
	PlanModeAdvisory = "advisory"
	PlanModeRequired = "required"
)

Plan-mode constants. See PermissionsConfig.PlanMode for behavior. Listed weakest to strongest; each includes the one before it.

View Source
const AgentsDirName = ".agents"

AgentsDirName is the project-local directory we discover, analogous to `.git`. It contains config.json, mcp.json, skills/, sessions/, etc.

View Source
const ConfigFileName = "config.json"

ConfigFileName is the per-project config file inside AgentsDirName.

View Source
const (
	MultiSessionAuthKindBearerTable = "bearer_table"
)

Recognized values for MultiSessionAuthConfig.Kind. Only the bearer table is implemented in v2.4; other kinds are reserved and return a validation error.

View Source
const SchemaVersion = 1

SchemaVersion is the current major version of the on-disk config format. Bump when making a breaking change; older versions are rejected at load time with a clear error suggesting the upgrade path.

Variables

AlertTemplates is every template validateAlerts accepts, in the order error messages list them. pkg/tools/alert ranges over it to prove its renderer knows each one: a template the config layer admits and the renderer does not is a daemon that boots and then fails at the moment the alert is fired.

View Source
var ErrNoChange = errors.New("config: no change")

ErrNoChange, returned by a Mutate fn, means "the config is already in the desired state — skip the save". Mutate then returns nil. Lets idempotent helpers avoid rewriting an unchanged file.

Functions

func AppendBuiltinAllowExtra added in v2.8.0

func AppendBuiltinAllowExtra(agentsDir, name string) error

AppendBuiltinAllowExtra adds name to permissions.builtin_allow_extras. Idempotent — re-enabling a bundle that's already on is a no-op. Validation against the bundle catalog (permissions.KnownBundles) happens in the caller's UX before this is called, so an invalid name never reaches disk.

func AppendPathScope added in v2.8.0

func AppendPathScope(agentsDir, pattern string) error

AppendPathScope adds pattern to path_scope.allow and rewrites the file atomically. If the file doesn't exist yet it is created with defaults so the addition has somewhere to live. Idempotent.

func AppendPermissionsAllow added in v2.8.0

func AppendPermissionsAllow(agentsDir string, patterns []string) error

AppendPermissionsAllow adds one or more patterns to permissions.allow. Idempotent — duplicate patterns are skipped silently so /permissions can be re-run without growing the file.

func AppendPermissionsDeny added in v2.8.0

func AppendPermissionsDeny(agentsDir string, patterns []string) error

AppendPermissionsDeny mirrors AppendPermissionsAllow for the deny list. Idempotent.

func Find

func Find(startDir string) (string, bool, error)

Find walks up from startDir looking for a directory named .agents/. On match it returns the absolute path of that directory and ok=true. When no match is found up to the filesystem root, ok=false (not an error).

func Mutate added in v2.8.0

func Mutate(agentsDir string, fn func(*Config) error) error

Mutate runs fn against the loaded config for agentsDir and atomically persists the result, all under the package-wide serialization lock (#482). fn returning ErrNoChange skips the save and reports success; any other error aborts without saving.

func ParseAlertRateLimit added in v2.9.0

func ParseAlertRateLimit(s string) (count int, window time.Duration, err error)

ParseAlertRateLimit parses an alerts.rate_limit_per_target spec into a (count, window) pair meaning "count alerts per window". Accepted forms:

"30s"      → 1 per 30s   (a bare Go duration is shorthand for "1/<dur>")
"1/30s"    → 1 per 30s
"5/min"    → 5 per minute
"100/hour" → 100 per hour

The window accepts Go duration syntax ("30s", "1m30s", "2h") plus the friendly bare units "sec"/"min"/"hour"/"day" (and "s"/"m"/"h"/"d") that time.ParseDuration rejects on their own. An empty string is an error; callers treat "" as "no limit" before calling.

func PersistModelChoice added in v2.8.0

func PersistModelChoice(agentsDir, modelID string) error

PersistModelChoice writes the new model name so /model survives across runs. Caller is responsible for first invoking the in-memory rebuild — this is purely the disk side.

func PersistMouseChoice added in v2.9.0

func PersistMouseChoice(agentsDir string, on bool) error

PersistMouseChoice writes the /mouse toggle so it survives across runs (core-agent #859, core-tui #287).

Always writes an explicit value, never clears the field back to nil. UI.Mouse is a tristate where nil means "no opinion, use the default" — and the default is on, so clearing it on a toggle-off would persist the opposite of what the operator just asked for. An operator who wants the nil back edits the file.

This one matters more than the other two toggles: while capture is on the terminal never sees click-drag, so mouse-on removes native text selection. Without this, an operator who wanted selection back re-typed /mouse every single launch.

func PersistThemeChoice added in v2.8.0

func PersistThemeChoice(agentsDir, themeName string) error

PersistThemeChoice writes the picker's selection so /theme survives across runs. Validates before save so a bad name surfaces as a picker error instead of silently corrupting the file.

func Save

func Save(path string, cfg *Config) error

Save writes cfg to path atomically (temp file in the same directory followed by rename). The output is human-edit-friendly: stable key order, two-space indentation, trailing newline.

Save is safe for round-tripping a config the current binary only partially understands:

  • Unknown top-level keys already on disk (e.g. a section written by a newer binary) are preserved rather than dropped. Load → mutate → Save by an older binary no longer deletes a newer binary's fields.
  • The original file mode is retained; a fresh file is created 0600 (the schema can hold API keys). Save never re-chmods to a wider mode the way the previous hardcoded 0644 did.
  • Default-valued sections that were not already present on disk are NOT materialized. Writing a partial config keeps it partial, so a future bump to a substrate default (e.g. the default model) still reaches operators who never pinned that value.

Save does not validate; callers should run cfg.Validate() first.

Types

type AgentConfig

type AgentConfig struct {
	MaxSteps int `json:"max_steps,omitempty"`

	// AppendSystemPrompt is layer-5 operator text appended to the
	// assembled system prompt (agent.WithExtraInstruction) — the
	// documented, encouraged customization path: the harness
	// contract and mode overlay stay intact underneath. Mirrored by
	// the --append-system-prompt flag (flag beats config). #459.
	AppendSystemPrompt string `json:"append_system_prompt,omitempty"`

	// SystemPromptFile names a file whose contents REPLACE the
	// assembled system prompt wholesale (agent.WithInstruction).
	// You lose the harness contract — compaction summaries arrive
	// unexplained and tool-use degradation is on you; prefer
	// AppendSystemPrompt. Mirrored by --system-prompt-file (flag
	// beats config). #459.
	SystemPromptFile string `json:"system_prompt_file,omitempty"`

	// MaxTurnCostUSD caps a single conversation turn's cumulative
	// spend. When the post-turn hook detects spend ≥ this value, the
	// agent emits a structured turn-error (kind=cost_ceiling) and
	// refuses new turns until the operator clears the flag via
	// Agent.ResetCostCeiling. Pointer so unset is distinguishable
	// from the deliberate 0 (which would mean "no budget — refuse
	// every turn", which we treat as "disabled"). 0 or negative ==
	// disabled. Defense against the read-file-loop class of bug
	// (#144) within a single turn.
	MaxTurnCostUSD *float64 `json:"max_turn_cost_usd,omitempty"`

	// MaxSessionCostUSD caps the session's cumulative spend across
	// all turns (parent + subtask). Tripped → same behavior as
	// MaxTurnCostUSD. Useful for long-running autonomous deploys
	// where individual turns are reasonable but the session adds up.
	MaxSessionCostUSD *float64 `json:"max_session_cost_usd,omitempty"`

	// AutoContinue controls continuing restart-interrupted turns
	// automatically (#539, #559, docs/auto-continue-design.md). Nil/absent
	// leaves the feature at its precondition-gated default (on for a
	// multi-session or --no-repl daemon with a durable eventlog, off
	// elsewhere — see AutoContinueConfig.Enabled). A session interrupted
	// mid-turn by a daemon restart resumes with intact history; whether it
	// then finishes the turn or waits for the next message is what this
	// controls. Applies to lazily-resumed multi-session agents; autonomous
	// runs have their own checkpoint/resume machinery.
	AutoContinue *AutoContinueConfig `json:"auto_continue,omitempty"`

	// SessionTitle controls whether each session gets a short label
	// derived from its first prompt, so a session picker lists work
	// instead of IDs (#808). Nil/absent = on: the cost is one
	// cheap-tier call per session, and a picker full of UUIDs is not
	// a picker. Set false to turn the call off — the session keeps
	// whatever an operator names it by hand, so this disables
	// inference, not titles.
	//
	// Pointer for the usual reason: absent has to be distinguishable
	// from a deliberate false.
	SessionTitle *bool `json:"session_title,omitempty"`

	// DisplayName overrides the brand line at the top of the TUI. By
	// default the TUI shows the AppName (e.g. "core-agent"); set this
	// to give the agent a human-friendly identity ("Triage Bot",
	// "Code Reviewer", etc.). Empty falls back to AppName.
	DisplayName string `json:"display_name,omitempty"`

	// Description is a one-line summary of what this agent does.
	// Used in two places: (1) ADK's llmagent.Config.Description, which
	// becomes part of the system prompt ("you are an agent named X,
	// description: ..."), and (2) the /.well-known/agent-card.json
	// `description` field if the card endpoint is enabled. Set once,
	// fanned out to both. Empty = no description in the system prompt
	// and the card endpoint stays off unless --agent-card-description
	// overrides.
	Description string `json:"description,omitempty"`
}

AgentConfig tunes runtime agent behavior.

type AlertAuth added in v2.9.0

type AlertAuth struct {
	BearerEnv    string `json:"bearer_env,omitempty"`
	BasicEnvUser string `json:"basic_env_user,omitempty"`
	BasicEnvPass string `json:"basic_env_pass,omitempty"`
}

AlertAuth carries per-target auth, as NAMES of env vars (never the secret itself) so credentials stay out of config files and audit rows. Set either BearerEnv, or both BasicEnvUser+BasicEnvPass — not both schemes.

type AlertTarget added in v2.9.0

type AlertTarget struct {
	// Name is the identifier the agent uses and the suffix in
	// permissions.allow patterns ("alert:<name>"). [A-Za-z0-9_-]{1,64}.
	Name string `json:"name"`
	// Kind is the transport. "" or "webhook" today; the field exists
	// so future kinds (smtp, polling APIs) can be added without a
	// schema break.
	Kind string `json:"kind,omitempty"`
	// URL or URLEnv — exactly one. Prefer URLEnv for any destination
	// whose URL embeds a secret (Slack Incoming Webhooks put the token
	// in the path), so the secret stays in a K8s Secret / env var.
	URL    string `json:"url,omitempty"`
	URLEnv string `json:"url_env,omitempty"`
	// Template selects the wire format (see AlertTemplate* constants).
	Template string `json:"template"`
	// Conversation is the destination conversation, for templates that
	// address one: required by "switchboard", rejected by every other
	// template so a field that would silently do nothing cannot be set.
	//
	// It is the gateway's platform-specific conversation key — for Slack
	// a channel ID ("C0123") or "channel:thread_ts" to post into an
	// existing thread. Routing config, like the URL, so it lives beside
	// the URL rather than in the model's arguments: the same reason the
	// tool has no arbitrary-URL parameter, one level in. A literal (no
	// *_env sibling) because a conversation key is not a secret — unlike
	// a Slack Incoming Webhook URL, which carries its token in the path.
	Conversation string `json:"conversation,omitempty"`
	// Auth is optional per-target auth material, resolved from env at
	// call time. Absent = no auth headers (the Slack Incoming Webhook
	// case — the token is already in the URL).
	Auth *AlertAuth `json:"auth,omitempty"`
	// Description is a human/LLM-facing hint surfaced in the tool
	// schema so the model knows what each target is for.
	Description string `json:"description,omitempty"`
}

AlertTarget is one named escalation destination.

type AlertsConfig added in v2.9.0

type AlertsConfig struct {
	// Targets is the operator-declared allow-list of escalation
	// destinations. The agent fires a target by name; there is no
	// arbitrary-URL parameter, so a hallucinated target name is
	// rejected rather than dialed (SSRF-safe by construction).
	Targets []AlertTarget `json:"targets,omitempty"`
	// RateLimitPerTarget bounds how often a SINGLE target can fire,
	// to catch pathological alert loops (not to enforce operational
	// cadence — distinct targets are independent). Empty = no limit.
	// Format: a Go duration ("30s" = 1 per 30s) or "N/window"
	// ("5/min", "100/hour"). See ParseAlertRateLimit.
	RateLimitPerTarget string `json:"rate_limit_per_target,omitempty"`
}

AlertsConfig declares the named webhook targets the native `alert` tool (pkg/tools/alert) can fire, plus an optional per-target rate limit. When Targets is empty the tool is not registered at all — the model never sees an `alert` in its schema (the fetch_url pattern).

The design rationale (SSRF-safe by construction, distroless-native escalation, audit through the eventlog) lives in docs/alert-tool-design.md.

type AnthropicConfig

type AnthropicConfig struct {
	APIKey      string             `json:"api_key,omitempty"`
	Vertex      *VertexConfig      `json:"vertex,omitempty"`
	PromptCache *PromptCacheConfig `json:"prompt_cache,omitempty"`
}

AnthropicConfig holds Claude-specific settings for the anthropic provider family. APIKey is used by the first-party "anthropic" provider (api.anthropic.com); Vertex is used by "anthropic-vertex" (Claude served via Google Vertex AI).

PromptCache toggles Anthropic prompt caching (cache_control breakpoints on the stable prefix and the conversation tail). When nil or Enabled != false, caching is ON. Unlike Vertex context caching this needs no separate resource — the breakpoints ride the ordinary Messages request — so it applies to both backends in the family. See docs/anthropic-prompt-caching-design.md.

type AttachConfig

type AttachConfig struct {
	// Server-side: where the attach listener binds. Set at most one.
	// Non-loopback listen addresses (e.g. "0.0.0.0:7777") refuse to
	// start without authentication (TokenEnv bearer token, mTLS, or
	// enforced multi-session auth) — see pkg/attach.NewServer (#376).
	Listen     string `json:"listen,omitempty"`      // e.g. "127.0.0.1:7777"
	UnixSocket string `json:"unix_socket,omitempty"` // e.g. "/var/run/core-agent.sock"

	// TLS material. TLSCert + TLSKey enable HTTPS; ClientCA additionally
	// enables mTLS (client cert required). Paths only — keys live on disk.
	TLSCert  string `json:"tls_cert,omitempty"`
	TLSKey   string `json:"tls_key,omitempty"`
	ClientCA string `json:"client_ca,omitempty"`

	// TokenEnv is the name of the env var that holds the bearer token
	// clients must present. The secret itself never lives in config.
	TokenEnv string `json:"token_env,omitempty"`

	// ReadOnly disables POST /inject and /wake; read endpoints stay open.
	ReadOnly bool `json:"readonly,omitempty"`

	// PeerHub turns on the peer-registration endpoints on this listener.
	PeerHub bool `json:"peer_hub,omitempty"`

	// PeerStateFile makes the hub's peer registry durable across
	// restarts (#595): registrations are snapshotted here on every
	// change and reloaded at startup, so a hub restart doesn't blank
	// the fleet until every peer's next heartbeat fails. Ignored
	// unless PeerHub is set. Put it on a volume that outlives the
	// pod; the file holds registration IDs, so it is written 0600 and
	// wants a directory to match. Empty keeps the registry in-memory.
	PeerStateFile string `json:"peer_state_file,omitempty"`

	// Peer-side: this agent registers with a remote hub.
	RegisterTo       string `json:"register_to,omitempty"`       // hub URL
	RegisterEndpoint string `json:"register_endpoint,omitempty"` // expanded via os.ExpandEnv
	RegisterName     string `json:"register_name,omitempty"`     // defaults to hostname when empty

	// MultiSession enables per-caller authentication + per-session
	// ACL on the attach listener. Zero value (disabled) keeps the
	// daemon in single-user mode — the listener treats every request
	// as the same anonymous Caller and TokenEnv / BearerToken still
	// gate at the transport layer as today. See
	// docs/multi-session-design.md.
	MultiSession MultiSessionConfig `json:"multi_session,omitempty"`

	// CostRateLimit tunes the per-caller token bucket that bounds the
	// COST-BEARING attach endpoints (the five slash ops, POST
	// /sessions, pricing/refresh) — see #463. Omitted (nil) keeps the
	// library defaults: burst 5, 10/minute per caller. Reads, /events
	// streams, /inject, and /wake are never limited.
	CostRateLimit *CostRateLimitConfig `json:"cost_rate_limit,omitempty"`

	// ShutdownTimeout caps how long the attach listener's graceful
	// HTTP shutdown waits for in-flight requests once SSE streams are
	// hung up, as a time.Duration string (e.g. "5s"). Omitted/empty
	// keeps the library default (5s). Counts toward the daemon's
	// total teardown budget — keep it comfortably under the
	// supervisor's kill timeout (K8s terminationGracePeriodSeconds,
	// default 30s). See #538.
	ShutdownTimeout string `json:"shutdown_timeout,omitempty"`
}

AttachConfig holds defaults for the attach-mode listener and the peer-registration client. Every field is also exposed as a CLI flag (--attach-*); the CLI flag wins when set, otherwise the config value supplies the default. Fields holding URLs / addresses pass through os.ExpandEnv so per-pod values like "https://${POD_IP}:7777" can live in a shared ConfigMap.

BearerToken is intentionally NOT a field here. The CLI flag form is --attach-token=ENVVAR (the name of the env var holding the secret), not the secret itself, and that env-var indirection should not be duplicated in a config file. Configure the env var via your secret manager (K8s Secret, sealed-secret, etc.) and set TokenEnv if you want to nail the env-var name down per-deployment.

type AutoContinueConfig added in v2.8.0

type AutoContinueConfig struct {
	// Enabled turns the feature on. A *bool tristate (#559): nil (unset,
	// or a nil parent pointer) means "on by default when the feature can
	// apply" — i.e. a multi-session daemon or a --no-repl single-user
	// daemon with a durable eventlog; interactive REPL/TUI and in-process
	// library use are excluded by that precondition, so they never
	// auto-continue by default. An explicit false is a hard opt-out; an
	// explicit true forces it on (and, in a mode where it cannot apply,
	// warns and is ignored). The precondition gate lives with the CLI
	// wiring (resolveAutoContinue in cmd/core-agent) since config alone
	// cannot see the run mode or eventlog presence.
	Enabled *bool `json:"enabled,omitempty"`

	// Freshness bounds how old an interruption may be and still get
	// auto-continued, as a time.Duration string. Omitted/empty
	// defaults to "1h". Explicit "0s" disables the window (always
	// continue). Staler interruptions wait for the next real
	// message.
	Freshness string `json:"freshness,omitempty"`

	// MaxPerBoot caps how many sessions the boot-time scan will
	// continue in one daemon start, oldest interruption first.
	// Omitted/0 defaults to 10. (The scan itself lands with the
	// design doc's PR 2; the lazy-resume trigger ignores this cap —
	// a touched session is one the operator is already paying
	// attention to.)
	MaxPerBoot int `json:"max_per_boot,omitempty"`

	// Retry controls the in-lifetime retry driver: a background pass
	// that re-attempts a stranded continuation without waiting for a
	// reboot or a human message (#575 defect B), so a transient
	// continuation failure self-heals on a long-lived daemon. It stays
	// bounded by the crash-loop breaker + per-session cumulative cap —
	// a daemon-killing turn kills the driver too, so only survivable
	// failures are ever re-fired.
	//
	// A *bool so the default (nil) can be "on wherever auto-continue is
	// enabled": with the driver on, the promise shifts from "one
	// automatic retry, then wait for a human" to "self-heal up to the
	// cap, minutes apart, unattended". Set an explicit false to keep the
	// one-shot-then-wait contract.
	Retry *bool `json:"retry,omitempty"`

	// RetryInterval is how often the retry driver re-runs a guarded
	// pass, as a time.Duration string. Omitted/empty defaults to "5m".
	// The per-session single-retry guard (breakerWindow, 10m) is the
	// effective per-session cadence, so values below it simply no-op on
	// recently-attempted sessions. Must parse and be > 0.
	RetryInterval string `json:"retry_interval,omitempty"`
}

AutoContinueConfig tunes opt-in continuation of restart-interrupted turns (#539). See docs/auto-continue-design.md for the full semantics, including the crash-loop breaker that ships with the boot scan.

func (*AutoContinueConfig) RetryEnabled added in v2.8.0

func (c *AutoContinueConfig) RetryEnabled() bool

RetryEnabled reports whether the in-lifetime retry driver should run. Nil (unset) defaults to on — the driver is enabled wherever auto-continue itself is enabled; an explicit false opts out. Callers must have already checked Enabled.

type BuiltinToolsConfig added in v2.9.0

type BuiltinToolsConfig struct {
	// WebSearch toggles server-side web search grounding.
	// Provider defaults: Gemini on, Anthropic off.
	WebSearch *bool `json:"web_search,omitempty"`

	// URLContext toggles fetching and grounding on URLs the model
	// decides to visit. Gemini only; default on.
	URLContext *bool `json:"url_context,omitempty"`

	// CodeExecution toggles sandboxed Python execution on the
	// provider's servers. Gemini only; default off.
	CodeExecution *bool `json:"code_execution,omitempty"`
}

BuiltinToolsConfig gates the provider's server-side built-in tools — the ones the model invokes inside the provider's own infrastructure and whose results come back folded into the response. core-agent never sees a tool call for them, so nothing in the function-tool layer can reach them: not `tools.disable`, not a subagent's `tools` allowlist, not `--no-builtin-tools` (which is about core-agent's own suite). This block is the only lever.

Every field is a tri-state *bool: absent leaves the provider default alone, and only an explicit `true` / `false` moves one. That matters because the defaults are NOT symmetric across providers — Gemini ships web search and URL context on, Anthropic ships web search off — so a deployment that switches providers silently switches the agent's internet reachability with them. Stating the posture here makes it hold across that switch.

The names are provider-neutral, not provider-native:

web_search      Gemini google_search; Anthropic web_search
url_context     Gemini url_context; no Anthropic equivalent
code_execution  Gemini code_execution; no Anthropic equivalent

A field with no equivalent on the resolved provider is ignored. That only ever fails safe: "no equivalent" means the provider has no such tool to leave on, so a `false` that lands nowhere describes a state that already holds. The startup summary prints the effective set, so an operator can confirm what actually took — worth doing, since a misspelled key is silently discarded (config decoding does not reject unknown fields).

A subagent that declares its own `model` block inherits each field it does not set, per field. An operator who turned web search off for the project meant it for the whole process; declaring a model must not quietly hand it back.

type CallPeerConfig added in v2.9.0

type CallPeerConfig struct {
	// Enabled registers the tool. Absent it, a hub daemon can still be
	// registered WITH by peers; it just can't call them.
	Enabled bool `json:"enabled,omitempty"`

	// Name renames the tool (default "call_peer"). Recipes with their
	// own vocabulary for delegation can match it. Note the permission
	// key follows the name: rename to "ask_operator" and the allow
	// pattern becomes "ask_operator:<peer>".
	Name string `json:"name,omitempty"`

	// Description replaces the model-facing description. Use it to
	// pin the prompt shape a fleet expects ("always name the cluster
	// and the namespace"); the built-in text explains the mechanics
	// but knows nothing about the deployment.
	Description string `json:"description,omitempty"`

	// TokenEnv names the env var holding the bearer token this agent
	// presents to peers. Empty means unauthenticated calls, which only
	// makes sense when the peers themselves run without
	// attach.token_env. Named-but-empty at call time is an error, not
	// an anonymous request.
	TokenEnv string `json:"token_env,omitempty"`

	// TimeoutSeconds bounds one delegated call end to end. Default 120;
	// ceiling 900. The callee runs a full agent turn, so this is not an
	// HTTP timeout — but it must stay short enough that a wedged peer
	// can't pin this agent's turn open indefinitely.
	TimeoutSeconds int `json:"timeout_seconds,omitempty"`

	// MaxResponseBytes caps how much of the peer's answer enters this
	// agent's context. Default 16384. Over the cap the answer is cut
	// and flagged truncated; the peer's session ID comes back either
	// way, so the full transcript stays reachable.
	MaxResponseBytes int `json:"max_response_bytes,omitempty"`
}

CallPeerConfig configures the `call_peer` built-in — named delegation to another core-agent daemon registered with this one's peer hub (#595, docs/kube-agents-platform-fit.md Gap 2).

Off by default, and Enabled alone is not enough: the daemon must also be running as a peer hub (--attach-peer-hub), because the registry is the only place the tool will accept a destination from. Asking for the tool without a hub is a startup error, not a silently inert tool.

type CheckpointConfig added in v2.9.0

type CheckpointConfig struct {
	// Mode selects which parties can declare a task boundary.
	//
	// Values, strongest to weakest:
	//   - "model"    (default) — the model-facing mark_task_done tool,
	//     the operator's /done, and the Checkpointer heuristic are all
	//     live. Today's behavior.
	//   - "operator" — /done and the heuristic stay; mark_task_done is
	//     not registered, so the model cannot self-declare completion.
	//   - "off"      — no checkpointing at all. /done reports that the
	//     agent was built without a checkpointer.
	//
	// Empty == "model".
	//
	// "operator" exists for long-lived services (#905). mark_task_done's
	// description tells the model to use it "generously at natural task
	// boundaries", and its detail arg asks for a completion summary —
	// framing written for an interactive coding session. A daemon
	// consuming machine signals has no conversation about to shift to a
	// new task, so every inbox bundle after a closed incident looks like
	// a boundary; a live deployment produced sixteen mark_task_done
	// calls in one session and answered an unrelated operator question
	// with a completion report instead. A tool description outranks the
	// persona at the point of decision, and a recipe author cannot edit
	// it, so the lever has to be here.
	//
	// This field exists so a recipe can ship its posture instead of
	// relying on every invocation and deploy manifest remembering a
	// flag — same argument as SafetyConfig.Watchdog (#660). It also
	// splits what --no-checkpoint conflated: that flag removes the
	// model's trigger, the operator's /done, and the heuristic
	// together, which is not what its help text promised.
	//
	// CLI override: --checkpoint=model|operator|off. The older
	// --no-checkpoint is retained as an alias for "off".
	Mode string `json:"mode,omitempty"`
}

CheckpointConfig configures task-boundary checkpointing — Mechanism C of docs/context-management-design.md. Distinct from compaction: compaction fires on context-window utilization with no model involvement, while a checkpoint slices at a *task boundary* and frames the handover record as "this task is now done". Turning checkpointing off does not turn off context reduction.

type CompactionConfig

type CompactionConfig struct {
	// Threshold overrides the fallback utilization threshold used
	// when the current model's tier isn't classified or isn't in
	// ThresholdByTier. Pointer so absence is distinguishable from
	// the deliberate value 0 (which would disable compaction).
	// Must be in (0, 1) when set.
	Threshold *float64 `json:"threshold,omitempty"`

	// ThresholdByTier overrides per-tier defaults. Keys are tier
	// labels from pkg/modeltier ("frontier", "mid", "small"). Set
	// only the tiers you want to override; the rest take their
	// package defaults (0.85 / 0.65 / 0.35). Values must be in
	// (0, 1).
	//
	// Example — keep frontier sessions on the historical default
	// while compacting Flash/Haiku much earlier:
	//   "compaction": {
	//     "threshold_by_tier": { "small": 0.30 }
	//   }
	ThresholdByTier map[string]float64 `json:"threshold_by_tier,omitempty"`
}

CompactionConfig configures the automatic context-window compaction trigger. Both fields are optional — leave empty for the substrate defaults (per-tier thresholds from pkg/modeltier).

type Config

type Config struct {
	Version     int               `json:"version"`
	Model       ModelConfig       `json:"model"`
	Permissions PermissionsConfig `json:"permissions,omitempty"`
	PathScope   PathScopeConfig   `json:"path_scope,omitempty"`
	Agent       AgentConfig       `json:"agent,omitempty"`
	ToolOutput  ToolOutputConfig  `json:"tool_output,omitempty"`
	Tools       ToolsConfig       `json:"tools,omitempty"`
	Subagents   []SubagentSpec    `json:"subagents,omitempty"`
	Hooks       hooks.Config      `json:"hooks,omitempty"`
	Mock        MockConfig        `json:"mock,omitempty"`
	OTEL        OTELConfig        `json:"otel,omitempty"`
	URLScope    URLScopeConfig    `json:"url_scope,omitempty"`
	Alerts      AlertsConfig      `json:"alerts,omitempty"`
	Attach      AttachConfig      `json:"attach,omitempty"`
	Pricing     PricingFileConfig `json:"pricing,omitempty"`
	UI          UIConfig          `json:"ui,omitempty"`
	Compaction  CompactionConfig  `json:"compaction,omitempty"`
	Checkpoint  CheckpointConfig  `json:"checkpoint,omitempty"`
	Session     SessionConfig     `json:"session,omitempty"`
	Safety      SafetyConfig      `json:"safety,omitempty"`

	// ContentRoots are operator-declared external directories trusted as
	// additional instruction/skill scopes, so an unmodified external agent
	// tree (e.g. a kube-agents checkout) can be consumed without vendoring a
	// copy. Paths are resolved relative to the agents dir. Each root is loaded
	// as its own trusted scope: instruction @include stays confined within the
	// root, and skills compose at precedence project > content_roots (listed
	// order) > home-agents > user. Empty = only the project root is trusted
	// (today's behavior exactly). CLI: --agents-content-dir (repeatable),
	// merged with this field. See docs/external-content-root-design.md.
	ContentRoots []string `json:"content_roots,omitempty"`
}

Config is the in-memory representation of `.agents/config.json`.

All sub-sections except Model have sensible zero-valued defaults, so a minimal `config.json` only needs to set what the user wants to override.

func DefaultConfig

func DefaultConfig() *Config

DefaultConfig returns a Config with all fields populated by sensible defaults. Override-then-merge happens at Load time.

func Load

func Load(agentsDir string) (*Config, error)

Load reads <agentsDir>/config.json, merges it over DefaultConfig(), and validates the result. agentsDir must be the absolute path returned by Find. Missing config.json is treated as "use defaults" (not an error) so that an empty .agents/ directory still yields a working config.

func LoadOrDefault

func LoadOrDefault(startDir string) (cfg *Config, agentsDir string, err error)

LoadOrDefault is the convenience entry point used by main: it walks up from startDir, loads `.agents/config.json` if found, otherwise returns pristine defaults. The returned agentsDir is "" when no .agents/ was discovered — callers can use that to skip writes that require a project.

func (*Config) EnvRefs added in v2.9.0

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

EnvRefs returns every env-var NAME the config refers to by name, sorted and deduplicated.

The bundle has two ways to consume an env var, and they are not interchangeable:

  • ${env:NAME} in instruction files, skills, and mcp.json values — splices the VALUE into text the model reads, at load time.
  • a *_env config field naming the var — hands the NAME to a component that resolves it late (pkg/tools/alert re-reads url_env on every fire, so rotating a Secret needs no restart, and the token never lands in the in-memory Config that log and status surfaces can echo).

pkg/agentenv only ever saw the first, because config never flows through Interpolate. That made .agents/env.yaml's drift report lie in both directions: a var declared in the manifest and used only by alerts.targets[].url_env was reported "nothing in the bundle references it" (observed live 2026-08-14 on the kube-platform-native deployment), and a bearer_env naming a var the manifest never declared drew no warning at all. Feeding this set into Resolver.NoteConfigRefs before ReportDrift closes both.

Discovery is reflective over the JSON tags rather than a hand-written field list, for the same reason pkg/tools derives its cross-reference catalog from the specs table (#759): a list maintained beside the thing it describes drifts the first time someone adds a field and doesn't know the list exists. The cost is that the convention IS the contract — a future env-name field tagged `json:"webhook_secret"` would go unseen. That is a naming review comment, not a silent bug: the failure mode is one spurious drift warning, never a boot failure or a misresolved value.

A nil *Config returns nil.

func (*Config) Validate

func (c *Config) Validate() error

Validate returns an error if the config is internally inconsistent. Validation here is structural; environmental concerns (is GOOGLE_API_KEY set? does the GCP project exist?) are checked at provider-construction time so test fixtures don't need real creds.

type ContextCacheConfig

type ContextCacheConfig struct {
	// Enabled defaults to true (nil = ON). Set to false to disable
	// caching without touching the other fields.
	Enabled *bool `json:"enabled,omitempty"`
	// TTL is how long each Create/Update requests the cache live for.
	// Format: any string time.ParseDuration accepts (e.g. "6h", "30m").
	// Empty → 6h. Vertex caps at 24h.
	TTL string `json:"ttl,omitempty"`
	// Refresh triggers a background Update when time-to-expiry drops
	// below this value. Format matches TTL. Empty → 30min.
	Refresh string `json:"refresh,omitempty"`
}

ContextCacheConfig tunes Vertex explicit context caching. All fields have sensible defaults — an empty struct enables caching with the design-doc defaults (6h TTL, 30min refresh window).

Enabled is a pointer so the config surface distinguishes "unset" (default ON) from an explicit "off" — operators typing `"enabled": false` disable caching while leaving TTL/Refresh in place for future re-enabling. The --no-context-cache CLI flag takes precedence over both.

func (*ContextCacheConfig) IsEnabled

func (c *ContextCacheConfig) IsEnabled() bool

IsEnabled reports whether caching should be turned on given this config. Nil receiver → true (default ON when the whole block is absent from config.json). Set Enabled to false to disable.

type CostRateLimitConfig added in v2.8.0

type CostRateLimitConfig struct {
	PerMinute int  `json:"per_minute,omitempty"`
	Burst     int  `json:"burst,omitempty"`
	Disabled  bool `json:"disabled,omitempty"`
}

MultiSessionConfig configures the per-caller authentication + per-session ACL surface introduced in v2.4. Disabled (zero value) preserves single-user behavior. When enabled, the attach server authenticates every request against the configured Authenticator (typically a bearer table loaded from users.json) and threads the resolved Caller through audit logs, per-session permission grants, and outbound MCP context.

Field-by-field detail in docs/multi-session-design.md §"Config surface". CostRateLimitConfig is the config-file form of the attach listener's per-caller cost rate limit (attach.CostRateLimit). PerMinute/Burst <= 0 fall back to the library defaults (10/min, burst 5); Disabled switches enforcement off entirely.

type MockConfig

type MockConfig struct {
	Script string `json:"script,omitempty"`
	Strict bool   `json:"strict,omitempty"`
	Record string `json:"record,omitempty"`
}

MockConfig configures the mock providers (echo, scripted) and the orthogonal recording wrapper.

Script is the path to a JSONL transcript consumed by the scripted provider; it's required when model.provider is "scripted".

Strict makes the scripted provider assert that each incoming request's Contents JSON-equal the recorded request. Off by default — the typical use is replaying without caring about prompt drift.

Record is a path to write a JSONL recording of every LLM turn. Works with any provider, not just the mocks; lives in MockConfig because it shares the file format the scripted provider consumes.

type ModelConfig

type ModelConfig struct {
	Provider  string           `json:"provider,omitempty"`
	Name      string           `json:"name"`
	APIKey    string           `json:"api_key,omitempty"`
	Vertex    *VertexConfig    `json:"vertex,omitempty"`
	Anthropic *AnthropicConfig `json:"anthropic,omitempty"`
	// Pricing is a per-model rate override keyed by model name
	// (case-insensitive). Survives /model switches mid-session —
	// every model the operator routes to can carry its own rates.
	// Layered with .agents/pricing.json + ~/.core-agent/pricing.json
	// + the compiled-in fallback; see pkg/pricing for the
	// lookup chain. Previously a single *PricingConfig that matched
	// only Model.Name; PR core-agent/#NN renamed the JSON key
	// `pricing` from "{input_per_mtok, output_per_mtok}" to a map.
	Pricing PricingMap `json:"pricing,omitempty"`

	// BuiltinTools gates the PROVIDER's server-side built-in tools.
	// Absent (the common case) means every tool keeps its provider
	// default. See BuiltinToolsConfig — and note this is a different
	// axis from `tools.disable` / `--no-builtin-tools`, which govern
	// core-agent's own function tools.
	BuiltinTools *BuiltinToolsConfig `json:"builtin_tools,omitempty"`
}

ModelConfig selects the LLM provider and model.

Provider: one of "gemini", "vertex", "anthropic". When empty, the resolver auto-detects from the environment (see models.Resolve). Name: a model ID, e.g. "gemini-3.7-flash" or "claude-opus-5". APIKey: optional inline key for Provider="gemini"; usually unset and read from GOOGLE_API_KEY at runtime. Vertex: required when Provider="vertex"; project + location. Anthropic: optional credentials for Provider="anthropic"; usually unset and read from ANTHROPIC_API_KEY at runtime. BuiltinTools: optional gate on the provider's server-side built-ins (web search, URL context, code execution).

type MultiSessionAuthConfig

type MultiSessionAuthConfig struct {
	// Kind selects the Authenticator implementation.
	// Recognized values:
	//   - "" or "bearer_table" — static token → identity table loaded
	//     from TableFile (default; v2.4)
	// Future: "oidc" / "mtls" / "k8s_sa" (interfaces designed; not
	// shipped in v2.4).
	Kind string `json:"kind,omitempty"`

	// TableFile is the path to users.json when Kind="bearer_table".
	// File must be mode 0600 or stricter (the loader rejects anything
	// laxer). Required when Kind="bearer_table" and Enabled=true.
	TableFile string `json:"table_file,omitempty"`
}

MultiSessionAuthConfig selects which Authenticator implementation the multi-session attach listener uses. Only "bearer_table" is shipped in v2.4; the other kinds are designed but deferred (see docs/multi-session-design.md §"Non-goals").

type MultiSessionConfig

type MultiSessionConfig struct {
	// Enabled switches the attach listener from single-user mode
	// (daemon-level bearer token, no per-caller threading) to
	// multi-session mode (per-caller authentication, ACL enforcement,
	// audit log identity threading). Default false.
	Enabled bool `json:"enabled,omitempty"`

	// UsersDir is the directory holding per-caller instruction
	// overlays (Phase 3 / PR γ). Each subdirectory named after a
	// Caller's Identity may contain an .agents/ tree merged on top of
	// the daemon-wide instruction stack for sessions belonging to
	// that Caller. Empty disables the overlay path.
	UsersDir string `json:"users_dir,omitempty"`

	// Auth selects the Authenticator implementation and its
	// configuration. Only "bearer_table" is shipped in v2.4; OIDC /
	// JWT / mTLS / K8s ServiceAccount kinds are designed but
	// deferred.
	Auth MultiSessionAuthConfig `json:"auth,omitempty"`

	// AdminIdentities lists the Caller identities that bypass every
	// per-session authorization check (Admin role). Use sparingly —
	// these identities can read every session in the daemon.
	AdminIdentities []string `json:"admin_identities,omitempty"`

	// AllowAnonymous, when true, lets requests without a valid
	// credential resolve to the DefaultIdentity Caller instead of
	// returning 401. Dangerous in shared environments where every
	// unauthenticated request becomes the same Caller. Default false.
	AllowAnonymous bool `json:"allow_anonymous,omitempty"`

	// DefaultIdentity is the Caller.Identity stamped onto the
	// implicit anonymous Caller (when multi-session is disabled or
	// AllowAnonymous=true). Default "anon".
	DefaultIdentity string `json:"default_identity,omitempty"`

	// ProxyIdentities lists Caller identities permitted to assert
	// other Callers via the AssertedCallerHeader. Typical use:
	// chat-bot service-account identities ("sa:slack-bot") that
	// authenticate as themselves but speak on behalf of human users.
	// Empty disables the proxy path.
	ProxyIdentities []string `json:"proxy_identities,omitempty"`

	// AssertedCallerHeader is the header name a proxy Caller uses to
	// assert the effective identity. Default "X-Asserted-Caller".
	AssertedCallerHeader string `json:"asserted_caller_header,omitempty"`

	// SessionIdleTimeout bounds how long an in-memory session may
	// sit untouched before the eviction sweep removes it. Evicted
	// sessions remain resumable from disk (the ACL row stays);
	// the next Lookup re-resumes them lazily via the SessionResumer.
	//
	// Parsed via time.ParseDuration ("24h", "30m", "7d" all work).
	// Omitted or empty → default 24h. Explicit "0s" DISABLES the
	// sweep entirely — sessions stay in memory until the daemon
	// stops. Use "0s" for tiny local-dev daemons where memory
	// isn't a concern; use a shorter value for tight-budget pods.
	//
	// Only meaningful when Enabled=true and the daemon has an
	// aclStore wired (i.e., --session-db is set). See
	// docs/session-resume-design.md §"Lifecycle primitive".
	SessionIdleTimeout string `json:"session_idle_timeout,omitempty"`
}

type OTELConfig

type OTELConfig struct {
	Exporter string            `json:"exporter,omitempty"` // "none" | "console" | "otlp"
	Endpoint string            `json:"endpoint,omitempty"`
	Metrics  OTELMetricsConfig `json:"metrics,omitempty"`
}

OTELConfig configures the OpenTelemetry exporter.

Exporter/Endpoint govern trace export. Metrics is an independent nested block for the metric pipeline — separate because the traces path delegates to ADK while metrics do not (see docs/metrics-design.md).

type OTELMetricsConfig added in v2.8.0

type OTELMetricsConfig struct {
	Exporter       string `json:"exporter,omitempty"`
	PrometheusAddr string `json:"prometheus_addr,omitempty"`

	// SessionLabels controls whether per-session identity attributes
	// (session.id, app.name, user.id) are stamped on the usage
	// metrics. Absent/true — the default — preserves the per-session
	// series shape; false aggregates across sessions before export,
	// for fleet operators where per-session labels would blow up
	// series cardinality (many short-lived attach sessions × models).
	// Per-tool and per-turn histograms never carry session labels
	// regardless of this setting.
	SessionLabels *bool `json:"session_labels,omitempty"`
}

OTELMetricsConfig configures the OpenTelemetry metrics pipeline.

Exporter values:

  • "none" — default; no MeterProvider is installed
  • "otlp" — OTLP metrics via HTTP; honors OTEL_EXPORTER_OTLP_METRICS_ENDPOINT
  • "prometheus" — served at PrometheusAddr for scrape
  • "both" — OTLP push + Prometheus pull on the same MeterProvider

The standard OTel env var OTEL_METRICS_EXPORTER overrides Exporter when set — matches the OTEL_TRACES_EXPORTER convention (#315) and lets K8s Deployments with shared ConfigMaps flip the metrics surface per-Pod without duplicating config.json.

PrometheusAddr is the bind address for the /metrics scrape endpoint (e.g. ":9464"). Ignored unless Exporter is "prometheus" or "both". Empty + Prometheus mode selected implies ":9464" (the OTel-conventional Prometheus reader port).

func (OTELMetricsConfig) SessionLabelsEnabled added in v2.8.0

func (c OTELMetricsConfig) SessionLabelsEnabled() bool

SessionLabelsEnabled reports whether usage metrics carry per-session identity attributes. Nil field → true (default ON; same tri-state convention as ContextCacheConfig.IsEnabled).

type PathScopeAllowEntry

type PathScopeAllowEntry struct {
	Path   string `json:"path"`
	Access string `json:"access"`
}

PathScopeAllowEntry is one typed allow-list entry. Access is one of "r" / "w" / "rw" (long forms "read" / "write" / "readwrite" also accepted); empty Access fails validation rather than silently broadening to rw. Path uses the same matching rules as Allow: exact path, "/.../" subtree, or filepath.Match glob.

type PathScopeConfig

type PathScopeConfig struct {
	Allow      []string              `json:"allow,omitempty"`
	AllowPaths []PathScopeAllowEntry `json:"allow_paths,omitempty"`
}

PathScopeConfig holds extra paths that file tools may read/write outside the default project + user-home scope. Patterns may be exact paths or directory globs (terminating "/...") and are typically appended via the "Always allow this path/tree" prompt path.

Two shapes coexist:

  • Allow: legacy untyped list; each entry implicitly grants both read and write so behavior matches what existed before the access-level work landed.
  • AllowPaths: typed entries with per-path access spec ("r" / "w" / "rw"). New configurations should prefer this form — it lets the operator say "agent may read this tree but writes still prompt", which the legacy list can't express.

type PermissionsConfig

type PermissionsConfig struct {
	Mode  string   `json:"mode,omitempty"`  // "ask" | "allow" | "yolo" | "plan" | "acceptEdits"
	Allow []string `json:"allow,omitempty"` // pattern allowlist
	Deny  []string `json:"deny,omitempty"`  // pattern denylist

	// UseBuiltinAllow toggles core-agent's built-in conservative
	// read-only allowlist bundle. Defaults to true when nil (the
	// pointer carries an explicit "off" signal vs "unset"). false
	// drops the entire built-in bundle including any opt-ins in
	// BuiltinAllowExtras. See permissions/builtin_allow.go for the
	// bundle catalog.
	UseBuiltinAllow *bool `json:"use_builtin_allow,omitempty"`

	// BuiltinAllowExtras names additional built-in bundles to merge
	// on top of read_only when UseBuiltinAllow is on. Unknown names
	// fail at config-validation time rather than silently dropping
	// permissions. Known bundles: see permissions.KnownBundles().
	BuiltinAllowExtras []string `json:"builtin_allow_extras,omitempty"`

	// RequirePlanArtifact is the deprecated two-state spelling of
	// PlanMode: true means PlanModeRequired, absent/false means "no
	// opinion" (PlanMode, then the task profile, then off). It cannot
	// express PlanModeAdvisory, which is why PlanMode exists.
	//
	// Read it through ResolvedPlanMode rather than directly — a
	// consumer switching on this bool sees "off" for an advisory run
	// and would skip registering record_plan.
	//
	// Deprecated: set PlanMode instead. Removed in the next major.
	RequirePlanArtifact bool `json:"require_plan_artifact,omitempty"`

	// PlanMode controls the two things record_plan does — persist a
	// plan artifact, and gate mutating tools on one existing —
	// independently, because operators need them independently
	// (#215).
	//
	//   "off"       record_plan is not registered; no artifact, no gate.
	//   "advisory"  record_plan IS registered and the artifact persists
	//               to .agents/plans/plan-N.md, but NO mutating call is
	//               ever blocked on plan state. The audit surface
	//               without the two-turn ceremony — the shape an
	//               autonomous triage or alert-response agent wants.
	//   "required"  advisory plus the plan-first gating pre-check:
	//               mutating tool calls (write/edit/delete/bash, spawn
	//               family, MCP tools) are denied until the model calls
	//               record_plan. Read-only tools and record_plan itself
	//               remain allowed so research happens normally.
	//               Composes with every Mode — even ModeYolo denies
	//               before a plan is recorded; once recorded, the
	//               mode's usual semantics resume.
	//
	// Empty means unset: ResolvedPlanMode falls back to
	// RequirePlanArtifact, then to off. Prompting the model to
	// actually call record_plan in advisory mode is the recipe's job
	// (AGENTS.md / skill instructions) — the runtime only guarantees
	// the tool is there and the artifact lands.
	// See docs/plan-first-design.md.
	PlanMode string `json:"plan_mode,omitempty"`
}

PermissionsConfig configures the permission gate.

func (*PermissionsConfig) NormalizePlanMode added in v2.9.0

func (p *PermissionsConfig) NormalizePlanMode(mode string)

NormalizePlanMode collapses the two spellings into PlanMode alone and clears the deprecated bool, making PlanMode the single source of truth for everything downstream. Call it once, after CLI flags have been folded in. Keeping two fields *in sync* is the drift this whole pair exists to prevent, so nothing re-reads the bool afterwards.

func (PermissionsConfig) PlanGateArmed added in v2.9.0

func (p PermissionsConfig) PlanGateArmed() bool

PlanGateArmed reports whether mutating tool calls are denied until a plan is recorded. True in required mode only.

func (PermissionsConfig) PlanModeSet added in v2.9.0

func (p PermissionsConfig) PlanModeSet() bool

PlanModeSet reports whether the config expressed an opinion about plan mode at all, in either spelling. Distinct from ResolvedPlanMode() != PlanModeOff, which cannot tell "the operator wrote off" from "the operator said nothing" — a precedence chain with a task-class default underneath it needs that difference.

func (PermissionsConfig) PlanModeSpelling added in v2.9.0

func (p PermissionsConfig) PlanModeSpelling() string

PlanModeSpelling reports which field supplied the resolved mode: "plan_mode", "require_plan_artifact", or "" when neither was set. Only for operator-facing provenance messages; behavior must come from ResolvedPlanMode and its two predicates.

func (PermissionsConfig) PlanToolRegistered added in v2.9.0

func (p PermissionsConfig) PlanToolRegistered() bool

PlanToolRegistered reports whether record_plan should be registered for this config. True in both advisory and required — the artifact is the point of advisory mode.

func (PermissionsConfig) ResolvedPlanMode added in v2.9.0

func (p PermissionsConfig) ResolvedPlanMode() string

ResolvedPlanMode reports the effective plan mode, folding the deprecated RequirePlanArtifact bool forward. This is the ONLY supported way to ask "is record_plan registered" or "is the gate armed" — reading either field raw is how the two spellings drift.

PlanMode wins when set, since it is the more expressive field and an operator who wrote it meant it. An unknown value can't reach here: Validate rejects it at load.

type PricingConfig

type PricingConfig struct {
	InputPerMTok                float64 `json:"input_per_mtok,omitempty"`
	CachedInputPerMTok          float64 `json:"cached_input_per_mtok,omitempty"`
	CacheCreationInputPerMTok   float64 `json:"cache_creation_input_per_mtok,omitempty"`
	CacheCreation1hInputPerMTok float64 `json:"cache_creation_1h_input_per_mtok,omitempty"`
	OutputPerMTok               float64 `json:"output_per_mtok,omitempty"`
}

PricingConfig overrides the built-in price table for cost estimation. CachedInputPerMTok is the rate for prompt-cache-hit input tokens; when zero, cache hits are billed at InputPerMTok (no assumed discount). CacheCreationInputPerMTok is the rate for input tokens that WRITE a cache entry (Anthropic's cache_creation_input_tokens, a premium over base input); when zero, written tokens are billed at InputPerMTok, which understates the real bill. It is the 5-minute-TTL rate; CacheCreation1hInputPerMTok overrides the 1-hour one, and falls back to the 5-minute rate when zero rather than to base input.

type PricingFileConfig

type PricingFileConfig struct {
	// Refresh enables the daily background fetch from Source into
	// ~/.core-agent/pricing.json's external section. Defaults to
	// true (most operators want fresh rates). Disable for
	// air-gapped pods or CI where outbound network is blocked or
	// undesirable.
	//
	// Pointer so the JSON unmarshaler can distinguish "unset
	// (default true)" from "explicit false". A bare `null` or
	// missing field yields the default.
	Refresh *bool `json:"refresh,omitempty"`

	// Source overrides the upstream URL the refresher fetches from.
	// Empty defaults to pricing.DefaultRefreshSource (LiteLLM's
	// model_prices_and_context_window.json). Override for mirrors
	// or internal pricing services.
	Source string `json:"source,omitempty"`
}

PricingFileConfig governs the pricing-catalog refresh behavior — distinct from ModelConfig.Pricing (which is the per-model rate override map). Defaults: refresh enabled, daily cadence, LiteLLM upstream. See pkg/pricing and docs/pricing-design.md.

type PricingMap

type PricingMap map[string]PricingConfig

PricingMap is the model-keyed override map used by ModelConfig. Aliased so future expansions (per-context rates, cached vs uncached, etc.) localize to one type.

type PromptCacheConfig added in v2.9.0

type PromptCacheConfig struct {
	// Enabled defaults to true (nil = ON). Set to false to disable
	// caching for this provider.
	Enabled *bool `json:"enabled,omitempty"`

	// TTL is the breakpoint lifetime requested on cache writes:
	// "5m" (default) or "1h". Empty means 5m.
	//
	// The trade is a real one and the reason this is a knob rather
	// than a default: a 1-hour breakpoint bills writes at 2x base
	// input where a 5-minute one bills 1.25x, so it pays only when
	// the gaps between turns routinely exceed five minutes — a
	// human-paced review session, a cron-driven agent, an operator
	// approving each tool call. A tight agentic loop refreshes the
	// 5-minute window on every turn and should stay on the default,
	// where the extra 0.75x would be pure loss.
	//
	// Both TTLs are priced separately and the response reports which
	// one each write used, so the ledger tells the truth either way
	// (#770).
	TTL string `json:"ttl,omitempty"`
}

PromptCacheConfig tunes Anthropic prompt caching. An empty struct enables caching with the defaults, matching ContextCacheConfig's shape so the two provider families read the same way in a config file.

Enabled is a pointer so "unset" (default ON) is distinguishable from an explicit "off". The --no-prompt-cache CLI flag takes precedence over both.

func (*PromptCacheConfig) CacheTTL added in v2.9.0

func (c *PromptCacheConfig) CacheTTL() string

CacheTTL returns the normalized breakpoint TTL — PromptCacheTTL5m or PromptCacheTTL1h. Nil receiver, empty value, and (defensively) any value Validate would have rejected all resolve to 5m: the cheaper TTL is the safe reading of an unclear config.

func (*PromptCacheConfig) IsEnabled added in v2.9.0

func (c *PromptCacheConfig) IsEnabled() bool

IsEnabled reports whether prompt caching should be turned on given this config. Nil receiver → true (default ON when the block is absent from config.json).

type SafetyConfig

type SafetyConfig struct {
	// SmallTierParent controls what happens when an interactive
	// session starts on a small-tier parent model (Flash/Haiku-class).
	// These models work well as agentic_* subtask workers (#118-122)
	// but loop and stall as the parent for long interactive sessions
	// — see #121 for the smoke that motivated this guard.
	//
	// Values: "warn" (default) logs a one-line operator notice but
	// proceeds; "refuse" exits with a config-error code; "allow"
	// suppresses the check entirely. Empty == "warn".
	//
	// The check is skipped regardless when:
	//   - `-p` one-shot mode (operator knows what they're doing;
	//     might be a script invoking Flash on purpose)
	//   - `--yolo` (trust-the-operator mode)
	//   - The parent's tier doesn't classify (unknown model)
	//
	// CLI override: --small-tier-parent=warn|refuse|allow.
	SmallTierParent string `json:"small_tier_parent,omitempty"`

	// Watchdog selects the behavioral watchdog's posture — the
	// runaway-tool-loop backstop from #123/#623.
	//
	// Values: "off" (no observation), "warn" (observe the tool-call
	// stream and log structured alerts, but never halt), "feedback"
	// (same, plus the alert is injected into the model's next-turn
	// context as a "[watchdog]" block so the party making the looping
	// call finds out about it — #159), "enforce" (all of that plus a
	// turn-error of kind=watchdog; the agent refuses new turns until
	// Agent.ResetWatchdog clears it).
	//
	// The values form a ladder — each includes the one before it. In
	// particular "enforce" injects too: an operator reset resumes a
	// model whose context still ends in the loop it was halted for,
	// and without the observation the next turn re-trips.
	//
	// "feedback" is a correction, not a backstop: nothing stops a model
	// that reads the observation and loops anyway. Unattended runs want
	// "enforce".
	//
	// Empty == unset, which resolves to a *mode-dependent* default:
	// "enforce" for unattended runs (-p one-shot, --no-repl daemon,
	// or a non-TTY stdin) and "warn" for interactive REPL/TUI runs.
	// An unattended daemon has no operator watching the alert stream,
	// so observe-and-log is not a backstop there (#642).
	//
	// This field exists so a recipe can ship its own backstop instead
	// of relying on every invocation and deploy manifest remembering
	// --watchdog=enforce (#660).
	//
	// CLI override: --watchdog=off|warn|feedback|enforce.
	Watchdog string `json:"watchdog,omitempty"`

	// BashSearchGate controls what happens when the model reaches for
	// a search-shaped shell command — `grep -rn foo .`, `find . -name
	// '*.go'`, `rg`, `ag`, `ack`, `fd` — while the native `grep` /
	// `glob` tools are registered to do the same job.
	//
	// Bash-as-grep is a training prior strong enough that advisory
	// tool-description hints bounce off it: probe data in
	// docs/gemini-tier1-followup-plan.md measured a Gemini variant
	// picking `bash` for search 15/27 times with the structured tools
	// right there in its catalog, and the description literally saying
	// "PREFERRED over bash grep". A description is read once at
	// registration and never reinforced; a refusal is in-context,
	// immediate, and names the tool to use instead (#158).
	//
	// Values: "enforce" (default) refuses the call with a structured
	// error naming the native equivalent; "warn" runs the command but
	// attaches a notice to the tool result; "allow" disables the check.
	// Empty == "enforce".
	//
	// This is a steering control, not a security boundary: it refuses
	// a *shape*, and only the shape a model reaches for by reflex.
	// Everything else bash can do — tests, builds, git, formatters —
	// is untouched, which is the point. `--disable-tools=bash` is the
	// blunt version and it takes `go test` with it.
	//
	// The gate only refuses what it can redirect. Disable `grep` and
	// `glob` here in tools.disable and it goes inert rather than
	// refusing with a pointer to tools that aren't in the catalog.
	//
	// CLI override: --bash-search-gate=enforce|warn|allow.
	BashSearchGate string `json:"bash_search_gate,omitempty"`
}

SafetyConfig carries operator-facing safety guardrails — things that are NOT permission gates (those live in PermissionsConfig) but rather "the operator probably misconfigured something" checks and runaway backstops: the small-tier-parent guard (#121) and the behavioral watchdog mode (#660).

type SessionConfig

type SessionConfig struct {
	// TaskClass is the operator-declared task class. Must be one
	// of pkg/taskclass.Classes() ("debug" | "implement" | "chat"
	// | "research" | "review") or empty. When set, the CLI applies
	// the matching Profile to whichever flags the operator left
	// unspecified (--model, --ask, compaction threshold, etc.).
	// Explicit CLI flags always win over the task profile.
	//
	// Useful for project-local defaults — e.g. an infra repo's
	// .agents/config.json sets "debug" because debugging is what
	// happens there; operators get the right defaults without
	// having to remember --task=debug on every invocation.
	TaskClass string `json:"task_class,omitempty"`
}

SessionConfig carries per-session presets — currently just the operator-declared task class (#123). CLI flag --task overrides this field; both default to unset, which leaves the substrate defaults in place.

type SpawnAgentConfig added in v2.9.0

type SpawnAgentConfig struct {
	// SyncWaitTimeout bounds how long `spawn_agent {wait: true}` holds
	// the parent's turn open, as a time.Duration string ("10m").
	// Omitted/empty keeps the 5m default.
	//
	// The cap is on the *wait*, not on the subagent: past it the tool
	// returns and the subagent keeps running, its result delivered on
	// a later turn as a pushed report. So raising this trades parent
	// latency for the parent seeing the answer in the turn that asked
	// for it — which is what a deep diagnostic wants, since a parent
	// that gets a timeout tends to redo the work itself (#692).
	//
	// An explicit "0s" removes the cap: the wait then ends when the
	// subagent finishes on its own turn/wallclock budget, or when the
	// parent's context is canceled. That is a real setting for a
	// recipe with tight per-subagent budgets, not a way to hang — but
	// it does mean the subagent's budgets are the only bound left.
	// Negative is rejected.
	SyncWaitTimeout string `json:"sync_wait_timeout,omitempty"`
}

SpawnAgentConfig tunes the `spawn_agent` built-in — in-process delegation to a subagent (#626). It sits beside CallPeerConfig because the two are the same shape of thing: a delegation tool whose blocking form has to be bounded so a slow callee can't pin the parent's turn open.

The subagents themselves are declared in the top-level `subagents` array; this is only the tool's own knobs.

type SubagentBudgets added in v2.9.0

type SubagentBudgets struct {
	MaxTurns            int     `json:"max_turns,omitempty"`
	MaxCostUSD          float64 `json:"max_cost_usd,omitempty"`
	MaxWallclockSeconds int     `json:"max_wallclock_seconds,omitempty"`
}

SubagentBudgets caps one delegation. Field names mirror spawn_agent's tool arguments (max_turns / max_cost_usd / max_wallclock_seconds) so an operator reading a recipe and a model reading the tool schema are looking at the same three dimensions under the same three names.

Each dimension is independent and each zero value means unset. Turns are the subagent's own model turns, not the parent's; the dollar cap is evaluated against the same per-turn pricing the session ledger uses; wall-clock is measured from the start of the delegation.

A cap that fires does not fail the delegation. Whatever the subagent produced up to that point is returned, labelled as a partial and naming the cap that stopped it — the parent holds the goal and can re-ask with specifics, where a discarded partial makes it pay twice for the same work (#691).

type SubagentSpec added in v2.9.0

type SubagentSpec struct {
	Name         string       `json:"name"`
	Description  string       `json:"description,omitempty"`
	Instructions string       `json:"instructions,omitempty"`
	Model        *ModelConfig `json:"model,omitempty"`
	MaxDepth     int          `json:"max_depth,omitempty"`
	Tools        []string     `json:"tools,omitempty"`
	MCP          []string     `json:"mcp,omitempty"`
	Skills       []string     `json:"skills,omitempty"`
	Root         string       `json:"root,omitempty"`

	// Budgets bounds one delegation to this subagent. Honored on BOTH
	// doors the subagent is reachable through — as a parent tool call
	// and by spawn_agent reference — because a cap that binds only one
	// of them is worse than none: the operator reads the config and
	// believes the subagent is bounded.
	//
	// Zero/omitted means "no declared cap": the async door then falls
	// back to the manager's defaults (50 turns / $1 / 10m), and the
	// synchronous door stays unbounded, which is what it has always
	// been. A per-spawn override may only tighten what is declared here.
	Budgets *SubagentBudgets `json:"budgets,omitempty"`
}

SubagentSpec declares one in-process subagent the parent agent may call by name. Subagents are wired in cmd/core-agent onto the shipped subagent substrate (agent.WithSubagents / agent.NewSubagentTool) — see docs/declarative-subagents-design.md. This type is the on-disk schema; it carries no runtime behavior.

A subagent runs on its own Model (or the parent's, when Model is nil) and its own Instructions (which support the @include directive).

There are two ways to give a subagent a tool surface:

  • Inline refs against the SHARED config (Root unset). MCP names servers from .agents/mcp.json, Skills names skills from .agents/skills/, Tools is a built-in allowlist. Each dimension inherits the parent's full surface when unset, selects a subset when a non-empty list, or grants none when an explicit empty list (e.g. "mcp": []). The whole recipe stays in one config.json + one mcp.json + one skills/ tree.
  • A dedicated content root (Root set). Root names a trusted directory the subagent loads as its OWN scope, independent of the parent: its persona auto-assembles from <root>/AGENTS.md (Instructions overrides), skills load from <root>/skills/, and MCP servers from <root>/mcp.json. The parent loads none of it — this is how a subagent gets a persona, skills, or servers the parent must NOT have (e.g. a read-only single-cluster specialist under a fleet parent). With Root set, MCP and Skills (when non-empty) filter WITHIN the root; Tools remains a built-in allowlist (built-ins live in the binary, not a directory). See docs/declarative-subagents-design.md, "Per-subagent content root".

A declarative subagent is operator-authored and trusted, but still bound by the same permission gate (and require_plan_artifact) as the parent, so it cannot escalate.

type ToolOutputConfig

type ToolOutputConfig struct {
	MaxBytes int                              `json:"max_bytes,omitempty"`
	MaxLines int                              `json:"max_lines,omitempty"`
	PerTool  map[string]ToolOutputPerToolCaps `json:"per_tool,omitempty"`
}

ToolOutputConfig caps tool result size before it enters model context.

type ToolOutputPerToolCaps

type ToolOutputPerToolCaps struct {
	MaxBytes int `json:"max_bytes,omitempty"`
	MaxLines int `json:"max_lines,omitempty"`
}

ToolOutputPerToolCaps overrides global tool-output limits for one tool.

type ToolsConfig

type ToolsConfig struct {
	Disable       []string            `json:"disable,omitempty"`
	WaitAndVerify WaitAndVerifyConfig `json:"wait_and_verify,omitempty"`
	CallPeer      CallPeerConfig      `json:"call_peer,omitempty"`
	SpawnAgent    SpawnAgentConfig    `json:"spawn_agent,omitempty"`
}

ToolsConfig configures the bundled CLI's built-in tool suite.

Disable lists tools to turn off. Names must match the canonical built-in names (see tools.BuiltinToolNames). Unknown names cause a startup error from tools.BuiltinTools.Disable, so typos fail loudly rather than silently leaving a tool on.

The CLI's --disable-tools flag composes with this list by union; --no-builtin-tools disables the entire suite and makes Disable moot.

type UIConfig

type UIConfig struct {
	// Theme picks the rendering style for the core-tui surface.
	// Three reserved buckets:
	//   - "auto"  (default) — detect via terminal background query.
	//   - "dark"            — force dark theme; skips the OSC-11 query.
	//   - "light"           — force light theme; skips the OSC-11 query.
	// Any other lowercase identifier (letters, digits, dash,
	// underscore) is treated as a named theme from core-tui's
	// BuiltinThemes registry (e.g. "gopher", "google"). The /theme
	// picker writes back through PersistThemeChoice using these
	// names, so the field round-trips picker choices. Unknown
	// names fall back to the auto path at launch.
	Theme string `json:"theme,omitempty"`

	// Mouse enables terminal mouse capture so the wheel scrolls the
	// chat viewport. When enabled, plain click-drag no longer selects
	// text — terminals route around the capture when Shift is held
	// (Shift-drag to select, copy as usual). Pointer so unset means
	// "use the default" (true). Toggle at runtime with /mouse.
	Mouse *bool `json:"mouse,omitempty"`
}

UIConfig holds presentation choices for the in-process TUI (both internal/tui and the core-tui adapter). Both fields are optional with sensible defaults — operators only need to set what they want to override.

func (UIConfig) MouseEnabled

func (u UIConfig) MouseEnabled() bool

MouseEnabled reports whether mouse capture should be on at startup. Defaults to true when the field is unset.

type URLScopeConfig

type URLScopeConfig struct {
	Allow                  []string                     `json:"allow,omitempty"`
	Deny                   []string                     `json:"deny,omitempty"`
	MaxBodyBytes           int                          `json:"max_body_bytes,omitempty"`
	TimeoutSeconds         int                          `json:"timeout_seconds,omitempty"`
	Headers                map[string]map[string]string `json:"headers,omitempty"`
	AllowMetadataEndpoints bool                         `json:"allow_metadata_endpoints,omitempty"`

	// Proxy controls outbound proxying for fetch_url (#429):
	//
	//   ""       (default) — no proxy. HTTP_PROXY/HTTPS_PROXY env
	//            vars are deliberately IGNORED: with a proxy in the
	//            path, hostname targets are resolved AT the proxy,
	//            outside the SSRF guard's resolve-validate-pin dial,
	//            so proxying must be an explicit operator decision,
	//            not ambient environment.
	//   "env"    — honor the standard proxy environment variables
	//            (HTTP_PROXY / HTTPS_PROXY / NO_PROXY).
	//   <url>    — route through this fixed proxy URL
	//            (http://, https://, or socks5://).
	//
	// In either non-empty mode the operator delegates private/
	// metadata-range SSRF policy for hostname targets to the proxy;
	// literal-IP targets are still screened locally on the initial
	// URL and every redirect hop.
	Proxy string `json:"proxy,omitempty"`
}

URLScopeConfig governs which URLs the fetch_url built-in is allowed to reach. Same Allow/Deny grammar + precedence as PathScopeConfig: Deny wins on overlap; an empty Allow list with the tool registered is treated as default-deny (the tool refuses every fetch and returns a clear error pointing at this config field).

Patterns are host-only globs (e.g. "github.com", "*.googleapis.com", "*.svc.cluster.local"). HTTPS is assumed unless the pattern is prefixed with "http://", in which case plain HTTP is allowed for that pattern only (intentionally awkward — operators have to type the prefix to opt out of TLS).

MaxBodyBytes caps the response body the tool returns to the model; zero means use the built-in default (64 KiB). TimeoutSeconds caps the HTTP timeout; zero means 30s.

Headers maps host patterns to header bundles. Header values pass through os.ExpandEnv at request time, so values like "Bearer ${GITHUB_TOKEN}" pick up rotated env vars without a restart. The model never sets headers directly — keeps credential exfiltration off the tool argument surface.

AllowMetadataEndpoints opts back into fetching link-local and cloud-metadata addresses (169.254.0.0/16 — including the 169.254.169.254 metadata service — plus fe80::/10 and the AWS IMDS IPv6 address fd00:ec2::254). fetch_url hard-blocks those ranges in every permission mode regardless of the allowlist; this flag is the only way to reach them. Default false — leave it off unless you are deliberately building a metadata-service integration and understand the credential-theft blast radius.

type VertexConfig

type VertexConfig struct {
	Project  string `json:"project"`
	Location string `json:"location"`

	// ContextCache toggles Vertex explicit context caching for the
	// stable request prefix (system instruction + tools). When nil
	// or Enabled != false, caching is ON — the daemon creates a
	// CachedContent resource on the first turn and stamps it onto
	// every subsequent turn's GenerateContentConfig.CachedContent.
	// See docs/vertex-context-caching-design.md and
	// internal/vertexcache/manager.go.
	ContextCache *ContextCacheConfig `json:"context_cache,omitempty"`
}

VertexConfig holds GCP-specific settings for the vertex provider.

type WaitAndVerifyConfig added in v2.9.0

type WaitAndVerifyConfig struct {
	// PollAllow names tools that may be polled despite not being
	// classified read-only by the runtime. This exists for MCP: ADK's
	// MCP adapter does not surface the server's readOnlyHint
	// annotation, so an MCP tool lands on the fail-safe "mutating"
	// side of tools.IsReadOnlyTool and would be refused. Listing a
	// tool here is the operator asserting it only observes state.
	//
	// For a server that is read-only in its entirety — a provider's
	// /mcp/read-only endpoint — prefer `read_only: true` on the
	// ServerSpec in mcp.json (#693), which classifies every tool the
	// server exposes. This list is then for the finer case: one
	// read-only tool on a server that also mutates.
	//
	// Names are the ones the model sees, i.e. namespaced for MCP
	// ("gke_get_pod" and not "get_pod").
	PollAllow []string `json:"poll_allow,omitempty"`

	// MaxTimeoutSeconds caps the total wall clock one wait may spend.
	// Default 300. A call asking for more is rejected rather than
	// silently shortened.
	MaxTimeoutSeconds int `json:"max_timeout_seconds,omitempty"`

	// MaxAttempts caps how many times one wait may call its target.
	// Default 60.
	MaxAttempts int `json:"max_attempts,omitempty"`
}

WaitAndVerifyConfig bounds the wait_and_verify poll loop (#648). Zero values mean the built-in defaults; see pkg/tools.

Jump to

Keyboard shortcuts

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