mirror of
https://github.com/Tencent/BrowserSkill.git
synced 2026-09-10 13:01:23 +08:00
342 lines
13 KiB
Rust
342 lines
13 KiB
Rust
//! Namespaced RPC methods (§4.3).
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// Observable browser-side effect class for a protocol method.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum MethodEffect {
|
|
/// Reads browser/session state without dispatching page input.
|
|
PassiveRead,
|
|
/// Dispatches temporary input such as hover probes. It must not submit,
|
|
/// navigate, or persist page state, but it can trigger page event handlers.
|
|
TransientInput,
|
|
/// Drives browser/page state such as clicking, filling, navigation, tabs,
|
|
/// or arbitrary page script.
|
|
BrowserMutation,
|
|
/// Control-plane/session lifecycle operation. These are deliberately not
|
|
/// gated by the pending browser-action interrupt path.
|
|
ControlPlane,
|
|
}
|
|
|
|
/// Namespaced method string (`system.handshake`, `tool.tab_list`, …).
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
pub enum Method {
|
|
#[serde(rename = "system.handshake")]
|
|
SystemHandshake,
|
|
#[serde(rename = "system.ping")]
|
|
SystemPing,
|
|
#[serde(rename = "system.status")]
|
|
SystemStatus,
|
|
|
|
#[serde(rename = "session.start")]
|
|
SessionStart,
|
|
#[serde(rename = "session.stop")]
|
|
SessionStop,
|
|
#[serde(rename = "session.stop_all")]
|
|
SessionStopAll,
|
|
#[serde(rename = "session.list")]
|
|
SessionList,
|
|
|
|
#[serde(rename = "browser.list")]
|
|
BrowserList,
|
|
|
|
#[serde(rename = "tool.session_start")]
|
|
ToolSessionStart,
|
|
#[serde(rename = "tool.session_stop")]
|
|
ToolSessionStop,
|
|
#[serde(rename = "tool.window_resize")]
|
|
ToolWindowResize,
|
|
#[serde(rename = "tool.emulate")]
|
|
ToolEmulate,
|
|
#[serde(rename = "tool.tab_list")]
|
|
ToolTabList,
|
|
#[serde(rename = "tool.tab_create")]
|
|
ToolTabCreate,
|
|
#[serde(rename = "tool.tab_close")]
|
|
ToolTabClose,
|
|
#[serde(rename = "tool.tab_borrow")]
|
|
ToolTabBorrow,
|
|
#[serde(rename = "tool.tab_return")]
|
|
ToolTabReturn,
|
|
#[serde(rename = "tool.tab_select")]
|
|
ToolTabSelect,
|
|
#[serde(rename = "tool.navigate")]
|
|
ToolNavigate,
|
|
#[serde(rename = "tool.navigate_back")]
|
|
ToolNavigateBack,
|
|
#[serde(rename = "tool.navigate_forward")]
|
|
ToolNavigateForward,
|
|
#[serde(rename = "tool.reload")]
|
|
ToolReload,
|
|
#[serde(rename = "tool.click")]
|
|
ToolClick,
|
|
#[serde(rename = "tool.hover")]
|
|
ToolHover,
|
|
#[serde(rename = "tool.fill")]
|
|
ToolFill,
|
|
#[serde(rename = "tool.press")]
|
|
ToolPress,
|
|
#[serde(rename = "tool.select")]
|
|
ToolSelect,
|
|
#[serde(rename = "tool.snapshot")]
|
|
ToolSnapshot,
|
|
#[serde(rename = "tool.observe")]
|
|
ToolObserve,
|
|
#[serde(rename = "tool.get_html")]
|
|
ToolGetHtml,
|
|
#[serde(rename = "tool.screenshot")]
|
|
ToolScreenshot,
|
|
#[serde(rename = "tool.console")]
|
|
ToolConsole,
|
|
#[serde(rename = "tool.network")]
|
|
ToolNetwork,
|
|
#[serde(rename = "tool.evaluate")]
|
|
ToolEvaluate,
|
|
#[serde(rename = "tool.wait_for_navigation")]
|
|
ToolWaitForNavigation,
|
|
#[serde(rename = "tool.wait_ms")]
|
|
ToolWaitMs,
|
|
#[serde(rename = "tool.request_help")]
|
|
ToolRequestHelp,
|
|
#[serde(rename = "tool.record_start")]
|
|
ToolRecordStart,
|
|
#[serde(rename = "tool.record_stop")]
|
|
ToolRecordStop,
|
|
#[serde(rename = "tool.record_await")]
|
|
ToolRecordAwait,
|
|
|
|
#[serde(rename = "cancel")]
|
|
Cancel,
|
|
}
|
|
|
|
impl Method {
|
|
/// Browser-side effect classification for this method.
|
|
///
|
|
/// Used by the daemon's pending-interrupt machinery: when the
|
|
/// user has clicked the agent-window mask's stop button, the
|
|
/// next tool call that dispatches browser/page input for that
|
|
/// session is rejected with `ErrorCode::UserAborted`. Passive reads
|
|
/// and control-plane RPCs pass through transparently.
|
|
///
|
|
/// **Compile-time enforcement.** The match below is exhaustive
|
|
/// (no `_ =>` fallthrough). Adding a new `Method` variant is a
|
|
/// compile error here, so classification cannot silently be
|
|
/// skipped — the author has to make a deliberate choice.
|
|
///
|
|
/// **Judgment calls** (read these before adding new variants):
|
|
///
|
|
/// * `tool.evaluate` is classified as browser-mutating because the
|
|
/// daemon cannot statically distinguish a `document.title`
|
|
/// read from a `form.submit()` write.
|
|
/// * `tool.observe` is classified as transient input: its bounded
|
|
/// hover probes do not commit browser state, but they dispatch real
|
|
/// page input events and therefore must be gated like automation.
|
|
/// * `tool.wait_*` are classified as read-only: they do not
|
|
/// initiate any browser action; they observe state only.
|
|
/// * `session.*` and `tool.session_*` are NOT gated. Blocking
|
|
/// `session.stop` would prevent the agent from gracefully
|
|
/// tearing down after observing the user's interrupt.
|
|
/// * `cancel` is NOT gated. It's a control-plane operation
|
|
/// (stops another in-flight RPC), not a browser action.
|
|
pub fn effect(&self) -> MethodEffect {
|
|
match self {
|
|
// Browser/page mutations — gated by pending-interrupt.
|
|
Method::ToolTabCreate
|
|
| Method::ToolTabClose
|
|
| Method::ToolTabBorrow
|
|
| Method::ToolTabReturn
|
|
| Method::ToolTabSelect
|
|
| Method::ToolWindowResize
|
|
| Method::ToolEmulate
|
|
| Method::ToolNavigate
|
|
| Method::ToolNavigateBack
|
|
| Method::ToolNavigateForward
|
|
| Method::ToolReload
|
|
| Method::ToolClick
|
|
| Method::ToolFill
|
|
| Method::ToolPress
|
|
| Method::ToolSelect
|
|
| Method::ToolEvaluate
|
|
// May navigate via optional `url` and changes Agent Window
|
|
// chrome; gate behind pending-interrupt like other writes.
|
|
| Method::ToolRecordStart => MethodEffect::BrowserMutation,
|
|
|
|
// Transient input — no committed browser action, but still page
|
|
// input. It must be stopped by pending user interrupts.
|
|
Method::ToolHover | Method::ToolObserve => MethodEffect::TransientInput,
|
|
|
|
// Passive reads — transparent.
|
|
// `record_stop` / `record_await` observe / finish a recording
|
|
// without driving new automation gestures, so they stay
|
|
// ungated (teardown after interrupt must still work).
|
|
Method::ToolTabList
|
|
| Method::ToolSnapshot
|
|
| Method::ToolGetHtml
|
|
| Method::ToolScreenshot
|
|
| Method::ToolConsole
|
|
| Method::ToolNetwork
|
|
| Method::ToolWaitForNavigation
|
|
| Method::ToolWaitMs
|
|
| Method::ToolRequestHelp
|
|
| Method::ToolRecordStop
|
|
| Method::ToolRecordAwait => MethodEffect::PassiveRead,
|
|
|
|
// Session lifecycle — not gated.
|
|
Method::SessionStart
|
|
| Method::SessionStop
|
|
| Method::SessionStopAll
|
|
| Method::SessionList
|
|
| Method::ToolSessionStart
|
|
| Method::ToolSessionStop => MethodEffect::ControlPlane,
|
|
|
|
// System / control — not gated.
|
|
Method::SystemHandshake
|
|
| Method::SystemPing
|
|
| Method::SystemStatus
|
|
| Method::BrowserList
|
|
| Method::Cancel => MethodEffect::ControlPlane,
|
|
}
|
|
}
|
|
|
|
/// Whether this RPC drives browser/page state in the traditional sense.
|
|
pub fn is_mutating(&self) -> bool {
|
|
self.effect() == MethodEffect::BrowserMutation
|
|
}
|
|
|
|
/// Whether a pending user interrupt should reject this method.
|
|
pub fn requires_interrupt_gate(&self) -> bool {
|
|
matches!(
|
|
self.effect(),
|
|
MethodEffect::TransientInput | MethodEffect::BrowserMutation
|
|
)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::{CancelParams, CancelResult};
|
|
use serde_json::json;
|
|
|
|
#[test]
|
|
fn cancel_method_round_trips() {
|
|
let method: Method = serde_json::from_value(json!("cancel")).unwrap();
|
|
assert_eq!(method, Method::Cancel);
|
|
assert_eq!(serde_json::to_value(method).unwrap(), json!("cancel"));
|
|
}
|
|
|
|
#[test]
|
|
fn console_method_round_trips() {
|
|
let method: Method = serde_json::from_value(json!("tool.console")).unwrap();
|
|
assert_eq!(method, Method::ToolConsole);
|
|
assert_eq!(serde_json::to_value(method).unwrap(), json!("tool.console"));
|
|
}
|
|
|
|
#[test]
|
|
fn network_method_round_trips() {
|
|
let method: Method = serde_json::from_value(json!("tool.network")).unwrap();
|
|
assert_eq!(method, Method::ToolNetwork);
|
|
assert_eq!(serde_json::to_value(method).unwrap(), json!("tool.network"));
|
|
}
|
|
|
|
#[test]
|
|
fn emulate_method_round_trips() {
|
|
let method: Method = serde_json::from_value(json!("tool.emulate")).unwrap();
|
|
assert_eq!(method, Method::ToolEmulate);
|
|
assert_eq!(serde_json::to_value(method).unwrap(), json!("tool.emulate"));
|
|
}
|
|
|
|
#[test]
|
|
fn cancel_params_and_result_round_trip() {
|
|
let params: CancelParams = serde_json::from_value(json!({ "rpc_id": "wait-1" })).unwrap();
|
|
assert_eq!(params.rpc_id, "wait-1");
|
|
let result = CancelResult { cancelled: true };
|
|
assert_eq!(
|
|
serde_json::to_value(result).unwrap(),
|
|
json!({ "cancelled": true })
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn is_mutating_classifies_read_only_tools_as_non_mutating() {
|
|
assert!(!Method::ToolTabList.is_mutating());
|
|
assert!(!Method::ToolSnapshot.is_mutating());
|
|
assert!(!Method::ToolHover.is_mutating());
|
|
assert!(!Method::ToolObserve.is_mutating());
|
|
assert!(!Method::ToolGetHtml.is_mutating());
|
|
assert!(!Method::ToolScreenshot.is_mutating());
|
|
assert!(!Method::ToolConsole.is_mutating());
|
|
assert!(!Method::ToolNetwork.is_mutating());
|
|
assert!(!Method::ToolWaitForNavigation.is_mutating());
|
|
assert!(!Method::ToolWaitMs.is_mutating());
|
|
}
|
|
|
|
#[test]
|
|
fn is_mutating_classifies_mutating_tools_as_mutating() {
|
|
assert!(Method::ToolTabCreate.is_mutating());
|
|
assert!(Method::ToolTabClose.is_mutating());
|
|
assert!(Method::ToolTabBorrow.is_mutating());
|
|
assert!(Method::ToolTabReturn.is_mutating());
|
|
assert!(Method::ToolTabSelect.is_mutating());
|
|
assert!(Method::ToolNavigate.is_mutating());
|
|
assert!(Method::ToolNavigateBack.is_mutating());
|
|
assert!(Method::ToolNavigateForward.is_mutating());
|
|
assert!(Method::ToolReload.is_mutating());
|
|
assert!(Method::ToolClick.is_mutating());
|
|
assert!(Method::ToolFill.is_mutating());
|
|
assert!(Method::ToolPress.is_mutating());
|
|
assert!(Method::ToolSelect.is_mutating());
|
|
assert!(Method::ToolEvaluate.is_mutating());
|
|
assert!(Method::ToolRecordStart.is_mutating());
|
|
assert!(Method::ToolWindowResize.is_mutating());
|
|
assert!(Method::ToolEmulate.is_mutating());
|
|
}
|
|
|
|
#[test]
|
|
fn is_mutating_classifies_record_stop_await_as_non_mutating() {
|
|
assert!(!Method::ToolRecordStop.is_mutating());
|
|
assert!(!Method::ToolRecordAwait.is_mutating());
|
|
}
|
|
|
|
#[test]
|
|
fn is_mutating_classifies_session_lifecycle_as_non_mutating() {
|
|
// Session lifecycle RPCs are not "mutating" for the purposes
|
|
// of pending-interrupt gating — gating them would prevent the
|
|
// agent from gracefully tearing down after observing the
|
|
// user's interrupt.
|
|
assert!(!Method::SessionStart.is_mutating());
|
|
assert!(!Method::SessionStop.is_mutating());
|
|
assert!(!Method::SessionStopAll.is_mutating());
|
|
assert!(!Method::SessionList.is_mutating());
|
|
assert!(!Method::ToolSessionStart.is_mutating());
|
|
assert!(!Method::ToolSessionStop.is_mutating());
|
|
}
|
|
|
|
#[test]
|
|
fn is_mutating_classifies_system_methods_as_non_mutating() {
|
|
assert!(!Method::SystemHandshake.is_mutating());
|
|
assert!(!Method::SystemPing.is_mutating());
|
|
assert!(!Method::SystemStatus.is_mutating());
|
|
assert!(!Method::BrowserList.is_mutating());
|
|
assert!(!Method::Cancel.is_mutating());
|
|
}
|
|
|
|
#[test]
|
|
fn effect_classifies_observe_as_transient_input() {
|
|
assert_eq!(Method::ToolSnapshot.effect(), MethodEffect::PassiveRead);
|
|
assert_eq!(Method::ToolHover.effect(), MethodEffect::TransientInput);
|
|
assert_eq!(Method::ToolObserve.effect(), MethodEffect::TransientInput);
|
|
assert_eq!(Method::ToolClick.effect(), MethodEffect::BrowserMutation);
|
|
assert_eq!(Method::Cancel.effect(), MethodEffect::ControlPlane);
|
|
}
|
|
|
|
#[test]
|
|
fn interrupt_gate_includes_transient_input() {
|
|
assert!(!Method::ToolSnapshot.requires_interrupt_gate());
|
|
assert!(Method::ToolHover.requires_interrupt_gate());
|
|
assert!(Method::ToolObserve.requires_interrupt_gate());
|
|
assert!(Method::ToolClick.requires_interrupt_gate());
|
|
assert!(!Method::Cancel.requires_interrupt_gate());
|
|
}
|
|
}
|