@phi-code-admin/camofox-browser 1.0.0
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.
- package/AGENTS.md +571 -0
- package/Dockerfile +86 -0
- package/LICENSE +21 -0
- package/README.md +691 -0
- package/camofox.config.json +10 -0
- package/dist/plugin.js +616 -0
- package/lib/auth.js +134 -0
- package/lib/camoufox-executable.js +189 -0
- package/lib/config.js +153 -0
- package/lib/cookies.js +119 -0
- package/lib/downloads.js +168 -0
- package/lib/extract.js +74 -0
- package/lib/fly.js +54 -0
- package/lib/images.js +88 -0
- package/lib/inflight.js +16 -0
- package/lib/launcher.js +47 -0
- package/lib/macros.js +31 -0
- package/lib/metrics.js +184 -0
- package/lib/openapi.js +105 -0
- package/lib/persistence.js +89 -0
- package/lib/plugins.js +175 -0
- package/lib/proxy.js +277 -0
- package/lib/reporter.js +1102 -0
- package/lib/request-utils.js +59 -0
- package/lib/resources.js +76 -0
- package/lib/snapshot.js +41 -0
- package/lib/tmp-cleanup.js +108 -0
- package/lib/tracing.js +137 -0
- package/openclaw.plugin.json +268 -0
- package/package.json +148 -0
- package/plugin.js +616 -0
- package/plugin.ts +758 -0
- package/plugins/persistence/AGENTS.md +37 -0
- package/plugins/persistence/README.md +48 -0
- package/plugins/persistence/index.js +124 -0
- package/plugins/vnc/AGENTS.md +42 -0
- package/plugins/vnc/README.md +165 -0
- package/plugins/vnc/apt.txt +7 -0
- package/plugins/vnc/index.js +142 -0
- package/plugins/vnc/spawn.js +8 -0
- package/plugins/vnc/vnc-launcher.js +64 -0
- package/plugins/vnc/vnc-watcher.sh +82 -0
- package/plugins/youtube/AGENTS.md +25 -0
- package/plugins/youtube/apt.txt +1 -0
- package/plugins/youtube/index.js +206 -0
- package/plugins/youtube/post-install.sh +5 -0
- package/plugins/youtube/youtube.js +301 -0
- package/run.sh +37 -0
- package/scripts/exec.js +8 -0
- package/scripts/generate-openapi.js +24 -0
- package/scripts/install-plugin-deps.sh +63 -0
- package/scripts/plugin.js +342 -0
- package/scripts/postinstall.js +20 -0
- package/scripts/sync-version.js +25 -0
- package/server.js +6059 -0
- package/tsconfig.json +12 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
# camofox-browser Agent Guide
|
|
2
|
+
|
|
3
|
+
Headless browser automation server for AI agents. Run locally or deploy to any cloud provider.
|
|
4
|
+
|
|
5
|
+
## Quick Start for Agents
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Install and start
|
|
9
|
+
npm install && npm start
|
|
10
|
+
# Server runs on http://localhost:9377
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Core Workflow
|
|
14
|
+
|
|
15
|
+
1. **Create a tab** -> Get `tabId`
|
|
16
|
+
2. **Navigate** -> Go to URL or use search macro
|
|
17
|
+
3. **Get snapshot** -> Receive page content with element refs (`e1`, `e2`, etc.)
|
|
18
|
+
4. **Interact** -> Click/type using refs
|
|
19
|
+
5. **Repeat** steps 3-4 as needed
|
|
20
|
+
|
|
21
|
+
## API Reference
|
|
22
|
+
|
|
23
|
+
### Create Tab
|
|
24
|
+
```bash
|
|
25
|
+
POST /tabs
|
|
26
|
+
{"userId": "agent1", "sessionKey": "task1", "url": "https://example.com"}
|
|
27
|
+
```
|
|
28
|
+
Returns: `{"tabId": "abc123", "url": "...", "title": "..."}`
|
|
29
|
+
|
|
30
|
+
### Navigate
|
|
31
|
+
```bash
|
|
32
|
+
POST /tabs/:tabId/navigate
|
|
33
|
+
{"userId": "agent1", "url": "https://google.com"}
|
|
34
|
+
# Or use macro:
|
|
35
|
+
{"userId": "agent1", "macro": "@google_search", "query": "weather today"}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Get Snapshot
|
|
39
|
+
```bash
|
|
40
|
+
GET /tabs/:tabId/snapshot?userId=agent1
|
|
41
|
+
```
|
|
42
|
+
Returns accessibility tree with refs:
|
|
43
|
+
```
|
|
44
|
+
[heading] Example Domain
|
|
45
|
+
[paragraph] This domain is for use in examples.
|
|
46
|
+
[link e1] More information...
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Click Element
|
|
50
|
+
```bash
|
|
51
|
+
POST /tabs/:tabId/click
|
|
52
|
+
{"userId": "agent1", "ref": "e1"}
|
|
53
|
+
# Or CSS selector:
|
|
54
|
+
{"userId": "agent1", "selector": "button.submit"}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Type Text
|
|
58
|
+
```bash
|
|
59
|
+
POST /tabs/:tabId/type
|
|
60
|
+
{"userId": "agent1", "ref": "e2", "text": "hello world"}
|
|
61
|
+
# Add enter: {"userId": "agent1", "ref": "e2", "text": "search query", "pressEnter": true}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Scroll
|
|
65
|
+
```bash
|
|
66
|
+
POST /tabs/:tabId/scroll
|
|
67
|
+
{"userId": "agent1", "direction": "down", "amount": 500}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Navigation
|
|
71
|
+
```bash
|
|
72
|
+
POST /tabs/:tabId/back {"userId": "agent1"}
|
|
73
|
+
POST /tabs/:tabId/forward {"userId": "agent1"}
|
|
74
|
+
POST /tabs/:tabId/refresh {"userId": "agent1"}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Get Links
|
|
78
|
+
```bash
|
|
79
|
+
GET /tabs/:tabId/links?userId=agent1&limit=50
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Close Tab
|
|
83
|
+
```bash
|
|
84
|
+
DELETE /tabs/:tabId?userId=agent1
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Search Macros
|
|
88
|
+
|
|
89
|
+
Use these instead of constructing URLs:
|
|
90
|
+
|
|
91
|
+
| Macro | Site |
|
|
92
|
+
|-------|------|
|
|
93
|
+
| `@google_search` | Google |
|
|
94
|
+
| `@youtube_search` | YouTube |
|
|
95
|
+
| `@amazon_search` | Amazon |
|
|
96
|
+
| `@reddit_search` | Reddit |
|
|
97
|
+
| `@wikipedia_search` | Wikipedia |
|
|
98
|
+
| `@twitter_search` | Twitter/X |
|
|
99
|
+
| `@yelp_search` | Yelp |
|
|
100
|
+
| `@linkedin_search` | LinkedIn |
|
|
101
|
+
|
|
102
|
+
## Element Refs
|
|
103
|
+
|
|
104
|
+
Refs like `e1`, `e2` are stable identifiers for page elements:
|
|
105
|
+
|
|
106
|
+
1. Call `/snapshot` to get current refs
|
|
107
|
+
2. Use ref in `/click` or `/type`
|
|
108
|
+
3. Refs reset on navigation - get new snapshot after
|
|
109
|
+
|
|
110
|
+
## Session Management
|
|
111
|
+
|
|
112
|
+
- `userId` isolates cookies/storage between users
|
|
113
|
+
- `sessionKey` groups tabs by conversation/task (legacy: `listItemId` also accepted)
|
|
114
|
+
- Sessions timeout after 30 minutes of inactivity
|
|
115
|
+
- Delete all user data: `DELETE /sessions/:userId`
|
|
116
|
+
|
|
117
|
+
## Running Engines
|
|
118
|
+
|
|
119
|
+
### Camoufox (Default)
|
|
120
|
+
```bash
|
|
121
|
+
npm start
|
|
122
|
+
# Or: ./run.sh
|
|
123
|
+
```
|
|
124
|
+
Firefox-based with anti-detection. Bypasses Google captcha.
|
|
125
|
+
|
|
126
|
+
## Testing
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
npm test # All tests (unit + e2e + plugin)
|
|
130
|
+
npm run test:plugins # All plugin tests
|
|
131
|
+
npm run test:e2e # E2E tests
|
|
132
|
+
npm run test:live # Live Google tests
|
|
133
|
+
npm run test:debug # With server output
|
|
134
|
+
npx jest plugins/youtube # Single plugin's tests
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Docker
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
docker build -t camofox-browser .
|
|
141
|
+
docker run -p 9377:9377 camofox-browser
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Key Files
|
|
145
|
+
|
|
146
|
+
- `server.js` - Camoufox engine (routes + browser logic only -- NO `process.env` or `child_process`)
|
|
147
|
+
- `lib/openapi.js` - OpenAPI spec generation via swagger-jsdoc + docs route setup
|
|
148
|
+
- `lib/config.js` - All `process.env` reads centralized here
|
|
149
|
+
- `plugins/youtube/youtube.js` - YouTube transcript extraction via yt-dlp (`child_process` isolated here)
|
|
150
|
+
- `lib/launcher.js` - Subprocess spawning (`child_process` isolated here)
|
|
151
|
+
- `lib/cookies.js` - Cookie file I/O
|
|
152
|
+
- `lib/metrics.js` - Prometheus metrics (lazy-loaded, off by default -- set `PROMETHEUS_ENABLED=1`)
|
|
153
|
+
- `lib/request-utils.js` - HTTP request classification helpers (`actionFromReq`, `classifyError`)
|
|
154
|
+
- `lib/snapshot.js` - Accessibility tree snapshot
|
|
155
|
+
- `lib/macros.js` - Search macro URL expansion
|
|
156
|
+
- `lib/plugins.js` - Plugin loader and event bus
|
|
157
|
+
- `lib/auth.js` - Shared auth middleware (API key / loopback)
|
|
158
|
+
- `camofox.config.json` - Plugin configuration (which plugins to load)
|
|
159
|
+
- `plugins/` - Plugin directory (loaded per camofox.config.json)
|
|
160
|
+
- `plugins/youtube/` - Default plugin: YouTube transcript extraction
|
|
161
|
+
- `scripts/install-plugin-deps.sh` - Installs plugin deps (apt.txt + post-install.sh)
|
|
162
|
+
- `plugins/vnc/index.js` - VNC plugin routes (no `child_process` -- spawning isolated in `vnc-launcher.js`)
|
|
163
|
+
- `plugins/vnc/vnc-launcher.js` - VNC process management (`child_process` isolated here)
|
|
164
|
+
- `plugins/persistence/index.js` - Session persistence lifecycle hooks
|
|
165
|
+
- `lib/persistence.js` - Atomic storage state read/write
|
|
166
|
+
- `lib/inflight.js` - Inflight request coalescing
|
|
167
|
+
- `lib/tmp-cleanup.js` - Orphaned temp file cleanup
|
|
168
|
+
- `lib/reporter.js` - Crash/hang reporter with anonymization + GitHub App auth (see README "Crash Reporter" for setup)
|
|
169
|
+
- `Dockerfile` - Production container with default plugin deps pre-installed
|
|
170
|
+
|
|
171
|
+
## OpenAPI Spec (REQUIRED for route changes)
|
|
172
|
+
|
|
173
|
+
The API spec is auto-generated from `@openapi` JSDoc comments in `server.js` via [swagger-jsdoc](https://github.com/Surnet/swagger-jsdoc). It's served at `GET /openapi.json` (machine-readable) and `GET /docs` ([swagger-stripey](https://github.com/skyfallsin/swagger-stripey) three-panel UI).
|
|
174
|
+
|
|
175
|
+
**When adding, modifying, or removing a route, you MUST update the `@openapi` JSDoc block above it.**
|
|
176
|
+
|
|
177
|
+
Every route handler in `server.js` has a JSDoc comment block directly above it like:
|
|
178
|
+
|
|
179
|
+
```js
|
|
180
|
+
/**
|
|
181
|
+
* @openapi
|
|
182
|
+
* /tabs/{tabId}/click:
|
|
183
|
+
* post:
|
|
184
|
+
* tags: [Interaction]
|
|
185
|
+
* summary: Click an element
|
|
186
|
+
* parameters:
|
|
187
|
+
* - name: tabId
|
|
188
|
+
* in: path
|
|
189
|
+
* required: true
|
|
190
|
+
* schema:
|
|
191
|
+
* type: string
|
|
192
|
+
* requestBody:
|
|
193
|
+
* required: true
|
|
194
|
+
* content:
|
|
195
|
+
* application/json:
|
|
196
|
+
* schema:
|
|
197
|
+
* type: object
|
|
198
|
+
* required: [userId]
|
|
199
|
+
* properties:
|
|
200
|
+
* userId:
|
|
201
|
+
* type: string
|
|
202
|
+
* ref:
|
|
203
|
+
* type: string
|
|
204
|
+
* responses:
|
|
205
|
+
* 200:
|
|
206
|
+
* description: Click result.
|
|
207
|
+
* content:
|
|
208
|
+
* application/json:
|
|
209
|
+
* schema:
|
|
210
|
+
* type: object
|
|
211
|
+
* 404:
|
|
212
|
+
* description: Tab not found.
|
|
213
|
+
* content:
|
|
214
|
+
* application/json:
|
|
215
|
+
* schema:
|
|
216
|
+
* $ref: '#/components/schemas/Error'
|
|
217
|
+
*/
|
|
218
|
+
app.post('/tabs/:tabId/click', async (req, res) => {
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**Rules:**
|
|
222
|
+
- New routes: add a `@openapi` JSDoc block immediately above the `app.get/post/delete(...)` call
|
|
223
|
+
- Path params use `{tabId}` syntax (not `:tabId`) in the JSDoc YAML
|
|
224
|
+
- Tag must be one of: `System`, `Tabs`, `Navigation`, `Interaction`, `Content`, `Sessions`, `Browser`, `Legacy`
|
|
225
|
+
- Every operation must have `tags`, `summary`, and `responses`
|
|
226
|
+
- Include `requestBody` for POST/PUT/DELETE routes that accept JSON
|
|
227
|
+
- Include `parameters` for path params and required query params
|
|
228
|
+
- Mark backward-compat endpoints with `deprecated: true`
|
|
229
|
+
- Removing a route: delete the `@openapi` block along with the handler
|
|
230
|
+
- **After any route change, run `npm run generate-openapi`** to regenerate the committed `openapi.json`. The test suite will fail if it's stale.
|
|
231
|
+
- Run `npx jest tests/unit/openapi.test.js` to verify coverage -- the test fails if any route is missing from the spec, if a stale route exists, or if `openapi.json` is out of date
|
|
232
|
+
- Reusable schemas go in `components.schemas` in `lib/openapi.js` (the `swaggerDefinition`); reference them via `$ref: '#/components/schemas/Name'`
|
|
233
|
+
|
|
234
|
+
## Telemetry
|
|
235
|
+
|
|
236
|
+
**No credentials are embedded in this package.** `lib/reporter.js` is a stateless HTTP client that sends anonymized crash/hang telemetry to a Cloudflare Worker endpoint (`camofox-telemetry.askjo.workers.dev`). The endpoint holds the GitHub App credentials as environment secrets -- see `workers/crash-reporter/index.ts`. The source is in-repo and auditable.
|
|
237
|
+
|
|
238
|
+
- **Architecture**: `lib/reporter.js` (client, no secrets, no `fs`) -> POST -> Cloudflare Worker endpoint -> GitHub Issues
|
|
239
|
+
- **`lib/reporter.js`** has ZERO credentials, ZERO private keys, ZERO `fs` imports. It only does `fetch()` to the telemetry endpoint.
|
|
240
|
+
- **`lib/resources.js`** handles `fs`-based resource snapshots (reading /proc on Linux) -- separated from reporter.js so no file-read + network-send pattern exists in any single file. No `child_process` import.
|
|
241
|
+
- **Anonymization** is in `lib/reporter.js` L28-290 -- text scrubbing (`anonymize()`), URL anonymization (`createUrlAnonymizer()`), and tab health tracking (`createTabHealthTracker()`)
|
|
242
|
+
- **Public domain list** (~120 entries) determines which domains are shown verbatim vs HMAC-hashed
|
|
243
|
+
- **Tests**: `tests/unit/crashRelay.test.js` (telemetry client), `tests/unit/crashRelayWorker.test.js` (worker contract), `tests/unit/noSecrets.test.js` (asserts no key material in shipped files)
|
|
244
|
+
- Self-hosted endpoint: see README "Self-hosted telemetry endpoint" section
|
|
245
|
+
- Disable with `CAMOFOX_CRASH_REPORT_ENABLED=false`
|
|
246
|
+
|
|
247
|
+
## Code Separation Conventions
|
|
248
|
+
|
|
249
|
+
The codebase separates concerns across files for clarity and auditability:
|
|
250
|
+
|
|
251
|
+
- **Configuration**: `process.env` reads live in `lib/config.js`, which exports a plain config object. No other file reads environment variables directly.
|
|
252
|
+
- **Subprocess management**: `child_process` usage lives in dedicated launcher modules (`lib/launcher.js`, `plugins/youtube/youtube.js`, `plugins/vnc/vnc-launcher.js`), not in route handlers.
|
|
253
|
+
- **Route handlers**: `server.js` defines Express routes but delegates env/config reads and subprocess spawning to the modules above.
|
|
254
|
+
- **Metrics**: `lib/metrics.js` lazy-loads prom-client. `lib/request-utils.js` handles HTTP method classification.
|
|
255
|
+
|
|
256
|
+
When adding features that need env vars or subprocesses, put that code in a `lib/` module and import the result into `server.js`.
|
|
257
|
+
|
|
258
|
+
## Plugin System
|
|
259
|
+
|
|
260
|
+
Plugins extend camofox-browser with new endpoints, background processes, and lifecycle hooks. The server auto-loads all plugins from `plugins/<name>/index.js` on startup.
|
|
261
|
+
|
|
262
|
+
### Creating a Plugin
|
|
263
|
+
|
|
264
|
+
```
|
|
265
|
+
plugins/
|
|
266
|
+
my-plugin/
|
|
267
|
+
index.js Required -- exports register(app, ctx)
|
|
268
|
+
apt.txt Optional -- system packages (one per line)
|
|
269
|
+
post-install.sh Optional -- executable hook for binary downloads
|
|
270
|
+
*.test.js Optional -- Jest tests (auto-discovered)
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
```js
|
|
274
|
+
// plugins/my-plugin/index.js
|
|
275
|
+
|
|
276
|
+
export function register(app, ctx) {
|
|
277
|
+
const { sessions, config, log, events, auth, ensureBrowser, getSession, destroySession,
|
|
278
|
+
withUserLimit, safePageClose, normalizeUserId, validateUrl, safeError,
|
|
279
|
+
buildProxyUrl, proxyPool, failuresTotal } = ctx;
|
|
280
|
+
|
|
281
|
+
// Register Express routes (auth() enforces API key or loopback)
|
|
282
|
+
app.get('/my-endpoint', auth(), async (req, res) => {
|
|
283
|
+
const session = sessions.get(req.params.userId);
|
|
284
|
+
res.json({ ok: true });
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// Listen to lifecycle events
|
|
288
|
+
events.on('browser:launched', ({ browser, display }) => {
|
|
289
|
+
log('info', 'browser is up', { display });
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
events.on('session:created', ({ userId, context }) => {
|
|
293
|
+
log('info', 'new session', { userId });
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
events.on('tab:navigated', ({ userId, tabId, url }) => {
|
|
297
|
+
log('info', 'navigation', { userId, tabId, url });
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### Plugin Context (`ctx`)
|
|
303
|
+
|
|
304
|
+
| Property | Type | Description |
|
|
305
|
+
|----------|------|-------------|
|
|
306
|
+
| `sessions` | `Map` | Live sessions: `userId -> { context, tabGroups, lastAccess }` |
|
|
307
|
+
| `config` | `object` | Server CONFIG (port, apiKey, nodeEnv, proxy, etc.) |
|
|
308
|
+
| `log` | `function` | `log(level, msg, fields)` -- structured JSON logging |
|
|
309
|
+
| `events` | `EventEmitter` | Plugin event bus (29 events -- see below) |
|
|
310
|
+
| `auth` | `function` | `auth()` returns Express middleware enforcing API key / loopback |
|
|
311
|
+
| `ensureBrowser` | `async function` | Launch browser if not running, return browser instance |
|
|
312
|
+
| `getSession` | `async function` | `getSession(userId)` -- get or create a session |
|
|
313
|
+
| `destroySession` | `function` | `destroySession(userId)` -- tear down a session |
|
|
314
|
+
| `withUserLimit` | `async function` | `withUserLimit(userId, fn)` -- run `fn` within per-user concurrency limit |
|
|
315
|
+
| `safePageClose` | `async function` | `safePageClose(page)` -- close a page with timeout guard |
|
|
316
|
+
| `normalizeUserId` | `function` | `normalizeUserId(id)` -- coerce to string for map keys |
|
|
317
|
+
| `validateUrl` | `function` | `validateUrl(url)` -- returns error string or null |
|
|
318
|
+
| `safeError` | `function` | `safeError(err)` -- sanitize error for client response |
|
|
319
|
+
| `buildProxyUrl` | `function` | `buildProxyUrl(pool, proxyConfig)` -- get proxy URL for external requests |
|
|
320
|
+
| `proxyPool` | `object\|null` | Proxy pool instance (null if no proxy configured) |
|
|
321
|
+
| `failuresTotal` | `Counter` | Prometheus counter: `failuresTotal.labels(type, action).inc()` |
|
|
322
|
+
| `createMetric` | `async function` | Create a Prometheus metric registered to the shared registry (see below) |
|
|
323
|
+
| `metricsRegistry` | `function` | `metricsRegistry()` -- raw prom-client Registry or null |
|
|
324
|
+
|
|
325
|
+
### Events (29)
|
|
326
|
+
|
|
327
|
+
28 emitted by core, 1 (`session:storage:export`) emitted by plugins.
|
|
328
|
+
|
|
329
|
+
#### Browser Lifecycle
|
|
330
|
+
| Event | Payload | Mutating? |
|
|
331
|
+
|-------|---------|-----------|
|
|
332
|
+
| `browser:launching` | `{ options }` | (ok) Modify launch options in-place |
|
|
333
|
+
| `browser:launched` | `{ browser, display }` | |
|
|
334
|
+
| `browser:restart` | `{ reason }` | |
|
|
335
|
+
| `browser:closed` | `{ reason }` | |
|
|
336
|
+
| `browser:error` | `{ error }` | |
|
|
337
|
+
|
|
338
|
+
#### Session Lifecycle
|
|
339
|
+
| Event | Payload | Mutating? |
|
|
340
|
+
|-------|---------|-----------|
|
|
341
|
+
| `session:creating` | `{ userId, contextOptions }` | (ok) Modify context options in-place |
|
|
342
|
+
| `session:created` | `{ userId, context }` | |
|
|
343
|
+
| `session:destroyed` | `{ userId, reason }` | |
|
|
344
|
+
| `session:expired` | `{ userId, idleMs }` | |
|
|
345
|
+
|
|
346
|
+
#### Tab Lifecycle
|
|
347
|
+
| Event | Payload |
|
|
348
|
+
|-------|---------|
|
|
349
|
+
| `tab:created` | `{ userId, tabId, page, url }` |
|
|
350
|
+
| `tab:navigated` | `{ userId, tabId, url, prevUrl }` |
|
|
351
|
+
| `tab:destroyed` | `{ userId, tabId, reason }` |
|
|
352
|
+
| `tab:recycled` | `{ userId, tabId }` |
|
|
353
|
+
| `tab:error` | `{ userId, tabId, error }` |
|
|
354
|
+
|
|
355
|
+
#### Content
|
|
356
|
+
| Event | Payload |
|
|
357
|
+
|-------|---------|
|
|
358
|
+
| `tab:snapshot` | `{ userId, tabId, snapshot }` |
|
|
359
|
+
| `tab:screenshot` | `{ userId, tabId, buffer }` |
|
|
360
|
+
| `tab:evaluate` | `{ userId, tabId, expression }` |
|
|
361
|
+
| `tab:evaluated` | `{ userId, tabId, result }` |
|
|
362
|
+
|
|
363
|
+
#### Input
|
|
364
|
+
| Event | Payload |
|
|
365
|
+
|-------|---------|
|
|
366
|
+
| `tab:click` | `{ userId, tabId, ref, selector }` |
|
|
367
|
+
| `tab:type` | `{ userId, tabId, text, ref, mode }` |
|
|
368
|
+
| `tab:scroll` | `{ userId, tabId, direction, amount }` |
|
|
369
|
+
| `tab:press` | `{ userId, tabId, key }` |
|
|
370
|
+
|
|
371
|
+
#### Downloads
|
|
372
|
+
| Event | Payload |
|
|
373
|
+
|-------|---------|
|
|
374
|
+
| `tab:download:start` | `{ userId, tabId, filename, url }` |
|
|
375
|
+
| `tab:download:complete` | `{ userId, tabId, filename, path, size }` |
|
|
376
|
+
|
|
377
|
+
#### Cookies / Auth
|
|
378
|
+
| Event | Payload |
|
|
379
|
+
|-------|---------|
|
|
380
|
+
| `session:cookies:import` | `{ userId, count }` |
|
|
381
|
+
| `session:storage:export` | `{ userId }` |
|
|
382
|
+
|
|
383
|
+
#### Server
|
|
384
|
+
| Event | Payload |
|
|
385
|
+
|-------|---------|
|
|
386
|
+
| `server:starting` | `{ port }` |
|
|
387
|
+
| `server:started` | `{ port, pid }` |
|
|
388
|
+
| `server:shutdown` | `{ signal }` |
|
|
389
|
+
|
|
390
|
+
### Mutating Hooks
|
|
391
|
+
|
|
392
|
+
`browser:launching`, `session:creating`, `session:created`, and `session:destroyed` are emitted via `events.emitAsync()` -- the server awaits all listeners (including async ones) before proceeding. This ensures async work like loading storage state from disk completes before the context is created.
|
|
393
|
+
|
|
394
|
+
Other events use regular `events.emit()` (fire-and-forget).
|
|
395
|
+
|
|
396
|
+
Modify payload objects in-place:
|
|
397
|
+
|
|
398
|
+
```js
|
|
399
|
+
// Change Xvfb resolution (e.g., for VNC plugin)
|
|
400
|
+
events.on('browser:launching', ({ options }) => {
|
|
401
|
+
options.virtual_display_resolution = '1920x1080x24';
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
// Inject saved auth state into new sessions
|
|
405
|
+
events.on('session:creating', ({ userId, contextOptions }) => {
|
|
406
|
+
const saved = loadStorageState(userId);
|
|
407
|
+
if (saved) contextOptions.storageState = saved;
|
|
408
|
+
});
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
### System Packages (`apt.txt`) and Post-Install Hooks
|
|
412
|
+
|
|
413
|
+
Plugins that need system packages list them one per line in `apt.txt`:
|
|
414
|
+
|
|
415
|
+
```
|
|
416
|
+
# plugins/vnc/apt.txt
|
|
417
|
+
x11vnc
|
|
418
|
+
novnc
|
|
419
|
+
python3-websockify
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
For binary downloads or setup not available via apt, add an executable `post-install.sh`:
|
|
423
|
+
|
|
424
|
+
```bash
|
|
425
|
+
# plugins/youtube/post-install.sh
|
|
426
|
+
#!/bin/sh
|
|
427
|
+
set -e
|
|
428
|
+
curl -fL https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -o /usr/local/bin/yt-dlp
|
|
429
|
+
chmod +x /usr/local/bin/yt-dlp
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
Both are run by `scripts/install-plugin-deps.sh` during Docker build.
|
|
433
|
+
|
|
434
|
+
### Configuration (`camofox.config.json`)
|
|
435
|
+
|
|
436
|
+
`camofox.config.json` controls which plugins are loaded at runtime and during Docker build:
|
|
437
|
+
|
|
438
|
+
```json
|
|
439
|
+
{
|
|
440
|
+
"id": "camofox-browser",
|
|
441
|
+
"name": "Camofox Browser",
|
|
442
|
+
"version": "1.5.2",
|
|
443
|
+
"plugins": ["youtube"]
|
|
444
|
+
}
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
- **`plugins`** -- array of plugin directory names to load. Only these are loaded at startup and have deps installed during build.
|
|
448
|
+
- If the file is missing or has no `plugins` key, **all** plugins in `plugins/` are loaded (backward-compatible).
|
|
449
|
+
- This is camofox's own config. `openclaw.plugin.json` is separate -- it tells the OpenClaw Gateway how to configure camofox as an external service.
|
|
450
|
+
|
|
451
|
+
### Installing Plugins
|
|
452
|
+
|
|
453
|
+
Use the plugin manager to install third-party plugins from git or local paths:
|
|
454
|
+
|
|
455
|
+
```bash
|
|
456
|
+
# Install from git
|
|
457
|
+
npm run plugin install https://github.com/user/camofox-screenshot-plugin
|
|
458
|
+
npm run plugin install git:github.com/user/my-plugin
|
|
459
|
+
|
|
460
|
+
# Install from local directory
|
|
461
|
+
npm run plugin install ./path/to/my-plugin
|
|
462
|
+
|
|
463
|
+
# List installed plugins
|
|
464
|
+
npm run plugin list
|
|
465
|
+
|
|
466
|
+
# Remove a plugin
|
|
467
|
+
npm run plugin remove my-plugin
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
The installer copies the plugin into `plugins/`, adds it to `camofox.config.json`, and runs `npm install` for any npm dependencies. System deps (`apt.txt`, `post-install.sh`) are flagged but must be installed manually or via Docker rebuild.
|
|
471
|
+
|
|
472
|
+
Plugin sources can be:
|
|
473
|
+
- **Git repos** where the root has `index.js` with `register()` (installed as one plugin)
|
|
474
|
+
- **Git repos** with a `plugins/` subdirectory (each subdirectory installed as a separate plugin)
|
|
475
|
+
- **Local directories** with `index.js` and `register()`
|
|
476
|
+
|
|
477
|
+
### Default Plugins
|
|
478
|
+
|
|
479
|
+
Three plugins ship by default:
|
|
480
|
+
|
|
481
|
+
- **youtube** -- YouTube transcript extraction (enabled by default)
|
|
482
|
+
- **persistence** -- Per-user session state persistence to `~/.camofox/profiles/` (enabled by default)
|
|
483
|
+
- **vnc** -- Interactive browser login via noVNC (disabled by default, requires `ENABLE_VNC=1`)
|
|
484
|
+
|
|
485
|
+
The `youtube` plugin ships as a default plugin -- it's listed in `camofox.config.json` and included in the base Docker image with its deps pre-installed. The base image runs `scripts/install-plugin-deps.sh` which reads the config and installs `apt.txt` packages + `post-install.sh` hooks for listed plugins.
|
|
486
|
+
|
|
487
|
+
The `with-plugins` Dockerfile stage is for rebuilding after adding third-party plugins:
|
|
488
|
+
|
|
489
|
+
```bash
|
|
490
|
+
docker build --target with-plugins -t camofox-browser .
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
The `with-plugins` stage re-runs `install-plugin-deps.sh` to pick up any new plugins added to `plugins/`.
|
|
494
|
+
|
|
495
|
+
### Code Separation Rules
|
|
496
|
+
|
|
497
|
+
Plugins follow the same separation conventions as core (see "Code Separation Conventions" above):
|
|
498
|
+
- **No `process.env` in plugin files that also have route handlers** -- read config from `ctx.config`
|
|
499
|
+
- **No `child_process` in plugin files that also have route handlers** -- spawn from a separate `lib/` module
|
|
500
|
+
|
|
501
|
+
### Custom Metrics
|
|
502
|
+
|
|
503
|
+
Plugins create Prometheus metrics via `ctx.createMetric()`. Returns a no-op stub when Prometheus is disabled -- no null checks needed.
|
|
504
|
+
|
|
505
|
+
```js
|
|
506
|
+
// In register(app, ctx):
|
|
507
|
+
const transcriptsTotal = await ctx.createMetric('counter', {
|
|
508
|
+
name: 'camofox_youtube_transcripts_total',
|
|
509
|
+
help: 'YouTube transcripts extracted',
|
|
510
|
+
labelNames: ['method'],
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
// Use anywhere -- works whether Prometheus is enabled or not
|
|
514
|
+
transcriptsTotal.labels('yt-dlp').inc();
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
Supported types: `'counter'`, `'histogram'`, `'gauge'`. Options are standard [prom-client](https://github.com/siimon/prom-client) options (`name`, `help`, `labelNames`, `buckets`, etc.). Metrics auto-register to the shared registry and appear on `/metrics`.
|
|
518
|
+
|
|
519
|
+
For advanced use, `ctx.metricsRegistry()` returns the raw prom-client `Registry` (or `null` when disabled).
|
|
520
|
+
|
|
521
|
+
### Example: YouTube Transcript Plugin
|
|
522
|
+
|
|
523
|
+
The YouTube plugin (`plugins/youtube/`) is the reference implementation. It extracts transcripts via yt-dlp with browser fallback, using `ctx` helpers for auth, logging, browser access, and concurrency control.
|
|
524
|
+
|
|
525
|
+
```
|
|
526
|
+
plugins/
|
|
527
|
+
youtube/
|
|
528
|
+
index.js # register(app, ctx) -- route handler + browser fallback
|
|
529
|
+
youtube.js # yt-dlp process management + transcript parsing
|
|
530
|
+
youtube.test.js # parser unit tests
|
|
531
|
+
apt.txt # python3-minimal (yt-dlp runtime dep)
|
|
532
|
+
post-install.sh # downloads yt-dlp binary
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
```js
|
|
536
|
+
// plugins/youtube/index.js (simplified)
|
|
537
|
+
import { detectYtDlp, hasYtDlp, ensureYtDlp, ytDlpTranscript } from './youtube.js';
|
|
538
|
+
import { classifyError } from '../../lib/request-utils.js';
|
|
539
|
+
|
|
540
|
+
export async function register(app, ctx) {
|
|
541
|
+
const { log, config, sessions, ensureBrowser, getSession,
|
|
542
|
+
withUserLimit, safePageClose, normalizeUserId,
|
|
543
|
+
validateUrl, safeError, buildProxyUrl, proxyPool,
|
|
544
|
+
failuresTotal } = ctx;
|
|
545
|
+
|
|
546
|
+
await detectYtDlp(log);
|
|
547
|
+
|
|
548
|
+
app.post('/youtube/transcript', ctx.auth(), async (req, res) => {
|
|
549
|
+
// ... validate URL, extract videoId, try yt-dlp then browser fallback
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
async function browserTranscript(reqId, url, videoId, lang) {
|
|
553
|
+
return await withUserLimit('__yt_transcript__', async () => {
|
|
554
|
+
await ensureBrowser();
|
|
555
|
+
const session = await getSession('__yt_transcript__');
|
|
556
|
+
const page = await session.context.newPage();
|
|
557
|
+
// ... intercept captions, parse transcript
|
|
558
|
+
await safePageClose(page);
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
Key patterns:
|
|
565
|
+
- **Auth**: `ctx.auth()` middleware on the route
|
|
566
|
+
- **Logging**: `ctx.log('info', ...)` -- never `console.log`
|
|
567
|
+
- **Browser access**: `ctx.ensureBrowser()` + `ctx.getSession()` for browser-backed features
|
|
568
|
+
- **Concurrency**: `ctx.withUserLimit()` to respect per-user limits
|
|
569
|
+
- **Metrics**: `ctx.failuresTotal.labels(...)` for core counters, `ctx.createMetric()` for custom
|
|
570
|
+
- **Code separation**: `child_process` in `youtube.js`, route handler in `index.js` -- separate files
|
|
571
|
+
- **System deps**: `apt.txt` lists packages installed via `scripts/install-plugin-deps.sh`
|
package/Dockerfile
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
FROM node:22-slim AS camofox-browser
|
|
2
|
+
|
|
3
|
+
# Pinned Camoufox version for reproducible builds
|
|
4
|
+
# Update these when upgrading Camoufox
|
|
5
|
+
ARG CAMOUFOX_VERSION=135.0.1
|
|
6
|
+
ARG CAMOUFOX_RELEASE=beta.24
|
|
7
|
+
ARG ARCH=x86_64
|
|
8
|
+
|
|
9
|
+
# Install dependencies for Camoufox (Firefox-based)
|
|
10
|
+
RUN apt-get update && apt-get install -y \
|
|
11
|
+
# Firefox dependencies
|
|
12
|
+
libgtk-3-0 \
|
|
13
|
+
libdbus-glib-1-2 \
|
|
14
|
+
libxt6 \
|
|
15
|
+
libasound2 \
|
|
16
|
+
libx11-xcb1 \
|
|
17
|
+
libxcomposite1 \
|
|
18
|
+
libxcursor1 \
|
|
19
|
+
libxdamage1 \
|
|
20
|
+
libxfixes3 \
|
|
21
|
+
libxi6 \
|
|
22
|
+
libxrandr2 \
|
|
23
|
+
libxrender1 \
|
|
24
|
+
libxss1 \
|
|
25
|
+
libxtst6 \
|
|
26
|
+
# Mesa OpenGL/EGL for WebGL support (software rendering via llvmpipe)
|
|
27
|
+
# Without these, Firefox cannot create WebGL contexts -- a major bot detection signal
|
|
28
|
+
libegl1-mesa \
|
|
29
|
+
libgl1-mesa-dri \
|
|
30
|
+
libgbm1 \
|
|
31
|
+
# Xvfb virtual display -- runs Camoufox as if on a real desktop (better anti-detection)
|
|
32
|
+
xvfb \
|
|
33
|
+
# Fonts
|
|
34
|
+
fonts-liberation \
|
|
35
|
+
fonts-noto-color-emoji \
|
|
36
|
+
fontconfig \
|
|
37
|
+
# Utils
|
|
38
|
+
ca-certificates \
|
|
39
|
+
curl \
|
|
40
|
+
unzip \
|
|
41
|
+
# yt-dlp runtime dependency
|
|
42
|
+
python3-minimal \
|
|
43
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
44
|
+
|
|
45
|
+
# Pre-bake Camoufox browser binary into image via bind mount (downloaded by Makefile)
|
|
46
|
+
# Note: unzip returns exit code 1 for warnings (Unicode filenames), so we use || true and verify
|
|
47
|
+
RUN --mount=type=bind,source=dist,target=/dist \
|
|
48
|
+
mkdir -p /root/.cache/camoufox \
|
|
49
|
+
&& (unzip -q /dist/camoufox-${ARCH}.zip -d /root/.cache/camoufox || true) \
|
|
50
|
+
&& chmod -R 755 /root/.cache/camoufox \
|
|
51
|
+
&& echo "{\"version\":\"${CAMOUFOX_VERSION}\",\"release\":\"${CAMOUFOX_RELEASE}\"}" > /root/.cache/camoufox/version.json \
|
|
52
|
+
&& test -f /root/.cache/camoufox/camoufox-bin && echo "Camoufox installed successfully"
|
|
53
|
+
|
|
54
|
+
# Install yt-dlp for YouTube transcript extraction (no browser needed)
|
|
55
|
+
RUN --mount=type=bind,source=dist,target=/dist \
|
|
56
|
+
install -m 755 /dist/yt-dlp-${ARCH} /usr/local/bin/yt-dlp
|
|
57
|
+
|
|
58
|
+
WORKDIR /app
|
|
59
|
+
|
|
60
|
+
COPY package.json ./
|
|
61
|
+
COPY scripts/ ./scripts/
|
|
62
|
+
RUN npm install --production
|
|
63
|
+
|
|
64
|
+
COPY server.js ./
|
|
65
|
+
COPY camofox.config.json ./
|
|
66
|
+
COPY lib/ ./lib/
|
|
67
|
+
COPY plugins/ ./plugins/
|
|
68
|
+
COPY scripts/ ./scripts/
|
|
69
|
+
|
|
70
|
+
# Install default plugin dependencies (apt packages + post-install hooks)
|
|
71
|
+
RUN scripts/install-plugin-deps.sh
|
|
72
|
+
|
|
73
|
+
ENV NODE_ENV=production
|
|
74
|
+
ENV CAMOFOX_PORT=9377
|
|
75
|
+
|
|
76
|
+
EXPOSE 9377
|
|
77
|
+
|
|
78
|
+
CMD ["sh", "-c", "node --max-old-space-size=${MAX_OLD_SPACE_SIZE:-128} server.js"]
|
|
79
|
+
|
|
80
|
+
# Optional: rebuild plugin deps after adding third-party plugins
|
|
81
|
+
# Usage: docker build --target with-plugins -t camofox-browser .
|
|
82
|
+
FROM camofox-browser AS with-plugins
|
|
83
|
+
COPY plugins/ ./plugins/
|
|
84
|
+
COPY camofox.config.json ./
|
|
85
|
+
COPY scripts/install-plugin-deps.sh /tmp/install-plugin-deps.sh
|
|
86
|
+
RUN /tmp/install-plugin-deps.sh && rm /tmp/install-plugin-deps.sh
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Jo, Inc
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|