Skip to content

Commit 3f06b58

Browse files
committed
브라우저: 긴 screenshot 경계를 공개 오류로 정규화
최신 CSS content bounds를 읽고 dimension, area, clip extent를 capture 전에 막는다. 오류는 측정값, 안정 limit, 비자동 복구 경계를 Control detail로 보존한다. 설치 제품이 32768px 성공과 세 초과 조건을 실 Edge에서 검증하며 10번 계획을 닫았다. 검증: npm test 13/13, 계약 47개, browser-control 74/74, Control 제품 30/30. provider 원문을 던지는 음성 fixture는 capture 호출 0회와 안정 code를 확인했다.
1 parent 37cfdba commit 3f06b58

12 files changed

Lines changed: 269 additions & 170 deletions

File tree

mainPlan/10-longPageScreenshotEvidence/README.md

Lines changed: 0 additions & 117 deletions
This file was deleted.

mainPlan/README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,8 @@
1717

1818
## 현재 상태
1919

20-
현재 실행 중인 이니셔티브는 [10-longPageScreenshotEvidence](10-longPageScreenshotEvidence/README.md)다.
21-
긴 content bounds가 `fullPage` screenshot 한계를 넘을 때 provider 원문 대신 공개 오류 코드와 측정
22-
detail을 반환하도록 계약을 정규화한다.
20+
현재 실행 중인 이니셔티브는 없다. 새 제품 간극이 재현되면 이 규칙에 따라 다음 번호의 폴더를 만들고,
21+
종료 조건까지 같은 직렬 흐름에서 수행한다.
2322

2423
이 번호는 agent-computer 포트폴리오의 실행 순서다. North Star ceiling ladder의 기존 번호와 우선순위를
2524
대체하지 않는다.

