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>
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. UnUpdateSnapshot/ExecutionResultdu 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 :
- Élargir les unions (
OsFamily,AptProxyMode,ActionType) — additif, aucun retrait. - Ajouter des blocs optionnels (
docker?,errors?,aptdétaillé optionnel,reboot?,postInstall?) — un payload sans ces blocs reste valide. - Ne jamais retirer ni renommer un champ existant. Le jalon 1 émet
apt: { enabled, count, rebootRequired, packages }; on ajoute des champs optionnels à côté. - Versionner via
schemaVersion?: number(alignésnapshots.schema_version/executions.schema_versiondetache1.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) etreboot_verifiedcoexistent :reboot_verifiedajoute la vérification boot_id ; le code jalon 1 continue d'émettrereboot.
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
aptreste requis (présent au jalon 1) ; seuls ses champs additifs sont optionnels.docker,errors,machineKind,kind,schemaVersionsont 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é dansapt_planned_packages.dedup_key/apt_applied_packages.dedup_key. - Docker :
dedupKey = image + "|" + fromDigest + "|" + toDigest; fallbackimage + "|" + fromImageId + "|" + toImageIdquand 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).