// Package config loads and validates the application's runtime configuration. // // Cloud selection is CONFIGURATION, not code (the cloud-portability mandate): the same binary runs // on AWS GovCloud or Azure Government by flipping MM_CLOUD and the provider settings. Nothing here // imports a cloud SDK, adapters live behind interfaces in their own packages and are wired in // cmd/api from this config. package config import ( "encoding/base64" "encoding/hex" "errors" "fmt" "os" "strconv" "strings" ) // Env is the deployment environment. type Env string const ( EnvDevelopment Env = "development" EnvProduction Env = "production" ) // Cloud is the target government cloud. Local is for dev (MinIO, self-hosted embeddings, no managed services). type Cloud string const ( CloudLocal Cloud = "local" CloudAWSGovCloud Cloud = "aws_govcloud" CloudAzureGov Cloud = "azure_gov" ) // EmbeddingProvider selects the embedding adapter. "fake" is a deterministic offline provider for // dev/tests; "openai" is any OpenAI compatible endpoint (self hosted vLLM, the production CUI path, // or OpenAI); "bedrock"/"azure_openai" are the managed gov cloud adapters (wired at deploy time). type EmbeddingProvider string const ( ProviderFake EmbeddingProvider = "fake" ProviderOpenAI EmbeddingProvider = "openai" ProviderBedrock EmbeddingProvider = "bedrock" ProviderAzureOpenAI EmbeddingProvider = "azure_openai" // ProviderMegamind routes embeddings through the stateless MEGAMIND inference plane (the typed // inference client to the gateway), the production self hosted path for cleared CUI data. ProviderMegamind EmbeddingProvider = "megamind" ) // Embeddings configures the embedding provider. Dimension MUST match the vector(N) column width in // the schema (default 384); changing the provider's native dimension is a re-embed migration, not a // config flip (STACK_DECISIONS.md ยง6). type Embeddings struct { Provider EmbeddingProvider Endpoint string // base URL for the OpenAI-compatible / Azure endpoint Model string // e.g. "text-embedding-3-large", "amazon.titan-embed-text-v2:0", a local model id Dimension int APIKey string // bearer/api-key; for Bedrock/Azure-Entra the adapter uses cloud credentials instead MegamindURL string // base URL of the MEGAMIND gateway when Provider is "megamind" } // Inference configures the typed client to the stateless MEGAMIND inference plane (embeddings, // reranking, generation, faithfulness). The heavy models run out of process on the Apple Silicon // cluster; this is the only AI compute the FIPS API binary reaches, over HTTP. URL empty means the AI // layer is unconfigured and its routes degrade rather than fail the boot. type Inference struct { URL string // MEGAMIND gateway base, reached over Tailscale EmbedModel string // 384 dim embedder (all-minilm or bge-small-en-v1.5) RerankModel string // cross encoder (bge-reranker-v2-m3) or the cosine baseline GenerateModel string // local instruct model (e.g. llama3.1:8b) Dimension int // must match the vector(N) column width TimeoutSeconds int // per request MaxRetries int // additional attempts on a transient failure BreakerTrips int // consecutive failures before the breaker opens BreakerCoolSec int // how long the breaker stays open TeachingEnabled bool // federated craft signal transmission; disabled until multiple live clients exist } // ExtractorProvider selects the Mission Check structured extraction adapter (resume or job description // text to proposed fields). "deterministic" is the offline rules plus ontology extractor and the // default; an LLM backed provider (self hosted vLLM for CUI in production) is a future addition behind // the same port. Extraction runs server side only, so a key here never reaches the browser bundle. type ExtractorProvider string const ( ExtractorDeterministic ExtractorProvider = "deterministic" ) // Extraction configures the Mission Check structured extraction provider. Endpoint, Model, and APIKey // are reserved for a future provider backed extractor and are unused by the deterministic one. type Extraction struct { Provider ExtractorProvider Endpoint string Model string APIKey string } // Auth holds the server side secrets and tunables for the authentication module. Every secret here // loads from the environment (the root owned secrets file), never the repo or the browser bundle. type Auth struct { PepperVersion int // active credential pepper version stored on new credentials Pepper []byte // active credential pepper key (HMAC applied before PBKDF2) PepperPrevious []byte // optional prior pepper key (version PepperVersion minus 1) for rotation TOTPKEKVersion int // active TOTP secret KEK version TOTPKEK []byte // 32 byte AES 256 key encrypting TOTP secrets at rest TOTPKEKPrevious []byte // optional prior TOTP KEK (version TOTPKEKVersion minus 1) so a rotated KEK still decrypts older secrets CSRFKey []byte // HMAC key binding a CSRF token to its session RecoveryLookupKey []byte // HMAC key for recovery code lookup tags TOTPIssuer string // issuer shown in the authenticator app SessionIdleSeconds int // idle timeout (NIST 800-171 3.1.11 default 900) SessionAbsoluteSeconds int // absolute session cap (default 43200, 12 hours) PendingAbsoluteSeconds int // auth_pending (pre MFA) session cap (default 300) InviteExpirySeconds int // invite and activation link lifetime (default 259200, 72 hours) CookieSecure bool // Secure attribute on the session cookie (true except plain http dev) MFARequired bool // when false, login mints an active session after password with no TOTP step (pilot only, forbidden in production) } // Config is the fully-resolved, validated application configuration. type Config struct { Env Env HTTPAddr string AppDatabaseURL string // NOBYPASSRLS app role DSN MigratorDatabaseURL string // BYPASSRLS migrator role DSN (used by goose only) ComplianceDatabaseURL string // compliance role DSN, the only path that reads the eeo schema; empty disables adverse impact reporting (503) DecisionRetentionDays int // retention window for identifiable screening artifacts after a decision (default 90) Cloud Cloud Embeddings Embeddings Inference Inference Extraction Extraction RequireFIPS bool // when true, cmd/api aborts unless crypto/fips140.Enabled() AllowDevHeaderAuth bool // dev only X-Org-ID resolver toggle; MUST be false in production CurationEnforceSeparation bool // MM_CURATION_SEPARATION_OF_DUTIES: require the curation verifier to differ from the approver (default true); a single operator pilot may set it false to self verify, an explicit audited choice AllowedOrigins []string // CSRF Origin allowlist for unsafe verbs; empty disables the check (dev) TrustedProxyHeader string // header carrying the real client IP (set by a trusted reverse proxy); empty uses the TCP peer Auth Auth Federated Federated } // Federated configures the cross tenant differential privacy aggregation path (WS8). It is INERT by // default: the whole federated package is dead code unless Enabled is true and a real cohort of five or // more live non test tenants exists. Epsilon and Delta are the privacy budget, expressed as // configuration and never as code constants so a privacy officer can sign off and tune them without a // rebuild. In tenant LIVE keeps this off, only the in tenant producer and the human approved curation // pipeline ship enabled. type Federated struct { DPEnabled bool // MM_FEDERATED_DP_ENABLED: master switch for the cross tenant DP release path (default false) DPEpsilon float64 // MM_FEDERATED_DP_EPSILON: the (epsilon, delta) DP budget epsilon target (default 10) DPDelta float64 // MM_FEDERATED_DP_DELTA: the (epsilon, delta) DP budget delta target (default 1e-7) } // Load reads configuration from the environment, applies defaults, and validates it. func Load() (Config, error) { // Secret key decode errors are collected and surfaced alongside validation errors, so a // misconfigured deploy sees every problem at once instead of one boot failure at a time. var secretErrs []error getB := func(key string) []byte { b, err := getBytes(key) if err != nil { secretErrs = append(secretErrs, err) } return b } c := Config{ Env: Env(getDefault("MM_ENV", string(EnvDevelopment))), HTTPAddr: getDefault("MM_HTTP_ADDR", ":8080"), AppDatabaseURL: os.Getenv("MM_DATABASE_URL"), MigratorDatabaseURL: os.Getenv("MM_MIGRATOR_DATABASE_URL"), ComplianceDatabaseURL: os.Getenv("MM_COMPLIANCE_DATABASE_URL"), DecisionRetentionDays: getInt("MM_DECISION_RETENTION_DAYS", 90), Cloud: Cloud(getDefault("MM_CLOUD", string(CloudLocal))), Embeddings: Embeddings{ Provider: EmbeddingProvider(getDefault("MM_EMBED_PROVIDER", string(ProviderFake))), Endpoint: os.Getenv("MM_EMBED_ENDPOINT"), Model: os.Getenv("MM_EMBED_MODEL"), Dimension: getInt("MM_EMBED_DIMENSION", 384), APIKey: os.Getenv("MM_EMBED_API_KEY"), MegamindURL: os.Getenv("MM_MEGAMIND_URL"), }, Inference: Inference{ URL: os.Getenv("MM_MEGAMIND_URL"), // bge-small-en-v1.5 (384 dim) is the production embedder, served on Ollama as the qllama handle. // Stored and query vectors MUST use the same model: switching the model requires a FULL re // backfill (existing vectors from another model are incompatible), see DEPLOY_PLAN.md. EmbedModel: getDefault("MM_MEGAMIND_EMBED_MODEL", "qllama/bge-small-en-v1.5"), RerankModel: getDefault("MM_MEGAMIND_RERANK_MODEL", "bge-reranker-v2-m3"), GenerateModel: getDefault("MM_MEGAMIND_GENERATE_MODEL", "llama3.1:8b"), Dimension: getInt("MM_EMBED_DIMENSION", 384), TimeoutSeconds: getInt("MM_MEGAMIND_TIMEOUT_SECONDS", 30), MaxRetries: getInt("MM_MEGAMIND_MAX_RETRIES", 2), BreakerTrips: getInt("MM_MEGAMIND_BREAKER_TRIPS", 5), BreakerCoolSec: getInt("MM_MEGAMIND_BREAKER_COOL_SECONDS", 30), TeachingEnabled: getBool("MM_MEGAMIND_TEACHING_ENABLED", false), }, Extraction: Extraction{ Provider: ExtractorProvider(getDefault("MM_EXTRACT_PROVIDER", string(ExtractorDeterministic))), Endpoint: os.Getenv("MM_EXTRACT_ENDPOINT"), Model: os.Getenv("MM_EXTRACT_MODEL"), APIKey: os.Getenv("MM_EXTRACT_API_KEY"), }, RequireFIPS: getBool("MM_REQUIRE_FIPS", false), AllowDevHeaderAuth: getBool("MM_ALLOW_DEV_HEADER_AUTH", false), CurationEnforceSeparation: getBool("MM_CURATION_SEPARATION_OF_DUTIES", true), AllowedOrigins: splitCSV(os.Getenv("MM_ALLOWED_ORIGINS")), TrustedProxyHeader: os.Getenv("MM_TRUSTED_PROXY_HEADER"), Auth: Auth{ PepperVersion: getInt("MM_CREDENTIAL_PEPPER_VERSION", 1), Pepper: getB("MM_CREDENTIAL_PEPPER"), PepperPrevious: getB("MM_CREDENTIAL_PEPPER_PREVIOUS"), TOTPKEKVersion: getInt("MM_TOTP_KEK_VERSION", 1), TOTPKEK: getB("MM_TOTP_KEK"), TOTPKEKPrevious: getB("MM_TOTP_KEK_PREVIOUS"), CSRFKey: getB("MM_CSRF_KEY"), RecoveryLookupKey: getB("MM_RECOVERY_LOOKUP_KEY"), TOTPIssuer: getDefault("MM_TOTP_ISSUER", "missionMATCH"), SessionIdleSeconds: getInt("MM_SESSION_IDLE_SECONDS", 900), SessionAbsoluteSeconds: getInt("MM_SESSION_ABSOLUTE_SECONDS", 43200), PendingAbsoluteSeconds: getInt("MM_SESSION_PENDING_SECONDS", 300), InviteExpirySeconds: getInt("MM_INVITE_EXPIRY_SECONDS", 259200), CookieSecure: getBool("MM_COOKIE_SECURE", true), MFARequired: getBool("MM_MFA_REQUIRED", true), }, Federated: Federated{ DPEnabled: getBool("MM_FEDERATED_DP_ENABLED", false), DPEpsilon: getFloat("MM_FEDERATED_DP_EPSILON", 10), DPDelta: getFloat("MM_FEDERATED_DP_DELTA", 1e-7), }, } if err := c.Validate(); err != nil { secretErrs = append(secretErrs, err) } if len(secretErrs) > 0 { return Config{}, errors.Join(secretErrs...) } return c, nil } // Validate enforces invariants and environment-specific requirements. Errors are joined so a // misconfigured deploy surfaces every problem at once. func (c Config) Validate() error { var errs []error switch c.Env { case EnvDevelopment, EnvProduction: default: errs = append(errs, fmt.Errorf("MM_ENV %q invalid (want development|production)", c.Env)) } switch c.Cloud { case CloudLocal, CloudAWSGovCloud, CloudAzureGov: default: errs = append(errs, fmt.Errorf("MM_CLOUD %q invalid (want local|aws_govcloud|azure_gov)", c.Cloud)) } switch c.Embeddings.Provider { case ProviderFake, ProviderOpenAI, ProviderBedrock, ProviderAzureOpenAI, ProviderMegamind: default: errs = append(errs, fmt.Errorf("MM_EMBED_PROVIDER %q invalid", c.Embeddings.Provider)) } if c.Embeddings.Dimension <= 0 { errs = append(errs, errors.New("MM_EMBED_DIMENSION must be > 0")) } // Empty selects the deterministic extractor (the same default extraction.New applies), so a Config // built without an explicit provider validates. switch c.Extraction.Provider { case ExtractorDeterministic, "": default: errs = append(errs, fmt.Errorf("MM_EXTRACT_PROVIDER %q invalid (want deterministic)", c.Extraction.Provider)) } // OpenAI-compatible providers need an endpoint + model. if c.Embeddings.Provider == ProviderOpenAI || c.Embeddings.Provider == ProviderAzureOpenAI { if c.Embeddings.Endpoint == "" { errs = append(errs, fmt.Errorf("MM_EMBED_ENDPOINT required for provider %q", c.Embeddings.Provider)) } if c.Embeddings.Model == "" { errs = append(errs, fmt.Errorf("MM_EMBED_MODEL required for provider %q", c.Embeddings.Provider)) } } // The MEGAMIND provider needs the gateway URL and the embed model name. if c.Embeddings.Provider == ProviderMegamind { if c.Embeddings.MegamindURL == "" { errs = append(errs, errors.New("MM_MEGAMIND_URL required for provider \"megamind\"")) } if c.Embeddings.Model == "" && c.Inference.EmbedModel == "" { errs = append(errs, errors.New("MM_EMBED_MODEL or MM_MEGAMIND_EMBED_MODEL required for provider \"megamind\"")) } } // TOTP secrets are AES 256, so the KEK must be exactly 32 bytes whenever it is set, in any env. if len(c.Auth.TOTPKEK) != 0 && len(c.Auth.TOTPKEK) != 32 { errs = append(errs, errors.New("MM_TOTP_KEK must decode to exactly 32 bytes (AES 256)")) } if len(c.Auth.TOTPKEKPrevious) != 0 && len(c.Auth.TOTPKEKPrevious) != 32 { errs = append(errs, errors.New("MM_TOTP_KEK_PREVIOUS must decode to exactly 32 bytes (AES 256)")) } if c.Auth.PepperVersion <= 0 { errs = append(errs, errors.New("MM_CREDENTIAL_PEPPER_VERSION must be positive")) } if c.Auth.SessionIdleSeconds <= 0 || c.Auth.SessionAbsoluteSeconds <= 0 || c.Auth.PendingAbsoluteSeconds <= 0 { errs = append(errs, errors.New("session timeout seconds must all be positive")) } if c.DecisionRetentionDays <= 0 { errs = append(errs, errors.New("MM_DECISION_RETENTION_DAYS must be positive")) } if c.Env == EnvProduction { if c.AppDatabaseURL == "" { errs = append(errs, errors.New("MM_DATABASE_URL required in production")) } if c.Embeddings.Provider == ProviderFake { errs = append(errs, errors.New("the fake embedding provider must not be used in production")) } if c.Cloud == CloudLocal { errs = append(errs, errors.New("MM_CLOUD must be a government cloud in production")) } // The dev X-Org-ID resolver may never be the production tenant source. if c.AllowDevHeaderAuth { errs = append(errs, errors.New("MM_ALLOW_DEV_HEADER_AUTH must be false in production")) } // FIPS must be enforced, not optional, so the validated crypto path is the one that runs. if !c.RequireFIPS { errs = append(errs, errors.New("MM_REQUIRE_FIPS must be true in production")) } // Every auth secret must be present so the validated crypto has its keys. if len(c.Auth.Pepper) == 0 { errs = append(errs, errors.New("MM_CREDENTIAL_PEPPER required in production")) } if len(c.Auth.TOTPKEK) != 32 { errs = append(errs, errors.New("MM_TOTP_KEK (32 byte AES 256 key) required in production")) } if len(c.Auth.CSRFKey) == 0 { errs = append(errs, errors.New("MM_CSRF_KEY required in production")) } if len(c.Auth.RecoveryLookupKey) == 0 { errs = append(errs, errors.New("MM_RECOVERY_LOOKUP_KEY required in production")) } // The CSRF Origin allowlist must be enforced; an empty list disables it (a dev only path). if len(c.AllowedOrigins) == 0 { errs = append(errs, errors.New("MM_ALLOWED_ORIGINS required in production (the CSRF Origin allowlist must be enforced)")) } // Both cookies use the __Host- prefix, which a browser only accepts when Secure is set; a non // Secure production deploy silently breaks all auth and exposes the token. if !c.Auth.CookieSecure { errs = append(errs, errors.New("MM_COOKIE_SECURE must be true in production (the __Host- cookie prefix requires Secure)")) } // MFA is mandatory in production. The pilot only password only relaxation can never ship to a // government cloud, so a production deploy with it set is a hard configuration error. if !c.Auth.MFARequired { errs = append(errs, errors.New("MM_MFA_REQUIRED must be true in production (MFA is mandatory)")) } } return errors.Join(errs...) } // IsProduction reports whether this is a production deployment. func (c Config) IsProduction() bool { return c.Env == EnvProduction } func getDefault(key, def string) string { if v := strings.TrimSpace(os.Getenv(key)); v != "" { return v } return def } func getInt(key string, def int) int { if v := strings.TrimSpace(os.Getenv(key)); v != "" { if n, err := strconv.Atoi(v); err == nil { return n } } return def } func getBool(key string, def bool) bool { if v := strings.TrimSpace(os.Getenv(key)); v != "" { if b, err := strconv.ParseBool(v); err == nil { return b } } return def } func getFloat(key string, def float64) float64 { if v := strings.TrimSpace(os.Getenv(key)); v != "" { if f, err := strconv.ParseFloat(v, 64); err == nil { return f } } return def } // splitCSV splits a comma separated env value into trimmed, non empty entries. func splitCSV(v string) []string { if strings.TrimSpace(v) == "" { return nil } parts := strings.Split(v, ",") out := make([]string, 0, len(parts)) for _, p := range parts { if t := strings.TrimSpace(p); t != "" { out = append(out, t) } } return out } // getBytes loads a secret key from the environment using an EXPLICIT encoding prefix so the key // material is never ambiguous: "base64:", "hex:", or "raw:". A bare value (no prefix) is decoded as // standard base64, and a decode failure is a hard error rather than a silent fall back to the raw // string. The silent fall back was a real footgun: a passphrase that happens to be valid base64 would // be decoded to different bytes than the operator intended, so the same variable could mean two // different keys in two contexts (a TOTP KEK that no longer decrypts its secrets, a pepper that no // longer verifies its passwords). Forcing the operator to declare the encoding removes that class of // bug. An unset variable yields nil with no error. func getBytes(key string) ([]byte, error) { v := strings.TrimSpace(os.Getenv(key)) if v == "" { return nil, nil } switch { case strings.HasPrefix(v, "raw:"): return []byte(strings.TrimPrefix(v, "raw:")), nil case strings.HasPrefix(v, "hex:"): b, err := hex.DecodeString(strings.TrimPrefix(v, "hex:")) if err != nil { return nil, fmt.Errorf("%s: invalid hex value: %w", key, err) } return b, nil case strings.HasPrefix(v, "base64:"): b, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(v, "base64:")) if err != nil { return nil, fmt.Errorf("%s: invalid base64 value: %w", key, err) } return b, nil default: b, err := base64.StdEncoding.DecodeString(v) if err != nil { return nil, fmt.Errorf("%s: value must be base64, or carry a raw: / hex: / base64: prefix: %w", key, err) } return b, nil } }