Serveur MCP donnant à des sessions Claude multiples et isolées un accès en lecture à des bases de connaissance markdown, et un moyen de les alimenter sans jamais les modifier directement.
Implémente la spécification « OKF Bundle Hub v0 » (identifiant de version dans
les manifestes : bundle-spec: "0.1").
| Document | Contenu |
|---|---|
docs/SPEC-okf-bundle-hub-v0.md |
La spécification. Elle fait autorité ; tous les renvois « § x.y » du code y renvoient. |
docs/ARCHITECTURE.md |
Conception : choix d'implémentation, mécanismes de correction, écarts assumés, traçabilité test ↔ exigence. |
docs/API.md |
Contrat des sept outils kb_* : schémas, sorties, codes d'erreur, séquences typiques. |
docs/J0-verification-okf.md |
La spec OKF externe et ses trois divergences avec celle du hub. |
CLAUDE.md |
Orientation pour une session ouvrant ce dépôt. |
skills/kb-review/SKILL.md |
Déroulé de revue du rôle gestionnaire. |
- Git est canonique. Tout l'état vit dans les dépôts git des bases. Aucune base de données. Tout index ou cache est dérivé et régénérable.
- Frontière de confiance à l'écriture. Les sessions consommatrices ne modifient jamais le corpus : elles déposent des propositions. Seul le rôle gestionnaire intègre.
- Une base sans le hub reste utilisable. Un bundle est un dépôt markdown lisible par un humain ou n'importe quel outil.
- Optimisation = économie de tokens. Les outils retournent le minimum pertinent ; toute sortie volumineuse est plafonnée (~4 000 tokens) avec troncature signalée.
Une fois les dépendances installées (uv sync, déjà fait par le
post-create du devcontainer), okf-hub setup enchaîne les quatre étapes
détaillées plus bas — sans en remplacer aucune : c'est le même résultat,
automatisé pour les cas qu'il peut détecter sans deviner un identifiant ou un
secret externe.
uv run okf-hub setup- Identité git — configurée si elle manque encore (interactif ;
--yespour sauter la saisie plutôt qu'attendre une réponse). - Clé(s) SSH — dans un devcontainer, délègue à
.devcontainer/deploy-keys.sh(idempotent : relancerokf-hub setupaprès avoir enregistré une clé sur GitHub la valide). - Client MCP — enregistre
okf-hubauprès de Claude Code (claude mcp add) si la commandeclaudeest dans le PATH, et met à jour la config de Claude Desktop si elle est détectée sur la machine. - Bases livrées — installe ce qui manque dans
bases/(déjà fait automatiquement au premier démarrage du serveur ; utile ici pour une confirmation immédiate, sans attendre une connexion cliente).
Chaque étape sans objet dans l'environnement courant (pas de devcontainer,
aucun client MCP détecté, Claude Desktop absent, bootstrap-bundles: false)
est signalée comme telle dans le rapport final — jamais silencieuse — et
renvoie vers la procédure manuelle correspondante ci-dessous : hub hors
devcontainer, client MCP configuré à la main, hub derrière docker exec.
Ouvrir le dépôt dans VS Code → Reopen in Container. Le post-create installe
ripgrep, uv et les dépendances, pose l'identité git, prépare les deploy keys,
puis lance les tests.
Un commit poussé sous une adresse qui n'existe pas n'est rattaché à aucun compte
GitHub : ni auteur identifiable dans l'historique, ni contribution comptée. Le
conteneur lit donc .devcontainer/git-identity.env, non versionné — chaque
mainteneur a la sienne :
cat > .devcontainer/git-identity.env <<'EOF'
OKF_GIT_NAME="Prénom NOM"
OKF_GIT_EMAIL="<id>+<login>@users.noreply.github.com"
EOFL'adresse noreply de GitHub (Settings > Emails) garantit le rattachement au
compte sans publier d'adresse personnelle. Sans ce fichier, post-create.sh
retombe sur une identité locale et le signale bruyamment : c'est un défaut
de repli, pas un réglage.
Un remote en git@github.com: n'a dans un conteneur ni clé ni agent —
git push échoue en « Permission denied (publickey) », et le commit reste sur
place sans que rien ne le signale. Les remotes en https:// semblent marcher
sans rien faire, mais par le helper de credentials de VS Code : ils tombent dès
qu'un processus git tourne hors de cette session — bootstrap.py au démarrage
du serveur, un docker exec.
.devcontainer/deploy-keys.sh règle les deux cas avec une deploy key par
dépôt, dans un volume nommé monté sur ~/.ssh (elles survivent aux rebuilds).
Il inventorie les dépôts — origin du hub, amonts de bundles/upstreams.yaml,
remotes des bases clonées —, génère la clé manquante de chacun, écrit le
~/.ssh/config associant dépôt et clé, et affiche les clés publiques restant à
enregistrer avec l'URL exacte où le faire (Settings > Deploy keys > Add deploy
key, cocher Allow write access pour pouvoir pousser). Il est idempotent :
relancez-le après chaque enregistrement.
Jamais de clé de compte (Settings > SSH and GPG keys) : elle ouvrirait tous les dépôts du compte, en écriture, depuis un conteneur où tournent des sessions Claude. Une deploy key ne vaut que son dépôt et se révoque seule.
Les URL versionnées ne sont pas touchées : bundles/upstreams.yaml doit rester
clonable depuis n'importe quelle machine. L'aiguillage vers la bonne clé passe
par des url.<alias>.insteadOf dans la config git globale du conteneur,
posés uniquement pour les deploy keys déjà acceptées par GitHub — tant qu'une
clé n'est pas enregistrée, l'accès existant continue de fonctionner. La
migration se fait donc dépôt par dépôt, sans coupure.
Sans aucun matériel de clé dans le conteneur : faire tourner un ssh-agent
sur l'hôte avant d'attacher VS Code, qui en transmet la socket. Le montage
devient alors inutile et peut être retiré — mais le hub ne peut plus rien
pousser quand VS Code est détaché.
Ce montage est un écart assumé à la lettre du § 4.3 de la spec (« montage : le
répertoire du hub uniquement ») : motif, mesure de ce qu'il ouvre et manière de
l'annuler sont dans docs/ARCHITECTURE.md § 5.3. C'est
un volume, jamais un bind sur un répertoire de l'hôte — la distinction est
gardée par un test.
L'enregistrement d'une deploy key reste, par défaut, la procédure manuelle ci-dessus : coller la clé publique affichée dans Settings > Deploy keys. Pour l'automatiser — utile dès qu'on ajoute plusieurs bases — installer une GitHub App dédiée, une fois, hors du conteneur :
-
Créer la GitHub App (github.com → Settings > Developer settings > GitHub Apps > New GitHub App), avec la permission de dépôt Administration : Read & write, et aucune autre. C'est la permission GitHub App la plus étroite qui couvre la gestion des deploy keys par API — GitHub n'expose pas de permission plus fine dédiée aux seules deploy keys ;
Administrationpermet aussi des actions plus larges sur le dépôt concerné (renommer, gérer la protection de branches, etc.). Aucun webhook n'est nécessaire. -
Installer l'App sur le ou les dépôts concernés (le hub, et/ou les bases dont on gère aussi le dépôt) — ce choix borne exactement les dépôts couverts, comme pour une deploy key classique.
-
Sur votre machine, jamais dans le conteneur, fabriquer un jeton d'installation de courte durée (~1h, révocable en désinstallant l'App) à partir de la clé privée de l'App téléchargée à l'étape 1 :
APP_ID=<identifiant de la GitHub App> INSTALLATION_ID=<identifiant de l'installation — visible dans l'URL après l'installation> PEM=<chemin vers la clé privée .pem de l'App> now=$(date +%s) b64url() { openssl base64 -A | tr '+/' '-_' | tr -d '='; } jwt_header=$(printf '{"alg":"RS256","typ":"JWT"}' | b64url) jwt_payload=$(printf '{"iat":%d,"exp":%d,"iss":"%s"}' "$((now-60))" "$((now+300))" "$APP_ID" | b64url) jwt_sig=$(printf '%s.%s' "$jwt_header" "$jwt_payload" | openssl dgst -sha256 -sign "$PEM" | b64url) jwt="$jwt_header.$jwt_payload.$jwt_sig" curl -sS -X POST -H "Authorization: Bearer $jwt" \ -H "Accept: application/vnd.github+json" \ "https://api.github.com/app/installations/$INSTALLATION_ID/access_tokens" \ | python3 -c 'import json,sys; print(json.load(sys.stdin)["token"])'
-
Dans le conteneur, exporter ce jeton pour la durée d'une seule invocation, puis relancer l'enregistrement :
export OKF_HUB_GH_APP_TOKEN=<jeton collé depuis l'étape 3> .devcontainer/deploy-keys.sh
Le jeton n'est jamais écrit sur disque ni journalisé — il ne vit que dans cette variable, le temps de l'appel API, et expire de lui-même. Sans lui (cas par défaut), le script se comporte exactement comme avant : rien n'est cassé si vous ignorez cette section.
Ce que ce choix n'est pas : jamais un jeton de compte GitHub, jamais un
scope repo global, et surtout — la clé privée de la GitHub App elle-même
n'entre jamais dans ce conteneur, où tournent des sessions Claude qui
exécutent du code ; seul un jeton d'installation déjà limité et expirant y
transite, une fois, à l'initiative de l'opérateur. Détail du raisonnement et
de ce que la permission Administration ouvre au-delà des deploy keys :
docs/ARCHITECTURE.md § 5.3 bis.
Prérequis : Python ≥ 3.11, git, ripgrep.
curl -LsSf https://astral.sh/uv/install.sh | sh # si uv n'est pas installé
uv sync
uv run pytest -q| Outil | Rôle |
|---|---|
kb_list |
Bases disponibles, avec titre, objet, nombre de documents et de propositions en attente. include_pending_concerns ajoute les sujets en attente. |
kb_search |
Recherche plein texte dans une base. Mode keyword (ET strict, repli automatique en OU signalé) ou regex (dialecte ripgrep). Chaque extrait porte le heading de sa section, après §, à reporter tel quel dans kb_read. |
kb_read |
Lecture d'un document, ou d'une seule section. Au-delà du seuil, retourne la table des headings — force: true pour passer outre. |
kb_governance |
Golden rules et schéma de frontmatter d'une base. Signale par un bandeau une gouvernance en status: draft. |
kb_propose |
Dépose une proposition dans proposals/pending/. Seul outil d'écriture, et il ne touche jamais au corpus. |
kb_proposal_status |
État et résolution des propositions : intégrée (avec les documents modifiés) ou rejetée (avec le motif). Lecture pure. id ou submitted_by requis. |
kb_hub_rescan |
Rapport de découverte : bundles rejetés avec motif, collisions de name. La découverte elle-même est déjà déclenchée par kb_list. |
Le paramètre base est toujours le champ name du manifeste, jamais le nom du
répertoire dans bases/ — les deux diffèrent dès qu'un clone est renommé.
Le schema.yaml d'une base décrit le frontmatter de son corpus, pas celui des
propositions. Une proposition n'a pas à s'y conformer : soumettez
l'information, sa mise en forme conforme au schéma relève du gestionnaire à
l'intégration. Les champs de kb_propose sont le seul format requis.
Le transport est stdio : chaque client connecté lance sa propre instance du serveur.
claude mcp add okf-hub -- /chemin/vers/okf-hub/.venv/bin/python -m okf_hub \
--hub-root /chemin/vers/okf-hubOu dans .mcp.json à la racine d'un projet :
{
"mcpServers": {
"okf-hub": {
"command": "/chemin/vers/okf-hub/.venv/bin/python",
"args": ["-m", "okf_hub", "--hub-root", "/chemin/vers/okf-hub"],
"env": { "PYTHONPATH": "/chemin/vers/okf-hub/src" }
}
}
}Dans claude_desktop_config.json : mêmes command et args.
La commande doit traverser la frontière WSL. wsl.exe prend le relais :
{
"mcpServers": {
"okf-hub": {
"command": "wsl.exe",
"args": [
"-d", "Ubuntu", "--cd", "/home/<utilisateur>/okf-hub", "--",
"/home/<utilisateur>/okf-hub/.venv/bin/python", "-m", "okf_hub"
]
}
}
}Points de vigilance : utiliser des chemins Linux après --, et vérifier le
nom de la distribution avec wsl.exe -l -q.
Le client doit lancer le serveur à l'intérieur du conteneur :
{
"mcpServers": {
"okf-hub": {
"command": "docker",
"args": [
"exec", "-i", "<nom-ou-id-du-conteneur>",
"/workspaces/okf-hub/.venv/bin/python", "-m", "okf_hub"
]
}
}
}-i est indispensable : sans lui, stdin est fermé et la poignée de main MCP
échoue sans message.
.venv/bin/python -m okf_hub --hub-root . --verboseLe serveur attend sur stdin ; le journal part sur stderr. Ctrl-D pour sortir.
Si rien n'apparaît, hub.log porte la trace du démarrage.
git clone <url> bases/<nom>Puis kb_hub_rescan depuis une session connectée. Il n'y a pas d'autre
étape — c'est un invariant du produit.
Pour créer une base : partir du template
okf-bundle-template et dérouler son
INSTANTIATE.md.
bundles/upstreams.yaml peut porter, en plus de l'URL, un titre, une
description et des tags par base connue :
uv run okf-hub catalog list # ce qui est connu, déployé ou non
uv run okf-hub catalog list --tag rh # filtré par tag
uv run okf-hub catalog show <nom> # détail d'une entrée
uv run okf-hub catalog add <nom> <url> --title "…" --description "…" --tag rh
uv run okf-hub catalog import <nom> # git clone <url> bases/<nom> — rien d'autrecatalog import exécute exactement la commande ci-dessus : c'est le même
invariant, seulement l'URL exacte à ne plus connaître ou taper. catalog add/remove ne font qu'éditer bundles/upstreams.yaml ; une entrée sans
titre ni description ni tag reste une simple URL, comme avant ce catalogue
(compatibilité intégrale avec le format historique).
Importer un bundle en v0 est sans risque d'exécution — tools/ et
skills/ ne sont pas chargés — mais pas sans risque d'influence. Trois
vecteurs d'injection de prompt existent :
titleetdescriptiondu manifeste, injectés dans les descriptions d'outils MCP, donc dans le contexte de toutes les sessions connectées, sans même que quiconque ouvre le bundle ;GOVERNANCE.md, injecté dans le contexte du gestionnaire ;CLAUDE.md, dans celui de toute session ouvrant le dépôt.
La validation du manifeste limite la surface (pas de retour à la ligne dans
title, description normalisée et plafonnée à 500 caractères) sans
l'éliminer.
Consigne v0 : n'importer que des bundles de confiance, et relire le
manifeste, GOVERNANCE.md et CLAUDE.md avant le premier usage de tout
bundle tiers.
Avant ce cycle, retirer une base n'était qu'une suppression manuelle de
répertoire, documentée en prose (okf-hub-guide, cycle de vie § 5), jamais
outillée :
uv run okf-hub catalog retire <nom> # garde-fous, puis suppression
uv run okf-hub catalog retire <nom> --forget # + oublie l'entrée du catalogueDeux garde-fous, jamais bloquants avec --force : aucune proposition ne doit
dormir dans proposals/pending/ de la base, et — si elle a un remote — sa
branche amont suivie ne doit pas être en retard sur HEAD (vérification
locale uniquement, sans nouvel accès réseau ; un dépôt purement local n'est
jamais concerné). La suppression elle-même reste ce qu'elle a toujours été : le
répertoire du bundle sous bases/, jamais l'entrée du catalogue (séparée,
--forget pour l'oublier aussi) ni le dépôt distant. Rien n'est perdu tant que
le dépôt existe ailleurs — un bundle est un dépôt git autonome. Le retrait est
visible dès le prochain kb_list ou kb_hub_rescan.
Le transport stdio implique qu'une instance du serveur tourne par client connecté. Plusieurs processus opèrent donc simultanément sur les mêmes dépôts git. Conséquences pratiques :
Il n'y a ni état partagé ni démon : la vérité est sur le disque, chaque instance la relit. Deux déclencheurs automatiques, chacun sous un cooldown de 5 s par instance, font qu'une base importée après le démarrage d'une session lui devient visible sans intervention :
- tout
kb_listdéclenche la découverte avant de répondre ; - une erreur
UNKNOWN_BASEdéclenche un re-scan silencieux, puis retente l'appel.
Les deux comptent leur cooldown séparément : lister puis appeler dans la foulée une base importée entre-temps fonctionne, le premier appel ne consomme pas le re-scan du second.
kb_hub_rescan reste utile pour voir le rapport d'un import — bundles rejetés
avec leur motif, collisions de name — pas pour rafraîchir.
Un rescan « partagé au niveau du hub » a été demandé et refusé : il supposerait précisément l'état partagé que ce modèle exclut.
Le serveur émet la notification MCP tools/list_changed quand la liste des
bases change. Claude Desktop l'a historiquement ignorée. L'implémentation ne
compte pas dessus : ce sont les re-scans ci-dessus qui garantissent le
fonctionnement.
Conséquence visible, et purement cosmétique : la description de kb_list,
qui énumère les bases connues, peut rester périmée dans le contexte d'une
session. Le contenu que l'outil retourne, lui, est à jour.
Toute écriture git prend un verrou flock() exclusif sur
bases/<nom>/.okf-hub.lock — libéré automatiquement à la mort du processus, et
partagé entre le serveur et le script okf-lock. Au-delà de 15 s d'attente, une
erreur BASE_BUSY invite à réessayer.
Les lectures (kb_search, kb_read) ne prennent aucun verrou. Une lecture
pendant une intégration peut donc voir un état intermédiaire du worktree.
Accepté en v0.
Le gestionnaire n'est pas un démon : c'est une session Claude invoquée à la
demande, outillée par la skill kb-review.
Installer la skill pour Claude Code :
mkdir -p ~/.claude/skills
ln -s "$PWD/skills/kb-review" ~/.claude/skills/kb-reviewPuis, dans une session : « passe en revue les propositions de la base <name> ».
Le moteur sous-jacent est utilisable seul :
bin/okf-review reconcile <base> # étape 0, rattrapage
bin/okf-review reconcile <base> --apply
bin/okf-review context <base> # golden rules + schéma + corpus
bin/okf-review inventory <base> --full # propositions en attente
bin/okf-review resolve <base> --plan plan.json --dry-run
bin/okf-review resolve <base> --plan plan.jsonresolve et reconcile --apply prennent eux-mêmes le verrou, à la granularité
imposée : une résolution complète = une acquisition. Ne pas les envelopper
dans okf-lock.
Pour toute autre séquence git sur une base, passer par le wrapper :
root=$(bin/okf-base-path ma-base root)
bin/okf-lock ma-base -- sh -c "git -C '$root' … && git -C '$root' commit -m '…'"Le verrou doit couvrir la séquence complète, jamais commande par commande.
kb_proposal_status rend l'état, la résolution, integrated-into et le motif de
rejet — c'est la boucle complète du contributeur, sans accès git. Ce qu'il ne
rend pas, délibérément, c'est le corps de la proposition : il peut peser
16 Ko, et une fois intégrée, ce qui compte est le corpus, lisible par kb_read
en suivant integrated-into.
Pour relire le texte exact d'une proposition rejetée :
git -C bases/<nom> log --grep "Proposal: prop-2026-08-30-a3f2"
cat bases/<nom>/proposals/rejected/prop-2026-08-30-a3f2.mdkb_search interroge une base à la fois. L'extension multi-bases est reportée en
v1 optionnelle : un seul retour d'usage l'a demandée, on attend la récurrence
avant d'élargir la surface d'outils.
C'est un champ déclaratif. Il ne doit peser dans aucune décision d'intégration.
Le clone présent dans bases/ est la copie canonique. Aucun push ni pull
automatique n'est effectué, même si le bundle a un remote. Toute
synchronisation est manuelle et hors garanties : un pull qui écrase des
propositions locales non poussées est de la responsabilité de l'opérateur.
Pratique recommandée : pousser après chaque session de revue.
git -C bases/<nom> pushDeux bases documentent le hub lui-même. Ce sont des bundles ordinaires —
aucun traitement de faveur dans le code, et le hub tourne sans elles — mais elles
comblent une lacune structurelle : une session connectée en MCP ne voit ni ce
README, ni docs/API.md, ni CLAUDE.md. Elle ne dispose que des outils et de
leurs descriptions.
Le serveur les annonce dans son champ instructions, le seul texte qu'une
session reçoit sans dépenser d'appel — et seulement si elles sont déployées.
Séquences d'appels, stratégie de recherche et de lecture, rôles et frontière de confiance à l'écriture, ce qu'est une proposition recevable, et le cycle de vie complet d'une base : créer, déployer, alimenter, réviser, retirer — avec à chaque étape le rôle qui l'exécute et le moyen employé.
Elle ne contient aucun schéma d'outil, par golden rule. La référence vit dans
les descriptions d'outils et dans docs/API.md ; une troisième copie serait la
seule qu'aucun test ne garde, et une base se met à jour par le circuit de
propositions quand une référence d'API doit bouger en verrou avec le code.
Cette exclusion n'est pas qu'une intention : tests/test_bases_meta.py lit les
SCHEMA du code et échoue si un corpus meta cite un outil inexistant, attribue à
un outil un paramètre absent de son schéma, ou introduit un tableau de référence.
Le hub est son propre premier cas d'usage : les retours sur les outils — pas sur le contenu métier des autres bases — arrivent par le circuit standard.
kb_governance base=okf-hub-feedback → ce qu'un retour recevable contient
kb_propose base=okf-hub-feedback … → le dépôt
kb_proposal_status base=okf-hub-feedback id=… → le verdict, plus tard
Deux golden rules décident de la recevabilité : citer l'outil concerné et décrire le comportement observé (entrées, base, sortie obtenue, sortie attendue) avant toute demande d'évolution. Son corpus porte la roadmap des évolutions — décidées, reportées, refusées, avec le motif de chaque arbitrage — et les limitations connues.
Leur source est versionnée dans bundles/. Au démarrage, le
serveur installe dans bases/ celles qui manquent : un git clone du hub suffit
donc à disposer du guide, sans second dépôt à cloner.
bin/okf-bootstrap --list # ce qui est livré, et ce qui est déployé
bin/okf-bootstrap # installe ce qui manque, sans rien écraserPour maîtriser entièrement le contenu de bases/, mettre
bootstrap-bundles: false dans hub-config.yaml.
Une base clonée depuis un dépôt canonique (bundles/upstreams.yaml, ou
importée manuellement avec son propre remote) est mise à jour en
fast-forward-only à chaque démarrage d'une instance de serveur, avant sa
première découverte. Jamais de push : le § 4.5 de la spec confie toute
synchronisation à l'opérateur, ce mécanisme ne fait qu'automatiser le pull
quand il est sûr.
Une divergence — des propositions locales commitées par kb_propose mais
jamais poussées, pendant que le dépôt canonique a lui aussi avancé — n'est
jamais écrasée : elle est signalée dans hub.log, et la synchronisation de
cette base attend une résolution manuelle. Un remote absent, injoignable, ou
une base occupée par une écriture en cours ne bloque jamais le démarrage.
Désactivable par sync-on-start: false dans hub-config.yaml, pour un
opérateur qui gère ses pulls lui-même.
Pourquoi deux emplacements. Une base doit être son propre dépôt git. Si
elle n'était qu'un sous-répertoire du dépôt du hub, gitops.commit_paths
exécuterait git -C dans le dépôt englobant, et un kb_propose de n'importe
quelle session commiterait sur la branche main du hub — sans erreur. Le
détail est dans bundles/README.md.
La source de vérité diffère ensuite selon la base.
okf-hub-guide est rédigée par les mainteneurs, en verrou avec le code :
bundles/ fait foi, elle est semée de là, et un test vérifie que la copie
déployée n'en diverge pas.
okf-hub-feedback est alimentée par les sessions : son dépôt publié est
l'original, et elle est donc clonée, pas semée.
https://github.com/Movida/okf-hub-feedback
Semer une base qui a un dépôt canonique produirait sur chaque machine une
histoire git sans rapport avec la sienne, et les propositions qu'on y déposerait
seraient irrécupérables. Les dépôts canoniques sont déclarés dans
bundles/upstreams.yaml ; si le clone échoue, la base
n'est pas installée et le journal dit comment rattraper — absente vaut mieux
qu'orpheline.
Le hub lit hub-config.yaml à sa racine (§ 4.1 de la spec). Cinq paramètres
configurables : bases-dir (emplacement des bundles), read-toc-threshold
(seuil pour retourner la table des matières au lieu du contenu complet),
log-file (journal partagé par toutes les instances), bootstrap-bundles et
sync-on-start (mentionnés plus haut).
Plutôt que de configurer chaque paramètre individuellement, on peut choisir un
profil prédéfini via le champ profile de hub-config.yaml :
profile: devProfils disponibles :
solo(par défaut) : comportement actuel — bases dans./bases, seuil 8 Ko, journal activé, bootstrap et sync automatiques au démarrage.dev: développement local —bootstrap-bundles: false,sync-on-start: false; reste identique àsolo.ci: CI/tests — seuil 16 Ko,log-filedésactivé ; reste identique àsolo.
Chaque paramètre explicite dans hub-config.yaml surcharge la valeur du profil.
Un opérateur qui ne configure rien (fichier absent ou vide) bénéficie du profil
solo, garantissant le comportement actuel.
Exemple combinant profil et surcharge :
profile: dev
read-toc-threshold: 4096 # surcharge du profil devToute proposition apparaît dans exactement deux commits : un de soumission, un de résolution (éventuellement partagé avec d'autres propositions du même sujet).
git -C bases/<nom> log --grep "Proposal: <id>" # histoire d'une proposition
git -C bases/<nom> log --grep "Submitted-By: <qui>" # contributions d'un auteurCes invariants reposent sur le rejet des retours à la ligne dans concerns,
submitted_by et sources : sans cette validation, un contributeur pourrait
forger de faux trailers.
src/okf_hub/
├── __main__.py point d'entrée stdio
├── server.py câblage MCP, re-scan (kb_list et UNKNOWN_BASE), notifications
├── config.py hub-config.yaml
├── registry.py découverte, corpus, exclusions, confinement des chemins
├── manifest.py validation de okf-bundle.yaml
├── locking.py flock() — fd neuf par acquisition
├── gitops.py index git temporaire initialisé depuis HEAD, identité explicite
├── search.py ripgrep, ET strict puis repli OU
├── bootstrap.py installation des bases livrées (bundles/ → bases/)
├── remote_sync.py synchronisation fast-forward-only avec le remote, au démarrage
├── governance.py statut draft/stable d'un GOVERNANCE.md
├── mdutil.py frontmatter, headings, sections
├── frontmatter.py fusion de frontmatter à représentation préservée, ou refus
├── textutil.py plafonnement des sorties
├── review.py moteur du rôle gestionnaire
└── tools/ un module par outil kb_*
Langage, SDK, bibliothèque YAML, accès git, forme du wrapper de verrouillage :
ces choix étaient laissés ouverts par la spec (§ 10.1). Le raisonnement derrière
chacun, la carte détaillée des modules et les parcours d'appel sont dans
docs/ARCHITECTURE.md.
Trois, tous mesurés, documentés et réversibles :
- Déclassement de
index.mdetlog.mddanskb_search— sur un corpus réel de 856 documents, 28 % des résultats étaient des sommaires générés ; 2 % après déclassement. - Synchronisation de l'index git partagé après commit — sans elle,
git statusaffiche toutes les propositions commitées comme supprimées, et l'étape de réconciliation les re-commite, cassant l'invariant d'audit. ruamel.yamlpour fusionner un frontmatter — le principe § 1.7 exige une bibliothèque YAML, pas nommément PyYAML ; réémettre tout le frontmatter avecsafe_dumpreformatait les champs non ciblés (tags: [a, b]repassé en style bloc, timestamp ISO réécrit), ce qui égarait sans erreur les parseurs mono-ligne des corpus réels. La fidélité est vérifiée avant écriture ; ce qui n'est pas vérifiable est refusé.
Motif complet, mesure et manière de les annuler :
docs/ARCHITECTURE.md, section « Écarts assumés ».
uv run pytest -q # tout, dont la boucle de stress
uv run pytest -q -m "not slow" # sans le test deux instances × 25 itérationsCouvre notamment : validation de manifeste et collisions de name, confinement
des chemins et symlinks sortants, repli ET→OU, headings dupliqués et formatés,
plafonnement des sorties, non-destruction du tree, worktree sale, dépôt sans
HEAD, collision d'identifiant, tentatives d'injection de trailers, exclusion
mutuelle entre okf-lock et le serveur, et deux instances proposant en
parallèle.
tests/test_boucle_contribution.py déroule le critère d'acceptation de la
rév. 4.1 : dépôt d'une proposition par un vrai client MCP en stdio, résolution
par okf-review, puis relecture du verdict — intégration ou motif de rejet —
toujours en MCP seul. Il tourne sur une copie du bundle okf-hub-feedback
réellement déployé quand il est présent.
Extensions tools/skills ; review: agent|auto ; validation automatique de
schéma ; authentification des contributeurs ; politique d'incrément de version ;
index de recherche dérivé ; revue d'import outillée ; synchronisation remote ;
multi-hub. Reporté en v1 optionnelle : kb_search multi-bases.
Apache 2.0. Voir NOTICE.
La spécification transcrite dans docs/ est de David Morvan et suit la même
licence. Le format OKF auquel le hub se réfère est publié séparément par
Google Cloud Platform, également sous Apache 2.0.
CONTRIBUTING.md — et lire d'abord la section « Ce qui ne se
négocie pas ».
Vulnérabilité : SECURITY.md, advisory privée, jamais d'issue
publique.