AgentDepthAgent 工程学习指南
输入概念或问题,检索正文和待编写目录。
第一次学习?从 L0 总览开始 →

身份、能力发现与信任

我的学习 →

正在读取学习记录…

身份回答“当前连接的服务是谁”,能力发现回答“它声明能做什么”,授权回答“当前任务允许它做什么”。信任需要这些信息相互核对,不能由一张漂亮的自我介绍代替。

一个服务叫“退款资料助手”,声明支持读取和退款支付,这能帮助调用者找到候选服务。可是相同名称可以被别人复制,能力声明也可以夸大。应用需要从可信登记与认证结果判断能否调用。

能力是能做,权限是获准做。一个工作人员会操作财务系统,不代表他在每笔订单上都有付款权限。给Agent也要保留这一区别,工牌不能靠自己画。

能读退款规则,为什么不能写退款

本地登记中,refund-reader对应身份service:refund-reader,允许的地址来源是https://refund.example,授予的能力只有refund.read,即读取退款资料。

服务的能力声明却包含refund.read和refund.write。应用先核对已认证身份,再核对地址来源,最后检查所需能力既已声明、又在登记授权中。因此读操作允许,写操作拒绝。

程序中的authenticated_subject代表连接层已经完成认证后得到的主体名称;固定例子直接传入它来观察门禁行为。网络上的证书、令牌或签名验证,需要由真实认证组件产生这个结果,不能从待核对声明中取同名字段来代替。

保存完整程序并运行

在自己的学习目录打开PowerShell,创建一个新的子目录:

Bash
mkdir l6-identity
cd l6-identity

把下面完整代码保存为 identity_demo.py。固定数据和运行入口都已包含,文件名保留 .py 后缀。

Python
"""本地身份与能力门禁;认证结果作为独立输入。"""
from urllib.parse import urlparse

REGISTRY = {
    "refund-reader": {
        "subject": "service:refund-reader", "origin": "https://refund.example",
        "grants": {"refund.read"}, "enabled": True,
    }
}
ANNOUNCEMENT = {
    "id": "refund-reader", "endpoint": "https://refund.example/query",
    "skills": ["refund.read", "refund.write"],
}

def discover(announcement, authenticated_subject, requested_skill):
    # 名字和技能来自待核对声明,身份来自已完成认证的连接信息。
    entry = REGISTRY.get(announcement.get("id"))
    if not entry or not entry["enabled"]:
        raise PermissionError("服务未登记或已停用")
    if authenticated_subject != entry["subject"]:
        raise PermissionError("认证身份不匹配")
    endpoint = urlparse(announcement.get("endpoint", ""))
    origin = f"{endpoint.scheme}://{endpoint.netloc}"
    if endpoint.scheme != "https" or origin != entry["origin"]:
        raise PermissionError("服务地址不在信任登记中")
    advertised = set(announcement.get("skills", []))
    if requested_skill not in advertised:
        raise ValueError("服务未声明所需能力")
    if requested_skill not in entry["grants"]:
        raise PermissionError("能力已声明,但执行权限不足")
    return announcement["endpoint"]

def main():
    address = discover(ANNOUNCEMENT, "service:refund-reader", "refund.read")
    print("可信读取:" + address)
    for subject, skill in [("service:impostor", "refund.read"),
                           ("service:refund-reader", "refund.write")]:
        try:
            discover(ANNOUNCEMENT, subject, skill)
        except PermissionError as error:
            print(skill + ":" + str(error))
    changed = dict(ANNOUNCEMENT, endpoint="https://attacker.example/query")
    try:
        discover(changed, "service:refund-reader", "refund.read")
    except PermissionError as error:
        print("换地址:" + str(error))

if __name__ == "__main__":
    main()

保存后执行 dir,确认能看到 identity_demo.py,再运行:

Bash
ls
python -X utf8 identity_demo.py

程序输出:

TXT
可信读取:https://refund.example/query
refund.read:认证身份不匹配
refund.write:能力已声明,但执行权限不足
换地址:服务地址不在信任登记中

三道检查为什么要分开

登记表REGISTRY由应用维护。announcement是服务提供的待核对能力描述,不能修改登记表。第一道检查使用服务id找到登记项,并判断enabled,表示当前是否仍允许使用它。

第二道检查比对authenticated_subject与登记中的subject。服务名称相同而认证主体不同,会得到“认证身份不匹配”。这能阻止只复制服务名称的候选混入已登记身份。

第三道检查解析endpoint,把协议与主机组成origin,并要求它等于登记值且为HTTPS。固定路径/query可以变化,但来源不能偷偷换成attacker.example;普通HTTP也被拒绝。地址验证与身份验证共同限定调用目标。

最后还要检查所需skill是否在能力声明中,并且在grants中。两者取交集,而不是把声明列表直接当权限列表。service被停用时,其他条件都成立也不会继续调用。

服务声明依次核对认证身份、HTTPS地址来源和已授予权限;声明的refund.write仍因未授权而拒绝
把待核对声明、连接身份与受信任登记分开保存,能力发现才能成为授权前的线索。

发现能力之后还要验证任务归属

能力发现通常会使用服务名片、登记目录或预先配置的地址。A2A的AgentCard提供可交互方式与技能等信息,调用者据此找候选服务。描述包含某项技能,不等于已完成身份验证,也不等于对当前客户数据拥有访问权。

例子只有一个受信任服务和读取权限。扩展到多租户时,授权还要带任务数据范围,例如只能读取shop-A的O-17,不能用同一read能力访问所有商家订单。权限判断应放在真正执行读取的服务端,再由调用端减少不必要的暴露。

代理调用还有另一个问题:主管有权限,不代表子Agent可以拿到主管的全部凭证。可以传递受限的任务授权,或让主管代为调用特定接口,再把必要结果给子Agent。凭证的范围、期限和撤销方式需要和委派任务一致。

哪些信息可以相信到什么程度

名称、描述和技能标签是可读线索。认证主体是连接经过验证后的身份。登记授权是应用决定的允许范围。历史运行表现则回答服务是否稳定、产物是否符合契约。它们支持不同判断,不应全部缩成一个“可信=True”。

HTTPS保护连接并帮助验证服务器地址,也不会证明服务的退款结论正确。通过门禁后,仍要检查返回对象、订单归属、资料版本与业务证据。可信身份可以出错,甚至读取过期规则。

登记也会过时。服务迁址、权限被收回、密钥撤销时,发现缓存应按合适策略刷新;不能因为昨天连通过,就永久跳过核对。缓存可减少开销,但失效和撤销需要明确路径。

改声明,看授权会不会跟着变

把ANNOUNCEMENT的skills增加refund.delete,再把main中("service:refund-reader", "refund.write")这组请求的技能改为refund.delete,运行python -X utf8 identity_demo.py。声明里有新能力,登记grants里没有,仍会显示执行权限不足。第一条refund.read调用保持原样。

先将新增的refund.delete声明删除,并把改过的请求恢复为refund.write。再把REGISTRY中的enabled改为False,运行同一命令,第一条正常读取就会报“服务未登记或已停用”。最终恢复enabled=True,重跑确认读取允许、写入拒绝,再运行配套检查。

若修改地址后失败,核对完整scheme与netloc;若同名服务无法通过,核对认证subject,而不是修改显示名称。若能力已声明但权限不足,需要改变正式授权配置,不能靠更强的角色提示词绕过去。

参考阅读:A2A Agent Discovery关注名片与候选服务发现;Authentication and Authorization区分客户端认证与服务端执行授权。

运行配套检查

把下面测试完整保存为 test_identity_demo.py,与主程序放在同一目录。6项检查覆盖正常读取、冒名身份、未授权写入、地址来源、停用登记和未声明技能。

查看测试代码:test_identity_demo.py
Python
"""身份、地址和授予权限需要分别成立。"""
import copy
import sys
import unittest
from pathlib import Path
from unittest.mock import patch
sys.path.insert(0, str(Path(__file__).resolve().parent))
import identity_demo as demo

class IdentityTests(unittest.TestCase):
    def test_trusted_registered_read(self):
        self.assertEqual(demo.discover(demo.ANNOUNCEMENT, "service:refund-reader", "refund.read"),
                         "https://refund.example/query")

    def test_same_name_different_subject_rejected(self):
        with self.assertRaises(PermissionError):
            demo.discover(demo.ANNOUNCEMENT, "service:impostor", "refund.read")

    def test_claimed_write_does_not_grant_write(self):
        with self.assertRaisesRegex(PermissionError, "权限不足"):
            demo.discover(demo.ANNOUNCEMENT, "service:refund-reader", "refund.write")

    def test_changed_origin_and_plain_http_rejected(self):
        for endpoint in ("https://attacker.example/query", "http://refund.example/query"):
            with self.subTest(endpoint=endpoint), self.assertRaises(PermissionError):
                demo.discover(dict(demo.ANNOUNCEMENT, endpoint=endpoint),
                              "service:refund-reader", "refund.read")

    def test_revoked_registry_entry(self):
        registry = copy.deepcopy(demo.REGISTRY)
        registry["refund-reader"]["enabled"] = False
        with patch.object(demo, "REGISTRY", registry), self.assertRaises(PermissionError):
            demo.discover(demo.ANNOUNCEMENT, "service:refund-reader", "refund.read")

    def test_unadvertised_skill_rejected(self):
        with self.assertRaises(ValueError):
            demo.discover(dict(demo.ANNOUNCEMENT, skills=[]), "service:refund-reader", "refund.read")

if __name__ == "__main__":
    unittest.main()
Bash
python -X utf8 -m unittest -v test_identity_demo.py

测试名称后应显示 ok,末尾显示 Ran 6 tests 和 OK。若找不到模块,先用 dir 确认两个文件保存在同一目录。

课后练习展开习题收起习题

4 道练习 · 客观题规则评分 · 其余题目待批改

正在读取本地记录…

不调用模型,不执行代码。简答、代码和场景题仅保存答案并展示自查标准,不生成评分、徽章或认证。输入内容不会自动保存,请点击提交;不要填写个人信息或密钥。

练习 1 / 4 · v1.1.0

选择题 · 服务介绍宣称可以查询订单,应用还要核对什么

服务介绍宣称可以查询订单,应用还要核对什么?

对应知识点:L6-identity

选择一个答案
练习 2 / 4 · v1.1.0

简答题 · 解释机制

能力发现、认证和授权分别回答什么?

对应知识点:L6-identity

练习 3 / 4 · v1.1.0

代码题 · 核对给定数据

用户owner与订单owner相同才可读,写一条布尔判断。

对应知识点:L6-identity

练习 4 / 4 · v1.1.0

场景题 · 分析具体变化

两个执行者技能相同,一个只能读当前客户的订单,怎样选择?

对应知识点:L6-identity

图片查看器
100%

文章配图 · 按 Esc 关闭