@mearl/provider 3.0.0-beta.5 → 3.0.0-beta.8

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/README.md CHANGED
@@ -8,7 +8,7 @@ action 清单直接复用 `@mearl/browser-core/browserActionProtocol` 的能力
8
8
 
9
9
  ## 核心模型
10
10
 
11
- 控制会话由 Mearl 管理。Provider 继续接收目标实例、页面与普通 action,无需处理控制会话 ID、资源归属或人工交接状态。带 `--session` 的释放只结束 Mearl 任务控制;实例关闭、退出接管与 Cookie 同步仍使用 Provider 的生命周期方法。上传与下载分别按 `capabilities.actions` 中的 `page_upload`、`page_download` 声明使用。
11
+ 默认由 Mearl 管理控制会话。Provider 继续接收目标实例、页面与普通 action,无需处理控制会话 ID、资源归属或人工交接状态。带 `--session` 的释放只结束 Mearl 任务控制;实例关闭、退出接管与 Cookie 同步仍使用 Provider 的生命周期方法。上传与下载分别按 `capabilities.actions` 中的 `page_upload`、`page_download` 声明使用。
12
12
 
13
13
  普通 action 响应中的 `session` 由 Mearl 附加,只包含路由与控制状态;`browser_control` 交接时还包含会话页面清单。`page_snapshot` 的文本 ref 可直接用于后续操作,Provider 应在自身运行时保存其定位映射,不在响应中输出内部节点 ID 字典。
14
14
 
