Add security hardening, Ghidra, recreate skill, checkpoints, shells, web search and themes
- Sandbox (bubblewrap) for non-admin shell/git, MCP per-user rights, hard budgets and quotas, login rate limiting, audit log and backups - Ghidra headless decompilation (ARM64 natives built from source), binary_info tool, clean room / deep workflow per session - Recreate skill and commands for analysing and re-building old software - File checkpoints with rewind/restore, background shells (bash_output, kill_shell) - web_search tool (DuckDuckGo, SearXNG, Brave, Tavily) - In-app dialogs and toasts instead of browser dialogs - 15 base themes (trilium-themes palettes plus Nord, VS Code, GitHub, Catppuccin, One, Dracula, Rose Pine, Everforest, Kanagawa) with light/dark/auto switcher and license notes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fab9add5d5
commit
5b569b6835
108 files changed
+6785
-279
No files matched your search
@@ -0,0 +1,134 @@
|
||||
//! Per-user sandbox for shell commands (and git) using bubblewrap.
|
||||
//!
|
||||
//! A sandboxed command sees the system read-only, only the user's folders
|
||||
//! writable, its own home directory, a private /tmp, its own process
|
||||
//! namespace and an empty environment (no server secrets). The network is
|
||||
//! shared so package managers keep working.
|
||||
|
||||
use crate::app::App;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::OnceLock;
|
||||
use tokio::process::Command;
|
||||
|
||||
/// What a sandboxed process may access.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SandboxSpec {
|
||||
pub username: String,
|
||||
/// Writable directories (the user's roots).
|
||||
pub roots: Vec<PathBuf>,
|
||||
/// Host directory mounted as the user's home.
|
||||
pub home: PathBuf,
|
||||
}
|
||||
|
||||
impl SandboxSpec {
|
||||
pub fn home_inside(&self) -> String {
|
||||
format!("/home/{}", self.username)
|
||||
}
|
||||
}
|
||||
|
||||
static AVAILABLE: OnceLock<Result<(), String>> = OnceLock::new();
|
||||
|
||||
/// Check once whether bubblewrap works in this environment.
|
||||
pub async fn probe() -> Result<(), String> {
|
||||
if let Some(r) = AVAILABLE.get() {
|
||||
return r.clone();
|
||||
}
|
||||
let res = match Command::new("bwrap")
|
||||
.args(["--ro-bind", "/usr", "/usr", "--ro-bind", "/bin", "/bin", "--ro-bind", "/lib", "/lib", "--dev", "/dev", "--proc", "/proc"])
|
||||
.args(["--unshare-all", "--die-with-parent", "--clearenv", "/bin/true"])
|
||||
.env_clear()
|
||||
.output()
|
||||
.await
|
||||
{
|
||||
Ok(o) if o.status.success() => Ok(()),
|
||||
Ok(o) => Err(String::from_utf8_lossy(&o.stderr).trim().to_string()),
|
||||
Err(e) => Err(format!("bubblewrap is not installed: {e}")),
|
||||
};
|
||||
match &res {
|
||||
Ok(()) => tracing::info!("sandbox: bubblewrap available"),
|
||||
Err(e) => tracing::warn!("sandbox: bubblewrap unavailable – shell commands of non-admin users are disabled ({e})"),
|
||||
}
|
||||
let _ = AVAILABLE.set(res.clone());
|
||||
res
|
||||
}
|
||||
|
||||
pub fn available() -> bool {
|
||||
matches!(AVAILABLE.get(), Some(Ok(())))
|
||||
}
|
||||
|
||||
/// The sandbox for a session owner, or None if commands run unsandboxed
|
||||
/// (admins in "auto" mode, or sandbox mode "off").
|
||||
pub fn spec_for(app: &App, owner_id: Option<&str>) -> Option<SandboxSpec> {
|
||||
let mode = app.config().sandbox.mode.clone();
|
||||
if mode == "off" {
|
||||
return None;
|
||||
}
|
||||
let user = owner_id.and_then(|id| app.store.user(id).ok().flatten())?;
|
||||
if user.is_admin() && mode != "always" {
|
||||
return None;
|
||||
}
|
||||
let home = app.data_dir().join("users").join(&user.username).join("home");
|
||||
let _ = std::fs::create_dir_all(&home);
|
||||
Some(SandboxSpec { username: user.username.clone(), roots: app.user_roots(Some(&user.id)), home })
|
||||
}
|
||||
|
||||
/// Build a command that runs `program args` inside the sandbox in `cwd`.
|
||||
pub fn command(spec: &SandboxSpec, cwd: &Path, program: &str, args: &[&str]) -> Command {
|
||||
command_with(spec, cwd, &[], program, args)
|
||||
}
|
||||
|
||||
/// Like `command`, with extra host directories mounted read-only (e.g. tools).
|
||||
pub fn command_with(spec: &SandboxSpec, cwd: &Path, extra_ro: &[&Path], program: &str, args: &[&str]) -> Command {
|
||||
let mut c = Command::new("bwrap");
|
||||
// Empty environment: bubblewrap's own helper process must not carry secrets.
|
||||
c.env_clear();
|
||||
for dir in ["/usr", "/bin", "/lib", "/etc", "/sbin", "/opt"] {
|
||||
if Path::new(dir).is_dir() && !Path::new(dir).is_symlink() {
|
||||
c.args(["--ro-bind", dir, dir]);
|
||||
} else if let Ok(target) = std::fs::read_link(dir) {
|
||||
c.arg("--symlink").arg(target).arg(dir);
|
||||
}
|
||||
}
|
||||
for dir in ["/lib64", "/lib32"] {
|
||||
let p = Path::new(dir);
|
||||
if let Ok(target) = std::fs::read_link(p) {
|
||||
c.arg("--symlink").arg(target).arg(dir);
|
||||
} else if p.is_dir() {
|
||||
c.args(["--ro-bind", dir, dir]);
|
||||
}
|
||||
}
|
||||
for root in &spec.roots {
|
||||
c.arg("--bind").arg(root).arg(root);
|
||||
}
|
||||
// Built-in skills and templates are readable (not writable) in the sandbox.
|
||||
let builtin = crate::prompts::builtin_resources();
|
||||
for dir in extra_ro.iter().copied().chain(std::iter::once(builtin.as_path())) {
|
||||
if dir.exists() {
|
||||
c.arg("--ro-bind").arg(dir).arg(dir);
|
||||
}
|
||||
}
|
||||
let home = spec.home_inside();
|
||||
c.arg("--bind").arg(&spec.home).arg(&home);
|
||||
c.args(["--tmpfs", "/tmp", "--dev", "/dev", "--proc", "/proc"]);
|
||||
c.args(["--unshare-all", "--share-net", "--die-with-parent", "--new-session", "--clearenv"]);
|
||||
let path = format!("{home}/.local/bin:{home}/.cargo/bin:{home}/.npm-global/bin:/usr/local/bin:/usr/bin:/bin");
|
||||
for (k, v) in [
|
||||
("HOME", home.as_str()),
|
||||
("USER", spec.username.as_str()),
|
||||
("PATH", path.as_str()),
|
||||
("LANG", "C.UTF-8"),
|
||||
("TERM", "xterm-256color"),
|
||||
("CODECRAFT", "1"),
|
||||
("GIT_TERMINAL_PROMPT", "0"),
|
||||
("PAGER", "cat"),
|
||||
("PIP_BREAK_SYSTEM_PACKAGES", "1"),
|
||||
] {
|
||||
c.args(["--setenv", k, v]);
|
||||
}
|
||||
c.arg("--setenv").arg("NPM_CONFIG_PREFIX").arg(format!("{home}/.npm-global"));
|
||||
c.arg("--setenv").arg("PIPX_HOME").arg(format!("{home}/.local/pipx"));
|
||||
c.arg("--setenv").arg("PIPX_BIN_DIR").arg(format!("{home}/.local/bin"));
|
||||
c.arg("--chdir").arg(cwd);
|
||||
c.arg(program).args(args);
|
||||
c
|
||||
}
|
||||
Reference in new issue
Block a user