chore: import upstream snapshot with attribution
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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:30:13 +08:00
commit 09e9f3545f
1231 changed files with 649127 additions and 0 deletions
+61
View File
@@ -0,0 +1,61 @@
# `otmP3b` (GeneratePromptSuggestions) mode → surface map
Evidence record for the `suggest_prompts` mode enum (field-4 `C0` of the
`SUGGEST_PROMPTS` / `otmP3b` request). Backs the MCP `suggest_prompts` tool's
`surface` labels and the client cap (`_notebook_payloads._PROMPT_SUGGESTIONS_MODE_MAX = 10`).
Investigated live 2026-07-01 (#1726); supersedes the earlier output-only #1612 guess.
## Map
| mode | surface · format | how established |
|---|---|---|
| 1 | Audio · Deep Dive | **browser-verified** — opened the Audio Overview *Customize* dialog headless (Playwright + stored `storage_state` auth), clicked the "Deep Dive" format card, decoded the `otmP3b` request's trailing mode |
| 2 | Audio · Brief | browser-verified (Brief card) |
| 3 | Video · Explainer | real web capture — `otmP3b` request while customizing a Video Explainer |
| 4 | Chat · ask (default) | client live-probe — returns "ask about the content" chat questions; the web chat default |
| 5 | Audio · Critique | browser-verified (Critique card) |
| 6 | Audio · Debate | browser-verified (Debate card); output = debate scaffolding |
| 7 | — unidentified | no UI surface sends it; excluded from the tool |
| 8 | Quiz | client live-probe — returns quiz-generation prompts |
| 9 | Flashcards | client live-probe — returns flashcard prompts |
| 10 | Video · Short | real web capture — controlled pair (same notebook + source, mode 3 vs 10) |
| 0, 11+ | INTERNAL (server error) | out of range |
## Two axes (why the labels are honest)
There are two *different* measurements, and they are **consistent**, not contradictory:
- **Surface axis (#1726):** which mode a studio *Customize* dialog SENDS for its
format's suggestions. This is request-side wiring — the `otmP3b` mode the UI
emits per format. It's how the labels are assigned (a card labeled "Deep Dive"
sends mode 1, so mode 1 = the Audio Deep Dive suggestion surface).
- **Output axis (#1612, 2026-06-20 live A/B):** what the backend RETURNS per mode.
Modes 5/6/8/9 (critique / debate / quiz / flashcards) are format-distinctive in
the returned text; modes 1/2/3/10 return content-direction prompts
(persona / format / topic) that read ~like the mode-4 default.
Reconciliation: for deep-dive / brief / explainer / short, NotebookLM steers the
format via **content direction**, not format jargon — so those surfaces' suggestion
*text* is general by design, even though the *surface* (which mode the format's
dialog sends) is distinct and verified. The tool's `surface` labels therefore name
the **surface** (the format whose dialog sends the mode), not a promise about the
returned tone. Modes 5/6/8/9 also differ in output; 1/2/3/10 are the "same shape,
different surface" cases.
## Not exposed / not supported
- Mode **7**: no UI surface sends it; unmapped.
- **data-table / infographic / mind map / slide deck**: their Customize dialogs
exist but fire NO `otmP3b` (no suggestion surface). Modes 8/9 (quiz/flashcards)
are backend-reachable via this RPC but the web UI doesn't wire their dialogs to
it — still valid to expose since the RPC serves them.
- **Report** suggestions: a *separate* RPC (`GET_SUGGESTED_REPORTS` / `ciyUvf`),
not `otmP3b`.
## Method (reproducible)
Headless Playwright loaded the authenticated app via the repo's `storage_state`
auth (`browser.new_context(storage_state=…)`), navigated to a notebook, and clicked
each artifact's `button[aria-label="Customize {Type}"]` chevron (NOT the card body —
that generates). For Audio, each format card inside the dialog was clicked while
capturing the `otmP3b` request post-data (`f.req` → trailing mode int). Video modes
came from real browser network captures (`otmP3b` requests). Chat/quiz/flashcards
were client live-probes of `client.notebooks.suggest_prompts(mode=…)`.
@@ -0,0 +1,140 @@
# REST server vs MCP server — gap review (2026-07-02)
**Inputs (4 lenses):** 2 native Claude (capability-parity matrix · shared-capability contract
divergence) + Codex CLI (REST security/hardening vs MCP) + agy CLI (unique-to-each inventory). All
read the current `src/notebooklm/{server,mcp}/` tree. Both adapters are thin wrappers over the same
`src/notebooklm/_app/` cores, so a "gap" = an `_app/` capability one adapter exposes and the other
doesn't (or exposes with a materially different contract).
## Headline
The gap is **overwhelmingly one-directional**. The REST server is missing ~11 capability areas the
MCP server has; the MCP server is missing **~zero** real capabilities REST has. On top of that, REST
lags MCP on a whole tier of **hardening** (confirm gates, redaction, resource caps, poll semantics).
- **MCP → REST (things REST lacks):** deep research (entire core), source **content** read,
`chat_configure`, studio lifecycle (delete/retry/rename/get_prompt), `source_wait`,
`suggest_prompts`, notebook/source rename, `server_info`, Drive/YouTube/batch source-add.
- **REST → MCP (things MCP lacks):** **none of substance.** REST's dedicated `/notes` CRUD looks
unique but MCP covers all of it through the cross-type Studio surface (`note_save` upsert +
`studio_list` read/enumerate with `kind="note"` + `studio_delete`). This is an interface-shape
difference, not a capability gap. *(Corrects one lens that called MCP a "write-only notes client" —
`_studio_items.py:95-111` reads `client.notes.list`; `studio_delete` deletes notes.)*
**Bottom line:** REST is **not** a viable substitute for MCP today. It can create notebooks, add
url/text/file sources, run a blocking chat, and generate + download artifacts — but a REST client
**cannot read what a source actually says, cannot run deep research, and cannot tune chat.** It is
also the softer target of the two on security.
---
## A. Capability gaps — MCP has it, REST doesn't
Ranked by user impact. Backing `_app/` core in parens.
| # | Gap (REST-missing) | `_app/` core | Impact | Close cost |
|---|---|---|---|---|
| 1 | **Deep research** (start/status/cancel/import) — no research router mounted at all | `research`, `source_research` | **HIGH** | New `/research` router |
| 2 | **Source content read**`GET /sources/{id}` returns metadata/status only (`get_or_none`), never the extracted fulltext MCP's `source_read` serves | `source_content` | **HIGH** | New content route |
| 3 | **`chat_configure`** — REST chat is locked to notebook defaults (no goal/persona/length) | `chat.execute_configure` | **MED-HIGH** | New route |
| 4 | **Studio delete** — REST generates artifacts but can never remove them | `artifacts.delete_artifact` | **MED** | New route |
| 5 | **Studio retry** — a failed generation can't be retried in place | `generate_retry` | **MED** | New route |
| 6 | **`source_wait`** — no readiness primitive; REST callers hand-roll polling | `source_wait` | **MED** | New route |
| 7 | **Drive/YouTube/batch source-add** — REST has only url/text/file, one at a time | `source_add` | **MED** | Extend routes/models |
| 8 | **`suggest_prompts`** — no recommended-question affordance | (client namespace) | **LOW-MED** | New route |
| 9 | **`studio_get_prompt`** — can't retrieve the prompt an artifact was generated with | `artifacts.get_artifact_prompt` | **LOW** | New route |
| 10 | **notebook/source/artifact rename** — no PATCH/rename verbs | `notebooks`, `source_mutations`, `artifacts` | **LOW** | Trivial passthrough routes |
| 11 | **`server_info`** — REST has only unauthed `/healthz` liveness, no account/version block | `profile`, `auth_check` | **LOW** | New route |
## B. Capability gaps — REST has it, MCP doesn't
**Effectively none.** REST's `/notes` list/get/put/delete map to MCP's `note_save` + `studio_list`
(`kind="note"`, single-fetch by ref returns `content`) + `studio_delete`. Same underlying
`_app.notes` ops, different URL shape. Not a gap.
---
## C. Contract divergence on shared capabilities (both surfaces expose these, but differently)
Consensus items (2+ lenses) first.
1. **Async generation poll semantics are genuinely incompatible — HIGH.** MCP `studio_status` is
**stateless**: any `task_id` polls its real status and terminal states (FAILED/REMOVED) come back
as fields. REST records the id in a **process-local in-memory `PendingRegistry`**
(`server/_pending.py:18-22`) and maps unknown→404, REMOVED→410, FAILED→409. A real, still-running
task the process didn't create (or created before a restart) returns **404 on REST but live status
on MCP**. (`artifacts.py:298-324`) *(Lens B)*
2. **Destructive + outward ops: MCP confirm-gated, REST fires immediately — HIGH.** *(Codex + Lens B
consensus)* MCP two-steps every delete and gates public-widening / user-grant behind `confirm=`.
REST: `POST /share/public` makes a notebook public, and `POST /share/users` grants access **and
emails the user**, each on a single unconfirmed request. Deletes are bare `DELETE` verbs (defensible
HTTP idiom) but the **outward sharing** ops have no guard at all. (`share.py:74,86,115`)
3. **`notify` default is inverted — MED.** *(Codex + Lens B consensus)* `share_set_user` defaults
`notify=False` on MCP; REST `POST /share/users` defaults `notify=True` (`share.py:31`) — the same
grant emails a third party by default on REST, silently on MCP. (REST's `PATCH /users/{email}`
carries no notify flag at all.)
4. **Output leaks MCP intentionally strips — MED.** *(Lens B)* REST `to_jsonable`s raw results:
sharing status ships bare integer enums (`access=1`, `permission=3`) instead of MCP's string labels
(`share.py:71,83,143`); chat ships the `raw_response` debug blob MCP pops (`chat.py:47`). Source
list ships raw integer status/type codes with no `kind`/`status_label`.
5. **Error contract thinner + a redaction hole — MED.** *(Codex + Lens B)* MCP errors carry
`retriable` + `hint` and redact home paths / file tokens; REST returns `{category, message}` + HTTP
status with **no `retriable`, no `hint`, and no home-path redaction** (`server/_errors.py:94-107`).
A file-upload path error can leak `/home/<user>/…` in a REST body.
6. **Identifiers: MCP resolves name-or-id-prefix, REST is id-only — MED.** *(Lens B)* REST resolvers
are literal passthroughs (`_passthrough.py:27-31`); a REST caller must hold canonical UUIDs.
7. **Lists: MCP paginated (limit/offset/total/has_more), REST returns the whole collection raw — MED.**
*(Lens B)* Large notebooks/accounts dump unbounded raw-code payloads over REST.
8. **Note create/update ergonomics inverted — MED.** *(Lens B)* MCP requires title+content on create,
allows partial update; REST allows an empty-body create (`title="New Note"`, `content=""`) but
forces a full title+content replacement on `PUT` (`notes.py:35-47,70-79`).
> Verified non-issue: the `source_ids` **None=all vs []=none** footgun is honored correctly on **both**
> sides (`artifacts.py:269` + `_passthrough.py:52` mirror `studio.py:424` + `173`). No divergence.
---
## D. Security / hardening asymmetry (REST lags MCP)
From Codex, all REST-side, all citing an MCP protection REST doesn't replicate:
| Sev | Finding | Where |
|---|---|---|
| HIGH | Public-share + user-grant widen access with no confirm gate (dup of C.2) | `share.py:74,86,115` |
| HIGH | Share invite **emails by default** (`notify=True`) (dup of C.3) | `share.py:31` |
| HIGH | Destructive deletes have no confirm/preview | `notebooks.py:62`, `sources.py:235`, `notes.py:82` |
| MED | Multipart uploads spool to disk before the size cap runs (chunked/no-`Content-Length` bypass) | `app.py:97`, `sources.py:204` |
| MED | Artifact downloads unbounded + cleanup only via background task — no disconnect-safe slot cap like MCP | `artifacts.py:339,353,382` |
| MED | `--token` CLI arg writes the bearer token where `ps` can read it; MCP is env-only on purpose | `__main__.py:107,152` |
| MED | Loopback guard is launcher-only + spoofable `Host` header in-app; running the ASGI app directly on `0.0.0.0` is reachable | `__main__.py:55,157`, `_auth.py:114` |
| LOW | Error redaction lacks MCP's home-path / file-link patterns (dup of C.5) | `_errors.py:94` |
| LOW | Artifact generate silently accepts wrong-kind options and drops mind-map `instructions` | `artifacts.py:264,275` |
| LOW | Upload filename sanitization weaker than MCP's `_safe_upload_name` | `sources.py:57` |
*(Auth token comparison itself: worth confirming `server/_auth.py` uses `compare_digest` — Codex
didn't flag a timing leak, so assume present; verify if hardening this.)*
---
## Recommended priority if REST is to be taken seriously
**Tier 1 (makes REST usable at all):** source content read (A.2), deep research router (A.1),
`chat_configure` (A.3). Without the first, a REST client is blind to its own documents.
**Tier 2 (safety parity):** confirm-gate outward sharing + flip `notify` default (C.2/C.3/D),
port MCP's home-path redaction (C.5/D), add download concurrency cap (D). These are small diffs that
close the sharpest security asymmetries.
**Tier 3 (completeness):** studio delete/retry (A.4/A.5), `source_wait` (A.6), rename verbs (A.10),
pagination (C.7), fix the stateful-poll 404 (C.1 — either document it or back the registry with a real
status probe on cache-miss).
**Cheapest high-value single fix:** flip `notify` default to `False` and confirm-gate `POST
/share/public` — one-line-ish changes that remove two HIGH outward-facing footguns.