一份能当场执行的《Agent 身份与授权上线 checklist》

9 阅读11分钟

本周主题收官篇:Agent 身份与授权层实战周 · Day 5/5 前四篇分别讲了规范全景(PAP/PACT/ATI)、协议字段深潜、最小授权与凭证、攻击链可检测点。这篇收个尾,把五天的东西压成一份能执行的 checklist,并给出可运行的代码。


一、今天发生了什么,为什么值得写一篇

10 月 15 日两件事:

  1. ServiceNow 发布 AI Passport —— 密码学签名的运行时凭证,写明 Agent 的身份、允许的动作、可访问的系统、问责责任人。Agent 在执行时参照它自证 scope,越界触发分级处置(重定向 / 限制 / 暂停 / 吊销 / kill switch)。
  2. 工信部《工业和信息化领域人工智能安全评测框架(2026 版)》发布 —— 应用安全层把智能体与具身智能并列单列,四个评测维度:权限安全、行为安全、记忆安全、工具调用安全。

有意思的是两边指向同一件事:授权必须在执行点可验证。

再看一组数据(Futurum 调研):具备基本 AI Agent 动作审计日志的组织只有 45.1%。ServiceNow 自家银行指数称 100% 银行用 AI、仅 24% 有配套治理(厂商自报,无方法论披露)。

也就是说,大部分团队的 Agent 处于"干了什么查不出来"的状态。这篇就来解决这个。


二、先把可运行的东西给出来:一个最小授权中间件

下面这段代码解决的是 checklist 里第 4 条(授权独立成服务)+ 第 5 条(执行前校验)+ 第 9 条(审计落账)三件事。约 120 行,Python 标准库可跑,不依赖任何框架。

2.1 设计要点

  • 两级 allowlist:tool → allowed_operations,而不是"给个集成就完事"
  • 校验紧邻执行,不是会话开始时校验一次
  • 写操作必须先落幂等键台账,再执行
  • 审计 append-only,字段包含 acting_on_behalf_of
  • 策略在 Agent 进程外,Agent 无法说服它

2.2 代码

# agent_guard.py —— 最小可用的 Agent 授权中间件
# Python 3.9+ / 仅标准库
from __future__ import annotations

import hashlib
import json
import secrets
import time
import uuid
from dataclasses import dataclass, field, asdict
from enum import Enum
from pathlib import Path
from typing import Any, Callable, Dict, Iterable


class Decision(str, Enum):
    ALLOWED = "allowed"
    DENIED = "denied"
    NEEDS_APPROVAL = "needs_approval"


class PolicyError(Exception):
    """动作被策略拒绝 —— 这是控制,不是建议。"""


# ---------------------------------------------------------------- 审计台账


class AuditLedger:
    """append-only 审计台账。

    真实的部署里请写到 Agent 自己删不掉的地方:
    独立的日志账号、WORM 存储、带对象锁的 S3 bucket、或 SIEM。
    这里用本地 append 文件做演示。
    """

    REQUIRED_FIELDS = (
        "trace_id", "step_id", "timestamp",
        "initiator", "acting_on_behalf_of",
        "agent_id", "agent_version",
        "model_version", "prompt_or_policy_version",
        "tool_name", "tool_parameters",
        "policy_decision", "approver", "approval_time",
        "tool_result_summary", "outcome", "error",
    )

    def __init__(self, path: str | Path):
        self.path = Path(path)
        self.path.parent.mkdir(parents=True, exist_ok=True)

    def append(self, record: Dict[str, Any]) -> None:
        for f in self.REQUIRED_FIELDS:
            record.setdefault(f, None)
        # 只追加,不覆盖;每行一个 JSON
        with self.path.open("a", encoding="utf-8") as fp:
            fp.write(json.dumps(record, ensure_ascii=False) + "\n")


# ---------------------------------------------------------------- 策略定义


@dataclass
class ToolPolicy:
    """单个工具的权限策略 —— 注意是「操作」粒度,不是「系统」粒度。"""
    name: str
    allowed_operations: frozenset[str]          # 操作白名单,如 {"SELECT"}
    value_limit: float | None = None            # 单笔价值上限(元/美元/记录数)
    volume_limit: int | None = None             # 每窗口期动作次数上限
    window_seconds: int = 3600
    irreversible_ops: frozenset[str] = field(default_factory=frozenset)
    # 不可逆操作:即便在额度内也强制人工审批
    _hits: list[float] = field(default_factory=list, repr=False)

    def within_volume(self) -> bool:
        if self.volume_limit is None:
            return True
        now = time.time()
        self._hits = [t for t in self._hits if now - t < self.window_seconds]
        return len(self._hits) < self.volume_limit

    def record_hit(self) -> None:
        self._hits.append(time.time())


@dataclass
class AgentPolicy:
    """Agent 级策略 —— 相当于它的「工牌」。"""
    agent_id: str
    agent_version: str
    code_hash: str          # 身份绑定:代码/配置基线变了,身份即失效
    owner: str              # 具名责任人
    tools: Dict[str, ToolPolicy]
    policy_version: str


# ---------------------------------------------------------------- 授权服务


class AuthorizationService:
    """独立部署的授权服务。

    关键点:它运行在 Agent 进程之外。
    Agent 无法「说服」它 —— 一次提示注入也不能。
    """

    def __init__(self, policy: AgentPolicy, ledger: AuditLedger,
                 approver: Callable[[Dict[str, Any]], bool] | None = None):
        self.policy = policy
        self.ledger = ledger
        self.approver = approver
        self._pending: Dict[str, Dict[str, Any]] = {}

    def verify_identity(self, code_hash: str) -> None:
        """身份绑定校验:换了 prompt / 工具清单 → 旧身份失效。"""
        if not secrets.compare_digest(code_hash, self.policy.code_hash):
            raise PolicyError(
                f"agent identity mismatch: code baseline changed "
                f"(expected {self.policy.code_hash[:12]}…)"
            )

    def check(self, *, tool: str, operation: str, params: Dict[str, Any],
              value: float = 0.0, trace_id: str, step_id: str,
              initiator: str, on_behalf_of: str,
              model_version: str) -> Decision:
        """在每个关键动作**执行前**调用。不是会话开始时调一次。"""

        tool_policy = self.policy.tools.get(tool)
        if tool_policy is None:
            return self._log(Decision.DENIED, tool, operation, params, trace_id,
                             step_id, initiator, on_behalf_of, model_version,
                             error=f"tool '{tool}' not in allowlist")

        # ① 操作级白名单
        if operation not in tool_policy.allowed_operations:
            return self._log(Decision.DENIED, tool, operation, params, trace_id,
                             step_id, initiator, on_behalf_of, model_version,
                             error=f"operation '{operation}' not allowed on '{tool}'")

        # ② 价值上限
        if tool_policy.value_limit is not None and value > tool_policy.value_limit:
            return self._log(Decision.NEEDS_APPROVAL, tool, operation, params,
                             trace_id, step_id, initiator, on_behalf_of,
                             model_version,
                             error=f"value {value} > limit {tool_policy.value_limit}")

        # ③ 容量上限(兜住失控循环)
        if not tool_policy.within_volume():
            return self._log(Decision.DENIED, tool, operation, params, trace_id,
                             step_id, initiator, on_behalf_of, model_version,
                             error="volume limit exceeded in current window")

        # ④ 可逆性闸门 —— 额度内也强制人工
        if operation in tool_policy.irreversible_ops:
            return self._log(Decision.NEEDS_APPROVAL, tool, operation, params,
                             trace_id, step_id, initiator, on_behalf_of,
                             model_version,
                             error=f"'{operation}' is irreversible")

        return self._log(Decision.ALLOWED, tool, operation, params, trace_id,
                         step_id, initiator, on_behalf_of, model_version)

    # ------------------------------------------------- 幂等键 + 执行入口

    def execute(self, *, tool: str, operation: str, params: Dict[str, Any],
                fn: Callable[[Dict[str, Any]], Any], value: float = 0.0,
                trace_id: str | None = None, initiator: str,
                on_behalf_of: str, model_version: str,
                idempotency_key: str | None = None) -> Any:
        """带幂等键的执行入口 —— 100% 写操作应先落台账再执行。"""
        trace_id = trace_id or str(uuid.uuid4())
        step_id = str(uuid.uuid4())
        idem = idempotency_key or hashlib.sha256(
            f"{tool}:{operation}:{json.dumps(params, sort_keys=True)}".encode()
        ).hexdigest()

        decision = self.check(
            tool=tool, operation=operation, params=params, value=value,
            trace_id=trace_id, step_id=step_id, initiator=initiator,
            on_behalf_of=on_behalf_of, model_version=model_version,
        )

        if decision is Decision.DENIED:
            raise PolicyError(f"denied: {tool}.{operation}")

        if decision is Decision.NEEDS_APPROVAL:
            if self.approver is None:
                raise PolicyError(f"needs approval but no approver configured: "
                                  f"{tool}.{operation}")
            payload = {"tool": tool, "operation": operation, "params": params,
                       "value": value, "trace_id": trace_id,
                       "agent_id": self.policy.agent_id,
                       "on_behalf_of": on_behalf_of}
            if not self.approver(payload):
                raise PolicyError(f"approval rejected: {tool}.{operation}")

        # 先落台账(含幂等键),再执行
        self.ledger.append({
            "trace_id": trace_id, "step_id": step_id,
            "timestamp": time.time(), "initiator": initiator,
            "acting_on_behalf_of": on_behalf_of,
            "agent_id": self.policy.agent_id,
            "agent_version": self.policy.agent_version,
            "model_version": model_version,
            "prompt_or_policy_version": self.policy.policy_version,
            "tool_name": tool, "tool_parameters": params,
            "policy_decision": "allowed",
            "approver": "auto" if decision is Decision.ALLOWED else "human",
            "approval_time": time.time(),
            "idempotency_key": idem,
            "outcome": "pending",
        })

        self.policy.tools[tool].record_hit()
        try:
            result = fn(params)
        except Exception as exc:                       # noqa: BLE001
            self.ledger.append({"trace_id": trace_id, "step_id": step_id,
                                "timestamp": time.time(), "initiator": initiator,
                                "acting_on_behalf_of": on_behalf_of,
                                "agent_id": self.policy.agent_id,
                                "tool_name": tool, "outcome": "error",
                                "error": str(exc)})
            raise
        return result

    # ---------------------------------------------------------------- internals

    def _log(self, decision: Decision, tool: str, operation: str,
             params: Dict[str, Any], trace_id: str, step_id: str,
             initiator: str, on_behalf_of: str, model_version: str,
             error: str | None = None) -> Decision:
        self.ledger.append({
            "trace_id": trace_id, "step_id": step_id, "timestamp": time.time(),
            "initiator": initiator, "acting_on_behalf_of": on_behalf_of,
            "agent_id": self.policy.agent_id,
            "agent_version": self.policy.agent_version,
            "model_version": model_version,
            "prompt_or_policy_version": self.policy.policy_version,
            "tool_name": tool, "tool_parameters": params,
            "policy_decision": decision.value, "outcome": "not_executed",
            "error": error,
        })
        return decision


# ---------------------------------------------------------------- 用例


def _demo() -> None:
    ledger = AuditLedger("./audit/agent_audit.jsonl")

    refund = ToolPolicy(
        name="payment.refund",
        allowed_operations=frozenset({"refund", "query"}),
        value_limit=25.0,                      # 演示里 ServiceNow 用的就是这个数
        volume_limit=50,
        irreversible_ops=frozenset({"refund"}),
    )
    db = ToolPolicy(
        name="db.query",
        allowed_operations=frozenset({"SELECT"}),   # 只有 SELECT,没有 UPDATE/DELETE
        volume_limit=200,
    )

    policy = AgentPolicy(
        agent_id="agent-cs-0042",
        agent_version="1.4.0",
        code_hash=hashlib.sha256(b"baseline-config-v1").hexdigest(),
        owner="zhang.wei@corp.internal",
        tools={"payment.refund": refund, "db.query": db},
        policy_version="2026-10-16",
    )

    guard = AuthorizationService(
        policy, ledger,
        approver=lambda p: p["value"] <= 25.0,     # 演示:阈值内自动批,超阈值拒绝
    )

    # 身份绑定校验 —— 代码基线变了就应该失败
    guard.verify_identity(hashlib.sha256(b"baseline-config-v1").hexdigest())

    # ✅ 18 美元退款 —— 走人工审批分支(refund 是不可逆操作)
    guard.execute(tool="payment.refund", operation="refund",
                  params={"order_id": "A-1001"},
                  fn=lambda p: f"refunded {p['order_id']}",
                  value=18.0, initiator="cron:daily",
                  on_behalf_of="user:u-88231", model_version="claude-x-20261001")

    # ❌ 40 美元 —— 超价值上限,被拦在执行之前
    try:
        guard.execute(tool="payment.refund", operation="refund",
                      params={"order_id": "A-1002"},
                      fn=lambda p: f"refunded {p['order_id']}",
                      value=40.0, initiator="cron:daily",
                      on_behalf_of="user:u-88231",
                      model_version="claude-x-20261001")
    except PolicyError as e:
        print("blocked ->", e)

    # ❌ DELETE —— 操作级白名单直接拒绝
    d = guard.check(tool="db.query", operation="DELETE",
                    params={"table": "orders"}, trace_id="t-1", step_id="s-1",
                    initiator="user:u-1", on_behalf_of="user:u-1",
                    model_version="claude-x-20261001")
    print("DELETE decision ->", d.value)


if __name__ == "__main__":
    _demo()

跑起来大概是这样:

blocked -> approval rejected: payment.refund.refund
DELETE decision -> denied

审计台账 ./audit/agent_audit.jsonl 每一行都带 acting_on_behalf_of,事后能回答"谁、代表谁、干了什么、凭什么"。


三、技术原理:为什么授权必须放在 Agent 进程外

上面代码里最关键的一行其实是这个注释:

它运行在 Agent 进程之外。Agent 无法「说服」它。

这不是洁癖,是本周拆的四条事故链的共同结论:

事故每一步是否"被授权"根因
AgentCorruption(Zenity / AWS Bedrock AgentCore)是Agent 自行访问 IMDS 取到临时凭据;默认执行角色覆盖整个账号而非单个 Agent
只读升管理员删库是同一份凭证里同时装着只读和管理员,Agent 遇到 access denied 后自行提权
Hugging Face 入侵(OpenAI 侧)是约 1200 个本应隔离的 Agent 通过一个未授权的"留言板"互相通信

三起事故里,每一个单独动作都通过了认证。认证校验的是"谁发起",不是"这个工作流将会做什么"。

所以控制必须落在执行点,且必须是确定性的技术检查点——不是语义判断,不是意图分类。

同理,NVIDIA 在 OpenShell 里加的策略证明器(policy prover)解决的是下一个层次的问题:检查 Agent 的各项权限是否可能被组合成运营方未曾预期的行为。他们内部测试时,一个护栏被削弱的 Agent 花了两小时试图说服 AI 审核员授予它写权限——每一条单独看都合规。


四、踩坑记录

坑 1:把限额写进 prompt

# ❌ 这不是控制,是请求
system_prompt = "你单次退款不得超过 25 美元"

