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
+58 -21
View File
@@ -1,9 +1,15 @@
//! Command parsing and the minimal line console.
use anyhow::{Result, bail};
use clap::{Parser, Subcommand};
use include_dir::{Dir, include_dir};
use sausage::{Host, world::World};
use serde_json::json;
use std::{io::Write, path::PathBuf};
use std::{
io::Write,
path::{Path, PathBuf},
};
static AGENT: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../agent/src");
mod test_runner;
#[derive(Parser)]
#[command(version)]
@@ -13,14 +19,16 @@ struct Cli {
#[arg(long, global = true)]
cache_dir: Option<PathBuf>,
#[command(subcommand)]
command: Cmd,
command: Option<Cmd>,
}
#[derive(Subcommand)]
enum Cmd {
Init {
#[arg(default_value = "world")]
dir: PathBuf,
},
Run {
#[arg(default_value = "world")]
dir: PathBuf,
},
/// Run a WASI libtest component in isolated instances.
@@ -48,31 +56,56 @@ fn setting(var: &str, question: &str, default: Option<&str>) -> Result<String> {
None => bail!("{question} is required"),
}
}
/// Creates a configured world and builds its embedded agent through the standalone builder.
async fn init(dir: &Path) -> Result<()> {
let api = setting("SAUSAGE_API", "API shape", Some("openai-responses"))?;
if api != "openai-responses" && api != "anthropic-messages" {
bail!("unknown API shape {api:?}");
}
let base_url = setting("SAUSAGE_BASE_URL", "Base URL", None)?;
let model = setting("SAUSAGE_MODEL", "Model", None)?;
let token = setting("SAUSAGE_API_TOKEN", "API token", None)?;
let world = World::create(dir)?;
std::fs::write(
world.root.join("config/agent.json"),
serde_json::to_vec_pretty(
&json!({"api":api,"base_url":base_url.trim_end_matches('/'),"model":model,"token":token}),
)?,
)?;
let builder = sausage::cargo::Cargo::new(world.clone())?;
let source = world.plugin_dir("agent").join("source");
std::fs::create_dir_all(source.join("src"))?;
AGENT.extract(source.join("src"))?;
std::fs::write(
source.join("Cargo.toml"),
include_str!("../../agent/Cargo.toml"),
)?;
println!("The first agent build takes a few minutes.");
let output = builder.build(&source).await?;
std::fs::copy(output.wasm, world.plugin_dir("agent").join("plugin.wasm"))?;
println!("Start the world with: sausage run {}", dir.display());
Ok(())
}
#[tokio::main]
async fn main() -> Result<()> {
let cli = Cli::parse();
match cli.command {
let command = match cli.command {
Some(command) => command,
None => {
let dir = PathBuf::from("world");
if !dir.join("config/boot.json").is_file() {
init(&dir).await?;
}
Cmd::Run { dir }
}
};
match command {
Cmd::TestRunner { wasm, args } => {
if !test_runner::run(&wasm, args).await? {
std::process::exit(1);
}
}
Cmd::Init { dir } => {
let api = setting("SAUSAGE_API", "API shape", Some("openai-responses"))?;
if api != "openai-responses" && api != "anthropic-messages" {
bail!("unknown API shape {api:?}");
}
let base_url = setting("SAUSAGE_BASE_URL", "Base URL", None)?;
let model = setting("SAUSAGE_MODEL", "Model", None)?;
let token = setting("SAUSAGE_API_TOKEN", "API token", None)?;
let world = World::create(&dir)?;
std::fs::write(
world.root.join("config/agent.json"),
serde_json::to_vec_pretty(
&json!({"api":api,"base_url":base_url.trim_end_matches('/'),"model":model,"token":token}),
)?,
)?;
}
Cmd::Init { dir } => init(&dir).await?,
Cmd::Run { dir } => {
let mut term =
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())?;
@@ -83,8 +116,12 @@ async fn main() -> Result<()> {
let mut notifications = host.subscribe();
let notification_task = cli.headless.then(|| {
tokio::spawn(async move {
while let Ok(notification) = notifications.recv().await {
println!("{notification}");
loop {
match notifications.recv().await {
Ok(notification) => println!("{notification}"),
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => continue,
Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
}
}
})
});