Files
rea/src/browser/CdpScriptContextFacts.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 <[email protected]>

* 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 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-11-03 14:50:00 +08:00

156 lines
5.2 KiB
TypeScript

import { z } from "zod";
import {
cdpStringValue,
numberValue,
recordValue,
} from "./CdpCaptureValues.js";
import { cdpResourceColumnSchema } from "../domain/cdpObservationSchemas.js";
const integer = z.int().min(0);
const contextAuxDataSchema = z
.object({
frameId: z.string().optional(),
isDefault: z.boolean().optional(),
type: z.string().optional(),
})
.passthrough();
const scriptDeclaration = z.object({
scriptId: z.string(),
url: z.string(),
executionContextId: z.number(),
});
const contextDeclaration = z.object({
id: z.number(),
origin: z.string().optional(),
name: z.string().optional(),
uniqueId: z.string().optional(),
auxData: contextAuxDataSchema.optional(),
});
/** Active runtime admission retains its required positions and exact auxiliary declarations. */
export const runtimeScriptParsedSchema = scriptDeclaration.extend({
scriptId: z.string().min(1),
executionContextId: integer,
startLine: integer,
startColumn: cdpResourceColumnSchema,
endLine: integer,
endColumn: cdpResourceColumnSchema,
hash: z.string().optional(),
sourceMapURL: z.string().optional(),
hasSourceURL: z.boolean().optional(),
scriptLanguage: z.string().optional(),
executionContextAuxData: contextAuxDataSchema.optional(),
});
export const runtimeContextSchema = z.object({
context: contextDeclaration.extend({ id: integer }).passthrough(),
});
/** Private storage seeding keeps its original numeric and text admission policy. */
export const storageScriptParsedSchema = scriptDeclaration;
export const storageContextSchema = z.object({
context: contextDeclaration
.pick({ id: true, origin: true, name: true, auxData: true })
.required({ origin: true, name: true })
.extend({
auxData: contextAuxDataSchema.pick({ frameId: true }).strip().optional(),
}),
});
export const storageContextDestroyedSchema = z.object({
executionContextId: z.number(),
});
/** Narrow frame claims may be retained without borrowing unbounded auxiliary JSON. */
export interface CdpContextFrameClaim {
readonly state: "missing" | "invalid" | "observed";
/** Supplied string spelling, including an invalid empty claim, remains available. */
readonly frameId: string | undefined;
}
export const cdpContextFrameClaim = (
auxiliary: unknown,
): CdpContextFrameClaim => {
if (auxiliary === undefined) return { state: "missing", frameId: undefined };
const value = recordValue(auxiliary);
if (value === undefined) return { state: "invalid", frameId: undefined };
if (value.frameId === undefined)
return { state: "missing", frameId: undefined };
const frameId = cdpStringValue(value.frameId);
return {
state: frameId === undefined || frameId === "" ? "invalid" : "observed",
frameId,
};
};
/** Numeric producer identity and the safe integer lookup spelling have distinct policies. */
const contextIdentity = (reported: unknown) => {
const id = numberValue(reported);
return {
id,
contextKey:
id !== undefined && Number.isSafeInteger(id) ? String(id) : null,
};
};
export const readCdpExecutionContextCreated = (params: unknown) => {
const context = recordValue(recordValue(params)?.context);
return {
...contextIdentity(context?.id),
name: cdpStringValue(context?.name),
origin: cdpStringValue(context?.origin),
frameClaim: cdpContextFrameClaim(context?.auxData),
};
};
export const readCdpExecutionContextDestroyed = (params: unknown) =>
contextIdentity(recordValue(params)?.executionContextId);
/** Partial passive observations do not impose the active runtime's required fields. */
export const readCdpScriptParsed = (params: unknown) => {
const value = recordValue(params);
const identity = contextIdentity(value?.executionContextId);
const length = numberValue(value?.length);
return {
scriptId: cdpStringValue(value?.scriptId),
url: cdpStringValue(value?.url),
contextId: identity.id,
contextKey: identity.contextKey,
metadata: {
hash: cdpStringValue(value?.hash) ?? null,
length:
length !== undefined && Number.isSafeInteger(length) && length >= 0
? length
: null,
isModule: typeof value?.isModule === "boolean" ? value.isModule : null,
language: cdpStringValue(value?.scriptLanguage) ?? null,
},
frameClaim: cdpContextFrameClaim(value?.executionContextAuxData),
hasSourceUrl:
typeof value?.hasSourceURL === "boolean" ? value.hasSourceURL : null,
invalidHasSourceUrl:
value?.hasSourceURL !== undefined &&
typeof value.hasSourceURL !== "boolean",
sourceMapUrl: value?.sourceMapURL,
};
};
/** Missing claims allow other producer evidence; malformed and conflicting claims never do. */
export const selectCdpScriptFrame = (
script: CdpContextFrameClaim,
context: CdpContextFrameClaim | undefined,
): {
readonly frameId: string | null;
readonly invalid: boolean;
readonly conflicting: boolean;
} => {
const invalid = script.state === "invalid" || context?.state === "invalid";
const conflicting =
script.state === "observed" &&
context?.state === "observed" &&
script.frameId !== context.frameId;
return {
frameId:
invalid || conflicting
? null
: (script.frameId ?? context?.frameId ?? null),
invalid,
conflicting,
};
};