Aira

Inline authorize() gate

The universal pattern for gating agent actions. Works with any framework, including ones Aira labels audit-only. Call aira.authorize() before the side effect, check auth.status, act only when authorized.

The pattern in one sentence. Call aira.authorize() at the top of your tool function, before any side effect. If auth.status != "authorized", bail out. If it is, do the work, then call aira.notarize() with the outcome. Framework-agnostic. Works everywhere.

Why this page exists

Aira ships first-class integrations for LangChain, Vercel AI, OpenAI Agents, Bedrock, Google ADK, CrewAI, and MCP. Five of those integrations are real pre-execution gates — they wrap the framework's own hook and block denied calls automatically. You install the extra, attach the handler, and every tool is gated.

But one integration — CrewAI — is audit-only, because CrewAI exposes no upstream pre-execution hook to intercept. For CrewAI agents, the task_callback and step_callback fire after the work already ran.

Also, sometimes you want to gate a specific branch inside a tool, or a non-tool side effect, or a custom runner that isn't one of the integrations we ship. In any of those cases, you write the gate yourself. It's ten lines of code and works with any framework.

This page is the canonical pattern.

The pattern

inline_gate.py
from aira import Aira, AiraError

aira = Aira(api_key="aira_live_...")

def send_wire_transfer(amount: float, to: str) -> str:
    """
    Real-world side effect, gated inline.

    The gate is three parts:
      1. authorize() BEFORE any side effect
      2. check auth.status — authorized / pending_approval / implicit denial
         via POLICY_DENIED exception
      3. notarize() AFTER with outcome="completed" or "failed"
    """
    # 1. Authorize before acting
    try:
        auth = aira.authorize(
            action_type="wire_transfer",
            details=f"Send €{amount} to {to}",
            agent_id="payments-agent",
            # optional: everything below strengthens the audit trail
            model_id="claude-sonnet-4-6",
            instruction_hash="sha256:abc123...",
        )
    except AiraError as e:
        if e.code == "POLICY_DENIED":
            # Policy engine explicitly denied. Surface a clean error
            # to whatever caller is asking — agent, chain, UI.
            return f"Denied by Aira policy: {e.message}"
        # Network / 5xx — fail closed or open depending on your taste
        raise

    # 2. Check the non-denial outcomes
    if auth.status == "pending_approval":
        return (
            f"Held for human approval. Action id: {auth.action_uuid}. "
            "The agent should pause or retry once approved."
        )
    # Anything else is "authorized" — we're good to act

    # 3. Do the side effect, with a try/finally so we always notarize
    try:
        tx_id = stripe.transfers.create(amount=int(amount * 100), destination=to)
    except Exception as e:
        aira.notarize(
            action_uuid=auth.action_uuid,
            outcome="failed",
            outcome_details=f"{type(e).__name__}: {str(e)[:200]}",
        )
        raise

    aira.notarize(
        action_uuid=auth.action_uuid,
        outcome="completed",
        outcome_details=f"stripe_tx={tx_id}",
    )
    return f"Wire sent: €{amount} to {to}"

That's it. Every call to send_wire_transfer now:

  • Runs your policies first (rules / AI / consensus / content scan)
  • Blocks denied calls so the wire never happens
  • Holds for human review when a policy says so
  • Emits a signed receipt committing both the intent and the outcome
  • Fails closed on execution errors so your audit trail reflects reality

TypeScript version

inlineGate.ts
import { Aira, AiraError } from "aira-sdk";

const aira = new Aira({ apiKey: "aira_live_..." });

