mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-10-19 01:50:51 -05:00
feat(craft): give the agent headless browser use via a built-in browser skill (#12564)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
74cfa63dd8
commit
5895337a05
12 files changed
+2880
-2
No files matched your search
@@ -148,7 +148,9 @@ jobs:
|
||||
context: ./backend/onyx/server/features/build/sandbox/image
|
||||
file: ./backend/onyx/server/features/build/sandbox/image/Dockerfile
|
||||
# CI tests don't exercise skill runtime deps (soffice/pdftoppm/pptxgenjs),
|
||||
# so skip LibreOffice et al. to keep the CI image lean.
|
||||
# so skip LibreOffice et al. to keep the CI image lean. Browser stays
|
||||
# on (ENABLE_BROWSER defaults true) — chromium is lighter than the
|
||||
# skill bundle and keeps the CI image consistent with prod.
|
||||
extra_build_args: "ENABLE_SKILLS=false"
|
||||
- image: backend
|
||||
context: ./backend
|
||||
|
||||
@@ -162,7 +162,9 @@ jobs:
|
||||
context: ./backend/onyx/server/features/build/sandbox/image
|
||||
file: ./backend/onyx/server/features/build/sandbox/image/Dockerfile
|
||||
# CI tests don't exercise skill runtime deps (soffice/pdftoppm/pptxgenjs),
|
||||
# so skip LibreOffice et al. to keep the CI image lean.
|
||||
# so skip LibreOffice et al. to keep the CI image lean. Browser stays
|
||||
# on (ENABLE_BROWSER defaults true) — chromium is lighter and the
|
||||
# browser integration test needs it.
|
||||
extra_build_args: "ENABLE_SKILLS=false"
|
||||
- image: backend
|
||||
context: ./backend
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
"""seed_browser_built_in_skill
|
||||
|
||||
Seeds the built-in ``browser`` skill row (drives the in-pod agent-browser via
|
||||
the session-pinned ``browser`` wrapper). Mirrors the seeding in
|
||||
7f5b159041be_skill_built_in_id_discriminator, but — per that migration's own
|
||||
guidance — guards against a tenant's custom skill already owning the ``browser``
|
||||
slug: a built-in must never silently clobber or shadow a custom row.
|
||||
|
||||
Availability is gated per-deployment by the skill's registry ``is_available``
|
||||
(keyed on ENABLE_BROWSER, in onyx.skills.built_in); this migration only creates
|
||||
the row.
|
||||
|
||||
Revision ID: 989bc57562e4
|
||||
Revises: a4c9d2e7f1b8
|
||||
Create Date: 2026-07-01 11:08:10.633955
|
||||
|
||||
"""
|
||||
|
||||
import uuid
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "989bc57562e4"
|
||||
down_revision = "a4c9d2e7f1b8"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
_BROWSER_SLUG = "browser"
|
||||
_BROWSER_DESCRIPTION = (
|
||||
"Drive a real (headless) browser for anything webfetch can't do: navigating "
|
||||
"JS-rendered pages, clicking, filling forms, multi-step flows, taking "
|
||||
"screenshots, extracting data, logging into sites, testing web apps, or "
|
||||
"visually checking your own app. Use whenever the task needs a rendered, "
|
||||
"interactive browser rather than static page content."
|
||||
)
|
||||
|
||||
_skill_table = sa.table(
|
||||
"skill",
|
||||
sa.column("id", postgresql.UUID(as_uuid=True)),
|
||||
sa.column("slug", sa.String),
|
||||
sa.column("name", sa.String),
|
||||
sa.column("description", sa.Text),
|
||||
sa.column("built_in_skill_id", sa.String),
|
||||
sa.column("bundle_file_id", sa.String),
|
||||
sa.column("bundle_sha256", sa.String),
|
||||
sa.column("author_user_id", postgresql.UUID(as_uuid=True)),
|
||||
sa.column("public_permission", sa.String),
|
||||
sa.column("enabled", sa.Boolean),
|
||||
)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
|
||||
# Fail loud if a custom skill already owns the slug (built_in_skill_id IS
|
||||
# NULL): the built-in must not clobber it via the upsert below.
|
||||
existing = bind.execute(
|
||||
sa.text("SELECT built_in_skill_id FROM skill WHERE slug = :slug"),
|
||||
{"slug": _BROWSER_SLUG},
|
||||
).first()
|
||||
if existing is not None and existing[0] is None:
|
||||
raise RuntimeError(
|
||||
"Cannot seed built-in 'browser' skill: a custom skill already owns "
|
||||
"the 'browser' slug. Rename that custom skill before upgrading."
|
||||
)
|
||||
|
||||
insert_stmt = postgresql.insert(_skill_table).values(
|
||||
[
|
||||
{
|
||||
"id": uuid.uuid4(),
|
||||
"slug": _BROWSER_SLUG,
|
||||
"name": _BROWSER_SLUG,
|
||||
"description": _BROWSER_DESCRIPTION,
|
||||
"built_in_skill_id": _BROWSER_SLUG,
|
||||
"bundle_file_id": None,
|
||||
"bundle_sha256": None,
|
||||
"author_user_id": None,
|
||||
"public_permission": "VIEWER",
|
||||
"enabled": True,
|
||||
}
|
||||
]
|
||||
)
|
||||
stmt = insert_stmt.on_conflict_do_update(
|
||||
index_elements=["slug"],
|
||||
set_={
|
||||
"name": insert_stmt.excluded.name,
|
||||
"description": insert_stmt.excluded.description,
|
||||
},
|
||||
)
|
||||
bind.execute(stmt)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.get_bind().execute(
|
||||
sa.text("DELETE FROM skill WHERE slug = :slug AND built_in_skill_id = :slug"),
|
||||
{"slug": _BROWSER_SLUG},
|
||||
)
|
||||
@@ -69,6 +69,10 @@ SANDBOX_SERVICE_ACCOUNT_NAME = os.environ.get("SANDBOX_SERVICE_ACCOUNT_NAME", "s
|
||||
|
||||
ENABLE_CRAFT = os.environ.get("ENABLE_CRAFT", "false").lower() == "true"
|
||||
|
||||
# Gates the built-in `browser` skill. Distinct from the sandbox image's
|
||||
# build-time ENABLE_BROWSER ARG — set this runtime env to match it.
|
||||
ENABLE_BROWSER = os.environ.get("ENABLE_BROWSER", "false").lower() == "true"
|
||||
|
||||
SANDBOX_PUSH_PRIVATE_KEY = os.environ.get("ONYX_SANDBOX_PUSH_PRIVATE_KEY", "")
|
||||
|
||||
|
||||
|
||||
@@ -47,7 +47,9 @@ FROM ${BASE_IMAGE_REGISTRY}/library/python:3.13-slim@sha256:f82c96458eedc847b233
|
||||
# the API server at session-setup, but their runtime tools must already be in
|
||||
# the image.
|
||||
ARG ENABLE_SKILLS=true
|
||||
ARG ENABLE_BROWSER=true
|
||||
ARG OPENCODE_VERSION=1.15.7
|
||||
ARG AGENT_BROWSER_VERSION=0.31.1
|
||||
|
||||
# firewall-init.sh needs: iptables (egress lockdown), ca-certificates (trust
|
||||
# store), gosu (legacy, not currently invoked). setpriv ships with util-linux in
|
||||
@@ -90,6 +92,27 @@ RUN if [ "$ENABLE_SKILLS" = "true" ]; then \
|
||||
&& rm -rf /var/lib/apt/lists/*; \
|
||||
fi
|
||||
|
||||
# Chromium engine + certutil (libnss3-tools imports the egress-proxy CA into
|
||||
# Chromium's NSS db). Its own layer: big and rarely-changing, so an
|
||||
# AGENT_BROWSER_VERSION bump below re-pulls only the small CLI layer, not this.
|
||||
RUN if [ "$ENABLE_BROWSER" = "true" ]; then \
|
||||
apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||
chromium \
|
||||
libnss3-tools \
|
||||
&& rm -rf /var/lib/apt/lists/*; \
|
||||
fi
|
||||
|
||||
# agent-browser CLI/daemon. Do NOT run `agent-browser install` — it fetches
|
||||
# Chrome-for-Testing (no arm64 build); we use system chromium via
|
||||
# AGENT_BROWSER_EXECUTABLE_PATH. --allow-scripts lets the npm postinstall run on
|
||||
# npm 11+.
|
||||
RUN if [ "$ENABLE_BROWSER" = "true" ]; then \
|
||||
npm install -g --allow-scripts=agent-browser "agent-browser@${AGENT_BROWSER_VERSION}" && \
|
||||
agent-browser --version && \
|
||||
npm cache clean --force && \
|
||||
rm -rf /root/.npm; \
|
||||
fi
|
||||
|
||||
# Reconcile UID/GID 1000 with whatever the base image already ships with.
|
||||
RUN EXISTING_USER=$(id -nu 1000 2>/dev/null || echo ""); \
|
||||
EXISTING_GROUP=$(getent group 1000 | cut -d: -f1 2>/dev/null || echo ""); \
|
||||
@@ -164,6 +187,9 @@ RUN chmod +x /workspace/firewall-init.sh
|
||||
# kubernetes_sandbox_manager._OPENCODE_SESSION_TAG_PLUGIN_PATH.
|
||||
COPY opencode-plugins/ /workspace/opencode-plugins/
|
||||
|
||||
COPY browser-cli.sh /usr/local/bin/browser
|
||||
RUN chmod 0755 /usr/local/bin/browser
|
||||
|
||||
RUN chown -R sandbox:sandbox /workspace
|
||||
|
||||
USER sandbox
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
# `browser` — wraps `agent-browser` so the Craft agent can drive a browser in the
|
||||
# locked-down pod: pins this session and supplies the env the bare binary needs.
|
||||
# A real PATH executable, not an alias — opencode's bash tool is non-interactive
|
||||
# (no ~/.bashrc / alias expansion).
|
||||
set -euo pipefail
|
||||
|
||||
# Chromium needs --no-sandbox (pod drops caps/seccomp) and the proxy as a FLAG
|
||||
# (it ignores *_PROXY env; userinfo stripped — proxy authorizes by source IP);
|
||||
# agent-browser otherwise launches a non-installed Chrome-for-Testing.
|
||||
CLEAN_PROXY="$(printf '%s' "${HTTPS_PROXY:-${HTTP_PROXY:-}}" | sed -E 's#^([a-z]+://)?[^@/]*@#\1#')"
|
||||
# No proxy → an empty --proxy-server= sends Chromium direct, which egress blocks; fail fast.
|
||||
if [ -z "${AGENT_BROWSER_ARGS:-}" ] && [ -z "$CLEAN_PROXY" ]; then
|
||||
echo "browser: no egress proxy configured (HTTPS_PROXY/HTTP_PROXY unset)" >&2
|
||||
exit 1
|
||||
fi
|
||||
export AGENT_BROWSER_EXECUTABLE_PATH="${AGENT_BROWSER_EXECUTABLE_PATH:-/usr/bin/chromium}"
|
||||
export AGENT_BROWSER_ARGS="${AGENT_BROWSER_ARGS:---no-sandbox,--proxy-server=${CLEAN_PROXY},--proxy-bypass-list=127.0.0.1;localhost,--disable-dev-shm-usage,--disable-gpu}"
|
||||
|
||||
# Pin this session's browser (one pod hosts many; cwd is /workspace/sessions/<id>/).
|
||||
SESSION_ID="$(pwd | sed -n 's#.*/sessions/\([0-9a-fA-F-]\{36\}\).*#\1#p')"
|
||||
if [ -n "$SESSION_ID" ]; then
|
||||
exec agent-browser --session "$SESSION_ID" "$@"
|
||||
fi
|
||||
exec agent-browser "$@"
|
||||
@@ -19,6 +19,34 @@ if [ -z "${OPENCODE_SERVER_PASSWORD:-}" ]; then
|
||||
echo "[entrypoint] WARNING: OPENCODE_SERVER_PASSWORD is empty — opencode serve will run without auth"
|
||||
fi
|
||||
|
||||
# Trust the egress-proxy MITM CA in Chromium's NSS db (Chromium reads trust only
|
||||
# from there) so browser HTTPS works. Best-effort — a failure, or no browser
|
||||
# runtime, must not block boot.
|
||||
import_proxy_ca() {
|
||||
# Non-zero on no-import so the caller logs the "not imported" branch rather
|
||||
# than falsely claiming success (a browser image with a broken CA mount).
|
||||
command -v certutil >/dev/null 2>&1 || return 1
|
||||
local bundle="${SANDBOX_PROXY_CA_BUNDLE_DST:-/etc/ssl/sandbox/ca-bundle.crt}"
|
||||
[ -f "$bundle" ] || return 1
|
||||
local nssdb="${HOME:-/home/sandbox}/.pki/nssdb"
|
||||
mkdir -p "$nssdb"
|
||||
[ -f "$nssdb/cert9.db" ] || certutil -d "sql:$nssdb" -N --empty-password
|
||||
# The bundle is many roots; `certutil -A` imports one cert at a time, so split.
|
||||
local splitdir imported=0 f
|
||||
splitdir="$(mktemp -d)"
|
||||
csplit -z -f "$splitdir/ca-" -b "%03d.pem" "$bundle" "/BEGIN CERTIFICATE/" "{*}" >/dev/null 2>&1 || true
|
||||
for f in "$splitdir"/ca-*.pem; do
|
||||
[ -f "$f" ] || continue
|
||||
certutil -d "sql:$nssdb" -A -t "C,," -n "proxy-$(basename "$f" .pem)" -i "$f" 2>/dev/null \
|
||||
&& imported=$((imported + 1))
|
||||
done
|
||||
rm -rf "$splitdir"
|
||||
[ "$imported" -gt 0 ]
|
||||
}
|
||||
import_proxy_ca \
|
||||
&& echo "[entrypoint] imported proxy CA into Chromium NSS db" \
|
||||
|| echo "[entrypoint] proxy CA not imported into NSS (browser runtime absent or CA missing)"
|
||||
|
||||
backoff=1
|
||||
max_backoff=30
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from onyx.db.enums import ExternalAppType
|
||||
from onyx.image_gen.generation import is_image_generation_configured
|
||||
from onyx.server.features.build.configs import ENABLE_BROWSER
|
||||
|
||||
# On-disk root for built-in skill content (one ``<skill_id>/`` dir each). Pushed
|
||||
# to sandboxes at session setup; not baked into the sandbox image.
|
||||
@@ -177,6 +178,11 @@ _REGISTRY: Final = BuiltInSkillRegistry(
|
||||
),
|
||||
),
|
||||
SeededBuiltInProvider(skill_id="company-search"),
|
||||
SeededBuiltInProvider(
|
||||
skill_id="browser",
|
||||
is_available=lambda _: ENABLE_BROWSER,
|
||||
unavailable_reason="Browser runtime is not enabled for this deployment.",
|
||||
),
|
||||
ExternalAppBuiltInProvider(skill_id="slack", app_type=ExternalAppType.SLACK),
|
||||
ExternalAppBuiltInProvider(skill_id="linear", app_type=ExternalAppType.LINEAR),
|
||||
ExternalAppBuiltInProvider(
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -23,6 +23,7 @@ from sqlalchemy.orm import Session
|
||||
from onyx.db.models import Skill
|
||||
from onyx.db.models import User
|
||||
from onyx.db.skill import fetch_skill_for_user
|
||||
from onyx.db.skill import list_skills_for_sandbox_injection
|
||||
from onyx.db.skill import list_skills_for_user
|
||||
from onyx.error_handling.exceptions import OnyxError
|
||||
from onyx.server.features.skill.mutation_helpers import ensure_custom_skill
|
||||
@@ -108,6 +109,28 @@ class TestAvailabilityGate:
|
||||
}
|
||||
assert available_built_ins <= visible_built_ins
|
||||
|
||||
def test_browser_built_in_gated_on_enable_browser(
|
||||
self,
|
||||
db_session: Session,
|
||||
test_user: User,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
_seed_canonical(db_session)
|
||||
|
||||
monkeypatch.setattr(built_in_module, "ENABLE_BROWSER", False)
|
||||
off = {
|
||||
s.built_in_skill_id
|
||||
for s in list_skills_for_sandbox_injection(test_user, db_session)
|
||||
}
|
||||
assert "browser" not in off
|
||||
|
||||
monkeypatch.setattr(built_in_module, "ENABLE_BROWSER", True)
|
||||
on = {
|
||||
s.built_in_skill_id
|
||||
for s in list_skills_for_sandbox_injection(test_user, db_session)
|
||||
}
|
||||
assert "browser" in on
|
||||
|
||||
def test_unavailable_built_in_cannot_be_fetched_by_id(
|
||||
self,
|
||||
db_session: Session,
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
"""Browser-use test in the Craft k8s integration lane.
|
||||
|
||||
The agent's `browser` command has to work inside the locked-down sandbox pod,
|
||||
which is the intersection of four things that have each broken before: Chromium
|
||||
launching with --no-sandbox under the pod's dropped caps/seccomp, reaching the
|
||||
public internet through the MITM egress proxy, and trusting the proxy CA for
|
||||
HTTPS. A clean snapshot of a real HTTPS page exercises all of them at once
|
||||
against a real pod + the chart's sandbox proxy.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from kubernetes import client
|
||||
|
||||
from onyx.server.features.build.configs import SANDBOX_BACKEND
|
||||
from onyx.server.features.build.configs import SANDBOX_NAMESPACE
|
||||
from onyx.server.features.build.configs import SandboxBackend
|
||||
from tests.integration.tests.craft.k8s.k8s_fixtures import pod_exec
|
||||
from tests.integration.tests.craft.k8s.k8s_fixtures import PoolSession
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
SANDBOX_BACKEND != SandboxBackend.KUBERNETES,
|
||||
reason="K8s tests require SANDBOX_BACKEND=kubernetes; run in the dedicated K8s CI job.",
|
||||
)
|
||||
|
||||
|
||||
class TestBrowserUse:
|
||||
def test_browser_fetches_https_through_egress_proxy(
|
||||
self,
|
||||
k8s_client: client.CoreV1Api,
|
||||
pool_session: PoolSession,
|
||||
) -> None:
|
||||
_sandbox_id, session_id, pod_name = pool_session
|
||||
|
||||
# Run `browser` from the session workspace (its cwd is how the wrapper
|
||||
# resolves --session), exactly as the agent would.
|
||||
out = pod_exec(
|
||||
k8s_client,
|
||||
pod_name,
|
||||
SANDBOX_NAMESPACE,
|
||||
f"cd /workspace/sessions/{session_id} "
|
||||
"&& browser open https://example.com && browser snapshot -i",
|
||||
)
|
||||
|
||||
assert "Example Domain" in out, out
|
||||
# Guard the specific MITM-cert failure mode that has regressed before.
|
||||
assert "ERR_CERT_AUTHORITY_INVALID" not in out, out
|
||||
@@ -0,0 +1,108 @@
|
||||
# Browser use
|
||||
|
||||
Give the Craft **agent** a real browser it can drive for tasks `webfetch` can't
|
||||
do — JS-rendered pages, clicking, forms, multi-step flows, screenshots, scraping,
|
||||
logging into sites, and visually checking the app it's building.
|
||||
|
||||
This is **agent-only**: the browser is headless and the user does not see or
|
||||
drive it. The webapp preview is unchanged (it remains the normal webapp preview).
|
||||
There is no streamed browser, no human takeover, and no open-internet streaming.
|
||||
|
||||
## How it works
|
||||
|
||||
The agent drives a real Chromium (via [`vercel-labs/agent-browser`](https://github.com/vercel-labs/agent-browser),
|
||||
a Rust daemon + CLI over CDP) from its `bash` tool. It works the accessibility
|
||||
tree: `browser open <url>`, `browser snapshot -i` (compact `@eN` refs), then
|
||||
`browser click/type` those refs — ~200–400 tokens per step instead of raw HTML.
|
||||
|
||||
### The `browser` command (`image/browser-cli.sh` → `/usr/local/bin/browser`)
|
||||
|
||||
A thin wrapper over `agent-browser`, baked into the sandbox image, that makes the
|
||||
agent's life simple — it just uses the plain commands. The wrapper, per call:
|
||||
|
||||
- **Pins the session** — injects `--session <uuid>` resolved from the cwd
|
||||
(`/workspace/sessions/<uuid>/`), so each session drives its own browser. One
|
||||
pod hosts many sessions, so this can't be a global env; the cwd is the only
|
||||
reliable per-call signal. Skipped if the caller already passed
|
||||
`--session`/`--auto-connect`/`--cdp`.
|
||||
- **Carries the Chromium env** — the invoking command launches Chromium when a
|
||||
session has none, so it must carry `--no-sandbox` (the pod drops caps +
|
||||
seccomp), the egress proxy as a `--proxy-server` flag (Chromium ignores
|
||||
`*_PROXY` env; userinfo is stripped since the proxy authorizes by source IP),
|
||||
and headless-stability flags. Defaults only — anything already exported wins.
|
||||
|
||||
Proxy-CA trust is **not** done here — it's a once-at-startup step in
|
||||
`entrypoint.sh` (the main container, as uid 1000), which imports the egress-proxy
|
||||
CA bundle into Chromium's per-user NSS db (`~/.pki/nssdb`) so HTTPS through the
|
||||
MITM proxy is trusted. It runs in the main container (not the K8s
|
||||
`firewall-init` initContainer, whose `/home/sandbox` isn't shared), splits the
|
||||
bundle and imports every cert (`certutil -A -i` imports only the first), and is
|
||||
a no-op without the browser runtime.
|
||||
|
||||
Why a real PATH executable and not a bashrc alias: opencode runs the agent's
|
||||
`bash` tool in non-interactive shells, which don't source `~/.bashrc` or expand
|
||||
aliases.
|
||||
|
||||
### The `browser` built-in skill
|
||||
|
||||
`backend/onyx/skills/builtin/browser/SKILL.md` is `agent-browser`'s own core
|
||||
usage guide (pulled from `agent-browser skills get core --full`, version-matched
|
||||
to the pinned CLI) with every `agent-browser` rewritten to `browser`, the install
|
||||
lines dropped (pre-installed), and a short Onyx note (headless; pinned session;
|
||||
no `--session`). It's a normal seeded built-in skill:
|
||||
|
||||
- Registered in `onyx/skills/built_in.py` with `is_available` keyed on
|
||||
`ENABLE_BROWSER`.
|
||||
- Row seeded by migration `c4e7b1a9f2d3_seed_browser_built_in_skill`.
|
||||
- **Gated per-deployment**, not per-user: the registry's `is_available` returns
|
||||
`ENABLE_BROWSER`, so `_exclude_unavailable_built_ins` hides the skill from
|
||||
sandbox injection wherever the image was built without the browser runtime. No
|
||||
PostHog flag — like `pptx`/`company-search`, it's a plain built-in skill; the
|
||||
only question is whether the image has the `browser` command, which is a
|
||||
deployment property, not a per-user one.
|
||||
|
||||
The agent discovers and uses it like any other built-in skill — it appears in the
|
||||
AGENTS.md skills list with its description, and the agent reads its `SKILL.md`.
|
||||
No dedicated AGENTS.md section is needed.
|
||||
|
||||
## Gating
|
||||
|
||||
`ENABLE_BROWSER` (`onyx/server/features/build/configs.py`, api-server runtime
|
||||
env, default OFF): set it to match the sandbox image's build-time
|
||||
`ENABLE_BROWSER` ARG. It's the single signal of "does this deployment's image
|
||||
include the browser runtime," and gates the built-in `browser` skill via the
|
||||
registry. There is no per-user feature flag.
|
||||
|
||||
## Image
|
||||
|
||||
`image/Dockerfile`, gated on `ENABLE_BROWSER=true` (default; set false in dev/CI
|
||||
to skip ~400 MB): installs `chromium` (system, arm64-native — we point the daemon
|
||||
at it via `AGENT_BROWSER_EXECUTABLE_PATH` rather than `agent-browser install`,
|
||||
which fetches a no-arm64 Chrome-for-Testing) + `libnss3-tools` (certutil) +
|
||||
`agent-browser` (npm global). The `browser` wrapper is always copied; it's inert
|
||||
when `ENABLE_BROWSER=false` (agent-browser absent) and only advertised via the
|
||||
per-user-gated skill.
|
||||
|
||||
## Browser state is ephemeral
|
||||
|
||||
agent-browser keeps per-session state (cookies, localStorage, the Chromium
|
||||
profile) under a fixed `~/.agent-browser/` (= `/home/sandbox/.agent-browser/`,
|
||||
outside the snapshotted `/workspace/sessions/<id>` volume). The wrapper passes
|
||||
neither `--profile` nor `--restore`, so browser state is intentionally ephemeral
|
||||
— it lives only for the daemon's lifetime and is **not** captured by session
|
||||
snapshots; logins/cookies do not survive a pod reap or restore.
|
||||
|
||||
Making it persist would mean relocating agent-browser's data dir under
|
||||
`/workspace` (no data-dir env override exists beyond `AGENT_BROWSER_SOCKET_DIR`;
|
||||
the lever is `HOME`) and using `--restore` (cookies+localStorage only, not a full
|
||||
`--profile`) — which writes auth cookies into the S3 snapshot. That's a
|
||||
deliberate security/size tradeoff to revisit only if a "resume a logged-in
|
||||
browser across reaps" use case appears.
|
||||
|
||||
## Not in scope
|
||||
|
||||
- No screencast / streamed browser to the frontend.
|
||||
- No human-driven browser or pixel-input takeover.
|
||||
- No socat forwarder, stream ports (8930/8931), WS proxy, or `/browser-input`
|
||||
endpoint — the agent uses snapshot/click (CDP), which don't need the screencast.
|
||||
- The webapp preview is untouched.
|
||||
Reference in new issue
Block a user