@vellumai/assistant 0.12.2-staging.2 → 0.12.2-staging.3
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/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/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/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__/plugins-routes.test.ts +25 -3
- 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/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
|
@@ -10,12 +10,12 @@
|
|
|
10
10
|
* need to be renamed when the convention shifts.
|
|
11
11
|
*
|
|
12
12
|
* <pluginDir>/
|
|
13
|
-
* package.json ← manifest
|
|
14
|
-
* (npm scope stripped);
|
|
13
|
+
* package.json ← legacy Vellum manifest, selected first;
|
|
15
14
|
* peerDependencies["@vellumai/plugin-api"]
|
|
16
15
|
* semver range is checked against the
|
|
17
16
|
* running assistant version and rejects
|
|
18
17
|
* the plugin if unsatisfied
|
|
18
|
+
* plugin.json ← standard fallback when package.json is absent
|
|
19
19
|
* hooks/
|
|
20
20
|
* <name>.ts ← default export → plugin.hooks[<name>]
|
|
21
21
|
* (today the runtime invokes "init" at
|
|
@@ -41,7 +41,6 @@
|
|
|
41
41
|
*/
|
|
42
42
|
|
|
43
43
|
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
44
|
-
import { readFile } from "node:fs/promises";
|
|
45
44
|
import { basename, join } from "node:path";
|
|
46
45
|
import { pathToFileURL } from "node:url";
|
|
47
46
|
|
|
@@ -53,6 +52,10 @@ import { PLUGIN_SECRET_PATTERN_LIMITS } from "../security/plugin-secret-patterns
|
|
|
53
52
|
import { finalizeTool } from "../tools/tool-defaults.js";
|
|
54
53
|
import type { Tool, ToolDefinition } from "../tools/types.js";
|
|
55
54
|
import { getLogger } from "../util/logger.js";
|
|
55
|
+
import {
|
|
56
|
+
LEGACY_PLUGIN_MANIFEST,
|
|
57
|
+
readPluginManifest,
|
|
58
|
+
} from "../util/plugin-manifest.js";
|
|
56
59
|
import { registerPlugin } from "./registry.js";
|
|
57
60
|
import type {
|
|
58
61
|
HookFunction,
|
|
@@ -69,39 +72,12 @@ const log = getLogger("external-plugin-loader");
|
|
|
69
72
|
/** Default upper bound on how long a single plugin load may take. */
|
|
70
73
|
const DEFAULT_IMPORT_TIMEOUT_MS = 10_000;
|
|
71
74
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
* against the running assistant version and rejects the plugin on
|
|
79
|
-
* mismatch. If absent, the plugin loads without a host-compat claim
|
|
80
|
-
* (with a warning).
|
|
81
|
-
* - `credentialKeyPatterns` is typed `unknown` here and shape-validated
|
|
82
|
-
* separately ({@link parseCredentialKeyPatterns}) so a malformed
|
|
83
|
-
* declaration degrades to `undefined` instead of failing the whole
|
|
84
|
-
* `safeParse` and blocking plugin load.
|
|
85
|
-
* - Unknown fields pass through (`passthrough`) so the loader does not
|
|
86
|
-
* destructively reshape the file when the rest of the npm ecosystem
|
|
87
|
-
* writes to it.
|
|
88
|
-
*/
|
|
89
|
-
const PluginPackageJsonSchema = z
|
|
90
|
-
.object({
|
|
91
|
-
name: z.string().min(1, "package.json `name` must be a non-empty string"),
|
|
92
|
-
version: z.string().optional(),
|
|
93
|
-
peerDependencies: z.record(z.string(), z.string()).optional(),
|
|
94
|
-
credentialKeyPatterns: z.unknown().optional(),
|
|
95
|
-
/** Human title, when the package name is not one ("@vellumai/imessage"). */
|
|
96
|
-
displayName: z.string().min(1).optional(),
|
|
97
|
-
/** Standard npm field, reused as the one-line description clients show. */
|
|
98
|
-
description: z.string().min(1).optional(),
|
|
99
|
-
/** Lucide icon name without the `lucide-` prefix, matching `ChannelInfo`. */
|
|
100
|
-
icon: z.string().min(1).optional(),
|
|
101
|
-
})
|
|
102
|
-
.passthrough();
|
|
103
|
-
|
|
104
|
-
type PluginPackageJson = z.infer<typeof PluginPackageJsonSchema>;
|
|
75
|
+
interface LegacyPluginFields extends Record<string, unknown> {
|
|
76
|
+
readonly peerDependencies?: Record<string, string>;
|
|
77
|
+
readonly credentialKeyPatterns?: unknown;
|
|
78
|
+
readonly displayName?: string;
|
|
79
|
+
readonly icon?: string;
|
|
80
|
+
}
|
|
105
81
|
|
|
106
82
|
/**
|
|
107
83
|
* Shape-level caps for the `credentialKeyPatterns` manifest field. The
|
|
@@ -270,23 +246,8 @@ async function loadHooks(
|
|
|
270
246
|
* timeout/try-catch/register triple.
|
|
271
247
|
*/
|
|
272
248
|
async function buildPluginFromDir(pluginDir: string): Promise<Plugin> {
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
try {
|
|
276
|
-
rawPkg = JSON.parse(await readFile(pkgPath, "utf8"));
|
|
277
|
-
} catch (err) {
|
|
278
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
279
|
-
throw new Error(
|
|
280
|
-
`package.json at ${pluginDir} could not be read or parsed: ${reason}`,
|
|
281
|
-
);
|
|
282
|
-
}
|
|
283
|
-
const parsed = PluginPackageJsonSchema.safeParse(rawPkg);
|
|
284
|
-
if (!parsed.success) {
|
|
285
|
-
throw new Error(
|
|
286
|
-
`package.json at ${pluginDir} failed schema validation: ${parsed.error.message}`,
|
|
287
|
-
);
|
|
288
|
-
}
|
|
289
|
-
const pkg: PluginPackageJson = parsed.data;
|
|
249
|
+
const resolvedManifest = readPluginManifest(pluginDir);
|
|
250
|
+
const legacyFields = resolvedManifest.raw as LegacyPluginFields;
|
|
290
251
|
// A plugin's identity is its install directory name — the slug the plugin
|
|
291
252
|
// was installed under (a marketplace slug or a GitHub path leaf). This is
|
|
292
253
|
// the identity every other surface uses: `plugins list`, enable/disable,
|
|
@@ -296,7 +257,10 @@ async function buildPluginFromDir(pluginDir: string): Promise<Plugin> {
|
|
|
296
257
|
// registry from the identity the user toggles and the scope filters match.
|
|
297
258
|
// The manifest is still required (schema-validated above) as the load gate.
|
|
298
259
|
const name = basename(pluginDir);
|
|
299
|
-
const version =
|
|
260
|
+
const version =
|
|
261
|
+
resolvedManifest.version && resolvedManifest.version.length > 0
|
|
262
|
+
? resolvedManifest.version
|
|
263
|
+
: "0.0.0";
|
|
300
264
|
|
|
301
265
|
// Host-compat negotiation: plugins declare their plugin-api version
|
|
302
266
|
// range via standard `peerDependencies["@vellumai/plugin-api"]`. We
|
|
@@ -310,7 +274,7 @@ async function buildPluginFromDir(pluginDir: string): Promise<Plugin> {
|
|
|
310
274
|
//
|
|
311
275
|
// If the peerDep is absent, the plugin loads without a host-compat
|
|
312
276
|
// claim; we log a warning so the omission is visible at boot.
|
|
313
|
-
const range =
|
|
277
|
+
const range = legacyFields.peerDependencies?.[PLUGIN_API_PEER_DEP];
|
|
314
278
|
if (range !== undefined) {
|
|
315
279
|
if (!semver.validRange(range)) {
|
|
316
280
|
log.error(
|
|
@@ -333,7 +297,7 @@ async function buildPluginFromDir(pluginDir: string): Promise<Plugin> {
|
|
|
333
297
|
`external plugin ${name}: peerDependencies["${PLUGIN_API_PEER_DEP}"] requires "${range}" but assistant is ${assistantPkg.version} — loading anyway`,
|
|
334
298
|
);
|
|
335
299
|
}
|
|
336
|
-
} else {
|
|
300
|
+
} else if (resolvedManifest.source === LEGACY_PLUGIN_MANIFEST) {
|
|
337
301
|
log.warn(
|
|
338
302
|
{ pluginDir, plugin: name, peerDep: PLUGIN_API_PEER_DEP },
|
|
339
303
|
"external plugin missing plugin-api peerDependency — loading without host-compat claim",
|
|
@@ -342,7 +306,9 @@ async function buildPluginFromDir(pluginDir: string): Promise<Plugin> {
|
|
|
342
306
|
|
|
343
307
|
const manifest: PluginManifest = { name, version };
|
|
344
308
|
const credentialKeyPatterns = parseCredentialKeyPatterns(
|
|
345
|
-
|
|
309
|
+
resolvedManifest.source === LEGACY_PLUGIN_MANIFEST
|
|
310
|
+
? legacyFields.credentialKeyPatterns
|
|
311
|
+
: undefined,
|
|
346
312
|
pluginDir,
|
|
347
313
|
);
|
|
348
314
|
if (credentialKeyPatterns !== undefined) {
|
|
@@ -459,9 +425,9 @@ export async function loadExternalPlugin(
|
|
|
459
425
|
}
|
|
460
426
|
|
|
461
427
|
/**
|
|
462
|
-
* Parse a plugin's
|
|
428
|
+
* Parse a plugin's selected manifest from disk. Returns the plugin
|
|
463
429
|
* identity (its install directory name), version, and any declared
|
|
464
|
-
* `credentialKeyPatterns`, or `undefined` when the
|
|
430
|
+
* legacy `credentialKeyPatterns`, or `undefined` when the manifest is
|
|
465
431
|
* missing, unparseable, or fails schema validation.
|
|
466
432
|
*
|
|
467
433
|
* Exported so the mtime cache can discover plugin identity without going
|
|
@@ -497,20 +463,23 @@ export interface PluginPresentation {
|
|
|
497
463
|
export async function parsePluginPresentation(
|
|
498
464
|
pluginDir: string,
|
|
499
465
|
): Promise<PluginPresentation | undefined> {
|
|
500
|
-
let rawPkg: unknown;
|
|
501
466
|
try {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
467
|
+
const manifest = readPluginManifest(pluginDir);
|
|
468
|
+
const legacyFields = manifest.raw as LegacyPluginFields;
|
|
469
|
+
return {
|
|
470
|
+
displayName:
|
|
471
|
+
manifest.source === LEGACY_PLUGIN_MANIFEST
|
|
472
|
+
? legacyFields.displayName
|
|
473
|
+
: manifest.name,
|
|
474
|
+
description: manifest.description,
|
|
475
|
+
icon:
|
|
476
|
+
manifest.source === LEGACY_PLUGIN_MANIFEST
|
|
477
|
+
? legacyFields.icon
|
|
478
|
+
: undefined,
|
|
479
|
+
};
|
|
505
480
|
} catch {
|
|
506
481
|
return undefined;
|
|
507
482
|
}
|
|
508
|
-
const parsed = PluginPackageJsonSchema.safeParse(rawPkg);
|
|
509
|
-
if (!parsed.success) {
|
|
510
|
-
return undefined;
|
|
511
|
-
}
|
|
512
|
-
const { displayName, description, icon } = parsed.data;
|
|
513
|
-
return { displayName, description, icon };
|
|
514
483
|
}
|
|
515
484
|
|
|
516
485
|
export async function parsePluginManifest(
|
|
@@ -519,35 +488,29 @@ export async function parsePluginManifest(
|
|
|
519
488
|
): Promise<
|
|
520
489
|
Pick<PluginManifest, "name" | "version" | "credentialKeyPatterns"> | undefined
|
|
521
490
|
> {
|
|
522
|
-
|
|
523
|
-
let rawPkg: unknown;
|
|
491
|
+
let resolvedManifest: ReturnType<typeof readPluginManifest>;
|
|
524
492
|
try {
|
|
525
|
-
|
|
493
|
+
resolvedManifest = readPluginManifest(pluginDir);
|
|
526
494
|
} catch (err) {
|
|
527
495
|
const reason = err instanceof Error ? err.message : String(err);
|
|
528
496
|
if (!opts.quiet) {
|
|
529
497
|
log.error(
|
|
530
498
|
{ err, pluginDir },
|
|
531
|
-
`
|
|
532
|
-
);
|
|
533
|
-
}
|
|
534
|
-
return undefined;
|
|
535
|
-
}
|
|
536
|
-
const parsed = PluginPackageJsonSchema.safeParse(rawPkg);
|
|
537
|
-
if (!parsed.success) {
|
|
538
|
-
if (!opts.quiet) {
|
|
539
|
-
log.error(
|
|
540
|
-
{ err: parsed.error, pluginDir },
|
|
541
|
-
`package.json at ${pluginDir} failed schema validation: ${parsed.error.message}`,
|
|
499
|
+
`Plugin manifest at ${pluginDir} could not be read or validated: ${reason}`,
|
|
542
500
|
);
|
|
543
501
|
}
|
|
544
502
|
return undefined;
|
|
545
503
|
}
|
|
546
|
-
const
|
|
504
|
+
const legacyFields = resolvedManifest.raw as LegacyPluginFields;
|
|
547
505
|
const name = basename(pluginDir);
|
|
548
|
-
const version =
|
|
506
|
+
const version =
|
|
507
|
+
resolvedManifest.version && resolvedManifest.version.length > 0
|
|
508
|
+
? resolvedManifest.version
|
|
509
|
+
: "0.0.0";
|
|
549
510
|
const credentialKeyPatterns = parseCredentialKeyPatterns(
|
|
550
|
-
|
|
511
|
+
resolvedManifest.source === LEGACY_PLUGIN_MANIFEST
|
|
512
|
+
? legacyFields.credentialKeyPatterns
|
|
513
|
+
: undefined,
|
|
551
514
|
pluginDir,
|
|
552
515
|
);
|
|
553
516
|
if (credentialKeyPatterns !== undefined) {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Enumerate installed plugin directories under the workspace plugins dir.
|
|
3
3
|
*
|
|
4
|
-
* An installed plugin is a non-hidden directory carrying a `package.json
|
|
5
|
-
* whose realpath stays under the realpath of the plugins
|
|
4
|
+
* An installed plugin is a non-hidden directory carrying a `package.json` or
|
|
5
|
+
* root `plugin.json` whose realpath stays under the realpath of the plugins
|
|
6
|
+
* directory. This walk
|
|
6
7
|
* is shared by the plugin source-version collector
|
|
7
8
|
* (`./collect-source-versions.ts`) and the schedule reconciler
|
|
8
9
|
* (`../schedule/plugin-schedule-reconciler.ts`) so both agree on what counts
|
|
@@ -17,10 +18,11 @@
|
|
|
17
18
|
* declaration is available to run.
|
|
18
19
|
*/
|
|
19
20
|
|
|
20
|
-
import {
|
|
21
|
+
import { readdirSync, realpathSync, statSync } from "node:fs";
|
|
21
22
|
import { join, sep } from "node:path";
|
|
22
23
|
|
|
23
24
|
import { getWorkspacePluginsDir } from "../util/platform.js";
|
|
25
|
+
import { hasPluginManifest } from "../util/plugin-manifest.js";
|
|
24
26
|
|
|
25
27
|
export interface InstalledPluginDir {
|
|
26
28
|
/** Directory basename: the plugin's install identity. */
|
|
@@ -92,7 +94,7 @@ export function listInstalledPluginDirs(): InstalledPluginDir[] {
|
|
|
92
94
|
if (!isInsidePluginRoot(dir, pluginsDir)) {
|
|
93
95
|
continue;
|
|
94
96
|
}
|
|
95
|
-
if (!
|
|
97
|
+
if (!hasPluginManifest(dir)) {
|
|
96
98
|
continue;
|
|
97
99
|
}
|
|
98
100
|
out.push({ name: entry, dir });
|
|
@@ -21,19 +21,11 @@
|
|
|
21
21
|
* disables only that entry. Neither ever throws, because a malformed file
|
|
22
22
|
* in one plugin must not remove another plugin's servers from a listing.
|
|
23
23
|
*
|
|
24
|
-
* Authentication is
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* Consumers must not resolve a plugin server's id against the
|
|
31
|
-
* `mcp:<serverId>:*` credential namespace. Those keys belong to
|
|
32
|
-
* workspace-configured servers, and a plugin controls both its server key
|
|
33
|
-
* and its URL, so honoring them for a plugin-declared server would send a
|
|
34
|
-
* workspace credential to an endpoint the plugin chose. Every config built
|
|
35
|
-
* here carries `source: "plugin"`, which is what `McpClient` reads to
|
|
36
|
-
* decide, so the rule travels with the server rather than with the caller.
|
|
24
|
+
* Authentication is client-managed. Agent Plugins 1.0.0 defines no portable
|
|
25
|
+
* OAuth or credential-reference fields, and any `headers` in the file are
|
|
26
|
+
* literal package data. Runtime configs carry the declaring plugin directory
|
|
27
|
+
* name and original server key so OAuth credentials can be isolated from the
|
|
28
|
+
* public server id and from workspace credentials.
|
|
37
29
|
*/
|
|
38
30
|
|
|
39
31
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -141,7 +133,7 @@ export function interpolatePluginPaths(
|
|
|
141
133
|
*
|
|
142
134
|
* `listAllPlugins` is an inventory of directories and reports a malformed
|
|
143
135
|
* entry rather than dropping it, so it happily returns a directory with no
|
|
144
|
-
* usable
|
|
136
|
+
* usable selected manifest. The runtime loader rejects those in
|
|
145
137
|
* `parsePluginManifest`: the manifest must parse and carry a non-empty
|
|
146
138
|
* `name`. Applying the same gate here keeps `mcp.json` from being honored
|
|
147
139
|
* for a directory that will never load as a plugin.
|
|
@@ -192,7 +184,7 @@ export function readPluginMcpServers(
|
|
|
192
184
|
if (!hasLoadableManifest(plugin)) {
|
|
193
185
|
issues.push({
|
|
194
186
|
pluginName: plugin.name,
|
|
195
|
-
message: `${PLUGIN_MCP_MANIFEST} ignored:
|
|
187
|
+
message: `${PLUGIN_MCP_MANIFEST} ignored: the selected plugin manifest is missing or invalid, so the runtime will not load this directory as a plugin`,
|
|
196
188
|
});
|
|
197
189
|
continue;
|
|
198
190
|
}
|
|
@@ -257,6 +249,8 @@ export function readPluginMcpServers(
|
|
|
257
249
|
config: {
|
|
258
250
|
transport: projectTransport(entry.data, plugin.target),
|
|
259
251
|
source: "plugin",
|
|
252
|
+
pluginName: plugin.name,
|
|
253
|
+
serverKey,
|
|
260
254
|
},
|
|
261
255
|
});
|
|
262
256
|
}
|
|
@@ -59,6 +59,7 @@ import {
|
|
|
59
59
|
getWorkspaceHooksDir,
|
|
60
60
|
getWorkspacePluginsDir,
|
|
61
61
|
} from "../util/platform.js";
|
|
62
|
+
import { hasPluginManifest } from "../util/plugin-manifest.js";
|
|
62
63
|
import { collectSourceVersions } from "./collect-source-versions.js";
|
|
63
64
|
import {
|
|
64
65
|
deriveToolName,
|
|
@@ -768,7 +769,7 @@ async function scanPlugins(): Promise<void> {
|
|
|
768
769
|
);
|
|
769
770
|
continue;
|
|
770
771
|
}
|
|
771
|
-
if (!
|
|
772
|
+
if (!hasPluginManifest(pluginDir)) {
|
|
772
773
|
continue;
|
|
773
774
|
}
|
|
774
775
|
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* User plugin loader — discovers plugins under `<workspaceDir>/plugins/*` and
|
|
3
3
|
* populates the mtime cache.
|
|
4
4
|
*
|
|
5
|
-
* A plugin directory is recognized by a `package.json`
|
|
5
|
+
* A plugin directory is recognized by a legacy `package.json` or standard
|
|
6
|
+
* `plugin.json` manifest. The loader
|
|
6
7
|
* delegates to the mtime cache's `populateCacheAtBoot`, which builds each
|
|
7
8
|
* plugin via `buildExternalPlugin` and stores it keyed by mtime. The cache
|
|
8
9
|
* is the single source of truth for user plugin state — the registry is no
|
|
@@ -107,6 +107,51 @@ describe("testInferenceConnection", () => {
|
|
|
107
107
|
expect(calls[0].body).not.toHaveProperty(OPENCODE_SESSION_HEADER);
|
|
108
108
|
});
|
|
109
109
|
|
|
110
|
+
test("probes /responses for an OpenCode responses-only model", async () => {
|
|
111
|
+
const calls: ProbeCall[] = [];
|
|
112
|
+
const result = await testInferenceConnection(
|
|
113
|
+
{
|
|
114
|
+
provider: "opencode",
|
|
115
|
+
auth: { type: "none" },
|
|
116
|
+
baseUrl: "https://opencode.ai/zen/v1",
|
|
117
|
+
models: [{ id: "muse-spark-1.2-contributor-free" }],
|
|
118
|
+
},
|
|
119
|
+
stubFetch(200, calls),
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
expect(result).toMatchObject({
|
|
123
|
+
ok: true,
|
|
124
|
+
resolved_url: "https://opencode.ai/zen/v1/responses",
|
|
125
|
+
});
|
|
126
|
+
expect(calls[0].body).toMatchObject({
|
|
127
|
+
model: "muse-spark-1.2-contributor-free",
|
|
128
|
+
input: "ping",
|
|
129
|
+
max_output_tokens: 16,
|
|
130
|
+
});
|
|
131
|
+
expect(calls[0].headers[OPENCODE_SESSION_HEADER]).toBeTruthy();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("keeps other OpenCode models on /chat/completions", async () => {
|
|
135
|
+
const calls: ProbeCall[] = [];
|
|
136
|
+
const result = await testInferenceConnection(
|
|
137
|
+
{
|
|
138
|
+
provider: "opencode",
|
|
139
|
+
auth: { type: "none" },
|
|
140
|
+
baseUrl: "https://opencode.ai/zen/v1",
|
|
141
|
+
models: [{ id: "mimo-v2.5-free" }],
|
|
142
|
+
},
|
|
143
|
+
stubFetch(200, calls),
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
expect(result?.resolved_url).toBe(
|
|
147
|
+
"https://opencode.ai/zen/v1/chat/completions",
|
|
148
|
+
);
|
|
149
|
+
expect(calls[0].body).toMatchObject({
|
|
150
|
+
model: "mimo-v2.5-free",
|
|
151
|
+
max_tokens: 1,
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
110
155
|
test("does not send OpenCode headers to other providers", async () => {
|
|
111
156
|
const calls: ProbeCall[] = [];
|
|
112
157
|
await testInferenceConnection(CONNECTION, stubFetch(200, calls));
|
|
@@ -10,7 +10,10 @@
|
|
|
10
10
|
import { z } from "zod";
|
|
11
11
|
|
|
12
12
|
import { getLogger } from "../../util/logger.js";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
OPENCODE_RESPONSES_ONLY_MODELS,
|
|
15
|
+
resolveOpenCodeRequestHeaders,
|
|
16
|
+
} from "../opencode/client.js";
|
|
14
17
|
import type { Auth, ConnectionModel } from "./auth.js";
|
|
15
18
|
import { resolveAuth } from "./resolve-auth.js";
|
|
16
19
|
|
|
@@ -46,10 +49,12 @@ function hintForStatus(status: number): string {
|
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
/**
|
|
49
|
-
* Probe a connection's custom endpoint with a minimal
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
52
|
+
* Probe a connection's custom endpoint with a minimal request on the wire
|
|
53
|
+
* its first model is served over: chat completions (`max_tokens: 1`), or
|
|
54
|
+
* the Responses API for OpenCode's responses-only models, which answer
|
|
55
|
+
* `/chat/completions` with HTTP 500. Returns `null` when there is nothing
|
|
56
|
+
* to probe: no custom base URL, no model id to send, or auth that cannot be
|
|
57
|
+
* resolved (a missing credential surfaces through its own error path).
|
|
53
58
|
*/
|
|
54
59
|
export async function testInferenceConnection(
|
|
55
60
|
connection: {
|
|
@@ -78,7 +83,12 @@ export async function testInferenceConnection(
|
|
|
78
83
|
const providerHeaders =
|
|
79
84
|
connection.provider === "opencode" ? resolveOpenCodeRequestHeaders() : {};
|
|
80
85
|
|
|
81
|
-
const
|
|
86
|
+
const useResponses =
|
|
87
|
+
connection.provider === "opencode" &&
|
|
88
|
+
OPENCODE_RESPONSES_ONLY_MODELS.has(model);
|
|
89
|
+
const url = `${connection.baseUrl.replace(/\/+$/, "")}${
|
|
90
|
+
useResponses ? "/responses" : "/chat/completions"
|
|
91
|
+
}`;
|
|
82
92
|
try {
|
|
83
93
|
const res = await fetchImpl(url, {
|
|
84
94
|
method: "POST",
|
|
@@ -87,12 +97,17 @@ export async function testInferenceConnection(
|
|
|
87
97
|
...providerHeaders,
|
|
88
98
|
"Content-Type": "application/json",
|
|
89
99
|
},
|
|
90
|
-
body: JSON.stringify(
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
100
|
+
body: JSON.stringify(
|
|
101
|
+
useResponses
|
|
102
|
+
? // The Responses API rejects output budgets below 16 tokens.
|
|
103
|
+
{ model, input: "ping", max_output_tokens: 16, stream: false }
|
|
104
|
+
: {
|
|
105
|
+
model,
|
|
106
|
+
messages: [{ role: "user", content: "ping" }],
|
|
107
|
+
max_tokens: 1,
|
|
108
|
+
stream: false,
|
|
109
|
+
},
|
|
110
|
+
),
|
|
96
111
|
signal: AbortSignal.timeout(PROBE_TIMEOUT_MS),
|
|
97
112
|
});
|
|
98
113
|
void res.body?.cancel();
|
|
@@ -109,7 +109,9 @@ function mapResponsesReasoningEffort(
|
|
|
109
109
|
return "none";
|
|
110
110
|
}
|
|
111
111
|
const clamped = clampReasoningEffort(raw, effortCeilingForModel(model));
|
|
112
|
-
return supported
|
|
112
|
+
return supported
|
|
113
|
+
? snapReasoningEffortToSupported(clamped, supported)
|
|
114
|
+
: clamped;
|
|
113
115
|
}
|
|
114
116
|
|
|
115
117
|
/** Values accepted by the Responses API `text.verbosity` parameter. */
|
|
@@ -280,7 +282,11 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
280
282
|
const effort = configObj?.effort as string | undefined;
|
|
281
283
|
const verbosity = configObj?.verbosity as string | undefined;
|
|
282
284
|
const usageAttributionHeaders = configObj?.usageAttributionHeaders as
|
|
283
|
-
Record<string, string>
|
|
285
|
+
| Record<string, string>
|
|
286
|
+
| undefined;
|
|
287
|
+
const perRequestHeaders = configObj?.requestHeaders as
|
|
288
|
+
| Record<string, string>
|
|
289
|
+
| undefined;
|
|
284
290
|
const disableCache = configObj?.disableCache === true;
|
|
285
291
|
const disableTurnStartCache = configObj?.disableTurnStartCache === true;
|
|
286
292
|
const promptCacheKey =
|
|
@@ -463,6 +469,7 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
463
469
|
const requestHeaders = {
|
|
464
470
|
...this.requestHeaders,
|
|
465
471
|
...(usageAttributionHeaders ?? {}),
|
|
472
|
+
...(perRequestHeaders ?? {}),
|
|
466
473
|
};
|
|
467
474
|
const stream = await responsesApi.create(
|
|
468
475
|
{ ...params, stream: true },
|