fix(buzz): missing CLI binary degrades platform instead of killing gateway (OOF-208) - #88310
fix(buzz): missing CLI binary degrades platform instead of killing gateway (OOF-208)#88310shannonsands wants to merge 2 commits into
Conversation
…teway (OOF-208)
Five hosted instances reported gateway_state=startup_failed with exit
reason 'buzz: buzz CLI binary not found' — the entire gateway hard-exited
(exit 78) because one optional platform's binary was absent from the
hosted image.
Two root causes, both fixed:
A. Fatal-vs-degraded classification
- buzz check_requirements() now gates on the CLI binary (in addition
to relay URL + private key). A configured-but-binary-less install
makes create_adapter() return None, so the gateway boots degraded
(NousResearch#5196) exactly like Photon when node/npm are missing, instead of
constructing the adapter and dying in connect() with a
non-retryable cli_missing fatal. Warning is latched once per
process since check_fn runs on hot status-poll paths.
- connect()'s cli_missing fatal is now retryable=True as
defense-in-depth: a missing binary is install-state, not a config
contradiction — the reconnect watcher recovers the platform if the
binary appears while the gateway is up.
- gateway/run.py no-adapter branch now writes per-platform runtime
status (state=fatal, error_code=adapter_unavailable,
needs_attention=true). Previously an enabled platform whose adapter
could not be created was invisible in /api/status, so outages could
not be attributed.
B. Packaging gap
- block/buzz publishes no prebuilt CLI artifacts (releases contain
only desktop app bundles), so the binary must be built from source.
New buzz_build Docker stage (rust:1.90-slim-bookworm, digest-pinned)
builds buzz-cli --locked from a pinned commit SHA (v0.5.2 /
3e48f1b2365d) and copies the binary onto PATH in the runtime image,
where the adapter's _resolve_cli_path() finds it. Pure-rustls
workspace: no OpenSSL linkage, glibc-only, bookworm->trixie safe
(same rationale as node_source).
Tests:
- check_requirements() binary gating (missing/present/explicit
BUZZ_CLI_PATH precedence/warn-once)
- connect() missing-binary fatal is retryable
- runner surfaces adapter_unavailable in runtime status while
gateway_state stays running
- Dockerfile contract test for the buzz_build stage (SHA-pinned ref,
--locked build, binary on PATH)
…ain, durable retry Review round 2 on PR NousResearch#88310 (OOF-208) flagged three issues: 1. check_requirements() only consulted BUZZ_* env vars, so a documented config.yaml-only setup (extra.cli_path / extra.credentials_file / extra.relay_url) was parked as adapter_unavailable even though BuzzAdapter(config) resolves those extras fine. The gate now reads the same YAML sources via a best-effort _config_yaml_extra() helper (top-level buzz: → platforms.buzz → gateway.platforms.buzz, env wins, fail-quiet to env-only on any read error). Also bridges extra.credentials_file → BUZZ_CREDENTIALS_FILE in _apply_yaml_config — the one documented key the hook skipped. 2. The buzz_build stage pinned the rust image digest but the official rust images are rustup-managed and the pinned Buzz checkout ships rust-toolchain.toml — rustup would silently download whatever compiler the checkout requested, making the effective toolchain undetermined by the digest pin. Bumped the builder to rust:1.95-slim-bookworm (matching the checkout's channel = "1.95.0") and pinned RUSTUP_TOOLCHAIN=1.95.0 explicitly; contract test now asserts the image version and toolchain pin agree. 3. cli_missing being retryable was correct, but the retry was not durable: the reconnect watcher EVICTED a platform from the retry queue whenever _create_adapter() returned None (check_fn failing), so an operator installing the binary while the gateway was up could never be noticed — recovery required a full restart. The watcher now applies the standard backoff and keeps re-probing (check_fn is cheap, no network), surfacing platform_state=retrying / error_code=adapter_unavailable; startup also queues unavailable-adapter platforms for retry instead of only marking them fatal. The OOF-156 needs-attention escalation still flags perma-failing platforms. Nit: reworded the Dockerfile rustls comment — the tree includes aws-lc-rs/aws-lc-sys alongside ring; the no-OpenSSL claim is now scoped to what's actually true (no OpenSSL/native-tls linkage). Tests: YAML-extras-only gate pass, env-over-YAML precedence, read-failure fallback, credentials_file bridge; watcher keeps-queued-with-backoff and end-to-end recovery once the adapter becomes creatable; Dockerfile toolchain-pin contract.
|
Pushed 1. Config-aware 2. Pinned Rust toolchain — the digest pin on the builder image didn't pin the compiler: the official rust images are rustup-managed and the pinned Buzz checkout ships 3. Durable retry for missing binary — Nit also taken: reworded the Dockerfile rustls comment — the dep tree includes Validation: 63 targeted tests pass (buzz adapter, reconnect watcher, Dockerfile contracts — 8 new), broader |
Problem (OOF-208)
Five hosted Hermes Cloud instances fail gateway startup with a missing "Buzz CLI binary" (fingerprint
hermes-cloud:prod:gateway-runtime-missing-buzz-cli). Two independent defects compound:A (systemic — one optional binary kills the whole gateway):
plugins/platforms/buzz/adapter.py::check_requirements()only checkedBUZZ_RELAY_URL+ private key, not the binary. On hosted images the registry constructed the adapter anyway →connect()hit_set_fatal_error("cli_missing", retryable=False)→ with Buzz as the only connectable platform,gateway/run.pyclassified it intostartup_nonretryable_errors→gateway_state=startup_failed, exit 78, s6 stops the restart loop. Same escalation class as NS-609/NS-541 (exit-78 on mixed startup failures, fixed in #70987) — this is the per-platform gating half.B (packaging — the binary was never in the image):
block/buzzpublishes no prebuilt CLI artifacts (releases are desktop bundles only; no crates.io crate). The Dockerfile baked Photon sidecar deps but never built/copied the buzz binary.Fix
plugins/platforms/buzz/adapter.pycheck_requirements()now gates on_resolve_cli_path()(warn-once latch — this runs on hot/api/statuspoll paths); explicitBUZZ_CLI_PATHtakes precedence with no PATH fallback;connect()'scli_missingfatal flipped toretryable=Trueas defense-in-depth against the check→connect racegateway/run.pystate=fatal,error_code=adapter_unavailable,needs_attention=true) — previously a requirements-failed platform was completely invisible in/api/statusDockerfilebuzz_buildstage: digest-pinnedrust:1.90-slim-bookworm, buildsbuzz-cli --lockedfrom a 40-char commit SHA (= tag v0.5.2), binary to/usr/local/bin/buzz. Pure-rustls workspace — no OpenSSL, glibc-only, bookworm→trixie safe (same rationale as the node_source stage). Layer-cached onBUZZ_GIT_REF; ~2 min cold-cache Rust build, amd64+arm64 cross-compile cleanFix A alone stops the outage class on old images (gateway degrades, platform parked with a visible error); Fix B actually brings Buzz up on the affected instances at next image release.
Validation
test_buzz_adapter.py30 passed (7 new: binary gating,BUZZ_CLI_PATHprecedence, warn-once, retryable connect fatal)test_runner_startup_failures.py8 passed (new: gateway staysrunningwhile parked platform surfacesadapter_unavailable)test_dockerfile_immutable_install.py6 passed (contract: buzz stage exists, SHA-pinned not tag-pinned,--locked, binary on PATH)test_runner_fatal_adapter,test_platform_registry,test_startup_no_eager_platform_install,test_startup_connect_parallel): 46 passedhadolint --config .hadolint.yaml Dockerfileexit 0cargo build --release -p buzz-cli --lockedat the pinned SHA → 12MB self-contained binary in 1m51sRebased onto current main (
fe8c9f1c92) before opening; all suites re-run post-rebase.No live instances were mutated (per ticket instruction) — affected instances recover via image upgrade, or degrade gracefully immediately once a Fix-A build ships.
Linear: OOF-208