feat: strategy knobs in the database — config_overrides table, admin-managed

Owner preference: the bulk of TRADING_*/TRIGGER_* logic switches should live
in the DB, not the compose file (keys + connection info stay in compose —
the DB address cannot live inside the DB, and secrets don't belong in a
table the stack can SELECT). Precedence at boot: compose env < overrides.env
< config_overrides table; every entrypoint already routes through
env_overrides.load(), which now applies the table last. Admin /api/config
and profile-apply write the table (secrets whitelist unchanged); the config
page shows source=database. DB down = silent fall-through to file/compose,
boot never blocks on config. One-shot migration:
  python -m runtime.env_overrides seed-from-env
run in any stack container to copy its current effective knobs into the
table, after which the compose lines are dead weight strippable at the next
redeploy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
momentum devandClaude Fable 5 committed 2026-07-22 20:31:16 -05:00
1 parent 108f61a9bc
commit 495a3fa5d6
3 files changed
+148 -10

No files matched your search

+16 -7
View File
@@ -123,8 +123,9 @@ def _safe_overrides() -> dict:
"""Overrides for API responses with secret VALUES redacted — a /api/mode live
switch writes ALPACA_LIVE_* into overrides.env; without this they'd echo back
in every /api/status and /api/config response."""
merged = {**env_overrides.read_overrides(), **env_overrides.read_db_overrides()}
return {k: ("***" if any(m in k for m in SECRET_MARKERS) else v)
for k, v in env_overrides.read_overrides().items()}
for k, v in merged.items()}
def _editable(key: str) -> bool:
@@ -169,8 +170,10 @@ def effective_config() -> dict:
both surface as ``compose``); a documented .env.example key that is absent
surfaces as ``missing`` (drift). Secret values are masked."""
overrides = env_overrides.read_overrides()
db_overrides = env_overrides.read_db_overrides()
example = _env_example_keys()
keys = set(example) | set(overrides) | {k for k in os.environ if _editable(k)}
keys = (set(example) | set(overrides) | set(db_overrides)
| {k for k in os.environ if _editable(k)})
out: dict[str, dict] = {}
for key in sorted(keys):
value = os.environ.get(key)
@@ -179,7 +182,8 @@ def effective_config() -> dict:
"source": "missing" if key in example else "unset",
"drift": key in example}
continue
source = "overrides.env" if key in overrides else "compose"
source = ("database" if key in db_overrides
else "overrides.env" if key in overrides else "compose")
out[key] = {"value_masked": _mask_value(key, str(value)), "source": source}
return out
@@ -350,9 +354,14 @@ class Handler(BaseHTTPRequestHandler):
bad = [k for k in list(updates) + list(deletes) if not _editable(k)]
if bad:
return self._send(400, {"error": f"not editable: {bad}"})
merged = env_overrides.write_overrides(
{**{k: str(v) for k, v in updates.items()},
**{k: None for k in deletes}})
# knobs go to the DATABASE (owner-facing store; whitelist above
# already refused secrets). overrides.env keeps secrets/bootstrap.
try:
merged = env_overrides.write_db_overrides(
{**{k: str(v) for k, v in updates.items()},
**{k: None for k in deletes}})
except Exception as exc: # noqa: BLE001
return self._send(502, {"error": f"db write failed: {exc}"})
return self._send(200, {"ok": True, "overrides": merged,
"note": "restart affected services to apply"})
@@ -362,7 +371,7 @@ class Handler(BaseHTTPRequestHandler):
if not prof:
return self._send(404, {"error": f"no profile {name!r}"})
knobs = {k: str(v) for k, v in prof.items() if not k.startswith("_")}
merged = env_overrides.write_overrides(knobs)
merged = env_overrides.write_db_overrides(knobs)
return self._send(200, {"ok": True, "applied": name, "overrides": merged,
"note": "restart the loop to apply"})