@modelprofile.com/browser-runtime 5.4.1 → 5.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modelprofile.com/browser-runtime",
3
- "version": "5.4.1",
3
+ "version": "5.5.0",
4
4
  "private": false,
5
5
  "description": "Parent-owned, resource-centric Chromium runtime with revisioned attachment fencing, authenticated human and agent control, fail-closed egress, bounded artifacts, and Flex/MCP adapters.",
6
6
  "main": "dist_ts/index.js",
@@ -30,7 +30,7 @@
30
30
  "@push.rocks/smartagent": "^6.0.0",
31
31
  "@push.rocks/smartipc": "^2.6.0",
32
32
  "@push.rocks/smartmcp": "^0.3.0",
33
- "@push.rocks/smartpuppeteer": "^2.12.0",
33
+ "@push.rocks/smartpuppeteer": "^2.12.1",
34
34
  "ipaddr.js": "^2.5.0"
35
35
  },
36
36
  "devDependencies": {
package/readme.md CHANGED
@@ -30,7 +30,7 @@ Chrome DevTools UI.
30
30
 
31
31
  ## Native video viewers
32
32
 
33
- Human leases expose `openVideoPeer(options?)`, `answerVideoPeer(negotiationId, description, options?)`, `closeVideoPeer(options?)`, and `getVideoStatistics(options?)`. Each operation runs through the resource's bounded, authorized and audited operation queue. Peer IDs are private to the lease; offers expose the negotiation ID and exact tab/generation/viewport source identity. Agent leases cannot open media peers. Releasing or revoking a human lease closes only its peer, preserving other viewers and agent work. Failed peer cleanup remains owned for a subsequent release attempt.
33
+ Human leases expose `openVideoPeer(options?)`, `answerVideoPeer(negotiationId, description, options?)`, `closeVideoPeer(options?)`, and `getVideoStatistics(options?)`. Each operation retains bounded authorization, auditing and cancellation. Statistics use an independent observation slot so a slow telemetry read cannot block input or media lifecycle work. Peer IDs are private to the lease; offers expose the negotiation ID and exact tab/generation/viewport source identity. Agent leases cannot open media peers. Releasing or revoking a human lease closes only its peer, preserving other viewers and agent work. Failed peer cleanup remains owned for a subsequent release attempt.
34
34
 
35
35
  Set `video` on `BrowserRuntime` for trusted host limits or ICE configuration. Defaults use direct connections with no external STUN/TURN service and automatic GPU support. The private capture extension is loaded without weakening ordinary page proxy, DNS, permission or WebRTC confinement. `state.videoAcceleration` and peer statistics report actual browser acceleration/encoder support.
36
36
 
@@ -162,7 +162,7 @@ by `maxCapabilities`. Hosts must stop scheduling renewal when their viewer disco
162
162
 
163
163
  Agent actions are exactly `navigate`, `snapshot`, `screenshot`, `click`, `fill`, and `press`. Agents can read or delete screenshots created by their exact lease using `readArtifact()` and `deleteArtifact()`. Human leases additionally expose tab lifecycle, viewport, raw input, frame subscription/acknowledgement and refresh, and exact-resource artifact reads/deletes. JavaScript evaluation is not public.
164
164
 
165
- All participants share a bounded resource queue. Up to four wheel operations may run concurrently, with authorization and native-send admission kept in order. Other input, viewport, and semantic operations remain barriers behind preceding wheel work. Dialog replies use one interrupt slot so a paused document operation cannot block its own response. `maxQueuedOperationsPerLease` defaults to 128 and accepts 1 through 1,024; each participant has that bound, and the resource queue is bounded by its product with `maxCapabilitiesPerResource`. Overflow fails with `QUOTA_EXCEEDED`. Releasing a participant cancels only its queued and active work. Resource termination and shutdown cancel everyone. Started uncertain work must quiesce or the exact incarnation is terminated before operation settlement.
165
+ BrowserRuntime alone owns the bounded execution scheduler shared by all participants. Host transports submit operations in order without adding a second execution queue or performing asynchronous authorization ahead of admission. One statistics read may run independently of mutations; its preflight and completion cannot fence input, navigation, or dialog replies. Up to four wheel operations may run concurrently, with authorization and native-send admission kept in order. Other input, viewport, and semantic operations remain barriers behind preceding wheel work. Dialog replies use one interrupt slot so a paused document operation cannot block its own response. `maxQueuedOperationsPerLease` defaults to 128 and accepts 1 through 1,024; each participant has that bound, and the resource queue is bounded by its product with `maxCapabilitiesPerResource`. Overflow fails with `QUOTA_EXCEEDED`. Releasing a participant cancels only its queued and active work. Resource termination and shutdown cancel everyone. Started uncertain work must quiesce or the exact incarnation is terminated before operation settlement.
166
166
 
167
167
  `setViewport()` stores the viewer's preferred width, height and device scale factor and returns `IBrowserRuntimeViewportResult` with the effective `viewport` and `viewportRevision`. The effective values are the componentwise minima across active viewers that have supplied a preference. Unchanged effective sizes do not restart the stream. Removing a viewer recomputes the viewport in the resource FIFO. Held keys and buttons are tracked per participant; leaving releases only inputs that no other participant holds. Viewport and tab/navigation transitions clear held input before changing the target. Resource-owned departure cleanup is bounded and audited as `releaseParticipant`; it does not call the external `beforeOperation` gate after the participant has lost access.
168
168
 
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@modelprofile.com/browser-runtime',
6
- version: '5.4.1',
6
+ version: '5.5.0',
7
7
  description: 'Parent-owned, resource-centric Chromium runtime with revisioned attachment fencing, authenticated human and agent control, fail-closed egress, bounded artifacts, and Flex/MCP adapters.'
8
8
  }
