Security and input bounds

vibe-sketcher runs as a local server that the browser shell, the desktop window and agents talk to, and it reads files that come from elsewhere: a co-author’s ChemDraw file, a QVF from another program, a SMILES list from a database. This page records what it trusts, what it bounds and what is still open. It is the first part of the 0.8 reliability and security qualification (issue #69). That qualification as a whole is not complete, and no support claim rests on this page.

What is trusted

Party

Trusted with

Why

The user at this machine

Everything the server can do, including writing files they name

It is their sketcher.

Agents the user starts (vibe-sketcher mcp)

The same, through the REST API

They act for the user.

Web pages in the user’s browser

Nothing

Any site can make the browser send requests to 127.0.0.1.

Files and pasted data

Their contents only

They are read, never run.

Other machines on the network

Nothing, unless the user binds to a network interface

The default bind is 127.0.0.1.

The local server

vibe-sketcher serve and the desktop window bind to 127.0.0.1 by default. Such a loopback server:

  • answers only requests whose Host is a name of this machine (127.0.0.1, localhost, ::1), and refuses others with 421. A site that points its own name at 127.0.0.1 (DNS rebinding) shares the server’s origin in the browser, but it still sends its own name.

  • refuses a request whose Origin is another site’s, or null, with 403. The browser shell, the desktop window and a development server on localhost are on this machine and pass. Malformed Host or Origin values, including invalid bracketed addresses, receive the same structured 421 or 403 refusal before document operations run; they do not cause a server error or prevent a subsequent valid request.

A server bound to another interface (--host 0.0.0.0) was asked for by name. It takes any Host, prints that anyone who can reach it can use the API, and leaves access control to the network. It has no login.

Every request body is read up to 272 MiB (the largest QVF in base64, with room to spare) before the application parses it; a longer one answers 413.

What a file may hold

Each check runs before the work it protects, and a refusal says which bound was met (vibesketch.limits):

Input

Bound

Protects

An imported file or text, a .vsk document

64 MiB

parsing

A QVF archive

192 MiB packed; 10,000 files; 64 MiB for a JSON member and 1 GiB for any other member, unpacked; 2 GiB in all

memory: a small archive that unpacks to gigabytes (a zip bomb) is refused from its directory, before anything is unpacked

An imported run result or result-view companion

32 MiB

result parsing and retained companion data

JSON (KET, .vsk)

64 levels of nesting

the parser and the validator; JSON too deep for the parser itself is refused too

One import

25,000 atoms

derivation of species and reactions

A structure without 2D coordinates (SMILES, InChI, a name, XYZ)

2,500 atoms

2D layout, which grows faster than linearly: 5,000 atoms take about 17 s; a drawn structure of that size, with coordinates, imports

All the structures without 2D coordinates in one import (several SMILES, a FASTA file of sequences)

5,000 atoms

2D layout: molecules of 500 to 1,000 atoms are the slowest per atom, and five of them take about 19 s; structures with coordinates (MOL, SDF, KET) do not count

CDXML

expat’s limits on entity expansion

memory: the “billion laughs” document is refused by the XML parser

A campaign

2,000 nodes (two per species and stage); 4,096 cpus and 16 TiB of memory per node

disk: every node is a directory of files written before the queue sees it

A JSON specification or campaign record: the CLI’s recipe-set --from, --coefficients, campaign-review --review and --stage-runtimes files; a campaign’s own campaign.json, run journal, manifests, checks, verdicts and geometries; a stage’s result container

64 MiB (a result container: the QVF bounds); 64 levels of nesting; NaN and Infinity refused, as JSON has neither

memory and the parser: a co-author’s specification or campaign is read like an import

Document loading (SDK and server startup) and CLI file conversion check the size of the opened file before reading it into memory. They also cap the read at the permitted size plus one detection byte, so a file that grows after the size check is refused too. The limits count file bytes, before UTF-8 decoding. Pasted or SDK text is counted as UTF-8 bytes too. JSON numbers that overflow the finite floating-point range (for example 1e999) are refused alongside explicit NaN and Infinity; they cannot silently become infinite energies or coefficients. CLI conversion uses the selected input format’s limit, including an explicit --from qvf for an archive with another suffix. A refused conversion leaves an existing output file unchanged.

The same capped reads apply to CLI import-result and attach-view-file (32 MiB), and return-geometry (192 MiB for a returned QVF). These commands refuse oversized or growing source files before result parsing or attachment, and leave the input document and existing output unchanged on refusal.

The CLI’s specification files are read the same way, from a file or from standard input (-), and a refusal names the file’s role (the review decision, the stage runtimes) and the bound it met. It leaves the document and any output directory as they were. A campaign’s records are read within the same bounds when results, reviews and renders read them, and a stage’s result container passes the QVF archive checks before anything in it is unpacked. The stage driver and validator that run on a compute node read only the files vibe-sketcher and vibe-scheduler staged for them, and use the Python standard library alone, so they are not bounded here.

Result artifacts retained for vibe-viewer are read back from vibe-sketcher’s cache within the same 32 MiB, and an existing cache file is checked by a streamed digest, never read whole. The cache is the user’s directory and can change outside vibe-sketcher, so an oversized entry is refused with the way out (reimport the file). vibe-viewer’s converted result is read up to the QVF bound. Campaign source pins are digested in streamed chunks, so a large source file is hashed without being held in memory, and the desktop viewer’s log is read only up to its last 16 KiB.

MCP open_document and import_run_result apply these limits locally before decoding text, encoding uploads or sending REST requests. Starting MCP with QVF files against an existing server also caps each local read. A refused file is not uploaded and cannot create or update a server document.

A canvas that is not one, such as KET whose nodes are not a list, is refused as an invalid canvas instead of failing inside the server.

Files are read, never executed: formats are parsed into data, and no imported file names a program to run. The routes that take a filesystem path (campaign and render directories) and Save, which writes to the file the server opened, act for whoever passes the checks above: the user and their agents.

Saving

Every save writes a hidden temporary file beside the target, makes it reach the disk, renames it over the target and then syncs the directory. A crash, a full disk or an interrupted save therefore leaves either the old file or the new one, never a mixture. A crash at the wrong moment can leave a hidden .NAME.*.tmp file beside the document; it is safe to delete.

A document the server opened from a file (vibe-sketcher serve FILE, the browser’s Save) remembers the file’s bytes as it read or last wrote them. If another program changed, replaced, deleted or created the file since, Save is refused with 409 and the file is left as the other program wrote it. The edit stays in the open document: reopen the file to see the other change, or keep this version with a download under another name. The desktop window checks the same before its Save. A stale revision (two clients editing at once) answers 409 with the current revision, and the client re-reads; missing optional programs answer with an installation hint instead of failing.

External-change digests stream the complete file in bounded chunks, even if another program replaced it with a much larger file. Inspecting a QVF save destination uses the same 192 MiB packed limit as opening one, including symlink targets and files that grow while being inspected. An oversized destination is left untouched; use Save As to keep the new drawing elsewhere.

When the server cannot save because the disk is full, its save route returns 507; other filesystem errors return 500 with a readable explanation. Edits remain open and unsaved. Check disk space and permissions and retry, or download a copy. Failures before the atomic rename leave the original file unchanged and remove the temporary file.

Directories

Compiling a campaign and rendering its stages write into a directory the client names. It must be absolute, and new or empty; a directory with anything in it is never touched. If the write fails (no permission, a full disk), the refusal says why. A directory that was made for the write is removed, and one the client gave is left empty, as it was. The paths a campaign’s own files and records hold may come from a document someone else wrote, so a render refuses any that lead outside the campaign, and a campaign whose files are missing or unreadable is refused with the reason.

A drawing sent over REST (PUT /api/documents/{id}/canvas) and a posted document meet the same bounds as an import: nesting, atoms, and a canvas that is one.

Updating

scripts/update.sh only fast-forwards the checkout, so an interrupted fetch leaves it as it was. With --recreate-venv, the previous virtualenv is set aside as VENV.previous until the new one is installed and answers; if the update fails or is interrupted (Ctrl-C included), the new one is removed and the previous one restored. A VENV.previous left behind by a killed update stops the next update with the way out: move it back or delete it. An update without --recreate-venv reinstalls into the existing virtualenv, where pip replaces one package at a time; run the update again if it was interrupted.

Desktop startup

The desktop window starts its own backend and waits for it. Only the backend’s own answer counts as ready (200 {"status": "ok"} from /health), so another program on the port, or an error page, never opens the editor. A backend that dies while starting ends the wait at once and the setup window says it did not start, with its exit status in the log. A backend that stops later closes the editor window and shows The vibe-sketcher server stopped, with Try again to start a new one; changes since the last save were held by the stopped backend.

Still open

These parts of #69 are not done yet:

  • Clean installation and packaged flows on each supported platform.