Reverse engineering tools per mode; raw firmware in decompile; skill

- AgentConfig.reverse_tools (Settings -> Agents & commands) gives any mode
  binary_info and decompile; the clean-room check of the tools applies to
  craft only (ToolCtx.clean_room = clean room && craft).
- decompile: optional processor (Ghidra language id) and base_address for raw
  images (BinaryLoader); separate cache per processor/address.
- Skill "reverse-engineering": identify, convert HEX, processor table for
  common chips, decompile, explore, notes in analysis/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-11 07:47:05 +01:00
1 parent b71dfc49c3
commit e7d4b9ff76
11 files changed
+151 -14

No files matched your search

+4
View File
@@ -398,6 +398,10 @@ pub struct AgentConfig {
/// Review loop after each turn of this mode.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub refine: Option<RefineConfig>,
/// Reverse engineering tools (binary_info, decompile with Ghidra) in this mode;
/// craft has them anyway (deep mode).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reverse_tools: Option<bool>,
}
/// Refinement loop: after the agent finishes, a reviewer checks the result
+11 -6
View File
@@ -32,6 +32,8 @@ pub struct Agent {
pub max_parallel: Option<usize>,
/// Review loop after each turn (None/0 rounds = off).
pub refine: Option<crate::config::RefineConfig>,
/// binary_info and decompile available (besides craft).
pub reverse: bool,
/// Source file of Markdown agents.
#[serde(skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
@@ -43,12 +45,12 @@ pub use codecraft_core::prompts::*;
pub fn builtin_agents(clean_room: bool) -> Vec<Agent> {
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, style: "coding".into(), model: None, source: "builtin".into(), max_parallel: None, refine: None, path: 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, style: "coding".into(), model: None, source: "builtin".into(), max_parallel: None, refine: None, path: None },
Agent { name: "chat".into(), description: "Just talk: answers, explanations, web search – no files or shell".into(), mode: "primary".into(), prompt: CHAT_PROMPT.into(), tools: Some(vec!["web_search".into(), "web_fetch".into()]), read_only: true, style: "chat".into(), model: None, source: "builtin".into(), max_parallel: None, refine: None, path: None },
Agent { name: "craft".into(), description: "Study a target, build your own version, check and refine".into(), mode: "primary".into(), prompt: craft, tools: None, read_only: false, style: "coding".into(), model: None, source: "builtin".into(), max_parallel: None, refine: None, path: None },
Agent { name: "general".into(), description: "General-purpose subagent for multi-step tasks".into(), mode: "subagent".into(), prompt: GENERAL_PROMPT.into(), tools: None, read_only: false, style: "coding".into(), model: None, source: "builtin".into(), max_parallel: None, refine: None, path: None },
Agent { name: "explore".into(), description: "Read-only subagent for fast codebase searches".into(), mode: "subagent".into(), prompt: EXPLORE_PROMPT.into(), tools: None, read_only: true, style: "coding".into(), model: None, source: "builtin".into(), max_parallel: None, refine: None, path: None },
Agent { name: "build".into(), description: "Full access: implement, edit, run".into(), mode: "primary".into(), prompt: BUILD_PROMPT.into(), tools: None, read_only: false, style: "coding".into(), model: None, source: "builtin".into(), max_parallel: None, refine: None, reverse: false, path: 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, style: "coding".into(), model: None, source: "builtin".into(), max_parallel: None, refine: None, reverse: false, path: None },
Agent { name: "chat".into(), description: "Just talk: answers, explanations, web search – no files or shell".into(), mode: "primary".into(), prompt: CHAT_PROMPT.into(), tools: Some(vec!["web_search".into(), "web_fetch".into()]), read_only: true, style: "chat".into(), model: None, source: "builtin".into(), max_parallel: None, refine: None, reverse: false, path: None },
Agent { name: "craft".into(), description: "Study a target, build your own version, check and refine".into(), mode: "primary".into(), prompt: craft, tools: None, read_only: false, style: "coding".into(), model: None, source: "builtin".into(), max_parallel: None, refine: None, reverse: false, path: None },
Agent { name: "general".into(), description: "General-purpose subagent for multi-step tasks".into(), mode: "subagent".into(), prompt: GENERAL_PROMPT.into(), tools: None, read_only: false, style: "coding".into(), model: None, source: "builtin".into(), max_parallel: None, refine: None, reverse: false, path: None },
Agent { name: "explore".into(), description: "Read-only subagent for fast codebase searches".into(), mode: "subagent".into(), prompt: EXPLORE_PROMPT.into(), tools: None, read_only: true, style: "coding".into(), model: None, source: "builtin".into(), max_parallel: None, refine: None, reverse: false, path: None },
]
}
@@ -122,6 +124,7 @@ pub fn agents(app: &App, root: &Path, cwd: &Path) -> Vec<Agent> {
source: "file".into(),
max_parallel: meta.get("max_parallel").and_then(|v| v.parse().ok()),
refine: meta.get("refine_rounds").and_then(|v| v.parse().ok()).map(|rounds| crate::config::RefineConfig { rounds, model: meta.get("refine_model").cloned(), criteria: None, after_todos: meta.get("refine_after_todos").is_none_or(|v| v != "false") }),
reverse: meta.get("reverse_tools").is_some_and(|v| v == "true"),
path: Some(p.display().to_string()),
});
}
@@ -141,6 +144,7 @@ pub fn agents(app: &App, root: &Path, cwd: &Path) -> Vec<Agent> {
source: "config".into(),
max_parallel: a.max_parallel.or(base.as_ref().and_then(|b| b.max_parallel)),
refine: a.refine.clone().or(base.as_ref().and_then(|b| b.refine.clone())),
reverse: a.reverse_tools.unwrap_or_else(|| base.as_ref().is_some_and(|b| b.reverse)),
path: None,
});
}
@@ -173,6 +177,7 @@ pub fn agents_for(app: &App, root: &Path, cwd: &Path, user_id: Option<&str>) ->
source: "personal".into(),
max_parallel: None,
refine: a.refine,
reverse: a.reverse_tools.unwrap_or(false),
path: None,
});
}
+30 -4
View File
@@ -142,7 +142,10 @@ impl Tool for Decompile {
"Decompile an executable or library with Ghidra (x86/x64/ARM/MIPS/PowerPC/68k …, PE, ELF, \
Mach-O, raw DOS). Writes pseudo-C for every function plus strings, imports and exports \
into .codecraft/decompiled/<name>/ and returns an overview. Can take several minutes for \
large programs; results are cached per file."
large programs; results are cached per file. Raw firmware images (.bin without a file \
format) need `processor` (Ghidra language id, e.g. \"ARM:LE:32:Cortex\" for Cortex-M, \
\"ARM:LE:32:v8\", \"AVR8:LE:16:atmega256\", \"Xtensa:LE:32:default\" for ESP32) and usually \
`base_address` (load address, e.g. \"0x08000000\" for STM32 flash)."
.into()
}
fn schema(&self) -> Value {
@@ -151,7 +154,9 @@ impl Tool for Decompile {
"properties": {
"path": {"type": "string"},
"max_functions": {"type": "integer", "description": "Limit for very large binaries (default: all)"},
"force": {"type": "boolean", "description": "Re-run even if cached"}
"force": {"type": "boolean", "description": "Re-run even if cached"},
"processor": {"type": "string", "description": "Ghidra language id for raw images, e.g. ARM:LE:32:Cortex (default: detected from the file format)"},
"base_address": {"type": "string", "description": "Load address of a raw image, e.g. 0x08000000"}
},
"required": ["path"]
})
@@ -171,7 +176,20 @@ impl Tool for Decompile {
bail!("{} is not a file", path.display());
}
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("binary").replace(|c: char| !c.is_ascii_alphanumeric() && c != '.' && c != '-', "_");
let out_dir = ctx.cwd.join(".codecraft/decompiled").join(format!("{name}-{}", file_hash(&path)?));
let processor = input["processor"].as_str().map(str::trim).filter(|s| !s.is_empty()).map(String::from);
let base = input["base_address"].as_str().map(str::trim).filter(|s| !s.is_empty()).map(String::from);
if processor.as_deref().is_some_and(|p| !p.chars().all(|c| c.is_ascii_alphanumeric() || ":_-.".contains(c))) {
bail!("invalid processor id (expected e.g. ARM:LE:32:Cortex)");
}
if base.as_deref().is_some_and(|b| !b.trim_start_matches("0x").chars().all(|c| c.is_ascii_hexdigit())) {
bail!("invalid base_address (expected e.g. 0x08000000)");
}
// Another processor or load address is another analysis.
let variant = match (&processor, &base) {
(None, None) => String::new(),
(p, b) => format!("-{}", crate::blobs::hash(format!("{p:?}{b:?}").as_bytes()).get(..8).unwrap_or("")),
};
let out_dir = ctx.cwd.join(".codecraft/decompiled").join(format!("{name}-{}{variant}", file_hash(&path)?));
if out_dir.join(".done").exists() && !input["force"].as_bool().unwrap_or(false) {
return Ok(ToolOutput::text(format!("(cached)\n{}", summarize(&out_dir, &ctx.cwd)?)));
}
@@ -183,7 +201,15 @@ impl Tool for Decompile {
let scripts = scripts_dir().display().to_string();
let max = input["max_functions"].as_u64().map(|n| n.to_string()).unwrap_or_else(|| "2147483647".into());
let (p, proj, out) = (path.display().to_string(), project.display().to_string(), out_dir.display().to_string());
let args = vec![proj.as_str(), "cc", "-import", p.as_str(), "-scriptPath", scripts.as_str(), "-postScript", "CodeCraftExport.java", out.as_str(), max.as_str(), "-deleteProject", "-max-cpu", "2"];
let mut args = vec![proj.as_str(), "cc", "-import", p.as_str()];
// Raw firmware: the binary loader with the given processor and load address.
if let Some(proc_id) = processor.as_deref() {
args.extend(["-processor", proc_id]);
}
if let Some(addr) = base.as_deref() {
args.extend(["-loader", "BinaryLoader", "-loader-baseAddr", addr]);
}
args.extend(["-scriptPath", scripts.as_str(), "-postScript", "CodeCraftExport.java", out.as_str(), max.as_str(), "-deleteProject", "-max-cpu", "2"]);
let java = tools.join("jdk").display().to_string();
let mut cmd = match &ctx.sandbox {
Some(spec) => {
+8 -2
View File
@@ -226,7 +226,12 @@ pub fn available_tools(app: &App, agent: &Agent, owner: Option<&crate::store::Us
all.into_iter()
.filter(|t| !agent.read_only || t.read_only())
// Craft tools only where they are useful.
.filter(|t| agent.name == "craft" || !matches!(t.name(), "browser_snapshot" | "image_compare" | "binary_info" | "decompile" | "craft_phase") || agent.tools.is_some())
.filter(|t| {
agent.name == "craft"
|| agent.tools.is_some()
|| (agent.reverse && matches!(t.name(), "binary_info" | "decompile"))
|| !matches!(t.name(), "browser_snapshot" | "image_compare" | "binary_info" | "decompile" | "craft_phase")
})
.filter(|t| match &agent.tools {
None => true,
Some(list) => {
@@ -456,7 +461,8 @@ pub async fn run_session(app: Arc<App>, session_id: &str, cancel: CancellationTo
agent: agent.name.clone(),
roots: roots.clone(),
sandbox: crate::sandbox::spec_for(&app, session.owner_id.as_deref()),
clean_room,
// Clean room is a craft rule; other modes with reverse tools may analyse binaries.
clean_room: clean_room && agent.name == "craft",
owner_id: None,
};
let by_name = |n: &str| tools.iter().find(|t| t.name() == n).cloned();