reloaded-code-core

June 5, 2026 · View on GitHub

Crates.io Docs.rs

Framework-agnostic core tools for building coding agents - file operations, search, shell execution, sandboxing, permissions, custom tool registries, and system prompt generation.

Headless, TUI, or anything in between. Production-grade implementations with minimal overhead.

Documentation · API Reference

Table of contents

Install

# Async (default)
reloaded-code-core = "0.2"

# Sync/blocking
reloaded-code-core = { version = "0.2", default-features = false, features = ["blocking"] }

Feature flags

  • tokio (default): async runtime support
  • blocking: sync/blocking mode
  • async: internal base async feature (enabled by runtimes, not directly)
  • linux-bubblewrap: Sandboxing support for Linux, by leveraging bwrap tool.

tokio and blocking are mutually exclusive.

Tools, context, and integration

Canonical tool metadata lives in tool_metadata.

Each grouped module exposes the model-facing tool name plus the provider-facing metadata used by wrappers such as SerdesAI: read, write, edit, glob, grep, bash, webfetch, todoread, todowrite, and task.

Standard tools

Path safety and sandboxing

Path-based tools are generic over PathResolver, so wrappers can choose unrestricted access or sandboxed access.

  • AbsolutePathResolver enforces absolute-path inputs (unrestricted mode).
  • AllowedPathResolver constrains operations to configured directories (sandbox mode).
  • AllowedGlobResolver constrains to a workspace root with glob pattern filtering (fine-grained sandbox mode).
  • Failed resolution rejects traversal and out-of-sandbox paths before tool execution.
use reloaded_code_core::{
    path::{AllowedGlobResolver, GlobPolicy, RuleAction},
    resolve_workspace_root, AbsolutePathResolver, AllowedPathResolver, PathResolver, ToolResult,
};

fn demo() -> ToolResult<()> {
    // Unrestricted mode: any absolute path is allowed.
    let any_path = AbsolutePathResolver;
    let _hosts = any_path.resolve("/etc/hosts")?;

    // Sandboxed mode: multiple allowed directories.
    let sandbox = AllowedPathResolver::new(["/workspace/project", "/tmp"])?;
    let _lib = sandbox.resolve("src/lib.rs")?;

    // Fine-grained sandbox with glob policy (workspace-relative patterns).
    let root = resolve_workspace_root()?;
    let glob = AllowedGlobResolver::new(&root)?
        .with_policy(
            GlobPolicy::builder()
                .add("src/**", RuleAction::Allow)?    // Matches src/lib.rs
                .add("*.rs", RuleAction::Allow)?      // Also matches src/lib.rs
                .add("target/**", RuleAction::Deny)?  // Blocks target/ even if *.rs matches
                .build()?
        );
    let _lib = glob.resolve("src/lib.rs")?;
    let _main = glob.resolve("main.rs")?;
    // glob.resolve("target/debug/app")?; // Denied
    Ok(())
}

