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:
1 parent
108f61a9bc
commit
495a3fa5d6
3 files changed
+148
-10
No files matched your search
+16
-7
@@ -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"})
|
||||
|
||||
|
||||
Reference in new issue
Block a user