Repository navigation
feat: Environment browser on the web, with an agent-browser session per thread - #41
Conversation
…session On a Lazurio Remote Environment the agents of every thread work in the Environment browser, one shared Chromium, through the agent-browser CLI, each thread in a window of its own (root decision 0191, plan DEV-6646). The CLI takes its session from AGENT_BROWSER_SESSION, so every provider process T3 starts for a thread now carries the thread's name: `t3-` and the thread id with every character outside [A-Za-z0-9_-] replaced by `-`, at most 64 characters (agent-browser's grammar and its dashboard's limit). ProviderService records the name with the thread's provider session, whose environment every adapter already spreads into the processes it starts for the thread (the path the agent-device CLI takes). So the Codex app-server, Claude Code and the other adapters get it from one seam: a single call in ProviderService.ts; the rest lives in apps/server/src/lazurio/. The name wins over one T3 itself inherited. It only names the session: nothing is granted, and it is set whether or not T3's own agent browser access is on. Tests cover the name for UUIDs, dots, colons, slashes, non-ASCII and long ids, ProviderService recording it before the adapter starts, and the environment the Codex app-server and Claude Code start with. CI runs them (`vp test run src/lazurio` in apps/server) and the release contract test keeps the seam. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The web client has no browser of its own: the right panel's Browser is
disabled with "Only available in the desktop app" wherever Electron's
desktopBridge.preview is missing. On a Lazurio Remote Environment one
shared Chromium runs on the Environment, each thread's agents drive a
window of their own in it, and a person watches and takes over through
a view behind the Environment's gateway (root decision 0191, plan
DEV-6646).
Without the desktop preview, and only for threads of the environment
that serves the page, the right panel now asks this origin for
GET /.lazurio/browser.json?session=<the thread's agent-browser session>
(same-origin credentials, redirects refused, no cache, 10 s timeout).
The Environment's Launchpad answers there behind the same sign-in. A
200 {"available": true, "view": <https URL>, "session": <name> | null}
enables Browser; anything else (available false, a 404 from an older
Platform, a network error, T3's own index.html, a view that is not
https: or is on this page's origin, another session) keeps today's
disabled state and text.
Opening it adds a surface kind of its own, environment-browser:
preview surfaces are dropped by reconcileBrowserSurfaces when the
server has no tab for them. The surface asks for the view each time it
opens and on Reload, because the view URL carries a short-lived access
token in its fragment; the URL lives only in component state, and the
persisted surface is {id, kind}. The view is framed with
allow="clipboard-read; clipboard-write; fullscreen" and
sandbox="allow-scripts allow-same-origin allow-forms allow-popups
allow-downloads allow-modals"; the header has Reload and "Open in new
tab", since the gateway's sign-in page cannot render in a frame once the
sign-in has expired. Desktop profiles are offered only where the desktop
preview exists.
The desktop app is unchanged: with desktopBridge.preview nothing is
asked and Browser is the desktop preview. Upstream files change only at
seams (the surface kind in rightPanelStore.ts, its tab in
RightPanelTabs.tsx, enabling and rendering it in ChatView.tsx); the rest
is in apps/web/src/lazurio/, which holds a copy of the session name
function. The allowlist lists every file; the release contract test
keeps the seams, the same-origin fetch, the frame attributes, that the
URL is never stored, and that the server's and the web's names are one
function.
Refs #40, #28.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The overlay table gains the two commits of root decision 0191 (plan DEV-6646), both "retain": the thread's agent-browser session for every provider process, and the right panel's Browser showing the Environment browser on the web, the documented minimal UI intervention, since the web client has no browser. The intro no longer calls the shell slot the only UI intervention. A section "Prohlížeč Environmentu" says what the server sets and how, what the web asks /.lazurio/browser.json and what counts as available, why the surface is a kind of its own, that the view URL is asked for on every open and never stored, the frame's attributes and the new-tab way out, what does not change (desktop, mobile, the preview.toggle shortcut), which upstream files carry seams and what the contract test and CI keep. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ts own Replacing characters outside [A-Za-z0-9_-] and cutting at 64 characters can make two thread ids read alike. An imported thread's id is import:<provider instance>:<provider session>, and an instance id may take 64 characters, so cutting alone would give every thread imported from such an instance the same session, and their agents would drive one window (review finding on #41). The name stays t3-<thread id> whenever the id already fits agent-browser's grammar and length, which is every ordinary thread (a UUID). Any other id keeps a readable start of its sanitized form and ends in an FNV-1a hash of the exact id, still at most 64 characters. The server's and the web's copies change together; the contract test keeps them one function, the tests cover long imported ids and ids that differ only in rewritten characters, and the runbook states the rule. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The /.lazurio/browser.json contract enables Browser for a 200 answer with an available view; the check accepted any 2xx status (review finding on #41). It now takes exactly 200, the test covers a 202 with an otherwise valid document, and the contract test keeps the check. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
agentrozjedemeai
left a comment
There was a problem hiding this comment.
P1 — Imported threads can still share one browser session (apps/server/src/lazurio/agentBrowserSession.ts:28-33, mirrored in web). The 8-hex-digit FNV-1a suffix is not unique. I ran the exact function on this head: for prefix = "import:codex-" + "w".repeat(58) + ":", agentBrowserSessionName(prefix + "f64cccd6-59c8-42a7-aa0e-319969aeccc9") equals agentBrowserSessionName(prefix + "3c4a8834-35dc-418d-a6dd-d8d1934ab83f"); both yield t3-import-codex-wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww-dc7d7bc4. These are distinct, valid imported thread IDs of the shape generated by AgentSessionImporter.ts:168-170. Their providers therefore receive the same AGENT_BROWSER_SESSION and can drive the same window, violating the per-thread isolation this change promises. Please address the collision (e.g. a sufficiently long collision-resistant digest plus this regression pair, or collision-checked persistent allocation if strict uniqueness is required) and synchronize the server/web implementations. The 17/17 release-contract tests pass locally, but do not cover this case. No merge/release action taken.
Pablo's review of #41 found two distinct imported thread ids of the same long instance whose 32-bit FNV-1a suffixes collided, so their providers would have shared one agent-browser session and one window. The suffix is now the first 32 hex digits of the exact id's SHA-256 (node:crypto on the server, @noble/hashes in the web; one function text, compared by the release contract test). Ordinary ids are still exactly t3-<id>. The colliding pair is a regression case on both sides and in the contract test. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
@agentrozjedemeai Fixed in 1a43117: the suffix of a cut or rewritten id is now the first 32 hex digits (128 bits) of the exact id's SHA-256 (node:crypto on the server, @noble/hashes in the web, one function text compared by the release contract test). Your colliding pair is a regression case on both sides and in the contract test. Locally on Node 24.16: format check, server and web typecheck, server overlay 6/6, web overlay 19/19, contract 17/17. |
agentrozjedemeai
left a comment
There was a problem hiding this comment.
@immakermatty Approved at exact head 1a43117. The prior P1 collision pair now maps to distinct 64-character, grammar-valid session names with the 128-bit SHA-256 suffix on both server and web; ordinary UUIDs retain t3-. I inspected the one-commit repair and the server/web parity and release contract, and ran the isolated server overlay tests (6/6), web overlay tests (19/19), release contract (17/17), format check, and server/web typechecks (all passed; local Node 26 versus required Node 24). Fork CI has four successful jobs on this head; review threads are resolved. No browser integration smoke was available. This approval is review-only, not merge or release authorization.
…ts own Replacing characters outside [A-Za-z0-9_-] and cutting at 64 characters can make two thread ids read alike. An imported thread's id is import:<provider instance>:<provider session>, and an instance id may take 64 characters, so cutting alone would give every thread imported from such an instance the same session, and their agents would drive one window (review finding on #41). The name stays t3-<thread id> whenever the id already fits agent-browser's grammar and length, which is every ordinary thread (a UUID). Any other id keeps a readable start of its sanitized form and ends in an FNV-1a hash of the exact id, still at most 64 characters. The server's and the web's copies change together; the contract test keeps them one function, the tests cover long imported ids and ids that differ only in rewritten characters, and the runbook states the rule. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The /.lazurio/browser.json contract enables Browser for a 200 answer with an available view; the check accepted any 2xx status (review finding on #41). It now takes exactly 200, the test covers a 202 with an otherwise valid document, and the contract test keeps the check. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Status: in progress — awaiting DEV-6646 integration review
Why
Root decision 0191 (plan DEV-6646): on a Lazurio Remote Environment the agents of every thread work in the Environment browser, one shared headed Chromium on the Environment. Each thread drives a window of its own through the
agent-browserCLI, and a person watches and takes over through a view (the agent-browser dashboard) behind the Environment's gateway.T3 Code's web client has no browser: the right panel's Browser is disabled ("Only available in the desktop app") wherever Electron's
desktopBridge.previewis missing. Nothing told an agent which agent-browser session belongs to its thread either.What
Two overlays, each with a small seam in upstream code.
A. Server: an agent-browser session per thread. Lives in
apps/server/src/lazurio/agentBrowserSession.ts, plus one call inProviderService.ts.AGENT_BROWSER_SESSION=<name>.[A-Za-z0-9_-]) within its dashboard's 64 characters gets exactlyt3-<thread id>. Every ordinary thread id is a UUID, so this is the normal case.-and the name is cut. It then ends in 32 hex digits (128 bits) of the SHA-256 of the exact id, so two threads never share a session (a 32-bit FNV-1a suffix in an earlier head could be made to collide; see the review). Long imported ids (import:<instance>:<session>, where an instance id may take 64 characters) are the case this covers.ProviderService.prepareMcpSessionrecords the name with the thread's provider session (withAgentBrowserSession). Every adapter already spreads that session's environment into the processes it starts for the thread (withAgentDeviceEnvironment, the path the agent-device CLI takes). So Codex's per-thread app-server, Claude Code, Cursor, Grok, OpenCode and Antigravity all get it without adapter changes.B. Web: the right panel's Browser shows the Environment browser. Lives in
apps/web/src/lazurio/, with seams inrightPanelStore.ts,RightPanelTabs.tsxandChatView.tsx.GET /.lazurio/browser.json?session=<name>withcredentials: "same-origin",redirect: "error",cache: "no-store"and a 10 s timeout.{"available": true, "view": "<https URL>", "session": "<name>" | null}.available: false, any other status (a 404 from an older Platform, 400, 202, and so on), a network error, an answer that is not JSON (including T3's ownindex.htmlfallback), a view that is nothttps:or sits on this page's origin, or a different session.environment-browser. It cannot bepreview, becausereconcileBrowserSurfacesdrops preview surfaces that have no live server tab.{id, kind}.<iframe allow="clipboard-read; clipboard-write; fullscreen" sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-downloads allow-modals">fills the panel.target=_blank rel=noopener, same URL). The new tab is there because the gateway's sign-in page cannot render inside a frame.allow-same-originthe frame keeps its own origin, so a view on T3's own origin is refused.apps/server/src/lazurio/agentBrowserSession.tsandapps/web/src/lazurio/agentBrowserSession.ts. Sharing it throughpackages/sharedwould need an entry in that package's export map, and upstream has changed thatpackage.jsonin 31 commits since August. The release contract test asserts the two copies are the same function.Fork policy.
allowed_upstream_changeswith a reason.ChatView.tsxwas already listed.vp test run src/lazurioinapps/server.Review. Greptile raised four findings. Two were real and are fixed in follow-up commits:
1bb1015.ab3a24b.The other two are answered in their threads and left as designed:
What does not change
desktopBridge.preview: no request is made, Browser is the desktop preview, and profiles work as before.packages/.preview.togglekeybinding and "open in preview" links still open URLs in a desktop preview tab only.Verified
ab3a24b, all green (run 37375471330):Server and web compatibility: allowlist guard,vp fmt --check, typecheck of server and web, the hosted-server tests, the newTest the Lazurio server overlay,Test the Lazurio web overlay, the release contract test and the release image build.CLI archives / Build darwin-arm64,CLI archives / Build linux-x64andCLI archives / Launcher install linux-x64.fb2c560) lost its Linux jobs to the GitHub Actions incident of 2026-10-05. Its darwin archive job passed.pnpm exec vp fmt --check: clean.pnpm exec vp run --filter t3 typecheckandpnpm exec vp run --filter @t3tools/web typecheck: pass.pnpm exec vp lint --report-unused-disable-directiveson every changed file: exit 0 and no new warnings. ChatView.tsx (69) and RightPanelTabs.tsx (1) have the same warnings as the upstream baseline.cd apps/server && pnpm exec vp test run src/lazurio: 6/6. This covers the name rule (exact for clean ids; distinct, readable and in the grammar for rewritten, long and imported ids), the merge into the provider-session environment, ProviderService recording the name before the adapter starts, and the environment the Codex app-server and Claude Code start with.ProviderService.ts, the ProviderService test fails.cd apps/web && pnpm exec vp test run src/lazurio: 19/19 (this includes the prompt-draft tests). This covers the name, every browser.json case (available, unscoped, not offered, 404, 400, 202, network or redirect, HTML, not JSON, http, relative,javascript:, this page's origin, missing or mistyped fields, another session, other response origin, array, timeout) and the surface store (singleton, survives preview reconciliation, toggle, close, persisted as{id, kind}).node --test scripts/lazurio-release-contract.test.mjs: 17/17.src/rightPanelStore.test.ts src/components/RightPanelTabs.test.tsx src/components/ChatView.logic.test.ts: 228/228.src/provider/Layers/ProviderService.test.ts src/mcp/McpProviderSession.test.ts: 94/94.src/server.test.ts: 212/212.AntigravityAdapter > serves client file reads and writes only inside the session roots, fails the same way on the unchanged base. It is a macOS/varvs/private/vartemp-path issue that does not affect Linux CI.pnpm exec vp run --filter @t3tools/web buildandpnpm exec vp run --filter t3 build: both pass. The bundles carryAGENT_BROWSER_SESSIONand/.lazurio/browser.json.Risks
frame-ancestors/X-Frame-Optionson the gateway'sbrowser.origin). The Platform and gateway own this; it is to be confirmed in the integration review.allow-scriptstogether withallow-same-originonly confines the frame because the view is on another origin, so a same-origin view is refused. The lint rule is suppressed on that line with the reason.t3-<id>for clean UUIDs, otherwise the cut name plus 32 hex digits of the id's SHA-256. Today nothing outside T3 derives it; the Platform readsAGENT_BROWSER_SESSIONas given.Related
🤖 Generated with Claude Code