@vellumai/assistant 0.12.2-staging.1 → 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/node_modules/@vellumai/slack-text/src/index.ts +13 -8
- package/node_modules/@vellumai/slack-text/src/label-resolution-entities.test.ts +95 -0
- package/openapi.yaml +50 -4
- package/package.json +1 -1
- package/scripts/smoke-desktop-browser-cli.ts +279 -0
- package/src/__tests__/always-loaded-tools-guard.test.ts +5 -5
- 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 +33 -3
- package/src/__tests__/conversation-surfaces-point-at-budget.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-point-at-capability.test.ts +1 -0
- package/src/__tests__/credential-routes.test.ts +38 -0
- package/src/__tests__/credential-security-invariants.test.ts +1 -1
- package/src/__tests__/cu-unified-flow.test.ts +6 -2
- 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 +185 -30
- 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__/oauth-commands-routes.test.ts +55 -0
- package/src/__tests__/oauth-provider-serializer.test.ts +22 -0
- package/src/__tests__/oauth-providers-routes.test.ts +1 -0
- package/src/__tests__/secret-routes-acp-guard.test.ts +59 -1
- package/src/__tests__/skills.test.ts +10 -0
- package/src/__tests__/subagent-tool-gate-mode.test.ts +51 -0
- package/src/__tests__/ui-channel-variants.test.ts +1 -56
- package/src/__tests__/user-plugin-loader.test.ts +21 -0
- package/src/acp/__tests__/acp-claude-oauth.test.ts +257 -6
- package/src/acp/__tests__/acp-credentials.test.ts +13 -0
- package/src/acp/__tests__/claude-token-refresh.test.ts +257 -0
- package/src/acp/__tests__/prepare-agent-env.test.ts +60 -1
- package/src/acp/acp-claude-oauth.ts +328 -14
- package/src/acp/acp-credentials.ts +19 -0
- package/src/acp/claude-token-refresh.ts +150 -0
- package/src/acp/prepare-agent-env.ts +21 -6
- 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 +69 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +81 -0
- package/src/calls/progress-narration.ts +15 -2
- package/src/calls/voice-control-protocol.ts +132 -0
- package/src/calls/voice-session-bridge.ts +56 -26
- package/src/calls/voice-triage-escalate.ts +3 -3
- 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/oauth/status.ts +5 -0
- 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 +43 -4
- package/src/config/bundled-skills/computer-use/TOOLS.json +3 -3
- package/src/config/bundled-skills/screen-annotation/SKILL.md +2 -2
- package/src/config/feature-flag-registry.json +11 -3
- package/src/config/loader.ts +1 -0
- 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/services.ts +10 -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 +19 -7
- package/src/daemon/conversation-surfaces.ts +6 -1
- package/src/daemon/conversation-tool-setup.ts +37 -43
- package/src/daemon/host-cu-proxy.ts +94 -31
- 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 +143 -0
- package/src/live-voice/__tests__/live-voice-events.test.ts +9 -7
- package/src/live-voice/__tests__/live-voice-progress.test.ts +79 -0
- package/src/live-voice/__tests__/live-voice-session-telemetry.test.ts +19 -0
- package/src/live-voice/__tests__/protocol.test.ts +47 -0
- package/src/live-voice/__tests__/session-controls.test.ts +113 -0
- package/src/live-voice/live-voice-metrics.ts +9 -0
- package/src/live-voice/live-voice-session.ts +88 -7
- package/src/live-voice/protocol.ts +69 -0
- package/src/live-voice/session-controls.ts +139 -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/__tests__/seed-providers-managed.test.ts +95 -0
- package/src/oauth/connection-resolver.test.ts +27 -0
- package/src/oauth/connection-resolver.ts +25 -1
- package/src/oauth/provider-serializer.ts +9 -0
- package/src/oauth/seed-providers.ts +121 -2
- 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-claude-auth-routes.test.ts +16 -5
- package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +74 -4
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +25 -3
- package/src/runtime/routes/acp-claude-auth-routes.ts +11 -3
- package/src/runtime/routes/app-management-routes.ts +17 -2
- 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/credential-routes.ts +1 -4
- 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/oauth-commands-routes.ts +14 -0
- package/src/runtime/routes/oauth-providers.ts +7 -0
- 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 +3 -4
- package/src/tools/ui-surface/channel-variants.ts +10 -59
- package/src/util/abort-reasons.ts +12 -1
- package/src/util/plugin-manifest.ts +203 -0
- package/src/watch/watch-retro.ts +6 -8
- package/src/desktop/desktop-feature.test.ts +0 -18
- package/src/desktop/desktop-feature.ts +0 -19
|
@@ -15,14 +15,27 @@
|
|
|
15
15
|
* connection.
|
|
16
16
|
*/
|
|
17
17
|
type McpAuthState =
|
|
18
|
-
| {
|
|
18
|
+
| {
|
|
19
|
+
status: "pending";
|
|
20
|
+
authUrl: string;
|
|
21
|
+
attemptId: string;
|
|
22
|
+
targetKey: string;
|
|
23
|
+
expiresAt: number;
|
|
24
|
+
}
|
|
19
25
|
| {
|
|
20
26
|
status: "complete";
|
|
21
27
|
serverId: string;
|
|
22
28
|
attemptId: string;
|
|
29
|
+
targetKey: string;
|
|
23
30
|
completedAt: number;
|
|
24
31
|
}
|
|
25
|
-
| {
|
|
32
|
+
| {
|
|
33
|
+
status: "error";
|
|
34
|
+
error: string;
|
|
35
|
+
attemptId: string;
|
|
36
|
+
targetKey: string;
|
|
37
|
+
failedAt: number;
|
|
38
|
+
};
|
|
26
39
|
|
|
27
40
|
const activeMcpAuthFlows = new Map<string, McpAuthState>();
|
|
28
41
|
|
|
@@ -41,11 +54,13 @@ export function setMcpAuthPending(
|
|
|
41
54
|
serverId: string,
|
|
42
55
|
authUrl: string,
|
|
43
56
|
attemptId: string,
|
|
57
|
+
targetKey = serverId,
|
|
44
58
|
): void {
|
|
45
59
|
activeMcpAuthFlows.set(serverId, {
|
|
46
60
|
status: "pending",
|
|
47
61
|
authUrl,
|
|
48
62
|
attemptId,
|
|
63
|
+
targetKey,
|
|
49
64
|
expiresAt: Date.now() + PENDING_TTL_MS,
|
|
50
65
|
});
|
|
51
66
|
}
|
|
@@ -59,15 +74,20 @@ export function setMcpAuthPending(
|
|
|
59
74
|
export function setMcpAuthComplete(
|
|
60
75
|
serverId: string,
|
|
61
76
|
attemptId: string,
|
|
77
|
+
targetKey = serverId,
|
|
62
78
|
): boolean {
|
|
63
79
|
const current = activeMcpAuthFlows.get(serverId);
|
|
64
|
-
if (
|
|
80
|
+
if (
|
|
81
|
+
current &&
|
|
82
|
+
(current.attemptId !== attemptId || current.targetKey !== targetKey)
|
|
83
|
+
) {
|
|
65
84
|
return false; // superseded
|
|
66
85
|
}
|
|
67
86
|
activeMcpAuthFlows.set(serverId, {
|
|
68
87
|
status: "complete",
|
|
69
88
|
serverId,
|
|
70
89
|
attemptId,
|
|
90
|
+
targetKey,
|
|
71
91
|
completedAt: Date.now(),
|
|
72
92
|
});
|
|
73
93
|
return true;
|
|
@@ -81,15 +101,20 @@ export function setMcpAuthError(
|
|
|
81
101
|
serverId: string,
|
|
82
102
|
error: string,
|
|
83
103
|
attemptId: string,
|
|
104
|
+
targetKey = serverId,
|
|
84
105
|
): boolean {
|
|
85
106
|
const current = activeMcpAuthFlows.get(serverId);
|
|
86
|
-
if (
|
|
107
|
+
if (
|
|
108
|
+
current &&
|
|
109
|
+
(current.attemptId !== attemptId || current.targetKey !== targetKey)
|
|
110
|
+
) {
|
|
87
111
|
return false; // superseded
|
|
88
112
|
}
|
|
89
113
|
activeMcpAuthFlows.set(serverId, {
|
|
90
114
|
status: "error",
|
|
91
115
|
error,
|
|
92
116
|
attemptId,
|
|
117
|
+
targetKey,
|
|
93
118
|
failedAt: Date.now(),
|
|
94
119
|
});
|
|
95
120
|
return true;
|
|
@@ -100,7 +125,10 @@ export function setMcpAuthError(
|
|
|
100
125
|
* expired entries on every read so a long-polling CLI can never observe
|
|
101
126
|
* a stale flow past its TTL.
|
|
102
127
|
*/
|
|
103
|
-
export function getMcpAuthState(
|
|
128
|
+
export function getMcpAuthState(
|
|
129
|
+
serverId: string,
|
|
130
|
+
targetKey?: string,
|
|
131
|
+
): McpAuthState | null {
|
|
104
132
|
const now = Date.now();
|
|
105
133
|
for (const [id, state] of activeMcpAuthFlows) {
|
|
106
134
|
if (state.status === "pending" && now > state.expiresAt) {
|
|
@@ -117,5 +145,9 @@ export function getMcpAuthState(serverId: string): McpAuthState | null {
|
|
|
117
145
|
activeMcpAuthFlows.delete(id);
|
|
118
146
|
}
|
|
119
147
|
}
|
|
120
|
-
|
|
148
|
+
const state = activeMcpAuthFlows.get(serverId) ?? null;
|
|
149
|
+
if (state && targetKey !== undefined && state.targetKey !== targetKey) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
return state;
|
|
121
153
|
}
|
|
@@ -38,23 +38,16 @@ import {
|
|
|
38
38
|
import { openInHostBrowser } from "../util/browser.js";
|
|
39
39
|
import { getLogger } from "../util/logger.js";
|
|
40
40
|
import { APP_VERSION } from "../version.js";
|
|
41
|
+
import {
|
|
42
|
+
MCP_OAUTH_CREDENTIAL_LEAVES,
|
|
43
|
+
mcpOAuthCredentialKey,
|
|
44
|
+
type McpOAuthCredentialTarget,
|
|
45
|
+
pluginMcpOAuthCredentialPrefix,
|
|
46
|
+
workspaceMcpOAuthCredentialTarget,
|
|
47
|
+
} from "./credential-target.js";
|
|
41
48
|
|
|
42
49
|
const log = getLogger("mcp-oauth");
|
|
43
50
|
|
|
44
|
-
// Credential store key helpers
|
|
45
|
-
function tokensKey(serverId: string): string {
|
|
46
|
-
return `mcp:${serverId}:tokens`;
|
|
47
|
-
}
|
|
48
|
-
function clientInfoKey(serverId: string): string {
|
|
49
|
-
return `mcp:${serverId}:client_info`;
|
|
50
|
-
}
|
|
51
|
-
function discoveryKey(serverId: string): string {
|
|
52
|
-
return `mcp:${serverId}:discovery`;
|
|
53
|
-
}
|
|
54
|
-
function clientBindingKey(serverId: string): string {
|
|
55
|
-
return `mcp:${serverId}:client_binding`;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
51
|
/**
|
|
59
52
|
* Logo shown on an authorization server's consent screen.
|
|
60
53
|
*
|
|
@@ -91,12 +84,15 @@ export interface McpOAuthProviderOptions {
|
|
|
91
84
|
* URL to the IPC caller (CLI / web client).
|
|
92
85
|
*/
|
|
93
86
|
onAuthorizationUrl?: (url: string) => void;
|
|
87
|
+
/** Credential identity. Defaults to the legacy workspace server id. */
|
|
88
|
+
credentialTarget?: McpOAuthCredentialTarget;
|
|
94
89
|
}
|
|
95
90
|
|
|
96
91
|
export class McpOAuthProvider implements OAuthClientProvider {
|
|
97
92
|
private readonly serverId: string;
|
|
98
93
|
private readonly serverUrl: string;
|
|
99
94
|
private readonly interactive: boolean;
|
|
95
|
+
private readonly credentialTarget: McpOAuthCredentialTarget;
|
|
100
96
|
private _codeVerifier: string | undefined;
|
|
101
97
|
private _state: string | undefined;
|
|
102
98
|
private _redirectUrl: string | undefined;
|
|
@@ -121,6 +117,8 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
121
117
|
this.serverUrl = serverUrl;
|
|
122
118
|
this.interactive = interactive;
|
|
123
119
|
this._onAuthorizationUrl = options.onAuthorizationUrl;
|
|
120
|
+
this.credentialTarget =
|
|
121
|
+
options.credentialTarget ?? workspaceMcpOAuthCredentialTarget(serverId);
|
|
124
122
|
}
|
|
125
123
|
|
|
126
124
|
// --- redirectUrl ---
|
|
@@ -167,7 +165,9 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
167
165
|
// --- Tokens ---
|
|
168
166
|
|
|
169
167
|
async tokens(): Promise<OAuthTokens | undefined> {
|
|
170
|
-
const raw = await getSecureKeyAsync(
|
|
168
|
+
const raw = await getSecureKeyAsync(
|
|
169
|
+
mcpOAuthCredentialKey(this.credentialTarget, "tokens"),
|
|
170
|
+
);
|
|
171
171
|
if (!raw) {
|
|
172
172
|
return undefined;
|
|
173
173
|
}
|
|
@@ -191,7 +191,9 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
191
191
|
// the previous refresh_token when the incoming response omits one.
|
|
192
192
|
let toPersist: OAuthTokens = tokens;
|
|
193
193
|
if (!tokens.refresh_token) {
|
|
194
|
-
const previous = await getSecureKeyAsync(
|
|
194
|
+
const previous = await getSecureKeyAsync(
|
|
195
|
+
mcpOAuthCredentialKey(this.credentialTarget, "tokens"),
|
|
196
|
+
);
|
|
195
197
|
if (previous) {
|
|
196
198
|
try {
|
|
197
199
|
const parsed = JSON.parse(previous) as OAuthTokens;
|
|
@@ -204,7 +206,7 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
204
206
|
}
|
|
205
207
|
}
|
|
206
208
|
const ok = await setSecureKeyAsync(
|
|
207
|
-
|
|
209
|
+
mcpOAuthCredentialKey(this.credentialTarget, "tokens"),
|
|
208
210
|
JSON.stringify(toPersist),
|
|
209
211
|
);
|
|
210
212
|
if (!ok) {
|
|
@@ -263,7 +265,9 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
263
265
|
* fronts the resource.
|
|
264
266
|
*/
|
|
265
267
|
async clientInformation(): Promise<OAuthClientInformationMixed | undefined> {
|
|
266
|
-
const raw = await getSecureKeyAsync(
|
|
268
|
+
const raw = await getSecureKeyAsync(
|
|
269
|
+
mcpOAuthCredentialKey(this.credentialTarget, "client_info"),
|
|
270
|
+
);
|
|
267
271
|
if (!raw) {
|
|
268
272
|
return undefined;
|
|
269
273
|
}
|
|
@@ -294,7 +298,7 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
294
298
|
info: OAuthClientInformationMixed,
|
|
295
299
|
): Promise<void> {
|
|
296
300
|
const ok = await setSecureKeyAsync(
|
|
297
|
-
|
|
301
|
+
mcpOAuthCredentialKey(this.credentialTarget, "client_info"),
|
|
298
302
|
JSON.stringify(info),
|
|
299
303
|
);
|
|
300
304
|
if (!ok) {
|
|
@@ -313,7 +317,7 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
313
317
|
redirectUri: this._redirectUrl,
|
|
314
318
|
};
|
|
315
319
|
const boundOk = await setSecureKeyAsync(
|
|
316
|
-
|
|
320
|
+
mcpOAuthCredentialKey(this.credentialTarget, "client_binding"),
|
|
317
321
|
JSON.stringify(binding),
|
|
318
322
|
);
|
|
319
323
|
if (!boundOk) {
|
|
@@ -349,7 +353,9 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
349
353
|
* redirect check already covers the case this plugin actually changes.
|
|
350
354
|
*/
|
|
351
355
|
private async describeStaleBinding(): Promise<string | null> {
|
|
352
|
-
const raw = await getSecureKeyAsync(
|
|
356
|
+
const raw = await getSecureKeyAsync(
|
|
357
|
+
mcpOAuthCredentialKey(this.credentialTarget, "client_binding"),
|
|
358
|
+
);
|
|
353
359
|
if (!raw) {
|
|
354
360
|
// Registered before the binding was recorded. Keep it: discarding
|
|
355
361
|
// every pre-existing registration would remake all of them at once.
|
|
@@ -410,7 +416,9 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
410
416
|
// --- Discovery State ---
|
|
411
417
|
|
|
412
418
|
async discoveryState(): Promise<OAuthDiscoveryState | undefined> {
|
|
413
|
-
const raw = await getSecureKeyAsync(
|
|
419
|
+
const raw = await getSecureKeyAsync(
|
|
420
|
+
mcpOAuthCredentialKey(this.credentialTarget, "discovery"),
|
|
421
|
+
);
|
|
414
422
|
if (!raw) {
|
|
415
423
|
return undefined;
|
|
416
424
|
}
|
|
@@ -423,7 +431,7 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
423
431
|
|
|
424
432
|
async saveDiscoveryState(state: OAuthDiscoveryState): Promise<void> {
|
|
425
433
|
const ok = await setSecureKeyAsync(
|
|
426
|
-
|
|
434
|
+
mcpOAuthCredentialKey(this.credentialTarget, "discovery"),
|
|
427
435
|
JSON.stringify(state),
|
|
428
436
|
);
|
|
429
437
|
if (!ok) {
|
|
@@ -498,7 +506,9 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
498
506
|
);
|
|
499
507
|
|
|
500
508
|
if (scope === "all" || scope === "tokens") {
|
|
501
|
-
const result = await deleteSecureKeyAsync(
|
|
509
|
+
const result = await deleteSecureKeyAsync(
|
|
510
|
+
mcpOAuthCredentialKey(this.credentialTarget, "tokens"),
|
|
511
|
+
);
|
|
502
512
|
if (result === "error") {
|
|
503
513
|
log.warn(
|
|
504
514
|
{ serverId: this.serverId },
|
|
@@ -512,7 +522,9 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
512
522
|
}
|
|
513
523
|
}
|
|
514
524
|
if (scope === "all" || scope === "client") {
|
|
515
|
-
const result = await deleteSecureKeyAsync(
|
|
525
|
+
const result = await deleteSecureKeyAsync(
|
|
526
|
+
mcpOAuthCredentialKey(this.credentialTarget, "client_info"),
|
|
527
|
+
);
|
|
516
528
|
if (result === "error") {
|
|
517
529
|
log.warn(
|
|
518
530
|
{ serverId: this.serverId },
|
|
@@ -527,14 +539,18 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
527
539
|
// The binding describes the registration being dropped, so it goes
|
|
528
540
|
// with it. Leaving it would let a later registration inherit the
|
|
529
541
|
// previous one's issuer and redirect URI.
|
|
530
|
-
await deleteSecureKeyAsync(
|
|
542
|
+
await deleteSecureKeyAsync(
|
|
543
|
+
mcpOAuthCredentialKey(this.credentialTarget, "client_binding"),
|
|
544
|
+
);
|
|
531
545
|
}
|
|
532
546
|
if (scope === "all" || scope === "verifier") {
|
|
533
547
|
this._codeVerifier = undefined;
|
|
534
548
|
this._state = undefined;
|
|
535
549
|
}
|
|
536
550
|
if (scope === "all" || scope === "discovery") {
|
|
537
|
-
const result = await deleteSecureKeyAsync(
|
|
551
|
+
const result = await deleteSecureKeyAsync(
|
|
552
|
+
mcpOAuthCredentialKey(this.credentialTarget, "discovery"),
|
|
553
|
+
);
|
|
538
554
|
if (result === "error") {
|
|
539
555
|
log.warn(
|
|
540
556
|
{ serverId: this.serverId },
|
|
@@ -617,8 +633,10 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
617
633
|
/**
|
|
618
634
|
* Check whether OAuth tokens exist in the credential store for a server.
|
|
619
635
|
*/
|
|
620
|
-
export async function hasMcpOAuthTokens(
|
|
621
|
-
|
|
636
|
+
export async function hasMcpOAuthTokens(
|
|
637
|
+
target: McpOAuthCredentialTarget,
|
|
638
|
+
): Promise<boolean> {
|
|
639
|
+
const raw = await getSecureKeyAsync(mcpOAuthCredentialKey(target, "tokens"));
|
|
622
640
|
return raw != null && raw.length > 0;
|
|
623
641
|
}
|
|
624
642
|
|
|
@@ -631,10 +649,14 @@ export async function deleteMcpOAuthCredentials(
|
|
|
631
649
|
): Promise<{ ok: boolean; failedKeys: string[] }> {
|
|
632
650
|
const [tokensResult, clientResult, bindingResult, discoveryResult] =
|
|
633
651
|
await Promise.all([
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
652
|
+
...MCP_OAUTH_CREDENTIAL_LEAVES.map((leaf) =>
|
|
653
|
+
deleteSecureKeyAsync(
|
|
654
|
+
mcpOAuthCredentialKey(
|
|
655
|
+
workspaceMcpOAuthCredentialTarget(serverId),
|
|
656
|
+
leaf,
|
|
657
|
+
),
|
|
658
|
+
),
|
|
659
|
+
),
|
|
638
660
|
]);
|
|
639
661
|
const results = [
|
|
640
662
|
{ key: "tokens", result: tokensResult },
|
|
@@ -660,3 +682,49 @@ export async function deleteMcpOAuthCredentials(
|
|
|
660
682
|
);
|
|
661
683
|
return { ok, failedKeys };
|
|
662
684
|
}
|
|
685
|
+
|
|
686
|
+
export interface PluginMcpOAuthCleanupResult {
|
|
687
|
+
readonly ok: boolean;
|
|
688
|
+
readonly unreachable: boolean;
|
|
689
|
+
readonly matchedKeys: string[];
|
|
690
|
+
readonly failedKeys: string[];
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/** Delete every endpoint-scoped MCP OAuth record owned by one plugin. */
|
|
694
|
+
export async function deletePluginMcpOAuthCredentials(
|
|
695
|
+
pluginName: string,
|
|
696
|
+
): Promise<PluginMcpOAuthCleanupResult> {
|
|
697
|
+
const { listSecureKeysAsync } = await import("../security/secure-keys.js");
|
|
698
|
+
const listed = await listSecureKeysAsync();
|
|
699
|
+
if (listed.unreachable) {
|
|
700
|
+
return {
|
|
701
|
+
ok: false,
|
|
702
|
+
unreachable: true,
|
|
703
|
+
matchedKeys: [],
|
|
704
|
+
failedKeys: [],
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const prefix = pluginMcpOAuthCredentialPrefix(pluginName);
|
|
709
|
+
const matchedKeys = listed.accounts.filter((key) => key.startsWith(prefix));
|
|
710
|
+
const deletionResults = await Promise.all(
|
|
711
|
+
matchedKeys.map(async (key) => {
|
|
712
|
+
try {
|
|
713
|
+
return { key, result: await deleteSecureKeyAsync(key) };
|
|
714
|
+
} catch {
|
|
715
|
+
return { key, result: "error" as const };
|
|
716
|
+
}
|
|
717
|
+
}),
|
|
718
|
+
);
|
|
719
|
+
const failedKeys = deletionResults
|
|
720
|
+
.filter(({ result }) => result === "error")
|
|
721
|
+
.map(({ key }) => key);
|
|
722
|
+
const ok = failedKeys.length === 0;
|
|
723
|
+
if (!ok) {
|
|
724
|
+
log.warn(
|
|
725
|
+
{ pluginName, failedKeys },
|
|
726
|
+
"Some plugin MCP OAuth credentials could not be deleted",
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
return { ok, unreachable: false, matchedKeys, failedKeys };
|
|
730
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
collectMountSnapshot,
|
|
5
|
+
diffMountSnapshots,
|
|
6
|
+
isWatchedMount,
|
|
7
|
+
mountDiffIsEmpty,
|
|
8
|
+
type MountInfoEntry,
|
|
9
|
+
type MountSnapshot,
|
|
10
|
+
parseMountinfo,
|
|
11
|
+
type PathFsSnapshot,
|
|
12
|
+
unescapeMountField,
|
|
13
|
+
} from "../mount-watch.js";
|
|
14
|
+
|
|
15
|
+
const SAMPLE_MOUNTINFO = `22 1 0:21 / / rw,relatime - overlay overlay rw,lowerdir=/a,upperdir=/b,workdir=/c
|
|
16
|
+
36 22 0:26 / /workspace rw,relatime - virtiofs kataShared rw
|
|
17
|
+
48 22 0:27 / /data rw,relatime - virtiofs kataShared rw
|
|
18
|
+
25 22 0:23 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
function entry(
|
|
22
|
+
fields: Partial<MountInfoEntry> & Pick<MountInfoEntry, "mountPoint" | "fsType">,
|
|
23
|
+
): MountInfoEntry {
|
|
24
|
+
return {
|
|
25
|
+
mountId: fields.mountId ?? "1",
|
|
26
|
+
parentId: fields.parentId ?? "0",
|
|
27
|
+
majorMinor: fields.majorMinor ?? "0:1",
|
|
28
|
+
root: fields.root ?? "/",
|
|
29
|
+
mountPoint: fields.mountPoint,
|
|
30
|
+
mountOptions: fields.mountOptions ?? "rw,relatime",
|
|
31
|
+
fsType: fields.fsType,
|
|
32
|
+
source: fields.source ?? "kataShared",
|
|
33
|
+
superOptions: fields.superOptions ?? "rw",
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function pathFs(
|
|
38
|
+
fields: Partial<PathFsSnapshot> & Pick<PathFsSnapshot, "path">,
|
|
39
|
+
): PathFsSnapshot {
|
|
40
|
+
return {
|
|
41
|
+
path: fields.path,
|
|
42
|
+
type: fields.type ?? 0x65735546,
|
|
43
|
+
blockSize: fields.blockSize ?? 4096,
|
|
44
|
+
blocks: fields.blocks ?? 12_884_901,
|
|
45
|
+
files: fields.files ?? 1_000_000,
|
|
46
|
+
...(fields.error != null ? { error: fields.error } : {}),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function snapshot(partial: Partial<MountSnapshot>): MountSnapshot {
|
|
51
|
+
return {
|
|
52
|
+
mounts: partial.mounts ?? [],
|
|
53
|
+
paths: partial.paths ?? [],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
describe("unescapeMountField", () => {
|
|
58
|
+
test("decodes octal-escaped spaces", () => {
|
|
59
|
+
expect(unescapeMountField("/workspace/my\\040dir")).toBe("/workspace/my dir");
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("parseMountinfo", () => {
|
|
64
|
+
test("reads mount point, fstype, and source", () => {
|
|
65
|
+
const parsed = parseMountinfo(SAMPLE_MOUNTINFO);
|
|
66
|
+
expect(parsed).toEqual([
|
|
67
|
+
entry({
|
|
68
|
+
mountId: "22",
|
|
69
|
+
parentId: "1",
|
|
70
|
+
majorMinor: "0:21",
|
|
71
|
+
mountPoint: "/",
|
|
72
|
+
fsType: "overlay",
|
|
73
|
+
source: "overlay",
|
|
74
|
+
superOptions: "rw,lowerdir=/a,upperdir=/b,workdir=/c",
|
|
75
|
+
}),
|
|
76
|
+
entry({
|
|
77
|
+
mountId: "36",
|
|
78
|
+
parentId: "22",
|
|
79
|
+
majorMinor: "0:26",
|
|
80
|
+
mountPoint: "/workspace",
|
|
81
|
+
fsType: "virtiofs",
|
|
82
|
+
}),
|
|
83
|
+
entry({
|
|
84
|
+
mountId: "48",
|
|
85
|
+
parentId: "22",
|
|
86
|
+
majorMinor: "0:27",
|
|
87
|
+
mountPoint: "/data",
|
|
88
|
+
fsType: "virtiofs",
|
|
89
|
+
}),
|
|
90
|
+
entry({
|
|
91
|
+
mountId: "25",
|
|
92
|
+
parentId: "22",
|
|
93
|
+
majorMinor: "0:23",
|
|
94
|
+
mountPoint: "/proc",
|
|
95
|
+
mountOptions: "rw,nosuid,nodev,noexec,relatime",
|
|
96
|
+
fsType: "proc",
|
|
97
|
+
source: "proc",
|
|
98
|
+
}),
|
|
99
|
+
]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("skips malformed lines", () => {
|
|
103
|
+
expect(parseMountinfo("not-a-mount-line\n")).toEqual([]);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe("isWatchedMount", () => {
|
|
108
|
+
test("keeps virtiofs regardless of mount point", () => {
|
|
109
|
+
expect(
|
|
110
|
+
isWatchedMount(entry({ mountPoint: "/secret-bind", fsType: "virtiofs" })),
|
|
111
|
+
).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("keeps /workspace and /data even when they are overlay", () => {
|
|
115
|
+
expect(
|
|
116
|
+
isWatchedMount(entry({ mountPoint: "/workspace", fsType: "overlay" })),
|
|
117
|
+
).toBe(true);
|
|
118
|
+
expect(isWatchedMount(entry({ mountPoint: "/data", fsType: "overlay" }))).toBe(
|
|
119
|
+
true,
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("drops proc and the root overlay", () => {
|
|
124
|
+
expect(isWatchedMount(entry({ mountPoint: "/proc", fsType: "proc" }))).toBe(
|
|
125
|
+
false,
|
|
126
|
+
);
|
|
127
|
+
expect(isWatchedMount(entry({ mountPoint: "/", fsType: "overlay" }))).toBe(
|
|
128
|
+
false,
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("diffMountSnapshots", () => {
|
|
134
|
+
test("stays empty when the table is unchanged", () => {
|
|
135
|
+
const current = snapshot({
|
|
136
|
+
mounts: [entry({ mountPoint: "/workspace", fsType: "virtiofs" })],
|
|
137
|
+
paths: [pathFs({ path: "/workspace" })],
|
|
138
|
+
});
|
|
139
|
+
expect(mountDiffIsEmpty(diffMountSnapshots(current, current))).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("reports a dropped virtiofs bind as removed", () => {
|
|
143
|
+
const previous = snapshot({
|
|
144
|
+
mounts: [
|
|
145
|
+
entry({ mountPoint: "/workspace", fsType: "virtiofs" }),
|
|
146
|
+
entry({ mountPoint: "/data", fsType: "virtiofs" }),
|
|
147
|
+
],
|
|
148
|
+
});
|
|
149
|
+
const current = snapshot({
|
|
150
|
+
mounts: [entry({ mountPoint: "/data", fsType: "virtiofs" })],
|
|
151
|
+
});
|
|
152
|
+
const diff = diffMountSnapshots(previous, current);
|
|
153
|
+
expect(diff.removed.map((row) => row.mountPoint)).toEqual(["/workspace"]);
|
|
154
|
+
expect(diff.added).toEqual([]);
|
|
155
|
+
expect(diff.changed).toEqual([]);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("reports a fstype swap at the same mount point as changed", () => {
|
|
159
|
+
const previous = snapshot({
|
|
160
|
+
mounts: [entry({ mountPoint: "/workspace", fsType: "virtiofs" })],
|
|
161
|
+
});
|
|
162
|
+
const current = snapshot({
|
|
163
|
+
mounts: [entry({ mountPoint: "/workspace", fsType: "overlay", source: "overlay" })],
|
|
164
|
+
});
|
|
165
|
+
const diff = diffMountSnapshots(previous, current);
|
|
166
|
+
expect(diff.changed).toHaveLength(1);
|
|
167
|
+
expect(diff.changed[0].previous.fsType).toBe("virtiofs");
|
|
168
|
+
expect(diff.changed[0].current.fsType).toBe("overlay");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("reports a statfs size collapse on a watched path", () => {
|
|
172
|
+
const previous = snapshot({
|
|
173
|
+
paths: [pathFs({ path: "/workspace", blocks: 12_884_901 })],
|
|
174
|
+
});
|
|
175
|
+
const current = snapshot({
|
|
176
|
+
paths: [pathFs({ path: "/workspace", blocks: 25_165_824, type: 0x794c7630 })],
|
|
177
|
+
});
|
|
178
|
+
const diff = diffMountSnapshots(previous, current);
|
|
179
|
+
expect(diff.pathChanges).toHaveLength(1);
|
|
180
|
+
expect(diff.pathChanges[0].previous.blocks).toBe(12_884_901);
|
|
181
|
+
expect(diff.pathChanges[0].current.blocks).toBe(25_165_824);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe("collectMountSnapshot", () => {
|
|
186
|
+
test("returns null on Windows and macOS", () => {
|
|
187
|
+
expect(collectMountSnapshot({ platform: "win32" })).toBeNull();
|
|
188
|
+
expect(collectMountSnapshot({ platform: "darwin" })).toBeNull();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("filters to assistant-container volume mounts", () => {
|
|
192
|
+
const snapshot = collectMountSnapshot({
|
|
193
|
+
platform: "linux",
|
|
194
|
+
readMountinfo: () => SAMPLE_MOUNTINFO,
|
|
195
|
+
statPath: (path) => pathFs({ path }),
|
|
196
|
+
watchedMountPoints: ["/workspace", "/data"],
|
|
197
|
+
});
|
|
198
|
+
expect(snapshot).not.toBeNull();
|
|
199
|
+
expect(snapshot?.mounts.map((row) => row.mountPoint)).toEqual([
|
|
200
|
+
"/data",
|
|
201
|
+
"/workspace",
|
|
202
|
+
]);
|
|
203
|
+
expect(snapshot?.paths.map((row) => row.path)).toEqual([
|
|
204
|
+
"/workspace",
|
|
205
|
+
"/data",
|
|
206
|
+
]);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("stats watched paths even when they are missing from the host", () => {
|
|
210
|
+
const snapshot = collectMountSnapshot({
|
|
211
|
+
platform: "linux",
|
|
212
|
+
readMountinfo: () => SAMPLE_MOUNTINFO,
|
|
213
|
+
statPath: (path) =>
|
|
214
|
+
pathFs({ path, type: null, blockSize: null, blocks: null, files: null, error: "statfs-failed" }),
|
|
215
|
+
watchedMountPoints: ["/workspace", "/data"],
|
|
216
|
+
});
|
|
217
|
+
expect(snapshot?.paths).toEqual([
|
|
218
|
+
pathFs({
|
|
219
|
+
path: "/workspace",
|
|
220
|
+
type: null,
|
|
221
|
+
blockSize: null,
|
|
222
|
+
blocks: null,
|
|
223
|
+
files: null,
|
|
224
|
+
error: "statfs-failed",
|
|
225
|
+
}),
|
|
226
|
+
pathFs({
|
|
227
|
+
path: "/data",
|
|
228
|
+
type: null,
|
|
229
|
+
blockSize: null,
|
|
230
|
+
blocks: null,
|
|
231
|
+
files: null,
|
|
232
|
+
error: "statfs-failed",
|
|
233
|
+
}),
|
|
234
|
+
]);
|
|
235
|
+
});
|
|
236
|
+
});
|