Files
Alfredv2/alfred/app/services/alfred_hermes_session_page.py
T

90 lines
3.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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)