Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/session-sandbox-record.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"browserhive": minor
---

Closed sessions now keep showing whether they ran inside Chromium's sandbox, and with which browser version.

- **Recorded at launch.** When a session's browser starts, BrowserHive stores whether it runs sandboxed and the browser's real version with the session. Under the default `--sandbox auto` the answer depends on the browser and the machine (on Ubuntu, Google Chrome sandboxes and the bundled Chromium falls back), so it is recorded rather than worked out later.
- **In the dashboard.** A session's Details tab shows the browser version and a **sandboxed** / **not sandboxed** state for finished sessions too, not only while they run. Sessions from before this release show **not recorded**, with a note that this does not mean the sandbox was off; a session whose browser never started shows **not launched**.
- **In the API.** `browser: { version, sandboxed }` on `GET /api/v1/sessions` and `GET /api/v1/sessions/{id}` is now filled for closed sessions from what was recorded at launch. It is still left out when nothing was recorded, so read a missing `browser` as "unknown", never as "not sandboxed".
- The database upgrades on start (schema v4, two new columns, a backup is written first); older releases can still open it. Nothing is guessed for existing sessions.
4 changes: 2 additions & 2 deletions docs/guide/dashboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Every session, live and finished. Filter by state (live, closed, archived), owne

## Session detail

The header shows the session id, state and actions: terminate, open the trace viewer, download `trace.zip`, archive, delete. When the agent is waiting on you, a banner shows the attention request or pending vault confirmations. A side rail lists owner, channel, headless or headed, persistence, current URL, lease and identity. The **Client** panel shows the harness and how it was recognised, the declared model ("not reported" when none), the workspace, the client's name, version and protocol, and any extra labels it sent.
The header shows the session id, state and actions: terminate, open the trace viewer, download `trace.zip`, archive, delete. When the agent is waiting on you, a banner shows the attention request or pending vault confirmations. A side rail lists owner, channel, headless or headed, persistence, current URL, lease and identity. The **Client** panel shows the harness and how it was recognised, the declared model ("not reported" when none), the workspace, the client's name, version and protocol, and any extra labels it sent. The **Session** panel shows the browser with its real version and whether it ran inside Chromium's sandbox. Both are recorded when the browser launches, so a closed session still shows them. A session from before this was recorded (BrowserHive 0.1.x) shows **not recorded**, which does not mean the sandbox was off; a session whose browser never started shows **not launched**.

Tabs:

Expand Down Expand Up @@ -81,7 +81,7 @@ A live tail of the server log with level, module, session and trace filters, pau

## System

Version, transport, uptime, bind address, sessions live versus the cap, open attention requests, live views, dashboard connections, default persistence, whether `evaluate` is allowed, vault backend, blocklist, retention, database size and schema version, telemetry endpoint and data directory. A stealth section shows the stealth level, driver, fingerprint and humanize defaults and CAPTCHA mode. **Browsers and sandbox** lists the browsers found on the machine (the bundled Chromium, an installed Google Chrome or Microsoft Edge) with version and path, marks the default one, and shows for each whether it runs inside Chromium's sandbox, with Chrome's own reason when it cannot. **MCP connections** lists the agents connected over MCP, live ones first, then recent ones; select one to see how its harness was recognised, its model and workspace, client and protocol, User-Agent, IP, any conflicting signals and extra labels. A session's Details tab shows its browser version and whether that session runs sandboxed. Banners warn when `evaluate` is enabled together with the vault, when disk space is low, or when a subsystem is degraded. The configuration table lists every effective setting with its source (`cli`, `file`, `env`, `default`, `derived`) and the values it shadowed; secrets are shown as redacted. A value the config file reads from an environment variable has a `$NAME` chip under its source (outlined when the variable was not set and the default is used); select it to see the value as written in the file, or, for a secret, just the variable. **Only values from references** narrows the table to those keys, and the filter also matches variable names. See [References](configuration.md#references).
Version, transport, uptime, bind address, sessions live versus the cap, open attention requests, live views, dashboard connections, default persistence, whether `evaluate` is allowed, vault backend, blocklist, retention, database size and schema version, telemetry endpoint and data directory. A stealth section shows the stealth level, driver, fingerprint and humanize defaults and CAPTCHA mode. **Browsers and sandbox** lists the browsers found on the machine (the bundled Chromium, an installed Google Chrome or Microsoft Edge) with version and path, marks the default one, and shows for each whether it runs inside Chromium's sandbox, with Chrome's own reason when it cannot. **MCP connections** lists the agents connected over MCP, live ones first, then recent ones; select one to see how its harness was recognised, its model and workspace, client and protocol, User-Agent, IP, any conflicting signals and extra labels. A session's Details tab shows its browser version and whether that session ran sandboxed, for live and closed sessions alike. Banners warn when `evaluate` is enabled together with the vault, when disk space is low, or when a subsystem is degraded. The configuration table lists every effective setting with its source (`cli`, `file`, `env`, `default`, `derived`) and the values it shadowed; secrets are shown as redacted. A value the config file reads from an environment variable has a `$NAME` chip under its source (outlined when the variable was not set and the default is used); select it to see the value as written in the file, or, for a secret, just the variable. **Only values from references** narrows the table to those keys, and the filter also matches variable names. See [References](configuration.md#references).

## Notifications

Expand Down
2 changes: 1 addition & 1 deletion docs/guide/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ Ubuntu 23.10 and later only let programs with an AppArmor profile create the use

The profile has the same shape as the one Ubuntu ships for Google Chrome. BrowserHive only prints it; it never installs anything.

`browserhive doctor` shows, per installed browser, whether it can run sandboxed here and, when your configured browser cannot, the options for your machine. The dashboard's System page lists the browsers and each one's sandbox state, and a session's Details tab shows whether that session runs sandboxed.
`browserhive doctor` shows, per installed browser, whether it can run sandboxed here and, when your configured browser cannot, the options for your machine. The dashboard's System page lists the browsers and each one's sandbox state, and a session's Details tab shows whether that session ran sandboxed. That is recorded when its browser launches and kept with the session, so you can still check it after the session closed (`browser.sandboxed` in `GET /api/v1/sessions/{id}`). Sessions from before BrowserHive 0.2 show "not recorded".

An agent can ask for the sandbox for one session (`launch_options: { chromiumSandbox: true }`); if the browser cannot provide it, the launch fails with `SANDBOX_UNAVAILABLE` instead of starting unsandboxed. An agent can never turn the sandbox off: `chromiumSandbox: false` is refused.

Expand Down
2 changes: 2 additions & 0 deletions docs/guide/upgrading.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ browserhive db migrate

**The browser sandbox is on by default since the release that added `--sandbox`.** Sessions now run inside Chromium's sandbox wherever the machine allows it (`sandbox=auto`). Where it cannot (Ubuntu 23.10+ with the bundled browser, root, Docker), sessions run as before and `browserhive doctor` explains why and how to fix it (still exit code 0). `--sandbox off` restores the previous behaviour exactly. See [Security: the browser sandbox](security.md#the-browser-sandbox).

**Each session records whether it ran sandboxed (schema v4).** From 0.2 on, the database stores each session's sandbox state and browser version when its browser launches, so closed sessions keep showing them. The migration only adds columns: an older release still opens the database. Sessions from before the upgrade show "not recorded"; nothing is guessed for them.

Prereleases are published under the `next` tag: `bun add -g browserhive@next`.

## Downgrade
Expand Down
6 changes: 5 additions & 1 deletion packages/contracts/src/http/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,11 @@ export const SessionSummary = z.object({
humanize: z.boolean(),
stealth_recorded: z.boolean(),
identity: AppliedIdentity.nullable(),
/** The running browser's real version (null for a persistent context) and whether it runs sandboxed; live sessions only. */
/**
* The browser's real version (null when the driver cannot read it) and whether it runs sandboxed (D-31):
* from the live browser while it runs, else as recorded at launch (schema v4). Absent = not recorded
* (sessions from before schema v4, or whose browser never launched), never "not sandboxed".
*/
browser: z.object({ version: z.string().nullable(), sandboxed: z.boolean() }).optional(),
proxy_label: z.string().nullable(),
counts: SessionCounts,
Expand Down
89 changes: 87 additions & 2 deletions packages/core/src/app/sessions/metadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,15 @@
import { describe, expect, it } from 'bun:test';
import { SessionSummary } from '@browserhive/contracts/http';
import { SessionMetadata } from '@browserhive/contracts/tools';
import type { AppliedIdentity } from '../../ports/browser-driver.ts';
import { configJson, toSessionMetadata, toSessionRecord, toSessionSummary } from './metadata.ts';
import { FakeSessionHandle } from '../../../test/helpers/fake-session-handle.ts';
import type { AppliedIdentity, SessionBrowserInfo } from '../../ports/browser-driver.ts';
import {
configJson,
toSessionMetadata,
toSessionPatch,
toSessionRecord,
toSessionSummary,
} from './metadata.ts';
import { testSession } from './test-support.ts';

const T0 = 1_700_000_000_000;
Expand Down Expand Up @@ -121,4 +128,82 @@ describe('session metadata projections', () => {
expect(closed.closedReason).toBe('user');
expect(closed.closedAt).toBe(T0 + 6);
});

describe('the launch record of the browser (D-31)', () => {
function launched(info?: SessionBrowserInfo) {
const session = testSession();
session.apply({ type: 'launch', phase: 'launch', at: T0 + 1 });
const handle = new FakeSessionHandle(session.id, {
...(info !== undefined && { browserInfo: info }),
});
session.attach({ handle, driver: 'playwright', launchedAt: T0 + 2, launchMs: 2 });
session.apply({ type: 'launched', at: T0 + 2 });
return { session, handle };
}

it('a reserved session records nothing and its patch leaves the columns alone', () => {
const session = testSession();
const record = toSessionRecord(session);
expect(record.sandboxed).toBeNull();
expect(record.browserVersion).toBeNull();
expect(toSessionPatch(session)).not.toHaveProperty('sandboxed');
expect(toSessionPatch(session)).not.toHaveProperty('browserVersion');
expect(toSessionSummary(session, T0)).not.toHaveProperty('browser');
});

it('a launched session writes what the handle reported and serves it', () => {
const { session } = launched({ version: '154.0.8037.57', sandboxed: true });
expect(toSessionPatch(session)).toMatchObject({
sandboxed: true,
browserVersion: '154.0.8037.57',
});
const summary = toSessionSummary(session, T0 + 3);
expect(summary.browser).toEqual({ version: '154.0.8037.57', sandboxed: true });
expect(SessionSummary.parse(summary)).toEqual(summary);
});

it('keeps the record after teardown, so a closed aggregate still serves it', () => {
const { session } = launched({ version: '153.0.8010.12', sandboxed: false });
session.apply({ type: 'drain', reason: 'user', at: T0 + 5 });
session.apply({ type: 'closed', at: T0 + 6 });
session.detach();
expect(session.handle).toBeNull();
expect(toSessionSummary(session, T0 + 7).browser).toEqual({
version: '153.0.8010.12',
sandboxed: false,
});
expect(toSessionPatch(session)).toMatchObject({
sandboxed: false,
browserVersion: '153.0.8010.12',
});
});

it('the latest launch wins', () => {
const { session } = launched({ version: '153.0.8010.12', sandboxed: false });
session.attach({
handle: new FakeSessionHandle(session.id, {
browserInfo: { version: '154.0.8037.57', sandboxed: true },
}),
driver: 'playwright',
launchedAt: T0 + 9,
launchMs: 9,
});
expect(toSessionPatch(session)).toMatchObject({
sandboxed: true,
browserVersion: '154.0.8037.57',
});
});

it('a launch without a readable version records its verdict with a null version', () => {
const { session } = launched({ version: null, sandboxed: true });
expect(toSessionPatch(session)).toMatchObject({ sandboxed: true, browserVersion: null });
expect(toSessionSummary(session, T0 + 3).browser).toEqual({ version: null, sandboxed: true });
});

it('a driver that reports nothing records nothing', () => {
const { session } = launched();
expect(toSessionPatch(session)).not.toHaveProperty('sandboxed');
expect(toSessionSummary(session, T0 + 3)).not.toHaveProperty('browser');
});
});
});
30 changes: 24 additions & 6 deletions packages/core/src/app/sessions/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,26 @@ export function launchHarnessOf(client: SessionClientInfo | null): string | null
return client?.harness ?? null;
}

/**
* `SessionSummary.browser` of an aggregate (D-31): the live handle's facts while the browser runs,
* else what its launch recorded (kept after teardown); absent when nothing was recorded.
*/
function browserOf(session: Session): Pick<SessionSummary, 'browser'> {
const info = session.handle?.browserInfo ?? session.browserInfo;
return info === undefined || info === null
? {}
: { browser: { version: info.version, sandboxed: info.sandboxed } };
}

/** The recorded launch facts as columns: both `null` until a browser that reports them launched. */
function launchColumns(session: Session): Pick<SessionRecord, 'sandboxed' | 'browserVersion'> {
const info = session.browserInfo;
return {
sandboxed: info?.sandboxed ?? null,
browserVersion: info?.version ?? null,
};
}

