Блог • Claude Code / Политика gateway

После обновления до Claude Code 2.1.316 с managed.policy_mode layered на Claude apps gateway: как объединять несколько совпавших JSON permission policy и писать в audit logs?

31 октября 2026 Anthropic выпустил Claude Code 2.1.316. Managed settings на Claude apps gateway добавляют managed.policy_mode layered: вместо первого совпадения все подходящие policy попадают в matchedPolicies и сливаются снизу вверх; верхние tool effects перекрывают нижние allow, deny или ask.

Evaluation context JSON должен содержать policyMode layered и matchedPolicies с id, order и rules по tool и effect allow, deny или ask. Audit 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; контекст auto evaluation Managed Agents — в auto evaluation Managed Agents; JSON workflow сессии — в Claude Code workflow. Эта статья только о merge layered 2.1.316, evaluation context, полях audit log и ветвлении layered_policy_gate.

Сначала это: 2.1.316 меняет layered merge нескольких policy на gateway и поля audit; форматы локальных permission policy не переписываются. Далее: changelog, managed settings JSON, evaluation context, вызовы Write и Bash, audit logs и таблица, исходник layered_policy_gate, проверка форм в JSONNote.

Как 2.1.316 превращает несколько совпавших policy в layered merge

Изменения — в changelog Claude Code, страница тега релиз v2.1.316. По сравнению со старым gateway заметен managed.policy_mode layered: не останавливаться на первом match, merge всех по order, верхние tool rules перекрывают нижние.

Merge на этапе evaluation tool call. Audit JSON: appliedPolicyIds по порядку, decision, reason layered_merge, winningPolicyId для SIEM-атрибуции к эффективному слою, не только first_match policy id.

Как задать managed.policy_mode layered в managed settings JSON

В managed settings от Claude apps gateway задайте policy_mode layered в managed. Gateway отправляет все совпавшие policy в evaluation pipeline; при first или без layered — first_only_legacy, не поля layered_merge.

Managed settings: managed.policy_mode layered.
{
  "managed": {
    "policy_mode": "layered"
  }
}

Неверный регистр policy_mode или нет managed → schema сначала, до merge layered_policy_gate. Не считать ошибку формы not_layered.

Как policyMode и matchedPolicies выглядят в evaluation context

Gateway пишет policyMode layered и matchedPolicies в evaluation context JSON. У каждой записи id, order, rules; rule связывает tool с effect allow, deny или ask. Порядок списка — стек merge: поздние policy перекрывают ранние rules для того же tool.

Evaluation 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 минимум одна запись с integer id и order и непустыми rules; нет policyMode или пусто → schema. policyMode не layered → not_layered.

JSON Schema evaluation context: форма до 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 после layered merge

Образец 1: нижний и верхний allow для Write → decision allow. Образец 2: Bash deny снизу, ask сверху → decision ask, winningPolicyId верхний id. Audit JSON: tool и effect по слоям до merge для layered_policy_gate.

После merge allow: вызов tool Write.
{
  "tool": "Write",
  "path": "/srv/repo/src/app.ts"
}
После merge ask: Bash верхний ask перекрывает нижний deny.
{
  "tool": "Bash",
  "command": "npm test"
}

Как писать appliedPolicyIds и winningPolicyId в audit logs

После layered merge: appliedPolicyIds — упорядоченные id участников, decision allow deny или ask, reason layered_merge, winningPolicyId — id эффективного слоя. Write allow обычно max order; Bash ask winningPolicyId на верхний ask, не нижний deny.

Audit log: 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_mergeallowLog decision allow; winningPolicyId — max order.
Bash deny снизу ask сверху, reason layered_mergeaskLog decision ask; winningPolicyId — id верхней ask policy.
Итоговый effect deny без ask overridedenyLog decision deny; appliedPolicyIds полный порядок.
Settings всё ещё first или layered не включёнfirst_only_legacyНе писать reason layered_merge; сверять с legacy first-match отчётами.

Как layered_policy_gate выбирает между schema и семью business-ветками

Порядок: schema evaluation context для policyMode и matchedPolicies; policyMode не layered → not_layered; settings first → first_only_legacy; merge effects одного tool снизу вверх по order; нет rule → no_rule; итог allow deny или ask; плохая форма → schema.

Не схлопывайте deny и ask в один blocked. Считайте отдельно first_only_legacy, no_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

Как проверять settings, context и audit logs в JSONNote

  1. 1
    Зафиксировать форму context через Schema

    Откройте JSON Schema. Schema evaluation context в первое поле, policyMode layered и matchedPolicies во второе. Ошибки required или minItems сначала; при плохой форме не allow или deny от layered_policy_gate.

  2. 2
    Format для вызовов tool

    Откройте JSON Формат, вставьте JSON Write allow или Bash ask. Проверьте tool, затем ask или allow у layered_policy_gate.

  3. 3
    Diff audit logs allow и ask

    Откройте JSON Diff. Слева образец Write allow, справа Bash ask. Подсвеченный winningPolicyId — ключ SIEM.

Вопросы

Чем layered отличается от старого first-match-only?

Legacy останавливался на первом match, в audit часто один policy id. 2.1.316 layered пишет все совпадения в appliedPolicyIds, merge по order, reason layered_merge и winningPolicyId. Gateway не layered → first_only_legacy.

Как layered связан с локальным файлом permission policy?

layered merge в managed gateway settings идёт в cloud evaluation pipeline и даёт matchedPolicies и поля audit. Локальные permission policy файлы — для defaults Agent; id и order не совпадают автоматически. Проверяйте managed settings, evaluation context и local policy JSON отдельно.

Может ли нижний deny перекрыть верхний ask?

Да. layered merge складывает снизу вверх по order; итоговый effect для tool — с верхнего совпавшего слоя. Bash deny снизу ask сверху → decision ask, winningPolicyId вверх. Без rule сверху остаётся deny снизу.

Как SIEM использует winningPolicyId для атрибуции?

appliedPolicyIds — полный порядок merge, winningPolicyId — эффективный слой. SIEM связывает owner и тикеты по winningPolicyId и по длине appliedPolicyIds ловит first-match-only. При reason layered_merge агрегируйте decision allow deny ask.

Есть ли в статье атака или bypass?

Нет. Статья только о формах JSON layered merge 2.1.316, полях audit и ветках layered_policy_gate для проверок и SIEM. Без шагов обхода контролей через многослойный ask или deny.

Итог

Claude Code 2.1.316 через managed.policy_mode layered на Claude apps gateway сливает несколько policy и пишет в audit JSON appliedPolicyIds, decision, reason layered_merge и winningPolicyId. Кормите layered_policy_gate образцами managed settings, evaluation context, Write и Bash и audit logs, а не только первым policy id.

Решение: context не проходит schema → schema; policyMode не layered → not_layered; settings first → first_only_legacy; нет rule для tool → no_rule; merged allow deny или ask согласованы с audit reason layered_merge и winningPolicyId.

Дальше: Schema для context и settings, Format для Write и Bash, Diff audit logs allow и ask, отдельно отчитывать возвраты layered_policy_gate.

Назад в блог