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

返回博客