Skip to content

v1.6: cloud transport fallback for BusyBarClient - #14

Merged
sumitake merged 2 commits into
mainfrom
dev/claude/cloud-transport-v1.6
Aug 4, 2026
Merged

v1.6: cloud transport fallback for BusyBarClient#14
sumitake merged 2 commits into
mainfrom
dev/claude/cloud-transport-v1.6

Conversation

@sumitake

@sumitake sumitake commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Adds an optional cloud transport (BUSY account API token) to BusyBarClient, mirroring busylib-py's single-class transport-flag design:

  • transport = "auto" (default): local-first with cloud fallback on connection failure; 60s local-recovery probe while degraded; INFO logs on degradation/recovery transitions. Forced local/cloud modes available.
  • New [device] keys: cloud_token (empty disables; per-device scoped, lives only in git-ignored config), cloud_base_url, transport. Config splat guarded by signature-derived key filtering — typos warn (key name only, never values) instead of crashing.
  • Contracts locked by tests: fallback ONLY on connection exceptions (local HTTP 500 stays ERROR, cloud untouched); cloud 409 still REJECTED; token barred from all log output; UNREACHABLE = all configured transports failed.
  • DrawResult semantics and all consumer code unchanged. Live probe checklist (forced-cloud draw, cadence headroom, base-URL resolution) documented for post-merge controller pass.

Tests: 346.

🤖 Generated with Claude Code

sumitake and others added 2 commits August 4, 2026 12:18
Adds an optional cloud relay fallback for BusyBarClient, mirroring
busylib-py's single-class transport-flag design, so an integration keeps
working when the local device (USB/Wi-Fi) becomes unreachable, as long
as the operator has configured a BUSY cloud API token.

New [device] config: cloud_token = "" (empty disables cloud fallback
entirely -- out-of-the-box behavior is byte-for-byte unchanged from
pre-v1.6), cloud_base_url (default https://api.busy.app/busybar),
transport = "auto" | "local" | "cloud". Key names were chosen to match
BusyBarClient's constructor kwargs exactly, so both integration call
sites simplify to BusyBarClient(**cfg["device"]).

In "auto" mode (the default), _request tries local first with the
existing (3, 5) timeout; on a requests.RequestException with a non-empty
cloud_token, the same request is retried against cloud_base_url with a
(5, 15) timeout and an Authorization: Bearer header. A per-client
active_transport attribute tracks which transport last succeeded; while
degraded, subsequent calls skip the local attempt entirely and go
straight to cloud until LOCAL_RETRY_SECONDS (60) have elapsed since the
last local failure, at which point local is retried first again as a
recovery probe -- cheap to do inline (no background thread) since a down
local device fails fast. DrawResult.UNREACHABLE now means both
transports failed when cloud is configured; unchanged (local-only)
semantics when it isn't. Local paths ("/api/...") map to cloud paths by
stripping the "/api" prefix and letting cloud_base_url (which already
carries "/busybar") supply the rest.

DrawResult's members, all six public BusyBarClient methods, and both
integrations' logic are unchanged -- the fallback lives entirely inside
the private _request/_try_local/_try_cloud layer.

Per the coordinator's explicit security requirement, cloud_token is
never logged at any level including DEBUG -- only transport transitions
are logged (INFO, static strings, no interpolation). Verified both by a
manual grep of every log call site in client.py and by a dedicated
caplog-based regression test.

No live cloud verification was performed this round -- the operator
hadn't provisioned a real token yet, per explicit instruction. Tests use
a literal placeholder token string only; no network call was made to any
busy.app host. README's new "Cloud transport" section documents the
token creation/rotation walkthrough and the post-merge live-probe
checklist (forced-cloud draw probe, cadence headroom check at 10s
redraws, and resolving the api.busy.app/busybar vs. busylib-py's
proxy.busy.app base-URL discrepancy) for the controller pass that runs
once a token exists.

323 -> 337 tests passing (14 net-new: 12 in test_client.py covering
fallback/no-fallback/forced-mode/request-shape/recovery-probe-timing/
token-never-logged, 2 in test_config.py for config.example.toml/DEFAULTS
parity on the new [device] keys).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ack contracts in tests

Final-gate review fixes for the v1.6 cloud transport fallback feature,
same branch (dev/claude/cloud-transport-v1.6), addressing two Important
and three Minor findings.

Important #1: BusyBarClient(**cfg["device"]) crashed with a cryptic
TypeError on any unknown/typo'd [device] key -- a regression from
pre-v1.6, where only host= was ever passed explicitly and an unrelated
key was silently ignored. Worst-timed failure mode: an operator typo'ing
cloud_token during first-time setup would hit this. Fixed with a new
busybar.config.device_kwargs(cfg) helper that filters cfg["device"] down
to BusyBarClient's actual constructor kwargs (introspected via
inspect.signature so it can't drift out of sync with the constructor)
and logs one WARNING per dropped key, restoring "ignored, not fatal"
while adding observability the pre-v1.6 code never had. Both
calendar_countdown/main.py and ci_status/main.py now construct
BusyBarClient(**device_kwargs(cfg)).

Important #2: added the single most important untested behavioral
contract of the whole feature -- a client with cloud_token configured,
where local returns HTTP 500 (a real response, no exception), must
result in DrawResult.ERROR with cloud never attempted. Locks in
"fallback triggers strictly on requests.RequestException, never on any
HTTP response the device actually returned."

Minor #1: fixed an inaccurate comment in
test_cloud_token_never_appears_in_log_output (claimed the second call
re-attempts local; it actually skips local within the recovery window)
and added time.monotonic mocking for determinism instead of relying on
real wall-clock time staying under LOCAL_RETRY_SECONDS between two
adjacent statements.

Minor #2: added cloud-409 -> REJECTED coverage for both forced-cloud and
degraded-auto transport, mirroring the existing local-409 test.

Minor #3: added a direct test for the transport ValueError guard.

337 -> 346 tests passing (9 net-new: 5 in test_config.py for
device_kwargs, 4 in test_client.py for the HTTP-500/cloud-409/ValueError
contracts). Report appended to .superpowers/sdd/display-v1.6-report.md
(gitignored); spec doc gained a "Final-gate review fixes" subsection
under the v1.6 section.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sumitake
sumitake merged commit 77a3997 into main Aug 4, 2026
4 checks passed
@sumitake
sumitake deleted the dev/claude/cloud-transport-v1.6 branch August 4, 2026 19:31

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 628e3dded9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/busybar/client.py
return requests.request(method, f"{self.cloud_base}{self._cloud_path(path)}",
timeout=self.cloud_timeout, headers=headers, **kwargs)
except requests.RequestException as exc:
log.debug("cloud unreachable: %s", exc)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid logging cloud request exceptions verbatim

When the configured cloud token contains an accidental control character (for example a pasted newline), requests raises an InvalidHeader before sending the request and the exception text includes the rejected Authorization header value. Formatting exc here means DEBUG logs during token setup can write the bearer token to disk, contradicting the new guarantee that cloud_token is never logged; log only a sanitized failure or exception type for cloud failures.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant