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