Skip to content
Rust conventions

Rust conventions

  • Edition: 2024 (requires Rust ≥ 1.85)

  • Format: standard rustfmt

  • Async runtime: tokio (global LazyLock runtime in gdext)

  • Grid sharing: Arc<Mutex<TermGrid>> - lock briefly, clone the grid, release

  • Thread Safety: Godot’s SceneTree is strictly single-threaded. NEVER call Godot methods, mutate nodes, or emit signals directly from background tokio threads. Instead, queue the state changes for GDScript to poll, or use Godot’s thread-safe call_deferred().

  • Lifecycle & Teardown: When a GptyTerminal is destroyed (e.g., queue_free() in Godot), the Rust side MUST ensure the spawned shell and background tokio tasks are cleanly terminated (via the Drop trait) to prevent zombie processes or memory leaks.

  • State directory creation: gpty_ipc::transport::state_dir() creates its directory on both arms. The Unix private_state_dir always did; the Windows local_app_data() now creates %LOCALAPPDATA%\gpty on first use too. This is the contract stores rely on - vaults.db (VaultStore::open), the recovery copies and the plugin store open files inside it without creating the parent - and a fresh Windows profile used to fail the wiki index with unable to open database file until the smoke’s seeded plugin record happened to create the directory. Do not move the creation into individual stores, and keep the Windows arm equal to the Unix one (a test pins local_app_dir’s creation on every platform).

  • Vault index: gpty_core::vault is the wiki pane’s note store - one SQLite FTS5 file at state_dir()/vaults.db, reached from GDScript only through the static GptyTerminal.vault_index/vault_search (they answer about a directory, not a terminal, so a pane needs no grid). Its walk (scan_vault) is the single definition of what a note is, and MAX_NOTES (2000) / MAX_DEPTH (16) / MAX_NOTE_BYTES (1 MiB) live there so the pane’s list and the index cannot disagree - the pane must not grow a second copy of those rules. It reuses the scrollback store’s skeleton deliberately: the same pragmas, the same user_version gate pattern (its own counter, now at 2 - the bump rebuilds the derived tables rather than migrating them), external-content FTS5 with the three triggers, restrict_to_owner on the db and its -wal/-shm siblings, and the process-wide write_guard held innermost across one transaction - with a (vault_id, rel_path) key and porter unicode61, because notes are prose (“running” matching “run”) where terminal lines are not. A pass re-reads only notes whose mtime (nanoseconds; seconds would miss a same-second same-size edit) or size changed, drops rows for vanished notes, prunes rows whose vault root is no longer a directory, and never prunes from a truncated walk - the notes it did not reach would be deleted. Bodies are cut at MAX_NOTE_BYTES with the torn UTF-8 tail dropped, the same cut TextRead makes on the display path, so a hit can never name text the view does not show. Both FFI functions return JSON with error present only when the query could not run, so “no match” stays distinguishable from “not indexed”.

  • A note’s label - the title a vault_search hit shows and notes_fts ranks by (title, body) - is note_title: the first heading past a leading, closed YAML frontmatter block (---…---/..., the shape Obsidian writes its properties as), else the file stem. The block never becomes the label (without the skip its opening --- was the body’s first non-empty line and every such note was named by its stem), an unterminated --- is not frontmatter so the scan behaves exactly as before, and no title: key is read - that is a properties-UI decision, not a label rule. The block’s text stays in the indexed body: it is ordinary visible Markdown (the renderer shows it), so a property value remains searchable.

  • Wikilinks (gpty_core::links, the links table, GptyTerminal.vault_links): [[target]] occurrences are parsed in the index pass - the pass already reads every changed body, so the parse is free, sits in the same transaction as the note row, and cannot drift from the text the FTS index holds. One row per occurrence (links(vault_id, src, seq, line, target, resolved), primary key (vault_id, src, seq), indexed on (vault_id, resolved)), because a backlink is not syntax: it is the same edge read in reverse (WHERE resolved = …), so nothing is authored twice and the per-mention line is what the panel lists. A fenced block, an inline code span or a backslash-escaped opener is literal text (a note documenting the syntax must not create edges); the caps are 512 occurrences per note and 1024 bytes per target; and alias/fragment are parsed but never stored - the edge is to the note, and a fragment is navigation: links_for re-parses the note body it already holds and attaches each occurrence’s fragment by seq (the pane half below follows it). Resolution (settled - do not re-derive it elsewhere): a target containing / names a path (note matches note.md), a bare target names a file stem; exact case first, then a unique case-insensitive match; more than one match at either stage is ambiguous and is never silently picked. resolved is a derived column, maintained in exactly two places: upsert_batch rewrites a row’s edges (resolving against the note set after its own inserts, so a batch that adds a stub and its linker settles both ways), and reresolve_links re-derives the whole vault when the note set actually moved (added/removed) - a stub that appears resolves its linkers, a vanished target loses its resolution, a stem that is now shared stops resolving; an ordinary pass writes nothing. A removed note’s rows are deleted with it, and clear_vault/prune_dead_vaults drop a vault’s links. The user_version=2 bump rebuilds the derived tables instead of migrating them (a v1 store’s notes were never link-parsed; the next pass re-reads the vault), and ambiguous candidates are derived at query time from the same rules rather than stored - no column for a list, and the answer cannot disagree with resolved because both come from one note set. Nothing here executes: a link is an edge, never an action.

    The pane half (2026-10): Link carries the occurrence’s byte span (start/end), and the Markdown renderer’s mention pass is the same parser, so what the preview links and what the index stores cannot disagree - a fenced block, a code span and an escaped opener are literal in both. GptyMarkdown.render(text, wikilinks) replaces each accepted occurrence with an opaque sentinel before Markdown parsing (emphasis, headings and tables around it are untouched, and the label is never re-parsed), then substitutes [url=gpty-note:<percent-encoded target>[#<percent-encoded fragment>]]label[/url] in the finished BBCode; the target and the fragment are each encoded so a space or ] cannot end the tag, and the one literal # is the only separator (a # inside either part is percent-encoded). A mention written inside a Markdown link’s label (or an image’s alt text) yields to its container and renders as plain label text: nested [url] tags leave the click target undefined, and the outer link is what the author explicitly wrote. The flag is off for every non-wiki caller (MarkdownView.wikilinks_enabled; Inspector, Reasoning and code previews keep literal [[…]]), and only this renderer can emit the scheme (safe_url still admits just http/https/mailto, so a document’s own Markdown link cannot impersonate a mention). MarkdownView never opens it: it emits note_link_clicked(target, fragment) (both percent-decoded) and the owning pane decides. Following resolves through GptyTerminal.vault_link_resolve → VaultStore::resolve_target, which runs the index’s own NoteIndex over the index’s note set (note_paths, now shared with links_for/reresolve_links) rather than a second GDScript rule - so an ambiguous stem answers with its candidates and is never picked, and a mention typed but not yet saved resolves too, because resolution needs the note set rather than the source note’s stored rows. The wiki pane’s link panel (Links to / Linked from) shows one row per occurrence with its line, an unresolved row names the target (with the candidates when ambiguous) and is not clickable, and a resolved/backlink row follows in place through _request_leave (Save/Discard/Cancel; no history stack in v1 - the notes list is the escape). A resolved outgoing row lands on the heading its mention named (fragment, re-derived by links_for from the stored body - the table still stores no fragment) and a backlink row opens the linking note at the mention’s line; a mention into the note already open replaces nothing and only moves the view, so it asks nothing. The landing is derived in two steps: MarkdownAnchors scans the source for ATX headings (leading frontmatter and fenced blocks skipped) to answer which heading a fragment - exact text first, then case-insensitive - or a source line (its nearest heading above) means, and MarkdownView.scroll_to_heading finds that heading line in the rendered parsed text, because RichTextLabel.scroll_to_line counts rendered lines while the index counts source lines. The preview scroll is queued for one frame (the label has no visual lines before its layout pass; get_character_line is -1, measured on 4.7.2), and the view skips an identical re-render - the editor’s settled render 0.4 s after a note opens would otherwise reset the scroll - while an actual render cancels a pending scroll. Edit mode scrolls the source by line directly. A fragment the note does not contain still opens the note and is reported; a line with no heading above it lands at the top, which is its nearest derived position. The panel is rebuilt when a note opens and again after a save’s re-index, and is capped at MAX_LINK_ROWS (50) per section with the remainder counted, because the parser admits 512 occurrences and backlinks are unbounded. A click is a read: resolution answers a vault-relative note and _open_note loads it through vault_note_read, with the editor’s containment, cap and regular-file gates unchanged.

  • Link graph canvas (GptyTerminal.vault_graph → VaultStore::graph → VaultGraphModel → VaultGraphView): the wiki pane’s Graph view draws the vault’s own link structure - no new file type, no second store, and nothing written. The store answers {"nodes", "edges", "truncated"} from vaults.db: every indexed note (labelled by title), a ghost:<target> node per distinct unresolved target, and one aggregated row per ordered (src, resolved) pair with its COUNT(*) of mentions (then the unresolved (src, target) rows), deterministically ordered (count DESC, then src/dst; notes by path, ghosts by target). MAX_GRAPH_EDGES (10k per class) and MAX_GRAPH_GHOSTS (2k) set truncated instead of drawing part of a vault as if it were all of it, and graph_limited is the test seam for the caps (the scan_vault_limited pattern). The pane queries it when the Graph button is pressed - every mutation (save/create/rename/delete/link rewrite) already re-indexes - and VaultGraphModel (pure, unit-tested) normalizes the answer: self-references are dropped (the panel lists them), the two directions of a pair become one edge carrying ab/ba/mutual, stroke width and layout springs are logarithmic and clamped (EDGE_MIN_WIDTH..EDGE_MAX_WIDTH, MAX_SPRING) so one hub cannot drown the drawing or collapse the graph, and the layout is deterministic (sunflower seed + FNV-1a jitter, never hash(); a size-bounded Fruchterman–Reingold relaxation with grid-approximated repulsion, because the walk’s 2000 notes make all-pairs repulsion 4M pairs per sweep). VaultGraphView is one custom-drawn Control, not a GraphEdit of node controls (the 2000-note ceiling is where that renderer stops being right): pan/zoom/drag are session geometry that a re-query preserves (only unseen ids are placed, near a neighbour; Relayout discards every drag for the deterministic arrangement), off-screen nodes/edges are culled, labels appear for all nodes only above LABEL_ZOOM (hovered/selected always), arrows are an optional toggle with mutual pairs drawn double-headed, and ghosts are outline circles. A node click opens the note through the same leave guard as the list; a ghost click runs the preview’s own _follow_target (missing → create-from-link, ambiguous → report - never a guess). Display-only: nothing in the canvas reads a path the index did not name or writes anything.

  • Declared edge weights (LinkWeights, LinkWeightsStore, vaultWeight): the link graph’s authored layer. Measured weight is COUNT(*) mentions, a crude proxy - two notes can be deeply related and mention each other once, or name-drop ten times and share nothing - so the user (a right-click Set/Clear declared weight control on an edge) or an agent (gpty vault weight <vault> <src> <target> --weight N / --clear; MCP vault-weight) may state one number that overrides it for display. Rules, settled 2026-10: (1) never in vaults.db - that store is a derived cache a schema bump rebuilds, and authored work must survive a rebuild; the map is user-owned (user://link_weights.json, {"vaults": {<absolute root>: {src: {target: weight}}}}, the PaneEnvStore/TrustedStore shape: typed reads, capped at MAX_VAULTS 32 / MAX_ENTRIES 1024 per vault and weight 1–100, atomic temp+rename 0600). (2) Display-only, absolutely: VaultGraphModel.apply_weights sets declared and weight on the parsed edge and never touches count; thickness, the layout spring and link-panel order read weight, while the drawn edge set, navigation, writes and every trust decision read only the index - an authored weight can never hide an edge, create one, gate a click, trigger a write or count as consent (the Tier 2 declaration rule). (3) Pair-shaped write, directed keys: the canvas draws one edge per note pair, so its control writes both directions of (src, target) (set_pair/clear_pair); the IPC verb writes exactly one directed pair; the model takes the larger of a pair’s two declarations. (4) Pruned lazily, never on a truncated answer: showing an untruncated graph drops declarations whose endpoint is no longer an indexed note; a truncated answer must not, because the notes it does not name are still real. An in-app rename re-keys both endpoints (LinkWeightsStore.rekey_note, one write, run by the pane immediately after the verb succeeds and independent of the optional mention rewrite) before any complete graph can prune the old name, so the curation follows the file; a stale destination row (a rename requires that note to be absent) merges with the moved row, and the moved declaration wins the shared targets. An outside move is indistinguishable from a delete to the index and still prunes (queued in Future) - the prune is the only thing that bounds the file. (5) The GUI answers vaultWeight (the store is user://, which the CLI cannot resolve), so the agent verb needs a running GUI and never auto-spawns one.

  • Note editing (vault_note_read / vault_note_write): the wiki pane’s editor reads and writes one note through the same module, so the vocabulary keeps one owner. read_vault_note reuses the index’s cut (read_file_capped), refuses any path the walk could not have produced (relative, no hidden component at any level, .md last), and requires a regular file - a FIFO planted in a vault must not reach File::open, which would block the GUI thread the FFI runs on. It answers the file’s CRLF shape and a note stamp - mtime in microseconds, not the index’s nanoseconds, because the stamp round-trips through JSON, whose numbers are doubles (1.7e18 ns loses its low bits there; 1.8e15 µs is exact) - plus size. write_vault_note edits an existing note and never creates one: the path goes through the same vocabulary, the canonicalized result must stay under the canonical vault root (so a note symlinked inside the vault is written through and one pointing outside is refused), the content is capped at MAX_NOTE_BYTES, and a stamp that differs from the one the caller loaded is refused with NoteError::ChangedOnDisk (code: changed_on_disk) unless the caller forces it. The replacement is a hidden sibling temp file (random name via temp_suffix, never a fixed one) renamed over the target, which keeps the note’s own mode - the temp file is created with the process umask, and a vault may be shared - and cannot truncate a file it fails to rewrite. vault_note_write re-indexes the vault before answering (indexed: false when the save landed but the index could not), because the file is the index’s source of truth and a caller must not have to remember a second call. Only Outside and Io failures are logged: a bad path or a stale stamp is a refusal the caller asked about, and the pane reports it through its toast or overwrite dialog.

    Creating, renaming, and deleting (2026-10): the editor’s other file verbs live in the same module and are held to the same vocabulary - a vault-relative .md path with no hidden component - with three distinctions that matter. (1) They act on the vault entry, never through it: a symlinked note is unlinked (delete) or moved as a link (rename), and its target stays where it is, because deleting must not reach a file the vault does not contain; reads and writes keep following links, with the Outside refusal. (2) Creation and the rename destination must have a directory that already exists and canonicalizes inside the vault - creating through a directory symlinked out of the vault would write outside - and never make directories. (3) A create or rename claims the destination with create_new before anything else (rename then replaces only its own claim, and removes it if the rename fails), so an existing note, symlink or directory answers NoteError::Exists (code: exists): there is no overwrite path and no check-then-write race. A rename preserves the file’s mtime/size (the caller’s stamp stays valid) and re-indexes like a save, so removed+added re-derive the link table - links by the old stem stop resolving, links by path follow it. The pane wires these to the list/toolbar: New note (default Untitled.md, a bare name gains .md, a collision re-opens the same question with the typed text), Rename and Delete on the open note (delete is a confirmation that also says unsaved work will be lost), and create-from-link for an unresolved mention or panel row - missing only, never an ambiguous stem, which stays a report. A dirty buffer’s recovery copy is re-keyed on rename and dropped on a confirmed delete; _has_open_ask counts only visible dialogs (an answered one is hidden before its action runs), because the create-from-link question’s success may open the leave question for the note being edited. A rename also offers the reference update as its own step (GptyTerminal.vault_link_rewrite_preview then vault_link_rewrite; core preview_link_rewrite/rewrite_links_after_rename): the rename makes mentions of the old stem/path unresolved, so the pane asks - with the count - before any other note is written, and only the occurrences that resolved to the old name before the move are respelled (alias and # fragment preserved, CRLF kept, fenced code and code spans untouched because the parser defines what a link is). The note at its new path is a candidate only when its buffer is clean: the pane passes that state as include_renamed (the preview and the apply take the same flag, so the question’s counts are what can land), a dirty buffer passes false and is never written under, and the apply answers renamed_note - the text that landed plus its fresh stamp - so the editor adopts both in place rather than re-reading (a save right after therefore does not see a stale stamp). A new name a link cannot spell ([, ], |, # or a control character would be re-read as syntax, and a control character breaks the line-oriented parse) refuses the whole pass rather than writing a broken mention; a note over the read cap is skipped rather than written back as a prefix. The write goes through write_vault_note and the apply re-plans in the same call, so an edit made while the question was open is read fresh and only a note that changes between that plan and its write answers changed_on_disk (counted as skipped); the apply re-indexes, so the new backlinks appear at once.

  • Note recovery copies (vault_scratch_read / vault_scratch_write / vault_scratch_clear, gpty_core::vault::write_scratch/read_scratch/clear_scratch): the buffer lives in RAM and write_vault_note runs only on explicit save, so a crash or an outside kill takes every unsaved edit - these three statics are the other half, a snapshot of the dirty buffer kept outside the vault and offered back when the same note is reopened. The layout is state_dir()/scratch/<fnv1a64(canonical root, NUL, rel path)>.json, written atomically (hidden random temp + rename) with the directory at 0700 and the file at 0600, FNV-1a spelled out locally rather than DefaultHasher (whose algorithm Rust does not promise across releases - a toolchain bump would orphan every copy). The record repeats the identity it was written for (vault, path, mtime, size, text), and a read that does not match answers absent - a stale file, a collision, or an unparseable record can never restore the wrong text - as does an unreadable one, since an absent copy is the normal state and no scratch failure is toasted (the pane cannot offer a copy that was never written, the buffer is on screen, and its own save reports what a full disk means). scratch_error logs under the rule note_failure_is_diagnostic already defines - only the failures nobody asked for - because a refusal a caller requested on purpose must stay quiet: GUT counts an engine error as a failure, and the wire test asks for bad_path deliberately. The copy’s text is the editor’s LF buffer space - the same space _saved_text is normalized into, or a CRLF note would open already dirty, write a copy of its own file, and offer it back as an edit nobody made - and its mtime/size are the stamp the buffer was loaded from, so a restore after the file moved on still hits changed_on_disk and the pane’s overwrite dialog instead of silently overwriting. The pane’s write is a throttle (SCRATCH_DEBOUNCE, armed by the first dirty edit and not restarted per keystroke: a restarted debounce never fires during a long typing run, which is exactly when the most work is at risk), and every deliberate end drops the copy - a save, the Discard answer of the close ask, and the vault change the settings panel performs without a dialog (keyed to the vault the buffer belonged to). A restore is never automatic (a RestoreConfirm dialog: Restore / Discard / Escape = not now), is refused while the note is read-only (a restored buffer could not be saved and the close guard would not see it as unsaved work, so the offer would be a promise the pane cannot keep - the copy is kept instead), and joins _has_open_ask so it cannot stack on another ask. Nothing on this recovery path touches a vault: a save or a file verb (_create_note/_rename_note/_delete_open_note) is the pane’s only note write, and a copy is never one. The copy shares MAX_NOTE_BYTES with the note it copies, so a buffer past that cap gets no copy - write_vault_note refuses its save by the same limit, and the two must not disagree about what can be written back. A prune rides every write (prune_scratch, the first thing write_scratch does - the 1.5 s throttle is the only writer, so it is a few short directory listings a minute at most, and running it before the copy is located means even a write refused for a vault that is no longer a directory tidies the directory): a copy is dropped only when its own record names a vault that is definitively gone - the path is absent, or exists as something that is not a directory - because anything else (a permission error, an unmounted network path, an I/O error) cannot say the vault is gone and a vault that is merely hard to reach must keep its copies. A copy whose note is missing is kept too: nothing on this path walks a vault, so a note that merely was not listed (a truncated walk) can never cost the work this feature exists to keep, and a deletion needs a record that says “unreachable” - never one the code could not read, parse, or find a vault in, and never a name the module did not build. Stale .gpty-scratch-*.tmp remains of a write that died mid-rename are reclaimed on the same pass once older than an hour (a live temp lives microseconds, so no writer can lose one to the age check). Every prune failure is a diagnostic - housekeeping never fails the write it rides on.

  • The vault’s public API is native IPC, not a pane (vaultSearch / vaultRead, crates/gpty-gdext/src/vault_ipc.rs): gpty vault search <vault> <pattern> and gpty vault read <vault> <path> (MCP vault-search / vault-read) expose the knowledge base to agents through the same control socket as every other primitive. Both are answered in Rust, registered in ipc.rs beside version/shutdown, because a vault is a directory rather than workspace state - there is nothing for WorkspaceIpcHandlers to consult - and the walk plus SQLite must run on the blocking pool (spawn_blocking), not on the GUI thread that polls drain_ipc_requests. Search re-indexes before it queries (index_vault: changed notes only, bounded by MAX_NOTES/MAX_DEPTH; its truncated flag rides in the answer), so a note edited outside gPTY - by Obsidian, an editor, another agent - is searchable without any pane reopening the vault; the cost is one stat walk per query, not a write. Read is a read: read_vault_note supplies the path vocabulary, the regular-file gate and the 1 MiB cut, and neither method writes to the vault. Errors are JSON-RPC: -32602 for a malformed request (missing param, non-absolute or non-directory vault) and -32000 with NoteError::code() in data.code for a refusal, so a client can tell not_found from bad_path without parsing prose. No vault command auto-spawns a GUI (may_autospawn returns false, the v0.5.6 rule): search and read are queries, and the weight write needs the GUI’s store but still refuses to create one. vaultWeight is the one vault method that is not native: its store is the user’s user://link_weights.json, which only the GUI can resolve, so it is registered in gdscript_methods and answered by WorkspaceIpcHandlers (display-only; it writes that store and nothing else).

  • Media backend (gpty-media, v0.7.0): the only crate that links FFmpeg. scripts/media-deps downloads the SHA256-pinned FFmpeg 9.0.2 tarball and builds the LGPLv3 shared library set from crates/gpty-media/components.txt (--disable-everything --disable-network, decoders/demuxers/parsers only); Windows uses vcpkg. components.txt is the single component policy, and tests/build_recipe.rs checks it against the runtime ALLOWED_* lists in both directions - adding a format means editing that file, not a second list. Every job that compiles or loads the GDExtension needs the prefix (./scripts/media-deps appends the env in Actions); local ci-check sources dist/ffmpeg/<platform>-<arch>/env.sh.

    • Trust boundary: validate_path is applied in Rust as well as GDScript (absolute, local, regular file; a FIFO/device never reaches FFmpeg), the demuxer allowlist is checked after open (input.format().name() reports an alias list, e.g. mov,mp4,m4a,3gp,3g2,mj2, so any alias must match), and the selected stream’s codec is checked by codec id (Id::name(), stable) rather than the implementation name (libdav1d for av1). Attached-picture video streams (cover art) are skipped, not treated as video tracks. Caps: MAX_STREAMS 16, MAX_VIDEO_DIMENSION 4096, MAX_FRAME_BYTES 34 MiB, MAX_CHANNELS 8, OPEN_TIMEOUT_MS 10 000 (an interrupt callback, cleared once open succeeds).
    • Decode model: one std::thread per session - never the Godot thread and never the two-worker tokio runtime. Bounded queues (VIDEO_QUEUE_FRAMES 2, AUDIO_QUEUE_CHUNKS 16) provide backpressure; send_bounded waits on a full queue while checking stop, so a pane that stops polling cannot deadlock close. poll_video/poll_audio are non-blocking. seek is acknowledged by the worker (the caller drains queued frames while waiting, so a worker blocked on a full queue still reaches the command); the worker increments an epoch before the ack, frames carry the epoch they were queued under, and poll_* discards older ones. Per-stream cursors drop frames decoded from the keyframe before the target. The worker stays alive after EOF so a seek/replay can restart it (Finished is not a death).
    • Binding rules (asserted by tests/binding_rules.rs): never use Input::packets() (its iterator’s mem::transmute_copy relies on repr(Rust) field order), container metadata (not necessarily UTF-8), frame dimension setters, sample::Buffer, filters or subtitles. Packets are read with Packet::read and resolved by stream index. ffmpeg-the-third is pinned; the facade keeps the binding replaceable.
    • Output: video is tightly packed RGBA8 (width * height * 4) from swscale with row-stride repacking; audio is interleaved f32 from swresample at the caller’s rate/channels, accumulated into ~2048-frame chunks. PTS are microseconds. GptyMedia returns one frame/chunk per call as Dictionary + PackedByteArray/PackedFloat32Array (empty dictionary = nothing queued); GString inputs are JSON.
    • Licensing: the FFmpeg build is LGPLv3 (--enable-version3, no --enable-gpl) and dynamically linked; ship its license texts with release artifacts. The build is a media attack surface: no sandboxing claim, no hardware decode yet, no network protocols compiled in.