Coverage for node / src / stigmem_node / settings.py: 95%

160 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-06-18 05:34 +0000

1from datetime import datetime 

2from typing import Annotated 

3 

4from pydantic import field_validator, model_validator 

5from pydantic_settings import BaseSettings, NoDecode, SettingsConfigDict 

6 

7 

8class Settings(BaseSettings): 

9 model_config = SettingsConfigDict( 

10 env_prefix="STIGMEM_", 

11 env_file=".env", 

12 env_file_encoding="utf-8", 

13 extra="ignore", 

14 ) 

15 

16 db_path: str = "stigmem.db" 

17 host: str = "0.0.0.0" # noqa: S104 # nosec B104 — overridable via STIGMEM_HOST 

18 port: int = 8765 

19 # Optional stable node identity for deterministic local/CI federation fixtures. 

20 # When unset, the node creates and persists a stigmem:node:<uuid> identity. 

21 node_id: str = "" 

22 node_url: str = "http://localhost:8765" 

23 # The node's own org identity URI (Phase 2a). Empty → derived from node_url. 

24 entity_uri: str = "" 

25 log_level: str = "info" 

26 cors_allowed_origins: Annotated[list[str], NoDecode] = [] 

27 cors_allowed_origin_regex: str | None = None 

28 cors_allow_credentials: bool = True 

29 cors_dev_localhost: bool = False 

30 

31 # When True (default), every request must carry a valid Bearer token. 

32 # Set to False only for local development / single-operator installs. 

33 auth_required: bool = True 

34 # /metrics (Prometheus) leaks per-principal entity_uri/tenant/peer labels and 

35 # enables unbounded-cardinality DoS, so it requires the admin capability by 

36 # default (M11 / F-AVAIL-1). Set False only where /metrics is otherwise 

37 # access-controlled, e.g. behind a private scrape interface or a sidecar proxy. 

38 metrics_require_auth: bool = True 

39 # Static API-key lifecycle controls. 0 disables max-age enforcement. 

40 api_key_max_age_days: int = 90 

41 api_key_expiring_soon_days: int = 30 

42 legacy_sha256_accept_until: datetime | None = None 

43 

44 @field_validator("cors_allowed_origins", mode="before") 

45 @classmethod 

46 def _parse_cors_allowed_origins(cls, v: object) -> list[str]: 

47 if v is None or v == "": 47 ↛ 48line 47 didn't jump to line 48 because the condition on line 47 was never true

48 return [] 

49 if isinstance(v, str): 

50 return [origin.strip() for origin in v.split(",") if origin.strip()] 

51 if isinstance(v, list): 51 ↛ 53line 51 didn't jump to line 53 because the condition on line 51 was always true

52 return [str(origin).strip() for origin in v if str(origin).strip()] 

53 return v # type: ignore[return-value] 

54 

55 # Federation — Phase 3 (spec §6) 

56 federation_enabled: bool = False 

57 # Base64url Ed25519 keypair. If both are empty, auto-generated and stored in node_meta. 

58 federation_pubkey: str = "" 

59 federation_privkey: str = "" 

60 # Pull replication interval in seconds (spec §6.3); advisory pull_interval_s 

61 # from peer overrides this. 

62 federation_pull_interval_s: int = 30 

63 federation_push_enabled: bool = False 

64 # Explicit dev/test escape hatch for federation without mTLS. Production 

65 # federation should leave this false and configure STIGMEM_TLS_* instead. 

66 federation_insecure: bool = False 

67 # Additional acknowledgement for local Docker/dev networks whose service DNS 

68 # names are not loopback. Never set in production. 

69 local_dev_allow_insecure_non_loopback: bool = False 

70 # Nonce window: how long (seconds) a nonce is kept to detect replays 

71 # (spec §6.6, default 5 min). 

72 federation_nonce_window_s: int = 300 

73 # Clock-skew leeway for peer-token exp/iat/nbf claim checks. 

74 peer_token_leeway_s: int = 30 

75 # Maximum accepted remote HLC skew for federated fact ingest. Future skew is 

76 # strict by default because it can advance local logical time; past skew is a 

77 # wider archival bound and may be set to 0 for one-off historical backfills. 

78 federation_hlc_max_future_skew_s: int = 300 

79 federation_hlc_max_past_skew_s: int = 2_592_000 

80 # Dual-trust grace window (hours) for an origin's PRIOR (retired) signing key. 

