feat(agent): 选定方案即批准,服务端绑定计划(AGUI-3)

- agent.propose_options 的方案可带 action(capabilityId + planId)。提出方案时服务端按计划当前的
  payload hash + revision 绑定,并用计划本身生成动作要点(对象、大小 / 文件数)发给界面,不用模型的说法。
- 控制台管理员会话选定绑定了计划的方案时,建一份 scope.via=option 的已批准单次审批(审计 approval.approved);
  执行时授权器 optionApproval 核对能力、计划类型 + ID、payload hash、revision、有效期后放行,用后 consumed,
  不再出第二张审批卡。API Key 作答、超时自动选、计划已变化都不算批准;移动 / 替换文件的整理与发布策略
  (删除、覆盖类)选定后仍单独确认。会话停止时撤销没用掉的这类审批。
- contracts:CAPABILITY_ACTIVITY 给每个能力加 verb / noun(会话进度行的人话计数);agent.plan /
  agent.propose_options 标为 hidden。
- e12 补测试:管理员选定 → 无 approval_requested、审批绑定同一计划并 consumed、换 hash 不放行;
  API Key 选定 → 仍出审批卡。

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-29 22:40:57 +08:00
1 parent c9c6505d71
commit 4e0e6712da
8 files changed
+507 -33

No files matched your search

+15 -5
View File
@@ -220,15 +220,25 @@ export function registerAgentRoutes(
runToApi(await appendMessage(ctx, params.id, body.text, actorId).catch(mapErr)),
);
route(app, Api.agentRuns.choose, async ({ params, body, actorId }) => {
route(app, Api.agentRuns.choose, async ({ params, body, actorId, c }) => {
const run = await db().selectFrom("agent_run").selectAll().where("id", "=", params.id).executeTakeFirst();
if (!run) throw new ApiException("not_found", "智能体运行不存在");
if (body.optionId === null && !body.reason?.trim())
throw new ApiException("validation_failed", "都不要时写一句原因,智能体按它调整方案");
const r = await answerOptions(ctx, run.id, body, actorId);
if (r === "not_found") throw new ApiException("not_found", "这个会话没有这项方案选择");
if (r === "bad_option") throw new ApiException("validation_failed", "没有这个方案");
if (r === "closed") throw new ApiException("conflict", "这项方案选择已处理");
// 选定即批准只认控制台管理员会话(与 PATCH /approvals 同一口径);API Key 作答只选方案
const r = await answerOptions(ctx, run.id, body, actorId, {
approver: c.get("principal")?.kind === "admin_session",
});
if (r.status === "not_found") throw new ApiException("not_found", "这个会话没有这项方案选择");
if (r.status === "bad_option") throw new ApiException("validation_failed", "没有这个方案");
if (r.status === "closed") throw new ApiException("conflict", "这项方案选择已处理");
if (r.status === "ok" && r.approvalId)
await audit(ctx, actorId, "approval.approved", r.approvalId, {
via: "option",
runId: run.id,
decisionId: body.decisionId,
optionId: body.optionId,
});
const fresh = await db().selectFrom("agent_run").selectAll().where("id", "=", run.id).executeTakeFirstOrThrow();
return runToApi(fresh);
});