mirror of
https://github.com/morluto/rea.git
synced 2026-11-11 17:28:08 +00:00
* 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>
201 lines
6.5 KiB
TypeScript
201 lines
6.5 KiB
TypeScript
import {
|
|
observeBrowserUrl,
|
|
type BrowserUrlObservation,
|
|
type SanitizedBrowserUrl,
|
|
} from "../domain/browserObservation.js";
|
|
import { z } from "zod";
|
|
import { err, ok, type Result } from "../domain/result.js";
|
|
import {
|
|
cdpStringValue,
|
|
numberValue,
|
|
recordValue,
|
|
type UnknownRecord,
|
|
} from "./CdpCaptureValues.js";
|
|
import { cdpResourceColumnSchema } from "../domain/cdpObservationSchemas.js";
|
|
|
|
const integer = z.int().min(0);
|
|
/** Active runtime admission remains stricter than passive request observations. */
|
|
export const runtimeRequestSchema = z.object({
|
|
requestId: z.string().min(1),
|
|
frameId: z.string().optional(),
|
|
timestamp: z.number().min(0),
|
|
request: z.object({ url: z.string(), method: z.string() }),
|
|
initiator: z
|
|
.object({
|
|
type: z.string(),
|
|
stack: z.unknown().optional(),
|
|
url: z.string().optional(),
|
|
lineNumber: integer.optional(),
|
|
columnNumber: cdpResourceColumnSchema.optional(),
|
|
})
|
|
.passthrough(),
|
|
});
|
|
|
|
/** A CDP Response occurs in both responseReceived and redirect continuation events. */
|
|
export interface CdpResponseFacts {
|
|
readonly source: UnknownRecord;
|
|
readonly url: BrowserUrlObservation | undefined;
|
|
readonly status: Result<
|
|
number | null,
|
|
{ readonly _tag: "InvalidCdpResponseStatus" }
|
|
>;
|
|
readonly mimeType: string | undefined;
|
|
readonly encodedDataLength: number | undefined;
|
|
readonly headers: unknown;
|
|
}
|
|
export const readCdpResponseFacts = (
|
|
reported: unknown,
|
|
): CdpResponseFacts | undefined => {
|
|
const source = recordValue(reported);
|
|
if (source === undefined) return undefined;
|
|
const url = cdpStringValue(source.url);
|
|
return {
|
|
source,
|
|
url: url === undefined ? undefined : observeBrowserUrl(url),
|
|
status: readResponseStatus(source.status),
|
|
mimeType: cdpStringValue(source.mimeType),
|
|
encodedDataLength: numberValue(source.encodedDataLength),
|
|
headers: source.headers,
|
|
};
|
|
};
|
|
|
|
/** Omission is unknown; a supplied non-number is a protocol failure. */
|
|
const readResponseStatus = (value: unknown): CdpResponseFacts["status"] => {
|
|
if (value === undefined) return ok(null);
|
|
const status = numberValue(value);
|
|
return status === undefined
|
|
? err({ _tag: "InvalidCdpResponseStatus" })
|
|
: ok(status);
|
|
};
|
|
|
|
type ScopedCdpResponse =
|
|
| {
|
|
readonly scope: "approved";
|
|
readonly url: SanitizedBrowserUrl;
|
|
readonly response: CdpResponseFacts;
|
|
}
|
|
| { readonly scope: "outside_policy"; readonly reason: "disallowed_origin" }
|
|
| { readonly scope: "unsupported"; readonly reason: "unsupported_url" }
|
|
| {
|
|
readonly scope: null;
|
|
readonly reason: "invalid_protocol_value" | "unattributed_origin";
|
|
};
|
|
|
|
/** Apply caller origin policy to producer-owned URL syntax, without reparsing it. */
|
|
export const scopeCdpResponse = (
|
|
response: CdpResponseFacts | undefined,
|
|
allowedOrigins: ReadonlySet<string>,
|
|
missingUrlReason:
|
|
| "invalid_protocol_value"
|
|
| "unattributed_origin" = "invalid_protocol_value",
|
|
): ScopedCdpResponse => {
|
|
if (response === undefined)
|
|
return { scope: null, reason: "invalid_protocol_value" };
|
|
const url = response.url;
|
|
if (url === undefined || url.source === "")
|
|
return { scope: null, reason: missingUrlReason };
|
|
if (url.kind === "malformed")
|
|
return { scope: null, reason: "invalid_protocol_value" };
|
|
if (url.protocol !== "http:" && url.protocol !== "https:")
|
|
return { scope: "unsupported", reason: "unsupported_url" };
|
|
if (
|
|
url.sanitized.origin === null ||
|
|
!allowedOrigins.has(url.sanitized.origin)
|
|
)
|
|
return { scope: "outside_policy", reason: "disallowed_origin" };
|
|
return { scope: "approved", url: url.sanitized, response };
|
|
};
|
|
|
|
const requestIdentity = (source: UnknownRecord | undefined): string | null => {
|
|
const id = cdpStringValue(source?.requestId);
|
|
return id === undefined || id === "" ? null : id;
|
|
};
|
|
export interface CdpRequestWillBeSentFacts {
|
|
readonly source: UnknownRecord | undefined;
|
|
readonly requestId: string | null;
|
|
readonly frameId: string | undefined;
|
|
readonly loaderId: string | undefined;
|
|
readonly resourceType: string | undefined;
|
|
readonly timestamp: number | undefined;
|
|
readonly request:
|
|
| {
|
|
readonly source: UnknownRecord;
|
|
readonly url: string | undefined;
|
|
readonly method: string | undefined;
|
|
}
|
|
| undefined;
|
|
readonly initiator:
|
|
| {
|
|
readonly source: UnknownRecord;
|
|
readonly type: string | undefined;
|
|
}
|
|
| undefined;
|
|
readonly redirect:
|
|
| { readonly kind: "missing" }
|
|
| { readonly kind: "malformed" }
|
|
| { readonly kind: "response"; readonly response: CdpResponseFacts };
|
|
}
|
|
|
|
/** Borrow raw declarations while parsing each shared field once, before consumer policy. */
|
|
export const readCdpRequestWillBeSent = (
|
|
reported: unknown,
|
|
): CdpRequestWillBeSentFacts => {
|
|
const source = recordValue(reported);
|
|
const request = recordValue(source?.request);
|
|
const initiator = recordValue(source?.initiator);
|
|
const response = readCdpResponseFacts(source?.redirectResponse);
|
|
return {
|
|
source,
|
|
requestId: requestIdentity(source),
|
|
frameId: cdpStringValue(source?.frameId),
|
|
loaderId: cdpStringValue(source?.loaderId),
|
|
resourceType: cdpStringValue(source?.type),
|
|
timestamp: numberValue(source?.timestamp),
|
|
request:
|
|
request === undefined
|
|
? undefined
|
|
: {
|
|
source: request,
|
|
url: cdpStringValue(request.url),
|
|
method: cdpStringValue(request.method),
|
|
},
|
|
initiator:
|
|
initiator === undefined
|
|
? undefined
|
|
: { source: initiator, type: cdpStringValue(initiator.type) },
|
|
redirect:
|
|
source === undefined || !Object.hasOwn(source, "redirectResponse")
|
|
? { kind: "missing" }
|
|
: response === undefined
|
|
? { kind: "malformed" }
|
|
: { kind: "response", response },
|
|
};
|
|
};
|
|
|
|
export const readCdpResponseReceived = (reported: unknown) => {
|
|
const source = recordValue(reported);
|
|
return {
|
|
source,
|
|
requestId: requestIdentity(source),
|
|
response: readCdpResponseFacts(source?.response),
|
|
};
|
|
};
|
|
export const readCdpLoadingFinished = (reported: unknown) => {
|
|
const source = recordValue(reported);
|
|
return {
|
|
source,
|
|
requestId: requestIdentity(source),
|
|
timestamp: numberValue(source?.timestamp),
|
|
encodedDataLength: numberValue(source?.encodedDataLength),
|
|
};
|
|
};
|
|
export const readCdpLoadingFailed = (reported: unknown) => {
|
|
const source = recordValue(reported);
|
|
return {
|
|
source,
|
|
requestId: requestIdentity(source),
|
|
timestamp: numberValue(source?.timestamp),
|
|
errorText: source?.errorText,
|
|
};
|
|
};
|