| name | fix-arm-browser |
| description | Fix headless Chromium crashes on ARM64 PRoot (Android/Termux) by switching to Firefox. |
| category | devops |
Fix ARM64 PRoot Browser
Chromium's headless shell crashes on ARM64 PRoot containers due to seccomp blocking critical system calls. Firefox works reliably as a drop-in replacement.
Symptoms
browser_navigate returns "Daemon failed to start" or "Target crashed"
browser_snapshot returns "Empty page"
- Chromium processes die immediately on navigation
- Running on: Termux PRoot, aarch64, seccomp enabled
Diagnosis
uname -m
/root/.cache/ms-playwright/chromium_headless_shell-*/chrome-linux/headless_shell --version
node -e "
const { firefox } = require('playwright-core');
(async () => {
const b = await firefox.launch({headless:true});
const p = await b.newPage();
await p.goto('https://httpbin.org/html');
console.log('OK:', p.url());
await b.close();
})();"
Fix Steps
1. Install Firefox for Playwright
cd /root/.hermes/hermes-agent
npx playwright install firefox
Firefox installs to /root/.cache/ms-playwright/firefox-XXXX/
2. Patch browser_tool.py - Inject env var into daemon subprocess
File: tools/browser_tool.py
Find (~line 889):
browser_env["AGENT_BROWSER_SOCKET_DIR"] = task_socket_dir
Add after it:
browser_env["AGENT_BROWSER_BROWSER"] = os.environ.get("AGENT_BROWSER_BROWSER", "firefox")
This ensures the browser tool passes the Firefox env var to the spawned agent-browser CLI daemon. Without this, the daemon still defaults to chromium when auto-launched by the gateway.
3. Patch daemon.js - Read and pass the browser env var to launcher
File: node_modules/agent-browser/dist/daemon.js
Two changes needed:
a) Read the env var (~line 334, where extensions are parsed):
const extensions = process.env.AGENT_BROWSER_EXTENSIONS
? process.env.AGENT_BROWSER_EXTENSIONS.split(',')
.map((p) => p.trim())
.filter(Boolean)
: undefined;
const browser = process.env.AGENT_BROWSER_BROWSER;
b) Pass it to the launcher (~line 359, in the manager.launch({...}) call):
await manager.launch({
id: 'auto',
action: 'launch',
headless: process.env.AGENT_BROWSER_HEADED !== '1',
executablePath: process.env.AGENT_BROWSER_EXECUTABLE_PATH,
extensions: extensions,
browser,
profile: process.env.AGENT_BROWSER_PROFILE,
...
});
4. Patch browser.js - Change default fallback to Firefox
File: node_modules/agent-browser/dist/browser.js
Find line (~970):
const browserType = options.browser ?? 'chromium';
Change to:
const browserType = options.browser ?? process.env.AGENT_BROWSER_BROWSER ?? 'firefox';
This way:
options.browser (if passed explicitly) takes priority
- Falls back to
AGENT_BROWSER_BROWSER env var
- Finally defaults to
firefox instead of chromium
4. Restart the agent browser daemon
Kill existing daemons — the gateway will auto-spawn with Firefox on next browser command:
for pid in $(ps aux | grep "daemon.js" | grep -v grep | awk '{print $2}'); do kill -9 $pid 2>/dev/null; done
5. Verify
ls /root/.cache/ms-playwright/firefox-*/firefox/firefox-bin
node -e "
const { firefox } = require('/root/.hermes/hermes-agent/node_modules/playwright-core');
(async () => {
const b = await firefox.launch({headless:true, executablePath:'/root/.cache/ms-playwright/firefox-1511/firefox/firefox-bin'});
const p = await b.newPage();
await p.goto('https://httpbin.org/html');
console.log('SUCCESS:', (await p.content()).includes('Herman') ? 'Firefox works' : 'FAIL');
await b.close();
})();\"
ls /root/.cache/ms-playwright/firefox-*/firefox/firefox-bin
node -e "
const { firefox } = require('/root/.hermes/hermes-agent/node_modules/playwright-core');
(async () => {
const b = await firefox.launch({headless:true, executablePath:'/root/.cache/ms-playwright/firefox-1511/firefox/firefox-bin'});
const p = await b.newPage();
await p.goto('https://httpbin.org/html');
console.log('SUCCESS:', (await p.content()).includes('Herman') ? 'Firefox works' : 'FAIL');
await b.close();
})();"
Important Notes from Live Testing
Limitations of browser tool on ARM64 PRoot even after this fix
- Site blocking: Many sites (eBay, Facebook, etc.) still serve CAPTCHAs or block headless browsers because the browser runs without residential proxies. Login sessions are also hard to maintain between conversations.
- No access to user's actual device screen: The built-in
browser_* tools run in an isolated headless browser session. They cannot see or control the user's own Android browser tabs, Chrome windows, or app screens. They can only browse in their own hidden window.
- For true on-device form automation: An ADB (Android Debug Bridge) bridge is required. Enable Wireless Debugging on the Android tablet, connect via
adb connect IP:PORT, then use adb shell input commands to tap and type into the user's actual screen tabs. The agent can build a shell script bridge to automate listing forms in the user's real browser.
Architecture pattern: Hub-and-spoke agent teams
For scaling multi-platform automation:
- One orchestrator agent (main agent on tablet) coordinates tasks
- Specialized worker agents on other devices (phones, laptops) handle specific platforms
- Communication via Discord channels
- Each worker agent handles one or two platforms natively where browser support is stable
Why This Works
If anything breaks, reverse the three patches:
- Remove
browser_env["AGENT_BROWSER_BROWSER"] = ... from browser_tool.py (~line 890)
- Remove
const browser = process.env.AGENT_BROWSER_BROWSER; and browser, from daemon.js
- Change
?? 'firefox' back to ?? 'chromium' in browser.js (~line 970)
Chromium will be used again (but will still crash on PRoot ARM64).