@clinebot/core 0.0.11 → 0.0.13

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 (68) hide show
  1. package/README.md +1 -1
  2. package/dist/agents/agent-config-loader.d.ts +1 -1
  3. package/dist/agents/agent-config-parser.d.ts +5 -2
  4. package/dist/agents/index.d.ts +1 -1
  5. package/dist/agents/plugin-config-loader.d.ts +4 -0
  6. package/dist/agents/plugin-loader.d.ts +1 -0
  7. package/dist/agents/plugin-sandbox-bootstrap.js +446 -0
  8. package/dist/agents/plugin-sandbox.d.ts +4 -0
  9. package/dist/index.node.d.ts +5 -0
  10. package/dist/index.node.js +685 -413
  11. package/dist/runtime/commands.d.ts +11 -0
  12. package/dist/runtime/sandbox/subprocess-sandbox.d.ts +8 -1
  13. package/dist/runtime/skills.d.ts +13 -0
  14. package/dist/session/default-session-manager.d.ts +5 -0
  15. package/dist/session/session-config-builder.d.ts +4 -1
  16. package/dist/session/session-manager.d.ts +1 -0
  17. package/dist/session/session-service.d.ts +22 -22
  18. package/dist/session/unified-session-persistence-service.d.ts +12 -6
  19. package/dist/session/utils/helpers.d.ts +2 -2
  20. package/dist/session/utils/types.d.ts +9 -0
  21. package/dist/tools/definitions.d.ts +2 -2
  22. package/dist/tools/presets.d.ts +3 -3
  23. package/dist/tools/schemas.d.ts +15 -14
  24. package/dist/types/config.d.ts +5 -0
  25. package/dist/types/events.d.ts +22 -0
  26. package/package.json +5 -4
  27. package/src/agents/agent-config-loader.test.ts +2 -0
  28. package/src/agents/agent-config-loader.ts +1 -0
  29. package/src/agents/agent-config-parser.ts +12 -5
  30. package/src/agents/index.ts +1 -0
  31. package/src/agents/plugin-config-loader.test.ts +49 -0
  32. package/src/agents/plugin-config-loader.ts +10 -73
  33. package/src/agents/plugin-loader.test.ts +127 -1
  34. package/src/agents/plugin-loader.ts +72 -5
  35. package/src/agents/plugin-sandbox-bootstrap.ts +445 -0
  36. package/src/agents/plugin-sandbox.test.ts +198 -1
  37. package/src/agents/plugin-sandbox.ts +223 -353
  38. package/src/index.node.ts +14 -0
  39. package/src/runtime/commands.test.ts +98 -0
  40. package/src/runtime/commands.ts +83 -0
  41. package/src/runtime/hook-file-hooks.test.ts +1 -1
  42. package/src/runtime/hook-file-hooks.ts +16 -6
  43. package/src/runtime/index.ts +10 -0
  44. package/src/runtime/runtime-builder.test.ts +67 -0
  45. package/src/runtime/runtime-builder.ts +70 -16
  46. package/src/runtime/sandbox/subprocess-sandbox.ts +35 -11
  47. package/src/runtime/skills.ts +44 -0
  48. package/src/runtime/workflows.ts +20 -29
  49. package/src/session/default-session-manager.e2e.test.ts +52 -33
  50. package/src/session/default-session-manager.test.ts +453 -1
  51. package/src/session/default-session-manager.ts +210 -12
  52. package/src/session/rpc-session-service.ts +14 -96
  53. package/src/session/session-config-builder.ts +2 -0
  54. package/src/session/session-manager.ts +1 -0
  55. package/src/session/session-service.ts +127 -64
  56. package/src/session/session-team-coordination.ts +30 -0
  57. package/src/session/unified-session-persistence-service.test.ts +3 -3
  58. package/src/session/unified-session-persistence-service.ts +159 -141
  59. package/src/session/utils/helpers.ts +22 -41
  60. package/src/session/utils/types.ts +10 -0
  61. package/src/storage/sqlite-team-store.ts +16 -5
  62. package/src/tools/definitions.test.ts +137 -8
  63. package/src/tools/definitions.ts +115 -70
  64. package/src/tools/presets.test.ts +2 -3
  65. package/src/tools/presets.ts +3 -3
  66. package/src/tools/schemas.ts +28 -28
  67. package/src/types/config.ts +5 -0
  68. package/src/types/events.ts +23 -0