81 # After a key rotation, the retiring key is accepted alongside the current key 

82 # only while now - rotation_event.rotated_at <= this window; past it the prior 

83 # key is DROPPED (a stale/compromised retired key can no longer forge origin 

84 # signatures, direct or relayed). Generous 7-day default so legitimate 

85 # in-progress rotations are never disrupted; the current key is always accepted. 

86 federation_key_rotation_grace_hours: int = 168 

87 # Allow team-scoped facts to cross federation boundaries 

88 # (must be explicitly enabled; audit-logged). 

89 federation_allow_team: bool = False 

90 # Relay re-federates inbound facts only when ON, subject to origin 

91 # scope/tenant propagation (Phase 2c). Default OFF = today's behaviour. 

92 federation_relay_enabled: bool = False 

93 

94 # Phase 3 (DNSSEC-rooted origin key first-trust, Rev 6 §10). All 

95 # default-safe/inert: the master gate is OFF, so the first-trust ladder is 

96 # unreachable on a default node (proven by reachability + lazy-import CI 

97 # guards). Schema (migrations 053-055) lands empty; ENFORCEMENT (ladder, 

98 # epoch pins, quarantine cap/TTL) arrives in later 3b tasks. The recency 

99 # re-check floor/cap settings belong to 3c and are NOT defined here. 

100 # 

101 # Master gate for the DNSSEC first-trust ladder (operator-pin -> DNSSEC -> 

102 # operator-confirm -> fail-closed). Default OFF; only meaningful when 

103 # federation_relay_enabled is also ON. 

104 federation_dnssec_trust_enabled: bool = False 

105 # Per-origin-overridable ceiling on RRSIG age. An RRSIG older than this 

106 # falls through to operator-confirm on a never-fresh host (and hard-rejects 

107 # on a previously-fresh sticky-signed host, I4). Default 7 days (in seconds) 

108 # — generous relative to a typical zone re-sign interval so a slow-resigning 

109 # zone is not mistaken for an attack. 

110 federation_dnssec_max_rrsig_age: int = 7 * 24 * 60 * 60 

111 # Per-relay-peer cap on pending_first_trust inserts (I9 queue bound): an 

112 # untrusted relay cannot flood the operator-confirm queue. Default 100. 

113 federation_dnssec_pending_confirm_cap: int = 100 

114 # TTL (seconds) for unconfirmed pending_first_trust quarantine rows; rows 

115 # older than this are evicted (I9 queue bound). Default 7 days. 

116 federation_dnssec_pending_confirm_ttl: int = 7 * 24 * 60 * 60 

117 

118 # Phase 3 build-phase 3c — relay-path recency/revocation re-check cadence 

119 # (Rev 6 I5 / §7, NF-R5C-5). The effective re-check interval is 

120 # clamp(record_DNS_TTL, floor, cap): the origin's DNS TTL drives the cadence 

121 # (its freshness signal), the admin sets the bounds. Within the effective 

122 # interval a pinned binding is honored WITHOUT a fresh DNS re-resolution 

123 # (re-checks are cached per-origin, not per-fact). 

124 # 

125 # Anti-storm floor (seconds): a pinned binding is never re-resolved more 

126 # often than this even when the record's DNS TTL is shorter. Default 300s. 

127 federation_dnssec_recheck_floor_seconds: int = 300 

128 # Cap (seconds): a pinned binding is re-resolved at least this often even 

129 # when the record's DNS TTL is longer (admin-raisable for lower DNS load). 

130 # Default 3600s (1h). 

131 federation_dnssec_recheck_cap_seconds: int = 3600 

132 # Unreachable/suppression grace (Rev 6 I5): when a relay-path re-check gets 

133 # NO validatable answer (transport SERVFAIL/timeout, UNVALIDATABLE, BOGUS, 

134 # INSECURE, ABSENT_AUTHENTICATED) on an already-pinned binding, the pinned 

135 # key is honored only up to min(this, ttl_multiple x record_DNS_TTL) 

136 # measured from the pin's last_validated_at, then relayed facts FAIL CLOSED 

137 # (unreachable, NEVER treated as a positive revocation). Default 24h cap. 

138 federation_dnssec_unreachable_grace_seconds: int = 86400 

139 # Multiple of the record's DNS TTL that bounds the unreachable grace from 

140 # below (the grace is the MIN of the absolute cap above and this multiple of 

