@mearl/client 3.0.0-beta.3 → 3.0.0-beta.5
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 +94 -15
- package/dist/check.d.ts +5 -6
- package/dist/check.js +3 -6
- package/dist/cli.js +15 -43
- package/dist/commandHelp.d.ts +3 -0
- package/dist/commandHelp.js +79 -0
- package/dist/commands.d.ts +24 -14
- package/dist/commands.js +23 -18
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -0
- package/dist/nativeHost.js +1 -1
- package/dist/plugins/clients/pluginHostConnection.d.ts +2 -0
- package/dist/plugins/clients/pluginHostConnection.js +3 -1
- package/dist/plugins/pluginRegistry.d.ts +1 -1
- package/dist/plugins/pluginRegistry.js +12 -4
- package/dist/plugins/runtime/pluginCommandWorker.js +8 -6
- package/dist/plugins/runtime/pluginExecutor.d.ts +5 -0
- package/dist/plugins/runtime/pluginExecutor.js +15 -2
- package/dist/plugins/runtime/pluginExecutorProtocol.d.ts +4 -0
- package/dist/plugins/runtime/pluginExecutorSupervisor.js +2 -1
- package/dist/plugins/runtime/pluginHostServer.js +4 -0
- package/dist/plugins/runtime/pluginPageManager.js +0 -4
- package/dist/plugins/runtime/pluginRevision.js +0 -51
- package/dist/plugins/runtime/pluginStateStore.js +3 -8
- package/dist/plugins/runtime/pluginTaskManager.js +3 -7
- package/dist/plugins/sdk/plugin.js +1 -1
- package/dist/plugins/sdk/pluginModelDefinition.js +0 -6
- package/dist/plugins/sdk/pluginPageDefinition.d.ts +0 -1
- package/dist/plugins/sdk/pluginPageDefinition.js +2 -18
- package/dist/plugins/sdk/pluginRecoveryDefinition.js +0 -3
- package/dist/plugins/sdk/pluginStateDefinition.d.ts +0 -1
- package/dist/plugins/sdk/pluginStateDefinition.js +0 -11
- package/dist/runtimePaths.d.ts +1 -0
- package/dist/runtimePaths.js +1 -0
- package/dist/script.d.ts +30 -0
- package/dist/script.js +260 -0
- package/dist/scriptCli.d.ts +1 -0
- package/dist/scriptCli.js +94 -0
- package/dist/scriptErrors.d.ts +8 -0
- package/dist/scriptErrors.js +21 -0
- package/dist/scriptWorker.d.ts +1 -0
- package/dist/scriptWorker.js +50 -0
- package/dist/sdkMethods.d.ts +3 -0
- package/dist/sdkMethods.js +9 -0
- package/dist/socket.js +2 -1
- package/package.json +10 -8
|
@@ -2,13 +2,10 @@ import { createHash, randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync, } from 'node:fs';
|
|
3
3
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
4
4
|
import { claimRecord, readRecord, writeRecord } from '@mearl/daemon-core';
|
|
5
|
-
import { isBuiltin } from 'node:module';
|
|
6
|
-
import { parse } from 'acorn';
|
|
7
5
|
import { pluginHostPaths } from './pluginHostPaths.js';
|
|
8
6
|
import { skillManifestPath } from '../skillManifest.js';
|
|
9
7
|
import { findPluginRegistration, PluginError } from '../pluginRegistry.js';
|
|
10
8
|
const MAX_REVISION_BYTES = 64 * 1024 * 1024;
|
|
11
|
-
const validatedSources = new Set();
|
|
12
9
|
function within(root, file) {
|
|
13
10
|
const path = relative(root, file);
|
|
14
11
|
return path === '' || (!path.startsWith(`..${sep}`) && path !== '..' && !isAbsolute(path));
|
|
@@ -68,48 +65,6 @@ function readSources(root) {
|
|
|
68
65
|
visit(root, new Set());
|
|
69
66
|
return files;
|
|
70
67
|
}
|
|
71
|
-
function validateDependencies(files) {
|
|
72
|
-
const paths = new Set(files.map(file => file.path));
|
|
73
|
-
for (const file of files) {
|
|
74
|
-
if (!/\.[cm]?js$/.test(file.path))
|
|
75
|
-
continue;
|
|
76
|
-
const tree = parse(file.bytes.toString('utf8'), {
|
|
77
|
-
ecmaVersion: 'latest',
|
|
78
|
-
sourceType: 'module',
|
|
79
|
-
allowReturnOutsideFunction: true,
|
|
80
|
-
});
|
|
81
|
-
const check = (specifier) => {
|
|
82
|
-
if (typeof specifier !== 'string' || isBuiltin(specifier))
|
|
83
|
-
return;
|
|
84
|
-
if (!specifier.startsWith('./') && !specifier.startsWith('../'))
|
|
85
|
-
throw new PluginError('PLUGIN_SOURCE_NOT_SELF_CONTAINED', `后台任务需要自包含 Skill,请先打包依赖: ${file.path}: ${specifier}`);
|
|
86
|
-
const local = resolve('/source', dirname(file.path), specifier);
|
|
87
|
-
const normalized = relative('/source', local).split(sep).join('/');
|
|
88
|
-
if (!within('/source', local) ||
|
|
89
|
-
![normalized, `${normalized}.js`, `${normalized}.json`, `${normalized}/index.js`].some(path => paths.has(path)))
|
|
90
|
-
throw new PluginError('PLUGIN_SOURCE_NOT_SELF_CONTAINED', `依赖不在 Skill 快照中: ${file.path}: ${specifier}`);
|
|
91
|
-
};
|
|
92
|
-
const visit = (node) => {
|
|
93
|
-
if (!node || typeof node !== 'object')
|
|
94
|
-
return;
|
|
95
|
-
if (['ImportDeclaration', 'ExportNamedDeclaration', 'ExportAllDeclaration'].includes(node.type))
|
|
96
|
-
check(node.source?.value);
|
|
97
|
-
if (node.type === 'ImportExpression')
|
|
98
|
-
check(node.source?.value);
|
|
99
|
-
if (node.type === 'CallExpression' &&
|
|
100
|
-
node.callee?.type === 'Identifier' &&
|
|
101
|
-
node.callee.name === 'require')
|
|
102
|
-
check(node.arguments[0]?.value);
|
|
103
|
-
for (const value of Object.values(node)) {
|
|
104
|
-
if (Array.isArray(value))
|
|
105
|
-
value.forEach(visit);
|
|
106
|
-
else if (value && typeof value === 'object')
|
|
107
|
-
visit(value);
|
|
108
|
-
}
|
|
109
|
-
};
|
|
110
|
-
visit(tree);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
68
|
function fingerprint(files, entry) {
|
|
114
69
|
return createHash('sha256')
|
|
115
70
|
.update(JSON.stringify({ entry, files: files.map(file => [file.path, file.hash, file.executable]) }))
|
|
@@ -139,12 +94,6 @@ export function capturePluginRevision(plugin) {
|
|
|
139
94
|
const relativeEntry = relative(root, entry).split(sep).join('/');
|
|
140
95
|
const files = readSources(root);
|
|
141
96
|
const revision = fingerprint(files, relativeEntry);
|
|
142
|
-
if (!validatedSources.has(revision)) {
|
|
143
|
-
validateDependencies(files);
|
|
144
|
-
if (validatedSources.size >= 64)
|
|
145
|
-
validatedSources.delete(validatedSources.values().next().value);
|
|
146
|
-
validatedSources.add(revision);
|
|
147
|
-
}
|
|
148
97
|
const revisionRoot = join(paths.revisions, installation.installationId, revision);
|
|
149
98
|
if (!existsSync(revisionRoot)) {
|
|
150
99
|
const temporary = `${revisionRoot}.${randomUUID()}.tmp`;
|
|
@@ -3,7 +3,6 @@ import { existsSync, readFileSync, readdirSync, statSync, rmSync } from 'node:fs
|
|
|
3
3
|
import { join, sep } from 'node:path';
|
|
4
4
|
import { pluginHostPaths } from './pluginHostPaths.js';
|
|
5
5
|
import { writePluginHostFile } from './pluginHostFiles.js';
|
|
6
|
-
import { compilePluginStateSchema } from '../sdk/pluginStateDefinition.js';
|
|
7
6
|
function fail(code, message) {
|
|
8
7
|
throw Object.assign(new Error(message), { code });
|
|
9
8
|
}
|
|
@@ -316,7 +315,6 @@ export class PluginStateStore {
|
|
|
316
315
|
else
|
|
317
316
|
delete entry.expiresAt;
|
|
318
317
|
}
|
|
319
|
-
const validate = compilePluginStateSchema(definition.schema);
|
|
320
318
|
for (const operation of input.operations) {
|
|
321
319
|
key(operation.key);
|
|
322
320
|
if (names.has(operation.key) ||
|
|
@@ -328,9 +326,9 @@ export class PluginStateStore {
|
|
|
328
326
|
delete entries[operation.key];
|
|
329
327
|
continue;
|
|
330
328
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
fail('
|
|
329
|
+
const json = JSON.stringify(operation.value);
|
|
330
|
+
if (json === undefined || Buffer.byteLength(json) > 256 * 1024)
|
|
331
|
+
fail('PLUGIN_STATE_INVALID', 'State value must be JSON within 256 KiB');
|
|
334
332
|
const ttl = operation.ttlMs ?? definition.ttlMs ?? 86400000;
|
|
335
333
|
if (definition.storage === 'persistent' && operation.ttlMs !== undefined)
|
|
336
334
|
fail('PLUGIN_STATE_INVALID', 'Persistent state does not accept TTL');
|
|
@@ -342,9 +340,6 @@ export class PluginStateStore {
|
|
|
342
340
|
...(definition.storage !== 'persistent' ? { expiresAt: now + ttl } : {}),
|
|
343
341
|
};
|
|
344
342
|
}
|
|
345
|
-
for (const entry of Object.values(entries))
|
|
346
|
-
if (!validate(entry.value))
|
|
347
|
-
fail('PLUGIN_STATE_SCHEMA_INVALID', 'Migration leaves entries incompatible with the target schema');
|
|
348
343
|
const commit = {
|
|
349
344
|
revision,
|
|
350
345
|
schemaVersion: definition.schemaVersion,
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { normalizeModelDiagnostic, diagnosticBinding } from './pluginDiagnostics.js';
|
|
2
2
|
import { pluginModelCapabilities, normalizePluginModel, validatePluginModelResult, } from '../sdk/pluginModelDefinition.js';
|
|
3
3
|
import { pluginRecoverySignature } from '../sdk/pluginRecoveryDefinition.js';
|
|
4
|
-
import { compilePluginStateSchema } from '../sdk/pluginStateDefinition.js';
|
|
5
4
|
import { createHash, randomUUID } from 'node:crypto';
|
|
6
5
|
import { EventEmitter } from 'node:events';
|
|
7
6
|
import { isAbsolute, join, relative, sep } from 'node:path';
|
|
@@ -482,8 +481,7 @@ export class PluginTaskManager {
|
|
|
482
481
|
signal.throwIfAborted();
|
|
483
482
|
if (!sourceDefinition ||
|
|
484
483
|
sourceDefinition.checkpointVersion !== record.checkpoint.version ||
|
|
485
|
-
digest(sourceDefinition.schema) !== record.checkpoint.schemaHash
|
|
486
|
-
!compilePluginStateSchema(sourceDefinition.schema)(record.checkpoint.data))
|
|
484
|
+
digest(sourceDefinition.schema) !== record.checkpoint.schemaHash)
|
|
487
485
|
throw error('PLUGIN_TASK_RECOVERY_UNAVAILABLE', 'Checkpoint does not match the retained recovery declaration');
|
|
488
486
|
let checkpoint = structuredClone(record.checkpoint);
|
|
489
487
|
let declarationHash = pluginRecoverySignature(sourceDefinition);
|
|
@@ -542,8 +540,6 @@ export class PluginTaskManager {
|
|
|
542
540
|
data: JSON.parse(json),
|
|
543
541
|
};
|
|
544
542
|
}
|
|
545
|
-
if (!compilePluginStateSchema(target.schema)(checkpoint.data))
|
|
546
|
-
throw error('PLUGIN_TASK_CHECKPOINT_INVALID', 'Migrated checkpoint does not match the target schema');
|
|
547
543
|
}
|
|
548
544
|
signal.throwIfAborted();
|
|
549
545
|
// Admission is synchronous after both immutable snapshots and execution ownership are verified.
|
|
@@ -809,8 +805,8 @@ export class PluginTaskManager {
|
|
|
809
805
|
if (declaration &&
|
|
810
806
|
payload.expectedDeclaration !== pluginRecoverySignature(declaration))
|
|
811
807
|
throw error('PLUGIN_RECOVERY_DECLARATION_CHANGED', 'Checkpoint declaration differs from the executing command');
|
|
812
|
-
if (!declaration
|
|
813
|
-
throw error('PLUGIN_TASK_CHECKPOINT_INVALID', 'Checkpoint is undeclared
|
|
808
|
+
if (!declaration)
|
|
809
|
+
throw error('PLUGIN_TASK_CHECKPOINT_INVALID', 'Checkpoint is undeclared');
|
|
814
810
|
const current = this.store.read(taskId);
|
|
815
811
|
if (current.task.state !== 'running' ||
|
|
816
812
|
payload.expectedSequence !== (current.checkpoint?.sequence ?? 0))
|
|
@@ -72,7 +72,7 @@ function missingPluginEntryMessage(entry, pluginName) {
|
|
|
72
72
|
`插件:${name}`,
|
|
73
73
|
`入口:${entry}`,
|
|
74
74
|
'',
|
|
75
|
-
`卸载命令:npx
|
|
75
|
+
`卸载命令:npx @mearl/setup plugin uninstall ${name}`,
|
|
76
76
|
].join('\n');
|
|
77
77
|
}
|
|
78
78
|
const registrationRefreshes = new Map();
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { CODEX_MODEL_CAPABILITY } from '@mearl/agent-runtime/capabilities';
|
|
2
|
-
import { compilePluginStateSchema } from './pluginStateDefinition.js';
|
|
3
2
|
export function pluginModelCapabilities() {
|
|
4
3
|
return { providers: [CODEX_MODEL_CAPABILITY] };
|
|
5
4
|
}
|
|
@@ -44,7 +43,6 @@ export function normalizePluginModel(value) {
|
|
|
44
43
|
request.outputSchema.$async ||
|
|
45
44
|
Buffer.byteLength(JSON.stringify(request.outputSchema)) > 64 * 1024)
|
|
46
45
|
throw new Error('Invalid model output schema');
|
|
47
|
-
compilePluginStateSchema(request.outputSchema);
|
|
48
46
|
}
|
|
49
47
|
return JSON.parse(JSON.stringify(request));
|
|
50
48
|
}
|
|
@@ -65,10 +63,6 @@ export function validatePluginModelResult(request, value) {
|
|
|
65
63
|
code: 'PLUGIN_MODEL_SCHEMA_INVALID',
|
|
66
64
|
});
|
|
67
65
|
}
|
|
68
|
-
if (!compilePluginStateSchema(request.outputSchema)(output))
|
|
69
|
-
throw Object.assign(new Error('Model result does not match the output schema'), {
|
|
70
|
-
code: 'PLUGIN_MODEL_SCHEMA_INVALID',
|
|
71
|
-
});
|
|
72
66
|
return { ...result, output };
|
|
73
67
|
}
|
|
74
68
|
return result;
|
|
@@ -1,3 +1,2 @@
|
|
|
1
1
|
import type { PluginPageRuntime } from '@mearl/browser-core';
|
|
2
2
|
export declare function normalizePluginPageRuntime(value: unknown): PluginPageRuntime | undefined;
|
|
3
|
-
export declare function compilePluginPageSchema(schema: Record<string, unknown>): import("ajv/dist/core.js").AnyValidateFunction<unknown>;
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Ajv } from 'ajv';
|
|
1
|
+
import { Script } from 'node:vm';
|
|
3
2
|
import { isPluginPageMatchPattern } from '@mearl/browser-core';
|
|
4
|
-
const validators = new WeakMap();
|
|
5
3
|
export function normalizePluginPageRuntime(value) {
|
|
6
4
|
if (value === undefined)
|
|
7
5
|
return undefined;
|
|
@@ -16,12 +14,7 @@ export function normalizePluginPageRuntime(value) {
|
|
|
16
14
|
Buffer.byteLength(runtime.code) > 384 * 1024 ||
|
|
17
15
|
Buffer.byteLength(JSON.stringify(runtime)) > 448 * 1024)
|
|
18
16
|
throw new Error('Invalid plugin pageRuntime declaration');
|
|
19
|
-
|
|
20
|
-
const statement = program.body[0];
|
|
21
|
-
if (program.body.length !== 1 ||
|
|
22
|
-
statement.type !== 'ExpressionStatement' ||
|
|
23
|
-
!['ArrowFunctionExpression', 'FunctionExpression'].includes(statement.expression?.type ?? ''))
|
|
24
|
-
throw new Error('pageRuntime.code must be a self-contained function expression');
|
|
17
|
+
new Script(`(${runtime.code}\n)`, { filename: 'plugin-page-runtime.js' });
|
|
25
18
|
if (runtime.events !== undefined &&
|
|
26
19
|
(!runtime.events ||
|
|
27
20
|
typeof runtime.events !== 'object' ||
|
|
@@ -46,17 +39,8 @@ export function normalizePluginPageRuntime(value) {
|
|
|
46
39
|
event.maxPending > 64)) ||
|
|
47
40
|
event.schema.$async)
|
|
48
41
|
throw new Error(`Invalid page event binding: ${name}`);
|
|
49
|
-
compilePluginPageSchema(event.schema);
|
|
50
42
|
}
|
|
51
43
|
if (runtime.initialize !== undefined && !/^[a-z][a-z0-9-]*$/.test(runtime.initialize))
|
|
52
44
|
throw new Error('Invalid page runtime initialize command');
|
|
53
45
|
return JSON.parse(JSON.stringify(runtime));
|
|
54
46
|
}
|
|
55
|
-
export function compilePluginPageSchema(schema) {
|
|
56
|
-
let validate = validators.get(schema);
|
|
57
|
-
if (!validate) {
|
|
58
|
-
validate = new Ajv({ strict: true, allErrors: false }).compile(schema);
|
|
59
|
-
validators.set(schema, validate);
|
|
60
|
-
}
|
|
61
|
-
return validate;
|
|
62
|
-
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { compilePluginStateSchema } from './pluginStateDefinition.js';
|
|
3
2
|
export function normalizePluginTaskRecovery(value) {
|
|
4
3
|
if (value === undefined)
|
|
5
4
|
return;
|
|
@@ -22,7 +21,6 @@ export function normalizePluginTaskRecovery(value) {
|
|
|
22
21
|
Array.isArray(definition.schema) ||
|
|
23
22
|
definition.schema.$async)
|
|
24
23
|
throw new Error('Invalid task checkpoint definition');
|
|
25
|
-
compilePluginStateSchema(definition.schema);
|
|
26
24
|
if (definition.migrateFrom !== undefined) {
|
|
27
25
|
if (!Array.isArray(definition.migrateFrom) ||
|
|
28
26
|
definition.migrateFrom.length < 1 ||
|
|
@@ -40,7 +38,6 @@ export function normalizePluginTaskRecovery(value) {
|
|
|
40
38
|
Array.isArray(source.schema) ||
|
|
41
39
|
source.schema.$async)
|
|
42
40
|
throw new Error('Checkpoint migrations require distinct older versions and schemas');
|
|
43
|
-
compilePluginStateSchema(source.schema);
|
|
44
41
|
versions.add(source.version);
|
|
45
42
|
}
|
|
46
43
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
1
|
import type { PluginStateDeclaration } from '@mearl/browser-core';
|
|
2
|
-
export declare function compilePluginStateSchema(schema: Record<string, unknown>): import("ajv/dist/core.js").AnyValidateFunction<unknown>;
|
|
3
2
|
export declare function normalizePluginState(value: unknown): PluginStateDeclaration | undefined;
|
|
4
3
|
export declare function pluginStateSignature(collection: import('@mearl/browser-core').PluginStateCollection): string;
|
|
@@ -1,14 +1,4 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { Ajv } from 'ajv';
|
|
3
|
-
const validators = new WeakMap();
|
|
4
|
-
export function compilePluginStateSchema(schema) {
|
|
5
|
-
let validate = validators.get(schema);
|
|
6
|
-
if (!validate) {
|
|
7
|
-
validate = new Ajv({ strict: true }).compile(schema);
|
|
8
|
-
validators.set(schema, validate);
|
|
9
|
-
}
|
|
10
|
-
return validate;
|
|
11
|
-
}
|
|
12
2
|
export function normalizePluginState(value) {
|
|
13
3
|
if (value === undefined)
|
|
14
4
|
return;
|
|
@@ -55,7 +45,6 @@ export function normalizePluginState(value) {
|
|
|
55
45
|
throw new Error('State TTL must be between 1 ms and 7 days');
|
|
56
46
|
if (collection.storage === 'persistent' && collection.ttlMs !== undefined)
|
|
57
47
|
throw new Error('Persistent state does not accept TTL');
|
|
58
|
-
compilePluginStateSchema(collection.schema);
|
|
59
48
|
}
|
|
60
49
|
return declaration;
|
|
61
50
|
}
|
package/dist/runtimePaths.d.ts
CHANGED
package/dist/runtimePaths.js
CHANGED
|
@@ -5,4 +5,5 @@ const require = createRequire(import.meta.url);
|
|
|
5
5
|
export const CLIENT_VERSION = require('../package.json').version;
|
|
6
6
|
export const PLUGIN_TASK_HOST_PATH = fileURLToPath(new URL('./plugin-task-host.js', import.meta.url));
|
|
7
7
|
export const PLUGIN_WORKER_PATH = fileURLToPath(new URL('./plugin-worker.js', import.meta.url));
|
|
8
|
+
export const SCRIPT_WORKER_PATH = fileURLToPath(new URL('./scriptWorker.js', import.meta.url));
|
|
8
9
|
export const PLUGIN_MODEL_SESSION_WORKER_PATH = fileURLToPath(new URL('./pluginModelSessionWorker.js', import.meta.url));
|
package/dist/script.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type UnifiedInvokeOptions } from './unified.js';
|
|
2
|
+
export interface ScriptExecutionOptions extends UnifiedInvokeOptions {
|
|
3
|
+
serverUrl?: string;
|
|
4
|
+
/** Overall deadline, independent of individual action timeouts. Default: 60 seconds. */
|
|
5
|
+
executionTimeoutSec?: number;
|
|
6
|
+
/** Combined stdout/stderr limit in bytes. Default: 256 KiB; maximum: 1 MiB. */
|
|
7
|
+
maxOutputBytes?: number;
|
|
8
|
+
signal?: AbortSignal;
|
|
9
|
+
source?: 'cli' | 'mcp' | 'sdk';
|
|
10
|
+
}
|
|
11
|
+
export interface ScriptExecutionResult {
|
|
12
|
+
success: boolean;
|
|
13
|
+
stdout: string;
|
|
14
|
+
stderr: string;
|
|
15
|
+
session?: Record<string, unknown>;
|
|
16
|
+
error?: {
|
|
17
|
+
code: string;
|
|
18
|
+
message: string;
|
|
19
|
+
stack?: string;
|
|
20
|
+
details?: unknown;
|
|
21
|
+
stage?: string;
|
|
22
|
+
attempt?: unknown;
|
|
23
|
+
};
|
|
24
|
+
executionStopped: boolean;
|
|
25
|
+
/** Dispatched browser/plugin calls can finish after the script has stopped. */
|
|
26
|
+
mayHaveLateEffects: boolean;
|
|
27
|
+
pendingActions: string[];
|
|
28
|
+
}
|
|
29
|
+
/** Runs trusted agent code in a short-lived Node process, with one shared SDK client. */
|
|
30
|
+
export declare function executeScript(code: string, options?: ScriptExecutionOptions): Promise<ScriptExecutionResult>;
|
package/dist/script.js
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { isBrowserCommandAction } from '@mearl/browser-core/browserActionProtocol';
|
|
3
|
+
import { exportArtifact } from './artifacts.js';
|
|
4
|
+
import { UnifiedClient } from './unified.js';
|
|
5
|
+
import { loadCloudServerConfig } from './unifiedConfig.js';
|
|
6
|
+
import { stopPluginProcessTree } from './plugins/runtime/pluginProcessTree.js';
|
|
7
|
+
import { SCRIPT_WORKER_PATH } from './runtimePaths.js';
|
|
8
|
+
import { SDK_METHOD_ACTIONS as METHOD_ACTIONS, SDK_OPTIONS_ONLY_METHODS as OPTIONS_ONLY, } from './sdkMethods.js';
|
|
9
|
+
import { getCommandHelp } from './commandHelp.js';
|
|
10
|
+
import { executeRegisteredPlugin } from './plugins/runtime/pluginExecutor.js';
|
|
11
|
+
import { findPluginRegistration } from './plugins/pluginRegistry.js';
|
|
12
|
+
import { findOfficialPlugin } from './plugins/officialPluginCatalog.js';
|
|
13
|
+
import { ensureOfficialPluginRegistered } from './plugins/officialPluginInstaller.js';
|
|
14
|
+
import { serializeScriptError as serializeError } from './scriptErrors.js';
|
|
15
|
+
const METHODS = [...METHOD_ACTIONS.keys(), 'invoke', 'invokePlugin', 'saveArtifact', 'help'];
|
|
16
|
+
const MAX_SCRIPT_BYTES = 1024 * 1024;
|
|
17
|
+
/** Runs trusted agent code in a short-lived Node process, with one shared SDK client. */
|
|
18
|
+
export async function executeScript(code, options = {}) {
|
|
19
|
+
if (typeof code !== 'string' || !code.trim() || Buffer.byteLength(code) > MAX_SCRIPT_BYTES)
|
|
20
|
+
throw new Error('Script must be non-empty JavaScript of at most 1 MiB');
|
|
21
|
+
const { serverUrl, executionTimeoutSec = 60, maxOutputBytes = 256 * 1024, signal, source = 'sdk', ...defaults } = options;
|
|
22
|
+
if (!Number.isFinite(executionTimeoutSec) ||
|
|
23
|
+
executionTimeoutSec <= 0 ||
|
|
24
|
+
executionTimeoutSec * 1000 > 2_147_483_647)
|
|
25
|
+
throw new Error('executionTimeoutSec must be positive and within the timer range');
|
|
26
|
+
if (!Number.isSafeInteger(maxOutputBytes) || maxOutputBytes < 1 || maxOutputBytes > 1024 * 1024)
|
|
27
|
+
throw new Error('maxOutputBytes must be an integer between 1 and 1048576');
|
|
28
|
+
const client = new UnifiedClient({
|
|
29
|
+
serverUrl: serverUrl ?? process.env.MEARL_SERVER_URL ?? loadCloudServerConfig()?.server,
|
|
30
|
+
});
|
|
31
|
+
let session = defaults.session && defaults.session !== 'new' ? { sessionId: defaults.session } : undefined;
|
|
32
|
+
let sessionId = defaults.session;
|
|
33
|
+
let uncertainEffects = false;
|
|
34
|
+
const pending = new Map();
|
|
35
|
+
const controller = new AbortController();
|
|
36
|
+
const invoke = async (action, payload, overrides = {}) => {
|
|
37
|
+
controller.signal.throwIfAborted();
|
|
38
|
+
const result = await client.invoke(action, payload, {
|
|
39
|
+
...defaults,
|
|
40
|
+
session: sessionId,
|
|
41
|
+
...overrides,
|
|
42
|
+
});
|
|
43
|
+
if (result?.action?.stage === 'dispatch-uncertain' || result?.stage === 'dispatch-uncertain')
|
|
44
|
+
uncertainEffects = true;
|
|
45
|
+
if (result?.session?.sessionId &&
|
|
46
|
+
(!overrides.session || overrides.session === 'new' || overrides.session === sessionId)) {
|
|
47
|
+
session = result.session;
|
|
48
|
+
if (!overrides.session || overrides.session === 'new')
|
|
49
|
+
sessionId = result.session.sessionId;
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
};
|
|
53
|
+
try {
|
|
54
|
+
return await new Promise((resolve, reject) => {
|
|
55
|
+
if (signal?.aborted) {
|
|
56
|
+
resolve({
|
|
57
|
+
success: false,
|
|
58
|
+
stdout: '',
|
|
59
|
+
stderr: '',
|
|
60
|
+
...(session ? { session } : {}),
|
|
61
|
+
error: { code: 'SCRIPT_CANCELLED', message: 'Script execution cancelled' },
|
|
62
|
+
executionStopped: true,
|
|
63
|
+
mayHaveLateEffects: false,
|
|
64
|
+
pendingActions: [],
|
|
65
|
+
});
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const child = spawn(process.execPath, [SCRIPT_WORKER_PATH], {
|
|
69
|
+
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
|
|
70
|
+
serialization: 'advanced',
|
|
71
|
+
detached: process.platform !== 'win32',
|
|
72
|
+
});
|
|
73
|
+
const output = { stdout: [], stderr: [] };
|
|
74
|
+
let outputBytes = 0;
|
|
75
|
+
let outcome;
|
|
76
|
+
let cleanup;
|
|
77
|
+
let cleanupError;
|
|
78
|
+
let pendingActions = [];
|
|
79
|
+
let closed = false;
|
|
80
|
+
const pluginCalls = new Set();
|
|
81
|
+
const stop = (error) => {
|
|
82
|
+
if (outcome !== undefined || closed)
|
|
83
|
+
return;
|
|
84
|
+
outcome = error ?? true;
|
|
85
|
+
pendingActions = [...pending.values()];
|
|
86
|
+
controller.abort();
|
|
87
|
+
client.disconnect();
|
|
88
|
+
if (child.pid) {
|
|
89
|
+
cleanup = stopPluginProcessTree(child.pid).catch(error => {
|
|
90
|
+
cleanupError = error;
|
|
91
|
+
child.kill('SIGKILL');
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
const abort = () => stop({ code: 'SCRIPT_CANCELLED', message: 'Script execution cancelled' });
|
|
96
|
+
const deadline = setTimeout(() => stop({ code: 'SCRIPT_TIMEOUT', message: 'Script execution deadline exceeded' }), executionTimeoutSec * 1000);
|
|
97
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
98
|
+
if (signal?.aborted)
|
|
99
|
+
abort();
|
|
100
|
+
const appendOutput = (stream, chunk) => {
|
|
101
|
+
const remaining = maxOutputBytes - outputBytes;
|
|
102
|
+
output[stream].push(chunk.subarray(0, Math.max(0, remaining)));
|
|
103
|
+
outputBytes += Math.min(chunk.length, Math.max(0, remaining));
|
|
104
|
+
if (chunk.length > remaining)
|
|
105
|
+
stop({
|
|
106
|
+
code: 'SCRIPT_OUTPUT_LIMIT',
|
|
107
|
+
message: `Script output exceeded ${maxOutputBytes} bytes`,
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
for (const stream of ['stdout', 'stderr']) {
|
|
111
|
+
child[stream].on('data', (chunk) => appendOutput(stream, chunk));
|
|
112
|
+
}
|
|
113
|
+
const runPlugin = async (name, invocation, overrides = {}, help) => {
|
|
114
|
+
if (!findPluginRegistration(name) && findOfficialPlugin(name))
|
|
115
|
+
await ensureOfficialPluginRegistered(name);
|
|
116
|
+
controller.signal.throwIfAborted();
|
|
117
|
+
const pluginOptions = { ...defaults, session: sessionId, ...overrides };
|
|
118
|
+
if (pluginOptions.session === 'new' && !help)
|
|
119
|
+
throw new Error('Create the control session with a browser action before calling a Skill command');
|
|
120
|
+
const operation = executeRegisteredPlugin(name, invocation, {
|
|
121
|
+
invoke: (action, payload = {}, options) => invoke(action, payload, options),
|
|
122
|
+
}, {
|
|
123
|
+
...pluginOptions,
|
|
124
|
+
source,
|
|
125
|
+
signal: controller.signal,
|
|
126
|
+
cancellationGraceMs: 50,
|
|
127
|
+
onOutput: appendOutput,
|
|
128
|
+
...(help ? { help } : {}),
|
|
129
|
+
});
|
|
130
|
+
pluginCalls.add(operation);
|
|
131
|
+
try {
|
|
132
|
+
return await operation;
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
pluginCalls.delete(operation);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
const send = (message) => {
|
|
139
|
+
if (outcome !== undefined || !child.connected)
|
|
140
|
+
return;
|
|
141
|
+
child.send(message, error => {
|
|
142
|
+
if (error)
|
|
143
|
+
stop(serializeError(error));
|
|
144
|
+
});
|
|
145
|
+
};
|
|
146
|
+
child.on('message', (message) => {
|
|
147
|
+
if (outcome !== undefined || !message || typeof message !== 'object')
|
|
148
|
+
return;
|
|
149
|
+
if (message.type === 'done' || message.type === 'error') {
|
|
150
|
+
stop(message.type === 'error'
|
|
151
|
+
? serializeError(message.error)
|
|
152
|
+
: pending.size
|
|
153
|
+
? {
|
|
154
|
+
code: 'SCRIPT_PENDING_CALLS',
|
|
155
|
+
message: 'Script finished with unawaited SDK calls',
|
|
156
|
+
}
|
|
157
|
+
: undefined);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (message.type !== 'call')
|
|
161
|
+
return;
|
|
162
|
+
const { id, method, args } = message;
|
|
163
|
+
if (!Number.isSafeInteger(id) ||
|
|
164
|
+
pending.has(id) ||
|
|
165
|
+
!METHODS.includes(method) ||
|
|
166
|
+
!Array.isArray(args)) {
|
|
167
|
+
stop({ code: 'SCRIPT_PROTOCOL_ERROR', message: 'Invalid script SDK call' });
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (method !== 'help' && sessionId === 'new' && pending.size) {
|
|
171
|
+
send({
|
|
172
|
+
type: 'response',
|
|
173
|
+
id,
|
|
174
|
+
error: {
|
|
175
|
+
code: 'SCRIPT_SESSION_INITIALIZING',
|
|
176
|
+
message: 'Await the first session call before starting concurrent calls',
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
pending.set(id, method === 'invoke' || method === 'invokePlugin' ? String(args[0]) : method);
|
|
182
|
+
void Promise.resolve()
|
|
183
|
+
.then(async () => {
|
|
184
|
+
if (outcome !== undefined)
|
|
185
|
+
throw new Error('Script execution stopped');
|
|
186
|
+
if (method === 'help') {
|
|
187
|
+
const target = args[0];
|
|
188
|
+
if (target && typeof target === 'object') {
|
|
189
|
+
if (typeof target.plugin !== 'string' ||
|
|
190
|
+
!target.plugin.trim() ||
|
|
191
|
+
(target.command !== undefined && typeof target.command !== 'string'))
|
|
192
|
+
throw new Error('Plugin help requires { plugin, command? }');
|
|
193
|
+
return runPlugin(target.plugin, { command: '', input: {} }, {}, { namespace: `mearl ${target.plugin}`, command: target.command });
|
|
194
|
+
}
|
|
195
|
+
return getCommandHelp(target);
|
|
196
|
+
}
|
|
197
|
+
if (method === 'invoke') {
|
|
198
|
+
if (!isBrowserCommandAction(args[0]) && args[0] !== 'browser_select_browser')
|
|
199
|
+
throw new Error(`Unsupported browser action: ${args[0]}; use invokePlugin for registered Skill commands`);
|
|
200
|
+
return invoke(args[0], args[1] ?? {}, args[2]);
|
|
201
|
+
}
|
|
202
|
+
if (method === 'invokePlugin') {
|
|
203
|
+
return runPlugin(args[0], args[1], args[2]);
|
|
204
|
+
}
|
|
205
|
+
if (method === 'saveArtifact')
|
|
206
|
+
return exportArtifact({ invoke }, args[0], args[1], {
|
|
207
|
+
...defaults,
|
|
208
|
+
session: sessionId,
|
|
209
|
+
...args[2],
|
|
210
|
+
});
|
|
211
|
+
const optionsOnly = OPTIONS_ONLY.has(method) || (method === 'browserRelease' && args.length < 2);
|
|
212
|
+
return invoke(METHOD_ACTIONS.get(method), optionsOnly ? {} : (args[0] ?? {}), optionsOnly ? args[0] : args[1]);
|
|
213
|
+
})
|
|
214
|
+
.then(result => send({ type: 'response', id, result, session }), error => send({ type: 'response', id, error: serializeError(error), session }))
|
|
215
|
+
.finally(() => pending.delete(id));
|
|
216
|
+
});
|
|
217
|
+
child.once('error', error => {
|
|
218
|
+
stop(serializeError(error));
|
|
219
|
+
});
|
|
220
|
+
child.once('exit', () => {
|
|
221
|
+
if (outcome === undefined)
|
|
222
|
+
stop({
|
|
223
|
+
code: 'SCRIPT_EXITED',
|
|
224
|
+
message: 'Script process exited before reporting completion',
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
child.once('close', () => {
|
|
228
|
+
closed = true;
|
|
229
|
+
clearTimeout(deadline);
|
|
230
|
+
signal?.removeEventListener('abort', abort);
|
|
231
|
+
void Promise.allSettled([cleanup, ...pluginCalls])
|
|
232
|
+
.then(outcomes => {
|
|
233
|
+
if (outcomes.some(result => result.status === 'rejected' &&
|
|
234
|
+
result.reason?.code === 'PLUGIN_EXECUTOR_CLEANUP_FAILED'))
|
|
235
|
+
cleanupError = new Error('Unable to confirm Skill process cleanup');
|
|
236
|
+
const error = cleanupError
|
|
237
|
+
? serializeError(cleanupError, 'SCRIPT_CLEANUP_FAILED')
|
|
238
|
+
: outcome === true
|
|
239
|
+
? undefined
|
|
240
|
+
: outcome;
|
|
241
|
+
resolve({
|
|
242
|
+
success: !error && outcome === true,
|
|
243
|
+
stdout: Buffer.concat(output.stdout).toString('utf8'),
|
|
244
|
+
stderr: Buffer.concat(output.stderr).toString('utf8'),
|
|
245
|
+
...(session ? { session } : {}),
|
|
246
|
+
...(error ? { error } : {}),
|
|
247
|
+
executionStopped: !cleanupError,
|
|
248
|
+
mayHaveLateEffects: pendingActions.length > 0 || uncertainEffects,
|
|
249
|
+
pendingActions,
|
|
250
|
+
});
|
|
251
|
+
})
|
|
252
|
+
.catch(reject);
|
|
253
|
+
});
|
|
254
|
+
send({ type: 'start', code, methods: METHODS, session });
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
finally {
|
|
258
|
+
client.disconnect();
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runScriptCli(args: string[], commandName: string): Promise<number>;
|