agentmb 0.1.0 → 0.3.1

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.
Files changed (56) hide show
  1. package/README.md +841 -95
  2. package/dist/browser/actions.d.ts +295 -0
  3. package/dist/browser/actions.d.ts.map +1 -1
  4. package/dist/browser/actions.js +902 -6
  5. package/dist/browser/actions.js.map +1 -1
  6. package/dist/browser/manager.d.ts +102 -0
  7. package/dist/browser/manager.d.ts.map +1 -1
  8. package/dist/browser/manager.js +348 -4
  9. package/dist/browser/manager.js.map +1 -1
  10. package/dist/cli/client.d.ts +1 -0
  11. package/dist/cli/client.d.ts.map +1 -1
  12. package/dist/cli/client.js +21 -0
  13. package/dist/cli/client.js.map +1 -1
  14. package/dist/cli/commands/actions.d.ts.map +1 -1
  15. package/dist/cli/commands/actions.js +1023 -36
  16. package/dist/cli/commands/actions.js.map +1 -1
  17. package/dist/cli/commands/browser-launch.d.ts +7 -0
  18. package/dist/cli/commands/browser-launch.d.ts.map +1 -0
  19. package/dist/cli/commands/browser-launch.js +116 -0
  20. package/dist/cli/commands/browser-launch.js.map +1 -0
  21. package/dist/cli/commands/session.d.ts.map +1 -1
  22. package/dist/cli/commands/session.js +67 -4
  23. package/dist/cli/commands/session.js.map +1 -1
  24. package/dist/cli/index.js +3 -1
  25. package/dist/cli/index.js.map +1 -1
  26. package/dist/daemon/index.js +2 -2
  27. package/dist/daemon/index.js.map +1 -1
  28. package/dist/daemon/routes/actions.d.ts.map +1 -1
  29. package/dist/daemon/routes/actions.js +896 -20
  30. package/dist/daemon/routes/actions.js.map +1 -1
  31. package/dist/daemon/routes/browser_control.d.ts +12 -0
  32. package/dist/daemon/routes/browser_control.d.ts.map +1 -0
  33. package/dist/daemon/routes/browser_control.js +172 -0
  34. package/dist/daemon/routes/browser_control.js.map +1 -0
  35. package/dist/daemon/routes/interaction.d.ts +11 -0
  36. package/dist/daemon/routes/interaction.d.ts.map +1 -0
  37. package/dist/daemon/routes/interaction.js +176 -0
  38. package/dist/daemon/routes/interaction.js.map +1 -0
  39. package/dist/daemon/routes/sessions.d.ts.map +1 -1
  40. package/dist/daemon/routes/sessions.js +208 -3
  41. package/dist/daemon/routes/sessions.js.map +1 -1
  42. package/dist/daemon/routes/state.d.ts +11 -0
  43. package/dist/daemon/routes/state.d.ts.map +1 -0
  44. package/dist/daemon/routes/state.js +216 -0
  45. package/dist/daemon/routes/state.js.map +1 -0
  46. package/dist/daemon/server.d.ts.map +1 -1
  47. package/dist/daemon/server.js +7 -1
  48. package/dist/daemon/server.js.map +1 -1
  49. package/dist/daemon/session.d.ts +19 -0
  50. package/dist/daemon/session.d.ts.map +1 -1
  51. package/dist/daemon/session.js +13 -0
  52. package/dist/daemon/session.js.map +1 -1
  53. package/dist/policy/types.d.ts.map +1 -1
  54. package/dist/policy/types.js +14 -12
  55. package/dist/policy/types.js.map +1 -1
  56. package/package.json +1 -1
package/README.md CHANGED
@@ -4,28 +4,22 @@ Agent-ready local browser runtime for stable, auditable web automation.
4
4
 
5
5
  ## What It Does
6
6
 
