Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
87 changes: 86 additions & 1 deletion examples/safe-agent-api/app/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
- [OpenAPI]({PUBLIC_URL}/openapi.json): machine-readable API schema
- [Swagger UI]({PUBLIC_URL}/docs): interactive API documentation
- [Playground Markdown]({PUBLIC_URL}/index.md): text-first description of the demo
- [Glossary]({PUBLIC_URL}/glossary): definitions for the safety terms used by the demo
- [Source]({SOURCE_URL}): MIT-licensed source code and contribution workflow

## Safety model
Expand Down Expand Up @@ -71,6 +72,7 @@
- [OpenAPI / Swagger]({PUBLIC_URL}/docs)
- [OpenAPI JSON]({PUBLIC_URL}/openapi.json)
- [Markdown mirror]({PUBLIC_URL}/index.md)
- [Glossary]({PUBLIC_URL}/glossary)

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 Add the glossary to the XML sitemap

When a crawler follows the sitemap advertised by ROBOTS_TXT, this addition is absent because it updates only SITEMAP_MD, while SITEMAP_XML still contains just the root URL. Machine sitemap consumers therefore do not receive the new indexable canonical /glossary page; add a corresponding <url> entry to the XML sitemap and cover it in the sitemap test.

Useful? React with 👍 / 👎.

- [LLM discovery]({PUBLIC_URL}/llms.txt)
- [Agent instructions]({PUBLIC_URL}/AGENTS.md)
- [Source code]({SOURCE_URL})
Expand Down Expand Up @@ -125,6 +127,7 @@
canonical: {PUBLIC_URL}/
version: {RELEASE_VERSION}
last_modified: {LAST_MODIFIED}
last_updated: {LAST_MODIFIED}
---

