fix(browser): stop bridge token divergence (atomic write + host-only + reload)#57
Conversation
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
There was a problem hiding this comment.
Code Review
This pull request addresses bridge token divergence issues by implementing atomic writes for bridge.json and enabling long-lived hosts to auto-reload rotated tokens on handshake mismatches. Additionally, token writing and advertising are restricted to the host instance. A review comment points out a critical bug in advertiseToken where port changes and explicit tokens are ignored because the code only checks for the existence of a shared token before writing. A suggestion is provided to check if the existing port or token has changed before skipping the write.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| const advertiseToken = () => { | ||
| const shared = readBridgeFile()?.token; | ||
| if (!shared) { | ||
| writeBridgeFile(options.port, token); | ||
| } | ||
| if (source !== "explicit") { | ||
| logger.info("browser_bridge_token", { paste_into_extension: token, source, mode: "host" }); | ||
| logger.info("browser_bridge_token", { | ||
| paste_into_extension: shared ?? token, | ||
| source, | ||
| mode: "host" | ||
| }); | ||
| } | ||
| }; |
There was a problem hiding this comment.
⚠️ Critical Bug: Port Changes and Explicit Tokens are Ignored / Stale Tokens Logged
The current implementation of advertiseToken checks if (!shared) before writing the token to bridge.json. This introduces several critical issues:
- Port Changes are Ignored: If
bridge.jsonalready exists with a token (e.g., from a previous run on port4517),sharedwill be truthy. If the host is restarted on a different port (e.g.,4518),writeBridgeFileis never called. Thus,bridge.jsonis never updated with the new port, breaking discovery for guest adapters (like the MCP server) which will continue trying to connect to the old port. - Explicit Tokens are Ignored: If the user configures an explicit token, it will not be written to
bridge.jsonif a token already exists. Furthermore, on a handshake mismatch, the broker'sreloadTokenwill read the stale token frombridge.jsonand overwrite the explicit token, completely ignoring the user's explicit configuration. - Stale Token Logging: When the port or token changes,
shared ?? tokenwill evaluate to the stalesharedtoken from the file, logging an incorrect token that mismatches the actual running broker's token.
Solution
We should only skip writing if the file already exists and matches both the current port and token. Otherwise, we must write the updated port and token to bridge.json to keep it as the single source of truth. Additionally, ensure that connection tokens or critical initialization logs are emitted dynamically on every transition to active/host mode rather than only once during initial startup.
| const advertiseToken = () => { | |
| const shared = readBridgeFile()?.token; | |
| if (!shared) { | |
| writeBridgeFile(options.port, token); | |
| } | |
| if (source !== "explicit") { | |
| logger.info("browser_bridge_token", { paste_into_extension: token, source, mode: "host" }); | |
| logger.info("browser_bridge_token", { | |
| paste_into_extension: shared ?? token, | |
| source, | |
| mode: "host" | |
| }); | |
| } | |
| }; | |
| const advertiseToken = () => { | |
| const existing = readBridgeFile(); | |
| if (!existing || existing.port !== options.port || existing.token !== token) { | |
| writeBridgeFile(options.port, token); | |
| } | |
| if (source !== "explicit") { | |
| logger.info("browser_bridge_token", { | |
| paste_into_extension: token, | |
| source, | |
| mode: "host" | |
| }); | |
| } | |
| }; |
References
- Ensure that connection tokens or critical initialization logs are emitted dynamically on every transition to active/host mode (such as during re-election or failover), rather than only once during initial startup.
8484a64 to
9cbbb74
Compare
…+ reload) The failure where the extension sends the *current* bridge token but a long-lived host keeps rejecting it. Root cause: every instance wrote `bridge.json` non-atomically at load, so a concurrent boot could catch a torn/empty file, fall through to GENERATE a fresh token, and overwrite the shared one — orphaning the already-running host (which resolves its token once at boot and never re-reads). Confirmed in a real log: file held `c856e1cf…` while the host still checked against `ea9cdad6…`. Three changes: 1. `writeBridgeFile` is now atomic (per-writer pid+uuid temp → rename, unlink on failure) — a reader can never catch a torn write, so no spurious regenerate. 2. Only the **host** writes `bridge.json` (moved into the onHost path, not unconditionally at load), and it never clobbers a token the file already carries — so the file stays the single source of truth and a guest that resolved a fresh token on a transient miss can't overwrite it. 3. On a bad-token handshake the host re-reads `bridge.json` via an injected `reloadToken` and adopts a rotated token (logged `browser_bridge_token_reloaded`), so a rotation reaches a running host without a restart — then re-checks the handshake. Threaded `reloadToken` through endpoint → broker deps. Adds token-file atomic-write + broker token-adoption tests, updates the opencode token-file mock, the browser.md troubleshooting row, and CHANGELOG. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Gemini flagged that 'skip write if any token exists' regressed two cases: a port change left a stale port in bridge.json (breaking guest discovery), and an explicit operator token could be ignored — then a file reload would override it. Now the host writes whenever the file doesn't match its (port, token), and reloadToken is disabled for an explicit token so the file can never override a pinned secret. Adds regression tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
afd25e2 to
188e8d2
Compare
|
Addressed the Gemini high-priority finding on
Rebased onto the updated #55. |
What
Fixes the bug we diagnosed live: the extension sends the current token but a long-lived host (an IDE-embedded OpenCode) keeps rejecting it as
bad_token.Root cause
Every instance wrote
bridge.jsonwith a non-atomicwriteFileSyncat load. When the desktop app boots many instances at once, one can read the file mid-write (torn/empty), fall through to generate a fresh token inresolveSharedToken, and overwrite the shared one — orphaning the already-running host, which resolves its token once at boot and never re-reads. Confirmed in a real log:bridge.jsonheldc856e1cf…while the running host still checked againstea9cdad6…, so every handshake (with the correct file token) was rejected.Fix — three layers
writeBridgeFile— per-writerpid+uuidtemp thenrename(+ unlink on failure). A reader can never catch a torn write, so no spurious regenerate. (Same lesson as fix(oauth2): per-writer temp file for model-sync cache (concurrent-boot ENOENT) #54.)writeBridgeFilemoved into theonHostpath instead of running unconditionally at load, and it never clobbers a token the file already carries. A guest that resolved a fresh token on a transient miss can no longer overwrite the live host's token; the file stays the single source of truth.bridge.json(injectedreloadTokendep) and adopts a rotated token (logsbrowser_bridge_token_reloaded), then re-checks the handshake. So a rotation reaches a running host within the extension's retry window — no process kill needed (the exact thing that bit us: rotating the file did nothing while the old host lived).Tests / docs
token-file: atomic concurrent-write test (no.tmpleftovers, last writer wins).broker: adopts a rotated token on mismatch; still rejects when the reload doesn't match either.opencode.test.tstoken-file mock (newreadBridgeFileexport).docs/browser.mdtroubleshooting row (host now auto-reloads; restart is the older-build fallback) +CHANGELOG.md.Full pre-push gate green (browser 119 tests, 91% stmts — every coverage threshold met).
🤖 Generated with Claude Code