@mearl/client 2.20.5 → 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 (107) 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-app-profile-definition.js +20 -10
  6. package/dist/generated-app-profile-runtime-types.d.ts +4 -0
  7. package/dist/generated-browser-action-protocol.d.ts +11 -1
  8. package/dist/generated-browser-action-protocol.js +3 -0
  9. package/dist/generated-plugin-diagnostics.d.ts +110 -0
  10. package/dist/generated-plugin-diagnostics.js +1 -0
  11. package/dist/generated-plugin-execution.d.ts +27 -0
  12. package/dist/generated-plugin-execution.js +1 -0
  13. package/dist/generated-plugin-models.d.ts +64 -0
  14. package/dist/generated-plugin-models.js +1 -0
  15. package/dist/generated-plugin-pages.d.ts +76 -0
  16. package/dist/generated-plugin-pages.js +59 -0
  17. package/dist/generated-plugin-recovery.d.ts +32 -0
  18. package/dist/generated-plugin-recovery.js +1 -0
  19. package/dist/generated-plugin-state.d.ts +50 -0
  20. package/dist/generated-plugin-state.js +1 -0
  21. package/dist/generated-plugin-tasks.d.ts +74 -0
  22. package/dist/generated-plugin-tasks.js +7 -0
  23. package/dist/generated-toolbox-icon.d.ts +10 -0
  24. package/dist/generated-toolbox-icon.js +37 -0
  25. package/dist/index.d.ts +11 -0
  26. package/dist/index.js +6 -0
  27. package/dist/plugin-task-host.d.ts +1 -0
  28. package/dist/plugin-task-host.js +14 -0
  29. package/dist/plugin-worker.d.ts +1 -0
  30. package/dist/plugin-worker.js +17 -0
  31. package/dist/plugin.d.ts +57 -4
  32. package/dist/plugin.js +296 -15
  33. package/dist/pluginCliOutput.d.ts +2 -0
  34. package/dist/pluginCliOutput.js +7 -0
  35. package/dist/pluginCommandWorker.d.ts +2 -0
  36. package/dist/pluginCommandWorker.js +174 -0
  37. package/dist/pluginDiagnostics.d.ts +26 -0
  38. package/dist/pluginDiagnostics.js +309 -0
  39. package/dist/pluginExecution.d.ts +21 -0
  40. package/dist/pluginExecution.js +43 -0
  41. package/dist/pluginExecutor.d.ts +29 -0
  42. package/dist/pluginExecutor.js +223 -0
  43. package/dist/pluginExecutorProtocol.d.ts +55 -0
  44. package/dist/pluginExecutorProtocol.js +47 -0
  45. package/dist/pluginExecutorSupervisor.d.ts +4 -0
  46. package/dist/pluginExecutorSupervisor.js +63 -0
  47. package/dist/pluginHost.d.ts +20 -0
  48. package/dist/pluginHost.js +53 -0
  49. package/dist/pluginHostConnection.d.ts +35 -0
  50. package/dist/pluginHostConnection.js +257 -0
  51. package/dist/pluginHostFiles.d.ts +1 -0
  52. package/dist/pluginHostFiles.js +21 -0
  53. package/dist/pluginHostPaths.d.ts +11 -0
  54. package/dist/pluginHostPaths.js +22 -0
  55. package/dist/pluginHostServer.d.ts +7 -0
  56. package/dist/pluginHostServer.js +252 -0
  57. package/dist/pluginModelCatalog.d.ts +14 -0
  58. package/dist/pluginModelCatalog.js +51 -0
  59. package/dist/pluginModelDefinition.d.ts +6 -0
  60. package/dist/pluginModelDefinition.js +75 -0
  61. package/dist/pluginModelSessionWorker.d.ts +23 -0
  62. package/dist/pluginModelSessionWorker.js +60 -0
  63. package/dist/pluginModelSessions.d.ts +16 -0
  64. package/dist/pluginModelSessions.js +234 -0
  65. package/dist/pluginModels.d.ts +17 -0
  66. package/dist/pluginModels.js +47 -0
  67. package/dist/pluginPageBundle.js +74 -17
  68. package/dist/pluginPageDefinition.d.ts +3 -0
  69. package/dist/pluginPageDefinition.js +62 -0
  70. package/dist/pluginPageManager.d.ts +57 -0
  71. package/dist/pluginPageManager.js +877 -0
  72. package/dist/pluginPages.d.ts +21 -0
  73. package/dist/pluginPages.js +46 -0
  74. package/dist/pluginProcessTree.d.ts +2 -0
  75. package/dist/pluginProcessTree.js +31 -0
  76. package/dist/pluginRecoveryDefinition.d.ts +3 -0
  77. package/dist/pluginRecoveryDefinition.js +56 -0
  78. package/dist/pluginRegistry.d.ts +2 -3
  79. package/dist/pluginRegistry.js +14 -13
  80. package/dist/pluginRevision.d.ts +14 -0
  81. package/dist/pluginRevision.js +227 -0
  82. package/dist/pluginRuntimeCleanup.d.ts +2 -0
  83. package/dist/pluginRuntimeCleanup.js +55 -0
  84. package/dist/pluginState.d.ts +26 -0
  85. package/dist/pluginState.js +28 -0
  86. package/dist/pluginStateDefinition.d.ts +4 -0
  87. package/dist/pluginStateDefinition.js +68 -0
  88. package/dist/pluginStateManager.d.ts +24 -0
  89. package/dist/pluginStateManager.js +132 -0
  90. package/dist/pluginStateStore.d.ts +57 -0
  91. package/dist/pluginStateStore.js +396 -0
  92. package/dist/pluginTaskManager.d.ts +92 -0
  93. package/dist/pluginTaskManager.js +1080 -0
  94. package/dist/pluginTaskProtocol.d.ts +47 -0
  95. package/dist/pluginTaskProtocol.js +1 -0
  96. package/dist/pluginTaskStore.d.ts +53 -0
  97. package/dist/pluginTaskStore.js +319 -0
  98. package/dist/pluginTaskWire.d.ts +4 -0
  99. package/dist/pluginTaskWire.js +42 -0
  100. package/dist/pluginTasks.d.ts +51 -0
  101. package/dist/pluginTasks.js +181 -0
  102. package/dist/pythonPlugin.d.ts +13 -2
  103. package/dist/pythonPlugin.js +235 -30
  104. package/dist/socket.js +5 -2
  105. package/dist/unified.d.ts +1 -0
  106. package/dist/unified.js +81 -0
  107. package/package.json +7 -3
