concierge: lire airtria_ro depuis l'env/.env + alerte d'echec d'auth

Le mot de passe airtria_ro est desormais lu depuis AIRTRIA_RO_PASSWORD
(variable d'environnement, sinon .env monte dans le conteneur), plus depuis
.pg-airtria : EVCS renouvelle ce mot de passe, un fichier fige faisait echouer
l'auth EN SILENCE. readPgPassword LEVE (rattrapable) au lieu de process.exit ;
mot de passe resolu paresseusement dans makeClient.

Nouveau module concierge-health.js (meme patron que le moniteur LLM) : battement
de coeur horodate a CHAQUE cycle detect, statut live dans HA, et alerte WhatsApp
UNIQUEMENT sur transition ok<->erreur (dedup, pas de rappel periodique). Chemin
HA independant de PostgreSQL : l'alerte auth KO part meme quand c'est l'auth qui
tombe. Best-effort : si les helpers HA manquent, le concierge n'echoue pas.

- concierge-health.test.js : machine a etats (dedup + transitions) verifiee, sans reseau
- concierge-health-ha-package.yaml.draft : helpers HA (a integrer cote proprietaire)
- SPEC : source du secret corrigee + section supervision

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RZsbhyGmRwZogdoBwT6C6y
This commit is contained in:
2026-09-16 03:26:50 +02:00
parent d0805331ad
commit 6da250914d
6 changed files with 276 additions and 16 deletions

View File

@@ -18,28 +18,52 @@ const path = require('path');
const { Client } = require('pg'); // dépendance à ajouter : npm install pg
// --- Paramètres -------------------------------------------------------------
const PG = {
// Connexion PG sans mot de passe ici : il est résolu PARESSEUSEMENT dans makeClient()
// (voir readPgPassword), pour qu'un secret absent LÈVE une erreur rattrapable au moment de
// la détection, et non un process.exit au chargement du module. L'étage live la transforme
// alors en alerte HA/WhatsApp — jamais en mort silencieuse.
const PG_BASE = {
host: process.env.PGHOST || '10.10.10.22',
port: Number(process.env.PGPORT || 5433),
database: process.env.PGDATABASE || 'evcs',
user: process.env.PGUSER || 'airtria_ro',
// mot de passe lu depuis un fichier protégé, jamais en dur ni en argument
password: readSecret(process.env.PG_PASS_FILE ||
path.join(__dirname, '.pg-airtria')),
};
// Règle invité (voir SPEC, sens unique) : conclusif SEULEMENT au-dessus du seuil.
const CONSIGNE_MIN_A = 20; // en dessous : aucune information → demander
const P_INVITE_W = 4200; // VW/JEEP ~3,6-3,8 kW ; Megane 6,36 kW à 28 A
function readSecret(file) {
try {
return fs.readFileSync(file, 'utf8').trim();
} catch (e) {
console.error(`[concierge] Mot de passe introuvable (${file}). ` +
`Dépose-le en lecture seule (chmod 600) avant de lancer.`);
process.exit(2);
// Mot de passe airtria_ro — source, dans l'ordre :
// 1. variable d'environnement AIRTRIA_RO_PASSWORD (si un jour injectée par le compose
// via env_file — un ajout au .env du compose exige alors de RECRÉER le conteneur) ;
// 2. lecture du .env monté dans le conteneur : /home/node/.openclaw/.env, soit côté hôte
// /mnt/ARCH3/docker/openclaw-data/.env. C'EST la source réelle aujourd'hui : le
// propriétaire y met AIRTRIA_RO_PASSWORD, et comme ce fichier est monté, l'éditer
// suffit (aucune recréation). OPENCLAW_ENV_FILE peut désigner un autre fichier (tests).
// Pas de repli sur un fichier de secret dédié : EVCS possède le rôle et RENOUVELLE le mot
// de passe ; un fichier figé (.pg-airtria) donnerait un secret périmé et ferait échouer
// l'auth EN SILENCE. Si rien n'est trouvé, on LÈVE (rattrapé par l'étage live → alerte).
function envValue(file) {
const txt = fs.readFileSync(file, 'utf8');
for (const line of txt.split('\n')) {
const m = line.match(/^\s*(?:export\s+)?AIRTRIA_RO_PASSWORD\s*=\s*(.*)$/);
if (m) {
let v = m[1].trim();
if ((v.startsWith('"') && v.endsWith('"')) ||
(v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1);
if (v) return v;
}
}
return null;
}
function readPgPassword() {
if (process.env.AIRTRIA_RO_PASSWORD) return process.env.AIRTRIA_RO_PASSWORD;
const envFile = process.env.OPENCLAW_ENV_FILE || path.join(__dirname, '..', '.env');
try { const v = envValue(envFile); if (v) return v; } catch { /* .env absent/illisible ici */ }
throw new Error('AIRTRIA_RO_PASSWORD introuvable (ni variable d\'environnement, ni ' +
`${envFile}) — ajouter la clé au .env puis relancer ` +
'(si c\'est le .env du compose, recréer le conteneur)');
}
// --- Requêtes (lecture seule) ----------------------------------------------
@@ -129,7 +153,7 @@ async function main() {
const since = process.env.SINCE || '2026-09-08T19:00:00+02:00';
const until = process.env.UNTIL || '2026-09-13T00:00:00+02:00';
const client = new Client(PG);
const client = makeClient();
await client.connect();
try {
const tr = await client.query(SQL_TRANSITIONS, [since, until]);
@@ -183,7 +207,7 @@ async function main() {
}
}
function makeClient() { return new Client(PG); }
function makeClient() { return new Client({ ...PG_BASE, password: readPgPassword() }); }
if (require.main === module) {
main().catch(e => { console.error('[concierge]', e.message); process.exit(1); });