Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
8232aec
Design spec: Delegated Authorization for AI Agents workshop
sohanmaheshwar Aug 27, 2026
5a53d9e
Implementation plan: Delegated Authorization for AI Agents workshop
sohanmaheshwar Aug 27, 2026
c1867be
feat(workshop): starter scaffolding, provided plumbing, CP1 stubs, ve…
sohanmaheshwar Aug 27, 2026
f48c93d
fix(workshop): drop rollback preset, point goose-extension at verify.py
sohanmaheshwar Aug 27, 2026
4e12589
docs(workshop): 0-setup
sohanmaheshwar Aug 27, 2026
d6a2d87
docs(workshop): 0-setup fix round 1 — CP2 schema wording, venv in dev…
sohanmaheshwar Aug 27, 2026
a0818ab
docs(workshop): CP1 run-the-agent
sohanmaheshwar Aug 27, 2026
5387aeb
docs(workshop): CP2 delegated-authorization
sohanmaheshwar Aug 27, 2026
b3b8cbe
docs(workshop): CP3 time-bound-and-revocable
sohanmaheshwar Aug 27, 2026
ea47988
docs(workshop): CP3 fix round 1 — web-UI grant card disappears (drop …
sohanmaheshwar Aug 27, 2026
b175857
docs(workshop): CP4 relationship-based-hierarchy
sohanmaheshwar Aug 27, 2026
b27c200
docs(workshop): README, next steps, end-to-end validated
sohanmaheshwar Aug 27, 2026
8a4b7cb
fix(workshop): final review — CP3 reset note, Codespaces devcontainer…
sohanmaheshwar Aug 27, 2026
ed0ac53
docs(workshop): humanizer + my-writing prose polish across all checkp…
sohanmaheshwar Aug 28, 2026
51df6c8
Web-UI-only operation; relationship expiration is GA
sohanmaheshwar Aug 28, 2026
8fa2848
docs(workshop): make the two-path structure explicit
sohanmaheshwar Aug 31, 2026
80e706f
docs(workshop): add newcomer glosses; rename Checkpoint → Part
sohanmaheshwar Aug 31, 2026
79d56c3
docs(workshop): rename Checkpoint → Part in starter stub comments
sohanmaheshwar Aug 31, 2026
e6de87d
docs(workshop): add FIG.01 architecture diagram to 0-setup
sohanmaheshwar Sep 1, 2026
abcf053
docs(workshop): tighten intro + next-steps (ReBAC framing, AuthZed Cl…
sohanmaheshwar Sep 1, 2026
daf7077
chore(workshop): drop internal planning/spec docs from the workshop c…
sohanmaheshwar Sep 1, 2026
de94ff2
docs(workshop): tighten Part 1 prose
sohanmaheshwar Sep 1, 2026
6134605
docs(workshop): step-by-step goose configure walkthrough for the depl…
sohanmaheshwar Sep 1, 2026
be46af1
docs(workshop): re-apply goose configure walkthrough (lost in a local…
sohanmaheshwar Sep 1, 2026
036f62f
docs(workshop): add FIG.02 relationship graph to Part 2
sohanmaheshwar Sep 1, 2026
ec43ee0
docs(workshop): prose polish across setup, Part 1, Part 3, Part 4, an…
sohanmaheshwar Sep 1, 2026
6adf207
docs(workshop): remove SpiceDB logo from FIG.01 authorization-layer beam
sohanmaheshwar Sep 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 149 additions & 0 deletions delegated-agent-authorization/0-setup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# Introduction

In this workshop you will build a DevOps deploy Agent with fine-grained authorization: scoped grants, time-bound windows, instant
revocation, and a permission hierarchy where revoking a base grant cascades to everything that
depends on it. The purpose of the workshop is to understand why fine-grained authorization is required for AI Agents,
and how it can be implemented using ReBAC.

The `starter/` folder in this repo is stubbed on purpose: the plumbing (MCP
extension, docker-compose, the seed script, the web UI) is already there, and you'll
write the schema and the decision engine yourself across the parts to learn each of the concepts.

![Architecture diagram of the project](/delegated-agent-authorization/images/fig1-permission-check.svg)

## Two ways to drive the agent

As a reminder, you can complete every part in this workshop with just the web UI — no LLM key, no goose
install required. That's the primary path, and it's all you need.

Each part page also ends with an optional *drive it with goose* step: the same requests in natural language, through a real
LLM, hitting the exact same authorization boundary. Use this if you want to watch the agent work with a live LLM; skip it and you miss nothing, because the authorization decision is identical either way.

## Get the code

```bash
git clone https://github.com/authzed/workshops.git
cd workshops/delegated-agent-authorization/starter
```

## Installation

#### Option A - Run locally with Docker

1. Copy the example `.env` file:

```bash
cp .env.example .env
```

`.env` holds the SpiceDB connection details the app itself needs: endpoint, and preshared-token,
and which agent identity the deploy bot acts as.

2. Start the infrastructure:

```bash
docker compose up -d --wait
```

This brings up two containers, `postgres` (SpiceDB's datastore) and `spicedb`, plus a
short-lived `spicedb-migrate` container that runs SpiceDB's own datastore migration (setting up
its Postgres tables, not the `schema.zed` you'll write later) and exits. SpiceDB serves
on `localhost:50051` with a preshared key of `devtoken` (not recommended for prod, obviously).

3. Create a virtual environment and install dependencies:

```bash
python3 -m venv .venv && source .venv/bin/activate && pip install -r requirements.txt
```

#### Option B - Run in GitHub Codespaces

<!-- TODO: verify on a live Codespace before the conference -->

For anyone who can't run Docker locally, Codespaces is the path. This repo's devcontainer config
lives at `delegated-agent-authorization/starter/.devcontainer/devcontainer.json` (nested under
`starter/`, not at the repo root), which most Codespaces-creation flows don't auto-detect. Be
explicit about which folder you're opening:

1. On the repo page, click **Code ▸ Codespaces ▸ Create codespace on main**. Because the
devcontainer config isn't at the repo root, this may open a plain Codespace at the repo root
with no devcontainer applied, rather than the Python image this workshop expects.
2. Once the Codespace is up, check whether `delegated-agent-authorization/starter/.venv` already
exists. If it does, the devcontainer ran and did its job — skip to step 4.
3. If it doesn't, the devcontainer wasn't picked up automatically. In VS Code, open the Command
Palette and run **Dev Containers: Reopen in Container**, pointing it at
`delegated-agent-authorization/starter` (or open that folder directly and let VS Code prompt
you to reopen in its container). That runs the same `postCreateCommand` — creating `.venv`,
installing dependencies, and running `docker compose up -d --wait` — so the `.venv/bin/python`
path the goose step below relies on exists.
4. Once dependencies are installed and infra is up, `cd delegated-agent-authorization/starter` (if
you're not already there) and copy `.env.example` to `.env` as in Option A.

Codespaces gets you most of the way there. It isn't zero-config: confirm `.venv` and
`docker compose ps` both look right before moving on, and fall back to the manual Dev Containers
step above if they don't.

## Install goose and register the extension (optional)

Installing goose is optional for this workshop. Install goose if you want to drive the agent with natural language
("Deploy checkout to staging") and watch its tool calls resolve through SpiceDB live.

If you do want the goose path:

1. Install goose by following the [Agentic AI Foundation goose](https://github.com/aaif-goose/goose)
project's own install instructions, then run `goose configure` to pick an LLM provider and set
its API key — this is where the "LLM key" lives, in goose's own config, not in this project's
`.env`.
2. Register the `deploybot` MCP extension so goose can call this repo's deploy tools. goose launches
`deploybot_server.py` with your virtualenv's Python, so it needs the **absolute path** to both.
From `starter/`, print that path once:

```bash
pwd
```

Then run `goose configure` and answer the prompts (exact wording varies slightly by goose
version):

- **What would you like to configure?** → `Add Extension`
- **What type of extension would you like to add?** → `Command-line Extension`
- **What would you like to call this extension?** → `deploybot`
- **What command should be run?** → your venv Python and the server script, both as absolute
paths — take the `pwd` output above and append `/.venv/bin/python` and `/deploybot_server.py`:

```
/ABSOLUTE/PATH/to/starter/.venv/bin/python /ABSOLUTE/PATH/to/starter/deploybot_server.py
```

- **Please set the timeout for this tool (in secs):** → `300`
- **Would you like to add a description?** → `No`
- **Would you like to add environment variables?** → `Yes`, then add these three (goose asks for
a name, then a value, then "add another?" after each):

| Name | Value |
| --- | --- |
| `SPICEDB_ENDPOINT` | `localhost:50051` |
| `SPICEDB_TOKEN` | `devtoken` |
| `AGENT_SUBJECT` | `agent:goose_alice` |

`AGENT_SUBJECT` pins the agent's identity: every authorization check goose triggers runs as
`agent:goose_alice`. goose writes all of this into `~/.config/goose/config.yaml` — see
`goose-extension.md` for the equivalent YAML if you'd rather edit it by hand.

`goose-extension.md` also has a manual verification checklist for once goose is wired up. Worth
skimming now, but there's nothing to verify yet: SpiceDB has no authorization schema until
Part 2, where you write it and the agent's decisions (via goose or the web UI) first come
online. Part 1 is next, and it drives the agent from the web UI to watch it over-reach.

---

## Completion Milestone: Setup

- [ ] Cloned the repo
- [ ] Infrastructure is up — Docker (`docker compose up -d --wait`) or Codespaces
- [ ] `.venv` created and dependencies installed — manually in Option A, automatically by the
devcontainer in Option B
- [ ] (Goose path only) goose installed with an LLM provider configured, and the `deploybot`
extension registered per `goose-extension.md`

Next: [Part 1 — Run the agent](1-run-the-agent.md)
127 changes: 127 additions & 0 deletions delegated-agent-authorization/1-run-the-agent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# Part 1 — Run the Agent (and Watch It Over-Reach)

In this section we'll get the deploy agent running, try a few actions and catch it doing something it should
never be allowed to do. For example: this agent can tear down production servers since there are no permission checks to stop it from doing so.

---

## Going Live

The web UI is how you drive the agent throughout this workshop. It needs no LLM key: it turns your
text into the same tool calls goose would, and hands them to the same gated backend. From
`starter/`, with SpiceDB up:

```bash
python web.py
```

Open `http://127.0.0.1:8000`. Type something reasonable into the request box (or click one of the buttons)

> Deploy checkout to production.

The UI calls `deploy(service="checkout", environment="production")`. It comes back
**✅ ALLOWED**, and the version bumps. So far so good.

Now ask it to do something the agent should **not** be able to decide on its own:

> Tear down the production environment.

The UI calls `destroy(environment="production")`. It comes back **✅ ALLOWED**, and production environment is
gone. The tool's own docstring says destroying "requires elevated authority" — but there's no permission check to enforce it.
The stub doesn't look at `permission` or `environment_id` at all, so it can't tell a deploy from a destroy any more than it
can tell staging from production.

Nothing about the *prompt* told the agent to be reckless. Nothing about the *user's* request was
malicious: "deploy checkout to production" and "tear down the production environment" are both
plausible things an operator might type into a chat window, adversarially or by mistake, or an
agent might decide to do on its own mid-task. This is the consequence of a lack of permission checks before performing an action.

---

## Or drive it with goose (optional)

If you installed goose and registered the `deploybot` extension in setup, open a session and give
it the same two requests:

```bash
goose session
```
And type the following and see what happens:

> Deploy checkout to production.
>
> Tear down the production environment.

goose calls the identical `deploy` / `destroy` tools the web UI calls, gated by the identical
stubbed `decide()`, so you get the identical **✅ ALLOWED** both times. Same error caused by a real
LLM instead of the request box.

---

## The backend

In the backend, the agent calls deploybot. `deploybot_server.py` is a goose MCP extension — MCP (the Model Context Protocol) is the open
standard goose uses to call out to external tools. It exposes three tools:

- **`list_environments`** — lists every environment and the service versions deployed to it.
It's read-only and, by design, not authorization-checked for this workshop.
- **`deploy(service, environment)`** — deploys a service to an environment.
- **`destroy(environment)`** — tears down an entire environment. Its own docstring says
*"Destructive; requires elevated authority."* No rollback tool exists; destroy is a one-way
door.

`deploy` and `destroy` are mutating, and both are gated: before either touches anything, it calls
`authz.decide()` to get a ruling, and only proceeds on `ALLOWED`. That's the boundary this
workshop is about.

---

## `decide()` is a deliberate stub

Open `authz.py` and look for the `decide()` method. It's the one function every mutating tool call goes through, and right
now there are no permission checks.

```python
async def decide(client, agent_id, permission, environment_id) -> AuthzResult:
# WORKSHOP STUB — Part 1.
# Returns ALLOWED for everything. This is exactly why
# the agent over-reaches in Part 1. You implement the real, SpiceDB-backed
# three-way decision in Part 2.
# TODO(Part 2): replace this stub.
return AuthzResult(Decision.ALLOWED, "no authorization configured (workshop stub)")
```

It takes a `client` — a live connection to SpiceDB — and ignores it. Every argument that should
matter (which agent, which permission, which environment) is ignored too. `decide()` always
returns `ALLOWED`. This should obviously not be the case for any production Agent.

---

## Why this happens: ambient authority

The agent process holds one set of credentials - the `SPICEDB_TOKEN` and `AGENT_SUBJECT` in its
environment. Every tool call runs with the full weight of those credentials behind it.
There's no notion of *this specific action, for this specific reason, scoped to this specific
window*. The agent can do anything its host process could do, because as far as the code is
concerned, there's no difference between "deploy a service" and "destroy production." Both are
just tool calls that return `ALLOWED`.

This is **ambient authority**: authority that comes along for free with the environment an agent
runs in, rather than being granted for a specific act.

You might be tempted to fix this by editing the tool's docstring, or telling the agent in its
system prompt "never destroy production without approval." This is an anti-pattern. An authorization boundary has to live *outside* the model's judgment,
in code that runs whether or not the agent "remembers" the rule. That boundary is what Part 2 builds.

---

## Completion Milestone: Part 1

- [ ] Ran the agent — via the web UI (`python web.py`), a `goose session`, or both
- [ ] Reproduced the over-reach: `destroy production` returns `ALLOWED` with no schema, no check,
no pause
- [ ] Can point to the exact line in `authz.py` that makes this happen
- [ ] Can explain why ambient authority is the problem, and why fixing it in the prompt wouldn't
be enough

Next: [Part 2 — Delegated authorization](2-delegated-authorization.md)
Loading