@mearl/client 2.20.6 → 2.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/README.md +153 -3
  2. package/dist/actionTimeouts.js +1 -1
  3. package/dist/artifacts.d.ts +1 -1
  4. package/dist/browserRegistry.d.ts +1 -1
  5. package/dist/check.d.ts +1 -1
  6. package/dist/cli.js +14 -8
  7. package/dist/commands.d.ts +23 -14
  8. package/dist/commands.js +24 -4
  9. package/dist/{generated-app-profile-definition.d.ts → generated/app-profile-definition.d.ts} +2 -2
  10. package/dist/{generated-app-profile-runtime-types.d.ts → generated/app-profile-runtime-types.d.ts} +2 -2
  11. package/dist/{generated-browser-action-protocol.d.ts → generated/browser-action-protocol.d.ts} +11 -1
  12. package/dist/{generated-browser-action-protocol.js → generated/browser-action-protocol.js} +3 -0
  13. package/dist/generated/plugin-diagnostics.d.ts +110 -0
  14. package/dist/generated/plugin-diagnostics.js +1 -0
  15. package/dist/generated/plugin-execution.d.ts +27 -0
  16. package/dist/generated/plugin-execution.js +1 -0
  17. package/dist/generated/plugin-models.d.ts +64 -0
  18. package/dist/generated/plugin-models.js +1 -0
  19. package/dist/generated/plugin-pages.d.ts +76 -0
  20. package/dist/generated/plugin-pages.js +59 -0
  21. package/dist/generated/plugin-recovery.d.ts +32 -0
  22. package/dist/generated/plugin-recovery.js +1 -0
  23. package/dist/generated/plugin-state.d.ts +50 -0
  24. package/dist/generated/plugin-state.js +1 -0
  25. package/dist/generated/plugin-tasks.d.ts +74 -0
  26. package/dist/generated/plugin-tasks.js +7 -0
  27. package/dist/generated/toolbox-icon.d.ts +10 -0
  28. package/dist/generated/toolbox-icon.js +37 -0
  29. package/dist/index.d.ts +15 -4
  30. package/dist/index.js +7 -1
  31. package/dist/pageEvalArguments.d.ts +1 -1
  32. package/dist/pageEvalArguments.js +1 -1
  33. package/dist/plugin-task-host.d.ts +1 -0
  34. package/dist/plugin-task-host.js +14 -0
  35. package/dist/plugin-worker.d.ts +1 -0
  36. package/dist/plugin-worker.js +17 -0
  37. package/dist/plugin.d.ts +1 -65
  38. package/dist/plugin.js +1 -391
  39. package/dist/pluginCliOutput.d.ts +2 -0
  40. package/dist/pluginCliOutput.js +7 -0
  41. package/dist/pluginModelSessionWorker.d.ts +23 -0
  42. package/dist/pluginModelSessionWorker.js +60 -0
  43. package/dist/plugins/clients/pluginHostConnection.d.ts +35 -0
  44. package/dist/plugins/clients/pluginHostConnection.js +256 -0
  45. package/dist/plugins/clients/pluginModels.d.ts +17 -0
  46. package/dist/plugins/clients/pluginModels.js +47 -0
  47. package/dist/plugins/clients/pluginPages.d.ts +21 -0
  48. package/dist/plugins/clients/pluginPages.js +46 -0
  49. package/dist/plugins/clients/pluginState.d.ts +26 -0
  50. package/dist/plugins/clients/pluginState.js +28 -0
  51. package/dist/plugins/clients/pluginTasks.d.ts +51 -0
  52. package/dist/plugins/clients/pluginTasks.js +181 -0
  53. package/dist/{officialPluginInstaller.js → plugins/officialPluginInstaller.js} +1 -3
  54. package/dist/{pluginPageBundle.js → plugins/pluginPageBundle.js} +74 -17
  55. package/dist/{pluginRegistry.d.ts → plugins/pluginRegistry.d.ts} +3 -4
  56. package/dist/{pluginRegistry.js → plugins/pluginRegistry.js} +16 -15
  57. package/dist/plugins/pluginTaskProtocol.d.ts +47 -0
  58. package/dist/plugins/pluginTaskProtocol.js +1 -0
  59. package/dist/plugins/pluginTaskWire.d.ts +4 -0
  60. package/dist/plugins/pluginTaskWire.js +42 -0
  61. package/dist/plugins/runtime/pluginCommandWorker.d.ts +2 -0
  62. package/dist/plugins/runtime/pluginCommandWorker.js +174 -0
  63. package/dist/plugins/runtime/pluginDiagnostics.d.ts +26 -0
  64. package/dist/plugins/runtime/pluginDiagnostics.js +309 -0
  65. package/dist/plugins/runtime/pluginExecution.d.ts +21 -0
  66. package/dist/plugins/runtime/pluginExecution.js +43 -0
  67. package/dist/plugins/runtime/pluginExecutor.d.ts +29 -0
  68. package/dist/plugins/runtime/pluginExecutor.js +223 -0
  69. package/dist/plugins/runtime/pluginExecutorProtocol.d.ts +55 -0
  70. package/dist/plugins/runtime/pluginExecutorProtocol.js +47 -0
  71. package/dist/plugins/runtime/pluginExecutorSupervisor.d.ts +4 -0
  72. package/dist/plugins/runtime/pluginExecutorSupervisor.js +63 -0
  73. package/dist/plugins/runtime/pluginHost.d.ts +20 -0
  74. package/dist/plugins/runtime/pluginHost.js +53 -0
  75. package/dist/plugins/runtime/pluginHostFiles.d.ts +1 -0
  76. package/dist/plugins/runtime/pluginHostFiles.js +21 -0
  77. package/dist/plugins/runtime/pluginHostPaths.d.ts +11 -0
  78. package/dist/plugins/runtime/pluginHostPaths.js +22 -0
  79. package/dist/plugins/runtime/pluginHostServer.d.ts +7 -0
  80. package/dist/plugins/runtime/pluginHostServer.js +252 -0
  81. package/dist/plugins/runtime/pluginModelCatalog.d.ts +14 -0
  82. package/dist/plugins/runtime/pluginModelCatalog.js +51 -0
  83. package/dist/plugins/runtime/pluginModelSessions.d.ts +16 -0
  84. package/dist/plugins/runtime/pluginModelSessions.js +234 -0
  85. package/dist/plugins/runtime/pluginPageManager.d.ts +57 -0
  86. package/dist/plugins/runtime/pluginPageManager.js +877 -0
  87. package/dist/plugins/runtime/pluginProcessTree.d.ts +2 -0
  88. package/dist/plugins/runtime/pluginProcessTree.js +31 -0
  89. package/dist/plugins/runtime/pluginRevision.d.ts +14 -0
  90. package/dist/plugins/runtime/pluginRevision.js +227 -0
  91. package/dist/plugins/runtime/pluginRuntimeCleanup.d.ts +2 -0
  92. package/dist/plugins/runtime/pluginRuntimeCleanup.js +55 -0
  93. package/dist/plugins/runtime/pluginStateManager.d.ts +24 -0
  94. package/dist/plugins/runtime/pluginStateManager.js +132 -0
  95. package/dist/plugins/runtime/pluginStateStore.d.ts +57 -0
  96. package/dist/plugins/runtime/pluginStateStore.js +396 -0
  97. package/dist/plugins/runtime/pluginTaskManager.d.ts +92 -0
  98. package/dist/plugins/runtime/pluginTaskManager.js +1080 -0
  99. package/dist/plugins/runtime/pluginTaskStore.d.ts +53 -0
  100. package/dist/plugins/runtime/pluginTaskStore.js +319 -0
  101. package/dist/plugins/runtime/pythonPlugin.d.ts +31 -0
  102. package/dist/{pythonPlugin.js → plugins/runtime/pythonPlugin.js} +236 -31
  103. package/dist/plugins/sdk/plugin.d.ts +118 -0
  104. package/dist/plugins/sdk/plugin.js +672 -0
  105. package/dist/{pluginAppProfiles.d.ts → plugins/sdk/pluginAppProfiles.d.ts} +1 -1
  106. package/dist/{pluginAppProfiles.js → plugins/sdk/pluginAppProfiles.js} +2 -2
  107. package/dist/plugins/sdk/pluginModelDefinition.d.ts +6 -0
  108. package/dist/plugins/sdk/pluginModelDefinition.js +75 -0
  109. package/dist/plugins/sdk/pluginPageDefinition.d.ts +3 -0
  110. package/dist/plugins/sdk/pluginPageDefinition.js +62 -0
  111. package/dist/plugins/sdk/pluginRecoveryDefinition.d.ts +3 -0
  112. package/dist/plugins/sdk/pluginRecoveryDefinition.js +56 -0
  113. package/dist/plugins/sdk/pluginStateDefinition.d.ts +4 -0
  114. package/dist/plugins/sdk/pluginStateDefinition.js +68 -0
  115. package/dist/runtimePaths.d.ts +4 -0
  116. package/dist/runtimePaths.js +8 -0
  117. package/dist/socket.d.ts +2 -2
  118. package/dist/socket.js +10 -6
  119. package/dist/unified.d.ts +3 -2
  120. package/dist/unified.js +83 -2
  121. package/package.json +7 -3
  122. package/dist/pythonPlugin.d.ts +0 -20
  123. /package/dist/{generated-app-profile-definition.js → generated/app-profile-definition.js} +0 -0
  124. /package/dist/{generated-app-profile-runtime-types.js → generated/app-profile-runtime-types.js} +0 -0
  125. /package/dist/{officialPluginCatalog.d.ts → plugins/officialPluginCatalog.d.ts} +0 -0
  126. /package/dist/{officialPluginCatalog.js → plugins/officialPluginCatalog.js} +0 -0
  127. /package/dist/{officialPluginInstaller.d.ts → plugins/officialPluginInstaller.d.ts} +0 -0
  128. /package/dist/{pluginPageBundle.d.ts → plugins/pluginPageBundle.d.ts} +0 -0
  129. /package/dist/{pluginSource.d.ts → plugins/pluginSource.d.ts} +0 -0
  130. /package/dist/{pluginSource.js → plugins/pluginSource.js} +0 -0
