Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
73cd393
chore: regenerate openapi.yaml for version 0.6.2 bump
noanflaherty Apr 7, 2026
b77fb8e
fix(daemon): backport host-browser-proxy defensive guards to host-bas…
noanflaherty Apr 7, 2026
54be472
docs(browser): document chrome.debugger infobar decision (#24106)
noanflaherty Apr 7, 2026
258b74c
feat(clients/macos): decode host_browser_request and host_browser_can…
noanflaherty Apr 7, 2026
46fb3bd
feat(browser-session): add BrowserSessionManager scaffold with extens…
noanflaherty Apr 7, 2026
d12f2f9
feat(chrome-extension): add standalone CDP proxy module (#24112)
noanflaherty Apr 7, 2026
404798b
feat(chrome-extension-native-host): add native messaging helper scaff…
noanflaherty Apr 7, 2026
f951484
feat(chrome-extension): add cloud OAuth sign-in skeleton (#24117)
noanflaherty Apr 7, 2026
a288491
feat(channels): add chrome-extension interface id and per-capability …
noanflaherty Apr 7, 2026
f8fd76b
feat(runtime): route host_browser_request to connected chrome-extensi…
noanflaherty Apr 7, 2026
f023c90
feat(chrome-extension): dispatch host_browser_request frames via CDP …
noanflaherty Apr 7, 2026
b602eaa
feat(runtime): add /v1/browser-extension-pair capability token endpoi…
noanflaherty Apr 7, 2026
e27c423
feat(installer): write Chrome native messaging host manifest on macOS…
noanflaherty Apr 7, 2026
52cfdaf
feat(chrome-extension): bootstrap self-hosted capability token via na…
noanflaherty Apr 7, 2026
20e19b9
feat(chrome-extension): connect to cloud gateway browser-relay WebSoc…
noanflaherty Apr 7, 2026
00eb2ba
test(host-browser): e2e smoke test for self-hosted native-messaging c…
noanflaherty Apr 7, 2026
1f9190f
test(host-browser): e2e smoke test for cloud-hosted host_browser_requ…
noanflaherty Apr 7, 2026
9801b23
test(cdp-proxy): add unit tests and fix sync targetToDebuggee throw (…
noanflaherty Apr 8, 2026
2f1a0ad
fix(chrome-extension): evict attached-target cache on CDP send failur…
noanflaherty Apr 8, 2026
0664ec3
test(host-browser-e2e): rewrite header and convert test.skip to test.…
noanflaherty Apr 8, 2026
52dd154
test(host-bash-proxy): use bun:test fake timers for timeout regressio…
noanflaherty Apr 8, 2026
be9c866
fix(chrome-extension): popup pairing reply + relay-aware host_browser…
noanflaherty Apr 8, 2026
660afc5
fix(chrome-extension-native-host): halt unauthorized origins and forw…
noanflaherty Apr 8, 2026
8470237
fix(daemon): gate host tools by per-capability supportsHostProxy (#24…
noanflaherty Apr 8, 2026
cfe5004
chore(chrome-extension): typecheck worker.ts + popup.ts and use "assi…
noanflaherty Apr 8, 2026
c80a76a
fix(chrome-extension): popup connect handler honors selected relay mo…
noanflaherty Apr 8, 2026
ad74419
chore(chrome-extension): extend bun:test ambient shim with common sym…
noanflaherty Apr 8, 2026
e30f200
fix(daemon): preserve host_browser for chrome-extension in per-capabi…
noanflaherty Apr 8, 2026
a8b850e
fix(chrome-extension): read live relay mode per request + defensive w…
noanflaherty Apr 8, 2026
c971ac0
chore(chrome-extension): remove stale cdp-proxy declarations and outd…
noanflaherty Apr 8, 2026
98b776f
chore(chrome-extension-native-host): split writeFrameAndExit + rewrit…
noanflaherty Apr 8, 2026
0cf30e8
chore(chrome-extension): tighten bun:test shim so only test.todo has …
noanflaherty Apr 8, 2026
c4f0aa3
chore(daemon): rewrite host-tool gating test comment in forward-looki…
noanflaherty Apr 8, 2026
9728b44
chore(chrome-extension): dedupe RelayConnection.mode accessor (keep g…
noanflaherty Apr 8, 2026
9996168
fix(chrome-extension): worker reads live relay mode from storage on c…
noanflaherty Apr 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion assistant/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
openapi: 3.0.0
info:
title: Vellum Assistant API
version: 0.6.1
version: 0.6.2
description: Auto-generated OpenAPI specification for the Vellum Assistant runtime HTTP server.
servers:
- url: http://127.0.0.1:7821
Expand Down
155 changes: 155 additions & 0 deletions assistant/src/__tests__/conversation-confirmation-signals.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,8 @@ mock.module("../memory/canonical-guardian-store.js", () => ({
// ---------------------------------------------------------------------------

import { Conversation } from "../daemon/conversation.js";
import { HostBashProxy } from "../daemon/host-bash-proxy.js";
import { HostBrowserProxy } from "../daemon/host-browser-proxy.js";

// ---------------------------------------------------------------------------
// Helpers
Expand Down Expand Up @@ -558,3 +560,156 @@ describe("sendToClient receives state signals", () => {
});
});
});

describe("restoreBrowserProxyAvailability", () => {
test("re-enables only the host browser proxy after clearProxyAvailability", () => {
const conversation = makeConversation();
const browserProxy = new HostBrowserProxy(() => {});
const bashProxy = new HostBashProxy(() => {});
conversation.setHostBrowserProxy(browserProxy);
conversation.setHostBashProxy(bashProxy);

// Mark as having a connected client (interactive desktop path).
conversation.updateClient(() => {}, false);
expect(browserProxy.isAvailable()).toBe(true);
expect(bashProxy.isAvailable()).toBe(true);

// The drain queue clears all proxies for non-interactive turns.
conversation.clearProxyAvailability();
expect(browserProxy.isAvailable()).toBe(false);
expect(bashProxy.isAvailable()).toBe(false);

// restoreBrowserProxyAvailability should bring back ONLY the browser proxy.
conversation.restoreBrowserProxyAvailability();
expect(browserProxy.isAvailable()).toBe(true);
expect(bashProxy.isAvailable()).toBe(false);
});

test("re-enables the browser proxy even when hasNoClient is true (chrome-extension)", () => {
// Regression: chrome-extension is non-interactive (hasNoClient stays
// true so host_bash/host_file tools remain gated), but we still need
// to provision the hostBrowserProxy so it can service CDP commands.
// The helper must NOT gate on hasNoClient.
const conversation = makeConversation();
const browserProxy = new HostBrowserProxy(() => {});
conversation.setHostBrowserProxy(browserProxy);

// updateClient with hasNoClient=true emulates the non-interactive
// chrome-extension turn. Host proxies start disabled because
// updateClient propagates hasNoClient through to updateSender.
conversation.updateClient(() => {}, true);
expect(browserProxy.isAvailable()).toBe(false);
expect(conversation["hasNoClient"]).toBe(true);

// The targeted helper bypasses the hasNoClient gate so the
// single-capability chrome-extension turn can drive the browser
// via CDP without flipping hasNoClient (which would also enable
// host_bash/host_file gating downstream).
conversation.restoreBrowserProxyAvailability();
expect(browserProxy.isAvailable()).toBe(true);
// hasNoClient itself MUST remain true so that
// isToolActiveForContext keeps host_bash/host_file/host_cu gated.
expect(conversation["hasNoClient"]).toBe(true);
});

test("leaves bash/file/cu proxies disabled when called for chrome-extension", () => {
// Regression: the targeted helper must not accidentally re-enable
// proxies other than host_browser, even when called from a path that
// owns multiple proxies (e.g. macOS holdover state with hasNoClient
// forced true for an explicit non-interactive run).
const conversation = makeConversation();
const browserProxy = new HostBrowserProxy(() => {});
const bashProxy = new HostBashProxy(() => {});
conversation.setHostBrowserProxy(browserProxy);
conversation.setHostBashProxy(bashProxy);

conversation.updateClient(() => {}, true);
expect(browserProxy.isAvailable()).toBe(false);
expect(bashProxy.isAvailable()).toBe(false);

conversation.restoreBrowserProxyAvailability();
expect(browserProxy.isAvailable()).toBe(true);
// Crucial: bash proxy stays disabled. The helper must touch ONLY the
// browser proxy.
expect(bashProxy.isAvailable()).toBe(false);
});

test("uses hostBrowserSenderOverride when set so drain-queue restores preserve the registry-routed sender", () => {
// Regression (PR #24129 cycle 2): the queue-drain path calls
// `restoreBrowserProxyAvailability()` on dequeue, which used to pass
// `this.sendToClient` (the SSE hub emitter) to the proxy, clobbering the
// chrome-extension registry-routed sender established by the POST
// /messages handler. The override field lets the HTTP handler pin the
// registry-routed sender so the drain path preserves it.
const sseHub: ServerMessage[] = [];
const registry: ServerMessage[] = [];
const conversation = makeConversation((msg) => sseHub.push(msg));
const browserProxy = new HostBrowserProxy(() => {});
conversation.setHostBrowserProxy(browserProxy);

// Simulate updateClient setting sendToClient to the SSE hub and
// marking the conversation as client-less (chrome-extension is
// non-interactive).
conversation.updateClient((msg) => sseHub.push(msg), true);
expect(browserProxy.isAvailable()).toBe(false);

// The HTTP handler stashes the registry-routed sender as the override.
const registrySender = (msg: ServerMessage) => registry.push(msg);
conversation.hostBrowserSenderOverride = registrySender;

// Drain-queue path calls restoreBrowserProxyAvailability — it must now
// prefer the override over sendToClient.
conversation.restoreBrowserProxyAvailability();
expect(browserProxy.isAvailable()).toBe(true);

// Send a frame through the proxy and verify it flows through the
// registry sender, not the SSE hub.
const internalSend = (
browserProxy as unknown as {
sendToClient: (msg: ServerMessage) => void;
}
).sendToClient;
const probe: ServerMessage = {
type: "host_browser_cancel",
requestId: "probe-1",
} as ServerMessage;
internalSend(probe);
expect(registry).toHaveLength(1);
expect(sseHub.some((m) => m === probe)).toBe(false);
});

test("falls back to sendToClient when hostBrowserSenderOverride is cleared", () => {
// When a non-chrome-extension turn takes over, the HTTP handler clears
// the override and restoreBrowserProxyAvailability must fall back to
// sendToClient (the SSE hub), otherwise macOS turns would route their
// host_browser frames through the stale chrome-extension registry.
const sseHub: ServerMessage[] = [];
const conversation = makeConversation((msg) => sseHub.push(msg));
const browserProxy = new HostBrowserProxy(() => {});
conversation.setHostBrowserProxy(browserProxy);

// First the chrome-extension path pins the override.
const registry: ServerMessage[] = [];
conversation.hostBrowserSenderOverride = (msg) => registry.push(msg);
conversation.updateClient((msg) => sseHub.push(msg), true);
conversation.restoreBrowserProxyAvailability();

// Then a macOS handoff clears the override.
conversation.hostBrowserSenderOverride = undefined;
conversation.updateClient((msg) => sseHub.push(msg), false);
conversation.restoreBrowserProxyAvailability();

const internalSend = (
browserProxy as unknown as {
sendToClient: (msg: ServerMessage) => void;
}
).sendToClient;
const probe: ServerMessage = {
type: "host_browser_cancel",
requestId: "probe-2",
} as ServerMessage;
internalSend(probe);
expect(sseHub).toContain(probe);
expect(registry).not.toContain(probe);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ function createFakeConversation(conversationId: string): Conversation {
setHostCuProxy(this: { hostCuProxy: unknown }, proxy: unknown) {
this.hostCuProxy = proxy;
},
restoreBrowserProxyAvailability: () => {},
addPreactivatedSkillId: () => {},
hasAnyPendingConfirmation: () => false,
hasPendingConfirmation: () => false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ describe("handleSendMessage canonical guardian reply interception", () => {
setHostBrowserProxy: () => {},
setHostFileProxy: () => {},
setHostCuProxy: () => {},
restoreBrowserProxyAvailability: () => {},
addPreactivatedSkillId: () => {},
} as unknown as import("../daemon/conversation.js").Conversation;

Expand Down Expand Up @@ -251,6 +252,7 @@ describe("handleSendMessage canonical guardian reply interception", () => {
setHostBrowserProxy: () => {},
setHostFileProxy: () => {},
setHostCuProxy: () => {},
restoreBrowserProxyAvailability: () => {},
addPreactivatedSkillId: () => {},
} as unknown as import("../daemon/conversation.js").Conversation;

Expand Down Expand Up @@ -325,6 +327,7 @@ describe("handleSendMessage canonical guardian reply interception", () => {
setHostBrowserProxy: () => {},
setHostFileProxy: () => {},
setHostCuProxy: () => {},
restoreBrowserProxyAvailability: () => {},
addPreactivatedSkillId: () => {},
} as unknown as import("../daemon/conversation.js").Conversation;

Expand Down Expand Up @@ -403,6 +406,7 @@ describe("handleSendMessage canonical guardian reply interception", () => {
setHostBrowserProxy: () => {},
setHostFileProxy: () => {},
setHostCuProxy: () => {},
restoreBrowserProxyAvailability: () => {},
addPreactivatedSkillId: () => {},
} as unknown as import("../daemon/conversation.js").Conversation;

Expand Down Expand Up @@ -477,6 +481,7 @@ describe("handleSendMessage canonical guardian reply interception", () => {
setHostBrowserProxy: () => {},
setHostFileProxy: () => {},
setHostCuProxy: () => {},
restoreBrowserProxyAvailability: () => {},
addPreactivatedSkillId: () => {},
} as unknown as import("../daemon/conversation.js").Conversation;

Expand Down Expand Up @@ -545,6 +550,7 @@ describe("handleSendMessage canonical guardian reply interception", () => {
setHostBrowserProxy: () => {},
setHostFileProxy: () => {},
setHostCuProxy: () => {},
restoreBrowserProxyAvailability: () => {},
addPreactivatedSkillId: () => {},
} as unknown as import("../daemon/conversation.js").Conversation;

Expand Down Expand Up @@ -615,6 +621,7 @@ describe("handleSendMessage canonical guardian reply interception", () => {
setHostBrowserProxy: () => {},
setHostFileProxy: () => {},
setHostCuProxy: () => {},
restoreBrowserProxyAvailability: () => {},
addPreactivatedSkillId: () => {},
} as unknown as import("../daemon/conversation.js").Conversation;

Expand Down Expand Up @@ -686,6 +693,7 @@ describe("handleSendMessage canonical guardian reply interception", () => {
setHostBrowserProxy: () => {},
setHostFileProxy: () => {},
setHostCuProxy: () => {},
restoreBrowserProxyAvailability: () => {},
addPreactivatedSkillId: () => {},
} as unknown as import("../daemon/conversation.js").Conversation;

Expand Down
Loading
Loading