@@ -0,0 +1,445 @@
1
+ import { existsSync } from "node:fs";
2
+ import { builtinModules, createRequire } from "node:module";
3
+ import { dirname, resolve } from "node:path";
4
+ /**
5
+ * Bootstrap script for the plugin sandbox subprocess.
6
+ *
7
+ * This file runs inside an isolated Node.js child process spawned by
8
+ * {@link SubprocessSandbox}. It receives RPC calls over IPC and dynamically
9
+ * imports plugin modules, wiring up their contributions (tools, commands,
10
+ * shortcuts, flags, renderers, providers) and lifecycle hooks.
11
+ *
12
+ * Because it executes in a separate process it must be self-contained — no
13
+ * imports from the rest of the codebase are allowed.
14
+ */
15
+
16
+ import { fileURLToPath } from "node:url";
17
+ import createJiti from "jiti";
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Types (intentionally minimal – mirrors only what the RPC protocol needs)
21
+ // ---------------------------------------------------------------------------
22
+
23
+ interface PluginTool {
24
+ name: string;
25
+ description?: string;
26
+ inputSchema?: unknown;
27
+ timeoutMs?: number;
28
+ retryable?: boolean;
29
+ execute: (input: unknown, context: unknown) => Promise<unknown>;
30
+ }
31
+
32
+ interface PluginCommand {
33
+ name: string;
34
+ description?: string;
35
+ handler?: (input: string) => Promise<string>;
36
+ }
37
+
38
+ interface PluginShortcut {
39
+ name: string;
40
+ value: string;
41
+ description?: string;
42
+ }
43
+
44
+ interface PluginFlag {
45
+ name: string;
46
+ description?: string;
47
+ defaultValue?: boolean | string | number;
48
+ }
49
+
50
+ interface PluginMessageRenderer {
51
+ name: string;
52
+ render: (message: unknown) => string;
53
+ }
54
+
55
+ interface PluginProvider {
56
+ name: string;
57
+ description?: string;
58
+ metadata?: Record<string, unknown>;
59
+ }
60
+
61
+ interface PluginApi {
62
+ registerTool(tool: PluginTool): void;
63
+ registerCommand(command: PluginCommand): void;
64
+ registerShortcut(shortcut: PluginShortcut): void;
65
+ registerFlag(flag: PluginFlag): void;
66
+ registerMessageRenderer(renderer: PluginMessageRenderer): void;
67
+ registerProvider(provider: PluginProvider): void;
68
+ }
69
+
70
+ interface PluginModule {
71
+ name: string;
72
+ manifest: Record<string, unknown>;
73
+ setup?: (api: PluginApi) => void | Promise<void>;
74
+ [hookName: string]: unknown;
75
+ }
76
+
77
+ interface ContributionDescriptor {
78
+ id: string;
79
+ name: string;
80
+ description?: string;
81
+ inputSchema?: unknown;
82
+ timeoutMs?: number;
83
+ retryable?: boolean;
84
+ value?: string;
85
+ defaultValue?: boolean | string | number;
86
+ metadata?: Record<string, unknown>;
87
+ }
88
+
89
+ interface PluginDescriptor {
90
+ pluginId: string;
91
+ name: string;
92
+ manifest: Record<string, unknown>;
93
+ contributions: {
94
+ tools: ContributionDescriptor[];
95
+ commands: ContributionDescriptor[];
96
+ shortcuts: ContributionDescriptor[];
97
+ flags: ContributionDescriptor[];
98
+ messageRenderers: ContributionDescriptor[];
99
+ providers: ContributionDescriptor[];
100
+ };
101
+ }
102
+
103
+ interface PluginState {
104
+ plugin: PluginModule;
105
+ handlers: {
106
+ tools: Map<string, PluginTool["execute"]>;
107
+ commands: Map<string, NonNullable<PluginCommand["handler"]>>;
108
+ messageRenderers: Map<string, PluginMessageRenderer["render"]>;
109
+ };
110
+ }
111
+
112
+ function isObject(value: unknown): value is Record<string, unknown> {
113
+ return typeof value === "object" && value !== null;
114
+ }
115
+
116
+ function assertValidPluginModule(
117
+ plugin: unknown,
118
+ pluginPath: string,
119
+ ): asserts plugin is PluginModule {
120
+ if (!isObject(plugin)) {
121
+ throw new Error(`Invalid plugin module: ${pluginPath}`);
122
+ }
123
+ if (typeof plugin.name !== "string" || !plugin.name) {
124
+ throw new Error(`Invalid plugin name: ${pluginPath}`);
125
+ }
126
+ if (!isObject(plugin.manifest)) {
127
+ throw new Error(`Invalid plugin manifest: ${pluginPath}`);
128
+ }
129
+ }
130
+
131
+ // ---------------------------------------------------------------------------
132
+ // State
133
+ // ---------------------------------------------------------------------------
134
+
135
+ let pluginCounter = 0;
136
+ const pluginState = new Map<string, PluginState>();
137
+ const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
138
+ const WORKSPACE_ALIASES = collectWorkspaceAliases(MODULE_DIR);
139
+ const BUILTIN_MODULES = new Set(
140
+ builtinModules.flatMap((id) => [id, id.replace(/^node:/, "")]),
141
+ );
142
+
143
+ // ---------------------------------------------------------------------------
144
+ // IPC helpers
145
+ // ---------------------------------------------------------------------------
146
+
147
+ function toErrorPayload(error: unknown): { message: string; stack?: string } {
148
+ const message = error instanceof Error ? error.message : String(error);
149
+ const stack = error instanceof Error ? error.stack : undefined;
150
+ return { message, stack };
151
+ }
152
+
153
+ function sendResponse(
154
+ id: string,
155
+ ok: boolean,
156
+ result: unknown,
157
+ error?: { message: string; stack?: string },
158
+ ): void {
159
+ if (!process.send) return;
160
+ process.send({ type: "response", id, ok, result, error });
161
+ }
162
+
163
+ function emitEvent(name: string, payload?: unknown): void {
164
+ if (!process.send) return;
165
+ process.send({ type: "event", name, payload });
166
+ }
167
+
168
+ // Expose event emitter to plugins.
169
+ (globalThis as Record<string, unknown>).__clinePluginHost = { emitEvent };
170
+
171
+ // ---------------------------------------------------------------------------
172
+ // Helpers
173
+ // ---------------------------------------------------------------------------
174
+
175
+ function sanitizeObject(value: unknown): Record<string, unknown> {
176
+ if (!value || typeof value !== "object") return {};
177
+ return value as Record<string, unknown>;
178
+ }
179
+
180
+ function makeId(pluginId: string, prefix: string): string {
181
+ return `${pluginId}_${prefix}_${Math.random().toString(36).slice(2, 10)}`;
182
+ }
183
+
184
+ function getPlugin(pluginId: string): PluginState {
185
+ const state = pluginState.get(pluginId);
186
+ if (!state) {
187
+ throw new Error(`Unknown sandbox plugin id: ${pluginId}`);
188
+ }
189
+ return state;
190
+ }
191
+
192
+ function collectWorkspaceAliases(startDir: string): Record<string, string> {
193
+ const root = resolve(startDir, "..", "..", "..", "..");
194
+ const aliases: Record<string, string> = {};
195
+ const candidates: Record<string, string> = {
196
+ "@clinebot/agents": resolve(root, "packages/agents/src/index.ts"),
197
+ "@clinebot/core": resolve(root, "packages/core/src/index.node.ts"),
198
+ "@clinebot/llms": resolve(root, "packages/llms/src/index.ts"),
199
+ "@clinebot/rpc": resolve(root, "packages/rpc/src/index.ts"),
200
+ "@clinebot/scheduler": resolve(root, "packages/scheduler/src/index.ts"),
201
+ "@clinebot/shared": resolve(root, "packages/shared/src/index.ts"),
202
+ "@clinebot/shared/storage": resolve(
203
+ root,
204
+ "packages/shared/src/storage/index.ts",
205
+ ),
206
+ "@clinebot/shared/db": resolve(root, "packages/shared/src/db/index.ts"),
207
+ };
208
+ for (const [key, value] of Object.entries(candidates)) {
209
+ if (existsSync(value)) {
210
+ aliases[key] = value;
211
+ }
212
+ }
213
+ return aliases;
214
+ }
215
+
216
+ function collectPluginImportAliases(
217
+ pluginPath: string,
218
+ ): Record<string, string> {
219
+ const pluginRequire = createRequire(pluginPath);
220
+ const aliases: Record<string, string> = {};
221
+ for (const [specifier, sourcePath] of Object.entries(WORKSPACE_ALIASES)) {
222
+ try {
223
+ pluginRequire.resolve(specifier);
224
+ continue;
225
+ } catch {
226
+ // Use the workspace source only when the plugin package does not provide
227
+ // its own installed SDK dependency.
228
+ }
229
+ aliases[specifier] = sourcePath;
230
+ }
231
+ return aliases;
232
+ }
233
+
234
+ async function importPluginModule(
235
+ pluginPath: string,
236
+ ): Promise<Record<string, unknown>> {
237
+ const aliases = collectPluginImportAliases(pluginPath);
238
+ const jiti = createJiti(pluginPath, {
239
+ alias: aliases,
240
+ cache: false,
241
+ requireCache: false,
242
+ esmResolve: true,
243
+ interopDefault: false,
244
+ nativeModules: [...BUILTIN_MODULES],
245
+ transformModules: Object.keys(aliases),
246
+ });
247
+ return (await jiti.import(pluginPath, {})) as Record<string, unknown>;
248
+ }
249
+
250
+ // ---------------------------------------------------------------------------
251
+ // RPC methods
252
+ // ---------------------------------------------------------------------------
253
+
254
+ async function initialize(args: {
255
+ pluginPaths?: string[];
256
+ exportName?: string;
257
+ }): Promise<PluginDescriptor[]> {
258
+ const descriptors: PluginDescriptor[] = [];
259
+ const exportName = args.exportName || "plugin";
260
+
261
+ for (const pluginPath of args.pluginPaths || []) {
262
+ const moduleExports = await importPluginModule(pluginPath);
263
+ const plugin = (moduleExports.default ??
264
+ moduleExports[exportName]) as unknown;
265
+ assertValidPluginModule(plugin, pluginPath);
266
+
267
+ const pluginId = `plugin_${++pluginCounter}`;
268
+ const contributions: PluginDescriptor["contributions"] = {
269
+ tools: [],
270
+ commands: [],
271
+ shortcuts: [],
272
+ flags: [],
273
+ messageRenderers: [],
274
+ providers: [],
275
+ };
276
+ const handlers: PluginState["handlers"] = {
277
+ tools: new Map(),
278
+ commands: new Map(),
279
+ messageRenderers: new Map(),
280
+ };
281
+
282
+ const api: PluginApi = {
283
+ registerTool: (tool) => {
284
+ const id = makeId(pluginId, "tool");
285
+ handlers.tools.set(id, tool.execute);
286
+ contributions.tools.push({
287
+ id,
288
+ name: tool.name,
289
+ description: tool.description,
290
+ inputSchema: tool.inputSchema,
291
+ timeoutMs: tool.timeoutMs,
292
+ retryable: tool.retryable,
293
+ });
294
+ },
295
+ registerCommand: (command) => {
296
+ const id = makeId(pluginId, "command");
297
+ if (typeof command.handler === "function") {
298
+ handlers.commands.set(id, command.handler);
299
+ }
300
+ contributions.commands.push({
301
+ id,
302
+ name: command.name,
303
+ description: command.description,
304
+ });
305
+ },
306
+ registerShortcut: (shortcut) => {
307
+ contributions.shortcuts.push({
308
+ id: makeId(pluginId, "shortcut"),
309
+ name: shortcut.name,
310
+ value: shortcut.value,
311
+ description: shortcut.description,
312
+ });
313
+ },
314
+ registerFlag: (flag) => {
315
+ contributions.flags.push({
316
+ id: makeId(pluginId, "flag"),
317
+ name: flag.name,
318
+ description: flag.description,
319
+ defaultValue: flag.defaultValue,
320
+ });
321
+ },
322
+ registerMessageRenderer: (renderer) => {
323
+ const id = makeId(pluginId, "renderer");
324
+ handlers.messageRenderers.set(id, renderer.render);
325
+ contributions.messageRenderers.push({ id, name: renderer.name });
326
+ },
327
+ registerProvider: (provider) => {
328
+ contributions.providers.push({
329
+ id: makeId(pluginId, "provider"),
330
+ name: provider.name,
331
+ description: provider.description,
332
+ metadata: sanitizeObject(provider.metadata),
333
+ });
334
+ },
335
+ };
336
+
337
+ if (typeof plugin.setup === "function") {
338
+ await plugin.setup(api);
339
+ }
340
+
341
+ pluginState.set(pluginId, { plugin, handlers });
342
+ descriptors.push({
343
+ pluginId,
344
+ name: plugin.name,
345
+ manifest: plugin.manifest,
346
+ contributions,
347
+ });
348
+ }
349
+
350
+ return descriptors;
351
+ }
352
+
353
+ async function invokeHook(args: {
354
+ pluginId: string;
355
+ hookName: string;
356
+ payload: unknown;
357
+ }): Promise<unknown> {
358
+ const state = getPlugin(args.pluginId);
359
+ const handler = state.plugin[args.hookName];
360
+ if (typeof handler !== "function") {
361
+ return undefined;
362
+ }
363
+ return await (handler as (payload: unknown) => Promise<unknown>)(
364
+ args.payload,
365
+ );
366
+ }
367
+
368
+ async function executeTool(args: {
369
+ pluginId: string;
370
+ contributionId: string;
371
+ input: unknown;
372
+ context: unknown;
373
+ }): Promise<unknown> {
374
+ const state = getPlugin(args.pluginId);
375
+ const handler = state.handlers.tools.get(args.contributionId);
376
+ if (typeof handler !== "function") {
377
+ throw new Error("Unknown sandbox tool contribution");
378
+ }
379
+ return await handler(args.input, args.context);
380
+ }
381
+
382
+ async function executeCommand(args: {
383
+ pluginId: string;
384
+ contributionId: string;
385
+ input: string;
386
+ }): Promise<string> {
387
+ const state = getPlugin(args.pluginId);
388
+ const handler = state.handlers.commands.get(args.contributionId);
389
+ if (typeof handler !== "function") {
390
+ return "";
391
+ }
392
+ return await handler(args.input);
393
+ }
394
+
395
+ async function renderMessage(args: {
396
+ pluginId: string;
397
+ contributionId: string;
398
+ message: unknown;
399
+ }): Promise<string> {
400
+ const state = getPlugin(args.pluginId);
401
+ const handler = state.handlers.messageRenderers.get(args.contributionId);
402
+ if (typeof handler !== "function") {
403
+ return "";
404
+ }
405
+ return await handler(args.message);
406
+ }
407
+
408
+ // ---------------------------------------------------------------------------
409
+ // Message dispatch
410
+ // ---------------------------------------------------------------------------
411
+
412
+ const methods: Record<string, (args: never) => Promise<unknown>> = {
413
+ initialize,
414
+ invokeHook,
415
+ executeTool,
416
+ executeCommand,
417
+ renderMessage,
418
+ };
419
+
420
+ process.on(
421
+ "message",
422
+ async (message: {
423
+ type: string;
424
+ id: string;
425
+ method: string;
426
+ args?: unknown;
427
+ }) => {
428
+ if (!message || message.type !== "call") {
429
+ return;
430
+ }
431
+ const method = methods[message.method];
432
+ if (!method) {
433
+ sendResponse(message.id, false, undefined, {
434
+ message: `Unknown method: ${String(message.method)}`,
435
+ });
436
+ return;
437
+ }
438
+ try {
439
+ const result = await method((message.args || {}) as never);
440
+ sendResponse(message.id, true, result);
441
+ } catch (error) {
442
+ sendResponse(message.id, false, undefined, toErrorPayload(error));
443
+ }
444
+ },
445
+ );
@@ -1,4 +1,4 @@
1
- import { mkdtemp, rm, writeFile } from "node:fs/promises";
1
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import type { AgentConfig, Tool, ToolContext } from "@clinebot/agents";
@@ -117,4 +117,201 @@ describe("plugin-sandbox", () => {
117
117
  await rm(dir, { recursive: true, force: true });
118
118
  }
119
119
  });
