博客 • Claude Code / Gateway 策略

升级到 Claude Code 2.1.316 之后,Claude apps gateway 设置 managed.policy_mode 为 layered 时,多条匹配的权限策略 JSON 要怎样合并并写进审计日志?

2026 年 10 月 31 日,Anthropic 发布 Claude Code 2.1.316。Claude apps gateway 托管设置新增 managed.policy_mode 取值 layered:相对旧版只取第一条匹配策略,现在凡匹配当前开发者的策略都会进入 matchedPolicies,并按列表顺序自下而上合并,上层同 tool 的 effect 覆盖下层 allow、deny 或 ask。

评测 context JSON 应写出 policyMode layered、matchedPolicies 数组(含 id、order、按 tool 与 effect 的 rules),tool 调用后的审计 JSON 应写 appliedPolicyIds、decision、reason layered_merge 与 winningPolicyId。运维可用本地 layered_policy_gate 对照 schema、not_layered、first_only_legacy、no_rule、allow、deny、ask 七条分支,而不是只盯第一条 policy id。

allow 与 deny 队列写法见 Agent permission policy;Managed Agents 的 auto 评测上下文见 Managed Agents auto 评测;会话级工作流 JSON 见 Claude Code workflow。本篇只写 2.1.316 的 layered 策略合并、评测 context 与审计日志字段,以及 layered_policy_gate 怎样分支。

先记住这一点:2.1.316 改的是 gateway 上多条匹配策略的 layered 合并与审计字段,不是重写本地 permission policy 文件格式。下面按 changelog、managed settings JSON、评测 context、Write 与 Bash 调用样本、审计日志与对照表、layered_policy_gate 源码,以及 JSONNote 里怎样校验形状。

2.1.316 怎样把多条匹配策略改成 layered 合并?

变更说明见 Claude Code changelog,标签页见 v2.1.316 release。相对更早 gateway 行为,最显眼的是 managed.policy_mode layered:不再在第一条匹配策略处停止,而是把全部匹配项按 order 合并,上层 tool 规则覆盖下层。

合并发生在 tool 调用评估阶段。审计 JSON 应保留 appliedPolicyIds 全序、decision、reason layered_merge 与 winningPolicyId,方便 SIEM 按最终生效层归因,而不是只记 first_match policy id。

managed.policy_mode layered 在托管 settings JSON 里怎样写?

在 Claude apps gateway 下发的托管 settings 里,managed 对象设置 policy_mode 为 layered。gateway 会把匹配当前开发者的策略全部送入评测 pipeline;若仍为 first 或未声明 layered,客户端应走 first_only_legacy 路径而不是写 layered_merge 审计字段。

托管 settings:managed.policy_mode 为 layered。
{
  "managed": {
    "policy_mode": "layered"
  }
}

若运维把 policy_mode 写成 Layered 大小写混用或漏掉 managed 层级,本地 schema 校验应先失败并返回 schema,再进入 layered_policy_gate 的合并分支。不要把形状错误当成 not_layered。

评测 context 里 policyMode 与 matchedPolicies 怎样写?

gateway 在评测 context JSON 里写入 policyMode layered 与 matchedPolicies 数组。每项含 id、order 与 rules 列表;每条 rule 绑定 tool 与 effect allow、deny 或 ask。列表顺序表示合并栈:后列出的策略在合并时覆盖先列出的同 tool 规则。

评测 context:policyMode layered 与 matchedPolicies。
{
  "policyMode": "layered",
  "matchedPolicies": [
    {
      "id": "org-baseline",
      "order": 0,
      "rules": [
        { "tool": "Bash", "effect": "deny" }
      ]
    },
    {
      "id": "repo-contributors",
      "order": 1,
      "rules": [
        { "tool": "Write", "effect": "allow" },
        { "tool": "Bash", "effect": "ask" }
      ]
    }
  ]
}

matchedPolicies 至少一项且每项 id 与 order 为整数、rules 为非空数组;缺 policyMode 或空 matchedPolicies 时 layered_policy_gate 先返回 schema。policyMode 不是 layered 时返回 not_layered,避免误用 layered_merge 日志模板。

评测 context JSON Schema:先过形状再进 layered_policy_gate。
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "additionalProperties": false,
  "required": ["policyMode", "matchedPolicies"],
  "properties": {
    "policyMode": { "type": "string", "enum": ["layered", "first"] },
    "matchedPolicies": {
      "type": "array",
      "minItems": 1,
      "items": {
        "type": "object",
        "required": ["id", "order", "rules"],
        "properties": {
          "id": { "type": "string", "minLength": 1 },
          "order": { "type": "integer" },
          "rules": {
            "type": "array",
            "items": {
              "type": "object",
              "required": ["tool", "effect"],
              "properties": {
                "tool": { "type": "string" },
                "effect": { "type": "string", "enum": ["allow", "deny", "ask"] }
              }
            }
          }
        }
      }
    }
  }
}

