@modelprofile.com/browser-runtime 2.1.2 → 3.0.1
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 +18 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.flexprovider.js +10 -10
- package/dist_ts/classes.framed.d.ts +3 -0
- package/dist_ts/classes.framed.js +16 -3
- package/dist_ts/classes.runtime.d.ts +47 -6
- package/dist_ts/classes.runtime.js +436 -123
- package/dist_ts/index.d.ts +1 -1
- package/dist_ts/interfaces.d.ts +15 -0
- package/package.json +2 -2
- package/readme.hints.md +9 -5
- package/readme.md +11 -5
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexprovider.ts +10 -11
- package/ts/classes.framed.ts +16 -2
- package/ts/classes.runtime.ts +581 -150
- package/ts/index.ts +2 -0
- package/ts/interfaces.ts +22 -0
package/ts/classes.runtime.ts
CHANGED
|
@@ -19,6 +19,8 @@ import type {
|
|
|
19
19
|
IBrowserConfinementProbeContext,
|
|
20
20
|
IBrowserObservationResult,
|
|
21
21
|
TBrowserRuntimeAuditEvent,
|
|
22
|
+
TBrowserRuntimeLeaseAuthority,
|
|
23
|
+
TBrowserRuntimeOperationClassification,
|
|
22
24
|
TBrowserRuntimeOperationIdentity,
|
|
23
25
|
IBrowserRuntimeFrameSubscription,
|
|
24
26
|
IBrowserRuntimeOperationOptions,
|
|
@@ -45,6 +47,7 @@ import {
|
|
|
45
47
|
randomId,
|
|
46
48
|
truncateString,
|
|
47
49
|
validateBoundedString,
|
|
50
|
+
validateExactKeys,
|
|
48
51
|
validateInteger,
|
|
49
52
|
validateOptionalInteger,
|
|
50
53
|
waitBounded,
|
|
@@ -73,14 +76,17 @@ interface INormalizedRuntimeOptions {
|
|
|
73
76
|
authorizationTimeoutMs: number;
|
|
74
77
|
beforeOperationTimeoutMs: number;
|
|
75
78
|
maxTabsPerResource: number;
|
|
79
|
+
maxQueuedOperationsPerLease: number;
|
|
76
80
|
operationTimeoutMs: number;
|
|
77
81
|
quiescenceTimeoutMs: number;
|
|
78
82
|
terminationGraceMs: number;
|
|
79
83
|
terminationForceMs: number;
|
|
84
|
+
cleanupTimeoutMs: number;
|
|
80
85
|
auditTimeoutMs: number;
|
|
81
86
|
confinementProbeTimeoutMs: number;
|
|
82
87
|
idleTerminationMs: number;
|
|
83
88
|
maxFrameBytes: number;
|
|
89
|
+
maxOutstandingFrames: number;
|
|
84
90
|
frameAcknowledgementTimeoutMs: number;
|
|
85
91
|
egress: NonNullable<IBrowserRuntimeOptions['egress']>;
|
|
86
92
|
beforeLeasePublication?: IBrowserRuntimeTestingOptions['beforeLeasePublication'];
|
|
@@ -112,6 +118,7 @@ interface ILeaseRecord {
|
|
|
112
118
|
role: TBrowserActorRole;
|
|
113
119
|
slot: IResourceSlot;
|
|
114
120
|
controller: AbortController;
|
|
121
|
+
authorityGeneration: number;
|
|
115
122
|
released: boolean;
|
|
116
123
|
releasePromise?: Promise<void>;
|
|
117
124
|
releaseGeneration?: number;
|
|
@@ -129,13 +136,39 @@ interface IOperationRecord {
|
|
|
129
136
|
promise: Promise<void>;
|
|
130
137
|
}
|
|
131
138
|
|
|
139
|
+
interface IQueuedOperationRecord {
|
|
140
|
+
operationId: string;
|
|
141
|
+
lease: ILeaseRecord;
|
|
142
|
+
action: string;
|
|
143
|
+
classification: TBrowserRuntimeOperationClassification;
|
|
144
|
+
timeoutMs: number;
|
|
145
|
+
externalSignal?: AbortSignal;
|
|
146
|
+
onOperationStarted?: (operationId: string) => void;
|
|
147
|
+
signal: AbortSignal;
|
|
148
|
+
execute: (signal: AbortSignal, session: ILiveBrowserSessionLike) => Promise<unknown>;
|
|
149
|
+
resolve(value: unknown): void;
|
|
150
|
+
reject(error: unknown): void;
|
|
151
|
+
state: 'queued' | 'starting' | 'active' | 'settled';
|
|
152
|
+
onQueuedAbort?: () => void;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
interface IOutstandingFrameRecord {
|
|
156
|
+
acknowledgement: plugins.smartpuppeteer.ILiveBrowserFrameAcknowledgementRequest;
|
|
157
|
+
timer: ReturnType<typeof setTimeout>;
|
|
158
|
+
session: ILiveBrowserSessionLike;
|
|
159
|
+
incarnationGeneration: number;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
type TFrameAcknowledgementOutcome =
|
|
163
|
+
| { status: 'fulfilled'; accepted: boolean }
|
|
164
|
+
| { status: 'rejected' }
|
|
165
|
+
| { status: 'timedOut' };
|
|
166
|
+
|
|
132
167
|
interface IFrameSubscriptionRecord {
|
|
133
168
|
lease: ILeaseRecord;
|
|
134
169
|
listener: (event: TBrowserRuntimeEvent) => void;
|
|
135
|
-
outstanding
|
|
136
|
-
|
|
137
|
-
timer: ReturnType<typeof setTimeout>;
|
|
138
|
-
};
|
|
170
|
+
outstanding: Map<string, IOutstandingFrameRecord>;
|
|
171
|
+
highestSequence: number;
|
|
139
172
|
closed: boolean;
|
|
140
173
|
}
|
|
141
174
|
|
|
@@ -147,6 +180,7 @@ interface IResourceSlot {
|
|
|
147
180
|
mutex: TransitionMutex;
|
|
148
181
|
arbitrationGeneration: number;
|
|
149
182
|
incarnationGeneration: number;
|
|
183
|
+
authorityGeneration: number;
|
|
150
184
|
fencingGeneration?: number;
|
|
151
185
|
permanentlyFenced: boolean;
|
|
152
186
|
retirementPending: boolean;
|
|
@@ -158,6 +192,8 @@ interface IResourceSlot {
|
|
|
158
192
|
unsubscribeSession?: () => void;
|
|
159
193
|
lease?: ILeaseRecord;
|
|
160
194
|
operation?: IOperationRecord;
|
|
195
|
+
operationQueue: IQueuedOperationRecord[];
|
|
196
|
+
operationSchedulerRunning: boolean;
|
|
161
197
|
frameSubscription?: IFrameSubscriptionRecord;
|
|
162
198
|
idleTimer?: ReturnType<typeof setTimeout>;
|
|
163
199
|
lifecycleTail: Promise<void>;
|
|
@@ -182,6 +218,7 @@ export class BrowserRuntime {
|
|
|
182
218
|
private readonly artifactRoot: string;
|
|
183
219
|
private readonly lockPath: string;
|
|
184
220
|
private readonly artifactStore: BrowserArtifactStore;
|
|
221
|
+
private readonly runtimeAuthorityId = randomId(18);
|
|
185
222
|
private readonly slots = new Map<string, IResourceSlot>();
|
|
186
223
|
private readonly retiredResourceIds = new Uint8Array(64 * 1024);
|
|
187
224
|
private readonly retiredResourceKeys = new Uint8Array(64 * 1024);
|
|
@@ -195,6 +232,7 @@ export class BrowserRuntime {
|
|
|
195
232
|
private lockHandleClosed = false;
|
|
196
233
|
private startPromise?: Promise<void>;
|
|
197
234
|
private stopPromise?: Promise<void>;
|
|
235
|
+
private stopCleanupPromise?: Promise<void>;
|
|
198
236
|
private lifecycleState: 'stopped' | 'starting' | 'running' | 'stopping' = 'stopped';
|
|
199
237
|
private lifecycleEpoch = 0;
|
|
200
238
|
private lifecycleController = new AbortController();
|
|
@@ -331,6 +369,13 @@ export class BrowserRuntime {
|
|
|
331
369
|
256,
|
|
332
370
|
32,
|
|
333
371
|
),
|
|
372
|
+
maxQueuedOperationsPerLease: validateOptionalInteger(
|
|
373
|
+
options.maxQueuedOperationsPerLease,
|
|
374
|
+
'maxQueuedOperationsPerLease',
|
|
375
|
+
1,
|
|
376
|
+
1024,
|
|
377
|
+
128,
|
|
378
|
+
),
|
|
334
379
|
operationTimeoutMs: validateOptionalInteger(
|
|
335
380
|
options.operationTimeoutMs,
|
|
336
381
|
'operationTimeoutMs',
|
|
@@ -359,6 +404,13 @@ export class BrowserRuntime {
|
|
|
359
404
|
60_000,
|
|
360
405
|
5000,
|
|
361
406
|
),
|
|
407
|
+
cleanupTimeoutMs: validateOptionalInteger(
|
|
408
|
+
options.cleanupTimeoutMs,
|
|
409
|
+
'cleanupTimeoutMs',
|
|
410
|
+
100,
|
|
411
|
+
120_000,
|
|
412
|
+
30_000,
|
|
413
|
+
),
|
|
362
414
|
auditTimeoutMs: validateOptionalInteger(
|
|
363
415
|
options.auditTimeoutMs,
|
|
364
416
|
'auditTimeoutMs',
|
|
@@ -387,6 +439,13 @@ export class BrowserRuntime {
|
|
|
387
439
|
16 * 1024 * 1024,
|
|
388
440
|
4 * 1024 * 1024,
|
|
389
441
|
),
|
|
442
|
+
maxOutstandingFrames: validateOptionalInteger(
|
|
443
|
+
options.maxOutstandingFrames,
|
|
444
|
+
'maxOutstandingFrames',
|
|
445
|
+
1,
|
|
446
|
+
32,
|
|
447
|
+
4,
|
|
448
|
+
),
|
|
390
449
|
frameAcknowledgementTimeoutMs: validateOptionalInteger(
|
|
391
450
|
options.frameAcknowledgementTimeoutMs,
|
|
392
451
|
'frameAcknowledgementTimeoutMs',
|
|
@@ -413,7 +472,7 @@ export class BrowserRuntime {
|
|
|
413
472
|
public start(): Promise<void> {
|
|
414
473
|
if (this.lifecycleState === 'running') return Promise.resolve();
|
|
415
474
|
if (this.startPromise) return this.startPromise;
|
|
416
|
-
if (this.
|
|
475
|
+
if (this.stopCleanupPromise) return this.stopCleanupPromise.then(() => this.start());
|
|
417
476
|
this.lifecycleState = 'starting';
|
|
418
477
|
const epoch = ++this.lifecycleEpoch;
|
|
419
478
|
this.lifecycleController = new AbortController();
|
|
@@ -435,17 +494,38 @@ export class BrowserRuntime {
|
|
|
435
494
|
|
|
436
495
|
public stop(): Promise<void> {
|
|
437
496
|
if (this.stopPromise) return this.stopPromise;
|
|
438
|
-
if (
|
|
497
|
+
if (
|
|
498
|
+
this.lifecycleState === 'stopped'
|
|
499
|
+
&& !this.lockHandle
|
|
500
|
+
&& !this.startPromise
|
|
501
|
+
&& !this.stopCleanupPromise
|
|
502
|
+
) {
|
|
439
503
|
return Promise.resolve();
|
|
440
504
|
}
|
|
441
|
-
this.
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
505
|
+
if (!this.stopCleanupPromise) {
|
|
506
|
+
this.lifecycleState = 'stopping';
|
|
507
|
+
this.lifecycleEpoch += 1;
|
|
508
|
+
this.lifecycleController.abort(new BrowserRuntimeError('ABORTED'));
|
|
509
|
+
const startup = this.startPromise;
|
|
510
|
+
const cleanup = (async () => {
|
|
511
|
+
await startup?.catch(() => undefined);
|
|
512
|
+
await this.stopInternal();
|
|
513
|
+
this.lifecycleState = 'stopped';
|
|
514
|
+
})();
|
|
515
|
+
this.stopCleanupPromise = cleanup;
|
|
516
|
+
void cleanup.then(
|
|
517
|
+
() => {
|
|
518
|
+
if (this.stopCleanupPromise === cleanup) this.stopCleanupPromise = undefined;
|
|
519
|
+
},
|
|
520
|
+
() => {
|
|
521
|
+
if (this.stopCleanupPromise === cleanup) this.stopCleanupPromise = undefined;
|
|
522
|
+
},
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
const cleanup = this.stopCleanupPromise;
|
|
445
526
|
this.stopPromise = (async () => {
|
|
446
|
-
await
|
|
447
|
-
|
|
448
|
-
this.lifecycleState = 'stopped';
|
|
527
|
+
const result = await waitBounded(cleanup, this.options.cleanupTimeoutMs);
|
|
528
|
+
if (!result.settled) throw new BrowserRuntimeError('TIMEOUT');
|
|
449
529
|
})().finally(() => {
|
|
450
530
|
this.stopPromise = undefined;
|
|
451
531
|
});
|
|
@@ -501,9 +581,12 @@ export class BrowserRuntime {
|
|
|
501
581
|
mutex: new TransitionMutex(),
|
|
502
582
|
arbitrationGeneration: 0,
|
|
503
583
|
incarnationGeneration: 0,
|
|
584
|
+
authorityGeneration: 0,
|
|
504
585
|
permanentlyFenced: false,
|
|
505
586
|
retirementPending: false,
|
|
506
587
|
attachmentFenceCount: 0,
|
|
588
|
+
operationQueue: [],
|
|
589
|
+
operationSchedulerRunning: false,
|
|
507
590
|
lifecycleTail: Promise.resolve(),
|
|
508
591
|
lifecycleOperationCount: 0,
|
|
509
592
|
terminationRequestGeneration: 0,
|
|
@@ -638,16 +721,22 @@ export class BrowserRuntime {
|
|
|
638
721
|
const channelId = requestArg.channelId === undefined
|
|
639
722
|
? undefined
|
|
640
723
|
: this.validateIdentifier(requestArg.channelId, 'channelId');
|
|
724
|
+
const runId = requestArg.runId === undefined
|
|
725
|
+
? undefined
|
|
726
|
+
: this.validateIdentifier(requestArg.runId, 'runId');
|
|
641
727
|
const sessionId = requestArg.sessionId === undefined
|
|
642
728
|
? undefined
|
|
643
729
|
: this.validateQualifiedSessionId(requestArg.sessionId);
|
|
644
730
|
if (
|
|
645
731
|
(role === 'agent' && !sessionId)
|
|
646
732
|
|| (role === 'human' && sessionId !== undefined)
|
|
647
|
-
|| (source === 'flex' && (!scopeId || !channelId ||
|
|
733
|
+
|| (source === 'flex' && (!scopeId || !channelId || !runId
|
|
734
|
+
|| sessionId?.harnessId !== 'flex'))
|
|
648
735
|
|| (source === 'mcp' && (scopeId !== undefined || channelId !== undefined
|
|
736
|
+
|| runId !== undefined
|
|
649
737
|
|| sessionId?.harnessId !== 'opencode'))
|
|
650
|
-
|| (source === 'human' && (scopeId !== undefined || channelId !== undefined
|
|
738
|
+
|| (source === 'human' && (scopeId !== undefined || channelId !== undefined
|
|
739
|
+
|| runId !== undefined))
|
|
651
740
|
) throw new BrowserRuntimeError('INVALID_INPUT');
|
|
652
741
|
const authority = this.validateExpectedCapabilityBinding({
|
|
653
742
|
projectId,
|
|
@@ -660,6 +749,7 @@ export class BrowserRuntime {
|
|
|
660
749
|
source,
|
|
661
750
|
...(scopeId ? { scopeId } : {}),
|
|
662
751
|
...(channelId ? { channelId } : {}),
|
|
752
|
+
...(runId ? { runId } : {}),
|
|
663
753
|
...(sessionId ? { sessionId } : {}),
|
|
664
754
|
} as TBrowserCapabilityAuthorizationRequest);
|
|
665
755
|
this.assertCapabilityBinding(slot, {
|
|
@@ -925,6 +1015,34 @@ export class BrowserRuntime {
|
|
|
925
1015
|
return this.resourceState(session.getState());
|
|
926
1016
|
}
|
|
927
1017
|
|
|
1018
|
+
/** @internal */
|
|
1019
|
+
public getLeaseAuthority(lease: ILeaseRecord): TBrowserRuntimeLeaseAuthority {
|
|
1020
|
+
this.requireRunning();
|
|
1021
|
+
this.requireValidLease(lease);
|
|
1022
|
+
return this.createLeaseAuthority(lease);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
/** @internal */
|
|
1026
|
+
public isLeaseAuthorityCurrent(
|
|
1027
|
+
lease: ILeaseRecord,
|
|
1028
|
+
authorityArg: TBrowserRuntimeLeaseAuthority,
|
|
1029
|
+
): boolean {
|
|
1030
|
+
const authority = this.validateLeaseAuthority(authorityArg);
|
|
1031
|
+
try {
|
|
1032
|
+
this.requireRunning();
|
|
1033
|
+
this.requireValidLease(lease);
|
|
1034
|
+
this.assertSlotAvailable(lease.slot);
|
|
1035
|
+
} catch {
|
|
1036
|
+
return false;
|
|
1037
|
+
}
|
|
1038
|
+
return authority.runtimeAuthorityId === this.runtimeAuthorityId
|
|
1039
|
+
&& authority.authorityGeneration === lease.authorityGeneration
|
|
1040
|
+
&& authority.incarnationGeneration === lease.slot.incarnationGeneration
|
|
1041
|
+
&& authority.capabilityId === lease.capability.capabilityId
|
|
1042
|
+
&& authority.leaseId === lease.leaseId
|
|
1043
|
+
&& this.capabilityIdentitiesEqual(authority, this.describeCapabilityIdentity(lease.capability));
|
|
1044
|
+
}
|
|
1045
|
+
|
|
928
1046
|
/** @internal */
|
|
929
1047
|
public async executeLeaseAgentAction(
|
|
930
1048
|
lease: ILeaseRecord,
|
|
@@ -933,9 +1051,13 @@ export class BrowserRuntime {
|
|
|
933
1051
|
): Promise<TBrowserAgentActionResult> {
|
|
934
1052
|
if (lease.role !== 'agent') throw new BrowserRuntimeError('CAPABILITY_INVALID');
|
|
935
1053
|
const action = validateAgentAction(actionArg);
|
|
936
|
-
return this.runOperation(
|
|
937
|
-
|
|
938
|
-
|
|
1054
|
+
return this.runOperation(
|
|
1055
|
+
lease,
|
|
1056
|
+
action.action,
|
|
1057
|
+
this.classifyAgentAction(action),
|
|
1058
|
+
operationOptions,
|
|
1059
|
+
async (signal, session) => this.executeAgentActionInternal(lease, session, action, signal),
|
|
1060
|
+
);
|
|
939
1061
|
}
|
|
940
1062
|
|
|
941
1063
|
/** @internal */
|
|
@@ -945,7 +1067,7 @@ export class BrowserRuntime {
|
|
|
945
1067
|
operationOptions: IBrowserRuntimeOperationOptions = {},
|
|
946
1068
|
): Promise<IBrowserRuntimeState> {
|
|
947
1069
|
this.requireHuman(lease);
|
|
948
|
-
return this.runOperation(lease, 'createTab', operationOptions, async (signal, session) => {
|
|
1070
|
+
return this.runOperation(lease, 'createTab', 'tab', operationOptions, async (signal, session) => {
|
|
949
1071
|
if (session.getState().tabs.length >= this.options.maxTabsPerResource) {
|
|
950
1072
|
throw new BrowserRuntimeError('QUOTA_EXCEEDED');
|
|
951
1073
|
}
|
|
@@ -962,7 +1084,7 @@ export class BrowserRuntime {
|
|
|
962
1084
|
): Promise<IBrowserRuntimeState> {
|
|
963
1085
|
this.requireHuman(lease);
|
|
964
1086
|
const tabId = validateBoundedString(tabIdArg, 'tabId', 1, 128);
|
|
965
|
-
return this.runOperation(lease, 'activateTab', operationOptions, async (signal, session) => {
|
|
1087
|
+
return this.runOperation(lease, 'activateTab', 'tab', operationOptions, async (signal, session) => {
|
|
966
1088
|
await session.activateTab(tabId, { signal });
|
|
967
1089
|
return this.resourceState(session.getState());
|
|
968
1090
|
});
|
|
@@ -976,7 +1098,7 @@ export class BrowserRuntime {
|
|
|
976
1098
|
): Promise<IBrowserRuntimeState> {
|
|
977
1099
|
this.requireHuman(lease);
|
|
978
1100
|
const tabId = validateBoundedString(tabIdArg, 'tabId', 1, 128);
|
|
979
|
-
return this.runOperation(lease, 'closeTab', operationOptions, async (signal, session) => {
|
|
1101
|
+
return this.runOperation(lease, 'closeTab', 'tab', operationOptions, async (signal, session) => {
|
|
980
1102
|
await session.closeTab(tabId, { signal });
|
|
981
1103
|
return this.resourceState(session.getState());
|
|
982
1104
|
});
|
|
@@ -990,7 +1112,7 @@ export class BrowserRuntime {
|
|
|
990
1112
|
operationOptions: IBrowserRuntimeOperationOptions = {},
|
|
991
1113
|
): Promise<IBrowserRuntimeState> {
|
|
992
1114
|
this.requireHuman(lease);
|
|
993
|
-
return this.runOperation(lease, action, operationOptions, async (signal, session) => {
|
|
1115
|
+
return this.runOperation(lease, action, 'navigation', operationOptions, async (signal, session) => {
|
|
994
1116
|
await session[action](options, { signal });
|
|
995
1117
|
return this.resourceState(session.getState());
|
|
996
1118
|
});
|
|
@@ -1004,9 +1126,13 @@ export class BrowserRuntime {
|
|
|
1004
1126
|
): Promise<TBrowserAgentActionResult> {
|
|
1005
1127
|
this.requireHuman(lease);
|
|
1006
1128
|
const action = validateAgentAction(actionArg);
|
|
1007
|
-
return this.runOperation(
|
|
1008
|
-
|
|
1009
|
-
|
|
1129
|
+
return this.runOperation(
|
|
1130
|
+
lease,
|
|
1131
|
+
action.action,
|
|
1132
|
+
this.classifyAgentAction(action),
|
|
1133
|
+
operationOptions,
|
|
1134
|
+
async (signal, session) => this.executeAgentActionInternal(lease, session, action, signal),
|
|
1135
|
+
);
|
|
1010
1136
|
}
|
|
1011
1137
|
|
|
1012
1138
|
/** @internal */
|
|
@@ -1016,9 +1142,15 @@ export class BrowserRuntime {
|
|
|
1016
1142
|
operationOptions: IBrowserRuntimeOperationOptions = {},
|
|
1017
1143
|
): Promise<void> {
|
|
1018
1144
|
this.requireHuman(lease);
|
|
1019
|
-
await this.runOperation(
|
|
1145
|
+
await this.runOperation(
|
|
1146
|
+
lease,
|
|
1147
|
+
'setViewport',
|
|
1148
|
+
'viewport',
|
|
1149
|
+
operationOptions,
|
|
1150
|
+
async (signal, session) => {
|
|
1020
1151
|
await session.setViewport(viewport, { signal });
|
|
1021
|
-
|
|
1152
|
+
},
|
|
1153
|
+
);
|
|
1022
1154
|
}
|
|
1023
1155
|
|
|
1024
1156
|
/** @internal */
|
|
@@ -1033,7 +1165,7 @@ export class BrowserRuntime {
|
|
|
1033
1165
|
operationOptions: IBrowserRuntimeOperationOptions = {},
|
|
1034
1166
|
): Promise<void> {
|
|
1035
1167
|
this.requireHuman(lease);
|
|
1036
|
-
await this.runOperation(lease, action, operationOptions, async (_signal, session) => {
|
|
1168
|
+
await this.runOperation(lease, action, 'raw-input', operationOptions, async (_signal, session) => {
|
|
1037
1169
|
if (action === 'dispatchMouse') {
|
|
1038
1170
|
await session.dispatchMouse(input as plugins.smartpuppeteer.ILiveBrowserMouseInput);
|
|
1039
1171
|
} else if (action === 'dispatchWheel') {
|
|
@@ -1054,12 +1186,19 @@ export class BrowserRuntime {
|
|
|
1054
1186
|
this.requireHuman(lease);
|
|
1055
1187
|
if (typeof listener !== 'function') throw new BrowserRuntimeError('INVALID_INPUT');
|
|
1056
1188
|
const slot = lease.slot;
|
|
1057
|
-
const release =
|
|
1189
|
+
const release = slot.mutex.tryAcquire();
|
|
1190
|
+
if (!release) throw new BrowserRuntimeError('BUSY');
|
|
1058
1191
|
let subscription: IFrameSubscriptionRecord;
|
|
1059
1192
|
try {
|
|
1060
1193
|
this.requireValidLease(lease);
|
|
1061
1194
|
if (slot.frameSubscription) throw new BrowserRuntimeError('BUSY');
|
|
1062
|
-
subscription = {
|
|
1195
|
+
subscription = {
|
|
1196
|
+
lease,
|
|
1197
|
+
listener,
|
|
1198
|
+
outstanding: new Map(),
|
|
1199
|
+
highestSequence: 0,
|
|
1200
|
+
closed: false,
|
|
1201
|
+
};
|
|
1063
1202
|
slot.frameSubscription = subscription;
|
|
1064
1203
|
this.pushToSubscription(subscription, {
|
|
1065
1204
|
type: 'state',
|
|
@@ -1076,46 +1215,28 @@ export class BrowserRuntime {
|
|
|
1076
1215
|
/** @internal */
|
|
1077
1216
|
public async acknowledgeLeaseFrame(
|
|
1078
1217
|
lease: ILeaseRecord,
|
|
1079
|
-
|
|
1218
|
+
acknowledgementArg: plugins.smartpuppeteer.ILiveBrowserFrameAcknowledgementRequest,
|
|
1080
1219
|
): Promise<boolean> {
|
|
1081
1220
|
this.requireHuman(lease);
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
}
|
|
1085
|
-
validateBoundedString(acknowledgement.tabId, 'tabId', 1, 128);
|
|
1086
|
-
validateInteger(acknowledgement.sequence, 'sequence', 0, Number.MAX_SAFE_INTEGER);
|
|
1087
|
-
validateInteger(acknowledgement.generation, 'generation', 0, Number.MAX_SAFE_INTEGER);
|
|
1088
|
-
validateInteger(
|
|
1089
|
-
acknowledgement.viewportRevision,
|
|
1090
|
-
'viewportRevision',
|
|
1091
|
-
0,
|
|
1092
|
-
Number.MAX_SAFE_INTEGER,
|
|
1093
|
-
);
|
|
1221
|
+
const acknowledgement = this.validateFrameAcknowledgement(acknowledgementArg, true);
|
|
1222
|
+
const key = this.frameAcknowledgementKey(acknowledgement);
|
|
1094
1223
|
const slot = lease.slot;
|
|
1095
1224
|
const subscription = slot.frameSubscription;
|
|
1096
1225
|
this.requireValidLease(lease);
|
|
1097
|
-
if (!subscription || subscription.lease !== lease
|
|
1098
|
-
const
|
|
1099
|
-
if (
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
.catch(() => ({ settled: true as const, value: { accepted: false } }));
|
|
1112
|
-
if (!result.settled || !result.value.accepted) {
|
|
1113
|
-
operation.catch(() => undefined);
|
|
1114
|
-
await this.options.beforeFrameFailureTermination?.();
|
|
1115
|
-
await this.handleFrameFailure(slot, session, incarnationGeneration, lease);
|
|
1116
|
-
return false;
|
|
1117
|
-
}
|
|
1118
|
-
return true;
|
|
1226
|
+
if (!subscription || subscription.lease !== lease) return false;
|
|
1227
|
+
const outstanding = subscription.outstanding.get(key);
|
|
1228
|
+
if (!outstanding) return false;
|
|
1229
|
+
subscription.outstanding.delete(key);
|
|
1230
|
+
clearTimeout(outstanding.timer);
|
|
1231
|
+
const { session, incarnationGeneration } = outstanding;
|
|
1232
|
+
const outcome = await this.waitForFrameAcknowledgement(
|
|
1233
|
+
session,
|
|
1234
|
+
outstanding.acknowledgement,
|
|
1235
|
+
);
|
|
1236
|
+
if (outcome.status === 'fulfilled') return outcome.accepted;
|
|
1237
|
+
await this.options.beforeFrameFailureTermination?.();
|
|
1238
|
+
await this.handleFrameFailure(slot, session, incarnationGeneration, subscription.lease);
|
|
1239
|
+
return false;
|
|
1119
1240
|
}
|
|
1120
1241
|
|
|
1121
1242
|
/** @internal */
|
|
@@ -1229,7 +1350,9 @@ export class BrowserRuntime {
|
|
|
1229
1350
|
}
|
|
1230
1351
|
|
|
1231
1352
|
private requireRunning(): void {
|
|
1232
|
-
if (this.lifecycleState !== 'running'
|
|
1353
|
+
if (this.lifecycleState !== 'running' || this.lifecycleController.signal.aborted) {
|
|
1354
|
+
throw new BrowserRuntimeError('NOT_RUNNING');
|
|
1355
|
+
}
|
|
1233
1356
|
}
|
|
1234
1357
|
|
|
1235
1358
|
private async acquireAgentLease(
|
|
@@ -1347,6 +1470,11 @@ export class BrowserRuntime {
|
|
|
1347
1470
|
clearTimeout(slot.idleTimer);
|
|
1348
1471
|
slot.idleTimer = undefined;
|
|
1349
1472
|
}
|
|
1473
|
+
if (slot.authorityGeneration >= Number.MAX_SAFE_INTEGER) {
|
|
1474
|
+
slot.permanentlyFenced = true;
|
|
1475
|
+
throw new BrowserRuntimeError('FENCED');
|
|
1476
|
+
}
|
|
1477
|
+
slot.authorityGeneration += 1;
|
|
1350
1478
|
const lease: ILeaseRecord = {
|
|
1351
1479
|
leaseId: randomId(18),
|
|
1352
1480
|
capability,
|
|
@@ -1355,6 +1483,7 @@ export class BrowserRuntime {
|
|
|
1355
1483
|
role: capability.role,
|
|
1356
1484
|
slot,
|
|
1357
1485
|
controller: new AbortController(),
|
|
1486
|
+
authorityGeneration: slot.authorityGeneration,
|
|
1358
1487
|
released: false,
|
|
1359
1488
|
};
|
|
1360
1489
|
slot.lease = lease;
|
|
@@ -1387,6 +1516,9 @@ export class BrowserRuntime {
|
|
|
1387
1516
|
forceNoSandbox: false,
|
|
1388
1517
|
usePipe: true,
|
|
1389
1518
|
allowEvaluation: true,
|
|
1519
|
+
screencast: {
|
|
1520
|
+
maxOutstandingFrames: this.options.maxOutstandingFrames,
|
|
1521
|
+
},
|
|
1390
1522
|
launchOptions: {
|
|
1391
1523
|
headless: true,
|
|
1392
1524
|
userDataDir: profileDirectory,
|
|
@@ -1503,9 +1635,10 @@ export class BrowserRuntime {
|
|
|
1503
1635
|
}
|
|
1504
1636
|
}
|
|
1505
1637
|
|
|
1506
|
-
private
|
|
1638
|
+
private runOperation<T>(
|
|
1507
1639
|
lease: ILeaseRecord,
|
|
1508
1640
|
action: string,
|
|
1641
|
+
classification: TBrowserRuntimeOperationClassification,
|
|
1509
1642
|
operationOptions: IBrowserRuntimeOperationOptions,
|
|
1510
1643
|
execute: (signal: AbortSignal, session: ILiveBrowserSessionLike) => Promise<T>,
|
|
1511
1644
|
): Promise<T> {
|
|
@@ -1522,10 +1655,94 @@ export class BrowserRuntime {
|
|
|
1522
1655
|
) throw new BrowserRuntimeError('INVALID_INPUT');
|
|
1523
1656
|
if (operationOptions.signal?.aborted) throw new BrowserRuntimeError('ABORTED');
|
|
1524
1657
|
const slot = lease.slot;
|
|
1525
|
-
|
|
1526
|
-
|
|
1658
|
+
this.requireValidLease(lease);
|
|
1659
|
+
this.assertSlotAvailable(slot);
|
|
1660
|
+
if (slot.operationQueue.length >= this.options.maxQueuedOperationsPerLease) {
|
|
1661
|
+
throw new BrowserRuntimeError('QUOTA_EXCEEDED');
|
|
1662
|
+
}
|
|
1663
|
+
const timeoutMs = operationOptions.timeoutMs === undefined
|
|
1664
|
+
? this.options.operationTimeoutMs
|
|
1665
|
+
: validateInteger(operationOptions.timeoutMs, 'timeoutMs', 100, 120_000);
|
|
1666
|
+
const signal = AbortSignal.any([
|
|
1667
|
+
lease.controller.signal,
|
|
1668
|
+
this.lifecycleController.signal,
|
|
1669
|
+
operationOptions.signal ?? new AbortController().signal,
|
|
1670
|
+
]);
|
|
1671
|
+
return new Promise<T>((resolve, reject) => {
|
|
1672
|
+
const queued: IQueuedOperationRecord = {
|
|
1673
|
+
operationId: randomId(18),
|
|
1674
|
+
lease,
|
|
1675
|
+
action,
|
|
1676
|
+
classification,
|
|
1677
|
+
timeoutMs,
|
|
1678
|
+
externalSignal: operationOptions.signal,
|
|
1679
|
+
onOperationStarted: operationOptions.onOperationStarted,
|
|
1680
|
+
signal,
|
|
1681
|
+
execute: execute as IQueuedOperationRecord['execute'],
|
|
1682
|
+
resolve: (value) => resolve(value as T),
|
|
1683
|
+
reject,
|
|
1684
|
+
state: 'queued',
|
|
1685
|
+
};
|
|
1686
|
+
const onQueuedAbort = (): void => {
|
|
1687
|
+
if (queued.state !== 'queued' && queued.state !== 'starting') return;
|
|
1688
|
+
if (queued.state === 'queued') {
|
|
1689
|
+
const index = slot.operationQueue.indexOf(queued);
|
|
1690
|
+
if (index >= 0) slot.operationQueue.splice(index, 1);
|
|
1691
|
+
}
|
|
1692
|
+
queued.state = 'settled';
|
|
1693
|
+
signal.removeEventListener('abort', onQueuedAbort);
|
|
1694
|
+
reject(this.queuedOperationAbortError(queued));
|
|
1695
|
+
};
|
|
1696
|
+
queued.onQueuedAbort = onQueuedAbort;
|
|
1697
|
+
signal.addEventListener('abort', onQueuedAbort, { once: true });
|
|
1698
|
+
if (signal.aborted) {
|
|
1699
|
+
onQueuedAbort();
|
|
1700
|
+
return;
|
|
1701
|
+
}
|
|
1702
|
+
slot.operationQueue.push(queued);
|
|
1703
|
+
this.drainOperationQueue(slot);
|
|
1704
|
+
});
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
private drainOperationQueue(slot: IResourceSlot): void {
|
|
1708
|
+
if (slot.operationSchedulerRunning) return;
|
|
1709
|
+
slot.operationSchedulerRunning = true;
|
|
1710
|
+
const scheduler = (async () => {
|
|
1711
|
+
while (slot.operationQueue.length > 0) {
|
|
1712
|
+
const queued = slot.operationQueue.shift()!;
|
|
1713
|
+
if (queued.state === 'settled') continue;
|
|
1714
|
+
queued.state = 'starting';
|
|
1715
|
+
try {
|
|
1716
|
+
const result = await this.executeQueuedOperation(queued);
|
|
1717
|
+
if ((queued as IQueuedOperationRecord).state !== 'settled') {
|
|
1718
|
+
queued.state = 'settled';
|
|
1719
|
+
queued.resolve(result);
|
|
1720
|
+
}
|
|
1721
|
+
} catch (error) {
|
|
1722
|
+
if (queued.state !== 'settled') {
|
|
1723
|
+
queued.state = 'settled';
|
|
1724
|
+
queued.reject(error);
|
|
1725
|
+
}
|
|
1726
|
+
} finally {
|
|
1727
|
+
if (queued.onQueuedAbort) {
|
|
1728
|
+
queued.signal.removeEventListener('abort', queued.onQueuedAbort);
|
|
1729
|
+
queued.onQueuedAbort = undefined;
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
})().finally(() => {
|
|
1734
|
+
slot.operationSchedulerRunning = false;
|
|
1735
|
+
if (slot.operationQueue.length > 0) this.drainOperationQueue(slot);
|
|
1736
|
+
});
|
|
1737
|
+
void scheduler.catch(() => undefined);
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
private async executeQueuedOperation(queued: IQueuedOperationRecord): Promise<unknown> {
|
|
1741
|
+
const { lease, action, classification } = queued;
|
|
1742
|
+
const slot = lease.slot;
|
|
1743
|
+
const release = await slot.mutex.acquire();
|
|
1527
1744
|
let operation: IOperationRecord;
|
|
1528
|
-
let executionPromise: Promise<
|
|
1745
|
+
let executionPromise: Promise<unknown> | undefined;
|
|
1529
1746
|
let combinedSignal: AbortSignal;
|
|
1530
1747
|
let timeout: ReturnType<typeof setTimeout>;
|
|
1531
1748
|
let session: ILiveBrowserSessionLike;
|
|
@@ -1534,35 +1751,34 @@ export class BrowserRuntime {
|
|
|
1534
1751
|
let externalAbort = false;
|
|
1535
1752
|
let onExternalAbort: (() => void) | undefined;
|
|
1536
1753
|
let resolvePreflight!: () => void;
|
|
1537
|
-
|
|
1754
|
+
let startedAt: number;
|
|
1538
1755
|
try {
|
|
1756
|
+
if (queued.state === 'settled') return undefined;
|
|
1757
|
+
queued.signal.throwIfAborted();
|
|
1539
1758
|
this.requireValidLease(lease);
|
|
1540
1759
|
this.assertSlotAvailable(slot);
|
|
1541
1760
|
if (slot.operation) throw new BrowserRuntimeError('BUSY');
|
|
1542
1761
|
session = slot.session!;
|
|
1543
1762
|
generation = slot.arbitrationGeneration;
|
|
1544
1763
|
incarnationGeneration = slot.incarnationGeneration;
|
|
1545
|
-
|
|
1546
|
-
? this.options.operationTimeoutMs
|
|
1547
|
-
: validateInteger(operationOptions.timeoutMs, 'timeoutMs', 100, 120_000);
|
|
1764
|
+
startedAt = Date.now();
|
|
1548
1765
|
const controller = new AbortController();
|
|
1549
1766
|
const timeoutController = new AbortController();
|
|
1550
1767
|
timeout = setTimeout(() => {
|
|
1551
1768
|
timeoutController.abort(new BrowserRuntimeError('TIMEOUT'));
|
|
1552
|
-
}, timeoutMs);
|
|
1769
|
+
}, queued.timeoutMs);
|
|
1553
1770
|
timeout.unref();
|
|
1554
|
-
const externalSignal =
|
|
1771
|
+
const externalSignal = queued.externalSignal;
|
|
1555
1772
|
onExternalAbort = () => { externalAbort = true; };
|
|
1556
1773
|
externalSignal?.addEventListener('abort', onExternalAbort, { once: true });
|
|
1557
1774
|
if (externalSignal?.aborted) externalAbort = true;
|
|
1558
1775
|
combinedSignal = AbortSignal.any([
|
|
1559
1776
|
controller.signal,
|
|
1560
|
-
|
|
1561
|
-
externalSignal ?? new AbortController().signal,
|
|
1777
|
+
queued.signal,
|
|
1562
1778
|
timeoutController.signal,
|
|
1563
1779
|
]);
|
|
1564
1780
|
operation = {
|
|
1565
|
-
operationId:
|
|
1781
|
+
operationId: queued.operationId,
|
|
1566
1782
|
lease,
|
|
1567
1783
|
controller,
|
|
1568
1784
|
action,
|
|
@@ -1572,8 +1788,13 @@ export class BrowserRuntime {
|
|
|
1572
1788
|
promise: new Promise<void>((resolve) => { resolvePreflight = resolve; }),
|
|
1573
1789
|
};
|
|
1574
1790
|
slot.operation = operation;
|
|
1791
|
+
queued.state = 'active';
|
|
1792
|
+
if (queued.onQueuedAbort) {
|
|
1793
|
+
queued.signal.removeEventListener('abort', queued.onQueuedAbort);
|
|
1794
|
+
queued.onQueuedAbort = undefined;
|
|
1795
|
+
}
|
|
1575
1796
|
try {
|
|
1576
|
-
|
|
1797
|
+
queued.onOperationStarted?.(operation.operationId);
|
|
1577
1798
|
} catch {
|
|
1578
1799
|
controller.abort(new BrowserRuntimeError('ABORTED'));
|
|
1579
1800
|
}
|
|
@@ -1581,9 +1802,15 @@ export class BrowserRuntime {
|
|
|
1581
1802
|
release();
|
|
1582
1803
|
}
|
|
1583
1804
|
|
|
1584
|
-
const operationIdentity = this.createOperationIdentity(
|
|
1805
|
+
const operationIdentity = this.createOperationIdentity(
|
|
1806
|
+
lease,
|
|
1807
|
+
operation,
|
|
1808
|
+
action,
|
|
1809
|
+
classification,
|
|
1810
|
+
startedAt,
|
|
1811
|
+
);
|
|
1585
1812
|
|
|
1586
|
-
let result:
|
|
1813
|
+
let result: unknown;
|
|
1587
1814
|
let failed = false;
|
|
1588
1815
|
let failure: unknown;
|
|
1589
1816
|
let executionStarted = false;
|
|
@@ -1602,7 +1829,7 @@ export class BrowserRuntime {
|
|
|
1602
1829
|
|| slot.incarnationGeneration !== incarnationGeneration
|
|
1603
1830
|
) throw new BrowserRuntimeError('ABORTED');
|
|
1604
1831
|
executionStarted = true;
|
|
1605
|
-
executionPromise = execute(combinedSignal, session);
|
|
1832
|
+
executionPromise = queued.execute(combinedSignal, session);
|
|
1606
1833
|
operation.promise = executionPromise.then(() => undefined, () => undefined);
|
|
1607
1834
|
} finally {
|
|
1608
1835
|
executionRelease();
|
|
@@ -1628,7 +1855,7 @@ export class BrowserRuntime {
|
|
|
1628
1855
|
failure = this.normalizeOperationError(error, combinedSignal!, externalAbort);
|
|
1629
1856
|
} finally {
|
|
1630
1857
|
clearTimeout(timeout!);
|
|
1631
|
-
|
|
1858
|
+
queued.externalSignal?.removeEventListener('abort', onExternalAbort!);
|
|
1632
1859
|
resolvePreflight();
|
|
1633
1860
|
const cleanupSlot = async (): Promise<void> => {
|
|
1634
1861
|
const cleanupRelease = await slot.mutex.acquire();
|
|
@@ -1658,7 +1885,14 @@ export class BrowserRuntime {
|
|
|
1658
1885
|
}));
|
|
1659
1886
|
}
|
|
1660
1887
|
if (failed) throw failure;
|
|
1661
|
-
return result
|
|
1888
|
+
return result;
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
private queuedOperationAbortError(queued: IQueuedOperationRecord): BrowserRuntimeError {
|
|
1892
|
+
if (queued.externalSignal?.aborted) return new BrowserRuntimeError('ABORTED');
|
|
1893
|
+
return queued.signal.reason instanceof BrowserRuntimeError
|
|
1894
|
+
? queued.signal.reason
|
|
1895
|
+
: new BrowserRuntimeError('ABORTED');
|
|
1662
1896
|
}
|
|
1663
1897
|
|
|
1664
1898
|
private normalizeOperationError(
|
|
@@ -1733,6 +1967,7 @@ export class BrowserRuntime {
|
|
|
1733
1967
|
leaseArg: ILeaseRecord,
|
|
1734
1968
|
operationArg: IOperationRecord,
|
|
1735
1969
|
actionArg: string,
|
|
1970
|
+
classificationArg: TBrowserRuntimeOperationClassification,
|
|
1736
1971
|
startedAtArg: number,
|
|
1737
1972
|
): TBrowserRuntimeOperationIdentity {
|
|
1738
1973
|
const identity = this.describeCapabilityIdentity(leaseArg.capability);
|
|
@@ -1746,10 +1981,90 @@ export class BrowserRuntime {
|
|
|
1746
1981
|
capabilityId: leaseArg.capability.capabilityId,
|
|
1747
1982
|
leaseId: leaseArg.leaseId,
|
|
1748
1983
|
action: actionArg,
|
|
1984
|
+
classification: classificationArg,
|
|
1749
1985
|
startedAt: startedAtArg,
|
|
1750
1986
|
}) as TBrowserRuntimeOperationIdentity;
|
|
1751
1987
|
}
|
|
1752
1988
|
|
|
1989
|
+
private createLeaseAuthority(lease: ILeaseRecord): TBrowserRuntimeLeaseAuthority {
|
|
1990
|
+
const identity = this.describeCapabilityIdentity(lease.capability);
|
|
1991
|
+
const sessionId = identity.role === 'agent'
|
|
1992
|
+
? Object.freeze({ ...identity.sessionId })
|
|
1993
|
+
: undefined;
|
|
1994
|
+
return Object.freeze({
|
|
1995
|
+
...identity,
|
|
1996
|
+
...(sessionId ? { sessionId } : {}),
|
|
1997
|
+
runtimeAuthorityId: this.runtimeAuthorityId,
|
|
1998
|
+
authorityGeneration: lease.authorityGeneration,
|
|
1999
|
+
incarnationGeneration: lease.slot.incarnationGeneration,
|
|
2000
|
+
capabilityId: lease.capability.capabilityId,
|
|
2001
|
+
leaseId: lease.leaseId,
|
|
2002
|
+
}) as TBrowserRuntimeLeaseAuthority;
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
private validateLeaseAuthority(value: unknown): TBrowserRuntimeLeaseAuthority {
|
|
2006
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
2007
|
+
throw new BrowserRuntimeError('INVALID_INPUT');
|
|
2008
|
+
}
|
|
2009
|
+
const candidate = value as Record<string, unknown>;
|
|
2010
|
+
const isFlex = candidate.role === 'agent' && candidate.source === 'flex';
|
|
2011
|
+
const isMcp = candidate.role === 'agent' && candidate.source === 'mcp';
|
|
2012
|
+
const isHuman = candidate.role === 'human' && candidate.source === 'human';
|
|
2013
|
+
if (!isFlex && !isMcp && !isHuman) throw new BrowserRuntimeError('INVALID_INPUT');
|
|
2014
|
+
const record = validateExactKeys(value, [
|
|
2015
|
+
'projectId',
|
|
2016
|
+
'browserResourceId',
|
|
2017
|
+
'attachmentAuthorityId',
|
|
2018
|
+
'attachmentRevision',
|
|
2019
|
+
'actorId',
|
|
2020
|
+
'peerId',
|
|
2021
|
+
'role',
|
|
2022
|
+
'source',
|
|
2023
|
+
'runtimeAuthorityId',
|
|
2024
|
+
'authorityGeneration',
|
|
2025
|
+
'incarnationGeneration',
|
|
2026
|
+
'capabilityId',
|
|
2027
|
+
'leaseId',
|
|
2028
|
+
...(isFlex ? ['sessionId', 'scopeId', 'channelId', 'runId'] : []),
|
|
2029
|
+
...(isMcp ? ['sessionId'] : []),
|
|
2030
|
+
], 'lease authority');
|
|
2031
|
+
const identity = this.validateExpectedCapabilityBinding(
|
|
2032
|
+
record as unknown as TBrowserCapabilityAuthorizationRequest,
|
|
2033
|
+
);
|
|
2034
|
+
const runtimeAuthorityId = validateBoundedString(
|
|
2035
|
+
record.runtimeAuthorityId,
|
|
2036
|
+
'runtimeAuthorityId',
|
|
2037
|
+
16,
|
|
2038
|
+
128,
|
|
2039
|
+
);
|
|
2040
|
+
const capabilityId = validateBoundedString(record.capabilityId, 'capabilityId', 1, 128);
|
|
2041
|
+
const leaseId = validateBoundedString(record.leaseId, 'leaseId', 1, 128);
|
|
2042
|
+
return {
|
|
2043
|
+
...identity,
|
|
2044
|
+
runtimeAuthorityId,
|
|
2045
|
+
authorityGeneration: validateInteger(
|
|
2046
|
+
record.authorityGeneration,
|
|
2047
|
+
'authorityGeneration',
|
|
2048
|
+
1,
|
|
2049
|
+
Number.MAX_SAFE_INTEGER,
|
|
2050
|
+
),
|
|
2051
|
+
incarnationGeneration: validateInteger(
|
|
2052
|
+
record.incarnationGeneration,
|
|
2053
|
+
'incarnationGeneration',
|
|
2054
|
+
1,
|
|
2055
|
+
Number.MAX_SAFE_INTEGER,
|
|
2056
|
+
),
|
|
2057
|
+
capabilityId,
|
|
2058
|
+
leaseId,
|
|
2059
|
+
} as TBrowserRuntimeLeaseAuthority;
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
private classifyAgentAction(
|
|
2063
|
+
action: TBrowserAgentAction,
|
|
2064
|
+
): TBrowserRuntimeOperationClassification {
|
|
2065
|
+
return action.action === 'navigate' ? 'navigation' : 'agent-action';
|
|
2066
|
+
}
|
|
2067
|
+
|
|
1753
2068
|
private async executeAgentActionInternal(
|
|
1754
2069
|
lease: ILeaseRecord,
|
|
1755
2070
|
session: ILiveBrowserSessionLike,
|
|
@@ -1897,12 +2212,22 @@ export class BrowserRuntime {
|
|
|
1897
2212
|
}
|
|
1898
2213
|
const subscription = slot.frameSubscription;
|
|
1899
2214
|
if (event.type === 'frame') {
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
2215
|
+
let acknowledgement: plugins.smartpuppeteer.ILiveBrowserFrameAcknowledgementRequest;
|
|
2216
|
+
try {
|
|
2217
|
+
if (!(event.frame.data instanceof Uint8Array)) {
|
|
2218
|
+
throw new BrowserRuntimeError('INVALID_INPUT');
|
|
2219
|
+
}
|
|
2220
|
+
acknowledgement = this.validateFrameAcknowledgement(event.frame);
|
|
2221
|
+
} catch {
|
|
2222
|
+
const failedLease = slot.session === session ? (slot.lease ?? null) : null;
|
|
2223
|
+
this.trackCleanup(this.handleFrameFailure(
|
|
2224
|
+
slot,
|
|
2225
|
+
session,
|
|
2226
|
+
slot.incarnationGeneration,
|
|
2227
|
+
failedLease,
|
|
2228
|
+
));
|
|
2229
|
+
return;
|
|
2230
|
+
}
|
|
1906
2231
|
if (!subscription || subscription.closed) {
|
|
1907
2232
|
this.trackCleanup(this.acknowledgeFrameInBackground(slot, session, acknowledgement));
|
|
1908
2233
|
return;
|
|
@@ -1914,10 +2239,17 @@ export class BrowserRuntime {
|
|
|
1914
2239
|
this.trackCleanup(this.revokeCapabilityRecord(subscription.lease.capability));
|
|
1915
2240
|
return;
|
|
1916
2241
|
}
|
|
1917
|
-
|
|
1918
|
-
|
|
2242
|
+
const key = this.frameAcknowledgementKey(acknowledgement);
|
|
2243
|
+
if (acknowledgement.sequence <= subscription.highestSequence) {
|
|
2244
|
+
this.trackCleanup(this.handleFrameFailure(
|
|
2245
|
+
slot,
|
|
2246
|
+
session,
|
|
2247
|
+
slot.incarnationGeneration,
|
|
2248
|
+
subscription.lease,
|
|
2249
|
+
));
|
|
1919
2250
|
return;
|
|
1920
2251
|
}
|
|
2252
|
+
subscription.highestSequence = acknowledgement.sequence;
|
|
1921
2253
|
if (event.frame.data.byteLength > this.options.maxFrameBytes) {
|
|
1922
2254
|
this.trackCleanup(this.acknowledgeFrameInBackground(
|
|
1923
2255
|
slot,
|
|
@@ -1931,18 +2263,23 @@ export class BrowserRuntime {
|
|
|
1931
2263
|
});
|
|
1932
2264
|
return;
|
|
1933
2265
|
}
|
|
2266
|
+
while (subscription.outstanding.size >= this.options.maxOutstandingFrames) {
|
|
2267
|
+
const oldestKey = subscription.outstanding.keys().next().value as string | undefined;
|
|
2268
|
+
if (!oldestKey) break;
|
|
2269
|
+
this.trackCleanup(
|
|
2270
|
+
this.retireOutstandingFrame(subscription, oldestKey, false).then(() => undefined),
|
|
2271
|
+
);
|
|
2272
|
+
}
|
|
1934
2273
|
const timer = setTimeout(() => {
|
|
1935
|
-
|
|
1936
|
-
subscription.outstanding = undefined;
|
|
1937
|
-
this.trackCleanup(this.acknowledgeFrameInBackground(
|
|
1938
|
-
slot,
|
|
1939
|
-
session,
|
|
1940
|
-
acknowledgement,
|
|
1941
|
-
true,
|
|
1942
|
-
));
|
|
2274
|
+
this.trackCleanup(this.retireOutstandingFrame(subscription, key, true).then(() => undefined));
|
|
1943
2275
|
}, this.options.frameAcknowledgementTimeoutMs);
|
|
1944
2276
|
timer.unref();
|
|
1945
|
-
subscription.outstanding
|
|
2277
|
+
subscription.outstanding.set(key, {
|
|
2278
|
+
acknowledgement,
|
|
2279
|
+
timer,
|
|
2280
|
+
session,
|
|
2281
|
+
incarnationGeneration: slot.incarnationGeneration,
|
|
2282
|
+
});
|
|
1946
2283
|
this.pushToSubscription(subscription, { type: 'frame', frame: event.frame });
|
|
1947
2284
|
return;
|
|
1948
2285
|
}
|
|
@@ -1975,16 +2312,9 @@ export class BrowserRuntime {
|
|
|
1975
2312
|
const slot = subscription.lease.slot;
|
|
1976
2313
|
const session = slot.session;
|
|
1977
2314
|
const incarnationGeneration = slot.incarnationGeneration;
|
|
1978
|
-
if (event.type === 'frame'
|
|
1979
|
-
const
|
|
1980
|
-
|
|
1981
|
-
subscription.outstanding = undefined;
|
|
1982
|
-
this.trackCleanup(this.acknowledgeFrameInBackground(
|
|
1983
|
-
slot,
|
|
1984
|
-
session!,
|
|
1985
|
-
outstanding.acknowledgement,
|
|
1986
|
-
true,
|
|
1987
|
-
));
|
|
2315
|
+
if (event.type === 'frame') {
|
|
2316
|
+
const key = this.frameAcknowledgementKey(this.validateFrameAcknowledgement(event.frame));
|
|
2317
|
+
this.trackCleanup(this.retireOutstandingFrame(subscription, key, true).then(() => undefined));
|
|
1988
2318
|
return;
|
|
1989
2319
|
}
|
|
1990
2320
|
if (!session) {
|
|
@@ -2008,27 +2338,91 @@ export class BrowserRuntime {
|
|
|
2008
2338
|
subscription.closed = true;
|
|
2009
2339
|
const slot = subscription.lease.slot;
|
|
2010
2340
|
if (slot.frameSubscription === subscription) slot.frameSubscription = undefined;
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2341
|
+
for (const key of [...subscription.outstanding.keys()]) {
|
|
2342
|
+
await this.retireOutstandingFrame(subscription, key, false);
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
private async retireOutstandingFrame(
|
|
2347
|
+
subscription: IFrameSubscriptionRecord,
|
|
2348
|
+
key: string,
|
|
2349
|
+
failLease: boolean,
|
|
2350
|
+
): Promise<boolean> {
|
|
2351
|
+
const outstanding = subscription.outstanding.get(key);
|
|
2352
|
+
if (!outstanding) return false;
|
|
2353
|
+
subscription.outstanding.delete(key);
|
|
2354
|
+
clearTimeout(outstanding.timer);
|
|
2355
|
+
const outcome = await this.waitForFrameAcknowledgement(
|
|
2356
|
+
outstanding.session,
|
|
2357
|
+
outstanding.acknowledgement,
|
|
2358
|
+
);
|
|
2359
|
+
if (outcome.status === 'fulfilled' && !failLease) return outcome.accepted;
|
|
2360
|
+
await this.options.beforeFrameFailureTermination?.();
|
|
2361
|
+
await this.handleFrameFailure(
|
|
2362
|
+
subscription.lease.slot,
|
|
2363
|
+
outstanding.session,
|
|
2364
|
+
outstanding.incarnationGeneration,
|
|
2365
|
+
subscription.lease,
|
|
2366
|
+
);
|
|
2367
|
+
return outcome.status === 'fulfilled' ? outcome.accepted : false;
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
private validateFrameAcknowledgement(
|
|
2371
|
+
value: unknown,
|
|
2372
|
+
exact = false,
|
|
2373
|
+
): plugins.smartpuppeteer.ILiveBrowserFrameAcknowledgementRequest {
|
|
2374
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
2375
|
+
throw new BrowserRuntimeError('INVALID_INPUT');
|
|
2376
|
+
}
|
|
2377
|
+
const record = exact
|
|
2378
|
+
? validateExactKeys(
|
|
2379
|
+
value,
|
|
2380
|
+
['tabId', 'sequence', 'generation', 'viewportRevision'],
|
|
2381
|
+
'frame acknowledgement',
|
|
2382
|
+
)
|
|
2383
|
+
: value as Record<string, unknown>;
|
|
2384
|
+
const tabId = validateBoundedString(record.tabId, 'tabId', 1, 128);
|
|
2385
|
+
if (tabId !== record.tabId) throw new BrowserRuntimeError('INVALID_INPUT');
|
|
2386
|
+
return {
|
|
2387
|
+
tabId,
|
|
2388
|
+
sequence: validateInteger(record.sequence, 'sequence', 1, Number.MAX_SAFE_INTEGER),
|
|
2389
|
+
generation: validateInteger(record.generation, 'generation', 0, Number.MAX_SAFE_INTEGER),
|
|
2390
|
+
viewportRevision: validateInteger(
|
|
2391
|
+
record.viewportRevision,
|
|
2392
|
+
'viewportRevision',
|
|
2393
|
+
1,
|
|
2394
|
+
Number.MAX_SAFE_INTEGER,
|
|
2395
|
+
),
|
|
2396
|
+
};
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
private frameAcknowledgementKey(
|
|
2400
|
+
acknowledgement: plugins.smartpuppeteer.ILiveBrowserFrameAcknowledgementRequest,
|
|
2401
|
+
): string {
|
|
2402
|
+
return `${acknowledgement.tabId.length}:${acknowledgement.tabId}`
|
|
2403
|
+
+ `:${acknowledgement.sequence}:${acknowledgement.generation}`
|
|
2404
|
+
+ `:${acknowledgement.viewportRevision}`;
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
private async waitForFrameAcknowledgement(
|
|
2408
|
+
session: ILiveBrowserSessionLike,
|
|
2409
|
+
acknowledgement: plugins.smartpuppeteer.ILiveBrowserFrameAcknowledgementRequest,
|
|
2410
|
+
): Promise<TFrameAcknowledgementOutcome> {
|
|
2411
|
+
let operation: Promise<plugins.smartpuppeteer.ILiveBrowserFrameAcknowledgement>;
|
|
2412
|
+
try {
|
|
2413
|
+
operation = session.acknowledgeFrame(acknowledgement);
|
|
2414
|
+
} catch {
|
|
2415
|
+
return { status: 'rejected' };
|
|
2416
|
+
}
|
|
2417
|
+
try {
|
|
2418
|
+
const result = await waitBounded(operation, this.options.frameAcknowledgementTimeoutMs);
|
|
2419
|
+
if (!result.settled) {
|
|
2420
|
+
operation.catch(() => undefined);
|
|
2421
|
+
return { status: 'timedOut' };
|
|
2031
2422
|
}
|
|
2423
|
+
return { status: 'fulfilled', accepted: result.value.accepted };
|
|
2424
|
+
} catch {
|
|
2425
|
+
return { status: 'rejected' };
|
|
2032
2426
|
}
|
|
2033
2427
|
}
|
|
2034
2428
|
|
|
@@ -2038,13 +2432,10 @@ export class BrowserRuntime {
|
|
|
2038
2432
|
acknowledgement: plugins.smartpuppeteer.ILiveBrowserFrameAcknowledgementRequest,
|
|
2039
2433
|
failLease = false,
|
|
2040
2434
|
): Promise<void> {
|
|
2041
|
-
const failedLease = slot.session === session ? slot.lease :
|
|
2435
|
+
const failedLease = slot.session === session ? (slot.lease ?? null) : null;
|
|
2042
2436
|
const incarnationGeneration = slot.incarnationGeneration;
|
|
2043
|
-
const
|
|
2044
|
-
|
|
2045
|
-
.catch(() => ({ settled: true as const, value: { accepted: false } }));
|
|
2046
|
-
if (result.settled && result.value.accepted && !failLease) return;
|
|
2047
|
-
operation.catch(() => undefined);
|
|
2437
|
+
const outcome = await this.waitForFrameAcknowledgement(session, acknowledgement);
|
|
2438
|
+
if (outcome.status === 'fulfilled' && !failLease) return;
|
|
2048
2439
|
await this.options.beforeFrameFailureTermination?.();
|
|
2049
2440
|
await this.handleFrameFailure(slot, session, incarnationGeneration, failedLease);
|
|
2050
2441
|
}
|
|
@@ -2053,27 +2444,43 @@ export class BrowserRuntime {
|
|
|
2053
2444
|
slot: IResourceSlot,
|
|
2054
2445
|
session: ILiveBrowserSessionLike,
|
|
2055
2446
|
incarnationGeneration: number,
|
|
2056
|
-
|
|
2447
|
+
expectedLease: ILeaseRecord | null,
|
|
2057
2448
|
): Promise<void> {
|
|
2058
2449
|
let transitionGeneration = 0;
|
|
2059
2450
|
let ownsTransitionFence = false;
|
|
2451
|
+
const lease = expectedLease ?? undefined;
|
|
2060
2452
|
const release = await slot.mutex.acquire();
|
|
2061
2453
|
try {
|
|
2062
|
-
|
|
2454
|
+
const exactLeaseIsCurrent = expectedLease === null
|
|
2455
|
+
? slot.lease === undefined
|
|
2456
|
+
: slot.lease === expectedLease && expectedLease.capability.lease === expectedLease;
|
|
2457
|
+
let leaseAttachmentIsCurrent = true;
|
|
2458
|
+
if (lease) {
|
|
2459
|
+
try {
|
|
2460
|
+
this.assertCapabilityStillAttached(lease.capability, slot);
|
|
2461
|
+
} catch {
|
|
2462
|
+
leaseAttachmentIsCurrent = false;
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2063
2465
|
const borrowsLeaseReleaseFence = Boolean(
|
|
2064
|
-
lease
|
|
2466
|
+
lease
|
|
2467
|
+
&& exactLeaseIsCurrent
|
|
2468
|
+
&& lease.releaseGeneration !== undefined
|
|
2065
2469
|
&& slot.fencingGeneration === lease.releaseGeneration,
|
|
2066
2470
|
);
|
|
2067
2471
|
if (
|
|
2068
2472
|
slot.session !== session
|
|
2069
2473
|
|| slot.incarnationGeneration !== incarnationGeneration
|
|
2070
|
-
||
|
|
2071
|
-
||
|
|
2474
|
+
|| slot.permanentlyFenced
|
|
2475
|
+
|| slot.retirementPending
|
|
2476
|
+
|| slot.terminationFenceGeneration !== undefined
|
|
2477
|
+
|| slot.attachmentFenceCount > 0
|
|
2072
2478
|
|| slot.attachmentRetryBinding
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2479
|
+
|| !exactLeaseIsCurrent
|
|
2480
|
+
|| !leaseAttachmentIsCurrent
|
|
2481
|
+
|| (slot.fencingGeneration !== undefined && !borrowsLeaseReleaseFence)
|
|
2482
|
+
) return;
|
|
2483
|
+
if (lease) this.invalidateCapabilityRecord(lease.capability);
|
|
2077
2484
|
if (borrowsLeaseReleaseFence) {
|
|
2078
2485
|
transitionGeneration = slot.fencingGeneration!;
|
|
2079
2486
|
} else {
|
|
@@ -2848,13 +3255,19 @@ export class BrowserRuntime {
|
|
|
2848
3255
|
const channelId = value.channelId === undefined
|
|
2849
3256
|
? undefined
|
|
2850
3257
|
: this.validateIdentifier(value.channelId, 'channelId');
|
|
3258
|
+
const runId = value.runId === undefined
|
|
3259
|
+
? undefined
|
|
3260
|
+
: this.validateIdentifier(value.runId, 'runId');
|
|
2851
3261
|
if (
|
|
2852
3262
|
(role === 'agent' && !sessionId)
|
|
2853
3263
|
|| (role === 'human' && sessionId !== undefined)
|
|
2854
|
-
|| (source === 'flex' && (!scopeId || !channelId ||
|
|
3264
|
+
|| (source === 'flex' && (!scopeId || !channelId || !runId
|
|
3265
|
+
|| sessionId?.harnessId !== 'flex'))
|
|
2855
3266
|
|| (source === 'mcp' && (scopeId !== undefined || channelId !== undefined
|
|
3267
|
+
|| runId !== undefined
|
|
2856
3268
|
|| sessionId?.harnessId !== 'opencode'))
|
|
2857
|
-
|| (source === 'human' && (scopeId !== undefined || channelId !== undefined
|
|
3269
|
+
|| (source === 'human' && (scopeId !== undefined || channelId !== undefined
|
|
3270
|
+
|| runId !== undefined))
|
|
2858
3271
|
) throw new BrowserRuntimeError('INVALID_INPUT');
|
|
2859
3272
|
const base = {
|
|
2860
3273
|
projectId: this.validateIdentifier(value.projectId, 'projectId'),
|
|
@@ -2876,7 +3289,15 @@ export class BrowserRuntime {
|
|
|
2876
3289
|
return { ...base, role, source };
|
|
2877
3290
|
}
|
|
2878
3291
|
if (role === 'agent' && source === 'flex' && sessionId?.harnessId === 'flex') {
|
|
2879
|
-
return {
|
|
3292
|
+
return {
|
|
3293
|
+
...base,
|
|
3294
|
+
role,
|
|
3295
|
+
source,
|
|
3296
|
+
sessionId,
|
|
3297
|
+
scopeId: scopeId!,
|
|
3298
|
+
channelId: channelId!,
|
|
3299
|
+
runId: runId!,
|
|
3300
|
+
};
|
|
2880
3301
|
}
|
|
2881
3302
|
if (role === 'agent' && source === 'mcp' && sessionId?.harnessId === 'opencode') {
|
|
2882
3303
|
return { ...base, role, source, sessionId };
|
|
@@ -2910,6 +3331,7 @@ export class BrowserRuntime {
|
|
|
2910
3331
|
sessionId: capability.sessionId,
|
|
2911
3332
|
scopeId: capability.scopeId!,
|
|
2912
3333
|
channelId: capability.channelId!,
|
|
3334
|
+
runId: capability.runId!,
|
|
2913
3335
|
};
|
|
2914
3336
|
}
|
|
2915
3337
|
if (
|
|
@@ -2941,6 +3363,7 @@ export class BrowserRuntime {
|
|
|
2941
3363
|
&& left.source === right.source
|
|
2942
3364
|
&& left.scopeId === right.scopeId
|
|
2943
3365
|
&& left.channelId === right.channelId
|
|
3366
|
+
&& left.runId === right.runId
|
|
2944
3367
|
&& this.sessionsEqual(left.sessionId, right.sessionId);
|
|
2945
3368
|
}
|
|
2946
3369
|
|
|
@@ -3044,6 +3467,14 @@ export class BrowserRuntimeLease {
|
|
|
3044
3467
|
return this.runtime.getLeaseState(this.record);
|
|
3045
3468
|
}
|
|
3046
3469
|
|
|
3470
|
+
public getAuthority(): TBrowserRuntimeLeaseAuthority {
|
|
3471
|
+
return this.runtime.getLeaseAuthority(this.record);
|
|
3472
|
+
}
|
|
3473
|
+
|
|
3474
|
+
public isAuthorityCurrent(authority: TBrowserRuntimeLeaseAuthority): boolean {
|
|
3475
|
+
return this.runtime.isLeaseAuthorityCurrent(this.record, authority);
|
|
3476
|
+
}
|
|
3477
|
+
|
|
3047
3478
|
public executeAgentAction(
|
|
3048
3479
|
action: TBrowserAgentAction,
|
|
3049
3480
|
options?: IBrowserRuntimeOperationOptions,
|