feat: add bsk network read-only command

Add `bsk network` as a sibling of the `console` command from #7,
reading the buffered network responses / failures for a tab. Fills the
remaining half of the request in #2 (console landed in #7; network did
not).

Mirrors the console command's conventions exactly:

  bsk network --session <id> [--tab-id N] [--since C]
                             [--limit N] [--max-text-chars N]

The extension enables `Network` on attach (best-effort) and buffers
`responseReceived` / `loadingFailed` events per tab with a monotonic
sequence, correlating them to `requestWillBeSent` for method/URL. Reads
are cursor-paginated (`since` -> `next_since`) and bounded (`limit`,
`max_text_chars`) for agent-context safety, same as console. Entries
carry the real HTTP status code (response) or the CDP failure reason
(failure) — things `performance.getEntriesByType('resource')` can't
give.

Classified read-only (pass-through under the pending-interrupt gate);
defaults to the Agent Window's active tab.

Wires bsk-protocol (Method + params/result/entry structs + schema),
bsk-cli (network subcommand + daemon dispatch allow-list) and the
extension (handler + dispatcher route), with unit / parse / IPC tests
paralleling the console command.

Refs #2.
This commit is contained in:
hjxccc
2026-07-18 22:37:57 +08:00
committed by BB-fat
parent 3d6c95c22a
commit 259b4826b7
19 changed files with 1156 additions and 3 deletions
+35
View File
@@ -104,6 +104,41 @@ fn rejects_zero_console_bounds() {
);
}
#[test]
fn parses_network_command_with_context_safety_flags() {
let cli = parse(&[
"bsk",
"network",
"--session",
"s1",
"--tab-id",
"9",
"--since",
"12",
"--limit",
"75",
"--max-text-chars",
"2048",
]);
let Command::Network(args) = cli.command else {
panic!("expected network command");
};
assert_eq!(args.session, "s1");
assert_eq!(args.tab_id, Some(9));
assert_eq!(args.since, Some(12));
assert_eq!(args.limit, Some(75));
assert_eq!(args.max_text_chars, Some(2048));
}
#[test]
fn rejects_zero_network_bounds() {
assert!(Cli::try_parse_from(["bsk", "network", "--session", "s1", "--limit", "0"]).is_err());
assert!(
Cli::try_parse_from(["bsk", "network", "--session", "s1", "--max-text-chars", "0"])
.is_err()
);
}
#[test]
fn parses_install_skill_subcommand() {
let cli = parse(&["bsk", "install-skill", "--list"]);
+59 -2
View File
@@ -11,8 +11,9 @@ use bsk::ipc_client::IpcClient;
use bsk_protocol::system::{HandshakeParams, HandshakeResult};
use bsk_protocol::tools::{
ConsoleEntry, ConsoleEntryKind, ConsoleParams, ConsoleResult, GetHtmlParams, GetHtmlResult,
ScreenshotParams, ScreenshotResult, SessionStartParams, SessionStartResult, SnapshotParams,
SnapshotResult, TabInfo, TabListParams, TabListResult, TabScope,
NetworkEntry, NetworkEntryKind, NetworkParams, NetworkResult, ScreenshotParams,
ScreenshotResult, SessionStartParams, SessionStartResult, SnapshotParams, SnapshotResult,
TabInfo, TabListParams, TabListResult, TabScope,
};
use bsk_protocol::{
BrowserPeerInfo, ErrorCode, Frame, Method, RequestFrame, ResponseBody, ResponseFrame, RpcError,
@@ -365,6 +366,62 @@ async fn console_returns_buffered_entries() {
handle.shutdown().await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn network_returns_buffered_entries() {
let (handle, sock) = spawn_daemon().await;
let mut ws = connect_ext(handle.ws_addr()).await;
let _ = do_handshake(&mut ws).await;
run_extension(ws, |req| {
assert_eq!(req.method, Method::ToolNetwork);
let params: NetworkParams = serde_json::from_value(req.params.clone().unwrap()).unwrap();
assert_eq!(params.tab_id, Some(7));
assert_eq!(params.since, Some(3));
assert_eq!(params.limit, Some(50));
assert_eq!(params.max_text_chars, Some(1000));
ResponseBody::Ok(
serde_json::to_value(NetworkResult {
tab_id: 7,
entries: vec![NetworkEntry {
sequence: 4,
kind: NetworkEntryKind::Response,
method: Some("GET".into()),
url: "https://example.test/api".into(),
status: Some(404),
status_text: Some("Not Found".into()),
mime_type: Some("application/json".into()),
resource_type: Some("Fetch".into()),
error_text: None,
timestamp: None,
truncated: false,
}],
next_since: 4,
truncated: false,
})
.unwrap(),
)
});
let session_id = ipc_session_start(&sock).await;
let result: NetworkResult = ipc_tool_call(
&sock,
Method::ToolNetwork,
NetworkParams {
session_id,
tab_id: Some(7),
since: Some(3),
limit: Some(50),
max_text_chars: Some(1000),
},
)
.await
.expect("network ok");
assert_eq!(result.tab_id, 7);
assert_eq!(result.next_since, 4);
assert_eq!(result.entries[0].status, Some(404));
handle.shutdown().await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn snapshot_returns_text_and_ref_count() {
let (handle, sock) = spawn_daemon().await;