|
12 | 12 | * :class:`DbAuditSink` for compliance trail |
13 | 13 | * :class:`V3ReferenceSeller` (the platform impl) |
14 | 14 |
|
15 | | -4. ``adcp.decisioning.serve(transport="both", ...)`` — single |
16 | | - binary serving MCP at ``/mcp`` and A2A at ``/``. |
| 15 | +4. ``adcp.decisioning.serve(transport="both", asgi_middleware=[...])`` |
| 16 | + — single binary serving MCP at ``/mcp`` and A2A at ``/`` with |
| 17 | + :class:`SubdomainTenantMiddleware` layered on the outer HTTP app. |
17 | 18 |
|
18 | 19 | Adopters fork this file and replace the platform impl, the seller- |
19 | 20 | specific column populators, and the seed fixtures. Everything else |
|
38 | 39 |
|
39 | 40 | from adcp.decisioning import serve |
40 | 41 | from adcp.server import ( |
| 42 | + SubdomainTenantMiddleware, |
41 | 43 | ToolContext, |
42 | 44 | current_tenant, |
43 | 45 | ) |
|
54 | 56 | logger = logging.getLogger(__name__) |
55 | 57 |
|
56 | 58 |
|
57 | | -def _build_context_factory(router: SqlSubdomainTenantRouter): |
| 59 | +def _build_context_factory(): |
58 | 60 | """``context_factory`` that pins :attr:`ToolContext.tenant_id` |
59 | 61 | from the resolved tenant. |
60 | 62 |
|
61 | 63 | The middleware sets ``current_tenant()`` on the contextvar before |
62 | 64 | dispatch; this factory reads it and writes ``tenant_id`` so the |
63 | 65 | framework's idempotency middleware scopes correctly. |
64 | 66 | """ |
65 | | - del router # router is consumed via current_tenant(); kept for symmetry |
66 | 67 |
|
67 | 68 | def build(meta: RequestMetadata) -> ToolContext: |
68 | 69 | tenant = current_tenant() |
@@ -114,84 +115,28 @@ def main() -> None: |
114 | 115 | ) |
115 | 116 | logger.info("Audit sink wired: %s. Tenant router cache: 256 hosts.", type(audit_sink).__name__) |
116 | 117 |
|
117 | | - # The framework's serve() drives uvicorn. We layer the tenant |
118 | | - # middleware on the parent Starlette app via a build hook — |
119 | | - # for the MVP we wire it inline by passing a custom ``app`` |
120 | | - # builder. For the Tier-2 wiring we use the ``buyer_agent_registry`` |
121 | | - # and ``context_factory`` kwargs the framework already supports. |
122 | 118 | serve( |
123 | 119 | platform=platform, |
124 | 120 | name="v3-reference-seller", |
125 | 121 | port=port, |
126 | | - transport="both", |
127 | | - buyer_agent_registry=buyer_registry, |
128 | | - context_factory=_build_context_factory(router), |
129 | | - # NOTE: SubdomainTenantMiddleware is added to the unified |
130 | | - # Starlette app via ``add_middleware``. The current public |
131 | | - # surface of ``serve()`` doesn't expose a hook for adopter |
132 | | - # ASGI middleware on the parent — adopters who want it today |
133 | | - # call ``_build_mcp_and_a2a_app`` directly and run uvicorn |
134 | | - # themselves. Filed as a follow-up DX issue: the helper |
135 | | - # below shows the pattern. |
136 | 122 | host="0.0.0.0", |
137 | | - ) |
138 | | - |
139 | | - |
140 | | -# Adopter-side helper for wiring SubdomainTenantMiddleware directly. |
141 | | -# Until ``serve()`` accepts an ``asgi_middleware=`` kwarg, callers who |
142 | | -# need tenant routing build the unified app themselves and run |
143 | | -# uvicorn outside the framework's ``serve()`` wrapper. |
144 | | -def build_app( |
145 | | - platform: V3ReferenceSeller, |
146 | | - *, |
147 | | - router: SqlSubdomainTenantRouter, |
148 | | - buyer_registry, # type: ignore[no-untyped-def] |
149 | | - audit_sink, # type: ignore[no-untyped-def] |
150 | | -): |
151 | | - """Construct the unified MCP+A2A app with the tenant middleware. |
152 | | -
|
153 | | - Returns an ASGI app callable; the caller runs uvicorn against it. |
154 | | - """ |
155 | | - from concurrent.futures import ThreadPoolExecutor |
156 | | - |
157 | | - from adcp.decisioning.handler import PlatformHandler |
158 | | - from adcp.decisioning.serve import _build_mcp_and_a2a_app |
159 | | - from adcp.decisioning.task_registry import InMemoryTaskRegistry |
160 | | - |
161 | | - executor = ThreadPoolExecutor(max_workers=8, thread_name_prefix="v3-ref-") |
162 | | - handler = PlatformHandler( |
163 | | - platform, |
164 | | - executor=executor, |
165 | | - registry=InMemoryTaskRegistry(), |
| 123 | + transport="both", |
166 | 124 | buyer_agent_registry=buyer_registry, |
| 125 | + context_factory=_build_context_factory(), |
| 126 | + # SubdomainTenantMiddleware reads the request Host header, |
| 127 | + # resolves it via the SQL router, and sets the |
| 128 | + # ``current_tenant()`` contextvar before the handler runs. |
| 129 | + # The buyer_registry, AccountStore, and audit sink all read |
| 130 | + # that contextvar — this is the only multi-tenant wiring |
| 131 | + # point. |
| 132 | + asgi_middleware=[ |
| 133 | + (SubdomainTenantMiddleware, {"router": router}), |
| 134 | + ], |
167 | 135 | ) |
168 | | - app = _build_mcp_and_a2a_app( |
169 | | - handler, |
170 | | - name="v3-reference-seller", |
171 | | - port=3001, |
172 | | - host="0.0.0.0", |
173 | | - instructions=None, |
174 | | - test_controller=None, |
175 | | - context_factory=_build_context_factory(router), |
176 | | - ) |
177 | | - # Wrap with the tenant middleware. ASGI middleware composes |
178 | | - # outermost-first: requests pass through the tenant resolver |
179 | | - # before reaching the dispatcher. |
180 | | - from adcp.server.tenant_router import SubdomainTenantMiddleware as _SubdomainTenantMiddleware |
181 | | - |
182 | | - class _Wrapped: |
183 | | - def __init__(self, app): |
184 | | - self._app = app |
185 | | - self._mw = _SubdomainTenantMiddleware(app, router=router) |
186 | | - |
187 | | - async def __call__(self, scope, receive, send): |
188 | | - await self._mw(scope, receive, send) |
189 | | - |
190 | | - return _Wrapped(app) |
191 | 136 |
|
192 | 137 |
|
193 | 138 | if __name__ == "__main__": |
194 | 139 | main() |
195 | 140 |
|
196 | 141 |
|
197 | | -__all__ = ["build_app", "main"] |
| 142 | +__all__ = ["main"] |
0 commit comments