Harness Decorator
@harness decorator — the primary integration pattern for governed agents.
Overview
The @harness decorator (alias: validate) wraps an agent function in a two-phase gate:
- Pre-check —
POST /v1/runtime/pre-check, before your function body runs. Ablockhere raisesBlockedErrorand the function body never executes, so the real downstream system is never touched. - Your function runs — only reached when the pre-check does not block.
- Post-check —
POST /v1/runtime/post-check, after your function returns. This governs whether the caller ever sees the result; it cannot undo what the body already did.
It supports sync and async functions, configurable block behaviour, and transparent
pass-through when AGENTRUST_ENABLED=false.
`@harness` swallows transport errors by default
Both phases require a gateway that serves /v1/runtime/pre-check and
/v1/runtime/post-check — the Edge gateway and the embedded gateway both do.
If either call fails, the decorator logs a warning and runs your function anyway. It
catches every exception except BlockedError, so AGENTRUST_FAILURE_MODE=closed is not
enough on its own: the GatewayUnavailableError it produces is caught by the decorator
and discarded. To make a governance outage stop the call, you need both:
@harness(raise_on_error=True) # re-raise transport errors instead of proceeding
def my_agent(user, input): ...export AGENTRUST_FAILURE_MODE=closed # turn an unreachable gateway into an exceptionUse that pairing while integrating, so a wrong AGENTRUST_GATEWAY_URL fails loudly
instead of looking like a healthy system.
Why It Matters
@harness is the most explicit and auditable integration pattern — ideal for new agent functions where governance boundaries should be visible in code.
Prerequisites
pip install "agentrust-py[embedded,retry]"Step-by-Step Guide
1. Basic usage
from agentrust_sdk import harness
@harness
def my_agent(user: str, input: str) -> dict:
return {"answer": call_llm(input)}
result = my_agent(user="alice", input="Hello")2. Async support
@harness
async def async_agent(user: str, input: str) -> dict:
return {"answer": await call_llm_async(input)}3. Configure agent metadata
@harness(agent_id="payment-agent", action="transfer_funds", framework="Custom")
def payment_agent(user, input):
return process_payment(input)agent_id and action both default to the wrapped function's name. action is what the
pre-check matches against the agent's declared tools in its manifest.
4. Full parameter reference
| Parameter | Default | Purpose |
|---|---|---|
agent_id | function name | Identifier used for audit, analytics, and reliability |
action | function name | Tool/action name checked against the declared-tools manifest |
user_kwarg | "user" | Which keyword argument carries the end-user identity |
input_kwarg | "input" | Which keyword argument carries the input text |
base_url | env / config | Gateway base URL override |
api_key | env / config | API key override |
block_on_block | True | Raise BlockedError on a block outcome |
block_on_review | False | Also raise on escalate / request_evidence |
raise_on_tier_gate | False | Raise TierGateError instead of warning |
framework | auto-detected | Framework label recorded on the envelope |
raise_on_error | False | Re-raise transport errors instead of proceeding |
5. Block behaviour
from agentrust_sdk import BlockedError
@harness(block_on_block=True, block_on_review=True)
def high_stakes_agent(user, input):
return {"action": "execute"}
try:
high_stakes_agent(user="alice", input="run")
except BlockedError as e:
print(e.outcome) # "block" | "escalate" | "request_evidence"
print(e.reason) # why the decision engine chose it
print(e.envelope_id) # audit record to quote in a support ticket6. Reading the governance result
@harness returns your function's return value unchanged — there is no option to make it
return the ValidateResponse alongside it. When you need the scores, call the client
directly instead:
from agentrust_sdk import AgentTrustClient
with AgentTrustClient() as client:
result = client.validate(
agent_id="agent-with-metadata", user="alice",
input="test", output={"ok": True},
)
print(result.decision.outcome, result.validation.final_confidence)Or read the audit trail afterwards with agentrust audit tail.
7. Positional arguments
The decorator reads the user and input from keyword arguments (user_kwarg /
input_kwarg). Call your agent with keywords — my_agent(user="alice", input="…") — or
the envelope records user="unknown" and an empty input. Every argument, positional or
keyword, is still bound by name and sent to the pre-check as params.
Examples
FastAPI route handler:
from contextlib import asynccontextmanager
from fastapi import FastAPI
from agentrust_sdk import harness, embed_gateway
@asynccontextmanager
async def lifespan(app: FastAPI):
gw = embed_gateway() # dev only — point at the Edge gateway in production
yield
gw.stop()
app = FastAPI(lifespan=lifespan)
# @harness must sit closest to the function: FastAPI registers whatever
# @app.post receives, so the route must wrap the already-governed callable.
@app.post("/agent")
@harness(agent_id="api-agent", framework="REST")
def run_agent(user: str, input: str):
return {"result": process(input)}Best Practices
- Set explicit
agent_idfor production agents - Use
block_on_block=Truefor financial, healthcare, or data-access agents - Combine with
embed_gateway()in dev; remote gateway in production - Return structured dicts (not raw strings) for schema validation
- Call decorated agents with keyword arguments so
userandinputare captured
Common Mistakes
- Decorating non-agent utility functions (adds unnecessary latency)
- Not handling
BlockedErrorin user-facing code paths - Returning non-JSON-serializable objects as output
- Passing
user/inputpositionally — they are read from keyword arguments - Ordering the decorators so
@harnesssits above the route decorator
Troubleshooting
| Issue | Fix |
|---|---|
| Decorator is no-op | Check AGENTRUST_ENABLED=false |
| Governance appears to be skipped | Combine @harness(raise_on_error=True) with AGENTRUST_FAILURE_MODE=closed — the decorator otherwise catches and logs the failure, then proceeds |
Envelope shows user="unknown" | Call with keyword arguments, or set user_kwarg / input_kwarg |
| Every call blocked | Review the policy pack; inspect BlockedError.reason |
| TypeError on async | @harness supports async def natively — do not wrap in asyncio.run |