8.6 KiB
SPECS — POST /api/v1/lea/competences/candidate/persist
DRAFT specs pour implémentation — pas encore appliqué — relecture Codex/Dom attendue
Date : 2026-06-01 Auteur : Claude (agent Plan) Statut : DRAFT — aucun code écrit, aucun fichier de prod modifié Cible :
agent_v0/server_v1/api_stream.py
Contexte
L'audit Claude du 2026-06-01 17h00 confirme l'absence de ce endpoint. Le cycle Shadow apprentissage actuel s'arrête à /api/v1/shadow/build (renvoie le workflow_ir en mémoire, ne le persiste pas).
Ce endpoint clôt le cycle Léa-first (6 phases) :
start → stop → understanding → feedback (N×) → build → **persist**
Sources :
docs/coordination/inbox_codex/2026-06-01_1620_claude-to-codex_ARCHI-apprentissage-Lea-first-validee-Dom.mddocs/coordination/inbox_codex/2026-06-01_1745_claude-to-codex_ADDENDUM-archi-Lea-lecture-semantique-agent-externe.mddocs/coordination/inbox_codex/2026-06-01_1800_claude-to-codex_PRECISIONS-desapprentissage-regle-or-HDS.md
Schéma YAML de référence : data/competences/candidate/key_win_r_wait_explorer_exe.yaml
Sommaire
- Contrat de l'endpoint
- Validation pré-écriture
- Génération YAML
- Journalisation
- Cas particuliers
- Sécurité
- Tests à prévoir
- Estimation effort implémentation
1. Contrat de l'endpoint
Méthode / URL : POST /api/v1/lea/competences/candidate/persist
Headers requis :
Authorization: Bearer <token-poste>(cohérent avec patch P0 révocation token Codex)Content-Type: application/json
Payload entrée (JSON) :
{
"name": "Saisir texte Word",
"machine_id": "DESKTOP-58D5CAC_windows",
"session_id": "sess_20260601T180000_abc123",
"workflow_ir": { "steps": [...], "preconditions": [...], "success_marker": {...} },
"parameters": [ { "name": "texte", "type": "string", "required": true } ],
"external_decision": {
"agent_id": "t2a_v2_codeur",
"decision_contract": "..."
},
"annotations_semantiques": {
"intent_fr": "saisir le texte transmis dans Word"
},
"learning_metadata": {
"persist_id": "uuid-v4-client",
"partial": false,
"dropout_reason": null,
"source_phase": "shadow_build"
}
}
Codes HTTP :
201 Created— YAML écrit, audit journalisé400 Bad Request— payload invalide (schema, slug, agent externe inconnu)401 Unauthorized— token absent / invalide / révoqué403 Forbidden—machine_iddu payload ≠ machine du token409 Conflict— slug existe déjà dansdata/competences/candidate/429 Too Many Requests— rate limit dépassé500 Internal Server Error— échec atomic write ou roundtrip
Payload sortie (201) :
{
"competence_id": "saisir_texte_word",
"yaml_path": "data/competences/candidate/saisir_texte_word.yaml",
"learning_state": "candidate",
"persist_id": "uuid-v4-serveur",
"audit_entry_id": 4271
}
2. Validation pré-écriture
Slugification (name → competence_id) :
- Conversion kebab-case ASCII (translittération accents)
- Lowercase, espaces →
_, chars non[a-z0-9_]retirés - Longueur ≤ 80 caractères, ≥ 3 caractères
- Pattern regex final :
^[a-z][a-z0-9_]{2,79}$ - Si collision avec un fichier existant en
candidate/: 409 sans suffixage automatique
Schema YAML cohérent — champs obligatoires : schema_version, id, name, version, learning_state, intent, parameters, preconditions, methods, success_marker, failure_message_template, promotion, generalisation, failure_log, created_at, last_updated_at, methods_execution
WorkflowIR :
workflow_ir.stepsnon vide sauf silearning_metadata.partial == true- Chaque step possède
kind∈ primitives connues (réutiliser registry primitives existante)
Idempotence :
persist_id(UUID v4 client) déduit la clé d'idempotence- Si
persist_iddéjà présent danspersist_audit.jsonl→ renvoyer200 OKavec le payload de l'entrée précédente (pas 409)
3. Génération YAML
Chemin cible : data/competences/candidate/<slug>.yaml
Atomic write :
- Écrire dans
data/competences/candidate/.<slug>.yaml.tmp.<persist_id> os.rename()vers<slug>.yaml(atomique POSIX)- Si collision détectée entre validation et rename → 409, supprimer le
.tmp
Post-write roundtrip :
- Recharger via
core.competences.catalog.load_competence(slug) - Vérifier que
competence.to_dict()est cohérent - Si KO → 500, supprimer le fichier, log critique
4. Journalisation
Audit principal — append-only data/competences/persist_audit.jsonl :
{
"persist_id": "uuid-v4",
"timestamp": "2026-06-01T18:05:32+02:00",
"machine_id": "...",
"session_id": "...",
"competence_name": "Saisir texte Word",
"competence_id": "saisir_texte_word",
"yaml_path": "data/competences/candidate/saisir_texte_word.yaml",
"learning_state": "candidate",
"partial": false,
"dropout_reason": null,
"external_agent_id": null,
"audit_entry_id": 4271
}
Apprentissages incomplets — si partial: true, entrée en plus dans data/competences/incomplete_learnings.jsonl avec dropout_reason obligatoire.
Append-only strict : ouvrir en mode a, jamais réécrire. Verrou fcntl.flock par fichier.
5. Cas particuliers
| Cas | Comportement |
|---|---|
learning_metadata.partial == true |
accepté, learning_state forcé incomplete, entrée double dans incomplete_learnings.jsonl, dropout_reason obligatoire (sinon 400) |
Payload demande learning_state: stable |
rejet silencieux — forcer candidate (jamais stable par persist direct — règle d'or HDS) |
Aucun learning_state fourni |
défaut candidate |
external_decision.agent_id inconnu de ExternalDecisionClient registry |
400 avec liste agents valides |
workflow_ir.steps vide et partial == false |
400 |
YAML déjà présent en data/competences/stable/ ou supervised/ avec même slug |
409 (collision cross-states) |
| Désapprentissage en cours sur le slug | 409 detail: "désapprentissage actif" |
6. Sécurité
- Token Bearer obligatoire — réutiliser middleware d'auth du patch P0 révocation token. Pas de fallback "agent local".
- Couplage machine_id ↔ token : token référence
machine_id; sipayload.machine_id != token.machine_id→ 403. - Rate limit : 10 persists/min/
machine_id(in-memory token-bucket). Au-delà → 429 avecRetry-After. - Pas d'écriture hors
data/competences/candidate/: path traversal interdit (slug strict, jamais lu d'un champ user). - Logs : ne jamais journaliser le token.
persist_idetmachine_idonly. - Règle d'or HDS : aucune donnée patient ne doit transiter — refuser le payload si
workflow_irouannotations_semantiquescontient pattern PII détecté. Détection MVP via regex paramétrable.
7. Tests à prévoir
Unit (tests/unit/test_competence_persist.py) :
test_slug_generation_normal/_with_accents/_too_short/_collision_409test_yaml_schema_required_fields_presenttest_atomic_write_then_renametest_post_write_roundtrip_ok/_corrupt_yaml_500test_idempotence_same_persist_id_returns_previoustest_partial_true_forces_incomplete_statetest_payload_stable_forced_to_candidatetest_external_agent_unknown_400test_pii_detected_rejected
Integration (tests/integration/test_shadow_full_cycle.py) :
- Cycle complet :
shadow/start → POST événements → shadow/stop → shadow/build → persist - Vérifier YAML présent,
persist_audit.jsonlenrichi - Cas partial : double entrée
incomplete_learnings.jsonl
Sécurité (tests/security/test_persist_auth.py) :
test_no_token_401test_token_revoked_401test_machine_id_mismatch_403test_rate_limit_11th_call_429test_path_traversal_in_name_blocked
8. Estimation effort implémentation
| Item | Volume |
|---|---|
| Handler FastAPI + Pydantic models | ~80-120 lignes (api_stream.py) |
| Helpers (slugify, atomic_write, audit_append) | ~40 lignes (nouveau module core/competences/persist.py) |
| Tests unit + integration + sécu | ~150 lignes |
Effort total : faible-moyen — 0.5 à 1 jour. Pas de dépendance externe nouvelle, réutilise core.competences.catalog et le pattern atomic-write de C-γ.
Points d'attention pour la review :
- Confirmer comportement collision (409 strict vs suffixage
_v2) - Valider mapping
workflow_ir.steps[*].kind→yaml.methods[*].kindavec registry primitives à jour - Décider si rate limit par
machine_idou par token - Statuer sur format
audit_entry_id(auto-incrément vs hash)
Fin DRAFT — relecture Codex/Dom attendue avant kick-off implémentation.