Typed config section adapters for module extraction¶
- Authors
- Matt Cockayne, Codex (AI drafting assistant)
- Date
- 7 July 2026
- Status
- IMPLEMENTED
- Builds on
2026-07-07-slog-first-extraction-seams.md- Related
2026-07-07-package-extraction-report.md
1. Context¶
pkg/config is one of GTB's core framework packages. It wraps Viper with
file/env/default precedence, afero-backed loading, env-prefix binding,
multi-file merging, embedded defaults, hot reload, observers, schema validation,
pflag binding, and testable filesystem behaviour.
Many packages proposed for extraction currently depend on pkg/config directly
or indirectly. That makes it tempting to extract pkg/config first and use it
as the shared configuration abstraction for every future module.
That would solve one coupling problem by creating another. Standalone modules should not have to import GTB's full runtime config stack just to receive a few settings. A package that needs a timeout, endpoint, token reference, or TLS paths should not care whether the host application uses GTB config, envconfig, Koanf, JSON, flags, Kubernetes config maps, or hard-coded test values.
This spec defines the decoupling pattern:
- extracted packages own typed configuration structs,
- GTB's config container unmarshals resolved sections into those structs,
- GTB adapter code performs existence checks, default merging, validation, and credential-resolution wiring,
- extracted packages receive typed values and remain config-system agnostic.
2. Problem Statement¶
config.Containable is useful inside GTB but too broad as a dependency seam for
extracted modules.
Current Containable includes:
type Containable interface {
Get(key string) any
GetBool(key string) bool
GetInt(key string) int
GetFloat(key string) float64
GetString(key string) string
GetTime(key string) time.Time
GetDuration(key string) time.Duration
GetViper() *viper.Viper
BindPFlag(key string, flag *pflag.Flag) error
Has(key string) bool
IsSet(key string) bool
Set(key string, value any)
WriteConfigAs(dest string) error
ConfigFiles() []string
Sub(key string) Containable
AddObserver(o Observable)
AddObserverFunc(f func(Containable) error)
OnReloadError(f func(error))
ToJSON() string
Dump(w io.Writer)
Validate(schema *Schema) *ValidationResult
}
Most extraction candidates need only a small typed subset of this information.
If they import pkg/config, they also inherit Viper and GTB-specific runtime
behaviour.
3. Goals¶
- Make typed package-owned config structs the primary configuration boundary for reusable and extracted packages.
- Add first-class config-container APIs for unmarshalling resolved config sections into typed structs.
- Add first-class observer-backed binding APIs so GTB adapters can rehydrate typed settings automatically on successful config reloads.
- Preserve GTB's config precedence and env-prefix semantics in adapter code.
- Make section existence explicit so adapters can distinguish absent config, present-but-empty config, and invalid config.
- Keep typed package-owned structs the primary boundary so modules can be
built without
pkg/config, and do not require it. Note thatpkg/configis itself an early planned extraction (it already works standalone); once it is its own lightweight module, a package may import it directly where there is a genuine use case. The binding constraint is severing coupling to GTB's main-module weight — chieflypkg/propsand the composition layer — not topkg/configitself. - Provide a package-by-package migration plan for extraction candidates.
- Treat documentation as a first-class deliverable for every migrated package, including component docs, concept docs, migration notes, and generated-project guidance where applicable.
- Keep
pkg/configextractable later as its own optional CLI config module, but not as a prerequisite or universal abstraction for other modules.
4. Non-goals¶
- Do not extract
pkg/configin this spec. - Do not remove existing
config.ContainableAPIs immediately. - Do not force every package to use config structs if the package has no config.
- Do not make GTB's main module — chiefly
pkg/propsand the composition glue — a dependency of extracted modules.pkg/configis exempt: it is itself an early extraction candidate and an acceptable lightweight dependency, so this spec does not forbid packages from importing it. - Do not change user-facing config keys in this spec unless a later package migration explicitly requires it.
5. Design Principles¶
5.1 Extracted modules own their config shape¶
The extracted module defines the struct it needs:
package chat
type OpenAIConfig struct {
BaseURL string `mapstructure:"base_url" json:"base_url" yaml:"base_url"`
Model string `mapstructure:"model" json:"model" yaml:"model"`
Timeout time.Duration `mapstructure:"timeout" json:"timeout" yaml:"timeout"`
}
func NewOpenAIClient(cfg OpenAIConfig, opts ...Option) (*OpenAIClient, error)
GTB owns how that struct is populated:
section, err := config.UnmarshalSection[chat.OpenAIConfig](props.Config, "openai")
if err != nil {
return nil, err
}
client, err := chat.NewOpenAIClient(section.Value, chat.WithLogger(log))
This keeps the extracted module independent of GTB while still allowing GTB to provide its rich config system to applications built on the framework.
5.2 Typed options beat lookup interfaces¶
Lookup interfaces such as GetString(key string) are useful transitional seams,
but they should not be the default extraction design. Typed config structs are
preferable because they:
- document the package's configuration contract in code,
- allow package-specific validation,
- avoid stringly-typed config key access inside extracted modules,
- make tests simpler,
- make default merging explicit,
- allow non-GTB consumers to populate config any way they like.
Lookup interfaces remain acceptable for provider registries or compatibility adapters where dynamic provider-specific keys are unavoidable.
5.3 Existence is separate from value¶
Adapters often need to know whether a section exists, not just what values unmarshal from it. This matters for:
- optional provider blocks,
- "configured" checks in setup/init flows,
- fallback-to-default behaviour,
- preserving existing config during migration,
- deciding whether a missing token is an error or merely means "try env".
The config API should expose this explicitly.
5.4 Config loading stays in GTB composition¶
Extracted modules should receive already-resolved typed values. GTB remains responsible for:
- file paths,
- embedded defaults,
- environment variable precedence,
- pflag binding,
- config hot reload,
- observer-backed rehydration of typed settings,
- config file writes,
- user-facing migration commands,
- config schema validation at application startup.
6. Proposed Config API Additions¶
6.1 Section result type¶
Add a reusable result type to pkg/config:
Exists means the section or key is present in the resolved configuration view
according to the chosen existence policy. The zero value means absent.
6.2 Unmarshal APIs¶
Add APIs to Containable and Container:
type Containable interface {
// existing methods...
Unmarshal(target any) error
UnmarshalKey(key string, target any) error
SectionExists(key string) bool
}
Add generic helpers:
func UnmarshalSection[T any](cfg Containable, key string) (Section[T], error)
func MustUnmarshalSection[T any](cfg Containable, key string) Section[T]
MustUnmarshalSection should be used sparingly, mostly in tests or package
defaults where panics are acceptable. Most production code should use
UnmarshalSection.
6.3 Observer-backed section binding¶
Long-lived GTB adapters must not perform a one-shot unmarshal and then leave runtime components with stale settings. Add a shared helper that performs the initial unmarshal and registers a config observer for future successful reloads:
type ObservedSection[T any] struct {
// unexported snapshot/observer state
}
// Value returns the latest complete settings snapshot.
func (s *ObservedSection[T]) Value() T
// Current returns a pointer to the latest immutable settings snapshot.
// Callers must treat the returned value as read-only and must call Current
// again when they need to observe a later reload.
func (s *ObservedSection[T]) Current() *T
// Exists reports whether the latest snapshot came from an explicit section.
func (s *ObservedSection[T]) Exists() bool
// Version increments only when a successful reload changes the typed section.
func (s *ObservedSection[T]) Version() uint64
type SectionChange[T any] struct {
Previous Section[T]
Current Section[T]
Initial bool
Changed bool
Version uint64
}
type SectionBindingOption[T any] func(*SectionBindingConfig[T])
func ObserveSection[T any](
cfg Containable,
key string,
opts ...SectionBindingOption[T],
) (*ObservedSection[T], error)
ObserveSection is the default GTB adapter API for config-backed long-lived
components. It must:
- unmarshal the initial resolved section,
- merge package defaults when configured by an option,
- validate the typed settings when configured by an option,
- store a complete immutable settings snapshot,
- register an observer with
AddObserverFunc, - rehydrate and validate a fresh snapshot after each successful reload,
- compare the previous and current typed snapshots as complete structs,
- atomically replace the published snapshot and increment the version only when the typed section changed, and
- invoke an optional apply callback with
SectionChange[T]so components with live reload support can reconfigure immediately without hand-checking individual fields.
Recommended options:
func WithSectionDefaults[T any](defaults T, merge func(defaults, overlay T) T) SectionBindingOption[T]
func WithSectionDefaultFunc[T any](defaultFunc func(Containable) T, merge func(defaults, overlay T) T) SectionBindingOption[T]
func WithSectionValidator[T any](validate func(T) error) SectionBindingOption[T]
func WithSectionEqual[T any](equal func(previous, current Section[T]) bool) SectionBindingOption[T]
func WithSectionApply[T any](apply func(SectionChange[T]) error) SectionBindingOption[T]
Adapters for short-lived one-shot commands may still call UnmarshalSection
directly, but service constructors, clients, transports, health checks, and any
component that survives config reload must use ObserveSection or a package
specific wrapper built on top of it.
Reload-aware extracted packages should define their own tiny settings-source interfaces when they need to read the latest snapshot directly:
*config.ObservedSection[package.Settings] can satisfy that interface without
the extracted package importing pkg/config.
6.4 Existence policy¶
SectionExists(key) should answer "is there meaningful configuration for this
section?" without requiring callers to inspect Viper directly.
Recommended semantics:
truewhenIsSet(key)is true,truewhenSub(key)returns a non-nil structural subtree,truewhen any known nested key underkeyis set by file/env/flag/default,falsewhen no value, no subtree, and no nested key exists.
Implementation should be careful with Viper defaults. A default-only section may need to count as existing for runtime option materialisation, but setup "is configured" checks may need file/env/keychain presence only. If both semantics are required, expose two methods:
SectionExists(key string) bool // resolved view, includes defaults/env
SectionInConfig(key string) bool // persisted file/config-source view
Open question: whether both should ship in the first implementation.
6.5 Unmarshal source¶
UnmarshalKey must preserve GTB's env-aware resolution. Viper's native Sub
can drop AutomaticEnv behaviour; GTB's Sub deliberately works around this for
typed getters. The unmarshal implementation must not regress that behaviour.
Implementation options:
- Use Viper's
UnmarshalKeyagainst the root resolver after ensuring env bindings/defaults are visible. - Build a map by walking the target struct fields and resolving values via typed getters.
- Use
AllSettingsonly as a base and overlay env/flag values for fields.
This is the highest-risk implementation detail. The tests must prove env-prefix values override file values during section unmarshal.
6.6 Tags¶
Package config structs should use mapstructure tags as the primary decode
tags, with json/yaml tags where useful for docs and examples:
type TLSConfig struct {
Enabled bool `mapstructure:"enabled" json:"enabled" yaml:"enabled"`
Cert string `mapstructure:"cert" json:"cert" yaml:"cert"`
Key string `mapstructure:"key" json:"key" yaml:"key"`
}
Adapters should avoid exposing Viper-specific tags in public documentation; they are an implementation detail of GTB's adapter.
6.7 Defaults and validation¶
Each extracted module should expose either:
or constructor behaviour that clearly documents defaults and validation errors.
GTB adapters should generally:
- start from module defaults,
- unmarshal the GTB section,
- overlay explicit runtime options,
- resolve credentials/secrets,
- call module validation,
- construct the module object.
7. Adapter Pattern¶
7.1 Package adapter shape¶
For each package that needs GTB configuration, create an adapter in GTB, close to the framework integration point. Prefer unexported helpers unless downstream GTB applications need to reuse them.
Example:
func openAIConfigFromGTB(cfg config.Containable) (chat.OpenAIConfig, bool, error) {
section, err := config.UnmarshalSection[chat.OpenAIConfig](cfg, "openai")
if err != nil {
return chat.OpenAIConfig{}, false, err
}
out := chat.DefaultOpenAIConfig()
if section.Exists {
out = out.Merge(section.Value)
}
return out, section.Exists, out.Validate()
}
7.2 Constructor shape in extracted modules¶
Constructors should accept typed config plus functional options for dependencies and runtime objects:
func New(cfg Config, opts ...Option) (*Client, error)
func WithLogger(log *slog.Logger) Option
func WithHTTPClient(client *http.Client) Option
func WithCredentialResolver(resolver CredentialResolver) Option
Configuration values are data. Dependencies are options.
7.3 Hot reload¶
Hot reload should remain a GTB concern. Extracted modules should not import GTB's observer system.
GTB adapters for long-lived components must use config observers to rehydrate
typed settings whenever a reload succeeds. This is an adapter contract, not an
optional enhancement. The preferred implementation is config.ObserveSection,
or a package-specific helper that wraps ObserveSection with package defaults,
validation, and apply behaviour.
The adapter is responsible for unmarshalling a fresh settings snapshot from the
updated config.Containable, validating it, and then propagating it to the
runtime component. Direct one-shot calls to config.UnmarshalSection are only
appropriate for short-lived command paths, tests, or the implementation of the
observer-backed helper itself.
For components that support live reconfiguration, adapters should propagate the latest settings through pointers rather than copying values into long-lived closures. The preferred pattern is either:
- a package-owned settings-source interface, such as
Current() *Settings, backed byconfig.ObserveSection, where the package documents its concurrency guarantees, or - a GTB-owned pointer/holder that the adapter updates on reload before invoking
a package-specific
Reconfiguremethod.
Avoid mutating shared structs in place without synchronization. If the settings
may be read concurrently by request handlers, health checks, transports, or
background goroutines, use a mutex, atomic.Pointer[T], or an equivalent
package-owned concurrency boundary so readers always observe a consistent
settings snapshot.
binding, err := config.ObserveSection[http.Settings](
cfg,
"server.http",
config.WithSectionDefaults(http.DefaultSettings(), http.MergeSettings),
config.WithSectionValidator(func(settings http.Settings) error {
return settings.Validate()
}),
config.WithSectionApply(func(change config.SectionChange[http.Settings]) error {
return server.Reconfigure(&change.Current.Value)
}),
)
if err != nil {
return err
}
server.SetSettingsSource(binding)
Only packages that genuinely support live reconfiguration should expose reload
methods. Otherwise GTB should document that changes require restart and the
adapter should still use ObserveSection to keep a rehydrated snapshot
available for newly constructed components, but must not pretend that an
already-running component applies those changes live.
8. Package-by-Package Boundary Plan¶
This section covers the packages scored above 5 in the extraction report.
pkg/chat¶
Current config coupling:
- Reads provider selection from
ai.provider. - Reads fallback settings from
ai.fallback.*. - Reads provider-specific API keys, env refs, keychain refs, model, base URL, and other provider settings.
- Uses
pkg/configdirectly in OpenAI credential resolution and elsewhere.
Extracted module shape:
type Config struct {
Provider Provider `mapstructure:"provider"`
Model string `mapstructure:"model"`
}
type Settings struct {
Config Config
Logger logger.Logger
}
type FallbackConfig struct {
Enabled bool `mapstructure:"enabled"`
Providers []Provider `mapstructure:"providers"`
}
type OpenAIConfig struct {
APIKey string `mapstructure:"api_key"`
BaseURL string `mapstructure:"base_url"`
Model string `mapstructure:"model"`
}
GTB adapter responsibilities:
- Unmarshal
ai,ai.fallback,openai,anthropic, andgeminisections. - Preserve existing GTB config keys and credential cascade.
- Convert env/keychain/literal credential references into a
CredentialResolver. - Inject GTB's hardened HTTP client if desired.
- Keep
New,NewWithFallbackSettings, andNewFallbackFromSettingson package-owned settings; keep GTB config and props integration inSettingsFromProps,NewFromProps,NewWithFallback, andNewWithFallbackFromProps. - Pass the logger through
Settingswhile the slog-first logger work continues.
Existence checks:
ai.providerabsence means use package or GTB default provider.- Provider section absence means use provider defaults plus env fallback.
- Credential presence checks remain in GTB because they know GTB's credential storage schema.
pkg/controls¶
Current config coupling: none.
Extracted module shape:
- No config struct required for the core controller unless restart/health policy defaults become configurable later.
- If needed, define
ControllerConfigwith restart policy, shutdown timeout, signal handling, and health intervals.
GTB adapter responsibilities:
- If GTB introduces config-driven controls defaults, unmarshal a
controls.Configfrom acontrolssection and pass it to the controller. - Keep service registration and transport health endpoint wiring outside the extracted core.
Existence checks:
- Not needed for first extraction wave.
pkg/redact¶
Current config coupling: none.
Extracted module shape:
- Existing functions can remain config-free.
- Optional future
Configmay include custom patterns or disabled defaults.
GTB adapter responsibilities:
- If GTB allows custom redaction rules in config, unmarshal them in telemetry or doctor adapters and pass explicit rule sets to redact.
Existence checks:
- Not needed unless custom user rules are introduced.
pkg/changelog¶
Current config coupling: none.
Extracted module shape:
- Existing generation options should remain explicit structs/options.
- If CLI defaults are config-driven, define
GenerateConfigin the changelog module rather than takingconfig.Containable.
GTB adapter responsibilities:
- CLI command maps flags/config into
changelog.GenerateConfig.
Existence checks:
- Not needed beyond command-level flag/config precedence.
pkg/authn¶
Current config coupling: none.
Extracted module shape:
type APIKeyConfig struct {
Header string `mapstructure:"header"`
Keys []string `mapstructure:"keys"`
}
type JWTConfig struct {
Issuer string `mapstructure:"issuer"`
Audience []string `mapstructure:"audience"`
JWKSURL string `mapstructure:"jwks_url"`
}
type MTLSConfig struct {
AllowedSubjects []string `mapstructure:"allowed_subjects"`
}
GTB adapter responsibilities:
- Transports unmarshal auth sections and construct middleware/verifiers.
- Credential references should be resolved by GTB before values reach authn.
Existence checks:
- Section absence means auth mode disabled unless explicitly required by server options.
- Present-but-invalid auth config is a startup error.
pkg/credentials, keychain, credtest¶
Current config coupling: none in the package. GTB setup/config migration code uses config heavily.
Extracted module shape:
type Reference struct {
Mode Mode `mapstructure:"mode"`
Env string `mapstructure:"env"`
Keychain string `mapstructure:"keychain"`
Value string `mapstructure:"value"`
}
GTB adapter responsibilities:
- Continue to own migration from literal config to env/keychain references.
- Unmarshal credential references from provider sections and pass
credentials.Referenceor aCredentialResolverto extracted modules.
Existence checks:
- Setup
IsConfiguredchecks need source-aware existence, not just resolved defaults. This is a prime use case forSectionInConfigor explicitHas/IsSetchecks in GTB adapters.
pkg/regexutil¶
Current config coupling: none.
Extracted module shape:
- Keep config-free.
- Optional future config may include max pattern length or compile timeout.
GTB adapter responsibilities:
- If configurable limits are introduced, unmarshal them where user patterns are accepted and pass explicit options to regexutil.
Existence checks:
- Not needed.
pkg/tls¶
Current config coupling:
- Imports
pkg/configto resolve TLS settings.
Extracted module shape:
type Pair struct {
Enabled bool `mapstructure:"enabled" yaml:"enabled" json:"enabled"`
Cert string `mapstructure:"cert" yaml:"cert" json:"cert"`
Key string `mapstructure:"key" yaml:"key" json:"key"`
}
type PairOverrides struct {
Enabled bool
Cert bool
Key bool
}
func ResolvePair(shared Pair, transport Pair, overrides PairOverrides) Pair
func (p Pair) ServerConfig(nextProtos ...string) (*tls.Config, error)
func ClientConfig(caFiles ...string) (*tls.Config, error)
GTB adapter responsibilities:
- Unmarshal
server.tls,server.http.tls,server.grpc.tls, or custom prefix sections into typedPairvalues. - Preserve fallback behaviour between shared and transport-specific TLS config.
- Resolve paths relative to GTB config/project conventions if needed.
Existence checks:
- Section absence means TLS disabled unless
Enabledis true through another source. - Present
enabled: truewithout required cert/key is a validation error.
pkg/vcs/release, releasetest¶
Current config coupling:
- Provider registry factory accepts
config.Containable. - Providers read host overrides, token refs, direct provider templates, filename patterns, and provider-specific parameters.
Extracted module shape:
type SourceConfig struct {
Type string `mapstructure:"type"`
Owner string `mapstructure:"owner"`
Repo string `mapstructure:"repo"`
Host string `mapstructure:"host"`
Params map[string]string `mapstructure:"params"`
}
type ProviderConfig struct {
Host string `mapstructure:"host"`
Token credentials.Reference `mapstructure:"token"`
Params map[string]string `mapstructure:"params"`
}
type ProviderFactory func(SourceConfig, ProviderConfig, ...Option) (Provider, error)
GTB adapter responsibilities:
- Convert
props.Tool.ReleaseSourceplus runtime config overrides intorelease.SourceConfig. - Unmarshal provider-specific sections (
github,gitlab,bitbucket,gitea,direct) into provider config structs. - Resolve credential refs through GTB's credential backend before constructing providers, or pass a credential resolver option.
Existence checks:
- Runtime
vcs.provideroverride remains a GTB adapter concern. - Provider section absence should not prevent public unauthenticated release lookups when a provider supports them.
pkg/vcs/repo and pkg/vcs/repo/aferobilly¶
Current config coupling:
repoimportspropsand readsprops.Configfor provider override, SSH key settings, and auth token resolution.aferobillyhas no config coupling.
Extracted module shape:
type Settings struct {
ReleaseSource release.ReleaseSourceConfig
Forge string
AuthEnabled bool
Auth vcs.TokenConfig
SSH SSHSettings
Logger diagnosticLogger
FS afero.Fs
}
type SSHSettings struct {
Configured bool
HasKey bool
Type string
Env string
Path string
}
GTB adapter responsibilities:
- Map
props.Tool.ReleaseSource,vcs.provider,github.ssh,gitlab.ssh, etc. intorepo.Settings. - Preserve token resolution through the narrow
vcs.TokenConfigsurface so env, keychain, literal, and fallback environment behaviour remains unchanged. - Keep
NewRepoFromPropsandSettingsFromContainableas GTB compatibility adapters whileNewRepoconsumes typed settings.
Existence checks:
- SSH section absence means use HTTPS/token or provider defaults.
type: agentshould not require a path.
pkg/browser¶
Current config coupling: none.
Extracted module shape:
type Config struct {
AllowedSchemes []string `mapstructure:"allowed_schemes"`
MaxURLLength int `mapstructure:"max_url_length"`
}
GTB adapter responsibilities:
- Only needed if GTB allows tool authors to override browser policy.
- Defaults should remain secure without config.
Existence checks:
- Not needed for current behaviour.
pkg/forms¶
Current config coupling: none.
Extracted module shape:
- Keep config-free.
- If defaults are needed later, use explicit
ThemeConfigorInteractionConfigstructs.
GTB adapter responsibilities:
- Setup commands choose interactive/non-interactive behaviour from GTB command flags/config and pass explicit options to forms.
Existence checks:
- Not needed in forms itself.
pkg/logger¶
Current config coupling: none.
Extracted module shape:
type Config struct {
Level string `mapstructure:"level"`
Format string `mapstructure:"format"`
Timestamp bool `mapstructure:"timestamp"`
Caller bool `mapstructure:"caller"`
}
GTB adapter responsibilities:
- Root command unmarshals
loginto logger config. - Construct Charm
slog.Handlerand sharedslog.LevelVar. - Preserve
--debugprecedence over config.
Existence checks:
- Absence means default human-friendly logger.
- Invalid level/format is a startup config error or warning according to current behaviour.
pkg/output¶
Current config coupling: none.
Extracted module shape:
GTB adapter responsibilities:
- Commands map
--output,--no-style, and config defaults into output renderer options. - Cobra writers remain the output destination.
Existence checks:
- Output flags usually override config; section absence means command defaults.
pkg/workspace¶
Current config coupling: none.
Extracted module shape:
GTB adapter responsibilities:
- Generator/internal commands may pass marker overrides explicitly if GTB ever makes them configurable.
Existence checks:
- Not needed for current behaviour.
pkg/http¶
Current config coupling:
- Adapters read server port, max header bytes, rate limit, circuit breaker, and
TLS paths from
config.Containable. - Core server construction now accepts typed
ServerSettings; compatibility adapters keep existing GTB config call sites working.
Extracted module shape:
type ServerSettings struct {
Port int `mapstructure:"port" yaml:"port" json:"port"`
MaxHeaderBytes int `mapstructure:"max_header_bytes" yaml:"max_header_bytes" json:"max_header_bytes"`
}
type ServerSettingsSource interface {
Current() *ServerSettings
Version() uint64
}
type ClientConfig struct {
Timeout time.Duration `mapstructure:"timeout"`
Retry RetryConfig `mapstructure:"retry"`
TLS tls.Config `mapstructure:"tls"`
CircuitBreaker CircuitBreakerConfig `mapstructure:"circuit_breaker"`
}
GTB adapter responsibilities:
- Unmarshal
server.httpintohttp.ServerSettings. - Bind long-lived HTTP composition with
ObserveServerSettingsFromConfig, which wrapsconfig.ObserveSectionand exposesCurrent()/Version(). - Preserve fallback from
server.http.*to sharedserver.*where currently supported. The first implemented fallback isserver.porttoserver.http.port, rehydrated on config reload via dynamic defaults. - Resolve TLS through typed TLS config structs.
- Keep controller registration and health endpoint wiring in GTB/transport integration code.
Existence checks:
- Section absence means use built-in server defaults.
- Explicit
port: 0should remain distinguishable from absent port if current behaviour allows ephemeral ports. - Existing servers do not auto-restart when an observed port changes; the observed source is for newly constructed servers or explicit package-level reconfiguration logic.
pkg/grpc¶
Current config coupling:
- Adapters read port, reflection, TLS, rate limit, circuit breaker, and dial
options from
config.Containable. - Core server construction, start, dial, and registration now accept typed
ServerSettingsand explicit TLS pairs; compatibility adapters keep existing GTB config call sites working.
Extracted module shape:
type ServerSettings struct {
Port int `mapstructure:"port" yaml:"port" json:"port"`
Reflection bool `mapstructure:"reflection" yaml:"reflection" json:"reflection"`
}
type ServerSettingsSource interface {
Current() *ServerSettings
Version() uint64
}
type ClientConfig struct {
Target string `mapstructure:"target"`
TLS tls.Config `mapstructure:"tls"`
}
GTB adapter responsibilities:
- Unmarshal
server.grpcintogrpc.ServerSettings. - Bind long-lived gRPC composition with
ObserveServerSettingsFromConfig, which wrapsconfig.ObserveSectionand exposesCurrent()/Version(). - Preserve fallback to shared server config where currently supported. The
first implemented fallback is
server.porttoserver.grpc.port, rehydrated on config reload via dynamic defaults. - Build
grpc.ServerOption/ dial options from typed config. - Resolve TLS through typed TLS config structs.
Existence checks:
- Reflection default must preserve existing generated-tool behaviour.
- TLS/auth sections should be optional unless enabled.
- Existing servers do not auto-restart when an observed port changes; the observed source is for newly constructed servers or explicit package-level reconfiguration logic.
pkg/gateway¶
Current config coupling:
- Adapters compose HTTP/gRPC settings through transport packages.
- Core gateway construction accepts a prepared gRPC client connection and typed HTTP server settings; compatibility adapters keep existing GTB config call sites working.
Extracted module shape:
type Settings struct {
HTTP http.ServerSettings `mapstructure:"http" yaml:"http" json:"http"`
HTTPTLS tls.Pair `mapstructure:"http_tls" yaml:"http_tls" json:"http_tls"`
GRPC grpc.ServerSettings `mapstructure:"grpc" yaml:"grpc" json:"grpc"`
GRPCTLS tls.Pair `mapstructure:"grpc_tls" yaml:"grpc_tls" json:"grpc_tls"`
}
type SettingsSource interface {
Current() *Settings
Version() uint64
}
GTB adapter responsibilities:
- Compose already-typed HTTP and gRPC settings with
SettingsFromConfig. - Bind long-lived gateway composition with
ObserveSettingsFromConfig, which wrapsconfig.ObserveSectionand exposesCurrent()/Version(). - Register generated gateway handlers.
Existence checks:
- Gateway absence means not registered.
- Existing gateway servers and gRPC client connections do not auto-restart or redial when observed settings change; the observed source is for newly constructed gateways or explicit package-level reconfiguration logic.
pkg/openapi¶
Current config coupling: none directly; depends on HTTP.
Extracted module shape:
type Config struct {
SpecPath string `mapstructure:"spec_path"`
Mount string `mapstructure:"mount"`
UI bool `mapstructure:"ui"`
}
GTB adapter responsibilities:
- Mount OpenAPI handlers on typed HTTP server config.
Existence checks:
- Absence means no OpenAPI route unless explicitly registered.
pkg/telemetry/otelcore, logs, metrics, tracing¶
Current config coupling:
otelcorereadstelemetry.*endpoint, headers, insecure flags, and signal-specific overrides fromconfig.Containable.
Extracted module shape:
type OTelConfig struct {
Endpoint string `mapstructure:"endpoint"`
Insecure bool `mapstructure:"insecure"`
Headers map[string]string `mapstructure:"headers"`
Traces SignalConfig `mapstructure:"traces"`
Metrics SignalConfig `mapstructure:"metrics"`
Logs SignalConfig `mapstructure:"logs"`
}
type SettingsSource interface {
Current() *Settings
Version() uint64
}
GTB adapter responsibilities:
- Resolve shared
telemetry.*config plustelemetry.<signal>.*overrides into a package-ownedSettingsvalue. - Bind long-lived signal setup with
ObserveSettingsFromConfigso reloads rehydrate the full resolved signal snapshot and exposeVersion()for change detection. - Apply GTB service name/version/resource attributes from
props.Tooland version metadata.
Existence checks:
- Section absence means observability disabled unless enabled by env or code.
- Signal-specific sections override root OTel config.
- Existing exporters do not rebuild automatically on observer changes; tracing, metrics, and logs packages need explicit rebuild/restart logic if live reload should affect already constructed providers.
pkg/telemetry¶
Current config coupling:
- Root telemetry reads product analytics enablement, local-only mode, delivery
mode, backend config, data dir, deletion request settings, and tool metadata
through
propsand config.
Extracted module shape:
type Config struct {
Enabled bool `mapstructure:"enabled"`
LocalOnly bool `mapstructure:"local_only"`
Backend string `mapstructure:"backend"`
Endpoint string `mapstructure:"endpoint"`
Delivery string `mapstructure:"delivery"`
DataDir string `mapstructure:"data_dir"`
}
GTB adapter responsibilities:
- Map
telemetryconfig, org policy, tool metadata, version, machine ID, and data directory into telemetry config/options. - Keep operational observability provider setup on package-owned
ObservabilitySettings, withObservabilitySettingsFromPropsandSetupFromPropsas GTB adapters for existingtelemetry.*keys. - Keep telemetry data-directory resolution on a package-owned config-file path,
with
ResolveDataDirFromPropsas the GTB adapter. - Keep consent prompts and config persistence in GTB command/setup layers.
Existence checks:
- Must distinguish absent consent from explicit disabled/enabled.
- This likely needs source-aware checks rather than default-only resolution.
pkg/telemetry/posthog, pkg/telemetry/datadog¶
Current config coupling:
- Backend adapters do not use config directly but are constructed by root telemetry code using config-derived values.
Extracted module shape:
type PostHogConfig struct {
ProjectKey string `mapstructure:"project_key"`
Endpoint string `mapstructure:"endpoint"`
}
type DatadogConfig struct {
APIKey string `mapstructure:"api_key"`
Endpoint string `mapstructure:"endpoint"`
}
GTB adapter responsibilities:
- Unmarshal backend-specific sections and resolve secret refs.
- Pass standard
*http.Clientor backend options.
Existence checks:
- Backend section required only when selected backend is active.
pkg/vcs¶
Current config coupling:
- Token resolution reads env refs, literal values, and keychain refs from
config.Containable.
Extracted module shape:
type AuthConfig struct {
Env string `mapstructure:"env"`
Value string `mapstructure:"value"`
Keychain string `mapstructure:"keychain"`
}
GTB adapter responsibilities:
- Convert provider auth sections into auth config or credential references.
- Resolve keychain values with GTB credential backend.
Existence checks:
- Env ref presence can be configured even if env var is missing; adapter decides whether that is a warning or error in context.
pkg/vcs providers¶
Current config coupling:
- Providers read host/API URL overrides, token refs, filename patterns, direct download templates, and provider-specific params.
Extracted module shape:
type GitHubSettings struct {
ReleaseSource release.ReleaseSourceConfig
APIURL string `mapstructure:"url.api"`
UploadURL string `mapstructure:"url.upload"`
Auth vcs.AuthConfig `mapstructure:"auth"`
}
type GitLabSettings struct {
ReleaseSource release.ReleaseSourceConfig
APIURL string `mapstructure:"url.api"`
Auth vcs.AuthConfig `mapstructure:"auth"`
}
type GiteaSettings struct {
ReleaseSource release.ReleaseSourceConfig
APIURL string `mapstructure:"url.api"`
Auth vcs.AuthConfig `mapstructure:"auth"`
TokenFallbackEnv string
}
type BitbucketSettings struct {
ReleaseSource release.ReleaseSourceConfig
Username string `mapstructure:"username"`
AppPassword string `mapstructure:"app_password"`
FilenamePattern string `mapstructure:"filename_pattern"`
}
type DirectSettings struct {
ReleaseSource release.ReleaseSourceConfig
Token string `mapstructure:"token"`
}
GTB adapter responsibilities:
- Preserve current provider-specific config keys.
- Resolve credentials.
- Pass standard HTTP clients or extracted transport clients.
Existence checks:
- Provider configs are optional unless the selected provider requires a field.
pkg/errorhandling¶
Current config coupling: none directly; it depends on logger and help config
from props.Tool.
Extracted module shape:
GTB adapter responsibilities:
- Continue to map
props.Tool.Helpinto error handler construction. - If help channels become runtime-configurable, unmarshal them in root/setup.
Existence checks:
- Absence means no help footer.
9. Config Package Work¶
Phase 1: Add typed unmarshal support¶
- Add
Unmarshal,UnmarshalKey, andSectionExiststoContainable. - Implement those methods on
Containerand sub-containers. - Add generic
Section[T]andUnmarshalSection[T]. - Add
ObservedSection[T],ObserveSection[T], and binding options for defaults, validation, and apply callbacks. - Decide whether
SectionInConfigis needed in phase 1.
Phase 2: Prove env-aware unmarshalling¶
- Add tests where file config provides one value and prefixed env vars override
nested fields during
UnmarshalKey. - Add tests for sub-container unmarshalling retaining env binding.
- Add tests for absent section, present empty section, present invalid section, and default-only section behaviour.
- Add tests proving
ObserveSectionperforms the initial unmarshal, registers an observer, rehydrates fresh typed settings after reload, preserves the prior valid snapshot when validation fails, and invokes apply callbacks only after a successful rehydrate.
Phase 3: Introduce package config structs¶
- Add typed config structs to first-wave packages that currently read
config.Containabledirectly:chat,tls,http,grpc,vcs/release,vcsproviders,telemetry/otelcore. - Keep constructors additive and backwards compatible where public APIs already exist.
- Update the component documentation for each package in the same package commit, showing the new typed settings structs, default behaviour, validation rules, and adapter boundary.
Phase 4: Move GTB config reads into adapters¶
- Replace direct package-internal
cfg.GetString/cfg.GetBoolreads with adapter code that unmarshals typed structs. - Use
ObserveSectionin adapters for long-lived services, clients, transports, health checks, and reload-aware runtime components. - Keep existing command/setup config keys.
- Add migration notes only if any public constructor names or config semantics change.
- Update concept and how-to documentation in the same package commit when the user-facing construction or configuration story changes.
Phase 5: Documentation consolidation¶
- Add or update concept docs explaining config as a framework adapter boundary,
including why extracted modules own typed settings instead of importing
pkg/config. - Add or update component docs for
pkg/configto coverUnmarshalSection,ObserveSection, existence checks, defaults, validation, and reload semantics. - Update generator documentation and generated-project docs whenever scaffolded code changes to use typed settings or config adapters.
- Add migration notes under
docs/migration/for every public API change, including pre-1.0 intentional breaks, so the eventual v1 migration guide has a complete audit trail. - Update the active extraction specs to reference the typed settings and
observer-backed adapter pattern instead of
config.Containable. - Cross-check examples against compiling code before marking a package phase complete.
Phase 6: Extraction readiness checks¶
- Add import-boundary tests or CI checks for packages that should no longer
import
pkg/config. - Update extraction specs to reference typed config structs rather than
ConfigLookupas the primary seam. - Treat missing or stale docs as an extraction-readiness failure even when code and tests pass.
10. TDD / BDD Strategy¶
Implementation must be test-first.
TDD requirements:
- Write config package tests for
UnmarshalKey,UnmarshalSection, existence semantics, env-prefix precedence, sub-container behaviour, observer-backed rehydration, invalid-reload preservation, apply callbacks, and error handling before implementation. - For each migrated package, write tests for typed config defaults, validation, and GTB adapter mapping before replacing direct config reads.
- For compatibility wrappers, write tests that prove existing constructors still behave as before.
- Add import-boundary tests before removing
pkg/configimports from extracted candidates.
BDD requirements:
- Config section unmarshalling itself is library behaviour and should be covered by unit/integration tests, not broad BDD.
- Add or update BDD scenarios when a user-visible CLI workflow changes, such as
init,config migrate-credentials,serve docs, generated server startup, or provider setup. - Generator-affecting config changes need generator E2E coverage: scaffold a project, build it, and confirm generated config-backed components still start.
- If no BDD scenario is added for a phase, implementation notes must state why unit/integration coverage is sufficient.
11. Documentation¶
Documentation is part of the implementation contract for this refactor. A package migration is not complete until its documentation has been updated, reviewed against the code, and included in the same package commit or a clearly paired documentation commit.
Required documentation updates:
docs/explanation/components/config/index.mddocs/explanation/components/config/sources-and-precedence.mddocs/explanation/components/config/validation.mddocs/components/<package>.mdor the package's existing component page for every migrated package.- The package-level
doc.gofile for every migrated package, so pkg.go.dev describes the typed settings boundary, GTB adapter helpers, and reload behaviour accurately. docs/concepts/pages that describe framework composition, dependency injection, service lifecycle, credentials, telemetry, or provider setup when those concepts change.docs/how-to/guides when downstream users need to change construction, configuration, testing, or generated-code patterns.docs/migration/notes for every public constructor/signature/config semantic change, including intentional pre-1.0 breaks.- Generator templates and generated-project docs when scaffolded projects need
to use typed settings or
*FromContainableadapters. docs/development/dependency-management.mddocs/development/specs/2026-07-05-chat-module-extraction.mddocs/development/specs/2026-07-07-slog-first-extraction-seams.md
Documentation must explain:
- config loading remains a GTB framework concern,
- extracted modules define typed config structs,
- GTB adapters unmarshal resolved config sections into those structs,
- long-lived GTB adapters use observer-backed rehydration through
ObserveSectionor package-specific wrappers, Existscontrols optional sections and setup/configured checks,- non-GTB consumers can populate structs with any config system,
- which config keys remain unchanged for GTB users,
- which constructors are the long-term typed settings API,
- which
*FromContainablehelpers are framework adapters, - how reload behaves for packages that apply changes live versus packages that require restart,
- how tests should stay on typed settings and keep config-container coverage in adapter tests.
Per-package documentation checklist:
- Document the package-owned settings structs and defaults.
- Document validation errors and required fields.
- Document the GTB adapter function names and the config sections they read.
- Document whether the package supports live reconfiguration, restart-only reconfiguration, or no reload semantics.
- Update package-level
doc.gocomments so generated API documentation matches the component docs. - Include at least one config-backed GTB example and one config-free typed settings example where the package is likely to be consumed outside GTB.
- Update examples after generator changes by scaffolding a project and checking generated code against the docs.
Documentation review requirements:
- Every package commit must state which docs were updated or why no docs changed.
- Examples must use current exported names and compile where practical.
- Stale references to
config.Containableas the primary package boundary must be removed or explicitly marked as GTB adapter compatibility. - The extraction report and active extraction specs must stay aligned with the implemented package boundaries.
- Missing documentation blocks progression to the next extraction-candidate package unless the gap is explicitly recorded as deferred in this spec or a follow-up issue.
12. Open Questions¶
Resolved during the first-wave implementation review (2026-07-10).
SectionExistsvsSectionInConfig. Resolved: shipSectionExistsonly. The resolved-view semantics (file/env/flag/default) cover every first-wave adapter, and the one source-aware "is configured" case (telemetry consent) is served by directIsSet/Haschecks in its adapter.SectionInConfigis deferred until a package genuinely needs persisted-source-only presence; it can be added without breaking callers.UnmarshalKeyonContainablevs helpers. Resolved: added directly toContainable(Unmarshal,UnmarshalKey,SectionExists), with the genericUnmarshalSection[T]/ObserveSection[T]helpers layered on top. The interface widening is justified because these are the canonical section-decode primitives every adapter builds on.- Tag strategy. Resolved:
mapstructureprimary, plusjson/yamlfor documentation and serialisation. Structs that are not decoded viaUnmarshalSection(see §7 note below) must not carrymapstructuretags that imply a decode contract they do not honour — dotted tags such asurl.apiare a footgun and were removed. - Adapter location. Resolved: inside each package, in a clearly named
config_adapter.go(and*FromProps/*FromContainablehelpers). This keeps the GTB coupling co-located with the package it adapts and trivially separable at extraction time, and is now enforced by a source-level guard test (test/architecture) that keepspkg/propsout of package cores. pkg/configextraction name. Still open, deferred to thepkg/configextraction spec itself. Not a prerequisite for this work.
Note on decode mechanism (recorded from the review)¶
Not every adapter uses UnmarshalSection, and that is intentional. Per §5.2,
lookup-style resolution remains correct for:
- provider registries with dynamic, provider-specific keys — the
pkg/vcsproviders decouple through a narrow localrelease.Config/vcs.TokenConfiginterface (which imports no GTB packages at all, the cleanest possible seam) rather than typed decode; - override-precedence resolution —
pkg/tlsandtelemetry/otelcoreresolve shared-plus-transport/signal settings using per-fieldIsSetoverride detection, which a single typed decode cannot express; - multi-prefix composition —
pkg/gatewaycomposes settings that live at different config prefixes, so itsSettingsis assembled by the adapter, not decoded from one section.
UnmarshalSection/ObserveSection are the default for self-contained sections
(chat, http, grpc server settings). The structs used by the lookup-based
adapters carry doc-only json/yaml tags and no mapstructure decode tags.
First-wave status (2026-07-10)¶
Landed in this MR: the config API additions (Phases 1–2), first-wave package
adapters (chat, tls, http, grpc, gateway, telemetry/otelcore,
vcs + providers + repo) with props coupling confined to adapter files, the
supporting docs (Phase 5), and the props import-boundary guard (part of Phase
6). vcs/release received a narrowed registry factory signature rather than a
full typed adapter, since its providers own the typed settings.
Deferred to later waves: the remaining §8 packages that currently need no config
or belong to a later extraction wave (authn, credentials, output,
browser, workspace, forms, changelog, redact, openapi,
errorhandling, and the telemetry backends).
Completion (2026-07-12)¶
This spec is marked IMPLEMENTED. The pattern it set out to establish — typed,
package-owned config structs as the primary boundary, with GTB unmarshalling
resolved sections through co-located adapters — is fully delivered and in use by
every first-wave package that reads config. The container APIs (Unmarshal,
UnmarshalKey, SectionExists, UnmarshalSection[T], ObserveSection[T]), the
test/architecture import-boundary guard, and all §12 open questions are
resolved.
The "deferred" §8 packages do not represent pending work under this spec:
- No-config packages (
authn,credentials,output,browser,workspace,forms,changelog,redact,regexutil,openapi,logger,controls) haveCurrent config coupling: none. There is nothing to migrate. Their §8 entries are forward-looking guidance for if and when each package is extracted, at which point the typed-struct +*FromContainableadapter pattern defined here applies directly.errorhandlingcouples to config only indirectly through help config and logger, and follows the same rule at its own extraction time. - The telemetry backends and root
pkg/telemetryare not blocked on a config adapter — root telemetry's config reads are already adapter-confined and itspkg/propstype coupling was severed via2026-07-10-telemetry-props-decoupling.md. What remains is the larger product-analytics ↔ observability split, which is an architectural effort rather than a config-boundary task and is tracked in its own spec:2026-07-12-telemetry-analytics-observability-split.md.
Follow-up landed on this branch (2026-07-11)¶
- Phase 6 automated import-boundary check — done. The guard in
test/architecture/boundary_test.gois now table-driven overpkg/propsandpkg/loggeracross every first-wave core pluspkg/telemetry. It runs in CI via the existinggo test ./...MR component; no separate job is required. - Root
pkg/telemetryprops-type coupling — done. Its config reads were already adapter-confined; the last coupling was toprops.EventType/props.DeliveryMode, resolved by relocating those types to the dependency-freepkg/telemetrytypesleaf under2026-07-10-telemetry-props-decoupling.md(IMPLEMENTED).pkg/propsretains the names as aliases, so downstream tools are unaffected. The larger product-analytics ↔ observability split needed for full telemetry extraction remains a separate, later effort.
Joint delivery with the slog-first spec (2026-07-10)¶
The 2026-07-07-slog-first-extraction-seams.md
spec was approved on 2026-07-10 and is delivered on this same branch so the
logging and config seams land as one cut-over. Consequences for this spec:
loggergains its typedConfig(this spec, §8) and its logging boundary is redefined to mirror*slog.Logger(slog-first, D1). The two are complementary:logger.Config.CharmOptions()feeds the slog-firstNewCharmHandlerconstruction.- The slog-first spec's config strategy (its §7) explicitly defers to this
spec: typed
UnmarshalSectionstructs are the primary boundary andpkg/configis an acceptable lightweight dependency (D6).
13. Acceptance Criteria¶
pkg/configcan unmarshal resolved, env-aware config sections into typed structs.- Adapters can distinguish absent sections from present invalid sections.
- First-wave extraction candidates have package-owned typed config structs or a documented reason they need no config.
- New extraction specs use typed config structs as the primary config boundary.
- Extracted modules are not required to import
pkg/config. - Existing GTB config keys and precedence semantics remain intact.