Skip to content

Entrée de configuration openclaw.json

源码版本v2026.6.11

Responsabilités

openclaw.json est l'unique fichier de configuration auteur (config) d'OpenClaw: port de la passerelle, provider de modèle, credentials des canaux, chemins de chargement des compétences, cron, stratégie de mémoire, approvals, hooks — tout est écrit dans ce seul fichier JSON5. Au runtime, toute config est lue depuis ce fichier, puis passe par expansion include, substitution de variables d'environnement, validation Zod schema, injection de valeurs par défaut, pour devenir l'objet OpenClawConfig en mémoire.

Ce n'est pas juste « lire une fois et c'est fini »: les config set/patch/unset du CLI écrivent atomiquement en retour; le snapshot runtime (runtime snapshot) est comparé au contenu disque pour empêcher les race conditions; en cas de problème, un backup last-known-good permet de rouler. Cette entrée assume donc cinq choses: lecture, écriture, validation, backup, migration.

Motivation de conception

Pourquoi un seul fichier plutôt qu'un répertoire éclaté? Aux débuts, OpenClaw mettait provider, channel, memory dans des JSON séparés, ce qui faisait que modifier un provider imposait de toucher trois fichiers, et les montées de version de plugin de canal éparpillaient la logique de migration. En convergeant vers un seul openclaw.json, version, backup et audit log sont tous accrochés au même fichier; openclaw doctor ne scanne qu'un seul chemin pour faire un bilan.

Mais un seul fichier devient gros, d'où deux échappatoires:

  • $include pousse de grands blocs provider/channel dans des sous-fichiers, le fichier racine ne gardant que la référence;
  • ${VAR} substitution de variables d'environnement pour sortir les secrets du fichier.

Ces deux features permettent à openclaw.json d'entrer dans un dépôt git (après脱敏), tout en s'exécutant dans des déploiements Docker/Tailscale qui ne fournissent que des variables d'environnement.

Fichiers clés

Flux de données

Au démarrage, createConfigIO épingle d'abord les constantes de chemin, puis passe par loadConfigLocal (loadConfigLocal:1654). Les étapes clés du chemin de lecture:

typescript
function loadConfigLocal(options: { skipSuspiciousRecovery?: boolean } = {}): OpenClawConfig {
  try {
    maybeLoadDotEnvForConfig(deps.env);
    const envBeforeRead = snapshotEnv(deps.env);
    if (!deps.fs.existsSync(configPath)) {
      // ... charge le shell env fallback puis renvoie une config vide
      return {};
    }
    const raw = deps.fs.readFileSync(configPath, "utf-8");
    const parsed = deps.json5.parse(raw);
    const readResolution = resolveConfigForRead(
      resolveConfigIncludesForRead(parsed, configPath, deps),
      deps.env,
      deps.lowerPrecedenceEnv,
    );
    // ... shipped plugin install records migration, validate, materialize

Trois choses à noter: maybeLoadDotEnvForConfig ne charge dotenv que si env est égal à process.env (l'env injecté en tests n'est pas touché); envBeforeRead sert à détecter à l'écriture « si l'env a été modifié pendant la lecture »; resolveConfigIncludesForRead lit d'abord les fichiers $include cibles, puis resolveConfigForRead procède à la substitution ${VAR}. L'ordre est crucial: include avant substitution env, ainsi les champs importés par include peuvent aussi utiliser ${VAR}.

Le chemin d'écriture passe par writeConfigFile (writeConfigFile:2826). Quand il existe un snapshot runtime, il convertit d'abord la config runtime fournie par le caller en patch, puis la projette vers la forme source:

typescript
export async function writeConfigFile(
  cfg: OpenClawConfig,
  options: ConfigWriteOptions = {},
): Promise<ConfigWriteResult> {
  options.assertConfigPathForWrite?.();
  const io = createConfigIO({ /* ... */ });
  assertConfigWriteAllowedInCurrentMode({ configPath: io.configPath });
  let nextCfg = cfg;
  const runtimeConfigSnapshot = getRuntimeConfigSnapshotState();
  const runtimeConfigSourceSnapshot = getRuntimeConfigSourceSnapshotState();
  const hadBothSnapshots = Boolean(runtimeConfigSnapshot && runtimeConfigSourceSnapshot);
  if (hadBothSnapshots) {
    const runtimePatch = createMergePatch(runtimeConfigSnapshot!, cfg);
    nextCfg = coerceConfig(applyMergePatch(runtimeConfigSourceSnapshot!, runtimePatch));
  }

Cette étape garantit que ce que le caller réécrit est « la forme auteur utilisateur », et non la forme polluée par les valeurs par défaut runtime — sinon les defaults du schema seraient figés dans le fichier, impossibles à revenir en arrière lors d'un changement de version.

Le mode patch CLI (runConfigPatch:2279) reçoit un objet JSON5 patch et le fusionne récursivement dans la config existante:

typescript
function mergeConfigValue(existing: unknown, patch: unknown, path: PathSegment[]): unknown {
  if (isProviderModelListPath(path) && Array.isArray(existing) && Array.isArray(patch)) {
    return mergeModelArrays(existing, patch);
  }
  if (isPlainRecord(existing) && isPlainRecord(patch)) {
    // deep merge: récursion sur les objets, écrasement direct sur les non-objets

Le tableau models[] passe par mergeModelArrays, qui dédoublonne par champ id puis fusionne les éléments, plutôt qu'un remplacement simple — ajouter un modèle à un provider n'écrase pas tous les modèles existants.

Relation entre les trois chemins lecture, écriture, patch:

Limites et modes d'échec

  • Dérive de chemin: la passerelle au démarrage doit d'abord appeler pinRuntimePaths (pinRuntimePaths:236-245), sinon OPENCLAW_CONFIG_PATH défini après l'import du module lira une valeur périmée, provoquant un déchirement « lire un chemin, écrire un autre ».
  • Repli legacy: resolveConfigPath scanne d'abord clawdbot.json, .clawdbot/, puis only le nouveau chemin. Lors d'un upgrade, si les deux répertoires existent, le nouveau est privilégié; mais l'utilisateur laissant l'ancien dans ~/.openclaw/, le nouveau process le lira aussi — la commande doctor scanne spécifiquement ce schizo.
  • $include hors périmètre: par défaut, ne peut référencer que des fichiers du même répertoire que openclaw.json ou de ses sous-répertoires. Pour sortir du répertoire, il faut explicitement poser $OPENCLAW_INCLUDE_ROOTS, chaque racine est tilde-expand + dédoublonnée + rejetée si non absolue (include roots filter:128-141).
  • ${VAR} manquante: ne jette pas, n'émet qu'un warn « feature using this value will be unavailable » (src/config/io.ts:1950-1956), la passerelle entre en mode dégradé plutôt que de refuser de démarrer. Mais une apiKey provider manquante jette à la première vraie requête.
  • Nix mode en lecture seule: OPENCLAW_NIX_MODE=1, assertConfigWriteAllowedInCurrentMode refuse toutes les écritures, la config est gérée par Nix externe (resolveIsNixMode:16-18).
  • Écriture atomique: replaceFileAtomic écrit un fichier temporaire puis rename, permission répertoire 0o700, permission fichier 0o600, Windows passe par copy fallback.
  • Blocage si chute brutale de taille: avant écriture, compare previousBytes vs nextBytes; si chute à 50% sans flag allowConfigSizeDrop, refuse d'écrire — pour empêcher qu'un patch ne vire tout un bloc provider.
  • last-known-good: chaque lecture réussie met à jour l'empreinte lastKnownGood (hash + bytes + mtime + dev/ino); si la lecture suivante détecte une chute de size ou que le champ gateway mode est effacé, restaure automatiquement depuis .bak, audit log enregistre un événement config.observe.
  • Avertissement future version: quand le fichier meta.lastTouchedVersion est plus récent que le binaire courant, warn suggère que le PATH pointe peut-être vers une vieille version openclaw — pour éviter « config écrite par nouvelle version lue par ancienne version ».

Résumé

openclaw.json est une entrée unique, mais背后 c'est tout un cycle de vie lecture-validation-matérialisation-écriture-audit. Le chemin lecture déplie include et env, le chemin écriture projette la forme runtime inverse vers la forme source, le mode patch offre au CLI une édition incrémentale structurée. Comment le schema définit la forme légale de chaque champ, voir validation runtime Zod Schema; les champs spécifiques du provider (baseUrl/api/auth/apiKey/models[]) sont détaillés dans couche capacités: modèle Provider; la config côté canaux dans canaux: configuration; comment le process daemon consomme la config lue dans déploiement: daemon.

Pour comparer avec la documentation officielle: Configuration docs · README.