# Shared Platform SDK and Tiered Documentation Search

- Status: Accepted
- Deciders: emmjoh
- Date: 2026-06-18
- Last updated: 2026-06-26 (clarified host-agnostic vs. Azure-free; see Decision Outcome / More Information. Phase 5: identity seam revised to keyed TokenCredential)

## Context and Problem Statement

The `saif` CLI and the Smithy agent host both need the same Forge domain capabilities
(service/app catalog, documentation search), but historically each reimplemented them. The CLI
owned a clean, interface-backed service layer entangled with interactive public-client MSAL
assumptions; Smithy reimplemented documentation search against Azure AI Search. Adding another
Smithy consumer of the same catalog/services would mean a third reimplementation.

Two questions had to be resolved:

1. **Where should shared domain logic live, and what are the host boundaries?** Both hosts call
   into Microsoft Graph and Azure under different identities (the CLI as the interactive user;
   Smithy on-behalf-of the user via the inbound bearer, with managed identity for system reads),
   so any shared code must depend on an identity abstraction rather than a host-fixed credential.
2. **How should documentation search work across both hosts?** Smithy's AI Search backend gives
   richer results than the CLI's public mkdocs lexical index, but it must be reachable from a
   developer laptop.

## Decision Drivers

- One source of truth for catalog and documentation capabilities, consumed in-process by both hosts.
- Keep the shared library a true leaf: no Spectre/System.CommandLine/TemplateEngine/MCP transport/LLM
  and no concrete authentication — only host-agnostic services, models, and abstractions. Host-agnostic
  is about presentation, transport, and identity, not about avoiding Azure: the SDK already depends on
  `Azure.Core` for the identity seam and may use Azure client libraries that run from any host.
- Preserve the existing `cli → remote Smithy` MCP bridge unchanged.
- Authorization parity: a user must never see catalog results broader than their own Graph scope.
- Documentation search must degrade gracefully when Smithy is unreachable or the user is offline.

## Considered Options

- **Shared SDK vs. duplicate-per-host.** Extract `SAIF.Platform.Sdk` referenced by both hosts, or
  keep each host's own implementation.
- **Documentation backend reachability.** (a) CLI queries Azure AI Search directly; (b) Smithy
  brokers AI Search and the CLI tiers through it with a public mkdocs fallback.

## Decision Outcome

**Extract a shared internal leaf library `SAIF.Platform.Sdk`** (project reference only, not
packaged) that owns the catalog, documentation, and the identity seam, and returns structured
objects. Each host supplies its own identity implementation and keeps its own
presentation/transport concerns. The name omits a `.CLI.` infix because the SDK is host-agnostic.