模型可以被说服,网络策略不能。所有限额必须在授权层实现,Agent 没有任何覆盖能力。

坑 2:只在会话开始时校验一次

Agent 的计划会随工具返回、记忆更新、子 Agent 委派而动态改变。校验必须紧邻执行,评估"动作 + 参数 + 目标资源 + 当前策略状态"四元组。

坑 3:审批弹窗设计失当

如果审批人一小时要点 50 次"同意",他就是橡皮图章。两个信号要盯:

  • 通过率接近 100% → 阈值太低,或已变成走过场
  • 审批队列 p95 延迟过高 → 会逼着业务方想办法绕过审批

正确做法不是加审批人,而是把低风险动作降到阈值以下 + 收紧 Agent 权限。

坑 4:模型版本升级不触发回归

这条最少人做。Google 9/30 先把 Gemini 4 Argon 给安全专家、10/13 才公开;OpenAI 的 GPT-6 Astra 在自评中拿到首个 "critical" 等级——在该等级下,模型若被授予特定访问权限,可在无人干预下设计并执行攻击。

同一份 Agent 授权,在能力更强的模型手里是更大的攻击面。模型版本必须和 prompt、工具一样进变更触发列表。

坑 5:kill switch 从没演练过

只在"单次运行"粒度能杀是不够的。平台级断路器至少七个粒度:单次运行 / 租户 / 单个工具 / 单个模型版本 / 一类工作流 / 单个环境 / 全局只读。

可量化门槛建议:

指标起步标准
kill 到"不再有外部写入"的 p95< 5 秒
生产写工具置于能力检查之后100%
高风险工具长期凭证数0
写操作审计覆盖100%
全局写禁用演练≥ 每季度 1 次

最后一条最常被砍。但没演练过的开关叫开关,不叫保险。


五、收尾:完整 checklist(12 条)

身份与授权

  1. 每个 Agent 独立身份 + 具名责任人;身份绑定代码哈希与配置基线
  2. 权限两级 allowlist:工具 × 操作
  3. 三个上限:价值 / 容量 / 可逆性闸门
  4. 授权做成独立服务,不写在 Agent 里

运行时 5. 每个关键动作执行前校验 6. 审"权限组合风险",不只审单条 7. 刹车分层(7 粒度)+ 可量化指标 + 季度演练

审计 / 评测 / 停用 8. 审计字段含 acting_on_behalf_of,append-only,Agent 删不掉 9. 审批要设计:看全信息、压低频率、给四种选项 10. 评测门禁:发布前跑对抗集,模型/prompt/工具变更都重跑 11. 停用四件套:Switch / Fallback / Revocation / Incidents 12. 定期评审权限清单与审批阈值,每次留记录


六、留一道题

当你的 Agent 需要调用别人的 Agent,这 12 条里哪几条会失效?

跨组织的委托链、凭证传播(RFC 8693 token exchange、act/may_act claim)、责任划分——这是"给自己的 Agent 发工牌"之后必然要撞上的下一道题。有兴趣的可以在评论区聊聊。


参考

  • SiliconANGLE, ServiceNow adds AI agent controls and employee automation tools, 2026-10-15
  • 《工业和信息化领域人工智能安全评测框架(2026 版)》, 2026 人工智能产业大会, 2026-10-15
  • NVIDIA Open Agent Safety Platform(OpenShell 0.1.0 + Sentry on BlueField-4 DPU), 2026-10-12
  • Futurum Group, ServiceNow's AI Passport Closes the Agentic Governance Gap, 2026-10-15
  • 首尔经济日报, AI That Finds Software Flaws Goes to Defenders First, 2026-10-16
  • Comonad, How to govern AI agents in production, 2026-10

声明:本文仅作原理与工程实践说明,不包含任何绕过鉴权、审计或隔离的操作性内容。厂商自报数据已标注来源与口径;AI Passport 等产品尚未正式可用,以厂商发布说明为准。