// SPEC: PJ2026-01040111 long-running Workbench observation. // Responsibility: Runner low-level IO, URL, YAML-derived policy parsing, redaction, and timeout utility source fragment. export function nodeWebObserveRunnerUtilitySource(): string { return String.raw`async function appendJsonl(file, value) { await appendFile(file, JSON.stringify(sanitize(value)) + "\n", { mode: 0o600 }); } async function fileMeta(file) { const [buffer, stats] = await Promise.all([readFile(file), stat(file)]); return { byteCount: stats.size, sha256: "sha256:" + createHash("sha256").update(buffer).digest("hex") }; } function currentPageUrl() { return pageUrl(page); } function pageUrl(targetPage) { try { return targetPage && !targetPage.isClosed() ? targetPage.url() : null; } catch { return null; } } function routeSessionIdFromUrl(value) { try { const pathname = new URL(String(value || ""), baseUrl).pathname; const match = pathname.match(/\/workbench\/sessions\/([^/?#]+)/u); return match ? decodeURIComponent(match[1] || "") : null; } catch { return null; } } function safeFrameUrl(frame) { try { return frame ? safeUrl(frame.url()) : null; } catch { return null; } } function safeUrl(value) { try { const url = new URL(String(value), baseUrl); for (const key of Array.from(url.searchParams.keys())) { if (/token|key|secret|password|auth|cookie/iu.test(key)) url.searchParams.set(key, "[redacted]"); } return url.toString(); } catch { return truncate(String(value || ""), 300); } } function safeUrlPath(value) { try { return new URL(String(value || ""), baseUrl).pathname; } catch { return null; } } function normalizeBaseUrl(value) { const raw = value || "http://127.0.0.1:3000"; const url = new URL(raw); return url.origin; } function parseViewport(value) { const match = String(value).match(/^(\d{3,5})x(\d{3,5})$/u); return match ? { width: Number(match[1]), height: Number(match[2]) } : { width: 1440, height: 900 }; } function positiveInteger(value, fallback) { const parsed = Number(value); return Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : fallback; } function boundedInteger(value, fallback, min, max) { const parsed = Number(value); if (!Number.isFinite(parsed)) return fallback; const integer = Math.floor(parsed); if (integer < min || integer > max) return fallback; return integer; } function positiveNumber(value, fallback) { const parsed = Number(value); return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; } function numberOrNull(value) { const parsed = Number(value); return Number.isFinite(parsed) ? parsed : null; } function requiredPositiveThreshold(raw, key) { const parsed = Number(raw?.[key]); if (!Number.isFinite(parsed) || parsed <= 0) { throw new Error("UNIDESK_WEB_OBSERVE_ALERT_THRESHOLDS_JSON requires positive " + key + "; configure config/hwlab-node-lanes.yaml webProbe.alertThresholds"); } return parsed; } function parseAlertThresholds(value) { if (!value) { throw new Error("UNIDESK_WEB_OBSERVE_ALERT_THRESHOLDS_JSON is required; configure config/hwlab-node-lanes.yaml webProbe.alertThresholds for the selected node/lane"); } const raw = (() => { try { return JSON.parse(value); } catch (error) { throw new Error("UNIDESK_WEB_OBSERVE_ALERT_THRESHOLDS_JSON is invalid JSON: " + (error instanceof Error ? error.message : String(error))); } })(); const sessionRailFallbackRatio = requiredPositiveThreshold(raw, "sessionRailFallbackRatio"); if (sessionRailFallbackRatio > 1) { throw new Error("UNIDESK_WEB_OBSERVE_ALERT_THRESHOLDS_JSON sessionRailFallbackRatio must be <= 1"); } return { sameOriginApiSlowMs: requiredPositiveThreshold(raw, "sameOriginApiSlowMs"), partialApiSlowMs: requiredPositiveThreshold(raw, "partialApiSlowMs"), longLivedStreamOpenSlowMs: requiredPositiveThreshold(raw, "longLivedStreamOpenSlowMs"), visibleLoadingSlowMs: requiredPositiveThreshold(raw, "visibleLoadingSlowMs"), turnTimingSampleSlackSeconds: requiredPositiveThreshold(raw, "turnTimingSampleSlackSeconds"), turnElapsedSevereTimeoutSeconds: requiredPositiveThreshold(raw, "turnElapsedSevereTimeoutSeconds"), domEvaluateTimeoutRedCount: requiredPositiveThreshold(raw, "domEvaluateTimeoutRedCount"), domEvaluateTimeoutRedWindowMs: requiredPositiveThreshold(raw, "domEvaluateTimeoutRedWindowMs"), screenshotTimeoutRedCount: requiredPositiveThreshold(raw, "screenshotTimeoutRedCount"), pageErrorRedCount: requiredPositiveThreshold(raw, "pageErrorRedCount"), longTaskRedMs: requiredPositiveThreshold(raw, "longTaskRedMs"), longAnimationFrameRedMs: requiredPositiveThreshold(raw, "longAnimationFrameRedMs"), eventLoopGapRedMs: requiredPositiveThreshold(raw, "eventLoopGapRedMs"), browserProcessSampleIntervalMs: requiredPositiveThreshold(raw, "browserProcessSampleIntervalMs"), requestRateBucketMs: requiredPositiveThreshold(raw, "requestRateBucketMs"), requestRateTotalRedPerMinute: requiredPositiveThreshold(raw, "requestRateTotalRedPerMinute"), requestRatePageRedPerMinute: requiredPositiveThreshold(raw, "requestRatePageRedPerMinute"), requestRateApiPathRedPerMinute: requiredPositiveThreshold(raw, "requestRateApiPathRedPerMinute"), browserTotalRssRedMb: requiredPositiveThreshold(raw, "browserTotalRssRedMb"), browserProcessRssRedMb: requiredPositiveThreshold(raw, "browserProcessRssRedMb"), browserRssGrowthRedMb: requiredPositiveThreshold(raw, "browserRssGrowthRedMb"), browserRssGrowthWindowMs: requiredPositiveThreshold(raw, "browserRssGrowthWindowMs"), playwrightResponsivenessRedMs: requiredPositiveThreshold(raw, "playwrightResponsivenessRedMs"), playwrightResponsivenessTimeoutRedCount: requiredPositiveThreshold(raw, "playwrightResponsivenessTimeoutRedCount"), cdpMetricsTimeoutRedCount: requiredPositiveThreshold(raw, "cdpMetricsTimeoutRedCount"), uncommandedStateChangeCommandWindowMs: requiredPositiveThreshold(raw, "uncommandedStateChangeCommandWindowMs"), scrollJumpCommandWindowMs: requiredPositiveThreshold(raw, "scrollJumpCommandWindowMs"), scrollJumpFromY: requiredPositiveThreshold(raw, "scrollJumpFromY"), scrollJumpToY: requiredPositiveThreshold(raw, "scrollJumpToY"), sessionRailFallbackRatio, crossPageProjectionDivergenceRedMs: positiveNumber(raw.crossPageProjectionDivergenceRedMs, requiredPositiveThreshold(raw, "visibleLoadingSlowMs")), source: "yaml-env", }; } function parseBrowserFreezePolicy(value) { if (!value) { throw new Error("UNIDESK_WEB_OBSERVE_BROWSER_FREEZE_POLICY_JSON is required; configure config/hwlab-node-lanes.yaml webProbe.browserFreezePolicy for the selected node/lane"); } const raw = (() => { try { return JSON.parse(value); } catch (error) { throw new Error("UNIDESK_WEB_OBSERVE_BROWSER_FREEZE_POLICY_JSON is invalid JSON: " + (error instanceof Error ? error.message : String(error))); } })(); const memory = requiredPolicyRecord(raw, "memory", "webProbe.browserFreezePolicy"); const responsiveness = requiredPolicyRecord(raw, "responsiveness", "webProbe.browserFreezePolicy"); const cdp = requiredPolicyRecord(raw, "cdp", "webProbe.browserFreezePolicy"); const kill = requiredPolicyRecord(raw, "kill", "webProbe.browserFreezePolicy"); return { enabled: requiredPolicyBoolean(raw, "enabled", "webProbe.browserFreezePolicy"), blockerWindowMs: requiredPolicyPositiveNumber(raw, "blockerWindowMs", "webProbe.browserFreezePolicy"), memory: { totalRssBlockerMb: requiredPolicyPositiveNumber(memory, "totalRssBlockerMb", "webProbe.browserFreezePolicy.memory"), processRssBlockerMb: requiredPolicyPositiveNumber(memory, "processRssBlockerMb", "webProbe.browserFreezePolicy.memory"), growthBlockerMb: requiredPolicyPositiveNumber(memory, "growthBlockerMb", "webProbe.browserFreezePolicy.memory"), }, responsiveness: { latencyBlockerMs: requiredPolicyPositiveNumber(responsiveness, "latencyBlockerMs", "webProbe.browserFreezePolicy.responsiveness"), eventBlockerCount: requiredPolicyPositiveNumber(responsiveness, "eventBlockerCount", "webProbe.browserFreezePolicy.responsiveness"), }, cdp: { metricsTimeoutBlockerCount: requiredPolicyPositiveNumber(cdp, "metricsTimeoutBlockerCount", "webProbe.browserFreezePolicy.cdp"), }, kill: { enabled: requiredPolicyBoolean(kill, "enabled", "webProbe.browserFreezePolicy.kill"), gracefulSignal: requiredPolicySignal(kill, "gracefulSignal", "webProbe.browserFreezePolicy.kill", "SIGTERM"), forceSignal: requiredPolicySignal(kill, "forceSignal", "webProbe.browserFreezePolicy.kill", "SIGKILL"), graceMs: requiredPolicyIntegerInRange(kill, "graceMs", "webProbe.browserFreezePolicy.kill", 1, 120000), pollIntervalMs: requiredPolicyIntegerInRange(kill, "pollIntervalMs", "webProbe.browserFreezePolicy.kill", 1, 10000), exitCode: requiredPolicyIntegerInRange(kill, "exitCode", "webProbe.browserFreezePolicy.kill", 1, 125), }, source: "yaml-env", valuesRedacted: true, }; } function requiredPolicyRecord(raw, key, pathLabel) { const value = raw?.[key]; if (!value || typeof value !== "object" || Array.isArray(value)) { throw new Error("UNIDESK_WEB_OBSERVE_BROWSER_FREEZE_POLICY_JSON requires object " + pathLabel + "." + key); } return value; } function requiredPolicyBoolean(raw, key, pathLabel) { const value = raw?.[key]; if (typeof value !== "boolean") { throw new Error("UNIDESK_WEB_OBSERVE_BROWSER_FREEZE_POLICY_JSON requires boolean " + pathLabel + "." + key); } return value; } function requiredPolicyPositiveNumber(raw, key, pathLabel) { const value = Number(raw?.[key]); if (!Number.isFinite(value) || value <= 0) { throw new Error("UNIDESK_WEB_OBSERVE_BROWSER_FREEZE_POLICY_JSON requires positive number " + pathLabel + "." + key); } return value; } function requiredPolicyIntegerInRange(raw, key, pathLabel, min, max) { const value = Number(raw?.[key]); if (!Number.isInteger(value) || value < min || value > max) { throw new Error("UNIDESK_WEB_OBSERVE_BROWSER_FREEZE_POLICY_JSON requires integer " + pathLabel + "." + key + " between " + min + " and " + max); } return value; } function requiredPolicySignal(raw, key, pathLabel, expected) { const value = String(raw?.[key] || ""); if (value !== expected) { throw new Error("UNIDESK_WEB_OBSERVE_BROWSER_FREEZE_POLICY_JSON requires " + pathLabel + "." + key + "=" + expected); } return value; } function parseProjectManagementConfig(value) { if (!value || value === "null") { return { enabled: false, targetPaths: [], readinessSelectors: [], naturalApiPathPrefixes: [], commandAllowlist: [], launchRoute: "", slowApiBudgetMs: 0, source: "yaml-env", valuesRedacted: true }; } const raw = (() => { try { return JSON.parse(value); } catch (error) { throw new Error("UNIDESK_WEB_OBSERVE_PROJECT_MANAGEMENT_JSON is invalid JSON: " + (error instanceof Error ? error.message : String(error))); } })(); const stringList = (key) => { const list = raw?.[key]; if (!Array.isArray(list) || list.some((item) => typeof item !== "string" || item.length === 0)) throw new Error("UNIDESK_WEB_OBSERVE_PROJECT_MANAGEMENT_JSON requires string[] " + key + "; configure config/hwlab-node-lanes.yaml webProbe.projectManagement"); return list; }; const positive = (key) => { const numeric = Number(raw?.[key]); if (!Number.isFinite(numeric) || numeric <= 0) throw new Error("UNIDESK_WEB_OBSERVE_PROJECT_MANAGEMENT_JSON requires positive " + key + "; configure config/hwlab-node-lanes.yaml webProbe.projectManagement"); return numeric; }; if (raw?.enabled !== true && raw?.enabled !== false) throw new Error("UNIDESK_WEB_OBSERVE_PROJECT_MANAGEMENT_JSON requires boolean enabled"); if (raw.enabled !== true) return { enabled: false, targetPaths: [], readinessSelectors: [], naturalApiPathPrefixes: [], commandAllowlist: [], launchRoute: "", slowApiBudgetMs: 0, source: "yaml-env", valuesRedacted: true }; const targetPaths = stringList("targetPaths"); const readinessSelectors = stringList("readinessSelectors"); const naturalApiPathPrefixes = stringList("naturalApiPathPrefixes"); const commandAllowlist = stringList("commandAllowlist"); const launchRoute = String(raw.launchRoute || ""); if (!launchRoute.startsWith("/")) throw new Error("UNIDESK_WEB_OBSERVE_PROJECT_MANAGEMENT_JSON launchRoute must be an absolute path"); return { enabled: true, targetPaths, readinessSelectors, naturalApiPathPrefixes, commandAllowlist, launchRoute, slowApiBudgetMs: positive("slowApiBudgetMs"), source: "yaml-env", valuesRedacted: true }; } function sha256Text(value) { return "sha256:" + createHash("sha256").update(String(value)).digest("hex"); } function safeId(value) { return String(value || "").replace(/[^A-Za-z0-9_.-]+/gu, "-").replace(/^-+|-+$/gu, "").slice(0, 120) || "item"; } function cssEscape(value) { return String(value).replace(/\\/gu, "\\\\").replace(/"/gu, "\\\""); } function truncate(value, limit) { const text = String(value || ""); return text.length > limit ? text.slice(0, limit) + "..." : text; } function sanitize(value) { if (value === null || value === undefined) return value; if (typeof value === "string") return value === password ? "[redacted]" : value.replaceAll(password, "[redacted]"); if (typeof value === "number" || typeof value === "boolean") return value; if (Array.isArray(value)) return value.map(sanitize); if (typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, item]) => /password|cookie|authorization|token|secret/iu.test(key) ? [key, "[redacted]"] : [key, sanitize(item)])); return String(value); } function errorSummary(error) { const summary = { name: error && error.name ? error.name : "Error", message: error && error.message ? truncate(error.message, 1000) : truncate(String(error), 1000), stackTail: error && error.stack ? truncate(error.stack, 2000) : null }; if (error && error.webProbeAuth) summary.auth = sanitize(error.webProbeAuth); if (error && Array.isArray(error.attempts)) summary.attempts = sanitize(error.attempts.slice(-5)); if (error && error.navigationReadiness) summary.navigationReadiness = sanitize(error.navigationReadiness); if (error && error.details) summary.details = sanitize(error.details); if (error && error.target) summary.target = sanitize(error.target); return summary; } function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms))); } function withHardTimeout(promise, timeoutMs, message) { const guarded = Promise.resolve(promise); let timer = null; const timeout = new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(message)), Math.max(1, Number(timeoutMs) || 1)); if (timer && typeof timer.unref === "function") timer.unref(); }); return Promise.race([guarded, timeout]).finally(() => { if (timer) clearTimeout(timer); guarded.catch(() => {}); }); } `; }