Pulp

module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 21, 2026 License: MIT

README

Pulp

A minimal, universal application runtime. Go host, WASM cells, Docker-shaped boundary.

Status

v0.4 — storage. v0.3 stood up transport (HTTP / HTTPS / WebSocket / SSE) on a shared port. v0.4 adds a scoped filesystem and a per-cell SQLite database. The combination is enough to host Hytale-Auth; the existing port lives in Hytale-Auth/pulp-cell/.

See plans/velvet-bouncing-horizon.md for the full design.

Running

go build -o pulp ./cmd/pulp
./pulp -manifest path/to/pulp.cell.toml
./pulp -manifest path/to/pulp.cell.toml -http-port 9090
./pulp -manifest path/to/pulp.cell.toml -http-cert cert.pem -http-key key.pem

Send SIGINT (Ctrl+C) or SIGTERM to shut down. On Windows, Ctrl+Break works (the runtime listens for SIGBREAK too).

Flags:

  • -manifest — path to pulp.cell.toml (required).
  • -http-port — HTTP / HTTPS / WS / SSE listener port (default 8080). Used only when the cell declares an inbound transport capability.
  • -http-cert, -http-key — PEM files. If both are given, the HTTP server switches to HTTPS.
  • -storage-root — root directory for cell-scoped storage (default ./data). Each cell gets {root}/{cell_name}/ — scoped filesystem and SQLite DB live inside.

Cell contract

A valid cell ships with a pulp.cell.toml next to a WASM module. The TOML declares identity, dependencies, capabilities, and a free-form [config] table:

name = "echo"
version = "0.1.0"
wasm = "echo.wasm"                # defaults to cell.wasm if omitted

provides = []
consumes = []
capabilities = ["transport.http.inbound"]

shared_memory_groups = []
dedicated_thread = false
snapshotable = false

[config]

The WASM module must export:

pulp_alloc(size u32) -> u32                      // returns a pointer into the cell's linear memory
pulp_free(ptr u32, size u32)                     // (optional) frees what pulp_alloc returned
pulp_init(cfg_ptr u32, cfg_len u32) -> i32       // receives MessagePack-encoded [config] bytes
pulp_step(in_ptr u32, in_len u32)  -> i32        // returns an arena handle, 0 = no output
pulp_shutdown()                    -> i32

The step envelope passed to pulp_step is little-endian:

call_number  u64
wall_time    u64  (unix nanoseconds)
payload_len  u32
payload      bytes            // MessagePack-encoded StepEvent (see below) or empty

When an inbound transport event is pending, the payload is a MessagePack-encoded StepEvent:

StepEvent {
  kind:    string    // "http.request", "ws.open", "ws.frame", "ws.close"
  payload: bytes     // MessagePack-encoded per-kind struct
}

Transport capabilities (v0.3)

Cells opt into transport by listing capabilities in the manifest and calling the matching host imports (from the pulp import module).

transport.http.inbound

Cell calls http_register during pulp_init to declare routes; incoming requests arrive as StepEvent{kind:"http.request"}; cell replies with http_respond.

http_register(req_ptr, req_len) -> error_code     // req = msgpack {method, path}
http_respond (resp_ptr, resp_len) -> error_code    // resp = msgpack HTTPResponse {id, status, headers, body}

Path patterns support :param segments (e.g. /echo/:msg).

transport.http.outbound

Cell calls http_fetch to make an outbound HTTP request. The host performs the call synchronously and writes a MessagePack HTTPResponse into the cell's linear memory via pulp_alloc.

http_fetch(req_ptr, req_len, resp_ptr_out, resp_len_out) -> error_code
// req  = msgpack HTTPFetchRequest {method, url, headers, body}
// resp = msgpack HTTPResponse {status, headers, body}
transport.ws.inbound

Cell registers a WebSocket path; the host upgrades matching HTTP requests. Connection events (ws.open, ws.frame, ws.close) are delivered through StepEvent. Cell sends frames via ws_send and closes via ws_close.