@@ -0,0 +1,81 @@
1
+ interface IQueuedBrowserOperation {
2
+ action: string;
3
+ state: 'queued' | 'starting' | 'active' | 'settled';
4
+ signal: AbortSignal;
5
+ onQueuedAbort?: () => void;
6
+ resolve(value: unknown): void;
7
+ reject(error: unknown): void;
8
+ }
9
+
10
+ /** Resource-owned execution order. Telemetry never owns a mutation slot. */
11
+ export class BrowserOperationScheduler<T extends IQueuedBrowserOperation> {
12
+ public readonly pending: T[] = [];
13
+ private readonly active = new Set<T>();
14
+ private admitting?: T;
15
+ private draining = false;
16
+
17
+ constructor(private execute: (operation: T, dispatched: () => void) => Promise<unknown>) {}
18
+
19
+ public enqueue(operation: T): void {
20
+ this.pending.push(operation);
21
+ this.drain();
22
+ }
23
+
24
+ public remove(operation: T): void {
25
+ const index = this.pending.indexOf(operation);
26
+ if (index >= 0) this.pending.splice(index, 1);
27
+ this.drain();
28
+ }
29
+
30
+ public canStart(action: string, active: readonly { action: string }[]): boolean {
31
+ if (action === 'getVideoStatistics') {
32
+ return !active.some(operation => operation.action === 'getVideoStatistics');
33
+ }
34
+ const mutations = active.filter(operation => operation.action !== 'getVideoStatistics');
35
+ if (action === 'respondToDialog') {
36
+ return !mutations.some(operation => operation.action === 'respondToDialog');
37
+ }
38
+ return mutations.length === 0 || action === 'dispatchWheel' && mutations.length < 4
39
+ && mutations.every(operation => operation.action === 'dispatchWheel');
40
+ }
41
+
42
+ private nextIndex(): number {
43
+ const active = [...this.active];
44
+ // A paused page cannot service its queue until the dialog is answered.
45
+ const dialog = this.pending.findIndex(operation => operation.action === 'respondToDialog');
46
+ if (dialog >= 0 && this.canStart('respondToDialog', active)) return dialog;
47
+ const mutation = this.pending.findIndex(operation => operation.action !== 'getVideoStatistics');
48
+ if (!this.admitting && mutation >= 0 && this.canStart(this.pending[mutation]!.action, active)) return mutation;
49
+ const telemetry = this.pending.findIndex(operation => operation.action === 'getVideoStatistics');
50
+ return telemetry >= 0 && this.canStart('getVideoStatistics', active) ? telemetry : -1;
51
+ }
52
+
53
+ private drain(): void {
54
+ if (this.draining) return;
55
+ this.draining = true;
56
+ try {
57
+ for (let index = this.nextIndex(); index >= 0; index = this.nextIndex()) {
58
+ const operation = this.pending.splice(index, 1)[0]!;
59
+ if (operation.state === 'settled') continue;
60
+ operation.state = 'starting';
61
+ this.active.add(operation);
62
+ if (operation.action !== 'getVideoStatistics' && operation.action !== 'respondToDialog') {
63
+ this.admitting = operation;
64
+ }
65
+ const dispatched = () => {
66
+ if (this.admitting === operation) this.admitting = undefined;
67
+ this.drain();
68
+ };
69
+ void this.execute(operation, dispatched).then(result => {
70
+ if (operation.state !== 'settled') { operation.state = 'settled'; operation.resolve(result); }
71
+ }, error => {
72
+ if (operation.state !== 'settled') { operation.state = 'settled'; operation.reject(error); }
73
+ }).finally(() => {
74
+ if (operation.onQueuedAbort) operation.signal.removeEventListener('abort', operation.onQueuedAbort);
75
+ this.active.delete(operation);
76
+ dispatched();
77
+ });
78
+ }
79
+ } finally { this.draining = false; }
80
+ }
81
+ }
@@ -3,6 +3,7 @@ import { validateAgentAction } from './actions.js';
3
3
  import { BrowserArtifactStore } from './classes.artifactstore.js';