141 # the TTL). Default 4. 

142 federation_dnssec_unreachable_ttl_multiple: int = 4 

143 

144 # Decay sweeper (Phase 6, spec §decay) 

145 # 0 = disabled; positive = decay non-expiring facts older than N seconds 

146 # when sweep runs without explicit ttl_seconds 

147 decay_ttl_seconds: int = 0 

148 # 0.0 = disabled; positive = decay facts below this confidence when sweep 

149 # runs without explicit min_confidence 

150 decay_min_confidence: float = 0.0 

151 

152 # Track C / C1: require Ed25519 attestation on all fact assertions. 

153 # When True, POST /v1/facts must include a valid attestation token. 

154 # Defaults to False for backward compatibility. 

155 attestation_required: bool = False 

156 

157 # OIDC bridge (Track B / B3): human identity → scoped API keys. 

158 # Set oidc_enabled=true and configure the remaining fields to activate. 

159 oidc_enabled: bool = False 

160 # IdP issuer URL; discovery doc fetched from {issuer_url}/.well-known/openid-configuration 

161 oidc_issuer_url: str = "" 

162 # client_id expected in the id_token's "aud" claim 

163 oidc_audience: str = "" 

164 # lifetime of issued API keys in hours (default 8 h working-day session) 

165 oidc_token_ttl_hours: int = 8 

166 # Accepted OIDC id_token signing algorithms. Operators can narrow this list. 

167 oidc_id_token_algorithms: list[str] = ["RS256", "ES256", "PS256", "EdDSA"] 

168 # comma-separated list of allowed email domains; empty = allow any 

169 oidc_allowed_domains: str = "" 

170 

171 # Async job threshold (spec §14.5 / §15.4): scopes with more facts than this 

172 # trigger the async 202 path. Override in tests to force async path at small scale. 

173 async_job_threshold: int = 100_000 

174 

175 # Source attestation mode (legacy compatibility field). 

176 # Source-attestation runtime behavior is gated by the experimental 

177 # stigmem-plugin-source-attestation package. Default installs keep this off. 

178 source_attestation_mode: str = "off" 

179 

180 # P-INJ-1: source ↔ identity binding, graduated to core. The `attested` flag 

181 # is always evaluated (a fact's source is attested when it matches the writing 

182 # principal). When this is true, an unattested source is REJECTED at write; 

183 # default false flags it (attested=False) but allows the write. Enforce belongs 

184 # in the hardened profile — it breaks writing facts sourced from elsewhere. 

185 source_attestation_enforce: bool = False 

186 

187 # F-CONF-1: garden ACL recall filtering, graduated to core and ON by default. 

188 # Restricts tenant-wide recall/query/graph/subscription results to gardens the 

189 # caller is a member of. Facts with garden_id=NULL (single-tenant installs) are 

190 # unaffected. Set false only to intentionally accept tenant-wide garden visibility. 

191 memory_garden_acl_recall_filter: bool = True 

192 

193 # Garden-membership-derived OIDC permission ceiling, graduated to core. 

194 # Off by default: enabling it caps OIDC-issued permissions to what the user's 

195 # garden memberships grant, which downgrades non-member sessions — a hardened 

196 # least-privilege posture, not a safe universal default. Belongs in the 

197 # hardened profile. 

198 oidc_permission_ceiling: bool = False 

199 

200 # Rate limiting for hosted offering (per API key, sliding 1-hour window). 

201 # 0 = disabled. 

202 rate_limit_write_per_hour: int = 1000 

203 rate_limit_read_per_hour: int = 5000 

204 rate_limit_disabled_ack: bool = False 

205 

206 # F-AVAIL-1: ingest size caps (DoS protection). 0 disables the cap. 

207 max_request_body_bytes: int = 1_048_576 # 1 MiB hard cap on any request body 

208 max_fact_value_bytes: int = 262_144 # 256 KiB cap on a single fact value 

209 

210 # Storage backend (Phase 8 / 11). 

211 # "sqlite" (default) — local SQLite file at db_path. 

212 # "libsql" — libSQL / Turso; uses db_path as the local replica 

213 # file; set libsql_url + libsql_auth_token for 

214 # embedded-replica sync with Turso. 

215 # "postgres" — PostgreSQL; set pg_dsn to a libpq connection string. 

216 storage_backend: str = "sqlite" 

