core: review fixes, init builds the agent, console without a terminal

Activation path (review T6R): a broken plugin or a stray file in
/plugins blocks no activation; candidates replace their own entries;
discovery has a 10 s deadline and its process is stopped after the
answer; an unchanged hash keeps plugin.prev.wasm; activations run in
their own task; the watcher is not recursive; no duplicate services;
rpc.retired waits for live processes of its version; reads and hashes
on blocking threads.

Guest I/O and per-call paths: a stdin write error is EOF, stdout and
stderr are drained before the status; stderr is lossy UTF-8 and goes
to the journal from its own reader; compiled components are freed;
shutdown starts no new processes; lock queues drop stopping processes;
compile before the admission gate; CommandPre per component;
core.unlock in order; null errors are successes; the journal thread
survives SQLite errors.

sausage init builds the agent with cargo.build; sausage without a
command creates ./world. The console reads plain lines without a
terminal. PROTOCOL.md and DECISIONS.md describe the decided rules. The
flake tests the whole workspace and checks the packaged cargo.build.
This commit is contained in:
2026-10-23 02:25:03 +01:00
parent 53b90a8ecf
commit d7b78ed0fd
27 changed files with 1800 additions and 352 deletions
+27 -36
View File
@@ -1,5 +1,5 @@
{
description = "Sausage: grow a system of WASI actors by talking to it";
description = "Sausage: grow a system of WASI plugins by talking to it";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
@@ -16,12 +16,12 @@
inherit system;
overlays = [ rust-overlay.overlays.default ];
};
# The toolchain that builds the host and, at runtime, the actors.
# The toolchain that builds the host and, at runtime, the plugins.
rust = pkgs.rust-bin.stable.latest.default.override {
targets = [ "wasm32-wasip2" ];
extensions = [ "rust-src" ];
};
# The runtime cargo actor needs Cargo, rustc (with wasm32-wasip2), and
# The native cargo plugin needs Cargo, rustc (with wasm32-wasip2), and
# the native linker, even when the host itself is started with a bare
# PATH. Keep this same runtime path in the packaged host and exercise
# the installed binary below with PATH deliberately empty.
@@ -41,7 +41,7 @@
src = ./.;
};
# The test packages for `checks.tests`.
testPackages = "-p sausage -p sausage-actor -p agent";
testPackages = "--workspace";
cargoArtifacts = craneLib.buildDepsOnly (common // {
doCheck = false;
# The dependencies of the host binary and of the tests.
@@ -56,7 +56,7 @@
cargoExtraArgs = "--locked -p sausage";
doCheck = false;
nativeBuildInputs = [ pkgs.makeWrapper ];
# The `cargo` actor runs cargo at runtime. Build scripts need a C linker,
# The native `cargo` plugin runs cargo at runtime. Build scripts need a C linker,
# and C code for WASI needs the WASI clang.
postInstall = ''
wrapProgram $out/bin/sausage --prefix PATH : ${runtimePath} \
@@ -67,9 +67,9 @@
});
# `nix run .#loop -- <world>` runs the newest `sausage run <world>`
# (without a folder: bare `sausage`, i.e. ./world, created if needed)
# from the public repo, again and again: after a crash or a broker
# `exit` request it refreshes the flake and starts the host again,
# so the world updates itself. `/quit` (exit code 0) ends the loop.
# from the public repo, again and again: after a crash or a call of
# `core.exit` it refreshes the flake and starts the host again, so
# the world updates itself. `quit` (exit code 0) ends the loop.
# SAUSAGE_FLAKE overrides where it fetches from (any flake URL,
# e.g. a fork or a local checkout).
repoUrl = "git+https://gitea.surma.technology/surma/sausage";
@@ -89,9 +89,9 @@
{
packages.default = sausage;
# `nix flake check` runs every test of the host, the SDK, and the
# agent. The tests build WASM actors with cargo, offline, from the
# vendored crates of Cargo.lock.
# `nix flake check` runs every test of the workspace. The tests build
# WASM plugins with cargo, offline, from the vendored crates of
# Cargo.lock.
# The binary the host loop runs (`nix run`), built from the same
# cached dependencies as the tests.
checks.package = sausage;
@@ -100,24 +100,22 @@
cargoExtraArgs = "--locked ${testPackages}";
# Nothing builds on the test run's target folder: don't store it.
doInstallCargoArtifacts = false;
preCheck = ''
# Its own lock file pins crates that the vendored set may lack.
rm -f cli/tests/actors/toolbox/Cargo.lock
'';
# The SDK tests run sample plugins with the wasmtime CLI.
nativeBuildInputs = [ pkgs.wasmtime ];
});
# Exercise the installed host exactly as the loop launches it: the
# caller has no Cargo on PATH, so only the packaged runtime toolchain
# can run the native tests and the wasm32-wasip2 build.
# can run `cargo.build`: the tests under WASI and the wasm32-wasip2
# build, with C code from the packaged WASI clang.
checks.runtime-toolchain = pkgs.runCommand "sausage-runtime-toolchain" { } ''
set -eu
root="$TMPDIR/runtime-toolchain"
world="$root/world"
emptyPath="$root/empty-bin"
mkdir -p "$emptyPath" "$root/home" "$root/cargo-home"
for actor in broker console cargo fs; do
mkdir -p "$world/actor/$actor/public" "$world/actor/$actor/data"
done
source="$world/actor/console/public/probe"
mkdir -p "$world/plugins" "$world/state" "$world/config"
echo "[]" > "$world/config/boot.json"
source="$world/plugins/probe/source"
mkdir -p "$source/src" "$source/tests"
cat > "$source/Cargo.toml" <<'TOML'
[package]
@@ -125,8 +123,7 @@
version = "0.1.0"
edition = "2024"
TOML
# C code for WASI, compiled with the packaged WASI clang. The native
# test build has no C, so it needs no C compiler for the host.
# C code for WASI, compiled with the packaged WASI clang.
printf 'int probe_add(int a, int b) { return a + b; }\n' > "$source/src/add.c"
cat > "$source/build.rs" <<'RUST'
use std::process::Command;
@@ -156,7 +153,7 @@
RUST
cat > "$source/tests/probe.rs" <<'RUST'
#[test]
fn native_test_harness_runs() {
fn test_runs_under_wasi() {
assert_eq!(2 + 2, 4);
}
RUST
@@ -169,29 +166,23 @@
hostOutput=''${host[0]}
hostPid=$host_PID
trap 'kill "$hostPid" 2>/dev/null || true' EXIT
printf '%s\n' '@cargo {"type":"build","source":"/actor/console/public/probe"}' >&"$hostInput"
printf '%s\n' 'cargo.build {"plugin":"probe"}' >&"$hostInput"
built=0
while IFS= read -r -t 300 line <&"$hostOutput"; do
while IFS= read -r -t 600 line <&"$hostOutput"; do
echo "$line"
case "$line" in
*'"type":"build_failed"'*)
echo "packaged cargo actor build failed with an empty caller PATH" >&2
*'error:'*)
echo "packaged cargo.build failed with an empty caller PATH" >&2
exit 1
;;
*'"type":"built"'*)
case "$line" in
*'"passed":1'*) built=1 ;;
*) echo "native test did not pass" >&2; exit 1 ;;
esac
break
;;
*'"passed": 1'*) built=1; break ;;
esac
done
if [ "$built" -ne 1 ]; then
echo "packaged host did not build the probe" >&2
echo "packaged host did not build the probe, or its test did not pass" >&2
exit 1
fi
printf '%s\n' '/quit' >&"$hostInput"
printf '%s\n' 'quit' >&"$hostInput"
wait "$hostPid"
mkdir -p "$out"
touch "$out/passed"