@askjo/camofox-browser 1.8.8 → 1.8.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,7 @@
2
2
  "id": "camofox-browser",
3
3
  "name": "Camofox Browser",
4
4
  "description": "Anti-detection browser automation for AI agents using Camoufox (Firefox-based)",
5
- "version": "1.8.8",
5
+ "version": "1.8.11",
6
6
  "envVars": {
7
7
  "CAMOFOX_API_KEY": {
8
8
  "description": "Secret key for the cookie-import endpoint. Cookie import is disabled when unset. Only set this if you need to import browser cookies and the server is local or access-controlled.",
@@ -163,5 +163,101 @@
163
163
  "label": "Crash Report Relay URL",
164
164
  "placeholder": "https://camofox-crash-relay.askjo.workers.dev/report"
165
165
  }
166
+ },
167
+ "tools": [
168
+ "camofox_create_tab",
169
+ "camofox_snapshot",
170
+ "camofox_click",
171
+ "camofox_type",
172
+ "camofox_navigate",
173
+ "camofox_scroll",
174
+ "camofox_screenshot",
175
+ "camofox_close_tab",
176
+ "camofox_list_tabs",
177
+ "camofox_import_cookies"
178
+ ],
179
+ "runtimeDependencies": {
180
+ "camoufox": {
181
+ "description": "Camoufox anti-detection browser (Firefox fork). Downloaded at install time by camoufox-js from the official Camoufox GitHub releases.",
182
+ "source": "https://github.com/nicedayzhu/camoufox/releases",
183
+ "installer": "camoufox-js (npm)",
184
+ "installedBy": "postinstall script: npx camoufox-js fetch",
185
+ "sizeApprox": "300MB",
186
+ "platforms": [
187
+ "linux-x86_64",
188
+ "linux-aarch64",
189
+ "macos-x86_64",
190
+ "macos-aarch64"
191
+ ],
192
+ "verifiedBy": "camoufox-js verifies download integrity via GitHub release checksums"
193
+ }
194
+ },
195
+ "permissions": {
196
+ "network": {
197
+ "outbound": [
198
+ {
199
+ "target": "User-specified URLs (browsed pages)",
200
+ "purpose": "Browser automation -- navigating to URLs the agent requests",
201
+ "gatedBy": "Agent request via API"
202
+ },
203
+ {
204
+ "target": "https://camofox-crash-relay.askjo.workers.dev/report",
205
+ "purpose": "Anonymized crash/hang reporting",
206
+ "gatedBy": "CAMOFOX_CRASH_REPORT_ENABLED (default: true, set false to disable)"
207
+ }
208
+ ],
209
+ "inbound": {
210
+ "target": "localhost:9377 (default)",
211
+ "purpose": "REST API for browser automation",
212
+ "gatedBy": "CAMOFOX_ACCESS_KEY (optional -- when set, all routes require Bearer auth)"
213
+ }
214
+ },
215
+ "filesystem": {
216
+ "read": [
217
+ {
218
+ "path": "~/.camofox/cookies/",
219
+ "purpose": "Read Netscape cookie files for authenticated browsing",
220
+ "gatedBy": "CAMOFOX_API_KEY (disabled entirely when unset)"
221
+ },
222
+ {
223
+ "path": "/proc/self/status (Linux only)",
224
+ "purpose": "Memory and resource metrics for crash reports",
225
+ "gatedBy": "Only read when crash reporting is enabled"
226
+ }
227
+ ],
228
+ "write": [
229
+ {
230
+ "path": "~/.camofox/profiles/<hashed-userId>/",
231
+ "purpose": "Persisted session state (cookies + localStorage) so users stay logged in across restarts",
232
+ "gatedBy": "persistence plugin (enabled by default, disable via camofox.config.json)"
233
+ },
234
+ {
235
+ "path": "~/.camofox/traces/<hashed-userId>/",
236
+ "purpose": "Optional Playwright session traces for debugging",
237
+ "gatedBy": "trace: true flag on tab creation (opt-in per session, off by default)"
238
+ }
239
+ ]
240
+ },
241
+ "subprocess": [
242
+ {
243
+ "binary": "Camoufox (Firefox fork)",
244
+ "purpose": "The browser engine -- core functionality",
245
+ "isolation": "lib/launcher.js (child_process isolated from route handlers)"
246
+ },
247
+ {
248
+ "binary": "yt-dlp (optional)",
249
+ "purpose": "YouTube transcript extraction (fast path)",
250
+ "isolation": "plugins/youtube/youtube.js (child_process isolated from route handlers)"
251
+ }
252
+ ]
253
+ },
254
+ "securityModel": {
255
+ "architecture": "All process.env reads are in lib/config.js. All child_process usage is in lib/launcher.js and plugins/youtube/youtube.js. server.js has route handlers but zero process.env reads and zero child_process imports. No single file combines secrets access with network sends.",
256
+ "cookieImport": "DISABLED by default. Requires CAMOFOX_API_KEY to be explicitly set. Without the key, the server rejects all cookie requests with 403. Cookie files are read from a sandboxed directory (~/.camofox/cookies/) with path traversal protection.",
257
+ "accessControl": "CAMOFOX_ACCESS_KEY provides global bearer auth for all routes (except /health). Recommended for any non-localhost deployment.",
258
+ "crashReporting": "Anonymized via lib/reporter.js (L28-290). Private domains are HMAC-hashed. No page content, cookies, tokens, IPs, or user data is ever sent. Relay source is in-repo and auditable. Verification endpoint: GET /source returns commit hash and sha256.",
259
+ "binaryDownload": "Camoufox is downloaded at npm install time by camoufox-js (an npm package from the Camoufox project). Downloaded from official GitHub releases with integrity verification by camoufox-js.",
260
+ "sessionPersistence": "User sessions are persisted to ~/.camofox/profiles/ so authenticated browsing survives restarts. UserIds are hashed for directory names. Disable via persistence plugin config.",
261
+ "noEmbeddedSecrets": "Zero credentials, private keys, or tokens ship in this package. All secrets are environment variables or Cloudflare Worker secrets."
166
262
  }