scripts/browserControl/browserAutomationCatalog.js

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
import {
1212
BROWSER_SCREENSHOT_FORMATS,
1313
BROWSER_SCREENSHOT_MAX_CSS_DIMENSION,
14+
validateBrowserScreenshotBounds,
1415
} from "./browserScreenshot.js";
1516
import {
1617
APX_LEGACY_REPRESENTATION,
@@ -470,17 +471,10 @@ export function validateBrowserAutomationAction(action) {
470471
for (const key of ["x", "y", "width", "height"]) {
471472
if (typeof action.clip[key] !== "number" || !Number.isFinite(action.clip[key])) fail(`screenshot.clip.${key} must be finite`);
472473
}
473-
if (action.clip.x < 0 || action.clip.y < 0 || action.clip.width <= 0 || action.clip.height <= 0
474-
|| action.clip.x > BROWSER_SCREENSHOT_MAX_CSS_DIMENSION
475-
|| action.clip.y > BROWSER_SCREENSHOT_MAX_CSS_DIMENSION
476-
|| action.clip.width > BROWSER_SCREENSHOT_MAX_CSS_DIMENSION
477-
|| action.clip.height > BROWSER_SCREENSHOT_MAX_CSS_DIMENSION
478-
|| action.clip.x + action.clip.width > BROWSER_SCREENSHOT_MAX_CSS_DIMENSION
479-
|| action.clip.y + action.clip.height > BROWSER_SCREENSHOT_MAX_CSS_DIMENSION) {
480-
fail("screenshot.clip is outside CSS bounds");
474+
if (action.clip.scale !== undefined && (typeof action.clip.scale !== "number" || !Number.isFinite(action.clip.scale))) {
475+
fail("screenshot.clip.scale must be finite");
481476
}
482-
if (action.clip.scale !== undefined && (typeof action.clip.scale !== "number" || !Number.isFinite(action.clip.scale)
483-
|| action.clip.scale < 0.1 || action.clip.scale > 3)) fail("screenshot.clip.scale is invalid");
477+
validateBrowserScreenshotBounds({ source: "clip", ...action.clip, scale: action.clip.scale ?? 1 });
484478
}
485479
}
486480
if (action.kind === "waitFor") {

scripts/browserControl/browserControlPort.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,13 @@ const TRUSTED_READ_METHODS = new Set([
2727
]);
2828

2929
export class BrowserControlError extends Error {
30-
constructor(code, message, { outcome = "notSent", retryable = false, cause = undefined } = {}) {
30+
constructor(code, message, { outcome = "notSent", retryable = false, cause = undefined, details = undefined } = {}) {
3131
super(message, cause ? { cause } : undefined);
3232
this.name = "BrowserControlError";
3333
this.code = code;
3434
this.outcome = outcome;
3535
this.retryable = retryable;
36+
if (details !== undefined) this.details = details;
3637
}
3738
}
3839

scripts/browserControl/browserScreenshot.js

Lines changed: 85 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,70 @@ function validSignature(bytes, format) {
1616
&& bytes.subarray(8, 12).toString("ascii") === "WEBP";
1717
}
1818

19-
function boundedDimension(value, label) {
19+
function reportedNumber(value) {
2020
const number = Number(value);
21-
if (!Number.isFinite(number) || number <= 0 || number > BROWSER_SCREENSHOT_MAX_CSS_DIMENSION) {
22-
throw new BrowserControlError("BROWSER_AUTOMATION_SCREENSHOT_BOUNDS",
23-
`browser screenshot ${label} is outside the supported CSS bounds`, { outcome: "notSent" });
24-
}
25-
return number;
21+
return Number.isFinite(number) ? number : null;
2622
}
2723

28-
function boundedScale(value) {
29-
const number = Number(value);
30-
if (!Number.isFinite(number) || number < 0.1 || number > 3) {
31-
throw new BrowserControlError("BROWSER_AUTOMATION_SCREENSHOT_BOUNDS",
32-
"browser screenshot clip scale is outside the supported bounds", { outcome: "notSent" });
24+
function boundsError(source, reason, measured) {
25+
const details = Object.freeze({
26+
reason,
27+
source,
28+
measured: Object.freeze({
29+
x: reportedNumber(measured.x),
30+
y: reportedNumber(measured.y),
31+
cssWidth: reportedNumber(measured.width),
32+
cssHeight: reportedNumber(measured.height),
33+
scale: reportedNumber(measured.scale),
34+
scaledCssPixels: reportedNumber(measured.scaledCssPixels),
35+
}),
36+
limits: Object.freeze({
37+
maxCssDimension: BROWSER_SCREENSHOT_MAX_CSS_DIMENSION,
38+
maxScaledCssPixels: BROWSER_SCREENSHOT_MAX_CSS_PIXELS,
39+
minScale: 0.1,
40+
maxScale: 3,
41+
}),
42+
recovery: Object.freeze({ automatic: false, viewportScrollMayTriggerEffects: true }),
43+
});
44+
return new BrowserControlError("BROWSER_AUTOMATION_SCREENSHOT_BOUNDS",
45+
`browser screenshot ${source} exceeds the supported ${reason} bounds`, {
46+
outcome: "notSent",
47+
retryable: false,
48+
details,
49+
});
50+
}
51+
52+
export function validateBrowserScreenshotBounds({ source, x = 0, y = 0, width, height, scale = 1 }) {
53+
const measured = {
54+
x: Number(x),
55+
y: Number(y),
56+
width: Number(width),
57+
height: Number(height),
58+
scale: Number(scale),
59+
};
60+
measured.scaledCssPixels = measured.width * measured.height * measured.scale * measured.scale;
61+
if (!Number.isFinite(measured.scale) || measured.scale < 0.1 || measured.scale > 3) {
62+
throw boundsError(source, "scale", measured);
63+
}
64+
if (!Number.isFinite(measured.x) || !Number.isFinite(measured.y) || measured.x < 0 || measured.y < 0) {
65+
throw boundsError(source, "origin", measured);
3366
}
34-
return number;
67+
if (!Number.isFinite(measured.width) || !Number.isFinite(measured.height)
68+
|| measured.width <= 0 || measured.height <= 0
69+
|| measured.width > BROWSER_SCREENSHOT_MAX_CSS_DIMENSION
70+
|| measured.height > BROWSER_SCREENSHOT_MAX_CSS_DIMENSION) {
71+
throw boundsError(source, "dimension", measured);
72+
}
73+
if (measured.x > BROWSER_SCREENSHOT_MAX_CSS_DIMENSION
74+
|| measured.y > BROWSER_SCREENSHOT_MAX_CSS_DIMENSION
75+
|| measured.x + measured.width > BROWSER_SCREENSHOT_MAX_CSS_DIMENSION
76+
|| measured.y + measured.height > BROWSER_SCREENSHOT_MAX_CSS_DIMENSION) {
77+
throw boundsError(source, "extent", measured);
78+
}
79+
if (measured.scaledCssPixels > BROWSER_SCREENSHOT_MAX_CSS_PIXELS) {
80+
throw boundsError(source, "area", measured);
81+
}
82+
return Object.freeze(measured);
3583
}
3684

3785
export class BrowserScreenshot {
@@ -49,26 +97,37 @@ export class BrowserScreenshot {
4997
const layout = await this._command(sessionRef, "Page.getLayoutMetrics", {}, commandResults, signal);
5098
const metrics = layout.result || {};
5199
const viewport = metrics.cssVisualViewport || metrics.visualViewport || {};
52-
const content = metrics.contentSize || {};
100+
// 최신 CDP의 CSS pixel 필드를 우선한다. deprecated contentSize는 device pixel일 수 있다.
101+
const content = metrics.cssContentSize || metrics.contentSize || {};
53102
let clip = null;
54103
let cssWidth;
55104
let cssHeight;
56105
if (options.clip) {
57-
clip = { ...options.clip, scale: boundedScale(options.clip.scale ?? 1) };
58-
cssWidth = boundedDimension(clip.width, "clip width");
59-
cssHeight = boundedDimension(clip.height, "clip height");
106+
const bounds = validateBrowserScreenshotBounds({
107+
source: "clip",
108+
...options.clip,
109+
scale: options.clip.scale ?? 1,
110+
});
111+
clip = { x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height, scale: bounds.scale };
112+
cssWidth = bounds.width;
113+
cssHeight = bounds.height;
60114
} else if (options.fullPage === true) {
61-
cssWidth = boundedDimension(content.width, "content width");
62-
cssHeight = boundedDimension(content.height, "content height");
63-
clip = { x: 0, y: 0, width: cssWidth, height: cssHeight, scale: 1 };
115+
const bounds = validateBrowserScreenshotBounds({
116+
source: "content",
117+
width: content.width,
118+
height: content.height,
119+
});
120+
cssWidth = bounds.width;
121+
cssHeight = bounds.height;
122+
clip = { x: 0, y: 0, width: bounds.width, height: bounds.height, scale: 1 };
64123
} else {
65-
cssWidth = boundedDimension(viewport.clientWidth, "viewport width");
66-
cssHeight = boundedDimension(viewport.clientHeight, "viewport height");
67-
}
68-
const scale = clip?.scale ?? 1;
69-
if (cssWidth * cssHeight * scale * scale > BROWSER_SCREENSHOT_MAX_CSS_PIXELS) {
70-
throw new BrowserControlError("BROWSER_AUTOMATION_SCREENSHOT_BOUNDS",
71-
"browser screenshot exceeds the CSS pixel area limit", { outcome: "notSent" });
124+
const bounds = validateBrowserScreenshotBounds({
125+
source: "viewport",
126+
width: viewport.clientWidth,
127+
height: viewport.clientHeight,
128+
});
129+
cssWidth = bounds.width;
130+
cssHeight = bounds.height;
72131
}
73132
const captured = await this._command(sessionRef, "Page.captureScreenshot", {
74133
format,

scripts/browserControl/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export {
5656
BROWSER_SCREENSHOT_FORMATS,
5757
BROWSER_SCREENSHOT_MAX_CSS_DIMENSION,
5858
BROWSER_SCREENSHOT_MAX_CSS_PIXELS,
59+
validateBrowserScreenshotBounds,
5960
} from "./browserScreenshot.js";
6061
export { BrowserObservation, redactBrowserUrl } from "./browserObservation.js";
6162
export {

0 commit comments

Comments
 (0)