docs overhaul: agent-facing doc bundle, read-only docs mount in containers
Nightly Build / build (push) Successful in 6m38s
Nightly Build / build (push) Successful in 6m38s
- Strip ~55 stale upstream docs (dev docs never meant for the agents)
- Write new slim index.md as an agent-facing guide to the app's features
- Add new plugin docs: comfyui, elevenlabs, kokoro_tts, orpheus_tts_3b,
remote_connectivity, whisper_local (replacing old names)
- Add docs_host to UserFs: docs/… and ~/docs/… resolve to {WD}/docs,
mounted read-only at /root/docs in every user's container
- Instruct assistant/kid/project-coordinator agents to read docs/index.md
when users ask how the software works
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
//! | `shared-memory/…` | SQLite (`system.db`) — routed *before* this |
|
||||
//! | `shared/{X}/…` | host `{WD}/shared/{X}`, mount `{home}/shared/{X}` |
|
||||
//! | `projects/{O}/{S}`| host `{WD}/projects/{owner_userid}/{S}`, mount `{home}/projects/{O}/{S}` (O = owner username) |
|
||||
//! | `~/docs/…`, `docs/…` | host `{WD}/docs` (read-only, same for every user), mount `{container_home}/docs` |
|
||||
//! | `~/…`, relative | host `{WD}/homes/{userid}`, mount `{container_home}`|
|
||||
//!
|
||||
//! `UserFs` is a **pure value type** with no filesystem access: it carries the
|
||||
@@ -68,6 +69,10 @@ pub struct UserFs {
|
||||
pub shared: Vec<SharedMount>,
|
||||
/// Projects this user can reach (owned + shared-with-them), by owner then slug.
|
||||
pub projects: Vec<ProjectMount>,
|
||||
/// Host directory backing the read-only docs mount (`{WD}/docs`), the same for
|
||||
/// every user. `None` when unset (inert placeholders, unit tests that don't
|
||||
/// touch it) — `docs/…` then resolves like any other unmounted path.
|
||||
pub docs_host: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl UserFs {
|
||||
@@ -78,6 +83,7 @@ impl UserFs {
|
||||
container_home: PathBuf,
|
||||
shared: Vec<SharedMount>,
|
||||
projects: Vec<ProjectMount>,
|
||||
docs_host: Option<PathBuf>,
|
||||
) -> Self {
|
||||
Self {
|
||||
user_id: user_id.into(),
|
||||
@@ -86,6 +92,7 @@ impl UserFs {
|
||||
container_home,
|
||||
shared,
|
||||
projects,
|
||||
docs_host,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,6 +117,9 @@ impl UserFs {
|
||||
for m in &self.projects {
|
||||
out.push((m.host.clone(), m.container.clone(), m.can_write));
|
||||
}
|
||||
if let Some(docs) = &self.docs_host {
|
||||
out.push((docs.clone(), self.container_home.join("docs"), false));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
@@ -144,6 +154,11 @@ impl UserFs {
|
||||
let mount = self.project_mount(owner, slug)?;
|
||||
Some((mount.host.clone(), tail.to_string()))
|
||||
}
|
||||
Some("docs") => {
|
||||
let host = self.docs_host.clone()?;
|
||||
let tail = parts.next().unwrap_or("");
|
||||
Some((host, tail.to_string()))
|
||||
}
|
||||
_ => Some((self.home_host.clone(), stripped.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
//! own image (`skald-runtime`, python + node). The container is created when the
|
||||
//! user is created and started at application boot; `execute_cmd` and — later —
|
||||
//! the user's stateful MCP servers run inside it, against the user's bind-mounted
|
||||
//! home (`{WD}/homes/{userid}` → `/root`) plus the shared folders they belong to.
|
||||
//! home (`{WD}/homes/{userid}` → `/root`) plus the shared folders they belong to,
|
||||
//! plus the read-only `{WD}/docs` bundle mounted at `/root/docs` for every user.
|
||||
//!
|
||||
//! Docker is a **hard requirement**: [`ContainerManager::check_docker`] fails
|
||||
//! construction if the daemon is unreachable, and the shell exits at boot.
|
||||
@@ -45,6 +46,9 @@ pub const SHARED_DIR: &str = "shared";
|
||||
/// Subdirectory of the working directory holding project folders
|
||||
/// (`{WD}/projects/{owner_userid}/{slug}`).
|
||||
pub const PROJECTS_DIR: &str = "projects";
|
||||
/// Subdirectory of the working directory holding the docs bundle, mounted
|
||||
/// read-only into every user's container at `{container_home}/docs`.
|
||||
pub const DOCS_DIR: &str = "docs";
|
||||
/// Home mount point inside the container.
|
||||
pub const CONTAINER_HOME: &str = "/root";
|
||||
/// Grace window `docker stop` gives in-container processes (SIGTERM → SIGKILL)
|
||||
@@ -106,7 +110,9 @@ pub async fn build_user_fs(system: &SqlitePool, user_id: &str) -> Result<UserFs>
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(UserFs::new(user_id, home_host, container_name(user_id), container_home, shared, projects))
|
||||
let docs_host = Some(wd.join(DOCS_DIR));
|
||||
|
||||
Ok(UserFs::new(user_id, home_host, container_name(user_id), container_home, shared, projects, docs_host))
|
||||
}
|
||||
|
||||
/// Owns the container lifecycle: the docker availability check, the runtime image,
|
||||
|
||||
@@ -345,6 +345,7 @@ impl Conversation {
|
||||
std::path::PathBuf::from("/root"),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
None,
|
||||
));
|
||||
|
||||
let manager = Arc::new(ChatSessionManager::new(
|
||||
|
||||
@@ -288,6 +288,7 @@ mod tests {
|
||||
PathBuf::from("/root"),
|
||||
vec![],
|
||||
vec![],
|
||||
None,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -304,10 +305,12 @@ mod tests {
|
||||
let shared = root.join("shared").join("family");
|
||||
// Project owned by user `owner-id`, agent-visible as `projects/alice/budget`.
|
||||
let project = root.join("projects").join("owner-id").join("budget");
|
||||
let docs = root.join("docs");
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
std::fs::create_dir_all(&home).unwrap();
|
||||
std::fs::create_dir_all(&shared).unwrap();
|
||||
std::fs::create_dir_all(&project).unwrap();
|
||||
std::fs::create_dir_all(&docs).unwrap();
|
||||
|
||||
let fs = UserFs::new(
|
||||
"u1",
|
||||
@@ -327,11 +330,13 @@ mod tests {
|
||||
container: PathBuf::from("/root/projects/alice/budget"),
|
||||
can_write: false,
|
||||
}],
|
||||
Some(docs.clone()),
|
||||
);
|
||||
|
||||
let home_canon = canonicalize_for_policy(&home.to_string_lossy(), Path::new("/"));
|
||||
let shared_canon = canonicalize_for_policy(&shared.to_string_lossy(), Path::new("/"));
|
||||
let project_canon = canonicalize_for_policy(&project.to_string_lossy(), Path::new("/"));
|
||||
let docs_canon = canonicalize_for_policy(&docs.to_string_lossy(), Path::new("/"));
|
||||
|
||||
// ~/… → private home (containment holds for a not-yet-existing file).
|
||||
let p = resolve_host_path(&fs, "~/notes.md").unwrap();
|
||||
@@ -344,6 +349,11 @@ mod tests {
|
||||
// projects/{owner}/{slug} → the project host dir (two-segment routing)
|
||||
let pr = resolve_host_path(&fs, "projects/alice/budget/plan.md").unwrap();
|
||||
assert!(path_under(&pr, &project_canon), "{pr:?}");
|
||||
// docs/… → the shared read-only docs dir (both bare and ~-prefixed)
|
||||
let d = resolve_host_path(&fs, "docs/index.md").unwrap();
|
||||
assert!(path_under(&d, &docs_canon), "{d:?}");
|
||||
let d2 = resolve_host_path(&fs, "~/docs/index.md").unwrap();
|
||||
assert_eq!(d, d2);
|
||||
|
||||
// a shared folder the user is NOT a member of → error
|
||||
assert!(resolve_host_path(&fs, "shared/secret/x.md").is_err());
|
||||
|
||||
Reference in New Issue
Block a user