ADR-0156 : Hiérarchie du tenant, champs communs stampés par PostgreSQL et identifiant runtime séparé
Contexte
La référence d'architecture place sous le tenant des organisations et des unités métier (A6.1 cœur, A6.2 « tenant vers organisations, projets, environnements »), demande sur les tables métier les champs communs environment_id, updated_by, version, metadata validé et correlation_id (B7.2), et sépare les connexions administrateur et runtime (B7.3).
Avant cette décision, eai.organizations et eai.business_units n'existaient pas, l'environnement n'était qu'un texte (usage_events.environment, application_deployments.environment), updated_by et version n'étaient portés que par quelques tables, metadata n'était validé nulle part en base, et l'API comme les migrations utilisaient le même identifiant de connexion (EAI_DATABASE_URL).
Décision
- Hiérarchie (V162) :
eai.organizations(tenant, slug unique par tenant, statutactiveouarchived) eteai.business_units(organisation,parent_idfacultatif pour l'imbrication, fixé à la création pour qu'aucun cycle ne puisse apparaître). Un projet se rattache à une organisation et à une de ses unités (projects.organization_id,projects.business_unit_id). Les clés étrangères sont composées avectenant_idouorganization_id: un contrôle de clé étrangère ne passe pas par la RLS, donc seule une clé composée empêche, même en contexte plateforme, de rattacher un projet ou une unité à l'organisation d'un autre tenant. RLS forcée sur les deux tables. - Routes tenancy : GET et POST
/v1/organizations, PATCH/v1/organizations/{organization_id}, GET et POST/v1/business-units, PATCH/v1/business-units/{business_unit_id}, etorganization_id,business_unit_id,metadatasur les projets. tenant_admin écrit, tout rôle du tenant lit ; chaque écriture est auditée. - Champs communs stampés par la base (V162, V164) : le déclencheur
eai.stamp_common_fields()écritcreated_byetupdated_bydepuis l'acteur vérifié (app.actor_id), incrémenteversionà chaque mise à jour et écrittrace_id(lecorrelation_idde la référence, nom déjà utilisé paraudit_events,usage_eventsettelemetry_spans) depuisapp.correlation_id, queeai.db.txfixe à partir de la trace W3C de la requête. Il s'applique àorganizations,business_units,projects,environments,agents,applicationsetincidents. Un appelant ne peut ni choisir sa version ni effacercreated_by. Les routes PATCH acceptentexpected_version: une version périmée donne 409version_conflict. metadataest validé en base pareai.valid_metadata()(contrainte CHECK) : objet plat, clés en minuscules (^[a-z][a-z0-9_.-]{0,62}$), valeurs texte (1024 caractères au plus), nombre, booléen ou null, 16 Ko au plus. Une contrainte CHECK a été préférée à une extension JSON Schema (pg_jsonschema) : aucune extension à installer chez le client, et la règle tient pour tout écrivain, l'API comme un script. Une métadonnée refusée donne 422invalid_metadata.environment_id(V163) :usage_eventsetapplication_deploymentsportent une clé étrangère verseai.environments(ON DELETE SET NULL), calculée en base par déclencheur depuis (tenant, projet par id ou par slug, nom d'environnement). La colonne texte reste pour les lignes dont le projet n'a pas cet environnement :environment_idvaut alors NULL, jamais une supposition. Une valeur fournie par l'écrivain est ignorée et recalculée dans le tenant de la ligne.- Identifiant runtime (V165) : le rôle groupe
eai_runtime(NOLOGIN, membre deeai_app, sans CREATE sur aucun schéma, propriétaire de rien, lecture seule deschema_migrations). L'exploitant crée le login de l'API (CREATE ROLE eai_api LOGIN PASSWORD '...' IN ROLE eai_runtime) et le donne dansEAI_RUNTIME_DATABASE_URL: le pool des requêtes et le worker l'utilisent ;eai.migrategarde toujoursEAI_DATABASE_URL, l'identifiant d'administration. SansEAI_RUNTIME_DATABASE_URL, le comportement antérieur reste (un seul identifiant, RLS parSET LOCAL ROLE eai_app), pour ne casser aucune installation ; le contrôle de démarrage en production exige TLS sur les deux URL.
Conséquences
- Les réponses des projets, environnements, agents, applications et incidents gagnent
created_by,updated_by,version,metadataettrace_id. - Un client qui écrit en parallèle sur un même projet ou une même organisation peut passer
expected_versionpour ne jamais écraser une modification. - La clé
EAI_RUNTIME_DATABASE_URLs'ajoute au secret OpenBao du Control Plane quand l'exploitant crée le login runtime ; le chart n'impose pas encore cette clé, pour ne pas bloquer la synchronisation d'un secret existant.
Preuves
services/control-plane/tests/test_tenancy_hierarchy.py (hiérarchie, isolation entre tenants jusqu'aux clés composées, champs stampés, concurrence optimiste, métadonnées refusées en base, environment_id résolu et recalculé, login runtime sans DDL qui sert l'API pendant que la migration le refuse), services/control-plane/tests/test_data_model_mapping.py, services/control-plane/tests/test_security_attack_suite.py, et les tests de forme de requête des quatre SDK.