博客 • 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 怎样分支。
- 托管 settings 里 managed.policy_mode 必须为 layered;若为 first 或未设置且 gateway 仍走旧路径,layered_policy_gate 应返回 first_only_legacy 或 not_layered,不要按 layered_merge 写 winningPolicyId。
- 评测 context 的 matchedPolicies 每项要有 id、order 与 rules(tool 加 effect allow、deny 或 ask);合并后 Write 可 allow,而 Bash 在下层 deny、上层 ask 时应得到 ask 而不是 deny。
- 审计日志应写 appliedPolicyIds 全序、decision allow deny 或 ask、reason layered_merge,以及 winningPolicyId 指向最终生效层;SIEM 应用 winningPolicyId 做归因而不是只看第一条 id。
- 没有任何 rule 命中 tool 时 layered_policy_gate 返回 no_rule;context 缺 policyMode 或 matchedPolicies 形状错误时先返回 schema,再进入合并逻辑。
先记住这一点: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 审计字段。
{
"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 规则。
{
"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 日志模板。
{
"$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 对照。
{
"tool": "Write",
"path": "/srv/repo/src/app.ts"
}
{
"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。
{
"tool": "Bash",
"policyMode": "layered",
"appliedPolicyIds": ["org-baseline", "repo-contributors"],
"decision": "ask",
"reason": "layered_merge",
"winningPolicyId": "repo-contributors"
}
| 日志或调用现象 | layered_policy_gate 返回 | 运维动作 |
|---|---|---|
| Write 多层均为 allow,reason layered_merge | allow | 日志 decision allow,winningPolicyId 指向最高 order。 |
| Bash 下层 deny、上层 ask,reason layered_merge | ask | 日志 decision ask,winningPolicyId 指向上层 ask 策略 id。 |
| 合并后最终 effect 为 deny,无 ask 覆盖 | deny | 日志 decision deny,appliedPolicyIds 仍列全序。 |
| settings 仍为 first 或未开 layered | first_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。
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用 Schema 固定 context 形状
打开 JSON Schema。第一栏贴评测 context schema,第二栏贴 policyMode layered 与 matchedPolicies 样本。required 或 minItems 错误应先标红,layered_policy_gate 不应在形状错误时返回 allow 或 deny。
- 2用格式化核对 tool 调用
打开 JSON 格式化,贴 Write allow 或 Bash ask 调用 JSON。确认 tool 键存在,再对照 layered_policy_gate 是否应走 ask 或 allow。
- 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 返回值。