← The Karsa Manual

The browser

@karsa/browser gives a project's agents a web browser: the machine's Google Chrome, headless, driven by Karsa (spec_undefinedMundefinedXBundefinedBFKundefinedT). An agent opens a page that needs JavaScript, reads it, clicks, types, and stays signed in to what you signed it in to. It works the same for agents on Claude Code, Codex and OpenCode.

Think of Safari's profiles on an iPhone ("Personal", "Work"): each has its own cookies and logins. Here each profile also has an owner, and an agent picks the profile it uses.

Set it up

binary. On the project's Apps page, install Browser (or karsa package install @karsa/browser --project <project>). undefined. Enable it. Its check needs Google Chrome (or Chromium, Brave or Edge; another one goes in its Config, binary). undefined. That's all: from the next turn, every agent of the project is told it's there.

Profiles: whose logins

| Profile | Who uses it | Example | |---|---|---| | Default profile (the project's, made on first use) | every agent of the project, unless it names another | @researcher reads pages, signed in to nothing until you sign it in | | An agent's own (profile_create) | that agent, and you | @buyer makes "amazon-work"; @researcher asking for it is refused | | Yours (from the page) | you | a profile you sign in to something only you use |

Its pages

Like Safari on an iPhone: a start page, a tab overview, and one tab full screen.

An error a browser extension of yours throws on Karsa's own page (MetaMask's "Failed to connect to MetaMask") isn't the app's, and Karsa doesn't show it.

How sites see it

As Google Chrome on this Mac: it sends the user agent your own Chrome sends (… Chrome/156.0.0.0 Safari/537.36), not the HeadlessChrome a headless Chrome sends by default. Some sites refuse that one outright: United's answered ERR_HTTP2_PROTOCOL_ERROR. On a phone's live view, it's an Android phone's Chrome.

What it doesn't do: pretend to be another machine, get past a site's bot detection, or solve a captcha. A captcha, a code or a sign-in is yours (below). A site's terms may forbid automated use: that's your call, per site.

Some sites still tell, and refuse a profile once an agent has used it: United's site loaded, then refused the Default profile for good after an agent clicked through from Google Flights. The agent says so; do that step in your own browser.

Watch it, and take over

The tab's page is live, like screen sharing: you see the page as it paints, and what you do goes straight to it. It comes as video (H.undefined), which Chrome encodes as it paints and your browser decodes, about undefined× less data than a picture per frame while the page moves. It's never more than two frames ahead of what your screen has drawn, so a slow connection gets fewer, newer ones instead of falling behind; a page that doesn't change sends nothing. A browser that can't play it gets the pictures instead. Chrome only captures what you draw, so watching a video doesn't keep it busy making pictures no one sees.

Sign a profile in

An agent never types your password. When a site needs a sign-in (or a code, a captcha), the agent sends you a link to its tab in the chat, like a colleague sharing their screen and handing you the keyboard:

> I need you to sign in to Booking.com: open the tab. Tell me when you're done.

undefined. Open the link. It's the agent's tab, live, where it stopped. undefined. Sign in on the page as you would anywhere (a phone works too). undefined. Say so in the chat ("done, carry on"). The agent picks up in the same tab, signed in.

The profile stays signed in, for whoever uses it. You can also sign a profile in ahead of time: open Browser, pick the profile, open the site's address, sign in.

What you do there goes to Chrome, not to any agent: an agent sees the page only when it next looks, and never a password field's value. It's the same idea as ChatGPT agent's "Take over browser". It's the agent's browser you take over, not yours: a site that won't load for the agent won't load for you there either.

For agents: using it

The primer names the browser fn and its id. Every call is a run:

curl -s -X POST $KARSA_API_BASE/rpc -H 'content-type: application/json' \
  -d '{"method": "karsa/run/create", "params": {"fn": "<browser fn id>", "params": {"action": "open", "url": "https://example.com"}}}'
→ {"result": {"fn": "fn_…", "output": {"tab": "tab_01M5K2B7XQ4F9R", "link": "/project/proj_…/app/app_…/tab/tab_01M5K2B7XQ4F9R", "profile": "Default profile", "url": "https://example.com/", "title": "Example Domain",
   "snapshot": "Example Domain This domain is for use in documentation …\n\n[1] link \"Learn more\" → https://iana.org/domains/example"}}}

Then act on the numbers: {"action": "click", "tab": "tab_01M5K2B7XQ4F9R", "ref": 1}, {"action": "type", "tab": "tab_01M5K2B7XQ4F9R", "ref": 3, "text": "cats", "submit": true}. Each returns the page after it. The Browser app's Overview lists every action (goto, press, scroll, back, forward, reload, screenshot, eval, tabs, close, and the profile ones).

What keeps it safe

Some sites stop playing for a browser driven by a program: YouTube plays about undefined seconds, then says "Something went wrong". That's YouTube's check, not a fault in Karsa.

Not yet