Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 8 additions & 4 deletions apps/extension/src/tools/__tests__/console.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,14 @@ describe("handleConsole", () => {
expect(deps.consoleEntriesSince).toHaveBeenCalledWith(7, undefined, 50, 1000, false);
});

it("reads an explicit tab and forwards bounded options", async () => {
it("rejects an explicit user tab before touching CDP", async () => {
const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) });
await sm.start("aa11");
const deps = makeDeps({
get: vi.fn(async () => ({ id: 9, windowId: 200, active: true }) as chrome.tabs.Tab),
});

await handleConsole(
const res = await handleConsole(
sm,
{
session_id: "aa11",
Expand All @@ -92,8 +92,12 @@ describe("handleConsole", () => {
deps,
);

expect(deps.ensureConsoleCapture).toHaveBeenCalledWith(9);
expect(deps.consoleEntriesSince).toHaveBeenCalledWith(9, 12, 200, 4096, true);
expect(res).toMatchObject({
code: "permission_denied",
data: { reason: "agent_window_scope" },
});
expect(deps.ensureConsoleCapture).not.toHaveBeenCalled();
expect(deps.consoleEntriesSince).not.toHaveBeenCalled();
});

it("rejects invalid bounds before touching CDP", async () => {
Expand Down
10 changes: 6 additions & 4 deletions apps/extension/src/tools/__tests__/observation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ describe("handleScreenshot", () => {
expect(res).toMatchObject({ code: "not_found" });
});

it("captures an explicit active user tab in its real window", async () => {
it("rejects an explicit active user tab until it is borrowed", async () => {
const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) });
await sm.start("aa11");
const capture = vi.fn(async (_w: number) => `data:image/png;base64,${TINY_PNG}`);
Expand All @@ -136,9 +136,11 @@ describe("handleScreenshot", () => {
{ session_id: "aa11", tab_id: 9 },
makeScreenshotDeps({ captureVisibleTab: capture, get, query: vi.fn() }),
);
if ("code" in res) throw new Error(`unexpected error: ${JSON.stringify(res)}`);
expect(res.tab_id).toBe(9);
expect(capture).toHaveBeenCalledWith(200, { format: "png" });
expect(res).toMatchObject({
code: "permission_denied",
data: { reason: "agent_window_scope" },
});
expect(capture).not.toHaveBeenCalled();
});

it("rejects screenshots for inactive explicit tabs", async () => {
Expand Down
31 changes: 30 additions & 1 deletion apps/extension/src/tools/__tests__/shared.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it, vi } from "vitest";
import { SessionManager } from "@/session-manager/manager";
import { lookupSession } from "../shared";
import { lookupSession, resolveTargetTab } from "../shared";

function fakeAgentWindow() {
return {
Expand Down Expand Up @@ -40,3 +40,32 @@ describe("lookupSession", () => {
expect(result).toBe(ctx);
});
});

describe("resolveTargetTab", () => {
it("requires a user tab to be borrowed into the Agent Window before access", async () => {
const sm = new SessionManager({ agentWindow: fakeAgentWindow() });
const ctx = await sm.start("aa11");

const result = await resolveTargetTab(sm, ctx, 9, {
get: vi.fn(async () => ({ id: 9, windowId: 200, active: true }) as chrome.tabs.Tab),
query: vi.fn(),
});

expect(result).toMatchObject({
code: "permission_denied",
data: { reason: "agent_window_scope" },
});
});

it("allows an explicit tab in the current session's Agent Window", async () => {
const sm = new SessionManager({ agentWindow: fakeAgentWindow() });
const ctx = await sm.start("aa11");

await expect(
resolveTargetTab(sm, ctx, 9, {
get: vi.fn(async () => ({ id: 9, windowId: 100, active: true }) as chrome.tabs.Tab),
query: vi.fn(),
}),
).resolves.toEqual({ tabId: 9, windowId: 100, active: true });
});
});
14 changes: 10 additions & 4 deletions apps/extension/src/tools/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,9 @@ export function lookupSession(
}

/**
* Resolve the target tab for a tool call. Explicit `tabId` values are
* checked against the session visibility rules: user tabs and the
* current session's Agent Window are visible, other sessions' Agent
* Windows are not.
* Resolve the target tab for a tool call. Explicit `tabId` values must
* belong to the current session's Agent Window. A user-owned tab becomes
* eligible only after `tab_borrow` moves it into that window.
*
* Returns the resolved `{tabId, windowId, active}` triple, or an
* `RpcError` the caller propagates verbatim.
Expand Down Expand Up @@ -125,6 +124,13 @@ export async function resolveTargetTab(
message: `tab ${tabId} not found in session scope`,
};
}
if (tab.windowId !== ctx.agentWindowId) {
return rpcError(
"permission_denied",
"agent_window_scope",
`tab ${tabId} is outside the Agent Window; borrow it before reading or interacting with it`,
);
}
return { tabId: tab.id, windowId: tab.windowId, active: tab.active === true };
}
const tabs = await api.query({ active: true, windowId: ctx.agentWindowId });
Expand Down
4 changes: 1 addition & 3 deletions apps/extension/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@ export default defineConfig({
},
define: {
__BSK_EXT_VERSION__: JSON.stringify(pkg.version),
__BSK_DAEMON_WS_URL__: JSON.stringify(
process.env.BSK_DAEMON_WS_URL ?? "ws://127.0.0.1:52800",
),
__BSK_DAEMON_WS_URL__: JSON.stringify(process.env.BSK_DAEMON_WS_URL ?? "ws://127.0.0.1:52800"),
},
resolve: {
alias: {
Expand Down
5 changes: 1 addition & 4 deletions crates/bsk-cli/tests/browser_list_ordering.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ use bsk_protocol::system::BrowserStatusEntry;
use bsk_protocol::{ErrorCode, Method};
use rand::Rng;
use serde::Deserialize;
use tokio::net::TcpListener;
use tokio::sync::mpsc;

fn tempfile_path(prefix: &str) -> PathBuf {
Expand All @@ -30,9 +29,7 @@ fn tempfile_path(prefix: &str) -> PathBuf {
}

async fn spawn_daemon() -> (daemon::DaemonHandle, PathBuf) {
let probe = TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = probe.local_addr().unwrap().port();
drop(probe);
let port = 0;

let config = DaemonConfig::new(port);
let sock = tempfile_path("bsk-test-browser-list");
Expand Down
5 changes: 1 addition & 4 deletions crates/bsk-cli/tests/browser_wait.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ use bsk_protocol::system::{BrowserListParams, HandshakeParams, HandshakeResult,
use bsk_protocol::{BrowserPeerInfo, Method, RequestFrame, ResponseBody, ResponseFrame};
use futures_util::{SinkExt, StreamExt};
use rand::Rng;
use tokio::net::TcpListener;
use tokio_tungstenite::tungstenite::handshake::client::generate_key;
use tokio_tungstenite::tungstenite::http::Request;
use tokio_tungstenite::tungstenite::protocol::Message;
Expand Down Expand Up @@ -77,9 +76,7 @@ fn tempfile_path(prefix: &str) -> PathBuf {
}

async fn spawn_daemon() -> (daemon::DaemonHandle, PathBuf) {
let probe = TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = probe.local_addr().unwrap().port();
drop(probe);
let port = 0;

let config = DaemonConfig::new(port);
let sock = tempfile_path("bsk-test-browser-wait");
Expand Down
5 changes: 1 addition & 4 deletions crates/bsk-cli/tests/cancel_forwarding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ use bsk_protocol::{
use futures_util::{SinkExt, StreamExt};
use rand::Rng;
use serde_json::json;
use tokio::net::TcpListener;
use tokio_tungstenite::tungstenite::handshake::client::generate_key;
use tokio_tungstenite::tungstenite::http::Request;
use tokio_tungstenite::tungstenite::protocol::Message;
Expand All @@ -40,9 +39,7 @@ fn tempfile_path(prefix: &str) -> PathBuf {
}

async fn spawn_daemon() -> (daemon::DaemonHandle, PathBuf) {
let probe = TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = probe.local_addr().unwrap().port();
drop(probe);
let port = 0;

let config = DaemonConfig::new(port);
let sock = tempfile_path("bsk-test-cancel");
Expand Down
5 changes: 1 addition & 4 deletions crates/bsk-cli/tests/handshake_compat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ use bsk_protocol::system::{HandshakeParams, HandshakeResult, StatusResult};
use bsk_protocol::{BrowserPeerInfo, ErrorCode, Method, RequestFrame, ResponseBody, ResponseFrame};
use futures_util::{SinkExt, StreamExt};
use rand::Rng;
use tokio::net::TcpListener;
use tokio_tungstenite::tungstenite::handshake::client::generate_key;
use tokio_tungstenite::tungstenite::http::Request;
use tokio_tungstenite::tungstenite::protocol::Message;
Expand All @@ -28,9 +27,7 @@ fn tempfile_path(prefix: &str) -> PathBuf {
}

async fn spawn_daemon() -> (daemon::DaemonHandle, PathBuf) {
let probe = TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = probe.local_addr().unwrap().port();
drop(probe);
let port = 0;

let config = DaemonConfig::new(port);
let sock = tempfile_path("bsk-test-handshake");
Expand Down
5 changes: 1 addition & 4 deletions crates/bsk-cli/tests/per_session_queue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ use futures_util::{SinkExt, StreamExt};
use rand::Rng;
use serde::Deserialize;
use serde_json::json;
use tokio::net::TcpListener;
use tokio::sync::{Mutex, mpsc};
use tokio_tungstenite::tungstenite::handshake::client::generate_key;
use tokio_tungstenite::tungstenite::http::Request;
Expand All @@ -46,9 +45,7 @@ fn tempfile_path(prefix: &str) -> PathBuf {
}

async fn spawn_daemon() -> (daemon::DaemonHandle, PathBuf) {
let probe = TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = probe.local_addr().unwrap().port();
drop(probe);
let port = 0;
let sock = tempfile_path("bsk-test-queue");
let handle = daemon::run(DaemonConfig::new(port), Some(sock.clone()))
.await
Expand Down
5 changes: 1 addition & 4 deletions crates/bsk-cli/tests/session_user_interrupt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ use bsk_protocol::{
use futures_util::{SinkExt, StreamExt};
use rand::Rng;
use serde_json::json;
use tokio::net::TcpListener;
use tokio_tungstenite::tungstenite::handshake::client::generate_key;
use tokio_tungstenite::tungstenite::http::Request;
use tokio_tungstenite::tungstenite::protocol::Message;
Expand All @@ -46,9 +45,7 @@ fn tempfile_path(prefix: &str) -> PathBuf {
}

async fn spawn_daemon() -> (daemon::DaemonHandle, PathBuf) {
let probe = TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = probe.local_addr().unwrap().port();
drop(probe);
let port = 0;

let config = DaemonConfig::new(port);
let sock = tempfile_path("bsk-test-user-interrupt");
Expand Down
5 changes: 1 addition & 4 deletions crates/bsk-cli/tests/sessions_ipc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ use bsk_protocol::tools::{SessionStartParams, SessionStartResult, SessionStopPar
use bsk_protocol::{BrowserPeerInfo, Frame, Method, RequestFrame, ResponseBody, ResponseFrame};
use futures_util::{SinkExt, StreamExt};
use rand::Rng;
use tokio::net::TcpListener;
use tokio_tungstenite::tungstenite::handshake::client::generate_key;
use tokio_tungstenite::tungstenite::http::Request;
use tokio_tungstenite::tungstenite::protocol::Message;
Expand All @@ -39,9 +38,7 @@ async fn spawn_daemon() -> (daemon::DaemonHandle, PathBuf) {
}

async fn spawn_daemon_with_connect_wait(connect_wait: Duration) -> (daemon::DaemonHandle, PathBuf) {
let probe = TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = probe.local_addr().unwrap().port();
drop(probe);
let port = 0;

let config = DaemonConfig::new(port).with_extension_connect_wait(connect_wait);
let sock = tempfile_path("bsk-test-ipc");
Expand Down
5 changes: 1 addition & 4 deletions crates/bsk-cli/tests/tools_ipc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ use futures_util::stream::{SplitSink, SplitStream};
use futures_util::{SinkExt, StreamExt};
use rand::Rng;
use serde_json::{Value, json};
use tokio::net::TcpListener;
use tokio::sync::Mutex;
use tokio_tungstenite::tungstenite::handshake::client::generate_key;
use tokio_tungstenite::tungstenite::http::Request;
Expand All @@ -40,9 +39,7 @@ fn tempfile_path(prefix: &str) -> PathBuf {
}

async fn spawn_daemon() -> (daemon::DaemonHandle, PathBuf) {
let probe = TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = probe.local_addr().unwrap().port();
drop(probe);
let port = 0;
let sock = tempfile_path("bsk-test-tools");
let handle = daemon::run(DaemonConfig::new(port), Some(sock.clone()))
.await
Expand Down
5 changes: 1 addition & 4 deletions crates/bsk-cli/tests/tools_m7_ipc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ use futures_util::stream::{SplitSink, SplitStream};
use futures_util::{SinkExt, StreamExt};
use rand::Rng;
use serde_json::{Value, json};
use tokio::net::TcpListener;
use tokio::sync::Mutex;
use tokio_tungstenite::tungstenite::handshake::client::generate_key;
use tokio_tungstenite::tungstenite::http::Request;
Expand All @@ -44,9 +43,7 @@ fn tempfile_path(prefix: &str) -> PathBuf {
}

async fn spawn_daemon() -> (daemon::DaemonHandle, PathBuf) {
let probe = TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = probe.local_addr().unwrap().port();
drop(probe);
let port = 0;
let sock = tempfile_path("bsk-test-tools-m7");
let handle = daemon::run(DaemonConfig::new(port), Some(sock.clone()))
.await
Expand Down
5 changes: 1 addition & 4 deletions crates/bsk-cli/tests/tools_m8_ipc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ use futures_util::stream::{SplitSink, SplitStream};
use futures_util::{SinkExt, StreamExt};
use rand::Rng;
use serde_json::Value;
use tokio::net::TcpListener;
use tokio::sync::Mutex;
use tokio_tungstenite::tungstenite::handshake::client::generate_key;
use tokio_tungstenite::tungstenite::http::Request;
Expand All @@ -43,9 +42,7 @@ fn tempfile_path(prefix: &str) -> PathBuf {
}

async fn spawn_daemon() -> (daemon::DaemonHandle, PathBuf) {
let probe = TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = probe.local_addr().unwrap().port();
drop(probe);
let port = 0;
let sock = tempfile_path("bsk-test-tools-m8");
let handle = daemon::run(DaemonConfig::new(port), Some(sock.clone()))
.await
Expand Down
5 changes: 1 addition & 4 deletions crates/bsk-cli/tests/tools_m9_ipc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ use rand::Rng;
use serde_json::{Value, json};
#[cfg(unix)]
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::TcpListener;
#[cfg(unix)]
use tokio::net::UnixStream;
use tokio::sync::Mutex;
Expand All @@ -53,9 +52,7 @@ fn tempfile_path(prefix: &str) -> PathBuf {
}

async fn spawn_daemon() -> (daemon::DaemonHandle, PathBuf) {
let probe = TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = probe.local_addr().unwrap().port();
drop(probe);
let port = 0;
let sock = tempfile_path("bsk-test-tools-m9");
let handle = daemon::run(DaemonConfig::new(port), Some(sock.clone()))
.await
Expand Down
5 changes: 1 addition & 4 deletions crates/bsk-cli/tests/ws_handshake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ use bsk::daemon::{self, DaemonConfig};
use bsk_protocol::system::{HandshakeParams, HandshakeResult};
use bsk_protocol::{BrowserPeerInfo, Method, RequestFrame, ResponseBody, ResponseFrame};
use futures_util::{SinkExt, StreamExt};
use tokio::net::TcpListener;
use tokio_tungstenite::tungstenite::handshake::client::generate_key;
use tokio_tungstenite::tungstenite::http::Request;
use tokio_tungstenite::tungstenite::protocol::Message;
Expand All @@ -17,9 +16,7 @@ pub const TEST_EXT_ID: &str = "abcdefghijklmnopabcdefghijklmnop"; // 32 chars in

pub async fn spawn_daemon() -> daemon::DaemonHandle {
// Bind to any free TCP port.
let probe = TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = probe.local_addr().unwrap().port();
drop(probe);
let port = 0;

let config = DaemonConfig::new(port);
daemon::run(config, None).await.unwrap()
Expand Down
Loading