Class: Nexo::Tools::WebSearch

Inherits:
RubyLLM::Tool
  • Object
show all
Defined in:
lib/nexo/tools/web_search.rb

Overview

Vendor-neutral web search, gated by the :search capability (denied by default, like +:fetch+/+:shell+). Follows the Tools::Fetch shape exactly: authorize first, act, and rescue Permissions::Denied into { error: ... } so the loop never crashes.

Nexo ships NO search provider. The query is delegated to a host-injected backend responding to #search(query, **opts) that returns an Enumerable of {title:, url:, snippet:} rows (Hashes or objects responding to #to_h). Results are count- and snippet-capped so untrusted provider output can't blow the context window. It pairs with Tools::Fetch: search finds URLs, fetch reads one.

Backend results are UNTRUSTED model input (prompt-injection risk in snippets); the backend is trust-bearing and runs in the host process, not the sandbox.

Return shape: success is { results: [{title:, url:, snippet:}, …] } (≤8 rows); any denial or error is { error: <message> }.

Constant Summary collapse

MAX_RESULTS =

At most this many result rows are returned.

8
MAX_SNIPPET =

Each result snippet is truncated to this many characters.

300

Instance Method Summary collapse

Constructor Details

#initialize(sandbox:, permissions:, backend:) ⇒ WebSearch

permissions gates the :search capability; backend is the injected, host-owned provider. sandbox: is accepted for signature parity with the other tools even though search runs in the host process, not the sandbox.



34
35
36
37
38
# File 'lib/nexo/tools/web_search.rb', line 34

def initialize(sandbox:, permissions:, backend:)
  @permissions = permissions
  @backend = backend
  super()
end

Instance Method Details

#execute(query:) ⇒ Object

Order of operations (every denial/error returns { error: }, never raises into the loop): capability gate → delegate to the backend (query-only) → normalize and cap. The gate runs before the backend, so a denied search never touches it.



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/nexo/tools/web_search.rb', line 44

def execute(query:)
  @permissions.authorize!(:search, query)
  rows = Array(@backend.search(query)).first(MAX_RESULTS)
  {results: rows.map { |r| normalize(r) }}
rescue Permissions::Denied => e
  {error: e.message}
rescue Nexo::ApprovalRequired
  # Durable approval (Spec 16): the :approve gate on authorize!(:search, query)
  # must PAUSE the run, not become a tool error. Re-raise past the broad
  # rescue below so run_agent can catch it and suspend. (Denied above still
  # becomes {error:} — that is the intended deny contract.)
  raise
rescue => e
  {error: "search failed: #{e.message}"}
end