# AgentTeam Extension Platform

> Canonical path: `specs/extension/extension-platform.md`
>
> Status: normative Package and runtime specification. Extension is an
> App-hosted capability exposed as flattened actions in the Agent's unified
> AgentTeam Exec Catalog.
> Gitea/GitHub register as Builtin Extensions; OpenAPI-derived Packages register
> as External Extensions. Agent-runtime path: `/ext/...`. Acceptance evidence
> is maintained in §16 and runs in the root, presets, and Desktop suites.
>
> Scope: OpenAPI-to-Package authoring rules, immutable Extension Definitions,
> App-hosted selection and server-side credentials, Agent authorization and
> discovery, Desktop unified Exec runtime catalogs, deterministic invocation,
> egress safety, and revocation.
>
> Related:
>
> - [`../app/app-system.md`](../app/app-system.md)
> - [`../service/service-actor-platform.md`](../service/service-actor-platform.md)
> - [`../daemon/runtime-outbound-tools.md`](../daemon/runtime-outbound-tools.md)
> - [`../daemon/mcp-tool-design-contract.md`](../daemon/mcp-tool-design-contract.md)
> - [`../daemon/desktop-architecture.md`](../daemon/desktop-architecture.md)
> - [`../api/server-profile-routing.md`](../api/server-profile-routing.md)
> - [`../deployment/workspace-gateway.md`](../deployment/workspace-gateway.md)

## 1. Purpose

AgentTeam Extensions turn reviewed external APIs into stable Agent actions. OpenAPI
is an optional authoring input, not a runtime contract. This specification
defines the smaller AgentTeam-owned Package that a future Agent produces: stable
names, curated JSON schemas, explicit HTTP bindings, safety metadata, and a
deterministic Definition digest.

The runtime does not parse OpenAPI and this repository does not ship a
dedicated OpenAPI authoring command. A generated Package crosses the
publication boundary only after the shared Package loader, validator,
normalizer, and digest verifier accept it.

An authorized Agent invokes an Extension through:

```text
agentteam exec <extension-name> <action> [<json|->]
```

There is no separate `agentteam extension` command. `agentteam exec` is a private runtime
command registered only inside daemon-launched Agent processes with a canonical
Exec base URL and absolute credential-file path. An empty Extension snapshot
still exposes the built-in Exec domains; a non-empty verified snapshot adds its
authorized Extension names to the same command Catalog.

External HTTP execution is server-side. Desktop and the Agent never receive
provider OAuth credentials or server-only HTTP bindings.

## 2. Goals and non-goals

### 2.1 Goals

- Define a deterministic, reviewable, versioned Package contract that an
  authoring Agent can derive from OpenAPI.
- Let authors constrain domain/action names and Agent-visible fields.
- Expose only Actions in the Server-projected Catalog for the current Agent.
- Accept one validated JSON value instead of per-field CLI flags or an
  HTTP-shaped path/query/body envelope.
- Keep OAuth and API credentials encrypted on AgentTeam Server.
- Make the outbound request deterministic for a fixed Definition digest,
  Action, and input.
- Apply revocation server-side immediately and reconcile Desktop through the
  existing daemon catalog channel.
- Support monolithic private/debug and split SaaS deployment with the same
  `agentteam-server` binary.

### 2.2 V1 non-goals

- Parsing or merging OpenAPI during invocation.
- Exposing arbitrary URLs, methods, paths, headers, or request templates.
- Exposing Extension Actions as MCP tools or as a separate public Desktop CLI.
- Sending provider access or refresh tokens to Desktop or Agent processes.
- Supporting binary responses, streaming, multipart requests, callbacks,
  webhooks, cookie authentication, mTLS, or arbitrary OAuth flows.
- Hot-patching an already running Agent process environment or prompt.
- Maintaining compatibility aliases for pre-release names or schemas.

## 3. Terms and ownership

| Term | Meaning | Owner |
| --- | --- | --- |
| OpenAPI source | Optional local file or HTTPS document used during Package authoring | Extension author or authoring Agent |
| Extension Definition | Immutable Spec-conforming Package identified by version and digest | Extension Library |
| Workspace Extension | A Definition installed in a workspace | Service Actor platform |
| Connection | Encrypted provider credential and connection metadata | AgentTeam Server |
| Agent Access | In V1, the user's accessible connected App Extensions, optionally narrowed by an explicit per-Agent authorization override | AgentTeam Server |
| Catalog | Agent-visible descriptions, schemas, examples, and safety | ProfileExtension |
| Binding | Server-only deterministic JSON-to-HTTP mapping | ProfileExtension |
| Agent Extension Catalog | The set of Extension Actions an Agent is currently authorized to use (persisted as a versioned, secret-free summary; the Desktop holds a process-generation snapshot of it) | AgentTeam Server |
| Catalog digest | Secret-free digest of the Agent's effective Extension Catalog | AgentTeam Server |

The ownership chain is:

```text
Authoring Agent
  reviewed OpenAPI + this Package Spec -> Extension Definition

AgentTeam Extension Library and App control plane
  Definition + App-hosted selection + connection + optional Agent override

agentteam-server ProfileExtension
  Catalog + authorization + binding + OAuth + egress

Desktop unified Exec RuntimeCatalog
  one process authorization + one credential + merged discovery/validation/forwarding

Agent
  Extension name + Action + validated JSON only
```

## 4. Extension Package Spec

### 4.1 Authoring and validation workflow

The normative authoring flow is:

```text
reviewed OpenAPI + Extension Package Spec
        -> authoring Agent creates a Package directory
        -> Human/repository review curates names, schemas, safety, and bindings
        -> publication validates, normalizes, and verifies the digest
        -> immutable Extension Registry
```

An authoring Agent must resolve the complete OpenAPI input before producing a
Package. References, server selection, operation security, parameter
serialization, request bodies, and response policies must be explicit in the
resulting files; the runtime never follows source references or fills gaps.

Publication validation is fail-closed. It loads the Package with strict YAML
fields, validates every JSON Schema and binding, normalizes order-independent
sets, recomputes the Definition digest, and accepts only an exact match. A
future upload API may additionally compare optional provenance with a supplied
OpenAPI document, but provenance never participates in invocation.

### 4.2 Package layout

One Extension is one directory:

```text
<external-definition-package>/github/
├── meta.yaml
├── source/
│   └── openapi.yaml       # optional authoring/provenance asset
└── actions/
    ├── create-issue.yaml
    ├── get-issue.yaml
    └── list-issues.yaml
```

`meta.yaml` explicitly lists every Action. A glob is not authoritative. Package
validation rejects:

- a listed file that does not exist;
- an unlisted Action file;
- duplicate names or paths;
- a filename that differs from the Action name;
- a meta name that differs from the Action file name;
- absolute paths, traversal, or symlink escape.

