Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.alterauth.com/llms.txt

Use this file to discover all available pages before exploring further.

This guide assumes familiarity with Concepts → Principals and grants, OAuth → JWT identity, and Agents → Overview. It puts those pieces together in one working example.

What this guide builds

A FastAPI backend with one endpoint, /chat, that:
  1. Authenticates the end user via the configured IDP (Auth0 in the example).
  2. Resolves the user’s grants from the JWT — no grant_id in the application database.
  3. Runs an agent (travel-bot) that has tools for Gmail (read), Calendar (write), and Stripe (refunds — gated on approval).
  4. Logs every tool call with the agent name, tool name, and run ID.
The agent is a managed agent provisioned in the developer portal. Its keys are minted there. The application is a single Python process — using shape (b) from Agents → Overview: the app key plus App.get_agent(uuid).

Prerequisites

  • An Alter app created in the portal.
  • An IDP configured for that app — see Auth0 setup.
  • A managed agent provisioned with the name travel-bot.
  • Stripe and Google OAuth providers configured for the app.
  • Approval policy configured: any POST to api.stripe.com/v1/refunds requires approval. See Reference → Policies.

Wiring the SDK

import os
from contextvars import ContextVar
from fastapi import FastAPI, Depends, HTTPException, Request
from alter_sdk import App, HttpMethod
from alter_sdk import PendingApproval

app_api_key = os.environ["ALTER_APP_KEY"]

# JWT plumbed via ContextVar so user_token_getter can find it.
_jwt_ctx: ContextVar[str | None] = ContextVar("jwt", default=None)

vault = App(
    api_key=app_api_key,
    user_token_getter=lambda: _jwt_ctx.get() or "",
)

# Boot-time: discover the managed agent's UUID. Stable for the lifetime of the process.
travel_bot_id: str | None = None

api = FastAPI()

@api.on_event("startup")
async def _bootstrap():
    global travel_bot_id
    info = await vault.agents.get_by_name("travel-bot")
    if info is None:
        raise RuntimeError("Managed agent 'travel-bot' not found")
    travel_bot_id = str(info.id)
vault is the operator-side App. An Agent is also constructed per request so audit attribution lands on travel-bot rather than the App.

The endpoint

@api.post("/chat")
async def chat(request: Request, message: dict):
    # 1. Pull the user's JWT.
    auth = request.headers.get("authorization", "")
    if not auth.startswith("Bearer "):
        raise HTTPException(401)
    jwt = auth.removeprefix("Bearer ")
    _jwt_ctx.set(jwt)

    # 2. Construct the agent. Same backend identity as a per-agent key,
    #    but the master key signs (shape (b)).
    agent = vault.get_agent(travel_bot_id)

    # 3. Bind a run_id so all tool calls in this request share an audit trail.
    async with agent.trace(run_id=request.headers.get("x-request-id", "unknown")):
        return await _run_agent_step(agent, message)

Tool calls

async def _list_emails(agent):
    response = await agent.request(
        HttpMethod.GET,
        "https://gmail.googleapis.com/gmail/v1/users/me/messages",
        provider="google",
        query_params={"maxResults": "10"},
        context={"tool": "list_emails"},
    )
    return response.json()

async def _create_event(agent, summary: str, start: str, end: str):
    response = await agent.request(
        HttpMethod.POST,
        "https://www.googleapis.com/calendar/v3/calendars/primary/events",
        provider="google",
        json={"summary": summary, "start": {"dateTime": start}, "end": {"dateTime": end}},
        context={"tool": "create_event"},
    )
    return response.json()

async def _refund(agent, charge_id: str, amount: int, ticket: str):
    # Sensitive call — goes through proxy_request so policy can gate it.
    result = await agent.proxy_request(
        HttpMethod.POST,
        "https://api.stripe.com/v1/refunds",
        grant_id=os.environ["STRIPE_GRANT_ID"],
        json_body={"charge": charge_id, "amount": amount},
        reason=f"Customer ticket {ticket}",
        context={"tool": "refund"},
    )
    if isinstance(result, PendingApproval):
        # Operator decides in the portal / Slack / Wallet.
        result = await agent.await_approval(result.approval_id, timeout=600)
    return result.body
Notes:
  • Gmail and Calendar use provider="google" because the user authorized Google for themselves — JWT identity resolution finds the right grant.
  • Stripe uses an explicit grant_id because it’s a managed secret bound to the app, not the user.
  • The refund call uses proxy_request so backend-side policy can gate it on approval.

What the audit log shows

For every call:
  • principal: the end user (resolved from JWT).
  • caller: travel-bot’s managed-agent UUID (audit attribution; not an access boundary change — JWT wins).
  • context: {"tool": "...", "run_id": "..."}.
  • status: 200 / 4xx / approval_pending / approval_denied / etc.
Filter the audit log by caller to answer “what did travel-bot do?” Filter by principal to answer “what was Alice’s data used for?”

See also