A working FastAPI app that shows every AgentWrit capability against a live broker —
dynamic agents, per-patient scope isolation, cross-patient denial, delegation, renewal, release, and a tamper-evident audit trail.
What it is · Why it exists · What you'll see · Run it · How it works · Code map · More
MedAssist AI is a small clinical-assistant app. You type a patient ID and a plain-language question. An LLM decides which tools to call (records, labs, billing, prescriptions). The app spawns broker-backed agents on demand, each scoped to one patient and one category of work, and every step shows up in a live execution trace — scope checks, denials, delegations, renewals, release.
If you've ever wondered "what does short-lived, task-scoped, per-user credentialing actually look like in a real app?" — this is that app.
Reading about ephemeral credentials is one thing. Watching three agents get spawned, one of them get denied mid-request because it asked about the wrong patient, and then seeing the whole chain die when the encounter ends — that's what makes the pattern stick.
We built MedAssist AI because:
- Beginners need a story. "Scoped JWTs" is abstract. "The clinical agent can only read Patient 1042's records, and when it tries Patient 2187 the broker says no" is concrete.
- Reviewers need evidence. The audit tab shows a hash-chained ledger of every broker event, which is what a security reviewer wants to see before approving production use.
- Contributors need a reference. Every SDK feature —
create_agent,validate,delegate,renew,release,scope_is_subset— is wired in here, used the way it's meant to be used.
| Capability | What the demo does |
|---|---|
| Dynamic agent creation | Agents spawn as the LLM picks tools. No pre-allocated pool. |
| Per-patient scope isolation | Each agent's scope contains one patient ID and nothing else. |
| Cross-patient denial | Ask about another patient mid-encounter. The scope check fails. The trace shows scope_denied. |
| Delegation with attenuation | The clinical agent delegates write:prescriptions:{patient} to the prescription agent. The broker refuses to widen. |
| Token lifecycle | renew() issues a fresh token under the same SPIFFE identity. release() kills the token immediately. |
| Audit trail | A dedicated tab shows every broker event in a hash chain that can't be retroactively altered. |
The trace panel in the UI is the point. Every capability surfaces as a line in the trace so you can read the whole story of one request.
One command, no Python setup:
AGENTWRIT_ADMIN_SECRET="your-secret" \
LLM_API_KEY="your-llm-key" \
docker compose up -d broker medassistOpen http://localhost:5000. The demo auto-registers itself with the broker on startup.
You need an OpenAI-compatible LLM endpoint. If you're not using OpenAI, set LLM_BASE_URL and LLM_MODEL in your shell before docker compose up — e.g. a local vLLM or llama.cpp server.
For when you want to edit the code:
# 1. Start the broker
docker compose up -d broker
# 2. Register the demo app (one time — writes client_id/client_secret)
export AGENTWRIT_ADMIN_SECRET="your-admin-secret"
uv run python demo/setup.py
# 3. Configure demo/.env
cp demo/.env.example demo/.env
# then fill in AGENTWRIT_CLIENT_ID, AGENTWRIT_CLIENT_SECRET, LLM_BASE_URL, LLM_API_KEY, LLM_MODEL
# 4. Run it
uv run uvicorn demo.app:app --reload --port 5000- Pick a patient from the dropdown.
- Ask something simple: "What are this patient's recent labs?" Watch agents spawn, watch each tool check scope, watch the final response render.
- Ask a crossing question: "And show me Patient 2187's records too." Watch the scope check fail. Read the
scope_deniedline in the trace. - Open the Audit tab. Every event is there, hash-chained.
The demo is built on one rule: the app never trusts the LLM for security. The LLM picks tools. The broker decides what credentials exist. The app enforces tool access against those credentials with scope_is_subset() before every call.
User types a request
↓
FastAPI receives it
↓
LLM chooses a tool (records / labs / billing / prescription)
↓
App asks: "Do I have an agent for this category yet?"
↓ no ↓ yes
Broker creates one, Reuse it
scoped to this patient
↓
App checks: scope_is_subset(tool-requires, agent-holds)?
↓ yes ↓ no
Run the tool Emit scope_denied, tell LLM "access denied"
↓
Return result to LLM. Repeat until LLM is done.
↓
App releases every agent. Tokens are dead.
Every branch of this flow appears in the execution trace. The trace is the documentation.
For the full walkthrough — sequence diagrams, how delegation flows from the clinical agent to the prescription agent, and what each UI panel shows — read the Beginner's Guide. For a scripted live presentation, read the Presenter's Guide.
| Piece | File |
|---|---|
| FastAPI entry point | app.py |
| Env config (broker + LLM) | config.py |
| Main API loop (LLM, agent spawning, trace) | routes/api.py |
| Page routes (encounter, audit, operator) | routes/pages.py |
| Tool definitions + scope templates | pipeline/tools.py |
| Mock patient and formulary data | data/ |
| Frontend (trace, markdown render) | static/app.js, static/style.css |
| One-shot app registration helper | setup.py |
Read routes/api.py first. That's where the agent-creation-and-scope-check loop lives, and everything else supports it.
| Go here for | Link |
|---|---|
| Step-by-step beginner walkthrough with diagrams | BEGINNERS_GUIDE.md |
| Live presentation script (timing, transitions) | PRESENTERS_GUIDE.md |
| SDK concepts (roles, scopes, delegation) | ../docs/concepts.md |
| Building real apps with the SDK | ../docs/developer-guide.md |
| Broker API (source of truth) | AgentWrit broker docs |