# ADR 0027: External Capability Registry ## Status Accepted. ## Context ADR-0026 introduced the `UsageFact` ledger with a `kind = external_capability` fact and a `capabilityId` field, but deferred the capability registry itself. Two concrete needs now force the issue: - **PDF→Markdown bundle** conversion (and, imminently, audio/video→text) must run as a side effect of an `AgentRun`, bill in non-token units (pages, seconds), use a different provider than the model loop, and report cost through a different channel. It is not a sub-run (ADR-0026 rejected that) and not a model-provider call (it does not speak the Anthropic/ OpenRouter protocol). - The Agent already has a Bash tool. Without a first-class capability seam, the path of least resistance is for the agent to shell out to ad-hoc scripts that embed API keys, write to arbitrary paths, and report nothing to the ledger. That is exactly the unattributed, uncontained external consumption ADR-0022/0026 exist to prevent. The model-provider connection (`OrganizationProviderConnection`, ADR-0024) is the wrong seam for these services: - Its payload schema (`baseUrl` + `authToken` + `anthropicApiKey`) and readiness probe (`/v1/models?supported_parameters=tools`) are specific to OpenRouter/Anthropic. MinerU, Whisper, and future OCR/ASR services have different auth shapes (an API token, optionally a project id) and no `/v1/models` endpoint. - Its uniqueness key is `(organizationId, providerId)` where `providerId` is an OpenRouter-style model-routing id. A capability provider id (`mineru`) names a *service*, not a model. - Coupling capability credentials into the model-provider table would force every capability's auth shape through `ProviderSecretPayloadV1` and every readiness probe through `probeOpenRouterCredential`. The Feishu Application Connection (`OrganizationFeishuApplicationConnection`) is the right structural precedent: it reuses the ADR-0024 envelope (KEK → DEK → AES-256-GCM, AAD-bound to purpose/org/connection/version) but has its own connection table, its own payload schema, its own readiness probe, and its own per-org uniqueness. Capability connections follow the same pattern. ## Decision ### External Capability An **External Capability** is a platform-registered, org-enabled document or media transform service invoked as a side effect of an `AgentRun`. It is identified by a stable `capabilityId` (e.g. `pdf_to_md_bundle`, `audio_video_to_text`). A capability: - Has an **input kind** (PDF, image, audio, video, …) and an **output contract** (markdown bundle with extracted images, transcript text, …). - Bills in **non-token units** (pages, audio-seconds) recorded on a `UsageFact` with `kind = external_capability`, or in tokens when the backing service reports them. - Writes its output **into the invoking run's workspace** (ADR-0018 `AgentSurface` — no escapes). - Is invoked through a **capability adapter** in Hub, never by the Agent shelling out with embedded credentials. ### Capability Connection Credentials for a capability live in an **`OrganizationCapabilityConnection`**, structurally identical to the Feishu Application Connection: - Belongs to exactly one Organization. - Unique by `(organizationId, capabilityId)`. - `DRAFT` / `ACTIVE` / `DISABLED`; resolution accepts only `ACTIVE` with a valid active secret version. - Secret material is an immutable, AAD-bound, KEK-wrapped envelope version (`CapabilityCredentialVersion`), reusing the ADR-0024 encryption machinery with `purpose = "capability"`. - Its payload schema is capability-specific (`CapabilitySecretPayloadV1`: `baseUrl`, `apiToken`, optional `projectId`). New capability types extend the payload, not the connection table. - A capability-specific **readiness probe** validates the credential before activation (e.g. MinerU: a trivial authenticated GET). The probe is injectable, matching the Feishu/provider pattern, so tests never hit the network. ### Capability Adapter The adapter is the seam between the Agent and the external service. It: - Resolves the org's active capability connection (fail-closed, no process-global fallback — ADR-0024). - Accepts a workspace-relative input path and an output directory. - Calls the backing service (MinerU, Whisper, …) via an injectable `Client` interface so the real HTTP client is swappable and mockable. - Writes the produced markdown + image assets into the run's workspace. - Writes one `UsageFact` (or more, if the service reports per-stage consumption) with `kind = external_capability`, `capabilityId`, `provider` (the service id), `quantity + unit` (pages / seconds), and `costUsd + costSource = provider_reported` when the service reports cost. ### Registry The platform maintains a **registry** of known capabilities: their id, input kind, output contract, metering unit, and adapter. This is code-level registration (like `ToolRegistry`), not a database table — a capability is available to an Organization only when (a) the platform knows the adapter and (b) the Organization has an `ACTIVE` connection for it. Both gates are required. ### What is NOT in this ADR - The capability invocation is **not** a first-class persisted record (`CapabilityInvocation` table) in this ADR. The `UsageFact` row with `capabilityId` + `correlationId` is the durable trace. If we later need a richer invocation log (retries, partial output, multi-stage status), that is a follow-up; for now the fact is enough. - **Pricebook** remains deferred (ADR-0026). Capability facts use `provider_reported` when the service returns cost; otherwise `unknown`. - **Org-scoped enable/disable policy** beyond connection status is deferred. An org with an `ACTIVE` connection has the capability; one without does not. A finer "enabled but no credential" toggle is not needed yet. - **Agent-facing tool exposure** (how the Agent discovers and calls the capability — MCP tool, Bash wrapper, or built-in) is an implementation detail of the adapter wiring, not a contract concern. The contract pins that the Agent never receives the capability credential. ## Consequences - Adding a new external capability (e.g. `image_ocr`) is: register an adapter, add a `capabilityId` constant, optionally extend the secret payload — no schema change to `UsageFact` or `AgentRun`. - The model-provider connection table stays focused on model routing; capability credentials do not pollute its payload or readiness probe. - Three connection types now share the ADR-0024 envelope: model-provider, Feishu application, and capability. Each has its own table, payload schema, and probe, but the same encryption, rotation, and resolver boundary. - The Agent's Bash tool remains available, but the intended path for document/media transforms is the capability adapter. Whether to narrow Bash for capability-shaped tasks is an operational policy decision, not a contract one. - Tests prove: workspace containment of capability output, fail-closed credential resolution, `UsageFact` attribution with non-token metering, and that the Agent process never receives the capability credential. ## Deferred - `CapabilityInvocation` as a first-class durable record (status, retries, partial output) — currently the `UsageFact` row is the only trace. - Pricebook derivation for capability costs (ADR-0026 deferred). - Org-scoped capability enable/disable policy finer than connection status. - Agent-facing tool discovery (MCP vs built-in) for capabilities.