Skip to content

[Enhancement]: Remote nemo_guardrails should check-then-forward coding-agent LLM and tool calls #830

Description

@vantu-fit

Affected area

Middleware or guardrails
Plugins
Documentation or examples

Problem or opportunity

The built-in remote nemo_guardrails backend is used in front of coding-agent
CLIs (Claude Code, Codex, OpenCode, and similar). Those agents send tool
definitions on the provider wire Relays already intercept
, then execute tools
through Relays managed tool callbacks (Bash, file edits, MCP, …).
The wires that matter in our embedding (NeMo Fabric, Relays >=0.7.2,<0.8):

  • Claude Code: Anthropic Messages (anthropic_messages), Anthropic tools —
    not OpenAI tools / tool_choice.
  • Codex: OpenAI Responses (openai_responses).
  • OpenCode / OpenAI-chat clients: Chat Completions, including OpenAI tools
    and tool_choice.
    Today that path breaks in two places:
  1. LLM intercept. Remote mode errors out when the Chat Completions request
    includes OpenAI tool definitions or tool_choice (does not support … yet).
    The turn never reaches Guardrails or _next. Equivalent tool-bearing
    Anthropic Messages and OpenAI Responses requests are also unusable if the
    remote path only understands Chat Completions. Coding-agent traffic cannot
    use the remote backend.
  2. Tool intercept. Remote mode supports managed tool_output but rejects
    tool_input because the stock Guardrails /v1/chat/completions contract
    does not activate pre-execution tool rails from submitted chat history.
    Relays fail-closes at config time rather than silently skipping the check.
    Result: arguments cannot be validated before the tool runs.
    A related gap: if LLM requests with tools are later forwarded via _next,
    output rails still need to run on the provider response (including
    streaming). Treating Guardrails as the LLM only works for tool-free chat.
    Some embeddings pin Relays >=0.7.2,<0.8. A fix that exists only on 0.8+
    main does not reach those runtimes.

Proposed enhancement

Keep Guardrails as a remote HTTP service. Do not require mode = local
(nemoguardrails in-process) for this.

  1. Managed LLM (input / output)
  • When the intercepted LLM request includes tool definitions on any of
    Chat Completions, Anthropic Messages, or OpenAI Responses: run a
    messages-only Guardrails input check (translate to the Guardrails HTTP
    check if needed), then call _next with the original request. Do not
    strip tool fields. Do not send the tool-bearing provider body to Guardrails
    as if Guardrails were the model.
  • After _next, run Guardrails output rails on the provider result.
    Streaming must not emit tokens that a blocking output rail would refuse
    (buffer / fail closed; no stream_first leak).
  • Tool-free requests may keep the current Guardrails-as-LLM proxy.
  1. Managed tools (tool_input / tool_output)
  • Keep tool_output: check the result after _next(tool).
  • Make remote tool_input enforcing: check tool name and arguments
    before _next(tool). Timeout or Guardrails errors fail closed (do not
    run the tool).
  • Only allow tool_input = true in remote config once that check actually
    runs rails. Do not remove the config reject while the stock HTTP path would
    no-op.
  • Document the HTTP payload Guardrails must honor (the synthetic chat
    history the remote backend already builds, or a dedicated check contract).
    If Guardrails 0.23 cannot activate those rails on /v1/chat/completions,
    this issue includes the Relays + Guardrails contract work. Remote consumers
    use nemoguardrails[server] 0.23.x, not in-process 0.22.0.
  1. Release
  • Ship on Relays 0.7.x (patch/backport) as well as current main.
  • Keep existing remote TOML keys (mode, codec, input, output,
    tool_input, tool_output, remote.endpoint, remote.config_id,
    timeout_millis). Forward config_id to the Guardrails server.

Runtime contract and binding impact

Rust core nemo_guardrails remote intercepts for llm.execute, LLM
streaming, and tools.execute on the wires Relays already manages:
openai_chat, anthropic_messages, and openai_responses. Other language
bindings inherit the same managed surfaces; no new public binding APIs.
Do not document remote LLM as Chat Completions-only. A request already in
Anthropic Messages or OpenAI Responses must be input-checked and forwarded
on that wire. tool_input = true is a valid remote setting only after
enforcement is proven.

Alternatives considered

  • Fix Chat Completions tools/tool_choice only. Unblocks OpenCode-shaped
    traffic; Claude Code and Codex remain stuck on Anthropic Messages /
    OpenAI Responses.
  • Forward LLM tool requests via _next but skip output rails and tool_input.
    Unblocks the current 400; tools still run unchecked; streamed output can leak.
  • Keep rejecting tools / tool_choice. Coding agents cannot use remote
    mode.
  • Strip tools from the request. The agent can no longer call tools.
  • mode = local. Requires Python 3.11+ and pinned nemoguardrails==0.22.0
    beside every Relays process; hard to operate when policy already lives in a
    shared Guardrails service.
  • Allow tool_input = true without a working Guardrails contract.
    Fail-open; worse than the current explicit reject.
  • 0.8+ only. Misses runtimes locked to Relays 0.7.x.

Acceptance criteria

  • A remote LLM request with tools on Chat Completions, Anthropic Messages,
    and OpenAI Responses
    can be input-checked, then forwarded to _next with
    those fields unchanged. A blocking input rail never calls _next.
  • After _next, a blocking output rail does not reach the client,
    including on streaming.
  • Tool-free remote LLM requests still use the existing Guardrails-as-LLM proxy
    unless docs say otherwise.
  • Remote tool_input = true is accepted by config validation. A blocked tool
    does not execute. Unsafe arguments are rejected or rewritten per rails.
    tool_output can block or redact results.
  • Guardrails timeout / 5xx on LLM or tool checks fail closed.
  • remote.config_id is forwarded. Existing remote TOML keys keep working.
  • Unit tests cover: LLM tools allow and block on each of the three wires;
    output block after _next; tool_input block-before-execute; tool_output
    block/redact; timeout.
  • Behavior is available on a 0.7.x release line, not only 0.8 main.
  • Docs update the remote vs local table and spell out the tool-bearing LLM
    path vs the tool-free proxy, plus the remote tool_input contract, including
    Anthropic Messages and OpenAI Responses — not Chat Completions only.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions