Skip to content

agent-browser: frontend-agnostic realm-cached auto-login + per-session isolation (WIP)#2355

Open
yardenhochman wants to merge 14 commits into
garrytan:mainfrom
nRichSolutions:feat/agent-browser-session-isolation
Open

agent-browser: frontend-agnostic realm-cached auto-login + per-session isolation (WIP)#2355
yardenhochman wants to merge 14 commits into
garrytan:mainfrom
nRichSolutions:feat/agent-browser-session-isolation

Conversation

@yardenhochman

@yardenhochman yardenhochman commented Jul 26, 2026

Copy link
Copy Markdown

What

Adds an agent-browser capability to browse: each agent session gets a fast,
isolated browser that is already logged in to the right frontend env — no
shared tabs, no profile-lock deadlocks, no login form.

The engine is frontend-agnostic: each consuming frontend ships one
.agent-browser.config.mjs adapter (envs, realm groupings, auth state machine,
token contract). No app-specific auth lives in the engine — adding a frontend is
config-only.

Shipped in this PR

Auto-login (realm-cached token)

  • frontend-adapter.ts — adapter schema + loader/validator.
  • token-manager.ts — direct-POST auth state machine (no form-driving) + cache
    keyed by (realm, user), chmod 600, expiry from the JWT exp (fallback TTL),
    injectable creds/TOTP providers for unit testing without live calls or secrets.
  • browse agent-token <env> — pre-server CLI command; op/Touch-ID prompt in the
    user's terminal, secret-free ENV/REALM/APPORIGIN/EXPIRES/CACHED summary.

Per-session isolation

  • SessionState facade over BrowserManager: one shared Browser +
    Map<sessionId, SessionState>; per-session context/pages/headers/UA/viewport/
    dialog/ownership/buffers, exposed as accessors so existing method bodies are
    untouched. Lazy default session preserves all pre-existing callers.
  • Async page listeners bind to the OWNING session at wire-time → buffers never
    cross-contaminate when the active session switches.
  • Session routing at the handleCommandInternal choke point (+ batch/chain);
    per-session log flush.
  • inject-auth — origin-scoped localStorage injection (writes only when
    location.origin === appOrigin; re-applied on context recreation).
  • browse agent-open <env> [path] — end-to-end: cached-or-acquired token,
    isolated session, origin-scoped inject, navigate.
  • trace start|stop + browse show-trace — per-session Playwright tracing.
  • isProcessAlive now treats EPERM as alive (was the zombie-lock bug: a live
    Chromium holding SingletonLock misread as dead and skipped by cleanup).

Deferred (follow-up, per plan Scope Boundaries)

Full headed profile-per-session + hardened reaper rig, and the agent-browser init
new-frontend scaffolder (auth-contract auto-detect + realm probe).

Tests

bun test browse/test/ green (frontend-adapter, token-acquire, token-cache,
session-characterization, session-isolation, inject-auth, tracing,
is-process-alive-eperm + full existing suite). Auth contract locked against a live
backend first (no CSRF/cookie; JWT-exp lifetime; step-1 {mfaRequired:true}).
Context isolation, origin-scoped no-leak, and tracing verified live.

🤖 Generated with Claude Code

yardenhochman and others added 3 commits July 23, 2026 18:10
…er U9)

Frontend-agnostic adapter contract so each consuming frontend declares its
envs/realms/auth-state-machine/token-contract in one config; the browse engine
carries no app-specific auth knowledge. Enables isolated per-session browser +
realm-cached auto-login for agent sessions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ache (agent-browser U1/U2)

Direct-POST auth state machine driven by the frontend adapter (no form-driving,
no in-browser biometric). Caches the validated auth object under the frontend
repo's .auth/ keyed by (realm,userName), chmod 600; realm-sharing envs reuse one
token. Expiry from the JWT exp claim, falling back to defaultTtlMs. Credentials
and TOTP flow through injectable providers so the state machine is unit-tested
without live calls or real secrets. Never logs password/OTP/bearer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pre-server subcommand that acquires-or-reuses a cached bearer for a frontend env
via the repo's .agent-browser.config.mjs adapter. Runs CLI-side so op/Touch-ID
prompt in the user's terminal, not the daemon. Emits a machine-parseable,
secret-free summary (ENV/REALM/APPORIGIN/EXPIRES/CACHED).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@trunk-io

