Skip to main content

reddb_server/
service_cli.rs

1use std::net::{SocketAddr, TcpListener, TcpStream, ToSocketAddrs};
2use std::path::PathBuf;
3use std::process::Command;
4use std::sync::Arc;
5use std::thread;
6use std::time::Duration;
7
8use crate::auth::store::AuthStore;
9use crate::replication::ReplicationConfig;
10use crate::runtime::RedDBRuntime;
11use crate::service_router::{serve_tcp_router, InProcessRouterConfig};
12use crate::storage::StorageProfileSelection;
13pub use crate::transport::{TransportListenerFailure, TransportListenerState, TransportReadiness};
14use crate::{
15    GrpcServerOptions, RedDBGrpcServer, RedDBOptions, RedDBServer, ServerOptions, StorageMode,
16};
17
18mod server_bootstrap;
19pub use server_bootstrap::{bootstrap_server, run_server, transport_set, BootedNode};
20
21pub const DEFAULT_ROUTER_BIND_ADDR: &str = "127.0.0.1:5050";
22
23/// Detect available CPU cores and suggest worker thread count.
24pub fn detect_runtime_config() -> RuntimeConfig {
25    let cpus = thread::available_parallelism()
26        .map(|n| n.get())
27        .unwrap_or(1);
28
29    // Reserve 1 core for OS, use the rest for workers (minimum 1)
30    let suggested_workers = cpus.saturating_sub(1).max(1);
31
32    RuntimeConfig {
33        available_cpus: cpus,
34        suggested_workers,
35        stack_size: 8 * 1024 * 1024, // 16MB default
36    }
37}
38
39#[derive(Debug, Clone)]
40pub struct RuntimeConfig {
41    pub available_cpus: usize,
42    pub suggested_workers: usize,
43    pub stack_size: usize,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum ServerTransport {
48    Grpc,
49    Http,
50    Wire,
51}
52
53impl ServerTransport {
54    pub const fn as_str(self) -> &'static str {
55        match self {
56            Self::Grpc => "gRPC",
57            Self::Http => "HTTP",
58            Self::Wire => "wire",
59        }
60    }
61
62    pub const fn default_bind_addr(self) -> &'static str {
63        match self {
64            Self::Grpc => "127.0.0.1:55055",
65            Self::Http => "127.0.0.1:5000",
66            Self::Wire => "127.0.0.1:5050",
67        }
68    }
69}
70
71#[derive(Debug, Clone)]
72pub struct ServerCommandConfig {
73    pub path: Option<PathBuf>,
74    pub router_bind_addr: Option<String>,
75    pub router_bind_explicit: bool,
76    pub grpc_bind_addr: Option<String>,
77    pub grpc_bind_explicit: bool,
78    /// TLS-encrypted gRPC bind address. Can run side-by-side with
79    /// `grpc_bind_addr` (e.g. `:55055` plain + `:55555` TLS) or
80    /// stand alone for TLS-only deploys. Defaults to `None`.
81    pub grpc_tls_bind_addr: Option<String>,
82    /// Path to PEM-encoded gRPC server certificate. Resolved through
83    /// `REDDB_GRPC_TLS_CERT` (with `_FILE` companion for k8s secret
84    /// mounts). When `None` and dev-mode is enabled
85    /// (`RED_GRPC_TLS_DEV=1`) the server auto-generates a self-signed
86    /// cert and prints its SHA-256 fingerprint to stderr.
87    pub grpc_tls_cert: Option<PathBuf>,
88    /// Path to PEM-encoded gRPC server private key. Same env-var
89    /// pattern as `grpc_tls_cert`.
90    pub grpc_tls_key: Option<PathBuf>,
91    /// Optional path to a PEM bundle of trust anchors used to verify
92    /// client certificates. When set, the gRPC listener requires
93    /// every client to present a cert that chains to this CA — i.e.
94    /// mutual TLS. When unset, one-way TLS only.
95    pub grpc_tls_client_ca: Option<PathBuf>,
96    pub http_bind_addr: Option<String>,
97    pub http_bind_explicit: bool,
98    /// HTTPS bind address. When set, the HTTP server also serves a
99    /// TLS-terminated listener on this addr. Plain HTTP and HTTPS can
100    /// run side by side (e.g. 5000 plain + 55555 TLS).
101    pub http_tls_bind_addr: Option<String>,
102    /// PEM cert for HTTPS. Reads `REDDB_HTTP_TLS_CERT` / its `_FILE`
103    /// companion when not set explicitly.
104    pub http_tls_cert: Option<PathBuf>,
105    /// PEM key for HTTPS. Reads `REDDB_HTTP_TLS_KEY` / its `_FILE`
106    /// companion when not set explicitly.
107    pub http_tls_key: Option<PathBuf>,
108    /// Optional PEM CA bundle. When set, the HTTPS listener requires
109    /// every client to present a cert that chains to a CA in this
110    /// bundle (mTLS). When unset, plain server-side TLS only.
111    pub http_tls_client_ca: Option<PathBuf>,
112    pub wire_bind_addr: Option<String>,
113    pub wire_bind_explicit: bool,
114    /// TLS-encrypted wire protocol bind address
115    pub wire_tls_bind_addr: Option<String>,
116    /// Path to TLS cert PEM (if None + wire_tls_bind, auto-generate)
117    pub wire_tls_cert: Option<PathBuf>,
118    /// Path to TLS key PEM
119    pub wire_tls_key: Option<PathBuf>,
120    /// PostgreSQL wire protocol bind address (Phase 3.1 PG parity).
121    /// When set the server accepts psql / JDBC / DBeaver clients on
122    /// this port via the v3 protocol. Defaults to None (disabled).
123    pub pg_bind_addr: Option<String>,
124    pub create_if_missing: bool,
125    pub bounded_disk_residency: bool,
126    pub read_only: bool,
127    pub role: String,
128    pub primary_addr: Option<String>,
129    pub storage_profile: StorageProfileSelection,
130    /// Explicit auth enable switch from CLI/env. `--no-auth` remains
131    /// the final override and forces this off at option resolution time.
132    pub auth: bool,
133    /// Require authenticated requests. Implies `auth = true` when set.
134    pub require_auth: bool,
135    pub vault: bool,
136    /// Issue #663 — explicit `--no-auth` / `--dev` flag for local
137    /// no-password mode. When `true`, the boot pipeline force-disables
138    /// auth (`auth.enabled = false`, `auth.require_auth = false`,
139    /// `auth.vault_enabled = false`) and skips
140    /// `AuthStore::bootstrap_from_env`, so an explicit
141    /// `REDDB_USERNAME` + `REDDB_PASSWORD` pair cannot accidentally
142    /// turn anonymous access into a logged-in admin. An unmissable
143    /// warning is emitted at startup. Default `false` — the existing
144    /// implicit no-auth behaviour (just don't set the envs) is
145    /// unchanged.
146    pub no_auth: bool,
147    /// Override worker thread count (None = auto-detect from CPUs)
148    pub workers: Option<usize>,
149    /// Telemetry config (Phase 6 logging). `None` falls back to the
150    /// built-in default derived from `path` + stderr-only.
151    pub telemetry: Option<crate::telemetry::TelemetryConfig>,
152    /// HTTP handler-pool knobs from the CLI layer (issue #574 slice 5).
153    /// Carries flag and env values; red_config and built-in defaults
154    /// are applied later by [`crate::server::http_limits::resolve_http_limits`]
155    /// once the runtime is open.
156    pub http_limits_cli: crate::server::HttpLimitsCliInput,
157    /// `red server --ui` (issue #1047, ADR 0051). When `true`, the HTTP
158    /// listener also serves the pinned red-ui bundle as static assets.
159    /// The bundle directory is resolved at boot from the local cache
160    /// (downloading on first use, ADR 0050) unless `ui_dir` overrides it.
161    pub ui: bool,
162    /// Explicit bundle directory for `--ui` (`--ui-dir <DIR>`). When set,
163    /// it is served as-is with no download — the seam tests and an
164    /// air-gapped deploy use this. `None` resolves the pinned bundle.
165    pub ui_dir: Option<PathBuf>,
166    /// First-boot bootstrap settings resolved from CLI flags and, when
167    /// unset here, from environment variables by the bootstrap layer.
168    pub bootstrap: BootstrapConfig,
169}
170
171#[derive(Debug, Clone, Default)]
172pub struct BootstrapConfig {
173    pub preset: Option<String>,
174    pub manifest: Option<PathBuf>,
175    pub admin_username: Option<String>,
176    pub admin_password: Option<String>,
177    pub cloud_head_admin: Option<String>,
178    pub cloud_head_admin_password: Option<String>,
179    pub customer_admin: Option<String>,
180    pub customer_admin_password: Option<String>,
181    /// `--bootstrap-cert-out <path>` (issue #1589). When set and the
182    /// first-boot self-bootstrap mints an unseal certificate, that
183    /// certificate hex is written to this file (in addition to the
184    /// stdout/stderr emission) so a distroless single-machine init can
185    /// read it back for the subsequent unseal via `REDDB_CERTIFICATE_FILE`
186    /// without scraping machine logs. Written only on the
187    /// bootstrap-creating boot; a re-boot against an existing vault does
188    /// not rewrite it.
189    pub cert_out: Option<PathBuf>,
190}
191
192#[derive(Debug, Clone)]
193pub struct SystemdServiceConfig {
194    pub service_name: String,
195    pub binary_path: PathBuf,
196    pub run_user: String,
197    pub run_group: String,
198    pub data_path: PathBuf,
199    pub router_bind_addr: Option<String>,
200    pub grpc_bind_addr: Option<String>,
201    pub http_bind_addr: Option<String>,
202}
203
204impl SystemdServiceConfig {
205    pub fn data_dir(&self) -> PathBuf {
206        self.data_path
207            .parent()
208            .map(PathBuf::from)
209            .unwrap_or_else(|| PathBuf::from("."))
210    }
211
212    pub fn unit_path(&self) -> PathBuf {
213        PathBuf::from(format!("/etc/systemd/system/{}.service", self.service_name))
214    }
215}
216
217/// Build a sane default `TelemetryConfig` from a server path when the
218/// caller didn't set one explicitly. Writes rotating logs into the
219/// parent directory of the DB file (or `./logs` for in-memory /
220/// pathless runs). Level defaults to `info`, pretty stderr format.
221pub fn default_telemetry_for_path(
222    path: Option<&std::path::Path>,
223) -> crate::telemetry::TelemetryConfig {
224    let log_dir = match path {
225        Some(p) => p
226            .parent()
227            .map(|parent| parent.join("logs"))
228            .or_else(|| Some(std::path::PathBuf::from("./logs"))),
229        None => None, // in-memory — no file, stderr-only
230    };
231    crate::telemetry::TelemetryConfig {
232        log_dir,
233        file_prefix: "reddb.log".to_string(),
234        level_filter: std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string()),
235        format: if std::io::IsTerminal::is_terminal(&std::io::stderr()) {
236            crate::telemetry::LogFormat::Pretty
237        } else {
238            crate::telemetry::LogFormat::Json
239        },
240        rotation_keep_days: 14,
241        service_name: "reddb",
242        // Implicit defaults — no CLI flag has claimed these values yet.
243        level_explicit: false,
244        format_explicit: false,
245        rotation_keep_days_explicit: false,
246        file_prefix_explicit: false,
247        log_dir_explicit: false,
248        log_file_disabled: false,
249    }
250}
251
252/// Metadata key used to thread the parsed backup config from
253/// `to_db_options` down to runner threads. The runner reads it back
254/// (via `runner_backup_intervals`) to spawn the periodic checkpointer
255/// and WAL-flush tasks. Threading through `metadata` avoids extending
256/// `RedDBOptions` with a public field that has no meaning for
257/// library consumers.
258const BACKUP_INTERVAL_META_CHECKPOINT: &str = "red.boot.backup.checkpoint_interval_secs";
259const BACKUP_INTERVAL_META_WAL_FLUSH: &str = "red.boot.backup.wal_flush_interval_secs";
260const BACKUP_KIND_META: &str = "red.boot.backup.backend_kind";
261/// Issue #519 — threaded through `metadata` like the existing interval
262/// values. `0` (default) means "feature disabled" and the runner skips
263/// the lag-monitor wiring entirely.
264const BACKUP_PAUSE_ON_LAG_META: &str = "red.boot.backup.pause_on_lag_secs";
265
266/// Issue #663 — metadata key set by `to_db_options` when the operator
267/// passes `--no-auth` / `--dev`. Read in `build_runtime_with_telemetry`
268/// to (a) skip `AuthStore::bootstrap_from_env` (so a stray
269/// `REDDB_USERNAME`/`REDDB_PASSWORD` cannot auto-create an admin) and
270/// (b) emit the loud "auth disabled" warning. Threaded via `metadata`
271/// — rather than a public `RedDBOptions` field — to keep the flag a
272/// CLI/boot concern with no meaning for library consumers.
273pub(crate) const NO_AUTH_META: &str = "red.boot.no_auth";
274
275/// Returns `true` when `--no-auth` / `--dev` was active for this boot,
276/// i.e. when `to_db_options` stamped [`NO_AUTH_META`] on `options.metadata`.
277pub(crate) fn no_auth_active(options: &RedDBOptions) -> bool {
278    options
279        .metadata
280        .get(NO_AUTH_META)
281        .map(|v| v == "true")
282        .unwrap_or(false)
283}
284
285/// Loud, unmissable warning printed when the operator opts into
286/// anonymous access via `--no-auth` / `--dev`. Goes to stderr (always
287/// visible at startup) **and** the tracing layer (captured by file
288/// logs once telemetry is wired).
289const NO_AUTH_WARNING: &str =
290    "⚠ auth disabled (--no-auth) — anonymous access, do NOT use in production";
291
292impl ServerCommandConfig {
293    fn to_db_options(&self) -> Result<RedDBOptions, String> {
294        let mut options = match &self.path {
295            Some(path) => RedDBOptions::persistent(path),
296            None => RedDBOptions::in_memory(),
297        };
298
299        options.mode = StorageMode::Persistent;
300        options.create_if_missing = self.create_if_missing;
301        options.bounded_disk_residency = self.bounded_disk_residency;
302        // PLAN.md Phase 4.3 — read_only resolution priority:
303        //   1. CLI flag (`--readonly`) — explicit operator intent.
304        //   2. `RED_READONLY=true` env — orchestrator override.
305        //   3. Persisted `<data>/.runtime-state.json` from a prior
306        //      `POST /admin/readonly` — survives restart.
307        //   4. Default `false`.
308        options.read_only = self.read_only
309            || env_nonempty("RED_READONLY")
310                .or_else(|| env_nonempty("REDDB_READONLY"))
311                .map(|v| matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on"))
312                .unwrap_or(false)
313            || self.path.as_ref().is_some_and(|data_path| {
314                crate::server::handlers_admin::load_runtime_readonly(std::path::Path::new(
315                    data_path,
316                ))
317                .unwrap_or(false)
318            });
319
320        options.replication = match self.role.as_str() {
321            "primary" => ReplicationConfig::primary(),
322            "replica" => {
323                let primary_addr = self
324                    .primary_addr
325                    .clone()
326                    .unwrap_or_else(|| "http://127.0.0.1:55055".to_string());
327                // Public-mutation rejection on replicas is enforced by
328                // `WriteGate` at the runtime/RPC boundary (PLAN.md W1).
329                // Leaving `options.read_only = false` keeps the pager
330                // writable so the internal logical-WAL apply path can
331                // ingest records from the primary; WriteGate ensures no
332                // client request reaches storage.
333                ReplicationConfig::replica(primary_addr)
334            }
335            _ => ReplicationConfig::standalone(),
336        };
337        options.storage_profile = self.storage_profile.validate()?;
338
339        let no_auth = self.no_auth || env_truthy("REDDB_NO_AUTH") || env_truthy("REDDB_DEV");
340        let preset = self.bootstrap.resolved_preset_for_options()?;
341        let preset_requires_auth = matches!(preset.as_str(), PRESET_PRODUCTION | PRESET_CLOUD);
342
343        if self.auth || env_truthy("REDDB_AUTH") || preset_requires_auth {
344            options.auth.enabled = true;
345        }
346        if self.require_auth || env_truthy("REDDB_REQUIRE_AUTH") || preset_requires_auth {
347            options.auth.enabled = true;
348            options.auth.require_auth = true;
349        }
350        if self.vault || env_truthy("REDDB_VAULT") || preset_requires_auth {
351            options.auth.vault_enabled = true;
352        }
353
354        // Issue #663 — `--no-auth` / `--dev` is the last word on auth
355        // for this boot: force every auth knob off, regardless of any
356        // env-derived config (`--vault`, `REDDB_USERNAME`/`PASSWORD`,
357        // OAuth, cert) the operator may also have
358        // set. We *also* stamp [`NO_AUTH_META`] so the auth-store
359        // builder downstream knows to skip `bootstrap_from_env`
360        // (which would otherwise auto-create an admin from
361        // `REDDB_USERNAME`/`REDDB_PASSWORD` even with auth disabled,
362        // a footgun for the local-dev workflow this flag exists to
363        // support).
364        if no_auth {
365            options.auth.enabled = false;
366            options.auth.require_auth = false;
367            options.auth.vault_enabled = false;
368            options
369                .metadata
370                .insert(NO_AUTH_META.to_string(), "true".to_string());
371        }
372
373        // Issue #1587 — first-boot self-bootstrap. A single distroless
374        // `red server` must be able to create the paged vault in place and
375        // serve in one process. The default embedded single-file packaging
376        // routes storage through `UnifiedStore::with_config`, which exposes no
377        // pager, so the vault gate in `build_runtime_with_bootstrap` aborts
378        // with "vault requires a paged database (persistent mode)". The
379        // separate `red bootstrap` subcommand escapes this by opening with the
380        // operational-directory packaging; compose the same storage selection
381        // here so the fresh database is paged, the vault pages exist, and the
382        // existing preset machinery (`ProceedLocal` → `apply_preset_from_config`)
383        // can mint the admins + certificate before listeners come up.
384        //
385        // Gated on (path absent AND a bootstrap intent is present AND the vault
386        // is enabled): an existing database keeps whatever packaging is already
387        // on disk (so a re-boot just serves — no re-bootstrap, no cert churn),
388        // and a fresh path with no intent keeps today's behaviour (a clear
389        // "vault requires a paged database" error rather than a silently
390        // created empty vault). `--no-auth` / `--dev` cleared `vault_enabled`
391        // above, so the development carveout never triggers first-boot either.
392        let bootstrap_intent_present = !matches!(
393            self.bootstrap.auth_bootstrap_input(),
394            crate::cluster::AuthBootstrapInput::None
395        );
396        let path_absent = self.path.as_ref().is_some_and(|path| !path.exists());
397        // Mirrors `embedded_single_file_selected`: only this profile lacks a
398        // pager, so it is the only one that needs promotion to create a vault.
399        let embedded_single_file = options.storage_profile.deploy_profile
400            == crate::storage::DeployProfile::Embedded
401            && options.storage_profile.packaging == crate::storage::StoragePackaging::SingleFile;
402        if options.auth.vault_enabled
403            && bootstrap_intent_present
404            && path_absent
405            && embedded_single_file
406        {
407            // Explicit disk residency already creates a native pager. Keep its
408            // single-file selection instead of routing it through the legacy
409            // snapshot factory's operational-directory bootstrap workaround.
410            if !options.bounded_disk_residency {
411                options.storage_profile.packaging =
412                    crate::storage::StoragePackaging::OperationalDirectory;
413            }
414            options.create_if_missing = true;
415            tracing::info!(
416                target: "reddb::bootstrap",
417                "first-boot self-bootstrap: creating paged vault in place for a fresh database with a bootstrap intent"
418            );
419        }
420
421        // Issue #652 — Control Event Ledger Compliance Mode.
422        // `REDDB_COMPLIANCE_MODE=true|1|yes|on` makes the producer
423        // slices (652b/c/d) fail-closed on ledger persistence
424        // failures. Default `false` — log-and-continue on emit error.
425        if let Some(raw) = env_nonempty("REDDB_COMPLIANCE_MODE") {
426            options.control_events.compliance_mode = matches!(
427                raw.to_ascii_lowercase().as_str(),
428                "1" | "true" | "yes" | "on"
429            );
430        }
431        if preset == PRESET_REGULATED {
432            options.control_events.compliance_mode = true;
433            options.query_audit = crate::runtime::query_audit::QueryAuditConfig::regulated();
434        }
435
436        // Issue #517 — canonical `REDDB_BACKUP_*` contract takes
437        // precedence. On Err, surface the partial-config message so
438        // boot exits non-zero with a clear operator message. On
439        // Ok(None), fall through to the legacy backend-from-env path.
440        match crate::backup_bootstrap::from_env(|k| std::env::var(k).ok()) {
441            Err(msg) => {
442                return Err(format!("backup bootstrap: {msg}"));
443            }
444            Ok(Some(cfg)) => {
445                apply_backup_config(&mut options, &cfg);
446            }
447            Ok(None) => {
448                configure_remote_backend_from_env(&mut options);
449            }
450        }
451
452        if options.remote_backend.is_some()
453            || options
454                .metadata
455                .contains_key(BACKUP_INTERVAL_META_CHECKPOINT)
456        {
457            let mut selection = options.storage_profile;
458            selection.managed_backup = true;
459            options.storage_profile = selection.validate()?;
460        }
461
462        Ok(options)
463    }
464
465    pub fn enabled_transports(&self) -> Vec<ServerTransport> {
466        let mut transports = Vec::with_capacity(3);
467        if self.router_bind_addr.is_some() || self.grpc_bind_addr.is_some() {
468            transports.push(ServerTransport::Grpc);
469        }
470        if self.router_bind_addr.is_some() || self.http_bind_addr.is_some() {
471            transports.push(ServerTransport::Http);
472        }
473        if self.router_bind_addr.is_some() || self.wire_bind_addr.is_some() {
474            transports.push(ServerTransport::Wire);
475        }
476        transports
477    }
478}
479
480/// Read an env var, treating empty / whitespace-only as `None`.
481/// Honors the `<NAME>_FILE` convention. Re-exports the shared
482/// `crate::utils::env_with_file_fallback` helper so call sites in
483/// this module can keep their short local name.
484/// A non-empty env value, also honouring the `<NAME>_FILE` companion for
485/// mounted secrets. Reads the file on demand instead of copying its
486/// contents into the process environment, so the secret never appears in
487/// `/proc/<pid>/environ` or in a child's inherited env.
488fn env_nonempty(name: &str) -> Option<String> {
489    if let Some(value) = crate::utils::env_with_file_fallback(name) {
490        let trimmed = value.trim();
491        if !trimmed.is_empty() {
492            return Some(trimmed.to_string());
493        }
494        return None;
495    }
496    env_nonempty_plain(name)
497}
498
499fn env_nonempty_plain(name: &str) -> Option<String> {
500    crate::utils::env_with_file_fallback(name)
501}
502
503fn env_truthy(name: &str) -> bool {
504    env_nonempty(name)
505        .map(|v| matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on"))
506        .unwrap_or(false)
507}
508
509impl BootstrapConfig {
510    fn resolved_preset(&self) -> String {
511        self.preset
512            .as_deref()
513            .map(str::trim)
514            .filter(|s| !s.is_empty())
515            .map(str::to_string)
516            .or_else(|| env_nonempty(BOOTSTRAP_PRESET_ENV).or_else(|| env_nonempty(PRESET_ENV)))
517            .unwrap_or_else(|| PRESET_SIMPLE.to_string())
518    }
519
520    fn resolved_preset_for_options(&self) -> Result<String, String> {
521        if let Some(path) = self.selected_manifest() {
522            if let Some(manifest_bootstrap) =
523                crate::cli::bootstrap_manifest::bootstrap_config_from_manifest_file(&path)?
524            {
525                return Ok(manifest_bootstrap.resolved_preset());
526            }
527        }
528        Ok(self.resolved_preset())
529    }
530
531    fn selected_manifest(&self) -> Option<PathBuf> {
532        self.manifest.clone().or_else(|| {
533            env_nonempty(crate::cli::bootstrap_manifest::MANIFEST_ENV).map(PathBuf::from)
534        })
535    }
536
537    /// Resolve the `--bootstrap-cert-out` target (issue #1589): the CLI
538    /// flag wins, else the `REDDB_BOOTSTRAP_CERT_OUT` env (with its
539    /// `_FILE` companion) — matching the flag/env pattern used by the
540    /// other bootstrap inputs.
541    fn selected_cert_out(&self) -> Option<PathBuf> {
542        self.cert_out
543            .clone()
544            .or_else(|| env_nonempty(BOOTSTRAP_CERT_OUT_ENV).map(PathBuf::from))
545    }
546
547    /// Classify the auth bootstrap this config would perform, for the
548    /// cluster bootstrap authority seam (ADR 0058). A manifest wins over
549    /// preset/credential env because it is the broadest authority input.
550    fn auth_bootstrap_input(&self) -> crate::cluster::AuthBootstrapInput {
551        use crate::cluster::AuthBootstrapInput;
552        if self.selected_manifest().is_some() {
553            return AuthBootstrapInput::Manifest;
554        }
555        let preset = self.resolved_preset();
556        let preset_creates_auth = matches!(
557            preset.as_str(),
558            PRESET_PRODUCTION | PRESET_CLOUD | PRESET_REGULATED
559        );
560        // Even the `simple` preset auto-creates an admin when
561        // `REDDB_USERNAME` + `REDDB_PASSWORD` are present (see
562        // `AuthStore::bootstrap_from_env`), so treat credential env as
563        // auth bootstrap input too.
564        let credentials_present =
565            self.production_username().is_some() && self.production_password().is_some();
566        if preset_creates_auth || credentials_present {
567            AuthBootstrapInput::Env
568        } else {
569            AuthBootstrapInput::None
570        }
571    }
572
573    fn explicit_value(value: &Option<String>) -> Option<String> {
574        value
575            .as_deref()
576            .map(str::trim)
577            .filter(|s| !s.is_empty())
578            .map(str::to_string)
579    }
580
581    fn value_or_env(value: &Option<String>, env: &str) -> Option<String> {
582        Self::explicit_value(value).or_else(|| env_nonempty(env))
583    }
584
585    fn production_username(&self) -> Option<String> {
586        Self::value_or_env(&self.admin_username, "REDDB_USERNAME")
587    }
588
589    fn production_password(&self) -> Option<String> {
590        Self::value_or_env(&self.admin_password, "REDDB_PASSWORD")
591    }
592
593    fn cloud_head_username(&self) -> Option<String> {
594        Self::explicit_value(&self.cloud_head_admin)
595            .or_else(|| Self::explicit_value(&self.admin_username))
596            .or_else(|| env_nonempty("REDDB_CLOUD_HEAD_ADMIN"))
597            .or_else(|| env_nonempty("REDDB_USERNAME"))
598    }
599
600    fn cloud_head_password(&self) -> Option<String> {
601        Self::explicit_value(&self.cloud_head_admin_password)
602            .or_else(|| Self::explicit_value(&self.admin_password))
603            .or_else(|| env_nonempty("REDDB_CLOUD_HEAD_ADMIN_PASSWORD"))
604            .or_else(|| env_nonempty("REDDB_PASSWORD"))
605    }
606
607    fn customer_username(&self) -> Option<String> {
608        Self::value_or_env(&self.customer_admin, "REDDB_CUSTOMER_ADMIN")
609    }
610
611    fn customer_password(&self) -> Option<String> {
612        Self::value_or_env(
613            &self.customer_admin_password,
614            "REDDB_CUSTOMER_ADMIN_PASSWORD",
615        )
616    }
617}
618
619/// Apply a parsed [`BackupConfig`] to `options`. Wires the S3
620/// backend via `with_remote_backend` + `with_atomic_remote_backend`
621/// when the `backend-s3` feature is on, stashes intervals + backend
622/// kind in `metadata` so the runner can spawn the periodic tasks,
623/// and emits the startup INFO log required by #517.
624fn apply_backup_config(options: &mut RedDBOptions, cfg: &crate::backup_bootstrap::BackupConfig) {
625    let endpoint_host = endpoint_host(&cfg.endpoint);
626
627    options.metadata.insert(
628        BACKUP_INTERVAL_META_CHECKPOINT.to_string(),
629        cfg.checkpoint_interval_secs.to_string(),
630    );
631    options.metadata.insert(
632        BACKUP_INTERVAL_META_WAL_FLUSH.to_string(),
633        cfg.wal_flush_interval_secs.to_string(),
634    );
635    options
636        .metadata
637        .insert(BACKUP_KIND_META.to_string(), "s3".to_string());
638    options.metadata.insert(
639        BACKUP_PAUSE_ON_LAG_META.to_string(),
640        cfg.pause_on_lag_secs.to_string(),
641    );
642
643    #[cfg(feature = "backend-s3")]
644    {
645        let s3_cfg = crate::storage::backend::S3Config {
646            endpoint: cfg.endpoint.clone(),
647            bucket: cfg.bucket.clone(),
648            key_prefix: cfg.prefix.clone(),
649            access_key: cfg.access_key_id.clone(),
650            secret_key: cfg.secret_access_key.clone(),
651            region: cfg.region.clone(),
652            path_style: true,
653        };
654        let mut backend = crate::storage::backend::S3Backend::new(s3_cfg);
655        if let Some(path) = &cfg.credentials_file {
656            backend = backend.with_credentials_file(path);
657        }
658        let backend = Arc::new(backend);
659        options.remote_backend = Some(backend.clone());
660        options.remote_backend_atomic = Some(backend);
661        // Use the operator-supplied prefix as the snapshot key root.
662        // The existing helpers (`default_snapshot_prefix`,
663        // `default_wal_archive_prefix`) derive sub-prefixes from the
664        // parent of `remote_key`.
665        let trimmed = cfg.prefix.trim_end_matches('/');
666        options.remote_key = Some(reddb_file::remote_database_key(trimmed));
667
668        tracing::info!(
669            backend = "s3",
670            endpoint = %endpoint_host,
671            bucket = %cfg.bucket,
672            prefix = %cfg.prefix,
673            checkpoint_interval_secs = cfg.checkpoint_interval_secs,
674            wal_flush_interval_secs = cfg.wal_flush_interval_secs,
675            "backup backend configured from REDDB_BACKUP_* env"
676        );
677    }
678
679    #[cfg(not(feature = "backend-s3"))]
680    {
681        tracing::warn!(
682            backend = "s3",
683            endpoint = %endpoint_host,
684            bucket = %cfg.bucket,
685            prefix = %cfg.prefix,
686            "REDDB_BACKUP_S3_* configured but binary built without `backend-s3` feature; \
687             backend wiring skipped (archiver/checkpointer also disabled)"
688        );
689    }
690}
691
692fn endpoint_host(endpoint: &str) -> &str {
693    let after_scheme = endpoint
694        .split_once("://")
695        .map(|(_, r)| r)
696        .unwrap_or(endpoint);
697    after_scheme.split('/').next().unwrap_or(after_scheme)
698}
699
700/// If `options` carry backup-task intervals threaded in via
701/// [`apply_backup_config`], spawn periodic checkpointer + WAL-flush
702/// tasks against `runtime`. Returns a `BackupTasksHandle` that
703/// stops the tasks when dropped; runners keep it alive for the
704/// server lifetime.
705fn spawn_backup_tasks_if_configured(
706    options: &RedDBOptions,
707    runtime: &RedDBRuntime,
708) -> Option<BackupTasksHandle> {
709    let checkpoint_secs: u64 = options
710        .metadata
711        .get(BACKUP_INTERVAL_META_CHECKPOINT)?
712        .parse()
713        .ok()?;
714    let wal_secs: u64 = options
715        .metadata
716        .get(BACKUP_INTERVAL_META_WAL_FLUSH)?
717        .parse()
718        .ok()?;
719    // Issue #519 — opt-in graceful read-only when remote archive lag
720    // exceeds the threshold. `0` (default) keeps legacy behaviour.
721    let pause_on_lag_secs: u64 = options
722        .metadata
723        .get(BACKUP_PAUSE_ON_LAG_META)
724        .and_then(|raw| raw.parse().ok())
725        .unwrap_or(0);
726    options.remote_backend.as_ref()?;
727
728    let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
729
730    // Stamp the gate with the threshold + a "now" baseline so the
731    // first auto-pause can only fire after `pause_on_lag_secs` of
732    // archive silence. The poller below re-evaluates on the same
733    // clock the archive-task wrapper uses.
734    if pause_on_lag_secs > 0 {
735        let now_ms = std::time::SystemTime::now()
736            .duration_since(std::time::UNIX_EPOCH)
737            .map(|d| d.as_millis() as u64)
738            .unwrap_or(0);
739        runtime
740            .write_gate()
741            .configure_archive_lag_pause(pause_on_lag_secs, now_ms);
742        tracing::info!(
743            pause_on_lag_secs,
744            "archive-lag pause enabled — engine will transition to read-only after threshold seconds of archiver silence"
745        );
746    }
747
748    let checkpoint_handle = {
749        let stop = Arc::clone(&stop);
750        let runtime = runtime.clone();
751        let interval = Duration::from_secs(checkpoint_secs);
752        thread::Builder::new()
753            .name("red-checkpointer".into())
754            .spawn(move || {
755                periodic_loop(stop, interval, move || {
756                    if let Err(err) = runtime.checkpoint() {
757                        tracing::warn!(error = %err, "periodic checkpoint failed");
758                    }
759                })
760            })
761            .ok()
762    };
763
764    let archiver_handle = {
765        let stop = Arc::clone(&stop);
766        let runtime = runtime.clone();
767        let interval = Duration::from_secs(wal_secs);
768        let lag_enabled = pause_on_lag_secs > 0;
769        thread::Builder::new()
770            .name("red-wal-archiver".into())
771            .spawn(move || {
772                periodic_loop(stop, interval, move || match runtime.trigger_backup() {
773                    Ok(_) if lag_enabled => {
774                        let now_ms = std::time::SystemTime::now()
775                            .duration_since(std::time::UNIX_EPOCH)
776                            .map(|d| d.as_millis() as u64)
777                            .unwrap_or(0);
778                        runtime.write_gate().record_archive_success(now_ms);
779                        // Same-tick re-evaluation: catching up while
780                        // already auto-paused must auto-resume without
781                        // waiting for the poller's cadence.
782                        runtime.write_gate().evaluate_archive_lag(now_ms);
783                    }
784                    Ok(_) => {}
785                    Err(err) => {
786                        tracing::warn!(error = %err, "periodic WAL archive/backup failed");
787                    }
788                })
789            })
790            .ok()
791    };
792
793    // Issue #519 — lag poller. Wakes on a short cadence so a frozen
794    // archiver (the worst case) still flips the gate within ~5s of
795    // crossing the threshold, instead of waiting up to a full
796    // `wal_secs` for the next archive attempt that may never come.
797    let lag_monitor_handle = if pause_on_lag_secs > 0 {
798        let stop = Arc::clone(&stop);
799        let runtime = runtime.clone();
800        // 5s is short enough that the threshold is honoured tightly
801        // and long enough that the atomic loads stay invisible at the
802        // process level.
803        let interval = Duration::from_secs(5);
804        thread::Builder::new()
805            .name("red-archive-lag-monitor".into())
806            .spawn(move || {
807                periodic_loop(stop, interval, move || {
808                    let now_ms = std::time::SystemTime::now()
809                        .duration_since(std::time::UNIX_EPOCH)
810                        .map(|d| d.as_millis() as u64)
811                        .unwrap_or(0);
812                    let was_paused = runtime.write_gate().is_auto_paused();
813                    let now_paused = runtime.write_gate().evaluate_archive_lag(now_ms);
814                    if now_paused && !was_paused {
815                        tracing::warn!(
816                            pause_on_lag_secs,
817                            last_archive_at_ms = runtime.write_gate().last_archive_at_ms(),
818                            "WAL archive lag exceeded threshold — entering graceful read-only mode (issue #519)"
819                        );
820                    } else if !now_paused && was_paused {
821                        tracing::info!(
822                            "WAL archive caught up — exiting graceful read-only mode (issue #519)"
823                        );
824                    }
825                })
826            })
827            .ok()
828    } else {
829        None
830    };
831
832    tracing::info!(
833        checkpoint_interval_secs = checkpoint_secs,
834        wal_flush_interval_secs = wal_secs,
835        "backup tasks spawned (checkpointer + WAL archiver)"
836    );
837
838    Some(BackupTasksHandle {
839        stop,
840        _checkpoint_handle: checkpoint_handle,
841        _archiver_handle: archiver_handle,
842        _lag_monitor_handle: lag_monitor_handle,
843    })
844}
845
846/// Shutdown handle for the periodic checkpointer + archiver tasks.
847/// Drop signals both loops to exit on their next wake.
848pub struct BackupTasksHandle {
849    stop: Arc<std::sync::atomic::AtomicBool>,
850    _checkpoint_handle: Option<thread::JoinHandle<()>>,
851    _archiver_handle: Option<thread::JoinHandle<()>>,
852    /// Issue #519 — periodic archive-lag poller, only spawned when
853    /// `REDDB_BACKUP_PAUSE_ON_LAG_SECS > 0`.
854    _lag_monitor_handle: Option<thread::JoinHandle<()>>,
855}
856
857impl Drop for BackupTasksHandle {
858    fn drop(&mut self) {
859        self.stop.store(true, std::sync::atomic::Ordering::Release);
860    }
861}
862
863fn periodic_loop<F: FnMut()>(
864    stop: Arc<std::sync::atomic::AtomicBool>,
865    interval: Duration,
866    mut tick: F,
867) {
868    // Wake on a short cadence so shutdown is responsive even when the
869    // operator-configured interval is large (e.g. 1h checkpoint).
870    let wake = Duration::from_secs(1);
871    let mut elapsed = Duration::ZERO;
872    while !stop.load(std::sync::atomic::Ordering::Acquire) {
873        thread::sleep(wake);
874        elapsed += wake;
875        if elapsed >= interval {
876            tick();
877            elapsed = Duration::ZERO;
878        }
879    }
880}
881
882fn configure_remote_backend_from_env(options: &mut RedDBOptions) {
883    // PLAN.md (cloud-agnostic) — prefer the new spelling
884    // `RED_BACKEND`; accept the legacy `REDDB_REMOTE_BACKEND` for
885    // existing dev installs. `none` (default) means standalone — no
886    // remote backend, valid for development and on-prem without
887    // remote.
888    let backend = env_nonempty("RED_BACKEND")
889        .or_else(|| env_nonempty("REDDB_REMOTE_BACKEND"))
890        .unwrap_or_else(|| "none".to_string())
891        .to_ascii_lowercase();
892
893    match backend.as_str() {
894        // Universal S3-compatible — covers AWS, R2, MinIO, Ceph,
895        // GCS-interop, B2, DO Spaces, Wasabi, Garage, SeaweedFS,
896        // IDrive, Storj. The `path_style` toggle in S3Config picks
897        // the right addressing for self-hosted vs hosted.
898        "s3" | "minio" | "r2" => {
899            #[cfg(feature = "backend-s3")]
900            {
901                if let Some(config) = s3_config_from_env() {
902                    let remote_key = env_nonempty("RED_REMOTE_KEY")
903                        .or_else(|| env_nonempty("REDDB_REMOTE_KEY"))
904                        .unwrap_or_else(|| reddb_file::remote_database_key("clusters/dev"));
905                    let backend = Arc::new(crate::storage::backend::S3Backend::new(config));
906                    options.remote_backend = Some(backend.clone());
907                    options.remote_backend_atomic = Some(backend);
908                    options.remote_key = Some(remote_key);
909                }
910            }
911            #[cfg(not(feature = "backend-s3"))]
912            {
913                tracing::warn!(
914                    backend = %backend,
915                    "RED_BACKEND={backend} requested but binary was built without `backend-s3` feature"
916                );
917            }
918        }
919        // Filesystem backend (NFS/EFS/SMB/local-disk). PLAN.md spec
920        // calls it `fs`; legacy code shipped it as `local`. Both
921        // names map to LocalBackend, with the remote_key derived
922        // from `RED_FS_PATH` + a per-database suffix when provided.
923        "fs" | "local" => {
924            let base_path = env_nonempty("RED_FS_PATH").or_else(|| env_nonempty("REDDB_FS_PATH"));
925            let remote_key = match (
926                base_path,
927                env_nonempty("RED_REMOTE_KEY").or_else(|| env_nonempty("REDDB_REMOTE_KEY")),
928            ) {
929                (Some(base), Some(rel)) => Some(format!(
930                    "{}/{}",
931                    base.trim_end_matches('/'),
932                    rel.trim_start_matches('/')
933                )),
934                (Some(base), None) => Some(reddb_file::remote_database_key(&format!(
935                    "{}/clusters/dev",
936                    base.trim_end_matches('/')
937                ))),
938                (None, Some(rel)) => Some(rel),
939                (None, None) => None,
940            };
941            if let Some(key) = remote_key {
942                let backend = Arc::new(crate::storage::backend::LocalBackend);
943                options.remote_backend = Some(backend.clone());
944                options.remote_backend_atomic = Some(backend);
945                options.remote_key = Some(key);
946            }
947        }
948        // Generic HTTP backend (PLAN.md Phase 2.3). Maximum
949        // portability: any service exposing PUT/GET/DELETE serves
950        // as a backup target. Optional auth via *_FILE secret
951        // path keeps the token out of the env.
952        "http" => {
953            let base_url = match env_nonempty("RED_HTTP_BACKEND_URL")
954                .or_else(|| env_nonempty("REDDB_HTTP_BACKEND_URL"))
955            {
956                Some(u) => u,
957                None => {
958                    tracing::warn!(
959                        "RED_BACKEND=http requires RED_HTTP_BACKEND_URL — backend disabled"
960                    );
961                    return;
962                }
963            };
964            let prefix = env_nonempty("RED_HTTP_BACKEND_PREFIX")
965                .or_else(|| env_nonempty("REDDB_HTTP_BACKEND_PREFIX"))
966                .unwrap_or_default();
967            let auth_header = if let Some(path) = env_nonempty("RED_HTTP_BACKEND_AUTH_HEADER_FILE")
968                .or_else(|| env_nonempty("REDDB_HTTP_BACKEND_AUTH_HEADER_FILE"))
969            {
970                std::fs::read_to_string(&path)
971                    .ok()
972                    .map(|s| s.trim().to_string())
973                    .filter(|s| !s.is_empty())
974            } else {
975                env_nonempty("RED_HTTP_BACKEND_AUTH_HEADER")
976                    .or_else(|| env_nonempty("REDDB_HTTP_BACKEND_AUTH_HEADER"))
977            };
978
979            let mut config =
980                crate::storage::backend::HttpBackendConfig::new(base_url).with_prefix(prefix);
981            if let Some(auth) = auth_header {
982                config = config.with_auth_header(auth);
983            }
984            let conditional_writes = env_truthy("RED_HTTP_CONDITIONAL_WRITES")
985                || env_truthy("RED_HTTP_BACKEND_CONDITIONAL_WRITES")
986                || env_truthy("REDDB_HTTP_BACKEND_CONDITIONAL_WRITES");
987            config = config.with_conditional_writes(conditional_writes);
988            // Always populate the snapshot-transport handle. When the
989            // operator confirmed CAS support, also populate the atomic
990            // handle via AtomicHttpBackend — without that flag,
991            // LeaseStore must remain unreachable on this backend.
992            if conditional_writes {
993                match crate::storage::backend::AtomicHttpBackend::try_new(config.clone()) {
994                    Ok(atomic) => {
995                        let atomic_arc = Arc::new(atomic);
996                        options.remote_backend = Some(atomic_arc.clone());
997                        options.remote_backend_atomic = Some(atomic_arc);
998                    }
999                    Err(err) => {
1000                        tracing::warn!(error = %err, "AtomicHttpBackend init failed; falling back to plain HTTP (no CAS)");
1001                        options.remote_backend =
1002                            Some(Arc::new(crate::storage::backend::HttpBackend::new(config)));
1003                    }
1004                }
1005            } else {
1006                options.remote_backend =
1007                    Some(Arc::new(crate::storage::backend::HttpBackend::new(config)));
1008            }
1009            options.remote_key = env_nonempty("RED_REMOTE_KEY")
1010                .or_else(|| env_nonempty("REDDB_REMOTE_KEY"))
1011                .or_else(|| Some(reddb_file::remote_database_key("clusters/dev")));
1012        }
1013        // `none` is the explicit standalone — no remote, no backup
1014        // pipeline. Boot never blocks on network reachability.
1015        "none" | "" => {}
1016        other => {
1017            tracing::warn!(
1018                backend = %other,
1019                "unknown RED_BACKEND value — supported: s3 | fs | http | none"
1020            );
1021        }
1022    }
1023}
1024
1025/// Resolve a value from env, accepting both the cloud-agnostic
1026/// `RED_S3_*` family (PLAN.md spec) and the legacy `REDDB_S3_*` form
1027/// kept for existing dev installs. The new spelling wins; the
1028/// legacy spelling is read with a warning hint in callers' logs.
1029#[cfg(feature = "backend-s3")]
1030fn env_s3(suffix: &str) -> Option<String> {
1031    env_nonempty(&format!("RED_S3_{suffix}"))
1032        .or_else(|| env_nonempty(&format!("REDDB_S3_{suffix}")))
1033}
1034
1035/// Read a secret value from either the literal env var or a file
1036/// path supplied via `*_FILE` (PLAN.md spec — compatible with
1037/// Kubernetes / Docker Secrets, HashiCorp Vault Agent, sealed-
1038/// secrets). The `_FILE` variant wins so an operator can set it to
1039/// override the inline value without touching the inline env.
1040#[cfg(feature = "backend-s3")]
1041fn env_s3_secret(suffix: &str) -> Option<String> {
1042    let file_key_red = format!("RED_S3_{suffix}_FILE");
1043    let file_key_legacy = format!("REDDB_S3_{suffix}_FILE");
1044    if let Some(path) = env_nonempty(&file_key_red).or_else(|| env_nonempty(&file_key_legacy)) {
1045        return std::fs::read_to_string(&path)
1046            .ok()
1047            .map(|s| s.trim().to_string())
1048            .filter(|s| !s.is_empty());
1049    }
1050    env_s3(suffix)
1051}
1052
1053#[cfg(feature = "backend-s3")]
1054fn s3_config_from_env() -> Option<crate::storage::backend::S3Config> {
1055    let endpoint = env_s3("ENDPOINT")?;
1056    let bucket = env_s3("BUCKET")?;
1057    let access_key = env_s3_secret("ACCESS_KEY")?;
1058    let secret_key = env_s3_secret("SECRET_KEY")?;
1059    let region = env_s3("REGION").unwrap_or_else(|| "us-east-1".to_string());
1060    let key_prefix = env_s3("KEY_PREFIX")
1061        .or_else(|| env_s3("PREFIX"))
1062        .unwrap_or_default();
1063    let path_style = env_s3("PATH_STYLE")
1064        .map(|v| matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on"))
1065        .unwrap_or(true);
1066    Some(crate::storage::backend::S3Config {
1067        endpoint,
1068        bucket,
1069        key_prefix,
1070        access_key,
1071        secret_key,
1072        region,
1073        path_style,
1074    })
1075}
1076
1077pub fn render_systemd_unit(config: &SystemdServiceConfig) -> String {
1078    let data_dir = config.data_dir();
1079    let exec_start = render_systemd_exec_start(config);
1080    format!(
1081        "[Unit]\n\
1082Description=RedDB unified database service\n\
1083After=network-online.target\n\
1084Wants=network-online.target\n\
1085\n\
1086[Service]\n\
1087Type=simple\n\
1088User={user}\n\
1089Group={group}\n\
1090WorkingDirectory={workdir}\n\
1091ExecStart={exec_start}\n\
1092Restart=always\n\
1093RestartSec=2\n\
1094LimitSTACK=16M\n\
1095NoNewPrivileges=true\n\
1096PrivateTmp=true\n\
1097ProtectSystem=strict\n\
1098ProtectHome=true\n\
1099ProtectControlGroups=true\n\
1100ProtectKernelTunables=true\n\
1101ProtectKernelModules=true\n\
1102RestrictNamespaces=true\n\
1103LockPersonality=true\n\
1104MemoryDenyWriteExecute=true\n\
1105ReadWritePaths={workdir}\n\
1106\n\
1107[Install]\n\
1108WantedBy=multi-user.target\n",
1109        user = config.run_user,
1110        group = config.run_group,
1111        workdir = data_dir.display(),
1112        exec_start = exec_start,
1113    )
1114}
1115
1116/// Install a systemd unit + start the service.
1117///
1118/// Linux-only. The helper shells out to `systemctl`, `useradd`,
1119/// `groupadd`, `install`, `getent`, and `id` — none of which exist on
1120/// Windows or macOS. The Windows/macOS branch returns a hard error so
1121/// callers (the CLI) surface a clear message instead of a confusing
1122/// syscall failure. A proper Windows-service equivalent (sc.exe /
1123/// NSSM) is a Phase 3.6 follow-up.
1124#[cfg(target_os = "linux")]
1125pub fn install_systemd_service(config: &SystemdServiceConfig) -> Result<(), String> {
1126    ensure_root()?;
1127    ensure_command_available("systemctl")?;
1128    ensure_command_available("getent")?;
1129    ensure_command_available("groupadd")?;
1130    ensure_command_available("useradd")?;
1131    ensure_command_available("install")?;
1132    ensure_executable(&config.binary_path)?;
1133
1134    if !command_success("getent", ["group", config.run_group.as_str()])? {
1135        run_command("groupadd", ["--system", config.run_group.as_str()])?;
1136    }
1137
1138    if !command_success("id", ["-u", config.run_user.as_str()])? {
1139        let data_dir = config.data_dir();
1140        run_command(
1141            "useradd",
1142            [
1143                "--system",
1144                "--gid",
1145                config.run_group.as_str(),
1146                "--home-dir",
1147                data_dir.to_string_lossy().as_ref(),
1148                "--shell",
1149                "/usr/sbin/nologin",
1150                config.run_user.as_str(),
1151            ],
1152        )?;
1153    }
1154
1155    let data_dir = config.data_dir();
1156    run_command(
1157        "install",
1158        [
1159            "-d",
1160            "-o",
1161            config.run_user.as_str(),
1162            "-g",
1163            config.run_group.as_str(),
1164            "-m",
1165            "0750",
1166            data_dir.to_string_lossy().as_ref(),
1167        ],
1168    )?;
1169
1170    std::fs::write(config.unit_path(), render_systemd_unit(config))
1171        .map_err(|err| format!("failed to write systemd unit: {err}"))?;
1172
1173    run_command("systemctl", ["daemon-reload"])?;
1174    run_command(
1175        "systemctl",
1176        [
1177            "enable",
1178            "--now",
1179            format!("{}.service", config.service_name).as_str(),
1180        ],
1181    )?;
1182
1183    Ok(())
1184}
1185
1186/// Non-Linux fallback — systemd is Linux-specific. Keep the signature
1187/// identical so callers compile on every platform; surface a clear
1188/// error at runtime. Windows/macOS service-manager integration is a
1189/// Phase 3.6 follow-up (sc.exe + NSSM on Windows, launchd on macOS).
1190#[cfg(not(target_os = "linux"))]
1191pub fn install_systemd_service(_config: &SystemdServiceConfig) -> Result<(), String> {
1192    Err("systemd install is Linux-only — use sc.exe (Windows) or \
1193         launchd (macOS) to install the service manually using the \
1194         unit printed by `red service print-unit`"
1195        .to_string())
1196}
1197
1198#[cfg(target_os = "linux")]
1199fn ensure_root() -> Result<(), String> {
1200    let output = Command::new("id")
1201        .arg("-u")
1202        .output()
1203        .map_err(|err| format!("failed to determine current uid: {err}"))?;
1204    if !output.status.success() {
1205        return Err("failed to determine current uid".to_string());
1206    }
1207    let uid = String::from_utf8_lossy(&output.stdout);
1208    if uid.trim() != "0" {
1209        return Err("run this command as root (sudo)".to_string());
1210    }
1211    Ok(())
1212}
1213
1214#[cfg(target_os = "linux")]
1215fn ensure_command_available(command: &str) -> Result<(), String> {
1216    let status = Command::new("sh")
1217        .args(["-lc", &format!("command -v {command} >/dev/null 2>&1")])
1218        .status()
1219        .map_err(|err| format!("failed to check command '{command}': {err}"))?;
1220    if status.success() {
1221        Ok(())
1222    } else {
1223        Err(format!("required command not found: {command}"))
1224    }
1225}
1226
1227#[cfg(target_os = "linux")]
1228fn ensure_executable(path: &std::path::Path) -> Result<(), String> {
1229    let metadata = std::fs::metadata(path)
1230        .map_err(|err| format!("binary not found '{}': {err}", path.display()))?;
1231    #[cfg(unix)]
1232    {
1233        use std::os::unix::fs::PermissionsExt;
1234        if metadata.permissions().mode() & 0o111 == 0 {
1235            return Err(format!("binary is not executable: {}", path.display()));
1236        }
1237    }
1238    #[cfg(not(unix))]
1239    {
1240        if !metadata.is_file() {
1241            return Err(format!("binary is not a file: {}", path.display()));
1242        }
1243    }
1244    Ok(())
1245}
1246
1247#[cfg(target_os = "linux")]
1248fn command_success<const N: usize>(program: &str, args: [&str; N]) -> Result<bool, String> {
1249    Command::new(program)
1250        .args(args)
1251        .status()
1252        .map(|status| status.success())
1253        .map_err(|err| format!("failed to run {program}: {err}"))
1254}
1255
1256#[cfg(target_os = "linux")]
1257fn run_command<const N: usize>(program: &str, args: [&str; N]) -> Result<(), String> {
1258    let output = Command::new(program)
1259        .args(args)
1260        .output()
1261        .map_err(|err| format!("failed to run {program}: {err}"))?;
1262    if output.status.success() {
1263        return Ok(());
1264    }
1265
1266    let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
1267    let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
1268    let detail = if !stderr.is_empty() {
1269        stderr
1270    } else if !stdout.is_empty() {
1271        stdout
1272    } else {
1273        format!("exit status {}", output.status)
1274    };
1275    Err(format!("{program} failed: {detail}"))
1276}
1277
1278fn render_systemd_exec_start(config: &SystemdServiceConfig) -> String {
1279    let mut parts = vec![
1280        config.binary_path.display().to_string(),
1281        "server".to_string(),
1282        "--path".to_string(),
1283        config.data_path.display().to_string(),
1284    ];
1285
1286    if let Some(bind_addr) = &config.router_bind_addr {
1287        parts.push("--bind".to_string());
1288        parts.push(bind_addr.clone());
1289    } else if let Some(bind_addr) = &config.grpc_bind_addr {
1290        parts.push("--grpc-bind".to_string());
1291        parts.push(bind_addr.clone());
1292    }
1293    if let Some(bind_addr) = &config.http_bind_addr {
1294        parts.push("--http-bind".to_string());
1295        parts.push(bind_addr.clone());
1296    }
1297
1298    parts.join(" ")
1299}
1300
1301pub fn probe_listener(target: &str, timeout: Duration) -> bool {
1302    let addresses: Vec<SocketAddr> = match target.to_socket_addrs() {
1303        Ok(addresses) => addresses.collect(),
1304        Err(_) => return false,
1305    };
1306
1307    addresses
1308        .into_iter()
1309        .any(|address| TcpStream::connect_timeout(&address, timeout).is_ok())
1310}
1311
1312/// Bind a TCP listener for a transport at startup and record the
1313/// outcome in the shared [`TransportReadiness`] state.
1314///
1315/// The split between *explicit* and *implicit/default* binds is the
1316/// contract from issue #545:
1317///
1318/// * `explicit == true` — the operator named this transport on the
1319///   CLI / env / config. A failed bind is fatal: this returns `Err`
1320///   and the boot path must propagate the error so the process exits
1321///   non-zero with the recorded `reason`.
1322/// * `explicit == false` — this is a default listener the server
1323///   would have spun up regardless. A failed bind degrades: this
1324///   returns `Ok(None)` (no listener) but the failure is still
1325///   recorded in `readiness.failed`, so the server keeps running and
1326///   the next `/health` probe enumerates the degraded listener.
1327///
1328/// On success the bound listener lands in `readiness.active`.
1329pub fn bind_listener_for_startup(
1330    readiness: &mut TransportReadiness,
1331    transport: &str,
1332    bind_addr: &str,
1333    explicit: bool,
1334) -> Result<Option<TcpListener>, String> {
1335    match TcpListener::bind(bind_addr) {
1336        Ok(listener) => {
1337            readiness.active(transport, bind_addr, explicit);
1338            Ok(Some(listener))
1339        }
1340        Err(err) => {
1341            let reason = format!("{transport} listener bind {bind_addr}: {err}");
1342            readiness.failed(transport, bind_addr, explicit, reason.clone());
1343            if explicit {
1344                tracing::error!(
1345                    transport,
1346                    bind = %bind_addr,
1347                    error = %err,
1348                    "fatal explicit bind failure"
1349                );
1350                Err(format!("explicit {reason}"))
1351            } else {
1352                tracing::warn!(
1353                    transport,
1354                    bind = %bind_addr,
1355                    error = %err,
1356                    "non-fatal implicit bind failure; listener degraded"
1357                );
1358                Ok(None)
1359            }
1360        }
1361    }
1362}
1363
1364/// Wire SIGTERM and SIGINT to `RedDBRuntime::graceful_shutdown`.
1365///
1366/// PLAN.md Phase 1.1 — orchestrators (K8s preStop, Fly autostop, ECS
1367/// drain, systemd, plain `docker stop`) all rely on SIGTERM with a
1368/// grace window. SIGKILL after that grace window is the OS's
1369/// fallback; we are responsible for finishing in time.
1370///
1371/// Spawns a tokio task on the caller's runtime that:
1372///   1. Awaits the first of SIGTERM / SIGINT.
1373///   2. Calls `runtime.graceful_shutdown(backup_on_shutdown)`. The
1374///      runtime moves to `Stopped` on its own; this just runs the
1375///      flush + checkpoint pipeline and (optionally) a final backup.
1376///   3. Calls `std::process::exit(0)` so the orchestrator sees a
1377///      clean exit code.
1378///
1379/// `RED_BACKUP_ON_SHUTDOWN` (default `true` if a remote backend is
1380/// configured) toggles step 3's backup branch. The flush + checkpoint
1381/// always run.
1382///
1383/// Idempotent across signal storms — `graceful_shutdown` returns the
1384/// cached report on second call, but we exit on the first one
1385/// regardless, so the second SIGTERM never reaches the handler.
1386async fn spawn_lifecycle_signal_handler(runtime: RedDBRuntime) {
1387    let backup_on_shutdown = std::env::var("RED_BACKUP_ON_SHUTDOWN")
1388        .ok()
1389        .map(|v| matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on"))
1390        .unwrap_or(true);
1391
1392    #[cfg(unix)]
1393    {
1394        use tokio::signal::unix::{signal, SignalKind};
1395
1396        let mut sigterm = match signal(SignalKind::terminate()) {
1397            Ok(s) => s,
1398            Err(err) => {
1399                tracing::warn!(
1400                    error = %err,
1401                    "could not install SIGTERM handler; orchestrator graceful shutdown will fall back to SIGKILL"
1402                );
1403                return;
1404            }
1405        };
1406        let mut sigint = match signal(SignalKind::interrupt()) {
1407            Ok(s) => s,
1408            Err(err) => {
1409                tracing::warn!(error = %err, "could not install SIGINT handler");
1410                return;
1411            }
1412        };
1413        // PLAN.md Phase 6.4 — SIGHUP triggers a reload of secrets from
1414        // their `_FILE` companions without restarting the process.
1415        // Useful for credential rotation pipelines (kubectl create
1416        // secret + kubectl rollout restart, but for systemd / Nomad /
1417        // bare-metal where rolling the process is heavier).
1418        let mut sighup = match signal(SignalKind::hangup()) {
1419            Ok(s) => Some(s),
1420            Err(err) => {
1421                tracing::warn!(error = %err, "could not install SIGHUP handler; secret reload via signal disabled");
1422                None
1423            }
1424        };
1425
1426        let reload_runtime = runtime.clone();
1427        tokio::spawn(async move {
1428            loop {
1429                let signal_name = match &mut sighup {
1430                    Some(hup) => tokio::select! {
1431                        _ = sigterm.recv() => "SIGTERM",
1432                        _ = sigint.recv() => "SIGINT",
1433                        _ = hup.recv() => "SIGHUP",
1434                    },
1435                    None => tokio::select! {
1436                        _ = sigterm.recv() => "SIGTERM",
1437                        _ = sigint.recv() => "SIGINT",
1438                    },
1439                };
1440
1441                if signal_name == "SIGHUP" {
1442                    handle_sighup_reload(&reload_runtime);
1443                    continue; // stay alive; SIGHUP isn't a shutdown
1444                }
1445
1446                tracing::info!(
1447                    signal = signal_name,
1448                    "lifecycle signal received; shutting down"
1449                );
1450                match runtime.graceful_shutdown(backup_on_shutdown) {
1451                    Ok(report) => {
1452                        tracing::info!(
1453                            duration_ms = report.duration_ms,
1454                            flushed_wal = report.flushed_wal,
1455                            final_checkpoint = report.final_checkpoint,
1456                            backup_uploaded = report.backup_uploaded,
1457                            "graceful shutdown complete"
1458                        );
1459                    }
1460                    Err(err) => {
1461                        tracing::error!(error = %err, "graceful shutdown failed");
1462                        // Issue #205 — graceful shutdown returning Err
1463                        // means the runtime is exiting without a clean
1464                        // flush/checkpoint. Operator-grade event so the
1465                        // operator notices the dirty exit even when the
1466                        // process restarts before they read tracing logs.
1467                        crate::telemetry::operator_event::OperatorEvent::ShutdownForced {
1468                            reason: format!("graceful shutdown failed: {err}"),
1469                        }
1470                        .emit_global();
1471                    }
1472                }
1473                std::process::exit(0);
1474            }
1475        });
1476    }
1477
1478    #[cfg(not(unix))]
1479    {
1480        tokio::spawn(async move {
1481            let interrupted = tokio::signal::ctrl_c().await;
1482            if let Err(err) = interrupted {
1483                tracing::warn!(error = %err, "could not install Ctrl+C handler");
1484                return;
1485            }
1486
1487            tracing::info!(
1488                signal = "Ctrl+C",
1489                "lifecycle signal received; shutting down"
1490            );
1491            match runtime.graceful_shutdown(backup_on_shutdown) {
1492                Ok(report) => {
1493                    tracing::info!(
1494                        duration_ms = report.duration_ms,
1495                        flushed_wal = report.flushed_wal,
1496                        final_checkpoint = report.final_checkpoint,
1497                        backup_uploaded = report.backup_uploaded,
1498                        "graceful shutdown complete"
1499                    );
1500                }
1501                Err(err) => {
1502                    tracing::error!(error = %err, "graceful shutdown failed");
1503                }
1504            }
1505            std::process::exit(0);
1506        });
1507    }
1508}
1509
1510/// PLAN.md Phase 6.4 — re-read secrets from `*_FILE` companion env
1511/// vars. Today this only refreshes the audit log + records the
1512/// reload event; the runtime modules that hold cached secret
1513/// material (S3 backend credentials, admin token, JWT keys) read
1514/// the env on each request so the next call after SIGHUP picks up
1515/// the new file contents automatically. A future extension can
1516/// punch through to the LeaseStore / AuthStore for in-memory
1517/// caches that don't re-read on each call.
1518fn handle_sighup_reload(runtime: &RedDBRuntime) {
1519    let now_ms = std::time::SystemTime::now()
1520        .duration_since(std::time::UNIX_EPOCH)
1521        .map(|d| d.as_millis() as u64)
1522        .unwrap_or(0);
1523    tracing::info!(
1524        target: "reddb::secrets",
1525        ts_unix_ms = now_ms,
1526        "SIGHUP received; secrets will be re-read from *_FILE on next access"
1527    );
1528    // Routed through AuditFieldEscaper (ADR 0010 / issue #177) so
1529    // every emission goes through the typed-field guard. The
1530    // arguments here are static, but using the typed entry point
1531    // keeps the discipline uniform across call sites.
1532    use crate::runtime::audit_log::{AuditAuthSource, AuditEvent, AuditFieldEscaper, Outcome};
1533    runtime.audit_log().record_event(
1534        AuditEvent::builder("config/sighup_reload")
1535            .source(AuditAuthSource::System)
1536            .resource("secrets")
1537            .outcome(Outcome::Success)
1538            .field(AuditFieldEscaper::field("ts_unix_ms", now_ms))
1539            .build(),
1540    );
1541}
1542
1543/// Resolve the TLS material the PG-Wire listener shares with the native
1544/// wire transport, if any.
1545///
1546/// Policy: when the operator has enabled wire TLS — an explicit cert/key
1547/// pair or a `--wire-tls-bind` that auto-generates a self-signed dev cert
1548/// — the PG-Wire listener answers `SSLRequest` with `'S'` over the *same*
1549/// cert config (`resolve_wire_tls_config`) rather than inventing a
1550/// parallel surface. With no wire TLS configured the listener stays
1551/// plaintext and declines `SSLRequest` with `'N'`. A config error degrades
1552/// to plaintext rather than killing the listener.
1553fn resolve_pg_wire_tls(config: &ServerCommandConfig) -> Option<crate::wire::WireTlsConfig> {
1554    let tls_configured = config.wire_tls_bind_addr.is_some()
1555        || (config.wire_tls_cert.is_some() && config.wire_tls_key.is_some());
1556    if !tls_configured {
1557        return None;
1558    }
1559    match resolve_wire_tls_config(config) {
1560        Ok(cfg) => Some(cfg),
1561        Err(e) => {
1562            tracing::error!(
1563                err = %e,
1564                "pg wire TLS config error; listener will run plaintext"
1565            );
1566            None
1567        }
1568    }
1569}
1570
1571/// Resolve gRPC TLS material into PEM bytes.
1572///
1573/// Lookup order, in priority:
1574///   1. Explicit `config.grpc_tls_cert` / `config.grpc_tls_key` (paths
1575///      passed via CLI/env). Both must be present together.
1576///   2. `RED_GRPC_TLS_DEV=1` — auto-generate a self-signed cert next
1577///      to the data dir. Refuses to run without the env flag so an
1578///      operator can't accidentally ship a dev cert in prod.
1579///
1580/// `client_ca` is loaded when `config.grpc_tls_client_ca` is set,
1581/// turning the listener into a mutual-TLS endpoint that requires
1582/// every client to present a chain that anchors at one of the CAs
1583/// in the bundle.
1584fn resolve_grpc_tls_options(config: &ServerCommandConfig) -> Result<crate::GrpcTlsOptions, String> {
1585    let (cert_pem, key_pem) = match (&config.grpc_tls_cert, &config.grpc_tls_key) {
1586        (Some(cert), Some(key)) => {
1587            let cert_pem = std::fs::read(cert)
1588                .map_err(|e| format!("read grpc cert {}: {e}", cert.display()))?;
1589            let key_pem =
1590                std::fs::read(key).map_err(|e| format!("read grpc key {}: {e}", key.display()))?;
1591            (cert_pem, key_pem)
1592        }
1593        _ => {
1594            // No explicit material → only proceed when dev-mode is on.
1595            let dev = std::env::var("RED_GRPC_TLS_DEV")
1596                .ok()
1597                .map(|v| matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on"))
1598                .unwrap_or(false);
1599            if !dev {
1600                return Err("gRPC TLS configured but no cert/key supplied — set \
1601                     REDDB_GRPC_TLS_CERT / REDDB_GRPC_TLS_KEY (or \
1602                     RED_GRPC_TLS_DEV=1 to auto-generate a self-signed cert)"
1603                    .to_string());
1604            }
1605            let dir = config
1606                .path
1607                .as_ref()
1608                .and_then(|p| p.parent())
1609                .map(PathBuf::from)
1610                .unwrap_or_else(|| PathBuf::from("."));
1611            let (cert_pem_str, key_pem_str) =
1612                crate::wire::tls::generate_self_signed_cert("localhost")
1613                    .map_err(|e| format!("auto-generate dev grpc cert: {e}"))?;
1614
1615            // Constant-time-friendly fingerprint to stderr so the
1616            // operator can pin a client trust store. We log via
1617            // `tracing::warn!` so it stands out next to ordinary
1618            // listener-online events.
1619            let fp = sha256_pem_fingerprint(cert_pem_str.as_bytes());
1620            tracing::warn!(
1621                target: "reddb::security",
1622                transport = "grpc",
1623                cert_sha256 = %fp,
1624                "RED_GRPC_TLS_DEV=1: using auto-generated self-signed cert; \
1625                 DO NOT use in production"
1626            );
1627            // Persist so that restarts reuse the same identity.
1628            let cert_path = dir.join("grpc-tls-cert.pem");
1629            let key_path = dir.join("grpc-tls-key.pem");
1630            if !cert_path.exists() || !key_path.exists() {
1631                let _ = std::fs::create_dir_all(&dir);
1632                std::fs::write(&cert_path, cert_pem_str.as_bytes())
1633                    .map_err(|e| format!("write grpc dev cert: {e}"))?;
1634                std::fs::write(&key_path, key_pem_str.as_bytes())
1635                    .map_err(|e| format!("write grpc dev key: {e}"))?;
1636                #[cfg(unix)]
1637                {
1638                    use std::os::unix::fs::PermissionsExt;
1639                    let _ =
1640                        std::fs::set_permissions(&key_path, std::fs::Permissions::from_mode(0o600));
1641                }
1642            }
1643            (cert_pem_str.into_bytes(), key_pem_str.into_bytes())
1644        }
1645    };
1646
1647    let client_ca_pem = match &config.grpc_tls_client_ca {
1648        Some(path) => Some(
1649            std::fs::read(path)
1650                .map_err(|e| format!("read grpc client CA {}: {e}", path.display()))?,
1651        ),
1652        None => None,
1653    };
1654
1655    Ok(crate::GrpcTlsOptions {
1656        cert_pem,
1657        key_pem,
1658        client_ca_pem,
1659    })
1660}
1661
1662/// Hex-encoded SHA-256 of a PEM blob, used for cert-pin operator log
1663/// lines. Constant-time hash; no token contents leave this fn.
1664fn sha256_pem_fingerprint(pem: &[u8]) -> String {
1665    use sha2::{Digest, Sha256};
1666    let mut h = Sha256::new();
1667    h.update(pem);
1668    let d = h.finalize();
1669    let mut buf = String::with_capacity(64);
1670    for b in d.iter() {
1671        buf.push_str(&format!("{b:02x}"));
1672    }
1673    buf
1674}
1675
1676/// Resolve TLS config: use provided cert/key or auto-generate.
1677fn resolve_wire_tls_config(
1678    config: &ServerCommandConfig,
1679) -> Result<crate::wire::WireTlsConfig, String> {
1680    match (&config.wire_tls_cert, &config.wire_tls_key) {
1681        (Some(cert), Some(key)) => Ok(crate::wire::WireTlsConfig {
1682            cert_path: cert.clone(),
1683            key_path: key.clone(),
1684        }),
1685        _ => {
1686            // Auto-generate self-signed cert
1687            let dir = config
1688                .path
1689                .as_ref()
1690                .and_then(|p| p.parent())
1691                .map(PathBuf::from)
1692                .unwrap_or_else(|| PathBuf::from("."));
1693            crate::wire::tls::auto_generate_cert(&dir).map_err(|e| e.to_string())
1694        }
1695    }
1696}
1697
1698#[inline(never)]
1699fn build_runtime_and_auth_store(
1700    config: &ServerCommandConfig,
1701    db_options: RedDBOptions,
1702) -> Result<
1703    (
1704        RedDBRuntime,
1705        Arc<AuthStore>,
1706        Option<crate::telemetry::TelemetryGuard>,
1707    ),
1708    String,
1709> {
1710    // Return the TelemetryGuard so server runners can bind it for
1711    // their full lifetime. Dropping the guard tears down the
1712    // non-blocking log writer thread and, because that writer is
1713    // built with `.lossy(true)`, any subsequent log event routed to
1714    // the file sink is silently dropped — so callers MUST keep the
1715    // returned `Option<TelemetryGuard>` alive until shutdown.
1716    build_runtime_with_bootstrap(db_options, config.telemetry.clone(), &config.bootstrap)
1717}
1718
1719/// Open the runtime, initialise structured logging from merged CLI +
1720/// `red_config` settings, and return a guard the caller must keep
1721/// alive for the server lifetime (drop flushes pending log writes).
1722///
1723/// Merge priority: CLI flag (explicit `Some`) beats `red.logging.*`
1724/// in red_config, beats the built-in default. A CLI-flag value of
1725/// `None` / empty means "inherit from config or default" — never
1726/// "disable". The one exception is `--no-log-file` which forces
1727/// `log_dir = None` regardless of config.
1728pub(crate) fn build_runtime_with_telemetry(
1729    db_options: RedDBOptions,
1730    cli_telemetry: Option<crate::telemetry::TelemetryConfig>,
1731) -> Result<
1732    (
1733        RedDBRuntime,
1734        Arc<AuthStore>,
1735        Option<crate::telemetry::TelemetryGuard>,
1736    ),
1737    String,
1738> {
1739    let bootstrap = BootstrapConfig::default();
1740    build_runtime_with_bootstrap(db_options, cli_telemetry, &bootstrap)
1741}
1742
1743fn build_runtime_with_bootstrap(
1744    db_options: RedDBOptions,
1745    cli_telemetry: Option<crate::telemetry::TelemetryConfig>,
1746    bootstrap: &BootstrapConfig,
1747) -> Result<
1748    (
1749        RedDBRuntime,
1750        Arc<AuthStore>,
1751        Option<crate::telemetry::TelemetryGuard>,
1752    ),
1753    String,
1754> {
1755    let runtime = RedDBRuntime::with_options(db_options.clone()).map_err(|err| {
1756        // Issue #205 — runtime construction failure is the canonical
1757        // StartupFailed phase. The audit sink isn't installed yet
1758        // (it would have been registered inside `with_options`), so
1759        // the emit falls through to tracing+eprintln only — operator
1760        // still sees it on stderr.
1761        let msg = err.to_string();
1762        crate::telemetry::operator_event::OperatorEvent::StartupFailed {
1763            phase: "runtime_construction".to_string(),
1764            error: msg.clone(),
1765        }
1766        .emit_global();
1767        msg
1768    })?;
1769
1770    // PLAN.md Phase 5 / W6 — opt into serverless writer-lease fencing
1771    // when `RED_LEASE_REQUIRED=true`. Failure here aborts boot: the
1772    // operator asked for a fence; running unfenced would silently
1773    // expose split-brain risk.
1774    crate::runtime::lease_loop::start_lease_loop_if_required(&runtime).map_err(|err| {
1775        let msg = err.to_string();
1776        crate::telemetry::operator_event::OperatorEvent::StartupFailed {
1777            phase: "lease_loop".to_string(),
1778            error: msg.clone(),
1779        }
1780        .emit_global();
1781        msg
1782    })?;
1783
1784    // #213 — edge-triggered disk-space watchdog. Watches the data
1785    // directory; falls back to polling when fanotify is unavailable
1786    // (non-Linux or unprivileged container).
1787    if let Some(data_path) = db_options.data_path.as_deref() {
1788        let watch_dir = data_path.parent().unwrap_or(data_path);
1789        crate::runtime::disk_space_monitor::DiskSpaceMonitor::new(watch_dir, 90).spawn();
1790    }
1791
1792    // #214 — inotify config hot-reload watcher. Watches the config file
1793    // (REDDB_CONFIG_FILE or /etc/reddb/config.json) for changes and
1794    // applies hot-reloadable keys without restart.
1795    {
1796        let config_path = crate::runtime::config_overlay::config_file_path();
1797        let store = runtime.db().store();
1798        crate::runtime::config_watcher::ConfigWatcher::new(config_path, store).spawn();
1799    }
1800
1801    // Phase 6 logging: merge red_config overrides onto the CLI-built
1802    // telemetry config, then install the global subscriber.
1803    let merged = merge_telemetry_with_config(
1804        cli_telemetry
1805            .unwrap_or_else(|| default_telemetry_for_path(db_options.data_path.as_deref())),
1806        &runtime,
1807    );
1808    let telemetry_guard = crate::telemetry::init(merged);
1809    if db_options.bounded_disk_residency {
1810        tracing::info!(
1811            target: "reddb::storage::residency",
1812            resident_memory_bytes_max = runtime.db().store().config().resident_memory_bytes_max,
1813            "bounded disk residency enabled"
1814        );
1815    }
1816
1817    let no_auth = no_auth_active(&db_options);
1818    let auth_store =
1819        if db_options.auth.vault_enabled {
1820            let pager =
1821                runtime.db().store().pager().cloned().ok_or_else(|| {
1822                    "vault requires a paged database (persistent mode)".to_string()
1823                })?;
1824            let store = AuthStore::with_vault(db_options.auth.clone(), pager)
1825                .map_err(|err| err.to_string())?;
1826            Arc::new(store)
1827        } else {
1828            Arc::new(AuthStore::new(db_options.auth.clone()))
1829        };
1830    auth_store.configure_control_events(
1831        runtime.control_event_ledger(),
1832        runtime.control_event_config(),
1833    );
1834    // ADR 0058 — cluster bootstrap authority fail-closed seam. A
1835    // cluster-shaped boot may only run auth bootstrap when a concrete
1836    // reserved-range owner can be proven; none is implemented yet, so the
1837    // seam rejects credentialled cluster boots and allows only the explicit
1838    // `--no-auth` / `--dev` development carveout. Issue #663 — when
1839    // `--no-auth` is active, deliberately skip the preset machinery so a
1840    // stray `REDDB_USERNAME`+`REDDB_PASSWORD` pair cannot silently create an
1841    // admin, defeating the point of anonymous mode.
1842    //
1843    // Issue #1230 — the durable bootstrap completion marker
1844    // (`system.bootstrap.completed`) is the authority path's record that
1845    // first boot already produced global auth state. Observing it makes a
1846    // restart or duplicate bootstrap attempt idempotent: the seam returns
1847    // `AlreadyComplete` and the caller rehydrates read-only state only,
1848    // recreating no users, reissuing no vault certificate, and reapplying no
1849    // mutable config over operator changes.
1850    let bootstrap_already_completed = runtime
1851        .db()
1852        .store()
1853        .try_get_config(BOOTSTRAP_COMPLETED_KEY)
1854        .map_err(|error| error.to_string())?
1855        .is_some();
1856    let disposition = crate::cluster::authorize_cluster_bootstrap(
1857        db_options.storage_profile.deploy_profile,
1858        no_auth,
1859        bootstrap.auth_bootstrap_input(),
1860        bootstrap_already_completed,
1861    )?;
1862    // Issue #1231 — wire cluster vault first boot through the bootstrap
1863    // authority. The authorized disposition selects the vault plan: the owner
1864    // path (`ProceedLocal`) opens the vault against the real cluster-global
1865    // auth store (`auth_store`, backed by the runtime pager) and consumes the
1866    // env/`_FILE` secret inputs to mint the certificate; a restart after
1867    // completion (`AlreadyComplete`) reopens and unseals that same store
1868    // without consuming secret inputs. A non-owner cluster boot already failed
1869    // closed above, so no scratch or per-member vault is minted.
1870    let vault_plan = crate::cluster::plan_vault_bootstrap(disposition);
1871    match disposition {
1872        crate::cluster::BootstrapDisposition::SkipDevBypass => {
1873            debug_assert_eq!(vault_plan, crate::cluster::VaultBootstrapPlan::SkipNoVault);
1874            eprintln!("{NO_AUTH_WARNING}");
1875            tracing::warn!("{NO_AUTH_WARNING}");
1876        }
1877        crate::cluster::BootstrapDisposition::AlreadyComplete => {
1878            // First boot is over. Rehydrate the read-only manifest registry
1879            // so config-derived state is observable, but create no users,
1880            // reissue no vault material, and reapply no mutable config
1881            // (issue #1230). `apply_preset_from_config` does exactly this
1882            // when the completion marker is present — matching the unseal-only
1883            // vault plan (no secret inputs consumed).
1884            debug_assert_eq!(
1885                vault_plan,
1886                crate::cluster::VaultBootstrapPlan::OpenClusterGlobalStore {
1887                    consume_secret_inputs: false,
1888                }
1889            );
1890            apply_preset_from_config(&runtime, &auth_store, bootstrap)?;
1891            tracing::info!("bootstrap already completed, unsealing existing cluster auth store");
1892        }
1893        crate::cluster::BootstrapDisposition::ProceedLocal => {
1894            // Owner first boot: open the vault against the real cluster-global
1895            // auth store and consume the secret inputs to mint the certificate.
1896            debug_assert_eq!(
1897                vault_plan,
1898                crate::cluster::VaultBootstrapPlan::OpenClusterGlobalStore {
1899                    consume_secret_inputs: true,
1900                }
1901            );
1902            apply_preset_from_config(&runtime, &auth_store, bootstrap)?;
1903            maybe_apply_policy_break_glass(&auth_store);
1904        }
1905    }
1906
1907    // Background session purge (every 5 minutes)
1908    {
1909        let store = Arc::clone(&auth_store);
1910        std::thread::Builder::new()
1911            .name("reddb-session-purge".into())
1912            .spawn(move || loop {
1913                std::thread::sleep(std::time::Duration::from_secs(300));
1914                store.purge_expired_sessions();
1915            })
1916            .ok();
1917    }
1918
1919    Ok((runtime, auth_store, telemetry_guard))
1920}
1921
1922/// Honour `REDDB_POLICY_BREAK_GLASS=1` at boot — see issue #713 and
1923/// the [`crate::auth::self_lock_guard`] module. Anything other than
1924/// `1`/`true`/`yes` (case-insensitive) is treated as not set.
1925fn maybe_apply_policy_break_glass(auth_store: &Arc<AuthStore>) {
1926    use crate::auth::self_lock_guard::BREAK_GLASS_ENV;
1927
1928    let enabled = std::env::var(BREAK_GLASS_ENV)
1929        .ok()
1930        .map(|v| {
1931            let trimmed = v.trim().to_ascii_lowercase();
1932            matches!(trimmed.as_str(), "1" | "true" | "yes")
1933        })
1934        .unwrap_or(false);
1935    if !enabled {
1936        return;
1937    }
1938    let now = crate::utils::now_unix_millis() as u128;
1939    match auth_store.apply_policy_break_glass(now) {
1940        Ok(()) => {
1941            tracing::warn!(env = BREAK_GLASS_ENV, "policy break-glass recovery applied");
1942        }
1943        Err(err) => {
1944            tracing::error!(env = BREAK_GLASS_ENV, %err, "policy break-glass recovery failed");
1945        }
1946    }
1947}
1948
1949/// Reserved config keys describing first-boot bootstrap state (issue #650).
1950/// Presence of [`BOOTSTRAP_COMPLETED_KEY`] is the idempotency hinge: when
1951/// it is set, [`apply_preset`] silently no-ops on subsequent boots so a
1952/// container restart with the same env is a no-op.
1953pub(crate) const BOOTSTRAP_COMPLETED_KEY: &str = "system.bootstrap.completed";
1954pub(crate) const BOOTSTRAP_PRESET_KEY: &str = "system.bootstrap.preset";
1955pub(crate) const BOOTSTRAP_FIRST_ADMIN_KEY: &str = "system.bootstrap.first_admin_id";
1956
1957/// Env vars selecting the bootstrap preset. `REDDB_BOOTSTRAP_PRESET`
1958/// is canonical; `REDDB_PRESET` remains accepted for compatibility.
1959pub(crate) const BOOTSTRAP_PRESET_ENV: &str = "REDDB_BOOTSTRAP_PRESET";
1960pub(crate) const PRESET_ENV: &str = "REDDB_PRESET";
1961/// Env equivalent of `--bootstrap-cert-out` (issue #1589). Honours the
1962/// `_FILE` companion via [`env_nonempty`].
1963pub(crate) const BOOTSTRAP_CERT_OUT_ENV: &str = "REDDB_BOOTSTRAP_CERT_OUT";
1964pub(crate) const PRESET_SIMPLE: &str = "simple";
1965pub(crate) const PRESET_PRODUCTION: &str = "production";
1966pub(crate) const PRESET_REGULATED: &str = "regulated";
1967pub(crate) const PRESET_CLOUD: &str = "cloud";
1968
1969/// Policy id installed by the `production` preset and attached to the
1970/// first admin. Grants `"*"` on `"*"` so the admin has policy-derived
1971/// broad authority (acceptance #3) — not an authorization bypass.
1972pub(crate) const FIRST_ADMIN_ALLOW_ALL_POLICY: &str = "system.bootstrap.first-admin-allow-all";
1973pub(crate) const REGULATED_PROTECT_MANAGED_POLICY: &str = "system.regulated.protect-managed";
1974pub(crate) const CLOUD_PROTECT_MANAGED_POLICY: &str = "system.cloud.protect-managed";
1975pub(crate) const CLOUD_CONFIG_NAMESPACE: &str = "red.config.cloud";
1976pub(crate) const REGULATED_AUDIT_CONFIG_NAMESPACE: &str = "red.config.audit";
1977pub(crate) const REGULATED_EVIDENCE_CONFIG_NAMESPACE: &str = "red.config.evidence";
1978pub(crate) const REGULATED_QUERY_AUDIT_CONFIG_NAMESPACE: &str = "red.config.query_audit";
1979
1980/// Apply the bootstrap preset selected by `REDDB_BOOTSTRAP_PRESET` (or
1981/// legacy `REDDB_PRESET`; default `simple`). Idempotent — if
1982/// `system.bootstrap.completed` is already set, this is a one-line
1983/// `tracing::info!` no-op and the server proceeds (issue #650
1984/// acceptance #5).
1985///
1986/// Caller must have already short-circuited the `--no-auth` / `--dev`
1987/// path (issue #663): when that flag is set, the preset must be skipped
1988/// entirely — no admin created, no bootstrap state written.
1989pub(crate) fn apply_preset(
1990    runtime: &RedDBRuntime,
1991    auth_store: &Arc<AuthStore>,
1992) -> Result<(), String> {
1993    let bootstrap = BootstrapConfig::default();
1994    apply_preset_from_config(runtime, auth_store, &bootstrap)
1995}
1996
1997fn apply_preset_from_config(
1998    runtime: &RedDBRuntime,
1999    auth_store: &Arc<AuthStore>,
2000    bootstrap: &BootstrapConfig,
2001) -> Result<(), String> {
2002    let store = runtime.db().store();
2003
2004    if store
2005        .try_get_config(BOOTSTRAP_COMPLETED_KEY)
2006        .map_err(|error| error.to_string())?
2007        .is_some()
2008    {
2009        crate::cli::bootstrap_manifest::rehydrate_manifest_registry(
2010            runtime,
2011            &runtime.config_registry(),
2012        )?;
2013        tracing::info!("bootstrap state present, skipping preset application");
2014        return Ok(());
2015    }
2016
2017    let preset = bootstrap.resolved_preset();
2018
2019    if let Some(path) = bootstrap.selected_manifest() {
2020        if let Some(manifest_bootstrap) =
2021            crate::cli::bootstrap_manifest::bootstrap_config_from_manifest_file(&path)?
2022        {
2023            apply_preset_from_config(runtime, auth_store, &manifest_bootstrap)?;
2024            return Ok(());
2025        }
2026        let first_admin_id = crate::cli::bootstrap_manifest::apply_manifest_file(
2027            runtime,
2028            auth_store,
2029            &runtime.config_registry(),
2030            &path,
2031        )?;
2032        persist_bootstrap_state(runtime, "manifest", Some(&first_admin_id))?;
2033        tracing::info!("bootstrap manifest applied");
2034        return Ok(());
2035    }
2036
2037    let (first_admin_id, certificate) = match preset.as_str() {
2038        PRESET_SIMPLE => {
2039            // `simple` is the default low-friction preset. Auth knobs
2040            // remain whatever the CLI/env set; we only persist the
2041            // bootstrap state so subsequent boots are idempotent.
2042            (None, None)
2043        }
2044        PRESET_PRODUCTION => {
2045            let (id, cert) = apply_production_preset_from_config(auth_store, bootstrap)?;
2046            (Some(id), cert)
2047        }
2048        PRESET_CLOUD => {
2049            let (id, cert) = apply_cloud_preset(runtime, auth_store, bootstrap)?;
2050            (Some(id), cert)
2051        }
2052        PRESET_REGULATED => {
2053            apply_regulated_preset(runtime, auth_store)?;
2054            (None, None)
2055        }
2056        other => {
2057            return Err(format!(
2058                "bootstrap preset {other:?} is not recognised (expected `simple`, `production`, `regulated`, or `cloud`)"
2059            ));
2060        }
2061    };
2062
2063    // Issue #1589 — write the freshly minted unseal certificate to the
2064    // caller-specified file so a distroless single-machine init can read
2065    // it back for the next boot's `REDDB_CERTIFICATE_FILE` unseal. This
2066    // path runs only on the bootstrap-creating boot (the completion
2067    // marker short-circuits above before any cert is minted), so the file
2068    // is written exactly once and a re-boot never churns it.
2069    if let Some(cert) = certificate.as_deref() {
2070        write_bootstrap_certificate(bootstrap, cert)?;
2071    }
2072
2073    persist_bootstrap_state(runtime, &preset, first_admin_id.as_deref())?;
2074    tracing::info!(preset = %preset, "bootstrap preset applied");
2075    Ok(())
2076}
2077
2078/// Write the unseal certificate to `--bootstrap-cert-out` when that
2079/// target is set (issue #1589). No-op when the flag/env is absent, so
2080/// the certificate is still emitted on stderr by the preset as before.
2081fn write_bootstrap_certificate(
2082    bootstrap: &BootstrapConfig,
2083    certificate_hex: &str,
2084) -> Result<(), String> {
2085    let Some(path) = bootstrap.selected_cert_out() else {
2086        return Ok(());
2087    };
2088    std::fs::write(&path, certificate_hex)
2089        .map_err(|err| format!("write --bootstrap-cert-out {}: {err}", path.display()))?;
2090    tracing::info!(
2091        target: "reddb::bootstrap",
2092        path = %path.display(),
2093        "first-boot self-bootstrap: wrote unseal certificate to --bootstrap-cert-out (consume via REDDB_CERTIFICATE_FILE on the next boot)"
2094    );
2095    Ok(())
2096}
2097
2098fn apply_production_preset_from_config(
2099    auth_store: &Arc<AuthStore>,
2100    bootstrap: &BootstrapConfig,
2101) -> Result<(String, Option<String>), String> {
2102    use crate::auth::store::PrincipalRef;
2103    use crate::auth::UserId;
2104
2105    let username = bootstrap.production_username().ok_or_else(|| {
2106        "production preset requires --bootstrap-admin or REDDB_USERNAME (or REDDB_USERNAME_FILE)"
2107            .to_string()
2108    })?;
2109    let password = bootstrap.production_password().ok_or_else(|| {
2110        "production preset requires --bootstrap-admin-password or REDDB_PASSWORD (or REDDB_PASSWORD_FILE)"
2111            .to_string()
2112    })?;
2113
2114    // (1) Create the first admin as an ordinary platform-scoped user.
2115    // Managed guardrails are policy-derived; bootstrap does not stamp a
2116    // parallel ownership flag.
2117    let result = auth_store
2118        .bootstrap(&username, &password)
2119        .map_err(|err| format!("bootstrap first admin: {err}"))?;
2120    let certificate = result.certificate.clone();
2121    if let Some(cert) = certificate.as_deref() {
2122        eprintln!("[reddb] CERTIFICATE: {}", cert);
2123        tracing::warn!(
2124            "vault certificate issued by REDDB_PRESET=production -- save it and update the runtime secret before restart"
2125        );
2126    }
2127    let first_admin = UserId::platform(result.user.username.clone());
2128
2129    // (2) Install the allow-all policy as an ordinary policy row.
2130    install_allow_all_policy(auth_store)?;
2131
2132    // (3) Attach the policy to the first admin.
2133    auth_store
2134        .attach_policy(
2135            PrincipalRef::User(first_admin.clone()),
2136            FIRST_ADMIN_ALLOW_ALL_POLICY,
2137        )
2138        .map_err(|err| format!("attach allow-all policy: {err}"))?;
2139
2140    Ok((first_admin.to_string(), certificate))
2141}
2142
2143fn apply_cloud_preset(
2144    runtime: &RedDBRuntime,
2145    auth_store: &Arc<AuthStore>,
2146    bootstrap: &BootstrapConfig,
2147) -> Result<(String, Option<String>), String> {
2148    use crate::auth::store::PrincipalRef;
2149    use crate::auth::{Role, UserId};
2150
2151    let head_username = bootstrap.cloud_head_username().ok_or_else(|| {
2152        "cloud preset requires --cloud-head-admin or REDDB_CLOUD_HEAD_ADMIN (or REDDB_USERNAME)"
2153            .to_string()
2154    })?;
2155    let head_password = bootstrap.cloud_head_password().ok_or_else(|| {
2156        "cloud preset requires --cloud-head-admin-password or REDDB_CLOUD_HEAD_ADMIN_PASSWORD (or REDDB_PASSWORD)"
2157            .to_string()
2158    })?;
2159    let customer_username = bootstrap.customer_username().ok_or_else(|| {
2160        "cloud preset requires --customer-admin or REDDB_CUSTOMER_ADMIN".to_string()
2161    })?;
2162    let customer_password = bootstrap.customer_password().ok_or_else(|| {
2163        "cloud preset requires --customer-admin-password or REDDB_CUSTOMER_ADMIN_PASSWORD"
2164            .to_string()
2165    })?;
2166    if head_username == customer_username {
2167        return Err("cloud preset requires distinct head and customer admin usernames".to_string());
2168    }
2169
2170    let head = auth_store
2171        .bootstrap_system_admin(&head_username, &head_password)
2172        .map_err(|err| format!("bootstrap cloud head admin: {err}"))?;
2173    let certificate = head.certificate.clone();
2174    let head_id = UserId::platform(head.user.username.clone());
2175
2176    auth_store
2177        .create_user(&customer_username, &customer_password, Role::Admin)
2178        .map_err(|err| format!("create cloud customer admin: {err}"))?;
2179    let customer_id = UserId::platform(customer_username.clone());
2180
2181    install_allow_all_policy(auth_store)?;
2182    for user_id in [&head_id, &customer_id] {
2183        auth_store
2184            .attach_policy(
2185                PrincipalRef::User(user_id.clone()),
2186                FIRST_ADMIN_ALLOW_ALL_POLICY,
2187            )
2188            .map_err(|err| format!("attach allow-all policy: {err}"))?;
2189    }
2190    install_cloud_guardrails(runtime, auth_store, &head_id)?;
2191    auth_store
2192        .attach_policy(
2193            PrincipalRef::User(customer_id),
2194            CLOUD_PROTECT_MANAGED_POLICY,
2195        )
2196        .map_err(|err| format!("attach cloud guardrail policy: {err}"))?;
2197
2198    Ok((head_id.to_string(), certificate))
2199}
2200
2201pub(crate) fn install_allow_all_policy(auth_store: &AuthStore) -> Result<(), String> {
2202    use crate::auth::policies::Policy;
2203
2204    let policy = Policy::from_json_str(&format!(
2205        r#"{{
2206            "id": "{id}",
2207            "version": 1,
2208            "statements": [{{
2209                "effect": "allow",
2210                "actions": ["*"],
2211                "resources": ["*"]
2212            }}]
2213        }}"#,
2214        id = FIRST_ADMIN_ALLOW_ALL_POLICY
2215    ))
2216    .map_err(|err| format!("compile allow-all policy: {err}"))?;
2217    auth_store
2218        .put_policy(policy)
2219        .map_err(|err| format!("install allow-all policy: {err}"))
2220}
2221
2222fn install_cloud_guardrails(
2223    runtime: &RedDBRuntime,
2224    auth_store: &Arc<AuthStore>,
2225    head_admin: &crate::auth::UserId,
2226) -> Result<(), String> {
2227    use crate::auth::policies::Policy;
2228    use crate::auth::registry::EvidenceRequirement;
2229
2230    let policy = Policy::from_json_str(&format!(
2231        r#"{{
2232            "id": "{id}",
2233            "version": 1,
2234            "statements": [
2235                {{
2236                    "effect": "deny",
2237                    "actions": ["policy:put", "policy:drop", "policy:attach", "policy:detach"],
2238                    "resources": ["policy:{id}"]
2239                }},
2240                {{
2241                    "effect": "deny",
2242                    "actions": [
2243                        "user:delete",
2244                        "user:disable",
2245                        "user:password:change",
2246                        "user:role:update"
2247                    ],
2248                    "resources": ["user:{head_admin}"]
2249                }},
2250                {{
2251                    "effect": "deny",
2252                    "actions": ["config:write"],
2253                    "resources": ["config:{cloud}.*"]
2254                }}
2255            ]
2256        }}"#,
2257        id = CLOUD_PROTECT_MANAGED_POLICY,
2258        head_admin = head_admin,
2259        cloud = CLOUD_CONFIG_NAMESPACE,
2260    ))
2261    .map_err(|err| format!("compile cloud guardrail policy: {err}"))?;
2262    auth_store
2263        .put_policy(policy)
2264        .map_err(|err| format!("install cloud guardrail policy: {err}"))?;
2265
2266    let now_ms = crate::utils::now_unix_millis() as u128;
2267    let entries = vec![
2268        regulated_registry_entry(
2269            CLOUD_PROTECT_MANAGED_POLICY,
2270            crate::auth::managed_policy::RESOURCE_TYPE_POLICY,
2271            "policy",
2272            "policy:*",
2273            &format!("policy:{CLOUD_PROTECT_MANAGED_POLICY}"),
2274            EvidenceRequirement::Metadata,
2275            now_ms,
2276        ),
2277        regulated_registry_entry(
2278            CLOUD_CONFIG_NAMESPACE,
2279            crate::auth::managed_config::RESOURCE_TYPE_CONFIG_NAMESPACE,
2280            "config_namespace",
2281            "config:write",
2282            &format!("config:{CLOUD_CONFIG_NAMESPACE}.*"),
2283            EvidenceRequirement::Metadata,
2284            now_ms,
2285        ),
2286    ];
2287    for entry in entries.iter().cloned() {
2288        runtime
2289            .config_registry()
2290            .restore_bootstrap_entry(entry)
2291            .map_err(|err| format!("install cloud registry entry: {err}"))?;
2292    }
2293    crate::cli::bootstrap_manifest::persist_registry_state(runtime, &entries)?;
2294    Ok(())
2295}
2296
2297fn apply_regulated_preset(
2298    runtime: &RedDBRuntime,
2299    auth_store: &Arc<AuthStore>,
2300) -> Result<(), String> {
2301    use crate::auth::policies::Policy;
2302    use crate::auth::registry::EvidenceRequirement;
2303    use crate::auth::store::{PrincipalRef, PUBLIC_IAM_GROUP};
2304
2305    runtime.query_audit().enable_infrastructure();
2306
2307    let policy = Policy::from_json_str(&format!(
2308        r#"{{
2309            "id": "{id}",
2310            "version": 1,
2311            "statements": [
2312                {{
2313                    "effect": "deny",
2314                    "actions": ["policy:put", "policy:drop", "policy:attach", "policy:detach"],
2315                    "resources": ["policy:{id}"]
2316                }},
2317                {{
2318                    "effect": "deny",
2319                    "actions": ["config:write"],
2320                    "resources": [
2321                        "config:{audit}.*",
2322                        "config:{evidence}.*",
2323                        "config:{query_audit}.*"
2324                    ]
2325                }}
2326            ]
2327        }}"#,
2328        id = REGULATED_PROTECT_MANAGED_POLICY,
2329        audit = REGULATED_AUDIT_CONFIG_NAMESPACE,
2330        evidence = REGULATED_EVIDENCE_CONFIG_NAMESPACE,
2331        query_audit = REGULATED_QUERY_AUDIT_CONFIG_NAMESPACE,
2332    ))
2333    .map_err(|err| format!("compile regulated guardrail policy: {err}"))?;
2334    auth_store
2335        .put_policy(policy)
2336        .map_err(|err| format!("install regulated guardrail policy: {err}"))?;
2337    auth_store
2338        .attach_policy(
2339            PrincipalRef::Group(PUBLIC_IAM_GROUP.to_string()),
2340            REGULATED_PROTECT_MANAGED_POLICY,
2341        )
2342        .map_err(|err| format!("attach regulated guardrail policy: {err}"))?;
2343
2344    let now_ms = crate::utils::now_unix_millis() as u128;
2345    let entries = vec![
2346        regulated_registry_entry(
2347            REGULATED_PROTECT_MANAGED_POLICY,
2348            crate::auth::managed_policy::RESOURCE_TYPE_POLICY,
2349            "iam_policy",
2350            "policy:*",
2351            &format!("policy:{REGULATED_PROTECT_MANAGED_POLICY}"),
2352            EvidenceRequirement::Metadata,
2353            now_ms,
2354        ),
2355        regulated_registry_entry(
2356            REGULATED_AUDIT_CONFIG_NAMESPACE,
2357            crate::auth::managed_config::RESOURCE_TYPE_CONFIG_NAMESPACE,
2358            "config_namespace",
2359            "config:write",
2360            &format!("config:{REGULATED_AUDIT_CONFIG_NAMESPACE}.*"),
2361            EvidenceRequirement::Metadata,
2362            now_ms,
2363        ),
2364        regulated_registry_entry(
2365            REGULATED_EVIDENCE_CONFIG_NAMESPACE,
2366            crate::auth::managed_config::RESOURCE_TYPE_CONFIG_NAMESPACE,
2367            "config_namespace",
2368            "config:write",
2369            &format!("config:{REGULATED_EVIDENCE_CONFIG_NAMESPACE}.*"),
2370            EvidenceRequirement::Metadata,
2371            now_ms,
2372        ),
2373        regulated_registry_entry(
2374            REGULATED_QUERY_AUDIT_CONFIG_NAMESPACE,
2375            crate::auth::managed_config::RESOURCE_TYPE_CONFIG_NAMESPACE,
2376            "config_namespace",
2377            "config:write",
2378            &format!("config:{REGULATED_QUERY_AUDIT_CONFIG_NAMESPACE}.*"),
2379            EvidenceRequirement::Metadata,
2380            now_ms,
2381        ),
2382    ];
2383
2384    for entry in entries.iter().cloned() {
2385        runtime
2386            .config_registry()
2387            .restore_bootstrap_entry(entry)
2388            .map_err(|err| format!("install regulated registry entry: {err}"))?;
2389    }
2390    crate::cli::bootstrap_manifest::persist_registry_state(runtime, &entries)?;
2391    Ok(())
2392}
2393
2394fn regulated_registry_entry(
2395    id: &str,
2396    resource_type: &str,
2397    schema: &str,
2398    required_action: &str,
2399    required_resource: &str,
2400    evidence_requirement: crate::auth::registry::EvidenceRequirement,
2401    updated_at_ms: u128,
2402) -> crate::auth::registry::ConfigRegistryEntry {
2403    crate::auth::registry::ConfigRegistryEntry {
2404        id: id.to_string(),
2405        version: 1,
2406        resource_type: resource_type.to_string(),
2407        schema: schema.to_string(),
2408        mutability: crate::auth::registry::Mutability::Immutable,
2409        sensitivity: crate::auth::registry::Sensitivity::Internal,
2410        managed: true,
2411        required_action: required_action.to_string(),
2412        required_resource: required_resource.to_string(),
2413        evidence_requirement,
2414        updated_by: "system:regulated-preset".to_string(),
2415        updated_at_ms,
2416    }
2417}
2418
2419fn persist_bootstrap_state(
2420    runtime: &RedDBRuntime,
2421    preset: &str,
2422    first_admin_id: Option<&str>,
2423) -> Result<(), String> {
2424    let store = runtime.db().store();
2425    let mut tree = crate::serde_json::Map::new();
2426    tree.insert(
2427        BOOTSTRAP_COMPLETED_KEY.to_string(),
2428        crate::serde_json::Value::Bool(true),
2429    );
2430    tree.insert(
2431        BOOTSTRAP_PRESET_KEY.to_string(),
2432        crate::serde_json::Value::String(preset.to_string()),
2433    );
2434    if let Some(id) = first_admin_id {
2435        tree.insert(
2436            BOOTSTRAP_FIRST_ADMIN_KEY.to_string(),
2437            crate::serde_json::Value::String(id.to_string()),
2438        );
2439    }
2440    let json = crate::serde_json::Value::Object(tree);
2441    store
2442        .try_set_config_tree("", &json)
2443        .map_err(|error| error.to_string())?;
2444    Ok(())
2445}
2446
2447/// Read `red.logging.*` keys from the persistent config store and
2448/// merge them into the CLI-built `TelemetryConfig`. Merge priority:
2449/// explicit CLI flag > red_config > built-in default.
2450///
2451/// The "was a flag passed" signal comes from the `*_explicit` bools
2452/// on `TelemetryConfig`, populated by the CLI parser. This replaces
2453/// an earlier equality-to-default heuristic that silently dropped
2454/// config whenever the CLI-derived default diverged from
2455/// `TelemetryConfig::default()` (e.g. path-derived `log_dir`,
2456/// non-TTY `format`) and that silently overrode `--no-log-file`.
2457fn merge_telemetry_with_config(
2458    mut cli: crate::telemetry::TelemetryConfig,
2459    runtime: &RedDBRuntime,
2460) -> crate::telemetry::TelemetryConfig {
2461    use reddb_types::Value;
2462
2463    let store = runtime.db().store();
2464
2465    if !cli.level_explicit {
2466        if let Some(Value::Text(v)) = store.get_config("red.logging.level") {
2467            cli.level_filter = v.to_string();
2468        }
2469    }
2470    if !cli.format_explicit {
2471        if let Some(Value::Text(v)) = store.get_config("red.logging.format") {
2472            if let Some(parsed) = crate::telemetry::LogFormat::parse(&v) {
2473                cli.format = parsed;
2474            }
2475        }
2476    }
2477    if !cli.rotation_keep_days_explicit {
2478        match store.get_config("red.logging.keep_days") {
2479            Some(Value::Integer(n)) if n >= 0 && n <= u16::MAX as i64 => {
2480                cli.rotation_keep_days = n as u16
2481            }
2482            Some(Value::UnsignedInteger(n)) if n <= u16::MAX as u64 => {
2483                cli.rotation_keep_days = n as u16
2484            }
2485            Some(Value::Text(v)) => {
2486                if let Ok(n) = v.parse::<u16>() {
2487                    cli.rotation_keep_days = n;
2488                }
2489            }
2490            _ => {}
2491        }
2492    }
2493    if !cli.file_prefix_explicit {
2494        if let Some(Value::Text(v)) = store.get_config("red.logging.file_prefix") {
2495            if !v.is_empty() {
2496                cli.file_prefix = v.to_string();
2497            }
2498        }
2499    }
2500    // --no-log-file is a kill-switch: config cannot resurrect the
2501    // file sink. Explicit --log-dir also wins.
2502    if !cli.log_dir_explicit && !cli.log_file_disabled {
2503        if let Some(Value::Text(v)) = store.get_config("red.logging.dir") {
2504            if !v.is_empty() {
2505                cli.log_dir = Some(std::path::PathBuf::from(v.as_ref()));
2506            }
2507        }
2508    }
2509
2510    cli
2511}
2512
2513#[cfg(test)]
2514mod telemetry_merge_tests {
2515    use super::*;
2516    use crate::telemetry::{LogFormat, TelemetryConfig};
2517
2518    fn fresh_runtime() -> RedDBRuntime {
2519        RedDBRuntime::with_options(RedDBOptions::in_memory()).expect("runtime")
2520    }
2521
2522    fn set_str(runtime: &RedDBRuntime, key: &str, value: &str) {
2523        runtime
2524            .db()
2525            .store()
2526            .set_config_tree(key, &crate::serde_json::Value::String(value.to_string()));
2527    }
2528
2529    fn cli_base() -> TelemetryConfig {
2530        // Emulate default_telemetry_for_path(Some(path)) on a non-TTY host:
2531        // log_dir = Some(...), format = Json. Nothing marked explicit.
2532        TelemetryConfig {
2533            log_dir: Some(std::path::PathBuf::from("/tmp/reddb-default/logs")),
2534            format: LogFormat::Json,
2535            ..Default::default()
2536        }
2537    }
2538
2539    #[test]
2540    fn config_log_dir_promoted_when_flag_absent() {
2541        let runtime = fresh_runtime();
2542        set_str(&runtime, "red.logging.dir", "/var/log/reddb");
2543        let merged = merge_telemetry_with_config(cli_base(), &runtime);
2544        assert_eq!(
2545            merged.log_dir.as_deref(),
2546            Some(std::path::Path::new("/var/log/reddb"))
2547        );
2548    }
2549
2550    #[test]
2551    fn explicit_log_dir_wins_over_config() {
2552        let runtime = fresh_runtime();
2553        set_str(&runtime, "red.logging.dir", "/var/log/reddb");
2554        let mut cli = cli_base();
2555        cli.log_dir = Some(std::path::PathBuf::from("/custom/dir"));
2556        cli.log_dir_explicit = true;
2557        let merged = merge_telemetry_with_config(cli, &runtime);
2558        assert_eq!(
2559            merged.log_dir.as_deref(),
2560            Some(std::path::Path::new("/custom/dir"))
2561        );
2562    }
2563
2564    #[test]
2565    fn no_log_file_beats_config_log_dir() {
2566        let runtime = fresh_runtime();
2567        set_str(&runtime, "red.logging.dir", "/var/log/reddb");
2568        let mut cli = cli_base();
2569        cli.log_dir = None;
2570        cli.log_file_disabled = true;
2571        let merged = merge_telemetry_with_config(cli, &runtime);
2572        assert!(
2573            merged.log_dir.is_none(),
2574            "--no-log-file must veto config dir"
2575        );
2576    }
2577
2578    #[test]
2579    fn config_format_promoted_on_non_tty_default() {
2580        // On non-TTY, default_telemetry_for_path yields format=Json even
2581        // though TelemetryConfig::default() is Pretty. The old equality
2582        // check silently dropped config here.
2583        let runtime = fresh_runtime();
2584        set_str(&runtime, "red.logging.format", "pretty");
2585        let merged = merge_telemetry_with_config(cli_base(), &runtime);
2586        assert_eq!(merged.format, LogFormat::Pretty);
2587    }
2588
2589    #[test]
2590    fn explicit_format_wins_over_config() {
2591        let runtime = fresh_runtime();
2592        set_str(&runtime, "red.logging.format", "pretty");
2593        let mut cli = cli_base();
2594        cli.format = LogFormat::Json;
2595        cli.format_explicit = true;
2596        let merged = merge_telemetry_with_config(cli, &runtime);
2597        assert_eq!(merged.format, LogFormat::Json);
2598    }
2599}
2600
2601#[inline(never)]
2602fn build_http_server(
2603    runtime: RedDBRuntime,
2604    auth_store: Arc<AuthStore>,
2605    bind_addr: String,
2606) -> RedDBServer {
2607    build_http_server_with_transport_readiness(
2608        runtime,
2609        auth_store,
2610        bind_addr,
2611        TransportReadiness::default(),
2612    )
2613}
2614
2615/// Apply the resolved HTTP limits to a freshly-built `RedDBServer`.
2616///
2617/// Centralised here so every `run_*` path goes through the same
2618/// resolver and the structured startup log line carries the same
2619/// `http_limits.*` fields regardless of transport combination.
2620fn apply_http_limits(
2621    server: RedDBServer,
2622    config: &ServerCommandConfig,
2623    runtime: &RedDBRuntime,
2624) -> RedDBServer {
2625    let store = runtime.db().store();
2626    let resolved =
2627        crate::server::http_limits::resolve_http_limits(&config.http_limits_cli, |key| match store
2628            .get_config(key)
2629        {
2630            Some(reddb_types::Value::Text(v)) => Some(v.to_string()),
2631            Some(reddb_types::Value::Integer(n)) if n >= 0 => Some(n.to_string()),
2632            Some(reddb_types::Value::UnsignedInteger(n)) => Some(n.to_string()),
2633            _ => None,
2634        });
2635    tracing::info!(
2636        target: "reddb::http_limits",
2637        max_handlers = resolved.max_handlers,
2638        handler_timeout_ms = resolved.handler_timeout_ms,
2639        retry_after_secs = resolved.retry_after_secs,
2640        max_inflight_per_principal = resolved.max_inflight_per_principal,
2641        "http_limits resolved"
2642    );
2643    server.with_http_limits(resolved)
2644}
2645
2646/// `red server --ui` (issue #1047, ADR 0051): attach the resolved red-ui
2647/// bundle directory to an HTTP server so it also serves the bundle as
2648/// static assets. No-op when `--ui` is off.
2649///
2650/// An explicit `--ui-dir <DIR>` is served as-is (no download); otherwise
2651/// the pinned bundle is resolved from the local cache, downloading on
2652/// first use (ADR 0050). A resolution failure is fatal: the operator
2653/// explicitly asked for the UI, so silently serving API-only would be
2654/// surprising and would mask an offline/checksum problem.
2655fn apply_ui_bundle(
2656    server: RedDBServer,
2657    config: &ServerCommandConfig,
2658) -> Result<RedDBServer, String> {
2659    if !config.ui {
2660        return Ok(server);
2661    }
2662    let ui_dir = match &config.ui_dir {
2663        Some(dir) => dir.clone(),
2664        None => {
2665            let cache_root = crate::server::ui_bundle_resolver::reddb_user_cache_root()
2666                .unwrap_or_else(|_| std::env::temp_dir().join("reddb"));
2667            crate::server::ui_bundle_resolver::resolve_ui_bundle(
2668                &cache_root,
2669                &crate::server::ui_bundle_resolver::HttpFetcher,
2670            )
2671            .map_err(|err| format!("resolve red-ui bundle for --ui: {err}"))?
2672        }
2673    };
2674    tracing::info!(target: "reddb::ui", dir = %ui_dir.display(), "serving red-ui bundle on HTTP surface");
2675    Ok(server.with_ui_dir(ui_dir))
2676}
2677
2678#[inline(never)]
2679fn build_http_server_with_transport_readiness(
2680    runtime: RedDBRuntime,
2681    auth_store: Arc<AuthStore>,
2682    bind_addr: String,
2683    transport_readiness: TransportReadiness,
2684) -> RedDBServer {
2685    RedDBServer::with_options(
2686        runtime,
2687        ServerOptions {
2688            bind_addr,
2689            transport_readiness,
2690            ..ServerOptions::default()
2691        },
2692    )
2693    .with_auth(auth_store)
2694}
2695
2696/// PLAN.md Phase 6.2 — build a listener that only serves
2697/// `/admin/*` + `/metrics` + `/health/*`. Defaults to `127.0.0.1`
2698/// when the env var has no host (loopback-only by default per spec).
2699#[inline(never)]
2700fn build_admin_only_server(
2701    runtime: RedDBRuntime,
2702    auth_store: Arc<AuthStore>,
2703    bind_addr: String,
2704) -> RedDBServer {
2705    RedDBServer::with_options(
2706        runtime,
2707        ServerOptions {
2708            bind_addr,
2709            surface: crate::server::ServerSurface::AdminOnly,
2710            ..ServerOptions::default()
2711        },
2712    )
2713    .with_auth(auth_store)
2714}
2715
2716/// PLAN.md Phase 6.2 — build a listener that only serves `/metrics`
2717/// + `/health/*`. Suitable for Prometheus scrape ports that may be
2718///   exposed wider than the admin port.
2719#[inline(never)]
2720fn build_metrics_only_server(
2721    runtime: RedDBRuntime,
2722    auth_store: Arc<AuthStore>,
2723    bind_addr: String,
2724) -> RedDBServer {
2725    RedDBServer::with_options(
2726        runtime,
2727        ServerOptions {
2728            bind_addr,
2729            surface: crate::server::ServerSurface::MetricsOnly,
2730            ..ServerOptions::default()
2731        },
2732    )
2733    .with_auth(auth_store)
2734}
2735
2736/// Spawn dedicated admin / metrics listeners when the operator set
2737/// `RED_ADMIN_BIND` / `RED_METRICS_BIND`. Both are optional; when
2738/// unset the existing listener keeps serving everything (back-compat).
2739fn spawn_admin_metrics_listeners(runtime: &RedDBRuntime, auth_store: &Arc<AuthStore>) {
2740    if let Some(addr) = env_nonempty("RED_ADMIN_BIND") {
2741        let server = build_admin_only_server(runtime.clone(), auth_store.clone(), addr.clone());
2742        let _ = server.serve_in_background();
2743        tracing::info!(transport = "http", surface = "admin", bind = %addr, "listener online");
2744    }
2745    if let Some(addr) = env_nonempty("RED_METRICS_BIND") {
2746        let server = build_metrics_only_server(runtime.clone(), auth_store.clone(), addr.clone());
2747        let _ = server.serve_in_background();
2748        tracing::info!(transport = "http", surface = "metrics", bind = %addr, "listener online");
2749    }
2750}
2751
2752/// Resolve the HTTP TLS config from CLI / env / dev defaults.
2753fn resolve_http_tls_config(
2754    config: &ServerCommandConfig,
2755) -> Result<crate::server::tls::HttpTlsConfig, String> {
2756    match (&config.http_tls_cert, &config.http_tls_key) {
2757        (Some(cert), Some(key)) => Ok(crate::server::tls::HttpTlsConfig {
2758            cert_path: cert.clone(),
2759            key_path: key.clone(),
2760            client_ca_path: config.http_tls_client_ca.clone(),
2761        }),
2762        (None, None) => {
2763            // Dev-mode auto-generate next to the data directory.
2764            let dir = config
2765                .path
2766                .as_ref()
2767                .and_then(|p| p.parent().map(std::path::PathBuf::from))
2768                .unwrap_or_else(|| std::path::PathBuf::from("."));
2769            let auto = crate::server::tls::auto_generate_dev_cert(&dir)
2770                .map_err(|err| format!("HTTP TLS dev: {err}"))?;
2771            Ok(crate::server::tls::HttpTlsConfig {
2772                cert_path: auto.cert_path,
2773                key_path: auto.key_path,
2774                client_ca_path: config.http_tls_client_ca.clone(),
2775            })
2776        }
2777        _ => Err("HTTP TLS requires both --http-tls-cert and --http-tls-key (or neither, with RED_HTTP_TLS_DEV=1)".to_string()),
2778    }
2779}
2780
2781#[cfg(test)]
2782mod tests {
2783    use super::*;
2784
2785    #[test]
2786    fn render_systemd_unit_contains_expected_execstart() {
2787        let config = SystemdServiceConfig {
2788            service_name: "reddb".to_string(),
2789            binary_path: PathBuf::from("/usr/local/bin/red"),
2790            run_user: "reddb".to_string(),
2791            run_group: "reddb".to_string(),
2792            data_path: reddb_file::default_service_database_path(),
2793            router_bind_addr: None,
2794            grpc_bind_addr: Some("0.0.0.0:55055".to_string()),
2795            http_bind_addr: None,
2796        };
2797
2798        let unit = render_systemd_unit(&config);
2799        assert!(unit.contains("ExecStart=/usr/local/bin/red server --path /var/lib/reddb/data.rdb --grpc-bind 0.0.0.0:55055"));
2800        assert!(unit.contains("ReadWritePaths=/var/lib/reddb"));
2801    }
2802
2803    #[test]
2804    fn systemd_service_config_derives_paths() {
2805        let config = SystemdServiceConfig {
2806            service_name: "reddb-api".to_string(),
2807            binary_path: PathBuf::from("/usr/local/bin/red"),
2808            run_user: "reddb".to_string(),
2809            run_group: "reddb".to_string(),
2810            data_path: PathBuf::from("/srv/reddb/live/data.rdb"),
2811            router_bind_addr: None,
2812            grpc_bind_addr: None,
2813            http_bind_addr: Some("127.0.0.1:5000".to_string()),
2814        };
2815
2816        assert_eq!(config.data_dir(), PathBuf::from("/srv/reddb/live"));
2817        assert_eq!(
2818            config.unit_path(),
2819            PathBuf::from("/etc/systemd/system/reddb-api.service")
2820        );
2821    }
2822
2823    #[test]
2824    fn render_systemd_unit_supports_dual_transport() {
2825        let config = SystemdServiceConfig {
2826            service_name: "reddb".to_string(),
2827            binary_path: PathBuf::from("/usr/local/bin/red"),
2828            run_user: "reddb".to_string(),
2829            run_group: "reddb".to_string(),
2830            data_path: reddb_file::default_service_database_path(),
2831            router_bind_addr: None,
2832            grpc_bind_addr: Some("0.0.0.0:55055".to_string()),
2833            http_bind_addr: Some("0.0.0.0:5000".to_string()),
2834        };
2835
2836        let unit = render_systemd_unit(&config);
2837        assert!(unit.contains("--grpc-bind 0.0.0.0:55055"));
2838        assert!(unit.contains("--http-bind 0.0.0.0:5000"));
2839    }
2840
2841    #[test]
2842    fn render_systemd_unit_supports_router_mode() {
2843        let config = SystemdServiceConfig {
2844            service_name: "reddb".to_string(),
2845            binary_path: PathBuf::from("/usr/local/bin/red"),
2846            run_user: "reddb".to_string(),
2847            run_group: "reddb".to_string(),
2848            data_path: reddb_file::default_service_database_path(),
2849            router_bind_addr: Some(DEFAULT_ROUTER_BIND_ADDR.to_string()),
2850            grpc_bind_addr: None,
2851            http_bind_addr: None,
2852        };
2853
2854        let unit = render_systemd_unit(&config);
2855        assert!(unit.contains("--bind 127.0.0.1:5050"));
2856        assert!(!unit.contains("--grpc-bind"));
2857        assert!(!unit.contains("--http-bind"));
2858    }
2859
2860    #[test]
2861    fn explicit_bind_collision_is_fatal() {
2862        let held = TcpListener::bind("127.0.0.1:0").expect("hold test port");
2863        let addr = held.local_addr().expect("held addr").to_string();
2864        let mut readiness = TransportReadiness::default();
2865
2866        let error = bind_listener_for_startup(&mut readiness, "http", &addr, true).unwrap_err();
2867
2868        assert!(error.contains("explicit http listener bind"));
2869        assert_eq!(readiness.active.len(), 0);
2870        assert_eq!(readiness.failed.len(), 1);
2871        assert!(readiness.failed[0].explicit);
2872        assert_eq!(readiness.failed[0].bind_addr, addr);
2873    }
2874
2875    // ---------- Issue #663 — `--no-auth` / `--dev` ----------
2876
2877    // Env access in tests is process-global; serialise the two
2878    // `--no-auth` tests so the REDDB_USERNAME / REDDB_PASSWORD pair
2879    // one of them sets cannot leak into the other under cargo's
2880    // default parallel runner.
2881    fn no_auth_env_lock() -> &'static std::sync::Mutex<()> {
2882        static LOCK: std::sync::OnceLock<std::sync::Mutex<()>> = std::sync::OnceLock::new();
2883        LOCK.get_or_init(|| std::sync::Mutex::new(()))
2884    }
2885
2886    #[test]
2887    fn server_options_preserve_explicit_disk_residency() {
2888        let _guard = no_auth_env_lock().lock().unwrap();
2889        let mut config = no_auth_test_config(true);
2890        assert!(!config.to_db_options().unwrap().bounded_disk_residency);
2891        config.bounded_disk_residency = true;
2892        assert!(config.to_db_options().unwrap().bounded_disk_residency);
2893    }
2894
2895    fn no_auth_test_config(no_auth: bool) -> ServerCommandConfig {
2896        ServerCommandConfig {
2897            path: None,
2898            router_bind_addr: Some(DEFAULT_ROUTER_BIND_ADDR.to_string()),
2899            router_bind_explicit: false,
2900            grpc_bind_addr: None,
2901            grpc_bind_explicit: false,
2902            grpc_tls_bind_addr: None,
2903            grpc_tls_cert: None,
2904            grpc_tls_key: None,
2905            grpc_tls_client_ca: None,
2906            http_bind_addr: None,
2907            http_bind_explicit: false,
2908            http_tls_bind_addr: None,
2909            http_tls_cert: None,
2910            http_tls_key: None,
2911            http_tls_client_ca: None,
2912            wire_bind_addr: None,
2913            wire_bind_explicit: false,
2914            wire_tls_bind_addr: None,
2915            wire_tls_cert: None,
2916            wire_tls_key: None,
2917            pg_bind_addr: None,
2918            create_if_missing: true,
2919            bounded_disk_residency: false,
2920            read_only: false,
2921            role: "standalone".to_string(),
2922            primary_addr: None,
2923            storage_profile: StorageProfileSelection::embedded_single_file(),
2924            auth: false,
2925            require_auth: false,
2926            // Operator-set `--vault`: `--no-auth` must override this
2927            // alongside REDDB_USERNAME/PASSWORD.
2928            vault: true,
2929            no_auth,
2930            workers: None,
2931            telemetry: None,
2932            http_limits_cli: crate::server::HttpLimitsCliInput::default(),
2933            ui: false,
2934            ui_dir: None,
2935            bootstrap: BootstrapConfig::default(),
2936        }
2937    }
2938
2939    #[test]
2940    fn no_auth_flag_disables_every_auth_knob_and_stamps_metadata() {
2941        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
2942        // Pre-existing env that *would* turn auth on if `--no-auth`
2943        // weren't the last word. The acceptance criterion is that
2944        // the flag wins over env.
2945        // SAFETY: serialised by `no_auth_env_lock` above.
2946        unsafe {
2947            std::env::set_var("REDDB_USERNAME", "admin");
2948            std::env::set_var("REDDB_PASSWORD", "hunter2");
2949        }
2950        let config = no_auth_test_config(true);
2951        let options = config.to_db_options().expect("to_db_options");
2952
2953        assert!(no_auth_active(&options), "metadata should be stamped");
2954        assert!(!options.auth.enabled, "auth.enabled must be forced off");
2955        assert!(
2956            !options.auth.require_auth,
2957            "require_auth must be forced off"
2958        );
2959        assert!(
2960            !options.auth.vault_enabled,
2961            "vault_enabled must be forced off (overrides --vault)"
2962        );
2963        assert_eq!(
2964            options.metadata.get(NO_AUTH_META).map(String::as_str),
2965            Some("true"),
2966        );
2967
2968        // SAFETY: serialised by `no_auth_env_lock` above.
2969        unsafe {
2970            std::env::remove_var("REDDB_USERNAME");
2971            std::env::remove_var("REDDB_PASSWORD");
2972        }
2973    }
2974
2975    #[test]
2976    fn default_behaviour_without_no_auth_flag_is_unchanged() {
2977        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
2978        let config = no_auth_test_config(false);
2979        let options = config.to_db_options().expect("to_db_options");
2980
2981        assert!(
2982            !no_auth_active(&options),
2983            "default boot must not be marked no-auth"
2984        );
2985        assert!(
2986            options.metadata.get(NO_AUTH_META).is_none(),
2987            "metadata key must be absent when flag is off"
2988        );
2989        // `--vault` should still take effect when `--no-auth` is not set.
2990        assert!(options.auth.vault_enabled);
2991    }
2992
2993    #[test]
2994    fn no_auth_active_blocks_bootstrap_from_env() {
2995        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
2996        // SAFETY: serialised by `no_auth_env_lock` above. The pair
2997        // would normally cause `AuthStore::bootstrap_from_env` to
2998        // create an admin; the boot pipeline must suppress that call
2999        // whenever `no_auth_active` is true.
3000        unsafe {
3001            std::env::set_var("REDDB_USERNAME", "admin");
3002            std::env::set_var("REDDB_PASSWORD", "hunter2");
3003        }
3004
3005        let options = no_auth_test_config(true)
3006            .to_db_options()
3007            .expect("to_db_options");
3008
3009        // Mirror the exact branch in `build_runtime_with_telemetry`:
3010        // build a non-vault AuthStore from `options.auth`, then call
3011        // `bootstrap_from_env` *only* when the no-auth gate is off.
3012        let auth_store = AuthStore::new(options.auth.clone());
3013        if !no_auth_active(&options) {
3014            auth_store.bootstrap_from_env();
3015        }
3016
3017        assert!(
3018            auth_store.needs_bootstrap(),
3019            "no admin user must be bootstrapped under --no-auth even with REDDB_USERNAME/PASSWORD set"
3020        );
3021
3022        // SAFETY: serialised by `no_auth_env_lock` above.
3023        unsafe {
3024            std::env::remove_var("REDDB_USERNAME");
3025            std::env::remove_var("REDDB_PASSWORD");
3026        }
3027    }
3028
3029    // ---------- Issue #650 — bootstrap presets ----------
3030
3031    // Preset tests mutate process-global env (`REDDB_PRESET`,
3032    // `REDDB_USERNAME`, `REDDB_PASSWORD`) and the global tracing
3033    // subscriber. Share the no_auth lock so they don't race with each
3034    // other or with the --no-auth tests above.
3035    fn clear_preset_env() {
3036        // SAFETY: callers hold `no_auth_env_lock()`.
3037        unsafe {
3038            std::env::remove_var(BOOTSTRAP_PRESET_ENV);
3039            std::env::remove_var(PRESET_ENV);
3040            std::env::remove_var("REDDB_BOOTSTRAP_MANIFEST");
3041            std::env::remove_var("REDDB_AUTH");
3042            std::env::remove_var("REDDB_REQUIRE_AUTH");
3043            std::env::remove_var("REDDB_NO_AUTH");
3044            std::env::remove_var("REDDB_DEV");
3045            std::env::remove_var("REDDB_VAULT");
3046            std::env::remove_var("REDDB_USERNAME");
3047            std::env::remove_var("REDDB_PASSWORD");
3048            std::env::remove_var("REDDB_USERNAME_FILE");
3049            std::env::remove_var("REDDB_PASSWORD_FILE");
3050            std::env::remove_var("REDDB_CLOUD_HEAD_ADMIN");
3051            std::env::remove_var("REDDB_CLOUD_HEAD_ADMIN_PASSWORD");
3052            std::env::remove_var("REDDB_CLOUD_HEAD_ADMIN_PASSWORD_FILE");
3053            std::env::remove_var("REDDB_CUSTOMER_ADMIN");
3054            std::env::remove_var("REDDB_CUSTOMER_ADMIN_PASSWORD");
3055            std::env::remove_var("REDDB_CUSTOMER_ADMIN_PASSWORD_FILE");
3056        }
3057    }
3058
3059    fn clear_backup_env() {
3060        // SAFETY: callers hold `no_auth_env_lock()`.
3061        unsafe {
3062            std::env::remove_var("REDDB_BACKUP_S3_ENDPOINT");
3063            std::env::remove_var("REDDB_BACKUP_S3_BUCKET");
3064            std::env::remove_var("REDDB_BACKUP_S3_PREFIX");
3065            std::env::remove_var("REDDB_BACKUP_S3_ACCESS_KEY_ID");
3066            std::env::remove_var("REDDB_BACKUP_S3_SECRET_ACCESS_KEY");
3067            std::env::remove_var("REDDB_BACKUP_S3_REGION");
3068            std::env::remove_var("REDDB_BACKUP_CHECKPOINT_INTERVAL_SECS");
3069            std::env::remove_var("REDDB_BACKUP_WAL_FLUSH_INTERVAL_SECS");
3070            std::env::remove_var("REDDB_BACKUP_PAUSE_ON_LAG_SECS");
3071        }
3072    }
3073
3074    fn fresh_runtime_and_store() -> (RedDBRuntime, Arc<AuthStore>) {
3075        let runtime = RedDBRuntime::with_options(RedDBOptions::in_memory()).expect("runtime");
3076        let auth_store = Arc::new(AuthStore::new(crate::auth::AuthConfig::default()));
3077        (runtime, auth_store)
3078    }
3079
3080    #[test]
3081    fn auth_env_knobs_enable_auth_require_auth_and_vault() {
3082        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3083        clear_preset_env();
3084        // SAFETY: env serialised by `no_auth_env_lock`.
3085        unsafe {
3086            std::env::set_var("REDDB_AUTH", "true");
3087            std::env::set_var("REDDB_REQUIRE_AUTH", "true");
3088            std::env::set_var("REDDB_VAULT", "true");
3089        }
3090
3091        let mut config = no_auth_test_config(false);
3092        config.vault = false;
3093        let options = config.to_db_options().expect("to_db_options");
3094
3095        assert!(options.auth.enabled);
3096        assert!(options.auth.require_auth);
3097        assert!(options.auth.vault_enabled);
3098
3099        clear_preset_env();
3100    }
3101
3102    #[test]
3103    fn production_and_cloud_presets_force_auth_require_auth_and_vault() {
3104        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3105        clear_preset_env();
3106
3107        for preset in [PRESET_PRODUCTION, PRESET_CLOUD] {
3108            let mut config = no_auth_test_config(false);
3109            config.vault = false;
3110            config.bootstrap.preset = Some(preset.to_string());
3111
3112            let options = config.to_db_options().expect("to_db_options");
3113            assert!(options.auth.enabled, "{preset} should enable auth");
3114            assert!(
3115                options.auth.require_auth,
3116                "{preset} should require authenticated requests"
3117            );
3118            assert!(options.auth.vault_enabled, "{preset} should enable vault");
3119            assert!(!no_auth_active(&options));
3120        }
3121
3122        clear_preset_env();
3123    }
3124
3125    #[test]
3126    fn no_auth_env_overrides_preset_forced_auth() {
3127        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3128        clear_preset_env();
3129        // SAFETY: env serialised by `no_auth_env_lock`.
3130        unsafe {
3131            std::env::set_var("REDDB_NO_AUTH", "true");
3132            std::env::set_var(BOOTSTRAP_PRESET_ENV, PRESET_CLOUD);
3133        }
3134
3135        let mut config = no_auth_test_config(false);
3136        config.auth = true;
3137        config.require_auth = true;
3138        let options = config.to_db_options().expect("to_db_options");
3139
3140        assert!(no_auth_active(&options));
3141        assert!(!options.auth.enabled);
3142        assert!(!options.auth.require_auth);
3143        assert!(!options.auth.vault_enabled);
3144
3145        clear_preset_env();
3146    }
3147
3148    #[test]
3149    fn simple_preset_is_default_and_persists_state() {
3150        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3151        clear_preset_env();
3152
3153        let (runtime, auth_store) = fresh_runtime_and_store();
3154        apply_preset(&runtime, &auth_store).expect("simple preset applies cleanly");
3155
3156        // No admin was created — `simple` is anonymous-friendly.
3157        assert!(
3158            auth_store.needs_bootstrap(),
3159            "simple preset must not create an admin"
3160        );
3161
3162        // Bootstrap state persisted so the next boot is a no-op.
3163        let store = runtime.db().store();
3164        let completed = store
3165            .get_config(BOOTSTRAP_COMPLETED_KEY)
3166            .expect("completed key persisted");
3167        assert!(matches!(completed, reddb_types::Value::Boolean(true)));
3168        let preset = store
3169            .get_config(BOOTSTRAP_PRESET_KEY)
3170            .expect("preset key persisted");
3171        match preset {
3172            reddb_types::Value::Text(s) => assert_eq!(s.as_ref(), PRESET_SIMPLE),
3173            other => panic!("expected Text(simple), got {other:?}"),
3174        }
3175        assert!(
3176            store.get_config(BOOTSTRAP_FIRST_ADMIN_KEY).is_none(),
3177            "simple preset must not record a first admin"
3178        );
3179
3180        clear_preset_env();
3181    }
3182
3183    #[test]
3184    fn production_preset_creates_first_admin_with_allow_all_policy() {
3185        use crate::auth::policies::{EvalContext, ResourceRef};
3186        use crate::auth::UserId;
3187
3188        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3189        clear_preset_env();
3190        // SAFETY: env serialised by `no_auth_env_lock`.
3191        unsafe {
3192            std::env::set_var(PRESET_ENV, PRESET_PRODUCTION);
3193            std::env::set_var("REDDB_USERNAME", "ops");
3194            std::env::set_var("REDDB_PASSWORD", "hunter2");
3195        }
3196
3197        let (runtime, auth_store) = fresh_runtime_and_store();
3198        apply_preset(&runtime, &auth_store).expect("production preset applies cleanly");
3199
3200        // Admin exists and the auth store is sealed.
3201        assert!(
3202            !auth_store.needs_bootstrap(),
3203            "production preset must seal bootstrap"
3204        );
3205        let users = auth_store.list_users();
3206        assert_eq!(users.len(), 1);
3207        let admin = &users[0];
3208        assert_eq!(admin.username, "ops");
3209        assert!(
3210            admin.tenant_id.is_none(),
3211            "first admin must be platform-scoped (tenant=None)"
3212        );
3213
3214        // Allow-all policy was installed and attached to the first admin.
3215        let policy = auth_store
3216            .get_policy(FIRST_ADMIN_ALLOW_ALL_POLICY)
3217            .expect("allow-all policy installed");
3218        assert!(!policy.statements.is_empty());
3219
3220        // Verify policy-derived authority via the policy evaluator —
3221        // not a bypass. Any action on any resource must Allow.
3222        let actor = UserId::platform("ops");
3223        let ctx = EvalContext {
3224            principal_tenant: None,
3225            current_tenant: None,
3226            peer_ip: None,
3227            mfa_present: false,
3228            now_ms: 1_700_000_000_000,
3229            principal_is_admin_role: true,
3230            principal_is_platform_scoped: true,
3231        };
3232        let arbitrary_resource = ResourceRef::new("config", "red.config.audit.enabled");
3233        assert!(
3234            auth_store.check_policy_authz(&actor, "config:write", &arbitrary_resource, &ctx),
3235            "allow-all policy must grant arbitrary actions via the evaluator"
3236        );
3237
3238        // Persisted state records the first admin id.
3239        let store = runtime.db().store();
3240        match store
3241            .get_config(BOOTSTRAP_FIRST_ADMIN_KEY)
3242            .expect("first_admin_id persisted")
3243        {
3244            reddb_types::Value::Text(s) => assert_eq!(s.as_ref(), "ops"),
3245            other => panic!("expected Text(ops), got {other:?}"),
3246        }
3247        match store.get_config(BOOTSTRAP_PRESET_KEY).unwrap() {
3248            reddb_types::Value::Text(s) => assert_eq!(s.as_ref(), PRESET_PRODUCTION),
3249            other => panic!("expected Text(production), got {other:?}"),
3250        }
3251
3252        clear_preset_env();
3253    }
3254
3255    #[test]
3256    fn bootstrap_preset_env_takes_precedence_over_legacy_preset_env() {
3257        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3258        clear_preset_env();
3259        // SAFETY: env serialised by `no_auth_env_lock`.
3260        unsafe {
3261            std::env::set_var(BOOTSTRAP_PRESET_ENV, PRESET_REGULATED);
3262            std::env::set_var(PRESET_ENV, PRESET_SIMPLE);
3263        }
3264
3265        let options = no_auth_test_config(false)
3266            .to_db_options()
3267            .expect("regulated options");
3268        assert!(
3269            options.control_events.compliance_mode,
3270            "canonical REDDB_BOOTSTRAP_PRESET should win over REDDB_PRESET"
3271        );
3272        assert!(options.query_audit.enabled);
3273
3274        clear_preset_env();
3275    }
3276
3277    #[test]
3278    fn cloud_preset_creates_system_head_and_customer_admins() {
3279        use crate::auth::policies::{EvalContext, ResourceRef};
3280        use crate::auth::UserId;
3281
3282        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3283        clear_preset_env();
3284        // SAFETY: env serialised by `no_auth_env_lock`.
3285        unsafe {
3286            std::env::set_var(BOOTSTRAP_PRESET_ENV, PRESET_CLOUD);
3287            std::env::set_var("REDDB_CLOUD_HEAD_ADMIN", "head");
3288            std::env::set_var("REDDB_CLOUD_HEAD_ADMIN_PASSWORD", "head-pass");
3289            std::env::set_var("REDDB_CUSTOMER_ADMIN", "customer");
3290            std::env::set_var("REDDB_CUSTOMER_ADMIN_PASSWORD", "customer-pass");
3291        }
3292
3293        let (runtime, auth_store) = fresh_runtime_and_store();
3294        apply_preset(&runtime, &auth_store).expect("cloud preset applies cleanly");
3295
3296        let head = auth_store
3297            .get_user(None, "head")
3298            .expect("head admin should exist");
3299        assert_eq!(head.tenant_id, None);
3300        let customer = auth_store
3301            .get_user(None, "customer")
3302            .expect("customer admin should exist");
3303        assert_eq!(customer.tenant_id, None);
3304
3305        let ctx = EvalContext {
3306            principal_tenant: None,
3307            current_tenant: None,
3308            peer_ip: None,
3309            mfa_present: false,
3310            now_ms: 1_700_000_000_000,
3311            principal_is_admin_role: true,
3312            principal_is_platform_scoped: true,
3313        };
3314        assert!(auth_store.check_policy_authz(
3315            &UserId::platform("customer"),
3316            "config:write",
3317            &ResourceRef::new("config", "red.config.customer.enabled"),
3318            &ctx,
3319        ));
3320        assert!(
3321            auth_store.check_policy_authz(
3322                &UserId::platform("head"),
3323                "config:write",
3324                &ResourceRef::new("config", "red.config.cloud.enabled"),
3325                &ctx,
3326            ),
3327            "cloud head keeps allow-all authority over managed cloud config"
3328        );
3329        assert!(
3330            !auth_store.check_policy_authz(
3331                &UserId::platform("customer"),
3332                "config:write",
3333                &ResourceRef::new("config", "red.config.cloud.enabled"),
3334                &ctx,
3335            ),
3336            "cloud customer must be denied managed cloud config writes"
3337        );
3338        assert!(
3339            !auth_store.check_policy_authz(
3340                &UserId::platform("customer"),
3341                "policy:drop",
3342                &ResourceRef::new("policy", CLOUD_PROTECT_MANAGED_POLICY),
3343                &ctx,
3344            ),
3345            "cloud customer must be denied mutations of the managed guardrail policy"
3346        );
3347
3348        assert!(auth_store.get_user(None, "head").is_some());
3349        assert!(auth_store
3350            .get_policy(CLOUD_PROTECT_MANAGED_POLICY)
3351            .is_some());
3352        assert!(runtime
3353            .config_registry()
3354            .get_active(CLOUD_CONFIG_NAMESPACE)
3355            .is_some());
3356
3357        let store = runtime.db().store();
3358        match store
3359            .get_config(BOOTSTRAP_FIRST_ADMIN_KEY)
3360            .expect("head admin id persisted")
3361        {
3362            reddb_types::Value::Text(s) => assert_eq!(s.as_ref(), "head"),
3363            other => panic!("expected Text(head), got {other:?}"),
3364        }
3365        match store.get_config(BOOTSTRAP_PRESET_KEY).unwrap() {
3366            reddb_types::Value::Text(s) => assert_eq!(s.as_ref(), PRESET_CLOUD),
3367            other => panic!("expected Text(cloud), got {other:?}"),
3368        }
3369
3370        clear_preset_env();
3371    }
3372
3373    #[test]
3374    fn cloud_preset_customer_cannot_disable_head_admin_or_drop_guardrail_policy() {
3375        use crate::auth::Role;
3376        use crate::runtime::mvcc::{clear_current_auth_identity, set_current_auth_identity};
3377
3378        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3379        clear_preset_env();
3380
3381        let bootstrap = BootstrapConfig {
3382            preset: Some(PRESET_CLOUD.to_string()),
3383            cloud_head_admin: Some("red_admin".to_string()),
3384            cloud_head_admin_password: Some("head-pass".to_string()),
3385            customer_admin: Some("admin".to_string()),
3386            customer_admin_password: Some("customer-pass".to_string()),
3387            ..BootstrapConfig::default()
3388        };
3389        let (runtime, auth_store) = fresh_runtime_and_store();
3390        apply_preset_from_config(&runtime, &auth_store, &bootstrap)
3391            .expect("cloud preset applies cleanly");
3392        runtime.set_auth_store(Arc::clone(&auth_store));
3393
3394        set_current_auth_identity("admin".to_string(), Role::Admin);
3395        let disable_head = runtime.execute_query("ALTER USER red_admin DISABLE");
3396        let drop_guardrail =
3397            runtime.execute_query(&format!("DROP POLICY '{}'", CLOUD_PROTECT_MANAGED_POLICY));
3398        let create_user = runtime.execute_query("CREATE USER app_user WITH PASSWORD 'p' ROLE read");
3399        let disable_user = runtime.execute_query("ALTER USER app_user DISABLE");
3400        clear_current_auth_identity();
3401
3402        let disable_head_err = disable_head.expect_err("customer admin must not disable red_admin");
3403        assert!(
3404            disable_head_err.to_string().contains("user:disable"),
3405            "error should name the denied lifecycle action: {disable_head_err}"
3406        );
3407        assert!(
3408            auth_store
3409                .get_user(None, "red_admin")
3410                .expect("red_admin should still exist")
3411                .enabled,
3412            "denied mutation must leave red_admin enabled"
3413        );
3414
3415        let drop_guardrail_err =
3416            drop_guardrail.expect_err("customer admin must not drop cloud guardrail policy");
3417        assert!(
3418            drop_guardrail_err.to_string().contains("managed policy"),
3419            "error should name the managed policy guardrail: {drop_guardrail_err}"
3420        );
3421        assert!(auth_store
3422            .get_policy(CLOUD_PROTECT_MANAGED_POLICY)
3423            .is_some());
3424
3425        create_user.expect("customer admin should retain normal create-user authority");
3426        disable_user.expect("customer admin should retain normal user-disable authority");
3427        assert!(
3428            !auth_store
3429                .get_user(None, "app_user")
3430                .expect("app_user should exist")
3431                .enabled,
3432            "normal capability should still apply outside the reserved head admin"
3433        );
3434
3435        clear_preset_env();
3436    }
3437
3438    #[test]
3439    fn cloud_preset_cli_admin_alias_wins_over_cloud_env_password() {
3440        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3441        clear_preset_env();
3442        // SAFETY: env serialised by `no_auth_env_lock`.
3443        unsafe {
3444            std::env::set_var("REDDB_CLOUD_HEAD_ADMIN_PASSWORD", "env-head-pass");
3445        }
3446
3447        let bootstrap = BootstrapConfig {
3448            preset: Some(PRESET_CLOUD.to_string()),
3449            admin_username: Some("head".to_string()),
3450            admin_password: Some("cli-head-pass".to_string()),
3451            customer_admin: Some("customer".to_string()),
3452            customer_admin_password: Some("customer-pass".to_string()),
3453            ..BootstrapConfig::default()
3454        };
3455        let (runtime, auth_store) = fresh_runtime_and_store();
3456        apply_preset_from_config(&runtime, &auth_store, &bootstrap)
3457            .expect("cloud preset applies cleanly");
3458
3459        auth_store
3460            .authenticate("head", "cli-head-pass")
3461            .expect("CLI alias password should win");
3462        assert!(
3463            auth_store.authenticate("head", "env-head-pass").is_err(),
3464            "cloud-specific env password must not beat CLI alias"
3465        );
3466
3467        clear_preset_env();
3468    }
3469
3470    #[test]
3471    fn bootstrap_intent_manifest_matches_cloud_flag_path() {
3472        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3473        clear_preset_env();
3474
3475        let manifest_dir = std::env::current_dir()
3476            .expect("current dir")
3477            .join(".red/tmp/bootstrap-manifest-tests");
3478        std::fs::create_dir_all(&manifest_dir).expect("create manifest test dir");
3479        let unique = format!(
3480            "{}-{}",
3481            std::process::id(),
3482            std::time::SystemTime::now()
3483                .duration_since(std::time::UNIX_EPOCH)
3484                .unwrap_or_default()
3485                .as_millis()
3486        );
3487        let head_password = manifest_dir.join(format!("head-{unique}.secret"));
3488        let customer_password = manifest_dir.join(format!("customer-{unique}.secret"));
3489        let manifest_path = manifest_dir.join(format!("intent-{unique}.json"));
3490        std::fs::write(&head_password, "head-pass\n").expect("write head secret");
3491        std::fs::write(&customer_password, "customer-pass\n").expect("write customer secret");
3492        std::fs::write(
3493            &manifest_path,
3494            format!(
3495                r#"{{
3496                    "bootstrap": {{
3497                        "preset": "cloud",
3498                        "cloud_head_admin": {{
3499                            "username": "head",
3500                            "password_file": "{}"
3501                        }},
3502                        "customer_admin": {{
3503                            "username": "customer",
3504                            "password_file": "{}"
3505                        }}
3506                    }}
3507                }}"#,
3508                head_password.display(),
3509                customer_password.display()
3510            ),
3511        )
3512        .expect("write manifest");
3513
3514        let mut config = no_auth_test_config(false);
3515        config.bootstrap.manifest = Some(manifest_path.clone());
3516        let options = config.to_db_options().expect("manifest options");
3517        assert!(options.auth.enabled);
3518        assert!(options.auth.require_auth);
3519        assert!(options.auth.vault_enabled);
3520
3521        let (runtime, auth_store) = fresh_runtime_and_store();
3522        apply_preset_from_config(&runtime, &auth_store, &config.bootstrap)
3523            .expect("manifest intent applies cleanly");
3524
3525        auth_store
3526            .authenticate("head", "head-pass")
3527            .expect("head password comes from file");
3528        auth_store
3529            .authenticate("customer", "customer-pass")
3530            .expect("customer password comes from file");
3531        assert!(auth_store.get_user(None, "head").is_some());
3532        assert!(auth_store.get_user(None, "customer").is_some());
3533        assert!(auth_store
3534            .get_policy(CLOUD_PROTECT_MANAGED_POLICY)
3535            .is_some());
3536        match runtime
3537            .db()
3538            .store()
3539            .get_config(BOOTSTRAP_PRESET_KEY)
3540            .expect("preset key persisted")
3541        {
3542            reddb_types::Value::Text(s) => assert_eq!(s.as_ref(), PRESET_CLOUD),
3543            other => panic!("expected Text(cloud), got {other:?}"),
3544        }
3545
3546        apply_preset_from_config(&runtime, &auth_store, &config.bootstrap)
3547            .expect("second boot is idempotent");
3548
3549        let _ = std::fs::remove_file(&manifest_path);
3550        let _ = std::fs::remove_file(&head_password);
3551        let _ = std::fs::remove_file(&customer_password);
3552        clear_preset_env();
3553    }
3554
3555    #[test]
3556    fn regulated_preset_enables_query_audit_infrastructure_without_rules() {
3557        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3558        clear_preset_env();
3559        // SAFETY: env serialised by `no_auth_env_lock`.
3560        unsafe {
3561            std::env::set_var(PRESET_ENV, PRESET_REGULATED);
3562        }
3563
3564        let (runtime, auth_store) = fresh_runtime_and_store();
3565        apply_preset(&runtime, &auth_store).expect("regulated preset applies cleanly");
3566
3567        assert!(runtime.query_audit().is_enabled());
3568        assert!(runtime.query_audit().rules().is_empty());
3569        assert!(
3570            runtime
3571                .db()
3572                .store()
3573                .get_collection(crate::runtime::query_audit::QUERY_AUDIT_COLLECTION)
3574                .is_some(),
3575            "regulated preset should create the query-audit stream"
3576        );
3577
3578        runtime
3579            .execute_query("CREATE TABLE docs (id INT)")
3580            .expect("create table");
3581        runtime
3582            .execute_query("INSERT INTO docs (id) VALUES (1)")
3583            .expect("insert");
3584        runtime.execute_query("SELECT * FROM docs").expect("select");
3585        let rows = runtime
3586            .db()
3587            .store()
3588            .get_collection(crate::runtime::query_audit::QUERY_AUDIT_COLLECTION)
3589            .expect("query audit collection")
3590            .query_all(|_| true);
3591        assert!(
3592            rows.is_empty(),
3593            "regulated preset must not globally audit every query"
3594        );
3595
3596        clear_preset_env();
3597    }
3598
3599    #[test]
3600    fn managed_backup_env_rejects_primary_replica_single_file_storage() {
3601        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3602        clear_backup_env();
3603        // SAFETY: env serialised by `no_auth_env_lock`.
3604        unsafe {
3605            std::env::set_var("REDDB_BACKUP_S3_ENDPOINT", "https://s3.example.test");
3606            std::env::set_var("REDDB_BACKUP_S3_BUCKET", "reddb");
3607            std::env::set_var("REDDB_BACKUP_S3_PREFIX", "clusters/prod");
3608            std::env::set_var("REDDB_BACKUP_S3_ACCESS_KEY_ID", "AK");
3609            std::env::set_var("REDDB_BACKUP_S3_SECRET_ACCESS_KEY", "SK");
3610        }
3611
3612        let mut config = no_auth_test_config(false);
3613        config.role = "primary".to_string();
3614        config.storage_profile = crate::storage::StorageDeployPreset::PrimaryReplicaDev.selection();
3615
3616        let err = config.to_db_options().unwrap_err();
3617        assert!(err.contains("managed backup"), "got: {err}");
3618        assert!(err.contains("operational-directory"), "got: {err}");
3619
3620        clear_backup_env();
3621    }
3622
3623    #[test]
3624    fn regulated_preset_installs_managed_evidence_guardrails_end_to_end() {
3625        use crate::auth::policies::{EvalContext, Policy, ResourceRef};
3626        use crate::auth::store::PrincipalRef;
3627        use crate::auth::{Role, UserId};
3628        use crate::runtime::mvcc::{clear_current_auth_identity, set_current_auth_identity};
3629        use reddb_types::Value;
3630
3631        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3632        clear_preset_env();
3633        // SAFETY: env serialised by `no_auth_env_lock`.
3634        unsafe {
3635            std::env::set_var(PRESET_ENV, PRESET_REGULATED);
3636        }
3637
3638        let options = no_auth_test_config(false)
3639            .to_db_options()
3640            .expect("regulated options");
3641        assert!(
3642            options.control_events.compliance_mode,
3643            "regulated preset must enable fail-closed control evidence before runtime boot"
3644        );
3645        assert!(
3646            options.query_audit.enabled && options.query_audit.rules.is_empty(),
3647            "regulated preset must enable query-audit infrastructure without global rules"
3648        );
3649
3650        let runtime = RedDBRuntime::with_options(options).expect("runtime");
3651        let auth_store = Arc::new(AuthStore::new(crate::auth::AuthConfig::default()));
3652        apply_preset(&runtime, &auth_store).expect("regulated preset applies cleanly");
3653        runtime.set_auth_store(Arc::clone(&auth_store));
3654
3655        assert!(runtime.control_events_require_persistence());
3656        assert!(runtime.query_audit().is_enabled());
3657        assert!(runtime.query_audit().rules().is_empty());
3658        assert!(auth_store
3659            .get_policy(REGULATED_PROTECT_MANAGED_POLICY)
3660            .is_some());
3661
3662        let managed_policy = runtime
3663            .config_registry()
3664            .get_active(REGULATED_PROTECT_MANAGED_POLICY)
3665            .expect("regulated managed policy registry entry");
3666        assert!(managed_policy.managed);
3667        assert_eq!(managed_policy.resource_type, "policy");
3668        assert!(
3669            runtime
3670                .config_registry()
3671                .get_active(REGULATED_AUDIT_CONFIG_NAMESPACE)
3672                .expect("regulated audit config namespace")
3673                .managed
3674        );
3675
3676        let registry_rows = runtime
3677            .execute_query(&format!(
3678                "SELECT id, managed FROM red.registry WHERE id = '{}'",
3679                REGULATED_PROTECT_MANAGED_POLICY
3680            ))
3681            .expect("red.registry query");
3682        assert_eq!(registry_rows.result.records.len(), 1);
3683        assert_eq!(
3684            registry_rows.result.records[0].get("managed"),
3685            Some(&Value::Boolean(true))
3686        );
3687
3688        let managed_policy_rows = runtime
3689            .execute_query(&format!(
3690                "SELECT policy_id FROM red.managed_policies WHERE policy_id = '{}'",
3691                REGULATED_PROTECT_MANAGED_POLICY
3692            ))
3693            .expect("red.managed_policies query");
3694        assert_eq!(managed_policy_rows.result.records.len(), 1);
3695
3696        let capability_rows = runtime
3697            .execute_query(
3698                "SELECT action FROM red.control_capabilities WHERE action = 'evidence:export'",
3699            )
3700            .expect("red.control_capabilities query");
3701        assert_eq!(capability_rows.result.records.len(), 1);
3702
3703        auth_store
3704            .create_user("alice", "p", Role::Admin)
3705            .expect("create ordinary admin");
3706        let allow_all = Policy::from_json_str(
3707            r#"{
3708                "id": "alice-allow-all",
3709                "version": 1,
3710                "statements": [{
3711                    "effect": "allow",
3712                    "actions": ["*"],
3713                    "resources": ["*"]
3714                }]
3715            }"#,
3716        )
3717        .expect("allow-all policy");
3718        auth_store.put_policy(allow_all).expect("install allow-all");
3719        auth_store
3720            .attach_policy(
3721                PrincipalRef::User(UserId::platform("alice")),
3722                "alice-allow-all",
3723            )
3724            .expect("attach allow-all");
3725        let ctx = EvalContext {
3726            principal_tenant: None,
3727            current_tenant: None,
3728            peer_ip: None,
3729            mfa_present: false,
3730            now_ms: 1_700_000_000_000,
3731            principal_is_admin_role: true,
3732            principal_is_platform_scoped: true,
3733        };
3734        assert!(
3735            !auth_store.check_policy_authz(
3736                &UserId::platform("alice"),
3737                "policy:drop",
3738                &ResourceRef::new("policy", REGULATED_PROTECT_MANAGED_POLICY),
3739                &ctx,
3740            ),
3741            "managed guardrail deny policy must be effective for ordinary admins"
3742        );
3743
3744        set_current_auth_identity("alice".to_string(), Role::Admin);
3745        let denied = runtime.execute_query(&format!(
3746            "DROP POLICY '{}'",
3747            REGULATED_PROTECT_MANAGED_POLICY
3748        ));
3749        clear_current_auth_identity();
3750        let err = denied.expect_err("managed policy guardrail must deny ordinary admin");
3751        assert!(
3752            err.to_string().contains("managed policy"),
3753            "error should name the managed guardrail: {err}"
3754        );
3755        assert!(
3756            auth_store
3757                .get_policy(REGULATED_PROTECT_MANAGED_POLICY)
3758                .is_some(),
3759            "denied mutation must leave managed policy installed"
3760        );
3761
3762        let denied_events = runtime
3763            .execute_query(&format!(
3764                "SELECT action, resource, outcome FROM red.control_events \
3765                 WHERE action = 'policy:drop' AND resource = 'policy:{}'",
3766                REGULATED_PROTECT_MANAGED_POLICY
3767            ))
3768            .expect("red.control_events denied policy drop");
3769        assert_eq!(denied_events.result.records.len(), 1);
3770        assert_eq!(
3771            denied_events.result.records[0].get("outcome"),
3772            Some(&Value::text("denied"))
3773        );
3774
3775        set_current_auth_identity("alice".to_string(), Role::Admin);
3776        let config_denied = runtime.execute_query("SET CONFIG red.config.audit.enabled = true");
3777        clear_current_auth_identity();
3778        let err = config_denied.expect_err("managed config guardrail must deny ordinary admin");
3779        assert!(
3780            err.to_string().contains("managed config"),
3781            "error should name the managed config guardrail: {err}"
3782        );
3783
3784        let denied_config_events = runtime
3785            .execute_query(
3786                "SELECT action, resource, outcome FROM red.control_events \
3787                 WHERE action = 'config:write' AND resource = 'config:red.config.audit.enabled'",
3788            )
3789            .expect("red.control_events denied config write");
3790        assert_eq!(denied_config_events.result.records.len(), 1);
3791        assert_eq!(
3792            denied_config_events.result.records[0].get("outcome"),
3793            Some(&Value::text("denied"))
3794        );
3795
3796        runtime
3797            .execute_query("CREATE TABLE regulated_docs (id INT)")
3798            .expect("create user table");
3799        runtime
3800            .execute_query("SELECT * FROM regulated_docs")
3801            .expect("select user table");
3802        let audit_rows = runtime
3803            .db()
3804            .store()
3805            .get_collection(crate::runtime::query_audit::QUERY_AUDIT_COLLECTION)
3806            .expect("query audit collection")
3807            .query_all(|_| true);
3808        assert!(
3809            audit_rows.is_empty(),
3810            "regulated preset must not globally audit data-plane queries"
3811        );
3812
3813        clear_preset_env();
3814    }
3815
3816    #[test]
3817    fn bootstrap_manifest_installs_initial_users_policies_guardrails_and_config() {
3818        use crate::auth::policies::{EvalContext, ResourceRef};
3819        use crate::auth::UserId;
3820        use reddb_types::Value;
3821
3822        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3823        clear_preset_env();
3824
3825        let manifest_dir = std::env::current_dir()
3826            .expect("current dir")
3827            .join(".red/tmp/bootstrap-manifest-tests");
3828        std::fs::create_dir_all(&manifest_dir).expect("create manifest test dir");
3829        let manifest_path = manifest_dir.join(format!(
3830            "reddb-bootstrap-manifest-{}-{}.json",
3831            std::process::id(),
3832            std::time::SystemTime::now()
3833                .duration_since(std::time::UNIX_EPOCH)
3834                .unwrap_or_default()
3835                .as_millis()
3836        ));
3837        std::fs::write(
3838            &manifest_path,
3839            r#"{
3840                "users": [
3841                    {
3842                        "username": "ops",
3843                        "password": "hunter2",
3844                        "role": "admin"
3845                    }
3846                ],
3847                "policies": [
3848                    {
3849                        "id": "bootstrap-registry-admin",
3850                        "version": 1,
3851                        "statements": [
3852                            {
3853                                "effect": "allow",
3854                                "actions": ["red.registry:*", "policy:*", "config:write", "select"],
3855                                "resources": ["registry:*", "policy:*", "config:*", "collection:docs"]
3856                            }
3857                        ]
3858                    }
3859                ],
3860                "managed_policies": [
3861                    {
3862                        "id": "managed-deny-drop",
3863                        "version": 1,
3864                        "statements": [
3865                            {
3866                                "effect": "deny",
3867                                "actions": ["policy:drop"],
3868                                "resources": ["policy:managed-deny-drop"]
3869                            }
3870                        ],
3871                        "required_resource": "policy:managed-deny-drop",
3872                        "evidence": "full"
3873                    }
3874                ],
3875                "attachments": [
3876                    {"user": "ops", "policy": "bootstrap-registry-admin"}
3877                ],
3878                "managed_config_namespaces": [
3879                    {
3880                        "id": "red.ai",
3881                        "required_action": "config:write",
3882                        "required_resource": "config:red.ai.*",
3883                        "evidence": "metadata"
3884                    }
3885                ],
3886                "config": [
3887                    {"key": "red.ai.default.provider", "value": "openai"},
3888                    {
3889                        "key": "red.ai.openai.default.secret_ref",
3890                        "secret_ref": {"collection": "red.vault", "key": "openai"}
3891                    }
3892                ],
3893                "actor": "ops"
3894            }"#,
3895        )
3896        .expect("write manifest");
3897        // SAFETY: env serialised by `no_auth_env_lock`.
3898        unsafe {
3899            std::env::set_var("REDDB_BOOTSTRAP_MANIFEST", &manifest_path);
3900        }
3901
3902        let (runtime, auth_store) = fresh_runtime_and_store();
3903        apply_preset(&runtime, &auth_store).expect("manifest applies cleanly");
3904
3905        let users = auth_store.list_users();
3906        assert_eq!(users.len(), 1);
3907        assert_eq!(users[0].username, "ops");
3908        assert!(users[0].tenant_id.is_none());
3909
3910        let actor = UserId::platform("ops");
3911        let ctx = EvalContext {
3912            principal_tenant: None,
3913            current_tenant: None,
3914            peer_ip: None,
3915            mfa_present: false,
3916            now_ms: 1_700_000_000_000,
3917            principal_is_admin_role: true,
3918            principal_is_platform_scoped: true,
3919        };
3920        // Manifest fixture pins a canonical data-plane read action.
3921        assert!(auth_store.check_policy_authz(
3922            &actor,
3923            "select",
3924            &ResourceRef::new("collection", "docs"),
3925            &ctx
3926        ));
3927
3928        let managed_policy = runtime
3929            .config_registry()
3930            .get_active("managed-deny-drop")
3931            .expect("managed policy registry entry");
3932        assert!(managed_policy.managed);
3933        assert_eq!(managed_policy.resource_type, "policy");
3934        let managed_config = runtime
3935            .config_registry()
3936            .get_active("red.ai")
3937            .expect("managed config namespace registry entry");
3938        assert!(managed_config.managed);
3939        assert_eq!(managed_config.resource_type, "config_namespace");
3940
3941        let store = runtime.db().store();
3942        match store
3943            .get_config("red.ai.default.provider")
3944            .expect("plain config persisted")
3945        {
3946            Value::Text(s) => assert_eq!(s.as_ref(), "openai"),
3947            other => panic!("expected provider text, got {other:?}"),
3948        }
3949        let Value::Json(bytes) = store
3950            .get_config("red.ai.openai.default.secret_ref")
3951            .expect("secret ref config persisted")
3952        else {
3953            panic!("secret ref must be stored as structured JSON");
3954        };
3955        let reference: crate::serde_json::Value =
3956            crate::serde_json::from_slice(&bytes).expect("secret ref json");
3957        assert_eq!(
3958            reference.get("type").and_then(|v| v.as_str()),
3959            Some("secret_ref")
3960        );
3961        assert!(
3962            !String::from_utf8_lossy(&bytes).contains("hunter2"),
3963            "manifest password must not leak into secret ref config"
3964        );
3965
3966        let completed = store
3967            .get_config(BOOTSTRAP_COMPLETED_KEY)
3968            .expect("bootstrap completion persisted");
3969        assert!(matches!(completed, Value::Boolean(true)));
3970        assert!(
3971            store
3972                .get_config("system.bootstrap.manifest.registry_entries")
3973                .is_some(),
3974            "managed registry entries must be persisted internally"
3975        );
3976
3977        std::fs::remove_file(&manifest_path).expect("remove manifest after first boot");
3978        let restored_registry = Arc::new(crate::auth::registry::ConfigRegistry::new());
3979        crate::cli::bootstrap_manifest::rehydrate_manifest_registry(&runtime, &restored_registry)
3980            .expect("registry rehydrates without manifest file");
3981        assert!(restored_registry.get_active("managed-deny-drop").is_some());
3982        assert!(restored_registry.get_active("red.ai").is_some());
3983
3984        let fresh = Arc::new(AuthStore::new(crate::auth::AuthConfig::default()));
3985        apply_preset(&runtime, &fresh).expect("re-run must not need manifest file");
3986        assert!(fresh.needs_bootstrap());
3987
3988        clear_preset_env();
3989    }
3990
3991    #[test]
3992    fn production_preset_refuses_to_start_without_password() {
3993        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3994        clear_preset_env();
3995        // SAFETY: env serialised by `no_auth_env_lock`.
3996        unsafe {
3997            std::env::set_var(PRESET_ENV, PRESET_PRODUCTION);
3998            std::env::set_var("REDDB_USERNAME", "ops");
3999        }
4000
4001        let (runtime, auth_store) = fresh_runtime_and_store();
4002        let err = apply_preset(&runtime, &auth_store).expect_err("must reject missing password");
4003        assert!(
4004            err.contains("REDDB_PASSWORD"),
4005            "error must name the missing env: {err}"
4006        );
4007
4008        // Nothing was persisted; nothing was created.
4009        assert!(auth_store.needs_bootstrap());
4010        assert!(runtime
4011            .db()
4012            .store()
4013            .get_config(BOOTSTRAP_COMPLETED_KEY)
4014            .is_none());
4015
4016        clear_preset_env();
4017    }
4018
4019    #[test]
4020    fn re_running_production_after_first_boot_is_a_silent_skip() {
4021        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
4022        clear_preset_env();
4023        // SAFETY: env serialised by `no_auth_env_lock`.
4024        unsafe {
4025            std::env::set_var(PRESET_ENV, PRESET_PRODUCTION);
4026            std::env::set_var("REDDB_USERNAME", "ops");
4027            std::env::set_var("REDDB_PASSWORD", "hunter2");
4028        }
4029
4030        let (runtime, auth_store) = fresh_runtime_and_store();
4031        apply_preset(&runtime, &auth_store).expect("first apply");
4032        assert_eq!(auth_store.list_users().len(), 1);
4033
4034        // Second invocation: same runtime/store, same env. Must be a
4035        // no-op — no error, no duplicate admin, no duplicate policy.
4036        // We do NOT reuse `auth_store` because production sealed it; the
4037        // idempotency hinge is the persisted config key, not the auth
4038        // store's in-memory seal. Build a fresh `AuthStore` as a restart
4039        // would and confirm `apply_preset` is a silent skip.
4040        let fresh = Arc::new(AuthStore::new(crate::auth::AuthConfig::default()));
4041        apply_preset(&runtime, &fresh).expect("re-run is silent-skip");
4042        assert!(
4043            fresh.needs_bootstrap(),
4044            "re-run must not create a second admin"
4045        );
4046        assert!(
4047            fresh.get_policy(FIRST_ADMIN_ALLOW_ALL_POLICY).is_none(),
4048            "re-run must not re-install the allow-all policy on the fresh store"
4049        );
4050
4051        clear_preset_env();
4052    }
4053
4054    #[test]
4055    fn unrecognised_preset_value_is_rejected() {
4056        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
4057        clear_preset_env();
4058        // SAFETY: env serialised by `no_auth_env_lock`.
4059        unsafe {
4060            std::env::set_var(PRESET_ENV, "weird");
4061        }
4062
4063        let (runtime, auth_store) = fresh_runtime_and_store();
4064        let err = apply_preset(&runtime, &auth_store).expect_err("must reject unknown preset");
4065        assert!(err.contains("weird"), "error must echo the value: {err}");
4066        assert!(auth_store.needs_bootstrap());
4067
4068        clear_preset_env();
4069    }
4070
4071    #[test]
4072    fn no_auth_short_circuits_preset_entirely() {
4073        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
4074        clear_preset_env();
4075        // SAFETY: env serialised by `no_auth_env_lock`. Production creds
4076        // are set but `--no-auth` must win — no admin, no bootstrap state.
4077        unsafe {
4078            std::env::set_var(PRESET_ENV, PRESET_PRODUCTION);
4079            std::env::set_var("REDDB_USERNAME", "ops");
4080            std::env::set_var("REDDB_PASSWORD", "hunter2");
4081        }
4082
4083        let options = no_auth_test_config(true)
4084            .to_db_options()
4085            .expect("to_db_options");
4086        assert!(no_auth_active(&options));
4087
4088        // Mirror `build_runtime_with_telemetry`: when no_auth_active,
4089        // `apply_preset` is never called.
4090        let (runtime, auth_store) = fresh_runtime_and_store();
4091        if !no_auth_active(&options) {
4092            apply_preset(&runtime, &auth_store).expect("would apply preset");
4093        }
4094
4095        assert!(
4096            auth_store.needs_bootstrap(),
4097            "--no-auth must prevent any admin creation"
4098        );
4099        assert!(
4100            runtime
4101                .db()
4102                .store()
4103                .get_config(BOOTSTRAP_COMPLETED_KEY)
4104                .is_none(),
4105            "--no-auth must skip bootstrap-state persistence"
4106        );
4107
4108        clear_preset_env();
4109    }
4110
4111    #[test]
4112    fn implicit_bind_collision_degrades() {
4113        let held = TcpListener::bind("127.0.0.1:0").expect("hold test port");
4114        let addr = held.local_addr().expect("held addr").to_string();
4115        let mut readiness = TransportReadiness::default();
4116
4117        let listener =
4118            bind_listener_for_startup(&mut readiness, "http", &addr, false).expect("nonfatal");
4119
4120        assert!(listener.is_none());
4121        assert_eq!(readiness.active.len(), 0);
4122        assert_eq!(readiness.failed.len(), 1);
4123        assert!(!readiness.failed[0].explicit);
4124        assert_eq!(readiness.failed[0].bind_addr, addr);
4125    }
4126}