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 | | Path | Access | Contents |
| --- | --- | --- | | --- | --- | --- |
| `/` | read-write by every actor | the whole world folder | | `/` | 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 | | `/data` | read-write | alias for the actor's state folder |
| `/public` | read-write | alias for the actor's published files | | `/public` | read-write | alias for the actor's published files |
| `/actor` | read-write by every actor | the `actor/` folder of the world | | `/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; New worlds include an empty `/config` for global configuration by convention;
existing worlds can simply create it. Keep secrets out of global configuration. existing worlds can simply create it. Keep secrets out of global configuration.
Candidate instances see the same writable world root. Their `/public` and Candidate instances see the same writable world root. Their `/self` alias
`/data` aliases still point to scratch copies, and network access remains off; 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. absolute world paths are not isolated from the live world.
`/actor/<id>/` holds everything about an actor: `actor.wasm`, `actor.json` `/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. **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/...` Paths are absolute world paths, or the sender's `/self/...`, `/public/...` or
aliases. Native senders (such as the console) can also supply absolute host `/data/...` aliases. Native senders (such as the console) can also supply
paths; apart from the three existing aliases (`/actor`, `/public`, `/data`), absolute host paths; apart from `/actor` and these aliases,
these retain their host meaning. these retain their host meaning.
**Taps.** After `tap`, the sender gets a copy of every message that the **Taps.** After `tap`, the sender gets a copy of every message that the
+1
View File
@@ -257,6 +257,7 @@ impl Broker {
mounts: vec![ mounts: vec![
Mount { guest: "/".into(), host: self.world.root.clone(), writable: true }, Mount { guest: "/".into(), host: self.world.root.clone(), writable: true },
Mount { guest: "/actor".into(), host: self.world.actors_root(), 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: "/public".into(), host: own.join("public"), writable: true },
Mount { guest: "/data".into(), host: own.join("data"), 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: //! 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 //! `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 //! ```text
//! <world>/ mounted at /, readable and writable by every actor //! <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. /// Map a path in an actor's namespace to a host path.
/// ///
/// - Absolute paths are relative to the world root. /// - 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. /// - Native senders (the console) can also give absolute host paths.
pub fn resolve(&self, sender: &str, path: &str) -> Result<PathBuf> { pub fn resolve(&self, sender: &str, path: &str) -> Result<PathBuf> {
let under = |base: PathBuf, rest: &str| -> Result<PathBuf> { let under = |base: PathBuf, rest: &str| -> Result<PathBuf> {
@@ -214,6 +216,9 @@ impl World {
if let Some(rest) = prefixed("/actor") { if let Some(rest) = prefixed("/actor") {
return under(self.actors_root(), rest); 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") { if let Some(rest) = prefixed("/public") {
return under(self.public_dir(sender), rest); 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() { async fn world_root_is_shared_writable_and_survives_restart() {
let (world, host, mut mailbox) = filesystem_world("world-root").await; let (world, host, mut mailbox) = filesystem_world("world-root").await;
assert!(world.root.join("config").is_dir()); 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"; let file = "/config/x";
ask_type(&host, &mut mailbox, "writer", json!({"type": "write", "path": file, "text": "first"}), "ok").await; ask_type(&host, &mut mailbox, "writer", json!({"type": "write", "path": file, "text": "first"}), "ok").await;
let read = json!({"type": "read", "path": file}); let read = json!({"type": "read", "path": file});