为任务型 Agent 建立可持续验证的访问模型

2026-08-05 49 预计阅读时间: 1 分钟
来源: blog.cloudflare.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:9 分钟

当 Agent 不再只是回答问题,而是代表用户读取数据、调用 API、修改资源时,传统的“登录一次、授权一段时间”就不够用了。The Agent Access Model 提出了一种面向任务范围的安全架构:由严格的身份代理负责确认“谁在执行”,由持续中介负责确认“此刻是否允许”,再通过有状态的信任记录判断“这个任务是否仍然值得信任”。

这套思路的重点不是给 Agent 一个更强的长期 Token,而是把访问权限绑定到具体任务、具体资源和当前上下文。

三个关键边界

1. 身份代理:Agent 不直接继承用户身份

Agent 通常需要代表用户完成工作,但不应直接持有用户的密码、长期访问令牌或完整会话。更稳妥的做法是引入身份代理层:

  • 用户登录和多因素认证由身份系统完成;
  • Agent 通过任务请求申请一个短期、范围明确的执行身份;
  • 身份代理验证用户、Agent、任务和目标资源之间的关系;
  • 下游服务只接受代理签发的、面向任务的凭证。

例如,一个“整理本周销售报告”的任务可以只获得读取销售数据库和写入指定报告目录的权限,而不能访问全部客户数据,也不能删除历史报告。

2. 持续中介:每次操作都重新判断

一次授权不应该自动等同于整个任务生命周期内的无限授权。持续中介层可以在 Agent 每次调用工具、API 或数据服务时检查:

  • 当前任务是否仍然有效;
  • 请求的资源是否属于任务范围;
  • 操作类型是否被允许;
  • 用户会话、设备和风险状态是否发生变化;
  • Agent 是否超出调用次数、时间或数据量限制。

这意味着权限检查应该放在工具调用路径上,而不是只放在 Agent 初始化阶段。即使 Agent 的推理过程被提示注入影响,后端仍然可以拒绝越界操作。

3. 有状态信任:信任会随任务变化

Agent 的信任不是一个永久布尔值。它应该是一条随着事件变化的状态记录。成功完成低风险读取操作,可能提高任务的可信度;出现异常参数、访问不相关资源或连续失败,则应降低信任度,触发重新认证、人工审批或任务终止。

可以把任务状态设计成 activestep_up_requiredsuspendedrevoked 等明确状态。状态变化必须可审计,并且由服务端保存,不能只依赖 Agent 自己传回的字段。

一个可落地的任务凭证模型

下面的 YAML 是一个简化的实践示例,展示如何把身份、任务范围和限制放在同一个可验证对象中。字段和签名格式需要根据实际身份平台调整,这里不代表某个特定协议的标准格式。

agent_id: report-agent
subject: user-123
job_id: job-2025-03-08-001
issued_at: 2025-03-08T09:00:00Z
expires_at: 2025-03-08T09:15:00Z
allowed_actions:
  - read:sales.weekly
  - write:reports/job-2025-03-08-001.md
limits:
  max_tool_calls: 20
  max_rows: 5000
  require_approval_for:
    - delete:*
trust_state: active
issuer: access-broker.internal
signature: replace-with-jws-or-mtls-proof

实践时,下游服务不能只相信客户端提交的 YAML 或 JWT 声明。它应当验证签名、发行者、有效期、任务状态和资源范围,并从服务端查询最新的撤销状态。

把持续中介放进工具调用路径

下面是一个可以运行和改造的 Python 最小示例。它用内存对象模拟身份代理和访问中介,重点演示每次工具调用都检查任务状态、动作范围和调用次数。生产环境应将状态放到数据库或专用授权服务中,并使用真实的签名验证和审计日志。

from dataclasses import dataclass, field
from datetime import datetime, timedelta, timezone


@dataclass
class TaskGrant:
    job_id: str
    subject: str
    expires_at: datetime
    allowed_actions: set[str]
    max_tool_calls: int
    tool_calls: int = 0
    state: str = 'active'
    events: list[str] = field(default_factory=list)

    def record(self, message: str) -> None:
        self.events.append(f'{datetime.now(timezone.utc).isoformat()} {message}')


class AccessBroker:
    def issue(self, subject: str) -> TaskGrant:
        grant = TaskGrant(
            job_id='job-001',
            subject=subject,
            expires_at=datetime.now(timezone.utc) + timedelta(minutes=15),
            allowed_actions={'read:sales.weekly', 'write:reports/job-001.md'},
            max_tool_calls=3,
        )
        grant.record('grant_issued')
        return grant


class ContinuousMediator:
    def authorize(self, grant: TaskGrant, action: str) -> None:
        now = datetime.now(timezone.utc)
        if grant.state != 'active':
            raise PermissionError(f'task is {grant.state}')
        if now >= grant.expires_at:
            grant.state = 'revoked'
            grant.record('grant_expired')
            raise PermissionError('grant expired')
        if grant.tool_calls >= grant.max_tool_calls:
            grant.state = 'suspended'
            grant.record('call_limit_reached')
            raise PermissionError('tool call limit reached')
        if action not in grant.allowed_actions:
            grant.record(f'denied action={action}')
            raise PermissionError(f'action not allowed: {action}')

        grant.tool_calls += 1
        grant.record(f'allowed action={action}')


def call_tool(grant: TaskGrant, action: str) -> str:
    mediator = ContinuousMediator()
    mediator.authorize(grant, action)
    return f'executed {action} for {grant.job_id}'


if __name__ == '__main__':
    grant = AccessBroker().issue('user-123')
    print(call_tool(grant, 'read:sales.weekly'))
    print(call_tool(grant, 'write:reports/job-001.md'))

    try:
        call_tool(grant, 'delete:reports/job-001.md')
    except PermissionError as exc:
        print(f'denied: {exc}')

    print('\\n'.join(grant.events))

运行方式:

python agent_access_demo.py

这个示例还有几个需要补齐的生产能力:使用不可伪造的任务凭证、在每次请求中绑定受众和资源、把撤销状态集中存储、记录完整的输入输出摘要,并对高风险写操作加入人工审批或二次认证。

采用时的检查清单

  • 每个 Agent 任务是否都有唯一的 job_id 和明确过期时间?
  • Agent 是否只拿到任务所需的短期凭证,而不是用户长期 Token?
  • 工具和下游 API 是否在每次调用时执行授权检查?
  • 资源范围是否能精确到项目、表、文件或具体记录?
  • 任务状态是否由服务端维护,并支持暂停、撤销和人工接管?
  • 是否能从审计记录还原“谁发起了任务、Agent 做了什么、为何被允许”?
  • 当风险上升时,系统是否会收紧权限,而不是继续沿用原授权?

任务型 Agent 的安全边界最终应落在服务端,而不是提示词、Agent 框架或模型的自我约束上。身份代理解决身份来源,持续中介解决即时授权,有状态信任解决任务过程中的变化。三者结合,才能让 Agent 在足够有用的同时保持可暂停、可撤销、可审计。


相关推荐