7
- `agent-managed-browser` provides a persistent Chromium daemon with session management, CLI/Python SDK access, and human login handoff support. It is designed for coding/ops agents that need reproducible browser workflows instead of fragile one-off scripts.
7
+ `agent-managed-browser` runs a persistent **Chromium stable** browser daemon (via Playwright's bundled Chromium stable channel) with session management, structured audit logs, multi-modal element targeting, and human login handoff. It exposes a REST API, a CLI, and a Python SDK.
8
8
 
9
- ## Use Cases
10
-
11
- - **Agent web tasks**: Let Codex/Claude run navigation, click/fill, extraction, screenshot, and evaluation in a controlled runtime.
12
- - **Human-in-the-loop login**: Switch to headed mode for manual login, then return to headless automation with the same profile.
13
- - **E2E and CI verification**: Run isolated smoke/auth/handoff/cdp checks with configurable port and data dir.
14
- - **Local automation service**: Keep one daemon running and let multiple tools/agents reuse sessions safely.
15
-
16
- Local Chromium runtime for AI agents, with:
9
+ The browser engine is Chromium (Chrome-compatible). Firefox and WebKit are not supported. Node.js 20 LTS is the runtime baseline.
17
10
 
18
- - daemon API (`agentmb`)
19
- - CLI (`agentmb`)
20
- - Python SDK (`agentmb`)
11
+ Designed for coding and ops agents that need reproducible, inspectable browser workflows rather than fragile one-off scripts.
21
12
 
22
- This repo supports macOS, Linux, and Windows.
13
+ ## Use Cases
23
14
 
24
- ## Agent Skill
15
+ - **Agent web tasks**: navigate, click, fill, extract, screenshot, evaluate JavaScript, all via API or SDK.
16
+ - **Human-in-the-loop login**: switch to headed mode for manual login, then return to headless automation with the same profile and cookies intact.
17
+ - **E2E and CI verification**: run isolated smoke/auth/CDP/policy checks with configurable port and data dir.
18
+ - **Local automation service**: one daemon, multiple sessions, multiple agents reusing sessions safely.
25
19
 
26
- For Codex/Claude/AgentMB operation guidance (initialization, core commands, troubleshooting), see:
20
+ Supports macOS, Linux, and Windows.
27
21
 
28
- - [agentmb-operations-skill/SKILL.md](./agentmb-operations-skill/SKILL.md)
22
+ ---
29
23
 
30
24
  ## Quick Start
31
25
 
@@ -56,158 +50,768 @@ agentmb screenshot <session-id> -o ./shot.png
56
50
  agentmb stop
57
51
  ```
58
52
 
53
+ ---
54
+
55
+ ## Install
56
+
57
+ ### npm + pip (macOS / Linux)
58
+
59
+ ```bash
60
+ npm i -g agentmb
61
+ python3 -m pip install --user agentmb
62
+ agentmb --help
63
+ python3 -c "import agentmb; print(agentmb.__version__)"
64
+ ```
65
+
66
+ ### npm + pip (Windows PowerShell)
67
+
68
+ ```powershell
69
+ npm i -g agentmb
70
+ py -m pip install --user agentmb
71
+ agentmb --help
72
+ ```
73
+
74
+ Package roles:
75
+ - `npm` package: CLI + daemon runtime (Chromium via Playwright)
76
+ - `pip` package: Python SDK client (httpx + pydantic v2)
77
+
78
+ ---
79
+
59
80
  ## Python SDK
60
81
 
61
82
  ```bash
62
- pip install -e sdk/python
63
- python3 -c "from agentmb import BrowserClient; print('SDK OK')"
83
+ python3 -m pip install -e sdk/python
64
84
  ```
65
85
 
66
- ## Install By Platform
86
+ ```python
87
+ from agentmb import BrowserClient
67
88
 
68
- For full installation steps on all environments:
89
+ with BrowserClient(base_url="http://127.0.0.1:19315") as client:
90
+ sess = client.sessions.create(headless=True, profile="demo")
91
+ sess.navigate("https://example.com")
92
+ res = sess.screenshot()
93
+ res.save("shot.png")
94
+ sess.close()
95
+ ```
69
96
 
70
- - macOS
71
- - Linux (Ubuntu / Debian)
72
- - Windows (PowerShell / WSL2)
97
+ ---
73
98
 
74
- See [INSTALL.md](./INSTALL.md).
99
+ ## Locator Models
75
100
 
76
- ## Action Reference
101
+ Three targeting modes based on page stability and replay requirements.
102
+
103
+ ### 1) Selector Mode
77
104
 
78
- | Action | CLI command | Description |
105
+ Plain CSS selectors passed directly.
106
+
107
+ ```bash
108
+ agentmb click <session-id> "#submit"
109
+ agentmb fill <session-id> "#email" "name@example.com"
110
+ agentmb get <session-id> text "#title"
111
+ ```
112
+
113
+ Best for: stable pages where selectors are reliable.
114
+
115
+ ### 2) Element-ID Mode (`element-map`)
116
+
117
+ Step 1: scan the page, get stable `element_id` values.
118
+
119
+ ```bash
120
+ agentmb element-map <session-id>
121
+ agentmb element-map <session-id> --include-unlabeled # also surface icon-only elements
122
+ ```
123
+
124
+ Step 2: pass the ID to any action.
125
+
126
+ ```bash
127
+ agentmb click <session-id> e3 --element-id
128
+ agentmb fill <session-id> e5 "hello" --element-id
129
+ agentmb get <session-id> text e3 --element-id
130
+ agentmb assert <session-id> visible e3 --element-id
131
+ ```
132
+
133
+ `label` field per element is synthesized using a 7-level priority chain:
134
+
135
+ | Priority | Source | `label_source` value |
79
136
  |---|---|---|
80
- | navigate | `agentmb navigate <sess> <url>` | Navigate to URL |
81
- | screenshot | `agentmb screenshot <sess> -o out.png` | Capture screenshot |
82
- | eval | `agentmb eval <sess> <expr>` | Run JavaScript expression |
83
- | extract | `agentmb extract <sess> <selector>` | Extract text/attributes |
84
- | click | `agentmb click <sess> <selector>` | Click element |
85
- | fill | `agentmb fill <sess> <selector> <value>` | Fill form field |
86
- | type | `agentmb type <sess> <selector> <text>` | Type char-by-char |
87
- | press | `agentmb press <sess> <selector> <key>` | Press key / combo (e.g. `Enter`, `Control+a`) |
88
- | select | `agentmb select <sess> <selector> <val>` | Select `<option>` in a `<select>` |
89
- | hover | `agentmb hover <sess> <selector>` | Hover over element |
90
- | wait-selector | `agentmb wait-selector <sess> <selector>` | Wait for element state |
91
- | wait-url | `agentmb wait-url <sess> <pattern>` | Wait for URL pattern |
92
- | upload | `agentmb upload <sess> <selector> <file>` | Upload local file to file input |
93
- | download | `agentmb download <sess> <selector> -o out` | Click link and save download |
137
+ | 1 | `aria-label` attribute | `"aria-label"` |
138
+ | 2 | `title` attribute | `"title"` |
139
+ | 3 | `aria-labelledby` target text | `"aria-labelledby"` |
140
+ | 4 | SVG `<title>` / `<desc>` | `"svg-title"` |
141
+ | 5 | `innerText` (trimmed) | `"text"` |
142
+ | 6 | `placeholder` attribute | `"placeholder"` |
143
+ | 7 | Fallback (icon-only) | `"none"` / `"[tag @ x,y]"` |
144
+
145
+ Icon-only elements get `label_source="none"` by default; `--include-unlabeled` adds a `[tag @ x,y]` coordinate fallback.
146
+
147
+ Best for: selector drift, dynamic class names, and icon-heavy SPAs.
148
+
149
+ ### 3) Snapshot-Ref Mode (`snapshot-map` + `ref_id`)
150
+
151
+ Step 1: create a server-side snapshot.
152
+
153
+ ```bash
154
+ agentmb snapshot-map <session-id>
155
+ agentmb snapshot-map <session-id> --include-unlabeled
156
+ ```
157
+
158
+ Step 2: use the returned `ref_id` (`snap_XXXXXX:eN`) in API/SDK calls.
159
+
160
+ - `page_rev` is an integer counter returned with each snapshot; it increments on every main-frame navigation. Poll it directly to detect page changes without taking a full snapshot:
161
+
162
+ ```http
163
+ GET /api/v1/sessions/:id/page_rev
164
+ → { "status": "ok", "session_id": "...", "page_rev": 3, "url": "https://..." }
165
+ ```
166
+
167
+ ```python
168
+ rev = sess.page_rev() # PageRevResult with .page_rev, .url
169
+ ```
170
+
171
+ - If the page has navigated since the snapshot, using a stale `ref_id` returns `409 stale_ref` with a structured payload:
172
+
173
+ ```json
174
+ {
175
+ "error": "stale_ref: page changed",
176
+ "suggestions": ["call snapshot_map to get fresh ref_ids", "re-run your step with the new ref_id"]
177
+ }
178
+ ```
179
+
180
+ - Recovery: call `snapshot-map` again, retry with new `ref_id`.
181
+
182
+ Best for: deterministic replay and safe automation on changing pages.
183
+
184
+ ### Mode Selection Guide
185
+
186
+ | Page Type | Recommended Mode |
187
+ |---|---|
188
+ | Text-rich pages (docs, GitHub, HN) | `element-map` + `--element-id` |
189
+ | Icon/SVG-dense SPAs (social apps, dashboards) | CSS selector or `--include-unlabeled` |
190
+ | `contenteditable` / custom components | `eval getBoundingClientRect` + `click-at` |
191
+ | Image feeds (Unsplash, Pinterest) | `snapshot-map` (images have `alt` text) |
192
+
193
+ | Action | Approach |
194
+ |---|---|
195
+ | Search / navigation | Construct the URL directly |
196
+ | Click a labeled button | `element-map` eid or CSS selector |
197
+ | Click `contenteditable` | `click-at <sess> <x> <y>` (get coords via `bbox`) |
198
+ | Scroll SPA content area | Check `scrolled` + `scrollable_hint` in response; use `eval el.scrollBy()` if needed |
199
+ | File upload from disk | `upload <sess> <selector> <file>` (MIME inferred from extension) |
200
+ | File upload from URL | API: `POST /sessions/:id/upload_url` |
201
+ | Click JS-signed links | `click-at` to trigger a real click event |
202
+
203
+ ---
204
+
205
+ ## Action Reference
206
+
207
+ Use `agentmb --help` and `agentmb <command> --help` for full flags.
208
+
209
+ ### Navigation
210
+
211
+ | Command | Notes |
212
+ |---|---|
213
+ | `agentmb navigate <sess> <url>` | Navigate; `--wait-until load\|networkidle\|commit` |
214
+ | `agentmb back <sess>` / `forward <sess>` / `reload <sess>` | Browser history |
215
+ | `agentmb wait-url <sess> <pattern>` | Wait for URL match |
216
+ | `agentmb wait-load-state <sess>` | Wait for load state |
217
+ | `agentmb wait-function <sess> <expr>` | Wait for JS condition |
218
+ | `agentmb wait-text <sess> <text>` | Wait for text to appear |
219
+ | `agentmb wait-stable <sess>` | Network idle + DOM quiet + optional overlay clear |
220
+
221
+ ### Locator / Read / Assert
222
+
223
+ | Command | Notes |
224
+ |---|---|
225
+ | `agentmb element-map <sess>` | Scan; inject `element_id`; return `label` + `label_source` |
226
+ | `agentmb element-map <sess> --include-unlabeled` | Include icon-only elements; fallback label = `[tag @ x,y]` |
227
+ | `agentmb snapshot-map <sess>` | Server snapshot with `page_rev`; returns `ref_id` per element |
228
+ | `agentmb get <sess> <property> <selector-or-eid>` | Read `text/html/value/attr/count/box` |
229
+ | `agentmb assert <sess> <property> <selector-or-eid>` | Assert `visible/enabled/checked` |
230
+ | `agentmb extract <sess> <selector>` | Extract text/attributes as list |
231
+
232
+ `selector-or-eid` accepts a CSS selector, `--element-id` (element-map), or `--ref-id` (snapshot-map) on all commands.
233
+
234
+ ### Element Interaction
235
+
236
+ | Command | Notes |
237
+ |---|---|
238
+ | `agentmb click <sess> <selector-or-eid>` | Click; `contenteditable` supported; returns `422` with diagnostics + `recovery_hint` on failure |
239
+ | `agentmb dblclick <sess> <selector-or-eid>` | Double-click |
240
+ | `agentmb fill <sess> <selector-or-eid> <value>` | Fast fill (replaces value) |
241
+ | `agentmb type <sess> <selector-or-eid> <text>` | Type character by character; `--delay-ms <ms>` |
242
+ | `agentmb press <sess> <selector-or-eid> <key>` | Key / combo (`Enter`, `Tab`, `Control+a`) |
243
+ | `agentmb select <sess> <selector> <value...>` | Select `<option>` in `<select>` |
244
+ | `agentmb hover <sess> <selector-or-eid>` | Hover |
245
+ | `agentmb focus <sess> <selector-or-eid>` | Focus |
246
+ | `agentmb check <sess> <selector-or-eid>` / `uncheck` | Checkbox / radio |
247
+ | `agentmb drag <sess> <source> <target>` | Drag-and-drop; also accepts `--source-ref-id` / `--target-ref-id` |
248
+
249
+ **API/SDK — click advanced options:**
250
+
251
+ ```python
252
+ # executor: 'strict' (default) or 'auto_fallback'
253
+ # auto_fallback: tries Playwright click; if it times out due to overlay/intercept,
254
+ # falls back to page.mouse.click(center_x, center_y).
255
+ # When clicking inside an <iframe>, auto_fallback automatically adds the frame's
256
+ # page-level offset so coordinates land correctly.
257
+ # Response includes executed_via: 'high_level' | 'low_level'
258
+ sess.click(selector="#btn", executor="auto_fallback", timeout_ms=3000)
259
+
260
+ # stability: optional pre/post waits to handle animated UIs
261
+ sess.click(selector="#btn", stability={
262
+ "wait_before_ms": 200, # pause before the action
263
+ "wait_after_ms": 100, # pause after the action
264
+ "wait_dom_stable_ms": 500 # wait for DOM readyState before acting
265
+ })
266
+ ```
267
+
268
+ **API/SDK — fill humanization:**
269
+
270
+ ```python
271
+ # fill_strategy='type': types character-by-character (slower, more human-like)
272
+ # char_delay_ms: delay between keystrokes in ms (used with fill_strategy='type')
273
+ sess.fill(selector="#inp", value="hello", fill_strategy="type", char_delay_ms=30)
274
+ ```
275
+
276
+ ### Scroll and Feed
277
+
278
+ | Command | Notes |
279
+ |---|---|
280
+ | `agentmb scroll <sess> <selector-or-eid>` | Scroll element; structured response (see below) |
281
+ | `agentmb scroll-into-view <sess> <selector-or-eid>` | Scroll element into viewport |
282
+ | `agentmb scroll-until <sess>` | Scroll until stop condition (`--stop-selector`, `--stop-text`, `--max-scrolls`) |
283
+ | `agentmb load-more-until <sess> <btn-selector> <item-selector>` | Repeatedly click load-more |
284
+
285
+ **`scroll` response fields:**
286
+
287
+ ```json
288
+ {
289
+ "scrolled": true,
290
+ "warning": "element not scrollable — scrolled nearest scrollable ancestor",
291
+ "scrollable_hint": [
292
+ { "selector": "#feed", "tag": "div", "scrollHeight": 4200, "clientHeight": 600 },
293
+ ...
294
+ ]
295
+ }
296
+ ```
297
+
298
+ - `scrolled` — `true` if any scroll movement occurred
299
+ - `warning` — present when the target element itself is not scrollable and a fallback was used
300
+ - `scrollable_hint` — top-5 scrollable descendants ranked by `scrollHeight`; use these selectors in subsequent `scroll` calls when `scrolled=false`
301
+
302
+ **`scroll_until` / `load_more_until` response** includes `session_id` for chaining:
303
+
304
+ ```json
305
+ { "status": "ok", "session_id": "sess_...", "scrolls": 12, "stop_reason": "stop_text_found" }
306
+ ```
307
+
308
+ **API/SDK — scroll_until with step_delay:**
309
+
310
+ ```python
311
+ # step_delay_ms: wait between each scroll step (default = stall_ms)
312
+ sess.scroll_until(scroll_selector="#feed", direction="down",
313
+ stop_selector=".end", max_scrolls=20, step_delay_ms=150)
314
+ ```
315
+
316
+ ### Coordinate and Low-Level Input
317
+
318
+ | Command | Notes |
319
+ |---|---|
320
+ | `agentmb click-at <sess> <x> <y>` | Click absolute page coordinates |
321
+ | `agentmb wheel <sess> --dx --dy` | Low-level wheel event |
322
+ | `agentmb insert-text <sess> <text>` | Insert text into focused element (no keyboard simulation) |
323
+ | `agentmb bbox <sess> <selector-or-eid>` | Bounding box + center coordinates; accepts `--element-id` / `--ref-id` |
324
+ | `agentmb mouse-move <sess> [x] [y]` | Move mouse to absolute coordinates; or use `--selector`/`--element-id`/`--ref-id` to resolve element center |
325
+ | `agentmb mouse-down <sess>` / `mouse-up <sess>` | Mouse button press / release |
326
+ | `agentmb key-down <sess> <key>` / `key-up <sess> <key>` | Raw key press / release |
327
+
328
+ **API/SDK — smooth mouse movement:**
329
+
330
+ ```python
331
+ # Move by absolute coordinates with smooth interpolation
332
+ res = sess.mouse_move(x=400, y=300, steps=10)
333
+
334
+ # Move to an element center by selector / element_id / ref_id (x/y resolved server-side)
335
+ res = sess.mouse_move(selector="#submit-btn", steps=5)
336
+ res = sess.mouse_move(element_id="e3", steps=5)
337
+ res = sess.mouse_move(ref_id="snap_000001:e3")
338
+
339
+ # Response includes x, y, steps fields
340
+ print(res.x, res.y, res.steps)
341
+ ```
342
+
343
+ CLI equivalents:
344
+ ```bash
345
+ agentmb mouse-move <sess> 400 300 --steps 10
346
+ agentmb mouse-move <sess> --selector "#btn" --steps 5
347
+ agentmb mouse-move <sess> --element-id e3
348
+ agentmb mouse-move <sess> --ref-id snap_000001:e3
349
+ ```
350
+
351
+ ### Semantic Find (API / SDK)
352
+
353
+ Locate elements by Playwright semantic locators without knowing CSS selectors.
354
+
355
+ ```python
356
+ # query_type: 'role' | 'text' | 'label' | 'placeholder' | 'alt_text'
357
+ # Returns: found (bool), count, tag, text, bbox, nth
358
+ res = sess.find(query_type="role", query="button", name="Submit")
359
+ res = sess.find(query_type="text", query="Sign in", exact=True)
360
+ res = sess.find(query_type="placeholder", query="Search…")
361
+ res = sess.find(query_type="label", query="Email address")
362
+ res = sess.find(query_type="alt_text", query="Product photo", nth=2)
363
+ ```
364
+
365
+ | `query_type` | Playwright call |
366
+ |---|---|
367
+ | `role` | `page.getByRole(query, { name, exact })` |
368
+ | `text` | `page.getByText(query, { exact })` |
369
+ | `label` | `page.getByLabel(query, { exact })` |
370
+ | `placeholder` | `page.getByPlaceholder(query, { exact })` |
371
+ | `alt_text` | `page.getByAltText(query, { exact })` |
372
+
373
+ Returns `FindResult` with `found`, `count`, `nth`, `tag`, `text`, `bbox`.
374
+
375
+ ### Batch Execution — run_steps (API / SDK)
376
+
377
+ Execute a sequence of actions in a single request. Supports `stop_on_error`.
378
+
379
+ Each step's `params` accepts `selector`, `element_id`, or `ref_id` interchangeably for element targeting:
380
+
381
+ ```python
382
+ # First, take a snapshot to get ref_ids
383
+ snap = sess.snapshot_map()
384
+ btn_ref = next(e.ref_id for e in snap.elements if "Login" in (e.label or ""))
385
+
386
+ result = sess.run_steps([
387
+ {"action": "navigate", "params": {"url": "https://example.com"}},
388
+ {"action": "click", "params": {"ref_id": btn_ref}}, # ref_id from snapshot
389
+ {"action": "fill", "params": {"element_id": "e5", "value": "user@example.com"}}, # element_id
390
+ {"action": "fill", "params": {"selector": "#pass", "value": "secret"}}, # CSS selector
391
+ {"action": "press", "params": {"selector": "#pass", "key": "Enter"}},
392
+ {"action": "wait_for_selector","params": {"selector": ".dashboard"}},
393
+ {"action": "screenshot", "params": {"format": "png"}},
394
+ ], stop_on_error=True)
395
+
396
+ print(result.status) # 'ok' | 'partial' | 'failed'
397
+ print(result.completed_steps) # number of steps that succeeded
398
+ for step in result.results:
399
+ print(step.step, step.action, step.error)
400
+ ```
401
+
402
+ - A stale `ref_id` (page navigated since snapshot) returns a step-level error, not a request crash. Use `stop_on_error=False` to continue remaining steps.
403
+ - Supported actions: `navigate`, `click`, `fill`, `type`, `press`, `hover`, `scroll`, `wait_for_selector`, `wait_text`, `screenshot`, `eval`. Max 100 steps per call.
404
+
405
+ ### File Transfer
406
+
407
+ | Command | Notes |
408
+ |---|---|
409
+ | `agentmb upload <sess> <selector> <file>` | Upload file from disk; MIME auto-inferred from extension (`--mime-type` to override) |
410
+ | `agentmb download <sess> <selector-or-eid> -o <file>` | Trigger download; accepts `--element-id` / `--ref-id`; requires `--accept-downloads` on session |
411
+
412
+ **download guard**: sessions created without `accept_downloads=True` return `422 download_not_enabled`:
413
+
414
+ ```python
415
+ # Correct — enable at session creation time
416
+ sess = client.sessions.create(accept_downloads=True)
417
+ sess.download(selector="#dl-link", output_path="./file.pdf")
418
+
419
+ # download also accepts element_id / ref_id
420
+ sess.download(element_id="e7", output_path="./file.pdf")
421
+ sess.download(ref_id="snap_000001:e7", output_path="./file.pdf")
422
+ ```
423
+
424
+ ```bash
425
+ agentmb session new --accept-downloads
426
+ agentmb download <sess> "#dl-link" -o file.pdf
427
+ agentmb download <sess> e7 --element-id -o file.pdf
428
+ ```
429
+
430
+ **API/SDK — upload from URL:**
431
+
432
+ ```python
433
+ # Fetches the URL server-side (Node fetch), writes to temp file, uploads to file input.
434
+ res = sess.upload_url(
435
+ url="https://example.com/assets/photo.jpg",
436
+ selector="#file-input",
437
+ filename="photo.jpg", # optional; defaults to last URL path segment
438
+ mime_type="image/jpeg", # optional; defaults to application/octet-stream
439
+ )
440
+ # res.size_bytes, res.fetched_bytes, res.filename
441
+ ```
442
+
443
+ ### Session State (Cookie / Storage)
444
+
445
+ | Command | Notes |
446
+ |---|---|
447
+ | `agentmb cookie-list <sess>` | List all cookies |
448
+ | `agentmb cookie-clear <sess>` | Clear all cookies |
449
+ | `agentmb storage-export <sess> -o state.json` | Export Playwright storageState (cookies + origins) |
450
+ | `agentmb storage-import <sess> state.json` | Restore cookies from storageState; `origins_skipped` count returned |
451
+
452
+ **API/SDK — delete cookie by name:**
453
+
454
+ ```python
455
+ # Removes matching cookies, preserves the rest. domain is optional filter.
456
+ res = sess.delete_cookie("session_token")
457
+ res = sess.delete_cookie("tracker", domain=".example.com")
458
+ # res.removed, res.remaining
459
+ ```
460
+
461
+ ### Observability and Debug
462
+
463
+ | Command | Notes |
464
+ |---|---|
465
+ | `agentmb screenshot <sess> -o out.png` | Screenshot; `--full-page`, `--format png\|jpeg` |
466
+ | `agentmb annotated-screenshot <sess> --highlight <sel>` | Screenshot with colored element overlays |
467
+ | `agentmb eval <sess> <expr>` | Evaluate JavaScript; returns raw result |
468
+ | `agentmb console-log <sess>` | Browser console entries; `--tail N` |
469
+ | `agentmb page-errors <sess>` | Uncaught JS errors from the page |
470
+ | `agentmb dialogs <sess>` | Auto-dismissed dialog history (alert/confirm/prompt) |
471
+ | `agentmb logs <sess>` | Session audit log tail (all actions, policy events, CDP calls) |
472
+ | `agentmb trace start <sess>` / `trace stop <sess> -o trace.zip` | Playwright trace capture |
473
+
474
+ ### Browser Environment and Controls
475
+
476
+ | Command | Notes |
477
+ |---|---|
478
+ | `agentmb set-viewport <sess> <w> <h>` | Resize viewport |
479
+ | `agentmb clipboard-write <sess> <text>` / `clipboard-read <sess>` | Clipboard access |
480
+ | `agentmb policy <sess> [profile]` | Get or set safety policy profile |
481
+ | `agentmb cdp-ws <sess>` | Print browser-level CDP WebSocket URL |
482
+
483
+ **API/SDK — browser settings:**
484
+
485
+ ```python
486
+ # Returns viewport, user_agent, url, headless, profile for a session.
487
+ settings = sess.get_settings()
488
+ print(settings.viewport, settings.user_agent, settings.headless)
489
+ ```
490
+
491
+ ---
94
492
 
95
493
  ## Multi-Page Management
96
494
 
97
495
  ```bash
98
- agentmb pages list <session-id> # list all open tabs
99
- agentmb pages new <session-id> # open a new tab
496
+ agentmb pages list <session-id> # list all open tabs
497
+ agentmb pages new <session-id> # open a new tab
100
498
  agentmb pages switch <session-id> <page-id> # make a tab the active target
101
499
  agentmb pages close <session-id> <page-id> # close a tab (last tab protected)
102
500
  ```
103
501
 
502
+ ---
503
+
104
504
  ## Network Route Mocks
105
505
 
106
506
  ```bash
107
- agentmb route list <session-id> # list active mocks
507
+ agentmb route list <session-id>
108
508
  agentmb route add <session-id> "**/api/**" \
109
509
  --status 200 --body '{"ok":true}' \
110
- --content-type application/json # intercept requests
111
- agentmb route rm <session-id> "**/api/**" # remove a mock
510
+ --content-type application/json
511
+ agentmb route rm <session-id> "**/api/**"
112
512
  ```
113
513
 
114
- ## Playwright Trace Recording
514
+ Route mocks are applied at context level, so they persist across page navigations within the same session.
515
+
516
+ ---
517
+
518
+ ## Three Browser Running Modes
519
+
520
+ agentmb supports three distinct browser modes, differing in **which browser binary is used and how it is connected**.
521
+
522
+ | Mode | Browser | How Connected | Profile Persistence |
523
+ |---|---|---|---|
524
+ | **1. Managed Chromium** | Playwright bundled Chromium | agentmb spawns & owns | Persistent or ephemeral |
525
+ | **2. Managed Chrome Stable** | System Chrome / Edge | agentmb spawns & owns | Persistent or ephemeral |
526
+ | **3. CDP Attach** (Bold Mode) | Any running Chrome-compatible | agentmb attaches via CDP | Owned by external process |
527
+
528
+ ```
529
+ ┌─────────────────────────────────────────────────────────┐
530
+ │ agentmb daemon │
531
+ │ REST API POST /api/v1/sessions (+ preflight check) │
532
+ └───────────┬──────────────────┬──────────────┬───────────┘
533
+ │ │ │
534
+ launchPersistent() launchPersistent() connectOverCDP()
535
+ (bundled Chromium) (system Chrome/Edge) (external process)
536
+ │ │ │
537
+ ┌────────────▼────┐ ┌──────────▼────┐ ┌────▼──────────────┐
538
+ │ Mode 1 │ │ Mode 2 │ │ Mode 3 │
539
+ │ Managed │ │ Managed │ │ CDP Attach │
540
+ │ Chromium │ │ Chrome Stable │ │ (Bold Mode) │
541
+ │ │ │ / Edge │ │ launch_mode= │
542
+ │ profile=name │ │ browser_ │ │ attach │
543
+ │ or ephemeral=T │ │ channel=chrome│ │ │
544
+ └─────────────────┘ └───────────────┘ └───────────────────┘
545
+ ```
546
+
547
+ ### Mode 1: Managed Chromium (default)
548
+
549
+ agentmb spawns the **Playwright-bundled Chromium** binary. No system Chrome required. Works in headless (CI) and headed modes.
550
+
551
+ Within managed modes, choose a **profile strategy**:
552
+
553
+ **Agent Workspace** — named profile; cookies, localStorage, and browser state persist across runs:
554
+
555
+ ```python
556
+ sess = client.sessions.create(profile="gmail-account")
557
+ ```
115
558
 
116
559
  ```bash
117
- agentmb trace start <session-id> # start recording
118
- # ... do actions ...
119
- agentmb trace stop <session-id> -o trace.zip # save ZIP
120
- npx playwright show-trace trace.zip # open in Playwright UI
560
+ agentmb session new --profile gmail-account
121
561
  ```
122
562
 
123
- ## CDP WebSocket URL
563
+ **Pure Sandbox** ephemeral temp directory; all data is auto-deleted on `close()`:
564
+
565
+ ```python
566
+ sess = client.sessions.create(ephemeral=True)
567
+ ```
124
568
 
125
569
  ```bash
126
- agentmb cdp-ws <session-id> # print browser CDP WebSocket URL
570
+ agentmb session new --ephemeral
127
571
  ```
128
572
 
129
- ## Linux Headed Mode
573
+ ### Mode 2: Managed Chrome Stable
130
574
 
131
- Linux visual/headed mode requires Xvfb.
575
+ agentmb spawns a **system-installed Chrome or Edge** binary via Playwright. Requires Chrome Stable or Edge to be installed on the host. Both Agent Workspace and Pure Sandbox profile strategies apply.
576
+
577
+ ```python
578
+ sess = client.sessions.create(browser_channel="chrome") # system Chrome Stable
579
+ sess = client.sessions.create(browser_channel="msedge") # system Edge
580
+ sess = client.sessions.create(executable_path="/path/to/chrome") # custom binary path
581
+ ```
132
582
 
133
583
  ```bash
134
- sudo apt-get install -y xvfb
135
- bash scripts/xvfb-headed.sh
584
+ agentmb session new --browser-channel chrome
585
+ agentmb session new --browser-channel msedge
586
+ agentmb session new --executable-path /usr/bin/chromium-browser
136
587
  ```
137
588
 
138
- ## Verify
589
+ Valid `browser_channel` values: `chromium` (Playwright bundled, default), `chrome` (system Chrome Stable), `msedge`. `browser_channel` and `executable_path` are mutually exclusive.
590
+
591
+ ### Mode 3: CDP Attach (Bold Mode)
592
+
593
+ agentmb **attaches to an already-running Chrome** process via the Chrome DevTools Protocol. The remote browser is **not terminated** on `close()` — only the Playwright connection is dropped. This mode exposes lower `navigator.webdriver` fingerprint than managed modes and supports extensions.
594
+
595
+ Three profile variants are available, depending on which `--user-data-dir` Chrome is launched with:
596
+
597
+ | Variant | `--user-data-dir` | State | Typical Use |
598
+ |---|---|---|---|
599
+ | **A. Sandbox** | temp dir (auto) | ephemeral | clean-slate CI runs, throwaway sessions |
600
+ | **B. Dedicated Profile** | custom persistent dir | persistent, isolated | automation account, persistent login |
601
+ | **C. User Chrome** | your real Chrome profile | inherits all cookies & extensions | leverage personal login state |
602
+
603
+ #### Variant A: Sandbox (ephemeral temp dir)
604
+
605
+ `agentmb browser-launch` creates a fresh temp profile automatically. Clean slate — no cookies, no extensions.
606
+
607
+ ```bash
608
+ agentmb browser-launch --port 9222
609
+ # → launches Chrome with --user-data-dir=/tmp/agentmb-cdp-9222 (temp, ephemeral)
610
+ # → CDP URL: http://127.0.0.1:9222
611
+ ```
612
+
613
+ ```python
614
+ sess = client.sessions.create(launch_mode="attach", cdp_url="http://127.0.0.1:9222")
615
+ sess.navigate("https://example.com")
616
+ sess.close() # disconnects only — Chrome stays alive
617
+ ```
618
+
619
+ #### Variant B: Dedicated Profile (isolated persistent profile)
620
+
621
+ Pass a fixed `--user-data-dir` to Chrome. State (cookies, localStorage) persists across restarts. Completely isolated from your personal Chrome.
139
622
 
140
623
  ```bash
141
- bash scripts/verify.sh
624
+ # macOS / Linux
625
+ /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
626
+ --remote-debugging-port=9222 \
627
+ --user-data-dir="$HOME/.agentmb-profiles/my-automation-profile" \
628
+ --no-first-run --no-default-browser-check
629
+
630
+ # Windows
631
+ "C:\Program Files\Google\Chrome\Application\chrome.exe" ^
632
+ --remote-debugging-port=9222 ^
633
+ --user-data-dir="%APPDATA%\agentmb-profiles\my-automation-profile"
142
634
  ```
143
635
 
144
- ## npm Release Setup
636
+ ```python
637
+ sess = client.sessions.create(launch_mode="attach", cdp_url="http://127.0.0.1:9222")
638
+ ```
639
+
640
+ #### Variant C: User Chrome (reuse your real Chrome profile)
641
+
642
+ Point Chrome at your existing user profile to inherit all logged-in sessions, saved passwords, and installed extensions. **Chrome must not already be running with that profile** when you launch with remote debugging.
145
643
 
146
644
  ```bash
147
- # login once
148
- npm login
149
- npm whoami
645
+ # macOS — close Chrome first, then:
646
+ /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
647
+ --remote-debugging-port=9222 \
648
+ --user-data-dir="$HOME/Library/Application Support/Google/Chrome"
649
+
650
+ # Linux
651
+ google-chrome --remote-debugging-port=9222 \
652
+ --user-data-dir="$HOME/.config/google-chrome"
653
+
654
+ # Windows
655
+ "C:\Program Files\Google\Chrome\Application\chrome.exe" ^
656
+ --remote-debugging-port=9222 ^
657
+ --user-data-dir="%LOCALAPPDATA%\Google\Chrome\User Data"
658
+ ```
659
+
660
+ ```python
661
+ sess = client.sessions.create(launch_mode="attach", cdp_url="http://127.0.0.1:9222")
662
+ # → all cookies, extensions, and login state from your personal Chrome are available
663
+ ```
664
+
665
+ **Warning**: actions performed via agentmb will affect your real Chrome profile (cookies written, history created, etc.). Use Variant B when in doubt.
150
666
 
151
- # check package payload before publish
152
- npm run pack:check
667
+ ---
153
668
 
154
- # publish from repo root
155
- npm publish
669
+ Attach a session (all variants):
670
+
671
+ ```bash
672
+ agentmb session new --launch-mode attach --cdp-url http://127.0.0.1:9222
156
673
  ```
157
674
 
158
- If your global npm cache has permission issues, this repo uses project-local cache (`.npm-cache`) via `.npmrc`.
675
+ **Note**: `launch_mode=attach` is incompatible with `browser_channel` and `executable_path` (preflight returns `400`). CDP attach gives agentmb control over **all tabs** in the connected browser.
159
676
 
160
- ## Environment Variables
677
+ ### Session Seal
678
+
679
+ Mark a session as sealed to prevent accidental deletion:
680
+
681
+ ```python
682
+ sess.seal()
683
+ # Now sess.close() / DELETE returns 423 session_sealed
684
+ ```
685
+
686
+ ```bash
687
+ agentmb session seal <session-id>
688
+ agentmb session rm <session-id> # → error: session is sealed
689
+ ```
690
+
691
+ ### Preflight Validation
692
+
693
+ The `POST /api/v1/sessions` endpoint validates parameters before launching and returns `400 preflight_failed` for:
694
+ - `browser_channel` + `executable_path` used together (mutually exclusive)
695
+ - `browser_channel` not in `['chromium', 'chrome', 'msedge']`
696
+ - `launch_mode=attach` without `cdp_url`
697
+ - `cdp_url` with invalid URL format
698
+ - `launch_mode=attach` combined with `browser_channel` or `executable_path`
699
+
700
+ ---
701
+
702
+ ## CDP Access
703
+
704
+ agentmb uses Chromium stable as the browser engine. The protocol exposed is the full **Chrome DevTools Protocol (CDP)** as implemented in Chromium/Chrome. Three distinct access modes are provided.
705
+
706
+ ### 1. CDP Command Passthrough (REST)
707
+
708
+ Send any DevTools Protocol method to the session's CDP session.
709
+
710
+ ```http
711
+ GET /api/v1/sessions/:id/cdp → session CDP info
712
+ POST /api/v1/sessions/:id/cdp
713
+ {"method": "Page.captureScreenshot", "params": {"format": "png"}}
714
+ ```
715
+
716
+ All CDP calls are written to the session audit log (`type="cdp"`, `method`, `session_id`, `purpose`, `operator`). Error responses are sanitized (stack frames and internal paths stripped before logging).
717
+
718
+ ### 2. CDP WebSocket Passthrough
719
+
720
+ Returns the browser-level `ws://` endpoint. Connect Puppeteer, Chrome DevTools, or any CDP client directly.
721
+
722
+ ```bash
723
+ agentmb cdp-ws <session-id>
724
+ # → ws://127.0.0.1:NNNN/devtools/browser/...
725
+ ```
726
+
727
+ ```python
728
+ ws_url = sess.cdp_ws_url()
729
+ # connect with puppeteer, pyppeteer, or raw websocket
730
+ ```
731
+
732
+ Note: The WebSocket URL is for the full browser process (not per-page). It is only available when the daemon uses a non-persistent browser launch. Auth-gated: requires the same `X-API-Token` as REST endpoints when auth is enabled.
733
+
734
+ ### 3. CDP Network Emulation
735
+
736
+ Apply network throttling or offline mode via an internal CDP session attached per-session. Does not require external CDP tooling.
737
+
738
+ ```bash
739
+ agentmb set-network <session-id> \
740
+ --latency-ms 200 \
741
+ --download-kbps 512 \
742
+ --upload-kbps 256
743
+
744
+ agentmb set-network <session-id> --offline # full offline mode
745
+ agentmb reset-network <session-id> # restore normal conditions
746
+ ```
747
+
748
+ ```python
749
+ sess.network_conditions(offline=False, latency_ms=200,
750
+ download_kbps=512, upload_kbps=256)
751
+ ```
752
+
753
+ ---
754
+
755
+ ## Profile Management (API / SDK)
756
+
757
+ Profiles persist cookies, localStorage, and browser state between sessions.
758
+
759
+ ```python
760
+ # List all profiles on disk
761
+ result = client.list_profiles()
762
+ for p in result.profiles:
763
+ print(p.name, p.path, p.last_used)
764
+
765
+ # Reset a profile (wipes data dir and recreates empty directory)
766
+ # Returns 409 if a live session is currently using the profile.
767
+ result = client.reset_profile("demo")
768
+ # result.status == "ok"
769
+ ```
770
+
771
+ REST:
772
+ ```
773
+ GET /api/v1/profiles → ProfileListResult
774
+ POST /api/v1/profiles/:name/reset → ProfileResetResult
775
+ ```
161
776
 
162
- Common runtime env vars:
777
+ Profile directories are stored under `AGENTMB_DATA_DIR/profiles/<name>/`.
163
778
 
164
- - `AGENTMB_PORT` (default `19315`)
165
- - `AGENTMB_DATA_DIR` (default `~/.agentmb`)
166
- - `AGENTMB_API_TOKEN` (optional API auth)
167
- - `AGENTMB_ENCRYPTION_KEY` (optional AES-256-GCM profile encryption key, 32 bytes as base64 or hex)
168
- - `AGENTMB_LOG_LEVEL` (default `info`)
169
- - `AGENTMB_POLICY_PROFILE` (default `safe`) — daemon-wide default safety policy profile
779
+ ---
170
780
 
171
781
  ## Safety Execution Policy
172
782
 
173
- agentmb enforces a configurable **safety execution policy** that throttles actions, enforces per-domain rate limits, and blocks sensitive actions (e.g. form submissions, file uploads) unless explicitly permitted.
783
+ Rate limiting and action guardrails enforced per-session, per-domain.
174
784
 
175
785
  ### Profiles
176
786
 
177
787
  | Profile | Min interval | Jitter | Max actions/min | Sensitive actions |
178
788
  |---|---|---|---|---|
179
- | `safe` | 1500 ms | 300–800 ms | 8 | blocked by default |
789
+ | `safe` | 1500 ms | 300–800 ms | 8 | blocked (HTTP 403) |
180
790
  | `permissive` | 200 ms | 0–100 ms | 60 | allowed |
181
791
  | `disabled` | 0 ms | 0 ms | unlimited | allowed |
182
792
 
183
- Set the daemon-wide default via env var:
793
+ Set daemon-wide default via environment variable:
794
+
184
795
  ```bash
185
796
  AGENTMB_POLICY_PROFILE=disabled node dist/daemon/index.js # CI / trusted automation
186
- AGENTMB_POLICY_PROFILE=safe node dist/daemon/index.js # social-media / sensitive workflows
797
+ AGENTMB_POLICY_PROFILE=safe node dist/daemon/index.js # untrusted / social-media flows
187
798
  ```
188
799
 
189
- ### Per-session override (CLI)
800
+ ### Per-session override
190
801
 
191
802
  ```bash
192
- agentmb policy <session-id> # get current policy
193
- agentmb policy <session-id> safe # switch to safe profile
194
- agentmb policy <session-id> permissive # switch to permissive
803
+ agentmb policy <session-id> # get current profile
804
+ agentmb policy <session-id> safe # switch to safe
805
+ agentmb policy <session-id> permissive # switch to permissive
195
806
  agentmb policy <session-id> safe --allow-sensitive # safe + allow sensitive actions
196
807
  ```
197
808
 
198
- ### Per-session override (Python SDK)
199
-
200
809
  ```python
201
- from agentmb import BrowserClient
202
-
203
- with BrowserClient() as client:
204
- sess = client.sessions.create()
205
- policy = sess.set_policy("safe", allow_sensitive_actions=False)
206
- print(policy.max_retries_per_domain) # 3
207
- current = sess.get_policy()
810
+ sess.set_policy("safe", allow_sensitive_actions=False)
811
+ info = sess.get_policy() # → PolicyInfo
208
812
  ```
209
813
 
210
- ### Audit logs
814
+ ### Audit log (policy events)
211
815
 
212
816
  All policy events (`throttle`, `jitter`, `cooldown`, `deny`, `retry`) are written to the session audit log with `type="policy"`.
213
817
 
@@ -215,14 +819,156 @@ All policy events (`throttle`, `jitter`, `cooldown`, `deny`, `retry`) are writte
215
819
  agentmb logs <session-id> # shows policy events inline
216
820
  ```
217
821
 
218
- ### Sensitive actions
822
+ ### Sensitive action guard
219
823
 
220
- Mark any action as sensitive by passing `"sensitive": true` in the request body. With `safe` profile and `allow_sensitive_actions=false`, the request returns HTTP 403:
824
+ Pass `"sensitive": true` in any request body to mark it as sensitive. With `safe` profile and `allow_sensitive_actions=false`:
221
825
 
222
826
  ```json
223
827
  { "error": "sensitive action blocked by policy", "policy_event": "deny" }
224
828
  ```
225
829
 
830
+ HTTP status: `403`.
831
+
832
+ ---
833
+
834
+ ## Security
835
+
836
+ ### API Token Authentication
837
+
838
+ All endpoints require `X-API-Token` or `Authorization: Bearer <token>` when `AGENTMB_API_TOKEN` is set.
839
+
840
+ ```bash
841
+ export AGENTMB_API_TOKEN="my-secret-token"
842
+ ```
843
+
844
+ Requests without a valid token return `401 Unauthorized`. CDP REST and WebSocket endpoints are subject to the same token check.
845
+
846
+ ### Profile Encryption
847
+
848
+ Browser profiles (cookies, storage) are encrypted at rest using AES-256-GCM when `AGENTMB_ENCRYPTION_KEY` is set.
849
+
850
+ ```bash
851
+ # 32-byte key, base64 or hex encoded
852
+ export AGENTMB_ENCRYPTION_KEY="$(openssl rand -base64 32)"
853
+ ```
854
+
855
+ Profiles written without a key cannot be read with one and vice versa.
856
+
857
+ ### Input Validation (Preflight)
858
+
859
+ Every action route runs preflight checks before execution:
860
+
861
+ - `timeout_ms`: must be in range `[50, 60000]` ms. Out-of-range values return `400 preflight_failed` with `{ field, constraint, value }`.
862
+ - `fill` value: max 100,000 characters. Longer values return `400 preflight_failed`.
863
+
864
+ ### Error Diagnostics and Recovery Hints
865
+
866
+ When an action fails (element not found, timeout, detached context, overlay intercept), the route returns `422` with a structured diagnostic payload:
867
+
868
+ ```json
869
+ {
870
+ "error": "Timeout 3000ms exceeded.",
871
+ "url": "https://example.com",
872
+ "readyState": "complete",
873
+ "recovery_hint": "Increase timeout_ms or add stability.wait_before_ms; ensure element is visible before acting"
874
+ }
875
+ ```
876
+
877
+ `recovery_hint` categories:
878
+ - **Timeout / waiting for**: increase `timeout_ms` or add `stability.wait_before_ms`; verify element visibility
879
+ - **Target closed / detached**: page navigated or element removed; re-navigate or call `snapshot_map` again
880
+ - **Not found / no element**: check selector; use `snapshot_map` to verify element exists on current page
881
+ - **Intercept / overlap / obscured**: element covered by overlay; try `executor=auto_fallback` or scroll into view first
882
+
883
+ ### Audit Logging
884
+
885
+ Every action, CDP call, and policy event is appended to a per-session JSONL audit log:
886
+
887
+ ```json
888
+ {
889
+ "ts": "2026-02-28T10:00:01.234Z",
890
+ "v": 1,
891
+ "session_id": "s_abc123",
892
+ "action_id": "act_xyz",
893
+ "type": "action",
894
+ "action": "click",
895
+ "url": "https://example.com",
896
+ "selector": "#submit",
897
+ "result": { "status": "ok", "duration_ms": 142 },
898
+ "purpose": "submit search form",
899
+ "operator": "codex-agent"
900
+ }
901
+ ```
902
+
903
+ Fields: `purpose` (why), `operator` (who/what). Set via request body or `X-Operator` header.
904
+
905
+ ```bash
906
+ agentmb logs <session-id> --tail 50
907
+ ```
908
+
909
+ ---
910
+
911
+ ## Human Login Handoff
912
+
913
+ Switch a session to headed (visible) mode, log in manually, then return to headless automation with the same cookies and storage.
914
+
915
+ ```bash
916
+ agentmb login <session-id>
917
+ # → browser window opens
918
+ # → log in manually
919
+ # → press Enter in terminal to return to headless mode
920
+ ```
921
+
922
+ ---
923
+
924
+ ## Linux Headed Mode
925
+
926
+ Linux visual/headed mode requires Xvfb:
927
+
928
+ ```bash
929
+ sudo apt-get install -y xvfb
930
+ bash scripts/xvfb-headed.sh
931
+ ```
932
+
933
+ ---
934
+
935
+ ## Playwright Trace Recording
936
+
937
+ ```bash
938
+ agentmb trace start <session-id>
939
+ # ... perform actions ...
940
+ agentmb trace stop <session-id> -o trace.zip
941
+ npx playwright show-trace trace.zip
942
+ ```
943
+
944
+ ---
945
+
946
+ ## Verify
947
+
948
+ Runs: build → daemon start → 19 pytest suites → daemon stop. Requires daemon to not be running on the configured port.
949
+
950
+ ```bash
951
+ bash scripts/verify.sh # uses default port 19315
952
+ AGENTMB_PORT=19320 bash scripts/verify.sh
953
+ ```
954
+
955
+ Expected output: `ALL GATES PASSED (24/24)`.
956
+
957
+ ---
958
+
959
+ ## Environment Variables
960
+
961
+ | Variable | Default | Purpose |
962
+ |---|---|---|
963
+ | `AGENTMB_PORT` | `19315` | Daemon HTTP port |
964
+ | `AGENTMB_DATA_DIR` | `~/.agentmb` | Profiles and logs directory |
965
+ | `AGENTMB_API_TOKEN` | _(none)_ | Require this token on all requests |
966
+ | `AGENTMB_ENCRYPTION_KEY` | _(none)_ | AES-256-GCM key for profile encryption (32 bytes, base64 or hex) |
967
+ | `AGENTMB_LOG_LEVEL` | `info` | Daemon log verbosity |
968
+ | `AGENTMB_POLICY_PROFILE` | `safe` | Default safety policy profile (`safe\|permissive\|disabled`) |
969
+
970
+ ---
971
+
226
972
  ## License
227
973
 
228
974
  MIT