@@ -120,7 +120,7 @@ server.updateBrowsers([
120
120
  Runtime 会统一校验 schema、状态机、操作能力和页面 action capability,校验通过后才调用实现。
121
121
  `capabilities.actions` 应明确列出适配器已经实现并验证的动作,避免通过全量动作减排除项生成清单,使 SDK 新增动作被误报为可用。适配器转发请求时,只有能证明请求尚未发送的连接失败才可自动重连重试;已经发送、响应丢失或业务执行失败的动作不应自动重放。
122
122
  `start`、`detach`、`getAccess`、`syncCookies` 等可选实现必须与实例声明的 operation 对应;
123
- 这些是 Provider runtime 的内部能力,不会要求 Mearl 公共协议增加一一对应的 action。
123
+ 这些生命周期方法由 Provider runtime 协调,浏览器动作使用统一的 Mearl 协议。
124
124
 
125
125
  公共 `browser_launch` 对所有 Provider 接收 `{ provider, providerOptions }`,返回与
126
126
  `browser_list.browsers[]` 相同的 `ManagedBrowserInfo`(对象形式的 Provider identity、状态、
@@ -201,3 +201,46 @@ npx @mearl/setup provider uninstall <provider-id>
201
201
  SDK 负责将 Socket 请求分发到 `configure/getLaunchForm/start/detach/syncCookies` 等方法。
202
202
  `browser_release` 普通 action 不承担实例解绑;公开 CLI 的无会话释放由 Mearl 转换为 `detach`。
203
203
  打包 SDK 的 Provider 必须更新 SDK 并重新构建,修改安装元数据不能替换运行中的旧代码。
204
+
205
+ ## 外部任务控制与页面增强
206
+
207
+ 已自行管理任务归属、人工交接的 Provider,可在 `browserType.controlSessionModes` 中声明
208
+ `["external"]`,并在实例的 `control.capabilities` 中返回 `controlSessionMode: "external"`。
209
+ SDK 在服务启动和操作前确认 Mearl 控制宿主支持此模式;旧宿主会明确拒绝。
210
+ 同一运行时内实例的控制模式固定。该模式不接受 Mearl `--session`,页面归属和交接仍由外部工具负责。
211
+ 未声明该字段的 Provider 保持原有行为。
212
+
213
+ 外部 Provider 可用 `createProviderEnhancementClient(providerId, browserId)` 为已获准页面绑定 Mearl
214
+ Extension 的环境增强,依次调用 `bind`、`invoke`、`release`。这条通道只允许设备、App、时区类增强,
215
+ 具体范围取决于 Extension 返回的能力;当前 Extension 实现仅开放时区。
216
+ 它沿用域授权和现有页面控制冲突检查,不登记自动化页面,不提供普通页面操作、任意代码执行或关闭权限。
217
+ 独立 Provider 包使用 `@mearl/provider-<id>` 命名。EgoLite 已有原生 Space 管理和人工交接,通过 [mearl-ego Skill](../../skills/mearl-ego/SKILL.md) 配合 Mearl 扩展进行端模拟与调试;该流程直接调用现有工具,不使用本节的 Provider 增强通道。
218
+
219
+ 实现必须提供只读的 `getEnhancementTarget({ browserId, tabId }, context)`:外部用户控制时返回
220
+ `{ tabId, ready: false }`;确认 Agent 可操作时返回
221
+ `{ tabId, ready: true, mainFrameId, url }`,也可用当前物理 `targetId` 代替 `mainFrameId`。
222
+ 身份必须来自当前浏览器目标;提供两项时,当前宿主同时核验两项。仅提供 target ID 时,需要宿主具备相应的目标核验能力。
223
+ URL 仅用于缩小候选范围,不能单独证明双方是同一页面。
224
+ 此回调和 `listBrowsers` 必须能在 `handleAction` 等待增强结果期间重入,不应获取该操作持有的锁或认领用户页面。
225
+
226
+ 调用可传入原请求的绝对 `deadline`,它不属于业务 payload。绑定句柄有页面、连接代次和动作范围限制;
227
+ 进程重启或页面身份变化后需要重新核验。变更响应丢失后,后续写入会被隔离,不能自动重放或改走普通 action。
228
+ 显式重新绑定只完成核验;无法确认的环境仍须由执行宿主处理恢复冲突。
229
+
230
+ ## 已安装 App 资源
231
+
232
+ 外部控制 Provider 可通过 `createProviderAppResourceClient(providerId, browserId)` 读取本机已接受的
233
+ App 定义与运行时。`catalog(params?, deadline?)` 只返回目录摘要;
234
+ `resolve({providerTabId, profile, options?}, deadline?)` 在重新核验页面控制和身份后返回编译配置、
235
+ 能力/安装 revision,以及必要运行时的确切 binding 与源码。它不执行页面安装或业务宿主服务。
236
+
237
+ 资源由完整 Native 能力服务解析,Skill 目录读取已注册快照;不会为发现目录重新执行 Skill 源文件。
238
+ 资源协议声明只表示读取口存在;显式读取时才刷新和校验安装,能力模块的慢初始化或失败不阻塞普通 Provider 控制宿主启动。
239
+ 含桥或全局变量且未显式指定运行时的档案,沿用已安装的 `app.defaultPageRuntime`;缺少运行时直接拒绝,
240
+ 不将其降级为已完整准备的 UA 配置。SDK 会再次核验定义 revision、页面证明和源码 SHA-256。
241
+ 独立的资源消息上限为 16 MiB,以容纳合法运行时的 JSON 编码;普通增强通道仍限 1 MiB。
242
+
243
+ Provider 应在自己的实例/页面范围中保存并引用已核验代码,保持原有资源配额与清理规则;
244
+ Space 清单不等于整个物理浏览器的目标清单,不能据此回收别人的资源。
245
+ 获得资源不代表后端已支持其根容器、桥回调或真实授权,只有逐项验证后才声明对应 App 能力。
246
+ 旧宿主缺少资源协议声明时明确拒绝,不回退到普通 action 或任意 Node 执行。
@@ -0,0 +1,15 @@
1
+ import { type ProviderAppResourceRequest, type ProviderAppResourceResponse } from '@mearl/browser-core';
2
+ export interface ProviderAppResourceClient {
3
+ catalog(params?: Extract<ProviderAppResourceRequest, {
4
+ operation: 'catalog';
5
+ }>['params'], deadline?: number): Promise<Extract<ProviderAppResourceResponse, {
6
+ catalog: unknown;
7
+ }>>;
8
+ resolve(request: Omit<Extract<ProviderAppResourceRequest, {
9
+ operation: 'resolve';
10
+ }>, 'operation'>, deadline?: number): Promise<Extract<ProviderAppResourceResponse, {
11
+ config: unknown;
12
+ }>>;
13
+ }
14
+ /** Reads accepted installed resources. It never installs code in a page or runs host services. */
15
+ export declare function createProviderAppResourceClient(providerId: string, providerBrowserId: string): ProviderAppResourceClient;
@@ -0,0 +1,55 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { PROVIDER_APP_RESOURCE_PROTOCOL_VERSION, MAX_PROVIDER_APP_RESOURCE_MESSAGE_BYTES, appProfileRevision, validateAppProfileDefinition, requireReadyProviderEnhancementTarget, BROWSER_DEVICE_PRESET_NAMES, } from '@mearl/browser-core';
3
+ import { providerBrowserKey } from './registry.js';
4
+ import { requestProviderHost } from './control-rpc.js';
5
+ /** Reads accepted installed resources. It never installs code in a page or runs host services. */
6
+ export function createProviderAppResourceClient(providerId, providerBrowserId) {
7
+ if (!providerId || !providerBrowserId)
8
+ throw new Error('Provider and browser IDs are required');
9
+ const target = providerBrowserKey(providerId, providerBrowserId);
10
+ async function request(data, deadline = Date.now() + 60_000) {
11
+ const result = await requestProviderHost(target, '__provider_app_resources', data, {
12
+ deadline, maximumBytes: MAX_PROVIDER_APP_RESOURCE_MESSAGE_BYTES,
13
+ assertHost(host) {
14
+ if (host.appResourceProtocolVersion !== PROVIDER_APP_RESOURCE_PROTOCOL_VERSION)
15
+ throw new Error('PROVIDER_APP_RESOURCES_UNSUPPORTED: update Mearl and restart the Provider control host');
16
+ },
17
+ });
18
+ for (const revision of [result.capabilityRevision, result.installationRevision])
19
+ if (revision !== null && (typeof revision !== 'string' || !/^[a-f0-9]{64}$/.test(revision)))
20
+ throw new Error('Invalid App resource revision');
21
+ return result;
22
+ }
23
+ return {
24
+ async catalog(params = {}, deadline) {
25
+ const result = await request({ operation: 'catalog', params }, deadline);
26
+ if (!('catalog' in result) || !result.catalog || !['available', 'partial', 'unsupported'].includes(result.catalog.status) ||
27
+ !Array.isArray(result.catalog.entries) || !Array.isArray(result.catalog.issues))
28
+ throw new Error('Invalid App resource catalog');
29
+ return result;
30
+ },
31
+ async resolve(data, deadline) {
32
+ if (!Number.isSafeInteger(data.providerTabId) || data.providerTabId < 0 || typeof data.profile !== 'string' || !data.profile || data.profile.length > 161)
33
+ throw new Error('Invalid App resource request');
34
+ const result = await request({ ...data, operation: 'resolve' }, deadline);
35
+ if (!('config' in result) || result.config?.binding?.version !== 1 || result.config.profile !== data.profile ||
36
+ typeof result.config.userAgent !== 'string' || !BROWSER_DEVICE_PRESET_NAMES.includes(result.recommendedPreset))
37
+ throw new Error('Invalid resolved App config');
38
+ const definition = validateAppProfileDefinition(result.config.binding.definition);
39
+ if (definition.name !== data.profile || result.config.binding.revision !== appProfileRevision(definition))
40
+ throw new Error('Invalid App definition revision');
41
+ requireReadyProviderEnhancementTarget(result.target, data.providerTabId);
42
+ const binding = result.config.pageRuntime;
43
+ if (binding?.id !== definition.pageRuntime?.id || binding?.sha256 !== definition.pageRuntime?.sha256)
44
+ throw new Error('App runtime binding mismatch');
45
+ if (binding) {
46
+ if (!result.runtime || result.runtime.binding.id !== binding.id || result.runtime.binding.sha256 !== binding.sha256 ||
47
+ typeof result.runtime.source !== 'string' || createHash('sha256').update(result.runtime.source).digest('hex') !== binding.sha256)
48
+ throw new Error('App runtime integrity check failed');
49
+ }
50
+ else if (result.runtime || definition.bridges.length || Object.keys(definition.globals).length)
51
+ throw new Error('Required App runtime is missing');
52
+ return result;
53
+ },
54
+ };
55
+ }
@@ -1,10 +1,20 @@
1
+ import type { BrowserControlSessionMode } from '@mearl/browser-core';
1
2
  import { type DaemonRecord } from '@mearl/daemon-core';
2
3
  export interface ProviderControlHostRecord extends DaemonRecord {
3
4
  socketPath: string;
4
5
  token: string;
5
6
  ready: boolean;
6
7
  protocolVersion: number;
8
+ /** Missing on older hosts, which only support Mearl sessions. */
9
+ controlSessionModes?: readonly BrowserControlSessionMode[];
10
+ enhancementProtocolVersion?: number;
11
+ enhancementNavigationOriginVersion?: number;
12
+ enhancementPreparationVersion?: number;
13
+ enhancementHostEnvironmentVersion?: number;
14
+ /** RPC support only; each read still prepares and validates the accepted App resources. */
15
+ appResourceProtocolVersion?: number;
7
16
  }
17
+ export declare function assertProviderControlSessionMode(host: Pick<ProviderControlHostRecord, 'controlSessionModes'> | null, mode?: BrowserControlSessionMode): void;
8
18
  export declare function providerControlHostFile(): string;
9
19
  export declare function providerControlSocketPath(pid: number): string;
10
20
  export declare function readProviderControlHost(): ProviderControlHostRecord | null;
@@ -4,6 +4,11 @@ import { tmpdir } from 'node:os';
4
4
  import { join } from 'node:path';
5
5
  import { assertProviderProtocolVersion, MEARL_PROVIDER_PROTOCOL_VERSION } from './protocol.js';
6
6
  import { isAlive, keyedConfigFile, claimRecord, readRecord, removeInvalidRecord, removeRecordIfPid, writeRecordIfPid, } from '@mearl/daemon-core';
7
+ export function assertProviderControlSessionMode(host, mode = 'mearl') {
8
+ if (mode === 'external' && (!Array.isArray(host?.controlSessionModes) || !host.controlSessionModes.includes('external'))) {
9
+ throw new Error('CONTROL_SESSION_MODE_UNSUPPORTED: the Provider requires external task control. Update Mearl and restart the Provider control host.');
10
+ }
11
+ }
7
12
  export function providerControlHostFile() {
8
13
  return keyedConfigFile('control', 'providers');
9
14
  }
@@ -0,0 +1,7 @@
1
+ import { type ProviderControlHostRecord } from './control-host.js';
2
+ /** Package-private transport shared by the two closed Provider host services. */
3
+ export declare function requestProviderHost<T>(providerTarget: string, action: '__provider_enhancement' | '__provider_app_resources', data: unknown, options: {
4
+ deadline: number;
5
+ maximumBytes: number;
6
+ assertHost(host: ProviderControlHostRecord): void;
7
+ }): Promise<T>;
@@ -0,0 +1,66 @@
1
+ import net from 'node:net';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { ensureProviderControlHost } from './control-host.js';
4
+ import { MEARL_PROVIDER_PROTOCOL_VERSION } from './protocol.js';
5
+ /** Package-private transport shared by the two closed Provider host services. */
6
+ export async function requestProviderHost(providerTarget, action, data, options) {
7
+ const remaining = () => {
8
+ const ms = options.deadline - Date.now();
9
+ if (!Number.isSafeInteger(options.deadline) || ms < 1 || ms > 2_147_483_647)
10
+ throw new Error('Provider host deadline exceeded or invalid');
11
+ return ms;
12
+ };
13
+ const host = await ensureProviderControlHost(Math.min(remaining(), 10_000));
14
+ options.assertHost(host);
15
+ const id = randomUUID();
16
+ const line = JSON.stringify({ id, action, providerTarget, protocolVersion: MEARL_PROVIDER_PROTOCOL_VERSION,
17
+ controlSource: 'local', deadline: options.deadline, data }) + '\n';
18
+ if (Buffer.byteLength(line) > options.maximumBytes)
19
+ throw new Error('Provider host request exceeded its byte limit');
20
+ const waitMs = remaining();
21
+ return new Promise((resolve, reject) => {
22
+ const socket = net.createConnection(host.socketPath);
23
+ socket.setEncoding('utf8');
24
+ let buffer = '', settled = false;
25
+ const finish = (error, result) => {
26
+ if (settled)
27
+ return;
28
+ settled = true;
29
+ clearTimeout(timer);
30
+ socket.removeAllListeners();
31
+ socket.destroy();
32
+ if (error)
33
+ reject(error);
34
+ else
35
+ resolve(result);
36
+ };
37
+ const timer = setTimeout(() => finish(new Error('Provider host request timed out; any dispatched effect is unconfirmed')), waitMs);
38
+ socket.on('connect', () => socket.write(line));
39
+ socket.on('data', chunk => {
40
+ buffer += chunk;
41
+ if (Buffer.byteLength(buffer) > options.maximumBytes)
42
+ return finish(new Error('Provider host response exceeded its byte limit'));
43
+ while (buffer.includes('\n')) {
44
+ const index = buffer.indexOf('\n'), line = buffer.slice(0, index);
45
+ buffer = buffer.slice(index + 1);
46
+ let response;
47
+ try {
48
+ response = JSON.parse(line);
49
+ }
50
+ catch {
51
+ return finish(new Error('Invalid Provider host response'));
52
+ }
53
+ if (response?.id !== id)
54
+ continue;
55
+ if (response.success !== true)
56
+ return finish(new Error(response.error ?? 'Provider host request failed'));
57
+ if (!response.data || typeof response.data !== 'object')
58
+ return finish(new Error('Invalid Provider host response'));
59
+ return finish(undefined, response.data);
60
+ }
61
+ });
62
+ socket.on('error', error => finish(error));
63
+ socket.on('end', () => finish(new Error('Provider host connection closed before its response')));
64
+ socket.on('close', () => finish(new Error('Provider host connection closed before its response')));
65
+ });
66
+ }
@@ -0,0 +1,19 @@
1
+ import { type BrowserEnhancementInvocation, type ProviderEnhancementBinding, type ProviderEnhancementPreparation, type ProviderEnhancementRequest } from '@mearl/browser-core';
2
+ export interface ProviderEnhancementClient {
3
+ inspect(request: Omit<Extract<ProviderEnhancementRequest, {
4
+ operation: 'inspect';
5
+ }>, 'operation'>, deadline?: number): Promise<ProviderEnhancementPreparation>;
6
+ bind(request: Omit<Extract<ProviderEnhancementRequest, {
7
+ operation: 'bind';
8
+ }>, 'operation'>, deadline?: number): Promise<ProviderEnhancementBinding>;
9
+ invoke(binding: ProviderEnhancementBinding, invocation: BrowserEnhancementInvocation, deadline?: number): Promise<unknown>;
10
+ release(binding: ProviderEnhancementBinding, deadline?: number): Promise<{
11
+ released: boolean;
12
+ conflicts?: string[];
13
+ }>;
14
+ }
15
+ /** Uses the accepted instance's narrow control-host gateway; never forwards ordinary actions. */
16
+ export declare function createProviderEnhancementClient(providerId: string, providerBrowserId: string, options?: {
17
+ timeoutMs?: number;
18
+ requireHostEnvironment?: boolean;
19
+ }): ProviderEnhancementClient;
@@ -0,0 +1,100 @@
1
+ import { BROWSER_ENHANCEMENT_PROTOCOL_VERSION, BROWSER_ENHANCEMENT_NAVIGATION_ORIGIN_VERSION, BROWSER_ENHANCEMENT_PREPARATION_VERSION, isBrowserEnhancementPreparationUrl, BROWSER_ENHANCEMENT_HOST_ENVIRONMENT_VERSION, isBrowserEnhancementAction, isEnhancementNavigationOrigin, requiredBrowserEnhancementActions, } from '@mearl/browser-core';
2
+ import { requestProviderHost } from './control-rpc.js';
3
+ import { providerBrowserKey } from './registry.js';
4
+ /** Uses the accepted instance's narrow control-host gateway; never forwards ordinary actions. */
5
+ export function createProviderEnhancementClient(providerId, providerBrowserId, options = {}) {
6
+ if (!providerId || !providerBrowserId)
7
+ throw new Error('Provider and browser IDs are required');
8
+ const timeoutMs = options.timeoutMs ?? 60_000;
9
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 300_000)
10
+ throw new Error('Enhancement timeout must be between 1 and 300000 milliseconds');
11
+ const providerTarget = providerBrowserKey(providerId, providerBrowserId);
12
+ const uncertain = new Set();
13
+ async function request(data, callerDeadline) {
14
+ return requestProviderHost(providerTarget, '__provider_enhancement', data, {
15
+ deadline: callerDeadline ?? Date.now() + timeoutMs, maximumBytes: 1024 * 1024,
16
+ assertHost(host) {
17
+ if (host.enhancementProtocolVersion !== BROWSER_ENHANCEMENT_PROTOCOL_VERSION)
18
+ throw new Error('BROWSER_ENHANCEMENT_UNSUPPORTED: update Mearl and restart the Provider control host');
19
+ // Support negotiation only. The Creator's verified initial environment decides the policy.
20
+ if (options.requireHostEnvironment && host.enhancementHostEnvironmentVersion !== BROWSER_ENHANCEMENT_HOST_ENVIRONMENT_VERSION)
21
+ throw new Error('BROWSER_ENHANCEMENT_UNSUPPORTED: the control host cannot preserve the captured host environment');
22
+ if (data.operation === 'inspect' && host.enhancementPreparationVersion !== BROWSER_ENHANCEMENT_PREPARATION_VERSION)
23
+ throw new Error('BROWSER_ENHANCEMENT_UNSUPPORTED: the control host cannot negotiate a preparation page');
24
+ if ((data.operation === 'bind' || data.operation === 'inspect') && data.navigationOrigin !== undefined &&
25
+ host.enhancementNavigationOriginVersion !== BROWSER_ENHANCEMENT_NAVIGATION_ORIGIN_VERSION)
26
+ throw new Error('BROWSER_ENHANCEMENT_UNSUPPORTED: the control host cannot authorize a first navigation origin');
27
+ },
28
+ });
29
+ }
30
+ function assertConfirmed(binding) {
31
+ if (uncertain.has(binding.bindingId))
32
+ throw new Error('ENVIRONMENT_UNCONFIRMED: reverify and rebind before another mutation');
33
+ }
34
+ return {
35
+ async inspect(data, deadline) {
36
+ if (!Number.isSafeInteger(data.providerTabId) || data.providerTabId < 0 || !isEnhancementNavigationOrigin(data.navigationOrigin) ||
37
+ !Array.isArray(data.actions) || !data.actions.length || data.actions.some(action => !isBrowserEnhancementAction(action)))
38
+ throw new Error('Invalid enhancement inspection request');
39
+ const result = await request({ ...data, operation: 'inspect' }, deadline);
40
+ if (!('preparation' in result) || result.preparation.providerTabId !== data.providerTabId ||
41
+ !Number.isSafeInteger(result.preparation.tabId) || result.preparation.tabId < 0 ||
42
+ typeof result.preparation.targetId !== 'string' || !result.preparation.targetId || result.preparation.targetId.length > 256 ||
43
+ typeof result.preparation.nativeBrowserId !== 'string' || !result.preparation.nativeBrowserId ||
44
+ (data.nativeBrowserId !== undefined && result.preparation.nativeBrowserId !== data.nativeBrowserId) ||
45
+ !isBrowserEnhancementPreparationUrl(result.preparation.url) ||
46
+ result.preparation.capabilities?.protocolVersion !== BROWSER_ENHANCEMENT_PROTOCOL_VERSION ||
47
+ result.preparation.capabilities?.preparationVersion !== BROWSER_ENHANCEMENT_PREPARATION_VERSION ||
48
+ !Array.isArray(result.preparation.capabilities.actions) || data.actions.some(action => !result.preparation.capabilities.actions.includes(action)))
49
+ throw new Error('Invalid enhancement preparation response');
50
+ return result.preparation;
51
+ },
52
+ async bind(data, deadline) {
53
+ if (!Number.isSafeInteger(data.providerTabId) || data.providerTabId < 0 || !Array.isArray(data.actions) ||
54
+ (data.navigationOrigin !== undefined && !isEnhancementNavigationOrigin(data.navigationOrigin)) ||
55
+ !data.actions.length || data.actions.some(action => !isBrowserEnhancementAction(action)))
56
+ throw new Error('Invalid enhancement binding request');
57
+ const result = await request({ ...data, operation: 'bind' }, deadline);
58
+ if (!('binding' in result) || typeof result.binding?.bindingId !== 'string' || !result.binding.bindingId ||
59
+ !Number.isSafeInteger(result.binding.generation) || result.binding.generation < 1 ||
60
+ !Number.isSafeInteger(result.binding.tabId) || result.binding.tabId < 0 ||
61
+ typeof result.binding.mainFrameId !== 'string' || !result.binding.mainFrameId ||
62
+ (result.binding.targetId !== undefined && (typeof result.binding.targetId !== 'string' || !result.binding.targetId || result.binding.targetId.length > 256)) ||
63
+ typeof result.binding.nativeBrowserId !== 'string' || !result.binding.nativeBrowserId ||
64
+ (data.nativeBrowserId !== undefined && result.binding.nativeBrowserId !== data.nativeBrowserId) ||
65
+ result.binding.providerTabId !== data.providerTabId || !Array.isArray(result.binding.actions) ||
66
+ data.actions.some(action => !result.binding.actions.includes(action)))
67
+ throw new Error('Invalid enhancement binding response');
68
+ uncertain.delete(result.binding.bindingId);
69
+ return result.binding;
70
+ },
71
+ async invoke(binding, invocation, deadline) {
72
+ assertConfirmed(binding);
73
+ if (!isBrowserEnhancementAction(invocation.action) || requiredBrowserEnhancementActions(invocation).some(action => !binding.actions.includes(action)))
74
+ throw new Error('BROWSER_ENHANCEMENT_SCOPE_MISMATCH');
75
+ try {
76
+ const result = await request({ ...invocation, operation: 'invoke', bindingId: binding.bindingId, generation: binding.generation }, deadline);
77
+ if (!('result' in result))
78
+ throw new Error('Invalid enhancement invocation response');
79
+ return result.result;
80
+ }
81
+ catch (error) {
82
+ uncertain.add(binding.bindingId);
83
+ throw error;
84
+ }
85
+ },
86
+ async release(binding, deadline) {
87
+ assertConfirmed(binding);
88
+ try {
89
+ const result = await request({ operation: 'release', bindingId: binding.bindingId, generation: binding.generation }, deadline);
90
+ if (!('released' in result) || typeof result.released !== 'boolean')
91
+ throw new Error('Invalid enhancement release response');
92
+ return result;
93
+ }
94
+ catch (error) {
95
+ uncertain.add(binding.bindingId);
96
+ throw error;
97
+ }
98
+ },
99
+ };
100
+ }
package/dist/host.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export { listInstalledProviders, validateInstalledProviderManifest, registerProviderPackage, stopProvider, readLiveProvider, readProviderStart, uninstallProvider, } from './registry.js';
2
2
  export type { InstalledProviderManifest, ProviderPackageMetadata } from './types.js';
3
- export { ensureProviderControlHost, readProviderControlHost, providerControlHostFile, providerControlSocketPath, } from './control-host.js';
3
+ export { ensureProviderControlHost, assertProviderControlSessionMode, readProviderControlHost, providerControlHostFile, providerControlSocketPath, } from './control-host.js';
4
4
  export type { ProviderControlHostRecord } from './control-host.js';
package/dist/host.js CHANGED
@@ -1,2 +1,2 @@
1
1
  export { listInstalledProviders, validateInstalledProviderManifest, registerProviderPackage, stopProvider, readLiveProvider, readProviderStart, uninstallProvider, } from './registry.js';
2
- export { ensureProviderControlHost, readProviderControlHost, providerControlHostFile, providerControlSocketPath, } from './control-host.js';
2
+ export { ensureProviderControlHost, assertProviderControlSessionMode, readProviderControlHost, providerControlHostFile, providerControlSocketPath, } from './control-host.js';
package/dist/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export * from './types.js';
2
+ export { createProviderEnhancementClient, type ProviderEnhancementClient } from './enhancements.js';
3
+ export { createProviderAppResourceClient, type ProviderAppResourceClient } from './app-resources.js';
2
4
  export { assertProviderProtocolVersion } from './protocol.js';
3
5
  export { PROVIDER_ACTIONS, isProviderAction } from './actions.js';
4
6
  export { providerDataDir } from './registry.js';
package/dist/index.js CHANGED
@@ -1,4 +1,6 @@
1
1
  export * from './types.js';
2
+ export { createProviderEnhancementClient } from './enhancements.js';
3
+ export { createProviderAppResourceClient } from './app-resources.js';
2
4
  export { assertProviderProtocolVersion } from './protocol.js';
3
5
  export { PROVIDER_ACTIONS, isProviderAction } from './actions.js';
4
6
  export { providerDataDir } from './registry.js';
package/dist/launcher.js CHANGED
@@ -95,17 +95,28 @@ export async function ensureInstalledProvidersRunning(providerIds) {
95
95
  }
96
96
  if (starting.size === 0)
97
97
  return warnings;
98
- const deadline = Date.now() + 2_000;
99
- let remaining = [...starting.keys()];
100
- while (remaining.length > 0 && Date.now() < deadline) {
98
+ const startupTimeout = (providerId) => installed.find(manifest => manifest.providerId === providerId)?.browserType.controlSessionModes?.includes('external')
99
+ ? 12_000 : 2_000;
100
+ const deadlines = new Map([...starting.keys()].map(id => [id, Date.now() + startupTimeout(id)]));
101
+ const remaining = new Set(starting.keys());
102
+ const waiting = new Set(starting.keys());
103
+ while (waiting.size > 0) {
101
104
  await delay(100);
102
105
  const current = new Set(listLiveProviders().map(record => record.providerId));
103
- remaining = remaining.filter(providerId => !current.has(providerId));
106
+ for (const providerId of waiting) {
107
+ if (current.has(providerId)) {
108
+ waiting.delete(providerId);
109
+ remaining.delete(providerId);
110
+ }
111
+ else if (Date.now() >= deadlines.get(providerId)) {
112
+ waiting.delete(providerId);
113
+ }
114
+ }
104
115
  }
105
116
  for (const [providerId, token] of starting) {
106
- if (!remaining.includes(providerId) && token)
117
+ if (!remaining.has(providerId) && token)
107
118
  releaseProviderStart(providerId, token);
108
- if (!remaining.includes(providerId)) {
119
+ if (!remaining.has(providerId)) {
109
120
  try {
110
121
  hasCompatibleLiveProvider(providerId);
111
122
  }
@@ -116,7 +127,7 @@ export async function ensureInstalledProvidersRunning(providerIds) {
116
127
  }
117
128
  for (const providerId of remaining) {
118
129
  const logFile = providerLogFile(providerId);
119
- warnings.push(`provider ${providerId}: did not become ready within 2s${existsSync(logFile) ? `; see ${logFile}` : ''}`);
130
+ warnings.push(`provider ${providerId}: did not become ready within ${startupTimeout(providerId) / 1000}s${existsSync(logFile) ? `; see ${logFile}` : ''}`);
120
131
  }
121
132
  // A ready provider may still be publishing its first browser inventory.
122
133
  await delay(100);
@@ -1,2 +1,4 @@
1
+ import type { BrowserControlSessionMode } from '@mearl/browser-core';
1
2
  export declare const MEARL_PROVIDER_PROTOCOL_VERSION = 3;
3
+ export declare function isProviderControlSessionModes(value: unknown): value is BrowserControlSessionMode[];
2
4
  export declare function assertProviderProtocolVersion(actual: unknown, peer?: string): void;
package/dist/protocol.js CHANGED
@@ -1,4 +1,9 @@
1
1
  export const MEARL_PROVIDER_PROTOCOL_VERSION = 3;
2
+ export function isProviderControlSessionModes(value) {
3
+ return Array.isArray(value) && value.length > 0 &&
4
+ value.every(mode => mode === 'mearl' || mode === 'external') &&
5
+ new Set(value).size === value.length;
6
+ }
2
7
  export function assertProviderProtocolVersion(actual, peer = 'Provider') {
3
8
  if (actual !== MEARL_PROVIDER_PROTOCOL_VERSION) {
4
9
  throw new Error(`PROVIDER_PROTOCOL_MISMATCH: ${peer} uses protocol ${String(actual ?? 'unknown')}; expected ${MEARL_PROVIDER_PROTOCOL_VERSION}. Update Mearl and the Provider package, then restart the services.`);
@@ -1,5 +1,7 @@
1
1
  import { MEARL_PROVIDER_PROTOCOL_VERSION } from './types.js';
2
2
  import { isProviderAction } from './actions.js';
3
+ import { isProviderControlSessionModes } from './protocol.js';
4
+ import { isProviderEnhancementTarget } from '@mearl/browser-core';
3
5
  const LIFECYCLE_OPERATIONS = new Set([
4
6
  'start',
5
7
  'close',
@@ -8,6 +10,10 @@ const LIFECYCLE_OPERATIONS = new Set([
8
10
  'access',
9
11
  'syncCookies',
10
12
  ]);
13
+ function assertDeadline(context) {
14
+ if (context.deadline !== undefined && (!Number.isSafeInteger(context.deadline) || Date.now() >= context.deadline))
15
+ throw new Error('Provider request deadline exceeded');
16
+ }
11
17
  function isJsonValue(value, depth = 0) {
12
18
  if (depth > 32)
13
19
  return false;
@@ -329,6 +335,13 @@ export function validateProviderBrowser(browser) {
329
335
  if (!['native', 'reconstructed', 'none'].includes(capabilities.screenshot)) {
330
336
  throw new Error(`Provider browser ${browser.browserId} has invalid screenshot capabilities`);
331
337
  }
338
+ if (capabilities.controlSessionMode !== undefined &&
339
+ !['mearl', 'external'].includes(capabilities.controlSessionMode)) {
340
+ throw new Error(`Provider browser ${browser.browserId} has invalid control session mode`);
341
+ }
342
+ if (browser.control.kind === 'host' && capabilities.controlSessionMode === 'external') {
343
+ throw new Error('External control sessions require Provider control');
344
+ }
332
345
  const supportsScreenshot = capabilities.actions.includes('page_screenshot');
333
346
  if (supportsScreenshot === (capabilities.screenshot === 'none')) {
334
347
  throw new Error(`Provider browser ${browser.browserId} has inconsistent screenshot capabilities`);
@@ -390,6 +403,9 @@ function validateDefinition(definition) {
390
403
  }
391
404
  if (!definition.browserType.name?.trim())
392
405
  throw new Error('provider browser type name is required');
406
+ if (definition.browserType.controlSessionModes !== undefined &&
407
+ !isProviderControlSessionModes(definition.browserType.controlSessionModes))
408
+ throw new Error('Provider control session modes are invalid');
393
409
  if (!isProviderInputSchema(definition.browserType.inputSchema) ||
394
410
  definition.browserType.inputSchema.type !== 'object') {
395
411
  throw new Error('Provider input schema is invalid');
@@ -454,8 +470,24 @@ export function validateProviderLaunchForm(form) {
454
470
  export function createProviderRuntime(definition) {
455
471
  validateDefinition(definition);
456
472
  let publishedBrowsers = [];
473
+ const declaredModes = new Set(definition.browserType.controlSessionModes ?? ['mearl']);
474
+ const controlModes = new Map();
475
+ const rememberControlMode = (browser) => {
476
+ if (browser.control.kind !== 'none') {
477
+ controlModes.set(browser.browserId, browser.control.capabilities?.controlSessionMode ?? 'mearl');
478
+ }
479
+ };
457
480
  const validateRuntimeBrowser = (browser) => {
458
481
  validateProviderBrowser(browser);
482
+ if (browser.control.kind !== 'none') {
483
+ const previous = controlModes.get(browser.browserId);
484
+ const mode = browser.control.capabilities?.controlSessionMode ?? 'mearl';
485
+ if (!declaredModes.has(mode))
486
+ throw new Error(`Provider did not declare control session mode: ${mode}`);
487
+ if (previous !== undefined && previous !== mode) {
488
+ throw new Error('CONTROL_SESSION_MODE_CHANGED: use a new browserId to change task control mode');
489
+ }
490
+ }
459
491
  const implementations = {
460
492
  start: definition.start,
461
493
  detach: definition.detach,
@@ -470,6 +502,7 @@ export function createProviderRuntime(definition) {
470
502
  }
471
503
  };
472
504
  const listBrowsers = async (context = {}) => {
505
+ assertDeadline(context);
473
506
  const browsers = definition.listBrowsers
474
507
  ? await definition.listBrowsers(context)
475
508
  : publishedBrowsers;
@@ -481,6 +514,7 @@ export function createProviderRuntime(definition) {
481
514
  }
482
515
  seen.add(browser.browserId);
483
516
  }
517
+ browsers.forEach(rememberControlMode);
484
518
  return browsers;
485
519
  };
486
520
  const requireBrowser = async (browserId, operation, context) => {
@@ -490,6 +524,7 @@ export function createProviderRuntime(definition) {
490
524
  if (!browser.operations.includes(operation)) {
491
525
  throw new Error(`Provider browser ${browserId} does not support ${operation}`);
492
526
  }
527
+ assertDeadline(context);
493
528
  return browser;
494
529
  };
495
530
  return {
@@ -500,9 +535,11 @@ export function createProviderRuntime(definition) {
500
535
  if (new Set(browsers.map(browser => browser.browserId)).size !== browsers.length) {
501
536
  throw new Error('Provider browser ids must be unique');
502
537
  }
538
+ browsers.forEach(rememberControlMode);
503
539
  publishedBrowsers = [...browsers];
504
540
  },
505
541
  async getLaunchForm(request, context = {}) {
542
+ assertDeadline(context);
506
543
  if (!definition.browserType.dynamicLaunchForm || !definition.getLaunchForm)
507
544
  throw new Error('Provider does not support dynamic launch forms');
508
545
  if (!request ||
@@ -523,9 +560,11 @@ export function createProviderRuntime(definition) {
523
560
  return form;
524
561
  },
525
562
  async launch(options, context = {}) {
563
+ assertDeadline(context);
526
564
  validateProviderInput(definition.browserType.inputSchema, options);
527
565
  const browser = await definition.launch(options, context);
528
566
  validateRuntimeBrowser(browser);
567
+ rememberControlMode(browser);
529
568
  return browser;
530
569
  },
531
570
  async start(request, context = {}) {
@@ -534,6 +573,7 @@ export function createProviderRuntime(definition) {
534
573
  throw new Error('Provider does not implement start');
535
574
  const browser = await definition.start(request, context);
536
575
  validateRuntimeBrowser(browser);
576
+ rememberControlMode(browser);
537
577
  return browser;
538
578
  },
539
579
  async close(request, context = {}) {
@@ -581,6 +621,7 @@ export function createProviderRuntime(definition) {
581
621
  return { ...access, url: url.toString() };
582
622
  },
583
623
  async configure(options, context = {}) {
624
+ assertDeadline(context);
584
625
  const configuration = definition.browserType.configuration;
585
626
  if (!configuration || !definition.configure) {
586
627
  throw new Error('Provider does not support configuration');
@@ -588,8 +629,32 @@ export function createProviderRuntime(definition) {
588
629
  validateProviderInput(configuration.inputSchema, options);
589
630
  return definition.configure(options, context);
590
631
  },
632
+ async getEnhancementTarget(request, context = {}) {
633
+ assertDeadline(context);
634
+ if (!definition.getEnhancementTarget)
635
+ throw new Error('BROWSER_ENHANCEMENT_TARGET_UNSUPPORTED');
636
+ if (!Number.isSafeInteger(request.tabId) || request.tabId < 0)
637
+ throw new Error('A valid Provider tabId is required');
638
+ const browser = (await listBrowsers(context)).find(item => item.browserId === request.browserId);
639
+ if (browser?.status !== 'connected' || browser.control.kind !== 'provider' ||
640
+ browser.control.capabilities.controlSessionMode !== 'external')
641
+ throw new Error('BROWSER_ENHANCEMENT_OWNER_UNAVAILABLE');
642
+ assertDeadline(context);
643
+ const target = await definition.getEnhancementTarget(request, context);
644
+ if (!isProviderEnhancementTarget(target) || target.tabId !== request.tabId)
645
+ throw new Error('Invalid Provider enhancement target');
646
+ assertDeadline(context);
647
+ return target.ready
648
+ ? { tabId: target.tabId, ready: true, ...(target.mainFrameId ? { mainFrameId: target.mainFrameId } : {}),
649
+ ...(target.targetId ? { targetId: target.targetId } : {}), url: target.url,
650
+ ...(target.initialEnvironment ? { initialEnvironment: structuredClone(target.initialEnvironment) } : {}),
651
+ }
652
+ : { tabId: target.tabId, ready: false };
653
+ },
591
654
  async invokeAction(actionContext) {
592
- const browser = (await listBrowsers({ controlSource: actionContext.controlSource })).find(item => item.browserId === actionContext.browserId);
655
+ const browser = (await listBrowsers({ controlSource: actionContext.controlSource,
656
+ ...(actionContext.deadline !== undefined ? { deadline: actionContext.deadline } : {}),
657
+ })).find(item => item.browserId === actionContext.browserId);
593
658
  if (!browser)
594
659
  throw new Error(`Provider browser not found: ${actionContext.browserId}`);
595
660
  if (browser.status !== 'connected') {
@@ -603,6 +668,7 @@ export function createProviderRuntime(definition) {
603
668
  }
604
669
  if (!definition.handleAction)
605
670
  throw new Error('Provider does not implement browser actions');
671
+ assertDeadline(actionContext);
606
672
  return definition.handleAction(actionContext);
607
673
  },
608
674
  };
package/dist/registry.js CHANGED
@@ -4,7 +4,7 @@ import { isAbsolute, join, relative, resolve, sep } from 'node:path';
4
4
  import { claimRecord, ensureDir, hashKey, isAlive, keyedConfigFile, keyedDir, listRecordFiles, readLiveRecords, readRecord, removeInvalidRecord, removeRecordIfPid, writeRecord, writeRecordIfPid, } from '@mearl/daemon-core';
5
5
  import { MEARL_PROVIDER_PROTOCOL_VERSION, } from './types.js';
6
6
  import { validateProviderLaunchForm, isProviderInputSchema } from './provider-runtime.js';
7
- import { assertProviderProtocolVersion } from './protocol.js';
7
+ import { assertProviderProtocolVersion, isProviderControlSessionModes } from './protocol.js';
8
8
  export const INSTALLED_PROVIDERS_SUBDIR = 'providers/installed';
9
9
  export const LIVE_PROVIDERS_SUBDIR = 'providers/live';
10
10
  export const STARTING_PROVIDERS_SUBDIR = 'providers/starting';
@@ -149,6 +149,8 @@ function isProviderBrowserType(value) {
149
149
  const browserType = value;
150
150
  if (typeof browserType.name !== 'string' || !browserType.name.trim())
151
151
  return false;
152
+ if (browserType.controlSessionModes !== undefined && !isProviderControlSessionModes(browserType.controlSessionModes))
153
+ return false;
152
154
  if (browserType.documentationUrl !== undefined &&
153
155
  (typeof browserType.documentationUrl !== 'string' ||
154
156
  !/^https?:\/\//.test(browserType.documentationUrl)))
package/dist/server.js CHANGED
@@ -4,12 +4,19 @@ import { chmodSync, rmSync } from 'node:fs';
4
4
  import { dirname } from 'node:path';
5
5
  import { ensureDir } from '@mearl/daemon-core';
6
6
  import { isProviderAction } from './actions.js';
7
- import { createProviderRuntime } from './provider-runtime.js';
7
+ import { createProviderRuntime, validateProviderBrowser } from './provider-runtime.js';
8
8
  import { PROVIDER_START_TOKEN_ENV, claimProviderStart, providerBrowserKey, providerSocketPath, publishLiveProvider, publishProviderBrowsers, readLiveProvider, releaseProviderStart, removeStaleProviderSocket, touchProviderBrowser, unpublishLiveProvider, unpublishProviderBrowsers, } from './registry.js';
9
9
  import { MEARL_PROVIDER_PROTOCOL_VERSION } from './types.js';
10
10
  import { assertProviderProtocolVersion } from './protocol.js';
11
+ import { assertProviderControlSessionMode, ensureProviderControlHost, readProviderControlHost } from './control-host.js';
11
12
  const MAX_REQUEST_BYTES = 16 * 1024 * 1024;
12
13
  const activeProviderIds = new Set();
14
+ function assertControlHost(browser) {
15
+ validateProviderBrowser(browser);
16
+ if (browser.control.kind === 'provider' && browser.control.capabilities.controlSessionMode === 'external') {
17
+ assertProviderControlSessionMode(readProviderControlHost(), 'external');
18
+ }
19
+ }
13
20
  function errorMessage(error) {
14
21
  return error instanceof Error ? error.message : String(error);
15
22
  }
@@ -31,6 +38,7 @@ function isProviderActionRequest(value) {
31
38
  typeof request.action === 'string' &&
32
39
  request.action.length > 0 &&
33
40
  typeof request.protocolVersion === 'number' &&
41
+ (request.deadline === undefined || (Number.isSafeInteger(request.deadline) && request.deadline > 0)) &&
34
42
  (request.data === undefined ||
35
43
  (request.data !== null &&
36
44
  typeof request.data === 'object' &&
@@ -44,6 +52,7 @@ function providerContext(request) {
44
52
  const hostData = request.host;
45
53
  return {
46
54
  controlSource: request.controlSource,
55
+ ...(request.deadline !== undefined ? { deadline: request.deadline } : {}),
47
56
  ...(hostData
48
57
  ? {
49
58
  host: {
@@ -65,6 +74,7 @@ function providerContext(request) {
65
74
  export function createProviderServer(options) {
66
75
  const runtime = createProviderRuntime(options.definition);
67
76
  const descriptor = options.definition.descriptor;
77
+ const requiresExternalControl = options.definition.browserType.controlSessionModes?.includes('external') === true;
68
78
  const socketPath = options.socketPath ?? providerSocketPath(descriptor.providerId);
69
79
  let server = null;
70
80
  let browserIds = new Set();
@@ -72,20 +82,43 @@ export function createProviderServer(options) {
72
82
  let state = 'idle';
73
83
  let stopPromise = null;
74
84
  const connections = new Set();
85
+ async function prepareControlHost() {
86
+ if (requiresExternalControl) {
87
+ assertProviderControlSessionMode(await ensureProviderControlHost(), 'external');
88
+ }
89
+ }
75
90
  async function providerBrowserId(requested) {
76
- if (requested)
77
- return requested;
78
91
  const available = await runtime.listBrowsers();
79
- if (available.length === 1)
80
- return available[0].browserId;
92
+ const selected = requested
93
+ ? available.find(browser => browser.browserId === requested)
94
+ : available.length === 1 ? available[0] : undefined;
95
+ if (selected) {
96
+ assertControlHost(selected);
97
+ return selected.browserId;
98
+ }
99
+ if (requested)
100
+ throw new Error(`Provider browser not found: ${requested}`);
81
101
  if (available.length === 0)
82
102
  throw new Error('Provider has no browser instances');
83
103
  throw new Error('browserId is required when the provider has multiple browsers');
84
104
  }
85
105
  async function dispatch(request) {
106
+ const assertDeadline = () => {
107
+ if (request.deadline !== undefined && Date.now() >= request.deadline)
108
+ throw new Error('Provider request deadline exceeded');
109
+ };
110
+ assertDeadline();
86
111
  if (request.controlSessionId)
87
112
  throw new Error('CONTROL_SESSION_UNSUPPORTED: this Provider does not implement control sessions');
113
+ await prepareControlHost();
114
+ assertDeadline();
88
115
  const context = providerContext(request);
116
+ if (request.action === '__provider_enhancement_target') {
117
+ const browserId = await providerBrowserId(request.browserId);
118
+ if (!runtime.getEnhancementTarget)
119
+ throw new Error('BROWSER_ENHANCEMENT_TARGET_UNSUPPORTED');
120
+ return runtime.getEnhancementTarget({ browserId, tabId: request.data?.tabId }, context);
121
+ }
89
122
  if (request.action === 'browser_providers') {
90
123
  if (request.browserId)
91
124
  throw new Error('Provider management does not accept browserId');
@@ -104,7 +137,9 @@ export function createProviderServer(options) {
104
137
  if (request.action === 'browser_launch') {
105
138
  if (request.browserId)
106
139
  throw new Error('browser_launch does not accept browserId');
107
- return runtime.launch(request.data ?? {}, context);
140
+ const browser = await runtime.launch(request.data ?? {}, context);
141
+ assertControlHost(browser);
142
+ return browser;
108
143
  }
109
144
  if (request.action === 'browser_instances') {
110
145
  if (!['start', 'detach', 'sync_cookies'].includes(String(request.data?.operation))) {
@@ -156,6 +191,7 @@ export function createProviderServer(options) {
156
191
  action: request.action,
157
192
  data: request.data ?? {},
158
193
  controlSource: request.controlSource,
194
+ ...(request.deadline !== undefined ? { deadline: request.deadline } : {}),
159
195
  });
160
196
  }
161
197
  function handleConnection(socket) {
@@ -229,6 +265,7 @@ export function createProviderServer(options) {
229
265
  throw new Error(`Provider ${providerId} start is already in progress`);
230
266
  }
231
267
  try {
268
+ await prepareControlHost();
232
269
  const competing = readLiveProvider(providerId);
233
270
  if (competing) {
234
271
  throw new Error(`Provider ${providerId} is already running (pid ${competing.pid})`);
@@ -271,6 +308,7 @@ export function createProviderServer(options) {
271
308
  updateBrowsers(nextBrowsers) {
272
309
  if (state !== 'running')
273
310
  throw new Error('Provider server is not running');
311
+ nextBrowsers.forEach(assertControlHost);
274
312
  runtime.updateBrowsers(nextBrowsers);
275
313
  browsers = [...nextBrowsers];
276
314
  browserIds = publishProviderBrowsers(descriptor, socketPath, browsers, browserIds);
package/dist/types.d.ts CHANGED
@@ -1,15 +1,15 @@
1
1
  import type { ProviderAction } from './actions.js';
2
+ import type { BrowserCapabilities, BrowserControlSessionMode, ProviderEnhancementTarget } from '@mearl/browser-core';
2
3
  export { MEARL_PROVIDER_PROTOCOL_VERSION } from './protocol.js';
3
4
  export type { ProviderAction } from './actions.js';
5
+ export type { BrowserEnhancementInvocation, ProviderEnhancementBinding, ProviderEnhancementTarget } from '@mearl/browser-core';
4
6
  export type JsonPrimitive = string | number | boolean | null;
5
7
  export type JsonValue = JsonPrimitive | JsonValue[] | JsonObject;
6
8
  export interface JsonObject {
7
9
  [key: string]: JsonValue;
8
10
  }
9
11
  export type ProviderScreenshotKind = 'native' | 'reconstructed' | 'none';
10
- export interface ProviderCapabilities {
11
- actions: ProviderAction[];
12
- screenshot: ProviderScreenshotKind;
12
+ export interface ProviderCapabilities extends BrowserCapabilities {
13
13
  }
14
14
  /** Completed download staged on the local Provider process host for Mearl transfer. */
15
15
  export interface ProviderDownloadResult {
@@ -123,6 +123,8 @@ export interface ProviderConfigurationDefinition {
123
123
  }
124
124
  export interface ProviderBrowserType {
125
125
  name: string;
126
+ /** Host must support every listed mode before launch. Omitted means Mearl only. */
127
+ controlSessionModes?: readonly BrowserControlSessionMode[];
126
128
  documentationUrl?: string;
127
129
  /** Provider-owned usage guidance rendered by `mearl <providerId>`. */
128
130
  help?: readonly string[];
@@ -198,6 +200,8 @@ export interface ProviderActionRequest {
198
200
  browserId?: string;
199
201
  version?: string;
200
202
  protocolVersion: number;
203
+ /** Absolute caller deadline, outside Provider business options. */
204
+ deadline?: number;
201
205
  controlSource?: string;
202
206
  host?: ProviderHostContextData;
203
207
  }
@@ -206,6 +210,7 @@ export interface ProviderActionContext {
206
210
  action: ProviderAction;
207
211
  data: JsonObject;
208
212
  controlSource?: string;
213
+ deadline?: number;
209
214
  }
210
215
  export interface ProviderHostServices {
211
216
  request<T = JsonValue>(method: string, data?: JsonObject): Promise<T>;
@@ -218,6 +223,7 @@ export interface ProviderHostContextData {
218
223
  export interface ProviderContext {
219
224
  host?: ProviderHostServices;
220
225
  controlSource?: string;
226
+ deadline?: number;
221
227
  }
222
228
  export interface ProviderInstanceRequest {
223
229
  browserId: string;
@@ -245,6 +251,10 @@ export interface BrowserProviderDefinition {
245
251
  getAccess?: (request: ProviderInstanceRequest, context: ProviderContext) => Promise<ProviderAccess>;
246
252
  configure?: (options: JsonObject, context: ProviderContext) => Promise<unknown>;
247
253
  getLaunchForm?: (request: ProviderLaunchFormRequest, context: ProviderContext) => Promise<ProviderLaunchForm>;
254
+ /** Reentrant read-only preflight: must reflect current control and identity without claiming a page. */
255
+ getEnhancementTarget?: (request: ProviderInstanceRequest & {
256
+ tabId: number;
257
+ }, context: ProviderContext) => Promise<ProviderEnhancementTarget>;
248
258
  handleAction?: (context: ProviderActionContext) => Promise<unknown>;
249
259
  }
250
260
  export interface ProviderRuntime {
@@ -260,6 +270,9 @@ export interface ProviderRuntime {
260
270
  configure(options: JsonObject, context?: ProviderContext): Promise<unknown>;
261
271
  getLaunchForm(request: ProviderLaunchFormRequest, context?: ProviderContext): Promise<ProviderLaunchForm>;
262
272
  invokeAction(context: ProviderActionContext): Promise<unknown>;
273
+ getEnhancementTarget?(request: ProviderInstanceRequest & {
274
+ tabId: number;
275
+ }, context?: ProviderContext): Promise<ProviderEnhancementTarget>;
263
276
  }
264
277
  export interface ProviderServerOptions {
265
278
  definition: BrowserProviderDefinition;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mearl/provider",
3
- "version": "3.0.0-beta.5",
3
+ "version": "3.0.0-beta.8",
4
4
  "description": "Unified contracts and runtime utilities for built-in and external Mearl browser providers",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -29,14 +29,14 @@
29
29
  ],
30
30
  "license": "Apache-2.0",
31
31
  "engines": {
32
- "node": ">=22.12.0"
32
+ "node": ">=20.18.0"
33
33
  },
34
34
  "publishConfig": {
35
35
  "registry": "https://registry.npmjs.org"
36
36
  },
37
37
  "dependencies": {
38
- "@mearl/daemon-core": "3.0.0-beta.5",
39
- "@mearl/browser-core": "3.0.0-beta.5"
38
+ "@mearl/daemon-core": "3.0.0-beta.8",
39
+ "@mearl/browser-core": "3.0.0-beta.8"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/node": "^24.9.1",
@@ -44,7 +44,7 @@
44
44
  },
45
45
  "repository": {
46
46
  "type": "git",
47
- "url": "git+https://github.com/F-loat/mearl.git",
47
+ "url": "git+https://github.com/mearlhq/mearl.git",
48
48
  "directory": "packages/provider"
49
49
  },
50
50
  "scripts": {