Skip to main content
AgentOS provides 4 web tools with security-first design, including SSRF protection, timeout enforcement, and HTML-to-text conversion.

Available Tools

Multi-provider web search with automatic fallback.
query
string
required
Search query string
provider
string
Search provider: "tavily", "brave", or "duckduckgo" (default)
maxResults
number
Maximum results to return (default: 5)
results
array
Array of search results:
  • title (string) — result title
  • url (string) — result URL
  • content (string) — snippet or description
provider
string
Provider used: “tavily”, “brave”, or “duckduckgo”
Example:
const result = await trigger("tool::web_search", {
  query: "iii-engine documentation",
  provider: "brave",
  maxResults: 3
});

// => {
//   results: [
//     {
//       title: "iii-engine - Function orchestration framework",
//       url: "https://iii.dev/docs",
//       content: "iii-engine is a polyglot function orchestration framework..."
//     },
//     // ...
//   ],
//   provider: "brave"
// }
Provider Selection:
  1. Tavily — Requires TAVILY_API_KEY environment variable
    • Enterprise search API
    • Best for production use
    • POST to https://api.tavily.com/search
  2. Brave — Requires BRAVE_API_KEY environment variable
    • Fast, privacy-focused search
    • GET https://api.search.brave.com/res/v1/web/search
    • Includes web results, news, and images
  3. DuckDuckGo — No API key required (fallback)
    • Free instant answer API
    • GET https://api.duckduckgo.com/?format=json
    • Limited to “RelatedTopics” results
Implementation:
// src/tools.ts:336-418
if (provider === "tavily" && process.env.TAVILY_API_KEY) {
  const resp = await fetch("https://api.tavily.com/search", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      api_key: process.env.TAVILY_API_KEY,
      query,
      max_results: limit,
    }),
    signal: controller.signal,
  });
  const data = await resp.json();
  return { results: data.results || [], provider: "tavily" };
}

// Falls back to DuckDuckGo if no API keys configured

web_fetch

SSRF-protected HTTP fetch with HTML-to-text conversion.
url
string
required
URL to fetch
maxSize
number
Maximum response size in bytes (default: 500,000)
url
string
Requested URL
status
number
HTTP status code
contentType
string
Response content-type header
content
string
Response body (HTML stripped if content-type is text/html)
truncated
boolean
True if response exceeded maxSize
Example:
const result = await trigger("tool::web_fetch", {
  url: "https://example.com/api/data",
  maxSize: 100000
});

// => {
//   url: "https://example.com/api/data",
//   status: 200,
//   contentType: "application/json",
//   content: "{...}",
//   truncated: false
// }
HTML-to-Text Conversion: For text/html responses, AgentOS strips HTML tags and converts entities:
// src/tools.ts:594-605
function htmlToText(html: string): string {
  return html
    .replace(/<script[\s\S]*?<\/script>/gi, "")  // Remove scripts
    .replace(/<style[\s\S]*?<\/style>/gi, "")    // Remove styles
    .replace(/<[^>]+>/g, " ")                      // Strip tags
    .replace(/&nbsp;/g, " ")
    .replace(/&amp;/g, "&")
    .replace(/&lt;/g, "<")
    .replace(/&gt;/g, ">")
    .replace(/\s+/g, " ")                          // Normalize whitespace
    .trim();
}
Security:
  • SSRF Protection — All URLs validated by assertNoSsrf()
  • Timeout — 30-second abort controller
  • Size Limit — Response truncated at maxSize
  • User-Agent — Identifies as AgentOS/0.0.1

browser_screenshot

Capture screenshots using headless browser automation.
Browser tools require the browser.ts worker and Playwright/Puppeteer installation. See Browser Automation for setup.
url
string
required
URL to screenshot
viewport
object
Viewport dimensions: { width: number, height: number }
fullPage
boolean
Capture full scrollable page (default: false)
screenshot
string
Base64-encoded PNG image
url
string
Captured URL
Example:
const result = await trigger("tool::browser_screenshot", {
  url: "https://example.com",
  viewport: { width: 1920, height: 1080 },
  fullPage: true
});

// Save screenshot
const buffer = Buffer.from(result.screenshot, "base64");
await writeFile("screenshot.png", buffer);

browser_action

Perform browser automation actions (click, type, navigate).
action
string
required
Action to perform: "navigate", "click", "type", "wait"
url
string
URL for navigate action
selector
string
CSS selector for click/type actions
text
string
Text to type (for type action)
timeout
number
Action timeout in milliseconds
Example:
// Navigate to login page
await trigger("tool::browser_action", {
  action: "navigate",
  url: "https://app.example.com/login"
});

// Type username
await trigger("tool::browser_action", {
  action: "type",
  selector: "#username",
  text: "[email protected]"
});

