Context
0155 decision 4 puts the one MCP gateway in theai service; OS is porting that service now (STA-705, a skeleton on
os figentra/sta-705-ai). The edge is a Cloudflare Worker per product (os workers/edge) and the static-assets Worker
(0284), so a Worker gateway was a fair question. @stackra/search already fuses full-text, trigram and pgvector lanes
by reciprocal rank (0221 decision 1) and folds Arabic (0404 decision 2), with an OpenAI-compatible embedder at 1024
dimensions; there is no reranker, no Arabic stemming and no Arabizi. OCR runs as a pinned Python image in the regional
tools stack on ECS Fargate (0444), reached as a Connection behind TextExtractor. Speech already sits behind
SpeechService with region-mapped providers (0376). Forecasting and recommendations have nothing yet.
Decision
Runtime and gateway
-
The AI runtime and the MCP gateway are modules of OS’s
aiservice, deployed once per region (GCC first, EU when it opens) with the rest of OS on the generated Encore app on AWS. The service holds the agent runtime, the run and conversation stores, the client-tool relay of 0490 decision 4, the speech stream, and the MCP server (Streamable HTTP, one logical server per tenant, OAuth through ZITADEL, protected-resource metadata). Model calls are routed by region (0360 decision 14). Every product uses this one deployment; no product runs its own. -
The gateway is not a Cloudflare Worker. Cloudflare is layer 1 of 0237 in front of it:
mcp.figentra.comand the sidekick’s/app/ai/*routes get TLS, WAF, bot control and a per-IP and per-token rate limit, and are routed to the ai service of the tenant’s region. Theui://bundles are static assets served by the static Worker (0284) and referenced by the gateway. A Worker gateway is refused because every call needs the registry, IAM, entitlements, the approval gate, the audit and the run store, which live in-region in the service; a Worker would either duplicate them or be one more hop. Long-lived streams stay on the service. -
One tool table across products. gen writes each service’s tool rows into its
stackra.manifest.json; the ai service’s build reads the manifests of every service in the region’s deployment (@stackra/mcpreadManifests,toolTable) and dispatches each call through the generated client of the owning service (0260). A tenant sees only the tools of the products and modules it is entitled to. A product whose services are deployed outside OS’s Encore app registers its manifest and its client endpoint with the ai service (an OS dependency, STA-1638).
Retrieval
-
Postgres hybrid stays, and a reranker follows it.
@stackra/searchgains aRerankerport: it takes the query and the top N fused candidates (default 50, with their searchable text) and returns them reordered with scores.SearchServicereranks when the request asks for it or the searchable model setsrerank: true(products do). Drivers:HttpRerankerDriverfor any endpoint speaking the text-embeddings-inference/rerankcontract (the self-hosted image of decision 8, or a provider’s in-region endpoint), and a memory driver. No reranker bound means fused order, as today. -
Arabic quality. On top of the fold of 0404: the Arabic Snowball stemmer as an extra full-text configuration
lane for Arabic locales; Arabizi (Latin letters and digits written for Arabic,
7for ح,3for ع) mapped to Arabic in the query only; a per-tenant synonym list (search.synonym, data, edited through actions) expanded in the query; and the multilingual embedder (BAAI/bge-m3, 1024 dimensions, unchanged index). A fixed evaluation set of Arabic, English and mixed shopper queries per catalogue scores NDCG@10 and recall@50; a change that lowers either fails its test. - pgvector until a measured trigger. Qdrant, or any separate vector store, is not added (0227). The trigger is one tenant index above 5 million vectors, or a p95 vector-lane latency above 150 ms at 200 candidates after HNSW tuning and partitioning by tenant. The OpenSearch trigger of 0221 decision 9 is unchanged.
Python and the ML workers
-
Python runs only as tools-stack images behind ports. Each is a pinned image built from
packages/infrastructure/infra-tools/images/<name>, published to the registry, run by the regional tools stack on ECS Fargate (0444) or, where it needs a GPU, on an ECS capacity provider of GPU instances in the same cluster, and reached as a Connection by a TypeScript driver behind a port. No Python service, no Python in a member, no Python business logic: an image takes data in and returns numbers or text. -
The images.
tools-retrievalis the one image that is not ours: it runs unmodified, with the models pinned by revision. Forecasts run as scheduled jobs in the owning service (demand per item and location, nightly), which call the image in batches and store the results as records. Recommendations answer from Postgres on day one (co-occurrence from orders as an indexer read model, plus nearest products by vector);tools-recommendreplaces that per tenant once its interactions pass a threshold in settings, behind the same port. -
Speech stays behind 0376. The region map names the speech provider;
tools-speechis one more provider a region may name when audio must not leave it. Voice input on mobile and the till is 0490 decision 15.
Consequences
- OS’s ai service is on the critical path of every AI surface; its port (STA-705) gates the sidekick, the shopping assistant and MCP hosts going live.
- The tools stack gains a GPU capacity provider in each region that runs
tools-retrieval(andtools-speechwhere named); this is a cost line the owner approves per region. - Search quality becomes measured: each catalogue carries an evaluation set, and reindexing to the stemmer lane follows the versioned swap of 0221 decision 7.
- No new language enters any workspace member; the images are build recipes, like
tools-ocr.
Enforcement
- planned — STA-1636: the search evaluation test (NDCG@10, recall@50) over the shipped fixture catalogue, Arabic and
English, as a
test:intof@stackra/search. - planned — STA-1636: the
Rerankerdriver contract test shared by the HTTP and memory drivers. - planned — STA-1637:
validate-repository.mjsrefuses a.pyfile outsidepackages/infrastructure/infra-tools/images/. TOOLS_CATALOGUEpins every image to a tag (packages/infrastructure/infra-tools/src/core/constants/tools-catalogue.constant.tsand its test).
Note 2026-10-30: speech providers for GCC (decision 9)
The owner chose all three speech providers behind the speech Connection of 0376. Azure AI Speech in UAE North is the region’s platform default: Microsoft lists real-time and batch speech to text there, processed in the resource’s region. The self-hostedtools-speech is pinned for the health and privileged data classes (0376 decision 7). AWS
Transcribe is not offered in me-central-1, only in me-south-1 (Bahrain), so it can be only a per-tenant choice that
processes outside the tenant’s region, pending the owner’s answer and a catalog entry. Creating the Azure account and
its Connection is an owner step.
Note 2026-10-30: where synonyms live (decision 5)
@stackra/search sits below @stackra/iam (0462 decision 1), so it cannot declare permissions or own a tenant
model with actions. It keeps the SynonymSource port, its memory driver and the query expansion. The synonym model,
its permissions (data_maintenance.synonym.*), its actions and its table data_maintenance.synonyms live in
@stackra/data-maintenance, which implements the port (STA-1636).
Supersedes and sources
Supersedes: none. Sources: the leader’s brief and the owner’s message of 2026-10-30 (STA-1629); os
figentra/sta-705-ai (4f57f0df); packages/data/search and packages/infrastructure/infra-tools on core main
6f3536ed.