refactor: use YAML map for gateway environment variables instead of string parsing
Benefits:
- More idiomatic configuration format for Home Assistant add-ons
- config.yaml: Change default from empty string to empty YAML map
- config.yaml: Update schema type from str? to map(str)?
- config.yaml: Improve documentation to explain YAML map usage
- run.sh: Parse JSON object from jq using to_entries instead of semicolon splitting
- oc_config_helper.py: Accept JSON string and parse as dict instead of semicolon-separated values
- Better error handling for invalid JSON
- Cleaner and more maintainable parsing logic
This allows users to configure environment variables more naturally:
gateway_env_vars:
OPENAI_API_KEY: sk-abc123
SERVICE_URL: https://api.example.com
This commit is contained in:
@@ -88,15 +88,20 @@ options:
|
|||||||
# (can affect Telegram API polling in some HAOS/VM setups).
|
# (can affect Telegram API polling in some HAOS/VM setups).
|
||||||
force_ipv4_dns: false
|
force_ipv4_dns: false
|
||||||
|
|
||||||
# Environment variables to pass to the gateway (semicolon-separated key=value pairs)
|
# Environment variables to pass to the gateway as a YAML map
|
||||||
# Format: "KEY1=value1;KEY2=value2" (semicolon-separated, no spaces around '=')
|
# These variables are exported to the gateway process at startup
|
||||||
# Examples:
|
# Examples:
|
||||||
# - API keys: "OPENAI_API_KEY=sk-abc123;ANTHROPIC_API_KEY=claude-key"
|
# API keys:
|
||||||
# - Service URLs: "SERVICE_URL=https://api.example.com;LOG_LEVEL=debug"
|
# OPENAI_API_KEY: "sk-abc123"
|
||||||
# - Features: "FEATURE_FLAG_X=1;FEATURE_FLAG_Y=0"
|
# ANTHROPIC_API_KEY: "claude-key"
|
||||||
|
# Service URLs:
|
||||||
|
# SERVICE_URL: "https://api.example.com"
|
||||||
|
# LOG_LEVEL: "debug"
|
||||||
|
# Feature flags:
|
||||||
|
# FEATURE_FLAG_X: "1"
|
||||||
|
# FEATURE_FLAG_Y: "0"
|
||||||
# Limits: max 50 variables, max key length 255 chars, max value length 10000 chars
|
# Limits: max 50 variables, max key length 255 chars, max value length 10000 chars
|
||||||
# Note: Values may contain special characters; they are not shell-escaped
|
gateway_env_vars: {}
|
||||||
gateway_env_vars: ""
|
|
||||||
|
|
||||||
|
|
||||||
schema:
|
schema:
|
||||||
@@ -119,4 +124,4 @@ schema:
|
|||||||
enable_openai_api: bool?
|
enable_openai_api: bool?
|
||||||
allow_insecure_auth: bool?
|
allow_insecure_auth: bool?
|
||||||
force_ipv4_dns: bool?
|
force_ipv4_dns: bool?
|
||||||
gateway_env_vars: str?
|
gateway_env_vars: map(str)?
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ def set_gateway_setting(key, value):
|
|||||||
return write_config(cfg)
|
return write_config(cfg)
|
||||||
|
|
||||||
|
|
||||||
def apply_gateway_settings(mode: str, bind_mode: str, port: int, enable_openai_api: bool, allow_insecure_auth: bool, env_vars: str = ""):
|
def apply_gateway_settings(mode: str, bind_mode: str, port: int, enable_openai_api: bool, allow_insecure_auth: bool, env_vars: str = "{}"):
|
||||||
"""
|
"""
|
||||||
Apply gateway settings to OpenClaw config.
|
Apply gateway settings to OpenClaw config.
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ def apply_gateway_settings(mode: str, bind_mode: str, port: int, enable_openai_a
|
|||||||
port: Port number to listen on (must be 1-65535)
|
port: Port number to listen on (must be 1-65535)
|
||||||
enable_openai_api: Enable OpenAI-compatible Chat Completions endpoint
|
enable_openai_api: Enable OpenAI-compatible Chat Completions endpoint
|
||||||
allow_insecure_auth: Allow insecure HTTP authentication
|
allow_insecure_auth: Allow insecure HTTP authentication
|
||||||
env_vars: Semicolon-separated environment variables (e.g., "VAR1=value1;VAR2=value2")
|
env_vars: JSON string with environment variables as key-value pairs (e.g., '{"VAR1":"value1","VAR2":"value2"}')
|
||||||
"""
|
"""
|
||||||
# Validate gateway mode
|
# Validate gateway mode
|
||||||
if mode not in ["local", "remote"]:
|
if mode not in ["local", "remote"]:
|
||||||
@@ -142,23 +142,20 @@ def apply_gateway_settings(mode: str, bind_mode: str, port: int, enable_openai_a
|
|||||||
control_ui["allowInsecureAuth"] = allow_insecure_auth
|
control_ui["allowInsecureAuth"] = allow_insecure_auth
|
||||||
changes.append(f"allowInsecureAuth: {current_insecure} -> {allow_insecure_auth}")
|
changes.append(f"allowInsecureAuth: {current_insecure} -> {allow_insecure_auth}")
|
||||||
|
|
||||||
# Parse and apply environment variables
|
# Parse and apply environment variables from JSON
|
||||||
new_env_vars = {}
|
new_env_vars = {}
|
||||||
if env_vars.strip():
|
if env_vars.strip() and env_vars != "{}":
|
||||||
try:
|
try:
|
||||||
# Parse semicolon-separated key=value pairs
|
# Parse JSON object containing environment variables
|
||||||
pairs = env_vars.split(';')
|
parsed = json.loads(env_vars)
|
||||||
for pair in pairs:
|
if not isinstance(parsed, dict):
|
||||||
pair = pair.strip()
|
print(f"ERROR: Environment variables must be a JSON object (dict), got {type(parsed).__name__}")
|
||||||
if not pair:
|
return False
|
||||||
continue
|
|
||||||
if '=' not in pair:
|
|
||||||
print(f"WARN: Invalid env var format '{pair}' (missing '=')")
|
|
||||||
continue
|
|
||||||
|
|
||||||
key, value = pair.split('=', 1)
|
for key, value in parsed.items():
|
||||||
key = key.strip()
|
# Ensure value is string
|
||||||
value = value.strip()
|
if not isinstance(value, str):
|
||||||
|
value = str(value)
|
||||||
|
|
||||||
# Validate key format
|
# Validate key format
|
||||||
if not key:
|
if not key:
|
||||||
@@ -176,18 +173,17 @@ def apply_gateway_settings(mode: str, bind_mode: str, port: int, enable_openai_a
|
|||||||
print(f"ERROR: Environment variable '{key}' value too large (max {MAX_ENV_VAR_VALUE_SIZE} chars)")
|
print(f"ERROR: Environment variable '{key}' value too large (max {MAX_ENV_VAR_VALUE_SIZE} chars)")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Warn about duplicate keys
|
|
||||||
if key in new_env_vars:
|
|
||||||
print(f"WARN: Environment variable '{key}' appears multiple times; using last value")
|
|
||||||
|
|
||||||
new_env_vars[key] = value
|
new_env_vars[key] = value
|
||||||
|
|
||||||
# Enforce limit on total variables
|
# Enforce limit on total variables
|
||||||
if len(new_env_vars) > MAX_ENV_VARS:
|
if len(new_env_vars) > MAX_ENV_VARS:
|
||||||
print(f"ERROR: Too many environment variables ({len(new_env_vars)}), max {MAX_ENV_VARS}")
|
print(f"ERROR: Too many environment variables ({len(new_env_vars)}), max {MAX_ENV_VARS}")
|
||||||
return False
|
return False
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
print(f"ERROR: Failed to parse environment variables JSON: {e}")
|
||||||
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"ERROR: Failed to parse environment variables: {e}")
|
print(f"ERROR: Failed to process environment variables: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if new_env_vars != current_env_vars:
|
if new_env_vars != current_env_vars:
|
||||||
@@ -220,14 +216,14 @@ def main():
|
|||||||
|
|
||||||
if cmd == "apply-gateway-settings":
|
if cmd == "apply-gateway-settings":
|
||||||
if len(sys.argv) < 7:
|
if len(sys.argv) < 7:
|
||||||
print("Usage: oc_config_helper.py apply-gateway-settings <local|remote> <auto|loopback|lan|tailnet> <port> <enable_openai_api:true|false> <allow_insecure:true|false> [env_vars]")
|
print("Usage: oc_config_helper.py apply-gateway-settings <local|remote> <auto|loopback|lan|tailnet> <port> <enable_openai_api:true|false> <allow_insecure:true|false> [env_vars_json]")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
mode = sys.argv[2]
|
mode = sys.argv[2]
|
||||||
bind_mode = sys.argv[3]
|
bind_mode = sys.argv[3]
|
||||||
port = int(sys.argv[4])
|
port = int(sys.argv[4])
|
||||||
enable_openai_api = sys.argv[5].lower() == "true"
|
enable_openai_api = sys.argv[5].lower() == "true"
|
||||||
allow_insecure_auth = sys.argv[6].lower() == "true"
|
allow_insecure_auth = sys.argv[6].lower() == "true"
|
||||||
env_vars = sys.argv[7] if len(sys.argv) > 7 else ""
|
env_vars = sys.argv[7] if len(sys.argv) > 7 else "{}"
|
||||||
success = apply_gateway_settings(mode, bind_mode, port, enable_openai_api, allow_insecure_auth, env_vars)
|
success = apply_gateway_settings(mode, bind_mode, port, enable_openai_api, allow_insecure_auth, env_vars)
|
||||||
sys.exit(0 if success else 1)
|
sys.exit(0 if success else 1)
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ GATEWAY_PORT=$(jq -r '.gateway_port // 18789' "$OPTIONS_FILE")
|
|||||||
ENABLE_OPENAI_API=$(jq -r '.enable_openai_api // false' "$OPTIONS_FILE")
|
ENABLE_OPENAI_API=$(jq -r '.enable_openai_api // false' "$OPTIONS_FILE")
|
||||||
ALLOW_INSECURE_AUTH=$(jq -r '.allow_insecure_auth // false' "$OPTIONS_FILE")
|
ALLOW_INSECURE_AUTH=$(jq -r '.allow_insecure_auth // false' "$OPTIONS_FILE")
|
||||||
FORCE_IPV4_DNS=$(jq -r '.force_ipv4_dns // false' "$OPTIONS_FILE")
|
FORCE_IPV4_DNS=$(jq -r '.force_ipv4_dns // false' "$OPTIONS_FILE")
|
||||||
GW_ENV_VARS=$(jq -r '.gateway_env_vars // empty' "$OPTIONS_FILE")
|
GW_ENV_VARS=$(jq -c '.gateway_env_vars // {}' "$OPTIONS_FILE")
|
||||||
|
|
||||||
export TZ="$TZNAME"
|
export TZ="$TZNAME"
|
||||||
|
|
||||||
@@ -150,22 +150,22 @@ export PATH="${PNPM_HOME}:${PATH}"
|
|||||||
|
|
||||||
# Export gateway environment variables from add-on config
|
# Export gateway environment variables from add-on config
|
||||||
# These are user-defined variables that should be available to the gateway process
|
# These are user-defined variables that should be available to the gateway process
|
||||||
if [ -n "$GW_ENV_VARS" ]; then
|
if [ "$GW_ENV_VARS" != "{}" ] && [ -n "$GW_ENV_VARS" ]; then
|
||||||
echo "INFO: Setting gateway environment variables from add-on config..."
|
echo "INFO: Setting gateway environment variables from add-on config..."
|
||||||
env_count=0
|
env_count=0
|
||||||
max_env_vars=50
|
max_env_vars=50
|
||||||
max_var_name_size=255
|
max_var_name_size=255
|
||||||
|
|
||||||
|
# Parse JSON object and export each key=value pair
|
||||||
while IFS='=' read -r key value; do
|
while IFS='=' read -r key value; do
|
||||||
# Skip empty lines
|
# Skip empty lines and trim whitespace
|
||||||
|
key=$(echo "$key" | xargs)
|
||||||
|
value=$(echo "$value" | xargs)
|
||||||
|
|
||||||
if [ -z "$key" ]; then
|
if [ -z "$key" ]; then
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Trim whitespace
|
|
||||||
key=$(echo "$key" | xargs)
|
|
||||||
value=$(echo "$value" | xargs)
|
|
||||||
|
|
||||||
# Validate variable name format
|
# Validate variable name format
|
||||||
if ! [[ "$key" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; then
|
if ! [[ "$key" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; then
|
||||||
echo "WARN: Invalid environment variable name: '$key' (must start with letter/underscore, skip)"
|
echo "WARN: Invalid environment variable name: '$key' (must start with letter/underscore, skip)"
|
||||||
@@ -187,7 +187,7 @@ if [ -n "$GW_ENV_VARS" ]; then
|
|||||||
export "$key=$value"
|
export "$key=$value"
|
||||||
((env_count++))
|
((env_count++))
|
||||||
echo "INFO: Exported gateway env var: $key"
|
echo "INFO: Exported gateway env var: $key"
|
||||||
done < <(echo "$GW_ENV_VARS" | tr ';' '\n')
|
done < <(echo "$GW_ENV_VARS" | jq -r 'to_entries[] | "\(.key)=\(.value)"')
|
||||||
|
|
||||||
if [ $env_count -gt 0 ]; then
|
if [ $env_count -gt 0 ]; then
|
||||||
echo "INFO: Successfully exported $env_count gateway environment variable(s)"
|
echo "INFO: Successfully exported $env_count gateway environment variable(s)"
|
||||||
|
|||||||
Reference in New Issue
Block a user