Permission glob semantics

  • * matches any characters within a single path component (e.g., *.rs matches lib.rs but not src/lib.rs).
  • ** matches any number of path components, relative to the workspace root (e.g., src/**/*.rs matches src/deep/nested/mod.rs).
  • /** matches any absolute path on the system (e.g., matches /etc/passwd, C:/Windows/system32).
  • Bare allow maps to ** (all files under the workspace root).
  • Relative patterns are implicitly joined with the workspace root at construction time.
  • Absolute patterns (leading / or drive-root like C:/) are treated as-is.

Last-match-wins: both deny-then-allow and allow-then-deny orders work depending on whether you want a default-deny or default-allow posture.

Linux shell sandboxing

Enable the linux-bubblewrap feature flag to sandbox bash (execute_command) via Linux bwrap. This limits visible filesystem, environment, and network access for executed commands.

Two profiles are available:

  • Public Bot (Profile::public_bot_defaults) Strictest containment for hostile input. No host filesystem access, synthetic home, memory-backed /tmp, network disabled.

  • Trusted Maintenance (Profile::trusted_maintenance_defaults) Broader profile for builds and repairs in a more trusted environment. Read-only host / with writable overlays, disk-backed /tmp, network enabled.

We default to the Public Bot profile when sandboxing is enabled. In either case, evaluate whether the chosen profile fits your security needs.

See Sandboxing docs for the full operator guide and checklist.

Context and wrapper mapping

context provides reusable guidance constants.

Wrappers usually bind a tool's canonical name and guidance through ToolContext:

Any-path read tool:

use reloaded_code_core::context::{PathMode, ToolPrompt};
use reloaded_code_core::{ToolContext, tool_metadata};

struct ReadTool;

impl ReadTool {
    fn new() -> Self {
        Self
    }
}

impl ToolContext for ReadTool {
    fn name(&self) -> &'static str {
        tool_metadata::read::NAME
    }

    fn context(&self) -> ToolPrompt {
        ToolPrompt::Read {
            path_mode: PathMode::Absolute,
            line_numbers: true,
        }
    }
}

let _tool = ReadTool::new();

Sandboxed read tool:

use reloaded_code_core::{
    AllowedPathResolver, ToolContext, tool_metadata,
};
use reloaded_code_core::context::{PathMode, ToolPrompt};

struct ReadTool {
    _resolver: AllowedPathResolver,
}

impl ReadTool {
    fn new(resolver: AllowedPathResolver) -> Self {
        Self {
            _resolver: resolver,
        }
    }
}

impl ToolContext for ReadTool {
    fn name(&self) -> &'static str {
        tool_metadata::read::NAME
    }

    fn context(&self) -> ToolPrompt {
        ToolPrompt::Read {
            path_mode: PathMode::Allowed,
            line_numbers: true,
        }
    }
}

let resolver = AllowedPathResolver::new(["/workspace/project"])
    .expect("valid allowed path");
let _tool = ReadTool::new(resolver);

Core tool functions are generic over PathResolver, but wrappers usually expose separate absolute/allowed tool types for simpler ergonomics. This avoids extra generic parameters.

This keeps registration names such as tool_metadata::read::NAME and prompt guidance in sync.

System prompt builder

SystemPromptBuilder builds one prompt string for agent runtimes.

You usually build framework wrappers from these primitives (ToolContext + SystemPromptBuilder).

If tools are selected from a catalog or created later, use ToolFactory, ToolCatalogEntry, and CustomToolRegistry from core.

Typical wrapper integration (serdesAI)

For example with reloaded-code-serdesai, wrappers are built from these primitives.

# #[cfg(any())]
# {
use reloaded_code_serdesai::absolute::{GlobTool, GrepTool, ReadTool};
use reloaded_code_serdesai::{BashTool, SystemPromptBuilder};
use serdes_ai::prelude::*;

let mut pb = SystemPromptBuilder::new()
    .working_directory(std::env::current_dir()?.display().to_string());

let agent = AgentBuilder::<(), String>::new(model)
    .tool(pb.track(ReadTool::<true>::new()))
    .tool(pb.track(GlobTool::new()))
    .tool(pb.track(GrepTool::<true>::new()))
    .tool(pb.track(BashTool::host()))
    .system_prompt(pb.build())
    .build();
# }

To preview the built-in guidelines and their static cost, run the system_prompt_preview example (and its variants).

The system prompt is auto-optimized: cross-tool references e.g. prefer X tool over Y for Z are ommitted unless all tools are present. Currently uses ~2000 tokens for full toolset, ~560 tokens for search-only.

Custom tool registry

Core provides framework-agnostic plumbing for user-defined tools:

Adapters wrap the CustomTool from ToolFactory::create in their framework's native tool trait, keeping core framework-free and enabling reuse across adapters.

Permissions

permissions provides ordered allow/deny rules for tool access and delegation.

  • Rule stores (permission_key, subject_pattern, action).
  • Ruleset uses last-match-wins; no match defaults to PermissionAction::Deny.
  • Both fields support patterns:
    • * means any number of characters (including none)
    • ? means exactly one character
    • Permission keys (exact or wildcard): bash, task, webfetch-*
    • Subject patterns (wildcard only): *, orchestrator-*, agent-?

Frontmatter-style config is typically translated into this model:

permission:
  bash: allow              # → ("bash", "*", allow)
  task:
    orchestrator-*: allow  # → ("task", "orchestrator-*", allow)
    "*": deny              # → ("task", "*", deny)

With last-match-wins, the final "*": deny rule overrides earlier task matches.

use reloaded_code_core::permissions::{ExpandError, PermissionAction, Rule, Ruleset};

# fn main() -> Result<(), ExpandError> {
let mut rules = Ruleset::new();
rules.push(Rule::new("bash", "*", PermissionAction::Allow)?);
rules.push(Rule::new("task", "*", PermissionAction::Deny)?);               // catch-all
rules.push(Rule::new("task", "orchestrator-*", PermissionAction::Allow)?); // specific (last match wins)

assert_eq!(rules.evaluate("bash", "any-agent"), PermissionAction::Allow);
assert_eq!(rules.evaluate("task", "orchestrator-review"), PermissionAction::Allow); // last-match-wins
assert_eq!(rules.evaluate("task", "other-agent"), PermissionAction::Deny); // no match, defaults to deny
# Ok(())
# }

Credentials

CredentialResolver looks up named credentials (like API keys) from overrides or environment variables.

use reloaded_code_core::{CredentialLookup, CredentialResolver};

let mut resolver = CredentialResolver::new();
resolver.set_override("OPENAI_API_KEY", "sk-override");
let key = resolver.resolve("OPENAI_API_KEY");