@clinebot/core 0.0.21 → 0.0.23

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 (260) hide show
  1. package/dist/ClineCore.d.ts +110 -0
  2. package/dist/ClineCore.d.ts.map +1 -0
  3. package/dist/account/cline-account-service.d.ts +2 -1
  4. package/dist/account/cline-account-service.d.ts.map +1 -1
  5. package/dist/account/index.d.ts +1 -1
  6. package/dist/account/index.d.ts.map +1 -1
  7. package/dist/account/rpc.d.ts +3 -1
  8. package/dist/account/rpc.d.ts.map +1 -1
  9. package/dist/account/types.d.ts +3 -0
  10. package/dist/account/types.d.ts.map +1 -1
  11. package/dist/agents/plugin-loader.d.ts.map +1 -1
  12. package/dist/agents/plugin-sandbox-bootstrap.js +17 -17
  13. package/dist/auth/client.d.ts +1 -1
  14. package/dist/auth/client.d.ts.map +1 -1
  15. package/dist/auth/cline.d.ts +1 -1
  16. package/dist/auth/cline.d.ts.map +1 -1
  17. package/dist/auth/codex.d.ts +1 -1
  18. package/dist/auth/codex.d.ts.map +1 -1
  19. package/dist/auth/oca.d.ts +1 -1
  20. package/dist/auth/oca.d.ts.map +1 -1
  21. package/dist/auth/utils.d.ts +2 -2
  22. package/dist/auth/utils.d.ts.map +1 -1
  23. package/dist/index.d.ts +50 -5
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +949 -0
  26. package/dist/providers/local-provider-service.d.ts +4 -4
  27. package/dist/providers/local-provider-service.d.ts.map +1 -1
  28. package/dist/runtime/runtime-builder.d.ts +1 -0
  29. package/dist/runtime/runtime-builder.d.ts.map +1 -1
  30. package/dist/runtime/session-runtime.d.ts +2 -1
  31. package/dist/runtime/session-runtime.d.ts.map +1 -1
  32. package/dist/runtime/team-runtime-registry.d.ts +13 -0
  33. package/dist/runtime/team-runtime-registry.d.ts.map +1 -0
  34. package/dist/session/default-session-manager.d.ts +2 -2
  35. package/dist/session/default-session-manager.d.ts.map +1 -1
  36. package/dist/session/rpc-runtime-ensure.d.ts +53 -0
  37. package/dist/session/rpc-runtime-ensure.d.ts.map +1 -0
  38. package/dist/session/session-config-builder.d.ts +2 -3
  39. package/dist/session/session-config-builder.d.ts.map +1 -1
  40. package/dist/session/session-host.d.ts +8 -18
  41. package/dist/session/session-host.d.ts.map +1 -1
  42. package/dist/session/session-manager.d.ts +1 -1
  43. package/dist/session/session-manager.d.ts.map +1 -1
  44. package/dist/session/session-manifest.d.ts +1 -2
  45. package/dist/session/session-manifest.d.ts.map +1 -1
  46. package/dist/session/unified-session-persistence-service.d.ts +2 -2
  47. package/dist/session/unified-session-persistence-service.d.ts.map +1 -1
  48. package/dist/session/utils/helpers.d.ts +1 -1
  49. package/dist/session/utils/helpers.d.ts.map +1 -1
  50. package/dist/session/utils/types.d.ts +1 -1
  51. package/dist/session/utils/types.d.ts.map +1 -1
  52. package/dist/storage/provider-settings-legacy-migration.d.ts.map +1 -1
  53. package/dist/telemetry/OpenTelemetryProvider.d.ts.map +1 -1
  54. package/dist/telemetry/distinct-id.d.ts +2 -0
  55. package/dist/telemetry/distinct-id.d.ts.map +1 -0
  56. package/dist/telemetry/{opentelemetry.d.ts → index.d.ts} +1 -1
  57. package/dist/telemetry/index.d.ts.map +1 -0
  58. package/dist/telemetry/index.js +28 -0
  59. package/dist/tools/constants.d.ts +1 -1
  60. package/dist/tools/constants.d.ts.map +1 -1
  61. package/dist/tools/definitions.d.ts +3 -3
  62. package/dist/tools/definitions.d.ts.map +1 -1
  63. package/dist/tools/executors/apply-patch.d.ts +1 -1
  64. package/dist/tools/executors/apply-patch.d.ts.map +1 -1
  65. package/dist/tools/executors/bash.d.ts +1 -1
  66. package/dist/tools/executors/bash.d.ts.map +1 -1
  67. package/dist/tools/executors/editor.d.ts +1 -1
  68. package/dist/tools/executors/editor.d.ts.map +1 -1
  69. package/dist/tools/executors/file-read.d.ts +1 -1
  70. package/dist/tools/executors/file-read.d.ts.map +1 -1
  71. package/dist/tools/executors/index.d.ts +14 -14
  72. package/dist/tools/executors/index.d.ts.map +1 -1
  73. package/dist/tools/executors/search.d.ts +1 -1
  74. package/dist/tools/executors/search.d.ts.map +1 -1
  75. package/dist/tools/executors/web-fetch.d.ts +1 -1
  76. package/dist/tools/executors/web-fetch.d.ts.map +1 -1
  77. package/dist/tools/helpers.d.ts +1 -1
  78. package/dist/tools/helpers.d.ts.map +1 -1
  79. package/dist/tools/index.d.ts +10 -10
  80. package/dist/tools/index.d.ts.map +1 -1
  81. package/dist/tools/model-tool-routing.d.ts +1 -1
  82. package/dist/tools/model-tool-routing.d.ts.map +1 -1
  83. package/dist/tools/presets.d.ts +1 -1
  84. package/dist/tools/presets.d.ts.map +1 -1
  85. package/dist/types/common.d.ts +17 -8
  86. package/dist/types/common.d.ts.map +1 -1
  87. package/dist/types/config.d.ts +4 -3
  88. package/dist/types/config.d.ts.map +1 -1
  89. package/dist/types/provider-settings.d.ts +1 -1
  90. package/dist/types/provider-settings.d.ts.map +1 -1
  91. package/dist/types.d.ts +5 -2
  92. package/dist/types.d.ts.map +1 -1
  93. package/dist/version.d.ts +2 -0
  94. package/dist/version.d.ts.map +1 -0
  95. package/package.json +44 -38
  96. package/src/ClineCore.ts +137 -0
  97. package/src/account/cline-account-service.test.ts +101 -0
  98. package/src/account/cline-account-service.ts +300 -0
  99. package/src/account/featurebase-token.test.ts +175 -0
  100. package/src/account/index.ts +23 -0
  101. package/src/account/rpc.test.ts +63 -0
  102. package/src/account/rpc.ts +185 -0
  103. package/src/account/types.ts +102 -0
  104. package/src/agents/agent-config-loader.test.ts +236 -0
  105. package/src/agents/agent-config-loader.ts +108 -0
  106. package/src/agents/agent-config-parser.ts +198 -0
  107. package/src/agents/hooks-config-loader.test.ts +20 -0
  108. package/src/agents/hooks-config-loader.ts +118 -0
  109. package/src/agents/index.ts +85 -0
  110. package/src/agents/plugin-config-loader.test.ts +140 -0
  111. package/src/agents/plugin-config-loader.ts +97 -0
  112. package/src/agents/plugin-loader.test.ts +210 -0
  113. package/src/agents/plugin-loader.ts +175 -0
  114. package/src/agents/plugin-sandbox-bootstrap.ts +448 -0
  115. package/src/agents/plugin-sandbox.test.ts +296 -0
  116. package/src/agents/plugin-sandbox.ts +341 -0
  117. package/src/agents/unified-config-file-watcher.test.ts +196 -0
  118. package/src/agents/unified-config-file-watcher.ts +483 -0
  119. package/src/agents/user-instruction-config-loader.test.ts +158 -0
  120. package/src/agents/user-instruction-config-loader.ts +438 -0
  121. package/src/auth/client.test.ts +40 -0
  122. package/src/auth/client.ts +25 -0
  123. package/src/auth/cline.test.ts +130 -0
  124. package/src/auth/cline.ts +420 -0
  125. package/src/auth/codex.test.ts +170 -0
  126. package/src/auth/codex.ts +491 -0
  127. package/src/auth/oca.test.ts +215 -0
  128. package/src/auth/oca.ts +573 -0
  129. package/src/auth/server.ts +216 -0
  130. package/src/auth/types.ts +81 -0
  131. package/src/auth/utils.test.ts +128 -0
  132. package/src/auth/utils.ts +247 -0
  133. package/src/chat/chat-schema.ts +82 -0
  134. package/src/index.ts +479 -0
  135. package/src/input/file-indexer.d.ts +11 -0
  136. package/src/input/file-indexer.test.ts +127 -0
  137. package/src/input/file-indexer.ts +327 -0
  138. package/src/input/index.ts +7 -0
  139. package/src/input/mention-enricher.test.ts +85 -0
  140. package/src/input/mention-enricher.ts +122 -0
  141. package/src/mcp/config-loader.test.ts +238 -0
  142. package/src/mcp/config-loader.ts +219 -0
  143. package/src/mcp/index.ts +26 -0
  144. package/src/mcp/manager.test.ts +106 -0
  145. package/src/mcp/manager.ts +262 -0
  146. package/src/mcp/types.ts +88 -0
  147. package/src/providers/local-provider-registry.ts +232 -0
  148. package/src/providers/local-provider-service.test.ts +783 -0
  149. package/src/providers/local-provider-service.ts +471 -0
  150. package/src/runtime/commands.test.ts +98 -0
  151. package/src/runtime/commands.ts +83 -0
  152. package/src/runtime/hook-file-hooks.test.ts +237 -0
  153. package/src/runtime/hook-file-hooks.ts +859 -0
  154. package/src/runtime/index.ts +37 -0
  155. package/src/runtime/rules.ts +34 -0
  156. package/src/runtime/runtime-builder.team-persistence.test.ts +245 -0
  157. package/src/runtime/runtime-builder.test.ts +371 -0
  158. package/src/runtime/runtime-builder.ts +631 -0
  159. package/src/runtime/runtime-parity.test.ts +143 -0
  160. package/src/runtime/sandbox/subprocess-sandbox.ts +231 -0
  161. package/src/runtime/session-runtime.ts +49 -0
  162. package/src/runtime/skills.ts +44 -0
  163. package/src/runtime/team-runtime-registry.ts +46 -0
  164. package/src/runtime/tool-approval.ts +104 -0
  165. package/src/runtime/workflows.test.ts +119 -0
  166. package/src/runtime/workflows.ts +45 -0
  167. package/src/session/default-session-manager.e2e.test.ts +384 -0
  168. package/src/session/default-session-manager.test.ts +1931 -0
  169. package/src/session/default-session-manager.ts +1422 -0
  170. package/src/session/file-session-service.ts +280 -0
  171. package/src/session/index.ts +45 -0
  172. package/src/session/rpc-runtime-ensure.ts +521 -0
  173. package/src/session/rpc-session-service.ts +107 -0
  174. package/src/session/rpc-spawn-lease.test.ts +49 -0
  175. package/src/session/rpc-spawn-lease.ts +122 -0
  176. package/src/session/runtime-oauth-token-manager.test.ts +137 -0
  177. package/src/session/runtime-oauth-token-manager.ts +272 -0
  178. package/src/session/session-agent-events.ts +248 -0
  179. package/src/session/session-artifacts.ts +106 -0
  180. package/src/session/session-config-builder.ts +113 -0
  181. package/src/session/session-graph.ts +92 -0
  182. package/src/session/session-host.test.ts +89 -0
  183. package/src/session/session-host.ts +205 -0
  184. package/src/session/session-manager.ts +69 -0
  185. package/src/session/session-manifest.ts +29 -0
  186. package/src/session/session-service.team-persistence.test.ts +48 -0
  187. package/src/session/session-service.ts +673 -0
  188. package/src/session/session-team-coordination.ts +229 -0
  189. package/src/session/session-telemetry.ts +100 -0
  190. package/src/session/sqlite-rpc-session-backend.ts +303 -0
  191. package/src/session/unified-session-persistence-service.test.ts +85 -0
  192. package/src/session/unified-session-persistence-service.ts +994 -0
  193. package/src/session/utils/helpers.ts +139 -0
  194. package/src/session/utils/types.ts +57 -0
  195. package/src/session/utils/usage.ts +32 -0
  196. package/src/session/workspace-manager.ts +98 -0
  197. package/src/session/workspace-manifest.ts +100 -0
  198. package/src/storage/artifact-store.ts +1 -0
  199. package/src/storage/file-team-store.ts +257 -0
  200. package/src/storage/index.ts +11 -0
  201. package/src/storage/provider-settings-legacy-migration.test.ts +424 -0
  202. package/src/storage/provider-settings-legacy-migration.ts +826 -0
  203. package/src/storage/provider-settings-manager.test.ts +191 -0
  204. package/src/storage/provider-settings-manager.ts +152 -0
  205. package/src/storage/session-store.ts +1 -0
  206. package/src/storage/sqlite-session-store.ts +275 -0
  207. package/src/storage/sqlite-team-store.ts +454 -0
  208. package/src/storage/team-store.ts +40 -0
  209. package/src/team/index.ts +4 -0
  210. package/src/team/projections.ts +285 -0
  211. package/src/telemetry/ITelemetryAdapter.ts +94 -0
  212. package/src/telemetry/LoggerTelemetryAdapter.test.ts +42 -0
  213. package/src/telemetry/LoggerTelemetryAdapter.ts +114 -0
  214. package/src/telemetry/OpenTelemetryAdapter.test.ts +157 -0
  215. package/src/telemetry/OpenTelemetryAdapter.ts +348 -0
  216. package/src/telemetry/OpenTelemetryProvider.test.ts +113 -0
  217. package/src/telemetry/OpenTelemetryProvider.ts +325 -0
  218. package/src/telemetry/TelemetryService.test.ts +134 -0
  219. package/src/telemetry/TelemetryService.ts +141 -0
  220. package/src/telemetry/core-events.ts +400 -0
  221. package/src/telemetry/distinct-id.test.ts +57 -0
  222. package/src/telemetry/distinct-id.ts +58 -0
  223. package/src/telemetry/index.ts +20 -0
  224. package/src/tools/constants.ts +35 -0
  225. package/src/tools/definitions.test.ts +704 -0
  226. package/src/tools/definitions.ts +709 -0
  227. package/src/tools/executors/apply-patch-parser.ts +520 -0
  228. package/src/tools/executors/apply-patch.ts +359 -0
  229. package/src/tools/executors/bash.test.ts +87 -0
  230. package/src/tools/executors/bash.ts +207 -0
  231. package/src/tools/executors/editor.test.ts +35 -0
  232. package/src/tools/executors/editor.ts +219 -0
  233. package/src/tools/executors/file-read.test.ts +49 -0
  234. package/src/tools/executors/file-read.ts +110 -0
  235. package/src/tools/executors/index.ts +87 -0
  236. package/src/tools/executors/search.ts +278 -0
  237. package/src/tools/executors/web-fetch.ts +259 -0
  238. package/src/tools/helpers.ts +130 -0
  239. package/src/tools/index.ts +169 -0
  240. package/src/tools/model-tool-routing.test.ts +86 -0
  241. package/src/tools/model-tool-routing.ts +132 -0
  242. package/src/tools/presets.test.ts +62 -0
  243. package/src/tools/presets.ts +168 -0
  244. package/src/tools/schemas.ts +327 -0
  245. package/src/tools/types.ts +329 -0
  246. package/src/types/common.ts +26 -0
  247. package/src/types/config.ts +86 -0
  248. package/src/types/events.ts +74 -0
  249. package/src/types/index.ts +24 -0
  250. package/src/types/provider-settings.ts +43 -0
  251. package/src/types/sessions.ts +16 -0
  252. package/src/types/storage.ts +64 -0
  253. package/src/types/workspace.ts +7 -0
  254. package/src/types.ts +132 -0
  255. package/src/version.ts +3 -0
  256. package/dist/index.node.d.ts +0 -47
  257. package/dist/index.node.d.ts.map +0 -1
  258. package/dist/index.node.js +0 -948
  259. package/dist/telemetry/opentelemetry.d.ts.map +0 -1
  260. package/dist/telemetry/opentelemetry.js +0 -27
@@ -0,0 +1,296 @@
1
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import type { AgentConfig, Tool, ToolContext } from "@clinebot/agents";
5
+ import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
6
+ import { loadSandboxedPlugins } from "./plugin-sandbox";
7
+
8
+ function createApiCapture() {
9
+ const tools: Tool[] = [];
10
+ const api = {
11
+ registerTool: (tool: Tool) => tools.push(tool),
12
+ registerCommand: () => {},
13
+ registerShortcut: () => {},
14
+ registerFlag: () => {},
15
+ registerMessageRenderer: () => {},
16
+ registerProvider: () => {},
17
+ };
18
+ return { tools, api };
19
+ }
20
+
21
+ describe("plugin-sandbox", () => {
22
+ let dir = "";
23
+ let sharedSandbox:
24
+ | Awaited<ReturnType<typeof loadSandboxedPlugins>>
25
+ | undefined;
26
+ let sharedExtensions = new Map<
27
+ string,
28
+ NonNullable<AgentConfig["extensions"]>[number]
29
+ >();
30
+ const forwardedEvents: Array<{ name: string; payload?: unknown }> = [];
31
+
32
+ beforeAll(async () => {
33
+ dir = await mkdtemp(join(tmpdir(), "core-plugin-sandbox-"));
34
+
35
+ await writeFile(
36
+ join(dir, "plugin.mjs"),
37
+ [
38
+ "export default {",
39
+ " name: 'sandbox-test',",
40
+ " manifest: { capabilities: ['hooks','tools'], hookStages: ['input'] },",
41
+ " setup(api) {",
42
+ " api.registerTool({",
43
+ " name: 'sandbox_echo',",
44
+ " description: 'echo',",
45
+ " inputSchema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'] },",
46
+ " execute: async (input) => ({ echoed: input.value }),",
47
+ " });",
48
+ " },",
49
+ " onInput(ctx) { return { overrideInput: String(ctx.input || '').toUpperCase() }; }",
50
+ "};",
51
+ ].join("\n"),
52
+ "utf8",
53
+ );
54
+
55
+ await writeFile(
56
+ join(dir, "plugin-events.mjs"),
57
+ [
58
+ "export default {",
59
+ " name: 'sandbox-events',",
60
+ " manifest: { capabilities: ['tools'] },",
61
+ " setup(api) {",
62
+ " api.registerTool({",
63
+ " name: 'emit_event',",
64
+ " description: 'emit host event',",
65
+ " inputSchema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'] },",
66
+ " execute: async (input) => {",
67
+ " globalThis.__clinePluginHost?.emitEvent?.('test_event', { value: input.value });",
68
+ " return { ok: true };",
69
+ " },",
70
+ " });",
71
+ " },",
72
+ "};",
73
+ ].join("\n"),
74
+ "utf8",
75
+ );
76
+
77
+ await writeFile(
78
+ join(dir, "plugin-ts.ts"),
79
+ [
80
+ "const TOOL_NAME: string = 'sandbox_ts_echo';",
81
+ "export default {",
82
+ " name: 'sandbox-ts',",
83
+ " manifest: { capabilities: ['tools'] },",
84
+ " setup(api) {",
85
+ " api.registerTool({",
86
+ " name: TOOL_NAME,",
87
+ " description: 'echo',",
88
+ " inputSchema: { type: 'object', properties: { value: { type: 'string' } }, required: ['value'] },",
89
+ " execute: async (input) => ({ echoed: input.value }),",
90
+ " });",
91
+ " },",
92
+ "};",
93
+ ].join("\n"),
94
+ "utf8",
95
+ );
96
+
97
+ const localDepDir = join(dir, "node_modules", "sandbox-local-dep");
98
+ await mkdir(localDepDir, { recursive: true });
99
+ await writeFile(
100
+ join(localDepDir, "package.json"),
101
+ JSON.stringify({
102
+ name: "sandbox-local-dep",
103
+ type: "module",
104
+ exports: "./index.js",
105
+ }),
106
+ "utf8",
107
+ );
108
+ await writeFile(
109
+ join(localDepDir, "index.js"),
110
+ "export const depName = 'sandbox-local-dep';\n",
111
+ "utf8",
112
+ );
113
+ await writeFile(
114
+ join(dir, "plugin-dep.ts"),
115
+ [
116
+ "import { depName } from 'sandbox-local-dep';",
117
+ "export default {",
118
+ " name: depName,",
119
+ " manifest: { capabilities: ['tools'] },",
120
+ "};",
121
+ ].join("\n"),
122
+ "utf8",
123
+ );
124
+
125
+ const sdkDepDir = join(dir, "node_modules", "@clinebot", "shared");
126
+ await mkdir(sdkDepDir, { recursive: true });
127
+ await writeFile(
128
+ join(sdkDepDir, "package.json"),
129
+ JSON.stringify({
130
+ name: "@clinebot/shared",
131
+ type: "module",
132
+ exports: "./index.js",
133
+ }),
134
+ "utf8",
135
+ );
136
+ await writeFile(
137
+ join(sdkDepDir, "index.js"),
138
+ "export const sdkMarker = 'sandbox-plugin-installed-sdk';\n",
139
+ "utf8",
140
+ );
141
+ await writeFile(
142
+ join(dir, "plugin-sdk.ts"),
143
+ [
144
+ "import { sdkMarker } from '@clinebot/shared';",
145
+ "export default {",
146
+ " name: sdkMarker,",
147
+ " manifest: { capabilities: ['tools'] },",
148
+ "};",
149
+ ].join("\n"),
150
+ "utf8",
151
+ );
152
+
153
+ sharedSandbox = await loadSandboxedPlugins({
154
+ pluginPaths: [
155
+ join(dir, "plugin.mjs"),
156
+ join(dir, "plugin-events.mjs"),
157
+ join(dir, "plugin-ts.ts"),
158
+ join(dir, "plugin-dep.ts"),
159
+ join(dir, "plugin-sdk.ts"),
160
+ ],
161
+ onEvent: (event) => {
162
+ forwardedEvents.push(event);
163
+ },
164
+ });
165
+ sharedExtensions = new Map(
166
+ (sharedSandbox.extensions ?? []).map((extension) => [
167
+ extension.name,
168
+ extension,
169
+ ]),
170
+ );
171
+ });
172
+
173
+ beforeEach(() => {
174
+ forwardedEvents.length = 0;
175
+ });
176
+
177
+ afterAll(async () => {
178
+ await sharedSandbox?.shutdown();
179
+ if (dir) {
180
+ await rm(dir, { recursive: true, force: true });
181
+ }
182
+ });
183
+
184
+ it("runs plugin hooks and tool contributions in sandbox process", async () => {
185
+ expect(sharedSandbox?.extensions).toBeDefined();
186
+ const extension = sharedExtensions.get("sandbox-test");
187
+ expect(extension?.name).toBe("sandbox-test");
188
+ type AgentExtensionInputContext = Parameters<
189
+ NonNullable<NonNullable<AgentConfig["extensions"]>[number]["onInput"]>
190
+ >[0];
191
+ const inputContext: AgentExtensionInputContext = {
192
+ agentId: "agent-1",
193
+ conversationId: "conv-1",
194
+ parentAgentId: null,
195
+ mode: "run",
196
+ input: "hello",
197
+ };
198
+ const control = await extension?.onInput?.(inputContext);
199
+ expect(control?.overrideInput).toBe("HELLO");
200
+
201
+ const { tools, api } = createApiCapture();
202
+ await extension?.setup?.(api);
203
+ expect(tools.map((tool) => tool.name)).toContain("sandbox_echo");
204
+ const echoTool = tools.find((tool) => tool.name === "sandbox_echo");
205
+ expect(echoTool).toBeDefined();
206
+ const result = await echoTool?.execute({ value: "ok" }, {
207
+ agentId: "agent-1",
208
+ conversationId: "conv-1",
209
+ iteration: 1,
210
+ } as ToolContext);
211
+ expect(result).toEqual({ echoed: "ok" });
212
+ });
213
+
214
+ it("enforces hook timeout and cancels sandbox process", async () => {
215
+ const timeoutDir = await mkdtemp(
216
+ join(tmpdir(), "core-plugin-sandbox-timeout-"),
217
+ );
218
+ try {
219
+ const pluginPath = join(timeoutDir, "plugin-timeout.mjs");
220
+ await writeFile(
221
+ pluginPath,
222
+ [
223
+ "export default {",
224
+ " name: 'sandbox-timeout',",
225
+ " manifest: { capabilities: ['hooks'], hookStages: ['input'] },",
226
+ " onInput() { return new Promise(() => {}); }",
227
+ "};",
228
+ ].join("\n"),
229
+ "utf8",
230
+ );
231
+
232
+ const sandboxed = await loadSandboxedPlugins({
233
+ pluginPaths: [pluginPath],
234
+ hookTimeoutMs: 50,
235
+ });
236
+ const extension = sandboxed.extensions?.[0];
237
+ await expect(
238
+ extension?.onInput?.({
239
+ agentId: "agent-1",
240
+ conversationId: "conv-1",
241
+ parentAgentId: null,
242
+ mode: "run",
243
+ input: "hello",
244
+ }),
245
+ ).rejects.toThrow(/timed out/i);
246
+ await sandboxed.shutdown();
247
+ } finally {
248
+ await rm(timeoutDir, { recursive: true, force: true });
249
+ }
250
+ });
251
+
252
+ it("forwards sandbox plugin events to the host", async () => {
253
+ const extension = sharedExtensions.get("sandbox-events");
254
+ const { tools, api } = createApiCapture();
255
+ await extension?.setup?.(api);
256
+ const tool = tools.find((entry) => entry.name === "emit_event");
257
+ await tool?.execute({ value: "hello" }, {
258
+ agentId: "agent-1",
259
+ conversationId: "conv-1",
260
+ iteration: 1,
261
+ } as ToolContext);
262
+ expect(forwardedEvents).toEqual([
263
+ {
264
+ name: "test_event",
265
+ payload: { value: "hello" },
266
+ },
267
+ ]);
268
+ });
269
+
270
+ it("loads TypeScript plugins in the sandbox process", async () => {
271
+ const extension = sharedExtensions.get("sandbox-ts");
272
+ expect(extension?.name).toBe("sandbox-ts");
273
+ const { tools, api } = createApiCapture();
274
+ await extension?.setup?.(api);
275
+ const tool = tools.find((entry) => entry.name === "sandbox_ts_echo");
276
+ expect(tool).toBeDefined();
277
+ const result = await tool?.execute({ value: "ok" }, {
278
+ agentId: "agent-1",
279
+ conversationId: "conv-1",
280
+ iteration: 1,
281
+ } as ToolContext);
282
+ expect(result).toEqual({ echoed: "ok" });
283
+ });
284
+
285
+ it("resolves plugin-local dependencies in the sandbox process", async () => {
286
+ expect(sharedExtensions.get("sandbox-local-dep")?.name).toBe(
287
+ "sandbox-local-dep",
288
+ );
289
+ });
290
+
291
+ it("prefers plugin-installed SDK packages in the sandbox process", async () => {
292
+ expect(sharedExtensions.get("sandbox-plugin-installed-sdk")?.name).toBe(
293
+ "sandbox-plugin-installed-sdk",
294
+ );
295
+ });
296
+ });
@@ -0,0 +1,341 @@
1
+ import { existsSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import type { AgentConfig, Tool } from "@clinebot/agents";
5
+ import { SubprocessSandbox } from "../runtime/sandbox/subprocess-sandbox";
6
+
7
+ export interface PluginSandboxOptions {
8
+ pluginPaths: string[];
9
+ exportName?: string;
10
+ importTimeoutMs?: number;
11
+ hookTimeoutMs?: number;
12
+ contributionTimeoutMs?: number;
13
+ onEvent?: (event: { name: string; payload?: unknown }) => void;
14
+ }
15
+
16
+ type AgentExtension = NonNullable<AgentConfig["extensions"]>[number];
17
+ type AgentExtensionApi = Parameters<NonNullable<AgentExtension["setup"]>>[0];
18
+ type HookStage =
19
+ | "input"
20
+ | "runtime_event"
21
+ | "session_start"
22
+ | "before_agent_start"
23
+ | "tool_call_before"
24
+ | "tool_call_after"
25
+ | "turn_end"
26
+ | "session_shutdown"
27
+ | "error";
28
+
29
+ type SandboxedContributionDescriptor = {
30
+ id: string;
31
+ name: string;
32
+ description?: string;
33
+ inputSchema?: unknown;
34
+ timeoutMs?: number;
35
+ retryable?: boolean;
36
+ value?: string;
37
+ defaultValue?: boolean | string | number;
38
+ metadata?: Record<string, unknown>;
39
+ };
40
+
41
+ type SandboxedPluginDescriptor = {
42
+ pluginId: string;
43
+ name: string;
44
+ manifest: AgentExtension["manifest"];
45
+ contributions: {
46
+ tools: SandboxedContributionDescriptor[];
47
+ commands: SandboxedContributionDescriptor[];
48
+ shortcuts: SandboxedContributionDescriptor[];
49
+ flags: SandboxedContributionDescriptor[];
50
+ messageRenderers: SandboxedContributionDescriptor[];
51
+ providers: SandboxedContributionDescriptor[];
52
+ };
53
+ };
54
+
55
+ /**
56
+ * Resolve the bootstrap for the sandbox subprocess.
57
+ *
58
+ * In production (bundled), the compiled `.js` file lives next to this module
59
+ * and can be passed directly as a file to spawn. In development/test
60
+ * (unbundled, where only the `.ts` source exists), we load the TypeScript
61
+ * bootstrap through jiti from an inline script.
62
+ */
63
+ function resolveBootstrap(): { file: string } | { script: string } {
64
+ const dir = dirname(fileURLToPath(import.meta.url));
65
+ // In dev, the bootstrap sits next to this file in src/agents/.
66
+ // In production, the main bundle is at dist/ but bootstrap is at dist/agents/.
67
+ const candidates = [
68
+ join(dir, "plugin-sandbox-bootstrap.js"),
69
+ join(dir, "agents", "plugin-sandbox-bootstrap.js"),
70
+ ];
71
+ for (const candidate of candidates) {
72
+ if (existsSync(candidate)) return { file: candidate };
73
+ }
74
+ const tsPath = join(dir, "plugin-sandbox-bootstrap.ts");
75
+ return {
76
+ script: [
77
+ "const createJiti = require('jiti');",
78
+ `const jiti = createJiti(${JSON.stringify(tsPath)}, { cache: false, requireCache: false, esmResolve: true, interopDefault: false });`,
79
+ `Promise.resolve(jiti.import(${JSON.stringify(tsPath)}, {})).catch((error) => {`,
80
+ " console.error(error);",
81
+ " process.exitCode = 1;",
82
+ "});",
83
+ ].join("\n"),
84
+ };
85
+ }
86
+
87
+ const BOOTSTRAP = resolveBootstrap();
88
+
89
+ /**
90
+ * Map from hook stage names in the manifest to the property name on AgentExtension
91
+ * and the corresponding hook method name inside the sandbox subprocess.
92
+ */
93
+ const HOOK_BINDINGS: Array<{
94
+ stage: HookStage;
95
+ extensionKey: keyof AgentExtension;
96
+ sandboxHookName: string;
97
+ }> = [
98
+ { stage: "input", extensionKey: "onInput", sandboxHookName: "onInput" },
99
+ {
100
+ stage: "session_start",
101
+ extensionKey: "onSessionStart",
102
+ sandboxHookName: "onSessionStart",
103
+ },
104
+ {
105
+ stage: "before_agent_start",
106
+ extensionKey: "onBeforeAgentStart",
107
+ sandboxHookName: "onBeforeAgentStart",
108
+ },
109
+ {
110
+ stage: "tool_call_before",
111
+ extensionKey: "onToolCall",
112
+ sandboxHookName: "onToolCall",
113
+ },
114
+ {
115
+ stage: "tool_call_after",
116
+ extensionKey: "onToolResult",
117
+ sandboxHookName: "onToolResult",
118
+ },
119
+ {
120
+ stage: "turn_end",
121
+ extensionKey: "onAgentEnd",
122
+ sandboxHookName: "onAgentEnd",
123
+ },
124
+ {
125
+ stage: "session_shutdown",
126
+ extensionKey: "onSessionShutdown",
127
+ sandboxHookName: "onSessionShutdown",
128
+ },
129
+ {
130
+ stage: "runtime_event",
131
+ extensionKey: "onRuntimeEvent",
132
+ sandboxHookName: "onRuntimeEvent",
133
+ },
134
+ { stage: "error", extensionKey: "onError", sandboxHookName: "onError" },
135
+ ];
136
+
137
+ function hasHookStage(extension: AgentExtension, stage: HookStage): boolean {
138
+ return extension.manifest.hookStages?.includes(stage) === true;
139
+ }
140
+
141
+ function withTimeoutFallback(
142
+ timeoutMs: number | undefined,
143
+ fallback: number,
144
+ ): number {
145
+ return typeof timeoutMs === "number" && timeoutMs > 0 ? timeoutMs : fallback;
146
+ }
147
+
148
+ export async function loadSandboxedPlugins(
149
+ options: PluginSandboxOptions,
150
+ ): Promise<{
151
+ extensions: AgentConfig["extensions"];
152
+ shutdown: () => Promise<void>;
153
+ }> {
154
+ const sandbox = new SubprocessSandbox({
155
+ name: "plugin-sandbox",
156
+ ...("file" in BOOTSTRAP
157
+ ? { bootstrapFile: BOOTSTRAP.file }
158
+ : { bootstrapScript: BOOTSTRAP.script }),
159
+ onEvent: options.onEvent,
160
+ });
161
+ const importTimeoutMs = withTimeoutFallback(options.importTimeoutMs, 4000);
162
+ const hookTimeoutMs = withTimeoutFallback(options.hookTimeoutMs, 3000);
163
+ const contributionTimeoutMs = withTimeoutFallback(
164
+ options.contributionTimeoutMs,
165
+ 5000,
166
+ );
167
+
168
+ let descriptors: SandboxedPluginDescriptor[];
169
+ try {
170
+ descriptors = await sandbox.call<SandboxedPluginDescriptor[]>(
171
+ "initialize",
172
+ {
173
+ pluginPaths: options.pluginPaths,
174
+ exportName: options.exportName,
175
+ },
176
+ { timeoutMs: importTimeoutMs },
177
+ );
178
+ } catch (error) {
179
+ await sandbox.shutdown().catch(() => {
180
+ // Best-effort cleanup when sandbox initialization fails.
181
+ });
182
+ throw error;
183
+ }
184
+
185
+ const extensions: NonNullable<AgentConfig["extensions"]> = descriptors.map(
186
+ (descriptor) => {
187
+ const extension: AgentExtension = {
188
+ name: descriptor.name,
189
+ manifest: descriptor.manifest,
190
+ setup: (api: AgentExtensionApi) => {
191
+ registerTools(api, sandbox, descriptor, contributionTimeoutMs);
192
+ registerCommands(api, sandbox, descriptor, contributionTimeoutMs);
193
+ registerSimpleContributions(api, descriptor);
194
+ },
195
+ };
196
+
197
+ bindHooks(extension, sandbox, descriptor.pluginId, hookTimeoutMs);
198
+
199
+ return extension;
200
+ },
201
+ );
202
+
203
+ return {
204
+ extensions,
205
+ shutdown: async () => {
206
+ await sandbox.shutdown();
207
+ },
208
+ };
209
+ }
210
+
211
+ // ---------------------------------------------------------------------------
212
+ // Contribution registration helpers
213
+ // ---------------------------------------------------------------------------
214
+
215
+ function registerTools(
216
+ api: AgentExtensionApi,
217
+ sandbox: SubprocessSandbox,
218
+ descriptor: SandboxedPluginDescriptor,
219
+ timeoutMs: number,
220
+ ): void {
221
+ for (const td of descriptor.contributions.tools) {
222
+ const tool: Tool = {
223
+ name: td.name,
224
+ description: td.description ?? "",
225
+ inputSchema: (td.inputSchema ?? {
226
+ type: "object",
227
+ properties: {},
228
+ }) as Tool["inputSchema"],
229
+ timeoutMs: td.timeoutMs,
230
+ retryable: td.retryable,
231
+ execute: async (input: unknown, context: unknown) =>
232
+ await sandbox.call(
233
+ "executeTool",
234
+ {
235
+ pluginId: descriptor.pluginId,
236
+ contributionId: td.id,
237
+ input,
238
+ context,
239
+ },
240
+ { timeoutMs },
241
+ ),
242
+ };
243
+ api.registerTool(tool);
244
+ }
245
+ }
246
+
247
+ function registerCommands(
248
+ api: AgentExtensionApi,
249
+ sandbox: SubprocessSandbox,
250
+ descriptor: SandboxedPluginDescriptor,
251
+ timeoutMs: number,
252
+ ): void {
253
+ for (const cd of descriptor.contributions.commands) {
254
+ api.registerCommand({
255
+ name: cd.name,
256
+ description: cd.description,
257
+ handler: async (input: string) =>
258
+ await sandbox.call<string>(
259
+ "executeCommand",
260
+ {
261
+ pluginId: descriptor.pluginId,
262
+ contributionId: cd.id,
263
+ input,
264
+ },
265
+ { timeoutMs },
266
+ ),
267
+ });
268
+ }
269
+ }
270
+
271
+ function registerSimpleContributions(
272
+ api: AgentExtensionApi,
273
+ descriptor: SandboxedPluginDescriptor,
274
+ ): void {
275
+ for (const sd of descriptor.contributions.shortcuts) {
276
+ api.registerShortcut({
277
+ name: sd.name,
278
+ value: sd.value ?? "",
279
+ description: sd.description,
280
+ });
281
+ }
282
+
283
+ for (const fd of descriptor.contributions.flags) {
284
+ api.registerFlag({
285
+ name: fd.name,
286
+ description: fd.description,
287
+ defaultValue: fd.defaultValue,
288
+ });
289
+ }
290
+
291
+ for (const rd of descriptor.contributions.messageRenderers) {
292
+ api.registerMessageRenderer({
293
+ name: rd.name,
294
+ render: () => `[sandbox renderer ${rd.name} requires async bridge]`,
295
+ });
296
+ }
297
+
298
+ for (const pd of descriptor.contributions.providers) {
299
+ api.registerProvider({
300
+ name: pd.name,
301
+ description: pd.description,
302
+ metadata: pd.metadata,
303
+ });
304
+ }
305
+ }
306
+
307
+ function makeHookHandler(
308
+ sandbox: SubprocessSandbox,
309
+ pluginId: string,
310
+ hookName: string,
311
+ timeoutMs: number,
312
+ ): (payload: unknown) => Promise<unknown> {
313
+ return async (payload: unknown) =>
314
+ await sandbox.call(
315
+ "invokeHook",
316
+ { pluginId, hookName, payload },
317
+ { timeoutMs },
318
+ );
319
+ }
320
+
321
+ function bindHooks(
322
+ extension: AgentExtension,
323
+ sandbox: SubprocessSandbox,
324
+ pluginId: string,
325
+ hookTimeoutMs: number,
326
+ ): void {
327
+ for (const { stage, extensionKey, sandboxHookName } of HOOK_BINDINGS) {
328
+ if (hasHookStage(extension, stage)) {
329
+ const handler = makeHookHandler(
330
+ sandbox,
331
+ pluginId,
332
+ sandboxHookName,
333
+ hookTimeoutMs,
334
+ );
335
+ // Each hook property on AgentExtension accepts (payload: unknown) => Promise<unknown>.
336
+ // TypeScript cannot narrow a union of optional callback keys via computed access,
337
+ // so we use Object.assign to set the property safely.
338
+ Object.assign(extension, { [extensionKey]: handler });
339
+ }
340
+ }
341
+ }