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 host host.fetch may reach, on the default HTTPS port. Without it the publish fails, because the code calls host.fetch. The installer sees this host under What it can reach.
  • settings declares one secret field. In the console it's a write-only password field; the value is encrypted and never shown again. required: true means the plugin isn't usable until it's filled in. Secrets can't have a default.

The handler.

  1. await host.settings.getSecret("apiKey") — the key is fetched into the sandbox only now, only for this call.
  2. input.maxResults is what the agent's web_search call asked for. Brave caps count at 20, so the plugin clamps it. (The platform caps results at 10 on the way back anyway.)
  3. host.fetch makes the request. It resolves for any HTTP status, so the plugin checks status and 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.
  4. 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

  1. Install Brave Search, open its page, paste the Brave API key.
  2. 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.fetch in 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.