SSD Nodes Learn 🎉 VPS dès $5.50/mois
Guides Matt ConnorPar Matt Connor

Comment auto-héberger SandBase Harness sur un VPS

Installez SandBase Harness v0.3.2 sur votre VPS avec une version taguée, un agent YAML, des serveurs MCP, les modes sandbox et le SDK Anthropic.

Ce que vous obtenez en auto-hébergeant le runtime d’agents SandBase

Auto-héberger le runtime d’agents SandBase consiste à exécuter SandBase Harness sur un serveur que vous contrôlez. Les sessions, les identifiants, la mémoire et les journaux d’audit restent ainsi sur votre disque, et non sur celui d’un tiers. Il s’agit d’un service Node. Il écoute sur 127.0.0.1:3000, fournit une API HTTP /v1 et une console web, puis stocke son état dans SQLite, à côté de vos fichiers d’agents.

L’API /v1 reprend la structure de Claude Managed Agents (CMA), l’API d’agents managés hébergée. C’est ce qui rend ce runtime intéressant dans les deux cas : vous pouvez écrire du code avec le SDK Anthropic et faire pointer son baseURL vers votre propre serveur, puis déplacer le même code vers un déploiement hébergé.

SandBase Harness ne fournit pas de modèle. Il en appelle un. En août 2026, il prend en charge les endpoints OpenAI, Anthropic et compatibles avec OpenAI. Cela couvre les gateways auto-hébergées et les fournisseurs tels que DeepSeek V4. Vous devez toujours fournir une clé API ou un serveur local qui parle l’API OpenAI.

Ce qu’il vous faut avant de commencer

  • Un VPS sous Ubuntu 24.04 avec au moins 2 GB de RAM. La compilation TypeScript est l’étape la plus lourde de l’installation.
  • Node.js 22 ou une version ultérieure, et npm 10 ou une version ultérieure. Ce sont les versions minimales strictes indiquées par le projet.
  • git, ainsi qu’une clé d’API pour le fournisseur de modèles que vous prévoyez d’utiliser.
  • Docker, uniquement si vous voulez des sandbox conteneurisées par session.

Ubuntu 24.04 fournit Node 18.19 dans son propre dépôt. Cette version est inférieure au minimum requis. Installez donc Node depuis NodeSource.

curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs git
node -v
npm -v

node -v doit afficher v22 ou une version ultérieure, et npm -v doit afficher 10 ou une version ultérieure. Si node -v affiche encore v18.19.1, le paquet de la distribution est toujours installé et prioritaire dans PATH. Supprimez-le avant de continuer, car la compilation utilise la version de node trouvée par le shell.

Installer SandBase depuis le tag v0.3.2

Installez depuis un tag, jamais depuis une branche qui évolue. Un clone bare de main récupère tout ce qui a été ajouté il y a une heure, et les clés de configuration ci-dessous peuvent ne pas correspondre. v0.3.2 est le tag actuel au 16 août 2026.

sudo install -d -o "$USER" -g "$USER" /opt/sandbase
cd /opt/sandbase
git clone --branch v0.3.2 --depth 1 https://github.com/sandbaseai/sandbase-harness.git
cd sandbase-harness
npm ci
npm run build

Utilisez npm ci, pas npm install. ci installe exactement les versions enregistrées dans le lockfile commité, afin que votre arborescence corresponde à celle testée par les mainteneurs. npm install peut résoudre des versions plus récentes. C’est ainsi qu’un tag épinglé cesse discrètement de l’être.

Créez maintenant un workspace. Le workspace est un répertoire distinct qui contient les fichiers de votre agent et tout son état d’exécution. Le conserver en dehors du checkout des sources vous permet de récupérer un tag plus récent sans modifier vos données.

mkdir -p /opt/sandbase/workspace
cd /opt/sandbase/workspace
node /opt/sandbase/sandbase-harness/dist/index.js init
node /opt/sandbase/sandbase-harness/dist/index.js start

init crée un répertoire .managed-agents/ dans le workspace. start démarre la console sur http://127.0.0.1:3000/dashboard et l’API sur http://127.0.0.1:3000/v1. Aucun des deux n’est encore accessible depuis votre ordinateur portable, ce qui est normal et sera expliqué plus loin. Accédez pour l’instant à la console via SSH :

ssh -N -L 3000:127.0.0.1:3000 you@your-server

Le long chemin node .../dist/index.js devient vite contraignant. Donnez-lui donc un nom.

alias sandbase='node /opt/sandbase/sandbase-harness/dist/index.js'

Les commandes ci-dessous sont écrites avec sandbase <command> dans cette configuration.

N’installez pas le paquet depuis npm

Le projet le précise dans sa propre documentation d’installation : le paquet managed-agents sans scope visible sur npm ne correspond pas à ce projet. npx managed-agents et npm install -g managed-agents récupèrent donc un composant sans rapport avec le runtime recherché. Installez le projet depuis la source GitHub taguée jusqu’à ce que les mainteneurs annoncent un paquet officiel avec scope. Ce point n’est pas une simple note dans l’historique du projet : v0.3.1 sert principalement à remplacer l’installation rapide depuis npm par l’utilisation de la source taguée et figée.

Indiquez le fournisseur de modèles de l’espace de travail

init écrit .managed-agents/config.yaml. Un seul fournisseur est configuré pour l’ensemble de l’espace de travail, puis chaque agent sélectionne des identifiants de modèle précis.

model:
  provider: openai
  api_key: ${OPENAI_API_KEY}
storage:
  metadata:
    provider: sqlite
    options: {}
  artifacts:
    provider: local
    options:
      base_path: files

Le formulaire ${OPENAI_API_KEY} récupère sa valeur dans l’environnement du processus. La clé reste ainsi absente du fichier de configuration et de toutes les sauvegardes de ce fichier. Placez-la dans un fichier d’environnement que seul root peut lire, car systemd lit EnvironmentFile= en tant que root avant d’abandonner ses privilèges.

sudo install -d -m 750 /etc/sandbase
sudo touch /etc/sandbase/runtime.env
sudo chmod 600 /etc/sandbase/runtime.env

Ouvrez ce fichier dans un éditeur et ajoutez une ligne, OPENAI_API_KEY=sk-.... Les clés des fournisseurs doivent être placées ici. Les secrets qu’un agent utilise pendant une session doivent plutôt être stockés dans les coffres d’identifiants du runtime. Il s’agit d’un problème différent, avec un périmètre d’impact différent. Lisez garder les secrets hors des agents IA avant de coller un token de production dans l’un ou l’autre emplacement.

L’agent au format YAML : mcp_servers, tools et stratégies d’autorisation

Les agents sont définis dans des fichiers YAML du répertoire de l’espace de travail agents/. C’est dans cette partie du runtime que vous passerez réellement le plus de temps.

name: Incident commander
description: Triages alerts and coordinates response.
model: gpt-4o
system: |-
  You are an on-call incident commander.
mcp_servers:
  - name: sentry
    type: url
    url: https://mcp.sentry.dev/mcp
tools:
  - type: agent_toolset_20260401
    default_config:
      permission_policy: { type: always_ask }
    configs:
      - name: bash
        permission_policy: { type: always_ask }
  - type: mcp_toolset
    mcp_server_name: sentry
metadata:
  template: incident-commander

Chargez-le et vérifiez qu’il a bien été importé :

sandbase reload
sandbase list
sandbase chat agent_assistant --message "hello"

reload importe le YAML initial dans SQLite. list doit maintenant afficher l’agent avec un ID. Si list ne l’affiche pas, le fichier n’a pas été analysé et .managed-agents/logs/runtime.log contient la raison.

mcp_servers déclare les endpoints MCP (model context protocol). type: url signifie que le runtime communique en HTTP avec un serveur exécuté ailleurs. Vous pouvez donc réutiliser les services que vous exploitez déjà, notamment des serveurs MCP hébergés sur le même VPS que le runtime.

La déclaration d’un serveur ne donne pas automatiquement ses outils à l’agent. La liste tools s’en charge, au moyen d’une entrée mcp_toolset dont mcp_server_name correspond à la valeur name ci-dessus. Si l’agent se comporte comme si les outils MCP n’existaient pas, comparez ces deux chaînes caractère par caractère avant de chercher ailleurs.

agent_toolset_20260401 désigne l’ensemble d’outils intégré. Le suffixe correspondant à une date est une version de schéma. Un agent qui y est épinglé conserve donc les définitions d’outils contre lesquelles il a été écrit. default_config définit la stratégie applicable à tous les outils de l’ensemble, et chaque entrée sous configs remplace cette stratégie pour un outil donné, bash dans l’exemple.

permission_policy est l’élément qui donne à un runtime un avantage sur un simple appel de modèle. always_ask suspend la session et attend qu’un humain approuve l’appel avant son exécution. always_allow l’autorise. Définir bash sur always_ask signifie que l’agent ne peut pas exécuter une commande shell sans que vous voyiez d’abord la commande exacte. C’est le même contrôle que celui que vous utiliseriez pour exécuter Claude Code en toute sécurité sur un VPS.

Les trois modes de sandbox et les cas d’utilisation de chacun

Les appels d’outils qui exécutent du code s’exécutent dans une sandbox. Le backend est choisi pour chaque environnement via sandbox_provider dans l’objet config de l’environnement, ou dans la console, sous Settings puis Sandbox. Les environnements sont créés via l’API à l’adresse POST /v1/environments.

local exécute le code comme processus enfant du runtime, sur l’hôte, avec le compte utilisateur du runtime. Il s’agit du mode par défaut. Il convient si vous êtes le seul utilisateur et si l’agent ne lit que les fichiers dont vous êtes propriétaire. Ce mode n’assure aucune isolation. Un appel d’outil qui supprime des fichiers supprime vos fichiers, et un appel d’outil qui lit /etc/sandbase/runtime.env lit votre clé de provider.

docker démarre un conteneur par session.

{
  "sandbox_provider": "docker",
  "image": "node:22-slim",
  "resources": { "memory": "1g", "cpu": 1 }
}

La session dispose de son propre système de fichiers, de sa propre limite mémoire et de sa propre part de CPU. Le conteneur est supprimé avec la session. Passez à ce mode dès qu’un agent exécute du code que vous n’avez pas écrit. En contrepartie, l’utilisateur du runtime doit pouvoir accéder au socket Docker, et l’appartenance au groupe docker équivaut à disposer des privilèges root sur l’hôte. Les conteneurs créés par session ont la même architecture que les sandboxes d’agents auto-hébergées avec un conteneur par exécution. Le raisonnement concernant les ressources auxquelles un processus échappé pourrait accéder s’applique donc sans modification.

kubernetes exécute la charge de travail de la session dans un pod et la pilote avec kubectl exec et kubectl cp. L’image du runtime doit contenir kubectl, et son ServiceAccount doit disposer des permissions RBAC (role-based access control) nécessaires pour créer, supprimer, obtenir, lister et surveiller les pods dans le namespace cible, ainsi que la sous-ressource exec. Ce mode ne justifie la configuration nécessaire que si vous exploitez déjà un cluster.

Pourquoi le runtime est-il lié à 127.0.0.1 ?

Parce qu’il démarre avec l’authentification désactivée. Le runtime active l’authentification par bearer token lorsqu’au moins une clé API existe, et une nouvelle init n’en crée aucune. Le lier à 0.0.0.0 dans cette configuration exposerait sur Internet un runtime d’agent sans authentification, qui dispose d’outils shell et de votre clé de fournisseur.

Si vous voulez donc le rendre accessible, ne modifiez pas l’adresse d’écoute et effectuez deux autres opérations.

Commencez par activer l’authentification. Définissez MANAGED_AGENTS_API_KEY dans le fichier d’environnement du service, ou créez une clé avec POST /v1/api-keys. Cette commande renvoie un champ secret_key une seule fois et ne l’affiche plus ensuite. Les clients envoient alors Authorization: Bearer <key> à chaque requête.

Ensuite, placez un reverse proxy devant le runtime et terminez TLS (transport layer security) à ce niveau. Le runtime sert volontairement du HTTP en clair et attend qu’un autre composant gère les certificats.

server {
    listen 443 ssl;
    server_name agents.example.com;

    ssl_certificate     /etc/letsencrypt/live/agents.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/agents.example.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Connection "";
        proxy_buffering off;
        proxy_read_timeout 3600s;
    }
}

Deux de ces lignes ne sont pas décoratives. proxy_buffering off est nécessaire, car les sessions sont transmises avec des server-sent events (SSE). Lorsque la mise en tampon est activée, nginx conserve la réponse jusqu’à ce que son buffer soit plein. La console n’affiche alors rien pendant que l’agent travaille, puis affiche tout le contenu à la fin. proxy_read_timeout 3600s est également nécessaire, car la valeur par défaut est de 60 secondes. Un flux qui reste silencieux plus d’une minute est donc fermé par le proxy au milieu d’une interaction. L’échec donne alors l’impression que le runtime s’est arrêté brutalement.

Sur le firewall, ouvrez 22 et 443. Laissez 3000 fermé, car le proxy y accède via la loopback et aucun accès extérieur à la machine ne doit être possible.

Pointez le SDK Anthropic vers votre propre serveur

Le runtime implémente une surface /v1 compatible avec CMA. Un client du SDK Anthropic peut donc communiquer avec lui en ne modifiant qu’un champ.

import Anthropic from '@anthropic-ai/sdk';

const client = new Anthropic({
  apiKey: process.env.MANAGED_AGENTS_API_KEY ?? 'local-dev-key',
  baseURL: 'http://127.0.0.1:3000'
});

Il accepte également les en-têtes bêta envoyés par les clients Claude Managed Agents, anthropic-beta: managed-agents-2026-04-01 et anthropic-beta: agent-memory-2026-07-22. Ils sont facultatifs avec un runtime local. Ils permettent au code écrit pour un déploiement hébergé de fonctionner ici sans modification.

La compatibilité est étroite, mais pas totale. Consultez docs/api-matrix.md dans le checkout avant de supposer qu’une fonctionnalité est disponible. Le projet y documente ses propres limites, notamment pour les outils personnalisés côté client, qui nécessitent encore un enregistrement nommé au-dessus du protocole actuel de résultats d’événements.

Le protocole HTTP simple fonctionne tout aussi bien. C’est le moyen le plus rapide de vérifier que le runtime est actif :

curl -N -X POST http://127.0.0.1:3000/v1/sessions/SESSION_ID/messages \
  -H "Content-Type: application/json" \
  -d '{"content": "Hello", "stream": true}'

Une réponse saine est un flux d’événements qui continue d’arriver. Si la connexion est interrompue, reprenez à partir du dernier événement reçu au lieu de rejouer tout le tour :

curl -N http://127.0.0.1:3000/v1/sessions/SESSION_ID/events/stream \
  -H "Last-Event-ID: EVENT_ID"

Ce flux reprenable permet à une session de survivre à la fermeture d’un ordinateur portable. Les événements sont persistés sur le serveur. Le client rejoue donc un journal au lieu de conserver l’unique copie.

Emplacement des identifiants, de la mémoire et des journaux d’audit sur le disque

Tout ce que gère le runtime se trouve sous .managed-agents/ dans le workspace.

.managed-agents/
├── config.yaml
├── data.db
├── logs/runtime.log
├── files/
├── skills/
├── snapshots/
└── sandbox/
  • data.db contient les métadonnées SQLite : agents, sessions, entrées du coffre d’identifiants, entrées du store de mémoire et clés API.
  • files/ contient les octets des fichiers téléversés et skills/ contient les packages de skills téléversés.
  • snapshots/ contient les snapshots des workspaces de session, et sandbox/ contient les répertoires de travail des sessions en mode local.
  • logs/runtime.log est le premier emplacement à vérifier lorsqu’une action ne produit aucun effet sans afficher d’erreur.

Les coffres d’identifiants regroupent des secrets. Chaque secret est ajouté avec un auth_type tel que environment_variable, puis associé à une session via vault_ids lors de la création de celle-ci. Les stores de mémoire contiennent des entrées nommées que vous montez dans une session sous la forme d’un memory_store, avec ses propres paramètres d’accès et instructions. Les deux sont stockés dans data.db. C’est précisément ce qui distingue ce fonctionnement d’un appel direct au modèle : le runtime conserve les informations entre les sessions et enregistre ce qui s’est passé.

Comme tout se trouve dans un seul répertoire, sauvegardez-le en une seule fois.

sudo systemctl stop sandbase
sudo tar czf /root/sandbase-$(date +%F).tgz -C /opt/sandbase/workspace .managed-agents
sudo systemctl start sandbase

Arrêtez d’abord le service. Copier une base de données SQLite pendant que le runtime y écrit peut produire un fichier qui ne s’ouvrira pas lors de la restauration. Vous risquez de ne le découvrir que le jour où vous en aurez besoin. Si vous préférez conserver les fichiers YAML des agents dans git et l’état ailleurs, la documentation de déploiement permet de fixer l’emplacement de l’état avec --data-dir sur start.

La restauration s’effectue dans l’ordre inverse : récupérez le même tag sur une nouvelle machine, décompressez l’archive dans le workspace, puis démarrez le service. La clé de votre provider ne figure pas dans l’archive si vous avez utilisé la forme ${OPENAI_API_KEY}. Conservez-la donc dans un emplacement auquel vous aurez toujours accès.

L’exécuter avec systemd

Attribuez au runtime son propre utilisateur afin qu’un appel d’outil en mode sandbox local ne puisse pas agir à votre place.

sudo adduser --system --group --no-create-home --home /opt/sandbase sandbase
sudo chown -R sandbase:sandbase /opt/sandbase

Enregistrez ce contenu sous /etc/systemd/system/sandbase.service.

[Unit]
Description=SandBase Harness runtime
After=network-online.target

[Service]
User=sandbase
Group=sandbase
WorkingDirectory=/opt/sandbase/workspace
EnvironmentFile=/etc/sandbase/runtime.env
ExecStart=/usr/bin/node /opt/sandbase/sandbase-harness/dist/index.js start --host 127.0.0.1 --port 3000
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

L’exemple de déploiement du projet appelle un binaire managed-agents sur PATH. Une installation depuis les sources taguées n’en crée pas. ExecStart exécute donc node avec le point d’entrée compilé.

sudo systemctl daemon-reload
sudo systemctl enable --now sandbase
sudo systemctl status sandbase
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3000/dashboard

Un résultat correct est active (running) depuis status et 200 depuis curl. Dans les autres cas, consultez d’abord journalctl -u sandbase -n 50, puis .managed-agents/logs/runtime.log. enable --now est la partie importante, car un processus démarré manuellement disparaît au redémarrage suivant.

Ce qui échoue et le message affiché

npm run build est arrêté sans message d’erreur de npm. Sur un VPS de 1 GB, la compilation TypeScript est arrêtée par l’out-of-memory killer du kernel. Celui-ci écrit le message dans le kernel log, pas dans npm. Vérifiez-le avec journalctl -k | grep -i "out of memory", qui affiche une ligne indiquant le processus node arrêté. Ajoutez de la swap, ou effectuez le build sur une instance plus grande, puis copiez dist/.

Error: listen EADDRINUSE: address already in use 127.0.0.1:3000. Un autre processus utilise déjà le port. sudo ss -lntp | grep 3000 permet de l’identifier. Arrêtez ce processus ou démarrez le runtime avec --port 3001, puis mettez à jour le proxy.

Le dashboard ne se charge pas depuis votre laptop. C’est le comportement attendu, car le runtime écoute sur l’interface loopback. Utilisez le tunnel SSH indiqué plus haut ou terminez la configuration du reverse proxy. Ne corrigez pas le problème avec --host 0.0.0.0, car l’authentification reste désactivée tant qu’aucune key n’existe.

Les sandboxes Docker échouent avec permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock. L’utilisateur sandbase ne fait pas partie du groupe docker. Corrigez cela avec sudo usermod -aG docker sandbase, puis redémarrez le service. Gardez à l’esprit ce que vous avez accordé : ce groupe dispose des privilèges root sur l’hôte. Cela annule donc une partie de l’isolation recherchée en attribuant son propre utilisateur au runtime.

Les sandboxes Kubernetes échouent avec Error from server (Forbidden). La ServiceAccount ne dispose pas des permissions nécessaires sur les pods ou sur le subresource exec. Vérifiez-le directement avec kubectl auth can-i create pods/exec -n <namespace>, qui renvoie yes ou no.

Toutes les requêtes renvoient 401 après l’ajout d’une API key. L’authentification s’active dès qu’une première key existe. Elle s’applique à la console comme à l’API. Envoyez Authorization: Bearer <key>. Si vous avez perdu la key, créez-en une autre, car secret_key n’est renvoyé qu’une seule fois et n’est pas stocké sous une forme lisible.

Les outils d’un serveur MCP n’apparaissent jamais dans une session. Comparez le mcp_server_name du bloc tools avec le name dans mcp_servers. Vérifiez ensuite que le runtime peut atteindre l’URL depuis le serveur lui-même avec curl -i <url>. Un serveur MCP de type URL est une dépendance réseau. Un VPS résout les noms et route le trafic différemment de votre laptop.

FAQ

Puis-je exécuter SandBase Harness sans clé OpenAI ou Anthropic ?

Oui, si vous disposez d’un endpoint compatible avec OpenAI. Le runtime prend en charge les fournisseurs OpenAI, Anthropic et compatibles avec OpenAI. Un serveur local qui parle l’API OpenAI fonctionne donc également. Définissez le fournisseur du workspace dans .managed-agents/config.yaml, puis indiquez l’endpoint dans api_key. Le runtime n’inclut aucun modèle. Un service doit donc répondre aux appels.

Est-il sûr d’exposer le runtime sur un port public ?

Non, pas avec la configuration par défaut. Il écoute sur 127.0.0.1:3000 et démarre avec l’authentification désactivée. La correction ne consiste pas à modifier l’adresse d’écoute. Créez une clé d’API ou définissez MANAGED_AGENTS_API_KEY pour activer l’authentification par bearer token. Placez ensuite nginx ou Caddy devant le runtime pour gérer TLS. Fermez le port 3000 dans le firewall afin que le proxy soit le seul point d’accès.

Quelle est la différence entre les sandbox locales, Docker et Kubernetes ?

local exécute le code des outils comme un processus enfant du runtime sur l’hôte, avec les permissions de l’utilisateur du runtime et sans isolation. docker attribue à chaque session son propre conteneur, avec son propre système de fichiers, une limite de mémoire et une part de CPU, puis le supprime à la fin de la session. kubernetes exécute la session comme un pod et le pilote avec kubectl exec. Cette méthode nécessite kubectl dans l’image du runtime, ainsi que les droits RBAC sur les pods et sur la sous-ressource exec dans le namespace cible.

Que dois-je exactement sauvegarder ?

Le répertoire .managed-agents/ du workspace. Il contient config.yaml, la base de données SQLite data.db avec les agents, les sessions, les entrées du credential vault et les entrées de mémoire, ainsi que les fichiers importés, les packages de skills et les snapshots de session. Arrêtez le service avant de le copier afin que SQLite ne soit pas modifiée pendant la création de l’archive. Les clés d’API des fournisseurs référencées comme ${OPENAI_API_KEY} ne se trouvent pas dans la sauvegarde. Stockez-les séparément.

Pourquoi cloner le tag v0.3.2 au lieu de main ?

Un tag correspond à un arbre de fichiers fixe. Les clés de configuration et les commandes CLI que vous lisez sont donc celles que vous obtenez réellement. main évolue, et une clé de configuration peut être renommée entre la rédaction d’un guide et son exécution. Le projet précise également que le package managed-agents, non scoped, sur npm ne correspond pas à ce projet. npx managed-agents installe donc un élément sans rapport. La release v0.3.1 sert principalement à remplacer ce quick start npm par la procédure utilisant les sources du tag spécifié.