The required identity is:

```text
actions/<action-name>.yaml
meta action name == file basename == action file name
```

### 4.3 Metadata contract

A representative `meta.yaml` is:

```yaml
schema_version: 1
digest: sha256:3f5f2f2f7f8dc68f7f166fb496cc6150bf5760352450182fd32df0c7602f9e55

domain: github
version: 1
name: GitHub
description: GitHub REST actions curated for AgentTeam Agents.

source:
  type: openapi
  path: source/openapi.yaml
  openapi_version: 3.1.0
  document_version: "2026-08"
  sha256: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef

endpoint:
  base_url: https://api.github.com

auth:
  default: default
  profiles:
    - id: default
      type: oauth2_bearer

actions:
  - name: create-issue
    file: actions/create-issue.yaml
  - name: get-issue
    file: actions/get-issue.yaml
  - name: list-issues
    file: actions/list-issues.yaml
```

`schema_version` versions the file format. `version` versions the product
contract. They are independent.

`digest` is `sha256:` followed by the 64-character lowercase hex Definition
digest computed by the §4.6 projection. It is excluded from its own
canonical projection: it describes the package but never participates in the
hash that identifies it.

An authoring Agent does not silently increment `version`. Reauthoring identical
semantic input is byte-deterministic and preserves it. A changed Agent schema,
binding, auth requirement, safety classification, or response policy requires
an explicit version bump.

`source` is optional provenance. OpenAPI-derived metadata uses exactly one of
`path` or `url`; `sha256` is a lowercase 64-character hex digest of the
resolved source. Runtime treats `source` as display-only metadata: it does not
fetch, open, hash, or fail invocation because the source is absent or stale.
Source comparison belongs to authoring review or a future publication API.

`endpoint.base_url` is the one fixed V1 origin selected from OpenAPI `servers`.
It is an absolute HTTPS URL without credentials, query, fragment, or server
variables. Input cannot replace it.

Auth profiles describe credential slots, not credential values or local
credential refs. A profile carries only `{id, type, [in, name]}` — never scopes.
Per-Action OAuth scope requirements live on the Action's `auth` field (see
§4.4). `auth.default` is a display/manual default; the runtime never interprets
an absent Action `auth` as this default. Installation binds a profile to an
encrypted server-side connection. V1 supports `oauth2_bearer` and `api_key`
profiles; `api_key` supports `in: header` and `in: query` (cookie is rejected).

### 4.4 Action contract

One Action file contains an Agent-visible `catalog` and server-only `binding`:

```yaml
name: create-issue
source_operation_id: issues/create
auth:
  profile: default
  required_scopes:
    - repo

catalog:
  summary: Create an issue
  description: Create one issue in a repository.
  safety:
    class: mutation
    idempotent: false
    destructive: false

  input_schema:
    $schema: https://json-schema.org/draft/2020-12/schema
    type: object
    additionalProperties: false
    required:
      - owner
      - repo
      - title
    properties:
      owner:
        type: string
        minLength: 1
      repo:
        type: string
        minLength: 1
      title:
        type: string
        minLength: 1
        maxLength: 256
      description:
        type: string
        maxLength: 65536
      labels:
        type: array
        maxItems: 100
        items:
          type: string

  output_schema:
    $schema: https://json-schema.org/draft/2020-12/schema
    type: object

  example:
    owner: pingcap
    repo: agentteam
    title: Add Extension support
    description: Track the implementation.
    labels:
      - feature

binding:
  method: POST
  path: /repos/{owner}/{repo}/issues

  static_headers:
    Accept: application/vnd.github+json

  parameters:
    - from: /owner
      in: path
      name: owner
    - from: /repo
      in: path
      name: repo

  body:
    content_type: application/json
    mappings:
      - from: /title
        to: /title
      - from: /description
        to: /body
      - from: /labels
        to: /labels

  response:
    mode: passthrough
    success_statuses:
      - 201
    content_types:
      - application/json
```

The input schema is AgentTeam-owned and independent of the HTTP wire shape. Bindings
use RFC 6901 JSON Pointers and a closed target set. They are data, not scripts,
templates, expressions, or executable hooks.

The V1 binding method closed-set is `{GET, POST, PUT, PATCH, DELETE}`. `HEAD`,
`OPTIONS`, and `TRACE` are rejected: none produces a useful Agent data action in
V1 (`HEAD` omits the response body, `OPTIONS` is a CORS preflight, `TRACE` is a
diagnostic loopback). An authoring Agent must omit unsupported operations, and
the Definition validator rejects any Package carrying such a method.

`auth` is the per-Action requirement `{profile, required_scopes}`: which
credential profile to use and which OAuth scopes the operation needs. An omitted
(null) `auth` means EXPLICIT no-auth — the runtime never interprets an absent
`auth` as the meta-level `auth.default`, eliminating ambiguity. When deriving a
Package from OpenAPI, the authoring Agent resolves global security into EACH
Action: an operation that inherits the global default gets an explicit
`{profile}` written; an operation with `security: []` gets `auth` omitted.
`required_scopes` is non-empty only on `oauth2_bearer` profiles; `api_key` must
be empty. Two Actions may use the same profile with different scope
requirements (the profile is one credential slot bound once at install; scopes
are a per-Action requirement).

For an optional source pointer:

- absent means omit the parameter or body field;
- present `null` emits JSON null only into a body field whose schema permits
  null;
- path, query, and header parameters cannot serialize null.

Every path placeholder is required, bound exactly once, and percent-encoded as
one path segment. A nested path source (`/parent/child`) must be required at
every level along the pointer chain: the child of an optional parent would
never be reached, and an optional child of a required parent would leave the
placeholder unbound. Non-body parameters (path, query, header) whose source
schema allows `null` (via a `null` type or a null enum member) fail Package
validation — they cannot serialize null. Query and header values use an
explicit supported OpenAPI style. V1 supports common path `simple`, query
`form`, and header `simple` serialization. Unsupported styles fail Package
validation.

Two source fields cannot target the same HTTP location. One source value may be
copied to multiple non-secret targets only through explicit mappings. Body
target pointers cannot overlap ambiguously.

Reserved headers include:

```text
Authorization
Proxy-Authorization
Cookie
Host
Content-Length
Transfer-Encoding
Connection
```

The Agent cannot bind or override them. Auth injection runs after binding and
is the sole writer of provider credentials.

### 4.5 Naming and safety

Domain and Action names are lowercase kebab-case. Every built-in Exec domain is
reserved. An Extension name that collides with one rejects the entire Extension
snapshot before publication; Desktop never shadows a built-in domain, partially
merges a conflicting snapshot, or silently drops the conflict.

Action naming precedence is:

1. `x-agentteam-action`;
2. normalized OpenAPI `operationId`;
3. deterministic method/path fallback with a warning.

Duplicate normalized names fail Package validation.

Safety is a closed authorization and guidance contract:

```text
read
mutation
destructive
```

An authoring Agent may infer an initial value from HTTP semantics. Ambiguous
mutation methods must end with an explicit valid classification and never
default to `read`. `x-agentteam-safety` may select a valid class but cannot bypass
policy or silently downgrade a known destructive operation. The Action also
declares idempotency and destructive facts.

### 4.6 Canonical digest

Package normalization projects runtime-relevant metadata, catalogs, bindings,
auth requirements, and response policies, then computes one Definition digest.
Source location, fetch time, display provenance, YAML comments, and formatting
do not affect it.

The same semantic Package and digest-contract version produce the same
digest. One digest identifies exactly one immutable Definition version.

## 5. Control plane and authorization

### 5.1 Publication

Trusted Builtin Definitions are code-registered from
`internal/extension/providers` at bootstrap; the former
`presets/assets/extensions` embed is retired. Bootstrap registers each
definition's version and digest idempotently. A different payload cannot
overwrite an existing `(definition, version)`.

A future private publication surface accepts only a Spec-conforming Package.
It may ask an authoring Agent to derive that Package from uploaded OpenAPI, but
it never accepts raw OpenAPI for request-time translation.

`Builtin|External` is a server-local trusted registration kind — it is NOT a
field in `meta.yaml` (it is a runtime registry concern). An `External` Extension
loads an OpenAPI-derived Package through an `ExternalExecutor` whose binding is
HTTP. A `Builtin` Extension is a code-registered executor that shares the same
App-hosted selection, flattened Catalog, authoritative invocation, audit, and
discovery surfaces. Its digest-pinned connection policy is either `required`
(the backward-compatible zero/default) or `none` for a trusted connectionless
Builtin. Both kinds are gated by the same App authorization and review
controls.

### 5.2 App-hosted selection and connection

An Extension does not create a Service Definition, Installation, Connection,
or Authorization. Those identities belong to the host App and keep
`service_kind='app'`.

An App Definition declares which Extension Definitions it can expose. An App
Installation records the Workspace's active choices in
`service_installation_extensions`; each active row selects one immutable
Extension Definition and assigns one Agent-visible `extensionName`. Existing
For a connection-required Definition, App setup and connection flows store
encrypted provider credentials in the App's `service_connections` row. A
connection-none Definition creates no connection row.

OAuth/callback routes remain control-plane routes. Provider tokens never enter
Catalog, daemon WebSocket, Desktop, CLI environment, Agent input, help, logs,
or audit payload.

### 5.3 Agent authorization

The effective flattened Catalog is derived from:

```text
user-accessible host App
active App Installation and connection-policy satisfaction
active service_installation_extensions selection
the App Definition's closed Extension capability declaration
the selected immutable Extension Definition and Actions
optional explicit per-Agent authorization override
```

An active `extensionName` is unique within a Workspace. A conflicting default
selection remains disabled until a Human assigns an available name. Runtime
resolves the host App through the selected row and never guesses an
Installation, Connection, or credential.

V1 is default-allow for the user's Agents: if no per-Agent authorization row
exists, every Agent that belongs to the user-accessible Workspace sees the same
eligible connected App Extension. An explicit active row preserves access; an
explicit disabled row narrows it. The row is therefore an optional override,
not a prerequisite. Future Server policy may create or interpret these
overrides more strictly without changing Desktop: Desktop only consumes the
Server-projected snapshot and never evaluates App or Agent permission rules.

The App capability, connection policy/state, selection, Registry pin, and
optional override determine which Actions are visible. A broader provider OAuth
scope does not broaden the allowed Action set, and destructive Actions retain
their explicit safety policy. A connection-required Definition requires
exactly one active App connection; a connection-none Definition requires
exactly zero. Every mismatch fails closed before execution.

### 5.4 Catalog digest

The server computes a secret-free catalog digest over sorted:

```text
Extension name
Definition version and digest
allowed Action names
effective safety policy
Agent Extension Catalog version
```

The public digest contains only fields present in the Agent-visible Catalog, so
Desktop independently recomputes it before publishing a runtime snapshot.
Server-only authorization facets, including the authorizer member identity, are
persisted in a separate private policy digest. A change to either the visible
surface or the private policy digest advances the Catalog version; because the
version participates in the public digest, an authorizer-only change still
replaces the Agent process without exposing the authorizer identity.

Credential bytes, token expiry, refresh, transient health, and source metadata
are excluded. They do not restart an Agent. An authorization write that does
not change either the effective catalog or its private authorization policy is
a no-op: it does not advance the catalog version or emit a new digest. A
never-enabled Agent (catalog version 0) carries an empty sentinel digest; a
version > 0 carries an exact `extc_`+32hex digest (a last-revoke empty catalog
uses the deterministic empty-catalog digest at that version).

### 5.5 Built-in Extensions (Gitea/GitHub)

Gitea and GitHub ship as **Built-in Extensions**: their Definitions are registered
in the Extension Catalog/runtime/CLI like any OpenAPI Extension, but their
Actions are served in-process by the existing domain handlers (Builtin transport
`internal_handler`), not over HTTP.

**Reuse, no copy** — the existing `app_github`/`app_gitea` Definition,
installation, connection, credential, Agent authorization, and PR-watch rows on
the Service Actor substrate (`service_*`/`service_event_*`) are the canonical
identity. The Extension framework recognizes `service_kind='app'` (slug
`github`/`gitea`) as an Extension host; it does **not** copy rows into
`service_kind='extension'` or flip the `service_kind`.

**Preserved surfaces** — the existing Human setup, OAuth flow, webhook ingress,
connection management, and PR-watch backend APIs + URLs + wire contracts are all
retained. Only the Agent-runtime invocation path moves to
`/ext/{extensionName}/...`;
the Extension Action dispatches to the same in-process domain handler.

**Commands** — the legacy built-in `pr-watch` Exec domain is removed;
Gitea/GitHub Actions are invoked via `agentteam exec gitea|github <action>`. The
first batch migrates the real existing Actions: `watch-pull-request`,
`list-watches`, `pause-watch`, `resume-watch`, `unwatch`.

**Scope boundary** — this stage covers the Catalog/runtime/CLI unification and
the canonical-read cutover. Provider setup is canonical in
`service_provider_configs` and `service_provider_workspace_claims`; the App
facade does not read the retired `gitea_app_config` or `github_app_config`
tables.

### 5.6 Search engine Builtin Extensions

