Skip to content

feat: Mini Browser — a live Chromium that agents drive and users can take over - #483

Open
korivi-CraftOS wants to merge 8 commits into
mainfrom
feature/mini-browser
Open

korivi-CraftOS wants to merge 8 commits into
mainfrom
feature/mini-browser

Conversation

@korivi-CraftOS

@korivi-CraftOS korivi-CraftOS commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

Adds the Mini Browser: a real Chromium that CraftBot's agents drive while the user watches live in a new sidebar page, and can take over at any moment. It replaces the earlier "Web Agent" prototype, rebuilt to fit current main (sessions, i18n, provisioning, WebSocket lanes).

Commits: the first imports the old prototype onto main, the next two replace it, the fourth and the last two address the review comments, and the fifth and sixth fix a retype loop on Threads and let pages play sound. Review the branch as a whole, not commit by commit.

What users get

  • A Mini Browser item in the sidebar. The page has a live view, tabs, a URL bar, saved passwords, and a chat with its own session where the browser tools are preloaded.
  • The agent moves the mouse and types the way a person does, so the user can follow along. Clicking into the page takes control, which pauses the agent on that tab immediately, even mid-typing. Hand back resumes it.
  • Saved logins: the agent signs in through an encrypted password vault and never sees the password.
  • Pages can play sound (videos, calls, notifications) through the computer's speakers. Turn it off with mini_browser.sound: false.
  • Post boxes (Threads, X and similar): addresses are entered whole, the box is read back after typing, the page view shows the whole post (never half a link), and the agent is stopped from deleting and retyping the same post in a loop. Checked against the real Lexical editor with its auto-link plugin.
  • The page is fully translated (all 7 locales), and the live view accepts IME input (Japanese etc.), paste and shortcuts.

How it works

  • app/mini_browser/ is a new package that is import-light: it never imports Playwright at import time.
    • Threading: a dedicated mini-browser thread with its own event loop owns Playwright. Every caller marshals in via host.call(), so it works from the main loop and from sub-agents, which run on their own loops.
    • Tabs: each agent session gets its own tab in its own Chromium window, keyed on _session_id. Without separate windows, background tabs are throttled to about 1 s per input event.
    • Live view: CDP screencast of the viewed tab, streamed only while a UI tab is subscribed. Frames are latest-wins and never queued.
    • Ad blocking: CDP Network.setBlockedURLs. context.route is not used because it disables the HTTP cache.
    • Blocking CraftBot's own UI: the UI origins are blocked browser-wide, however the address is spelled.
    • Resilience: crash and driver-death recovery, a timeout on every Playwright call, idle shutdown, a cross-process profile lock, and a clean close during launch.
  • Agent actions: app/data/action/mini_browser_actions.py holds 13 thin mini_browser_* stubs: navigate, read, click, type, press_key, select_option, hover, scroll, wait, upload_file, login, screenshot and tabs. Every result carries a fresh, compact page observation, so agents need fewer LLM round-trips.
  • WebSocket protocol: mini_browser_* messages are handled in app/mini_browser/ws.py. Navigation, input, control and the vault each get their own lane, so a frozen page can't block "Take control".

Every kind of agent can use it

  • Main and chat sessions, the dedicated Mini Browser chat (new session type mini_browser), scheduled runs, Agent App trigger runs, and a new browser_agent sub-agent type.
  • Agents discover it through:
    • the catalog description;
    • a when-to-use rule in the action prompt, shown only when the browser can actually run;
    • an enabled mini-browser skill;
    • AGENT.md.
  • Stop cancels in-flight browser work for the session and its sub-agents.
  • Tab privacy: agents see only their own tabs' URLs. They can't take over a tab the user opened unless it is blank, or the user is viewing it and the agent switches to it explicitly.

Password vault

  • Fernet-encrypted, with the key wrapped by Windows DPAPI, stored in .credentials/ (0700 dir, 0600 files).
  • Writes are atomic. An unreadable vault is never overwritten: it can only be reset explicitly, which keeps a backup.
  • Autofill happens only on the exact site the login was saved for (with or without www.), over https or on localhost, and only for the current page's origin.
  • Passwords never reach the model, the logs or the UI, including their URL-, HTML- and JSON-encoded forms.

Changes outside app/mini_browser/ (please review closely)

