# Tâche 2 — SJ-5 : Docker pull-check + comparaison déterministe > Statut : **implémenté** (2026-06-05). tsc 0 erreur · 85 tests · build OK. > Réf. design : `docs/design/tache2/20-docker.md §4.3`, `40-contrats-json.md §3/§6`, `80-sous-jalons.md` SJ-5. ## Périmètre livré Télécharger les images candidates d'un stack Compose **sans démarrer de conteneur** (`docker compose pull`), comparer avant/après par image ID + repo digest + label OCI, et persister l'état des services — **sans toucher au flux jalon 1** et sans déclencher de pull automatique (action manuelle par stack, non incluse dans `refreshMachine`). ## Composants - **Template** `templates/docker/pull-check.sh.tpl` — délimiteurs Mustache `<% %>` (`<%stackDir%>`), Go-templates `{{.Id}}` / `{{join .RepoDigests ","}}` préservés. Sections `===SU:DOCKER_INSPECT_BEFORE/PULL/INSPECT_AFTER===` + `===SU:EXIT=N===`. - **`server/services/dockerPull.ts`** : - `parseDockerPullCheck(raw)` — lit BEFORE/AFTER (id, digest, version), code de sortie, et extrait les erreurs de pull **nettoyées de tout secret** (URLs, token/bearer/password). - `buildDockerPullResult(stackName, raw)` — comparaison déterministe → `services` (`up_to_date | updates_available | error` par image) + `changes` (`operation:"pulled"` uniquement pour les images modifiées) + `status` global + `errors`. - `dockerDedupKey(image, fromDigest, toDigest, fromId?, toId?)` — empreinte fonctionnelle (digests prioritaires, fallback image IDs), conforme `40 §6`. - `pullCheckStack(machineId, stackId, onData?)` — orchestration SSH + upsert des services dans `docker_stack_services` (par `stackId + serviceName`), maj `lastUpdateAt` du stack et `lastPullCheckAt` des settings. **Refuse un stack non `enabled`.** - **`server/services/dockerPull.test.ts`** — 7 cas (parse, nettoyage secret registry, classement up_to_date/updates_available, change unique, status global, dédup). - **Wiring** : - `runAction(machineId, action, opts?: { stackId })` — branche dédiée `docker_pull_check` (archivage report/log, `ExecutionResult.docker.pull.changes` + `dedupKey`, event). - Chemin générique : injection `stackDir` quand `stackId` fourni → **corrige aussi `docker_inspect_current`** (SJ-4 le déclarait sans orchestration par stack). - `POST /:id/actions` — allowlist élargie aux actions Docker passives/non-applicatives (`docker_scan`, `docker_inspect_current`, `docker_pull_check`) ; `stackId` requis pour les actions par-stack. **Destructives (apply/down/prune agressif) toujours hors API** jusqu'au socle `action_requests` (SJ-6). - **`shared/types.ts`** : `DockerImageChange.dedupKey?` (additif, pour mutualisation Hermes). ## Pas de migration Le schéma SJ-4 (`docker_stack_services` avec `current/candidate_image_id|digest`, `version_label`, `status` ; `docker_settings.last_pull_check_at`) couvrait déjà SJ-5. ## Sécurité - `docker compose pull` sans `up` → aucun conteneur recréé (pré-check pur applicatif). - Erreurs registry (`registry_auth_failed` / `pull_failed`) **nettoyées** : ni URL, ni token, ni mot de passe ne remontent vers UI/MCP (test dédié). - Credentials registry (`~/.docker/config.json`) jamais lus ni renvoyés. ## Reste pour SJ-6 `docker_compose_apply` (up -d --remove-orphans), `docker_prune_images`, `docker_compose_down`, table `docker_image_events`, et validation UI explicite via `action_requests`.