SearXNG, Brave Search, Tavily, and Serper are independently installed
first-party Apps and independently registered Builtin Extensions. Their
concrete names, shared `search` Action contract, App connection lifecycle,
provider adapters, avatars, and egress rules are defined by
[`search-engine-apps.md`](./search-engine-apps.md).

Search engine Extensions do not share a `web-search` alias. Each command uses
the concrete provider name, such as `agentteam exec searxng search`.

### 5.7 arXiv connectionless Builtin

The first-party `app_arxiv` Product App exposes a connection-none Builtin named
`arxiv`. It has only `search` and `get` read Actions. Paper results include PDF
and source URLs; AgentTeam Server does not download or stage those files. Its
catalog, Atom parsing, query, error, migration, and compatibility contracts are
defined by [`arxiv-app.md`](./arxiv-app.md).

### 5.8 OpenConnector-migrated provider Builtins

REST providers ported from OpenConnector live under
`internal/extension/providers/<domain>` and are composed through the
`extensions.Extension` assembly: the Agent-visible `Definition`, the trusted
`runtime.Implementation` (executor, revision, connection policy, auth contract,
credential validator), and the server-only `Distribution` (product App mapping).
`providers.Extensions(deps)` aggregates the full set and
`bootstrap.Register` validates catalog/executor/auth/distribution consistency
and registers every Builtin in one fail-closed startup pass. Distribution never
enters the registry digest.

The first batch ports three providers from upstream baseline
`0484cac3027025afdb3137f1ad1b58b9370c43d1` (`providers.OpenConnectorBaseline`;
bump it with each migration batch): `todoist` (Bearer token, cursor pagination),
`notion` (integration-secret Bearer plus `Notion-Version` header), and
`hackernews` (no-auth, connectionless, two fixed public origins). Upstream
snake_case action names are ported as kebab-case to satisfy the V1 naming
contract; every input/output wire property keeps the upstream field name and
shape verbatim (including explicit-null update semantics), and provider tables
cover the full upstream action set at the pinned baseline. Deviations are
fail-closed AgentTeam-side additions only (for example the hackernews tree budget)
and are documented in the provider package.

Migrated providers authenticate with a single-secret connection credential
collected by the App connection flow:

- `GET/PUT/DELETE /api/v1/workspaces/:workspaceId/app-installations/:installationId/extension-connection`
  and `POST .../extension-connection:test` manage it; PUT and :test live-check
  the credential against the provider before sealing it under AES-GCM in the
  App's canonical `service_connections` row.
- Executors resolve the decrypted credential at invoke time through the runtime
  credential seam; a missing connection classifies as
  `provider_auth_unavailable`, never as a silent skip.
- A provider whose static execution path depends on connection validation may
  return one small code-owned, non-secret config (for example a fixed topology
  enum). The provider declares the exact key and complete canonical ASCII value
  enum at startup. The server exact-validates it before persistence, after DB
  decode, and again at runtime resolution, and stores it separately from
  encrypted credentials. Browser payloads cannot supply or read it. Executors
  exhaustive-switch declared values only to choose among pre-registered static
  clients; they never interpret a value as an endpoint, tool, resource, or
  session. Discovery, session data, profiles, and secret-derived metadata
  remain forbidden.
- Provider egress uses the shared guarded transport (SSRF-safe dialer, fixed
  https origin confinement, no redirects, bounded response size) — the same
  guard family as External execution.

Generic connection-form discoverability is a separate, server-owned capability.
`Extension.Distribution` carries an explicit `GenericProviderConnection`
opt-in; its zero value is closed, and having a Definition domain or a
connection-required Implementation does not enable the browser form. An
opted-in registration must also carry a supported non-empty auth contract and
exactly one credential validator or connection preparer; declaring both or
neither fails provider aggregation at startup.

The trusted `ExtensionsByAppID` index derives the capability domain only from
the registered `Definition.Domain`. App records, installation configuration,
and browser input cannot override it. App definition and installation
responses reuse one projection helper and expose only
`providerConnection: {domain}`. The Web client strictly validates that object,
prefers installation capability, and falls back to definition capability only
for payload compatibility; absent or malformed capability stays closed.

The form remains gated by an active installation and Workspace administrator
authority. Its dynamic fields, configured/write-only secret state, optimistic
CAS, live probe, and disconnect behavior remain owned by the existing
installation-scoped backend contract; Web does not copy credential schemas.
The exact current opt-in set is Feishu Custom Bot, DingTalk Bot, WeCom Bot,
Tianyancha, Qichacha, Tushare, Alpha Vantage, Financial Modeling Prep, Jin10,
PKULaw, and Wind AIFin Market.

OAuth provider support (authorize/callback/refresh) is reserved: the auth
contract type carries the extension point, but no OAuth engine ships in this
batch.

### 5.9 MCP-backed Builtin Extensions

MCP-backed providers use the shared one-shot Streamable HTTP client under
`internal/extension/runner/mcp`; providers do not import the upstream MCP SDK
directly. The runtime pins `github.com/modelcontextprotocol/go-sdk` to the
reviewed release, constructs a fresh SDK client/session per invocation, disables
standalone SSE and automatic retries/multi-round-trip behavior, advertises no
client capabilities, and accepts only the protocol versions covered by its wire
suite.

Every MCP client definition has constructor-time exact allowlists for tool names
and resource URIs. An invocation may call only one allowlisted operation, so a
provider cannot accidentally turn Agent input into a generic `call-tool`,
`read-resource`, or `list-tools` catalog surface. SDK and JSON-RPC types remain
private to the runtime wrapper.

Connection validation may use a runtime `ProbeOnly` client when no reviewed
read-only tool/resource exists. It has no tool/resource authority and performs
only Connect, initialize/protocol validation, Close, and detached DELETE
cleanup; it never lists, calls, or reads a server capability. Multi-endpoint
providers must probe the complete fixed endpoint set required by the selected
static topology.

The initially reviewed dependency is Go SDK `v1.7.0`. Its repository is in an
Apache-2.0/MIT license transition and its documentation is CC-BY-4.0; it must
not be recorded as MIT-only, and upgrades rerun the same wire and import
boundary suite.

The injected HTTP stack binds one exact HTTPS endpoint before applying
connection-scoped credentials, rejects redirects and standalone GET, uses the
shared guarded DNS/dial policy, and accumulates all handshake and operation body
bytes against one invocation budget. Session cleanup is attempted on successful
calls and Connect failures; DELETE has a detached short timeout and an
independent small budget so exhausted/canceled invocation work cannot suppress
cleanup. HTTP, JSON-RPC, tool-envelope, timeout, and malformed-frame failures
map to the existing closed runtime error taxonomy without retaining raw SDK,
endpoint, session, credential, or upstream-body errors.

The reviewed Jin10, PKULaw, and Wind AIFin Market MCP catalog remains
evidence-bounded:

- Jin10 exposes only its reviewed static catalog over one fixed endpoint.
- PKULaw exposes five of the seven upstream operations as named read Actions:
  the five exact legal-research tool contracts are pinned, while dynamic
  `list_tools` and generic `call_tool` are excluded. Connection preparation
  probes the aggregate endpoint first and otherwise requires all four fixed
  fallback endpoints, persisting only `topology=aggregate|fallback`.
- Wind AIFin Market exposes thirteen of the thirty-four upstream operations
  over five fixed endpoints. Its reviewed code contracts pin endpoint, tool,
  input transform, and Bearer transport only. No audit credential was
  available, so the provider returns a strict opaque `{result: JSON value}`
  envelope and does not claim verified business output fields, resources, or
  OAuth state.
- Yingmi remains absent rather than partially registered: the reviewed public
  material does not provide per-tool schemas, a complete account-console
  endpoint, or audit credentials. No provider, App, migration, or catalog
  placeholder is created until one of those versioned machine-readable
  contracts or controlled audit inputs becomes available.

## 6. Server profile and runtime routes

### 6.1 Profiles

AgentTeam keeps one `agentteam-server` binary and adds:

```text
AGENTTEAM_SERVER_PROFILE=extension
```

Supported profiles become:

```text
all
central
workspace
extension
```

`all` registers central, workspace, and Extension routes for private/debug
deployments. `extension` registers only Extension runtime plus common
health/observability routes. It does not register Web, IM, daemon WebSocket,
LLM, workflow, or ordinary Workspace routes.

Background-agentteam selection uses an explicit profile switch. Extension profile
must not inherit Workspace workers merely because it is not central.

### 6.2 Agent Runtime API

All Extension runtime URL paths use `ext`, not `extensions`:

```http
GET  /api/v1/workspaces/{workspaceId}/agents/{agentId}/runtime/ext
GET  /api/v1/workspaces/{workspaceId}/agents/{agentId}/runtime/ext/{domain}
GET  /api/v1/workspaces/{workspaceId}/agents/{agentId}/runtime/ext/{domain}/actions/{action}
POST /api/v1/workspaces/{workspaceId}/agents/{agentId}/runtime/ext/{domain}/actions/{action}
```

The root GET returns authorized domains and bounded Action summaries. Domain
GET returns one Action list. Action GET returns the Catalog descriptor,
schemas, and example. POST invokes the Action.

Domain and Action are path identities. There is no generic body router with
caller-controlled capability, URL, or method.

Routes authenticate the existing Agent Runtime principal and bind path
Workspace/Agent before reading a mutation body.

### 6.3 Catalog

A representative root response is:

```json
{
  "catalogVersion": 7,
  "catalogDigest": "extc_...",
  "domains": [
    {
      "domain": "github",
      "displayName": "GitHub",
      "definitionVersion": 1,
      "definitionDigest": "sha256:...",
      "actions": [
        {
          "name": "create-issue",
          "summary": "Create an issue",
          "safety": {
            "class": "mutation",
            "idempotent": false,
            "destructive": false
          }
        }
      ]
    }
  ]
}
```

Catalog never contains a base URL, provider connection identity, credential
binding, HTTP binding, or secret.

### 6.4 Invocation envelope

The Agent supplies only validated Action input. Desktop forwards a minimal
private transport envelope:

```json
{
  "input": {
    "owner": "pingcap",
    "repo": "agentteam",
    "title": "Add Extension support"
  }
}
```

Definition identity, catalog policy, provider connection, credentials, and
authorization decisions are resolved server-side from the authenticated Agent
and the current catalog. Desktop does not forward conversation routing context
or an ambient target. An Action that writes to a AgentTeam target must carry that
target in its own typed input and the owning service reauthorizes it. The server
rejects stale Definition or policy with:

```text
agent_extension_catalog_stale
```

It never validates against an old schema and executes a new binding.

### 6.5 Result and errors

Successful JSON preserves the strategy result as `data` in the bounded action
envelope with domain and Action. It excludes upstream status, raw provider
headers, credential-bearing URLs, credentials, connection IDs, and internal
App authorization or selection IDs.

Errors include:

```text
validation
permission_denied
not_found
conflict
agent_extension_catalog_stale
provider_auth_unavailable
rate_limited
upstream_failure
internal
```

CLI failures always print the structured JSON envelope containing this stable
`error.code`. Exit status is only a coarse shell classification:
`provider_auth_unavailable` and `upstream_failure` both use 69, so Agents and
automation distinguish them by `error.code`, never by exit status alone.

## 7. Gateway routing

Split deployment configures:

```text
AGENTTEAM_GATEWAY_EXTENSION_UPSTREAM_URL=http://loop-server-extension:3002
```

Gateway adds an Extension family matching only:

```text
/api/v1/workspaces/{workspaceId}/agents/{agentId}/runtime/ext
```

and valid descendants.

Classification order is:

```text
internal-only
Extension runtime
central
Workspace
unknown
```

This prevents the generic Workspace Agent family from taking the route.
Gateway forwards the path unchanged. Route Workspace headers remain routing
hints; ProfileExtension re-authenticates Workspace and Agent.

Gateway readiness includes the Extension upstream and never exposes its URL in
errors.

`cmd/agentteam-route-contract` builds central, workspace, and Extension routers. CI
asserts:

- `/runtime/ext` belongs to `ProfileExtension`;
- aggregate `all` includes it;
- central/workspace do not register it;
- Gateway classifies every valid Extension route correctly;
- unknown descendants fail closed.

## 8. Authorization change notification

### 8.1 Transaction and catalog updates

An Agent authorization mutation atomically:

1. writes principal authorization;
2. increments the Agent Extension Catalog version;
3. computes the new catalog digest;
4. writes a durable catalog update event `agent_extension_catalog_changed`.

The worker resolves the Agent machine and reuses:

```text
Hub.SyncMachineCatalog(machineID)
  -> daemon WebSocket machine Agent catalog
```

No second Desktop/server channel is introduced. The agent-runtime Extension
invocation path is `/ext/...`.

### 8.2 Bounded hint

Agent catalog contains only:

```json
{
  "extensionCatalog": {
    "enabled": true,
    "version": 7,
    "catalogDigest": "extc_..."
  }
}
```

It contains no schemas, Definitions, bindings, or credentials.

The catalog update ledger retries failed/offline push. Reconnect and later
`agent:start` receive authoritative state, so correctness does not depend on
every notification.

## 9. Desktop unified Exec runtime

### 9.1 Snapshot

On a new catalog digest, Desktop fetches authorized Catalog and verifies policy
and catalog digest. It stores an immutable Agent-visible snapshot for one process
generation. It does not store OpenAPI, OAuth, or server binding.

### 9.2 Conditional registration

