Skip to content

Commit 6c26d68

Browse files
authored
fix: repair sandbox browser driver lifecycle (#186)
## Summary - Fixes browser initialization that was blocked by the driver's own global provider-fetch restriction. - Gives the driver explicit ownership of Chromium so startup failures and shutdowns cannot leak browser processes. - Makes readiness represent a fully initialized Stagehand context and reduces stale-driver detection from 90 seconds to 5 seconds. - Disables PTY echo before credentials are delivered over stdin so secrets cannot enter Daytona session logs. ## Context Production acceptance QA exposed this incident directly; there is no separate Linear issue or plan document. ## Architecture The browser driver now has two explicit transport boundaries: 1. A provider-scoped, response-bounded fetch implementation injected into Stagehand's AI SDK client. 2. Native loopback CDP transport whose WebSocket endpoint is fetched with a byte limit and validated against the exact owned Chromium port. The driver launches Chromium itself, attaches Stagehand through the validated CDP URL, and retains the launch handle for deterministic cleanup. The readiness endpoint returns success only after Stagehand and the browser connection guard are installed. ## Decisions Made | Decision | Choice | Alternatives Considered | Reasoning | |---|---|---|---| | Provider egress enforcement | Inject a restricted fetch into the AI SDK provider | Replace global fetch and allow-list local CDP | Keeps provider security policy scoped to provider I/O and cannot interfere with browser internals | | Chromium lifecycle | Driver-owned chrome-launcher process | Let Stagehand launch Chromium | Gives the driver a handle that can always be killed, including partial initialization failures | | Readiness | Eager initialization with stateful readiness | Lazy initialization from health/actions | Prevents the process manager from accepting an unusable driver | | Credential delivery | Disable PTY echo before stdin bootstrap | Redact logs after capture | Prevents secret persistence at the source | ## Edge Cases Handled | Scenario | Handling | |---|---| | CDP response is oversized or points off loopback | Reject before Stagehand attaches | | Stagehand fails after Chromium launches | Close Stagehand, kill the exact Chromium process, log a sanitized diagnostic, and exit non-zero | | Driver receives SIGINT, SIGTERM, or expires | Close Stagehand and kill the owned Chromium process | | Existing driver is stale | Health probe fails after 5 seconds and is replaced | | Daytona PTY echoes stdin | Echo is disabled before bootstrap input is sent | | Diagnostic contains a request-scoped secret | Exact credentials and provider-key patterns are redacted before the bounded message is written | ## How to Review 1. Start with infra/containers/sandbox/browser-driver/server.js for transport ownership, readiness, and cleanup. 2. Review infra/containers/sandbox/scripts/start-browser-driver.sh for the credential-delivery boundary. 3. Review packages/agent-core/src/tools/browser/actions.ts for stale-driver detection. 4. Confirm the lock and sandbox README match the implementation. ## Verification - [x] pnpm lint - [x] pnpm typecheck - [x] pnpm turbo build --force - [x] pnpm deadcode - [x] pnpm architecture:check - [x] pnpm turbo skills:build - [x] node --check infra/containers/sandbox/browser-driver/server.js - [x] git diff --check - [ ] Protected immutable sandbox snapshot build - [ ] Promote the reviewed snapshot in the agent-worker configuration - [ ] Repeat the production Pomodoro build and browser verification flow - [ ] Confirm bootstrap credentials are absent from the new Daytona session log - [ ] Confirm driver termination leaves no owned Chromium process
1 parent c730ec0 commit 6c26d68

6 files changed

Lines changed: 245 additions & 75 deletions

File tree

infra/containers/sandbox/README.md

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -110,14 +110,16 @@ Stagehand currently resolves `@ai-sdk/provider-utils` 3.0.29. That release conta
110110
the bounded JSON-response reader that Vercel shipped in 3.0.28. GitHub's current
111111
`GHSA-866g-f22w-33x8` range nevertheless marks every 3.x version through 3.0.97
112112
affected, so `npm audit` reports the resulting low-severity transitive paths. The
113-
driver independently caps every non-streaming provider response and restricts
114-
Node fetches to the exact selected Anthropic, Google, or OpenAI API hostname,
115-
containing both that resource-consumption surface and the related download-URL
116-
SSRF advisory. Chromium navigation remains inside the isolated sandbox and does
117-
not use this provider transport. Keep the exception visible and reassess it with
118-
each Stagehand/AI SDK release; do not apply npm's suggested breaking Stagehand
119-
downgrade. Static checks fail on moderate-or-higher findings across every
120-
sandbox lock without hiding this low-severity report.
113+
driver injects a bounded, provider-scoped fetch implementation into Stagehand's
114+
AI SDK client and restricts it to the exact selected Anthropic, Google, or OpenAI
115+
API hostname, containing both that resource-consumption surface and the related
116+
download-URL SSRF advisory. The driver owns Chromium's lifecycle explicitly and
117+
uses the native transport only for its validated loopback CDP connection;
118+
Chromium navigation does not use the provider transport. Keep the exception
119+
visible and reassess it with each Stagehand/AI SDK release; do not apply npm's
120+
suggested breaking Stagehand downgrade. Static checks fail on
121+
moderate-or-higher findings across every sandbox lock without hiding this
122+
low-severity report.
121123

122124
Snapshot publication builds and scans the exact local AMD64 image before pushing it
123125
to Daytona. Trivy fails on every fixable medium-or-higher vulnerability and every
@@ -137,8 +139,9 @@ user-programmable sandbox. Project commands run as `node`; only the immutable
137139
launcher may be started through the narrow sudo rule, and that launcher drops to
138140
the separate `cheatcode-browser` Unix user. The Agent Worker sends the
139141
request-scoped model key and driver bearer token once over the Daytona session's
140-
stdin. They are never command arguments, environment variables, workspace files,
141-
or persisted process metadata. The driver runs from its mode-0700 home with core
142+
stdin after terminal echo is disabled. They are never command arguments,
143+
environment variables, workspace files, session logs, or persisted process
144+
metadata. The driver runs from its mode-0700 home with core
142145
dumps disabled, deletes provider-key environment names defensively, expires
143146
after 55 minutes, and requires both the bearer token and run ID. Worker calls
144147
reach it through a short-lived Daytona-signed port URL; arbitrary workspace code

infra/containers/sandbox/browser-driver/package-lock.json

Lines changed: 3 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

infra/containers/sandbox/browser-driver/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
},
1111
"dependencies": {
1212
"@browserbasehq/stagehand": "3.7.0",
13+
"chrome-launcher": "1.2.1",
1314
"playwright-core": "1.61.1",
1415
"zod": "4.4.3"
1516
},

0 commit comments

Comments
 (0)