@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,21 @@
1
+ import type { PluginHostConnection } from './pluginHostConnection.js';
2
+ import type { UnifiedClientOptions, UnifiedInvokeOptions } from './unified.js';
3
+ import type { PluginPageInstance, PluginPageEvents, PluginPageOperation } from './generated-plugin-pages.js';
4
+ export type { PluginPageInstance, PluginPageEvents, PluginPageRuntime, PluginPageActivation, PluginPageTarget, PluginPageOperation, } from './generated-plugin-pages.js';
5
+ export declare class PluginPageClient {
6
+ private readonly connection?;
7
+ private readonly client?;
8
+ constructor(configuration?: UnifiedClientOptions, connection?: PluginHostConnection | undefined);
9
+ call(plugin: string, operation: PluginPageOperation, input?: Record<string, unknown>, options?: UnifiedInvokeOptions): Promise<any>;
10
+ attach(plugin: string, input: Record<string, unknown>, options?: UnifiedInvokeOptions): Promise<PluginPageInstance>;
11
+ get(plugin: string, instanceId: string): Promise<PluginPageInstance>;
12
+ list(plugin: string): Promise<{
13
+ instances: PluginPageInstance[];
14
+ }>;
15
+ send(plugin: string, instanceId: string, message: unknown, options?: {
16
+ timeoutSec?: number;
17
+ }): Promise<unknown>;
18
+ detach(plugin: string, instanceId: string): Promise<PluginPageInstance>;
19
+ events(plugin: string, instanceId: string, cursor?: number, waitMs?: number): Promise<PluginPageEvents>;
20
+ disconnect(): void;
21
+ }
@@ -0,0 +1,46 @@
1
+ import { UnifiedClient } from './unified.js';
2
+ export class PluginPageClient {
3
+ connection;
4
+ client;
5
+ constructor(configuration = {}, connection) {
6
+ this.connection = connection;
7
+ if (!connection)
8
+ this.client = new UnifiedClient(configuration);
9
+ }
10
+ call(plugin, operation, input = {}, options = {}) {
11
+ if (this.connection)
12
+ return this.connection.request('page.call', {
13
+ plugin,
14
+ input: { ...input, operation },
15
+ options,
16
+ execution: {
17
+ cwd: process.cwd(),
18
+ environment: Object.fromEntries(Object.entries(process.env).filter(entry => entry[1] !== undefined)),
19
+ },
20
+ }, Math.max(120000, typeof input.timeoutSec === 'number' && Number.isFinite(input.timeoutSec)
21
+ ? (Math.min(300, input.timeoutSec) + 5) * 1000
22
+ : 0));
23
+ return this.client.invoke('plugin_page', { ...input, plugin, operation }, options);
24
+ }
25
+ attach(plugin, input, options = {}) {
26
+ return this.call(plugin, 'attach', input, options);
27
+ }
28
+ get(plugin, instanceId) {
29
+ return this.call(plugin, 'get', { instanceId });
30
+ }
31
+ list(plugin) {
32
+ return this.call(plugin, 'list');
33
+ }
34
+ send(plugin, instanceId, message, options = {}) {
35
+ return this.call(plugin, 'send', { instanceId, message, ...options }, options);
36
+ }
37
+ detach(plugin, instanceId) {
38
+ return this.call(plugin, 'detach', { instanceId });
39
+ }
40
+ events(plugin, instanceId, cursor = 0, waitMs = 0) {
41
+ return this.call(plugin, 'events', { instanceId, cursor, waitMs });
42
+ }
43
+ disconnect() {
44
+ this.client?.disconnect();
45
+ }
46
+ }
@@ -0,0 +1,2 @@
1
+ /** Resolves after the owned process group can no longer execute. */
2
+ export declare function stopPluginProcessTree(pid: number): Promise<void>;
@@ -0,0 +1,31 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { isProcessGroupAlive } from '@mearl/daemon-core';
3
+ /** Resolves after the owned process group can no longer execute. */
4
+ export async function stopPluginProcessTree(pid) {
5
+ if (!Number.isSafeInteger(pid) || pid < 1)
6
+ throw new Error('Invalid plugin process group');
7
+ if (process.platform === 'win32') {
8
+ return new Promise((resolve, reject) => {
9
+ execFile('taskkill', ['/PID', String(pid), '/T', '/F'], { windowsHide: true, timeout: 5000 }, error => {
10
+ if (error)
11
+ reject(error);
12
+ else
13
+ resolve();
14
+ });
15
+ });
16
+ }
17
+ try {
18
+ process.kill(-pid, 'SIGKILL');
19
+ }
20
+ catch (error) {
21
+ if (error.code === 'ESRCH')
22
+ return;
23
+ throw error;
24
+ }
25
+ const deadline = Date.now() + 5000;
26
+ while (isProcessGroupAlive(pid)) {
27
+ if (Date.now() >= deadline)
28
+ throw new Error('Unable to confirm plugin process group stopped');
29
+ await new Promise(resolve => setTimeout(resolve, 20));
30
+ }
31
+ }
@@ -0,0 +1,3 @@
1
+ import type { PluginTaskRecoveryDeclaration } from './generated-plugin-recovery.js';
2
+ export declare function normalizePluginTaskRecovery(value: unknown): PluginTaskRecoveryDeclaration | undefined;
3
+ export declare function pluginRecoverySignature(value: unknown): string;
@@ -0,0 +1,56 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { compilePluginStateSchema } from './pluginStateDefinition.js';
3
+ export function normalizePluginTaskRecovery(value) {
4
+ if (value === undefined)
5
+ return;
6
+ const declaration = JSON.parse(JSON.stringify(value));
7
+ if (!declaration ||
8
+ declaration.version !== 1 ||
9
+ !declaration.commands ||
10
+ typeof declaration.commands !== 'object' ||
11
+ Array.isArray(declaration.commands) ||
12
+ Object.keys(declaration.commands).length > 32 ||
13
+ Buffer.byteLength(JSON.stringify(declaration)) > 128 * 1024)
14
+ throw new Error('Invalid taskRecovery declaration');
15
+ for (const [command, definition] of Object.entries(declaration.commands)) {
16
+ if (!/^[a-z][a-z0-9-]*$/.test(command) ||
17
+ !definition ||
18
+ !Number.isSafeInteger(definition.checkpointVersion) ||
19
+ definition.checkpointVersion < 1 ||
20
+ !definition.schema ||
21
+ typeof definition.schema !== 'object' ||
22
+ Array.isArray(definition.schema) ||
23
+ definition.schema.$async)
24
+ throw new Error('Invalid task checkpoint definition');
25
+ compilePluginStateSchema(definition.schema);
26
+ if (definition.migrateFrom !== undefined) {
27
+ if (!Array.isArray(definition.migrateFrom) ||
28
+ definition.migrateFrom.length < 1 ||
29
+ definition.migrateFrom.length > 16)
30
+ throw new Error('Invalid checkpoint migration sources');
31
+ const versions = new Set();
32
+ for (const source of definition.migrateFrom) {
33
+ if (!source ||
34
+ !Number.isSafeInteger(source.version) ||
35
+ source.version < 1 ||
36
+ source.version >= definition.checkpointVersion ||
37
+ versions.has(source.version) ||
38
+ !source.schema ||
39
+ typeof source.schema !== 'object' ||
40
+ Array.isArray(source.schema) ||
41
+ source.schema.$async)
42
+ throw new Error('Checkpoint migrations require distinct older versions and schemas');
43
+ compilePluginStateSchema(source.schema);
44
+ versions.add(source.version);
45
+ }
46
+ }
47
+ }
48
+ return declaration;
49
+ }
50
+ export function pluginRecoverySignature(value) {
51
+ return createHash('sha256')
52
+ .update(JSON.stringify(value, (_key, item) => item && typeof item === 'object' && !Array.isArray(item)
53
+ ? Object.fromEntries(Object.entries(item).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)))
54
+ : item))
55
+ .digest('hex');
56
+ }
@@ -8,9 +8,6 @@ export interface PluginToolboxSandboxPage {
8
8
  runtime?: 'preact';
9
9
  entry: string;
10
10
  commands: string[];
11
- lifecycle?: {
12
- onNavigation: string;
13
- };
14
11
  }
