DSH Agent Browser Fleet
by QuanQQQ
README.md
# DSH Agent Browser Fleet
A DSH Web bundle that provides reusable authenticated Chromium identities and one Shared-Control Session Browser per DSH session on hosts without a desktop environment.
## Product model
- An **Identity** is a reusable authenticated browser profile lineage that creates isolated Profiles on demand without a configured occupancy ceiling.
- A **Template Snapshot** is an immutable clean copy produced after user-driven login setup and a clean Chromium shutdown.
- A **Session Browser** is the single durable browser associated with one DSH session.
- An **Identity Binding** durably records which Identity the Session Browser uses.
- **Shared Control** means the user may operate noVNC while the Agent operates Playwright. Concurrent action ordering is not guaranteed.
- **Browser Allocation** assigns persistent profile capacity internally. It is not a public lease and its records are never exposed to the Agent or Sidebar.
- A **Browser Egress Policy** is one deployment-wide routing policy shared by template and Session Browsers; it is never copied into an Identity.
Profile copies never merge into the Template Snapshot or into one another.
## Agent tools
Fleet adds two Identity tools:
- agent_browser_identities lists usable Identities and reports the calling session's current binding.
- agent_browser_use_identity selects an Identity for the calling session and ensures its Session Browser is ready.
The calling DSH Agent identity is the sole source of the session ID; the model cannot provide a session ID. After selection, browser automation uses the official @playwright/mcp core browser_* catalog for navigation, accessibility snapshots, interaction, tabs, files, console, network, storage, and screenshots.
A Fleet Adapter resolves each tool execution to a hidden loopback CDP endpoint and caches one in-memory MCP connection per Session Browser. Raw CDP endpoints are never returned to the model. @playwright/mcp is pinned to 0.0.79 because its public Interface is pre-1.0, and tests lock the catalog and routing contract.
The DSH tool renderer preserves MCP text output. Image blocks are represented as saved-file placeholders, so accessibility snapshots are the preferred Agent observation until DSH attachment ingestion is available.
## Browser-first Sidebar
The Web client registers one Browser Fleet tab through dsh-better-sidebar 0.18.0 or later. Better Sidebar scopes the tab to the active DSH session and can resize, split, or float it beside the conversation.
The default view shows the current Session Browser through authenticated noVNC and labels Shared Control. The management drawer creates Identities, renames them, deletes unused ones, opens user-driven login setup, saves Template Snapshots, lists occupied Session Browsers, and shows activity. Each occupied row joins the Binding to the standard DSH client session catalog so the durable session display title, current/Agent status, short technical id, and last activity time are visible; unavailable and untitled sessions use explicit fallbacks. Fleet does not persist or serve conversation titles itself. Each occupied browser can be explicitly released after confirmation; release cleanly stops its runtime and removes its Identity Binding while retaining the reusable Profile and login state. Renaming rejects a case-insensitive duplicate; deletion is refused while any Session Browser or template login browser is still using the Identity, and otherwise removes its reusable profiles from disk. The Sidebar exposes no leases, runtime ports, capacity-record IDs, raw control endpoints, or exclusive-control modes. Identity selection belongs to the Agent tool.
Auto-open on first Identity binding is enabled by default and can be disabled from the Browser Fleet card's feature settings. After the active session has an unbound baseline, its first Agent-driven Identity binding creates or focuses the single Browser Fleet tab. A binding that already exists on page load or session switch never steals focus, and later Identity changes do not reopen it. Better Sidebar intentionally does not expand a collapsed panel for type-only opens, so in that case Browser Fleet becomes the active tab shown the next time the panel opens.
Hidden tabs stop polling and detach the noVNC iframe. No global overlay is registered.
## Lifecycle
Selecting the same Identity is idempotent. Selecting another Identity reuses an available healthy Profile or creates one on demand, starts an unbound replacement runtime, and then cleanly stops the old Session Browser before Fleet swaps the Identity Binding atomically. Invalid or failed target starts leave the old Session Browser untouched. A DSH session has at most one bound Session Browser even while the Identity changes.
A Session Browser and its Identity Binding are durable until an explicit Browser action changes or stops them. DSH turn completion, live-session disposal, client disconnects, and Host initialization never reclaim Browser Sessions. On initialization, failed status reads or observation writes trigger ownership-safe cleanup rather than quarantine-only handling, and every BROKEN runtime with residual evidence retries cleanup without becoming READY. Fleet restarts a bound READY browser only after trustworthy observation and confirmed cleanup. A runtime is controllable only when ownership is established, Chromium CDP is ready, and every required CDP/VNC/noVNC listener is present and loopback-only. Any Slot or Template whose runtime status, listener safety, or reconciled observation cannot be confirmed is force-cleaned or quarantined before allocation and control, including after a Host restart. A Runtime Start Error, including an invalid or unavailable Browser Egress Policy, does not by itself imply reusable-profile corruption. If startup cleanup is confirmed, the stopped slot remains READY and bound for a later explicit selection or Host reconciliation. If cleanup cannot be confirmed, Fleet preserves the binding and profile but marks the runtime BROKEN, records its observed or conservative running state, and requires operator recovery. An authenticated, confirmed Browser Release or user-started Template login recovery may then attempt Explicit Runtime Recovery: ordinary cleanup runs first, and only a unique same-UID process-group leader with complete canonical Profile/runtime-scope and role-specific instance evidence can receive a reconstructed generation-bound pidfile before the unchanged stop postconditions run. Any ambiguity preserves the BROKEN runtime and Binding; confirmed cleanup transitions that runtime-BROKEN Slot to READY, clears its stale error, and returns the Profile to allocation only as part of the same successful release or Identity-switch transaction. Partial Chromium descendants are tolerated only inside the uniquely proven browser leader's PGID; a partial process in any other group still rejects recovery. Agents, Host initialization, ordinary stop, and inactivity never trigger this recovery.
## Browser Egress Policy
Fleet optionally compiles an inline Chromium PAC for suffix-specific outbound routing. The default policy path is `$DSH_HOME/agent-browser-fleet/egress-policy.json`; an absent default file means the feature is disabled. `AGENT_BROWSER_FLEET_EGRESS_POLICY` may name a different **absolute** path, and an explicitly configured missing file is an error.
Example (replace the documentation IPv6 address with the current proxy endpoint):
```json
{
"version": 2,
"routes": [
{
"id": "overseas-sg",
"domains": ["tiktok.com"],
"proxy": "http://[2001:db8::10]:3128",
"agentExtensions": {
"enabled": true,
"maxDomains": 64
}
}
],
"fallback": "environment",
"directPlainHostnames": true
}
```
Policy version 1 remains supported, but its anonymous routes are operator-managed and immutable to Agents. Version 2 gives each route a stable `id`; `agentExtensions.enabled` explicitly authorizes Agents to extend only that route's domain suffixes. The operator still owns the route order, proxy endpoint, base domains, fallback, and optional `allowedSuffixes`. `maxDomains` defaults to 64. Without `allowedSuffixes`, an Agent may add any canonical registrable domain suffix within the configured capacity; Fleet uses the ICANN and private Public Suffix List to reject suffixes such as `co.uk` and `github.io`. Deployments that need a narrower boundary should set an allowlist.
A suffix includes its apex and true subdomains, so `tiktok.com` matches both `tiktok.com` and `seller-us.tiktok.com`, but not `nottiktok.com`; leading `.` and `*.` forms are also accepted and normalized. Applications may depend on sibling registrable domains. TikTok Seller, for example, can return its main document from `tiktok.com` while required JS/CSS from `tiktokcdn-us.com` times out and leaves a blank UI. An Agent should inspect browser network failures, call `agent_browser_egress_routes`, then call `agent_browser_update_egress_route` with only evidence-backed suffixes, the inspected revision, and an audit reason.
Agent-managed Route Extensions are persisted in Fleet state as canonical suffixes, a revision, and a route fingerprint; they contain no proxy endpoint. The extension, revision, audit event, and stopped runtime observation share one atomic state update. Revision comparison prevents lost concurrent updates. Base domains cannot be removed; single-label, IP, ICANN public-suffix, and private public-suffix entries are rejected; per-route capacity is bounded; and an Agent addition cannot overlap another route.
A successful update closes the calling session's cached Playwright MCP connection, cleanly stops the same Session Browser, re-reads the operator policy, and commits only if the post-stop plan exactly matches the pre-stop plan. After that atomic commit, Fleet performs one final policy read that compiles both an explicit inline-PAC or disabled-PAC launch snapshot and the corresponding current Route acceptance view. A successful result is returned only when that same view contains the committed Agent domains. Chromium startup consumes exactly that PAC snapshot and never falls back to another policy read, including when the final read says that the optional policy is absent. If the operator removes the policy or changes the route fingerprint after commit, Fleet restores the pre-call overlay in a new audited compensation revision, restarts the caller under the final PAC snapshot, and returns a conflict; this prevents a fingerprint-orphan domain from silently reactivating later. Acceptance compares the route fingerprint even when the update removes its last Agent domain. If the final policy cannot be parsed or compiled, Fleet performs the same compensation before returning the configuration error, preserves the Binding, and leaves the caller stopped rather than inventing or rereading a launch policy. The final read is the launch-policy linearization point, and later external changes apply to future launches. If the policy changed before commit, Fleet commits no extension and attempts to restore the caller's browser under the current policy. Other running browsers are not interrupted. A clean-stop failure commits nothing; if Chromium already exited before residual stack cleanup failed, Fleet force-cleans the stack and restores the caller where the operation requires it. After the final snapshot accepts the committed extension, a post-commit Runtime Start Error leaves that extension, reusable profile, Browser Allocation, and Identity Binding intact. Confirmed cleanup leaves a stopped READY slot for retry; failed cleanup leaves a conservatively running BROKEN slot for operator recovery. Policy-conflict restoration failures follow the same runtime-state rules while still committing no extension.
Routes are evaluated in file order and deliberately win over `NO_PROXY`. Remaining requests use this order:
1. Current `no_proxy` / `NO_PROXY` matches return `DIRECT` (domain suffixes, optional ports, exact IPv4/IPv6 literals, and IPv4/IPv6 CIDR are supported without DNS resolution).
2. HTTPS/WSS uses `https_proxy` / `HTTPS_PROXY`, falling back to the effective HTTP proxy; HTTP/WS/FTP uses `http_proxy` / `HTTP_PROXY`. For each pair, the first non-empty lowercase value wins, otherwise the uppercase value is used.
3. If no effective proxy exists, the result is `DIRECT`. `all_proxy` / `ALL_PROXY` is intentionally ignored because the Browser Egress Policy accepts only HTTP proxy endpoints.
`fallback: "direct"` makes non-selected traffic direct instead. `directPlainHostnames: true` additionally bypasses bare intranet hostnames. Proxy URLs are intentionally limited to unauthenticated `http://` endpoints; a selected route never adds `; DIRECT`, so an upstream failure cannot silently leak that route to another egress.
Ordinary Chromium launches read the current operator JSON file, Fleet-owned Route Extensions, and proxy environment. A Route Extension restart reads them before interruption and immediately after clean stop to authorize the commit, then reads them once more after commit to compile the actual launch PAC and Route acceptance snapshot. The PAC snapshot is explicitly inline or disabled; absence cannot be confused with a request to reread the provider. This enables an external devbox-sg discovery job to atomically refresh a dynamic IPv6 endpoint without hard-coding it in the plugin. A running Shared-Control browser keeps its launch policy until an explicit Session Browser Restart; Fleet never interrupts it merely because the operator file changed. PAC is injected as a base64 `data:` URL because Chromium ignores `file://` PAC on the target devbox. The URL is visible in Chromium's command line, so credentials and tokens are rejected.
Fleet does not start Whistle, mutate `http_proxy` / `https_proxy` / `no_proxy`, or touch system proxy services. The policy affects only Chromium processes launched through this Fleet runtime. It is URL-routing configuration, not a firewall or complete egress security boundary: a Chromium PAC engine failure and non-PAC transports such as direct sockets or WebRTC are outside its guarantee. Use OS-level network isolation when direct egress must be impossible.
## Runtime and security invariants
The Host adapter invokes `runtime-bin/devbox-chrome-debug` by absolute path with argv rather than a shell; it never resolves a user-level `devbox-chrome-debug`. The package exposes only the namespaced `dsh-agent-browser-fleet-runtime` and `dsh-agent-browser-fleet-chromium` bins so package archives preserve both scripts' executable modes, and does not claim the generic `devbox-chrome-debug` or `chromium` commands. Fleet invokes only start/stop/env, the user-gated `recover-stop` fallback, and non-mutating `fleet-doctor` paths through a sanitized environment; Fleet calls do not run sudo, start Whistle, modify system proxy settings, or honor external overrides for debugging addresses and VNC authentication.
CDP, x11vnc, and noVNC listeners must remain on loopback. CDP, VNC, and noVNC must all be present before a Session Browser is ready. Browser stack lifecycle commands are serialized because the bundled cleanup path is not concurrency-safe; a user release waits for an already-running browser operation in that same session instead of concurrently closing its Profile, while separate Session Browsers remain concurrent. Runtime pidfiles bind the PID generation, setsid PGID, role, and Fleet instance; stale or mismatched ownership is never signalled, and a still-live untrusted PID makes cleanup fail rather than masquerading as stopped. Explicit Runtime Recovery does not weaken this rule: after user intent it may reconstruct one role's pidfile only from a unique same-UID PGID leader carrying canonical Profile and runtime-root scope plus complete current-instance display/port evidence, and the ordinary signal path immediately revalidates that reconstructed generation. Auxiliary roles use a non-empty runtime marker or matching legacy runtime parent and instance evidence. Because Chromium can rewrite custom argv/environment after startup, its runtime-root proof is an inherited file descriptor resolving exactly to that instance's canonical, non-symlink Fleet log (with an exact readable legacy runtime parent only as fallback); any still-readable contradictory scope environment rejects ownership before log-FD proof is considered; a matching FD never overrides an explicit runtime-parent mismatch. The same pidfile-bound generation, UID, and PGID are checked immediately before TERM and again before TERM-to-KILL escalation. Role signatures require exact instance tokens. Because Chromium may rewrite its process title and drop launch flags, its profile proof is either a canonical `--user-data-dir` token or that profile's `SingletonLock` bound to the same PID, and its CDP proof is either the exact port token or the exact listener owned by that same PID. Legacy one-line pidfiles lack immutable generation metadata and can never authorize a signal. A ready X display is reusable only with a trusted generation-bound Xvfb pidfile for the same instance. `xdpyinfo` probes have a KILL-backed bound; startup readiness requires a responsive probe, while any probe failure remains occupied/inconclusive if an X lock or Unix socket exists. Stop cannot succeed while that display remains ready; an unowned residual Xvfb is never signalled or reported as cleaned. Runtime roots, instance directories, registries, locks, profiles, and pidfiles reject final symlinks, mismatched UIDs, and unsafe write permissions before use. Untracked orphan processes are not killed heuristically. Occupied listeners, a live same-UID Profile holder or `SingletonLock`, an exact pre-listen Xvfb/x11vnc/websockify role signature, or a still-ready X display all fail the stop postcondition and require operator recovery. Runtime readiness, status, and clean-close all verify the expected profile ownership before reporting or closing CDP. Identity switching starts an unbound replacement runtime before stopping the old binding and commits the binding swap atomically, so validation or target-start failures leave the old Session Browser intact. The Playwright MCP cache fingerprints the runtime generation as well as the slot and CDP endpoint, so a same-port restart cannot reuse a stale connection. Only a verified Controllable Browser Runtime is reported as running or may resolve MCP, VNC, screenshot, restart, or Route-update control. An incomplete, non-loopback, unobservable, quarantined, or conservatively tracked BROKEN Session/Template Browser exposes no control target until cleanup and verified recovery. If durable cleanup tracking itself cannot be written, Fleet preserves the Runtime Start Error classification and Identity Binding, retries force cleanup, and quarantines any still-unconfirmed Slot in memory so no other session, restart, capture, MCP, or VNC path can reuse it. Host initialization re-establishes that quarantine whenever ownership/status observation fails; only successful observation plus the required cleanup/recovery releases it.
The default dsh-session access mode requires no separate Fleet login. A browser-originated request from a trusted DSH authority exchanges the same-origin page context for an HttpOnly, SameSite=Strict Fleet cookie whose name is scoped to that authority, including its port. This prevents stable and isolated development DSH instances on the same hostname from overwriting each other's Fleet cookie. Cross-site and non-browser requests cannot bootstrap it. DSH Web does not expose a user principal to plugins, so this mode inherits the DSH Web origin as its trust boundary. The release endpoint therefore grants Host-wide management authority to any same-origin code holding the Fleet cookie and must be reassessed before using one DSH Host for mutually untrusted users. Administrators may set AGENT_BROWSER_FLEET_AUTH_MODE=token for a separate Host-local capability token.
The noVNC WebSocket route checks trusted authority, same-origin Origin, and the Fleet cookie before resolving a Session Browser or template browser. The gateway connects only to 127.0.0.1 on the resolved VNC port and performs VNC password authentication server-side.
A same-UID Host process can still enumerate and connect to loopback CDP. Strong protection against such a process requires Chromium remote-debugging pipe, a separate OS identity, or a network namespace. LAN clients have no direct VNC or CDP path.
## Prerequisites
Fleet requires DSH `>=0.1.2-rc.1 <0.2.0`; prereleases of a later 0.1.x minor are outside this npm range until their stable release. The Web client consumes the rc.1 session-controller API and remains loadable when the optional Better Sidebar service is absent; when both Better Sidebar and client sessions are available, their registrations are owned by a disposable Cordis child fiber.
Fleet bundles its browser-stack orchestration script and invokes it directly; no global `devbox-chrome-debug` command or `devbox-chrome-cdp` Skill installation is required. The authenticated Fleet doctor is available at `GET /api/agent-browser-fleet/doctor` and checks only Fleet runtime requirements.
Required host components are Linux, executable `/usr/bin/chromium`, Xvfb, x11vnc, noVNC/websockify, Bash, curl, OpenSSL, `ss` from iproute2, and GNU userland including `cp --reflink`. The plugin does not install system packages.
## Development
pnpm install
pnpm run check
pnpm run check runs TypeScript checking, all Node tests, Host/client builds, and a packed-artifact test that loads the Host entry and verifies the client ModuleLoader bundle and package-local regular-file Patch.
The real Profile experiment creates authenticated template state, closes Chromium cleanly, clones two isolated persistent profiles, mutates them concurrently through Playwright, restarts them, and verifies storage persistence plus loopback listeners:
pnpm experiment:profiles
The machine-readable report is written to .experiment/latest/experiment-report.json.
Use an isolated PDM-managed DSH development instance to load and validate the bundle. Do not install or restart stable DSH directly; stable rollout must use the PDM stable-update queue after an explicit user request.
## Known limits
- Website single-session policies and token rotation can invalidate an Identity externally.
- NEEDS_LOGIN is not inferred from website-specific probes.
- Browser Allocation has no configured occupancy ceiling; host filesystem and process resources remain physical limits.
- Full browser-driven WebSocket/RFB validation remains separate from route and VNC protocol tests.
- Playwright MCP image blocks are not yet ingested as DSH model attachments.
## Design and validation
- Domain language: CONTEXT.md
- Accepted architecture decisions: docs/adr/0001-session-browser-playwright-mcp.md, docs/adr/0002-durable-explicit-session-browser-lifecycle.md, docs/adr/0003-host-browser-egress-policy-via-inline-pac.md, docs/adr/0004-agent-managed-browser-egress-route-extensions.md, docs/adr/0005-user-directed-release-and-unbounded-allocation.md, and docs/adr/0006-explicit-fail-closed-runtime-recovery.md
- Architecture and scope: docs/PLAN.md
- Reproducible validation record: docs/VALIDATION.md
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues