From 73e714463ae12c7b82ba7e4acfe05d18e90322f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Chaonan=E2=80=9D?= Date: Mon, 17 Aug 2026 19:52:05 +0800 Subject: [PATCH 1/5] feat(protocol): negotiate Trace v3 with v2 compatibility Define Trace v3 wire types and generated schemas while keeping protocol 1.1 peers compatible with 1.0. Default record start still produces Trace v2, and only an explicit trace_version=3 request enables the new shape. Refs #90 Co-authored-by: Cursor --- .../__tests__/connection-controller.test.ts | 22 +- .../src/lib/__tests__/trace-reducer.test.ts | 8 +- apps/extension/src/lib/trace-reducer.ts | 8 +- .../src/transport/__tests__/handshake.test.ts | 23 +- apps/extension/src/transport/handshake.ts | 2 +- apps/extension/src/transport/types.ts | 104 +++- crates/bsk-cli/src/cli/doctor.rs | 10 +- crates/bsk-cli/src/cli/record.rs | 15 +- crates/bsk-cli/src/daemon/start.rs | 4 +- crates/bsk-cli/src/daemon/state.rs | 2 +- crates/bsk-cli/tests/browser_wait.rs | 4 +- crates/bsk-cli/tests/cancel_forwarding.rs | 4 +- crates/bsk-cli/tests/handshake_compat.rs | 45 +- crates/bsk-cli/tests/per_session_queue.rs | 4 +- .../bsk-cli/tests/session_user_interrupt.rs | 4 +- crates/bsk-cli/tests/sessions_ipc.rs | 8 +- crates/bsk-cli/tests/status_cmd.rs | 2 +- crates/bsk-cli/tests/tools_ipc.rs | 4 +- crates/bsk-cli/tests/tools_m7_ipc.rs | 4 +- crates/bsk-cli/tests/tools_m8_ipc.rs | 4 +- crates/bsk-cli/tests/tools_m9_ipc.rs | 4 +- crates/bsk-cli/tests/ws_handshake.rs | 6 +- .../schema/tool_record_await_result.json | 551 ++++++++++++++++-- .../schema/tool_record_start_params.json | 23 + .../schema/tool_record_stop_result.json | 551 ++++++++++++++++-- crates/bsk-protocol/schema/trace.json | 314 ++++++---- crates/bsk-protocol/schema/trace_step.json | 215 +++---- crates/bsk-protocol/schema/trace_v2.json | 405 +++++++++++++ crates/bsk-protocol/src/bin/dump-schema.rs | 1 + crates/bsk-protocol/src/tools/mod.rs | 1 + crates/bsk-protocol/src/tools/record.rs | 537 +++++++++++------ crates/bsk-protocol/src/tools/record_v2.rs | 215 +++++++ 32 files changed, 2503 insertions(+), 601 deletions(-) create mode 100644 crates/bsk-protocol/schema/trace_v2.json create mode 100644 crates/bsk-protocol/src/tools/record_v2.rs diff --git a/apps/extension/src/lib/__tests__/connection-controller.test.ts b/apps/extension/src/lib/__tests__/connection-controller.test.ts index 6d5b578b..53d34af5 100644 --- a/apps/extension/src/lib/__tests__/connection-controller.test.ts +++ b/apps/extension/src/lib/__tests__/connection-controller.test.ts @@ -27,19 +27,19 @@ function handshake( describe("computeConnectedState (protocol-based compat)", () => { it("returns connected when protocol strings match", () => { - expect(computeConnectedState(handshake("1.0", "1.0"), MIN_COMPATIBLE_PROTOCOL)).toEqual({ + expect(computeConnectedState(handshake("1.1", "1.0"), MIN_COMPATIBLE_PROTOCOL)).toEqual({ kind: "connected", }); }); it("returns version_skew when daemon protocol minor is newer", () => { - expect(computeConnectedState(handshake("1.1", "1.0"))).toEqual({ + expect(computeConnectedState(handshake("1.2", "1.0"))).toEqual({ kind: "version_skew", }); }); it("returns version_skew when daemon protocol string differs but floor is satisfied", () => { - expect(computeConnectedState(handshake("1", "1.0"))).toEqual({ + expect(computeConnectedState(handshake("1.1.0", "1.0"))).toEqual({ kind: "version_skew", }); }); @@ -53,7 +53,7 @@ describe("computeConnectedState (protocol-based compat)", () => { }); it("rejects when extension is below daemon min_compatible_protocol", () => { - const result = computeConnectedState(handshake("1.0", "1.5")); + const result = computeConnectedState(handshake("1.1", "1.5")); expect(result.kind).toBe("rejected"); if (result.kind === "rejected") { expect(result.reason).toContain("min_compatible_protocol"); @@ -65,7 +65,7 @@ describe("computeConnectedState (protocol-based compat)", () => { const result = computeConnectedState({ server: "browser-skill-daemon", version: "0.1.0", - protocol_version: "1.0", + protocol_version: "1.1", min_compatible_peer: "0.1.0", }); expect(result).toEqual({ kind: "connected" }); @@ -79,8 +79,14 @@ describe("computeConnectedState (protocol-based compat)", () => { } }); + it("returns version_skew when daemon protocol is 1.0 and floor is satisfied", () => { + expect(computeConnectedState(handshake("1.0", "1.0"))).toEqual({ + kind: "version_skew", + }); + }); + it("rejects malformed daemon min_compatible_protocol with a daemon-floor reason", () => { - const result = computeConnectedState(handshake("1.0", "not-a-protocol")); + const result = computeConnectedState(handshake("1.1", "not-a-protocol")); expect(result.kind).toBe("rejected"); if (result.kind === "rejected") { expect(result.reason).toContain("daemon min_compatible_protocol"); @@ -242,11 +248,11 @@ describe("ConnectionController connectionEnabled", () => { const second = transport.send.mock.calls[1]?.[0] as { id: string }; expect(second.id).not.toBe(first.id); - transport.emitMessage({ id: first.id, result: handshake("1.0", "1.0") }); + transport.emitMessage({ id: first.id, result: handshake("1.1", "1.0") }); await Promise.resolve(); expect(controller.snapshot().state).not.toBe("connected"); - transport.emitMessage({ id: second.id, result: handshake("1.0", "1.0") }); + transport.emitMessage({ id: second.id, result: handshake("1.1", "1.0") }); await vi.waitFor(() => expect(controller.snapshot().state).toBe("connected")); }); }); diff --git a/apps/extension/src/lib/__tests__/trace-reducer.test.ts b/apps/extension/src/lib/__tests__/trace-reducer.test.ts index f5c6db51..b1153be9 100644 --- a/apps/extension/src/lib/__tests__/trace-reducer.test.ts +++ b/apps/extension/src/lib/__tests__/trace-reducer.test.ts @@ -151,7 +151,7 @@ describe("reduceTraceSteps", () => { }); }); - it("keeps hover steps before menu clicks", () => { + it("drops hover steps unsupported by historical v2 clients", () => { const { steps } = reduceTraceSteps( [ { @@ -167,11 +167,11 @@ describe("reduceTraceSteps", () => { ], "https://example.com/app", ); - expect(steps.map((s) => s.op)).toEqual(["hover", "click"]); + expect(steps.map((s) => s.op)).toEqual(["click"]); expect(steps[0]).toMatchObject({ - op: "hover", + op: "click", page: "p1", - target: { name: "Account" }, + target: { name: "Profile" }, }); }); diff --git a/apps/extension/src/lib/trace-reducer.ts b/apps/extension/src/lib/trace-reducer.ts index 8f83fff3..2d5b0df2 100644 --- a/apps/extension/src/lib/trace-reducer.ts +++ b/apps/extension/src/lib/trace-reducer.ts @@ -18,6 +18,7 @@ export function shouldRecordPress( } function shouldIncludeDraft(step: DraftTraceStep): boolean { + if (step.op === "hover") return false; if (step.op === "press" && !shouldRecordPress(step.key, step.modifiers)) return false; return true; } @@ -139,12 +140,7 @@ function toV2Step( effectForNavigation(step.navigated_to, urlToId), ); case "hover": - return { - op: "hover", - id, - page, - target: step.target, - }; + return null; case "fill": return { op: "fill", diff --git a/apps/extension/src/transport/__tests__/handshake.test.ts b/apps/extension/src/transport/__tests__/handshake.test.ts index b511453c..6196f344 100644 --- a/apps/extension/src/transport/__tests__/handshake.test.ts +++ b/apps/extension/src/transport/__tests__/handshake.test.ts @@ -65,6 +65,11 @@ function deferredFakeTransport(): { transport: Transport; emit: (frame: Protocol } describe("performHandshake", () => { + it("advertises the protocol compatibility boundary", () => { + expect(PROTOCOL_VERSION).toBe("1.1"); + expect(MIN_COMPATIBLE_PROTOCOL).toBe("1.0"); + }); + it("sends system.handshake with identity and both compat fields", async () => { let sentFrame: ProtocolFrame | null = null; const transport = fakeTransport((req) => { @@ -74,7 +79,7 @@ describe("performHandshake", () => { result: { server: "browser-skill-daemon", version: "0.1.0", - protocol_version: "1.0", + protocol_version: "1.1", min_compatible_peer: "0.0.0", min_compatible_protocol: "1.0", }, @@ -129,8 +134,8 @@ describe("performHandshake", () => { const response = { server: "browser-skill-daemon", version: "0.1.0", - protocol_version: "1.0", - min_compatible_protocol: "1.0", + protocol_version: "1.1", + min_compatible_protocol: "1.1", } satisfies HandshakeResult; const transport = fakeTransport((req) => ({ id: (req as { id: string }).id, @@ -145,7 +150,7 @@ describe("performHandshake", () => { }); expect(outcome.result.min_compatible_peer).toBeUndefined(); - expect(outcome.result.min_compatible_protocol).toBe("1.0"); + expect(outcome.result.min_compatible_protocol).toBe("1.1"); }); it("rejects when the daemon responds with an error", async () => { @@ -178,9 +183,9 @@ describe("performHandshake", () => { result: { server: "browser-skill-daemon", version: "0.1.0", - protocol_version: "1.0", + protocol_version: "1.1", min_compatible_peer: "0.0.0", - min_compatible_protocol: "1.0", + min_compatible_protocol: "1.1", }, }); emit({ @@ -188,14 +193,14 @@ describe("performHandshake", () => { result: { server: "browser-skill-daemon", version: "0.1.0", - protocol_version: "1.0", + protocol_version: "1.1", min_compatible_peer: "0.0.0", - min_compatible_protocol: "1.0", + min_compatible_protocol: "1.1", }, }); await expect(pending).resolves.toMatchObject({ - result: { server: "browser-skill-daemon", protocol_version: "1.0" }, + result: { server: "browser-skill-daemon", protocol_version: "1.1" }, }); }); diff --git a/apps/extension/src/transport/handshake.ts b/apps/extension/src/transport/handshake.ts index 3da56fda..a869ca52 100644 --- a/apps/extension/src/transport/handshake.ts +++ b/apps/extension/src/transport/handshake.ts @@ -7,7 +7,7 @@ import type { ResponseFrame, } from "./types"; -export const PROTOCOL_VERSION = "1.0"; +export const PROTOCOL_VERSION = "1.1"; /** * Extension semver, injected at build time from `package.json` via * Vite's `define` (see `wxt.config.ts` and `vitest.config.ts`). diff --git a/apps/extension/src/transport/types.ts b/apps/extension/src/transport/types.ts index d53825a1..e855f67a 100644 --- a/apps/extension/src/transport/types.ts +++ b/apps/extension/src/transport/types.ts @@ -657,9 +657,62 @@ export interface EmulateResult { } // -------------------------------------------------------------------------- -// Semantic record payloads — mirror bsk-protocol record.rs (Trace v2) +// Semantic record payloads — mirror bsk-protocol record.rs // -------------------------------------------------------------------------- +export const TRACE_VERSION = 3; +export const TRACE_VERSION_V2 = 2; +export const DEFAULT_TRACE_VERSION = 2; +export const VOM_FORMAT_VERSION = 1; + +export interface TargetDescriptorV3 { + ref?: string; + role?: string; + name?: string; + ctx?: string; + unmatched?: boolean; +} + +export interface RecorderInfo { + bsk: string; + vom: number; +} + +export type StopReason = "user_finish" | "cli_stop"; + +export interface TraceState { + id: string; + url: string; + title?: string; + /** Wire-only: full page observation text. */ + body?: string; + /** Disk-only: filename under the bundle `pages/` directory. */ + page?: string; + truncated?: boolean; +} + +export interface StepResult { + state: string; +} + +export interface StepCommonV3 { + id: number; + state: string; + result: StepResult; +} + +export type NavigationCause = + | "user_typed" + | "link" + | "form_submit" + | "reload" + | "history" + | "script" + | "browser"; + +export type FillCommit = "enter" | "suggestion" | "blur"; + +/** Legacy v2 target shape retained for existing record producers. */ export interface TargetDescriptor { role?: string; name?: string; @@ -740,7 +793,6 @@ export type DraftTraceStep = export type Step = | ({ op: "navigate" } & StepCommon & { to: string }) | ({ op: "click" } & StepCommon & { target: TargetDescriptor }) - | ({ op: "hover" } & StepCommon & { target: TargetDescriptor }) | ({ op: "fill" } & StepCommon & { target: TargetDescriptor; value: string; @@ -756,6 +808,9 @@ export type Step = target?: TargetDescriptor; }); +export type TargetDescriptorV2 = TargetDescriptor; +export type StepV2 = Step; + export interface Trace { recorded_at: string; started_at?: string; @@ -765,11 +820,52 @@ export interface Trace { steps: Step[]; } +export type TraceV2 = Trace; + +export type StepV3 = + | ({ op: "navigate" } & StepCommonV3 & { to: string; cause: NavigationCause }) + | ({ op: "click" } & StepCommonV3 & { target: TargetDescriptorV3 }) + | ({ op: "hover" } & StepCommonV3 & { target: TargetDescriptorV3 }) + | ({ op: "fill" } & StepCommonV3 & { + target: TargetDescriptorV3; + value: string; + commit: FillCommit; + redacted?: boolean; + }) + | ({ op: "select" } & StepCommonV3 & { + target: TargetDescriptorV3; + selection: SelectedOption[]; + }) + | ({ op: "press" } & StepCommonV3 & { + key: string; + modifiers?: KeyModifier[]; + target?: TargetDescriptorV3; + }) + | ({ op: "scroll" } & StepCommonV3); + +export interface TraceV3 { + version: number; + recorded_at: string; + started_at?: string; + purpose?: string; + stopped_by: StopReason; + entry: TraceEntry; + recorder: RecorderInfo; + states: TraceState[]; + steps: StepV3[]; +} + +export type RecordedTrace = TraceV2 | TraceV3; + export interface RecordStartParams { session_id: string; tab_id?: number; url?: string; purpose?: string; + max_page_tokens?: number; + redact_values?: boolean; + /** Omitted means v2; `3` requests a state-linked v3 trace. */ + trace_version?: number; } export interface RecordStartResult { @@ -782,7 +878,7 @@ export interface RecordStopParams { } export interface RecordStopResult { - trace: Trace; + trace: RecordedTrace; } export interface RecordAwaitParams { @@ -791,5 +887,5 @@ export interface RecordAwaitParams { } export interface RecordAwaitResult { - trace: Trace; + trace: RecordedTrace; } diff --git a/crates/bsk-cli/src/cli/doctor.rs b/crates/bsk-cli/src/cli/doctor.rs index 3d0c57a9..2a839012 100644 --- a/crates/bsk-cli/src/cli/doctor.rs +++ b/crates/bsk-cli/src/cli/doctor.rs @@ -486,7 +486,7 @@ mod m2_tests { fn fake_status(browsers: Vec, skew: Vec) -> StatusResult { StatusResult { daemon_version: env!("CARGO_PKG_VERSION").into(), - protocol_version: "1.0".into(), + protocol_version: "1.1".into(), pid: 1, uptime_secs: 0, ws_port: 0, @@ -571,7 +571,7 @@ mod m2_tests { session_count: 0, connected_at_ms: 1, version_skew: false, - extension_protocol_version: "1.0".into(), + extension_protocol_version: "1.1".into(), }], Vec::new(), ); @@ -592,7 +592,7 @@ mod m2_tests { session_count: 0, connected_at_ms: 1, version_skew: true, - extension_protocol_version: "1.1".into(), + extension_protocol_version: "1.2".into(), }], vec![VersionSkewEntry { instance_id: "alpha".into(), @@ -600,8 +600,8 @@ mod m2_tests { label: "Personal".into(), server_version: env!("CARGO_PKG_VERSION").into(), client_version: "0.0.9".into(), - server_protocol_version: "1.0".into(), - client_protocol_version: "1.1".into(), + server_protocol_version: "1.1".into(), + client_protocol_version: "1.2".into(), }], ); let check = check_browsers_protocol_compatible(Some(&status)); diff --git a/crates/bsk-cli/src/cli/record.rs b/crates/bsk-cli/src/cli/record.rs index ec72d9a2..7cee47f9 100644 --- a/crates/bsk-cli/src/cli/record.rs +++ b/crates/bsk-cli/src/cli/record.rs @@ -8,7 +8,7 @@ use anyhow::Context; use bsk_protocol::Method; use bsk_protocol::tools::{ RecordAwaitParams, RecordAwaitResult, RecordStartParams, RecordStartResult, RecordStopParams, - RecordStopResult, Trace, + RecordStopResult, RecordedTrace, }; use clap::{Args, Subcommand}; @@ -98,6 +98,9 @@ fn dispatch_start(args: RecordStartArgs, format: Format) -> Result<(), CliError> tab_id: args.tab_id, url: args.url, purpose: args.purpose.clone(), + max_page_tokens: None, + redact_values: None, + trace_version: None, }; let start_result = business_rpc::call::( info.sock_path.clone(), @@ -190,7 +193,7 @@ fn record_await_ipc_timeout(timeout_ms: u32) -> Duration { .unwrap_or(Duration::from_secs(u64::from(timeout_ms / 1_000) + 15)) } -fn write_trace_file(output: &PathBuf, trace: &Trace) -> Result<(), CliError> { +fn write_trace_file(output: &PathBuf, trace: &RecordedTrace) -> Result<(), CliError> { let json = serde_json::to_string_pretty(trace) .context("serialize trace JSON") .map_err(CliError::Local)?; @@ -207,7 +210,7 @@ fn write_trace_file(output: &PathBuf, trace: &Trace) -> Result<(), CliError> { Ok(()) } -fn render_finish(trace: &Trace, output: &PathBuf, format: Format) -> Result<(), CliError> { +fn render_finish(trace: &RecordedTrace, output: &PathBuf, format: Format) -> Result<(), CliError> { match format { Format::Json => { println!( @@ -221,7 +224,11 @@ fn render_finish(trace: &Trace, output: &PathBuf, format: Format) -> Result<(), ); } Format::Human => { - println!("saved {} steps to {}", trace.steps.len(), output.display()); + let step_count = match trace { + RecordedTrace::V2(trace) => trace.steps.len(), + RecordedTrace::V3(trace) => trace.steps.len(), + }; + println!("saved {step_count} steps to {}", output.display()); } } Ok(()) diff --git a/crates/bsk-cli/src/daemon/start.rs b/crates/bsk-cli/src/daemon/start.rs index 582cb4c8..4bee196e 100644 --- a/crates/bsk-cli/src/daemon/start.rs +++ b/crates/bsk-cli/src/daemon/start.rs @@ -27,7 +27,7 @@ use crate::daemon::{ browsers::{BROWSER_LIVENESS_TICK, BROWSER_LIVENESS_TIMEOUT, EXTENSION_CONNECT_WAIT}, info as daemon_info, ipc, lockfile, paths, sessions::{StopSessionError, forget_session, stop_session}, - state::DaemonState, + state::{DaemonState, PROTOCOL_VERSION}, ws, }; @@ -299,7 +299,7 @@ pub fn run_foreground(cfg: DaemonConfig) -> Result<()> { ws_port, sock_path: sock_path.clone(), daemon_version: env!("CARGO_PKG_VERSION"), - protocol_version: "1.0", + protocol_version: PROTOCOL_VERSION, }; let handler = ipc::full_handler(status, Arc::clone(&state)); diff --git a/crates/bsk-cli/src/daemon/state.rs b/crates/bsk-cli/src/daemon/state.rs index 7ea9cee7..c8fee7a3 100644 --- a/crates/bsk-cli/src/daemon/state.rs +++ b/crates/bsk-cli/src/daemon/state.rs @@ -16,7 +16,7 @@ use super::start::DaemonConfig; use super::ws::WsHandle; pub const DAEMON_VERSION: &str = env!("CARGO_PKG_VERSION"); -pub const PROTOCOL_VERSION: &str = "1.0"; +pub const PROTOCOL_VERSION: &str = "1.1"; /// Lowest **protocol** version peers must speak (e.g. `"1.0"`). pub const MIN_COMPATIBLE_PROTOCOL: &str = "1.0"; /// Legacy app-semver floor used only when `HandshakeResult.min_compatible_peer` diff --git a/crates/bsk-cli/tests/browser_wait.rs b/crates/bsk-cli/tests/browser_wait.rs index 16513e0f..26f7620f 100644 --- a/crates/bsk-cli/tests/browser_wait.rs +++ b/crates/bsk-cli/tests/browser_wait.rs @@ -112,14 +112,14 @@ async fn handshake_as_ext( let params = HandshakeParams { client: "browser-skill-extension".into(), version: "0.1.0-dev.0".parse().unwrap(), - protocol_version: "1.0".into(), + protocol_version: "1.1".into(), instance_id: TEST_EXT_ID.into(), browser: BrowserPeerInfo { name: "chrome".into(), version: "131.0".into(), }, min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()), - min_compatible_protocol: Some("1.0".into()), + min_compatible_protocol: Some("1.1".into()), label: "Test".into(), }; let req = RequestFrame { diff --git a/crates/bsk-cli/tests/cancel_forwarding.rs b/crates/bsk-cli/tests/cancel_forwarding.rs index c48113ce..1abd8f9b 100644 --- a/crates/bsk-cli/tests/cancel_forwarding.rs +++ b/crates/bsk-cli/tests/cancel_forwarding.rs @@ -75,7 +75,7 @@ async fn handshake_as_ext( let params = HandshakeParams { client: "browser-skill-extension".into(), version: "0.1.0-dev.0".parse().unwrap(), - protocol_version: "1.0".into(), + protocol_version: "1.1".into(), instance_id: TEST_EXT_ID.into(), browser: BrowserPeerInfo { name: "chrome".into(), @@ -83,7 +83,7 @@ async fn handshake_as_ext( }, label: "Test".into(), min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()), - min_compatible_protocol: Some("1.0".into()), + min_compatible_protocol: Some("1.1".into()), }; let req = RequestFrame { id: "hs".into(), diff --git a/crates/bsk-cli/tests/handshake_compat.rs b/crates/bsk-cli/tests/handshake_compat.rs index a5e91b01..7c8fd507 100644 --- a/crates/bsk-cli/tests/handshake_compat.rs +++ b/crates/bsk-cli/tests/handshake_compat.rs @@ -108,12 +108,12 @@ async fn send_handshake_with_floors( async fn handshake_ok_when_protocol_matches() { let (handle, _sock) = spawn_daemon().await; let mut ws = open_ws(handle.ws_addr()).await; - let resp = send_handshake(&mut ws, "1.0", env!("CARGO_PKG_VERSION")).await; + let resp = send_handshake(&mut ws, "1.1", env!("CARGO_PKG_VERSION")).await; let result: HandshakeResult = match resp.body { ResponseBody::Ok(v) => serde_json::from_value(v).unwrap(), ResponseBody::Err(e) => panic!("expected ok handshake, got {e:?}"), }; - assert_eq!(result.protocol_version, "1.0"); + assert_eq!(result.protocol_version, "1.1"); assert_eq!( result .min_compatible_peer @@ -135,7 +135,7 @@ async fn handshake_ok_when_app_versions_differ_but_protocol_matches() { let (handle, _sock) = spawn_daemon().await; let mut ws = open_ws(handle.ws_addr()).await; let resp = - send_handshake_with_floors(&mut ws, "1.0", "9.9.9", Some("0.0.0"), Some("1.0")).await; + send_handshake_with_floors(&mut ws, "1.1", "9.9.9", Some("0.0.0"), Some("1.1")).await; match resp.body { ResponseBody::Ok(_) => {} other => panic!("expected ok when protocol matches, got {other:?}"), @@ -149,10 +149,10 @@ async fn handshake_skew_when_protocol_minor_differs() { let mut ws = open_ws(handle.ws_addr()).await; let resp = send_handshake_with_floors( &mut ws, - "1.1", + "1.2", env!("CARGO_PKG_VERSION"), Some("0.0.0"), - Some("1.0"), + Some("1.1"), ) .await; match resp.body { @@ -168,6 +168,31 @@ async fn handshake_skew_when_protocol_minor_differs() { handle.shutdown().await; } +#[tokio::test] +async fn handshake_skew_when_protocol_1_0_peer() { + let (handle, _sock) = spawn_daemon().await; + let mut ws = open_ws(handle.ws_addr()).await; + let resp = send_handshake_with_floors( + &mut ws, + "1.0", + env!("CARGO_PKG_VERSION"), + Some("0.0.0"), + Some("1.0"), + ) + .await; + match resp.body { + ResponseBody::Ok(_) => {} + other => panic!("protocol 1.0 peer should connect with skew, got {other:?}"), + } + let state = handle.state(); + let client = state + .browsers + .get(&bsk::daemon::browsers::BrowserId(TEST_EXT_ID.into())) + .expect("browser registered"); + assert!(client.version_skew); + handle.shutdown().await; +} + #[tokio::test] async fn handshake_rejected_on_protocol_major_mismatch() { let (handle, _sock) = spawn_daemon().await; @@ -210,7 +235,7 @@ async fn handshake_legacy_ext_without_protocol_floor_still_ok() { let mut ws = open_ws(handle.ws_addr()).await; let resp = send_handshake_with_floors( &mut ws, - "1.0", + "1.1", env!("CARGO_PKG_VERSION"), Some("0.1.0"), None, @@ -235,7 +260,7 @@ async fn status_surfaces_version_skew_for_skewed_browser() { browser_name: "chrome".into(), browser_version: "131.0".into(), extension_version: "9.9.9".into(), - extension_protocol_version: "1.1".into(), + extension_protocol_version: "1.2".into(), label: "Older".into(), sink: bsk::daemon::browsers::BrowserSink { tx }, pending: Mutex::new(bsk::daemon::browsers::Pending::default()), @@ -263,8 +288,8 @@ async fn status_surfaces_version_skew_for_skewed_browser() { .iter() .find(|s| s.instance_id == "skew-only-test") .expect("status must list our skew client"); - assert_eq!(skew.client_protocol_version, "1.1"); - assert_eq!(skew.server_protocol_version, "1.0"); + assert_eq!(skew.client_protocol_version, "1.2"); + assert_eq!(skew.server_protocol_version, "1.1"); assert_eq!(skew.client_version, "9.9.9"); let entry = status .browsers @@ -281,7 +306,7 @@ async fn handshake_rejects_when_local_below_peer_min_compatible_protocol() { let mut ws = open_ws(handle.ws_addr()).await; let resp = send_handshake_with_floors( &mut ws, - "1.0", + "1.1", env!("CARGO_PKG_VERSION"), Some("0.0.0"), Some("99.0.0"), diff --git a/crates/bsk-cli/tests/per_session_queue.rs b/crates/bsk-cli/tests/per_session_queue.rs index 79b53559..4f66d1e8 100644 --- a/crates/bsk-cli/tests/per_session_queue.rs +++ b/crates/bsk-cli/tests/per_session_queue.rs @@ -81,14 +81,14 @@ async fn handshake_as_ext( let params = HandshakeParams { client: "browser-skill-extension".into(), version: "0.1.0-dev.0".parse().unwrap(), - protocol_version: "1.0".into(), + protocol_version: "1.1".into(), instance_id: TEST_EXT_ID.into(), browser: BrowserPeerInfo { name: "chrome".into(), version: "131.0".into(), }, min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()), - min_compatible_protocol: Some("1.0".into()), + min_compatible_protocol: Some("1.1".into()), label: "Test".into(), }; let req = RequestFrame { diff --git a/crates/bsk-cli/tests/session_user_interrupt.rs b/crates/bsk-cli/tests/session_user_interrupt.rs index 9c5288cc..7529273a 100644 --- a/crates/bsk-cli/tests/session_user_interrupt.rs +++ b/crates/bsk-cli/tests/session_user_interrupt.rs @@ -81,7 +81,7 @@ async fn handshake_as_ext( let params = HandshakeParams { client: "browser-skill-extension".into(), version: "0.1.0-dev.0".parse().unwrap(), - protocol_version: "1.0".into(), + protocol_version: "1.1".into(), instance_id: TEST_EXT_ID.into(), browser: BrowserPeerInfo { name: "chrome".into(), @@ -89,7 +89,7 @@ async fn handshake_as_ext( }, label: "Test".into(), min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()), - min_compatible_protocol: Some("1.0".into()), + min_compatible_protocol: Some("1.1".into()), }; let req = RequestFrame { id: "hs".into(), diff --git a/crates/bsk-cli/tests/sessions_ipc.rs b/crates/bsk-cli/tests/sessions_ipc.rs index 69c87c93..aaf895b1 100644 --- a/crates/bsk-cli/tests/sessions_ipc.rs +++ b/crates/bsk-cli/tests/sessions_ipc.rs @@ -84,14 +84,14 @@ async fn handshake_as_ext( let params = HandshakeParams { client: "browser-skill-extension".into(), version: "0.1.0-dev.0".parse().unwrap(), - protocol_version: "1.0".into(), + protocol_version: "1.1".into(), instance_id: TEST_EXT_ID.into(), browser: BrowserPeerInfo { name: "chrome".into(), version: "131.0".into(), }, min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()), - min_compatible_protocol: Some("1.0".into()), + min_compatible_protocol: Some("1.1".into()), label: "Test".into(), }; let req = RequestFrame { @@ -400,14 +400,14 @@ async fn connect_second_ext( let params = HandshakeParams { client: "browser-skill-extension".into(), version: "0.1.0-dev.0".parse().unwrap(), - protocol_version: "1.0".into(), + protocol_version: "1.1".into(), instance_id: instance_id.into(), browser: BrowserPeerInfo { name: "edge".into(), version: "130".into(), }, min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()), - min_compatible_protocol: Some("1.0".into()), + min_compatible_protocol: Some("1.1".into()), label: label.into(), }; let hs = RequestFrame { diff --git a/crates/bsk-cli/tests/status_cmd.rs b/crates/bsk-cli/tests/status_cmd.rs index 0e329146..18513e2e 100644 --- a/crates/bsk-cli/tests/status_cmd.rs +++ b/crates/bsk-cli/tests/status_cmd.rs @@ -61,7 +61,7 @@ fn bsk_status_json_returns_structured_payload() { assert!(parsed["pid"].as_u64().unwrap() > 0); assert!(!parsed["daemon_version"].as_str().unwrap().is_empty()); - assert_eq!(parsed["protocol_version"], "1.0"); + assert_eq!(parsed["protocol_version"], "1.1"); assert!(parsed["sock_path"].as_str().is_some()); assert_eq!(parsed["browsers"], serde_json::json!([])); assert_eq!(parsed["sessions"], serde_json::json!([])); diff --git a/crates/bsk-cli/tests/tools_ipc.rs b/crates/bsk-cli/tests/tools_ipc.rs index d40bb9ca..97fa635a 100644 --- a/crates/bsk-cli/tests/tools_ipc.rs +++ b/crates/bsk-cli/tests/tools_ipc.rs @@ -73,14 +73,14 @@ async fn do_handshake(ws: &mut Ws) -> HandshakeResult { let params = HandshakeParams { client: "browser-skill-extension".into(), version: "0.1.0-dev.0".parse().unwrap(), - protocol_version: "1.0".into(), + protocol_version: "1.1".into(), instance_id: TEST_EXT_ID.into(), browser: BrowserPeerInfo { name: "chrome".into(), version: "131.0".into(), }, min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()), - min_compatible_protocol: Some("1.0".into()), + min_compatible_protocol: Some("1.1".into()), label: "Test".into(), }; let req = RequestFrame { diff --git a/crates/bsk-cli/tests/tools_m7_ipc.rs b/crates/bsk-cli/tests/tools_m7_ipc.rs index ee74b5e5..095584ab 100644 --- a/crates/bsk-cli/tests/tools_m7_ipc.rs +++ b/crates/bsk-cli/tests/tools_m7_ipc.rs @@ -76,14 +76,14 @@ async fn do_handshake(ws: &mut Ws) -> HandshakeResult { let params = HandshakeParams { client: "browser-skill-extension".into(), version: "0.1.0-dev.0".parse().unwrap(), - protocol_version: "1.0".into(), + protocol_version: "1.1".into(), instance_id: TEST_EXT_ID.into(), browser: BrowserPeerInfo { name: "chrome".into(), version: "131.0".into(), }, min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()), - min_compatible_protocol: Some("1.0".into()), + min_compatible_protocol: Some("1.1".into()), label: "Test".into(), }; let req = RequestFrame { diff --git a/crates/bsk-cli/tests/tools_m8_ipc.rs b/crates/bsk-cli/tests/tools_m8_ipc.rs index 14e7c72a..4bdce78e 100644 --- a/crates/bsk-cli/tests/tools_m8_ipc.rs +++ b/crates/bsk-cli/tests/tools_m8_ipc.rs @@ -75,14 +75,14 @@ async fn do_handshake(ws: &mut Ws) -> HandshakeResult { let params = HandshakeParams { client: "browser-skill-extension".into(), version: "0.1.0-dev.0".parse().unwrap(), - protocol_version: "1.0".into(), + protocol_version: "1.1".into(), instance_id: TEST_EXT_ID.into(), browser: BrowserPeerInfo { name: "chrome".into(), version: "131.0".into(), }, min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()), - min_compatible_protocol: Some("1.0".into()), + min_compatible_protocol: Some("1.1".into()), label: "Test".into(), }; let req = RequestFrame { diff --git a/crates/bsk-cli/tests/tools_m9_ipc.rs b/crates/bsk-cli/tests/tools_m9_ipc.rs index 2f8b46d9..7937c27d 100644 --- a/crates/bsk-cli/tests/tools_m9_ipc.rs +++ b/crates/bsk-cli/tests/tools_m9_ipc.rs @@ -85,14 +85,14 @@ async fn do_handshake(ws: &mut Ws) -> HandshakeResult { let params = HandshakeParams { client: "browser-skill-extension".into(), version: "0.1.0-dev.0".parse().unwrap(), - protocol_version: "1.0".into(), + protocol_version: "1.1".into(), instance_id: TEST_EXT_ID.into(), browser: BrowserPeerInfo { name: "chrome".into(), version: "131.0".into(), }, min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()), - min_compatible_protocol: Some("1.0".into()), + min_compatible_protocol: Some("1.1".into()), label: "Test".into(), }; let req = RequestFrame { diff --git a/crates/bsk-cli/tests/ws_handshake.rs b/crates/bsk-cli/tests/ws_handshake.rs index c6e2da14..300a8f0e 100644 --- a/crates/bsk-cli/tests/ws_handshake.rs +++ b/crates/bsk-cli/tests/ws_handshake.rs @@ -53,14 +53,14 @@ pub async fn send_handshake( let params = HandshakeParams { client: "browser-skill-extension".into(), version: "0.1.0-dev.0".parse().unwrap(), - protocol_version: "1.0".into(), + protocol_version: "1.1".into(), instance_id: instance_id.into(), browser: BrowserPeerInfo { name: "chrome".into(), version: "131.0".into(), }, min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()), - min_compatible_protocol: Some("1.0".into()), + min_compatible_protocol: Some("1.1".into()), label: "Test Chrome".into(), }; let req = RequestFrame { @@ -91,7 +91,7 @@ async fn ws_handshake_registers_browser_in_state() { let mut ws = connect_ext(handle.ws_addr(), &origin).await; let result = send_handshake(&mut ws, TEST_EXT_ID).await; assert_eq!(result.server, "browser-skill-daemon"); - assert_eq!(result.protocol_version, "1.0"); + assert_eq!(result.protocol_version, "1.1"); let state = handle.state(); let browsers = state.browsers.snapshot(); diff --git a/crates/bsk-protocol/schema/tool_record_await_result.json b/crates/bsk-protocol/schema/tool_record_await_result.json index 348edc8d..622e2704 100644 --- a/crates/bsk-protocol/schema/tool_record_await_result.json +++ b/crates/bsk-protocol/schema/tool_record_await_result.json @@ -7,10 +7,18 @@ ], "properties": { "trace": { - "$ref": "#/definitions/Trace" + "$ref": "#/definitions/RecordedTrace" } }, "definitions": { + "FillCommit": { + "type": "string", + "enum": [ + "enter", + "suggestion", + "blur" + ] + }, "KeyModifier": { "description": "Keyboard modifier flags. Multiple flags may be combined; the extension folds them into CDP's bitfield (`alt=1, ctrl=2, meta=4, shift=8`).", "type": "string", @@ -21,6 +29,18 @@ "shift" ] }, + "NavigationCause": { + "type": "string", + "enum": [ + "user_typed", + "link", + "form_submit", + "reload", + "history", + "script", + "browser" + ] + }, "PageRef": { "description": "Page context dictionary entry — referenced by steps via `page` id.", "type": "object", @@ -43,6 +63,33 @@ } } }, + "RecordedTrace": { + "oneOf": [ + { + "$ref": "#/definitions/TraceV2" + }, + { + "$ref": "#/definitions/Trace" + } + ] + }, + "RecorderInfo": { + "type": "object", + "required": [ + "bsk", + "vom" + ], + "properties": { + "bsk": { + "type": "string" + }, + "vom": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + } + }, "SelectedOption": { "description": "One selected option (`select` op).", "type": "object", @@ -61,6 +108,24 @@ } } }, + "SelectedOptionV2": { + "description": "One selected option (`select` op).", + "type": "object", + "required": [ + "value" + ], + "properties": { + "label": { + "type": [ + "string", + "null" + ] + }, + "value": { + "type": "string" + } + } + }, "Step": { "description": "One recorded user action — discriminated union by `op`.", "oneOf": [ @@ -68,22 +133,256 @@ "description": "Fields shared by every step variant (flattened in JSON).", "type": "object", "required": [ + "cause", "id", "op", - "page", + "result", + "state", "to" ], "properties": { - "effect": { + "cause": { + "$ref": "#/definitions/NavigationCause" + }, + "id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "op": { + "type": "string", + "enum": [ + "navigate" + ] + }, + "result": { + "$ref": "#/definitions/StepResult" + }, + "state": { + "description": "Observation id immediately before this action.", + "type": "string" + }, + "to": { + "type": "string" + } + } + }, + { + "description": "Fields shared by every step variant (flattened in JSON).", + "type": "object", + "required": [ + "id", + "op", + "result", + "state", + "target" + ], + "properties": { + "id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "op": { + "type": "string", + "enum": [ + "click" + ] + }, + "result": { + "$ref": "#/definitions/StepResult" + }, + "state": { + "description": "Observation id immediately before this action.", + "type": "string" + }, + "target": { + "$ref": "#/definitions/TargetDescriptor" + } + } + }, + { + "description": "Fields shared by every step variant (flattened in JSON).", + "type": "object", + "required": [ + "id", + "op", + "result", + "state", + "target" + ], + "properties": { + "id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "op": { + "type": "string", + "enum": [ + "hover" + ] + }, + "result": { + "$ref": "#/definitions/StepResult" + }, + "state": { + "description": "Observation id immediately before this action.", + "type": "string" + }, + "target": { + "$ref": "#/definitions/TargetDescriptor" + } + } + }, + { + "description": "Fields shared by every step variant (flattened in JSON).", + "type": "object", + "required": [ + "commit", + "id", + "op", + "result", + "state", + "target", + "value" + ], + "properties": { + "commit": { + "$ref": "#/definitions/FillCommit" + }, + "id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "op": { + "type": "string", + "enum": [ + "fill" + ] + }, + "redacted": { + "type": "boolean" + }, + "result": { + "$ref": "#/definitions/StepResult" + }, + "state": { + "description": "Observation id immediately before this action.", + "type": "string" + }, + "target": { + "$ref": "#/definitions/TargetDescriptor" + }, + "value": { + "type": "string" + } + } + }, + { + "description": "Fields shared by every step variant (flattened in JSON).", + "type": "object", + "required": [ + "id", + "op", + "result", + "state", + "target" + ], + "properties": { + "id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "op": { + "type": "string", + "enum": [ + "select" + ] + }, + "result": { + "$ref": "#/definitions/StepResult" + }, + "selection": { + "type": "array", + "items": { + "$ref": "#/definitions/SelectedOption" + } + }, + "state": { + "description": "Observation id immediately before this action.", + "type": "string" + }, + "target": { + "$ref": "#/definitions/TargetDescriptor" + } + } + }, + { + "description": "Fields shared by every step variant (flattened in JSON).", + "type": "object", + "required": [ + "id", + "key", + "op", + "result", + "state" + ], + "properties": { + "id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "key": { + "type": "string" + }, + "modifiers": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/KeyModifier" + } + }, + "op": { + "type": "string", + "enum": [ + "press" + ] + }, + "result": { + "$ref": "#/definitions/StepResult" + }, + "state": { + "description": "Observation id immediately before this action.", + "type": "string" + }, + "target": { "anyOf": [ { - "$ref": "#/definitions/StepEffect" + "$ref": "#/definitions/TargetDescriptor" }, { "type": "null" } ] - }, + } + } + }, + { + "description": "Fields shared by every step variant (flattened in JSON).", + "type": "object", + "required": [ + "id", + "op", + "result", + "state" + ], + "properties": { "id": { "type": "integer", "format": "uint32", @@ -92,32 +391,61 @@ "op": { "type": "string", "enum": [ - "navigate" + "scroll" ] }, - "page": { - "description": "Reference into `pages[]`.", - "type": "string" + "result": { + "$ref": "#/definitions/StepResult" }, - "to": { + "state": { + "description": "Observation id immediately before this action.", "type": "string" } } - }, + } + ] + }, + "StepEffectV2": { + "description": "Observed navigation after a step (objective fact only).", + "type": "object", + "required": [ + "navigated_to" + ], + "properties": { + "navigated_to": { + "description": "Reference into `pages[]` for the destination page.", + "type": "string" + } + } + }, + "StepResult": { + "type": "object", + "required": [ + "state" + ], + "properties": { + "state": { + "type": "string" + } + } + }, + "StepV2": { + "description": "One recorded user action — discriminated union by `op` (v2).", + "oneOf": [ { - "description": "Fields shared by every step variant (flattened in JSON).", + "description": "Fields shared by every v2 step variant (flattened in JSON).", "type": "object", "required": [ "id", "op", "page", - "target" + "to" ], "properties": { "effect": { "anyOf": [ { - "$ref": "#/definitions/StepEffect" + "$ref": "#/definitions/StepEffectV2" }, { "type": "null" @@ -132,20 +460,20 @@ "op": { "type": "string", "enum": [ - "click" + "navigate" ] }, "page": { "description": "Reference into `pages[]`.", "type": "string" }, - "target": { - "$ref": "#/definitions/TargetDescriptor" + "to": { + "type": "string" } } }, { - "description": "Fields shared by every step variant (flattened in JSON).", + "description": "Fields shared by every v2 step variant (flattened in JSON).", "type": "object", "required": [ "id", @@ -157,7 +485,7 @@ "effect": { "anyOf": [ { - "$ref": "#/definitions/StepEffect" + "$ref": "#/definitions/StepEffectV2" }, { "type": "null" @@ -172,7 +500,7 @@ "op": { "type": "string", "enum": [ - "hover" + "click" ] }, "page": { @@ -180,12 +508,12 @@ "type": "string" }, "target": { - "$ref": "#/definitions/TargetDescriptor" + "$ref": "#/definitions/TargetDescriptorV2" } } }, { - "description": "Fields shared by every step variant (flattened in JSON).", + "description": "Fields shared by every v2 step variant (flattened in JSON).", "type": "object", "required": [ "id", @@ -198,7 +526,7 @@ "effect": { "anyOf": [ { - "$ref": "#/definitions/StepEffect" + "$ref": "#/definitions/StepEffectV2" }, { "type": "null" @@ -227,7 +555,7 @@ ] }, "target": { - "$ref": "#/definitions/TargetDescriptor" + "$ref": "#/definitions/TargetDescriptorV2" }, "value": { "type": "string" @@ -235,7 +563,7 @@ } }, { - "description": "Fields shared by every step variant (flattened in JSON).", + "description": "Fields shared by every v2 step variant (flattened in JSON).", "type": "object", "required": [ "id", @@ -248,7 +576,7 @@ "effect": { "anyOf": [ { - "$ref": "#/definitions/StepEffect" + "$ref": "#/definitions/StepEffectV2" }, { "type": "null" @@ -273,16 +601,16 @@ "selection": { "type": "array", "items": { - "$ref": "#/definitions/SelectedOption" + "$ref": "#/definitions/SelectedOptionV2" } }, "target": { - "$ref": "#/definitions/TargetDescriptor" + "$ref": "#/definitions/TargetDescriptorV2" } } }, { - "description": "Fields shared by every step variant (flattened in JSON).", + "description": "Fields shared by every v2 step variant (flattened in JSON).", "type": "object", "required": [ "id", @@ -294,7 +622,7 @@ "effect": { "anyOf": [ { - "$ref": "#/definitions/StepEffect" + "$ref": "#/definitions/StepEffectV2" }, { "type": "null" @@ -331,7 +659,7 @@ "target": { "anyOf": [ { - "$ref": "#/definitions/TargetDescriptor" + "$ref": "#/definitions/TargetDescriptorV2" }, { "type": "null" @@ -342,21 +670,48 @@ } ] }, - "StepEffect": { - "description": "Observed navigation after a step (objective fact only).", + "StopReason": { + "type": "string", + "enum": [ + "user_finish", + "cli_stop" + ] + }, + "TargetDescriptor": { + "description": "Stable semantic handle for an interacted element within a page observation.", "type": "object", - "required": [ - "navigated_to" - ], "properties": { - "navigated_to": { - "description": "Reference into `pages[]` for the destination page.", - "type": "string" + "ctx": { + "type": [ + "string", + "null" + ] + }, + "name": { + "type": [ + "string", + "null" + ] + }, + "ref": { + "type": [ + "string", + "null" + ] + }, + "role": { + "type": [ + "string", + "null" + ] + }, + "unmatched": { + "type": "boolean" } } }, - "TargetDescriptor": { - "description": "Stable semantic handle for an interacted element.\n\n`name` and `nearby_label` are **untrusted page text**.", + "TargetDescriptorV2": { + "description": "Stable semantic handle for an interacted element (v2).\n\n`name` and `nearby_label` are **untrusted page text**.", "type": "object", "required": [ "tag" @@ -402,42 +757,54 @@ "type": "object", "required": [ "entry", - "pages", "recorded_at", - "steps" + "recorder", + "states", + "steps", + "stopped_by", + "version" ], "properties": { "entry": { "$ref": "#/definitions/TraceEntry" }, - "pages": { - "type": "array", - "items": { - "$ref": "#/definitions/PageRef" - } - }, "purpose": { - "description": "Optional user-provided goal from `--purpose` (metadata only).", "type": [ "string", "null" ] }, "recorded_at": { - "description": "RFC 3339 timestamp when recording stopped.", "type": "string" }, + "recorder": { + "$ref": "#/definitions/RecorderInfo" + }, "started_at": { "type": [ "string", "null" ] }, + "states": { + "type": "array", + "items": { + "$ref": "#/definitions/TraceState" + } + }, "steps": { "type": "array", "items": { "$ref": "#/definitions/Step" } + }, + "stopped_by": { + "$ref": "#/definitions/StopReason" + }, + "version": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 } } }, @@ -452,6 +819,88 @@ "type": "string" } } + }, + "TraceState": { + "description": "Page observation dictionary entry — referenced by steps via `state` / `result.state`.", + "type": "object", + "required": [ + "id", + "url" + ], + "properties": { + "body": { + "description": "Wire-only: full page observation (front matter + VOM body + annotations).", + "type": [ + "string", + "null" + ] + }, + "id": { + "type": "string" + }, + "page": { + "description": "Disk-only: filename under the bundle `pages/` directory.", + "type": [ + "string", + "null" + ] + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "truncated": { + "type": "boolean" + }, + "url": { + "type": "string" + } + } + }, + "TraceV2": { + "description": "Persisted user-action trace exported by legacy `tool.record_stop` / `await`.", + "type": "object", + "required": [ + "entry", + "pages", + "recorded_at", + "steps" + ], + "properties": { + "entry": { + "$ref": "#/definitions/TraceEntry" + }, + "pages": { + "type": "array", + "items": { + "$ref": "#/definitions/PageRef" + } + }, + "purpose": { + "type": [ + "string", + "null" + ] + }, + "recorded_at": { + "description": "RFC 3339 timestamp when recording stopped.", + "type": "string" + }, + "started_at": { + "type": [ + "string", + "null" + ] + }, + "steps": { + "type": "array", + "items": { + "$ref": "#/definitions/StepV2" + } + } + } } } } diff --git a/crates/bsk-protocol/schema/tool_record_start_params.json b/crates/bsk-protocol/schema/tool_record_start_params.json index 859ebf82..e81c2618 100644 --- a/crates/bsk-protocol/schema/tool_record_start_params.json +++ b/crates/bsk-protocol/schema/tool_record_start_params.json @@ -6,12 +6,26 @@ "session_id" ], "properties": { + "max_page_tokens": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, "purpose": { "type": [ "string", "null" ] }, + "redact_values": { + "type": [ + "boolean", + "null" + ] + }, "session_id": { "type": "string" }, @@ -22,6 +36,15 @@ ], "format": "int64" }, + "trace_version": { + "description": "Desired trace export format. Omitted ⇒ v2; `3` ⇒ state-linked v3 bundle.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, "url": { "type": [ "string", diff --git a/crates/bsk-protocol/schema/tool_record_stop_result.json b/crates/bsk-protocol/schema/tool_record_stop_result.json index c46ba691..49d84c02 100644 --- a/crates/bsk-protocol/schema/tool_record_stop_result.json +++ b/crates/bsk-protocol/schema/tool_record_stop_result.json @@ -7,10 +7,18 @@ ], "properties": { "trace": { - "$ref": "#/definitions/Trace" + "$ref": "#/definitions/RecordedTrace" } }, "definitions": { + "FillCommit": { + "type": "string", + "enum": [ + "enter", + "suggestion", + "blur" + ] + }, "KeyModifier": { "description": "Keyboard modifier flags. Multiple flags may be combined; the extension folds them into CDP's bitfield (`alt=1, ctrl=2, meta=4, shift=8`).", "type": "string", @@ -21,6 +29,18 @@ "shift" ] }, + "NavigationCause": { + "type": "string", + "enum": [ + "user_typed", + "link", + "form_submit", + "reload", + "history", + "script", + "browser" + ] + }, "PageRef": { "description": "Page context dictionary entry — referenced by steps via `page` id.", "type": "object", @@ -43,6 +63,33 @@ } } }, + "RecordedTrace": { + "oneOf": [ + { + "$ref": "#/definitions/TraceV2" + }, + { + "$ref": "#/definitions/Trace" + } + ] + }, + "RecorderInfo": { + "type": "object", + "required": [ + "bsk", + "vom" + ], + "properties": { + "bsk": { + "type": "string" + }, + "vom": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + } + }, "SelectedOption": { "description": "One selected option (`select` op).", "type": "object", @@ -61,6 +108,24 @@ } } }, + "SelectedOptionV2": { + "description": "One selected option (`select` op).", + "type": "object", + "required": [ + "value" + ], + "properties": { + "label": { + "type": [ + "string", + "null" + ] + }, + "value": { + "type": "string" + } + } + }, "Step": { "description": "One recorded user action — discriminated union by `op`.", "oneOf": [ @@ -68,22 +133,256 @@ "description": "Fields shared by every step variant (flattened in JSON).", "type": "object", "required": [ + "cause", "id", "op", - "page", + "result", + "state", "to" ], "properties": { - "effect": { + "cause": { + "$ref": "#/definitions/NavigationCause" + }, + "id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "op": { + "type": "string", + "enum": [ + "navigate" + ] + }, + "result": { + "$ref": "#/definitions/StepResult" + }, + "state": { + "description": "Observation id immediately before this action.", + "type": "string" + }, + "to": { + "type": "string" + } + } + }, + { + "description": "Fields shared by every step variant (flattened in JSON).", + "type": "object", + "required": [ + "id", + "op", + "result", + "state", + "target" + ], + "properties": { + "id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "op": { + "type": "string", + "enum": [ + "click" + ] + }, + "result": { + "$ref": "#/definitions/StepResult" + }, + "state": { + "description": "Observation id immediately before this action.", + "type": "string" + }, + "target": { + "$ref": "#/definitions/TargetDescriptor" + } + } + }, + { + "description": "Fields shared by every step variant (flattened in JSON).", + "type": "object", + "required": [ + "id", + "op", + "result", + "state", + "target" + ], + "properties": { + "id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "op": { + "type": "string", + "enum": [ + "hover" + ] + }, + "result": { + "$ref": "#/definitions/StepResult" + }, + "state": { + "description": "Observation id immediately before this action.", + "type": "string" + }, + "target": { + "$ref": "#/definitions/TargetDescriptor" + } + } + }, + { + "description": "Fields shared by every step variant (flattened in JSON).", + "type": "object", + "required": [ + "commit", + "id", + "op", + "result", + "state", + "target", + "value" + ], + "properties": { + "commit": { + "$ref": "#/definitions/FillCommit" + }, + "id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "op": { + "type": "string", + "enum": [ + "fill" + ] + }, + "redacted": { + "type": "boolean" + }, + "result": { + "$ref": "#/definitions/StepResult" + }, + "state": { + "description": "Observation id immediately before this action.", + "type": "string" + }, + "target": { + "$ref": "#/definitions/TargetDescriptor" + }, + "value": { + "type": "string" + } + } + }, + { + "description": "Fields shared by every step variant (flattened in JSON).", + "type": "object", + "required": [ + "id", + "op", + "result", + "state", + "target" + ], + "properties": { + "id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "op": { + "type": "string", + "enum": [ + "select" + ] + }, + "result": { + "$ref": "#/definitions/StepResult" + }, + "selection": { + "type": "array", + "items": { + "$ref": "#/definitions/SelectedOption" + } + }, + "state": { + "description": "Observation id immediately before this action.", + "type": "string" + }, + "target": { + "$ref": "#/definitions/TargetDescriptor" + } + } + }, + { + "description": "Fields shared by every step variant (flattened in JSON).", + "type": "object", + "required": [ + "id", + "key", + "op", + "result", + "state" + ], + "properties": { + "id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "key": { + "type": "string" + }, + "modifiers": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/KeyModifier" + } + }, + "op": { + "type": "string", + "enum": [ + "press" + ] + }, + "result": { + "$ref": "#/definitions/StepResult" + }, + "state": { + "description": "Observation id immediately before this action.", + "type": "string" + }, + "target": { "anyOf": [ { - "$ref": "#/definitions/StepEffect" + "$ref": "#/definitions/TargetDescriptor" }, { "type": "null" } ] - }, + } + } + }, + { + "description": "Fields shared by every step variant (flattened in JSON).", + "type": "object", + "required": [ + "id", + "op", + "result", + "state" + ], + "properties": { "id": { "type": "integer", "format": "uint32", @@ -92,32 +391,61 @@ "op": { "type": "string", "enum": [ - "navigate" + "scroll" ] }, - "page": { - "description": "Reference into `pages[]`.", - "type": "string" + "result": { + "$ref": "#/definitions/StepResult" }, - "to": { + "state": { + "description": "Observation id immediately before this action.", "type": "string" } } - }, + } + ] + }, + "StepEffectV2": { + "description": "Observed navigation after a step (objective fact only).", + "type": "object", + "required": [ + "navigated_to" + ], + "properties": { + "navigated_to": { + "description": "Reference into `pages[]` for the destination page.", + "type": "string" + } + } + }, + "StepResult": { + "type": "object", + "required": [ + "state" + ], + "properties": { + "state": { + "type": "string" + } + } + }, + "StepV2": { + "description": "One recorded user action — discriminated union by `op` (v2).", + "oneOf": [ { - "description": "Fields shared by every step variant (flattened in JSON).", + "description": "Fields shared by every v2 step variant (flattened in JSON).", "type": "object", "required": [ "id", "op", "page", - "target" + "to" ], "properties": { "effect": { "anyOf": [ { - "$ref": "#/definitions/StepEffect" + "$ref": "#/definitions/StepEffectV2" }, { "type": "null" @@ -132,20 +460,20 @@ "op": { "type": "string", "enum": [ - "click" + "navigate" ] }, "page": { "description": "Reference into `pages[]`.", "type": "string" }, - "target": { - "$ref": "#/definitions/TargetDescriptor" + "to": { + "type": "string" } } }, { - "description": "Fields shared by every step variant (flattened in JSON).", + "description": "Fields shared by every v2 step variant (flattened in JSON).", "type": "object", "required": [ "id", @@ -157,7 +485,7 @@ "effect": { "anyOf": [ { - "$ref": "#/definitions/StepEffect" + "$ref": "#/definitions/StepEffectV2" }, { "type": "null" @@ -172,7 +500,7 @@ "op": { "type": "string", "enum": [ - "hover" + "click" ] }, "page": { @@ -180,12 +508,12 @@ "type": "string" }, "target": { - "$ref": "#/definitions/TargetDescriptor" + "$ref": "#/definitions/TargetDescriptorV2" } } }, { - "description": "Fields shared by every step variant (flattened in JSON).", + "description": "Fields shared by every v2 step variant (flattened in JSON).", "type": "object", "required": [ "id", @@ -198,7 +526,7 @@ "effect": { "anyOf": [ { - "$ref": "#/definitions/StepEffect" + "$ref": "#/definitions/StepEffectV2" }, { "type": "null" @@ -227,7 +555,7 @@ ] }, "target": { - "$ref": "#/definitions/TargetDescriptor" + "$ref": "#/definitions/TargetDescriptorV2" }, "value": { "type": "string" @@ -235,7 +563,7 @@ } }, { - "description": "Fields shared by every step variant (flattened in JSON).", + "description": "Fields shared by every v2 step variant (flattened in JSON).", "type": "object", "required": [ "id", @@ -248,7 +576,7 @@ "effect": { "anyOf": [ { - "$ref": "#/definitions/StepEffect" + "$ref": "#/definitions/StepEffectV2" }, { "type": "null" @@ -273,16 +601,16 @@ "selection": { "type": "array", "items": { - "$ref": "#/definitions/SelectedOption" + "$ref": "#/definitions/SelectedOptionV2" } }, "target": { - "$ref": "#/definitions/TargetDescriptor" + "$ref": "#/definitions/TargetDescriptorV2" } } }, { - "description": "Fields shared by every step variant (flattened in JSON).", + "description": "Fields shared by every v2 step variant (flattened in JSON).", "type": "object", "required": [ "id", @@ -294,7 +622,7 @@ "effect": { "anyOf": [ { - "$ref": "#/definitions/StepEffect" + "$ref": "#/definitions/StepEffectV2" }, { "type": "null" @@ -331,7 +659,7 @@ "target": { "anyOf": [ { - "$ref": "#/definitions/TargetDescriptor" + "$ref": "#/definitions/TargetDescriptorV2" }, { "type": "null" @@ -342,21 +670,48 @@ } ] }, - "StepEffect": { - "description": "Observed navigation after a step (objective fact only).", + "StopReason": { + "type": "string", + "enum": [ + "user_finish", + "cli_stop" + ] + }, + "TargetDescriptor": { + "description": "Stable semantic handle for an interacted element within a page observation.", "type": "object", - "required": [ - "navigated_to" - ], "properties": { - "navigated_to": { - "description": "Reference into `pages[]` for the destination page.", - "type": "string" + "ctx": { + "type": [ + "string", + "null" + ] + }, + "name": { + "type": [ + "string", + "null" + ] + }, + "ref": { + "type": [ + "string", + "null" + ] + }, + "role": { + "type": [ + "string", + "null" + ] + }, + "unmatched": { + "type": "boolean" } } }, - "TargetDescriptor": { - "description": "Stable semantic handle for an interacted element.\n\n`name` and `nearby_label` are **untrusted page text**.", + "TargetDescriptorV2": { + "description": "Stable semantic handle for an interacted element (v2).\n\n`name` and `nearby_label` are **untrusted page text**.", "type": "object", "required": [ "tag" @@ -402,42 +757,54 @@ "type": "object", "required": [ "entry", - "pages", "recorded_at", - "steps" + "recorder", + "states", + "steps", + "stopped_by", + "version" ], "properties": { "entry": { "$ref": "#/definitions/TraceEntry" }, - "pages": { - "type": "array", - "items": { - "$ref": "#/definitions/PageRef" - } - }, "purpose": { - "description": "Optional user-provided goal from `--purpose` (metadata only).", "type": [ "string", "null" ] }, "recorded_at": { - "description": "RFC 3339 timestamp when recording stopped.", "type": "string" }, + "recorder": { + "$ref": "#/definitions/RecorderInfo" + }, "started_at": { "type": [ "string", "null" ] }, + "states": { + "type": "array", + "items": { + "$ref": "#/definitions/TraceState" + } + }, "steps": { "type": "array", "items": { "$ref": "#/definitions/Step" } + }, + "stopped_by": { + "$ref": "#/definitions/StopReason" + }, + "version": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 } } }, @@ -452,6 +819,88 @@ "type": "string" } } + }, + "TraceState": { + "description": "Page observation dictionary entry — referenced by steps via `state` / `result.state`.", + "type": "object", + "required": [ + "id", + "url" + ], + "properties": { + "body": { + "description": "Wire-only: full page observation (front matter + VOM body + annotations).", + "type": [ + "string", + "null" + ] + }, + "id": { + "type": "string" + }, + "page": { + "description": "Disk-only: filename under the bundle `pages/` directory.", + "type": [ + "string", + "null" + ] + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "truncated": { + "type": "boolean" + }, + "url": { + "type": "string" + } + } + }, + "TraceV2": { + "description": "Persisted user-action trace exported by legacy `tool.record_stop` / `await`.", + "type": "object", + "required": [ + "entry", + "pages", + "recorded_at", + "steps" + ], + "properties": { + "entry": { + "$ref": "#/definitions/TraceEntry" + }, + "pages": { + "type": "array", + "items": { + "$ref": "#/definitions/PageRef" + } + }, + "purpose": { + "type": [ + "string", + "null" + ] + }, + "recorded_at": { + "description": "RFC 3339 timestamp when recording stopped.", + "type": "string" + }, + "started_at": { + "type": [ + "string", + "null" + ] + }, + "steps": { + "type": "array", + "items": { + "$ref": "#/definitions/StepV2" + } + } + } } } } diff --git a/crates/bsk-protocol/schema/trace.json b/crates/bsk-protocol/schema/trace.json index 0a31d67f..f17a0280 100644 --- a/crates/bsk-protocol/schema/trace.json +++ b/crates/bsk-protocol/schema/trace.json @@ -5,45 +5,65 @@ "type": "object", "required": [ "entry", - "pages", "recorded_at", - "steps" + "recorder", + "states", + "steps", + "stopped_by", + "version" ], "properties": { "entry": { "$ref": "#/definitions/TraceEntry" }, - "pages": { - "type": "array", - "items": { - "$ref": "#/definitions/PageRef" - } - }, "purpose": { - "description": "Optional user-provided goal from `--purpose` (metadata only).", "type": [ "string", "null" ] }, "recorded_at": { - "description": "RFC 3339 timestamp when recording stopped.", "type": "string" }, + "recorder": { + "$ref": "#/definitions/RecorderInfo" + }, "started_at": { "type": [ "string", "null" ] }, + "states": { + "type": "array", + "items": { + "$ref": "#/definitions/TraceState" + } + }, "steps": { "type": "array", "items": { "$ref": "#/definitions/Step" } + }, + "stopped_by": { + "$ref": "#/definitions/StopReason" + }, + "version": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 } }, "definitions": { + "FillCommit": { + "type": "string", + "enum": [ + "enter", + "suggestion", + "blur" + ] + }, "KeyModifier": { "description": "Keyboard modifier flags. Multiple flags may be combined; the extension folds them into CDP's bitfield (`alt=1, ctrl=2, meta=4, shift=8`).", "type": "string", @@ -54,25 +74,32 @@ "shift" ] }, - "PageRef": { - "description": "Page context dictionary entry — referenced by steps via `page` id.", + "NavigationCause": { + "type": "string", + "enum": [ + "user_typed", + "link", + "form_submit", + "reload", + "history", + "script", + "browser" + ] + }, + "RecorderInfo": { "type": "object", "required": [ - "id", - "url" + "bsk", + "vom" ], "properties": { - "id": { + "bsk": { "type": "string" }, - "title": { - "type": [ - "string", - "null" - ] - }, - "url": { - "type": "string" + "vom": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 } } }, @@ -101,21 +128,16 @@ "description": "Fields shared by every step variant (flattened in JSON).", "type": "object", "required": [ + "cause", "id", "op", - "page", + "result", + "state", "to" ], "properties": { - "effect": { - "anyOf": [ - { - "$ref": "#/definitions/StepEffect" - }, - { - "type": "null" - } - ] + "cause": { + "$ref": "#/definitions/NavigationCause" }, "id": { "type": "integer", @@ -128,8 +150,11 @@ "navigate" ] }, - "page": { - "description": "Reference into `pages[]`.", + "result": { + "$ref": "#/definitions/StepResult" + }, + "state": { + "description": "Observation id immediately before this action.", "type": "string" }, "to": { @@ -143,20 +168,11 @@ "required": [ "id", "op", - "page", + "result", + "state", "target" ], "properties": { - "effect": { - "anyOf": [ - { - "$ref": "#/definitions/StepEffect" - }, - { - "type": "null" - } - ] - }, "id": { "type": "integer", "format": "uint32", @@ -168,8 +184,11 @@ "click" ] }, - "page": { - "description": "Reference into `pages[]`.", + "result": { + "$ref": "#/definitions/StepResult" + }, + "state": { + "description": "Observation id immediately before this action.", "type": "string" }, "target": { @@ -183,20 +202,11 @@ "required": [ "id", "op", - "page", + "result", + "state", "target" ], "properties": { - "effect": { - "anyOf": [ - { - "$ref": "#/definitions/StepEffect" - }, - { - "type": "null" - } - ] - }, "id": { "type": "integer", "format": "uint32", @@ -208,8 +218,11 @@ "hover" ] }, - "page": { - "description": "Reference into `pages[]`.", + "result": { + "$ref": "#/definitions/StepResult" + }, + "state": { + "description": "Observation id immediately before this action.", "type": "string" }, "target": { @@ -221,22 +234,17 @@ "description": "Fields shared by every step variant (flattened in JSON).", "type": "object", "required": [ + "commit", "id", "op", - "page", + "result", + "state", "target", "value" ], "properties": { - "effect": { - "anyOf": [ - { - "$ref": "#/definitions/StepEffect" - }, - { - "type": "null" - } - ] + "commit": { + "$ref": "#/definitions/FillCommit" }, "id": { "type": "integer", @@ -249,15 +257,15 @@ "fill" ] }, - "page": { - "description": "Reference into `pages[]`.", - "type": "string" - }, "redacted": { - "type": [ - "boolean", - "null" - ] + "type": "boolean" + }, + "result": { + "$ref": "#/definitions/StepResult" + }, + "state": { + "description": "Observation id immediately before this action.", + "type": "string" }, "target": { "$ref": "#/definitions/TargetDescriptor" @@ -273,21 +281,11 @@ "required": [ "id", "op", - "page", - "selection", + "result", + "state", "target" ], "properties": { - "effect": { - "anyOf": [ - { - "$ref": "#/definitions/StepEffect" - }, - { - "type": "null" - } - ] - }, "id": { "type": "integer", "format": "uint32", @@ -299,9 +297,8 @@ "select" ] }, - "page": { - "description": "Reference into `pages[]`.", - "type": "string" + "result": { + "$ref": "#/definitions/StepResult" }, "selection": { "type": "array", @@ -309,6 +306,10 @@ "$ref": "#/definitions/SelectedOption" } }, + "state": { + "description": "Observation id immediately before this action.", + "type": "string" + }, "target": { "$ref": "#/definitions/TargetDescriptor" } @@ -321,19 +322,10 @@ "id", "key", "op", - "page" + "result", + "state" ], "properties": { - "effect": { - "anyOf": [ - { - "$ref": "#/definitions/StepEffect" - }, - { - "type": "null" - } - ] - }, "id": { "type": "integer", "format": "uint32", @@ -357,8 +349,11 @@ "press" ] }, - "page": { - "description": "Reference into `pages[]`.", + "result": { + "$ref": "#/definitions/StepResult" + }, + "state": { + "description": "Observation id immediately before this action.", "type": "string" }, "target": { @@ -372,48 +367,74 @@ ] } } + }, + { + "description": "Fields shared by every step variant (flattened in JSON).", + "type": "object", + "required": [ + "id", + "op", + "result", + "state" + ], + "properties": { + "id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "op": { + "type": "string", + "enum": [ + "scroll" + ] + }, + "result": { + "$ref": "#/definitions/StepResult" + }, + "state": { + "description": "Observation id immediately before this action.", + "type": "string" + } + } } ] }, - "StepEffect": { - "description": "Observed navigation after a step (objective fact only).", + "StepResult": { "type": "object", "required": [ - "navigated_to" + "state" ], "properties": { - "navigated_to": { - "description": "Reference into `pages[]` for the destination page.", + "state": { "type": "string" } } }, + "StopReason": { + "type": "string", + "enum": [ + "user_finish", + "cli_stop" + ] + }, "TargetDescriptor": { - "description": "Stable semantic handle for an interacted element.\n\n`name` and `nearby_label` are **untrusted page text**.", + "description": "Stable semantic handle for an interacted element within a page observation.", "type": "object", - "required": [ - "tag" - ], "properties": { - "name": { - "type": [ - "string", - "null" - ] - }, - "name_attr": { + "ctx": { "type": [ "string", "null" ] }, - "nearby_label": { + "name": { "type": [ "string", "null" ] }, - "placeholder": { + "ref": { "type": [ "string", "null" @@ -425,8 +446,8 @@ "null" ] }, - "tag": { - "type": "string" + "unmatched": { + "type": "boolean" } } }, @@ -441,6 +462,45 @@ "type": "string" } } + }, + "TraceState": { + "description": "Page observation dictionary entry — referenced by steps via `state` / `result.state`.", + "type": "object", + "required": [ + "id", + "url" + ], + "properties": { + "body": { + "description": "Wire-only: full page observation (front matter + VOM body + annotations).", + "type": [ + "string", + "null" + ] + }, + "id": { + "type": "string" + }, + "page": { + "description": "Disk-only: filename under the bundle `pages/` directory.", + "type": [ + "string", + "null" + ] + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "truncated": { + "type": "boolean" + }, + "url": { + "type": "string" + } + } } } } diff --git a/crates/bsk-protocol/schema/trace_step.json b/crates/bsk-protocol/schema/trace_step.json index 69dbec90..79f5e1e7 100644 --- a/crates/bsk-protocol/schema/trace_step.json +++ b/crates/bsk-protocol/schema/trace_step.json @@ -7,21 +7,16 @@ "description": "Fields shared by every step variant (flattened in JSON).", "type": "object", "required": [ + "cause", "id", "op", - "page", + "result", + "state", "to" ], "properties": { - "effect": { - "anyOf": [ - { - "$ref": "#/definitions/StepEffect" - }, - { - "type": "null" - } - ] + "cause": { + "$ref": "#/definitions/NavigationCause" }, "id": { "type": "integer", @@ -34,8 +29,11 @@ "navigate" ] }, - "page": { - "description": "Reference into `pages[]`.", + "result": { + "$ref": "#/definitions/StepResult" + }, + "state": { + "description": "Observation id immediately before this action.", "type": "string" }, "to": { @@ -49,20 +47,11 @@ "required": [ "id", "op", - "page", + "result", + "state", "target" ], "properties": { - "effect": { - "anyOf": [ - { - "$ref": "#/definitions/StepEffect" - }, - { - "type": "null" - } - ] - }, "id": { "type": "integer", "format": "uint32", @@ -74,8 +63,11 @@ "click" ] }, - "page": { - "description": "Reference into `pages[]`.", + "result": { + "$ref": "#/definitions/StepResult" + }, + "state": { + "description": "Observation id immediately before this action.", "type": "string" }, "target": { @@ -89,20 +81,11 @@ "required": [ "id", "op", - "page", + "result", + "state", "target" ], "properties": { - "effect": { - "anyOf": [ - { - "$ref": "#/definitions/StepEffect" - }, - { - "type": "null" - } - ] - }, "id": { "type": "integer", "format": "uint32", @@ -114,8 +97,11 @@ "hover" ] }, - "page": { - "description": "Reference into `pages[]`.", + "result": { + "$ref": "#/definitions/StepResult" + }, + "state": { + "description": "Observation id immediately before this action.", "type": "string" }, "target": { @@ -127,22 +113,17 @@ "description": "Fields shared by every step variant (flattened in JSON).", "type": "object", "required": [ + "commit", "id", "op", - "page", + "result", + "state", "target", "value" ], "properties": { - "effect": { - "anyOf": [ - { - "$ref": "#/definitions/StepEffect" - }, - { - "type": "null" - } - ] + "commit": { + "$ref": "#/definitions/FillCommit" }, "id": { "type": "integer", @@ -155,15 +136,15 @@ "fill" ] }, - "page": { - "description": "Reference into `pages[]`.", - "type": "string" - }, "redacted": { - "type": [ - "boolean", - "null" - ] + "type": "boolean" + }, + "result": { + "$ref": "#/definitions/StepResult" + }, + "state": { + "description": "Observation id immediately before this action.", + "type": "string" }, "target": { "$ref": "#/definitions/TargetDescriptor" @@ -179,21 +160,11 @@ "required": [ "id", "op", - "page", - "selection", + "result", + "state", "target" ], "properties": { - "effect": { - "anyOf": [ - { - "$ref": "#/definitions/StepEffect" - }, - { - "type": "null" - } - ] - }, "id": { "type": "integer", "format": "uint32", @@ -205,9 +176,8 @@ "select" ] }, - "page": { - "description": "Reference into `pages[]`.", - "type": "string" + "result": { + "$ref": "#/definitions/StepResult" }, "selection": { "type": "array", @@ -215,6 +185,10 @@ "$ref": "#/definitions/SelectedOption" } }, + "state": { + "description": "Observation id immediately before this action.", + "type": "string" + }, "target": { "$ref": "#/definitions/TargetDescriptor" } @@ -227,19 +201,10 @@ "id", "key", "op", - "page" + "result", + "state" ], "properties": { - "effect": { - "anyOf": [ - { - "$ref": "#/definitions/StepEffect" - }, - { - "type": "null" - } - ] - }, "id": { "type": "integer", "format": "uint32", @@ -263,8 +228,11 @@ "press" ] }, - "page": { - "description": "Reference into `pages[]`.", + "result": { + "$ref": "#/definitions/StepResult" + }, + "state": { + "description": "Observation id immediately before this action.", "type": "string" }, "target": { @@ -278,9 +246,47 @@ ] } } + }, + { + "description": "Fields shared by every step variant (flattened in JSON).", + "type": "object", + "required": [ + "id", + "op", + "result", + "state" + ], + "properties": { + "id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "op": { + "type": "string", + "enum": [ + "scroll" + ] + }, + "result": { + "$ref": "#/definitions/StepResult" + }, + "state": { + "description": "Observation id immediately before this action.", + "type": "string" + } + } } ], "definitions": { + "FillCommit": { + "type": "string", + "enum": [ + "enter", + "suggestion", + "blur" + ] + }, "KeyModifier": { "description": "Keyboard modifier flags. Multiple flags may be combined; the extension folds them into CDP's bitfield (`alt=1, ctrl=2, meta=4, shift=8`).", "type": "string", @@ -291,6 +297,18 @@ "shift" ] }, + "NavigationCause": { + "type": "string", + "enum": [ + "user_typed", + "link", + "form_submit", + "reload", + "history", + "script", + "browser" + ] + }, "SelectedOption": { "description": "One selected option (`select` op).", "type": "object", @@ -309,45 +327,34 @@ } } }, - "StepEffect": { - "description": "Observed navigation after a step (objective fact only).", + "StepResult": { "type": "object", "required": [ - "navigated_to" + "state" ], "properties": { - "navigated_to": { - "description": "Reference into `pages[]` for the destination page.", + "state": { "type": "string" } } }, "TargetDescriptor": { - "description": "Stable semantic handle for an interacted element.\n\n`name` and `nearby_label` are **untrusted page text**.", + "description": "Stable semantic handle for an interacted element within a page observation.", "type": "object", - "required": [ - "tag" - ], "properties": { - "name": { - "type": [ - "string", - "null" - ] - }, - "name_attr": { + "ctx": { "type": [ "string", "null" ] }, - "nearby_label": { + "name": { "type": [ "string", "null" ] }, - "placeholder": { + "ref": { "type": [ "string", "null" @@ -359,8 +366,8 @@ "null" ] }, - "tag": { - "type": "string" + "unmatched": { + "type": "boolean" } } } diff --git a/crates/bsk-protocol/schema/trace_v2.json b/crates/bsk-protocol/schema/trace_v2.json new file mode 100644 index 00000000..22b0b328 --- /dev/null +++ b/crates/bsk-protocol/schema/trace_v2.json @@ -0,0 +1,405 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "TraceV2", + "description": "Persisted user-action trace exported by legacy `tool.record_stop` / `await`.", + "type": "object", + "required": [ + "entry", + "pages", + "recorded_at", + "steps" + ], + "properties": { + "entry": { + "$ref": "#/definitions/TraceEntry" + }, + "pages": { + "type": "array", + "items": { + "$ref": "#/definitions/PageRef" + } + }, + "purpose": { + "type": [ + "string", + "null" + ] + }, + "recorded_at": { + "description": "RFC 3339 timestamp when recording stopped.", + "type": "string" + }, + "started_at": { + "type": [ + "string", + "null" + ] + }, + "steps": { + "type": "array", + "items": { + "$ref": "#/definitions/StepV2" + } + } + }, + "definitions": { + "KeyModifier": { + "description": "Keyboard modifier flags. Multiple flags may be combined; the extension folds them into CDP's bitfield (`alt=1, ctrl=2, meta=4, shift=8`).", + "type": "string", + "enum": [ + "alt", + "ctrl", + "meta", + "shift" + ] + }, + "PageRef": { + "description": "Page context dictionary entry — referenced by steps via `page` id.", + "type": "object", + "required": [ + "id", + "url" + ], + "properties": { + "id": { + "type": "string" + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "url": { + "type": "string" + } + } + }, + "SelectedOptionV2": { + "description": "One selected option (`select` op).", + "type": "object", + "required": [ + "value" + ], + "properties": { + "label": { + "type": [ + "string", + "null" + ] + }, + "value": { + "type": "string" + } + } + }, + "StepEffectV2": { + "description": "Observed navigation after a step (objective fact only).", + "type": "object", + "required": [ + "navigated_to" + ], + "properties": { + "navigated_to": { + "description": "Reference into `pages[]` for the destination page.", + "type": "string" + } + } + }, + "StepV2": { + "description": "One recorded user action — discriminated union by `op` (v2).", + "oneOf": [ + { + "description": "Fields shared by every v2 step variant (flattened in JSON).", + "type": "object", + "required": [ + "id", + "op", + "page", + "to" + ], + "properties": { + "effect": { + "anyOf": [ + { + "$ref": "#/definitions/StepEffectV2" + }, + { + "type": "null" + } + ] + }, + "id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "op": { + "type": "string", + "enum": [ + "navigate" + ] + }, + "page": { + "description": "Reference into `pages[]`.", + "type": "string" + }, + "to": { + "type": "string" + } + } + }, + { + "description": "Fields shared by every v2 step variant (flattened in JSON).", + "type": "object", + "required": [ + "id", + "op", + "page", + "target" + ], + "properties": { + "effect": { + "anyOf": [ + { + "$ref": "#/definitions/StepEffectV2" + }, + { + "type": "null" + } + ] + }, + "id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "op": { + "type": "string", + "enum": [ + "click" + ] + }, + "page": { + "description": "Reference into `pages[]`.", + "type": "string" + }, + "target": { + "$ref": "#/definitions/TargetDescriptorV2" + } + } + }, + { + "description": "Fields shared by every v2 step variant (flattened in JSON).", + "type": "object", + "required": [ + "id", + "op", + "page", + "target", + "value" + ], + "properties": { + "effect": { + "anyOf": [ + { + "$ref": "#/definitions/StepEffectV2" + }, + { + "type": "null" + } + ] + }, + "id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "op": { + "type": "string", + "enum": [ + "fill" + ] + }, + "page": { + "description": "Reference into `pages[]`.", + "type": "string" + }, + "redacted": { + "type": [ + "boolean", + "null" + ] + }, + "target": { + "$ref": "#/definitions/TargetDescriptorV2" + }, + "value": { + "type": "string" + } + } + }, + { + "description": "Fields shared by every v2 step variant (flattened in JSON).", + "type": "object", + "required": [ + "id", + "op", + "page", + "selection", + "target" + ], + "properties": { + "effect": { + "anyOf": [ + { + "$ref": "#/definitions/StepEffectV2" + }, + { + "type": "null" + } + ] + }, + "id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "op": { + "type": "string", + "enum": [ + "select" + ] + }, + "page": { + "description": "Reference into `pages[]`.", + "type": "string" + }, + "selection": { + "type": "array", + "items": { + "$ref": "#/definitions/SelectedOptionV2" + } + }, + "target": { + "$ref": "#/definitions/TargetDescriptorV2" + } + } + }, + { + "description": "Fields shared by every v2 step variant (flattened in JSON).", + "type": "object", + "required": [ + "id", + "key", + "op", + "page" + ], + "properties": { + "effect": { + "anyOf": [ + { + "$ref": "#/definitions/StepEffectV2" + }, + { + "type": "null" + } + ] + }, + "id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "key": { + "type": "string" + }, + "modifiers": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/KeyModifier" + } + }, + "op": { + "type": "string", + "enum": [ + "press" + ] + }, + "page": { + "description": "Reference into `pages[]`.", + "type": "string" + }, + "target": { + "anyOf": [ + { + "$ref": "#/definitions/TargetDescriptorV2" + }, + { + "type": "null" + } + ] + } + } + } + ] + }, + "TargetDescriptorV2": { + "description": "Stable semantic handle for an interacted element (v2).\n\n`name` and `nearby_label` are **untrusted page text**.", + "type": "object", + "required": [ + "tag" + ], + "properties": { + "name": { + "type": [ + "string", + "null" + ] + }, + "name_attr": { + "type": [ + "string", + "null" + ] + }, + "nearby_label": { + "type": [ + "string", + "null" + ] + }, + "placeholder": { + "type": [ + "string", + "null" + ] + }, + "role": { + "type": [ + "string", + "null" + ] + }, + "tag": { + "type": "string" + } + } + }, + "TraceEntry": { + "description": "Recording entry point — first URL the flow starts from.", + "type": "object", + "required": [ + "start_url" + ], + "properties": { + "start_url": { + "type": "string" + } + } + } + } +} diff --git a/crates/bsk-protocol/src/bin/dump-schema.rs b/crates/bsk-protocol/src/bin/dump-schema.rs index de9b7f25..2071a057 100644 --- a/crates/bsk-protocol/src/bin/dump-schema.rs +++ b/crates/bsk-protocol/src/bin/dump-schema.rs @@ -113,6 +113,7 @@ fn main() { dump!(RequestHelpParams, "tool_request_help_params"); dump!(RequestHelpResult, "tool_request_help_result"); + dump!(TraceV2, "trace_v2"); dump!(Trace, "trace"); dump!(Step, "trace_step"); dump!(RecordStartParams, "tool_record_start_params"); diff --git a/crates/bsk-protocol/src/tools/mod.rs b/crates/bsk-protocol/src/tools/mod.rs index a9c04dbb..67e8f53f 100644 --- a/crates/bsk-protocol/src/tools/mod.rs +++ b/crates/bsk-protocol/src/tools/mod.rs @@ -9,6 +9,7 @@ pub mod navigation; pub mod network; pub mod observation; pub mod record; +pub mod record_v2; pub mod script; pub mod session; pub mod tabs; diff --git a/crates/bsk-protocol/src/tools/record.rs b/crates/bsk-protocol/src/tools/record.rs index 6dc2e097..74cd6343 100644 --- a/crates/bsk-protocol/src/tools/record.rs +++ b/crates/bsk-protocol/src/tools/record.rs @@ -1,35 +1,39 @@ //! Semantic user-action recording (`tool.record_start` / `stop` / `await`). //! -//! Trace v2 is a **record-only** log of user actions: what was clicked, filled, -//! selected, and where navigation occurred. No LLM runs during recording, so -//! variable-vs-constant classification is **not** stored — executing agents -//! infer that at run time from raw values and control names. +//! Trace v3 is a **state-action-state** chain: each step binds to page +//! observations (VOM) captured before and after the action. use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use super::interaction::KeyModifier; +pub use crate::record_v2::{ + PageRef, SelectedOptionV2, StepCommonV2, StepEffectV2, StepV2, TargetDescriptorV2, TraceV2, +}; + +pub const TRACE_VERSION: u32 = 3; +pub const TRACE_VERSION_V2: u32 = 2; +pub const DEFAULT_TRACE_VERSION: u32 = 2; +pub const VOM_FORMAT_VERSION: u32 = 1; + // --------------------------------------------------------------------------- // Target // --------------------------------------------------------------------------- -/// Stable semantic handle for an interacted element. -/// -/// `name` and `nearby_label` are **untrusted page text**. +/// Stable semantic handle for an interacted element within a page observation. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct TargetDescriptor { + #[serde(default, skip_serializing_if = "Option::is_none", rename = "ref")] + pub element_ref: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub role: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub name: Option, - pub tag: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub name_attr: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub placeholder: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub nearby_label: Option, + pub ctx: Option, + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub unmatched: bool, } // --------------------------------------------------------------------------- @@ -42,44 +46,82 @@ pub struct TraceEntry { pub start_url: String, } -/// Page context dictionary entry — referenced by steps via `page` id. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] -pub struct PageRef { +pub struct RecorderInfo { + pub bsk: String, + pub vom: u32, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum StopReason { + UserFinish, + CliStop, +} + +/// Page observation dictionary entry — referenced by steps via `state` / `result.state`. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct TraceState { pub id: String, pub url: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub title: Option, -} - -/// One selected option (`select` op). -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] -pub struct SelectedOption { - pub value: String, + /// Wire-only: full page observation (front matter + VOM body + annotations). #[serde(default, skip_serializing_if = "Option::is_none")] - pub label: Option, + pub body: Option, + /// Disk-only: filename under the bundle `pages/` directory. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub page: Option, + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub truncated: bool, } -/// Observed navigation after a step (objective fact only). #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] -pub struct StepEffect { - /// Reference into `pages[]` for the destination page. - pub navigated_to: String, +pub struct StepResult { + pub state: String, } /// Fields shared by every step variant (flattened in JSON). #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct StepCommon { pub id: u32, - /// Reference into `pages[]`. - pub page: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub effect: Option, + /// Observation id immediately before this action. + pub state: String, + pub result: StepResult, } // --------------------------------------------------------------------------- // Step op-specific payloads // --------------------------------------------------------------------------- +/// One selected option (`select` op). +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct SelectedOption { + pub value: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub label: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum NavigationCause { + UserTyped, + Link, + FormSubmit, + Reload, + History, + Script, + Browser, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum FillCommit { + Enter, + Suggestion, + Blur, +} + /// One recorded user action — discriminated union by `op`. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] #[serde(tag = "op", rename_all = "snake_case")] @@ -88,6 +130,7 @@ pub enum Step { #[serde(flatten)] common: StepCommon, to: String, + cause: NavigationCause, }, Click { #[serde(flatten)] @@ -104,13 +147,15 @@ pub enum Step { common: StepCommon, target: TargetDescriptor, value: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - redacted: Option, + commit: FillCommit, + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + redacted: bool, }, Select { #[serde(flatten)] common: StepCommon, target: TargetDescriptor, + #[serde(default, skip_serializing_if = "Vec::is_empty")] selection: Vec, }, Press { @@ -122,6 +167,10 @@ pub enum Step { #[serde(default, skip_serializing_if = "Option::is_none")] target: Option, }, + Scroll { + #[serde(flatten)] + common: StepCommon, + }, } // --------------------------------------------------------------------------- @@ -131,15 +180,16 @@ pub enum Step { /// Persisted user-action trace exported by `tool.record_stop` / `await`. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct Trace { - /// RFC 3339 timestamp when recording stopped. - pub recorded_at: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub started_at: Option, - /// Optional user-provided goal from `--purpose` (metadata only). + pub version: u32, #[serde(default, skip_serializing_if = "Option::is_none")] pub purpose: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub started_at: Option, + pub recorded_at: String, + pub stopped_by: StopReason, pub entry: TraceEntry, - pub pages: Vec, + pub recorder: RecorderInfo, + pub states: Vec, pub steps: Vec, } @@ -156,6 +206,13 @@ pub struct RecordStartParams { pub url: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub purpose: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub max_page_tokens: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub redact_values: Option, + /// Desired trace export format. Omitted ⇒ v2; `3` ⇒ state-linked v3 bundle. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub trace_version: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] @@ -169,9 +226,100 @@ pub struct RecordStopParams { pub session_id: String, } +/// Wire trace payload — v2 (legacy `pages[]`) or v3 (`version: 3`, `states[]`). +#[derive(Debug, Clone, PartialEq)] +pub enum RecordedTrace { + V2(TraceV2), + V3(Trace), +} + +impl RecordedTrace { + pub fn classify_value(v: &serde_json::Value) -> Result { + if let Some(ver) = v.get("version").and_then(|x| x.as_u64()) { + if ver == u64::from(TRACE_VERSION) { + return serde_json::from_value(v.clone()) + .map(RecordedTrace::V3) + .map_err(|e| e.to_string()); + } + return Err(format!("unsupported trace version {ver}")); + } + if v.get("pages").is_some() && v.get("states").is_none() { + return serde_json::from_value(v.clone()) + .map(RecordedTrace::V2) + .map_err(|e| e.to_string()); + } + if v.get("states").is_some() { + return serde_json::from_value(v.clone()) + .map(RecordedTrace::V3) + .map_err(|e| e.to_string()); + } + Err("ambiguous or unparseable trace".into()) + } + + pub fn is_v3(&self) -> bool { + matches!(self, RecordedTrace::V3(_)) + } + + pub fn as_v3(&self) -> Option<&Trace> { + match self { + RecordedTrace::V3(t) => Some(t), + RecordedTrace::V2(_) => None, + } + } + + pub fn as_v2(&self) -> Option<&TraceV2> { + match self { + RecordedTrace::V2(t) => Some(t), + RecordedTrace::V3(_) => None, + } + } +} + +impl Serialize for RecordedTrace { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + RecordedTrace::V2(t) => t.serialize(serializer), + RecordedTrace::V3(t) => t.serialize(serializer), + } + } +} + +impl<'de> Deserialize<'de> for RecordedTrace { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let value = serde_json::Value::deserialize(deserializer)?; + Self::classify_value(&value).map_err(serde::de::Error::custom) + } +} + +impl JsonSchema for RecordedTrace { + fn schema_name() -> String { + "RecordedTrace".into() + } + + fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::schema::Schema { + schemars::schema::SchemaObject { + subschemas: Some(Box::new(schemars::schema::SubschemaValidation { + one_of: Some(vec![ + generator.subschema_for::(), + generator.subschema_for::(), + ]), + ..Default::default() + })), + ..Default::default() + } + .into() + } +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct RecordStopResult { - pub trace: Trace, + pub trace: RecordedTrace, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] @@ -183,7 +331,7 @@ pub struct RecordAwaitParams { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct RecordAwaitResult { - pub trace: Trace, + pub trace: RecordedTrace, } // --------------------------------------------------------------------------- @@ -195,81 +343,79 @@ mod tests { use super::*; use serde_json::json; - fn sample_common(id: u32) -> StepCommon { + fn sample_common(id: u32, state: &str, result_state: &str) -> StepCommon { StepCommon { id, - page: "p1".into(), - effect: None, + state: state.into(), + result: StepResult { + state: result_state.into(), + }, } } fn sample_target() -> TargetDescriptor { TargetDescriptor { + element_ref: Some("e21".into()), role: Some("button".into()), name: Some("发布".into()), - tag: "button".into(), - name_attr: None, - placeholder: None, - nearby_label: None, + ctx: Some("金桔柠檬 6 号".into()), + unmatched: false, } } fn sample_trace() -> Trace { Trace { - recorded_at: "2026-07-17T09:01:10Z".into(), - started_at: Some("2026-07-17T09:00:00Z".into()), - purpose: Some("发布一篇文章".into()), + version: TRACE_VERSION, + purpose: Some("把草稿商品发布上架".into()), + started_at: Some("2026-08-10T02:10:41.080Z".into()), + recorded_at: "2026-08-10T02:12:55.360Z".into(), + stopped_by: StopReason::UserFinish, entry: TraceEntry { - start_url: "https://x.com/editor".into(), + start_url: "https://example.com/".into(), + }, + recorder: RecorderInfo { + bsk: "0.1.10".into(), + vom: VOM_FORMAT_VERSION, }, - pages: vec![ - PageRef { - id: "p1".into(), - url: "https://x.com/editor".into(), - title: Some("写文章".into()), + states: vec![ + TraceState { + id: "s1".into(), + url: "https://example.com/".into(), + title: Some("Example Domain".into()), + body: None, + page: Some("s1.vom.txt".into()), + truncated: false, }, - PageRef { - id: "p2".into(), - url: "https://x.com/p/99".into(), - title: None, + TraceState { + id: "s2".into(), + url: "https://shop.example.com/products?status=draft".into(), + title: Some("商品管理".into()), + body: None, + page: Some("s2.vom.txt".into()), + truncated: false, }, ], steps: vec![ + Step::Navigate { + common: sample_common(1, "s1", "s2"), + to: "https://shop.example.com/products?status=draft".into(), + cause: NavigationCause::UserTyped, + }, Step::Fill { - common: sample_common(1), + common: sample_common(2, "s2", "s2"), target: TargetDescriptor { + element_ref: Some("e12".into()), role: Some("textbox".into()), - name: Some("标题".into()), - tag: "input".into(), - name_attr: None, - placeholder: None, - nearby_label: None, - }, - value: "我的第一篇文章".into(), - redacted: None, - }, - Step::Select { - common: sample_common(3), - target: TargetDescriptor { - role: Some("combobox".into()), - name: Some("分类".into()), - tag: "select".into(), - name_attr: None, - placeholder: None, - nearby_label: None, + name: Some("搜索商品".into()), + ctx: None, + unmatched: false, }, - selection: vec![SelectedOption { - value: "tech".into(), - label: Some("技术分享".into()), - }], + value: "金桔柠檬".into(), + commit: FillCommit::Enter, + redacted: false, }, Step::Click { - common: StepCommon { - effect: Some(StepEffect { - navigated_to: "p2".into(), - }), - ..sample_common(4) - }, + common: sample_common(3, "s2", "s2"), target: sample_target(), }, ], @@ -279,35 +425,37 @@ mod tests { #[test] fn step_click_round_trips() { let step = Step::Click { - common: sample_common(1), + common: sample_common(1, "s1", "s2"), target: sample_target(), }; let v = serde_json::to_value(&step).unwrap(); assert_eq!(v.get("op").and_then(|v| v.as_str()), Some("click")); - assert_eq!(v.get("page").and_then(|v| v.as_str()), Some("p1")); - assert!(v.get("key").is_none()); + assert_eq!(v.get("state").and_then(|v| v.as_str()), Some("s1")); + assert_eq!(v["result"]["state"], "s2"); + assert_eq!(v["target"]["ref"], "e21"); let round: Step = serde_json::from_value(v).unwrap(); assert_eq!(round, step); } #[test] - fn step_fill_with_raw_value_round_trips() { + fn step_fill_with_commit_round_trips() { let step = Step::Fill { - common: sample_common(2), + common: sample_common(2, "s2", "s3"), target: TargetDescriptor { + element_ref: Some("e12".into()), role: Some("textbox".into()), - name: Some("服务名称".into()), - tag: "input".into(), - name_attr: Some("serviceName".into()), - placeholder: None, - nearby_label: None, + name: Some("搜索商品".into()), + ctx: None, + unmatched: false, }, - value: "my-svc".into(), - redacted: None, + value: "browser skill".into(), + commit: FillCommit::Enter, + redacted: false, }; let v = serde_json::to_value(&step).unwrap(); assert_eq!(v["op"], "fill"); - assert_eq!(v["value"], "my-svc"); + assert_eq!(v["commit"], "enter"); + assert!(v.get("redacted").is_none()); let round: Step = serde_json::from_value(v).unwrap(); assert_eq!(round, step); } @@ -315,17 +463,17 @@ mod tests { #[test] fn step_fill_password_is_redacted() { let step = Step::Fill { - common: sample_common(1), + common: sample_common(1, "s1", "s1"), target: TargetDescriptor { + element_ref: Some("e3".into()), role: Some("textbox".into()), name: Some("密码".into()), - tag: "input".into(), - name_attr: None, - placeholder: None, - nearby_label: None, + ctx: None, + unmatched: false, }, value: "***".into(), - redacted: Some(true), + commit: FillCommit::Blur, + redacted: true, }; let v = serde_json::to_value(&step).unwrap(); assert_eq!(v["value"], "***"); @@ -333,131 +481,140 @@ mod tests { } #[test] - fn step_select_uses_object_array() { - let step = Step::Select { - common: sample_common(3), - target: TargetDescriptor { - role: Some("combobox".into()), - name: Some("分类".into()), - tag: "select".into(), - name_attr: None, - placeholder: None, - nearby_label: None, - }, - selection: vec![SelectedOption { - value: "tech".into(), - label: Some("技术分享".into()), - }], + fn step_navigate_with_cause_round_trips() { + let step = Step::Navigate { + common: sample_common(1, "s1", "s2"), + to: "https://example.com".into(), + cause: NavigationCause::UserTyped, }; let v = serde_json::to_value(&step).unwrap(); - assert_eq!(v["selection"][0]["value"], "tech"); - assert_eq!(v["selection"][0]["label"], "技术分享"); + assert_eq!(v["op"], "navigate"); + assert_eq!(v["cause"], "user_typed"); let round: Step = serde_json::from_value(v).unwrap(); assert_eq!(round, step); } #[test] - fn step_navigate_round_trips() { - let step = Step::Navigate { - common: sample_common(1), - to: "https://example.com".into(), + fn trace_v3_round_trips() { + let trace = sample_trace(); + let v = serde_json::to_value(&trace).unwrap(); + assert_eq!(v.get("version").and_then(|v| v.as_u64()), Some(3)); + assert!(v.get("pages").is_none()); + assert_eq!(v["recorder"]["vom"], 1); + assert_eq!(v["states"].as_array().unwrap().len(), 2); + let round: Trace = serde_json::from_value(v).unwrap(); + assert_eq!(round, trace); + } + + #[test] + fn default_fields_are_omitted() { + let step = Step::Click { + common: sample_common(1, "s1", "s1"), + target: TargetDescriptor { + element_ref: Some("e1".into()), + role: Some("button".into()), + name: Some("OK".into()), + ctx: None, + unmatched: false, + }, }; let v = serde_json::to_value(&step).unwrap(); - assert_eq!(v["op"], "navigate"); - assert_eq!(v["to"], "https://example.com"); - let round: Step = serde_json::from_value(v).unwrap(); - assert_eq!(round, step); + assert!(v.get("unmatched").is_none()); + assert!(v["target"].get("ctx").is_none()); + assert!(v["target"].get("unmatched").is_none()); } #[test] - fn step_press_with_modifiers_round_trips() { - let step = Step::Press { - common: StepCommon { - effect: Some(StepEffect { - navigated_to: "p2".into(), - }), - ..sample_common(2) + fn unmatched_target_serializes_flag() { + let step = Step::Click { + common: sample_common(1, "s1", "s2"), + target: TargetDescriptor { + element_ref: None, + role: Some("button".into()), + name: Some("发布".into()), + ctx: None, + unmatched: true, }, - key: "Enter".into(), - modifiers: Some(vec![KeyModifier::Ctrl, KeyModifier::Shift]), - target: Some(sample_target()), }; let v = serde_json::to_value(&step).unwrap(); - assert_eq!(v["key"], "Enter"); - assert_eq!(v["modifiers"], json!(["ctrl", "shift"])); - assert_eq!(v["effect"]["navigated_to"], "p2"); - let round: Step = serde_json::from_value(v).unwrap(); - assert_eq!(round, step); + assert_eq!(v["target"]["unmatched"], true); + assert!(v["target"].get("ref").is_none()); } #[test] - fn trace_record_only_round_trips() { - let trace = sample_trace(); - let v = serde_json::to_value(&trace).unwrap(); - assert!(v.get("version").is_none()); - assert_eq!( - v.get("purpose").and_then(|v| v.as_str()), - Some("发布一篇文章") - ); - assert!(v.get("parameters").is_none()); - assert!(v.get("site").is_none()); - assert!(v.get("goal").is_none()); - assert_eq!( - v.get("entry") - .and_then(|e| e.get("start_url")) - .and_then(|u| u.as_str()), - Some("https://x.com/editor") - ); - let round: Trace = serde_json::from_value(v).unwrap(); - assert_eq!(round, trace); + fn recorded_trace_classifies_v2_and_v3() { + let v2 = json!({ + "recorded_at": "2026-07-21T08:00:00Z", + "entry": { "start_url": "https://example.com/" }, + "pages": [{ "id": "p1", "url": "https://example.com/" }], + "steps": [] + }); + match RecordedTrace::classify_value(&v2).unwrap() { + RecordedTrace::V2(_) => {} + other => panic!("expected v2, got {other:?}"), + } + + let v3 = json!({ + "version": 3, + "recorded_at": "2026-07-21T08:00:00Z", + "stopped_by": "user_finish", + "entry": { "start_url": "https://example.com/" }, + "recorder": { "bsk": "0.1.10", "vom": 1 }, + "states": [], + "steps": [] + }); + match RecordedTrace::classify_value(&v3).unwrap() { + RecordedTrace::V3(t) => assert_eq!(t.version, 3), + other => panic!("expected v3, got {other:?}"), + } } #[test] - fn discriminated_union_click_ignores_extra_key_field() { - let bad = json!({ - "op": "click", - "id": 1, - "page": "p1", - "target": { "tag": "button", "name": "OK" }, - "key": "Enter" - }); - let step: Step = serde_json::from_value(bad).unwrap(); - assert!(matches!(step, Step::Click { .. })); - let v = serde_json::to_value(&step).unwrap(); - assert!(v.get("key").is_none()); + fn recorded_trace_schema_includes_v2_and_v3() { + let schema = serde_json::to_value(schemars::schema_for!(RecordStopResult)).unwrap(); + let variants = schema["definitions"]["RecordedTrace"]["oneOf"] + .as_array() + .expect("RecordedTrace schema should use oneOf"); + + assert_eq!(variants.len(), 2); } #[test] fn extension_trace_deserializes() { let v = json!({ + "version": 3, "recorded_at": "2026-07-21T08:00:00Z", "started_at": "2026-07-21T07:59:00Z", "purpose": "demo", + "stopped_by": "user_finish", "entry": { "start_url": "https://example.com/editor" }, - "pages": [ - { "id": "p1", "url": "https://example.com/editor" }, - { "id": "p2", "url": "https://example.com/p/99" } + "recorder": { "bsk": "0.1.10", "vom": 1 }, + "states": [ + { "id": "s1", "url": "https://example.com/editor", "page": "s1.vom.txt" }, + { "id": "s2", "url": "https://example.com/p/99", "page": "s2.vom.txt" } ], "steps": [ { "op": "fill", "id": 1, - "page": "p1", - "target": { "tag": "input", "role": "textbox", "name": "标题" }, - "value": "hello" + "state": "s1", + "result": { "state": "s1" }, + "target": { "ref": "e1", "role": "textbox", "name": "标题" }, + "value": "hello", + "commit": "blur" }, { "op": "click", "id": 2, - "page": "p1", - "target": { "tag": "button", "role": "button", "name": "发布" }, - "effect": { "navigated_to": "p2" } + "state": "s1", + "result": { "state": "s2" }, + "target": { "ref": "e2", "role": "button", "name": "发布" } } ] }); let trace: Trace = serde_json::from_value(v).unwrap(); - assert_eq!(trace.entry.start_url, "https://example.com/editor"); - assert_eq!(trace.pages.len(), 2); + assert_eq!(trace.version, 3); + assert_eq!(trace.states.len(), 2); assert_eq!(trace.steps.len(), 2); } } diff --git a/crates/bsk-protocol/src/tools/record_v2.rs b/crates/bsk-protocol/src/tools/record_v2.rs new file mode 100644 index 00000000..77b3a37d --- /dev/null +++ b/crates/bsk-protocol/src/tools/record_v2.rs @@ -0,0 +1,215 @@ +//! Trace v2 — record-only user-action log with `pages[]` and step `page` refs. +//! +//! Legacy wire format: no top-level `version` field. New peers omit +//! `trace_version` on `record_start` to request this shape. + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use super::interaction::KeyModifier; +use super::record::TraceEntry; + +/// Stable semantic handle for an interacted element (v2). +/// +/// `name` and `nearby_label` are **untrusted page text**. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct TargetDescriptorV2 { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub role: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub name: Option, + pub tag: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub name_attr: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub placeholder: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub nearby_label: Option, +} + +/// Page context dictionary entry — referenced by steps via `page` id. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct PageRef { + pub id: String, + pub url: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub title: Option, +} + +/// One selected option (`select` op). +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct SelectedOptionV2 { + pub value: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub label: Option, +} + +/// Observed navigation after a step (objective fact only). +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct StepEffectV2 { + /// Reference into `pages[]` for the destination page. + pub navigated_to: String, +} + +/// Fields shared by every v2 step variant (flattened in JSON). +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct StepCommonV2 { + pub id: u32, + /// Reference into `pages[]`. + pub page: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub effect: Option, +} + +/// One recorded user action — discriminated union by `op` (v2). +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] +#[serde(tag = "op", rename_all = "snake_case")] +pub enum StepV2 { + Navigate { + #[serde(flatten)] + common: StepCommonV2, + to: String, + }, + Click { + #[serde(flatten)] + common: StepCommonV2, + target: TargetDescriptorV2, + }, + Fill { + #[serde(flatten)] + common: StepCommonV2, + target: TargetDescriptorV2, + value: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + redacted: Option, + }, + Select { + #[serde(flatten)] + common: StepCommonV2, + target: TargetDescriptorV2, + selection: Vec, + }, + Press { + #[serde(flatten)] + common: StepCommonV2, + key: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + modifiers: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + target: Option, + }, +} + +/// Persisted user-action trace exported by legacy `tool.record_stop` / `await`. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct TraceV2 { + /// RFC 3339 timestamp when recording stopped. + pub recorded_at: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub started_at: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub purpose: Option, + pub entry: TraceEntry, + pub pages: Vec, + pub steps: Vec, +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + fn sample_common(id: u32) -> StepCommonV2 { + StepCommonV2 { + id, + page: "p1".into(), + effect: None, + } + } + + fn sample_target() -> TargetDescriptorV2 { + TargetDescriptorV2 { + role: Some("button".into()), + name: Some("发布".into()), + tag: "button".into(), + name_attr: None, + placeholder: None, + nearby_label: None, + } + } + + #[test] + fn trace_v2_has_no_version_field() { + let trace = TraceV2 { + recorded_at: "2026-07-17T09:01:10Z".into(), + started_at: None, + purpose: None, + entry: TraceEntry { + start_url: "https://example.com/".into(), + }, + pages: vec![PageRef { + id: "p1".into(), + url: "https://example.com/".into(), + title: None, + }], + steps: vec![StepV2::Click { + common: sample_common(1), + target: sample_target(), + }], + }; + let v = serde_json::to_value(&trace).unwrap(); + assert!(v.get("version").is_none()); + assert!(v.get("pages").is_some()); + assert!(v.get("states").is_none()); + } + + #[test] + fn extension_v2_trace_deserializes() { + let v = json!({ + "recorded_at": "2026-07-21T08:00:00Z", + "started_at": "2026-07-21T07:59:00Z", + "purpose": "demo", + "entry": { "start_url": "https://example.com/editor" }, + "pages": [ + { "id": "p1", "url": "https://example.com/editor" }, + { "id": "p2", "url": "https://example.com/p/99" } + ], + "steps": [ + { + "op": "fill", + "id": 1, + "page": "p1", + "target": { "tag": "input", "role": "textbox", "name": "标题" }, + "value": "hello" + }, + { + "op": "click", + "id": 2, + "page": "p1", + "target": { "tag": "button", "role": "button", "name": "发布" }, + "effect": { "navigated_to": "p2" } + } + ] + }); + let trace: TraceV2 = serde_json::from_value(v).unwrap(); + assert_eq!(trace.pages.len(), 2); + assert_eq!(trace.steps.len(), 2); + } + + #[test] + fn historical_v2_rejects_hover_steps() { + let value = json!({ + "recorded_at": "2026-07-21T08:00:00Z", + "entry": { "start_url": "https://example.com/" }, + "pages": [{ "id": "p1", "url": "https://example.com/" }], + "steps": [{ + "op": "hover", + "id": 1, + "page": "p1", + "target": { "tag": "button", "name": "结束" } + }] + }); + + assert!(serde_json::from_value::(value).is_err()); + } +} From fff33425c7603b53ba60e8504434ee5520867085 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Chaonan=E2=80=9D?= Date: Mon, 17 Aug 2026 20:26:42 +0800 Subject: [PATCH 2/5] fix(protocol): align Trace v2/v3 classification and hover steps Require Trace v3 version const 3, reject mixed pages/states payloads, and align TypeScript selection optionality with the generated schema. Trace v2 on main already records hover steps, so the frozen v2 schema must accept them instead of dropping them during reduction. Co-authored-by: Cursor --- .../src/lib/__tests__/trace-reducer.test.ts | 8 +- apps/extension/src/lib/trace-reducer.ts | 8 +- apps/extension/src/transport/types.ts | 5 +- .../schema/tool_record_await_result.json | 49 ++++++++++- .../schema/tool_record_stop_result.json | 49 ++++++++++- crates/bsk-protocol/schema/trace.json | 4 +- crates/bsk-protocol/schema/trace_v2.json | 41 +++++++++ crates/bsk-protocol/src/tools/record.rs | 84 ++++++++++++++++--- crates/bsk-protocol/src/tools/record_v2.rs | 17 +++- 9 files changed, 234 insertions(+), 31 deletions(-) diff --git a/apps/extension/src/lib/__tests__/trace-reducer.test.ts b/apps/extension/src/lib/__tests__/trace-reducer.test.ts index b1153be9..f5c6db51 100644 --- a/apps/extension/src/lib/__tests__/trace-reducer.test.ts +++ b/apps/extension/src/lib/__tests__/trace-reducer.test.ts @@ -151,7 +151,7 @@ describe("reduceTraceSteps", () => { }); }); - it("drops hover steps unsupported by historical v2 clients", () => { + it("keeps hover steps before menu clicks", () => { const { steps } = reduceTraceSteps( [ { @@ -167,11 +167,11 @@ describe("reduceTraceSteps", () => { ], "https://example.com/app", ); - expect(steps.map((s) => s.op)).toEqual(["click"]); + expect(steps.map((s) => s.op)).toEqual(["hover", "click"]); expect(steps[0]).toMatchObject({ - op: "click", + op: "hover", page: "p1", - target: { name: "Profile" }, + target: { name: "Account" }, }); }); diff --git a/apps/extension/src/lib/trace-reducer.ts b/apps/extension/src/lib/trace-reducer.ts index 2d5b0df2..8f83fff3 100644 --- a/apps/extension/src/lib/trace-reducer.ts +++ b/apps/extension/src/lib/trace-reducer.ts @@ -18,7 +18,6 @@ export function shouldRecordPress( } function shouldIncludeDraft(step: DraftTraceStep): boolean { - if (step.op === "hover") return false; if (step.op === "press" && !shouldRecordPress(step.key, step.modifiers)) return false; return true; } @@ -140,7 +139,12 @@ function toV2Step( effectForNavigation(step.navigated_to, urlToId), ); case "hover": - return null; + return { + op: "hover", + id, + page, + target: step.target, + }; case "fill": return { op: "fill", diff --git a/apps/extension/src/transport/types.ts b/apps/extension/src/transport/types.ts index e855f67a..7d876677 100644 --- a/apps/extension/src/transport/types.ts +++ b/apps/extension/src/transport/types.ts @@ -793,6 +793,7 @@ export type DraftTraceStep = export type Step = | ({ op: "navigate" } & StepCommon & { to: string }) | ({ op: "click" } & StepCommon & { target: TargetDescriptor }) + | ({ op: "hover" } & StepCommon & { target: TargetDescriptor }) | ({ op: "fill" } & StepCommon & { target: TargetDescriptor; value: string; @@ -834,7 +835,7 @@ export type StepV3 = }) | ({ op: "select" } & StepCommonV3 & { target: TargetDescriptorV3; - selection: SelectedOption[]; + selection?: SelectedOption[]; }) | ({ op: "press" } & StepCommonV3 & { key: string; @@ -844,7 +845,7 @@ export type StepV3 = | ({ op: "scroll" } & StepCommonV3); export interface TraceV3 { - version: number; + version: 3; recorded_at: string; started_at?: string; purpose?: string; diff --git a/crates/bsk-protocol/schema/tool_record_await_result.json b/crates/bsk-protocol/schema/tool_record_await_result.json index 622e2704..a16fdf89 100644 --- a/crates/bsk-protocol/schema/tool_record_await_result.json +++ b/crates/bsk-protocol/schema/tool_record_await_result.json @@ -512,6 +512,46 @@ } } }, + { + "description": "Fields shared by every v2 step variant (flattened in JSON).", + "type": "object", + "required": [ + "id", + "op", + "page", + "target" + ], + "properties": { + "effect": { + "anyOf": [ + { + "$ref": "#/definitions/StepEffectV2" + }, + { + "type": "null" + } + ] + }, + "id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "op": { + "type": "string", + "enum": [ + "hover" + ] + }, + "page": { + "description": "Reference into `pages[]`.", + "type": "string" + }, + "target": { + "$ref": "#/definitions/TargetDescriptorV2" + } + } + }, { "description": "Fields shared by every v2 step variant (flattened in JSON).", "type": "object", @@ -803,10 +843,10 @@ }, "version": { "type": "integer", - "format": "uint32", - "minimum": 0.0 + "const": 3 } - } + }, + "additionalProperties": false }, "TraceEntry": { "description": "Recording entry point — first URL the flow starts from.", @@ -900,7 +940,8 @@ "$ref": "#/definitions/StepV2" } } - } + }, + "additionalProperties": false } } } diff --git a/crates/bsk-protocol/schema/tool_record_stop_result.json b/crates/bsk-protocol/schema/tool_record_stop_result.json index 49d84c02..6fe6d48e 100644 --- a/crates/bsk-protocol/schema/tool_record_stop_result.json +++ b/crates/bsk-protocol/schema/tool_record_stop_result.json @@ -512,6 +512,46 @@ } } }, + { + "description": "Fields shared by every v2 step variant (flattened in JSON).", + "type": "object", + "required": [ + "id", + "op", + "page", + "target" + ], + "properties": { + "effect": { + "anyOf": [ + { + "$ref": "#/definitions/StepEffectV2" + }, + { + "type": "null" + } + ] + }, + "id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "op": { + "type": "string", + "enum": [ + "hover" + ] + }, + "page": { + "description": "Reference into `pages[]`.", + "type": "string" + }, + "target": { + "$ref": "#/definitions/TargetDescriptorV2" + } + } + }, { "description": "Fields shared by every v2 step variant (flattened in JSON).", "type": "object", @@ -803,10 +843,10 @@ }, "version": { "type": "integer", - "format": "uint32", - "minimum": 0.0 + "const": 3 } - } + }, + "additionalProperties": false }, "TraceEntry": { "description": "Recording entry point — first URL the flow starts from.", @@ -900,7 +940,8 @@ "$ref": "#/definitions/StepV2" } } - } + }, + "additionalProperties": false } } } diff --git a/crates/bsk-protocol/schema/trace.json b/crates/bsk-protocol/schema/trace.json index f17a0280..2abfd555 100644 --- a/crates/bsk-protocol/schema/trace.json +++ b/crates/bsk-protocol/schema/trace.json @@ -51,10 +51,10 @@ }, "version": { "type": "integer", - "format": "uint32", - "minimum": 0.0 + "const": 3 } }, + "additionalProperties": false, "definitions": { "FillCommit": { "type": "string", diff --git a/crates/bsk-protocol/schema/trace_v2.json b/crates/bsk-protocol/schema/trace_v2.json index 22b0b328..c301ee9f 100644 --- a/crates/bsk-protocol/schema/trace_v2.json +++ b/crates/bsk-protocol/schema/trace_v2.json @@ -42,6 +42,7 @@ } } }, + "additionalProperties": false, "definitions": { "KeyModifier": { "description": "Keyboard modifier flags. Multiple flags may be combined; the extension folds them into CDP's bitfield (`alt=1, ctrl=2, meta=4, shift=8`).", @@ -189,6 +190,46 @@ } } }, + { + "description": "Fields shared by every v2 step variant (flattened in JSON).", + "type": "object", + "required": [ + "id", + "op", + "page", + "target" + ], + "properties": { + "effect": { + "anyOf": [ + { + "$ref": "#/definitions/StepEffectV2" + }, + { + "type": "null" + } + ] + }, + "id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "op": { + "type": "string", + "enum": [ + "hover" + ] + }, + "page": { + "description": "Reference into `pages[]`.", + "type": "string" + }, + "target": { + "$ref": "#/definitions/TargetDescriptorV2" + } + } + }, { "description": "Fields shared by every v2 step variant (flattened in JSON).", "type": "object", diff --git a/crates/bsk-protocol/src/tools/record.rs b/crates/bsk-protocol/src/tools/record.rs index 74cd6343..8111965b 100644 --- a/crates/bsk-protocol/src/tools/record.rs +++ b/crates/bsk-protocol/src/tools/record.rs @@ -4,7 +4,7 @@ //! observations (VOM) captured before and after the action. use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; use super::interaction::KeyModifier; @@ -17,6 +17,28 @@ pub const TRACE_VERSION_V2: u32 = 2; pub const DEFAULT_TRACE_VERSION: u32 = 2; pub const VOM_FORMAT_VERSION: u32 = 1; +fn deserialize_trace_v3_version<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let version = u32::deserialize(deserializer)?; + if version != TRACE_VERSION { + return Err(serde::de::Error::custom(format!( + "unsupported trace version {version} (expected {TRACE_VERSION})" + ))); + } + Ok(version) +} + +fn trace_v3_version_schema(_: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { + schemars::schema::SchemaObject { + instance_type: Some(schemars::schema::InstanceType::Integer.into()), + const_value: Some(serde_json::json!(TRACE_VERSION)), + ..Default::default() + } + .into() +} + // --------------------------------------------------------------------------- // Target // --------------------------------------------------------------------------- @@ -179,7 +201,10 @@ pub enum Step { /// Persisted user-action trace exported by `tool.record_stop` / `await`. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] +#[serde(deny_unknown_fields)] pub struct Trace { + #[serde(deserialize_with = "deserialize_trace_v3_version")] + #[schemars(schema_with = "trace_v3_version_schema")] pub version: u32, #[serde(default, skip_serializing_if = "Option::is_none")] pub purpose: Option, @@ -236,21 +261,22 @@ pub enum RecordedTrace { impl RecordedTrace { pub fn classify_value(v: &serde_json::Value) -> Result { if let Some(ver) = v.get("version").and_then(|x| x.as_u64()) { - if ver == u64::from(TRACE_VERSION) { - return serde_json::from_value(v.clone()) - .map(RecordedTrace::V3) - .map_err(|e| e.to_string()); + if ver != u64::from(TRACE_VERSION) { + return Err(format!("unsupported trace version {ver}")); + } + if v.get("pages").is_some() { + return Err("trace v3 must not include legacy pages[]".into()); } - return Err(format!("unsupported trace version {ver}")); - } - if v.get("pages").is_some() && v.get("states").is_none() { return serde_json::from_value(v.clone()) - .map(RecordedTrace::V2) + .map(RecordedTrace::V3) .map_err(|e| e.to_string()); } if v.get("states").is_some() { + return Err("trace v2 must not include states[]; set version: 3 for Trace v3".into()); + } + if v.get("pages").is_some() { return serde_json::from_value(v.clone()) - .map(RecordedTrace::V3) + .map(RecordedTrace::V2) .map_err(|e| e.to_string()); } Err("ambiguous or unparseable trace".into()) @@ -569,6 +595,42 @@ mod tests { } } + #[test] + fn recorded_trace_rejects_mixed_and_unsupported_versions() { + let v2_with_states = json!({ + "recorded_at": "2026-07-21T08:00:00Z", + "entry": { "start_url": "https://example.com/" }, + "pages": [{ "id": "p1", "url": "https://example.com/" }], + "states": [], + "steps": [] + }); + assert!(RecordedTrace::classify_value(&v2_with_states).is_err()); + + let v3_with_pages = json!({ + "version": 3, + "recorded_at": "2026-07-21T08:00:00Z", + "stopped_by": "user_finish", + "entry": { "start_url": "https://example.com/" }, + "recorder": { "bsk": "0.1.10", "vom": 1 }, + "pages": [{ "id": "p1", "url": "https://example.com/" }], + "states": [], + "steps": [] + }); + assert!(RecordedTrace::classify_value(&v3_with_pages).is_err()); + + let unsupported = json!({ + "version": 2, + "recorded_at": "2026-07-21T08:00:00Z", + "stopped_by": "user_finish", + "entry": { "start_url": "https://example.com/" }, + "recorder": { "bsk": "0.1.10", "vom": 1 }, + "states": [], + "steps": [] + }); + assert!(RecordedTrace::classify_value(&unsupported).is_err()); + assert!(serde_json::from_value::(unsupported).is_err()); + } + #[test] fn recorded_trace_schema_includes_v2_and_v3() { let schema = serde_json::to_value(schemars::schema_for!(RecordStopResult)).unwrap(); @@ -577,6 +639,8 @@ mod tests { .expect("RecordedTrace schema should use oneOf"); assert_eq!(variants.len(), 2); + let trace_schema = schema["definitions"]["Trace"].clone(); + assert_eq!(trace_schema["properties"]["version"]["const"], 3); } #[test] diff --git a/crates/bsk-protocol/src/tools/record_v2.rs b/crates/bsk-protocol/src/tools/record_v2.rs index 77b3a37d..27f7f3f0 100644 --- a/crates/bsk-protocol/src/tools/record_v2.rs +++ b/crates/bsk-protocol/src/tools/record_v2.rs @@ -75,6 +75,11 @@ pub enum StepV2 { common: StepCommonV2, target: TargetDescriptorV2, }, + Hover { + #[serde(flatten)] + common: StepCommonV2, + target: TargetDescriptorV2, + }, Fill { #[serde(flatten)] common: StepCommonV2, @@ -102,6 +107,7 @@ pub enum StepV2 { /// Persisted user-action trace exported by legacy `tool.record_stop` / `await`. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] +#[serde(deny_unknown_fields)] pub struct TraceV2 { /// RFC 3339 timestamp when recording stopped. pub recorded_at: String, @@ -197,7 +203,7 @@ mod tests { } #[test] - fn historical_v2_rejects_hover_steps() { + fn v2_hover_steps_round_trip() { let value = json!({ "recorded_at": "2026-07-21T08:00:00Z", "entry": { "start_url": "https://example.com/" }, @@ -206,10 +212,15 @@ mod tests { "op": "hover", "id": 1, "page": "p1", - "target": { "tag": "button", "name": "结束" } + "target": { "tag": "span", "role": "button", "name": "Account" } }] }); - assert!(serde_json::from_value::(value).is_err()); + let trace: TraceV2 = serde_json::from_value(value).unwrap(); + assert!(matches!(trace.steps.as_slice(), [StepV2::Hover { .. }])); + assert_eq!( + serde_json::to_value(&trace).unwrap()["steps"][0]["op"], + "hover" + ); } } From db1f8c4dd62f8a9da9b45591ab84315f80c6898f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Chaonan=E2=80=9D?= Date: Tue, 18 Aug 2026 15:28:08 +0800 Subject: [PATCH 3/5] test(protocol): align handshake fixtures with 1.0 floor and clarify v2/v3 naming --- .../extension/src/lib/__tests__/connection-controller.test.ts | 2 +- apps/extension/src/transport/types.ts | 4 +++- crates/bsk-cli/tests/browser_wait.rs | 2 +- crates/bsk-cli/tests/cancel_forwarding.rs | 2 +- crates/bsk-cli/tests/per_session_queue.rs | 2 +- crates/bsk-cli/tests/session_user_interrupt.rs | 2 +- crates/bsk-cli/tests/sessions_ipc.rs | 4 ++-- crates/bsk-cli/tests/tools_ipc.rs | 2 +- crates/bsk-cli/tests/tools_m7_ipc.rs | 2 +- crates/bsk-cli/tests/tools_m8_ipc.rs | 2 +- crates/bsk-cli/tests/tools_m9_ipc.rs | 2 +- crates/bsk-cli/tests/ws_handshake.rs | 2 +- 12 files changed, 15 insertions(+), 13 deletions(-) diff --git a/apps/extension/src/lib/__tests__/connection-controller.test.ts b/apps/extension/src/lib/__tests__/connection-controller.test.ts index 53d34af5..44945235 100644 --- a/apps/extension/src/lib/__tests__/connection-controller.test.ts +++ b/apps/extension/src/lib/__tests__/connection-controller.test.ts @@ -26,7 +26,7 @@ function handshake( } describe("computeConnectedState (protocol-based compat)", () => { - it("returns connected when protocol strings match", () => { + it("returns connected when daemon protocol equals extension protocol", () => { expect(computeConnectedState(handshake("1.1", "1.0"), MIN_COMPATIBLE_PROTOCOL)).toEqual({ kind: "connected", }); diff --git a/apps/extension/src/transport/types.ts b/apps/extension/src/transport/types.ts index 7d876677..e430a8e9 100644 --- a/apps/extension/src/transport/types.ts +++ b/apps/extension/src/transport/types.ts @@ -657,7 +657,9 @@ export interface EmulateResult { } // -------------------------------------------------------------------------- -// Semantic record payloads — mirror bsk-protocol record.rs +// Semantic record payloads — legacy Trace v2 shapes (`Trace`/`Step`) mirror +// bsk-protocol record_v2.rs; Trace v3 shapes (`TraceV3`/`StepV3`) mirror +// record.rs (note: in Rust, `Trace` is the v3 shape). // -------------------------------------------------------------------------- export const TRACE_VERSION = 3; diff --git a/crates/bsk-cli/tests/browser_wait.rs b/crates/bsk-cli/tests/browser_wait.rs index 26f7620f..b2a9a140 100644 --- a/crates/bsk-cli/tests/browser_wait.rs +++ b/crates/bsk-cli/tests/browser_wait.rs @@ -119,7 +119,7 @@ async fn handshake_as_ext( version: "131.0".into(), }, min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()), - min_compatible_protocol: Some("1.1".into()), + min_compatible_protocol: Some("1.0".into()), label: "Test".into(), }; let req = RequestFrame { diff --git a/crates/bsk-cli/tests/cancel_forwarding.rs b/crates/bsk-cli/tests/cancel_forwarding.rs index 1abd8f9b..01894c82 100644 --- a/crates/bsk-cli/tests/cancel_forwarding.rs +++ b/crates/bsk-cli/tests/cancel_forwarding.rs @@ -83,7 +83,7 @@ async fn handshake_as_ext( }, label: "Test".into(), min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()), - min_compatible_protocol: Some("1.1".into()), + min_compatible_protocol: Some("1.0".into()), }; let req = RequestFrame { id: "hs".into(), diff --git a/crates/bsk-cli/tests/per_session_queue.rs b/crates/bsk-cli/tests/per_session_queue.rs index 4f66d1e8..1bd7fa5e 100644 --- a/crates/bsk-cli/tests/per_session_queue.rs +++ b/crates/bsk-cli/tests/per_session_queue.rs @@ -88,7 +88,7 @@ async fn handshake_as_ext( version: "131.0".into(), }, min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()), - min_compatible_protocol: Some("1.1".into()), + min_compatible_protocol: Some("1.0".into()), label: "Test".into(), }; let req = RequestFrame { diff --git a/crates/bsk-cli/tests/session_user_interrupt.rs b/crates/bsk-cli/tests/session_user_interrupt.rs index 7529273a..4ae1ce46 100644 --- a/crates/bsk-cli/tests/session_user_interrupt.rs +++ b/crates/bsk-cli/tests/session_user_interrupt.rs @@ -89,7 +89,7 @@ async fn handshake_as_ext( }, label: "Test".into(), min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()), - min_compatible_protocol: Some("1.1".into()), + min_compatible_protocol: Some("1.0".into()), }; let req = RequestFrame { id: "hs".into(), diff --git a/crates/bsk-cli/tests/sessions_ipc.rs b/crates/bsk-cli/tests/sessions_ipc.rs index aaf895b1..e267a3b7 100644 --- a/crates/bsk-cli/tests/sessions_ipc.rs +++ b/crates/bsk-cli/tests/sessions_ipc.rs @@ -91,7 +91,7 @@ async fn handshake_as_ext( version: "131.0".into(), }, min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()), - min_compatible_protocol: Some("1.1".into()), + min_compatible_protocol: Some("1.0".into()), label: "Test".into(), }; let req = RequestFrame { @@ -407,7 +407,7 @@ async fn connect_second_ext( version: "130".into(), }, min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()), - min_compatible_protocol: Some("1.1".into()), + min_compatible_protocol: Some("1.0".into()), label: label.into(), }; let hs = RequestFrame { diff --git a/crates/bsk-cli/tests/tools_ipc.rs b/crates/bsk-cli/tests/tools_ipc.rs index 97fa635a..b64ff9bd 100644 --- a/crates/bsk-cli/tests/tools_ipc.rs +++ b/crates/bsk-cli/tests/tools_ipc.rs @@ -80,7 +80,7 @@ async fn do_handshake(ws: &mut Ws) -> HandshakeResult { version: "131.0".into(), }, min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()), - min_compatible_protocol: Some("1.1".into()), + min_compatible_protocol: Some("1.0".into()), label: "Test".into(), }; let req = RequestFrame { diff --git a/crates/bsk-cli/tests/tools_m7_ipc.rs b/crates/bsk-cli/tests/tools_m7_ipc.rs index 095584ab..4635828e 100644 --- a/crates/bsk-cli/tests/tools_m7_ipc.rs +++ b/crates/bsk-cli/tests/tools_m7_ipc.rs @@ -83,7 +83,7 @@ async fn do_handshake(ws: &mut Ws) -> HandshakeResult { version: "131.0".into(), }, min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()), - min_compatible_protocol: Some("1.1".into()), + min_compatible_protocol: Some("1.0".into()), label: "Test".into(), }; let req = RequestFrame { diff --git a/crates/bsk-cli/tests/tools_m8_ipc.rs b/crates/bsk-cli/tests/tools_m8_ipc.rs index 4bdce78e..777d2128 100644 --- a/crates/bsk-cli/tests/tools_m8_ipc.rs +++ b/crates/bsk-cli/tests/tools_m8_ipc.rs @@ -82,7 +82,7 @@ async fn do_handshake(ws: &mut Ws) -> HandshakeResult { version: "131.0".into(), }, min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()), - min_compatible_protocol: Some("1.1".into()), + min_compatible_protocol: Some("1.0".into()), label: "Test".into(), }; let req = RequestFrame { diff --git a/crates/bsk-cli/tests/tools_m9_ipc.rs b/crates/bsk-cli/tests/tools_m9_ipc.rs index 7937c27d..f5015cac 100644 --- a/crates/bsk-cli/tests/tools_m9_ipc.rs +++ b/crates/bsk-cli/tests/tools_m9_ipc.rs @@ -92,7 +92,7 @@ async fn do_handshake(ws: &mut Ws) -> HandshakeResult { version: "131.0".into(), }, min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()), - min_compatible_protocol: Some("1.1".into()), + min_compatible_protocol: Some("1.0".into()), label: "Test".into(), }; let req = RequestFrame { diff --git a/crates/bsk-cli/tests/ws_handshake.rs b/crates/bsk-cli/tests/ws_handshake.rs index 300a8f0e..be413d27 100644 --- a/crates/bsk-cli/tests/ws_handshake.rs +++ b/crates/bsk-cli/tests/ws_handshake.rs @@ -60,7 +60,7 @@ pub async fn send_handshake( version: "131.0".into(), }, min_compatible_peer: Some("0.1.0-dev.0".parse().unwrap()), - min_compatible_protocol: Some("1.1".into()), + min_compatible_protocol: Some("1.0".into()), label: "Test Chrome".into(), }; let req = RequestFrame { From e80093d1ae6c54bc11814f60a84a4d562f84f84f Mon Sep 17 00:00:00 2001 From: Ljy-0827 Date: Wed, 19 Aug 2026 11:55:15 +0800 Subject: [PATCH 4/5] fix(protocol): integrate with vom --- .../__tests__/connection-controller.test.ts | 24 +- apps/extension/src/lib/trace-reducer.ts | 20 +- apps/extension/src/tools/record.ts | 18 +- .../src/transport/__tests__/handshake.test.ts | 23 +- apps/extension/src/transport/handshake.ts | 2 +- apps/extension/src/transport/types.ts | 85 ++- crates/bsk-cli/src/cli/doctor.rs | 10 +- crates/bsk-cli/src/cli/record.rs | 3 - crates/bsk-cli/src/daemon/start.rs | 4 +- crates/bsk-cli/src/daemon/state.rs | 2 +- crates/bsk-cli/tests/browser_wait.rs | 2 +- crates/bsk-cli/tests/cancel_forwarding.rs | 2 +- crates/bsk-cli/tests/handshake_compat.rs | 45 +- crates/bsk-cli/tests/per_session_queue.rs | 2 +- .../bsk-cli/tests/session_user_interrupt.rs | 2 +- crates/bsk-cli/tests/sessions_ipc.rs | 4 +- crates/bsk-cli/tests/status_cmd.rs | 2 +- crates/bsk-cli/tests/tools_ipc.rs | 2 +- crates/bsk-cli/tests/tools_m7_ipc.rs | 2 +- crates/bsk-cli/tests/tools_m8_ipc.rs | 2 +- crates/bsk-cli/tests/tools_m9_ipc.rs | 2 +- crates/bsk-cli/tests/ws_handshake.rs | 4 +- .../schema/tool_record_await_result.json | 626 +++++++++--------- .../schema/tool_record_start_params.json | 23 - .../schema/tool_record_stop_result.json | 626 +++++++++--------- crates/bsk-protocol/schema/trace.json | 80 +-- crates/bsk-protocol/schema/trace_step.json | 34 +- crates/bsk-protocol/schema/trace_v2.json | 5 +- crates/bsk-protocol/src/bin/dump-schema.rs | 4 +- crates/bsk-protocol/src/tools/mod.rs | 3 +- crates/bsk-protocol/src/tools/record.rs | 184 +++-- .../bsk-protocol/src/tools/record_common.rs | 7 + crates/bsk-protocol/src/tools/record_v2.rs | 11 +- 33 files changed, 867 insertions(+), 998 deletions(-) create mode 100644 crates/bsk-protocol/src/tools/record_common.rs diff --git a/apps/extension/src/lib/__tests__/connection-controller.test.ts b/apps/extension/src/lib/__tests__/connection-controller.test.ts index 44945235..6d5b578b 100644 --- a/apps/extension/src/lib/__tests__/connection-controller.test.ts +++ b/apps/extension/src/lib/__tests__/connection-controller.test.ts @@ -26,20 +26,20 @@ function handshake( } describe("computeConnectedState (protocol-based compat)", () => { - it("returns connected when daemon protocol equals extension protocol", () => { - expect(computeConnectedState(handshake("1.1", "1.0"), MIN_COMPATIBLE_PROTOCOL)).toEqual({ + it("returns connected when protocol strings match", () => { + expect(computeConnectedState(handshake("1.0", "1.0"), MIN_COMPATIBLE_PROTOCOL)).toEqual({ kind: "connected", }); }); it("returns version_skew when daemon protocol minor is newer", () => { - expect(computeConnectedState(handshake("1.2", "1.0"))).toEqual({ + expect(computeConnectedState(handshake("1.1", "1.0"))).toEqual({ kind: "version_skew", }); }); it("returns version_skew when daemon protocol string differs but floor is satisfied", () => { - expect(computeConnectedState(handshake("1.1.0", "1.0"))).toEqual({ + expect(computeConnectedState(handshake("1", "1.0"))).toEqual({ kind: "version_skew", }); }); @@ -53,7 +53,7 @@ describe("computeConnectedState (protocol-based compat)", () => { }); it("rejects when extension is below daemon min_compatible_protocol", () => { - const result = computeConnectedState(handshake("1.1", "1.5")); + const result = computeConnectedState(handshake("1.0", "1.5")); expect(result.kind).toBe("rejected"); if (result.kind === "rejected") { expect(result.reason).toContain("min_compatible_protocol"); @@ -65,7 +65,7 @@ describe("computeConnectedState (protocol-based compat)", () => { const result = computeConnectedState({ server: "browser-skill-daemon", version: "0.1.0", - protocol_version: "1.1", + protocol_version: "1.0", min_compatible_peer: "0.1.0", }); expect(result).toEqual({ kind: "connected" }); @@ -79,14 +79,8 @@ describe("computeConnectedState (protocol-based compat)", () => { } }); - it("returns version_skew when daemon protocol is 1.0 and floor is satisfied", () => { - expect(computeConnectedState(handshake("1.0", "1.0"))).toEqual({ - kind: "version_skew", - }); - }); - it("rejects malformed daemon min_compatible_protocol with a daemon-floor reason", () => { - const result = computeConnectedState(handshake("1.1", "not-a-protocol")); + const result = computeConnectedState(handshake("1.0", "not-a-protocol")); expect(result.kind).toBe("rejected"); if (result.kind === "rejected") { expect(result.reason).toContain("daemon min_compatible_protocol"); @@ -248,11 +242,11 @@ describe("ConnectionController connectionEnabled", () => { const second = transport.send.mock.calls[1]?.[0] as { id: string }; expect(second.id).not.toBe(first.id); - transport.emitMessage({ id: first.id, result: handshake("1.1", "1.0") }); + transport.emitMessage({ id: first.id, result: handshake("1.0", "1.0") }); await Promise.resolve(); expect(controller.snapshot().state).not.toBe("connected"); - transport.emitMessage({ id: second.id, result: handshake("1.1", "1.0") }); + transport.emitMessage({ id: second.id, result: handshake("1.0", "1.0") }); await vi.waitFor(() => expect(controller.snapshot().state).toBe("connected")); }); }); diff --git a/apps/extension/src/lib/trace-reducer.ts b/apps/extension/src/lib/trace-reducer.ts index 8f83fff3..b58b7939 100644 --- a/apps/extension/src/lib/trace-reducer.ts +++ b/apps/extension/src/lib/trace-reducer.ts @@ -1,4 +1,4 @@ -import type { DraftTraceStep, PageRef, SelectedOption, Step } from "@/transport/types"; +import type { DraftTraceStep, PageRefV2, SelectedOptionV2, StepV2 } from "@/transport/types"; const CLIPBOARD_KEYS = new Set(["a", "c", "v", "x", "A", "C", "V", "X"]); const MODIFIER_ONLY_KEYS = new Set(["Meta", "Control", "Alt", "Shift", "OS", "Hyper", "Super"]); @@ -60,7 +60,7 @@ function collectUrls(steps: DraftTraceStep[], startUrl?: string): string[] { function buildPageRegistry( steps: DraftTraceStep[], startUrl?: string, -): { pages: PageRef[]; urlToId: Map } { +): { pages: PageRefV2[]; urlToId: Map } { const urls = collectUrls(steps, startUrl); const urlToId = new Map(); const pages = urls.map((url, index) => { @@ -90,19 +90,19 @@ function pageUrlForDraft(step: DraftTraceStep, fallbackUrl?: string): string | u function effectForNavigation( navigatedTo: string | undefined, urlToId: Map, -): Step["effect"] { +): StepV2["effect"] { if (!navigatedTo) return undefined; const pageId = urlToId.get(navigatedTo); if (!pageId) return undefined; return { navigated_to: pageId }; } -function withEffect(step: Step, effect: Step["effect"]): Step { +function withEffect(step: StepV2, effect: StepV2["effect"]): StepV2 { if (!effect) return step; return { ...step, effect }; } -function toSelection(values: string[], labels?: string[]): SelectedOption[] { +function toSelection(values: string[], labels?: string[]): SelectedOptionV2[] { return values.map((value, index) => ({ value, ...(labels?.[index] ? { label: labels[index] } : {}), @@ -114,7 +114,7 @@ function toV2Step( id: number, urlToId: Map, fallbackUrl?: string, -): Step | null { +): StepV2 | null { if (!shouldIncludeDraft(step)) return null; const pageUrl = pageUrlForDraft(step, fallbackUrl); @@ -181,8 +181,8 @@ function toV2Step( } export interface ReducedTrace { - pages: PageRef[]; - steps: Step[]; + pages: PageRefV2[]; + steps: StepV2[]; } /** @@ -192,7 +192,7 @@ export interface ReducedTrace { export function reduceTraceSteps(steps: DraftTraceStep[], startUrl?: string): ReducedTrace { const collapsed = collapseNavigations(steps); const { pages, urlToId } = buildPageRegistry(collapsed, startUrl); - const out: Step[] = []; + const out: StepV2[] = []; let id = 1; let lastUrl = startUrl; for (const draft of collapsed) { @@ -210,7 +210,7 @@ export function reduceTraceSteps(steps: DraftTraceStep[], startUrl?: string): Re export function resolveTraceStartUrl( drafts: DraftTraceStep[], startUrl?: string, - pages?: PageRef[], + pages?: PageRefV2[], ): string { if (startUrl) return startUrl; const navigate = drafts.find((step): step is Extract => { diff --git a/apps/extension/src/tools/record.ts b/apps/extension/src/tools/record.ts index c797ef16..be869ef7 100644 --- a/apps/extension/src/tools/record.ts +++ b/apps/extension/src/tools/record.ts @@ -29,7 +29,7 @@ import type { RecordStopParams, RecordStopResult, RpcError, - Trace, + TraceV2, } from "@/transport/types"; import { handleNavigate } from "./navigation"; import { @@ -50,8 +50,8 @@ interface ActiveRecording { steps: DraftTraceStep[]; startedAt: string; startedAtMs: number; - finishPromise: Promise; - resolveFinish: (trace: Trace) => void; + finishPromise: Promise; + resolveFinish: (trace: TraceV2) => void; rejectFinish: (err: Error) => void; settled: boolean; finishing: boolean; @@ -154,7 +154,7 @@ async function sendRecordStartWithAck( throw lastError ?? new Error("failed to start recording in content script"); } -function buildTrace(recording: ActiveRecording): Trace { +function buildTrace(recording: ActiveRecording): TraceV2 { const { pages, steps } = reduceTraceSteps(recording.steps, recording.startUrl); const startUrl = resolveTraceStartUrl(recording.steps, recording.startUrl, pages); return { @@ -532,7 +532,7 @@ async function finishRecordingByRequest( } } -async function finishRecording(sessionId: string, deps: RecordDeps): Promise { +async function finishRecording(sessionId: string, deps: RecordDeps): Promise { const recording = recordings.get(sessionId); if (!recording || recording.settled || recording.finishing) return null; recording.finishing = true; @@ -574,9 +574,9 @@ export async function handleRecordStart( // on the destination page can RECORD_QUERY → rearm → show RecordOverlay // instead of flashing ControlOverlay ("Agent 正在控制"). const requestId = makeRequestId(target.tabId); - let resolveFinish!: (trace: Trace) => void; + let resolveFinish!: (trace: TraceV2) => void; let rejectFinish!: (err: Error) => void; - const finishPromise = new Promise((resolve, reject) => { + const finishPromise = new Promise((resolve, reject) => { resolveFinish = resolve; rejectFinish = reject; }); @@ -785,9 +785,9 @@ export async function handleRecordAwait( return { code: "cancelled", message: "record_await aborted" }; } - const outcome = await new Promise<{ trace: Trace } | { error: RpcError }>((resolve) => { + const outcome = await new Promise<{ trace: TraceV2 } | { error: RpcError }>((resolve) => { let settled = false; - const finish = (result: { trace: Trace } | { error: RpcError }) => { + const finish = (result: { trace: TraceV2 } | { error: RpcError }) => { if (settled) return; settled = true; if (timer) clearTimeout(timer); diff --git a/apps/extension/src/transport/__tests__/handshake.test.ts b/apps/extension/src/transport/__tests__/handshake.test.ts index 6196f344..b511453c 100644 --- a/apps/extension/src/transport/__tests__/handshake.test.ts +++ b/apps/extension/src/transport/__tests__/handshake.test.ts @@ -65,11 +65,6 @@ function deferredFakeTransport(): { transport: Transport; emit: (frame: Protocol } describe("performHandshake", () => { - it("advertises the protocol compatibility boundary", () => { - expect(PROTOCOL_VERSION).toBe("1.1"); - expect(MIN_COMPATIBLE_PROTOCOL).toBe("1.0"); - }); - it("sends system.handshake with identity and both compat fields", async () => { let sentFrame: ProtocolFrame | null = null; const transport = fakeTransport((req) => { @@ -79,7 +74,7 @@ describe("performHandshake", () => { result: { server: "browser-skill-daemon", version: "0.1.0", - protocol_version: "1.1", + protocol_version: "1.0", min_compatible_peer: "0.0.0", min_compatible_protocol: "1.0", }, @@ -134,8 +129,8 @@ describe("performHandshake", () => { const response = { server: "browser-skill-daemon", version: "0.1.0", - protocol_version: "1.1", - min_compatible_protocol: "1.1", + protocol_version: "1.0", + min_compatible_protocol: "1.0", } satisfies HandshakeResult; const transport = fakeTransport((req) => ({ id: (req as { id: string }).id, @@ -150,7 +145,7 @@ describe("performHandshake", () => { }); expect(outcome.result.min_compatible_peer).toBeUndefined(); - expect(outcome.result.min_compatible_protocol).toBe("1.1"); + expect(outcome.result.min_compatible_protocol).toBe("1.0"); }); it("rejects when the daemon responds with an error", async () => { @@ -183,9 +178,9 @@ describe("performHandshake", () => { result: { server: "browser-skill-daemon", version: "0.1.0", - protocol_version: "1.1", + protocol_version: "1.0", min_compatible_peer: "0.0.0", - min_compatible_protocol: "1.1", + min_compatible_protocol: "1.0", }, }); emit({ @@ -193,14 +188,14 @@ describe("performHandshake", () => { result: { server: "browser-skill-daemon", version: "0.1.0", - protocol_version: "1.1", + protocol_version: "1.0", min_compatible_peer: "0.0.0", - min_compatible_protocol: "1.1", + min_compatible_protocol: "1.0", }, }); await expect(pending).resolves.toMatchObject({ - result: { server: "browser-skill-daemon", protocol_version: "1.1" }, + result: { server: "browser-skill-daemon", protocol_version: "1.0" }, }); }); diff --git a/apps/extension/src/transport/handshake.ts b/apps/extension/src/transport/handshake.ts index a869ca52..3da56fda 100644 --- a/apps/extension/src/transport/handshake.ts +++ b/apps/extension/src/transport/handshake.ts @@ -7,7 +7,7 @@ import type { ResponseFrame, } from "./types"; -export const PROTOCOL_VERSION = "1.1"; +export const PROTOCOL_VERSION = "1.0"; /** * Extension semver, injected at build time from `package.json` via * Vite's `define` (see `wxt.config.ts` and `vitest.config.ts`). diff --git a/apps/extension/src/transport/types.ts b/apps/extension/src/transport/types.ts index e430a8e9..5659392b 100644 --- a/apps/extension/src/transport/types.ts +++ b/apps/extension/src/transport/types.ts @@ -657,12 +657,10 @@ export interface EmulateResult { } // -------------------------------------------------------------------------- -// Semantic record payloads — legacy Trace v2 shapes (`Trace`/`Step`) mirror -// bsk-protocol record_v2.rs; Trace v3 shapes (`TraceV3`/`StepV3`) mirror -// record.rs (note: in Rust, `Trace` is the v3 shape). +// Semantic record payloads mirror the versioned Rust protocol models. // -------------------------------------------------------------------------- -export const TRACE_VERSION = 3; +export const TRACE_VERSION_V3 = 3; export const TRACE_VERSION_V2 = 2; export const DEFAULT_TRACE_VERSION = 2; export const VOM_FORMAT_VERSION = 1; @@ -682,25 +680,22 @@ export interface RecorderInfo { export type StopReason = "user_finish" | "cli_stop"; -export interface TraceState { +export interface TraceStateV3 { id: string; url: string; title?: string; - /** Wire-only: full page observation text. */ - body?: string; - /** Disk-only: filename under the bundle `pages/` directory. */ - page?: string; + body: string; truncated?: boolean; } -export interface StepResult { +export interface StepResultV3 { state: string; } export interface StepCommonV3 { id: number; state: string; - result: StepResult; + result: StepResultV3; } export type NavigationCause = @@ -715,7 +710,7 @@ export type NavigationCause = export type FillCommit = "enter" | "suggestion" | "blur"; /** Legacy v2 target shape retained for existing record producers. */ -export interface TargetDescriptor { +export interface TargetDescriptorV2 { role?: string; name?: string; tag: string; @@ -728,43 +723,43 @@ export interface TraceEntry { start_url: string; } -export interface PageRef { +export interface PageRefV2 { id: string; url: string; title?: string; } -export interface SelectedOption { +export interface SelectedOptionV2 { value: string; label?: string; } -export interface StepEffect { +export interface StepEffectV2 { navigated_to: string; } -export interface StepCommon { +export interface StepCommonV2 { id: number; page: string; - effect?: StepEffect; + effect?: StepEffectV2; } /** Capture/buffer draft before v2 reduction. */ export type DraftTraceStep = | { op: "click"; - target: TargetDescriptor; + target: TargetDescriptorV2; navigated_to?: string; page_url?: string; } | { op: "hover"; - target: TargetDescriptor; + target: TargetDescriptorV2; page_url?: string; } | { op: "fill"; - target: TargetDescriptor; + target: TargetDescriptorV2; value: string; redacted?: boolean; page_url?: string; @@ -772,14 +767,14 @@ export type DraftTraceStep = | { op: "press"; key: string; - target?: TargetDescriptor; + target?: TargetDescriptorV2; modifiers?: KeyModifier[]; navigated_to?: string; page_url?: string; } | { op: "select"; - target: TargetDescriptor; + target: TargetDescriptorV2; values: string[]; labels?: string[]; navigated_to?: string; @@ -792,38 +787,38 @@ export type DraftTraceStep = }; /** Exported record-only step (trace v2). */ -export type Step = - | ({ op: "navigate" } & StepCommon & { to: string }) - | ({ op: "click" } & StepCommon & { target: TargetDescriptor }) - | ({ op: "hover" } & StepCommon & { target: TargetDescriptor }) - | ({ op: "fill" } & StepCommon & { - target: TargetDescriptor; +export type StepV2 = + | ({ op: "navigate" } & StepCommonV2 & { to: string }) + | ({ op: "click" } & StepCommonV2 & { target: TargetDescriptorV2 }) + | ({ op: "hover" } & StepCommonV2 & { target: TargetDescriptorV2 }) + | ({ op: "fill" } & StepCommonV2 & { + target: TargetDescriptorV2; value: string; redacted?: boolean; }) - | ({ op: "select" } & StepCommon & { - target: TargetDescriptor; - selection: SelectedOption[]; + | ({ op: "select" } & StepCommonV2 & { + target: TargetDescriptorV2; + selection: SelectedOptionV2[]; }) - | ({ op: "press" } & StepCommon & { + | ({ op: "press" } & StepCommonV2 & { key: string; modifiers?: KeyModifier[]; - target?: TargetDescriptor; + target?: TargetDescriptorV2; }); -export type TargetDescriptorV2 = TargetDescriptor; -export type StepV2 = Step; - -export interface Trace { +export interface TraceV2 { recorded_at: string; started_at?: string; purpose?: string; entry: TraceEntry; - pages: PageRef[]; - steps: Step[]; + pages: PageRefV2[]; + steps: StepV2[]; } -export type TraceV2 = Trace; +export interface SelectedOptionV3 { + value: string; + label?: string; +} export type StepV3 = | ({ op: "navigate" } & StepCommonV3 & { to: string; cause: NavigationCause }) @@ -837,7 +832,7 @@ export type StepV3 = }) | ({ op: "select" } & StepCommonV3 & { target: TargetDescriptorV3; - selection?: SelectedOption[]; + selection?: SelectedOptionV3[]; }) | ({ op: "press" } & StepCommonV3 & { key: string; @@ -847,14 +842,14 @@ export type StepV3 = | ({ op: "scroll" } & StepCommonV3); export interface TraceV3 { - version: 3; + version: typeof TRACE_VERSION_V3; recorded_at: string; started_at?: string; purpose?: string; stopped_by: StopReason; entry: TraceEntry; recorder: RecorderInfo; - states: TraceState[]; + states: TraceStateV3[]; steps: StepV3[]; } @@ -865,10 +860,6 @@ export interface RecordStartParams { tab_id?: number; url?: string; purpose?: string; - max_page_tokens?: number; - redact_values?: boolean; - /** Omitted means v2; `3` requests a state-linked v3 trace. */ - trace_version?: number; } export interface RecordStartResult { diff --git a/crates/bsk-cli/src/cli/doctor.rs b/crates/bsk-cli/src/cli/doctor.rs index 2a839012..3d0c57a9 100644 --- a/crates/bsk-cli/src/cli/doctor.rs +++ b/crates/bsk-cli/src/cli/doctor.rs @@ -486,7 +486,7 @@ mod m2_tests { fn fake_status(browsers: Vec, skew: Vec) -> StatusResult { StatusResult { daemon_version: env!("CARGO_PKG_VERSION").into(), - protocol_version: "1.1".into(), + protocol_version: "1.0".into(), pid: 1, uptime_secs: 0, ws_port: 0, @@ -571,7 +571,7 @@ mod m2_tests { session_count: 0, connected_at_ms: 1, version_skew: false, - extension_protocol_version: "1.1".into(), + extension_protocol_version: "1.0".into(), }], Vec::new(), ); @@ -592,7 +592,7 @@ mod m2_tests { session_count: 0, connected_at_ms: 1, version_skew: true, - extension_protocol_version: "1.2".into(), + extension_protocol_version: "1.1".into(), }], vec![VersionSkewEntry { instance_id: "alpha".into(), @@ -600,8 +600,8 @@ mod m2_tests { label: "Personal".into(), server_version: env!("CARGO_PKG_VERSION").into(), client_version: "0.0.9".into(), - server_protocol_version: "1.1".into(), - client_protocol_version: "1.2".into(), + server_protocol_version: "1.0".into(), + client_protocol_version: "1.1".into(), }], ); let check = check_browsers_protocol_compatible(Some(&status)); diff --git a/crates/bsk-cli/src/cli/record.rs b/crates/bsk-cli/src/cli/record.rs index 7cee47f9..8cc66dba 100644 --- a/crates/bsk-cli/src/cli/record.rs +++ b/crates/bsk-cli/src/cli/record.rs @@ -98,9 +98,6 @@ fn dispatch_start(args: RecordStartArgs, format: Format) -> Result<(), CliError> tab_id: args.tab_id, url: args.url, purpose: args.purpose.clone(), - max_page_tokens: None, - redact_values: None, - trace_version: None, }; let start_result = business_rpc::call::( info.sock_path.clone(), diff --git a/crates/bsk-cli/src/daemon/start.rs b/crates/bsk-cli/src/daemon/start.rs index 4bee196e..582cb4c8 100644 --- a/crates/bsk-cli/src/daemon/start.rs +++ b/crates/bsk-cli/src/daemon/start.rs @@ -27,7 +27,7 @@ use crate::daemon::{ browsers::{BROWSER_LIVENESS_TICK, BROWSER_LIVENESS_TIMEOUT, EXTENSION_CONNECT_WAIT}, info as daemon_info, ipc, lockfile, paths, sessions::{StopSessionError, forget_session, stop_session}, - state::{DaemonState, PROTOCOL_VERSION}, + state::DaemonState, ws, }; @@ -299,7 +299,7 @@ pub fn run_foreground(cfg: DaemonConfig) -> Result<()> { ws_port, sock_path: sock_path.clone(), daemon_version: env!("CARGO_PKG_VERSION"), - protocol_version: PROTOCOL_VERSION, + protocol_version: "1.0", }; let handler = ipc::full_handler(status, Arc::clone(&state)); diff --git a/crates/bsk-cli/src/daemon/state.rs b/crates/bsk-cli/src/daemon/state.rs index c8fee7a3..7ea9cee7 100644 --- a/crates/bsk-cli/src/daemon/state.rs +++ b/crates/bsk-cli/src/daemon/state.rs @@ -16,7 +16,7 @@ use super::start::DaemonConfig; use super::ws::WsHandle; pub const DAEMON_VERSION: &str = env!("CARGO_PKG_VERSION"); -pub const PROTOCOL_VERSION: &str = "1.1"; +pub const PROTOCOL_VERSION: &str = "1.0"; /// Lowest **protocol** version peers must speak (e.g. `"1.0"`). pub const MIN_COMPATIBLE_PROTOCOL: &str = "1.0"; /// Legacy app-semver floor used only when `HandshakeResult.min_compatible_peer` diff --git a/crates/bsk-cli/tests/browser_wait.rs b/crates/bsk-cli/tests/browser_wait.rs index b2a9a140..16513e0f 100644 --- a/crates/bsk-cli/tests/browser_wait.rs +++ b/crates/bsk-cli/tests/browser_wait.rs @@ -112,7 +112,7 @@ async fn handshake_as_ext( let params = HandshakeParams { client: "browser-skill-extension".into(), version: "0.1.0-dev.0".parse().unwrap(), - protocol_version: "1.1".into(), + protocol_version: "1.0".into(), instance_id: TEST_EXT_ID.into(), browser: BrowserPeerInfo { name: "chrome".into(), diff --git a/crates/bsk-cli/tests/cancel_forwarding.rs b/crates/bsk-cli/tests/cancel_forwarding.rs index 01894c82..c48113ce 100644 --- a/crates/bsk-cli/tests/cancel_forwarding.rs +++ b/crates/bsk-cli/tests/cancel_forwarding.rs @@ -75,7 +75,7 @@ async fn handshake_as_ext( let params = HandshakeParams { client: "browser-skill-extension".into(), version: "0.1.0-dev.0".parse().unwrap(), - protocol_version: "1.1".into(), + protocol_version: "1.0".into(), instance_id: TEST_EXT_ID.into(), browser: BrowserPeerInfo { name: "chrome".into(), diff --git a/crates/bsk-cli/tests/handshake_compat.rs b/crates/bsk-cli/tests/handshake_compat.rs index 7c8fd507..a5e91b01 100644 --- a/crates/bsk-cli/tests/handshake_compat.rs +++ b/crates/bsk-cli/tests/handshake_compat.rs @@ -108,12 +108,12 @@ async fn send_handshake_with_floors( async fn handshake_ok_when_protocol_matches() { let (handle, _sock) = spawn_daemon().await; let mut ws = open_ws(handle.ws_addr()).await; - let resp = send_handshake(&mut ws, "1.1", env!("CARGO_PKG_VERSION")).await; + let resp = send_handshake(&mut ws, "1.0", env!("CARGO_PKG_VERSION")).await; let result: HandshakeResult = match resp.body { ResponseBody::Ok(v) => serde_json::from_value(v).unwrap(), ResponseBody::Err(e) => panic!("expected ok handshake, got {e:?}"), }; - assert_eq!(result.protocol_version, "1.1"); + assert_eq!(result.protocol_version, "1.0"); assert_eq!( result .min_compatible_peer @@ -135,7 +135,7 @@ async fn handshake_ok_when_app_versions_differ_but_protocol_matches() { let (handle, _sock) = spawn_daemon().await; let mut ws = open_ws(handle.ws_addr()).await; let resp = - send_handshake_with_floors(&mut ws, "1.1", "9.9.9", Some("0.0.0"), Some("1.1")).await; + send_handshake_with_floors(&mut ws, "1.0", "9.9.9", Some("0.0.0"), Some("1.0")).await; match resp.body { ResponseBody::Ok(_) => {} other => panic!("expected ok when protocol matches, got {other:?}"), @@ -149,32 +149,7 @@ async fn handshake_skew_when_protocol_minor_differs() { let mut ws = open_ws(handle.ws_addr()).await; let resp = send_handshake_with_floors( &mut ws, - "1.2", - env!("CARGO_PKG_VERSION"), - Some("0.0.0"), - Some("1.1"), - ) - .await; - match resp.body { - ResponseBody::Ok(_) => {} - other => panic!("minor protocol drift should warn-but-allow, got {other:?}"), - } - let state = handle.state(); - let client = state - .browsers - .get(&bsk::daemon::browsers::BrowserId(TEST_EXT_ID.into())) - .expect("browser registered"); - assert!(client.version_skew); - handle.shutdown().await; -} - -#[tokio::test] -async fn handshake_skew_when_protocol_1_0_peer() { - let (handle, _sock) = spawn_daemon().await; - let mut ws = open_ws(handle.ws_addr()).await; - let resp = send_handshake_with_floors( - &mut ws, - "1.0", + "1.1", env!("CARGO_PKG_VERSION"), Some("0.0.0"), Some("1.0"), @@ -182,7 +157,7 @@ async fn handshake_skew_when_protocol_1_0_peer() { .await; match resp.body { ResponseBody::Ok(_) => {} - other => panic!("protocol 1.0 peer should connect with skew, got {other:?}"), + other => panic!("minor protocol drift should warn-but-allow, got {other:?}"), } let state = handle.state(); let client = state @@ -235,7 +210,7 @@ async fn handshake_legacy_ext_without_protocol_floor_still_ok() { let mut ws = open_ws(handle.ws_addr()).await; let resp = send_handshake_with_floors( &mut ws, - "1.1", + "1.0", env!("CARGO_PKG_VERSION"), Some("0.1.0"), None, @@ -260,7 +235,7 @@ async fn status_surfaces_version_skew_for_skewed_browser() { browser_name: "chrome".into(), browser_version: "131.0".into(), extension_version: "9.9.9".into(), - extension_protocol_version: "1.2".into(), + extension_protocol_version: "1.1".into(), label: "Older".into(), sink: bsk::daemon::browsers::BrowserSink { tx }, pending: Mutex::new(bsk::daemon::browsers::Pending::default()), @@ -288,8 +263,8 @@ async fn status_surfaces_version_skew_for_skewed_browser() { .iter() .find(|s| s.instance_id == "skew-only-test") .expect("status must list our skew client"); - assert_eq!(skew.client_protocol_version, "1.2"); - assert_eq!(skew.server_protocol_version, "1.1"); + assert_eq!(skew.client_protocol_version, "1.1"); + assert_eq!(skew.server_protocol_version, "1.0"); assert_eq!(skew.client_version, "9.9.9"); let entry = status .browsers @@ -306,7 +281,7 @@ async fn handshake_rejects_when_local_below_peer_min_compatible_protocol() { let mut ws = open_ws(handle.ws_addr()).await; let resp = send_handshake_with_floors( &mut ws, - "1.1", + "1.0", env!("CARGO_PKG_VERSION"), Some("0.0.0"), Some("99.0.0"), diff --git a/crates/bsk-cli/tests/per_session_queue.rs b/crates/bsk-cli/tests/per_session_queue.rs index 1bd7fa5e..79b53559 100644 --- a/crates/bsk-cli/tests/per_session_queue.rs +++ b/crates/bsk-cli/tests/per_session_queue.rs @@ -81,7 +81,7 @@ async fn handshake_as_ext( let params = HandshakeParams { client: "browser-skill-extension".into(), version: "0.1.0-dev.0".parse().unwrap(), - protocol_version: "1.1".into(), + protocol_version: "1.0".into(), instance_id: TEST_EXT_ID.into(), browser: BrowserPeerInfo { name: "chrome".into(), diff --git a/crates/bsk-cli/tests/session_user_interrupt.rs b/crates/bsk-cli/tests/session_user_interrupt.rs index 4ae1ce46..9c5288cc 100644 --- a/crates/bsk-cli/tests/session_user_interrupt.rs +++ b/crates/bsk-cli/tests/session_user_interrupt.rs @@ -81,7 +81,7 @@ async fn handshake_as_ext( let params = HandshakeParams { client: "browser-skill-extension".into(), version: "0.1.0-dev.0".parse().unwrap(), - protocol_version: "1.1".into(), + protocol_version: "1.0".into(), instance_id: TEST_EXT_ID.into(), browser: BrowserPeerInfo { name: "chrome".into(), diff --git a/crates/bsk-cli/tests/sessions_ipc.rs b/crates/bsk-cli/tests/sessions_ipc.rs index e267a3b7..69c87c93 100644 --- a/crates/bsk-cli/tests/sessions_ipc.rs +++ b/crates/bsk-cli/tests/sessions_ipc.rs @@ -84,7 +84,7 @@ async fn handshake_as_ext( let params = HandshakeParams { client: "browser-skill-extension".into(), version: "0.1.0-dev.0".parse().unwrap(), - protocol_version: "1.1".into(), + protocol_version: "1.0".into(), instance_id: TEST_EXT_ID.into(), browser: BrowserPeerInfo { name: "chrome".into(), @@ -400,7 +400,7 @@ async fn connect_second_ext( let params = HandshakeParams { client: "browser-skill-extension".into(), version: "0.1.0-dev.0".parse().unwrap(), - protocol_version: "1.1".into(), + protocol_version: "1.0".into(), instance_id: instance_id.into(), browser: BrowserPeerInfo { name: "edge".into(), diff --git a/crates/bsk-cli/tests/status_cmd.rs b/crates/bsk-cli/tests/status_cmd.rs index 18513e2e..0e329146 100644 --- a/crates/bsk-cli/tests/status_cmd.rs +++ b/crates/bsk-cli/tests/status_cmd.rs @@ -61,7 +61,7 @@ fn bsk_status_json_returns_structured_payload() { assert!(parsed["pid"].as_u64().unwrap() > 0); assert!(!parsed["daemon_version"].as_str().unwrap().is_empty()); - assert_eq!(parsed["protocol_version"], "1.1"); + assert_eq!(parsed["protocol_version"], "1.0"); assert!(parsed["sock_path"].as_str().is_some()); assert_eq!(parsed["browsers"], serde_json::json!([])); assert_eq!(parsed["sessions"], serde_json::json!([])); diff --git a/crates/bsk-cli/tests/tools_ipc.rs b/crates/bsk-cli/tests/tools_ipc.rs index b64ff9bd..d40bb9ca 100644 --- a/crates/bsk-cli/tests/tools_ipc.rs +++ b/crates/bsk-cli/tests/tools_ipc.rs @@ -73,7 +73,7 @@ async fn do_handshake(ws: &mut Ws) -> HandshakeResult { let params = HandshakeParams { client: "browser-skill-extension".into(), version: "0.1.0-dev.0".parse().unwrap(), - protocol_version: "1.1".into(), + protocol_version: "1.0".into(), instance_id: TEST_EXT_ID.into(), browser: BrowserPeerInfo { name: "chrome".into(), diff --git a/crates/bsk-cli/tests/tools_m7_ipc.rs b/crates/bsk-cli/tests/tools_m7_ipc.rs index 4635828e..ee74b5e5 100644 --- a/crates/bsk-cli/tests/tools_m7_ipc.rs +++ b/crates/bsk-cli/tests/tools_m7_ipc.rs @@ -76,7 +76,7 @@ async fn do_handshake(ws: &mut Ws) -> HandshakeResult { let params = HandshakeParams { client: "browser-skill-extension".into(), version: "0.1.0-dev.0".parse().unwrap(), - protocol_version: "1.1".into(), + protocol_version: "1.0".into(), instance_id: TEST_EXT_ID.into(), browser: BrowserPeerInfo { name: "chrome".into(), diff --git a/crates/bsk-cli/tests/tools_m8_ipc.rs b/crates/bsk-cli/tests/tools_m8_ipc.rs index 777d2128..14e7c72a 100644 --- a/crates/bsk-cli/tests/tools_m8_ipc.rs +++ b/crates/bsk-cli/tests/tools_m8_ipc.rs @@ -75,7 +75,7 @@ async fn do_handshake(ws: &mut Ws) -> HandshakeResult { let params = HandshakeParams { client: "browser-skill-extension".into(), version: "0.1.0-dev.0".parse().unwrap(), - protocol_version: "1.1".into(), + protocol_version: "1.0".into(), instance_id: TEST_EXT_ID.into(), browser: BrowserPeerInfo { name: "chrome".into(), diff --git a/crates/bsk-cli/tests/tools_m9_ipc.rs b/crates/bsk-cli/tests/tools_m9_ipc.rs index f5015cac..2f8b46d9 100644 --- a/crates/bsk-cli/tests/tools_m9_ipc.rs +++ b/crates/bsk-cli/tests/tools_m9_ipc.rs @@ -85,7 +85,7 @@ async fn do_handshake(ws: &mut Ws) -> HandshakeResult { let params = HandshakeParams { client: "browser-skill-extension".into(), version: "0.1.0-dev.0".parse().unwrap(), - protocol_version: "1.1".into(), + protocol_version: "1.0".into(), instance_id: TEST_EXT_ID.into(), browser: BrowserPeerInfo { name: "chrome".into(), diff --git a/crates/bsk-cli/tests/ws_handshake.rs b/crates/bsk-cli/tests/ws_handshake.rs index be413d27..c6e2da14 100644 --- a/crates/bsk-cli/tests/ws_handshake.rs +++ b/crates/bsk-cli/tests/ws_handshake.rs @@ -53,7 +53,7 @@ pub async fn send_handshake( let params = HandshakeParams { client: "browser-skill-extension".into(), version: "0.1.0-dev.0".parse().unwrap(), - protocol_version: "1.1".into(), + protocol_version: "1.0".into(), instance_id: instance_id.into(), browser: BrowserPeerInfo { name: "chrome".into(), @@ -91,7 +91,7 @@ async fn ws_handshake_registers_browser_in_state() { let mut ws = connect_ext(handle.ws_addr(), &origin).await; let result = send_handshake(&mut ws, TEST_EXT_ID).await; assert_eq!(result.server, "browser-skill-daemon"); - assert_eq!(result.protocol_version, "1.1"); + assert_eq!(result.protocol_version, "1.0"); let state = handle.state(); let browsers = state.browsers.snapshot(); diff --git a/crates/bsk-protocol/schema/tool_record_await_result.json b/crates/bsk-protocol/schema/tool_record_await_result.json index a16fdf89..dee452f4 100644 --- a/crates/bsk-protocol/schema/tool_record_await_result.json +++ b/crates/bsk-protocol/schema/tool_record_await_result.json @@ -41,7 +41,7 @@ "browser" ] }, - "PageRef": { + "PageRefV2": { "description": "Page context dictionary entry — referenced by steps via `page` id.", "type": "object", "required": [ @@ -69,7 +69,7 @@ "$ref": "#/definitions/TraceV2" }, { - "$ref": "#/definitions/Trace" + "$ref": "#/definitions/TraceV3" } ] }, @@ -90,7 +90,7 @@ } } }, - "SelectedOption": { + "SelectedOptionV2": { "description": "One selected option (`select` op).", "type": "object", "required": [ @@ -108,7 +108,7 @@ } } }, - "SelectedOptionV2": { + "SelectedOptionV3": { "description": "One selected option (`select` op).", "type": "object", "required": [ @@ -126,23 +126,52 @@ } } }, - "Step": { - "description": "One recorded user action — discriminated union by `op`.", + "StepEffectV2": { + "description": "Observed navigation after a step (objective fact only).", + "type": "object", + "required": [ + "navigated_to" + ], + "properties": { + "navigated_to": { + "description": "Reference into `pages[]` for the destination page.", + "type": "string" + } + } + }, + "StepResultV3": { + "type": "object", + "required": [ + "state" + ], + "properties": { + "state": { + "type": "string" + } + } + }, + "StepV2": { + "description": "One recorded user action — discriminated union by `op` (v2).", "oneOf": [ { - "description": "Fields shared by every step variant (flattened in JSON).", + "description": "Fields shared by every v2 step variant (flattened in JSON).", "type": "object", "required": [ - "cause", "id", "op", - "result", - "state", + "page", "to" ], "properties": { - "cause": { - "$ref": "#/definitions/NavigationCause" + "effect": { + "anyOf": [ + { + "$ref": "#/definitions/StepEffectV2" + }, + { + "type": "null" + } + ] }, "id": { "type": "integer", @@ -155,11 +184,8 @@ "navigate" ] }, - "result": { - "$ref": "#/definitions/StepResult" - }, - "state": { - "description": "Observation id immediately before this action.", + "page": { + "description": "Reference into `pages[]`.", "type": "string" }, "to": { @@ -168,16 +194,25 @@ } }, { - "description": "Fields shared by every step variant (flattened in JSON).", + "description": "Fields shared by every v2 step variant (flattened in JSON).", "type": "object", "required": [ "id", "op", - "result", - "state", + "page", "target" ], "properties": { + "effect": { + "anyOf": [ + { + "$ref": "#/definitions/StepEffectV2" + }, + { + "type": "null" + } + ] + }, "id": { "type": "integer", "format": "uint32", @@ -189,29 +224,35 @@ "click" ] }, - "result": { - "$ref": "#/definitions/StepResult" - }, - "state": { - "description": "Observation id immediately before this action.", + "page": { + "description": "Reference into `pages[]`.", "type": "string" }, "target": { - "$ref": "#/definitions/TargetDescriptor" + "$ref": "#/definitions/TargetDescriptorV2" } } }, { - "description": "Fields shared by every step variant (flattened in JSON).", + "description": "Fields shared by every v2 step variant (flattened in JSON).", "type": "object", "required": [ "id", "op", - "result", - "state", + "page", "target" ], "properties": { + "effect": { + "anyOf": [ + { + "$ref": "#/definitions/StepEffectV2" + }, + { + "type": "null" + } + ] + }, "id": { "type": "integer", "format": "uint32", @@ -223,33 +264,35 @@ "hover" ] }, - "result": { - "$ref": "#/definitions/StepResult" - }, - "state": { - "description": "Observation id immediately before this action.", + "page": { + "description": "Reference into `pages[]`.", "type": "string" }, "target": { - "$ref": "#/definitions/TargetDescriptor" + "$ref": "#/definitions/TargetDescriptorV2" } } }, { - "description": "Fields shared by every step variant (flattened in JSON).", + "description": "Fields shared by every v2 step variant (flattened in JSON).", "type": "object", "required": [ - "commit", "id", "op", - "result", - "state", + "page", "target", "value" ], "properties": { - "commit": { - "$ref": "#/definitions/FillCommit" + "effect": { + "anyOf": [ + { + "$ref": "#/definitions/StepEffectV2" + }, + { + "type": "null" + } + ] }, "id": { "type": "integer", @@ -262,18 +305,18 @@ "fill" ] }, - "redacted": { - "type": "boolean" - }, - "result": { - "$ref": "#/definitions/StepResult" - }, - "state": { - "description": "Observation id immediately before this action.", + "page": { + "description": "Reference into `pages[]`.", "type": "string" }, + "redacted": { + "type": [ + "boolean", + "null" + ] + }, "target": { - "$ref": "#/definitions/TargetDescriptor" + "$ref": "#/definitions/TargetDescriptorV2" }, "value": { "type": "string" @@ -281,16 +324,26 @@ } }, { - "description": "Fields shared by every step variant (flattened in JSON).", + "description": "Fields shared by every v2 step variant (flattened in JSON).", "type": "object", "required": [ "id", "op", - "result", - "state", + "page", + "selection", "target" ], "properties": { + "effect": { + "anyOf": [ + { + "$ref": "#/definitions/StepEffectV2" + }, + { + "type": "null" + } + ] + }, "id": { "type": "integer", "format": "uint32", @@ -302,35 +355,41 @@ "select" ] }, - "result": { - "$ref": "#/definitions/StepResult" + "page": { + "description": "Reference into `pages[]`.", + "type": "string" }, "selection": { "type": "array", "items": { - "$ref": "#/definitions/SelectedOption" + "$ref": "#/definitions/SelectedOptionV2" } }, - "state": { - "description": "Observation id immediately before this action.", - "type": "string" - }, "target": { - "$ref": "#/definitions/TargetDescriptor" + "$ref": "#/definitions/TargetDescriptorV2" } } }, { - "description": "Fields shared by every step variant (flattened in JSON).", + "description": "Fields shared by every v2 step variant (flattened in JSON).", "type": "object", "required": [ "id", "key", "op", - "result", - "state" + "page" ], "properties": { + "effect": { + "anyOf": [ + { + "$ref": "#/definitions/StepEffectV2" + }, + { + "type": "null" + } + ] + }, "id": { "type": "integer", "format": "uint32", @@ -354,17 +413,14 @@ "press" ] }, - "result": { - "$ref": "#/definitions/StepResult" - }, - "state": { - "description": "Observation id immediately before this action.", + "page": { + "description": "Reference into `pages[]`.", "type": "string" }, "target": { "anyOf": [ { - "$ref": "#/definitions/TargetDescriptor" + "$ref": "#/definitions/TargetDescriptorV2" }, { "type": "null" @@ -372,17 +428,27 @@ ] } } - }, + } + ] + }, + "StepV3": { + "description": "One recorded user action — discriminated union by `op`.", + "oneOf": [ { "description": "Fields shared by every step variant (flattened in JSON).", "type": "object", "required": [ + "cause", "id", "op", "result", - "state" + "state", + "to" ], "properties": { + "cause": { + "$ref": "#/definitions/NavigationCause" + }, "id": { "type": "integer", "format": "uint32", @@ -391,67 +457,32 @@ "op": { "type": "string", "enum": [ - "scroll" + "navigate" ] }, "result": { - "$ref": "#/definitions/StepResult" + "$ref": "#/definitions/StepResultV3" }, "state": { "description": "Observation id immediately before this action.", "type": "string" + }, + "to": { + "type": "string" } } - } - ] - }, - "StepEffectV2": { - "description": "Observed navigation after a step (objective fact only).", - "type": "object", - "required": [ - "navigated_to" - ], - "properties": { - "navigated_to": { - "description": "Reference into `pages[]` for the destination page.", - "type": "string" - } - } - }, - "StepResult": { - "type": "object", - "required": [ - "state" - ], - "properties": { - "state": { - "type": "string" - } - } - }, - "StepV2": { - "description": "One recorded user action — discriminated union by `op` (v2).", - "oneOf": [ + }, { - "description": "Fields shared by every v2 step variant (flattened in JSON).", + "description": "Fields shared by every step variant (flattened in JSON).", "type": "object", "required": [ "id", "op", - "page", - "to" + "result", + "state", + "target" ], "properties": { - "effect": { - "anyOf": [ - { - "$ref": "#/definitions/StepEffectV2" - }, - { - "type": "null" - } - ] - }, "id": { "type": "integer", "format": "uint32", @@ -460,78 +491,32 @@ "op": { "type": "string", "enum": [ - "navigate" + "click" ] }, - "page": { - "description": "Reference into `pages[]`.", - "type": "string" + "result": { + "$ref": "#/definitions/StepResultV3" }, - "to": { + "state": { + "description": "Observation id immediately before this action.", "type": "string" + }, + "target": { + "$ref": "#/definitions/TargetDescriptorV3" } } }, { - "description": "Fields shared by every v2 step variant (flattened in JSON).", + "description": "Fields shared by every step variant (flattened in JSON).", "type": "object", "required": [ "id", "op", - "page", - "target" - ], - "properties": { - "effect": { - "anyOf": [ - { - "$ref": "#/definitions/StepEffectV2" - }, - { - "type": "null" - } - ] - }, - "id": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, - "op": { - "type": "string", - "enum": [ - "click" - ] - }, - "page": { - "description": "Reference into `pages[]`.", - "type": "string" - }, - "target": { - "$ref": "#/definitions/TargetDescriptorV2" - } - } - }, - { - "description": "Fields shared by every v2 step variant (flattened in JSON).", - "type": "object", - "required": [ - "id", - "op", - "page", + "result", + "state", "target" ], "properties": { - "effect": { - "anyOf": [ - { - "$ref": "#/definitions/StepEffectV2" - }, - { - "type": "null" - } - ] - }, "id": { "type": "integer", "format": "uint32", @@ -543,35 +528,33 @@ "hover" ] }, - "page": { - "description": "Reference into `pages[]`.", + "result": { + "$ref": "#/definitions/StepResultV3" + }, + "state": { + "description": "Observation id immediately before this action.", "type": "string" }, "target": { - "$ref": "#/definitions/TargetDescriptorV2" + "$ref": "#/definitions/TargetDescriptorV3" } } }, { - "description": "Fields shared by every v2 step variant (flattened in JSON).", + "description": "Fields shared by every step variant (flattened in JSON).", "type": "object", "required": [ + "commit", "id", "op", - "page", + "result", + "state", "target", "value" ], "properties": { - "effect": { - "anyOf": [ - { - "$ref": "#/definitions/StepEffectV2" - }, - { - "type": "null" - } - ] + "commit": { + "$ref": "#/definitions/FillCommit" }, "id": { "type": "integer", @@ -584,18 +567,18 @@ "fill" ] }, - "page": { - "description": "Reference into `pages[]`.", - "type": "string" - }, "redacted": { - "type": [ - "boolean", - "null" - ] + "type": "boolean" + }, + "result": { + "$ref": "#/definitions/StepResultV3" + }, + "state": { + "description": "Observation id immediately before this action.", + "type": "string" }, "target": { - "$ref": "#/definitions/TargetDescriptorV2" + "$ref": "#/definitions/TargetDescriptorV3" }, "value": { "type": "string" @@ -603,26 +586,16 @@ } }, { - "description": "Fields shared by every v2 step variant (flattened in JSON).", + "description": "Fields shared by every step variant (flattened in JSON).", "type": "object", "required": [ "id", "op", - "page", - "selection", + "result", + "state", "target" ], "properties": { - "effect": { - "anyOf": [ - { - "$ref": "#/definitions/StepEffectV2" - }, - { - "type": "null" - } - ] - }, "id": { "type": "integer", "format": "uint32", @@ -634,41 +607,35 @@ "select" ] }, - "page": { - "description": "Reference into `pages[]`.", - "type": "string" + "result": { + "$ref": "#/definitions/StepResultV3" }, "selection": { "type": "array", "items": { - "$ref": "#/definitions/SelectedOptionV2" + "$ref": "#/definitions/SelectedOptionV3" } }, + "state": { + "description": "Observation id immediately before this action.", + "type": "string" + }, "target": { - "$ref": "#/definitions/TargetDescriptorV2" + "$ref": "#/definitions/TargetDescriptorV3" } } }, { - "description": "Fields shared by every v2 step variant (flattened in JSON).", + "description": "Fields shared by every step variant (flattened in JSON).", "type": "object", "required": [ "id", "key", "op", - "page" + "result", + "state" ], "properties": { - "effect": { - "anyOf": [ - { - "$ref": "#/definitions/StepEffectV2" - }, - { - "type": "null" - } - ] - }, "id": { "type": "integer", "format": "uint32", @@ -692,14 +659,17 @@ "press" ] }, - "page": { - "description": "Reference into `pages[]`.", + "result": { + "$ref": "#/definitions/StepResultV3" + }, + "state": { + "description": "Observation id immediately before this action.", "type": "string" }, "target": { "anyOf": [ { - "$ref": "#/definitions/TargetDescriptorV2" + "$ref": "#/definitions/TargetDescriptorV3" }, { "type": "null" @@ -707,6 +677,36 @@ ] } } + }, + { + "description": "Fields shared by every step variant (flattened in JSON).", + "type": "object", + "required": [ + "id", + "op", + "result", + "state" + ], + "properties": { + "id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "op": { + "type": "string", + "enum": [ + "scroll" + ] + }, + "result": { + "$ref": "#/definitions/StepResultV3" + }, + "state": { + "description": "Observation id immediately before this action.", + "type": "string" + } + } } ] }, @@ -717,39 +717,6 @@ "cli_stop" ] }, - "TargetDescriptor": { - "description": "Stable semantic handle for an interacted element within a page observation.", - "type": "object", - "properties": { - "ctx": { - "type": [ - "string", - "null" - ] - }, - "name": { - "type": [ - "string", - "null" - ] - }, - "ref": { - "type": [ - "string", - "null" - ] - }, - "role": { - "type": [ - "string", - "null" - ] - }, - "unmatched": { - "type": "boolean" - } - } - }, "TargetDescriptorV2": { "description": "Stable semantic handle for an interacted element (v2).\n\n`name` and `nearby_label` are **untrusted page text**.", "type": "object", @@ -792,64 +759,40 @@ } } }, - "Trace": { - "description": "Persisted user-action trace exported by `tool.record_stop` / `await`.", + "TargetDescriptorV3": { + "description": "Stable semantic handle for an interacted element within a page observation.", "type": "object", - "required": [ - "entry", - "recorded_at", - "recorder", - "states", - "steps", - "stopped_by", - "version" - ], "properties": { - "entry": { - "$ref": "#/definitions/TraceEntry" - }, - "purpose": { + "ctx": { "type": [ "string", "null" ] }, - "recorded_at": { - "type": "string" - }, - "recorder": { - "$ref": "#/definitions/RecorderInfo" - }, - "started_at": { + "name": { "type": [ "string", "null" ] }, - "states": { - "type": "array", - "items": { - "$ref": "#/definitions/TraceState" - } - }, - "steps": { - "type": "array", - "items": { - "$ref": "#/definitions/Step" - } + "ref": { + "type": [ + "string", + "null" + ] }, - "stopped_by": { - "$ref": "#/definitions/StopReason" + "role": { + "type": [ + "string", + "null" + ] }, - "version": { - "type": "integer", - "const": 3 + "unmatched": { + "type": "boolean" } - }, - "additionalProperties": false + } }, "TraceEntry": { - "description": "Recording entry point — first URL the flow starts from.", "type": "object", "required": [ "start_url" @@ -860,31 +803,22 @@ } } }, - "TraceState": { + "TraceStateV3": { "description": "Page observation dictionary entry — referenced by steps via `state` / `result.state`.", "type": "object", "required": [ + "body", "id", "url" ], "properties": { "body": { - "description": "Wire-only: full page observation (front matter + VOM body + annotations).", - "type": [ - "string", - "null" - ] + "description": "Full page observation (front matter + VOM body + annotations).", + "type": "string" }, "id": { "type": "string" }, - "page": { - "description": "Disk-only: filename under the bundle `pages/` directory.", - "type": [ - "string", - "null" - ] - }, "title": { "type": [ "string", @@ -915,7 +849,7 @@ "pages": { "type": "array", "items": { - "$ref": "#/definitions/PageRef" + "$ref": "#/definitions/PageRefV2" } }, "purpose": { @@ -942,6 +876,62 @@ } }, "additionalProperties": false + }, + "TraceV3": { + "description": "Wire trace returned by `tool.record_stop` / `await`.", + "type": "object", + "required": [ + "entry", + "recorded_at", + "recorder", + "states", + "steps", + "stopped_by", + "version" + ], + "properties": { + "entry": { + "$ref": "#/definitions/TraceEntry" + }, + "purpose": { + "type": [ + "string", + "null" + ] + }, + "recorded_at": { + "type": "string" + }, + "recorder": { + "$ref": "#/definitions/RecorderInfo" + }, + "started_at": { + "type": [ + "string", + "null" + ] + }, + "states": { + "type": "array", + "items": { + "$ref": "#/definitions/TraceStateV3" + } + }, + "steps": { + "type": "array", + "items": { + "$ref": "#/definitions/StepV3" + } + }, + "stopped_by": { + "$ref": "#/definitions/StopReason" + }, + "version": { + "type": "integer", + "const": 3 + } + }, + "additionalProperties": false } } } diff --git a/crates/bsk-protocol/schema/tool_record_start_params.json b/crates/bsk-protocol/schema/tool_record_start_params.json index e81c2618..859ebf82 100644 --- a/crates/bsk-protocol/schema/tool_record_start_params.json +++ b/crates/bsk-protocol/schema/tool_record_start_params.json @@ -6,26 +6,12 @@ "session_id" ], "properties": { - "max_page_tokens": { - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, "purpose": { "type": [ "string", "null" ] }, - "redact_values": { - "type": [ - "boolean", - "null" - ] - }, "session_id": { "type": "string" }, @@ -36,15 +22,6 @@ ], "format": "int64" }, - "trace_version": { - "description": "Desired trace export format. Omitted ⇒ v2; `3` ⇒ state-linked v3 bundle.", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, "url": { "type": [ "string", diff --git a/crates/bsk-protocol/schema/tool_record_stop_result.json b/crates/bsk-protocol/schema/tool_record_stop_result.json index 6fe6d48e..5d1e04a9 100644 --- a/crates/bsk-protocol/schema/tool_record_stop_result.json +++ b/crates/bsk-protocol/schema/tool_record_stop_result.json @@ -41,7 +41,7 @@ "browser" ] }, - "PageRef": { + "PageRefV2": { "description": "Page context dictionary entry — referenced by steps via `page` id.", "type": "object", "required": [ @@ -69,7 +69,7 @@ "$ref": "#/definitions/TraceV2" }, { - "$ref": "#/definitions/Trace" + "$ref": "#/definitions/TraceV3" } ] }, @@ -90,7 +90,7 @@ } } }, - "SelectedOption": { + "SelectedOptionV2": { "description": "One selected option (`select` op).", "type": "object", "required": [ @@ -108,7 +108,7 @@ } } }, - "SelectedOptionV2": { + "SelectedOptionV3": { "description": "One selected option (`select` op).", "type": "object", "required": [ @@ -126,23 +126,52 @@ } } }, - "Step": { - "description": "One recorded user action — discriminated union by `op`.", + "StepEffectV2": { + "description": "Observed navigation after a step (objective fact only).", + "type": "object", + "required": [ + "navigated_to" + ], + "properties": { + "navigated_to": { + "description": "Reference into `pages[]` for the destination page.", + "type": "string" + } + } + }, + "StepResultV3": { + "type": "object", + "required": [ + "state" + ], + "properties": { + "state": { + "type": "string" + } + } + }, + "StepV2": { + "description": "One recorded user action — discriminated union by `op` (v2).", "oneOf": [ { - "description": "Fields shared by every step variant (flattened in JSON).", + "description": "Fields shared by every v2 step variant (flattened in JSON).", "type": "object", "required": [ - "cause", "id", "op", - "result", - "state", + "page", "to" ], "properties": { - "cause": { - "$ref": "#/definitions/NavigationCause" + "effect": { + "anyOf": [ + { + "$ref": "#/definitions/StepEffectV2" + }, + { + "type": "null" + } + ] }, "id": { "type": "integer", @@ -155,11 +184,8 @@ "navigate" ] }, - "result": { - "$ref": "#/definitions/StepResult" - }, - "state": { - "description": "Observation id immediately before this action.", + "page": { + "description": "Reference into `pages[]`.", "type": "string" }, "to": { @@ -168,16 +194,25 @@ } }, { - "description": "Fields shared by every step variant (flattened in JSON).", + "description": "Fields shared by every v2 step variant (flattened in JSON).", "type": "object", "required": [ "id", "op", - "result", - "state", + "page", "target" ], "properties": { + "effect": { + "anyOf": [ + { + "$ref": "#/definitions/StepEffectV2" + }, + { + "type": "null" + } + ] + }, "id": { "type": "integer", "format": "uint32", @@ -189,29 +224,35 @@ "click" ] }, - "result": { - "$ref": "#/definitions/StepResult" - }, - "state": { - "description": "Observation id immediately before this action.", + "page": { + "description": "Reference into `pages[]`.", "type": "string" }, "target": { - "$ref": "#/definitions/TargetDescriptor" + "$ref": "#/definitions/TargetDescriptorV2" } } }, { - "description": "Fields shared by every step variant (flattened in JSON).", + "description": "Fields shared by every v2 step variant (flattened in JSON).", "type": "object", "required": [ "id", "op", - "result", - "state", + "page", "target" ], "properties": { + "effect": { + "anyOf": [ + { + "$ref": "#/definitions/StepEffectV2" + }, + { + "type": "null" + } + ] + }, "id": { "type": "integer", "format": "uint32", @@ -223,33 +264,35 @@ "hover" ] }, - "result": { - "$ref": "#/definitions/StepResult" - }, - "state": { - "description": "Observation id immediately before this action.", + "page": { + "description": "Reference into `pages[]`.", "type": "string" }, "target": { - "$ref": "#/definitions/TargetDescriptor" + "$ref": "#/definitions/TargetDescriptorV2" } } }, { - "description": "Fields shared by every step variant (flattened in JSON).", + "description": "Fields shared by every v2 step variant (flattened in JSON).", "type": "object", "required": [ - "commit", "id", "op", - "result", - "state", + "page", "target", "value" ], "properties": { - "commit": { - "$ref": "#/definitions/FillCommit" + "effect": { + "anyOf": [ + { + "$ref": "#/definitions/StepEffectV2" + }, + { + "type": "null" + } + ] }, "id": { "type": "integer", @@ -262,18 +305,18 @@ "fill" ] }, - "redacted": { - "type": "boolean" - }, - "result": { - "$ref": "#/definitions/StepResult" - }, - "state": { - "description": "Observation id immediately before this action.", + "page": { + "description": "Reference into `pages[]`.", "type": "string" }, + "redacted": { + "type": [ + "boolean", + "null" + ] + }, "target": { - "$ref": "#/definitions/TargetDescriptor" + "$ref": "#/definitions/TargetDescriptorV2" }, "value": { "type": "string" @@ -281,16 +324,26 @@ } }, { - "description": "Fields shared by every step variant (flattened in JSON).", + "description": "Fields shared by every v2 step variant (flattened in JSON).", "type": "object", "required": [ "id", "op", - "result", - "state", + "page", + "selection", "target" ], "properties": { + "effect": { + "anyOf": [ + { + "$ref": "#/definitions/StepEffectV2" + }, + { + "type": "null" + } + ] + }, "id": { "type": "integer", "format": "uint32", @@ -302,35 +355,41 @@ "select" ] }, - "result": { - "$ref": "#/definitions/StepResult" + "page": { + "description": "Reference into `pages[]`.", + "type": "string" }, "selection": { "type": "array", "items": { - "$ref": "#/definitions/SelectedOption" + "$ref": "#/definitions/SelectedOptionV2" } }, - "state": { - "description": "Observation id immediately before this action.", - "type": "string" - }, "target": { - "$ref": "#/definitions/TargetDescriptor" + "$ref": "#/definitions/TargetDescriptorV2" } } }, { - "description": "Fields shared by every step variant (flattened in JSON).", + "description": "Fields shared by every v2 step variant (flattened in JSON).", "type": "object", "required": [ "id", "key", "op", - "result", - "state" + "page" ], "properties": { + "effect": { + "anyOf": [ + { + "$ref": "#/definitions/StepEffectV2" + }, + { + "type": "null" + } + ] + }, "id": { "type": "integer", "format": "uint32", @@ -354,17 +413,14 @@ "press" ] }, - "result": { - "$ref": "#/definitions/StepResult" - }, - "state": { - "description": "Observation id immediately before this action.", + "page": { + "description": "Reference into `pages[]`.", "type": "string" }, "target": { "anyOf": [ { - "$ref": "#/definitions/TargetDescriptor" + "$ref": "#/definitions/TargetDescriptorV2" }, { "type": "null" @@ -372,17 +428,27 @@ ] } } - }, + } + ] + }, + "StepV3": { + "description": "One recorded user action — discriminated union by `op`.", + "oneOf": [ { "description": "Fields shared by every step variant (flattened in JSON).", "type": "object", "required": [ + "cause", "id", "op", "result", - "state" + "state", + "to" ], "properties": { + "cause": { + "$ref": "#/definitions/NavigationCause" + }, "id": { "type": "integer", "format": "uint32", @@ -391,67 +457,32 @@ "op": { "type": "string", "enum": [ - "scroll" + "navigate" ] }, "result": { - "$ref": "#/definitions/StepResult" + "$ref": "#/definitions/StepResultV3" }, "state": { "description": "Observation id immediately before this action.", "type": "string" + }, + "to": { + "type": "string" } } - } - ] - }, - "StepEffectV2": { - "description": "Observed navigation after a step (objective fact only).", - "type": "object", - "required": [ - "navigated_to" - ], - "properties": { - "navigated_to": { - "description": "Reference into `pages[]` for the destination page.", - "type": "string" - } - } - }, - "StepResult": { - "type": "object", - "required": [ - "state" - ], - "properties": { - "state": { - "type": "string" - } - } - }, - "StepV2": { - "description": "One recorded user action — discriminated union by `op` (v2).", - "oneOf": [ + }, { - "description": "Fields shared by every v2 step variant (flattened in JSON).", + "description": "Fields shared by every step variant (flattened in JSON).", "type": "object", "required": [ "id", "op", - "page", - "to" + "result", + "state", + "target" ], "properties": { - "effect": { - "anyOf": [ - { - "$ref": "#/definitions/StepEffectV2" - }, - { - "type": "null" - } - ] - }, "id": { "type": "integer", "format": "uint32", @@ -460,78 +491,32 @@ "op": { "type": "string", "enum": [ - "navigate" + "click" ] }, - "page": { - "description": "Reference into `pages[]`.", - "type": "string" + "result": { + "$ref": "#/definitions/StepResultV3" }, - "to": { + "state": { + "description": "Observation id immediately before this action.", "type": "string" + }, + "target": { + "$ref": "#/definitions/TargetDescriptorV3" } } }, { - "description": "Fields shared by every v2 step variant (flattened in JSON).", + "description": "Fields shared by every step variant (flattened in JSON).", "type": "object", "required": [ "id", "op", - "page", - "target" - ], - "properties": { - "effect": { - "anyOf": [ - { - "$ref": "#/definitions/StepEffectV2" - }, - { - "type": "null" - } - ] - }, - "id": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, - "op": { - "type": "string", - "enum": [ - "click" - ] - }, - "page": { - "description": "Reference into `pages[]`.", - "type": "string" - }, - "target": { - "$ref": "#/definitions/TargetDescriptorV2" - } - } - }, - { - "description": "Fields shared by every v2 step variant (flattened in JSON).", - "type": "object", - "required": [ - "id", - "op", - "page", + "result", + "state", "target" ], "properties": { - "effect": { - "anyOf": [ - { - "$ref": "#/definitions/StepEffectV2" - }, - { - "type": "null" - } - ] - }, "id": { "type": "integer", "format": "uint32", @@ -543,35 +528,33 @@ "hover" ] }, - "page": { - "description": "Reference into `pages[]`.", + "result": { + "$ref": "#/definitions/StepResultV3" + }, + "state": { + "description": "Observation id immediately before this action.", "type": "string" }, "target": { - "$ref": "#/definitions/TargetDescriptorV2" + "$ref": "#/definitions/TargetDescriptorV3" } } }, { - "description": "Fields shared by every v2 step variant (flattened in JSON).", + "description": "Fields shared by every step variant (flattened in JSON).", "type": "object", "required": [ + "commit", "id", "op", - "page", + "result", + "state", "target", "value" ], "properties": { - "effect": { - "anyOf": [ - { - "$ref": "#/definitions/StepEffectV2" - }, - { - "type": "null" - } - ] + "commit": { + "$ref": "#/definitions/FillCommit" }, "id": { "type": "integer", @@ -584,18 +567,18 @@ "fill" ] }, - "page": { - "description": "Reference into `pages[]`.", - "type": "string" - }, "redacted": { - "type": [ - "boolean", - "null" - ] + "type": "boolean" + }, + "result": { + "$ref": "#/definitions/StepResultV3" + }, + "state": { + "description": "Observation id immediately before this action.", + "type": "string" }, "target": { - "$ref": "#/definitions/TargetDescriptorV2" + "$ref": "#/definitions/TargetDescriptorV3" }, "value": { "type": "string" @@ -603,26 +586,16 @@ } }, { - "description": "Fields shared by every v2 step variant (flattened in JSON).", + "description": "Fields shared by every step variant (flattened in JSON).", "type": "object", "required": [ "id", "op", - "page", - "selection", + "result", + "state", "target" ], "properties": { - "effect": { - "anyOf": [ - { - "$ref": "#/definitions/StepEffectV2" - }, - { - "type": "null" - } - ] - }, "id": { "type": "integer", "format": "uint32", @@ -634,41 +607,35 @@ "select" ] }, - "page": { - "description": "Reference into `pages[]`.", - "type": "string" + "result": { + "$ref": "#/definitions/StepResultV3" }, "selection": { "type": "array", "items": { - "$ref": "#/definitions/SelectedOptionV2" + "$ref": "#/definitions/SelectedOptionV3" } }, + "state": { + "description": "Observation id immediately before this action.", + "type": "string" + }, "target": { - "$ref": "#/definitions/TargetDescriptorV2" + "$ref": "#/definitions/TargetDescriptorV3" } } }, { - "description": "Fields shared by every v2 step variant (flattened in JSON).", + "description": "Fields shared by every step variant (flattened in JSON).", "type": "object", "required": [ "id", "key", "op", - "page" + "result", + "state" ], "properties": { - "effect": { - "anyOf": [ - { - "$ref": "#/definitions/StepEffectV2" - }, - { - "type": "null" - } - ] - }, "id": { "type": "integer", "format": "uint32", @@ -692,14 +659,17 @@ "press" ] }, - "page": { - "description": "Reference into `pages[]`.", + "result": { + "$ref": "#/definitions/StepResultV3" + }, + "state": { + "description": "Observation id immediately before this action.", "type": "string" }, "target": { "anyOf": [ { - "$ref": "#/definitions/TargetDescriptorV2" + "$ref": "#/definitions/TargetDescriptorV3" }, { "type": "null" @@ -707,6 +677,36 @@ ] } } + }, + { + "description": "Fields shared by every step variant (flattened in JSON).", + "type": "object", + "required": [ + "id", + "op", + "result", + "state" + ], + "properties": { + "id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "op": { + "type": "string", + "enum": [ + "scroll" + ] + }, + "result": { + "$ref": "#/definitions/StepResultV3" + }, + "state": { + "description": "Observation id immediately before this action.", + "type": "string" + } + } } ] }, @@ -717,39 +717,6 @@ "cli_stop" ] }, - "TargetDescriptor": { - "description": "Stable semantic handle for an interacted element within a page observation.", - "type": "object", - "properties": { - "ctx": { - "type": [ - "string", - "null" - ] - }, - "name": { - "type": [ - "string", - "null" - ] - }, - "ref": { - "type": [ - "string", - "null" - ] - }, - "role": { - "type": [ - "string", - "null" - ] - }, - "unmatched": { - "type": "boolean" - } - } - }, "TargetDescriptorV2": { "description": "Stable semantic handle for an interacted element (v2).\n\n`name` and `nearby_label` are **untrusted page text**.", "type": "object", @@ -792,64 +759,40 @@ } } }, - "Trace": { - "description": "Persisted user-action trace exported by `tool.record_stop` / `await`.", + "TargetDescriptorV3": { + "description": "Stable semantic handle for an interacted element within a page observation.", "type": "object", - "required": [ - "entry", - "recorded_at", - "recorder", - "states", - "steps", - "stopped_by", - "version" - ], "properties": { - "entry": { - "$ref": "#/definitions/TraceEntry" - }, - "purpose": { + "ctx": { "type": [ "string", "null" ] }, - "recorded_at": { - "type": "string" - }, - "recorder": { - "$ref": "#/definitions/RecorderInfo" - }, - "started_at": { + "name": { "type": [ "string", "null" ] }, - "states": { - "type": "array", - "items": { - "$ref": "#/definitions/TraceState" - } - }, - "steps": { - "type": "array", - "items": { - "$ref": "#/definitions/Step" - } + "ref": { + "type": [ + "string", + "null" + ] }, - "stopped_by": { - "$ref": "#/definitions/StopReason" + "role": { + "type": [ + "string", + "null" + ] }, - "version": { - "type": "integer", - "const": 3 + "unmatched": { + "type": "boolean" } - }, - "additionalProperties": false + } }, "TraceEntry": { - "description": "Recording entry point — first URL the flow starts from.", "type": "object", "required": [ "start_url" @@ -860,31 +803,22 @@ } } }, - "TraceState": { + "TraceStateV3": { "description": "Page observation dictionary entry — referenced by steps via `state` / `result.state`.", "type": "object", "required": [ + "body", "id", "url" ], "properties": { "body": { - "description": "Wire-only: full page observation (front matter + VOM body + annotations).", - "type": [ - "string", - "null" - ] + "description": "Full page observation (front matter + VOM body + annotations).", + "type": "string" }, "id": { "type": "string" }, - "page": { - "description": "Disk-only: filename under the bundle `pages/` directory.", - "type": [ - "string", - "null" - ] - }, "title": { "type": [ "string", @@ -915,7 +849,7 @@ "pages": { "type": "array", "items": { - "$ref": "#/definitions/PageRef" + "$ref": "#/definitions/PageRefV2" } }, "purpose": { @@ -942,6 +876,62 @@ } }, "additionalProperties": false + }, + "TraceV3": { + "description": "Wire trace returned by `tool.record_stop` / `await`.", + "type": "object", + "required": [ + "entry", + "recorded_at", + "recorder", + "states", + "steps", + "stopped_by", + "version" + ], + "properties": { + "entry": { + "$ref": "#/definitions/TraceEntry" + }, + "purpose": { + "type": [ + "string", + "null" + ] + }, + "recorded_at": { + "type": "string" + }, + "recorder": { + "$ref": "#/definitions/RecorderInfo" + }, + "started_at": { + "type": [ + "string", + "null" + ] + }, + "states": { + "type": "array", + "items": { + "$ref": "#/definitions/TraceStateV3" + } + }, + "steps": { + "type": "array", + "items": { + "$ref": "#/definitions/StepV3" + } + }, + "stopped_by": { + "$ref": "#/definitions/StopReason" + }, + "version": { + "type": "integer", + "const": 3 + } + }, + "additionalProperties": false } } } diff --git a/crates/bsk-protocol/schema/trace.json b/crates/bsk-protocol/schema/trace.json index 2abfd555..750ce6a7 100644 --- a/crates/bsk-protocol/schema/trace.json +++ b/crates/bsk-protocol/schema/trace.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Trace", - "description": "Persisted user-action trace exported by `tool.record_stop` / `await`.", + "title": "TraceV3", + "description": "Wire trace returned by `tool.record_stop` / `await`.", "type": "object", "required": [ "entry", @@ -37,13 +37,13 @@ "states": { "type": "array", "items": { - "$ref": "#/definitions/TraceState" + "$ref": "#/definitions/TraceStateV3" } }, "steps": { "type": "array", "items": { - "$ref": "#/definitions/Step" + "$ref": "#/definitions/StepV3" } }, "stopped_by": { @@ -103,7 +103,7 @@ } } }, - "SelectedOption": { + "SelectedOptionV3": { "description": "One selected option (`select` op).", "type": "object", "required": [ @@ -121,7 +121,18 @@ } } }, - "Step": { + "StepResultV3": { + "type": "object", + "required": [ + "state" + ], + "properties": { + "state": { + "type": "string" + } + } + }, + "StepV3": { "description": "One recorded user action — discriminated union by `op`.", "oneOf": [ { @@ -151,7 +162,7 @@ ] }, "result": { - "$ref": "#/definitions/StepResult" + "$ref": "#/definitions/StepResultV3" }, "state": { "description": "Observation id immediately before this action.", @@ -185,14 +196,14 @@ ] }, "result": { - "$ref": "#/definitions/StepResult" + "$ref": "#/definitions/StepResultV3" }, "state": { "description": "Observation id immediately before this action.", "type": "string" }, "target": { - "$ref": "#/definitions/TargetDescriptor" + "$ref": "#/definitions/TargetDescriptorV3" } } }, @@ -219,14 +230,14 @@ ] }, "result": { - "$ref": "#/definitions/StepResult" + "$ref": "#/definitions/StepResultV3" }, "state": { "description": "Observation id immediately before this action.", "type": "string" }, "target": { - "$ref": "#/definitions/TargetDescriptor" + "$ref": "#/definitions/TargetDescriptorV3" } } }, @@ -261,14 +272,14 @@ "type": "boolean" }, "result": { - "$ref": "#/definitions/StepResult" + "$ref": "#/definitions/StepResultV3" }, "state": { "description": "Observation id immediately before this action.", "type": "string" }, "target": { - "$ref": "#/definitions/TargetDescriptor" + "$ref": "#/definitions/TargetDescriptorV3" }, "value": { "type": "string" @@ -298,12 +309,12 @@ ] }, "result": { - "$ref": "#/definitions/StepResult" + "$ref": "#/definitions/StepResultV3" }, "selection": { "type": "array", "items": { - "$ref": "#/definitions/SelectedOption" + "$ref": "#/definitions/SelectedOptionV3" } }, "state": { @@ -311,7 +322,7 @@ "type": "string" }, "target": { - "$ref": "#/definitions/TargetDescriptor" + "$ref": "#/definitions/TargetDescriptorV3" } } }, @@ -350,7 +361,7 @@ ] }, "result": { - "$ref": "#/definitions/StepResult" + "$ref": "#/definitions/StepResultV3" }, "state": { "description": "Observation id immediately before this action.", @@ -359,7 +370,7 @@ "target": { "anyOf": [ { - "$ref": "#/definitions/TargetDescriptor" + "$ref": "#/definitions/TargetDescriptorV3" }, { "type": "null" @@ -390,7 +401,7 @@ ] }, "result": { - "$ref": "#/definitions/StepResult" + "$ref": "#/definitions/StepResultV3" }, "state": { "description": "Observation id immediately before this action.", @@ -400,17 +411,6 @@ } ] }, - "StepResult": { - "type": "object", - "required": [ - "state" - ], - "properties": { - "state": { - "type": "string" - } - } - }, "StopReason": { "type": "string", "enum": [ @@ -418,7 +418,7 @@ "cli_stop" ] }, - "TargetDescriptor": { + "TargetDescriptorV3": { "description": "Stable semantic handle for an interacted element within a page observation.", "type": "object", "properties": { @@ -452,7 +452,6 @@ } }, "TraceEntry": { - "description": "Recording entry point — first URL the flow starts from.", "type": "object", "required": [ "start_url" @@ -463,31 +462,22 @@ } } }, - "TraceState": { + "TraceStateV3": { "description": "Page observation dictionary entry — referenced by steps via `state` / `result.state`.", "type": "object", "required": [ + "body", "id", "url" ], "properties": { "body": { - "description": "Wire-only: full page observation (front matter + VOM body + annotations).", - "type": [ - "string", - "null" - ] + "description": "Full page observation (front matter + VOM body + annotations).", + "type": "string" }, "id": { "type": "string" }, - "page": { - "description": "Disk-only: filename under the bundle `pages/` directory.", - "type": [ - "string", - "null" - ] - }, "title": { "type": [ "string", diff --git a/crates/bsk-protocol/schema/trace_step.json b/crates/bsk-protocol/schema/trace_step.json index 79f5e1e7..14a33ee1 100644 --- a/crates/bsk-protocol/schema/trace_step.json +++ b/crates/bsk-protocol/schema/trace_step.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Step", + "title": "StepV3", "description": "One recorded user action — discriminated union by `op`.", "oneOf": [ { @@ -30,7 +30,7 @@ ] }, "result": { - "$ref": "#/definitions/StepResult" + "$ref": "#/definitions/StepResultV3" }, "state": { "description": "Observation id immediately before this action.", @@ -64,14 +64,14 @@ ] }, "result": { - "$ref": "#/definitions/StepResult" + "$ref": "#/definitions/StepResultV3" }, "state": { "description": "Observation id immediately before this action.", "type": "string" }, "target": { - "$ref": "#/definitions/TargetDescriptor" + "$ref": "#/definitions/TargetDescriptorV3" } } }, @@ -98,14 +98,14 @@ ] }, "result": { - "$ref": "#/definitions/StepResult" + "$ref": "#/definitions/StepResultV3" }, "state": { "description": "Observation id immediately before this action.", "type": "string" }, "target": { - "$ref": "#/definitions/TargetDescriptor" + "$ref": "#/definitions/TargetDescriptorV3" } } }, @@ -140,14 +140,14 @@ "type": "boolean" }, "result": { - "$ref": "#/definitions/StepResult" + "$ref": "#/definitions/StepResultV3" }, "state": { "description": "Observation id immediately before this action.", "type": "string" }, "target": { - "$ref": "#/definitions/TargetDescriptor" + "$ref": "#/definitions/TargetDescriptorV3" }, "value": { "type": "string" @@ -177,12 +177,12 @@ ] }, "result": { - "$ref": "#/definitions/StepResult" + "$ref": "#/definitions/StepResultV3" }, "selection": { "type": "array", "items": { - "$ref": "#/definitions/SelectedOption" + "$ref": "#/definitions/SelectedOptionV3" } }, "state": { @@ -190,7 +190,7 @@ "type": "string" }, "target": { - "$ref": "#/definitions/TargetDescriptor" + "$ref": "#/definitions/TargetDescriptorV3" } } }, @@ -229,7 +229,7 @@ ] }, "result": { - "$ref": "#/definitions/StepResult" + "$ref": "#/definitions/StepResultV3" }, "state": { "description": "Observation id immediately before this action.", @@ -238,7 +238,7 @@ "target": { "anyOf": [ { - "$ref": "#/definitions/TargetDescriptor" + "$ref": "#/definitions/TargetDescriptorV3" }, { "type": "null" @@ -269,7 +269,7 @@ ] }, "result": { - "$ref": "#/definitions/StepResult" + "$ref": "#/definitions/StepResultV3" }, "state": { "description": "Observation id immediately before this action.", @@ -309,7 +309,7 @@ "browser" ] }, - "SelectedOption": { + "SelectedOptionV3": { "description": "One selected option (`select` op).", "type": "object", "required": [ @@ -327,7 +327,7 @@ } } }, - "StepResult": { + "StepResultV3": { "type": "object", "required": [ "state" @@ -338,7 +338,7 @@ } } }, - "TargetDescriptor": { + "TargetDescriptorV3": { "description": "Stable semantic handle for an interacted element within a page observation.", "type": "object", "properties": { diff --git a/crates/bsk-protocol/schema/trace_v2.json b/crates/bsk-protocol/schema/trace_v2.json index c301ee9f..7db328c2 100644 --- a/crates/bsk-protocol/schema/trace_v2.json +++ b/crates/bsk-protocol/schema/trace_v2.json @@ -16,7 +16,7 @@ "pages": { "type": "array", "items": { - "$ref": "#/definitions/PageRef" + "$ref": "#/definitions/PageRefV2" } }, "purpose": { @@ -54,7 +54,7 @@ "shift" ] }, - "PageRef": { + "PageRefV2": { "description": "Page context dictionary entry — referenced by steps via `page` id.", "type": "object", "required": [ @@ -431,7 +431,6 @@ } }, "TraceEntry": { - "description": "Recording entry point — first URL the flow starts from.", "type": "object", "required": [ "start_url" diff --git a/crates/bsk-protocol/src/bin/dump-schema.rs b/crates/bsk-protocol/src/bin/dump-schema.rs index 2071a057..6e565fe4 100644 --- a/crates/bsk-protocol/src/bin/dump-schema.rs +++ b/crates/bsk-protocol/src/bin/dump-schema.rs @@ -114,8 +114,8 @@ fn main() { dump!(RequestHelpResult, "tool_request_help_result"); dump!(TraceV2, "trace_v2"); - dump!(Trace, "trace"); - dump!(Step, "trace_step"); + dump!(TraceV3, "trace"); + dump!(StepV3, "trace_step"); dump!(RecordStartParams, "tool_record_start_params"); dump!(RecordStartResult, "tool_record_start_result"); dump!(RecordStopParams, "tool_record_stop_params"); diff --git a/crates/bsk-protocol/src/tools/mod.rs b/crates/bsk-protocol/src/tools/mod.rs index 67e8f53f..11ab651d 100644 --- a/crates/bsk-protocol/src/tools/mod.rs +++ b/crates/bsk-protocol/src/tools/mod.rs @@ -9,7 +9,8 @@ pub mod navigation; pub mod network; pub mod observation; pub mod record; -pub mod record_v2; +mod record_common; +mod record_v2; pub mod script; pub mod session; pub mod tabs; diff --git a/crates/bsk-protocol/src/tools/record.rs b/crates/bsk-protocol/src/tools/record.rs index 8111965b..d4e862b4 100644 --- a/crates/bsk-protocol/src/tools/record.rs +++ b/crates/bsk-protocol/src/tools/record.rs @@ -8,11 +8,12 @@ use serde::{Deserialize, Deserializer, Serialize}; use super::interaction::KeyModifier; -pub use crate::record_v2::{ - PageRef, SelectedOptionV2, StepCommonV2, StepEffectV2, StepV2, TargetDescriptorV2, TraceV2, +pub use super::record_common::TraceEntry; +pub use super::record_v2::{ + PageRefV2, SelectedOptionV2, StepCommonV2, StepEffectV2, StepV2, TargetDescriptorV2, TraceV2, }; -pub const TRACE_VERSION: u32 = 3; +pub const TRACE_VERSION_V3: u32 = 3; pub const TRACE_VERSION_V2: u32 = 2; pub const DEFAULT_TRACE_VERSION: u32 = 2; pub const VOM_FORMAT_VERSION: u32 = 1; @@ -22,9 +23,9 @@ where D: Deserializer<'de>, { let version = u32::deserialize(deserializer)?; - if version != TRACE_VERSION { + if version != TRACE_VERSION_V3 { return Err(serde::de::Error::custom(format!( - "unsupported trace version {version} (expected {TRACE_VERSION})" + "unsupported trace version {version} (expected {TRACE_VERSION_V3})" ))); } Ok(version) @@ -33,7 +34,7 @@ where fn trace_v3_version_schema(_: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { schemars::schema::SchemaObject { instance_type: Some(schemars::schema::InstanceType::Integer.into()), - const_value: Some(serde_json::json!(TRACE_VERSION)), + const_value: Some(serde_json::json!(TRACE_VERSION_V3)), ..Default::default() } .into() @@ -45,7 +46,7 @@ fn trace_v3_version_schema(_: &mut schemars::r#gen::SchemaGenerator) -> schemars /// Stable semantic handle for an interacted element within a page observation. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] -pub struct TargetDescriptor { +pub struct TargetDescriptorV3 { #[serde(default, skip_serializing_if = "Option::is_none", rename = "ref")] pub element_ref: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -59,15 +60,9 @@ pub struct TargetDescriptor { } // --------------------------------------------------------------------------- -// Trace envelope +// Trace v3 envelope // --------------------------------------------------------------------------- -/// Recording entry point — first URL the flow starts from. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] -pub struct TraceEntry { - pub start_url: String, -} - #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct RecorderInfo { pub bsk: String, @@ -83,42 +78,38 @@ pub enum StopReason { /// Page observation dictionary entry — referenced by steps via `state` / `result.state`. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] -pub struct TraceState { +pub struct TraceStateV3 { pub id: String, pub url: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub title: Option, - /// Wire-only: full page observation (front matter + VOM body + annotations). - #[serde(default, skip_serializing_if = "Option::is_none")] - pub body: Option, - /// Disk-only: filename under the bundle `pages/` directory. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub page: Option, + /// Full page observation (front matter + VOM body + annotations). + pub body: String, #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub truncated: bool, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] -pub struct StepResult { +pub struct StepResultV3 { pub state: String, } /// Fields shared by every step variant (flattened in JSON). #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] -pub struct StepCommon { +pub struct StepCommonV3 { pub id: u32, /// Observation id immediately before this action. pub state: String, - pub result: StepResult, + pub result: StepResultV3, } // --------------------------------------------------------------------------- -// Step op-specific payloads +// Trace v3 step payloads // --------------------------------------------------------------------------- /// One selected option (`select` op). #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] -pub struct SelectedOption { +pub struct SelectedOptionV3 { pub value: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub label: Option, @@ -147,27 +138,27 @@ pub enum FillCommit { /// One recorded user action — discriminated union by `op`. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] #[serde(tag = "op", rename_all = "snake_case")] -pub enum Step { +pub enum StepV3 { Navigate { #[serde(flatten)] - common: StepCommon, + common: StepCommonV3, to: String, cause: NavigationCause, }, Click { #[serde(flatten)] - common: StepCommon, - target: TargetDescriptor, + common: StepCommonV3, + target: TargetDescriptorV3, }, Hover { #[serde(flatten)] - common: StepCommon, - target: TargetDescriptor, + common: StepCommonV3, + target: TargetDescriptorV3, }, Fill { #[serde(flatten)] - common: StepCommon, - target: TargetDescriptor, + common: StepCommonV3, + target: TargetDescriptorV3, value: String, commit: FillCommit, #[serde(default, skip_serializing_if = "std::ops::Not::not")] @@ -175,34 +166,34 @@ pub enum Step { }, Select { #[serde(flatten)] - common: StepCommon, - target: TargetDescriptor, + common: StepCommonV3, + target: TargetDescriptorV3, #[serde(default, skip_serializing_if = "Vec::is_empty")] - selection: Vec, + selection: Vec, }, Press { #[serde(flatten)] - common: StepCommon, + common: StepCommonV3, key: String, #[serde(default, skip_serializing_if = "Option::is_none")] modifiers: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] - target: Option, + target: Option, }, Scroll { #[serde(flatten)] - common: StepCommon, + common: StepCommonV3, }, } // --------------------------------------------------------------------------- -// Trace root +// Trace v3 root // --------------------------------------------------------------------------- -/// Persisted user-action trace exported by `tool.record_stop` / `await`. +/// Wire trace returned by `tool.record_stop` / `await`. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] #[serde(deny_unknown_fields)] -pub struct Trace { +pub struct TraceV3 { #[serde(deserialize_with = "deserialize_trace_v3_version")] #[schemars(schema_with = "trace_v3_version_schema")] pub version: u32, @@ -214,8 +205,8 @@ pub struct Trace { pub stopped_by: StopReason, pub entry: TraceEntry, pub recorder: RecorderInfo, - pub states: Vec, - pub steps: Vec, + pub states: Vec, + pub steps: Vec, } // --------------------------------------------------------------------------- @@ -231,13 +222,6 @@ pub struct RecordStartParams { pub url: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub purpose: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub max_page_tokens: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub redact_values: Option, - /// Desired trace export format. Omitted ⇒ v2; `3` ⇒ state-linked v3 bundle. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub trace_version: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] @@ -255,13 +239,13 @@ pub struct RecordStopParams { #[derive(Debug, Clone, PartialEq)] pub enum RecordedTrace { V2(TraceV2), - V3(Trace), + V3(TraceV3), } impl RecordedTrace { pub fn classify_value(v: &serde_json::Value) -> Result { if let Some(ver) = v.get("version").and_then(|x| x.as_u64()) { - if ver != u64::from(TRACE_VERSION) { + if ver != u64::from(TRACE_VERSION_V3) { return Err(format!("unsupported trace version {ver}")); } if v.get("pages").is_some() { @@ -281,24 +265,6 @@ impl RecordedTrace { } Err("ambiguous or unparseable trace".into()) } - - pub fn is_v3(&self) -> bool { - matches!(self, RecordedTrace::V3(_)) - } - - pub fn as_v3(&self) -> Option<&Trace> { - match self { - RecordedTrace::V3(t) => Some(t), - RecordedTrace::V2(_) => None, - } - } - - pub fn as_v2(&self) -> Option<&TraceV2> { - match self { - RecordedTrace::V2(t) => Some(t), - RecordedTrace::V3(_) => None, - } - } } impl Serialize for RecordedTrace { @@ -333,7 +299,7 @@ impl JsonSchema for RecordedTrace { subschemas: Some(Box::new(schemars::schema::SubschemaValidation { one_of: Some(vec![ generator.subschema_for::(), - generator.subschema_for::(), + generator.subschema_for::(), ]), ..Default::default() })), @@ -369,18 +335,18 @@ mod tests { use super::*; use serde_json::json; - fn sample_common(id: u32, state: &str, result_state: &str) -> StepCommon { - StepCommon { + fn sample_common(id: u32, state: &str, result_state: &str) -> StepCommonV3 { + StepCommonV3 { id, state: state.into(), - result: StepResult { + result: StepResultV3 { state: result_state.into(), }, } } - fn sample_target() -> TargetDescriptor { - TargetDescriptor { + fn sample_target() -> TargetDescriptorV3 { + TargetDescriptorV3 { element_ref: Some("e21".into()), role: Some("button".into()), name: Some("发布".into()), @@ -389,9 +355,9 @@ mod tests { } } - fn sample_trace() -> Trace { - Trace { - version: TRACE_VERSION, + fn sample_trace() -> TraceV3 { + TraceV3 { + version: TRACE_VERSION_V3, purpose: Some("把草稿商品发布上架".into()), started_at: Some("2026-08-10T02:10:41.080Z".into()), recorded_at: "2026-08-10T02:12:55.360Z".into(), @@ -404,32 +370,30 @@ mod tests { vom: VOM_FORMAT_VERSION, }, states: vec![ - TraceState { + TraceStateV3 { id: "s1".into(), url: "https://example.com/".into(), title: Some("Example Domain".into()), - body: None, - page: Some("s1.vom.txt".into()), + body: "@vom 1\nRootWebArea \"Example Domain\"".into(), truncated: false, }, - TraceState { + TraceStateV3 { id: "s2".into(), url: "https://shop.example.com/products?status=draft".into(), title: Some("商品管理".into()), - body: None, - page: Some("s2.vom.txt".into()), + body: "@vom 1\nRootWebArea \"商品管理\"".into(), truncated: false, }, ], steps: vec![ - Step::Navigate { + StepV3::Navigate { common: sample_common(1, "s1", "s2"), to: "https://shop.example.com/products?status=draft".into(), cause: NavigationCause::UserTyped, }, - Step::Fill { + StepV3::Fill { common: sample_common(2, "s2", "s2"), - target: TargetDescriptor { + target: TargetDescriptorV3 { element_ref: Some("e12".into()), role: Some("textbox".into()), name: Some("搜索商品".into()), @@ -440,7 +404,7 @@ mod tests { commit: FillCommit::Enter, redacted: false, }, - Step::Click { + StepV3::Click { common: sample_common(3, "s2", "s2"), target: sample_target(), }, @@ -450,7 +414,7 @@ mod tests { #[test] fn step_click_round_trips() { - let step = Step::Click { + let step = StepV3::Click { common: sample_common(1, "s1", "s2"), target: sample_target(), }; @@ -459,15 +423,15 @@ mod tests { assert_eq!(v.get("state").and_then(|v| v.as_str()), Some("s1")); assert_eq!(v["result"]["state"], "s2"); assert_eq!(v["target"]["ref"], "e21"); - let round: Step = serde_json::from_value(v).unwrap(); + let round: StepV3 = serde_json::from_value(v).unwrap(); assert_eq!(round, step); } #[test] fn step_fill_with_commit_round_trips() { - let step = Step::Fill { + let step = StepV3::Fill { common: sample_common(2, "s2", "s3"), - target: TargetDescriptor { + target: TargetDescriptorV3 { element_ref: Some("e12".into()), role: Some("textbox".into()), name: Some("搜索商品".into()), @@ -482,15 +446,15 @@ mod tests { assert_eq!(v["op"], "fill"); assert_eq!(v["commit"], "enter"); assert!(v.get("redacted").is_none()); - let round: Step = serde_json::from_value(v).unwrap(); + let round: StepV3 = serde_json::from_value(v).unwrap(); assert_eq!(round, step); } #[test] fn step_fill_password_is_redacted() { - let step = Step::Fill { + let step = StepV3::Fill { common: sample_common(1, "s1", "s1"), - target: TargetDescriptor { + target: TargetDescriptorV3 { element_ref: Some("e3".into()), role: Some("textbox".into()), name: Some("密码".into()), @@ -508,7 +472,7 @@ mod tests { #[test] fn step_navigate_with_cause_round_trips() { - let step = Step::Navigate { + let step = StepV3::Navigate { common: sample_common(1, "s1", "s2"), to: "https://example.com".into(), cause: NavigationCause::UserTyped, @@ -516,7 +480,7 @@ mod tests { let v = serde_json::to_value(&step).unwrap(); assert_eq!(v["op"], "navigate"); assert_eq!(v["cause"], "user_typed"); - let round: Step = serde_json::from_value(v).unwrap(); + let round: StepV3 = serde_json::from_value(v).unwrap(); assert_eq!(round, step); } @@ -528,15 +492,15 @@ mod tests { assert!(v.get("pages").is_none()); assert_eq!(v["recorder"]["vom"], 1); assert_eq!(v["states"].as_array().unwrap().len(), 2); - let round: Trace = serde_json::from_value(v).unwrap(); + let round: TraceV3 = serde_json::from_value(v).unwrap(); assert_eq!(round, trace); } #[test] fn default_fields_are_omitted() { - let step = Step::Click { + let step = StepV3::Click { common: sample_common(1, "s1", "s1"), - target: TargetDescriptor { + target: TargetDescriptorV3 { element_ref: Some("e1".into()), role: Some("button".into()), name: Some("OK".into()), @@ -552,9 +516,9 @@ mod tests { #[test] fn unmatched_target_serializes_flag() { - let step = Step::Click { + let step = StepV3::Click { common: sample_common(1, "s1", "s2"), - target: TargetDescriptor { + target: TargetDescriptorV3 { element_ref: None, role: Some("button".into()), name: Some("发布".into()), @@ -628,7 +592,7 @@ mod tests { "steps": [] }); assert!(RecordedTrace::classify_value(&unsupported).is_err()); - assert!(serde_json::from_value::(unsupported).is_err()); + assert!(serde_json::from_value::(unsupported).is_err()); } #[test] @@ -639,7 +603,7 @@ mod tests { .expect("RecordedTrace schema should use oneOf"); assert_eq!(variants.len(), 2); - let trace_schema = schema["definitions"]["Trace"].clone(); + let trace_schema = schema["definitions"]["TraceV3"].clone(); assert_eq!(trace_schema["properties"]["version"]["const"], 3); } @@ -654,8 +618,8 @@ mod tests { "entry": { "start_url": "https://example.com/editor" }, "recorder": { "bsk": "0.1.10", "vom": 1 }, "states": [ - { "id": "s1", "url": "https://example.com/editor", "page": "s1.vom.txt" }, - { "id": "s2", "url": "https://example.com/p/99", "page": "s2.vom.txt" } + { "id": "s1", "url": "https://example.com/editor", "body": "@vom 1" }, + { "id": "s2", "url": "https://example.com/p/99", "body": "@vom 1" } ], "steps": [ { @@ -676,7 +640,7 @@ mod tests { } ] }); - let trace: Trace = serde_json::from_value(v).unwrap(); + let trace: TraceV3 = serde_json::from_value(v).unwrap(); assert_eq!(trace.version, 3); assert_eq!(trace.states.len(), 2); assert_eq!(trace.steps.len(), 2); diff --git a/crates/bsk-protocol/src/tools/record_common.rs b/crates/bsk-protocol/src/tools/record_common.rs new file mode 100644 index 00000000..02617366 --- /dev/null +++ b/crates/bsk-protocol/src/tools/record_common.rs @@ -0,0 +1,7 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct TraceEntry { + pub start_url: String, +} diff --git a/crates/bsk-protocol/src/tools/record_v2.rs b/crates/bsk-protocol/src/tools/record_v2.rs index 27f7f3f0..43b36e7e 100644 --- a/crates/bsk-protocol/src/tools/record_v2.rs +++ b/crates/bsk-protocol/src/tools/record_v2.rs @@ -1,13 +1,12 @@ //! Trace v2 — record-only user-action log with `pages[]` and step `page` refs. //! -//! Legacy wire format: no top-level `version` field. New peers omit -//! `trace_version` on `record_start` to request this shape. +//! Legacy wire format with no top-level `version` field. use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use super::interaction::KeyModifier; -use super::record::TraceEntry; +use super::record_common::TraceEntry; /// Stable semantic handle for an interacted element (v2). /// @@ -29,7 +28,7 @@ pub struct TargetDescriptorV2 { /// Page context dictionary entry — referenced by steps via `page` id. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] -pub struct PageRef { +pub struct PageRefV2 { pub id: String, pub url: String, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -116,7 +115,7 @@ pub struct TraceV2 { #[serde(default, skip_serializing_if = "Option::is_none")] pub purpose: Option, pub entry: TraceEntry, - pub pages: Vec, + pub pages: Vec, pub steps: Vec, } @@ -153,7 +152,7 @@ mod tests { entry: TraceEntry { start_url: "https://example.com/".into(), }, - pages: vec![PageRef { + pages: vec![PageRefV2 { id: "p1".into(), url: "https://example.com/".into(), title: None, From 4167df5c456b356d843968a9dcc7319ae0a83978 Mon Sep 17 00:00:00 2001 From: Ljy-0827 Date: Fri, 21 Aug 2026 11:37:20 +0800 Subject: [PATCH 5/5] fix(protocol): compatible with dto --- apps/extension/src/transport/types.ts | 1 - crates/bsk-protocol/src/tools/record.rs | 1 - 2 files changed, 2 deletions(-) diff --git a/apps/extension/src/transport/types.ts b/apps/extension/src/transport/types.ts index 5659392b..b10425c7 100644 --- a/apps/extension/src/transport/types.ts +++ b/apps/extension/src/transport/types.ts @@ -662,7 +662,6 @@ export interface EmulateResult { export const TRACE_VERSION_V3 = 3; export const TRACE_VERSION_V2 = 2; -export const DEFAULT_TRACE_VERSION = 2; export const VOM_FORMAT_VERSION = 1; export interface TargetDescriptorV3 { diff --git a/crates/bsk-protocol/src/tools/record.rs b/crates/bsk-protocol/src/tools/record.rs index d4e862b4..fe7c7329 100644 --- a/crates/bsk-protocol/src/tools/record.rs +++ b/crates/bsk-protocol/src/tools/record.rs @@ -15,7 +15,6 @@ pub use super::record_v2::{ pub const TRACE_VERSION_V3: u32 = 3; pub const TRACE_VERSION_V2: u32 = 2; -pub const DEFAULT_TRACE_VERSION: u32 = 2; pub const VOM_FORMAT_VERSION: u32 = 1; fn deserialize_trace_v3_version<'de, D>(deserializer: D) -> Result