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:
- Authenticates the end user via the configured IDP (Auth0 in the example).
- Resolves the user’s grants from the JWT — no
grant_id in the application database.
- Runs an agent (
travel-bot) that has tools for Gmail (read), Calendar (write), and Stripe (refunds — gated on approval).
- 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)
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