Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Binary file modified assets/demo/safe-agent-demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/demo/safe-agent-playground.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
109 changes: 82 additions & 27 deletions examples/safe-agent-api/app/discovery.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
PUBLIC_URL = "https://safe-agent-playground.onrender.com"
SOURCE_URL = "https://github.com/Videirafo/AI-Agent-Production-Checklist"
SOCIAL_IMAGE_URL = (
"https://raw.githubusercontent.com/Videirafo/AI-Agent-Production-Checklist/"
"main/assets/demo/safe-agent-playground.png"
)
RELEASE_VERSION = "0.6.0"
LAST_MODIFIED = "2026-09-07"

LLMS_TXT = f"""# Safe Agent Playground

Expand All @@ -9,7 +15,7 @@
- [Live Playground]({PUBLIC_URL}/): interactive policy, approval, execution and audit demo
- [OpenAPI]({PUBLIC_URL}/openapi.json): machine-readable API schema
- [Swagger UI]({PUBLIC_URL}/docs): interactive API documentation
- [Playground Markdown]({PUBLIC_URL}/playground.md): text-first description of the demo
- [Playground Markdown]({PUBLIC_URL}/index.md): text-first description of the demo
- [Source]({SOURCE_URL}): MIT-licensed source code and contribution workflow

## Safety model
Expand Down Expand Up @@ -49,63 +55,98 @@

SITEMAP_XML = f"""<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url><loc>{PUBLIC_URL}/</loc></url>
<url><loc>{PUBLIC_URL}/docs</loc></url>
<url><loc>{PUBLIC_URL}/openapi.json</loc></url>
<url><loc>{PUBLIC_URL}/playground.md</loc></url>
<url><loc>{PUBLIC_URL}/llms.txt</loc></url>
<url><loc>{PUBLIC_URL}/AGENTS.md</loc></url>
<url>
<loc>{PUBLIC_URL}/</loc>
<lastmod>{LAST_MODIFIED}</lastmod>
</url>
</urlset>
"""

SITEMAP_MD = f"""# Safe Agent Playground Sitemap

## Canonical page
- [Playground]({PUBLIC_URL}/)

## Developer resources
- [OpenAPI / Swagger]({PUBLIC_URL}/docs)
- [OpenAPI JSON]({PUBLIC_URL}/openapi.json)
- [Markdown mirror]({PUBLIC_URL}/playground.md)
- [Markdown mirror]({PUBLIC_URL}/index.md)
- [LLM discovery]({PUBLIC_URL}/llms.txt)
- [Agent instructions]({PUBLIC_URL}/AGENTS.md)
- [Source code]({SOURCE_URL})
"""

AGENTS_MD = f"""# AGENTS.md

## Purpose
Safe Agent Playground demonstrates production boundaries for AI-agent tool execution.
Safe Agent Playground demonstrates production boundaries for AI-agent tool execution. Core invariant: Model suggestion is not authorization. Authorization is enforced by deterministic application policy outside the model.

## Installation

Clone the source and install the example in an isolated Python environment:

```bash
git clone {SOURCE_URL}.git
cd AI-Agent-Production-Checklist/examples/safe-agent-api
python -m venv .venv
pip install -e \".[dev]\"
Comment on lines +90 to +91

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Install dependencies through the newly created virtualenv

When these documented commands are run in a fresh shell, creating .venv does not activate it, so the subsequent bare pip resolves to the caller's existing environment and installs the package globally or into another environment despite the isolation claim. Activate .venv first or invoke .venv/bin/python -m pip directly.

Useful? React with 👍 / 👎.

```

Docker is also supported through the repository's `docker compose` configuration.

## Configuration

No secrets, API keys, LLM provider, external database, or signup are required. The public demo is intentionally deterministic and simulated. Do not send credentials, private customer data, proprietary payloads, or secrets.

## Core invariant
Model suggestion is not authorization. Authorization is enforced by deterministic application policy outside the model.
## Usage

## Public API
Public endpoints:
- GET {PUBLIC_URL}/
- GET {PUBLIC_URL}/health
- POST {PUBLIC_URL}/v1/tool-check
- POST {PUBLIC_URL}/v1/run-demo
- GET {PUBLIC_URL}/openapi.json

## Safe usage
The public demo is deterministic and simulated. It performs no real notification, deletion or tenant data access. Do not send secrets, credentials, private customer data or proprietary payloads.
The model may suggest an action, but the application policy decides whether execution is permitted.

## Contribution source
{SOURCE_URL}
## Examples

Use the browser Playground to run five reference scenarios: same-tenant read, cross-tenant denial, approval-required notification, approved notification, and blocked destructive delete.

For API examples and the exact schema, use {PUBLIC_URL}/docs or {PUBLIC_URL}/openapi.json.

