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
View File
@@ -41,13 +41,40 @@ fn main() {
"serve" | "hold" => {
send(json!({"jsonrpc":"2.0","id":request["id"],"result":{"version":version}}));
if request["method"] == "serve" {
let exclusive = format!("/run/{plugin}-service");
if std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&exclusive)
.is_err()
{
std::fs::write(format!("/state/{plugin}/duplicate-service"), "duplicate")
.unwrap();
}
std::fs::write(format!("/state/{plugin}/service"), version).unwrap();
}
for _ in lines {}
if request["method"] == "serve" {
let _ = std::fs::remove_file(format!("/run/{plugin}-service"));
}
std::fs::write(format!("/state/{plugin}/ended-{version}"), "ended").unwrap();
return;
}
_ => json!({"version":version}),
};
send(json!({"jsonrpc":"2.0","id":request["id"],"result":result}));
if request["method"] == "rpc.discover" && result["x-test-linger"] == true {
if result["x-test-stubborn"] == true {
while !std::path::Path::new(&format!("/state/{plugin}/discovery-exit")).exists() {
std::thread::sleep(std::time::Duration::from_millis(1));
}
} else {
for _ in lines {}
}
std::fs::write(
format!("/state/{plugin}/discovery-ended-{version}"),
"ended",
)
.unwrap();
}
}
+65
View File
@@ -292,9 +292,59 @@ fn locked_write(lines: &mut Lines) -> Value {
assert_eq!(std::fs::read_to_string("/state/loop").unwrap(), "new");
json!({})
}
fn unlock_answer(request: &Value, lines: &mut Lines) {
lock(&json!({"path":"ordered-unlock"}), lines);
let unlock = json!({"jsonrpc":"2.0","method":"core.unlock","params":{"path":"ordered-unlock"}});
let answer =
json!({"jsonrpc":"2.0","id":request["id"],"result":std::env::var("SAUSAGE_PID").unwrap()});
let mut stdout = io::stdout().lock();
writeln!(stdout, "{unlock}\n{answer}").unwrap();
stdout.flush().unwrap();
drop(stdout);
for _ in lines {}
}
fn notification_child(lines: &mut Lines) -> Value {
send(json!({"jsonrpc":"2.0","method":"probe.orphan_notify","params":{}}));
let pid = loop {
if let Ok(pid) = std::fs::read_to_string("/state/orphan") {
break pid;
}
std::thread::sleep(std::time::Duration::from_millis(1));
};
send(json!({"jsonrpc":"2.0","id":7,"method":"core.stop","params":{"pid":pid}}));
let mut count = 0;
loop {
let response = receive(lines);
if response["method"] == "core.notify" {
count += 1;
}
if response["id"] == 7 {
break;
}
}
json!(count)
}
fn dispatch(request: &Value, lines: &mut Lines) -> Option<Value> {
let params = &request["params"];
let result = match request["method"].as_str().unwrap() {
"invalid_stderr" => {
io::stderr().write_all(&[255, 10]).unwrap();
eprintln!("after invalid UTF-8");
json!(std::env::var("SAUSAGE_PID").unwrap())
}
"unlock_answer" => {
unlock_answer(request, lines);
return None;
}
"null_error" => {
send(json!({"jsonrpc":"2.0","id":request["id"],"error":null,"result":42}));
return None;
}
"notification_child" => notification_child(lines),
"orphan_notify" => {
std::fs::write("/state/orphan", std::env::var("SAUSAGE_PID").unwrap()).unwrap();
notify()
}
"self_serve" => {
std::fs::write("/self/endpoint", "endpoint").unwrap();
serve(request, lines);
@@ -359,6 +409,21 @@ fn dispatch(request: &Value, lines: &mut Lines) -> Option<Value> {
Some(result)
}
fn main() {
if let Ok(mode) = std::fs::read_to_string("/state/early") {
if mode == "flood" {
for index in 0..5000 {
eprintln!("flood {index} {}", "x".repeat(128));
}
} else {
eprintln!("last early log");
if mode == "answer" {
send(
json!({"jsonrpc":"2.0","id":std::env::var("SAUSAGE_PID").unwrap(),"result":"early"}),
);
}
std::process::exit(23);
}
}
let mut lines = io::BufReader::new(io::stdin()).lines();
let request = receive(&mut lines);
if let Some(result) = dispatch(&request, &mut lines)