For every authorized Agent runtime, Desktop injects one Exec capability:

```text
AGENTTEAM_EXEC_BASE_URL=http://127.0.0.1:<daemon>/api/internal/exec/servers/{serverId}/agents/{agentId}
AGENTTEAM_EXEC_TOKEN_FILE=/absolute/private/runtime/path/token
```

There is no raw `AGENTTEAM_EXEC_TOKEN` fallback and no `AGENTTEAM_EXTENSION_*`
environment family.

CLI registration depends only on the canonical Exec runtime environment:

```go
if execRuntimeEnvAvailable(os.Getenv) {
    rootCmd.AddCommand(execCmd)
}
```

Therefore:

- a normal terminal has no `exec` in `agentteam --help`;
- a daemon-launched Agent always gets built-in `agentteam exec` actions even when
  its Extension snapshot is empty;
- an Agent with a non-empty verified snapshot additionally gets
  `agentteam exec <extension-name> <action>`;
- `agentteam extension` never exists.

### 9.3 Local credential lifecycle

Desktop owns one `RuntimeAuthorizationStore` and one
`RuntimeCredentialLease` per process authorization. The credential is bound to
server, Agent, session, process generation, and immutable `RuntimeCatalog`
(including Extension Catalog version/digest); published only in an
owner-private absolute file; rotated atomically; and revoked before cleanup on
replacement, exit, launch failure, or shutdown. It authorizes built-in and
Extension routes through the same local bearer but never contains or grants a
provider credential.

### 9.4 Discovery

The local merged Catalog is available at:

```text
GET /api/internal/exec/servers/{serverId}/agents/{agentId}/catalog
```

`agentteam exec --help` renders separate **Built-in actions** and **Authorized
Extensions** groups from this Catalog. Hierarchical help and schemas use the
same syntax:

```text
agentteam exec <domain-or-extension> --help
agentteam exec <domain-or-extension> <action> --help
agentteam exec <domain-or-extension> <action> --input-schema
agentteam exec <domain-or-extension> <action> --output-schema
```

The System Prompt likewise compiles two persistent sections:
`platform.agentteam.exec` for built-in actions and `platform.agentteam.extension` for a
non-empty authorized Extension snapshot. Both sections instruct the same
`agentteam exec` syntax. Keeping the sections separate preserves ownership and
catalog-change fingerprinting without creating a second command or credential.
Unauthorized Extension names cannot be discovered by probing.

### 9.5 Replacement

The catalog digest participates in runtime start/resume policy. A changed
Extension Catalog requires controlled process replacement and fresh-thread
cutover before the next turn because environment/prompt cannot be safely
updated in place.

The cutover replaces runtime process/thread state, not durable AgentTeam
conversation. The next process receives the same persisted history through the
normal launch input path.

Replacement revokes the process's one Exec authorization before publishing its
successor. Built-in Exec calls from that old process therefore fail during the
brief replacement window together with Extension calls; the new process
restores built-in access even when the replacement Extension Catalog is empty.
This momentary interruption is the intentional V1 semantic of a single
process-scoped capability, not a separate built-in outage state.

Token refresh and transient connection health do not change the catalog digest.

## 10. Invocation flow

One invocation is:

```text
Agent
  -> agentteam exec <extension-name> <action> <json>
  -> CLI reads local Exec token file
  -> Desktop authenticates process generation and RuntimeCatalog
  -> Desktop enforces 1 MiB input cap and validates snapshot schema
  -> Desktop injects Definition digest and catalog version
  -> Gateway routes /runtime/ext to ProfileExtension
  -> ProfileExtension re-authenticates Workspace and Agent
  -> server resolves active selection and rechecks host App Definition,
     Installation, connection policy/state, authorization, Action, and digest
  -> server validates the same AgentTeam input schema
  -> immutable binding creates outbound request
  -> server resolves or refreshes provider OAuth
  -> guarded client executes against fixed origin
  -> response is normalized and budgeted
  -> stable result/error returns to Agent
```

Desktop validation gives fast feedback; server validation, authorization, and
the digest-pinned connection-policy check are authoritative. Catalog identity
does not encode provider connection count, so even a freshly fetched Desktop
snapshot cannot bypass a later required/none connection-count mismatch.

## 11. Revocation and changes

### 11.1 Revocation

Every POST rechecks authorization. A committed revocation blocks new calls even
when Desktop is offline or notification is delayed.

When Desktop receives the catalog digest, it revokes the old process
authorization before fetching or publishing a replacement. Until replacement
the old process may still see its command help, but built-in and Extension calls
both fail closed because they share the one authorization. Replacement
publishes the same Exec environment with a new credential and `RuntimeCatalog`;
the Extension prompt section is added, changed, or removed atomically with that
Catalog.

An external mutation already dispatched cannot be undone. Server may cancel
and fence result publication, but provider side effects require
Action-specific idempotency, audit, and reconciliation.

### 11.2 Selection, App authorization, or Definition update

An old process cannot gain a new environment variable. A newly enabled
selection, changed App authorization, rename, or Definition digest is visible
only after Desktop gets the new snapshot and replaces the process before the
next turn.

The server never executes old input under a new binding. Stale digest returns
`agent_extension_catalog_stale`.

## 12. Determinism

For fixed:

```text
Definition digest
domain
Action
validated JSON input
```

the binder produces byte-identical:

```text
HTTP method
normalized URL
query ordering and encoding
non-secret headers
canonical JSON body
response policy
```

Only injected secret bytes may differ. Binding cannot read clock, randomness,
environment, filesystem, network discovery, or mutable runtime config.

Package digest/binding fixtures and server tests assert this. Audit hashes use
the deterministic bound request with secrets removed, so equivalent calls share
a hash without storing body plaintext.

## 13. OAuth and egress security

### 13.1 Credential ownership

Provider access/refresh tokens stay encrypted on AgentTeam Server. Refresh is
singleflight per connection. Provider `401` may refresh once and replay the
same rejected request once; ordinary network errors do not auto-replay a
non-idempotent mutation.

### 13.2 Closed egress engine

ProfileExtension is not an open proxy. Caller cannot control:

- scheme, origin, port, or base path;
- method or raw path template;
- auth profile or connection;
- Host, Authorization, Cookie, proxy, connection, or framing headers;
- redirect policy or result limits.

All outbound facts come from one immutable registered ActionPlan.

### 13.3 SSRF and redirects

Egress:

- allows only the fixed HTTPS origin;
- percent-encodes path substitutions as data;
- rejects cross-origin redirects;
- revalidates destination at connect time against DNS rebinding;
- rejects loopback, link-local, multicast, unspecified, and private IPs by
  default;
- applies V1 five-second connect, ten-second TLS/header, and thirty-second total
  deadlines.

There is no global private-network exception or disable-SSRF switch. A future
internal-provider policy must be explicit per Extension/Definition and is
tracked by #t2465; until then private and special-purpose addresses fail closed.

### 13.4 Budgets

V1 accepts JSON requests and JSON, `application/*+json`, or bounded text
responses. Binary, streaming, and multipart operations fail Package
validation.

Server caps raw upstream response at 1 MiB before parsing and applies the
existing 64 KiB final serialized Agent budget. A Definition may request a lower
cap but cannot raise either global limit. Large collections expose bounded
pagination rather than relying on truncation.

## 14. Audit and observability

Audit records:

```text
Workspace
Agent
service_installation_extension selection
Extension name and Action
Definition version and digest
catalog version
canonical secret-free request hash
execution status class
result classification
latency
request/response byte counts
timestamp
```

It excludes credentials, auth headers, cookies, body plaintext, raw query
values, internal upstream URLs, provider subjects, and App connection IDs.

Metrics use bounded labels such as safety/result/status class. Domain, Agent,
Workspace, token, and raw path are not labels.

## 15. Implementation sequence

1. Define the Package Spec, strict loader/validator, deterministic digest, and
   fixtures; future authoring Agents generate Packages from reviewed OpenAPI.
2. Add immutable Definition loading and built-in presets.
3. Add `ProfileExtension`, `/runtime/ext` Catalog/invoke, and profile dependency
   validation.
4. Add Gateway Extension upstream, route family, readiness, and route-contract
   coverage.
5. Add App Definition Extension declarations, App Installation selections,
   Workspace-unique Extension names, and selection management.
6. Add catalog version, catalog digest, durable catalog updates, and machine catalog hint.
7. Merge the verified Desktop Extension snapshot into the Exec
   `RuntimeCatalog`, process authorization, private credential file, local
   Catalog route, and dynamic action dispatcher.
8. Add unified `agentteam exec` discovery with separate built-in/Extension prompt
   sections, hierarchical help/schema, and start/resume replacement; delete
   `agentteam extension`, `/api/internal/ext`, `AGENTTEAM_EXTENSION_*`, and the built-in
   `pr-watch` domain.
9. Add deterministic binding, OAuth resolver, guarded egress, budgets, errors,
   and audit.
10. Verify App authorization and selection enable/disable, offline/reconnect,
    stale digest, replacement, token rotation, SSRF, pagination, and end-to-end
    invocation.

## 16. Acceptance criteria

- Semantically equivalent Packages produce the same Definition digest and
  deterministic encoded files.
- Package refs, schemas, examples, bindings, auth, and safety validate before
  publication.
- Ordinary `agentteam --help` omits private `exec`; daemon-launched Agent processes
  expose only `exec`, never `extension`.
- App-hosted selections require valid App identity, enforce Workspace-unique
  Extension names, and never create an Extension Service actor.
- Connection-required Builtins require exactly one active App connection;
  digest-pinned connection-none Builtins require exactly zero.
- Authorized processes expose built-in and snapshot Extension names/Actions
  through one immutable Exec Catalog and credential.
- A user-accessible connection-policy-satisfied App plus an active selection
  reaches all of that
  user's Agents by default; an explicit per-Agent authorization row is an
  optional allow/deny override. Desktop reflects only the Server projection.
- App authorization revoke, deselection, or disconnect blocks server
  invocation immediately and later removes the local Catalog.
- No provider OAuth crosses the server/Desktop boundary.
- A stale Definition digest cannot execute.
- Caller cannot create an arbitrary URL/header proxy.
- GitHub/Gitea Builtin Actions use the real shared App watch service, and old
  built-in `pr-watch`, separate `agentteam extension`, and ProviderApp forwarding
  entry points remain absent.
- Determinism, budgets, errors, audit redaction, private egress rejection,
  and Gateway route ownership have automated coverage.

### 16.1 Automated evidence

The following table is the acceptance-to-test map. Test names are stable
contract evidence, not a substitute for the root, presets, and Desktop broad
suites that execute them.

