feat(routines): enable tool access in lightweight routine execution (#257) (#730)

* Rebase onto staging

* fix(routines): prevent autonomy-escalation in lightweight routines

  - Add ROUTINE_TOOL_DENYLIST to block routine_create/update/delete/fire
    and restart from being callable by lightweight routines
  - Deduplicate sentinel logic by reusing handle_text_response() in the
    no-tools path
  - Filter tool definitions sent to LLM to only include callable tools,
    avoiding wasted tokens on tools that would be rejected
This commit is contained in:
Reid
2026-03-12 11:38:27 -07:00
committed by GitHub
parent 006c15e79c
commit 6bbf87ba3a
6 changed files with 260 additions and 27 deletions
+21
View File
@@ -104,6 +104,14 @@ impl Tool for RoutineCreateTool {
"enum": ["lightweight", "full_job"],
"description": "Execution mode: 'lightweight' (single LLM call, default) or 'full_job' (multi-turn with tools)"
},
"use_tools": {
"type": "boolean",
"description": "Enable tool access in lightweight mode (default: false). Only safe tools (no approval required) are available. Ignored for full_job mode."
},
"max_tool_rounds": {
"type": "integer",
"description": "Max tool call rounds in lightweight mode (default: 3). Only used when use_tools is true."
},
"cooldown_secs": {
"type": "integer",
"description": "Minimum seconds between fires (default: 300)"
@@ -262,11 +270,24 @@ impl Tool for RoutineCreateTool {
})
.unwrap_or_default();
let use_tools = params
.get("use_tools")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let max_tool_rounds = params
.get("max_tool_rounds")
.and_then(|v| v.as_u64())
.map(|v| v.clamp(1, crate::agent::routine::MAX_TOOL_ROUNDS_LIMIT as u64) as u32)
.unwrap_or(3);
let action = match action_type {
"lightweight" => RoutineAction::Lightweight {
prompt: prompt.to_string(),
context_paths,
max_tokens: 4096,
use_tools,
max_tool_rounds,
},
"full_job" => {
let tool_permissions = crate::agent::routine::parse_tool_permissions(&params);
+33 -1
View File
@@ -23,7 +23,7 @@ use crate::tools::builtin::{
ToolUpgradeTool, WriteFileTool,
};
use crate::tools::rate_limiter::RateLimiter;
use crate::tools::tool::{Tool, ToolDomain};
use crate::tools::tool::{ApprovalRequirement, Tool, ToolDomain};
use crate::tools::wasm::{
Capabilities, OAuthRefreshConfig, ResourceLimits, SharedCredentialRegistry, WasmError,
WasmStorageError, WasmToolRuntime, WasmToolStore, WasmToolWrapper,
@@ -278,6 +278,38 @@ impl ToolRegistry {
.collect()
}
/// Get tool definitions excluding specific tools by name.
///
/// Used by lightweight routines to filter out denylisted and approval-gated tools
/// so the LLM only sees tools it is actually allowed to call.
pub async fn tool_definitions_excluding(&self, deny: &[&str]) -> Vec<ToolDefinition> {
let empty_params = serde_json::Value::Object(serde_json::Map::new());
let mut defs: Vec<ToolDefinition> = self
.tools
.read()
.await
.values()
.filter(|tool| {
// Exclude denylisted tools
if deny.contains(&tool.name()) {
return false;
}
// Exclude tools that require approval
matches!(
tool.requires_approval(&empty_params),
ApprovalRequirement::Never
)
})
.map(|tool| ToolDefinition {
name: tool.name().to_string(),
description: tool.description().to_string(),
parameters: tool.parameters_schema(),
})
.collect();
defs.sort_unstable_by(|a, b| a.name.cmp(&b.name));
defs
}
/// Register development tools for building software.
///
/// These tools provide shell access, file operations, and code editing