trunk-io Bot commented Jul 26, 2026

Copy link
Copy Markdown

Merging to main in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

yardenhochman and others added 11 commits July 26, 2026 09:52
…ion extraction (agent-browser U4)

Pins tab-ownership + global-buffer semantics that the default/legacy session must
reproduce unchanged after the isolation refactor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…cade (agent-browser U4)

BrowserManager holds Map<sessionId,SessionState> + the shared Browser. Per-tab /
per-context fields (context, pages, tabs, headers, UA, viewport, dialog policy,
ownership, guardrails, and the three observability buffers) become private
accessors into the current session's bundle, so existing method bodies operate
on the active session unchanged. A lazily-created default session reproduces
pre-U4 single-session behavior for callers that send no session.

wirePageEvents binds each page's async listeners to the session that OWNED the
page at wire-time, so console/network/dialog events never cross-contaminate when
the active session switches. Adds ensureSession/closeSession/getBuffers/
getAllSessions. read-commands resolves buffers via bm.getBuffers().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…agent-browser U4)

handleCommandInternal (the choke point every caller passes through) resolves an
optional `session` field to an isolated context via ensureSession before the
command runs and restores the prior session in a finally, so a subsequent
no-session command lands on the default. Chain subcommands inherit the pinned
session; batch forwards session per subcommand. flushBuffers iterates every
session with per-session cursors, tagging non-default lines by session id.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nt-browser U4)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-auth command (agent-browser U5)

injectOriginScopedStorage adds a per-session addInitScript that writes
localStorage[key] ONLY when location.origin === appOrigin, so a bearer never
leaks to a non-app origin (R5). Injections are recorded on the session and
re-applied on context recreation (viewport/scale) so auth isn't silently
dropped. Exposed as the `inject-auth <appOrigin> <storageKey> <authJson>`
write command, running within the routed session. Payload must be a non-empty
JSON object; adapter-level shape validation already happened at acquire.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… (agent-browser U5)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…sion buffers (agent-browser U4)

The shared test wrapper dropped the browserManager arg; console/network/dialog
reads now resolve buffers per-session via bm.getBuffers(), so the wrapper must
forward it. Buffer-seeding in the console-errors suite targets the session buffer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
env [path] -> isolated headless session with a cached-or-acquired token injected
origin-scoped, then navigated. Acquires CLI-side (op/Touch-ID in the terminal),
auto-generates a hidden session id (<user>-<worktree>-<shortid>; --session
overrides), ensures the daemon, then POSTs inject-auth + goto with the session.
Emits a parseable, secret-free summary (SESSION/ENV/REALM/URL/EXPIRES/CACHED).
--headed is deferred to Phase 2.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…owser U7)

The zombie-lock bug: a live Chromium holding the profile SingletonLock, owned
such that process.kill(pid,0) returns EPERM, was misread as dead so cleanup
skipped it. EPERM = exists-but-unsignalable = alive; only ESRCH is dead. The
full headed profile-per-session + reaper rig remains Phase-2 follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nt-browser U8)

trace start / trace stop <path> record a per-session Playwright trace on the
session's context; show-trace <path> opens it in Playwright's viewer (CLI-side,
no daemon). Completes the mode-map's 'watch what happened' path without a live
headed browser.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rsing

--print emits the bare bearer on stdout (secret-free summary moves to stderr)
so shell capture like BACKEND_JWT=$(browse agent-token <env> --print) works;
default output stays summary-only with no bearer. agent-open no longer treats
the --session flag's value as a positional nav path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@yardenhochman
yardenhochman marked this pull request as ready for review July 26, 2026 11:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant