Skip to content

Commit 339979f

Browse files
committed
feat(browser): fence client page placements
1 parent 855fe94 commit 339979f

4 files changed

Lines changed: 336 additions & 36 deletions

File tree

config/reliability-gates.jsonc

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2584,6 +2584,7 @@
25842584
"capability-gated native and SSH execution-host adapters",
25852585
"main-owned loopback SOCKS5 route",
25862586
"runtime-owned browser-host lease and placement authority",
2587+
"exact live client-page placement admission",
25872588
"dedicated paired-runtime E2EE tunnel subscription"
25882589
],
25892590
"platforms": ["macos", "linux", "windows"],
@@ -2594,9 +2595,10 @@
25942595
"motivatingLinks": [
25952596
"https://linear.app/stably/issue/STA-4150/refactor-remote-browser-to-client-hosted-electron-webviews"
25962597
],
2597-
"invariant": "A client-hosted browser network route exists only for an exact live server-owned browser-host lease, authority epoch, host generation, paired identity, execution-host revision or SSH provider authority, and server-owned route generation. Non-native descriptors require explicit capability negotiation and an exact runtime-minted lease-bound execution-host grant. Host selection never chooses arbitrarily, stale cleanup cannot remove a replacement, destination names remain unresolved until the execution host, credit returns only after writes settle, and route loss never falls back to desktop DNS or sockets. A main-owned local listener remains stable across upstream transport loss, rejects CONNECT while offline, accepts only a strictly newer tunnel generation, ignores superseded callbacks, and never reuses a stream ID within one generation. Pending opens, admitted open rate, per-route application bytes, aggregate browser-host/process bytes, claims, and socket sources remain bounded with exact release on settlement and retirement. Queued transport drain yields after a bounded quantum. The local SOCKS endpoint accepts only loopback CONNECT and rejects unsupported commands.",
2598-
"oracle": "Attach two browser hosts and require unqualified selection to fail ambiguous while exact selection succeeds. Reject a second identity on one connection and a fifth identity for one paired device, release one exact lease, then admit its replacement without starving another device. Saturate the browser-host long-poll sub-cap, require an ordinary wait to remain admitted, close the socket, and prove both counters and handlers release. Replace one same-device connection, require the old subscription and route to fence, then prove late old cleanup leaves the new generation live. Mint an exact execution-host grant, reject a different key, prove late old grant cleanup leaves its replacement, and invalidate every grant with lease replacement. Retire a closed page placement by exact token, reject delayed cleanup against a replacement and server placement, and require its reused ID to receive a higher global generation without retaining a tombstone. Admit 16 pending destination opens and reject the seventeenth until one connects; admit 128 opens in one monotonic 10-second window and reject the 129th until the window expires. Fill 8 MiB across destination-to-client queues and separately across unsettled client-to-destination writes, release one exact retired stream, admit one replacement, and require the next byte claim to close the route without stale-callback or reentrant-close underflow. Across multiple routes, charge application copies, encrypted queue entries, and dynamic native socket bytes to one 32 MiB browser-host and 128 MiB process ceiling; reject zero-byte claim/socket-source floods, fence released leases, synchronously release JavaScript queue claims while retaining native claims through exact socket close, and yield a parked queue after four frames without reordering. Drop an authenticated transport, require the same listener to reject CONNECT before and during reconnect, attach the exact existing native v1 payload and capability pair to a strictly newer generation, ignore late old callbacks, and roll an exhausted stream-ID ledger only by replacing the generation. Require SSH descriptors to add the execution-host capability and exact grant, reject missing grants and stale provider authority before resolver or binary registration, pass exact domains to ssh2 and system-SSH SOCKS without local resolution, sanitize connector diagnostics, and close every stream and standalone route process once on timeout, synchronous failure, or rotation. Drive the accepted lease through real paired E2EE native SOCKS-to-HTTP and ephemeral Docker ssh2 remote-only DNS journeys, then exercise exact frame, credit, half-close, stale-stream, remote-DNS, unsupported-command, offline, and teardown assertions.",
2598+
"invariant": "A client-hosted browser network route exists only for an exact live server-owned browser-host lease, authority epoch, host generation, paired identity, execution-host revision or SSH provider authority, and server-owned route generation. A client page is usable only when its runtime ID, authority epoch, host identity and generation, logical page ID, page-host generation, current placement, and still-live lease all match atomically; a retired or server-placed page cannot be claimed as client placement. Live logical placements and their identities are bounded, while the runtime-wide page generation stays monotonic without tombstones. Non-native descriptors require explicit capability negotiation and an exact runtime-minted lease-bound execution-host grant. Host selection never chooses arbitrarily, stale cleanup cannot remove a replacement, destination names remain unresolved until the execution host, credit returns only after writes settle, and route loss never falls back to desktop DNS or sockets. A main-owned local listener remains stable across upstream transport loss, rejects CONNECT while offline, accepts only a strictly newer tunnel generation, ignores superseded callbacks, and never reuses a stream ID within one generation. Pending opens, admitted open rate, per-route application bytes, aggregate browser-host/process bytes, claims, and socket sources remain bounded with exact release on settlement and retirement. Queued transport drain yields after a bounded quantum. The local SOCKS endpoint accepts only loopback CONNECT and rejects unsupported commands.",
2599+
"oracle": "Attach two browser hosts and require unqualified selection to fail ambiguous while exact selection succeeds. Reject a second identity on one connection and a fifth identity for one paired device, release one exact lease, then admit its replacement without starving another device. Saturate the browser-host long-poll sub-cap, require an ordinary wait to remain admitted, close the socket, and prove both counters and handlers release. Replace one same-device connection, require the old subscription and route to fence, then prove late old cleanup leaves the new generation live. Mint an exact execution-host grant, reject a different key, prove late old grant cleanup leaves its replacement, and invalidate every grant with lease replacement. Retire a closed page placement by exact token, reject delayed cleanup against a replacement and server placement, and require its reused ID to receive a higher global generation without retaining a tombstone. Require a client page through the exact runtime, epoch, host, and page tuple; reject every mismatch, released or replaced leases, retired pages, server placements, invalid identities, and the 257th live placement while allowing exact replacement at capacity. Admit 16 pending destination opens and reject the seventeenth until one connects; admit 128 opens in one monotonic 10-second window and reject the 129th until the window expires. Fill 8 MiB across destination-to-client queues and separately across unsettled client-to-destination writes, release one exact retired stream, admit one replacement, and require the next byte claim to close the route without stale-callback or reentrant-close underflow. Across multiple routes, charge application copies, encrypted queue entries, and dynamic native socket bytes to one 32 MiB browser-host and 128 MiB process ceiling; reject zero-byte claim/socket-source floods, fence released leases, synchronously release JavaScript queue claims while retaining native claims through exact socket close, and yield a parked queue after four frames without reordering. Drop an authenticated transport, require the same listener to reject CONNECT before and during reconnect, attach the exact existing native v1 payload and capability pair to a strictly newer generation, ignore late old callbacks, and roll an exhausted stream-ID ledger only by replacing the generation. Require SSH descriptors to add the execution-host capability and exact grant, reject missing grants and stale provider authority before resolver or binary registration, pass exact domains to ssh2 and system-SSH SOCKS without local resolution, sanitize connector diagnostics, and close every stream and standalone route process once on timeout, synchronous failure, or rotation. Drive the accepted lease through real paired E2EE native SOCKS-to-HTTP and ephemeral Docker ssh2 remote-only DNS journeys, then exercise exact frame, credit, half-close, stale-stream, remote-DNS, unsupported-command, offline, and teardown assertions.",
25992600
"commands": [
2601+
"pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/browser-host-page-placement.test.ts src/main/runtime/browser-host-lease-registry.test.ts",
26002602
"pnpm exec vitest run --config config/vitest.config.ts src/shared/browser-network-capabilities.test.ts src/shared/browser-network-tunnel-protocol.test.ts src/main/browser/browser-network-tunnel-session.test.ts src/main/browser/remote-browser-socks-server.test.ts",
26012603
"pnpm exec vitest run --config config/vitest.config.ts src/main/browser/browser-network-tunnel-client.test.ts src/main/browser/paired-runtime-browser-network-route.test.ts src/main/runtime/runtime-binary-message-router.test.ts src/main/runtime/rpc/methods/browser-network-tunnel.test.ts src/main/runtime/browser-network-tunnel-paired-runtime.integration.test.ts",
26022604
"pnpm exec vitest run --config config/vitest.config.ts src/main/browser/browser-network-tunnel-outbound-memory-budget.test.ts src/main/browser/browser-network-tunnel-client-memory-budget.test.ts src/main/browser/browser-network-tunnel-session-aggregate-memory.test.ts src/shared/ws-outbound-backpressure-queue.test.ts src/shared/remote-runtime-client.test.ts",
@@ -2610,6 +2612,7 @@
26102612
"src/shared/browser-network-capabilities.test.ts",
26112613
"src/shared/browser-client-host-protocol.test.ts",
26122614
"src/shared/browser-network-tunnel-protocol.test.ts",
2615+
"src/main/runtime/browser-host-page-placement.test.ts",
26132616
"src/main/runtime/browser-host-lease-registry.test.ts",
26142617
"src/main/runtime/runtime-rpc-browser-host-admission.test.ts",
26152618
"src/main/runtime/rpc/methods/browser-client-host.test.ts",
@@ -2635,6 +2638,15 @@
26352638
"tests/e2e/ssh-browser-network-execution-route.docker.unit.test.ts"
26362639
],
26372640
"assertionRefs": [
2641+
{
2642+
"file": "src/main/runtime/browser-host-page-placement.test.ts",
2643+
"assertions": [
2644+
"client placement requires the exact runtime, epoch, host, and page generation on a live lease",
2645+
"lease release and replacement fence an otherwise matching client placement",
2646+
"retired, replaced, and server placements cannot be claimed as live client pages",
2647+
"live placement count and page/host identities are bounded before generation allocation"
2648+
]
2649+
},
26382650
{
26392651
"file": "src/main/runtime/browser-host-lease-registry.test.ts",
26402652
"assertions": [
@@ -2841,6 +2853,15 @@
28412853
}
28422854
],
28432855
"evidenceRuns": [
2856+
{
2857+
"date": "2026-08-14",
2858+
"runner": "local",
2859+
"platform": "macos",
2860+
"command": "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/browser-host-page-placement.test.ts src/main/runtime/browser-host-lease-registry.test.ts",
2861+
"result": "passed",
2862+
"durationSeconds": 0.2,
2863+
"summary": "Two files passed 17 exact runtime/epoch/host/page authority, live-lease fencing, monotonic replacement, invalid-identity, default 256-placement capacity, and stale-cleanup tests."
2864+
},
28442865
{
28452866
"date": "2026-08-14",
28462867
"runner": "local",

src/main/runtime/browser-host-lease-registry.ts

Lines changed: 28 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import { randomUUID } from 'node:crypto'
22
import { BrowserExecutionHostGrantRegistry } from './browser-execution-host-grant-registry'
3+
import {
4+
BrowserHostPagePlacementRegistry,
5+
type BrowserClientPageAuthority,
6+
type RuntimeBrowserClientPlacement,
7+
type RuntimeBrowserPlacement
8+
} from './browser-host-page-placement'
39
import {
410
createBrowserHostFence,
511
type BrowserHostFence,
@@ -11,14 +17,7 @@ const MAX_BROWSER_HOSTS_PER_CONNECTION = 1
1117
// Why: tolerate brief desktop restart/update overlap while keeping one paired identity bounded.
1218
const MAX_BROWSER_HOSTS_PER_PAIRED_DEVICE = 4
1319

14-
export type RuntimeBrowserPlacement =
15-
| { kind: 'server' }
16-
| {
17-
kind: 'client'
18-
browserHostClientId: string
19-
browserHostGeneration: number
20-
pageHostGeneration: number
21-
}
20+
export type { RuntimeBrowserPlacement } from './browser-host-page-placement'
2221

2322
export type BrowserHostLease = Readonly<{
2423
authorityRuntimeId: string
@@ -68,16 +67,18 @@ export class BrowserHostLeaseRegistry {
6867
readonly authorityRuntimeId: string
6968
readonly authorityEpoch: string
7069
private nextHostGeneration = 1
71-
// Why: a global counter lets closed page IDs leave no tombstone without reusing a stale generation.
72-
private nextPageGeneration = 1
7370
private nextTunnelGeneration = 1
7471
private readonly leasesByClientId = new Map<string, LeaseState>()
7572
private readonly routesByKey = new Map<string, RouteState>()
76-
private readonly placementsByPageId = new Map<string, RuntimeBrowserPlacement>()
73+
private readonly pagePlacements: BrowserHostPagePlacementRegistry
7774

7875
constructor(options: { authorityRuntimeId: string; authorityEpoch?: string }) {
7976
this.authorityRuntimeId = options.authorityRuntimeId
8077
this.authorityEpoch = options.authorityEpoch ?? randomUUID()
78+
this.pagePlacements = new BrowserHostPagePlacementRegistry({
79+
authorityRuntimeId: this.authorityRuntimeId,
80+
authorityEpoch: this.authorityEpoch
81+
})
8182
}
8283

8384
attach(input: {
@@ -175,33 +176,35 @@ export class BrowserHostLeaseRegistry {
175176
}
176177

177178
placeServerPage(browserPageId: string): RuntimeBrowserPlacement {
178-
const placement = Object.freeze({ kind: 'server' as const })
179-
this.placementsByPageId.set(browserPageId, placement)
180-
return placement
179+
return this.pagePlacements.placeServerPage(browserPageId)
181180
}
182181

183182
placeClientPage(browserPageId: string, browserHostClientId?: string): RuntimeBrowserPlacement {
184183
const lease = this.select(browserHostClientId)
185-
const pageHostGeneration = this.takePageGeneration()
186-
const placement = Object.freeze({
187-
kind: 'client' as const,
184+
return this.pagePlacements.placeClientPage(browserPageId, {
188185
browserHostClientId: lease.browserHostClientId,
189-
browserHostGeneration: lease.browserHostGeneration,
190-
pageHostGeneration
186+
browserHostGeneration: lease.browserHostGeneration
191187
})
192-
this.placementsByPageId.set(browserPageId, placement)
188+
}
189+
190+
requireClientPage(authority: BrowserClientPageAuthority): RuntimeBrowserClientPlacement {
191+
const placement = this.pagePlacements.requireClientPage(authority)
192+
const lease = this.leasesByClientId.get(authority.browserHostClientId)
193+
if (!lease) {
194+
throw new Error('browser_host_lease_required')
195+
}
196+
if (lease.lease.browserHostGeneration !== authority.browserHostGeneration) {
197+
throw new Error('browser_host_lease_stale')
198+
}
193199
return placement
194200
}
195201

196202
getPlacement(browserPageId: string): RuntimeBrowserPlacement | undefined {
197-
return this.placementsByPageId.get(browserPageId)
203+
return this.pagePlacements.getPlacement(browserPageId)
198204
}
199205

200206
retirePage(browserPageId: string, expected: RuntimeBrowserPlacement): boolean {
201-
if (this.placementsByPageId.get(browserPageId) !== expected) {
202-
return false
203-
}
204-
return this.placementsByPageId.delete(browserPageId)
207+
return this.pagePlacements.retirePage(browserPageId, expected)
205208
}
206209

207210
openTunnel(
@@ -300,15 +303,6 @@ export class BrowserHostLeaseRegistry {
300303
}
301304
return value
302305
}
303-
304-
private takePageGeneration(): number {
305-
const value = this.nextPageGeneration
306-
if (value > MAX_GENERATION) {
307-
throw new Error('browser_page_generation_exhausted')
308-
}
309-
this.nextPageGeneration += 1
310-
return value
311-
}
312306
}
313307

314308
const registries = new WeakMap<object, BrowserHostLeaseRegistry>()

0 commit comments

Comments
 (0)