feat(vom): update vom algorithm, fix overlay problem with background state machine

This commit is contained in:
Ljy-0827
2026-07-27 13:46:33 +08:00
parent e14ae9cea0
commit 4fb2e331a6
36 changed files with 2276 additions and 452 deletions
+115 -3
View File
@@ -32,6 +32,13 @@ export const chromeTabsApi: ChromeTabsApi = {
query: (q) => chrome.tabs.query(q),
};
export interface ResolvedTargetTab {
tabId: number;
windowId: number;
active: boolean;
url?: string;
}
export type { DialogCursor };
/**
@@ -44,6 +51,7 @@ export type { DialogCursor };
*/
export interface CdpRunner {
send<T = unknown>(tabId: number, method: string, params?: object): Promise<T>;
ensureAttachedToUrl?(tabId: number, expectedUrl: string | undefined): Promise<void>;
trackSessionTab?(sessionId: string, tabId: number): void;
onEvent?(handler: (source: chrome.debugger.Debuggee, method: string, params: unknown) => void): {
dispose(): void;
@@ -146,7 +154,7 @@ export async function resolveTargetTab(
ctx: SessionContext,
tabId: number | undefined,
api: ChromeTabsApi,
): Promise<{ tabId: number; windowId: number; active: boolean } | RpcError> {
): Promise<ResolvedTargetTab | RpcError> {
if (tabId !== undefined) {
if (!Number.isSafeInteger(tabId) || tabId <= 0) {
return {
@@ -176,7 +184,7 @@ export async function resolveTargetTab(
message: `tab ${tabId} not found in session scope`,
};
}
return { tabId: tab.id, windowId: tab.windowId, active: tab.active === true };
return { tabId: tab.id, windowId: tab.windowId, active: tab.active === true, url: tab.url };
}
const tabs = await api.query({ active: true, windowId: ctx.agentWindowId });
const first = tabs.find((t) => typeof t.id === "number");
@@ -186,7 +194,12 @@ export async function resolveTargetTab(
message: `no active tab in Agent Window ${ctx.agentWindowId}`,
};
}
return { tabId: first.id, windowId: ctx.agentWindowId, active: first.active === true };
return {
tabId: first.id,
windowId: ctx.agentWindowId,
active: first.active === true,
url: first.url,
};
}
export function isRpcError(v: unknown): v is RpcError {
@@ -202,6 +215,91 @@ export function isRpcError(v: unknown): v is RpcError {
/** Re-export so M6/M7 tools keep a stable import path. */
export { normaliseRef };
export type ToolEffect = "passive_read" | "transient_input" | "browser_mutation";
const CDP_BLOCKED_PROTOCOLS = new Set([
"chrome:",
"chrome-extension:",
"devtools:",
"edge:",
"brave:",
"vivaldi:",
"opera:",
]);
export function cdpBlockedUrlReason(url: string | undefined): string | null {
if (!url) return null;
let parsed: URL;
try {
parsed = new URL(url);
} catch {
return null;
}
if (CDP_BLOCKED_PROTOCOLS.has(parsed.protocol)) return parsed.protocol;
if (parsed.protocol === "about:" && parsed.pathname !== "blank") return "about:";
return null;
}
/**
* Page CDP tools cannot inspect browser/extension internal pages. Check this
* at the tool boundary so Chrome's low-level "Cannot access..." errors do not
* leak as ambiguous page-read failures.
*/
export function enforceCdpAccessibleTarget(
target: ResolvedTargetTab,
toolName: string,
): RpcError | null {
const reason = cdpBlockedUrlReason(target.url);
if (!reason) return null;
return rpcError(
"permission_denied",
"restricted_tab_url",
`${toolName} cannot access tab ${target.tabId} because its URL is ${target.url}; navigate the Agent Window to a web page first`,
);
}
function resolvedTargetFromChromeTab(
tab: chrome.tabs.Tab,
fallbackWindowId: number,
): ResolvedTargetTab | null {
if (typeof tab.id !== "number") return null;
return {
tabId: tab.id,
windowId: typeof tab.windowId === "number" ? tab.windowId : fallbackWindowId,
active: tab.active === true,
url: tab.url,
};
}
/**
* Resolve a target for page CDP reads. Explicit tab_ids stay exact and are
* rejected when browser security blocks the URL. For default targeting, skip a
* restricted Agent Window active tab if another CDP-accessible tab exists in
* the same Agent Window.
*/
export async function resolveCdpAccessibleTargetTab(
manager: SessionManager,
ctx: SessionContext,
tabId: number | undefined,
api: ChromeTabsApi,
toolName: string,
): Promise<ResolvedTargetTab | RpcError> {
const target = await resolveTargetTab(manager, ctx, tabId, api);
if (isRpcError(target)) return target;
const restricted = enforceCdpAccessibleTarget(target, toolName);
if (!restricted) return target;
if (tabId !== undefined) return restricted;
const tabs = await api.query({ windowId: ctx.agentWindowId });
for (const tab of tabs) {
const candidate = resolvedTargetFromChromeTab(tab, ctx.agentWindowId);
if (!candidate) continue;
if (!enforceCdpAccessibleTarget(candidate, toolName)) return candidate;
}
return restricted;
}
/**
* Sandbox guard: M7 write tools (click / fill / press / navigate*)
* MUST refuse to touch a tab outside the session's Agent Window
@@ -224,3 +322,17 @@ export function enforceAgentWindow(
}
return null;
}
/**
* Unified target-scope policy by tool effect. Passive reads may inspect user
* tabs; any tool that dispatches page input must stay inside the Agent Window.
*/
export function enforceToolTargetScope(
ctx: SessionContext,
target: { tabId: number; windowId: number },
effect: ToolEffect,
toolName: string,
): RpcError | null {
if (effect === "passive_read") return null;
return enforceAgentWindow(ctx, target, toolName);
}