写了个 PAP/PACT 授权面审计器:把 `poppy.json` 和 PACT 委托链的检查项跑成一条命令

背景:为什么需要这个脚本

上周 Meta+Sierra 的 PAP(Poppy)Draft 0.1 和 Decagon 的 PACT 1.0 同周发布。两份规范有一个共性:它们的检查项都是"可机械验证"的------字段在不在、域名对不对、有效期超没超、scope 有没有越界、receipt 缺不缺 claim。但规范文本是自然语言,实际集成时这些检查散在文档各处,非常容易漏。

更麻烦的是,这类检查漏掉不会报错。PAP 的 issuer 交叉校验如果被删掉,一切照常运行------直到有人拿一个伪造的域名指向你的真实登录服务器。而就在这个月,MCP Python SDK 刚因为一条"校验被跳过而不是失败"的回退路径,暴露了一个可以把 authorization code + client secret + PKCE verifier 一起外发的高危漏洞。

所以我写了一个离线审计器:把 PAP 发现文件、issuer 元数据交叉校验、PACT 委托令牌与 receipt 的检查项,全部固化成代码里的固定判定规则。核心设计原则只有一条:fail closed。


一、先把检查项列清楚(这就是规范给你的全部)

1. poppy.json 发现文件(Draft 0.1)

检查 规则
protocol_version 必需;major.minor;主版本不支持 → 直接拒绝
organization 必需;domain 必须与请求主机名一致(忽略 www.)
auth 条件必需(列出 agent/apis/web.browser_session_endpoint 时);issuer 必须 HTTPS
custom_scopes 不得占用保留前缀 poppy:(只有 poppy:read/poppy:write 合法)
路由 agent.protocols[].endpoint、apis[].url、browser_session_endpoint 一律 HTTPS
apis[].type 只能是 openapi 或 mcp
extensions 内置 operations 之外,第三方扩展名必须 域名/功能名 形式

2. issuer 元数据交叉校验(真正的信任锚)

检查 规则
issuer 一致 元数据 issuer == poppy.json 的 auth.issuer(精确匹配)
域名认领 元数据 poppy_domains 必须列出本域名
端点齐备 必须有 token_endpoint + revocation_endpoint;direct 登录需 authorization_endpoint;device 登录需 device_authorization_endpoint
三份对齐 列出 MCP 接口时,issuer 需出现在 MCP 资源元数据的 authorization_servers 中
取不到 判为不通过(fail-closed)

3. PACT 委托链与 receipt

检查 规则
平台身份 JWT 有效期 ≤ 300s;建议公布 JWKS
委托令牌 必须绑定 user / pa / brand / scopes;有效期 ≤ 3600s;无 jti 则无法跟踪重放(规范默认不跟踪)
user 标识 禁止从姓名/邮箱/手机号派生(含加盐哈希)
receipt 必需 claim:grantId/user/pa/brand/scopesUsed/actions/ts
越权检测 scopesUsed 必须是授予 scope 的子集
argsHash 缺失则只能证明"调用过",证不了"改了什么"
留存方 receipt 留存是 SHOULD,且落在个人 Agent 一侧------业务方要自己补台账

二、完整脚本

纯标准库、无网络依赖(把"取元数据"当作输入,因为决定 fail-closed 的应该是发起请求的那一层)。

python 复制代码
# -*- coding: utf-8 -*-
"""
PAP/PACT 授权面审计器(纯标准库,可离线运行)
Part 1: poppy.json 静态校验(发现文件)
Part 2: issuer 元数据交叉校验(信任锚 / fail-closed)
Part 3: PACT 委托令牌与 receipt 校验(授权面 + 可验证同意)
"""
import json
import re

VIOL = []
def add(level, code, msg):
    VIOL.append((level, code, msg))


# ---------------- Part 1 ----------------
SUPPORTED_MAJOR = "0"          # Draft 0.1
RESERVED_PREFIX = "poppy:"
ALLOWED_RESERVED = {"poppy:read", "poppy:write"}
SIGNIN_TYPES = ("direct", "device", "mediated")


def _is_https(u):
    return isinstance(u, str) and u.startswith("https://")


