@vellumai/assistant 0.12.2-staging.2 → 0.12.2-staging.4
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/docs/desktop-browser-cli.md +33 -0
- package/openapi.yaml +17 -5
- package/package.json +1 -1
- package/scripts/smoke-desktop-browser-cli.ts +279 -0
- package/src/__tests__/config-schema.test.ts +1 -1
- package/src/__tests__/config-sounds-sync.test.ts +23 -0
- package/src/__tests__/conversation-error.test.ts +4 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +6 -4
- package/src/__tests__/external-plugin-loader.test.ts +52 -0
- package/src/__tests__/headless-browser-mode.test.ts +48 -0
- package/src/__tests__/host-cu-proxy.test.ts +116 -18
- package/src/__tests__/list-all-apps.test.ts +40 -8
- package/src/__tests__/mcp-auth-routes.test.ts +99 -0
- package/src/__tests__/mcp-client-auth.test.ts +54 -0
- package/src/__tests__/mcp-health-check.test.ts +32 -32
- package/src/__tests__/mcp-list-plugin-servers.test.ts +77 -32
- package/src/__tests__/mcp-tool-annotations-risk.test.ts +4 -2
- package/src/__tests__/skills.test.ts +10 -0
- package/src/__tests__/user-plugin-loader.test.ts +21 -0
- package/src/acp/session-snapshot.ts +260 -0
- package/src/apps/app-store.ts +6 -5
- package/src/browser/operations.ts +2 -1
- package/src/browser/types.ts +7 -0
- package/src/browser/virtual-desktop-target.ts +34 -0
- package/src/calls/__tests__/progress-narration.test.ts +23 -0
- package/src/calls/__tests__/voice-control-protocol.test.ts +7 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +62 -0
- package/src/calls/progress-narration.ts +15 -2
- package/src/calls/voice-control-protocol.ts +34 -4
- package/src/calls/voice-session-bridge.ts +23 -0
- package/src/cli/commands/__tests__/browser.test.ts +84 -3
- package/src/cli/commands/browser.help.ts +43 -0
- package/src/cli/commands/browser.ts +52 -8
- package/src/cli/commands/plugins.ts +16 -5
- package/src/cli/lib/__tests__/install-from-github.test.ts +111 -0
- package/src/cli/lib/__tests__/install-from-platform.test.ts +77 -2
- package/src/cli/lib/__tests__/list-installed-plugins.test.ts +45 -3
- package/src/cli/lib/__tests__/plugin-details.test.ts +26 -0
- package/src/cli/lib/__tests__/uninstall-plugin.test.ts +122 -5
- package/src/cli/lib/bundled-marketplace.json +13 -0
- package/src/cli/lib/install-from-github.ts +17 -18
- package/src/cli/lib/install-from-platform.ts +14 -9
- package/src/cli/lib/list-installed-plugins.ts +21 -35
- package/src/cli/lib/plugin-details.ts +21 -8
- package/src/cli/lib/uninstall-plugin.ts +59 -1
- package/src/config/__tests__/plugin-resident-skill-discovery.test.ts +21 -0
- package/src/config/bundled-skills/acp/SKILL.md +1 -1
- package/src/config/bundled-skills/acp/TOOLS.json +2 -2
- package/src/config/bundled-skills/computer-use/SKILL.md +30 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +2 -2
- package/src/config/bundled-skills/screen-annotation/SKILL.md +2 -2
- package/src/config/feature-flag-registry.json +2 -2
- package/src/config/schemas/__tests__/voice.test.ts +2 -2
- package/src/config/schemas/mcp.ts +9 -7
- package/src/config/schemas/monitoring.ts +9 -0
- package/src/config/schemas/voice.ts +2 -2
- package/src/config/skills.ts +13 -21
- package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +47 -0
- package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +1 -0
- package/src/daemon/conversation-client-surface.ts +26 -0
- package/src/daemon/conversation-runtime-assembly.ts +1 -1
- package/src/daemon/conversation-tool-setup.ts +31 -29
- package/src/daemon/host-cu-proxy.ts +51 -26
- package/src/daemon/host-proxy-preactivation.ts +2 -0
- package/src/daemon/mcp-reload-service.ts +7 -0
- package/src/daemon/message-types/sync.ts +1 -0
- package/src/daemon/providers-setup.ts +4 -0
- package/src/desktop/__tests__/fake-desktop.ts +1 -0
- package/src/desktop/desktop-automation-lease.test.ts +208 -0
- package/src/desktop/desktop-automation-lease.ts +270 -0
- package/src/desktop/desktop-browser-client.test.ts +413 -0
- package/src/desktop/desktop-browser-client.ts +430 -0
- package/src/desktop/desktop-browser-cursor.ts +40 -0
- package/src/desktop/desktop-browser-endpoint.test.ts +78 -0
- package/src/desktop/desktop-browser-endpoint.ts +145 -0
- package/src/desktop/desktop-browser-operations.ts +130 -0
- package/src/desktop/desktop-chrome-session.test.ts +48 -0
- package/src/desktop/desktop-chrome-session.ts +58 -8
- package/src/desktop/desktop-dependencies.test.ts +29 -0
- package/src/desktop/desktop-dependencies.ts +26 -5
- package/src/desktop/desktop-display.ts +9 -0
- package/src/desktop/desktop-panel-config.ts +14 -1
- package/src/desktop/desktop-session-manager.test.ts +86 -3
- package/src/desktop/desktop-session-manager.ts +144 -24
- package/src/desktop/desktop-stream-bridge.ts +1 -1
- package/src/desktop/virtual-desktop-feature.ts +18 -0
- package/src/desktop/virtual-desktop-platform.test.ts +61 -0
- package/src/i18n/__tests__/i18n.test.ts +9 -5
- package/src/i18n/messages.ts +13 -4
- package/src/ipc/__tests__/browser-ipc.test.ts +230 -2
- package/src/ipc/__tests__/cancel-on-disconnect.test.ts +54 -0
- package/src/ipc/assistant-server.ts +15 -1
- package/src/ipc/cli-client.ts +13 -5
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +22 -0
- package/src/live-voice/__tests__/live-voice-events.test.ts +3 -2
- package/src/live-voice/__tests__/live-voice-session-telemetry.test.ts +19 -0
- package/src/live-voice/__tests__/protocol.test.ts +11 -3
- package/src/live-voice/__tests__/session-controls.test.ts +34 -0
- package/src/live-voice/live-voice-metrics.ts +9 -0
- package/src/live-voice/live-voice-session.ts +3 -0
- package/src/live-voice/protocol.ts +10 -1
- package/src/live-voice/session-controls.ts +28 -0
- package/src/mcp/__tests__/credential-target.test.ts +59 -0
- package/src/mcp/__tests__/effective-config.test.ts +11 -0
- package/src/mcp/__tests__/manager-state.test.ts +128 -0
- package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +20 -6
- package/src/mcp/__tests__/mcp-auth-state-target.test.ts +49 -0
- package/src/mcp/__tests__/plugin-mcp-oauth-cleanup.test.ts +107 -0
- package/src/mcp/__tests__/plugin-mcp-oauth-provider-isolation.test.ts +89 -0
- package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +63 -11
- package/src/mcp/__tests__/reload-signal-emission.test.ts +39 -2
- package/src/mcp/client.ts +64 -16
- package/src/mcp/credential-target.ts +105 -0
- package/src/mcp/effective-config.ts +42 -15
- package/src/mcp/manager.ts +61 -8
- package/src/mcp/mcp-auth-orchestrator.ts +17 -4
- package/src/mcp/mcp-auth-state.ts +38 -6
- package/src/mcp/mcp-oauth-provider.ts +101 -33
- package/src/monitoring/__tests__/mount-watch.test.ts +236 -0
- package/src/monitoring/mount-watch.ts +348 -0
- package/src/monitoring/worker.ts +9 -0
- package/src/notifications/__tests__/decision-engine.test.ts +175 -0
- package/src/notifications/decision-engine.ts +43 -8
- package/src/oauth/seed-providers.ts +11 -0
- package/src/onboarding/checkin-event.ts +2 -1
- package/src/plugins/__tests__/installed-plugin-dirs.test.ts +14 -1
- package/src/plugins/__tests__/mcp-servers.test.ts +38 -8
- package/src/plugins/external-plugin-loader.ts +49 -86
- package/src/plugins/installed-plugin-dirs.ts +6 -4
- package/src/plugins/mcp-servers.ts +9 -15
- package/src/plugins/mtime-cache.ts +2 -1
- package/src/plugins/user-loader.ts +2 -1
- package/src/providers/inference/__tests__/endpoint-probe.test.ts +45 -0
- package/src/providers/inference/endpoint-probe.ts +27 -12
- package/src/providers/openai/responses-provider.ts +9 -2
- package/src/providers/opencode/client.test.ts +167 -0
- package/src/providers/opencode/client.ts +73 -4
- package/src/runtime/AGENTS.md +4 -0
- package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +45 -2
- package/src/runtime/http-server.ts +6 -6
- package/src/runtime/routes/__tests__/acp-routes.test.ts +19 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +25 -3
- package/src/runtime/routes/acp-routes.ts +17 -161
- package/src/runtime/routes/browser-context.ts +39 -0
- package/src/runtime/routes/browser-routes.ts +16 -47
- package/src/runtime/routes/browser-tabs-routes.ts +72 -29
- package/src/runtime/routes/desktop-setup-routes.test.ts +16 -1
- package/src/runtime/routes/desktop-setup-routes.ts +7 -5
- package/src/runtime/routes/mcp-auth-routes.ts +106 -94
- package/src/runtime/routes/plugins-routes.ts +14 -1
- package/src/runtime/sync/resource-sync-events.ts +4 -0
- package/src/tools/acp/status.test.ts +276 -21
- package/src/tools/acp/status.ts +98 -32
- package/src/tools/browser/browser-execution.ts +44 -13
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +69 -0
- package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +1 -0
- package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +5 -0
- package/src/tools/computer-use/definitions.ts +2 -3
- package/src/util/abort-reasons.ts +12 -1
- package/src/util/plugin-manifest.ts +203 -0
- package/src/desktop/desktop-feature.test.ts +0 -18
- package/src/desktop/desktop-feature.ts +0 -19
|
@@ -22,6 +22,8 @@ import { tmpdir } from "node:os";
|
|
|
22
22
|
import { dirname, join } from "node:path";
|
|
23
23
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
24
24
|
|
|
25
|
+
import { readPluginMcpServers } from "../../../plugins/mcp-servers.js";
|
|
26
|
+
import { PluginManifestError } from "../../../util/plugin-manifest.js";
|
|
25
27
|
import type { FetchLike } from "../fetch-like.js";
|
|
26
28
|
import {
|
|
27
29
|
type GitRunner,
|
|
@@ -1225,6 +1227,115 @@ describe("installPlugin — direct (untrusted) install", () => {
|
|
|
1225
1227
|
expect(meta?.commit).toBe("a".repeat(40));
|
|
1226
1228
|
});
|
|
1227
1229
|
|
|
1230
|
+
test("installs a standard-only plugin without synthesizing package.json", async () => {
|
|
1231
|
+
const fetch = makeContentsFetch({ tree: {} });
|
|
1232
|
+
const runGit = fakeGitRunner({
|
|
1233
|
+
tree: {
|
|
1234
|
+
"plugin.json": JSON.stringify({
|
|
1235
|
+
$schema: "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
1236
|
+
name: "standard-plugin",
|
|
1237
|
+
version: "1.4.0",
|
|
1238
|
+
}),
|
|
1239
|
+
"mcp.json": JSON.stringify({
|
|
1240
|
+
$schema: "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json",
|
|
1241
|
+
mcpServers: {
|
|
1242
|
+
"standard-plugin": {
|
|
1243
|
+
type: "streamable-http",
|
|
1244
|
+
url: "https://mcp.example.com",
|
|
1245
|
+
},
|
|
1246
|
+
},
|
|
1247
|
+
}),
|
|
1248
|
+
},
|
|
1249
|
+
commit: "9".repeat(40),
|
|
1250
|
+
});
|
|
1251
|
+
|
|
1252
|
+
const result = await installPlugin(
|
|
1253
|
+
{
|
|
1254
|
+
name: "standard-plugin",
|
|
1255
|
+
directSource: {
|
|
1256
|
+
owner: "owner",
|
|
1257
|
+
repo: "standard-plugin",
|
|
1258
|
+
rootPath: "",
|
|
1259
|
+
ref: "HEAD",
|
|
1260
|
+
},
|
|
1261
|
+
},
|
|
1262
|
+
{ fetch, runGit, workspacePluginsDir: pluginsDir },
|
|
1263
|
+
);
|
|
1264
|
+
|
|
1265
|
+
expect(existsSync(join(result.target, "plugin.json"))).toBe(true);
|
|
1266
|
+
expect(existsSync(join(result.target, "package.json"))).toBe(false);
|
|
1267
|
+
expect(readInstallMeta(result.target)?.version).toBe("1.4.0");
|
|
1268
|
+
const mcp = readPluginMcpServers({ workspacePluginsDir: pluginsDir });
|
|
1269
|
+
expect(mcp.issues).toEqual([]);
|
|
1270
|
+
expect(mcp.servers.map((server) => server.id)).toEqual(["standard-plugin"]);
|
|
1271
|
+
});
|
|
1272
|
+
|
|
1273
|
+
test("synthesizes package.json beside a foreign plugin.json", async () => {
|
|
1274
|
+
const fetch = makeContentsFetch({ tree: {} });
|
|
1275
|
+
const runGit = fakeGitRunner({
|
|
1276
|
+
tree: {
|
|
1277
|
+
"plugin.json": JSON.stringify({ name: "foreign-plugin" }),
|
|
1278
|
+
"mcp.json": JSON.stringify({
|
|
1279
|
+
mcpServers: {
|
|
1280
|
+
"foreign-plugin": {
|
|
1281
|
+
type: "streamable-http",
|
|
1282
|
+
url: "https://mcp.example.com",
|
|
1283
|
+
},
|
|
1284
|
+
},
|
|
1285
|
+
}),
|
|
1286
|
+
},
|
|
1287
|
+
commit: "8".repeat(40),
|
|
1288
|
+
});
|
|
1289
|
+
|
|
1290
|
+
const result = await installPlugin(
|
|
1291
|
+
{
|
|
1292
|
+
name: "foreign-plugin",
|
|
1293
|
+
directSource: {
|
|
1294
|
+
owner: "owner",
|
|
1295
|
+
repo: "foreign-plugin",
|
|
1296
|
+
rootPath: "",
|
|
1297
|
+
ref: "HEAD",
|
|
1298
|
+
},
|
|
1299
|
+
},
|
|
1300
|
+
{ fetch, runGit, workspacePluginsDir: pluginsDir },
|
|
1301
|
+
);
|
|
1302
|
+
|
|
1303
|
+
expect(existsSync(join(result.target, "plugin.json"))).toBe(true);
|
|
1304
|
+
expect(existsSync(join(result.target, "package.json"))).toBe(true);
|
|
1305
|
+
const mcp = readPluginMcpServers({ workspacePluginsDir: pluginsDir });
|
|
1306
|
+
expect(mcp.issues).toEqual([]);
|
|
1307
|
+
expect(mcp.servers.map((server) => server.id)).toEqual(["foreign-plugin"]);
|
|
1308
|
+
});
|
|
1309
|
+
|
|
1310
|
+
test("rejects a malformed claimed standard manifest before install", async () => {
|
|
1311
|
+
const fetch = makeContentsFetch({ tree: {} });
|
|
1312
|
+
const runGit = fakeGitRunner({
|
|
1313
|
+
tree: {
|
|
1314
|
+
"plugin.json": JSON.stringify({
|
|
1315
|
+
$schema: "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
1316
|
+
name: "Invalid Standard Name",
|
|
1317
|
+
}),
|
|
1318
|
+
},
|
|
1319
|
+
commit: "7".repeat(40),
|
|
1320
|
+
});
|
|
1321
|
+
|
|
1322
|
+
await expect(
|
|
1323
|
+
installPlugin(
|
|
1324
|
+
{
|
|
1325
|
+
name: "invalid-standard",
|
|
1326
|
+
directSource: {
|
|
1327
|
+
owner: "owner",
|
|
1328
|
+
repo: "invalid-standard",
|
|
1329
|
+
rootPath: "",
|
|
1330
|
+
ref: "HEAD",
|
|
1331
|
+
},
|
|
1332
|
+
},
|
|
1333
|
+
{ fetch, runGit, workspacePluginsDir: pluginsDir },
|
|
1334
|
+
),
|
|
1335
|
+
).rejects.toBeInstanceOf(PluginManifestError);
|
|
1336
|
+
expect(existsSync(join(pluginsDir, "invalid-standard"))).toBe(false);
|
|
1337
|
+
});
|
|
1338
|
+
|
|
1228
1339
|
test("never overlays a curated adapter stub, even if one matches the name", async () => {
|
|
1229
1340
|
// GIVEN a fetch that WOULD serve an adapter stub for the name, and a
|
|
1230
1341
|
// postinstall runner that fails the test if it is ever invoked
|
|
@@ -14,6 +14,8 @@ import { join } from "node:path";
|
|
|
14
14
|
import { gzipSync } from "node:zlib";
|
|
15
15
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
16
16
|
|
|
17
|
+
import { readPluginMcpServers } from "../../../plugins/mcp-servers.js";
|
|
18
|
+
import { PluginManifestError } from "../../../util/plugin-manifest.js";
|
|
17
19
|
import type { FetchLike } from "../fetch-like.js";
|
|
18
20
|
import {
|
|
19
21
|
PluginInstallDeclinedError,
|
|
@@ -145,7 +147,13 @@ const noSleep = async () => {};
|
|
|
145
147
|
describe("installPluginFromPlatform — success", () => {
|
|
146
148
|
test("downloads, verifies, and extracts files at the plugin root", async () => {
|
|
147
149
|
const entries: TarEntry[] = [
|
|
148
|
-
{
|
|
150
|
+
{
|
|
151
|
+
name: "plugin.json",
|
|
152
|
+
content: JSON.stringify({
|
|
153
|
+
$schema: "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
154
|
+
name: "reading-pal",
|
|
155
|
+
}),
|
|
156
|
+
},
|
|
149
157
|
{ name: "README.md", content: "# reading pal" },
|
|
150
158
|
{ name: "skills/read/SKILL.md", content: "skill body" },
|
|
151
159
|
];
|
|
@@ -177,7 +185,7 @@ describe("installPluginFromPlatform — success", () => {
|
|
|
177
185
|
);
|
|
178
186
|
expect(existsSync(join(target, "README.md"))).toBe(true);
|
|
179
187
|
expect(existsSync(join(target, "skills", "read", "SKILL.md"))).toBe(true);
|
|
180
|
-
expect(existsSync(join(target, "package.json"))).toBe(
|
|
188
|
+
expect(existsSync(join(target, "package.json"))).toBe(false);
|
|
181
189
|
|
|
182
190
|
// Provenance records the pinned commit and the verified ETag.
|
|
183
191
|
const meta = readInstallMeta(target);
|
|
@@ -227,6 +235,73 @@ describe("installPluginFromPlatform — success", () => {
|
|
|
227
235
|
expect(pkg.peerDependencies["@vellumai/plugin-api"]).toBeDefined();
|
|
228
236
|
});
|
|
229
237
|
|
|
238
|
+
test("synthesizes package.json beside a foreign plugin.json", async () => {
|
|
239
|
+
const fetchFn = makeInstallFetch({
|
|
240
|
+
entries: [
|
|
241
|
+
{
|
|
242
|
+
name: "plugin.json",
|
|
243
|
+
content: JSON.stringify({ name: "foreign-plugin" }),
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
name: "mcp.json",
|
|
247
|
+
content: JSON.stringify({
|
|
248
|
+
mcpServers: {
|
|
249
|
+
"foreign-plugin": {
|
|
250
|
+
type: "streamable-http",
|
|
251
|
+
url: "https://mcp.example.com",
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
}),
|
|
255
|
+
},
|
|
256
|
+
],
|
|
257
|
+
ref: "d".repeat(40),
|
|
258
|
+
repo: "example-org/foreign-plugin",
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const result = await installPluginFromPlatform(
|
|
262
|
+
{ name: "foreign-plugin" },
|
|
263
|
+
{
|
|
264
|
+
fetch: fetchFn,
|
|
265
|
+
platformBaseUrl: PLATFORM,
|
|
266
|
+
workspacePluginsDir: pluginsDir,
|
|
267
|
+
},
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
expect(existsSync(join(result.target, "plugin.json"))).toBe(true);
|
|
271
|
+
expect(existsSync(join(result.target, "package.json"))).toBe(true);
|
|
272
|
+
const mcp = readPluginMcpServers({ workspacePluginsDir: pluginsDir });
|
|
273
|
+
expect(mcp.issues).toEqual([]);
|
|
274
|
+
expect(mcp.servers.map((server) => server.id)).toEqual(["foreign-plugin"]);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test("rejects a malformed claimed standard manifest before install", async () => {
|
|
278
|
+
const fetchFn = makeInstallFetch({
|
|
279
|
+
entries: [
|
|
280
|
+
{
|
|
281
|
+
name: "plugin.json",
|
|
282
|
+
content: JSON.stringify({
|
|
283
|
+
$schema:
|
|
284
|
+
"https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
285
|
+
}),
|
|
286
|
+
},
|
|
287
|
+
],
|
|
288
|
+
ref: "e".repeat(40),
|
|
289
|
+
repo: "example-org/invalid-standard",
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
await expect(
|
|
293
|
+
installPluginFromPlatform(
|
|
294
|
+
{ name: "invalid-standard" },
|
|
295
|
+
{
|
|
296
|
+
fetch: fetchFn,
|
|
297
|
+
platformBaseUrl: PLATFORM,
|
|
298
|
+
workspacePluginsDir: pluginsDir,
|
|
299
|
+
},
|
|
300
|
+
),
|
|
301
|
+
).rejects.toBeInstanceOf(PluginManifestError);
|
|
302
|
+
expect(existsSync(join(pluginsDir, "invalid-standard"))).toBe(false);
|
|
303
|
+
});
|
|
304
|
+
|
|
230
305
|
test("leaves an upstream package.json in place", async () => {
|
|
231
306
|
const fetchFn = makeInstallFetch({
|
|
232
307
|
entries: [
|
|
@@ -93,6 +93,46 @@ describe("listInstalledPlugins", () => {
|
|
|
93
93
|
expect(result[0]!.packageJson?.icon).toBe("🚀");
|
|
94
94
|
});
|
|
95
95
|
|
|
96
|
+
test("lists metadata from a standard-only plugin.json", () => {
|
|
97
|
+
mkdirSync(join(pluginsDir, "standard"));
|
|
98
|
+
writeFileSync(
|
|
99
|
+
join(pluginsDir, "standard", "plugin.json"),
|
|
100
|
+
JSON.stringify({
|
|
101
|
+
$schema: "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
102
|
+
name: "standard",
|
|
103
|
+
version: "3.0.0",
|
|
104
|
+
description: "standard plugin",
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const result = listInstalledPlugins({ workspacePluginsDir: pluginsDir });
|
|
109
|
+
|
|
110
|
+
expect(result[0]!.packageJson).toEqual({
|
|
111
|
+
name: "standard",
|
|
112
|
+
version: "3.0.0",
|
|
113
|
+
description: "standard plugin",
|
|
114
|
+
peerDependencies: undefined,
|
|
115
|
+
});
|
|
116
|
+
expect(result[0]!.issues).toEqual([]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("does not validate an unselected plugin.json beside package.json", () => {
|
|
120
|
+
mkdirSync(join(pluginsDir, "legacy"));
|
|
121
|
+
writeFileSync(
|
|
122
|
+
join(pluginsDir, "legacy", "package.json"),
|
|
123
|
+
JSON.stringify({ name: "legacy", version: "1.0.0" }),
|
|
124
|
+
);
|
|
125
|
+
writeFileSync(
|
|
126
|
+
join(pluginsDir, "legacy", "plugin.json"),
|
|
127
|
+
"{ foreign and malformed }",
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const result = listInstalledPlugins({ workspacePluginsDir: pluginsDir });
|
|
131
|
+
|
|
132
|
+
expect(result[0]!.packageJson?.version).toBe("1.0.0");
|
|
133
|
+
expect(result[0]!.issues).toEqual([]);
|
|
134
|
+
});
|
|
135
|
+
|
|
96
136
|
test.each([
|
|
97
137
|
["missing vellum block", { name: "p", version: "1.0.0" }],
|
|
98
138
|
["non-string icon", { name: "p", version: "1.0.0", vellum: { icon: 42 } }],
|
|
@@ -120,7 +160,9 @@ describe("listInstalledPlugins", () => {
|
|
|
120
160
|
const result = listInstalledPlugins({ workspacePluginsDir: pluginsDir });
|
|
121
161
|
expect(result).toHaveLength(1);
|
|
122
162
|
expect(result[0]!.packageJson).toBeNull();
|
|
123
|
-
expect(result[0]!.issues).
|
|
163
|
+
expect(result[0]!.issues[0]).toContain(
|
|
164
|
+
"missing package.json and plugin.json",
|
|
165
|
+
);
|
|
124
166
|
});
|
|
125
167
|
|
|
126
168
|
test("reports malformed JSON as an issue rather than failing", () => {
|
|
@@ -130,7 +172,7 @@ describe("listInstalledPlugins", () => {
|
|
|
130
172
|
const result = listInstalledPlugins({ workspacePluginsDir: pluginsDir });
|
|
131
173
|
expect(result).toHaveLength(1);
|
|
132
174
|
expect(result[0]!.packageJson).toBeNull();
|
|
133
|
-
expect(result[0]!.issues[0]).
|
|
175
|
+
expect(result[0]!.issues[0]).toContain("could not be read or parsed");
|
|
134
176
|
});
|
|
135
177
|
|
|
136
178
|
test("reports non-object package.json as an issue", () => {
|
|
@@ -143,7 +185,7 @@ describe("listInstalledPlugins", () => {
|
|
|
143
185
|
const result = listInstalledPlugins({ workspacePluginsDir: pluginsDir });
|
|
144
186
|
expect(result).toHaveLength(1);
|
|
145
187
|
expect(result[0]!.packageJson).toBeNull();
|
|
146
|
-
expect(result[0]!.issues).toContain("
|
|
188
|
+
expect(result[0]!.issues[0]).toContain("failed schema validation");
|
|
147
189
|
});
|
|
148
190
|
|
|
149
191
|
test("skips hidden entries and non-directories", () => {
|
|
@@ -302,6 +302,32 @@ describe("getPluginDetails (bundled catalog, offline)", () => {
|
|
|
302
302
|
expect(details.icon).toBeNull();
|
|
303
303
|
});
|
|
304
304
|
|
|
305
|
+
test("reads metadata from an installed standard-only plugin.json", async () => {
|
|
306
|
+
const target = join(workspace, "caveman");
|
|
307
|
+
mkdirSync(target, { recursive: true });
|
|
308
|
+
writeFileSync(
|
|
309
|
+
join(target, "plugin.json"),
|
|
310
|
+
JSON.stringify({
|
|
311
|
+
$schema: "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
312
|
+
name: "caveman",
|
|
313
|
+
version: "4.0.0",
|
|
314
|
+
description: "Installed standard plugin",
|
|
315
|
+
homepage: "https://example.com/plugin",
|
|
316
|
+
license: "MIT",
|
|
317
|
+
}),
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
const details = await getPluginDetails(
|
|
321
|
+
{ name: "caveman" },
|
|
322
|
+
{ fetch: makeFetch({}), workspacePluginsDir: workspace },
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
expect(details.version).toBe("4.0.0");
|
|
326
|
+
expect(details.description).toBe("Installed standard plugin");
|
|
327
|
+
expect(details.homepage).toBe("https://example.com/plugin");
|
|
328
|
+
expect(details.license).toBe("MIT");
|
|
329
|
+
});
|
|
330
|
+
|
|
305
331
|
test("surfaces the installed copy's vellum.icon", async () => {
|
|
306
332
|
// GIVEN an installed copy whose package.json declares vellum.icon
|
|
307
333
|
const target = join(workspace, "caveman");
|
|
@@ -21,15 +21,34 @@ import {
|
|
|
21
21
|
} from "node:fs";
|
|
22
22
|
import { tmpdir } from "node:os";
|
|
23
23
|
import { join } from "node:path";
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
afterEach,
|
|
26
|
+
beforeEach,
|
|
27
|
+
describe,
|
|
28
|
+
expect,
|
|
29
|
+
jest,
|
|
30
|
+
mock,
|
|
31
|
+
test,
|
|
32
|
+
} from "bun:test";
|
|
33
|
+
|
|
34
|
+
const deletePluginMcpOAuthCredentials = jest.fn(async () => ({
|
|
35
|
+
ok: true,
|
|
36
|
+
unreachable: false,
|
|
37
|
+
matchedKeys: [] as string[],
|
|
38
|
+
failedKeys: [] as string[],
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
mock.module("../../../mcp/mcp-oauth-provider.js", () => ({
|
|
42
|
+
deletePluginMcpOAuthCredentials,
|
|
43
|
+
}));
|
|
25
44
|
|
|
26
45
|
import { resetHookCacheForTests } from "../../../hooks/hook-loader.js";
|
|
27
46
|
import { getWorkspacePluginsDir } from "../../../util/platform.js";
|
|
28
47
|
import { InvalidPluginNameError } from "../install-from-github.js";
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
48
|
+
const { PluginNotInstalledError, uninstallPlugin } =
|
|
49
|
+
await import("../uninstall-plugin.js");
|
|
50
|
+
const { PLUGIN_UNINSTALL_WARNING_KEYS } =
|
|
51
|
+
await import("../uninstall-plugin.js");
|
|
33
52
|
|
|
34
53
|
let pluginsDir: string;
|
|
35
54
|
|
|
@@ -38,6 +57,13 @@ beforeEach(() => {
|
|
|
38
57
|
rmSync(pluginsDir, { recursive: true, force: true });
|
|
39
58
|
mkdirSync(pluginsDir, { recursive: true });
|
|
40
59
|
resetHookCacheForTests();
|
|
60
|
+
deletePluginMcpOAuthCredentials.mockReset();
|
|
61
|
+
deletePluginMcpOAuthCredentials.mockImplementation(async () => ({
|
|
62
|
+
ok: true,
|
|
63
|
+
unreachable: false,
|
|
64
|
+
matchedKeys: [],
|
|
65
|
+
failedKeys: [],
|
|
66
|
+
}));
|
|
41
67
|
});
|
|
42
68
|
|
|
43
69
|
afterEach(() => {
|
|
@@ -164,6 +190,97 @@ describe("uninstallPlugin", () => {
|
|
|
164
190
|
expect(existsSync(marker)).toBe(false);
|
|
165
191
|
});
|
|
166
192
|
|
|
193
|
+
test("preserves the plugin when credential deletion fails", async () => {
|
|
194
|
+
const target = writePlugin("cleanup-fails");
|
|
195
|
+
deletePluginMcpOAuthCredentials.mockImplementationOnce(async () => ({
|
|
196
|
+
ok: false,
|
|
197
|
+
unreachable: false,
|
|
198
|
+
matchedKeys: ["mcp-plugin/v1/key"],
|
|
199
|
+
failedKeys: ["mcp-plugin/v1/key"],
|
|
200
|
+
}));
|
|
201
|
+
|
|
202
|
+
await expect(
|
|
203
|
+
uninstallPlugin({
|
|
204
|
+
name: "cleanup-fails",
|
|
205
|
+
workspacePluginsDir: pluginsDir,
|
|
206
|
+
}),
|
|
207
|
+
).rejects.toThrow("MCP OAuth credentials could not be deleted");
|
|
208
|
+
expect(existsSync(target)).toBe(true);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("preserves an MCP plugin when credential storage is unavailable", async () => {
|
|
212
|
+
const target = writePlugin("offline-mcp");
|
|
213
|
+
writeFileSync(join(target, "mcp.json"), JSON.stringify({ mcpServers: {} }));
|
|
214
|
+
deletePluginMcpOAuthCredentials.mockImplementationOnce(async () => ({
|
|
215
|
+
ok: false,
|
|
216
|
+
unreachable: true,
|
|
217
|
+
matchedKeys: [],
|
|
218
|
+
failedKeys: [],
|
|
219
|
+
}));
|
|
220
|
+
|
|
221
|
+
await expect(
|
|
222
|
+
uninstallPlugin({
|
|
223
|
+
name: "offline-mcp",
|
|
224
|
+
workspacePluginsDir: pluginsDir,
|
|
225
|
+
}),
|
|
226
|
+
).rejects.toThrow("credential storage is unavailable");
|
|
227
|
+
expect(existsSync(target)).toBe(true);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("uses the install fingerprint as MCP evidence when the file was removed", async () => {
|
|
231
|
+
const target = writePlugin("recorded-mcp");
|
|
232
|
+
writeFileSync(
|
|
233
|
+
join(target, "install-meta.json"),
|
|
234
|
+
JSON.stringify({
|
|
235
|
+
name: "recorded-mcp",
|
|
236
|
+
source: {
|
|
237
|
+
kind: "github",
|
|
238
|
+
owner: "example",
|
|
239
|
+
repo: "plugin",
|
|
240
|
+
ref: "main",
|
|
241
|
+
},
|
|
242
|
+
fingerprint: {
|
|
243
|
+
algorithm: "sha256",
|
|
244
|
+
files: { "mcp.json": "digest" },
|
|
245
|
+
},
|
|
246
|
+
}),
|
|
247
|
+
);
|
|
248
|
+
deletePluginMcpOAuthCredentials.mockImplementationOnce(async () => ({
|
|
249
|
+
ok: false,
|
|
250
|
+
unreachable: true,
|
|
251
|
+
matchedKeys: [],
|
|
252
|
+
failedKeys: [],
|
|
253
|
+
}));
|
|
254
|
+
|
|
255
|
+
await expect(
|
|
256
|
+
uninstallPlugin({
|
|
257
|
+
name: "recorded-mcp",
|
|
258
|
+
workspacePluginsDir: pluginsDir,
|
|
259
|
+
}),
|
|
260
|
+
).rejects.toThrow("credential storage is unavailable");
|
|
261
|
+
expect(existsSync(target)).toBe(true);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("removes a non-MCP plugin offline and returns a visible warning", async () => {
|
|
265
|
+
const target = writePlugin("offline-simple");
|
|
266
|
+
deletePluginMcpOAuthCredentials.mockImplementationOnce(async () => ({
|
|
267
|
+
ok: false,
|
|
268
|
+
unreachable: true,
|
|
269
|
+
matchedKeys: [],
|
|
270
|
+
failedKeys: [],
|
|
271
|
+
}));
|
|
272
|
+
|
|
273
|
+
const result = await uninstallPlugin({
|
|
274
|
+
name: "offline-simple",
|
|
275
|
+
workspacePluginsDir: pluginsDir,
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
expect(existsSync(target)).toBe(false);
|
|
279
|
+
expect(result.warnings).toEqual([
|
|
280
|
+
PLUGIN_UNINSTALL_WARNING_KEYS.MCP_OAUTH_CREDENTIALS_UNCHECKED,
|
|
281
|
+
]);
|
|
282
|
+
});
|
|
283
|
+
|
|
167
284
|
test.each([
|
|
168
285
|
["../escape"],
|
|
169
286
|
["/abs/path"],
|
|
@@ -135,6 +135,19 @@
|
|
|
135
135
|
"homepage": "https://github.com/topoteretes/cognee-integrations/tree/main/integrations/vellum-assistant",
|
|
136
136
|
"icon": "\ud83d\udd78\ufe0f"
|
|
137
137
|
},
|
|
138
|
+
{
|
|
139
|
+
"name": "contractor-voice-invoicer",
|
|
140
|
+
"description": "Converts 20-second truck voice memos and job photos into itemized client invoices with custom labor rates, material markups, and one-click accounting staging.",
|
|
141
|
+
"category": "commerce",
|
|
142
|
+
"icon": "🔧",
|
|
143
|
+
"license": "MIT",
|
|
144
|
+
"homepage": "https://github.com/ZeebBoyBlue/contractor-voice-invoicer",
|
|
145
|
+
"source": {
|
|
146
|
+
"source": "github",
|
|
147
|
+
"repo": "ZeebBoyBlue/contractor-voice-invoicer",
|
|
148
|
+
"ref": "31e21ea1aa90b3ba462ed7208988931411226daa"
|
|
149
|
+
}
|
|
150
|
+
},
|
|
138
151
|
{
|
|
139
152
|
"name": "creatorland",
|
|
140
153
|
"source": {
|
|
@@ -52,6 +52,10 @@ import {
|
|
|
52
52
|
getWorkspacePluginsDir,
|
|
53
53
|
isWindows,
|
|
54
54
|
} from "../../util/platform.js";
|
|
55
|
+
import {
|
|
56
|
+
getPluginManifestInstallAction,
|
|
57
|
+
readPluginManifest,
|
|
58
|
+
} from "../../util/plugin-manifest.js";
|
|
55
59
|
import type { FetchLike } from "./fetch-like.js";
|
|
56
60
|
import {
|
|
57
61
|
type DependencyInstaller,
|
|
@@ -860,16 +864,18 @@ export async function materializePluginTree(
|
|
|
860
864
|
// stub for it, overlay the stub and run its transform so the materialized
|
|
861
865
|
// tree is a valid Vellum plugin. Raw clones (no stub) are left untouched,
|
|
862
866
|
// except for a minimal package.json synthesis when the upstream repo shipped
|
|
863
|
-
//
|
|
864
|
-
//
|
|
865
|
-
// fixed peer-dep range), so it produces the same bytes on initial install
|
|
867
|
+
// no recognized manifest. The synthesis is deterministic (name + fixed
|
|
868
|
+
// version + fixed peer-dep range), so it produces the same bytes on initial install
|
|
866
869
|
// and upgrade re-materialization; the fingerprint is computed after
|
|
867
870
|
// materialization, so the synthesized file is present in both baselines and
|
|
868
871
|
// the comparison stays clean.
|
|
869
872
|
if (cloned.fileCount > 0 && opts.stubRef !== null) {
|
|
870
873
|
await applyAdapterStub(opts.name, opts.stubRef, opts.destDir, deps);
|
|
871
874
|
}
|
|
872
|
-
if (
|
|
875
|
+
if (
|
|
876
|
+
cloned.fileCount > 0 &&
|
|
877
|
+
getPluginManifestInstallAction(opts.destDir) === "synthesize-legacy"
|
|
878
|
+
) {
|
|
873
879
|
synthesizeMinimalPackageJson(opts.name, opts.destDir);
|
|
874
880
|
}
|
|
875
881
|
return cloned;
|
|
@@ -1557,22 +1563,15 @@ interface WriteInstallMetaParams {
|
|
|
1557
1563
|
}
|
|
1558
1564
|
|
|
1559
1565
|
/**
|
|
1560
|
-
* Read the `version` field from a staged plugin's
|
|
1561
|
-
* missing or malformed manifest
|
|
1566
|
+
* Read the `version` field from a staged plugin's selected manifest. A
|
|
1567
|
+
* missing or malformed manifest yields `undefined` so provenance is
|
|
1562
1568
|
* recorded without it rather than failing the install.
|
|
1563
1569
|
*/
|
|
1564
|
-
function
|
|
1565
|
-
const pkgPath = join(stagingDir, "package.json");
|
|
1566
|
-
if (!existsSync(pkgPath)) {
|
|
1567
|
-
return undefined;
|
|
1568
|
-
}
|
|
1570
|
+
function readStagedManifestVersion(stagingDir: string): string | undefined {
|
|
1569
1571
|
try {
|
|
1570
|
-
const
|
|
1571
|
-
if (
|
|
1572
|
-
|
|
1573
|
-
if (typeof version === "string" && version.length > 0) {
|
|
1574
|
-
return version;
|
|
1575
|
-
}
|
|
1572
|
+
const version = readPluginManifest(stagingDir).version;
|
|
1573
|
+
if (version && version.length > 0) {
|
|
1574
|
+
return version;
|
|
1576
1575
|
}
|
|
1577
1576
|
} catch {
|
|
1578
1577
|
// fall through to undefined
|
|
@@ -1602,7 +1601,7 @@ function writeInstallMeta(
|
|
|
1602
1601
|
const meta: InstallMeta = {
|
|
1603
1602
|
origin: "vellum",
|
|
1604
1603
|
installedAt: new Date().toISOString(),
|
|
1605
|
-
version:
|
|
1604
|
+
version: readStagedManifestVersion(stagingDir),
|
|
1606
1605
|
sourceRepo: `${source.owner}/${source.repo}`,
|
|
1607
1606
|
contentHash,
|
|
1608
1607
|
author: "user",
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
* and extracts it into the plugin's install directory — it does NOT clone the
|
|
9
9
|
* plugin from GitHub itself, and it does NOT emit `plugin_installed` telemetry
|
|
10
10
|
* (hitting this endpoint *is* the recorded install; the server rejects that
|
|
11
|
-
* event type on the usual ingest path). A tarball that ships
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* event type on the usual ingest path). A tarball that ships neither supported
|
|
12
|
+
* manifest gets the same synthesized manifest the GitHub clone path writes, so
|
|
13
|
+
* the loader still registers the plugin's skills.
|
|
14
14
|
*
|
|
15
15
|
* GET {PLATFORM_BASE_URL}/v1/plugins/{name}/install/
|
|
16
16
|
*
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
import { getPlatformBaseUrl } from "../../config/env.js";
|
|
37
37
|
import { getExistingDeviceId } from "../../util/device-id.js";
|
|
38
38
|
import { getWorkspacePluginsDir } from "../../util/platform.js";
|
|
39
|
+
import { getPluginManifestInstallAction } from "../../util/plugin-manifest.js";
|
|
39
40
|
import { APP_VERSION } from "../../version.js";
|
|
40
41
|
import type { FetchLike } from "./fetch-like.js";
|
|
41
42
|
import {
|
|
@@ -205,12 +206,16 @@ export async function installPluginFromPlatform(
|
|
|
205
206
|
throw new PluginNotFoundError(name, meta.ref ?? "", meta.repo ?? name);
|
|
206
207
|
}
|
|
207
208
|
|
|
208
|
-
// Same default as the GitHub clone path: a marketplace tarball
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
209
|
+
// Same default as the GitHub clone path: a marketplace tarball without a
|
|
210
|
+
// recognized manifest still has to load. A claimed standard manifest is
|
|
211
|
+
// validated before the fingerprint and swap.
|
|
212
|
+
try {
|
|
213
|
+
if (getPluginManifestInstallAction(stagingDir) === "synthesize-legacy") {
|
|
214
|
+
synthesizeMinimalPackageJson(name, stagingDir);
|
|
215
|
+
}
|
|
216
|
+
} catch (err) {
|
|
217
|
+
rmSync(stagingDir, { recursive: true, force: true });
|
|
218
|
+
throw err;
|
|
214
219
|
}
|
|
215
220
|
|
|
216
221
|
await confirmStagedOrAbort(name, stagingDir, deps.confirmStaged);
|