@vellumai/assistant 0.12.2-staging.2 → 0.12.2-staging.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/desktop-browser-cli.md +33 -0
- package/openapi.yaml +17 -5
- package/package.json +1 -1
- package/scripts/smoke-desktop-browser-cli.ts +279 -0
- package/src/__tests__/config-schema.test.ts +1 -1
- package/src/__tests__/config-sounds-sync.test.ts +23 -0
- package/src/__tests__/conversation-error.test.ts +4 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +6 -4
- package/src/__tests__/external-plugin-loader.test.ts +52 -0
- package/src/__tests__/headless-browser-mode.test.ts +48 -0
- package/src/__tests__/host-cu-proxy.test.ts +116 -18
- package/src/__tests__/list-all-apps.test.ts +40 -8
- package/src/__tests__/mcp-auth-routes.test.ts +99 -0
- package/src/__tests__/mcp-client-auth.test.ts +54 -0
- package/src/__tests__/mcp-health-check.test.ts +32 -32
- package/src/__tests__/mcp-list-plugin-servers.test.ts +77 -32
- package/src/__tests__/mcp-tool-annotations-risk.test.ts +4 -2
- package/src/__tests__/skills.test.ts +10 -0
- package/src/__tests__/user-plugin-loader.test.ts +21 -0
- package/src/apps/app-store.ts +6 -5
- package/src/browser/operations.ts +2 -1
- package/src/browser/types.ts +7 -0
- package/src/browser/virtual-desktop-target.ts +34 -0
- package/src/calls/__tests__/progress-narration.test.ts +23 -0
- package/src/calls/__tests__/voice-control-protocol.test.ts +7 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +62 -0
- package/src/calls/progress-narration.ts +15 -2
- package/src/calls/voice-control-protocol.ts +34 -4
- package/src/calls/voice-session-bridge.ts +23 -0
- package/src/cli/commands/__tests__/browser.test.ts +84 -3
- package/src/cli/commands/browser.help.ts +43 -0
- package/src/cli/commands/browser.ts +52 -8
- package/src/cli/commands/plugins.ts +16 -5
- package/src/cli/lib/__tests__/install-from-github.test.ts +111 -0
- package/src/cli/lib/__tests__/install-from-platform.test.ts +77 -2
- package/src/cli/lib/__tests__/list-installed-plugins.test.ts +45 -3
- package/src/cli/lib/__tests__/plugin-details.test.ts +26 -0
- package/src/cli/lib/__tests__/uninstall-plugin.test.ts +122 -5
- package/src/cli/lib/bundled-marketplace.json +13 -0
- package/src/cli/lib/install-from-github.ts +17 -18
- package/src/cli/lib/install-from-platform.ts +14 -9
- package/src/cli/lib/list-installed-plugins.ts +21 -35
- package/src/cli/lib/plugin-details.ts +21 -8
- package/src/cli/lib/uninstall-plugin.ts +59 -1
- package/src/config/__tests__/plugin-resident-skill-discovery.test.ts +21 -0
- package/src/config/bundled-skills/computer-use/SKILL.md +30 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +2 -2
- package/src/config/bundled-skills/screen-annotation/SKILL.md +2 -2
- package/src/config/feature-flag-registry.json +2 -2
- package/src/config/schemas/__tests__/voice.test.ts +2 -2
- package/src/config/schemas/mcp.ts +9 -7
- package/src/config/schemas/monitoring.ts +9 -0
- package/src/config/schemas/voice.ts +2 -2
- package/src/config/skills.ts +13 -21
- package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +47 -0
- package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +1 -0
- package/src/daemon/conversation-client-surface.ts +26 -0
- package/src/daemon/conversation-runtime-assembly.ts +1 -1
- package/src/daemon/conversation-tool-setup.ts +31 -29
- package/src/daemon/host-cu-proxy.ts +51 -26
- package/src/daemon/host-proxy-preactivation.ts +2 -0
- package/src/daemon/mcp-reload-service.ts +7 -0
- package/src/daemon/message-types/sync.ts +1 -0
- package/src/daemon/providers-setup.ts +4 -0
- package/src/desktop/__tests__/fake-desktop.ts +1 -0
- package/src/desktop/desktop-automation-lease.test.ts +208 -0
- package/src/desktop/desktop-automation-lease.ts +270 -0
- package/src/desktop/desktop-browser-client.test.ts +413 -0
- package/src/desktop/desktop-browser-client.ts +430 -0
- package/src/desktop/desktop-browser-cursor.ts +40 -0
- package/src/desktop/desktop-browser-endpoint.test.ts +78 -0
- package/src/desktop/desktop-browser-endpoint.ts +145 -0
- package/src/desktop/desktop-browser-operations.ts +130 -0
- package/src/desktop/desktop-chrome-session.test.ts +48 -0
- package/src/desktop/desktop-chrome-session.ts +58 -8
- package/src/desktop/desktop-dependencies.test.ts +29 -0
- package/src/desktop/desktop-dependencies.ts +26 -5
- package/src/desktop/desktop-display.ts +9 -0
- package/src/desktop/desktop-panel-config.ts +14 -1
- package/src/desktop/desktop-session-manager.test.ts +86 -3
- package/src/desktop/desktop-session-manager.ts +144 -24
- package/src/desktop/desktop-stream-bridge.ts +1 -1
- package/src/desktop/virtual-desktop-feature.ts +18 -0
- package/src/desktop/virtual-desktop-platform.test.ts +61 -0
- package/src/i18n/__tests__/i18n.test.ts +9 -5
- package/src/i18n/messages.ts +13 -4
- package/src/ipc/__tests__/browser-ipc.test.ts +230 -2
- package/src/ipc/__tests__/cancel-on-disconnect.test.ts +54 -0
- package/src/ipc/assistant-server.ts +15 -1
- package/src/ipc/cli-client.ts +13 -5
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +22 -0
- package/src/live-voice/__tests__/live-voice-events.test.ts +3 -2
- package/src/live-voice/__tests__/live-voice-session-telemetry.test.ts +19 -0
- package/src/live-voice/__tests__/protocol.test.ts +11 -3
- package/src/live-voice/__tests__/session-controls.test.ts +34 -0
- package/src/live-voice/live-voice-metrics.ts +9 -0
- package/src/live-voice/live-voice-session.ts +3 -0
- package/src/live-voice/protocol.ts +10 -1
- package/src/live-voice/session-controls.ts +28 -0
- package/src/mcp/__tests__/credential-target.test.ts +59 -0
- package/src/mcp/__tests__/effective-config.test.ts +11 -0
- package/src/mcp/__tests__/manager-state.test.ts +128 -0
- package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +20 -6
- package/src/mcp/__tests__/mcp-auth-state-target.test.ts +49 -0
- package/src/mcp/__tests__/plugin-mcp-oauth-cleanup.test.ts +107 -0
- package/src/mcp/__tests__/plugin-mcp-oauth-provider-isolation.test.ts +89 -0
- package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +63 -11
- package/src/mcp/__tests__/reload-signal-emission.test.ts +39 -2
- package/src/mcp/client.ts +64 -16
- package/src/mcp/credential-target.ts +105 -0
- package/src/mcp/effective-config.ts +42 -15
- package/src/mcp/manager.ts +61 -8
- package/src/mcp/mcp-auth-orchestrator.ts +17 -4
- package/src/mcp/mcp-auth-state.ts +38 -6
- package/src/mcp/mcp-oauth-provider.ts +101 -33
- package/src/monitoring/__tests__/mount-watch.test.ts +236 -0
- package/src/monitoring/mount-watch.ts +348 -0
- package/src/monitoring/worker.ts +9 -0
- package/src/oauth/seed-providers.ts +11 -0
- package/src/onboarding/checkin-event.ts +2 -1
- package/src/plugins/__tests__/installed-plugin-dirs.test.ts +14 -1
- package/src/plugins/__tests__/mcp-servers.test.ts +38 -8
- package/src/plugins/external-plugin-loader.ts +49 -86
- package/src/plugins/installed-plugin-dirs.ts +6 -4
- package/src/plugins/mcp-servers.ts +9 -15
- package/src/plugins/mtime-cache.ts +2 -1
- package/src/plugins/user-loader.ts +2 -1
- package/src/providers/inference/__tests__/endpoint-probe.test.ts +45 -0
- package/src/providers/inference/endpoint-probe.ts +27 -12
- package/src/providers/openai/responses-provider.ts +9 -2
- package/src/providers/opencode/client.test.ts +167 -0
- package/src/providers/opencode/client.ts +73 -4
- package/src/runtime/AGENTS.md +4 -0
- package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +45 -2
- package/src/runtime/http-server.ts +6 -6
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +25 -3
- package/src/runtime/routes/browser-context.ts +39 -0
- package/src/runtime/routes/browser-routes.ts +16 -47
- package/src/runtime/routes/browser-tabs-routes.ts +72 -29
- package/src/runtime/routes/desktop-setup-routes.test.ts +16 -1
- package/src/runtime/routes/desktop-setup-routes.ts +7 -5
- package/src/runtime/routes/mcp-auth-routes.ts +106 -94
- package/src/runtime/routes/plugins-routes.ts +14 -1
- package/src/runtime/sync/resource-sync-events.ts +4 -0
- package/src/tools/browser/browser-execution.ts +44 -13
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +69 -0
- package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +1 -0
- package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +5 -0
- package/src/tools/computer-use/definitions.ts +2 -3
- package/src/util/abort-reasons.ts +12 -1
- package/src/util/plugin-manifest.ts +203 -0
- package/src/desktop/desktop-feature.test.ts +0 -18
- package/src/desktop/desktop-feature.ts +0 -19
|
@@ -9,18 +9,72 @@ import {
|
|
|
9
9
|
EMPTY_ASSISTANT_TURN_PLACEHOLDER,
|
|
10
10
|
OpenAIChatCompletionsProvider,
|
|
11
11
|
} from "../openai/chat-completions-provider.js";
|
|
12
|
+
import { OpenAIResponsesProvider } from "../openai/responses-provider.js";
|
|
13
|
+
import type { Message } from "../types.js";
|
|
12
14
|
import {
|
|
13
15
|
buildOpenCodeRequestHeaders,
|
|
14
16
|
OPENCODE_GO_BASE_URL,
|
|
15
17
|
OPENCODE_REQUEST_HEADER,
|
|
18
|
+
OPENCODE_RESPONSES_ONLY_MODELS,
|
|
16
19
|
OPENCODE_SESSION_HEADER,
|
|
17
20
|
OPENCODE_ZEN_BASE_URL,
|
|
18
21
|
OpenCodeProvider,
|
|
22
|
+
OpenCodeResponsesProvider,
|
|
19
23
|
resetOpenCodeFallbackSessionForTests,
|
|
20
24
|
resolveOpenCodeBaseURL,
|
|
21
25
|
resolveOpenCodeRequestHeaders,
|
|
22
26
|
} from "./client.js";
|
|
23
27
|
|
|
28
|
+
const USER_TURN: Message[] = [
|
|
29
|
+
{ role: "user", content: [{ type: "text", text: "question" }] },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
/** Stub both wires on an OpenCodeProvider and record which one was called. */
|
|
33
|
+
function stubTransports(provider: OpenCodeProvider) {
|
|
34
|
+
const chat: unknown[] = [];
|
|
35
|
+
const responses: Array<{ params: unknown; options: unknown }> = [];
|
|
36
|
+
(provider as unknown as { client: unknown }).client = {
|
|
37
|
+
chat: {
|
|
38
|
+
completions: {
|
|
39
|
+
create: async (params: unknown) => {
|
|
40
|
+
chat.push(params);
|
|
41
|
+
return {
|
|
42
|
+
async *[Symbol.asyncIterator]() {
|
|
43
|
+
yield {
|
|
44
|
+
choices: [{ delta: { content: "ok" }, finish_reason: "stop" }],
|
|
45
|
+
usage: { prompt_tokens: 2, completion_tokens: 1 },
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
const inner = (
|
|
54
|
+
provider as unknown as { getResponsesInner(): OpenCodeResponsesProvider }
|
|
55
|
+
).getResponsesInner();
|
|
56
|
+
(inner as unknown as { client: unknown }).client = {
|
|
57
|
+
responses: {
|
|
58
|
+
create: async (params: unknown, options: unknown) => {
|
|
59
|
+
responses.push({ params, options });
|
|
60
|
+
return {
|
|
61
|
+
async *[Symbol.asyncIterator]() {
|
|
62
|
+
yield { type: "response.output_text.delta", delta: "ok" };
|
|
63
|
+
yield {
|
|
64
|
+
type: "response.completed",
|
|
65
|
+
response: {
|
|
66
|
+
status: "completed",
|
|
67
|
+
usage: { input_tokens: 2, output_tokens: 1 },
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
return { chat, responses };
|
|
76
|
+
}
|
|
77
|
+
|
|
24
78
|
describe("resolveOpenCodeBaseURL", () => {
|
|
25
79
|
test("defaults to OpenCode Zen", () => {
|
|
26
80
|
expect(resolveOpenCodeBaseURL()).toBe(OPENCODE_ZEN_BASE_URL);
|
|
@@ -241,3 +295,116 @@ describe("OpenCodeProvider", () => {
|
|
|
241
295
|
expect(assistantMsg.content).toBe(EMPTY_ASSISTANT_TURN_PLACEHOLDER);
|
|
242
296
|
});
|
|
243
297
|
});
|
|
298
|
+
|
|
299
|
+
describe("OPENCODE_RESPONSES_ONLY_MODELS", () => {
|
|
300
|
+
test("lists the verified responses-only models and nothing else", () => {
|
|
301
|
+
expect([...OPENCODE_RESPONSES_ONLY_MODELS].sort()).toEqual([
|
|
302
|
+
"muse-spark-1.2-contributor-free",
|
|
303
|
+
"muse-spark-1.3-contributor-free",
|
|
304
|
+
]);
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
describe("OpenCodeResponsesProvider", () => {
|
|
309
|
+
test("is the Responses transport under the opencode name on OpenCode Zen", () => {
|
|
310
|
+
const provider = new OpenCodeResponsesProvider(
|
|
311
|
+
"sk-test",
|
|
312
|
+
"muse-spark-1.2-contributor-free",
|
|
313
|
+
);
|
|
314
|
+
expect(provider).toBeInstanceOf(OpenAIResponsesProvider);
|
|
315
|
+
expect(provider.name).toBe("opencode");
|
|
316
|
+
expect(
|
|
317
|
+
(provider as unknown as { client: { baseURL?: string } }).client.baseURL,
|
|
318
|
+
).toBe(OPENCODE_ZEN_BASE_URL);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test("keeps a configured OpenCode Go origin", () => {
|
|
322
|
+
const provider = new OpenCodeResponsesProvider(
|
|
323
|
+
"sk-test",
|
|
324
|
+
"muse-spark-1.2-contributor-free",
|
|
325
|
+
{ baseURL: OPENCODE_GO_BASE_URL },
|
|
326
|
+
);
|
|
327
|
+
expect(
|
|
328
|
+
(provider as unknown as { client: { baseURL?: string } }).client.baseURL,
|
|
329
|
+
).toBe(OPENCODE_GO_BASE_URL);
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
describe("OpenCodeProvider transport selection", () => {
|
|
334
|
+
beforeEach(() => {
|
|
335
|
+
mockDeviceId = null;
|
|
336
|
+
resetOpenCodeFallbackSessionForTests();
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test("sends a responses-only model through the Responses API with the caller's OpenCode headers", async () => {
|
|
340
|
+
const provider = new OpenCodeProvider(
|
|
341
|
+
"sk-test",
|
|
342
|
+
"muse-spark-1.2-contributor-free",
|
|
343
|
+
);
|
|
344
|
+
const { chat, responses } = stubTransports(provider);
|
|
345
|
+
|
|
346
|
+
const result = await provider.sendMessage(USER_TURN, {
|
|
347
|
+
config: {
|
|
348
|
+
requestHeaders: {
|
|
349
|
+
[OPENCODE_SESSION_HEADER]: "conv-xyz",
|
|
350
|
+
[OPENCODE_REQUEST_HEADER]: "req-123",
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
expect(chat).toHaveLength(0);
|
|
356
|
+
expect(responses).toHaveLength(1);
|
|
357
|
+
expect((responses[0]!.params as { model: string }).model).toBe(
|
|
358
|
+
"muse-spark-1.2-contributor-free",
|
|
359
|
+
);
|
|
360
|
+
const options = responses[0]!.options as {
|
|
361
|
+
headers?: Record<string, string>;
|
|
362
|
+
};
|
|
363
|
+
expect(options.headers?.[OPENCODE_SESSION_HEADER]).toBe("conv-xyz");
|
|
364
|
+
expect(options.headers?.[OPENCODE_REQUEST_HEADER]).toBe("req-123");
|
|
365
|
+
expect(options.headers).not.toHaveProperty("session_id");
|
|
366
|
+
expect(result.content).toEqual([{ type: "text", text: "ok" }]);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test("stamps the session backstop on a configless Responses call", async () => {
|
|
370
|
+
mockDeviceId = "device-abc";
|
|
371
|
+
const provider = new OpenCodeProvider(
|
|
372
|
+
"sk-test",
|
|
373
|
+
"muse-spark-1.2-contributor-free",
|
|
374
|
+
);
|
|
375
|
+
const { responses } = stubTransports(provider);
|
|
376
|
+
|
|
377
|
+
await provider.sendMessage(USER_TURN);
|
|
378
|
+
|
|
379
|
+
const options = responses[0]!.options as {
|
|
380
|
+
headers?: Record<string, string>;
|
|
381
|
+
};
|
|
382
|
+
expect(options.headers?.[OPENCODE_SESSION_HEADER]).toBe("device-abc");
|
|
383
|
+
expect(options.headers?.[OPENCODE_REQUEST_HEADER]).toBeTruthy();
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test("keeps a chat-completions model on the chat wire", async () => {
|
|
387
|
+
const provider = new OpenCodeProvider("sk-test", "mimo-v2.5-free");
|
|
388
|
+
const { chat, responses } = stubTransports(provider);
|
|
389
|
+
|
|
390
|
+
await provider.sendMessage(USER_TURN);
|
|
391
|
+
|
|
392
|
+
expect(chat).toHaveLength(1);
|
|
393
|
+
expect(responses).toHaveLength(0);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
test("a per-call model override picks the transport that model needs", async () => {
|
|
397
|
+
const provider = new OpenCodeProvider("sk-test", "mimo-v2.5-free");
|
|
398
|
+
const { chat, responses } = stubTransports(provider);
|
|
399
|
+
|
|
400
|
+
await provider.sendMessage(USER_TURN, {
|
|
401
|
+
config: { model: "muse-spark-1.2-contributor-free" },
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
expect(chat).toHaveLength(0);
|
|
405
|
+
expect(responses).toHaveLength(1);
|
|
406
|
+
expect((responses[0]!.params as { model: string }).model).toBe(
|
|
407
|
+
"muse-spark-1.2-contributor-free",
|
|
408
|
+
);
|
|
409
|
+
});
|
|
410
|
+
});
|
|
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
|
|
3
3
|
import { getExistingDeviceId } from "../../util/device-id.js";
|
|
4
4
|
import { OpenAIChatCompletionsProvider } from "../openai/chat-completions-provider.js";
|
|
5
|
+
import { OpenAIResponsesProvider } from "../openai/responses-provider.js";
|
|
5
6
|
import type {
|
|
6
7
|
Message,
|
|
7
8
|
ProviderResponse,
|
|
@@ -18,6 +19,18 @@ export interface OpenCodeProviderOptions {
|
|
|
18
19
|
streamTimeoutMs?: number;
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
/**
|
|
23
|
+
* OpenCode Zen models served only by the Responses API: Zen answers
|
|
24
|
+
* `/chat/completions` for them with HTTP 500 and completes the same request
|
|
25
|
+
* on `/responses`. OpenCode's own client picks the transport per model from
|
|
26
|
+
* its catalog's `api.endpoint`; the assistant has no such feed, so the
|
|
27
|
+
* verified ids live here. Everything else stays on chat completions.
|
|
28
|
+
*/
|
|
29
|
+
export const OPENCODE_RESPONSES_ONLY_MODELS: ReadonlySet<string> = new Set([
|
|
30
|
+
"muse-spark-1.2-contributor-free",
|
|
31
|
+
"muse-spark-1.3-contributor-free",
|
|
32
|
+
]);
|
|
33
|
+
|
|
21
34
|
/**
|
|
22
35
|
* Resolve the OpenCode chat-completions origin. A stored connection URL
|
|
23
36
|
* wins; otherwise the request goes to OpenCode Zen.
|
|
@@ -82,7 +95,38 @@ export function resolveOpenCodeRequestHeaders(
|
|
|
82
95
|
});
|
|
83
96
|
}
|
|
84
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Responses-API transport for OpenCode. Shares the chat client's origin
|
|
100
|
+
* resolution and provider name, so the same `x-opencode-session` /
|
|
101
|
+
* `x-opencode-request` headers reach its requests.
|
|
102
|
+
*/
|
|
103
|
+
export class OpenCodeResponsesProvider extends OpenAIResponsesProvider {
|
|
104
|
+
constructor(
|
|
105
|
+
apiKey: string,
|
|
106
|
+
model: string,
|
|
107
|
+
options: OpenCodeProviderOptions = {},
|
|
108
|
+
) {
|
|
109
|
+
super(apiKey, model, {
|
|
110
|
+
baseURL: resolveOpenCodeBaseURL(options.baseURL),
|
|
111
|
+
providerName: "opencode",
|
|
112
|
+
providerLabel: "OpenCode",
|
|
113
|
+
streamTimeoutMs: options.streamTimeoutMs,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* OpenCode adapter. Chat completions is the default wire; models in
|
|
120
|
+
* {@link OPENCODE_RESPONSES_ONLY_MODELS} are delegated to an
|
|
121
|
+
* {@link OpenCodeResponsesProvider} on the same origin. The choice is made
|
|
122
|
+
* per send from the effective model, so a per-call model override lands on
|
|
123
|
+
* the transport that model needs.
|
|
124
|
+
*/
|
|
85
125
|
export class OpenCodeProvider extends OpenAIChatCompletionsProvider {
|
|
126
|
+
private readonly openCodeApiKey: string;
|
|
127
|
+
private readonly openCodeOptions: OpenCodeProviderOptions;
|
|
128
|
+
private responsesInner: OpenCodeResponsesProvider | undefined;
|
|
129
|
+
|
|
86
130
|
constructor(
|
|
87
131
|
apiKey: string,
|
|
88
132
|
model: string,
|
|
@@ -96,20 +140,22 @@ export class OpenCodeProvider extends OpenAIChatCompletionsProvider {
|
|
|
96
140
|
assistantReasoningField: "reasoning_content",
|
|
97
141
|
omitToolChoiceWhenReasoning: true,
|
|
98
142
|
});
|
|
143
|
+
this.openCodeApiKey = apiKey;
|
|
144
|
+
this.openCodeOptions = options;
|
|
99
145
|
}
|
|
100
146
|
|
|
101
147
|
/**
|
|
102
148
|
* Transport-level backstop so a call that reaches this provider without
|
|
103
149
|
* going through `RetryProvider` (or with no config at all) still carries
|
|
104
|
-
* the session and request headers zen/go requires. Headers
|
|
105
|
-
* already resolved (conversation-derived) win.
|
|
150
|
+
* the session and request headers zen/go requires on either wire. Headers
|
|
151
|
+
* the caller already resolved (conversation-derived) win.
|
|
106
152
|
*/
|
|
107
153
|
override async sendMessage(
|
|
108
154
|
messages: Message[],
|
|
109
155
|
options?: SendMessageOptions,
|
|
110
156
|
): Promise<ProviderResponse> {
|
|
111
157
|
const config = options?.config ?? {};
|
|
112
|
-
|
|
158
|
+
const stamped: SendMessageOptions = {
|
|
113
159
|
...options,
|
|
114
160
|
config: {
|
|
115
161
|
...config,
|
|
@@ -118,6 +164,29 @@ export class OpenCodeProvider extends OpenAIChatCompletionsProvider {
|
|
|
118
164
|
...config.requestHeaders,
|
|
119
165
|
},
|
|
120
166
|
},
|
|
121
|
-
}
|
|
167
|
+
};
|
|
168
|
+
if (
|
|
169
|
+
OPENCODE_RESPONSES_ONLY_MODELS.has(this.resolveEffectiveModel(config))
|
|
170
|
+
) {
|
|
171
|
+
return this.getResponsesInner().sendMessage(messages, stamped);
|
|
172
|
+
}
|
|
173
|
+
return super.sendMessage(messages, stamped);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private resolveEffectiveModel(config: Record<string, unknown>): string {
|
|
177
|
+
const override =
|
|
178
|
+
typeof config.model === "string" && config.model.trim().length > 0
|
|
179
|
+
? config.model.trim()
|
|
180
|
+
: undefined;
|
|
181
|
+
return override ?? this.defaultModel;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private getResponsesInner(): OpenCodeResponsesProvider {
|
|
185
|
+
this.responsesInner ??= new OpenCodeResponsesProvider(
|
|
186
|
+
this.openCodeApiKey,
|
|
187
|
+
this.defaultModel,
|
|
188
|
+
this.openCodeOptions,
|
|
189
|
+
);
|
|
190
|
+
return this.responsesInner;
|
|
122
191
|
}
|
|
123
192
|
}
|
package/src/runtime/AGENTS.md
CHANGED
|
@@ -161,6 +161,10 @@ In the CDP factory the bridge is the internal `"host-bridge"` candidate kind (`I
|
|
|
161
161
|
|
|
162
162
|
**After the first successful CDP command**, the selected backend becomes **sticky** for the remainder of the tool invocation. Subsequent commands always route through the same backend so multi-command tool flows do not hop transports mid-step.
|
|
163
163
|
|
|
164
|
+
### Browser CLI surface defaults
|
|
165
|
+
|
|
166
|
+
The browser execute and tab routes share `browser/virtual-desktop-target.ts`. Platform-hosted web guardian conversations use installed, enabled virtual desktop Chrome by default. The shared `isVirtualDesktopEnabled` gate requires both `IS_PLATFORM` and `IS_CONTAINERIZED` alongside the `assistant-desktop` flag. Native renderer turns also use the `web` transport, so the frozen turn `clientOs` excludes native apps from automatic streamed-browser selection. Native apps retain their existing backend selection and fallback behavior. Explicit desktop/backend/client targets and existing personal-browser sessions override the surface default. Disabled or uninstalled desktop support retains the existing browser path; selection never triggers installation.
|
|
167
|
+
|
|
164
168
|
### Per-tool `browser_mode` override
|
|
165
169
|
|
|
166
170
|
All CDP-backed browser tools (`browser_navigate`, `browser_snapshot`, `browser_screenshot`, `browser_click`, `browser_type`, `browser_hover`, `browser_scroll`, `browser_press_key`, `browser_select_option`, `browser_wait_for`, `browser_extract`, `browser_fill_credential`, `browser_attach`, `browser_detach`, `browser_close`, `browser_status`) accept an optional `browser_mode` input parameter that overrides the automatic backend selection for that invocation.
|
|
@@ -15,6 +15,7 @@ describe("RuntimeHttpServer /v1/desktop/stream upgrade", () => {
|
|
|
15
15
|
let baseUrl: string;
|
|
16
16
|
let restoreAuthEnv: () => void;
|
|
17
17
|
const originalContainerized = process.env.IS_CONTAINERIZED;
|
|
18
|
+
const originalPlatform = process.env.IS_PLATFORM;
|
|
18
19
|
|
|
19
20
|
beforeEach(async () => {
|
|
20
21
|
restoreAuthEnv = requireHttpAuth();
|
|
@@ -22,6 +23,7 @@ describe("RuntimeHttpServer /v1/desktop/stream upgrade", () => {
|
|
|
22
23
|
server = new RuntimeHttpServer({ port, hostname: "127.0.0.1" });
|
|
23
24
|
await server.start();
|
|
24
25
|
baseUrl = `127.0.0.1:${server.actualPort}`;
|
|
26
|
+
process.env.IS_PLATFORM = "true";
|
|
25
27
|
});
|
|
26
28
|
|
|
27
29
|
afterEach(async () => {
|
|
@@ -32,6 +34,11 @@ describe("RuntimeHttpServer /v1/desktop/stream upgrade", () => {
|
|
|
32
34
|
} else {
|
|
33
35
|
process.env.IS_CONTAINERIZED = originalContainerized;
|
|
34
36
|
}
|
|
37
|
+
if (originalPlatform === undefined) {
|
|
38
|
+
delete process.env.IS_PLATFORM;
|
|
39
|
+
} else {
|
|
40
|
+
process.env.IS_PLATFORM = originalPlatform;
|
|
41
|
+
}
|
|
35
42
|
setOverridesForTesting({});
|
|
36
43
|
});
|
|
37
44
|
|
|
@@ -70,7 +77,9 @@ describe("RuntimeHttpServer /v1/desktop/stream upgrade", () => {
|
|
|
70
77
|
);
|
|
71
78
|
const closed = await waitForClose(ws);
|
|
72
79
|
expect(closed.code).toBe(4008);
|
|
73
|
-
expect(closed.reason).toBe(
|
|
80
|
+
expect(closed.reason).toBe(
|
|
81
|
+
"Virtual desktop is available only on enabled platform-hosted assistants",
|
|
82
|
+
);
|
|
74
83
|
});
|
|
75
84
|
for (const flag of [false, undefined]) {
|
|
76
85
|
test(`refuses a containerized stream with a disabled or missing flag (${flag})`, async () => {
|
|
@@ -83,7 +92,41 @@ describe("RuntimeHttpServer /v1/desktop/stream upgrade", () => {
|
|
|
83
92
|
);
|
|
84
93
|
const closed = await waitForClose(ws);
|
|
85
94
|
expect(closed.code).toBe(4008);
|
|
86
|
-
expect(closed.reason).toBe(
|
|
95
|
+
expect(closed.reason).toBe(
|
|
96
|
+
"Virtual desktop is available only on enabled platform-hosted assistants",
|
|
97
|
+
);
|
|
87
98
|
});
|
|
88
99
|
}
|
|
89
100
|
});
|
|
101
|
+
|
|
102
|
+
test("self-hosted containers cannot stream with the feature flag enabled", async () => {
|
|
103
|
+
const originalPlatform = process.env.IS_PLATFORM;
|
|
104
|
+
const originalContainerized = process.env.IS_CONTAINERIZED;
|
|
105
|
+
const restoreAuth = requireHttpAuth();
|
|
106
|
+
const server = new RuntimeHttpServer({ port: 0, hostname: "127.0.0.1" });
|
|
107
|
+
try {
|
|
108
|
+
await server.start();
|
|
109
|
+
process.env.IS_PLATFORM = "false";
|
|
110
|
+
process.env.IS_CONTAINERIZED = "true";
|
|
111
|
+
setOverridesForTesting({ "assistant-desktop": true });
|
|
112
|
+
const baseUrl = `127.0.0.1:${server.actualPort}`;
|
|
113
|
+
const ws = new WebSocket(
|
|
114
|
+
`ws://${baseUrl}/v1/desktop/stream?token=${encodeURIComponent(mintGatewayToken())}`,
|
|
115
|
+
);
|
|
116
|
+
expect((await waitForClose(ws)).code).toBe(4008);
|
|
117
|
+
} finally {
|
|
118
|
+
await server.stop();
|
|
119
|
+
restoreAuth();
|
|
120
|
+
if (originalPlatform === undefined) {
|
|
121
|
+
delete process.env.IS_PLATFORM;
|
|
122
|
+
} else {
|
|
123
|
+
process.env.IS_PLATFORM = originalPlatform;
|
|
124
|
+
}
|
|
125
|
+
if (originalContainerized === undefined) {
|
|
126
|
+
delete process.env.IS_CONTAINERIZED;
|
|
127
|
+
} else {
|
|
128
|
+
process.env.IS_CONTAINERIZED = originalContainerized;
|
|
129
|
+
}
|
|
130
|
+
setOverridesForTesting({});
|
|
131
|
+
}
|
|
132
|
+
});
|
|
@@ -32,12 +32,12 @@ import {
|
|
|
32
32
|
} from "../daemon/daemon-readiness.js";
|
|
33
33
|
import { processMessage } from "../daemon/process-message.js";
|
|
34
34
|
import { makeAddrInUseError } from "../daemon/startup-error.js";
|
|
35
|
-
import { isAssistantDesktopEnabled } from "../desktop/desktop-feature.js";
|
|
36
35
|
import {
|
|
37
36
|
DESKTOP_CLOSE,
|
|
38
37
|
destroyDesktopSessionManager,
|
|
39
38
|
} from "../desktop/desktop-session-manager.js";
|
|
40
39
|
import { DesktopStreamBridge } from "../desktop/desktop-stream-bridge.js";
|
|
40
|
+
import { isVirtualDesktopEnabled } from "../desktop/virtual-desktop-feature.js";
|
|
41
41
|
import {
|
|
42
42
|
createLiveVoiceConnection,
|
|
43
43
|
type LiveVoiceConnection,
|
|
@@ -252,9 +252,9 @@ type AllWebSocketData =
|
|
|
252
252
|
| WatchStreamWebSocketData
|
|
253
253
|
| DesktopStreamWebSocketData;
|
|
254
254
|
|
|
255
|
-
function
|
|
255
|
+
function virtualDesktopEnabled(): boolean {
|
|
256
256
|
try {
|
|
257
|
-
return
|
|
257
|
+
return isVirtualDesktopEnabled(getConfig());
|
|
258
258
|
} catch (err) {
|
|
259
259
|
log.warn({ err }, "Failed to read config for desktop stream gate");
|
|
260
260
|
return false;
|
|
@@ -453,15 +453,15 @@ export class RuntimeHttpServer {
|
|
|
453
453
|
}
|
|
454
454
|
if (data.wsType === "desktop-stream") {
|
|
455
455
|
log.info("Desktop stream WebSocket opened");
|
|
456
|
-
if (!
|
|
456
|
+
if (!virtualDesktopEnabled()) {
|
|
457
457
|
ws.close(
|
|
458
458
|
DESKTOP_CLOSE.unavailable,
|
|
459
|
-
"
|
|
459
|
+
"Virtual desktop is available only on enabled platform-hosted assistants",
|
|
460
460
|
);
|
|
461
461
|
return;
|
|
462
462
|
}
|
|
463
463
|
const bridge = new DesktopStreamBridge(ws, {
|
|
464
|
-
isEnabled:
|
|
464
|
+
isEnabled: virtualDesktopEnabled,
|
|
465
465
|
});
|
|
466
466
|
data.bridge = bridge;
|
|
467
467
|
void bridge.start();
|
|
@@ -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");
|
|
@@ -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
|
|