def check_poppy(doc, requested_domain):
    """按 Draft 0.1 应做的检查逐条执行。任何一项取不到 -> 记为失败(fail-closed)。"""
    if not isinstance(doc, dict):
        add("HIGH", "PAP-000", "发现文件无法解析为 JSON 对象")
        return

    # protocol_version
    pv = doc.get("protocol_version")
    if not pv:
        add("HIGH", "PAP-001", "缺少 protocol_version(必需字段)")
    elif not re.fullmatch(r"\d+\.\d+", str(pv)):
        add("HIGH", "PAP-002", "protocol_version 不是 major.minor 格式")
    elif str(pv).split(".")[0] != SUPPORTED_MAJOR:
        add("HIGH", "PAP-003",
            "主版本 %s 不受支持(本审计器只认 Draft %s.x)"
            % (str(pv).split(".")[0], SUPPORTED_MAJOR))

    # organization
    org = doc.get("organization")
    if not isinstance(org, dict) or not org.get("name") or not org.get("domain"):
        add("HIGH", "PAP-004", "organization 缺 name 或 domain(必需字段)")
    else:
        got = str(org["domain"]).lower()
        want = str(requested_domain).lower()
        if got.startswith("www."):
            got = got[4:]
        if want.startswith("www."):
            want = want[4:]
        if got != want:
            add("HIGH", "PAP-005",
                "organization.domain(%s)与请求域名(%s)不一致 ------ 这份文件在替别的域名说话"
                % (org["domain"], requested_domain))

    # 三类接口至少有一类
    has_agent = "agent" in doc
    has_apis = "apis" in doc
    web = doc.get("web") or {}
    has_web = "web" in doc
    if not (has_agent or has_apis or has_web):
        add("HIGH", "PAP-006", "agent / apis / web 三类接口一个都没有(至少需其一)")

    # auth 的触发条件
    need_auth = has_agent or has_apis or bool(web.get("browser_session_endpoint"))
    auth = doc.get("auth")
    if need_auth and not isinstance(auth, dict):
        add("HIGH", "PAP-007", "列出 agent/apis/browser_session 却缺 auth 块")
    if isinstance(auth, dict):
        if not _is_https(auth.get("issuer")):
            add("HIGH", "PAP-008", "auth.issuer 缺失或不是 HTTPS")
        custom = auth.get("custom_scopes") or {}
        if not isinstance(custom, dict):
            add("MEDIUM", "PAP-009", "custom_scopes 应为 名称->说明 的映射")
        else:
            for name in custom:
                if name in ALLOWED_RESERVED:
                    continue
                if name.startswith(RESERVED_PREFIX):
                    add("HIGH", "PAP-010",
                        "自定义 scope「%s」占用了保留前缀 %s" % (name, RESERVED_PREFIX))

    # 路由一律 HTTPS
    for p in (doc.get("agent") or {}).get("protocols", []) or []:
        if not _is_https(p.get("endpoint")):
            add("HIGH", "PAP-011", "agent.protocols 端点不是 HTTPS")
    for api in doc.get("apis", []) or []:
        if api.get("type") not in ("openapi", "mcp"):
            add("MEDIUM", "PAP-012", "apis[].type 只能是 openapi 或 mcp")
        if not _is_https(api.get("url")):
            add("HIGH", "PAP-013", "apis[].url 不是 HTTPS")
    if web.get("browser_session_endpoint") and not _is_https(web["browser_session_endpoint"]):
        add("HIGH", "PAP-014", "web.browser_session_endpoint 不是 HTTPS")

    # extensions 命名空间
    for name in (doc.get("extensions") or {}):
        if name == "operations":
            continue
        if "." not in name or "/" not in name:
            add("MEDIUM", "PAP-015",
                "扩展名「%s」未用 域名/功能名 形式(协议内置的 operations 除外)" % name)


