HarnessRouter self-hosted: una API per gli agenti
Scopri il deploy Docker di HarnessRouter, il bind su loopback, le credenziali predefinite da cambiare e l’accesso TLS per Codex, Claude Code e Hermes.
Cosa rimuove HarnessRouter
Esegui HarnessRouter Community Edition in self-hosting per mettere una singola API davanti a diversi harness di agenti su un server di tua proprietà. Un harness di agenti è il programma a riga di comando che esegue un modello in un ciclo: mantiene una sessione, modifica i file, esegue comandi e invia in streaming l’avanzamento a chi ha richiesto il lavoro. Codex, Claude Code e Hermes svolgono tutti questo compito, ma ciascuno ha una propria procedura di installazione, un proprio formato per le credenziali e una propria definizione di sessione. HarnessRouter esegue tutti questi strumenti in un unico container e mette davanti un singolo endpoint HTTP, un singolo accesso e un unico archivio dei secret.
Questa è l’idea completa, ma vale la pena esplicitarne il costo. Aggiungi un container, un accesso, un volume e una procedura di aggiornamento al server per trasformare diversi componenti in uno solo. Se oggi esegui esattamente un harness, questa configurazione è peggiore rispetto all’installazione diretta di quell’harness. Questo compromesso viene analizzato nell’ultima sezione, quindi leggila prima di eseguire il deployment.
Tutto quanto segue è stato verificato con il tag dell’immagine 0.5.5, scaricato il 19 agosto 2026. Il progetto pubblica nuovi tag quasi ogni giorno, quindi controlla il tag effettivamente in esecuzione invece di fare affidamento su questa pagina tra un mese. I comandi provengono dal README del progetto disponibile all’indirizzo github.com/HarnessRouter/harnessrouter.
Che cos'è realmente l'Unified Harness Protocol
HarnessRouter implementa l'Unified Harness Protocol (UHP), pubblicato su unifiedharnessprotocol.org. UHP descrive come un prodotto avvia un'attività su un harness, ne segue l'esecuzione, gestisce sessioni e file e segnala gli errori. La specifica è versionata in base alla data. La versione in vigore al 19 agosto 2026 è datata 2026-08-11 e il sito la definisce uno standard in bozza, «sufficientemente stabile per costruirci sopra e versionato per poter evolvere in sicurezza».
Occorre interpretare con attenzione l'espressione «standard aperto». La stessa azienda redige la specifica, sviluppa l'implementazione di riferimento e gestisce la suite di conformità composta da 52 controlli che stabilisce chi è conforme. È normale per un protocollo così recente e la licenza Apache-2.0 consente di creare un fork di qualsiasi sua parte. Significa anche che UHP non è ancora uno standard multi-vendor. Consideratelo un protocollo emergente: utile, in evoluzione e tale che il vostro codice dovrebbe poter smettere di usarlo senza richiedere una riscrittura.
Cosa serve prima di iniziare
Docker e circa 4 GB di spazio libero su disco. Serve anche una chiave API di un provider di modelli per cui disponi già di un abbonamento. Il download dell'immagine richiede circa 700 MB; lo spazio rimanente viene utilizzato dalle CLI degli agenti e dai workspace in cui scrivono. L'immagine non include alcun modello né una chiave di prova, quindi le attività falliscono finché non colleghi un provider. HarnessRouter è distribuito con licenza Apache-2.0. Le CLI degli agenti non sono coperte da questa licenza. Per questo vengono scaricate al primo avvio invece di essere incluse nell'immagine.
Self-host HarnessRouter con un solo docker run
docker pull harnessrouter/harnessrouter
docker run -d --name harnessrouter \
-p 127.0.0.1:3000:3000 \
-v harnessrouter:/data \
harnessrouter/harnessrouterQuindi monitora l'avvio del container. Il primo avvio è lento e i log spiegano il motivo.
docker logs -f harnessrouterDurante l'avvio vedrai righe simili alle seguenti:
installing Claude Code (Anthropic's terms apply)…
installing Codex (Apache-2.0)…
installing Hermes (check its upstream license before use)…Attendi ready on :3000. L'installazione viene eseguita una sola volta per volume. Per questo, gli avvii successivi richiedono pochi secondi e non mostrano alcuna riga di installazione.
Da questo download derivano due aspetti importanti, soprattutto su un VPS. Primo: il primo avvio richiede l'accesso alla rete in uscita. L'immagine non è autosufficiente. Un server protetto da un filtro per il traffico in uscita, oppure privo di una route verso l'esterno, resta bloccato in questo punto e non stampa mai ready on :3000. Il problema si verifica al primo avvio, non a docker pull. Accorgersene in quel momento può creare confusione. Secondo: stai installando software di terze parti soggetto a termini stabiliti da terzi. Claude Code viene fornito secondo i termini di Anthropic, mentre Hermes è soggetto ai termini indicati dal relativo progetto upstream. Verifica entrambi prima di usarli per scopi commerciali.
-v harnessrouter:/data crea un volume Docker denominato. Tutti i dati persistenti si trovano in /data: i database SQLite, i file archiviati, il secret store e gli spazi di lavoro degli agenti. Se elimini quel volume, elimini l'istanza, incluse le chiavi dei provider e tutte le trascrizioni. Esegui il backup con il container arrestato, perché copiare un database SQLite mentre è in scrittura produce un file che potrebbe non aprirsi. La stessa disciplina, ovvero arrestare il container prima di copiare i dati, si applica a ogni container con stato presente sul server. I dettagli cambiano in base al servizio, perché PhotoPrism e Immich richiedono ciascuno i propri comandi di backup.
docker stop harnessrouter
docker run --rm -v harnessrouter:/data -v "$PWD":/backup alpine \
tar czf /backup/harnessrouter-data.tgz -C / data
docker start harnessrouterLa variante compose e la riga da modificare
Il repository include un file compose. Pubblica "3000:3000", quindi tutte le interfacce del sistema host. Modifica quella riga prima di avviarlo su un server pubblico.
services:
harnessrouter:
image: harnessrouter/harnessrouter:0.5.5
ports:
- "127.0.0.1:3000:3000"
env_file:
- .env
volumes:
- harnessrouter-data:/data
restart: unless-stopped
volumes:
harnessrouter-data:Rispetto alla versione upstream cambiano due elementi: l'indirizzo di bind e un tag di versione fissato al posto di latest. Il pinning è importante perché tra il 9 e il 18 agosto 2026 sono stati pubblicati sedici tag di versione. Un runtime dell'agente che cambia senza preavviso è difficile da sottoporre a troubleshooting. Copia quindi il file dell'ambiente, limita i relativi permessi e avvia il servizio.
cp .env.example .env
chmod 600 .env
docker compose up -d
docker compose logs -f.env contiene la chiave del provider in testo in chiaro, quindi la modalità 600 è il requisito minimo. Se il sottocomando docker compose non ti è familiare, la cheat sheet dei comandi Docker Compose illustra i comandi di uso quotidiano.
Perché la porta è pubblicata su 127.0.0.1 e non su 0.0.0.0
-p 3000:3000 pubblica la porta su tutte le interfacce disponibili sull'host. -p 127.0.0.1:3000:3000 la pubblica solo sull'interfaccia di loopback, quindi è possibile accedere al servizio soltanto dal VPS stesso. Il container resta sempre in ascolto sulla porta 3000 al suo interno; perciò devi modificare il valore a sinistra. Verifica il risultato:
docker port harnessrouter
sudo ss -ltnp | grep 3000La stampa di 127.0.0.1:3000 da parte di ss è corretta. 0.0.0.0:3000 indica che la console è esposta su Internet. In questo caso il rischio è maggiore rispetto alla maggior parte delle applicazioni self-hosted, perché la console crea harness, legge tutte le trascrizioni, esegue agenti e fornisce loro una shell e un filesystem reale nella rispettiva area di lavoro. Inoltre contiene la chiave del provider che hai configurato. Chiunque raggiunga una console non protetta può leggere il tuo lavoro, eseguire comandi e consumare la tua chiave.
Un firewall sull'host non protegge da questo problema. Docker pubblica le porte inserendo le proprie regole nella tabella del kernel nat; queste regole vengono valutate prima della catena gestita da ufw. Di conseguenza, una porta pubblicata resta raggiungibile anche quando sudo ufw status la elenca come negata. Esegui il test da un'altra macchina, non dal VPS, altrimenti non verificherai nulla. È la stessa lezione di eseguire dsh senza interfaccia su porta 3080: associa il servizio all'interfaccia di loopback, quindi stabilisci deliberatamente come raggiungerlo.
Modificare subito le credenziali di accesso predefinite
Accedi a http://localhost:3000 con il nome utente harnessrouter e la password harnessrouter. Queste credenziali sono riportate nel README perché sono segnaposto, non secret, e il container mostra un avviso a ogni avvio finché non le modifichi:
using the DEFAULT password. Set HR_AUTH_PASSWORD, or change it from the profile page, before exposing this instance.Modificale dalla pagina Profile oppure impostale all'avvio per una distribuzione tramite script. HR_AUTH_USER e HR_AUTH_PASSWORD sostituiscono i valori predefiniti.
docker run -d --name harnessrouter \
-p 127.0.0.1:3000:3000 \
-v harnessrouter:/data \
-e HR_AUTH_USER='you' \
-e HR_AUTH_PASSWORD='the-password-you-chose' \
harnessrouter/harnessrouterNon è disponibile alcuna email di reimpostazione, perché non esistono né un sistema di account né un mail server. Se perdi la password, elimina il file di autenticazione dal volume e riavvia. Quindi accedi nuovamente usando i valori predefiniti.
docker stop harnessrouter
docker run --rm -v harnessrouter:/data alpine rm -f /data/selfhost-auth.json
docker start harnessrouterHR_AUTH_DISABLED=1 rimuove completamente il controllo di accesso. Il README lo limita a «un computer che nessun altro può raggiungere». Un VPS con un indirizzo IP pubblico non rientra in questo caso, quindi lascia attivo il controllo di accesso, a meno che tu non stia eseguendo il servizio su un laptop.
Verifica la versione, perché quelle precedenti non avevano alcun controllo di accesso
Questa è la parte da prendere sul serio. Le versioni 0.1.x e 0.2.0 sono state rilasciate senza alcun controllo di autenticazione: chiunque potesse raggiungere la porta 3000 era già dentro la console. 0.3.0 è stata la prima release con una procedura di login. Questi tag precedenti sono ancora pubblicati e possono ancora essere scaricati. Di conseguenza, un tag precedente fissato nella configurazione o un file Compose copiato da un collega può esporre oggi una console senza controllo di accesso su una porta pubblica.
Al 19 agosto 2026, il tag pubblicato più recente è 0.5.5, datato 18 agosto 2026, e latest punta a questo tag. Verifica ciò che hai installato, quindi confrontalo con l’elenco dei tag su Docker Hub:
docker image ls harnessrouter/harnessrouterQualsiasi versione precedente a 0.3.0 deve essere sostituita subito, senza rimandare l’intervento. Anche le versioni pari o successive richiedono la modifica della password, perché per chi esegue scansioni sulla porta 3000 una password predefinita e l’assenza di password sono la stessa cosa. Non considerare aggiornati i numeri di versione riportati in questa pagina. Erano validi alla data indicata all’inizio e questo progetto rilascia nuove versioni rapidamente.
Collegare un provider
Nulla viene eseguito finché non si collega un provider di modelli. Aggiungine uno dalla pagina Integrations della console oppure passalo a docker run nell’ambiente. Il valore è JSON, quindi racchiudilo tra virgolette nella shell:
-e HR_SECRET_GLOBAL_HARNESS_CONN_ANTHROPIC='{"name":"anthropic","provider":"anthropic","api_key":"sk-ant-…"}'.env.example identifica una variabile di connessione per ogni famiglia di provider: HR_SECRET_GLOBAL_HARNESS_CONN_ANTHROPIC per il backend claude-code, HR_SECRET_GLOBAL_HARNESS_CONN_OPENAI per il backend codex e HR_SECRET_GLOBAL_HARNESS_CONN_CUSTOM per qualsiasi endpoint compatibile con OpenAI, ad esempio un aggregatore o un server di inferenza gestito internamente. Le variabili HR_SECRET_GLOBAL_HARNESS_POLICY_CLAUDE, HR_SECRET_GLOBAL_HARNESS_POLICY_CODEX e HR_SECRET_GLOBAL_HARNESS_POLICY_HERMES corrispondenti indicano quale connessione usa per impostazione predefinita ciascun backend. HR_SECRET_KEY è un elemento separato ed è richiesto solo quando colleghi un database a un agent.
HR_BACKENDS seleziona i backend da caricare, come in HR_BACKENDS=claude,codex,hermes. È importante conoscere un problema già noto: qualsiasi valore che omette hermes fa terminare immediatamente il container con stato 1 e senza messaggi di errore. Un secondo dopo l’avvio viene visualizzato Exited (1) in docker ps -a, mentre docker logs non mostra informazioni utili. Mantieni hermes nell’elenco finché il progetto upstream non risolve il problema. Se vuoi usare Hermes come unico harness, eseguire l’agent Hermes sul proprio VPS è la configurazione più semplice.
Chiama l'API senza la console
La console è facoltativa. La stessa API serve entrambi e usa un contratto in stile Responses. Accedi prima per ottenere un cookie di sessione:
curl -c hr.cookies http://localhost:3000/api/selfhost/login \
-H 'content-type: application/json' \
-d '{"username":"harnessrouter","password":"your-password"}'Quindi invia un'attività, specificando l'harness in metadata.harness_id e un modello effettivamente fornito dal provider connesso:
curl -s -b hr.cookies http://localhost:3000/api/harness/v1/responses \
-H 'content-type: application/json' \
-d '{"input":"Reply with exactly this and nothing else: it works.",
"metadata":{"harness_id":"codex"},
"model":"gpt-5.4-mini",
"stream":false}'Un oggetto JSON contenente un blocco di output e un conteggio dei token indica che l'harness è stato eseguito. Modificando harness_id da codex a claude, invii la stessa richiesta a un harness diverso. Questo scambio è l'intero motivo per cui esiste questo software. La connessione personalizzata configurata sopra consente di indirizzare un harness verso un endpoint compatibile con OpenAI già ospitato da te, come nel caso di un harness DeepSeek self-hosted su un VPS.
Raggiungerlo dal portatile senza pubblicare una porta
Sono disponibili due metodi. Nessuno dei due espone direttamente una porta su 0.0.0.0.
Un tunnel SSH è la soluzione più semplice e non richiede installazioni sul server. Inoltra una porta locale del computer al loopback del VPS.
ssh -N -L 3000:127.0.0.1:3000 you@your-vpsLasciate il tunnel attivo e aprite http://localhost:3000 nel browser. Se SSH visualizza bind: Address already in use, sul portatile è già in uso la porta 3000. Scegliete quindi un'altra porta locale con -L 3100:127.0.0.1:3000 e aprite la porta 3100 nel browser.
Un reverse proxy con terminazione TLS è la soluzione adatta quando devono accedere anche altre persone. Il proxy gestisce il certificato TLS (transport layer security) e inoltra le richieste al loopback. Il README include una configurazione Caddy:
console.example.com {
encode zstd gzip
reverse_proxy 127.0.0.1:3000 {
flush_interval -1 # agent turns stream for minutes; never buffer them
}
}flush_interval -1 è la riga che viene omessa più spesso. Agent trasmette i token dello stream per diversi minuti. Un proxy che memorizza nella cache la risposta trattiene questi token fino al termine dell'elaborazione. La console sembra quindi bloccata e visualizza tutto in una volta sola. L'equivalente in Nginx è proxy_buffering off; all'interno del blocco location. Qualunque soluzione scegliate, mantenete il nome DNS puntato al proxy e il container sul loopback. Confronto tra Nginx, Caddy e Traefik come reverse proxy spiega quale soluzione è più adatta al vostro server.
Eseguilo con un utente dedicato, non come root
Il demone Docker viene eseguito come root e l'appartenenza al gruppo docker equivale all'accesso root, perché un membro può avviare un container che monta il filesystem dell'host. Quindi, «aggiungere il team al gruppo docker» assegna l'accesso root al server che conserva la chiave del provider.
La versione semplice consiste nel creare un account di servizio proprietario del file Compose e di .env, mantenendo questi file fuori da qualsiasi home directory condivisa.
sudo adduser --disabled-password --gecos "" harness
sudo install -d -o harness -g harness -m 750 /srv/harnessrouterLa soluzione più sicura è Docker rootless, in cui il demone stesso viene eseguito da quell'utente non privilegiato. Richiede il pacchetto uidmap per newuidmap e newgidmap, nonché almeno 65536 UID subordinati in /etc/subuid e /etc/subgid per l'utente. uidmap è disponibile nell'archivio Ubuntu, ma docker-ce-rootless-extras no: viene distribuito dal repository apt di Docker, disponibile all'indirizzo download.docker.com, che viene aggiunto durante l'installazione del motore Docker. Se il motore non è stato installato da quel repository, grep -rl download.docker.com /etc/apt/sources.list.d/ non restituisce alcun output e l'installazione seguente non troverà il pacchetto.
sudo apt install -y uidmap docker-ce-rootless-extras
sudo loginctl enable-linger harness
sudo -iu harness
dockerd-rootless-setuptool.sh install
export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock
systemctl --user enable --now dockerloginctl enable-linger non è facoltativo in questo caso. Senza di esso, l'istanza systemd dell'utente si arresta quando termina l'ultima sessione; di conseguenza, il container si arresta quando si esegue il logout. Verifica il risultato con docker info, che elenca rootless nella sezione Security Options. La modalità rootless non può associare porte inferiori a 1024 senza una configurazione aggiuntiva. In questo caso non è un problema, perché la porta 3000 è superiore a tale soglia. La configurazione dell'account è descritta in creazione di utenti con privilegi minimi su un VPS.
Cosa non funziona e cosa vedrai
Il container termina un secondo dopo l'avvio e i log sono vuoti. docker ps -a mostra Exited (1). Questo indica il problema HR_BACKENDS descritto sopra: il valore non includeva hermes. Ripristinalo.
Il primo avvio non termina mai. Il log si interrompe dopo una riga installing e ready on :3000 non compare mai. Il sistema non riesce a raggiungere la rete per scaricare le CLI dell'agente, perché non sono incluse nell'immagine. Correggi la route in uscita o le impostazioni del proxy, quindi riavvia.
La console viene caricata, ma ogni attività non riesce. Nessun provider è connesso. Nell'immagine non sono inclusi né un modello né un piano gratuito, quindi una nuova istanza può autenticarti senza eseguire alcuna attività.
La console si blocca a metà della risposta dietro un proxy. L'output viene mostrato in un unico blocco al termine dell'elaborazione. Si tratta del buffering della risposta. Imposta flush_interval -1 in Caddy oppure proxy_buffering off; in Nginx.
Non riesci a raggiungerlo dal laptop, anche se il tunnel è attivo. Esegui docker port harnessrouter sul server. Se non stampa nulla, il container non pubblica alcuna porta, quindi è stato avviato senza -p.
Vale la pena eseguirlo?
Vale la pena eseguirlo se usi realmente più di un harness e vuoi un solo endpoint e un solo archivio delle credenziali invece di tre endpoint e tre archivi. Vale la pena anche se stai sviluppando un prodotto basato su questi componenti e vuoi che l’harness sia un valore di configurazione anziché richiedere una riscrittura. È questo il vantaggio offerto da UHP, con la considerazione precedente sul fatto che il protocollo è ancora recente.
Non vale la pena eseguirlo se usi un solo harness. Installare quella CLI sul server comporta meno componenti e nessun login tra te e il servizio. È inoltre la soluzione sbagliata se vuoi che più agenti collaborino alla stessa attività invece di avere una sola API davanti a più harness: si tratta di uno strumento diverso. Per questo scenario, consulta un harness multi-agent come Omnigent. In ogni caso, le regole di deployment non cambiano. Bind su loopback, password modificata, un tag fissato a 0.3.0 o superiore e un utente dedicato.
FAQ
È sicuro pubblicare HarnessRouter sulla porta 3000?
No. La console crea harness, legge ogni trascrizione, esegue agent con accesso alla shell e al filesystem e contiene la chiave del provider collegato. Una porta esposta rende quindi disponibili tutte queste funzioni. Pubblicala sull'interfaccia loopback con -p 127.0.0.1:3000:3000 e raggiungila tramite un tunnel SSH o un reverse proxy con terminazione TLS. Un firewall dell'host non è sufficiente: Docker scrive le proprie regole nella tabella del kernel nat, quindi una porta pubblicata risponde da Internet anche quando ufw la mostra come negata. Verifica con sudo ss -ltnp | grep 3000, che dovrebbe stampare 127.0.0.1:3000.
Quale versione di HarnessRouter ha aggiunto il controllo di accesso?
0.3.0. Le versioni 0.1.x e 0.2.0 sono state rilasciate senza alcuna autenticazione. Entrambi i tag sono ancora pubblicati e possono ancora essere scaricati, quindi chi li esegue si affida soltanto al fatto che nessuno trovi la porta. Al 19 agosto 2026 il tag più recente è 0.5.5, datato 18 agosto 2026. Esegui docker image ls harnessrouter/harnessrouter per verificare la versione installata, confrontala con l'elenco dei tag su Docker Hub e non con questa pagina, quindi modifica la password predefinita anche nelle versioni aggiornate.
Perché il container termina subito dopo che imposto HR_BACKENDS?
Qualsiasi valore di HR_BACKENDS che omette hermes fa terminare immediatamente il container con stato 1 e senza messaggi di errore. Si tratta di un problema noto, documentato nel README del progetto. Il sintomo è Exited (1) in docker ps -a entro uno o due secondi, senza informazioni utili in docker logs. Mantieni hermes nell'elenco, come in HR_BACKENDS=claude,codex,hermes, finché il progetto upstream non risolve il problema.
HarnessRouter deve avere accesso a Internet al primo avvio?
Sì. Le CLI degli agent vengono scaricate al primo avvio invece di essere incluse nell'immagine, perché ciascuna include la propria licenza. Un host senza una route in uscita stampa le righe installing e poi non raggiunge mai ready on :3000. Il download viene eseguito una volta per volume. Gli avvii successivi richiedono alcuni secondi e non hanno bisogno di rete, salvo quella necessaria per raggiungere il provider del modello collegato.
Ho perso la password della console. Come posso accedere di nuovo?
Non esiste un'email di reimpostazione, perché non esistono né un sistema di account né un server di posta. Arresta il container, elimina /data/selfhost-auth.json dal volume, riavvialo, quindi accedi con le credenziali predefinite e imposta una nuova password dalla pagina Profile. Se il container e il volume si chiamano entrambi harnessrouter, esegui docker stop harnessrouter, poi docker run --rm -v harnessrouter:/data alpine rm -f /data/selfhost-auth.json e infine docker start harnessrouter.