@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
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* implementations unless this file's tests are running.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { mkdirSync, rmSync, utimesSync, writeFileSync } from "node:fs";
|
|
9
9
|
import { tmpdir } from "node:os";
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
import {
|
|
@@ -33,6 +33,9 @@ const realCompiler = { ...(await import("../../../bundler/app-compiler.js")) };
|
|
|
33
33
|
const realNotify = {
|
|
34
34
|
...(await import("../../../daemon/app-change-notify.js")),
|
|
35
35
|
};
|
|
36
|
+
const realPinStore = {
|
|
37
|
+
...(await import("../../../apps/app-pin-store.js")),
|
|
38
|
+
};
|
|
36
39
|
|
|
37
40
|
const compileApp = mock(
|
|
38
41
|
async (
|
|
@@ -70,6 +73,16 @@ mock.module("../../../daemon/app-change-notify.js", () => ({
|
|
|
70
73
|
},
|
|
71
74
|
}));
|
|
72
75
|
|
|
76
|
+
mock.module("../../../apps/app-pin-store.js", () => ({
|
|
77
|
+
...realPinStore,
|
|
78
|
+
listAppPins: () => {
|
|
79
|
+
if (!mockActive) {
|
|
80
|
+
return realPinStore.listAppPins();
|
|
81
|
+
}
|
|
82
|
+
return [];
|
|
83
|
+
},
|
|
84
|
+
}));
|
|
85
|
+
|
|
73
86
|
const { createApp } = await import("../../../apps/app-store.js");
|
|
74
87
|
const { getWorkspacePluginsDir } = await import("../../../util/platform.js");
|
|
75
88
|
const { ROUTES } = await import("../app-management-routes.js");
|
|
@@ -83,6 +96,7 @@ function findHandler(operationId: string) {
|
|
|
83
96
|
}
|
|
84
97
|
|
|
85
98
|
const handleRefreshApp = findHandler("apps_refresh");
|
|
99
|
+
const handleListApps = findHandler("apps_list");
|
|
86
100
|
|
|
87
101
|
let workspaceDir: string;
|
|
88
102
|
let previousWorkspaceDir: string | undefined;
|
|
@@ -142,6 +156,30 @@ describe("apps_refresh route", () => {
|
|
|
142
156
|
});
|
|
143
157
|
});
|
|
144
158
|
|
|
159
|
+
test("advances the workspace app list revision when compiled output changes", async () => {
|
|
160
|
+
const created = createApp({
|
|
161
|
+
name: "Budget",
|
|
162
|
+
schemaJson: "{}",
|
|
163
|
+
htmlDefinition: "<h1>Budget</h1>",
|
|
164
|
+
});
|
|
165
|
+
const builtAt = Math.ceil(created.updatedAt) + 1_000;
|
|
166
|
+
compileApp.mockImplementationOnce(async (appDir) => {
|
|
167
|
+
const distDir = join(appDir, "dist");
|
|
168
|
+
mkdirSync(distDir, { recursive: true });
|
|
169
|
+
utimesSync(distDir, new Date(builtAt), new Date(builtAt));
|
|
170
|
+
return { ok: true, errors: [], warnings: [], durationMs: 11 };
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
await handleRefreshApp({ pathParams: { id: created.id } });
|
|
174
|
+
const listed = (await handleListApps({})) as {
|
|
175
|
+
apps: Array<{ id: string; updatedAt: number }>;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
expect(listed.apps.find((app) => app.id === created.id)?.updatedAt).toBe(
|
|
179
|
+
builtAt,
|
|
180
|
+
);
|
|
181
|
+
});
|
|
182
|
+
|
|
145
183
|
test("compiles a plugin app in place", async () => {
|
|
146
184
|
const pluginName = `charts-${Math.random().toString(36).slice(2, 8)}`;
|
|
147
185
|
const pluginDir = join(getWorkspacePluginsDir(), pluginName);
|
|
@@ -157,9 +195,7 @@ describe("apps_refresh route", () => {
|
|
|
157
195
|
pathParams: { id: pluginAppId },
|
|
158
196
|
});
|
|
159
197
|
|
|
160
|
-
expect(compileApp).toHaveBeenCalledWith(
|
|
161
|
-
join(pluginDir, "apps", "viewer"),
|
|
162
|
-
);
|
|
198
|
+
expect(compileApp).toHaveBeenCalledWith(join(pluginDir, "apps", "viewer"));
|
|
163
199
|
expect(notifyAppSurfacesChanged).toHaveBeenCalledWith(pluginAppId, {
|
|
164
200
|
fileChange: true,
|
|
165
201
|
});
|
|
@@ -171,6 +207,40 @@ describe("apps_refresh route", () => {
|
|
|
171
207
|
});
|
|
172
208
|
});
|
|
173
209
|
|
|
210
|
+
test("advances the plugin app list revision when compiled output changes", async () => {
|
|
211
|
+
const pluginName = `charts-${Math.random().toString(36).slice(2, 8)}`;
|
|
212
|
+
const appDir = join(getWorkspacePluginsDir(), pluginName, "apps", "viewer");
|
|
213
|
+
mkdirSync(appDir, { recursive: true });
|
|
214
|
+
writeFileSync(
|
|
215
|
+
join(getWorkspacePluginsDir(), pluginName, "package.json"),
|
|
216
|
+
JSON.stringify({ name: pluginName, version: "1.0.0" }),
|
|
217
|
+
);
|
|
218
|
+
const pluginAppId = `plugins~${pluginName}~viewer`;
|
|
219
|
+
const before = (await handleListApps({})) as {
|
|
220
|
+
apps: Array<{ id: string; updatedAt: number }>;
|
|
221
|
+
};
|
|
222
|
+
const previousRevision = before.apps.find(
|
|
223
|
+
(app) => app.id === pluginAppId,
|
|
224
|
+
)?.updatedAt;
|
|
225
|
+
expect(previousRevision).toBeDefined();
|
|
226
|
+
const builtAt = Math.ceil(previousRevision!) + 1_000;
|
|
227
|
+
compileApp.mockImplementationOnce(async (sourceDir) => {
|
|
228
|
+
const distDir = join(sourceDir, "dist");
|
|
229
|
+
mkdirSync(distDir, { recursive: true });
|
|
230
|
+
utimesSync(distDir, new Date(builtAt), new Date(builtAt));
|
|
231
|
+
return { ok: true, errors: [], warnings: [], durationMs: 11 };
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
await handleRefreshApp({ pathParams: { id: pluginAppId } });
|
|
235
|
+
const after = (await handleListApps({})) as {
|
|
236
|
+
apps: Array<{ id: string; updatedAt: number }>;
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
expect(after.apps.find((app) => app.id === pluginAppId)?.updatedAt).toBe(
|
|
240
|
+
builtAt,
|
|
241
|
+
);
|
|
242
|
+
});
|
|
243
|
+
|
|
174
244
|
test("returns compile errors without throwing", async () => {
|
|
175
245
|
const created = createApp({
|
|
176
246
|
name: "Budget",
|
|
@@ -1239,8 +1239,12 @@ describe("GET /v1/plugins/search", () => {
|
|
|
1239
1239
|
|
|
1240
1240
|
async function invokeUninstall(
|
|
1241
1241
|
args: RouteHandlerArgs = {},
|
|
1242
|
-
): Promise<{ name: string; target: string }> {
|
|
1243
|
-
return (await uninstallHandler(args)) as {
|
|
1242
|
+
): Promise<{ name: string; target: string; warnings?: string[] }> {
|
|
1243
|
+
return (await uninstallHandler(args)) as {
|
|
1244
|
+
name: string;
|
|
1245
|
+
target: string;
|
|
1246
|
+
warnings?: string[];
|
|
1247
|
+
};
|
|
1244
1248
|
}
|
|
1245
1249
|
|
|
1246
1250
|
describe("DELETE /v1/plugins/:name", () => {
|
|
@@ -1278,6 +1282,22 @@ describe("DELETE /v1/plugins/:name", () => {
|
|
|
1278
1282
|
expectPluginsListBroadcast();
|
|
1279
1283
|
});
|
|
1280
1284
|
|
|
1285
|
+
test("returns non-fatal cleanup warnings from the shared uninstall", async () => {
|
|
1286
|
+
uninstallSpy.mockImplementation(async (opts) => ({
|
|
1287
|
+
name: opts.name,
|
|
1288
|
+
target: `/workspace/.vellum/plugins/${opts.name}`,
|
|
1289
|
+
warnings: ["plugin.uninstall.mcp_oauth_credentials_unchecked"],
|
|
1290
|
+
}));
|
|
1291
|
+
|
|
1292
|
+
const result = await invokeUninstall({
|
|
1293
|
+
pathParams: { name: "simple-memory" },
|
|
1294
|
+
});
|
|
1295
|
+
|
|
1296
|
+
expect(result.warnings).toEqual([
|
|
1297
|
+
"plugin.uninstall.mcp_oauth_credentials_unchecked",
|
|
1298
|
+
]);
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1281
1301
|
test("threads x-vellum-client-id into the published event's originClientId", async () => {
|
|
1282
1302
|
uninstallSpy.mockImplementation(async (opts) => ({
|
|
1283
1303
|
name: opts.name,
|
|
@@ -2759,7 +2779,9 @@ describe("POST /v1/plugins/:name/disable", () => {
|
|
|
2759
2779
|
toggleResult(name, "disable"),
|
|
2760
2780
|
);
|
|
2761
2781
|
|
|
2762
|
-
const result = await invokeDisable({
|
|
2782
|
+
const result = await invokeDisable({
|
|
2783
|
+
pathParams: { name: "simple-memory" },
|
|
2784
|
+
});
|
|
2763
2785
|
|
|
2764
2786
|
expect(result).toEqual({ ok: true });
|
|
2765
2787
|
expect(disablePluginSpy.mock.calls[0]?.[0]).toBe("simple-memory");
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* has landed.
|
|
19
19
|
* POST /v1/acp/claude/auth/exchange — complete a manual/cloud flow: accept the
|
|
20
20
|
* pasted `code#state` (or a raw code + state), exchange it, and store the
|
|
21
|
-
* Claude OAuth token.
|
|
21
|
+
* Claude OAuth token set (access token plus any refresh token and expiry).
|
|
22
22
|
*
|
|
23
23
|
* `handleStartAuth` picks loopback vs. manual: a containerized host, or a client
|
|
24
24
|
* that sets `preferManual` because its browser can't reach the daemon's
|
|
@@ -126,7 +126,11 @@ async function handleStartLocalAuth(): Promise<StartResponse> {
|
|
|
126
126
|
// can react. Runs in the background — the web client opens `authorize_url`.
|
|
127
127
|
void flow.completion
|
|
128
128
|
.then(async (result) => {
|
|
129
|
-
await storeAcpClaudeToken(
|
|
129
|
+
await storeAcpClaudeToken({
|
|
130
|
+
accessToken: result.tokens.accessToken,
|
|
131
|
+
refreshToken: result.tokens.refreshToken,
|
|
132
|
+
expiresIn: result.tokens.expiresIn,
|
|
133
|
+
});
|
|
130
134
|
pendingFlows.mark(flow.state, "connected");
|
|
131
135
|
log.info("ACP Claude local OAuth flow connected");
|
|
132
136
|
})
|
|
@@ -216,7 +220,11 @@ async function handleExchange(args: RouteHandlerArgs): Promise<{ ok: true }> {
|
|
|
216
220
|
pending.codeVerifier,
|
|
217
221
|
state,
|
|
218
222
|
);
|
|
219
|
-
await storeAcpClaudeToken(
|
|
223
|
+
await storeAcpClaudeToken({
|
|
224
|
+
accessToken: result.tokens.accessToken,
|
|
225
|
+
refreshToken: result.tokens.refreshToken,
|
|
226
|
+
expiresIn: result.tokens.expiresIn,
|
|
227
|
+
});
|
|
220
228
|
} catch (err) {
|
|
221
229
|
const message = err instanceof Error ? err.message : String(err);
|
|
222
230
|
pendingFlows.mark(state, "error", { error: message });
|
|
@@ -181,7 +181,7 @@ function workspaceAppItem(a: AppDefinition): AppListItem {
|
|
|
181
181
|
description: a.description,
|
|
182
182
|
icon: a.icon,
|
|
183
183
|
createdAt: a.createdAt,
|
|
184
|
-
updatedAt: a.updatedAt,
|
|
184
|
+
updatedAt: appContentUpdatedAt(getAppDirPath(a.id), a.updatedAt),
|
|
185
185
|
version: a.version ?? "1.0.0",
|
|
186
186
|
contentId: computeContentId(a.name),
|
|
187
187
|
origin: "workspace",
|
|
@@ -203,13 +203,28 @@ function pluginAppItem(app: EnumeratedApp): AppListItem {
|
|
|
203
203
|
id: app.id,
|
|
204
204
|
name: app.name,
|
|
205
205
|
createdAt: mtime,
|
|
206
|
-
updatedAt: mtime,
|
|
206
|
+
updatedAt: appContentUpdatedAt(app.sourcePath, mtime),
|
|
207
207
|
version: "1.0.0",
|
|
208
208
|
contentId: computeContentId(app.name),
|
|
209
209
|
origin: `plugin:${pluginName}`,
|
|
210
210
|
};
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
/** The newest persisted metadata or compiled-output revision for an app. */
|
|
214
|
+
function appContentUpdatedAt(
|
|
215
|
+
sourcePath: string,
|
|
216
|
+
metadataUpdatedAt: number,
|
|
217
|
+
): number {
|
|
218
|
+
try {
|
|
219
|
+
return Math.max(
|
|
220
|
+
metadataUpdatedAt,
|
|
221
|
+
statSync(join(sourcePath, "dist")).mtimeMs,
|
|
222
|
+
);
|
|
223
|
+
} catch {
|
|
224
|
+
return metadataUpdatedAt;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
213
228
|
function getAppDataResult(
|
|
214
229
|
method: string,
|
|
215
230
|
appId: string,
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { resolveTurnClientOs } from "../../daemon/conversation-client-surface.js";
|
|
2
|
+
import { findConversation } from "../../daemon/conversation-registry.js";
|
|
3
|
+
import type { ToolContext } from "../../tools/types.js";
|
|
4
|
+
import { resolveActorPrincipalIdForLocalGuardian } from "../local-actor-identity.js";
|
|
5
|
+
|
|
6
|
+
export function browserCliConversationKey(sessionId: string): string {
|
|
7
|
+
return `browser-cli:${sessionId}`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function resolveBrowserContext(
|
|
11
|
+
conversationId: string | undefined,
|
|
12
|
+
sessionId: string,
|
|
13
|
+
headers: Record<string, string>,
|
|
14
|
+
abortSignal?: AbortSignal,
|
|
15
|
+
): Promise<ToolContext> {
|
|
16
|
+
const conversation = conversationId
|
|
17
|
+
? findConversation(conversationId)
|
|
18
|
+
: undefined;
|
|
19
|
+
const actor = await resolveActorPrincipalIdForLocalGuardian(
|
|
20
|
+
conversation?.getTurnActorPrincipalId() ??
|
|
21
|
+
headers["x-vellum-actor-principal-id"]?.trim(),
|
|
22
|
+
);
|
|
23
|
+
const signals = [abortSignal, conversation?.abortController?.signal].filter(
|
|
24
|
+
(signal): signal is AbortSignal => signal !== undefined,
|
|
25
|
+
);
|
|
26
|
+
return {
|
|
27
|
+
workingDir: process.cwd(),
|
|
28
|
+
conversationId: conversation
|
|
29
|
+
? conversationId!
|
|
30
|
+
: browserCliConversationKey(sessionId),
|
|
31
|
+
trustClass: conversation?.trustContext?.trustClass ?? "unknown",
|
|
32
|
+
transportInterface: conversation?.transportInterface,
|
|
33
|
+
clientOs: conversation
|
|
34
|
+
? resolveTurnClientOs(conversation).clientOs
|
|
35
|
+
: undefined,
|
|
36
|
+
sourceActorPrincipalId: actor,
|
|
37
|
+
signal: signals.length ? AbortSignal.any(signals) : undefined,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -16,10 +16,12 @@ import {
|
|
|
16
16
|
BROWSER_OPERATIONS,
|
|
17
17
|
type BrowserOperation,
|
|
18
18
|
} from "../../browser/types.js";
|
|
19
|
-
import {
|
|
19
|
+
import { shouldUseVirtualDesktopBrowser } from "../../browser/virtual-desktop-target.js";
|
|
20
|
+
import { executeDesktopBrowserOperation } from "../../desktop/desktop-browser-operations.js";
|
|
20
21
|
import type { ContentBlock } from "../../providers/types.js";
|
|
21
22
|
import { LOCAL_PRINCIPALS } from "../auth/route-policy.js";
|
|
22
|
-
import {
|
|
23
|
+
import { resolveBrowserContext } from "./browser-context.js";
|
|
24
|
+
export { browserCliConversationKey } from "./browser-context.js";
|
|
23
25
|
import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
|
|
24
26
|
|
|
25
27
|
// ── Param validation ─────────────────────────────────────────────────
|
|
@@ -29,18 +31,9 @@ const BrowserExecuteParams = z.object({
|
|
|
29
31
|
input: z.record(z.string(), z.unknown()).default({}),
|
|
30
32
|
sessionId: z.string().min(1).default("default"),
|
|
31
33
|
conversationId: z.string().min(1).optional(),
|
|
34
|
+
desktop: z.boolean().optional(),
|
|
32
35
|
});
|
|
33
36
|
|
|
34
|
-
// ── Conversation key ─────────────────────────────────────────────────
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Build a deterministic conversation key from a session ID.
|
|
38
|
-
* All CLI browser calls with the same session share browser state.
|
|
39
|
-
*/
|
|
40
|
-
export function browserCliConversationKey(sessionId: string): string {
|
|
41
|
-
return `browser-cli:${sessionId}`;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
37
|
// ── Screenshot extraction ────────────────────────────────────────────
|
|
45
38
|
|
|
46
39
|
/**
|
|
@@ -70,45 +63,21 @@ function extractScreenshots(
|
|
|
70
63
|
async function handleBrowserExecute({
|
|
71
64
|
body = {},
|
|
72
65
|
headers = {},
|
|
66
|
+
abortSignal,
|
|
73
67
|
}: RouteHandlerArgs) {
|
|
74
|
-
const { operation, input, sessionId, conversationId } =
|
|
68
|
+
const { operation, input, sessionId, conversationId, desktop } =
|
|
75
69
|
BrowserExecuteParams.parse(body);
|
|
76
70
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
: undefined;
|
|
83
|
-
|
|
84
|
-
const resolvedConversationId = conversation
|
|
85
|
-
? conversationId!
|
|
86
|
-
: browserCliConversationKey(sessionId);
|
|
87
|
-
|
|
88
|
-
// Actor principal lets host-browser routing enforce same-actor ownership.
|
|
89
|
-
// A live conversation's turn actor owns the request (e.g. a nested-bash
|
|
90
|
-
// browser call inherits the conversation's actor), so it wins over the
|
|
91
|
-
// request header — which over IPC may carry a synthetic local-guardian id
|
|
92
|
-
// injected for header-less local callers. Resolve through the local-guardian
|
|
93
|
-
// translation so the value matches the actorPrincipalId host_browser clients
|
|
94
|
-
// register with (dev-bypass otherwise mismatches).
|
|
95
|
-
const headerActor =
|
|
96
|
-
headers["x-vellum-actor-principal-id"]?.trim() || undefined;
|
|
97
|
-
const sourceActorPrincipalId = await resolveActorPrincipalIdForLocalGuardian(
|
|
98
|
-
conversation?.getTurnActorPrincipalId() ?? headerActor,
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
const result = await executeBrowserOperation(
|
|
102
|
-
operation as BrowserOperation,
|
|
103
|
-
input,
|
|
104
|
-
{
|
|
105
|
-
workingDir: process.cwd(),
|
|
106
|
-
conversationId: resolvedConversationId,
|
|
107
|
-
trustClass: conversation?.trustContext?.trustClass ?? "unknown",
|
|
108
|
-
transportInterface: conversation?.transportInterface,
|
|
109
|
-
sourceActorPrincipalId,
|
|
110
|
-
},
|
|
71
|
+
const context = await resolveBrowserContext(
|
|
72
|
+
conversationId,
|
|
73
|
+
sessionId,
|
|
74
|
+
headers,
|
|
75
|
+
abortSignal,
|
|
111
76
|
);
|
|
77
|
+
const execute = shouldUseVirtualDesktopBrowser(desktop, input, context)
|
|
78
|
+
? executeDesktopBrowserOperation
|
|
79
|
+
: executeBrowserOperation;
|
|
80
|
+
const result = await execute(operation as BrowserOperation, input, context);
|
|
112
81
|
|
|
113
82
|
const screenshots = extractScreenshots(result.contentBlocks);
|
|
114
83
|
|
|
@@ -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({
|
|
@@ -34,7 +34,6 @@ import { credentialKey } from "../../security/credential-key.js";
|
|
|
34
34
|
import {
|
|
35
35
|
deleteSecureKeyAsync,
|
|
36
36
|
getActiveBackendInfoAsync,
|
|
37
|
-
getSecureKeyAsync,
|
|
38
37
|
getSecureKeyResultAsync,
|
|
39
38
|
} from "../../security/secure-keys.js";
|
|
40
39
|
import {
|
|
@@ -561,9 +560,7 @@ async function handleCredentialsDelete({ body }: RouteHandlerArgs) {
|
|
|
561
560
|
|
|
562
561
|
const key = credentialKey(service, field);
|
|
563
562
|
const affectedConnections = assertCredentialNotInUse(key, force === true);
|
|
564
|
-
const
|
|
565
|
-
const deleteResult =
|
|
566
|
-
existing != null ? await deleteSecureKeyAsync(key) : "not-found";
|
|
563
|
+
const deleteResult = await deleteSecureKeyAsync(key);
|
|
567
564
|
|
|
568
565
|
if (deleteResult === "error") {
|
|
569
566
|
throw new InternalError(
|
|
@@ -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
|
}));
|