Files
system_update/docs/design/tache2/40-contrats-json.md
T
gilles 0fbca06d3d docs: roadmap tâches 1.9-8 (briefs, gates de validation, designs tâche 2) + plans d'implémentation
Cartographie complète (liste_taches/coherence_taches), briefs tacheN + gates
validation_tacheN, design tâche 2 (docs/design/tache2/), specs/plans jalon 1-2
et tâche 1.9/2 (Phase 1, Phase 2, SJ-0→3). Validations consignées (1.9 , 2-8 🟡).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 19:50:25 +02:00

12 KiB

40 — Contrats JSON canoniques étendus + types TypeScript

Axe B + livrable §4.3. Tranche la question §3.5 (extensions de shared/types.ts). Tous les ajouts sont rétro-compatibles : champs optionnels, unions élargies. Un UpdateSnapshot/ExecutionResult du jalon 1 reste strictement valide.


1. Principe de rétro-compatibilité

État actuel (shared/types.ts) :

export type OsFamily = "debian" | "ubuntu" | "unknown";
export type AptProxyMode = "direct" | "runtime";
export type ActionType = "apt_full_upgrade" | "reboot";
// UpdateSnapshot.apt: { enabled, count, rebootRequired, packages: AptPackage[] }
// ExecutionResult: { ... action: ActionType, status, rebootRequiredAfterRun, importantLogLines, rawLogRef, reportRef }

Règles d'extension :

  1. Élargir les unions (OsFamily, AptProxyMode, ActionType) — additif, aucun retrait.
  2. Ajouter des blocs optionnels (docker?, errors?, apt détaillé optionnel, reboot?, postInstall?) — un payload sans ces blocs reste valide.
  3. Ne jamais retirer ni renommer un champ existant. Le jalon 1 émet apt: { enabled, count, rebootRequired, packages } ; on ajoute des champs optionnels à côté.
  4. Versionner via schemaVersion?: number (aligné snapshots.schema_version / executions.schema_version de tache1.9.md). Absence ⇒ version 1.

2. Extensions des unions

// Élargissement additif. Le jalon 1 ("debian"|"ubuntu"|"unknown") reste valide.
export type OsFamily = "debian" | "ubuntu" | "proxmox" | "raspbian" | "unknown";

export type MachineKind =
  | "physical" | "vm" | "proxmox_host" | "lxc"
  | "raspberry_pi" | "workstation" | "unknown";

// "persistent" ajouté (écriture dans /etc/apt/apt.conf.d/).
export type AptProxyMode = "direct" | "runtime" | "persistent";

export type ActionType =
  // jalon 1 (conservés tels quels)
  | "apt_full_upgrade" | "reboot"
  // APT
  | "apt_update_analyze" | "apt_upgrade" | "apt_dist_upgrade"
  | "apt_autoremove" | "apt_clean" | "reboot_verified"
  // Docker
  | "docker_scan" | "docker_inspect_current" | "docker_pull_check"
  | "docker_compose_apply" | "docker_prune_images" | "docker_compose_down"
  // probe + custom
  | "machine_probe" | "post_install";

export type SnapshotStatus = "ok" | "updates_available" | "warning" | "error";
export type ExecutionStatus = "ok" | "warning" | "error"; // inchangé

reboot (jalon 1) et reboot_verified coexistent : reboot_verified ajoute la vérification boot_id ; le code jalon 1 continue d'émettre reboot.


3. Snapshot canonique étendu (UpdateSnapshot)

export interface AptPackage {
  name: string;
  currentVersion: string | null;
  targetVersion: string;
  origin: string | null;
  // Ajouts optionnels (rétro-compatibles) :
  arch?: string;
  operation?: "upgrade" | "install" | "remove" | "hold";
  severityHint?: "normal" | "security";
}

export interface AptSnapshotDetail {
  enabled: boolean;
  count: number;
  rebootRequired: boolean;
  packages: AptPackage[];
  // Ajouts optionnels :
  status?: SnapshotStatus;          // ok | updates_available | warning | error
  upgradeCount?: number;            // simulation `upgrade`
  distUpgradeCount?: number;        // simulation `dist-upgrade`
  installed?: AptPackage[];         // nouveaux paquets (dist-upgrade)
  removed?: AptPackage[];           // suppressions prévues (=> status warning)
  held?: string[];                  // paquets retenus (=> status warning)
  rebootPkgs?: string[];            // depuis reboot-required.pkgs
}

