OpenClaw环境变量加载和使用机制研究

OpenClaw环境变量加载和使用机制研究
Environment Variables

OpenClaw环境变量加载和使用机制研究报告

一、核心机制概述

OpenClaw提供了完善的环境变量加载和管理机制,主要用于安全地处理API密钥等敏感信息。其核心设计理念是:

  • 配置优先,环境变量兜底:优先使用配置文件中的设置,环境变量作为备选方案
  • 类型安全的秘密引用:支持通过SecretRef对象引用环境变量、文件或执行命令获取的秘密
  • 自动注入机制:技能可以自动从环境变量加载配置的API密钥
  • 安全沙箱隔离:对环境变量进行 sanitize 处理,防止注入攻击

二、环境变量文件位置与生效优先级

1. 环境变量文件位置

OpenClaw支持多种环境变量文件位置,按加载顺序排列:

(1)系统级环境变量

  • 位置:操作系统级别的环境变量,通过export命令设置
  • 示例export MY_SKILL_API_KEY="sk-xxxxxxxx"
  • 生效范围:全局生效,影响所有OpenClaw实例

(2)用户级环境变量文件

  • 位置~/.openclaw/.env
  • 格式:标准dotenv格式,每行一个环境变量
  • 示例
    MY_SKILL_API_KEY="sk-xxxxxxxx"
    NODE_ENV="production"
    
  • 生效范围:当前用户的所有OpenClaw实例

(3)项目级环境变量文件

  • 位置:项目根目录下的.env文件
  • 格式:标准dotenv格式
  • 生效范围:仅影响当前项目的OpenClaw实例

(4)运行时临时环境变量

  • 位置:通过命令行参数传递
  • 示例MY_SKILL_API_KEY="sk-xxxxxxxx" openclaw start
  • 生效范围:仅影响当前运行的OpenClaw实例

2. 环境变量生效优先级

OpenClaw环境变量的生效优先级从高到低为:

运行时临时环境变量 > 项目级.env文件 > 用户级~/.openclaw/.env文件 > 系统级环境变量

3. 配置文件与环境变量的优先级

当配置文件和环境变量同时存在时,OpenClaw的优先级规则为:

  • 显式配置优先:如果配置文件中明确设置了值(非SecretRef),则优先使用配置文件中的值
  • SecretRef次之:如果配置文件中使用了SecretRef引用环境变量,则解析对应的环境变量
  • 环境变量兜底:如果配置文件中未设置,且没有SecretRef引用,则直接读取环境变量

三、环境变量读取的三种方式

1. 直接读取(简单场景)

// 直接读取环境变量
const apiKey = process.env.MY_SKILL_API_KEY;

适用场景:快速原型开发、简单技能 示例:web-search.ts中直接读取BRAVE_API_KEY

2. 使用normalizeResolvedSecretInputString(推荐方式)

import { normalizeResolvedSecretInputString } from "../config/types.secrets.js";

// 从配置或环境变量读取API密钥
const apiKey = normalizeResolvedSecretInputString({
  value: config.apiKey, // 配置文件中的值
  path: "skills.entries.my-skill.apiKey", // 配置路径,用于错误提示
});

适用场景:正式技能开发,支持配置文件和环境变量两种方式 示例:tts.ts中读取ElevenLabs和OpenAI的API密钥

3. 使用SecretRef对象(高级场景)

// 在配置文件中定义SecretRef
{
  "apiKey": {
    "source": "env",
    "provider": "default",
    "id": "MY_SKILL_API_KEY"
  }
}

// 在代码中解析
const { ref } = resolveSecretInputRef({
  value: config.apiKey,
  defaults: config.secrets?.defaults,
});

适用场景:需要动态切换秘密来源的复杂场景 示例:secrets/command-config.ts中的秘密解析逻辑

四、技能中读取API密钥的最佳实践

1. 配置文件优先,环境变量兜底

import { normalizeResolvedSecretInputString } from "../config/types.secrets.js";
import { normalizeSecretInput } from "../utils/normalize-secret-input.js";

