凌晨 2 点,发布任务读到 approved=true,把一篇活动稿发了出去。
审计记录显示负责人确实点过同意。但她批准后,正文被改过,封面被替换过,目标账号也从测试号切到了正式号。布尔值没有撒谎,只是它从来没说清楚:那次同意究竟针对哪个版本。
GitHub 最近给 Copilot code review 增加了 PR approval。官方设计里有两个很值得借鉴的细节:
- Copilot 的 approval assessment 只是“建议可以批准”,本身不计入合并要求;
- 真正 approval 默认关闭,启用后如果又推送新提交,原批准会失效并要求重新评审。
这其实在提醒我们:模型判断、权限授权和外部执行,是三种不同能力。

“通过”至少有三种身份
先别急着设计审批表,先问谁在说“通过”。
type ReviewDecision =
| { kind: "assessment"; by: "agent"; result: "pass" | "fail" | "uncertain" }
| { kind: "authorization"; by: Principal; action: ActionScope }
| { kind: "execution"; by: Executor; result: ExecutionResult };
Assessment 表示当前证据下的专业判断。Authorization 表示某个有权限的主体允许执行特定动作。Execution 则记录动作是否真的发生,以及平台返回了什么。
把它们合并成 approved 会产生两个越权:
- Agent 判断“看起来没问题”,被系统误当成“有权批准”;
- 人批准了版本 A,执行器却把授权用于版本 B。
Approval 更像租约,不像勋章
一个批准对象应该是短期、可验证、可失效的:
interface ApprovalLease {
id: string;
subjectId: string;
subjectVersion: string;
evidenceHash: string;
policyVersion: string;
approverId: string;
action: {
name: "publish" | "pay" | "deploy" | "send";
target: string;
constraints: Record<string, unknown>;
};
issuedAt: string;
expiresAt: string;
}
租约的意思不是一定要 30 分钟过期,而是授权只能在约定条件下成立。一旦对象、证据、策略、账户或动作范围变化,它就不再有效。
版本应该包含什么
对代码来说,subjectVersion 可以是 commit SHA。对内容发布,可以是下面这份 manifest 的哈希:
{
"platform": "juejin",
"accountId": "writer-001",
"titleHash": "sha256:...",
"bodyHash": "sha256:...",
"images": [
{ "role": "cover", "sha256": "..." },
{ "role": "body", "sha256": "..." }
],
"tags": ["人工智能"],
"visibility": "public"
}
注意不要直接哈希一段随意序列化的 JSON。标签顺序、换行风格和对象属性顺序可能制造假变更。先 canonicalize,再计算摘要。
function contentVersion(m: PublishManifest): string {
return sha256(stableJson({
platform: m.platform,
accountId: m.accountId,
title: m.title.trim(),
body: m.body.replace(/\r\n/g, "\n").trimEnd(),
images: [...m.images].sort(byRole).map(i => [i.role, i.sha256]),
tags: [...m.tags].sort(),
visibility: m.visibility,
}));
}
不要签发一张“全平台通行证”
权限范围也必须进入租约。
批准“把文章 X 发布到掘金账号 A”,不能自然扩展为:
- 同步到 CSDN;
- 换成另一个账号;
- 把私密草稿改成公开;
- 修改正文后继续发布;
- 自动开启平台自己的多端分发。
GitHub 允许管理员用文件路径限制 Copilot approval 是否计入合并要求,背后的思路相同:授权的价值来自边界,而不是来自一句抽象的“允许”。
可以定义一份简单策略:
policy: content-publish-v4
rules:
- risk: low
action: prefill
approval: none
- risk: medium
action: publish
approval: content-owner
invalidateOn: [content, assets, account, visibility]
- risk: high
action: cross-platform-publish
approval: content-owner-and-admin
autoRenew: false
预填和正式发布不是同一个权限。让 Agent 填好编辑器,不代表它可以忽略页面状态直接提交。
执行前重新证明“还是那一版”
审批完成到执行之间可能隔几秒,也可能隔几小时。执行器必须重新读取真实目标:
async function executeWithLease(lease: ApprovalLease) {
const current = await captureCurrentSubject();
if (current.version !== lease.subjectVersion) {
throw new DomainError("APPROVAL_SUBJECT_CHANGED");
}
if (current.evidenceHash !== lease.evidenceHash) {
throw new DomainError("APPROVAL_EVIDENCE_CHANGED");
}
if (current.policyVersion !== lease.policyVersion) {
throw new DomainError("APPROVAL_POLICY_CHANGED");
}
if (Date.now() >= Date.parse(lease.expiresAt)) {
throw new DomainError("APPROVAL_EXPIRED");
}
const claimed = await claimOnce(lease.id, current.version);
if (!claimed) throw new DomainError("APPROVAL_ALREADY_CONSUMED");
return performAndVerify(lease.action);
}
claimOnce 要做条件更新或唯一约束,避免两个定时任务同时消费一份批准。performAndVerify 则必须回读真实结果。点击按钮不是成功证据;如果平台没有明确状态,就停在 RESULT_UNKNOWN,只做只读查询,不再次点击。

