Skip to content

Add agent-fullstack-app example: API + frontend + seeded DB in one sandbox - #12

Open
opencolin wants to merge 1 commit into
mainfrom
claude/agent-fullstack-app
Open

Add agent-fullstack-app example: API + frontend + seeded DB in one sandbox#12
opencolin wants to merge 1 commit into
mainfrom
claude/agent-fullstack-app

Conversation

@opencolin

Copy link
Copy Markdown
Collaborator

Adds a full-stack example for the "an agent drives a real app" use case — a customer asked whether Claude could hit backend endpoints and manipulate a frontend, with data behind it.

What it does: boots one sandbox, seeds a SQLite DB, starts a stdlib-Python API + frontend, drives GET/POST/PATCH /api/tasks, and exposes the UI on a public preview URL.

Why it's useful: the README maps every step to its Tenki MCP tool (tenki_create_sandbox / tenki_write_file / tenki_exec / tenki_expose_port), so an agent can run this exact loop with no bespoke backend.

Verification: verify.mjs asserts seed → health → POST → PATCH → count → frontend 200, then terminates the sandbox. Live-verified 3x against api.tenki.cloud (green each time, no leaked sandboxes). Stdlib-only app (no pip install), so it boots clean.

Conventions: follows the current repo style — TENKI_WORKSPACE_ID only (no project_id), README row under Sandbox API basics. run.mjs leaves the sandbox up for exploration but sets idle/lifetime billing guards.

Also documents the multi-sandbox pattern in the README (verified separately: one sandbox can reach another via its exposed URL — a public-gateway hop, not a private mesh).

…ndbox

A full-stack app (stdlib Python http.server + sqlite3) that boots in a single microVM: seed the DB, start the server, drive GET/POST/PATCH endpoints, and expose the frontend on a public preview URL. Each step is mapped in the README to its Tenki MCP tool (create_sandbox / write_file / exec / expose_port) so an agent can run the same loop — the 'Claude steers a backend and manipulates a frontend' use case. verify.mjs asserts seed -> health -> POST -> PATCH -> count -> frontend 200 and terminates the sandbox; live-verified 3x. run.mjs leaves the sandbox up for exploration with idle/lifetime billing guards. Also documents the multi-sandbox pattern (verified: one sandbox reaches another via its exposed URL).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@tenki-reviewer

tenki-reviewer Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review complete. 🟠 1 high · 🟡 2 medium

💬 Inline comments (3)

  • 🟠 Stored XSS via unescaped task title/status in innerHTMLserver.py:33
  • 🟡 Write handlers crash on malformed or non-dict JSON instead of 400server.py:64
  • 🟡 Public preview port exposes unauthenticated write APIrun.mjs:58

This PR introduces examples/agent-fullstack-app, a demo showing an agent driving a cloud sandbox: app/server.py (a Python http.server app with an in-memory SQLite store and GET/POST/PATCH JSON endpoints), run.mjs (provisions a sandbox with allowInbound and exposePort, uploads the app, and prints a preview URL), verify.mjs, package.json, and READMEs.

The server renders task rows via innerHTML with unescaped title/status, which becomes stored XSS because the write endpoints are unauthenticated and the port is published publicly. The write handlers also crash (no HTTP response) on malformed or non-object JSON bodies. run.mjs resolves app files relative to the CWD, unlike verify.mjs, so invoking it outside the example directory fails.

Files Change
examples/agent-fullstack-app/app/server.py Adds the Python task API server: innerHTML rendering, _body JSON parsing, and POST/PATCH write handlers over SQLite.
examples/agent-fullstack-app/run.mjs Provisions the sandbox (allowInbound, exposePort(PORT), up-to-2h lifetime) and uploads/runs the app.
examples/agent-fullstack-app/verify.mjs Health-checks the running app, resolving sources via import.meta.url.
examples/agent-fullstack-app/package.json, README.md, examples/agent-fullstack-app/README.md, README.md Adds demo metadata, scripts, and run instructions.

Reviewed commit: cdbcde3

@tenki-reviewer tenki-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Adds an agent-fullstack-app example (FastAPI-style Python server, JS runner, verifier, and docs) that spins up a public sandbox VM exposing a mutable task API.

Key findings

  • 🟠 Stored XSS via unescaped task title/status in innerHTMLserver.py:33
  • 🟡 Write handlers crash on malformed or non-dict JSON instead of 400server.py:64
  • 🟡 Public preview port exposes unauthenticated write APIrun.mjs:58

Comment on lines +33 to +34
document.getElementById('list').innerHTML = tasks.map(t =>
`<li><span class="s ${t.status}">${t.status}</span><span>${t.title}</span></li>`).join('');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 security · high

