mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-25 14:53:34 +00:00
* feat: add web_fetch built-in tool and web-fetch skill - New web_fetch Rust built-in tool (GET-only, auto-approved, structured output: url/title/content/word_count) with HTML to Markdown via Readability - Full SSRF protection: HTTPS-only, no private IPs, DNS rebinding defence, outbound/inbound leak scanning, 5 MB cap, no redirect following - Rate limited: 30 req/min, 500/hr (same as http tool) - Protected tool name; registered in register_builtin_tools() - validate_url made pub(crate) so web_fetch can reuse it from http.rs - New skills/web-fetch/SKILL.md for agent guidance on web browsing - Fixes unicode panic in extract_title: use to_ascii_lowercase not to_lowercase to preserve byte offsets when indexing original string Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * chore: remove web-fetch skill (tool description is self-sufficient) The web_fetch tool's schema description already tells the LLM when and how to use it. A SKILL.md would only add redundant prompt context. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * fix: include HTTP status in web_fetch output The LLM had no way to distinguish a 404 error page from a 200 success. Including status in the structured output (alongside url/title/content/ word_count) lets the agent report failures correctly and matches the behaviour of the http tool which always returns status. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * feat(web_fetch): add Chrome UA and safe redirect following - Set a Chrome-like User-Agent so sites that block the default reqwest string return real content instead of bot-rejection pages. - Add Accept: text/markdown, text/html header (mirrors OpenClaw). - Follow up to 3 redirects manually instead of blocking all 3xx. Every Location URL is run through validate_url() before the next request is sent, so SSRF protection applies to every hop identically to how it applies to the original URL. - Resolve relative Location values against the current URL before SSRF-validating them. - Log each followed hop at DEBUG level. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * fix(web_fetch): expose final_url after redirect following When redirects are followed, the original `url` field no longer reflects where the content actually came from. Add `final_url` so the LLM can cite the canonical source correctly. Equals `url` when no redirects occurred. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * fix(web_fetch): address review comments and fix CI failures - Store LeakDetector in WebFetchTool struct (init once in new(), not per execute() call) - Use self.leak_detector for both outbound scan and redirect re-validation - Simplify HTML/cfg blocks to reduce duplication (gemini-code-assist suggestion) - Fix pub use ordering in mod.rs (cargo fmt) - Add web_fetch to core_registration_covers_expected_tools snapshot test Co-Authored-By: Claude Sonnet 4.6 <[email protected]> --------- Co-authored-by: Claude Sonnet 4.6 <[email protected]>
142 lines
4.6 KiB
Rust
142 lines
4.6 KiB
Rust
//! Validates that all built-in tool schemas conform to OpenAI strict-mode rules.
|
|
//!
|
|
//! This catches the class of bugs where `required` keys aren't in `properties`,
|
|
//! properties are missing `type` (intentional freeform is allowed), or nested
|
|
//! objects/arrays are malformed.
|
|
//!
|
|
//! See: <https://github.com/nearai/ironclaw/issues/352> (QA plan, item 1.1)
|
|
|
|
use ironclaw::tools::validate_tool_schema;
|
|
use ironclaw::tools::{Tool, ToolRegistry};
|
|
|
|
/// Validate schemas of all tools registered via `register_builtin_tools()` and
|
|
/// `register_dev_tools()` (echo, time, json, http, shell, file tools).
|
|
///
|
|
/// These tools can be constructed without external dependencies (no DB, no
|
|
/// workspace, no extension manager). Tools requiring dependencies (memory, job,
|
|
/// skill, extension, routine) are validated individually below where test
|
|
/// construction helpers exist.
|
|
#[tokio::test]
|
|
async fn all_core_builtin_tool_schemas_are_valid() {
|
|
let registry = ToolRegistry::new();
|
|
registry.register_builtin_tools();
|
|
registry.register_dev_tools();
|
|
|
|
let tools = registry.all().await;
|
|
assert!(
|
|
!tools.is_empty(),
|
|
"registry should have tools after registration"
|
|
);
|
|
|
|
let mut all_errors = Vec::new();
|
|
for tool in &tools {
|
|
let schema = tool.parameters_schema();
|
|
let errors = validate_tool_schema(&schema, tool.name());
|
|
if !errors.is_empty() {
|
|
all_errors.push(format!(
|
|
"Tool '{}' has schema errors:\n {}",
|
|
tool.name(),
|
|
errors.join("\n ")
|
|
));
|
|
}
|
|
}
|
|
|
|
assert!(
|
|
all_errors.is_empty(),
|
|
"Tool schema validation failures:\n{}",
|
|
all_errors.join("\n\n")
|
|
);
|
|
}
|
|
|
|
/// Verify the exact set of tools registered by the core registration methods.
|
|
/// This guards against a new tool being added without schema validation coverage.
|
|
#[tokio::test]
|
|
async fn core_registration_covers_expected_tools() {
|
|
let registry = ToolRegistry::new();
|
|
registry.register_builtin_tools();
|
|
registry.register_dev_tools();
|
|
|
|
let mut names = registry.list().await;
|
|
names.sort();
|
|
|
|
let expected = &[
|
|
"apply_patch",
|
|
"echo",
|
|
"http",
|
|
"json",
|
|
"list_dir",
|
|
"read_file",
|
|
"shell",
|
|
"time",
|
|
"web_fetch",
|
|
"write_file",
|
|
];
|
|
|
|
assert_eq!(
|
|
names, expected,
|
|
"Core tool set changed. Update this test and ensure new tools have valid schemas."
|
|
);
|
|
}
|
|
|
|
/// Validate individual tool schemas that are known to use non-trivial patterns.
|
|
/// These are regression tests for specific bugs.
|
|
#[test]
|
|
fn json_tool_freeform_data_field_is_valid() {
|
|
// Regression: json tool's "data" field intentionally has no "type" for
|
|
// OpenAI compatibility (union types with arrays require "items").
|
|
let tool = ironclaw::tools::builtin::JsonTool;
|
|
let schema = tool.parameters_schema();
|
|
let errors = validate_tool_schema(&schema, "json");
|
|
assert!(errors.is_empty(), "json tool schema errors: {errors:?}");
|
|
|
|
// Verify the freeform pattern is still in place
|
|
let data = schema
|
|
.get("properties")
|
|
.and_then(|p| p.get("data"))
|
|
.expect("json tool should have 'data' property");
|
|
assert!(
|
|
data.get("type").is_none(),
|
|
"json.data should be freeform (no type) for OpenAI compatibility"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn http_tool_headers_array_is_valid() {
|
|
// Regression: http tool's "headers" is an array of {name, value} objects.
|
|
let tool = ironclaw::tools::builtin::HttpTool::new();
|
|
let schema = tool.parameters_schema();
|
|
let errors = validate_tool_schema(&schema, "http");
|
|
assert!(errors.is_empty(), "http tool schema errors: {errors:?}");
|
|
|
|
// Verify array structure
|
|
let headers = schema
|
|
.get("properties")
|
|
.and_then(|p| p.get("headers"))
|
|
.expect("http tool should have 'headers' property");
|
|
assert_eq!(
|
|
headers.get("type").and_then(|t| t.as_str()),
|
|
Some("array"),
|
|
"headers should be an array"
|
|
);
|
|
assert!(
|
|
headers.get("items").is_some(),
|
|
"headers array should have items defined"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn time_tool_schema_is_valid() {
|
|
let tool = ironclaw::tools::builtin::TimeTool;
|
|
let schema = tool.parameters_schema();
|
|
let errors = validate_tool_schema(&schema, "time");
|
|
assert!(errors.is_empty(), "time tool schema errors: {errors:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn shell_tool_schema_is_valid() {
|
|
let tool = ironclaw::tools::builtin::ShellTool::new();
|
|
let schema = tool.parameters_schema();
|
|
let errors = validate_tool_schema(&schema, "shell");
|
|
assert!(errors.is_empty(), "shell tool schema errors: {errors:?}");
|
|
}
|