# ---------------- Part 2 ----------------
def check_issuer(poppy_doc, meta, ):
    """issuer 元数据交叉校验。meta 为 None 表示取不到 ------ 必须 fail-closed。"""
    auth = (poppy_doc or {}).get("auth") or {}
    declared = auth.get("issuer")
    org_domain = ((poppy_doc or {}).get("organization") or {}).get("domain")

    if meta is None:
        add("HIGH", "ISS-000", "取不到 issuer 元数据 -> fail-closed,判定为不通过"
                               "(不要写成 if meta: check(),那是一条静默放行通道)")
        return

    if meta.get("issuer") != declared:
        add("HIGH", "ISS-001",
            "元数据 issuer(%s)与 poppy.json 的 auth.issuer(%s)不一致 ------ 典型的混淆代理入口"
            % (meta.get("issuer"), declared))

    domains = meta.get("poppy_domains")
    if domains is None:
        add("HIGH", "ISS-002", "元数据缺 poppy_domains ------ 无法阻止别的域名冒名指向这个 issuer")
    elif org_domain not in domains:
        add("HIGH", "ISS-003",
            "poppy_domains 未列出本域名(%s)------ 登录服务器不认领这份文件" % org_domain)

    for ep in ("token_endpoint", "revocation_endpoint"):
        if not _is_https(meta.get(ep)):
            add("HIGH", "ISS-004", "元数据缺 %s(PAP 要求公布,缺了就无法撤销)" % ep)

    if isinstance(auth.get("direct"), dict) and not _is_https(meta.get("authorization_endpoint")):
        add("HIGH", "ISS-005", "提供 direct 登录却缺 authorization_endpoint")
    if isinstance(auth.get("device"), dict) and not _is_https(meta.get("device_authorization_endpoint")):
        add("HIGH", "ISS-006", "提供 device 登录却缺 device_authorization_endpoint")

    # 列出 MCP 接口时,三份文档必须对齐
    if any(a.get("type") == "mcp" for a in (poppy_doc.get("apis") or [])):
        auth_servers = meta.get("authorization_servers") or []
        if declared not in auth_servers:
            add("HIGH", "ISS-007",
                "有 MCP 接口,但 issuer 未出现在 MCP 资源元数据的 authorization_servers 中"
                "(poppy.json / issuer 元数据 / MCP 资源元数据三份必须互相对得上)")


# ---------------- Part 3 ----------------
JWT_MAX = 300
DELEG_MAX = 3600
RECEIPT_REQ = ("grantId", "user", "pa", "brand", "scopesUsed", "actions", "ts")


def check_pact(grant, agent_jwt, delegation, receipt):
    """PACT 委托链与 receipt 校验。"""
    # 平台身份 JWT
    life = None
    if not agent_jwt or "exp" not in agent_jwt or "iat" not in agent_jwt:
        add("HIGH", "PACT-100", "平台身份 JWT 缺 exp / iat,无法判断有效期")
    else:
        life = agent_jwt["exp"] - agent_jwt["iat"]
        if life > JWT_MAX:
            add("HIGH", "PACT-101", "平台身份 JWT 有效期 %ss > %ss 上限" % (life, JWT_MAX))
        if not agent_jwt.get("jwk_url"):
            add("MEDIUM", "PACT-102", "未给出 JWKS 地址,签名无法独立复核")

    # 委托令牌
    if not delegation:
        add("HIGH", "PACT-110", "没有委托令牌 ------ 只有平台身份,没有客户授权")
    else:
        bound = {k: delegation.get(k) for k in ("sub", "pa", "brand", "scopes")}
        for k, v in bound.items():
            if not v:
                add("HIGH", "PACT-111", "委托令牌未绑定 %s" % k)
        if delegation.get("exp_in", DELEG_MAX + 1) > DELEG_MAX:
            add("HIGH", "PACT-112",
                "委托令牌有效期 %ss > %ss 上限" % (delegation.get("exp_in"), DELEG_MAX))
        if not delegation.get("jti"):
            add("MEDIUM", "PACT-113",
                "委托令牌缺 jti -> 无法跟踪重放(规范里 jti 是可选项,provider「无需」跟踪重放,"
                "这一条只能你自己加)")
        if delegation.get("sub", {}).get("kind") == "pii":
            add("HIGH", "PACT-114",
                "user 标识疑似由姓名/邮箱/手机号派生 ------ 规范禁止(含加盐哈希)")

    # receipt
    if not receipt:
        add("HIGH", "PACT-120", "没有 receipt ------ 「客户同意了」目前只是聊天记录里的一句话")
        return
    for k in RECEIPT_REQ:
        if k not in receipt:
            add("HIGH", "PACT-121", "receipt 缺必需 claim:%s" % k)
    granted = set(grant.get("scopes") or [])
    used = set(receipt.get("scopesUsed") or [])
    extra = used - granted
    if extra:
        add("HIGH", "PACT-122", "receipt 记录了未授权 scope:%s" % sorted(extra))
    for i, act in enumerate(receipt.get("actions") or []):
        if not act.get("argsHash"):
            add("MEDIUM", "PACT-123",
                "receipt.actions[%d] 无 argsHash ------ 只能证明「调用过」,证不了「改了什么」" % i)
    if receipt.get("receipt_kept_by") != "business":
        add("MEDIUM", "PACT-124",
            "receipt 由个人 Agent 侧留存 ------ 而签字方(业务方)恰恰是规范没要求留存任何东西的一方")


# ---------------- 编排 ----------------
def run_case(title, poppy, meta, requested_domain, grant=None,
             agent_jwt=None, delegation=None, receipt=None):
    VIOL.clear()
    print("=" * 68)
    print("CASE:", title)
    check_poppy(poppy, requested_domain)
    check_issuer(poppy, meta)
    if grant is not None:
        check_pact(grant, agent_jwt, delegation, receipt)
    if not VIOL:
        print("  通过:未发现问题")
    else:
        rank = {"HIGH": 0, "MEDIUM": 1, "LOW": 2}
        for lv, code, msg in sorted(VIOL, key=lambda v: rank[v[0]]):
            print("  [%-6s] %-9s %s" % (lv, code, msg))
        hi = sum(1 for v in VIOL if v[0] == "HIGH")
        print("  -> 共 %d 项问题(HIGH %d / MEDIUM %d)"
              % (len(VIOL), hi, len(VIOL) - hi))
    print()


GOOD = {
    "protocol_version": "0.1",
    "organization": {"name": "Northwind Outfitters", "domain": "northwind.example"},
    "auth": {
        "issuer": "https://auth.northwind.example",
        "direct": {"scopes": ["poppy:read", "poppy:write", "accounts"]},
        "device": {"scopes": ["poppy:read", "poppy:write"]},
        "custom_scopes": {"accounts": "Manage saved shipping accounts"},
    },
    "agent": {"protocols": [{"type": "poppy",
                             "endpoint": "https://api.northwind.example/poppy/conversations"}]},
    "web": {"browser_session_endpoint": "https://northwind.example/poppy/browser-session"},
    "apis": [{"type": "mcp", "url": "https://mcp.northwind.example/mcp",
              "description": "Product search and sizing"}],
    "extensions": {"operations": {"version": "0.1"},
                   "northwind.example/gift-wrap": {"version": "0.1"}},
}
GOOD_META = {
    "issuer": "https://auth.northwind.example",
    "poppy_domains": ["northwind.example"],
    "token_endpoint": "https://auth.northwind.example/token",
    "revocation_endpoint": "https://auth.northwind.example/revoke",
    "authorization_endpoint": "https://auth.northwind.example/authorize",
    "device_authorization_endpoint": "https://auth.northwind.example/device",
    "authorization_servers": ["https://auth.northwind.example"],
}
GRANT = {"scopes": ["poppy:read", "orders.read"]}
JWT_OK = {"iat": 1000, "exp": 1250, "jwk_url": "https://pa.example/jwks.json"}
DELEG_OK = {"sub": {"id": "opaque-9f2c", "kind": "opaque"},
            "pa": "https://pa.example", "brand": "northwind.example",
            "scopes": ["poppy:read", "orders.read"], "exp_in": 1800,
            "jti": "d-7f31"}
