# Search Engine Apps and Builtin Extensions

> Canonical path: `specs/extension/search-engine-apps.md`
>
> Status: normative product and runtime specification.
>
> Related:
>
> - [`extension-platform.md`](./extension-platform.md)
> - [`../app/app-system.md`](../app/app-system.md)
> - [`../kb/knowledge-qa-runtime-sessions.md`](../kb/knowledge-qa-runtime-sessions.md)

## 1. Purpose

AgentTeam exposes each supported web search provider as one first-party App in the
App Marketplace. Installing and configuring an App makes its independently
named Builtin Extension available to authorized Agents. Search providers do not
share one `web-search` alias and do not use an External Extension HTTP binding.

The search engine App catalog contains:

| App ID | App slug and Extension name | Display name | Connection |
| --- | --- | --- | --- |
| `app_searxng` | `searxng` | SearXNG | Base URL and optional language |
| `app_brave_search` | `brave` | Brave Search | API key |
| `app_tavily` | `tavily` | Tavily Search | API key |
| `app_serper` | `serper` | Serper Search | API key |

Each App carries a local, repository-owned brand avatar under
`web/public/app-icons/<slug>.svg`. Marketplace and App detail surfaces prefer
that asset over a remote URL.

## 2. Product model

Search engine Apps are trusted first-party Product Apps. They use the existing
App and Service Actor ownership chain:

```text
App registry definition
  -> Workspace App installation
  -> one active service connection
  -> one active service_installation_extensions selection
  -> Agent Extension Catalog
```

The App is independently installable, configurable, disconnectable, and
uninstallable. Installing one search engine does not install, configure, enable,
or disable another. Multiple search engine Apps may be active in the same
Workspace because their Extension names are distinct.

The first-party App catalog must recognize the exact trusted ID/slug pairs
above. A user-created or database-spoofed App cannot acquire Product App
behavior by reusing only an ID, slug, display name, avatar, or Extension name.
The four slugs are reserved from new custom-App creation. During the first
upgrade that introduces these Apps, a pre-existing custom App that owns one of
the slugs keeps its immutable App ID and all dependent data, while its slug and
stored manifest slug are moved to
`<old-slug>-legacy-<first-16-hex-of-SHA256(app-id)>`. The canonical App can then
claim the reserved slug without overwriting the custom row.

Search Apps do not receive messages, join channels, subscribe to provider
events, or expose App Runtime callback/runner credentials. Their only runtime
capability is the declared Builtin Extension.

Migration rollback is non-destructive: it hides the App definitions and
disables their capabilities but retains versions, installations, selections,
and encrypted connections. Roll-forward re-enables the same rows and restores
the configured workspaces without requiring credentials to be entered again.

## 3. Extension identity and registration

Each provider owns one independent Builtin Definition and executor, following
the Gitea registration pattern:

```go
registry.RegisterBuiltin(Definition(), Executor, Revision)
```

The Definition domain and the default Agent-visible Extension name are the App
slug. Version and implementation revision are provider-owned. A behavior or
schema change that alters the immutable runtime contract requires the
corresponding version or revision change.

The commands are:

```text
agentteam exec searxng search '{"query":"...", "limit":5}'
agentteam exec brave search '{"query":"...", "limit":5}'
agentteam exec tavily search '{"query":"...", "limit":5}'
agentteam exec serper search '{"query":"...", "limit":5}'
```

The App capability pins the exact Definition domain, version, digest, default
name, and default-enabled state. App installation creates the default
selection. The selection becomes visible to Agents only while the App
installation, selection, App connection, and applicable Agent authorization
are all active.

## 4. Search action contract

Every provider exposes one Action named `search`. Provider
Definitions share schema-building and result-normalization code, but they do
not share identity or dispatch through a provider argument.

### 4.1 Input

```json
{
  "query": "AgentTeam extension platform",
  "limit": 5
}
```

The JSON Schema contract is:

- object with `additionalProperties: false`;
- `query` is required, trimmed, from 1 through 512 Unicode characters;
- `limit` is optional, integer, from 1 through 10;
- omitted `limit` means 5.

Provider, endpoint, credential, language, region, raw query parameters, headers,
and response mode are never Agent input.

### 4.2 Output

```json
{
  "results": [
    {
      "url": "https://example.com/page",
      "title": "Example",
      "snippet": "A bounded provider summary."
    }
  ]
}
```

The result contract is:

- `results` is always present and contains at most the effective limit;
- `url` is required and is an absolute HTTP or HTTPS URL;
- duplicate URLs are removed while preserving provider order;
- URL fragments are removed;
- `title` is bounded to 512 Unicode characters;
- `snippet` is bounded to 4096 Unicode characters;
- malformed entries are omitted;
- provider payloads, headers, status codes, credentials, connection IDs, and
  internal App identities are not returned.

`search` is `read`, idempotent, and non-destructive. Search results are
navigation hints, not trusted page evidence. Reading and citing page content
remains a separate consumer responsibility.

## 5. App connection management

Each installed search App owns exactly one active connection with the stable
external subject reference `default`. Zero active connections keeps the
Extension out of the effective Agent Catalog. More than one active connection
fails closed under the existing Extension invocation rule.