File Change
app/agent_base.py Lifecycle hooks: run state, Stop, chat deletion, sub-agent removal, shutdown, settings hot-reload. A full /reset now recreates the streams of surviving sessions. The Mini Browser chat can be cleared but not deleted.
run.py, app/state_sync.py Managed installs now receive newly shipped action files and skills on upgrade, using a manifest. User edits are never overwritten and deleted files never come back. This also fixes the gap for any future new action.
agent_core/core/impl/action/router.py Optional prompt_filter hook, used to hide the Mini Browser rule when the browser is unavailable.
agent_core/core/session/session.py, impl/session/manager.py New session type mini_browser.
agent_core/core/prompts/action.py, context.py When-to-use rule for websites that must be used, not just read.
app/internal_action_interface.py add_action_sets matches names loosely (-/_, case) and reports unknown sets with hints instead of silently succeeding.
app/ui_layer/adapters/browser_adapter.py, ws_lanes.py One delegation branch, a strict send-to-requester helper, start/stop hooks, new lanes.
app/config.py, app/errors/codebook.py mini_browser settings section and the Mini Browser error codes.
skills/playwright-mcp/SKILL.md Its bare browser_* tool names collided with the new actions, so it now uses the real mcp_playwright-mcp_* names and points user-facing browsing to the Mini Browser.
.gitignore, .dockerignore Profile and lock-file patterns, so secrets and cookies never get committed or baked into images.
Frontend outside the page Route and sidebar item; miniBrowser slice; resource; UI state; ToastContext gains optional action buttons; ActivityBlocks previews for mini_browser_* actions; a small onboarding fix so long error text wraps.

Testing

  • Full Python suite: 2452 passed, 0 failed. Excluded: the 3 test files that already fail to collect on main (tests/llm/golden, test_installer_stream_routing, test_prompt_profile) and the stale macOS shortcut test in test_craftbot_service.
  • 16 new or extended test files, many driving real headless Chromium against local test servers. They cover:
    • vault matching against lookalike domains, and password masking;
    • stale element ids;
    • concurrent owners from several loops and threads;
    • close during launch, crash recovery, and take-control mid-typing;
    • downloads, popups and URL policy;
    • the WebSocket protocol and lanes;
    • the upgrade sync;
    • every action stub executed exactly as the action executor runs it;
    • the review fixes: no agent input after take-control, tab limits under concurrent opens, screenshot masking, download names, copy/cut, and both settings readers agreeing.
  • Browser tests skip only when Playwright or Chromium is not installed; any other startup failure fails them. CRAFTBOT_REQUIRE_BROWSER_TESTS=1 makes a missing browser a failure too.
  • ruff format --check . and ruff check . are clean (ruff 0.16.1, same as CI), and compileall passes.
  • Frontend: tsc --noEmit reports 0 errors, the production vite build succeeds, and the i18n keys match across all 7 locales.
  • An internal multi-agent review (concurrency, security, agent effectiveness, platform integration, frontend, end-to-end) confirmed 53 issues. All are fixed with regression tests; the residual edge cases are listed below.
  • No CI workflow runs on PRs to main for these paths, so all of the above was run locally on Windows (Python 3.10, Playwright 1.58 / Chromium 145).

How to try it

  1. Check out the branch and start CraftBot as usual (run.py rebuilds the frontend).
  2. Click Mini Browser in the sidebar and enter a URL, or ask its chat for a web task, e.g. "find X on Y and add it to the cart".
  3. Click into the page to take control, then press Hand back.
  4. Save a login under Passwords and ask the agent to sign in.

If Chromium isn't installed (it is an optional provisioning stage), the page offers an Install browser button.

Known limits / follow-ups

  • Existing managed installs receive new built-in files, but not updates to built-in files they already have (e.g. web_fetch.py). This predates this PR.
  • scripts/package_source.py leaves out the AGENT.md template. This also predates this PR.
  • The Docker image ships without Chromium, so the prompt rule is hidden there. Enabling it would add about 500 MB.
  • Downloads get the "from the internet" marker (Mark-of-the-Web) on Windows only.
  • A login saved for example.com no longer fills on other subdomains. A per-login "also use on subdomains" option would need a UI decision.
  • One rare residual: quitting the app during a launch that hangs for more than 90 s leaves the Playwright driver until the process exits.
  • Only Windows was exercised end to end. Headed mode and the macOS/Linux profile lock are implemented but untested.
  • Follow-ups from the review:
    • give the WebSocket layer a small typed adapter interface, injected, instead of reaching into the adapter's private members;
    • split BrowserCore's browser lifecycle (launch, recovery, teardown) from tab registry and ownership;
    • a CI job that installs Chromium and runs the browser tests with CRAFTBOT_REQUIRE_BROWSER_TESTS=1.

Squash of the original korivi-Exp1 prototype (web agent V1/V2, password
vault, onboarding error wrap) applied on top of current main. Conflicts
resolved against main's newer browser adapter and sidebar; the local-only
slow_mode and profile-interview workarounds were dropped.

The page still targets the old task API; it is adapted in the next commits.
A real Chromium that agents drive and the user watches live, usable by every
kind of agent (main and chat sessions, a dedicated Mini Browser chat,
scheduled runs, Agent App trigger runs and the new browser_agent sub-agent).

