Блог • 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.
- Managed settings требуют managed.policy_mode layered; при first или отсутствии на legacy-пути layered_policy_gate → first_only_legacy или not_layered, без winningPolicyId layered_merge.
- Каждый matchedPolicies нужен id, order и rules с tool и effect allow, deny или ask; после merge Write → allow, Bash при нижнем deny и верхнем ask → ask, не deny.
- Audit logs: appliedPolicyIds по порядку, decision allow deny или ask, reason layered_merge, winningPolicyId эффективного слоя; SIEM атрибутирует по winningPolicyId, не только первому id.
- Нет rule для tool → layered_policy_gate no_rule; нет policyMode или плохая форма matchedPolicies → schema до merge.
Сначала это: 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": {
"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.
{
"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.
{
"$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.
{
"tool": "Write",
"path": "/srv/repo/src/app.ts"
}
{
"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.
{
"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 | Log decision allow; winningPolicyId — max order. |
| Bash deny снизу ask сверху, reason layered_merge | ask | Log decision ask; winningPolicyId — id верхней ask policy. |
| Итоговый effect deny без ask override | deny | Log 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.
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Зафиксировать форму context через Schema
Откройте JSON Schema. Schema evaluation context в первое поле, policyMode layered и matchedPolicies во второе. Ошибки required или minItems сначала; при плохой форме не allow или deny от layered_policy_gate.
- 2Format для вызовов tool
Откройте JSON Формат, вставьте JSON Write allow или Bash ask. Проверьте tool, затем ask или allow у layered_policy_gate.
- 3Diff 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.