Feature gate registry¶
This page is the single index of the environment-variable gates that change the behaviour of the control plane. It exists so an operator can answer two questions without grepping the codebase:
- What is the default? Every gate below ships default-OFF / legacy behaviour preserved unless explicitly noted, per charter §12a Rule 4.
- Is it safe to flip? Each row states whether the gate is a production hardening toggle (safe to enable after a soak window), an escape hatch (only for a specific known-safe situation), or a local-debug-only switch that must never reach a deployed Container App.
Adding a new gate? Name it
STRICT_<area>orENFORCE_<area>(hardening) and register it here in the same change, with the planned flip date. That is the §12a Rule 4 contract.
Production hardening gates¶
New hardening gates follow the §12a Rule 4 lifecycle: ship default-OFF behind the env var, soak for one release cycle with the gate forced ON in dogfood + a green Persona Matrix run, then flip the shared deployment default in a separate change. Rows marked on below have completed that activation; their process-level fallback can still be OFF when the environment variable is absent.
| Gate | Default | Effect when =true |
Read by |
|---|---|---|---|
STRICT_JWT |
off | Lowers the claims cache TTL from 300 s to 60 s and pins the token azp/audience on every validation. |
api/auth.py |
STRICT_CORS |
off | Locks the CORS allow-list to same-origin; STRICT_CORS_ALLOW_METHODS / STRICT_CORS_ALLOW_HEADERS (comma-separated) override the defaults for custom flows. |
api/main.py |
STRICT_EXEC_RATE_LIMIT |
off | Enables a per-window rate limit on the loopback exec server in the terminal sidecar. Setting it back to false re-opens the gate immediately. |
terminal/exec_server.py |
STRICT_CSP |
off | Emits a strict Content-Security-Policy response header on API + proxied SPA responses (kept in sync with web/nginx.conf). STRICT_CSP_POLICY overrides the default policy string. |
api/app/security_headers.py |
STRICT_READINESS_DETAIL |
off | Collapses the /api/health/ready body to the overall status only (drops the per-component components map that leaks internal topology to an anonymous recon probe). Default OFF preserves the full-detail body the cli-upgrade Tier-1 gate reads. |
api/routes/health.py |
ENABLE_DOCS |
off | Exposes FastAPI's internal /api/docs Swagger UI and /openapi.json. Production leaves both hidden; this does not affect the separate, authenticated product API Reference page. |
api/main.py |
STRICT_SSE_TICKET_BINDING |
off | Binds the one-shot SSE ticket to the caller object id, client IP, and User-Agent hash, and rejects consumption when any differs (audit P0 #2 #3). | api/routes/monitor/sidecars.py, api/routes/monitor/logs.py |
STRICT_AUDIT_HASH |
off | Redacts PII out of jobhistory.payload_json by hashing matched fields before the append-blob audit write (audit P2 #13 #14). |
api/services/state/repository.py |
STRICT_CLIENT_LOG_REDACTION |
off | Hashes the authenticated caller identifier and removes query/fragment data from browser error-report URLs before logging. The 2026-08-15 review date passed without a shared-default flip; it remains off pending documented dogfood and Persona Matrix evidence. | api/routes/client_log.py |
ENFORCE_OPENAPI_EXEC_RBAC |
off (false in Bicep) |
Requires the caller to hold an Azure RBAC write role on the target resource group before a state-changing OpenAPI proxy call is forwarded under the admin token. See OpenAPI execution RBAC gate. | api/services/openapi/exec_gate.py |
ENFORCE_DASHBOARD_RBAC |
on (true in the shared deployment defaults; unset fallback is off) |
Requires the caller to hold a readable Azure role on the configured platform subscription or resource group before /api/me admits the SPA. Permission-enumeration failures degrade open so an ARM outage cannot lock out every operator; downstream Azure calls still enforce their own RBAC. |
api/services/dashboard_access.py |
ALLOW_OPENAPI_TOKEN_AUTH |
on (true in the shared deployment defaults; unset fallback is off) |
Universal M2M shared-token auth (activated 2026-07). Lets every require_caller-gated route also accept the shared elb-openapi X-ELB-API-Token (constant-time compared against the api-sidecar env / Redis cache token) in addition to the MSAL bearer, so a peer-VNet automation caller manages one credential across the whole API surface — read and write. The shared token has no caller-specific Azure RBAC gate; keep ingress access controlled and rotate the token through Settings. Setting the gate to false restores MSAL-bearer-only authentication. |
api/auth.py |
STRICT_RBAC_REMOVAL_HALT |
off (warn-only) | Makes the azd preprovision RBAC-removal preflight halt azd provision when a Microsoft.Authorization/roleAssignments resource would be deleted, unless ACCEPT_RBAC_REMOVAL is set for the run. See charter §12a Rule 7. |
scripts/dev/check_rbac_removal.py |
ENFORCE_AUTO_ORACLE_RBAC |
off (tenant-auth legacy path) | Requires current AKS read capability for preference listing and current AKS plus Storage write capability for preference mutation/background dispatch. Permission lookup degradation fails closed. The 2026-09-10 review passed without a shared-default flip; it remains off pending documented dogfood and Persona Matrix evidence. | api/services/auto_oracle_reconcile.py |
Feature flags (behaviour switches, not hardening)¶
These select between two supported behaviours rather than tightening a safety check. They do not follow the §12a Rule 4 hardening lifecycle.
| Gate | Default | Effect when =true |
Read by |
|---|---|---|---|
BLAST_GATE_ENABLED |
off (legacy direct-submit path) | Routes BLAST submit through the AKS capacity gate instead of submitting directly. The /api/blast/capacity preview endpoint reports the would-have-been decision even when the gate is off. |
api/routes/blast/capacity.py, api/tasks/blast/submit_task.py |
BLAST_COORD_BACKEND |
redis (process and checked-in deployment fallback) |
Set to k8s to coordinate dashboard and direct OpenAPI submissions through one Kubernetes Lease plus an active-finalizer ceiling. The K8s backend takes precedence over BLAST_GATE_ENABLED and never touches the Redis capacity reservation store. A deployed environment may override the checked-in fallback; keep both submitters aligned. |
Cross-Path BLAST Submit Coordination |
BLAST_JOBS_SHARED_VISIBILITY |
on (shared deployment; unset fallback is off) | Lets every authenticated caller list and open all jobs, including jobs owned by other callers. Set it to false before multi-tenant or shared-subscription use to restore per-owner visibility checks. |
api/services/blast/job_state.py |
BLAST_DB_METADATA_INVALIDATE_DISABLED |
off (cross-sidecar invalidation is on) | Stops best-effort Redis publication/subscription for BLAST database metadata cache invalidation. Local invalidation still runs and TTL bounds stale reads; intended for tests or pub/sub incident isolation. | api/services/blast/db_metadata.py |
STRICT_BLUEGREEN |
off (single-revision upgrade) | Enables staged green revision health checks, traffic cutover, confirm-window rollback, and revision garbage collection. It must be activated together with Container Apps activeRevisionsMode: Multiple; the checked-in Bicep remains Single. |
api/services/upgrade/revisions.py |
OPENAPI_ALLOW_PUBLIC_LB |
on (shared deployment; unset fallback is off) | Allows the dashboard proxy to forward the OpenAPI admin token to a public load-balancer IPv4 address. Set it to false to require a private, loopback, or link-local target. Public OpenAPI remains a separate operator opt-in and must retain its token, TLS, NSG, and rate-limit controls. |
api/routes/aks/openapi_proxy.py |
SERVICEBUS_ENABLED |
empty (defer to Settings; saved config defaults disabled) | Acts as a three-state deployment override: empty defers to the durable Settings row, truthy permits activation when that row is configured and enabled, and falsy is a kill switch. | Service Bus integration |
ENABLE_SB_SUBMIT_INGRESS |
off (direct OpenAPI submit) | Sends eligible unscoped dashboard submissions through the configured Service Bus request queue. Explicitly scoped requests stay direct, and publish failure falls back to the direct path. | api/services/blast/submit_ingress.py |
SERVICEBUS_RESIDENT_CONSUMER |
on (shared deployment; unset fallback is off) | Runs the low-latency resident request consumer in the dedicated Service Bus worker. The periodic Celery drain remains the recovery fallback. | api/services/blast/resident_consumer.py |
SERVICEBUS_QUEUE_AUTOSTART |
on (shared deployment; unset fallback is off) | Starts an exactly Stopped AKS cluster when the request queue has pending work. A fail-closed Redis lease prevents duplicate cost-bearing starts; the default cooldown is 600 seconds. |
api/services/aks/queue_autostart.py |
SERVICEBUS_ATOMIC_CLAIM |
on | Atomically reserves a correlation id before OpenAPI submit. If disabled while drain concurrency exceeds one, the worker forces serial drain instead of risking duplicate execution. | api/tasks/servicebus/tasks.py |
SERVICEBUS_DRAIN_SINGLEFLIGHT |
on (false override retained for compatibility but ignored) | Keeps one queue-scoped Redis drain lease across resident and periodic consumers. Current code always takes the lease because Settings routing changes use it as a mutation fence. | api/tasks/servicebus/tasks.py |
STORAGE_DATE_LAYOUT_ENABLED |
on | Writes new native and external results under results/YYYY/MM/DD/<job_id>/. Existing flat-layout jobs remain readable through their persisted prefix. |
api/services/storage/job_prefix.py |
JOBSTATE_TIME_INDEX_ENABLED |
on | Uses the jobstateindex Table for bounded newest-first job listing and runs the repair reconciler. Existing deployments must complete the index backfill before enabling it. |
api/services/state/time_index.py |
DB_CONSISTENCY_RECONCILE_ENABLED |
on | Runs the guarded database consistency reconciler, which can prune ghost volume blobs and rebuild shard aliases. It skips when authority is missing, drift exceeds safety bounds, or a prepare lock is held. | api/tasks/storage/reconcile_db_consistency.py |
ENABLE_MGMT_CLIENT_POOL |
on (unset fallback is on) | Reuses Azure management SDK clients by credential and subscription so dashboard polling keeps warm HTTP pools. Set false only to isolate client-pool diagnostics. | api/services/azure_clients.py |
JOBS_EVENTS_SSE_DISABLED |
off (job-event SSE is on) | Kill switch for the one-shot-ticket job-change SSE channel. When true, clients retain their polling fallback. | api/routes/monitor/jobs_events.py |
K8S_CLUSTER_BREAKER_DISABLED |
off (breaker is on) | Emergency bypass for the per-cluster Kubernetes connection-failure circuit breaker. Leaving it false prevents repeated probes from flooding a stopped or unreachable cluster. | api/services/k8s/cluster_breaker.py |
NCBI_DURABLE_CACHE_DISABLED |
off (durable cache is on) | Bypasses the best-effort ops-Redis cache for NCBI record responses and falls back to process cache plus live lookup. | api/services/ncbi/nuccore.py |
NCBI_REDIS_BUCKET_DISABLED |
off (Redis limiter is on) | Bypasses the cross-process NCBI request token bucket and uses the in-process fallback. Use only for diagnostics; disabling it weakens aggregate rate coordination. | api/services/ncbi/_eutils.py |
OPENAPI_PLS_ENABLED |
off | Adds Private Link Service annotations to the OpenAPI internal LoadBalancer. Enabling requires an explicit OPENAPI_PLS_LB_SUBNET; see OpenAPI Direct Access. |
api/tasks/openapi/constants.py |
PREPARE_DB_ORPHAN_RECONCILE_ENABLED |
on | Kill switch for the idempotent prepare-DB orphan reconciler. Set false to pause metadata repair while investigating an active incident. | api/services/storage/orphan_prepare_db.py |
REQUEST_DETAIL_CAPTURE_ENABLED |
metadata-only when unset | Tri-state request inspector: unset records bounded metadata, true additionally buffers eligible request/response bodies, and false disables detail samples while aggregate request metrics continue. | api/app/middleware.py |
SIDECAR_REPORTER_DISABLED |
off (reporters are on) | Stops api/worker/beat/terminal cgroup metric reporters. Intended for tests and incident isolation; Sidecar Runtime loses local samples while disabled. | api/main.py |
STALE_DBOPS_RECONCILE_ENABLED |
on | Kill switch for the stale warmup/prepare/oracle JobState terminalizer. Set false only while diagnosing rows the reconciler would repair. | api/services/db/stale_dbops.py |
STORAGE_DFS_ENABLED |
off (Blob fallback) | Uses ADLS Gen2 directory APIs for HNS listing and recursive deletion, with Blob fallback where documented. Retention deletion also requires this gate. | api/services/storage/dfs_client_pool.py |
ELB_OPENAPI_ETA_ENABLED |
off | Enables the sibling OpenAPI build overlay's learned queue-aware ETA projection. The overlay degrades to no ETA on storage or model failure. | scripts/dev/openapi-overlays/eta.py |
SERVICEBUS_EXTERNAL_CONSUMER |
off (no demo observer) | Starts a worker-side demo consumer that drains the Service Bus completion topic into the in-memory event-bus (UI Live SB Events). The dashboard is the producer on elastic-blast-completions; the demo consumer exists only to exercise the contract. It defaults to the dedicated playground-observer subscription and does not consume shared default. Footgun (#78): an explicit SERVICEBUS_COMPLETION_SUBSCRIPTION override that includes default can still compete-consume with a real external integrator; startup emits a strong WARNING when configured that way. Queue completion mode keeps the observer disabled so it cannot steal external ACKs. |
api/services/service_bus_external_consumer.py |
BLAST_AUTO_RETRY_ENABLED |
off (no auto-resubmit) | Lets the blast-auto-retry-failed-jobs beat sweep auto-resubmit transient submit-phase failures (terminal sidecar / Azure auth / node-warmup) with bounded backoff, quarantining a job once its budget is exhausted. K8s runtime failures (blast_search_failed), cluster-state failures, and external-origin jobs are never auto-retried. Tunables: BLAST_AUTO_RETRY_MAX (default 2, 1–10), BLAST_AUTO_RETRY_SWEEP_LIMIT (resubmits per pass, default 5, 1–50), BLAST_AUTO_RETRY_SCAN_LIMIT (rows read per pass, default 200, 10–1000), CELERY_BEAT_BLAST_AUTO_RETRY_SECONDS (sweep interval, default 180). Planned flip date: TBD — keep OFF until the auto-retry UI affordance ships and one dogfood release cycle has soaked. |
api/tasks/blast/auto_retry_task.py, api/services/blast/auto_retry.py |
COST_PRICING_LIVE |
off (static price map) | Lets the Cost estimate card price VM SKUs from the public, no-auth Azure Retail Prices API (Linux on-demand, per region) instead of the hardcoded map. Positive prices use a 24h best-effort shared Redis cache plus the bounded process cache; misses use a 15-minute TTL so newly available SKUs recover without an operator cache flush. Redis or Retail API failure falls back to the bundled approximate map. Off keeps the bundled map and makes no external call. The card surfaces which source was used. | api/services/cost/pricing.py, api/services/cost/estimate.py |
WEBHOOK_NOTIFICATIONS_ENABLED |
off (no outbound POST) | Lets the dispatch-job-webhooks beat sweep POST a provider-aware rich message (Slack Block Kit / Teams MessageCard / Discord embed; generic {text, content} for Logic Apps + custom integrators) to the configured webhook when a BLAST job reaches a terminal state. Requires a webhook configured + enabled in Settings → Webhooks. The URL is SSRF-validated against an allowlist (hooks.slack.com / *.webhook.office.com / Discord / Logic Apps, extendable via WEBHOOK_ALLOWED_HOSTS) on save AND at send time; https-only, IP literals rejected. Messages include a deep-link to the job ({DASHBOARD_PUBLIC_URL}/blast/jobs/{job_id} resolved via api.services.control_plane_url). Tunables: WEBHOOK_SWEEP_LIMIT (sends per pass, default 20, 1–100), CELERY_BEAT_WEBHOOK_SECONDS (sweep interval, default 60). |
api/tasks/webhooks.py, api/services/webhooks_pref.py |
K8S_RUNTIME_GC_ENABLED |
on | Runs a bounded five-minute sweep that deletes old terminal ElasticBLAST Jobs and terminal OpenAPI job ConfigMaps. Active, recent, and timestamp-invalid objects are preserved. Defaults: K8S_RUNTIME_GC_JOB_RETENTION_SECONDS=3600, K8S_RUNTIME_GC_CONFIGMAP_RETENTION_SECONDS=1209600 (14 days), K8S_RUNTIME_GC_MAX_DELETES=200, and K8S_RUNTIME_GC_DEADLINE_SECONDS=180 split across at most two clusters. Set the gate to false as an emergency retention hold. Durable dashboard history and results remain in Azure Table/Blob Storage after K8s GC. |
api/tasks/blast/runtime_gc_task.py, api/services/k8s/runtime_gc.py |
AUTO_ORACLE_RECONCILE_ENABLED |
off (preferences only) | Enables immediate and 120-second recovery reconciliation for DB order-oracle preferences after exact-scope Auto warm readiness. Work is capped at two builds globally and one per Storage account per pass. The 2026-09-10 review passed without a shared-default flip; it remains off pending an explicit activation decision. See Auto Oracle. | api/services/auto_oracle_reconcile.py |
AUTO_ORACLE_RETENTION_ENABLED |
off (no deletion) | Enables the daily 14-day oracle-history sweep. Current, previous-ready, active, referenced, recent, malformed, and nonterminal runs are preserved; deletion is capped and tombstone-protected. Flip only in a separate review after reconcile soak. | api/services/db/oracle_retention.py |
PREPARE_DB_NCBI_DIRECT_ENABLED |
off (false in the shared deployment defaults) |
Enables an explicit operator-confirmed NCBI Direct (HTTPS) update when the official NCBI release is newer than the S3 cloud mirror. The AKS Indexed Job pins every archive URL, size, and MD5; stages into a generation prefix; and promotes only after marker, file-size, and shard-layout validation. It never silently falls back to an older S3 snapshot. Live rollout evidence was recorded on 2026-08-29, but the 2026-09-15 review passed without a shared-default flip; individual environments may still opt in explicitly. Tunables: PREPARE_DB_NCBI_DIRECT_PARALLELISM (default 4, capped at 8), PREPARE_DB_NCBI_DIRECT_TIMEOUT_SECONDS (default 8 hours), and PREPARE_DB_DIRECT_RECOVERY_GRACE_SECONDS (default 120 seconds, clamped to 30–3600) before the orphan reconciler promotes a completed Job after worker loss. |
api/services/storage/prepare_db_direct_dispatch.py, api/tasks/storage/prepare_db_direct.py |
Escape hatches (use only for the specific named situation)¶
These intentionally relax a safety check. They are not hardening toggles — they exist so a known-safe operation is not blocked. Do not bake them into automation.
| Gate | Default | Effect when set | Read by |
|---|---|---|---|
ALLOW_ANONYMOUS_CLIENT_LOG |
unset/off | Allows pre-login browser error reports to reach /api/client-log without a bearer token. Payload sanitization and size limits still apply; use only when pre-auth diagnostics justify the relaxed route. |
api/routes/client_log.py |
OPENAPI_RATE_LIMIT_DISABLED |
unset/off (limiter is on) | Disables the token/IP sliding-window limiter for OpenAPI execution routes. Incident diagnostics only; never leave disabled on reachable ingress. | api/app/openapi_rate_limit.py |
ACCEPT_RBAC_REMOVAL |
unset | Overrides STRICT_RBAC_REMOVAL_HALT for a single run. The value must encode the phase-2 PR, e.g. phase-2-of-pr-<N>, and is cross-checked against the matching phase-1 PR at review. |
scripts/dev/check_rbac_removal.py |
ELB_ALLOW_SUB_MISMATCH |
unset | Lets quick-deploy.sh proceed when the active az login subscription differs from the azd env subscription. Needed when the azd env points at one tenant but you are logged into another. |
scripts/dev/quick-deploy.sh |
ELB_ALLOW_AUTH_BYPASS_IN_CLOUD |
unset | Disarms the frontend deploy die-guard that aborts when VITE_AUTH_DEV_BYPASS=true would be baked into a cloud build. Only for a deliberate non-production sandbox. |
scripts/dev/quick-deploy.sh |
ELB_SKIP_HOOKS |
unset | Skips the version-controlled pre-commit / pre-push CI-mirror git hooks for one command. Emergency use only — never push a red build knowingly. | scripts/dev/install-git-hooks.sh |
Local-debug-only switches (never in a deployed Container App)¶
These change behaviour for a developer iterating from a laptop. Every one keeps
a CONTAINER_APP_NAME guard so a deployed Container App can never honour them.
| Gate | Default | Effect when =true |
Read by |
|---|---|---|---|
AUTH_DEV_BYPASS |
false | Returns a synthetic anonymous caller (OID 00000…0) instead of validating an MSAL bearer token. The cloud is_dev_bypass_caller() guard rejects this identity even if it slips through to a deployed revision. Toggle the full local "real az login" session with scripts/dev/local-run.sh auth-on / auth-off. |
api/auth.py |
LOCAL_DEBUG_AUTO_OPEN_STORAGE |
false | Lets the local backend call ensure_local_storage_access() to enable publicNetworkAccess with defaultAction=Deny, bypass=None, and exactly the caller's public IPv4 when a route has full Storage ARM scope. Normal local-run.sh and VS Code debug launches leave this unset/OFF; enable it only as an explicit local-debug opt-in and close Storage immediately afterward. The CONTAINER_APP_NAME guard prevents deployed apps from honoring it. See charter §9. |
api/services/storage/public_access.py |