- Run reporter.test.js with node --test (uses node:test, not Jest)
- Exclude reporter.test from Jest run
- Bump client timeout to 60s in CI (Camoufox cold start on GitHub runners)
- Disable bail so full suite runs even on failure
- Drop --ignore-scripts so camoufox-js fetch runs
- Add playwright install --with-deps firefox for system libs
- Use xvfb-run for headless display on Ubuntu
- Only exclude live/ tests (external sites), run e2e/security/cookies/tabRecycling
- Node 20+24 matrix (drop 18, add 24 to match production)
- Remove unused jest-junit dep
Adds opt-out crash reporter that files GitHub issues when sites cause
persistent problems. Only fires on frustration patterns (3+ consecutive
failures on the same tab), not individual errors.
Reporter (lib/reporter.js):
- Text anonymizer: strips paths, IPs, tokens, secrets, env vars, Fly IDs
- URL anonymizer: per-report salted HMAC for private domains, public infra
domains (Cloudflare, Google, GitHub etc) preserved verbatim, paths stripped
to depth-only (bullet points), query param count only, no content
- Per-tab health tracker via Playwright events: page crashes, JS errors,
console error rate, request failures, dialog storms, redirect depth,
HTTP 4xx/5xx histogram, frame count — all count-based, no content
- Rate limited (10/hr), deduped by stack signature, fire-and-forget
- GitHub PAT scoped to issues:write on single repo (swappable for forks)
Integration (server.js):
- Frustration detection in handleRouteError: tracks consecutiveFailures
per tab, only reports at threshold=3 with full failure journal
- Health tracker attached to every new tab via createTabState()
- Event loop watchdog (5s stall threshold)
- Reports include: anonymized URL, health snapshot, failure journal
Config: opt-out (CAMOFOX_CRASH_REPORT_ENABLED!=false), requires PAT.
Tests: 69 passing (anonymization, URL privacy, dedup, rate limiting).
Adds CI status checks for PRs targeting master:
- Runs on Node 18 and 20 (matrix)
- Installs deps with npm ci (skipping postinstall browser fetch)
- Installs jest-junit for CI test reporting
- Runs pure unit tests (excludes server-dependent tests that need
a running camoufox browser instance: cookies, security, tabRecycling)
- Removes .github/ from .gitignore to allow workflow tracking
Fixes#73
- install-plugin-deps.sh: handle both array and object plugin config formats
Object format: only install deps for plugins where enabled !== false
- Dockerfile with-plugins stage: add missing COPY for plugins/ and config
- Sync package-lock.json version to 1.5.2
VNC plugin (plugins/vnc/) exposes Camoufox's virtual display via noVNC,
enabling interactive login for sites with fingerprint-based session
validation, CAPTCHAs, or MFA prompts.
- Plugin subclasses VirtualDisplay to override Xvfb resolution (default
1920x1080, configurable via plugins.vnc.resolution or VNC_RESOLUTION)
- vnc-watcher.sh detects Camoufox's dynamically-assigned Xvfb display,
attaches x11vnc, and proxies via noVNC on port 6080
- Registers GET /sessions/:userId/storage_state to export Playwright
storageState (cookies + localStorage) after interactive login
- Emits session:storage:export event for persistence plugin integration
- System deps declared in apt.txt, installed via install-plugin-deps.sh
Plugin system changes:
- ctx passed by reference (not spread) so plugins can mutate factories
like ctx.createVirtualDisplay
- pluginConfig moved to 3rd arg of register(app, ctx, pluginConfig)
- server.js exposes createVirtualDisplay factory + VirtualDisplay class
on pluginCtx
Dockerfile: fix build for local Makefile (bind mounts from dist/),
name first stage for multi-stage support.
Co-authored-by: Leone Parise <leone.parise@gmail.com>
Persistence as a plugin (not core) — camofox stays stateless by default.
Plugin hooks into session:creating, session:created, session:cookies:import,
session:destroyed, and server:shutdown lifecycle events to save/restore
Playwright storageState.
Plugin system now supports per-plugin config objects in camofox.config.json:
{ "plugins": { "youtube": { "enabled": true }, "persistence": { ... } } }
Array format still supported for backward compatibility.
Plugin config is passed to register() as ctx.pluginConfig.
Co-authored-by: company8 <compan@post.com>
Consolidate 7+ copy-pasted session teardown blocks into closeSession() and
closeAllSessions() helpers. Wrap getSession() in coalesceInflight to prevent
concurrent session creation races. All teardown paths now go through one
function that handles: context close, map cleanup, tab lock drain, download
cleanup, metrics refresh, and plugin event emission.
Based on the structural refactor from PR #62.
Co-authored-by: company8 <compan@post.com>
Standalone library modules from PR #62:
- lib/persistence.js: atomic storageState save/restore with SHA256-hashed user dirs
- lib/inflight.js: coalesceInflight primitive for concurrent session creation dedup
- lib/cookies.js: importBootstrapCookies helper for first-run cookie seeding
- Unit tests for all three modules
Co-authored-by: company8 <compan@post.com>
npm run plugin install <git-url|local-path>
npm run plugin remove <name>
npm run plugin list
Supports git repos (root plugin or plugins/ subdirs) and local
directories. Auto-updates camofox.config.json, installs npm deps,
warns about system deps needing Docker rebuild.
- Add defaultPlugins: ["youtube"] to openclaw.plugin.json
- Add plugins/youtube/post-install.sh for yt-dlp binary download
- Run install-plugin-deps.sh in base Dockerfile stage (not just with-plugins)
- install-plugin-deps.sh now runs post-install.sh hooks after apt packages
- with-plugins stage is now for rebuilding after adding third-party plugins
Race 1 (YT transcript): browserTranscript cleanup counted tabGroups which
was always 0 (YT pages aren't registered in tabGroups). First concurrent
request to finish would close the context, killing other requests' pages.
Fix: use context.pages() to check actual live pages before closing.
Race 2 (tab reaper / session expiry): context.close() fired without await,
then session deleted from map. A request that already got the session ref
(between getSession return and newPage) would use a closing context.
Fix: set session._closing = true before teardown; getSession() treats
_closing sessions as dead and creates a new one.
Both fixes also applied to the session expiry timer (same pattern).
16 unit tests added covering _closing flag, getSession skip, YT concurrent
cleanup, and session expiry sentinel.
- Navigate abort on tab delete: AbortController on tabState cancels
in-flight page.goto when DELETE /tabs/:tabId fires, preventing
hung navigations on deleted tabs
- snapshotBytes histogram (camofox_snapshot_bytes): tracks accessibility
tree snapshot sizes by type (google_serp, full) at 4 observation points
- navigateAbort.test.js: 6 unit tests covering the abort lifecycle
Extends POST /tabs/:tabId/type (and the openclaw /act type kind) with new
optional params:
mode: 'fill' | 'keyboard' default 'fill'
delay: number ms per character in keyboard mode, default 30
submit: boolean press Enter after typing, default false
pressEnter: boolean alias for submit
mode='fill' (default) preserves existing behavior: locator.fill() or
page.fill(). mode='keyboard' focuses the target (if ref/selector is given)
and types char-by-char via page.keyboard.type(text, { delay }), producing
real key events.
Why: Ember/React contenteditable forms (LinkedIn's DM compose is the canonical
example) track input provenance via beforeinput/InputEvent and refuse to
recognize DOM-mutation writes like locator.fill() or execCommand('insertText').
Only OS-level key events from char-by-char typing satisfy them. Without this,
text appears in the editor but Send buttons never materialize and synthetic
Enter is ignored.
In keyboard mode, ref/selector is optional — if omitted the endpoint types
into whatever currently has focus. The submit flag adds Enter after typing in
a single locked operation (saves a roundtrip for click-to-send flows).
No behavior change for callers not passing mode.
When the server is killed with SIGKILL or OOM, camoufox's internal
.fea5*.so and .5ef7*.node temp files survive in os.tmpdir() and
accumulate. SIGTERM/SIGINT already trigger gracefulShutdown ->
browser.close() which removes them; this adds a one-shot startup
sweep for files older than 5 minutes to reclaim what survived a
non-graceful exit.
The 5-minute age threshold prevents deleting temp files from a
concurrently-running instance.
Fixes#58
Two fixes from the code audit in #38:
1. plugin.ts camofox_screenshot: check Content-Type header before
base64-encoding response. If server returns JSON/text instead of
image (e.g. error with 200 status), return as text error instead
of crashing the client. Also reject responses < 100 bytes.
2. Tab reaper now cleans up sessions with zero tabs remaining.
Previously, after the reaper closed all idle tabs in a session,
the empty session (browser context ~50-100MB) would linger until
the 10-minute session timeout. Now it's closed immediately and
the browser idle shutdown is triggered if no sessions remain.
OpenClaw's security scanner flagged lib/metrics.js with env-harvesting
(critical) because the file contained both `process.env` (in a comment!)
and `'POST'` string literals (in actionFromReq). The scanner regex is
case-insensitive: /\bpost\b/i matches string constants.
Changes:
- Lazy-load prom-client via dynamic import, only when PROMETHEUS_ENABLED=1
- When disabled (default), all metrics are no-op stubs — zero overhead
- Extract actionFromReq/classifyError into lib/request-utils.js (has POST
strings but no process.env — scanner-safe)
- Extract extractPageImages into lib/images.js (has browser-side fetch
inside page.evaluate but no fs.readFile — scanner-safe)
- Remove all process.env references from metrics.js (including comments)
- /metrics endpoint returns 404 when prometheus is disabled
- Add prometheusEnabled flag to lib/config.js
Verified against OpenClaw's actual scanner (skill-scanner.ts):
- Old metrics.js: CRITICAL env-harvesting finding → install blocked
- New split: 0 findings across all files → clean install
- Tested with `openclaw plugins install` on local OpenClaw build
- buildRefs timer leak: clearTimeout on resolve/reject paths
- DELETE /tabs, /tabs/group: accept userId from query OR body
- Scroll: support left/right horizontal scroll
- YouTube transcript: clean up phantom __yt_transcript__ session
- Plugin: kill orphan server process on startup failure
Cherry-picked from imtylervo's audit (PR #39). Excluded Claude Code-specific
changes (screenshot caps, timeout caps, includeScreenshot default, PROXY_PROTOCOL)
and the nth tracking fix (already present in our codebase).