feat(lite): runtime kill-switch + staged-rollout gate (M5b)

Adds a fail-open, local-only gate that decides whether the lite wallet may run,
so a post-release issue can disable it and rollout can be staged — without any
phone-home (privacy posture: no runtime network fetch; the per-install rollout
bucket is a hashed, never-transmitted local id).

- wallet/lite_rollout_policy.{h,cpp}: a pure decision core. Order — emergency env
  kill-switch (absolute) -> local override -> manifest gates (global enable /
  version floor-ceiling / blocklist / staged-rollout permille) -> fail-open allow.
  Plus a JSON manifest loader (missing/invalid -> fail-open) and FNV-1a bucketing.
- Threads the decision through LiteWalletController -> LiteWalletLifecycleService:
  new availability() reason RolloutDisabled blocks create/open/restore and surfaces
  the gate's user-facing message via the lifecycle status.
- App::rebuildLiteWallet() resolves it from: DRAGONX_LITE_KILL_SWITCH (env), the
  lite_rollout setting (auto/force_on/force_off), and a locally-cached manifest at
  <config-dir>/lite_rollout.json. install id generated once via libsodium.
- Settings: persist lite_rollout override + the install id.

A signed remote fetcher can populate the manifest cache later without touching the
policy. Unit-tested (version compare, bucketing, override/env precedence, manifest
gates, staged rollout, loader fail-open, controller integration) and runtime-verified
on Linux (env kill-switch, manifest disable, control sync). Both variants build;
full suite passes; hygiene clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-06 12:01:08 -05:00
parent ca14aaddc7
commit b3c2282b53
12 changed files with 584 additions and 8 deletions

View File

@@ -222,6 +222,13 @@ bool Settings::load(const std::string& path)
lite_servers_.push_back(preference);
}
}
if (lite.contains("rollout_override") && lite["rollout_override"].is_string()) {
const auto v = lite["rollout_override"].get<std::string>();
lite_rollout_override_ = (v == "force_on" || v == "force_off") ? v : "auto";
}
if (lite.contains("install_id") && lite["install_id"].is_string()) {
lite_install_id_ = lite["install_id"].get<std::string>();
}
}
if (j.contains("verbose_logging")) verbose_logging_ = j["verbose_logging"].get<bool>();
if (j.contains("debug_categories") && j["debug_categories"].is_array()) {
@@ -355,6 +362,8 @@ bool Settings::save(const std::string& path)
entry["enabled"] = server.enabled;
lite["servers"].push_back(entry);
}
lite["rollout_override"] = lite_rollout_override_;
lite["install_id"] = lite_install_id_;
j["lite_wallet"] = lite;
}
j["verbose_logging"] = verbose_logging_;

View File

@@ -244,6 +244,15 @@ public:
const std::vector<LiteServerPreference>& getLiteServers() const { return lite_servers_; }
void setLiteServers(const std::vector<LiteServerPreference>& servers) { lite_servers_ = servers; }
// Lite wallet rollout / kill-switch (see wallet/lite_rollout_policy.h).
// Override: "auto" (honor rollout manifest), "force_on", or "force_off".
std::string getLiteRolloutOverride() const { return lite_rollout_override_; }
void setLiteRolloutOverride(const std::string& v) { lite_rollout_override_ = v; }
// Stable, locally-generated install id used only to derive the staged-rollout bucket.
// Never transmitted; carries no PII. Generated on first use if empty.
std::string getLiteInstallId() const { return lite_install_id_; }
void setLiteInstallId(const std::string& v) { lite_install_id_ = v; }
// Verbose diagnostic logging (connection attempts, daemon state, port owner, etc.)
bool getVerboseLogging() const { return verbose_logging_; }
void setVerboseLogging(bool v) { verbose_logging_ = v; }
@@ -392,6 +401,8 @@ private:
std::string lite_chain_name_ = "main"; // SDXL backend chain id; must be main/test/regtest
std::size_t lite_random_selection_seed_ = 0;
bool lite_persist_selected_server_ = true;
std::string lite_rollout_override_ = "auto"; // auto|force_on|force_off
std::string lite_install_id_; // random local-only id; rollout-bucket source
std::vector<LiteServerPreference> lite_servers_ = {
{"https://lite.dragonx.is", "DragonX Lite", true},
{"https://lite1.dragonx.is", "DragonX Lite 1", true},