// Click login button
await trigger("tool::browser_action", {
  action: "click",
  selector: "button[type=submit]"
});

SSRF Protection

All web tools validate URLs to prevent Server-Side Request Forgery attacks:
// src/shared/utils.ts
export async function assertNoSsrf(url: string): Promise<void> {
  const parsed = new URL(url);
  
  // Block non-HTTP(S) protocols
  if (!['http:', 'https:'].includes(parsed.protocol)) {
    throw new Error(`Protocol not allowed: ${parsed.protocol}`);
  }
  
  // Resolve hostname to IP
  const { address } = await dns.promises.lookup(parsed.hostname);
  
  // Block private/reserved IP ranges
  const BLOCKED_RANGES = [
    /^127\./,           // Loopback
    /^10\./,            // Private Class A
    /^172\.(1[6-9]|2\d|3[01])\./,  // Private Class B
    /^192\.168\./,      // Private Class C
    /^169\.254\./,      // Link-local
    /^0\./,             // Reserved
    /^::1$/,            // IPv6 loopback
    /^fe80:/,           // IPv6 link-local
  ];
  
  for (const range of BLOCKED_RANGES) {
    if (range.test(address)) {
      throw new Error(
        `SSRF protection: ${url} resolves to blocked IP ${address}`
      );
    }
  }
}
Blocked Requests:
// These will throw SSRF errors:
await trigger("tool::web_fetch", { url: "http://127.0.0.1:8080" });
await trigger("tool::web_fetch", { url: "http://169.254.169.254/metadata" });
await trigger("tool::web_fetch", { url: "http://localhost/admin" });
await trigger("tool::web_fetch", { url: "file:///etc/passwd" });
Allowed Requests:
// These pass SSRF validation:
await trigger("tool::web_fetch", { url: "https://api.github.com" });
await trigger("tool::web_fetch", { url: "https://docs.iii.dev" });

Rate Limiting & Timeouts

Timeout Enforcement

All web tools use AbortController for timeout management:
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 30_000);  // 30s

try {
  const resp = await fetch(url, {
    signal: controller.signal,
    headers: { "User-Agent": "AgentOS/0.0.1" }
  });
  // Process response...
} finally {
  clearTimeout(timer);
}

Size Limits

  • web_fetch — 500KB default, 100KB returned to agent (configurable)
  • web_search — 5 results default, max 20
  • HTML stripping — Reduces token overhead for LLM processing

Best Practices

For APIs returning JSON, web_fetch is more efficient than search:
// Fetch API directly
const { content } = await trigger("tool::web_fetch", {
  url: "https://api.github.com/repos/iii-hq/agentos"
});
const repo = JSON.parse(content);
Check HTTP status before parsing:
const result = await trigger("tool::web_fetch", {
  url: "https://api.example.com/data"
});

if (result.status !== 200) {
  throw new Error(`HTTP ${result.status}: ${result.content}`);
}

const data = JSON.parse(result.content);
For production use, configure Tavily or Brave API keys:
export TAVILY_API_KEY=tvly-...
export BRAVE_API_KEY=BSA...
DuckDuckGo’s instant answer API has limited coverage.

Common Patterns

Search + Fetch Pipeline

// 1. Search for relevant pages
const searchResult = await trigger("tool::web_search", {
  query: "iii-engine function registration",
  maxResults: 3
});

// 2. Fetch top result
const topUrl = searchResult.results[0].url;
const page = await trigger("tool::web_fetch", {
  url: topUrl,
  maxSize: 50000
});

// 3. Process content
console.log(page.content);

API Data Extraction

const { content, contentType } = await trigger("tool::web_fetch", {
  url: "https://api.example.com/v1/status"
});

if (contentType.includes("application/json")) {
  const data = JSON.parse(content);
  console.log("API Status:", data.status);
} else {
  console.warn("Unexpected content type:", contentType);
}

Screenshot for Visual Testing

// Capture before and after states
const before = await trigger("tool::browser_screenshot", {
  url: "https://staging.example.com",
  viewport: { width: 1920, height: 1080 }
});

// Make changes...

const after = await trigger("tool::browser_screenshot", {
  url: "https://staging.example.com",
  viewport: { width: 1920, height: 1080 }
});

// Compare screenshots
const diff = await compareImages(before.screenshot, after.screenshot);

Error Handling

try {
  const result = await trigger("tool::web_fetch", {
    url: "http://192.168.1.1"
  });
} catch (err) {
  console.error(err.message);
  // "SSRF protection: http://192.168.1.1 resolves to blocked IP 192.168.1.1"
}

try {
  const result = await trigger("tool::web_fetch", {
    url: "https://very-slow-api.com/data"
  });
} catch (err) {
  console.error(err.message);
  // "The operation was aborted" (timeout after 30s)
}

Build docs developers (and LLMs) love