Write 与 Bash 的 tool 调用在 layered 合并后怎样判定?

样本一:下层与上层都对 Write 设 allow,合并后 decision 为 allow。样本二:下层对 Bash 设 deny、上层设 ask,合并后 decision 为 ask,winningPolicyId 指向上层 id。审计 JSON 应同时保留 tool 名与合并前各层 effect,便于与 layered_policy_gate 对照。

合并后 allow:Write tool 调用。
{
  "tool": "Write",
  "path": "/srv/repo/src/app.ts"
}
合并后 ask:Bash 上层 ask 覆盖下层 deny。
{
  "tool": "Bash",
  "command": "npm test"
}

审计日志里 appliedPolicyIds 与 winningPolicyId 应怎样写?

layered 合并完成后,permission 或 tool 审计 JSON 应写 appliedPolicyIds 为参与合并的策略 id 全序数组,decision 为 allow、deny 或 ask,reason 固定为 layered_merge,winningPolicyId 指向最终生效层的 id。Write allow 样本的 winningPolicyId 通常是最高 order;Bash ask 样本的 winningPolicyId 应指上层 ask 而非下层 deny。

审计日志:appliedPolicyIds、decision、reason layered_merge、winningPolicyId。
{
  "tool": "Bash",
  "policyMode": "layered",
  "appliedPolicyIds": ["org-baseline", "repo-contributors"],
  "decision": "ask",
  "reason": "layered_merge",
  "winningPolicyId": "repo-contributors"
}
日志或调用现象layered_policy_gate 返回运维动作
Write 多层均为 allow,reason layered_mergeallow日志 decision allow,winningPolicyId 指向最高 order。
Bash 下层 deny、上层 ask,reason layered_mergeask日志 decision ask,winningPolicyId 指向上层 ask 策略 id。
合并后最终 effect 为 deny,无 ask 覆盖deny日志 decision deny,appliedPolicyIds 仍列全序。
settings 仍为 first 或未开 layeredfirst_only_legacy不要写 reason layered_merge;对照旧 first-match 报表。

layered_policy_gate 怎样在 schema 与七条业务分支间选择?

实现顺序:先用评测 context JSON Schema 校验 policyMode 与 matchedPolicies;policyMode 不是 layered 返回 not_layered;托管 settings 仍为 first 返回 first_only_legacy;按 order 自下而上合并同 tool 的 effect,无命中 rule 返回 no_rule;合并结果为 allow、deny 或 ask;形状错误返回 schema。

不要把 deny 与 ask 合成单一 blocked 位。报表分开计数,才能区分 first_only_legacy、无 rule、以及真正的 layered_merge allow ask deny。

layered_policy_gate:schema、not_layered、first_only_legacy、no_rule、allow、deny、ask。
def actual_type(data: object) -> str:
    if data is None:
        return "null"
    if isinstance(data, bool):
        return "boolean"
    if isinstance(data, list):
        return "array"
    if isinstance(data, int):
        return "integer"
    if isinstance(data, float):
        return "number"
    if isinstance(data, str):
        return "string"
    if isinstance(data, dict):
        return "object"
    raise TypeError(type(data))

def validate(data: object, schema: dict) -> list[str]:
    errors: list[str] = []
    if not isinstance(schema, dict):
        return errors
    types = schema.get("type")
    if isinstance(types, str):
        types = [types]
    actual = actual_type(data)
    if types and not any(t == actual or (t == "number" and actual == "integer") for t in types):
        errors.append("type")
    if "enum" in schema and not any(value == data for value in schema["enum"]):
        errors.append("enum")
    if isinstance(data, str) and "minLength" in schema and len(data) < schema["minLength"]:
        errors.append("minLength")
    if isinstance(data, dict) and schema.get("properties"):
        for key in schema.get("required") or []:
            if key not in data:
                errors.append(f"required:{key}")
        if schema.get("additionalProperties") is False:
            for key in data:
                if key not in schema["properties"]:
                    errors.append(f"additional:{key}")
        for key, sub in schema["properties"].items():
            if key in data and data[key] is not None:
                errors.extend(validate(data[key], sub))
    return errors

