Connecting it

The same checks, wherever your agent runs. In each, your code runs the tool, never the guardrail, and only after the guardrail allowed it.

In plain Python

from stalwart_agents.guardrails import Guardrail

rail = Guardrail(tools=tools, rules="compiled", tool_kinds="tool_kinds.yaml")
verdict = rail.check_call(call, {"turns": turns, "results": results, "session": sid})

call may be our shape ({"tool", "arguments"}), an OpenAI tool call, an Anthropic tool_use block or a LangChain ToolCall. Run it only where verdict.proceed. See Building a guardrail for the confirmation round trip.

The context the checks read:

Key What it is
turns the person's turns, latest last: grounding and the checks of what was said read them
results what tools returned, by call or tool, so a value in a write can be traced to them
session keeps one conversation's history apart: the order rules and the session record read it
confirmation the token approve gave for the person's yes
language the turns' language, en or pt-BR, where it is not the guardrail's own (turns_in)
labels, origins, plan, plan_token, usage trust labels, where values came from, an approved plan, and usage counts

In front of MCP tools: the gateway

stalwart gateway sits between your agent and your MCP servers, and checks every tools/call before it reaches a server. snippet("gateway", upstream=...) in stalwart mcp writes catalogue.json (your tools, their kinds and your rules' overlay) and a gateway.yaml to start from:

$ stalwart gateway --config gateway.yaml

A call that needs a person becomes an MCP elicitation with the confirmation's sentence; a client that cannot be asked gets a refusal, never an unconfirmed write.

Inside Guardrails AI

$ pip install "stalwart-agents[guardrails]"
from guardrails import Guard
from stalwart_agents.guardrails import gai

guard = Guard().use(gai.ToolCall(tools=tools, rules="compiled", on_fail=gai.ask))

for call in response.tool_calls:                    # OpenAI or Anthropic shape, LangChain's, or a dict
    outcome = guard.validate(gai.as_json(call), metadata={"stalwart": {"turns": turns, "results": results,
                                                                       "session": sid}})
    if outcome.validation_passed:
        results[call.id] = run_tool(call)
    elif (payload := gai.asked(guard)) is not None:  # show payload["sentence"]; on the person's yes:
        outcome = guard.validate(gai.as_json(call), metadata={"stalwart": {
            "turns": turns, "results": results, "session": sid, "confirmation": gai.approve(payload)}})

Validate each call; do not wrap the model. Guardrails AI hands its validators the text a model wrote. When it wraps a model call, a validator receives only the last tool call's arguments, without the tool's name, on every version in the range we support (0.7 to 0.11). So each call the model proposes is validated on its own, before your code runs it.

The validators:

Validator Name Runs
ToolCall stalwart/tool_call every check
GroundedArguments stalwart/grounded_arguments grounding
PolicyRules stalwart/policy_rules your rule sheet
AllowedTool stalwart/allowed_tool the allowed tools

The checks that cannot be switched off run in each of them, so a write waits for a person in each.

What each answer does:

The context comes from metadata["stalwart"] (the keys above). A write checked with no turns fails, naming the missing key: closed, never a silent pass. Our validators never change a call and never call a model, so fix and reask change nothing: a refused call stays refused.

Several validators on one guard: Guard().use_many(theirs, ours) up to 0.10, Guard().use(theirs, ours) from 0.11.

Guardrails AI itself sends usage metrics unless ~/.guardrailsrc says enable_metrics=false. Nothing of ours does.

On NeMo Guardrails

$ pip install "stalwart-agents[nemo]"
from nemoguardrails import LLMRails, RailsConfig
from stalwart_agents.guardrails import Guardrail
from stalwart_agents.guardrails.nemo import register, snippet

rails = LLMRails(RailsConfig.from_path("config"))
register(rails, Guardrail(tools=tools, rules="compiled"))
print(snippet("2.x"))          # or "1.0"

register adds one action, stalwart_check_tool (StalwartCheckToolAction in Colang 2). The snippet is the guard shape the verifier proves: the tool action runs only inside the branch where the check allowed it; a call that waits for a person asks them, and on their yes is checked again with the token; every other branch ends in the refusal sentence. Rename the intent and the tool for your own; keep the shape.

What the action reads from NeMo's context, where its own arguments do not give it:

Key Read as
stalwart_arguments the call's arguments (set it from the action that proposes the call)
user_message, else last_user_message the person's latest turn
stalwart_turns the person's turns, latest last, in place of the latest alone
stalwart_results what tools returned
stalwart_session the conversation (nemo where unset)
stalwart_confirmation the token of the person's yes

A write checked with no turn is refused, never allowed. In Colang 2, bot say waits for your client to report the utterance finished before the flow goes on, as a chat loop does.

NeMo's own Guardrails AI integration validates only the user's and the bot's messages, never tool calls, so our validators are used through register above, not through it.