diff --git a/src/agents/tools/browser-tool.actions.ts b/src/agents/tools/browser-tool.actions.ts index 12cb54e323d..37fc4a099c5 100644 --- a/src/agents/tools/browser-tool.actions.ts +++ b/src/agents/tools/browser-tool.actions.ts @@ -288,11 +288,37 @@ export async function executeConsoleAction(params: { level, targetId, }, - })) as { ok?: boolean; targetId?: string; messages?: unknown[] }; - return formatConsoleToolResult(result); + })) as { ok?: boolean; targetId?: string; url?: string; messages?: unknown[] }; + const wrapped = wrapBrowserExternalJson({ + kind: "console", + payload: result, + includeWarning: false, + }); + return { + content: [{ type: "text" as const, text: wrapped.wrappedText }], + details: { + ...wrapped.safeDetails, + targetId: typeof result.targetId === "string" ? result.targetId : undefined, + url: typeof result.url === "string" ? result.url : undefined, + messageCount: Array.isArray(result.messages) ? result.messages.length : undefined, + }, + }; } const result = await browserConsoleMessages(baseUrl, { level, targetId, profile }); - return formatConsoleToolResult(result); + const wrapped = wrapBrowserExternalJson({ + kind: "console", + payload: result, + includeWarning: false, + }); + return { + content: [{ type: "text" as const, text: wrapped.wrappedText }], + details: { + ...wrapped.safeDetails, + targetId: result.targetId, + url: result.url, + messageCount: result.messages.length, + }, + }; } export async function executeActAction(params: { diff --git a/src/browser/client-actions-observe.ts b/src/browser/client-actions-observe.ts index 7f7d8cd6926..23cc21a1a37 100644 --- a/src/browser/client-actions-observe.ts +++ b/src/browser/client-actions-observe.ts @@ -25,7 +25,7 @@ function buildQuerySuffix(params: Array<[string, string | boolean | undefined]>) export async function browserConsoleMessages( baseUrl: string | undefined, opts: { level?: string; targetId?: string; profile?: string } = {}, -): Promise<{ ok: true; messages: BrowserConsoleMessage[]; targetId: string }> { +): Promise<{ ok: true; messages: BrowserConsoleMessage[]; targetId: string; url?: string }> { const suffix = buildQuerySuffix([ ["level", opts.level], ["targetId", opts.targetId], @@ -35,6 +35,7 @@ export async function browserConsoleMessages( ok: true; messages: BrowserConsoleMessage[]; targetId: string; + url?: string; }>(withBaseUrl(baseUrl, `/console${suffix}`), { timeoutMs: 20000 }); } diff --git a/src/browser/routes/agent.act.ts b/src/browser/routes/agent.act.ts index af0d8e40794..1c731d91ba2 100644 --- a/src/browser/routes/agent.act.ts +++ b/src/browser/routes/agent.act.ts @@ -626,7 +626,7 @@ export function registerBrowserAgentActRoutes( typeRequest.timeoutMs = timeoutMs; } await pw.typeViaPlaywright(typeRequest); - return res.json({ ok: true, targetId: tab.targetId }); + return res.json({ ok: true, targetId: tab.targetId, url: tab.url }); } case "press": { const key = toStringOrEmpty(body.key); @@ -656,7 +656,7 @@ export function registerBrowserAgentActRoutes( key, delayMs: delayMs ?? undefined, }); - return res.json({ ok: true, targetId: tab.targetId }); + return res.json({ ok: true, targetId: tab.targetId, url: tab.url }); } case "hover": { const ref = toStringOrEmpty(body.ref) || undefined; @@ -699,7 +699,7 @@ export function registerBrowserAgentActRoutes( selector, timeoutMs: timeoutMs ?? undefined, }); - return res.json({ ok: true, targetId: tab.targetId }); + return res.json({ ok: true, targetId: tab.targetId, url: tab.url }); } case "scrollIntoView": { const ref = toStringOrEmpty(body.ref) || undefined; @@ -750,7 +750,7 @@ export function registerBrowserAgentActRoutes( scrollRequest.timeoutMs = timeoutMs; } await pw.scrollIntoViewViaPlaywright(scrollRequest); - return res.json({ ok: true, targetId: tab.targetId }); + return res.json({ ok: true, targetId: tab.targetId, url: tab.url }); } case "drag": { const startRef = toStringOrEmpty(body.startRef) || undefined; @@ -801,7 +801,7 @@ export function registerBrowserAgentActRoutes( endSelector, timeoutMs: timeoutMs ?? undefined, }); - return res.json({ ok: true, targetId: tab.targetId }); + return res.json({ ok: true, targetId: tab.targetId, url: tab.url }); } case "select": { const ref = toStringOrEmpty(body.ref) || undefined; @@ -854,7 +854,7 @@ export function registerBrowserAgentActRoutes( values, timeoutMs: timeoutMs ?? undefined, }); - return res.json({ ok: true, targetId: tab.targetId }); + return res.json({ ok: true, targetId: tab.targetId, url: tab.url }); } case "fill": { const rawFields = Array.isArray(body.fields) ? body.fields : []; @@ -899,7 +899,7 @@ export function registerBrowserAgentActRoutes( fields, timeoutMs: timeoutMs ?? undefined, }); - return res.json({ ok: true, targetId: tab.targetId }); + return res.json({ ok: true, targetId: tab.targetId, url: tab.url }); } case "resize": { const width = toNumber(body.width); @@ -1001,7 +1001,7 @@ export function registerBrowserAgentActRoutes( fn, timeoutMs, }); - return res.json({ ok: true, targetId: tab.targetId }); + return res.json({ ok: true, targetId: tab.targetId, url: tab.url }); } case "evaluate": { if (!evaluateEnabled) { @@ -1152,7 +1152,7 @@ export function registerBrowserAgentActRoutes( timeoutMs: timeoutMs ?? undefined, maxChars: maxChars ?? undefined, }); - res.json({ ok: true, targetId: tab.targetId, response: result }); + res.json({ ok: true, targetId: tab.targetId, url: tab.url, response: result }); }, }); }); @@ -1204,7 +1204,7 @@ export function registerBrowserAgentActRoutes( targetId: tab.targetId, ref, }); - res.json({ ok: true, targetId: tab.targetId }); + res.json({ ok: true, targetId: tab.targetId, url: tab.url }); }, }); }); diff --git a/src/browser/routes/agent.debug.ts b/src/browser/routes/agent.debug.ts index f5c0d7b2030..888bdd5576a 100644 --- a/src/browser/routes/agent.debug.ts +++ b/src/browser/routes/agent.debug.ts @@ -32,7 +32,7 @@ export function registerBrowserAgentDebugRoutes( targetId: tab.targetId, level: level.trim() || undefined, }); - res.json({ ok: true, messages, targetId: tab.targetId }); + res.json({ ok: true, messages, targetId: tab.targetId, url: tab.url }); }, }); }); diff --git a/src/browser/routes/agent.shared.ts b/src/browser/routes/agent.shared.ts index cc82e00d004..97d77f1dbb9 100644 --- a/src/browser/routes/agent.shared.ts +++ b/src/browser/routes/agent.shared.ts @@ -109,6 +109,56 @@ export async function withRouteTabContext( } try { const tab = await profileCtx.ensureTabAvailable(params.targetId); + + // Enrich every successful tab-targeting response with the resolved tab's + // current page URL. This gives downstream consumers (security plugins, + // audit loggers, etc.) a consistent way to know which page was targeted + // without issuing a separate tabs query. Existing explicit values win; + // the wrapper only fills in missing fields. + // + // We attempt to resolve the *live* page URL via Playwright (which queries + // the actual browser page), falling back to the tab list URL if + // Playwright is unavailable. This corrects stale URL caches when the + // user navigates in the browser without triggering a relay metadata + // refresh (e.g. Chrome extension relay). + let liveUrl: string | undefined; + try { + const pwMod = await getPwAiModuleBase({ mode: "soft" }); + if (pwMod?.getPageForTargetId) { + const page = await pwMod.getPageForTargetId({ + cdpUrl: profileCtx.profile.cdpUrl, + targetId: tab.targetId, + }); + if (page) { + liveUrl = page.url(); + } + } + } catch { + // Playwright not available or page not found — fall back to tab.url + } + const resolvedUrl = liveUrl || tab.url; + + const originalJson = params.res.json.bind(params.res); + params.res.json = (body: unknown) => { + if ( + body && + typeof body === "object" && + !Array.isArray(body) && + (body as Record).ok === true + ) { + const record = body as Record; + if (record.targetId === undefined) { + record.targetId = tab.targetId; + } + if (resolvedUrl) { + // Always override url with live value — the route may have used a + // stale tab.url from the relay cache. + record.url = resolvedUrl; + } + } + return originalJson(body); + }; + return await params.run({ profileCtx, tab,