Engine (app/mini_browser):
- dedicated "mini-browser" thread + event loop owns Playwright; callers on any
  loop marshal in (fixes sub-agents, which run on their own loops)
- one tab per agent session with per-tab locks, owner labels, take-control,
  follow-view, crash and driver-death recovery, idle shutdown, profile lock
- live view via CDP screencast of the viewed tab, only while a UI tab is
  subscribed, latest-frame-wins delivery
- ad blocking via CDP Network.setBlockedURLs (context.route disabled the HTTP
  cache); CraftBot's own UI origins blocked browser-wide
- human-like mouse paths, typing rhythm and scrolling
- 13 mini_browser_* actions; every result carries a fresh page observation

Password vault: Fernet + Windows DPAPI in .credentials/, atomic writes, no
silent data loss, strict origin matching; autofill only for the current
page's origin and never exposed to the model, logs or UI.

Platform: mini_browser_* WebSocket protocol and lanes, new session type
mini_browser, settings section, error codes, AgentBase run-state / stop /
delete / sub-agent / shutdown hooks, skill, prompt rule and AGENT.md.

Frontend: Mini Browser page (sidebar entry, live view with IME/paste/
shortcuts, tabs, take control, passwords dialog), 7 languages.
…ngs)

Engine
- shared start/close/recovery tasks are awaited without inheriting their
  cancellation; a close during a launch aborts at a phase boundary, keeps
  the profile lock until Chromium exits and leaks no driver
- every agent tab gets its own Chromium window, so background owners are no
  longer throttled (clicks went from ~25 s to ~1 s)
- ops race against a per-tab closed event: a closed tab or browser ends the
  op with MINI_BROWSER_TAB_CLOSED / MINI_BROWSER_CLOSED, never a fake success
- run states and Stop are kept process-wide, so Stop before the first
  browser use still revokes sub-agents; the host is terminal after shutdown
- agents see only their own tabs' URLs; user tabs are claimed only when
  blank, by the Mini Browser chat for the viewed tab, or by an explicit
  switch to the viewed tab
- UI-origin blocking covers IPv4-mapped / 0.0.0.0 / [::] spellings;
  secrets are masked in their URL/HTML/JSON-encoded forms too
- downloads get Mark-of-the-Web on Windows and a "dangerous" flag

Agent operations
- take-control stops in-flight typing/mouse/scroll at once
- date/time inputs, framework click handlers, slotted labels, row context,
  iframes, modal text, slow-XHR settling and results kept under 16k chars
- conservative login outcomes; CAPTCHA/2FA flow via wait(for_user)
- vault autofill limited to the exact site (with or without www.)

Platform
- managed installs receive newly shipped action files and skills on
  upgrade (app/state_sync.py), without overwriting user edits
- the Mini Browser prompt rule is shown only when the browser is usable
- full /reset keeps per-session streams; input/control/shutdown get their
  own WebSocket lanes with input coalescing; settings reload live

Frontend
- no one-click Open for executable downloads; address bar binds to the
  edited tab; pending input never follows a view switch; passwords dialog
  error states; contrast, layout and translation fixes
@ahmad-ajmal ahmad-ajmal added the feature New feature or request label Oct 14, 2026

@ahmad-ajmal ahmad-ajmal left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reproduced the six issues below with real browser tests and isolated storage. End-to-end checks used the production Mini Browser frontend and backend with a stub agent controller. Browser testing used Playwright 1.58 and installed Edge because the downloaded Chromium builds could not launch on this machine.

Comment thread app/mini_browser/ops.py Outdated
Comment thread app/mini_browser/core.py
Comment thread app/mini_browser/ops.py
Comment thread app/ui_layer/browser/frontend/src/pages/MiniBrowser/live/useKeyboardSink.ts Outdated
Comment thread app/mini_browser/core.py Outdated
Comment thread app/mini_browser/core.py Outdated

@ahmad-ajmal ahmad-ajmal left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Four additional maintainability comments. The adapter contract and BrowserCore decomposition comments are follow-up suggestions.

Comment thread app/mini_browser/config.py Outdated
Comment thread tests/mini_browser_fixtures.py Outdated
Comment thread app/mini_browser/ws.py
Comment thread app/mini_browser/core.py
- Agent input never lands after the user takes control. Waits for an
  element run in 250 ms slices that check control; clicks press through
  page.mouse at a point just checked to hit the element instead of
  Playwright's click, which kept waiting inside the driver. Focus, clear,
  caret, select, upload and whole-value inputs, and the vault login's fill
  and Enter, act on element handles right after the control check.
- Agents cannot close a tab the user controls.
- Opening a tab reserves its slot before the first await, so concurrent
  opens never exceed the browser-wide or per-owner tab limits.