function resolveMySkillApiKey(config?: MySkillConfig): string | undefined {
  // 优先使用配置文件中的值
  const fromConfig = normalizeResolvedSecretInputString({
    value: config?.apiKey,
    path: "skills.entries.my-skill.apiKey",
  });
  
  // 环境变量作为备选
  const fromEnv = normalizeSecretInput(process.env.MY_SKILL_API_KEY);
  
  return fromConfig || fromEnv;
}

2. 在技能元数据中声明依赖

// 在SKILL.md或技能配置中声明所需环境变量
{
  "metadata": {
    "primaryEnv": "MY_SKILL_API_KEY",
    "requires": {
      "env": ["MY_SKILL_API_KEY"]
    }
  }
}

3. 使用applySkillEnvOverrides自动注入

import { applySkillEnvOverrides } from "../agents/skills/env-overrides.js";

// 自动将技能配置中的环境变量注入到process.env
const revert = applySkillEnvOverrides({
  skills: [mySkillEntry],
  config: openClawConfig,
});

// 使用后清理
revert();

五、环境变量的安全机制

1. 敏感信息过滤

  • 自动过滤包含null字节的环境变量值
  • 对危险的环境变量名称进行拦截(如OPENSSL_CONF)
  • 支持环境变量白名单机制

2. 沙箱隔离

import { sanitizeEnvVars } from "../agents/sandbox/sanitize-env-vars.js";

// 清理环境变量,只保留允许的变量
const sanitizedEnv = sanitizeEnvVars(process.env, {
  allowlist: ["MY_SKILL_API_KEY", "NODE_ENV"],
});

3. 秘密解析验证

import { assertSecretInputResolved } from "../config/types.secrets.js";

// 验证秘密是否已解析
assertSecretInputResolved({
  value: config.apiKey,
  path: "skills.entries.my-skill.apiKey",
});

六、完整示例:开发一个使用环境变量的Python技能

1. 技能配置文件

{
  "name": "my-python-skill",
  "version": "1.0.0",
  "description": "使用环境变量API密钥的Python示例技能",
  "metadata": {
    "primaryEnv": "MY_PYTHON_SKILL_API_KEY",
    "requires": {
      "env": ["MY_PYTHON_SKILL_API_KEY"]
    }
  },
  "config": {
    "apiKey": "${MY_PYTHON_SKILL_API_KEY}", // 环境变量引用模板
    "endpoint": "https://api.example.com/v1",
    "pythonPath": "/usr/bin/python3"
  }
}

2. 技能实现代码(Python)

import os
import sys
import json
import requests
from typing import Dict, Any, Optional

def resolve_api_key(config: Optional[Dict[str, Any]]) -> Optional[str]:
    """
    从配置文件或环境变量解析API密钥
    :param config: 技能配置字典
    :return: API密钥或None
    """
    # 优先使用配置文件中的值
    if config and "apiKey" in config:
        api_key = config["apiKey"]
        # 如果是环境变量引用模板,解析对应的环境变量
        if api_key.startswith("${") and api_key.endswith("}"):
            env_var_name = api_key[2:-1]
            return os.getenv(env_var_name)
        return api_key
    
    # 环境变量作为备选
    return os.getenv("MY_PYTHON_SKILL_API_KEY")

def execute_skill(tool_call_id: str, args: Dict[str, Any], config: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
    """
    执行Python技能
    :param tool_call_id: 工具调用ID
    :param args: 工具调用参数
    :param config: 技能配置
    :return: 执行结果
    """
    # 解析API密钥
    api_key = resolve_api_key(config)
    if not api_key:
        return {
            "success": False,
            "error": "missing_api_key",
            "message": "MY_PYTHON_SKILL_API_KEY环境变量未设置"
        }
    
    # 获取查询参数
    query = args.get("query")
    if not query:
        return {
            "success": False,
            "error": "missing_parameter",
            "message": "缺少必填参数: query"
        }
    
    # 获取API端点
    endpoint = config.get("endpoint", "https://api.example.com/v1")
    
    try:
        # 使用API密钥调用外部服务
        response = requests.post(
            f"{endpoint}/search",
            headers={
                "Authorization": f"Bearer {api_key}",
                "Content-Type": "application/json"
            },
            json={"query": query},
            timeout=10
        )
        response.raise_for_status()
        data = response.json()
        
        return {
            "success": True,
            "result": data
        }
    except requests.exceptions.RequestException as e:
        return {
            "success": False,
            "error": "api_request_failed",
            "message": f"API请求失败: {str(e)}"
        }

if __name__ == "__main__":
    # 从命令行参数获取输入
    if len(sys.argv) < 2:
        print(json.dumps({
            "success": False,
            "error": "missing_input",
            "message": "缺少输入参数"
        }))
        sys.exit(1)
    
    try:
        input_data = json.loads(sys.argv[1])
        tool_call_id = input_data.get("toolCallId", "")
        args = input_data.get("args", {})
        config = input_data.get("config", {})
        
        result = execute_skill(tool_call_id, args, config)
        print(json.dumps(result))
    except json.JSONDecodeError as e:
        print(json.dumps({
            "success": False,
            "error": "invalid_json",
            "message": f"无效的JSON输入: {str(e)}"
        }))
        sys.exit(1)

3. TypeScript包装器(用于OpenClaw集成)

import { Type } from "@sinclair/typebox";
import { spawn } from "node:child_process";
import type { AnyAgentTool } from "./common.js";
import { jsonResult, readStringParam } from "./common.js";

const MyPythonSkillSchema = Type.Object({
  query: Type.String({ description: "查询字符串" }),
});

export function createMyPythonSkillTool(config?: OpenClawConfig): AnyAgentTool | null {
  const skillConfig = config?.skills?.entries?.["my-python-skill"];
  if (!skillConfig) return null;

  return {
    label: "我的Python技能",
    name: "my_python_skill",
    description: "使用环境变量API密钥的Python示例技能",
    parameters: MyPythonSkillSchema,
    execute: async (_toolCallId, args) => {
      const query = readStringParam(args, "query", { required: true });
      const pythonPath = skillConfig.config?.pythonPath || "python3";
      
      return new Promise((resolve) => {
        // 生成Python子进程执行技能
        const pythonProcess = spawn(pythonPath, [
          "-c",
          `import sys, json; sys.path.insert(0, '${__dirname}'); from my_python_skill import execute_skill; print(json.dumps(execute_skill('', ${JSON.stringify(args)}, ${JSON.stringify(skillConfig.config)})))`
        ]);
        
        let output = "";
        let errorOutput = "";
        
        pythonProcess.stdout.on("data", (data) => {
          output += data.toString();
        });
        
        pythonProcess.stderr.on("data", (data) => {
          errorOutput += data.toString();
        });
        
        pythonProcess.on("close", (code) => {
          if (code !== 0) {
            resolve(jsonResult({
              success: false,
              error: "python_process_failed",
              message: `Python进程执行失败: ${errorOutput}`
            }));
            return;
          }
          
          try {
            const result = JSON.parse(output);
            resolve(jsonResult(result));
          } catch (e) {
            resolve(jsonResult({
              success: false,
              error: "invalid_json_output",
              message: `Python脚本返回无效JSON: ${output}`
            }));
          }
        });
      });
    },
  };
}

4. 使用方式

(1)设置环境变量

# 方式1: 临时设置
export MY_PYTHON_SKILL_API_KEY="sk-xxxxxxxxxxxxxxxx"

# 方式2: 添加到~/.openclaw/.env文件
cat >> ~/.openclaw/.env << EOF
MY_PYTHON_SKILL_API_KEY="sk-xxxxxxxxxxxxxxxx"
EOF

(2)运行OpenClaw

openclaw start

(3)调用技能

{
  "tool": "my_python_skill",
  "args": {
    "query": "example search query"
  }
}

七、总结

OpenClaw提供了灵活且安全的环境变量管理机制,技能开发者可以根据需求选择合适的方式读取API密钥:

  • 对于简单技能,直接使用process.env即可
  • 对于正式技能,推荐使用normalizeResolvedSecretInputString支持配置文件和环境变量两种方式
  • 对于复杂场景,可以使用SecretRef对象实现动态秘密来源切换

同时,通过环境变量白名单、敏感信息过滤和沙箱隔离等安全机制,OpenClaw确保了环境变量的安全使用,防止敏感信息泄露和注入攻击。

📖 相似文章推荐

上一篇
卷王小组:GitHub趋势分析报告(2026年2月24日-3月2日)
下一篇
OpenClaw 2026.3.2 版本更新详解