Add writable /self alias alongside /public and /data
ci / check (push) Successful in 15s

This commit is contained in:
Sausage Agent
2026-10-22 13:35:13 +00:00
parent f932ab9719
commit 34e2f35272
4 changed files with 17 additions and 7 deletions
+7 -5
View File
@@ -64,6 +64,7 @@ The world folder is the foundation of every instance's filesystem:
| Path | Access | Contents |
| --- | --- | --- |
| `/` | read-write by every actor | the whole world folder |
| `/self` | read-write | alias for the actor's own folder (`public/`, `data/`, `source/`, etc.) |
| `/data` | read-write | alias for the actor's state folder |
| `/public` | read-write | alias for the actor's published files |
| `/actor` | read-write by every actor | the `actor/` folder of the world |
@@ -75,8 +76,9 @@ Creating a folder in the world makes it visible to running actors immediately:
New worlds include an empty `/config` for global configuration by convention;
existing worlds can simply create it. Keep secrets out of global configuration.
Candidate instances see the same writable world root. Their `/public` and
`/data` aliases still point to scratch copies, and network access remains off;
Candidate instances see the same writable world root. Their `/self` alias
points to the scratch actor folder; `/public` and `/data` still point to its
corresponding children, and network access remains off;
absolute world paths are not isolated from the live world.
`/actor/<id>/` holds everything about an actor: `actor.wasm`, `actor.json`
@@ -146,9 +148,9 @@ the request: the request's `type` in past tense (`spawned`, `upgraded`,
**Headless.** `sausage run --headless [dir]` (or `sausage --headless`, or `SAUSAGE_HEADLESS=1`) turns the console input off: the host does not read stdin, so end of input (`/dev/null`) does not stop the world. `nix run .#loop -- --headless <world>` passes it through the loop. It runs until SIGTERM or SIGINT (stops every actor, exit code 0) or a broker `exit` request (exit code 75, as above). Without the flag, EOF on stdin or `/quit` ends the host.
Paths are absolute world paths, or the sender's `/public/...` or `/data/...`
aliases. Native senders (such as the console) can also supply absolute host
paths; apart from the three existing aliases (`/actor`, `/public`, `/data`),
Paths are absolute world paths, or the sender's `/self/...`, `/public/...` or
`/data/...` aliases. Native senders (such as the console) can also supply
absolute host paths; apart from `/actor` and these aliases,
these retain their host meaning.
**Taps.** After `tap`, the sender gets a copy of every message that the
+1
View File
@@ -257,6 +257,7 @@ impl Broker {
mounts: vec![
Mount { guest: "/".into(), host: self.world.root.clone(), writable: true },
Mount { guest: "/actor".into(), host: self.world.actors_root(), writable: true },
Mount { guest: "/self".into(), host: own.clone(), writable: true },
Mount { guest: "/public".into(), host: own.join("public"), writable: true },
Mount { guest: "/data".into(), host: own.join("data"), writable: true },
],
+7 -2
View File
@@ -1,6 +1,7 @@
//! On-disk layout of a Sausage world. The folder is the source of truth:
//! `sausage run` boots every actor that it finds in `actor/`, and then
//! watches the folder (`watch.rs`).
//! watches the folder (`watch.rs`). `/self` aliases the running actor's folder;
//! `/public` and `/data` remain aliases for its `public/` and `data/`.
//!
//! ```text
//! <world>/ mounted at /, readable and writable by every actor
@@ -194,7 +195,8 @@ impl World {
/// Map a path in an actor's namespace to a host path.
///
/// - Absolute paths are relative to the world root.
/// - `/public/...` and `/data/...` alias `sender`'s folders.
/// - `/self/...` aliases `sender`'s folder; `/public/...` and `/data/...`
/// alias its corresponding children.
/// - Native senders (the console) can also give absolute host paths.
pub fn resolve(&self, sender: &str, path: &str) -> Result<PathBuf> {
let under = |base: PathBuf, rest: &str| -> Result<PathBuf> {
@@ -214,6 +216,9 @@ impl World {
if let Some(rest) = prefixed("/actor") {
return under(self.actors_root(), rest);
}
if let Some(rest) = prefixed("/self") {
return under(self.actor_dir(sender), rest);
}
if let Some(rest) = prefixed("/public") {
return under(self.public_dir(sender), rest);
}
+2
View File
@@ -220,6 +220,8 @@ async fn actors_can_read_and_write_other_actor_data() {
async fn world_root_is_shared_writable_and_survives_restart() {
let (world, host, mut mailbox) = filesystem_world("world-root").await;
assert!(world.root.join("config").is_dir());
ask_type(&host, &mut mailbox, "writer", json!({"type": "write", "path": "/self/data/x", "text": "self-alias"}), "ok").await;
assert_eq!(ask_type(&host, &mut mailbox, "writer", json!({"type": "read", "path": "/data/x"}), "content").await.body["text"], "self-alias");
let file = "/config/x";
ask_type(&host, &mut mailbox, "writer", json!({"type": "write", "path": file, "text": "first"}), "ok").await;
let read = json!({"type": "read", "path": file});