RECEIPT_OK = {"grantId": "g-42", "user": "opaque-9f2c", "pa": "https://pa.example",
              "brand": "northwind.example", "scopesUsed": ["orders.read"],
              "actions": [{"name": "orders.read", "argsHash": "sha256:1a2b"}],
              "ts": "2026-10-13T02:10:00Z", "receipt_kept_by": "business"}

# 1) 合规样本
run_case("合规样本(Draft 0.1 最小可用 + 完整委托链)",
         GOOD, GOOD_META, "northwind.example", GRANT, JWT_OK, DELEG_OK, RECEIPT_OK)

# 2) 子域/品牌冒名 + 取不到 issuer 元数据
bad1 = json.loads(json.dumps(GOOD))
bad1["organization"]["domain"] = "northwind-global.example"
run_case("冒名样本:organization.domain 写成集团主域 + issuer 元数据取不到",
         bad1, None, "northwind.example")

# 3) 保留前缀 + 软 404 + MCP 三份文档不对齐
bad2 = json.loads(json.dumps(GOOD))
bad2["auth"]["custom_scopes"] = {"poppy:refund": "退款", "gift-wrap": "礼品包装"}
bad2["apis"] = [{"type": "mcpx", "url": "http://mcp.northwind.example/mcp",
                 "description": "Product search"}]
bad2["extensions"] = {"giftwrap": {"version": "0.1"}}
bad2_meta = dict(GOOD_META)
bad2_meta["authorization_servers"] = ["https://other-idp.example"]
bad2_meta["poppy_domains"] = ["northwind.example", "sister-brand.example"]
run_case("错误样本:保留前缀被占用 / 路由非 HTTPS / 扩展命名空间 / MCP 三份不对齐",
         bad2, bad2_meta, "northwind.example")

# 4) 委托链越权 + receipt 缺字段 + 无撤销端点
bad_deleg = {"sub": {"id": "zhang.san@northwind.example", "kind": "pii"},
             "pa": "https://pa.example", "brand": "northwind.example",
             "scopes": ["poppy:read", "orders.read", "orders.cancel"], "exp_in": 86400}
bad_receipt = {"grantId": "g-42", "user": "opaque-9f2c", "pa": "https://pa.example",
               "brand": "northwind.example", "scopesUsed": ["poppy:read", "orders.cancel"],
               "actions": [{"name": "orders.cancel"}], "ts": "2026-10-13T02:10:00Z"}
bad_jwt = {"iat": 1000, "exp": 1000 + 3600}
meta_no_revoke = dict(GOOD_META)
meta_no_revoke.pop("revocation_endpoint")
run_case("越权样本:委托令牌 24h + user 由邮箱派生 + receipt 用了未授权 scope + 无撤销端点",
         GOOD, meta_no_revoke, "northwind.example", GRANT, bad_jwt, bad_deleg, bad_receipt)

print("=" * 68)
print("审计完成:共 4 组样本。判定规则固定为「取不到即不通过」。")

三、实机运行结果(Python 3.13.12,两次运行 diff 完全一致)

ini 复制代码
====================================================================
CASE: 合规样本(Draft 0.1 最小可用 + 完整委托链)
  通过:未发现问题

====================================================================
CASE: 冒名样本:organization.domain 写成集团主域 + issuer 元数据取不到
  [HIGH  ] PAP-005   organization.domain(northwind-global.example)与请求域名(northwind.example)不一致 ------ 这份文件在替别的域名说话
  [HIGH  ] ISS-000   取不到 issuer 元数据 -> fail-closed,判定为不通过(不要写成 if meta: check(),那是一条静默放行通道)
  -> 共 2 项问题(HIGH 2 / MEDIUM 0)

====================================================================
CASE: 错误样本:保留前缀被占用 / 路由非 HTTPS / 扩展命名空间 / MCP 三份不对齐
  [HIGH  ] PAP-010   自定义 scope「poppy:refund」占用了保留前缀 poppy:
  [HIGH  ] PAP-013   apis[].url 不是 HTTPS
  [MEDIUM] PAP-012   apis[].type 只能是 openapi 或 mcp
  [MEDIUM] PAP-015   扩展名「giftwrap」未用 域名/功能名 形式(协议内置的 operations 除外)
  -> 共 4 项问题(HIGH 2 / MEDIUM 2)

