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:
Richard ZinkandClaude Opus 5.5 committed 2026-11-08 19:12:30 +01:00
1 parent fab9add5d5
commit 5b569b6835
108 files changed
+6785 -279

No files matched your search

+33 -8
View File
@@ -36,15 +36,32 @@ struct Out {
stderr: String,
}
/// Options that keep repository content from running code (hooks, fsmonitor).
const SAFE_OPTS: &[&str] = &["-c", "core.hooksPath=/dev/null", "-c", "core.fsmonitor=false", "-c", "protocol.ext.allow=never"];
tokio::task_local! {
/// Sandbox of the requesting user for all git calls of this request.
static SANDBOX: Option<crate::sandbox::SandboxSpec>;
}
async fn git(dir: &Path, args: &[&str], timeout: u64) -> Result<Out, ApiError> {
let fut = Command::new("git")
.args(args)
.current_dir(dir)
.env("GIT_TERMINAL_PROMPT", "0")
.env("GIT_PAGER", "cat")
.env("LC_ALL", "C")
.kill_on_drop(true)
.output();
let mut all: Vec<&str> = SAFE_OPTS.to_vec();
all.extend_from_slice(args);
let spec = SANDBOX.try_with(|s| s.clone()).ok().flatten();
let mut cmd = match &spec {
Some(spec) => {
if !crate::sandbox::available() {
return Err(ApiError(StatusCode::SERVICE_UNAVAILABLE, "git is disabled: the sandbox is not available".into()));
}
crate::sandbox::command(spec, dir, "git", &all)
}
None => {
let mut c = Command::new("git");
c.args(&all).current_dir(dir).env("GIT_TERMINAL_PROMPT", "0").env("GIT_PAGER", "cat").env("LC_ALL", "C");
c
}
};
let fut = cmd.kill_on_drop(true).output();
let out = tokio::time::timeout(Duration::from_secs(timeout), fut)
.await
.map_err(|_| ApiError(StatusCode::GATEWAY_TIMEOUT, "git timed out".into()))?
@@ -147,6 +164,11 @@ fn parse_status(raw: &str) -> Value {
}
pub async fn handle(Extension(user): Extension<User>, State(app): State<AppState>, Json(b): Json<GitBody>) -> ApiResult {
let spec = crate::sandbox::spec_for(&app, Some(&user.id));
SANDBOX.scope(spec, handle_inner(user, app, b)).await
}
async fn handle_inner(user: User, app: AppState, b: GitBody) -> ApiResult {
let target = checked_path(&app, &user, &b.path, b.action != "clone")?;
// Actions that create a repository.
@@ -309,6 +331,9 @@ pub async fn handle(Extension(user): Extension<User>, State(app): State<AppState
}
};
let o = git(&repo, &args, 300).await?;
if b.action == "push" {
app.audit(Some(&user), "git.push", format!("{} ({})", repo.display(), if o.ok { "ok" } else { "failed" }), "");
}
if !o.ok {
return Err(fail(o));
}