Skip to content

feat(tools): add web_search, web_fetch, and run_command tools (ENG-382) - #5

Merged
folathecoder merged 2 commits into
mainfrom
folathecoder/eng-382-6-built-in-tools-web-search-web-fetch-and-terminal
Oct 18, 2026
Merged

folathecoder merged 2 commits into
mainfrom
folathecoder/eng-382-6-built-in-tools-web-search-web-fetch-and-terminal

Conversation

@folathecoder

Copy link
Copy Markdown
Contributor

Summary

Adds Aldwin's built-in web and terminal tools (web_search, web_fetch, and run_command), registers the fixed six-tool set for 0.0.1 through builtin_tools(), and maps one-line tool card summaries into ToolResultEvent while preserving full tool outputs in session memory. Web fetches block localhost and private IPs across redirects, cap downloads at 2 MB, and wrap extracted text in <untrusted> tags; terminal commands refuse sudo and interactive programs before prompting, require explicit y/n approval via ask(), scrub secret environment variables, and run in their own process group with a 60-second timeout.

Ticket

Changes

  • Tool summary contract & registry (src/aldwin/core/tools/__init__.py, src/aldwin/core/agent.py, src/aldwin/host/runner.py):
    • Added ToolOutput(str) carrying .summary and .ok across str(), +, and copy.deepcopy.
    • Added summarize_tool_result(), builtin_tools(), and list_builtin_tools() (re-exported as get_builtin_tools() in aldwin.host and aldwin.client for /tools).
    • Wired builtin_tools() as the sole default tool registry in build_agent() and updated RememberTool and SetPersonaTool to return ToolOutput(..., ok=False) on validation errors or user decline.
  • Web search (src/aldwin/core/tools/web_search.py):
    • Added SearchResult, SearchProvider protocol, TavilyProvider (httpx.AsyncClient against POST https://api.tavily.com/search with API key redaction), and WebSearchTool (1..400 char query, 1..10 limit, 15 s timeout, and missing-key setup message).
  • Web fetch (src/aldwin/core/tools/web_fetch.py):
    • Added validate_public_url() and is_public_ip() blocking non-http/https schemes, localhost/.local/.internal hosts, and loopback, RFC 1918, link-local (169.254.0.0/16, fe80::/10), CGNAT, and IPv4-mapped private IPv6 addresses.
    • Added manual 5-hop redirect validation, 2 MB streaming download cap, trafilatura + HTMLParser fallback extraction with Content-Type charset decoding, 20,000-char truncation, and <untrusted source="..."> wrapping with closing-tag escaping.
  • Terminal execution (src/aldwin/core/tools/run_command.py):
    • Added check_command_allowed() rejecting empty or > 2,000-char commands, sudo/su/doas, interactive programs (vim, less, top, ssh, etc.), and bare REPLs/shells across pipelines, compound operators, $(...)/backtick subshells, and sh -c scripts before calling ask().
    • Added scrub_env() stripping *_KEY, *_TOKEN, *_SECRET, and *PASSWORD* variables and setting TERM=dumb, PAGER=cat, GIT_PAGER=cat.
    • Added RunCommandTool with start_new_session=True, 60 s process-group timeout and < 1 s Esc cancellation (SIGTERM then SIGKILL + proc.wait()), and 4,000-head / 6,000-tail truncation at 10,000 chars.

How it was tested

  • Automated:
    • uv run pytest --cov=aldwin --cov-report=term-missing --cov-fail-under=90: 169 passed, 94.32% total coverage (tools/__init__.py 98%, memory_tools.py 100%, persona_tool.py 100%, web_search.py 99%, web_fetch.py 92%, run_command.py 92%).
    • uv run ruff check src tests and uv run ruff format --check src tests: clean.
    • uv run mypy src tests: 0 errors across 50 source files.
  • Manual:
    • Live web_fetch("https://docs.python.org/3/") (M-02): returned 1k chars wrapped in <untrusted source="https://docs.python.org/3/">.
    • Live run_command("df -h | head -n 5 && env") (M-03): returned exit 0 with disk usage and verified OPENAI_API_KEY was stripped from the child environment.

Notes for reviewers

  • Socket-level DNS pinning inside a custom httpcore network backend is deferred past 0.0.1 because rewriting the request host to a literal IP breaks TLS SNI and certificate hostname verification without custom stream overrides; per-hop getaddrinfo validation is enforced on the initial request and every redirect hop.

Folarin Akinloye added 2 commits October 18, 2026 10:22
Add SearchProvider, TavilyProvider, and WebSearchTool, WebFetchTool with SSRF and redirect validation, 2 MB stream cap, and <untrusted> wrapping, and RunCommandTool with pre-approval denylist, secret env scrubbing, process-group timeout, and 4k/6k output truncation. Wire builtin_tools() into build_agent() and map ToolOutput card summaries into ToolResultEvent.
@folathecoder
folathecoder merged commit ad9cdd2 into main Oct 18, 2026
4 checks passed
@folathecoder
folathecoder deleted the folathecoder/eng-382-6-built-in-tools-web-search-web-fetch-and-terminal branch October 18, 2026 09:38
@folathecoder folathecoder self-assigned this Oct 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant