Files
sausage/PROTOCOL.md
T
surma 9d575b6a62 Host: fixes from the review, with regression tests
- one live instance per actor: check after the lock; dropping an instance kills it
- state snapshots keep target/ and .git/; only source copies skip them
- one shutdown deadline; output during shutdown is still routed
- deliveries route stdout meanwhile; a stuck stdin kills the instance
  (no partial lines) and the sender gets undeliverable; control requests
  are not blocked by a stuck delivery
- Host::stop aborts running broker and cargo requests first
- compile a version before installing it; roll back if it does not start
- unique archive names; a failed archive keeps the actor manageable
- commit on close: truncate without create; directory renames
- candidates see SAUSAGE_VERSION and the existing state of a new actor
- the host publishes PROTOCOL.md at /actor/broker/public/
2026-10-15 12:03:26 +01:00

5.8 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. 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/<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.
  • 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/<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 (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:

{
  "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.