import * as plugins from './plugins.js'; import { controllerMcpBrowserMaximumImageBytes, controllerMcpBrowserOperationTimeoutMs, type TControllerMcpBrowserActionResult, } from '../ts_interfaces/mcpworkspacerequests.js'; import type { IControllerResourceRuntimeHost } from './classes.resourcecoordinator.js'; import { FlexBrowserChannelNotificationUnavailableError } from './classes.flexsupervisor.js'; import type { IControllerResourceDocument } from './interfaces.projects.js'; import { flexIpcMaximumBrowserChannels, type IFlexBrowserChannelBinding, } from './interfaces.flexipc.js'; import { controllerBrowserViewClosedErrorCode, controllerBrowserViewResourceChangedErrorCode, type TControllerBrowserViewCloseCode, } from '../ts_interfaces/browsertransport.js'; import { findSessionAttachment, hasBrowserSessionMember, resourceBrowserSessionIds, } from './functions.resourceattachments.js'; const maximumPendingFlexCapabilityIssuances = 128; const maximumActiveHumanBrowserViews = 64; const maximumTrackedCapabilityRevocations = 512; const maximumCleanupOperationWaiters = 512; const browserCapabilityAuthorizationTimeoutMs = 10_000; /** * A rebind fences for as long as the sessions it removes take to quiesce, so a handful of short * retries covers it without letting a pathological rebind loop spin: the operation deadline on the * caller's signal still ends the whole action. */ const maximumBrowserActionBusyRetries = 4; const browserActionBusyRetryDelayMs = 50; const browserResourceCleanupTimeoutMs = 40_000; /** * Screencast policy for every live browser session. Chrome encodes JPEG at * this quality and scales frames to fit the bounds in device pixels, so a * high-DPI viewer can neither produce frames near the runtime's 4 MiB frame * cap nor spend the client's decode budget on pixels the canvas cannot show. * Every produced frame reaches subscribers; slow human transports retain only * their newest unsent frame. Idle pages produce none. */ export const controllerBrowserScreencastOptions: Readonly< Required > = Object.freeze({ quality: 70, maxWidth: 2560, maxHeight: 1600, everyNthFrame: 1, firstFrameTimeoutMs: 5_000, }); /** Waits out a retry backoff, giving the abort up immediately and releasing its timer. */ const delayWithSignal = async ( delayMsArg: number, signalArg: AbortSignal, ): Promise => new Promise((resolve, reject) => { const finish = (settle: () => void): void => { clearTimeout(timer); signalArg.removeEventListener('abort', abort); settle(); }; const abort = (): void => finish(() => reject( signalArg.reason ?? new DOMException('The operation was aborted.', 'AbortError'), )); const timer = setTimeout(() => finish(resolve), delayMsArg); signalArg.addEventListener('abort', abort, { once: true }); if (signalArg.aborted) abort(); }); const waitForSignal = async ( promiseArg: Promise, signalArg: AbortSignal, ): Promise => new Promise((resolve, reject) => { const abort = () => { signalArg.removeEventListener('abort', abort); reject(signalArg.reason ?? new DOMException('The operation was aborted.', 'AbortError')); }; signalArg.addEventListener('abort', abort, { once: true }); if (signalArg.aborted) { abort(); return; } void promiseArg.then( (value) => { signalArg.removeEventListener('abort', abort); resolve(value); }, (errorArg) => { signalArg.removeEventListener('abort', abort); reject(errorArg); }, ); }); const waitForCleanupDeadline = async ( promiseArg: Promise, deadlineAtArg: number, messageArg: string, signalArg?: AbortSignal, ): Promise => new Promise((resolve, reject) => { let timeout: NodeJS.Timeout | undefined; let settled = false; const finish = (operationArg: () => void) => { if (settled) return; settled = true; if (timeout) clearTimeout(timeout); signalArg?.removeEventListener('abort', abort); operationArg(); }; const abort = () => finish(() => reject( signalArg?.reason ?? new DOMException('The operation was aborted.', 'AbortError'), )); void promiseArg.then( (value) => finish(() => resolve(value)), (errorArg) => finish(() => reject(errorArg)), ); if (signalArg?.aborted) { abort(); return; } signalArg?.addEventListener('abort', abort, { once: true }); const remainingMs = deadlineAtArg - Date.now(); if (remainingMs <= 0) { finish(() => reject(new Error(messageArg))); return; } timeout = setTimeout(() => finish(() => reject(new Error(messageArg))), remainingMs); }); export interface IControllerBrowserResourceHostOptions { videoBackend?: 'chromium' | 'native'; runtimeDirectory: string; authorizeCapability: plugins.browserRuntime.IBrowserRuntimeOptions['authorizeCapability']; /** * Closes the views a resource owns before the host mutates or tears down * its runtime. Every teardown reaches the runtime through this host, so * announcing here covers the paths that never touch a controller request * handler. Human views survive task attachment changes; resource teardown * still announces closure. Without it the streams die under the client, which * reads an intentional teardown as a transport fault, spends its whole * recovery budget, and reports that the view cannot be recovered. */ closeResourceViews?( projectIdArg: string, resourceIdArg: string, closeCodeArg: TControllerBrowserViewCloseCode, signalArg: AbortSignal, ): Promise; isFlexRunActive?(bindingArg: IFlexBrowserChannelBinding, runIdArg: string): boolean; beforeOperation?: plugins.browserRuntime.IBrowserRuntimeOptions['beforeOperation']; audit?: plugins.browserRuntime.IBrowserRuntimeOptions['audit']; } export interface IControllerBrowserHumanView { lease: plugins.browserRuntime.BrowserRuntimeLease; activate(listenerArg: (eventArg: plugins.browserRuntime.TBrowserRuntimeEvent) => void): Promise; close(): Promise; } export interface IControllerFlexBrowserChannel { binding: IFlexBrowserChannelBinding; capabilityToken: string; } interface IActiveFlexBrowserChannel { binding: IFlexBrowserChannelBinding; runId: string; capabilityId: string; inbound: plugins.stream.PassThrough; peer: plugins.browserRuntime.BrowserRuntimeFramedServerPeer; outbound: plugins.stream.Writable; notifyChildClosed(): Promise; notificationRequired: boolean; notificationDelivered: boolean; notificationOperation?: IObservedCleanupOperation; closing: boolean; peerCloseOperation?: IObservedCleanupOperation; peerClosed: boolean; capabilityRevoked: boolean; } interface IFlexBrowserAuthorization { binding: IFlexBrowserChannelBinding; runId: string; } interface IFlexBrowserChannelAdmission { projectId: string; sessionId: string; runId: string; peerId: string; abortController: AbortController; settled: Promise; resolveSettled(): void; } interface IHumanBrowserViewAdmission { abortController: AbortController; settled: Promise; resolveSettled(): void; } interface IMcpBrowserActionAdmission extends IHumanBrowserViewAdmission { binding: plugins.browserRuntime.IBrowserMcpAuthenticatedBinding; } type TBrowserCapabilityOwner = 'flex' | 'human' | 'mcp'; type TCleanupOperationOutcome = | { status: 'fulfilled' } | { status: 'rejected'; reason: unknown }; interface IObservedCleanupOperation { outcome?: TCleanupOperationOutcome; waiters: Set<(outcomeArg: TCleanupOperationOutcome) => void>; } interface ICapabilityRevocationOperation extends IObservedCleanupOperation { runtime: plugins.browserRuntime.BrowserRuntime; owner: TBrowserCapabilityOwner; onFulfilled: Set<() => void>; } interface IRuntimeStopOperation extends IObservedCleanupOperation { runtime: plugins.browserRuntime.BrowserRuntime; } const observeCleanupOperation = ( operationArg: () => Promise, onSettledArg?: (outcomeArg: TCleanupOperationOutcome) => void, ): IObservedCleanupOperation => { const operation: IObservedCleanupOperation = { waiters: new Set() }; const settle = (outcomeArg: TCleanupOperationOutcome) => { if (operation.outcome) return; operation.outcome = outcomeArg; onSettledArg?.(outcomeArg); const waiters = [...operation.waiters]; operation.waiters.clear(); for (const waiter of waiters) waiter(outcomeArg); }; void Promise.resolve().then(operationArg).then( () => settle({ status: 'fulfilled' }), (reason) => settle({ status: 'rejected', reason }), ); return operation; }; const waitForObservedCleanup = async ( operationArg: IObservedCleanupOperation, deadlineAtArg: number, messageArg: string, signalArg?: AbortSignal, ): Promise => { if (operationArg.outcome) return operationArg.outcome; signalArg?.throwIfAborted(); const remainingMs = deadlineAtArg - Date.now(); if (remainingMs <= 0) throw new Error(messageArg); if (operationArg.waiters.size >= maximumCleanupOperationWaiters) { throw new Error('Browser cleanup waiter capacity was reached.'); } return new Promise((resolve, reject) => { let timeout: NodeJS.Timeout | undefined; let settled = false; const finish = (finishArg: () => void) => { if (settled) return; settled = true; if (timeout) clearTimeout(timeout); operationArg.waiters.delete(complete); signalArg?.removeEventListener('abort', abort); finishArg(); }; const complete = (outcomeArg: TCleanupOperationOutcome) => { finish(() => resolve(outcomeArg)); }; const abort = () => finish(() => reject( signalArg?.reason ?? new DOMException('The operation was aborted.', 'AbortError'), )); operationArg.waiters.add(complete); signalArg?.addEventListener('abort', abort, { once: true }); if (operationArg.outcome) { complete(operationArg.outcome); return; } if (signalArg?.aborted) { abort(); return; } timeout = setTimeout(() => finish(() => reject(new Error(messageArg))), remainingMs); }); }; export class ControllerBrowserResourceHost implements IControllerResourceRuntimeHost { private runtime?: plugins.browserRuntime.BrowserRuntime; private started = false; private unavailable = false; private stopping = false; private cleanupTimeoutMs = browserResourceCleanupTimeoutMs; private startPromise?: Promise; private stopPromise?: Promise; private readonly flexAuthorizationBindings = new Map(); private readonly flexChannels = new Map(); private readonly flexChannelAdmissions = new Map(); private readonly capabilityRevocationOperations = new Map(); private readonly lateCapabilityIssuanceTasks = new Map, TBrowserCapabilityOwner>(); private readonly humanViews = new Set(); private readonly humanViewAdmissions = new Set(); private readonly mcpActionAdmissions = new Map(); private admissionAbortController = new AbortController(); private readonly runtimeStopOperations = new WeakMap< plugins.browserRuntime.BrowserRuntime, IRuntimeStopOperation >(); constructor(private readonly options: IControllerBrowserResourceHostOptions) {} public async start(signalArg?: AbortSignal): Promise { if (this.started || this.unavailable) return; if (this.startPromise) return await waitForSignal( this.startPromise, signalArg ?? new AbortController().signal, ); if (this.stopping) throw new Error('Browser resource host cleanup is incomplete.'); if (this.admissionAbortController.signal.aborted) { this.admissionAbortController = new AbortController(); } const startPromise = this.performStart(signalArg); this.startPromise = startPromise; try { await startPromise; } finally { if (this.startPromise === startPromise) this.startPromise = undefined; } } /** Options for the runtime this host owns; the screencast policy is fixed. */ private runtimeOptions(): plugins.browserRuntime.IBrowserRuntimeOptions { return { runtimeDirectory: this.options.runtimeDirectory, authorizationTimeoutMs: browserCapabilityAuthorizationTimeoutMs, authorizeCapability: this.options.authorizeCapability, screencast: { ...controllerBrowserScreencastOptions }, video: { backend: this.options.videoBackend ?? 'chromium', maxFrameRate: this.options.videoBackend === 'native' ? 30 : 60 }, devTools: true, ...(this.options.beforeOperation === undefined ? {} : { beforeOperation: this.options.beforeOperation, }), ...(this.options.audit === undefined ? {} : { audit: this.options.audit }), }; } private async performStart(signalArg?: AbortSignal): Promise { signalArg?.throwIfAborted(); const runtime = new plugins.browserRuntime.BrowserRuntime(this.runtimeOptions()); this.runtime = runtime; try { await (signalArg ? waitForSignal(runtime.start(), signalArg) : runtime.start()); signalArg?.throwIfAborted(); if (this.stopping) throw new Error('Browser resource host cleanup began during startup.'); this.started = true; } catch (errorArg) { if ( errorArg instanceof plugins.browserRuntime.BrowserRuntimeError && (errorArg.code === 'CONFINEMENT_FAILED' || errorArg.code === 'INVALID_INPUT') ) { await runtime.stop().catch((cleanupErrorArg) => { throw new AggregateError( [errorArg, cleanupErrorArg], 'BrowserRuntime startup and cleanup failed.', ); }); if (this.runtime === runtime) this.runtime = undefined; this.unavailable = true; return; } await runtime.stop().catch((cleanupErrorArg) => { throw new AggregateError( [errorArg, cleanupErrorArg], 'BrowserRuntime startup and cleanup failed.', ); }); if (this.runtime === runtime) this.runtime = undefined; throw errorArg; } } public async stop(): Promise { if (this.stopPromise) return this.stopPromise; const stopPromise = this.performStop(); this.stopPromise = stopPromise; try { await stopPromise; } finally { if (this.stopPromise === stopPromise) this.stopPromise = undefined; } } private async performStop(): Promise { const runtime = this.runtime; if (!runtime) { if ( this.humanViews.size > 0 || this.flexChannels.size > 0 || this.flexChannelAdmissions.size > 0 || this.humanViewAdmissions.size > 0 || this.mcpActionAdmissions.size > 0 || this.capabilityRevocationOperations.size > 0 || this.lateCapabilityIssuanceTasks.size > 0 ) throw new Error('Browser resource host cleanup has no runtime owner.'); this.stopping = false; return; } this.stopping = true; if (!this.admissionAbortController.signal.aborted) { this.admissionAbortController.abort(new Error('Browser resource host is stopping.')); } const deadlineAt = Date.now() + this.cleanupTimeoutMs; const cleanupErrors: unknown[] = []; for (const cleanup of [ () => this.cancelAndDrainFlexChannelAdmissions(deadlineAt), () => this.drainHumanViewAdmissions(deadlineAt), () => this.drainMcpActionAdmissions(deadlineAt), () => this.closeAllHumanViews(deadlineAt), () => this.closeAllFlexChannels(undefined, deadlineAt), () => this.drainLateCapabilityIssuanceTasks(deadlineAt), () => this.retryPendingCapabilityRevocations(deadlineAt), ]) { await cleanup().catch((errorArg) => cleanupErrors.push(errorArg)); } if (cleanupErrors.length > 0) { throw new AggregateError(cleanupErrors, 'Browser resource host cleanup is incomplete.'); } await this.stopRuntime(runtime, deadlineAt); if (this.runtime === runtime) this.runtime = undefined; this.started = false; this.stopping = false; } public authorizeFlexCapability( requestArg: plugins.browserRuntime.TBrowserCapabilityAuthorizationRequest, ): boolean { if (requestArg.role !== 'agent' || requestArg.source !== 'flex') return false; const expected = this.flexAuthorizationBindings.get(requestArg.channelId); const requestGeneration = requestArg as typeof requestArg & { sessionGenerationId?: unknown; sessionGenerationSequence?: unknown; }; const carriesGenerationId = Object.hasOwn(requestGeneration, 'sessionGenerationId'); const carriesGenerationSequence = Object.hasOwn( requestGeneration, 'sessionGenerationSequence', ); return expected !== undefined && carriesGenerationId === carriesGenerationSequence && (!carriesGenerationId || ( expected.binding.sessionGenerationId === requestGeneration.sessionGenerationId && expected.binding.sessionGenerationSequence === requestGeneration.sessionGenerationSequence )) && this.isFlexRunActive(expected.binding, expected.runId) && expected.binding.projectId === requestArg.projectId && expected.binding.browserResourceId === requestArg.browserResourceId && expected.binding.attachmentAuthorityId === requestArg.attachmentAuthorityId && expected.binding.attachmentRevision === requestArg.attachmentRevision && expected.binding.actorId === requestArg.actorId && expected.binding.peerId === requestArg.peerId && expected.binding.scopeId === requestArg.scopeId && expected.binding.channelId === requestArg.channelId && expected.binding.runId === requestArg.runId && expected.binding.sessionId.harnessId === requestArg.sessionId.harnessId && expected.binding.sessionId.nativeId === requestArg.sessionId.nativeId; } public authorizeMcpCapability( requestArg: plugins.browserRuntime.TBrowserCapabilityAuthorizationRequest, ): boolean { if (requestArg.role !== 'agent' || requestArg.source !== 'mcp' || this.stopping) return false; const admission = this.mcpActionAdmissions.get(requestArg.peerId); const expected = admission?.binding; return expected !== undefined && !admission!.abortController.signal.aborted && expected.projectId === requestArg.projectId && expected.browserResourceId === requestArg.browserResourceId && expected.attachmentAuthorityId === requestArg.attachmentAuthorityId && expected.attachmentRevision === requestArg.attachmentRevision && expected.actorId === requestArg.actorId && expected.sessionId.harnessId === requestArg.sessionId.harnessId && expected.sessionId.nativeId === requestArg.sessionId.nativeId; } /** * `actingSessionId` is the conversation the caller acts for, proved upstream against the * attachment set. It is not read off the resource: a resource may be attached to several * conversations, and the binding must name the one this action belongs to. It is a qualified * BrowserRuntime session — an AGL chat, or the `claude` conversation that owns an attached * terminal — and is checked against this resource's own binding here as well. */ public async executeMcpAction(inputArg: { resource: IControllerResourceDocument; actingSessionId: plugins.browserRuntime.TQualifiedBrowserSessionId; action: plugins.browserRuntime.TBrowserAgentAction; signal: AbortSignal; }): Promise { const runtime = this.runtime; const resource = inputArg.resource; if (!runtime || !this.started || this.stopping || resource.kind !== 'browser' || resource.lifecycle !== 'active' || resource.pendingAttachment !== undefined || !hasBrowserSessionMember(resource, inputArg.actingSessionId)) { throw new Error('The attached browser is unavailable.'); } if (this.mcpActionAdmissions.size >= maximumPendingFlexCapabilityIssuances) { throw new Error('Browser action capacity was reached.'); } const action = plugins.browserRuntime.validateAgentAction(inputArg.action); const signal = AbortSignal.any([ inputArg.signal, this.admissionAbortController.signal, AbortSignal.timeout(controllerMcpBrowserOperationTimeoutMs), ]); signal.throwIfAborted(); // BrowserRuntime answers `BUSY` while an attachment rebind is fencing: the call did not // happen and the same call succeeds once the rebind settles. That is now reachable in normal // use, because another conversation attaching to or detaching from this browser rebinds it // while this action is being admitted. Every attempt is a whole admission — a fresh peer, // capability and lease, fully torn down by the attempt's own cleanup — so a retry re-runs a // call that was refused before dispatch and never repeats a click. for (let attempt = 0; ; attempt += 1) { try { return await this.performMcpActionAttempt({ runtime, resource, actingSessionId: inputArg.actingSessionId, action, signal, }); } catch (errorArg) { if ( attempt >= maximumBrowserActionBusyRetries || !(errorArg instanceof plugins.browserRuntime.BrowserRuntimeError) || errorArg.code !== 'BUSY' ) throw errorArg; await delayWithSignal(browserActionBusyRetryDelayMs, signal); } } } private async performMcpActionAttempt(inputArg: { runtime: plugins.browserRuntime.BrowserRuntime; resource: IControllerResourceDocument; actingSessionId: plugins.browserRuntime.TQualifiedBrowserSessionId; action: plugins.browserRuntime.TBrowserAgentAction; signal: AbortSignal; }): Promise { const { runtime, resource, action, signal } = inputArg; if (this.mcpActionAdmissions.size >= maximumPendingFlexCapabilityIssuances) { throw new Error('Browser action capacity was reached.'); } let resolveSettled!: () => void; const binding: plugins.browserRuntime.IBrowserMcpAuthenticatedBinding = { projectId: resource.projectId, browserResourceId: resource.id, attachmentAuthorityId: resource.attachmentAuthorityId, attachmentRevision: resource.attachmentRevision, actorId: 'mcp', peerId: `mcp-${plugins.crypto.randomUUID()}`, role: 'agent', source: 'mcp', sessionId: { harnessId: inputArg.actingSessionId.harnessId, nativeId: inputArg.actingSessionId.nativeId, }, }; const admission: IMcpBrowserActionAdmission = { binding, abortController: new AbortController(), settled: new Promise((resolve) => { resolveSettled = resolve; }), resolveSettled: () => resolveSettled(), }; const abort = () => admission.abortController.abort(signal.reason); signal.addEventListener('abort', abort, { once: true }); if (signal.aborted) abort(); this.mcpActionAdmissions.set(binding.peerId, admission); let issuance: ReturnType | undefined; let capabilityId: string | undefined; let operationError: unknown; let result: TControllerMcpBrowserActionResult | undefined; try { await this.retryPendingCapabilityRevocations( Date.now() + browserCapabilityAuthorizationTimeoutMs, admission.abortController.signal, ); if (this.lateCapabilityIssuanceTasks.size >= maximumPendingFlexCapabilityIssuances) { throw new Error('Browser capability issuance capacity was reached.'); } issuance = runtime.issueCapability({ ...binding, disconnect: async () => { admission.abortController.abort(new Error('Browser action authority ended.')); }, }); const issued = await waitForSignal(issuance, admission.abortController.signal); issuance = undefined; capabilityId = issued.capabilityId; const lease = await runtime.acquireLease({ ...binding, capabilityToken: issued.capabilityToken, signal: admission.abortController.signal, }); const actionResult = plugins.browserRuntime.validateAgentActionResult( await lease.executeAgentAction(action, { signal: admission.abortController.signal }), ); admission.abortController.signal.throwIfAborted(); if (actionResult.action === 'screenshot') { try { const { mimeType, size, artifactId } = actionResult.artifact; if (size > controllerMcpBrowserMaximumImageBytes) { throw new plugins.typedrequest.TypedResponseError( 'The screenshot exceeds 512 KiB; request JPEG with lower quality.', { code: 'browser_image_limit' }, ); } if (mimeType !== 'image/jpeg' && mimeType !== 'image/png') { throw new Error('The browser returned an unsupported image format.'); } const bytes = await lease.readArtifact(artifactId); admission.abortController.signal.throwIfAborted(); if (bytes.byteLength !== size || bytes.byteLength > controllerMcpBrowserMaximumImageBytes) { throw new Error('The browser image size is invalid.'); } result = { action: 'screenshot', image: { mimeType, data: Buffer.from(bytes).toString('base64') } }; } finally { await lease.deleteArtifact(actionResult.artifact.artifactId); } } else { result = actionResult; } } catch (errorArg) { operationError = errorArg; } finally { admission.abortController.abort(new Error('Browser action completed.')); const cleanupErrors: unknown[] = []; try { if (issuance) { this.trackLateCapabilityIssuance(runtime, issuance, 'mcp'); } else if (capabilityId) { this.trackCapabilityRevocation(runtime, capabilityId, 'mcp'); await this.awaitCapabilityRevocation(capabilityId, Date.now() + this.cleanupTimeoutMs); } } catch (errorArg) { cleanupErrors.push(errorArg); } signal.removeEventListener('abort', abort); this.mcpActionAdmissions.delete(binding.peerId); admission.resolveSettled(); if (cleanupErrors.length) throw new AggregateError( [...(operationError === undefined ? [] : [operationError]), ...cleanupErrors], 'Browser action cleanup is incomplete.', ); } if (operationError !== undefined) throw operationError; if (!result) throw new Error('The browser action returned no result.'); return result; } private async drainMcpActionAdmissions(deadlineAtArg: number): Promise { const admissions = [...this.mcpActionAdmissions.values()]; for (const admission of admissions) { admission.abortController.abort(new Error('Browser resource host is stopping.')); } await Promise.all(admissions.map((admission) => waitForCleanupDeadline( admission.settled, deadlineAtArg, 'Browser action cleanup timed out.', ))); } public async openFlexChannel(inputArg: { resource: IControllerResourceDocument; scopeId: string; sessionId: string; sessionGenerationId: string; sessionGenerationSequence: number; runId: string; channelId: string; peerId: string; signal?: AbortSignal; sendFrame(channelIdArg: string, bytesArg: Buffer): Promise; notifyClosed(channelIdArg: string): Promise; }): Promise { const runtime = this.runtime; const resource = inputArg.resource; if ( !runtime || !this.started || resource.kind !== 'browser' || resource.lifecycle !== 'active' || resource.pendingAttachment !== undefined || findSessionAttachment(resource, { harnessId: 'flex', nativeId: inputArg.sessionId, }) === undefined || resource.projectId !== inputArg.scopeId || typeof inputArg.sessionGenerationId !== 'string' || inputArg.sessionGenerationId.length === 0 || !Number.isSafeInteger(inputArg.sessionGenerationSequence) || inputArg.sessionGenerationSequence < 1 || this.flexChannels.has(inputArg.channelId) || this.flexChannelAdmissions.has(inputArg.channelId) || this.stopping ) throw new Error('The Flex browser channel is unavailable.'); if ( this.flexChannels.size + this.flexChannelAdmissions.size >= flexIpcMaximumBrowserChannels ) throw new Error('The Flex browser channel capacity was reached.'); inputArg.signal?.throwIfAborted(); this.admissionAbortController.signal.throwIfAborted(); let resolveAdmissionSettled!: () => void; const admission: IFlexBrowserChannelAdmission = { projectId: resource.projectId, sessionId: inputArg.sessionId, runId: inputArg.runId, peerId: inputArg.peerId, abortController: new AbortController(), settled: new Promise((resolve) => { resolveAdmissionSettled = resolve; }), resolveSettled: () => resolveAdmissionSettled(), }; const abortAdmission = (signalArg: AbortSignal) => { if (!admission.abortController.signal.aborted) { admission.abortController.abort( signalArg.reason ?? new DOMException('The operation was aborted.', 'AbortError'), ); } }; const abortFromInput = () => abortAdmission(inputArg.signal!); const abortFromHost = () => abortAdmission(this.admissionAbortController.signal); inputArg.signal?.addEventListener('abort', abortFromInput, { once: true }); this.admissionAbortController.signal.addEventListener('abort', abortFromHost, { once: true }); this.flexChannelAdmissions.set(inputArg.channelId, admission); if (inputArg.signal?.aborted) abortFromInput(); if (this.admissionAbortController.signal.aborted) abortFromHost(); const binding: IFlexBrowserChannelBinding = { projectId: resource.projectId, browserResourceId: resource.id, attachmentAuthorityId: resource.attachmentAuthorityId, attachmentRevision: resource.attachmentRevision, actorId: `flex-run:${plugins.crypto.createHash('sha256') .update(JSON.stringify([ inputArg.scopeId, inputArg.sessionId, inputArg.sessionGenerationId, inputArg.sessionGenerationSequence, inputArg.runId, ])) .digest('hex')}`, peerId: inputArg.peerId, role: 'agent', source: 'flex', scopeId: inputArg.scopeId, channelId: inputArg.channelId, runId: inputArg.runId, sessionId: { harnessId: 'flex', nativeId: inputArg.sessionId }, sessionGenerationId: inputArg.sessionGenerationId, sessionGenerationSequence: inputArg.sessionGenerationSequence, }; const inbound = new plugins.stream.PassThrough(); const outbound = new plugins.stream.Writable({ write: (chunkArg, _encodingArg, callbackArg) => { const bytes = Buffer.from(chunkArg as Uint8Array); void inputArg.sendFrame(inputArg.channelId, bytes).then( () => callbackArg(), (errorArg) => callbackArg(errorArg instanceof Error ? errorArg : new Error('Frame send failed.')), ); }, destroy: (errorArg, callbackArg) => { const channel = this.flexChannels.get(inputArg.channelId); if (channel && !channel.closing) { // Stream destruction cannot await peer.close without deadlocking its own cleanup. void this.closeFlexChannel(inputArg.channelId, true).catch(() => undefined); } callbackArg(errorArg); }, }); this.flexAuthorizationBindings.set(inputArg.channelId, { binding, runId: inputArg.runId, }); let capabilityId = ''; let issuancePromise: ReturnType | undefined; let channelRegistered = false; try { admission.abortController.signal.throwIfAborted(); if (!this.isFlexRunActive(binding, inputArg.runId)) { throw new Error('The Flex browser run is no longer active.'); } await this.retryPendingCapabilityRevocations( Date.now() + browserCapabilityAuthorizationTimeoutMs, admission.abortController.signal, ); if (this.lateCapabilityIssuanceTasks.size >= maximumPendingFlexCapabilityIssuances) { throw new Error('Browser capability issuance capacity was reached.'); } issuancePromise = runtime.issueCapability({ ...binding, expiresInMs: 24 * 60 * 60 * 1000, }); const issued = await waitForSignal(issuancePromise, admission.abortController.signal); issuancePromise = undefined; capabilityId = issued.capabilityId; const peer = runtime.attachTrustedFramedPeer({ ...binding, readable: inbound, writable: outbound, }); this.flexChannels.set(inputArg.channelId, { binding, runId: inputArg.runId, capabilityId, inbound, peer, outbound, notifyChildClosed: () => inputArg.notifyClosed(inputArg.channelId), notificationRequired: true, notificationDelivered: false, closing: false, peerClosed: false, capabilityRevoked: false, }); channelRegistered = true; admission.abortController.signal.throwIfAborted(); if (!this.isFlexRunActive(binding, inputArg.runId)) { throw new Error('The Flex browser run is no longer active.'); } return { binding, capabilityToken: issued.capabilityToken }; } catch (errorArg) { const cleanupErrors: unknown[] = []; if (issuancePromise) { this.trackLateCapabilityIssuance(runtime, issuancePromise, 'flex'); issuancePromise = undefined; } if (channelRegistered) { const channel = this.flexChannels.get(inputArg.channelId); if (channel) channel.notificationRequired = false; await this.closeFlexChannel( inputArg.channelId, false, undefined, Date.now() + this.cleanupTimeoutMs, ).catch((cleanupErrorArg) => cleanupErrors.push(cleanupErrorArg)); } else { this.flexAuthorizationBindings.delete(inputArg.channelId); inbound.destroy(); outbound.destroy(); if (capabilityId) { this.trackCapabilityRevocation(runtime, capabilityId, 'flex'); await this.awaitCapabilityRevocation( capabilityId, Date.now() + this.cleanupTimeoutMs, ).catch((cleanupErrorArg) => cleanupErrors.push(cleanupErrorArg)); } } if (cleanupErrors.length > 0) { throw new AggregateError( [errorArg, ...cleanupErrors], 'Flex browser channel admission and cleanup failed.', ); } throw errorArg; } finally { inputArg.signal?.removeEventListener('abort', abortFromInput); this.admissionAbortController.signal.removeEventListener('abort', abortFromHost); this.flexChannelAdmissions.delete(inputArg.channelId); admission.resolveSettled(); } } public async receiveFlexFrame(channelIdArg: string, bytesArg: Buffer, peerIdArg: string): Promise { const channel = this.flexChannels.get(channelIdArg); if ( !channel || channel.binding.peerId !== peerIdArg || channel.closing || !this.isFlexRunActive(channel.binding, channel.runId) ) { if (channel && !channel.closing && !this.isFlexRunActive(channel.binding, channel.runId)) { void this.closeFlexChannel(channelIdArg, true).catch(() => undefined); } throw new Error('The Flex browser channel is unavailable.'); } await new Promise((resolve, reject) => { channel.inbound.write(bytesArg, (errorArg) => errorArg ? reject(errorArg) : resolve()); }); } public async closeFlexChannel( channelIdArg: string, notifyChildArg = false, peerIdArg?: string, deadlineAtArg = Date.now() + this.cleanupTimeoutMs, ): Promise { const channel = this.flexChannels.get(channelIdArg); if (!channel) return; if (peerIdArg !== undefined && channel.binding.peerId !== peerIdArg) { throw new Error('The Flex browser channel is unavailable.'); } if (notifyChildArg) channel.notificationRequired = true; else if (!channel.closing) channel.notificationRequired = false; channel.closing = true; this.flexAuthorizationBindings.delete(channelIdArg); const peerCleanup = (async () => { if (channel.peerClosed) return; channel.peerCloseOperation ??= observeCleanupOperation( () => channel.peer.close(), (outcomeArg) => { if (outcomeArg.status === 'fulfilled') channel.peerClosed = true; }, ); const outcome = await waitForObservedCleanup( channel.peerCloseOperation, deadlineAtArg, 'Flex browser peer cleanup timed out.', ); if (outcome.status === 'rejected') { channel.peerCloseOperation = undefined; throw outcome.reason; } })(); const capabilityCleanup = (async () => { if (channel.capabilityRevoked) return; const runtime = this.runtime; if (!runtime) throw new Error('BrowserRuntime is unavailable during channel cleanup.'); this.trackCapabilityRevocation( runtime, channel.capabilityId, 'flex', () => { channel.capabilityRevoked = true; }, ); await this.awaitCapabilityRevocation(channel.capabilityId, deadlineAtArg); channel.capabilityRevoked = true; })(); const notificationCleanup = (async () => { if (!channel.notificationRequired || channel.notificationDelivered) return; let operation = channel.notificationOperation; if (operation?.outcome?.status === 'rejected') { if (channel.notificationOperation === operation) channel.notificationOperation = undefined; operation = undefined; } if (!operation) { operation = observeCleanupOperation(() => channel.notifyChildClosed()); channel.notificationOperation = operation; } const outcome = await waitForObservedCleanup( operation, deadlineAtArg, 'Flex child channel notification timed out.', ); if (outcome.status === 'fulfilled') { channel.notificationDelivered = true; return; } if (outcome.reason instanceof FlexBrowserChannelNotificationUnavailableError) { channel.notificationDelivered = true; return; } if (channel.notificationOperation === operation) channel.notificationOperation = undefined; throw outcome.reason; })(); const results = await Promise.allSettled([ peerCleanup, capabilityCleanup, notificationCleanup, ]); if (channel.peerClosed && channel.capabilityRevoked) { channel.inbound.destroy(); channel.outbound.destroy(); if (!channel.notificationRequired || channel.notificationDelivered) { this.flexChannels.delete(channelIdArg); } } const errors = results .filter((result): result is PromiseRejectedResult => result.status === 'rejected') .map((result) => result.reason); if (errors.length > 0) { throw new AggregateError(errors, 'Flex browser channel cleanup is incomplete.'); } } public async closeFlexChannelFromPeer(inputArg: { channelId: string; peerId: string; sessionGenerationId: string; sessionGenerationSequence: number; }): Promise { const channel = this.flexChannels.get(inputArg.channelId); if ( !channel || channel.binding.peerId !== inputArg.peerId || channel.binding.sessionGenerationId !== inputArg.sessionGenerationId || channel.binding.sessionGenerationSequence !== inputArg.sessionGenerationSequence ) throw new Error('The Flex browser channel is unavailable.'); channel.notificationRequired = false; await this.closeFlexChannel(inputArg.channelId); } public async closeAllFlexChannels( peerIdArg?: string, deadlineAtArg = Date.now() + this.cleanupTimeoutMs, ): Promise { const admissions = [...this.flexChannelAdmissions.entries()].filter(([, admission]) => ( peerIdArg === undefined || admission.peerId === peerIdArg )); for (const [channelId, admission] of admissions) { this.flexAuthorizationBindings.delete(channelId); if (!admission.abortController.signal.aborted) { admission.abortController.abort(new Error('Flex browser channel admission was cancelled.')); } } const admissionResults = await Promise.allSettled(admissions.map(([, admission]) => ( waitForCleanupDeadline( admission.settled, deadlineAtArg, 'Flex browser channel admission cleanup timed out.', ) ))); const channels = [...this.flexChannels.values()].filter((channel) => ( peerIdArg === undefined || channel.binding.peerId === peerIdArg )); const results = await Promise.allSettled( channels.map((channel) => this.closeFlexChannel( channel.binding.channelId, true, undefined, deadlineAtArg, )), ); const errors = [...admissionResults, ...results] .filter((result): result is PromiseRejectedResult => result.status === 'rejected') .map((result) => result.reason); if (peerIdArg === undefined) { await this.drainLateCapabilityIssuanceTasks(deadlineAtArg, 'flex') .catch((errorArg) => errors.push(errorArg)); await this.retryPendingCapabilityRevocations(deadlineAtArg, undefined, 'flex') .catch((errorArg) => errors.push(errorArg)); } if (errors.length > 0) throw new AggregateError(errors, 'Flex browser channel cleanup is incomplete.'); } public async closeFlexChannelsForRun( projectIdArg: string, sessionIdArg: string, runIdArg: string, ): Promise { const deadlineAt = Date.now() + this.cleanupTimeoutMs; const admissions = [...this.flexChannelAdmissions.entries()].filter(([, admission]) => ( admission.projectId === projectIdArg && admission.sessionId === sessionIdArg && admission.runId === runIdArg )); for (const [channelId, admission] of admissions) { this.flexAuthorizationBindings.delete(channelId); if (!admission.abortController.signal.aborted) { admission.abortController.abort(new Error('Flex browser run ended.')); } } const admissionResults = await Promise.allSettled(admissions.map(([, admission]) => ( waitForCleanupDeadline( admission.settled, deadlineAt, 'Flex browser channel admission cleanup timed out.', ) ))); const channels = [...this.flexChannels.values()].filter((channel) => ( channel.binding.projectId === projectIdArg && channel.binding.sessionId.nativeId === sessionIdArg && channel.runId === runIdArg )); const channelResults = await Promise.allSettled(channels.map((channel) => ( this.closeFlexChannel(channel.binding.channelId, true, undefined, deadlineAt) ))); const errors = [...admissionResults, ...channelResults] .filter((result): result is PromiseRejectedResult => result.status === 'rejected') .map((result) => result.reason); if (errors.length > 0) { throw new AggregateError(errors, 'Flex browser run channel cleanup is incomplete.'); } } public async disconnectPeer(peerIdArg: string): Promise { await this.runtime?.disconnectPeer(peerIdArg); } public async openHumanView(inputArg: { resource: IControllerResourceDocument; actorId: string; peerId: string; signal: AbortSignal; onDisconnect(signalArg: AbortSignal): Promise | void; }): Promise { const runtime = this.runtime; if (!runtime || !this.started || inputArg.resource.kind !== 'browser' || this.stopping) { throw new Error('BrowserRuntime is unavailable.'); } if (this.humanViews.size + this.humanViewAdmissions.size >= maximumActiveHumanBrowserViews) { throw new Error('Human browser view capacity was reached.'); } inputArg.signal.throwIfAborted(); this.admissionAbortController.signal.throwIfAborted(); let resolveAdmissionSettled!: () => void; const admission: IHumanBrowserViewAdmission = { abortController: new AbortController(), settled: new Promise((resolve) => { resolveAdmissionSettled = resolve; }), resolveSettled: () => resolveAdmissionSettled(), }; const abortAdmission = (signalArg: AbortSignal) => { if (!admission.abortController.signal.aborted) { admission.abortController.abort( signalArg.reason ?? new DOMException('The operation was aborted.', 'AbortError'), ); } }; const abortFromInput = () => abortAdmission(inputArg.signal); const abortFromHost = () => abortAdmission(this.admissionAbortController.signal); inputArg.signal.addEventListener('abort', abortFromInput, { once: true }); this.admissionAbortController.signal.addEventListener('abort', abortFromHost, { once: true }); this.humanViewAdmissions.add(admission); if (inputArg.signal.aborted) abortFromInput(); if (this.admissionAbortController.signal.aborted) abortFromHost(); const binding = { projectId: inputArg.resource.projectId, browserResourceId: inputArg.resource.id, attachmentAuthorityId: inputArg.resource.attachmentAuthorityId, attachmentRevision: inputArg.resource.attachmentRevision, actorId: inputArg.actorId, peerId: inputArg.peerId, role: 'human' as const, source: 'human' as const, }; let humanView: IControllerBrowserHumanView | undefined; let subscription: plugins.browserRuntime.IBrowserRuntimeFrameSubscription | undefined; let activationPromise: Promise | undefined; let subscriptionCloseOperation: IObservedCleanupOperation | undefined; let subscriptionClosed = false; let capabilityRevoked = false; let closing = false; let runtimeDisconnectActive = false; let runtimeDisconnectComplete = false; let capabilityId = ''; let issuancePromise: ReturnType | undefined; const closeSubscription = async (deadlineAtArg: number): Promise => { if (subscriptionClosed || !subscription) return; subscriptionCloseOperation ??= observeCleanupOperation( () => subscription!.close(), (outcomeArg) => { if (outcomeArg.status === 'fulfilled') subscriptionClosed = true; }, ); const outcome = await waitForObservedCleanup( subscriptionCloseOperation, deadlineAtArg, 'Browser view subscription cleanup timed out.', ); if (outcome.status === 'rejected') { subscriptionCloseOperation = undefined; throw outcome.reason; } }; try { await this.retryPendingCapabilityRevocations( Date.now() + browserCapabilityAuthorizationTimeoutMs, admission.abortController.signal, ); if (this.lateCapabilityIssuanceTasks.size >= maximumPendingFlexCapabilityIssuances) { throw new Error('Browser capability issuance capacity was reached.'); } issuancePromise = runtime.issueCapability({ ...binding, disconnect: async (signalArg) => { closing = true; runtimeDisconnectActive = true; try { await inputArg.onDisconnect(signalArg); } finally { runtimeDisconnectActive = false; runtimeDisconnectComplete = true; subscriptionClosed = true; capabilityRevoked = true; if (humanView) this.humanViews.delete(humanView); } }, }); const issued = await waitForSignal(issuancePromise, admission.abortController.signal); issuancePromise = undefined; capabilityId = issued.capabilityId; const lease = await runtime.acquireLease({ ...binding, capabilityToken: issued.capabilityToken, signal: admission.abortController.signal, }); admission.abortController.signal.throwIfAborted(); humanView = { lease, activate: (listenerArg) => { if (closing) return Promise.reject(new Error('Browser view cleanup has begun.')); if (subscription && !subscriptionClosed) return Promise.resolve(); if (subscriptionClosed) { subscription = undefined; subscriptionCloseOperation = undefined; subscriptionClosed = false; } activationPromise ??= (async () => { let initialStateReceived = false; const candidate = await lease.subscribeEvents((eventArg) => { if (!initialStateReceived) { if (eventArg.type !== 'state') { throw new Error('BrowserRuntime did not publish an initial state event.'); } initialStateReceived = true; } listenerArg(eventArg); }, { includeFrames: false }); subscription = candidate; if (!initialStateReceived) { await closeSubscription(Date.now() + this.cleanupTimeoutMs); throw new Error('BrowserRuntime did not publish an initial state event.'); } if (closing) { await closeSubscription(Date.now() + this.cleanupTimeoutMs); throw new Error('Browser view cleanup began during activation.'); } })().finally(() => { if (!subscription || subscriptionClosed) activationPromise = undefined; }); return activationPromise; }, close: async () => { closing = true; const deadlineAt = Date.now() + this.cleanupTimeoutMs; if (runtimeDisconnectActive) { await closeSubscription(deadlineAt); return; } const errors: unknown[] = []; if (activationPromise) { await waitForCleanupDeadline( activationPromise.catch(() => undefined), deadlineAt, 'Browser view activation cleanup timed out.', ).catch((errorArg) => errors.push(errorArg)); } if (!subscriptionClosed) { if (subscription) { await closeSubscription(deadlineAt).catch((errorArg) => errors.push(errorArg)); } else { subscriptionClosed = true; } } if (!capabilityRevoked) { this.trackCapabilityRevocation( runtime, capabilityId, 'human', () => { capabilityRevoked = true; }, ); await this.awaitCapabilityRevocation(capabilityId, deadlineAt).then( () => { capabilityRevoked = true; }, (errorArg) => errors.push(errorArg), ); } if (runtimeDisconnectComplete || capabilityRevoked) subscriptionClosed = true; if (errors.length > 0) throw new AggregateError(errors, 'Browser view cleanup is incomplete.'); this.humanViews.delete(humanView!); }, }; this.humanViews.add(humanView); return humanView; } catch (errorArg) { const cleanupErrors: unknown[] = []; if (issuancePromise) { this.trackLateCapabilityIssuance(runtime, issuancePromise, 'human'); } else if (capabilityId && !capabilityRevoked) { this.trackCapabilityRevocation(runtime, capabilityId, 'human'); await this.awaitCapabilityRevocation( capabilityId, Date.now() + this.cleanupTimeoutMs, ).catch((cleanupErrorArg) => cleanupErrors.push(cleanupErrorArg)); } if (humanView) this.humanViews.delete(humanView); if (cleanupErrors.length > 0) { throw new AggregateError( [errorArg, ...cleanupErrors], 'Browser view admission and cleanup failed.', ); } throw errorArg; } finally { inputArg.signal.removeEventListener('abort', abortFromInput); this.admissionAbortController.signal.removeEventListener('abort', abortFromHost); this.humanViewAdmissions.delete(admission); admission.resolveSettled(); } } public async startResource( resourceArg: IControllerResourceDocument, signalArg: AbortSignal, ): Promise { signalArg.throwIfAborted(); await this.reconcileAttachment(resourceArg, signalArg); } public async stopResource( resourceArg: IControllerResourceDocument, signalArg: AbortSignal, ): Promise { signalArg.throwIfAborted(); if (!this.runtime || this.stopping) return; await this.closeResourceViews(resourceArg, controllerBrowserViewClosedErrorCode, signalArg); await this.runtime.terminateResource(resourceArg.projectId, resourceArg.id); } public isRunning(resourceArg: IControllerResourceDocument): boolean { if (!this.runtime || this.stopping) return false; const registration = this.runtime.listResources().find((candidate) => ( candidate.projectId === resourceArg.projectId && candidate.browserResourceId === resourceArg.id )); return registration?.incarnationStatus === 'running'; } public isAvailable(): boolean { return this.started && !this.stopping; } public async reconcileAttachment( resourceArg: IControllerResourceDocument, signalArg: AbortSignal, ): Promise { signalArg.throwIfAborted(); if (resourceArg.kind !== 'browser') return; if (this.unavailable) return; if (!this.runtime || this.stopping) throw new Error('BrowserRuntime is not started.'); const attachmentBinding = { attachmentAuthorityId: resourceArg.attachmentAuthorityId, attachmentRevision: resourceArg.attachmentRevision, sessionIds: resourceBrowserSessionIds(resourceArg), }; const existing = this.runtime.listResources().find((candidate) => ( candidate.projectId === resourceArg.projectId && candidate.browserResourceId === resourceArg.id )); if (!existing) { this.runtime.registerResource({ projectId: resourceArg.projectId, browserResourceId: resourceArg.id, attachmentBinding, }); return; } if (existing.attachmentBinding.attachmentAuthorityId !== attachmentBinding.attachmentAuthorityId) { await this.closeResourceViews( resourceArg, controllerBrowserViewResourceChangedErrorCode, signalArg, ); } await this.runtime.applyAttachmentBinding({ projectId: resourceArg.projectId, browserResourceId: resourceArg.id, attachmentBinding, }); } public async retireResource( resourceArg: IControllerResourceDocument, signalArg: AbortSignal, ): Promise { signalArg.throwIfAborted(); if (!this.runtime || this.stopping || resourceArg.kind !== 'browser') return; const registered = this.runtime.listResources().some((candidate) => ( candidate.projectId === resourceArg.projectId && candidate.browserResourceId === resourceArg.id )); if (!registered) { this.runtime.registerResource({ projectId: resourceArg.projectId, browserResourceId: resourceArg.id, attachmentBinding: { attachmentAuthorityId: resourceArg.attachmentAuthorityId, attachmentRevision: resourceArg.attachmentRevision, sessionIds: resourceBrowserSessionIds(resourceArg), }, }); } await this.closeResourceViews(resourceArg, controllerBrowserViewClosedErrorCode, signalArg); await this.runtime.retireResource(resourceArg.projectId, resourceArg.id); } /** Runs the controller's announced close for this resource, when one is wired. */ private async closeResourceViews( resourceArg: IControllerResourceDocument, closeCodeArg: TControllerBrowserViewCloseCode, signalArg: AbortSignal, ): Promise { if (resourceArg.kind !== 'browser') return; await this.options.closeResourceViews?.( resourceArg.projectId, resourceArg.id, closeCodeArg, signalArg, ); } public get availability(): 'available' | 'unavailable' { return this.started && !this.stopping ? 'available' : 'unavailable'; } private isFlexRunActive(bindingArg: IFlexBrowserChannelBinding, runIdArg: string): boolean { if (bindingArg.runId !== runIdArg) return false; return this.options.isFlexRunActive?.(bindingArg, runIdArg) ?? true; } private trackCapabilityRevocation( runtimeArg: plugins.browserRuntime.BrowserRuntime, capabilityIdArg: string, ownerArg: TBrowserCapabilityOwner, onFulfilledArg?: () => void, ): void { const existing = this.capabilityRevocationOperations.get(capabilityIdArg); if (existing && existing.owner !== ownerArg) { throw new Error('Browser capability cleanup ownership changed unexpectedly.'); } if (existing && existing.outcome?.status !== 'rejected') { if (onFulfilledArg) existing.onFulfilled.add(onFulfilledArg); return; } const onFulfilled = new Set(existing?.onFulfilled ?? []); if (onFulfilledArg) onFulfilled.add(onFulfilledArg); if (existing) this.capabilityRevocationOperations.delete(capabilityIdArg); if (this.capabilityRevocationOperations.size >= maximumTrackedCapabilityRevocations) { throw new Error('Browser capability cleanup capacity was reached.'); } const operation = observeCleanupOperation( () => runtimeArg.revokeCapability(capabilityIdArg), (outcomeArg) => { if (outcomeArg.status === 'fulfilled') { for (const callback of operation.onFulfilled) callback(); operation.onFulfilled.clear(); if (this.capabilityRevocationOperations.get(capabilityIdArg) === operation) { this.capabilityRevocationOperations.delete(capabilityIdArg); } } }, ) as ICapabilityRevocationOperation; operation.runtime = runtimeArg; operation.owner = ownerArg; operation.onFulfilled = onFulfilled; this.capabilityRevocationOperations.set(capabilityIdArg, operation); } private async awaitCapabilityRevocation( capabilityIdArg: string, deadlineAtArg: number, signalArg?: AbortSignal, ): Promise { const operation = this.capabilityRevocationOperations.get(capabilityIdArg); if (!operation) return; const outcome = await waitForObservedCleanup( operation, deadlineAtArg, 'Browser capability cleanup timed out.', signalArg, ); if (outcome.status === 'rejected') throw outcome.reason; } private trackLateCapabilityIssuance( runtimeArg: plugins.browserRuntime.BrowserRuntime, issuancePromiseArg: ReturnType, ownerArg: TBrowserCapabilityOwner, ): void { const task = (async () => { const issued = await issuancePromiseArg; this.trackCapabilityRevocation(runtimeArg, issued.capabilityId, ownerArg); await this.awaitCapabilityRevocation( issued.capabilityId, Date.now() + this.cleanupTimeoutMs, ); })(); this.lateCapabilityIssuanceTasks.set(task, ownerArg); void task.catch(() => undefined).finally(() => this.lateCapabilityIssuanceTasks.delete(task)); } private async closeAllHumanViews(deadlineAtArg: number): Promise { const results = await Promise.allSettled( [...this.humanViews].map((view) => waitForCleanupDeadline( view.close(), deadlineAtArg, 'Human browser view cleanup timed out.', )), ); const errors = results .filter((result): result is PromiseRejectedResult => result.status === 'rejected') .map((result) => result.reason); if (errors.length > 0) { throw new AggregateError(errors, 'Human browser view cleanup is incomplete.'); } } private async retryPendingCapabilityRevocations( deadlineAtArg: number, signalArg?: AbortSignal, ownerArg?: TBrowserCapabilityOwner, ): Promise { const entries = [...this.capabilityRevocationOperations.entries()].filter(([, operation]) => ( ownerArg === undefined || operation.owner === ownerArg )); for (const [capabilityId, operation] of entries) { if (operation.outcome?.status === 'rejected') { this.trackCapabilityRevocation(operation.runtime, capabilityId, operation.owner); } } const results = await Promise.allSettled( [...this.capabilityRevocationOperations.entries()] .filter(([, operation]) => ownerArg === undefined || operation.owner === ownerArg) .map(([capabilityId]) => ( this.awaitCapabilityRevocation(capabilityId, deadlineAtArg, signalArg) )), ); const errors = results .filter((result): result is PromiseRejectedResult => result.status === 'rejected') .map((result) => result.reason); if (errors.length > 0) { throw new AggregateError(errors, 'Pending browser capability cleanup is incomplete.'); } } private async cancelAndDrainFlexChannelAdmissions(deadlineAtArg: number): Promise { const admissions = [...this.flexChannelAdmissions.entries()]; for (const [channelId, admission] of admissions) { this.flexAuthorizationBindings.delete(channelId); if (!admission.abortController.signal.aborted) { admission.abortController.abort(new Error('Browser resource host is stopping.')); } } await Promise.all(admissions.map(([, admission]) => waitForCleanupDeadline( admission.settled, deadlineAtArg, 'Flex browser channel admission cleanup timed out.', ))); } private async drainHumanViewAdmissions(deadlineAtArg: number): Promise { const admissions = [...this.humanViewAdmissions]; for (const admission of admissions) { if (!admission.abortController.signal.aborted) { admission.abortController.abort(new Error('Browser resource host is stopping.')); } } await Promise.all(admissions.map((admission) => waitForCleanupDeadline( admission.settled, deadlineAtArg, 'Human browser view admission cleanup timed out.', ))); } private async drainLateCapabilityIssuanceTasks( deadlineAtArg: number, ownerArg?: TBrowserCapabilityOwner, ): Promise { const tasks = [...this.lateCapabilityIssuanceTasks.entries()] .filter(([, owner]) => ownerArg === undefined || owner === ownerArg) .map(([task]) => task); const results = await Promise.allSettled(tasks.map((task) => waitForCleanupDeadline( task, deadlineAtArg, 'Late browser capability issuance cleanup timed out.', ))); const errors = results .filter((result): result is PromiseRejectedResult => result.status === 'rejected') .map((result) => result.reason); if (errors.length > 0) { throw new AggregateError(errors, 'Late browser capability issuance cleanup is incomplete.'); } } private async stopRuntime( runtimeArg: plugins.browserRuntime.BrowserRuntime, deadlineAtArg: number, ): Promise { let operation = this.runtimeStopOperations.get(runtimeArg); if (operation?.outcome?.status === 'rejected') { this.runtimeStopOperations.delete(runtimeArg); operation = undefined; } if (!operation) { operation = observeCleanupOperation(() => runtimeArg.stop()) as IRuntimeStopOperation; operation.runtime = runtimeArg; this.runtimeStopOperations.set(runtimeArg, operation); } const outcome = await waitForObservedCleanup( operation, deadlineAtArg, 'BrowserRuntime shutdown timed out.', ); if (outcome.status === 'rejected') throw outcome.reason; } }