export interface DockerSnapshotService {
  serviceName: string;
  image: string;                    // image ref (ex. jellyfin/jellyfin:latest)
  currentImageId?: string | null;
  currentDigest?: string | null;
  candidateImageId?: string | null; // après pull-check
  candidateDigest?: string | null;
  currentVersion?: string | null;   // label OCI org.opencontainers.image.version
  candidateVersion?: string | null;
  sourceUrl?: string | null;        // label OCI source
  status?: "up_to_date" | "updates_available" | "warning" | "error";
}

export interface DockerSnapshotStack {
  name: string;
  workingDir: string;
  composeFiles: string[];
  projectName?: string | null;
  status: "candidate" | "enabled" | "ignored" | "error";
  detectedBy?: "root_scan" | "label" | "manual";
  services: DockerSnapshotService[];
}

export interface DockerSnapshot {
  enabled: boolean;
  installed: boolean;
  count: number;                    // services avec update dispo
  declaredRoots?: string[];
  stacks: DockerSnapshotStack[];
  status?: SnapshotStatus;
}

export interface SnapshotError {
  source: "apt" | "docker" | "post_install" | "ssh" | "system";
  kind: string;                     // voir 50-erreurs.md (taxonomie)
  severity: "info" | "warning" | "error";
  message: string;                  // nettoyé, jamais de secret
  remediation?: string;
  importantLines?: string[];
}

export interface UpdateSnapshot {
  machineId: string;
  hostname: string;
  os: { family: OsFamily; version: string };
  checkedAt: string;                // ISO 8601
  status: MachineStatus;
  apt: AptSnapshotDetail;           // bloc jalon 1 conservé, champs additifs optionnels
  // Ajouts optionnels (rétro-compatibles) :
  schemaVersion?: number;
  kind?: "apt_update_analyze" | "docker_scan" | "reboot_check" | "combined";
  machineKind?: MachineKind;
  docker?: DockerSnapshot;
  errors?: SnapshotError[];
  rawHints?: { logImportantLines: string[] };
}

Le bloc apt reste requis (présent au jalon 1) ; seuls ses champs additifs sont optionnels. docker, errors, machineKind, kind, schemaVersion sont optionnels → un snapshot jalon 1 (sans eux) reste valide.

Bloc Docker minimal exigé par la validation (couverture §6)

Le bloc snapshot Docker contient au minimum : stacks déclarés (declaredRoots), stacks candidats (stacks[].status="candidate"), services (stacks[].services[]), image ref actuelle (image), image ID actuelle (currentImageId), digest actuel si dispo (currentDigest), labels de version si dispo (currentVersion), image ID/digest candidat après pull (candidateImageId/candidateDigest), statut up_to_date|updates_available|warning|error. ✔


4. Résultat d'exécution étendu (ExecutionResult)

export interface AptChange {
  name: string;
  arch?: string;
  fromVersion: string | null;
  toVersion: string | null;
  operation: "upgraded" | "installed" | "removed" | "unchanged";
  origin?: string | null;
}

export interface AptExecutionResult {
  planned: AptPackage[];            // ce qui était prévu (simulation pré-action)
  applied: AptChange[];             // diff dpkg réel before/after
  installed: AptChange[];
  removed: AptChange[];
  held: string[];
  errors?: SnapshotError[];
  rebootRequiredAfterRun: boolean;
}

export interface DockerImageChange {
  stack: string;
  serviceName?: string;
  imageRef?: string;
  fromImageId?: string | null;
  toImageId?: string | null;
  fromDigest?: string | null;
  toDigest?: string | null;
  operation: "pulled" | "recreated" | "pruned";
}

export interface DockerExecutionResult {
  pull?: { changes: DockerImageChange[]; errors?: SnapshotError[] };
  up?: { recreated: string[]; running: string[]; exited: string[]; errors?: SnapshotError[] };
  prune?: { imagesDeleted: string[]; bytesReclaimed: number; errors?: SnapshotError[] };
  errors?: SnapshotError[];
}

export interface RebootResult {
  beforeBootId: string | null;
  afterBootId: string | null;
  requestedAt: string;
  sshWentDownAt: string | null;
  sshCameBackAt: string | null;
  waitedSeconds: number;
  status: "ok" | "reboot_command_failed" | "ssh_never_went_down"
        | "machine_did_not_return" | "boot_id_unchanged" | "timeout";
  lastRebootDurationSeconds?: number;
  nextRecommendedWaitSeconds?: number;
  errors?: SnapshotError[];
}

