@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/changelog.md +10 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.operationscheduler.d.ts +25 -0
- package/dist_ts/classes.operationscheduler.js +86 -0
- package/dist_ts/classes.runtime.d.ts +2 -4
- package/dist_ts/classes.runtime.js +10 -67
- package/package.json +2 -2
- package/readme.md +2 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.operationscheduler.ts +81 -0
- package/ts/classes.runtime.ts +11 -54
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@modelprofile.com/browser-runtime",
|
|
3
|
-
"version": "5.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@modelprofile.com/browser-runtime',
|
|
6
|
-
version: '5.
|
|
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
|
+
}
|
package/ts/classes.runtime.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
2371
|
+
slot.operationScheduler.pending.filter((entry) => entry.lease === lease).length
|
|
2375
2372
|
>= this.options.maxQueuedOperationsPerLease
|
|
2376
|
-
|| slot.
|
|
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
|
-
|
|
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.
|
|
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 (
|
|
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'
|
|
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();
|