## Contribution

Source, tests, issues, and contribution workflow: {SOURCE_URL}
"""

PLAYGROUND_MD = f"""# Safe Agent Playground
PLAYGROUND_MD = f"""---
title: Safe Agent Playground
description: Runnable open-source FastAPI demo for deterministic AI-agent tool authorization boundaries.
canonical: {PUBLIC_URL}/
version: {RELEASE_VERSION}
last_modified: {LAST_MODIFIED}
---

# Safe Agent Playground

Safe Agent Playground is an MIT-licensed FastAPI demo showing how agent tool execution can remain outside model authority.
Safe Agent Playground is an MIT-licensed FastAPI demo showing how agent tool execution can remain outside model authority. The model may suggest an action, while deterministic application policy, tenant boundaries, and human approval decide whether execution is allowed.

## Live demo

{PUBLIC_URL}/

## What it demonstrates
- tenant isolation
- deterministic tool policies
- human approval gates
- destructive-action denial
- structured audit events
- correlation IDs

- tenant isolation;
- deterministic tool policies;
- human approval gates;
- destructive-action denial;
- structured audit events;
- correlation IDs;
- an execution boundary that does not depend on an LLM making its own authorization decision.

## Scenarios

| Scenario | Expected result |
|---|---|
| same-tenant read_record | allowed and executed |
Expand All @@ -115,15 +156,29 @@
| delete_record | denied: destructive_tool_disabled_in_demo |

## Flow
`model suggestion -> policy -> approval -> execution -> audit -> correlation`

```text
model suggestion -> policy -> approval -> execution -> audit -> correlation
```

## API

- [Swagger]({PUBLIC_URL}/docs)
- [OpenAPI JSON]({PUBLIC_URL}/openapi.json)
- [Health]({PUBLIC_URL}/health)

## Sitemap

- [Canonical Playground]({PUBLIC_URL}/)
- [Agent discovery]({PUBLIC_URL}/llms.txt)
- [Agent instructions]({PUBLIC_URL}/AGENTS.md)
- [Human-readable sitemap]({PUBLIC_URL}/sitemap.md)

## Source and contributions

{SOURCE_URL}
"""

HEAD_DISCOVERY_HTML = f'''\n <meta name="description" content="Runnable open-source Safe Agent Playground demonstrating tenant isolation, deterministic tool policy, human approval, audit events and correlation IDs for AI-agent execution." />\n <meta name="robots" content="index, follow, max-snippet:-1, max-image-preview:large" />\n <link rel="canonical" href="{PUBLIC_URL}/" />\n <link rel="describedby" href="/llms.txt" />\n <link rel="alternate" type="text/markdown" href="/playground.md" />\n <meta property="og:title" content="Safe Agent Playground" />\n <meta property="og:description" content="Try deterministic agent-tool authorization, human approvals, tenant isolation and audit correlation in a runnable FastAPI demo." />\n <meta property="og:type" content="website" />\n <meta property="og:url" content="{PUBLIC_URL}/" />\n <meta name="twitter:card" content="summary" />\n <script type="application/ld+json">{{"@context":"https://schema.org","@type":"SoftwareApplication","name":"Safe Agent Playground","description":"Runnable open-source demo for AI-agent tool authorization boundaries.","url":"{PUBLIC_URL}/","applicationCategory":"DeveloperApplication","operatingSystem":"Web","codeRepository":"{SOURCE_URL}"}}</script>\n'''
HEAD_DISCOVERY_HTML = f'''\n <meta name="description" content="Runnable open-source Safe Agent Playground demonstrating tenant isolation, deterministic tool policy, human approval, audit events and correlation IDs for AI-agent execution." />\n <meta name="robots" content="index, follow, max-snippet:-1, max-image-preview:large" />\n <link rel="canonical" href="{PUBLIC_URL}/" />\n <link rel="describedby" href="/llms.txt" />\n <link rel="alternate" type="text/markdown" href="/index.md" />\n <meta property="og:title" content="Safe Agent Playground" />\n <meta property="og:description" content="Try deterministic agent-tool authorization, human approvals, tenant isolation and audit correlation in a runnable FastAPI demo." />\n <meta property="og:type" content="website" />\n <meta property="og:url" content="{PUBLIC_URL}/" />\n <meta property="og:image" content="{SOCIAL_IMAGE_URL}" />\n <meta name="twitter:card" content="summary_large_image" />\n <meta name="twitter:title" content="Safe Agent Playground" />\n <meta name="twitter:description" content="Runnable open-source demo for AI-agent tool authorization boundaries." />\n <meta name="twitter:image" content="{SOCIAL_IMAGE_URL}" />\n <script type="application/ld+json">{{"@context":"https://schema.org","@type":"SoftwareApplication","name":"Safe Agent Playground","headline":"Safe Agent Playground","description":"Runnable open-source demo for AI-agent tool authorization boundaries.","url":"{PUBLIC_URL}/","dateModified":"{LAST_MODIFIED}","softwareVersion":"{RELEASE_VERSION}","applicationCategory":"DeveloperApplication","operatingSystem":"Web","codeRepository":"{SOURCE_URL}"}}</script>\n'''

