From 90ed905efc0f835f794f4ed2087068f75f26a0c6 Mon Sep 17 00:00:00 2001 From: iacker Date: Mon, 13 Jul 2026 09:55:56 +0200 Subject: [PATCH 01/14] docs: add French translations (README, CONTRIBUTING, SECURITY) Adds French versions of the core documentation following the existing locale-suffix convention (README.es.md, CONTRIBUTING.es.md, SECURITY.es.md): - README.fr.md - CONTRIBUTING.fr.md - SECURITY.fr.md Translated from the current English sources by a native French speaker. Technical identifiers (commands, paths, env vars, provider/model names, URLs, code blocks) are kept verbatim. The English LICENSE remains the authoritative legal version. Closes #60535 Signed-off-by: iacker --- CONTRIBUTING.fr.md | 1022 ++++++++++++++++++++++++++++++++++++++++++++ README.fr.md | 265 ++++++++++++ SECURITY.fr.md | 371 ++++++++++++++++ 3 files changed, 1658 insertions(+) create mode 100644 CONTRIBUTING.fr.md create mode 100644 README.fr.md create mode 100644 SECURITY.fr.md diff --git a/CONTRIBUTING.fr.md b/CONTRIBUTING.fr.md new file mode 100644 index 0000000000000..f6330cba0e906 --- /dev/null +++ b/CONTRIBUTING.fr.md @@ -0,0 +1,1022 @@ +# Contribuer à Hermes Agent + +Merci de contribuer à Hermes Agent ! Ce guide couvre tout ce dont vous avez besoin : mettre en place votre environnement de développement, comprendre l'architecture, décider quoi construire et faire fusionner votre PR. + +--- + +## Priorités de contribution + +Nous valorisons les contributions dans cet ordre : + +1. **Corrections de bugs** — plantages, comportement incorrect, perte de données. Toujours la priorité absolue. +2. **Compatibilité multiplateforme** — macOS, différentes distributions Linux et WSL2 sous Windows. Nous voulons qu'Hermes fonctionne partout. +3. **Durcissement de la sécurité** — injection shell, injection de prompt, traversée de chemins, élévation de privilèges. Voir [Sécurité](#considérations-de-sécurité). +4. **Performance et robustesse** — logique de nouvelle tentative, gestion des erreurs, dégradation contrôlée. +5. **Nouvelles compétences** — mais uniquement celles largement utiles. Voir [Compétence ou outil ?](#compétence-ou-outil-) +6. **Nouveaux outils** — rarement nécessaires. La plupart des capacités devraient être des compétences. Voir plus bas. +7. **Documentation** — corrections, clarifications, nouveaux exemples. + +--- + +## Avant de commencer : cherchez d'abord + +Une recherche rapide avant de vous lancer vous fait gagner du temps et garde la file des PR propre — les doublons sont fréquents ici, alors une minute en amont vaut le coup. + +- **Cherchez dans les PR et issues ouvertes *et* fusionnées** votre sujet ou le symptôme de votre erreur — la vérification de doublons du template de PR n'intervient qu'au moment de la revue, une fois le travail déjà fait : + ```bash + gh search issues --repo NousResearch/hermes-agent "" + gh search prs --repo NousResearch/hermes-agent --state all "" + ``` + Ou passez par l'interface web : [issues](https://github.com/NousResearch/hermes-agent/issues?q=) · [PRs (tous états)](https://github.com/NousResearch/hermes-agent/pulls?q=is%3Apr). +- **Le suivi des issues peut être en retard sur le code.** Beaucoup de fonctionnalités demandées sont déjà implémentées dans l'arborescence ; cherchez donc aussi la capacité dans le code source (`search_files`, ou le grep de votre éditeur) avant de la proposer. +- **Si une PR ouverte traite déjà le sujet**, envisagez de la relire ou de l'améliorer plutôt que d'ouvrir un doublon concurrent. +- **Pour les travaux d'envergure**, commentez l'issue pour signaler que vous vous en occupez, afin que personne d'autre ne démarre la même chose. + +En lien : #38284 couvre le pendant côté agent — Hermes lui-même vérifiant les issues et PR existantes avant de se lancer dans un auto-dépannage approfondi. Cette section en est le complément pour les contributeurs humains. + +--- + +## Compétence ou outil ? + +C'est la question la plus fréquente chez les nouveaux contributeurs. La réponse est presque toujours **compétence**. + +### Faites-en une compétence quand : + +- La capacité peut s'exprimer comme des instructions + des commandes shell + des outils existants +- Elle enveloppe une CLI externe ou une API que l'agent peut appeler via `terminal` ou `web_extract` +- Elle n'a pas besoin d'intégration Python sur mesure ni de gestion de clés API embarquée dans l'agent +- Exemples : recherche arXiv, workflows git, gestion de Docker, traitement de PDF, e-mail via des outils CLI + +### Faites-en un outil quand : + +- Il exige une intégration de bout en bout avec clés API, flux d'authentification ou configuration multi-composants gérée par le harnais de l'agent +- Il nécessite une logique de traitement sur mesure qui doit s'exécuter avec précision à chaque fois (pas du « au mieux » issu de l'interprétation du LLM) +- Il manipule des données binaires, du streaming ou des événements temps réel qui ne peuvent pas passer par le terminal +- Exemples : automatisation de navigateur (gestion de sessions Browserbase), TTS (encodage audio + livraison sur la plateforme), analyse de vision (manipulation d'images en base64) + +### La compétence doit-elle être embarquée ? + +Les compétences embarquées (dans `skills/`) sont livrées avec chaque installation d'Hermes. Elles doivent être **largement utiles à la majorité des utilisateurs** : + +- Traitement de documents, recherche web, workflows de développement courants, administration système +- Utilisées régulièrement par un large éventail de personnes + +Si votre compétence est officielle et utile mais pas universellement nécessaire (par exemple l'intégration d'un service payant, une dépendance lourde), placez-la dans **`optional-skills/`** — elle est livrée avec le dépôt mais n'est pas activée par défaut. Les utilisateurs peuvent la découvrir via `hermes skills browse` (étiquetée « official ») et l'installer avec `hermes skills install` (sans avertissement tiers, confiance intégrée). + +Si votre compétence est spécialisée, issue de la communauté ou de niche, elle a plus sa place sur un **Skills Hub** — téléversez-la sur un registre de compétences et partagez-la sur le [Discord de Nous Research](https://discord.gg/NousResearch). Les utilisateurs peuvent l'installer avec `hermes skills install`. + +--- + +## Fournisseurs de mémoire : à publier comme plugin autonome + +**Nous n'acceptons plus de nouveaux fournisseurs de mémoire dans ce dépôt.** L'ensemble des fournisseurs intégrés sous `plugins/memory/` (honcho, mem0, supermemory, byterover, hindsight, holographic, openviking, retaindb) est clos. Si vous voulez ajouter un nouveau backend de mémoire, publiez-le comme **dépôt de plugin autonome** que les utilisateurs installent dans `~/.hermes/plugins/` (ou via un entry point pip). + +Les plugins de mémoire autonomes : + +- Implémentent la même ABC `MemoryProvider` (`agent/memory_provider.py`) — `sync_turn`, `prefetch`, `shutdown`, et éventuellement `post_setup(hermes_home, config)` pour l'intégration avec l'assistant de configuration +- Utilisent le même système de découverte — `discover_memory_providers()` les récupère dans les répertoires de plugins utilisateur/projet et les entry points pip +- S'intègrent à `hermes memory setup` via `post_setup()` — sans toucher au code du cœur +- Peuvent enregistrer leurs propres sous-commandes CLI via `register_cli(subparser)` dans un fichier `cli.py` +- Bénéficient des mêmes hooks de cycle de vie et de la même plomberie de configuration que les fournisseurs intégrés + +Les PR qui ajoutent un nouveau répertoire sous `plugins/memory/` seront fermées avec un renvoi vers la publication du fournisseur dans son propre dépôt. Les fournisseurs déjà intégrés restent ; les corrections de bugs les concernant sont les bienvenues. + +Ce n'est pas une question de niveau de qualité — c'est une décision de couplage et de maintenance. Les fournisseurs de mémoire sont le type de plugin le plus courant et ils n'ont pas tous vocation à vivre dans cette arborescence. + +--- + +## Intégrations de produits tiers : à publier comme plugin autonome + +La même règle s'étend à **tout plugin qui intègre le produit ou le projet de quelqu'un d'autre** — backends d'observabilité/métriques, connecteurs SaaS d'éditeurs, tableaux de bord d'analytique, intégrations de services payants et autres intégrations tierces similaires. **Elles n'entrent pas dans ce dépôt.** + +La raison est la charge de maintenance, pas la qualité. Chaque produit externe absorbé dans le cœur devient notre responsabilité : le maintenir en état de marche face à une base de code qui évolue vite, pour un backend que nous ne possédons pas et ne contrôlons pas. Hermes publie beaucoup et le cœur avance rapidement ; y coupler des produits tiers crée une charge sans fin pour les mainteneurs. + +Publiez-les plutôt comme **dépôt de plugin autonome** : + +- Implémentez l'ABC concernée et utilisez le chemin de découverte de plugins existant (`~/.hermes/plugins/`, `.hermes/plugins/` du projet, ou un entry point pip) — voir [Build a Hermes Plugin](https://hermes-agent.nousresearch.com/docs/guides/build-a-hermes-plugin) +- Enregistrez les hooks de cycle de vie (`pre_tool_call`, `post_tool_call`, `pre_llm_call`, `post_llm_call`, `on_session_start`, `on_session_end`), les outils (`ctx.register_tool`) et les sous-commandes CLI (`ctx.register_cli_command`) à travers la surface que nous exposons déjà — aucun changement du cœur nécessaire +- Si votre plugin a besoin d'une capacité que le framework n'expose pas, c'est une demande de fonctionnalité pour **élargir la surface générique des plugins** (un nouveau hook ou une méthode `ctx`) — jamais un cas particulier pour votre plugin dans le cœur +- Faites-en la promotion sur le canal `#plugins-skills-and-skins` du [Discord de Nous Research](https://discord.gg/NousResearch) pour que les utilisateurs puissent le trouver et l'installer + +Un plugin de produit tiers bien construit peut passer la revue automatisée et être fermé quand même pour cette raison — c'est une décision de placement, pas un verdict sur le code. Les PR qui ajoutent un tel répertoire sous `plugins/` seront fermées avec un renvoi vers la publication dans un dépôt dédié. + +--- + +## Mise en place de l'environnement de développement + +### Prérequis + +| Prérequis | Notes | +|-------------|-------| +| **Git** | Avec l'extension `git-lfs` installée | +| **Python 3.11–3.13** | uv l'installera s'il manque | +| **uv** | Gestionnaire de paquets Python rapide ([installation](https://docs.astral.sh/uv/)) | +| **Node.js 20+** | Optionnel — nécessaire pour les outils navigateur et le pont WhatsApp (correspond aux engines du `package.json` racine) | + +### Installation avec l'installeur standard + +Pour la plupart des contributeurs, le meilleur amorçage de développement est le même +chemin que celui des utilisateurs : lancer l'installeur standard, puis travailler dans +le dépôt qu'il a cloné. L'installeur crée le venv d'Hermes, câble la commande +`hermes`, enregistre la méthode d'installation pour `hermes update` et clone le +projet git complet dans `$HERMES_HOME/hermes-agent` (généralement +`~/.hermes/hermes-agent`). Votre environnement de développement reste ainsi sur la +même disposition que celle attendue par la CLI, l'outil de mise à jour, l'installeur +paresseux de dépendances, le gateway et la documentation. + +```bash +curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash +cd "${HERMES_HOME:-$HOME/.hermes}/hermes-agent" + +# Add dev/test extras on top of the standard install. +uv pip install -e ".[all,dev]" + +# Optional: browser tools / docs site dependencies. +npm install +``` + +Ensuite, créez vos branches et lancez les tests depuis ce checkout : + +```bash +git checkout -b fix/description +scripts/run_tests.sh +``` + +### Repli : clonage manuel + +À n'utiliser que si vous ne voulez délibérément pas de la disposition d'installation +gérée par Hermes (par exemple un clone jetable dans un conteneur ou un job CI). Si +vous installez de cette manière, veillez à lancer le point d'entrée `hermes` depuis +ce venv ; exécuter le `python3 -m hermes_cli.main` du système peut ramasser des +paquets Python système sans rapport. + +Créez le venv **en dehors** de l'arborescence source clonée. Un venv qui vit dans le +répertoire depuis lequel l'agent opère peut être effacé par une commande à chemin +relatif que l'agent exécute contre son propre checkout (`rm -rf venv`, +`uv venv venv`, etc.), ce qui détruit silencieusement le runtime en cours en pleine +session. Le garder hors de l'arborescence garantit qu'aucun chemin relatif depuis +l'espace de travail ne le résout. + +```bash +git clone https://github.com/NousResearch/hermes-agent.git +cd hermes-agent + +# Create venv with Python 3.11, OUTSIDE the source tree +uv venv ~/.hermes/venvs/hermes-dev --python 3.11 +export VIRTUAL_ENV="$HOME/.hermes/venvs/hermes-dev" +export PATH="$VIRTUAL_ENV/bin:$PATH" + +# Install with all extras (messaging, cron, CLI menus, dev tools) +uv pip install -e ".[all,dev]" + +# Optional: browser tools +npm install +``` + +### Configurer pour le développement + +```bash +mkdir -p ~/.hermes/{cron,sessions,logs,memories,skills} +cp cli-config.yaml.example ~/.hermes/config.yaml +touch ~/.hermes/.env + +# Add at minimum an LLM provider key: +echo "OPENROUTER_API_KEY=***" >> ~/.hermes/.env +``` + +### Lancer + +```bash +# The standard installer already put `hermes` on PATH. +hermes doctor +hermes chat -q "Hello" +``` + +Si vous avez utilisé le repli par clonage manuel, lancez `./hermes` depuis le +checkout ou créez explicitement un lien symbolique vers le venv de ce clone : + +```bash +mkdir -p ~/.local/bin +ln -sf "$(pwd)/venv/bin/hermes" ~/.local/bin/hermes +``` + +### Lancer les tests + +```bash +# Preferred — matches CI (hermetic env, 4 xdist workers); see AGENTS.md +scripts/run_tests.sh + +# Alternative (activate the venv first). The wrapper is still recommended +# for parity with GitHub Actions before you open a PR: +pytest tests/ -v +``` + +--- + +## Structure du projet + +``` +hermes-agent/ +├── run_agent.py # AIAgent class — core conversation loop, tool dispatch, session persistence +├── cli.py # HermesCLI class — interactive TUI, prompt_toolkit integration +├── model_tools.py # Tool orchestration (thin layer over tools/registry.py) +├── toolsets.py # Tool groupings and presets (hermes-cli, hermes-telegram, etc.) +├── hermes_state.py # SQLite session database with FTS5 full-text search, session titles +├── batch_runner.py # Parallel batch processing for trajectory generation +│ +├── agent/ # Agent internals (extracted modules) +│ ├── prompt_builder.py # System prompt assembly (identity, skills, context files, memory) +│ ├── context_compressor.py # Auto-summarization when approaching context limits +│ ├── auxiliary_client.py # Resolves auxiliary OpenAI clients (summarization, vision) +│ ├── display.py # KawaiiSpinner, tool progress formatting +│ ├── model_metadata.py # Model context lengths, token estimation +│ └── trajectory.py # Trajectory saving helpers +│ +├── hermes_cli/ # CLI command implementations +│ ├── main.py # Entry point, argument parsing, command dispatch +│ ├── config.py # Config management, migration, env var definitions +│ ├── setup.py # Interactive setup wizard +│ ├── auth.py # Provider resolution, OAuth, Nous Portal +│ ├── models.py # OpenRouter model selection lists +│ ├── banner.py # Welcome banner, ASCII art +│ ├── commands.py # Central slash command registry (CommandDef), autocomplete, gateway helpers +│ ├── callbacks.py # Interactive callbacks (clarify, sudo, approval) +│ ├── doctor.py # Diagnostics +│ ├── skills_hub.py # Skills Hub CLI + /skills slash command +│ └── skin_engine.py # Skin/theme engine — data-driven CLI visual customization +│ +├── tools/ # Tool implementations (self-registering) +│ ├── registry.py # Central tool registry (schemas, handlers, dispatch) +│ ├── approval.py # Dangerous command detection + per-session approval +│ ├── terminal_tool.py # Terminal orchestration (sudo, env lifecycle, backends) +│ ├── file_operations.py # read_file, write_file, search, patch, etc. +│ ├── web_tools.py # web_search, web_extract (Parallel/Firecrawl + Gemini summarization) +│ ├── vision_tools.py # Image analysis via multimodal models +│ ├── delegate_tool.py # Subagent spawning and parallel task execution +│ ├── code_execution_tool.py # Sandboxed Python with RPC tool access +│ ├── session_search_tool.py # Search past conversations with FTS5 + anchored windows +│ ├── cronjob_tools.py # Scheduled task management +│ ├── skill_tools.py # Skill search, load, manage +│ └── environments/ # Terminal execution backends +│ ├── base.py # BaseEnvironment ABC +│ ├── local.py, docker.py, ssh.py, singularity.py, modal.py, daytona.py +│ +├── gateway/ # Messaging gateway +│ ├── run.py # GatewayRunner — platform lifecycle, message routing, cron +│ ├── config.py # Platform configuration resolution +│ ├── session.py # Session store, context prompts, reset policies +│ └── platforms/ # Platform adapters +│ ├── telegram.py, discord_adapter.py, slack.py, whatsapp.py +│ +├── scripts/ # Installer and bridge scripts +│ ├── install.sh # Linux/macOS installer +│ ├── install.ps1 # Windows PowerShell installer +│ └── whatsapp-bridge/ # Node.js WhatsApp bridge (Baileys) +│ +├── skills/ # Bundled skills (copied to ~/.hermes/skills/ on install) +├── optional-skills/ # Official optional skills (discoverable via hub, not activated by default) +├── tests/ # Test suite +├── website/ # Documentation site (hermes-agent.nousresearch.com) +│ +├── cli-config.yaml.example # Example configuration (copied to ~/.hermes/config.yaml) +└── AGENTS.md # Development guide for AI coding assistants +``` + +### Configuration utilisateur (stockée dans `~/.hermes/`) + +| Chemin | Rôle | +|------|---------| +| `~/.hermes/config.yaml` | Réglages (modèle, terminal, toolsets, compression, etc.) | +| `~/.hermes/.env` | Clés API et secrets | +| `~/.hermes/auth.json` | Identifiants OAuth (Nous Portal) | +| `~/.hermes/skills/` | Toutes les compétences actives (embarquées + installées depuis le hub + créées par l'agent) | +| `~/.hermes/memories/` | Mémoire persistante (MEMORY.md, USER.md) | +| `~/.hermes/state.db` | Base de données de sessions SQLite | +| `~/.hermes/sessions/` | Index de routage du gateway (`sessions.json`), traces de request-dump, transcriptions `*.jsonl` du gateway et (en option) instantanés JSON par session quand `sessions.write_json_snapshots: true` est défini. Les instantanés par session sont désactivés par défaut ; state.db fait foi. | +| `~/.hermes/cron/` | Données des tâches planifiées | +| `~/.hermes/whatsapp/session/` | Identifiants du pont WhatsApp | + +--- + +## Vue d'ensemble de l'architecture + +### Boucle principale + +``` +User message → AIAgent._run_agent_loop() + ├── Build system prompt (prompt_builder.py) + ├── Build API kwargs (model, messages, tools, reasoning config) + ├── Call LLM (OpenAI-compatible API) + ├── If tool_calls in response: + │ ├── Execute each tool via registry dispatch + │ ├── Add tool results to conversation + │ └── Loop back to LLM call + ├── If text response: + │ ├── Persist session to DB + │ └── Return final_response + └── Context compression if approaching token limit +``` + +### Patrons de conception clés + +- **Outils auto-enregistrés** : chaque fichier d'outil appelle `registry.register()` à l'import. `model_tools.py` déclenche la découverte en important tous les modules d'outils. +- **Regroupement en toolsets** : les outils sont regroupés en toolsets (`web`, `terminal`, `file`, `browser`, etc.) qui peuvent être activés/désactivés par plateforme. +- **Persistance des sessions** : toutes les conversations sont stockées dans SQLite (`hermes_state.py`) avec recherche plein texte et titres de session uniques. Les instantanés JSON par session dans `~/.hermes/sessions/` ont été remplacés par le stockage SQLite et sont désactivés par défaut ; réactivez-les avec `sessions.write_json_snapshots: true` si un outillage externe consomme directement les fichiers JSON. +- **Injection éphémère** : les prompts système et les messages de préremplissage sont injectés au moment de l'appel API, jamais persistés dans la base de données ni dans les logs. +- **Abstraction des fournisseurs** : l'agent fonctionne avec n'importe quelle API compatible OpenAI. La résolution du fournisseur a lieu à l'initialisation (OAuth Nous Portal, clé API OpenRouter ou endpoint personnalisé). +- **Routage des fournisseurs** : avec OpenRouter, `provider_routing` dans config.yaml contrôle la sélection du fournisseur (tri par débit/latence/prix, autorisation/exclusion de fournisseurs spécifiques, politiques de rétention des données). Ces réglages sont injectés dans `extra_body.provider` des requêtes API. + +--- + +## Style de code + +- **PEP 8** avec des exceptions pragmatiques (nous n'imposons pas de longueur de ligne stricte) +- **Commentaires** : uniquement pour expliquer une intention non évidente, des compromis ou des bizarreries d'API. Ne racontez pas ce que fait le code — `# increment counter` n'apporte rien +- **Gestion des erreurs** : attrapez des exceptions spécifiques. Loguez avec `logger.warning()`/`logger.error()` — utilisez `exc_info=True` pour les erreurs inattendues afin que les stack traces apparaissent dans les logs +- **Multiplateforme** : ne supposez jamais Unix. Voir [Compatibilité multiplateforme](#compatibilité-multiplateforme) + +--- + +## Ajouter un nouvel outil + +Avant d'écrire un outil, demandez-vous : [ne devrait-ce pas être une compétence ?](#compétence-ou-outil-) + +Les outils s'enregistrent eux-mêmes auprès du registre central. Chaque fichier d'outil regroupe au même endroit son schéma, son handler et son enregistrement : + +```python +"""my_tool — Brief description of what this tool does.""" + +import json +from tools.registry import registry + + +def my_tool(param1: str, param2: int = 10, **kwargs) -> str: + """Handler. Returns a string result (often JSON).""" + result = do_work(param1, param2) + return json.dumps(result) + + +MY_TOOL_SCHEMA = { + "type": "function", + "function": { + "name": "my_tool", + "description": "What this tool does and when the agent should use it.", + "parameters": { + "type": "object", + "properties": { + "param1": {"type": "string", "description": "What param1 is"}, + "param2": {"type": "integer", "description": "What param2 is", "default": 10}, + }, + "required": ["param1"], + }, + }, +} + + +def _check_requirements() -> bool: + """Return True if this tool's dependencies are available.""" + return True + + +registry.register( + name="my_tool", + toolset="my_toolset", + schema=MY_TOOL_SCHEMA, + handler=lambda args, **kw: my_tool(**args, **kw), + check_fn=_check_requirements, +) +``` + +**Câblage dans un toolset (obligatoire) :** les outils intégrés sont découverts +automatiquement : tout fichier `tools/*.py` contenant un appel +`registry.register(...)` au niveau supérieur est importé par +`discover_builtin_tools()` dans `tools/registry.py` au chargement de `model_tools`. +Il n'y a **aucune** liste d'imports manuelle à maintenir dans `model_tools.py`. + +Vous devez néanmoins ajouter le nom de l'outil à la liste appropriée dans +`toolsets.py` (par exemple `_HERMES_CORE_TOOLS` ou un toolset dédié) ; sinon +l'outil s'enregistre mais n'est jamais exposé à l'agent. Si vous introduisez un +nouveau toolset, ajoutez-le dans `toolsets.py` et câblez-le dans les presets de +plateforme concernés. + +Voir `AGENTS.md` (section **Adding New Tools**) pour les chemins sensibles aux +profils et l'arbitrage plugin vs cœur. + +--- + +## Ajouter une compétence + +Les compétences embarquées vivent dans `skills/`, organisées par catégorie. Les compétences optionnelles officielles utilisent la même structure dans `optional-skills/` : + +``` +skills/ +├── research/ +│ └── arxiv/ +│ ├── SKILL.md # Required: main instructions +│ └── scripts/ # Optional: helper scripts +│ └── search_arxiv.py +├── productivity/ +│ └── ocr-and-documents/ +│ ├── SKILL.md +│ ├── scripts/ +│ └── references/ +└── ... +``` + +### Format de SKILL.md + +```markdown +--- +name: my-skill +description: Brief description (shown in skill search results) +version: 1.0.0 +author: Your Name +license: MIT +platforms: [macos, linux] # Optional — restrict to specific OS platforms + # Valid: macos, linux, windows + # Omit to load on all platforms (default) +required_environment_variables: # Optional — secure setup-on-load metadata + - name: MY_API_KEY + prompt: API key + help: Where to get it + required_for: full functionality +prerequisites: # Optional legacy runtime requirements + env_vars: [MY_API_KEY] # Backward-compatible alias for required env vars + commands: [curl, jq] # Advisory only; does not hide the skill +metadata: + hermes: + tags: [Category, Subcategory, Keywords] + related_skills: [other-skill-name] + fallback_for_toolsets: [web] # Optional — show only when toolset is unavailable + requires_toolsets: [terminal] # Optional — show only when toolset is available +--- + +# Skill Title + +Brief intro. + +## When to Use +Trigger conditions — when should the agent load this skill? + +## Prerequisites +Env vars, install steps, MCP setup, API key sourcing. + +## How to Run +Canonical invocation through the `terminal` tool. + +## Quick Reference +Table of common commands or API calls. + +## Procedure +Step-by-step instructions the agent follows. + +## Pitfalls +Known failure modes and how to handle them. + +## Verification +How the agent confirms it worked. +``` + +### Compétences spécifiques à une plateforme + +Les compétences peuvent déclarer les plateformes qu'elles prennent en charge via le champ de frontmatter `platforms`. Les compétences portant ce champ sont automatiquement masquées du prompt système, de `skills_list()` et des commandes slash sur les plateformes incompatibles. + +```yaml +platforms: [macos] # macOS only (e.g., iMessage, Apple Reminders) +platforms: [macos, linux] # macOS and Linux +platforms: [windows] # Windows only +``` + +Si le champ est omis ou vide, la compétence se charge sur toutes les plateformes (rétrocompatible). Voir `skills/apple/` pour des exemples de compétences réservées à macOS. + +### Activation conditionnelle des compétences + +Les compétences peuvent déclarer des conditions qui contrôlent leur apparition dans le prompt système, selon les outils et toolsets disponibles dans la session courante. C'est principalement utilisé pour les **compétences de repli** — des alternatives qui ne doivent apparaître que lorsqu'un outil principal est indisponible. + +Quatre champs sont pris en charge sous `metadata.hermes` : + +```yaml +metadata: + hermes: + fallback_for_toolsets: [web] # Show ONLY when these toolsets are unavailable + requires_toolsets: [terminal] # Show ONLY when these toolsets are available + fallback_for_tools: [web_search] # Show ONLY when these specific tools are unavailable + requires_tools: [terminal] # Show ONLY when these specific tools are available +``` + +**Sémantique :** +- `fallback_for_*` : la compétence est une solution de secours. Elle est **masquée** quand les outils/toolsets listés sont disponibles, et **affichée** quand ils ne le sont pas. À utiliser pour des alternatives gratuites à des outils premium. +- `requires_*` : la compétence a besoin de certains outils pour fonctionner. Elle est **masquée** quand les outils/toolsets listés sont indisponibles. À utiliser pour des compétences qui dépendent de capacités précises (par exemple une compétence qui n'a de sens qu'avec un accès au terminal). +- Si les deux sont spécifiés, les deux conditions doivent être satisfaites pour que la compétence apparaisse. +- Si aucun n'est spécifié, la compétence est toujours affichée (rétrocompatible). + +**Exemples :** + +```yaml +# DuckDuckGo search — shown when Firecrawl (web toolset) is unavailable +metadata: + hermes: + fallback_for_toolsets: [web] + +# Smart home skill — only useful when terminal is available +metadata: + hermes: + requires_toolsets: [terminal] + +# Local browser fallback — shown when Browserbase is unavailable +metadata: + hermes: + fallback_for_toolsets: [browser] +``` + +Le filtrage a lieu au moment de la construction du prompt, dans `agent/prompt_builder.py`. La fonction `build_skills_system_prompt()` reçoit l'ensemble des outils et toolsets disponibles depuis l'agent et utilise `_skill_should_show()` pour évaluer les conditions de chaque compétence. + +### Métadonnées de configuration d'une compétence + +Les compétences peuvent déclarer des métadonnées de configuration sécurisée au chargement via le champ de frontmatter `required_environment_variables`. Des valeurs manquantes ne masquent pas la compétence à la découverte ; elles déclenchent une invite sécurisée (CLI uniquement) au moment où la compétence est réellement chargée. + +```yaml +required_environment_variables: + - name: TENOR_API_KEY + prompt: Tenor API key + help: Get a key from https://developers.google.com/tenor + required_for: full functionality +``` + +L'utilisateur peut ignorer la configuration et continuer à charger la compétence. Hermes n'expose au modèle que des métadonnées (`stored_as`, `skipped`, `validated`) — jamais la valeur du secret. + +L'ancien `prerequisites.env_vars` reste pris en charge et est normalisé vers la nouvelle représentation. + +```yaml +prerequisites: + env_vars: [TENOR_API_KEY] # Legacy alias for required_environment_variables + commands: [curl, jq] # Advisory CLI checks +``` + +Les sessions gateway et de messagerie ne collectent jamais de secrets dans le flux de conversation ; elles demandent à l'utilisateur de lancer `hermes setup` ou de mettre à jour `~/.hermes/.env` en local. + +**Quand déclarer des variables d'environnement requises :** +- La compétence utilise une clé API ou un token qui doit être collecté de manière sécurisée au chargement +- La compétence peut rester utile si l'utilisateur ignore la configuration, quitte à se dégrader proprement + +**Quand déclarer des prérequis de commandes :** +- La compétence repose sur un outil CLI qui peut ne pas être installé (par exemple `himalaya`, `openhue`, `ddgs`) +- Traitez les vérifications de commandes comme des indications, pas comme un masquage à la découverte + +Voir `skills/gifs/gif-search/` et `skills/email/himalaya/` pour des exemples. + +### Normes de rédaction des compétences (NON NÉGOCIABLES) + +Toute compétence nouvelle ou modernisée — embarquée, optionnelle ou contribuée — doit respecter ces normes avant la fusion. Les relecteurs rejettent les PR qui les enfreignent. + +1. **`description` ≤ 60 caractères, une seule phrase, terminée par un point.** Les descriptions longues alourdissent l'interface de listage des compétences et diluent l'attention du modèle quand de nombreuses compétences sont chargées. Énoncez la capacité, pas l'implémentation. Pas de vocabulaire marketing (« powerful », « comprehensive », « seamless », « advanced »). Ne répétez pas le nom de la compétence. Vérifiez avec : + ```python + import re, pathlib + m = re.search(r'^description: (.*)$', + pathlib.Path('skills///SKILL.md').read_text(), + re.MULTILINE) + assert len(m.group(1)) <= 60, len(m.group(1)) + ``` + + Bien : `Search arXiv papers by keyword, author, category, or ID.` + Mal : `A powerful and comprehensive skill that allows the agent to search arXiv for relevant academic papers using various criteria including keywords, authors, and categories.` + +2. **Les outils cités dans la prose de SKILL.md doivent être des outils Hermes natifs ou des serveurs MCP que la compétence attend explicitement.** Quand la compétence a besoin d'une capacité, désignez le bon outil par son nom entre backticks : `` `terminal` ``, `` `web_extract` ``, `` `web_search` ``, `` `read_file` ``, `` `write_file` ``, `` `patch` ``, `` `search_files` ``, `` `vision_analyze` ``, `` `browser_navigate` ``, `` `delegate_task` ``, `` `image_generate` ``, `` `text_to_speech` ``, `` `cronjob` ``, `` `memory` ``, `` `skill_view` ``, `` `todo` ``, `` `execute_code` ``. + + Ne nommez PAS des utilitaires shell que l'agent a déjà enveloppés : + + | À ne pas dire | À dire | + |---|---| + | `grep`, `rg` | `search_files` | + | `cat`, `head`, `tail` | `read_file` | + | `sed`, `awk` | `patch` | + | `find`, `ls` | `search_files` (avec `target='files'`) | + | `curl` pour extraire du contenu | `web_extract` | + | `echo > file`, `cat < Skill`, introduction de 2-3 phrases indiquant ce que la compétence fait et ne fait pas, puis : + - `## When to Use` — conditions de déclenchement + - `## Prerequisites` — variables d'environnement, étapes d'installation, configuration MCP, obtention des clés API + - `## How to Run` — invocation canonique via l'outil `terminal` + - `## Quick Reference` — référence brute des commandes/API + - `## Procedure` — étapes numérotées avec commandes prêtes à copier-coller + - `## Pitfalls` — limites connues, quotas, choses qui semblent cassées mais ne le sont pas + - `## Verification` — une seule commande qui prouve que la compétence fonctionne + + Visez ~200 lignes pour une compétence complexe, ~100 pour une simple. Supprimez les introductions redondantes, la prose marketing et les ré-explications de variables d'environnement déjà documentées dans `## Prerequisites`. + +6. **Les scripts vont dans `scripts/`, les références dans `references/`, les templates dans `templates/`.** N'attendez pas du modèle qu'il réécrive inline des parseurs, des parcours XML ou de la logique non triviale à chaque appel — livrez un script utilitaire. Référencez les scripts depuis SKILL.md par chemin relatif au répertoire de la compétence. + +7. **Les tests vivent dans `tests/skills/test__skill.py`** et n'utilisent que la stdlib + pytest + `unittest.mock`. Aucun appel réseau réel. Lancez-les via `scripts/run_tests.sh tests/skills/test__skill.py -q`. Ils doivent passer dans l'environnement CI hermétique (aucune clé API qui fuite). Utilisez `monkeypatch` et `tmp_path` pour toute dépendance aux variables d'environnement ou au système de fichiers. + +8. **Les ajouts à `.env.example` sont isolés dans un bloc clairement délimité.** Ne touchez pas au reste du fichier — les versions de `.env.example` fournies par les contributeurs sont généralement obsolètes, et les modifications hors du bloc propre à la compétence seront écartées lors de la récupération. Commentez toutes les valeurs avec `#` (c'est de la documentation, pas de la configuration active). + +### Recommandations pour les compétences + +- **Pas de dépendances externes sauf nécessité absolue.** Préférez la stdlib Python, curl et les outils Hermes existants (`web_extract`, `terminal`, `read_file`). +- **Divulgation progressive.** Placez le workflow le plus courant en premier. Les cas limites et l'usage avancé vont en bas. +- **Fournissez des scripts utilitaires** pour le parsing XML/JSON ou la logique complexe — n'attendez pas du LLM qu'il écrive des parseurs inline à chaque fois. +- **Testez-la.** Lancez `hermes --toolsets skills -q "Use the X skill to do Y"` et vérifiez que l'agent suit correctement les instructions. + +--- + +## Ajouter un skin / thème + +Hermes utilise un système de skins piloté par les données — aucun changement de code n'est nécessaire pour ajouter un nouveau skin. + +**Option A : skin utilisateur (fichier YAML)** + +Créez `~/.hermes/skins/.yaml` : + +```yaml +name: mytheme +description: Short description of the theme + +colors: + banner_border: "#HEX" # Panel border color + banner_title: "#HEX" # Panel title color + banner_accent: "#HEX" # Section header color + banner_dim: "#HEX" # Muted/dim text color + banner_text: "#HEX" # Body text color + response_border: "#HEX" # Response box border + +spinner: + waiting_faces: ["(⚔)", "(⛨)"] + thinking_faces: ["(⚔)", "(⌁)"] + thinking_verbs: ["forging", "plotting"] + wings: # Optional left/right decorations + - ["⟪⚔", "⚔⟫"] + +branding: + agent_name: "My Agent" + welcome: "Welcome message" + response_label: " ⚔ Agent " + prompt_symbol: "⚔" + +tool_prefix: "╎" # Tool output line prefix +``` + +Tous les champs sont optionnels — les valeurs manquantes héritent du skin par défaut. + +**Option B : skin intégré** + +Ajoutez-le au dict `_BUILTIN_SKINS` dans `hermes_cli/skin_engine.py`. Utilisez le même schéma que ci-dessus, mais sous forme de dict Python. Les skins intégrés sont livrés avec le paquet et toujours disponibles. + +**Activation :** +- CLI : `/skin mytheme` ou définissez `display.skin: mytheme` dans config.yaml +- Config : `display: { skin: mytheme }` + +Voir `hermes_cli/skin_engine.py` pour le schéma complet et les skins existants en exemples. + +--- + +## Compatibilité multiplateforme + +Hermes tourne sous Linux, macOS et Windows natif (plus WSL2). Quand vous écrivez du +code qui touche à l'OS, partez du principe que *n'importe quelle* plateforme peut +emprunter votre chemin de code. + +> **Avant d'ouvrir votre PR :** lancez `scripts/check-windows-footguns.py` pour +> attraper les pièges Windows courants dans votre diff. C'est du grep, donc peu +> coûteux ; la CI le lance aussi sur chaque PR. + +### Règles critiques + +1. **N'appelez jamais `os.kill(pid, 0)` pour tester si un processus est vivant.** + `os.kill(pid, 0)` est un idiome POSIX standard pour vérifier « ce PID est-il + vivant » — le signal 0 est une simple vérification de permission sans effet. + **Sous Windows, ce n'est PAS sans effet.** Le `os.kill` de Python sous Windows + mappe `sig=0` sur `CTRL_C_EVENT` (ils entrent en collision à la valeur entière + 0) et le fait passer par `GenerateConsoleCtrlEvent(0, pid)`, qui diffuse Ctrl+C + à **tout le groupe de processus de la console** contenant le PID cible. + « Sonder si vivant » devient silencieusement « tuer la cible et souvent des + processus sans rapport partageant sa console ». Voir [bpo-14484](https://bugs.python.org/issue14484) + (ouvert depuis 2012 — ne sera jamais corrigé pour raisons de compatibilité). + + **À privilégier :** utilisez `psutil` (une dépendance de base — toujours disponible) : + + ```python + import psutil + if psutil.pid_exists(pid): + # process is alive — safe on every platform + ... + ``` + + Si vous avez spécifiquement besoin du wrapper hermes (il a un repli stdlib + pour les imports en phase d'amorçage, avant que pip install ne se termine), + utilisez `gateway.status._pid_exists(pid)`. Il appelle d'abord + `psutil.pid_exists` et se rabat sur une danse maison + `OpenProcess + WaitForSingleObject` sous Windows, uniquement si psutil est + introuvable. + + Grep d'audit pour les nouveaux sites d'appel : `rg "os\.kill\([^,]+,\s*0\s*\)"`. + Tout résultat hors code de test est présumé être un bug de kill silencieux sous + Windows. + +2. **Utilisez `shutil.which()` avant de lancer une commande — ne supposez pas que + Windows a les outils de Linux.** `wmic` a été retiré à partir de Windows 10 + 21H1. `ps`, `kill`, `grep`, `awk`, `fuser`, `lsof`, `pgrep` et la plupart des + outils CLI POSIX n'existent tout simplement pas sous Windows. Testez la + disponibilité avec `shutil.which("tool")` et prévoyez un équivalent natif + Windows — généralement PowerShell via `subprocess.run(["powershell", + "-NoProfile", "-Command", ...])`. + + Pour l'énumération des processus : `Get-CimInstance Win32_Process` de + PowerShell est le remplaçant moderne de `wmic process`. Voir + `hermes_cli/gateway.py::_scan_gateway_pids` pour le motif à suivre. + +3. **`termios` et `fcntl` sont réservés à Unix.** Attrapez toujours à la fois + `ImportError` et `NotImplementedError` : + ```python + try: + from simple_term_menu import TerminalMenu + menu = TerminalMenu(options) + idx = menu.show() + except (ImportError, NotImplementedError): + # Fallback: numbered menu for Windows + for i, opt in enumerate(options): + print(f" {i+1}. {opt}") + idx = int(input("Choice: ")) - 1 + ``` + +4. **Encodage des fichiers.** Windows peut enregistrer les fichiers `.env` en + `cp1252`. Gérez toujours les erreurs d'encodage : + ```python + try: + load_dotenv(env_path) + except UnicodeDecodeError: + load_dotenv(env_path, encoding="latin-1") + ``` + Les fichiers de configuration (`config.yaml`) peuvent être enregistrés avec un + BOM UTF-8 par le Bloc-notes et éditeurs similaires — utilisez + `encoding="utf-8-sig"` pour lire les fichiers qui ont pu être touchés par un + éditeur graphique Windows. + +5. **Gestion des processus.** `os.setsid()`, `os.killpg()`, `os.fork()`, + `os.getuid()` et la gestion des signaux POSIX diffèrent sous Windows. Protégez + avec `platform.system()`, `sys.platform` ou `hasattr(os, "setsid")` : + ```python + if platform.system() != "Windows": + kwargs["preexec_fn"] = os.setsid + else: + kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP + ``` + + **À privilégier :** pour tuer un processus ET ses enfants (ce que fait + `os.killpg` sous POSIX), utilisez `psutil` — il fonctionne sur toutes les + plateformes : + ```python + import psutil + try: + parent = psutil.Process(pid) + # Kill children first (leaf-up), then the parent. + for child in parent.children(recursive=True): + child.kill() + parent.kill() + except psutil.NoSuchProcess: + pass + ``` + +6. **Signaux inexistants sous Windows : `SIGALRM`, `SIGCHLD`, `SIGHUP`, + `SIGUSR1`, `SIGUSR2`, `SIGPIPE`, `SIGQUIT`, `SIGKILL`.** Le module `signal` de + Python lève `AttributeError` à l'import si vous les référencez sous Windows. + Utilisez `getattr(signal, "SIGKILL", signal.SIGTERM)` ou placez tout le bloc + derrière une vérification de plateforme. `loop.add_signal_handler` lève + `NotImplementedError` sous Windows — attrapez-la toujours. + +7. **Séparateurs de chemins.** Utilisez `pathlib.Path` plutôt que la concaténation + de chaînes avec `/`. Les slashs fonctionnent presque partout sous Windows, mais + `subprocess.run(["cmd.exe", "/c", ...])` et d'autres contextes shell peuvent + exiger des antislashs — convertissez avec `str(path)` à la frontière du + subprocess, pas au cœur de la logique Python. + +8. **Les liens symboliques exigent des privilèges élevés sous Windows** (sauf si + le mode développeur est activé). Les tests qui créent des liens symboliques ont + besoin de `@pytest.mark.skipif(sys.platform == + "win32", reason="Symlinks require elevated privileges on Windows")`. + +9. **Les modes de fichiers POSIX (0o600, 0o644, etc.) ne sont PAS appliqués sur + NTFS** par défaut. Les tests qui font des assertions sur + `stat().st_mode & 0o777` doivent être ignorés sous Windows — le concept ne se + transpose pas. Utilisez des ACL (`icacls`, `pywin32`) pour protéger les + fichiers de secrets sous Windows si nécessaire. + +10. **Les démons d'arrière-plan détachés sous Windows exigent `pythonw.exe`, PAS + `python.exe`.** `python.exe` alloue toujours une console ou s'y attache, ce + qui le rend vulnérable aux diffusions de `CTRL_C_EVENT` depuis n'importe quel + processus frère. `pythonw.exe` est la variante sans console. Combinez avec + `CREATE_NO_WINDOW | DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP | + CREATE_BREAKAWAY_FROM_JOB` dans `subprocess.Popen(creationflags=...)`. + Voir `hermes_cli/gateway_windows.py::_spawn_detached` pour l'implémentation + de référence. + +11. **`subprocess.Popen` avec des shims `.cmd` ou `.bat` a besoin de + `shutil.which` pour la résolution.** Passer `"agent-browser"` à `Popen` sous + Windows trouve le shim shebang POSIX sans extension dans + `node_modules/.bin/`, que `CreateProcessW` ne peut pas exécuter — vous + obtiendrez `WinError 193 "not a valid Win32 application"`. Utilisez + `shutil.which("agent-browser", path=local_bin)`, qui honore PATHEXT et + choisit la variante `.CMD` sous Windows. + +12. **N'utilisez pas les shebangs shell pour lancer du Python.** `#!/usr/bin/env + python` ne fonctionne que lorsque le fichier est exécuté à travers un shell + Unix. `subprocess.run(["./myscript.py"])` échoue sous Windows même si le + fichier a une ligne shebang. Invoquez toujours Python explicitement : + `[sys.executable, "myscript.py"]`. + +13. **Commandes shell dans les installeurs.** Si vous modifiez + `scripts/install.sh`, faites la modification équivalente dans + `scripts/install.ps1`. Ces deux scripts sont l'exemple canonique de + « fonctionne sous Linux ne veut pas dire fonctionne sous Windows » et ont + divergé plusieurs fois — gardez-les synchronisés. + +14. **Chemins connus redirigés vers OneDrive sous Windows :** Desktop, + Documents, Pictures, Videos. Le « vrai » chemin quand OneDrive Backup est + activé est `%USERPROFILE%\OneDrive\Desktop` (etc.), et NON + `%USERPROFILE%\Desktop` (qui existe en coquille vide). Résolvez + l'emplacement réel via `ctypes` + `SHGetKnownFolderPath` ou en lisant la clé + de registre `Shell Folders` — ne supposez jamais `~/Desktop`. + +15. **CRLF vs LF dans les scripts générés.** `cmd.exe` et `schtasks` sous Windows + parsent ligne par ligne ; des fins de ligne mixtes ou uniquement LF peuvent + casser des fichiers `.cmd` / `.bat` multilignes. Utilisez `open(path, "w", + encoding="utf-8", newline="\r\n")` — ou `open(path, "wb")` + octets + explicites — pour générer des scripts que Windows exécutera. + +16. **Deux schémas de quoting différents sur une même ligne de commande.** + `subprocess.run(["schtasks", "/TR", some_cmd])` → schtasks parse lui-même + `/TR`, ET la chaîne `some_cmd` est re-parsée par `cmd.exe` quand la tâche se + déclenche. Parseurs différents, règles d'échappement différentes. Utilisez + deux helpers de quoting distincts et ne les croisez jamais. Voir + `hermes_cli/gateway_windows.py::_quote_cmd_script_arg` et + `_quote_schtasks_arg` pour la paire de référence. + +### Tester le multiplateforme + +Les tests qui utilisent des appels système exclusivement POSIX ont besoin d'un marqueur de skip. Les cas courants : +- Liens symboliques → `@pytest.mark.skipif(sys.platform == "win32", ...)` +- Modes de fichiers `0o600` → `@pytest.mark.skipif(sys.platform.startswith("win"), ...)` +- `signal.SIGALRM` → Unix uniquement (voir `tests/conftest.py::_enforce_test_timeout`) +- `os.setsid` / `os.fork` → Unix uniquement +- Tests de régression Winsock réels / propres à Windows → + `@pytest.mark.skipif(sys.platform != "win32", reason="Windows-specific regression")` + +Si vous monkeypatchez `sys.platform` pour des tests multiplateformes, patchez aussi +`platform.system()` / `platform.release()` / `platform.mac_ver()` — chacun relit +indépendamment le véritable OS, si bien que des tests à moitié patchés empruntent +quand même la mauvaise branche sur un runner Windows. + +--- + +## Considérations de sécurité + +Hermes a accès au terminal. La sécurité compte. + +### Protections existantes + +| Couche | Implémentation | +|-------|---------------| +| **Passage du mot de passe sudo** | Utilise `shlex.quote()` pour empêcher l'injection shell | +| **Détection des commandes dangereuses** | Motifs regex dans `tools/approval.py` avec flux d'approbation utilisateur | +| **Injection de prompt via cron** | Un scanner dans `tools/cronjob_tools.py` bloque les motifs de contournement d'instructions | +| **Liste de refus en écriture** | Chemins protégés (`~/.ssh/authorized_keys`, `/etc/shadow`) résolus via `os.path.realpath()` pour empêcher le contournement par lien symbolique | +| **Garde des compétences** | Scanner de sécurité pour les compétences installées depuis le hub (`tools/skills_guard.py`) | +| **Sandbox d'exécution de code** | Le processus enfant de `execute_code` tourne avec les clés API retirées de l'environnement | +| **Durcissement des conteneurs** | Docker : toutes les capabilities supprimées, pas d'élévation de privilèges, limites de PID, tmpfs à taille limitée | + +### Contribuer du code sensible côté sécurité + +- **Utilisez toujours `shlex.quote()`** quand vous interpolez une saisie utilisateur dans des commandes shell +- **Résolvez les liens symboliques** avec `os.path.realpath()` avant tout contrôle d'accès fondé sur les chemins +- **Ne loguez pas de secrets.** Les clés API, tokens et mots de passe ne doivent jamais apparaître dans les logs +- **Attrapez des exceptions larges** autour de l'exécution des outils, pour qu'un seul échec ne fasse pas planter la boucle de l'agent +- **Testez sur toutes les plateformes** si votre changement touche aux chemins de fichiers, à la gestion des processus ou aux commandes shell + +Si votre PR touche à la sécurité, signalez-le explicitement dans la description. + +### Politique d'épinglage des dépendances (durcissement de la chaîne d'approvisionnement) + +Après la [compromission de la chaîne d'approvisionnement de litellm](https://github.com/BerriAI/litellm/issues/24512) en mars 2026 et la [campagne du ver Mini Shai-Hulud](https://socket.dev/blog/tanstack-npm-packages-compromised-mini-shai-hulud-supply-chain-attack) en mai 2026, toutes les dépendances doivent suivre ces règles : + +| Type de source | Traitement requis | Justification | +|---|---|---| +| **Paquet PyPI** | `>=floor, # vX.Y.Z` | +| **Installations pip réservées à la CI** | `==exact` | Builds CI hermétiques ; le churn est acceptable. | + +**Toute nouvelle dépendance PyPI dans une PR doit avoir une borne supérieure `=X.Y.Z` sans borne seront rejetées par les relecteurs. Le workflow CI `supply-chain-audit.yml` signale aussi les changements de manifestes de dépendances pour revue manuelle. + +**Comment déterminer le plafond :** +- Si le paquet est en version `1.x.y`, utilisez `<2`. +- Si le paquet est en version `0.x.y` (pré-1.0), utilisez `<0.(current_minor + 2)` — par exemple si la version courante est `0.29.x`, utilisez `<0.32`. Cela donne ~2 versions mineures de marge tout en gardant la fenêtre assez étroite pour qu'une version issue d'une prise de contrôle hostile ait peu de chances d'y atterrir. +- Exception : les paquets aux API très stables (par exemple `aiohttp-socks`) peuvent utiliser `<1` à la discrétion du relecteur. + +**Exemples :** +```toml +# ✅ Correct — post-1.0 +"openai>=2.21.0,<3" +"pydantic>=2.12.5,<3" + +# ✅ Correct — pre-1.0 (tight minor window) +"asyncpg>=0.29,<0.32" +"aiosqlite>=0.20,<0.23" +"hindsight-client>=0.4.22,<0.5" + +# ❌ Rejected — no upper bound +"some-package>=1.2.3" + +# ❌ Rejected — too tight (blocks legitimate patches) +"some-package==1.2.3" + +# ❌ Rejected — too loose for pre-1.0 (allows 80 minor versions) +"some-package>=0.20,<1" +``` + +**PR de référence :** #2796 (retrait de litellm), #2810 (passe sur les bornes supérieures), #9801 (épinglage par SHA + CI supply-chain-audit). + +--- + +## Processus de pull request + +### Nommage des branches + +``` +fix/description # Bug fixes +feat/description # New features +docs/description # Documentation +test/description # Tests +refactor/description # Code restructuring +``` + +### Avant de soumettre + +1. **Lancez les tests** : `scripts/run_tests.sh` (recommandé ; identique à la CI) ou `pytest tests/ -v` avec le venv du projet activé +2. **Testez manuellement** : lancez `hermes` et exercez le chemin de code que vous avez modifié +3. **Vérifiez l'impact multiplateforme** : si vous touchez aux E/S de fichiers, à la gestion des processus ou au terminal, pensez à macOS, Linux et WSL2 +4. **Gardez les PR ciblées** : un seul changement logique par PR. Ne mélangez pas une correction de bug avec un refactoring et une nouvelle fonctionnalité. + +### Description de la PR + +Incluez : +- **Quoi** a changé et **pourquoi** +- **Comment le tester** (étapes de reproduction pour les bugs, exemples d'usage pour les fonctionnalités) +- **Les plateformes** sur lesquelles vous avez testé +- Les références aux issues liées + +### Messages de commit + +Nous utilisons les [Conventional Commits](https://www.conventionalcommits.org/) : + +``` +(): +``` + +| Type | À utiliser pour | +|------|---------| +| `fix` | Corrections de bugs | +| `feat` | Nouvelles fonctionnalités | +| `docs` | Documentation | +| `test` | Tests | +| `refactor` | Restructuration du code (sans changement de comportement) | +| `chore` | Build, CI, mises à jour de dépendances | + +Scopes : `cli`, `gateway`, `tools`, `skills`, `agent`, `install`, `whatsapp`, `security`, etc. + +Exemples : +``` +fix(cli): prevent crash in save_config_value when model is a string +feat(gateway): add WhatsApp multi-user session isolation +fix(security): prevent shell injection in sudo password piping +test(tools): add unit tests for file_operations +``` + +--- + +## Signaler un problème + +- Utilisez les [GitHub Issues](https://github.com/NousResearch/hermes-agent/issues) +- Incluez : OS, version de Python, version d'Hermes (`hermes version`), traceback d'erreur complet +- Incluez les étapes de reproduction +- Vérifiez les issues existantes avant de créer des doublons +- Pour les vulnérabilités de sécurité, merci de les signaler en privé + +--- + +## Communauté + +- **Discord** : [discord.gg/NousResearch](https://discord.gg/NousResearch) — pour poser des questions, présenter vos projets et partager des compétences +- **GitHub Discussions** : pour les propositions de conception et les discussions d'architecture +- **Skills Hub** : téléversez des compétences spécialisées sur un registre et partagez-les avec la communauté + +--- + +## Licence + +En contribuant, vous acceptez que vos contributions soient placées sous la [licence MIT](LICENSE). diff --git a/README.fr.md b/README.fr.md new file mode 100644 index 0000000000000..3cb9b8c80f1a4 --- /dev/null +++ b/README.fr.md @@ -0,0 +1,265 @@ +

+ Hermes Agent +

+ +# Hermes Agent ☤ +

+ Hermes Agent | Hermes Desktop +

+

+ Documentation + Discord + Licence : MIT + Créé par Nous Research + English + 中文 + اردو + Español +

+ +**L'agent d'IA auto-améliorant conçu par [Nous Research](https://nousresearch.com).** C'est le seul agent doté d'une boucle d'apprentissage intégrée : il crée des compétences à partir de son expérience, les améliore à l'usage, se pousse lui-même à pérenniser ses connaissances, fouille ses propres conversations passées et construit, session après session, un modèle de plus en plus fin de qui vous êtes. Faites-le tourner sur un VPS à 5 $, un cluster GPU ou une infrastructure serverless qui ne coûte presque rien à l'arrêt. Il n'est pas rivé à votre ordinateur portable — parlez-lui depuis Telegram pendant qu'il travaille sur une VM dans le cloud. + +Utilisez le modèle de votre choix — [Nous Portal](https://portal.nousresearch.com), OpenRouter, OpenAI, votre propre endpoint et [bien d'autres](https://hermes-agent.nousresearch.com/docs/integrations/providers). Changez de modèle avec `hermes model` — pas de modification de code, pas de verrouillage. + + + + + + + + + +
Une vraie interface de terminalTUI complète avec édition multiligne, autocomplétion des commandes slash, historique des conversations, interruption et redirection à la volée, et sortie des outils en streaming.
Il vit là où vous vivezTelegram, Discord, Slack, WhatsApp, Signal et CLI — le tout depuis un seul processus gateway. Transcription des mémos vocaux, continuité des conversations d'une plateforme à l'autre.
Une boucle d'apprentissage ferméeMémoire organisée par l'agent, avec des rappels périodiques. Création autonome de compétences après les tâches complexes. Les compétences s'améliorent d'elles-mêmes à l'usage. Recherche de sessions FTS5 avec résumé par LLM pour retrouver des informations d'une session à l'autre. Modélisation dialectique de l'utilisateur via Honcho. Compatible avec le standard ouvert agentskills.io.
Automatisations planifiéesPlanificateur cron intégré avec livraison sur n'importe quelle plateforme. Rapports quotidiens, sauvegardes nocturnes, audits hebdomadaires — le tout en langage naturel, sans aucune surveillance.
Il délègue et paralléliseLancez des sous-agents isolés pour mener plusieurs chantiers en parallèle. Écrivez des scripts Python qui appellent les outils via RPC, condensant des pipelines à plusieurs étapes en tours sans aucun coût de contexte.
Tourne partout, pas seulement sur votre laptopSix backends de terminal — local, Docker, SSH, Singularity, Modal et Daytona. Daytona et Modal offrent une persistance serverless : l'environnement de votre agent hiberne quand il est inactif et se réveille à la demande, pour un coût quasi nul entre les sessions. Faites-le tourner sur un VPS à 5 $ ou un cluster GPU.
Prêt pour la rechercheGénération de trajectoires par lots, compression de trajectoires pour entraîner la prochaine génération de modèles d'appel d'outils.
+ +--- + +## Installation rapide + +### Linux, macOS, WSL2, Termux + +```bash +curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash +``` + +### Windows (natif, PowerShell) + +> **Attention :** sous Windows natif, Hermes fonctionne sans WSL — la CLI, le gateway, la TUI et les outils tournent tous nativement. Si vous préférez passer par WSL2, la commande Linux/macOS ci-dessus y fonctionne aussi. Vous avez trouvé un bug ? Merci d'[ouvrir un ticket](https://github.com/NousResearch/hermes-agent/issues). + +Exécutez ceci dans PowerShell : + +```powershell +iex (irm https://hermes-agent.nousresearch.com/install.ps1) +``` + +L'installateur s'occupe de tout : uv, Python 3.11, Node.js, ripgrep, ffmpeg, **et un Git Bash portable** (MinGit, décompressé dans `%LOCALAPPDATA%\hermes\git` — aucun droit administrateur requis, complètement isolé de toute installation Git du système). Hermes utilise ce Git Bash embarqué pour exécuter les commandes shell. + +Si Git est déjà installé, l'installateur le détecte et l'utilise à la place. Sinon, un téléchargement MinGit d'environ 45 Mo suffit — il ne touchera pas au Git du système et n'interférera pas avec lui. + +> **Android / Termux :** le chemin d'installation manuel testé est documenté dans le [guide Termux](https://hermes-agent.nousresearch.com/docs/getting-started/termux). Sur Termux, Hermes installe un extra `.[termux]` restreint, car l'extra complet `.[all]` tire actuellement des dépendances vocales incompatibles avec Android. +> +> **Windows :** Windows natif est entièrement pris en charge — la commande PowerShell ci-dessus installe tout. Si vous préférez WSL2, la commande Linux y fonctionne aussi. L'installation Windows native se trouve dans `%LOCALAPPDATA%\hermes` ; sous WSL2, elle s'installe dans `~/.hermes` comme sous Linux. + +Après l'installation : + +```bash +source ~/.bashrc # reload shell (or: source ~/.zshrc) +hermes # start chatting! +``` + +### Dépannage + +#### Windows Defender ou un antivirus signale `uv.exe` comme malveillant + +Si votre antivirus (Bitdefender, Windows Defender, etc.) met en quarantaine `uv.exe` depuis le dossier `bin` de Hermes (`%LOCALAPPDATA%\hermes\bin\uv.exe`), il s'agit d'un **faux positif**. Ce fichier est `uv` d'Astral — le gestionnaire de paquets Python écrit en Rust qu'Hermes embarque pour gérer son environnement Python. Les moteurs antivirus à base de ML signalent couramment les binaires Rust non signés qui téléchargent et installent des paquets. + +**Pour vérifier que votre copie est authentique :** + +```powershell +# Install GitHub CLI if needed +winget install --id GitHub.cli + +# Login to GitHub +gh auth login + +# Run verification +$uv = "$env:LOCALAPPDATA\hermes\bin\uv.exe" +$ver = (& $uv --version).Split(' ')[1] +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$zip = "$env:TEMP\uv.zip" +Invoke-WebRequest "https://github.com/astral-sh/uv/releases/download/$ver/uv-x86_64-pc-windows-msvc.zip" -OutFile $zip -UseBasicParsing +gh attestation verify $zip --repo astral-sh/uv +Expand-Archive $zip "$env:TEMP\uv_x" -Force +(Get-FileHash "$env:TEMP\uv_x\uv.exe").Hash -eq (Get-FileHash $uv).Hash +``` + +Si l'attestation affiche « Verification succeeded » et que la dernière ligne renvoie `True`, tout est en ordre. + +**Pour mettre Hermes en liste blanche :** +- **Windows Defender :** lancez PowerShell en administrateur → `Add-MpPreference -ExclusionPath "$env:LOCALAPPDATA\hermes\bin"` +- **Bitdefender :** ajoutez une exception dans la console Bitdefender (Protection > Antivirus > Paramètres > Gérer les exceptions) +- Mettez le **dossier** en liste blanche, pas le hash du fichier — Hermes met `uv` à jour et le hash change à chaque version + +Pour plus de contexte, consultez les rapports upstream chez Astral : [astral-sh/uv#13553](https://github.com/astral-sh/uv/issues/13553), [astral-sh/uv#15011](https://github.com/astral-sh/uv/issues/15011), [astral-sh/uv#10079](https://github.com/astral-sh/uv/issues/10079). + +--- + +## Premiers pas + +```bash +hermes # Interactive CLI — start a conversation +hermes model # Choose your LLM provider and model +hermes tools # Configure which tools are enabled +hermes config set # Set individual config values +hermes gateway # Start the messaging gateway (Telegram, Discord, etc.) +hermes setup # Run the full setup wizard (configures everything at once) +hermes claw migrate # Migrate from OpenClaw (if coming from OpenClaw) +hermes update # Update to the latest version +hermes doctor # Diagnose any issues +``` + +📖 **[Documentation complète →](https://hermes-agent.nousresearch.com/docs/)** + +--- + +## Fini la collection de clés API — Nous Portal + +Hermes fonctionne avec le fournisseur de votre choix — ça ne changera pas. Mais si vous préférez ne pas accumuler cinq clés API distinctes pour le modèle, la recherche web, la génération d'images, le TTS et un navigateur cloud, **[Nous Portal](https://portal.nousresearch.com)** couvre tout cela avec un seul abonnement : + +- **Plus de 300 modèles** — choisissez n'importe lequel avec `/model ` +- **Tool Gateway** — recherche web (Firecrawl), génération d'images (FAL), synthèse vocale (OpenAI), navigateur cloud (Browser Use), le tout routé via votre abonnement. Aucun compte supplémentaire. + +Une seule commande depuis une installation vierge : + +```bash +hermes setup --portal +``` + +Elle vous connecte via OAuth, définit Nous comme fournisseur et active le Tool Gateway. Vérifiez à tout moment ce qui est branché avec `hermes portal info`. Tous les détails sur la [page de documentation du Tool Gateway](https://hermes-agent.nousresearch.com/docs/user-guide/features/tool-gateway). + +Vous pouvez toujours apporter vos propres clés, outil par outil — le gateway se configure backend par backend, pas en tout ou rien. + +--- + +## Référence rapide : CLI vs messagerie + +Hermes a deux points d'entrée : lancez l'interface de terminal avec `hermes`, ou démarrez le gateway et parlez-lui depuis Telegram, Discord, Slack, WhatsApp, Signal ou par e-mail. Une fois la conversation engagée, beaucoup de commandes slash sont communes aux deux interfaces. + +| Action | CLI | Plateformes de messagerie | +| ----------------------------------------- | --------------------------------------------- | -------------------------------------------------------------------------------- | +| Commencer à discuter | `hermes` | Lancez `hermes gateway setup` + `hermes gateway start`, puis envoyez un message au bot | +| Repartir d'une conversation vierge | `/new` ou `/reset` | `/new` ou `/reset` | +| Changer de modèle | `/model [provider:model]` | `/model [provider:model]` | +| Définir une personnalité | `/personality [name]` | `/personality [name]` | +| Réessayer ou annuler le dernier tour | `/retry`, `/undo` | `/retry`, `/undo` | +| Compresser le contexte / voir la consommation | `/compress`, `/usage`, `/insights [--days N]` | `/compress`, `/usage`, `/insights [days]` | +| Parcourir les compétences | `/skills` ou `/` | `/` | +| Interrompre le travail en cours | `Ctrl+C` ou envoyer un nouveau message | `/stop` ou envoyer un nouveau message | +| Statut propre à la plateforme | `/platforms` | `/status`, `/sethome` | + +Pour la liste complète des commandes, consultez le [guide CLI](https://hermes-agent.nousresearch.com/docs/user-guide/cli) et le [guide du gateway de messagerie](https://hermes-agent.nousresearch.com/docs/user-guide/messaging). + +--- + +## Documentation + +Toute la documentation se trouve sur **[hermes-agent.nousresearch.com/docs](https://hermes-agent.nousresearch.com/docs/)** : + +| Section | Contenu | +| --------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- | +| [Démarrage rapide](https://hermes-agent.nousresearch.com/docs/getting-started/quickstart) | Installation → configuration → première conversation en 2 minutes | +| [Utilisation de la CLI](https://hermes-agent.nousresearch.com/docs/user-guide/cli) | Commandes, raccourcis clavier, personnalités, sessions | +| [Configuration](https://hermes-agent.nousresearch.com/docs/user-guide/configuration) | Fichier de configuration, fournisseurs, modèles, toutes les options | +| [Gateway de messagerie](https://hermes-agent.nousresearch.com/docs/user-guide/messaging) | Telegram, Discord, Slack, WhatsApp, Signal, Home Assistant | +| [Sécurité](https://hermes-agent.nousresearch.com/docs/user-guide/security) | Approbation des commandes, appairage par DM, isolation en conteneur | +| [Outils et toolsets](https://hermes-agent.nousresearch.com/docs/user-guide/features/tools) | Plus de 40 outils, système de toolsets, backends de terminal | +| [Système de compétences](https://hermes-agent.nousresearch.com/docs/user-guide/features/skills) | Mémoire procédurale, Skills Hub, création de compétences | +| [Mémoire](https://hermes-agent.nousresearch.com/docs/user-guide/features/memory) | Mémoire persistante, profils utilisateur, bonnes pratiques | +| [Intégration MCP](https://hermes-agent.nousresearch.com/docs/user-guide/features/mcp) | Connectez n'importe quel serveur MCP pour étendre les capacités | +| [Planification cron](https://hermes-agent.nousresearch.com/docs/user-guide/features/cron) | Tâches planifiées avec livraison sur les plateformes | +| [Fichiers de contexte](https://hermes-agent.nousresearch.com/docs/user-guide/features/context-files) | Un contexte de projet qui façonne chaque conversation | +| [Architecture](https://hermes-agent.nousresearch.com/docs/developer-guide/architecture) | Structure du projet, boucle de l'agent, classes principales | +| [Contribuer](https://hermes-agent.nousresearch.com/docs/developer-guide/contributing) | Environnement de développement, processus de PR, style de code | +| [Référence CLI](https://hermes-agent.nousresearch.com/docs/reference/cli-commands) | Toutes les commandes et tous les flags | +| [Variables d'environnement](https://hermes-agent.nousresearch.com/docs/reference/environment-variables) | Référence complète des variables d'environnement | + +--- + +## Migration depuis OpenClaw + +Si vous venez d'OpenClaw, Hermes peut importer automatiquement vos réglages, vos mémoires, vos compétences et vos clés API. + +**Lors de la première configuration :** l'assistant de configuration (`hermes setup`) détecte automatiquement `~/.openclaw` et propose la migration avant de commencer la configuration. + +**À tout moment après l'installation :** + +```bash +hermes claw migrate # Interactive migration (full preset) +hermes claw migrate --dry-run # Preview what would be migrated +hermes claw migrate --preset user-data # Migrate without secrets +hermes claw migrate --overwrite # Overwrite existing conflicts +``` + +Ce qui est importé : + +- **SOUL.md** — fichier de persona +- **Mémoires** — entrées de MEMORY.md et USER.md +- **Compétences** — compétences créées par l'utilisateur → `~/.hermes/skills/openclaw-imports/` +- **Liste blanche de commandes** — motifs d'approbation +- **Réglages de messagerie** — configuration des plateformes, utilisateurs autorisés, répertoire de travail +- **Clés API** — secrets en liste blanche (Telegram, OpenRouter, OpenAI, Anthropic, ElevenLabs) +- **Assets TTS** — fichiers audio de l'espace de travail +- **Instructions d'espace de travail** — AGENTS.md (avec `--workspace-target`) + +Voir `hermes claw migrate --help` pour toutes les options, ou utilisez la compétence `openclaw-migration` pour une migration interactive guidée par l'agent, avec prévisualisation en dry-run. + +--- + +## Contribuer + +Les contributions sont les bienvenues ! Consultez le [Guide de contribution](CONTRIBUTING.fr.md) pour l'environnement de développement, le style de code et le processus de PR. + +Démarrage rapide pour les contributeurs — utilisez l'installateur standard, puis +travaillez depuis le checkout git complet qu'il crée dans `$HERMES_HOME/hermes-agent` +(généralement `~/.hermes/hermes-agent`). C'est la disposition attendue par +`hermes update`, le venv managé, les dépendances chargées à la demande, le gateway +et l'outillage de la documentation. + +```bash +curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash +cd "${HERMES_HOME:-$HOME/.hermes}/hermes-agent" +uv pip install -e ".[all,dev]" +scripts/run_tests.sh +``` + +Solution de repli avec clone manuel (pour les clones jetables ou la CI, quand vous +ne voulez volontairement pas de la disposition d'installation managée) : + +Créez le venv en dehors de l'arborescence clonée — un venv placé dans le répertoire +depuis lequel l'agent opère peut être effacé par une commande en chemin relatif que +l'agent exécute contre son propre checkout, détruisant le runtime en pleine session. + +```bash +curl -LsSf https://astral.sh/uv/install.sh | sh +uv venv ~/.hermes/venvs/hermes-dev --python 3.11 +source ~/.hermes/venvs/hermes-dev/bin/activate +uv pip install -e ".[all,dev]" +scripts/run_tests.sh +``` + +--- + +## Communauté + +- 💬 [Discord](https://discord.gg/NousResearch) +- 📚 [Skills Hub](https://agentskills.io) +- 🐛 [Issues](https://github.com/NousResearch/hermes-agent/issues) +- 🔌 [computer-use-linux](https://github.com/avifenesh/computer-use-linux) — Serveur MCP de contrôle du bureau Linux pour Hermes et d'autres hôtes MCP, avec arbres d'accessibilité AT-SPI, entrée Wayland/X11, captures d'écran et ciblage des fenêtres du compositeur. +- 🔌 [HermesClaw](https://github.com/AaronWong1999/hermesclaw) — Passerelle WeChat communautaire : faites tourner Hermes Agent et OpenClaw sur le même compte WeChat. + +--- + +## Licence + +MIT — voir [LICENSE](LICENSE). + +Conçu par [Nous Research](https://nousresearch.com). diff --git a/SECURITY.fr.md b/SECURITY.fr.md new file mode 100644 index 0000000000000..e63ca1566fab6 --- /dev/null +++ b/SECURITY.fr.md @@ -0,0 +1,371 @@ +# Politique de sécurité de Hermes Agent + +Ce document décrit le modèle de confiance de Hermes Agent, identifie +l'unique frontière de sécurité que le projet considère comme structurelle +et définit le périmètre des signalements de vulnérabilités. + +## 1. Signaler une vulnérabilité + +Signalez de manière privée via les [GitHub Security Advisories](https://github.com/NousResearch/hermes-agent/security/advisories/new) +ou **security@nousresearch.com**. N'ouvrez pas d'issues publiques pour +des vulnérabilités de sécurité. **Hermes Agent n'opère pas de programme +de bug bounty.** + +Un signalement utile comprend : + +- Une description concise et une évaluation de la gravité. +- Le composant affecté, identifié par chemin de fichier et plage de + lignes (ex. `path/to/file.py:120-145`). +- Les détails de l'environnement (`hermes version`, SHA du commit, OS, + version de Python). +- Une reproduction sur `main` ou la dernière release. +- Une indication de la frontière de confiance du §2 qui est franchie. + +Merci de lire le §2 et le §3 avant d'envoyer. Les signalements qui +démontrent les limites d'une heuristique intra-processus que cette +politique ne considère pas comme une frontière seront fermés comme hors +périmètre au titre du §3 — mais voyez le §3.2 : ils restent les +bienvenus sous forme d'issues ou de pull requests classiques, simplement +pas via le canal de sécurité privé. + +--- + +## 2. Modèle de confiance + +Hermes Agent est un agent personnel mono-utilisateur. Sa posture est en +couches, et ces couches n'ont pas toutes le même poids. Les rapporteurs +et les opérateurs doivent raisonner à leur sujet dans les mêmes termes. + +### 2.1 Définitions + +- **Processus de l'agent.** L'interpréteur Python qui exécute Hermes + Agent, y compris tout module Python qu'il a chargé (skills, plugins, + gestionnaires de hooks). +- **Backend de terminal.** Une cible d'exécution enfichable pour + l'outil `terminal()`. Le backend par défaut exécute les commandes + directement sur l'hôte. D'autres backends les exécutent dans un + conteneur, une sandbox cloud ou un hôte distant. +- **Surface d'entrée.** Tout canal par lequel du contenu entre dans le + contexte de l'agent : saisie de l'opérateur, requêtes web, e-mails, + messages du gateway, lectures de fichiers, réponses de serveurs MCP, + résultats d'outils. +- **Enveloppe de confiance.** L'ensemble des ressources auxquelles un + opérateur a implicitement donné accès à Hermes Agent en le lançant — + typiquement, tout ce que le compte utilisateur de l'opérateur peut + atteindre sur l'hôte. +- **Position.** Une déclaration explicite dans la documentation ou le + code de Hermes Agent sur la façon dont une couche consommatrice + (adaptateur, UI, écriture de fichiers, shell) doit traiter la sortie + de l'agent — ex. « le dashboard rend la sortie de l'agent comme du + HTML inerte ». + +### 2.2 La frontière : l'isolation au niveau de l'OS + +**La seule frontière de sécurité face à un LLM adverse est le système +d'exploitation.** Rien à l'intérieur du processus de l'agent ne +constitue un confinement — ni le portail d'approbation, ni la rédaction +des sorties, ni aucun scanner de motifs, ni aucune liste d'outils +autorisés. Tout composant intra-processus qui filtre la sortie du LLM +est une heuristique opérant sur une chaîne influencée par l'attaquant, +et cette politique le traite comme tel. + +Hermes Agent prend en charge deux postures d'isolation au niveau de +l'OS. Elles répondent à des menaces différentes et un opérateur doit +choisir en connaissance de cause. + +#### Isolation par le backend de terminal + +Un backend de terminal autre que celui par défaut exécute les commandes +shell émises par le LLM dans un conteneur, un hôte distant ou une +sandbox cloud. Les outils de fichiers (`read_file`, `write_file`, +`patch`) passent aussi par ce backend, puisqu'ils sont implémentés +au-dessus du contrat du shell — ils ne peuvent pas atteindre de chemins +que le backend n'expose pas. + +Ce que cela confine : tout ce que l'agent fait en émettant des +opérations shell ou fichiers. Ce que cela ne confine **pas** : tout ce +que l'agent fait dans son propre processus Python. Cela inclut l'outil +d'exécution de code (lancé comme sous-processus sur l'hôte), les +sous-processus MCP (lancés depuis l'environnement de l'agent), le +chargement des plugins, le déclenchement des hooks et le chargement des +skills (tous importés dans l'interpréteur de l'agent). + +L'isolation par le backend de terminal est la bonne posture lorsque le +risque redouté est l'émission par le LLM de commandes shell +destructrices ou d'écritures indésirables via les outils de fichiers, +et que l'opérateur est par ailleurs de confiance. + +#### Encapsulation du processus complet + +L'encapsulation du processus complet exécute l'intégralité de l'arbre +de processus de l'agent dans une sandbox. Chaque chemin de code — +shell, exécution de code, MCP, outils de fichiers, plugins, hooks, +chargement des skills — est soumis à la même politique de système de +fichiers, de réseau, de processus et (le cas échéant) d'inférence. + +Hermes Agent le permet de deux façons : + +- **L'image Docker et la configuration Compose de Hermes Agent.** Plus + légère ; l'agent tourne dans un conteneur standard avec des montages + et une politique réseau configurés par l'opérateur. +- **[NVIDIA OpenShell](https://github.com/NVIDIA/OpenShell)**. + OpenShell fournit des sandboxes par session avec une politique + déclarative couvrant le système de fichiers, le réseau (egress L7), + les processus/syscalls et le routage d'inférence. Les politiques + réseau et d'inférence sont rechargeables à chaud. Les identifiants + sont injectés depuis un magasin Provider et ne touchent jamais le + système de fichiers de la sandbox. + +Sous une encapsulation du processus complet, les heuristiques +intra-processus de Hermes Agent (§2.4) jouent le rôle de prévention +des accidents, par-dessus une véritable frontière. C'est la posture +prise en charge lorsque l'agent ingère du contenu provenant de surfaces +que l'opérateur ne contrôle pas — le web ouvert, les e-mails entrants, +les canaux multi-utilisateurs, les serveurs MCP non fiables — ainsi que +pour les déploiements en production ou partagés. + +Les opérateurs qui exécutent le backend local par défaut avec des +surfaces d'entrée non fiables, ou qui exécutent une sandbox de backend +de terminal en s'attendant à ce qu'elle confine des chemins de code qui +ne passent pas par le shell, opèrent en dehors de la posture de +sécurité prise en charge. + +### 2.3 Portée des identifiants + +Hermes Agent filtre l'environnement qu'il transmet à ses composants +intra-processus de moindre confiance : sous-processus shell, +sous-processus MCP, scripts de tâches cron et processus enfant +d'exécution de code. Les identifiants comme les clés API des providers +et les tokens du gateway sont retirés par défaut ; les variables +explicitement déclarées par l'opérateur ou par une skill chargée sont +transmises. + +Cela réduit l'exfiltration opportuniste. Ce n'est pas du confinement. +Tout composant s'exécutant dans le processus de l'agent (skills, +plugins, gestionnaires de hooks) peut lire tout ce que l'agent lui-même +peut lire, y compris les identifiants en mémoire. La parade contre un +composant intra-processus compromis est la revue par l'opérateur avant +installation (§2.4, §2.5), pas le nettoyage de l'environnement. + +### 2.4 Heuristiques intra-processus + +Les composants suivants filtrent le comportement du LLM ou alertent à +son sujet. Ils sont utiles. Ce ne sont pas des frontières. + +- Le **portail d'approbation** détecte les motifs shell destructeurs + courants et demande confirmation à l'opérateur avant exécution. Le + shell est Turing-complet ; une liste noire sur des chaînes shell est + structurellement incomplète. Le portail attrape les erreurs en mode + coopératif, pas les sorties adverses. +- La **rédaction des sorties** supprime de l'affichage les motifs + ressemblant à des secrets. Un producteur de sorties déterminé la + contournera. +- **Skills Guard** analyse le contenu des skills installables à la + recherche de motifs d'injection. C'est une aide à la revue ; la + frontière pour les skills tierces est la revue par l'opérateur avant + installation. Passer une skill en revue signifie lire son code Python + et ses scripts, pas seulement sa description SKILL.md — les skills + exécutent du Python arbitraire au moment de l'import. + +### 2.5 Modèle de confiance des plugins + +Les plugins se chargent dans le processus de l'agent et s'exécutent +avec tous ses privilèges : ils peuvent lire les mêmes identifiants, +appeler les mêmes outils, enregistrer les mêmes hooks et importer les +mêmes modules que n'importe quel code livré dans le dépôt. La frontière +pour les plugins tiers est la revue par l'opérateur avant installation +— la même règle que pour les skills (§2.4), mentionnée à part parce que +les plugins sont architecturalement plus lourds et embarquent souvent +leurs propres services d'arrière-plan, écouteurs réseau et dépendances. + +Un plugin malveillant ou bogué n'est pas une vulnérabilité de Hermes +Agent en soi. Les bugs dans le chemin d'installation ou de découverte +des plugins de Hermes Agent qui empêchent l'opérateur de voir ce qu'il +installe sont dans le périmètre au titre du §3.1. + +### 2.6 Surfaces externes + +Une **surface externe** est tout canal extérieur au processus local de +l'agent par lequel un appelant peut déclencher du travail de l'agent, +résoudre des approbations ou recevoir la sortie de l'agent. Chaque +surface a son propre modèle d'autorisation, mais les règles ci-dessous +s'appliquent uniformément. + +**Surfaces dans Hermes Agent :** + +- **Adaptateurs de plateforme du gateway.** Les intégrations de + messagerie dans `gateway/platforms/` (Telegram, Discord, Slack, + e-mail, SMS, etc.) et les adaptateurs analogues livrés sous forme de + plugins. +- **Surfaces HTTP exposées au réseau.** L'adaptateur du serveur API, le + plugin du dashboard, les endpoints HTTP du plugin kanban, et tout + autre plugin qui ouvre un socket d'écoute. +- **Adaptateurs éditeur / IDE.** L'adaptateur ACP (`acp_adapter/`) et + les intégrations équivalentes qui acceptent des requêtes d'un + processus client local. +- **Le gateway TUI (`tui_gateway/`).** Backend JSON-RPC de l'interface + terminal Ink, joint via IPC local. + +**Règles uniformes :** + +1. **Une autorisation est requise à chaque surface qui franchit une + frontière de confiance.** Pour les surfaces de messagerie et HTTP + réseau, la frontière est le réseau : l'autorisation prend la forme + d'une liste d'appelants autorisés configurée par l'opérateur. Pour + les surfaces éditeur et IPC local (ACP, gateway TUI), la frontière + est le compte utilisateur de l'hôte : l'autorisation consiste à + s'appuyer sur le contrôle d'accès de l'OS (permissions de fichiers, + liaisons loopback uniquement) et à ne pas exposer la surface au-delà + de l'utilisateur local sans une couche d'authentification réseau + explicite. +2. **Une liste d'autorisation est requise pour chaque adaptateur exposé + au réseau qui est activé.** Les adaptateurs doivent refuser de + déclencher du travail de l'agent, de résoudre des approbations ou de + relayer des sorties tant qu'aucune liste d'autorisation n'est + définie. Les chemins de code qui laissent passer par défaut + lorsqu'aucune liste n'est configurée sont des bugs de code dans le + périmètre au titre du §3.1. +3. **Les identifiants de session sont des poignées de routage, pas des + frontières d'autorisation.** Connaître l'ID de session d'un autre + appelant ne donne pas accès à ses approbations ni à ses sorties ; + l'autorisation est toujours revérifiée contre la liste + d'autorisation (ou son équivalent au niveau de l'OS). +4. **Au sein de l'ensemble autorisé, tous les appelants bénéficient de + la même confiance.** Hermes Agent ne modélise pas de capacités par + appelant au sein d'un même adaptateur. Les opérateurs qui ont besoin + d'une séparation des capacités doivent exécuter des instances + d'agent distinctes avec des listes d'autorisation distinctes. +5. **Lier une surface locale à une interface autre que loopback est une + décision d'opérateur de dernier recours (§3.2).** Le dashboard et + les autres serveurs HTTP de plugins écoutent en loopback par + défaut ; les exposer via `--host 0.0.0.0` ou équivalent fait du + durcissement pour exposition publique (§4) la responsabilité de + l'opérateur. + +--- + +## 3. Périmètre + +### 3.1 Dans le périmètre + +- L'échappement d'une posture d'isolation OS déclarée (§2.2) : un + chemin de code contrôlé par l'attaquant atteignant un état que la + posture prétendait confiner. +- L'accès non autorisé à une surface externe : un appelant hors de + l'ensemble d'autorisation configuré (liste d'autorisation, ou + équivalent au niveau de l'OS pour les surfaces IPC locales) qui + déclenche du travail, reçoit des sorties ou résout des approbations + (§2.6). +- L'exfiltration d'identifiants : fuite d'identifiants de l'opérateur + ou de matériel d'autorisation de session vers une destination hors de + l'enveloppe de confiance, via un mécanisme qui aurait dû l'empêcher + (bug de nettoyage de l'environnement, journalisation d'un adaptateur, + erreur de transport qui déverse des identifiants vers un amont, + etc.). +- Les violations de la documentation du modèle de confiance : du code + qui se comporte contrairement à ce que cette politique, la propre + documentation de Hermes Agent ou les attentes raisonnables d'un + opérateur laisseraient prévoir — y compris les cas où Hermes Agent a + documenté une position sur la façon dont sa sortie doit être rendue + par une couche consommatrice (dashboard, adaptateur de gateway, + écriture de fichiers, shell) et où un chemin de code enfreint cette + position. + +### 3.2 Hors périmètre + +« Hors périmètre » signifie ici « pas une vulnérabilité de sécurité au +sens de cette politique ». Cela ne signifie pas « pas digne d'être +signalé ». Les améliorations des heuristiques intra-processus, les +idées de durcissement et les corrections d'UX sont les bienvenues sous +forme d'issues ou de pull requests classiques — le portail +d'approbation peut toujours attraper plus de motifs, la rédaction peut +toujours devenir plus fine, le comportement des adaptateurs peut +toujours être resserré. Ces éléments ne passent simplement pas par le +canal de divulgation privé et ne donnent pas lieu à des avis de +sécurité. + +- **Les contournements des heuristiques intra-processus (§2.4)** — + contournements des regex du portail d'approbation, contournements de + la rédaction, contournements des motifs de Skills Guard, et + signalements analogues visant de futures heuristiques. Ces composants + ne sont pas des frontières ; les déjouer n'est pas une vulnérabilité + au sens de cette politique. +- **L'injection de prompt en soi.** Amener le LLM à émettre une sortie + inhabituelle — via du contenu injecté, une hallucination, des + artefacts d'entraînement ou toute autre cause — n'est pas en soi une + vulnérabilité. « J'ai réussi une injection de prompt » sans + enchaînement vers un résultat du §3.1 n'est pas un signalement + exploitable au sens de cette politique. +- **Les conséquences d'une posture d'isolation choisie.** Les + signalements indiquant qu'un chemin de code opérant dans le périmètre + de sa posture peut faire ce que cette posture permet ne sont pas des + vulnérabilités. Exemples : des outils shell ou fichiers atteignant + l'état de l'hôte sous le backend local ; des sous-processus + d'exécution de code ou MCP atteignant l'état de l'hôte sous une + isolation par backend de terminal qui ne sandboxe que le shell ; des + signalements dont les préconditions exigent un accès en écriture + préexistant à des fichiers de configuration ou d'identifiants + appartenant à l'opérateur (ceux-ci sont déjà à l'intérieur de + l'enveloppe de confiance). +- **Les réglages de dernier recours documentés.** Les compromis choisis + par l'opérateur qui désactivent explicitement des protections : + `--insecure` et les flags équivalents sur le dashboard ou d'autres + composants, approbations désactivées, backend local en production, + profils de développement qui contournent la sécurité de hermes-home, + et similaires. Les signalements contre ces configurations ne sont pas + des vulnérabilités — c'est précisément le rôle du flag. +- **Les skills et plugins issus de la communauté.** Les skills tierces + (y compris le dépôt de skills de la communauté) et les plugins tiers + relèvent de la surface de revue de l'opérateur, pas de la surface de + confiance de Hermes Agent (§2.4, §2.5). Une skill ou un plugin qui + fait quelque chose de malveillant est le mode de défaillance attendu + d'un élément qui n'a pas été passé en revue, pas une vulnérabilité de + Hermes Agent. Les bugs dans le chemin d'installation des skills ou + des plugins de Hermes Agent qui empêchent l'opérateur de voir ce + qu'il installe sont dans le périmètre au titre du §3.1. +- **L'exposition publique sans contrôles externes.** Exposer le gateway + ou l'API à l'internet public sans authentification, VPN ni pare-feu. +- **Les restrictions de lecture/écriture au niveau des outils dans une + posture où le shell est permis.** Si un chemin est atteignable via + l'outil terminal, les signalements indiquant que d'autres outils de + fichiers peuvent l'atteindre n'apportent rien. + +--- + +## 4. Durcissement du déploiement + +La décision de durcissement la plus importante est de faire +correspondre l'isolation (§2.2) au niveau de confiance du contenu que +l'agent va ingérer. Au-delà de cela : + +- Exécutez l'agent en tant qu'utilisateur non root. L'image de + conteneur fournie le fait par défaut. +- Conservez les identifiants dans le fichier d'identifiants de + l'opérateur avec des permissions strictes, jamais dans la + configuration principale, jamais sous contrôle de version. Sous + OpenShell, utilisez le magasin Provider plutôt qu'un fichier + d'identifiants sur disque. +- N'exposez pas le gateway ou l'API à l'internet public sans VPN, + Tailscale ou protection par pare-feu. Sous OpenShell, utilisez la + couche de politique réseau pour restreindre l'egress. +- Configurez une liste d'appelants autorisés pour chaque adaptateur + exposé au réseau que vous activez (§2.6). +- Passez en revue les skills et plugins tiers avant installation + (§2.4, §2.5). Pour les skills, cela signifie lire le Python et les + scripts, pas seulement SKILL.md. Les rapports de Skills Guard et le + journal d'audit d'installation constituent la surface de revue. +- Hermes Agent inclut des garde-fous de chaîne d'approvisionnement pour + le lancement des serveurs MCP et pour les changements de dépendances + / paquets embarqués en CI ; voir `CONTRIBUTING.md` pour les détails. + +--- + +## 5. Divulgation + +- **Fenêtre de divulgation coordonnée :** 90 jours à compter du + signalement, ou jusqu'à la publication d'un correctif, selon la + première échéance. +- **Canal :** le fil GHSA ou la correspondance par e-mail avec + security@nousresearch.com. +- **Crédit :** les rapporteurs sont crédités dans les notes de version, + sauf demande d'anonymat. From 45107d1e5e22c558e576d317fc215594cb885d7a Mon Sep 17 00:00:00 2001 From: Billard <82095453+iacker@users.noreply.github.com> Date: Fri, 17 Jul 2026 07:20:12 +0200 Subject: [PATCH 02/14] docs(i18n): fix French cross-refs and add README language badge Address hermes-sweeper review on #63660: - SECURITY.fr.md: point security-policy cross-reference to CONTRIBUTING.fr.md (matching SECURITY.es.md localization convention) - README.md: add README.fr.md language badge so the French translation is discoverable from the primary entry point Signed-off-by: Billard <82095453+iacker@users.noreply.github.com> --- README.md | 1 + SECURITY.fr.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c05112266746f..248fa894826c0 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ 中文 اردو Español + Français

**The self-improving AI agent built by [Nous Research](https://nousresearch.com).** It's the only agent with a built-in learning loop — it creates skills from experience, improves them during use, nudges itself to persist knowledge, searches its own past conversations, and builds a deepening model of who you are across sessions. Run it on a $5 VPS, a GPU cluster, or serverless infrastructure that costs nearly nothing when idle. It's not tied to your laptop — talk to it from Telegram while it works on a cloud VM. diff --git a/SECURITY.fr.md b/SECURITY.fr.md index e63ca1566fab6..c09b90c0a085a 100644 --- a/SECURITY.fr.md +++ b/SECURITY.fr.md @@ -356,7 +356,7 @@ l'agent va ingérer. Au-delà de cela : journal d'audit d'installation constituent la surface de revue. - Hermes Agent inclut des garde-fous de chaîne d'approvisionnement pour le lancement des serveurs MCP et pour les changements de dépendances - / paquets embarqués en CI ; voir `CONTRIBUTING.md` pour les détails. + / paquets embarqués en CI ; voir `CONTRIBUTING.fr.md` pour les détails. --- From fb1452453948052f46d268a6a6316ee3d1270876 Mon Sep 17 00:00:00 2001 From: "Andrex Ibiza, MBA" <84248988+andrexibiza@users.noreply.github.com> Date: Thu, 6 Aug 2026 09:08:52 -0500 Subject: [PATCH 03/14] test(conformance): cross-language docs germination gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The localized-docs drift class, enforced: - scripts/docs_germination.py: the canonical pipeline (extract -> template -> assemble -> check). The parity gate verifies, per locale file against its English source: byte-identical code-fence sequence (code is never translated), verbatim backtick technical identifiers (root-doc names allowed as locale twins), link-target parity under the locale rewrite rule, resolvable internal fragments, heading-level structure parity, locale README back-link, and English-hub discoverability for germinated locales. Manifest carries the top-10 global languages (Ethnologue order) with status and provenance. - tests/conformance/test_docs_i18n_germination.py: behavior contracts (extractor correctness incl. GFM fence-closing rules and unbalanced backtick pairing) plus the gate itself: germinated locales must pass every class; manual/legacy locales (es, zh-CN, ur-pk) report all drift as warnings — debt visible in CI, roadmap to re-germinate. - docs/developer-guide/docs-i18n-germination-spec.md: the portable standard + measured debt report. French (README.fr.md, CONTRIBUTING.fr.md, SECURITY.fr.md) is the first germinated locale; the gate caught real drift in the seed translation (stale backend count, missing commands, stale conftest symbol, drifted security-surface paragraph). Signed-off-by: Andrex Ibiza, MBA <84248988+andrexibiza@users.noreply.github.com> --- .../docs-i18n-germination-spec.md | 84 +++ scripts/docs_germination.py | 595 ++++++++++++++++++ .../conformance/test_docs_i18n_germination.py | 241 +++++++ 3 files changed, 920 insertions(+) create mode 100644 docs/developer-guide/docs-i18n-germination-spec.md create mode 100644 scripts/docs_germination.py create mode 100644 tests/conformance/test_docs_i18n_germination.py diff --git a/docs/developer-guide/docs-i18n-germination-spec.md b/docs/developer-guide/docs-i18n-germination-spec.md new file mode 100644 index 0000000000000..16e0bc6410332 --- /dev/null +++ b/docs/developer-guide/docs-i18n-germination-spec.md @@ -0,0 +1,84 @@ +# Cross-Language Docs Germination — Spec & Runbook + +**Status: LIVE.** Enforced by `tests/conformance/test_docs_i18n_germination.py` +(CI). The gate imports the canonical pipeline from +`scripts/docs_germination.py` — the same module the CLI ships. + +## The model + +Root documentation (`README.md`, `CONTRIBUTING.md`, `SECURITY.md`) is a +**technical graph**: every code fence, every backtick identifier, every link +target, every heading is an edge. A localized document must reproduce that +graph with the same structure — translated prose is the only thing that may +change. + +| Edge | Localized rule | +|---|---| +| Code fences | Byte-identical sequence: marker, info string, body hash. **Code is never translated.** | +| Backtick spans | Every English span must survive verbatim (or, for root-doc names, as the locale twin). | +| Link targets | Every English target present under its locale-rewritten form (`CONTRIBUTING.md` → `CONTRIBUTING.fr.md`); fragments must resolve against the locale doc's own headings. | +| Headings | Same level sequence as English (structure fingerprint). | +| Hub edges | Locale README links back to `README.md`; a germinated locale is linked **from** `README.md` (badge hub). | + +A locale is **germinated** when it passes every class with zero errors. +Locales whose translation predates the pipeline (es, zh-CN, ur-pk) run the +same checks at **warning** severity — debt is visible in every CI run and +measured in the debt report below — and are re-germinated through the +pipeline when adopted. + +## The manifest + +`scripts/docs_germination.py` holds the manifest: the **top-10 global +languages** (Ethnologue 26th-edition order) plus the in-flight PRs for each +(interlocked, never duplicated): + +| Locale | Status | Provenance / interlock | +|---|---|---| +| zh-CN | manual | existing translation | +| hi | pending | PR #4763 in flight | +| es | manual | existing translation | +| fr | **germinated** | seed: iacker (#63660), refreshed by pipeline | +| ar | pending | RTL review required | +| bn | pending | PR #51306 in flight | +| pt | pending | — | +| ru | pending | PR #69658 in flight | +| ur-pk | manual | existing translation | +| id | pending | 11th by speakers — next in line | + +## Germination runbook (new language) + +1. `python scripts/docs_germination.py extract --doc README.md --locale ` + — the span inventory (fences, spans, links, headings). +2. `python scripts/docs_germination.py template --doc README.md --locale ` + — prose-placeholder template; translate the prose, keep every technical + span and code block verbatim. +3. Assemble the file (replace placeholders, fix headings in place). +4. `python scripts/docs_germination.py check` — iterate until the locale + passes every class. **Do not ship a locale that fails the gate.** +5. Add the language badge to `README.md` (the gate enforces this). +6. Add the locale to the manifest with `status: germinated` and its + provenance (credit ledger). + +## Why this exists + +Before the pipeline, every translation was a one-shot copy: `README.es.md` +was missing `hermes config get`, `README.fr.md` said "six backends" after +English added Vercel Sandbox (seven), and `CONTRIBUTING.es.md` had drifted to +602 lines against 1,009 in English. The gate turns that drift class into CI +failures with the exact edge named. + +## Debt report (measured 2026-08-06, current main) + +`python scripts/docs_germination.py check` → 35 warnings, 0 errors. + +| File | Drift classes (warnings) | +|---|---| +| README.zh-CN.md | fence sequence (9 vs 7 blocks), 9 missing code spans, missing external targets, heading levels | +| README.es.md | fence sequence (9 vs 8), 13 missing spans, missing external targets, heading levels | +| README.ur-pk.md | fence sequence (9 vs 8), 9 missing spans, missing external targets, heading levels | +| CONTRIBUTING.es.md | fence sequence (22 vs 13), 173 missing spans, missing external targets, heading levels | +| SECURITY.es.md | `CONTRIBUTING.md` span not localized | +| CONTRIBUTING.zh-CN.md / ur-pk | missing files | + +Re-germination of es is the highest-value next step (Spanish is the +largest manual locale and the deepest drift). diff --git a/scripts/docs_germination.py b/scripts/docs_germination.py new file mode 100644 index 0000000000000..611513aa37eaa --- /dev/null +++ b/scripts/docs_germination.py @@ -0,0 +1,595 @@ +#!/usr/bin/env python3 +"""Cross-language docs germination — root documentation i18n pipeline. + +The germination model (graph-gated): + + * The ENGLISH root docs are the canonical source graph. Every claim the + English docs make is an edge; a localized doc must reproduce the same + *technical graph* — identical code fences, identical link targets + (locally rewritten), identical backtick technical identifiers, identical + heading structure, resolvable internal anchors. + + * A locale file's parity with the English source is a CI-enforced gate + (`tests/conformance/test_docs_i18n_germination.py`). "Germinated" files + must pass every check; "legacy" files must pass the mechanical checks + (fences, link targets, code spans, anchors) with heading drift reported + as roadmap debt, never silently. + + * New languages germinate from the manifest below (top-10 global languages + by total speakers, Ethnologue 26th ed. order). The pipeline is: + extract (span inventory) -> translate (template) -> assemble (merge) + -> check (parity gate) -> ship. + +Pure stdlib. No network. No LLM calls in this file — it is the *gate*; the +translation itself happens out-of-band (human or agent) and is verified here. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import re +import sys +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] + +# ───────────────────────────────────────────────────────────────────────────── +# Manifest — the top-10 global languages (by total speakers, Ethnologue +# 26th edition order). This is the roadmap: status is either "germinated" +# (full parity gate), "manual" (existing translation, mechanical gate + +# reported heading debt), or "pending" (roadmap; no file yet). +# +# Provenance records who produced the seed translation — credit ledger. +# ───────────────────────────────────────────────────────────────────────────── + +ROOT_DOCS = ("README.md", "CONTRIBUTING.md", "SECURITY.md") + +TOP_10_LANGUAGES = ( + "zh-CN", # Mandarin Chinese (existing translation) + "hi", # Hindi (issue #4763 in flight) + "es", # Spanish (existing translation) + "fr", # French (this campaign; seed by iacker via #63660) + "ar", # Modern Standard Arabic + "bn", # Bengali (PR #51306 in flight) + "pt", # Portuguese + "ru", # Russian (PR #69658 in flight) + "ur-pk", # Urdu (existing translation) + "id", # Indonesian (next in line after the top-10) +) + +MANIFEST = { + "zh-CN": { + "name": "Chinese (Simplified)", + "native": "中文", + "badge": "中文", + "color": "red", + "status": "manual", + "provenance": "existing in-tree translation", + "notes": "legacy; heading parity debt reported, not gating", + }, + "hi": { + "name": "Hindi", + "native": "हिन्दी", + "badge": "हिन्दी", + "color": "orange", + "status": "pending", + "provenance": "—", + "notes": "PR #4763 (feat(docs): Hindi locale) in flight — interlocked", + }, + "es": { + "name": "Spanish", + "native": "Español", + "badge": "Español", + "color": "orange", + "status": "manual", + "provenance": "existing in-tree translation", + "notes": "legacy; CONTRIBUTING.es.md lags English (602 vs 1009 lines)", + }, + "fr": { + "name": "French", + "native": "Français", + "badge": "Français", + "color": "blue", + "status": "germinated", + "provenance": "seed: iacker (#63660), cherry-picked with authorship; refreshed against current main by the germination pipeline", + "notes": "full parity gate", + }, + "ar": { + "name": "Arabic", + "native": "العربية", + "badge": "العربية", + "color": "green", + "status": "pending", + "provenance": "—", + "notes": "RTL layout review required when germinating", + }, + "bn": { + "name": "Bengali", + "native": "বাংলা", + "badge": "বাংলা", + "color": "green", + "status": "pending", + "provenance": "—", + "notes": "PR #51306 (README.bn-BD.md) in flight — interlocked", + }, + "pt": { + "name": "Portuguese", + "native": "Português", + "badge": "Português", + "color": "yellow", + "status": "pending", + "provenance": "—", + "notes": "", + }, + "ru": { + "name": "Russian", + "native": "Русский", + "badge": "Русский", + "color": "purple", + "status": "pending", + "provenance": "—", + "notes": "PR #69658 (README.ru.md) in flight — interlocked", + }, + "ur-pk": { + "name": "Urdu", + "native": "اردو", + "badge": "اردو", + "color": "green", + "status": "manual", + "provenance": "existing in-tree translation", + "notes": "RTL layout; legacy heading parity debt reported, not gating", + }, + "id": { + "name": "Indonesian", + "native": "Bahasa Indonesia", + "badge": "Bahasa", + "color": "blue", + "status": "pending", + "provenance": "—", + "notes": "11th by speakers — next in line", + }, +} + + +def locale_file(doc: str, locale: str) -> str: + """Map a root doc to its locale file name (convention: README.fr.md).""" + stem, ext = doc.rsplit(".", 1) + return f"{stem}.{locale}.{ext}" + + +# ───────────────────────────────────────────────────────────────────────────── +# Extractors — the span inventory. Every extractor is pure over text. +# ───────────────────────────────────────────────────────────────────────────── + +FENCE_RE = re.compile(r"^(```+|~~~+)([^\n`]*)\n(.*?)^\1[^\n]*$", re.M | re.S) +LINK_RE = re.compile(r"\[([^\]]*)\]\(([^)\s]+)(?:\s+[^)]*)?\)") +HREF_RE = re.compile(r']*href="([^"]+)"[^>]*>') +CODE_SPAN_RE = re.compile(r"`([^`]+)`", re.S) +HEADING_RE = re.compile(r"^(#{1,6})\s+(.+?)\s*#*\s*$", re.M) + + +def _scan(text: str) -> tuple[str, list[dict]]: + """Line-based fence scanner (GFM rules): an opening fence carries the + info string; a closing fence is the same marker char repeated >= 3 times + with NO trailing text. A `` ```yaml `` line can therefore never close a + plain `` ``` `` block (the regex backreference form closes early and + leaves real code unmasked). Returns (masked_text, fence_records) where + masked_text has every fenced body blanked to spaces (line count and + prose position preserved).""" + lines = text.split("\n") + masked: list[str] = [] + fences: list[dict] = [] + marker: str | None = None + lang = "" + body: list[str] = [] + for line in lines: + if marker is None: + m = re.match(r"^(```+|~~~+)(.*)$", line) + if m: + marker, lang = m.group(1), m.group(2).strip() + body = [] + masked.append(" " * len(line)) + continue + masked.append(line) + else: + m = re.match(rf"^({re.escape(marker[0])}{{3,}})[ \t]*$", line) + if m: + fences.append( + { + "marker": marker, + "lang": lang, + "sha256": hashlib.sha256("\n".join(body).encode("utf-8")).hexdigest()[:16], + } + ) + marker = None + masked.append(" " * len(line)) + else: + body.append(line) + masked.append(" " * len(line)) + if marker is not None: # unterminated fence — record what we saw + fences.append( + { + "marker": marker, + "lang": lang, + "sha256": hashlib.sha256("\n".join(body).encode("utf-8")).hexdigest()[:16], + } + ) + return "\n".join(masked), fences + + +def extract_fences(text: str) -> list[dict]: + """Sequence of fenced code blocks: marker, language, body hash.""" + _, fences = _scan(text) + return fences + + +def extract_code_spans(text: str) -> list[str]: + """Verbatim backtick spans OUTSIDE code fences (technical identifiers, + commands, paths). Fenced code is adjudicated verbatim by fence parity. + + Backticks are paired GLOBALLY in position order (odd/even), not by + regex pair-matching: a dangling opener on one line must not turn the + NEXT line's backtick into a phantom closer. A pair whose span crosses a + line boundary is authoring noise (unbalanced backticks in the source), + not a technical identifier — it is excluded from the required set, so a + translation that fixes the imbalance is not penalized.""" + masked, _ = _scan(text) + positions = [m.start() for m in re.finditer(r"`", masked)] + spans: list[str] = [] + for i in range(0, len(positions) - 1, 2): + span = masked[positions[i] + 1 : positions[i + 1]] + if "\n" not in span: + spans.append(span) + return spans + + +def extract_links(text: str) -> list[tuple[str, str]]: + """(label, target) for markdown links and raw href attributes, outside + code fences (URLs inside code samples are code, not doc links).""" + masked, _ = _scan(text) + links = [(m.group(1), m.group(2)) for m in LINK_RE.finditer(masked)] + links += [(m.group(1), m.group(1)) for m in HREF_RE.finditer(masked)] + return links + + +def extract_headings(text: str) -> list[tuple[int, str]]: + """(level, title) pairs in document order, excluding fence-interior + comment lines that merely start with '# ' (code, not structure).""" + masked, _ = _scan(text) + return [ + (len(m.group(1)), m.group(2).strip()) + for m in HEADING_RE.finditer(masked) + ] + + +def slugify(title: str) -> str: + """GitHub-style anchor slug: lowercase, punctuation stripped, spaces→-, + leading/trailing hyphens removed (github-slugger normalization).""" + t = re.sub(r"[^\w\- ]", "", title, flags=re.UNICODE) + t = re.sub(r" ", "-", t.strip().lower()) + return t.strip("-") + + +def extract_anchors(text: str) -> set[str]: + """Slug set of the document's headings (what #fragments resolve to).""" + return {slugify(t) for _, t in extract_headings(text)} + + +def heading_levels(text: str) -> list[int]: + """The level sequence of headings — structure fingerprint.""" + return [lv for lv, _ in extract_headings(text)] + + +# ───────────────────────────────────────────────────────────────────────────── +# The locale rewrite rule: a relative link to one of the ROOT_DOCS inside a +# localized doc points at that doc's locale twin. Links to other locale +# files and to non-root targets (assets/, docs/, website/, URLs) stay as-is. +# ───────────────────────────────────────────────────────────────────────────── + +def rewrite_target(target: str, doc: str, locale: str) -> str: + if target.startswith(("http://", "https://", "#", "mailto:")): + return target + path, _, frag = target.partition("#") + base = path.rsplit("/", 1)[-1] + if base in ROOT_DOCS: + twin = locale_file(base, locale) + # Already the locale twin (e.g. the discoverability badge in the EN + # README links README.fr.md while the locale under check IS fr) — + # never double-suffix it. + if target == twin: + return target + return twin + (f"#{frag}" if frag else "") + return target + + +# ───────────────────────────────────────────────────────────────────────────── +# The parity gate. check_doc_parity returns a list of issue dicts: +# {"class": ..., "severity": "error"|"warning", "detail": ...} +# ───────────────────────────────────────────────────────────────────────────── + +def check_doc_parity( + en_text: str, loc_text: str, doc: str, locale: str, status: str +) -> list[dict]: + issues: list[dict] = [] + + def sev(cls: str) -> str: + # Germinated locales must be perfect: every drift class is an error. + # Legacy/manual translations report ALL drift classes as warnings — + # the debt is visible in every CI run and in the manifest, and the + # roadmap is to re-germinate them through the pipeline. + return "error" if status == "germinated" else "warning" + + def err(cls: str, detail: str) -> None: + issues.append({"class": cls, "severity": sev(cls), "detail": detail}) + + def warn(cls: str, detail: str) -> None: + issues.append({"class": cls, "severity": "warning", "detail": detail}) + + # 1. Fence parity — identical code fence sequence, byte-identical bodies + # (marker + language + body hash). Code is never translated. + en_f, loc_f = extract_fences(en_text), extract_fences(loc_text) + en_sig = [(f["marker"], f["lang"], f["sha256"]) for f in en_f] + loc_sig = [(f["marker"], f["lang"], f["sha256"]) for f in loc_f] + if en_sig != loc_sig: + err( + "fence_parity", + f"code fence sequence differs: EN {len(en_f)} blocks, " + f"{locale} {len(loc_f)} blocks " + f"(EN={en_sig} LOC={loc_sig})", + ) + + # 2. Code-span parity — every EN backtick identifier must survive. + # A span naming a root doc (``CONTRIBUTING.md``) is allowed its locale + # twin (``CONTRIBUTING.fr.md``) — same rewrite rule as link targets. + en_spans = extract_code_spans(en_text) + loc_spans = set(extract_code_spans(loc_text)) + missing = [ + s + for s in dict.fromkeys(en_spans) + if s not in loc_spans and rewrite_target(s, doc, locale) not in loc_spans + ] + if missing: + err("code_span_parity", f"{len(missing)} EN code spans missing from {locale}: {missing[:10]}{'…' if len(missing) > 10 else ''}") + + # 3. Link-target parity — every EN target must appear in the locale doc + # under its locale-rewritten form. Exceptions: + # - other-locale README targets (`README.es.md` etc.) are hub-selector + # links; each locale chooses its own hub subset, so they are not + # required (only the back-link to the English README is, check 6). + # - fragments are resolved separately (check 4). + en_targets = [t for _, t in extract_links(en_text)] + loc_targets = set(t for _, t in extract_links(loc_text)) + other_locale = re.compile(r"^README\.[A-Za-z0-9-]+\.md$") + for t in dict.fromkeys(en_targets): + if t.startswith(("http://", "https://", "mailto:")): + if t not in loc_targets: + err("link_target_parity", f"EN external target missing from {locale}: {t}") + elif t.startswith("#"): + continue # fragment resolution checked separately + elif other_locale.match(t): + continue # hub-selector exemption + else: + rt = rewrite_target(t, doc, locale) + if rt not in loc_targets: + err("link_target_parity", f"EN target missing from {locale} (rewritten {t} -> {rt})") + + # 4. Anchor parity — every internal fragment in the locale doc resolves + # against the locale doc's own headings (or its rewritten twin). + loc_anchors = extract_anchors(loc_text) + for _, t in extract_links(loc_text): + if t.startswith("#"): + frag = t[1:] + if frag and frag not in loc_anchors: + err("anchor_parity", f"{locale} fragment #{frag} does not resolve to a heading") + elif "#" in t and not t.startswith(("http", "mailto")): + path, _, frag = t.partition("#") + if not frag: + continue + base = path.rsplit("/", 1)[-1] + if base in ROOT_DOCS: + # The twin file's anchors are checked when the twin itself is + # checked; here only the fragment must exist in the twin's file. + twin = locale_file(base, locale) + twin_text = _read_twin(twin) + if twin_text is not None and frag not in extract_anchors(twin_text): + err("anchor_parity", f"{locale} link {t}: #{frag} does not resolve in {twin}") + else: + if frag not in loc_anchors: + err("anchor_parity", f"{locale} link {t}: #{frag} does not resolve locally") + + # 5. Heading-structure parity. + en_lv, loc_lv = heading_levels(en_text), heading_levels(loc_text) + if en_lv != loc_lv: + msg = f"heading level sequence differs: EN={en_lv} {locale}={loc_lv}" + if status == "germinated": + err("heading_parity", msg) + else: + warn("heading_parity", msg + " (legacy debt — reported, not gating)") + + # 6. Hub/back-link parity — a locale README must link back to the English + # source so readers can escape to canonical docs. + if doc == "README.md" and "README.md" not in [t for _, t in extract_links(loc_text)]: + err("backlink_parity", f"{locale} README has no back-link to README.md") + + # 7. Discoverability — a germinated locale must be linked from the + # English README (the badge hub), or readers can never find it. + if doc == "README.md" and status == "germinated": + twin = locale_file("README.md", locale) + if twin not in [t for _, t in extract_links(en_text)]: + err( + "discoverability", + f"germinated locale {locale} is not linked from README.md " + f"(add the language badge for {twin})", + ) + + return issues + + +_twin_cache: dict[str, str | None] = {} + + +def _read_twin(name: str) -> str | None: + if name not in _twin_cache: + p = REPO_ROOT / name + _twin_cache[name] = p.read_text(encoding="utf-8") if p.exists() else None + return _twin_cache[name] + + +def check_all(repo_root: Path | None = None) -> dict: + """Run the full gate over every existing locale file. Returns report.""" + global REPO_ROOT + if repo_root is not None: + REPO_ROOT = repo_root + _twin_cache.clear() + report: dict = {"checks": [], "errors": 0, "warnings": 0} + for doc in ROOT_DOCS: + en_p = REPO_ROOT / doc + if not en_p.exists(): + continue + en_text = en_p.read_text(encoding="utf-8") + for locale, meta in MANIFEST.items(): + if meta["status"] == "pending": + continue + loc_p = REPO_ROOT / locale_file(doc, locale) + if not loc_p.exists(): + report["checks"].append( + { + "doc": doc, + "locale": locale, + "status": meta["status"], + "result": "missing", + "issues": [], + } + ) + if meta["status"] == "germinated": + report["errors"] += 1 + else: + report["warnings"] += 1 + continue + loc_text = loc_p.read_text(encoding="utf-8") + issues = check_doc_parity(en_text, loc_text, doc, locale, meta["status"]) + result = "pass" if not any(i["severity"] == "error" for i in issues) else "fail" + report["checks"].append( + {"doc": doc, "locale": locale, "status": meta["status"], "result": result, "issues": issues} + ) + report["errors"] += sum(1 for i in issues if i["severity"] == "error") + report["warnings"] += sum(1 for i in issues if i["severity"] == "warning") + return report + + +# ───────────────────────────────────────────────────────────────────────────── +# Germination template — the extraction half of the pipeline. Prose becomes +# a placeholder the translator fills; every technical span is preserved. +# ───────────────────────────────────────────────────────────────────────────── + +def germinate_template(en_text: str, locale: str) -> str: + lines = en_text.split("\n") + out: list[str] = [] + in_fence = False + for line in lines: + if FENCE_RE.match(line) or line.startswith(("```", "~~~")): + in_fence = not in_fence + out.append(line) + continue + if in_fence: + out.append(line) # code bodies are never translated + continue + if not line.strip(): + out.append(line) + continue + if HEADING_RE.match(line): + out.append(line) # headings translated by the translator in place + continue + if re.match(r"^\s*(<\|?\s*[-|]|\|?\s*[-|])", line): + out.append(line) # table rows: translator edits cells in place + continue + if line.startswith(("<", " rather than guessing. + +The document follows the template form: lines wrapped in ⟪{locale}:...⟫ are +the prose you translate IN PLACE (replace the marker with the translation). +Lines outside the markers (fences, tables, headings, HTML, links) stay as-is +per the rules above. + +=== DOCUMENT START === +{template} +=== DOCUMENT END === +""" + + +def germinate( + locale: str, + doc: str, + llm_cmd: list[str], + out_dir: Path | None = None, + lang_name: str | None = None, + timeout: int = 600, +) -> tuple[Path, str, list[dict]]: + """Run the LLM, write the translation, and return (path, llm_output, + gate_issues). Callers decide whether the output is acceptable — the + gate verdict is authoritative, not the LLM.""" + src = REPO_ROOT / doc + text = src.read_text(encoding="utf-8") + meta = MANIFEST.get(locale, {}) + lang_name = lang_name or meta.get("name", locale) + prompt = GERMINATE_PROMPT_TEMPLATE.format( + lang_name=lang_name, locale=locale, template=germinate_template(text, locale, doc) + ) + import subprocess + + proc = subprocess.run( + llm_cmd, input=prompt, capture_output=True, text=True, encoding="utf-8", timeout=timeout + ) + if proc.returncode != 0: + raise RuntimeError(f"LLM command failed ({proc.returncode}): {proc.stderr[:500]}") + out = proc.stdout.strip() + + target_dir = out_dir or REPO_ROOT + target = target_dir / locale_file(doc, locale) + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(out + "\n", encoding="utf-8") + + # The gate is authoritative regardless of destination: verify the LLM + # output against the real English source before anyone trusts it. + status = MANIFEST.get(locale, {}).get("status", "germinated") + issues = check_doc_parity(text, out, doc, locale, status) + return target, out, issues + + # ───────────────────────────────────────────────────────────────────────────── # CLI # ───────────────────────────────────────────────────────────────────────────── @@ -536,9 +629,11 @@ def _status_table(repo_root: Path) -> str: def main(argv: list[str] | None = None) -> int: ap = argparse.ArgumentParser(description=__doc__) - ap.add_argument("action", choices=["check", "status", "extract", "template"]) - ap.add_argument("--locale", default=None, help="locale code (extract/template)") - ap.add_argument("--doc", default=None, help="root doc name (extract/template)") + ap.add_argument("action", choices=["check", "status", "extract", "template", "germinate"]) + ap.add_argument("--locale", default=None, help="locale code (extract/template/germinate)") + ap.add_argument("--doc", default=None, help="root doc name (extract/template/germinate)") + ap.add_argument("--llm", default=None, help="LLM command for germinate, e.g. 'hermes chat -Q -q' (reads prompt on stdin)") + ap.add_argument("--out", default=None, help="output dir for germinate (default: repo root)") ap.add_argument("--json", action="store_true", help="JSON output") args = ap.parse_args(argv) @@ -586,7 +681,29 @@ def main(argv: list[str] | None = None) -> int: } print(json.dumps(payload, indent=2, ensure_ascii=False)) else: - print(germinate_template(text, args.locale)) + print(germinate_template(text, args.locale, args.doc)) + return 0 + + if args.action == "germinate": + if not args.locale or not args.doc or not args.llm: + ap.error("--locale, --doc and --llm are required for germinate") + import shlex + + llm_cmd = shlex.split(args.llm) + out_dir = Path(args.out) if args.out else None + try: + target, out_text, issues = germinate( + args.locale, args.doc, llm_cmd, out_dir=out_dir + ) + except RuntimeError as e: + print(f"germinate failed: {e}", file=sys.stderr) + return 1 + print(f"wrote {target} ({len(out_text)} chars)") + if issues: + for i in issues: + print(f" {i['severity']:7} {i['class']}: {i['detail']}") + return 1 if any(i["severity"] == "error" for i in issues) else 0 + print("gate: PASS (no drift)") return 0 return 0 diff --git a/tests/conformance/test_docs_i18n_germination.py b/tests/conformance/test_docs_i18n_germination.py index 2a75ebe66df30..73638c7e05839 100644 --- a/tests/conformance/test_docs_i18n_germination.py +++ b/tests/conformance/test_docs_i18n_germination.py @@ -33,6 +33,7 @@ extract_fences, extract_headings, extract_links, + germinate, locale_file, rewrite_target, slugify, @@ -239,3 +240,100 @@ def test_english_sources_have_no_missing_twin_targets(doc): for _, t in extract_links(text): if t.startswith("#") and t[1:]: assert t[1:] in anchors, f"{doc}: dangling fragment {t}" + + +# ── germination runner (mocked LLM — no network in CI) ────────────────────── + + +def _fake_llm_that_translates_prose(monkeypatch, translation_map): + """The germinate action pipes the prompt to the LLM command and treats + stdout as the translation. This fake verifies the prompt carried the + template, and returns a translation whose prose lines are the marker + content suffixed with the locale (a crude 'translation').""" + import subprocess + + captured = {} + + class FakeProc: + returncode = 0 + stdout = "" + stderr = "" + + def fake_run(cmd, input, capture_output, text, encoding, timeout): + captured["cmd"] = cmd + captured["prompt"] = input + # Simulate an LLM that returns ONLY the translated document (it + # drops the instruction preamble and the end marker) and keeps + # everything but prose markers, which it 'translates' by suffixing + # the locale code. + doc_lines = input.split("=== DOCUMENT START ===")[1].split( + "=== DOCUMENT END ===" + )[0].splitlines() + out_lines = [] + for line in doc_lines: + if line.startswith("⟪"): + # ⟪fr:Some prose⟫ -> "Some prose [fr]" + inner = line.split(":", 1)[1].rsplit("⟫", 1)[0] + out_lines.append(f"{inner} [{translation_map.get(inner, 'fr')}]") + else: + out_lines.append(line) + FakeProc.stdout = "\n".join(out_lines) + return FakeProc + + monkeypatch.setattr(subprocess, "run", fake_run) + return captured + + +def test_germinate_writes_file_and_runs_gate(monkeypatch, tmp_path): + """germinate() writes the LLM output to the locale file and returns the + gate verdict. A translation that keeps every technical edge passes; the + prompt must carry the template with prose placeholders.""" + import subprocess + + captured = _fake_llm_that_translates_prose(monkeypatch, {}) + src = _read("README.md") + target, out, issues = germinate("fr", "README.md", ["fake-llm"], out_dir=tmp_path) + assert target == tmp_path / "README.fr.md" + assert target.exists() + # The prompt carried the template (markers present). + assert "⟪fr:" in captured["prompt"] + # Prose markers got replaced, technical edges preserved. + assert "⟪fr:" not in out + assert "hermes model" in out # code span survived the LLM + # The gate ran on the output: prose-only translation keeps every edge. + errors = [i for i in issues if i["severity"] == "error"] + assert not errors, f"gate errors: {errors}" + + +def test_germinate_rejects_llm_that_drops_code_spans(monkeypatch, tmp_path): + """A translator that drops a backtick identifier fails the gate — the + action reports the drift instead of shipping it.""" + + def evil_run(cmd, input, capture_output, text, encoding, timeout): + class P: + returncode = 0 + stdout = input.replace("`hermes model`", "the model command") + stderr = "" + + return P() + + import subprocess + + monkeypatch.setattr(subprocess, "run", evil_run) + _, out, issues = germinate("fr", "README.md", ["evil-llm"], out_dir=tmp_path) + assert "the model command" in out + classes = {i["class"] for i in issues} + assert "code_span_parity" in classes + + +def test_germinate_fails_loudly_on_llm_error(monkeypatch, tmp_path): + import subprocess + + class FailProc: + returncode = 1 + stdout = "" + stderr = "boom" + + monkeypatch.setattr(subprocess, "run", lambda *a, **k: FailProc()) + with pytest.raises(RuntimeError, match="LLM command failed"): + germinate("fr", "README.md", ["broken-llm"], out_dir=tmp_path) From c7089d4ba6cf5df8e6783e4db308ca9022c05021 Mon Sep 17 00:00:00 2001 From: "Andrex Ibiza, MBA" <84248988+andrexibiza@users.noreply.github.com> Date: Thu, 6 Aug 2026 09:15:47 -0500 Subject: [PATCH 06/14] docs(i18n): document the germinate action in the germination spec Signed-off-by: Andrex Ibiza, MBA <84248988+andrexibiza@users.noreply.github.com> --- docs/developer-guide/docs-i18n-germination-spec.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/developer-guide/docs-i18n-germination-spec.md b/docs/developer-guide/docs-i18n-germination-spec.md index 16e0bc6410332..eec593995a324 100644 --- a/docs/developer-guide/docs-i18n-germination-spec.md +++ b/docs/developer-guide/docs-i18n-germination-spec.md @@ -52,7 +52,12 @@ languages** (Ethnologue 26th-edition order) plus the in-flight PRs for each 2. `python scripts/docs_germination.py template --doc README.md --locale ` — prose-placeholder template; translate the prose, keep every technical span and code block verbatim. -3. Assemble the file (replace placeholders, fix headings in place). +3. **Automatic path:** `python scripts/docs_germination.py germinate --locale + --doc README.md --llm "hermes chat -Q -q"` — renders the template, + pipes it to the LLM command (stdin → stdout), writes the locale file, and + runs the parity gate on the output. **The gate is the arbiter**: a + translation that drops a technical edge fails and is not shipped. (Manual + path: fill the template by hand, then assemble.) 4. `python scripts/docs_germination.py check` — iterate until the locale passes every class. **Do not ship a locale that fails the gate.** 5. Add the language badge to `README.md` (the gate enforces this). From 41a78cd3a8353686b7a38a99ed63fb9ed921e7c4 Mon Sep 17 00:00:00 2001 From: "Andrex Ibiza, MBA" <84248988+andrexibiza@users.noreply.github.com> Date: Thu, 6 Aug 2026 09:43:56 -0500 Subject: [PATCH 07/14] docs(i18n): French semantic-quality pass (blind witness) + fence-comment localization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Blind semantic review of the French translations (28 findings, zero meaning-changing technical errors) — all applied: - False friend: "éventuellement" -> "en option" (MemoryProvider post_setup). - Calques removed: harnais -> infrastructure de l'agent; plomberie -> infrastructure de configuration; exercez -> testez; amorçage phrasing; "un amont" -> "un service en amont"; "n'opère pas" -> "n'a pas de programme"; sandboxe -> n'isole que le shell. - Meaning shift: "exfiltration opportuniste" -> "exfiltration accidentelle" (casual != opportunistic). - Anglicisms: managé -> géré; template de PR -> gabarit de PR; gating -> verrouillage; Assets TTS -> Ressources TTS; sites d'appel -> points d'appel; "official" -> « officielle ». - Terminology standardized: allowlist -> « liste d'autorisation » everywhere (3 renderings collapsed to 1); "core" -> « cœur ». - Residual English: reload/start-chatting comments, skill-example comments, branch-type table, version-window comments, tree-structure comments, project-tree comments — all translated. - Recurring calque: "résoudre des approbations" -> « statuer sur les approbations » (3 sites). Gate refinement enabling this: fence comments are now LOCALIZABLE prose — comment-normalized body hashes (commands must match exactly, comments may be translated, comment code spans like `env -i` still required). This is the principled line: code never translated, comments are docs. chore: map contributor email iacker (credit ledger for #63660 seed). Signed-off-by: Andrex Ibiza, MBA <84248988+andrexibiza@users.noreply.github.com> --- CONTRIBUTING.fr.md | 84 ++++++++--------- README.fr.md | 24 ++--- SECURITY.fr.md | 24 ++--- .../emails/iacker@users.noreply.github.com | 1 + scripts/docs_germination.py | 94 +++++++++++++++++-- .../conformance/test_docs_i18n_germination.py | 28 +++++- 6 files changed, 180 insertions(+), 75 deletions(-) create mode 100644 contributors/emails/iacker@users.noreply.github.com diff --git a/CONTRIBUTING.fr.md b/CONTRIBUTING.fr.md index c34becdf93b8d..e62c6e98e3f65 100644 --- a/CONTRIBUTING.fr.md +++ b/CONTRIBUTING.fr.md @@ -22,7 +22,7 @@ Nous valorisons les contributions dans cet ordre : Une recherche rapide avant de vous lancer vous fait gagner du temps et garde la file des PR propre — les doublons sont fréquents ici, alors une minute en amont vaut le coup. -- **Cherchez dans les PR et issues ouvertes *et* fusionnées** votre sujet ou le symptôme de votre erreur — la vérification de doublons du template de PR n'intervient qu'au moment de la revue, une fois le travail déjà fait : +- **Cherchez dans les PR et issues ouvertes *et* fusionnées** votre sujet ou le symptôme de votre erreur — la vérification de doublons du gabarit de PR n'intervient qu'au moment de la revue, une fois le travail déjà fait : ```bash gh search issues --repo NousResearch/hermes-agent "" gh search prs --repo NousResearch/hermes-agent --state all "" @@ -49,7 +49,7 @@ C'est la question la plus fréquente chez les nouveaux contributeurs. La répons ### Faites-en un outil quand : -- Il exige une intégration de bout en bout avec clés API, flux d'authentification ou configuration multi-composants gérée par le harnais de l'agent +- Il exige une intégration de bout en bout avec clés API, flux d'authentification ou configuration multi-composants gérée par l'infrastructure de l'agent - Il nécessite une logique de traitement sur mesure qui doit s'exécuter avec précision à chaque fois (pas du « au mieux » issu de l'interprétation du LLM) - Il manipule des données binaires, du streaming ou des événements temps réel qui ne peuvent pas passer par le terminal - Exemples : automatisation de navigateur (gestion de sessions Browserbase), TTS (encodage audio + livraison sur la plateforme), analyse de vision (manipulation d'images en base64) @@ -61,7 +61,7 @@ Les compétences embarquées (dans `skills/`) sont livrées avec chaque installa - Traitement de documents, recherche web, workflows de développement courants, administration système - Utilisées régulièrement par un large éventail de personnes -Si votre compétence est officielle et utile mais pas universellement nécessaire (par exemple l'intégration d'un service payant, une dépendance lourde), placez-la dans **`optional-skills/`** — elle est livrée avec le dépôt mais n'est pas activée par défaut. Les utilisateurs peuvent la découvrir via `hermes skills browse` (étiquetée « official ») et l'installer avec `hermes skills install` (sans avertissement tiers, confiance intégrée). +Si votre compétence est officielle et utile mais pas universellement nécessaire (par exemple l'intégration d'un service payant, une dépendance lourde), placez-la dans **`optional-skills/`** — elle est livrée avec le dépôt mais n'est pas activée par défaut. Les utilisateurs peuvent la découvrir via `hermes skills browse` (étiquetée « officielle ») et l'installer avec `hermes skills install` (sans avertissement tiers, confiance intégrée). Si votre compétence est spécialisée, issue de la communauté ou de niche, elle a plus sa place sur un **Skills Hub** — téléversez-la sur un registre de compétences et partagez-la sur le [Discord de Nous Research](https://discord.gg/NousResearch). Les utilisateurs peuvent l'installer avec `hermes skills install`. @@ -73,11 +73,11 @@ Si votre compétence est spécialisée, issue de la communauté ou de niche, ell Les plugins de mémoire autonomes : -- Implémentent la même ABC `MemoryProvider` (`agent/memory_provider.py`) — `sync_turn`, `prefetch`, `shutdown`, et éventuellement `post_setup(hermes_home, config)` pour l'intégration avec l'assistant de configuration +- Implémentent la même ABC `MemoryProvider` (`agent/memory_provider.py`) — `sync_turn`, `prefetch`, `shutdown`, et, en option, `post_setup(hermes_home, config)` pour l'intégration avec l'assistant de configuration - Utilisent le même système de découverte — `discover_memory_providers()` les récupère dans les répertoires de plugins utilisateur/projet et les entry points pip - S'intègrent à `hermes memory setup` via `post_setup()` — sans toucher au code du cœur - Peuvent enregistrer leurs propres sous-commandes CLI via `register_cli(subparser)` dans un fichier `cli.py` -- Bénéficient des mêmes hooks de cycle de vie et de la même plomberie de configuration que les fournisseurs intégrés +- Bénéficient des mêmes hooks de cycle de vie et de la même infrastructure de configuration que les fournisseurs intégrés Les PR qui ajoutent un nouveau répertoire sous `plugins/memory/` seront fermées avec un renvoi vers la publication du fournisseur dans son propre dépôt. Les fournisseurs déjà intégrés restent ; les corrections de bugs les concernant sont les bienvenues. @@ -115,7 +115,7 @@ Un plugin de produit tiers bien construit peut passer la revue automatisée et ### Installation avec l'installeur standard -Pour la plupart des contributeurs, le meilleur amorçage de développement est le même +Pour la plupart des contributeurs, la meilleure façon d'amorcer le développement est la même chemin que celui des utilisateurs : lancer l'installeur standard, puis travailler dans le dépôt qu'il a cloné. L'installeur crée le venv d'Hermes, câble la commande `hermes`, enregistre la méthode d'installation pour `hermes update` et clone le @@ -128,10 +128,10 @@ paresseux de dépendances, le gateway et la documentation. curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash cd "${HERMES_HOME:-$HOME/.hermes}/hermes-agent" -# Add dev/test extras on top of the standard install. +# Ajoutez les extras dev/test par-dessus l'installation standard. uv pip install -e ".[all,dev]" -# Optional: browser tools / docs site dependencies. +# Optionnel : outils navigateur / dépendances du site de docs. npm install ``` @@ -161,15 +161,15 @@ l'espace de travail ne le résout. git clone https://github.com/NousResearch/hermes-agent.git cd hermes-agent -# Create venv with Python 3.11, OUTSIDE the source tree +# Créez le venv avec Python 3.11, HORS de l'arborescence source uv venv ~/.hermes/venvs/hermes-dev --python 3.11 export VIRTUAL_ENV="$HOME/.hermes/venvs/hermes-dev" export PATH="$VIRTUAL_ENV/bin:$PATH" -# Install with all extras (messaging, cron, CLI menus, dev tools) +# Installez avec tous les extras (messagerie, cron, menus CLI, outils de dev) uv pip install -e ".[all,dev]" -# Optional: browser tools +# Optionnel : outils navigateur npm install ``` @@ -180,14 +180,14 @@ mkdir -p ~/.hermes/{cron,sessions,logs,memories,skills} cp cli-config.yaml.example ~/.hermes/config.yaml touch ~/.hermes/.env -# Add at minimum an LLM provider key: +# Ajoutez au minimum une clé de fournisseur LLM : echo "OPENROUTER_API_KEY=***" >> ~/.hermes/.env ``` ### Lancer ```bash -# The standard installer already put `hermes` on PATH. +# L'installeur standard a déjà placé `hermes` sur le PATH. hermes doctor hermes chat -q "Hello" ``` @@ -203,12 +203,12 @@ ln -sf "$(pwd)/venv/bin/hermes" ~/.local/bin/hermes ### Lancer les tests ```bash -# Preferred — matches CI (hermetic `env -i`, per-file subprocess isolation -# via run_tests_parallel.py, worker count auto-scaled); see AGENTS.md +# Préféré — correspond à la CI (hermétique `env -i`, isolation par sous-processus +# via run_tests_parallel.py, nombre de workers auto-ajusté) ; voir AGENTS.md scripts/run_tests.sh -# Alternative (activate the venv first). The wrapper is still recommended -# for parity with GitHub Actions before you open a PR: +# Alternative (activez d'abord le venv). Le wrapper reste recommandé +# pour la parité avec GitHub Actions avant d'ouvrir une PR : pytest tests/ -v ``` @@ -413,8 +413,8 @@ Les compétences embarquées vivent dans `skills/`, organisées par catégorie. skills/ ├── research/ │ └── arxiv/ -│ ├── SKILL.md # Required: main instructions -│ └── scripts/ # Optional: helper scripts +│ ├── SKILL.md # Requis : instructions principales +│ └── scripts/ # Facultatif : scripts d'aide │ └── search_arxiv.py ├── productivity/ │ └── ocr-and-documents/ @@ -433,10 +433,10 @@ description: Brief description (shown in skill search results) version: 1.0.0 author: Your Name license: MIT -platforms: [macos, linux] # Optional — restrict to specific OS platforms - # Valid: macos, linux, windows - # Omit to load on all platforms (default) -required_environment_variables: # Optional — secure setup-on-load metadata +platforms: [macos, linux] # Facultatif — restreindre à des plateformes OS précises + # Valide : macos, linux, windows + # Omettre pour charger sur toutes les plateformes (défaut) +required_environment_variables: # Facultatif — métadonnées de configuration sécurisée au chargement - name: MY_API_KEY prompt: API key help: Where to get it @@ -448,8 +448,8 @@ metadata: hermes: tags: [Category, Subcategory, Keywords] related_skills: [other-skill-name] - fallback_for_toolsets: [web] # Optional — show only when toolset is unavailable - requires_toolsets: [terminal] # Optional — show only when toolset is available + fallback_for_toolsets: [web] # Facultatif — afficher seulement si le toolset est indisponible + requires_toolsets: [terminal] # Facultatif — afficher seulement si le toolset est disponible --- # Skill Title @@ -514,17 +514,17 @@ metadata: **Exemples :** ```yaml -# DuckDuckGo search — shown when Firecrawl (web toolset) is unavailable +# DuckDuckGo search — affichée quand Firecrawl (toolset web) est indisponible metadata: hermes: fallback_for_toolsets: [web] -# Smart home skill — only useful when terminal is available +# Smart home skill — utile uniquement quand le terminal est disponible metadata: hermes: requires_toolsets: [terminal] -# Local browser fallback — shown when Browserbase is unavailable +# Local browser fallback — affichée quand Browserbase est indisponible metadata: hermes: fallback_for_toolsets: [browser] @@ -597,7 +597,7 @@ Toute compétence nouvelle ou modernisée — embarquée, optionnelle ou contrib Si la compétence dépend d'un serveur MCP, nommez-le et documentez son installation dans `## Prerequisites`. Les CLI tierces (par exemple `ffmpeg`, `gh`, un SDK particulier) peuvent être invoquées depuis les fichiers de script, mais la prose doit présenter l'interaction comme « invoquer via l'outil `terminal` », pas comme une session shell manuelle. -3. **Le gating `platforms:` doit être audité contre les imports réels des scripts.** Les compétences qui utilisent des primitives exclusivement POSIX (`fcntl`, `termios`, `os.setsid`, `os.kill(pid, 0)` pour tester la vivacité, `/proc`, chemins `/tmp` codés en dur, `signal.SIGKILL`, heredocs bash, `osascript`, `apt`, `systemctl`) doivent déclarer leurs plateformes prises en charge via le frontmatter `platforms:`. La posture par défaut est de d'abord rendre le code multiplateforme — `tempfile.gettempdir()`, `pathlib.Path`, `psutil.pid_exists()`, filtrage au niveau Python plutôt que `grep`. Ne restreignez à un ensemble plus étroit que si la dépendance est réellement liée à une plateforme (par exemple `osascript` est propre à macOS, `/proc` à Linux). +3. **Le verrouillage par `platforms:` doit être audité contre les imports réels des scripts.** Les compétences qui utilisent des primitives exclusivement POSIX (`fcntl`, `termios`, `os.setsid`, `os.kill(pid, 0)` pour tester la vivacité, `/proc`, chemins `/tmp` codés en dur, `signal.SIGKILL`, heredocs bash, `osascript`, `apt`, `systemctl`) doivent déclarer leurs plateformes prises en charge via le frontmatter `platforms:`. La posture par défaut est de d'abord rendre le code multiplateforme — `tempfile.gettempdir()`, `pathlib.Path`, `psutil.pid_exists()`, filtrage au niveau Python plutôt que `grep`. Ne restreignez à un ensemble plus étroit que si la dépendance est réellement liée à une plateforme (par exemple `osascript` est propre à macOS, `/proc` à Linux). 4. **`author` crédite d'abord le contributeur humain.** Pour les contributions externes, le vrai nom du contributeur + son pseudo GitHub viennent en premier (`Jane Doe (jane-doe)`) ; « Hermes Agent » est le collaborateur secondaire. Si le commit du contributeur affiche « Hermes Agent » comme auteur parce qu'il a utilisé Hermes pour rédiger la compétence, remplacez-le par son nom réel — on crédite l'humain, pas l'outil. @@ -700,12 +700,12 @@ emprunter votre chemin de code. processus sans rapport partageant sa console ». Voir [bpo-14484](https://bugs.python.org/issue14484) (ouvert depuis 2012 — ne sera jamais corrigé pour raisons de compatibilité). - **À privilégier :** utilisez `psutil` (une dépendance de base — toujours disponible) : + **À privilégier :** utilisez `psutil` (une dépendance du cœur — toujours disponible) : ```python import psutil if psutil.pid_exists(pid): - # process is alive — safe on every platform + # processus vivant — sûr sur toutes les plateformes ... ``` @@ -716,7 +716,7 @@ emprunter votre chemin de code. `OpenProcess + WaitForSingleObject` sous Windows, uniquement si psutil est introuvable. - Grep d'audit pour les nouveaux sites d'appel : `rg "os\.kill\([^,]+,\s*0\s*\)"`. + Pour l'audit, cherchez avec grep les nouveaux points d'appel : `rg "os\.kill\([^,]+,\s*0\s*\)"`. Tout résultat hors code de test est présumé être un bug de kill silencieux sous Windows. @@ -740,7 +740,7 @@ emprunter votre chemin de code. menu = TerminalMenu(options) idx = menu.show() except (ImportError, NotImplementedError): - # Fallback: numbered menu for Windows + # Repli : menu numéroté pour Windows for i, opt in enumerate(options): print(f" {i+1}. {opt}") idx = int(input("Choice: ")) - 1 @@ -776,7 +776,7 @@ emprunter votre chemin de code. import psutil try: parent = psutil.Process(pid) - # Kill children first (leaf-up), then the parent. + # Tuez d'abord les enfants (des feuilles vers la racine), puis le parent. for child in parent.children(recursive=True): child.kill() parent.kill() @@ -925,18 +925,18 @@ Après la [compromission de la chaîne d'approvisionnement de litellm](https://g "openai>=2.21.0,<3" "pydantic>=2.12.5,<3" -# ✅ Correct — pre-1.0 (tight minor window) +# ✅ Correct — pre-1.0 (fenêtre de mineure serrée) "asyncpg>=0.29,<0.32" "aiosqlite>=0.20,<0.23" "hindsight-client>=0.4.22,<0.5" -# ❌ Rejected — no upper bound +# ❌ Rejeté — pas de borne supérieure "some-package>=1.2.3" -# ❌ Rejected — too tight (blocks legitimate patches) +# ❌ Rejeté — trop serré (bloque les correctifs légitimes) "some-package==1.2.3" -# ❌ Rejected — too loose for pre-1.0 (allows 80 minor versions) +# ❌ Rejeté — trop lâche pour pre-1.0 (autorise 80 versions mineures) "some-package>=0.20,<1" ``` @@ -949,17 +949,17 @@ Après la [compromission de la chaîne d'approvisionnement de litellm](https://g ### Nommage des branches ``` -fix/description # Bug fixes -feat/description # New features +fix/description # Corrections de bugs +feat/description # Nouvelles fonctionnalités docs/description # Documentation test/description # Tests -refactor/description # Code restructuring +refactor/description # Restructuration du code ``` ### Avant de soumettre 1. **Lancez les tests** : `scripts/run_tests.sh` (recommandé ; identique à la CI) ou `pytest tests/ -v` avec le venv du projet activé -2. **Testez manuellement** : lancez `hermes` et exercez le chemin de code que vous avez modifié +2. **Testez manuellement** : lancez `hermes` et testez le chemin de code que vous avez modifié 3. **Vérifiez l'impact multiplateforme** : si vous touchez aux E/S de fichiers, à la gestion des processus ou au terminal, pensez à macOS, Linux et WSL2 4. **Gardez les PR ciblées** : un seul changement logique par PR. Ne mélangez pas une correction de bug avec un refactoring et une nouvelle fonctionnalité. diff --git a/README.fr.md b/README.fr.md index b93793a0ee118..b38fdd812fee3 100644 --- a/README.fr.md +++ b/README.fr.md @@ -62,8 +62,8 @@ Si Git est déjà installé, l'installateur le détecte et l'utilise à la place Après l'installation : ```bash -source ~/.bashrc # reload shell (or: source ~/.zshrc) -hermes # start chatting! +source ~/.bashrc # rechargez le shell (ou : source ~/.zshrc) +hermes # lancez la discussion ! ``` ### Dépannage @@ -75,13 +75,13 @@ Si votre antivirus (Bitdefender, Windows Defender, etc.) met en quarantaine `uv. **Pour vérifier que votre copie est authentique :** ```powershell -# Install GitHub CLI if needed +# Installez GitHub CLI si nécessaire winget install --id GitHub.cli -# Login to GitHub +# Connectez-vous à GitHub gh auth login -# Run verification +# Lancez la vérification $uv = "$env:LOCALAPPDATA\hermes\bin\uv.exe" $ver = (& $uv --version).Split(' ')[1] [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 @@ -94,10 +94,10 @@ Expand-Archive $zip "$env:TEMP\uv_x" -Force Si l'attestation affiche « Verification succeeded » et que la dernière ligne renvoie `True`, tout est en ordre. -**Pour mettre Hermes en liste blanche :** +**Pour mettre Hermes sur la liste d'autorisation :** - **Windows Defender :** lancez PowerShell en administrateur → `Add-MpPreference -ExclusionPath "$env:LOCALAPPDATA\hermes\bin"` - **Bitdefender :** ajoutez une exception dans la console Bitdefender (Protection > Antivirus > Paramètres > Gérer les exceptions) -- Mettez le **dossier** en liste blanche, pas le hash du fichier — Hermes met `uv` à jour et le hash change à chaque version +- Ajoutez le **dossier** à la liste d'autorisation, pas le hash du fichier — Hermes met `uv` à jour et le hash change à chaque version Pour plus de contexte, consultez les rapports upstream chez Astral : [astral-sh/uv#13553](https://github.com/astral-sh/uv/issues/13553), [astral-sh/uv#15011](https://github.com/astral-sh/uv/issues/15011), [astral-sh/uv#10079](https://github.com/astral-sh/uv/issues/10079). @@ -205,10 +205,10 @@ Ce qui est importé : - **SOUL.md** — fichier de persona - **Mémoires** — entrées de MEMORY.md et USER.md - **Compétences** — compétences créées par l'utilisateur → `~/.hermes/skills/openclaw-imports/` -- **Liste blanche de commandes** — motifs d'approbation +- **Liste d'autorisation de commandes** — motifs d'approbation - **Réglages de messagerie** — configuration des plateformes, utilisateurs autorisés, répertoire de travail -- **Clés API** — secrets en liste blanche (Telegram, OpenRouter, OpenAI, Anthropic, ElevenLabs) -- **Assets TTS** — fichiers audio de l'espace de travail +- **Clés API** — secrets sur liste d'autorisation (Telegram, OpenRouter, OpenAI, Anthropic, ElevenLabs) +- **Ressources TTS** — fichiers audio de l'espace de travail - **Instructions d'espace de travail** — AGENTS.md (avec `--workspace-target`) Voir `hermes claw migrate --help` pour toutes les options, ou utilisez la compétence `openclaw-migration` pour une migration interactive guidée par l'agent, avec prévisualisation en dry-run. @@ -222,7 +222,7 @@ Les contributions sont les bienvenues ! Consultez le [Guide de contribution](CON Démarrage rapide pour les contributeurs — utilisez l'installateur standard, puis travaillez depuis le checkout git complet qu'il crée dans `$HERMES_HOME/hermes-agent` (généralement `~/.hermes/hermes-agent`). C'est la disposition attendue par -`hermes update`, le venv managé, les dépendances chargées à la demande, le gateway +`hermes update`, le venv géré, les dépendances chargées à la demande, le gateway et l'outillage de la documentation. ```bash @@ -233,7 +233,7 @@ scripts/run_tests.sh ``` Solution de repli avec clone manuel (pour les clones jetables ou la CI, quand vous -ne voulez volontairement pas de la disposition d'installation managée) : +ne voulez volontairement pas de la disposition d'installation gérée) : Créez le venv en dehors de l'arborescence clonée — un venv placé dans le répertoire depuis lequel l'agent opère peut être effacé par une commande en chemin relatif que diff --git a/SECURITY.fr.md b/SECURITY.fr.md index d585d676b34dc..8ac10363858f2 100644 --- a/SECURITY.fr.md +++ b/SECURITY.fr.md @@ -8,7 +8,7 @@ et définit le périmètre des signalements de vulnérabilités. Signalez de manière privée via les [GitHub Security Advisories](https://github.com/NousResearch/hermes-agent/security/advisories/new) ou **security@nousresearch.com**. N'ouvrez pas d'issues publiques pour -des vulnérabilités de sécurité. **Hermes Agent n'opère pas de programme +des vulnérabilités de sécurité. **Hermes Agent n'a pas de programme de bug bounty.** Un signalement utile comprend : @@ -113,7 +113,7 @@ Hermes Agent le permet de deux façons : déclarative couvrant le système de fichiers, le réseau (egress L7), les processus/syscalls et le routage d'inférence. Les politiques réseau et d'inférence sont rechargeables à chaud. Les identifiants - sont injectés depuis un magasin Provider et ne touchent jamais le + sont injectés depuis le store Provider et ne touchent jamais le système de fichiers de la sandbox. Sous une encapsulation du processus complet, les heuristiques @@ -140,7 +140,7 @@ et les tokens du gateway sont retirés par défaut ; les variables explicitement déclarées par l'opérateur ou par une skill chargée sont transmises. -Cela réduit l'exfiltration opportuniste. Ce n'est pas du confinement. +Cela réduit l'exfiltration accidentelle. Ce n'est pas du confinement. Tout composant s'exécutant dans le processus de l'agent (skills, plugins, gestionnaires de hooks) peut lire tout ce que l'agent lui-même peut lire, y compris les identifiants en mémoire. La parade contre un @@ -187,7 +187,7 @@ installe sont dans le périmètre au titre du §3.1. Une **surface externe** est tout canal extérieur au processus local de l'agent par lequel un appelant peut déclencher du travail de l'agent, -résoudre des approbations ou recevoir la sortie de l'agent. Chaque +statuer sur les approbations ou recevoir la sortie de l'agent. Chaque surface a son propre modèle d'autorisation, mais les règles ci-dessous s'appliquent uniformément. @@ -208,7 +208,7 @@ s'appliquent uniformément. 1. **Une autorisation est requise à chaque surface qui franchit une frontière de confiance.** Pour les surfaces de messagerie et HTTP réseau, la frontière est le réseau : l'autorisation prend la forme - d'une liste d'appelants autorisés configurée par l'opérateur. Pour + d'une liste d'autorisation des appelants configurée par l'opérateur. Pour les surfaces éditeur et IPC local (ACP, gateway TUI), la frontière est le compte utilisateur de l'hôte : l'autorisation consiste à s'appuyer sur le contrôle d'accès de l'OS (permissions de fichiers, @@ -217,7 +217,7 @@ s'appliquent uniformément. explicite. 2. **Une liste d'autorisation est requise pour chaque adaptateur exposé au réseau qui est activé.** Les adaptateurs doivent refuser de - déclencher du travail de l'agent, de résoudre des approbations ou de + déclencher du travail de l'agent, de statuer sur des approbations ou de relayer des sorties tant qu'aucune liste d'autorisation n'est définie. Les chemins de code qui laissent passer par défaut lorsqu'aucune liste n'est configurée sont des bugs de code dans le @@ -251,13 +251,13 @@ s'appliquent uniformément. - L'accès non autorisé à une surface externe : un appelant hors de l'ensemble d'autorisation configuré (liste d'autorisation, ou équivalent au niveau de l'OS pour les surfaces IPC locales) qui - déclenche du travail, reçoit des sorties ou résout des approbations + déclenche du travail, reçoit des sorties ou statue sur des approbations (§2.6). - L'exfiltration d'identifiants : fuite d'identifiants de l'opérateur ou de matériel d'autorisation de session vers une destination hors de l'enveloppe de confiance, via un mécanisme qui aurait dû l'empêcher (bug de nettoyage de l'environnement, journalisation d'un adaptateur, - erreur de transport qui déverse des identifiants vers un amont, + erreur de transport qui déverse des identifiants vers un service en amont, etc.). - Les violations de la documentation du modèle de confiance : du code qui se comporte contrairement à ce que cette politique, la propre @@ -291,7 +291,7 @@ sécurité. inhabituelle — via du contenu injecté, une hallucination, des artefacts d'entraînement ou toute autre cause — n'est pas en soi une vulnérabilité. « J'ai réussi une injection de prompt » sans - enchaînement vers un résultat du §3.1 n'est pas un signalement + aboutir à un résultat du §3.1 n'est pas un signalement exploitable au sens de cette politique. - **Les conséquences d'une posture d'isolation choisie.** Les signalements indiquant qu'un chemin de code opérant dans le périmètre @@ -299,7 +299,7 @@ sécurité. vulnérabilités. Exemples : des outils shell ou fichiers atteignant l'état de l'hôte sous le backend local ; des sous-processus d'exécution de code ou MCP atteignant l'état de l'hôte sous une - isolation par backend de terminal qui ne sandboxe que le shell ; des + isolation par backend de terminal qui n'isole que le shell ; des signalements dont les préconditions exigent un accès en écriture préexistant à des fichiers de configuration ou d'identifiants appartenant à l'opérateur (ceux-ci sont déjà à l'intérieur de @@ -340,12 +340,12 @@ l'agent va ingérer. Au-delà de cela : - Conservez les identifiants dans le fichier d'identifiants de l'opérateur avec des permissions strictes, jamais dans la configuration principale, jamais sous contrôle de version. Sous - OpenShell, utilisez le magasin Provider plutôt qu'un fichier + OpenShell, utilisez le store Provider plutôt qu'un fichier d'identifiants sur disque. - N'exposez pas le gateway ou l'API à l'internet public sans VPN, Tailscale ou protection par pare-feu. Sous OpenShell, utilisez la couche de politique réseau pour restreindre l'egress. -- Configurez une liste d'appelants autorisés pour chaque adaptateur +- Configurez une liste d'autorisation des appelants pour chaque adaptateur exposé au réseau que vous activez (§2.6). - Passez en revue les skills et plugins tiers avant installation (§2.4, §2.5). Pour les skills, cela signifie lire le Python et les diff --git a/contributors/emails/iacker@users.noreply.github.com b/contributors/emails/iacker@users.noreply.github.com new file mode 100644 index 0000000000000..9570b5ec9bfc7 --- /dev/null +++ b/contributors/emails/iacker@users.noreply.github.com @@ -0,0 +1 @@ +iacker diff --git a/scripts/docs_germination.py b/scripts/docs_germination.py index bb5ebda564edc..671359aced4e8 100644 --- a/scripts/docs_germination.py +++ b/scripts/docs_germination.py @@ -200,6 +200,7 @@ def _scan(text: str) -> tuple[str, list[dict]]: { "marker": marker, "lang": lang, + "body": "\n".join(body), "sha256": hashlib.sha256("\n".join(body).encode("utf-8")).hexdigest()[:16], } ) @@ -213,15 +214,90 @@ def _scan(text: str) -> tuple[str, list[dict]]: { "marker": marker, "lang": lang, + "body": "\n".join(body), "sha256": hashlib.sha256("\n".join(body).encode("utf-8")).hexdigest()[:16], } ) return "\n".join(masked), fences +# ───────────────────────────────────────────────────────────────────────────── +# Fence comment localization. Code fences stay byte-identical EXCEPT comment +# lines: a line-leading comment or a trailing ' # ...' comment is localizable +# prose (translators render it in the target language) while every non- +# comment byte of the body must match exactly. Code tokens inside comments +# (backtick spans) still must survive — see _fence_comment_spans. +# ───────────────────────────────────────────────────────────────────────────── + +_TRAILING_COMMENT_RE = re.compile(r"(?<=\s)#(?=[ \t]|$)") + + +def _comment_segments(line: str) -> list[tuple[int, int]]: + """(start, end) spans of comment text on a code line: a line-leading + comment (unless shebang) or a trailing ' # ...' comment. A '#' inside a + quoted string (e.g. '"# hi"') is not preceded by whitespace and is left + alone.""" + s = line.lstrip() + lead = len(line) - len(s) + if s.startswith("#") and not s.startswith("#!"): + return [(lead, len(line))] + segs = [] + for m in _TRAILING_COMMENT_RE.finditer(line): + if m.start() >= lead: + segs.append((m.start(), len(line))) + break # only the first trailing comment per line + return segs + + +def _comment_normalized(body: str) -> str: + """Body with every comment span blanked and trailing padding stripped + (positions preserved per line, padding length irrelevant). Two fences + whose non-comment bytes match compare equal.""" + out = [] + for line in body.split("\n"): + segs = _comment_segments(line) + if not segs: + out.append(line.rstrip()) + continue + chars = list(line) + for a, b in segs: + chars[a:b] = " " * (b - a) + out.append("".join(chars).rstrip()) + return "\n".join(out) + + +def _fence_comment_spans(text: str) -> list[str]: + """Backtick spans inside fence comment lines — technical identifiers in + comments must survive translation too (e.g. `env -i` in a CI comment).""" + spans: list[str] = [] + for f in _scan(text)[1]: + seg_texts = [] + for line in (f.get("body") or "").split("\n"): + for a, b in _comment_segments(line): + seg_texts.append(line[a:b]) + joined = "\n".join(seg_texts) + positions = [m.start() for m in re.finditer(r"`", joined)] + for i in range(0, len(positions) - 1, 2): + span = joined[positions[i] + 1 : positions[i + 1]] + if "\n" not in span: + spans.append(span) + return spans + + def extract_fences(text: str) -> list[dict]: - """Sequence of fenced code blocks: marker, language, body hash.""" - _, fences = _scan(text) + """Sequence of fenced code blocks: marker, language, body hash. + + Body hashes compare COMMENT-NORMALIZED bodies: comment lines (and + trailing comments) are localizable prose; every other byte of the body + must match exactly. Comment backtick spans are required separately via + _fence_comment_spans (code_span_parity).""" + fences = [] + for f in _scan(text)[1]: + f = dict(f) + f["body_sha256"] = hashlib.sha256( + _comment_normalized(f["body"]).encode("utf-8") + ).hexdigest()[:16] + fences.append(f) return fences @@ -327,11 +403,13 @@ def err(cls: str, detail: str) -> None: def warn(cls: str, detail: str) -> None: issues.append({"class": cls, "severity": "warning", "detail": detail}) - # 1. Fence parity — identical code fence sequence, byte-identical bodies - # (marker + language + body hash). Code is never translated. + # 1. Fence parity — identical code fence sequence (marker + language), + # with COMMENT-NORMALIZED body hashes: command bytes must match + # exactly; comment lines/trailing comments are localizable prose. + # Comment code spans are required via code_span_parity below. en_f, loc_f = extract_fences(en_text), extract_fences(loc_text) - en_sig = [(f["marker"], f["lang"], f["sha256"]) for f in en_f] - loc_sig = [(f["marker"], f["lang"], f["sha256"]) for f in loc_f] + en_sig = [(f["marker"], f["lang"], f["body_sha256"]) for f in en_f] + loc_sig = [(f["marker"], f["lang"], f["body_sha256"]) for f in loc_f] if en_sig != loc_sig: err( "fence_parity", @@ -345,6 +423,10 @@ def warn(cls: str, detail: str) -> None: # twin (``CONTRIBUTING.fr.md``) — same rewrite rule as link targets. en_spans = extract_code_spans(en_text) loc_spans = set(extract_code_spans(loc_text)) + # Comment code spans inside fences are required too; they may be + # satisfied in the locale's prose OR in its fence comments. + en_spans += _fence_comment_spans(en_text) + loc_spans |= set(_fence_comment_spans(loc_text)) missing = [ s for s in dict.fromkeys(en_spans) diff --git a/tests/conformance/test_docs_i18n_germination.py b/tests/conformance/test_docs_i18n_germination.py index 73638c7e05839..68ca91f445bc0 100644 --- a/tests/conformance/test_docs_i18n_germination.py +++ b/tests/conformance/test_docs_i18n_germination.py @@ -131,16 +131,38 @@ def test_fence_scanner_gfm_closing_rules(): text = "```\nkey: value\n```yaml\n# comment inside the same block\nstill code\n```\n" fences = extract_fences(text) assert len(fences) == 1, f"expected ONE fence, got {fences}" - body_hashes = {f["sha256"] for f in fences} - # The single block contains all three body lines. + body_hashes = {f["body_sha256"] for f in fences} + # The single block contains all three body lines; the hash is of the + # COMMENT-NORMALIZED body (comments are localizable prose). import hashlib - expect = hashlib.sha256("key: value\n```yaml\n# comment inside the same block\nstill code".encode()).hexdigest()[:16] + norm_body = "key: value\n```yaml\n# comment inside the same block\nstill code" + from docs_germination import _comment_normalized # noqa: E402 + expect = hashlib.sha256(_comment_normalized(norm_body).encode()).hexdigest()[:16] assert body_hashes == {expect} # And the ```yaml interior line must not be scanned as prose. assert extract_code_spans(text) == [] assert extract_headings(text) == [] +def test_fence_comments_are_localizable_commands_are_not(): + """A translated comment inside a fence passes parity; an altered + command line fails. Comment code spans are still required.""" + en = "```bash\n# Preferred — matches CI (hermetic `env -i`)\nscripts/run_tests.sh\n```\n" + # Comment translated to French, command byte-identical, `env -i` kept: + loc_ok = "```bash\n# Préféré — correspond à CI (hermétique `env -i`)\nscripts/run_tests.sh\n```\n" + issues = check_doc_parity(en, loc_ok, "README.md", "xx", "germinated") + hard = [i for i in issues if i["class"] in ("fence_parity", "code_span_parity")] + assert not hard, f"translated comment should pass: {hard}" + # Command altered -> fence_parity error: + loc_bad = "```bash\n# Préféré — correspond à CI (hermétique `env -i`)\nscripts/run_tests.sh --xdist\n```\n" + issues = check_doc_parity(en, loc_bad, "README.md", "xx", "germinated") + assert any(i["class"] == "fence_parity" for i in issues) + # Comment drops `env -i` -> code_span_parity error: + loc_missing = "```bash\n# Préféré — correspond à CI\nscripts/run_tests.sh\n```\n" + issues = check_doc_parity(en, loc_missing, "README.md", "xx", "germinated") + assert any(i["class"] == "code_span_parity" for i in issues) + + def test_extract_code_spans_verbatim(): text = "run `hermes model` then `~/.hermes/config.yaml`" assert extract_code_spans(text) == ["hermes model", "~/.hermes/config.yaml"] From 757c9e58c22a2d5607e4770afdc6e04b55e1b1b1 Mon Sep 17 00:00:00 2001 From: "Andrex Ibiza, MBA" Date: Sun, 23 Aug 2026 07:30:24 -0500 Subject: [PATCH 08/14] docs(i18n): version the non-English target-locale contract --- .../docs-i18n-germination-spec.md | 55 ++++++++++++------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/docs/developer-guide/docs-i18n-germination-spec.md b/docs/developer-guide/docs-i18n-germination-spec.md index eec593995a324..8ade7c2be5a48 100644 --- a/docs/developer-guide/docs-i18n-germination-spec.md +++ b/docs/developer-guide/docs-i18n-germination-spec.md @@ -21,48 +21,65 @@ change. | Hub edges | Locale README links back to `README.md`; a germinated locale is linked **from** `README.md` (badge hub). | A locale is **germinated** when it passes every class with zero errors. -Locales whose translation predates the pipeline (es, zh-CN, ur-pk) run the -same checks at **warning** severity — debt is visible in every CI run and +Locales whose translation predates the pipeline (`zh-CN`, `es`, `ur-pk`) run +the same checks at **warning** severity — debt is visible in every CI run and measured in the debt report below — and are re-germinated through the -pipeline when adopted. +pipeline when adopted. `manual` is grandfathered debt for those three +translations only; new locales have no manual waiver. -## The manifest +## The versioned target-locale contract -`scripts/docs_germination.py` holds the manifest: the **top-10 global -languages** (Ethnologue 26th-edition order) plus the in-flight PRs for each -(interlocked, never duplicated): +The roadmap is the **top 10 non-English documentation locales** by worldwide +usage in **Ethnologue 200, 29th Edition (2026)**, using the `All Users` metric +(first-language plus second-language users, L1+L2). English is the canonical +source graph, not a translation target, so it is intentionally excluded from +the count. This keeps both Indonesian and Russian in scope: Russian is 11th in +the worldwide table but tenth after excluding canonical English. + +`scripts/docs_germination.py` holds this contract and the manifest. The +canonical target-locale tuple is the only completeness authority; tests must +iterate it directly rather than repeat a hardcoded subset. | Locale | Status | Provenance / interlock | |---|---|---| | zh-CN | manual | existing translation | | hi | pending | PR #4763 in flight | | es | manual | existing translation | -| fr | **germinated** | seed: iacker (#63660), refreshed by pipeline | | ar | pending | RTL review required | +| fr | **germinated** | seed: iacker (#63660), refreshed by pipeline | | bn | pending | PR #51306 in flight | | pt | pending | — | -| ru | pending | PR #69658 in flight | +| id | pending | claimed implementation: #92191 / #92192; full trio, gate first | | ur-pk | manual | existing translation | -| id | pending | 11th by speakers — next in line | +| ru | pending | PR #69658 in flight; retained as tenth non-English target | + +Every new locale must ship the complete `README` / `CONTRIBUTING` / `SECURITY` +trio and pass the gate before merge. A claimed lane is recorded in the +manifest so contributors extend the existing authority instead of opening a +duplicate implementation. ## Germination runbook (new language) -1. `python scripts/docs_germination.py extract --doc README.md --locale ` +1. Claim or adopt the locale's existing issue/PR and record that authority in + the manifest; do not open a duplicate lane. +2. `python scripts/docs_germination.py extract --doc README.md --locale ` — the span inventory (fences, spans, links, headings). -2. `python scripts/docs_germination.py template --doc README.md --locale ` +3. `python scripts/docs_germination.py template --doc README.md --locale ` — prose-placeholder template; translate the prose, keep every technical span and code block verbatim. -3. **Automatic path:** `python scripts/docs_germination.py germinate --locale +4. Repeat extraction/template/translation for the full `README.md`, + `CONTRIBUTING.md`, and `SECURITY.md` trio. +5. **Automatic path:** `python scripts/docs_germination.py germinate --locale --doc README.md --llm "hermes chat -Q -q"` — renders the template, pipes it to the LLM command (stdin → stdout), writes the locale file, and runs the parity gate on the output. **The gate is the arbiter**: a translation that drops a technical edge fails and is not shipped. (Manual - path: fill the template by hand, then assemble.) -4. `python scripts/docs_germination.py check` — iterate until the locale - passes every class. **Do not ship a locale that fails the gate.** -5. Add the language badge to `README.md` (the gate enforces this). -6. Add the locale to the manifest with `status: germinated` and its - provenance (credit ledger). + translation is allowed; manual *status* is not available to a new locale.) +6. `python scripts/docs_germination.py check` — iterate until all three locale + documents pass every class. **Do not ship a locale that fails the gate.** +7. Add the language badge to `README.md` (the gate enforces this). +8. Set the locale to `status: germinated` and preserve its contributor and PR + provenance in the manifest (credit ledger). ## Why this exists From 1dcefa80232f7b870c206417b84d4798b6c8df5a Mon Sep 17 00:00:00 2001 From: "Andrex Ibiza, MBA" Date: Sun, 23 Aug 2026 07:32:35 -0500 Subject: [PATCH 09/14] fix(i18n): bind the versioned non-English locale manifest --- scripts/docs_germination.py | 101 +++++++++++++++++++++--------------- 1 file changed, 59 insertions(+), 42 deletions(-) diff --git a/scripts/docs_germination.py b/scripts/docs_germination.py index 671359aced4e8..014c0cb4675e8 100644 --- a/scripts/docs_germination.py +++ b/scripts/docs_germination.py @@ -15,10 +15,11 @@ (fences, link targets, code spans, anchors) with heading drift reported as roadmap debt, never silently. - * New languages germinate from the manifest below (top-10 global languages - by total speakers, Ethnologue 26th ed. order). The pipeline is: - extract (span inventory) -> translate (template) -> assemble (merge) - -> check (parity gate) -> ship. + * New languages germinate from the manifest below: the top 10 non-English + documentation locales by worldwide `All Users` (L1+L2), Ethnologue 200, + 29th Edition (2026). English is the canonical source, not a translation + target. The pipeline is: extract (span inventory) -> translate (template) + -> assemble (merge) -> check (parity gate) -> ship. Pure stdlib. No network. No LLM calls in this file — it is the *gate*; the translation itself happens out-of-band (human or agent) and is verified here. @@ -36,27 +37,41 @@ REPO_ROOT = Path(__file__).resolve().parents[1] # ───────────────────────────────────────────────────────────────────────────── -# Manifest — the top-10 global languages (by total speakers, Ethnologue -# 26th edition order). This is the roadmap: status is either "germinated" -# (full parity gate), "manual" (existing translation, mechanical gate + -# reported heading debt), or "pending" (roadmap; no file yet). +# Manifest — the top 10 non-English documentation locales by worldwide usage, +# Ethnologue 200, 29th Edition (2026), `All Users` (L1+L2). English is the +# canonical source graph and is intentionally excluded from translation-target +# counting. Status is either "germinated" (full parity gate), "manual" +# (grandfathered existing translation, mechanical gate + reported heading +# debt), or "pending" (claimed or unclaimed roadmap lane; no landed files yet). # -# Provenance records who produced the seed translation — credit ledger. +# Provenance records who produced or owns the implementation — credit ledger +# and duplicate-work lock. # ───────────────────────────────────────────────────────────────────────────── ROOT_DOCS = ("README.md", "CONTRIBUTING.md", "SECURITY.md") -TOP_10_LANGUAGES = ( - "zh-CN", # Mandarin Chinese (existing translation) - "hi", # Hindi (issue #4763 in flight) - "es", # Spanish (existing translation) - "fr", # French (this campaign; seed by iacker via #63660) - "ar", # Modern Standard Arabic - "bn", # Bengali (PR #51306 in flight) - "pt", # Portuguese - "ru", # Russian (PR #69658 in flight) - "ur-pk", # Urdu (existing translation) - "id", # Indonesian (next in line after the top-10) +TARGET_LOCALE_CONTRACT = { + "version": "ethnologue-200-29e-2026-all-users-top-10-non-english", + "scope": "top 10 non-English documentation locales", + "source": "Ethnologue 200", + "edition": 29, + "year": 2026, + "metric": "All Users", + "speaker_basis": "L1+L2", + "canonical_source_locale": "en", +} + +TARGET_LOCALES = ( + "zh-CN", # Mandarin Chinese (global #2; existing translation) + "hi", # Hindi (global #3; issue #4763 in flight) + "es", # Spanish (global #4; existing translation) + "ar", # Modern Standard Arabic (global #5) + "fr", # French (global #6; seed by iacker via #63660) + "bn", # Bengali (global #7; PR #51306 in flight) + "pt", # Portuguese (global #8) + "id", # Indonesian (global #9; claimed by #92191 / #92192) + "ur-pk", # Urdu (global #10; existing translation) + "ru", # Russian (global #11; tenth non-English target) ) MANIFEST = { @@ -87,15 +102,6 @@ "provenance": "existing in-tree translation", "notes": "legacy; CONTRIBUTING.es.md lags English (602 vs 1009 lines)", }, - "fr": { - "name": "French", - "native": "Français", - "badge": "Français", - "color": "blue", - "status": "germinated", - "provenance": "seed: iacker (#63660), cherry-picked with authorship; refreshed against current main by the germination pipeline", - "notes": "full parity gate", - }, "ar": { "name": "Arabic", "native": "العربية", @@ -105,6 +111,15 @@ "provenance": "—", "notes": "RTL layout review required when germinating", }, + "fr": { + "name": "French", + "native": "Français", + "badge": "Français", + "color": "blue", + "status": "germinated", + "provenance": "seed: iacker (#63660), cherry-picked with authorship; refreshed against current main by the germination pipeline", + "notes": "full parity gate", + }, "bn": { "name": "Bengali", "native": "বাংলা", @@ -123,14 +138,14 @@ "provenance": "—", "notes": "", }, - "ru": { - "name": "Russian", - "native": "Русский", - "badge": "Русский", - "color": "purple", + "id": { + "name": "Indonesian", + "native": "Bahasa Indonesia", + "badge": "Bahasa", + "color": "blue", "status": "pending", - "provenance": "—", - "notes": "PR #69658 (README.ru.md) in flight — interlocked", + "provenance": "claimed implementation: issue #92191 / PR #92192", + "notes": "full README/CONTRIBUTING/SECURITY trio; gate-first interlock", }, "ur-pk": { "name": "Urdu", @@ -141,14 +156,14 @@ "provenance": "existing in-tree translation", "notes": "RTL layout; legacy heading parity debt reported, not gating", }, - "id": { - "name": "Indonesian", - "native": "Bahasa Indonesia", - "badge": "Bahasa", - "color": "blue", + "ru": { + "name": "Russian", + "native": "Русский", + "badge": "Русский", + "color": "purple", "status": "pending", "provenance": "—", - "notes": "11th by speakers — next in line", + "notes": "PR #69658 in flight; retained because English is canonical source, not a target locale", }, } @@ -724,6 +739,8 @@ def main(argv: list[str] | None = None) -> int: if args.json: out = { "root_docs": list(ROOT_DOCS), + "target_locale_contract": TARGET_LOCALE_CONTRACT, + "target_locales": list(TARGET_LOCALES), "languages": [ {"code": c, **{k: v for k, v in m.items() if k != "notes"}} for c, m in MANIFEST.items() From 4e8f81f4e83d1d0be697e26e02d33c73f42289c2 Mon Sep 17 00:00:00 2001 From: "Andrex Ibiza, MBA" Date: Sun, 23 Aug 2026 07:33:41 -0500 Subject: [PATCH 10/14] test(i18n): derive completeness from the canonical locale tuple --- .../conformance/test_docs_i18n_germination.py | 42 ++++++++++++++----- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/tests/conformance/test_docs_i18n_germination.py b/tests/conformance/test_docs_i18n_germination.py index 68ca91f445bc0..71bd4ca2d88f8 100644 --- a/tests/conformance/test_docs_i18n_germination.py +++ b/tests/conformance/test_docs_i18n_germination.py @@ -26,6 +26,8 @@ from docs_germination import ( # noqa: E402 MANIFEST, ROOT_DOCS, + TARGET_LOCALE_CONTRACT, + TARGET_LOCALES, check_all, check_doc_parity, extract_anchors, @@ -44,18 +46,36 @@ def _read(doc: str) -> str: return (REPO_ROOT / doc).read_text(encoding="utf-8") -def test_manifest_covers_top_ten_languages_and_french(): - """The manifest IS the roadmap: French must be germinated; the top-10 - global languages must all be listed (in Ethnologue order).""" - codes = list(MANIFEST) - assert "fr" in codes, "French must be in the manifest" +def test_manifest_matches_versioned_non_english_target_contract(): + """One versioned tuple is the completeness authority; the manifest and + every test consume it directly rather than repeating a drifting subset.""" + assert TARGET_LOCALE_CONTRACT == { + "version": "ethnologue-200-29e-2026-all-users-top-10-non-english", + "scope": "top 10 non-English documentation locales", + "source": "Ethnologue 200", + "edition": 29, + "year": 2026, + "metric": "All Users", + "speaker_basis": "L1+L2", + "canonical_source_locale": "en", + } + assert len(TARGET_LOCALES) == 10 + assert len(set(TARGET_LOCALES)) == len(TARGET_LOCALES) + assert "en" not in TARGET_LOCALES + assert tuple(MANIFEST) == TARGET_LOCALES + for code in TARGET_LOCALES: + assert code in MANIFEST, f"target locale {code} missing from manifest" + + assert {"id", "ru"} <= set(TARGET_LOCALES) assert MANIFEST["fr"]["status"] == "germinated" - # Top-10 global languages, minus zh-CN's slot being present: - for code in ("zh-CN", "hi", "es", "ar", "bn", "pt", "ru", "ur-pk"): - assert code in MANIFEST, f"top-10 language {code} missing from manifest" - # French must not be alone in germinated status (the gate must be - # enforceable without French being special-cased). - assert sum(1 for m in MANIFEST.values() if m["status"] == "germinated") >= 1 + assert {code for code, meta in MANIFEST.items() if meta["status"] == "manual"} == { + "zh-CN", + "es", + "ur-pk", + } + id_contract = f"{MANIFEST['id']['provenance']} {MANIFEST['id']['notes']}" + assert "#92191" in id_contract and "#92192" in id_contract + assert "11th" not in id_contract and "next in line" not in id_contract def test_french_docs_are_present_and_germinated(): From 6f43b4088ea1914723c364e09db2ea47969afcc1 Mon Sep 17 00:00:00 2001 From: "Andrex Ibiza, MBA" Date: Sun, 23 Aug 2026 07:39:35 -0500 Subject: [PATCH 11/14] docs(i18n): restore French discoverability on current main --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index c05112266746f..248fa894826c0 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ 中文 اردو Español + Français

**The self-improving AI agent built by [Nous Research](https://nousresearch.com).** It's the only agent with a built-in learning loop — it creates skills from experience, improves them during use, nudges itself to persist knowledge, searches its own past conversations, and builds a deepening model of who you are across sessions. Run it on a $5 VPS, a GPU cluster, or serverless infrastructure that costs nearly nothing when idle. It's not tied to your laptop — talk to it from Telegram while it works on a cloud VM. From d5440d44fce551aeaee7a0b57f80f73965278583 Mon Sep 17 00:00:00 2001 From: "Andrex Ibiza, MBA" Date: Sun, 23 Aug 2026 07:47:08 -0500 Subject: [PATCH 12/14] docs(i18n): refresh French security version command --- SECURITY.fr.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SECURITY.fr.md b/SECURITY.fr.md index 8ac10363858f2..93ea5215aabd6 100644 --- a/SECURITY.fr.md +++ b/SECURITY.fr.md @@ -16,7 +16,7 @@ Un signalement utile comprend : - Une description concise et une évaluation de la gravité. - Le composant affecté, identifié par chemin de fichier et plage de lignes (ex. `path/to/file.py:120-145`). -- Les détails de l'environnement (`hermes version`, SHA du commit, OS, +- Les détails de l'environnement (`hermes --version`, SHA du commit, OS, version de Python). - Une reproduction sur `main` ou la dernière release. - Une indication de la frontière de confiance du §2 qui est franchie. From c885fe3483e180b869f76659054066ecc2d14fe1 Mon Sep 17 00:00:00 2001 From: "Andrex Ibiza, MBA" Date: Sun, 23 Aug 2026 07:50:44 -0500 Subject: [PATCH 13/14] docs(i18n): refresh French contributing graph against current main --- CONTRIBUTING.fr.md | 66 ++++++++++++++++++---------------------------- 1 file changed, 25 insertions(+), 41 deletions(-) diff --git a/CONTRIBUTING.fr.md b/CONTRIBUTING.fr.md index e62c6e98e3f65..1428d7e1148ca 100644 --- a/CONTRIBUTING.fr.md +++ b/CONTRIBUTING.fr.md @@ -131,7 +131,7 @@ cd "${HERMES_HOME:-$HOME/.hermes}/hermes-agent" # Ajoutez les extras dev/test par-dessus l'installation standard. uv pip install -e ".[all,dev]" -# Optionnel : outils navigateur / dépendances du site de docs. +# Optionnel : dépendances du site de docs + de l'espace de travail. npm install ``` @@ -169,7 +169,7 @@ export PATH="$VIRTUAL_ENV/bin:$PATH" # Installez avec tous les extras (messagerie, cron, menus CLI, outils de dev) uv pip install -e ".[all,dev]" -# Optionnel : outils navigateur +# Optionnel : dépendances de l'espace de travail / des docs npm install ``` @@ -732,21 +732,7 @@ emprunter votre chemin de code. PowerShell est le remplaçant moderne de `wmic process`. Voir `hermes_cli/gateway.py::_scan_gateway_pids` pour le motif à suivre. -3. **`termios` et `fcntl` sont réservés à Unix.** Attrapez toujours à la fois - `ImportError` et `NotImplementedError` : - ```python - try: - from simple_term_menu import TerminalMenu - menu = TerminalMenu(options) - idx = menu.show() - except (ImportError, NotImplementedError): - # Repli : menu numéroté pour Windows - for i, opt in enumerate(options): - print(f" {i+1}. {opt}") - idx = int(input("Choice: ")) - 1 - ``` - -4. **Encodage des fichiers.** Windows peut enregistrer les fichiers `.env` en +3. **Encodage des fichiers.** Windows peut enregistrer les fichiers `.env` en `cp1252`. Gérez toujours les erreurs d'encodage : ```python try: @@ -759,7 +745,7 @@ emprunter votre chemin de code. `encoding="utf-8-sig"` pour lire les fichiers qui ont pu être touchés par un éditeur graphique Windows. -5. **Gestion des processus.** `os.setsid()`, `os.killpg()`, `os.fork()`, +4. **Gestion des processus.** `os.setsid()`, `os.killpg()`, `os.fork()`, `os.getuid()` et la gestion des signaux POSIX diffèrent sous Windows. Protégez avec `platform.system()`, `sys.platform` ou `hasattr(os, "setsid")` : ```python @@ -784,31 +770,31 @@ emprunter votre chemin de code. pass ``` -6. **Signaux inexistants sous Windows : `SIGALRM`, `SIGCHLD`, `SIGHUP`, +5. **Signaux inexistants sous Windows : `SIGALRM`, `SIGCHLD`, `SIGHUP`, `SIGUSR1`, `SIGUSR2`, `SIGPIPE`, `SIGQUIT`, `SIGKILL`.** Le module `signal` de Python lève `AttributeError` à l'import si vous les référencez sous Windows. Utilisez `getattr(signal, "SIGKILL", signal.SIGTERM)` ou placez tout le bloc derrière une vérification de plateforme. `loop.add_signal_handler` lève `NotImplementedError` sous Windows — attrapez-la toujours. -7. **Séparateurs de chemins.** Utilisez `pathlib.Path` plutôt que la concaténation +6. **Séparateurs de chemins.** Utilisez `pathlib.Path` plutôt que la concaténation de chaînes avec `/`. Les slashs fonctionnent presque partout sous Windows, mais `subprocess.run(["cmd.exe", "/c", ...])` et d'autres contextes shell peuvent exiger des antislashs — convertissez avec `str(path)` à la frontière du subprocess, pas au cœur de la logique Python. -8. **Les liens symboliques exigent des privilèges élevés sous Windows** (sauf si +7. **Les liens symboliques exigent des privilèges élevés sous Windows** (sauf si le mode développeur est activé). Les tests qui créent des liens symboliques ont besoin de `@pytest.mark.skipif(sys.platform == "win32", reason="Symlinks require elevated privileges on Windows")`. -9. **Les modes de fichiers POSIX (0o600, 0o644, etc.) ne sont PAS appliqués sur +8. **Les modes de fichiers POSIX (0o600, 0o644, etc.) ne sont PAS appliqués sur NTFS** par défaut. Les tests qui font des assertions sur `stat().st_mode & 0o777` doivent être ignorés sous Windows — le concept ne se transpose pas. Utilisez des ACL (`icacls`, `pywin32`) pour protéger les fichiers de secrets sous Windows si nécessaire. -10. **Les démons d'arrière-plan détachés sous Windows exigent `pythonw.exe`, PAS +9. **Les démons d'arrière-plan détachés sous Windows exigent `pythonw.exe`, PAS `python.exe`.** `python.exe` alloue toujours une console ou s'y attache, ce qui le rend vulnérable aux diffusions de `CTRL_C_EVENT` depuis n'importe quel processus frère. `pythonw.exe` est la variante sans console. Combinez avec @@ -817,7 +803,7 @@ emprunter votre chemin de code. Voir `hermes_cli/gateway_windows.py::_spawn_detached` pour l'implémentation de référence. -11. **`subprocess.Popen` avec des shims `.cmd` ou `.bat` a besoin de +10. **`subprocess.Popen` avec des shims `.cmd` ou `.bat` a besoin de `shutil.which` pour la résolution.** Passer `"agent-browser"` à `Popen` sous Windows trouve le shim shebang POSIX sans extension dans `node_modules/.bin/`, que `CreateProcessW` ne peut pas exécuter — vous @@ -825,32 +811,32 @@ emprunter votre chemin de code. `shutil.which("agent-browser", path=local_bin)`, qui honore PATHEXT et choisit la variante `.CMD` sous Windows. -12. **N'utilisez pas les shebangs shell pour lancer du Python.** `#!/usr/bin/env +11. **N'utilisez pas les shebangs shell pour lancer du Python.** `#!/usr/bin/env python` ne fonctionne que lorsque le fichier est exécuté à travers un shell Unix. `subprocess.run(["./myscript.py"])` échoue sous Windows même si le fichier a une ligne shebang. Invoquez toujours Python explicitement : `[sys.executable, "myscript.py"]`. -13. **Commandes shell dans les installeurs.** Si vous modifiez +12. **Commandes shell dans les installeurs.** Si vous modifiez `scripts/install.sh`, faites la modification équivalente dans `scripts/install.ps1`. Ces deux scripts sont l'exemple canonique de « fonctionne sous Linux ne veut pas dire fonctionne sous Windows » et ont divergé plusieurs fois — gardez-les synchronisés. -14. **Chemins connus redirigés vers OneDrive sous Windows :** Desktop, +13. **Chemins connus redirigés vers OneDrive sous Windows :** Desktop, Documents, Pictures, Videos. Le « vrai » chemin quand OneDrive Backup est activé est `%USERPROFILE%\OneDrive\Desktop` (etc.), et NON `%USERPROFILE%\Desktop` (qui existe en coquille vide). Résolvez l'emplacement réel via `ctypes` + `SHGetKnownFolderPath` ou en lisant la clé de registre `Shell Folders` — ne supposez jamais `~/Desktop`. -15. **CRLF vs LF dans les scripts générés.** `cmd.exe` et `schtasks` sous Windows +14. **CRLF vs LF dans les scripts générés.** `cmd.exe` et `schtasks` sous Windows parsent ligne par ligne ; des fins de ligne mixtes ou uniquement LF peuvent casser des fichiers `.cmd` / `.bat` multilignes. Utilisez `open(path, "w", encoding="utf-8", newline="\r\n")` — ou `open(path, "wb")` + octets explicites — pour générer des scripts que Windows exécutera. -16. **Deux schémas de quoting différents sur une même ligne de commande.** +15. **Deux schémas de quoting différents sur une même ligne de commande.** `subprocess.run(["schtasks", "/TR", some_cmd])` → schtasks parse lui-même `/TR`, ET la chaîne `some_cmd` est re-parsée par `cmd.exe` quand la tâche se déclenche. Parseurs différents, règles d'échappement différentes. Utilisez @@ -860,18 +846,16 @@ emprunter votre chemin de code. ### Tester le multiplateforme -Les tests qui utilisent des appels système exclusivement POSIX ont besoin d'un marqueur de skip. Les cas courants : -- Liens symboliques → `@pytest.mark.skipif(sys.platform == "win32", ...)` -- Modes de fichiers `0o600` → `@pytest.mark.skipif(sys.platform.startswith("win"), ...)` -- `signal.SIGALRM` → Unix uniquement (les délais par test ne l'utilisent plus directement ; voir le shim de délai win32 dans `tests/conftest.py::pytest_configure`) -- `os.setsid` / `os.fork` → Unix uniquement -- Tests de régression Winsock réels / propres à Windows → - `@pytest.mark.skipif(sys.platform != "win32", reason="Windows-specific regression")` +Les tests qui exercent un comportement propre à certaines plateformes doivent s'exécuter sur leurs plateformes cibles. + +```python +@pytest.mark.linux_only +@pytest.mark.macos_only +@pytest.mark.windows_only +``` -Si vous monkeypatchez `sys.platform` pour des tests multiplateformes, patchez aussi -`platform.system()` / `platform.release()` / `platform.mac_ver()` — chacun relit -indépendamment le véritable OS, si bien que des tests à moitié patchés empruntent -quand même la mauvaise branche sur un runner Windows. +Évitez de monkeypatcher `sys.platform` sauf nécessité absolue ; si vous le faites, patchez aussi `platform.system()` / `platform.release()` / `platform.mac_ver()`. +Les liens symboliques, les permissions 0o600, SIGALRM et os.setsid/fork sont tous propres à Unix. --- @@ -1003,7 +987,7 @@ test(tools): add unit tests for file_operations ## Signaler un problème - Utilisez les [GitHub Issues](https://github.com/NousResearch/hermes-agent/issues) -- Incluez : OS, version de Python, version d'Hermes (`hermes version`), traceback d'erreur complet +- Incluez : OS, version de Python, version d'Hermes (`hermes --version`), traceback d'erreur complet - Incluez les étapes de reproduction - Vérifiez les issues existantes avant de créer des doublons - Pour les vulnérabilités de sécurité, merci de les signaler en privé From 34c6c1c33fe0d1494a7f430673c9b36ff7d3e405 Mon Sep 17 00:00:00 2001 From: "Axl Ibiza, MBA" Date: Mon, 31 Aug 2026 12:41:06 -0500 Subject: [PATCH 14/14] fix(docs): remove unmatched contributing fence --- CONTRIBUTING.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 61bacafbd888d..3ef9e2b9a3b97 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -723,7 +723,6 @@ that touches the OS, assume *any* platform can hit your code path. For process enumeration: PowerShell's `Get-CimInstance Win32_Process` is the modern replacement for `wmic process`. See `hermes_cli/gateway.py::_scan_gateway_pids` for the pattern. - ``` 3. **File encoding.** Windows may save `.env` files in `cp1252`. Always handle encoding errors: