Skip to content

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.