Files
rea/src/domain/browserSession.ts
T
all-black-493 62c8b3da1b feat(browser): attach passive tools to browsers without HTTP discovery (#2346)
* feat(browser): attach passive tools to browsers without HTTP discovery

Accept a literal loopback browser WebSocket,
ws://127.0.0.1:PORT/devtools/browser/ID, as the cdp_endpoint of the six
passive browser tools. Discovery then reads Browser.getVersion and
Target.getTargets over that socket, and the target session adopts the
same socket, so a browser that asks for consent per connection prompts
once per call.

The socket identifier gates access, so Evidence records the endpoint as
ws://127.0.0.1:PORT/devtools/browser/[REDACTED], and runtime
reconciliation accepts that recorded form. HTTP endpoints and their
Evidence are unchanged. Electron, web runtime, scenario and Inspector
endpoints stay HTTP-only.

Refs #2288

* test(browser): match browser-socket captures against HTTP discovery

Check in the real Chrome lane that DevToolsActivePort names the same
browser socket as /json/version, and that inspecting the selected page
through that socket leaves DOM structure and scripts unchanged compared
with an inspection through HTTP discovery.

Refs #2288

* fix(browser): bound browser-socket handshake and report discovery timeouts

Run the browser-socket WebSocket handshake and both discovery commands
under one discovery deadline, so a port that accepts the connection but
never completes the upgrade times out instead of waiting forever.

Report a reached deadline as a timeout without carrying the internal
abort as its cause, so CLI and MCP project provider_timeout rather than
cancelled. Caller cancellation is still reported as cancelled.

Refs #2288
2026-10-14 00:36:37 +08:00

67 lines
2.0 KiB
TypeScript

import { z } from "zod";
import {
browserAllowedOriginsSchema,
browserObservationEndpointSchema,
} from "./browserObservation.js";
import { browserCompletenessSchema } from "./browserCompleteness.js";
import { browserVersionSchema } from "./browserObservationSchemas.js";
/** Input for a navigation-aware browser observation window. */
export const observeWebSessionInputSchema = z.strictObject({
cdp_endpoint: browserObservationEndpointSchema,
allowed_origins: browserAllowedOriginsSchema,
target_id: z.string().trim().min(1),
observation_ms: z.number().int().min(1).default(10_000),
});
export type ObserveWebSessionInput = z.infer<
typeof observeWebSessionInputSchema
>;
const timelineEventSchema = z.object({
sequence: z.number().int().min(1),
type: z.enum([
"navigation_requested",
"navigation_committed",
"same_origin_reload",
"same_document_navigation",
"redirect",
"load_failed",
"lifecycle",
"target_terminated",
]),
timestamp: z.number().min(0),
frame_id: z.string().nullable(),
loader_id: z.string().nullable(),
request_id: z.string().nullable(),
url: z.string().nullable(),
destination_scope: z
.enum(["approved", "outside_policy", "unsupported"])
.nullable(),
detail: z.string().nullable(),
});
/** Navigation-aware session result for external user actions. */
export const webObservationSessionSchema = z.object({
browser: browserVersionSchema,
target: z.object({
target_id: z.string(),
initial_url: z.string(),
final_url: z.string().nullable(),
}),
window: z.object({
armed_at: z.iso.datetime(),
ended_at: z.iso.datetime(),
requested_ms: z.number().int().min(1),
end_reason: z.enum([
"window_elapsed",
"target_left_scope",
"target_terminated",
]),
}),
timeline: z.array(timelineEventSchema),
completeness: browserCompletenessSchema,
limitations: z.array(z.string()),
});
export type WebObservationSession = z.infer<typeof webObservationSessionSchema>;