v0.10.0 — Remote browser_tool bridging, per-workspace browser tab isolation, and #824 basic-auth fix
Features
-
Remote
browser_toolbridged into the user's local Electron browser — Agents running on a remote workspace (headless server, docker, WebUI) can now drive the user's local desktopBrowserPaneManagerend-to-end. Adds aclient:browser:invokeWS capability (advertised on handshake, server invokes client, plainErrorwith.codepreserved through both directions), mirroring howshell.openExternalalready works forOPEN_URL. Transport gainshasClientCapability/findClientsWithCapabilityfor routing; Electron gets a new__browser:invokeIPC dispatcher with per-method owner-key authorization, no-manual-window-reuse for remote callers, session-scopedlistInstances, and screenshotBuffer↔Uint8Arrayconversion across the wire.server-coregets aRemoteBrowserPaneManager(session-boundIBPMimpl) andSessionManager.getBrowserPaneManagerForSessionwith capability-aware host-client fallback and per-session pin cleanup on disconnect.uploadFileis blocked over the bridge;evaluateis gated by a localallowRemoteEvaluatesetting. The Pi runtime learns friendly error mappings forBROWSER_NO_CAPABLE_CLIENT,CAPABILITY_UNAVAILABLE,CLIENT_DISCONNECTED,CLIENT_REQUEST_TIMEOUT,BROWSER_INSTANCE_NOT_OWNED,BROWSER_REMOTE_UPLOAD_NOT_SUPPORTED, andBROWSER_REMOTE_EVALUATE_BLOCKED, and now mirrors Claude'sgetBrowserToolEnabledgate so Pi no longer advertisesbrowser_toolwhen the toggle is off. 27 new tests cover wire packaging, per-method authz, capability introspection, host-client fallback, screenshot round-trip, error-code preservation, and the Pi error-mapping contract. (1d926c33) -
Browser tabs isolated per workspace —
BrowserPaneManageris process-global andSTATE_CHANGEDused to broadcast{ to: 'all' }, so a chat in workspace A saw browser tabs and status banners owned by sessions in workspace B. EveryBrowserInstance(and theBrowserInstanceInfoDTO) now carries a nullableworkspaceId;STATE_CHANGEDroutes to{ to: 'workspace', workspaceId }when set (falling back to{ to: 'all' }for unbound manual windows); thebrowserPane.LISThandler filters byctx.workspaceId; and the renderer reads a newbrowserInstancesForWorkspaceAtomFamilykeyed byactiveWorkspaceId. Windows still run in parallel as realBrowserWindows — this is a UI visibility filter, not a sandbox.REMOVED/INTERACTEDstay broadcast-to-all (id-only payloads, harmless no-op on workspaces that never saw the entry).workspaceIdships optional on the DTO, so old renderers tolerate missing values (treatsundefinedasnull→ passes the filter, equivalent to today's behavior). 17 new tests across atom filter, BPM stamping, and broadcast/LIST routing. (af817192)
Improvements
-
markdown-previewblock documented in the rich-output reference — Themarkdown-previewblock (shipped in v0.9.6) is now covered inapps/online-docs/go-further/rich-output.mdxalongside the existinghtml-preview/pdf-preview/image-previewentries, so users discovering the block in-chat can find usage examples and thesrc/itemsfield reference in the docs. (2d9693b1,70c2955f) -
Browser-bridge wiring is observable from server logs — Three
sessionLog.infolines inSessionManagernow confirm whethersetRpcServerran at bootstrap and whether the browser-pane-forwarding block executed at agent init. Makes it possible to diagnose remote workspaces that still hit "Browser window controls are not available" from server logs alone, without attaching a debugger. (ad26e61d)
Bug Fixes
-
Renderer accepts both local AND remote workspace ids when filtering tabs — When connected to a remote workspace, a renderer has two relevant workspace ids:
activeWorkspaceId(the LOCAL Craft Agents window's identity, used for locally-opened manual tabs) andactiveWorkspace.remoteServer.remoteWorkspaceId(the REMOTE server's id, used by the remote agent when it stamps tabs through the WS bridge). The first iteration of the workspace-isolation filter only matchedactiveWorkspaceId, so tabs stamped with the remote id (which is what every remote agent-opened browser carries) got filtered out — the TopBar tab strip and the toolbar status badge became invisible for remote browsers, and opening one then hiding it left it inaccessible. Replaced theatomFamilywith a plain helperfilterInstancesForWorkspace(local, remote): tabs match if either id matches (or ifworkspaceIdis null/undefined for back-compat). (bf8429fa) -
STATE_CHANGEDbroadcasts to all clients again; the visibility filter lives in the renderer — The Phase-4 server-side workspace filter (on bothSTATE_CHANGEDrouting and theLISThandler) was wrong for remote-mirror workspaces: a renderer's transport-levelworkspaceIdis the LOCAL window's identity, while remote-bridged browser tabs carry the REMOTE server's workspace id. The two never match, soSTATE_CHANGEDtargeted at the remote id got dropped by the WS routing layer (no local renderer reports itself as being in the remote workspace) andLISTreturned empty for the same reason. Workspace isolation now lives entirely in the renderer (filterInstancesForWorkspace), which knows both ids viaactiveWorkspace.remoteServer.remoteWorkspaceId. The handler reverts to{ to: 'all' }broadcasts and a fullLISTresponse. Privacy is unchanged — every locally-connected renderer belongs to the same user — and remote tabs finally show up in the TopBar of the workspace that owns them. (f831bb42) -
No window reuse on the remote-bridge lifecycle path (closes the cross-workspace hijack) — The capability dispatcher correctly set
allowReuseManual=falseforcreateForSessionbut flowedgetOrCreateForSessionandfocusBoundForSessionthrough the public helpers, which defaultallowReuseManual=true. The remote agent'sbrowser_tool openmaps tofocusBoundForSession, so it could adopt an unbound window left behind by a local session — exactly the cross-workspace hijack theworkspaceIdfilter was meant to block. TheworkspaceIdfilter still helps but it's best-effort: windows created before workspace stamping (or via paths that never setworkspaceId) haveworkspaceId=nulland remain universally adoptable. Belt-and-brace fix: every remote lifecycle call now passesallowReuseManual=false, so remote sessions always create fresh windows unless they already own one. (ce3340a1) -
Unbound-window reuse scoped to the owning workspace (no more "tab moved from workspace A to B") — When a session's turn ends,
unbindAllForSession()clearsboundSessionIdand flipsownerTypeto'manual'so the next turn of the same session can re-bind the window; theworkspaceIdstamped at creation is preserved.findReusableUnboundInstance()was matching ANY unbound 'manual' window regardless of workspace, so a session in workspace B would happily pick up the leftover window from workspace A —bindSession()would then overwriteworkspaceIdto B, effectively "moving" the window from A to B and making workspace A's tab strip lose the entry while B's gained it. Reuse is now allowed only when the candidate'sworkspaceIdis null (truly user-opened manual window — adoptable by anyone) or matches the caller'sworkspaceId. Same-workspace next-turn reuse still works. (ceb24603) -
TopBar-opened manual windows inherit the workspace they were opened in — The
browserPane.CREATEhandler created manual windows withworkspaceIddefaulting tonull, which the workspace-isolation filter intentionally treats as "visible to all workspaces" — so a TopBar-opened tab leaked into every workspace's tab strip. The renderer that firesCREATEalways hasctx.workspaceIdset, so the handler now passes it through tocreateInstance/createForSession. CLI / agent-harness callers with no workspace context (ctx.workspaceId === null) still get the broadcast-to-all behavior as a safe fallback. (7dfcaeac) -
BrowserInstanceprojected to a plain snapshot before IPC return — The localBrowserPaneManager.getInstance(id)returns the liveBrowserInstancewhich embeds Electron native references (window: BrowserWindow,pageView: BrowserView, ...). When the__browser:invokedispatcher returned that object over IPC, Electron's structured-clone serializer threwAn object could not be clonedand the remote agent's logging-sidegetInstanceAsynccall failed. AddedtoSnapshot(instance)that emits only theIBPM-declared fields (ownerType,ownerSessionId,isVisible,title,currentUrl) and routed the dispatcher'sgetInstancebranch through it. (8e2534b5) -
source_testbase64-encodes basic-auth credentials —testApiConnectionWithAuthwas interpolating the raw vault value into theAuthorizationheader, so basic-auth sources gotBasic {"username":"...","password":"..."}and 401'd against every provider. The vault storessource_basiccredentials as JSON (written bysource_credential_prompt/ the WebUI); the runtime path inapi-tools.tsbuildHeadersalready parses and base64-encodes — the validator path was just left out. Now parses the token as JSON when it hasusername+password; falls through to pass-through behavior for legacy / hand-edited base64 entries and any non-JSON string, mirroringbuildHeaders(). Three regression tests cover the JSON form, the legacy already-encoded form, and a non-JSON garbage token. Fixes #824. (96dd7c0d)
Breaking Changes
- None. The
workspaceIdfield on theBrowserInstanceInfoDTO is optional, so older renderers and older agents tolerate missing values (treatsundefinedasnull→ passes the visibility filter, equivalent to pre-0.10.0 behavior).