export async function sendWireTransfer(amount: number, to: string): Promise<string> {
  // 1. Authorize before acting
  let auth;
  try {
    auth = await aira.authorize({
      actionType: "wire_transfer",
      details: `Send €${amount} to ${to}`,
      agentId: "payments-agent",
      modelId: "claude-sonnet-4-6",
    });
  } catch (e) {
    if (e instanceof AiraError && e.code === "POLICY_DENIED") {
      return `Denied by Aira policy: ${e.message}`;
    }
    throw e;
  }

  // 2. Check the non-denial outcomes
  if (auth.status === "pending_approval") {
    return `Held for human approval. Action id: ${auth.actionId}.`;
  }

  // 3. Do the side effect, notarize either outcome
  try {
    const tx = await stripe.transfers.create({ amount, destination: to });
    await aira.notarize({
      actionId: auth.actionId,
      outcome: "completed",
      outcomeDetails: `stripe_tx=${tx.id}`,
    });
    return `Wire sent: €${amount} to ${to}`;
  } catch (e) {
    const err = e as Error;
    await aira.notarize({
      actionId: auth.actionId,
      outcome: "failed",
      outcomeDetails: `${err.name}: ${err.message.slice(0, 200)}`,
    });
    throw e;
  }
}

When to use this vs the framework integrations

Use a framework integration when…Use inline authorize() when…
You're using LangChain, Vercel AI, OpenAI Agents, Bedrock, or Google ADK — all real pre-execution gatesYou're using CrewAI (audit-only — inline is the only way to get real gating)
You want blanket gating on every tool with no code changes in the tool bodiesYou need to gate a specific branch inside a tool, or a non-tool side effect
Your tool details string can be generic ("Tool 'X' called with arg keys [a, b]")You need the details string to include actual argument values
You want the framework integration to own the action_uuid lifecycleYou want the action_uuid in your tool body for logging, correlation ids, etc.

The two patterns compose. You can use the LangChain callback handler for blanket gating on most tools AND drop inline aira.authorize() calls inside specific high-stakes tools for extra business-logic gates. The receipts chain correctly via parent_action_uuid.

Anti-patterns to avoid

Calling notarize() without authorize() first

Notarize is a free bookkeeping call — it can't mint a receipt for an action_uuid that doesn't exist. Every successful notarize must be backed by a prior authorize. The endpoint returns 404 for unknown action ids and 409 for actions that aren't in the authorized or approved state.

Putting the side effect before the authorize call

# WRONG — receipt committed but the action already happened
tx_id = stripe.transfers.create(...)
auth = aira.authorize(action_type="wire_transfer", ...)  # too late

The whole point is that denied actions never run. If you call the side effect first, you've lost the gate property — you just have an audit log, not authorization.

Swallowing POLICY_DENIED silently

# WRONG — agent thinks the call succeeded
try:
    auth = aira.authorize(...)
except AiraError:
    pass  # don't do this
tx_id = stripe.transfers.create(...)

If authorize() raised, you don't have a valid action_uuid and the side effect should not run. Propagate the error (or return a clean denial string if you're in a tool function that needs to return a message to the agent).

Forgetting to notarize on failure

# INCOMPLETE — authorized ops that failed leave the action in "authorized" state forever
auth = aira.authorize(...)
tx_id = stripe.transfers.create(...)  # raises
aira.notarize(action_uuid=auth.action_uuid, outcome="completed", ...)  # never runs

Use a try/finally so both success and failure paths notarize. The action transitions to notarized (success) or failed (error) and the audit trail matches reality.

Composing with CrewAI's audit hook

On CrewAI you can use both patterns at once — the audit hook records step/task completions for the whole crew, and inline gates run on the specific tools that need to be blocked:

from aira import Aira
from aira.extras.crewai import AiraCrewHook
from crewai import Agent, Crew, Task, tool

aira = Aira(api_key="aira_live_...")

# Audit hook — records every step + task completion
audit_kwargs = AiraCrewHook.for_crew(aira, "research-crew")

# Inline gate on the high-stakes tool
@tool("send_wire_transfer")
def send_wire_transfer(amount: float, to: str) -> str:
    # paste the pattern from above
    ...

crew = Crew(
    agents=[Agent(role="payer", tools=[send_wire_transfer])],
    tasks=[Task(description="Pay vendor-x €75K", agent=...)],
    **audit_kwargs,
)
crew.kickoff()

You get:

  • A post-hoc audit record for every step (from the callback hook)
  • A real pre-execution gate specifically on send_wire_transfer (from the inline pattern)

On this page