@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
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Virtual desktop browser CLI
|
|
2
|
+
|
|
3
|
+
`assistant browser --virtual-desktop` uses the browser CLI's shared operation handlers against the Chrome process owned by `DesktopSessionManager`. It requires a platform-hosted container, the `assistant-desktop` flag, completed automatic desktop installation and an identified guardian conversation. Availability is checked before starting or reusing control and after asynchronous startup; loss of the flag or installation readiness cancels active control. The first eligible browser command waits for the shared installation job, starts Chrome, and executes the original action exactly once. Cancellation and feature revocation stop the pending action while shared installation can finish for other viewers. Browser CLI requests allow ten minutes, with an eight-minute setup wait limit. Eligible web conversations receive browser guidance in the bash tool description, including the configured shell timeout; native clients retain their existing tool description. Opening the picture-in-picture panel also starts setup and shows progress automatically; failed installs retain a retry action. The persistent profile remains `data/desktop-profile`; no data migration or extension installation is required.
|
|
4
|
+
|
|
5
|
+
Automatic browser selection in platform-hosted web conversations uses the streamed Chrome when the desktop flag is enabled and the actor is an identified guardian. The macOS and Windows apps keep their existing browser selection and fallback behavior; they use the streamed desktop only with explicit `--virtual-desktop`. Their shared renderer reports a `web` transport, so selection also reads the active turn's frozen `clientOs`; it does not change transport identity or host capability checks. Explicit `--virtual-desktop`, `--browser-mode`, `--target-client-id` and active-tab requests override the default. Existing personal-browser sessions and tab pins remain on their selected browser. Tab commands use the same streamed-browser default as page commands. `--browser-mode local` selects assistant-side Playwright, not the user's Chrome.
|
|
6
|
+
|
|
7
|
+
The legacy `--desktop` option remains an alias. The deployment flag key `assistant-desktop`, API paths, sync tags and persistent profile path are unchanged for compatibility. The shared `isVirtualDesktopEnabled` gate rejects self-hosted assistants, including local Docker, for setup, streaming and control.
|
|
8
|
+
|
|
9
|
+
```mermaid
|
|
10
|
+
flowchart LR
|
|
11
|
+
CLI[assistant browser --virtual-desktop] --> IPC[Existing browser IPC routes]
|
|
12
|
+
IPC --> Lease[DesktopAutomationLease conversation and actor lease]
|
|
13
|
+
Lease --> Shared[Shared browser operation handlers]
|
|
14
|
+
Shared --> CDP[Scoped desktop CDP client]
|
|
15
|
+
CDP --> Chrome[Managed Chrome on display :99]
|
|
16
|
+
Chrome --> Stream[Existing desktop stream]
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Chrome and its dock launcher share the managed profile and loopback debug port. For Chrome reopened from the dock, the profile singleton lock identifies a candidate PID, validated against the installed executable, profile and loopback debug arguments. Discovery checks `/proc` socket ownership against that browser PID, refuses redirects and validates the returned browser WebSocket endpoint. The connection stays within the container. No CDP endpoint or token is exposed to the renderer.
|
|
20
|
+
|
|
21
|
+
The desktop client bypasses personal-browser discovery, extension reconnect waits and backend fallback. It reuses the existing AX snapshot, DOM element resolution, mouse, keyboard, extraction and credential-fill implementations. Operation-scoped clients borrow the lease's connection; disposing one does not release the lease. `detach`, `close`, cancellation, errors and idle expiry release control. Only `tabs close` closes a Chrome tab.
|
|
22
|
+
|
|
23
|
+
`assistant browser --virtual-desktop screenshot --output /tmp/desktop-page.jpg` captures a color page image through CDP `Page.captureScreenshot`. It excludes the browser toolbar and desktop dock. `snapshot` returns semantic page structure. Agents should use these interfaces and let the manager start the desktop, including when older memory notes describe manual Xvnc, `xdotool` or scratch XWD conversion scripts.
|
|
24
|
+
|
|
25
|
+
Tab IDs are ephemeral numeric aliases for this managed browser's CDP target IDs. They are never personal extension tab IDs. Initial attachment selects an existing HTTP(S) page or opens a blank tab. Use `tabs list` and `tabs select --tab-id <id>` to choose explicitly. Navigation, tab selection, document replacement and release clear the desktop snapshot map. Personal-browser snapshots use a separate namespace.
|
|
26
|
+
|
|
27
|
+
CDP mouse events use page viewport CSS coordinates. Before dispatching them, the client animates a purple arrow overlay to the same point. The overlay is excluded from accessibility and hit testing. It appears in the existing desktop stream and is removed on release. It does not move the OS pointer, appear in browser toolbar UI or visualize every programmatic DOM operation. Native dialogs and other applications are outside the browser CLI; users can interact with them directly in the expanded desktop. The picture-in-picture preview is view-only. Direct user interaction does not pause automation.
|
|
28
|
+
|
|
29
|
+
The client records key and mouse presses before dispatch. On release it opens a fresh bounded cleanup connection, attaches to the same live targets, releases uncertain held input and removes overlays. A failed cleanup preserves state and the automation slot for retry. Dispatched actions are never automatically retried. Closed targets need no input cleanup. Browser-process loss disposes the client, and later requests discover the replacement process.
|
|
30
|
+
|
|
31
|
+
`--use-active-tab` and personal browser targeting are rejected with `--virtual-desktop`. Download waiting is unsupported. Browser operations are bounded to two minutes and share the desktop lease's action budget and idle expiry.
|
|
32
|
+
|
|
33
|
+
Validation: focused client tests exercise shared snapshot/click behavior, namespace isolation, stale references, target changes, cancellation and uncertain-input cleanup. Lease tests cover browser ownership, cancellation and cleanup independently of native input. The Linux smoke script exercises real Chrome, the CLI and visible pointer feedback.
|
|
@@ -113,7 +113,7 @@ export async function buildSlackUserLabelMap(
|
|
|
113
113
|
// that label directly; only queue a lookup when the label is missing,
|
|
114
114
|
// empty, or ID-shaped. Mirrors the channel builder below.
|
|
115
115
|
const [, embeddedLabel] = splitSlackLabel(match[0].slice(1, -1));
|
|
116
|
-
const sanitizedEmbeddedLabel =
|
|
116
|
+
const sanitizedEmbeddedLabel = sanitizeEmbeddedSlackLabel(embeddedLabel);
|
|
117
117
|
if (sanitizedEmbeddedLabel && sanitizedEmbeddedLabel !== id) continue;
|
|
118
118
|
if (seen.has(id)) continue;
|
|
119
119
|
seen.add(id);
|
|
@@ -151,7 +151,7 @@ export async function buildSlackChannelLabelMap(
|
|
|
151
151
|
for (const match of text.matchAll(SLACK_CHANNEL_REFERENCE_RE)) {
|
|
152
152
|
const id = match[1];
|
|
153
153
|
const [, embeddedLabel] = splitSlackLabel(match[0].slice(1, -1));
|
|
154
|
-
const sanitizedEmbeddedLabel =
|
|
154
|
+
const sanitizedEmbeddedLabel = sanitizeEmbeddedSlackLabel(embeddedLabel);
|
|
155
155
|
if (sanitizedEmbeddedLabel && sanitizedEmbeddedLabel !== id) continue;
|
|
156
156
|
if (!seen.has(id)) {
|
|
157
157
|
seen.add(id);
|
|
@@ -192,9 +192,7 @@ function renderUserMention(
|
|
|
192
192
|
// prefer it over a lookup, mirroring renderChannelReference. The embedded
|
|
193
193
|
// label is Slack-sourced (part of the token), so entities decode before
|
|
194
194
|
// sanitization; the caller-resolved label below is not.
|
|
195
|
-
const embeddedLabel =
|
|
196
|
-
label === undefined ? undefined : decodeSlackHtmlEntities(label),
|
|
197
|
-
);
|
|
195
|
+
const embeddedLabel = sanitizeEmbeddedSlackLabel(label);
|
|
198
196
|
if (embeddedLabel && embeddedLabel !== id) {
|
|
199
197
|
return `@${embeddedLabel}`;
|
|
200
198
|
}
|
|
@@ -219,9 +217,7 @@ function renderChannelReference(
|
|
|
219
217
|
);
|
|
220
218
|
// The embedded label is Slack-sourced (part of the token), so entities
|
|
221
219
|
// decode before sanitization; the caller-resolved label below is not.
|
|
222
|
-
const embeddedLabel =
|
|
223
|
-
label === undefined ? undefined : decodeSlackHtmlEntities(label),
|
|
224
|
-
);
|
|
220
|
+
const embeddedLabel = sanitizeEmbeddedSlackLabel(label);
|
|
225
221
|
if (embeddedLabel && embeddedLabel !== channelId) {
|
|
226
222
|
return `#${embeddedLabel}`;
|
|
227
223
|
}
|
|
@@ -320,6 +316,15 @@ export function sanitizeSlackLabel(
|
|
|
320
316
|
return sanitized || undefined;
|
|
321
317
|
}
|
|
322
318
|
|
|
319
|
+
// Slack-sourced labels are decoded before validation in both lookup and render.
|
|
320
|
+
function sanitizeEmbeddedSlackLabel(
|
|
321
|
+
label: string | undefined,
|
|
322
|
+
): string | undefined {
|
|
323
|
+
return sanitizeOptionalLabel(
|
|
324
|
+
label === undefined ? undefined : decodeSlackHtmlEntities(label),
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
323
328
|
function sanitizeOptionalLabel(label: string | undefined): string | undefined {
|
|
324
329
|
return sanitizeSlackLabel(label);
|
|
325
330
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, test } from "bun:test";
|
|
3
|
+
import {
|
|
4
|
+
buildSlackChannelLabelMap,
|
|
5
|
+
buildSlackUserLabelMap,
|
|
6
|
+
renderSlackTextForModel,
|
|
7
|
+
} from "./index.js";
|
|
8
|
+
|
|
9
|
+
describe("Slack embedded-label lookup parity", () => {
|
|
10
|
+
const cases = [
|
|
11
|
+
{ id: "U123", prefix: "@", build: buildSlackUserLabelMap, mapKey: "userLabels" },
|
|
12
|
+
{
|
|
13
|
+
id: "C123",
|
|
14
|
+
prefix: "#",
|
|
15
|
+
build: buildSlackChannelLabelMap,
|
|
16
|
+
mapKey: "channelLabels",
|
|
17
|
+
},
|
|
18
|
+
] as const;
|
|
19
|
+
|
|
20
|
+
for (const { id, prefix, build, mapKey } of cases) {
|
|
21
|
+
for (const label of ["<>", `<${id}>`, "< @# >"]) {
|
|
22
|
+
test(`${prefix} resolves label ${label} after Slack entity decoding`, async () => {
|
|
23
|
+
const text = `<${prefix}${id}|${label}>`;
|
|
24
|
+
const requested: string[] = [];
|
|
25
|
+
const labels = await build([text], async (value) => {
|
|
26
|
+
requested.push(value);
|
|
27
|
+
return "Example Name";
|
|
28
|
+
});
|
|
29
|
+
assert.deepEqual(requested, [id]);
|
|
30
|
+
assert.equal(
|
|
31
|
+
renderSlackTextForModel(text, { [mapKey]: labels }),
|
|
32
|
+
`${prefix}Example Name`,
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for (const label of ["<Example>", "R&D", "&lt;&gt;"]) {
|
|
38
|
+
test(`${prefix} keeps usable embedded label ${label} without lookup`, async () => {
|
|
39
|
+
const requested: string[] = [];
|
|
40
|
+
const text = `<${prefix}${id}|${label}>`;
|
|
41
|
+
const labels = await build([text], async (value) => {
|
|
42
|
+
requested.push(value);
|
|
43
|
+
return "Wrong Label";
|
|
44
|
+
});
|
|
45
|
+
assert.deepEqual(requested, []);
|
|
46
|
+
assert.deepEqual(labels, {});
|
|
47
|
+
const expected =
|
|
48
|
+
label === "<Example>"
|
|
49
|
+
? "Example"
|
|
50
|
+
: label === "R&D"
|
|
51
|
+
? "R&D"
|
|
52
|
+
: "<>";
|
|
53
|
+
assert.equal(
|
|
54
|
+
renderSlackTextForModel(text, { [mapKey]: labels }),
|
|
55
|
+
`${prefix}${expected}`,
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
test(`${prefix} deduplicates required lookups across encoded and bare mentions`, async () => {
|
|
61
|
+
const requested: string[] = [];
|
|
62
|
+
const labels = await build(
|
|
63
|
+
[`<${prefix}${id}|<>>`, `<${prefix}${id}>`],
|
|
64
|
+
async (value) => {
|
|
65
|
+
requested.push(value);
|
|
66
|
+
return "Example";
|
|
67
|
+
},
|
|
68
|
+
);
|
|
69
|
+
assert.deepEqual(requested, [id]);
|
|
70
|
+
assert.deepEqual(labels, { [id]: "Example" });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test(`${prefix} does not decode caller-resolved entity text`, async () => {
|
|
74
|
+
const text = `<${prefix}${id}|<>>`;
|
|
75
|
+
const labels = await build([text], async () => "<Example>");
|
|
76
|
+
assert.equal(
|
|
77
|
+
renderSlackTextForModel(text, { [mapKey]: labels }),
|
|
78
|
+
`${prefix}<Example>`,
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test(`${prefix} preserves unknown fallback after a failed required lookup`, async () => {
|
|
83
|
+
let attempts = 0;
|
|
84
|
+
const text = `<${prefix}${id}|<>>`;
|
|
85
|
+
const labels = await build([text], async () => {
|
|
86
|
+
attempts++;
|
|
87
|
+
throw new Error("unavailable");
|
|
88
|
+
});
|
|
89
|
+
assert.equal(attempts, 1);
|
|
90
|
+
assert.deepEqual(labels, {});
|
|
91
|
+
const expected = prefix === "@" ? "@unknown-user" : "#unknown-channel";
|
|
92
|
+
assert.equal(renderSlackTextForModel(text, { [mapKey]: labels }), expected);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
});
|
package/openapi.yaml
CHANGED
|
@@ -4146,6 +4146,8 @@ paths:
|
|
|
4146
4146
|
conversationId:
|
|
4147
4147
|
type: string
|
|
4148
4148
|
minLength: 1
|
|
4149
|
+
desktop:
|
|
4150
|
+
type: boolean
|
|
4149
4151
|
required:
|
|
4150
4152
|
- operation
|
|
4151
4153
|
responses:
|
|
@@ -4181,7 +4183,7 @@ paths:
|
|
|
4181
4183
|
post:
|
|
4182
4184
|
operationId: browser_tabs_post
|
|
4183
4185
|
summary: Manage browser tabs
|
|
4184
|
-
description: List, create, select, or close browser tabs
|
|
4186
|
+
description: List, create, select, or close browser tabs in the Chrome extension or streamed desktop browser.
|
|
4185
4187
|
tags:
|
|
4186
4188
|
- browser
|
|
4187
4189
|
requestBody:
|
|
@@ -4205,6 +4207,10 @@ paths:
|
|
|
4205
4207
|
conversationId:
|
|
4206
4208
|
type: string
|
|
4207
4209
|
minLength: 1
|
|
4210
|
+
desktop:
|
|
4211
|
+
type: boolean
|
|
4212
|
+
browserMode:
|
|
4213
|
+
type: string
|
|
4208
4214
|
tabId:
|
|
4209
4215
|
type: number
|
|
4210
4216
|
url:
|
|
@@ -11916,7 +11922,7 @@ paths:
|
|
|
11916
11922
|
/v1/desktop/setup:
|
|
11917
11923
|
get:
|
|
11918
11924
|
operationId: desktop_setup_get
|
|
11919
|
-
summary: Get desktop setup status
|
|
11925
|
+
summary: Get virtual desktop setup status
|
|
11920
11926
|
tags:
|
|
11921
11927
|
- desktop
|
|
11922
11928
|
responses:
|
|
@@ -11946,7 +11952,7 @@ paths:
|
|
|
11946
11952
|
additionalProperties: false
|
|
11947
11953
|
post:
|
|
11948
11954
|
operationId: desktop_setup_post
|
|
11949
|
-
summary: Install desktop components
|
|
11955
|
+
summary: Install virtual desktop components
|
|
11950
11956
|
tags:
|
|
11951
11957
|
- desktop
|
|
11952
11958
|
responses:
|
|
@@ -18426,7 +18432,7 @@ paths:
|
|
|
18426
18432
|
get:
|
|
18427
18433
|
operationId: internal_mcp_list_get
|
|
18428
18434
|
summary: List MCP servers with health status
|
|
18429
|
-
description: Returns configured MCP servers with
|
|
18435
|
+
description: Returns configured MCP servers with recorded runtime status without opening new connections.
|
|
18430
18436
|
tags:
|
|
18431
18437
|
- internal
|
|
18432
18438
|
responses:
|
|
@@ -24481,6 +24487,22 @@ paths:
|
|
|
24481
24487
|
anyOf:
|
|
24482
24488
|
- type: string
|
|
24483
24489
|
- type: "null"
|
|
24490
|
+
tenant_host:
|
|
24491
|
+
anyOf:
|
|
24492
|
+
- type: object
|
|
24493
|
+
properties:
|
|
24494
|
+
pattern:
|
|
24495
|
+
type: string
|
|
24496
|
+
label:
|
|
24497
|
+
type: string
|
|
24498
|
+
placeholder:
|
|
24499
|
+
type: string
|
|
24500
|
+
required:
|
|
24501
|
+
- pattern
|
|
24502
|
+
- label
|
|
24503
|
+
- placeholder
|
|
24504
|
+
additionalProperties: false
|
|
24505
|
+
- type: "null"
|
|
24484
24506
|
acts_as:
|
|
24485
24507
|
type: string
|
|
24486
24508
|
enum:
|
|
@@ -24497,6 +24519,7 @@ paths:
|
|
|
24497
24519
|
- supports_managed_mode
|
|
24498
24520
|
- managed_service_is_paid
|
|
24499
24521
|
- feature_flag
|
|
24522
|
+
- tenant_host
|
|
24500
24523
|
- acts_as
|
|
24501
24524
|
additionalProperties: false
|
|
24502
24525
|
- type: "null"
|
|
@@ -25015,6 +25038,22 @@ paths:
|
|
|
25015
25038
|
anyOf:
|
|
25016
25039
|
- type: string
|
|
25017
25040
|
- type: "null"
|
|
25041
|
+
tenant_host:
|
|
25042
|
+
anyOf:
|
|
25043
|
+
- type: object
|
|
25044
|
+
properties:
|
|
25045
|
+
pattern:
|
|
25046
|
+
type: string
|
|
25047
|
+
label:
|
|
25048
|
+
type: string
|
|
25049
|
+
placeholder:
|
|
25050
|
+
type: string
|
|
25051
|
+
required:
|
|
25052
|
+
- pattern
|
|
25053
|
+
- label
|
|
25054
|
+
- placeholder
|
|
25055
|
+
additionalProperties: false
|
|
25056
|
+
- type: "null"
|
|
25018
25057
|
acts_as:
|
|
25019
25058
|
type: string
|
|
25020
25059
|
enum:
|
|
@@ -25031,6 +25070,7 @@ paths:
|
|
|
25031
25070
|
- supports_managed_mode
|
|
25032
25071
|
- managed_service_is_paid
|
|
25033
25072
|
- feature_flag
|
|
25073
|
+
- tenant_host
|
|
25034
25074
|
- acts_as
|
|
25035
25075
|
additionalProperties: false
|
|
25036
25076
|
required:
|
|
@@ -26510,6 +26550,12 @@ paths:
|
|
|
26510
26550
|
target:
|
|
26511
26551
|
type: string
|
|
26512
26552
|
description: Absolute path that was removed on the assistant host. Useful for audit logs and confirmation toasts.
|
|
26553
|
+
warnings:
|
|
26554
|
+
description: Stable keys for non-fatal cleanup limitations that should be localized at the presentation edge.
|
|
26555
|
+
type: array
|
|
26556
|
+
items:
|
|
26557
|
+
type: string
|
|
26558
|
+
const: plugin.uninstall.mcp_oauth_credentials_unchecked
|
|
26513
26559
|
required:
|
|
26514
26560
|
- name
|
|
26515
26561
|
- target
|
package/package.json
CHANGED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdir, mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import { createServer } from "node:net";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
|
|
7
|
+
import { IpcFrameReader, writeMessage } from "@vellumai/ipc-server-utils";
|
|
8
|
+
import { Command } from "commander";
|
|
9
|
+
import sharp from "sharp";
|
|
10
|
+
|
|
11
|
+
import { executeBrowserOperation } from "../src/browser/operations.js";
|
|
12
|
+
import type { BrowserOperation } from "../src/browser/types.js";
|
|
13
|
+
import { registerBrowserCommand } from "../src/cli/commands/browser.js";
|
|
14
|
+
import { DesktopAutomationLease } from "../src/desktop/desktop-automation-lease.js";
|
|
15
|
+
import {
|
|
16
|
+
desktopChromePath,
|
|
17
|
+
desktopDependencyInstaller,
|
|
18
|
+
} from "../src/desktop/desktop-dependencies.js";
|
|
19
|
+
import { DesktopSessionManager } from "../src/desktop/desktop-session-manager.js";
|
|
20
|
+
import { getAssistantSocketPath } from "../src/ipc/socket-path.js";
|
|
21
|
+
|
|
22
|
+
if (
|
|
23
|
+
process.platform !== "linux" ||
|
|
24
|
+
!process.argv[2] ||
|
|
25
|
+
!process.env.ASSISTANT_IPC_SOCKET_DIR
|
|
26
|
+
) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
"Run in disposable Linux with desktop packages, a temporary ASSISTANT_IPC_SOCKET_DIR and a Chrome executable argument or --install for a cold install.",
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
const coldInstall = process.argv[2] === "--install";
|
|
32
|
+
const executable = coldInstall ? desktopChromePath() : process.argv[2];
|
|
33
|
+
if (coldInstall) {
|
|
34
|
+
assert.equal(desktopDependencyInstaller.getStatus().state, "required");
|
|
35
|
+
assert.equal(Bun.which("Xtigervnc"), null);
|
|
36
|
+
}
|
|
37
|
+
const directory = await mkdtemp(join(tmpdir(), "desktop-browser-cli-"));
|
|
38
|
+
const manager = new DesktopSessionManager({
|
|
39
|
+
resolveChromePath: async () => executable,
|
|
40
|
+
profileDir: join(directory, "profile"),
|
|
41
|
+
panelConfigDir: join(directory, "panel"),
|
|
42
|
+
renderWallpaper: async () => null,
|
|
43
|
+
});
|
|
44
|
+
const control = new DesktopAutomationLease({
|
|
45
|
+
enabled: () => true,
|
|
46
|
+
ready: () =>
|
|
47
|
+
!coldInstall || desktopDependencyInstaller.getStatus().state === "ready",
|
|
48
|
+
ensureReady: (signal) => desktopDependencyInstaller.ensureReady(signal),
|
|
49
|
+
manager: () => manager,
|
|
50
|
+
});
|
|
51
|
+
const context = {
|
|
52
|
+
workingDir: directory,
|
|
53
|
+
conversationId: "conv-smoke",
|
|
54
|
+
sourceActorPrincipalId: "user-123",
|
|
55
|
+
trustClass: "guardian" as const,
|
|
56
|
+
};
|
|
57
|
+
let submissions = 0;
|
|
58
|
+
let navigations = 0;
|
|
59
|
+
const page = Bun.serve({
|
|
60
|
+
hostname: "127.0.0.1",
|
|
61
|
+
port: 0,
|
|
62
|
+
fetch(request) {
|
|
63
|
+
if (new URL(request.url).pathname === "/save") {
|
|
64
|
+
submissions++;
|
|
65
|
+
return Response.json({ ok: true });
|
|
66
|
+
}
|
|
67
|
+
if (new URL(request.url).pathname === "/") {
|
|
68
|
+
navigations++;
|
|
69
|
+
}
|
|
70
|
+
return new Response(
|
|
71
|
+
`<!doctype html><html><head><title>Desktop browser CLI</title><style>body{font:24px sans-serif;padding:70px;background:#f3f4f8}input,button{font:inherit;padding:12px;margin:16px}#result{color:#5140bd}#colors{position:fixed;left:0;top:0;display:flex}#colors span{width:100px;height:50px}</style></head><body><div id="colors"><span style="background:#ff0000"></span><span style="background:#00ff00"></span><span style="background:#0000ff"></span></div><h1>Streamed desktop browser</h1><label>Example text<input id="text"></label><button id="save" onclick="fetch('/save');document.getElementById('result').textContent='Saved '+document.getElementById('text').value">Save</button><p id="result"></p><button id="open-dialog" onclick="document.querySelector('dialog').showModal()">Open dialog</button><dialog style="background:white"><button id="inside" onclick="this.textContent='Modal clicked'">Inside dialog</button></dialog></body></html>`,
|
|
72
|
+
{
|
|
73
|
+
headers: {
|
|
74
|
+
"content-type": "text/html",
|
|
75
|
+
"content-security-policy":
|
|
76
|
+
"require-trusted-types-for 'script'; trusted-types 'none'",
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
);
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
await mkdir(process.env.ASSISTANT_IPC_SOCKET_DIR, { recursive: true });
|
|
83
|
+
let setupNotifications = 0;
|
|
84
|
+
const ipc = createServer((socket) => {
|
|
85
|
+
const reader = new IpcFrameReader((request) => {
|
|
86
|
+
void (async () => {
|
|
87
|
+
try {
|
|
88
|
+
if (request.method === "/events/publish") {
|
|
89
|
+
setupNotifications++;
|
|
90
|
+
writeMessage(socket, { id: request.id, result: { ok: true } });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const body = (
|
|
94
|
+
request.params as {
|
|
95
|
+
body: {
|
|
96
|
+
operation: BrowserOperation;
|
|
97
|
+
input: Record<string, unknown>;
|
|
98
|
+
desktop: boolean;
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
).body;
|
|
102
|
+
assert.equal(request.method, "browser_execute");
|
|
103
|
+
assert.equal(body.desktop, true);
|
|
104
|
+
const result = await control.runBrowser(
|
|
105
|
+
context,
|
|
106
|
+
async (signal) => {
|
|
107
|
+
const cdpClient = await manager.browser.client(
|
|
108
|
+
context.conversationId,
|
|
109
|
+
signal,
|
|
110
|
+
);
|
|
111
|
+
return executeBrowserOperation(body.operation, body.input, {
|
|
112
|
+
...context,
|
|
113
|
+
signal,
|
|
114
|
+
cdpClient,
|
|
115
|
+
});
|
|
116
|
+
},
|
|
117
|
+
body.operation === "detach",
|
|
118
|
+
);
|
|
119
|
+
const screenshots = (result.contentBlocks ?? []).flatMap((block) =>
|
|
120
|
+
block.type === "image" && block.source.type === "base64"
|
|
121
|
+
? [{ mediaType: block.source.media_type, data: block.source.data }]
|
|
122
|
+
: [],
|
|
123
|
+
);
|
|
124
|
+
writeMessage(socket, {
|
|
125
|
+
id: request.id,
|
|
126
|
+
result: { ...result, screenshots },
|
|
127
|
+
});
|
|
128
|
+
} catch (error) {
|
|
129
|
+
writeMessage(socket, { id: request.id, error: String(error) });
|
|
130
|
+
}
|
|
131
|
+
})();
|
|
132
|
+
});
|
|
133
|
+
socket.on("data", (chunk) => reader.push(chunk));
|
|
134
|
+
});
|
|
135
|
+
await new Promise<void>((resolve, reject) => {
|
|
136
|
+
ipc.once("error", reject);
|
|
137
|
+
ipc.listen(getAssistantSocketPath(), resolve);
|
|
138
|
+
});
|
|
139
|
+
async function cli(...args: string[]) {
|
|
140
|
+
const program = new Command();
|
|
141
|
+
program.exitOverride();
|
|
142
|
+
registerBrowserCommand(program);
|
|
143
|
+
const chunks: string[] = [];
|
|
144
|
+
const original = process.stdout.write;
|
|
145
|
+
process.stdout.write = ((chunk: unknown) => {
|
|
146
|
+
chunks.push(String(chunk));
|
|
147
|
+
return true;
|
|
148
|
+
}) as typeof original;
|
|
149
|
+
try {
|
|
150
|
+
await program.parseAsync([
|
|
151
|
+
"bun",
|
|
152
|
+
"assistant",
|
|
153
|
+
"browser",
|
|
154
|
+
"--virtual-desktop",
|
|
155
|
+
"--json",
|
|
156
|
+
...args,
|
|
157
|
+
]);
|
|
158
|
+
} finally {
|
|
159
|
+
process.stdout.write = original;
|
|
160
|
+
}
|
|
161
|
+
const result = JSON.parse(chunks.join("")) as {
|
|
162
|
+
ok: boolean;
|
|
163
|
+
content: string;
|
|
164
|
+
error?: string;
|
|
165
|
+
};
|
|
166
|
+
assert.equal(result.ok, true, JSON.stringify(result));
|
|
167
|
+
return result.content;
|
|
168
|
+
}
|
|
169
|
+
function ref(snapshot: string, name: string): string {
|
|
170
|
+
const line = snapshot
|
|
171
|
+
.split("\n")
|
|
172
|
+
.find((value) => value.includes(name) && /\be\d+\b/.test(value));
|
|
173
|
+
assert(line, `Missing ${name}: ${snapshot}`);
|
|
174
|
+
return /\be\d+\b/.exec(line)![0];
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
await cli(
|
|
178
|
+
"navigate",
|
|
179
|
+
"--url",
|
|
180
|
+
`http://127.0.0.1:${page.port}`,
|
|
181
|
+
"--allow-private-network",
|
|
182
|
+
);
|
|
183
|
+
assert.equal(navigations, 1);
|
|
184
|
+
if (coldInstall) {
|
|
185
|
+
assert.equal(desktopDependencyInstaller.getStatus().state, "ready");
|
|
186
|
+
assert(
|
|
187
|
+
setupNotifications >= 3,
|
|
188
|
+
"Installation progress must reach the viewer",
|
|
189
|
+
);
|
|
190
|
+
console.error(
|
|
191
|
+
"PASS: one CLI navigate installed desktop and Chrome from scratch, then loaded the requested page once",
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
const snapshot = await cli("snapshot");
|
|
195
|
+
await cli(
|
|
196
|
+
"type",
|
|
197
|
+
"--element-id",
|
|
198
|
+
ref(snapshot, "Example text"),
|
|
199
|
+
"--text",
|
|
200
|
+
"Hello café 世界",
|
|
201
|
+
"--clear-first",
|
|
202
|
+
);
|
|
203
|
+
await cli("click", "--element-id", ref(snapshot, "Save"));
|
|
204
|
+
const saved = await cli("extract");
|
|
205
|
+
assert.match(saved, /Saved Hello café 世界/);
|
|
206
|
+
assert.equal(submissions, 1);
|
|
207
|
+
const screenshotPath = join(tmpdir(), "desktop-browser-cli-page.jpg");
|
|
208
|
+
await cli("screenshot", "--output", screenshotPath);
|
|
209
|
+
const { data: pixels, info } = await sharp(screenshotPath)
|
|
210
|
+
.removeAlpha()
|
|
211
|
+
.raw()
|
|
212
|
+
.toBuffer({ resolveWithObject: true });
|
|
213
|
+
for (let channel = 0; channel < 3; channel++) {
|
|
214
|
+
const offset = (25 * info.width + 50 + channel * 100) * info.channels;
|
|
215
|
+
for (let component = 0; component < 3; component++) {
|
|
216
|
+
const expected = component === channel ? 255 : 0;
|
|
217
|
+
assert(
|
|
218
|
+
Math.abs(pixels[offset + component] - expected) < 10,
|
|
219
|
+
`Incorrect screenshot color for swatch ${channel}, channel ${component}`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
async function assertCursorPainted(expected: boolean) {
|
|
224
|
+
await control.runBrowser(context, async (signal) => {
|
|
225
|
+
const cdp = await manager.browser.client(context.conversationId, signal);
|
|
226
|
+
const screenshot = await cdp.send<{ data: string }>(
|
|
227
|
+
"Page.captureScreenshot",
|
|
228
|
+
{ format: "png" },
|
|
229
|
+
);
|
|
230
|
+
const { data, info } = await sharp(Buffer.from(screenshot.data, "base64"))
|
|
231
|
+
.removeAlpha()
|
|
232
|
+
.raw()
|
|
233
|
+
.toBuffer({ resolveWithObject: true });
|
|
234
|
+
let purple = 0;
|
|
235
|
+
for (let i = 0; i < data.length; i += info.channels) {
|
|
236
|
+
if (
|
|
237
|
+
Math.abs(data[i]! - 112) < 5 &&
|
|
238
|
+
Math.abs(data[i + 1]! - 87) < 5 &&
|
|
239
|
+
Math.abs(data[i + 2]! - 255) < 5
|
|
240
|
+
) {
|
|
241
|
+
purple++;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
assert.equal(purple > 40, expected, `Cursor pixels: ${purple}`);
|
|
245
|
+
return { content: "verified", isError: false };
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
await assertCursorPainted(true);
|
|
249
|
+
await cli("click", "--selector", "#open-dialog");
|
|
250
|
+
await assertCursorPainted(true);
|
|
251
|
+
await cli("hover", "--selector", "#inside");
|
|
252
|
+
await assertCursorPainted(true);
|
|
253
|
+
await cli("click", "--selector", "#inside");
|
|
254
|
+
assert.match(await cli("extract"), /Modal clicked/);
|
|
255
|
+
for (const key of ["Enter", "Space"]) {
|
|
256
|
+
await cli("press-key", "--key", "Escape");
|
|
257
|
+
await cli("press-key", "--key", key, "--selector", "#open-dialog");
|
|
258
|
+
await assertCursorPainted(true);
|
|
259
|
+
}
|
|
260
|
+
await cli(
|
|
261
|
+
"navigate",
|
|
262
|
+
"--url",
|
|
263
|
+
`http://127.0.0.1:${page.port}/next`,
|
|
264
|
+
"--allow-private-network",
|
|
265
|
+
);
|
|
266
|
+
await assertCursorPainted(true);
|
|
267
|
+
await cli("detach");
|
|
268
|
+
await assertCursorPainted(false);
|
|
269
|
+
await cli("detach");
|
|
270
|
+
console.log(
|
|
271
|
+
"PASS: real browser CLI over IPC, shared AX snapshot, Unicode typing, one click submission, RGB page screenshot, cursor pixels above dialogs and after navigation, detach cleanup",
|
|
272
|
+
);
|
|
273
|
+
} finally {
|
|
274
|
+
await cli("detach");
|
|
275
|
+
await manager.destroy();
|
|
276
|
+
await new Promise<void>((resolve) => ipc.close(() => resolve()));
|
|
277
|
+
page.stop(true);
|
|
278
|
+
await rm(directory, { recursive: true, force: true });
|
|
279
|
+
}
|
|
@@ -44,11 +44,11 @@ describe("always-loaded tool count", () => {
|
|
|
44
44
|
);
|
|
45
45
|
const activeNames = activeTools.map((t) => t.name).sort();
|
|
46
46
|
|
|
47
|
-
// `watch_retro_report` survives this baseline
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
47
|
+
// `watch_retro_report` survives this baseline because a watch
|
|
48
|
+
// retrospective runs clientless and can only report through a tool that
|
|
49
|
+
// stays on this baseline. The ui_* tools stay here because background
|
|
50
|
+
// surfaces persist and return instead of awaiting action, so a connected
|
|
51
|
+
// client is not required for the definitions to be present.
|
|
52
52
|
// Host tool definitions stay on the wire for background turns: schemas
|
|
53
53
|
// are cache-stable, and the shared execution gate rejects host tools on
|
|
54
54
|
// explicitly non-interactive turns before any proxy dispatch.
|
|
@@ -5,6 +5,7 @@ import { SYNC_TAGS } from "../daemon/message-types/sync.js";
|
|
|
5
5
|
import { assistantEventHub } from "../runtime/assistant-event-hub.js";
|
|
6
6
|
import {
|
|
7
7
|
publishConfigChanged,
|
|
8
|
+
publishMcpChanged,
|
|
8
9
|
publishSchedulesChanged,
|
|
9
10
|
publishSoundsConfigUpdated,
|
|
10
11
|
} from "../runtime/sync/resource-sync-events.js";
|
|
@@ -94,4 +95,26 @@ describe("config and sounds sync events", () => {
|
|
|
94
95
|
subscription.dispose();
|
|
95
96
|
}
|
|
96
97
|
});
|
|
98
|
+
|
|
99
|
+
test("MCP changes emit a sync tag without a client origin", async () => {
|
|
100
|
+
const received: AssistantEventEnvelope[] = [];
|
|
101
|
+
const subscription = assistantEventHub.subscribe({
|
|
102
|
+
type: "process",
|
|
103
|
+
callback: (event) => {
|
|
104
|
+
received.push(event);
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
publishMcpChanged();
|
|
110
|
+
await waitFor(() => received.length === 1);
|
|
111
|
+
|
|
112
|
+
expect(received[0].message).toEqual({
|
|
113
|
+
type: "sync_changed",
|
|
114
|
+
tags: [SYNC_TAGS.mcpList],
|
|
115
|
+
});
|
|
116
|
+
} finally {
|
|
117
|
+
subscription.dispose();
|
|
118
|
+
}
|
|
119
|
+
});
|
|
97
120
|
});
|