Websearch LLMTool Plugin
Note
Experimental. The WebsearchPlugin was designed and coded by Claude Code (Anthropic’s Claude Opus 5.5), with Lawrence McDaniel as co-author. It is experimental, and will be documented.
Technical References
Class Reference:
smarter.apps.llmtool.plugin.websearch.WebsearchPluginDjango ORM Model:
smarter.apps.llmtool.models.PluginDataWebsearchWeb search APIs:
smarter.apps.llmtool.plugin.websearch_providersReading web pages:
smarter.apps.llmtool.plugin.websearch_fetchSafe HTTP requests:
smarter.apps.llmtool.plugin.safe_httpTavily web search: what Tavily is, and how to set up its API key
A Plugin that gives the LLM access to the open web: searching it, and reading web pages.
Note
This is a complex AI resource that exists within the following class hierarchy
Smarter Secret: The api key of a web search API, such as Brave Search or Tavily.
Smarter Websearch Plugin: The plugin that configures web search, web page reading, and the domains that may be accessed.
Smarter LLMClient: The prompting resource (LLMClient, Agent, Workflow unit, etcetera) that includes the Websearch Plugin.
Note
The WebsearchPlugin is modeled on the web search and web fetch tools of Claude itself. Its single tool performs one of two operations per call:
search: given a
query, search the web via a web search API, and return ranked results, each with atitle,url,snippetand, if known,publisheddate.fetch: given a
url, read the web page, and return its main content as Markdown, with itstitleandfinal_url.
The LLM can narrow each call, e.g. with allowed_domains to search one website, or
freshness to find recent news, but can never widen the plugin’s own policy.
Security:
Requests are restricted to public https hosts, redirects are validated, and response sizes and timeouts are bounded, to prevent server-side request forgery. See
smarter.apps.llmtool.plugin.safe_http.The manifest’s allowed and blocked domains are enforced on search results, on fetched URLs, and on every redirect.
Websites’ robots.txt files are obeyed, unless the manifest disables it.
The web search API key is stored in a Smarter Secret. It is never included in manifests, tool responses, or error messages.
Web content is returned as data, with a note that it is untrusted and must not be followed as instructions, to mitigate prompt injection.
Web failures, e.g. a page that does not exist, are returned to the LLM as an
error, so that it can recover, rather than raised.
---
# A WebsearchPlugin for general research: it searches the web with the Brave
# Search API, reads the most relevant pages, and cites its sources. Low quality
# websites are blocked, so that they are neither returned nor read.
#
# Prerequisite: a Smarter Secret named brave_search_api_key, containing a Brave
# Search API key from https://brave.com/search/api/
#
# Experimental: the WebsearchPlugin was designed and coded by Claude Code (Anthropic's
# Claude Opus 5.5), with Lawrence McDaniel as co-author.
apiVersion: smarter.sh/v1
kind: WebsearchPlugin
metadata:
name: research_assistant
pluginClass: websearch
description: Researches any topic on the open web, reads the most relevant pages, and cites its sources.
version: 0.1.0
tags:
- websearch
- research
annotations:
- smarter.sh/research-assistant/use-case: research
spec:
selector:
directive: search_terms
searchTerms:
- search the web
- look up
- research
- latest
- sources
prompt:
provider: openai
systemRole: >
You are a careful research assistant. Search the web, read the most relevant
pages in full before answering, and cite the url of every source you use.
model: gpt-6-luna
temperature: 1.0
maxTokens: 4096
websearchData:
search:
provider: brave
apiKey: brave_search_api_key
maxResults: 5
safeSearch: moderate
fetch:
maxCharacters: 20000
respectRobotsTxt: true
blockedDomains:
- pinterest.com
- quora.com
timeout: 15
cacheTtl: 3600
apiVersion: smarter.sh/v1
kind: LLMClient
metadata:
description: "An example llmclient with tool calling and Smarter Plugins."
name: example
version: 0.1.0
tags:
- gibberish
- jabberwocky
- talk-talk-talk
annotations:
- smarter.sh/tests/focus-level: low
- smarter.sh/tests/purpose: Provide an example configuration for OpenAI API Function Calling.
- smarter.sh/tests/last-updated: 2025-12-31
- smarter.sh/tests/documentation: https://docs.tests.edu/example-function-calling
- smarter.sh/tests/owner: Lawrence McDaniel
spec:
apiKey: null
config:
prompt:
appAssistant: Lawrence
appExamplePrompts:
- What is the weather in San Francisco?
- What is an Everlasting Gobstopper?
- example function calling configuration
appFileAttachment: false
appInfoUrl: https://smarter.sh
appName: Smarter Demo
appPlaceholder: Ask me anything...
appWelcomeMessage: Welcome to the Smarter demo!
customDomain: null
defaultMaxTokens: 4096
defaultModel: gpt-6-luna
defaultSystemRole: You are a helpful llmclient. When given the opportunity to utilize
function calling, you should always do so. This will allow you to provide the
best possible responses to the user. If you are unable to provide a response,
you should prompt the user for more information. If you are still unable to
provide a response, you should inform the user that you are unable to help them
at this time.
Your replies are displayed in a chat window that renders GitHub flavored markdown.
Use **bold** for key facts, bulleted lists for several items, and a small table when
you present structured data from a function. When a function returns
an image url, show the image with markdown image syntax, .
defaultTemperature: 1.0
deployed: false
provider: openai
subdomain: null
styles:
appLogoUrl: https://cdn.smarter.sh/images/logo/smarter-crop.png
appBackgroundImageUrl: https://cdn.smarter.sh/cms/img/techdots.png
assistantAvatarUrl: https://cdn.smarter.sh/images/llmclients/example/assistant.png
googleFont: Montserrat
borderRadius: 14px
conversationHeader:
backgroundColor: '#f15a29'
color: '#ffffff'
infoColor: '#ffe3d1'
messageList:
backgroundColor: rgba(255, 255, 255, 0.7)
color: '#3a2a1a'
incomingMessage:
backgroundColor: '#ffffff'
color: '#3a2a1a'
borderColor: '#f7c9b5'
outgoingMessage:
backgroundColor: '#fdb913'
color: '#3a2a00'
buttons:
color: '#f15a29'
typingIndicator:
color: '#f15a29'
functions:
- get_current_weather
plugins:
- everlasting_gobstopper
- example_configuration
- image_search
guardrails:
- prompt_injection_keyword_input
- moderation_input
- moderation_output
Note
Experimental. The WebsearchPlugin was designed and coded by Claude Code (Anthropic’s Claude Opus 5.5), with Lawrence McDaniel as co-author. It is experimental, and will be documented.
- exception smarter.apps.llmtool.plugin.websearch.SmarterWebsearchPluginError(message='')[source]
Bases:
SmarterPluginErrorBase class for all Websearch plugin errors.
- class smarter.apps.llmtool.plugin.websearch.WebsearchPlugin(*args, manifest=None, **kwargs)[source]
Bases:
PluginBaseImplements a plugin that searches the open web, and reads web pages.
Key Features:
Web search via a pluggable web search API: Brave Search or Tavily.
Reading web pages by URL, converted to Markdown, with robots.txt support.
Domain allow and block lists, which the LLM can narrow per call, but never widen.
Localization, safe search, and freshness of search results.
Caching of search results and web pages.
Signals for every search, fetch and failure, for observability and auditing.
Example Use Cases:
A research assistant that searches the web and cites its sources.
A documentation assistant restricted to a product’s own documentation website.
A news monitor that searches for recent news.
A reader that summarizes the web pages that users link to, without searching.
See also
PluginBasePluginDataWebsearchsmarter.apps.llmtool.plugin.websearch_providerssmarter.apps.llmtool.plugin.websearch_fetch- SAMPluginType
alias of
SAMWebsearchPlugin
- __init__(*args, manifest=None, **kwargs)[source]
Initialize a PluginBase instance.
Key features:
Lazy instantiation of class properties.
Supports multiple initialization methods including manifest, database model, and YAML/JSON.
Integrates with Django ORM for plugin metadata management.
Utilizes Pydantic models for manifest validation and serialization.
Provides hooks for plugin selection and prompt customization.
Ensures plugin readiness and validation before use.
Sends signals upon plugin creation or update.
This constructor supports several ways to create a plugin object:
Manifest-based initialization:
Pass a Pydantic model instance via the
manifestargument. This is the preferred method and is typically used when loading plugins from a manifest broker.Database model initialization:
Provide a Django model plugin ID via
plugin_idor aPluginMetainstance viaplugin_metato load an existing plugin from the database.YAML/JSON manifest initialization:
Supply a YAML or JSON string (or dictionary) via the
dataargument to create a plugin from a manifest representation.
See
./data/sample-plugins/everlasting-gobstopper.yamlfor an example manifest.The constructor sets up internal state, initializes plugin properties, and triggers plugin creation or update logic as needed. It also sends signals when the plugin is ready.
- Parameters:
user_profile (Optional[UserProfile]) – The user profile associated with the plugin.
selected (bool) – Whether the plugin is initially selected.
api_version (Optional[str]) – The API version for the plugin manifest.
manifest (
SAMWebsearchPlugin|None) – A Pydantic model representing the plugin manifest.plugin_id (Optional[int]) – The Django ORM plugin ID.
plugin_meta (Optional[PluginMeta]) – The Django ORM PluginMeta instance.
data (Union[SmarterDictType, str, None]) – YAML/JSON manifest as a string or dictionary.
name (Optional[str]) – The name of the plugin.
kwargs – Additional keyword arguments.
- property custom_tool: dict[str, Any] | None
Return the plugin tool definition for OpenAI function calling.
The parameters depend on the operations that the plugin performs. Each call must set exactly one of
queryorurl.- Returns:
The tool definition, or
Noneif the plugin is not ready.
Example:
tool = { "type": "function", "function": { "name": "smarter_plugin_0000000042", "description": "Research any topic. Set query to search the web ... Set url to read a web page ...", "parameters": { "type": "object", "properties": { "query": {"type": "string", "maxLength": 400, "description": "..."}, "max_results": {"type": "integer", "minimum": 1, "maximum": 5, "description": "..."}, "freshness": {"type": "string", "enum": ["day", "week", "month", "year"], "description": "..."}, "url": {"type": "string", "maxLength": 2048, "description": "..."}, "allowed_domains": {"type": "array", "items": {"type": "string"}, "description": "..."}, "blocked_domains": {"type": "array", "items": {"type": "string"}, "description": "..."}, }, "required": [], }, }, }
- error_response(operation, target, error)[source]
Return an error to the LLM, and send the websearch_failed signal.
- classmethod example_manifest(kwargs=None)[source]
Use Pydantic models to generate an example manifest for a WebsearchPlugin.
- Parameters:
kwargs (
dict[str,Any] |None) – Optional keyword arguments to customize the example manifest.- Return type:
- Returns:
An example manifest as a dictionary.
See Also:
SAMWebsearchPluginsmarter/apps/llmtool/data/sample-plugins/websearch-*.yamlfor more examples.
- fetch(url, policy)[source]
Read a web page.
- Parameters:
url (
str) – The URL of the page.policy (
DomainPolicy) – The domain policy for this fetch.
- Return type:
- Returns:
The page, or an error.
- property kind: str
Returns the kind identifier for this plugin.
- Returns:
WebsearchPlugin- Return type:
- property manifest: SAMWebsearchPlugin | None
Return the Pydantic model representation of the plugin manifest.
If the manifest has not been set but the plugin is in a ready state, it is reconstructed from the Django ORM data, using
to_json().- Returns:
The manifest, or
Noneif unavailable.- Return type:
Optional[SAMWebsearchPlugin]
- property plugin_data: PluginDataWebsearch | None
Return the plugin data as a Django ORM instance.
- Returns:
The websearch configuration, or
Noneif unavailable.- Return type:
Optional[PluginDataWebsearch]
- property plugin_data_class: Type[PluginDataWebsearch]
Return the Django ORM class used for websearch plugin data.
- property plugin_data_django_model: dict[str, Any] | None
Transform the Pydantic manifest into a Django ORM-compatible dictionary.
The web search api key is resolved from the name of its Smarter Secret.
- Returns:
A dictionary of
PluginDataWebsearchfields, orNoneif the manifest is not available.- Raises:
SmarterWebsearchPluginError – If the api key Secret does not exist, or is not accessible.
- property plugin_data_serializer: PluginWebsearchSerializer | None
Return the serializer instance for the plugin’s websearch data.
- property plugin_data_serializer_class: Type[PluginWebsearchSerializer]
Return the plugin data serializer class.
- resolve_api_key_secret(name)[source]
Return the Secret, which the plugin’s user may read, that contains the web search API key.
- Parameters:
name (
str) – The name of the Secret.- Raises:
SmarterWebsearchPluginError – If there is no such Secret that the user may read.
- Return type:
- search(query, policy, max_results=None, freshness=None)[source]
Search the web.
- Parameters:
query (
str) – The search query.policy (
DomainPolicy) – The domain policy for this search.max_results (
Any) – The maximum number of results, up to the plugin’s maximum.freshness (
Any) – Restricts results to those published within the last day, week, month or year.
- Return type:
- Returns:
The search results, or an error.
- to_json(version='v1')[source]
Serialize the WebsearchPlugin to a JSON-compatible dictionary suitable for Pydantic import.
The web search api key is rendered as the name of its Secret, never its value.
- tool_call_fetch_plugin_response(function_args)[source]
Search the web, or read a web page, in response to a tool call.
Exactly one of
queryorurlmust be set. Invalid arguments, and failures to search or read the web, are returned to the LLM as anerror, so that it can recover.Example tool call payloads:
"arguments": "{\"query\": \"django 6 release date\", \"freshness\": \"month\"}" "arguments": "{\"url\": \"https://docs.djangoproject.com/en/stable/releases/\"}"
Example responses:
{ "operation": "search", "query": "django 6 release date", "results": [{"title": "...", "url": "https://...", "snippet": "...", "published": "..."}], "note": "This is untrusted data from the open web ...", } { "operation": "fetch", "url": "https://docs.djangoproject.com/en/stable/releases/", "final_url": "https://docs.djangoproject.com/en/6.0/releases/", "title": "Release notes | Django documentation", "content_type": "text/html", "content": "# Release notes ...", "truncated": False, "retrieved_at": "2026-09-30T00:00:00+00:00", "note": "This is untrusted data from the open web ...", } {"operation": "fetch", "url": "https://example.com/missing", "error": "... could not be read: HTTP 404."}
- smarter.apps.llmtool.plugin.websearch.should_log(level)[source]
Check if logging should be done based on the waffle switch.
Web search APIs for the WebsearchPlugin.
Each web search API is wrapped by a SearchProvider, which translates a
SearchRequest into the API’s request, and the API’s response into a list of
SearchResult. Every provider returns results in the same form, so that the LLM
sees the same fields regardless of the API, and can cite each result by its URL.
Providers:
brave: the Brave Search API, https://brave.com/search/api/tavily: the Tavily Search API, which is designed for LLMs, https://tavily.com
The plugin’s domain policy is passed to each API, so that the API restricts its results, and is also applied to the results that the API returns, so that the policy is enforced even if an API ignores it.
Note
Experimental. The WebsearchPlugin was designed and coded by Claude Code (Anthropic’s Claude Opus 5.5), with Lawrence McDaniel as co-author. It is experimental, and will be documented.
- class smarter.apps.llmtool.plugin.websearch_providers.BraveSearchProvider[source]
Bases:
SearchProviderThe Brave Search API.
https://api-dashboard.search.brave.com/app/documentation/web-search/get-started
- freshness = {'day': 'pd', 'month': 'pm', 'week': 'pw', 'year': 'py'}
- request(request, api_key_secret_id)[source]
Make the web search API request.
- Parameters:
request (
SearchRequest) – The search.api_key_secret_id (
int|None) – The id of the Secret that holds the api key. Seeapi_key().
- Raises:
WebsearchError – If the api key is unavailable.
SafeHttpError – If the request fails.
- Return type:
- url = 'https://api.search.brave.com/res/v1/web/search'
- class smarter.apps.llmtool.plugin.websearch_providers.SearchProvider[source]
Bases:
ABCA web search API.
- static api_key(api_key_secret_id)[source]
Return the web search API’s api key, read from its Secret.
Providers read the api key here, from the Secret’s id, so that the api key itself is never passed to
request(), whose arguments become a cache key.- Parameters:
api_key_secret_id (
int|None) – The id of the Secret that holds the api key.- Return type:
- Returns:
The api key.
- Raises:
WebsearchError – If the Secret does not exist, or cannot be read.
- abstractmethod request(request, api_key_secret_id)[source]
Make the web search API request.
- Parameters:
request (
SearchRequest) – The search.api_key_secret_id (
int|None) – The id of the Secret that holds the api key. Seeapi_key().
- Raises:
WebsearchError – If the api key is unavailable.
SafeHttpError – If the request fails.
- Return type:
- search(request, api_key_secret_id, cache_ttl=0)[source]
Search the web.
The web search API’s response, from
request(), is cached forcache_ttlseconds, keyed on the provider, the search, and the api key Secret’s id, never on the api key itself. Failed requests are never cached.- Parameters:
request (
SearchRequest) – The search.api_key_secret_id (
int|None) – The id of the Secret that holds the web search API’s api key.cache_ttl (
int) – Seconds to cache the API response. 0 disables caching.
- Return type:
- Returns:
The results, which satisfy the request’s domain policy, and True if the API response was served from the cache.
- Raises:
WebsearchError – If the api key is unavailable, or the search fails.
- class smarter.apps.llmtool.plugin.websearch_providers.SearchRequest(query, max_results, policy=DomainPolicy(allowed=(), blocked=()), safe_search='moderate', country=None, language=None, freshness=None, timeout=15)[source]
Bases:
objectA web search.
- Variables:
query – The search query.
max_results – The maximum number of results.
policy – The domain policy that results must satisfy.
safe_search – Adult content filtering: off, moderate or strict.
country – An ISO 3166-1 alpha-2 country code, to localize results.
language – An ISO 639-1 language code, to localize results.
freshness – Restricts results to those published within the last day, week, month or year.
timeout – Seconds to wait for the web search API.
- __init__(query, max_results, policy=DomainPolicy(allowed=(), blocked=()), safe_search='moderate', country=None, language=None, freshness=None, timeout=15)
- policy: DomainPolicy = DomainPolicy(allowed=(), blocked=())
- class smarter.apps.llmtool.plugin.websearch_providers.SearchResult(title, url, snippet, published=None)[source]
Bases:
objectA web search result.
- Variables:
title – The title of the web page.
url – The URL of the web page, which the LLM should cite.
snippet – An excerpt of the web page that is relevant to the query.
published – When the web page was published or updated, as reported by the API, if known.
- __init__(title, url, snippet, published=None)
- class smarter.apps.llmtool.plugin.websearch_providers.TavilySearchProvider[source]
Bases:
SearchProviderThe Tavily Search API, which is designed for LLMs.
https://docs.tavily.com/documentation/api-reference/endpoint/search
Note
Tavily localizes results by country name rather than by country code, so the plugin’s
countryis not passed to Tavily.- request(request, api_key_secret_id)[source]
Make the web search API request.
- Parameters:
request (
SearchRequest) – The search.api_key_secret_id (
int|None) – The id of the Secret that holds the api key. Seeapi_key().
- Raises:
WebsearchError – If the api key is unavailable.
SafeHttpError – If the request fails.
- Return type:
- url = 'https://api.tavily.com/search'
- exception smarter.apps.llmtool.plugin.websearch_providers.WebsearchError(message='')[source]
Bases:
SmarterValueErrorRaised when a web search cannot be performed.
The message is safe to show to the LLM.
- smarter.apps.llmtool.plugin.websearch_providers.clean_text(value, max_length)[source]
Return a value as single-line text, truncated to a maximum length.
- Return type:
- smarter.apps.llmtool.plugin.websearch_providers.describe_http_error(provider, error)[source]
Describe a failed web search API request, without disclosing the api key.
- Return type:
- smarter.apps.llmtool.plugin.websearch_providers.get_provider(name)[source]
Return the web search API with the given name.
- Raises:
WebsearchError – If the name is not a supported web search API.
- Return type:
- smarter.apps.llmtool.plugin.websearch_providers.post_filter(results, request)[source]
Enforce the domain policy on search results, remove duplicate and non-http(s) URLs, and limit the number of results.
- Return type:
Reading web pages for the WebsearchPlugin.
fetch_page() downloads a web page by URL, and returns its main content as Markdown,
which is compact, and preserves the structure and links that an LLM needs to understand
and cite a page.
Behavior:
httpURLs are upgraded tohttps. Other schemes are rejected.The plugin’s domain policy is enforced on the URL, and on every redirect.
robots.txtis obeyed, per RFC 9309, unless the plugin disables it: if robots.txt is missing (4xx) the page may be read, and if it is unreachable (5xx, or a network error) the page may not.Only text formats are read: HTML, XHTML, plain text, Markdown, JSON, XML, RSS, Atom and CSV. Binary formats, e.g. PDF and images, are rejected.
HTML is converted to Markdown. Scripts, styles, forms and embedded media are removed. If the page has a
<main>or<article>element, only it is read. Otherwise page navigation, headers, footers and sidebars are removed. Links are made absolute, so that the LLM can cite, and read, them.Content longer than the plugin’s limit is truncated, at a word boundary.
All requests are made with smarter.apps.llmtool.plugin.safe_http.
Note
Experimental. The WebsearchPlugin was designed and coded by Claude Code (Anthropic’s Claude Opus 5.5), with Lawrence McDaniel as co-author. It is experimental, and will be documented.
- class smarter.apps.llmtool.plugin.websearch_fetch.FetchedPage(url, final_url, title, content, content_type, truncated=False, retrieved_at=<factory>, cached=False)[source]
Bases:
objectA web page.
- Variables:
url – The requested URL, after upgrading http to https.
final_url – The URL of the page that was read, after any redirects. The LLM should cite it.
title – The page title, if any.
content – The page’s main content, as Markdown for HTML, and as text for other formats.
content_type – The media type of the page, e.g.
text/html.truncated – True if the content was truncated.
retrieved_at – When the page was read.
cached – True if the page was served from the cache.
- __init__(url, final_url, title, content, content_type, truncated=False, retrieved_at=<factory>, cached=False)
- smarter.apps.llmtool.plugin.websearch_fetch.USER_AGENT_TOKEN = 'SmarterBot'
The user agent token that robots.txt rules are matched against.
- exception smarter.apps.llmtool.plugin.websearch_fetch.WebFetchError(message='')[source]
Bases:
SmarterValueErrorRaised when a web page cannot be read.
The message is safe to show to the LLM.
- smarter.apps.llmtool.plugin.websearch_fetch.collapse_whitespace(text)[source]
Collapse runs of whitespace to single spaces.
- Return type:
- smarter.apps.llmtool.plugin.websearch_fetch.decode(content, charset)[source]
Decode a response body, falling back to UTF-8.
- Return type:
- smarter.apps.llmtool.plugin.websearch_fetch.fetch_page(url, *, policy=DomainPolicy(allowed=(), blocked=()), max_characters=20000, timeout=15, respect_robots_txt=True, cache_ttl=60)[source]
Read a web page, and return its main content.
The HTTP response is cached for
cache_ttlseconds, keyed on the URL and on everything that decides whether its redirects may be followed: the domain policy andrespect_robots_txt. Failed requests are never cached.- Parameters:
url (
str) – The URL of the page.policy (
DomainPolicy) – The domain policy that the URL, and any redirects, must satisfy.max_characters (
int) – The maximum number of characters of content to return.timeout (
float) – Seconds to wait for each request.respect_robots_txt (
bool) – Whether to obey the website’s robots.txt.cache_ttl (
int) – Seconds to cache the HTTP response. 0 disables caching.
- Return type:
- Returns:
The page.
- Raises:
WebFetchError – If the page cannot be read. The message is safe to show to the LLM.
Example:
page = fetch_page("https://docs.python.org/3/library/json.html", max_characters=5000) page.title # 'json — JSON encoder and decoder — Python 3 documentation'
- smarter.apps.llmtool.plugin.websearch_fetch.html_to_markdown(html, base_url, encoding=None)[source]
Convert an HTML page to Markdown.
- smarter.apps.llmtool.plugin.websearch_fetch.make_redirect_policy(policy, respect_robots_txt, timeout)[source]
Return a safe_http redirect policy that enforces the domain policy and robots.txt on each redirect.
- smarter.apps.llmtool.plugin.websearch_fetch.normalize_url(url)[source]
Normalize a URL to read: upgrade http to https, and remove the fragment.
- Raises:
WebFetchError – If the value is not an http(s) URL with a host, or is too long.
- Return type:
- smarter.apps.llmtool.plugin.websearch_fetch.render(node, base_url, list_depth=0)[source]
Render an HTML node as Markdown.
- Return type:
- smarter.apps.llmtool.plugin.websearch_fetch.robots_permit(url, timeout)[source]
Return True if a website’s robots.txt permits the Smarter user agent to read a URL.
- Return type:
- smarter.apps.llmtool.plugin.websearch_fetch.robots_rules(url, timeout)[source]
Return the robots.txt rules of a URL’s website, or None if the website may not be read.
Per RFC 9309, a missing robots.txt (4xx) permits everything, and an unreachable robots.txt (5xx, or a network error) permits nothing. robots.txt files are cached for an hour.
- Return type:
- smarter.apps.llmtool.plugin.websearch_fetch.truncate(content, max_characters)[source]
Truncate content to a maximum number of characters, at a word boundary if possible.
Safe HTTP requests to user-supplied URLs on the open web.
Plugins such as the SkillPlugin and the WebsearchPlugin make HTTP requests to URLs that are supplied by manifest authors and, in the case of the WebsearchPlugin, by the LLM itself. Such requests must not be able to reach the Smarter platform’s own network, nor exhaust its resources. This module provides a single, hardened request function for them.
Protections:
https only. Plain http, and every other scheme, is rejected.
Public addresses only. The host must resolve, and every address that it resolves to must be public. This prevents server-side request forgery (SSRF) against loopback, private, link-local (e.g. cloud instance metadata at 169.254.169.254), shared and reserved addresses.
Standard port only. Only the default https port, 443, is permitted.
No credentials in URLs.
Validated redirects. Redirects are followed manually, up to a limit, and every hop is subject to the same validation, plus an optional caller-supplied policy, e.g. a domain allow list.
Bounded responses. Responses are streamed, and abandoned once they exceed a size limit.
Timeouts on every request.
Warning
Host names are resolved and validated immediately before each request, which leaves a small window for DNS rebinding. Deployments that require stronger guarantees should also route plugin traffic through an egress proxy that enforces the same policy.
Note
Experimental. This module was designed and coded by Claude Code (Anthropic’s Claude Opus 5.5), with Lawrence McDaniel as co-author. It is experimental, and will be documented.
- smarter.apps.llmtool.plugin.safe_http.DEFAULT_MAX_BYTES = 2000000
The default maximum size of a response body, in bytes.
- smarter.apps.llmtool.plugin.safe_http.DEFAULT_TIMEOUT = 15
Seconds to wait for each request.
- exception smarter.apps.llmtool.plugin.safe_http.SafeHttpError(message='', status_code=None)[source]
Bases:
SmarterValueErrorRaised when a URL is not permitted, or a request to it fails.
- Variables:
status_code – The HTTP status code, if the server responded with an unexpected status.
- class smarter.apps.llmtool.plugin.safe_http.SafeResponse(url, status_code, headers, content, redirects=<factory>)[source]
Bases:
objectThe response to a safe HTTP request.
- Variables:
url – The final URL, after any redirects.
status_code – The HTTP status code.
headers – The response headers, with lower case names.
content – The response body.
redirects – The URLs that were redirected from, in order.
- __init__(url, status_code, headers, content, redirects=<factory>)
- smarter.apps.llmtool.plugin.safe_http.fetch(url, *, method='GET', headers=None, params=None, json_body=None, timeout=15, max_bytes=2000000, max_redirects=3, redirect_policy=None, accept_status=(200,))[source]
Make a safe HTTP request.
- Parameters:
url (
str) – The https URL to request.method (
str) – The HTTP method.headers (
dict[str,str] |None) – Additional request headers. A User-Agent is added if absent.params (
dict[str,Any] |None) – URL query string parameters.json_body (
TypeAliasType|None) – A JSON request body.timeout (
float) – Seconds to wait for each request.max_bytes (
int) – The maximum size of the response body.max_redirects (
int) – The maximum number of redirects to follow.redirect_policy (
Callable[[str],None] |None) – Called with each redirect URL. It raises to refuse the redirect.accept_status (
tuple[int,...]) – The HTTP status codes that constitute success.
- Return type:
- Returns:
The response.
- Raises:
SafeHttpError – If a URL is not permitted, a request fails or times out, the response status is not accepted, or the response is too large.
Example:
response = fetch("https://example.com/", max_bytes=100_000) response.content_type # 'text/html'
Note
This function is deliberately not cached with
@cache_results, and must stay that way. Cache the expensive call that is made with it instead, in its caller, keyed on the caller’s own parameters and with the caller’s TTL, e.g.ImageSearchPlugin.brave_images().A brief experiment decorated its request with
@cache_results(timeout=PluginCacheTtl.DEFAULT), to cache every plugin’s remote calls in one place. Because this is the transport that every plugin shares, the cache sat beneath each plugin’s own, and silently overrode it:a manifest’s
cacheTtl: 0no longer disabled caching, and a plugin’s longer or shorter TTL was masked by the transport’s ten minutes;re-applying a remote SkillPlugin served the old skill, instead of retrieving the latest version;
each hop of a redirect was replayed from the cache, so a redirect loop was no longer requested, and detected, as it happened;
HEAD requests that validate image urls were answered from the cache, instead of confirming that each url is still available.
- smarter.apps.llmtool.plugin.safe_http.is_loopback_host(hostname)[source]
Return whether a host name is the local machine:
localhost, a subdomain of it, or a loopback address.The local hosts are those of
SmarterValidator.LOCAL_HOSTS, any subdomain oflocalhost, e.g.api.localhost, and any loopback address, e.g.::1. The name is not resolved, so this tells only what the name says.validate_public_url()refuses all of them. Callers that permit loopback hosts, e.g. the MCPClient in the local environment, use this to decide when to skip it.
- smarter.apps.llmtool.plugin.safe_http.validate_public_url(url)[source]
Validate that a URL is https, on the standard port, without credentials, and that every address its host resolves to is public.
- Parameters:
url (
str) – The URL to validate.- Raises:
SafeHttpError – If the URL is not permitted.
- Return type: