# 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. The host publishes this file at `/actor/broker/public/PROTOCOL.md`, so every actor can read it. ## 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`. - An actor must keep reading stdin. If a message waits 30 seconds for the actor to read it, the broker kills and restarts the actor, and the sender gets `undeliverable`. Before the broker stops an instance, it sends `{"id": "broker/shutdown", "from": "broker", "body": {"type": "shutdown"}}` and then closes stdin. The broker still routes what the actor writes until it exits. 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` (empty for a new actor), without HTTP. The candidate sees its future `SAUSAGE_VERSION` and `SAUSAGE_CANDIDATE=1`. 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.