The planning package had grown to where a new agent could not tell what was authoritative. Phase 0 execution prompts sat beside the specification; four completed milestone reports sat beside the current one; and upstream AI-DnD's own `plan/` build log and `docs/` project site still described a hosted, scripted, multi-user product with accounts — every screenshot in it showed a Scripts tab and a Sign up button, none of which has existed since M2. `planning/archive/` now holds the history and says so in its own README: `phase0/` for the research that chose AI-DnD, `milestone-reports/` for M1 and M2, `decisions/` for ADR 008, the Phase-0-before-build gate Phase 0 satisfied. `planning/reports/` holds only the current milestone's report, because that is the one M4 planning has to read; it moves to the archive when M4's replaces it. Deleted rather than archived: the Phase 0B execution prompts and the handoff/status/summary documents, the Phase 0A discovery and triage reports, upstream's `plan/` and `docs/` trees, and `frontend/README.md`, which was Vite's template boilerplate. All of it is in Git history, and the two recommendation reports carry every conclusion the deleted research reached. Archived documents are kept verbatim. Paths written inside them point at where those files were when the document was written, which is the point: an evidence record that has been quietly edited is no longer evidence. Active documentation is corrected where it pointed at the removed trees or described removed capability as present. `DEVELOPMENT.md`'s "things M1 did not touch" list had gone stale at M2 and claimed QuickJS scripting was still tested; its test count was 604 against an actual 638. `README.md` loses the upstream CI badge, which reported upstream's pipeline rather than this fork's, and a reference to `backend/app/worldstate/engine.py`, a file that does not exist. `planning/README.md` is rewritten as the documentation index. New: `planning/PROJECT-SOURCES.md` and `planning/project-sources.txt`, the manifest of what belongs in the ChatGPT project's Sources. Source comments referring to the deleted trees are reworded; no behaviour changes. 638 backend tests pass, frontend lints and builds, and a reference scan over all 48 tracked Markdown files reports no unresolved path in active documentation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NCbwH7yLGKsj1rhXXzKSCu
4.9 KiB
Phase 0B — Offline and Network Behavior
Method: a Docker network created with --internal (no NAT, no DNS to the
outside). The Ollama container was attached to it so the app could reach a model
while having no path to the Internet. Isolation was verified from inside the
app container before testing:
blocked ('1.1.1.1', 443) OSError
blocked ('openrouter.ai', 443) gaierror
blocked ('fonts.googleapis.com', 443) gaierror
AI-DnD — fails offline as shipped; fine once one file is vendored
First turn on the isolated network died. The SSE stream emitted the player
event and stopped. Container log:
requests.exceptions.ConnectionError: HTTPSConnectionPool(
host='openaipublic.blob.core.windows.net', port=443):
Max retries exceeded with url: /encodings/cl100k_base.tiktoken
(NameResolutionError ... Temporary failure in name resolution)
tiktoken downloads its BPE encoding on first use, and AI-DnD calls it for
context budgeting on every turn. On the host this was invisible, because the
file had already been cached to /tmp/data-gym-cache/9b5ad71b… during an
earlier online run.
After copying that 1.7 MB file into the container:
OFFLINE TURN GENERATED: "I am Vale, an explorer. I journey alone through the
darkened depths of the lighthouse..."
So: a hard blocker on a clean air-gapped install, and a packaging fix — vendor
the encoding (or pre-seed TIKTOKEN_CACHE_DIR, or replace the tokenizer). Worth
stressing that static analysis could not have found this; it took an actually
isolated run.
Other AI-DnD network surface:
- Google Fonts at runtime. The built SPA's
index.htmlstill containsfonts.googleapis.com/css2?family=Cinzel...&family=Crimson+Pro...&family=Inter..., andmain.py's CSP explicitly allowsfonts.googleapis.com/fonts.gstatic.com. Violates specification §12. Fix: self-host three families. - Only other remote host in backend Python is
https://openrouter.ai/api/v1, used as a default endpoint constant and a header-attribution host check. Both removable with the cloud-provider path. analytics.pyis a self-hosted counter writing to two local tables. No third-party script, no outbound request. It records no IP, no user agent, and hashes the user id with HMAC. Removable, and not a telemetry leak in the meantime.- Connection test and turns honour the configured endpoint only; no automatic URL retrieval or content fetching was observed.
Open Dungeon — runtime clean, build and telemetry are not
- Next.js telemetry is on by default and printed its notice on first start.
Needs
NEXT_TELEMETRY_DISABLED=1ornext telemetry disablein the production configuration. - Fonts are fine at runtime.
layout.tsxusesnext/font/google, which downloads at build time and self-hosts. The served page contains nofonts.googleapis.comreference. The trade-off is that the build needs network access. - Remote hosts referenced in
src/:openrouter.ai(preset provider URL and a model list), plusko-fi.comandgithub.com/sponsorsdonation links in the UI. Everything else is127.0.0.1/localhost(13 occurrences). - Local story play needs no cloud service: Ollama for text, a local FLUX worker or the user's own ComfyUI for images.
- 335 npm packages with 6 high-severity advisories is the largest supply-chain surface of the three.
ai-adventure — verifiably local-only
The strongest posture by a wide margin, and the easiest to audit:
- Exactly one outbound call site in the whole codebase:
urlopeninllm/lm_studio.py. Nothing else inlocal_adventure/opens a socket. - No hardcoded remote host anywhere. The only
httpstring in the package is the scheme check incontent/models.py. - One direct dependency (
pydantic), six packages in the closure. - It is the only candidate that already implements specification §12's non-local-endpoint warning:
def _endpoint_warnings(config):
hostname = urlparse(config.model.base_url).hostname
if hostname not in {"127.0.0.1", "localhost", "::1"}:
return ["model.base_url is not loopback; game prompts and content will
be sent to that endpoint. Enable API authentication."]
audit.store_promptsdefaults tofalse, with prompt hashes stored instead.- Tests, world validation, session creation, replay, branching and export are all offline by construction.
Summary
| Local play works offline | Cloud service required | Telemetry / remote assets | Removal difficulty | |
|---|---|---|---|---|
| AI-DnD | Only after vendoring the tiktoken encoding | No | Google Fonts at runtime; local-only analytics tables | Low — one vendored file, three self-hosted fonts, delete analytics |
| Open Dungeon | Yes at runtime; build needs network | No | Next.js telemetry on by default | Low — one env var; fonts already self-hosted |
| ai-adventure | Yes, unconditionally | No | None | Nothing to remove |