====================================================================
CASE: 越权样本:委托令牌 24h + user 由邮箱派生 + receipt 用了未授权 scope + 无撤销端点
  [HIGH  ] ISS-004   元数据缺 revocation_endpoint(PAP 要求公布,缺了就无法撤销)
  [HIGH  ] PACT-101  平台身份 JWT 有效期 3600s > 300s 上限
  [HIGH  ] PACT-112  委托令牌有效期 86400s > 3600s 上限
  [HIGH  ] PACT-114  user 标识疑似由姓名/邮箱/手机号派生 ------ 规范禁止(含加盐哈希)
  [HIGH  ] PACT-122  receipt 记录了未授权 scope:['orders.cancel']
  [MEDIUM] PACT-102  未给出 JWKS 地址,签名无法独立复核
  [MEDIUM] PACT-113  委托令牌缺 jti -> 无法跟踪重放(规范里 jti 是可选项,provider「无需」跟踪重放,这一条只能你自己加)
  [MEDIUM] PACT-123  receipt.actions[0] 无 argsHash ------ 只能证明「调用过」,证不了「改了什么」
  [MEDIUM] PACT-124  receipt 由个人 Agent 侧留存 ------ 而签字方(业务方)恰恰是规范没要求留存任何东西的一方
  -> 共 9 项问题(HIGH 5 / MEDIUM 4)

====================================================================
审计完成:共 4 组样本。判定规则固定为「取不到即不通过」。

四、三条真实踩坑(都是设计过程中撞出来的)

1. if meta: 就是一条静默放行通道(最重要的坑)

第一版 check_issuer 我本能地写成了:

python 复制代码
def check_issuer(poppy_doc, meta):
    if meta:                       # ← 这里就是坑
        if meta.get("issuer") != declared: ...
        if org_domain not in meta.get("poppy_domains", []): ...

看起来没问题------有元数据才检查,没元数据就跳过。但"取不到元数据"恰恰是攻击者最容易制造的状态:让发现请求返回 404、超时、或者一份畸形 JSON,这条守卫就放行了。

这和本月的 MCP Python SDK 漏洞是同一个结构 :那条回退路径上 issuer 校验只在"已经存在授权服务器 URL"时执行,回退时该值为空 → 校验被跳过而不是失败。正确写法是把"取不到"变成一条显式的失败:

python 复制代码
    if meta is None:
        add("HIGH", "ISS-000", "取不到 issuer 元数据 -> fail-closed,判定为不通过")
        return

经验:任何"如果拿得到就校验"的守卫,都要先问一句"拿不到时会发生什么"。答案必须是"拒绝",不能是"继续"。

2. 保留前缀检查:天真的 startswith 会误伤两个合法 scope

第二版我写成了:

python 复制代码
if name.startswith("poppy:"):
    add("HIGH", "PAP-010", ...)

结果是合规样本里的 poppy:read 和 poppy:write 被自己判成了违规 。规范里 poppy: 是保留前缀,但恰好这两个词是合法的------所以判定要先排除白名单:

python 复制代码
ALLOWED_RESERVED = {"poppy:read", "poppy:write"}
...
    for name in custom:
        if name in ALLOWED_RESERVED:
            continue
        if name.startswith(RESERVED_PREFIX):
            add("HIGH", "PAP-010", ...)

经验:凡是"禁止某前缀"的规则,务必先把该前缀下被显式允许的例外列出来。否则你的校验器会在第一份合规样本上自我打脸。

3. 缺失有效期要按"超限"处理,不能按"不适用"处理

委托令牌的默认值我一开始想写成 delegation.get("exp_in", 0)------"没写有效期就当它没问题"。这又是一个 fail-open。最后改成:

python 复制代码
if delegation.get("exp_in", DELEG_MAX + 1) > DELEG_MAX:

没写有效期 → 用一个必然越界的默认值 → 直接判不通过。 同样思路用在 jti、argsHash、receipt_kept_by 上:缺字段本身就是一个发现,而不是"规范没要求所以忽略"。


