Initial CodeCraft: web-based coding harness for AI models

Rust backend (axum/tokio): agent loop with streaming, tools (fs, bash,
web_fetch, todo, ask_user, subagents, skills, craft snapshots/compare),
permissions with modes and rules, Anthropic + OpenAI-compatible providers,
MCP client (stdio/HTTP, elicitation), SQLite sessions, usage statistics,
compaction, commented TOML config editable via toml_edit.

React PWA: chat with live streaming, permission/question cards, modes
(build/plan/craft), settings for providers/models/prices/MCP/permissions,
raw config editor, statistics dashboard, i18n (en/de). Docker setup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-08 14:39:24 +01:00
commit f3a2fe3816
62 files changed
+21835

No files matched your search

+154
View File
@@ -0,0 +1,154 @@
//! Built-in tools. Each tool declares a JSON schema, whether it is read-only
//! (allowed in plan mode) and how to run it.
pub mod agent;
pub mod bash;
pub mod craft;
pub mod fs;
pub mod web;
use crate::app::App;
use crate::provider::ToolDef;
use crate::types::ImageData;
use anyhow::{Result, bail};
use async_trait::async_trait;
use serde_json::Value;
use std::path::{Component, Path, PathBuf};
use std::sync::Arc;
use tokio_util::sync::CancellationToken;
#[derive(Debug, Default)]
pub struct ToolOutput {
pub content: String,
pub images: Vec<ImageData>,
pub meta: Option<Value>,
pub is_error: bool,
}
impl ToolOutput {
pub fn text(s: impl Into<String>) -> Self {
Self { content: s.into(), ..Default::default() }
}
pub fn error(s: impl Into<String>) -> Self {
Self { content: s.into(), is_error: true, ..Default::default() }
}
pub fn with_meta(mut self, meta: Value) -> Self {
self.meta = Some(meta);
self
}
}
pub struct ToolCtx {
pub app: Arc<App>,
pub session_id: String,
pub call_id: String,
pub cwd: PathBuf,
pub cancel: CancellationToken,
/// Agent mode running this tool (subagents inherit restrictions).
pub agent: String,
}
impl ToolCtx {
/// Resolve a user/model supplied path against the session cwd and make sure
/// it stays inside the workspace.
pub fn resolve(&self, p: &str) -> Result<PathBuf> {
let p = p.trim();
let raw = if Path::new(p).is_absolute() { PathBuf::from(p) } else { self.cwd.join(p) };
let norm = normalize(&raw);
let ws = normalize(&self.app.workspace());
if !norm.starts_with(&ws) && !norm.starts_with("/tmp") {
bail!("path {} is outside the workspace {}", norm.display(), ws.display());
}
Ok(norm)
}
pub fn progress(&self, output: impl Into<String>) {
self.app.emit(crate::types::Event::ToolProgress {
session_id: self.session_id.clone(),
call_id: self.call_id.clone(),
output: output.into(),
});
}
}
/// Lexical normalization (no symlink resolution, works for non-existing files).
pub fn normalize(p: &Path) -> PathBuf {
let mut out = PathBuf::new();
for c in p.components() {
match c {
Component::ParentDir => {
out.pop();
}
Component::CurDir => {}
other => out.push(other.as_os_str()),
}
}
out
}
#[async_trait]
pub trait Tool: Send + Sync {
fn name(&self) -> &str;
fn description(&self) -> String;
fn schema(&self) -> Value;
/// Read-only tools are available in plan mode and never modify state.
fn read_only(&self) -> bool {
false
}
/// Category used by the "accept_edits" permission mode.
fn is_edit(&self) -> bool {
false
}
/// Short human title for permission prompts.
fn title(&self, input: &Value) -> String {
let _ = input;
self.name().to_string()
}
/// Optional unified diff shown in permission prompts.
async fn preview(&self, _input: &Value, _ctx: &ToolCtx) -> Option<String> {
None
}
async fn run(&self, input: Value, ctx: &ToolCtx) -> Result<ToolOutput>;
fn def(&self) -> ToolDef {
ToolDef { name: self.name().into(), description: self.description(), input_schema: self.schema() }
}
}
pub fn builtin_tools() -> Vec<Arc<dyn Tool>> {
vec![
Arc::new(fs::Read),
Arc::new(fs::Write),
Arc::new(fs::Edit),
Arc::new(fs::List),
Arc::new(fs::Glob),
Arc::new(fs::Grep),
Arc::new(bash::Bash),
Arc::new(web::WebFetch),
Arc::new(agent::TodoWrite),
Arc::new(agent::AskUser),
Arc::new(agent::Task),
Arc::new(craft::BrowserSnapshot),
Arc::new(craft::ImageCompare),
Arc::new(crate::prompts::SkillTool),
]
}
/// Helper for reading typed arguments.
pub fn arg_str<'a>(input: &'a Value, key: &str) -> Result<&'a str> {
input
.get(key)
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("missing required string argument '{key}'"))
}
pub fn truncate(s: &str, max: usize) -> String {
if s.len() <= max {
return s.to_string();
}
let mut cut = max;
while !s.is_char_boundary(cut) {
cut -= 1;
}
format!("{}\n\n[... output truncated: {} of {} bytes shown ...]", &s[..cut], cut, s.len())
}