v2.2 - Small improvement Interface change

This commit is contained in:
pporcheret
2026-10-29 11:13:02 +01:00
parent a17c3cb614
commit 7f26f9b015
271 changed files with 6207 additions and 16044 deletions
@@ -0,0 +1,89 @@
"""Reset only the Hermes conversation ID used by Alfred.
Admin-protected, CSRF-protected, and concurrency-safe with ask_hermes().
Does NOT delete the actual Hermes conversation history or change MCP/skills.
"""
from __future__ import annotations
import fcntl
import os
from pathlib import Path
import secrets
from datetime import datetime, timezone
from flask import Blueprint, jsonify
from services import admin_console, hermes_client
class HermesSessionBusy(RuntimeError):
"""Hermes is currently processing a message."""
def archive_current_session() -> bool:
"""Return True when an existing session was archived.
The exact same thread lock and filesystem lock are used by ask_hermes.
Both locks are non-blocking here to keep HTTP responsive.
"""
if os.getenv('HERMES_SESSION_MODE', 'persistent').strip().lower() == 'legacy':
raise ValueError('Hermes est en mode legacy : les conversations sont déjà indépendantes.')
request_lock = hermes_client._REQUEST_LOCK
if not request_lock.acquire(blocking=False):
raise HermesSessionBusy('Hermes traite une demande. Réessaie lorsqu’il a terminé.')
try:
path = hermes_client._session_file()
if path.is_symlink():
raise ValueError('Fichier de session symbolique non autorisé.')
path.parent.mkdir(parents=True, exist_ok=True)
lock_path = path.with_name(path.name + '.lock')
flags = os.O_CREAT | os.O_RDWR
if hasattr(os, 'O_NOFOLLOW'):
flags |= os.O_NOFOLLOW
fd = os.open(lock_path, flags, 0o600)
with os.fdopen(fd, 'r+') as lock_file:
try:
fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
except BlockingIOError as exc:
raise HermesSessionBusy('Une conversation Hermes est en cours. Réessaie plus tard.') from exc
try:
if not path.exists():
return False
if not path.is_file():
raise ValueError('État de session Hermes inattendu.')
# Archive the file even if its JSON is invalid: this action is also a recovery path.
archive_dir = path.parent / 'hermes_session_archives'
if archive_dir.is_symlink():
raise ValueError('Répertoire d’archives symbolique non autorisé.')
archive_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
stem = datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%SZ')
name = f'{path.stem}-{stem}-{secrets.token_hex(4)}.json'
os.replace(path, archive_dir / name)
print('[ALFRED] Nouvelle session Hermes demandée depuis l’interface administrateur.', flush=True)
return True
finally:
fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)
finally:
request_lock.release()
def register_hermes_session_page(app):
bp = Blueprint('alfred_hermes_session_button_v1', __name__)
@bp.post('/api/control/hermes/new-session')
@admin_console._require_admin
def new_session():
try:
archived = archive_current_session()
except HermesSessionBusy as exc:
return jsonify(ok=False, error=str(exc)), 409
except (OSError, ValueError, RuntimeError) as exc:
app.logger.warning('Nouvelle session Hermes impossible : %s', exc)
return jsonify(ok=False, error='Impossible de réinitialiser la session : '+str(exc)), 503
response = jsonify(ok=True, archived=archived,
message=('Session précédente sauvegardée. La prochaine demande ouvrira une nouvelle session Hermes.'
if archived else 'Aucune session précédente : Hermes en créera une à la prochaine demande.'))
response.headers['Cache-Control'] = 'no-store, private'
return response
app.register_blueprint(bp)