SDK

Harness Decorator

@harness decorator — the primary integration pattern for governed agents.

Harness Decorator

Overview

The @harness decorator (alias: validate) wraps an agent function in a two-phase gate:

  1. Pre-checkPOST /v1/runtime/pre-check, before your function body runs. A block here raises BlockedError and the function body never executes, so the real downstream system is never touched.
  2. Your function runs — only reached when the pre-check does not block.
  3. Post-checkPOST /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 exception

Use 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

ParameterDefaultPurpose
agent_idfunction nameIdentifier used for audit, analytics, and reliability
actionfunction nameTool/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_urlenv / configGateway base URL override
api_keyenv / configAPI key override
block_on_blockTrueRaise BlockedError on a block outcome
block_on_reviewFalseAlso raise on escalate / request_evidence
raise_on_tier_gateFalseRaise TierGateError instead of warning
frameworkauto-detectedFramework label recorded on the envelope
raise_on_errorFalseRe-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 ticket

6. 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_id for production agents
  • Use block_on_block=True for 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 user and input are captured

Common Mistakes

  • Decorating non-agent utility functions (adds unnecessary latency)
  • Not handling BlockedError in user-facing code paths
  • Returning non-JSON-serializable objects as output
  • Passing user / input positionally — they are read from keyword arguments
  • Ordering the decorators so @harness sits above the route decorator

Troubleshooting

IssueFix
Decorator is no-opCheck AGENTRUST_ENABLED=false
Governance appears to be skippedCombine @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 blockedReview the policy pack; inspect BlockedError.reason
TypeError on async@harness supports async def natively — do not wrap in asyncio.run