feat(browser): vision automation loop with screenshots, coordinate actions and bounded challenge solving

- browser.screenshot returns the page as an image content block (≤1280px wide, JPEG quality
  configurable) for vision-capable upstreams; non-vision upstreams get a redacted text snapshot.
  Login pages / password fields are marked sensitive and withheld unless a
  browser.sensitive_screenshot grant exists and allowSensitive is passed.
- browser.click_at / type_at / scroll / wait act in screenshot pixels; the server converts via
  viewport/DPR, requires automation control and an allowlisted host, and writes a thumbnail
  evidence file (DATA_DIR/tmp/evidence) referenced from agent_run_event.
- browser.solve_challenge runs a bounded child agent_run (vision caps only, maxSteps ≤ 12,
  budget), judges success from page facts, stops immediately on 2FA / SMS / device confirmation,
  and otherwise moves the session to waiting with evidence and a takeover link.
- PiAgentRuntime passes modelImage outputs as pi-ai image content when model.input has image;
  the ledger never stores image data; coordinate actions use per-invocation idempotency keys.
- Tests: image codec, runtime image content, E14 e2e (login solved, 2FA stop, step limit,
  sensitive withheld). Real camofox 1.17.0 check on example.com recorded in the runbook.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-29 01:27:48 +08:00
1 parent 0765264a47
commit c437c6bff6
34 files changed
+2194 -35

No files matched your search

+11 -4
View File
@@ -25,6 +25,7 @@ import {
import { type AgentRunRow, type DownloaderInstanceRow, sql } from "@umt/db";
import { type RunUpstream, resolveRunUpstream } from "../pi/index.ts";
import type { ServerContext } from "../types.ts";
import { PER_INVOCATION } from "./capability-registry.ts";
import { DEMO_SCRIPTS, parseFakeScripts } from "./fake-scripts.ts";
import { canAskUser, canRequestApproval, sessionOptions } from "./interaction.ts";
import { buildGoalReport } from "./report.ts";
@@ -125,7 +126,12 @@ export interface DriveResult {
* 设置 → @umt/agent 的运行参数。按上游运行时(W7-A)凭据由上游的 CredentialStore 在请求时解密注入,这里不取明文;
* 只走上游:v0.1 的旧字段(pi.provider / model / apiKeySecretRef)已弃用,不再读取(W8-A)。
*/
function agentSettings(s: GlobalSettings, budget: AgentBudget, fake: boolean, upstream: RunUpstream): AgentSettings {
export function agentSettings(
s: GlobalSettings,
budget: AgentBudget,
fake: boolean,
upstream: RunUpstream,
): AgentSettings {
const pi = s.pi;
const maxTokens = budget.maxTokens ?? s.agent.maxTokensPerRun ?? undefined;
const maxCost = budget.maxCostUsd ?? s.agent.maxCostUsdPerRun ?? undefined;
@@ -154,7 +160,7 @@ function agentSettings(s: GlobalSettings, budget: AgentBudget, fake: boolean, up
}
/** 事件:先落库(run_id, seq 唯一)再推全局 SSE;未授权的工具失败在总线上标为 denied */
function eventSink(env: AgentEnv) {
export function eventSink(env: AgentEnv) {
const { ctx } = env;
return async (e: AgentRunEvent) => {
const row = agentRunEventToRow(e);
@@ -226,7 +232,8 @@ export async function reconcilePending(
.where("status", "=", "running")
.orderBy("created_at")
.execute();
for (const r of rows) {
// 页面交互(坐标点击 / 输入 / 视觉子流程)崩溃后不重放:页面状态未知,由模型重新截图判断
for (const r of rows.filter((x) => !PER_INVOCATION.has(x.capability_id))) {
try {
await registry.invoke(r.capability_id, r.input, {
actorId: r.actor_id,
@@ -385,7 +392,7 @@ export async function drive(
}
}
const runtime = fake
? createFakeRuntime({ registry, scripts, onEvent, onEventError }).runtime
? createFakeRuntime({ registry, scripts, onEvent, onEventError, fake: { vision: true } }).runtime
: new PiAgentRuntime({
registry,
onEvent,