15
12
  export interface PluginToolboxLocale {
16
13
  title?: string;
@@ -38,6 +35,7 @@ export interface PluginPageBundleRegistration {
38
35
  }
39
36
  export interface PluginRegistration {
40
37
  entry: string;
38
+ hasPublicCommands?: boolean;
41
39
  description?: string;
42
40
  managedBy?: string;
43
41
  toolbox?: PluginToolboxContribution;
@@ -52,6 +50,7 @@ export interface PluginRegistration {
52
50
  }
53
51
  export declare function pluginRegistrationNeedsRefresh(registration: PluginRegistration, includePage?: boolean): boolean;
54
52
  export interface PluginRegistrationOptions {
53
+ hasPublicCommands?: boolean;
55
54
  managedBy?: string;
56
55
  toolbox?: PluginToolboxContribution;
57
56
  appProfiles?: AppProfileContribution;
@@ -1,5 +1,6 @@
1
1
  import { validateAppProfileContribution, } from './generated-app-profile-definition.js';
2
2
  import { randomUUID } from 'node:crypto';
3
+ import { parseToolboxIcon } from './generated-toolbox-icon.js';
3
4
  import { closeSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, } from 'node:fs';
4
5
  import { homedir } from 'node:os';
5
6
  import { isAbsolute, join, resolve } from 'node:path';
@@ -134,8 +135,8 @@ export function normalizePluginToolboxContribution(value) {
134
135
  if (description && description.length > 240) {
135
136
  throw new Error('toolbox.description 不能超过 240 个字符');
136
137
  }
137
- if (icon && (!/^[a-z][a-z0-9-]*$/.test(icon) || icon.length > 40)) {
138
- throw new Error('toolbox.icon 必须是短横线命名的图标名称');
138
+ if (icon && !parseToolboxIcon(icon)) {
139
+ throw new Error('toolbox.icon 必须是图标名称、SVG、图片 Data URL 或 HTTP(S) 图片地址,最多 32768 个字符');
139
140
  }
140
141
  let locales;
141
142
  if (candidate.locales !== undefined) {
@@ -209,17 +210,8 @@ export function normalizePluginToolboxContribution(value) {
209
210
  entry: pageEntry,
210
211
  commands,
211
212
  };
212
- if (pageCandidate.lifecycle !== undefined) {
213
- const lifecycle = pageCandidate.lifecycle;
214
- if (!lifecycle ||
215
- typeof lifecycle !== 'object' ||
216
- Array.isArray(lifecycle) ||
217
- typeof lifecycle.onNavigation !== 'string' ||
218
- !commands.includes(lifecycle.onNavigation)) {
219
- throw new Error('toolbox.page.lifecycle.onNavigation 必须是 page.commands 中的命令名称');
220
- }
221
- page.lifecycle = { onNavigation: lifecycle.onNavigation };
222
- }
213
+ if ('lifecycle' in pageCandidate)
214
+ throw new Error('toolbox.page.lifecycle is unsupported; declare pageRuntime and activation rules');
223
215
  let agent;
224
216
  if (candidate.agent !== undefined) {
225
217
  if (!candidate.agent || typeof candidate.agent !== 'object' || Array.isArray(candidate.agent)) {
@@ -309,6 +301,9 @@ export function registerPlugin(name, target, description, options = {}) {
309
301
  }
310
302
  const record = {
311
303
  entry,
304
+ ...(options.hasPublicCommands !== undefined
305
+ ? { hasPublicCommands: options.hasPublicCommands }
306
+ : {}),
312
307
  source: {
313
308
  entry: entryVersion,
314
309
  ...(options.profileSources ? { profiles: options.profileSources } : {}),
@@ -339,6 +334,7 @@ export function unregisterPlugin(name) {
339
334
  const file = join(pluginDirectory(), `${name}.json`);
340
335
  try {
341
336
  rmSync(file);
337
+ rmSync(join(pluginDirectory(), '.host', 'installations', `${name}.json`), { force: true });
342
338
  removePluginPageBundles(name);
343
339
  return true;
344
340
  }
@@ -365,6 +361,8 @@ export function findPluginRegistration(name) {
365
361
  const record = JSON.parse(content);
366
362
  if (typeof record?.entry !== 'string' || !isAbsolute(record.entry))
367
363
  throw new Error('invalid entry');
364
+ if (record.hasPublicCommands !== undefined && typeof record.hasPublicCommands !== 'boolean')
365
+ throw new Error('invalid public commands');
368
366
  const description = typeof record.description === 'string' && record.description.trim()
369
367
  ? record.description.trim()
370
368
  : undefined;
@@ -396,6 +394,9 @@ export function findPluginRegistration(name) {
396
394
  }
397
395
  return {
398
396
  entry: record.entry,
397
+ ...(record.hasPublicCommands !== undefined
398
+ ? { hasPublicCommands: record.hasPublicCommands }
399
+ : {}),
399
400
  ...(description ? { description } : {}),
400
401
  ...(managedBy ? { managedBy } : {}),
401
402
  ...(toolbox ? { toolbox } : {}),
@@ -0,0 +1,14 @@
1
+ import type { PluginRegistration } from './pluginRegistry.js';
2
+ export interface PluginRevision {
3
+ plugin: string;
4
+ installationId: string;
5
+ revision: string;
6
+ originalRoot: string;
7
+ entry: string;
8
+ registration: PluginRegistration;
9
+ }
10
+ /** Captures relative source and resources before a task is accepted; execution never reads the live entry. */
11
+ export declare function capturePluginRevision(plugin: string): PluginRevision;
12
+ export declare function prunePluginRevisions(references: Set<string>): void;
13
+ /** Recovery uses the retained snapshot after checking its identity and every source byte. */
14
+ export declare function restorePluginRevision(plugin: string, installationId: string, revision: string, entry: string): PluginRevision;
@@ -0,0 +1,227 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync, } from 'node:fs';
3
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
4
+ import { claimRecord, readRecord, writeRecord } from '@mearl/daemon-core';
5
+ import { isBuiltin } from 'node:module';
6
+ import { parse } from 'acorn';
7
+ import { pluginHostPaths } from './pluginHostPaths.js';
8
+ import { findPluginRegistration, PluginError } from './pluginRegistry.js';
9
+ const MAX_REVISION_BYTES = 64 * 1024 * 1024;
10
+ const validatedSources = new Set();
11
+ function within(root, file) {
12
+ const path = relative(root, file);
13
+ return path === '' || (!path.startsWith(`..${sep}`) && path !== '..' && !isAbsolute(path));
14
+ }
15
+ function sourceRoot(entry) {
16
+ let candidate = dirname(entry);
17
+ for (let depth = 0; depth < 6; depth++) {
18
+ if (existsSync(join(candidate, 'SKILL.md')))
19
+ return candidate;
20
+ const parent = dirname(candidate);
21
+ if (parent === candidate)
22
+ break;
23
+ candidate = parent;
24
+ }
25
+ return basename(dirname(entry)) === 'scripts' ? dirname(dirname(entry)) : dirname(entry);
26
+ }
27
+ function readSources(root) {
28
+ const files = [];
29
+ const registry = realpathSync(pluginHostPaths().registry);
30
+ let bytes = 0;
31
+ const visit = (directory, ancestors) => {
32
+ const real = realpathSync(directory);
33
+ if (!within(root, real) || ancestors.has(real))
34
+ throw new Error('Plugin snapshot has an external or cyclic directory link');
35
+ const next = new Set([...ancestors, real]);
36
+ for (const item of readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
37
+ if (['.git', '__pycache__', 'node_modules', '.DS_Store'].includes(item.name))
38
+ continue;
39
+ const file = join(directory, item.name);
40
+ if (within(root, registry) && within(registry, file))
41
+ continue;
42
+ const target = realpathSync(file);
43
+ if (!within(root, target))
44
+ throw new Error(`Plugin snapshot resource leaves the Skill directory: ${file}`);
45
+ const stat = statSync(file);
46
+ if (stat.isDirectory()) {
47
+ visit(file, next);
48
+ continue;
49
+ }
50
+ if (!stat.isFile())
51
+ throw new Error(`Unsupported plugin resource: ${file}`);
52
+ bytes += stat.size;
53
+ if (bytes > MAX_REVISION_BYTES || files.length >= 10000)
54
+ throw new Error('Plugin source snapshot exceeds 64 MiB or 10000 files');
55
+ const content = readFileSync(file);
56
+ bytes += content.length - stat.size;
57
+ if (bytes > MAX_REVISION_BYTES)
58
+ throw new Error('Plugin source snapshot exceeds 64 MiB');
59
+ files.push({
60
+ path: relative(root, file).split(sep).join('/'),
61
+ hash: createHash('sha256').update(content).digest('hex'),
62
+ bytes: content,
63
+ executable: (stat.mode & 0o111) !== 0,
64
+ });
65
+ }
66
+ };
67
+ visit(root, new Set());
68
+ return files;
69
+ }
70
+ function validateDependencies(files) {
71
+ const paths = new Set(files.map(file => file.path));
72
+ for (const file of files) {
73
+ if (!/\.[cm]?js$/.test(file.path))
74
+ continue;
75
+ const tree = parse(file.bytes.toString('utf8'), {
76
+ ecmaVersion: 'latest',
77
+ sourceType: 'module',
78
+ allowReturnOutsideFunction: true,
79
+ });
80
+ const check = (specifier) => {
81
+ if (typeof specifier !== 'string' || isBuiltin(specifier))
82
+ return;
83
+ if (!specifier.startsWith('./') && !specifier.startsWith('../'))
84
+ throw new PluginError('PLUGIN_SOURCE_NOT_SELF_CONTAINED', `后台任务需要自包含 Skill,请先打包依赖: ${file.path}: ${specifier}`);
85
+ const local = resolve('/source', dirname(file.path), specifier);
86
+ const normalized = relative('/source', local).split(sep).join('/');
87
+ if (!within('/source', local) ||
88
+ ![normalized, `${normalized}.js`, `${normalized}.json`, `${normalized}/index.js`].some(path => paths.has(path)))
89
+ throw new PluginError('PLUGIN_SOURCE_NOT_SELF_CONTAINED', `依赖不在 Skill 快照中: ${file.path}: ${specifier}`);
90
+ };
91
+ const visit = (node) => {
92
+ if (!node || typeof node !== 'object')
93
+ return;
94
+ if (['ImportDeclaration', 'ExportNamedDeclaration', 'ExportAllDeclaration'].includes(node.type))
95
+ check(node.source?.value);
96
+ if (node.type === 'ImportExpression')
97
+ check(node.source?.value);
98
+ if (node.type === 'CallExpression' &&
99
+ node.callee?.type === 'Identifier' &&
100
+ node.callee.name === 'require')
101
+ check(node.arguments[0]?.value);
102
+ for (const value of Object.values(node)) {
103
+ if (Array.isArray(value))
104
+ value.forEach(visit);
105
+ else if (value && typeof value === 'object')
106
+ visit(value);
107
+ }
108
+ };
109
+ visit(tree);
110
+ }
111
+ }
112
+ function fingerprint(files, entry) {
113
+ return createHash('sha256')
114
+ .update(JSON.stringify({ entry, files: files.map(file => [file.path, file.hash, file.executable]) }))
115
+ .digest('hex');
116
+ }
117
+ /** Captures relative source and resources before a task is accepted; execution never reads the live entry. */
118
+ export function capturePluginRevision(plugin) {
119
+ const registration = findPluginRegistration(plugin);
120
+ if (!registration)
121
+ throw new PluginError('PLUGIN_NOT_REGISTERED', `插件未注册: ${plugin}`);
122
+ const entry = realpathSync(registration.entry);
123
+ const root = realpathSync(sourceRoot(entry));
124
+ const paths = pluginHostPaths();
125
+ mkdirSync(paths.installations, { recursive: true, mode: 0o700 });
126
+ const identityFile = join(paths.installations, `${plugin}.json`);
127
+ let installation = readRecord(identityFile);
128
+ if (!installation || installation.root !== root) {
129
+ const proposed = { pid: process.pid, root, installationId: randomUUID() };
130
+ if (installation)
131
+ writeRecord(identityFile, proposed);
132
+ else
133
+ claimRecord(identityFile, proposed);
134
+ installation = readRecord(identityFile);
135
+ }
136
+ if (!installation || installation.root !== root)
137
+ throw new Error('Plugin installation changed during capture');
138
+ const relativeEntry = relative(root, entry).split(sep).join('/');
139
+ const files = readSources(root);
140
+ const revision = fingerprint(files, relativeEntry);
141
+ if (!validatedSources.has(revision)) {
142
+ validateDependencies(files);
143
+ if (validatedSources.size >= 64)
144
+ validatedSources.delete(validatedSources.values().next().value);
145
+ validatedSources.add(revision);
146
+ }
147
+ const revisionRoot = join(paths.revisions, installation.installationId, revision);
148
+ if (!existsSync(revisionRoot)) {
149
+ const temporary = `${revisionRoot}.${randomUUID()}.tmp`;
150
+ try {
151
+ mkdirSync(temporary, { recursive: true, mode: 0o700 });
152
+ for (const file of files) {
153
+ const destination = resolve(temporary, file.path);
154
+ mkdirSync(dirname(destination), { recursive: true, mode: 0o700 });
155
+ writeFileSync(destination, file.bytes, {
156
+ flag: 'wx',
157
+ mode: file.executable ? 0o555 : 0o444,
158
+ });
159
+ }
160
+ if (fingerprint(readSources(root), relativeEntry) !== revision)
161
+ throw new PluginError('PLUGIN_SOURCE_CHANGED', '插件源码在创建快照时发生变化');
162
+ mkdirSync(dirname(revisionRoot), { recursive: true, mode: 0o700 });
163
+ renameSync(temporary, revisionRoot);
164
+ }
165
+ finally {
166
+ rmSync(temporary, { recursive: true, force: true });
167
+ }
168
+ }
169
+ return {
170
+ plugin,
171
+ installationId: installation.installationId,
172
+ revision,
173
+ originalRoot: root,
174
+ entry: join(revisionRoot, relativeEntry),
175
+ registration: { ...registration, entry: join(revisionRoot, relativeEntry), source: undefined },
176
+ };
177
+ }
178
+ export function prunePluginRevisions(references) {
179
+ const directory = pluginHostPaths().revisions;
180
+ if (!existsSync(directory))
181
+ return;
182
+ for (const installation of readdirSync(directory, { withFileTypes: true })) {
183
+ if (!installation.isDirectory())
184
+ continue;
185
+ const root = join(directory, installation.name);
186
+ for (const revision of readdirSync(root, { withFileTypes: true })) {
187
+ if (revision.isDirectory() && !references.has(`${installation.name}/${revision.name}`))
188
+ rmSync(join(root, revision.name), { recursive: true, force: true });
189
+ }
190
+ if (!readdirSync(root).length)
191
+ rmSync(root, { recursive: true, force: true });
192
+ }
193
+ }
194
+ /** Recovery uses the retained snapshot after checking its identity and every source byte. */
195
+ export function restorePluginRevision(plugin, installationId, revision, entry) {
196
+ if (!/^[a-f0-9-]{36}$/.test(installationId) || !/^[a-f0-9]{64}$/.test(revision))
197
+ throw new Error('Invalid recovery revision');
198
+ const paths = pluginHostPaths();
199
+ const registration = findPluginRegistration(plugin);
200
+ const installation = readRecord(join(paths.installations, `${plugin}.json`));
201
+ if (!registration ||
202
+ !installation ||
203
+ installation.installationId !== installationId ||
204
+ sourceRoot(realpathSync(registration.entry)) !== installation.root)
205
+ throw new PluginError('PLUGIN_TASK_INSTALLATION_CHANGED', 'Task installation was removed or replaced');
206
+ try {
207
+ const root = realpathSync(join(paths.revisions, installationId, revision));
208
+ const restoredEntry = resolve(root, entry);
209
+ if (!entry ||
210
+ isAbsolute(entry) ||
211
+ !within(root, restoredEntry) ||
212
+ !within(root, realpathSync(restoredEntry)) ||
213
+ fingerprint(readSources(root), entry) !== revision)
214
+ throw new Error('Snapshot identity changed');
215
+ return {
216
+ plugin,
217
+ installationId,
218
+ revision,
219
+ originalRoot: installation.root,
220
+ entry: restoredEntry,
221
+ registration: { ...registration, entry: restoredEntry, source: undefined },
222
+ };
223
+ }
224
+ catch {
225
+ throw new PluginError('PLUGIN_TASK_REVISION_UNAVAILABLE', 'Task source snapshot is missing or has changed');
226
+ }
227
+ }
@@ -0,0 +1,2 @@
1
+ /** Complete uninstall discards execution caches after every plugin has been removed. */
2
+ export declare function cleanupPluginRuntime(): Promise<void>;
@@ -0,0 +1,55 @@
1
+ import { rmSync } from 'node:fs';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { claimRecord, isProcessOwnerAlive, processIdentity, readRecord, removeInvalidRecord, removeRecordIfPid, } from '@mearl/daemon-core';
4
+ import { pluginHostPaths } from './pluginHostPaths.js';
5
+ import { listPluginNames } from './pluginRegistry.js';
6
+ import { PluginTaskClient } from './pluginTasks.js';
7
+ import { PluginTaskStore, pluginTaskExecutionUnconfirmed } from './pluginTaskStore.js';
8
+ /** Complete uninstall discards execution caches after every plugin has been removed. */
9
+ export async function cleanupPluginRuntime() {
10
+ const paths = pluginHostPaths();
11
+ if (listPluginNames().length)
12
+ throw new Error('Registered plugins still use the execution service');
13
+ // The service launcher uses this same claim before creating a replacement host.
14
+ const lock = `${paths.lease}.start`;
15
+ const starter = readRecord(lock);
16
+ if (starter && !isProcessOwnerAlive(starter))
17
+ removeRecordIfPid(lock, starter.pid);
18
+ else if (!starter)
19
+ removeInvalidRecord(lock);
20
+ if (!claimRecord(lock, { pid: process.pid, processIdentity: processIdentity(process.pid) }))
21
+ throw new Error('Plugin execution service is starting; caches were retained');
22
+ try {
23
+ const lease = readRecord(paths.lease);
24
+ if (lease && isProcessOwnerAlive(lease)) {
25
+ const client = new PluginTaskClient({ autoStart: false });
26
+ try {
27
+ await client.stopHost();
28
+ }
29
+ finally {
30
+ client.disconnect();
31
+ }
32
+ }
33
+ const current = readRecord(paths.lease);
34
+ if ((current && isProcessOwnerAlive(current)) || listPluginNames().length)
35
+ throw new Error('Plugin execution service restarted during cleanup; caches were retained');
36
+ const store = new PluginTaskStore();
37
+ if (store.list().some(pluginTaskExecutionUnconfirmed))
38
+ throw new Error('Plugin execution is not confirmed stopped; task records were retained');
39
+ store.recover(randomUUID());
40
+ if (store.list().some(pluginTaskExecutionUnconfirmed))
41
+ throw new Error('Plugin execution is not confirmed stopped; task records were retained');
42
+ const replacement = readRecord(paths.lease);
43
+ if ((replacement && isProcessOwnerAlive(replacement)) || listPluginNames().length)
44
+ throw new Error('Plugin execution service restarted during cleanup; caches were retained');
45
+ for (const file of [paths.tasks, paths.revisions, paths.log, `${paths.log}.1`])
46
+ rmSync(file, { recursive: true, force: true });
47
+ if (current)
48
+ removeRecordIfPid(paths.lease, current.pid);
49
+ if (process.platform !== 'win32')
50
+ rmSync(paths.socket, { force: true });
51
+ }
52
+ finally {
53
+ removeRecordIfPid(lock, process.pid);
54
+ }
55
+ }
@@ -0,0 +1,26 @@
1
+ import { PluginHostConnection } from './pluginHostConnection.js';
2
+ import type { PluginHostClientOptions } from './pluginHostConnection.js';
3
+ import type { PluginOptions } from './plugin.js';
4
+ import type { PluginStateOperation, PluginStateSnapshot, PluginStateList, PluginStateTransaction } from './generated-plugin-state.js';
5
+ export type * from './generated-plugin-state.js';
6
+ export declare class PluginStateClient {
7
+ private readonly options;
8
+ readonly connection: PluginHostConnection;
9
+ constructor(options?: PluginHostClientOptions);
10
+ call(plugin: string, collection: string, operation: PluginStateOperation, input?: Record<string, unknown>, options?: PluginOptions): Promise<any>;
11
+ get(plugin: string, collection: string, key: string, input?: {
12
+ instanceId?: string;
13
+ }): Promise<PluginStateSnapshot>;
14
+ getMany(plugin: string, collection: string, keys: string[], input?: {
15
+ instanceId?: string;
16
+ }): Promise<PluginStateList>;
17
+ list(plugin: string, collection: string, input?: Record<string, unknown>): Promise<PluginStateList>;
18
+ transact(plugin: string, collection: string, transaction: PluginStateTransaction, input?: {
19
+ instanceId?: string;
20
+ }): Promise<{
21
+ revision: number;
22
+ schemaVersion: number;
23
+ reused?: boolean;
24
+ }>;
25
+ disconnect(): void;
26
+ }
@@ -0,0 +1,28 @@
1
+ import { PluginHostConnection } from './pluginHostConnection.js';
2
+ export class PluginStateClient {
3
+ options;
4
+ connection;
5
+ constructor(options = {}) {
6
+ this.options = options;
7
+ this.connection = options.connection ?? new PluginHostConnection(options);
8
+ }
9
+ call(plugin, collection, operation, input = {}, options = {}) {
10
+ return this.connection.request('state.call', { plugin, input: { ...input, collection, operation }, options }, 30000);
11
+ }
12
+ get(plugin, collection, key, input = {}) {
13
+ return this.call(plugin, collection, 'get', { ...input, key });
14
+ }
15
+ getMany(plugin, collection, keys, input = {}) {
16
+ return this.call(plugin, collection, 'get-many', { ...input, keys });
17
+ }
18
+ list(plugin, collection, input = {}) {
19
+ return this.call(plugin, collection, 'list', input);
20
+ }
21
+ transact(plugin, collection, transaction, input = {}) {
22
+ return this.call(plugin, collection, 'transact', { ...input, transaction });
23
+ }
24
+ disconnect() {
25
+ if (!this.options.connection)
26
+ this.connection.disconnect();
27
+ }
28
+ }
@@ -0,0 +1,4 @@
1
+ import type { PluginStateDeclaration } from './generated-plugin-state.js';
2
+ export declare function compilePluginStateSchema(schema: Record<string, unknown>): import("ajv/dist/core.js").AnyValidateFunction<unknown>;
3
+ export declare function normalizePluginState(value: unknown): PluginStateDeclaration | undefined;
4
+ export declare function pluginStateSignature(collection: import('./generated-plugin-state.js').PluginStateCollection): string;