Docs
CLI

Browser automation

Run a private Playwright browser in your devcontainer or Linux project.

Starting with CLI 0.66.0, agents can use Chromium through convoy browser run script.mjs. convoy browser smoke checks the installation. These commands run where you invoke them. On the A/B VM, run them inside convoy project shell a or from an agent connected with convoy connect --profile a. Execution from the managed environment's control account is rejected. For a host-managed container, the host can run convoy browser smoke --env dev --profile a after pausing A's connection. That route accepts --session and --no-browser-sandbox, but not --headed, --allowed-url or --blocked-url. Run custom scripts and network/display checks inside the private project shell or through its agent. browser run --env is not supported.

They do not connect to your laptop's browser. Node 20 or newer is required; use Node 22 for the project environment.

Distributed images include the browser. There is no convoy browser install command; manual setup is only for existing environments.

Install in a devcontainer

Updated single-repository and --projects templates install Chromium and its OS libraries during the image build. Rebuild the container after updating its Dockerfile. Existing containers do not receive those dependencies merely by updating the CLI. Review template changes before replacing a customized setup.

For an existing single-repository container, install the CLI candidate or release, then run inside the container:

npx --yes playwright@1.63.0 install --with-deps chromium
convoy browser smoke --session browser-test

Installing OS libraries requires administrator access. If the container account cannot use sudo for package installation, rebuild with the updated Dockerfile. The package and browser downloads need network access at setup time. The new images install them before the strict firewall starts; ordinary browser runs do not download binaries.

Chromium sandboxing defaults on. If it fails with No usable sandbox inside your dedicated devcontainer or registered project boundary, explicitly run:

convoy browser smoke --session browser-test --no-browser-sandbox

This disables Chromium's inner sandbox for that invocation. It keeps the outer container/project isolation and firewall unchanged, but gives a compromised page more access within that project. Do not use it in an ordinary unsandboxed host session. There is no automatic fallback or requirement to make Docker privileged.

Install on the existing A/B VM

Keep the registered projects and their private HOME directories. Upgrade the root-owned CLI under /opt/convoy to 0.66.1, or install the candidate .tgz there before publication. Use the same Node 22 path as the existing launcher setup.

Run as ubuntu, outside either project shell:

# For an unpublished build, copy the candidate package to the VM first.
sudo npm install --prefix /opt/convoy ./useconvoy-cli-0.66.1.tgz

sudo env PLAYWRIGHT_BROWSERS_PATH=/usr/local/share/convoy-browsers \
  /opt/convoy/node_modules/.bin/playwright install --with-deps chromium
sudo chmod -R a+rX /usr/local/share/convoy-browsers

# Refresh the helper using the paths saved by the working A/B installation.
sudo python3 -I - <<'PYTHON'
import json
import pwd
import subprocess
from pathlib import Path

config = json.loads(Path("/etc/convoy-projects.json").read_text())
installer = Path(config["package_root"]) / "dist/templates/project-isolation/install.py"
subprocess.run([
    "/usr/bin/python3", "-I", str(installer),
    "--control-user", pwd.getpwuid(config["control_uid"]).pw_name,
    "--workspace", config["workspace"],
    "--package-root", config["package_root"],
    "--node", config["node"],
    "--state", config["state"],
], check=True)
PYTHON

Stop active project connections/shells before replacing the CLI/helper. Preserve the existing workspace, registry and state directory. If your original setup used different paths, use those paths again. Do not re-register A/B or copy authentication from one project to another.

Then test A:

convoy project shell a
convoy browser smoke --session browser-test
# If nested Chromium sandboxing is unavailable:
convoy browser smoke --session browser-test --no-browser-sandbox
exit

Repeat in convoy project shell b. Both use the same path inside their private HOME, but the launcher maps that HOME to different physical directories and UIDs. The smoke result prints a screenshot and downloaded file path. Inspect the image and check that the probe file contains convoy browser download.

Ask an agent to use the browser

Exit the project shell, run convoy connect --profile a, and send the agent:

Run convoy browser smoke --session agent-demo. If Chromium reports no usable
sandbox, this is our isolated test project: retry with --no-browser-sandbox.
Inspect the resulting screenshot and report its path and the download result.
Then use convoy browser run with a script to test the application.

The shell and direct connection cannot hold the project lease simultaneously. The runtime must already be installed, authenticated and enabled for testing.

Create browser-check.mjs in the project:

export default async function ({ page, context, paths }) {
  await page.goto('http://127.0.0.1:3000');
  await page.getByRole('button', { name: 'Sign in' }).click();
  // Continue with selectors and test-account credentials for your application.
  await page.screenshot({ path: `${paths.artifacts}/application.png`, fullPage: true });
  console.log(`${paths.artifacts}/application.png`);
}

Run it on the execution machine, where the app is listening:

convoy browser run browser-check.mjs --session app-test

The script receives Playwright's page and context objects. It does not need its own Playwright dependency. For downloads, call download.saveAs with a path under paths.downloads; Playwright deletes unsaved downloads at context close. Scripts run as project code with that project's access. They are not another sandbox boundary. The browser closes after the script succeeds or throws.

Login, persistence and concurrent runs

Without --session, every invocation gets a fresh profile. With a session name, cookies and browser storage persist across runs. State lives under:

$HOME/.local/share/convoy/browser/<session>/
  profile/
  cache/
  downloads/
  artifacts/

These directories must be owned by the current identity with mode 0700. The CLI refuses symlinks or public state directories instead of changing existing files. Use a different session name for concurrent runs. Chromium refuses a second browser using the same profile. Retained directories can be deleted after the browser exits; deleting profile also removes login state.

Headless tests can use test accounts, application-provided test login URLs and supported device authorization flows. A human login requiring CAPTCHA, a passkey or a visible consent dialog may need a visible browser. --headed works only with an already configured display available to that identity. This phase does not provide a remote desktop or mount shared X sockets into isolated projects. convoy login authenticates Convoy, not websites opened by Chromium.

Verify A/B separation and networking

Inside A, save a marker in its browser artifacts directory and record its UID with id. From B, verify that you cannot read or write A's physical private HOME under /var/lib/convoy-projects/<a-identity>, its workspace marker or its browser files. Confirm the A marker exists from A before treating a denial as evidence. Use separate sessions to verify the expected cookies are absent in B. Test both projects through real direct agent connections as well as their shells.

Browser control uses pipes, not an open debugging port. The application's localhost port is still shared across projects. Authenticate services containing private data; file isolation does not protect credentials served over TCP.

In strict container mode, allow the application's outbound hosts and all required asset/auth/redirect domains using DEVCONTAINER_EXTRA_ALLOWED_DOMAINS. Reapply the existing firewall as the control account. Do not disable it to make a login work. The VM retains its existing network policy; this feature installs no VM firewall. Optional smoke probes exercise that policy:

convoy browser smoke --session network-check \
  --allowed-url https://your-allowed-test-site.example \
  --blocked-url https://your-known-reachable-denied-site.example

Replace both URLs. Add --no-browser-sandbox only when using the explicit mode above. A denied-page load failure can also mean DNS, TLS or server failure. Verify the target from outside the firewall and inspect its deny counters before claiming network enforcement. The loopback smoke test alone proves no outbound network restrictions.

Host browser sessions

This release does not attach to your laptop's signed-in browser or copy cookies. Playwright supports CDP attachment, but the endpoint gives broad browser control. Chrome 136 and later require a non-default data directory for remote debugging. A dedicated automation profile can be signed in deliberately; arbitrary access to the normal Chrome profile is not a supported assumption.

A future bridge must grant one project access through an authenticated transport, handle conflicting clients and revoke access on disconnect. Simply forwarding port 9222 into a VM would expose it to other projects on the shared network. The recommended follow-up is a dedicated host automation profile and a project-authorized broker, after the host controller phase. Direct CDP also shares tabs/storage, so independent callers are not isolated by tab selection.

References: Playwright persistent contexts, CDP attachment, Chrome remote debugging changes.

View a screenshot from Multipass on your Mac

After the smoke check in project A, export only the screenshot:

# Inside convoy project shell a:
cp /home/convoy/.local/share/convoy/browser/browser-test/artifacts/smoke.png \
  /srv/convoy-workspaces/a/browser-smoke.png
exit

# Back at the Ubuntu control account:
sudo install -o ubuntu -g ubuntu -m 600 \
  /srv/convoy-workspaces/a/browser-smoke.png /home/ubuntu/browser-smoke-a.png

On your Mac, use the instance name from multipass list:

multipass transfer convoy-test:/home/ubuntu/browser-smoke-a.png ~/Downloads/
open ~/Downloads/browser-smoke-a.png

This uses Multipass's managed connection, so no separate SSH key is needed. Replace the project, instance and account names if your setup differs.

On this page