The App management API exposes a search-engine-specific connection resource:

```text
GET    /api/v1/workspaces/:workspaceId/app-installations/:installationId/search-engine-connection
PUT    /api/v1/workspaces/:workspaceId/app-installations/:installationId/search-engine-connection
DELETE /api/v1/workspaces/:workspaceId/app-installations/:installationId/search-engine-connection
POST   /api/v1/workspaces/:workspaceId/app-installations/:installationId/search-engine-connection:test
```

Only a current Workspace owner or administrator may use these routes. Every
mutation rechecks the exact trusted search App identity and current installation
under the App mutation lock.

`PUT` accepts provider-owned `config` and write-only `credentials` objects.
The server validates a closed schema selected from the trusted App identity:

| Provider | Non-secret config | Write-only credentials |
| --- | --- | --- |
| SearXNG | `baseUrl` required; `language` optional | none |
| Brave | none | `apiKey` |
| Tavily | none | `apiKey` |
| Serper | none | `apiKey` |

An omitted credential field preserves an existing secret during a non-secret
configuration update. An empty supplied secret is rejected. Connection
responses contain only non-secret config, active state, and per-field
`configured` booleans.

Credentials use the existing App AES-GCM envelope and
`AGENTTEAM_APP_CREDENTIALS_MASTER_KEY`; plaintext never enters App list/detail
responses, realtime events, logs, audit rows, Agent catalogs, Desktop, or CLI
environment. A credential-changing response is non-cacheable.

The test action uses the same provider adapter, timeout, response budgets, and
connection snapshot as normal invocation. It returns only success or a stable
sanitized error and does not expose search results.

## 6. Provider adapters

Builtin executors bind a fixed provider identity before accepting Agent input.
They pass the authenticated Workspace, Agent, Agent member, and resolved App
installation identity to the shared Extension-owned search runtime under
`internal/extension/providers/searchengine/runtime`. That runtime loads and validates the
App's exact active connection, decrypts credentials, performs one bounded
upstream request, and normalizes the provider response. Search provider network
code must not live under `internal/service`.

Provider wire contracts are:

- SearXNG: `GET <baseUrl>/search?q=...&format=json`; normalize
  `results[].url/title/content`.
- Brave: `GET https://api.search.brave.com/res/v1/web/search`; send
  `X-Subscription-Token`; normalize `web.results[].url/title/description`.
- Tavily: `POST https://api.tavily.com/search`; send bearer authentication;
  normalize `results[].url/title/content`.
- Serper: `POST https://google.serper.dev/search`; send `X-API-KEY`; normalize
  position-ordered `organic[].link/title/snippet`.

Public providers use fixed HTTPS origins. SearXNG is the only provider whose
origin is installation configuration because it is commonly self-hosted. Its
URL must have an HTTP or HTTPS scheme, a host, no userinfo, query, or fragment.
Redirects are disabled. Private-address egress is allowed only for this trusted
SearXNG adapter and only from the administrator-configured origin; no global
Extension or web-fetch SSRF exception is introduced.

All adapters enforce:

- five-second connect and fifteen-second total deadlines;
- one MiB raw response limit and the global Extension final response limit;
- no ambient proxy for credential-bearing requests;
- no cross-origin redirect;
- stable `rate_limited` classification for HTTP 429;
- stable `provider_auth_unavailable` for missing or invalid local credentials;
- sanitized `upstream_failure` for other transport, status, or decode failures.

Automatic replay is not required. The caller may retry the idempotent Action.

## 7. User interface

Marketplace renders one Integration card per search App with its local avatar,
provider display name, description, and normal App install/request state.

The installed App detail shows:

- connection state;
- the provider-owned configuration form;
- masked API-key state where applicable;
- Save, Test connection, and Disconnect actions;
- the existing Extension section with the provider's concrete Extension name.

The UI never asks the user to choose a shared alias and never sends provider
identity as a form field. Provider identity comes from the selected App.

## 8. Knowledge Q&A boundary

The Knowledge Q&A `knowledge_web_search` function is an internal MaaS tool, not
a Desktop MCP tool. Search engine App installation and Agent Extension
invocation do not change that internal runtime.

A Knowledge Q&A integration that consumes installed Extensions must select a
concrete Extension name such as `searxng` or `brave`; it must not introduce a
`web-search` alias, guess among multiple Apps, or impersonate an Agent. That
service-consumer authorization is outside this App/Agent Extension contract.

## 9. Verification

Required automated coverage includes:

- exact Product App ID/slug admission and spoof rejection;
- Marketplace visibility and local avatar selection for all four Apps;
- install/request/uninstall lifecycle and default Extension selection;
- connection read/write/delete authorization, encryption, masking, and secret
  preservation;
- zero/ambiguous connection fail-closed behavior;
- Definition digest, Catalog projection, help, input schema, and output schema
  for each concrete Extension name;
- adapter request fixtures and provider response normalization;
- timeout, response budget, redirect, rate-limit, auth, and malformed-response
  errors;
- immediate Catalog removal after disconnect, deselection, authorization
  removal, or uninstall;
- Desktop merged Exec help and invocation for each provider name.

Live provider credentials and live Internet calls are not part of the test
suite. Provider tests use bounded local HTTP fixtures.
