Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,8 @@ harnesses and clients can be added as independent integrations.
`credentialsource:read`, `credentialsource:write` and
`credentialsource:delete`
- A current Sandbox0 `coding-agent` template with the official Playwright CLI,
Chromium, Xvfb, Openbox and x11vnc. At least 2 GiB of Sandbox memory is
Chromium, TigerVNC and Openbox. Sandpi retains an Xvfb/x11vnc compatibility
path for older template images. At least 2 GiB of Sandbox memory is
recommended for interactive Browser work alongside a coding agent.
- Docker Engine with Compose v2 for the container workflow

Expand Down Expand Up @@ -317,8 +318,10 @@ Sandbox0
account. They are not isolated checkouts. Use separate Environments when work
must not affect each other.
- Interactive Browser takeover requires the current Sandbox0 `coding-agent`
image. Recreate an older Environment to pick up its headed-browser runtime;
agent-only Playwright remains available on older compatible images.
image. Older compatible images can still use scaled Xvfb/x11vnc takeover;
recreate the Environment with v0.4 or newer for a human desktop that follows
the full Browser panel size. Agent-only Playwright remains available on older
compatible images.
- Built-in administrator mode is for a trusted single-user deployment. Use OIDC
and a proper network/TLS boundary for public or multi-user deployments.
- The `/api/v1` contract is versioned but may still change between pre-1.0
Expand Down
34 changes: 21 additions & 13 deletions docs/architecture/native-session-authority.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,15 +295,21 @@ so the agent always lists tabs and takes a new snapshot after control returns.
Human control replaces the same AppService rather than starting a second
service. Its lazy process first closes the Playwright daemon and validates the
profile lock, then runs one headed browser as the unprivileged
`sandbox-browser` user on Xvfb and Openbox. It prefers an operator-supplied
`google-chrome-stable` binary when one exists and otherwise uses the bundled
Chrome for Testing executable. The launch has no headless, automation or
remote-debugging flag and exposes no CDP port. x11vnc binds only to loopback; a
small WebSocket-to-TCP bridge publishes `/vnc` through the existing protected
AppService. The Web client dynamically loads noVNC only in human mode. This
mode is suitable for sites that require real human interaction, but it cannot
promise that a third-party identity provider will accept every browser build
or future policy.
`sandbox-browser` user on a TigerVNC X server and Openbox. It prefers an
operator-supplied `google-chrome-stable` binary when one exists and otherwise
uses the bundled Chrome for Testing executable. Chrome starts maximized and
keeps that window-manager state as the X desktop changes size. The bundled
browser runs under the image's normal Linux browser-sandbox setup; Sandpi does
not force `--no-sandbox`. The launch has no headless, automation or
remote-debugging flag and exposes no CDP port.
TigerVNC binds only to loopback and accepts the noVNC client's requested panel
size; a small WebSocket-to-TCP bridge publishes `/vnc` through the existing
protected AppService. The client keeps viewport scaling enabled as a fallback
for older images, where Sandpi uses the legacy fixed-size Xvfb/x11vnc runtime.
The Web client dynamically loads noVNC only in human mode. This mode is
suitable for sites that require real human interaction, but it cannot promise
that a third-party identity provider will accept every browser build or future
policy.

Returning control replaces the AppService with the agent command. The human
process receives a graceful stop so Chrome can flush the profile before the
Expand All @@ -319,10 +325,12 @@ would require moving the browser into a separately privileged sidecar or
Sandbox.

Only one browser renderer runs during a handoff. The coding-agent image omits
Playwright's otherwise redundant headless-shell payload, adds only Xvfb,
Openbox, x11vnc and the already-used `ws` transport dependency, and loads the
noVNC client on demand. Two GiB or more of Sandbox memory is recommended when a
headed browser and coding agent are used together.
Playwright's otherwise redundant headless-shell payload. The v0.4 image adds
TigerVNC while temporarily retaining Xvfb/x11vnc for rollout compatibility;
the current human path runs one combined X/VNC server instead of separate Xvfb
and x11vnc processes. Sandpi reuses the existing `ws` transport dependency and
loads the noVNC client on demand. Two GiB or more of Sandbox memory is
recommended when a headed browser and coding agent are used together.

