Files
wehub-resource-sync 09e9f3545f
Test / Code Quality (push) Has been cancelled
Test / Test (macos-latest, Python 3.10) (push) Has been cancelled
Test / Test (macos-latest, Python 3.11) (push) Has been cancelled
Test / Test (macos-latest, Python 3.12) (push) Has been cancelled
Test / Test (macos-latest, Python 3.13) (push) Has been cancelled
Test / Test (macos-latest, Python 3.14) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.10) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.11) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.12) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.13) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.14) (push) Has been cancelled
Test / Test (windows-latest, Python 3.10) (push) Has been cancelled
Test / Test (windows-latest, Python 3.11) (push) Has been cancelled
Test / Test (windows-latest, Python 3.12) (push) Has been cancelled
Test / Test (windows-latest, Python 3.13) (push) Has been cancelled
Test / Test (windows-latest, Python 3.14) (push) Has been cancelled
CodeQL / Analyze (push) Has been cancelled
dependency-audit / pip-audit (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:30:13 +08:00

122 KiB

Architecture

This document is the canonical map of notebooklm-py's current runtime shape. The historical refactor narrative (including the program that first established this layering) lives in docs/refactor-history.md.

Layered overview

            three thin, transport-specific adapters
+----------------+  +----------------+  +----------------+
| CLI    (cli/)  |  | MCP    (mcp/)  |  | REST (server/) |
| Click commands |  | FastMCP tools  |  | FastAPI routes |
+----------------+  +----------------+  +----------------+
         \                  |                  /
          \                 |                 /
           +----------------+----------------+
                            ▼
+----------------------------------------------------------+
| Application Layer  (src/notebooklm/_app/*)               |
|   Transport-neutral business logic shared by all three   |
|   adapters: id validation/resolution, plan-building,     |
|   status projection, retry/wait orchestration,           |
|   errors.classify (the single failure-category source),  |
|   diagnostics. Imports no click / rich / fastmcp /       |
|   fastapi (boundary lint-enforced; ADR-0021).            |
+----------------------------------------------------------+
                            ▼
+----------------------------------------------------------+
| Client Layer (client.py + feature APIs)                  |
|   NotebookLMClient + namespaced sub-clients:             |
|     .notebooks  .sources  .artifacts  .chat              |
|     .notes      .mind_maps .research   .settings         |
|     .sharing    .labels                                  |
+----------------------------------------------------------+
                            ▼
+----------------------------------------------------------+
| Runtime Layer (client-owned collaborators)               |
|   ClientComposed + RpcExecutor, RuntimeTransport,        |
|   ClientLifecycle, Kernel.                               |
+----------------------------------------------------------+
                            ▼
+----------------------------------------------------------+
| RPC Layer (src/notebooklm/rpc/*)                         |
|   types.py    method IDs + enums (source of truth)       |
|   encoder.py  request encoding                           |
|   decoder.py  response parsing                           |
+----------------------------------------------------------+

Three thin transport adapters fan into that one shared core; everything below _app/ is then identical regardless of which adapter drove the call — there is exactly one client runtime and one RPC stack:

Adapter Package Transport Console script Install Failures render as
CLI cli/ terminal (Click) notebooklm base exit codes + the byte-stable --json error envelope (ADR-0015)
MCP mcp/ Model Context Protocol (FastMCP) notebooklm-mcp mcp extra · experimental MCP tool error content (CODE: message)
REST server/ HTTP (FastAPI) notebooklm-server server extra · experimental HTTP status + {"error": {"category": "...", "message": "..."}}

Transport-neutral application layer (_app/)

The CLI, the MCP server (mcp/), and the REST server (server/) are each thin adapters over src/notebooklm/_app/ — transport-neutral business logic (id validation/resolution, plan-building, status projection, retry/wait orchestration, error classification, diagnostics) shared by all three front-ends. Each adapter parses its transport's inputs into typed Request/Plan/Result dataclasses, calls the neutral core (which receives the live client), and renders the typed result into its own envelope vocabulary; simple reads/mutations call the client.* namespaces directly, while multi-step flows go through the _app/ cores. The package imports no transport framework — click / rich / fastmcp / fastapi, nor the cli / server / rpc sibling packages — with the boundary lint-enforced (tests/_guardrails/test_app_boundary.py). It raises only the public notebooklm.exceptions hierarchy, with _app.errors.classify as the single neutral source of the failure-category decision each adapter projects onto its own codes (CLI exit codes, MCP error shapes, REST HTTP statuses). See ADR-0021. The per-module index and the full tree are in File map below.

Library call flows

NotebookLMClient is the composition root. It constructs the shared runtime collaborator graph, wires feature APIs to narrow runtime Protocols, and injects stateful services such as SourceUploadPipeline, NoteService, NoteBackedMindMapService, and ArtifactDownloadService. Feature modules build NotebookLM params and parse domain rows; client-owned collaborators own dispatch, transport, auth refresh, metrics, and lifecycle.

Typed batchexecute RPCs

Most public methods (client.notebooks.list(), client.sources.rename(), client.settings.get(), artifact generation, note CRUD, etc.) follow this path:

+----------------------------------------------------------------+
| CLI command / MCP tool / REST route / library call             |
+----------------------------------------------------------------+
                                 |
                                 v
+----------------------------------------------------------------+
| NotebookLMClient.<feature>.<method>()                          |
|   feature API / service builds params and chooses RPCMethod    |
+----------------------------------------------------------------+
                                 |
                                 v
+----------------------------------------------------------------+
| RpcExecutor.rpc_call(...)                 satisfies RpcCaller  |
|   - pre-open guard via Kernel.get_http_client()                |
|   - logical-RPC request id + rpc_calls_started metric          |
+----------------------------------------------------------------+
                                 |
                                 v
+----------------------------------------------------------------+
| RpcExecutor._execute_once(...)                                 |
|   - idempotency policy resolution                              |
|   - method-id resolution, request encoding, URL/body builder   |
+----------------------------------------------------------------+
                                 |
                                 v
+----------------------------------------------------------------+
| RuntimeTransport.perform_authed_post(...)                      |
|   - loop-affinity guard, auth snapshot                         |
|   - RpcRequest materialization                                 |
+----------------------------------------------------------------+
                                 |
                                 v
+----------------------------------------------------------------+
| ADR-0009 middleware chain                                      |
|   Drain -> Metrics -> Sema -> Retry -> AuthRefresh             |
|   -> ErrInj -> Tracing                                         |
+----------------------------------------------------------------+
                                 |
                                 v
+----------------------------------------------------------------+
| MiddlewareChainHost._authed_post_chain_terminal(...)           |
|   chain leaf — ADR-0014 Rule 4                                 |
+----------------------------------------------------------------+
                                 |
                                 v
+----------------------------------------------------------------+
| RuntimeTransport.terminal(...)                                 |
|   - final auth-freshness rebuild immediately before POST       |
+----------------------------------------------------------------+
                                 |
                                 v
+----------------------------------------------------------------+
| Kernel.post(...) -> _streaming_post -> httpx.AsyncClient       |
+----------------------------------------------------------------+
                                 |
                                 v  response unwinds back up
+----------------------------------------------------------------+
| RpcExecutor decodes via rpc.decode_response(...)               |
| Feature API maps decoded payload -> typed/domain result        |
+----------------------------------------------------------------+

Production wires RpcExecutor directly into each feature as its RpcCaller per ADR-0014 Rule 1; NotebookLMClient.rpc_call dispatches through the same RpcExecutor stored as NotebookLMClient._rpc_executor for the public raw-RPC escape hatch.

NotebookLMClient.rpc_call(method, params) is the public raw-RPC escape hatch. It skips feature-specific param builders and result parsers, but still enters the same RpcExecutor.rpc_call → RuntimeTransport → Kernel pipeline.

Chat ask path

ChatAPI.ask() is the major transport-sharing exception to the pure RpcExecutor shape. Streaming chat has a custom request body and chat-flavored error mapping, so the first ask POST goes through:

+----------------------------------------------------------------+
| ChatAPI.ask(...)                                               |
|   - loop_guard.assert_bound_loop()                             |
|   - source-id lookup                                           |
|   - conversation lock / cache                                  |
|   - reqid.next_reqid()                                         |
+----------------------------------------------------------------+
                                 |
                                 v
+----------------------------------------------------------------+
| chat_aware_authed_post(transport, ...)                         |
+----------------------------------------------------------------+
                                 |
                                 v
+----------------------------------------------------------------+
| RuntimeTransport.perform_authed_post(...)                      |
+----------------------------------------------------------------+
                                 |
                                 v
+----------------------------------------------------------------+
| ADR-0009 middleware chain                                      |
+----------------------------------------------------------------+
                                 |
                                 v
+----------------------------------------------------------------+
| RuntimeTransport.terminal(...) -> Kernel.post                  |
+----------------------------------------------------------------+
                                 |
                                 v  streaming response
+----------------------------------------------------------------+
| streaming chat parser + citation/reference parser              |
+----------------------------------------------------------------+

ChatAPI holds the four collaborators it needs (rpc, transport, reqid, loop_guard) directly — there is no ChatRuntime composite or broad runtime transport indirection.

For a new conversation, ChatAPI.ask() then calls GET_LAST_CONVERSATION_ID through the normal RpcExecutor path. Other chat methods such as get_conversation_turns() and delete_conversation() also use normal rpc_call.

Uploads, downloads, and polling

Some feature workflows intentionally combine RPC with non-RPC HTTP work:

Flow Runtime shape
Source file upload SourcesAPI.add_file() delegates to SourceUploadPipeline.add_file(). The pipeline opens an operation_scope, takes its own upload semaphore, registers the file source through runtime.rpc_call(ADD_SOURCE_FILE), then uses a dedicated httpx.AsyncClient and live Kernel cookies for the Scotty resumable-upload start/finalize calls. Optional wait/rename steps return to rpc_call.
Source URL/text/Drive add SourceAddService wraps URL and Drive mutating RPCs in idempotent_create(...) because those flows have stable probes. Text-source adds are intentionally non-idempotent unless the caller handles dedupe externally.
Artifact generation ArtifactsAPI delegates the generate_* / revise_slide / retry_failed kickoff paths to ArtifactGenerationService (_artifact/generation.py), which builds CREATE_ARTIFACT params (via the _artifact/payloads.py builders) and uses the normal rpc_call path; the facade keeps thin signature-preserving delegators. ArtifactPollingService owns leader/follower polling with operation_scope(...) and a feature-local PollRegistry; ArtifactsAPI registers a close-time drain hook for poll cleanup.
Artifact download ArtifactDownloadService lists/selects artifacts through RpcCaller, but media downloads use a separate streaming httpx.AsyncClient with storage cookies, trusted-host checks, and a producer/writer split. They do not go through RpcExecutor or Kernel.post.
Notes and mind maps NoteService owns note-row CRUD/classification through RpcCaller. NoteBackedMindMapService adapts those note rows for artifact-facing mind-map behavior so notes and artifacts do not import each other.

Cross-cutting policies

Three policies thread through the layers above and are easy to violate by accident. Each is pinned by an ADR.

Loop affinity (ADR-0004)

Why we need it. The client is built on httpx.AsyncClient plus a network of asyncio primitives — locks, semaphores, condition variables, queues, and a keepalive Task. Every one of those binds to the event loop on which it is first awaited. Re-using a client across loops either deadlocks (the wake-up is scheduled on a loop that will never run again) or raises a confusing RuntimeError from deep inside the primitive — both fail far away from the actual cause. The contract is the simplest mitigation that makes the failure mode visible: bind to one loop and fail loudly on the first violating call instead of hanging ten minutes later. The cost of cross-loop safety is paid once at the lifecycle layer instead of in every seam, so individual collaborators can use plain asyncio.Lock / asyncio.Semaphore without defensive re-binding logic.

The contract. One NotebookLMClient instance is bound to its open()-time event loop. Cross-loop reuse (a different asyncio.run, a different thread's loop) is unsupported and raises RuntimeError at the first authed POST. Cross-thread reuse is unsupported for the same reason — every thread has its own default loop. Cross-tenant reuse is unsupported because a live client owns per-instance chat state and auth state. ChatAPI._cache keys on conversation_id without an account_email dimension, so tenant-switching a client risks mixing local chat history if a conversation id is reused across accounts.

The contract is enforced by the free function assert_bound_loop(...) in _loop_affinity.py, which is called from every helper that captured a loop reference at open() time (transport drain, reqid counter, auth refresh, artifact polling, chat). The LoopGuard capability Protocol (assert_bound_loop()) is how feature APIs surface the same check without taking a Session dependency.

See ADR-0004 and the consumer notes in docs/python-api.md.

Idempotency (ADR-0005)

Why we need it. batchexecute runs over HTTPS, so every mutating call (create, delete, refresh, share, generate, …) is exposed to a commit-lost failure: the server commits the write, then the response is lost in transit. A naive retry on top of a commit-lost failure produces a duplicate write — a duplicate notebook, a duplicate source, an extra LLM inference, a re-sent invite email — depending on the RPC. The transport's inner retry loop is correct for read-only RPCs and dangerous for mutating ones. Before the taxonomy existed, the only mitigation was a per-call-site disable_internal_retries=True flag that didn't document why a given RPC was retry-unsafe, so the decision was easy to lose during refactors. The taxonomy makes retry safety a property of the RPC (declared once in the registry) instead of a property of the call site (re-derived every time someone touches the code).

The classification. Every active RPC is classified into one of five retry-safety profiles by the IdempotencyRegistry in _idempotency.py:

Policy Meaning Effect on the inner retry loop
UNCLASSIFIED Placeholder for hand-built test/future registries; not used by the production registry for active RPCs Silent, retries enabled (preserves pre-taxonomy behavior)
PROBE_THEN_CREATE Caller owns a probe loop; transport must not blind-retry Force-disable inner retries
IDEMPOTENT_SET_OP Replay-safe read-only, delete, rename, or set-state RPC Retries are safe; left enabled
AT_LEAST_ONCE_ACCEPTED Caller has explicitly accepted duplicate side-effect cost (emails / billing / notifications) Retries enabled; rate-limited WARN emitted so operators can see the trade-off
NON_IDEMPOTENT_NO_RETRY No dedupe key and no probe; first failure must surface Force-disable inner retries

The axis is closed. A sixth policy would need an ADR update and an executor change in lockstep — the five-policy cap is intentional so a reviewer can hold the whole taxonomy in mind during a code review.

RpcExecutor._execute_once consults the registry once per call to resolve the effective disable_internal_retries. The caller's explicit disable_internal_retries=True always wins over the registry default.

Every PROBE_THEN_CREATE entry must carry a documented notes rationale describing how that mutation recovers (a probe/recovery wrapper exists) or why inner retries stay disabled. The registry-audit test test_retry_disabled_entries_are_intentional_and_documented fails if a new PROBE_THEN_CREATE policy is added without one.

The production registry has explicit coverage for every active RPCMethod, including read-only RPCs. Read-only entries are registered as replay-safe IDEMPOTENT_SET_OP rows rather than left as production-UNCLASSIFIED; UNCLASSIFIED is retained only as a placeholder for tests and future development.

See ADR-0005. Side-effect probing (idempotent_create(...)) is a separate mechanism not owned by the registry; see the upload/source-add row in the "Uploads, downloads, and polling" table above.

Schema validation (ADR-0011)

Batchexecute responses are undocumented and Google reshapes them without notice. Decoders walk nested positional lists; a single index shift either crashes with raw IndexError from inside a feature module or silently degrades.

The single helper that decoders use to navigate row shapes is notebooklm.rpc.safe_index in rpc/_safe_index.py. It always raises a typed shape-drift error: strict decoding is the only mode (the legacy soft-mode opt-out was retired in v0.7.0). The RpcExecutor decode path narrowly wraps json.JSONDecodeError, KeyError, IndexError, and TypeError into RPCError; other exception types (e.g. AttributeError) intentionally propagate as code bugs rather than being conflated with shape drift.

See ADR-0011.

Per-capability protocol model

ADR-0013 ("Composable Session Capabilities") is the design rationale: feature APIs depend on narrow capability Protocols rather than on the deleted concrete Session class. ADR-0014 extends that intent at runtime: each feature receives the specific collaborator it needs, never a broad runtime facade. NotebookLMClient.__init__ is the composition root that wires each feature with the satisfier it needs.

Three shared Protocols live in _runtime/contracts.py: Kernel, RpcCaller, and LoopGuard. RpcCaller and LoopGuard meet ADR-0013's "shared by at least two features" promotion bar. Kernel remains shared because it is the typed transport surface implemented by the concrete client-owned kernel and consumed by the upload pipeline. Single-consumer capabilities stay beside their owner: AuthMetadata lives in _source/upload.py, and OperationScopeProvider lives in _artifact/polling.py. The unused AsyncWorkRuntime composite and the feature-local composite runtime Protocols (ChatRuntime, ArtifactsRuntime, UploadRuntime) were deleted once they no longer represented independently varying production dependencies.

Module-level Protocols (defined in _runtime/contracts.py):

Protocol Responsibility
RpcCaller Exposes rpc_call(method, params, ...) — the chokepoint every feature API uses for batchexecute calls.
LoopGuard Exposes assert_bound_loop() — single-method cross-loop affinity check; consumed by anything that may touch the HTTP client.
Kernel Pure transport surface — post() method, cookies property, aclose(). Single consumer today: SourceUploadPipeline.

Feature-module-local Protocols. Single-consumer capability shapes live next to their owner (AuthMetadata in _source/upload.py, OperationScopeProvider in _artifact/polling.py). No feature-local composite-runtime unions or adapter dataclasses exist in production. Every multi-capability feature takes its collaborators by keyword-only constructor argument:

  • ArtifactsAPI and SourceUploadPipeline take rpc: RpcCaller, drain: TransportDrainTracker, lifecycle: ClientLifecycle.
  • ChatAPI takes rpc: RpcCaller, transport: RuntimeTransport, reqid: ReqidCounter, loop_guard: LoopGuard.

Production satisfies shared Protocols via the underlying collaborators (ADR-0014 Rule 1: RpcExecutor satisfies RpcCaller, ClientLifecycle satisfies LoopGuard, and the concrete Kernel satisfies the Kernel Protocol). There is no production Session class in the runtime graph. Tests substitute tests/_fixtures/fake_core.py:FakeSession (constructed via make_fake_core(...)) — the sanctioned ADR-0007 / ADR-0013 fixture pattern. FakeSession is a backward-compatible test-fixture name, not a production runtime class. Tests that inject narrow fakes into a single feature (e.g. MagicMock(spec=RpcCaller, rpc_call=AsyncMock(...))) construct the feature directly under ADR-0014.

Executor takes its collaborators directly

Per ADR-0014 Rule 5, RpcExecutor takes its kernel, transport, auth-refresh coordinator, and metrics tracker directly — there is no Session-shaped owner Protocol. The constructor takes kernel: Kernel, transport: RuntimeTransport, auth_refresh: AuthRefreshCoordinator, and metrics: ClientMetrics as keyword-only parameters, plus constructor-injected providers for timeout, refresh-callback enablement, and retry-delay values. The executor enters transport through RuntimeTransport.perform_authed_post directly; the middleware terminal is MiddlewareChainHost._authed_post_chain_terminal → RuntimeTransport.terminal → Kernel.post. The chain leaf lives on MiddlewareChainHost so the chain owns its own terminal and retry tunables (ADR-0014 Rule 4 chain-ownership carve-out). Request types, transport errors, and streaming helpers live in separate owning modules. This keeps feature APIs on narrow capability Protocols and the executor on direct collaborator dependencies.

Client-owned runtime collaborator graph

                        +---------------------+
                        |  NotebookLMClient   |
                        +----------+----------+
                                   |
        +--------------------------+--------------------------+
        |                          |                          |
        v                          v                          v
  _auth: AuthTokens        _seams: ClientSeams        feature API objects
  one mutable instance     decode/sleep/auth-error     notebooks/sources/
                           runtime callables           artifacts/chat/...
        |
        v
  _collaborators: RuntimeCollaborators
  metrics | drain_tracker | reqid | auth_coord | kernel | lifecycle | cookie_persistence
        |
        v
  Kernel owns httpx.AsyncClient + cookie jar; ClientLifecycle opens/closes it

        +--------------------------+
        | _composed: ClientComposed|
        +--------------------------+
        | transport: RuntimeTransport
        | executor: RpcExecutor
        | chain_host: MiddlewareChainHost
        | chain_builder + middlewares
        | get_rpc_semaphore()
        +--------------------------+
             |
             v
  RpcExecutor.rpc_call → RuntimeTransport.perform_authed_post
      → ADR-0009 chain → RuntimeTransport.terminal → Kernel.post → httpx
Collaborator Module Responsibility
NotebookLMClient client.py Public surface and composition root. Owns _auth, _seams, _composed, _collaborators, _rpc_executor, and the ten feature API attributes (notebooks, sources, artifacts, chat, notes, mind_maps, research, settings, sharing, labels). __aenter__, close, drain, is_connected, metrics_snapshot, and rpc_call route directly to the owning collaborator.
ClientSeams _client_seams.py Mutable holder for runtime callables that closures re-read after construction: decode_response, sleep, and is_auth_error. Construction-only seams such as async_client_factory stay on compose_client_internals(...) and the client-shell test helper, not on the public constructor.
ClientComposed _client_composed.py Write-once holder for composition state: transport, executor, chain_host, chain_builder, middlewares, lazy RPC semaphore, and runtime_collaborators. Pre-binding access raises a clear RuntimeError; the holder deliberately does not expose a broad .collaborators alias.
RpcExecutor _rpc_executor.py Single logical batchexecute RPC dispatch path. Owns request-id/started-metric bracketing, idempotency policy lookup, method-ID resolution, request encoding, response decode, RPC error mapping, and decode-time auth refresh retry. Takes its Kernel, RuntimeTransport, AuthRefreshCoordinator, and ClientMetrics collaborators directly via keyword-only constructor parameters (ADR-0014 Rule 5). Enters transport through RuntimeTransport.perform_authed_post.
RuntimeTransport _runtime/transport.py Authed POST collaborator. Owns perform_authed_post() (loop guard, auth snapshot, request materialization, chain dispatch, queue-wait recording), refresh_request_for_current_auth(), and terminal() (freshness rebuild + Kernel.post). Called directly by RpcExecutor and by chat_aware_authed_post (ChatAPI's chat-flavoured transport call); the middleware chain leaf at MiddlewareChainHost._authed_post_chain_terminal continues to dispatch through RuntimeTransport.terminal per ADR-0014 Rule 4.
MiddlewareChainHost _middleware/chain_host.py Owns the wired middleware chain (_authed_post_chain), the chain leaf (_authed_post_chain_terminal), the three retry-budget tunables (_rate_limit_max_retries, _server_error_max_retries, _refresh_retry_delay), and the dynamic await_refresh delegate that the auth-refresh middleware captures. The chain's provider lambdas and the transport's chain_provider closure read the host's attributes live, so post-construction mutation (e.g. tests setting client._composed.chain_host._rate_limit_max_retries = 0) still steers the live chain.
AuthRefreshCoordinator _runtime/auth.py Owns the auth-snapshot lock and refresh task. Canonical implementation for AuthRefreshCoordinator.snapshot(auth=...), update_auth_tokens(auth=..., csrf=..., session_id=...), and update_auth_headers(auth=..., kernel=...); callers pass explicit collaborators rather than a host object.
ClientLifecycle _runtime/lifecycle.py HTTP-client open/close, keepalive task, cookie save coordination. Holds _timeout, _bound_loop, _http_client, _keepalive_*.
MiddlewareChainBuilder _middleware/chain.py Constructs the middleware chain in the canonical ADR-0009 order.
TransportDrainTracker _transport_drain.py Tracks in-flight transport operations + the drain condition variable. Gates graceful shutdown.
ClientMetrics _client_metrics.py Per-instance counters (ClientMetricsSnapshot) + the on_rpc_event user callback.
ReqidCounter _reqid_counter.py Monotonic _reqid for the chat backend; lock-protected next_reqid(...).
CookiePersistence _cookie_persistence.py Cookie-jar persistence + __Secure-1PSIDTS rotation.
IdempotencyRegistry _idempotency.py Policy/classification registry keyed by (RPCMethod, operation_variant). The production registry explicitly covers every active RPCMethod; UNCLASSIFIED is retained only as a placeholder for hand-built test/future registries. RpcExecutor._execute_once() consults it to resolve effective_disable_internal_retries. It is part of the RPC dispatch path, not lifecycle state. Side-effect probing (idempotent_create(...)) is a separate mechanism not owned by this registry.
_request_types _request_types.py Owns AuthSnapshot, BuildRequest, and request materialization shapes shared by RPC, chat, auth refresh, and the chain terminal.
_transport_errors _transport_errors.py Owns transport-level exceptions, Retry-After parsing, and raw Kernel.post error mapping consumed by RetryMiddleware and AuthRefreshMiddleware.
_streaming_post _streaming_post.py Low-level streaming POST helper with the response-size cap used by Kernel.post.
Kernel _kernel.py Pure transport core. Owns the httpx.AsyncClient and cookie jar; exposes post(), the cookies property, and aclose() (the close path wraps it in asyncio.shield from ClientLifecycle.close()). Concrete class behind the Kernel Protocol in _runtime/contracts.py; constructed by build_collaborators(...) and called from the middleware leaf via RuntimeTransport.terminal → Kernel.post.
_runtime/init _runtime/init.py Construction-time helpers for NotebookLMClient: validate_constructor_args (kwarg validation/normalization), build_collaborators (the seven collaborators in dependency order: metrics, drain_tracker, reqid, auth_coord, kernel, lifecycle, cookie_persistence), build_runtime_transport, wire_middleware_chain, and compose_client_internals. It binds the runtime graph into ClientComposed and returns ClientInternals(collaborators, executor).
_loop_affinity _loop_affinity.py Tiny free-function assert_bound_loop(bound_loop) shared by every helper that captures a loop reference at open() time (TransportDrainTracker, ReqidCounter, AuthRefreshCoordinator, ArtifactPollingService, ChatAPI). Enforces ADR-0004 without coupling those helpers to the public client.

Shipped runtime invariants

ADR-0016 pins two compatibility-sensitive details that survive the session-elimination work:

  • NotebookLMClient._auth is the authoritative mutable AuthTokens instance. Refresh paths mutate that object in place, and collaborators that observe auth must alias it rather than holding detached copies.
  • CORE_LOGGER_NAME intentionally remains the literal "notebooklm._core" even though the _core.py compatibility module was deleted. Runtime code keeps using this logger key through CORE_LOGGER_NAME for downstream log filters and caplog selectors. Treat it as a logging compatibility contract, not evidence that notebooklm._core is an active module or that a concrete Session owner remains in the runtime graph.

Domain-service collaborators

Beyond the client-owned runtime graph, several feature APIs are implemented via dedicated domain services and helper modules:

Service / Module Module Responsibility
NoteService _note_service.py Service layer managing note CRUD, note-backed content generation, and sync.
NoteBackedMindMapService _mind_map.py Specific adapter service representing mind-maps, backed by standard notebook notes.
ArtifactDownloadService _artifact/downloads.py Asynchronous download coordinator for finished artifacts.
ArtifactGenerationService _artifact/generation.py Generation kickoff service (generate_*, revise_slide, retry_failed) extracted from ArtifactsAPI.
_artifact_formatters _artifact/formatters.py Markdown, HTML, and plain text formatters for artifacts.
_artifact/listing _artifact/listing.py Listing and filtering operations for notebook artifacts.
_row_adapters* _row_adapters/artifacts.py, _row_adapters/chat.py, _row_adapters/labels.py, _row_adapters/notebooks.py, _row_adapters/notes.py, _row_adapters/research.py, _row_adapters/sources.py Wire-shape adapters that wrap raw batchexecute rows (ArtifactRow, LabelRow, NoteRow, SourceRow, the POLL_RESEARCH rows, the SUGGEST_PROMPTS suggestion rows) and the streamed-chat rows (AnswerRow/CitationRow/…) behind named accessors so downloads, polling, listing, labels, research, and the chat parser don't open-code positional indices. Strict decode behavior is pinned in tests/unit/test_row_adapters.py, tests/unit/test_chat_row_adapter.py, tests/unit/test_notebooks_row_adapter.py, and tests/unit/test_research_row_adapter.py.
_research_task_parser _research_task_parser.py Parses deep-research task results from raw rows. Returns dict-shaped output today; a typed-model migration is not yet complete.
_types/ _types/ Private package holding the dataclass and Protocol implementations behind the public types.py / per-feature public schemas. Split per domain (artifacts.py, chat.py, labels.py, mind_maps.py, notebooks.py, notes.py, research.py, sharing.py, sources.py, plus common.py for shared shapes like ConnectionLimits).

Authentication subpackage

auth.py is a thin public facade that re-exports the canonical implementations under _auth/. ADR-0014 closed ADR-0003's deferred flat-re-export goal: AuthTokens and load_auth_from_storage() now live in _auth.tokens, _validate_required_cookies is a direct _auth.cookie_policy re-export, and async def enumerate_accounts is the only remaining auth.py function body because it binds _poke_session as the default dependency.

Module Responsibility
_auth/tokens.py Token dataclass + storage-loading helpers.
_auth/paths.py Storage paths and filesystem helpers.
_auth/storage.py Profile/state persistence on disk.
_auth/extraction.py Cookie/token extraction from browser sessions.
_auth/headers.py HTTP header construction.
_auth/cookies.py Cookie maps + _update_cookie_input helper.
_auth/cookie_policy.py Domain allowlist, cookie-domain builder (build_cookie_domain_allowlist), and cookie policy decisions.
_auth/browser_capture.py Transport-neutral browser launch→navigate→capture→filter→persist core (lazy playwright); shared by the interactive CLI login adapter and the layer-3 headless re-auth layer (ADR-0021). The headless arm classifies the landing URL (authenticated→capture, redirected-to-login→HeadlessLoginRequiredError). run_cdp_capture is an alternative credential source: attach to an operator-pointed already-running Chrome over CDP (connect_over_cdp, disconnect-only teardown) using the SAME landing classification + cookie-domain allowlist.
_auth/headless_reauth.py Layer-3 (deepest) auth recovery: when first-party cookies are dead, drive a headless browser against the persistent profile to silently re-mint cookies. Typed honest outcomes (HeadlessReauthStatus UNAVAILABLE/FAILED/SUCCESS — never silent None). Opt-in only (refresh_auth(allow_headless=True) or NOTEBOOKLM_HEADLESS_REAUTH=1); local-unattended-only, never the remote/MCP auth path. Alternative credential source: NOTEBOOKLM_HEADLESS_REAUTH_CDP_URL (or attempt_headless_reauth(cdp_url=...)) attaches to an operator-pointed running Chrome instead of the dedicated profile (freshness mitigation). Also exposes headless_reauth_readiness() — a credential-free, browser-free probe (profile present + playwright installed) surfaced by doctor.
_auth/account.py Account profile + multi-account switching.
_auth/session.py refresh_auth_session(auth=..., kernel=..., auth_coord=..., lifecycle=..., cookie_persistence=...) implementation called by AuthRefreshCoordinator. Takes five explicit keyword-only collaborators instead of a Session-shaped owner Protocol; the previous RefreshAuthCore Protocol and the update_auth_tokens / update_auth_headers Session-level forwards have been removed.
_auth/refresh.py Token refresh driver (external login command, coalesced runs, secret redaction).
_auth/keepalive.py Cookie keepalive + __Secure-1PSIDTS rotation.
_auth/psidts_recovery.py Inline PSIDTS recovery for cold-start (see issue #865).
_auth/master_token.py Headless master-token auth: mint/persist web cookies from a durable Google master token; layer-4 re-mint recovery (ADR-0023).

The cookie lifecycle — what gets written, who rotates, what the keepalive contract is — is documented separately in docs/auth-cookie-lifecycle.md.

CLI layer (ADR-0008)

The CLI is intentionally a thin adapter over the public Python client. It does not build raw batchexecute payloads, import the RPC layer, or reach into private notebooklm._* implementation modules. Click commands in src/notebooklm/cli/*_cmd.py own argument parsing, user-visible rendering, JSON envelopes, and exit codes; workflow logic lives in src/notebooklm/cli/services/. This separation is the ADR-0008 extraction pattern.

The console-script entry point is notebooklm_cli.py. It declares the root notebooklm Click group with SectionedGroup, owns process-wide options (--storage, --profile, --verbose, --quiet), canonicalizes the storage path into ctx.obj, stores the selected profile/quiet values there, and registers the top-level commands plus command groups. SectionedGroup is a presentation concern only: it bins commands in help output, and tests/unit/cli/test_grouped.py rejects new unbinned commands.

A typical authenticated command follows this path:

+----------------------------------------------------------------+
| notebooklm_cli.cli root group                                  |
|   - SectionedGroup                                             |
|   - process-wide options:                                      |
|     --storage / --profile / --verbose / --quiet                |
+----------------------------------------------------------------+
                                 |
                                 v
+----------------------------------------------------------------+
| cli/<domain>_cmd.py Click command                              |
+----------------------------------------------------------------+
                                 |
                                 v
+----------------------------------------------------------------+
| cli.auth_runtime.with_auth_and_errors(...)                     |
|   or run_client_workflow(...)                                  |
|   - handle_errors(...) wraps command-body failures             |
|   - AuthSource resolves precedence:                            |
|     --storage > NOTEBOOKLM_AUTH_JSON > active profile storage  |
|   - get_auth_tokens(...) builds AuthTokens                     |
|   - cli.runtime.run_async(...) -> one top-level asyncio.run    |
+----------------------------------------------------------------+
                                 |
                                 v
+----------------------------------------------------------------+
| async with NotebookLMClient(auth) as client                    |
+----------------------------------------------------------------+
                                 |
                                 v
+----------------------------------------------------------------+
| cli/services/<domain>.py plan/executor                         |
|   or direct public client call                                 |
+----------------------------------------------------------------+
                                 |
                                 v
+----------------------------------------------------------------+
| command module:                                                |
|   - renders text / JSON                                        |
|   - applies exit-code policy                                   |
+----------------------------------------------------------------+
Layer Owns Does NOT own
notebooklm_cli.py Root Click group, global options, profile/storage setup, command registration Per-command workflows, rendering of command results
cli/*_cmd.py Click decorators, option parsing, stdout/stderr rendering, JSON output, exit codes Business logic, RPC dispatch, retry loops
cli/services/*.py Workflow orchestration, plan dataclasses, result types, retry/wait policy Click context, console.print, SystemExit (target end-state; some modules are still mid-migration)

Command modules are named *_cmd.py (e.g. source_cmd.py, notebook_cmd.py) to avoid Python's package-attribute shadowing — the historical short names (source, notebook, …) are re-exported from cli/__init__.py so existing imports keep working. The shadowing invariant is pinned by tests/_guardrails/test_no_module_shadowing.py.

CLI services are organised by feature family; notable examples include cli/services/login/ (browser-profile enumeration split across Chromium and Firefox cookie jars), cli/services/source_* (URL/file/research source flows), and cli/services/generate.py. The CLI service-layer boundary is guarded by tests/unit/cli/test_services_boundary.py: new service modules must either be fully cleaned of Click/rendering/exit ownership or be added to the explicit transitional inventory with the current violations and rationale.

The cross-command helpers form a small internal CLI stack:

Module Role
cli/runtime.py Leaf runtime helpers: root --quiet lookup and the single asyncio.run(...) bridge for sync Click handlers.
cli/auth_runtime.py Shared auth bootstrap, command-body error wrapping, and optional opened-client workflow helper.
cli/master_token_login.py Command driver for notebooklm login --master-token[-refresh], rendering over the master-token login service cli/services/login/master_token.py (mint/persist/refresh + browser oauth_token capture; ADR-0023).
cli/services/auth_source.py Single resolver for CLI auth-source precedence (--storage, NOTEBOOKLM_AUTH_JSON, active profile).
cli/context.py Profile/storage-scoped context.json persistence for active notebook, conversation, and account metadata.
cli/resolve.py Notebook/source/artifact/note ID resolution, including partial-ID matching against public client list calls.
cli/options.py + cli/completion.py Shared Click option decorators and best-effort shell completion. Completion providers may load auth and list public client resources, but swallow all failures so shells never print diagnostics during TAB completion.
cli/rendering.py Rich/text/JSON rendering helpers. Status lines in JSON mode go to stderr so stdout remains parseable JSON.
cli/error_handler.py Canonical CLI error-to-exit mapping. Under --json, command-body failures use the typed error envelope from ADR-0015. Parse-time Click parser errors remain Click-owned.
cli/helpers.py Backward-compatible facade for historical imports and test patch targets. New production code should import from the owning helper module instead.

The boundary is enforced statically by tests/_guardrails/test_cli_boundary.py: CLI modules may import public notebooklm modules and their own intra-CLI private helpers, but not notebooklm._*, notebooklm.rpc.*, or private names from public modules. Two sanctioned exceptions to the notebooklm._* rule are whitelisted in that gate: notebooklm._app (the transport-neutral business-logic layer every adapter consumes) and the single module notebooklm._auth.browser_capture (the transport-neutral browser launch→capture→filter→persist core that the Playwright login adapter cli/services/playwright_login.py sits over, per ADR-0021 — interactive presentation stays in cli/ while the neutral core moves down to _auth, reachable by the client runtime and the future headless re-auth layer). No other _auth.* module may be imported by the CLI — the rest stays behind the auth.py facade. The same test keeps low-level helpers (runtime, context, resolve, rendering, auth_runtime, options) from growing upward dependencies on command modules or the cli.helpers compatibility facade.

MCP adapter (mcp/)

The MCP server is a second thin adapter beside cli/, opt-in behind the mcp extra and experimental (preview). create_server() builds a FastMCP server that exposes the _app/ cores as MCP tools driving a single long-lived NotebookLMClient; run it with the notebooklm-mcp console script (stdio or loopback HTTP). It imports no click / rich / cli — like the CLI, it is built on the _app/ cores only (enforced by tests/_guardrails/test_mcp_boundary.py). Failures surface as CODE: message strings projected from _app.errors.classify, and mutating tools are confirmation-gated (they return a needs_confirmation preview unless called with confirm=true). notebooklm mcp install <client> wires it into Claude Desktop/Code, Cursor, or Windsurf, and desktop-extension/ packages a one-click .mcpb bundle. Full guide: docs/mcp-guide.md.

REST server (server/)

The single-tenant REST server is the third adapter (ADR-0021), opt-in behind the server extra and experimental. A FastAPI app maps /v1 routes onto the _app/ cores and the public client namespaces, with one NotebookLMClient opened once at the ASGI lifespan inside the server loop (honoring the ADR-0004 loop- affinity contract). Every /v1 request requires a static bearer token (constant-time compare) plus a loopback Host literal (a DNS-rebinding guard); /healthz is the one public route, and the /docs / /openapi.json schema surface is disabled. Long-running work (source ingest, artifact generation) uses the poll-the-resource model — the create call returns immediately and the matching GET reports pending / 200 / 404 / 409 / 410. Failures project from _app.errors.classify onto an HTTP status plus the {"error": {"category": "...", "message": "..."}} envelope. It imports no click / rich / cli (enforced by tests/_guardrails/test_server_boundary.py). Launch and configuration: docs/installation.md. Expensive route groups have lifespan-owned concurrency limiters, tuned by NOTEBOOKLM_SERVER_*_CONCURRENCY env vars, so source mutation/wait, artifact generation/download, research, and blocking chat work cannot unboundedly starve cheap reads or /healthz.

Middleware chain (ADR-0009)

The runtime chain order is pinned by tests/unit/test_chain_wiring.py (facade-level) and tests/unit/test_middleware_chain_builder.py (builder-level). The order is load-bearing: changing it without simultaneously updating the pin tests (test_chain_seeded_with_final_adr_009_ordering) is a bug.

The chain list in MiddlewareChainBuilder.build() reads outermost-first (index 0 wraps everything below it):

DrainMiddleware              outermost — admits and tracks for shutdown drain
   ↓
MetricsMiddleware            starts timing here (latency includes queue wait)
   ↓
SemaphoreMiddleware          max_concurrent_rpcs slot acquired AFTER Drain/Metrics,
                             BEFORE Retry can re-enter (one slot per logical RPC)
   ↓
RetryMiddleware              429 / 5xx with Retry-After honor
   ↓
AuthRefreshMiddleware        refresh-on-auth-error; capped retries
   ↓
ErrorInjectionMiddleware     synthetic-error harness; no-op in prod
   ↓
TracingMiddleware            innermost — structured-logging boundary
                             (OpenTelemetry export is future work)
   ↓
Authed POST leaf             (RuntimeTransport.terminal → Kernel → httpx)

Client as composition root

NotebookLMClient is both the public surface and the composition root. It owns ClientComposed, the collaborator bundle, the RPC executor, and the feature API instances. ClientLifecycle owns open/close behavior (loop-affinity binding, keepalive task, cookie persistence, and transport teardown); TransportDrainTracker owns drain semantics.

Concretely, the client-owned runtime retains:

  1. Late-bound composition slots. ClientComposed.transport, the chain metadata slots (chain_builder / middlewares), and ClientComposed.executor are bound exactly once by compose_client_internals(...) through write-once binders. Pre-binding access trips the ClientComposed guard. ClientComposed exposes runtime_collaborators, not a broad collaborators alias. tests/_guardrails/test_client_composition.py guards against inlining holder state back onto NotebookLMClient.
  2. Middleware-chain seams. The chain leaf (_authed_post_chain_terminal), the chain slot (_authed_post_chain), the dynamic refresh delegate (await_refresh), and the three retry-budget tunables (_rate_limit_max_retries, _server_error_max_retries, _refresh_retry_delay) live on MiddlewareChainHost. wire_middleware_chain and build_runtime_transport take that host directly and read its attributes live.
  3. Lifecycle methods. Public client __aenter__, __aexit__, close, drain, and is_connected call ClientLifecycle and TransportDrainTracker directly.

NotebookLMClient.rpc_call(method, params) dispatches directly through self._rpc_executor.rpc_call(...) — the RpcExecutor captured during the shared _client_assembly.py::_assemble_client(...) construction path from compose_client_internals(...) and shared with every feature API.

Feature APIs receive the collaborator they need (RpcExecutor for RpcCaller, ClientLifecycle for LoopGuard, the concrete Kernel for upload cookies/posting, and TransportDrainTracker for local operation scopes / close hooks) per ADR-0014 Rules 1 + 3. Features that need more than one capability — ChatAPI, ArtifactsAPI, and SourceUploadPipeline — take each collaborator by keyword-only constructor argument. The composition wiring is centralized in _client_assembly.py, which is called by both NotebookLMClient.__init__ and the canonical test factory.

Testing patterns

Two policies define how tests interact with the architecture above.

Constructor-injection fixtures (ADR-0007)

The forbidden patterns are monkeypatch.setattr("notebooklm.…") against module-level seams and direct attribute assignment like target.rpc_call = AsyncMock(...). The sanctioned substitute is tests/_fixtures/fake_core.py:make_fake_core(...), which returns a FakeSession configured to satisfy the narrow shared protocols plus the upload/polling local protocols used by legacy feature tests. The name is backward-compatible test vocabulary; it is not a production Session replacement. Multi-capability features (ChatAPI, ArtifactsAPI, SourceUploadPipeline) take their direct collaborators by keyword-only constructor argument, so unit tests can inject narrow MagicMock(spec=RpcCaller, rpc_call=AsyncMock(...))-style fakes directly via those constructors.

The meta-lint at tests/_guardrails/test_no_forbidden_monkeypatches.py enforces the policy; the file-level allowlist shrinks as legacy tests migrate. See ADR-0007.

Test suite taxonomy

  • Unit tests (tests/unit/): No network, decode/encode only.
  • Integration tests (tests/integration/): Mock HTTP responses or use VCR cassettes scrubbed per ADR-0006.
  • E2E tests (tests/e2e/): Real API; require auth; marked @pytest.mark.e2e and excluded from the default run.

Pin tests that lock architectural invariants (chain ordering, narrow Protocol membership, no forbidden monkeypatch) live in tests/unit/ and tests/_guardrails/ — changing the underlying invariant without updating the pin is a bug.

A fuller taxonomy can be generated with scripts/test_taxonomy_inventory.py.

Implementation surface convention (ADR-0012)

notebooklm-py keeps a small set of public-named modules (artifacts.py, auth.py, client.py, config.py, exceptions.py, io.py, log.py, migration.py, notebooklm_cli.py, paths.py, research.py, types.py, urls.py, utils.py) and routes everything else through underscore-prefixed seam modules. Anything underscored is not a supported import surface; it can be moved, renamed, or deleted without a deprecation cycle. See ADR-0012.

The corollary for contributors: if you find yourself reaching into notebooklm._foo, prefer a capability Protocol or a public function in one of the named modules.

Boundary moratorium

New architectural carve-outs are expensive: every ADR amendment and tests/_guardrails/ pin becomes load-bearing for contributors who have to read the docs before touching the relevant seam. To keep that surface from drifting upward without bound, the following discipline applies to any future change that would expand the documented boundary set:

  • Justify by failure mode. A new ADR amendment or tests/_guardrails/ pin must cite a concrete user-visible failure mode it prevents (loop-affinity break, auth-snapshot tear, transport drain regression, public-API breakage, etc.). "Future-proofing" or "in case someone refactors X" is not sufficient.
  • Prefer deletion over carve-out. When a compatibility seam can be removed instead of documented, remove it. Carve-outs are the fallback when removal is genuinely infeasible, not the default.
  • One owner per rule. A pin without a corresponding ADR clause (and vice versa) is a smell — it means the rule is enforced but not explained, or explained but not enforced.

The intent is architectural: shrink the boundary set whenever the underlying code allows it, and resist growing it on speculative grounds.

Glossary

Vocabulary that recurs in this document and the surrounding code.

Term Meaning
batchexecute Google's internal RPC protocol over HTTPS. The wire is positional lists keyed by an obfuscated method id; see rpc/types.py.
Capability Protocol A narrow structural Protocol (e.g. RpcCaller, LoopGuard) a feature depends on instead of taking the deleted concrete Session class or a broad runtime facade. See ADR-0013.
Chain / leaf / terminal The middleware chain's ordering vocabulary. The chain wraps outermost-first; the leaf is the innermost middleware (TracingMiddleware); the terminal is the authed-POST function (RuntimeTransport.terminal → Kernel.post) that ends the chain.
Drain Graceful-shutdown waiting on in-flight transport operations to complete. Owned by TransportDrainTracker and admitted by DrainMiddleware.
idempotent_create(...) Caller-owned probe-then-create wrapper used by source-add / Drive-add flows. Distinct from the IdempotencyRegistry (which only classifies retry safety inside the executor).
operation_variant Optional kwarg on rpc_call(...) that selects a method-variant-specific idempotency policy from the registry (e.g. ADD_SOURCE "url" vs "drive"). Unknown variants raise IdempotencyVariantError.
RPC method id A short obfuscated identifier (rpcids=) Google uses to route batchexecute calls. Source of truth: RPCMethod enum in rpc/types.py.
Snapshot An AuthSnapshot (see _request_types.py) — an immutable, point-in-time view of session id, CSRF token, authuser, and account email. Taken inside the auth-snapshot lock so a refresh racing with a transport build cannot tear.

File map

Per-file index plus the full src/notebooklm + tests repository tree. The tree is the hand-maintained module map that scripts/check_claude_md_freshness.py checks in both directions (documented paths exist; every module is documented or intentionally omitted).

Key Files

File Purpose
client.py Main NotebookLMClient class
_client_assembly.py Single private assembly seam (_assemble_client) that wires every constructor-set attribute; shared by NotebookLMClient.__init__ and the canonical test factory (tests/_helpers/client_factory.py) so the two construction paths cannot drift.
_client_composed.py Client-owned composition holder for transport, executor, chain host, middleware metadata, and runtime collaborator bundle.
_client_seams.py Constructor-only injectable seams used by tests and collaborator construction.
_runtime/init.py Constructor helpers that validate client runtime kwargs, build collaborators (returning a RuntimeCollaborators bundle), wire middleware, and bind ClientComposed.
_kernel.py Concrete Kernel transport core (owns httpx.AsyncClient + cookie jar)
_runtime/config.py DEFAULT_* knobs and module-level constants. CORE_LOGGER_NAME = "notebooklm._core" is intentionally preserved as a compatibility logging contract even though the _core module was deleted; renaming it silently breaks downstream caplog/logger filters.
_env.py, config.py Runtime environment defaults and the public config re-export surface
_logging.py, log.py Redaction/correlation logging internals and the public logging helper surface
_secrets.py Canonical runtime registry of must-scrub bare session-cookie names (RUNTIME_SESSION_COOKIES), __Secure-* / __Host-* prefix umbrellas (SECURE_HOST_UMBRELLA_PATTERNS, fail-closed for future names), and carrier-agnostic Google credential shapes (AUTH_TOKEN_SHAPE_PATTERNSg.a000- / sidts- / ya29. tokens + the AIza… API key) that _logging.py redaction and exceptions.py scrubbing DERIVE from. Runtime code cannot import from tests/, so this restates the cassette sanitizer's must-scrub shapes; tests/_guardrails/test_runtime_secret_registry_parity.py asserts lockstep with tests/cassette_patterns.py on every axis — bare-cookie superset, umbrella coverage, and regex-string shape equality (issues #1517/#1518).
_callbacks.py Sync-or-async callback invocation helper used by telemetry/retry hooks
_lookup.py unwrap_or_raise(obj, exc) — the shared single-row-lookup helper backing the public get/get_or_none pair (ADR-0019 Enforcement tier-2). The four sources/artifacts/notes/mind_maps get() methods call it directly to raise their *NotFoundError on a miss (the v0.8.0 flip, issue #1247); notebooks.get() already raised on its own path and does not route through it.
_loop_bound.py LoopBoundPrimitive — template-method base for the loop-affinity set_bound_loop protocol. Owns the _bound_loop field + a set_bound_loop that always stores the binding and fires the _on_loop_rebind(old, new) hook only on a real loop change (hook before store). Trivial owners (TransportDrainTracker/ReqidCounter/AuthRefreshCoordinator) use the default no-op hook; clear-on-rebind owners (ClientComposed/SourceUploadPipeline/ChatAPI) override it to discard their cached loop-bound primitive/locks. Owns only the binding + rebind hook — the cross-loop assert stays in _loop_affinity, and each owner keeps its own reset_after_open.
_deprecation.py Deprecation helper, gated by NOTEBOOKLM_QUIET_DEPRECATIONS: warn_deprecated — generic gated primitive for one-off deprecations (e.g. awaiting from_storage(...)); pass removal=None when no removal version is pinned yet. deprecations_quiet / _deprecations_quiet / _QUIET_ENV_VAR back the suppression gate. ADR-0018 forbids inline warnings.warn(..., DeprecationWarning) outside this module — tests/_guardrails/test_no_inline_deprecation_warnings.py enforces it (governs the DeprecationWarning category only; an inline RuntimeWarning/UserWarning is allowed). Note save_cookies_to_storage(original_snapshot=None) is NOT a deprecation — it's a permanent back-compat shim emitting an inline RuntimeWarning race advisory, outside ADR-0018 scope and ungated (issue #1369). The v0.7.0 error-contract machinery (NOTEBOOKLM_FUTURE_ERRORS/future_errors_enabled, warn_get_returns_none, deprecated_kwarg, MappingCompatMixin) was removed in v0.8.0 once every break it staged became the default (issue #1365). See docs/deprecations.md.
_runtime/helpers.py is_auth_error, AUTH_ERROR_PATTERNS, _resolve_keepalive_interval
_error_injection.py Synthetic-error env-var resolver + startup guard
_client_metrics.py ClientMetricsClientMetricsSnapshot counters + on_rpc_event callback
_transport_drain.py TransportDrainTracker — in-flight transport counters + _TransportOperationToken
_deadline.py RuntimeDeadline helper shared by retry and polling loops so aggregate timeouts clamp sleep consistently
_backoff.py Shared capped exponential-backoff calculation with deterministic test injection
_reqid_counter.py ReqidCounter — monotonic _reqid for the chat backend
_runtime/auth.py AuthRefreshCoordinator — refresh task + auth-snapshot lock
_auth_refresh_retry.py Shared auth refresh-and-retry core for the two retry layers (HTTP-status AuthRefreshMiddleware + decoded-RPC RpcExecutor): the once-per-logical-call RefreshBudget token and the common refresh_and_count body (log/refresh/sleep/rpc_auth_retries metric). Unifies the previously-divergent copies per issue #1205; the two layers keep their distinct triggers and refresh-failure exception shapes.
_runtime/lifecycle.py ClientLifecycle — loop-affinity guard + keepalive task
_runtime/transport.py RuntimeTransport — authed-POST transport wrapper that drives the middleware chain and typed transport response handling
_rpc_executor.py RPC dispatch executor. Takes its Kernel, RuntimeTransport, AuthRefreshCoordinator, and ClientMetrics collaborators directly via keyword-only constructor parameters (ADR-0014 Rule 5). Defines a single local DecodeResponse Protocol.
_request_types.py Shared authed POST request construction types: AuthSnapshot, BuildRequest, PostBody, and materialization helpers.
_transport_errors.py Transport exceptions, Retry-After parsing, and terminal Kernel.post error mapping for retry/auth middleware.
_streaming_post.py Size-capped streaming POST helper used by Kernel.post.
_middleware/core.py HTTP-shaped middleware request/response envelope, chain composition, and middleware Protocol
_middleware/context.py Canonical per-request context-key vocabulary for middleware
_middleware/chain_host.py Mutable owner for the live middleware chain slots and retry-budget tunables
_conversation_cache.py Per-instance true-LRU conversation cache for ChatAPI (caps conversation count via MAX_CONVERSATION_CACHE_SIZE and per-conversation turns via MAX_TURNS_PER_CONVERSATION)
_polling_registry.py Pending-poll registry for long-running artifact generations
_cookie_persistence.py Cookie-jar persistence + __Secure-1PSIDTS rotation
_runtime/contracts.py Shared runtime Protocols consumed by sub-clients
_idempotency.py Mutating-RPC idempotency policy registry and probe-then-retry wrapper; ADR-0005 is the taxonomy source
_idempotency_policy.py Declarative per-RPC idempotency classification data, applied to IDEMPOTENCY_REGISTRY via register_default_policies at _idempotency import time (#1331). Holds the load-bearing two-pass seeding order (pre-seed register()_seed_defaults() → post-seed register() + the read/set-op loop).
_atomic_io.py, io.py Atomic JSON write/update internals and public I/O re-export surface for CLI boundary compliance
exceptions.py Public exception hierarchy plus safe diagnostic preview/redaction helpers
paths.py, migration.py Profile-aware path resolution and locked migration from the legacy flat layout
_types/, types.py Dataclass implementation package and public type/re-export facade
_types/labels.py Label pure-value type (source-label topic grouping; source_ids only, no artifact members) re-exported by types.py
_row_adapters/artifacts.py ArtifactRow typed view over raw positional artifact RPC rows, plus ReportSuggestionRow over GET_SUGGESTED_REPORTS rows
_row_adapters/chat.py Streamed-chat row adapters (AnswerRow / CitationRow / CitationDetail / PassageRow / StreamFrameRow / ErrorPayloadRow / TextLeafRow) that centralise the chat wire positions _chat/wire.py used to open-code (#1491)
_row_adapters/labels.py LabelRow strict typed view over the raw positional label tuple [name, sources, id, emoji] (fails loud on schema drift)
_row_adapters/notebooks.py SUGGEST_PROMPTS (otmP3b) suggestion-row view (PromptSuggestionRow / unwrap_prompt_suggestions) backing NotebooksAPI.suggest_prompts
_row_adapters/notes.py NoteRow typed view over raw positional note and mind-map RPC rows
_row_adapters/research.py ResearchTaskRow / ResearchTaskInfoRow / ResearchResultRow typed views over raw positional POLL_RESEARCH rows that centralise the single-level positions _research_task_parser.py used to open-code (#1501)
_row_adapters/sources.py SourceRow / SourceRowShape typed views over raw positional source RPC rows
artifacts.py, research.py, utils.py Public helper modules for artifact retry, research citation/report utilities, and common async helpers
_research_task_parser.py Internal parser for research task result-type selection
_notebooks.py client.notebooks API + source-id resolver
_notebook_payloads.py Stable batchexecute notebook RPC request payload builders (currently SUGGEST_PROMPTS)
_sources.py client.sources API
_artifacts.py client.artifacts API — owns artifact generation orchestration directly (see ADR-0012)
_chat/api.py client.chat API
_research.py client.research API
_notes.py client.notes API
_sharing.py client.sharing API
_labels.py client.labels API — source labels (topic groupings); pure-RPC like SharingAPI, plus a narrow list_sources callable for the membership→Source join in sources()
_settings.py client.settings API
_note_service.py Service layer managing note CRUD, note-backed content generation, and sync
_mind_map.py Specific adapter service representing mind-maps, backed by standard notes
_mind_maps_api.py client.mind_maps API — unified surface over both mind-map backends (note-backed JSON + interactive studio-artifact), dispatching each op to the correct RPC family (#1256)
_artifact/downloads.py Asynchronous download coordinator for finished artifacts
_artifact/_redirect_guard.py Per-redirect-hop host/scheme revalidation for downloads — rejects off-allowlist / non-HTTPS redirect targets before the request is sent (#1521)
_artifact/_download_client.py Download trusted-host allowlist + transport-aware client factory — wires the #1521 redirect guard for httpx (event hook) or the opt-in curl_cffi (get_guarded manual loop)
_artifact/formatters.py Markdown, HTML, and plain text formatters for artifacts
_artifact/payloads.py Stable CREATE_ARTIFACT / GENERATE_MIND_MAP request payload builders
_artifact/validation.py Input-validation guards for the ArtifactsAPI facade (generate_report format coercion, export exactly-one-of target), kept in a sibling module so the facade stays under the module-size ratchet (#1874)
_artifact/generation.py Generation kickoff service (generate_*, revise_slide, retry_failed) extracted from ArtifactsAPI; the facade keeps thin delegators
_artifact/listing.py Listing and filtering operations for notebook artifacts
_artifact/polling.py Poll coordination service for artifact generation tasks
_source/add.py Core service layer for adding text, URL, or Google Drive sources
_source/content.py Core service layer for fetching source HTML/markdown content
_source/listing.py Core service layer for listing notebook sources
_source/polling.py Poll coordination service for active source conversions
_source/upload.py Concurrency-gated upload pipeline for source files
_source/_upload_decode.py Pure decode/validation helpers for the upload pipeline (URL redaction, ADD_SOURCE_FILE source-id extraction, content-type policy), extracted from upload.py
_source/upload_payloads.py Stable source upload registration, rename, and resumable-upload request builders
_label/params.py Stable CREATE_LABEL / LIST_LABELS / UPDATE_LABEL / DELETE_LABEL request payload builders (with the shared _opts() request-options wrapper)
_notebook_metadata.py Metadata protocol schemas for sub-clients
_url_utils.py, urls.py URL parsing/validation internals and the public URL helper facade
_sharing_manager.py Direct sharing management logic
_version_check.py Dynamic client-side version deprecation guard
_version_info.py Human-facing version_string() — package version + short git commit (embedded by hatch_build.py at build time, or live git from a checkout)
_chat/notes.py Chat-adjacent note saving workflow adapter
_chat/wire.py Streamed-chat wire request construction + response parsing for the chat client
_chat/transport.py Chat-specific error mapping over the shared transport pipeline
_chat/deleted_tracker.py Bounded RecentlyDeletedConversations set — delete_conversation records the id (under the conversation lock) so a concurrent null-conversation ask, after acquiring that lock, detects a mid-flight delete and drops resolved_id_override to recover the server's real conversation id post-POST (#1875)
_middleware/chain.py Constructs the middleware chain in the canonical ADR-0009 order
_middleware/*.py Modular middleware implementations (drain, metrics, semaphore, retry, auth, error injection, tracing)
rpc/types.py RPC method IDs (source of truth)
auth.py Authentication facade — almost pure re-exports (the only remaining function body is async def enumerate_accounts, which binds _poke_session as a default dependency; ADR-0003 records the optional-async audit command). Every other top-level name forwards from the relevant _auth/* module: auth._validate_required_cookies is identity-equal to _auth.cookie_policy._validate_required_cookies, and load_auth_from_storage / AuthTokens live in _auth/tokens.py. ADR-0003's flat-re-export goal was closed by ADR-0014. Tests that need to rebind policy names patch _auth.cookie_policy.X directly.
_auth/paths.py Storage paths and filesystem helpers
_auth/extraction.py Cookie/token extraction from browser sessions
_auth/headers.py HTTP header construction
_auth/cookies.py Cookie map manipulation + _update_cookie_input
_auth/cookie_policy.py Cookie-domain allowlist, build_cookie_domain_allowlist builder, and policy decisions
_auth/browser_capture.py Transport-neutral browser launch→capture→filter→persist core (lazy playwright); shared by the interactive CLI login adapter (cli/services/playwright_login.py) and the layer-3 headless re-auth layer (ADR-0021)
_auth/headless_reauth.py Layer-3 headless re-auth decision layer: opt-in/profile-gated, typed honest outcomes (HeadlessReauthStatus); drives run_browser_capture(headless=True, interactive=False). Local-unattended-only
cli/label_cmd.py label command group (list/sources/generate/create/rename/emoji/add/remove/delete); thin Click shells over client.labels, _app.labels, and the label-listing service (ADR-0008/0021)
cli/services/label_listing.py label CLI service: the label list members→source-titles join (execute_label_list/LabelListPlan). Re-exports resolve_label_id + LabelResolutionError from _app/labels.py (the composite <id|name> resolver moved to the neutral layer; the re-export keeps from .services.label_listing import resolve_label_id resolving for the command layer + tests)

Repository Structure

src/notebooklm/
├── __init__.py                  # Public exports
├── __main__.py                  # `python -m notebooklm` entry point
├── client.py                    # NotebookLMClient
├── auth.py                      # Authentication facade — almost pure re-exports (`enumerate_accounts` exception; ADR-0003 flat-re-export goal closed by ADR-0014; see file table above)
├── types.py                     # Dataclasses
├── artifacts.py                 # Public artifact-generation retry helpers
├── config.py                    # Public config facade over _env
├── exceptions.py                # Public exception hierarchy
├── io.py                        # Public atomic-I/O facade for CLI boundary compliance
├── log.py                       # Public logging helper facade
├── migration.py                 # Legacy flat-layout to profile migration
├── paths.py                     # Profile-aware path resolution
├── research.py                  # Public research citation/report helpers
├── urls.py                      # Public URL helper facade
├── utils.py                     # Public async utility helpers
├── _atomic_io.py                # Atomic JSON write/update helpers
├── _auth_refresh_retry.py       # Shared auth refresh-and-retry core (RefreshBudget + refresh_and_count) for both retry layers
├── _backoff.py                  # Shared retry backoff calculation
├── _callbacks.py                # Sync/async callback invocation helper
├── _client_assembly.py          # Shared client-assembly seam (constructor + test factory)
├── _client_composed.py          # Client-owned composition holder
├── _client_seams.py             # Constructor-only injectable seams
├── _deadline.py                 # RuntimeDeadline helper for aggregate timeouts
├── _deprecation.py              # Deprecation helper (warn_deprecated) gated by NOTEBOOKLM_QUIET_DEPRECATIONS
├── _env.py                      # Runtime environment/default endpoint helpers
├── _idempotency.py              # Mutating-RPC idempotency registry + wrappers
├── _idempotency_policy.py       # Declarative per-RPC idempotency classification data (register_default_policies)
├── _kernel.py                   # Concrete Kernel transport core
├── _logging.py                  # Redaction + correlation logging internals
├── _secrets.py                  # Canonical runtime secret registry (cookie names + secure/host umbrellas + token/API-key shapes) the redaction patterns derive from
├── _lookup.py                   # unwrap_or_raise — shared single-row-lookup helper for get/get_or_none
├── _serving.py                  # Shared bootstrap for both HTTP entry points: single-source loopback classification (IPv4-mapped-IPv6-aware) + non-loopback bind guard (mcp/server __main__ + server/_auth all route through it)
├── _loop_affinity.py            # Event-loop affinity guard helper (assert_bound_loop free function)
├── _loop_bound.py               # LoopBoundPrimitive mixin — template-method set_bound_loop + _on_loop_rebind hook for the loop-bound collaborators
├── _error_injection.py          # Synthetic-error env-var resolver + startup guard
├── _request_types.py            # AuthSnapshot, BuildRequest, PostBody, request materialization helpers
├── _transport_errors.py         # Transport exceptions, Retry-After parsing, Kernel.post error mapping
├── _streaming_post.py           # Size-capped streaming POST helper
├── _curl_cffi_transport.py      # Opt-in curl_cffi browser-impersonation transport (NOTEBOOKLM_TRANSPORT=curl_cffi)
├── _rpc_executor.py             # RPC dispatch executor
├── _client_metrics.py           # Telemetry / metrics seam
├── _transport_drain.py          # In-flight transport drain coordinator
├── _reqid_counter.py            # Request-counter / request-id helpers
├── _conversation_cache.py       # Per-instance true-LRU conversation cache (bounded conversation count + per-conversation turns)
├── _polling_registry.py         # Artifact polling helpers
├── _cookie_persistence.py       # Cookie-jar persistence + __Secure-1PSIDTS rotation
├── _note_service.py             # NoteService
├── _mind_map.py                 # NoteBackedMindMapService
├── _mind_maps_api.py            # MindMapsAPI — unified mind-map surface over both backends (#1256)
├── _notebook_metadata.py        # Metadata protocols
├── _url_utils.py                # URL validation helpers
├── _sharing_manager.py          # Sharing management logic
├── _version_check.py            # Deprecation version guard
├── _version_info.py             # version_string(): version + short git commit
├── _research_task_parser.py     # Research task result-type parser
├── _redact.py                   # Transport-neutral secret/home-path/file-link scrubber (redact(msg, max_length)); shared chokepoint under both mcp/_errors.py and server/_errors.py
├── _app/                        # Transport-neutral business-logic layer (CLI/MCP/HTTP adapters share it)
│   ├── __init__.py              # Re-exports the neutral primitives
│   ├── artifacts.py             # Click-free artifact core: get/rename/delete/export + poll/wait/retry; kind-aware mind-map dispatch (mind_maps.list for rename, notes.list_mind_maps for delete), get_artifact raises ArtifactNotFoundError, typed Rename/Export results + ArtifactStatusView/status_view neutral status DTO (CLI builds every --json envelope from the typed fields)
│   ├── auth_check.py            # Click-free `auth check` diagnostics core: run_auth_check(plan, read_env_auth_json=…) -> AuthCheckResult (storage-exists/json-valid/cookies-present/SID + optional token-fetch); AuthCheckPlan carries pre-resolved values + the auth_source display label; inline-auth read injected (CLI owns the AuthSource plan-build + Rich table + exit code)
│   ├── chat.py                  # Click-free chat core: conversation-id selection ladder + configure mode/goal/length dispatch + history fetch/format-as-data + ask save-as-note workflow (raises public ValidationError; status emitted into injected ProgressSink)
│   ├── doctor.py                # Click-free doctor core: run_checks(*, fix, paths) -> DoctorReport (five checks incl. headless-reauth readiness + fixes + has_failures; DoctorPaths injects the path helpers; CLI owns rendering/exit codes)
│   ├── download.py              # Click-free download core: DownloadPlan/Result/TypeSpec + build_download_plan/execute_download (injected resolvers; CLI builds the --json envelope from the typed DownloadResult)
│   ├── errors.py                # classify(exc) -> ClassifiedError (category + retriable); class-sensitive
│   ├── events.py                # ProgressEvent + ProgressSink Protocol (neutral progress seam)
│   ├── generate.py              # Click-free `generate` executor: execute_generation (injected notebook/source resolvers preserve the RPC fast paths) + GenerationExecutionResult; re-exports the plan/retry surface so `_app.generate` is the single import point
│   ├── generate_plans.py        # Click-free `generate` plan-building: enum/format maps, GenerationPlan/GenerationKind/GenerationPlanValidationError, build_generation_plan + per-kind builders (parameter_explicit/language_resolver injected)
│   ├── generate_retry.py        # Click-free `generate` retry/wait: GenerationOutcome, generate_with_retry, handle_generation_result, status extractors, spinner status-line formatter (wait_context/wait_start_sink neutral seams)
│   ├── labels.py                # Click-free label core: create/sources/generate/rename/emoji/add/remove/delete + the composite resolve_label_id (<id|name>) resolver + LabelResolutionError (injected notebook/source resolvers; members→titles JOIN render stays in cli/services/label_listing.py)
│   ├── language.py              # Click-free language core: SUPPORTED_LANGUAGES catalog + is_supported_language + LanguageConfigStore (injected config-path/home/atomic-update; get/save/get_language/set_language)
│   ├── mcp_install.py           # Click-free `mcp install <client>` core: supported-client catalog (claude-desktop/claude-code/cursor/windsurf) + per-OS resolve_config_path + uvx build_server_block + merge_server_config read-modify-merge into mcpServers (created/updated/unchanged; never clobbers unrelated keys); UnsupportedClientError. CLI owns the atomic write (cli/mcp_cmd.py)
│   ├── notebooks.py             # Click-free notebook core: create/delete/rename/describe(summary)/metadata fetch+compute (injected resolve_notebook_id; summary/metadata serializers stay in cli/notebook_cmd.py)
│   ├── notes.py                 # Click-free note core: create/get/save/rename/delete (typed-facade only — notes.create returns a Note) + content-preserving rename (resolve_note_content); found-flag results map to the CLI NOT_FOUND/exit-1 path (injected notebook/note resolvers)
│   ├── pagination.py            # Transport-neutral bounded-slice paginate(items, limit, offset) -> (page, {total,offset,has_more}) with bound validation; the shared slice under both the MCP *_list tools and the REST list-route envelope (Option B-lite)
│   ├── profile.py               # Click-free profile core: gather_profile_list -> ProfileEntry rows (injected list_profiles/resolve_profile/get_storage_path/read_account_metadata), is_protected_profile delete-guard decision, set_default/retarget_default config.json mutators (CLI keeps the locked _atomic_write_config + click.confirm + Rich render)
│   ├── research.py              # Click-free `research` status/wait core: poll_and_classify -> ResearchStatusResult, ResearchWaitPlan/Result + execute_research_wait (resolver/importer/wait-context injected), validate_research_wait_flags (-> ValidationError); returns typed results only (CLI owns the --json envelope)
│   ├── resolve.py               # Click-free validate_id + resolve_ref (AmbiguousIdError/Resolution)
│   ├── serialize.py             # to_jsonable(obj) recursive JSON-able conversion (enum-before-primitive)
│   ├── session.py               # Click-free session-context core: `use` verify_and_set_notebook (injected resolve_notebook_id) + `status` read_status(StatusInputs) read+project -> StatusReport + `auth logout` execute_logout(LogoutInputs) filesystem-teardown -> typed LogoutOutcome (path/context/clear_context helpers injected via bundles; CLI owns Rich render + exit codes)
│   ├── sharing.py               # Click-free sharing core: status/set_public/set_view_level/add_user/update_user/remove_user (injected resolve_notebook_id; permission/view-level display + str→enum parse stay in cli/share_cmd.py)
│   ├── skill.py                 # Click-free skill-install core: TARGETS/SCOPES catalog + path/version helpers + classify_target (create/up_to_date/overwrite) + report_mixed_no_clobber_up_to_date (CLI owns the atomic write + packaged-source loader)
│   ├── source_add.py            # Click-free `source add` core: input detection + URL SSRF/upload-path validation + add workflow (SourceAddPlan/Result; CLI builds the --json source-summary from the typed result via the neutral serialize.source_summary helper)
│   ├── source_batch.py          # Transport-neutral batch-add policy shared by the MCP tool + REST route (#1871): MAX_BATCH_URLS cap + batch_item_is_fatal (fatal auth/rate-limit/5xx classification via _app.errors.classify → abort the batch; per-URL 4xx-input failures isolate). Neutral frozenset, not server's CATEGORY_STATUS (the _app boundary forbids fastapi); parity pinned by tests/server/test_source_batch_parity.py
│   ├── source_clean.py          # Click-free `source clean` core: junk-source classification + batched-deletion orchestration (SourceCleanResult; injected list/delete/confirm callables)
│   ├── source_content.py        # Click-free read-only source-content fetchers for get/fulltext/guide/stale (typed plan/result pairs)
│   ├── source_listing.py        # Click-free `source list` fetch core: fetch_sources (label_filter resolution; label_resolver injected)
│   ├── source_mutations.py      # Click-free source delete/delete-by-title/rename/refresh/add-drive core: resolvers + SourceMutationError + typed results (validate_id/resolve_source_id injected; confirmer injected)
│   ├── source_research.py       # Click-free `source add-research` start/wait/import workflow + validate_add_research_flags (importer injected; SourceAddResearchPlan/Result)
│   ├── source_wait.py           # Click-free `source wait` readiness-poll core: execute_source_wait + typed SourceWaitOutcome (wait_context injected) + wait_all_sources (single-snapshot loop via client.sources.wait_all_until_ready — one notebook poll per tick, order-preserving; #1870) shared by the MCP tool + REST route (#1871) + the MAX_WAIT_TIMEOUT / MAX_WAIT_SOURCE_IDS caps
│   └── views.py                 # Transport-neutral output-projection views: share_status_view (access/permission/view_level enum→label), source_view (kind/status_label added), ask_result_view (raw_response debug blob stripped); shared by the MCP tools + REST routes so both emit the identical enriched shape (Option B)
├── _runtime/                    # Client-runtime subpackage (promoted from flat _runtime_*.py, #1328)
│   ├── __init__.py              # Re-exports the cluster's public names
│   ├── auth.py                  # AuthRefreshCoordinator (refresh task + auth-snapshot lock)
│   ├── config.py                # DEFAULT_* knobs + module-level constants
│   ├── contracts.py             # Shared runtime Protocols consumed by feature APIs
│   ├── helpers.py               # is_auth_error / AUTH_ERROR_PATTERNS / keepalive helpers
│   ├── init.py                  # Runtime collaborator construction + validation
│   ├── lifecycle.py             # Open/close lifecycle seam (loop affinity + keepalive task)
│   └── transport.py             # Middleware-chain transport wrapper
├── _middleware/                 # Middleware subpackage (promoted from flat _middleware*.py, #1328)
│   ├── __init__.py              # Re-exports the cluster's public names
│   ├── core.py                  # Middleware envelope + Protocol + chain composition primitive (was _middleware.py)
│   ├── context.py               # Middleware context-key vocabulary
│   ├── chain.py                 # Middleware chain builder
│   ├── chain_host.py            # Live middleware chain slots and retry tunables
│   ├── tracing.py               # Tracing middleware
│   ├── metrics.py               # Metrics middleware
│   ├── drain.py                 # Drain middleware
│   ├── error_injection.py       # Error injection middleware
│   ├── retry.py                 # Retry middleware
│   ├── auth_refresh.py          # Auth refresh middleware
│   └── semaphore.py             # Concurrency semaphore middleware
├── _source/                     # Source-feature subpackage (promoted from flat _source_*.py, #1328)
│   ├── __init__.py              # Re-exports the cluster's public service classes
│   ├── _upload_decode.py        # Pure URL/source-id/content-type decode + validation helpers (extracted from upload.py)
│   ├── add.py                   # Source addition coordinator
│   ├── content.py               # Source content fetcher
│   ├── listing.py               # Source listing helper
│   ├── polling.py               # Source polling coordinator
│   ├── upload.py                # Gated source upload service
│   └── upload_payloads.py       # Source upload request payload builders
├── _artifact/                   # Artifact-feature subpackage (promoted from flat _artifact_*.py, #1328)
│   ├── __init__.py              # Re-exports the cluster's public service classes/builders
│   ├── _download_client.py      # Download trusted-host allowlist + transport-aware client factory (httpx event hook / curl_cffi get_guarded)
│   ├── _redirect_guard.py       # Per-redirect-hop host/scheme revalidation for downloads (#1521)
│   ├── downloads.py             # Artifact download coordinator
│   ├── formatters.py            # Artifact formatting helpers
│   ├── generation.py            # Artifact generation kickoff service (generate_*, revise_slide, retry_failed)
│   ├── payloads.py              # Stable artifact request payload builders
│   ├── validation.py            # Facade input-validation guards (generate_report coercion, export exactly-one-of) (#1874)
│   ├── listing.py               # Artifact listing helper
│   └── polling.py               # Artifact polling coordinator
├── _label/                      # Source-label feature subpackage: stable RPC payload builders
│   ├── __init__.py              # Re-exports the label param builders
│   └── params.py                # Source-label RPC payload builders (CREATE/LIST/UPDATE/DELETE_LABEL)
├── _row_adapters/               # Positional-RPC-row adapters subpackage (promoted from flat _row_adapters_*.py, #1328)
│   ├── __init__.py              # Re-exports the typed row views
│   ├── artifacts.py             # Artifact + GET_SUGGESTED_REPORTS row adapters (ArtifactRow / ReportSuggestionRow)
│   ├── chat.py                  # Streamed-chat row adapters (AnswerRow / CitationRow / CitationDetail / PassageRow / StreamFrameRow / ErrorPayloadRow / TextLeafRow) — closes the chat positional-decode perimeter (#1491)
│   ├── labels.py                # Source-label row adapter
│   ├── notebooks.py             # SUGGEST_PROMPTS suggestion-row adapter (PromptSuggestionRow / unwrap_prompt_suggestions)
│   ├── notes.py                 # Note and mind-map row adapter
│   ├── research.py              # POLL_RESEARCH row adapters (ResearchTaskRow / ResearchTaskInfoRow / ResearchResultRow) — drains the research parser's single-level positional reads (#1501)
│   └── sources.py               # Source row adapter
├── _chat/                       # Chat-feature subpackage — facade + helpers unified (#1328)
│   ├── __init__.py              # Re-exports ChatAPI so `from ._chat import ChatAPI` keeps resolving
│   ├── api.py                   # ChatAPI facade (was _chat.py)
│   ├── notes.py                 # Note saving workflow adapter
│   ├── wire.py                  # Streamed-chat wire request/response parser
│   ├── transport.py             # Chat error mapping
│   └── deleted_tracker.py       # Bounded RecentlyDeletedConversations set — serializes null-ask vs delete (#1875)
├── _auth/                       # Auth subpackage (forwarded through auth.py facade)
│   ├── __init__.py
│   ├── paths.py                 # Storage paths and filesystem helpers
│   ├── extraction.py            # Cookie/token extraction from browser sessions
│   ├── headers.py               # HTTP header construction
│   ├── cookies.py               # Cookie maps + _update_cookie_input
│   ├── cookie_policy.py         # Domain allowlist + cookie-domain builder and policy
│   ├── browser_capture.py       # Transport-neutral browser launch→capture→filter→persist core (lazy playwright)
│   ├── headless_reauth.py       # Layer-3 headless re-auth (opt-in; typed outcomes; local-unattended-only)
│   ├── account.py               # Account profile + multi-account switching
│   ├── session.py               # Auth-session refresh implementation via `refresh_auth_session()` and explicit collaborators
│   ├── storage.py               # Profile/state persistence on disk
│   ├── keepalive.py             # Cookie keepalive + __Secure-1PSIDTS rotation
│   ├── psidts_recovery.py       # Inline PSIDTS recovery for cold-start (issue #865)
│   ├── master_token.py          # Headless master-token auth: mint cookies + layer-4 re-mint (ADR-0023)
│   ├── refresh.py               # Token refresh driver (external login cmd, coalesced runs, redaction)
│   └── tokens.py                # AuthTokens container + load_auth_from_storage loader
├── _types/                      # Dataclass implementation package re-exported by types.py
│   ├── __init__.py
│   ├── artifacts.py
│   ├── chat.py
│   ├── common.py
│   ├── labels.py                # Label pure-value type (source membership; no kind/artifact_ids)
│   ├── mind_maps.py             # MindMap + MindMapKind pure-value types (#1256)
│   ├── notebooks.py
│   ├── notes.py
│   ├── research.py              # ResearchStatus enum + ResearchTask/ResearchSource/ResearchStart/MindMapResult/SourceGuide typed returns (#1209)
│   ├── sharing.py
│   └── sources.py
├── _notebooks.py                # NotebooksAPI
├── _notebook_payloads.py        # batchexecute notebook RPC payload builders (SUGGEST_PROMPTS)
├── _sources.py                  # SourcesAPI
├── _artifacts.py                # ArtifactsAPI
├── _research.py                 # ResearchAPI
├── _notes.py                    # NotesAPI
├── _sharing.py                  # SharingAPI
├── _settings.py                 # SettingsAPI
├── _labels.py                   # LabelsAPI — client.labels (source labels: generate/create/list/…)
├── notebooklm_cli.py            # Entry-point assembler — imports + registers cli/ groups
├── mcp/                         # MCP server (opt-in `mcp` extra) — transport-neutral adapter over _app/, sibling to cli/
│   ├── __init__.py              # Re-exports create_server / SERVER_NAME / SERVER_INSTRUCTIONS
│   ├── __main__.py              # `notebooklm-mcp` entrypoint: argparse (--profile/--transport/--host/--port/--log-level), stderr logging, loopback HTTP bind guard + fail-closed auth guard (non-loopback bind requires a bearer token AND/OR self-hosted OAuth); composes the auth provider (build_auth) and passes it to create_server on the http path
│   ├── server.py                # create_server(profile, client_factory, auth): FastMCP server; lifespan binds one NotebookLMClient; register_all tool-registration seam; auth passed explicitly (never reads the token env)
│   ├── _auth.py                 # Remote-transport bearer auth: McpBearerAuthProvider(TokenVerifier) with constant-time hmac.compare_digest over NOTEBOOKLM_MCP_TOKEN (env-only, never logged/repr'd); build_auth_provider/get_configured_token; build_auth(token, oauth) composes bearer | OAuth | MultiAuth | None (IdP-agnostic) — mirrors server/_auth.py, NOT fastmcp StaticTokenVerifier
│   ├── _oauth.py                # Optional self-hosted OAuth 2.1 AS for claude.ai (OAuth-only connector UI): SelfHostedOAuthProvider(InMemoryOAuthProvider) + a password-gated /login (override authorize()→stash SDK-validated (client,params) under a single-use sid→/login→InMemoryOAuthProvider.authorize); scrypt password digest + per-IP throttle, capped DCR + evict-oldest pending stash, atomic 0600 persistence of clients+tokens; get_oauth_config/build_oauth_provider (env NOTEBOOKLM_MCP_OAUTH_PASSWORD + _BASE_URL). Composed with the bearer via MultiAuth
│   ├── _host_guard.py           # LoopbackHostGuardMiddleware: ASGI guard that rejects HTTP requests with a non-loopback Host header (403; DNS-rebinding guard, #1869) on the loopback-bound HTTP transport via _serving.host_header_is_loopback; skipped when allow_external (REST-parity bearer/OAuth auth is mandatory there) — mirrors server/_auth
│   ├── _urlcheck.py             # _validate_bare_https_origin(url, env) — shared "bare public https origin" check (https scheme, host, no path/query/fragment); guards the OAuth base URL AND the file-transfer public URL so a /mcp-suffixed/non-https value can't mint broken links
│   ├── _filelink.py             # HMAC-signed self-describing file-transfer tokens (ADR-0024): FileLinkSigner.sign(payload, ttl→injects exp)/verify(token, op) (stdlib hmac/base64/json; pre-decode length cap, base64url re-pad, compare_digest, exp+op check) + FileTransferConfig(signer, base_url).upload_url/download_url (UPLOAD_TTL 15m / DOWNLOAD_TTL 30m); FileLinkError
│   ├── _fileroutes.py           # register_file_routes(mcp, config): the /files/{dl,ul} custom routes mounted on the FastMCP http app (ADR-0024). GET /files/dl streams the artifact (download core → FileResponse, meaningful filename, inside-tempdir assert, BackgroundTask cleanup); GET /files/ul = minimal upload page (file picker + raw-body fetch POST); POST|PUT /files/ul streams request.stream() into a 0600 temp under a running byte cap (real DoS guard) + Content-Length early 413 → neutral source_add core. Signed token is the sole auth (custom routes bypass the bearer gate); HTML pages set no-referrer/no-store/DENY; local _safe_upload_name (no server/ import)
│   ├── _context.py              # AppState dataclass (client + optional file_transfer) + get_client(ctx) / get_file_transfer(ctx) (lifespan-bound) + get_client_from_app(request) (the guarded private-attr accessor for the bare-Request custom routes)
│   ├── _errors.py               # Structured tool-error projection (CATEGORY_TABLE/ERROR_CODES/mcp_errors/to_tool_error/tool_error_payload) over _app.errors.classify
│   ├── _resolve.py              # resolve_notebook/resolve_source/resolve_note/resolve_artifact — name + partial-id resolution over _app.resolve plus exact-title matching
│   ├── _confirm.py              # needs_confirmation() both-mode envelope + READ_ONLY/DESTRUCTIVE ToolAnnotations
│   ├── _coerce.py               # coerce_list(value) — tolerant list-param normalizer (real list/tuple, JSON-array string, comma string, scalar → list[str]; None stays None for the "all sources" contract); used by studio_generate/chat_ask source_ids
│   ├── _paginate.py             # paginate(items, limit) — bounded page + {total, has_more} for the *_list tools (client-side slice; RPCs don't page); DEFAULT_LIMIT=50
│   └── tools/                   # Per-domain tool modules; each exposes register(mcp) wired by server.register_all
│       ├── __init__.py          # Tools package marker (no click/rich/cli)
│       ├── _content_sanity.py   # _annotate_thin_warnings/_thin_content_warning — advisory thin/soft-404 web-page warning over _app.source_content (used by source_wait + source_add batch)
│       ├── _fileupload.py       # file-transfer slice of the source tools: _broker_upload (signed-URL upload_required) + _decode_upload_b64/_add_bytes (in-channel base64 byte upload for source_upload_bytes) + the shared _add_one plan/execute seam (split from sources.py for the ADR-0008 size budget)
│       ├── _passthrough.py      # Shared pass-through resolvers (passthrough_notebook_id/passthrough_child_id) for the CLI-shaped _app executors
│       ├── _preview.py          # title_for_id() — shared id→title lookup for the delete tools' needs_confirmation previews
│       ├── _studio_items.py     # cross-type Studio plumbing: studio_items (merge notes+artifacts into one items list) + resolve_studio_item (cross-type ref → StudioResolvedItem) for studio_list/studio_rename/studio_delete (split from studio.py for the ADR-0008 size budget)
│       ├── _studio_download.py  # download plumbing shared by studio.py + _fileroutes.py: _DOWNLOAD_SPECS registry (rebuilt from _app.download) + DownloadType + _resolve_artifact_id / _broker_download / _is_http_transport / _passthrough_download_notebook (split from studio.py for the ADR-0008 size budget)
│       ├── _waitagg.py          # source-wait outcome aggregation shared by source_wait + source_add_and_wait: _wait_all_sources (concurrent per-source wait) + _aggregate_wait_outcomes (typed SourceWaitOutcome → {ok, ready, timed_out, failed, not_found} + thin-warning annotation) (split from sources.py for the ADR-0008 size budget)
│       ├── notebooks.py         # notebook_list/create/describe/rename/delete over _app.notebooks
│       ├── sources.py           # source_list/read/rename/delete/wait/add over _app.source_* (add: url/text/file/youtube via source_add, drive via source_mutations) + source_add_and_wait (single-mode add + wait composed via _waitagg) + source_upload_bytes (in-channel small-file byte upload via _fileupload)
│       ├── chat.py              # chat_ask (client.chat.ask + get_history recall + suggest_followups) + chat_configure (_app.chat.execute_configure) + suggest_prompts (client.notebooks.suggest_prompts surface selector)
│       ├── notes.py             # note_save (create-or-update upsert) over _app.notes; note reading/renaming/deleting fold into the cross-type Studio tools
│       ├── studio.py            # hosts the Studio tools: studio_list (merges notes+artifacts via _studio_items.studio_items) / generate / status / download (via _studio_download) / rename / retry / get_prompt / studio_delete — both rename and delete are cross-type via _studio_items.resolve_studio_item (note→_app.notes.execute_note_rename/execute_note_delete, artifact→_app.artifacts kind-aware core); enum dispatch over _app.generate + _app.download; stateless poll via _app.artifacts.poll_artifact
│       ├── research.py          # research_start (client.research.start) + research_status (_app.research.poll_and_classify) + research_import
│       ├── sharing.py           # share_status/set_access/set_user/remove_user (thin adapters over client.sharing; set_access folds public+view_level, set_user upserts add/update; string-labeled enums; view_level surfaced only when set)
│       └── meta.py              # server_info — package version + auth-health over _app.auth_check (no notebook arg)
├── rpc/                         # RPC protocol layer
│   ├── types.py                 # Method IDs and enums
│   ├── encoder.py               # Request encoding
│   ├── decoder.py               # Response parsing
│   ├── _safe_index.py           # Strict bounds-checked positional access for decoded RPC payloads
│   └── overrides.py             # Runtime RPC ID override policy (env-driven)
├── cli/                         # CLI implementation
    ├── __init__.py              # Re-exports click groups under historical names from *_cmd modules
    ├── _chromium_profiles.py    # Multi-user-data-profile cookie extraction for Chromium browsers
    ├── _cookie_import.py        # `auth import-cookies` helpers: parse/normalize/validate cookie JSON + backup-then-atomic-write storage_state
    ├── _download_specs.py       # Registry data for `download <type>` leaf commands
    ├── _encoding.py             # Encoding-safe CLI output helpers
    ├── _firefox_containers.py   # Container-aware Firefox cookie extraction
    ├── _session_render.py       # Session-command render helpers (status/auth tables)
    ├── _source_render.py        # Source CLI render/validation helpers (extracted from source_cmd.py)
    ├── agent_cmd.py             # agent show commands
    ├── agent_templates.py       # agent prompts and configurations
    ├── artifact_cmd.py          # artifact commands
    ├── auth_runtime.py          # CLI authentication + command runtime helpers
    ├── chat_cmd.py              # ask, configure, history
    ├── completion.py            # Best-effort shell-completion providers for live IDs
    ├── context.py              # CLI context persistence helpers
    ├── doctor_cmd.py            # diagnostic/repair tool
    ├── download_cmd.py          # download commands
    ├── download_helpers.py      # Helper functions for download commands
    ├── error_handler.py         # Centralized CLI error handling
    ├── generate_cmd.py          # generate audio, video, etc.
    ├── grouped.py               # Custom Click group with sectioned help output
    ├── helpers.py               # Shared Click utilities
    ├── input.py                 # CLI prompt and stdin input helpers
    ├── label_cmd.py             # label list/sources/generate/create/rename/emoji/add/remove/delete
    ├── language_cmd.py          # Language configuration CLI commands
    ├── master_token_login.py    # Command driver for `login --master-token[-refresh]` (ADR-0023)
    ├── mcp_cmd.py               # `mcp install <client>` command — thin Click adapter over `_app/mcp_install.py`; resolves the client config path (`--config-path` override) and applies the merge inside `notebooklm.io.atomic_update_json` (locked, crash-safe, merge-not-clobber)
    ├── notebook_cmd.py          # list, create, delete, rename
    ├── note_cmd.py              # note commands
    ├── options.py               # Shared CLI option decorators
    ├── playwright_login_io.py   # Command-side LoginIO sink + wrappers for the Playwright login service (#1391)
    ├── polling_ui.py            # Command-layer UI helpers for long-running polling
    ├── profile_cmd.py           # Profile management CLI commands
    ├── rendering.py             # CLI rendering helpers
    ├── research_cmd.py          # Research management CLI commands
    ├── research_import.py       # Research import helpers shared by CLI commands
    ├── resolve.py               # CLI notebook/entity ID resolution helpers
    ├── runtime.py               # CLI runtime primitives
    ├── session_cmd.py           # login, use, status, clear
    ├── share_cmd.py             # Sharing management CLI commands
    ├── skill_cmd.py             # Skill management commands
    ├── source_cmd.py            # source add, list, delete
    └── services/                # CLI-specific service layer (ADR-0008 Click-to-service extraction)
        ├── __init__.py
        ├── auth_diagnostics.py  # `auth check` CLI adapter over `_app/auth_check.py` — re-exports AuthCheckPlan/Result; builds the plan from the AuthSource Click-context precedence (plan_from_click_context + the auth_source display label) and injects read_env_auth_json into the neutral run_auth_check
        ├── auth_source.py       # Single source of truth for the active CLI auth source (Click-context precedence resolver; stays in cli/ — reads ctx.obj + NOTEBOOKLM_AUTH_JSON)
        ├── confirming_mutation.py # Shared confirmed-mutation pipeline for CLI resources
        ├── download.py          # CLI adapter over _app/download.py: re-exports plan types, injects cli.resolve resolvers (keeps resolve_notebook_id patch seam), projects DownloadResult → envelope dict
        ├── generate.py          # `generate` CLI adapter over `_app/generate.py` — re-exports plan/result/error + build_generation_plan; injects cli.resolve resolve_notebook_id/resolve_source_ids (read at call time, preserving the resolve_module monkeypatch seam) into the neutral execute_generation; re-exports _INFOGRAPHIC_STYLE_MAP from `_app/generate_plans.py` for generate_cmd
        ├── label_listing.py     # `label list` members→titles join service; re-exports resolve_label_id + LabelResolutionError from _app/labels.py
        ├── listing.py           # Shared list-command pipeline for CLI resources
        ├── login/               # Browser-cookie login helper package
        │   ├── __init__.py      # re-export-only patch surface
        │   ├── browser_accounts.py
        │   ├── chromium_accounts.py
        │   ├── cookie_domains.py
        │   ├── cookie_jar.py
        │   ├── cookie_writes.py
        │   ├── exceptions.py
        │   ├── firefox_accounts.py
        │   ├── io_seam.py        # Caller-injected LoginIO Protocol + resolver (#1393)
        │   ├── master_token.py   # Headless master-token bootstrap/refresh + browser oauth_token capture (ADR-0023)
        │   ├── outcomes.py
        │   ├── profile_targets.py
        │   ├── refresh.py
        │   └── rookiepy_errors.py
        ├── playwright_login.py  # Playwright-driven Google login service
        ├── playwright_redaction.py # Subprocess-output redaction helpers for the Playwright login service
        ├── polling.py           # Shared polling helpers for CLI wait commands
        ├── research.py          # `research wait` CLI adapter over `_app/research.py` — re-exports plan/result/outcome; injects cli.resolve.resolve_notebook_id + cli.research_import.import_research_sources defaults (preserves their patch seams)
        ├── session_context.py   # Notebook-context CLI adapter over `_app/session.py` for `use`/`status`/`auth logout` — re-exports the typed result classes; builds the injected StatusInputs/LogoutInputs bundles from its own session_context-namespace path helpers (read at call time, preserving the get_context_path/get_storage_path/clear_context patch seams)
        ├── source_listing.py    # `source list` CLI adapter over `_app/source_listing.py` — owns the ListSpec/prepare_list presentation half; injects resolve_label_id into the neutral fetch_sources
        ├── source_mutations.py  # Source-mutation CLI adapter over `_app/source_mutations.py` — re-exports plan/result/error/helpers; injects cli.resolve validate_id + resolve_source_id (preserves the resolve_source_id monkeypatch seam) and the click.confirm confirmer
        ├── source_research.py   # `source add-research` CLI adapter — thin wrapper over `_app/source_research.py` (injects the rich-coupled importer; re-exports plan/result + validate_add_research_flags; preserves the import_research_sources monkeypatch seam)
        └── source_serializers.py # Shared JSON serializers for source CLI output
└── server/                      # Single-tenant REST API adapter (the third _app adapter, after cli/ and mcp/; behind the optional `server` extra). EXPERIMENTAL: /v1 surface may change, excluded from the api-compat gate. Imports no click/rich/cli.
    ├── __init__.py              # Re-exports create_app + SERVER_NAME; importing it without the `server` extra fails on the fastapi import
    ├── __main__.py              # `notebooklm-server` entry: argparse + NOTEBOOKLM_SERVER_* env defaults + loopback-bind guard + fail-closed token check
    ├── app.py                   # create_app(*, client_factory=None) -> FastAPI; ASGI lifespan binds one client; public /healthz; auth-gated /v1 mount (docs/redoc/openapi disabled)
    ├── _context.py              # AppState (lifespan-bound client + pending registry) + get_client / get_pending FastAPI dependencies
    ├── _limits.py               # Lifespan-owned REST route-group concurrency limiters for expensive source/chat/research/artifact work
    ├── _auth.py                 # Bearer-token (constant-time, 401) + loopback-Host (DNS-rebinding guard, 403) dependency for /v1
    ├── _errors.py               # ErrorCategory -> HTTP status table + _redact + the classify-once exception handler emitting {error:{category,message}}
    ├── _pagination.py           # Opt-in, non-breaking list-route envelope: paginate_envelope(items, key=…, limit, offset, **extra) — default (no limit) returns the full list under its existing key unchanged; ?limit= slices via _app.pagination.paginate + adds a meta:{total,has_more,limit,offset} block (Option B-lite)
    ├── _pending.py              # In-process pending-id registry (per-notebook provenance for poll -> 200-pending vs 404)
    └── routes/                  # Per-resource FastAPI routers; handlers call _app.serialize.to_jsonable directly
        ├── __init__.py          # Aggregates the resource routers for the app factory
        ├── _passthrough.py      # Pass-through resolvers handed to the _app cores (REST works in full ids)
        ├── notebooks.py         # /v1/notebooks list/get/create/rename(PATCH)/delete + GET /{id}/suggested-prompts (client.notebooks.suggest_prompts; surface→mode map pinned to MCP)
        ├── sources.py           # /v1/notebooks/{id}/sources list/get/add(url·text·file·drive·batch)/rename(PATCH)/wait/delete + poll-the-resource status
        ├── notes.py             # /v1/notebooks/{id}/notes list/get/create/update(PUT)/delete — thin adapter over client.notes
        ├── chat.py              # POST /v1/notebooks/{id}/chat — blocking ask (no SSE) + POST /chat/configure over _app.chat.execute_configure
        ├── artifacts.py         # /v1/notebooks/{id}/artifacts list/generate/poll/download/rename(PATCH)/retry/delete + GET /{id}/prompt (per-kind generate-option validation pinned to core maps; registry-projected poll; server-generated temp download path)
        ├── research.py          # /v1/notebooks/{id}/research start(202)/status/cancel/import — split-tool shape over client.research + _app.research.poll_and_classify (poll_id = report_id or task_id)
        ├── share.py             # /v1/notebooks/{id}/share status/public/users/view-level over _app.sharing
        └── meta.py              # GET /v1/server/info — version + local auth-health probe (run_auth_check) + opt-in account block; scrubs the on-disk storage path (mirrors MCP server_info)

ADR cross-references

  • ADR-0001 — Layered seams + property-bridge policy (superseded; shims retired).
  • ADR-0002 — Capability Protocol pattern (Superseded by ADR-0013).
  • ADR-0003auth.py write-through facade (Superseded — closed by ADR-0014; auth.py is now almost pure re-exports with enumerate_accounts as the sole function-body exception).
  • ADR-0004 — Loop-affinity contract (Accepted; enforced by _loop_affinity.assert_bound_loop).
  • ADR-0005 — Mutating-RPC idempotency taxonomy (Accepted; enforced by _idempotency.IdempotencyRegistry).
  • ADR-0006 — VCR cassette scrubber strategy (Accepted).
  • ADR-0007 — Constructor-injection test pattern via tests/_fixtures/ (Accepted; enforced by tests/_guardrails/test_no_forbidden_monkeypatches.py).
  • ADR-0008cli/services/ extraction pattern (Accepted).
  • ADR-0009 — Middleware chain ordering (Accepted; load-bearing).
  • ADR-0010 — Session/Kernel split (Superseded by ADR-0013).
  • ADR-0011 — Schema validation policy (Accepted; safe_index is the canonical decode helper).
  • ADR-0012 — Implementation surface convention (Accepted; underscore-prefix = unsupported import surface).
  • ADR-0013 — Composable Session Capabilities (the composable session-capability model).
  • ADR-0014 — Feature-local runtime adapters (Accepted; features receive direct collaborators instead of Session).
  • ADR-0015 — Typed JSON error envelope for post-parse CLI failures (Accepted).
  • ADR-0016 — Auth identity + core logger compatibility (Accepted).
  • ADR-0017 — Public-facade / private-implementation re-export convention (Accepted).
  • ADR-0018 — Deprecation strategy (Accepted).
  • ADR-0019 — Error-and-return contract for the public API (Accepted; the breaking half shipped in v0.8.0).
  • ADR-0020 — Sealed async result types for artifact generation (Accepted).
  • ADR-0021 — Transport-neutral application layer (_app/) (Accepted; boundary enforced by tests/_guardrails/test_app_boundary.py, classify↔error_handler agreement by tests/_guardrails/test_classify_error_handler_consistency.py).
  • ADR-0022 — Regenerable test baselines (Accepted).
  • ADR-0023 — Master-token headless auth (Accepted; the L4 unattended re-mint path, [headless] extra).
  • ADR-0024 — Remote-MCP file transfer via signed-URL side-channel (Accepted).
  • ADR-0025 — MCP tool granularity (Accepted).
  • ADR-0026 — MCP Studio surface — notes + artifacts unified (Accepted).

See also