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:
1 parent
b71dfc49c3
commit
e7d4b9ff76
11 files changed
+151
-14
No files matched your search
@@ -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
@@ -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
@@ -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) => {
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user