Sandpi adapts only the embedded Dashboard shell: it binds the Dashboard to the
shared `default` session explicitly, projects the current Sandpi theme tokens
Expand Down
8 changes: 2 additions & 6 deletions src/components/environment-browser-vnc.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { useEffect, useRef } from "react";

import { apiUrl } from "@/lib/api-client";
import { configureEnvironmentBrowserRfb } from "@/lib/environment-browser-vnc";

interface EnvironmentBrowserVncProps {
environmentId: string;
Expand Down Expand Up @@ -37,12 +38,7 @@ export function EnvironmentBrowserVnc({
);
endpoint.protocol = endpoint.protocol === "https:" ? "wss:" : "ws:";
rfb = new Rfb(element, endpoint.toString(), { shared: true });
rfb.scaleViewport = true;
rfb.resizeSession = false;
rfb.focusOnClick = true;
rfb.showDotCursor = true;
rfb.qualityLevel = 6;
rfb.compressionLevel = 2;
configureEnvironmentBrowserRfb(rfb);
rfb.addEventListener("connect", () => {
connected = true;
onReady();
Expand Down
26 changes: 26 additions & 0 deletions src/lib/environment-browser-vnc.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import assert from "node:assert/strict";
import test from "node:test";

import { configureEnvironmentBrowserRfb } from "./environment-browser-vnc";

test("requests the human-control panel size with scaling as a legacy fallback", () => {
const rfb = {
scaleViewport: false,
resizeSession: false,
focusOnClick: false,
showDotCursor: false,
qualityLevel: 0,
compressionLevel: 0,
};

configureEnvironmentBrowserRfb(rfb);

assert.deepEqual(rfb, {
scaleViewport: true,
resizeSession: true,
focusOnClick: true,
showDotCursor: true,
qualityLevel: 6,
compressionLevel: 2,
});
});
21 changes: 21 additions & 0 deletions src/lib/environment-browser-vnc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export interface EnvironmentBrowserRfbClient {
scaleViewport: boolean;
resizeSession: boolean;
focusOnClick: boolean;
showDotCursor: boolean;
qualityLevel: number;
compressionLevel: number;
}

export function configureEnvironmentBrowserRfb(
rfb: EnvironmentBrowserRfbClient,
) {
// Remote resize fills the human-control panel when TigerVNC is available.
// Scaling stays enabled so older Xvfb/x11vnc Environments remain usable.
rfb.scaleViewport = true;
rfb.resizeSession = true;
rfb.focusOnClick = true;
rfb.showDotCursor = true;
rfb.qualityLevel = 6;
rfb.compressionLevel = 2;
}
38 changes: 29 additions & 9 deletions src/server/runtime/environment-browser-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,28 +208,46 @@ fi
test -n "$browser"

mkdir -p /tmp/sandpi-browser-openbox
HOME=/tmp/sandpi-browser-openbox Xvfb "$display" -screen 0 1440x900x24 -nolisten tcp -ac +extension RANDR >/tmp/sandpi-browser-xvfb.log 2>&1 &
xvfb_pid="$!"
pids="$xvfb_pid $pids"
using_tigervnc=false
if command -v Xtigervnc >/dev/null; then
using_tigervnc=true
HOME=/tmp/sandpi-browser-openbox Xtigervnc "$display" \
-geometry 1440x900 \
-depth 24 \
-rfbport "$vnc_port" \
-localhost \
-SecurityTypes None \
-AlwaysShared \
-AcceptSetDesktopSize \
-ac \
-nolisten tcp >/tmp/sandpi-browser-tigervnc.log 2>&1 &
display_pid="$!"
else
HOME=/tmp/sandpi-browser-openbox Xvfb "$display" -screen 0 1440x900x24 -nolisten tcp -ac +extension RANDR >/tmp/sandpi-browser-xvfb.log 2>&1 &
display_pid="$!"
fi
pids="$display_pid $pids"
attempts=0
while test ! -S /tmp/.X11-unix/X99; do
kill -0 "$xvfb_pid"
kill -0 "$display_pid"
test "$attempts" -lt 100
sleep 0.05
attempts=$((attempts + 1))
done
DISPLAY="$display" HOME=/tmp/sandpi-browser-openbox openbox >/tmp/sandpi-browser-openbox.log 2>&1 &
pids="$! $pids"
DISPLAY="$display" x11vnc -display "$display" -rfbport "$vnc_port" -localhost -forever -shared -nopw -noxdamage -repeat -quiet >/tmp/sandpi-browser-x11vnc.log 2>&1 &
pids="$! $pids"
if test "$using_tigervnc" = false; then
DISPLAY="$display" x11vnc -display "$display" -rfbport "$vnc_port" -localhost -forever -shared -nopw -noxdamage -repeat -quiet >/tmp/sandpi-browser-x11vnc.log 2>&1 &
pids="$! $pids"
fi

setpriv --reuid="$browser_user" --regid="$browser_user" --init-groups env DISPLAY="$display" HOME="/home/$browser_user" "$browser" \
--user-data-dir="$profile" \
--no-first-run \
--no-default-browser-check \
--password-store=basic \
--disable-dev-shm-usage \
--window-size=1440,900 \
--start-maximized \
--restore-last-session >/tmp/sandpi-browser-chrome.log 2>&1 &
pids="$! $pids"

Expand All @@ -241,9 +259,11 @@ wait "$bridge_pid"`;
}

export const HUMAN_BROWSER_PREFLIGHT_SCRIPT = String.raw`set -eu
command -v Xvfb >/dev/null
command -v openbox >/dev/null
command -v x11vnc >/dev/null
if ! command -v Xtigervnc >/dev/null; then
command -v Xvfb >/dev/null
command -v x11vnc >/dev/null
fi
command -v node >/dev/null
command -v setpriv >/dev/null
browser_user="${"${SANDPI_BROWSER_USER:-sandbox-browser}"}"
Expand Down
13 changes: 9 additions & 4 deletions src/server/runtime/sandbox0.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1395,7 +1395,7 @@ test("uses the AppService spec as the Browser owner handoff fence", async () =>
assert.equal(capabilityChecks, 1);
assert.match(
String(capabilityCommand?.command?.at(-1)),
/command -v Xvfb[\s\S]+command -v x11vnc/,
/command -v Xtigervnc[\s\S]+command -v Xvfb[\s\S]+command -v x11vnc/,
);
assert.doesNotMatch(
String(capabilityCommand?.command?.at(-1)),
Expand Down Expand Up @@ -1429,7 +1429,7 @@ test("uses the AppService spec as the Browser owner handoff fence", async () =>
const takeoverPreparation = String(preflightCommand?.command?.at(-1));
assert.match(
takeoverPreparation,
/playwright_guard=\/workspace\/\.sandpi\/bin\/playwright-cli[\s\S]+command -v Xvfb/,
/playwright_guard=\/workspace\/\.sandpi\/bin\/playwright-cli[\s\S]+command -v Xtigervnc/,
);
assert.match(
Buffer.from(
Expand All @@ -1448,11 +1448,16 @@ test("uses the AppService spec as the Browser owner handoff fence", async () =>
humanService.runtime.envVars.SANDPI_BROWSER_SESSION_REVISION,
"1",
);
assert.match(humanCommand, /Xvfb.*openbox.*x11vnc.*setpriv/s);
assert.match(
humanCommand,
/command -v Xtigervnc[\s\S]+Xtigervnc[\s\S]+AcceptSetDesktopSize[\s\S]+Xvfb[\s\S]+openbox[\s\S]+x11vnc[\s\S]+setpriv/,
);
assert.match(humanCommand, /google-chrome-stable.*chrome-linux/s);
assert.match(humanCommand, /--start-maximized/);
assert.doesNotMatch(humanCommand, /--window-size/);
assert.doesNotMatch(
humanCommand,
/--headless|--remote-debugging|--enable-automation/,
/--headless|--remote-debugging|--enable-automation|--no-sandbox/,
);
assert.equal(spawnSync("sh", ["-n", "-c", humanCommand]).status, 0);
assert.equal(
Expand Down