/** The one HTTP/WS shape (spec 03 §4.2). `has_live_viewers` is enriched by the interface layer. */
export function toSessionSummary(session: Session, now: number): SessionSummary {
const request = session.request;
Expand Down Expand Up @@ -157,12 +177,7 @@ export function toSessionSummary(session: Session, now: number): SessionSummary
humanize: request.humanize,
stealth_recorded: request.stealth && session.identity !== null,
identity: identityJson(session),
...(session.handle?.browserInfo !== undefined && {
browser: {
version: session.handle.browserInfo.version,
sandboxed: session.handle.browserInfo.sandboxed,
},
}),
...browserOf(session),
proxy_label: session.proxyLabel,
counts: {
tool_calls: session.counts.toolCalls,
Expand Down Expand Up @@ -235,6 +250,7 @@ export function toSessionRecord(session: Session): SessionRecord {
launchMs: session.launchMs,
config: configJson(session),
harness: launchHarnessOf(request.client),
...launchColumns(session),
};
}

Expand All @@ -252,5 +268,7 @@ export function toSessionPatch(session: Session): SessionPatch {
launchMs: session.launchMs,
closedAt: session.closedAt,
closedReason: session.closedReason,
// Only once a launch recorded them, so no patch ever clears a stored record (D-31).
...(session.browserInfo !== null && launchColumns(session)),
};
}
20 changes: 19 additions & 1 deletion packages/core/src/domain/session/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
import type { ClosedReason, StealthDriverName } from '@browserhive/contracts/enums';
import { AppError } from '../../kernel/errors/app-error.ts';
import type { Result } from '../../kernel/result.ts';
import type { AppliedIdentity, SessionHandle } from '../../ports/browser-driver.ts';
import type {
AppliedIdentity,
SessionBrowserInfo,
SessionHandle,
} from '../../ports/browser-driver.ts';
import type { CreateSessionRequest } from './create-request.ts';
import {
isLeaseExpired,
Expand Down Expand Up @@ -82,6 +86,7 @@ export class Session {
private currentState: SessionState;
private currentLease: Lease;
private attachment: LaunchAttachment | null = null;
private launchBrowser: SessionBrowserInfo | null = null;
private appliedIdentity: AppliedIdentity | null = null;
private label: string | null = null;
private restoredSeed: string | null = null;
Expand Down Expand Up @@ -128,6 +133,15 @@ export class Session {
return this.attachment?.driver ?? null;
}

/**
* What the latest launch reported about the browser (real version, sandbox verdict), kept after
* teardown so a closed session still knows what it ran with (D-31). `null` until a browser that
* reports it has launched.
*/
get browserInfo(): SessionBrowserInfo | null {
return this.launchBrowser;
}

/** Epoch ms the browser became usable, once known. */
get launchedAt(): number | null {
return this.attachment?.launchedAt ?? null;
Expand Down Expand Up @@ -261,6 +275,10 @@ export class Session {
attach(attachment: LaunchAttachment): void {
this.attachment = attachment;
this.appliedIdentity = attachment.handle.identity;
// The latest launch wins (D-31); `detach` keeps it.
const info = attachment.handle.browserInfo;
this.launchBrowser =
info === undefined ? null : { version: info.version, sandboxed: info.sandboxed };
}

/** Drops the driver handle after teardown so nothing can drive a closed browser. */
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/infra/persistence/generated/db.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions packages/core/src/infra/persistence/mappers/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ export function sessionFromRow(row: Selectable<Sessions>): SessionRecord {
launchMs: row.launch_ms,
config: parseJsonObject(row.config_json, where),
harness: row.harness,
sandboxed: row.sandboxed === null ? null : intToBool(row.sandboxed),
browserVersion: row.browser_version,
};
}

Expand Down Expand Up @@ -91,6 +93,8 @@ export function sessionToRow(record: SessionRecord): Selectable<Sessions> {
launch_ms: record.launchMs,
config_json: toJson(record.config),
harness: record.harness,
sandboxed: record.sandboxed === null ? null : boolToInt(record.sandboxed),
browser_version: record.browserVersion,
};
}

Expand All @@ -109,5 +113,9 @@ export function sessionPatchToRow(patch: SessionPatch): Updateable<Sessions> {
...(patch.launchMs !== undefined && { launch_ms: patch.launchMs }),
...(patch.closedAt !== undefined && { closed_at: patch.closedAt }),
...(patch.closedReason !== undefined && { closed_reason: patch.closedReason }),
...(patch.sandboxed !== undefined && {
sandboxed: patch.sandboxed === null ? null : boolToInt(patch.sandboxed),
}),
...(patch.browserVersion !== undefined && { browser_version: patch.browserVersion }),
};
}
Loading
Loading