Skip to content

feat(tools): add curl tool with domain whitelist and configurable limits (gh:1845)#2416

Draft
fakhriaunur wants to merge 1 commit intosipeed:mainfrom
fakhriaunur:feat/curl-tool
Draft

feat(tools): add curl tool with domain whitelist and configurable limits (gh:1845)#2416
fakhriaunur wants to merge 1 commit intosipeed:mainfrom
fakhriaunur:feat/curl-tool

Conversation

@fakhriaunur
Copy link
Copy Markdown

📝 Description

Add a dedicated curl tool that allows agents to make HTTP requests to external APIs with domain whitelisting for security. This enables skills like Clawdfeed that need to call external APIs without requiring shell command execution.

🗣️ Type of Change

  • 🐞 Bug fix (non-breaking change which fixes an issue)
  • ✨ New feature (non-breaking change which adds functionality)
  • 📖 Documentation update
  • ⚡ Code refactoring (no functional changes, no api changes)

🤖 AI Code Generation

  • 🤖 Fully AI-generated (100% AI, 0% Human)
  • 🛠️ Mostly AI-generated (AI draft, Human verified/modified)
  • 👨‍💻 Mostly Human-written (Human lead, AI assisted or none)

🔗 Related Issue

Fixes #1845

📚 Technical Context

  • Reference URL: [Feature] allowing curl #1845
  • Reasoning: Skills need to call external APIs securely. Instead of removing curl from the blacklist, a dedicated tool with domain whitelisting provides better security control.

Implementation:

  • pkg/tools/curl.go - New CurlTool with domain whitelist, method/header/body/timeout support
  • pkg/tools/curl_test.go - 13 tests covering all security gates and HTTP operations
  • pkg/config/config.go - Added CurlConfig struct with allowed_domains, timeout_seconds, max_bytes
  • pkg/agent/loop.go - Tool registration wired to config

Security features:

  • URL validation (parseable, http/https only)
  • Domain whitelist with subdomain matching (example.com allows api.example.com)
  • Empty whitelist = allow all (optional security gate)
  • Redirect blocking to disallowed domains (max 5 redirects)
  • Timeout protection (default 30s, max 120s)
  • Response size limit (default 1MB)

🧪 Test Environment

  • Hardware: PC
  • OS: WSL Ubuntu
  • Model/Provider: N/A (tool implementation)
  • Channels: N/A (tool implementation)

☑️ Checklist

  • My code/docs follow the style of this project.
  • I have performed a self-review of my own changes.
  • I have updated the documentation accordingly.

Copilot AI review requested due to automatic review settings April 7, 2026 21:10
@CLAassistant
Copy link
Copy Markdown

CLAassistant commented Apr 7, 2026

CLA assistant check
All committers have signed the CLA.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a dedicated curl tool to let agents perform outbound HTTP requests with configurable safety limits (domain allowlist, timeouts, size limits), intended to avoid needing shell exec for API calls.

Changes:

  • Introduces pkg/tools/CurlTool with URL validation, domain allowlisting, redirect checks, headers/body/method support, and timeout/size limiting.
  • Adds unit tests covering common request paths and allowlist behavior.
  • Extends ToolsConfig with a new curl section and IsToolEnabled("curl") support.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 7 comments.