@@ -0,0 +1,47 @@
1
+ export const MAX_PLUGIN_RESULT_BYTES = 64 * 1024 * 1024;
2
+ export function serializePluginExecutionError(error) {
3
+ const value = error;
4
+ const metadata = Object.fromEntries(Object.entries(value && typeof value === 'object' ? value : {}).filter(([key, field]) => !['__proto__', 'prototype', 'constructor', 'message', 'stack', 'code', 'details'].includes(key) &&
5
+ (field === null || ['string', 'number', 'boolean'].includes(typeof field))));
6
+ const serialized = {
7
+ message: typeof value?.message === 'string' ? value.message : String(error),
8
+ ...(typeof value?.code === 'string' ? { code: value.code } : {}),
9
+ ...(value?.details && typeof value.details === 'object' && !Array.isArray(value.details)
10
+ ? { details: value.details }
11
+ : {}),
12
+ ...(Object.keys(metadata).length ? { metadata } : {}),
13
+ };
14
+ try {
15
+ if (Buffer.byteLength(JSON.stringify(serialized)) <= 64 * 1024)
16
+ return serialized;
17
+ }
18
+ catch {
19
+ /* Non-JSON diagnostic data cannot cross the executor boundary. */
20
+ }
21
+ return {
22
+ message: Buffer.from(serialized.message).subarray(0, 8192).toString('utf8'),
23
+ ...(serialized.code ? { code: serialized.code.slice(0, 256) } : {}),
24
+ details: { diagnosticTruncated: true },
25
+ metadata: Object.fromEntries(Object.entries(metadata)
26
+ .slice(0, 32)
27
+ .map(([key, field]) => [
28
+ key.slice(0, 128),
29
+ typeof field === 'string' ? field.slice(0, 512) : field,
30
+ ])),
31
+ };
32
+ }
33
+ export function pluginExecutionError(value) {
34
+ return Object.assign(new Error(value.message), {
35
+ ...Object.fromEntries(Object.entries(value.metadata ?? {}).filter(([key]) => ![
36
+ '__proto__',
37
+ 'prototype',
38
+ 'constructor',
39
+ 'message',
40
+ 'stack',
41
+ 'code',
42
+ 'details',
43
+ ].includes(key))),
44
+ ...(value.code ? { code: value.code } : {}),
45
+ ...(value.details ? { details: value.details } : {}),
46
+ });
47
+ }
@@ -0,0 +1,4 @@
1
+ import { type Serializable } from 'node:child_process';
2
+ import type { PluginWorkerEvent, PluginWorkerRequest } from './pluginExecutorProtocol.js';
3
+ /** Ownership monitoring runs outside the event loop that executes plugin code. */
4
+ export declare function supervisePluginExecutor<Request extends Serializable = PluginWorkerRequest, Event extends Serializable = PluginWorkerEvent>(workerUrl: string): void;
@@ -0,0 +1,63 @@
1
+ import { execFileSync, fork } from 'node:child_process';
2
+ import { fileURLToPath } from 'node:url';
3
+ /** Ownership monitoring runs outside the event loop that executes plugin code. */
4
+ export function supervisePluginExecutor(workerUrl) {
5
+ const child = fork(fileURLToPath(workerUrl), ['--command'], {
6
+ stdio: ['ignore', 'ignore', 'pipe', 'ipc'],
7
+ serialization: 'advanced',
8
+ execArgv: [],
9
+ });
10
+ let ending = false;
11
+ let forwarded = Promise.resolve();
12
+ child.stderr?.on('data', chunk => process.stderr.write(chunk));
13
+ const terminate = () => {
14
+ if (process.platform === 'win32') {
15
+ try {
16
+ execFileSync('taskkill', ['/PID', String(process.pid), '/T', '/F'], {
17
+ stdio: 'ignore',
18
+ windowsHide: true,
19
+ });
20
+ }
21
+ catch {
22
+ child.kill('SIGKILL');
23
+ }
24
+ }
25
+ else {
26
+ try {
27
+ process.kill(-process.pid, 'SIGKILL');
28
+ }
29
+ catch {
30
+ child.kill('SIGKILL');
31
+ }
32
+ }
33
+ process.exit(1);
34
+ };
35
+ process.on('disconnect', terminate);
36
+ process.on('message', (message) => {
37
+ if (!child.connected || ending)
38
+ return;
39
+ child.send(message, error => {
40
+ if (error && !ending)
41
+ terminate();
42
+ });
43
+ });
44
+ child.on('message', (message) => {
45
+ forwarded = forwarded.then(() => new Promise((resolve, reject) => {
46
+ if (!process.send || !process.connected) {
47
+ reject(new Error('Plugin owner disconnected'));
48
+ return;
49
+ }
50
+ process.send(message, error => (error ? reject(error) : resolve()));
51
+ }));
52
+ void forwarded.catch(terminate);
53
+ });
54
+ child.once('error', error => {
55
+ process.stderr.write(error.message);
56
+ terminate();
57
+ });
58
+ child.once('exit', code => {
59
+ ending = true;
60
+ child.stderr?.destroy();
61
+ void forwarded.then(() => process.exit(code ?? 1), terminate);
62
+ });
63
+ }
@@ -0,0 +1,20 @@
1
+ import { PluginTaskManager } from './pluginTaskManager.js';
2
+ import type { PluginTaskManagerOptions } from './pluginTaskManager.js';
3
+ import { PluginStateManager } from './pluginStateManager.js';
4
+ import { PluginPageManager } from './pluginPageManager.js';
5
+ import { PluginModelCatalog } from './pluginModelCatalog.js';
6
+ import { PluginModelSessions } from './pluginModelSessions.js';
7
+ import type { PluginDiagnosticOptions } from '../../generated/plugin-diagnostics.js';
8
+ import type { PluginTaskScope } from '../pluginTaskProtocol.js';
9
+ /** The host owns service composition; task execution receives explicit service dependencies. */
10
+ export declare class PluginHost {
11
+ readonly tasks: PluginTaskManager;
12
+ readonly state: PluginStateManager;
13
+ readonly pages: PluginPageManager;
14
+ readonly models: PluginModelCatalog;
15
+ readonly modelSessions: PluginModelSessions;
16
+ private closing?;
17
+ constructor(configuration?: PluginTaskManagerOptions);
18
+ diagnose(value: PluginDiagnosticOptions, scope: PluginTaskScope): import("../../generated/plugin-diagnostics.js").PluginDiagnosticReport;
19
+ close(): Promise<void>;
20
+ }
@@ -0,0 +1,53 @@
1
+ import { PluginTaskManager } from './pluginTaskManager.js';
2
+ import { PluginStateManager } from './pluginStateManager.js';
3
+ import { PluginPageManager } from './pluginPageManager.js';
4
+ import { PluginModelCatalog } from './pluginModelCatalog.js';
5
+ import { PluginModelSessions } from './pluginModelSessions.js';
6
+ import { dirname, join } from 'node:path';
7
+ import { buildDiagnosticReport, normalizeDiagnosticOptions } from './pluginDiagnostics.js';
8
+ /** The host owns service composition; task execution receives explicit service dependencies. */
9
+ export class PluginHost {
10
+ tasks;
11
+ state;
12
+ pages;
13
+ models = new PluginModelCatalog();
14
+ modelSessions;
15
+ closing;
16
+ constructor(configuration = {}) {
17
+ this.modelSessions = new PluginModelSessions(undefined, Math.max(2, configuration.modelConcurrency ?? 2), configuration.workerPath
18
+ ? join(dirname(configuration.workerPath), 'pluginModelSessionWorker.js')
19
+ : undefined);
20
+ this.tasks = new PluginTaskManager(configuration, {
21
+ state: (...args) => this.state.call(...args),
22
+ page: (...args) => this.pages.call(...args),
23
+ listModels: provider => this.models.list(provider),
24
+ model: (owner, ...args) => this.modelSessions.execute(owner, ...args),
25
+ releaseModels: owner => this.modelSessions.release(owner),
26
+ });
27
+ this.state = new PluginStateManager(this.tasks);
28
+ this.pages = new PluginPageManager(this.tasks, this.state, owner => this.modelSessions.release(owner));
29
+ this.state.resolveTarget = (...args) => this.pages.resolveStateTarget(...args);
30
+ }
31
+ diagnose(value, scope) {
32
+ const options = normalizeDiagnosticOptions(value);
33
+ if (scope.plugin && options.plugin && scope.plugin !== options.plugin)
34
+ throw Object.assign(new Error('Plugin task was not found'), {
35
+ code: 'PLUGIN_TASK_NOT_FOUND',
36
+ });
37
+ return buildDiagnosticReport(this.tasks.host, scope, options, this.tasks.store.diagnosticRecords(options.taskId), this.pages.diagnosticSnapshot());
38
+ }
39
+ close() {
40
+ this.closing ??= Promise.allSettled([
41
+ this.pages.close(),
42
+ this.tasks.close(),
43
+ this.state.close(),
44
+ this.models.close(),
45
+ this.modelSessions.close(),
46
+ ]).then(results => {
47
+ const failures = results.flatMap(result => result.status === 'rejected' ? [result.reason] : []);
48
+ if (failures.length)
49
+ throw new AggregateError(failures, 'Plugin host cleanup failed');
50
+ });
51
+ return this.closing;
52
+ }
53
+ }
@@ -0,0 +1 @@
1
+ export declare function writePluginHostFile(file: string, data: string | Buffer): void;
@@ -0,0 +1,21 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { closeSync, fsyncSync, mkdirSync, openSync, renameSync, rmSync, writeFileSync, } from 'node:fs';
3
+ import { dirname } from 'node:path';
4
+ export function writePluginHostFile(file, data) {
5
+ mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
6
+ const temporary = `${file}.${randomUUID()}.tmp`;
7
+ let descriptor;
8
+ try {
9
+ descriptor = openSync(temporary, 'wx', 0o600);
10
+ writeFileSync(descriptor, data);
11
+ fsyncSync(descriptor);
12
+ closeSync(descriptor);
13
+ descriptor = undefined;
14
+ renameSync(temporary, file);
15
+ }
16
+ finally {
17
+ if (descriptor !== undefined)
18
+ closeSync(descriptor);
19
+ rmSync(temporary, { force: true });
20
+ }
21
+ }
@@ -0,0 +1,11 @@
1
+ export declare function pluginHostPaths(): {
2
+ registry: string;
3
+ home: string;
4
+ lease: string;
5
+ log: string;
6
+ installations: string;
7
+ revisions: string;
8
+ tasks: string;
9
+ state: string;
10
+ socket: string;
11
+ };
@@ -0,0 +1,22 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { pluginDirectory } from '../pluginRegistry.js';
5
+ export function pluginHostPaths() {
6
+ const registry = pluginDirectory();
7
+ const key = createHash('sha256').update(registry).digest('hex').slice(0, 16);
8
+ const home = join(registry, '.host');
9
+ return {
10
+ registry,
11
+ home,
12
+ lease: join(home, 'service.json'),
13
+ log: join(home, 'service.log'),
14
+ installations: join(home, 'installations'),
15
+ revisions: join(home, 'revisions'),
16
+ tasks: join(home, 'tasks'),
17
+ state: join(home, 'state'),
18
+ socket: process.platform === 'win32'
19
+ ? `\\\\.\\pipe\\mearl-plugin-${key}`
20
+ : join(tmpdir(), `mp-${key}.sock`),
21
+ };
22
+ }
@@ -0,0 +1,7 @@
1
+ import { PluginHost } from './pluginHost.js';
2
+ import type { PluginTaskManagerOptions } from './pluginTaskManager.js';
3
+ export declare function startPluginHostServer(configuration?: PluginTaskManagerOptions): Promise<{
4
+ close: () => Promise<void>;
5
+ host: PluginHost;
6
+ socketPath: string;
7
+ }>;
@@ -0,0 +1,252 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { createServer } from 'node:net';
3
+ import { chmodSync, rmSync } from 'node:fs';
4
+ import { claimRecord, isProcessOwnerAlive, processIdentity, readRecord, removeInvalidRecord, removeRecordIfPid, writeRecordIfPid, } from '@mearl/daemon-core';
5
+ import { PLUGIN_TASK_PROTOCOL_VERSION } from '../../generated/plugin-tasks.js';
6
+ import { pluginHostPaths } from './pluginHostPaths.js';
7
+ import { PluginHost } from './pluginHost.js';
8
+ import { readPluginTaskMessages, writePluginTaskMessage } from '../pluginTaskWire.js';
9
+ import { pluginExecutionError, serializePluginExecutionError } from './pluginExecutorProtocol.js';
10
+ export async function startPluginHostServer(configuration = {}) {
11
+ const paths = pluginHostPaths();
12
+ const existing = readRecord(paths.lease);
13
+ if (existing && !isProcessOwnerAlive(existing))
14
+ removeRecordIfPid(paths.lease, existing.pid);
15
+ else if (!existing)
16
+ removeInvalidRecord(paths.lease);
17
+ const owner = {
18
+ pid: process.pid,
19
+ protocolVersion: PLUGIN_TASK_PROTOCOL_VERSION,
20
+ processIdentity: processIdentity(process.pid),
21
+ };
22
+ if (!claimRecord(paths.lease, owner))
23
+ throw Object.assign(new Error('Plugin execution service already has an owner'), {
24
+ code: 'PLUGIN_TASK_HOST_OWNED',
25
+ });
26
+ let host;
27
+ try {
28
+ host = new PluginHost(configuration);
29
+ }
30
+ catch (error) {
31
+ removeRecordIfPid(paths.lease, process.pid);
32
+ throw error;
33
+ }
34
+ const { tasks: manager, pages, state, models } = host;
35
+ if (!writeRecordIfPid(paths.lease, process.pid, {
36
+ ...owner,
37
+ ...manager.host,
38
+ socketPath: paths.socket,
39
+ startedAt: new Date().toISOString(),
40
+ }))
41
+ throw new Error('Plugin host lease ownership was lost');
42
+ const sessions = new Set();
43
+ let closing;
44
+ const server = createServer(socket => {
45
+ sessions.add(socket);
46
+ const disconnected = new AbortController();
47
+ let scope;
48
+ let sequence = 0;
49
+ let requests = 0;
50
+ const pending = new Map();
51
+ const bridge = {
52
+ id: randomUUID(),
53
+ get scope() {
54
+ if (!scope)
55
+ throw new Error('Plugin task connection is not initialized');
56
+ return scope;
57
+ },
58
+ invoke(action, payload, options) {
59
+ if (socket.destroyed)
60
+ return Promise.reject(Object.assign(new Error('The browser invocation bridge is unavailable'), {
61
+ code: 'PLUGIN_TASK_BRIDGE_UNAVAILABLE',
62
+ }));
63
+ const id = ++sequence;
64
+ return new Promise((resolve, reject) => {
65
+ const timer = setTimeout(() => {
66
+ pending.delete(id);
67
+ reject(Object.assign(new Error('The browser invocation bridge timed out; external operation status is unknown'), { code: 'PLUGIN_TASK_BRIDGE_UNAVAILABLE', details: { action } }));
68
+ }, Math.min(Math.max(options.timeoutSec ?? 180, 1), 3600) * 1000);
69
+ pending.set(id, { resolve, reject, timer });
70
+ void writePluginTaskMessage(socket, {
71
+ type: 'invoke',
72
+ id,
73
+ action,
74
+ payload,
75
+ options,
76
+ }).catch(error => {
77
+ pending.delete(id);
78
+ clearTimeout(timer);
79
+ reject(error);
80
+ });
81
+ });
82
+ },
83
+ };
84
+ const handle = async (method, input) => {
85
+ if (method === 'hello') {
86
+ if (scope || input.protocolVersion !== PLUGIN_TASK_PROTOCOL_VERSION)
87
+ throw new Error('Plugin task protocol version mismatch');
88
+ if (!['public', 'toolbox', 'runtime'].includes(String(input.access)) ||
89
+ !['service', 'caller'].includes(String(input.bridge)) ||
90
+ !['cli', 'sdk', 'mcp', 'toolbox', 'runtime'].includes(String(input.source)) ||
91
+ (input.plugin !== undefined &&
92
+ (typeof input.plugin !== 'string' || !/^[a-z][a-z0-9-]*$/.test(input.plugin))) ||
93
+ (input.scopeId !== undefined &&
94
+ (typeof input.scopeId !== 'string' || input.scopeId.length > 256)) ||
95
+ (input.access !== 'public' && (!input.plugin || input.bridge !== 'caller')))
96
+ throw new Error('Invalid plugin task caller scope');
97
+ scope = {
98
+ access: input.access,
99
+ bridge: input.bridge,
100
+ source: input.source,
101
+ ...(input.plugin ? { plugin: String(input.plugin) } : {}),
102
+ ...(input.scopeId ? { scopeId: String(input.scopeId) } : {}),
103
+ };
104
+ return {
105
+ protocolVersion: PLUGIN_TASK_PROTOCOL_VERSION,
106
+ host: manager.host,
107
+ pid: process.pid,
108
+ };
109
+ }
110
+ if (!scope)
111
+ throw new Error('Plugin task connection requires a handshake');
112
+ if (method === 'status')
113
+ return {
114
+ protocolVersion: PLUGIN_TASK_PROTOCOL_VERSION,
115
+ host: manager.host,
116
+ pid: process.pid,
117
+ };
118
+ if (method === 'diagnose')
119
+ return host.diagnose(input, scope);
120
+ if (method === 'state.call')
121
+ return state.call(String(input.plugin), input.input, bridge, input.options);
122
+ if (method === 'model.list')
123
+ return models.list(input.provider);
124
+ if (method === 'model.capabilities')
125
+ return (await import('../sdk/pluginModelDefinition.js')).pluginModelCapabilities();
126
+ if (method === 'page.call') {
127
+ const execution = input.execution;
128
+ return pages.call(String(input.plugin), input.input, bridge, input.options, execution
129
+ ? {
130
+ cwd: execution.cwd,
131
+ environment: execution.environment,
132
+ signal: disconnected.signal,
133
+ }
134
+ : undefined);
135
+ }
136
+ if (method === 'page.event') {
137
+ if (scope.access !== 'runtime')
138
+ throw new Error('Page events require the browser runtime bridge');
139
+ return pages.receive(String(input.plugin), input.input, bridge, input.options);
140
+ }
141
+ if (method === 'start')
142
+ return manager.start(input, bridge);
143
+ if (method === 'resume')
144
+ return manager.resume(input, bridge);
145
+ if (method === 'list')
146
+ return manager.list(scope, input.activeOnly === true, input.cursor, input.limit);
147
+ if (method === 'shutdown') {
148
+ if (scope.access !== 'public')
149
+ throw new Error('Tool pages cannot stop the plugin host');
150
+ return { stopped: true };
151
+ }
152
+ if (typeof input.taskId !== 'string')
153
+ throw new Error('Plugin task ID is required');
154
+ switch (method) {
155
+ case 'get':
156
+ return manager.get(input.taskId, scope);
157
+ case 'checkpoint':
158
+ return manager.checkpoint(input.taskId, scope);
159
+ case 'cancel':
160
+ return manager.cancel(input.taskId, scope);
161
+ case 'events':
162
+ return manager.events(input.taskId, scope, input.cursor, input.waitMs, disconnected.signal);
163
+ case 'result':
164
+ return manager.resultChunk(input.taskId, scope, input.index);
165
+ default:
166
+ throw new Error(`Unknown plugin task method: ${method}`);
167
+ }
168
+ };
169
+ readPluginTaskMessages(socket, message => {
170
+ if (message.type === 'invoke-result') {
171
+ const request = pending.get(message.id);
172
+ if (!request)
173
+ return;
174
+ clearTimeout(request.timer);
175
+ pending.delete(message.id);
176
+ if (message.error)
177
+ request.reject(pluginExecutionError(message.error));
178
+ else
179
+ request.resolve(message.result);
180
+ }
181
+ else if (message.type === 'request') {
182
+ if (++requests > 64 ||
183
+ !message.input ||
184
+ typeof message.input !== 'object' ||
185
+ Array.isArray(message.input)) {
186
+ socket.destroy(new Error('Invalid or excessive plugin task requests'));
187
+ return;
188
+ }
189
+ void Promise.resolve()
190
+ .then(() => handle(message.method, message.input))
191
+ .then(async (result) => {
192
+ await writePluginTaskMessage(socket, { type: 'response', id: message.id, result });
193
+ if (message.method === 'shutdown')
194
+ setImmediate(() => {
195
+ void close().catch(() => { });
196
+ });
197
+ }, error => writePluginTaskMessage(socket, {
198
+ type: 'response',
199
+ id: message.id,
200
+ error: serializePluginExecutionError(error),
201
+ }))
202
+ .catch(() => socket.destroy())
203
+ .finally(() => {
204
+ requests--;
205
+ });
206
+ }
207
+ else
208
+ socket.destroy(new Error('Invalid plugin task message direction'));
209
+ });
210
+ socket.on('error', () => { });
211
+ socket.once('close', () => {
212
+ disconnected.abort();
213
+ sessions.delete(socket);
214
+ for (const request of pending.values()) {
215
+ clearTimeout(request.timer);
216
+ request.reject(Object.assign(new Error('The invocation bridge disconnected; external operation status may need reconciliation'), { code: 'PLUGIN_TASK_BRIDGE_UNAVAILABLE' }));
217
+ }
218
+ pending.clear();
219
+ });
220
+ });
221
+ async function close() {
222
+ closing ??= (async () => {
223
+ const servicesClosed = host.close();
224
+ for (const socket of sessions)
225
+ socket.destroy();
226
+ await servicesClosed;
227
+ await new Promise(resolve => server.close(() => resolve()));
228
+ if (removeRecordIfPid(paths.lease, process.pid) && process.platform !== 'win32')
229
+ rmSync(paths.socket, { force: true });
230
+ })();
231
+ return closing;
232
+ }
233
+ try {
234
+ if (process.platform !== 'win32')
235
+ rmSync(paths.socket, { force: true });
236
+ await new Promise((resolve, reject) => {
237
+ server.once('error', reject);
238
+ server.listen(paths.socket, () => {
239
+ server.off('error', reject);
240
+ resolve();
241
+ });
242
+ });
243
+ if (process.platform !== 'win32')
244
+ chmodSync(paths.socket, 0o600);
245
+ }
246
+ catch (error) {
247
+ removeRecordIfPid(paths.lease, process.pid);
248
+ await host.close();
249
+ throw error;
250
+ }
251
+ return { close, host, socketPath: paths.socket };
252
+ }
@@ -0,0 +1,14 @@
1
+ import type { PluginModelList, PluginModelOption } from '../../generated/plugin-models.js';
2
+ /** A bounded shared lookup avoids starting a discovery process for each open tool. */
3
+ export declare class PluginModelCatalog {
4
+ private readonly discover;
5
+ private readonly now;
6
+ private readonly controller;
7
+ private cached?;
8
+ private pending?;
9
+ constructor(discover?: (signal: AbortSignal) => Promise<PluginModelOption[]>, now?: () => number);
10
+ list(provider?: unknown): Promise<PluginModelList>;
11
+ private refresh;
12
+ private load;
13
+ close(): Promise<void>;
14
+ }
@@ -0,0 +1,51 @@
1
+ /** A bounded shared lookup avoids starting a discovery process for each open tool. */
2
+ export class PluginModelCatalog {
3
+ discover;
4
+ now;
5
+ controller = new AbortController();
6
+ cached;
7
+ pending;
8
+ constructor(discover = async (signal) => {
9
+ const { listCodexModels } = await import('@mearl/agent-runtime/codex');
10
+ return listCodexModels(signal);
11
+ }, now = Date.now) {
12
+ this.discover = discover;
13
+ this.now = now;
14
+ }
15
+ list(provider = 'codex') {
16
+ this.controller.signal.throwIfAborted();
17
+ if (provider !== 'codex')
18
+ throw new Error('Unsupported model provider');
19
+ if (this.cached) {
20
+ if (this.cached.expires <= this.now())
21
+ void this.refresh().catch(() => { });
22
+ return Promise.resolve(structuredClone(this.cached.value));
23
+ }
24
+ return this.refresh().then(value => structuredClone(value));
25
+ }
26
+ refresh() {
27
+ this.pending ??= this.load()
28
+ .catch(error => {
29
+ // A failed refresh keeps the last usable catalog and bounds repeat discovery.
30
+ if (this.cached)
31
+ this.cached.expires = this.now() + 30000;
32
+ throw error;
33
+ })
34
+ .finally(() => {
35
+ this.pending = undefined;
36
+ });
37
+ return this.pending;
38
+ }
39
+ async load() {
40
+ const signal = AbortSignal.any([this.controller.signal, AbortSignal.timeout(15000)]);
41
+ const value = { provider: 'codex', models: await this.discover(signal) };
42
+ signal.throwIfAborted();
43
+ this.cached = { value, expires: this.now() + 120000 };
44
+ return value;
45
+ }
46
+ async close() {
47
+ this.controller.abort();
48
+ await this.pending?.catch(() => { });
49
+ this.cached = undefined;
50
+ }
51
+ }
@@ -0,0 +1,16 @@
1
+ import type { executeRegisteredPlugin } from './pluginExecutor.js';
2
+ /** Opt-in conversations retain a supervised process and thread within one execution owner. */
3
+ export declare class PluginModelSessions {
4
+ private readonly idleMs;
5
+ private readonly limit;
6
+ private readonly workerPath;
7
+ private readonly sessions;
8
+ private readonly closing;
9
+ private closed;
10
+ constructor(idleMs?: number, limit?: number, workerPath?: string);
11
+ execute(owner: string, ...args: Parameters<typeof executeRegisteredPlugin>): Promise<unknown>;
12
+ private run;
13
+ private remove;
14
+ release(owner: string): Promise<void>;
15
+ close(): Promise<void>;
16
+ }