Sandbox and Browser Use
Station supplies two separate server execution primitives: native shell workspaces and live browser sessions. A Headquarters service can expose their authenticated API while private, specialized workers own the resources. Use the host backend for trusted work. Customer execution requires tenant-scoped authorization and isolated, network-restricted container backends.
This page is the configuration and security reference. The illustrated guides above explain the lifecycle before you wire the primitives into a network.
For a step-by-step sandbox walkthrough, read the detailed Sandbox guide. It covers custom tool installation, Git credentials, dashboard pages, tenant API payloads and the different recovery behavior of host and container backends.
| Primitive | Runs where | Purpose |
|---|---|---|
| station-sandbox | POSIX worker | Persistent files, native Bash and bounded commands through a SandboxAdapter. |
| station-browser-use | Server browser worker | Navigation, interaction, evaluation and screenshots through Bun WebView or Playwright. |
| station-browser | Web Worker/service worker | Browser-local Station signals, DAGs and beacons with IndexedDB. |
Browser Use does not require a Sandbox workspace. The separate browser runtime executes Station inside the browser; it does not run Bash or control server browser sessions. For this checkout, use workspace dependencies and the execution-network example. These additions are prepared for Station 3.0.0 and still require release and target deployment validation.
Hosted browsers and access reliability
Browser Use supports Browserbase and Steel through provider-managed sessions and Playwright CDP. The existing agent tools, screenshots and timed recordings use the selected worker. Provider credentials and profile grants stay in worker configuration. Read the remote browser guidefor proxy settings, tenant deployment requirements and session cleanup.
Remote adapters enable action pacing and challenge checks; local and container Playwright can opt in. Diagnostics reports challenges, blocked pages and throttling. Pause the agent for human Live takeover when needed. These checks do not guarantee CAPTCHA-free access. Remote downloads and trace exports remain disabled pending provider-specific artifact handling; uncertain session creation requires reconciliation.
Persistent accounts and media
For WhatsApp, TikTok and Instagram workflows, retain a separate profile grant per tenant and account, then open short-lived sessions with that profile. A browser closing is not an account logout. Steel saves profile changes on release; the operator must wait until the profile is READY before reuse. Authentication can still require human renewal.
WhatsApp QR linking was tested through Steel and Station dashboard Live view. Authenticated profile reuse and TikTok/Instagram workflows remain unverified. Browser uploads currently accept at most 16 files and 4 MiB of decoded data per command. Large-video staging and remote provider downloads still need artifact integrations; uploading a file and publishing it are separate actions. Read the account and media workflow guide for lifecycle, transfer limits and acceptance checks.
Native trusted workspaces
import { HostSandboxAdapter } from "station-sandbox"; const sandboxes = new HostSandboxAdapter({ rootDir: "/data/workspaces", maxEnvironments: 8, maxConcurrent: 3, maxOutputBytes: 256 * 1024, maxTimeoutMs: 300_000, maxHistoryPerSandbox: 100, env: { PATH: "/opt/tools/bin:/usr/local/bin:/usr/bin:/bin" },});const workspace = await sandboxes.create();const started = await sandboxes.exec(workspace.id, { command: "node --version && git --version && printf hello > greeting.txt", timeoutMs: 30_000,});// Poll this until finishedAt before treating the result as final.const result = await sandboxes.command(workspace.id, started.id);console.log(result.status, result.stdout, result.stderr);// Graceful shutdown interrupts commands and preserves saved files.await sandboxes.close();Install Bash, Node, Git and other native tools in the worker image or host. Commands use those real programs; Unix is not emulated. Each command starts a fresh noninteractive shell with a separate workspace home. Files persist; shell bindings do not. The file API supports bounded reads, writes, uploads and directory listings. An optional relative cwd must resolve inside the workspace.
The host-process adapter advertises isolated: false and pty: true when explicitly enabled on a Node controller. Commands can access everything permitted to the worker's OS user, including other workspaces. Directory validation and explicit environment variables organize trusted work; they are not a security boundary. Provision OS/container CPU, memory, disk and process limits separately.
Defaults are 20 workspaces, four concurrent commands, 256 KiB of combined captured output, a 30-second timeout with a configurable five-minute maximum, and 100 retained completed commands per workspace. Output is byte-bounded and UTF-8 aware; older completed command IDs expire. Ordinary descendants are cleaned up on shell exit, cancellation, timeout and shutdown. Deliberately escaped process groups are outside this backend's guarantees.
Container workspaces use a stronger cancellation boundary: cancelling or timing out a command, stopping a running service, or closing a live terminal stops the entire workspace container, including escaped process sessions. Other active commands, terminals and services become interrupted; their restart policies do not replay them. Files and installed tools remain on the persistent volume. The next file, command, terminal or service operation waits for old handles to finish and starts a fresh container; restart interrupted services explicitly. Other workspaces are unaffected. Failed containment leaves this workspace unavailable. Ordinary completed commands use controller-captured process identity for descendant cleanup; workload-written files never establish ownership.
Install tools into a workspace
npm install --global --ignore-scripts --no-audit --no-fund /data/custom-tool.tgz# After installation succeeds, run its binary by name in a new command:custom-tool --versionCommands prepend workspace/node_modules/.bin and HOME/.local/bin to the configured or host PATH. npm's global prefix defaults to HOME/.local, so custom tools stay in the workspace home. Project-local npm binaries take precedence over workspace global tools. npm and required native dependencies must already be installed on the worker; a dependency-free local tarball can be installed offline.
Tools survive fresh shells and worker restarts when the workspace volume survives. Other workspaces do not gain them through PATH. This does not restrict filesystem access: trusted commands retain the OS user's permissions. An explicit env.NPM_CONFIG_PREFIX override changes installation location; include its bin directory in env.PATHwhen required.
Terminals, services and files
const sandbox = new HostSandboxAdapter({ rootDir: "/data/workspaces", enablePty: true,}); // Install optional node-pty; controller must run Node.const ws = await sandbox.create();await sandbox.writeFile(ws.id, "hello.txt", { base64: Buffer.from("hello").toString("base64"),});const terminal = await sandbox.openTerminal(ws.id, { cols: 100, rows: 24 });await sandbox.terminalInput(ws.id, terminal.id, "node --version
");const output = await sandbox.terminal(ws.id, terminal.id, 0);await sandbox.resizeTerminal(ws.id, terminal.id, 120, 30);const service = await sandbox.startService(ws.id, { name: "app", command: "node server.js", restart: { policy: "on-failure", maxRestarts: 5, delayMs: 1000 },});await sandbox.stopService(ws.id, service.id);Terminal output uses byte offsets and a bounded retained buffer. Reconnect while the worker lives; restart interrupts the shell. Service restart policy is explicit, bounded and stored with service intent. File APIs reject traversal and symlinks; the trusted host backend still cannot confine commands to those paths.
Isolated container workspaces
import { ContainerSandboxAdapter } from "station-sandbox/container";const sandbox = new ContainerSandboxAdapter({ rootDir: "/data/container-state", image: "your-registry/station-tools@sha256:YOUR_VERIFIED_DIGEST", engine: "docker", // Podman is also supported. network: "none", memoryMb: 512, cpus: 1, pidsLimit: 128, enablePty: true, seccompProfile: "/etc/station/seccomp.json", // Reviewed deny-default policy.});await sandbox.ready();Provision a Linux engine and pre-pull an operator-controlled image containing Node, Bash and setsid. Each workspace has a nonroot container and persistent named volume. The adapter drops capabilities, uses a read-only root, bounds CPU/memory/PIDs and exposes no engine socket or arbitrary host mounts to workload code. Engine access belongs exclusively to the controller. It never falls back to host execution.
Both ContainerSandboxAdapter and ContainerBrowserAdapter require enforced seccomp. If the engine default is unconfined, supply an absolute seccompProfilepointing to an operator-reviewed deny-default JSON policy. Station validates and stages a private copy. Missing or unsafe policy fails admission; it does not change global engine settings. This policy is independent of the network and disk controls below.
Network access defaults to none. Public customers must not receive unrestricted bridge networking: protect cloud metadata, private networks and other tenants with an operator-enforced egress policy. Named volumes need storage-level disk quotas; command output limits do not limit what a program can write to disk. Root ownership locks are local, not distributed fencing.
Agent-controlled browser workflows
Mount Browser Use as agent tools; use the dashboard to observe sessions or take temporary human control. The toolset binds to one authenticated worker and scopes resource IDs to a workflow.
import { BrowserUseClient, createBrowserAgentTools } from "station-browser-use/agent"; const tools = createBrowserAgentTools({ client: new BrowserUseClient({ baseUrl: "https://station.example.com", stationId: "tenant-browser-worker", apiKey: process.env.STATION_EXECUTION_KEY!, }), maxSessions: 2, profileIds: ["research"],});// Mount name, description, inputSchema and execute(input, { signal }).// Feed result.images into the model's native image input channel.try { await runYourAgent(tools); }finally { await tools.close(); }Tools cover opening sessions, DOM/ARIA observation, navigation, structured interactions, screenshots, checkpoints, resume and close. Each name starts with station_browser_. The model cannot choose credentials, tenant identity, worker or a human-control token. Retained profiles, sessions and checkpoints require explicit host grants.
Screenshot bytes are returned separately from text. The Foundry example maps them to native model image observations after tool results have been committed. Ordinary JSON containing base64 does not provide vision. Treat website content as untrusted data. Text results are bounded and explicitly marked when truncated.
The client defaults to tenant execution-only API keys. Operator access requires an explicit development configuration. Requests refuse redirects and never retry mutations; an unknown transport outcome requires checking worker state. Cleanup respects human control and surfaces failures for retry after the lease releases.
An uncertain open or resume blocks further admission in that toolset. uncertainOpenings() reports the count and cleanup reports unresolved sessions. The host must reconcile before creating another toolset; definitive missing-session responses during close release local capacity.
The Foundry browser-agent example shows the complete tool and image bridge. Its sample budget is 14 turns, 600 output tokens per call and one live session, with no agent-level retries. Replace its in-memory conversation store with your application's durable storage when retaining history across runs.
Independent browser sessions
import { BrowserSessionManager } from "station-browser-use";import { BunBrowserAdapter } from "station-browser-use/bun"; const browsers = new BrowserSessionManager(new BunBrowserAdapter({ bunPath: "bun", backend: "chrome", operationTimeoutMs: 30_000,}), 3);try { const session = await browsers.open(); await browsers.perform(session.id, "navigate", "https://example.com"); const title = await browsers.perform(session.id, "evaluate", "document.title"); const image = await browsers.perform(session.id, "screenshot"); // image: { mimeType: "image/png", base64: string } console.log(title, image); await browsers.closeSession(session.id);} finally { await browsers.close();}Bun uses a dedicated subprocess per session with an ephemeral profile. Install a Bun version providing WebView and a compatible Chromium binary; chromePath can select its executable. The default backend is Chrome; WebKit is an explicit macOS-only option. A Node Station controller can manage these Bun children without migrating its own runtime.
// Alternatively use the optional Playwright peer and installed Chromium:import { PlaywrightBrowserAdapter } from "station-browser-use/playwright";const browsers = new BrowserSessionManager( new PlaywrightBrowserAdapter({ timeoutMs: 30_000 }), 3,);Both adapters support navigate, evaluate, click, type, press and screenshot. Use CSS selectors; focus an element before typing. Evaluation returns JSON-compatible values; wrap multiple statements in an IIFE for Bun. Screenshots capture the current viewport as PNG. The manager rejects concurrent actions on the same handle with busy.
Browser sessions have independent lifecycles and are not tenant isolation boundaries. Playwright supports persistent profiles, multiple pages, structured form actions, uploads/downloads and operator-configured proxy settings. Configure profileRootDir to retain cookies across sessions. Live tabs and process memory are still lost on restart. The manager expires idle sessions and retains bounded audit metadata. Bun WebView is experimental. Both adapters passed real Chromium checks on macOS and in a Debian ARM64 container. Linux fixture tests disable Chromium's own sandbox; they do not establish production isolation, Railway deployment support, or a throughput/memory advantage.
Direct navigation and new-page URLs accept absolute HTTP(S) addresses without embedded credentials, plus about:blank. Local files, executable URLs and internal browser pages are rejected. Playwright also guards document requests and closes unexpected non-web popup or frame navigations. Ordinary about:srcdoc frames and Chromium network-error pages are allowed. These checks are not a domain or IP egress firewall. Host Playwright launches Chromium with an allowlisted environment and private temporary home, excluding worker secrets and loader overrides; this is credential hygiene, not OS isolation.
Profiles, page tools and durable recordings
const browsers = new BrowserSessionManager( new PlaywrightBrowserAdapter({ profileRootDir: "/data/profiles" }), 3, { recordingRootDir: "/data/recordings", stateRootDir: "/data/browser-state", idleTimeoutMs: 900_000 },);const session = await browsers.open({ profileId: "research" });await browsers.execute(session.id, { op: "newPage", url: "https://example.com" });await browsers.execute(session.id, { op: "fill", selector: "#query", value: "Station" });const pages = await browsers.execute(session.id, { op: "pages" });const recording = browsers.startRecording(session.id);Structured operations include fill, select, check, hover, scroll, waitFor, content, history navigation, page management and bounded upload/download artifacts. Bun advertises only its supported basic capabilities; the dashboard hides unsupported tools. A profile can be open only once per owning manager, and storage roots must have exactly one live owner. Download and trace artifacts are bounded and session-owned. Configure a separate stateRootDir for durable action history/checkpoints; recordings and saved profiles have their own storage roots.
Semantic targets and page inspection
await browsers.execute(session.id, { op: "click", target: { by: "role", role: "button", name: "Continue", exact: true },});await browsers.execute(session.id, { op: "fill", target: { by: "label", value: "Code", frame: ["#payment-frame"] }, value: "1234",});const page = await browsers.execute(session.id, { op: "inspect", maxElements: 100, maxTextLength: 256,});const accessible = await browsers.execute(session.id, { op: "accessibility", depth: 10, boxes: true,});Targets support selector, role/name, text, label and testId, optional exact matching, an iframe-selector chain of up to eight frames, and an explicit zero-based nth match. Ambiguous matches fail. Existing form/upload/download operations accept exactly one selector or target; click, focus and press are structured operations too.
Inspection returns bounded DOM metadata, truncation and coordinateSpace. Password and file input values are omitted. Defaults are 100 elements and 256 characters per field; maxima are 500 elements, 4096 characters and 4 MiB total output. Accessibility returns an ARIA YAML snapshot, bounded to 1 MiB and depth 20. Frame-targeted DOM boxes use that frame's viewport; main-page boxes use the main viewport. Re-resolve targets after the page changes.
mouseClick and mouseMove use main-viewport CSS-pixel coordinates. drag takes source and destination targets; dragCoordinates takes from/to points and bounded movement steps. A one-shot dialog policy can accept or dismiss the next selected-page dialog, optionally supplying prompt text. It expires after ten seconds by default (maximum thirty seconds). Unexpected or expired dialogs dismiss immediately, so actions do not wait for a later RPC to resolve a modal.
Live viewing and human control
const lease = browsers.acquireControl(session.id); // Default 30 seconds.try { await browsers.execute(session.id, { op: "mouseClick", x: 200, y: 120, }, lease.token); const frame = await browsers.liveFrame(session.id); // Renew before expiry when keeping manual control: browsers.renewControl(session.id, lease.token);} finally { browsers.releaseControl(session.id, lease.token);}Live view polls current PNG frames with timestamps; it is not a video stream. Busy frames are skipped and viewing does not extend idle lifetime. acquireControl grants an exclusive live lease for 1–120 seconds. While held, actions require its token, so automation cannot interleave browser input. control reports mode/expiry without exposing the token. Renew or release intentionally; expiry returns control to automation. Leases do not survive worker restart. Remote close honors the lease; direct lifecycle cleanup remains available to the worker.
Diagnostics and trace artifacts
import type { BrowserArtifact } from "station-browser-use";const diagnostics = await browsers.execute(session.id, { op: "diagnostics" });// Console text is intentionally absent unless explicitly enabled for future events:await browsers.execute(session.id, { op: "diagnostics", consoleText: true });await browsers.execute(session.id, { op: "traceStart" });// Perform the browser actions to investigate, then export:const trace = await browsers.execute(session.id, { op: "traceStop" }) as BrowserArtifact;const zip = await browsers.execute(session.id, { op: "downloadRead", artifactId: trace.id,});await browsers.execute(session.id, { op: "downloadDelete", artifactId: trace.id });Diagnostics retain at most 200 console, network and dialog events. Default events omit console text, headers and bodies; URLs omit credentials, queries and fragments. Opt-in console text is bounded to 2 KiB per event and can still contain arbitrary application secrets. Turning it off purges retained console text; clear removes the ring. Dialog events contain type/action, not prompt contents or answers.
Tracing is explicit and can contain sensitive screenshots, DOM and network/action data. ZIP files share the session's download artifact count/byte budget and expire on close. Traces abort and discard partial data at sixty seconds or their monitored raw-file budget. A failed/limited trace does not export a partial archive. Disk growth is sampled every fifty milliseconds; enforce a filesystem/container quota for a hard transient limit. Bun advertises these richer capabilities as unsupported.
Durable action history and explicit resume
const browsers = new BrowserSessionManager(adapter, 4, { stateRootDir: "/data/browser-state", recordingRootDir: "/data/browser-recordings", auditLimit: 1000, // tenantId: "customer-a", // Required for already tenant-bound roots.});const checkpoint = await browsers.checkpoint(session.id);await browsers.closeSession(session.id);const replacement = await browsers.resumeCheckpoint(checkpoint.id);const journal = browsers.audit();await browsers.deleteCheckpoint(checkpoint.id);stateRootDir enables an atomic single-owner journal separate from recordings and profiles. Action starts are persisted before execution and finishes afterward, with monotonic sequence numbers. Write failures stop action admission. A started entry without a finish has an unknown outcome; do not automatically replay the mutation. Default retention is the latest 1000 entries (maximum 10000), with an 8 MiB combined state limit. This bounded operational history is not an immutable compliance archive. Tenant identity is verified before recovery or retention can modify stored data.
Checkpoints record backend, validated open options, selected page and sanitized HTTP(S) origin/path URLs; credentials, query strings and fragments are omitted. about:blank is also supported, with at most 64 checkpoints. Resume explicitly opens a new session on the same backend and navigates those URLs. It never runs automatically on startup. Persistent profiles can restore saved cookies after old ownership ends; they do not restore JavaScript memory, filled forms, pending requests or exact workflow progress. Re-navigation can itself have effects. Keep roots distinct, preserve their logical owner and use external fencing for failover.
Use the Headquarters dashboard
Sign in to Headquarters with its configured administrator account. /sandboxes provides worker selection, workspace creation, commands/output, cancellation, interactive terminals, supervised services, files and deletion. /browser-useprovides separate browser session controls, navigation, interaction and screenshots, profiles, pages, semantic/frame targeting, live human control, inspection, diagnostics/traces, checkpoints and recording playback. Both use Headquarters as their public entry point.
The admin-only GET /api/v1/execution endpoint discovers advertised workers and returns their identities, statuses, capabilities, backend names and availability. Capabilities are not guessed from labels. Discovery still requires selecting the exact owner; it does not schedule or migrate resources. The internal worker token stays between services.
Record screenshots and play them back
Select a live browser and choose Start recording. The worker captures a viewport PNG immediately and then every five seconds, even when the dashboard is closed. Busy browser operations skip a capture rather than queueing screenshots. This is a sequence of still frames, not a video or a complete audit of every action.
Select a recording to play, pause or scrub through timestamped frames. Closing a browser stops its recording and keeps captured frames available. Defaults are 120 frames per recording, 16 retained recordings, and 64 MiB of PNG data across the worker manager. Reaching a limit stops capture and preserves existing frames. Delete recordings to release space. Recordings use memory by default. Configure recordingRootDir on persistent storage to survive worker replacement; recordingTtlMs defaults to seven days. A recovered recording is stopped: the platform does not reconstruct the old browser.
const recording = browsers.startRecording(session.id);// Later, stop capture without closing the browser:await browsers.stopRecording(recording.id);const metadata = browsers.getRecording(recording.id);const image = browsers.recordingFrame(recording.id, metadata.frames[0].id);// image: { mimeType: "image/png", base64: string }await browsers.deleteRecording(recording.id);The same owner-routed admin endpoint supports recordingStart (session id), recordingStop, recording, recordingDelete (recording id), recordings (list), and recordingFrame (recording id and frameId). Metadata omits PNG payloads; playback fetches individual frames on demand.
Route through the exact owner
Configure three services: public Headquarters, private Sandbox worker and private Browser Use worker. All share Station's network ID and durable coordination adapters. Workers configure execution.sandbox or execution.browser; Headquarters needs the shared execution.token. This service token must contain at least 32 characters and stays between trusted services. Public clients use a separate admin API key or authenticated admin session.
// Worker configuration fragment, merged with normal network/storage config:execution: { token: process.env.STATION_EXECUTION_TOKEN!, sandbox: sandboxes }// Browser worker: execution: { token, browser: browsers }// Headquarters: execution: { token } // All public calls are JSON POST requests:/api/v1/stations/:stationId/execution/sandbox/api/v1/stations/:stationId/execution/browser// Sandbox request bodies:{ "method": "create" }{ "method": "exec", "id": "WORKSPACE_ID", "command": "node --version", "timeoutMs": 30000 }{ "method": "command", "id": "WORKSPACE_ID", "runId": "COMMAND_ID" }{ "method": "cancel", "id": "WORKSPACE_ID", "runId": "COMMAND_ID" }{ "method": "destroy", "id": "WORKSPACE_ID" } // Browser request bodies:{ "method": "open" }{ "method": "execute", "id": "SESSION_ID", "command": { "op": "inspect" } }{ "method": "liveFrame", "id": "SESSION_ID" }{ "method": "controlAcquire", "id": "SESSION_ID", "ttlMs": 30000 }{ "method": "checkpoint", "id": "SESSION_ID" }{ "method": "action", "id": "SESSION_ID", "action": "navigate", "value": "https://example.com" }{ "method": "action", "id": "SESSION_ID", "action": "screenshot" }{ "method": "close", "id": "SESSION_ID" }Successful responses wrap results in data. Both primitives also support list; Sandbox supports get. Keep the selected owner station ID with every resource ID. This API does not automatically place environments or persist distributed session ownership. Existing signal queue placement remains separate.
Headquarters rejects offline, expired-lease and wrong-network owners, follows no redirects and never forwards the public API key to a worker. Draining blocks new work and browser actions while preserving Sandbox inspection/cancellation/deletion and browser list/close operations. It does not reroute a live resource to another station. Requests are capped at 128 KiB for ordinary operations; file uploads allow an 8 MiB JSON envelope with at most 4 MiB decoded content. Successful proxied responses are capped at 33 MiB including JSON/base64 overhead. A timeout can leave the operation's outcome unknown: inspect the owner before repeating create, open, exec or any other mutation.
Public tenant execution
Keep the dashboard and operator API restricted to your staff. Customer keys must have only the execution scope; Headquarters maps their verified key record IDs to tenant IDs. Each private worker is dedicated to one tenant. Both Headquarters and the worker check ownership, and tenant mode refuses host or unrestricted-network backends. Persisted owner bindings prevent reusing a data root for a different tenant.
// Headquarters: operator-owned configurationexecution: { token: hqSecret, targets: { "worker-a": { endpoint: "https://worker-a.internal", token: workerASecret, tenantId: "customer-a", } }, tenants: { apiKeyTenants: { "VERIFIED_KEY_RECORD_ID": "customer-a" } },}// Dedicated private worker, with an isolated/restricted adapter:execution: { token: workerASecret, tenantId: "customer-a", sandbox }// Customer endpoints:// GET /api/v1/tenant/execution// POST /api/v1/tenant/stations/:stationId/execution/:primitiveConfigure independent operator authentication on each tenant worker. Headquarters pins the endpoint, tenant and distinct worker credential in execution.targets; heartbeat advertisements cannot change those grants or redirect credentials. Tenant workers reject missing tenant, worker or network assertions. Use HTTPS for private remote targets.
Session cookies default to Secure, including behind TLS termination. Set auth.secureCookies to false only for local HTTP development. The trustedProxies configuration accepts explicit ingress IP addresses; untrusted forwarded headers cannot change rate-limit buckets. Dashboard binding uses STATION_DASHBOARD_HOST with a loopback default, ignoring ambient container HOSTNAME.
The default daemon FileLogStorage streams disk reads and keeps a current and previous segment, each bounded to 64 MiB by default. Pending writes are limited to 4 MiB; queries retain at most the newest 10,000 matching entries or 4 MiB. Oversized or over-capacity writes are dropped and reported through onError. These configurable limits provide bounded operational logs, not a complete audit archive or distributed log. Use a suitable custom store for that requirement.
ContainerBrowserAdapter from station-browser-use/container runs each Playwright session inside a separately constrained Linux container with an immutable image worker. Profile volumes and recordings retain their tenant ownership. Default networking is disabled. An operator-enforced named egress network is required for permitted internet browsing; a configuration flag alone does not install that policy.
The included Linux deployment profile under scripts/execution-container/enforcedprovisions a dedicated internal Docker network, an HTTPS CONNECT proxy that connects to validated public IPv4 addresses, host firewall rules and XFS project quotas. Configure its verified network and proxy on ContainerBrowserAdapter, and use profileStorageRoot for quota-backed profiles. The immutable image includes a syscall guard that prevents browser descendants from changing quota metadata.
This profile requires local rootful Linux Docker and XFS project-quota support. Direct internet connections, ordinary HTTP, UDP/QUIC, IPv6 and private destinations are denied. Both stateRootDir and recordingRootDir are required for public browser workers; place them under the same tenant quota tree. Verify the policy before starting workers and after host networking changes. Run pnpm test:execution:policy for fast proxy/verifier regressions and the supplied live harness for real network and disk-exhaustion checks.
Read the tenant deployment contractfor image builds, key provisioning, storage quotas, egress controls and rollout checks. These APIs supply execution boundaries; customer onboarding, billing, automatic provisioning and distributed failover remain responsibilities of the surrounding platform.
Persistence and service deployment
Use one process/replica per stable worker ID and one manager per Sandbox root. Mount persistent storage for workspace files and Station data; persist Headquarters' data directory for API keys and session secrets. Shared Postgres coordinates Station membership, jobs and schedules; it does not store browser memory or workspace files. A service volume is not a shared multi-worker filesystem.
On restart, leftover running command records become interrupted and are never automatically replayed. Saved files and configured profiles/recordings can survive; processes, shells and browser sessions do not. Supervisors must reap the old process tree before a replacement takes ownership. Workers need private reachable HTTP endpoints, packaged tools and browser libraries, and required outbound access. Disable sleeping when retaining live sessions.
The example describes an ordinary service deployment contract. Linux primitive checks passed separately from the local SQLite/PostgreSQL dashboard tests; a Railway deployment remains unvalidated. Automatic placement, distributed ownership, migration, high availability, customer onboarding and billing require a platform layer beyond these execution primitives.
Exercise the full dashboard topology
pnpm test:execution:dashboardThe integration harness targets the built Headquarters dashboard, real private execution workers, browser interaction/screenshots and a custom CLI installed from a dependency-free local package offline. Prepare the browser dependencies first; the command builds the dashboard. It covers worker restart and installed-tool persistence, command failures, cancellation, timeouts, workspace deletion, and closing browsers during pending actions. Passing this local test does not establish cloud deployment readiness.
Verify browser-agent integrations
# Real authenticated browser control, without model inferencepnpm test:browser-use:tools# Native image delivery and error handlingpnpm test:browser-use:bridge# Optional paid real-model test; configure the Foundry example firstpnpm test:browser-use:agentNormal tests and release preflight include the local tool and bridge checks and do not require a model-provider key. The real-model test needs a built Glove checkout and a configured OpenRouter key. It asks an agent to read an image, complete a generated form, verify confirmation and close its browser. Its explicit protocol-only mode verifies Foundry assembly without inference. Local browser and protocol checks are separate from successful model-driven execution; a rejected credential leaves that external test unverified.
Opt in to Bun signal and beacon children
import { defineConfig } from "station-daemon";import { BunProcessRuntime } from "station-signal"; export default defineConfig({ signalsDir: "./src/signals", beaconsDir: "./src/beacons", processRuntime: new BunProcessRuntime("bun"),});Node remains the default. ProcessRuntime selects signal/beacon bootstrap children and preserves JSON IPC; Bun loads TypeScript natively. The same option is available on SignalRunner and BeaconRunner. It does not switch the controller, Sandbox shell or browser adapter, and provides no isolation. Validate dependencies and representative workload behavior before rollout; benchmark actual Station jobs before claiming faster throughput or lower resource use.
Read the Sandbox reference, Browser Use reference, three-service example and Station Network guide for the complete setup.