167
263
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askjo/camofox-browser",
3
- "version": "1.8.8",
3
+ "version": "1.8.11",
4
4
  "description": "Headless browser automation server and OpenClaw plugin for AI agents - anti-detection, element refs, and session isolation",
5
5
  "type": "module",
6
6
  "main": "server.js",
@@ -45,7 +45,8 @@
45
45
  "run.sh",
46
46
  "Dockerfile",
47
47
  "README.md",
48
- "LICENSE"
48
+ "LICENSE",
49
+ "AGENTS.md"
49
50
  ],
50
51
  "openclaw": {
51
52
  "extensions": [
@@ -56,7 +57,49 @@
56
57
  },
57
58
  "build": {
58
59
  "openclawVersion": "2026.4.26"
59
- }
60
+ },
61
+ "tools": [
62
+ {
63
+ "name": "camofox_create_tab",
64
+ "description": "Open a new browser tab at a URL"
65
+ },
66
+ {
67
+ "name": "camofox_snapshot",
68
+ "description": "Get accessibility snapshot with element refs"
69
+ },
70
+ {
71
+ "name": "camofox_click",
72
+ "description": "Click an element by ref or CSS selector"
73
+ },
74
+ {
75
+ "name": "camofox_type",
76
+ "description": "Type text into an element"
77
+ },
78
+ {
79
+ "name": "camofox_navigate",
80
+ "description": "Navigate to URL or search macro"
81
+ },
82
+ {
83
+ "name": "camofox_scroll",
84
+ "description": "Scroll page up/down/left/right"
85
+ },
86
+ {
87
+ "name": "camofox_screenshot",
88
+ "description": "Capture page screenshot"
89
+ },
90
+ {
91
+ "name": "camofox_close_tab",
92
+ "description": "Close a browser tab"
93
+ },
94
+ {
95
+ "name": "camofox_list_tabs",
96
+ "description": "List open tabs for a user"
97
+ },
98
+ {
99
+ "name": "camofox_import_cookies",
100
+ "description": "Import Netscape cookie file (requires CAMOFOX_API_KEY)"
101
+ }
102
+ ]
60
103
  },
