@clinebot/core 0.0.33 → 0.0.34
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/dist/auth/client.d.ts +19 -0
- package/dist/auth/client.d.ts.map +1 -1
- package/dist/auth/cline.d.ts.map +1 -1
- package/dist/auth/oca.d.ts.map +1 -1
- package/dist/auth/server.d.ts +32 -0
- package/dist/auth/server.d.ts.map +1 -1
- package/dist/auth/types.d.ts +29 -0
- package/dist/auth/types.d.ts.map +1 -1
- package/dist/extensions/index.d.ts +2 -1
- package/dist/extensions/index.d.ts.map +1 -1
- package/dist/extensions/plugin/plugin-config-loader.d.ts +2 -1
- package/dist/extensions/plugin/plugin-config-loader.d.ts.map +1 -1
- package/dist/extensions/plugin/plugin-load-report.d.ts +19 -0
- package/dist/extensions/plugin/plugin-load-report.d.ts.map +1 -0
- package/dist/extensions/plugin/plugin-loader.d.ts +6 -0
- package/dist/extensions/plugin/plugin-loader.d.ts.map +1 -1
- package/dist/extensions/plugin/plugin-sandbox.d.ts +2 -1
- package/dist/extensions/plugin/plugin-sandbox.d.ts.map +1 -1
- package/dist/extensions/plugin-sandbox-bootstrap.js +148 -148
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +207 -207
- package/dist/runtime/runtime-builder.d.ts +1 -1
- package/dist/runtime/runtime-builder.d.ts.map +1 -1
- package/dist/runtime/subprocess-sandbox.d.ts +2 -0
- package/dist/runtime/subprocess-sandbox.d.ts.map +1 -1
- package/dist/runtime/tool-approval.d.ts.map +1 -1
- package/dist/session/default-session-manager.d.ts.map +1 -1
- package/dist/session/persistence-service.d.ts.map +1 -1
- package/dist/session/session-artifacts.d.ts +2 -0
- package/dist/session/session-artifacts.d.ts.map +1 -1
- package/dist/session/session-config-builder.d.ts.map +1 -1
- package/dist/team/team-tools.d.ts.map +1 -1
- package/dist/types/config.d.ts +1 -0
- package/dist/types/config.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/auth/client.test.ts +29 -0
- package/src/auth/client.ts +21 -0
- package/src/auth/cline.ts +2 -0
- package/src/auth/oca.ts +2 -0
- package/src/auth/server.test.ts +287 -0
- package/src/auth/server.ts +50 -1
- package/src/auth/types.ts +29 -0
- package/src/extensions/index.ts +6 -0
- package/src/extensions/plugin/plugin-config-loader.test.ts +37 -0
- package/src/extensions/plugin/plugin-config-loader.ts +18 -10
- package/src/extensions/plugin/plugin-load-report.ts +20 -0
- package/src/extensions/plugin/plugin-loader.test.ts +45 -0
- package/src/extensions/plugin/plugin-loader.ts +57 -3
- package/src/extensions/plugin/plugin-sandbox-bootstrap.ts +158 -86
- package/src/extensions/plugin/plugin-sandbox.test.ts +70 -0
- package/src/extensions/plugin/plugin-sandbox.ts +17 -6
- package/src/index.ts +11 -0
- package/src/runtime/hook-file-hooks.test.ts +42 -7
- package/src/runtime/runtime-builder.test.ts +98 -0
- package/src/runtime/runtime-builder.ts +112 -65
- package/src/runtime/subprocess-sandbox.ts +26 -23
- package/src/runtime/tool-approval.ts +13 -15
- package/src/session/default-session-manager.ts +1 -3
- package/src/session/persistence-service.test.ts +38 -0
- package/src/session/persistence-service.ts +16 -1
- package/src/session/session-artifacts.ts +16 -0
- package/src/session/session-config-builder.ts +46 -0
- package/src/team/team-tools.test.ts +104 -0
- package/src/team/team-tools.ts +35 -16
- package/src/types/config.ts +1 -0
- package/dist/runtime/team-runtime-registry.d.ts +0 -13
- package/dist/runtime/team-runtime-registry.d.ts.map +0 -1
- package/src/runtime/team-runtime-registry.ts +0 -43
package/src/auth/types.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ITelemetryService } from "@clinebot/shared";
|
|
2
|
+
import type { OAuthServerCloseInfo, OAuthServerListeningInfo } from "./server";
|
|
2
3
|
|
|
3
4
|
export interface OAuthPrompt {
|
|
4
5
|
message: string;
|
|
@@ -31,6 +32,34 @@ export interface OAuthLoginCallbacks {
|
|
|
31
32
|
onPrompt: (prompt: OAuthPrompt) => Promise<string>;
|
|
32
33
|
onProgress?: (message: string) => void;
|
|
33
34
|
onManualCodeInput?: () => Promise<string>;
|
|
35
|
+
/**
|
|
36
|
+
* Called when the local OAuth redirect server successfully binds to a port
|
|
37
|
+
* and is ready to receive the browser callback. The `info` object contains
|
|
38
|
+
* the host, the bound port number, and the full `callbackUrl`.
|
|
39
|
+
*
|
|
40
|
+
* Use this to:
|
|
41
|
+
* - Show a "waiting for OAuth callback on port N" status indicator in your UI.
|
|
42
|
+
* - Forward the port in remote-development environments (e.g. JetBrains
|
|
43
|
+
* Gateway) from the remote machine to the machine running the browser.
|
|
44
|
+
*
|
|
45
|
+
* Paired with `onServerClose` for teardown.
|
|
46
|
+
*
|
|
47
|
+
* Only fired when the provider uses a local callback server
|
|
48
|
+
* (`OAuthProviderInterface.usesCallbackServer === true`).
|
|
49
|
+
*/
|
|
50
|
+
onServerListening?: (info: OAuthServerListeningInfo) => void | Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* Called when the local OAuth redirect server closes — either because the
|
|
53
|
+
* callback was received, the flow was cancelled, or the timeout elapsed.
|
|
54
|
+
*
|
|
55
|
+
* Use this to:
|
|
56
|
+
* - Clear any "waiting for callback" status UI shown in `onServerListening`.
|
|
57
|
+
* - Tear down port-forwards set up in `onServerListening`.
|
|
58
|
+
*
|
|
59
|
+
* Only fired when the provider uses a local callback server
|
|
60
|
+
* (`OAuthProviderInterface.usesCallbackServer === true`).
|
|
61
|
+
*/
|
|
62
|
+
onServerClose?: (info: OAuthServerCloseInfo) => void | Promise<void>;
|
|
34
63
|
}
|
|
35
64
|
|
|
36
65
|
export interface OAuthProviderInterface {
|
package/src/extensions/index.ts
CHANGED
|
@@ -5,8 +5,14 @@ export {
|
|
|
5
5
|
resolveAndLoadAgentPlugins,
|
|
6
6
|
resolvePluginConfigSearchPaths,
|
|
7
7
|
} from "./plugin/plugin-config-loader";
|
|
8
|
+
export type {
|
|
9
|
+
PluginInitializationFailure,
|
|
10
|
+
PluginInitializationWarning,
|
|
11
|
+
PluginLoadDiagnostics,
|
|
12
|
+
} from "./plugin/plugin-load-report";
|
|
8
13
|
export type { LoadAgentPluginFromPathOptions } from "./plugin/plugin-loader";
|
|
9
14
|
export {
|
|
10
15
|
loadAgentPluginFromPath,
|
|
11
16
|
loadAgentPluginsFromPaths,
|
|
17
|
+
loadAgentPluginsFromPathsWithDiagnostics,
|
|
12
18
|
} from "./plugin/plugin-loader";
|
|
@@ -6,6 +6,7 @@ import { afterEach, describe, expect, it } from "vitest";
|
|
|
6
6
|
import {
|
|
7
7
|
discoverPluginModulePaths,
|
|
8
8
|
resolveAgentPluginPaths,
|
|
9
|
+
resolveAndLoadAgentPlugins,
|
|
9
10
|
resolvePluginConfigSearchPaths,
|
|
10
11
|
} from "./plugin-config-loader";
|
|
11
12
|
|
|
@@ -142,4 +143,40 @@ describe("plugin-config-loader", () => {
|
|
|
142
143
|
await rm(workspace, { recursive: true, force: true });
|
|
143
144
|
}
|
|
144
145
|
});
|
|
146
|
+
|
|
147
|
+
it("loads valid plugins while reporting failures and duplicate overrides", async () => {
|
|
148
|
+
const root = await mkdtemp(join(tmpdir(), "core-plugin-config-loader-"));
|
|
149
|
+
try {
|
|
150
|
+
const first = join(root, "duplicate-one.js");
|
|
151
|
+
const second = join(root, "duplicate-two.js");
|
|
152
|
+
const invalid = join(root, "invalid.js");
|
|
153
|
+
await writeFile(
|
|
154
|
+
first,
|
|
155
|
+
"export default { name: 'duplicate-plugin', manifest: { capabilities: ['tools'] } };",
|
|
156
|
+
"utf8",
|
|
157
|
+
);
|
|
158
|
+
await writeFile(
|
|
159
|
+
second,
|
|
160
|
+
"export default { name: 'duplicate-plugin', manifest: { capabilities: ['commands'] } };",
|
|
161
|
+
"utf8",
|
|
162
|
+
);
|
|
163
|
+
await writeFile(invalid, "export default { name: 'broken' };", "utf8");
|
|
164
|
+
|
|
165
|
+
const loaded = await resolveAndLoadAgentPlugins({
|
|
166
|
+
mode: "in_process",
|
|
167
|
+
pluginPaths: [first, invalid, second],
|
|
168
|
+
cwd: root,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
expect(loaded.extensions.map((plugin) => plugin.name)).toEqual([
|
|
172
|
+
"duplicate-plugin",
|
|
173
|
+
]);
|
|
174
|
+
expect(loaded.extensions[0]?.manifest.capabilities).toEqual(["commands"]);
|
|
175
|
+
expect(loaded.failures).toHaveLength(1);
|
|
176
|
+
expect(loaded.warnings).toHaveLength(1);
|
|
177
|
+
expect(loaded.warnings[0]?.overriddenPluginPath).toBe(first);
|
|
178
|
+
} finally {
|
|
179
|
+
await rm(root, { recursive: true, force: true });
|
|
180
|
+
}
|
|
181
|
+
});
|
|
145
182
|
});
|
|
@@ -5,7 +5,8 @@ import {
|
|
|
5
5
|
resolveConfiguredPluginModulePaths,
|
|
6
6
|
resolvePluginConfigSearchPaths as resolvePluginConfigSearchPathsFromShared,
|
|
7
7
|
} from "@clinebot/shared/storage";
|
|
8
|
-
import {
|
|
8
|
+
import type { PluginLoadDiagnostics } from "./plugin-load-report";
|
|
9
|
+
import { loadAgentPluginsFromPathsWithDiagnostics } from "./plugin-loader";
|
|
9
10
|
import { loadSandboxedPlugins } from "./plugin-sandbox";
|
|
10
11
|
|
|
11
12
|
type AgentPlugin = NonNullable<AgentConfig["extensions"]>[number];
|
|
@@ -64,21 +65,26 @@ export interface ResolveAndLoadAgentPluginsOptions
|
|
|
64
65
|
|
|
65
66
|
export async function resolveAndLoadAgentPlugins(
|
|
66
67
|
options: ResolveAndLoadAgentPluginsOptions = {},
|
|
67
|
-
): Promise<
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
): Promise<
|
|
69
|
+
{
|
|
70
|
+
extensions: AgentPlugin[];
|
|
71
|
+
shutdown?: () => Promise<void>;
|
|
72
|
+
} & PluginLoadDiagnostics
|
|
73
|
+
> {
|
|
71
74
|
const paths = resolveAgentPluginPaths(options);
|
|
72
75
|
if (paths.length === 0) {
|
|
73
|
-
return { extensions: [] };
|
|
76
|
+
return { extensions: [], failures: [], warnings: [] };
|
|
74
77
|
}
|
|
75
78
|
|
|
76
79
|
if (options.mode === "in_process") {
|
|
80
|
+
const report = await loadAgentPluginsFromPathsWithDiagnostics(paths, {
|
|
81
|
+
cwd: options.cwd,
|
|
82
|
+
exportName: options.exportName,
|
|
83
|
+
});
|
|
77
84
|
return {
|
|
78
|
-
extensions:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}),
|
|
85
|
+
extensions: report.plugins,
|
|
86
|
+
failures: report.failures,
|
|
87
|
+
warnings: report.warnings,
|
|
82
88
|
};
|
|
83
89
|
}
|
|
84
90
|
|
|
@@ -93,5 +99,7 @@ export async function resolveAndLoadAgentPlugins(
|
|
|
93
99
|
return {
|
|
94
100
|
extensions: sandboxed.extensions ?? [],
|
|
95
101
|
shutdown: sandboxed.shutdown,
|
|
102
|
+
failures: sandboxed.failures,
|
|
103
|
+
warnings: sandboxed.warnings,
|
|
96
104
|
};
|
|
97
105
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface PluginInitializationFailure {
|
|
2
|
+
pluginPath: string;
|
|
3
|
+
pluginName?: string;
|
|
4
|
+
phase: "load" | "setup";
|
|
5
|
+
message: string;
|
|
6
|
+
stack?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface PluginInitializationWarning {
|
|
10
|
+
type: "duplicate_plugin_override";
|
|
11
|
+
pluginPath: string;
|
|
12
|
+
pluginName: string;
|
|
13
|
+
overriddenPluginPath: string;
|
|
14
|
+
message: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface PluginLoadDiagnostics {
|
|
18
|
+
failures: PluginInitializationFailure[];
|
|
19
|
+
warnings: PluginInitializationWarning[];
|
|
20
|
+
}
|
|
@@ -5,6 +5,7 @@ import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
|
5
5
|
import {
|
|
6
6
|
loadAgentPluginFromPath,
|
|
7
7
|
loadAgentPluginsFromPaths,
|
|
8
|
+
loadAgentPluginsFromPathsWithDiagnostics,
|
|
8
9
|
} from "./plugin-loader";
|
|
9
10
|
|
|
10
11
|
describe("plugin-loader", () => {
|
|
@@ -158,6 +159,17 @@ describe("plugin-loader", () => {
|
|
|
158
159
|
"export default { name: 'invalid-plugin' };",
|
|
159
160
|
"utf8",
|
|
160
161
|
);
|
|
162
|
+
|
|
163
|
+
await writeFile(
|
|
164
|
+
join(dir, "duplicate-one.mjs"),
|
|
165
|
+
"export default { name: 'duplicate-plugin', manifest: { capabilities: ['tools'] } };",
|
|
166
|
+
"utf8",
|
|
167
|
+
);
|
|
168
|
+
await writeFile(
|
|
169
|
+
join(dir, "duplicate-two.mjs"),
|
|
170
|
+
"export default { name: 'duplicate-plugin', manifest: { capabilities: ['commands'] } };",
|
|
171
|
+
"utf8",
|
|
172
|
+
);
|
|
161
173
|
});
|
|
162
174
|
|
|
163
175
|
afterAll(async () => {
|
|
@@ -244,4 +256,37 @@ describe("plugin-loader", () => {
|
|
|
244
256
|
loadAgentPluginFromPath(join(dir, "invalid-plugin.mjs")),
|
|
245
257
|
).rejects.toThrow(/missing required "manifest"/i);
|
|
246
258
|
});
|
|
259
|
+
|
|
260
|
+
it("continues loading valid plugins when one plugin fails", async () => {
|
|
261
|
+
const report = await loadAgentPluginsFromPathsWithDiagnostics([
|
|
262
|
+
join(dir, "plugin-a.mjs"),
|
|
263
|
+
join(dir, "invalid-plugin.mjs"),
|
|
264
|
+
join(dir, "plugin-b.mjs"),
|
|
265
|
+
]);
|
|
266
|
+
|
|
267
|
+
expect(report.plugins.map((plugin) => plugin.name)).toEqual([
|
|
268
|
+
"plugin-a",
|
|
269
|
+
"plugin-b",
|
|
270
|
+
]);
|
|
271
|
+
expect(report.failures).toHaveLength(1);
|
|
272
|
+
expect(report.failures[0]?.pluginPath).toBe(
|
|
273
|
+
join(dir, "invalid-plugin.mjs"),
|
|
274
|
+
);
|
|
275
|
+
expect(report.warnings).toEqual([]);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("keeps the later duplicate plugin and reports the override", async () => {
|
|
279
|
+
const report = await loadAgentPluginsFromPathsWithDiagnostics([
|
|
280
|
+
join(dir, "duplicate-one.mjs"),
|
|
281
|
+
join(dir, "duplicate-two.mjs"),
|
|
282
|
+
]);
|
|
283
|
+
|
|
284
|
+
expect(report.plugins).toHaveLength(1);
|
|
285
|
+
expect(report.plugins[0]?.name).toBe("duplicate-plugin");
|
|
286
|
+
expect(report.plugins[0]?.manifest.capabilities).toEqual(["commands"]);
|
|
287
|
+
expect(report.warnings).toHaveLength(1);
|
|
288
|
+
expect(report.warnings[0]?.overriddenPluginPath).toBe(
|
|
289
|
+
join(dir, "duplicate-one.mjs"),
|
|
290
|
+
);
|
|
291
|
+
});
|
|
247
292
|
});
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import type { AgentConfig } from "@clinebot/shared";
|
|
3
|
+
import type {
|
|
4
|
+
PluginInitializationFailure,
|
|
5
|
+
PluginInitializationWarning,
|
|
6
|
+
} from "./plugin-load-report";
|
|
3
7
|
import { importPluginModule } from "./plugin-module-import";
|
|
4
8
|
|
|
5
9
|
type AgentPlugin = NonNullable<AgentConfig["extensions"]>[number];
|
|
@@ -98,9 +102,59 @@ export async function loadAgentPluginsFromPaths(
|
|
|
98
102
|
pluginPaths: string[],
|
|
99
103
|
options: LoadAgentPluginFromPathOptions = {},
|
|
100
104
|
): Promise<AgentPlugin[]> {
|
|
101
|
-
const
|
|
105
|
+
const report = await loadAgentPluginsFromPathsWithDiagnostics(
|
|
106
|
+
pluginPaths,
|
|
107
|
+
options,
|
|
108
|
+
);
|
|
109
|
+
return report.plugins;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function loadAgentPluginsFromPathsWithDiagnostics(
|
|
113
|
+
pluginPaths: string[],
|
|
114
|
+
options: LoadAgentPluginFromPathOptions = {},
|
|
115
|
+
): Promise<{
|
|
116
|
+
plugins: AgentPlugin[];
|
|
117
|
+
failures: PluginInitializationFailure[];
|
|
118
|
+
warnings: PluginInitializationWarning[];
|
|
119
|
+
}> {
|
|
120
|
+
const failures: PluginInitializationFailure[] = [];
|
|
121
|
+
const warnings: PluginInitializationWarning[] = [];
|
|
122
|
+
const loadedByName = new Map<
|
|
123
|
+
string,
|
|
124
|
+
{ plugin: AgentPlugin; pluginPath: string; order: number }
|
|
125
|
+
>();
|
|
126
|
+
let order = 0;
|
|
127
|
+
|
|
102
128
|
for (const pluginPath of pluginPaths) {
|
|
103
|
-
|
|
129
|
+
try {
|
|
130
|
+
const plugin = await loadAgentPluginFromPath(pluginPath, options);
|
|
131
|
+
const existing = loadedByName.get(plugin.name);
|
|
132
|
+
if (existing) {
|
|
133
|
+
warnings.push({
|
|
134
|
+
type: "duplicate_plugin_override",
|
|
135
|
+
pluginName: plugin.name,
|
|
136
|
+
pluginPath,
|
|
137
|
+
overriddenPluginPath: existing.pluginPath,
|
|
138
|
+
message: `Plugin "${plugin.name}" from ${pluginPath} overrides ${existing.pluginPath}`,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
loadedByName.set(plugin.name, { plugin, pluginPath, order: order++ });
|
|
142
|
+
} catch (error) {
|
|
143
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
144
|
+
failures.push({
|
|
145
|
+
pluginPath,
|
|
146
|
+
phase: "load",
|
|
147
|
+
message,
|
|
148
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
104
151
|
}
|
|
105
|
-
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
plugins: [...loadedByName.values()]
|
|
155
|
+
.sort((left, right) => left.order - right.order)
|
|
156
|
+
.map((entry) => entry.plugin),
|
|
157
|
+
failures,
|
|
158
|
+
warnings,
|
|
159
|
+
};
|
|
106
160
|
}
|
|
@@ -84,6 +84,7 @@ interface ContributionDescriptor {
|
|
|
84
84
|
|
|
85
85
|
interface PluginDescriptor {
|
|
86
86
|
pluginId: string;
|
|
87
|
+
pluginPath: string;
|
|
87
88
|
name: string;
|
|
88
89
|
manifest: Record<string, unknown>;
|
|
89
90
|
contributions: {
|
|
@@ -96,6 +97,28 @@ interface PluginDescriptor {
|
|
|
96
97
|
};
|
|
97
98
|
}
|
|
98
99
|
|
|
100
|
+
interface PluginInitializationFailure {
|
|
101
|
+
pluginPath: string;
|
|
102
|
+
pluginName?: string;
|
|
103
|
+
phase: "load" | "setup";
|
|
104
|
+
message: string;
|
|
105
|
+
stack?: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
interface PluginInitializationWarning {
|
|
109
|
+
type: "duplicate_plugin_override";
|
|
110
|
+
pluginPath: string;
|
|
111
|
+
pluginName: string;
|
|
112
|
+
overriddenPluginPath: string;
|
|
113
|
+
message: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
interface InitializeResult {
|
|
117
|
+
plugins: PluginDescriptor[];
|
|
118
|
+
failures: PluginInitializationFailure[];
|
|
119
|
+
warnings: PluginInitializationWarning[];
|
|
120
|
+
}
|
|
121
|
+
|
|
99
122
|
interface PluginState {
|
|
100
123
|
plugin: PluginModule;
|
|
101
124
|
handlers: {
|
|
@@ -191,104 +214,153 @@ function getPlugin(pluginId: string): PluginState {
|
|
|
191
214
|
async function initialize(args: {
|
|
192
215
|
pluginPaths?: string[];
|
|
193
216
|
exportName?: string;
|
|
194
|
-
}): Promise<
|
|
217
|
+
}): Promise<InitializeResult> {
|
|
195
218
|
pluginState.clear();
|
|
196
219
|
pluginCounter = 0;
|
|
197
220
|
contributionCounters.clear();
|
|
198
221
|
|
|
199
222
|
const descriptors: PluginDescriptor[] = [];
|
|
223
|
+
const failures: PluginInitializationFailure[] = [];
|
|
224
|
+
const warnings: PluginInitializationWarning[] = [];
|
|
200
225
|
const exportName = args.exportName || "plugin";
|
|
226
|
+
const pluginIndexByName = new Map<string, number>();
|
|
201
227
|
|
|
202
228
|
for (const pluginPath of args.pluginPaths || []) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
moduleExports
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
id,
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
229
|
+
let plugin: PluginModule | undefined;
|
|
230
|
+
try {
|
|
231
|
+
const moduleExports = await importPluginModule(pluginPath);
|
|
232
|
+
plugin = (moduleExports.default ??
|
|
233
|
+
moduleExports[exportName]) as unknown as PluginModule;
|
|
234
|
+
assertValidPluginModule(plugin, pluginPath);
|
|
235
|
+
|
|
236
|
+
const pluginId = `plugin_${++pluginCounter}`;
|
|
237
|
+
const contributions: PluginDescriptor["contributions"] = {
|
|
238
|
+
tools: [],
|
|
239
|
+
commands: [],
|
|
240
|
+
shortcuts: [],
|
|
241
|
+
flags: [],
|
|
242
|
+
messageBuilders: [],
|
|
243
|
+
providers: [],
|
|
244
|
+
};
|
|
245
|
+
const handlers: PluginState["handlers"] = {
|
|
246
|
+
tools: new Map(),
|
|
247
|
+
commands: new Map(),
|
|
248
|
+
messageBuilders: new Map(),
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const api: PluginApi = {
|
|
252
|
+
registerTool: (tool) => {
|
|
253
|
+
const id = makeId(pluginId, "tool");
|
|
254
|
+
handlers.tools.set(id, tool.execute);
|
|
255
|
+
contributions.tools.push({
|
|
256
|
+
id,
|
|
257
|
+
name: tool.name,
|
|
258
|
+
description: tool.description,
|
|
259
|
+
inputSchema: tool.inputSchema,
|
|
260
|
+
timeoutMs: tool.timeoutMs,
|
|
261
|
+
retryable: tool.retryable,
|
|
262
|
+
});
|
|
263
|
+
},
|
|
264
|
+
registerCommand: (command) => {
|
|
265
|
+
const id = makeId(pluginId, "command");
|
|
266
|
+
if (typeof command.handler === "function") {
|
|
267
|
+
handlers.commands.set(id, command.handler);
|
|
268
|
+
}
|
|
269
|
+
contributions.commands.push({
|
|
270
|
+
id,
|
|
271
|
+
name: command.name,
|
|
272
|
+
description: command.description,
|
|
273
|
+
});
|
|
274
|
+
},
|
|
275
|
+
registerShortcut: (shortcut) => {
|
|
276
|
+
contributions.shortcuts.push({
|
|
277
|
+
id: makeId(pluginId, "shortcut"),
|
|
278
|
+
name: shortcut.name,
|
|
279
|
+
value: shortcut.value,
|
|
280
|
+
description: shortcut.description,
|
|
281
|
+
});
|
|
282
|
+
},
|
|
283
|
+
registerFlag: (flag) => {
|
|
284
|
+
contributions.flags.push({
|
|
285
|
+
id: makeId(pluginId, "flag"),
|
|
286
|
+
name: flag.name,
|
|
287
|
+
description: flag.description,
|
|
288
|
+
defaultValue: flag.defaultValue,
|
|
289
|
+
});
|
|
290
|
+
},
|
|
291
|
+
registerMessageBuilder: (builder) => {
|
|
292
|
+
const id = makeId(pluginId, "builder");
|
|
293
|
+
handlers.messageBuilders.set(id, builder.build);
|
|
294
|
+
contributions.messageBuilders.push({ id, name: builder.name });
|
|
295
|
+
},
|
|
296
|
+
registerProvider: (provider) => {
|
|
297
|
+
contributions.providers.push({
|
|
298
|
+
id: makeId(pluginId, "provider"),
|
|
299
|
+
name: provider.name,
|
|
300
|
+
description: provider.description,
|
|
301
|
+
metadata: sanitizeObject(provider.metadata),
|
|
302
|
+
});
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
if (typeof plugin.setup === "function") {
|
|
307
|
+
try {
|
|
308
|
+
await plugin.setup(api);
|
|
309
|
+
} catch (error) {
|
|
310
|
+
failures.push({
|
|
311
|
+
pluginPath,
|
|
312
|
+
pluginName: plugin.name,
|
|
313
|
+
phase: "setup",
|
|
314
|
+
message: error instanceof Error ? error.message : String(error),
|
|
315
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
316
|
+
});
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const previousIndex = pluginIndexByName.get(plugin.name);
|
|
322
|
+
if (previousIndex !== undefined) {
|
|
323
|
+
const previous = descriptors[previousIndex];
|
|
324
|
+
if (!previous) {
|
|
325
|
+
pluginIndexByName.delete(plugin.name);
|
|
326
|
+
} else {
|
|
327
|
+
warnings.push({
|
|
328
|
+
type: "duplicate_plugin_override",
|
|
329
|
+
pluginName: plugin.name,
|
|
330
|
+
pluginPath,
|
|
331
|
+
overriddenPluginPath: previous.pluginPath,
|
|
332
|
+
message: `Plugin "${plugin.name}" from ${pluginPath} overrides ${previous.pluginPath}`,
|
|
333
|
+
});
|
|
334
|
+
pluginState.delete(previous.pluginId);
|
|
335
|
+
descriptors.splice(previousIndex, 1);
|
|
336
|
+
pluginIndexByName.clear();
|
|
337
|
+
for (const [index, descriptor] of descriptors.entries()) {
|
|
338
|
+
pluginIndexByName.set(descriptor.name, index);
|
|
339
|
+
}
|
|
240
340
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
defaultValue: flag.defaultValue,
|
|
261
|
-
});
|
|
262
|
-
},
|
|
263
|
-
registerMessageBuilder: (builder) => {
|
|
264
|
-
const id = makeId(pluginId, "builder");
|
|
265
|
-
handlers.messageBuilders.set(id, builder.build);
|
|
266
|
-
contributions.messageBuilders.push({ id, name: builder.name });
|
|
267
|
-
},
|
|
268
|
-
registerProvider: (provider) => {
|
|
269
|
-
contributions.providers.push({
|
|
270
|
-
id: makeId(pluginId, "provider"),
|
|
271
|
-
name: provider.name,
|
|
272
|
-
description: provider.description,
|
|
273
|
-
metadata: sanitizeObject(provider.metadata),
|
|
274
|
-
});
|
|
275
|
-
},
|
|
276
|
-
};
|
|
277
|
-
|
|
278
|
-
if (typeof plugin.setup === "function") {
|
|
279
|
-
await plugin.setup(api);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
pluginState.set(pluginId, { plugin, handlers });
|
|
344
|
+
pluginIndexByName.set(plugin.name, descriptors.length);
|
|
345
|
+
descriptors.push({
|
|
346
|
+
pluginId,
|
|
347
|
+
pluginPath,
|
|
348
|
+
name: plugin.name,
|
|
349
|
+
manifest: plugin.manifest,
|
|
350
|
+
contributions,
|
|
351
|
+
});
|
|
352
|
+
} catch (error) {
|
|
353
|
+
failures.push({
|
|
354
|
+
pluginPath,
|
|
355
|
+
pluginName: plugin?.name,
|
|
356
|
+
phase: "load",
|
|
357
|
+
message: error instanceof Error ? error.message : String(error),
|
|
358
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
359
|
+
});
|
|
280
360
|
}
|
|
281
|
-
|
|
282
|
-
pluginState.set(pluginId, { plugin, handlers });
|
|
283
|
-
descriptors.push({
|
|
284
|
-
pluginId,
|
|
285
|
-
name: plugin.name,
|
|
286
|
-
manifest: plugin.manifest,
|
|
287
|
-
contributions,
|
|
288
|
-
});
|
|
289
361
|
}
|
|
290
362
|
|
|
291
|
-
return descriptors;
|
|
363
|
+
return { plugins: descriptors, failures, warnings };
|
|
292
364
|
}
|
|
293
365
|
|
|
294
366
|
async function invokeHook(args: {
|
|
@@ -184,6 +184,31 @@ describe("plugin-sandbox", () => {
|
|
|
184
184
|
"utf8",
|
|
185
185
|
);
|
|
186
186
|
|
|
187
|
+
await writeFile(
|
|
188
|
+
join(dir, "plugin-broken-setup.mjs"),
|
|
189
|
+
[
|
|
190
|
+
"export default {",
|
|
191
|
+
" name: 'sandbox-broken-setup',",
|
|
192
|
+
" manifest: { capabilities: ['tools'] },",
|
|
193
|
+
" async setup() {",
|
|
194
|
+
" throw new Error('broken setup');",
|
|
195
|
+
" },",
|
|
196
|
+
"};",
|
|
197
|
+
].join("\n"),
|
|
198
|
+
"utf8",
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
await writeFile(
|
|
202
|
+
join(dir, "plugin-duplicate-a.mjs"),
|
|
203
|
+
"export default { name: 'sandbox-duplicate', manifest: { capabilities: ['tools'] } };",
|
|
204
|
+
"utf8",
|
|
205
|
+
);
|
|
206
|
+
await writeFile(
|
|
207
|
+
join(dir, "plugin-duplicate-b.mjs"),
|
|
208
|
+
"export default { name: 'sandbox-duplicate', manifest: { capabilities: ['commands'] } };",
|
|
209
|
+
"utf8",
|
|
210
|
+
);
|
|
211
|
+
|
|
187
212
|
sharedSandbox = await loadSandboxedPlugins({
|
|
188
213
|
pluginPaths: [
|
|
189
214
|
join(dir, "plugin.mjs"),
|
|
@@ -321,6 +346,51 @@ describe("plugin-sandbox", () => {
|
|
|
321
346
|
expect(result).toEqual({ echoed: "ok" });
|
|
322
347
|
});
|
|
323
348
|
|
|
349
|
+
it("continues loading remaining sandbox plugins when one setup fails", async () => {
|
|
350
|
+
const sandboxed = await loadSandboxedPlugins({
|
|
351
|
+
pluginPaths: [
|
|
352
|
+
join(dir, "plugin.mjs"),
|
|
353
|
+
join(dir, "plugin-broken-setup.mjs"),
|
|
354
|
+
join(dir, "plugin-events.mjs"),
|
|
355
|
+
],
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
try {
|
|
359
|
+
expect(sandboxed.extensions?.map((extension) => extension.name)).toEqual([
|
|
360
|
+
"sandbox-test",
|
|
361
|
+
"sandbox-events",
|
|
362
|
+
]);
|
|
363
|
+
expect(sandboxed.failures).toHaveLength(1);
|
|
364
|
+
expect(sandboxed.failures[0]?.pluginName).toBe("sandbox-broken-setup");
|
|
365
|
+
expect(sandboxed.failures[0]?.phase).toBe("setup");
|
|
366
|
+
} finally {
|
|
367
|
+
await sandboxed.shutdown();
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it("keeps the later duplicate sandbox plugin and reports the override", async () => {
|
|
372
|
+
const sandboxed = await loadSandboxedPlugins({
|
|
373
|
+
pluginPaths: [
|
|
374
|
+
join(dir, "plugin-duplicate-a.mjs"),
|
|
375
|
+
join(dir, "plugin-duplicate-b.mjs"),
|
|
376
|
+
],
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
try {
|
|
380
|
+
expect(sandboxed.extensions).toHaveLength(1);
|
|
381
|
+
expect(sandboxed.extensions?.[0]?.name).toBe("sandbox-duplicate");
|
|
382
|
+
expect(sandboxed.extensions?.[0]?.manifest.capabilities).toEqual([
|
|
383
|
+
"commands",
|
|
384
|
+
]);
|
|
385
|
+
expect(sandboxed.warnings).toHaveLength(1);
|
|
386
|
+
expect(sandboxed.warnings[0]?.overriddenPluginPath).toBe(
|
|
387
|
+
join(dir, "plugin-duplicate-a.mjs"),
|
|
388
|
+
);
|
|
389
|
+
} finally {
|
|
390
|
+
await sandboxed.shutdown();
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
|
|
324
394
|
it("resolves plugin-local dependencies in the sandbox process", async () => {
|
|
325
395
|
expect(sharedExtensions.get("sandbox-local-dep")?.name).toBe(
|
|
326
396
|
"sandbox-local-dep",
|