v3: host crate skeleton, userland workspace, AGENTS.md for v3
The workspace is now host/ and vfs/. v2 crates leave the workspace as reference. host/src/exec.rs is the contract between the process table and the harness.
This commit is contained in:
@@ -1,56 +1,66 @@
|
||||
# Working on Sausage
|
||||
# Working on Sausage (v3)
|
||||
|
||||
Sausage is meant to be read and changed by agents, including its own agent.
|
||||
Keep it that way.
|
||||
Sausage is a world of WASI programs and durable agents. It is meant to be read and changed by agents,
|
||||
including its own. Keep it that way.
|
||||
|
||||
This branch (`v3`) rebuilds Sausage. The plan and its decisions (D1, D2, …) are in
|
||||
`~/research/sausage-v3/PLAN.md`. `PROTOCOL.md`, `DECISIONS.md` and `README.md` still describe v2.
|
||||
|
||||
## The boundary
|
||||
|
||||
- **The world is the sandbox.** Everything inside the world sees and changes the whole world. Do not add
|
||||
inner boundaries, permissions or per-agent views. The VFS is a safety net against mistakes, not a wall
|
||||
against attackers.
|
||||
- **Host calls pierce the sandbox.** The only host import beyond standard WASI is the process interface
|
||||
(spawn, wait, kill). Do not add another one. Everything else uses standard WASI 0.2 interfaces: files
|
||||
through the VFS, `wasi:http` (the host API on its reserved host name, credential injection, signing),
|
||||
and `wasi:sockets`.
|
||||
- **The credentials manager in the host is the one real security boundary.** Secrets never enter the
|
||||
world. Approvals need a passkey.
|
||||
- **Native code is the exception.** The only native work for the world is the Rust builder, and it runs in
|
||||
user, mount and network namespaces.
|
||||
|
||||
## Priorities
|
||||
|
||||
1. **Correct.** Every behavior in `PROTOCOL.md` has a test in `cli/tests/host/`.
|
||||
2. **Simple and short.** Prefer the obvious code. No abstraction for one
|
||||
caller. No configuration that nobody asked for.
|
||||
3. **Readable.** A new reader must understand a file without the others.
|
||||
Each module starts with a doc comment that says what it owns.
|
||||
4. **Efficient** on the paths that run per call.
|
||||
5. **Raw and minimal.** Sausage has no permissions, sandboxing beyond WASI,
|
||||
limits, or authentication. Do not add them to the core. Add nothing to
|
||||
the host that a plugin (or the agent, by changing itself) could do.
|
||||
1. **Correct.** Every behavior has a test.
|
||||
2. **Simple and short.** Prefer the obvious code. No abstraction for one caller. No configuration that
|
||||
nobody asked for.
|
||||
3. **Readable.** A new reader must understand a file without the others. Each module starts with a doc
|
||||
comment that says what it owns.
|
||||
4. **Efficient** on the paths that run per command and per model turn.
|
||||
|
||||
## Layout
|
||||
|
||||
- `cli/`: the host (`sausage` binary and library): the core, the native
|
||||
`cargo` plugin, the WASI test runner, and the console.
|
||||
- `vfs/`: `sausage-vfs`, the virtual filesystem for WASI guests. It knows
|
||||
nothing about Sausage.
|
||||
- `interface/`: `sausage-interface`: OpenRPC documents, the compatibility
|
||||
check, and the generator of client code.
|
||||
- `sdk/`: `sausage-plugin`, the SDK for Rust plugins, and `sdk/macros/`, its
|
||||
macros. Never published: `cargo` points `sausage-plugin = "0.1"` at its
|
||||
local copy.
|
||||
- `agent/`: the agent plugin. `sausage init` embeds and builds it.
|
||||
- `examples/`: small plugins. They are not workspace members.
|
||||
- `PROTOCOL.md`: the contract between the core and the plugins. Change it in
|
||||
the same commit as the behavior. `DECISIONS.md` says why; when the two
|
||||
disagree, `PROTOCOL.md` and the code win.
|
||||
- `host/`: the host crate (package and binary `sausage`): the world, the process table, the harness.
|
||||
`host/src/exec.rs` is the contract between the harness and the process table.
|
||||
- `vfs/`: `sausage-vfs`, the virtual filesystem for WASI guests and for host code. It knows nothing
|
||||
about Sausage. Host code reads and writes the world only through it.
|
||||
- `userland/`: the default CLIs of a world, a separate workspace built for `wasm32-wasip2`.
|
||||
- `spikes/`: accepted prototypes. Read them for evidence, copy from them, do not build on them.
|
||||
- `cli/`, `interface/`, `sdk/`, `agent/`, `examples/`: v2 code, outside the workspace. Copy what v3
|
||||
needs. They are deleted at the end of P1.
|
||||
|
||||
## Programs in the world
|
||||
|
||||
- Agents write CLIs and servers. That they are WASI binaries is a detail, not a concept to explain.
|
||||
- The platform is WASI 0.2 components. Rust programs build for `wasm32-wasip2`. WASI 0.1 programs run
|
||||
through an adapter.
|
||||
|
||||
## Checks
|
||||
|
||||
```sh
|
||||
nix flake check -L # all tests below; CI runs this on every push
|
||||
nix develop -c cargo test --workspace # fast with a warm cache: run it during development
|
||||
nix develop -c cargo test -p sausage --test host -- core:: # one topic of the host tests
|
||||
nix develop -c cargo build --release -p agent --target wasm32-wasip2
|
||||
nix develop -c cargo clippy --workspace
|
||||
nix develop -c cargo test --workspace # host and vfs
|
||||
nix develop -c cargo clippy --workspace --all-targets -- -D warnings
|
||||
nix develop -c cargo build --manifest-path userland/Cargo.toml --release
|
||||
```
|
||||
|
||||
`nix run -- init` and `nix run -- run` must keep working without other
|
||||
setup.
|
||||
`nix flake check` still describes v2 and is updated later in P1.
|
||||
|
||||
Tests must stay fast:
|
||||
|
||||
- Only a few tests may wait for a timing (grace periods, deadlines).
|
||||
- Poll for a condition with a deadline. Do not sleep.
|
||||
- Do not test unrealistic load, such as many fast cycles.
|
||||
- `#[ignore]` only disables a test for a short time, with the reason in the
|
||||
attribute.
|
||||
- Tests that build plugins use the persistent cache in
|
||||
`env!("CARGO_TARGET_TMPDIR")`.
|
||||
- `#[ignore]` only disables a test for a short time, with the reason in the attribute.
|
||||
- Tests that build or compile guests use a persistent cache (for example under
|
||||
`env!("CARGO_TARGET_TMPDIR")`), so a warm run does not compile again.
|
||||
|
||||
Generated
+3
-985
File diff suppressed because it is too large
Load Diff
+6
-5
@@ -1,11 +1,12 @@
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = ["cli", "vfs", "interface", "sdk", "sdk/macros", "agent"]
|
||||
default-members = ["cli"]
|
||||
# Examples are standalone plugins, each with its own empty [workspace].
|
||||
exclude = ["examples"]
|
||||
members = ["host", "vfs"]
|
||||
default-members = ["host"]
|
||||
# Standalone crates with their own empty [workspace]: v2 code kept as reference
|
||||
# until the end of P1, the spikes, and the userland (its own wasm32-wasip2 workspace).
|
||||
exclude = ["cli", "interface", "sdk", "agent", "examples", "spikes", "userland"]
|
||||
|
||||
# Wasmtime compiles components with Cranelift. Optimize dependencies in dev
|
||||
# builds, so that tests and debug runs start plugins quickly.
|
||||
# builds, so that tests and debug runs start guests quickly.
|
||||
[profile.dev.package."*"]
|
||||
opt-level = 2
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "sausage"
|
||||
version = "0.3.0"
|
||||
edition = "2024"
|
||||
description = "A world of WASI programs and durable agents."
|
||||
|
||||
[[bin]]
|
||||
name = "sausage"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
async-trait = "0.1"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
sausage-vfs = { version = "0.1.0", path = "../vfs" }
|
||||
@@ -0,0 +1,37 @@
|
||||
//! The contract between the harness and the process table: run a command in the world.
|
||||
//! The process table implements it; the harness's `bash` tool uses it.
|
||||
use std::time::Duration;
|
||||
|
||||
/// What to run: a shell string (run by the world's `sh -c`) or an argv without a shell.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Command {
|
||||
Shell(String),
|
||||
Argv(Vec<String>),
|
||||
}
|
||||
|
||||
/// How to run it. `cwd` is a world path. `timeout` ends the command and all its processes.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Options {
|
||||
pub cwd: String,
|
||||
pub env: Vec<(String, String)>,
|
||||
pub timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
/// The stream that one output chunk came from.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Stream {
|
||||
Stdout,
|
||||
Stderr,
|
||||
}
|
||||
|
||||
/// Run commands in the world. Dropping the future kills the command and all processes it started.
|
||||
#[async_trait::async_trait]
|
||||
pub trait Exec: Send + Sync {
|
||||
/// Run to completion and return the exit status. `output` gets each chunk as it arrives, in order.
|
||||
async fn exec(
|
||||
&self,
|
||||
command: Command,
|
||||
options: Options,
|
||||
output: &mut (dyn FnMut(Stream, &[u8]) + Send),
|
||||
) -> anyhow::Result<i32>;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
//! The durable agent harness: V8 with pi-durable, harness storage, and the tools' environment.
|
||||
//! Owned by task T9.
|
||||
use std::path::Path;
|
||||
|
||||
/// Start the world at `world` with the harness and a console.
|
||||
pub async fn run(_world: &Path) -> anyhow::Result<()> {
|
||||
anyhow::bail!("the harness does not exist yet (T9)")
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
//! The Sausage v3 host: a world folder behind a virtual filesystem, WASI programs in a process table,
|
||||
//! and the durable agent harness. See `~/research/sausage-v3/PLAN.md` until the docs move here.
|
||||
|
||||
pub mod exec;
|
||||
pub mod harness;
|
||||
pub mod process;
|
||||
pub mod world;
|
||||
@@ -0,0 +1,33 @@
|
||||
//! The `sausage` binary: subcommands only, each delegates to its module.
|
||||
use clap::{Parser, Subcommand};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(version, about)]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Command,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Command {
|
||||
/// Start a world: the harness, its services and a console.
|
||||
Run { world: PathBuf },
|
||||
/// Run a shell in a world. With `-c`, run one command and exit with its status.
|
||||
Sh {
|
||||
world: PathBuf,
|
||||
#[arg(short = 'c')]
|
||||
command: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
match Cli::parse().command {
|
||||
Command::Run { world } => sausage::harness::run(&world).await,
|
||||
Command::Sh { world, command } => {
|
||||
let status = sausage::process::sh(&world, command).await?;
|
||||
std::process::exit(status);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
//! The process table: WASI programs in their own stores, the process interface (spawn, wait, kill),
|
||||
//! `/proc`, and the `Exec` implementation. Owned by task T8.
|
||||
use std::path::Path;
|
||||
|
||||
/// Run a shell in the world at `world`, or one command with `command`. Returns the exit status.
|
||||
pub async fn sh(_world: &Path, _command: Option<String>) -> anyhow::Result<i32> {
|
||||
anyhow::bail!("the process table does not exist yet (T8)")
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
//! A world folder and the one virtual root that host code and guests share.
|
||||
use sausage_vfs::{Host, Layer, Mounts, Process};
|
||||
use std::{path::Path, sync::Arc};
|
||||
|
||||
/// An open world. `root` is the filesystem every guest sees as `/`; host code (the harness's file
|
||||
/// tools) reads and writes through the same layer, so both always see the same files.
|
||||
pub struct World {
|
||||
pub root: Arc<dyn Layer>,
|
||||
}
|
||||
|
||||
impl World {
|
||||
/// Open the world folder at `path`. The process table adds more mounts (`/proc`, `/run`).
|
||||
pub fn open(path: &Path) -> anyhow::Result<Self> {
|
||||
let mut mounts = Mounts::default();
|
||||
mounts.mount("/", Arc::new(Host::new(path, Process::default())?), false);
|
||||
Ok(World { root: Arc::new(mounts) })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
[build]
|
||||
target = "wasm32-wasip2"
|
||||
@@ -0,0 +1,7 @@
|
||||
# The default CLIs of a world, built for wasm32-wasip2. Its own workspace, outside the host's.
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = []
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
Reference in New Issue
Block a user