@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
|
@@ -2,22 +2,23 @@
|
|
|
2
2
|
* Routes for browser tab management commands.
|
|
3
3
|
*
|
|
4
4
|
* Exposes `browser_tabs` so CLI commands can list, create, select, and
|
|
5
|
-
* close browser tabs
|
|
5
|
+
* close browser tabs in the Chrome extension or streamed desktop browser.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { z } from "zod";
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { shouldUseVirtualDesktopBrowser } from "../../browser/virtual-desktop-target.js";
|
|
11
11
|
import { HostBrowserProxy } from "../../daemon/host-browser-proxy.js";
|
|
12
|
+
import { executeDesktopBrowserTabs } from "../../desktop/desktop-browser-operations.js";
|
|
13
|
+
import { normalizeBrowserMode } from "../../tools/browser/browser-mode.js";
|
|
12
14
|
import { getCdpClient } from "../../tools/browser/cdp-client/factory.js";
|
|
13
15
|
import {
|
|
14
16
|
clearPinnedTab,
|
|
15
17
|
clearPinnedTabByTabId,
|
|
16
18
|
setPinnedTab,
|
|
17
19
|
} from "../../tools/browser/pinned-tabs.js";
|
|
18
|
-
import type { ToolContext } from "../../tools/types.js";
|
|
19
20
|
import { LOCAL_PRINCIPALS } from "../auth/route-policy.js";
|
|
20
|
-
import {
|
|
21
|
+
import { resolveBrowserContext } from "./browser-context.js";
|
|
21
22
|
import { BadRequestError } from "./errors.js";
|
|
22
23
|
import { parseBody } from "./parse-body.js";
|
|
23
24
|
import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
|
|
@@ -26,6 +27,8 @@ const BrowserTabsParams = z.object({
|
|
|
26
27
|
command: z.enum(["list", "select", "new", "close"]),
|
|
27
28
|
sessionId: z.string().min(1).default("default"),
|
|
28
29
|
conversationId: z.string().min(1).optional(),
|
|
30
|
+
desktop: z.boolean().optional(),
|
|
31
|
+
browserMode: z.string().optional(),
|
|
29
32
|
tabId: z.number().optional(),
|
|
30
33
|
url: z.string().optional(),
|
|
31
34
|
// Route tab operations to a specific extension client in multi-client
|
|
@@ -33,33 +36,73 @@ const BrowserTabsParams = z.object({
|
|
|
33
36
|
targetClientId: z.string().min(1).optional(),
|
|
34
37
|
});
|
|
35
38
|
|
|
36
|
-
async function handleBrowserTabs({
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
transportInterface: conversation?.transportInterface,
|
|
52
|
-
} as unknown as ToolContext;
|
|
39
|
+
async function handleBrowserTabs({
|
|
40
|
+
body = {},
|
|
41
|
+
headers = {},
|
|
42
|
+
abortSignal,
|
|
43
|
+
}: RouteHandlerArgs) {
|
|
44
|
+
const {
|
|
45
|
+
command,
|
|
46
|
+
sessionId,
|
|
47
|
+
conversationId,
|
|
48
|
+
tabId,
|
|
49
|
+
url,
|
|
50
|
+
targetClientId,
|
|
51
|
+
desktop,
|
|
52
|
+
browserMode,
|
|
53
|
+
} = parseBody(BrowserTabsParams, body);
|
|
53
54
|
|
|
54
|
-
const
|
|
55
|
+
const context = await resolveBrowserContext(
|
|
56
|
+
conversationId,
|
|
57
|
+
sessionId,
|
|
58
|
+
headers,
|
|
59
|
+
abortSignal,
|
|
60
|
+
);
|
|
61
|
+
const resolvedConversationId = context.conversationId;
|
|
62
|
+
const mode = normalizeBrowserMode(browserMode);
|
|
63
|
+
if ("error" in mode) {
|
|
64
|
+
throw new BadRequestError(mode.error);
|
|
65
|
+
}
|
|
66
|
+
if (
|
|
67
|
+
shouldUseVirtualDesktopBrowser(
|
|
68
|
+
desktop,
|
|
69
|
+
{ browser_mode: browserMode, target_client_id: targetClientId },
|
|
70
|
+
context,
|
|
71
|
+
)
|
|
72
|
+
) {
|
|
73
|
+
if (
|
|
74
|
+
targetClientId ||
|
|
75
|
+
(mode.mode !== "auto" && mode.mode !== "cdp-inspect")
|
|
76
|
+
) {
|
|
77
|
+
throw new BadRequestError(
|
|
78
|
+
"--virtual-desktop cannot target a personal browser client",
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
const result = await executeDesktopBrowserTabs(
|
|
82
|
+
{ command, tabId, url },
|
|
83
|
+
context,
|
|
84
|
+
);
|
|
85
|
+
if (result.isError) {
|
|
86
|
+
throw new BadRequestError(result.content);
|
|
87
|
+
}
|
|
88
|
+
return JSON.parse(result.content);
|
|
89
|
+
}
|
|
55
90
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
91
|
+
const cdpOptions = {
|
|
92
|
+
mode:
|
|
93
|
+
targetClientId || mode.mode === "auto"
|
|
94
|
+
? ("extension" as const)
|
|
95
|
+
: mode.mode,
|
|
61
96
|
targetClientId,
|
|
62
|
-
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// Absorb brief reconnects before dispatching to the extension.
|
|
100
|
+
if (cdpOptions.mode === "extension") {
|
|
101
|
+
await HostBrowserProxy.instance.waitForExtensionClient(
|
|
102
|
+
context.sourceActorPrincipalId,
|
|
103
|
+
targetClientId,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
63
106
|
|
|
64
107
|
if (command === "list") {
|
|
65
108
|
const cdp = getCdpClient(context, cdpOptions);
|
|
@@ -185,7 +228,7 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
185
228
|
handler: handleBrowserTabs,
|
|
186
229
|
summary: "Manage browser tabs",
|
|
187
230
|
description:
|
|
188
|
-
"List, create, select, or close browser tabs
|
|
231
|
+
"List, create, select, or close browser tabs in the Chrome extension or streamed desktop browser.",
|
|
189
232
|
tags: ["browser"],
|
|
190
233
|
requestBody: BrowserTabsParams,
|
|
191
234
|
responseBody: z.object({
|
|
@@ -14,6 +14,7 @@ import { ROUTES } from "./desktop-setup-routes.js";
|
|
|
14
14
|
import { NotFoundError } from "./errors.js";
|
|
15
15
|
|
|
16
16
|
const originalContainerized = process.env.IS_CONTAINERIZED;
|
|
17
|
+
const originalPlatform = process.env.IS_PLATFORM;
|
|
17
18
|
const status = spyOn(desktopDependencyInstaller, "getStatus");
|
|
18
19
|
const start = spyOn(desktopDependencyInstaller, "start");
|
|
19
20
|
|
|
@@ -24,6 +25,7 @@ afterAll(() => {
|
|
|
24
25
|
|
|
25
26
|
beforeEach(() => {
|
|
26
27
|
process.env.IS_CONTAINERIZED = "true";
|
|
28
|
+
process.env.IS_PLATFORM = "true";
|
|
27
29
|
status.mockReturnValue({ state: "required" });
|
|
28
30
|
start.mockReturnValue({ state: "installing", stage: "packages" });
|
|
29
31
|
});
|
|
@@ -34,6 +36,11 @@ afterEach(() => {
|
|
|
34
36
|
} else {
|
|
35
37
|
process.env.IS_CONTAINERIZED = originalContainerized;
|
|
36
38
|
}
|
|
39
|
+
if (originalPlatform === undefined) {
|
|
40
|
+
delete process.env.IS_PLATFORM;
|
|
41
|
+
} else {
|
|
42
|
+
process.env.IS_PLATFORM = originalPlatform;
|
|
43
|
+
}
|
|
37
44
|
setOverridesForTesting({});
|
|
38
45
|
status.mockClear();
|
|
39
46
|
start.mockClear();
|
|
@@ -60,7 +67,15 @@ describe("desktop setup route feature gate", () => {
|
|
|
60
67
|
expect(start).not.toHaveBeenCalled();
|
|
61
68
|
});
|
|
62
69
|
|
|
63
|
-
test(`${route.method}
|
|
70
|
+
test(`${route.method} rejects self-hosted Docker assistants without inspecting or installing dependencies`, () => {
|
|
71
|
+
process.env.IS_PLATFORM = "false";
|
|
72
|
+
setOverridesForTesting({ "assistant-desktop": true });
|
|
73
|
+
expect(() => route.handler({})).toThrow(NotFoundError);
|
|
74
|
+
expect(status).not.toHaveBeenCalled();
|
|
75
|
+
expect(start).not.toHaveBeenCalled();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test(`${route.method} serves setup only for enabled platform containers`, () => {
|
|
64
79
|
setOverridesForTesting({ "assistant-desktop": true });
|
|
65
80
|
expect(route.handler({})).toEqual(
|
|
66
81
|
route.method === "GET"
|
|
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
|
|
3
3
|
import { getConfig } from "../../config/loader.js";
|
|
4
4
|
import { desktopDependencyInstaller } from "../../desktop/desktop-dependencies.js";
|
|
5
|
-
import {
|
|
5
|
+
import { isVirtualDesktopEnabled } from "../../desktop/virtual-desktop-feature.js";
|
|
6
6
|
import { GATEWAY_PRINCIPALS } from "../auth/route-policy.js";
|
|
7
7
|
import { NotFoundError } from "./errors.js";
|
|
8
8
|
import type { RouteDefinition } from "./types.js";
|
|
@@ -19,8 +19,10 @@ export const ROUTES: RouteDefinition[] = ["GET", "POST"].map((method) => ({
|
|
|
19
19
|
method,
|
|
20
20
|
policy: { requiredScopes: [], allowedPrincipalTypes: GATEWAY_PRINCIPALS },
|
|
21
21
|
handler: () => {
|
|
22
|
-
if (!
|
|
23
|
-
throw new NotFoundError(
|
|
22
|
+
if (!isVirtualDesktopEnabled(getConfig())) {
|
|
23
|
+
throw new NotFoundError(
|
|
24
|
+
"Virtual desktop is available only on enabled platform-hosted assistants",
|
|
25
|
+
);
|
|
24
26
|
}
|
|
25
27
|
return method === "GET"
|
|
26
28
|
? desktopDependencyInstaller.getStatus()
|
|
@@ -28,8 +30,8 @@ export const ROUTES: RouteDefinition[] = ["GET", "POST"].map((method) => ({
|
|
|
28
30
|
},
|
|
29
31
|
summary:
|
|
30
32
|
method === "GET"
|
|
31
|
-
? "Get desktop setup status"
|
|
32
|
-
: "Install desktop components",
|
|
33
|
+
? "Get virtual desktop setup status"
|
|
34
|
+
: "Install virtual desktop components",
|
|
33
35
|
tags: ["desktop"],
|
|
34
36
|
responseBody: statusSchema,
|
|
35
37
|
}));
|
|
@@ -17,7 +17,12 @@ import { z } from "zod";
|
|
|
17
17
|
import type { McpServerConfig } from "../../config/schemas/mcp.js";
|
|
18
18
|
import { estimateToolDefinitionTokens } from "../../context/token-estimator.js";
|
|
19
19
|
import { reloadMcpServers } from "../../daemon/mcp-reload-service.js";
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
mcpOAuthCredentialKey,
|
|
22
|
+
resolveMcpOAuthCredentialTarget,
|
|
23
|
+
workspaceMcpOAuthCredentialTarget,
|
|
24
|
+
} from "../../mcp/credential-target.js";
|
|
25
|
+
import { readEffectiveMcpConfig } from "../../mcp/effective-config.js";
|
|
21
26
|
import { getMcpServerManager } from "../../mcp/manager.js";
|
|
22
27
|
import { orchestrateMcpOAuthConnect } from "../../mcp/mcp-auth-orchestrator.js";
|
|
23
28
|
import { getMcpAuthState } from "../../mcp/mcp-auth-state.js";
|
|
@@ -74,9 +79,7 @@ function persistWorkspaceMcpConfig(config: {
|
|
|
74
79
|
try {
|
|
75
80
|
saveWorkspaceMcpConfig(config);
|
|
76
81
|
} catch (err) {
|
|
77
|
-
throw new InternalError(
|
|
78
|
-
err instanceof Error ? err.message : String(err),
|
|
79
|
-
);
|
|
82
|
+
throw new InternalError(err instanceof Error ? err.message : String(err));
|
|
80
83
|
}
|
|
81
84
|
}
|
|
82
85
|
|
|
@@ -91,8 +94,9 @@ async function handleMcpAuthStart({
|
|
|
91
94
|
}> {
|
|
92
95
|
const { serverId } = parseBody(McpServerIdParams, body);
|
|
93
96
|
|
|
94
|
-
const
|
|
95
|
-
|
|
97
|
+
const serverConfig = readEffectiveMcpConfig(loadWorkspaceMcpConfig()).servers[
|
|
98
|
+
serverId
|
|
99
|
+
];
|
|
96
100
|
|
|
97
101
|
if (!serverConfig) {
|
|
98
102
|
throw new BadRequestError(`MCP server "${serverId}" not configured`);
|
|
@@ -105,10 +109,29 @@ async function handleMcpAuthStart({
|
|
|
105
109
|
);
|
|
106
110
|
}
|
|
107
111
|
|
|
112
|
+
const credentialTarget = resolveMcpOAuthCredentialTarget(
|
|
113
|
+
serverId,
|
|
114
|
+
serverConfig,
|
|
115
|
+
);
|
|
116
|
+
if (!credentialTarget) {
|
|
117
|
+
throw new BadRequestError(
|
|
118
|
+
`OAuth only supported for sse/streamable-http transports (server "${serverId}" uses ${transport.type})`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
mcpOAuthCredentialKey(credentialTarget, "tokens");
|
|
124
|
+
} catch {
|
|
125
|
+
throw new BadRequestError(
|
|
126
|
+
`MCP server "${serverId}" has an invalid transport URL`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
108
130
|
let result: { auth_url: string; already_authenticated?: boolean };
|
|
109
131
|
try {
|
|
110
132
|
result = await orchestrateMcpOAuthConnect({
|
|
111
133
|
serverId,
|
|
134
|
+
credentialTarget,
|
|
112
135
|
transport: {
|
|
113
136
|
url: transport.url,
|
|
114
137
|
type: transport.type,
|
|
@@ -135,7 +158,19 @@ function handleMcpAuthStatus({
|
|
|
135
158
|
| { status: "complete" }
|
|
136
159
|
| { status: "error"; error: string } {
|
|
137
160
|
const { serverId } = pathParams as { serverId: string };
|
|
138
|
-
const
|
|
161
|
+
const serverConfig = readEffectiveMcpConfig(loadWorkspaceMcpConfig()).servers[
|
|
162
|
+
serverId
|
|
163
|
+
];
|
|
164
|
+
const target = serverConfig
|
|
165
|
+
? resolveMcpOAuthCredentialTarget(serverId, serverConfig)
|
|
166
|
+
: null;
|
|
167
|
+
let targetKey: string | undefined;
|
|
168
|
+
try {
|
|
169
|
+
targetKey = target ? mcpOAuthCredentialKey(target, "tokens") : undefined;
|
|
170
|
+
} catch {
|
|
171
|
+
targetKey = undefined;
|
|
172
|
+
}
|
|
173
|
+
const state = targetKey ? getMcpAuthState(serverId, targetKey) : null;
|
|
139
174
|
|
|
140
175
|
if (state === null) {
|
|
141
176
|
throw new NotFoundError(`No active OAuth flow for server "${serverId}"`);
|
|
@@ -170,53 +205,6 @@ function handleMcpReload(_args: { body?: Record<string, unknown> }): {
|
|
|
170
205
|
return { ok: true };
|
|
171
206
|
}
|
|
172
207
|
|
|
173
|
-
// ---------------------------------------------------------------------------
|
|
174
|
-
// Health check helper
|
|
175
|
-
// ---------------------------------------------------------------------------
|
|
176
|
-
|
|
177
|
-
const HEALTH_CHECK_TIMEOUT_MS = 10_000;
|
|
178
|
-
|
|
179
|
-
async function checkMachineReadableHealth(
|
|
180
|
-
serverId: string,
|
|
181
|
-
config: McpServerConfig,
|
|
182
|
-
timeoutMs = HEALTH_CHECK_TIMEOUT_MS,
|
|
183
|
-
): Promise<string> {
|
|
184
|
-
const client = new McpClient(serverId);
|
|
185
|
-
try {
|
|
186
|
-
await Promise.race([
|
|
187
|
-
client.connect(config.transport),
|
|
188
|
-
new Promise<never>((_, reject) => {
|
|
189
|
-
const t = setTimeout(() => reject(new Error("timeout")), timeoutMs);
|
|
190
|
-
if (typeof t === "object" && "unref" in t) {
|
|
191
|
-
t.unref();
|
|
192
|
-
}
|
|
193
|
-
}),
|
|
194
|
-
]);
|
|
195
|
-
|
|
196
|
-
if (client.isConnected) {
|
|
197
|
-
await client.disconnect();
|
|
198
|
-
return "connected";
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const err = client.lastError;
|
|
202
|
-
if (err) {
|
|
203
|
-
if (err.message.includes("timeout")) {
|
|
204
|
-
return "error";
|
|
205
|
-
}
|
|
206
|
-
return "error";
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
return "needs-auth";
|
|
210
|
-
} catch {
|
|
211
|
-
try {
|
|
212
|
-
await client.disconnect();
|
|
213
|
-
} catch {
|
|
214
|
-
/* ignore */
|
|
215
|
-
}
|
|
216
|
-
return "error";
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
208
|
// ---------------------------------------------------------------------------
|
|
221
209
|
// List
|
|
222
210
|
// ---------------------------------------------------------------------------
|
|
@@ -271,12 +259,19 @@ async function handleMcpList(_args: {
|
|
|
271
259
|
const configEntries = (
|
|
272
260
|
Object.entries(servers) as [string, McpServerConfig][]
|
|
273
261
|
).filter(([, config]) => config && typeof config === "object");
|
|
262
|
+
const manager = getMcpServerManager();
|
|
274
263
|
|
|
275
264
|
const workspaceEntries: McpServerEntry[] = await Promise.all(
|
|
276
265
|
configEntries.map(async ([id, config]) => {
|
|
277
|
-
const
|
|
266
|
+
const runtimeState = manager.getServerState(id, "workspace");
|
|
267
|
+
const status =
|
|
268
|
+
runtimeState === "connected" || runtimeState === "needs-auth"
|
|
269
|
+
? runtimeState
|
|
270
|
+
: "error";
|
|
278
271
|
const hasOAuth =
|
|
279
|
-
config.transport.type !== "stdio"
|
|
272
|
+
config.transport.type !== "stdio"
|
|
273
|
+
? await hasMcpOAuthTokens(workspaceMcpOAuthCredentialTarget(id))
|
|
274
|
+
: false;
|
|
280
275
|
|
|
281
276
|
// Check credential store for stored static auth headers
|
|
282
277
|
const storedHeaders = await getMcpHeaders(id);
|
|
@@ -316,21 +311,23 @@ async function handleMcpList(_args: {
|
|
|
316
311
|
);
|
|
317
312
|
|
|
318
313
|
return {
|
|
319
|
-
servers: [
|
|
314
|
+
servers: [
|
|
315
|
+
...workspaceEntries,
|
|
316
|
+
...(await listPluginServerEntries(configEntries)),
|
|
317
|
+
],
|
|
320
318
|
};
|
|
321
319
|
}
|
|
322
320
|
|
|
323
321
|
/**
|
|
324
322
|
* Project plugin-declared servers onto listing entries.
|
|
325
323
|
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
* from the manager's in-memory client
|
|
329
|
-
* {@link PLUGIN_SERVER_STATUS}).
|
|
324
|
+
* OAuth presence is read from the plugin's scoped credential identity. Static
|
|
325
|
+
* workspace headers are never consulted, no network probe runs, and status
|
|
326
|
+
* comes from the manager's in-memory client (see {@link PLUGIN_SERVER_STATUS}).
|
|
330
327
|
*/
|
|
331
|
-
function listPluginServerEntries(
|
|
328
|
+
async function listPluginServerEntries(
|
|
332
329
|
configEntries: [string, McpServerConfig][],
|
|
333
|
-
): McpServerEntry[] {
|
|
330
|
+
): Promise<McpServerEntry[]> {
|
|
334
331
|
const { servers, issues } = readPluginMcpServers();
|
|
335
332
|
for (const issue of issues) {
|
|
336
333
|
log.warn(
|
|
@@ -349,36 +346,51 @@ function listPluginServerEntries(
|
|
|
349
346
|
const workspaceIds = new Set(configEntries.map(([id]) => id));
|
|
350
347
|
const manager = getMcpServerManager();
|
|
351
348
|
|
|
352
|
-
return
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
349
|
+
return Promise.all(
|
|
350
|
+
servers
|
|
351
|
+
.filter((server) => {
|
|
352
|
+
if (!workspaceIds.has(server.id)) {
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
log.warn(
|
|
356
|
+
{ plugin: server.pluginName, serverId: server.id },
|
|
357
|
+
"Plugin MCP server shadowed by a workspace server of the same id; skipping",
|
|
358
|
+
);
|
|
359
|
+
return false;
|
|
360
|
+
})
|
|
361
|
+
.map(async (server) => {
|
|
362
|
+
const { headers: _stripped, ...safeTransport } = server.config
|
|
363
|
+
.transport as Record<string, unknown>;
|
|
364
|
+
let hasOAuth = false;
|
|
365
|
+
const credentialTarget = resolveMcpOAuthCredentialTarget(
|
|
366
|
+
server.id,
|
|
367
|
+
server.config,
|
|
368
|
+
);
|
|
369
|
+
if (credentialTarget) {
|
|
370
|
+
try {
|
|
371
|
+
hasOAuth = await hasMcpOAuthTokens(credentialTarget);
|
|
372
|
+
} catch (err) {
|
|
373
|
+
log.warn(
|
|
374
|
+
{ err, plugin: server.pluginName, serverId: server.id },
|
|
375
|
+
"Plugin MCP OAuth credential identity could not be resolved",
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return {
|
|
380
|
+
id: server.id,
|
|
381
|
+
status:
|
|
382
|
+
manager.getServerState(server.id, "plugin") === "connected"
|
|
383
|
+
? "connected"
|
|
384
|
+
: PLUGIN_SERVER_STATUS,
|
|
385
|
+
transport: safeTransport as McpServerEntry["transport"],
|
|
386
|
+
hasOAuth,
|
|
387
|
+
hasStaticAuth: false,
|
|
388
|
+
authType: "none" as const,
|
|
389
|
+
source: "plugin" as const,
|
|
390
|
+
pluginName: server.pluginName,
|
|
391
|
+
};
|
|
392
|
+
}),
|
|
393
|
+
);
|
|
382
394
|
}
|
|
383
395
|
|
|
384
396
|
// ---------------------------------------------------------------------------
|
|
@@ -687,7 +699,7 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
687
699
|
},
|
|
688
700
|
summary: "List MCP servers with health status",
|
|
689
701
|
description:
|
|
690
|
-
"Returns configured MCP servers with
|
|
702
|
+
"Returns configured MCP servers with recorded runtime status without opening new connections.",
|
|
691
703
|
tags: ["internal"],
|
|
692
704
|
responseBody: z.object({
|
|
693
705
|
servers: z.array(
|
|
@@ -80,6 +80,7 @@ import {
|
|
|
80
80
|
PluginDirectoryNotFoundError,
|
|
81
81
|
} from "../../cli/lib/toggle-plugin.js";
|
|
82
82
|
import {
|
|
83
|
+
PLUGIN_UNINSTALL_WARNING_KEYS,
|
|
83
84
|
PluginNotInstalledError,
|
|
84
85
|
uninstallPlugin,
|
|
85
86
|
} from "../../cli/lib/uninstall-plugin.js";
|
|
@@ -255,6 +256,14 @@ const pluginUninstallResponseSchema = z.object({
|
|
|
255
256
|
.describe(
|
|
256
257
|
"Absolute path that was removed on the assistant host. Useful for audit logs and confirmation toasts.",
|
|
257
258
|
),
|
|
259
|
+
warnings: z
|
|
260
|
+
.array(
|
|
261
|
+
z.literal(PLUGIN_UNINSTALL_WARNING_KEYS.MCP_OAUTH_CREDENTIALS_UNCHECKED),
|
|
262
|
+
)
|
|
263
|
+
.optional()
|
|
264
|
+
.describe(
|
|
265
|
+
"Stable keys for non-fatal cleanup limitations that should be localized at the presentation edge.",
|
|
266
|
+
),
|
|
258
267
|
});
|
|
259
268
|
|
|
260
269
|
const pluginDetailsResponseSchema = z.object({
|
|
@@ -1134,7 +1143,11 @@ async function handleUninstallPlugin({
|
|
|
1134
1143
|
const result = await uninstallPlugin({ name: rawName });
|
|
1135
1144
|
await reconcilePluginSourcesNow();
|
|
1136
1145
|
publishPluginsChanged(getOriginClientId(headers));
|
|
1137
|
-
return {
|
|
1146
|
+
return {
|
|
1147
|
+
name: result.name,
|
|
1148
|
+
target: result.target,
|
|
1149
|
+
...(result.warnings && { warnings: result.warnings }),
|
|
1150
|
+
};
|
|
1138
1151
|
} catch (err) {
|
|
1139
1152
|
if (err instanceof InvalidPluginNameError) {
|
|
1140
1153
|
throw new BadRequestError(err.message);
|
|
@@ -152,6 +152,10 @@ export function publishPluginsChanged(originClientId?: string): void {
|
|
|
152
152
|
void publishSyncInvalidation([SYNC_TAGS.pluginsList], originClientId);
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
export function publishMcpChanged(): void {
|
|
156
|
+
void publishSyncInvalidation([SYNC_TAGS.mcpList]);
|
|
157
|
+
}
|
|
158
|
+
|
|
155
159
|
/**
|
|
156
160
|
* Invalidate the activation-checklist progress resource on every client.
|
|
157
161
|
*
|