- Agent screenshots cover password and secret fields, including fields
  that were password fields before a "show password" toggle.
- A saved password in a download's suggested name is masked before it
  becomes the file name, a log line or a notice.
- Cut deletes the selection only after its own copy reached the
  clipboard, in the tab it was copied from (copy replies echo requestId).
- One settings normalizer (mini_browser.config.clean_setting) for
  app.config and MiniBrowserSettings.
- Browser tests skip only when Playwright or Chromium is not installed;
  CRAFTBOT_REQUIRE_BROWSER_TESTS=1 makes that a failure too.

@ahmad-ajmal ahmad-ajmal left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review of ccf5237: the original reproductions now pass, including the slow Ctrl+A/Ctrl+X production UI scenario. These three additional findings require specific timing conditions. I deliberately created the race windows to confirm the behavior; this does not establish how frequently users would encounter it. The dropdown issue is medium severity.

Comment thread app/mini_browser/ops.py Outdated
Comment thread app/mini_browser/core.py
Comment thread app/ui_layer/browser/frontend/src/pages/MiniBrowser/live/useKeyboardSink.ts Outdated
Threads (and other sites that link addresses as you type) put agents into
a delete-and-retype loop, with link previews flipping between sites:

- Web and e-mail addresses are inserted whole, like a paste. Typed key by
  key, the site saw every partial address ("https://e", ...,
  "example.co") and previewed the wrong ones.
- After typing into a rich text box (contenteditable), the box is read
  back and the result says whether it holds the whole text; links, link
  previews and styled mentions are named as the site's formatting.
- The same text is typed into the same page at most 3 times in 10
  minutes; the 4th attempt is refused with a "stop and check or ask the
  user" message. Only a hash of the text is kept.
- The mini-browser skill and the type action describe link previews and
  say never to delete and retype a post because it looks different.

Sound: Playwright starts Chromium with --mute-audio, so no page could play
sound. New setting mini_browser.sound (default true) drops that switch; it
applies at the browser's next start.
The compact page observation showed only the first 40 characters of a
post or message box, so a post with a link often appeared as
"https://example.c…": a broken-looking link the agent took for its own
typo, one more trigger of the Threads retype loop.

- Textareas and rich text boxes show up to 300 characters, cut between
  words (also in the tighter fallback format), and say how long the full
  text is when it is cut.
- The read-back after typing covers the whole editor, also when the
  element is one of its paragraphs.
- The retype guard counts a retry that only writes an address
  differently ("https://" added or dropped) as the same text.

Checked against the real Lexical editor with its AutoLinkPlugin: typing
an address key by key changed the link five times (example.c ->
example.co -> example.com -> ...), now it is created once. Sound was
measured in the Windows mixer: Chromium plays (live-view and agent
clicks) with sound on, and is silent with it off.
- Dropdowns: Playwright's select_option keeps waiting in the driver while
  the option is missing (force does not stop that retry), so a choice
  could land after the user took control. Each attempt is now one
  in-page step that finds the option again and selects it only if it is
  there and enabled (input + change, like Playwright); between attempts
  the wait only reads, and every attempt checks control first.
- Closing the last tab: the owner and take-control checks run again after
  the replacement tab has opened, right before the close.
- Cut: the copy is marked as a cut and the backend keeps the text it
  copied (memory only, 30 s). The delete is a "cut" input event in the
  ordered input queue, after whatever the user did meanwhile, and runs
  only while the selection is still exactly that text; otherwise it is
  skipped, so nothing that is not on the clipboard is deleted.

@ahmad-ajmal ahmad-ajmal left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consolidated review of d9800ff: four requests to address before merge. Each inline comment includes the condition and reproduction. Controlled timing tests establish possible behavior, not production frequency. The earlier reported scenarios now pass.

Comment thread app/mini_browser/core.py
Comment thread app/mini_browser/ops.py Outdated
Comment thread app/mini_browser/core.py
- Cut: only a complete copy can be cut. A selection longer than the copy
  limit, or one with a saved password masked out, reaches the clipboard
  only in part, so it is copied but never deleted; the delete compares the
  whole selection. Pending cuts are kept per request, with ids scoped to
  the viewer connection (and unique per view mount), so two viewers' cuts
  never share a record and a mismatched delete leaves the others intact.
- Retype guard: scoped to the same field (data-mb-field) and to text that
  was not sent; Enter or the page moving on resets it. Addresses only
  ignore "https://" and "www.", so different links are different texts.
  Filling several fields with the same value is never refused.
- Navigation: control is checked again right before an operation runs
  (after crash recovery and bringing its window forward), right before
  navigate's goto / back / forward / reload, and before a new tab opened
  with a URL loads it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants