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:
@@ -47,6 +47,13 @@ pub async fn create(
|
||||
.users()
|
||||
.register_user(username, body.display_name.as_deref(), &body.role_id, Some(&body.password), body.encrypted)
|
||||
.await?;
|
||||
|
||||
// Provision the user's container now (blueprint §6). Best-effort: a failure here
|
||||
// is not fatal to user creation — boot reconciliation will retry.
|
||||
if let Err(e) = skald.container().ensure(&id).await {
|
||||
tracing::warn!(user = %id, error = %e, "failed to provision user container (will retry at next boot)");
|
||||
}
|
||||
|
||||
Ok(Json(CreatedUser { id }))
|
||||
}
|
||||
|
||||
@@ -92,6 +99,10 @@ pub async fn delete(
|
||||
Path(id): Path<String>,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
skald.users().delete_user(&id).await?;
|
||||
// Tear down the user's container (best-effort; a missing one is fine).
|
||||
if let Err(e) = skald.container().remove(&id).await {
|
||||
tracing::warn!(user = %id, error = %e, "failed to remove user container");
|
||||
}
|
||||
Ok(Json(serde_json::json!({ "ok": true })))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user