一个够用的状态机
ASSESSED
└─> WAITING_APPROVAL
└─> APPROVED
├─> EXECUTING ─> SUCCEEDED
│ ├─> FAILED
│ └─> RESULT_UNKNOWN
└─> STALE_APPROVAL ─> WAITING_APPROVAL
会触发 STALE_APPROVAL 的事件至少包括:
- 对象内容或素材变了;
- 目标账号或收件人变了;
- 可见范围、金额、环境变了;
- 权限策略升级或收紧;
- 租约超过有效期;
- 关键证据无法重新取得。
格式空格、同哈希文件重新上传等确定性等价变化,可以由规则重新计算并留痕。无法确定是否等价时,不要再调用一个模型“猜它应该没关系”。
审计日志要能回答四个问题
发生争议时,日志至少要回答:
- 批准的是哪个对象版本?
- 批准者当时看到了哪些证据?
- 使用的是哪一版权限策略?
- 最终执行了什么动作,平台如何证明结果?
因此不要只记录:
{ "task": "publish-42", "approved": true }
至少记录 subjectVersion、evidenceHash、policyVersion、approverId、actionScope 和结果证据。
我们在 Tipkay 里的对应取舍
这也是我们做 Tipkay 博客发布助手时坚持的一条边界:研究、写作、配图和平台预填可以由岗位助手接力完成,但最终提交前仍要回读真实页面,确认账号、标题、正文、图片、标签、分类和可见范围。
工具调用成功只说明“尝试填过”,不说明页面里的当前版本已经满足交付条件。把页面状态变成证据,再让授权落到这份证据上,才是一条可追责的交付链。
Tipkay 面向小微企业、一人公司和小团队,提供按实际使用量计费的垂类 AI 员工。我们更关心的不是堆多少 Agent,而是每个岗位能否接手一类工作,并在交接处留下清楚的责任边界。
上线前测这五个反例
- 批准后修改标题,旧租约是否立即失效;
- 批准后更换账号,执行器是否阻断;
- 两个 worker 同时领取,是否只有一个成功;
- 点击提交后网络超时,是否进入 RESULT_UNKNOWN 而非自动重试;
- 权限策略升级后,旧租约是否需要重新授权。
这五个反例能过,再谈“自动审批”。否则 Agent 的“同意”,很可能只是一张没有写版本号的空白支票。
参考资料:
- GitHub Changelog, Copilot code review can now approve pull requests: github.blog/changelog/2…
- GitHub Docs, Configuring code review by GitHub Copilot: docs.github.com/en/copilot/…
- GitHub Docs, Available rules for rulesets: docs.github.com/en/reposito…
- GitHub Docs, REST API endpoints for pull request reviews: docs.github.com/en/rest/pul…