Voicebox documentation

System Architecture

This document describes the runtime architecture of Voicebox as implemented in the repository. All generated sections below are verified against the live codebase and server by scripts/docs-check.mjs (npm run docs:check).


1. Component Overview

Component Key Files Responsibility
Browser Interface public/index.html, public/fused.js, public/style.css, public/live-voice.js, public/pip-mic.mjs, public/extensions-ui.mjs, public/settings-ui.mjs, public/components/environment-root-dialog.mjs, public/components/changelog-dialog.mjs, public/components/mic-hotkey-controller.mjs, public/components/sync-toast.mjs, public/components/audio-device-controller.mjs, public/components/activity-log-ui.mjs, public/components/mini-app-surface.mjs, public/components/harnesses-dialog.mjs, public/components/environment-list.mjs, public/components/vision-controls.mjs, public/components/deck-controller.mjs Renders the centered voice stage (#mic, #text-form), floating pop-over bubble tray (#sqeh-deck) for Files (#sqeh-files-bubble, #made-list), File Viewer (#sqeh-reader-bubble, #reader), Mini-Apps (#sqeh-actions, #mini-app-container — listing apps only: a saved, shelf or sandbox app, plus apps launched this session, never a bare workspace page, and without hardcoded default apps), and Recent Turns (#sqeh-toggle-history, #session), plus top-bar dialogs for Settings, Harnesses, Extensions, and Environments (integrating project workspace and machine/browser root configuration directly within the room UI, with a setup guide that says what setup does, why it is needed and in what order, and project creation by name).
HTTP & WebSocket Server server.mjs, lib/routes/workspace.mjs, lib/routes/extensions.mjs, lib/routes/presence.mjs, lib/routes/activity.mjs, lib/routes/agents-read.mjs, lib/routes/changelog.mjs, lib/routes/tool-catalog.mjs, lib/routes/gemini-media.mjs, lib/ws-server.mjs Zero-dependency (node:http) loopback server binding 127.0.0.1, serving static assets and REST API routes (workspace file/list/grep, extension inventory/staging/decisions, GET/POST presence, GET/DELETE activity, GET agents, GET changelog, GET tools, and the Gemini media capabilities/image/video/embed handlers live in route modules), /live voice WebSockets, and /channel routed-action sockets.
Live Voice Gateway lib/live-session.mjs, lib/commands.mjs, lib/project-instruction.mjs Manages full-duplex streaming audio and tool-call execution with Gemini Multimodal Live and OpenAI Realtime.
Turn Resolver lib/resolver.mjs Translates text transcripts from POST /api/turn into structured actions (write, read, list, make-tool, tool, etc.).
Policy & Domain Core core/root.ts, core/extensions.ts, core/tasks.ts, core/wire.ts, core/fleet.ts, core/harness-config.ts, core/mini-app.ts, core/paths.ts, core/tier-table.ts Runtime-agnostic pure TypeScript modules defining workspace roots, capability admission rules, task state machines, wire envelopes, and path normalization. Imports nothing outside core/.
Host Capabilities lib/state-dirs.mjs, lib/extensions.mjs, lib/wasm-shelf.mjs, lib/tasks.mjs, lib/task-placement.mjs, lib/fleet.mjs, lib/harness-config.mjs Server-side implementations of extension admission, digest-pinned WebAssembly execution (tools/create-asset.wat), ACP task delegation, and multi-environment fleet routing.
Dev Proxy vite.config.js, lib/browser-sources.mjs Development-only HMR front-end server on port 5173 that proxies /api, /live, /channel, and browser module graphs (core, browser, tools, tests, lib) to server.mjs.

2. Live Voice Architecture & Barge-In

public/live-voice.js captures microphone audio through the pcm-worklet.js AudioWorklet and streams PCM frames over /live to lib/live-session.mjs.

Live Voice Providers (lib/live-session.mjs): claude ((registered, but this check has no capture for it)), gemini (models/gemini-3.8-live), openai (gpt-realtime). The default fallback provider is gemini (configurable via VOICEBOX_LIVE_PROVIDER or selected per session in the UI Settings dialog).

Full-Duplex Barge-In Detection

While the assistant is speaking, microphone capture remains active so you can interrupt naturally at any time:

  1. Adaptive Client Energy Detection: public/live-voice.js monitors incoming microphone frame energy against both an absolute floor and an adaptive quiet baseline measured at the microphone. Sustained voice energy above the threshold immediately flushes local audio playback and sends { type: "interrupt" } over /live.
  2. Upstream Cancellation & Buffer Flush: On receiving { type: "interrupt" }, /live invokes session.interrupt() (canceling active OpenAI Realtime generation; Gemini Live detects barge-in server-side). When the upstream provider emits an interrupt event, the server forwards state: "interrupt" to the browser to flush any remaining buffered audio frames.

3. Workspace Roots & Turn Resolution

Browser UI ──POST /api/turn { transcript }──▶ server.mjs ──resolveTurn()──▶ lib/resolver.mjs
           ◀── { transcript, action, result } ── Executes action in active project root

Voicebox supports both machine-owned filesystem roots (kind: "machine") and browser-owned roots (kind: "opfs" for Origin Private File System or kind: "handle" for directories opened via the File System Access API, defined in core/root.ts):

Registered turn resolvers (lib/resolver.mjs): claude, gemini, openai, script


4. The Agent Loop

The table below traces a complete turn executed against a temporary workspace during documentation generation:

Step Stage Mechanism Verified Output
1. Turn Request Client submits transcript POST /api/turn { transcript } "create a file called hello.txt with hi"
2. Turn Resolution Resolver parses transcript into an action resolveTurn(transcript, "script") in lib/resolver.mjs {"verb":"write","name":"hello.txt","content":"hi"}
3. Action Execution Executor runs action inside the active root execute(action) in server.mjs (POST /api/root) wrote hello.txt (2 bytes) (root.kind: "machine")
4. Turn Response Server returns structured result to client { transcript, action, result } ok: true, logged: 2
5. Audit Trail Append-only log records 2 entries (attempt/attempted → allow/writes-inside, linking outcome to attempt sequence) and 1 entry for pre-flight refusal <root>/.audit/<writer>.jsonl (core/shared-log.ts), GET /api/audit seq 1 attempt, seq 2 allow; refusal: seq 3 refuse/outside-root

Boundary & Admission Guarantees (Verified Against Live Server):

Unified Workspace Root: Admitted file extensions operate on the active project root (["hello.txt"]).

Extension Registry Integrity at Startup

When the server starts or reloads the extension registry, lib/extensions.mjs re-runs the admission gate (core/extensions.ts) on every admitted descriptor and cross-checks .ledger.jsonl:


5. Input Paths & Tool Surface

User input reaches the shared action executor through three paths:

Input Path Active Transport Execution Pipeline
Text Composer Yes public/fused.js → POST /api/turn resolveTurn() (lib/resolver.mjs, default script) → execute() (server.mjs) → callTool() (lib/extensions.mjs)
Browser Dictation (SpeechRecognition) Yes public/fused.js → POST /api/turn Same pipeline as Text Composer
Live Voice Audio Audio: Yes; Tools: Yes public/live-voice.js → /live → lib/live-session.mjs Provider tool call → commandToAction() → execute() → correlated tool response + {type:"tool"} UI notification

Live Session Tool Declarations: claude declares: (not captured); gemini declares: list_extensions, call_extension, propose_extension, write_file, read_file, list_files, delete_file, edit_file, diff_file, grep_files, list_agents, delegate_task, contact_agent, launch_mini_app, git_status, git_diff, git_log, inspect_environment, undo_last_action, list_tools, search_tools, run_command, open_workspace; openai declares: list_extensions, call_extension, propose_extension, write_file, read_file, list_files, delete_file, edit_file, diff_file, grep_files, list_agents, delegate_task, contact_agent, launch_mini_app, git_status, git_diff, git_log, inspect_environment, undo_last_action, list_tools, search_tools, run_command, open_workspace.

Script Resolver Sample Utterances: "create a file called hello.txt with hi" → write, "read hello.txt" → read, "list files" → list, "create a tool called clock that tells the time" → make-tool, "run the tool clock" → tool.

Built-In Extension Primitives (6 closed primitives in core/extensions.ts; extensions parameterize primitives as pure JSON descriptors rather than executing arbitrary model-authored code):

Primitive Capability Required Host-Mediated Interface
now — Returns current host timestamp
read-file read a root-scoped read function: paths resolve inside the project root or refuse
write-file write a root-scoped write function: paths resolve inside the project root, writes are reported and revertible
list-files read a root-scoped read function: paths resolve inside the project root or refuse
http-get network a mediated fetch: hosts outside bounds.hosts are refused by name — INCLUDING across redirects, every hop charged to bounds.maxRequests — and the audit records the URL that actually served the bytes
wasm — Isolated WebAssembly module verified by SHA-256 digest at admission and invocation under strict host memory and timeout ceilings

Capabilities Prohibited on machine Placement:

Extension Catalogue (catalogue/*.json, 4 descriptors):

Extension ID Tools Declared Capabilities Bounds Admission Verdict
brave-search brave_search → http-get network hosts: api.search.brave.com; maxRequests: 20 admitted — network via mediated-fetch
mcp-server-local mcp_list_tools → process exec command: npx -y @modelcontextprotocol/server-filesystem /tmp refused exec-absent
mcp-server-remote mcp_remote_list_tools → http-get network hosts: mcp.example.com; maxRequests: 20 admitted — network via mediated-fetch
web-search web_search → http-get network hosts: api.duckduckgo.com; maxRequests: 5 admitted — network via mediated-fetch

Named Refusal Codes by Subsystem:

Runtime Inspection & Admission Endpoints:


6. Environment Variables & Configuration

Environment variables read by the server and runtime libraries:

| Variable | Read In | Description | |—|—|—| | ANTHROPIC_API_KEY | lib/live-providers/claude.mjs, lib/pi-acp.mjs, lib/resolver.mjs, server.mjs | Anthropic API key used by the claude resolver/provider and forwarded to the pi-acp adapter as a fallback when no store credential exists (can also be configured in the UI Settings dialog). | | BRAVE_API_KEY | lib/extensions.mjs | Brave Search API subscription token used by http-get extensions targeting api.search.brave.com. | | FORCE_COLOR | lib/logger.mjs | Terminal color override (0 disables ANSI colors in lib/logger.mjs; non-zero enables them when stdout is not a TTY). | | GEMINI_API_KEY | lib/gemini-models.mjs, lib/live-providers/gemini.mjs, lib/resolver.mjs, server.mjs | Google Gemini API key for Gemini Live voice sessions and the gemini text turn resolver (can also be configured in the UI Settings dialog). | | LIVE_PROVIDER | lib/live-session.mjs, server.mjs | Deprecated alias for VOICEBOX_LIVE_PROVIDER, retained for backward compatibility. | | NODE_DISABLE_COLORS | lib/logger.mjs | Node.js built-in flag that disables ANSI terminal colors alongside NO_COLOR. | | NO_COLOR | lib/logger.mjs | Disables ANSI color sequences in lib/logger.mjs when set to a non-empty value. | | OPENAI_API_KEY | lib/live-providers/openai.mjs, lib/resolver.mjs, server.mjs | OpenAI API key for OpenAI Realtime voice sessions and the openai text turn resolver (can also be configured in the UI Settings dialog). | | PATH | lib/claude-acp.mjs, lib/tool-index.mjs | System executable search path, also inherited by task-adapter child processes. | | PORT | server.mjs | HTTP server port bound on 127.0.0.1 (default 8787). | | VOICEBOX_ACP_ADAPTER | lib/pi-acp.mjs | Path or command override for the pi-acp stdio adapter binary in lib/pi-acp.mjs. | | VOICEBOX_ACP_PI | lib/pi-acp.mjs | Path or command override for the pi coding agent CLI used by lib/pi-acp.mjs. | | VOICEBOX_BIND_DEADLINE_MS | server.mjs | Maximum duration in milliseconds to retry binding the server port before failing. | | VOICEBOX_BIND_RETRY_MS | server.mjs | Interval in milliseconds between port bind retries at startup. | | VOICEBOX_CLAUDE_CLI | lib/claude-acp.mjs | Path override for the Claude Code CLI executable (CLAUDE_CODE_EXECUTABLE) used by lib/claude-acp.mjs. | | VOICEBOX_CLAUDE_KEEP_API_KEY | lib/claude-acp.mjs | Set to 1 to retain ANTHROPIC_API_KEY in the Claude Code adapter child environment (omitted by default so CLI login takes precedence). | | VOICEBOX_ENABLE_STUB_PROVIDER | server.mjs | Set to 1 to register the key-free stub live voice provider for local audio testing. | | VOICEBOX_EXTENSIONS_DIR | lib/state-dirs.mjs | Host state directory storing admitted extensions, .host-token, .ledger.jsonl, .pairings.json, .api-keys.json, and .harness-settings.json (mode 0600). | | VOICEBOX_HARNESS | server.mjs | Default host coding agent harness (pi or claude; can also be switched at runtime in the Harnesses UI dialog). | | VOICEBOX_HELLO_BOUND_MS | server.mjs | Timeout in milliseconds to receive an authentication hello frame on /channel or /live (default 5000). | | VOICEBOX_INSTANCE | server.mjs | Writer identifier recorded in the active workspace’s .audit/<writer>.jsonl log (default machine). | | VOICEBOX_LIVE_PROVIDER | lib/live-session.mjs, server.mjs | Fallback live voice provider (gemini, openai, or claude) when the client session does not specify one. | | VOICEBOX_LOOPBACK_AUTH | server.mjs | Set to 1 to require a single-use bootstrap ticket (?bootstrap=<ticket>) and HttpOnly session cookie for local browser access. One ticket is minted at startup, and its startup text names the host it must be opened on: a cookie is scoped to the host that redeemed it, so a 127.0.0.1:8787 ticket opened from a localhost:5173 page leaves every /api call on that page answering 401 (open the same ticket on localhost:5173 when that is the dev front you browse). Redemption answers 303 to the plain route with the ticket removed, so the first refresh is authenticated rather than a re-used-ticket 401. Default off (accepted testing posture): with the gate off, unauthenticated loopback clients — including browser-originated requests — can reach state-mutating routes such as active-workspace writes, so the server prints a startup warning naming the exposure and this remedy (see docs/18-loopback-session-auth.md). | | VOICEBOX_OPENAI_INPUT_TRANSCRIPTION | lib/live-providers/openai.mjs | Set to 1 to enable gpt-4o-mini-transcribe input audio transcription in the OpenAI Realtime session handshake. | | VOICEBOX_PROVIDER | server.mjs | Deprecated alias for VOICEBOX_RESOLVER, retained for backward compatibility. | | VOICEBOX_RESOLVER | server.mjs | Default text turn resolver used by POST /api/turn (script, gemini, openai, or claude; default script). | | VOICEBOX_SANDBOX_HOMES | lib/state-dirs.mjs | Base directory for fenced sandbox home directories (default ~/sandbox-homes/<key>, located outside /tmp for PrivateTmp compatibility). | | VOICEBOX_WASM_SHELF_DIR | lib/state-dirs.mjs | Directory containing the digest-pinned WebAssembly tool shelf (manifest.json and .wasm binaries; default ~/.isocan/modules/wasm-tools). | | VOICEBOX_WORKSPACE | lib/state-dirs.mjs | Declares an active machine project root at startup and stores extension proposals (proposals/) and extension audit logs (audit.jsonl). |


7. HTTP Routes & Frontend Scripts

The HTTP server (server.mjs, built on node:http) binds 127.0.0.1 and serves the core routes below:

Method Route Probed Status
GET / 200
GET /api/health 200
GET /api/files 200
POST /api/turn 200

Static frontend assets are served from public/. GET /api/health reports the active turn resolver (provider: "script"), whether a workspace root is declared (declared: false), and the active root: { kind, path } configured via POST /api/root. Before a root is declared, root-scoped file operations return root-not-declared.

A WEBSOCKET UPGRADE ON /live IS ACCEPTED (101) — the zero-dependency server owns it.

The room presence note in public/fused.js treats /api/health as the first machine check, not as a promise that later requests will reach it. If a later /api/root or /api/environments fetch loses the connection, the note and server dot immediately say the machine is not answering; an HTTP response with a route-specific refusal still names that route instead. tests/presence-downstream-drop.test.mjs stops the server between the successful heartbeat and the environment fetch to guard this distinction.

lib/routes/presence.mjs owns only exact GET /api/presence and POST /api/presence table handlers. server.mjs still constructs the single room-presence coordinator, applies the global loopback auth wall before dispatch, and supplies its bounded JSON reader, response writer and channel broadcaster; the route does not copy those security and lifecycle owners (voicebox-beads-gdq2.1).

lib/routes/activity.mjs owns only exact GET /api/activity and DELETE /api/activity table handlers. server.mjs retains the one mutable activity buffer, its producers, bounded trimming and workspace-reset behavior; the route receives that same array and clears it in place, never replacing the producer’s reference. The global loopback wall remains host-owned (voicebox-beads-gdq2.2).

lib/routes/agents-read.mjs owns only exact GET /api/agents: it filters the host registry and applies a public, secret-free projection plus the admission verdict from a fresh host context on each GET. server.mjs continues to own mutable harness/adapter configuration, the global loopback auth wall and the separate privileged POST /api/agents and PATCH /api/agents/:id handlers. The registration alias environmentKey: "local" is projected as this host’s minted environment key when querying the registry (voicebox-beads-gdq2.3).

lib/routes/fleet.mjs owns only exact GET /api/fleet, filtering and projecting the host’s current fleet list. POST /api/fleet/contact stays in server.mjs: it forwards a caller bearer and mints an audit token across an environment boundary. The GET spread keeps its position between agents-read and that host-owned POST, before harness routes (voicebox-beads-r9hv.3).

lib/routes/changelog.mjs owns only exact GET /api/changelog and its single private cache. It preserves the host’s raw .git/HEAD plus ref-mtime :v2 key, cache miss when HEAD is unavailable (as in a worktree .git file), bounded git log execution and original 200/500 JSON envelopes. Global loopback auth, the separate git-log tool verb, and the following HEAD/GET health handlers remain in server.mjs (voicebox-beads-gdq2.4).

lib/routes/tool-catalog.mjs owns only exact GET /api/tools — the room’s tool catalogue, listing or searching (?q=, ?kind=, ?limit=) across native, extension, system, script, mini-app and harness capabilities. server.mjs still owns everything it hands over: the two lib/tool-index.mjs functions, the mini-app tool registry, the harness inventory plus its configured-agent combination, ROOT, and the global loopback auth wall. The active root moves as a LIVE accessor, never as a value: active is a module-level let that is reassigned when a machine root is declared or deleted, and this route lists from whichever tree is active at request time, so the host passes getActiveRoot: () => active and a snapshot would answer from the boot-time root forever — the voicebox-beads-w1f9 shape, which tests/tool-catalog-route-seam.test.mjs turns red by calling the route twice with the accessor changing in between. The route’s original harnesses = [] fallback for an unavailable harness inventory is preserved, as is the unchanged Number(...) handling of limit. Measured, and stated so nobody claims otherwise: this extraction does NOT make the fast gate faster. scripts/test-changed.mjs maps a change to this module to ~137 tests, because server.mjs imports it and 137 test files name server.mjs; the win is that server.mjs is the repo’s collision hotspot (214 commits in 30 days against 1-2 for each lib/routes module). The seam test also pins the table position (after the tasks block, before GET /api/keys) with self-proving missing-spread, shadowing and snapshot mutants, and tests/tool-index.test.mjs keeps driving the route end to end over HTTP (voicebox-beads-r9hv.2).

lib/routes/gemini-media.mjs owns the four CONTIGUOUS Gemini media entries — exact GET /api/gemini/capabilities, POST /api/gemini/image, POST /api/gemini/video, POST /api/gemini/embed — spread back into the table at the position they occupied, between ...activityRoutes and DELETE /api/root, because in this table order is dispatch. Two seams define the slice. The active root moves as a LIVE accessor: POST /api/gemini/image honours saveAs only when the active root is a machine root, and active is reassigned when a root is declared or deleted, so the host passes getActiveRoot: () => active and each handler reads it per request — a snapshot would freeze the boot-time root (voicebox-beads-w1f9), which tests/gemini-media-route-seam.test.mjs turns red by changing the accessor’s answer between two calls. The provider calls arrive as dependencies (generateGeminiImage, startGeminiVideoGeneration, pollGeminiVideoOperation, embedGeminiTexts, semanticSearchDocuments), which is what makes the slice testable at all: the focused test drives every branch, including all three refusal paths, with injected fakes, and a fetch spy that throws on contact proves nothing in it reaches an external media API — a mutant that adds a real fetch call to the module dies on that spy, so the check has teeth rather than being decoration. server.mjs keeps the providers, CONTENT_JSON_BODY_MAX_BYTES (the image route’s larger cap, while the other two POSTs take the reader default), resolveActive, the global loopback auth wall, and the refusal shapes and their order: a non-JSON body is refused with 400 bad-request BEFORE any provider call, which is why a refusing request can never make an outbound call. The FENCED static/CSP entries are untouched, and no fast-gate speed win is claimed — a change to this module still maps to ~137 tests, because server.mjs imports it and 137 test files name server.mjs (voicebox-beads-r9hv.4).

A staged Content-Security-Policy — reported, not enforced (voicebox-beads-ul1m.1; parent voicebox-beads-ul1m, ADR voicebox-beads-28sc). Responses to the room, help.html, changelog.html, environment.html and harnesses.html carry Content-Security-Policy-Report-Only holding, character for character, the policy public/index.html already enforces through its meta tag — six request spellings covering five documents, since / and /index.html are the same file. Nothing is blocked: every violation carries disposition=report, and there is deliberately no report endpoint, so violations appear in a devtools console and in the page’s own securitypolicyviolation event — which is how tests/csp-report-only-browser.test.mjs asserts them. The header is keyed on the RESOLVED FILE, never on the requested URL and never on a content type: cspReportOnlyHeaders takes the path that resolvePublicFile has already decoded, containment-checked and realpath’d, and reduces it to its public path. That is a fix for a hole a reviewer found rather than the original shape: keyed on the raw request path, /%68elp.html, /help%2Ehtml and /help.html/ all reached the same Help document and were served with no header at all, the trailing-slash spelling needing no encoding to type. Patching the comparison with a decode would have closed those three and left the next alias open; keying on the file collapses every spelling — encodings, dot segments, trailing slashes — to one string by construction, and tests/csp-report-only.test.mjs asserts that each alias carries the policy AND serves the same bytes as the canonical spelling, with traversal and case-mismatch spellings asserted as still unresolvable so “covered everywhere” cannot be satisfied by covering everything. It remains a list of documents rather than a rule phrased as “any text/html response”, which would silently cover the mini-app pages under public/apps/, the mini-app-bridge.html host — excluded because it is the frame that renders user HTML, and because srcdoc children inheriting that policy is spec behaviour this change did NOT measure and so did not rely on — and every document added later by someone who never read the server code. Those are excluded, along with every asset and /api route. What the reports currently say is nothing at all. The worklist emptied with voicebox-beads-xggy.3, which moved environment.html’s inline stylesheet to public/environment.css and its one inline style attribute to a rule in that file: both were what style-src 'self' and style-src-attr would have blocked, and both are gone. That page also declares its scheme in the head as a <meta name="color-scheme">. Its effect is narrower than it first appears: measured with the real external stylesheet, on both OS schemes, the page’s own first painted frame is identical with and without the meta, because the render-blocking stylesheet already supplies the scheme and the ground. What the meta reaches was measured on one surface only: the canvas the browser draws for the page. With /environment.css held so that the stylesheet could not supply a scheme at all, the UA’s Canvas resolves dark with the meta and light without it on an otherwise identical page, and CanvasText follows. Scrollbars and the caret run on the same mechanism and the same hint covers them, but those surfaces were NOT driven here and this page does not claim to have measured them. A preventive hint, not a fix for a flash in the page’s own content. Its root-declaration script was the third until it was extracted to public/environment-root.js (voicebox-beads-xggy.2), and help.html’s search filter went the same way to public/help-search.js (voicebox-beads-xggy.1); an inline script is the one thing script-src 'self' would block, and the Modern Web Guidance security guide names refactoring as the alternative to nonces and hashes. Both of that page’s scripts are loaded WITHOUT defer, deliberately, and that is a behaviour requirement rather than a style choice — environment-root.js for the declaration and environment-dialogs.js for the create-project opener (voicebox-beads-5tv3). Inline at the end of <body> it ran while the document was still parsing, so its click listener always existed before a person could click “Declare this folder”. A deferred file attaches once the document is already interactive, and a click landing in that window is lost permanently — unlike a search box, a click leaves no state to re-apply, so the fix used on the Help page (apply the value already present) cannot recover it. tests/csp-report-only-browser.test.mjs holds the file at the request stage and requires the document NOT to have become interactive, measuring the same technique against a file that IS deferred as its control, and tests/csp-report-only.test.mjs fails if the tag ever gains defer, async or type="module". environment.html is now the only allowlisted page that still reports anything, which is what keeps the alias-coverage signal non-vacuous. index.html reports nothing, which is the check that this is a mirror of the meta and not a second, stricter policy in disguise.

The shared static-file resolver refuses decoded NUL bytes as not-found before filesystem calls and still rejects traversal/symlink escapes, while unexpected filesystem failures remain server errors (voicebox-beads-mgbv).

Every route that reads a JSON body reads it through one bounded door in server.mjs (readJsonBody, voicebox-beads-d808): a body past the cap (1 MiB by default; routes whose body carries content — file text, mini-app HTML, extension source, tool args, base64 reference images — pass 8 MiB) is refused 413 body-too-large with the rest of the upload discarded unread, and a declared non-JSON Content-Type is refused 415 unsupported-content-type before a byte is read. A refused body never reaches the route. The one exception is POST /api/mini-app/tools, which predates the door with its own stricter bound (64 KiB, refused 400 body-too-large — see docs/17).

public/index.html loads theme-boot.js, fused.js, pip-mic.mjs, live-voice.js from public/. AudioWorklet modules loaded by the frontend audio engine: pcm-worklet.js.

verify.mjs resides in public/ as a standalone verification utility and is not loaded by index.html.


8. Subsystem Ownership & Boundaries