[DTP-Worker 20260801_190412] Sprint 8 · buffer · Fiches agents/RBAC : gate d'APPARTENANCE à roles_allowed du chat OTOIA — 3e surface d'appartenance (DISTINCTE du contrat rbac_50_roles.json ET du Has Role workspace.json) : QUI peut INVOQUER l'assistant OTOIA d'un portail — asserée en prose par rendu:36 (∈ roles_allowed construction, triple ancrage) + mobile:41 (∉ tout, assertion négative #6), recomputée de chat_otoia/out/chat_mount.json (byte-gaté) ; 6 morsures vérifiées, 7 gates re-verts

This commit is contained in:
Claude Code DTP Worker
2026-08-01 19:12:16 +00:00
parent 47ef210910
commit 861260462f
3 changed files with 163 additions and 1 deletions
+112
View File
@@ -5215,6 +5215,118 @@ if _ws_json is not None and _ws_roles is not None:
f"MAIS workspace.json met « {rname} » dans son Has Role → "
f"SUR-EXPOSITION console, assertion #6 VIOLÉE")
# ============================================================================
# Fiches agents · appartenance à `roles_allowed` du chat OTOIA — QUI peut
# INVOQUER l'assistant OTOIA embarqué dans un portail. Surface data-derived
# DISTINCTE de la membership Workspace `Has Role` (QUELLE console le rôle atteint,
# gatée juste au-dessus) ET des attributs du contrat rbac_50_roles.json (gatés par
# les blocs Fiches Faisabilité / Fiche Mobile) : la liste `roles_allowed` ne vit
# PAS dans le contrat ni dans workspace.json mais dans
# frontend/chat_otoia/out/chat_mount.json (une config runtime par portail,
# artefact byte-gaté par check_artifacts). Deux fiches font, EN PROSE, une
# assertion d'appartenance à CE contrôle d'accès :
# • rendu (:36) — POSITIVE : le rôle « OTO Faisabilité Rendu 3D » fait partie
# des `roles_allowed` du portail NOMMÉ `construction` (droit
# d'invoquer le chat pour déclencher les rendus assistés).
# • mobile (:41) — NÉGATIVE d'honnêteté (#6) : le rôle « OTO Plateforme Mobile »
# (famille `plateforme`) n'est VOLONTAIREMENT dans AUCUNE liste
# `roles_allowed` — le chat ne couvre que les portails métier.
# Aucun gate ne les couvrait : le bloc Chat OTOIA amont gate l'IDENTITÉ de la
# persona/capabilities (ancrée CLAUDE.md) et la COMPOSITION (5 mounts ⇔ 5 portails),
# AVEUGLE à QUEL rôle figure dans quelle `roles_allowed` ; le bloc Fiches Workspace
# gate la membership `Has Role`, un artefact + une sémantique DIFFÉRENTS (accès
# console ≠ droit d'invocation du chat). Ajouter le rôle mobile à une `roles_allowed`
# (SUR-EXPOSITION : le dev mobile gagnerait le droit de piloter l'IA d'un portail
# métier — l'escalade même que la note #6 refuse), retirer le rôle rendu de la
# `roles_allowed` construction (perte du déclenchement assisté), ou le DÉPLACER vers
# un autre portail, laisse la fiche périmée EN SILENCE pendant que l'artefact dit
# autre chose ⇒ l'agent ERPNext Backend câblerait le mauvais contrôle d'accès chat.
# Aucune suite tests/ (qui teste des FONCTIONS de montage/résolution, pas la prose
# d'une fiche) n'attrape ce « vert trompeur ». On RECOMPUTE, depuis chat_mount.json,
# l'ensemble des portails dont la `roles_allowed` porte l'erpnext_role_name du rôle
# (résolu du contrat par id, zéro duplication) et on confronte l'assertion de la
# fiche. Triple ancrage bonus (POSITIVE) : le portail NOMMÉ par la fiche == le
# `portail` du rôle dans le contrat == l'unique portail dont la `roles_allowed`
# le porte. Un claim absent échoue AUSSI (traçabilité · #6).
# ============================================================================
FICHES_CHAT = [
{"file": "03_agents/rendu/AGENT.md", "rid": "faisabilite-rendu-3d",
"kind": "in_named_portail"},
{"file": "03_agents/mobile/AGENT.md", "rid": "plateforme-mobile",
"kind": "not_in_any"},
]
_chat_mounts = _chat_roles = None
try:
_chat_mounts = load("frontend/chat_otoia/out/chat_mount.json")
_chat_roles = load("rbac/rbac_50_roles.json")["roles"]
except (OSError, KeyError, ValueError, TypeError) as e:
bad(f"Fiches Chat · chat_mount.json / rbac_50_roles.json illisible : {e}")
if _chat_mounts is not None and _chat_roles is not None:
# Index recomputé : erpnext_role_name -> ensemble des portails dont la
# `roles_allowed` le porte (le droit d'invoquer le chat de CE portail).
_allow, _portails = {}, set()
for _mnt in _chat_mounts:
_pt = _mnt.get("portail")
_portails.add(_pt)
for _rn in (_mnt.get("roles_allowed") or []):
_allow.setdefault(_rn, set()).add(_pt)
if not _chat_mounts:
bad("Fiches Chat · aucun mount dans chat_mount.json → artefact suspect")
for fc in FICHES_CHAT:
tag = fc["file"].split("/")[1]
cand = [r for r in _chat_roles if r.get("id") == fc["rid"]]
if len(cand) != 1:
bad(f"Fiche {tag} · le contrat porte {len(cand)} rôle(s) `{fc['rid']}` "
f"(attendu exactement 1) → contrat changé de forme, vérifier")
continue
r0 = cand[0]
rname = r0.get("erpnext_role_name")
actual = _allow.get(rname, set()) # portails réels autorisant ce rôle
ftxt = read_norm(fc["file"])
if ftxt is None:
continue
if fc["kind"] == "in_named_portail":
# Le portail NOMMÉ par la fiche est data-derived : c'est le `portail` du
# rôle dans le contrat (triple ancrage). Il DOIT exister comme mount
# (anti-typo : sinon l'assertion « verte » viserait un portail fantôme).
named = r0.get("portail")
if named not in _portails:
bad(f"Fiche {tag} · le rôle `{fc['rid']}` a portail `{named}` (contrat) "
f"MAIS aucun mount chat pour ce portail (mounts : {sorted(_portails)}) "
f"→ contrat/artefact périmé")
continue
# La fiche doit NOMMER ce portail dans son assertion roles_allowed + chat.
if not re.search(rf"`roles_allowed` \(portail {re.escape(named)}\)", ftxt):
bad(f"Fiche {tag} · {fc['file']} — assertion « `roles_allowed` (portail "
f"{named}) » INTROUVABLE (claim absent = régression · #6)")
elif not re.search(r"autorisés à \*\*invoquer le chat OTOIA\*\*", ftxt):
bad(f"Fiche {tag} · {fc['file']} — la fiche nomme le portail mais pas "
f"le droit « invoquer le chat OTOIA » → assertion incomplète (#6)")
elif actual == {named}:
good(f"Fiche {tag} · rôle « {rname} » ∈ roles_allowed du portail "
f"`{named}` (et de lui seul) == chat_mount.json")
elif named in actual:
bad(f"Fiche {tag} · rôle « {rname} » ∈ roles_allowed de `{named}` MAIS "
f"AUSSI {sorted(actual - {named})} — la fiche nomme UN seul portail ; "
f"droit d'invocation élargi dans chat_mount.json → réviser fiche/artefact")
else:
bad(f"Fiche {tag} · {fc['file']} nomme le portail `{named}` MAIS "
f"chat_mount.json autorise « {rname} » sur "
f"{sorted(actual) or 'aucun portail'} → fiche périmée (rôle "
f"retiré/déplacé de roles_allowed), régénérer")
elif fc["kind"] == "not_in_any":
if not re.search(r"volontairement pas\*\* rattaché.*?`roles_allowed` du chat "
r"OTOIA", ftxt):
bad(f"Fiche {tag} · {fc['file']} — assertion négative « volontairement pas "
f"… `roles_allowed` du chat OTOIA » INTROUVABLE (#6)")
elif not actual:
good(f"Fiche {tag} · rôle « {rname} » ABSENT de TOUTE roles_allowed "
f"== chat_mount.json (assertion négative #6 tenue)")
else:
bad(f"Fiche {tag} · {fc['file']} affirme « pas dans roles_allowed » MAIS "
f"chat_mount.json autorise « {rname} » sur {sorted(actual)} → "
f"SUR-EXPOSITION du chat OTOIA, assertion #6 VIOLÉE")
# ============================================================================
# Publiciste/branding · TOKENS DESIGN CANONIQUES « ancrés sur CLAUDE.md #4 »
# — les 4 valeurs de la marque luxury (2 couleurs + 2 typographies), jamais gatées.