| Acceptance criterion | Automated evidence |
| --- | --- |
| Semantically equivalent Packages produce the same Definition digest and deterministic encoded files. | `internal/extension/spec.TestDefinitionDigestIgnoresProvenanceFormattingAndActionFileOrder`, `internal/extension/spec.TestDefinitionDigest_OrderIndependentSetsAreCanonicalized`, `internal/extension/spec.TestEncodeDefinition_RoundTrip` |
| Package refs, schemas, examples, bindings, auth, and safety validate before publication. | `internal/extension/spec.TestLoadDefinition_Rejections`, `internal/extension/spec.TestLoadDefinition_SymlinkRejected`, `internal/extension/spec.TestSchema_Draft202012Enforced`, `internal/extension/spec.TestBinding_SourceMustExistInSchema`, `internal/extension/spec.TestValidateSafety_DestructiveFlagMustAgreeWithClass`, `internal/extension/registry.TestRegisterEmbeddedExtensions_RealFixtures_PassPackageValidation` |
| App-hosted selections require valid App identity, enforce Workspace-unique Extension names, and never create an Extension Service actor. | `internal/store/serviceactor.TestInstallationExtensionRequiresAppHost`, `internal/store/serviceactor.TestInstallationExtensionActiveNameUniquePerWorkspace`, `internal/store/serviceactor.TestInstallationExtensionSameNameAllowedAcrossWorkspaces`, `internal/store/serviceactor.TestInstallationExtensionIdentityMismatchFailsClosed`, `desktop/integration/extensione2e.TestAppHostedDesktopInvokeAuditBudgetAndOfflineRevoke` |
| Builtin connection policy is digest-pinned and enforced consistently for Catalog and invocation. | `internal/extension/registry.TestRegisterBuiltin_ConnectionPolicy`, `internal/service/extensionflow.TestAppHostedCatalogProjectionConnectionlessPolicy`, `internal/service/extensionflow.TestInvokeConnectionlessBuiltinSkipsProviderCredentials` |
| Ordinary `agentteam --help` omits private `exec`; daemon-launched processes expose only `exec`, never `extension`. | `desktop/internal/cmd.TestExecCommandAvailableOnlyWithBaseURLAndAbsoluteCredentialFile`, `desktop/internal/cmd.TestPrivateRuntimeRegistrationExposesOnlyExec`, `guardrails/repository/no-retired-desktop-extension-test.sh` |
| One immutable Runtime Catalog and bearer expose built-in plus authorized Extension actions without namespace shadowing. | `desktop/internal/api/exec.TestRuntimeCatalogCombinesBuiltinAndExtensionActionsDeterministically`, `desktop/internal/api/exec.TestRuntimeCatalogRejectsExtensionNameCollidingWithBuiltinDomain`, `desktop/internal/api/exec.TestExecAuthorizationBindsRuntimeCatalogToBearerAndImmutableRecord`, `desktop/internal/api/exec.TestRuntimeRoutesReturnBearerBoundMergedCatalogAndInvokeExtension` |
| V1 default-allow reaches multiple Agents without per-Agent rows; explicit authorization remains an override and changes replace safely. | `internal/service/extensionflow.TestAppHostedCatalogProjectionAuthorizationModes`, `internal/service/extensionflow.TestCatalogDelivery_WorkerE2E`, `desktop/internal/dispatcher.TestGrantedCatalogWithoutCachedSnapshotRetiresLiveProcessBeforeNextDelivery`, `desktop/internal/dispatcher.TestMachineCatalogReconnectConvergesAndReplacesDesktopProcess` |
| App authorization revoke, deselection, or disconnect blocks invocation immediately and later removes the local Catalog. | `internal/service/extensionflow.TestAppHostedInvocationResolutionRejectsBeforeExecution`, `desktop/integration/extensione2e.TestAppHostedCatalogMutationsReplaceFlattenedDesktopRuntime`, `desktop/integration/extensione2e.TestAppHostedDesktopInvokeAuditBudgetAndOfflineRevoke`, `desktop/internal/dispatcher.TestCatalogHintChangeRevokesOldRuntimeLeaseBeforeReplacementFetch` |
| Provider OAuth and host App identity never cross the server/Desktop boundary. | `internal/api/extension.TestProjection_RootDropsEverySecretMarker`, `internal/api/extension.TestCatalogGetRoutes_NoSecretsLeak`, `desktop/integration/extensione2e.TestAppHostedDesktopInvokeAuditBudgetAndOfflineRevoke`, `desktop/integration/extensione2e.TestAppHostedCatalogMutationsReplaceFlattenedDesktopRuntime` |
| A stale Definition digest cannot execute. | `internal/service/extensionflow.TestAppHostedInvocationResolutionRejectsBeforeExecution/stale_definition_digest`, `internal/api/extension.TestInvokePassesAuthenticatedPrincipalAndPinnedEnvelope`, `desktop/integration/extensione2e.TestAppHostedCatalogMutationsReplaceFlattenedDesktopRuntime/Definition_digest_change` |
| Caller input cannot create an arbitrary URL/header proxy. | `internal/extension/binding.TestBindRejectsReservedHeaderEvenForTamperedPlan`, `internal/extension/egress.TestClientRejectsAPIKeyInReservedHeader`, `desktop/integration/extensione2e.TestAppHostedDesktopInvokeAuditBudgetAndOfflineRevoke` |
| MCP-backed Builtins keep SDK/protocol details private, allow only code-defined tools/resources, and preserve guarded one-shot session cleanup under JSON, SSE, cancellation, probes, and exhausted budgets. | `internal/extension/runner/mcp_test.TestSDKImportIsConfinedToMCPRuntime`, `internal/extension/runner/mcp_test.TestOperationAllowlistsFailBeforeNetwork`, `internal/extension/runner/mcp_test.TestCallToolUsesLegacyNegotiationSessionAndCleanup`, `internal/extension/runner/mcp_test.TestProbeNegotiatesAndCleansWithoutCapabilityOperation`, `internal/extension/runner/mcp_test.TestReadResourceUsesModernNegotiationAndPostSSE`, `internal/extension/runner/mcp_test.TestConnectFailureWithSessionPerformsDeleteCleanup`, `internal/extension/runner/mcp_test.TestCleanupBypassesExhaustedResponseBudget`, `internal/extension/runner/mcp_test.TestClientConcurrentInvocationsIsolateCredentialsSessionsAndBudgets` |
| GitHub/Gitea Builtin Actions use the real shared App watch service, and retired Desktop command/route/env/PR-watch entry points remain absent. | `internal/extension/providers/github.TestExecutorInvokesSharedServiceWithFixedGitHubIdentity`, `internal/extension/providers/gitea.TestExecutorInvokesSharedServiceWithFixedGiteaIdentity`, `internal/service/prwatch.TestInvokeLifecycleActionsPreserveProviderInstallationAndActorBinding`, `guardrails/repository/no-retired-desktop-extension-test.sh`, `guardrails/repository/no-vague-extension-names-test.sh` |
| Built-in and Extension prompt guidance remain separate while both use `agentteam exec`; catalog replacement revokes the single authorization before publication. | `desktop/internal/promptcompiler.TestCompileThreadContract_ExtensionGuidanceIsPersistentAndFingerprintBound`, `desktop/internal/dispatcher.TestResolveInjectsAuthorizedExtensionsOnlyThroughExec`, `desktop/internal/dispatcher.TestCatalogHintChangeRevokesOldRuntimeLeaseBeforeReplacementFetch`, `desktop/internal/api/exec.TestExecAuthorizationWithEmptyExtensionCatalogKeepsBuiltinExec` |
| Extension error codes remain machine-distinguishable even where `provider_auth_unavailable` and `upstream_failure` share exit 69. | `desktop/internal/cmd.TestExecExitCodePreservesExtensionFailureCodeInJSON`, `desktop/internal/api/exec.TestClassifyExtensionUpstreamErrorPreservesStableProviderCodes`, `desktop/internal/api/exec.TestRuntimeRoutesReturnBearerBoundMergedCatalogAndInvokeExtension` |
| Determinism, budgets, errors, audit redaction, private egress rejection, and Gateway ownership remain automated. | `internal/extension/binding.TestBindIsByteDeterministicAndSecretFree`; `internal/extension/egress.TestClientEnforcesRawAndFinalBudgets`; `internal/api/extension.TestInvokeMapsClosedExecutionErrors`; `internal/service/extensionflow.TestInvokeAuthoritativeSuccessPersistsSecretFreeAudit`; `internal/extension/egress.TestClientRejectsEveryNonPublicAddressClass`; `internal/gateway.TestGateway_ExtensionRoutesForwardedOnlyToExtensionUpstream`; `internal/router.TestExtensionRoutes_RegisteredOnlyUnderExtensionAndAll` |

Required verification also includes:

```bash
make harness-fast
go test ./internal/extension/... \
  ./internal/api/app ./internal/api/extension \
  ./internal/service/extensionflow ./internal/service/prwatch \
  ./internal/store/app ./internal/store/extension \
  ./internal/store/serviceactor ./internal/gateway ./internal/router \
  ./internal/server
(cd presets && go test ./...)
(cd desktop && make ci-required)
(cd desktop && go test -race ./internal/api/exec ./internal/dispatcher \
  ./integration/extensione2e -count=1 -p 1 -timeout=20m)
go build ./...
(cd desktop && go build ./...)
scripts/ci-web-smoke.sh
git diff --check
```
