Vercel published an OpenAI Agents SDK with FastAPI template on July 17, 2026. A template can remove setup work, but successful generation is not the production boundary that usually breaks. Task ownership is.
Primary source: Vercel template, “OpenAI Agents SDK with FastAPI”.
Before adopting any agent starter, I would add one vertical test: Alice must be able to create and cancel her task; Bob must not be able to read, stream, or cancel it—even if he guesses the task ID.
UI -> POST /tasks -> ownership row -> worker
UI <- GET /tasks/:id <- authorization <- state
UI <- event stream <- authorization <- events
UI -> POST /tasks/:id/cancel -> authorization -> cancellation
Use explicit states:
queued -> running -> succeeded
-> failed
queued|running -> cancelling -> cancelled
The database, API response, stream, and UI must agree on the same task and owner.
create table tasks (
id text primary key,
owner_id text not null,
state text not null check (state in (
'queued','running','succeeded','failed','cancelling','cancelled'
)),
created_at text not null,
updated_at text not null,
revision integer not null default 0
);
create table task_events (
task_id text not null,
revision integer not null,
kind text not null,
payload text not null,
primary key (task_id, revision)
);
Do not derive ownership from a browser-supplied field. Resolve the authenticated principal on the server and store it when creating the task.
from fastapi import Depends, FastAPI, HTTPException
app = FastAPI()
def current_user():
return {"id": "alice"}
def load_owned_task(task_id: str, user=Depends(current_user)):
task = db_get_task(task_id) # application function
if task is None or task["owner_id"] != user["id"]:
raise HTTPException(status_code=404, detail="task not found")
return task
@app.get("/tasks/{task_id}")
def get_task(task=Depends(load_owned_task)):
return task
@app.post("/tasks/{task_id}/cancel")
def cancel_task(task=Depends(load_owned_task)):
return request_cancel(task["id"], expected_revision=task["revision"])
The same dependency must protect event history and streaming endpoints. Securing GET /tasks/{id}
while leaving /tasks/{id}/events
open still leaks prompts and outputs.
def test_bob_cannot_observe_or_cancel_alices_task(client, alice, bob):
created = client.post("/tasks", headers=alice, json={"prompt": "demo"})
task_id = created.json()["id"]
for method, path in [
("get", f"/tasks/{task_id}"),
("get", f"/tasks/{task_id}/events"),
("post", f"/tasks/{task_id}/cancel"),
]:
response = getattr(client, method)(path, headers=bob)
assert response.status_code == 404
visible = client.get(f"/tasks/{task_id}", headers=alice)
assert visible.status_code == 200
Add a stream-specific test that authenticates before sending the first event. A late authorization check can leak initial metadata.
Two cancel requests or a completion racing with cancellation should not produce impossible transitions.
update tasks
set state = 'cancelling', revision = revision + 1
where id = :id
and owner_id = :owner
and state in ('queued', 'running')
and revision = :expected_revision;
Zero updated rows means “reload and decide,” not “pretend cancellation succeeded.” The worker should check the durable cancellation state before each consequential tool call.
The cancel button should:
queued
or running
;Do not optimistically label the task cancelled
when the server only recorded cancelling
.
The snippets omit a real identity provider, queue, database implementation, rate limits, sandbox policy, and secret management. Pin the template revision and dependencies before evaluating it.
Rollback checklist:
[ ] disable new task creation
[ ] revoke worker tool credentials
[ ] allow read-only task status
[ ] drain or mark queued work
[ ] preserve event and authorization logs
[ ] verify no cross-owner stream stayed open
A starter proves that the happy path can boot. The ownership slice proves something more valuable: the same security invariant survives UI, API, persistence, worker, stream, and cancellation behavior.
Which endpoint in your agent stack is most likely to miss the ownership check: events, artifacts, or cancellation?