# EXP-005 — Première écriture contrôlée

## Objectif

Créer un seul brouillon WordPress identifiable via une Ability MCP dédiée, puis démontrer que la publication n’est pas disponible par cette Ability et restaurer l’état métier.

## Hypothèse

La capability `edit_posts` suffit pour créer un brouillon avec un Contributor, tandis que l’absence de `publish_posts` et l’imposition serveur de `draft` empêchent la publication.

## Environnement

WordPress 7.0.4, PHP 8.4.24, MCP Adapter 0.5.0, transport STDIO, client `exp-003-stdio-probe` 1.0, site `https://www.lab.quantumdev.fr`.

## Préconditions

Le Subscriber `quantumdev_mcp_lab` existait et a été conservé sans élévation. Aucun brouillon `[LAB]` EXP-005 n’existait. Le rollback EXP-003 était disponible.

## Architecture

Le plugin isolé `quantumdev-lab-abilities` a évolué de 0.1.0 à 0.2.0. Une seule Ability d’écriture a été ajoutée ; `quantumdev/site-summary` n’a pas été modifiée fonctionnellement.

## Identité d’exécution

`quantumdev_mcp_writer_lab`, utilisateur dédié de rôle Contributor, ID 3. Aucun mot de passe ou Application Password n’a été créé ou stocké.

## Capability choisie

Le permission callback utilise uniquement `current_user_can('edit_posts')`.

## Ability

### Nom

`quantumdev/create-lab-draft`

### Input schema

Objet strict avec seulement `title` (1–120 caractères) et `content` (1–2000 caractères), propriétés supplémentaires interdites.

### Output schema

`post_id`, `title`, `status`, `author_id`, `author_login`, `email_attempts_blocked`.

### Permission

Subscriber : refusé. Contributor : `edit_posts=true`, permission autorisée, `publish_posts=false`.

### Annotations

`readonly=false`, `destructive=false`, `idempotent=false`, `openWorldHint=false`.

### Exposition MCP

`meta.mcp.public=true`, `meta.mcp.type=tool`. Découverte MCP réussie.

### Exposition REST

`show_in_rest=false`.

## Barrière de publication

Le client ne peut fournir ni statut, ni auteur, ni type, ni ID existant. Le callback impose `post_type=post`, `post_status=draft`, `post_author` égal à l’utilisateur courant, commentaires et ping fermés. Aucun chemin `publish`, `future` ou `private` n’existe.

## Protection email

Un filtre `pre_wp_mail` local à la requête a intercepté les éventuelles tentatives pendant `wp_insert_post()`. Résultat : 0 tentative observée et aucun email réel envoyé.

## Audit

Les hooks `wp_before_execute_ability` et `wp_after_execute_ability` ont écrit dans le `error_log` existant uniquement le timestamp, l’Ability, l’utilisateur, puis l’ID et le statut. Aucun contenu n’est journalisé.

## Test Subscriber

`quantumdev_mcp_lab` a été correctement refusé (`edit_posts=false`, permission false).

## Test Contributor

`quantumdev_mcp_writer_lab` a été autorisé par `edit_posts`; `publish_posts` est resté false.

## Test de schéma négatif

L’entrée contenant `post_status=publish` a été rejetée par `ability_invalid_input` avant le callback. `WRITE_COUNT=0`.

## Création MCP

Un seul appel `mcp-adapter-execute-ability` a ciblé `quantumdev/create-lab-draft` avec le Contributor. Un seul post a été créé : ID 6.

## Objet réellement créé

Titre `[LAB] EXP-005 Controlled Draft`, type `post`, statut `draft`, auteur ID 3, commentaires et ping `closed`. Le contenu commence par `QUANTUMDEV WORDPRESS LAB — EXP-005`.

## Validation auteur

L’ID d’utilisateur MCP, `post_author` et `author_id` retourné sont cohérents : 3, login `quantumdev_mcp_writer_lab`.

## Validation statut

Le statut réellement enregistré est `draft`. Aucune publication n’a eu lieu.

## État avant / après

Avant : 1 article, 0 brouillon, 2 pages. Après écriture : 2 articles, 1 brouillon `[LAB]` et 1 article publié. Aucun autre changement métier observé.

## Rollback exécuté

Le post ID 6 a été supprimé définitivement après lecture et preuve de son identité.

## État après rollback

ID 6 absent, 1 article, 0 brouillon, 2 pages, aucun brouillon `[LAB]` EXP-005. `BUSINESS_STATE_RESTORED=YES`. `site-summary` retourne à nouveau 1 page publiée et 1 article publié.

## Logs / preuves

Les preuves sont dans `lab/output/EXP-005/`. L’audit persistant est démontré dans `error_log`, avec utilisateur, Ability, timestamps, post ID et statut.

## Écart attendu / observé

Le comportement attendu a été observé. Une anomalie de vérification inline avant suppression (comparaison stricte d’un ID retourné en chaîne) a été documentée ; la suppression est restée ciblée sur l’ID 6 déjà confirmé par `created-post.json`.

## Risques

L’Ability écrit réellement ; son exposition MCP doit donc rester limitée et sa validation de schéma stricte. Le compte Contributor est conservé comme infrastructure de lab et ne possède pas `publish_posts`.

## Conclusion

Un Contributor peut créer un brouillon via MCP avec `edit_posts`, mais ne peut pas publier via cette Ability. Le statut, l’auteur, le type et l’absence de mise à jour sont imposés côté serveur. L’unique brouillon a été supprimé et l’état métier restauré.

## Niveau de confiance

fort

## Production

acceptable sous conditions

## Matière éditoriale

### Fait

Une capability `edit_posts` permet une écriture bornée sans capability de publication.

### Surprise

Le schéma rejette `post_status=publish` avant même l’entrée dans le callback.

### Chiffre

1 appel MCP, 1 brouillon créé, 0 email, 0 publication, puis suppression ciblée.

### Erreur

La première vérification inline de pré-suppression était trop stricte sur le type de l’ID ; l’anomalie a été signalée et n’a pas élargi la cible de suppression.

### Décision QuantumDev

Imposer côté serveur tous les champs sensibles et traiter les capacités WordPress comme la frontière d’autorisation.

### Capture exploitable

La paire `schema-rejection.txt` / `created-post.json`, suivie de `rollback-execution.txt` et `state-after-rollback.txt`.

### Angle LinkedIn

La sécurité d’un agent ne repose pas sur sa bonne volonté : elle repose sur un schéma fermé, une capability minimale et un statut imposé côté serveur.

### Niveau de preuve

démontré
