Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,29 @@

## Unreleased

### Fixed — `get_instance_state` reported empty storage as if it were the truth (F-869)

On any page that actually has localStorage or sessionStorage entries,
`get_instance_state` (and the `browser://{id}/state` and `browser://{id}/console`
resources) returned `"local_storage": {}`, `"session_storage": {}` and
`"partial": false`. Measured on 2.1.5 against `https://www.google.com/`: 28
cookies came back, both stores came back empty, and the record declared itself
complete. `nodriver`'s `Tab.evaluate` always sends deep `SerializationOptions` and
hands back `deep_serialized_value.value` raw, so `Object.keys(localStorage)`
arrives as `[{'type': 'string', 'value': 'alpha'}, …]` — measured against Chrome
152 — and the per-key loop raised `TypeError: unhashable type: 'dict'` when it used
one of those nodes as a dict key. An `except Exception` then logged it at INFO as
"Storage access unavailable", the sentence meant for opaque origins, and let the
empty record through; INFO is not error-reported, so the failure reached neither
the caller nor error reporting. The read now lives in `embedded/page_storage.py`
and asks the page for one `JSON.stringify` of both stores — the same idiom F-844
applied to the viewport eleven lines below — which also retires the
`localStorage.getItem('{key}')` string interpolation and 2N+2 CDP round trips. Only
a page that genuinely refuses the read still reports empty storage; anything else
propagates and `get_instance_state` answers with `partial: true` and a
`detail_error`, as its docstring always promised, with a WARNING and a traceback in
the backend log.

### Fixed — `status` reported a dead sibling's record as "the backend" (F-868)

On a machine whose `server.json` recorded three display contexts — two dead
Expand Down
5 changes: 3 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ Package root: `src/stealth_chrome_devtools_mcp/`. Two console scripts (`pyprojec
**Browser & interaction**
| File | Owns |
|---|---|
| `browser_manager.py` | `BrowserManager` — spawn/list/close instances; `close_instance` offloaded teardown |
| `browser_manager.py` | `BrowserManager` — spawn/list/close instances; `close_instance` offloaded teardown. `get_page_state` owns the page-state COMPOSITION and its error POLICY, not the storage read (F-869): `page_storage.StorageBlockedError` is the page refusing (INFO, empty dicts, still `partial: false`), and every other exception propagates so `get_instance_state` can answer its `partial: True` + `detail_error` record — the one degradation shape, reached by raising, exactly as this method's docstring always said |
| `page_storage.py` | **THE one home for "read a page's localStorage/sessionStorage"** (F-869) — the ONE `JSON.stringify` round trip that reads both stores (`READ_JS`, `read`), and the two outcomes it distinguishes: `StorageBlockedError` (Chrome threw *inside the page* on an opaque origin / `data:` URL / policy-blocked storage — the expected answer) and `StorageReadError` (the evaluate did not answer with the promised JSON, so empty storage would be a lie). It is `JSON.stringify` for the same reason the viewport read next to it is (F-844): `tab.evaluate` always sends deep `SerializationOptions` and returns `deep_serialized_value.value` RAW, so `Object.keys(localStorage)` arrives as `[{'type':'string','value':'k'}, …]` — measured on Chrome 152 — and the old per-key loop hashed a `dict`. One round trip also retires the `f"localStorage.getItem('{key}')"` interpolation (a quote in a key was a syntax error; `');…` was injection) and 2N+2 CDP calls. **No message here ever carries a stored VALUE** — a page's localStorage is where its session tokens live and a `StorageReadError` reaches the durable log, `get_instance_state`'s `detail_error` AND a Sentry breadcrumb at once, so every message reports shape and count only (type name, entry index, field count, character count). The one page-supplied string repeated at all is the refusal text, kept because Chrome's wording is the diagnostic and **bounded** by `BLOCKED_REASON_CHARS` because `window.localStorage` is an own accessor with `configurable: true` (measured, Chrome 152) — a page can install its own throwing getter and author it. A leaf: imports no other embedded module, takes the tab as an argument |
| `dom_handler.py` | DOM manipulation + element interaction |
| `element_resolution.py` | selector resolution that survives CDP document-node invalidation (route ALL selector resolution through here — never `tab.select`/`find` directly) |
| `proxy_forwarder.py` | authenticated egress-proxy forwarding + `_free_port` |
Expand Down Expand Up @@ -126,7 +127,7 @@ dependency order — each imports only the ones above it, and none imports `serv
| `cdp_params.py` | **THE one home for "a caller's JSON, as the type a CDP wrapper declares"** (F-861) — `typed`, which builds each `execute_cdp_command` argument into the type nodriver's generated wrapper declares for it (`from_json` for the generated classes, through `Optional[..]` and `List[..]`), read from the wrapper's own type hints so nothing is typed by hand; primitives and already-typed values pass through untouched, and a value the type cannot take raises `ToolError` naming the param and the type. Called from the ONE site `cdp_function_executor.build_cdp_call`, after F-816's name folding. A leaf: imports only `tool_errors` |
| `response_handler.py` | large-response handling + file fallbacks; **the one home for "can this payload survive the transport"** — `json_safe` (serializable, F-822) and `surrogate_safe` (utf-8-encodable, F-823) |
| `in_memory_storage.py` | `InMemoryStorage` — deliberately non-durable instance cross-check |
| `debug_logger.py` | in-memory debug log ring/view; `log_tool_failure` is the ring entry point for a failed tool call (ring only — the durable/Sentry-bridged log line is deliberately NOT written, F-835/F-782) |
| `debug_logger.py` | in-memory debug log ring/view; `log_tool_failure` is the ring entry point for a failed tool call (ring only — the durable/Sentry-bridged log line is deliberately NOT written, F-835/F-782). `log_warning` takes an optional `error=` (F-869) forwarded as `exc_info`, so a warning ABOUT a caught exception carries its traceback; the ring shape is unchanged either way, which is what keeps `get_debug_view`'s contract byte-stable |

### Tombstones — do NOT route a change to these (they were removed)

Expand Down
Loading
Loading