Connectivity, Tools, Flows & Prompts

This commit is contained in:
Alex Emmet 2026-07-13 22:36:18 +02:00
commit ad42c0e51f
5 changed files with 57 additions and 37 deletions

View file

@ -1,7 +1,7 @@
use serde::{Deserialize, Serialize};
/* ExploreAgent is intentionally limited to read-only tools so it can be
dispatched without approval for safe, unrestricted codebase exploration. */
dispatched without approval for safe, unrestricted codebase exploration. */
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "snake_case")]
pub enum AgentType {
@ -101,17 +101,17 @@ impl AgentPolicy {
max_depth: 3,
require_preview: false,
timeout_seconds: 300,
max_tokens: 16384,
max_tokens: 131072,
allowed_tools: vec![],
forbidden_mutations: vec!["Cargo.toml".into(), "package.json".into()],
},
AgentType::Planner => Self {
agent_type: agent_type.clone(),
auto_approve: false,
max_depth: 3,
max_depth: 5,
require_preview: true,
timeout_seconds: 120,
max_tokens: 16384,
max_tokens: 131072,
allowed_tools: vec![],
forbidden_mutations: vec![],
},
@ -126,7 +126,7 @@ impl AgentPolicy {
forbidden_mutations: vec![],
},
/* ExploreAgent: auto-approved, read-only tools only, short timeout.
Forbidden mutations set to wildcard to block all writes. */
Forbidden mutations set to wildcard to block all writes. */
AgentType::ExploreAgent => Self {
agent_type: agent_type.clone(),
auto_approve: true,
@ -149,8 +149,8 @@ impl AgentPolicy {
}
/* Per-agent prompt configuration. Stored in the DB and resolved on task
dispatch. Resolution order: task instructions > DB override > this config
> compiled-in default > tool protocol. */
dispatch. Resolution order: task instructions > DB override > this config
> compiled-in default > tool protocol. */
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentPromptConfig {
pub agent_type: AgentType,
@ -208,7 +208,12 @@ mod tests {
for agent_type in &types {
let s = agent_type.as_str();
let parsed = AgentType::from_str(s);
assert_eq!(parsed.as_ref(), Some(agent_type), "roundtrip failed for {:?}", agent_type);
assert_eq!(
parsed.as_ref(),
Some(agent_type),
"roundtrip failed for {:?}",
agent_type
);
}
}

View file

@ -18,7 +18,9 @@ pub mod task;
pub mod todo;
pub mod tools;
pub use agents::{AgentPolicy, AgentPromptConfig, AgentType, ApprovalMode, PlannedAction, SimulationResult};
pub use agents::{
AgentPolicy, AgentPromptConfig, AgentType, ApprovalMode, PlannedAction, SimulationResult,
};
pub use ai_response::{Conversation, Message};
pub use conclusion::{
Artifact, ArtifactType, ConclusionCard, ConclusionMetrics, ConclusionTrigger,
@ -32,11 +34,11 @@ pub use project::{Project, ProjectFile, ProjectId, ProjectSettings};
pub use task::{Task, TaskResult};
pub use todo::{Dependency, ToDo};
pub use tools::{
format_tool_error, format_tools_json, format_tool_result, parse_tool_call_blocks,
parse_tool_call_stream, tool_definitions, tool_ids_for_agent_type, ApprovalRequirement,
CompositionStep, ContextVisibility, ExecutionContext, MarketplaceSearchResults, ParsedToolCall,
REEF_PROXY_TOOLS, GLOBAL_TOOLS, is_reef_proxy_tool, is_global_tool,
TestAction, ToolComposition, ToolDefinition, ToolDependency, ToolDocumentation, ToolExample,
ToolListing, ToolParser, ToolPlugin, ToolExecutor, ToolRegistry, ToolTest, ToolTestResult, ToolTestSuite,
ToolTestSuiteResult, ToolVersion,
format_tool_error, format_tool_result, format_tools_json, is_global_tool, is_reef_proxy_tool,
parse_tool_call_blocks, parse_tool_call_stream, tool_definitions, tool_ids_for_agent_type,
ApprovalRequirement, CompositionStep, ContextVisibility, ExecutionContext,
MarketplaceSearchResults, ParsedToolCall, TestAction, ToolComposition, ToolDefinition,
ToolDependency, ToolDocumentation, ToolExample, ToolExecutor, ToolListing, ToolParser,
ToolPlugin, ToolRegistry, ToolTest, ToolTestResult, ToolTestSuite, ToolTestSuiteResult,
ToolVersion, GLOBAL_TOOLS, REEF_PROXY_TOOLS,
};

View file

@ -1,4 +1,4 @@
use std::path::{Path, PathBuf, Component};
use std::path::{Component, Path, PathBuf};
/* Maximum allowed path depth as a safety measure */
const MAX_PATH_DEPTH: usize = 64;
@ -54,15 +54,15 @@ pub fn resolve_sandboxed_path(validated: &Path, sandbox_root: &Path) -> Result<P
};
let canonical = if full_path.exists() {
full_path.canonicalize().map_err(|e| {
format!("Failed to canonicalize path: {}", e)
})?
full_path
.canonicalize()
.map_err(|e| format!("Failed to canonicalize path: {}", e))?
} else {
if let Some(parent) = full_path.parent() {
if parent.exists() {
let canonical_parent = parent.canonicalize().map_err(|e| {
format!("Failed to canonicalize parent path: {}", e)
})?;
let canonical_parent = parent
.canonicalize()
.map_err(|e| format!("Failed to canonicalize parent path: {}", e))?;
if !canonical_parent.starts_with(sandbox_root) {
return Err("Path escapes sandbox root boundaries".to_string());
}
@ -81,7 +81,8 @@ pub fn resolve_sandboxed_path(validated: &Path, sandbox_root: &Path) -> Result<P
}
fn has_directory_traversal(path: &Path) -> bool {
path.components().any(|comp| matches!(comp, Component::ParentDir))
path.components()
.any(|comp| matches!(comp, Component::ParentDir))
}
fn contains_null_bytes(path: &Path) -> bool {
@ -90,10 +91,11 @@ fn contains_null_bytes(path: &Path) -> bool {
fn contains_shell_metacharacters(path: &Path) -> bool {
const SHELL_METACHARACTERS: &[char] = &[
'|', ';', '&', '$', '`', '>', '<', '(', ')', '{', '}',
'!', '#', '*', '?', '[', ']', '~', '\n', '\r',
'|', ';', '&', '$', '`', '>', '<', '(', ')', '{', '}', '!', '#', '*', '?', '[', ']', '~',
'\n', '\r',
];
path.to_str().map_or(true, |s| s.contains(SHELL_METACHARACTERS))
path.to_str()
.map_or(true, |s| s.contains(SHELL_METACHARACTERS))
}
#[cfg(test)]
@ -152,7 +154,12 @@ mod tests {
#[test]
fn test_rejects_path_with_shell_metacharacters() {
let attacks = vec![
"file|echo", "file;rm", "file$(id)", "file`id`", "file>out", "file<in",
"file|echo",
"file;rm",
"file$(id)",
"file`id`",
"file>out",
"file<in",
];
for path in attacks {
let result = validate_path(path);

View file

@ -102,7 +102,10 @@ impl ToDo {
self
}
pub fn validate(&self, deps: &std::collections::HashMap<Uuid, Vec<Uuid>>) -> Result<(), ValidationError> {
pub fn validate(
&self,
deps: &std::collections::HashMap<Uuid, Vec<Uuid>>,
) -> Result<(), ValidationError> {
if self.title.is_empty() {
return Err(ValidationError::EmptyTitle);
}
@ -127,8 +130,10 @@ impl ToDo {
}
/* DFS cycle detection over a full dependency graph. Returns the first
todo_id found in a cycle, or None when the graph is acyclic. */
pub fn detect_cycle_in_graph(deps: &std::collections::HashMap<Uuid, Vec<Uuid>>) -> Option<Uuid> {
todo_id found in a cycle, or None when the graph is acyclic. */
pub fn detect_cycle_in_graph(
deps: &std::collections::HashMap<Uuid, Vec<Uuid>>,
) -> Option<Uuid> {
let mut visited: HashSet<Uuid> = HashSet::new();
let mut in_stack: HashSet<Uuid> = HashSet::new();
@ -210,10 +215,7 @@ impl ToDo {
}
/// Walk the parent_todo_id chain and return the depth (1 = root, 2 = child, etc.)
pub fn calculate_depth(
todo_id: Uuid,
get_parent: impl Fn(Uuid) -> Option<Uuid>,
) -> usize {
pub fn calculate_depth(todo_id: Uuid, get_parent: impl Fn(Uuid) -> Option<Uuid>) -> usize {
let mut depth = 1usize;
let mut current_id = get_parent(todo_id);
let mut visited = HashSet::new();
@ -264,7 +266,6 @@ impl ToDo {
}
}
#[cfg(test)]
mod tests {
use super::*;
@ -287,7 +288,10 @@ mod tests {
fn test_todo_validate_empty_title() {
let todo = ToDo::default();
let deps = std::collections::HashMap::new();
assert!(matches!(todo.validate(&deps), Err(ValidationError::EmptyTitle)));
assert!(matches!(
todo.validate(&deps),
Err(ValidationError::EmptyTitle)
));
}
#[test]

View file

@ -1867,6 +1867,8 @@ pub fn tool_ids_for_agent_type(agent_type: &crate::agents::AgentType) -> Option<
"get_documentation_context",
"documentation_tree",
"store_file_doc",
"kanban_create_todo",
"kanban_update_todo",
]),
crate::agents::AgentType::TestJudge => curated(&[
"read_file",