Files
reverse-engineering-anything/src/domain/browserObservationSchemas.ts
T
Paritosh LahreandClaude Opus 5.5 8db538a7ca fix(browser): keep negative script columns Chrome reports (#8684)
* fix(browser): keep negative script columns Chrome reports

Chrome reported a Debugger.scriptParsed event with startColumn -60 and
endColumn -15 for an inline script in WordPress's helper frame. CDP declares
these columns as plain integers, but REA's admission required min(0), so one
script failed the whole observe-web-execution run with unreadable_output.

CDP resource columns now share one representation, cdpResourceColumnSchema,
for script start/end columns, initiator call frames, parser initiators and
event listeners. Positions on a script's first line include its start column,
so keeping the reported values keeps the documented script-relative
conversion consistent. The domain contract and web runtime docs state that a
negative column does not locate a column in the resource.

Fixes #8674

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(browser): apply the CDP column policy to passive capture

inspect_web_page and discover_webmcp_tools still read CDP columns through
cdpSourceCoordinates, whose own >= 0 check turned a negative column into
null and reported invalid_protocol_value. WebMCP discovery also dropped the
tool's whole registration_source.

cdpResourceColumnSchema now lives with the domain's CDP observation schemas.
Runtime admission, the passive coordinate helper and the runtime, console,
initiator and WebMCP output schemas all use it. Lines keep their
non-negative rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 14:50:00 +08:00

346 lines
10 KiB
TypeScript

import { z } from "zod";
import { utcDateTimeSchema } from "./offsetDateTime.js";
import { browserCompletenessSchema } from "./browserCompleteness.js";
import {
cdpDomSnapshotSchema,
cdpResourceColumnSchema,
cdpScriptSourceSchema,
cdpScriptMetadataSchema,
} from "./cdpObservationSchemas.js";
import { jsonShapeSchema } from "./jsonShape.js";
import { prefixedDigestSchema } from "./digests.js";
/** Shared CDP/browser version identity carried on observation Evidence. */
export const browserVersionSchema = z.object({
product: z.string(),
protocol_version: z.string(),
revision: z.string(),
user_agent: z.string(),
js_version: z.string(),
});
const browserTargetSchema = z.object({
target_id: z.string(),
type: z.string(),
title: z.string(),
url: z.string(),
origin: z.string(),
attached: z
.boolean()
.nullable()
.describe(
"Discovery-reported debugger attachment state; null means unknown",
),
});
/** Complete policy-filtered browser target discovery result. */
export const browserTargetListSchema = z.object({
browser: browserVersionSchema,
targets: z.array(browserTargetSchema),
excluded: z.object({
disallowed_origin: z.int().min(0),
unsupported_url: z.int().min(0),
non_page: z.int().min(0),
}),
limitations: z.array(z.string()),
});
export type BrowserTargetList = z.infer<typeof browserTargetListSchema>;
const browserFrameSchema = z.object({
frame_id: z.string(),
parent_frame_id: z.string().nullable(),
url: z.string(),
origin: z.string().nullable(),
});
const browserAxNodeSchema = z.object({
node_id: z.string(),
parent_id: z.string().nullable(),
role: z.string().nullable(),
name: z.string().nullable(),
description: z.string().nullable(),
ignored: z.boolean(),
states: z
.array(
z.object({
name: z.string(),
value: z.union([z.boolean(), z.number(), z.string()]),
}),
)
.default([]),
});
const browserResourceReconciliationSchema = z.discriminatedUnion("status", [
z.object({ status: z.literal("exact"), resource_key: z.string() }),
z.object({
status: z.literal("ambiguous"),
candidate_resource_keys: z.array(z.string()),
}),
z.object({
status: z.literal("unmatched"),
reason: z.literal("no_exact_sanitized_url"),
}),
]);
const browserScriptSchema = z.object({
script_key: z.string().regex(/^scr_[a-f0-9]{64}(?:_[0-9]+)?$/u),
frame_id: z.string().nullable().default(null),
url: z.string(),
origin: z.string().nullable(),
...cdpScriptMetadataSchema.shape,
source_map_url: z.string().nullable(),
resource_reconciliation: browserResourceReconciliationSchema,
source: cdpScriptSourceSchema,
});
const browserResourceSchema = z.object({
resource_key: prefixedDigestSchema("res"),
url: z.string(),
origin: z.string().nullable(),
type: z.string(),
mime_type: z.string(),
content_size: z.number().min(0).nullable(),
});
const browserNetworkRequestSchema = z.object({
request_id: z.string(),
url: z.string(),
origin: z.string().nullable(),
method: z.string(),
resource_type: z.string().nullable(),
status: z.number().nullable(),
mime_type: z.string().nullable(),
encoded_data_length: z.number().min(0).nullable(),
redirects: z
.array(
z.object({
url: z.string(),
response_url: z.string(),
method: z.string(),
resource_type: z.string().nullable(),
status: z.number().nullable(),
mime_type: z.string().nullable(),
encoded_data_length: z.number().min(0).nullable(),
request_timestamp: z.number().nullable(),
redirect_event_timestamp: z.number().nullable(),
}),
)
.optional(),
initiator: z.object({
type: z.string(),
url: z.string().nullable(),
line: z.int().min(0).nullable(),
column: cdpResourceColumnSchema.nullable(),
}),
body_shapes: z.object({
status: z.enum(["not_approved", "included", "partial", "unavailable"]),
request: jsonShapeSchema.nullable(),
response: jsonShapeSchema.nullable(),
}),
});
const browserConsoleEventSchema = z.object({
type: z.string(),
timestamp: z
.number()
.nullable()
.describe(
"CDP Runtime.Timestamp in milliseconds since Unix epoch; null when the producer value is absent or invalid.",
),
argument_types: z
.array(z.string())
.nullable()
.describe(
"CDP argument types by argument position; null when the producer argument inventory is absent or not an array.",
),
url: z.string().nullable(),
line: z.int().min(0).nullable(),
column: cdpResourceColumnSchema.nullable(),
text_capture: z.object({
status: z.enum(["not_approved", "included", "unavailable"]),
values: z.array(
z.object({
argument_index: z.int().min(0),
type: z.string(),
text: z.string(),
}),
),
retained_bytes: z.int().min(0),
}),
});
const browserWebSocketFrameSchema = z.object({
direction: z.enum(["sent", "received"]),
opcode: z.int().min(0).max(15).nullable(),
payload_bytes: z.int().min(0).nullable(),
payload_shape: z
.object({
format: z.enum(["json", "text", "binary"]),
json_shape: jsonShapeSchema.nullable(),
})
.nullable(),
});
const browserWebSocketConnectionSchema = z.object({
request_id: z.string(),
url: z.string(),
events: z.array(browserWebSocketFrameSchema),
});
const browserWorkerSchema = z.object({
target_id: z.string(),
type: z.string(),
url: z.string(),
origin: z.string().nullable(),
attached: z.boolean(),
opener_target_id: z.string().nullable().default(null),
parent_frame_id: z.string().nullable().default(null),
});
const browserCspSourceSchema = z.object({
kind: z.enum([
"keyword",
"scheme",
"approved_origin",
"external_origin",
"other",
]),
value: z.string().nullable(),
});
const browserLinkMetadataSchema = z.object({
href: z.string().nullable(),
destination_scope: z.enum(["approved", "outside_policy", "unsupported"]),
context_url: z.string().nullable().optional(),
context_scope: z
.enum(["approved", "outside_policy", "unsupported"])
.optional(),
rel: z.array(z.string()),
as: z.string().nullable(),
type: z.string().nullable(),
crossorigin: z.string().nullable(),
});
const browserAgentHintSchema = z.object({
mechanism: z.enum([
"link_rel",
"dom_link_rel",
"well_known_resource",
"response_header",
]),
declaration: z.string(),
url: z.string().nullable(),
trust: z.literal("page-declared-untrusted"),
});
const browserSafeMetadataSchema = z.object({
responses: z.array(
z.object({
request_id: z.string(),
url: z.string(),
mime_type: z.string().nullable(),
content_length: z.int().min(0).nullable(),
content_encoding: z.string().nullable(),
csp: z.object({
directives: z.array(
z.object({
name: z.string(),
sources: z.array(browserCspSourceSchema),
}),
),
nonce_count: z.int().min(0),
hash_count: z.int().min(0),
}),
links: z.array(browserLinkMetadataSchema),
policies: z.object({
coop: z.string().nullable(),
coep: z.string().nullable(),
corp: z.string().nullable(),
referrer_policy: z.string().nullable(),
x_content_type_options: z.string().nullable(),
permissions_policy_features: z.array(z.string()),
}),
}),
),
dom_urls: z.array(
z.object({
node_index: z.int().min(0),
attribute: z.enum(["href", "src", "action", "formaction", "poster"]),
url: z.string().nullable(),
destination_scope: z.enum(["approved", "outside_policy", "unsupported"]),
}),
),
agent_hints: z.array(browserAgentHintSchema),
excluded_dom_urls: z.int().min(0),
headers_allowlisted: z.literal(true),
});
/** Provider-neutral normalized result for one passive web-page observation. */
export const webPageInspectionSchema = z.object({
browser: browserVersionSchema,
target: browserTargetSchema,
capture_window: z.object({
started_at: utcDateTimeSchema,
ended_at: utcDateTimeSchema,
observation_ms: z.int().min(0),
}),
completeness: browserCompletenessSchema,
frames: z.array(browserFrameSchema),
dom: cdpDomSnapshotSchema,
accessibility: z.object({
total_nodes: z.int().min(0),
text_capture: z.object({
status: z.enum(["not_approved", "included", "unavailable"]),
retained_bytes: z.int().min(0),
excluded_fields: z.int().min(0),
}),
nodes: z.array(browserAxNodeSchema),
}),
scripts: z.object({
total: z.int().min(0),
items: z.array(browserScriptSchema),
}),
resources: z.array(browserResourceSchema),
network: z.object({
requests: z.array(browserNetworkRequestSchema),
websocket_connections: z.array(browserWebSocketConnectionSchema),
coverage_started_at: utcDateTimeSchema,
prior_activity_available: z.literal(false),
}),
console: z.object({
events: z.array(browserConsoleEventSchema),
coverage_started_at: utcDateTimeSchema,
prior_activity_available: z.literal(false),
}),
workers: z.array(browserWorkerSchema),
metadata: browserSafeMetadataSchema,
storage: z.object({
origin: z.string(),
usage_bytes: z.number().min(0).nullable(),
quota_bytes: z.number().min(0).nullable(),
local_storage_keys: z.array(z.string()),
session_storage_keys: z.array(z.string()),
indexed_db_names: z.array(z.string()),
cache_names: z.array(z.string()),
content_fingerprints: z
.array(
z.object({
scope: z.enum([
"cookie",
"local_storage",
"session_storage",
"indexed_db_schema",
"indexed_db_record",
"cache_entry",
]),
identity_sha256: z.string().regex(/^[a-f0-9]{64}$/u),
value_sha256: z
.string()
.regex(/^[a-f0-9]{64}$/u)
.nullable(),
projection_version: z
.literal(2)
.optional()
.describe(
"IndexedDB schema projection including database version; absent on historical store-only fingerprints.",
),
complete: z.boolean(),
}),
)
.default([]),
fingerprint_algorithm: z.literal("sha256").default("sha256"),
fingerprints_complete: z.boolean().default(false),
values_redacted: z.literal(true),
}),
limitations: z.array(z.string()),
});
export type WebPageInspection = z.infer<typeof webPageInspectionSchema>;