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.
State the cross-layer contract
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.
Minimal schema
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.
FastAPI authorization seam
from fastapi import Depends, FastAPI, HTTPException
app = FastAPI()
def current_user():
# Replace with verified session/JWT middleware.
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"]:
# Avoid revealing whether another user's task exists.
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.
Cross-layer failure test
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.
Cancellation needs compare-and-set
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.
UI states are part of the contract
The cancel button should:
- be visible only for
queuedorrunning; - switch to “Cancelling…” after the API accepts the transition;
- remain disabled while waiting for the terminal event;
- show retry only for a transport failure, not a completed task;
- restore focus to the status heading after cancellation.
Do not optimistically label the task cancelled when the server only recorded cancelling.
Production caveats and rollback
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?
Top comments (0)