@mearl/client 2.20.6 → 2.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/README.md +153 -3
  2. package/dist/actionTimeouts.js +1 -1
  3. package/dist/artifacts.d.ts +1 -1
  4. package/dist/browserRegistry.d.ts +1 -1
  5. package/dist/check.d.ts +1 -1
  6. package/dist/cli.js +14 -8
  7. package/dist/commands.d.ts +23 -14
  8. package/dist/commands.js +24 -4
  9. package/dist/{generated-app-profile-definition.d.ts → generated/app-profile-definition.d.ts} +2 -2
  10. package/dist/{generated-app-profile-runtime-types.d.ts → generated/app-profile-runtime-types.d.ts} +2 -2
  11. package/dist/{generated-browser-action-protocol.d.ts → generated/browser-action-protocol.d.ts} +11 -1
  12. package/dist/{generated-browser-action-protocol.js → generated/browser-action-protocol.js} +3 -0
  13. package/dist/generated/plugin-diagnostics.d.ts +110 -0
  14. package/dist/generated/plugin-diagnostics.js +1 -0
  15. package/dist/generated/plugin-execution.d.ts +27 -0
  16. package/dist/generated/plugin-execution.js +1 -0
  17. package/dist/generated/plugin-models.d.ts +64 -0
  18. package/dist/generated/plugin-models.js +1 -0
  19. package/dist/generated/plugin-pages.d.ts +76 -0
  20. package/dist/generated/plugin-pages.js +59 -0
  21. package/dist/generated/plugin-recovery.d.ts +32 -0
  22. package/dist/generated/plugin-recovery.js +1 -0
  23. package/dist/generated/plugin-state.d.ts +50 -0
  24. package/dist/generated/plugin-state.js +1 -0
  25. package/dist/generated/plugin-tasks.d.ts +74 -0
  26. package/dist/generated/plugin-tasks.js +7 -0
  27. package/dist/generated/toolbox-icon.d.ts +10 -0
  28. package/dist/generated/toolbox-icon.js +37 -0
  29. package/dist/index.d.ts +15 -4
  30. package/dist/index.js +7 -1
  31. package/dist/pageEvalArguments.d.ts +1 -1
  32. package/dist/pageEvalArguments.js +1 -1
  33. package/dist/plugin-task-host.d.ts +1 -0
  34. package/dist/plugin-task-host.js +14 -0
  35. package/dist/plugin-worker.d.ts +1 -0
  36. package/dist/plugin-worker.js +17 -0
  37. package/dist/plugin.d.ts +1 -65
  38. package/dist/plugin.js +1 -391
  39. package/dist/pluginCliOutput.d.ts +2 -0
  40. package/dist/pluginCliOutput.js +7 -0
  41. package/dist/pluginModelSessionWorker.d.ts +23 -0
  42. package/dist/pluginModelSessionWorker.js +60 -0
  43. package/dist/plugins/clients/pluginHostConnection.d.ts +35 -0
  44. package/dist/plugins/clients/pluginHostConnection.js +256 -0
  45. package/dist/plugins/clients/pluginModels.d.ts +17 -0
  46. package/dist/plugins/clients/pluginModels.js +47 -0
  47. package/dist/plugins/clients/pluginPages.d.ts +21 -0
  48. package/dist/plugins/clients/pluginPages.js +46 -0
  49. package/dist/plugins/clients/pluginState.d.ts +26 -0
  50. package/dist/plugins/clients/pluginState.js +28 -0
  51. package/dist/plugins/clients/pluginTasks.d.ts +51 -0
  52. package/dist/plugins/clients/pluginTasks.js +181 -0
  53. package/dist/{officialPluginInstaller.js → plugins/officialPluginInstaller.js} +1 -3
  54. package/dist/{pluginPageBundle.js → plugins/pluginPageBundle.js} +74 -17
  55. package/dist/{pluginRegistry.d.ts → plugins/pluginRegistry.d.ts} +3 -4
  56. package/dist/{pluginRegistry.js → plugins/pluginRegistry.js} +16 -15
  57. package/dist/plugins/pluginTaskProtocol.d.ts +47 -0
  58. package/dist/plugins/pluginTaskProtocol.js +1 -0
  59. package/dist/plugins/pluginTaskWire.d.ts +4 -0
  60. package/dist/plugins/pluginTaskWire.js +42 -0
  61. package/dist/plugins/runtime/pluginCommandWorker.d.ts +2 -0
  62. package/dist/plugins/runtime/pluginCommandWorker.js +174 -0
  63. package/dist/plugins/runtime/pluginDiagnostics.d.ts +26 -0
  64. package/dist/plugins/runtime/pluginDiagnostics.js +309 -0
  65. package/dist/plugins/runtime/pluginExecution.d.ts +21 -0
  66. package/dist/plugins/runtime/pluginExecution.js +43 -0
  67. package/dist/plugins/runtime/pluginExecutor.d.ts +29 -0
  68. package/dist/plugins/runtime/pluginExecutor.js +223 -0
  69. package/dist/plugins/runtime/pluginExecutorProtocol.d.ts +55 -0
  70. package/dist/plugins/runtime/pluginExecutorProtocol.js +47 -0
  71. package/dist/plugins/runtime/pluginExecutorSupervisor.d.ts +4 -0
  72. package/dist/plugins/runtime/pluginExecutorSupervisor.js +63 -0
  73. package/dist/plugins/runtime/pluginHost.d.ts +20 -0
  74. package/dist/plugins/runtime/pluginHost.js +53 -0
  75. package/dist/plugins/runtime/pluginHostFiles.d.ts +1 -0
  76. package/dist/plugins/runtime/pluginHostFiles.js +21 -0
  77. package/dist/plugins/runtime/pluginHostPaths.d.ts +11 -0
  78. package/dist/plugins/runtime/pluginHostPaths.js +22 -0
  79. package/dist/plugins/runtime/pluginHostServer.d.ts +7 -0
  80. package/dist/plugins/runtime/pluginHostServer.js +252 -0
  81. package/dist/plugins/runtime/pluginModelCatalog.d.ts +14 -0
  82. package/dist/plugins/runtime/pluginModelCatalog.js +51 -0
  83. package/dist/plugins/runtime/pluginModelSessions.d.ts +16 -0
  84. package/dist/plugins/runtime/pluginModelSessions.js +234 -0
  85. package/dist/plugins/runtime/pluginPageManager.d.ts +57 -0
  86. package/dist/plugins/runtime/pluginPageManager.js +877 -0
  87. package/dist/plugins/runtime/pluginProcessTree.d.ts +2 -0
  88. package/dist/plugins/runtime/pluginProcessTree.js +31 -0
  89. package/dist/plugins/runtime/pluginRevision.d.ts +14 -0
  90. package/dist/plugins/runtime/pluginRevision.js +227 -0
  91. package/dist/plugins/runtime/pluginRuntimeCleanup.d.ts +2 -0
  92. package/dist/plugins/runtime/pluginRuntimeCleanup.js +55 -0
  93. package/dist/plugins/runtime/pluginStateManager.d.ts +24 -0
  94. package/dist/plugins/runtime/pluginStateManager.js +132 -0
  95. package/dist/plugins/runtime/pluginStateStore.d.ts +57 -0
  96. package/dist/plugins/runtime/pluginStateStore.js +396 -0
  97. package/dist/plugins/runtime/pluginTaskManager.d.ts +92 -0
  98. package/dist/plugins/runtime/pluginTaskManager.js +1080 -0
  99. package/dist/plugins/runtime/pluginTaskStore.d.ts +53 -0
  100. package/dist/plugins/runtime/pluginTaskStore.js +319 -0
  101. package/dist/plugins/runtime/pythonPlugin.d.ts +31 -0
  102. package/dist/{pythonPlugin.js → plugins/runtime/pythonPlugin.js} +236 -31
  103. package/dist/plugins/sdk/plugin.d.ts +118 -0
  104. package/dist/plugins/sdk/plugin.js +672 -0
  105. package/dist/{pluginAppProfiles.d.ts → plugins/sdk/pluginAppProfiles.d.ts} +1 -1
  106. package/dist/{pluginAppProfiles.js → plugins/sdk/pluginAppProfiles.js} +2 -2
  107. package/dist/plugins/sdk/pluginModelDefinition.d.ts +6 -0
  108. package/dist/plugins/sdk/pluginModelDefinition.js +75 -0
  109. package/dist/plugins/sdk/pluginPageDefinition.d.ts +3 -0
  110. package/dist/plugins/sdk/pluginPageDefinition.js +62 -0
  111. package/dist/plugins/sdk/pluginRecoveryDefinition.d.ts +3 -0
  112. package/dist/plugins/sdk/pluginRecoveryDefinition.js +56 -0
  113. package/dist/plugins/sdk/pluginStateDefinition.d.ts +4 -0
  114. package/dist/plugins/sdk/pluginStateDefinition.js +68 -0
  115. package/dist/runtimePaths.d.ts +4 -0
  116. package/dist/runtimePaths.js +8 -0
  117. package/dist/socket.d.ts +2 -2
  118. package/dist/socket.js +10 -6
  119. package/dist/unified.d.ts +3 -2
  120. package/dist/unified.js +83 -2
  121. package/package.json +7 -3
  122. package/dist/pythonPlugin.d.ts +0 -20
  123. /package/dist/{generated-app-profile-definition.js → generated/app-profile-definition.js} +0 -0
  124. /package/dist/{generated-app-profile-runtime-types.js → generated/app-profile-runtime-types.js} +0 -0
  125. /package/dist/{officialPluginCatalog.d.ts → plugins/officialPluginCatalog.d.ts} +0 -0
  126. /package/dist/{officialPluginCatalog.js → plugins/officialPluginCatalog.js} +0 -0
  127. /package/dist/{officialPluginInstaller.d.ts → plugins/officialPluginInstaller.d.ts} +0 -0
  128. /package/dist/{pluginPageBundle.d.ts → plugins/pluginPageBundle.d.ts} +0 -0
  129. /package/dist/{pluginSource.d.ts → plugins/pluginSource.d.ts} +0 -0
  130. /package/dist/{pluginSource.js → plugins/pluginSource.js} +0 -0
