fix(agent): solve_challenge authorization covers its child's coordinate steps
With only a browser.solve_challenge grant, the vision child run was denied browser.click_at, so the loop stopped after the first screenshot (REAL-2 task 4). Steps inside a run spawned by solve_challenge are now allowed; ancestors' allowlist and step budget still apply. Needs image rebuild. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
bd0e011748
commit
7dc7b1c5fa
3 files changed
+38
-1
No files matched your search
@@ -29,6 +29,9 @@ export const GRANT_ACTIONS: Record<string, readonly string[]> = {
|
|||||||
"browser.solve_challenge": ["browser.solve_challenge", "browser.interact"],
|
"browser.solve_challenge": ["browser.solve_challenge", "browser.interact"],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 视觉子流程内由 solve_challenge 授权覆盖的执行类坐标动作 */
|
||||||
|
const CHALLENGE_STEP_CAPS = new Set(["browser.click_at", "browser.type_at", "browser.scroll"]);
|
||||||
|
|
||||||
/** 单次审批绑定的计划类型与输入里的计划 ID 字段 */
|
/** 单次审批绑定的计划类型与输入里的计划 ID 字段 */
|
||||||
const APPROVAL_PLAN: Record<string, { planType: string; field: string }> = {
|
const APPROVAL_PLAN: Record<string, { planType: string; field: string }> = {
|
||||||
"download.submit": { planType: "acquisition_plan", field: "planId" },
|
"download.submit": { planType: "acquisition_plan", field: "planId" },
|
||||||
@@ -215,6 +218,13 @@ export function createAuthorizer(deps: {
|
|||||||
if (block) return { allowed: false, reason: block };
|
if (block) return { allowed: false, reason: block };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 视觉子流程(spawn_reason = browser.solve_challenge)里的坐标动作:父会话那次 solve_challenge 已过授权,
|
||||||
|
// 步数 / 预算 / 白名单由子流程约束,这里随之放行(否则只授权 solve_challenge 时子流程一步也点不了)
|
||||||
|
if (runId && CHALLENGE_STEP_CAPS.has(d.id)) {
|
||||||
|
const r = await db.selectFrom("agent_run").select("spawn_reason").where("id", "=", runId).executeTakeFirst();
|
||||||
|
if (r?.spawn_reason === "browser.solve_challenge") return { allowed: true };
|
||||||
|
}
|
||||||
|
|
||||||
// 1) 单次审批
|
// 1) 单次审批
|
||||||
const ap = APPROVAL_PLAN[d.id];
|
const ap = APPROVAL_PLAN[d.id];
|
||||||
const approvalId = typeof inp.approvalId === "string" ? inp.approvalId : null;
|
const approvalId = typeof inp.approvalId === "string" ? inp.approvalId : null;
|
||||||
|
|||||||
@@ -226,4 +226,31 @@ describe.skipIf(!hasPg)("单次审批(真实 PG)", () => {
|
|||||||
expect(denied.rows[0]!.n).toBeGreaterThanOrEqual(2);
|
expect(denied.rows[0]!.n).toBeGreaterThanOrEqual(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("视觉子流程里的坐标点击随 solve_challenge 放行;普通会话仍要授权", async () => {
|
||||||
|
const spec = { ...ORGANIZE_SPEC, id: "browser.click_at", scope: "admin" } as CapabilitySpec;
|
||||||
|
const run = async (spawn_reason: string | null) =>
|
||||||
|
(
|
||||||
|
await h.db
|
||||||
|
.insertInto("agent_run")
|
||||||
|
.values({ trigger: "user", goal: "g", status: "running", mode: "execute", spawn_reason } as never)
|
||||||
|
.returning("id")
|
||||||
|
.executeTakeFirstOrThrow()
|
||||||
|
).id;
|
||||||
|
const check = async (agentRunId: string) =>
|
||||||
|
await createAuthorizer({ db: h.db, settings: h.ctx.settings })({
|
||||||
|
descriptor: spec,
|
||||||
|
input: { sessionId: "s", x: 1, y: 1 },
|
||||||
|
ctx: {
|
||||||
|
actorId: "user:admin",
|
||||||
|
taskId: "t",
|
||||||
|
scopeIds: [],
|
||||||
|
signal: AbortSignal.timeout(10_000),
|
||||||
|
source: "agent",
|
||||||
|
agentRunId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(await check(await run("browser.solve_challenge"))).toEqual({ allowed: true });
|
||||||
|
expect(await check(await run(null))).toMatchObject({ allowed: false, reason: /browser.click_at/ });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -136,7 +136,7 @@ execute 类需要持续授权(`automation_grant`)或审批。元素引用只来
|
|||||||
- **证据**:每次截图(含动作后的截图)写 480 像素宽的 JPEG 缩略图到 `DATA_DIR/tmp/evidence/<id>.jpg`(0600,7 天后清理);
|
- **证据**:每次截图(含动作后的截图)写 480 像素宽的 JPEG 缩略图到 `DATA_DIR/tmp/evidence/<id>.jpg`(0600,7 天后清理);
|
||||||
能力输出只给引用 `evidence/<id>.jpg`,随工具结果摘要进 `agent_run_event`。图片数据不进 JSON 文本、不进操作账本。
|
能力输出只给引用 `evidence/<id>.jpg`,随工具结果摘要进 `agent_run_event`。图片数据不进 JSON 文本、不进操作账本。
|
||||||
- **授权**:`click_at / type_at / scroll / solve_challenge` 由 `browser.interact` 的持续授权覆盖(也可单独授权);
|
- **授权**:`click_at / type_at / scroll / solve_challenge` 由 `browser.interact` 的持续授权覆盖(也可单独授权);
|
||||||
父会话白名单里有 `browser.solve_challenge` 即覆盖子流程用到的截图与坐标能力。坐标动作按调用区分幂等键,崩溃恢复不重放。
|
父会话白名单里有 `browser.solve_challenge` 即覆盖子流程用到的截图与坐标能力;授权同理,solve_challenge 通过授权后,子流程(`spawn_reason = browser.solve_challenge`)内的坐标动作不再单独要持续授权。坐标动作按调用区分幂等键,崩溃恢复不重放。
|
||||||
- 交给模型的截图参数可在 `browser_provider.config` 调:`screenshotMaxWidth`(≤1280)、`screenshotFormat`(`jpeg` / `png`)、
|
- 交给模型的截图参数可在 `browser_provider.config` 调:`screenshotMaxWidth`(≤1280)、`screenshotFormat`(`jpeg` / `png`)、
|
||||||
`screenshotQuality`(20–95)。
|
`screenshotQuality`(20–95)。
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user