Skip to content

Latest commit

 

History

History
486 lines (347 loc) · 21.3 KB

File metadata and controls

486 lines (347 loc) · 21.3 KB

git-blinder-mcp — Spécification

Un MCP local. Plusieurs périmètres. Plusieurs agents en parallèle. Chacun ne voit que sa fenêtre.

Version 0.4 · Date 2026-08-11 · Statut Alignée sur l’implémentation actuelle (allowlist + binding URL + historique filtré + surface ops /mcp/root + /api Bearer)


1. Intention

Donner à un agent un accès GitHub borné, sans lui donner le token, et sans lui montrer ce qui existe hors de son cadre.

git-blinder-mcp est un programme local qui :

  1. s’authentifie auprès de GitHub avec un token ;
  2. expose un serveur MCP HTTP (multi-path) ;
  3. laisse l’opérateur définir plusieurs périmètres (outils + chemins + branches autorisées) ;
  4. permet à plusieurs agents de travailler en même temps, chacun collé à un périmètre et à une branche figée par l’URL de connexion.

L’agent ne doit jamais tenir le token GitHub. Il ne doit jamais découvrir, par erreur ou par sondage, ce qui est hors périmètre. Il ne gère pas les branches : l’orchestrateur choisit la branche en connectant l’agent à la bonne URL.


2. Scénario cible

Token GitHub: git_pat-…

                    ┌──────────────────────────────────────────────┐
                    │         git-blinder-mcp (1 process)          │
 Agent A ──/mcp/infra/feat-aws──►│  périmètre infra                  │──► GitHub
                    │  read  infra/**                              │
                    │  write infra/aws/**                          │
                    │  branches: [main, feat-aws]                  │
                    │                                              │
 Agent B ──/mcp/landing─────────►│  périmètre landing (→ main)       │
                    │  read/write apps/landing/**                  │
                    │  branches: [main]                            │
                    └──────────────────────────────────────────────┘
  • L’opérateur a allowlisté feat-aws sur infra (après création de la branche sur GitHub).
  • Agent A est branché sur /mcp/infra/feat-aws : il lit/écrit du contenu sur cette branche, path-filtré ; pas d’outil de découverte de branches.
  • Agent B est branché sur /mcp/landing : branche par défaut du dépôt.
  • Les deux passent par le même process, en parallèle, sans mélange.

3. Concepts

3.1 Token

Un secret GitHub (PAT ou équivalent) détenu uniquement par git-blinder-mcp. Il définit le plafond absolu de ce que le process peut faire. Fourni via --token ou GITHUB_TOKEN au serve — jamais dans la config versionnable.

Double rôle (V0) : le même secret sert de Bearer ops pour /api/* et pour les commandes CLI opérateur (perimeter*, repos, branches). Les endpoints MCP périmètre (/mcp/<id>[/<branch>]) ne sont pas authentifiés par Bearer — l’isolation repose sur l’URL (capability-by-path). Les URLs opaques / secrets par périmètre sont hors V0.

3.2 Périmètre

Une fenêtre découpée dans ce plafond. Un périmètre déclare :

Champ Rôle
id Identifiant URL-safe ([a-z0-9-]+), unique ; root est réservé (surface ops)
repo Dépôt cible (owner/name)
tools Sous-ensemble d’outils autorisés (optionnel ; sinon catalogue agent)
read Globs de chemins lisibles (relatifs à la racine du dépôt)
write Globs de chemins modifiables (relatifs à la racine)
default_branch Branche par défaut du dépôt (résolue à la création)
branches Allowlist de branches auxquelles l’orchestrateur peut binder

read et write sont indépendants. Écrire n’implique pas lire. Lire n’implique pas écrire.

À la création, branches = [default_branch]. L’opérateur ajoute explicitement d’autres branches (vérifiées sur GitHub).

Exemple :

perimeters:
  infra:
    repo: acme/platform
    tools: [get_tree, get_file, get_files, push_files, list_commits, get_commit]
    read:
      - "infra/**"
    write:
      - "infra/aws/**"
    # default_branch / branches sont gérés au runtime (registry)

3.3 Connexion MCP (= binding agent ↔ périmètre × branche)

Une connexion MCP = un périmètre + une branche allowlistée.

URL Branche liée
/mcp/<id> default_branch du périmètre
/mcp/<id>/<branch> <branch> si elle est dans l’allowlist ; sinon 404

Hors binding agent :

URL Rôle
/mcp Health / status (pas d’outils MCP)
/mcp/root Surface ops non filtrée — jamais pour un agent

Changer de branche = déconnecter / reconnecter sur une autre URL. Pas d’état « checkout » côté serveur. Pas d’outil agent pour lister ou créer des branches.

3.4 Rôles

Rôle Décide
Opérateur périmètres, globs, tools, allowlist de branches
Orchestrateur quelle URL MCP (donc quelle branche) donner à l’agent
Agent contenu (lire / écrire / historique filtré) — rien sur les branches

3.5 Session d’exécution

Le contexte runtime d’un agent sur une connexion : périmètre résolu, branche liée, outils exposés, règles de chemins. Plusieurs sessions coexistent dans le même process.


4. Architecture

4.1 Couches

┌──────────────────────────────────────────────┐
│  CLI                                         │  serve, perimeter*, branches, repos
├──────────────────────────────────────────────┤
│  HTTP / MCP transport                        │  /mcp (health), /mcp/root (ops),
│  (Option B : connexion = périmètre×branche)  │  /mcp/<id>[/<branch>], /api/* (Bearer)
├──────────────────────────────────────────────┤
│  Core (pur)                                  │  decide Allow / Deny / Invisible
│  - normalisation chemins                     │
│  - matching globs                            │
│  - table outils                              │
│  - filtre résultats (tree, commits, …)       │
│  - préfixes path pour list_commits           │
├──────────────────────────────────────────────┤
│  Upstream GitHub (Octokit)                   │  token + appels API
└──────────────────────────────────────────────┘

Règle : le Core ne sait pas comment le périmètre / la branche ont été choisis. Il reçoit (perimeter, tool, args) (éventuellement déjà bound) et rend un verdict. Zéro I/O réseau dans le Core.

4.2 Binding — Option B (implémenté)

Client ──► /mcp/infra           ──► Session(perimeter=infra, branch=main)
Client ──► /mcp/infra/feat-aws  ──► Session(perimeter=infra, branch=feat-aws)
Client ──► /mcp/root            ──► Ops non filtré (pas pour agents)
Client ──► /mcp                 ──► Health only (pas d’outils)

4.3 Process model

Élément Cardinalité
Process git-blinder-mcp 1 par token (local)
Token GitHub 1 par process
Périmètres N (registry en mémoire + option YAML au boot)
Connexions MCP actives N (une URL = une session)
Agents N, en parallèle

5. CLI (implémentée)

git-blinder serve --token "$GITHUB_TOKEN" [--port 7400] [--config config.yaml]

git-blinder tools

git-blinder repos list [--url …] [--token …]
git-blinder branches list --repo owner/repo [--url …] [--token …]

git-blinder perimeter create <id> --repo owner/repo --read <glob> [--write <glob>] [--tools a,b,c] [--url …] [--token …]
git-blinder perimeter list|show|delete <id> [--url …] [--token …]

git-blinder perimeter branch list <id>
git-blinder perimeter branch add <id> <branch>     # vérifie existence GitHub
git-blinder perimeter branch remove <id> <branch>  # refuse de retirer default_branch

git-blinder perimeter commits list <id> [--branch <name>]
git-blinder perimeter commits show <id> <sha> [--branch <name>]

Notes :

  • Les commandes perimeter* / repos / branches / commits parlent au serveur déjà up (--url, défaut http://127.0.0.1:7400).
  • Elles authentifient /api/* avec --token ou GITHUB_TOKEN (même secret que serve).
  • branches list et repos list sont des outils opérateur (découverte GitHub), pas la surface agent.
  • perimeter commits * applique le même filtrage que les tools MCP list_commits / get_commit.
  • Id root refusé à la création (réservé à /mcp/root).

5.1 Configuration

YAML optionnel au boot (--config) pour précharger des périmètres. Le token n’y figure pas.


6. Modèle de sécurité

6.0 Surfaces et confiance (V0)

Surface Auth Public cible
GET /mcp aucune status opérateur (ids de périmètres)
POST /mcp interdit (405) — ne sert plus d’outils
/mcp/root aucune* ops / debug — catalogue non filtré
/mcp/<id>[/<branch>] aucune* agent — scoped
/api/* Authorization: Bearer <token serve> CLI / ops uniquement

* Capability-by-path : connaître l’URL suffit. Un agent avec shell sur la même machine peut encore sonder /mcp/root ou d’autres ids — ce n’est pas un sandbox OS. V0 ferme le piège « POST /mcp = tout voir » et protège la surface d’administration /api. Suites possibles : secret de session, URL opaque par périmètre, Host/Origin hardening.

6.1 Absence, pas refus (lecture)

Hors périmètre en lecture :

  • l’outil non autorisé / non exposé → absent de tools/list / « Not found » à l’appel ;
  • le chemin non lisible → « not found », pas « access denied » ;
  • la branche hors allowlist → 404 sur l’URL de binding (l’agent ne la voit jamais).

6.2 Refus explicite (écriture)

Écriture hors write → erreur claire. Aucun appel GitHub partiel.

6.3 Tout ou rien

Si un appel touche plusieurs chemins (ex. push_files) et qu’un seul sort du write, l’appel entier est rejeté avant GitHub.

6.4 Deny-by-default sur les outils

Un outil n’est exposable que si git-blinder-mcp sait extraire / borner les chemins et filtrer la sortie. Sinon : non listé.

6.5 Normalisation des chemins

Avant tout matching, normalisation stricte (rejet si non normalisable) : . / .., pas d’absolu, casse sensible, etc. Logique unique et pure.

6.6 Binding branche

Sur /mcp/<id>[/<branch>] :

  • owner / repo / ref / branch (écriture) sont forcés par le binding ;
  • les schémas agent omettent ces champs quand c’est pertinent ;
  • l’agent ne peut pas pivoter vers une autre branche via les arguments d’outil.

7. Politique Git — allowlist + binding (implémentée)

Principe : l’opérateur autorise les branches ; l’orchestrateur en choisit une à la connexion ; l’agent ne voit que du contenu path-filtré sur cette branche.

7.1 Allowlist (opérateur)

Pour un périmètre P :

  1. À la création : branches = [default_branch du repo].
  2. branch add <name> : échoue si la branche n’existe pas sur GitHub (message clair : la créer d’abord) ; sinon l’ajoute à l’allowlist.
  3. branch remove <name> : interdit pour default_branch ; sinon retire de l’allowlist.

La découverte des branches GitHub se fait via la CLI globale branches list --repo …, pas via l’agent.

7.2 Binding (orchestrateur)

  • /mcp/<id> → branche = default_branch
  • /mcp/<id>/<branch> → autorisé ssi branch ∈ branches ; sinon 404 « not allowed »

7.3 Surface agent — pas de découverte de refs

Sur une connexion périmètre, jamais exposés :

  • list_branches
  • compare

Même s’ils figurent dans le grant tools du YAML, ils sont retirés de la surface agent.

Outils agent typiques : get_tree, get_file, get_files, push_files, list_commits, get_commit.

/mcp/root (non filtré) expose le catalogue complet pour le debug opérateur — ne pas y brancher un agent. POST /mcp ne sert pas d’outils.

7.4 Filtrage du contenu (toujours)

Quelle que soit la branche liée :

Surface Règle
Contenu / tree / blob seuls les paths dans read
Écriture (push_files) seuls les paths dans write ; tout-ou-rien ; commit atomique GitHub
Diff d’un commit (get_commit) seuls les fichiers in-scope + omitted_count
Liste de commits (list_commits) commits touchant un préfixe dérivé de read (voir 7.5)

7.5 Historique filtré

Deux outils :

  1. list_commits — sur la branche liée.
    Préfixe(s) GitHub path= dérivés des globs read (ex. infra/**path=infra).
    Plusieurs globs disjoints → un appel par préfixe, fusion dédupliquée.
    Pas d’enrichissement N+1 de chaque commit.

  2. get_commit — un SHA.
    Charge ce commit ; filtre les fichiers au read ; renvoie omitted_count opaque.
    Commit purement hors-scope → not found (pas de fantôme vide).
    Ne nomme jamais les chemins omis.

La CLI perimeter commits list|show applique les mêmes règles (branche allowlistée via --branch, défaut = default).

7.6 Écriture = commit

Pas d’outil commit séparé. push_files crée un commit GitHub atomique (blobs + tree + commit + avance de la ref de la branche liée).


8. Surface MCP

8.1 Catalogue

Tool Ops /mcp/root Perimeter /mcp/<id>[/<branch>] POST /mcp
get_tree oui oui (si grant)
get_file oui oui
get_files oui oui
push_files oui oui
list_commits oui oui (filtré path + branche liée)
get_commit oui oui (filtré fichiers)
list_branches oui non
compare oui non

POST /mcp405 (message pointant vers /mcp/root ou /mcp/<id>).

8.2 Ce que voit un agent

  • tools/list → outils du périmètre ∩ outils agent (sans list_branches / compare) ;
  • tools/call → revalidation (decide) + binding repo/branche + filtre de sortie ;
  • résultats → élagués au read.

8.3 Filtrage de sortie (obligatoire)

  • tree / fichiers → hors read retirés ;
  • commits purement hors-scope → absents / not found ;
  • commits mixtes → fichiers in-scope + omitted_count ;
  • compteurs recalculés sur le sous-ensemble visible.

8.4 Concurrence

Plusieurs sessions sans mélange : pas d’état mutable partagé non synchronisé entre sessions ; token amont partagé, verdicts non.


9. Endpoints HTTP

Méthode Path Rôle
GET /mcp Status + liste des ids de périmètres (pas d’outils)
POST /mcp 405 — outils absents
GET/POST /mcp/root MCP ops non filtré (root id réservé)
GET/POST /mcp/<id> Status / MCP bound à default_branch
GET/POST /mcp/<id>/<branch> Status / MCP bound à la branche allowlistée
* /api/* 401 sans Bearer ops (= token de serve)
GET/POST /api/perimeters List / create (root → 400 reserved)
GET/DELETE /api/perimeters/:id Show / delete
GET/POST/DELETE /api/perimeters/:id/branches[/:name] Allowlist
GET /api/perimeters/:id/commits[/:sha] Historique filtré
GET /api/repos Repos visibles du token
GET /api/repos/:owner/:repo/branches Branches GitHub (ops)

10. Flux de bout en bout

1. Opérateur: git-blinder serve --token …
2. Opérateur: git-blinder repos list / branches list --repo …
3. Opérateur: git-blinder perimeter create infra --repo … --read 'infra/**' --write 'infra/aws/**'
4. Opérateur: crée la branche sur GitHub si besoin, puis
              git-blinder perimeter branch add infra feat-aws
5. Orchestrateur: branche l’agent sur http://127.0.0.1:7400/mcp/infra/feat-aws
6. Agent: tools/list → pas de list_branches / compare
7. Agent: list_commits → historique path-filtré sur feat-aws
8. Agent: get_commit <sha> → fichiers in-scope + omitted_count
9. Agent: push_files → commit atomique sur feat-aws, borné par write
10. Agent B sur /mcp/landing en parallèle, zéro croisement

11. Non-objectifs (v1 actuelle)

  • UI web d’administration
  • Multi-tokens dans un même process
  • Multi-dépôts dans un même périmètre
  • Quotas / rate-limit produit (hors limites GitHub)
  • Mode distant / multi-hôte
  • Autres backends que GitHub
  • Binding Option A (multiplex par call)
  • Approche C (visibilité branches par intersection de diff) pour les agents — abandonnée au profit de l’allowlist + binding URL
  • Outils PR dédiés
  • Création de branche par l’agent

12. Décisions actées

# Décision Motif
1 1 process MCP local = 1 token GitHub Surface de contrôle unique
2 N périmètres par process Exigence produit
3 Agents simultanés sur le même process Exigence produit
4 Option B : URL /mcp/<id>[/<branch>] Isolation simple, clients MCP HTTP
5 Core isolé du binding Évolutivité
6 read / write indépendants Périmètre par verbe
7 Absence en lecture, refus explicite en écriture Ne rien révéler / ne pas mentir
8 Branches = allowlist opérateur + binding URL L’agent ne gère pas Git refs
9 Agents : pas de list_branches / compare Pas de surface découverte de branches
10 list_commits via path= dérivé du read Filtre côté GitHub, coût maîtrisé
11 get_commit + omitted_count opaque Honnêteté sans élargir le contexte
12 push_files = commit atomique Pas de commit local séparé
13 CLI d’abord, pas d’UI Simplicité opératoire
14 Token hors config versionnable Ne jamais committer le secret
15 Globs relatifs à la racine du dépôt repo est déjà un champ du périmètre
16 Outils non filtrés sur /mcp/root, pas /mcp Évite le piège agent → POST /mcp
17 Id root réservé Pas de collision avec la surface ops
18 /api/* protégé par Bearer (= token serve) CLI / admin hors portée agent naïve
19 MCP périmètre sans Bearer (V0) Simplicité clients MCP HTTP ; risque ids devinables accepté

13. Questions ouvertes / suites

  1. PRs filtrées — même modèle que get_commit / list, non livré.
  2. Stdio spawner — shim par périmètre devant le même daemon ; Core inchangé.
  3. Persistance allowlist — aujourd’hui registry mémoire (+ YAML boot) ; persistance runtime à préciser.
  4. Globs sans préfixe statique (**, *.md) — list_commits renvoie liste vide (pas de path= utilisable) ; politique à affiner si besoin.
  5. Auth MCP plus forte — secret de session, URL opaque par périmètre, validation Host/Origin (voir AUDIT C1).
  6. /mcp/root opt-in — éventuellement derrière un flag si on veut le couper en prod locale.

14. Critères d’acceptation (smoke)

Un incrément n’est « bon » que si, à la main :

  1. Deux périmètres disjoints existent.
  2. Deux agents y sont branchés en même temps (URLs distinctes).
  3. Chacun ne liste que ses outils agent (sans list_branches / compare).
  4. /mcp/<id>/<branch> hors allowlist → 404 ; branche allowlistée → binding OK.
  5. list_commits / perimeter commits list n’expose pas les commits purement hors read.
  6. get_commit mixte → fichiers in-scope + omitted_count ; hors-scope pur → not found.
  7. Une écriture hors write échoue clairement et n’atteint pas GitHub.
  8. Aucune réponse de A n’arrive chez B (et inversement).
  9. POST /mcp → 405 ; POST /mcp/root liste les 8 outils (ops).
  10. /api/perimeters sans Bearer → 401 ; avec le token de serve → OK.
  11. perimeter create root … → rejeté (id réservé).

Contrats exécutables : features/cli/*.feature, features/support/01_api_perimeters.feature.


Annexe — Vocabulaire

Terme Sens
Token Credential GitHub du process (= Bearer ops V0)
Ops token Même secret, présenté en Authorization: Bearer sur /api/*
Périmètre Fenêtre outils + paths + allowlist branches
Connexion / binding Canal MCP lié à un périmètre et une branche
Session État runtime d’un agent sur une connexion
Core Moteur de décision pur
Allowlist Branches explicitement autorisées sur un périmètre
Marqueur opaque omitted_count sans nommer les paths hors scope
/mcp/root Surface non filtrée (ops) — pas pour agents
Capability-by-path Connaître /mcp/<id> suffit pour s’y connecter (V0)

Annexe — Nom

GitBlinder : des blinders (œillères) pour un agent GitHub via MCP.

Usage Nom
Projet GitBlinder
Repository git-blinder-mcp
Package npm / CLI git-blinder

Accroche : Blinders for GitHub agents — one MCP, many perimeters.