Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 24 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,13 @@ MCP servers, timeouts, permissions, and agent behavior. LLM settings come from e
"plan_required": true,
"execute_enabled": false
},
"permissions": [
{ "operations": ["read"], "paths": ["./*"], "mode": "allow" }
]
"writable_paths": ["./src/*", "/tmp/out/*"],
"permissions": []
}
```

By default all paths are **read-only**. Only paths listed in `writable_paths` can be written to (by `write_file`, `edit_file`, `execute`). Patterns use glob syntax (`**`, `*`, `?`). The `permissions` array is kept for advanced allow/deny rules and is checked as a fallback when `writable_paths` does not match.

### Timeout Settings

Timeouts can be set in `agent-runner.json` and overridden by CLI flags:
Expand Down Expand Up @@ -137,9 +138,10 @@ API keys can be provided via:
"keep_tokens": 20000,
"trim_tokens": 4000
},
"writable_paths": ["./src/*"],
"permissions": [
{
"operations": ["read"],
"operations": ["write"],
"paths": ["./*"],
"mode": "allow"
}
Expand All @@ -158,10 +160,11 @@ agent-runner --agent-dir <DIR> --prompt <TEXT|FILE> [OPTIONS]
|--------|---------|-------------|
| `--agent-dir` | (required) | Path to agent folder |
| `--prompt` | (required) | Task prompt or path to a text file |
| `--plan-only` | `false` | Generate plan and exit without executing |
| `--plan-only` | `false` | Generate plan (`plan.json`) and exit without executing |
| `--max-iterations` | `50` | Maximum agent loop iterations |
| `--output-dir` | `./agent-output` | Output directory for reports and traces |
| `--working-dir` | `.` | Working directory for filesystem/execute tools |
| `--writable-paths` | (from config) | Comma-separated glob patterns of writable paths (e.g. `./src/*,/tmp/*`) |
| `--tool-timeout` | `120` | Timeout in seconds for each tool call |
| `--run-limit` | `3600` | Maximum total run time in seconds |
| `--verbose` | `false` | Print iteration details to stderr |
Expand All @@ -186,28 +189,37 @@ The agent has these tools available by default:
| `read_file` | Read file contents with line-based pagination |
| `write_file` | Write content to a file (creates parent dirs) |
| `edit_file` | Find-and-replace strings in a file |
| `glob` | Find files matching a glob pattern |
| `grep` | Search file contents with regex |
| `glob` | Find files matching a glob pattern (respects `.gitignore`) |
| `grep` | Search file contents with regex (respects `.gitignore`) |
| `execute` | Run a shell command (when enabled) |
| `read_plan` | Read the structured execution plan (`plan.json`) |
| `update_plan` | Update a plan step's status (`pending`/`in_progress`/`done`/`skipped`) |
| `task_done` | Signal task completion |
| `write_todos` | Update internal todo list |
| `compact_conversation` | Trigger conversation compaction |

### Permissions

Control which tools can access which paths:
By default **all paths are read-only** — `ls`, `read_file`, `glob`, and `grep` work everywhere. Write operations (`write_file`, `edit_file`, `execute`) are only allowed on paths matching `writable_paths`:

```json
{
"writable_paths": ["./src/*", "./tests/*", "/tmp/out/*"]
}
```

Patterns use glob syntax (`**` for recursive, `*` for single-level, `?` for one char). For advanced allow/deny rules, the legacy `permissions` array is still supported as a fallback:

```json
{
"writable_paths": ["./src/*"],
"permissions": [
{ "operations": ["read"], "paths": ["./*"], "mode": "allow" },
{ "operations": ["write"], "paths": ["./src/*"], "mode": "allow" },
{ "operations": ["write"], "paths": ["./secrets/*"], "mode": "deny" }
]
}
```

Operations: `"read"` covers `ls`, `read_file`, `glob`, `grep`. `"write"` covers `write_file`, `edit_file`, `execute`. Paths support `/*` for prefix matching.
`writable_paths` can also be set via the `--writable-paths` CLI flag (comma-separated); CLI values are added on top of config values.

## Output

Expand All @@ -216,7 +228,7 @@ After execution, the output directory contains:
| File | Description |
|------|-------------|
| `run.json` | Detailed run log with per-iteration and per-tool TAT, errors, and exceptions |
| `plan.md` | Generated execution plan |
| `plan.json` | Structured execution plan (steps with status, readable/writable by the agent) |
| `report.json` | Status, token usage, iterations, duration, todos |
| `transcript.json` | Full message history |
| `trace.jsonl` | Structured event log (one JSON object per line) |
Expand Down
106 changes: 100 additions & 6 deletions agent-runner/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion agent-runner/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.12", features = ["json"] }
glob = "0.3"
globset = "0.4"
ignore = "0.4"
regex = "1"
lettre = "0.11"
chrono = "0.4"
Expand Down
52 changes: 49 additions & 3 deletions agent-runner/src/agent/planner.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,25 @@
use crate::provider::{Message, Provider};
use crate::trace::TraceLogger;
use chrono::Utc;
use serde::{Deserialize, Serialize};
use std::sync::Arc;

/// A single step in an execution plan.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlanStep {
pub id: u32,
pub description: String,
pub status: String,
}

/// A structured execution plan, persisted as `plan.json`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Plan {
pub task: String,
pub created_at: String,
pub steps: Vec<PlanStep>,
}

pub struct Planner {
provider: Arc<dyn Provider>,
trace: Arc<TraceLogger>,
Expand Down Expand Up @@ -47,10 +65,38 @@ impl Planner {
Ok(plan)
}

pub fn save_plan(plan: &str, output_dir: &std::path::Path) -> Result<(), String> {
/// Parse the free-form plan text returned by the LLM into a structured `Plan`.
/// Each non-empty line becomes a step with an incrementing id (starting at 1)
/// and a default status of "pending".
pub fn parse_plan(plan_text: &str, task: &str) -> Plan {
let steps: Vec<PlanStep> = plan_text
.lines()
.map(|l| l.trim())
.filter(|l| !l.is_empty())
.enumerate()
.map(|(i, line)| PlanStep {
id: (i + 1) as u32,
description: line.trim_start_matches(|c| c == '-' || c == '*' || c == ' ' || c == '\t')
.trim()
.to_string(),
status: "pending".to_string(),
})
.collect();

Plan {
task: task.to_string(),
created_at: Utc::now().to_rfc3339(),
steps,
}
}

/// Persist the structured plan as `plan.json` in the output directory.
pub fn save_plan_json(plan: &Plan, output_dir: &std::path::Path) -> Result<(), String> {
std::fs::create_dir_all(output_dir)
.map_err(|e| format!("Failed to create output dir: {}", e))?;
std::fs::write(output_dir.join("plan.md"), plan)
.map_err(|e| format!("Failed to write plan: {}", e))
let json = serde_json::to_string_pretty(plan)
.map_err(|e| format!("Failed to serialize plan: {}", e))?;
std::fs::write(output_dir.join("plan.json"), json)
.map_err(|e| format!("Failed to write plan.json: {}", e))
}
}
3 changes: 3 additions & 0 deletions agent-runner/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ pub struct Config {
#[serde(default)]
pub permissions: Vec<FilesystemPermission>,
#[serde(default)]
pub writable_paths: Vec<String>,
#[serde(default)]
pub subagents: Vec<SubAgentConfig>,
#[serde(default)]
pub agent: AgentConfig,
Expand Down Expand Up @@ -220,6 +222,7 @@ impl Default for Config {
mcp_servers: HashMap::new(),
summarization: SummarizationConfig::default(),
permissions: Vec::new(),
writable_paths: Vec::new(),
subagents: Vec::new(),
agent: AgentConfig::default(),
timeouts: TimeoutConfig::default(),
Expand Down
Loading