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

312 lines
12 KiB
Markdown

# 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`) :
```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
```ts
// É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`)
```ts
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`)
```ts
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)
```ts
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`).