feat(container): per-user Docker sandbox + mapped per-user filesystem
Realizes blueprint §6: each user gets a permanent Docker container
(skald-{userid}, our own skald-runtime image with python+node) as their
execution sandbox. Docker is now a hard requirement — a missing daemon fails
Skald::new and the process exits at boot.
- ContainerManager (crates/skald-core/src/container/): docker availability
check, builds skald-runtime from the embedded Dockerfile, reconciles one
running container per active user at boot, stops them at shutdown, and
ensure/remove on user create/delete. Shells the docker CLI (no client crate).
- UserFs (core-api): pure value type carried in ToolContext, mapping the agent's
single namespace — ~/ → homes/{userid}, shared/{X}/ → shared/{X} (membership),
user-memory/ + shared-memory/ → SQLite — to host and container paths.
- execute_cmd now runs inside the caller's container via `docker exec`.
- fs-tools resolve every physical path through UserFs to the per-user host
workspace, host-side, with fail-closed symlink/`..` containment
(resolve_host_path: canonicalize + prefix-check). grep_files resolves its root
the same way but stays disk-only.
- shared_folders + shared_folder_members (registry, junction table with
can_write) back the shared-folder membership that drives both the container
mounts and the shared/{X} routing.
- Threading: UserContext.fs → ChatSessionManager → handler → ToolContext.fs.
Per-user MCP servers do not yet run in the container (next round).
This commit is contained in:
@@ -428,7 +428,11 @@ impl ChatSessionHandler {
|
||||
// the child via kill_on_drop when the work future is dropped on /stop).
|
||||
// The ToolContext carries this session's id and owner pool so owner-bound
|
||||
// registry tools (e.g. cron management) act on the caller's own database.
|
||||
let ctx = ToolContext { session_id: self.session_id, pool: Arc::clone(&self.db) };
|
||||
let ctx = ToolContext {
|
||||
session_id: self.session_id,
|
||||
pool: Arc::clone(&self.db),
|
||||
fs: Arc::clone(&self.fs),
|
||||
};
|
||||
self.tools.run(name, &ctx, args)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ use crate::config::DatetimeConfig;
|
||||
use crate::db::{chat_history, chat_sessions_stack};
|
||||
use crate::events::ServerEvent;
|
||||
use core_api::message_meta::MessageMetadata;
|
||||
use core_api::user_fs::UserFs;
|
||||
use crate::llm::LlmManager;
|
||||
use crate::mcp::McpManager;
|
||||
use crate::image_generate::ImageGeneratorManager;
|
||||
@@ -268,6 +269,10 @@ pub struct ChatSessionHandler {
|
||||
/// The authenticated user who owns this session. Threaded into `ChatOptions`
|
||||
/// so the telemetry metadata row in `system.db` carries `user_id`.
|
||||
pub(super) user_id: String,
|
||||
/// The owner's filesystem view (home + shared folders + container), threaded
|
||||
/// into every [`ToolContext`] so disk fs-tools resolve per-user host paths and
|
||||
/// `execute_cmd` execs into the owner's container (blueprint §6).
|
||||
pub(super) fs: Arc<UserFs>,
|
||||
pub(super) llm_manager: Arc<LlmManager>,
|
||||
pub(super) max_history_messages: usize,
|
||||
pub(super) max_tool_rounds: usize,
|
||||
@@ -337,6 +342,7 @@ impl ChatSessionHandler {
|
||||
db: Arc<SqlitePool>,
|
||||
shared_pool: Arc<SqlitePool>,
|
||||
user_id: String,
|
||||
fs: Arc<UserFs>,
|
||||
llm_manager: Arc<LlmManager>,
|
||||
max_history_messages: usize,
|
||||
max_tool_rounds: usize,
|
||||
@@ -363,6 +369,7 @@ impl ChatSessionHandler {
|
||||
db,
|
||||
shared_pool,
|
||||
user_id,
|
||||
fs,
|
||||
llm_manager,
|
||||
max_history_messages,
|
||||
max_tool_rounds,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use core_api::user_fs::UserFs;
|
||||
|
||||
use sqlx::SqlitePool;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
@@ -26,6 +28,9 @@ pub struct ChatSessionManager {
|
||||
/// reads such as injecting `shared-memory/` notes.
|
||||
shared_pool: Arc<SqlitePool>,
|
||||
user_id: String,
|
||||
/// The owner's filesystem view, threaded to each handler and on into every
|
||||
/// `ToolContext` (blueprint §6).
|
||||
user_fs: Arc<UserFs>,
|
||||
llm_manager: Arc<LlmManager>,
|
||||
max_history_messages: usize,
|
||||
max_tool_rounds: usize,
|
||||
@@ -53,6 +58,7 @@ impl ChatSessionManager {
|
||||
db: Arc<SqlitePool>,
|
||||
shared_pool: Arc<SqlitePool>,
|
||||
user_id: String,
|
||||
user_fs: Arc<UserFs>,
|
||||
llm_manager: Arc<LlmManager>,
|
||||
max_history_messages: usize,
|
||||
max_tool_rounds: usize,
|
||||
@@ -74,6 +80,7 @@ impl ChatSessionManager {
|
||||
db,
|
||||
shared_pool,
|
||||
user_id,
|
||||
user_fs,
|
||||
llm_manager,
|
||||
max_history_messages,
|
||||
max_tool_rounds,
|
||||
@@ -162,6 +169,7 @@ impl ChatSessionManager {
|
||||
self.db.clone(),
|
||||
self.shared_pool.clone(),
|
||||
self.user_id.clone(),
|
||||
Arc::clone(&self.user_fs),
|
||||
Arc::clone(&self.llm_manager),
|
||||
self.max_history_messages,
|
||||
self.max_tool_rounds,
|
||||
|
||||
Reference in New Issue
Block a user