aboutsummaryrefslogtreecommitdiff
path: root/extensions
diff options
context:
space:
mode:
Diffstat (limited to 'extensions')
-rw-r--r--extensions/chrome-profile-bridge/browser-extension/service_worker.js89
-rw-r--r--extensions/chrome-profile-bridge/index.ts11
2 files changed, 61 insertions, 39 deletions
diff --git a/extensions/chrome-profile-bridge/browser-extension/service_worker.js b/extensions/chrome-profile-bridge/browser-extension/service_worker.js
index d47b385..9ac247c 100644
--- a/extensions/chrome-profile-bridge/browser-extension/service_worker.js
+++ b/extensions/chrome-profile-bridge/browser-extension/service_worker.js
@@ -614,27 +614,36 @@ async function executeScriptWithFallback(options, label) {
if (!isScriptingAccessDenied(error) || !chrome.debugger) throw error;
const tabId = options?.target?.tabId;
recordAttachEvent({ kind: "scripting-denied-cdp-fallback", tabId, files: options?.files || null, message: String(error?.message || error).slice(0, 300) });
- if (Array.isArray(options?.files) && options.files.length) {
- for (const file of options.files) {
- if (!packagedFileSources.has(file)) {
- const response = await fetch(chrome.runtime.getURL(file));
- if (!response.ok) throw new Error(`Could not read ${file} for CDP injection (HTTP ${response.status})`);
- packagedFileSources.set(file, await response.text());
- }
- const injected = await cdpEval(tabId, packagedFileSources.get(file));
- if (injected.exceptionDetails) {
- throw new Error(`${label || file}: ${cdpExceptionText(injected.exceptionDetails) || "CDP injection failed"}`);
- }
+ return executeScriptViaCdp(options, label);
+ }
+}
+
+// The CDP half of executeScriptWithFallback, also used directly when scripting silently runs
+// nothing: a document served with CSP `sandbox` (no allow-scripts), such as
+// raw.githubusercontent.com, makes chrome.scripting.executeScript resolve with a null result
+// instead of throwing, while Runtime.evaluate still works there.
+async function executeScriptViaCdp(options, label) {
+ const tabId = options?.target?.tabId;
+ if (Array.isArray(options?.files) && options.files.length) {
+ for (const file of options.files) {
+ if (!packagedFileSources.has(file)) {
+ const response = await fetch(chrome.runtime.getURL(file));
+ if (!response.ok) throw new Error(`Could not read ${file} for CDP injection (HTTP ${response.status})`);
+ packagedFileSources.set(file, await response.text());
+ }
+ const injected = await cdpEval(tabId, packagedFileSources.get(file));
+ if (injected.exceptionDetails) {
+ throw new Error(`${label || file}: ${cdpExceptionText(injected.exceptionDetails) || "CDP injection failed"}`);
}
- return [{ result: undefined }];
- }
- const expression = `(${options.func.toString()})(...${JSON.stringify(Array.isArray(options.args) ? options.args : [])})`;
- const result = await cdpEval(tabId, expression);
- if (result.exceptionDetails) {
- throw new Error(`${label || "page script"}: ${cdpExceptionText(result.exceptionDetails) || "evaluation failed"}`);
}
- return [{ result: result.result?.value }];
+ return [{ result: undefined }];
+ }
+ const expression = `(${options.func.toString()})(...${JSON.stringify(Array.isArray(options.args) ? options.args : [])})`;
+ const result = await cdpEval(tabId, expression);
+ if (result.exceptionDetails) {
+ throw new Error(`${label || "page script"}: ${cdpExceptionText(result.exceptionDetails) || "evaluation failed"}`);
}
+ return [{ result: result.result?.value }];
}
// Wraps cdpRaw with one auto-recover on detached/closed sessions:
@@ -2412,6 +2421,28 @@ async function withOptionalSnapshot(params, actionFn) {
// chrome.scripting.executeScript({ files }). That file is free of eval/new Function, so it works
// on strict-CSP pages, and it installs globalThis.__piChromeSnapshotPage / __piChromeInspectTarget.
// It shares window.__PI_CHROME_STATE__ (same el- uid scheme) with the CDP-injected input helpers.
+// Injects snapshot_injected.js and runs `scriptOptions.func` (which must return an
+// { ok, value | error } envelope) in the tab's top frame. chrome.scripting is primary; if it
+// resolves without an envelope, the page ran no extension script (CSP `sandbox` without
+// allow-scripts, e.g. raw.githubusercontent.com), so both steps are redone over CDP.
+async function runSnapshotInjected(tab, kind, scriptOptions) {
+ const inject = { target: { tabId: tab.id, frameIds: [0] }, world: "MAIN", files: ["snapshot_injected.js"] };
+ const run = { target: { tabId: tab.id, frameIds: [0] }, world: "MAIN", ...scriptOptions };
+ const injectLabel = `inject ${kind} script in tab ${tab.id}`;
+ const runLabel = `run ${kind} script in tab ${tab.id}`;
+ await executeScriptWithFallback(inject, injectLabel);
+ const results = await executeScriptWithFallback(run, runLabel);
+ const first = results?.[0];
+ if (first?.error || first?.result != null || !chrome.debugger) return results;
+ recordAttachEvent({ kind: "scripting-no-result-cdp-fallback", tabId: tab.id, files: inject.files });
+ await executeScriptViaCdp(inject, injectLabel);
+ const viaCdp = await executeScriptViaCdp(run, runLabel);
+ if (viaCdp?.[0]?.result == null) {
+ throw new Error(`Chrome ${kind} script returned no result in tab ${tab.id} (${tab.url || "unknown url"}); the page may block script execution.`);
+ }
+ return viaCdp;
+}
+
async function snapshotInTab(params) {
const tab = await getTabByParams(params);
await bringToFront(tab, params);
@@ -2424,14 +2455,7 @@ async function snapshotInTab(params) {
params.query ?? null,
params.maxTextChars ?? null,
];
- await executeScriptWithFallback({
- target: { tabId: tab.id, frameIds: [0] },
- world: "MAIN",
- files: ["snapshot_injected.js"],
- }, `inject snapshot script in tab ${tab.id}`);
- const results = await executeScriptWithFallback({
- target: { tabId: tab.id, frameIds: [0] },
- world: "MAIN",
+ const results = await runSnapshotInjected(tab, "snapshot", {
func: async (invocationArgs) => {
try {
const snapshotPage = globalThis.__piChromeSnapshotPage;
@@ -2442,7 +2466,7 @@ async function snapshotInTab(params) {
}
},
args: [args],
- }, `run snapshot script in tab ${tab.id}`);
+ });
const first = results?.[0];
if (first?.error) {
const message = typeof first.error === "string" ? first.error : (first.error.message || JSON.stringify(first.error));
@@ -2460,14 +2484,7 @@ async function inspectInTab(params) {
const tab = await getTabByParams(params);
await bringToFront(tab, params);
const args = [params.uid ?? null, params.selector ?? null, params.scrollIntoView === true];
- await executeScriptWithFallback({
- target: { tabId: tab.id, frameIds: [0] },
- world: "MAIN",
- files: ["snapshot_injected.js"],
- }, `inject inspect script in tab ${tab.id}`);
- const results = await executeScriptWithFallback({
- target: { tabId: tab.id, frameIds: [0] },
- world: "MAIN",
+ const results = await runSnapshotInjected(tab, "inspect", {
func: async (invocationArgs) => {
try {
const inspectTarget = globalThis.__piChromeInspectTarget;
@@ -2478,7 +2495,7 @@ async function inspectInTab(params) {
}
},
args: [args],
- }, `run inspect script in tab ${tab.id}`);
+ });
const first = results?.[0];
if (first?.error) {
const message = typeof first.error === "string" ? first.error : (first.error.message || JSON.stringify(first.error));
diff --git a/extensions/chrome-profile-bridge/index.ts b/extensions/chrome-profile-bridge/index.ts
index 525de6e..0338c5c 100644
--- a/extensions/chrome-profile-bridge/index.ts
+++ b/extensions/chrome-profile-bridge/index.ts
@@ -133,8 +133,11 @@ function truncateText(text: string, maxChars = MAX_TEXT_CHARS): string {
return `${text.slice(0, maxChars)}\n\n[truncated ${text.length - maxChars} characters]`;
}
+// JSON.stringify returns undefined (not a string) for undefined, functions, and symbols. A tool
+// result block with `text: undefined` is persisted to the session and makes every later turn
+// fail with "Cannot read properties of undefined (reading 'length')", so always return a string.
function safeJson(value: unknown): string {
- return JSON.stringify(value, null, 2);
+ return JSON.stringify(value, null, 2) ?? String(value);
}
const snapshotModeValues = ["auto", "interactive", "forms", "pageMap", "text", "changes", "full"] as const;
@@ -150,7 +153,8 @@ function rectText(rect: any): string {
}
function formatChromeSnapshot(snapshot: any): string {
- if (!snapshot || typeof snapshot !== "object") return safeJson(snapshot);
+ if (snapshot == null) throw new Error("Chrome returned no snapshot for this page; it may block extension scripts. Try chrome_evaluate instead.");
+ if (typeof snapshot !== "object") return safeJson(snapshot);
if (snapshot.mode === "full") return truncateText(safeJson(snapshot));
const lines: string[] = [];
lines.push(`# Chrome snapshot${snapshot.mode ? ` (${snapshot.mode})` : ""}`);
@@ -351,7 +355,8 @@ async function pruneScreenshotDir(dir: string, retentionDays?: number): Promise<
// End screenshot pruning.
function formatChromeInspect(inspect: any): string {
- if (!inspect || typeof inspect !== "object") return safeJson(inspect);
+ if (inspect == null) throw new Error("Chrome returned no inspect result for this page; it may block extension scripts. Try chrome_evaluate instead.");
+ if (typeof inspect !== "object") return safeJson(inspect);
const t = inspect.target || {};
const lines: string[] = [];
lines.push(`# Chrome inspect ${t.uid || ""}`.trim());