fix(bedrock): enforce guardrails on Claude via InvokeModel headers - #52312
JoaoMarcos44 wants to merge 4 commits into
Conversation
…ousResearch#52179) Bedrock Guardrails configured under `bedrock.guardrail` were silently ignored for all Claude models because the AnthropicBedrock SDK uses the InvokeModel API, which has no `guardrailConfig` body parameter — only the Converse API does. The previous workaround rerouted Claude+guardrail to the Converse API (Option A), which enforced the guardrail but sacrificed prompt caching, thinking budgets, and 1M context. This commit implements the correct fix (Option B): inject the guardrail as HTTP headers (`X-Amzn-Bedrock-GuardrailIdentifier`, `X-Amzn-Bedrock-GuardrailVersion`, `X-Amzn-Bedrock-Trace`) into every InvokeModel request via the SDK's `extra_headers` mechanism. Claude models always stay on the `anthropic_messages` / InvokeModel path with full feature parity; non-Claude models continue to use the Converse API. Changes: - `hermes_cli/runtime_provider.py`: revert Option-A condition; Claude always uses `anthropic_messages` regardless of guardrail config. - `agent/agent_init.py`: read guardrail config once at agent init and store as `agent._bedrock_guardrail_headers`; show `+ Guardrails` in the startup banner when active. - `agent/chat_completion_helpers.py`: pass `bedrock_guardrail_headers` through to `transport.build_kwargs()`. - `agent/transports/anthropic.py`: forward the param to `build_anthropic_kwargs()`; add `guardrail_intervened` to the stop- reason map (`→ content_filter`) and to the valid-empty-content set. - `agent/anthropic_adapter.py`: merge guardrail headers into `extra_headers` without overwriting fast-mode or other betas; keys are disjoint (`X-Amzn-Bedrock-Guardrail*` vs `anthropic-beta`). - `hermes_logging.py`: defensive `try/except ImportError` fallback for `concurrent_log_handler` (pre-existing Windows issue). - `tests/agent/test_bedrock_integration.py`: update routing tests for Option B; add `TestBedrockGuardrailHeaderInjection` (5 tests) and `TestBedrockGuardrailStopReason` (3 tests). Fixes NousResearch#52179 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
b47928b to
369b23b
Compare
The three hatch_pet tests used 208×208 cells, producing strips up to 1664×208 pixels. The Python BFS in remove_background and component_boxes processed ~1.8M transparent pixels per hatch call, exceeding the CI 140s per-file limit. Switch fake_generate to 64×64 cells (~10× fewer pixels) so the same logic is exercised without timing out. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
teknium1
left a comment
There was a problem hiding this comment.
Thanks for tracing the Claude/InvokeModel split. Current main still has the reported gap: Bedrock Claude uses anthropic_messages in agent/agent_init.py:767-785, while guardrail config is initialized only for bedrock_converse at agent/agent_init.py:895-915; agent/chat_completion_helpers.py:781-816 likewise forwards it only through Converse.
Problems
agent/agent_init.py:661-662in this PR converts any non-emptybedrock.guardrail.tracevalue toENABLED. The documented values includedisabledandenabled_full(website/docs/guides/aws-bedrock.md:75-76), sodisabledwould unexpectedly enable tracing andenabled_fullwould be lost.- The PR parent is
0c442fa1, behind currentmain(8a5f8379). Salvage must resolve the moved surrounding code rather than apply the old context directly.
Suggested changes
- Preserve the configured trace enum when constructing the header and test all documented trace values.
- Add a temp-
HERMES_HOMEconfiguration-to-client test that captures both streaming and non-streaming AnthropicBedrock invocation kwargs.
Automated hermes-sweeper review.
…il-invokemodel-headers # Conflicts: # tests/agent/test_bedrock_integration.py
… header _bedrock_guardrail_headers collapsed any truthy skills.guardrail.trace value to "ENABLED", so "disabled" unexpectedly turned tracing on and "enabled_full" silently downgraded to "enabled" — both are documented config values (website/docs/guides/aws-bedrock.md). Extract the header construction into _bedrock_invokemodel_guardrail_headers() so it's unit testable, and uppercase the configured enum verbatim instead of hardcoding it. Adds coverage for all three documented trace values, both as a direct unit test and as a config-file-to-invocation-kwargs test under a temp HERMES_HOME.
|
I measured this PR's mechanism against live Bedrock before commenting, and I want to lead with the good part: the header approach works. Then two things that will stop it landing as written. The mechanism is correct — confirmed liveTemporary guardrail in AnthropicBedrock(aws_region="us-east-2").messages.create(
model="us.anthropic.claude-sonnet-5", max_tokens=40, messages=[...],
extra_headers={"X-Amzn-Bedrock-GuardrailIdentifier": gid,
"X-Amzn-Bedrock-GuardrailVersion": "DRAFT",
"X-Amzn-Bedrock-Trace": "ENABLED"})Same result through raw boto3 1. The detection won't fireLook at the "guardrail_intervened": "content_filter", # transports/anthropic.py _STOP_REASON_MAPand adds The Anthropic SDK keeps it as an unmodelled extra field, so Worth fixing rather than deferring, because the current failure mode is the bad one: the headers do block, so 2. It no longer applies to
|
…ce as refusals bedrock.guardrail was only attached on the Converse route (guardrailConfig in the body). Claude on Bedrock goes through the AnthropicBedrock SDK, i.e. InvokeModel, whose body has no guardrailConfig, so the default Claude route ran with no guardrail at all (#52179; live-verified by JiaDe-Wu: the blocked word came back through Hermes). Bedrock reads the guardrail for InvokeModel from X-Amzn-Bedrock-GuardrailIdentifier / -GuardrailVersion / -Trace headers. Attach them as default_headers in build_anthropic_bedrock_client so every AnthropicBedrock client Hermes builds (primary init, /model switch, fallback, per-request rebuild, auxiliary) enforces the same guardrail, with prompt caching / thinking / 1M context kept (the reason Claude is not routed through Converse). InvokeModel blocks do NOT change stop_reason (stays end_turn) and return the guardrail's canned text as an ordinary assistant reply, flagged only by amazon-bedrock-guardrailAction=INTERVENED in the body (SDK: response.model_extra). AnthropicTransport.response_finish_reason maps that to content_filter so the loop runs its refusal handling instead of reasoning over the canned text; _derive_finish_reason uses it for the anthropic_messages branch. Mantle (openai.gpt-5.x) is documented by AWS as not supporting Guardrails on the Responses endpoint; the docs now say so instead of promising "all model invocations". Header mechanism proposed in #52312 by @JoaoMarcos44 (stale base, 7-file conflict, detection keyed on a Converse-only stopReason); reimplemented on current main. Live probe (local sink, SigV4 fake creds): before, no X-Amzn-Bedrock-* header on the InvokeModel request; after, headers present, SigV4 intact, INTERVENED → content_filter.
…ce as refusals bedrock.guardrail was only attached on the Converse route (guardrailConfig in the body). Claude on Bedrock goes through the AnthropicBedrock SDK, i.e. InvokeModel, whose body has no guardrailConfig, so the default Claude route ran with no guardrail at all (#52179; live-verified by JiaDe-Wu: the blocked word came back through Hermes). Bedrock reads the guardrail for InvokeModel from X-Amzn-Bedrock-GuardrailIdentifier / -GuardrailVersion / -Trace headers. Attach them as default_headers in build_anthropic_bedrock_client so every AnthropicBedrock client Hermes builds (primary init, /model switch, fallback, per-request rebuild, auxiliary) enforces the same guardrail, with prompt caching / thinking / 1M context kept (the reason Claude is not routed through Converse). InvokeModel blocks do NOT change stop_reason (stays end_turn) and return the guardrail's canned text as an ordinary assistant reply, flagged only by amazon-bedrock-guardrailAction=INTERVENED in the body (SDK: response.model_extra). AnthropicTransport.response_finish_reason maps that to content_filter so the loop runs its refusal handling instead of reasoning over the canned text; _derive_finish_reason uses it for the anthropic_messages branch. Mantle (openai.gpt-5.x) is documented by AWS as not supporting Guardrails on the Responses endpoint; the docs now say so instead of promising "all model invocations". Header mechanism proposed in #52312 by @JoaoMarcos44 (stale base, 7-file conflict, detection keyed on a Converse-only stopReason); reimplemented on current main. Live probe (local sink, SigV4 fake creds): before, no X-Amzn-Bedrock-* header on the InvokeModel request; after, headers present, SigV4 intact, INTERVENED → content_filter.
|
Thanks @JoaoMarcos44, the header mechanism was right and is now on main via #107815 (f845624), with credit in the commit and PR body. Why a reimplementation rather than a rebase: the base was ~17k commits behind with conflicts in 7 of 8 files, the Bedrock routing had moved to Closing as superseded by #107815. |
Closes #52179
Problema
Bedrock Guardrails configurados em
bedrock.guardraileram silenciosamente ignorados para todos os modelos Claude. A causa raiz: o SDK AnthropicBedrock usa a API InvokeModel, que não tem parâmetroguardrailConfigno corpo da requisição — apenas a Converse API tem. O config era lido corretamente (load_config()retornava o bloco), mas nunca era anexado à chamada real.Isso afeta todos os três caminhos de entrada:
POST /v1/chat/completions— Open WebUI, etc.)hermes -z)Causa raiz confirmada
O SDK AnthropicBedrock roteia Claude via
InvokeModel, não Converse — eInvokeModelnão tem campoguardrailConfig. Uma abordagem anterior (Option A) roteava Claude+guardrail para a Converse API como workaround, mas isso sacrificava prompt caching, thinking budgets e contexto de 1M tokens.Solução ()
Injetar o guardrail como headers HTTP (
X-Amzn-Bedrock-GuardrailIdentifier,X-Amzn-Bedrock-GuardrailVersion,X-Amzn-Bedrock-Trace) em cada requisição InvokeModel viaextra_headersdo SDK Anthropic. O AWS Bedrock processa esses headers antes de encaminhar ao modelo — enforcement idêntico aoguardrailConfigda Converse API, sem perder nenhuma feature do Claude.Claude sempre permanece no caminho
anthropic_messages/ InvokeModel — sem regressão em features. Modelos não-Claude continuam na Converse API inalterados.Arquivos modificados
hermes_cli/runtime_provider.pyanthropic_messagesagent/agent_init.py_bedrock_guardrail_headers; banner+ Guardrailsagent/chat_completion_helpers.pybedrock_guardrail_headersao transportagent/transports/anthropic.pyguardrail_intervened → content_filter; aceita conteúdo vazio no stop reasonagent/anthropic_adapter.pyextra_headerssem sobrescrever betas existenteshermes_logging.pytry/except ImportErrorparaconcurrent_log_handler(Windows)tests/agent/test_bedrock_integration.pyTestes
Novos testes adicionados:
TestBedrockGuardrailRouting— Claude+guardrail agora fica emanthropic_messages(não maisbedrock_converse)TestBedrockGuardrailHeaderInjection— 5 testes: headers presentes, trace, coexistência com fast-mode, forwarding pelo transportTestBedrockGuardrailStopReason— 3 testes: mapeamentoguardrail_intervened → content_filter, resposta vazia válidaVerificação manual (evidência do reporter)
Impacto em features