Skip to content

feat(sessions): record each session's sandbox state and browser version - #24

Merged
arg1998 merged 5 commits into
mainfrom
feat/session-sandbox-record
Sep 27, 2026
Merged

arg1998 merged 5 commits into
mainfrom
feat/session-sandbox-record

Conversation

@arg1998

@arg1998 arg1998 commented Sep 27, 2026

Copy link
Copy Markdown
Owner

Closes #19

Every session now records, when its browser launches, whether that browser ran inside Chromium's sandbox and the browser's real version. Closed sessions keep showing both in the API, over WS and in the dashboard. Sessions from before this change, and sessions whose browser never started, read not recorded / not launched. They never read "no" or "not sandboxed".

Recording and display only: sandbox behaviour is unchanged.

Before / after

Surface Before After
GET /sessions/{id}, GET /sessions browser: {version, sandboxed} only while the session is live; absent once closed The same field, also for closed sessions, from what was recorded at launch. Still absent when nothing was recorded.
WS session.opened/updated From the live handle; lost after teardown Live handle first, then the launch record kept on the aggregate
Database Not stored sessions.sandboxed INTEGER CHECK IN (0,1), sessions.browser_version TEXT, both nullable (migration 0004-session-browser, compatible: true, nothing backfilled)
Dashboard → Details → Session Sandbox row for live sessions only; closed sessions showed the channel without a version Sandbox row for every session: sandboxed pill, muted not sandboxed, muted not launched (reserved/launching or launch_failed), or muted not recorded with an explainer ("Recorded for sessions launched with BrowserHive 0.2 or later … It does not mean the sandbox was off.", links to the security guide). The Browser row carries the version for closed sessions too.
MCP tools No browser Unchanged (D-12; spec 02 already kept the verdict off the tool output)

How it works

  • Session.browserInfo: the aggregate keeps the launch facts it attached (handle.browserInfo), including after detach(). The latest launch wins. No code path relaunches a session's browser today, but the rule is in the spec.
  • Write path: the existing session.updated patch (toSessionPatch) carries sandboxed/browserVersion only once a launch recorded them, so no patch ever clears a stored record. The first write is the register phase's update, when the session goes live. The insert at reserve time stores NULL.
  • One fallback order in every SessionSummary builder: toSessionSummary (aggregate, used by HTTP and WS), sessionRowToSummary (stored row), and overlayLive (the live summary wins; the row fills in when the aggregate has nothing).
  • Sessions export, retention and archive don't copy session columns, so nothing else needed changing.

Spec (committed first)

  • D-31 (new): what is recorded, when, latest launch wins, and that an absent record means "not recorded". D-27's consequences point to it.
  • 03 §4.2: the browser wire semantics. 03 §7: schema v4 DDL, migration paragraph, v4.db / schema-v4.json.
  • 04 Details panel wording, 09 test bullet, 11 §4.1 note, 02 "MCP unchanged".
  • Adjusted to measured behaviour in a follow-up commit: the spec said version is null for a persistent context, but Chromium's persistent context exposes a Browser, and the real run recorded 153.0.8010.12 for one. Spec, contract comment and port comment now say null only when no Browser can be read.

Verification

Real runs used the built package on a scratch data dir, port 9931. The previous release was origin/main a7a79d8, built in a second worktree.

  1. Pre-migration upgrade. The old build (0.1.3, schema v3) launched and closed legacy-chromium and legacy-chrome over MCP. At that point REST showed browser absent. The new build then started on the same data dir:

    • it wrote the browserhive-v3-….db backup and applied migration 3 → 4;
    • both old sessions still have no browser in the list and the detail, the DB holds NULL, and the dashboard shows "not recorded".
  2. New sessions under --sandbox auto. Each was checked live and again after close_session, via REST list, REST detail and the DB:

    Session Browser Result DB
    auto-chrome (kept live, then closed) /opt/google/chrome {154.0.8037.57, sandboxed: true} 1
    auto-chrome-done /opt/google/chrome {154.0.8037.57, sandboxed: true} 1
    auto-chromium bundled log sandbox fell back … No usable sandbox!, {153.0.8010.12, sandboxed: false} 0
  3. New sessions under --sandbox off:

    Session Result DB
    off-chromium (live, then closed) {153.0.8010.12, false} 0
    off-chrome {154.0.8037.57, false} 0
    off-persistent (persistent profile) {153.0.8010.12, false} 0
    no-edge (not installed, launch_failed) browser absent NULL; dashboard shows "not launched"
  4. MCP. list_sessions output is unchanged and has no browser key, by design.

