# 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//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**: ```json {"id": "m-17", "from": "console", "reply_to": "agent/a5", "body": {"type": "text", "text": "hi"}} ``` A line on **stdout**: ```json {"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 `"/"`. Otherwise, the broker assigns `m-`. 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//` contains `actor.wasm`, `actor.json`, `source/`, `public/`, `status.json`, `stderr.log`, and `versions//` 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//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//` 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`: ```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//actor.wasm`. If the file differs from the installed version, the broker copies it to `versions//` and upgrades to it. If the upgrade fails, the installed files come back. ## The `cargo` actor ```json {"type": "build", "source": "/actor/agent/public/drafts/notes"} ``` The reply is `{"type": "built", "wasm": "/actor/cargo/public/builds//.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.