def effect_for_tool(policies: list, tool: str, layered: bool) -> tuple[str, str | None]:
    ordered = sorted(policies, key=lambda p: p.get("order", 0))
    if not layered:
        first = ordered[0]
        for rule in first.get("rules") or []:
            if rule.get("tool") == tool:
                return str(rule.get("effect")), str(first.get("id"))
        return "no_rule", None
    effect: str | None = None
    winner: str | None = None
    for policy in ordered:
        for rule in policy.get("rules") or []:
            if rule.get("tool") == tool:
                effect = str(rule.get("effect"))
                winner = str(policy.get("id"))
    if effect is None:
        return "no_rule", None
    return effect, winner

def layered_policy_gate(ctx: dict, call: dict, schema: dict) -> str:
    if validate(ctx, schema):
        return "schema"
    mode = ctx.get("policyMode")
    if mode != "layered":
        return "not_layered"
    policies = ctx.get("matchedPolicies") or []
    if not policies:
        return "schema"
    tool = call.get("tool")
    if not isinstance(tool, str):
        return "schema"
    effect, _ = effect_for_tool(policies, tool, layered=True)
    if effect == "no_rule":
        return "no_rule"
    return effect

在 JSONNote 里怎样校验 settings、context 与审计日志?

  1. 1
    用 Schema 固定 context 形状

    打开 JSON Schema。第一栏贴评测 context schema,第二栏贴 policyMode layered 与 matchedPolicies 样本。required 或 minItems 错误应先标红,layered_policy_gate 不应在形状错误时返回 allow 或 deny。

  2. 2
    用格式化核对 tool 调用

    打开 JSON 格式化,贴 Write allow 或 Bash ask 调用 JSON。确认 tool 键存在,再对照 layered_policy_gate 是否应走 ask 或 allow。

  3. 3
    用 Diff 对比 allow 与 ask 审计日志

    打开 JSON Diff。左边放 Write allow 样本,右边放 Bash ask 样本。Diff 高亮的 winningPolicyId 就是 SIEM 归因最该锁定的一行。

常见问题

layered 与旧版 first-match-only 有何不同?

旧版在第一条匹配策略处停止,审计常只留一个 policy id。2.1.316 layered 会把全部匹配项写入 appliedPolicyIds,按 order 合并后写 reason layered_merge 与 winningPolicyId。若 gateway 未切 layered,应走 first_only_legacy 而不是套用新字段。

layered 与本地 permission policy 文件是什么关系?

gateway 托管 settings 的 layered 合并发生在云端评测 pipeline,输出 matchedPolicies 与审计字段。本地 permission policy 文件仍可用于 Agent 默认规则或离线对照,但不应假设二者 id 与 order 自动一致。运维应分别校验 managed settings、评测 context 与本地 policy JSON。

下层 deny 能否被上层 ask 覆盖?

可以。layered 合并按 order 自下而上叠层,同一 tool 的最终 effect 取最高层命中规则。样本里 Bash 下层 deny、上层 ask 时 decision 为 ask,winningPolicyId 指向上层。若上层无 rule,则仍保留下层 deny。

SIEM 应怎样用 winningPolicyId 做归因?

appliedPolicyIds 记录参与合并的全序,winningPolicyId 记录最终生效层。SIEM 告警与报表应以 winningPolicyId 关联策略 owner 与变更单,并用 appliedPolicyIds 长度检测是否仍误走 first-match-only。reason 为 layered_merge 时再聚合 decision allow deny ask。

本篇是否提供攻击或绕过教程?

不是。本文只解释 2.1.316 layered 合并的 JSON 字段形状、审计日志写法与 layered_policy_gate 对照分支,帮助运维与合规做校验和 SIEM 归因。不提供如何利用多层 ask 或 deny 绕过管控的步骤。

总结

Claude Code 2.1.316 在 Claude apps gateway 上用 managed.policy_mode layered 合并多条匹配策略,并在审计 JSON 里写 appliedPolicyIds、decision、reason layered_merge 与 winningPolicyId。运维应用托管 settings、评测 context、Write 与 Bash 调用、审计日志四套样本驱动 layered_policy_gate,而不是只盯第一条 policy id。

直接结论:context 不过 schema 返回 schema;policyMode 不是 layered 返回 not_layered;settings 仍为 first 返回 first_only_legacy;无 tool 规则返回 no_rule;合并后 allow、deny 或 ask 与审计 reason layered_merge 及 winningPolicyId 对齐。

下一步:用 Schema 固定 context 与 settings,用格式化看清 Write 与 Bash 调用,用 Diff 对比 allow 与 ask 审计日志,并在报表里分开统计 layered_policy_gate 返回值。

返回博客