博客 • 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 返回值。