五、这个脚本明确的局限(别把它当全部)

  • 它不联网 。meta 是作为参数传进来的。这是刻意的------决定 fail-closed 的那一行代码,必须住在真正发起请求、真正感知到"取不到"的那一层。审计器负责的是"拿到之后怎么判"。
  • 它不解析签名 。JWT 只检查 exp/iat/是否给了 JWKS,不做真正的验签。生产里必须用成熟的 JWT 库,并且算法白名单 要对(不能只信 token 自己声明的 alg)。
  • 它不覆盖 scope 语义 。规范里 poppy:write 的"做变更"是一个类别,不是一个契约------含义由每个公司自己定义。脚本只能查"用了没授权的 scope",查不出"授权的那个 scope 是不是太宽"。
  • 它不替你决定撤销 。PACT 没有撤销端点,脚本只能把"缺 revocation_endpoint"报出来。撤销流程要你自己设计,包括"撤销时派生的凭据、缓存、委托状态要不要一起作废"。

一句话:这个脚本能替你干掉 90% 的机械检查,但"该不该给这个 Agent 写权限"永远是你的决定。


六、抄作业清单(PR 描述可以直接贴)

  1. 所有校验函数入口先做"输入是否存在"的 fail-closed 断言,禁止 if x: 式守卫。
  2. organization.domain 与请求主机名做归一化后精确比较(统一去 www.、统一小写)。
  3. 保留前缀类规则必须先声明白名单,再判违规。
  4. 所有"缺字段"的默认值一律取"必然越界"的值,而不是 0 / 空 / None。
  5. 有效期上限写成常量(JWT_MAX=300、DELEG_MAX=3600)且集中定义,便于随规范改版统一调整。
  6. scopesUsed ⊆ granted 必须作为断言,而不是日志。
  7. argsHash 缺失要有独立告警码------它是"证明确改了什么"的唯一抓手。
  8. receipt 的留存方与留存期要写进设计文档,并明确由业务方补齐(规范只给了 SHOULD,且在个人 Agent 一侧)。
  9. 列出 MCP 接口的项目,必须同时校验三份文档的 issuer 一致性(poppy.json / issuer 元数据 / MCP 资源元数据)。
  10. 所有校验输出都带稳定错误码(PAP-xxx / ISS-xxx / PACT-xxx),方便进 CI 做基线对比。
  11. 校验器自带多组样本(合规 / 冒名 / 字段错误 / 越权),并断言样本 1 必须零问题------这是校验器自身的回归测试。

小结

写完这个脚本最大的收获不是代码,是它把两份规范里最容易被忽略的一句话变成了固定的判定规则:取不到即不通过。

PAP 给了你发现文件,PACT 给了你 receipt;但把"信任锚""fail-closed""台账"这三件事补上的,从来是工程侧,不是协议侧。

协议定义线格式,你定义边界。

域名与数据均为虚构示意;涉及漏洞的部分基于公开安全公告,不含任何可复现的操作细节。

相关推荐
楚楚2511 小时前
2026最新5款AI编程助手基础版免费深度对比
ai编程
fthux1 小时前
编程语言:从机器指令到人类思维的演进史诗
ai编程
Rain的Java大神之路2 小时前
被问麻了!Function Calling、MCP、Skill 到底啥关系?一张图讲透三层架构,附 Java 生产落地全流程+30个坑
java·后端·ai编程
桃西西呀2 小时前
LangGraph4j之三:状态与通道
人工智能·llm·ai编程
桃西西呀2 小时前
LangGraph4j之二:图模型与节点边
人工智能·llm·ai编程
技灵AI2 小时前
AI 家具视频提示词教程:单张客厅参考图生成竖版带货短片,提示词逐层怎么写
人工智能·prompt·aigc·电商运营
虞七月3 小时前
Claude Code 平替工具推荐:TRAE 是否值得作为首选替代方案?
copilot·ai编程
9i编程3 小时前
11. 教 AI 上班:带出我的数字同事 —— 前后端部署上线 130,按 v1.7 回归清零与总结
人工智能·openai·ai编程
唐老板3 小时前
Claude Code 的上下文与扩展:窗口满了之后怎么办(下)
ai编程