Skip to content

feat(render): locked-down DEMO mode for a public no-auth demo#5

Merged
Ho1yShif merged 4 commits into
mainfrom
feat/render-demo-mode
Jul 8, 2026
Merged

feat(render): locked-down DEMO mode for a public no-auth demo#5
Ho1yShif merged 4 commits into
mainfrom
feat/render-demo-mode

Conversation

@Ho1yShif

@Ho1yShif Ho1yShif commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Adds a DEMO=true mode so the template's hosted demo works with no auth (anyone can chat immediately), while anyone who forks the template keeps the fully authenticated default. The demo agent is chat-only and locked down and cannot reach sensitive info; public usage is bounded by env-configurable rate + per-session caps.

Design spec: docs/superpowers/specs/2026-07-07-demo-mode-design.md (gitignored, local).

What it does

  • Toggle: entrypoint.sh runs render-demo-config.json when DEMO=true, else render-config.json. render.yaml now passes only gateway (entrypoint owns --config). DEMO defaults false.
  • Lockdown (config-only): demo config disables shell, filesystem, subagent, MCP, cron, self-modify, image-gen; keeps chat + web search/fetch; sets restrictToWorkspace, webuiAllowLocalServiceAccess:false, and a cheaper default model.
  • Auth bypass (minimal core change): WebSocketConfig.demo relaxes the 0.0.0.0-requires-token validator; /webui/bootstrap skips its secret/localhost gate in demo and mints short-lived anonymous WS tokens (existing token path unchanged); response carries demo:true. Each browser still gets its own isolated websocket:{uuid} session.
  • New tools.subagent.enable flag — the one dangerous tool that lacked a config gate — off in demo. Cron is disabled by withholding cron_service from the demo agent.
  • Rate/session caps: per-connection token bucket + per-session message cap in the WS connection loop, via DEMO_RATE_LIMIT_PER_MINUTE (10) / DEMO_MAX_MESSAGES_PER_SESSION (30); 0 disables. Friendly reply on trip.
  • WebUI: reads demo, hides settings/apps/skills/automations, forces chat view, shows a demo banner.

Security (no sensitive-info access)

  • Shell + filesystem off → the agent has no way to read the process env (the ANTHROPIC_API_KEY lives in-memory only) or on-disk config.
  • Web fetch is already SSRF-guarded in core (nanobot/security/network.py): loopback, link-local incl. cloud metadata 169.254.169.254, and RFC1918 are blocked on every request and redirect hop. Demo adds no ssrf_whitelist.

Verification

  • ruff check clean on all touched files.
  • New tests pass (tests/channels/test_websocket_demo.py, tests/tools/test_tool_loader.py — 54 passed): demo bootstrap bypass returns 200 + demo:true without a secret; validator allows 0.0.0.0 in demo; rate + per-session caps trip the friendly stop; demo config registers no exec/file/subagent/mcp tools; non-demo still requires the token (regression).
  • Full tests/channels tests/config tests/webui run: no new failures — the 46 failures are all pre-existing on main (feishu/plugins, confirmed by running them on main).
  • Frontend bun run build succeeds.
  • render.yaml validates against https://render.com/schema/render.yaml.json (fixed a boolean DEMO value → quoted string).

🤖 Generated with Claude Code

Ho1yShif and others added 4 commits July 7, 2026 18:53
DEMO=true runs a hosted, unauthenticated, chat-only nanobot (what powers
the template's public demo); forks default to full token auth. Each browser
still gets its own isolated session, and public usage is bounded by
env-configurable rate + per-session caps.

- entrypoint.sh selects render-demo-config.json when DEMO=true (owns the
  --config flag; render.yaml passes only `gateway`).
- render-demo-config.json: chat + web only. Shell, filesystem, subagent, MCP,
  cron, self-modify, image-gen all off; restrictToWorkspace + no local service
  access + cheaper default model.
- WebSocketConfig.demo: relaxes the 0.0.0.0-requires-token validator; the
  /webui/bootstrap gate is skipped in demo and mints anonymous short-lived WS
  tokens; response carries demo:true.
- New tools.subagent.enable flag (the one dangerous tool lacking a config gate);
  cron_service withheld from the demo agent so the cron tool never registers.
- Per-connection rate limit + per-session message cap in the WS connection loop,
  configured via DEMO_RATE_LIMIT_PER_MINUTE / DEMO_MAX_MESSAGES_PER_SESSION
  (0 disables); friendly reply when a cap trips.
- WebUI reads demo flag: hides settings/apps/skills/automations, forces the chat
  view, shows a demo banner.
- Web fetch is already SSRF-guarded in core (loopback/link-local/metadata/RFC1918
  blocked, redirects re-validated), so the one live tool can't pivot internally.
- README/.env.example/render.yaml document DEMO, the lockdown, and the limits
  (incl. how to change or disable them on a fork). Adds tests for the demo
  bootstrap bypass, validator, rate/session caps, and tool-lockdown; non-demo
  auth path is regression-covered.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- ws_http bootstrap: invert the demo check so the auth gate is simply
  skipped instead of an empty `pass` branch.
- Sidebar: collapse three repeated `{!demo && ...}` guards into one
  fragment; move the useClient import into the provider import group.
- websocket: clarify the demo-limit env-var comment (0 disables;
  unset/negative/invalid falls back to the default).

No behavior change; 54 demo tests pass, ruff clean, frontend build green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…leak

Demo mode mints an API token for every anonymous visitor, so the token
is not an identity. But the WebUI session endpoints guarded only on that
token and then returned the *global* session store, so any demo visitor
(e.g. a fresh incognito tab) could list, open, and delete every other
visitor's chat via GET /api/sessions and /api/sessions/{key}/*.

Fix, enforced server-side (authoritative — the client can't be trusted):
- GET /api/sessions returns an empty list in demo mode.
- All per-session routes (messages, webui-thread, file-preview,
  automations, delete) return 403 in demo mode.

Each connection still gets its own fresh, unguessable websocket:{uuid}
chat and can chat normally; there is simply no cross-session HTTP access.
Non-demo mode is unchanged (single token-gated owner still sees all
sessions), verified by the existing bearer-token route test.

Note: no per-connection WS attach guard was added. The client re-attaches
every known chat_id on reconnect, so a per-connection ownership check
would break a visitor's own chat after any network blip, and demo has no
durable identity to distinguish owner from attacker. The boundary is
instead: unguessable uuid4 ids + an empty session list (no discovery) +
403 on every per-session read, which closes the exfiltration path.

Adds test_demo_mode_does_not_expose_other_sessions and updates the README
DEMO-mode isolation description.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up readability pass on the DEMO-mode code:
- ws_http: use the _demo property in _handle_bootstrap instead of
  re-deriving it inline, and flatten the nested secret/localhost guard.
- ws_http/websocket: read the declared config.demo field directly rather
  than defensive getattr.
- websocket: drop redundant whitespace handling in _demo_env_int (int()
  already trims and rejects empty strings).
- spawn: declare config_key/config_cls on SpawnTool to match the contract
  every other config-bearing tool follows.

No behavior change. ruff clean; 104 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Ho1yShif Ho1yShif merged commit 0775bbf into main Jul 8, 2026
6 checks passed
@Ho1yShif Ho1yShif deleted the feat/render-demo-mode branch July 8, 2026 03:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant