Class: Rubino::Run::AttachmentDownloader

Inherits:
Object
  • Object
show all
Defined in:
lib/rubino/run/attachment_downloader.rb

Overview

Fetches the URLs passed as ‘attachments` on a run and saves them under <workspace>/uploads/<run_id>/. The runner then tells the model “you have these local files” instead of forcing it to do tool calls (webfetch was crashing on binaries — see v0.2.5 fix —and even when it worked the model paid context for the bytes).

SSRF guard: only URLs whose host appears in attachments.allowed_hosts (config) or ENV (comma-separated) are fetched. Empty config + empty env = block everything. The list is case-insensitive and matched exactly against the URI host (no port, no path, no subdomain magic) so an admin knows exactly what is allowed without re-reading regex semantics.

Constant Summary collapse

MAX_BYTES_PER_FILE =

50 MB hard cap, matches uploads

50 * 1024 * 1024
HTTP_TIMEOUT =
30
LOOPBACK_HOSTS =

When an HTTP client is co-located on the same host as the agent, attachment URLs are loopback (localhost:3000/…). These are always allowed IN ADDITION to attachments.allowed_hosts so the common case works out of the box without opening the guard to arbitrary external hosts. SSRF risk is bounded: only the local host is reachable, which the agent could already talk to via the shell.

%w[localhost 127.0.0.1 ::1].freeze

Instance Method Summary collapse

Constructor Details

#initialize(workspace_root: nil, allowed_hosts: nil) ⇒ AttachmentDownloader

Returns a new instance of AttachmentDownloader.



33
34
35
36
# File 'lib/rubino/run/attachment_downloader.rb', line 33

def initialize(workspace_root: nil, allowed_hosts: nil)
  @workspace_root = workspace_root || Rubino::Workspace.primary_root
  @allowed_hosts  = normalize_hosts(allowed_hosts || default_allowed_hosts)
end

Instance Method Details

#fetch_all(run_id:, urls:) ⇒ Array<String>

Returns absolute paths of successfully saved files.

Returns:

  • (Array<String>)

    absolute paths of successfully saved files.



39
40
41
42
43
44
45
46
# File 'lib/rubino/run/attachment_downloader.rb', line 39

def fetch_all(run_id:, urls:)
  list = Array(urls).reject { |u| u.to_s.strip.empty? }
  return [] if list.empty?

  dir = File.join(@workspace_root, "uploads", run_id.to_s)
  FileUtils.mkdir_p(dir)
  list.filter_map { |url| fetch_one(dir, url) }
end