README
¶
claude-distant — client
Agent Go portable lancé sur le PC distant, en deux variantes de binaire
(console, le livrable par défaut, et GUI, voir plus bas) construites
depuis la même base de code. Il se connecte en sortant au relay, affiche un
code de session à 9 chiffres, puis exécute les commandes envoyées par le
harnais (via le relay) en respectant une politique de garde-fou locale. Voir
docs/PROTOCOL.md à la racine du dépôt pour le protocole complet.
Build
Go 1.22+ requis. Aucune dépendance système autre que le module Go
github.com/gorilla/websocket (WebSocket) — le reste n'utilise que la
bibliothèque standard (y compris system_info, sans dépendance lourde type
gopsutil).
Depuis client/ :
go build ./... # build natif (vérification rapide)
go vet ./... # analyse statique
go test ./... # tests unitaires
Build cross-plateforme (binaires de distribution)
Voir Makefile et docs/PACKAGING.md pour la
procédure complète (build reproductible, checksums, signature, prérequis
GUI). En bref :
make dist # console : linux/amd64, linux/arm64, windows/amd64 -> dist/
make dist-gui # GUI : linux/amd64, windows/amd64 -> dist/ (voir "Interface graphique" ci-dessous)
make checksums # dist/SHA256SUMS -- couvre les deux familles
RELAY_URL/CLIENT_TOKEN/IDENTITY_SECRET (vides par défaut, voir
Makefile) personnalisent ces builds pour produire un binaire qui se lance
sans argument (§ « Binaire personnalisé » plus bas).
Équivalent manuel pour une seule cible (ex. Linux amd64) :
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
-trimpath -ldflags "-s -w -X main.version=$(git describe --tags --always)" \
-o dist/claude-distant-client-linux-amd64 .
Le binaire résultant est autonome (statique, CGO_ENABLED=0, strippé) : il
peut être copié et lancé directement depuis un dossier temporaire sur la
machine cible, sans installation, service, clé de registre ni autostart.
Utilisation
./claude-distant-client \
--url wss://relay.example.com/ws/client \
--token <CLIENT_TOKEN>
Sans --policy, le client démarre en auto : les commandes envoyées par le
harnais s'exécutent sans invite de confirmation. Ajouter --policy confirm
pour valider chaque opération sensible une par une (voir « Politique de
garde-fou » ci-dessous).
Binaire personnalisé : lancement sans argument
Un binaire compilé avec RELAY_URL/CLIENT_TOKEN (voir Makefile et
docs/PACKAGING.md) embarque ces valeurs au link (-ldflags -X main.buildRelayURL=... -X main.buildClientToken=..., client/buildconfig.go)
et se lance alors sans le moindre argument :
./claude-distant-client
parseConfig résout chaque réglage dans l'ordre flag > variable
d'environnement > valeur compilée, le premier non vide gagnant ; l'erreur
« --url (ou CLAUDE_DISTANT_URL) est requis » n'est levée que si les
trois sources sont vides. Un binaire générique (ces variables laissées
vides à la compilation, le cas par défaut de make dist/make dist-gui) se
comporte donc exactement comme avant : --url/--token restent requis. Un
flag ou une variable d'environnement passés explicitement l'emportent
toujours sur la valeur compilée, y compris sur un binaire personnalisé — utile
pour rediriger ponctuellement un binaire déjà distribué vers un autre relay
sans le recompiler.
Sécurité, sans détour : une valeur compilée via -ldflags -X n'est ni
chiffrée ni obfusquée — un simple strings sur le binaire la récupère en
clair (client/buildconfig.go). Le binaire personnalisé lui-même EST donc un
secret à traiter comme tel (voir docs/SECURITY.md).
Flags / variables d'environnement
| Flag | Env | Défaut | Description |
|---|---|---|---|
--url |
CLAUDE_DISTANT_URL |
— (requis, sauf binaire personnalisé) | URL WebSocket du relay, ex. wss://relay.example.com/ws/client |
--token |
CLAUDE_DISTANT_TOKEN |
— (requis, sauf binaire personnalisé) | Jeton Bearer pré-configuré |
--policy |
CLAUDE_DISTANT_POLICY |
auto |
Garde-fou : auto | confirm | deny |
--insecure-skip-verify |
— | false |
Désactive la vérification TLS (dev uniquement, jamais en production) |
--remove-on-exit |
CLAUDE_DISTANT_REMOVE_ON_EXIT |
false |
À l'arrêt propre, supprime aussi le binaire lui-même (best-effort). Voir docs/PACKAGING.md §1 |
--ephemeral-code |
CLAUDE_DISTANT_EPHEMERAL_CODE |
false |
Revient au code de session aléatoire à chaque connexion (comportement d'avant l'adresse stable) au lieu du code stable dérivé de la machine. Voir « Adresse stable » ci-dessous |
Un flag l'emporte toujours sur la variable d'environnement correspondante.
CLAUDE_DISTANT_REMOVE_ON_EXIT/CLAUDE_DISTANT_EPHEMERAL_CODE acceptent
1/true/yes/on (insensible à la casse) comme valeurs activantes. Il
n'y a délibérément pas de --identity-secret : accepter ce secret en
ligne de commande viderait de son sens le fait de le compiler dans un binaire
personnalisé (IDENTITY_SECRET, voir Makefile) — seule la valeur compilée
(ou, à défaut, le sel par défaut du projet) est utilisée.
Adresse stable
Par défaut (sans --ephemeral-code), le client redemande à chaque connexion
le même code de session à 9 chiffres, dérivé de façon déterministe de
l'identifiant de la machine (client/identity.go:MachineID —
/etc/machine-id sur Linux, MachineGuid de la registry sur Windows) et
d'un secret (IDENTITY_SECRET compilé, ou un sel par défaut du projet sinon).
Le relay honore ce code souhaité (desired_code du register) s'il est
encore libre, sinon retombe sur un code aléatoire pour cette session — voir
docs/PROTOCOL.md pour le détail complet (dérivation, TTL/heartbeat
inchangés, course à la reconnexion). Ni le TTL ni le heartbeat ne changent :
seule l'adresse est stable, pas la durée de vie de la session.
Au démarrage, le client :
- se connecte au relay et envoie
register(OS détecté, hostname, version) ; - affiche le code de session reçu (
registered), formaté784 123 678; - exécute en boucle les
commandreçus (run_shell,run_command,system_info), streame stdout/stderr, puis renvoieresult, en journalisant chaque requête reçue à l'écran (voir « Journal d'activité » ci-dessous) ; - envoie un
heartbeattoutes les 20 s ; - se reconnecte automatiquement (backoff exponentiel + jitter, 1s→30s) si la connexion tombe, jusqu'à interruption (Ctrl-C / SIGTERM), gérée proprement (fermeture de la connexion WebSocket puis arrêt).
Journal d'activité (console)
Le client console affiche, sous le code de session, une ligne par événement :
chaque commande reçue du harnais avec son contenu, puis les
lectures/écritures de fichiers, les décisions d'approbation et les résultats.
C'est le pendant console du panneau « Journal d'activité » de la GUI, au même
format (commanddetail.go:describeCommand, consolelog.go) :
========================================
Code de session : 784 123 678
Communiquez ce code à l'opérateur.
========================================
Journal d'activité — chaque commande reçue du harnais s'affiche ici :
13:21:31 [command] system_info (request a1b2)
13:21:31 [result] request a1b2: exit=0
13:21:33 [command] run_shell : uname -a [timeout=30s] (request c3d4)
13:21:33 [result] request c3d4: exit=0
13:21:34 [command] read_file : /etc/hostname (request e5f6)
13:21:34 [file_read] /etc/hostname (3 octets)
13:21:34 [result] request e5f6: exit=0
Sans ce journal, un client lancé avec la politique auto (le défaut)
n'afficherait rien du tout de ce que fait le harnais : la sortie des
commandes est streamée vers le relay, pas vers ce terminal, et aucune invite
de confirmation ne vient les annoncer. Détails de rendu :
- une commande multi-lignes tient sur une seule ligne, les sauts de ligne
étant rendus par un
\nlittéral (les coller sur une même ligne avec un simple espace ferait lire deux commandes comme une seule) ; - les caractères de contrôle sont retirés (une séquence ANSI reçue du harnais ne doit pas piloter le terminal de l'utilisateur) ;
- au-delà de 400 caractères, la commande est tronquée avec un marqueur indiquant combien de caractères ont été coupés ;
- le contenu d'un
write_filen'est jamais journalisé, seulement sa taille — même choix que l'audit du relay, qui rédigecontent_base64.
Politique de garde-fou (--policy)
auto(défaut) : toutes les commandes s'exécutent sans confirmation, y comprisread_file/write_file. C'est le mode « le harnais travaille sans interrompre l'opérateur » : aucune invite locale n'est affichée, aucuneapproval_responsen'est émise. Le garde-fou reste disponible à tout moment via--policy confirm/deny, ou l'interrupteur « mode automatique » de la GUI (à décocher pour repasser enconfirmà chaud).confirm: les commandes classées destructives déclenchent une invite localeLe harnais veut exécuter : <commande> [Autoriser/Refuser]et attendent la réponse de l'opérateur avant exécution. Un refus renvoieresultavecerror:"refused_by_user".deny: les commandes destructives sont automatiquement refusées (error:"refused_by_policy"), sans invite.
La classification "destructive" (policy.go, destructivePatterns) est une
liste simple et extensible d'expressions régulières couvrant entre autres :
rm -rf/rm -fr, Remove-Item -Recurse/-Force, mkfs, dd ... of=,
wipefs, fdisk/parted, diskpart, format, écriture directe sur
/dev/sd*, shutdown/reboot/poweroff/Restart-Computer,
userdel/deluser, reg delete, iptables -F, et le fork bomb classique
:(){ :|:& };:. Pour l'étendre, ajouter une entrée à destructivePatterns
dans policy.go.
Outils exécutés côté client
run_shell— exécuteparams.commanddans un interpréteur choisi viaparams.shell:auto(défaut) : PowerShell (pwshsi présent, sinonpowershell) sur Windows ; Bash sur Linux.- overrides explicites :
powershell,pwsh,bash,sh. - stdout/stderr sont streamés séparément (
streammessages) ; PowerShell est forcé en UTF-8 (entrée/sortie) pour un décodage correct quel que soit le code page actif de la console.
run_command— exécute une commande simple sans shell (argv splitté en respectant guillemets simples/doubles et échappements).system_info— OS, hostname, uptime, CPU (nombre de cœurs), RAM totale/disponible (Mo). Implémenté sans dépendance lourde :/proc/uptimeet/proc/meminfosur Linux,GetTickCount64/GlobalMemoryStatusExdekernel32.dllviasyscallsur Windows.read_file/write_file(filetransfer.go) — transfert de fichiers, réservé aux relays qui les dispatchent (le client annonce la capacitéfile_transferà sonregister) :- plafond de 8 Mio par transfert, dans les deux sens ;
read_filedécoupe le fichier en tranches de 192 Kio brutes (≈256 Kio en base64) envoyées en plusieurs messagesfile_chunknumérotés parseq, et supporteoffset/max_bytes(troncature signalée partruncated) ;write_fileinstalle le contenu de façon atomique : fichier temporaire dans le même répertoire que la cible, puisrename— un échec à n'importe quelle étape laisse la cible préexistante intacte et ne laisse aucun temporaire derrière lui ; un chemin final qui est un lien symbolique est remplacé par un fichier régulier (conséquence durename), pas suivi ;- erreurs stables portées par
result.error(file_not_found,permission_denied,is_a_directory,file_exists,file_too_large,invalid_base64,invalid_params,io_error), classifiées viaerrors.Issur les sentinellesfs.Err*plutôt que par correspondance de texte (dépendante de l'OS/la locale) ; - comme
run_shell/run_command, systématiquement soumis au garde-fou local : enconfirm, chaque lecture ou écriture déclenche une invite (read_file <chemin>/write_file <chemin> (<n> octets)), sans passer par la classification « destructif » ; endeny, toujours refusés. Voirdocs/PROTOCOL.md§4 pour le détail protocolaire complet.
Les deux outils run_shell/run_command respectent params.timeout
(secondes) : au dépassement, le process (et son arbre de sous-processus) est
tué (SIGKILL + groupe de processus sur Linux, taskkill /T /F sur
Windows), et result est renvoyé avec error:"timeout".
Interface graphique (GUI)
En plus du binaire console (le livrable par défaut, utilisable partout — y
compris sans affichage), make dist-gui produit une variante avec fenêtre
Fyne (client/gui.go, tag de build gui), destinée aux postes de bureau
Windows/Ubuntu où un opérateur humain préfère une fenêtre à un terminal. Le
point d'entrée commun est runUI : client/gui_stub.go (tag !gui) le
délègue tel quel à la boucle console historique, client/gui.go (tag gui)
en fournit la version Fyne. Voir docs/PACKAGING.md pour le pourquoi du
dédoublement et les prérequis de build.
Dette technique connue : la variante GUI porte sa propre boucle de connexion (
guiRunForever/guiRunSession), qui duplique celle demain.go. Elle appelle les mêmes primitives de protocole (DialRelay,resolveDesiredCode,heartbeatLoop…), mais garde unPolicyControlleret unActivityLogvivants d'une reconnexion à l'autre, ce que la boucle console ne fait pas. Conséquence à garder en tête : une évolution du protocole doit être répercutée aux deux endroits, sous peine de divergence silencieuse entre les deux variantes.
À l'usage, la fenêtre GUI reprend les mêmes informations et décisions que la console, sous une autre forme :
- En-tête : le code de session en très gros caractères, groupé
784 123 678, avec un bouton « Copier » ; l'état de la connexion (connecté / reconnexion en cours / déconnecté) ; le hostname et l'OS de la machine. - Mode automatique : un interrupteur « Mode automatique — ne plus
demander de confirmation » bascule la politique de garde-fou de
confirmversautoen cours de session (et inversement), sans redémarrer le client. Il reflète la politique de lancement, donc coché par défaut (--policyvalantautoen l'absence de flag) ; le décocher repasse la session enconfirmà chaud. Un avertissement visible et permanent reste affiché tant qu'il est actif : plus aucune commande ni opération fichier (read_file/write_filecompris) n'est alors soumise à confirmation locale. C'est une décision de l'utilisateur du poste, à prendre en connaissance de cause — voirdocs/SECURITY.md. - Confirmation : en mode
confirm, une boîte de dialogue Fyne remplace l'invite console, avec les mêmes trois réponses — Autoriser, Refuser, Toujours (mémorisation de cette commande exacte pour le reste de la session, comme en console). - Journal d'activité : en bas de fenêtre, le journal d'activité en
direct (connexions, commandes, lectures/écritures de fichiers,
approbations, résultats), redimensionnable à la souris. Chaque commande
reçue du harnais y est affichée avec son contenu — la commande shell
elle-même, le chemin lu ou écrit — et non le seul nom de l'outil
(
commanddetail.go:describeCommand) : c'est ce qui permet de voir ce que fait le harnais, en particulier sous la politiqueautopar défaut, où aucune boîte de confirmation ne l'affiche plus. Détails :- une ligne par requête :
13:42:07 [command] run_shell : systemctl status nginx [timeout=30s] (request a1b2); - une commande multi-lignes est repliée sur une seule ligne, les caractères de contrôle sont retirés, et une commande très longue est tronquée avec un marqueur indiquant combien de caractères ont été coupés ;
- le contenu d'un
write_filen'est jamais journalisé, seulement sa taille — même choix que l'audit du relay, qui rédigecontent_base64; - clic sur une ligne : ouvre le détail complet (horodatage absolu, texte replié sur plusieurs lignes) avec un bouton « Copier » ;
- case « Commandes du harnais uniquement » : filtre le journal sur les seules requêtes reçues, en masquant approbations, résultats et messages de connexion. C'est un filtre d'affichage : rien n'est perdu, décocher la case fait tout réapparaître, et « Enregistrer le log… » écrit de toute façon l'historique complet ;
- bouton « Agrandir » (bascule vue réduite / plein écran) et bouton « Enregistrer le log… » qui écrit l'historique complet de la session dans un fichier choisi par l'utilisateur (horodatages RFC3339 complets, là où l'affichage se limite à l'heure pour laisser la place à la commande).
- une ligne par requête :
Le binaire GUI accepte les mêmes flags/variables d'environnement que la
console (tableau ci-dessus), y compris le lancement sans argument d'un
binaire personnalisé (RELAY_URL/CLIENT_TOKEN compilés) — double-clic et
c'est parti, sans terminal à ouvrir.
Structure du code
| Fichier | Rôle |
|---|---|
main.go |
flags/env (parseConfig), boucle de connexion/reconnexion, affichage du code de session, orchestration de l'arrêt propre |
buildconfig.go |
buildRelayURL/buildClientToken/buildIdentitySecret : valeurs injectées au link par un build personnalisé (Makefile) |
identity.go (+ identity_linux.go, identity_windows.go) |
MachineID/DeriveSessionCode : adresse stable (§ « Adresse stable » ci-dessus) |
wsconn.go |
connexion WebSocket (dial, JSON I/O thread-safe, deadlines) |
executor.go |
exécution run_shell/run_command (sélection d'interpréteur, streaming, timeout, répertoire de travail = workspace) |
filetransfer.go |
exécution read_file/write_file (chunking en lecture, écriture atomique, codes d'erreur stables) |
sysinfo.go (+ sysinfo_linux.go, sysinfo_windows.go) |
system_info cross-plateforme |
proc_linux.go, proc_windows.go |
démarrage/arrêt de l'arbre de processus par OS |
policy.go |
garde-fou local : classification destructive, invite confirm, PolicyController (bascule auto/confirm à chaud, utilisé par la GUI) |
activitylog.go |
ActivityLog : journal d'activité borné, thread-safe, alimente le panneau « journal » de la GUI et le bouton « Enregistrer le log… » |
commanddetail.go |
rendu d'une commande reçue en une ligne de journal lisible (describeCommand) : résumé par outil, repli sur une ligne, retrait des caractères de contrôle, troncature marquée |
consolelog.go |
ConsoleActivityLog : pendant console du panneau « journal » de la GUI — écrit chaque événement sur la sortie standard, sérialisé entre goroutines |
protocol.go |
types Go des messages du protocole |
workspace.go |
répertoire de travail temporaire dédié (NewWorkspace/Cleanup), « sans résidu » |
lifecycle.go |
RunGuarded : garantit le nettoyage à la sortie, y compris sur panic |
secrets.go |
SecretBytes : effacement best-effort des secrets (token) en mémoire |
cleanup_binary.go |
--remove-on-exit : suppression best-effort du binaire lui-même à l'arrêt |
gui_stub.go (!gui) / gui.go (gui) |
point d'entrée commun (runUI) : stub console par défaut, fenêtre Fyne derrière -tags gui (§ « Interface graphique » ci-dessus) |
*_test.go |
tests unitaires (sérialisation protocole, sélection de shell, classification destructive, parsing des flags, workspace/remove-on-exit/secrets, adresse stable, journal d'activité) |
Sans résidu
Le client ne s'installe pas : pas de service, pas de clé de registre, pas
d'autostart. Voir docs/PACKAGING.md pour le détail complet du modèle
« sans résidu » et le build portable ; en résumé :
- Aucun log sur disque par défaut : toute la sortie va sur la console (stdout/stderr) uniquement.
- Répertoire de travail temporaire dédié (
workspace.go,os.MkdirTemp), utilisé comme répertoire de travail des commandes exécutées (run_shell/run_command), supprimé intégralement à la sortie — arrêt propre, Ctrl-C/SIGTERM, ou panic (lifecycle.go:RunGuarded). - Secrets effacés en mémoire (
secrets.go:SecretBytes.Zero()) : le jeton Bearer n'est jamais stocké enstringGo immuable, et est écrasé par des zéros à la sortie. --remove-on-exit(désactivé par défaut) : supprime aussi le binaire lui-même à l'arrêt (best-effort ; direct sous Linux, via un script détaché sous Windows — voircleanup_binary.goetdocs/PACKAGING.md§1).
Un arrêt (Ctrl-C ou signal) ferme proprement la connexion WebSocket (frame de fermeture) avant de quitter.