@@ -0,0 +1,26 @@
1
+ import type { PluginModelDiagnostic } from './generated-plugin-models.js';
2
+ import type { PluginDiagnosticFailure, PluginDiagnosticOptions, PluginDiagnosticPage, PluginDiagnosticPageBinding, PluginDiagnosticReport } from './generated-plugin-diagnostics.js';
3
+ import type { PluginTaskRecord } from './pluginTaskStore.js';
4
+ import type { PluginTaskScope } from './pluginTaskProtocol.js';
5
+ import type { PluginExecutionHost } from './generated-plugin-execution.js';
6
+ export declare const MAX_PLUGIN_DIAGNOSTIC_BYTES: number;
7
+ export declare function normalizeDiagnosticOptions(value: unknown): PluginDiagnosticOptions;
8
+ export declare function diagnosticFailure(value: {
9
+ code?: string;
10
+ message?: string;
11
+ } | undefined, options: PluginDiagnosticOptions): PluginDiagnosticFailure | undefined;
12
+ /** A worker can publish only this small operational vocabulary. */
13
+ export declare function normalizeModelDiagnostic(value: unknown, previous?: PluginModelDiagnostic): PluginModelDiagnostic;
14
+ export declare function diagnosticBinding(value: {
15
+ instanceId: string;
16
+ target: {
17
+ browserSessionId: string;
18
+ tabId: number;
19
+ frameId: string | number;
20
+ documentId: string;
21
+ };
22
+ }): PluginDiagnosticPageBinding;
23
+ export declare function buildDiagnosticReport(host: PluginExecutionHost, scope: PluginTaskScope, options: PluginDiagnosticOptions, scan: {
24
+ records: PluginTaskRecord[];
25
+ limited: boolean;
26
+ }, snapshots: PluginDiagnosticPage[]): PluginDiagnosticReport;
@@ -0,0 +1,309 @@
1
+ import { isProcessOwnerAlive } from '@mearl/daemon-core';
2
+ import { basename } from 'node:path';
3
+ import { isPluginTaskId, PLUGIN_TASK_PROTOCOL_VERSION } from './generated-plugin-tasks.js';
4
+ import { isPluginPageInstanceId } from './generated-plugin-pages.js';
5
+ export const MAX_PLUGIN_DIAGNOSTIC_BYTES = 256 * 1024;
6
+ function text(value, maximum = 256) {
7
+ if (typeof value !== 'string')
8
+ return;
9
+ const bytes = Buffer.from(value);
10
+ return bytes.length <= maximum
11
+ ? value
12
+ : bytes
13
+ .subarray(0, maximum)
14
+ .toString('utf8')
15
+ .replace(/\uFFFD$/, '');
16
+ }
17
+ export function normalizeDiagnosticOptions(value) {
18
+ if (!value || typeof value !== 'object' || Array.isArray(value))
19
+ throw new Error('Invalid diagnostic options');
20
+ const input = value;
21
+ if (Object.keys(input).some(key => !['taskId', 'plugin', 'instanceId', 'includeErrorMessages', 'includeLocalPaths'].includes(key)))
22
+ throw new Error('Unknown diagnostic option');
23
+ if (input.taskId !== undefined && !isPluginTaskId(input.taskId))
24
+ throw new Error('Invalid diagnostic task ID');
25
+ if (input.instanceId !== undefined && !isPluginPageInstanceId(input.instanceId))
26
+ throw new Error('Invalid diagnostic instance ID');
27
+ if (input.plugin !== undefined &&
28
+ (typeof input.plugin !== 'string' ||
29
+ !/^[a-z][a-z0-9-]*$/.test(input.plugin) ||
30
+ input.plugin.length > 128))
31
+ throw new Error('Invalid diagnostic plugin');
32
+ for (const key of ['includeErrorMessages', 'includeLocalPaths'])
33
+ if (input[key] !== undefined && typeof input[key] !== 'boolean')
34
+ throw new Error('Invalid diagnostic content option');
35
+ return {
36
+ ...(input.taskId ? { taskId: String(input.taskId) } : {}),
37
+ ...(input.plugin ? { plugin: String(input.plugin) } : {}),
38
+ ...(input.instanceId ? { instanceId: String(input.instanceId) } : {}),
39
+ ...(input.includeErrorMessages !== undefined
40
+ ? { includeErrorMessages: input.includeErrorMessages }
41
+ : {}),
42
+ ...(input.includeLocalPaths !== undefined
43
+ ? { includeLocalPaths: input.includeLocalPaths }
44
+ : {}),
45
+ };
46
+ }
47
+ export function diagnosticFailure(value, options) {
48
+ if (!value)
49
+ return;
50
+ const message = options.includeErrorMessages ? text(value.message, 4096) : undefined;
51
+ return {
52
+ code: text(value.code) ?? 'PLUGIN_ERROR_UNCLASSIFIED',
53
+ ...(message !== undefined
54
+ ? {
55
+ message,
56
+ ...(Buffer.byteLength(value.message ?? '') > 4096 ? { messageTruncated: true } : {}),
57
+ }
58
+ : {}),
59
+ };
60
+ }
61
+ /** A worker can publish only this small operational vocabulary. */
62
+ export function normalizeModelDiagnostic(value, previous = {}) {
63
+ const result = { ...previous };
64
+ if (!value || typeof value !== 'object' || Array.isArray(value))
65
+ return result;
66
+ const input = value;
67
+ for (const key of ['requestedModel', 'selectedModel', 'activeModel'])
68
+ if (typeof input[key] === 'string')
69
+ result[key] = text(input[key]);
70
+ if (['apikey', 'chatgpt', 'chatgptAuthTokens', 'unknown'].includes(input.authentication))
71
+ result.authentication = input.authentication;
72
+ for (const key of ['threadRestrictionsAccepted', 'turnRestrictionsAccepted'])
73
+ if (typeof input[key] === 'boolean')
74
+ result[key] = input[key];
75
+ if (Number.isSafeInteger(input.retryCount) && input.retryCount >= 0)
76
+ result.retryCount = Math.min(input.retryCount, 1000000);
77
+ const runtime = input.runtime;
78
+ if (runtime && ['explicit', 'automatic'].includes(runtime.selection))
79
+ result.runtime = {
80
+ ...(typeof runtime.executable === 'string'
81
+ ? { executable: text(runtime.executable, 4096) }
82
+ : {}),
83
+ ...(typeof runtime.version === 'string' ? { version: text(runtime.version) } : {}),
84
+ selection: runtime.selection,
85
+ ...(typeof runtime.started === 'boolean' ? { started: runtime.started } : {}),
86
+ };
87
+ const config = input.configuration;
88
+ if (config &&
89
+ ['default', 'environment'].includes(config.source) &&
90
+ typeof config.filePresent === 'boolean')
91
+ result.configuration = {
92
+ source: config.source,
93
+ filePresent: config.filePresent,
94
+ ...(typeof config.home === 'string' ? { home: text(config.home, 4096) } : {}),
95
+ ...(typeof config.baseProvider === 'string'
96
+ ? { baseProvider: text(config.baseProvider) }
97
+ : {}),
98
+ ...(['file', 'keyring', 'auto', 'ephemeral', 'unknown'].includes(config.credentialStore)
99
+ ? { credentialStore: config.credentialStore }
100
+ : {}),
101
+ };
102
+ return result;
103
+ }
104
+ export function diagnosticBinding(value) {
105
+ return {
106
+ instanceId: value.instanceId,
107
+ browserSessionId: value.target.browserSessionId,
108
+ tabId: value.target.tabId,
109
+ frameId: value.target.frameId,
110
+ documentId: value.target.documentId,
111
+ };
112
+ }
113
+ function modelProjection(record, options) {
114
+ if (record.task.kind !== 'model')
115
+ return;
116
+ const request = record.task.input.request;
117
+ const metadata = normalizeModelDiagnostic(record.diagnostics?.model, typeof request?.model === 'string' ? { requestedModel: text(request.model) } : {});
118
+ if (!options.includeLocalPaths) {
119
+ if (metadata.runtime?.executable)
120
+ metadata.runtime = { ...metadata.runtime, executable: basename(metadata.runtime.executable) };
121
+ if (metadata.configuration) {
122
+ const { home: _home, ...configuration } = metadata.configuration;
123
+ metadata.configuration = configuration;
124
+ }
125
+ }
126
+ return metadata;
127
+ }
128
+ function taskProjection(record, options, observedAt) {
129
+ const { task, diagnostics, execution, executorOwner: owner } = record;
130
+ const events = record.events.filter(event => event.type !== 'progress');
131
+ const first = record.events[0]?.sequence ?? task.sequence + 1;
132
+ const alive = owner && !diagnostics?.executorStoppedAt ? isProcessOwnerAlive(owner) : false;
133
+ const stopped = diagnostics?.executorStoppedAt;
134
+ const state = alive
135
+ ? 'observed-alive'
136
+ : stopped
137
+ ? 'stop-confirmed'
138
+ : diagnostics?.executorAttempted === false
139
+ ? 'never-started'
140
+ : 'unknown';
141
+ const routing = execution?.options;
142
+ return {
143
+ taskId: task.taskId,
144
+ plugin: text(task.plugin),
145
+ kind: task.kind ?? 'command',
146
+ command: text(task.command),
147
+ state: task.state,
148
+ source: task.source,
149
+ host: { hostId: task.host.hostId, epoch: task.host.epoch },
150
+ installationId: task.installationId,
151
+ revision: task.revision,
152
+ ...(diagnostics?.invocationId ? { invocationId: diagnostics.invocationId } : {}),
153
+ createdAt: task.createdAt,
154
+ updatedAt: task.updatedAt,
155
+ sequence: task.sequence,
156
+ ...(diagnostics?.page
157
+ ? {
158
+ page: diagnosticBinding({
159
+ instanceId: diagnostics.page.instanceId,
160
+ target: diagnostics.page,
161
+ }),
162
+ }
163
+ : {}),
164
+ ...(routing
165
+ ? {
166
+ routing: {
167
+ ...(routing.browser ? { browser: text(routing.browser, 1024) } : {}),
168
+ ...(routing.session ? { session: text(routing.session) } : {}),
169
+ ...(routing.connector ? { connector: text(routing.connector) } : {}),
170
+ ...(routing.local !== undefined ? { local: routing.local } : {}),
171
+ ...(routing.transport ? { transport: routing.transport } : {}),
172
+ },
173
+ }
174
+ : {}),
175
+ ...(execution?.executionTimeoutMs ? { executionTimeoutMs: execution.executionTimeoutMs } : {}),
176
+ executor: {
177
+ state,
178
+ observedAt,
179
+ ...(owner ? { pid: owner.pid } : {}),
180
+ ...(stopped ? { stoppedAt: stopped } : {}),
181
+ },
182
+ ...(task.error ? { failure: diagnosticFailure(task.error, options) } : {}),
183
+ ...(task.checkpoint
184
+ ? {
185
+ checkpoint: {
186
+ sequence: task.checkpoint.sequence,
187
+ version: task.checkpoint.version,
188
+ schemaHash: task.checkpoint.schemaHash,
189
+ updatedAt: task.checkpoint.updatedAt,
190
+ },
191
+ }
192
+ : {}),
193
+ ...(task.result
194
+ ? {
195
+ result: {
196
+ bytes: task.result.bytes,
197
+ chunks: task.result.chunks,
198
+ sha256: task.result.sha256,
199
+ },
200
+ }
201
+ : {}),
202
+ ...(task.kind === 'model' ? { model: modelProjection(record, options) } : {}),
203
+ metadataIncomplete: !diagnostics ||
204
+ Boolean(diagnostics.updatesDropped) ||
205
+ (task.kind === 'model' && !diagnostics.model?.runtime),
206
+ history: {
207
+ firstRetainedSequence: first,
208
+ gap: first > 1,
209
+ omitted: Math.max(0, events.length - 8),
210
+ events: events.slice(-8).map(event => ({
211
+ sequence: event.sequence,
212
+ time: event.time,
213
+ state: event.state,
214
+ type: event.type,
215
+ })),
216
+ },
217
+ };
218
+ }
219
+ export function buildDiagnosticReport(host, scope, options, scan, snapshots) {
220
+ const records = scan.records.filter(record => (!scope.plugin || record.task.plugin === scope.plugin) &&
221
+ (scope.access !== 'toolbox' || record.access !== 'public') &&
222
+ (!options.plugin || record.task.plugin === options.plugin) &&
223
+ (!options.instanceId || record.diagnostics?.page?.instanceId === options.instanceId));
224
+ const visible = new Map(records.map(record => [record.task.taskId, record]));
225
+ let selected = records.sort((a, b) => b.task.createdAt - a.task.createdAt || a.task.taskId.localeCompare(b.task.taskId));
226
+ if (options.taskId) {
227
+ if (!visible.has(options.taskId))
228
+ throw Object.assign(new Error('Plugin task was not found'), {
229
+ code: 'PLUGIN_TASK_NOT_FOUND',
230
+ });
231
+ const ids = new Set([options.taskId]), pending = [options.taskId];
232
+ while (pending.length) {
233
+ const id = pending.shift(), item = visible.get(id);
234
+ const candidates = [
235
+ item.task.parentTaskId,
236
+ item.task.resumedFrom?.taskId,
237
+ ...records
238
+ .filter(record => record.task.parentTaskId === id || record.task.resumedFrom?.taskId === id)
239
+ .map(record => record.task.taskId),
240
+ ];
241
+ for (const candidate of candidates)
242
+ if (candidate && visible.has(candidate) && !ids.has(candidate)) {
243
+ ids.add(candidate);
244
+ pending.push(candidate);
245
+ }
246
+ }
247
+ selected = [...ids].map(id => visible.get(id));
248
+ }
249
+ const generatedAt = Date.now(), chosen = selected.slice(0, 128);
250
+ const tasks = chosen.map(record => taskProjection(record, options, generatedAt));
251
+ const pages = snapshots.filter(page => (!scope.plugin || page.plugin === scope.plugin) &&
252
+ (!options.plugin || page.plugin === options.plugin) &&
253
+ (!options.instanceId || page.binding.instanceId === options.instanceId) &&
254
+ (!options.taskId || tasks.some(task => task.page?.instanceId === page.binding.instanceId)));
255
+ for (const task of tasks)
256
+ if (task.page && !pages.some(page => page.binding.instanceId === task.page.instanceId))
257
+ pages.push({
258
+ plugin: task.plugin,
259
+ installationId: task.installationId,
260
+ revision: task.revision,
261
+ host: task.host,
262
+ binding: task.page,
263
+ observation: 'historical-binding',
264
+ });
265
+ const report = {
266
+ version: 1,
267
+ generatedAt,
268
+ host: { hostId: host.hostId, epoch: host.epoch },
269
+ protocolVersion: PLUGIN_TASK_PROTOCOL_VERSION,
270
+ selection: {
271
+ ...(options.taskId ? { taskId: options.taskId } : {}),
272
+ ...(options.plugin ? { plugin: options.plugin } : {}),
273
+ ...(options.instanceId ? { instanceId: options.instanceId } : {}),
274
+ },
275
+ coverage: {
276
+ scanLimited: scan.limited,
277
+ tasksOmitted: selected.length - tasks.length,
278
+ pagesOmitted: Math.max(0, pages.length - 64),
279
+ byteLimited: false,
280
+ },
281
+ tasks,
282
+ pages: pages.slice(0, 64),
283
+ links: [],
284
+ };
285
+ const links = () => {
286
+ const retained = new Set(report.tasks.map(task => task.taskId));
287
+ report.links = report.tasks.flatMap(task => {
288
+ const record = visible.get(task.taskId);
289
+ return [
290
+ ['parent', record.task.parentTaskId],
291
+ ['resume', record.task.resumedFrom?.taskId],
292
+ ].flatMap(([kind, from]) => from && retained.has(from) ? [{ from, to: task.taskId, kind }] : []);
293
+ });
294
+ };
295
+ links();
296
+ while (Buffer.byteLength(JSON.stringify(report)) > MAX_PLUGIN_DIAGNOSTIC_BYTES) {
297
+ report.coverage.byteLimited = true;
298
+ if (report.pages.length) {
299
+ report.pages.pop();
300
+ report.coverage.pagesOmitted++;
301
+ }
302
+ else {
303
+ report.tasks.pop();
304
+ report.coverage.tasksOmitted++;
305
+ links();
306
+ }
307
+ }
308
+ return report;
309
+ }
@@ -0,0 +1,21 @@
1
+ import type { PluginExecutionHost, PluginInvocationIdentity, PluginInvocationSource } from './generated-plugin-execution.js';
2
+ import type { PluginApp } from './plugin.js';
3
+ import type { PluginTaskRecoveryContext } from './generated-plugin-recovery.js';
4
+ export type { PluginExecutionHost, PluginInvocationIdentity, PluginInvocationSource };
5
+ export interface PluginCommandContext extends Pick<PluginApp, 'invoke' | 'progress'> {
6
+ readonly identity: Readonly<PluginInvocationIdentity>;
7
+ readonly signal: AbortSignal;
8
+ readonly state: PluginApp['state'];
9
+ readonly model: PluginApp['model'];
10
+ readonly recovery?: Readonly<PluginTaskRecoveryContext>;
11
+ readonly checkpoint: PluginApp['checkpoint'];
12
+ readonly page?: {
13
+ send(message: unknown, options?: {
14
+ timeoutSec?: number;
15
+ }): Promise<unknown>;
16
+ get(): Promise<unknown>;
17
+ };
18
+ }
19
+ /** Exclusive creation gives concurrent browser processes the same environment identity. */
20
+ export declare function localPluginExecutionHost(): PluginExecutionHost;
21
+ export declare function createPluginInvocationIdentity(plugin: string, source: PluginInvocationSource, parent?: PluginInvocationIdentity): PluginInvocationIdentity;
@@ -0,0 +1,43 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { linkSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { pluginDirectory } from './pluginRegistry.js';
5
+ const epoch = randomUUID();
6
+ const hostIds = new Map();
7
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
8
+ /** Exclusive creation gives concurrent browser processes the same environment identity. */
9
+ export function localPluginExecutionHost() {
10
+ const directory = pluginDirectory();
11
+ let hostId = hostIds.get(directory);
12
+ if (!hostId) {
13
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
14
+ const file = join(directory, '.execution-host-id');
15
+ const proposed = randomUUID();
16
+ const temporary = `${file}.${proposed}.tmp`;
17
+ try {
18
+ writeFileSync(temporary, proposed, { flag: 'wx', mode: 0o600 });
19
+ linkSync(temporary, file);
20
+ }
21
+ catch (error) {
22
+ if (error.code !== 'EEXIST')
23
+ throw error;
24
+ }
25
+ finally {
26
+ rmSync(temporary, { force: true });
27
+ }
28
+ hostId = readFileSync(file, 'utf8').trim();
29
+ if (!UUID.test(hostId))
30
+ throw new Error('Invalid plugin execution host identity');
31
+ hostIds.set(directory, hostId);
32
+ }
33
+ return Object.freeze({ hostId, epoch });
34
+ }
35
+ export function createPluginInvocationIdentity(plugin, source, parent) {
36
+ return Object.freeze({
37
+ invocationId: randomUUID(),
38
+ host: parent?.host ?? localPluginExecutionHost(),
39
+ plugin,
40
+ source,
41
+ ...(parent ? { parentInvocationId: parent.invocationId } : {}),
42
+ });
43
+ }
@@ -0,0 +1,29 @@
1
+ import type { PluginModelDiagnostic, PluginModelRequest } from './generated-plugin-models.js';
2
+ import { type PluginRegistration } from './pluginRegistry.js';
3
+ import type { PluginExecutionContext, PluginInvocation, PluginInvokeOptions } from './plugin.js';
4
+ export interface PluginExecutorDependencies {
5
+ stopProcessTree?: (pid: number) => Promise<void>;
6
+ registration?: PluginRegistration;
7
+ }
8
+ export interface PluginExecutorOptions extends PluginInvokeOptions {
9
+ describe?: boolean;
10
+ model?: PluginModelRequest;
11
+ recovery?: boolean;
12
+ migration?: boolean;
13
+ expectedRecovery?: string;
14
+ onDiagnostic?: (metadata: PluginModelDiagnostic) => void;
15
+ onExecutorStopped?: () => void;
16
+ onExecutorStarted?: (owner: {
17
+ pid: number;
18
+ processIdentity?: string;
19
+ }) => void;
20
+ access?: 'public' | 'toolbox' | 'runtime';
21
+ /** Execution deadline, independent of transport and caller wait timeouts. */
22
+ executionTimeoutMs?: number;
23
+ cancellationGraceMs?: number;
24
+ workerPath?: string;
25
+ cwd?: string;
26
+ environment?: NodeJS.ProcessEnv;
27
+ }
28
+ /** Executes registered trusted code in a process whose lifetime belongs to this invocation. */
29
+ export declare function executeRegisteredPlugin(plugin: string, invocation: PluginInvocation, context: PluginExecutionContext, options?: PluginExecutorOptions, dependencies?: PluginExecutorDependencies): Promise<unknown>;
@@ -0,0 +1,223 @@
1
+ import { fork } from 'node:child_process';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { findPluginRegistration, PluginError } from './pluginRegistry.js';
4
+ import { pluginFileVersion } from './pluginSource.js';
5
+ import { createPluginInvocationIdentity } from './pluginExecution.js';
6
+ import { pluginExecutionError, serializePluginExecutionError } from './pluginExecutorProtocol.js';
7
+ import { stopPluginProcessTree } from './pluginProcessTree.js';
8
+ import { processIdentity } from '@mearl/daemon-core';
9
+ /** Executes registered trusted code in a process whose lifetime belongs to this invocation. */
10
+ export async function executeRegisteredPlugin(plugin, invocation, context, options = {}, dependencies = {}) {
11
+ const { signal, onProgress, source, access = 'public', executionTimeoutMs, cancellationGraceMs = 1000, workerPath, cwd, environment, describe, model, recovery, migration, expectedRecovery, onExecutorStarted, onExecutorStopped, onDiagnostic, ...invokeOptions } = options;
12
+ for (const [name, value] of [
13
+ ['executionTimeoutMs', executionTimeoutMs],
14
+ ['cancellationGraceMs', cancellationGraceMs],
15
+ ]) {
16
+ if (value !== undefined && (!Number.isSafeInteger(value) || value < 1 || value > 2_147_483_647))
17
+ throw new Error(`${name} must be a positive integer within the timer range`);
18
+ }
19
+ signal?.throwIfAborted();
20
+ const registration = dependencies.registration ?? findPluginRegistration(plugin);
21
+ if (!registration)
22
+ throw new PluginError('PLUGIN_NOT_REGISTERED', `插件未注册: ${plugin}`);
23
+ const identity = context.invocation ??
24
+ createPluginInvocationIdentity(plugin, source ?? context.source ?? 'sdk', context.parentInvocation);
25
+ const entryVersion = pluginFileVersion(registration.entry);
26
+ return new Promise((resolve, reject) => {
27
+ const child = fork(workerPath ?? fileURLToPath(new URL('./plugin-worker.js', import.meta.url)), [], {
28
+ stdio: ['ignore', 'ignore', 'pipe', 'ipc'],
29
+ serialization: 'advanced',
30
+ execArgv: [],
31
+ detached: process.platform !== 'win32',
32
+ ...(cwd ? { cwd } : {}),
33
+ ...(environment ? { env: environment } : {}),
34
+ });
35
+ let stderr = '';
36
+ let outcome;
37
+ let stopping;
38
+ let cleanupError;
39
+ let treeStopRequested = false;
40
+ let treeCleanup = Promise.resolve();
41
+ let exited = false;
42
+ let graceTimer;
43
+ let deadlineTimer;
44
+ const pendingActions = new Map();
45
+ child.stderr?.setEncoding('utf8');
46
+ child.stderr?.on('data', chunk => {
47
+ stderr = `${stderr}${chunk}`.slice(-65536);
48
+ });
49
+ const send = (message) => {
50
+ if (!child.connected || exited)
51
+ return;
52
+ try {
53
+ child.send(message, error => {
54
+ if (error && !exited)
55
+ stop(error);
56
+ });
57
+ }
58
+ catch (error) {
59
+ stop(error instanceof Error ? error : new Error(String(error)));
60
+ }
61
+ };
62
+ const kill = () => {
63
+ if (!child.pid || exited || treeStopRequested)
64
+ return;
65
+ treeStopRequested = true;
66
+ const pid = child.pid;
67
+ treeCleanup = Promise.resolve()
68
+ .then(() => (dependencies.stopProcessTree ?? stopPluginProcessTree)(pid))
69
+ .catch(() => {
70
+ cleanupError = Object.assign(new Error('Unable to confirm plugin process-tree cleanup'), {
71
+ code: 'PLUGIN_EXECUTOR_CLEANUP_FAILED',
72
+ details: { executionStopped: false },
73
+ });
74
+ child.kill('SIGKILL');
75
+ });
76
+ };
77
+ const stop = (error) => {
78
+ if (stopping || outcome || exited)
79
+ return;
80
+ stopping = error;
81
+ send({ type: 'cancel', error: serializePluginExecutionError(error) });
82
+ graceTimer = setTimeout(kill, cancellationGraceMs);
83
+ };
84
+ const abort = () => stop(Object.assign(new Error('Plugin execution cancelled'), {
85
+ code: 'PLUGIN_EXECUTION_CANCELLED',
86
+ }));
87
+ signal?.addEventListener('abort', abort, { once: true });
88
+ if (signal?.aborted)
89
+ abort();
90
+ if (executionTimeoutMs !== undefined)
91
+ deadlineTimer = setTimeout(() => stop(Object.assign(new Error('Plugin execution deadline exceeded'), {
92
+ code: 'PLUGIN_EXECUTION_DEADLINE',
93
+ })), executionTimeoutMs);
94
+ child.on('message', (raw) => {
95
+ if (exited || !raw || typeof raw !== 'object')
96
+ return;
97
+ if (raw.type === 'result' || raw.type === 'error') {
98
+ if (!outcome && !stopping) {
99
+ try {
100
+ outcome =
101
+ raw.type === 'result'
102
+ ? { value: JSON.parse(raw.json).value }
103
+ : { error: pluginExecutionError(raw.error) };
104
+ if (raw.type === 'result' && pendingActions.size)
105
+ outcome = {
106
+ error: Object.assign(new Error('Plugin returned with pending host calls'), {
107
+ code: 'PLUGIN_PENDING_HOST_CALLS',
108
+ details: { pendingActions: [...pendingActions.values()] },
109
+ }),
110
+ };
111
+ }
112
+ catch (error) {
113
+ stop(error instanceof Error ? error : new Error(String(error)));
114
+ }
115
+ }
116
+ kill();
117
+ return;
118
+ }
119
+ if (stopping || outcome)
120
+ return;
121
+ if (raw.type === 'diagnostic' && model) {
122
+ try {
123
+ onDiagnostic?.(raw.metadata);
124
+ }
125
+ catch (error) {
126
+ stop(error instanceof Error ? error : new Error(String(error)));
127
+ }
128
+ }
129
+ else if (raw.type === 'progress') {
130
+ try {
131
+ onProgress?.(raw.progress);
132
+ }
133
+ catch (error) {
134
+ stop(error instanceof Error ? error : new Error(String(error)));
135
+ }
136
+ }
137
+ else if (raw.type === 'invoke') {
138
+ pendingActions.set(raw.id, raw.action);
139
+ void Promise.resolve()
140
+ .then(() => {
141
+ if (stopping || exited)
142
+ throw stopping ?? new Error('Plugin execution ended');
143
+ const options = { ...invokeOptions, ...raw.options };
144
+ if (!raw.stream)
145
+ return context.invoke(raw.action, raw.payload, options);
146
+ return context.invoke(raw.action, raw.payload, options, progress => {
147
+ if (!stopping && !outcome && pendingActions.has(raw.id))
148
+ send({ type: 'invoke-progress', id: raw.id, progress });
149
+ });
150
+ })
151
+ .then(result => send({ type: 'response', id: raw.id, result }), error => send({ type: 'response', id: raw.id, error: serializePluginExecutionError(error) }))
152
+ .finally(() => pendingActions.delete(raw.id));
153
+ }
154
+ });
155
+ const cleanup = () => {
156
+ exited = true;
157
+ signal?.removeEventListener('abort', abort);
158
+ if (graceTimer)
159
+ clearTimeout(graceTimer);
160
+ if (deadlineTimer)
161
+ clearTimeout(deadlineTimer);
162
+ };
163
+ child.once('error', error => {
164
+ cleanup();
165
+ reject(error);
166
+ });
167
+ child.once('exit', () => {
168
+ kill();
169
+ child.stderr?.destroy();
170
+ });
171
+ child.once('close', (code, exitSignal) => {
172
+ cleanup();
173
+ void treeCleanup.then(() => {
174
+ if (!cleanupError && treeStopRequested) {
175
+ try {
176
+ onExecutorStopped?.();
177
+ }
178
+ catch (error) {
179
+ reject(error);
180
+ return;
181
+ }
182
+ }
183
+ if (cleanupError)
184
+ reject(cleanupError);
185
+ else if (stopping) {
186
+ Object.assign(stopping, {
187
+ details: { pendingActions: [...pendingActions.values()], executionStopped: true },
188
+ });
189
+ reject(stopping);
190
+ }
191
+ else if (outcome?.error)
192
+ reject(outcome.error);
193
+ else if (outcome)
194
+ resolve(outcome.value);
195
+ else
196
+ reject(Object.assign(new Error(stderr.trim() || `Plugin executor exited (${code ?? exitSignal})`), { code: 'PLUGIN_EXECUTOR_EXITED' }));
197
+ });
198
+ });
199
+ try {
200
+ if (child.pid)
201
+ onExecutorStarted?.({ pid: child.pid, processIdentity: processIdentity(child.pid) });
202
+ send({
203
+ type: 'start',
204
+ plugin,
205
+ invocation,
206
+ options: invokeOptions,
207
+ identity,
208
+ entry: registration.entry,
209
+ entryVersion,
210
+ access,
211
+ describe,
212
+ model,
213
+ recovery,
214
+ migration,
215
+ expectedRecovery,
216
+ ...(dependencies.registration ? { registration } : {}),
217
+ });
218
+ }
219
+ catch (error) {
220
+ stop(error instanceof Error ? error : new Error(String(error)));
221
+ }
222
+ });
223
+ }