@modelprofile.com/browser-runtime 2.0.0 → 2.1.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.runtime.d.ts +6 -0
- package/dist_ts/classes.runtime.js +187 -37
- package/dist_ts/confinement.js +48 -7
- package/dist_ts/index.d.ts +1 -1
- package/dist_ts/interfaces.d.ts +19 -9
- package/package.json +1 -1
- package/readme.hints.md +1 -0
- package/readme.md +3 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.runtime.ts +207 -41
- package/ts/confinement.ts +49 -6
- package/ts/index.ts +1 -0
- package/ts/interfaces.ts +26 -9
package/dist_ts/confinement.js
CHANGED
|
@@ -20,14 +20,25 @@ const listenOnUnsupportedPort = async (allowedPorts, onRequest) => {
|
|
|
20
20
|
throw new BrowserRuntimeError('CONFINEMENT_FAILED');
|
|
21
21
|
};
|
|
22
22
|
export const runProductionConfinementProbe = async (context) => {
|
|
23
|
+
const initialState = context.session.getState();
|
|
24
|
+
const initialStateJson = JSON.stringify(initialState);
|
|
25
|
+
let probeTabId;
|
|
26
|
+
let probeFailed = false;
|
|
27
|
+
let probeError;
|
|
23
28
|
let originHits = 0;
|
|
24
29
|
const fixture = await listenOnUnsupportedPort(new Set(context.proxy.allowedTargetPorts), () => { originHits += 1; });
|
|
25
|
-
const closeFixture = async () => new Promise((resolve) => {
|
|
26
|
-
fixture.server.close(() => resolve());
|
|
30
|
+
const closeFixture = async () => new Promise((resolve, reject) => {
|
|
31
|
+
fixture.server.close((error) => error ? reject(error) : resolve());
|
|
27
32
|
});
|
|
28
33
|
try {
|
|
34
|
+
const probeTab = await context.session.createTab({ activate: false }, { signal: context.signal });
|
|
35
|
+
probeTabId = probeTab.id;
|
|
29
36
|
const beforeLoopback = context.proxy.getStats();
|
|
30
|
-
await context.session.navigate({
|
|
37
|
+
await context.session.navigate({
|
|
38
|
+
url: `http://127.0.0.1:${fixture.port}/browser-runtime-probe`,
|
|
39
|
+
tabId: probeTab.id,
|
|
40
|
+
timeoutMs: 3000,
|
|
41
|
+
}, { signal: context.signal }).catch(() => undefined);
|
|
31
42
|
const afterLoopback = context.proxy.getStats();
|
|
32
43
|
if (originHits !== 0
|
|
33
44
|
|| afterLoopback.ordinaryRequests <= beforeLoopback.ordinaryRequests
|
|
@@ -35,7 +46,11 @@ export const runProductionConfinementProbe = async (context) => {
|
|
|
35
46
|
throw new BrowserRuntimeError('CONFINEMENT_FAILED');
|
|
36
47
|
}
|
|
37
48
|
const beforeInvalid = context.proxy.getStats();
|
|
38
|
-
await context.session.navigate({
|
|
49
|
+
await context.session.navigate({
|
|
50
|
+
url: 'http://browser-runtime-confinement.invalid/',
|
|
51
|
+
tabId: probeTab.id,
|
|
52
|
+
timeoutMs: 3000,
|
|
53
|
+
}, { signal: context.signal }).catch(() => undefined);
|
|
39
54
|
const afterInvalid = context.proxy.getStats();
|
|
40
55
|
if (afterInvalid.ordinaryRequests <= beforeInvalid.ordinaryRequests
|
|
41
56
|
|| afterInvalid.rejectedRequests <= beforeInvalid.rejectedRequests) {
|
|
@@ -48,7 +63,7 @@ export const runProductionConfinementProbe = async (context) => {
|
|
|
48
63
|
pc.createDataChannel('probe');
|
|
49
64
|
try { await pc.setLocalDescription(await pc.createOffer()); } catch {}
|
|
50
65
|
setTimeout(() => { pc.close(); resolve({ candidateEmitted: emitted }); }, 1000);
|
|
51
|
-
})`, { timeoutMs: 2500, maxOutputBytes: 1024 }, { signal: context.signal });
|
|
66
|
+
})`, { tabId: probeTab.id, timeoutMs: 2500, maxOutputBytes: 1024 }, { signal: context.signal });
|
|
52
67
|
if (!result
|
|
53
68
|
|| typeof result !== 'object'
|
|
54
69
|
|| Array.isArray(result)
|
|
@@ -56,8 +71,34 @@ export const runProductionConfinementProbe = async (context) => {
|
|
|
56
71
|
throw new BrowserRuntimeError('CONFINEMENT_FAILED');
|
|
57
72
|
}
|
|
58
73
|
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
probeFailed = true;
|
|
76
|
+
probeError = error instanceof Error ? error : new BrowserRuntimeError('CONFINEMENT_FAILED');
|
|
77
|
+
}
|
|
59
78
|
finally {
|
|
60
|
-
|
|
79
|
+
const cleanupErrors = [];
|
|
80
|
+
if (probeTabId) {
|
|
81
|
+
await context.session.closeTab(probeTabId).catch((error) => cleanupErrors.push(error));
|
|
82
|
+
}
|
|
83
|
+
await closeFixture().catch((error) => cleanupErrors.push(error));
|
|
84
|
+
try {
|
|
85
|
+
if (JSON.stringify(context.session.getState()) !== initialStateJson) {
|
|
86
|
+
cleanupErrors.push(new BrowserRuntimeError('CONFINEMENT_FAILED'));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
cleanupErrors.push(error);
|
|
91
|
+
}
|
|
92
|
+
if (probeFailed && cleanupErrors.length > 0) {
|
|
93
|
+
throw new AggregateError([probeError, ...cleanupErrors], 'Browser confinement probe and cleanup failed.');
|
|
94
|
+
}
|
|
95
|
+
if (cleanupErrors.length === 1)
|
|
96
|
+
throw cleanupErrors[0];
|
|
97
|
+
if (cleanupErrors.length > 1) {
|
|
98
|
+
throw new AggregateError(cleanupErrors, 'Browser confinement probe cleanup failed.');
|
|
99
|
+
}
|
|
61
100
|
}
|
|
101
|
+
if (probeFailed)
|
|
102
|
+
throw probeError;
|
|
62
103
|
};
|
|
63
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
104
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29uZmluZW1lbnQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9jb25maW5lbWVudC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLGNBQWMsQ0FBQztBQUV4QyxPQUFPLEVBQUUsbUJBQW1CLEVBQUUsTUFBTSxhQUFhLENBQUM7QUFFbEQsTUFBTSx1QkFBdUIsR0FBRyxLQUFLLEVBQ25DLFlBQWlDLEVBQ2pDLFNBQXFCLEVBQ21DLEVBQUU7SUFDMUQsS0FBSyxJQUFJLE9BQU8sR0FBRyxDQUFDLEVBQUUsT0FBTyxHQUFHLENBQUMsRUFBRSxPQUFPLElBQUksQ0FBQyxFQUFFLENBQUM7UUFDaEQsTUFBTSxNQUFNLEdBQUcsT0FBTyxDQUFDLElBQUksQ0FBQyxZQUFZLENBQUMsQ0FBQyxRQUFRLEVBQUUsUUFBUSxFQUFFLEVBQUU7WUFDOUQsU0FBUyxFQUFFLENBQUM7WUFDWixRQUFRLENBQUMsU0FBUyxDQUFDLEdBQUcsQ0FBQyxDQUFDO1lBQ3hCLFFBQVEsQ0FBQyxHQUFHLEVBQUUsQ0FBQztRQUNqQixDQUFDLENBQUMsQ0FBQztRQUNILE1BQU0sSUFBSSxPQUFPLENBQU8sQ0FBQyxPQUFPLEVBQUUsTUFBTSxFQUFFLEVBQUU7WUFDMUMsTUFBTSxDQUFDLElBQUksQ0FBQyxPQUFPLEVBQUUsTUFBTSxDQUFDLENBQUM7WUFDN0IsTUFBTSxDQUFDLE1BQU0sQ0FBQyxDQUFDLEVBQUUsV0FBVyxFQUFFLE9BQU8sQ0FBQyxDQUFDO1FBQ3pDLENBQUMsQ0FBQyxDQUFDO1FBQ0gsTUFBTSxPQUFPLEdBQUcsTUFBTSxDQUFDLE9BQU8sRUFBRSxDQUFDO1FBQ2pDLElBQUksT0FBTyxJQUFJLE9BQU8sT0FBTyxLQUFLLFFBQVEsSUFBSSxDQUFDLFlBQVksQ0FBQyxHQUFHLENBQUMsT0FBTyxDQUFDLElBQUksQ0FBQyxFQUFFLENBQUM7WUFDOUUsT0FBTyxFQUFFLE1BQU0sRUFBRSxJQUFJLEVBQUUsT0FBTyxDQUFDLElBQUksRUFBRSxDQUFDO1FBQ3hDLENBQUM7UUFDRCxNQUFNLElBQUksT0FBTyxDQUFPLENBQUMsT0FBTyxFQUFFLEVBQUUsQ0FBQyxNQUFNLENBQUMsS0FBSyxDQUFDLEdBQUcsRUFBRSxDQUFDLE9BQU8sRUFBRSxDQUFDLENBQUMsQ0FBQztJQUN0RSxDQUFDO0lBQ0QsTUFBTSxJQUFJLG1CQUFtQixDQUFDLG9CQUFvQixDQUFDLENBQUM7QUFDdEQsQ0FBQyxDQUFDO0FBRUYsTUFBTSxDQUFDLE1BQU0sNkJBQTZCLEdBQUcsS0FBSyxFQUNoRCxPQUF3QyxFQUN6QixFQUFFO0lBQ2pCLE1BQU0sWUFBWSxHQUFHLE9BQU8sQ0FBQyxPQUFPLENBQUMsUUFBUSxFQUFFLENBQUM7SUFDaEQsTUFBTSxnQkFBZ0IsR0FBRyxJQUFJLENBQUMsU0FBUyxDQUFDLFlBQVksQ0FBQyxDQUFDO0lBQ3RELElBQUksVUFBOEIsQ0FBQztJQUNuQyxJQUFJLFdBQVcsR0FBRyxLQUFLLENBQUM7SUFDeEIsSUFBSSxVQUFtQixDQUFDO0lBQ3hCLElBQUksVUFBVSxHQUFHLENBQUMsQ0FBQztJQUNuQixNQUFNLE9BQU8sR0FBRyxNQUFNLHVCQUF1QixDQUMzQyxJQUFJLEdBQUcsQ0FBQyxPQUFPLENBQUMsS0FBSyxDQUFDLGtCQUFrQixDQUFDLEVBQ3pDLEdBQUcsRUFBRSxHQUFHLFVBQVUsSUFBSSxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQzNCLENBQUM7SUFDRixNQUFNLFlBQVksR0FBRyxLQUFLLElBQW1CLEVBQUUsQ0FBQyxJQUFJLE9BQU8sQ0FBQyxDQUFDLE9BQU8sRUFBRSxNQUFNLEVBQUUsRUFBRTtRQUM5RSxPQUFPLENBQUMsTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDLEtBQUssRUFBRSxFQUFFLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxNQUFNLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxDQUFDLE9BQU8sRUFBRSxDQUFDLENBQUM7SUFDckUsQ0FBQyxDQUFDLENBQUM7SUFDSCxJQUFJLENBQUM7UUFDSCxNQUFNLFFBQVEsR0FBRyxNQUFNLE9BQU8sQ0FBQyxPQUFPLENBQUMsU0FBUyxDQUM5QyxFQUFFLFFBQVEsRUFBRSxLQUFLLEVBQUUsRUFDbkIsRUFBRSxNQUFNLEVBQUUsT0FBTyxDQUFDLE1BQU0sRUFBRSxDQUMzQixDQUFDO1FBQ0YsVUFBVSxHQUFHLFFBQVEsQ0FBQyxFQUFFLENBQUM7UUFDekIsTUFBTSxjQUFjLEdBQUcsT0FBTyxDQUFDLEtBQUssQ0FBQyxRQUFRLEVBQUUsQ0FBQztRQUNoRCxNQUFNLE9BQU8sQ0FBQyxPQUFPLENBQUMsUUFBUSxDQUM1QjtZQUNFLEdBQUcsRUFBRSxvQkFBb0IsT0FBTyxDQUFDLElBQUksd0JBQXdCO1lBQzdELEtBQUssRUFBRSxRQUFRLENBQUMsRUFBRTtZQUNsQixTQUFTLEVBQUUsSUFBSTtTQUNoQixFQUNELEVBQUUsTUFBTSxFQUFFLE9BQU8sQ0FBQyxNQUFNLEVBQUUsQ0FDM0IsQ0FBQyxLQUFLLENBQUMsR0FBRyxFQUFFLENBQUMsU0FBUyxDQUFDLENBQUM7UUFDekIsTUFBTSxhQUFhLEdBQUcsT0FBTyxDQUFDLEtBQUssQ0FBQyxRQUFRLEVBQUUsQ0FBQztRQUMvQyxJQUNFLFVBQVUsS0FBSyxDQUFDO2VBQ2IsYUFBYSxDQUFDLGdCQUFnQixJQUFJLGNBQWMsQ0FBQyxnQkFBZ0I7ZUFDakUsYUFBYSxDQUFDLGdCQUFnQixJQUFJLGNBQWMsQ0FBQyxnQkFBZ0IsRUFDcEUsQ0FBQztZQUNELE1BQU0sSUFBSSxtQkFBbUIsQ0FBQyxvQkFBb0IsQ0FBQyxDQUFDO1FBQ3RELENBQUM7UUFFRCxNQUFNLGFBQWEsR0FBRyxPQUFPLENBQUMsS0FBSyxDQUFDLFFBQVEsRUFBRSxDQUFDO1FBQy9DLE1BQU0sT0FBTyxDQUFDLE9BQU8sQ0FBQyxRQUFRLENBQzVCO1lBQ0UsR0FBRyxFQUFFLDZDQUE2QztZQUNsRCxLQUFLLEVBQUUsUUFBUSxDQUFDLEVBQUU7WUFDbEIsU0FBUyxFQUFFLElBQUk7U0FDaEIsRUFDRCxFQUFFLE1BQU0sRUFBRSxPQUFPLENBQUMsTUFBTSxFQUFFLENBQzNCLENBQUMsS0FBSyxDQUFDLEdBQUcsRUFBRSxDQUFDLFNBQVMsQ0FBQyxDQUFDO1FBQ3pCLE1BQU0sWUFBWSxHQUFHLE9BQU8sQ0FBQyxLQUFLLENBQUMsUUFBUSxFQUFFLENBQUM7UUFDOUMsSUFDRSxZQUFZLENBQUMsZ0JBQWdCLElBQUksYUFBYSxDQUFDLGdCQUFnQjtlQUM1RCxZQUFZLENBQUMsZ0JBQWdCLElBQUksYUFBYSxDQUFDLGdCQUFnQixFQUNsRSxDQUFDO1lBQ0QsTUFBTSxJQUFJLG1CQUFtQixDQUFDLG9CQUFvQixDQUFDLENBQUM7UUFDdEQsQ0FBQztRQUVELE1BQU0sTUFBTSxHQUFHLE1BQU0sT0FBTyxDQUFDLE9BQU8sQ0FBQyxRQUFRLENBQzNDOzs7Ozs7O1NBT0csRUFDSCxFQUFFLEtBQUssRUFBRSxRQUFRLENBQUMsRUFBRSxFQUFFLFNBQVMsRUFBRSxJQUFJLEVBQUUsY0FBYyxFQUFFLElBQUksRUFBRSxFQUM3RCxFQUFFLE1BQU0sRUFBRSxPQUFPLENBQUMsTUFBTSxFQUFFLENBQzNCLENBQUM7UUFDRixJQUNFLENBQUMsTUFBTTtlQUNKLE9BQU8sTUFBTSxLQUFLLFFBQVE7ZUFDMUIsS0FBSyxDQUFDLE9BQU8sQ0FBQyxNQUFNLENBQUM7ZUFDcEIsTUFBa0MsQ0FBQyxnQkFBZ0IsS0FBSyxLQUFLLEVBQ2pFLENBQUM7WUFDRCxNQUFNLElBQUksbUJBQW1CLENBQUMsb0JBQW9CLENBQUMsQ0FBQztRQUN0RCxDQUFDO0lBQ0gsQ0FBQztJQUFDLE9BQU8sS0FBSyxFQUFFLENBQUM7UUFDZixXQUFXLEdBQUcsSUFBSSxDQUFDO1FBQ25CLFVBQVUsR0FBRyxLQUFLLFlBQVksS0FBSyxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLElBQUksbUJBQW1CLENBQUMsb0JBQW9CLENBQUMsQ0FBQztJQUM5RixDQUFDO1lBQVMsQ0FBQztRQUNULE1BQU0sYUFBYSxHQUFjLEVBQUUsQ0FBQztRQUNwQyxJQUFJLFVBQVUsRUFBRSxDQUFDO1lBQ2YsTUFBTSxPQUFPLENBQUMsT0FBTyxDQUFDLFFBQVEsQ0FBQyxVQUFVLENBQUMsQ0FBQyxLQUFLLENBQUMsQ0FBQyxLQUFLLEVBQUUsRUFBRSxDQUFDLGFBQWEsQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQztRQUN6RixDQUFDO1FBQ0QsTUFBTSxZQUFZLEVBQUUsQ0FBQyxLQUFLLENBQUMsQ0FBQyxLQUFLLEVBQUUsRUFBRSxDQUFDLGFBQWEsQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQztRQUNqRSxJQUFJLENBQUM7WUFDSCxJQUFJLElBQUksQ0FBQyxTQUFTLENBQUMsT0FBTyxDQUFDLE9BQU8sQ0FBQyxRQUFRLEVBQUUsQ0FBQyxLQUFLLGdCQUFnQixFQUFFLENBQUM7Z0JBQ3BFLGFBQWEsQ0FBQyxJQUFJLENBQUMsSUFBSSxtQkFBbUIsQ0FBQyxvQkFBb0IsQ0FBQyxDQUFDLENBQUM7WUFDcEUsQ0FBQztRQUNILENBQUM7UUFBQyxPQUFPLEtBQUssRUFBRSxDQUFDO1lBQ2YsYUFBYSxDQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsQ0FBQztRQUM1QixDQUFDO1FBQ0QsSUFBSSxXQUFXLElBQUksYUFBYSxDQUFDLE1BQU0sR0FBRyxDQUFDLEVBQUUsQ0FBQztZQUM1QyxNQUFNLElBQUksY0FBYyxDQUN0QixDQUFDLFVBQVUsRUFBRSxHQUFHLGFBQWEsQ0FBQyxFQUM5QiwrQ0FBK0MsQ0FDaEQsQ0FBQztRQUNKLENBQUM7UUFDRCxJQUFJLGFBQWEsQ0FBQyxNQUFNLEtBQUssQ0FBQztZQUFFLE1BQU0sYUFBYSxDQUFDLENBQUMsQ0FBQyxDQUFDO1FBQ3ZELElBQUksYUFBYSxDQUFDLE1BQU0sR0FBRyxDQUFDLEVBQUUsQ0FBQztZQUM3QixNQUFNLElBQUksY0FBYyxDQUFDLGFBQWEsRUFBRSwyQ0FBMkMsQ0FBQyxDQUFDO1FBQ3ZGLENBQUM7SUFDSCxDQUFDO0lBQ0QsSUFBSSxXQUFXO1FBQUUsTUFBTSxVQUFVLENBQUM7QUFDcEMsQ0FBQyxDQUFDIn0=
|
package/dist_ts/index.d.ts
CHANGED
|
@@ -8,4 +8,4 @@ export { BrowserRuntimeError } from './errors.js';
|
|
|
8
8
|
export { createBrowserRuntimeMcpHttpHandler } from './mcp.js';
|
|
9
9
|
export type { TAcquireBrowserRuntimeLeaseOptions } from './classes.runtime.js';
|
|
10
10
|
export type { TBrowserRuntimeErrorCode } from './errors.js';
|
|
11
|
-
export type { IAttachTrustedFramedPeerOptions, IApplyBrowserAttachmentBindingRequest, IBrowserAttachmentBinding, IBrowserActionStateResult, IBrowserArtifactMetadata, IBrowserArtifactStoreOptions, IBrowserMcpAuthenticatedBinding, TBrowserCapabilityAuthorizationRequest, TBrowserCapabilityDescriptor, IBrowserClickAction, IBrowserEgressConnectionOptions, IBrowserEgressProxyOptions, IBrowserEgressProxyStats, IBrowserFlexCapabilityBinding, IBrowserFlexSessionId, IBrowserFillAction, IBrowserNavigateAction, IBrowserObservationResult, IBrowserOpenCodeSessionId, IBrowserPressAction, TBrowserRuntimeAuditEvent, IBrowserRuntimeFlexToolProviderOptions, IBrowserRuntimeFrameSubscription, IBrowserRuntimeFramedClientOptions, IBrowserRuntimeMcpOptions, IBrowserRuntimeOperationOptions, IBrowserRuntimeOptions, IBrowserResourceKey, IBrowserResourceRegistration, IBrowserRuntimeState, IBrowserRuntimeTabState, IBrowserScreenshotAction, IBrowserScreenshotResult, IBrowserSnapshotAction, ICreateBrowserResourceRequest, TIssueBrowserCapabilityRequest, TIssuedBrowserCapability, TQualifiedBrowserSessionId, IRegisterBrowserResourceRequest, IResolvedBrowserFlexCapability, TBrowserActorRole, TBrowserAgentAction, TBrowserAgentActionResult, TBrowserCapabilitySource, TBrowserDnsResolver, TBrowserEgressConnector, TBrowserHarnessId, TBrowserRuntimeEvent, } from './interfaces.js';
|
|
11
|
+
export type { IAttachTrustedFramedPeerOptions, IApplyBrowserAttachmentBindingRequest, IBrowserAttachmentBinding, IBrowserActionStateResult, IBrowserArtifactMetadata, IBrowserArtifactStoreOptions, IBrowserMcpAuthenticatedBinding, TBrowserCapabilityAuthorizationRequest, TBrowserCapabilityDescriptor, IBrowserClickAction, IBrowserEgressConnectionOptions, IBrowserEgressProxyOptions, IBrowserEgressProxyStats, IBrowserFlexCapabilityBinding, IBrowserFlexSessionId, IBrowserFillAction, IBrowserNavigateAction, IBrowserObservationResult, IBrowserOpenCodeSessionId, IBrowserPressAction, TBrowserRuntimeAuditEvent, TBrowserRuntimeOperationIdentity, IBrowserRuntimeFlexToolProviderOptions, IBrowserRuntimeFrameSubscription, IBrowserRuntimeFramedClientOptions, IBrowserRuntimeMcpOptions, IBrowserRuntimeOperationOptions, IBrowserRuntimeOptions, IBrowserResourceKey, IBrowserResourceRegistration, IBrowserRuntimeState, IBrowserRuntimeTabState, IBrowserScreenshotAction, IBrowserScreenshotResult, IBrowserSnapshotAction, ICreateBrowserResourceRequest, TIssueBrowserCapabilityRequest, TIssuedBrowserCapability, TQualifiedBrowserSessionId, IRegisterBrowserResourceRequest, IResolvedBrowserFlexCapability, TBrowserActorRole, TBrowserAgentAction, TBrowserAgentActionResult, TBrowserCapabilitySource, TBrowserDnsResolver, TBrowserEgressConnector, TBrowserHarnessId, TBrowserRuntimeEvent, } from './interfaces.js';
|
package/dist_ts/interfaces.d.ts
CHANGED
|
@@ -81,15 +81,22 @@ export type TIssuedBrowserCapability = TBrowserCapabilityDescriptor & {
|
|
|
81
81
|
export interface IResolvedBrowserFlexCapability {
|
|
82
82
|
capabilityToken: string;
|
|
83
83
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
84
|
+
type TReadonlyBrowserCapabilityAuthorizationRequest = Readonly<IBrowserHumanCapabilityBinding> | (Omit<Readonly<IBrowserFlexCapabilityBinding>, 'sessionId'> & {
|
|
85
|
+
readonly sessionId: Readonly<IBrowserFlexSessionId>;
|
|
86
|
+
}) | (Omit<Readonly<IBrowserMcpCapabilityBinding>, 'sessionId'> & {
|
|
87
|
+
readonly sessionId: Readonly<IBrowserOpenCodeSessionId>;
|
|
88
|
+
});
|
|
89
|
+
export type TBrowserRuntimeOperationIdentity = TReadonlyBrowserCapabilityAuthorizationRequest & {
|
|
90
|
+
readonly operationId: string;
|
|
91
|
+
readonly capabilityId: string;
|
|
92
|
+
readonly leaseId: string;
|
|
93
|
+
readonly action: string;
|
|
94
|
+
readonly startedAt: number;
|
|
95
|
+
};
|
|
96
|
+
export type TBrowserRuntimeAuditEvent = TBrowserRuntimeOperationIdentity & {
|
|
97
|
+
readonly phase: 'completed' | 'failed';
|
|
98
|
+
readonly finishedAt: number;
|
|
99
|
+
readonly errorCode?: string;
|
|
93
100
|
};
|
|
94
101
|
export interface ILiveBrowserSessionLike {
|
|
95
102
|
start(options?: plugins.smartpuppeteer.ILiveBrowserOperationOptions): Promise<void>;
|
|
@@ -130,6 +137,7 @@ export interface IBrowserRuntimeEnvironment {
|
|
|
130
137
|
export interface IBrowserRuntimeOptions {
|
|
131
138
|
runtimeDirectory: string;
|
|
132
139
|
authorizeCapability(request: TBrowserCapabilityAuthorizationRequest, signal: AbortSignal): Promise<boolean> | boolean;
|
|
140
|
+
beforeOperation?(operation: TBrowserRuntimeOperationIdentity, signal: AbortSignal): Promise<void> | void;
|
|
133
141
|
audit?(event: TBrowserRuntimeAuditEvent, signal: AbortSignal): Promise<void> | void;
|
|
134
142
|
maxResources?: number;
|
|
135
143
|
maxResourcesPerProject?: number;
|
|
@@ -141,6 +149,7 @@ export interface IBrowserRuntimeOptions {
|
|
|
141
149
|
capabilityDefaultTtlMs?: number;
|
|
142
150
|
capabilityMaximumTtlMs?: number;
|
|
143
151
|
authorizationTimeoutMs?: number;
|
|
152
|
+
beforeOperationTimeoutMs?: number;
|
|
144
153
|
maxTabsPerResource?: number;
|
|
145
154
|
operationTimeoutMs?: number;
|
|
146
155
|
quiescenceTimeoutMs?: number;
|
|
@@ -347,3 +356,4 @@ export interface IBrowserRuntimeFlexToolProviderOptions<TScope> {
|
|
|
347
356
|
client: BrowserRuntimeFramedClient;
|
|
348
357
|
resolveCapability(context: plugins.flexharness.IFlexToolProviderContext<TScope>): Promise<IResolvedBrowserFlexCapability> | IResolvedBrowserFlexCapability;
|
|
349
358
|
}
|
|
359
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@modelprofile.com/browser-runtime",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
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",
|
package/readme.hints.md
CHANGED
|
@@ -17,6 +17,7 @@ Durable implementation findings for `@modelprofile.com/browser-runtime`.
|
|
|
17
17
|
- Agent capabilities require the exact current non-detached qualified session. Human capabilities bind the exact resource, project, attachment authority, and revision but intentionally carry no session ID and may be issued while detached. Any attachment advance revokes both human and agent capabilities.
|
|
18
18
|
- Capability tokens are returned once. Runtime records retain only SHA-256 digests and use `timingSafeEqual`. Authorization is rechecked after asynchronous host authorization, during lease acquisition, and before and after every operation.
|
|
19
19
|
- Capability, audit, and lease identity includes immutable project, resource, attachment authority and revision, actor, peer, role, source, qualified agent session, and Flex scope/channel where applicable.
|
|
20
|
+
- `beforeOperation` reserves the exact resource and lets the host persist policy/audit through an awaited fail-closed gate. Runtime then atomically revalidates lease, attachment, session, arbitration generation, and incarnation before starting the side effect. Terminal audit remains best effort, uses the same operation ID, and runs after bounded cleanup releases or fences the exact reservation.
|
|
20
21
|
|
|
21
22
|
## Browser confinement
|
|
22
23
|
|
package/readme.md
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
const runtime = new BrowserRuntime({
|
|
28
28
|
runtimeDirectory: '/var/lib/example/browser-runtime',
|
|
29
29
|
authorizeCapability: async (binding) => hostPolicy.authorizeBrowser(binding),
|
|
30
|
+
beforeOperation: async (operation) => hostAudit.recordAttempt(operation),
|
|
30
31
|
});
|
|
31
32
|
|
|
32
33
|
try {
|
|
@@ -96,6 +97,8 @@ Agent capabilities require the exact current non-detached qualified session. Hum
|
|
|
96
97
|
|
|
97
98
|
Agent actions are exactly `navigate`, `snapshot`, `screenshot`, `click`, `fill`, and `press`. Human leases additionally expose tab lifecycle, viewport, raw input, frame subscription/acknowledgement, and exact-resource artifact reads/deletes. JavaScript evaluation is not public.
|
|
98
99
|
|
|
100
|
+
`beforeOperation` is an optional awaited fail-closed gate. It receives the complete immutable authority, operation/capability/lease IDs, action, start time, and an `AbortSignal` after resource admission but before the browser side effect starts. Hosts that require durable attempt-before-side-effect auditing should persist the attempt there. Rejection denies the operation. The default `beforeOperationTimeoutMs` is 10,000 milliseconds and accepts values from 100 through 120,000; timeout fails with `TIMEOUT`. The terminal `audit` callback remains a best-effort `completed`/`failed` notification correlated by the same operation ID and runs after bounded operation cleanup releases or fences the exact reservation.
|
|
101
|
+
|
|
99
102
|
## Trusted Pipe And Flex
|
|
100
103
|
|
|
101
104
|
Trusted framed peers and clients receive the complete authority out of band: project, resource, attachment authority/revision, actor, peer, role, source, qualified session, Flex scope, and resource-specific channel. Incoming frames cannot select identity. One session may use multiple resource-specific channels concurrently.
|
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: '2.
|
|
6
|
+
version: '2.1.1',
|
|
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
|
}
|
package/ts/classes.runtime.ts
CHANGED
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
IBrowserConfinementProbeContext,
|
|
20
20
|
IBrowserObservationResult,
|
|
21
21
|
TBrowserRuntimeAuditEvent,
|
|
22
|
+
TBrowserRuntimeOperationIdentity,
|
|
22
23
|
IBrowserRuntimeFrameSubscription,
|
|
23
24
|
IBrowserRuntimeOperationOptions,
|
|
24
25
|
IBrowserRuntimeOptions,
|
|
@@ -53,6 +54,7 @@ import { BrowserRuntimeFramedServerPeer } from './classes.framed.js';
|
|
|
53
54
|
interface INormalizedRuntimeOptions {
|
|
54
55
|
runtimeDirectory: string;
|
|
55
56
|
authorizeCapability: IBrowserRuntimeOptions['authorizeCapability'];
|
|
57
|
+
beforeOperation?: IBrowserRuntimeOptions['beforeOperation'];
|
|
56
58
|
audit?: IBrowserRuntimeOptions['audit'];
|
|
57
59
|
sessionFactory: (
|
|
58
60
|
options: plugins.smartpuppeteer.ILiveBrowserSessionOptions,
|
|
@@ -69,6 +71,7 @@ interface INormalizedRuntimeOptions {
|
|
|
69
71
|
capabilityDefaultTtlMs: number;
|
|
70
72
|
capabilityMaximumTtlMs: number;
|
|
71
73
|
authorizationTimeoutMs: number;
|
|
74
|
+
beforeOperationTimeoutMs: number;
|
|
72
75
|
maxTabsPerResource: number;
|
|
73
76
|
operationTimeoutMs: number;
|
|
74
77
|
quiescenceTimeoutMs: number;
|
|
@@ -121,6 +124,8 @@ interface IOperationRecord {
|
|
|
121
124
|
controller: AbortController;
|
|
122
125
|
action: string;
|
|
123
126
|
startedAt: number;
|
|
127
|
+
session: ILiveBrowserSessionLike;
|
|
128
|
+
incarnationGeneration: number;
|
|
124
129
|
promise: Promise<void>;
|
|
125
130
|
}
|
|
126
131
|
|
|
@@ -212,6 +217,9 @@ export class BrowserRuntime {
|
|
|
212
217
|
if (options.audit !== undefined && typeof options.audit !== 'function') {
|
|
213
218
|
throw new BrowserRuntimeError('INVALID_INPUT', 'audit must be a function');
|
|
214
219
|
}
|
|
220
|
+
if (options.beforeOperation !== undefined && typeof options.beforeOperation !== 'function') {
|
|
221
|
+
throw new BrowserRuntimeError('INVALID_INPUT', 'beforeOperation must be a function');
|
|
222
|
+
}
|
|
215
223
|
const testingOptions = browserRuntimeTesting in options
|
|
216
224
|
&& options[browserRuntimeTesting] === true
|
|
217
225
|
? options as IBrowserRuntimeTestingOptions
|
|
@@ -244,6 +252,7 @@ export class BrowserRuntime {
|
|
|
244
252
|
this.options = {
|
|
245
253
|
runtimeDirectory: options.runtimeDirectory,
|
|
246
254
|
authorizeCapability: options.authorizeCapability,
|
|
255
|
+
beforeOperation: options.beforeOperation,
|
|
247
256
|
audit: options.audit,
|
|
248
257
|
sessionFactory: testingOptions?.sessionFactory ?? ((sessionOptions) => (
|
|
249
258
|
new plugins.smartpuppeteer.LiveBrowserSession(sessionOptions)
|
|
@@ -308,6 +317,13 @@ export class BrowserRuntime {
|
|
|
308
317
|
120_000,
|
|
309
318
|
10_000,
|
|
310
319
|
),
|
|
320
|
+
beforeOperationTimeoutMs: validateOptionalInteger(
|
|
321
|
+
options.beforeOperationTimeoutMs,
|
|
322
|
+
'beforeOperationTimeoutMs',
|
|
323
|
+
100,
|
|
324
|
+
120_000,
|
|
325
|
+
10_000,
|
|
326
|
+
),
|
|
311
327
|
maxTabsPerResource: validateOptionalInteger(
|
|
312
328
|
options.maxTabsPerResource,
|
|
313
329
|
'maxTabsPerResource',
|
|
@@ -1504,16 +1520,20 @@ export class BrowserRuntime {
|
|
|
1504
1520
|
operationOptions.onOperationStarted !== undefined
|
|
1505
1521
|
&& typeof operationOptions.onOperationStarted !== 'function'
|
|
1506
1522
|
) throw new BrowserRuntimeError('INVALID_INPUT');
|
|
1507
|
-
operationOptions.signal?.
|
|
1523
|
+
if (operationOptions.signal?.aborted) throw new BrowserRuntimeError('ABORTED');
|
|
1508
1524
|
const slot = lease.slot;
|
|
1509
1525
|
const release = slot.mutex.tryAcquire();
|
|
1510
1526
|
if (!release) throw new BrowserRuntimeError('BUSY');
|
|
1511
1527
|
let operation: IOperationRecord;
|
|
1512
|
-
let executionPromise: Promise<T
|
|
1528
|
+
let executionPromise: Promise<T> | undefined;
|
|
1513
1529
|
let combinedSignal: AbortSignal;
|
|
1514
1530
|
let timeout: ReturnType<typeof setTimeout>;
|
|
1515
1531
|
let session: ILiveBrowserSessionLike;
|
|
1516
1532
|
let generation: number;
|
|
1533
|
+
let incarnationGeneration: number;
|
|
1534
|
+
let externalAbort = false;
|
|
1535
|
+
let onExternalAbort: (() => void) | undefined;
|
|
1536
|
+
let resolvePreflight!: () => void;
|
|
1517
1537
|
const startedAt = Date.now();
|
|
1518
1538
|
try {
|
|
1519
1539
|
this.requireValidLease(lease);
|
|
@@ -1521,6 +1541,7 @@ export class BrowserRuntime {
|
|
|
1521
1541
|
if (slot.operation) throw new BrowserRuntimeError('BUSY');
|
|
1522
1542
|
session = slot.session!;
|
|
1523
1543
|
generation = slot.arbitrationGeneration;
|
|
1544
|
+
incarnationGeneration = slot.incarnationGeneration;
|
|
1524
1545
|
const timeoutMs = operationOptions.timeoutMs === undefined
|
|
1525
1546
|
? this.options.operationTimeoutMs
|
|
1526
1547
|
: validateInteger(operationOptions.timeoutMs, 'timeoutMs', 100, 120_000);
|
|
@@ -1530,20 +1551,25 @@ export class BrowserRuntime {
|
|
|
1530
1551
|
timeoutController.abort(new BrowserRuntimeError('TIMEOUT'));
|
|
1531
1552
|
}, timeoutMs);
|
|
1532
1553
|
timeout.unref();
|
|
1554
|
+
const externalSignal = operationOptions.signal;
|
|
1555
|
+
onExternalAbort = () => { externalAbort = true; };
|
|
1556
|
+
externalSignal?.addEventListener('abort', onExternalAbort, { once: true });
|
|
1557
|
+
if (externalSignal?.aborted) externalAbort = true;
|
|
1533
1558
|
combinedSignal = AbortSignal.any([
|
|
1534
1559
|
controller.signal,
|
|
1535
1560
|
lease.controller.signal,
|
|
1536
|
-
|
|
1561
|
+
externalSignal ?? new AbortController().signal,
|
|
1537
1562
|
timeoutController.signal,
|
|
1538
1563
|
]);
|
|
1539
|
-
executionPromise = Promise.resolve().then(() => execute(combinedSignal, session));
|
|
1540
1564
|
operation = {
|
|
1541
1565
|
operationId: randomId(18),
|
|
1542
1566
|
lease,
|
|
1543
1567
|
controller,
|
|
1544
1568
|
action,
|
|
1545
1569
|
startedAt,
|
|
1546
|
-
|
|
1570
|
+
session,
|
|
1571
|
+
incarnationGeneration,
|
|
1572
|
+
promise: new Promise<void>((resolve) => { resolvePreflight = resolve; }),
|
|
1547
1573
|
};
|
|
1548
1574
|
slot.operation = operation;
|
|
1549
1575
|
try {
|
|
@@ -1555,17 +1581,39 @@ export class BrowserRuntime {
|
|
|
1555
1581
|
release();
|
|
1556
1582
|
}
|
|
1557
1583
|
|
|
1558
|
-
|
|
1559
|
-
|
|
1584
|
+
const operationIdentity = this.createOperationIdentity(lease, operation, action, startedAt);
|
|
1585
|
+
|
|
1586
|
+
let result: T | undefined;
|
|
1587
|
+
let failed = false;
|
|
1588
|
+
let failure: unknown;
|
|
1589
|
+
let executionStarted = false;
|
|
1560
1590
|
try {
|
|
1591
|
+
await this.runBeforeOperation(operationIdentity, combinedSignal);
|
|
1592
|
+
const executionRelease = await slot.mutex.acquire();
|
|
1593
|
+
try {
|
|
1594
|
+
combinedSignal.throwIfAborted();
|
|
1595
|
+
this.requireValidLease(lease);
|
|
1596
|
+
this.assertSlotAvailable(slot);
|
|
1597
|
+
this.assertCapabilityStillAttached(lease.capability, slot);
|
|
1598
|
+
if (
|
|
1599
|
+
slot.operation !== operation
|
|
1600
|
+
|| slot.session !== session
|
|
1601
|
+
|| slot.arbitrationGeneration !== generation
|
|
1602
|
+
|| slot.incarnationGeneration !== incarnationGeneration
|
|
1603
|
+
) throw new BrowserRuntimeError('ABORTED');
|
|
1604
|
+
executionStarted = true;
|
|
1605
|
+
executionPromise = execute(combinedSignal, session);
|
|
1606
|
+
operation.promise = executionPromise.then(() => undefined, () => undefined);
|
|
1607
|
+
} finally {
|
|
1608
|
+
executionRelease();
|
|
1609
|
+
}
|
|
1561
1610
|
let onAbort!: () => void;
|
|
1562
1611
|
const aborted = new Promise<never>((_resolve, reject) => {
|
|
1563
1612
|
onAbort = () => reject(combinedSignal.reason ?? new BrowserRuntimeError('ABORTED'));
|
|
1564
1613
|
combinedSignal.addEventListener('abort', onAbort, { once: true });
|
|
1565
1614
|
});
|
|
1566
|
-
let result: T;
|
|
1567
1615
|
try {
|
|
1568
|
-
result = await Promise.race([executionPromise
|
|
1616
|
+
result = await Promise.race([executionPromise, aborted]);
|
|
1569
1617
|
} finally {
|
|
1570
1618
|
combinedSignal.removeEventListener('abort', onAbort);
|
|
1571
1619
|
}
|
|
@@ -1575,46 +1623,131 @@ export class BrowserRuntime {
|
|
|
1575
1623
|
if (slot.session !== session! || slot.arbitrationGeneration !== generation!) {
|
|
1576
1624
|
throw new BrowserRuntimeError('ABORTED');
|
|
1577
1625
|
}
|
|
1578
|
-
return result;
|
|
1579
1626
|
} catch (error) {
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
? error.code
|
|
1583
|
-
: error instanceof Error
|
|
1584
|
-
? truncateString(error.name, 64)
|
|
1585
|
-
: 'Error';
|
|
1586
|
-
if (combinedSignal!.aborted) {
|
|
1587
|
-
this.trackCleanup(this.enforceOperationQuiescence(operation!));
|
|
1588
|
-
const reason = combinedSignal!.reason;
|
|
1589
|
-
throw reason instanceof BrowserRuntimeError
|
|
1590
|
-
? reason
|
|
1591
|
-
: new BrowserRuntimeError(
|
|
1592
|
-
reason instanceof Error && reason.name === 'TimeoutError' ? 'TIMEOUT' : 'ABORTED',
|
|
1593
|
-
);
|
|
1594
|
-
}
|
|
1595
|
-
throw error;
|
|
1627
|
+
failed = true;
|
|
1628
|
+
failure = this.normalizeOperationError(error, combinedSignal!, externalAbort);
|
|
1596
1629
|
} finally {
|
|
1597
1630
|
clearTimeout(timeout!);
|
|
1598
|
-
|
|
1631
|
+
operationOptions.signal?.removeEventListener('abort', onExternalAbort!);
|
|
1632
|
+
resolvePreflight();
|
|
1633
|
+
const cleanupSlot = async (): Promise<void> => {
|
|
1599
1634
|
const cleanupRelease = await slot.mutex.acquire();
|
|
1600
1635
|
try {
|
|
1601
|
-
if (slot.operation === operation
|
|
1636
|
+
if (slot.operation === operation) slot.operation = undefined;
|
|
1602
1637
|
} finally {
|
|
1603
1638
|
cleanupRelease();
|
|
1604
1639
|
}
|
|
1605
|
-
}
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1640
|
+
};
|
|
1641
|
+
if (executionStarted) {
|
|
1642
|
+
if (combinedSignal!.aborted) {
|
|
1643
|
+
await this.enforceOperationQuiescence(operation).catch(() => undefined);
|
|
1644
|
+
}
|
|
1645
|
+
if (slot.operation === operation) {
|
|
1646
|
+
const settled = await waitBounded(operation.promise, this.options.quiescenceTimeoutMs);
|
|
1647
|
+
if (settled.settled) await cleanupSlot();
|
|
1648
|
+
}
|
|
1649
|
+
} else {
|
|
1650
|
+
await cleanupSlot();
|
|
1651
|
+
}
|
|
1652
|
+
const errorCode = failed ? this.operationErrorCode(failure) : undefined;
|
|
1653
|
+
await this.emitAudit(Object.freeze({
|
|
1654
|
+
...operationIdentity,
|
|
1655
|
+
phase: failed ? 'failed' : 'completed',
|
|
1614
1656
|
finishedAt: Date.now(),
|
|
1615
1657
|
...(errorCode ? { errorCode } : {}),
|
|
1616
|
-
});
|
|
1658
|
+
}));
|
|
1659
|
+
}
|
|
1660
|
+
if (failed) throw failure;
|
|
1661
|
+
return result as T;
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
private normalizeOperationError(
|
|
1665
|
+
errorArg: unknown,
|
|
1666
|
+
signalArg: AbortSignal,
|
|
1667
|
+
externalAbortArg: boolean,
|
|
1668
|
+
): unknown {
|
|
1669
|
+
if (signalArg.aborted) {
|
|
1670
|
+
if (externalAbortArg) return new BrowserRuntimeError('ABORTED');
|
|
1671
|
+
const reason = signalArg.reason;
|
|
1672
|
+
if (reason instanceof BrowserRuntimeError) return reason;
|
|
1673
|
+
return new BrowserRuntimeError(
|
|
1674
|
+
reason instanceof Error && reason.name === 'TimeoutError' ? 'TIMEOUT' : 'ABORTED',
|
|
1675
|
+
);
|
|
1676
|
+
}
|
|
1677
|
+
if (errorArg instanceof BrowserRuntimeError) return errorArg;
|
|
1678
|
+
if (errorArg instanceof Error && errorArg.name === 'TimeoutError') {
|
|
1679
|
+
return new BrowserRuntimeError('TIMEOUT');
|
|
1617
1680
|
}
|
|
1681
|
+
if (errorArg instanceof Error && errorArg.name === 'AbortError') {
|
|
1682
|
+
return new BrowserRuntimeError('ABORTED');
|
|
1683
|
+
}
|
|
1684
|
+
return errorArg;
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
private operationErrorCode(errorArg: unknown): string {
|
|
1688
|
+
return errorArg instanceof BrowserRuntimeError
|
|
1689
|
+
? errorArg.code
|
|
1690
|
+
: errorArg instanceof Error
|
|
1691
|
+
? truncateString(errorArg.name, 64)
|
|
1692
|
+
: 'Error';
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
private async runBeforeOperation(
|
|
1696
|
+
operationArg: TBrowserRuntimeOperationIdentity,
|
|
1697
|
+
signalArg: AbortSignal,
|
|
1698
|
+
): Promise<void> {
|
|
1699
|
+
if (!this.options.beforeOperation) return;
|
|
1700
|
+
signalArg.throwIfAborted();
|
|
1701
|
+
const controller = new AbortController();
|
|
1702
|
+
const signal = AbortSignal.any([signalArg, controller.signal]);
|
|
1703
|
+
const hook = Promise.resolve().then(() => this.options.beforeOperation!(operationArg, signal));
|
|
1704
|
+
void hook.catch(() => undefined);
|
|
1705
|
+
let onAbort!: () => void;
|
|
1706
|
+
const aborted = new Promise<never>((_resolve, reject) => {
|
|
1707
|
+
onAbort = () => reject(signal.reason ?? new BrowserRuntimeError('ABORTED'));
|
|
1708
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
1709
|
+
});
|
|
1710
|
+
let result: Awaited<ReturnType<typeof waitBounded<void>>>;
|
|
1711
|
+
try {
|
|
1712
|
+
try {
|
|
1713
|
+
result = await waitBounded(
|
|
1714
|
+
Promise.race([hook, aborted]),
|
|
1715
|
+
this.options.beforeOperationTimeoutMs,
|
|
1716
|
+
);
|
|
1717
|
+
} catch {
|
|
1718
|
+
if (signalArg.aborted) throw signalArg.reason;
|
|
1719
|
+
controller.abort(new BrowserRuntimeError('AUTHORIZATION_DENIED'));
|
|
1720
|
+
throw new BrowserRuntimeError('AUTHORIZATION_DENIED');
|
|
1721
|
+
}
|
|
1722
|
+
} finally {
|
|
1723
|
+
signal.removeEventListener('abort', onAbort);
|
|
1724
|
+
}
|
|
1725
|
+
if (!result.settled) {
|
|
1726
|
+
controller.abort(new BrowserRuntimeError('TIMEOUT'));
|
|
1727
|
+
throw new BrowserRuntimeError('TIMEOUT');
|
|
1728
|
+
}
|
|
1729
|
+
signalArg.throwIfAborted();
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
private createOperationIdentity(
|
|
1733
|
+
leaseArg: ILeaseRecord,
|
|
1734
|
+
operationArg: IOperationRecord,
|
|
1735
|
+
actionArg: string,
|
|
1736
|
+
startedAtArg: number,
|
|
1737
|
+
): TBrowserRuntimeOperationIdentity {
|
|
1738
|
+
const identity = this.describeCapabilityIdentity(leaseArg.capability);
|
|
1739
|
+
const sessionId = identity.role === 'agent'
|
|
1740
|
+
? Object.freeze({ ...identity.sessionId })
|
|
1741
|
+
: undefined;
|
|
1742
|
+
return Object.freeze({
|
|
1743
|
+
...identity,
|
|
1744
|
+
...(sessionId ? { sessionId } : {}),
|
|
1745
|
+
operationId: operationArg.operationId,
|
|
1746
|
+
capabilityId: leaseArg.capability.capabilityId,
|
|
1747
|
+
leaseId: leaseArg.leaseId,
|
|
1748
|
+
action: actionArg,
|
|
1749
|
+
startedAt: startedAtArg,
|
|
1750
|
+
}) as TBrowserRuntimeOperationIdentity;
|
|
1618
1751
|
}
|
|
1619
1752
|
|
|
1620
1753
|
private async executeAgentActionInternal(
|
|
@@ -2192,9 +2325,42 @@ export class BrowserRuntime {
|
|
|
2192
2325
|
}
|
|
2193
2326
|
|
|
2194
2327
|
private async enforceOperationQuiescence(operation: IOperationRecord): Promise<void> {
|
|
2195
|
-
|
|
2196
|
-
if (settled.settled
|
|
2197
|
-
|
|
2328
|
+
let settled = await waitBounded(operation.promise, this.options.quiescenceTimeoutMs);
|
|
2329
|
+
if (settled.settled) return;
|
|
2330
|
+
let cleanupError: unknown;
|
|
2331
|
+
try {
|
|
2332
|
+
if (!operation.lease.released) {
|
|
2333
|
+
await this.revokeCapabilityRecord(operation.lease.capability);
|
|
2334
|
+
} else if (operation.lease.releasePromise) {
|
|
2335
|
+
await waitBounded(operation.lease.releasePromise, this.options.quiescenceTimeoutMs);
|
|
2336
|
+
}
|
|
2337
|
+
} catch (error) {
|
|
2338
|
+
cleanupError = error;
|
|
2339
|
+
}
|
|
2340
|
+
if (operation.lease.slot.operation !== operation) {
|
|
2341
|
+
if (cleanupError) throw cleanupError;
|
|
2342
|
+
return;
|
|
2343
|
+
}
|
|
2344
|
+
settled = await waitBounded(operation.promise, this.options.quiescenceTimeoutMs);
|
|
2345
|
+
if (settled.settled) {
|
|
2346
|
+
if (cleanupError) throw cleanupError;
|
|
2347
|
+
return;
|
|
2348
|
+
}
|
|
2349
|
+
const slot = operation.lease.slot;
|
|
2350
|
+
const release = await slot.mutex.acquire();
|
|
2351
|
+
try {
|
|
2352
|
+
if (
|
|
2353
|
+
slot.operation === operation
|
|
2354
|
+
&& (
|
|
2355
|
+
slot.permanentlyFenced
|
|
2356
|
+
|| slot.session !== operation.session
|
|
2357
|
+
|| slot.incarnationGeneration !== operation.incarnationGeneration
|
|
2358
|
+
)
|
|
2359
|
+
) slot.operation = undefined;
|
|
2360
|
+
} finally {
|
|
2361
|
+
release();
|
|
2362
|
+
}
|
|
2363
|
+
if (cleanupError) throw cleanupError;
|
|
2198
2364
|
}
|
|
2199
2365
|
|
|
2200
2366
|
private async terminateSlotSession(
|