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:
- 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.
- 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.Corefor the identity seam and may use Azure client libraries that run from any host. - Preserve the existing
cli → remote SmithyMCP 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.Sdkreferenced 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()viaIHttpContextAccessor). It fails closed: no inbound assertion throwsAuthenticationFailedException; 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 deterministicsearch_docs/get_docMCP 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-agnosticIDocumentationProviderport 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_docwith 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
TokenCredentialkey, 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_servicesMCP tool andServiceCatalogToolsadapter. 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'slist_commands/get_commandandlist_templates/get_templatescope. - The documentation move, the Smithy
search_docs/get_doctools, and the CLI tiered provider shipped together in Phase 3 (PR #844) so the tool vocabulary changed once, coherently. - The
ITokenProvider/IIdentityContextabstraction pair mentioned in earlier planning notes no longer exists. The keyedAzure.Core.TokenCredentialdescribed above is the final design. AzureDevOpsServiceRepositoryLocatorshipped in Phase 5: lists ADO repositories under the OBO credential (resource499b84ac-.../.default) using per-requestHttpContext.Itemscache, 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 describedeployed-version feature (forge#867): the CLI queries Azure Resource Graph directly viaAzure.ResourceManager.ResourceGraphto show each environment's image tag and build number. The Azure coupling lives in the CLI host; the shared SDK gained only a host-agnosticRegistrationNamestring onServiceEnvironment, so the leaf stays free of host-fixed transport and identity.