120
+
121
+ it("forwards sandbox plugin events to the host", async () => {
122
+ const dir = await mkdtemp(join(tmpdir(), "core-plugin-sandbox-events-"));
123
+ try {
124
+ const pluginPath = join(dir, "plugin-events.mjs");
125
+ await writeFile(
126
+ pluginPath,
127
+ [
128
+ "export default {",
129
+ " name: 'sandbox-events',",
130
+ " manifest: { capabilities: ['tools'] },",
131
+ " setup(api) {",
132
+ " api.registerTool({",
133
+ " name: 'emit_event',",
134
+ " description: 'emit host event',",
135
+ " inputSchema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'] },",
136
+ " execute: async (input) => {",
137
+ " globalThis.__clinePluginHost?.emitEvent?.('test_event', { value: input.value });",
138
+ " return { ok: true };",
139
+ " },",
140
+ " });",
141
+ " },",
142
+ "};",
143
+ ].join("\n"),
144
+ "utf8",
145
+ );
146
+
147
+ const events: Array<{ name: string; payload?: unknown }> = [];
148
+ const sandboxed = await loadSandboxedPlugins({
149
+ pluginPaths: [pluginPath],
150
+ onEvent: (event) => {
151
+ events.push(event);
152
+ },
153
+ });
154
+ try {
155
+ const extension = sandboxed.extensions?.[0];
156
+ const { tools, api } = createApiCapture();
157
+ await extension?.setup?.(api);
158
+ const tool = tools.find((entry) => entry.name === "emit_event");
159
+ await tool?.execute({ value: "hello" }, {
160
+ agentId: "agent-1",
161
+ conversationId: "conv-1",
162
+ iteration: 1,
163
+ } as ToolContext);
164
+ expect(events).toEqual([
165
+ {
166
+ name: "test_event",
167
+ payload: { value: "hello" },
168
+ },
169
+ ]);
170
+ } finally {
171
+ await sandboxed.shutdown();
172
+ }
173
+ } finally {
174
+ await rm(dir, { recursive: true, force: true });
175
+ }
176
+ });
177
+
178
+ it("loads TypeScript plugins in the sandbox process", async () => {
179
+ const dir = await mkdtemp(join(tmpdir(), "core-plugin-sandbox-ts-"));
180
+ try {
181
+ const pluginPath = join(dir, "plugin-ts.ts");
182
+ await writeFile(
183
+ pluginPath,
184
+ [
185
+ "const TOOL_NAME: string = 'sandbox_ts_echo';",
186
+ "export default {",
187
+ " name: 'sandbox-ts',",
188
+ " manifest: { capabilities: ['tools'] },",
189
+ " setup(api) {",
190
+ " api.registerTool({",
191
+ " name: TOOL_NAME,",
192
+ " description: 'echo',",
193
+ " inputSchema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'] },",
194
+ " execute: async (input) => ({ echoed: input.value }),",
195
+ " });",
196
+ " },",
197
+ "};",
198
+ ].join("\n"),
199
+ "utf8",
200
+ );
201
+
202
+ const sandboxed = await loadSandboxedPlugins({
203
+ pluginPaths: [pluginPath],
204
+ });
205
+ try {
206
+ const extension = sandboxed.extensions?.[0];
207
+ expect(extension?.name).toBe("sandbox-ts");
208
+ const { tools, api } = createApiCapture();
209
+ await extension?.setup?.(api);
210
+ const tool = tools.find((entry) => entry.name === "sandbox_ts_echo");
211
+ expect(tool).toBeDefined();
212
+ const result = await tool?.execute({ value: "ok" }, {
213
+ agentId: "agent-1",
214
+ conversationId: "conv-1",
215
+ iteration: 1,
216
+ } as ToolContext);
217
+ expect(result).toEqual({ echoed: "ok" });
218
+ } finally {
219
+ await sandboxed.shutdown();
220
+ }
221
+ } finally {
222
+ await rm(dir, { recursive: true, force: true });
223
+ }
224
+ });
225
+
226
+ it("resolves plugin-local dependencies in the sandbox process", async () => {
227
+ const dir = await mkdtemp(join(tmpdir(), "core-plugin-sandbox-deps-"));
228
+ try {
229
+ const depDir = join(dir, "node_modules", "sandbox-local-dep");
230
+ await mkdir(depDir, { recursive: true });
231
+ await writeFile(
232
+ join(depDir, "package.json"),
233
+ JSON.stringify({
234
+ name: "sandbox-local-dep",
235
+ type: "module",
236
+ exports: "./index.js",
237
+ }),
238
+ "utf8",
239
+ );
240
+ await writeFile(
241
+ join(depDir, "index.js"),
242
+ "export const depName = 'sandbox-local-dep';\n",
243
+ "utf8",
244
+ );
245
+ const pluginPath = join(dir, "plugin-dep.ts");
246
+ await writeFile(
247
+ pluginPath,
248
+ [
249
+ "import { depName } from 'sandbox-local-dep';",
250
+ "export default {",
251
+ " name: depName,",
252
+ " manifest: { capabilities: ['tools'] },",
253
+ "};",
254
+ ].join("\n"),
255
+ "utf8",
256
+ );
257
+
258
+ const sandboxed = await loadSandboxedPlugins({
259
+ pluginPaths: [pluginPath],
260
+ });
261
+ try {
262
+ expect(sandboxed.extensions?.[0]?.name).toBe("sandbox-local-dep");
263
+ } finally {
264
+ await sandboxed.shutdown();
265
+ }
266
+ } finally {
267
+ await rm(dir, { recursive: true, force: true });
268
+ }
269
+ });
270
+
271
+ it("prefers plugin-installed SDK packages in the sandbox process", async () => {
272
+ const dir = await mkdtemp(join(tmpdir(), "core-plugin-sandbox-sdk-"));
273
+ try {
274
+ const depDir = join(dir, "node_modules", "@clinebot", "shared");
275
+ await mkdir(depDir, { recursive: true });
276
+ await writeFile(
277
+ join(depDir, "package.json"),
278
+ JSON.stringify({
279
+ name: "@clinebot/shared",
280
+ type: "module",
281
+ exports: "./index.js",
282
+ }),
283
+ "utf8",
284
+ );
285
+ await writeFile(
286
+ join(depDir, "index.js"),
287
+ "export const sdkMarker = 'sandbox-plugin-installed-sdk';\n",
288
+ "utf8",
289
+ );
290
+ const pluginPath = join(dir, "plugin-sdk.ts");
291
+ await writeFile(
292
+ pluginPath,
293
+ [
294
+ "import { sdkMarker } from '@clinebot/shared';",
295
+ "export default {",
296
+ " name: sdkMarker,",
297
+ " manifest: { capabilities: ['tools'] },",
298
+ "};",
299
+ ].join("\n"),
300
+ "utf8",
301
+ );
302
+
303
+ const sandboxed = await loadSandboxedPlugins({
304
+ pluginPaths: [pluginPath],
305
+ });
306
+ try {
307
+ expect(sandboxed.extensions?.[0]?.name).toBe(
308
+ "sandbox-plugin-installed-sdk",
309
+ );
310
+ } finally {
311
+ await sandboxed.shutdown();
312
+ }
313
+ } finally {
314
+ await rm(dir, { recursive: true, force: true });
315
+ }
316
+ });
120
317
  });