4
4
  import { BrowserEgressProxy } from './classes.egressproxy.js';
5
5
  import { BrowserRuntimeOwnership } from './classes.runtimeownership.js';
6
+ import { BrowserOperationScheduler } from './classes.operationscheduler.js';
6
7
  import { runProductionConfinementProbe } from './confinement.js';
7
8
  import { BrowserRuntimeError } from './errors.js';
8
9
  import {
@@ -263,10 +264,8 @@ interface IResourceSlot {
263
264
  unsubscribeSession?: () => void;
264
265
  leases: Map<string, ILeaseRecord>;
265
266
  operations: Set<IOperationRecord>;
266
- activeQueuedOperations: Set<IQueuedOperationRecord>;
267
267
  inputCleanup?: { session: ILiveBrowserSessionLike; incarnationGeneration: number; promise: Promise<void> };
268
- operationQueue: IQueuedOperationRecord[];
269
- operationSchedulerRunning: boolean;
268
+ operationScheduler: BrowserOperationScheduler<IQueuedOperationRecord>;
270
269
  frameSubscriptions: Map<string, IFrameSubscriptionRecord>;
271
270
  frameCaptureEnabled?: boolean;
272
271
  highestFrameSequence: number;
@@ -746,9 +745,7 @@ export class BrowserRuntime {
746
745
  highestFrameSequence: 0,
747
746
  producerAcknowledgements: new Set(),
748
747
  operations: new Set(),
749
- activeQueuedOperations: new Set(),
750
- operationQueue: [],
751
- operationSchedulerRunning: false,
748
+ operationScheduler: new BrowserOperationScheduler((operation, dispatched) => this.executeQueuedOperation(operation, dispatched)),
752
749
  lifecycleTail: Promise.resolve(),
753
750
  lifecycleOperationCount: 0,
754
751
  terminationRequestGeneration: 0,
@@ -2371,9 +2368,9 @@ export class BrowserRuntime {
2371
2368
  this.requireQueuedLease(lease, participantCleanup);
2372
2369
  this.assertSlotAvailable(slot);
2373
2370
  if (!participantCleanup && (
2374
- slot.operationQueue.filter((entry) => entry.lease === lease).length
2371
+ slot.operationScheduler.pending.filter((entry) => entry.lease === lease).length
2375
2372
  >= this.options.maxQueuedOperationsPerLease
2376
- || slot.operationQueue.length >= this.options.maxQueuedOperationsPerLease
2373
+ || slot.operationScheduler.pending.length >= this.options.maxQueuedOperationsPerLease
2377
2374
  * this.options.maxCapabilitiesPerResource
2378
2375
  )) {
2379
2376
  throw new BrowserRuntimeError('QUOTA_EXCEEDED');
@@ -2406,8 +2403,7 @@ export class BrowserRuntime {
2406
2403
  const onQueuedAbort = (): void => {
2407
2404
  if (queued.state !== 'queued' && queued.state !== 'starting') return;
2408
2405
  if (queued.state === 'queued') {
2409
- const index = slot.operationQueue.indexOf(queued);
2410
- if (index >= 0) slot.operationQueue.splice(index, 1);
2406
+ slot.operationScheduler.remove(queued);
2411
2407
  }
2412
2408
  queued.state = 'settled';
2413
2409
  signal.removeEventListener('abort', onQueuedAbort);
@@ -2427,49 +2423,10 @@ export class BrowserRuntime {
2427
2423
  onQueuedAbort();
2428
2424
  return;
2429
2425
  }
2430
- slot.operationQueue.push(queued);
2431
- this.drainOperationQueue(slot);
2426
+ slot.operationScheduler.enqueue(queued);
2432
2427
  });
2433
2428
  }
2434
2429
 
2435
- private drainOperationQueue(slot: IResourceSlot): void {
2436
- if (slot.operationSchedulerRunning) return;
2437
- slot.operationSchedulerRunning = true;
2438
- void (async () => {
2439
- try {
2440
- while (slot.operationQueue.length > 0) {
2441
- // Dialog replies must unblock a page operation already waiting on Chrome.
2442
- const dialogIndex = slot.operationQueue.findIndex(entry => entry.classification === 'dialog');
2443
- const index = dialogIndex >= 0 ? dialogIndex : 0;
2444
- const queued = slot.operationQueue[index]!;
2445
- const active = [...slot.activeQueuedOperations];
2446
- if (queued.classification === 'dialog') {
2447
- if (active.some(entry => entry.classification === 'dialog')) break;
2448
- } else if (active.length > 0 && !(queued.action === 'dispatchWheel'
2449
- && active.length < 4 && active.every(entry => entry.action === 'dispatchWheel'))) break;
2450
- slot.operationQueue.splice(index, 1);
2451
- if (queued.state === 'settled') continue;
2452
- queued.state = 'starting';
2453
- slot.activeQueuedOperations.add(queued);
2454
- let dispatched!: () => void;
2455
- const admission = new Promise<void>(resolve => { dispatched = resolve; });
2456
- void this.executeQueuedOperation(queued, dispatched).then(result => {
2457
- if (queued.state !== 'settled') { queued.state = 'settled'; queued.resolve(result); }
2458
- }, error => {
2459
- if (queued.state !== 'settled') { queued.state = 'settled'; queued.reject(error); }
2460
- }).finally(() => {
2461
- dispatched();
2462
- if (queued.onQueuedAbort) queued.signal.removeEventListener('abort', queued.onQueuedAbort);
2463
- slot.activeQueuedOperations.delete(queued);
2464
- this.drainOperationQueue(slot);
2465
- });
2466
- // Preserve authorization and native-send order without waiting for wheel ACKs.
2467
- await admission;
2468
- }
2469
- } finally { slot.operationSchedulerRunning = false; }
2470
- })();
2471
- }
2472
-
2473
2430
  private async executeQueuedOperation(queued: IQueuedOperationRecord, dispatched: () => void): Promise<unknown> {
2474
2431
  const { lease, action, classification } = queued;
2475
2432
  const slot = lease.slot;
@@ -2491,9 +2448,7 @@ export class BrowserRuntime {
2491
2448
  this.requireQueuedLease(lease, queued.participantCleanup);
2492
2449
  this.assertSlotAvailable(slot);
2493
2450
  const active = [...slot.operations];
2494
- if (classification === 'dialog' ? active.some(operation => operation.action === 'respondToDialog')
2495
- : active.length > 0 && !(action === 'dispatchWheel' && active.length < 4
2496
- && active.every(operation => operation.action === 'dispatchWheel'))) {
2451
+ if (!slot.operationScheduler.canStart(action, active)) {
2497
2452
  throw new BrowserRuntimeError('BUSY');
2498
2453
  }
2499
2454
  session = slot.session!;
@@ -2571,7 +2526,9 @@ export class BrowserRuntime {
2571
2526
  if (classification === 'navigation' || classification === 'tab') {
2572
2527
  await this.releaseAllParticipantInput(slot, session);
2573
2528
  }
2574
- if (classification !== 'dialog') await this.releaseStaleParticipantInput(slot, session);
2529
+ if (classification !== 'dialog' && action !== 'getVideoStatistics') {
2530
+ await this.releaseStaleParticipantInput(slot, session);
2531
+ }
2575
2532
  combinedSignal.throwIfAborted();
2576
2533
  const result = queued.execute(combinedSignal, session);
2577
2534
  dispatched();