@mearl/client 2.20.6 → 2.21.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 (105) hide show
  1. package/README.md +122 -2
  2. package/dist/cli.js +10 -5
  3. package/dist/commands.d.ts +14 -5
  4. package/dist/commands.js +21 -2
  5. package/dist/generated-browser-action-protocol.d.ts +11 -1
  6. package/dist/generated-browser-action-protocol.js +3 -0
  7. package/dist/generated-plugin-diagnostics.d.ts +110 -0
  8. package/dist/generated-plugin-diagnostics.js +1 -0
  9. package/dist/generated-plugin-execution.d.ts +27 -0
  10. package/dist/generated-plugin-execution.js +1 -0
  11. package/dist/generated-plugin-models.d.ts +64 -0
  12. package/dist/generated-plugin-models.js +1 -0
  13. package/dist/generated-plugin-pages.d.ts +76 -0
  14. package/dist/generated-plugin-pages.js +59 -0
  15. package/dist/generated-plugin-recovery.d.ts +32 -0
  16. package/dist/generated-plugin-recovery.js +1 -0
  17. package/dist/generated-plugin-state.d.ts +50 -0
  18. package/dist/generated-plugin-state.js +1 -0
  19. package/dist/generated-plugin-tasks.d.ts +74 -0
  20. package/dist/generated-plugin-tasks.js +7 -0
  21. package/dist/generated-toolbox-icon.d.ts +10 -0
  22. package/dist/generated-toolbox-icon.js +37 -0
  23. package/dist/index.d.ts +11 -0
  24. package/dist/index.js +6 -0
  25. package/dist/plugin-task-host.d.ts +1 -0
  26. package/dist/plugin-task-host.js +14 -0
  27. package/dist/plugin-worker.d.ts +1 -0
  28. package/dist/plugin-worker.js +17 -0
  29. package/dist/plugin.d.ts +57 -4
  30. package/dist/plugin.js +296 -15
  31. package/dist/pluginCliOutput.d.ts +2 -0
  32. package/dist/pluginCliOutput.js +7 -0
  33. package/dist/pluginCommandWorker.d.ts +2 -0
  34. package/dist/pluginCommandWorker.js +174 -0
  35. package/dist/pluginDiagnostics.d.ts +26 -0
  36. package/dist/pluginDiagnostics.js +309 -0
  37. package/dist/pluginExecution.d.ts +21 -0
  38. package/dist/pluginExecution.js +43 -0
  39. package/dist/pluginExecutor.d.ts +29 -0
  40. package/dist/pluginExecutor.js +223 -0
  41. package/dist/pluginExecutorProtocol.d.ts +55 -0
  42. package/dist/pluginExecutorProtocol.js +47 -0
  43. package/dist/pluginExecutorSupervisor.d.ts +4 -0
  44. package/dist/pluginExecutorSupervisor.js +63 -0
  45. package/dist/pluginHost.d.ts +20 -0
  46. package/dist/pluginHost.js +53 -0
  47. package/dist/pluginHostConnection.d.ts +35 -0
  48. package/dist/pluginHostConnection.js +257 -0
  49. package/dist/pluginHostFiles.d.ts +1 -0
  50. package/dist/pluginHostFiles.js +21 -0
  51. package/dist/pluginHostPaths.d.ts +11 -0
  52. package/dist/pluginHostPaths.js +22 -0
  53. package/dist/pluginHostServer.d.ts +7 -0
  54. package/dist/pluginHostServer.js +252 -0
  55. package/dist/pluginModelCatalog.d.ts +14 -0
  56. package/dist/pluginModelCatalog.js +51 -0
  57. package/dist/pluginModelDefinition.d.ts +6 -0
  58. package/dist/pluginModelDefinition.js +75 -0
  59. package/dist/pluginModelSessionWorker.d.ts +23 -0
  60. package/dist/pluginModelSessionWorker.js +60 -0
  61. package/dist/pluginModelSessions.d.ts +16 -0
  62. package/dist/pluginModelSessions.js +234 -0
  63. package/dist/pluginModels.d.ts +17 -0
  64. package/dist/pluginModels.js +47 -0
  65. package/dist/pluginPageBundle.js +74 -17
  66. package/dist/pluginPageDefinition.d.ts +3 -0
  67. package/dist/pluginPageDefinition.js +62 -0
  68. package/dist/pluginPageManager.d.ts +57 -0
  69. package/dist/pluginPageManager.js +877 -0
  70. package/dist/pluginPages.d.ts +21 -0
  71. package/dist/pluginPages.js +46 -0
  72. package/dist/pluginProcessTree.d.ts +2 -0
  73. package/dist/pluginProcessTree.js +31 -0
  74. package/dist/pluginRecoveryDefinition.d.ts +3 -0
  75. package/dist/pluginRecoveryDefinition.js +56 -0
  76. package/dist/pluginRegistry.d.ts +2 -3
  77. package/dist/pluginRegistry.js +14 -13
  78. package/dist/pluginRevision.d.ts +14 -0
  79. package/dist/pluginRevision.js +227 -0
  80. package/dist/pluginRuntimeCleanup.d.ts +2 -0
  81. package/dist/pluginRuntimeCleanup.js +55 -0
  82. package/dist/pluginState.d.ts +26 -0
  83. package/dist/pluginState.js +28 -0
  84. package/dist/pluginStateDefinition.d.ts +4 -0
  85. package/dist/pluginStateDefinition.js +68 -0
  86. package/dist/pluginStateManager.d.ts +24 -0
  87. package/dist/pluginStateManager.js +132 -0
  88. package/dist/pluginStateStore.d.ts +57 -0
  89. package/dist/pluginStateStore.js +396 -0
  90. package/dist/pluginTaskManager.d.ts +92 -0
  91. package/dist/pluginTaskManager.js +1080 -0
  92. package/dist/pluginTaskProtocol.d.ts +47 -0
  93. package/dist/pluginTaskProtocol.js +1 -0
  94. package/dist/pluginTaskStore.d.ts +53 -0
  95. package/dist/pluginTaskStore.js +319 -0
  96. package/dist/pluginTaskWire.d.ts +4 -0
  97. package/dist/pluginTaskWire.js +42 -0
  98. package/dist/pluginTasks.d.ts +51 -0
  99. package/dist/pluginTasks.js +181 -0
  100. package/dist/pythonPlugin.d.ts +13 -2
  101. package/dist/pythonPlugin.js +235 -30
  102. package/dist/socket.js +5 -2
  103. package/dist/unified.d.ts +1 -0
  104. package/dist/unified.js +81 -0
  105. package/package.json +7 -3
