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
This commit is contained in:
2026-10-15 11:14:11 +01:00
parent c9d9864743
commit 0a8e8c2e3c
13 changed files with 786 additions and 335 deletions
+138
View File
@@ -0,0 +1,138 @@
# 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**:
```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 `"<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`:
```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
```json
{"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.