feat(server): TenantRegistry.as_platform() adapter for serve() integration#649
Merged
Merged
Conversation
…ation Adds the missing bridge between TenantRegistry (5.0, #619) and adcp.decisioning.serve(), eliminating per-adopter DecisioningPlatform boilerplate that three teams independently wrote during the 5.0 migration. New public API: - TenantRegistry.resolve_by_id(tenant_id) — async lookup by tenant_id (mirrors resolve() but keyed on ctx.tenant_id instead of Host header) - TenantRegistry.as_platform(accounts, capabilities=None, serve_states=...) — returns a DecisioningPlatform that dispatches per-request via resolve_by_id(ctx.tenant_id) with configurable health gating Closes #645 https://claude.ai/code/session_01DboS2SpnZYd9zC87Mk3juT
The guard fires for any specialism method dispatched via _make_delegate, not just get_products. Update the inline comment to reflect actual behaviour. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #645
Three adopter teams independently hit the gap between
TenantRegistry(introduced in 5.0, #619) andadcp.decisioning.serve()— there was no standard way to connect the two without writing a boilerplateDecisioningPlatformsubclass per deployment. This PR bridges that gap.Changes
src/adcp/server/tenant_registry.pyresolve_by_id(tenant_id: str) -> TenantResolution | None— async lookup by registry key, triggers lazy factory construction the same wayresolve()does but skips the host-map pathas_platform(*, accounts, capabilities, serve_states) -> DecisioningPlatform— returns a_RegistryPlatformAdapterthat can be passed directly toserve(); lazy-imports fromplatform_routerto avoid circular imports_DEFAULT_SERVE_STATES = frozenset({"healthy", "unverified"})constantas_platform()is the primary example; adds explicitcontext_factorysnippet showing how to wireSubdomainTenantMiddlewaresoctx.tenant_idmatches the registry key; notesTenant.idmust equal the string passed toregister_lazy()src/adcp/decisioning/platform_router.py_RegistryPlatformAdapter(DecisioningPlatform)— synthesizes delegate methods using the same_all_specialism_methods()+_make_delegate()pattern asPlatformRouter/LazyPlatformRouter_make_registry_platform_adapter()factory (called byas_platform())SERVICE_UNAVAILABLE, notACCOUNT_NOT_FOUND, to avoid leaking which tenant IDs are enrolled (deliberate departure fromPlatformRouter's static-set rationale, documented in code)get_products/refine_get_productsimplemented explicitly (same pattern asPlatformRouter) rather than synthesisedtests/test_tenant_registry.pyresolve_by_idandas_platformpaths (58 total)SERVICE_UNAVAILABLE, unverified serves by default, customserve_statesfail-closed, unknown tenant →SERVICE_UNAVAILABLE(notACCOUNT_NOT_FOUND), missingtenant_id→ACCOUNT_NOT_FOUND, synthesised method dispatchBefore / after
Usage note — tenant_id wiring
as_platform()readsctx.tenant_id(set from the transport layer, e.g.Hostheader viaSubdomainTenantMiddleware). That value must equal the string key passed toregister_lazy(). If usingSubdomainTenantMiddleware, wire it like this:Test results
Ruff and mypy clean on Python 3.10–3.13.
Pre-PR review sign-offs
code-reviewer ✓ approved after fixes:
test_as_platform_unknown_tenant_raises_account_not_found→test_as_platform_unknown_tenant_raises_service_unavailable(name matched wrong error; implementation was already correct)_resolve_tenant_platformdocumenting theSERVICE_UNAVAILABLEvsACCOUNT_NOT_FOUNDchoicegetattr(ctx, "tenant_id", None)→ctx.tenant_id(declared field, defensive form was misleading)dx-expert ✓ approved after fixes:
context_factorysnippet +Tenant.id↔ registry key mismatch warning to class docstring (silentSERVICE_UNAVAILABLEfoot-gun)logger.warningwhencapabilities=Noneto surface misconfiguration earlyGenerated by Claude Code