61
104
  "scripts": {
62
105
  "start": "node server.js",
@@ -2,7 +2,7 @@
2
2
  * VNC plugin for camofox-browser.
3
3
  *
4
4
  * Exposes Camoufox's virtual display via noVNC so a human can interact with
5
- * the browser visually log into sites, solve CAPTCHAs, approve OAuth prompts.
5
+ * the browser visually -- log into sites, solve CAPTCHAs, approve OAuth prompts.
6
6
  * After interactive login, export the storage state via the API endpoint this
7
7
  * plugin registers.
8
8
  *
@@ -35,7 +35,7 @@
35
35
  * NOVNC_PORT=6080 noVNC web UI port
36
36
  *
37
37
  * Registers:
38
- * GET /sessions/:userId/storage_state export Playwright storageState as JSON
38
+ * GET /sessions/:userId/storage_state -- export Playwright storageState as JSON
39
39
  *
40
40
  * Events emitted:
41
41
  * vnc:watcher:started { pid }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * VNC launcher owns all process spawning and env reads.
2
+ * VNC launcher -- owns all process spawning and env reads.
3
3
  * Isolated from route handlers for OpenClaw scanner compliance.
4
4
  */
5
5
 
@@ -11,7 +11,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
11
 
12
12
  /**
13
13
  * Resolve VNC configuration from pluginConfig + env var fallbacks.
14
- * All process.env reads live here callers get a plain config object.
14
+ * All process.env reads live here -- callers get a plain config object.
15
15
  */
16
16
  export function resolveVncConfig(pluginConfig = {}) {
17
17
  const enabled = process.env.ENABLE_VNC === '1' || pluginConfig.enabled === true;
@@ -32,7 +32,7 @@ else
32
32
  log "x11vnc: NO password (bind $NOVNC_PORT to 127.0.0.1 on host + SSH tunnel)"
33
33
  fi
34
34
 
35
- # Start noVNC (websockify) proxies to x11vnc regardless of whether it's up yet
35
+ # Start noVNC (websockify) -- proxies to x11vnc regardless of whether it's up yet
36
36
  NOVNC_DIR="/usr/share/novnc"
37
37
  if [ ! -d "$NOVNC_DIR" ]; then
38
38
  log "ERROR: $NOVNC_DIR not found; noVNC cannot start"
@@ -42,7 +42,7 @@ VNC_BIND="${VNC_BIND:-127.0.0.1}"
42
42
  log "Starting noVNC (websockify) on $VNC_BIND:$NOVNC_PORT -> 127.0.0.1:$VNC_PORT"
43
43
  websockify --web "$NOVNC_DIR" "$VNC_BIND:$NOVNC_PORT" "127.0.0.1:$VNC_PORT" >/var/log/novnc.log 2>&1 &
44
44
 
45
- log "VNC watcher started will attach x11vnc when Camoufox's Xvfb appears"
45
+ log "VNC watcher started -- will attach x11vnc when Camoufox's Xvfb appears"
46
46
 
47
47
  while true; do
48
48
  # Find Xvfb with our patched resolution
@@ -53,7 +53,7 @@ while true; do
53
53
  ' | head -1)
54
54
 
55
55
  if [ -n "$FOUND" ] && [ "$FOUND" != "$CURRENT_DISPLAY" ]; then
56
- # New or changed display (re)attach x11vnc
56
+ # New or changed display -- (re)attach x11vnc
57
57
  if [ -n "$X11VNC_PID" ] && kill -0 "$X11VNC_PID" 2>/dev/null; then
58
58
  log "Camoufox display changed ($CURRENT_DISPLAY -> $FOUND), restarting x11vnc"
59
59
  kill "$X11VNC_PID" 2>/dev/null || true
@@ -1,7 +1,7 @@
1
1
  import { EventEmitter } from 'node:events';
2
2
  import { jest } from '@jest/globals';
3
3
 
4
- // Mock the launcher module index.js no longer imports child_process directly
4
+ // Mock the launcher module -- index.js no longer imports child_process directly
5
5
  const mockWatcher = () => {
6
6
  const proc = new EventEmitter();
7
7
  proc.pid = 12345;
@@ -169,7 +169,7 @@ describe('vnc plugin', () => {
169
169
 
170
170
  await handler(req, res);
171
171
  expect(res.status).toHaveBeenCalledWith(500);
172
- // safeError returns the message string not the raw Error object
172
+ // safeError returns the message string -- not the raw Error object
173
173
  expect(res.json).toHaveBeenCalledWith({ error: 'context destroyed' });
174
174
  });
175
175
 
@@ -20,7 +20,7 @@ export async function register(app, ctx, pluginConfig = {}) {
20
20
  await detectYtDlp(log);
21
21
 
22
22
  // Auth is on by default; set { "auth": false } in camofox.config.json to disable
23
- // Auth off by default matches pre-plugin behavior. Set { "auth": true } to require auth.
23
+ // Auth off by default -- matches pre-plugin behavior. Set { "auth": true } to require auth.
24
24
  const middleware = pluginConfig.auth === true ? ctx.auth() : (_req, _res, next) => next();
25
25
 
26
26
  app.post('/youtube/transcript', middleware, async (req, res) => {
@@ -73,7 +73,7 @@ export async function register(app, ctx, pluginConfig = {}) {
73
73
  }
74
74
  });
75
75
 
76
- // Browser fallback play video, intercept timedtext network response
76
+ // Browser fallback -- play video, intercept timedtext network response
77
77
  async function browserTranscript(reqId, url, videoId, lang) {
78
78
  return await withUserLimit('__yt_transcript__', async () => {
79
79
  await ensureBrowser();
@@ -94,7 +94,7 @@ async function detectYtDlp(log) {
94
94
  return true;
95
95
  } catch {}
96
96
  }
97
- log('warn', 'yt-dlp not found YouTube transcript endpoint will use browser fallback');
97
+ log('warn', 'yt-dlp not found -- YouTube transcript endpoint will use browser fallback');
98
98
  return false;
99
99
  }
100
100
 
@@ -24,7 +24,7 @@ if [ -f "$CONFIG" ] && command -v node >/dev/null 2>&1; then
24
24
  fi
25
25
 
26
26
  if [ -z "$PLUGIN_LIST" ]; then
27
- # No config or no plugins key use all plugin directories
27
+ # No config or no plugins key -- use all plugin directories
28
28
  PLUGIN_LIST=""
29
29
  for d in "$PLUGINS_DIR"/*/; do
30
30
  [ -d "$d" ] || continue
package/scripts/plugin.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * camofox plugin manager install, remove, and list plugins.
4
+ * camofox plugin manager -- install, remove, and list plugins.
5
5
  *
6
6
  * Usage:
7
7
  * node scripts/plugin.js install <source> Install a plugin from git URL or local path
@@ -32,7 +32,7 @@ const ROOT = path.join(__dirname, '..');
32
32
  const PLUGINS_DIR = path.join(ROOT, 'plugins');
33
33
  const CONFIG_PATH = path.join(ROOT, 'camofox.config.json');
34
34
 
35
- // ── Config helpers ──────────────────────────────────────────────────────────
35
+ // -- Config helpers ----------------------------------------------------------
36
36
 
37
37
  function readConfig() {
38
38
  try {
@@ -94,7 +94,7 @@ function removeFromConfig(name) {
94
94
  }
95
95
  }
96
96
 
97
- // ── Source parsing ──────────────────────────────────────────────────────────
97
+ // -- Source parsing ----------------------------------------------------------
98
98
 
99
99
  function parseSource(source) {
100
100
  // Local path
@@ -109,11 +109,11 @@ function parseSource(source) {
109
109
  return { type: 'local', path: resolved, name: path.basename(resolved) };
110
110
  }
111
111
 
112
- // Git URL https://, ssh://, git@, git:
112
+ // Git URL -- https://, ssh://, git@, git:
113
113
  let gitUrl = source;
114
114
  if (gitUrl.startsWith('git:')) {
115
115
  gitUrl = gitUrl.slice(4);
116
- // git:github.com/user/repo https://github.com/user/repo
116
+ // git:github.com/user/repo -> https://github.com/user/repo
117
117
  if (!gitUrl.startsWith('http') && !gitUrl.startsWith('ssh://') && !gitUrl.startsWith('git@')) {
118
118
  gitUrl = `https://${gitUrl}`;
119
119
  }
@@ -132,7 +132,7 @@ function parseSource(source) {
132
132
  return { type: 'git', url: cloneUrl, name };
133
133
  }
134
134
 
135
- // ── Install ─────────────────────────────────────────────────────────────────
135
+ // -- Install -----------------------------------------------------------------
136
136
 
137
137
  function isPluginDir(dir) {
138
138
  const indexPath = path.join(dir, 'index.js');
@@ -177,12 +177,12 @@ function installFromGit(url, name) {
177
177
  }
178
178
  }
179
179
  if (installed.length === 0) {
180
- fatal(`No plugins found in ${url} expected index.js with register() at root or in plugins/*/`);
180
+ fatal(`No plugins found in ${url} -- expected index.js with register() at root or in plugins/*/`);
181
181
  }
182
182
  return installed;
183
183
  }
184
184
 
185
- fatal(`No plugins found in ${url} expected index.js with register() at root or plugins/*/ subdirs`);
185
+ fatal(`No plugins found in ${url} -- expected index.js with register() at root or plugins/*/ subdirs`);
186
186
  } finally {
187
187
  if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true });
188
188
  }
@@ -198,16 +198,16 @@ function installPluginDeps(name) {
198
198
  execSync('npm install --omit=dev', { cwd: pluginDir, stdio: 'inherit' });
199
199
  }
200
200
 
201
- // Check for apt.txt / post-install.sh (just warn can't run apt locally)
201
+ // Check for apt.txt / post-install.sh (just warn -- can't run apt locally)
202
202
  if (fs.existsSync(path.join(pluginDir, 'apt.txt'))) {
203
- console.log(`⚠ ${name} has apt.txt system packages need Docker build or manual install`);
203
+ console.log(`WARNING ${name} has apt.txt -- system packages need Docker build or manual install`);
204
204
  }
205
205
  if (fs.existsSync(path.join(pluginDir, 'post-install.sh'))) {
206
- console.log(`⚠ ${name} has post-install.sh run it manually or rebuild Docker image`);
206
+ console.log(`WARNING ${name} has post-install.sh -- run it manually or rebuild Docker image`);
207
207
  }
208
208
  }
209
209
 
210
- // ── Remove ──────────────────────────────────────────────────────────────────
210
+ // -- Remove ------------------------------------------------------------------
211
211
 
212
212
  function removePlugin(name) {
213
213
  const pluginDir = path.join(PLUGINS_DIR, name);
@@ -217,10 +217,10 @@ function removePlugin(name) {
217
217
 
218
218
  fs.rmSync(pluginDir, { recursive: true });
219
219
  removeFromConfig(name);
220
- console.log(`✓ Removed plugin "${name}"`);
220
+ console.log(`[ok] Removed plugin "${name}"`);
221
221
  }
222
222
 
223
- // ── List ────────────────────────────────────────────────────────────────────
223
+ // -- List --------------------------------------------------------------------
224
224
 
225
225
  function listPlugins() {
226
226
  const config = readConfig();
@@ -244,7 +244,7 @@ function listPlugins() {
244
244
  console.log('Installed plugins:\n');
245
245
  for (const name of plugins.sort()) {
246
246
  const enabled = configPlugins.size === 0 || configPlugins.has(name);
247
- const status = enabled ? '' : '';
247
+ const status = enabled ? '[ok]' : 'o';
248
248
  const hasTest = fs.existsSync(path.join(PLUGINS_DIR, name, `${name}.test.js`))
249
249
  || fs.readdirSync(path.join(PLUGINS_DIR, name)).some(f => f.endsWith('.test.js'));
250
250
  const hasDeps = fs.existsSync(path.join(PLUGINS_DIR, name, 'apt.txt'))
@@ -263,11 +263,11 @@ function listPlugins() {
263
263
  if (configPlugins.size > 0) {
264
264
  console.log(`\n${configPlugins.size} plugin(s) enabled in camofox.config.json`);
265
265
  } else {
266
- console.log('\nNo plugins[] in config all plugins are loaded');
266
+ console.log('\nNo plugins[] in config -- all plugins are loaded');
267
267
  }
268
268
  }
269
269
 
270
- // ── Helpers ─────────────────────────────────────────────────────────────────
270
+ // -- Helpers -----------------------------------------------------------------
271
271
 
272
272
  function copyDirSync(src, dest) {
273
273
  fs.mkdirSync(dest, { recursive: true });
@@ -289,7 +289,7 @@ function fatal(msg) {
289
289
  process.exit(1);
290
290
  }
291
291
 
292
- // ── CLI ─────────────────────────────────────────────────────────────────────
292
+ // -- CLI ---------------------------------------------------------------------
293
293
 
294
294
  const [,, action, ...args] = process.argv;
295
295
 
@@ -308,7 +308,7 @@ switch (action) {
308
308
  installPluginDeps(name);
309
309
  }
310
310
 
311
- console.log(`\n Installed: ${installed.join(', ')}`);
311
+ console.log(`\n[ok] Installed: ${installed.join(', ')}`);
312
312
  console.log(' Restart the server to load new plugin(s).');
313
313
  break;
314
314
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Tests for scripts/plugin.js plugin install, remove, list.
2
+ * Tests for scripts/plugin.js -- plugin install, remove, list.
3
3
  */
4
4
 
5
5
  import fs from 'fs';
@@ -32,7 +32,7 @@ describe('plugin list', () => {
32
32
  test('lists youtube as enabled', () => {
33
33
  const out = run('list');
34
34
  expect(out).toContain('youtube');
35
- expect(out).toContain('');
35
+ expect(out).toContain('[ok]');
36
36
  });
37
37
  });
38
38