diff --git a/.github/workflows/tidy.yml b/.github/workflows/tidy.yml
new file mode 100644
index 00000000..8068192b
--- /dev/null
+++ b/.github/workflows/tidy.yml
@@ -0,0 +1,36 @@
+name: Tidy
+
+# Applies rust-llm-tidy fixes to PR-changed .rs/.md files in place and, on a
+# pull request, commits + pushes them back to the PR branch (mode: apply).
+
+on:
+ pull_request:
+ branches: [main]
+ paths:
+ - "src/**"
+ - "**/*.md"
+ - "**/*.MD"
+ workflow_dispatch:
+
+jobs:
+ tidy:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write # apply: commit + push fixes to the PR branch
+ pull-requests: write # post a report comment on success/failure
+ steps:
+ - uses: actions/checkout@v7
+ with:
+ # Check out the PR branch head, not the auto-generated merge ref:
+ # apply-mode commits on top of HEAD and pushes back to this branch,
+ # which would be a non-fast-forward from the merge commit.
+ ref: ${{ github.head_ref }}
+ # Full history so the PR base diff is resolvable for changed-files.
+ fetch-depth: 0
+
+ - name: Apply tidy fixes to changed files
+ uses: Sewer56/rust-llm-tidy-action@v1
+ with:
+ rust-project-path: "."
+ changed-files: "true"
+ mode: "apply"
diff --git a/README.MD b/README.MD
index 5da37b76..737775c8 100644
--- a/README.MD
+++ b/README.MD
@@ -17,7 +17,7 @@
ReloadedCode started as "an OpenCode for servers." Headless,
sandboxed, and cheap to host for non-commercial use.
-[OpenCode](https://opencode.ai) is a great interactive coding agent, but it's a
+[OpenCode] is a great interactive coding agent, but it's a
~305 MiB TypeScript application that runs as a separate process.
What if you need those same tools for a **server**? A **Discord bot**?
A **CI pipeline**? A **custom product**?
@@ -36,7 +36,7 @@ Shell sandboxing. Default-deny permissions. ~10 MiB footprint.
## Features
- **10 Built-in tools** - read, write, edit, glob, grep, bash, webfetch, todoread, todowrite, task
-- **Agents similar to [OpenCode](https://opencode.ai)** - load agent markdown files with YAML frontmatter
+- **Agents similar to [OpenCode]** - load agent markdown files with YAML frontmatter
- **Multi-agent delegation** - orchestrator pattern with depth-limited task chains
- **Linux sandboxing** - bubblewrap profiles for shell isolation (Public Bot + Trusted Maintenance)
- **Path security** - restrict file access with allowed directories and glob-based rules
@@ -55,7 +55,7 @@ Shell sandboxing. Default-deny permissions. ~10 MiB footprint.
reloaded-code-serdesai = "0.2"
```
-**1.** Create an agent file (markdown + YAML frontmatter similar to [OpenCode](https://opencode.ai)):
+**1.** Create an agent file (markdown + YAML frontmatter similar to [OpenCode]):
```markdown
---
@@ -114,14 +114,14 @@ async fn main() -> Result<(), Box> {
## Crate Map
-| Crate | Version | Description |
-| ------------------------------------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------ |
-| [**reloaded-code-core**](./src/reloaded-code-core/) | 0.2 | Framework-agnostic tool implementations, path resolvers, permissions, custom tool registry |
-| [**reloaded-code-agents**](./src/reloaded-code-agents/) | 0.1 | agent markdown loader similar to [OpenCode](https://opencode.ai), typed catalog, runtime builder |
-| [**reloaded-code-serdesai**](./src/reloaded-code-serdesai/) | 0.2 | SerdesAI framework integration, tool adapters, 15 provider bridges, task delegation |
-| [**reloaded-code-bubblewrap**](./src/reloaded-code-bubblewrap/) | 0.1 | Linux bubblewrap sandbox profiles (Public Bot + Trusted Maintenance) |
-| [**reloaded-code-models-dev**](./src/reloaded-code-models-dev/) | 0.1 | models.dev catalog sync with ETag caching and offline fallback |
-| [**reloaded-code-provider-config**](./src/reloaded-code-provider-config/) | 0.1 | Provider configuration loading and provider catalog overrides |
+| Crate | Version | Description |
+| ------------------------------------------------------------------------- | ------- | ------------------------------------------------------------------------------------------ |
+| [**reloaded-code-core**](./src/reloaded-code-core/) | 0.2 | Framework-agnostic tool implementations, path resolvers, permissions, custom tool registry |
+| [**reloaded-code-agents**](./src/reloaded-code-agents/) | 0.1 | agent markdown loader similar to [OpenCode], typed catalog, runtime builder |
+| [**reloaded-code-serdesai**](./src/reloaded-code-serdesai/) | 0.2 | SerdesAI framework integration, tool adapters, 15 provider bridges, task delegation |
+| [**reloaded-code-bubblewrap**](./src/reloaded-code-bubblewrap/) | 0.1 | Linux bubblewrap sandbox profiles (Public Bot + Trusted Maintenance) |
+| [**reloaded-code-models-dev**](./src/reloaded-code-models-dev/) | 0.1 | models.dev catalog sync with ETag caching and offline fallback |
+| [**reloaded-code-provider-config**](./src/reloaded-code-provider-config/) | 0.1 | Provider configuration loading and provider catalog overrides |
## Examples
@@ -161,3 +161,4 @@ our guidelines.
## License
Licensed under [Apache 2.0](./LICENSE).
+[OpenCode]: https://opencode.ai
diff --git a/src/docs/src/comparison.md b/src/docs/src/comparison.md
index 0c14c75b..aa696221 100644
--- a/src/docs/src/comparison.md
+++ b/src/docs/src/comparison.md
@@ -41,7 +41,7 @@ reloaded-code is for embedding agent tools into your own applications.
(`name`, `mode`, `description`, `model`, `permission`, `tool_settings`).
Agent files written for [OpenCode] are drop-in compatible (add
explicit permissions). See [Agents](agents.md) for the full format
- reference and [Migrating from OpenCode](migration.md) for the differences.
+ reference and [Migrating from OpenCode] for the differences.
- **Core tools** - both provide `read`, `write`, `edit`, `glob`, `grep`,
`bash`, and `webfetch`. See [Tools](tools.md) for the complete tool
@@ -65,7 +65,7 @@ reloaded-code uses **default-deny**: every tool is blocked unless you
explicitly allow it in the agent frontmatter. There is no interactive approval
flow because there is no user to prompt - the agent runs unattended.
-See [Migrating from OpenCode](migration.md) for a side-by-side YAML example,
+See [Migrating from OpenCode] for a side-by-side YAML example,
a [portable default-deny configuration](migration.md#portable-default-deny),
and a migration checklist.
@@ -123,7 +123,7 @@ configuration. See [Sandboxing](sandboxing.md) for the full guide.
---
Ready to get started? See [Getting Started](getting-started.md) or
-[Migrating from OpenCode](migration.md).
+[Migrating from OpenCode].
[OpenCode]: https://opencode.ai/
[SerdesAI]: https://crates.io/crates/serdes-ai
@@ -131,3 +131,4 @@ Ready to get started? See [Getting Started](getting-started.md) or
[bubblewrap]: https://github.com/containers/bubblewrap
[Bun]: https://bun.sh
[tokio]: https://tokio.rs
+[Migrating from OpenCode]: migration.md
diff --git a/src/docs/src/getting-started.md b/src/docs/src/getting-started.md
index 7f02fbf1..b95bf822 100644
--- a/src/docs/src/getting-started.md
+++ b/src/docs/src/getting-started.md
@@ -153,7 +153,7 @@ a Rust project and an LLM API key (e.g. `OPENAI_API_KEY`).
[serdesai-basic](https://github.com/Reloaded-Project/ReloadedCode/blob/main/src/reloaded-code-serdesai/examples/serdesai-basic.rs)
(without agent files) and
[serdesai-agents](https://github.com/Reloaded-Project/ReloadedCode/blob/main/src/reloaded-code-serdesai/examples/serdesai-agents.rs)
- (with agent files). See [Examples](examples.md) for the full list.
+ (with agent files). See [Examples] for the full list.
## Custom tools
@@ -224,7 +224,7 @@ cargo run --example serdesai-agents -p reloaded-code-serdesai
cargo run --example serdesai-task -p reloaded-code-serdesai
```
-See [Examples](examples.md) for the full list with descriptions and
+See [Examples] for the full list with descriptions and
source links.
## Sandboxing for production
@@ -242,10 +242,10 @@ to isolate shell execution. See [Sandboxing](sandboxing.md) for the full guide.
### Common deployment profiles
- **Discord bot / chat bot** - Use the Public Bot sandbox profile
- (restrictive; see [Sandboxing](sandboxing.md#the-two-profiles)) and
+ (restrictive; see [Sandboxing]) and
`AllowedPathResolver` to limit what the LLM can do with user-provided prompts.
- **CI/CD pipeline** - Use the Trusted Maintenance profile
- (permissive; see [Sandboxing](sandboxing.md#the-two-profiles)) for build jobs
+ (permissive; see [Sandboxing]) for build jobs
where you control the inputs. Explicitly mount the cache directories so that
build artifacts persist between runs.
@@ -274,3 +274,5 @@ reloaded-code-core = { version = "0.2", default-features = false, features = ["b
[`ToolContext`]: https://docs.rs/reloaded-code-core/latest/reloaded_code_core/trait.ToolContext.html
[`CustomTool`]: https://docs.rs/reloaded-code-core/latest/reloaded_code_core/trait.CustomTool.html
[`ToolFactory`]: https://docs.rs/reloaded-code-core/latest/reloaded_code_core/trait.ToolFactory.html
+[Examples]: examples.md
+[Sandboxing]: sandboxing.md#the-two-profiles
diff --git a/src/docs/src/guides/custom-providers.md b/src/docs/src/guides/custom-providers.md
index 0c6323f9..c2d53811 100644
--- a/src/docs/src/guides/custom-providers.md
+++ b/src/docs/src/guides/custom-providers.md
@@ -50,12 +50,12 @@ Each provider must include at least one model under `models`.
### Provider fields
-| Field | Type | Default | Notes |
-| ------------ | ----------- | ------------------- | ------------------------------------- |
-| `api_url` | string | required | Base URL for the API endpoint |
-| `api_type` | string | `openai-compatible` | Maps to provider behaviour profile |
-| `env` | string list | `[]` | Env var names checked for credentials |
-| `models` | map | required | Models offered by this provider |
+| Field | Type | Default | Notes |
+| ---------- | ----------- | ------------------- | ------------------------------------- |
+| `api_url` | string | required | Base URL for the API endpoint |
+| `api_type` | string | `openai-compatible` | Maps to provider behaviour profile |
+| `env` | string list | `[]` | Env var names checked for credentials |
+| `models` | map | required | Models offered by this provider |
### api_type values
diff --git a/src/docs/src/models-catalog.md b/src/docs/src/models-catalog.md
index ee996d6a..06e8d421 100644
--- a/src/docs/src/models-catalog.md
+++ b/src/docs/src/models-catalog.md
@@ -82,8 +82,8 @@ let result = ModelsDevCatalog::load_at(&cache_path).await?;
**Location** (platform default):
-| Platform | Path |
-| -------- | --------------------------------------------------------------- |
+| Platform | Path |
+| -------- | ------------------------------------------------------------ |
| Linux | `~/.cache/reloaded-code/models.dev.catalog.v1.cache` |
| macOS | `~/Library/Caches/reloaded-code/models.dev.catalog.v1.cache` |
| Windows | `%LOCALAPPDATA%\reloaded-code\models.dev.catalog.v1.cache` |
diff --git a/src/docs/src/sandboxing.md b/src/docs/src/sandboxing.md
index 69277363..8cdf1e05 100644
--- a/src/docs/src/sandboxing.md
+++ b/src/docs/src/sandboxing.md
@@ -31,7 +31,7 @@ protection:
Built on [bubblewrap](https://github.com/containers/bubblewrap), a lightweight
sandboxing tool that uses Linux kernel namespaces.
-- **Feature flag**: `linux-bubblewrap` (see [Feature Flags](feature-flags.md))
+- **Feature flag**: `linux-bubblewrap` (see [Feature Flags])
- **Requirement**: Linux host with `bwrap` installed
The sandbox never silently falls back to host execution. If `bwrap` is missing
@@ -48,7 +48,7 @@ reloaded-code-serdesai = { version = "0.2", features = ["linux-bubblewrap"] }
```
*(Also shown in [Getting Started](getting-started.md) and
-[Feature Flags](feature-flags.md).)*
+[Feature Flags].)*
When you enable sandboxing, start with the **Public Bot** profile.
@@ -262,16 +262,16 @@ full mount table, environment variables, and design rationale.
### Quick comparison
-| Aspect | Public Bot | Trusted Maintenance |
-| ------------------ | --------------------------------- | ----------------------------------------- |
-| Use case | Untrusted / hostile input | Trusted automation |
-| Network | Disabled | Enabled |
-| Host filesystem | Minimal (bins, libs, workspace) | Full `/` read-only |
-| Writable paths | Workspace, synthetic home, `/tmp` | Workspace, synthetic home, cache, `/tmp` |
-| `/etc` visible | No | Yes (except `/etc/shadow`) |
+| Aspect | Public Bot | Trusted Maintenance |
+| ------------------ | --------------------------------- | ------------------------------------------------------------- |
+| Use case | Untrusted / hostile input | Trusted automation |
+| Network | Disabled | Enabled |
+| Host filesystem | Minimal (bins, libs, workspace) | Full `/` read-only |
+| Writable paths | Workspace, synthetic home, `/tmp` | Workspace, synthetic home, cache, `/tmp` |
+| `/etc` visible | No | Yes (except `/etc/shadow`) |
| Environment | Cleared, minimal sanitized `PATH` | Cleared, sanitized host `PATH` + XDG Base Directory variables |
-| Credential mounts | Not supported | Supported (validated) |
-| Safe for untrusted | **Yes** | **No** |
+| Credential mounts | Not supported | Supported (validated) |
+| Safe for untrusted | **Yes** | **No** |
### Under the hood
@@ -328,3 +328,4 @@ and design rationale, see [Profile Reference](extra-sandboxing-notes.md).
[with_linux_bwrap]: https://docs.rs/reloaded-code-serdesai/latest/reloaded_code_serdesai/struct.BashTool.html#method.with_linux_bwrap
[new_with_temp_sandbox]: https://docs.rs/reloaded-code-serdesai/latest/reloaded_code_serdesai/struct.AgentBuildContext.html#method.new_with_temp_sandbox
[Preset]: https://docs.rs/reloaded-code-bubblewrap/latest/reloaded_code_bubblewrap/profile/enum.Preset.html
+[Feature Flags]: feature-flags.md
diff --git a/src/docs/src/tools.md b/src/docs/src/tools.md
index 0623b5c4..c5a9ab85 100644
--- a/src/docs/src/tools.md
+++ b/src/docs/src/tools.md
@@ -218,7 +218,7 @@ Reads a file, optionally with line numbers and a windowed range.
**Output:** Line-numbered file content. Lines beyond `max_line_length` are
truncated with `...`.
-**Configurable via [tool settings](#tool-settings):** `line_numbers`, `limit`,
+**Configurable via [tool settings]:** `line_numbers`, `limit`,
`max_line_length`
### write
@@ -269,7 +269,7 @@ crate for fast traversal.
**Output:** List of matching file paths.
-**Configurable via [tool settings](#tool-settings):** `limit`
+**Configurable via [tool settings]:** `limit`
### grep
@@ -285,7 +285,7 @@ Searches file contents by regex pattern. Returns matching lines with metadata.
**Output:** Matching lines with line numbers and file paths.
-**Configurable via [tool settings](#tool-settings):** `line_numbers`, `limit`,
+**Configurable via [tool settings]:** `line_numbers`, `limit`,
`max_line_length`
### bash
@@ -303,10 +303,10 @@ Executes a shell command with timeout and captured output.
**Output:** Combined stdout and stderr. Non-zero exit codes are included in
the output.
-**Configurable via [tool settings](#tool-settings):** `timeout_ms`, `max_timeout_ms`
+**Configurable via [tool settings]:** `timeout_ms`, `max_timeout_ms`
**Sandboxing:** On Linux, you can enable the `linux-bubblewrap` feature to run
-commands inside a [bubblewrap] sandbox. See [Sandboxing](sandboxing.md) for details.
+commands inside a [bubblewrap] sandbox. See [Sandboxing] for details.
### webfetch
@@ -322,7 +322,7 @@ to markdown.
**Output:** Page content as text or markdown.
-**Configurable via [tool settings](#tool-settings):** `timeout_ms`,
+**Configurable via [tool settings]:** `timeout_ms`,
`max_timeout_ms`, `max_response_size`
### todoread / todowrite
@@ -501,9 +501,11 @@ This controls which paths the tools can access:
Agents use `AllowedGlobResolver` by default. If you don't need glob-based rules,
`AllowedPathResolver` or `AbsolutePathResolver` are slightly faster.
-For a deeper dive into path security, see [Sandboxing](sandboxing.md).
+For a deeper dive into path security, see [Sandboxing].
[bubblewrap]: https://github.com/containers/bubblewrap
[create_todo_tools]: https://docs.rs/reloaded-code-serdesai/latest/reloaded_code_serdesai/tools/todo/fn.create_todo_tools.html
[reloaded-code-core]: https://docs.rs/reloaded-code-core
[reloaded-code-serdesai]: https://docs.rs/reloaded-code-serdesai
+[tool settings]: #tool-settings
+[Sandboxing]: sandboxing.md
diff --git a/src/reloaded-code-agents/benches/fixtures/orchestrator-quality-gate-gpt5.md b/src/reloaded-code-agents/benches/fixtures/orchestrator-quality-gate-gpt5.md
index 6d1f4294..5e2e20e6 100644
--- a/src/reloaded-code-agents/benches/fixtures/orchestrator-quality-gate-gpt5.md
+++ b/src/reloaded-code-agents/benches/fixtures/orchestrator-quality-gate-gpt5.md
@@ -119,9 +119,9 @@ Description of issue
Detailed explanation of the problem and why it matters
**Impact:** What could go wrong
**Fix:**
-```lang
+~~~lang
// replacement code if applicable
-```
+~~~
## Test Issues
[basic|no] - [PASS|FAIL|FORBIDDEN_TESTS_FOUND]
diff --git a/src/reloaded-code-agents/benches/parser.rs b/src/reloaded-code-agents/benches/parser.rs
index 389c1b64..f864a683 100644
--- a/src/reloaded-code-agents/benches/parser.rs
+++ b/src/reloaded-code-agents/benches/parser.rs
@@ -1,18 +1,13 @@
//! Benchmarks for agent parsing.
+criterion_group!(benches, benchmark_parse_frontmatter);
+
+criterion_main!(benches);
+
use core::hint::black_box;
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
use reloaded_code_agents::{AgentCatalog, AgentLoader};
-/// Loads a real agent fixture file at runtime.
-fn load_fixture() -> String {
- std::fs::read_to_string(concat!(
- env!("CARGO_MANIFEST_DIR"),
- "/benches/fixtures/orchestrator-quality-gate-gpt5.md"
- ))
- .expect("failed to load fixture file")
-}
-
fn benchmark_parse_frontmatter(c: &mut Criterion) {
let real_lf = load_fixture();
let real_crlf = real_lf.replace('\n', "\r\n");
@@ -38,5 +33,11 @@ fn benchmark_parse_frontmatter(c: &mut Criterion) {
group.finish();
}
-criterion_group!(benches, benchmark_parse_frontmatter);
-criterion_main!(benches);
+/// Loads a real agent fixture file at runtime.
+fn load_fixture() -> String {
+ std::fs::read_to_string(concat!(
+ env!("CARGO_MANIFEST_DIR"),
+ "/benches/fixtures/orchestrator-quality-gate-gpt5.md"
+ ))
+ .expect("failed to load fixture file")
+}
diff --git a/src/reloaded-code-agents/benches/runtime_task.rs b/src/reloaded-code-agents/benches/runtime_task.rs
index 7a3fb06a..7d9479b0 100644
--- a/src/reloaded-code-agents/benches/runtime_task.rs
+++ b/src/reloaded-code-agents/benches/runtime_task.rs
@@ -4,6 +4,10 @@
//! [`AgentRuntime::summarize_callable_targets`], and
//! [`AgentRuntime::can_delegate_to`] across varying agent counts.
+criterion_group!(benches, bench_runtime_task_caches);
+
+criterion_main!(benches);
+
use ahash::AHashMap;
use core::hint::black_box;
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
@@ -14,46 +18,46 @@ use reloaded_code_agents::{
use reloaded_code_core::permissions::PermissionAction;
use reloaded_code_core::tool_metadata::{read as read_meta, task as task_meta};
-/// Build a minimal [`AgentConfig`] for benchmark fixtures.
+/// Benchmark cached delegation queries against runtimes of 16, 64, and 256 agents.
///
-/// `permission` controls tool-access rules; all other fields are filled
-/// with placeholder values suitable for performance measurement only.
-fn build_agent(
- name: &str,
- mode: AgentMode,
- permission: IndexMap,
-) -> AgentConfig {
- AgentConfig {
- name: name.into(),
- mode,
- description: format!("{name} description").into(),
- model: None,
- hidden: false,
- temperature: None,
- top_p: None,
- permission,
- options: AHashMap::new(),
- tool_settings: AgentToolSettings::default(),
- prompt: Default::default(),
- }
-}
+/// Measures four operations:
+/// - **allowed_tools** – full tool-set resolution for the `caller` agent.
+/// - **summaries** – callable-target summary strings for `caller`.
+/// - **can_delegate_hit** – pattern-match hit (`caller` → `review-003`).
+/// - **can_delegate_miss** – pattern-match miss (`caller` → `misc-002`).
+fn bench_runtime_task_caches(c: &mut Criterion) {
+ let mut group = c.benchmark_group("runtime/task_caches");
-/// Create a permission map that denies all tools by default, but allows
-/// pattern-matched delegation to agents named `review-*` or `worker-*`
-/// via the task tool, and blanket-allows the read tool.
-fn patterned_task_permission() -> IndexMap {
- let mut patterns = IndexMap::new();
- patterns.insert("*".to_string(), PermissionAction::Deny);
- patterns.insert("review-*".to_string(), PermissionAction::Allow);
- patterns.insert("worker-*".to_string(), PermissionAction::Allow);
+ for agent_count in [16_usize, 64, 256] {
+ let runtime = build_runtime(agent_count);
+ group.throughput(Throughput::Elements(1));
- IndexMap::from([
- (task_meta::NAME.into(), PermissionRule::Pattern(patterns)),
- (
- read_meta::NAME.into(),
- PermissionRule::Action(PermissionAction::Allow),
- ),
- ])
+ group.bench_with_input(
+ BenchmarkId::new("allowed_tools", agent_count),
+ &runtime,
+ |b, runtime| b.iter(|| black_box(runtime.allowed_tools("caller"))),
+ );
+
+ group.bench_with_input(
+ BenchmarkId::new("summaries", agent_count),
+ &runtime,
+ |b, runtime| b.iter(|| black_box(runtime.summarize_callable_targets("caller"))),
+ );
+
+ group.bench_with_input(
+ BenchmarkId::new("can_delegate_hit", agent_count),
+ &runtime,
+ |b, runtime| b.iter(|| black_box(runtime.can_delegate_to("caller", "review-003"))),
+ );
+
+ group.bench_with_input(
+ BenchmarkId::new("can_delegate_miss", agent_count),
+ &runtime,
+ |b, runtime| b.iter(|| black_box(runtime.can_delegate_to("caller", "misc-002"))),
+ );
+ }
+
+ group.finish();
}
/// Build an [`AgentRuntime`] with one `caller` primary agent and
@@ -90,47 +94,44 @@ fn build_runtime(agent_count: usize) -> reloaded_code_agents::AgentRuntime {
.expect("benchmark fixture should not fail pattern expansion")
}
-/// Benchmark cached delegation queries against runtimes of 16, 64, and 256 agents.
+/// Build a minimal [`AgentConfig`] for benchmark fixtures.
///
-/// Measures four operations:
-/// - **allowed_tools** – full tool-set resolution for the `caller` agent.
-/// - **summaries** – callable-target summary strings for `caller`.
-/// - **can_delegate_hit** – pattern-match hit (`caller` → `review-003`).
-/// - **can_delegate_miss** – pattern-match miss (`caller` → `misc-002`).
-fn bench_runtime_task_caches(c: &mut Criterion) {
- let mut group = c.benchmark_group("runtime/task_caches");
-
- for agent_count in [16_usize, 64, 256] {
- let runtime = build_runtime(agent_count);
- group.throughput(Throughput::Elements(1));
-
- group.bench_with_input(
- BenchmarkId::new("allowed_tools", agent_count),
- &runtime,
- |b, runtime| b.iter(|| black_box(runtime.allowed_tools("caller"))),
- );
-
- group.bench_with_input(
- BenchmarkId::new("summaries", agent_count),
- &runtime,
- |b, runtime| b.iter(|| black_box(runtime.summarize_callable_targets("caller"))),
- );
-
- group.bench_with_input(
- BenchmarkId::new("can_delegate_hit", agent_count),
- &runtime,
- |b, runtime| b.iter(|| black_box(runtime.can_delegate_to("caller", "review-003"))),
- );
-
- group.bench_with_input(
- BenchmarkId::new("can_delegate_miss", agent_count),
- &runtime,
- |b, runtime| b.iter(|| black_box(runtime.can_delegate_to("caller", "misc-002"))),
- );
+/// `permission` controls tool-access rules; all other fields are filled
+/// with placeholder values suitable for performance measurement only.
+fn build_agent(
+ name: &str,
+ mode: AgentMode,
+ permission: IndexMap,
+) -> AgentConfig {
+ AgentConfig {
+ name: name.into(),
+ mode,
+ description: format!("{name} description").into(),
+ model: None,
+ hidden: false,
+ temperature: None,
+ top_p: None,
+ permission,
+ options: AHashMap::new(),
+ tool_settings: AgentToolSettings::default(),
+ prompt: Default::default(),
}
-
- group.finish();
}
-criterion_group!(benches, bench_runtime_task_caches);
-criterion_main!(benches);
+/// Create a permission map that denies all tools by default, but allows
+/// pattern-matched delegation to agents named `review-*` or `worker-*`
+/// via the task tool, and blanket-allows the read tool.
+fn patterned_task_permission() -> IndexMap {
+ let mut patterns = IndexMap::new();
+ patterns.insert("*".to_string(), PermissionAction::Deny);
+ patterns.insert("review-*".to_string(), PermissionAction::Allow);
+ patterns.insert("worker-*".to_string(), PermissionAction::Allow);
+
+ IndexMap::from([
+ (task_meta::NAME.into(), PermissionRule::Pattern(patterns)),
+ (
+ read_meta::NAME.into(),
+ PermissionRule::Action(PermissionAction::Allow),
+ ),
+ ])
+}
diff --git a/src/reloaded-code-agents/src/lib.rs b/src/reloaded-code-agents/src/lib.rs
index 9227bdbf..a77dc93d 100644
--- a/src/reloaded-code-agents/src/lib.rs
+++ b/src/reloaded-code-agents/src/lib.rs
@@ -1,15 +1,5 @@
#![doc = include_str!(concat!("../", env!("CARGO_PKG_README")))]
-mod catalog;
-mod extensions;
-mod loader;
-mod parser;
-mod path;
-mod runtime;
-#[cfg(test)]
-mod test_helpers;
-mod types;
-
pub use catalog::AgentCatalog;
pub use extensions::RulesetExt;
pub use loader::AgentLoader;
@@ -25,3 +15,13 @@ pub use types::{
BashToolSettings, GlobToolSettings, GrepToolSettings, PermissionRule, ReadToolSettings,
WebFetchToolSettings,
};
+
+mod catalog;
+mod extensions;
+mod loader;
+mod parser;
+mod path;
+mod runtime;
+#[cfg(test)]
+mod test_helpers;
+mod types;
diff --git a/src/reloaded-code-agents/src/loader.rs b/src/reloaded-code-agents/src/loader.rs
index 9f3dddbe..383710c0 100644
--- a/src/reloaded-code-agents/src/loader.rs
+++ b/src/reloaded-code-agents/src/loader.rs
@@ -269,6 +269,29 @@ impl AgentLoader {
}
}
+/// Strict parser for catalog-only string loading (validates non-empty name).
+fn config_from_str_strict(
+ markdown: impl Into,
+ default_name: impl Into>,
+) -> AgentLoadResult {
+ let config = parse_agent_config(markdown.into(), default_name)
+ .map_err(|err| map_parse_error(None, err))?;
+ if config.name.is_empty() {
+ return Err(AgentLoadError::schema_validation(
+ None,
+ "agent name is empty",
+ ));
+ }
+ Ok(config)
+}
+
+/// Loads a single agent configuration from a file.
+fn load_agent_file(path: &Path, name: impl Into>) -> AgentLoadResult {
+ let content =
+ fs::read_to_string(path).map_err(|e| AgentLoadError::io(Some(path.to_path_buf()), e))?;
+ parse_agent_config(content, name).map_err(|err| map_parse_error(Some(path.to_path_buf()), err))
+}
+
/// Shared directory scan helper used by catalog loading.
fn load_directory_with(
dir: &Path,
@@ -331,58 +354,6 @@ fn load_directory_with(
Ok(())
}
-/// Shared parse helper that reuses existing loader parsing.
-fn parse_agent_config(
- content: String,
- default_name: impl Into>,
-) -> Result {
- let result = parse_agent::(content)?;
- Ok(AgentConfig::from_raw(
- default_name,
- result.data,
- result.content,
- ))
-}
-
-fn map_parse_error(path: Option, err: AgentParseError) -> AgentLoadError {
- match err {
- AgentParseError::SchemaValidation { message } => {
- AgentLoadError::schema_validation(path, message)
- }
- other => AgentLoadError::parse(path, other),
- }
-}
-
-/// Loads a single agent configuration from a file.
-fn load_agent_file(path: &Path, name: impl Into>) -> AgentLoadResult {
- let content =
- fs::read_to_string(path).map_err(|e| AgentLoadError::io(Some(path.to_path_buf()), e))?;
- parse_agent_config(content, name).map_err(|err| map_parse_error(Some(path.to_path_buf()), err))
-}
-
-/// Strict parser for catalog-only string loading (validates non-empty name).
-fn config_from_str_strict(
- markdown: impl Into,
- default_name: impl Into>,
-) -> AgentLoadResult {
- let config = parse_agent_config(markdown.into(), default_name)
- .map_err(|err| map_parse_error(None, err))?;
- if config.name.is_empty() {
- return Err(AgentLoadError::schema_validation(
- None,
- "agent name is empty",
- ));
- }
- Ok(config)
-}
-
-/// Checks if a relative path matches `agent/**/*.md` or `agents/**/*.md`.
-fn matches_agent_pattern(rel_path: &str) -> bool {
- let is_agent_dir = rel_path.starts_with("agent/") || rel_path.starts_with("agents/");
- let is_md_file = rel_path.ends_with(".md");
- is_agent_dir && is_md_file
-}
-
/// Derives agent name from relative path.
///
/// Strips leading `agent/` or `agents/` segment and `.md` extension.
@@ -409,6 +380,35 @@ fn derive_agent_name_from_rel(rel_path: &str) -> Option {
}
}
+fn map_parse_error(path: Option, err: AgentParseError) -> AgentLoadError {
+ match err {
+ AgentParseError::SchemaValidation { message } => {
+ AgentLoadError::schema_validation(path, message)
+ }
+ other => AgentLoadError::parse(path, other),
+ }
+}
+
+/// Checks if a relative path matches `agent/**/*.md` or `agents/**/*.md`.
+fn matches_agent_pattern(rel_path: &str) -> bool {
+ let is_agent_dir = rel_path.starts_with("agent/") || rel_path.starts_with("agents/");
+ let is_md_file = rel_path.ends_with(".md");
+ is_agent_dir && is_md_file
+}
+
+/// Shared parse helper that reuses existing loader parsing.
+fn parse_agent_config(
+ content: String,
+ default_name: impl Into>,
+) -> Result {
+ let result = parse_agent::(content)?;
+ Ok(AgentConfig::from_raw(
+ default_name,
+ result.data,
+ result.content,
+ ))
+}
+
#[cfg(test)]
mod tests {
use super::*;
diff --git a/src/reloaded-code-agents/src/parser/mod.rs b/src/reloaded-code-agents/src/parser/mod.rs
index 7a418b7f..f186bd2e 100644
--- a/src/reloaded-code-agents/src/parser/mod.rs
+++ b/src/reloaded-code-agents/src/parser/mod.rs
@@ -17,14 +17,14 @@
//! - Trims leading/trailing ASCII whitespace from the body.
//! - Preprocesses YAML before deserialization (see [`preprocessor`]).
-mod preprocessor;
-
use crlf_to_lf_inplace::crlf_to_lf_inplace;
use preprocessor::preprocess_frontmatter_yaml;
use serde::de::DeserializeOwned;
use serde_yaml::Value;
use thiserror::Error;
+mod preprocessor;
+
/// Parser error variants independent of file paths.
#[derive(Debug, Error)]
pub enum AgentParseError {
@@ -57,6 +57,13 @@ pub(crate) struct AgentParseResult {
pub(crate) content: String,
}
+#[derive(Clone, Copy)]
+struct FrontmatterOffsets {
+ yaml_start: usize,
+ yaml_end: usize,
+ body_start: usize,
+}
+
/// Path-free agent parsing function.
pub(crate) fn parse_agent(
mut content: String,
@@ -91,68 +98,50 @@ pub(crate) fn parse_agent(
})
}
-/// Validates frontmatter is compatible with headless operation.
-///
-/// Rejects features requiring user interaction (e.g., "ask" permissions)
-/// that are unsupported in non-interactive contexts.
-fn validate_headless_compatibility(frontmatter: &Value) -> Result<(), AgentParseError> {
- // Skip if root isn't a mapping
- let Value::Mapping(root) = frontmatter else {
- return Ok(());
- };
+/// Extracts the body by mutating the original string in-place.
+/// Reuses the existing allocation and leaves only the trimmed body.
+#[inline]
+fn extract_body_inplace(mut content: String, body_start: usize) -> String {
+ if body_start >= content.len() {
+ content.clear();
+ return content;
+ }
- let permission_key = Value::String("permission".to_string());
- let task_key = Value::String("task".to_string());
+ let len = content.len();
+ let bytes = content.as_bytes();
+ let mut start_offset = body_start;
+ let mut end_offset = len;
- // Extract permission.task for validation
- //
- // ```yaml
- // permission:
- // task: # e.g., "allow", "deny", or "ask"
- // ```
- //
- // or:
- //
- // ```yaml
- // permission:
- // task:
- // : # e.g., "*": "ask"
- // ```
- //
- // See `PermissionRule` for the target type.
- let Some(Value::Mapping(permission_map)) = root.get(&permission_key) else {
- return Ok(());
- };
- let Some(task_rule) = permission_map.get(&task_key) else {
- return Ok(());
- };
+ // UTF-8 byte classes:
+ // | Range | Meaning | `is_ascii_whitespace()` |
+ // |-------------|--------------------------|--------------------------|
+ // | `0x00..=7F` | ASCII / single-byte UTF-8| can be true |
+ // | `0x80..=BF` | UTF-8 continuation byte | always false |
+ // | `0xC2..=F4` | UTF-8 leading byte | always false |
+ // Therefore ASCII byte-wise trimming cannot cut through a multibyte code point.
+ while start_offset < len && bytes[start_offset].is_ascii_whitespace() {
+ start_offset += 1;
+ }
+ while end_offset > start_offset && bytes[end_offset - 1].is_ascii_whitespace() {
+ end_offset -= 1;
+ }
- // Reject "ask" - requires interactive user confirmation
- if task_rule_contains_ask(task_rule) {
- return Err(AgentParseError::SchemaValidation {
- message: "permission.task: ask is unsupported; use allow or deny".to_string(),
- });
+ debug_assert!(content.is_char_boundary(body_start));
+ debug_assert!(content.is_char_boundary(start_offset));
+ debug_assert!(content.is_char_boundary(end_offset));
+
+ let body_len = end_offset - start_offset;
+ if start_offset == 0 && body_len == len {
+ return content;
}
- Ok(())
-}
-fn task_rule_contains_ask(rule: &Value) -> bool {
- match rule {
- // Scalar: `task: ask`
- Value::String(action) => action.eq_ignore_ascii_case("ask"),
- // Mapping: `task: "*": ask`
- Value::Mapping(patterns) => patterns.values().any(
- |value| matches!(value, Value::String(action) if action.eq_ignore_ascii_case("ask")),
- ),
- _ => false,
+ unsafe {
+ let vec = content.as_mut_vec();
+ core::ptr::copy(vec.as_ptr().add(start_offset), vec.as_mut_ptr(), body_len);
+ vec.set_len(body_len);
}
-}
-#[derive(Clone, Copy)]
-struct FrontmatterOffsets {
- yaml_start: usize,
- yaml_end: usize,
- body_start: usize,
+ content
}
#[inline]
@@ -199,50 +188,61 @@ fn find_frontmatter_offsets(content: &str) -> Option {
})
}
-/// Extracts the body by mutating the original string in-place.
-/// Reuses the existing allocation and leaves only the trimmed body.
-#[inline]
-fn extract_body_inplace(mut content: String, body_start: usize) -> String {
- if body_start >= content.len() {
- content.clear();
- return content;
- }
-
- let len = content.len();
- let bytes = content.as_bytes();
- let mut start_offset = body_start;
- let mut end_offset = len;
+/// Validates frontmatter is compatible with headless operation.
+///
+/// Rejects features requiring user interaction (e.g., "ask" permissions)
+/// that are unsupported in non-interactive contexts.
+fn validate_headless_compatibility(frontmatter: &Value) -> Result<(), AgentParseError> {
+ // Skip if root isn't a mapping
+ let Value::Mapping(root) = frontmatter else {
+ return Ok(());
+ };
- // UTF-8 byte classes:
- // | Range | Meaning | `is_ascii_whitespace()` |
- // |-------------|--------------------------|--------------------------|
- // | `0x00..=7F` | ASCII / single-byte UTF-8| can be true |
- // | `0x80..=BF` | UTF-8 continuation byte | always false |
- // | `0xC2..=F4` | UTF-8 leading byte | always false |
- // Therefore ASCII byte-wise trimming cannot cut through a multibyte code point.
- while start_offset < len && bytes[start_offset].is_ascii_whitespace() {
- start_offset += 1;
- }
- while end_offset > start_offset && bytes[end_offset - 1].is_ascii_whitespace() {
- end_offset -= 1;
- }
+ let permission_key = Value::String("permission".to_string());
+ let task_key = Value::String("task".to_string());
- debug_assert!(content.is_char_boundary(body_start));
- debug_assert!(content.is_char_boundary(start_offset));
- debug_assert!(content.is_char_boundary(end_offset));
+ // Extract permission.task for validation
+ //
+ // ```yaml
+ // permission:
+ // task: # e.g., "allow", "deny", or "ask"
+ // ```
+ //
+ // or:
+ //
+ // ```yaml
+ // permission:
+ // task:
+ // : # e.g., "*": "ask"
+ // ```
+ //
+ // See `PermissionRule` for the target type.
+ let Some(Value::Mapping(permission_map)) = root.get(&permission_key) else {
+ return Ok(());
+ };
+ let Some(task_rule) = permission_map.get(&task_key) else {
+ return Ok(());
+ };
- let body_len = end_offset - start_offset;
- if start_offset == 0 && body_len == len {
- return content;
+ // Reject "ask" - requires interactive user confirmation
+ if task_rule_contains_ask(task_rule) {
+ return Err(AgentParseError::SchemaValidation {
+ message: "permission.task: ask is unsupported; use allow or deny".to_string(),
+ });
}
+ Ok(())
+}
- unsafe {
- let vec = content.as_mut_vec();
- core::ptr::copy(vec.as_ptr().add(start_offset), vec.as_mut_ptr(), body_len);
- vec.set_len(body_len);
+fn task_rule_contains_ask(rule: &Value) -> bool {
+ match rule {
+ // Scalar: `task: ask`
+ Value::String(action) => action.eq_ignore_ascii_case("ask"),
+ // Mapping: `task: "*": ask`
+ Value::Mapping(patterns) => patterns.values().any(
+ |value| matches!(value, Value::String(action) if action.eq_ignore_ascii_case("ask")),
+ ),
+ _ => false,
}
-
- content
}
#[cfg(test)]
diff --git a/src/reloaded-code-agents/src/parser/preprocessor.rs b/src/reloaded-code-agents/src/parser/preprocessor.rs
index d55e8b3f..495c8b5b 100644
--- a/src/reloaded-code-agents/src/parser/preprocessor.rs
+++ b/src/reloaded-code-agents/src/parser/preprocessor.rs
@@ -51,6 +51,13 @@
use std::borrow::Cow;
+struct FirstBlockScalar<'a> {
+ line_start: usize,
+ rest_start: usize,
+ key: &'a str,
+ value: &'a str,
+}
+
/// Rewrites ambiguous frontmatter values so YAML parsing stays unambiguous.
pub(super) fn preprocess_frontmatter_yaml(input: &str) -> Cow<'_, str> {
if input.is_empty() {
@@ -102,13 +109,6 @@ fn convert_block_scalars(input: &str) -> Option {
Some(out)
}
-struct FirstBlockScalar<'a> {
- line_start: usize,
- rest_start: usize,
- key: &'a str,
- value: &'a str,
-}
-
/// Finds the first line that must be rewritten and returns its offsets.
fn find_first_block_scalar(input: &str) -> Option> {
let input_len = input.len();
diff --git a/src/reloaded-code-agents/src/path/mod.rs b/src/reloaded-code-agents/src/path/mod.rs
index 3caec212..6f604324 100644
--- a/src/reloaded-code-agents/src/path/mod.rs
+++ b/src/reloaded-code-agents/src/path/mod.rs
@@ -3,6 +3,6 @@
//! Re-exports [`FileToolResolver`] and [`build_resolver_for_tool`] from the
//! `resolver` submodule. See that module for optimisation-tier details.
-mod resolver;
-
pub use resolver::{build_resolver_for_tool, FileToolResolver};
+
+mod resolver;
diff --git a/src/reloaded-code-agents/src/path/resolver.rs b/src/reloaded-code-agents/src/path/resolver.rs
index ea7ab2c6..a55d4633 100644
--- a/src/reloaded-code-agents/src/path/resolver.rs
+++ b/src/reloaded-code-agents/src/path/resolver.rs
@@ -6,13 +6,13 @@
//!
//! # Optimisation tiers
//!
-//! | Config pattern | Resolver | Cost |
-//! |------------------------------------|-------------------------------|------------------|
-//! | No config for tool | `AllowedPathResolver([root])` | prefix check |
-//! | `Action(Allow)` | `AllowedPathResolver([root])` | prefix check |
-//! | Pattern `**` with Allow | `AllowedPathResolver([root])` | prefix check |
-//! | `/**` with Allow | `AbsolutePathResolver` | zero |
-//! | Otherwise | `AllowedGlobResolver` | glob matching |
+//! | Config pattern | Resolver | Cost |
+//! | ----------------------- | ----------------------------- | ------------- |
+//! | No config for tool | `AllowedPathResolver([root])` | prefix check |
+//! | `Action(Allow)` | `AllowedPathResolver([root])` | prefix check |
+//! | Pattern `**` with Allow | `AllowedPathResolver([root])` | prefix check |
+//! | `/**` with Allow | `AbsolutePathResolver` | zero |
+//! | Otherwise | `AllowedGlobResolver` | glob matching |
use crate::types::PermissionRule;
use indexmap::IndexMap;
@@ -133,19 +133,20 @@ pub fn build_resolver_for_tool(
}
}
-/// Checks if any pattern is `/**` (unrestricted access to all absolute paths).
-///
-/// Returns `Some(AbsolutePathResolver)` if found, `None` otherwise.
-fn try_globstar_optimisation(
+/// Builds a `GlobPolicy` from a pattern map.
+fn build_glob_policy(
patterns: &IndexMap,
-) -> Result