ws_register(path_ptr, path_len)         -> error_code
ws_send    (req_ptr, req_len)           -> error_code  // msgpack {conn_id, opcode, payload}
ws_close   (req_ptr, req_len)           -> error_code  // msgpack {conn_id, code, reason}
transport.sse

Cell registers an SSE path; clients that GET the path receive a long-poll event stream. Cell pushes events to every subscriber on that path via sse_emit. A 15-second keepalive comment is written automatically.

sse_register(path_ptr, path_len) -> error_code
sse_emit    (req_ptr, req_len)   -> error_code  // msgpack {path, id?, event?, data}

All four capabilities share a single listener (-http-port). The dispatcher routes WS upgrades and SSE long-polls before falling back to HTTP route matching.

Storage capabilities (v0.4)

Both capabilities confine the cell to {-storage-root}/{cell_name}/.

storage.fs

Per-cell scoped filesystem. Absolute paths, .., and null bytes are rejected at the host boundary.

fs_read  (path_ptr, path_len, data_ptr_out, data_len_out) -> error_code
         // host allocates the result buffer via pulp_alloc and stores (ptr, len) at the out-addresses
fs_write (path_ptr, path_len, data_ptr, data_len)          -> error_code
fs_delete(path_ptr, path_len)                              -> error_code
storage.sqlite

Per-cell SQLite database ({-storage-root}/{cell_name}/data.db), backed by modernc.org/sqlite — pure Go, no CGo.

sqlite_exec (query_ptr, query_len, params_ptr, params_len)                             -> error_code
sqlite_query(query_ptr, query_len, params_ptr, params_len, rows_ptr_out, rows_len_out) -> error_code
// params = msgpack []any (empty when no parameters)
// rows   = msgpack [][]any — outer slice is rows, inner slice is column values in declaration order

The [config] table is encoded to MessagePack (github.com/vmihailenco/msgpack/v5) before delivery, so cells in any language can decode it with any MessagePack library.

Test cells

  • testdata/heartbeat/ — minimal cell for lifecycle / envelope / config verification.
  • testdata/echo/ — HTTP demo cell. Declares transport.http.inbound, registers GET /echo/:msg and POST /echo.

Build either manually:

cd testdata/echo
GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o echo.wasm .

The integration tests rebuild them automatically:

go test ./...

Dependencies

  • github.com/tetratelabs/wazero — pure-Go WASM runtime.
  • github.com/BurntSushi/toml — manifest parser.
  • github.com/vmihailenco/msgpack/v5 — envelope and config encoding.
  • github.com/coder/websocket — inbound WebSocket upgrade and framing.
  • modernc.org/sqlite — pure-Go SQLite driver (storage.sqlite capability).

Directories

Path Synopsis
cmd
pulp command
Pulp — default CLI entry point.
Pulp — default CLI entry point.
pulp-demo command
pulp-demo is an example of a "deployment binary" — a custom Pulp build that statically links extensions.
pulp-demo is an example of a "deployment binary" — a custom Pulp build that statically links extensions.
pulp-http-test command
pulp-http-test is a test-only deployment binary that blank-imports Pulp-ext-http so the HTTP integration test can exercise the full path through a real HTTP server.
pulp-http-test is a test-only deployment binary that blank-imports Pulp-ext-http so the HTTP integration test can exercise the full path through a real HTTP server.
ext
Package ext is Pulp's public extension API.
Package ext is Pulp's public extension API.
demo
Package demo is an example compile-time Pulp extension.
Package demo is an example compile-time Pulp extension.
internal
manifest
Package manifest reads and validates pulp.cell.toml files.
Package manifest reads and validates pulp.cell.toml files.
safe
Package safe provides panic-recovery wrappers for extension entry points.
Package safe provides panic-recovery wrappers for extension entry points.
Package run is the Pulp runtime entry point.
Package run is the Pulp runtime entry point.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL