feat(cloud): add reflex cloud whoami and reflex cloud token - #6918
feat(cloud): add reflex cloud whoami and reflex cloud token#6918Kastier1 wants to merge 5 commits into
reflex cloud whoami and reflex cloud token#6918Conversation
Inspecting which Reflex Cloud credentials a machine is actually using required reading `hosting_v1.json` by hand. Two commands make it first-class: - `reflex cloud whoami` resolves the token with the control plane and reports the account, org, tier and where the token was loaded from. It never starts a browser login and never prints the token. - `reflex cloud token --print/--set/--clear` reads, replaces or removes the stored token. `--set` validates before saving, so a bad token cannot silently replace a working one. `get_existing_access_token_with_source` exposes the existing config-file over-`REFLEX_ACCESS_TOKEN` precedence so both commands can report which source won, with `get_existing_access_token` delegating to it. Also isolates the hosting config in tests: `test_save_token_to_config` and `test_authenticated_token_found_but_invalid` wrote to and emptied the developer's real token file, logging them out on every test run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Greptile SummaryThe PR adds commands for inspecting the active Reflex Cloud identity and managing stored access tokens, while hardening credential-file writes and isolating CLI tests from developers’ real credentials.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains; the previously reported destructive write, false clear success, and legacy cleanup issues are addressed at the current head.
|
| Filename | Overview |
|---|---|
| packages/reflex-hosting-cli/src/reflex_cli/utils/hosting.py | Adds token-source reporting, direct stored-token inspection, and atomic configuration writes with failure cleanup. |
| packages/reflex-hosting-cli/src/reflex_cli/v2/auth.py | Implements the new identity inspection and token-management commands with validation and post-operation verification. |
| packages/reflex-hosting-cli/src/reflex_cli/v2/deployments.py | Registers the new whoami and token commands under reflex cloud. |
| tests/units/reflex_cli/conftest.py | Redirects hosting configuration paths to temporary directories for CLI unit tests. |
| tests/units/reflex_cli/utils/test_hosting.py | Adds real-filesystem coverage for token precedence, atomic-write failures, malformed configuration, and legacy cleanup. |
| tests/units/reflex_cli/v2/test_auth.py | Comprehensively covers command output, validation, persistence verification, clear outcomes, and token secrecy. |
Reviews (4): Last reviewed commit: "fix(cloud): keep `token --clear` failure..." | Re-trigger Greptile
Merging this PR will not alter performance
Comparing Footnotes
|
There was a problem hiding this comment.
All reported issues were addressed across 6 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
Review found `save_token_to_config` and `delete_token_from_config` open the config with mode "w", which truncates before the write is attempted. A failing write (disk full, I/O error) therefore destroyed the existing token and project, and both helpers swallow the exception, so the caller saw only a warning. `token --set` read back and reported the failure, but only after the damage was done. Both now serialize to a temporary file alongside the target and move it into place, so a failed write leaves the previous credentials untouched and the temporary file is cleaned up. `token --clear` now reads back too: `delete_token_from_config` swallows filesystem errors, so success was previously reported without evidence. `token --print` forces the log level to ERROR while resolving. The shared console writes everything below ERROR to stdout, so `$(reflex cloud token --print --loglevel debug)` captured two Debug lines along with the token. Errors go to stderr, so stdout now carries the token or nothing. The config-file tests were rewritten against the real (now isolated) filesystem instead of asserting mock call counts, which pinned the old non-atomic write sequence, and cover the failed-write paths. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
All reported issues were addressed across 6 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
…g it
Follow-up review caught a regression from the atomic-write change:
`_read_hosting_config` swallowed read errors and returned `{}`, so
`delete_token_from_config` replaced a malformed config with `{}`,
destroying the token and project it could not parse. On main the read and
the write shared one try block, so a parse failure aborted before the
write. Confirmed against a main worktree: main leaves the malformed file
untouched, this branch emptied it.
`_read_hosting_config` now returns `{}` only for a missing file and
propagates read and parse errors. `delete_token_from_config` lets those
reach its existing handler, so an unreadable config is left alone.
`save_token_to_config` keeps its previous fallback of starting from an
empty config, so a corrupt file cannot block re-authenticating.
`token --clear` verified removal through a lookup that treats an
unreadable config as "no token", so it could report success while the
token was still on disk. It now reads the config directly through
`stored_access_token`, which distinguishes absent from unparseable, and
fails on either kind of unconfirmed removal.
The `--print` stdout test mocked the very helper whose debug records
contaminate stdout, so it passed with or without the fix. It now writes a
real config and exercises the lookup; verified it fails without the fix.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
All reported issues were addressed across 5 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
Two paths could escape the command's error handling as tracebacks. `delete_token_from_config` removed the legacy `hosting_v0.json` outside its try block, so an unlink failure propagated out and aborted `--clear` before the readback ran. The cleanup is now best-effort like the rest of the function, and uses `missing_ok=True` to close the exists/unlink race. The legacy file holds no token the CLI reads, so failing to remove it must not fail the removal that already succeeded. `stored_access_token` indexed whatever `json.load` returned, so a config holding valid JSON that is not an object raised `AttributeError` — not one of the types `--clear` catches. `_read_hosting_config` now rejects non-object JSON as a `ValueError`, which every caller already handles, making its dict return type honest. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Why
Answering "which credentials is this machine actually using?" meant reading
hosting_v1.jsonby hand. That came up chasing a customer whose token appeared not to update, and there was no supported way to confirm or disprove it.What
reflex cloud whoami— resolves the token against the control plane and reports the identity, plus which source the token came from:get_authenticated_client), so it is safe in CI and answers "am I logged in?" without changing the answer.token_fingerprintis a truncated sha256, so two machines can be compared in a support thread without anyone pasting a secret.--jsonfor scripting,--tokento inspect a specific token.reflex cloud token --print / --set TOKEN / --clear— exactly one required.--printwrites the raw token to stdout viaclick.echo, deliberately bypassingconsole.print, which wraps at 80 columns when piped and would corrupt a long token inexport REFLEX_ACCESS_TOKEN=$(reflex cloud token --print). The log level is forced to ERROR for this path, because the shared console writes everything below ERROR to stdout; stdout now carries the token or nothing.--setvalidates before saving, then reads back to confirm the write —save_token_to_configswallows write errors, so success was previously unverifiable. A rejected token exits non-zero and leaves the existing one untouched.--clearremoves the stored token and confirms removal, distinguishing three outcomes: gone (success), still present (exit 1), config unreadable (exit 1). It notes whenREFLEX_ACCESS_TOKENis still set and will now take over.get_existing_access_token_with_sourceexposes the existing precedence (config file wins;REFLEX_ACCESS_TOKENis only a fallback) so both commands can report which source won.get_existing_access_tokendelegates to it — no behavior change.stored_access_tokenreads the config directly, ignoring the environment and propagating read errors, so callers can tell "no token stored" from "cannot tell what is stored".Bug found in review: config writes were destructive
save_token_to_configanddelete_token_from_configopen the config with mode"w", which truncates on open, beforejson.dumpruns. Any write failure therefore destroyed the stored credentials — and since neither helper reports failure to the caller, it surfaced only as a log line. Reproduced by patchingjson.dumpto raise:Both now serialize to a temporary file alongside the target and move it into place with
Path.replace, closing the handle first so Windows can rename it and cleaning up the temp file on failure. A failed write leaves the previous credentials byte-identical. This also fixesreflex loginandreflex logout, which share these helpers.A follow-up review round caught a regression in that first fix: routing reads through a helper that returned
{}on failure turned "cannot read" into "is empty", sodelete_token_from_configreplaced a malformed config with{}. Verified against amainworktree — main left the file untouched. Reads now propagate errors,deleteleaves an unreadable config alone, andsavekeeps its fallback of starting fresh so a corrupt config cannot block re-authenticating.Incidental fix: tests were destroying the developer's login
Not cosmetic, and the reason this PR touches
conftest.py:test_save_token_to_configmocksPath.existsandPath.mkdirbut notPath.open, so it overwrote the realhosting_v1.jsonwith{"access_token": "test_token"}.test_authenticated_token_found_but_invalidcalls the realdelete_token_from_config, emptying it to{}.Verified with a sentinel: before,
pytest tests/units/reflex_clireduced the file to{}; after, it is untouched. An autouse fixture now pointsReflex.DIRand bothHOSTING_JSONpaths at a tmp dir, so no reflex_cli test can reach the real file.With that isolation in place, the config-file tests were rewritten to assert real on-disk contents instead of mock call counts. The old assertions (
mocked_open.call_count == 2) pinned the exact non-atomic write sequence, so they would have blocked the fix above while proving nothing about the resulting file.Testing
tests/units/reflex_cli/v2/test_auth.py;reflex_cli/v2/auth.pyat 100% coverage. Includes: a bad--setneither saves nor deletes, a failed write preserves the previous token and leaks no temp file, an unreadable config is preserved rather than replaced,--printround-trips a 300-char token verbatim and stays clean under--loglevel debug,--cleardistinguishes all three removal outcomes,whoaminever opens a browser, and the token never appears in either output mode.--printstdout test exercises the real lookup rather than mocking it, because the contaminating debug records originate inside the helper. Confirmed it fails when the fix is reverted.ruff check/formatclean.pyright reflex testsunchanged at 5 pre-existing errors (recharts/lucide stubs, unrelated).whoami,--printunder--loglevel debug,--clear, and the usage error.Docs need no change —
docs/.../cloud_cliref.pygenerates the CLI reference from the click tree, so both commands appear automatically. Command docstrings carry noArgs:/Raises:sections, matching every other command in the package, because a click docstring is its--helptext.Deliberate choices worth a reviewer's attention
hosting.validate_tokendirectly rather thanvalidate_token_with_retries, which deletes the cached token on access-denied — a bad side effect for--setin particular. The trade-off is no retry on a transient failure.--sethas no offline escape hatch; if validation cannot reach the control plane, nothing is saved. Strict on purpose, since silently storing an unusable token is the failure mode this PR exists to expose.--tokenandREFLEX_ACCESS_TOKENremain available meanwhile.--printsuppresses diagnostics rather than routing them to stderr. The stdout-for-non-errors split is a global convention of the shared console; rerouting it belongs in its own change.reflex cloud whoami --loglevel debuggives the lookup diagnostics.saveanddeletedeliberately disagree about an unreadable config:savestarts fresh so login always works,deleterefuses to touch what it cannot read. Both are tested as policies.Follow-ups, not in this PR
server error, notaccess denied:validate_tokenmaps a 401 (raise_for_status→HTTPStatusError⊂httpx.HTTPError) toTokenValidationError, and only a JSON parse failure reachesTokenAccessDeniedError. Sincevalidate_token_with_retriesonly clears the cached token onValueError, a revoked token is never evicted fromhosting_v1.json— and keeps shadowing a validREFLEX_ACCESS_TOKENindefinitely.reflex-hosting-cliignoresREFLEX_DIR, which relocates the framework's data dir, so the token file diverges from the rest of reflex state. Fixing it moves an existing file and needs a read-fallback migration.🤖 Generated with Claude Code