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-testInstalling 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-sandboxThis 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)
PYTHONStop 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
exitRepeat 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-testThe 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.exampleReplace 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.pngOn 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.pngThis uses Multipass's managed connection, so no separate SSH key is needed. Replace the project, instance and account names if your setup differs.