Captain custom tools previously used their own hand-rolled `Net::HTTP` request code. This moves them onto `SafeFetch`, the same shared HTTP-fetching helper already used by webhooks, uploads, avatar imports, and the Captain page crawler. Behavior for normal tools is unchanged — they just now share one consistent path for host resolution, timeouts, response size limits, and redirect handling. **What changed** - `HttpTool#execute_http_request` now delegates to `SafeFetch.fetch` instead of building `Net::HTTP` requests by hand. Auth headers, basic auth, metadata headers, JSON content-type, and the 1 MB response cap all map onto `SafeFetch` options. - Removed ~80 lines of bespoke request/validation plumbing from `HttpTool`. - The custom tools `test` endpoint now reads the response body string directly (the executor returns the body rather than a response object). **How to test** 1. Enable `custom_tools` (or `captain_integration_v2`) for an account. 2. Create a Captain custom tool pointing at a public HTTPS endpoint (e.g. a test API). 3. Use the **Test** button in the tool form — you should get a success result. 4. Run the tool from a Captain conversation and confirm the response is returned/templated as before. **Gotchas** - **Local dev:** `SafeFetch` blocks requests to private/loopback addresses by default. If you're testing a custom tool against a service on `localhost` or a private IP during development, set `SAFE_FETCH_ALLOW_PRIVATE_NETWORK=true` or the request will be rejected. (This matches how the rest of `SafeFetch` already behaves locally.) - **Test endpoint status field:** on success the `test` response now reports `status: 200` rather than the exact 2xx code (201/204/etc.), because `SafeFetch` signals success-vs-failure rather than exposing the raw response. The UI only checks the 2xx range, so this is invisible there — but worth knowing if anything consumes the API directly. - **Non-2xx responses** still surface as an error (same as before), now via `SafeFetch::HttpError`.
56 lines
1.8 KiB
Ruby
56 lines
1.8 KiB
Ruby
require 'agents'
|
|
|
|
class Captain::Tools::HttpTool < Agents::Tool
|
|
def initialize(assistant, custom_tool)
|
|
@assistant = assistant
|
|
@custom_tool = custom_tool
|
|
super()
|
|
end
|
|
|
|
def active?
|
|
@custom_tool.enabled?
|
|
end
|
|
|
|
def perform(tool_context, **params)
|
|
url = @custom_tool.build_request_url(params)
|
|
body = @custom_tool.build_request_body(params)
|
|
|
|
response_body = execute_http_request(url, body, tool_context)
|
|
@custom_tool.format_response(response_body)
|
|
rescue StandardError => e
|
|
Rails.logger.error("HttpTool execution error for #{@custom_tool.slug}: #{e.class} - #{e.message}")
|
|
'An error occurred while executing the request'
|
|
end
|
|
|
|
private
|
|
|
|
# Limit response size to prevent memory exhaustion and match LLM token limits
|
|
# 1MB of text ≈ 250K tokens, which exceeds most LLM context windows
|
|
MAX_RESPONSE_SIZE = 1.megabyte
|
|
|
|
# Route through SafeFetch so custom tool requests share the app's centralized HTTP
|
|
# fetching (resolution, timeouts, response size limits, and redirect handling).
|
|
def execute_http_request(url, body, tool_context)
|
|
json_body = body if @custom_tool.http_method == 'POST'
|
|
|
|
response_body = +''
|
|
SafeFetch.fetch(
|
|
url,
|
|
method: @custom_tool.http_method == 'POST' ? :post : :get,
|
|
body: json_body,
|
|
headers: request_headers(tool_context, json_body),
|
|
http_basic_authentication: @custom_tool.build_basic_auth_credentials,
|
|
max_bytes: MAX_RESPONSE_SIZE,
|
|
validate_content_type: false
|
|
) { |result| response_body = result.tempfile.read }
|
|
response_body
|
|
end
|
|
|
|
def request_headers(tool_context, json_body)
|
|
headers = @custom_tool.build_auth_headers
|
|
headers.merge!(@custom_tool.build_metadata_headers(tool_context&.state || {}))
|
|
headers['Content-Type'] = 'application/json' if json_body.present?
|
|
headers
|
|
end
|
|
end
|