Skip to content
Jacob NolletteCloud & software engineer

An AI coding agent can read a public page with curl. It cannot read a client
portal, check whether a deploy actually renders, or pull a number off a dashboard that has no
API. Those all need the same thing: a browser that is already signed in.

So I built a pool of them. Ten Chrome browsers — chrome-1 through
chrome-10 — running as long-lived pods in Kubernetes, each with its own profile on
its own volume, each reachable over the Chrome DevTools Protocol. Any agent or developer session
can drive any one of them with a single command.

The first design was wrong, and it was wrong in an interesting way

The fleet started out owner-named: chrome-agent-atlas,
chrome-client-<name>, chrome-dev-1 through -4.
Fifteen browsers, each belonging to somebody. That felt tidy and it was wrong, because a browser
that belongs to one consumer is idle whenever that consumer is idle, and unavailable whenever
that consumer is busy. Fifteen browsers reserved about 60 GiB of memory to serve maybe three
concurrent sessions.

Collapsing to a flat, neutral pool of ten dropped the reservation to roughly 40 GiB and the
declared storage from 300 GiB to 200 GiB, while increasing how many parallel work queues
were actually available, because nothing sat reserved for an absent owner.

The honest cost of that trade is worth writing down, because it is the part that will bite
later: a number does not tell you whose logins it holds. The owner-named fleet
was self-documenting about exactly the thing that matters most — which browser is carrying a
client’s session. The pool is not. So the assignment list carries a comment beside each member
saying what it is signed into, and a client’s sessions do not go into any member without a line
there. That is a discipline, not a guarantee, and I would rather say so plainly than pretend the
rename was free.

Three things that were not obvious

Chrome rejects a Host header it does not recognise. Chrome’s own remote
debugging port refuses any request addressed to a Kubernetes Service name, and the failure looks
exactly like a dead browser rather than like a rejected header. Every browser therefore sits
behind a tiny nginx shim on a different port, which rewrites the header. This cost an afternoon
the first time and is now the first thing in the runbook.

One profile, one lock. Chrome takes a singleton lock on its user-data
directory. That single fact dictates the whole shape of the deployment: one browser per pod, one
volume per browser, read-write-once, never a second process pointed at the same profile. There is
no clever way around it and trying is how you corrupt a profile.

The profiles are the product. When the fleet was rebuilt, the browsers were
scaled to zero and the profile directories were moved on the shared filesystem, one to one, into
their new names. Cookies, sessions and “remember this device” flags all survived. Had I recreated
them instead, the visible symptom would have been “I have to log into everything again,” and the
invisible symptom would have been every multi-factor prompt in the fleet firing at once.

Giving a human somewhere to look

An agent drives these over a protocol, but a person occasionally needs to watch one —
to sign a site in, to clear a stuck modal, to see what the agent is seeing. That is what Kasm
Workspaces does here: each browser gets a tile in a web console, so the same container an agent is
automating can be opened in a browser tab by a human.

That console was itself split by audience, which turned out to matter more than expected. One
portal fronts the browser pool and nothing else. A second fronts operating-system desktops. A
third fronts application desktops. A tile is reachable from exactly one portal.

The reason is not tidiness. Each portal is a separate install with its own database, and
user accounts do not copy between installs — the stored password is encrypted
with a key derived per-install, so a row copied from one portal to another authenticates against
nothing and returns a 500 with a padding error. I found that by testing it with a throwaway user
rather than by discovering it on a live account. Three portals means three accounts created
natively, which is a small, repeated chore and a completely reliable one.

What it actually enables

The loop an agent runs is: open a page, read it as text, act, re-read. Sometimes take a
screenshot and actually look at the image. The browser is long-lived, so the session it built up
last week is still there this week — which is the entire point, and the thing a fresh headless
browser per task can never do.

It is also the piece that makes the rest of the agent fleet useful. An agent that can only call
APIs is limited to systems that have them. An agent with a signed-in browser is limited to systems
a person could use, which is nearly all of them.

One security property is worth stating out loud: anything that can reach
the debugging port can read every session in that browser. The port is cluster-internal only,
never exposed, and the pods carry a network policy admitting only the consumers that should reach
them. A browser full of live sessions is a credential store, and it deserves to be treated like
one.