@@ -0,0 +1,55 @@
1
+ import type { PluginModelDiagnostic, PluginModelRequest } from './generated-plugin-models.js';
2
+ import type { PluginInvocationIdentity, PluginExecutionError } from './generated-plugin-execution.js';
3
+ import type { PluginInvocation, PluginOptions, PluginProgress } from './plugin.js';
4
+ import type { PluginRegistration } from './pluginRegistry.js';
5
+ export type PluginWorkerRequest = {
6
+ type: 'start';
7
+ plugin: string;
8
+ invocation: PluginInvocation;
9
+ options: PluginOptions;
10
+ identity: PluginInvocationIdentity;
11
+ entry: string;
12
+ entryVersion: string;
13
+ access: 'public' | 'toolbox' | 'runtime';
14
+ registration?: PluginRegistration;
15
+ describe?: boolean;
16
+ model?: PluginModelRequest;
17
+ recovery?: boolean;
18
+ migration?: boolean;
19
+ expectedRecovery?: string;
20
+ } | {
21
+ type: 'cancel';
22
+ error: PluginExecutionError;
23
+ } | {
24
+ type: 'invoke-progress';
25
+ id: number;
26
+ progress: PluginProgress;
27
+ } | {
28
+ type: 'response';
29
+ id: number;
30
+ result?: unknown;
31
+ error?: PluginExecutionError;
32
+ };
33
+ export type PluginWorkerEvent = {
34
+ type: 'invoke';
35
+ id: number;
36
+ action: string;
37
+ payload: Record<string, unknown>;
38
+ options: PluginOptions;
39
+ stream?: boolean;
40
+ } | {
41
+ type: 'diagnostic';
42
+ metadata: PluginModelDiagnostic;
43
+ } | {
44
+ type: 'progress';
45
+ progress: PluginProgress;
46
+ } | {
47
+ type: 'result';
48
+ json: string;
49
+ } | {
50
+ type: 'error';
51
+ error: PluginExecutionError;
52
+ };
53
+ export declare const MAX_PLUGIN_RESULT_BYTES: number;
54
+ export declare function serializePluginExecutionError(error: unknown): PluginExecutionError;
55
+ export declare function pluginExecutionError(value: PluginExecutionError): Error;
@@ -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,35 @@
1
+ import type { PluginInvocationSource, PluginExecutionHost } from './generated-plugin-execution.js';
2
+ import type { PluginExecutionContext } from './plugin.js';
3
+ export interface PluginHostConnectionOptions {
4
+ source?: PluginInvocationSource;
5
+ plugin?: string;
6
+ access?: 'public' | 'toolbox' | 'runtime';
7
+ invoke?: PluginExecutionContext['invoke'];
8
+ runtimePath?: string;
9
+ socketPath?: string;
10
+ autoStart?: boolean;
11
+ scopeId?: string;
12
+ }
13
+ export interface PluginHostClientOptions extends PluginHostConnectionOptions {
14
+ /** The creator owns an injected connection; disconnecting a service client leaves it open. */
15
+ connection?: PluginHostConnection;
16
+ }
17
+ export interface HostStatus {
18
+ protocolVersion: number;
19
+ host: PluginExecutionHost;
20
+ pid: number;
21
+ }
22
+ /** One connection owns caller scope, discovery and bridge lifetime. */
23
+ export declare class PluginHostConnection {
24
+ private readonly configuration;
25
+ get hasInvocationBridge(): boolean;
26
+ private connection?;
27
+ private socket?;
28
+ private readonly opening;
29
+ private generation;
30
+ constructor(configuration?: PluginHostConnectionOptions);
31
+ private connect;
32
+ private open;
33
+ request(method: string, input?: Record<string, unknown>, timeoutMs?: number): Promise<any>;
34
+ disconnect(): void;
35
+ }
@@ -0,0 +1,257 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { closeSync, mkdirSync, openSync } from 'node:fs';
3
+ import { createConnection } from 'node:net';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { claimRecord, isProcessOwnerAlive, processIdentity, readRecord, removeInvalidRecord, removeRecordIfPid, rotateLogIfNeeded, } from '@mearl/daemon-core';
6
+ import { pluginHostPaths } from './pluginHostPaths.js';
7
+ import { PLUGIN_TASK_PROTOCOL_VERSION } from './generated-plugin-tasks.js';
8
+ import { readPluginTaskMessages, writePluginTaskMessage } from './pluginTaskWire.js';
9
+ import { pluginExecutionError, serializePluginExecutionError } from './pluginExecutorProtocol.js';
10
+ const starting = new Map();
11
+ async function ensureHost(runtimePath) {
12
+ const paths = pluginHostPaths();
13
+ let work = starting.get(paths.registry);
14
+ if (!work) {
15
+ work = (async () => {
16
+ const lease = readRecord(paths.lease);
17
+ if (lease && isProcessOwnerAlive(lease)) {
18
+ if (lease.protocolVersion !== PLUGIN_TASK_PROTOCOL_VERSION)
19
+ throw new Error('Plugin execution service protocol differs; stop the service before updating');
20
+ return;
21
+ }
22
+ const lock = `${paths.lease}.start`;
23
+ const existing = readRecord(lock);
24
+ if (existing && isProcessOwnerAlive(existing))
25
+ return;
26
+ if (existing)
27
+ removeRecordIfPid(lock, existing.pid);
28
+ else
29
+ removeInvalidRecord(lock);
30
+ if (!claimRecord(lock, { pid: process.pid, processIdentity: processIdentity(process.pid) }))
31
+ return;
32
+ try {
33
+ const owner = readRecord(paths.lease);
34
+ if (owner && isProcessOwnerAlive(owner))
35
+ return;
36
+ mkdirSync(paths.home, { recursive: true, mode: 0o700 });
37
+ rotateLogIfNeeded(paths.log);
38
+ const log = openSync(paths.log, 'a', 0o600);
39
+ try {
40
+ const child = spawn(process.execPath, [runtimePath], {
41
+ detached: true,
42
+ stdio: ['ignore', log, log],
43
+ windowsHide: true,
44
+ });
45
+ await new Promise((resolve, reject) => {
46
+ child.once('spawn', resolve);
47
+ child.once('error', reject);
48
+ });
49
+ child.unref();
50
+ }
51
+ finally {
52
+ closeSync(log);
53
+ }
54
+ const deadline = Date.now() + 10000;
55
+ while (Date.now() < deadline) {
56
+ const ready = await new Promise(resolve => {
57
+ const probe = createConnection(paths.socket);
58
+ const finish = (value) => {
59
+ probe.destroy();
60
+ resolve(value);
61
+ };
62
+ probe.once('connect', () => finish(true));
63
+ probe.once('error', () => finish(false));
64
+ probe.setTimeout(200, () => finish(false));
65
+ });
66
+ if (ready)
67
+ return;
68
+ await new Promise(resolve => setTimeout(resolve, 50));
69
+ }
70
+ throw new Error('Plugin execution service startup timed out');
71
+ }
72
+ finally {
73
+ removeRecordIfPid(lock, process.pid);
74
+ }
75
+ })().finally(() => starting.delete(paths.registry));
76
+ starting.set(paths.registry, work);
77
+ }
78
+ await work;
79
+ }
80
+ /** One connection owns caller scope, discovery and bridge lifetime. */
81
+ export class PluginHostConnection {
82
+ configuration;
83
+ get hasInvocationBridge() {
84
+ return Boolean(this.configuration.invoke);
85
+ }
86
+ connection;
87
+ socket;
88
+ opening = new Set();
89
+ generation = 0;
90
+ constructor(configuration = {}) {
91
+ this.configuration = configuration;
92
+ }
93
+ async connect(generation) {
94
+ const check = () => {
95
+ if (generation !== this.generation)
96
+ throw Object.assign(new Error('Plugin task client was disconnected'), {
97
+ code: 'PLUGIN_TASK_CLIENT_DISCONNECTED',
98
+ });
99
+ };
100
+ const connectOnce = () => {
101
+ check();
102
+ return this.open(this.configuration.socketPath ?? pluginHostPaths().socket, generation);
103
+ };
104
+ try {
105
+ return await connectOnce();
106
+ }
107
+ catch (error) {
108
+ check();
109
+ if (this.configuration.autoStart === false || this.configuration.socketPath)
110
+ throw error;
111
+ const code = error.code;
112
+ if (!['ENOENT', 'ECONNREFUSED', 'PLUGIN_TASK_CONNECTION_LOST'].includes(code ?? ''))
113
+ throw error;
114
+ }
115
+ await ensureHost(this.configuration.runtimePath ??
116
+ fileURLToPath(new URL('./plugin-task-host.js', import.meta.url)));
117
+ const deadline = Date.now() + 10000;
118
+ let failure;
119
+ do {
120
+ check();
121
+ try {
122
+ return await connectOnce();
123
+ }
124
+ catch (error) {
125
+ failure = error;
126
+ await new Promise(resolve => setTimeout(resolve, 100));
127
+ }
128
+ } while (Date.now() < deadline);
129
+ throw new Error('Plugin execution service did not become ready; inspect its service.log', {
130
+ cause: failure,
131
+ });
132
+ }
133
+ async open(socketPath, generation) {
134
+ const socket = createConnection(socketPath);
135
+ this.opening.add(socket);
136
+ const pending = new Map();
137
+ let sequence = 0;
138
+ const fail = (error) => {
139
+ for (const request of pending.values()) {
140
+ clearTimeout(request.timer);
141
+ request.reject(error);
142
+ }
143
+ pending.clear();
144
+ };
145
+ socket.on('error', fail);
146
+ socket.once('close', () => {
147
+ fail(Object.assign(new Error('Plugin execution service connection was lost'), {
148
+ code: 'PLUGIN_TASK_CONNECTION_LOST',
149
+ }));
150
+ if (this.socket === socket) {
151
+ this.connection = undefined;
152
+ this.socket = undefined;
153
+ }
154
+ });
155
+ const request = (method, input = {}, timeoutMs = 15000) => new Promise((resolve, reject) => {
156
+ const id = ++sequence;
157
+ const timer = setTimeout(() => {
158
+ pending.delete(id);
159
+ reject(Object.assign(new Error('Plugin task request timed out; the task may still be running'), { code: 'PLUGIN_TASK_REQUEST_TIMEOUT' }));
160
+ }, timeoutMs);
161
+ pending.set(id, { resolve, reject, timer });
162
+ void writePluginTaskMessage(socket, { type: 'request', id, method, input }).catch(error => {
163
+ clearTimeout(timer);
164
+ pending.delete(id);
165
+ reject(error);
166
+ });
167
+ });
168
+ readPluginTaskMessages(socket, message => {
169
+ if (message.type === 'response') {
170
+ const call = pending.get(message.id);
171
+ if (!call)
172
+ return;
173
+ clearTimeout(call.timer);
174
+ pending.delete(message.id);
175
+ if (message.error)
176
+ call.reject(pluginExecutionError(message.error));
177
+ else
178
+ call.resolve(message.result);
179
+ }
180
+ else if (message.type === 'invoke') {
181
+ void Promise.resolve()
182
+ .then(() => {
183
+ if (!this.configuration.invoke)
184
+ throw new Error('Plugin task client has no invocation bridge');
185
+ return this.configuration.invoke(message.action, message.payload, message.options);
186
+ })
187
+ .then(result => writePluginTaskMessage(socket, { type: 'invoke-result', id: message.id, result }), error => writePluginTaskMessage(socket, {
188
+ type: 'invoke-result',
189
+ id: message.id,
190
+ error: serializePluginExecutionError(error),
191
+ }))
192
+ .catch(() => { });
193
+ }
194
+ else
195
+ socket.destroy(new Error('Invalid plugin task response direction'));
196
+ });
197
+ try {
198
+ await new Promise((resolve, reject) => {
199
+ const timer = setTimeout(() => {
200
+ socket.destroy();
201
+ reject(new Error('Plugin task connection timed out'));
202
+ }, 2000);
203
+ socket.once('connect', () => {
204
+ clearTimeout(timer);
205
+ resolve();
206
+ });
207
+ socket.once('error', error => {
208
+ clearTimeout(timer);
209
+ reject(error);
210
+ });
211
+ });
212
+ const scope = {
213
+ access: this.configuration.access ?? 'public',
214
+ source: this.configuration.source ?? 'sdk',
215
+ bridge: this.configuration.invoke ? 'caller' : 'service',
216
+ ...(this.configuration.plugin ? { plugin: this.configuration.plugin } : {}),
217
+ ...(this.configuration.scopeId ? { scopeId: this.configuration.scopeId } : {}),
218
+ };
219
+ const status = await request('hello', {
220
+ ...scope,
221
+ protocolVersion: PLUGIN_TASK_PROTOCOL_VERSION,
222
+ });
223
+ if (status.protocolVersion !== PLUGIN_TASK_PROTOCOL_VERSION)
224
+ throw new Error('Plugin task protocol version mismatch');
225
+ if (generation !== this.generation || socket.destroyed)
226
+ throw new Error('Plugin task client was disconnected');
227
+ this.socket = socket;
228
+ return { socket, request, status };
229
+ }
230
+ catch (error) {
231
+ socket.destroy();
232
+ throw error;
233
+ }
234
+ finally {
235
+ this.opening.delete(socket);
236
+ }
237
+ }
238
+ async request(method, input = {}, timeoutMs) {
239
+ if (!this.connection) {
240
+ const attempt = this.connect(this.generation);
241
+ this.connection = attempt;
242
+ void attempt.catch(() => {
243
+ if (this.connection === attempt)
244
+ this.connection = undefined;
245
+ });
246
+ }
247
+ return (await this.connection).request(method, input, timeoutMs);
248
+ }
249
+ disconnect() {
250
+ this.generation++;
251
+ this.socket?.destroy();
252
+ for (const socket of this.opening)
253
+ socket.destroy();
254
+ this.socket = undefined;
255
+ this.connection = undefined;
256
+ }
257
+ }
@@ -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
+ }>;