DISCOVERY_BODY_HTML = f'''\n <section class="card" style="margin-top:20px" aria-labelledby="why-title">\n <h2 id="why-title">Why this demo exists</h2>\n <p>Many AI-agent examples demonstrate how a model selects or calls a tool, but production systems need a separate authorization boundary. This Playground makes that boundary visible: tenant scope is checked first, sensitive actions can require human approval, destructive actions remain blocked, and every decision can be correlated with an audit event.</p>\n <h3>What to evaluate</h3>\n <p>Try an allowed same-tenant read, then change the resource tenant and observe the denial. Compare a notification request before and after human approval. Finally, try the destructive delete scenario and verify that approval alone cannot bypass a policy that disables the tool. The purpose is not to simulate intelligence; it is to make execution policy deterministic, inspectable, testable, and independent from model persuasion.</p>\n <p>Machine-readable resources are available through <a href="/openapi.json">OpenAPI</a>, <a href="/llms.txt">llms.txt</a>, <a href="/AGENTS.md">AGENTS.md</a>, and the <a href="/index.md">Markdown mirror</a>.</p>\n </section>\n'''
36 changes: 28 additions & 8 deletions examples/safe-agent-api/app/main.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
from fastapi import FastAPI
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse, PlainTextResponse, Response

from app.discovery import (
AGENTS_MD,
DISCOVERY_BODY_HTML,
HEAD_DISCOVERY_HTML,
LLMS_FULL_TXT,
LLMS_TXT,
PLAYGROUND_MD,
RELEASE_VERSION,
ROBOTS_TXT,
SITEMAP_MD,
SITEMAP_XML,
Expand All @@ -23,32 +25,49 @@

app = FastAPI(
title="Safe Agent API",
version="0.5.0",
version=RELEASE_VERSION,
description="Deterministic policy, approval and audit layer for agent tool execution demos.",
)


@app.get("/", response_class=HTMLResponse, include_in_schema=False)
def playground() -> HTMLResponse:
MARKDOWN_HEADERS = {
"Link": '</>; rel="canonical", </sitemap.md>; rel="sitemap"',
"X-Robots-Tag": "index, follow, max-snippet:-1, max-image-preview:large",
}


@app.api_route("/", methods=["GET", "HEAD"], response_class=HTMLResponse, include_in_schema=False)
def playground(request: Request) -> Response:
accept = request.headers.get("accept", "").lower()
if request.method == "GET" and "text/markdown" in accept:
Comment on lines +41 to +42

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor Accept quality weights when selecting Markdown

When a client sends an Accept header such as text/html, text/markdown;q=0 or prefers HTML with a higher quality value, this substring check still returns Markdown—even though Markdown may be explicitly unacceptable. Parse the media ranges and quality weights so Markdown is selected only when it is acceptable and preferred.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Negotiate HEAD requests consistently with GET

For HEAD / with Accept: text/markdown, the method guard skips the Markdown branch and reports HTML headers, while the corresponding GET selects Markdown. Clients and discovery scanners commonly use HEAD to inspect a representation, so apply the same content negotiation to HEAD while allowing the server to suppress the body.

Useful? React with 👍 / 👎.

return PlainTextResponse(
PLAYGROUND_MD,
media_type="text/markdown",
headers=MARKDOWN_HEADERS,
Comment on lines +43 to +46

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Mark the negotiated root response as varying on Accept

The Markdown representation returned from / is cacheable but does not include Vary: Accept. An intermediary that caches a Markdown request can therefore reuse that response for a later ordinary browser request to the same URL and serve Markdown instead of HTML. Add Vary: Accept to the negotiated root responses.

Useful? React with 👍 / 👎.

)

html = PLAYGROUND_HTML.replace("</head>", f"{HEAD_DISCOVERY_HTML}</head>")
html = html.replace('<p class="footer">', f'{DISCOVERY_BODY_HTML}<p class="footer">')
return HTMLResponse(
html,
headers={
"Cache-Control": "no-store",
"Content-Security-Policy": (
"default-src 'self'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; "
"connect-src 'self'; img-src 'self' data:; frame-ancestors 'none'"
"connect-src 'self'; img-src 'self' data: https://raw.githubusercontent.com; "
"frame-ancestors 'none'"
),
"Link": '</llms.txt>; rel="describedby", </playground.md>; rel="alternate"; type="text/markdown"',
"Link": '</llms.txt>; rel="describedby", </index.md>; rel="alternate"; type="text/markdown"',
"Referrer-Policy": "no-referrer",
"X-Content-Type-Options": "nosniff",
"X-Robots-Tag": "index, follow, max-snippet:-1, max-image-preview:large",
},
)


@app.get("/health")
def health() -> dict[str, str]:
return {"status": "ok", "service": "safe-agent-api", "version": "0.5.0"}
return {"status": "ok", "service": "safe-agent-api", "version": RELEASE_VERSION}


@app.get("/llms.txt", response_class=PlainTextResponse, include_in_schema=False)
Expand Down Expand Up @@ -81,12 +100,13 @@ def agents_md() -> PlainTextResponse:
return PlainTextResponse(AGENTS_MD, media_type="text/markdown")


@app.get("/index.md", response_class=PlainTextResponse, include_in_schema=False)
@app.get("/playground.md", response_class=PlainTextResponse, include_in_schema=False)
def playground_md() -> PlainTextResponse:
return PlainTextResponse(
PLAYGROUND_MD,
media_type="text/markdown",
headers={"Link": '</sitemap.md>; rel="sitemap"'},
headers=MARKDOWN_HEADERS,
)


Expand Down
2 changes: 1 addition & 1 deletion examples/safe-agent-api/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "safe-agent-api"
version = "0.5.0"
version = "0.6.0"
description = "Deterministic safety policy demo for AI agent tool execution"
requires-python = ">=3.12"
dependencies = [
Expand Down
34 changes: 32 additions & 2 deletions examples/safe-agent-api/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,29 @@ def test_playground_is_public_and_self_contained() -> None:
assert response.status_code == 200
assert "Safe Agent Playground" in response.text
assert "/v1/run-demo" in response.text
assert "Why this demo exists" in response.text
assert 'rel="canonical"' in response.text
assert 'rel="describedby" href="/llms.txt"' in response.text
assert 'property="og:title"' in response.text
assert 'type="application/ld+json"' in response.text
assert response.headers["x-content-type-options"] == "nosniff"
assert "connect-src 'self'" in response.headers["content-security-policy"]
assert response.headers["x-robots-tag"].startswith("index, follow")


def test_head_root_is_supported() -> None:
response = client.head("/")
assert response.status_code == 200
assert response.headers["content-type"].startswith("text/html")


def test_root_supports_markdown_content_negotiation() -> None:
response = client.get("/", headers={"Accept": "text/markdown"})
assert response.status_code == 200
assert response.headers["content-type"].startswith("text/markdown")
assert "# Safe Agent Playground" in response.text
assert "## Sitemap" in response.text
assert 'rel="canonical"' in response.headers["link"]


def test_health() -> None:
Expand All @@ -23,7 +41,7 @@ def test_health() -> None:
assert response.json() == {
"status": "ok",
"service": "safe-agent-api",
"version": "0.5.0",
"version": "0.6.0",
}


Expand All @@ -33,6 +51,7 @@ def test_discovery_documents_are_public() -> None:
assert llms.headers["content-type"].startswith("text/plain")
assert "# Safe Agent Playground" in llms.text
assert "/openapi.json" in llms.text
assert "/index.md" in llms.text

full = client.get("/llms-full.txt")
assert full.status_code == 200
Expand All @@ -47,6 +66,9 @@ def test_discovery_documents_are_public() -> None:
assert sitemap_xml.status_code == 200
assert sitemap_xml.headers["content-type"].startswith("application/xml")
assert "<urlset" in sitemap_xml.text
assert "<lastmod>2026-09-07</lastmod>" in sitemap_xml.text
assert "<loc>https://safe-agent-playground.onrender.com/</loc>" in sitemap_xml.text
assert "playground.md" not in sitemap_xml.text

sitemap_md = client.get("/sitemap.md")
assert sitemap_md.status_code == 200
Expand All @@ -55,11 +77,19 @@ def test_discovery_documents_are_public() -> None:
agents = client.get("/AGENTS.md")
assert agents.status_code == 200
assert "Model suggestion is not authorization" in agents.text
assert "## Installation" in agents.text
assert "## Configuration" in agents.text
assert "## Usage" in agents.text
assert "## Examples" in agents.text

markdown = client.get("/playground.md")
markdown = client.get("/index.md")
assert markdown.status_code == 200
assert markdown.headers["content-type"].startswith("text/markdown")
assert markdown.text.startswith("---\n")
assert "canonical: https://safe-agent-playground.onrender.com/" in markdown.text
assert "last_modified: 2026-09-07" in markdown.text
assert "## Scenarios" in markdown.text
assert "## Sitemap" in markdown.text


def test_read_is_allowed_in_same_tenant() -> None:
Expand Down
Loading