Files
kandev/apps/backend/internal/common/config/catalog.go
T

262 lines
18 KiB
Go

package config
import (
"fmt"
"sort"
"strings"
)
// SettingSource identifies the source that supplied a resolved startup value.
type SettingSource string
const (
SourceDefault SettingSource = "default"
SourceProfile SettingSource = "profile"
SourceConfiguration SettingSource = "configuration"
SourceEnvironment SettingSource = "environment"
SourceDatabase SettingSource = "database"
SourceFlag SettingSource = "flag"
)
// CatalogEntry describes one stable operator-facing startup setting.
//
// EnvVars is ordered by compatibility priority. The first non-empty variable
// wins when more than one alias is present. Internal aliases are included when
// a launcher or child process must use the same typed setting.
type CatalogEntry struct {
Key string
EnvVars []string
Owner string
Default string
Sensitive bool
Description string
}
// CatalogExclusion records an environment variable that is intentionally not
// exposed as a public YAML setting.
type CatalogExclusion struct {
EnvVar string
Class string
Reason string
}
// Office launch-safety catalog keys, shared with yamlOnlyStartupKeys
// (source.go) and clampOfficeLaunchSafetyConfig (source.go).
const (
officeMaxConcurrentInstanceKey = "office.maxConcurrentInstance"
officeMaxConcurrentWorkspaceKey = "office.maxConcurrentWorkspace"
officeWorkspaceBudgetPerHourKey = "office.workspaceBudgetPerHour"
officeRoutineBudgetPerHourKey = "office.routineBudgetPerHour"
officePromotionAgeMinutesKey = "office.promotionAgeMinutes"
officeMaxCausationDepthKey = "office.maxCausationDepth"
officeSelfTriggerAllowanceKey = "office.selfTriggerAllowance"
officeSelfTriggerTotalAllowanceKey = "office.selfTriggerTotalAllowance"
officeGateFailureThresholdKey = "office.gateFailureThreshold"
)
var startupCatalog = []CatalogEntry{
{Key: "homeDir", EnvVars: []string{"KANDEV_HOME_DIR"}, Owner: "common/config", Default: "~/.kandev"},
{Key: "server.host", EnvVars: []string{"KANDEV_SERVER_HOST"}, Owner: "backend", Default: "0.0.0.0"},
{Key: "server.port", EnvVars: []string{"KANDEV_SERVER_PORT", "KANDEV_BACKEND_PORT", "KANDEV_PORT"}, Owner: "launcher/backend", Default: "38429"},
{Key: "server.readTimeout", EnvVars: []string{"KANDEV_SERVER_READTIMEOUT"}, Owner: "backend", Default: "30"},
{Key: "server.writeTimeout", EnvVars: []string{"KANDEV_SERVER_WRITETIMEOUT"}, Owner: "backend", Default: "30"},
{Key: "server.webInternalUrl", EnvVars: []string{"KANDEV_WEB_INTERNAL_URL"}, Owner: "launcher/backend", Default: ""},
{Key: "server.webTitlePrefix", EnvVars: []string{"KANDEV_WEB_TITLE_PREFIX"}, Owner: "backend", Default: ""},
{Key: "server.trustedProxies", EnvVars: []string{"KANDEV_TRUSTED_PROXIES"}, Owner: "backend", Default: ""},
{Key: "database.driver", EnvVars: []string{"KANDEV_DATABASE_DRIVER"}, Owner: "database", Default: "sqlite"},
{Key: "database.path", EnvVars: []string{"KANDEV_DATABASE_PATH"}, Owner: "launcher/backend", Default: "<home>/data/kandev.db"},
{Key: "database.host", EnvVars: []string{"KANDEV_DATABASE_HOST"}, Owner: "database", Default: "localhost"},
{Key: "database.port", EnvVars: []string{"KANDEV_DATABASE_PORT"}, Owner: "database", Default: "5432"},
{Key: "database.user", EnvVars: []string{"KANDEV_DATABASE_USER"}, Owner: "database", Default: "kandev"},
{Key: "database.password", EnvVars: []string{"KANDEV_DATABASE_PASSWORD"}, Owner: "database", Default: "", Sensitive: true},
{Key: "database.dbName", EnvVars: []string{"KANDEV_DATABASE_DBNAME"}, Owner: "database", Default: "kandev"},
{Key: "database.sslMode", EnvVars: []string{"KANDEV_DATABASE_SSLMODE"}, Owner: "database", Default: "disable"},
{Key: "database.maxConns", EnvVars: []string{"KANDEV_DATABASE_MAXCONNS"}, Owner: "database", Default: "25"},
{Key: "database.minConns", EnvVars: []string{"KANDEV_DATABASE_MINCONNS"}, Owner: "database", Default: "5"},
{Key: "nats.url", EnvVars: []string{"KANDEV_NATS_URL"}, Owner: "events", Default: "", Sensitive: true},
{Key: "nats.clusterId", EnvVars: []string{"KANDEV_NATS_CLUSTERID"}, Owner: "events", Default: "kandev-cluster"},
{Key: "nats.clientId", EnvVars: []string{"KANDEV_NATS_CLIENTID"}, Owner: "events", Default: "kandev-client"},
{Key: "nats.maxReconnects", EnvVars: []string{"KANDEV_NATS_MAXRECONNECTS"}, Owner: "events", Default: "10"},
{Key: "events.namespace", EnvVars: []string{"KANDEV_EVENTS_NAMESPACE"}, Owner: "events", Default: ""},
{Key: "docker.enabled", EnvVars: []string{"KANDEV_DOCKER_ENABLED"}, Owner: "docker", Default: "true"},
{Key: "docker.host", EnvVars: []string{"KANDEV_DOCKER_HOST", "DOCKER_HOST"}, Owner: "docker", Default: "platform socket"},
{Key: "docker.apiVersion", EnvVars: []string{"KANDEV_DOCKER_APIVERSION"}, Owner: "docker", Default: ""},
{Key: "docker.tlsVerify", EnvVars: []string{"KANDEV_DOCKER_TLSVERIFY"}, Owner: "docker", Default: "false"},
{Key: "docker.defaultNetwork", EnvVars: []string{"KANDEV_DOCKER_DEFAULTNETWORK"}, Owner: "docker", Default: "kandev-network"},
{Key: "docker.volumeBasePath", EnvVars: []string{"KANDEV_DOCKER_VOLUMEBASEPATH"}, Owner: "docker", Default: "platform default"},
{Key: "agent.standaloneHost", EnvVars: []string{"KANDEV_AGENT_STANDALONE_HOST"}, Owner: "agentctl", Default: "localhost"},
{Key: "agent.standalonePort", EnvVars: []string{"AGENTCTL_PORT", "KANDEV_AGENT_STANDALONE_PORT"}, Owner: "agentctl", Default: "39429"},
{Key: "auth.jwtSecret", EnvVars: []string{"KANDEV_AUTH_JWTSECRET"}, Owner: "auth", Default: "generated", Sensitive: true},
{Key: "auth.tokenDuration", EnvVars: []string{"KANDEV_AUTH_TOKENDURATION"}, Owner: "auth", Default: "3600"},
{Key: "auth.sessionTTLHours", EnvVars: []string{"KANDEV_AUTH_SESSIONTTLHOURS"}, Owner: "auth", Default: "720"},
{Key: "auth.cookieName", EnvVars: []string{"KANDEV_AUTH_COOKIE_NAME"}, Owner: "auth", Default: ""},
{Key: "logging.level", EnvVars: []string{"KANDEV_LOG_LEVEL"}, Owner: "logging", Default: "info"},
{Key: "logging.format", EnvVars: []string{"KANDEV_LOGGING_FORMAT"}, Owner: "logging", Default: "text"},
{Key: "repositoryDiscovery.roots", EnvVars: []string{"KANDEV_REPOSITORYDISCOVERY_ROOTS"}, Owner: "repository discovery", Default: "[]"},
{Key: "repositoryDiscovery.maxDepth", EnvVars: []string{"KANDEV_REPOSITORYDISCOVERY_MAXDEPTH"}, Owner: "repository discovery", Default: "5"},
{Key: "worktree.enabled", EnvVars: []string{"KANDEV_WORKTREE_ENABLED"}, Owner: "worktree", Default: "true"},
{Key: "worktree.defaultBranch", EnvVars: []string{"KANDEV_WORKTREE_DEFAULTBRANCH"}, Owner: "worktree", Default: "main"},
{Key: "worktree.cleanupOnRemove", EnvVars: []string{"KANDEV_WORKTREE_CLEANUPONREMOVE"}, Owner: "worktree", Default: "true"},
{Key: "worktree.fetchTimeoutSeconds", EnvVars: []string{"KANDEV_WORKTREE_FETCHTIMEOUTSECONDS"}, Owner: "worktree", Default: "60"},
{Key: "worktree.pullTimeoutSeconds", EnvVars: []string{"KANDEV_WORKTREE_PULLTIMEOUTSECONDS"}, Owner: "worktree", Default: "60"},
{Key: "repoClone.basePath", EnvVars: []string{"KANDEV_REPOCLONE_BASEPATH"}, Owner: "repository clone", Default: "<home>/repos"},
{Key: "debug.devMode", EnvVars: []string{"KANDEV_DEBUG_DEV_MODE"}, Owner: "debug", Default: "false"},
{Key: "debug.pprofEnabled", EnvVars: []string{"KANDEV_DEBUG_PPROF_ENABLED"}, Owner: "debug", Default: "false"},
{Key: "office.jwtSigningKey", EnvVars: []string{"KANDEV_OFFICE_JWTSIGNINGKEY"}, Owner: "office", Default: "random per start", Sensitive: true},
{Key: "office.schedulerTickMs", EnvVars: []string{"KANDEV_OFFICE_SCHEDULER_TICK_MS"}, Owner: "office", Default: "5000"},
{Key: officeMaxConcurrentInstanceKey, EnvVars: []string{"KANDEV_OFFICE_MAX_CONCURRENT_INSTANCE"}, Owner: "office", Default: "8"},
{Key: officeMaxConcurrentWorkspaceKey, EnvVars: []string{"KANDEV_OFFICE_MAX_CONCURRENT_WORKSPACE"}, Owner: "office", Default: "4"},
{Key: officeWorkspaceBudgetPerHourKey, EnvVars: []string{"KANDEV_OFFICE_WORKSPACE_BUDGET_PER_HOUR"}, Owner: "office", Default: "120"},
{Key: officeRoutineBudgetPerHourKey, EnvVars: []string{"KANDEV_OFFICE_ROUTINE_BUDGET_PER_HOUR"}, Owner: "office", Default: "20"},
{Key: officePromotionAgeMinutesKey, EnvVars: []string{"KANDEV_OFFICE_PROMOTION_AGE_MINUTES"}, Owner: "office", Default: "15"},
{Key: officeMaxCausationDepthKey, EnvVars: []string{"KANDEV_OFFICE_MAX_CAUSATION_DEPTH"}, Owner: "office", Default: "8"},
{Key: officeSelfTriggerAllowanceKey, EnvVars: []string{"KANDEV_OFFICE_SELF_TRIGGER_ALLOWANCE"}, Owner: "office", Default: "3"},
{Key: officeSelfTriggerTotalAllowanceKey, EnvVars: []string{"KANDEV_OFFICE_SELF_TRIGGER_TOTAL_ALLOWANCE"}, Owner: "office", Default: "8"},
{Key: officeGateFailureThresholdKey, EnvVars: []string{"KANDEV_OFFICE_GATE_FAILURE_THRESHOLD"}, Owner: "office", Default: "3"},
{Key: "githubCredentialBroker.publicBaseUrl", EnvVars: []string{"KANDEV_GITHUB_CREDENTIAL_BROKER_PUBLIC_BASE_URL"}, Owner: "github credential broker", Default: ""},
{Key: "tasks.preparationTimeout", EnvVars: []string{"KANDEV_TASK_PREPARATION_TIMEOUT"}, Owner: "task lifecycle", Default: "10m"},
{Key: "tasks.stallDetectionThreshold", EnvVars: []string{"KANDEV_TASK_STALL_DETECTION_THRESHOLD"}, Owner: "task lifecycle", Default: "2h"},
{Key: "credentials.file", EnvVars: []string{"KANDEV_CREDENTIALS_FILE"}, Owner: "credentials", Default: ""},
{Key: "limits.ghMaxConcurrent", EnvVars: []string{"KANDEV_GH_MAX_CONCURRENT"}, Owner: "subprocess admission", Default: "8"},
{Key: "limits.gitMaxConcurrent", EnvVars: []string{"KANDEV_GIT_MAX_CONCURRENT"}, Owner: "subprocess admission", Default: "12"},
{Key: "limits.lspMaxConnections", EnvVars: []string{"KANDEV_LSP_MAX_CONNECTIONS"}, Owner: "LSP gateway", Default: "8", Description: "Maximum active LSP server leases, including leases detached from a browser."},
{Key: "messageQueue.maxPerSession", EnvVars: []string{"KANDEV_QUEUE_MAX_PER_SESSION"}, Owner: "message queue", Default: "10"},
{Key: "agentctl.idleTimeout", EnvVars: []string{"KANDEV_ACP_IDLE_TIMEOUT"}, Owner: "agentctl", Default: "1h"},
{Key: "agentctl.idleReaperInterval", EnvVars: []string{"KANDEV_ACP_IDLE_REAPER_INTERVAL"}, Owner: "agentctl", Default: "1m"},
{Key: "agentctl.notificationQueueCapacity", EnvVars: []string{"KANDEV_ACP_NOTIF_QUEUE"}, Owner: "agentctl", Default: "131072"},
{Key: "agentctl.recoveryDeadline", EnvVars: []string{"KANDEV_ACP_RECOVERY_DEADLINE"}, Owner: "backend", Default: "30s"},
{Key: "agentctl.recoveryReadTimeout", EnvVars: []string{"KANDEV_ACP_RECOVERY_READ_TIMEOUT"}, Owner: "backend", Default: "2s"},
{Key: "agentctl.recoveryReadRetries", EnvVars: []string{"KANDEV_ACP_RECOVERY_READ_RETRIES"}, Owner: "backend", Default: "2"},
{Key: "agentctl.unownedPeriod", EnvVars: []string{"KANDEV_ACP_UNOWNED_PERIOD"}, Owner: "agentctl", Default: "10m"},
{Key: "agentctl.detachedEventLimit", EnvVars: []string{"KANDEV_ACP_DETACHED_EVENT_LIMIT"}, Owner: "agentctl", Default: "100"},
{Key: "planning.coalesceWindowMs", EnvVars: []string{"KANDEV_PLAN_COALESCE_WINDOW_MS"}, Owner: "planning", Default: "300000"},
{Key: "observability.otlpEndpoint", EnvVars: []string{"OTEL_EXPORTER_OTLP_ENDPOINT"}, Owner: "observability", Default: "", Sensitive: true},
{Key: "launcher.webPort", EnvVars: []string{"KANDEV_WEB_PORT"}, Owner: "launcher", Default: "automatic"},
{Key: "launcher.healthTimeoutMs", EnvVars: []string{"KANDEV_HEALTH_TIMEOUT_MS"}, Owner: "launcher", Default: "45000"},
{Key: "launcher.noBrowser", EnvVars: []string{"KANDEV_NO_BROWSER"}, Owner: "launcher", Default: "false"},
{Key: "executors.sshReachabilityIntervalSeconds", EnvVars: []string{"KANDEV_EXECUTORS_SSHREACHABILITYINTERVALSECONDS"}, Owner: "executors/reachability", Default: "60"},
}
var startupExclusions = []CatalogExclusion{
{EnvVar: InternalConfigFileEnv, Class: "internal wiring", Reason: "launcher-selected configuration file handoff"},
{EnvVar: InternalConfigHomeFileEnv, Class: "internal wiring", Reason: "launcher-selected home-file handoff"},
{EnvVar: InternalAgentctlStartupConfigEnv, Class: "internal wiring", Reason: "resolved agentctl child configuration handoff"},
{EnvVar: "KANDEV_LAUNCHER_PARENT_PID", Class: "internal wiring", Reason: "launcher parent-watchdog handoff"},
{EnvVar: "KANDEV_BACKEND_PID_FILE", Class: "internal wiring", Reason: "supervisor-owned process state"},
{EnvVar: "KANDEV_DESKTOP_HEALTH_TOKEN", Class: "generated", Reason: "per-launch health authentication token"},
{EnvVar: "KANDEV_DESKTOP_NATIVE_NOTIFICATIONS", Class: "internal wiring", Reason: "desktop shell capability handoff"},
{EnvVar: "KANDEV_DESKTOP_RUNTIME", Class: "internal wiring", Reason: "desktop shell launch-policy handoff"},
{EnvVar: "KANDEV_BUNDLE_DIR", Class: "packaging", Reason: "runtime bundle discovery"},
{EnvVar: "KANDEV_WEB_DIST_DIR", Class: "packaging", Reason: "embedded web asset override"},
{EnvVar: "KANDEV_TASK_ID", Class: "workspace injection", Reason: "task-owned child process context"},
{EnvVar: "KANDEV_SESSION_ID", Class: "workspace injection", Reason: "session-owned child process context"},
{EnvVar: "KANDEV_WORKSPACE_ID", Class: "workspace injection", Reason: "workspace-owned child process context"},
{EnvVar: "KANDEV_E2E_MOCK", Class: "test", Reason: "profile selector and test harness"},
{EnvVar: "KANDEV_MOCK_AGENT", Class: "test", Reason: "profile-selected mock provider"},
{EnvVar: "KANDEV_MOCK_GITHUB", Class: "test", Reason: "test-only provider replacement"},
{EnvVar: "KANDEV_MOCK_GITLAB", Class: "test", Reason: "test-only provider replacement"},
{EnvVar: "KANDEV_MOCK_JIRA", Class: "test", Reason: "test-only provider replacement"},
{EnvVar: "KANDEV_MOCK_LINEAR", Class: "test", Reason: "test-only provider replacement"},
{EnvVar: "KANDEV_FEATURES_OFFICE", Class: "profile", Reason: "runtime feature flag registry"},
{EnvVar: "KANDEV_FEATURES_AUTH", Class: "profile", Reason: "runtime feature flag registry"},
{EnvVar: "KANDEV_FEATURES_CANVASES", Class: "profile", Reason: "runtime feature flag registry"},
{EnvVar: "KANDEV_FEATURES_CLAUDE_BACKGROUND_PROMPT_HANDOFF", Class: "debug", Reason: "runtime feature flag registry"},
{EnvVar: "KANDEV_FEATURES_CLAUDE_MID_TURN_STEERING", Class: "debug", Reason: "runtime feature flag registry"},
{EnvVar: "KANDEV_FEATURES_OFFICE_SESSION_IDENTITY", Class: "debug", Reason: "runtime feature flag registry"},
{EnvVar: "KANDEV_FEATURES_AGENT_SURVIVAL", Class: "profile", Reason: "runtime feature flag registry"},
{EnvVar: "KANDEV_DEBUG_AGENT_MESSAGES", Class: "debug", Reason: "ACP frame diagnostics"},
{EnvVar: "KANDEV_DEBUG_ACP_MAX_FILES", Class: "debug", Reason: "ACP debug retention"},
{EnvVar: "KANDEV_DEBUG_ACP_RETENTION_HOURS", Class: "debug", Reason: "ACP debug retention"},
{EnvVar: "KANDEV_DEBUG_ACP_MAX_FILE_BYTES", Class: "debug", Reason: "ACP debug retention"},
{EnvVar: "KANDEV_MCP_LOG_FILE", Class: "debug", Reason: "agentctl MCP debug logging"},
{EnvVar: "KANDEV_DEBUG_LOG_DIR", Class: "debug", Reason: "ACP debug logging directory"},
{EnvVar: "AGENTCTL_AUTO_APPROVE_PERMISSIONS", Class: "test", Reason: "profile-selected E2E behavior"},
{EnvVar: "KANDEV_MAX_CONCURRENT_SESSIONS", Class: "startup override", Reason: "instance session capacity override for the live install setting; resolved once at startup and takes precedence over the saved value"},
}
// ConfigurationCatalog returns a defensive copy of the stable startup
// catalog. Callers may sort or edit the returned slices.
func ConfigurationCatalog() []CatalogEntry {
out := make([]CatalogEntry, len(startupCatalog))
for i, entry := range startupCatalog {
out[i] = entry
out[i].EnvVars = append([]string(nil), entry.EnvVars...)
}
return out
}
// ConfigurationExclusions returns a defensive copy of the reviewed
// environment-variable exclusions.
func ConfigurationExclusions() []CatalogExclusion {
return append([]CatalogExclusion(nil), startupExclusions...)
}
// CatalogEntryForKey returns the catalog entry for a canonical YAML key.
func CatalogEntryForKey(key string) (CatalogEntry, bool) {
for _, entry := range startupCatalog {
if entry.Key == key {
entry.EnvVars = append([]string(nil), entry.EnvVars...)
return entry, true
}
}
return CatalogEntry{}, false
}
// ValidateCatalog checks that canonical keys and environment aliases are
// unique and that every audited environment variable has an owner.
func ValidateCatalog() error {
keys := make(map[string]struct{}, len(startupCatalog))
envs := make(map[string]string)
for _, entry := range startupCatalog {
if entry.Key == "" || entry.Owner == "" {
return fmt.Errorf("catalog entry has empty key or owner: %#v", entry)
}
if _, exists := keys[entry.Key]; exists {
return fmt.Errorf("duplicate catalog key %q", entry.Key)
}
keys[entry.Key] = struct{}{}
for _, envVar := range entry.EnvVars {
if envVar == "" {
return fmt.Errorf("catalog key %q has an empty environment alias", entry.Key)
}
if previous, exists := envs[envVar]; exists {
return fmt.Errorf("environment alias %q belongs to both %q and %q", envVar, previous, entry.Key)
}
envs[envVar] = entry.Key
}
}
for _, exclusion := range startupExclusions {
if exclusion.EnvVar == "" || exclusion.Class == "" || strings.TrimSpace(exclusion.Reason) == "" {
return fmt.Errorf("incomplete catalog exclusion: %#v", exclusion)
}
if owner, exists := envs[exclusion.EnvVar]; exists {
return fmt.Errorf("environment variable %q is both cataloged as %q and excluded", exclusion.EnvVar, owner)
}
envs[exclusion.EnvVar] = "excluded"
}
return nil
}
// CatalogEnvironmentVariables returns all cataloged and excluded variables in
// stable order. It is used by inventory and documentation completeness tests.
func CatalogEnvironmentVariables() []string {
seen := make(map[string]struct{})
var out []string
for _, entry := range startupCatalog {
for _, envVar := range entry.EnvVars {
if _, ok := seen[envVar]; !ok {
seen[envVar] = struct{}{}
out = append(out, envVar)
}
}
}
for _, exclusion := range startupExclusions {
if _, ok := seen[exclusion.EnvVar]; !ok {
seen[exclusion.EnvVar] = struct{}{}
out = append(out, exclusion.EnvVar)
}
}
sort.Strings(out)
return out
}