"""Tenant definitions — one entry per client of the platform. Royal Imports is the first tenant. Everything that was HARD-CODED in core/odoo.py (teams 5/6, the GIFTWARE house account, the sale/done statuses) moves here as per-client CONFIG. A second client is just another Tenant with its own connectors + config — the skills and UI don't change. In production this comes from a tenants store (DB / HF dataset), not a literal; kept literal here so the pivot is legible and runnable today. """ from __future__ import annotations from harness.base import Tenant from harness.canonical import Capability from harness.connectors.odoo import OdooConnector def royal_imports(doc_mode: str = 'order') -> Tenant: odoo = OdooConnector(doc_mode=doc_mode) return Tenant( key='royal-imports', name='Royal Imports', sources={odoo.key: odoo}, config={ 'business_units': {'Fisch': 5, 'Royal': 6}, # canonical BU -> Odoo team_id 'confirmed_statuses': ['sale', 'done'], 'excluded_customer_ids': OdooConnector.excluded_customer_ids(), # GIFTWARE house account 'channels': {'Amazon': 'GIFTWARE DEALS'}, # marketplace split (see Amazon channel memory) 'branding': {'navy': '#1F4E78', 'gold': '#C8A24B', 'name': 'Royal Imports'}, }) class _EmptyConnector: """A source that answers every canonical query with NOTHING — the X8 isolation fixture. Deliberately NOT a mock library object and NOT a subclass shortcut around `aggregate`: the isolation proof has to run against the SAME code path a real tenant uses, so this answers the real interface and returns real (empty) results. A test double that skipped the interface would prove the double is isolated, not that the runtime is. An EMPTY pool is the strongest possible fixture for the assertion that matters: tenant B's `/customers` must return ZERO rows. Any row at all in that response came from somewhere it should not have, and there is no ambiguity about whose it is. """ key = 'empty' name = 'Empty (QA)' def capabilities(self) -> set: return set() def aggregate(self, q) -> list[dict]: return [] def records(self, q) -> list[dict]: return [] def distinct_count(self, q, fieldname: str) -> int: return 0 def supports(self, cap: Capability) -> bool: return False def entities(self) -> set: return set() def qa_tenant_b() -> Tenant: """Tenant 'qa-b' — the SECOND tenant the isolation proof needs, with no Odoo behind it. D-2's assertion is "tenant B cannot reach tenant A", and that cannot be tested with one tenant configured. This is the minimum second tenant: its own slug (so its own store namespace and its own DuckDB path), no credentials, and a connector that returns nothing. ⚠ NOT registered by default. `harness.runtime` only admits it when `AIOS_ENABLE_QA_TENANT=1`, so a production deployment has exactly one tenant and an attacker cannot mint a session against a fixture. (X8 says "the registry gains qa-b"; gating it is an additive safety amendment, recorded in the S1 mailbox 2026-07-30 — the isolation MECHANISM under test, the store namespace and the cookie's tenant claim, is identical either way.) """ empty = _EmptyConnector() return Tenant( key='qa-b', name='QA Tenant B', sources={empty.key: empty}, config={ 'business_units': {}, 'confirmed_statuses': [], 'excluded_customer_ids': [], 'channels': {}, 'branding': {'name': 'QA Tenant B'}, }) # Example of a *different-shape* client (accounting-only), to show the harness adapts. Uncomment and # wire real QBO creds to light up ledger/AR/customer skills — and NOT the SKU/inventory ones. # def demo_qbo_client() -> Tenant: # from harness.connectors.quickbooks import QuickBooksConnector # qb = QuickBooksConnector(realm_id='...', token='...') # return Tenant(key='demo-qbo', name='Demo (QuickBooks)', sources={qb.key: qb}, # config={'business_units': {}, 'confirmed_statuses': [], 'excluded_customer_ids': []})