**Identity seam: keyed `Azure.Core.TokenCredential`.** The SDK catalog depends on a single
`TokenCredential` registered as a keyed DI service under `ForgeCatalog.CredentialKey`
(`"forge-catalog-credential"`). This replaces the earlier `ITokenProvider`/`IIdentityContext`
abstraction pair, which was deleted in Phase 5 (PR #849). The key prevents collision with a
host's general-purpose `DefaultAzureCredential`. Hosts supply:

- **CLI:** interactive/broker MSAL registered as the keyed `TokenCredential`.
- **Smithy:** `OnBehalfOfTokenCredential : Azure.Core.TokenCredential` — a per-request OBO
  credential built from the inbound user assertion (`IRequestAccessTokenService.GetLocalToken()`
  via `IHttpContextAccessor`). It **fails closed**: no inbound assertion throws
  `AuthenticationFailedException`; there is no managed-identity fallback. This is the stronger
  guarantee — authz parity holds by construction because the credential is always the user.

**Authorization parity** (catalog, Graph) is enforced by construction: Smithy's OBO credential
carries the user identity on every request. The AI Search documentation path (non-user-scoped)
continues to run under managed identity in Smithy — that concern is documented separately below.

**For documentation, adopt a single ordered provider chain** (`DocumentationService` over
`IDocumentationProvider`, lowest `Order` tried first, first non-empty result wins, a provider that
returns empty or throws is skipped). Both hosts run the same service; only the registered providers
differ:

- **Smithy** runs `[AiSearchDocumentationProvider (Order 0) → MkDocsDocumentationProvider (Order 100)]`
  and exposes deterministic `search_docs`/`get_doc` MCP tools that return structured JSON. The
  AI Search provider is implemented **in Smithy** (`Smithy.Documentation`), not in the shared SDK:
  it is **in-VNet-only** (it reaches a private endpoint no developer laptop can) and Smithy is its only
  consumer, so keeping it in the host avoids standing up a separate in-VNet add-on project. This is a
  reachability and single-consumer decision, not an "Azure-free SDK" mandate — the SDK may use Azure
  client libraries that work from any host. The SDK owns only the host-agnostic `IDocumentationProvider`
  port and the mkdocs provider.
- **CLI** runs `[SmithyRemoteDocumentationProvider (Order 0) → MkDocsDocumentationProvider (Order 100)]`,
  brokering AI Search through the remote Smithy MCP tools (reusing the existing bearer-token bridge)
  and falling back to the public mkdocs index.

This resolves the reachability question in favour of **Smithy brokering AI Search**: the `saif-*`
AI Search service sits behind a private endpoint with RBAC and managed-identity data-plane access
(verified in `cloud-foundations`), so a CLI user on a laptop has neither VNet reachability nor a
data-plane role and cannot query it directly. Documentation is public/non-user-scoped, so Smithy
serving AI Search under managed identity raises no authorization-parity concern — that concern is
specific to the catalog (Graph) path, which uses on-behalf-of the user.

To avoid presenting two near-identical documentation tools to an LLM, the remote MCP bridge filters
`search_docs`/`get_doc` out of the bridged Smithy tools (the CLI already exposes its own tiered
versions in-process) while continuing to bridge `smithy_ask`.

### Consequences

- Good, because catalog and documentation logic exist once and both hosts stay thin wrappers, keeping
  MCP tool vocabulary consistent (`search_docs`/`get_doc` with identical argument shapes).
- Good, because the SDK depends only on identity abstractions, so each host injects the correct
  principal (interactive user, on-behalf-of user, or managed identity) without the SDK knowing how.
- Good, because the CLI transparently gains AI Search quality when Smithy is reachable yet behaves
  exactly like today (mkdocs) when Smithy is down, unconfigured, or the user is offline — any remote
  failure returns empty and the chain falls through.
- Good, because the chain is ordered by an explicit `Order`, so behaviour never depends on DI
  registration order, and the serving provider is emitted as telemetry.
- Good, because the in-VNet-only provider (AI Search) lives in Smithy — the single host that consumes
  it and can reach the private endpoint — so the SDK keeps a host-agnostic provider set with no extra
  in-VNet add-on project or cross-CPM version to keep aligned. The SDK stays host-agnostic, not
  Azure-free: Azure client libraries that run from any host are permitted (e.g. the CLI resolves
  deployed versions with `Azure.ResourceManager.ResourceGraph`).
- Bad, because the CLI's best-quality documentation results now depend on Smithy availability; an
  unreachable Smithy silently degrades result quality (not correctness) to the public index.
- Bad, because the SDK abstracts identity behind a `TokenCredential` key, so a host that
  misconfigures the keyed registration will fail at runtime rather than compile time.

## More Information

- Phase 5 (PR #849) exposed the shared catalog to Smithy via the `search_services` MCP tool and
  `ServiceCatalogTools` adapter. This is adjacent to issue #824 (Smithy CLI command/template Q&A):
  it provides a catalog integration path that can unblock that follow-up, but does not deliver
  #824's `list_commands`/`get_command` and `list_templates`/`get_template` scope.
- The documentation move, the Smithy `search_docs`/`get_doc` tools, and the CLI tiered provider
  shipped together in Phase 3 (PR #844) so the tool vocabulary changed once, coherently.
- The `ITokenProvider`/`IIdentityContext` abstraction pair mentioned in earlier planning notes no
  longer exists. The keyed `Azure.Core.TokenCredential` described above is the final design.
- `AzureDevOpsServiceRepositoryLocator` shipped in Phase 5: lists ADO repositories under the
  OBO credential (resource `499b84ac-...`/`.default`) using per-request `HttpContext.Items` cache,
  ensuring catalog correlation only sees repos the user can access (CLI parity).
- The "host-agnostic, not Azure-free" clarification (2026-06-26) accompanied the `service describe`
  deployed-version feature (forge#867): the CLI queries Azure Resource Graph directly via
  `Azure.ResourceManager.ResourceGraph` to show each environment's image tag and build number. The
  Azure coupling lives in the CLI host; the shared SDK gained only a host-agnostic `RegistrationName`
  string on `ServiceEnvironment`, so the leaf stays free of host-fixed transport and identity.
