neko
One real Chromium desktop that a human and an agent share. The human watches and clicks it in a browser tab; an agent drives the same tabs over CDP. Because the profile persists, a human can log in by hand — 2FA included — and an agent can then act as that logged-in user without ever holding the password.
| Category | Automation |
| Deploy | ./uis deploy neko — opt-in only, in no stack |
| Undeploy | ./uis undeploy neko — keeps the profile volume |
| Verify | ./uis verify neko |
| Depends on | Nothing |
| Required by | Nothing — consumers opt in |
| Image | ghcr.io/m1k1o/neko/chromium pinned to 3.1.5 |
| Default namespace | browser |
| Web view | http://neko.localhost — internal only |
| CDP | :9222 on the LAN and in-cluster — token-gated (X-Neko-Token header) |
neko or browserless?
Both are browsers. They make opposite trades, and picking the wrong one is the usual first mistake.
| browserless | neko | |
|---|---|---|
| Nature | headless, throwaway | interactive, watchable |
| Identity | none — blank every time | persistent profile; logins survive |
| Drivers | agents only | human + agents, same session |
| Concurrency | many isolated contexts in parallel | one shared session, single-writer |
| Reach for it when | rendering JS pages, scraping, synthetic checks | a task needs the human's own login |
The rule: many blank browsers in parallel → browserless. One logged-in browser, shared → neko.
A login-walled task on browserless has no session to use. A fifty-page parallel scrape on neko serialises behind one profile and blocks the human out of it.
Cheapest tool that works, in order:
- plain HTTP — the page renders without JavaScript. Free.
- browserless — JavaScript pages, no login needed. Cheap, parallel.
- neko — only when the human's login must act. Scarce: one shared session.
Escalate only when the level below fails.
neko is not like the other UIS services, and it is opt-in for these reasons.
CDP is full browser control — cookies included. Anything that presents the token acts as whoever the browser is logged in as.
The token is the only guard. neko is deliberately reachable on the LAN so that agents, other services and the test harness need no Tailscale operator — which means the network is not a guard here. On the reference installation it was: CDP was tailnet-only. A NetworkPolicy cannot replace that, because it cannot distinguish an authorised LAN device from any other LAN device.
So treat NEKO_CDP_TOKEN exactly like the logins it protects. ./uis verify neko check F asserts an anonymous request is refused; if that check ever passes
anonymously, every device on the LAN is logged into everything this browser is.
The profile volume is a live credential store. Its blast radius is everything you log into. It is deliberately not backed up — a backup is a second copy of that blast radius, and re-login is cheap.
Never run an agent task while a password is being typed. The human logs in; then the agent acts. An agent driving mid-login can submit the form, navigate away, or read the field.
Single writer. Two drivers on one login collide.
Deploying
neko is in no stack, so ./uis stack install can never start it. It also
ships commented out of enabled-services.conf, so ./uis deploy skips it. The
only way it starts is naming it:
./uis deploy neko
./uis verify neko
Then open http://neko.localhost and log in with the password from the secret:
kubectl get secret urbalurba-secrets -n browser \
-o jsonpath='{.data.NEKO_MEMBER_MULTIUSER_USER_PASSWORD}' | base64 -d
LocalDevNekoUser456 keeps Rancher Desktop zero-config. It is a working
password for a browser holding live sessions — change it for anything beyond
your own laptop, in .uis.secrets/secrets-config/00-common-values.env, then
./uis secrets apply.
How an agent drives it
The recommended path is @playwright/mcp pointed at neko's CDP endpoint — the
same mechanism ./uis browserless verify-session exercises, and proven against
neko's locked-down Chromium on the reference installation.
TOKEN=$(kubectl get secret urbalurba-secrets -n browser \
-o jsonpath='{.data.NEKO_CDP_TOKEN}' | base64 -d)
claude mcp add neko -- npx -y @playwright/mcp \
--cdp-endpoint "http://neko.browser.svc.cluster.local:9222" \
--cdp-header "X-Neko-Token: $TOKEN"
From the LAN, swap the endpoint for the Service's external address
(kubectl get svc neko -n browser).
A CDP client first GETs /json/version, then opens the webSocketDebuggerUrl
that response hands back. Chromium generates that URL, and it carries no query
string — so a ?token= would authenticate the discovery request and then fail
the websocket upgrade, which looks like the browser being down. A header applies
to both. @playwright/mcp sends it with --cdp-header; Playwright itself with
connectOverCDP(url, { headers }).
Raw CDP over the websocket, with flattened sessions, remains the low-level fallback — set the same header on the handshake.
Reading pages: screenshot plus vision beats DOM selectors on sites that hide content in shadow DOM.
Exposure — LAN is the base, tailnet is optional
The web view is HTTP plus a websocket and reaches you through Traefik like every other UIS service. WebRTC media cannot use that path — it needs raw TCP.
neko advertises addresses to the viewer through ICE, and the browser connects to exactly those. So an advertised address must be one the viewer can reach.
| Who | How | Needs |
|---|---|---|
| agents, other services | in-cluster neko.browser.svc.cluster.local | nothing |
| agents, the test harness | the LAN address, :9222 with the token | nothing |
| a human, locally | http://neko.localhost (through Traefik) | nothing |
| a human, remotely | the tailnet overlay | Tailscale operator |
A plain LoadBalancer on the node's own address is the base, and it works on
every cluster UIS targets — no Tailscale operator, no CNI-specific feature. The
node's InternalIP is discovered at deploy time and always advertised, which is
why the default needs no configuration.
It deliberately does not ask for port 80. On a single-node cluster Traefik's
own svclb already binds host port 80, so requesting it leaves the
LoadBalancer Pending forever — and because all of a Service's ports share one
svclb pod, that collision would take CDP and media down with it. The web
view does not need it: it reaches users through the IngressRoute, like every
other UIS service.
The tailnet is an overlay, never a replacement. Enabling it adds a second
Service for the web view; the LAN path keeps working. In
.uis.extend/neko-exposure.yaml:
tailnet_overlay: true
hostname: "neko"
extra_advertise_addresses: ["100.97.5.40"] # for MEDIA over the tailnet
The web view works over the tailnet as soon as the overlay exists. Media does
not, until that tailnet address is in extra_advertise_addresses — ICE only
offers what it is told. A tailnet IP cannot be discovered before its
LoadBalancer exists, so the deploy prints the address and re-running picks it up.
Until then a remote viewer sees the page load and the screen stay black.
The Service port and NEKO_WEBRTC_TCPMUX are deliberately the same number. A
Service that renumbered it would make neko advertise a port the viewer cannot
open — and the symptom is a black screen with everything reporting healthy:
page loads, pod Ready, /health 200. Check E asserts they agree.
Verifying
./uis verify neko
| Check | ||
|---|---|---|
| A | Web view healthy (/health on 8080) | the human's door |
| B | CDP reachable through the Service | the agent's door |
| C | DevTools policy override live, and CDP lists page targets | the invisible one |
| D | Profile volume bound and writable | logins can persist |
| E | Advertised WebRTC port equals the reachable port | media path consistent |
| F | Anonymous and wrong-token CDP requests get 401 | the guard |
C is why the playbook exists. Upstream neko ships
DeveloperToolsAvailability: 2 in its Chromium policy, under which
Target.attachToTarget is silently refused and page commands are swallowed.
/json/version keeps answering 200, the pod stays Ready, checks A, B, D and E
all pass — and no agent can drive the browser. UIS overrides that one key to 1
and check C asserts the override is live in the running container.
The verify does not prove a human can see video; that needs a real WebRTC client. Check E proves the advertised values are consistent, not that media flows.
Troubleshooting
The page loads but the screen is black. The media path, not the web view.
Check that the advertised port equals the reachable one (./uis verify neko
check E), and that nothing between you and the cluster blocks that TCP port.
An agent connects but nothing happens — no error. The DevTools policy. Confirm with check C; if the ConfigMap was edited, the pod must restart to reload the policy.
CDP returns 401. The token is missing or wrong, and it must be the
X-Neko-Token header — a query parameter authenticates discovery and then
fails the upgrade.
CDP refuses to connect at all. The cdp sidecar, not the browser. Chromium
binds its debug port to 127.0.0.1 and refuses non-local binds, so that sidecar
both re-exposes it and enforces the token:
kubectl get pod -n browser -l app=neko \
-o jsonpath='{.items[0].status.containerStatuses[*].name}{"\n"}'
Both neko and cdp must be ready.
Logins vanished after a restart. The profile volume. Check it is Bound and
writable — a bound-but-unwritable volume means fsGroup is not 1000.
Everything is gone and I want it gone. That is the kill switch working:
kubectl delete pvc neko-profile -n browser
./uis undeploy neko deliberately does not do this — an accidental undeploy
should not log you out of everything.
Related
- browserless — the other browser, and usually the right one
- Automation overview