217 # Turso database endpoint, e.g. "libsql://my-db.turso.io" 

218 libsql_url: str = "" 

219 # Turso auth token (from `turso db tokens create`) 

220 libsql_auth_token: str = "" 

221 # PostgreSQL connection string, e.g. "postgresql://user:pw@localhost/stigmem" 

222 pg_dsn: str = "" 

223 # DATABASE_URL alias (Heroku / PaaS convention); also read from bare DATABASE_URL env var. 

224 database_url: str = "" 

225 # PostgreSQL schema for all tables (default: "public"). Use a unique 

226 # per-test schema to achieve row-level isolation without separate databases. 

227 pg_schema: str = "public" 

228 # Connection pool bounds for the Postgres backend. 

229 postgres_pool_min: int = 2 

230 postgres_pool_max: int = 10 

231 

232 # Encryption at rest (Phase 8). 

233 # "off" (default) — no encryption; plaintext DB (dev-friendly default). 

234 # "on" — SQLCipher for SQLite backend; native encryption for libSQL. 

235 # When "on", exactly one of at_rest_key_passphrase_env / at_rest_key_kms_uri 

236 # must be set — the node refuses to start otherwise. 

237 at_rest_encryption: str = "off" 

238 # Name of the env var whose value is the passphrase (not the passphrase itself). 

239 # e.g. STIGMEM_AT_REST_KEY_PASSPHRASE_ENV=MY_DB_PASSPHRASE 

240 at_rest_key_passphrase_env: str = "" 

241 # KMS URI for raw 32-byte key material. "env://VAR" reads a hex-encoded key 

242 # from env var VAR. Future schemes: "aws-kms://...", "gcp-kms://...". 

243 at_rest_key_kms_uri: str = "" 

244 

245 @field_validator("at_rest_encryption") 

246 @classmethod 

247 def _validate_encryption_mode(cls, v: str) -> str: 

248 if v not in ("on", "off"): 

249 raise ValueError(f"at_rest_encryption must be 'on' or 'off'; got {v!r}") 

250 return v 

251 

252 # Federation Trust — Phase 8 (spec §19) 

253 # trust_mode controls source-trust scoring and quarantine routing: 

254 # "strict" — trust is computed for all inbound facts; t < 0.2 → quarantine. 

255 # "relaxed" — trust is computed but quarantine is not auto-triggered (default). 

256 # "off" — trust not computed; source_trust is null on all facts. 

257 trust_mode: str = "relaxed" 

258 

259 # Sanitizer mode (§19.7) applied at recall time: 

260 # "block" — fact excluded, placeholder returned. 

261 # "quarantine"— fact moved to quarantine garden. 

262 # "warn" — fact returned with sanitizer_warnings (default). 

263 # "off" — no check (implied by trust_mode=off). 

264 sanitizer_mode: str = "warn" 

265 

266 # UUID of the node's designated quarantine garden. 

267 # Required in strict mode; facts below threshold are rejected with 403 if unset. 

268 quarantine_garden_id: str = "" 

269 

270 # Source-trust score weights (§19.4.2). Must sum to 1.0; deviations are not 

271 # validated at startup — set incorrectly and t will be out of [0,1] range. 

272 trust_weight_identity: float = 0.35 

273 trust_weight_peer_history: float = 0.30 

274 trust_weight_scope_authority: float = 0.25 

275 trust_weight_attestation_mode: float = 0.10 

276 

277 # Path to a newline-delimited file of extra sanitizer regex patterns (§19.7.2). 

278 sanitizer_extra_patterns_file: str = "" 

279 

280 # Path to YAML file defining operator auto-trust rules (always_trust / never_trust). 

281 trust_rules_file: str = "" 

282 

283 # Plugin signing gate (ADR-011 / PR 4-INF.3). 

284 # When true, installed entry-point plugins must pass signing verification 

285 # before registration. Set false only for local development; unsigned plugin 

286 # loading remains warning- and audit-visible. 

287 plugin_signing_required: bool = True 

288 # Required literal acknowledgement before unsigned-plugin loading is allowed. 

289 plugin_unsigned_ack: str = "" 

290 # Disable installed entry-point plugin discovery for test and smoke 

291 # environments that intentionally exercise the default no-plugin node. 

292 plugin_auto_discovery_enabled: bool = True 

293 # Comma-separated Sigstore signing identities accepted for production plugin 

294 # registration when plugin_signing_required=true. 