# Safe Agent Playground
Expand Down Expand Up @@ -161,6 +164,17 @@
model suggestion -> policy -> approval -> execution -> audit -> correlation
```

## Glossary

- **Model suggestion:** an action proposed by an AI model; it has no execution authority by itself.
- **Tool policy:** deterministic application logic that allows or denies a proposed tool invocation.
- **Human approval:** an explicit approval signal required before selected sensitive actions can execute.
- **Tenant isolation:** the rule that an actor may only operate on resources within the permitted tenant boundary.
- **Audit event:** a structured record of the decision, execution state, reason, and correlation identifier.
- **Correlation ID:** the identifier used to connect the request, policy decision, execution result, and audit record.

Full terminology reference: {PUBLIC_URL}/glossary

## API

- [Swagger]({PUBLIC_URL}/docs)
Expand All @@ -170,6 +184,7 @@
## Sitemap

- [Canonical Playground]({PUBLIC_URL}/)
- [Glossary]({PUBLIC_URL}/glossary)
- [Agent discovery]({PUBLIC_URL}/llms.txt)
- [Agent instructions]({PUBLIC_URL}/AGENTS.md)
- [Human-readable sitemap]({PUBLIC_URL}/sitemap.md)
Expand All @@ -179,6 +194,76 @@
{SOURCE_URL}
"""

GLOSSARY_MD = f"""---
title: Safe Agent Playground Glossary
description: Definitions for the production-safety terms used by Safe Agent Playground.
canonical: {PUBLIC_URL}/glossary
last_updated: {LAST_MODIFIED}
---

# Safe Agent Playground Glossary

## Model suggestion
An action proposed by a model. A suggestion is input to policy evaluation and never grants execution authority by itself.

## Tool policy
Deterministic application logic that evaluates a proposed tool action against tenant scope, approval requirements, and disabled-action rules.

## Human approval
An explicit approval signal required for selected sensitive tools. Approval is necessary where policy requires it, but approval does not override a policy that disables an action.

## Tenant isolation
The boundary that prevents an actor in one tenant from operating on a resource that belongs to another tenant.

## Audit event
A structured record capturing the correlation ID, actor tenant, resource tenant, tool, policy decision, execution state, reason, and approval state.

## Correlation ID
A request identifier used to connect a user-visible request with its policy decision, execution result, and audit record.

## Destructive action
An operation that can delete or irreversibly change data. The public demo deliberately blocks destructive execution even when human approval is present.

## Related resources
- [Live Playground]({PUBLIC_URL}/)
- [OpenAPI]({PUBLIC_URL}/openapi.json)
- [Agent instructions]({PUBLIC_URL}/AGENTS.md)
- [Source]({SOURCE_URL})
"""

GLOSSARY_HTML = f'''<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Safe Agent Playground Glossary</title>
<meta name="description" content="Definitions for tenant isolation, tool policy, human approval, audit events, correlation IDs, and other Safe Agent Playground terms." />
<meta name="robots" content="index, follow, max-snippet:-1, max-image-preview:large" />
<link rel="canonical" href="{PUBLIC_URL}/glossary" />
<link rel="describedby" href="/llms.txt" />
<link rel="alternate" type="text/markdown" href="/glossary.md" />
<meta property="og:title" content="Safe Agent Playground Glossary" />
<meta property="og:description" content="Definitions for the production-safety terms used by Safe Agent Playground." />
<meta property="og:type" content="website" />
<meta property="og:url" content="{PUBLIC_URL}/glossary" />
<style>body{{margin:0;font:16px/1.65 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:#08111f;color:#e8eef7}}main{{max-width:820px;margin:auto;padding:44px 20px 72px}}a{{color:#68a7ff}}h1{{font-size:42px;line-height:1.1}}h2{{margin-top:30px}}p{{color:#c6d2e1}}</style>
</head>
<body>
<main>
<p><a href="/">← Back to Safe Agent Playground</a></p>
<h1>Safe Agent Playground Glossary</h1>
<p>This glossary defines the production-safety terms used by the Playground so readers, search systems, and AI agents can resolve the architecture consistently.</p>
<h2>Model suggestion</h2><p>An action proposed by a model. It is evaluated as untrusted input and never grants execution authority by itself.</p>
<h2>Tool policy</h2><p>Deterministic application logic that evaluates tenant scope, approval requirements, and disabled-action rules before execution.</p>
<h2>Human approval</h2><p>An explicit approval signal required for selected sensitive operations. Approval can satisfy a policy requirement but cannot bypass a policy that disables an action.</p>
<h2>Tenant isolation</h2><p>The boundary preventing an actor associated with one tenant from operating on resources belonging to another tenant.</p>
<h2>Audit event</h2><p>A structured record of the policy decision and execution state, including reason, tenant context, approval state, and correlation identifier.</p>
<h2>Correlation ID</h2><p>A request identifier used to connect a request with its policy decision, execution result, and audit event.</p>
<h2>Destructive action</h2><p>An operation capable of irreversible or high-impact change. The public demo blocks destructive execution even when human approval is present.</p>
</main>
</body>
</html>'''

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'''
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 <h3>Why the boundary matters</h3>\n <p>Agent systems become safer when model output is treated as a proposal rather than a permission grant. A separate policy layer can enforce tenant isolation, require approval for sensitive capabilities, disable destructive tools, and record the decision independently of the model's wording. That separation makes the behavior easier to test, audit, review, and reproduce across model providers.</p>\n <p>The demo intentionally performs simulated actions only, so visitors can inspect an allowed execution and several denial paths without credentials, private data, or external side effects. See the <a href="/glossary">glossary</a> for definitions of model suggestion, tool policy, human approval, tenant isolation, audit event, correlation ID, and destructive action.</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'''
29 changes: 29 additions & 0 deletions examples/safe-agent-api/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from app.discovery import (
AGENTS_MD,
DISCOVERY_BODY_HTML,
GLOSSARY_HTML,
GLOSSARY_MD,
HEAD_DISCOVERY_HTML,
LLMS_FULL_TXT,
LLMS_TXT,
Expand Down Expand Up @@ -65,6 +67,33 @@ def playground(request: Request) -> Response:
)


@app.get("/glossary", response_class=HTMLResponse, include_in_schema=False)
def glossary() -> HTMLResponse:
return HTMLResponse(
GLOSSARY_HTML,
headers={
"Cache-Control": "public, max-age=300",
"Content-Security-Policy": "default-src 'self'; style-src 'unsafe-inline'; frame-ancestors 'none'",
"Link": '</llms.txt>; rel="describedby", </glossary.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("/glossary.md", response_class=PlainTextResponse, include_in_schema=False)
def glossary_md() -> PlainTextResponse:
return PlainTextResponse(
GLOSSARY_MD,
media_type="text/markdown",
headers={
"Link": '</glossary>; rel="canonical", </sitemap.md>; rel="sitemap"',
"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": RELEASE_VERSION}
Expand Down
24 changes: 24 additions & 0 deletions examples/safe-agent-api/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ def test_playground_is_public_and_self_contained() -> None:
assert "Safe Agent Playground" in response.text
assert "/v1/run-demo" in response.text
assert "Why this demo exists" in response.text
assert "Why the boundary matters" in response.text
assert 'href="/glossary"' 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
Expand All @@ -32,6 +34,7 @@ def test_root_supports_markdown_content_negotiation() -> None:
assert response.headers["content-type"].startswith("text/markdown")
assert "# Safe Agent Playground" in response.text
assert "## Sitemap" in response.text
assert "## Glossary" in response.text
assert 'rel="canonical"' in response.headers["link"]


Expand All @@ -52,6 +55,7 @@ def test_discovery_documents_are_public() -> None:
assert "# Safe Agent Playground" in llms.text
assert "/openapi.json" in llms.text
assert "/index.md" in llms.text
assert "/glossary" in llms.text

full = client.get("/llms-full.txt")
assert full.status_code == 200
Expand All @@ -73,6 +77,7 @@ def test_discovery_documents_are_public() -> None:
sitemap_md = client.get("/sitemap.md")
assert sitemap_md.status_code == 200
assert sitemap_md.headers["content-type"].startswith("text/markdown")
assert "/glossary" in sitemap_md.text

agents = client.get("/AGENTS.md")
assert agents.status_code == 200
Expand All @@ -88,10 +93,29 @@ def test_discovery_documents_are_public() -> None:
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 "last_updated: 2026-09-07" in markdown.text
assert "## Scenarios" in markdown.text
assert "## Glossary" in markdown.text
assert "## Sitemap" in markdown.text


def test_glossary_has_html_and_markdown_versions() -> None:
html = client.get("/glossary")
assert html.status_code == 200
assert html.headers["content-type"].startswith("text/html")
assert "Safe Agent Playground Glossary" in html.text
assert 'rel="canonical" href="https://safe-agent-playground.onrender.com/glossary"' in html.text
assert 'rel="alternate" type="text/markdown" href="/glossary.md"' in html.text
assert "Tenant isolation" in html.text

markdown = client.get("/glossary.md")
assert markdown.status_code == 200
assert markdown.headers["content-type"].startswith("text/markdown")
assert "last_updated: 2026-09-07" in markdown.text
assert "## Tool policy" in markdown.text
assert 'rel="canonical"' in markdown.headers["link"]


def test_read_is_allowed_in_same_tenant() -> None:
response = client.post(
"/v1/tool-check",
Expand Down
Loading