# ============================================================================= # CodeCraft – configuration # ============================================================================= # # This file is the single source of truth for the server. Everything here can # also be edited in the web UI (Settings). The UI edits this file in place and # keeps your comments and formatting intact. # # Location: $CODECRAFT_CONFIG, otherwise /config.toml (Docker: /data/config.toml) # # Secrets: any string value may reference environment variables with ${NAME}, # e.g. api_key = "${ANTHROPIC_API_KEY}". Prefer this over literal keys – keys # entered in the UI are stored here in plain text. # ============================================================================= [server] # Address the HTTP server listens on. bind = "0.0.0.0:8787" # Root directory the agents may work in. Sessions pick a project folder below it. # Docker: mount your projects here (see docker-compose.yml). workspace = "/workspace" # Where sessions (SQLite) and runtime state are stored. data_dir = "/data" # Default model for new sessions, as "/". default_model = "anthropic/claude-sonnet-5-5" # Cheap/fast model for titles and history compaction. Falls back to default_model. small_model = "anthropic/claude-haiku-5-5" # Compact the conversation automatically when the context is this full (0.0–1.0). auto_compact_threshold = 0.85 # Max. subagents running at the same time (all sessions). Additionally, each # subagent can be limited with max_parallel, and each provider/model with # max_concurrency (see below). max_parallel_subagents = 4 # MCP servers with more tools than this are loaded on demand (see [mcp.*] load). mcp_direct_limit = 15 # Currency label for the cost statistics (prices below are in this currency). currency = "USD" # ----------------------------------------------------------------------------- # Providers # ----------------------------------------------------------------------------- # kind = "anthropic" → Anthropic Messages API (and compatible endpoints) # kind = "openai" → OpenAI-compatible /chat/completions (Gemini, opencode Go, # fal, OpenRouter, Ollama, vLLM, llama.cpp, LM Studio, ...) # # models: optional list. If empty, the model list is fetched from /models. # Per-model options: context (tokens), max_output (tokens), reasoning (bool), # vision (bool, can read images), effort ("low" | "medium" | "high" | "xhigh" | "max"). # # Prices (per 1M tokens, used for the statistics): # input_price, output_price, cache_read_price, cache_write_price # Cache prices default to input_price when omitted. Costs are stored per request, # so changing a price only affects future requests. # # Concurrency: max_concurrency limits simultaneous requests – on the provider # (= the host) and/or per model. Requests beyond the limit wait in a queue, # e.g. for self-hosted models that can only process one request at a time. # # options: extra request body fields sent as-is, e.g. # options = { temperature = 0.5, reasoning_budget_tokens = 5000 } # # Authentication: by default "Authorization: Bearer " (openai) or # "x-api-key: " (anthropic). Override with auth_header / auth_prefix. # Extra static headers: headers = { "X-Foo" = "bar" } # ----------------------------------------------------------------------------- [providers.anthropic] kind = "anthropic" base_url = "https://api.anthropic.com/v1" api_key = "${ANTHROPIC_API_KEY}" # Current Claude models think adaptively; "effort" controls depth and cost. # Prices: Anthropic list prices in USD per 1M tokens (Nov 2026) – adjust if needed. models = [ { id = "claude-opus-5-5", context = 1000000, max_output = 64000, reasoning = true, vision = true, effort = "high", input_price = 4.0, output_price = 20.0, cache_read_price = 0.20, cache_write_price = 5.0 }, { id = "claude-sonnet-5-5", context = 1000000, max_output = 64000, reasoning = true, vision = true, effort = "medium", input_price = 2.0, output_price = 10.0, cache_read_price = 0.20, cache_write_price = 2.5 }, { id = "claude-haiku-5-5", context = 1000000, max_output = 32000, reasoning = true, vision = true, effort = "low", input_price = 0.10, output_price = 0.50, cache_read_price = 0.01, cache_write_price = 0.125 }, ] # Google Gemini through its OpenAI-compatible endpoint. [providers.gemini] kind = "openai" base_url = "https://generativelanguage.googleapis.com/v1beta/openai" api_key = "${GEMINI_API_KEY}" models = [] # discovered automatically enabled = false # set to true once GEMINI_API_KEY is available # opencode Go subscription (OpenAI-compatible endpoint for most models). # Current model list: https://opencode.ai/inference/go/v1/models [providers.opencode-go] kind = "openai" base_url = "https://opencode.ai/inference/go/openai/v1" api_key = "${OPENCODE_API_KEY}" models = [] enabled = false # fal.ai – OpenRouter-backed LLM router. fal expects "Authorization: Key ". # NOTE: verify base_url against the fal docs for your account. [providers.fal] kind = "openai" base_url = "https://fal.run/openrouter/router/openai/v1" api_key = "${FAL_KEY}" auth_prefix = "Key " models = [ { id = "google/gemini-2.5-flash", context = 1000000, max_output = 32000, vision = true }, ] enabled = false # Self-hosted (Ollama shown; vLLM/llama.cpp/LM Studio work the same way). # From inside Docker, the host is reachable as host.docker.internal. [providers.local] kind = "openai" base_url = "http://host.docker.internal:11434/v1" api_key = "" max_concurrency = 1 # one request at a time on this host models = [] enabled = false # ----------------------------------------------------------------------------- # MCP servers # ----------------------------------------------------------------------------- # type = "stdio" → command + args (+ env), started as a child process # type = "http" → Streamable HTTP endpoint (url + optional headers) # Tools appear to the model as mcp____. # users = [...]: who may use the server. Default: admins only. # Use ["*"] for all users or list usernames, e.g. ["anna", "ben"]. # load = "auto" (default) | "always" | "on_demand": servers with many tools # (more than server.mcp_direct_limit) are not sent with every request; the # model finds them with mcp_search and calls them with mcp_call. This keeps # requests small – important for self-hosted models. # MCP servers may ask you questions (elicitation); they show up in the UI. # ----------------------------------------------------------------------------- # [mcp.trilium] # type = "http" # url = "https://notes.example.org/mcp" # headers = { Authorization = "Bearer ${TRILIUM_TOKEN}" } # [mcp.filesystem] # type = "stdio" # command = "npx" # args = ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"] # env = {} # enabled = false # ----------------------------------------------------------------------------- # Permissions # ----------------------------------------------------------------------------- # For each tool: "allow" | "ask" | "deny". "ask" forwards the request to the UI. # MCP tools can be addressed as "mcp____" or "mcp____*". # Session permission modes in the UI override this: # default → these rules # accept_edits → file edits allowed without asking # bypass → everything allowed (use with care) # ----------------------------------------------------------------------------- [permissions] read = "allow" glob = "allow" grep = "allow" list = "allow" todo_write = "allow" ask_user = "allow" task = "allow" write = "ask" edit = "ask" bash = "ask" bash_output = "allow" kill_shell = "allow" web_fetch = "ask" web_search = "allow" diagnostics = "allow" browser_snapshot = "allow" image_compare = "allow" generate_image = "ask" # costs money per image "mcp__*" = "ask" # Shell command patterns (glob, matched against the full command). First match wins. [permissions.bash_rules] "git status*" = "allow" "git diff*" = "allow" "git log*" = "allow" "ls*" = "allow" "pwd" = "allow" "rm -rf /*" = "deny" # ----------------------------------------------------------------------------- # Agents (modes) # ----------------------------------------------------------------------------- # Built-in: build (full access), plan (read-only, proposes a plan), # craft (study → build → check → ready), and subagents # general + explore used by the "task" tool. # Define your own or override built-ins here. mode = "primary" shows up in the # mode switcher, mode = "subagent" is only available through the task tool. # ----------------------------------------------------------------------------- # Subagents can run on a different model (and host) than the chat, and in the # background: the main agent calls task(background = true), keeps working and # receives the result as a message when the subagent is done. # [agents.writer] # mode = "subagent" # description = "Writes documentation, summaries, guides and translations." # model = "local/qwen3-4b" # optional, otherwise the session model # tools = ["read", "glob", "grep"] # optional, default: all tools # max_parallel = 1 # optional, instances at the same time # prompt = ''' # You are a technical writer. Write clear, well-structured text. # ''' # ----------------------------------------------------------------------------- # Image generation (fal.ai) # ----------------------------------------------------------------------------- # Agents get the tool generate_image; the "Images" page creates images manually. # Images are saved in the project (output_dir) and listed in a gallery. # Model ids: https://fal.ai/models – price is per image (for the statistics). # size_param: "image_size" (FLUX-style presets, default) or "aspect_ratio" ("16:9"). # ----------------------------------------------------------------------------- [images] enabled = false base_url = "https://fal.run" api_key = "${FAL_KEY}" default_model = "fal-ai/flux/schnell" output_dir = "generated" models = [ { id = "fal-ai/flux/schnell", name = "FLUX.1 schnell – fast", price = 0.003 }, { id = "fal-ai/flux-2-pro", name = "FLUX.2 pro" }, { id = "fal-ai/ideogram/v3", name = "Ideogram 3 – text in images", size_param = "aspect_ratio" }, { id = "fal-ai/recraft/v4/pro/text-to-image", name = "Recraft V4 pro – design" }, ] # ----------------------------------------------------------------------------- # Users & sign-in # ----------------------------------------------------------------------------- # Users are managed in the web UI (Settings → Users). On first start without # users, the server prints a one-time setup code to its log; use it in the UI # to create the first admin – or set CODECRAFT_ADMIN_USER and # CODECRAFT_ADMIN_PASSWORD in the environment. # # Every user works in their own directory /; admins can # use the whole workspace. Note: file tools are confined to that directory, # but shell commands run in the shared container and are not sandboxed. # ----------------------------------------------------------------------------- [auth] # Lifetime of a login in days. session_days = 30 # External URL of this server – required for single sign-on (OIDC redirects). public_url = "" # Secure cookies: "auto" (when served via https), "true" or "false". cookie_secure = "auto" # Single sign-on via OpenID Connect (Authentik, Authelia, Keycloak, ...). # Redirect URI to register at the IdP: /api/auth/oidc/callback [auth.oidc] enabled = false issuer = "https://auth.example.org/application/o/codecraft/" client_id = "codecraft" client_secret = "${OIDC_CLIENT_SECRET}" scopes = ["openid", "profile", "email", "groups"] username_claim = "preferred_username" groups_claim = "groups" # Members of this group are admins; empty = manage roles in CodeCraft. admin_group = "" # Only members of this group may sign in; empty = no restriction. allowed_group = "" # Create users automatically on their first sign-in. auto_create = true # Link a sign-in to an existing local user with the same username. # Only enable this if you trust the usernames of your IdP. link_by_username = false button_label = "Single sign-on" # ----------------------------------------------------------------------------- # Web search (web_search tool) # ----------------------------------------------------------------------------- [search] # "duckduckgo" (no key, may be rate-limited), "searxng" (self-hosted, set url), # "brave" or "tavily" (api_key, e.g. "${BRAVE_API_KEY}") provider = "duckduckgo" url = "" api_key = "" # ----------------------------------------------------------------------------- # Language servers (diagnostics after edits, tool "diagnostics") # ----------------------------------------------------------------------------- # Built in: rust (rust-analyzer), typescript (typescript-language-server), # python (pyright-langserver), go (gopls), c (clangd) – used when installed. # TypeScript and Python servers ship with the image. Servers run in the # user's sandbox and stop after 15 minutes without use. # ----------------------------------------------------------------------------- [lsp] enabled = true # Add or override a server: # [lsp.servers.zig] # command = "zls" # extensions = ["zig"] # root_markers = ["build.zig"] # ----------------------------------------------------------------------------- # Hooks – shell commands on agent events # ----------------------------------------------------------------------------- # event: pre_tool | post_tool | user_prompt | stop # matcher: tool name pattern for tool events ("*", "bash", "edit|write", "mcp__*") # The hook gets the event as JSON on stdin (session_id, cwd, tool, input, # result, prompt, last_message) and runs in the user's sandbox in the session # directory. Exit 0: stdout is added as context (user_prompt, post_tool). # Exit 2: block – pre_tool rejects the call, user_prompt drops the prompt, # post_tool sends stderr to the agent as feedback, stop makes the agent # continue with stderr as instruction (max. 3 times per turn). # ----------------------------------------------------------------------------- # [[hooks]] # event = "post_tool" # matcher = "edit|write" # command = 'f=$(jq -r .input.path); case "$f" in *.py) ruff format -q "$f";; esac; exit 0' # # [[hooks]] # event = "pre_tool" # matcher = "bash" # command = 'jq -r .input.command | grep -q "git push" && { echo "No pushes from agents" >&2; exit 2; }; exit 0' # ----------------------------------------------------------------------------- # Sandbox # ----------------------------------------------------------------------------- # Shell commands and git of a user run in a bubblewrap sandbox: system # read-only, only the user's folders writable, own home, own processes, no # server environment. The network is shared (package managers keep working). # Needs the security options in docker-compose.yml. Without a working # sandbox, non-admin users cannot run shell commands. [sandbox] # "auto" = non-admin users, "always" = everybody (admins too), "off" = nobody mode = "auto" # ----------------------------------------------------------------------------- # Craft mode # ----------------------------------------------------------------------------- [craft] # Headless Chromium binary used to study websites and check builds. chromium = "chromium" # Screen widths used for screenshots and comparisons. widths = [390, 768, 1440] # Clean room: only what a visitor sees (screenshots, visible text, layout). # Set to false for "deep" mode (page source and scripts may be read). clean_room = true