Local gate:

  • bun run check: exit 0 (2642 server tests, 341 dashboard tests; openapi, docs and db-types fresh)
  • test:goldens, build, package:check: exit 0
  • test:integration: 67 pass
  • e2e, replicated as in CI: 9 passed, 1 skipped

Dashboard screenshots were reviewed at 1440 and 768 px, light and dark, in these states: live sandboxed, closed sandboxed, closed unsandboxed, pre-migration "not recorded" with the explainer open, and "not launched".

Tests added

  • test/persistence/session-browser-migration.test.ts:
    • v3.db → v4 keeps NULL and serves no browser;
    • insert and update round-trip;
    • an update without the fields keeps them;
    • a NULL version is kept alongside the verdict;
    • the CHECK refuses 2.
  • metadata.test.ts:
    • reserved: nothing recorded;
    • launched: written and served;
    • kept after detach;
    • the latest launch wins;
    • a NULL version;
    • a driver without facts records nothing.
  • serializers.test.ts: the row and overlay fallback.
  • Dashboard:
    • sandboxRecord covers all four states;
    • the Details panel renders sandboxed, not sandboxed and not recorded (with the explainer button) for closed sessions.
  • Fixture v4.db, golden schema-v4.json. The HTTP listSessions golden gains browser on the recorded seed session.

Not done

  • No sandboxed filter or facet on the sessions list. It would need a new query param, facet and openapi entries, so it is not the trivial addition the plan allowed.
  • One rare case gets the wrong wording. A session that was still launching when the daemon died is reconciled as interrupted with no record, so it shows "not recorded" with the pre-0.2 wording. The wire has no launched_at to tell the two cases apart.

Contract change: additive only. Schema v4: migration 0004-session-browser (compatible: true, so min_reader_version stays 1) adds two nullable sessions columns. There is a new fixture v4.db and golden schema-v4.json; v1–v3.db still upgrade to head and match a fresh install. The SessionSummary.browser shape is unchanged ({version: string | null, sandboxed: boolean}, optional); it is now also present for closed sessions that have a record. packages/contracts/generated/openapi.json is unchanged (the field has no description in OpenAPI; only the TS doc comment changed). WS payloads have the same shape. Tool goldens are unchanged.

…rd (D-31, schema v4)

Spec first for #19: sessions.sandboxed and sessions.browser_version
(migration 0004, compatible), written once the browser has launched
(latest launch wins), served as SessionSummary.browser for live and
closed sessions alike; absent means not recorded, never not sandboxed.
Dashboard Details shows sandboxed / not sandboxed / not launched / not
recorded. MCP tool output unchanged.
…on at launch (schema v4)

Migration 0004-session-browser (compatible) adds sessions.sandboxed
(INTEGER CHECK IN (0,1)) and sessions.browser_version, NULL for existing
rows. The Session aggregate keeps the launch facts it attached after
teardown (latest launch wins); the session.updated patch writes them once
known and never clears them. SessionSummary.browser falls back to the
recorded launch in every builder: the aggregate (HTTP detail/list overlay,
WS), the stored row and the live overlay. Absent still means not
recorded. Fixture v4.db, golden schema-v4.json, db types regenerated.
… sessions too

The Session panel's Sandbox row now shows for every session: a sandboxed
pill or muted "not sandboxed" from SessionSummary.browser (live or
recorded at launch), muted "not launched" when the browser never
started, and muted "not recorded" with an explainer for sessions from
before the record existed. It never reads "not sandboxed" without a
record.
Measured on the real run: Chromium's persistent context exposes a
Browser, so its version is recorded like any other. The spec, the
contract comment and the port comment said it was always null; they now
say null only when no Browser can be read.
@arg1998
arg1998 marked this pull request as ready for review September 27, 2026 02:37
@arg1998
arg1998 merged commit 6c90ade into main Sep 27, 2026
21 of 23 checks passed
@arg1998
arg1998 deleted the feat/session-sandbox-record branch September 27, 2026 02:46
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.

Record whether each session ran sandboxed, so closed sessions still show it

1 participant