v0.9.1 : le poste distant est integre a ajean (fini le binaire ajean-remote separe)

- nouvelle sous-commande `ajean remote install|connect|run|status|uninstall|logout`
  (reutilise internal/nodeclient), remplace le binaire cmd/ajean-remote supprime
- le service (Windows/systemd/launchd) lance desormais `ajean remote run`
- UI Postes distants : commande affichee `ajean remote install ...`
- CI release : retrait du cross-build des 6 binaires ajean-remote
This commit is contained in:
nathaninline committed 2026-08-14 09:24:40 +02:00
1 parent 1454dff2a7
commit 88d1e79e80
12 files changed
+97 -98

No files matched your search

-12
View File
@@ -114,18 +114,6 @@ jobs:
GOOS=$os GOARCH=$arch CGO_ENABLED=0 \
go build -trimpath -ldflags="$ldflags" -o "dist/$name" ./cmd/ajean
done
# Client léger « poste distant » ajean-remote : pas de CGO (ni UI ni
# systray), donc les 6 cibles se cross-compilent ici, macOS compris.
# Noms d'asset ajean-remote-<os>[-arm][.exe] — la page /download les
# présente à part (« piloter un autre PC »).
for t in linux/amd64:ajean-remote-linux linux/arm64:ajean-remote-linux-arm \
windows/amd64:ajean-remote-windows.exe windows/arm64:ajean-remote-windows-arm.exe \
darwin/amd64:ajean-remote-macos darwin/arm64:ajean-remote-macos-arm; do
target=${t%%:*}; name=${t#*:}
os=${target%/*}; arch=${target#*/}
GOOS=$os GOARCH=$arch CGO_ENABLED=0 \
go build -trimpath -ldflags="-s -w" -o "dist/$name" ./cmd/ajean-remote
done
- uses: actions/download-artifact@v4
with:
name: macos
+12 -8
View File
@@ -1,15 +1,19 @@
Piloter un autre PC fonctionne maintenant à distance, à travers ajean.link, sans que le relais ne voie jamais rien.
Le poste distant est maintenant intégré à AJEAN : plus de binaire séparé, plus de téléchargement à part.
## Le poste distant passe par ajean.link, chiffré de bout en bout
## Une seule application
La 0.8.9 a introduit le poste distant, mais uniquement sur le réseau local. Il fonctionne désormais **depuis n'importe où**, via ajean.link — et avec la même exigence que le reste d'ajean.link : le **relais reste aveugle**.
Jusqu'ici, piloter un autre PC demandait d'installer un second binaire, « ajean-remote ». C'était une application de plus à télécharger, à suivre et à mettre à jour. On l'a supprimée : tout vit désormais dans **AJEAN**.
Concrètement, l'identité d'un poste n'est plus une clé partagée mais une **paire de clés** qui lui est propre. À l'appairage, le poste scelle sa demande vers la clé publique de ton serveur : le relais ne voit ni le code, ni rien d'autre. Ensuite, tout ce qui circule entre le poste et ton serveur — les commandes de l'IA comme leurs résultats — est **chiffré de bout en bout** avec une clé que seuls ton serveur et ton poste peuvent calculer. Le relais ne transporte que de l'opaque.
Sur le PC à piloter, tu installes simplement AJEAN, puis tu lances :
## Rien à changer pour toi
```
ajean remote install https://ajean.link --machine <id> --key <clé> --code <code> --allow shell,read,write,list
```
Dans **Réglages → Postes distants**, générer un code affiche maintenant deux commandes : une pour l'**accès à distance** (via ajean.link) et une pour le **réseau local** (connexion directe). Tu choisis, tu copies, tu colles sur l'autre PC.
La commande s'installe en service — invisible, au démarrage, reconnexion automatique — exactement comme avant. Rien ne change au chiffrement : l'accès reste **chiffré de bout en bout**, le relais ajean.link ne voit toujours rien.
## Un client, toujours aussi léger
## Rien à changer dans ton usage
Le petit client s'appelle **ajean-remote** (quelques mégaoctets, aucune interface). Il s'installe en service — invisible, au démarrage, reconnexion automatique — et se télécharge depuis ajean.link, section « Piloter un autre PC ».
Dans **Réglages → Postes distants**, générer un code affiche la commande prête à copier — à distance (via ajean.link) ou en réseau local. Seul le nom de la commande change : `ajean remote install …` au lieu de `ajean-remote install …`.
Les postes déjà appairés continuent de fonctionner sans réappairage.
Binary file not shown.
Binary file not shown.
+3 -3
View File
@@ -3,13 +3,13 @@
"FileVersion": {
"Major": 0,
"Minor": 9,
"Patch": 0,
"Patch": 1,
"Build": 0
},
"ProductVersion": {
"Major": 0,
"Minor": 9,
"Patch": 0,
"Patch": 1,
"Build": 0
},
"FileFlagsMask": "3f",
@@ -25,7 +25,7 @@
"LegalCopyright": "Copyright (c) 2026 AJEAN contributors. MIT License.",
"OriginalFilename": "ajean.exe",
"ProductName": "AJEAN",
"ProductVersion": "0.9.0",
"ProductVersion": "0.9.1",
"Comments": "https://github.com/nathaninline/ajean — projet open source (MIT)"
},
"VarFileInfo": {
@@ -1,13 +1,17 @@
// ajean-remote — client LÉGER « poste distant ». Binaire séparé et minuscule
// (protocole partagé + WebSocket, sans UI ni moteur) : on l'installe sur un PC
// pour que l'IA d'un serveur AJEAN puisse agir dessus.
// Sous-commande « ajean remote » — client « poste distant » intégré au binaire
// principal. On l'installe sur un PC pour que l'IA d'un serveur AJEAN puisse agir
// dessus (shell/fichiers), chiffré de bout en bout via ajean.link (relais aveugle)
// ou en direct sur le réseau local.
//
// ajean-remote install <url-serveur> --code CODE [--allow shell,read,write,list] [--root DIR]
// appaire ce PC et l'installe en service (démarrage auto)
// ajean-remote connect <url> --code CODE [...] même chose mais en avant-plan (test)
// ajean-remote run boucle client (utilisé par le service)
// ajean-remote status | uninstall | logout
package main
// ajean remote install <url-serveur> --machine ID --key CLÉ --code CODE [--allow shell,read,write,list] [--root DIR]
// appaire ce PC et l'installe en service (démarrage auto)
// ajean remote connect <url> ... idem mais en avant-plan (test)
// ajean remote run boucle client (utilisé par le service)
// ajean remote status | uninstall | logout
//
// Historiquement un binaire séparé « ajean-remote » ; fusionné dans ce binaire
// pour n'avoir qu'une seule application.
package ajean
import (
"context"
@@ -19,65 +23,64 @@ import (
"github.com/nathaninline/ajean/internal/nodeclient"
)
func main() {
args := os.Args[1:]
cmd := ""
func cmdRemote(args []string) error {
sub := ""
if len(args) > 0 {
cmd = args[0]
sub = args[0]
args = args[1:]
}
var err error
switch cmd {
switch sub {
case "install":
err = doConnect(args, true)
return remoteConnect(args, true)
case "connect":
err = doConnect(args, false)
return remoteConnect(args, false)
case "run":
err = doRun()
return remoteRun()
case "uninstall":
err = nodeclient.Uninstall()
if err == nil {
fmt.Println("[ok] service retiré")
if err := nodeclient.Uninstall(); err != nil {
return err
}
fmt.Println("[ok] service retiré")
return nil
case "logout":
_ = nodeclient.Uninstall()
err = os.Remove(nodeclient.ConfigPath())
if err == nil || os.IsNotExist(err) {
err = nil
fmt.Println("[ok] configuration oubliée")
err := os.Remove(nodeclient.ConfigPath())
if err != nil && !os.IsNotExist(err) {
return err
}
fmt.Println("[ok] configuration oubliée")
return nil
case "status":
err = doStatus()
return remoteStatus()
default:
usage()
}
if err != nil {
fmt.Fprintln(os.Stderr, "[err]", err)
os.Exit(1)
remoteUsage()
return nil
}
}
func usage() {
fmt.Print(`ajean-remote — client léger « poste distant » pour AJEAN
func remoteUsage() {
fmt.Print(`ajean remote — piloter CE PC depuis l'IA d'un serveur AJEAN
ajean-remote install <url-serveur> --code CODE [options] appaire + installe en service (démarrage auto, invisible)
ajean-remote connect <url-serveur> --code CODE [options] idem en avant-plan (pour tester)
ajean-remote status état de la configuration
ajean-remote uninstall retire le service (garde la config)
ajean-remote logout retire le service ET oublie la clé
ajean remote install <url-serveur> --code CODE [options] appaire + installe en service (démarrage auto, invisible)
ajean remote connect <url-serveur> --code CODE [options] idem en avant-plan (pour tester)
ajean remote status état de la configuration
ajean remote uninstall retire le service (garde la config)
ajean remote logout retire le service ET oublie la clé
Options :
--code CODE code d'appairage (généré dans l'UI AJEAN → Postes distants)
--allow LISTE capacités acceptées : shell,read,write,list (défaut : ce que le serveur autorise)
--root DIR dossier autorisé (confine lecture/écriture)
--yes auto-approuve les actions (obligatoire pour le service, sans terminal)
--machine ID machine à joindre via ajean.link (fournie par l'UI → Postes distants)
--key CLÉ clé publique de l'agent (le poste scelle vers elle)
--code CODE code d'appairage (10 min, généré dans l'UI AJEAN → Postes distants)
--allow LISTE capacités acceptées : shell,read,write,list (défaut : ce que le serveur autorise)
--root DIR dossier autorisé (confine lecture/écriture)
--yes auto-approuve les actions (obligatoire pour le service, sans terminal)
`)
}
// doConnect appaire au besoin, sauvegarde, puis installe (service) ou lance
// remoteConnect appaire au besoin, sauvegarde, puis installe (service) ou lance
// (avant-plan) selon install.
func doConnect(args []string, install bool) error {
f := parseFlags(args)
func remoteConnect(args []string, install bool) error {
f := parseRemoteFlags(args)
cfg, lerr := nodeclient.LoadConfig()
needEnroll := lerr != nil || cfg.Priv == ""
if f.url != "" {
@@ -91,7 +94,7 @@ func doConnect(args []string, install bool) error {
}
if needEnroll {
if cfg.ServerURL == "" {
return fmt.Errorf("url du serveur requise (ex: http://192.168.1.127:8090)")
return fmt.Errorf("url du serveur requise (ex: https://ajean.link)")
}
if f.code == "" {
return fmt.Errorf("code d'appairage requis : --code CODE")
@@ -104,17 +107,16 @@ func doConnect(args []string, install bool) error {
}
fmt.Printf("[ok] appairé (id %s)\n", cfg.ID)
}
allow, root, yes := f.allow, f.root, f.yes
if len(allow) > 0 {
cfg.Caps = nodeclient.SanitizeCaps(allow)
if len(f.allow) > 0 {
cfg.Caps = nodeclient.SanitizeCaps(f.allow)
}
if root != "" {
cfg.Root = root
if f.root != "" {
cfg.Root = f.root
}
if cfg.Root == "" {
cfg.Root = nodeclient.DataDir() + string(os.PathSeparator) + "workspace"
}
if yes || install {
if f.yes || install {
cfg.AutoYes = true // un service n'a pas de terminal pour confirmer
}
if cfg.Name == "" {
@@ -137,15 +139,15 @@ func doConnect(args []string, install bool) error {
return nil
}
func doRun() error {
func remoteRun() error {
cfg, err := nodeclient.LoadConfig()
if err != nil {
return fmt.Errorf("aucune configuration — lance d'abord « ajean-remote install … »")
return fmt.Errorf("aucune configuration — lance d'abord « ajean remote install … »")
}
return nodeclient.RunService(cfg)
}
func doStatus() error {
func remoteStatus() error {
cfg, err := nodeclient.LoadConfig()
if err != nil {
fmt.Println("aucun poste configuré")
@@ -160,14 +162,14 @@ func doStatus() error {
return nil
}
type flags struct {
type remoteFlags struct {
url, code, root, key, machine string
allow []string
yes bool
}
func parseFlags(args []string) flags {
var f flags
func parseRemoteFlags(args []string) remoteFlags {
var f remoteFlags
for i := 0; i < len(args); i++ {
a := args[i]
next := func() string {
+9 -4
View File
@@ -10,7 +10,7 @@ import (
"strings"
)
const Version = "0.9.0"
const Version = "0.9.1"
// Main est le vrai main() du binaire (cmd/ajean ne fait que l'appeler).
func Main() {
@@ -74,6 +74,8 @@ func Main() {
mustExit(cmdWeb(args))
case "link":
mustExit(cmdLink(args))
case "remote":
mustExit(cmdRemote(args))
case "ui":
mustExit(cmdUI(args))
case "oai":
@@ -158,9 +160,12 @@ Accès distant (ajean.link) :
link code code d'appairage (10 min, usage unique) pour le portail
link status | logout état du jeton / l'oublier
Poste distant (piloter un autre PC depuis l'IA) :
le poste s'installe avec le binaire LÉGER séparé « ajean-remote » (cmd/ajean-remote),
pas avec ce binaire : ajean-remote install <url-serveur> --code CODE
Poste distant (piloter CE PC depuis l'IA d'un serveur AJEAN) :
remote install <url> --machine ID --key CLÉ --code CODE [--allow shell,read,write,list]
appaire ce PC + installe le service (démarrage auto)
remote connect <url> … idem en avant-plan (pour tester)
remote status | uninstall | logout
(commande d'appairage prête à copier : UI web → Postes distants)
Backend llama.cpp :
llamacpp install clone + compile llama.cpp (CUDA/ROCm/Metal/CPU), pointe BIN dessus
+4 -4
View File
@@ -1303,7 +1303,7 @@ button:hover{border-color:var(--dim);color:var(--text)}
</div>
</div>
<div id="node-block" class="gated-group">
<div class="subhead">Postes distants<span id="node-badge" class="sbadge"></span><span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Pilote un <b>autre PC</b> depuis l'IA. installe le petit client <b>ajean-remote</b> sur le PC secondaire et lance <code>ajean-remote install &lt;url&gt; --code CODE</code> : il se connecte en sortant (aucun port à ouvrir) et expose à l'IA un jeu d'outils restreint (shell, fichiers). ⚠ tu décides des capacités et du dossier autorisé pour chaque poste ; le poste peut aussi demander confirmation localement.</span></span></div>
<div class="subhead">Postes distants<span id="node-badge" class="sbadge"></span><span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Pilote un <b>autre PC</b> depuis l'IA. installe <b>ajean</b> sur le PC secondaire et lance <code>ajean remote install &lt;url&gt; --code CODE</code> : il se connecte en sortant (aucun port à ouvrir) et expose à l'IA un jeu d'outils restreint (shell, fichiers). ⚠ tu décides des capacités et du dossier autorisé pour chaque poste ; le poste peut aussi demander confirmation localement.</span></span></div>
<div class="slist">
<div class="rowbody">
<div id="node-list" class="mcp-list"><div class="muted" style="font-size:12px">(aucun poste — appaire un PC pour que l'IA puisse agir dessus)</div></div>
@@ -1723,7 +1723,7 @@ button:hover{border-color:var(--dim);color:var(--text)}
<div class="subhead" style="margin-top:6px">Code d'appairage</div>
<div class="node-code" id="node-pair-code">——</div>
<div class="muted" style="font-size:12px;line-height:1.5">
Sur le PC à piloter, installe <b>ajean-remote</b> (ajean.link/download) puis lance — <b>accès à distance</b>, chiffré de bout en bout :
Sur le PC à piloter, installe <b>ajean</b> (ajean.link/download) puis lance — <b>accès à distance</b>, chiffré de bout en bout :
<pre class="node-cmd" id="node-pair-cmd"></pre>
<span style="opacity:.8">Ou en <b>réseau local</b> (connexion directe) :</span>
<pre class="node-cmd" id="node-pair-cmd-lan"></pre>
@@ -5509,10 +5509,10 @@ async function genNodeCode(){
// Commande d'accès À DISTANCE (partout) via ajean.link : chiffrée de bout en
// bout, le relais reste aveugle. --key = clé publique de l'agent (le poste
// scelle vers elle), --machine = quelle machine joindre.
const remote = 'ajean-remote install https://ajean.link --machine '+r.machine+
const remote = 'ajean remote install https://ajean.link --machine '+r.machine+
' --key '+r.agent_pub+' --code '+r.code+' --allow '+allow+rootArg;
// Variante RÉSEAU LOCAL (connexion directe, sans passer par le relais).
const lan = 'ajean-remote install '+location.origin+
const lan = 'ajean remote install '+location.origin+
' --key '+r.agent_pub+' --code '+r.code+' --allow '+allow+rootArg;
document.getElementById('node-pair-cmd').textContent = remote;
const lanEl = document.getElementById('node-pair-cmd-lan');
+2 -2
View File
@@ -147,7 +147,7 @@
</div>
</div>
<div id="node-block" class="gated-group">
<div class="subhead">Postes distants<span id="node-badge" class="sbadge"></span><span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Pilote un <b>autre PC</b> depuis l'IA. installe le petit client <b>ajean-remote</b> sur le PC secondaire et lance <code>ajean-remote install &lt;url&gt; --code CODE</code> : il se connecte en sortant (aucun port à ouvrir) et expose à l'IA un jeu d'outils restreint (shell, fichiers). ⚠ tu décides des capacités et du dossier autorisé pour chaque poste ; le poste peut aussi demander confirmation localement.</span></span></div>
<div class="subhead">Postes distants<span id="node-badge" class="sbadge"></span><span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Pilote un <b>autre PC</b> depuis l'IA. installe <b>ajean</b> sur le PC secondaire et lance <code>ajean remote install &lt;url&gt; --code CODE</code> : il se connecte en sortant (aucun port à ouvrir) et expose à l'IA un jeu d'outils restreint (shell, fichiers). ⚠ tu décides des capacités et du dossier autorisé pour chaque poste ; le poste peut aussi demander confirmation localement.</span></span></div>
<div class="slist">
<div class="rowbody">
<div id="node-list" class="mcp-list"><div class="muted" style="font-size:12px">(aucun poste — appaire un PC pour que l'IA puisse agir dessus)</div></div>
@@ -567,7 +567,7 @@
<div class="subhead" style="margin-top:6px">Code d'appairage</div>
<div class="node-code" id="node-pair-code">——</div>
<div class="muted" style="font-size:12px;line-height:1.5">
Sur le PC à piloter, installe <b>ajean-remote</b> (ajean.link/download) puis lance — <b>accès à distance</b>, chiffré de bout en bout :
Sur le PC à piloter, installe <b>ajean</b> (ajean.link/download) puis lance — <b>accès à distance</b>, chiffré de bout en bout :
<pre class="node-cmd" id="node-pair-cmd"></pre>
<span style="opacity:.8">Ou en <b>réseau local</b> (connexion directe) :</span>
<pre class="node-cmd" id="node-pair-cmd-lan"></pre>
+2 -2
View File
@@ -115,10 +115,10 @@ async function genNodeCode(){
// Commande d'accès À DISTANCE (partout) via ajean.link : chiffrée de bout en
// bout, le relais reste aveugle. --key = clé publique de l'agent (le poste
// scelle vers elle), --machine = quelle machine joindre.
const remote = 'ajean-remote install https://ajean.link --machine '+r.machine+
const remote = 'ajean remote install https://ajean.link --machine '+r.machine+
' --key '+r.agent_pub+' --code '+r.code+' --allow '+allow+rootArg;
// Variante RÉSEAU LOCAL (connexion directe, sans passer par le relais).
const lan = 'ajean-remote install '+location.origin+
const lan = 'ajean remote install '+location.origin+
' --key '+r.agent_pub+' --code '+r.code+' --allow '+allow+rootArg;
document.getElementById('node-pair-cmd').textContent = remote;
const lanEl = document.getElementById('node-pair-cmd-lan');
+3 -3
View File
@@ -40,7 +40,7 @@ func Install() error {
case "darwin":
return installLaunchd(dst)
}
return fmt.Errorf("installation auto non gérée sur %s — lance « ajean-remote connect » via ton propre gestionnaire de démarrage", runtime.GOOS)
return fmt.Errorf("installation auto non gérée sur %s — lance « ajean remote connect » via ton propre gestionnaire de démarrage", runtime.GOOS)
}
func Uninstall() error {
@@ -58,7 +58,7 @@ func Uninstall() error {
func installSystemd(exe string) error {
unit := "[Unit]\nDescription=AJEAN poste distant\nAfter=network-online.target\n\n" +
"[Service]\nExecStart=" + exe + " run\nRestart=always\nRestartSec=5\n\n" +
"[Service]\nExecStart=" + exe + " remote run\nRestart=always\nRestartSec=5\n\n" +
"[Install]\nWantedBy=default.target\n"
dir := filepath.Join(os.Getenv("HOME"), ".config/systemd/user")
if err := os.MkdirAll(dir, 0o755); err != nil {
@@ -79,7 +79,7 @@ func installLaunchd(exe string) error {
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>Label</key><string>link.ajean.remote</string>
<key>ProgramArguments</key><array><string>` + exe + `</string><string>run</string></array>
<key>ProgramArguments</key><array><string>` + exe + `</string><string>remote</string><string>run</string></array>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
</dict></plist>
+2 -2
View File
@@ -73,13 +73,13 @@ func Install() error {
defer m.Disconnect()
if s, err := m.OpenService(serviceName); err == nil {
s.Close()
return fmt.Errorf("service déjà installé — « ajean-remote uninstall » d'abord")
return fmt.Errorf("service déjà installé — « ajean remote uninstall » d'abord")
}
s, err := m.CreateService(serviceName, dst, mgr.Config{
StartType: mgr.StartAutomatic,
DisplayName: "AJEAN — poste distant",
Description: "Pont léger permettant à votre serveur IA AJEAN d'agir sur ce PC.",
}, "run")
}, "remote", "run")
if err != nil {
return fmt.Errorf("création du service: %w", err)
}