@vellumai/assistant 0.12.2-staging.2 → 0.12.2-staging.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/desktop-browser-cli.md +33 -0
- package/openapi.yaml +17 -5
- package/package.json +1 -1
- package/scripts/smoke-desktop-browser-cli.ts +279 -0
- package/src/__tests__/config-schema.test.ts +1 -1
- package/src/__tests__/config-sounds-sync.test.ts +23 -0
- package/src/__tests__/conversation-error.test.ts +4 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +6 -4
- package/src/__tests__/external-plugin-loader.test.ts +52 -0
- package/src/__tests__/headless-browser-mode.test.ts +48 -0
- package/src/__tests__/host-cu-proxy.test.ts +116 -18
- package/src/__tests__/list-all-apps.test.ts +40 -8
- package/src/__tests__/mcp-auth-routes.test.ts +99 -0
- package/src/__tests__/mcp-client-auth.test.ts +54 -0
- package/src/__tests__/mcp-health-check.test.ts +32 -32
- package/src/__tests__/mcp-list-plugin-servers.test.ts +77 -32
- package/src/__tests__/mcp-tool-annotations-risk.test.ts +4 -2
- package/src/__tests__/skills.test.ts +10 -0
- package/src/__tests__/user-plugin-loader.test.ts +21 -0
- package/src/acp/session-snapshot.ts +260 -0
- package/src/apps/app-store.ts +6 -5
- package/src/browser/operations.ts +2 -1
- package/src/browser/types.ts +7 -0
- package/src/browser/virtual-desktop-target.ts +34 -0
- package/src/calls/__tests__/progress-narration.test.ts +23 -0
- package/src/calls/__tests__/voice-control-protocol.test.ts +7 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +62 -0
- package/src/calls/progress-narration.ts +15 -2
- package/src/calls/voice-control-protocol.ts +34 -4
- package/src/calls/voice-session-bridge.ts +23 -0
- package/src/cli/commands/__tests__/browser.test.ts +84 -3
- package/src/cli/commands/browser.help.ts +43 -0
- package/src/cli/commands/browser.ts +52 -8
- package/src/cli/commands/plugins.ts +16 -5
- package/src/cli/lib/__tests__/install-from-github.test.ts +111 -0
- package/src/cli/lib/__tests__/install-from-platform.test.ts +77 -2
- package/src/cli/lib/__tests__/list-installed-plugins.test.ts +45 -3
- package/src/cli/lib/__tests__/plugin-details.test.ts +26 -0
- package/src/cli/lib/__tests__/uninstall-plugin.test.ts +122 -5
- package/src/cli/lib/bundled-marketplace.json +13 -0
- package/src/cli/lib/install-from-github.ts +17 -18
- package/src/cli/lib/install-from-platform.ts +14 -9
- package/src/cli/lib/list-installed-plugins.ts +21 -35
- package/src/cli/lib/plugin-details.ts +21 -8
- package/src/cli/lib/uninstall-plugin.ts +59 -1
- package/src/config/__tests__/plugin-resident-skill-discovery.test.ts +21 -0
- package/src/config/bundled-skills/acp/SKILL.md +1 -1
- package/src/config/bundled-skills/acp/TOOLS.json +2 -2
- package/src/config/bundled-skills/computer-use/SKILL.md +30 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +2 -2
- package/src/config/bundled-skills/screen-annotation/SKILL.md +2 -2
- package/src/config/feature-flag-registry.json +2 -2
- package/src/config/schemas/__tests__/voice.test.ts +2 -2
- package/src/config/schemas/mcp.ts +9 -7
- package/src/config/schemas/monitoring.ts +9 -0
- package/src/config/schemas/voice.ts +2 -2
- package/src/config/skills.ts +13 -21
- package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +47 -0
- package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +1 -0
- package/src/daemon/conversation-client-surface.ts +26 -0
- package/src/daemon/conversation-runtime-assembly.ts +1 -1
- package/src/daemon/conversation-tool-setup.ts +31 -29
- package/src/daemon/host-cu-proxy.ts +51 -26
- package/src/daemon/host-proxy-preactivation.ts +2 -0
- package/src/daemon/mcp-reload-service.ts +7 -0
- package/src/daemon/message-types/sync.ts +1 -0
- package/src/daemon/providers-setup.ts +4 -0
- package/src/desktop/__tests__/fake-desktop.ts +1 -0
- package/src/desktop/desktop-automation-lease.test.ts +208 -0
- package/src/desktop/desktop-automation-lease.ts +270 -0
- package/src/desktop/desktop-browser-client.test.ts +413 -0
- package/src/desktop/desktop-browser-client.ts +430 -0
- package/src/desktop/desktop-browser-cursor.ts +40 -0
- package/src/desktop/desktop-browser-endpoint.test.ts +78 -0
- package/src/desktop/desktop-browser-endpoint.ts +145 -0
- package/src/desktop/desktop-browser-operations.ts +130 -0
- package/src/desktop/desktop-chrome-session.test.ts +48 -0
- package/src/desktop/desktop-chrome-session.ts +58 -8
- package/src/desktop/desktop-dependencies.test.ts +29 -0
- package/src/desktop/desktop-dependencies.ts +26 -5
- package/src/desktop/desktop-display.ts +9 -0
- package/src/desktop/desktop-panel-config.ts +14 -1
- package/src/desktop/desktop-session-manager.test.ts +86 -3
- package/src/desktop/desktop-session-manager.ts +144 -24
- package/src/desktop/desktop-stream-bridge.ts +1 -1
- package/src/desktop/virtual-desktop-feature.ts +18 -0
- package/src/desktop/virtual-desktop-platform.test.ts +61 -0
- package/src/i18n/__tests__/i18n.test.ts +9 -5
- package/src/i18n/messages.ts +13 -4
- package/src/ipc/__tests__/browser-ipc.test.ts +230 -2
- package/src/ipc/__tests__/cancel-on-disconnect.test.ts +54 -0
- package/src/ipc/assistant-server.ts +15 -1
- package/src/ipc/cli-client.ts +13 -5
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +22 -0
- package/src/live-voice/__tests__/live-voice-events.test.ts +3 -2
- package/src/live-voice/__tests__/live-voice-session-telemetry.test.ts +19 -0
- package/src/live-voice/__tests__/protocol.test.ts +11 -3
- package/src/live-voice/__tests__/session-controls.test.ts +34 -0
- package/src/live-voice/live-voice-metrics.ts +9 -0
- package/src/live-voice/live-voice-session.ts +3 -0
- package/src/live-voice/protocol.ts +10 -1
- package/src/live-voice/session-controls.ts +28 -0
- package/src/mcp/__tests__/credential-target.test.ts +59 -0
- package/src/mcp/__tests__/effective-config.test.ts +11 -0
- package/src/mcp/__tests__/manager-state.test.ts +128 -0
- package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +20 -6
- package/src/mcp/__tests__/mcp-auth-state-target.test.ts +49 -0
- package/src/mcp/__tests__/plugin-mcp-oauth-cleanup.test.ts +107 -0
- package/src/mcp/__tests__/plugin-mcp-oauth-provider-isolation.test.ts +89 -0
- package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +63 -11
- package/src/mcp/__tests__/reload-signal-emission.test.ts +39 -2
- package/src/mcp/client.ts +64 -16
- package/src/mcp/credential-target.ts +105 -0
- package/src/mcp/effective-config.ts +42 -15
- package/src/mcp/manager.ts +61 -8
- package/src/mcp/mcp-auth-orchestrator.ts +17 -4
- package/src/mcp/mcp-auth-state.ts +38 -6
- package/src/mcp/mcp-oauth-provider.ts +101 -33
- package/src/monitoring/__tests__/mount-watch.test.ts +236 -0
- package/src/monitoring/mount-watch.ts +348 -0
- package/src/monitoring/worker.ts +9 -0
- package/src/notifications/__tests__/decision-engine.test.ts +175 -0
- package/src/notifications/decision-engine.ts +43 -8
- package/src/oauth/seed-providers.ts +11 -0
- package/src/onboarding/checkin-event.ts +2 -1
- package/src/plugins/__tests__/installed-plugin-dirs.test.ts +14 -1
- package/src/plugins/__tests__/mcp-servers.test.ts +38 -8
- package/src/plugins/external-plugin-loader.ts +49 -86
- package/src/plugins/installed-plugin-dirs.ts +6 -4
- package/src/plugins/mcp-servers.ts +9 -15
- package/src/plugins/mtime-cache.ts +2 -1
- package/src/plugins/user-loader.ts +2 -1
- package/src/providers/inference/__tests__/endpoint-probe.test.ts +45 -0
- package/src/providers/inference/endpoint-probe.ts +27 -12
- package/src/providers/openai/responses-provider.ts +9 -2
- package/src/providers/opencode/client.test.ts +167 -0
- package/src/providers/opencode/client.ts +73 -4
- package/src/runtime/AGENTS.md +4 -0
- package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +45 -2
- package/src/runtime/http-server.ts +6 -6
- package/src/runtime/routes/__tests__/acp-routes.test.ts +19 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +25 -3
- package/src/runtime/routes/acp-routes.ts +17 -161
- package/src/runtime/routes/browser-context.ts +39 -0
- package/src/runtime/routes/browser-routes.ts +16 -47
- package/src/runtime/routes/browser-tabs-routes.ts +72 -29
- package/src/runtime/routes/desktop-setup-routes.test.ts +16 -1
- package/src/runtime/routes/desktop-setup-routes.ts +7 -5
- package/src/runtime/routes/mcp-auth-routes.ts +106 -94
- package/src/runtime/routes/plugins-routes.ts +14 -1
- package/src/runtime/sync/resource-sync-events.ts +4 -0
- package/src/tools/acp/status.test.ts +276 -21
- package/src/tools/acp/status.ts +98 -32
- package/src/tools/browser/browser-execution.ts +44 -13
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +69 -0
- package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +1 -0
- package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +5 -0
- package/src/tools/computer-use/definitions.ts +2 -3
- package/src/util/abort-reasons.ts +12 -1
- package/src/util/plugin-manifest.ts +203 -0
- package/src/desktop/desktop-feature.test.ts +0 -18
- package/src/desktop/desktop-feature.ts +0 -19
|
@@ -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
|
*
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tests for the `acp_status` tool's
|
|
2
|
+
* Tests for the `acp_status` tool's live and persisted session projection.
|
|
3
3
|
*
|
|
4
|
-
* The
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The session manager owns live process state. Completed sessions move to
|
|
5
|
+
* durable history, where the status tool must keep them discoverable for
|
|
6
|
+
* follow-up work without making internal lifecycle guards treat them as live.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { describe, expect, mock, test } from "bun:test";
|
|
9
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
10
10
|
|
|
11
11
|
import type { AcpSessionState } from "../../acp/types.js";
|
|
12
12
|
import type { ToolContext } from "../types.js";
|
|
@@ -26,24 +26,49 @@ const RUNNING_STATE: AcpSessionState = {
|
|
|
26
26
|
],
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
-
let
|
|
29
|
+
let liveStates: AcpSessionState[] = [];
|
|
30
|
+
let fakeStoredCredential: string | undefined;
|
|
30
31
|
|
|
31
32
|
const realAcpModule = await import("../../acp/index.js");
|
|
32
33
|
mock.module("../../acp/index.js", () => ({
|
|
33
34
|
...realAcpModule,
|
|
34
|
-
getAcpSessionManager: () => ({
|
|
35
|
+
getAcpSessionManager: () => ({
|
|
36
|
+
getStatus: () => liveStates,
|
|
37
|
+
getBufferedUpdates: () => [],
|
|
38
|
+
}),
|
|
35
39
|
}));
|
|
36
40
|
|
|
41
|
+
const realClaudeOauth = await import("../../acp/acp-claude-oauth.js");
|
|
42
|
+
mock.module("../../acp/acp-claude-oauth.js", () => ({
|
|
43
|
+
...realClaudeOauth,
|
|
44
|
+
storedClaudeTokenDigest: async () => fakeStoredCredential,
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
import {
|
|
48
|
+
clearHistory,
|
|
49
|
+
insertHistoryRow,
|
|
50
|
+
} from "../../acp/__tests__/helpers/acp-history-db.js";
|
|
51
|
+
import { claudeTokenDigest } from "../../acp/acp-auth-marker-store.js";
|
|
52
|
+
import { initializeDb } from "../../persistence/db-init.js";
|
|
53
|
+
|
|
37
54
|
const { executeAcpStatus } = await import("./status.js");
|
|
38
55
|
|
|
56
|
+
await initializeDb();
|
|
57
|
+
|
|
39
58
|
const context = {
|
|
40
59
|
conversationId: "conv-1",
|
|
41
60
|
workingDir: "/tmp",
|
|
42
61
|
} as unknown as ToolContext;
|
|
43
62
|
|
|
63
|
+
beforeEach(() => {
|
|
64
|
+
liveStates = [];
|
|
65
|
+
fakeStoredCredential = undefined;
|
|
66
|
+
clearHistory();
|
|
67
|
+
});
|
|
68
|
+
|
|
44
69
|
describe("executeAcpStatus", () => {
|
|
45
|
-
test("a
|
|
46
|
-
|
|
70
|
+
test("a live session reports its model but not the picker", async () => {
|
|
71
|
+
liveStates = [RUNNING_STATE];
|
|
47
72
|
|
|
48
73
|
const result = await executeAcpStatus({ acp_session_id: "acp-1" }, context);
|
|
49
74
|
|
|
@@ -62,33 +87,263 @@ describe("executeAcpStatus", () => {
|
|
|
62
87
|
expect(result.content).not.toContain("Most capable");
|
|
63
88
|
});
|
|
64
89
|
|
|
65
|
-
test("
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
90
|
+
test("a cleanly completed resumable session is returned as idle", async () => {
|
|
91
|
+
insertHistoryRow({
|
|
92
|
+
id: "completed-1",
|
|
93
|
+
acpSessionId: "proto-completed-1",
|
|
94
|
+
startedAt: 2000,
|
|
95
|
+
completedAt: 3000,
|
|
96
|
+
status: "completed",
|
|
97
|
+
stopReason: "end_turn",
|
|
98
|
+
cwd: "/tmp/project",
|
|
99
|
+
task: "implement the parser",
|
|
100
|
+
usedTokens: 1200,
|
|
101
|
+
contextSize: 10000,
|
|
102
|
+
});
|
|
70
103
|
|
|
71
|
-
const result = await executeAcpStatus(
|
|
104
|
+
const result = await executeAcpStatus(
|
|
105
|
+
{ acp_session_id: "completed-1" },
|
|
106
|
+
context,
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
expect(result.isError).toBe(false);
|
|
110
|
+
expect(JSON.parse(result.content)).toEqual({
|
|
111
|
+
id: "completed-1",
|
|
112
|
+
agentId: "claude",
|
|
113
|
+
acpSessionId: "proto-completed-1",
|
|
114
|
+
parentConversationId: "conv-1",
|
|
115
|
+
status: "idle",
|
|
116
|
+
startedAt: 2000,
|
|
117
|
+
completedAt: 3000,
|
|
118
|
+
stopReason: "end_turn",
|
|
119
|
+
task: "implement the parser",
|
|
120
|
+
latestUsage: {
|
|
121
|
+
usedTokens: 1200,
|
|
122
|
+
contextSize: 10000,
|
|
123
|
+
},
|
|
124
|
+
resumable: true,
|
|
125
|
+
lastRunStatus: "completed",
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("failed and cancelled history rows keep their terminal status", async () => {
|
|
130
|
+
insertHistoryRow({
|
|
131
|
+
id: "failed-1",
|
|
132
|
+
status: "failed",
|
|
133
|
+
error: "adapter failed",
|
|
134
|
+
cwd: "/tmp/project",
|
|
135
|
+
});
|
|
136
|
+
insertHistoryRow({
|
|
137
|
+
id: "cancelled-1",
|
|
138
|
+
status: "cancelled",
|
|
139
|
+
stopReason: "cancelled",
|
|
140
|
+
cwd: "/tmp/project",
|
|
141
|
+
});
|
|
72
142
|
|
|
143
|
+
const failed = await executeAcpStatus(
|
|
144
|
+
{ acp_session_id: "failed-1" },
|
|
145
|
+
context,
|
|
146
|
+
);
|
|
147
|
+
const cancelled = await executeAcpStatus(
|
|
148
|
+
{ acp_session_id: "cancelled-1" },
|
|
149
|
+
context,
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
expect(JSON.parse(failed.content)).toMatchObject({
|
|
153
|
+
status: "failed",
|
|
154
|
+
resumable: true,
|
|
155
|
+
lastRunStatus: "failed",
|
|
156
|
+
error: "adapter failed",
|
|
157
|
+
});
|
|
158
|
+
expect(JSON.parse(cancelled.content)).toMatchObject({
|
|
159
|
+
status: "cancelled",
|
|
160
|
+
resumable: true,
|
|
161
|
+
lastRunStatus: "cancelled",
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("a completed legacy row without cwd stays terminal", async () => {
|
|
166
|
+
insertHistoryRow({ id: "legacy-1", status: "completed", cwd: null });
|
|
167
|
+
|
|
168
|
+
const result = await executeAcpStatus(
|
|
169
|
+
{ acp_session_id: "legacy-1" },
|
|
170
|
+
context,
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
expect(JSON.parse(result.content)).toMatchObject({
|
|
174
|
+
status: "completed",
|
|
175
|
+
resumable: false,
|
|
176
|
+
lastRunStatus: "completed",
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("a completed run stopped by cancellation stays terminal", async () => {
|
|
181
|
+
insertHistoryRow({
|
|
182
|
+
id: "partial-1",
|
|
183
|
+
status: "completed",
|
|
184
|
+
stopReason: "cancelled",
|
|
185
|
+
cwd: "/tmp/project",
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const result = await executeAcpStatus(
|
|
189
|
+
{ acp_session_id: "partial-1" },
|
|
190
|
+
context,
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
expect(JSON.parse(result.content)).toMatchObject({
|
|
194
|
+
status: "completed",
|
|
195
|
+
resumable: true,
|
|
196
|
+
lastRunStatus: "completed",
|
|
197
|
+
stopReason: "cancelled",
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("the listing includes live and persisted idle sessions", async () => {
|
|
202
|
+
liveStates = [RUNNING_STATE];
|
|
203
|
+
insertHistoryRow({
|
|
204
|
+
id: "idle-1",
|
|
205
|
+
startedAt: 2000,
|
|
206
|
+
status: "completed",
|
|
207
|
+
cwd: "/tmp/project",
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const result = await executeAcpStatus({}, context);
|
|
73
211
|
const payload = JSON.parse(result.content) as Array<
|
|
74
212
|
Record<string, unknown>
|
|
75
213
|
>;
|
|
76
|
-
|
|
77
|
-
expect(payload.map((entry) => entry.
|
|
78
|
-
"
|
|
79
|
-
"
|
|
214
|
+
|
|
215
|
+
expect(payload.map((entry) => [entry.id, entry.status])).toEqual([
|
|
216
|
+
["idle-1", "idle"],
|
|
217
|
+
["acp-1", "running"],
|
|
80
218
|
]);
|
|
81
219
|
for (const entry of payload) {
|
|
82
220
|
expect(entry).not.toHaveProperty("availableModels");
|
|
221
|
+
expect(entry).not.toHaveProperty("eventLog");
|
|
222
|
+
expect(entry).not.toHaveProperty("cwd");
|
|
223
|
+
expect(entry).not.toHaveProperty("source");
|
|
224
|
+
expect(entry).not.toHaveProperty("authErrorCredential");
|
|
83
225
|
}
|
|
84
226
|
});
|
|
85
227
|
|
|
86
|
-
test("
|
|
87
|
-
|
|
228
|
+
test("live state wins over history for the same id", async () => {
|
|
229
|
+
liveStates = [RUNNING_STATE];
|
|
230
|
+
insertHistoryRow({
|
|
231
|
+
id: RUNNING_STATE.id,
|
|
232
|
+
status: "completed",
|
|
233
|
+
cwd: "/tmp/project",
|
|
234
|
+
});
|
|
88
235
|
|
|
236
|
+
const result = await executeAcpStatus(
|
|
237
|
+
{ acp_session_id: RUNNING_STATE.id },
|
|
238
|
+
context,
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
expect(JSON.parse(result.content)).toMatchObject({
|
|
242
|
+
id: RUNNING_STATE.id,
|
|
243
|
+
status: "running",
|
|
244
|
+
agentId: RUNNING_STATE.agentId,
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("a truly unknown id keeps the not-found error", async () => {
|
|
249
|
+
const result = await executeAcpStatus(
|
|
250
|
+
{ acp_session_id: "missing-1" },
|
|
251
|
+
context,
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
expect(result).toEqual({
|
|
255
|
+
content: 'ACP session "missing-1" not found',
|
|
256
|
+
isError: true,
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("an empty listing says so rather than rendering an empty array", async () => {
|
|
89
261
|
const result = await executeAcpStatus({}, context);
|
|
90
262
|
|
|
91
263
|
expect(result.content).toBe("No ACP sessions found.");
|
|
92
264
|
expect(result.isError).toBe(false);
|
|
93
265
|
});
|
|
266
|
+
|
|
267
|
+
test("withholds a persisted auth marker after the credential is replaced", async () => {
|
|
268
|
+
const refused = claudeTokenDigest("sk-ant-oat-refused");
|
|
269
|
+
insertHistoryRow({
|
|
270
|
+
id: "auth-failed-1",
|
|
271
|
+
status: "failed",
|
|
272
|
+
error: "authentication required",
|
|
273
|
+
cwd: "/tmp/project",
|
|
274
|
+
authErrorCode: "acp_claude_auth_required",
|
|
275
|
+
authErrorCredential: refused,
|
|
276
|
+
});
|
|
277
|
+
fakeStoredCredential = claudeTokenDigest("sk-ant-oat-replacement");
|
|
278
|
+
|
|
279
|
+
const result = await executeAcpStatus(
|
|
280
|
+
{ acp_session_id: "auth-failed-1" },
|
|
281
|
+
context,
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
expect(JSON.parse(result.content)).toMatchObject({
|
|
285
|
+
id: "auth-failed-1",
|
|
286
|
+
status: "failed",
|
|
287
|
+
lastRunStatus: "failed",
|
|
288
|
+
});
|
|
289
|
+
expect(JSON.parse(result.content).authErrorCode).toBeUndefined();
|
|
290
|
+
expect(result.content).not.toContain("authErrorCredential");
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test("keeps a persisted auth marker while the refused credential is current", async () => {
|
|
294
|
+
const refused = claudeTokenDigest("sk-ant-oat-refused");
|
|
295
|
+
insertHistoryRow({
|
|
296
|
+
id: "auth-failed-2",
|
|
297
|
+
status: "failed",
|
|
298
|
+
cwd: "/tmp/project",
|
|
299
|
+
authErrorCode: "acp_claude_auth_required",
|
|
300
|
+
authErrorCredential: refused,
|
|
301
|
+
});
|
|
302
|
+
fakeStoredCredential = refused;
|
|
303
|
+
|
|
304
|
+
const result = await executeAcpStatus(
|
|
305
|
+
{ acp_session_id: "auth-failed-2" },
|
|
306
|
+
context,
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
expect(JSON.parse(result.content)).toMatchObject({
|
|
310
|
+
id: "auth-failed-2",
|
|
311
|
+
status: "failed",
|
|
312
|
+
authErrorCode: "acp_claude_auth_required",
|
|
313
|
+
});
|
|
314
|
+
expect(result.content).not.toContain("authErrorCredential");
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test("keeps an older live session when 50 newer history rows exist", async () => {
|
|
318
|
+
liveStates = [
|
|
319
|
+
{
|
|
320
|
+
...RUNNING_STATE,
|
|
321
|
+
id: "old-live",
|
|
322
|
+
startedAt: 1,
|
|
323
|
+
},
|
|
324
|
+
];
|
|
325
|
+
for (let i = 0; i < 50; i++) {
|
|
326
|
+
insertHistoryRow({
|
|
327
|
+
id: `hist-${i}`,
|
|
328
|
+
acpSessionId: `proto-hist-${i}`,
|
|
329
|
+
startedAt: 1000 + i,
|
|
330
|
+
status: "completed",
|
|
331
|
+
cwd: "/tmp/project",
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const result = await executeAcpStatus({}, context);
|
|
336
|
+
const payload = JSON.parse(result.content) as Array<{
|
|
337
|
+
id: string;
|
|
338
|
+
status: string;
|
|
339
|
+
}>;
|
|
340
|
+
|
|
341
|
+
expect(payload).toHaveLength(50);
|
|
342
|
+
expect(payload.some((entry) => entry.id === "old-live")).toBe(true);
|
|
343
|
+
expect(payload.find((entry) => entry.id === "old-live")?.status).toBe(
|
|
344
|
+
"running",
|
|
345
|
+
);
|
|
346
|
+
expect(payload[0]?.id).toBe("hist-49");
|
|
347
|
+
expect(payload.some((entry) => entry.id === "hist-0")).toBe(false);
|
|
348
|
+
});
|
|
94
349
|
});
|