@@ -0,0 +1,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,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 '../clients/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,24 @@
1
+ import type { PluginRevision } from './pluginRevision.js';
2
+ import { PluginStateStore } from './pluginStateStore.js';
3
+ import type { PluginTaskManager, PluginTaskBridge } from './pluginTaskManager.js';
4
+ import type { PluginOptions } from '../sdk/plugin.js';
5
+ import type { PluginPageTarget } from '../../generated/plugin-pages.js';
6
+ export declare class PluginStateManager {
7
+ private readonly tasks;
8
+ readonly store: PluginStateStore;
9
+ private readonly descriptions;
10
+ private readonly pending;
11
+ private readonly controller;
12
+ resolveTarget?: (plugin: string, instanceId: unknown, bridge: PluginTaskBridge, options: PluginOptions) => Promise<{
13
+ target: PluginPageTarget;
14
+ assertActive(): void;
15
+ }>;
16
+ private readonly maintenance;
17
+ constructor(tasks: PluginTaskManager);
18
+ call(plugin: string, input: Record<string, unknown>, bridge: PluginTaskBridge, options?: PluginOptions, binding?: {
19
+ revision: PluginRevision;
20
+ signal?: AbortSignal;
21
+ expectedTarget?: PluginPageTarget;
22
+ }): Promise<unknown>;
23
+ close(): Promise<void>;
24
+ }
@@ -0,0 +1,132 @@
1
+ import { pluginStateSignature } from '../sdk/pluginStateDefinition.js';
2
+ import { readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { findPluginRegistration } from '../pluginRegistry.js';
5
+ import { pluginHostPaths } from './pluginHostPaths.js';
6
+ import { capturePluginRevision } from './pluginRevision.js';
7
+ import { PluginStateStore } from './pluginStateStore.js';
8
+ export class PluginStateManager {
9
+ tasks;
10
+ store = new PluginStateStore();
11
+ descriptions = new Map();
12
+ pending = new Set();
13
+ controller = new AbortController();
14
+ resolveTarget;
15
+ maintenance;
16
+ constructor(tasks) {
17
+ this.tasks = tasks;
18
+ this.store.prune();
19
+ this.maintenance = setInterval(() => {
20
+ try {
21
+ this.store.prune();
22
+ }
23
+ catch (error) {
24
+ console.error('Plugin cache maintenance failed:', error);
25
+ }
26
+ }, 60000);
27
+ this.maintenance.unref();
28
+ }
29
+ async call(plugin, input, bridge, options = {}, binding) {
30
+ this.controller.signal.throwIfAborted();
31
+ binding?.signal?.throwIfAborted();
32
+ if (!/^[a-z][a-z0-9-]*$/.test(plugin) ||
33
+ (bridge.scope.plugin && bridge.scope.plugin !== plugin) ||
34
+ !input ||
35
+ typeof input !== 'object' ||
36
+ Array.isArray(input))
37
+ throw new Error('Invalid plugin state scope');
38
+ const revision = binding?.revision ?? capturePluginRevision(plugin);
39
+ const id = `${revision.installationId}/${revision.revision}`;
40
+ let description = this.descriptions.get(id);
41
+ if (!description) {
42
+ if (this.descriptions.size >= 64)
43
+ this.descriptions.delete(this.descriptions.keys().next().value);
44
+ const release = this.tasks.pinRevision(revision);
45
+ description = this.tasks
46
+ .describeRevision(revision, this.controller.signal, { cache: true })
47
+ .finally(release);
48
+ this.descriptions.set(id, description);
49
+ this.pending.add(description);
50
+ void description.finally(() => this.pending.delete(description)).catch(() => { });
51
+ void description.catch(() => {
52
+ if (this.descriptions.get(id) === description)
53
+ this.descriptions.delete(id);
54
+ });
55
+ }
56
+ const declaration = (await description).stateStorage;
57
+ this.controller.signal.throwIfAborted();
58
+ binding?.signal?.throwIfAborted();
59
+ const name = String(input.collection ?? '');
60
+ const definition = declaration && Object.hasOwn(declaration.collections, name)
61
+ ? declaration.collections[name]
62
+ : undefined;
63
+ if (!definition || !/^[a-z][a-z0-9-]{0,63}$/.test(name))
64
+ throw Object.assign(new Error('Plugin state collection is not declared'), {
65
+ code: 'PLUGIN_STATE_NOT_DECLARED',
66
+ });
67
+ if (input.expectedDeclaration !== undefined &&
68
+ input.expectedDeclaration !== pluginStateSignature(definition))
69
+ throw Object.assign(new Error('Plugin state declaration changed during command execution'), {
70
+ code: 'PLUGIN_STATE_REVISION_CHANGED',
71
+ });
72
+ let scope = definition.scope;
73
+ let assertScope = () => { };
74
+ if (definition.scope === 'host')
75
+ scope += `:${this.tasks.host.hostId}`;
76
+ if (definition.scope === 'instance' || definition.scope === 'browser-session') {
77
+ if (!this.resolveTarget)
78
+ throw new Error('Page state scope is unavailable');
79
+ const resolved = await this.resolveTarget(plugin, input.instanceId, bridge, options);
80
+ const target = resolved.target;
81
+ const expected = binding?.expectedTarget;
82
+ if (expected &&
83
+ (target.browserSessionId !== expected.browserSessionId ||
84
+ target.tabId !== expected.tabId ||
85
+ target.frameId !== expected.frameId ||
86
+ target.documentId !== expected.documentId))
87
+ throw Object.assign(new Error('State access cannot change the inherited document'), {
88
+ code: 'PLUGIN_PAGE_TARGET_CHANGED',
89
+ });
90
+ assertScope = resolved.assertActive;
91
+ scope +=
92
+ definition.scope === 'instance'
93
+ ? `:${String(input.instanceId)}`
94
+ : `:${target.browserSessionId}`;
95
+ }
96
+ // No await occurs between the final target check and the atomic store operation.
97
+ this.controller.signal.throwIfAborted();
98
+ binding?.signal?.throwIfAborted();
99
+ let installation;
100
+ try {
101
+ installation = JSON.parse(readFileSync(join(pluginHostPaths().installations, `${plugin}.json`), 'utf8'));
102
+ }
103
+ catch {
104
+ /* Removed installations have no writable state scope. */
105
+ }
106
+ if (!findPluginRegistration(plugin) || installation?.installationId !== revision.installationId)
107
+ throw Object.assign(new Error('Plugin state installation was removed or replaced'), {
108
+ code: 'PLUGIN_STATE_SCOPE_ENDED',
109
+ });
110
+ assertScope();
111
+ const namespace = { installationId: revision.installationId, collection: name, scope };
112
+ switch (input.operation) {
113
+ case 'get':
114
+ return this.store.get(namespace, definition, input.key);
115
+ case 'get-many':
116
+ return this.store.getMany(namespace, definition, input.keys);
117
+ case 'list':
118
+ return this.store.list(namespace, definition, input);
119
+ case 'transact':
120
+ return this.store.transact(namespace, definition, input.transaction);
121
+ default:
122
+ throw new Error('Invalid state operation');
123
+ }
124
+ }
125
+ async close() {
126
+ clearInterval(this.maintenance);
127
+ this.controller.abort();
128
+ this.descriptions.clear();
129
+ this.store.clearMemory();
130
+ await Promise.allSettled([...this.pending]);
131
+ }
132
+ }
@@ -0,0 +1,57 @@
1
+ import type { PluginStateCollection, PluginStateEntry, PluginStateTransaction } from '../../generated/plugin-state.js';
2
+ export interface PluginStateNamespace {
3
+ installationId: string;
4
+ collection: string;
5
+ scope: string;
6
+ }
7
+ /** A collection namespace is one atomic record, including CAS and commit receipts. */
8
+ export declare class PluginStateStore {
9
+ readonly directory: string;
10
+ private readonly now;
11
+ private readonly memory;
12
+ private memoryBytes;
13
+ private memoryRevisionFloor;
14
+ clearMemory(): void;
15
+ private readonly valueBytes;
16
+ private memorySize;
17
+ private saveMemory;
18
+ constructor(directory?: string, now?: () => number);
19
+ private path;
20
+ private read;
21
+ private entry;
22
+ discardScope(installationId: string, scope: string): void;
23
+ prune(skipFile?: string): void;
24
+ get(namespace: PluginStateNamespace, definition: PluginStateCollection, name: string): {
25
+ revision: number;
26
+ schemaVersion: number;
27
+ key: string;
28
+ exists: boolean;
29
+ version: string;
30
+ value?: unknown;
31
+ expiresAt?: number;
32
+ };
33
+ getMany(namespace: PluginStateNamespace, definition: PluginStateCollection, names: unknown): {
34
+ entries: PluginStateEntry[];
35
+ revision: number;
36
+ schemaVersion: number;
37
+ };
38
+ list(namespace: PluginStateNamespace, definition: PluginStateCollection, input?: {
39
+ cursor?: string;
40
+ limit?: number;
41
+ expectedRevision?: number;
42
+ }): {
43
+ nextCursor?: string | undefined;
44
+ entries: PluginStateEntry[];
45
+ revision: number;
46
+ schemaVersion: number;
47
+ };
48
+ transact(namespace: PluginStateNamespace, definition: PluginStateCollection, input: PluginStateTransaction): {
49
+ revision: number;
50
+ schemaVersion: number;
51
+ reused: boolean;
52
+ } | {
53
+ revision: number;
54
+ schemaVersion: number;
55
+ reused?: undefined;
56
+ };
57
+ }