export interface PostInstallResult {
  profilesRun: string[];
  variablesUsed: Record<string, string | number | boolean>; // NON sensible uniquement
  filesModified: string[];
  packagesInstalled: string[];
  servicesEnabled: string[];
  rebootsRequested: boolean;
  networkChange?: {
    oldEndpoint: string | null;
    newEndpoint: string | null;
    reconnectHost: string | null;
  };
  errors?: SnapshotError[];
}

export interface ExecutionResult {
  executionId: string;
  machineId: string;
  startedAt: string;
  finishedAt: string;
  mode: "manual" | "scheduled" | "hermes_requested";  // élargi (jalon 1 = "manual")
  action: ActionType;
  status: ExecutionStatus;
  rebootRequiredAfterRun: boolean;
  importantLogLines: string[];
  rawLogRef: string;
  reportRef: string;
  // Ajouts optionnels (rétro-compatibles) :
  schemaVersion?: number;
  apt?: AptExecutionResult;
  docker?: DockerExecutionResult;
  reboot?: RebootResult;
  postInstall?: PostInstallResult;
  errors?: SnapshotError[];
}

mode était "manual" (littéral) au jalon 1. L'élargir en union "manual" | "scheduled" | "hermes_requested" reste compatible (le jalon 1 émet toujours "manual"). Tous les nouveaux blocs (apt, docker, reboot, postInstall, errors) sont optionnels → une exécution jalon 1 reste valide.


5. Extension de TemplateVars (rendu Mustache)

export interface TemplateVars {
  aptProxy?: string | null;       // existant
  // Ajouts (tous optionnels) :
  osProfile?: OsFamily;
  machineKind?: MachineKind;
  confValues?: boolean;
  inactivityTimeout?: number;
  // Docker :
  composeRoots?: string;          // liste rendue shell-safe par le backend
  composeScanDepth?: number;
  stackDir?: string;
  aggressive?: boolean;           // prune agressif
  // Custom :
  operatorUser?: string;
  packages?: string;              // liste shell-safe
  newHostname?: string;
  interfaceName?: string;
  staticAddress?: string;
  reconnectHost?: string;
  dockerUser?: string;
  composeRoot?: string;
  rebootAfterInstall?: boolean;
  // ... champs de profil custom (typés au cas par cas en tâche 4)
}

6. Déduplication (empreinte fonctionnelle)

  • APT : dedupKey = os_family + "|" + package + "|" + from + "|" + to + "|" + origin. Permet à Hermes de mutualiser une même mise à jour vue sur plusieurs machines (une seule recherche web, un seul résumé). Stocké dans apt_planned_packages.dedup_key / apt_applied_packages.dedup_key.
  • Docker : dedupKey = image + "|" + fromDigest + "|" + toDigest ; fallback image + "|" + fromImageId + "|" + toImageId quand le digest manque.

Le calcul de dedupKey se fait côté backend TS (déterministe), pas dans le shell.


7. Réduction déterministe avant Hermes/MCP

Le réducteur actuel (server/templates/aptReduce.ts) garde les lignes : Inst , Conf , Remv , Err , E:, W:, dpkg:, reboot-required/REBOOT_REQUIRED. Extension proposée (renommage suggéré reduceLines.ts, additif, sans casser reduceAptLines) ajoutant les préfixes Docker : Pulling, Digest, Status, Downloaded newer image, Recreating, Started, Error, deleted, Total reclaimed space.

Ce que Hermes reçoit : JSON canonique réduit (important_json) + lignes importantes (importantLogLines). Jamais le log brut complet (archivé dans raw_artifacts/rawLogPath), jamais de secret.


8. Mapping vers tache1.9.md (tables dérivées)

Bloc JSON Table dérivée Colonnes clés
apt.packages / installed / removed / held (simulation) apt_planned_packages mode, operation, current_version, target_version, origin, dedup_key
apt.applied (diff dpkg) apt_applied_packages from_version, to_version, operation, dedup_key
errors[] source apt apt_errors kind, severity, message, important_lines_json, remediation
docker.stacks[] docker_compose_stacks + docker_stack_services status, detected_by, current_image_id, candidate_digest, …
docker.pull/up/prune changes docker_image_events from_image_id, to_image_id, operation, bytes_reclaimed
lignes importantes/notices important_messages source, category, package_name, message
payload complet snapshot snapshots.payload_json + important_json kind, schema_version, status
payload complet exécution executions.result_json + important_json error_kind, error_message, exit_code

Le JSON complet reste la vérité canonique (archivé) ; les tables dérivées servent recherche/filtres/dédup/badges (conforme à la règle structurante tache1.9.md §2).