File Description
pkg/tools/curl.go New HTTP request tool implementation with allowlist/limits and formatted output.
pkg/tools/curl_test.go New unit tests for curl tool behaviors.
pkg/config/config.go Adds CurlConfig and enables IsToolEnabled("curl") lookup.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 1370 to +1374
return t.SendTTS.Enabled
case "write_file":
return t.WriteFile.Enabled
case "curl":
return t.Curl.Enabled
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR adds ToolsConfig.Curl and IsToolEnabled("curl"), but there is no corresponding tool registration/use of this config anywhere (e.g. no IsToolEnabled("curl") checks or NewCurlTool(...) calls in pkg/agent/*). As a result the tool will never be available at runtime even when enabled in config. Please wire CurlTool into the agent tool registry (likely in AgentLoop or NewAgentInstance) using this config.

Copilot uses AI. Check for mistakes.
Comment on lines +158 to +162
timeout := curlDefaultTimeout
if tSec, ok := args["timeout"].(float64); ok && tSec > 0 {
timeout = time.Duration(tSec) * time.Second
if timeout > 120*time.Second {
timeout = 120 * time.Second
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The per-call timeout argument is enforced via context.WithTimeout, but the http.Client is also created with a fixed Timeout (from opts.TimeoutSeconds/default 30s). This prevents callers from increasing the timeout beyond the client-level timeout, even though the tool parameter schema allows up to 120s. Consider relying on context timeouts only (no http.Client.Timeout) or setting the client timeout to the maximum allowed and enforcing the per-call limit via context.

Copilot uses AI. Check for mistakes.
Comment on lines +179 to +184
body, err := io.ReadAll(io.LimitReader(resp.Body, t.maxBytes))
if err != nil {
if err == io.ErrUnexpectedEOF || strings.Contains(err.Error(), "http: request body too large") {
return ErrorResult(fmt.Sprintf("response body exceeded %d bytes limit", t.maxBytes))
}
return ErrorResult(fmt.Sprintf("failed to read response: %v", err))
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

io.ReadAll(io.LimitReader(resp.Body, t.maxBytes)) will silently truncate the response if it exceeds t.maxBytes and will not return an oversize error, so the current oversize error handling path is ineffective. To detect oversize reliably (and set truncated accurately), read t.maxBytes+1 bytes and check whether an extra byte was present, then either error or truncate explicitly.

Copilot uses AI. Check for mistakes.
Comment on lines +201 to +205
"body": string(body),
"url": urlStr,
"method": method,
"truncated": int64(len(body)) >= t.maxBytes,
}
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

truncated is computed as len(body) >= t.maxBytes, which will mark responses of exactly t.maxBytes bytes as truncated even when they fit the limit. If you implement an oversize probe (read maxBytes+1), set truncated based on whether more than maxBytes bytes were available.

Copilot uses AI. Check for mistakes.
Comment on lines +102 to +103
"minimum": 1.0,
"maximum": 120.0,
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The schema for timeout declares type: integer but uses floating-point values for minimum/maximum (1.0/120.0). Some JSON-schema consumers treat this as inconsistent. Use integer values for min/max to match the declared type.

Suggested change
"minimum": 1.0,
"maximum": 120.0,
"minimum": 1,
"maximum": 120,

Copilot uses AI. Check for mistakes.
Comment on lines +50 to +56
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
if len(via) >= curlMaxRedirects {
return fmt.Errorf("stopped after %d redirects", curlMaxRedirects)
}
if !isDomainAllowed(req.URL.Hostname(), opts.AllowedDomains) {
return fmt.Errorf("redirect to disallowed domain %q", req.URL.Hostname())
}
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CheckRedirect validates redirects against opts.AllowedDomains (unnormalized) rather than the normalized domain list stored on the tool. This can incorrectly block redirects when config includes schemes, mixed case, or trailing slashes. Normalize once in NewCurlTool and use that normalized slice for both initial request and redirect validation.

Copilot uses AI. Check for mistakes.
Comment on lines +846 to +850
ToolConfig `json:"-" envPrefix:"PICOCLAW_TOOLS_CURL_"`
AllowedDomains []string `json:"allowed_domains,omitempty" env:"PICOCLAW_TOOLS_CURL_ALLOWED_DOMAINS"`
TimeoutSeconds int `json:"timeout_seconds" env:"PICOCLAW_TOOLS_CURL_TIMEOUT_SECONDS"`
MaxBytes int64 `json:"max_bytes" env:"PICOCLAW_TOOLS_CURL_MAX_BYTES"`
}
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CurlConfig embeds ToolConfig with json:"-", which prevents curl.enabled from being set via the JSON config file (unlike other tool configs such as ExecConfig/WebToolsConfig). Remove the json:"-" tag so enabled can be configured normally while keeping envPrefix for env var support.

Copilot uses AI. Check for mistakes.
@fakhriaunur fakhriaunur marked this pull request as draft April 7, 2026 22:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] allowing curl

3 participants