Stored XSS via unescaped task title/status in innerHTML

The GET / handler builds each task row with a template literal and assigns it to element.innerHTML (server.py:33-34), interpolating the untrusted title and status values with no HTML-escaping. do_POST accepts arbitrary title/status from the request body with no validation and persists them, and the app is exposed to the public internet via allowInbound: true plus exposePort(PORT) (run.mjs), so any visitor to the preview URL executes attacker-supplied markup in their browser. A remote attacker can POST a payload such as <img src=x onerror=...> and drive-by any user who opens the public URL.

📋 Prompt for AI Agents

In examples/agent-fullstack-app/app/server.py at lines 33-34, replace the innerHTML template-literal rendering in the GET / script with safe DOM construction: create each <li> via document.createElement, set the title text node with textContent, and map status through a whitelist (only todo/doing/done) to a CSS class (defaulting to todo) before assigning className. This prevents stored XSS because attacker-controlled title/status values are never parsed as HTML.

Comment on lines +64 to +89
return json.loads(self.rfile.read(n) or b"{}")

def do_GET(self):
if self.path == "/":
return self._send(200, INDEX.encode(), "text/html; charset=utf-8")
if self.path == "/api/tasks":
return self._send(200, q("SELECT id, title, status FROM tasks ORDER BY id"))
if self.path == "/api/health":
return self._send(200, {"ok": True, "tasks": q("SELECT COUNT(*) c FROM tasks")[0]["c"]})
return self._send(404, {"error": "not found"})

def do_POST(self):
if self.path != "/api/tasks":
return self._send(404, {"error": "not found"})
b = self._body()
if not b.get("title"):
return self._send(400, {"error": "title required"})
new_id = q("INSERT INTO tasks (title, status) VALUES (?, ?)",
(b["title"], b.get("status", "todo")), commit=True)
return self._send(201, {"id": new_id, "title": b["title"], "status": b.get("status", "todo")})

def do_PATCH(self):
if not self.path.startswith("/api/tasks/"):
return self._send(404, {"error": "not found"})
task_id = self.path.rsplit("/", 1)[-1]
status = self._body().get("status")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 bug · medium

Write handlers crash on malformed or non-dict JSON instead of 400

_body() (server.py:64) calls json.loads(...) with no try/except, and do_POST (server.py:78-79) and do_PATCH (server.py:89) dereference the result with .get(...). Malformed JSON raises json.JSONDecodeError and valid-but-non-object bodies (array, string, number, or null) raise AttributeError on .get; neither is caught, so the handler throws and the connection is torn down with no HTTP response instead of returning 400. Because the API is publicly reachable, any remote client can send a malformed body and get a failed/empty request for these write endpoints.

📋 Prompt for AI Agents

In examples/agent-fullstack-app/app/server.py, harden _body() (line 64) and its callers: wrap json.loads in try/except json.JSONDecodeError and return None on parse failure; in do_POST (line 78-79) and do_PATCH (line 89), guard with if not isinstance(b, dict): return self._send(400, {"error": "invalid body"}) before calling .get(...). This ensures malformed or non-object JSON bodies get a clean 400 response instead of crashing the request handler.

Comment on lines +58 to +61
const { previewUrl } = await sandbox.exposePort(PORT);
console.log(`\n-- the UI is live --\n ${previewUrl}`);
const res = await fetch(previewUrl);
console.log(` GET ${res.status} · ${(await res.text()).includes("<title>") ? "frontend served" : "unexpected body"}`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 security · medium

Public preview port exposes unauthenticated write API

The sandbox is created with allowInbound: true and the server binds 0.0.0.0 (server.py:102), then sandbox.exposePort(PORT) publishes the entire port to the public internet (run.mjs:58). This exposes not only the intended read-only frontend but also POST /api/tasks and PATCH /api/tasks/<id> (server.py:75-94), which mutate the SQLite data with no authentication, authorization, or rate limiting. Because run.mjs leaves the sandbox running up to 2 hours and prints the preview URL, anyone who obtains that URL can create or modify demo data and consume the public sandbox's resources.

📋 Prompt for AI Agents

In examples/agent-fullstack-app, gate the state-mutating endpoints: add a bearer-token check to do_POST (server.py:75) and do_PATCH (server.py:85) that rejects requests without a token from the sandbox environment before writing to the DB, OR restructure the demo so exposePort in run.mjs:58 (and verify.mjs:67) targets only a read-only static frontend while the write API is exercised solely via in-sandbox exec. This prevents the public preview URL from exposing unauthenticated writes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant