Examples
Example: Brave Search
A bring-your-own-key web search provider — settings, secrets, egress and host.fetch.
Official plugin · web.search provider · one secret setting · egress api.search.brave.com
Brave Search lets an account run every agent's web_search tool on the Brave Search API with its own Brave
key. It is the canonical example of calling a vendor from a plugin: a secret setting, a declared host, and
host.fetch.
The code
// plugins/brave-search/index.ts
globalThis.manifest = {
egress: ["api.search.brave.com"],
settings: [
{
key: "apiKey",
title: "Brave API key",
description: "From https://api-dashboard.search.brave.com — the free tier works.",
type: "secret",
required: true,
},
],
};
globalThis.web.search = async (input) => {
const apiKey = await host.settings.getSecret("apiKey");
const count = Math.min(Math.max(input.maxResults, 1), 20); // Brave caps count at 20
const response = await host.fetch({
url: `https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(input.query)}&count=${count}`,
headers: { "X-Subscription-Token": apiKey, Accept: "application/json" },
});
if (response.status !== 200) throw new Error(`Brave search API returned ${response.status}`);
const parsed = JSON.parse(response.body) as { web?: { results?: { title?: string; url?: string; description?: string }[] } };
const results = (parsed.web?.results ?? [])
.filter((result) => result.title && result.url)
.slice(0, input.maxResults)
.map((result) => ({ title: result.title!, url: result.url!, snippet: result.description ?? "" }));
return { results };
};
Walkthrough
The manifest.
egress: ["api.search.brave.com"]— the only hosthost.fetchmay reach, on the default HTTPS port. Without it the publish fails, because the code callshost.fetch. The installer sees this host under What it can reach.settingsdeclares onesecretfield. In the console it's a write-only password field; the value is encrypted and never shown again.required: truemeans the plugin isn't usable until it's filled in. Secrets can't have adefault.
The handler.
await host.settings.getSecret("apiKey")— the key is fetched into the sandbox only now, only for this call.input.maxResultsis what the agent'sweb_searchcall asked for. Brave capscountat 20, so the plugin clamps it. (The platform caps results at 10 on the way back anyway.)host.fetchmakes the request. It resolves for any HTTP status, so the plugin checksstatusand throws on anything but 200 — the agent then sees a clear "provider failed" error. The platform won't fall back to its own engine behind the user's back.- It maps Brave's shape into
{ results: [{ title, url, snippet }] }and drops rows without a title or URL.
Permissions. None. host.fetch needs an egress entry, not a permission, and host.settings is always there.
Using it
- Install Brave Search, open its page, paste the Brave API key.
- Settings → Engines → Web search → choose Brave Search.
Every agent's web_search now goes through the user's own Brave account and quota.
Going further
- Add
web.fetchin the same plugin; it becomes a second, separately selectable capability. Wiki Web does both: Wikipedia's opensearch for search, a reader service for fetch — see Web providers. - Surface bad keys clearly: throw
new Error("Brave rejected the API key (401) — check the key in the plugin's settings")on a 401; that sentence is what the model (and then the user) reads.