This reference describes the implemented native runtime. Historical paths and internal version labels are retained where relevant. Later extension chapters supersede older limitations.
Vanilla1 now supports server-side HTML/JSON applications through a CGI front controller, not an Apache module. Each request starts a fresh interpreter process. The front controller is Python 3 standard-library code; the language request/response API is implemented in C++. There is no shell evaluation of request data, no persistent worker, and no HTML-embedded template syntax.
Request flow
Apache → fixed executable launcher → gateway/vanilla1_cgi.py →
vanilla1 --cgi-worker ... run /operator/selected/script.v.
The gateway parses CGI input, supervises the worker and validates its entire
JSON response before sending CGI headers or body. A script that prints half a
page and then fails produces a generic 500, not half a successful response.
Normal CLI run output is unchanged. --cgi-worker is an internal protocol,
not a CGI entry point: do not point Apache directly at the interpreter or use
the historical unsupported cgi command. Use the fixed launcher.
Language API
web request $object
web status 201
web header Content-Type "text/html; charset=utf-8"
web header Set-Cookie "session=...; Secure; HttpOnly; SameSite=Lax"
web escape $safe $text
print "<p>" $safe "</p>"
web requestreturns a structured JSON object, assignable to normal JSON/map operations. The object hasmethod,path,query_string,query,form,headers,bodyandremote_addrfields. Request changes in script variables do not change the original request.pathis Apache's PATH_INFO relative to the launcher, not a filesystem path. Missing/empty PATH_INFO maps to/. Only exact configured routes are accepted.queryandformare objects whose values are arrays of strings. Repeated keys remain repeated, empty values remain empty.+decodes to a space; percent escapes and decoded UTF-8 are validated strictly. NUL parameters are rejected. Read a value with successivejson getoperations andjson text.headerscontains lower-case names derived from CGI HTTP_* variables plus Content-Type. HTTP_PROXY is omitted. Headers remain untrusted input; proxy forwarding headers do not establish client identity. Duplicate raw HTTP headers may already have been combined by Apache.bodyis the exact UTF-8 text body, not an automatically flattened object. JSON syntax is validated on JSON input; scripts can usejson parseand their own schema checks. Binary uploads and multipart forms are not supported.web statusaccepts 200..599. Default is 200. Body is suppressed for HEAD and statuses 204/205/304. HEAD receives the would-be body length where appropriate.web headerreplaces a header case-insensitively; Set-Cookie appends. Names are restricted to letters, digits and hyphens; values must be printable ASCII, with no CR/LF. Status, framing/hop-by-hop headers and gateway-generated X-Vanilla-Request-Id / X-Content-Type-Options are reserved. The gateway repeats validation and sets Content-Length from the encoded body, never from a script.printandechoappend to the buffered body in worker mode. Default media type is text/plain; charset=utf-8. All response text must be valid UTF-8.web escapeescapes& < > " 'for HTML text/quoted-attribute contexts. It is also available in normal CLI mode. It is not JavaScript, CSS or URL escaping. Use appropriate encoders and schema checks for those contexts.
Examples in examples/cgi/: hello.v (escaped HTML with a restrictive CSP),
request.v (request inspection), sqlite.v (prepared query). The request
inspection example can expose submitted cookies or other secrets; do not
publish it as a production diagnostic endpoint.
Deployment shape
Use a dedicated unprivileged application identity such as vanilla-web in a
dedicated Apache instance/container. Do not run the gateway as root: it refuses.
The test harness starts the Apache parent as root solely so it can drop actual
CGI execution to nobody; production should use a dedicated application user,
not a shared nobody or a shared account for mutually untrusted tenants.
Install the native executable and its shared-library dependencies, Python 3, and the gateway at operator-owned locations outside DocumentRoot. Example paths:
/opt/vanilla1/bin/vanilla1
/opt/vanilla1/gateway/vanilla1_cgi.py
/opt/vanilla1/cgi-bin/app # executable fixed launcher
/etc/vanilla1/web.json # operator configuration
/srv/vanilla1/app/hello.v # source outside DocumentRoot
/srv/vanilla1/app/request.v
/srv/vanilla1/app/sqlite.v
Templates: gateway/app.cgi.example, gateway/web.json.example, and
gateway/apache.conf.example. Replace/install paths deliberately; copying
these files into the repository does not modify or deploy any live server.
The launcher deliberately omits $@: CGI indexed-query arguments must never
be forwarded as interpreter/gateway switches. Run Python with -I to ignore
ambient Python import/path settings.
The operator config supplies absolute binary and root paths, an exact
route→relative .v script map, optional permissions, optional sql_config,
timeout, read_timeout, max_steps, and a test-only sanitizer boolean.
Unknown options fail closed. Defaults: no permissions, 5-second execution and
body-read deadlines, 100000 instructions. No listen permission can be granted
through this gateway. Permission flags are fixed per application, not per URL.
Use different app configurations/services for different privilege sets.
Configuration, binaries and selected scripts must be owned by root or the application UID and not group/other writable. Final-component symlinks are rejected for config/binary; route script components below the canonical root cannot be symlinks. Parent directories must remain operator-controlled; this is not an openat-based filesystem sandbox and does not eliminate replacement races by trusted owners. Includes retain the language's existing behavior.
For example, keep config root-owned, mode 0640 with application-group read access; binaries/scripts should be root-owned and not application-writable. Keep writable data and uploads in a separate directory, never the script root or CGI executable directory. Apache must not alias the script root or private configuration directory into public URLs. Use a dedicated DocumentRoot for static content and only ScriptAlias the fixed launcher. Test config changes with Apache's syntax check before deployment.
Remote SQL additionally requires permissions: ["db", "network"] and an
absolute sql_config path. The SQL profile file must satisfy the native SQL
policy: owned by the effective application UID, mode 0600/0400, regular file.
No DB passwords belong in Vanilla scripts, launcher arguments, web.json or
request headers. Use restricted database accounts and verified TLS; see
REMOTE-SQL.md. SQLite only requires the db permission.
Limits and error handling
- Methods: GET, HEAD, POST; others return 405 with Allow.
- Query string: 8 KiB. Request body: 64 KiB. Query and form: at most 128 fields each, keys at most 1024 UTF-8 bytes. Request headers: 64 names / 16 KiB.
- POST requires a valid CONTENT_LENGTH. Short bodies fail; oversized bodies return 413 before reading. The adapter reads exactly the declared length; Apache remains responsible for HTTP framing, dechunking and ambiguous headers.
- Nonempty bodies support text/plain, application/json and application/x-www-form-urlencoded. Everything is UTF-8 regardless of a charset parameter; configure clients accordingly. Unsupported media gives
- Invalid JSON/UTF-8/percent encodings give 400.
- Responses: 1 MiB UTF-8 body, 32 script headers, 8 KiB aggregate header storage, 4096 bytes per native header value. The worker JSON wire has a separate 7 MiB ceiling for escaping overhead. Worker diagnostics have a 64 KiB ceiling and are discarded, not logged or returned.
timeoutandread_timeout: 0.1..30 seconds. Parent monotonic deadlines supervise pipe I/O and execution. Timeout kills the worker process group and waits for it; a Linux parent-death SIGKILL also prevents an executing orphan if Apache kills the gateway. Worker core dumps are disabled; CPU and file descriptor limits are applied. This is not a cgroup memory sandbox or a guarantee about kernel uninterruptible waits. Gateway startup/config-file I/O and the final downstream response write are governed by the host/Apache, not the worker execution timer. Apache Timeout should exceed the configured body-read + execution time plus cleanup margin.- Runtime/parse failures produce generic 500. Worker deadline gives 504; body read deadline gives 408. A random request ID and a fixed failure category go to stderr; raw worker messages, submitted data and credentials do not. Apache access logging is separate: avoid logging sensitive query strings.
- Killing a worker cannot undo committed database writes, DDL, external HTTP requests or file changes. A client timeout does not prove that a write failed.
This runs trusted operator-installed scripts, not arbitrary user code. Authentication, sessions, CSRF protection, authorization, business validation, rate limiting and TLS termination remain application/server responsibilities. There is no FastCGI, embedded HTML interpreter, streaming response, SSE or WebSocket-over-CGI support. Keep the existing WebSocket service separate.
Real tests and retained evidence
tests/test_cgi.py runs direct CGI subprocess tests and an isolated real Apache
instance on an ephemeral loopback port, using extracted Apache modules. It
verifies unprivileged execution, GET/HEAD/POST, HTML escaping, JSON/forms,
repeated parameters/cookies, status/header rules, errors without partial data,
limits, timeouts, environment isolation, parent-death cleanup, fresh request
state, concurrent HTTP clients, private source/config paths and the shipped
examples including prepared SQLite. It stops its Apache process group.
make release sanitize
# On this Ubuntu host, the test-only Apache was extracted without installing
# or starting a system Apache service. Its APR runtime dependencies are needed.
# apt-get install libapr1t64 libaprutil1t64
# cd build/cgi && apt-get download apache2-bin
# dpkg-deb -x apache2-bin_*.deb apache-package
make test-cgi
make test-cgi-sanitize
make test
make test-sanitize
The separate tests/run_cgi_sql.py BINARY DISPOSABLE_PROFILES_JSON probe also
opens MySQL, MariaDB and Microsoft SQL Server through actual Apache HTTP
requests, executes a bound query inside begin/rollback, and closes each handle.
It copies generated credentials to a mode-0600 file owned by the CGI UID and
removes that copy afterward. Run it only against disposable profiles from
tests/provision_remote_sql.py, before invoking that fixture's cleanup script.
Both release and sanitizer HTTP/SQL results are in
build/cgi/sql-http-{release,asan}.json; the complete native SQL regression
results are in build/cgi/sql-final-fixtures/integration-{release,asan}.json.
The fixture harness requires root only to prove the child UID/GID drop; the
project path must already be traversable by that UID. It will not change
permissions on parent directories. Sanitizer mode sets ASan/UBSan options in
the clean child environment; vendor libraries/Python/Apache are not thereby
instrumented. Local evidence and absolute fixture/log paths are in
build/cgi/verification.json and build/cgi/results-{release,asan}.json.
Earlier failed runs are preserved, including an initial concurrent-load browser
timeout, a harness naming error and a disappearing-/proc test race. The final
results must have complete: true; interrupted runs cannot masquerade as an
old passing summary.
References consulted:
- Apache CGI tutorial: https://httpd.apache.org/docs/2.4/howto/cgi.html
- CGI/1.1 request/response semantics: https://www.rfc-editor.org/rfc/rfc3875
- HTTP HEAD/no-body semantics: https://www.rfc-editor.org/rfc/rfc9110
Raw retrieval evidence: build/cgi/authoritative-references.json.