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

+29 -8
View File
@@ -60,13 +60,18 @@ const CRAFT_PROMPT: &str = r#"Mode: CRAFT. You recreate the look and behaviour o
Rules: ask the user (ask_user) when a decision is theirs (stack, scope, which pages). {CLEAN_ROOM}"#;
const CLEAN_ROOM_ON: &str = "Clean room is ON: only use what a visitor can see (screenshots, visible text, measured layout). Do not read the target's page source, scripts or files (web_fetch with raw=true is refused).";
const CLEAN_ROOM_OFF: &str = "Deep mode is ON: you may also read the target's page source and scripts for the user's private use.";
const CLEAN_ROOM_OFF: &str = "Deep mode is ON: you may also read the target's page source and scripts, and analyse programs (binary_info, decompile with Ghidra) for the user's private use. The goal is a faithful RE-CREATION, not a copy. For software:\n1. STUDY: binary_info, then decompile. Find the program's structure: entry point and main loop, UI (windows, menus, dialogs, resources), input handling, file formats, algorithms and data structures, timing.\n2. SPECIFY: write .codecraft/spec.md – behaviour, screens, data and file formats, algorithms in your own words (pseudocode allowed), edge cases. This spec is the only input for the build.\n3. BUILD: implement from the spec in a modern stack (the user's choice, or a sensible default), with clean, idiomatic, documented code. Never paste or mechanically translate decompiled code; names, structure and code must be your own. Reuse only facts (file formats, constants needed for compatibility).\n4. CHECK: compare with the original – same files read/written, same outputs for the same inputs, same look where it matters – and refine.";
const GENERAL_PROMPT: &str = "You are a subagent. Complete the delegated task autonomously and end with a concise, complete report of what you found or did – it is the only thing the calling agent sees.";
const EXPLORE_PROMPT: &str = "You are a fast, read-only exploration subagent. Search broadly (glob, grep, read excerpts) and report the conclusion with precise file:line references. Do not modify anything.";
/// The craft prompt for clean room or deep mode.
pub fn craft_prompt(clean_room: bool) -> String {
CRAFT_PROMPT.replace("{CLEAN_ROOM}", if clean_room { CLEAN_ROOM_ON } else { CLEAN_ROOM_OFF })
}
pub fn builtin_agents(clean_room: bool) -> Vec<Agent> {
let craft = CRAFT_PROMPT.replace("{CLEAN_ROOM}", if clean_room { CLEAN_ROOM_ON } else { CLEAN_ROOM_OFF });
let craft = craft_prompt(clean_room);
vec![
Agent { name: "build".into(), description: "Full access: implement, edit, run".into(), mode: "primary".into(), prompt: BUILD_PROMPT.into(), tools: None, read_only: false, model: None, source: "builtin".into(), max_parallel: None },
Agent { name: "plan".into(), description: "Read-only: analyse and propose a plan".into(), mode: "primary".into(), prompt: PLAN_PROMPT.into(), tools: None, read_only: true, model: None, source: "builtin".into(), max_parallel: None },
@@ -95,8 +100,17 @@ pub fn frontmatter(text: &str) -> (BTreeMap<String, String>, String) {
}
/// Resource directories, lowest priority first: data dir, workspace, project.
/// Skills and commands shipped with CodeCraft (read-only for agents).
pub fn builtin_resources() -> PathBuf {
if let Ok(p) = std::env::var("CODECRAFT_RESOURCES") {
return PathBuf::from(p);
}
["/app/resources", "./resources", "./server/resources"].iter().map(PathBuf::from).find(|p| p.is_dir()).unwrap_or_else(|| PathBuf::from("/app/resources"))
}
/// Resource directories, lowest priority first: built-in, data dir, workspace, project.
fn resource_dirs(app: &App, root: &Path, cwd: &Path, kind: &str) -> Vec<PathBuf> {
let mut dirs = vec![app.data_dir().join(kind)];
let mut dirs = vec![builtin_resources().join(kind), app.data_dir().join(kind)];
let ws = root.to_path_buf();
let mut chain = vec![ws.clone()];
if cwd != ws {
@@ -191,10 +205,13 @@ fn instruction_files(app: &App, root: &Path, cwd: &Path) -> Vec<(PathBuf, String
out
}
pub fn system_prompt(app: &App, agent: &Agent, root: &Path, cwd: &Path, model: &str, permission_mode: &str) -> String {
pub fn system_prompt(app: &App, agent: &Agent, root: &Path, cwd: &Path, model: &str, permission_mode: &str, owner: Option<&crate::store::User>) -> String {
let mut s = String::from(BASE_PROMPT);
s.push_str("\n\n");
s.push_str(&agent.prompt);
if std::env::var("HOME").is_ok_and(|h| h.starts_with("/data")) {
s.push_str("\n\n# Installing tools\nYou run without root (no apt/sudo). Install what you need in user space – it persists: Python packages with `pip install <pkg>` (user site) or a venv, CLI tools with `pipx install`, Rust with `curl https://sh.rustup.rs -sSf | sh -s -- -y` (then `~/.cargo/bin` is on PATH), Node packages with `npm i -g`, and other binaries (e.g. Godot) by downloading them to `~/.local/bin`. Check with `which`/`--version` first whether a tool is already installed.");
}
if permission_mode == "auto" && !agent.read_only {
s.push_str("\n\n# Working autonomously\nThe user does not want to approve every step. Before you start a larger change (several files, installing dependencies, migrations, anything hard to undo), confirm the plan ONCE with ask_user in a short summary. After the user agrees, carry out the whole plan without asking again – reading, editing, building and testing are pre-approved. Only stop to ask when something unexpected changes the plan. Small, obvious fixes need no confirmation. Destructive or external actions (deleting, git push, deployments, changing other systems via MCP) still trigger an approval prompt automatically.");
}
@@ -219,7 +236,7 @@ pub fn system_prompt(app: &App, agent: &Agent, root: &Path, cwd: &Path, model: &
}
}
}
let deferred = app.mcp.deferred_summary(&app.config());
let deferred = app.mcp.deferred_summary(&app.config(), owner);
if !deferred.is_empty() {
s.push_str("\n# On-demand MCP servers\nThese servers have many tools that are not listed directly. Find tools with mcp_search, then call them with mcp_call:\n");
for (server, n, names) in &deferred {
@@ -253,6 +270,9 @@ pub struct Command {
#[serde(skip)]
pub body: String,
pub agent: Option<String>,
/// Craft: switch the session to clean room (true) or deep (false).
#[serde(skip)]
pub clean_room: Option<bool>,
}
pub fn commands(app: &App, root: &Path, cwd: &Path) -> Vec<Command> {
@@ -271,14 +291,15 @@ pub fn commands(app: &App, root: &Path, cwd: &Path) -> Vec<Command> {
.get("description")
.cloned()
.unwrap_or_else(|| body.lines().find(|l| !l.trim().is_empty()).unwrap_or("").chars().take(80).collect());
map.insert(name.clone(), Command { name, description, body, agent: meta.get("agent").cloned() });
let clean_room = meta.get("clean_room").and_then(|v| v.parse::<bool>().ok());
map.insert(name.clone(), Command { name, description, body, agent: meta.get("agent").cloned(), clean_room });
}
}
map.into_values().collect()
}
/// Expand "/name args" into the command's prompt. Returns None if not a known command.
pub fn expand_command(app: &App, root: &Path, cwd: &Path, text: &str) -> Option<(String, Option<String>)> {
pub fn expand_command(app: &App, root: &Path, cwd: &Path, text: &str) -> Option<(String, Option<String>, Option<bool>)> {
let rest = text.strip_prefix('/')?;
let (name, args) = rest.split_once(char::is_whitespace).unwrap_or((rest, ""));
let cmd = commands(app, root, cwd).into_iter().find(|c| c.name == name)?;
@@ -290,7 +311,7 @@ pub fn expand_command(app: &App, root: &Path, cwd: &Path, text: &str) -> Option<
if !cmd.body.contains("$ARGUMENTS") && !args.is_empty() {
body.push_str(&format!("\n\n{args}"));
}
Some((body, cmd.agent))
Some((body, cmd.agent, cmd.clean_room))
}
// ---------------------------------------------------------------------------