Skip to content

fix(auth): reliable auth-check exit codes + passive validation contract (#1569)#1570

Merged
teng-lin merged 2 commits into
mainfrom
fix/1569-auth-check-text-exit
Jun 12, 2026
Merged

fix(auth): reliable auth-check exit codes + passive validation contract (#1569)#1570
teng-lin merged 2 commits into
mainfrom
fix/1569-auth-check-text-exit

Conversation

@teng-lin

@teng-lin teng-lin commented Jun 12, 2026

Copy link
Copy Markdown
Owner

Summary

Implements the full auth-verification contract requested in #1569 for unattended automation (systemd/cron health checks). Four coordinated changes, one coherent theme: make auth verification a reliable, side-effect-aware contract.

1. auth check text mode exits non-zero on failure (the bug)

The Rich-table renderer (cli/_session_render.py) printed failed checks but always exited 0, while --json exited non-zero — so a health check using auth check --test without --json silently treated expired auth as healthy. Both modes now share one process contract: exit 0 only when every executed check passes; non-zero otherwise. Skipped (None) checks (e.g. the token fetch without --test) do not count as failures.

2. Passive validation mode (--passive + fetch_tokens_passive)

auth check --test runs fetch_tokens_with_domains, which on auth expiry runs NOTEBOOKLM_REFRESH_CMD, fires the keepalive rotation poke, can trigger inline PSIDTS RotateCookies recovery, and writes storage_state.json — none of which a passive readiness probe should do. New public notebooklm.auth.fetch_tokens_passive is strictly read-only:

  • never runs NOTEBOOKLM_REFRESH_CMD;
  • never fires the layer-1 keepalive poke (poke=False);
  • never fires inline PSIDTS recovery — it uses the no-recovery strict loader, so a missing/expired PSIDTS surfaces as a ValueError instead of a network POST + disk write;
  • never persists cookies.

auth check --test --passive routes the token probe through it. The transport-neutral run_auth_check(AuthCheckPlan(..., passive=True)) exposes the same probe to the MCP/HTTP adapters (the Python-API ask in the issue).

3. auth refresh --verify

A successful refresh command does not prove the resulting cookies authenticate (they may still redirect to sign-in). --verify runs the passive probe after refresh and exits non-zero if it fails — especially valuable with --browser-cookies, which rewrites the jar but does not otherwise verify it.

4. macOS login recovery hint

The Login not detected within 5 minutes timeout now suggests notebooklm login --browser chrome to reuse an already signed-in system Chrome session (often detects immediately; sidesteps bundled-Chromium issues on macOS).

Contract

auth check (both text and --json) exits 0 iff all executed checks pass. auth check --test --passive and auth refresh --verify give automation a fail-loud, side-effect-free readiness signal.

Testing

  • Full suite green: 9177 passed, 66 skipped; mypy + ruff clean.
  • New tests assert passivity, not just happy path: no keepalive poke, no disk write, no NOTEBOOKLM_REFRESH_CMD, and no inline PSIDTS recovery (a regression test proven to fail on the non-strict loader).
  • Updated the exit-code contract tests, CLI contract baseline, and auth.__all__ public-surface guardrail.

Compatibility

Additive public API (fetch_tokens_passive); scripts/audit_public_api_compat.py exits 0 with no new allowlist entries — not an API-breaking change. Docs updated: cli-reference.md, troubleshooting.md, CHANGELOG.md.

Closes #1569.

🤖 Generated with Claude Code

Review in cubic

Summary by CodeRabbit

  • New Features

    • Added --passive to auth check for read-only credential validation and --verify to auth refresh to confirm cookies after refresh
  • Bug Fixes

    • auth check text mode now exits non‑zero on failures (matches JSON behavior)
    • Improved macOS login timeout guidance suggesting --browser chrome
  • Documentation

    • Updated CLI reference and troubleshooting for new flags and exit semantics
  • Tests

    • Expanded tests/fixtures to cover passive/verify behaviors and exit-code contracts

…act (#1569)

Implements the full auth-verification contract from #1569 for unattended
automation (systemd/cron health checks).

1. `auth check` text mode now exits non-zero on any failed executed check,
   matching `--json`. The Rich-table renderer previously always exited 0, so a
   health check using `auth check --test` (without `--json`) silently treated
   expired auth as healthy. Both modes now share one process contract; skipped
   (`None`) checks do not count as failures.

2. Passive validation: new public `fetch_tokens_passive` does a strictly
   read-only token fetch — no `NOTEBOOKLM_REFRESH_CMD`, no keepalive poke, no
   inline PSIDTS `RotateCookies` recovery (it uses the no-recovery strict
   loader, so a missing/expired PSIDTS surfaces as a `ValueError` instead of a
   network POST + disk write), and no cookie persistence. `auth check --test
   --passive` routes through it; the transport-neutral
   `run_auth_check(AuthCheckPlan(passive=True))` exposes it to MCP/HTTP.

3. `auth refresh --verify` confirms a token fetch actually works after refresh
   (passive probe), exiting non-zero if the post-refresh cookies still fail —
   especially valuable with `--browser-cookies`, which rewrites the jar but
   does not otherwise verify it.

4. macOS login: the "Login not detected within 5 minutes" timeout now hints at
   `notebooklm login --browser chrome` to reuse an already signed-in system
   Chrome session.

Additive public API (compat audit clean, no allowlist entries). Docs
(cli-reference, troubleshooting, CHANGELOG) and the CLI contract baseline
updated. Full suite green (9177 passed); mypy + ruff clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@teng-lin

Copy link
Copy Markdown
Owner Author

@claude review

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cef60443-7dec-464f-80d8-b134636bf5c6

📥 Commits

Reviewing files that changed from the base of the PR and between 7ddfc96 and 11e2172.

📒 Files selected for processing (3)
  • tests/unit/cli/test_auth_subcommands.py
  • tests/unit/cli/test_session_characterization.py
  • tests/unit/test_auth_refresh.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/unit/cli/test_session_characterization.py

📝 Walkthrough

Walkthrough

This PR adds a read-only passive token-fetch API and CLI flag for auth readiness probes, fixes text-mode auth check exit codes to match JSON behavior, implements post-refresh verification for auth refresh, and improves macOS browser-login timeout guidance.

Changes

Passive authentication and health-check validation

Layer / File(s) Summary
Passive token fetch implementation
src/notebooklm/_auth/refresh.py, src/notebooklm/auth.py
fetch_tokens_passive() public API loads cookies strictly, validates tokens without keepalive rotation (poke=False), and never runs refresh subprocesses or writes to disk.
Auth check --passive integration
src/notebooklm/_app/auth_check.py, src/notebooklm/cli/services/auth_diagnostics.py, src/notebooklm/cli/session_cmd.py
AuthCheckPlan gains a passive flag; auth check adds --passive (no-op without --test, emits warning) and routes token validation through fetch_tokens_passive when requested.
Auth refresh --verify post-refresh validation
src/notebooklm/cli/session_cmd.py
New _verify_token_fetch_after_refresh() runs a passive probe after refresh; auth refresh --verify exits non-zero if post-refresh cookies still fail authentication.
Auth check text-mode exit code fix
src/notebooklm/cli/_session_render.py
Text-mode rendering now exits non-zero when any executed check fails, matching --json behavior.
Passive token-fetch test coverage
tests/unit/test_auth_refresh.py
New tests validate token extraction, absence of keepalive poke, no disk writes on rotated cookies, no inline PSIDTS recovery, and no refresh subprocess execution in passive mode.
Auth check failure and passive-mode tests
tests/unit/cli/test_auth_subcommands.py
Tests updated to expect non-zero exit on failed text-mode checks and to cover --test --passive routing, JSON failure structure, and non-test warning no-op.
Auth refresh --verify test coverage
tests/unit/cli/test_auth_subcommands.py
Tests cover verification success (exit 0), post-refresh passive failure (exit 1, clean), and --verify gating for browser-cookies rewrite flow.

User-facing documentation and error guidance

Layer / File(s) Summary
CLI reference documentation
docs/cli-reference.md
Documents auth check --passive (read-only validation with --test), adds example, clarifies exit-code semantics, and documents auth refresh --verify.
Browser-login error messaging and troubleshooting
src/notebooklm/_auth/browser_capture.py, docs/troubleshooting.md
Extended "Login not detected within 5 minutes" message to suggest notebooklm login --browser chrome and added a troubleshooting note for macOS bundled-Chromium timeouts.
Changelog documentation
CHANGELOG.md
Release notes for passive auth validation, auth refresh --verify, macOS login hint, and text-mode exit-code fix.
Documentation test updates
tests/unit/cli/test_playwright_login_render_contract.py
Login-timeout contract test updated to expect the new macOS recovery hint in output.

Test infrastructure and guardrail updates

Layer / File(s) Summary
Public API surface guardrails
tests/_guardrails/test_public_surface.py
Added fetch_tokens_passive to EXPECTED_AUTH_ALL snapshot for notebooklm.auth.
CLI contract baseline
tests/fixtures/cli_contract_baseline.json
Added boolean --passive to auth check and --verify to auth refresh with help text.
Characterization test exit-code alignment
tests/unit/cli/test_session_characterization.py
Characterization tests updated to require non-zero exit for auth check on missing/unreadable storage and to ensure clean SystemExit for OSError during storage reads.

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

"A rabbit tests with careful paws and cheer,
Passive fetches run with no state to smear,
Refresh then verify, exit codes align,
macOS hints to save you precious time, huzzah! 🐇"

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately summarizes the main changes: fixing auth-check exit codes to be reliable and adding a passive validation contract, directly addressing issue #1569.
Linked Issues check ✅ Passed All four objectives from issue #1569 are fully addressed: text-mode exit codes now match JSON behavior [multiple files], passive validation API and CLI implemented [fetch_tokens_passive, --passive flag], post-refresh verify option added [auth refresh --verify], and macOS login recovery guidance improved [browser_capture.py].
Out of Scope Changes check ✅ Passed All code changes are directly scoped to issue #1569 requirements: auth verification contracts, passive validation, post-refresh verification, and login recovery guidance. Documentation and test updates align with the core objectives.
Docstring Coverage ✅ Passed Docstring coverage is 87.50% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/1569-auth-check-text-exit

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@claude

claude Bot commented Jun 12, 2026

Copy link
Copy Markdown

Claude finished @teng-lin's task in 2m 39s —— View job


PR Review

  • Gather context and understand the PR
  • Review core auth changes (_app/auth_check.py, cli/session_cmd.py)
  • Review passive validation implementation (_auth/refresh.py, auth.py)
  • Review CLI rendering fix (cli/_session_render.py)
  • Review tests for correctness and coverage
  • Review docs and changelog
  • Post final review

Summary

This is a well-structured, coherent PR that fixes a real reliability gap for unattended automation. The four changes are tightly coordinated around a single theme (auth verification as a reliable, side-effect-aware contract), and the implementation is clean. Below are my findings across correctness, design, and edge cases.


Core Bug Fix: Exit Code in Text Mode

cli/_session_render.py:291-292 — The fix is correct and minimal:

if not all_passed:
    exit_with_code(1)

The fix correctly re-uses AuthCheckResult.all_passed, which already excludes skipped (None) checks from the failure count (all(v is True for v in self.checks.values() if v is not None)). Text mode now shares the exact same exit contract as --json mode. ✓


fetch_tokens_passive Implementation

_auth/refresh.py:870-923 — The passivity guarantees are well-implemented via three distinct mechanisms:

  1. No PSIDTS recovery: Uses _build_httpx_cookies_from_storage_strict(path) instead of build_httpx_cookies_from_storage(path). ✓
  2. No keepalive poke: Passes poke=False to _fetch_tokens_with_jar. ✓
  3. No disk write: Omits the save_cookies_to_storage call entirely. ✓
  4. No refresh cmd: Not calling _fetch_tokens_with_refresh at all. ✓

One observation worth flagging:

_auth/refresh.py:913 — The _fetch_tokens_with_jar still calls _replace_cookie_jar(cookie_jar, client.cookies) at line 760, which mutates the local cookie_jar object in memory (absorbing any Set-Cookie headers from redirects). Since the jar is a local variable in fetch_tokens_passive and never passed to save_cookies_to_storage, this in-memory mutation is harmless — the rotated cookies are discarded when the function returns. This is correct as-is, but the docstring says it "never fires the inline RotateCookies PSIDTS recovery" — it could be worth noting that redirect-received cookies are still received in memory (just not persisted). A minor docstring clarification rather than a code issue.


--passive Without --test Handling

cli/session_cmd.py:685-692 — The warning is emitted to stderr and the command proceeds normally (all local checks pass → exit 0). This is the right UX: don't break the common pattern of notebooklm auth check --passive in a shell wrapper where someone may have a habit of always passing --passive. ✓

One minor nit: the warning line goes to stderr via click.echo(..., err=True). In the test for this case (test_auth_check_passive_without_test_warns_no_effect), the assertion is:

assert "no effect without --test" in result.output

Click's CliRunner by default mixes stderr into result.output. If this test was ever run with mix_stderr=False, the assertion would fail because the warning goes to stderr. The current test works with the default runner but this is worth noting for future test authors. Not a bug but a potential fragility. Fix this →


auth refresh --verify Implementation

cli/session_cmd.py:119-139_verify_token_fetch_after_refresh is a clean synchronous wrapper. One potential question: it calls run_async(fetch_tokens_passive(storage_path, profile))run_async is the synchronous event-loop driver, and fetch_tokens_passive is the async function. The call signature matches fetch_tokens_passive(path, profile). ✓

The --verify flag is correctly gated only after the refresh path completes:

if verify:
    _verify_token_fetch_after_refresh(storage_path, profile, quiet=quiet)

This runs regardless of whether --browser-cookies or the default keepalive path was taken, which is the intended behavior. ✓

The error message on failure is clear:

Error: refresh completed but the post-refresh token fetch failed: {exc}

This is helpful because it distinguishes "refresh itself failed" (would exit earlier in the flow) from "refresh succeeded but cookies still don't authenticate".


AuthCheckPlan Design

_app/auth_check.py:79 — The passive: bool = False field on a frozen=True dataclass is fine. The field's docstring says "No effect without test_fetch" which is exactly what the code does — there's no validation in __post_init__ but that's acceptable since an invalid passive=True, test_fetch=False plan just silently has no effect (the warning is the CLI's job). ✓


run_auth_check Token Fetch Path

_app/auth_check.py:205-217 — The routing is clean:

from ..auth import fetch_tokens_passive, fetch_tokens_with_domains
fetch = fetch_tokens_passive if plan.passive else fetch_tokens_with_domains
token_path = None if plan.has_env_auth else plan.storage_path
csrf, session_id = await fetch(token_path, plan.profile)

When has_env_auth=True, token_path=None is passed to fetch_tokens_passive. This mirrors the same None-path logic in both fetch_tokens_with_domains and fetch_tokens_passive (both guard if path is None and (profile is not None or "NOTEBOOKLM_AUTH_JSON" not in os.environ)), so None is the correct signal for "read from env var". ✓


Testing Quality

The test coverage for the new behavior is excellent:

  • Passivity tests (TestFetchTokensPassive): Four tests explicitly assert the absence of side effects — no poke, no disk write, no PSIDTS recovery (with a regression guard that would fail if the non-strict loader were accidentally used), and no refresh cmd. This pattern of testing negative side effects (rather than just happy-path behavior) is exactly what's needed here.
  • Exit code tests: Updated to assert exit_code != 0 in all failure scenarios for text mode.
  • auth refresh --verify tests: Three tests covering success, failure, and the --browser-cookies path.
  • passive_without_test warning test: Correctly tests the no-op + warning behavior.

One missing test case: there's no test for fetch_tokens_passive when NOTEBOOKLM_AUTH_JSON is set (has_env_auth=True). The auth check --test --passive path with env-var auth routes through token_path=None, which hits the _build_httpx_cookies_from_storage_strict(None) code path. Given the strict loader is in _auth/cookies.py and follows the same None-path semantics as the non-strict loader, this is likely correct — but worth a targeted test if strict mode has any difference in its None handling. Low priority given the suite is green.


macOS Login Hint

_auth/browser_capture.py:613-620 — The new timeout message is helpful and correct:

Already signed in to Google in Chrome? Retry with notebooklm login --browser chrome ...

This is surfaced only in the interactive timeout path (5-minute wait), which is the right moment to suggest the faster alternative. ✓


Minor Observations

  1. cli/_session_render.py:350_render_auth_inspect_error is typed -> NoReturn but doesn't call exit_with_code on the text-mode path (it calls exit_with_code(1) only after the JSON branch). Looking more carefully, the JSON branch calls _output_error which itself calls exit_with_code internally. The non-JSON branch calls exit_with_code(1) explicitly. Both branches terminate — the NoReturn annotation is correct. ✓

  2. cli/session_cmd.py:112-116 — The fetch_tokens_passive forwarding wrapper is added alongside the existing fetch_tokens_with_domains wrapper, for test-patching compatibility. Consistent with the established pattern. ✓

  3. auth.pyfetch_tokens_passive is correctly added to __all__ and the public surface guardrail test (test_public_surface.py) is updated. The audit script is confirmed to exit 0 with no new allowlist entries. ✓


Overall Assessment

This is a high-quality implementation. The core fix (exit code in text mode) is minimal and correct. The passive validation contract is well-designed with meaningful test coverage for side-effect absence (not just happy-path). The --verify flag is a useful addition for automation. No blocking issues found.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces passive authentication validation for unattended monitors, adding a strictly read-only fetch_tokens_passive path that avoids mutating state, running refresh commands, or writing to disk. It also updates the auth check command to exit non-zero on failures in both text and JSON modes, introduces a --verify flag for auth refresh, and improves macOS login recovery hints. The feedback recommends enhancing diagnostic exception handling to prevent potential sensitive information disclosure and explicitly specifying encoding="utf-8" in Path.write_text() calls across tests to ensure cross-platform compatibility.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +132 to +136
except Exception as exc: # noqa: BLE001 — surface any failure as a clean exit 1
click.echo(
f"Error: refresh completed but the post-refresh token fetch failed: {exc}",
err=True,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

security-medium medium

When handling exceptions in read-only diagnostic tools or status checks, surface only the exception type name (e.g., using type(exc).__name__) instead of the raw exception message or object to avoid echoing sensitive information like raw file paths or values in the diagnostic output.

Suggested change
except Exception as exc: # noqa: BLE001 — surface any failure as a clean exit 1
click.echo(
f"Error: refresh completed but the post-refresh token fetch failed: {exc}",
err=True,
)
except Exception as exc: # noqa: BLE001 — surface any failure as a clean exit 1
click.echo(
f"Error: refresh completed but the post-refresh token fetch failed: {type(exc).__name__}",
err=True,
)
References
  1. When handling exceptions in read-only diagnostic tools or status checks, surface only the exception type name (e.g., using 'type(exc).name') instead of the raw exception message or object to avoid echoing sensitive information like raw file paths or values in the diagnostic output.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Keeping as-is — this would contradict the established convention in this module. test_auth_refresh_failure_does_not_print_exception_class documents that auth refresh deliberately surfaces the exception message (str(exc)) and drops type(exc).__name__ (the class name was the "implementation-detail leakage" that was removed). This handler already prints str(exc), not the class name, so it's consistent with that policy.

Showing the message is also the whole point of --verify for unattended use: the operator needs the actual failure reason (expired cookies vs. network error vs. missing file) in cron logs — collapsing it to a type name would gut the diagnostic value. The token-fetch redirect message is already URL-redacted via _safe_url, and storage paths aren't secret in this codebase (they're printed by auth check / auth refresh themselves).

Comment thread tests/unit/test_auth_refresh.py
Comment thread tests/unit/test_auth_refresh.py
Comment thread tests/unit/test_auth_refresh.py Outdated
Comment thread tests/unit/cli/test_auth_subcommands.py Outdated
Comment thread tests/unit/cli/test_auth_subcommands.py Outdated
Comment thread tests/unit/cli/test_auth_subcommands.py Outdated
Comment thread tests/unit/cli/test_session_characterization.py Outdated
… passive coverage

- Add explicit encoding="utf-8" to write_text/read_text in the new auth tests
  (gemini-code-assist: Windows cp1252 defensiveness).
- Add test_auth_check_passive_with_env_auth_passes_none_path: covers the
  passive probe under NOTEBOOKLM_AUTH_JSON (token_path=None), the env-auth gap
  noted by the claude review bot.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@teng-lin

Copy link
Copy Markdown
Owner Author

Thanks for the thorough reviews. Addressed in 11e2172:

  • @claude noted the missing env-auth coverage for the passive path — added test_auth_check_passive_with_env_auth_passes_none_path asserting fetch_tokens_passive is invoked with token_path=None under NOTEBOOKLM_AUTH_JSON.
  • @gemini-code-assist encoding="utf-8" suggestions applied to the new test writes; replied inline on the exception-message thread (kept as-is, with rationale re: the module's existing message-not-classname convention).

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Acknowledged. The additional test coverage for the environment-auth path and the consistent encoding usage are noted.

@teng-lin teng-lin merged commit 4eddc9a into main Jun 12, 2026
42 checks passed
@teng-lin teng-lin deleted the fix/1569-auth-check-text-exit branch June 12, 2026 19:12
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.

auth check --test text mode exits 0 on token-fetch failure; need passive validation contract

2 participants