295 plugin_trusted_publishers: str = "" 

296 # Comma-separated signing identities accepted through explicit operator 

297 # override. Overrides remain audit-visible and are not a substitute for the 

298 # trusted-publisher allowlist. 

299 plugin_trust_override_publishers: str = "" 

300 

301 # Transparency log backend (§19.2.3): 

302 # "local" — append-only JSONL file with hash chain (default, no external deps). 

303 # "rekor" — Sigstore Rekor REST API. 

304 # "off" — no TL submission; inclusion proofs are never verified. 

305 tl_backend: str = "local" 

306 tl_local_path: str = "stigmem_tl.jsonl" 

307 tl_rekor_url: str = "https://rekor.sigstore.dev" 

308 fact_chain_checkpoint_interval: int = 1000 

309 fact_chain_checkpoint_max_age_s: int = 60 

310 fact_chain_checkpoint_retry_s: int = 60 

311 

312 # Capability token signing — spec §19.3.2 (C-SEC-1). 

313 # Base64url-encoded raw 32-byte Ed25519 seed used to sign capability tokens and 

314 # revocation events. If empty, token signing is skipped and verify_token() will 

315 # reject all tokens (dev/test nodes that don't participate in trust federation). 

316 node_private_key: str = "" 

317 

318 @field_validator("node_private_key") 

319 @classmethod 

320 def _validate_node_private_key(cls, v: str) -> str: 

321 if not v: 

322 return v 

323 import base64 

324 

325 padded = v + "=" * (-len(v) % 4) 

326 try: 

327 raw = base64.urlsafe_b64decode(padded) 

328 except Exception as exc: 

329 raise ValueError(f"node_private_key is not valid base64url: {exc}") from exc 

330 if len(raw) != 32: 330 ↛ 331line 330 didn't jump to line 331 because the condition on line 330 was never true

331 raise ValueError(f"node_private_key must decode to exactly 32 bytes; got {len(raw)}") 

332 return v 

333 

334 # ------------------------------------------------------------------------- 

335 # Embeddings — Phase 9 (spec §20 / design memo §2) 

336 # ------------------------------------------------------------------------- 

337 # Set embed_enabled=true to activate sqlite-vec integration. 

338 # When false (default), no extension is loaded and no embeddings are stored. 

339 embed_enabled: bool = False 

340 

341 # "local" — Ollama HTTP API (default); requires a running Ollama instance. 

342 # "openai" — OpenAI embeddings API; requires OPENAI_API_KEY (or the env var 

343 # named by embed_openai_api_key_env). 

344 # "stub" — deterministic test stub; no external dependencies. 

345 embed_model_provider: str = "local" 

346 

347 # Model identifier passed to the provider. 

348 # Local default: "nomic-embed-text-v1.5" (768-dim, Apache-2.0, runs on laptop). 

349 # OpenAI default: "text-embedding-3-small" (1536-dim). 

350 embed_model_id: str = "nomic-embed-text-v1.5" 

351 

352 # Output dimensionality. MUST match the model; changing this after the first 

353 # embedding requires running `stigmem embed reindex` (migration tool). 

354 embed_dimension: int = 768 

355 

356 # Ollama base URL (local provider only). 

357 embed_ollama_url: str = "http://localhost:11434" 

358 

359 # Name of the env var holding the OpenAI API key (openai provider only). 

360 embed_openai_api_key_env: str = "OPENAI_API_KEY" 

361 

362 # Facts with confidence below this threshold have their vec_facts entry 

363 # deleted during the decay sweep (design memo §2 "Decay interaction"). 

364 embed_tombstone_threshold: float = 0.1 

365 

366 # Subscription primitive (Phase 9, spec §20) 

367 # How long (seconds) the replay window extends back from now (default 24 h). 

368 subscription_replay_s: int = 86400 

369 # How often (seconds) the background sweep retries pending/failed delivery. 

370 subscription_delivery_sweep_s: int = 30 

371 # Consecutive delivery failures before the circuit breaker opens on a subscription. 

372 subscription_circuit_threshold: int = 10 

373 # How long (seconds) an event may remain in 'delivering' state before the next 

374 # ``deliver_pending`` invocation reverts it to 'pending' for redelivery. 

375 # Guards against crashed workers stranding events. Must be larger than the 

376 # worst-case webhook timeout (10 s) by a comfortable margin. 

