Files
sausage/PROTOCOL.md
T
surma 0a8e8c2e3c World folder is the source of truth
- actor/<id>/ holds actor.wasm, actor.json, source/, public/, data/, versions/
- the broker boots by scanning actor/; a changed actor.wasm is an upgrade,
  a rejected one stays in versions/ with a reason and the files come back
- /actor hides every data/ folder
- subscriptions live in the subscriber's actor.json
- cargo installs the SDK and points sausage-actor at it; no build limits
- PROTOCOL.md
2026-10-15 11:14:11 +01:00

5.4 KiB

The Sausage protocol

This document is the contract between the host and the actors. An actor in any language that follows it works in Sausage.

Actors

An actor is a WASI 0.2 command (wasm32-wasip2): a program with main. The host runs one instance per actor. An instance can run forever.

The instance gets:

  • stdin: one message per line, from the broker
  • stdout: one message per line, to the broker
  • stderr: free text, appended to /actor/<id>/stderr.log
  • environment: SAUSAGE_ACTOR (its ID), SAUSAGE_VERSION (its version), and SAUSAGE_CANDIDATE=1 during a test run
  • outgoing HTTP and HTTPS through wasi:http (no raw sockets)
  • the filesystem described below

Messages

A line on stdin:

{"id": "m-17", "from": "console", "reply_to": "agent/a5", "body": {"type": "text", "text": "hi"}}

A line on stdout:

{"to": "console", "id": "a6", "reply_to": "m-17", "body": {"type": "text", "text": "hello"}}
  • body is required. By convention it is an object with a "type".
  • to is the recipient. Without to, the broker publishes the message to every subscriber of the sender.
  • id is optional. If the actor sets it, the message ID is "<sender>/<id>". Otherwise, the broker assigns m-<n>. The broker always sets from.
  • reply_to marks a reply. There is no request/response: a reply is a new message whose reply_to is the ID of the message it answers.
  • Delivery is at most once. Each actor has a mailbox of 1,000 messages. The mailbox keeps messages while the actor restarts or is upgraded.
  • If the broker cannot deliver a message, the sender gets {"type": "undeliverable", "reason": "...", "to": "..."} from broker.

Before the broker stops an instance, it sends {"id": "broker/shutdown", "from": "broker", "body": {"type": "shutdown"}} and then closes stdin. After 5 seconds, it kills the instance.

Filesystem

Each instance sees three folders:

Path Access Contents
/data read-write private state of the actor
/public read-write files that the actor publishes
/actor read-only the actor/ folder of the world

/actor/<id>/ contains actor.wasm, actor.json, source/, public/, status.json, stderr.log, and versions/<n>/ of every actor. The data/ folders (and the data snapshots in versions/) are hidden.

A new or truncated file becomes visible when the writer closes it. Before that, readers see the old file or no file. If the instance crashes, the write is lost. A write to an existing file without truncate (append, edit) goes in place.

Send large or binary data as a path under /actor/<id>/public/, not inside a message.

The broker

Requests to broker (the reply has the same reply_to):

type Fields Effect
spawn actor, wasm, source?, built_by? test, then start a new actor
upgrade actor, wasm, source?, built_by? test, then swap the version
rollback actor, version? go back to an older version and its data
stop, start actor stop or start an actor
remove actor move the actor's folder to archive/
subscribe, unsubscribe actor receive (or not) what actor publishes
scan boot new or changed actor.wasm files
list all actors and their state

Paths: /actor/..., /public/... (the sender's), or /data/... (the sender's).

Versions. versions/<n>/ keeps every version that the broker saw: actor.wasm, source/, and meta.json. A version that the broker refused has "rejected" in its meta.json.

Tests. Before a spawn or an upgrade, the broker runs the candidate on a copy of the actor's /data and /public, without HTTP. The tests are JSON files in source/tests/*.json:

{
  "description": "add, then list",
  "steps": [
    {"send": {"type": "add", "text": "milk"}, "expect": {"type": "added"}},
    {"send": {"type": "list"}, "expect": [{"type": "notes"}]}
  ]
}

An expect entry matches when it is a subset of the reply body. A step can also have from (the sender, default test) and to (the expected recipient). Each file runs in a fresh instance. Without test files, the candidate must stay up for 2 seconds.

Swap. The broker holds the mailbox, stops the old instance, saves a snapshot of /data and /public in the old version's folder, installs the new files, and starts the new instance.

Crashes. The broker restarts a crashed actor (0.5 s, then doubling, at most 30 s). It keeps the message that the actor got just before the crash, and delivers it again after the next upgrade. A new version that crashes 3 times within 30 seconds after an upgrade goes back to the previous version, with its data. The actor that asked for the upgrade gets rolled_back.

The folder is the truth. At start, and on scan, the broker boots every actor/<id>/actor.wasm. If the file differs from the installed version, the broker copies it to versions/<n>/ and upgrades to it. If the upgrade fails, the installed files come back.

The cargo actor

{"type": "build", "source": "/actor/agent/public/drafts/notes"}

The reply is {"type": "built", "wasm": "/actor/cargo/public/builds/<n>/<name>.wasm", ...} or {"type": "build_failed", "errors": "..."}. A Rust actor depends on the SDK with sausage-actor = "0.1". cargo points that dependency at the SDK of the host.