@clinebot/core 0.0.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.
- package/README.md +88 -0
- package/dist/account/cline-account-service.d.ts +34 -0
- package/dist/account/index.d.ts +3 -0
- package/dist/account/rpc.d.ts +38 -0
- package/dist/account/types.d.ts +74 -0
- package/dist/agents/agent-config-loader.d.ts +18 -0
- package/dist/agents/agent-config-parser.d.ts +25 -0
- package/dist/agents/hooks-config-loader.d.ts +23 -0
- package/dist/agents/index.d.ts +11 -0
- package/dist/agents/plugin-config-loader.d.ts +22 -0
- package/dist/agents/plugin-loader.d.ts +9 -0
- package/dist/agents/plugin-sandbox.d.ts +12 -0
- package/dist/agents/unified-config-file-watcher.d.ts +77 -0
- package/dist/agents/user-instruction-config-loader.d.ts +63 -0
- package/dist/auth/client.d.ts +11 -0
- package/dist/auth/cline.d.ts +41 -0
- package/dist/auth/codex.d.ts +39 -0
- package/dist/auth/oca.d.ts +22 -0
- package/dist/auth/server.d.ts +22 -0
- package/dist/auth/types.d.ts +72 -0
- package/dist/auth/utils.d.ts +32 -0
- package/dist/chat/chat-schema.d.ts +145 -0
- package/dist/default-tools/constants.d.ts +23 -0
- package/dist/default-tools/definitions.d.ts +96 -0
- package/dist/default-tools/executors/apply-patch-parser.d.ts +68 -0
- package/dist/default-tools/executors/apply-patch.d.ts +26 -0
- package/dist/default-tools/executors/bash.d.ts +49 -0
- package/dist/default-tools/executors/editor.d.ts +31 -0
- package/dist/default-tools/executors/file-read.d.ts +40 -0
- package/dist/default-tools/executors/index.d.ts +44 -0
- package/dist/default-tools/executors/search.d.ts +50 -0
- package/dist/default-tools/executors/web-fetch.d.ts +58 -0
- package/dist/default-tools/index.d.ts +57 -0
- package/dist/default-tools/presets.d.ts +124 -0
- package/dist/default-tools/schemas.d.ts +121 -0
- package/dist/default-tools/types.d.ts +237 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +220 -0
- package/dist/input/file-indexer.d.ts +5 -0
- package/dist/input/index.d.ts +4 -0
- package/dist/input/mention-enricher.d.ts +12 -0
- package/dist/mcp/config-loader.d.ts +15 -0
- package/dist/mcp/index.d.ts +4 -0
- package/dist/mcp/manager.d.ts +24 -0
- package/dist/mcp/types.d.ts +66 -0
- package/dist/runtime/hook-file-hooks.d.ts +18 -0
- package/dist/runtime/rules.d.ts +5 -0
- package/dist/runtime/runtime-builder.d.ts +5 -0
- package/dist/runtime/sandbox/subprocess-sandbox.d.ts +19 -0
- package/dist/runtime/session-runtime.d.ts +36 -0
- package/dist/runtime/tool-approval.d.ts +9 -0
- package/dist/runtime/workflows.d.ts +13 -0
- package/dist/server/index.d.ts +47 -0
- package/dist/server/index.js +641 -0
- package/dist/session/default-session-manager.d.ts +77 -0
- package/dist/session/rpc-session-service.d.ts +12 -0
- package/dist/session/runtime-oauth-token-manager.d.ts +28 -0
- package/dist/session/session-artifacts.d.ts +19 -0
- package/dist/session/session-graph.d.ts +15 -0
- package/dist/session/session-host.d.ts +21 -0
- package/dist/session/session-manager.d.ts +50 -0
- package/dist/session/session-manifest.d.ts +30 -0
- package/dist/session/session-service.d.ts +113 -0
- package/dist/session/sqlite-rpc-session-backend.d.ts +30 -0
- package/dist/session/unified-session-persistence-service.d.ts +93 -0
- package/dist/session/workspace-manager.d.ts +28 -0
- package/dist/session/workspace-manifest.d.ts +25 -0
- package/dist/storage/provider-settings-legacy-migration.d.ts +13 -0
- package/dist/storage/provider-settings-manager.d.ts +20 -0
- package/dist/storage/sqlite-session-store.d.ts +29 -0
- package/dist/storage/sqlite-team-store.d.ts +31 -0
- package/dist/storage/team-store.d.ts +2 -0
- package/dist/team/index.d.ts +1 -0
- package/dist/team/projections.d.ts +8 -0
- package/dist/types/common.d.ts +10 -0
- package/dist/types/config.d.ts +37 -0
- package/dist/types/events.d.ts +54 -0
- package/dist/types/provider-settings.d.ts +20 -0
- package/dist/types/sessions.d.ts +9 -0
- package/dist/types/storage.d.ts +37 -0
- package/dist/types/workspace.d.ts +7 -0
- package/dist/types.d.ts +26 -0
- package/package.json +63 -0
- package/src/account/cline-account-service.test.ts +101 -0
- package/src/account/cline-account-service.ts +267 -0
- package/src/account/index.ts +20 -0
- package/src/account/rpc.test.ts +62 -0
- package/src/account/rpc.ts +172 -0
- package/src/account/types.ts +80 -0
- package/src/agents/agent-config-loader.test.ts +234 -0
- package/src/agents/agent-config-loader.ts +107 -0
- package/src/agents/agent-config-parser.ts +191 -0
- package/src/agents/hooks-config-loader.ts +97 -0
- package/src/agents/index.ts +84 -0
- package/src/agents/plugin-config-loader.test.ts +91 -0
- package/src/agents/plugin-config-loader.ts +160 -0
- package/src/agents/plugin-loader.test.ts +102 -0
- package/src/agents/plugin-loader.ts +105 -0
- package/src/agents/plugin-sandbox.test.ts +120 -0
- package/src/agents/plugin-sandbox.ts +471 -0
- package/src/agents/unified-config-file-watcher.test.ts +196 -0
- package/src/agents/unified-config-file-watcher.ts +483 -0
- package/src/agents/user-instruction-config-loader.test.ts +158 -0
- package/src/agents/user-instruction-config-loader.ts +438 -0
- package/src/auth/client.test.ts +40 -0
- package/src/auth/client.ts +25 -0
- package/src/auth/cline.test.ts +130 -0
- package/src/auth/cline.ts +414 -0
- package/src/auth/codex.test.ts +170 -0
- package/src/auth/codex.ts +466 -0
- package/src/auth/oca.test.ts +215 -0
- package/src/auth/oca.ts +546 -0
- package/src/auth/server.ts +216 -0
- package/src/auth/types.ts +78 -0
- package/src/auth/utils.test.ts +128 -0
- package/src/auth/utils.ts +247 -0
- package/src/chat/chat-schema.ts +82 -0
- package/src/default-tools/constants.ts +35 -0
- package/src/default-tools/definitions.test.ts +233 -0
- package/src/default-tools/definitions.ts +632 -0
- package/src/default-tools/executors/apply-patch-parser.ts +520 -0
- package/src/default-tools/executors/apply-patch.ts +359 -0
- package/src/default-tools/executors/bash.ts +205 -0
- package/src/default-tools/executors/editor.ts +231 -0
- package/src/default-tools/executors/file-read.test.ts +25 -0
- package/src/default-tools/executors/file-read.ts +94 -0
- package/src/default-tools/executors/index.ts +75 -0
- package/src/default-tools/executors/search.ts +278 -0
- package/src/default-tools/executors/web-fetch.ts +259 -0
- package/src/default-tools/index.ts +161 -0
- package/src/default-tools/presets.test.ts +63 -0
- package/src/default-tools/presets.ts +168 -0
- package/src/default-tools/schemas.ts +228 -0
- package/src/default-tools/types.ts +324 -0
- package/src/index.ts +119 -0
- package/src/input/file-indexer.d.ts +11 -0
- package/src/input/file-indexer.test.ts +87 -0
- package/src/input/file-indexer.ts +280 -0
- package/src/input/index.ts +7 -0
- package/src/input/mention-enricher.test.ts +82 -0
- package/src/input/mention-enricher.ts +119 -0
- package/src/mcp/config-loader.test.ts +238 -0
- package/src/mcp/config-loader.ts +219 -0
- package/src/mcp/index.ts +26 -0
- package/src/mcp/manager.test.ts +106 -0
- package/src/mcp/manager.ts +262 -0
- package/src/mcp/types.ts +88 -0
- package/src/runtime/hook-file-hooks.test.ts +106 -0
- package/src/runtime/hook-file-hooks.ts +736 -0
- package/src/runtime/index.ts +27 -0
- package/src/runtime/rules.ts +34 -0
- package/src/runtime/runtime-builder.team-persistence.test.ts +203 -0
- package/src/runtime/runtime-builder.test.ts +215 -0
- package/src/runtime/runtime-builder.ts +515 -0
- package/src/runtime/runtime-parity.test.ts +132 -0
- package/src/runtime/sandbox/subprocess-sandbox.ts +207 -0
- package/src/runtime/session-runtime.ts +44 -0
- package/src/runtime/tool-approval.ts +104 -0
- package/src/runtime/workflows.test.ts +119 -0
- package/src/runtime/workflows.ts +54 -0
- package/src/server/index.ts +282 -0
- package/src/session/default-session-manager.e2e.test.ts +354 -0
- package/src/session/default-session-manager.test.ts +816 -0
- package/src/session/default-session-manager.ts +1286 -0
- package/src/session/index.ts +37 -0
- package/src/session/rpc-session-service.ts +189 -0
- package/src/session/runtime-oauth-token-manager.test.ts +137 -0
- package/src/session/runtime-oauth-token-manager.ts +265 -0
- package/src/session/session-artifacts.ts +106 -0
- package/src/session/session-graph.ts +90 -0
- package/src/session/session-host.ts +190 -0
- package/src/session/session-manager.ts +56 -0
- package/src/session/session-manifest.ts +29 -0
- package/src/session/session-service.team-persistence.test.ts +48 -0
- package/src/session/session-service.ts +610 -0
- package/src/session/sqlite-rpc-session-backend.ts +303 -0
- package/src/session/unified-session-persistence-service.ts +781 -0
- package/src/session/workspace-manager.ts +98 -0
- package/src/session/workspace-manifest.ts +100 -0
- package/src/storage/artifact-store.ts +1 -0
- package/src/storage/index.ts +11 -0
- package/src/storage/provider-settings-legacy-migration.test.ts +175 -0
- package/src/storage/provider-settings-legacy-migration.ts +637 -0
- package/src/storage/provider-settings-manager.test.ts +111 -0
- package/src/storage/provider-settings-manager.ts +129 -0
- package/src/storage/session-store.ts +1 -0
- package/src/storage/sqlite-session-store.ts +270 -0
- package/src/storage/sqlite-team-store.ts +443 -0
- package/src/storage/team-store.ts +5 -0
- package/src/team/index.ts +4 -0
- package/src/team/projections.ts +285 -0
- package/src/types/common.ts +14 -0
- package/src/types/config.ts +64 -0
- package/src/types/events.ts +46 -0
- package/src/types/index.ts +24 -0
- package/src/types/provider-settings.ts +43 -0
- package/src/types/sessions.ts +16 -0
- package/src/types/storage.ts +64 -0
- package/src/types/workspace.ts +7 -0
- package/src/types.ts +127 -0
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
import type { AgentConfig, Tool } from "@clinebot/agents";
|
|
2
|
+
import { SubprocessSandbox } from "../runtime/sandbox/subprocess-sandbox";
|
|
3
|
+
|
|
4
|
+
export interface PluginSandboxOptions {
|
|
5
|
+
pluginPaths: string[];
|
|
6
|
+
exportName?: string;
|
|
7
|
+
importTimeoutMs?: number;
|
|
8
|
+
hookTimeoutMs?: number;
|
|
9
|
+
contributionTimeoutMs?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type AgentExtension = NonNullable<AgentConfig["extensions"]>[number];
|
|
13
|
+
type AgentExtensionApi = Parameters<NonNullable<AgentExtension["setup"]>>[0];
|
|
14
|
+
type HookStage =
|
|
15
|
+
| "input"
|
|
16
|
+
| "runtime_event"
|
|
17
|
+
| "session_start"
|
|
18
|
+
| "before_agent_start"
|
|
19
|
+
| "tool_call_before"
|
|
20
|
+
| "tool_call_after"
|
|
21
|
+
| "turn_end"
|
|
22
|
+
| "session_shutdown"
|
|
23
|
+
| "error";
|
|
24
|
+
|
|
25
|
+
type SandboxedContributionDescriptor = {
|
|
26
|
+
id: string;
|
|
27
|
+
name: string;
|
|
28
|
+
description?: string;
|
|
29
|
+
inputSchema?: unknown;
|
|
30
|
+
timeoutMs?: number;
|
|
31
|
+
retryable?: boolean;
|
|
32
|
+
value?: string;
|
|
33
|
+
defaultValue?: boolean | string | number;
|
|
34
|
+
metadata?: Record<string, unknown>;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type SandboxedPluginDescriptor = {
|
|
38
|
+
pluginId: string;
|
|
39
|
+
name: string;
|
|
40
|
+
manifest: AgentExtension["manifest"];
|
|
41
|
+
contributions: {
|
|
42
|
+
tools: SandboxedContributionDescriptor[];
|
|
43
|
+
commands: SandboxedContributionDescriptor[];
|
|
44
|
+
shortcuts: SandboxedContributionDescriptor[];
|
|
45
|
+
flags: SandboxedContributionDescriptor[];
|
|
46
|
+
messageRenderers: SandboxedContributionDescriptor[];
|
|
47
|
+
providers: SandboxedContributionDescriptor[];
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const PLUGIN_SANDBOX_BOOTSTRAP = `
|
|
52
|
+
const { pathToFileURL } = require("node:url");
|
|
53
|
+
let pluginCounter = 0;
|
|
54
|
+
const pluginState = new Map();
|
|
55
|
+
|
|
56
|
+
function toErrorPayload(error) {
|
|
57
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
58
|
+
const stack = error instanceof Error ? error.stack : undefined;
|
|
59
|
+
return { message, stack };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function sendResponse(id, ok, result, error) {
|
|
63
|
+
if (!process.send) return;
|
|
64
|
+
process.send({ type: "response", id, ok, result, error });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function sanitizeObject(value) {
|
|
68
|
+
if (!value || typeof value !== "object") return {};
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function initialize(args) {
|
|
73
|
+
const descriptors = [];
|
|
74
|
+
const exportName = (args && args.exportName) || "plugin";
|
|
75
|
+
for (const pluginPath of args.pluginPaths || []) {
|
|
76
|
+
const moduleExports = await import(pathToFileURL(pluginPath).href);
|
|
77
|
+
const plugin = moduleExports.default || moduleExports[exportName];
|
|
78
|
+
if (!plugin || typeof plugin !== "object") {
|
|
79
|
+
throw new Error(\`Invalid plugin module: \${pluginPath}\`);
|
|
80
|
+
}
|
|
81
|
+
if (typeof plugin.name !== "string" || !plugin.name) {
|
|
82
|
+
throw new Error(\`Invalid plugin name: \${pluginPath}\`);
|
|
83
|
+
}
|
|
84
|
+
if (!plugin.manifest || typeof plugin.manifest !== "object") {
|
|
85
|
+
throw new Error(\`Invalid plugin manifest: \${pluginPath}\`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const pluginId = \`plugin_\${++pluginCounter}\`;
|
|
89
|
+
const contributions = {
|
|
90
|
+
tools: [],
|
|
91
|
+
commands: [],
|
|
92
|
+
shortcuts: [],
|
|
93
|
+
flags: [],
|
|
94
|
+
messageRenderers: [],
|
|
95
|
+
providers: [],
|
|
96
|
+
};
|
|
97
|
+
const handlers = {
|
|
98
|
+
tools: new Map(),
|
|
99
|
+
commands: new Map(),
|
|
100
|
+
messageRenderers: new Map(),
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const makeId = (prefix) => \`\${pluginId}_\${prefix}_\${Math.random().toString(36).slice(2, 10)}\`;
|
|
104
|
+
const api = {
|
|
105
|
+
registerTool: (tool) => {
|
|
106
|
+
const id = makeId("tool");
|
|
107
|
+
handlers.tools.set(id, tool.execute);
|
|
108
|
+
contributions.tools.push({
|
|
109
|
+
id,
|
|
110
|
+
name: tool.name,
|
|
111
|
+
description: tool.description,
|
|
112
|
+
inputSchema: tool.inputSchema,
|
|
113
|
+
timeoutMs: tool.timeoutMs,
|
|
114
|
+
retryable: tool.retryable,
|
|
115
|
+
});
|
|
116
|
+
},
|
|
117
|
+
registerCommand: (command) => {
|
|
118
|
+
const id = makeId("command");
|
|
119
|
+
if (typeof command.handler === "function") {
|
|
120
|
+
handlers.commands.set(id, command.handler);
|
|
121
|
+
}
|
|
122
|
+
contributions.commands.push({
|
|
123
|
+
id,
|
|
124
|
+
name: command.name,
|
|
125
|
+
description: command.description,
|
|
126
|
+
});
|
|
127
|
+
},
|
|
128
|
+
registerShortcut: (shortcut) => {
|
|
129
|
+
contributions.shortcuts.push({
|
|
130
|
+
id: makeId("shortcut"),
|
|
131
|
+
name: shortcut.name,
|
|
132
|
+
value: shortcut.value,
|
|
133
|
+
description: shortcut.description,
|
|
134
|
+
});
|
|
135
|
+
},
|
|
136
|
+
registerFlag: (flag) => {
|
|
137
|
+
contributions.flags.push({
|
|
138
|
+
id: makeId("flag"),
|
|
139
|
+
name: flag.name,
|
|
140
|
+
description: flag.description,
|
|
141
|
+
defaultValue: flag.defaultValue,
|
|
142
|
+
});
|
|
143
|
+
},
|
|
144
|
+
registerMessageRenderer: (renderer) => {
|
|
145
|
+
const id = makeId("renderer");
|
|
146
|
+
handlers.messageRenderers.set(id, renderer.render);
|
|
147
|
+
contributions.messageRenderers.push({ id, name: renderer.name });
|
|
148
|
+
},
|
|
149
|
+
registerProvider: (provider) => {
|
|
150
|
+
contributions.providers.push({
|
|
151
|
+
id: makeId("provider"),
|
|
152
|
+
name: provider.name,
|
|
153
|
+
description: provider.description,
|
|
154
|
+
metadata: sanitizeObject(provider.metadata),
|
|
155
|
+
});
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
if (typeof plugin.setup === "function") {
|
|
160
|
+
await plugin.setup(api);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
pluginState.set(pluginId, { plugin, handlers });
|
|
164
|
+
descriptors.push({
|
|
165
|
+
pluginId,
|
|
166
|
+
name: plugin.name,
|
|
167
|
+
manifest: plugin.manifest,
|
|
168
|
+
contributions,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
return descriptors;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function getPlugin(pluginId) {
|
|
175
|
+
const state = pluginState.get(pluginId);
|
|
176
|
+
if (!state) {
|
|
177
|
+
throw new Error(\`Unknown sandbox plugin id: \${pluginId}\`);
|
|
178
|
+
}
|
|
179
|
+
return state;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function invokeHook(args) {
|
|
183
|
+
const state = getPlugin(args.pluginId);
|
|
184
|
+
const handler = state.plugin[args.hookName];
|
|
185
|
+
if (typeof handler !== "function") {
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
return await handler(args.payload);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function executeTool(args) {
|
|
192
|
+
const state = getPlugin(args.pluginId);
|
|
193
|
+
const handler = state.handlers.tools.get(args.contributionId);
|
|
194
|
+
if (typeof handler !== "function") {
|
|
195
|
+
throw new Error("Unknown sandbox tool contribution");
|
|
196
|
+
}
|
|
197
|
+
return await handler(args.input, args.context);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function executeCommand(args) {
|
|
201
|
+
const state = getPlugin(args.pluginId);
|
|
202
|
+
const handler = state.handlers.commands.get(args.contributionId);
|
|
203
|
+
if (typeof handler !== "function") {
|
|
204
|
+
return "";
|
|
205
|
+
}
|
|
206
|
+
return await handler(args.input);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function renderMessage(args) {
|
|
210
|
+
const state = getPlugin(args.pluginId);
|
|
211
|
+
const handler = state.handlers.messageRenderers.get(args.contributionId);
|
|
212
|
+
if (typeof handler !== "function") {
|
|
213
|
+
return "";
|
|
214
|
+
}
|
|
215
|
+
return await handler(args.message);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const methods = { initialize, invokeHook, executeTool, executeCommand, renderMessage };
|
|
219
|
+
|
|
220
|
+
process.on("message", async (message) => {
|
|
221
|
+
if (!message || message.type !== "call") {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const method = methods[message.method];
|
|
225
|
+
if (!method) {
|
|
226
|
+
sendResponse(message.id, false, undefined, { message: \`Unknown method: \${String(message.method)}\` });
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
try {
|
|
230
|
+
const result = await method(message.args || {});
|
|
231
|
+
sendResponse(message.id, true, result);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
sendResponse(message.id, false, undefined, toErrorPayload(error));
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
`;
|
|
237
|
+
|
|
238
|
+
function hasHookStage(extension: AgentExtension, stage: HookStage): boolean {
|
|
239
|
+
return extension.manifest.hookStages?.includes(stage) === true;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function withTimeoutFallback(
|
|
243
|
+
timeoutMs: number | undefined,
|
|
244
|
+
fallback: number,
|
|
245
|
+
): number {
|
|
246
|
+
return typeof timeoutMs === "number" && timeoutMs > 0 ? timeoutMs : fallback;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export async function loadSandboxedPlugins(
|
|
250
|
+
options: PluginSandboxOptions,
|
|
251
|
+
): Promise<{
|
|
252
|
+
extensions: AgentConfig["extensions"];
|
|
253
|
+
shutdown: () => Promise<void>;
|
|
254
|
+
}> {
|
|
255
|
+
const sandbox = new SubprocessSandbox({
|
|
256
|
+
name: "plugin-sandbox",
|
|
257
|
+
bootstrapScript: PLUGIN_SANDBOX_BOOTSTRAP,
|
|
258
|
+
});
|
|
259
|
+
const importTimeoutMs = withTimeoutFallback(options.importTimeoutMs, 4000);
|
|
260
|
+
const hookTimeoutMs = withTimeoutFallback(options.hookTimeoutMs, 3000);
|
|
261
|
+
const contributionTimeoutMs = withTimeoutFallback(
|
|
262
|
+
options.contributionTimeoutMs,
|
|
263
|
+
5000,
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
let descriptors: SandboxedPluginDescriptor[];
|
|
267
|
+
try {
|
|
268
|
+
descriptors = await sandbox.call<SandboxedPluginDescriptor[]>(
|
|
269
|
+
"initialize",
|
|
270
|
+
{
|
|
271
|
+
pluginPaths: options.pluginPaths,
|
|
272
|
+
exportName: options.exportName,
|
|
273
|
+
},
|
|
274
|
+
{ timeoutMs: importTimeoutMs },
|
|
275
|
+
);
|
|
276
|
+
} catch (error) {
|
|
277
|
+
await sandbox.shutdown().catch(() => {
|
|
278
|
+
// Best-effort cleanup when sandbox initialization fails.
|
|
279
|
+
});
|
|
280
|
+
throw error;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const extensions: NonNullable<AgentConfig["extensions"]> = descriptors.map(
|
|
284
|
+
(descriptor) => {
|
|
285
|
+
const extension: AgentExtension = {
|
|
286
|
+
name: descriptor.name,
|
|
287
|
+
manifest: descriptor.manifest,
|
|
288
|
+
setup: (api: AgentExtensionApi) => {
|
|
289
|
+
for (const toolDescriptor of descriptor.contributions.tools) {
|
|
290
|
+
const tool: Tool = {
|
|
291
|
+
name: toolDescriptor.name,
|
|
292
|
+
description: toolDescriptor.description ?? "",
|
|
293
|
+
inputSchema: (toolDescriptor.inputSchema ?? {
|
|
294
|
+
type: "object",
|
|
295
|
+
properties: {},
|
|
296
|
+
}) as Tool["inputSchema"],
|
|
297
|
+
timeoutMs: toolDescriptor.timeoutMs,
|
|
298
|
+
retryable: toolDescriptor.retryable,
|
|
299
|
+
execute: async (input: unknown, context: unknown) =>
|
|
300
|
+
await sandbox.call(
|
|
301
|
+
"executeTool",
|
|
302
|
+
{
|
|
303
|
+
pluginId: descriptor.pluginId,
|
|
304
|
+
contributionId: toolDescriptor.id,
|
|
305
|
+
input,
|
|
306
|
+
context,
|
|
307
|
+
},
|
|
308
|
+
{ timeoutMs: contributionTimeoutMs },
|
|
309
|
+
),
|
|
310
|
+
};
|
|
311
|
+
api.registerTool(tool);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
for (const commandDescriptor of descriptor.contributions.commands) {
|
|
315
|
+
api.registerCommand({
|
|
316
|
+
name: commandDescriptor.name,
|
|
317
|
+
description: commandDescriptor.description,
|
|
318
|
+
handler: async (input: string) =>
|
|
319
|
+
await sandbox.call<string>(
|
|
320
|
+
"executeCommand",
|
|
321
|
+
{
|
|
322
|
+
pluginId: descriptor.pluginId,
|
|
323
|
+
contributionId: commandDescriptor.id,
|
|
324
|
+
input,
|
|
325
|
+
},
|
|
326
|
+
{ timeoutMs: contributionTimeoutMs },
|
|
327
|
+
),
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
for (const shortcutDescriptor of descriptor.contributions.shortcuts) {
|
|
332
|
+
api.registerShortcut({
|
|
333
|
+
name: shortcutDescriptor.name,
|
|
334
|
+
value: shortcutDescriptor.value ?? "",
|
|
335
|
+
description: shortcutDescriptor.description,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
for (const flagDescriptor of descriptor.contributions.flags) {
|
|
340
|
+
api.registerFlag({
|
|
341
|
+
name: flagDescriptor.name,
|
|
342
|
+
description: flagDescriptor.description,
|
|
343
|
+
defaultValue: flagDescriptor.defaultValue,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
for (const rendererDescriptor of descriptor.contributions
|
|
348
|
+
.messageRenderers) {
|
|
349
|
+
api.registerMessageRenderer({
|
|
350
|
+
name: rendererDescriptor.name,
|
|
351
|
+
render: () =>
|
|
352
|
+
`[sandbox renderer ${rendererDescriptor.name} requires async bridge]`,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
for (const providerDescriptor of descriptor.contributions.providers) {
|
|
357
|
+
api.registerProvider({
|
|
358
|
+
name: providerDescriptor.name,
|
|
359
|
+
description: providerDescriptor.description,
|
|
360
|
+
metadata: providerDescriptor.metadata,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
if (hasHookStage(extension, "input")) {
|
|
367
|
+
extension.onInput = async (payload: unknown) =>
|
|
368
|
+
await sandbox.call(
|
|
369
|
+
"invokeHook",
|
|
370
|
+
{ pluginId: descriptor.pluginId, hookName: "onInput", payload },
|
|
371
|
+
{ timeoutMs: hookTimeoutMs },
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
if (hasHookStage(extension, "session_start")) {
|
|
375
|
+
extension.onSessionStart = async (payload: unknown) =>
|
|
376
|
+
await sandbox.call(
|
|
377
|
+
"invokeHook",
|
|
378
|
+
{
|
|
379
|
+
pluginId: descriptor.pluginId,
|
|
380
|
+
hookName: "onSessionStart",
|
|
381
|
+
payload,
|
|
382
|
+
},
|
|
383
|
+
{ timeoutMs: hookTimeoutMs },
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
if (hasHookStage(extension, "before_agent_start")) {
|
|
387
|
+
extension.onBeforeAgentStart = async (payload: unknown) =>
|
|
388
|
+
await sandbox.call(
|
|
389
|
+
"invokeHook",
|
|
390
|
+
{
|
|
391
|
+
pluginId: descriptor.pluginId,
|
|
392
|
+
hookName: "onBeforeAgentStart",
|
|
393
|
+
payload,
|
|
394
|
+
},
|
|
395
|
+
{ timeoutMs: hookTimeoutMs },
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
if (hasHookStage(extension, "tool_call_before")) {
|
|
399
|
+
extension.onToolCall = async (payload: unknown) =>
|
|
400
|
+
await sandbox.call(
|
|
401
|
+
"invokeHook",
|
|
402
|
+
{ pluginId: descriptor.pluginId, hookName: "onToolCall", payload },
|
|
403
|
+
{ timeoutMs: hookTimeoutMs },
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
if (hasHookStage(extension, "tool_call_after")) {
|
|
407
|
+
extension.onToolResult = async (payload: unknown) =>
|
|
408
|
+
await sandbox.call(
|
|
409
|
+
"invokeHook",
|
|
410
|
+
{
|
|
411
|
+
pluginId: descriptor.pluginId,
|
|
412
|
+
hookName: "onToolResult",
|
|
413
|
+
payload,
|
|
414
|
+
},
|
|
415
|
+
{ timeoutMs: hookTimeoutMs },
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
if (hasHookStage(extension, "turn_end")) {
|
|
419
|
+
extension.onAgentEnd = async (payload: unknown) =>
|
|
420
|
+
await sandbox.call(
|
|
421
|
+
"invokeHook",
|
|
422
|
+
{ pluginId: descriptor.pluginId, hookName: "onAgentEnd", payload },
|
|
423
|
+
{ timeoutMs: hookTimeoutMs },
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
if (hasHookStage(extension, "session_shutdown")) {
|
|
427
|
+
extension.onSessionShutdown = async (payload: unknown) =>
|
|
428
|
+
await sandbox.call(
|
|
429
|
+
"invokeHook",
|
|
430
|
+
{
|
|
431
|
+
pluginId: descriptor.pluginId,
|
|
432
|
+
hookName: "onSessionShutdown",
|
|
433
|
+
payload,
|
|
434
|
+
},
|
|
435
|
+
{ timeoutMs: hookTimeoutMs },
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
if (hasHookStage(extension, "runtime_event")) {
|
|
439
|
+
extension.onRuntimeEvent = async (payload: unknown) => {
|
|
440
|
+
await sandbox.call(
|
|
441
|
+
"invokeHook",
|
|
442
|
+
{
|
|
443
|
+
pluginId: descriptor.pluginId,
|
|
444
|
+
hookName: "onRuntimeEvent",
|
|
445
|
+
payload,
|
|
446
|
+
},
|
|
447
|
+
{ timeoutMs: hookTimeoutMs },
|
|
448
|
+
);
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
if (hasHookStage(extension, "error")) {
|
|
452
|
+
extension.onError = async (payload: unknown) => {
|
|
453
|
+
await sandbox.call(
|
|
454
|
+
"invokeHook",
|
|
455
|
+
{ pluginId: descriptor.pluginId, hookName: "onError", payload },
|
|
456
|
+
{ timeoutMs: hookTimeoutMs },
|
|
457
|
+
);
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return extension;
|
|
462
|
+
},
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
return {
|
|
466
|
+
extensions,
|
|
467
|
+
shutdown: async () => {
|
|
468
|
+
await sandbox.shutdown();
|
|
469
|
+
},
|
|
470
|
+
};
|
|
471
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, rm, unlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { basename, join } from "node:path";
|
|
4
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
+
import {
|
|
6
|
+
type AgentYamlConfig,
|
|
7
|
+
parseAgentConfigFromYaml,
|
|
8
|
+
} from "./agent-config-loader";
|
|
9
|
+
import {
|
|
10
|
+
UnifiedConfigFileWatcher,
|
|
11
|
+
type UnifiedConfigWatcherEvent,
|
|
12
|
+
} from "./unified-config-file-watcher";
|
|
13
|
+
|
|
14
|
+
const WAIT_TIMEOUT_MS = 8_000;
|
|
15
|
+
const WAIT_INTERVAL_MS = 25;
|
|
16
|
+
|
|
17
|
+
async function waitForEvent<TType extends string, TItem>(
|
|
18
|
+
events: Array<UnifiedConfigWatcherEvent<TType, TItem>>,
|
|
19
|
+
predicate: (event: UnifiedConfigWatcherEvent<TType, TItem>) => boolean,
|
|
20
|
+
timeoutMs = WAIT_TIMEOUT_MS,
|
|
21
|
+
): Promise<UnifiedConfigWatcherEvent<TType, TItem>> {
|
|
22
|
+
const startedAt = Date.now();
|
|
23
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
24
|
+
const match = events.find(predicate);
|
|
25
|
+
if (match) {
|
|
26
|
+
return match;
|
|
27
|
+
}
|
|
28
|
+
await new Promise((resolve) => setTimeout(resolve, WAIT_INTERVAL_MS));
|
|
29
|
+
}
|
|
30
|
+
throw new Error("Timed out waiting for watcher event.");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("UnifiedConfigFileWatcher", () => {
|
|
34
|
+
const tempRoots: string[] = [];
|
|
35
|
+
|
|
36
|
+
afterEach(async () => {
|
|
37
|
+
await Promise.all(
|
|
38
|
+
tempRoots.map((dir) => rm(dir, { recursive: true, force: true })),
|
|
39
|
+
);
|
|
40
|
+
tempRoots.length = 0;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("emits upsert and remove events with config type for agent configs", async () => {
|
|
44
|
+
const tempRoot = await mkdtemp(
|
|
45
|
+
join(tmpdir(), "core-unified-config-watcher-"),
|
|
46
|
+
);
|
|
47
|
+
tempRoots.push(tempRoot);
|
|
48
|
+
const agentsDir = join(tempRoot, "agents");
|
|
49
|
+
await mkdir(agentsDir, { recursive: true });
|
|
50
|
+
const agentFilePath = join(agentsDir, "reviewer.yaml");
|
|
51
|
+
await writeFile(
|
|
52
|
+
agentFilePath,
|
|
53
|
+
`---
|
|
54
|
+
name: Reviewer
|
|
55
|
+
description: Reviews patches
|
|
56
|
+
tools: read_files
|
|
57
|
+
---
|
|
58
|
+
Review code carefully.`,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const watcher = new UnifiedConfigFileWatcher([
|
|
62
|
+
{
|
|
63
|
+
type: "agent" as const,
|
|
64
|
+
directories: [agentsDir],
|
|
65
|
+
includeFile: (fileName) => /\.(yaml|yml)$/i.test(fileName),
|
|
66
|
+
parseFile: (context) => parseAgentConfigFromYaml(context.content),
|
|
67
|
+
resolveId: (config) => config.name.toLowerCase(),
|
|
68
|
+
},
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
const events: Array<
|
|
72
|
+
UnifiedConfigWatcherEvent<
|
|
73
|
+
"agent",
|
|
74
|
+
ReturnType<typeof parseAgentConfigFromYaml>
|
|
75
|
+
>
|
|
76
|
+
> = [];
|
|
77
|
+
const unsubscribe = watcher.subscribe((event) => events.push(event));
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
await watcher.start();
|
|
81
|
+
await waitForEvent(
|
|
82
|
+
events,
|
|
83
|
+
(event) => event.kind === "upsert" && event.record.id === "reviewer",
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
events.length = 0;
|
|
87
|
+
await writeFile(
|
|
88
|
+
agentFilePath,
|
|
89
|
+
`---
|
|
90
|
+
name: Reviewer
|
|
91
|
+
description: Reviews patches
|
|
92
|
+
---
|
|
93
|
+
Review code with strictness.`,
|
|
94
|
+
);
|
|
95
|
+
await watcher.refreshType("agent");
|
|
96
|
+
await waitForEvent(
|
|
97
|
+
events,
|
|
98
|
+
(event) => event.kind === "upsert" && event.record.type === "agent",
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
events.length = 0;
|
|
102
|
+
await unlink(agentFilePath);
|
|
103
|
+
await watcher.refreshType("agent");
|
|
104
|
+
await waitForEvent(
|
|
105
|
+
events,
|
|
106
|
+
(event) => event.kind === "remove" && event.type === "agent",
|
|
107
|
+
);
|
|
108
|
+
} finally {
|
|
109
|
+
unsubscribe();
|
|
110
|
+
watcher.stop();
|
|
111
|
+
}
|
|
112
|
+
}, 15_000);
|
|
113
|
+
|
|
114
|
+
it("supports one watcher instance for multiple config types", async () => {
|
|
115
|
+
const tempRoot = await mkdtemp(
|
|
116
|
+
join(tmpdir(), "core-unified-config-watcher-"),
|
|
117
|
+
);
|
|
118
|
+
tempRoots.push(tempRoot);
|
|
119
|
+
const agentsDir = join(tempRoot, "agents");
|
|
120
|
+
const skillsDir = join(tempRoot, "skills");
|
|
121
|
+
await mkdir(agentsDir, { recursive: true });
|
|
122
|
+
await mkdir(skillsDir, { recursive: true });
|
|
123
|
+
|
|
124
|
+
await writeFile(
|
|
125
|
+
join(agentsDir, "researcher.yaml"),
|
|
126
|
+
`---
|
|
127
|
+
name: Researcher
|
|
128
|
+
description: Finds context
|
|
129
|
+
---
|
|
130
|
+
Investigate related code paths.`,
|
|
131
|
+
);
|
|
132
|
+
await writeFile(
|
|
133
|
+
join(skillsDir, "SKILL.md"),
|
|
134
|
+
`---
|
|
135
|
+
name: incident-response
|
|
136
|
+
description: Handle incidents
|
|
137
|
+
---
|
|
138
|
+
Escalation playbook`,
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
const watcher = new UnifiedConfigFileWatcher<
|
|
142
|
+
"agent" | "skill",
|
|
143
|
+
AgentYamlConfig | { path: string }
|
|
144
|
+
>([
|
|
145
|
+
{
|
|
146
|
+
type: "agent" as const,
|
|
147
|
+
directories: [agentsDir],
|
|
148
|
+
includeFile: (fileName) => /\.(yaml|yml)$/i.test(fileName),
|
|
149
|
+
parseFile: (context) =>
|
|
150
|
+
parseAgentConfigFromYaml(context.content) as
|
|
151
|
+
| AgentYamlConfig
|
|
152
|
+
| { path: string },
|
|
153
|
+
resolveId: (config) =>
|
|
154
|
+
"name" in config ? config.name.toLowerCase() : "invalid-agent-config",
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
type: "skill" as const,
|
|
158
|
+
directories: [skillsDir],
|
|
159
|
+
includeFile: (fileName) => fileName === "SKILL.md",
|
|
160
|
+
parseFile: (context) => ({ path: context.filePath }),
|
|
161
|
+
resolveId: (_parsed, context) => basename(context.directoryPath),
|
|
162
|
+
},
|
|
163
|
+
]);
|
|
164
|
+
|
|
165
|
+
const events: Array<
|
|
166
|
+
UnifiedConfigWatcherEvent<
|
|
167
|
+
"agent" | "skill",
|
|
168
|
+
AgentYamlConfig | { path: string }
|
|
169
|
+
>
|
|
170
|
+
> = [];
|
|
171
|
+
const unsubscribe = watcher.subscribe((event) => events.push(event));
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
await watcher.start();
|
|
175
|
+
await waitForEvent(
|
|
176
|
+
events,
|
|
177
|
+
(event) =>
|
|
178
|
+
event.kind === "upsert" &&
|
|
179
|
+
(event.record.type === "agent" || event.record.type === "skill"),
|
|
180
|
+
);
|
|
181
|
+
expect(
|
|
182
|
+
events.some(
|
|
183
|
+
(event) => event.kind === "upsert" && event.record.type === "agent",
|
|
184
|
+
),
|
|
185
|
+
).toBe(true);
|
|
186
|
+
expect(
|
|
187
|
+
events.some(
|
|
188
|
+
(event) => event.kind === "upsert" && event.record.type === "skill",
|
|
189
|
+
),
|
|
190
|
+
).toBe(true);
|
|
191
|
+
} finally {
|
|
192
|
+
unsubscribe();
|
|
193
|
+
watcher.stop();
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
});
|