377 subscription_claim_timeout_s: int = 300 

378 # F-AVAIL-3: cap on active subscriptions per (subscriber_identity, tenant). 

379 # 0 disables the cap. Default is generous; raise for high-fan-out operators. 

380 max_subscriptions_per_principal: int = 1000 

381 # M12 / F-AVAIL-2: terminal (delivered/failed) subscription_events older than 

382 # this are pruned by the delivery sweep so the table cannot grow unbounded. 

383 # Clamped to >= subscription_replay_s at prune time so the replay window is 

384 # never truncated. 0 disables pruning entirely. 

385 subscription_event_retention_s: int = 604800 # 7 days 

386 # GHSA-5p3m-vhh6-9236: webhook delivery_address must be https by default. 

387 # When False (default), a webhook delivery_address is required to be https at 

388 # both creation and delivery time. Set True ONLY for local/dev where http 

389 # webhooks are needed — this is the advisory's explicit, operator-controlled 

390 # opt-in for the insecure http scheme. https-only blocks plaintext exfil and 

391 # narrows the SSRF surface. 

392 webhook_allow_insecure_http: bool = False 

393 

394 @property 

395 def webhook_allowed_schemes(self) -> frozenset[str]: 

396 """Allowed schemes for webhook delivery_address (https-only by default).""" 

397 if self.webhook_allow_insecure_http: 

398 return frozenset({"https", "http"}) 

399 return frozenset({"https"}) 

400 

401 # ------------------------------------------------------------------------- 

402 # mTLS Federation Transport — Phase 12 (spec §22.1) 

403 # ------------------------------------------------------------------------- 

404 # Path to the node's PEM-encoded X.509 certificate for mTLS federation. 

405 # When tls_cert_path + tls_key_path are both set, mTLS is activated: 

406 # the uvicorn server requires client certs and the pull client presents this 

407 # cert to peers. Opt-out is only permitted for localhost deployments 

408 # (set host to "localhost" / "127.0.0.1" / "::1" and leave paths empty). 

409 tls_cert_path: str = "" 

410 # Path to the node's PEM-encoded private key corresponding to tls_cert_path. 

411 tls_key_path: str = "" 

412 # Path to a PEM CA bundle used to verify peer certificates. 

413 # Required when tls_cert_path + tls_key_path are configured. 

414 tls_ca_bundle: str = "" 

415 

416 @model_validator(mode="after") 

417 def _require_ca_bundle_for_mtls(self) -> "Settings": 

418 if self.tls_cert_path and self.tls_key_path and not self.tls_ca_bundle: 

419 raise ValueError( 

420 "STIGMEM_TLS_CA_BUNDLE is required when mTLS is enabled " 

421 "(STIGMEM_TLS_CERT_PATH + STIGMEM_TLS_KEY_PATH are set). " 

422 "Without it, peer certs fall back to the system CA store instead " 

423 "of the closed federation trust bundle (spec §22.1.2.2)." 

424 ) 

425 return self 

426 

427 @property 

428 def mtls_enabled(self) -> bool: 

429 """True when mTLS cert + key are configured (non-localhost deployments).""" 

430 return bool(self.tls_cert_path and self.tls_key_path) 

431 

432 # ------------------------------------------------------------------------- 

433 # Observability — Phase 13 (spec §23) 

434 # ------------------------------------------------------------------------- 

435 # Set otel_enabled=true to activate OpenTelemetry tracing. 

436 # Requires stigmem-node[observability] (opentelemetry-sdk + OTLP exporter). 

437 otel_enabled: bool = False 

438 

439 # Service name reported in OTel resource attributes. 

440 otel_service_name: str = "stigmem-node" 

441 

442 # OTLP collector base URL (HTTP protocol). 

443 # e.g. "http://localhost:4318" for a local OpenTelemetry Collector or Tempo. 

444 # Leave empty to disable OTLP export (spans collected locally only). 

445 otel_exporter_otlp_endpoint: str = "" 

446 

447 # ------------------------------------------------------------------------- 

448 # Time-travel / as_of — Phase 13 (spec §24.2.2) 

449 # ------------------------------------------------------------------------- 

450 # Minimum allowed as_of timestamp (ISO 8601 UTC). Queries before this floor 

451 # return 400 as_of_before_retention_floor. Empty string = no floor enforced. 

452 as_of_retention_floor: str = "" 

453 

454 

455settings = Settings()