@@ -317,26 +317,27 @@ with open(config_path, "r") as f:
317317 config = json.loads(strip_json5(f.read()))
318318
319319# --- Path-to-env-var mapping for known sensitive fields ---
320+ # 3rd element: "secretref" -> SecretRef object, "envvar" -> ${VAR} string
320321SENSITIVE_PATHS = [
321- ("channels.telegram.botToken", "TG_BOT_TOKEN"),
322- ("channels.discord.token", "DISCORD_TOKEN"),
323- ("channels.slack.botToken", "SLACK_BOT_TOKEN"),
324- ("channels.slack.appToken", "SLACK_APP_TOKEN"),
325- ("channels.whatsapp.authToken", "WHATSAPP_AUTH_TOKEN"),
326- ("channels.googlechat.serviceAccountKey", "GOOGLECHAT_SA_KEY"),
327- ("gateway.auth.token", "GATEWAY_AUTH_TOKEN"),
328- ("agents.defaults.sandbox.docker.env.OPENAI_API_KEY", "OPENAI_API_KEY"),
329- ("agents.defaults.sandbox.docker.env.GOOGLE_API_KEY", "GOOGLE_API_KEY"),
330- ("agents.defaults.sandbox.docker.env.ANTHROPIC_API_KEY","ANTHROPIC_API_KEY"),
331- ("agents.defaults.sandbox.docker.env.OPENROUTER_API_KEY","OPENROUTER_API_KEY"),
332- ("agents.defaults.sandbox.docker.env.GROQ_API_KEY", "GROQ_API_KEY"),
333- ("agents.defaults.sandbox.docker.env.XAI_API_KEY", "XAI_API_KEY"),
334- ("agents.defaults.sandbox.docker.env.MISTRAL_API_KEY", "MISTRAL_API_KEY"),
335- ("agents.defaults.sandbox.docker.env.CEREBRAS_API_KEY","CEREBRAS_API_KEY"),
336- ("agents.defaults.sandbox.docker.env.DEEPSEEK_API_KEY","DEEPSEEK_API_KEY"),
337- ("agents.defaults.sandbox.docker.env.OPENCODE_API_KEY","OPENCODE_API_KEY"),
338- ("agents.defaults.sandbox.docker.env.ZAI_API_KEY", "ZAI_API_KEY"),
339- ("agents.defaults.sandbox.docker.env.TG_BOT_TOKEN", "TG_BOT_TOKEN"),
322+ ("channels.telegram.botToken", "TG_BOT_TOKEN", "secretref" ),
323+ ("channels.discord.token", "DISCORD_TOKEN", "secretref" ),
324+ ("channels.slack.botToken", "SLACK_BOT_TOKEN", "secretref" ),
325+ ("channels.slack.appToken", "SLACK_APP_TOKEN", "secretref" ),
326+ ("channels.whatsapp.authToken", "WHATSAPP_AUTH_TOKEN","secretref" ),
327+ ("channels.googlechat.serviceAccountKey", "GOOGLECHAT_SA_KEY", "secretref" ),
328+ ("gateway.auth.token", "GATEWAY_AUTH_TOKEN","envvar" ),
329+ ("agents.defaults.sandbox.docker.env.OPENAI_API_KEY", "OPENAI_API_KEY", "envvar" ),
330+ ("agents.defaults.sandbox.docker.env.GOOGLE_API_KEY", "GOOGLE_API_KEY", "envvar" ),
331+ ("agents.defaults.sandbox.docker.env.ANTHROPIC_API_KEY","ANTHROPIC_API_KEY","envvar" ),
332+ ("agents.defaults.sandbox.docker.env.OPENROUTER_API_KEY","OPENROUTER_API_KEY","envvar" ),
333+ ("agents.defaults.sandbox.docker.env.GROQ_API_KEY", "GROQ_API_KEY", "envvar" ),
334+ ("agents.defaults.sandbox.docker.env.XAI_API_KEY", "XAI_API_KEY", "envvar" ),
335+ ("agents.defaults.sandbox.docker.env.MISTRAL_API_KEY", "MISTRAL_API_KEY", "envvar" ),
336+ ("agents.defaults.sandbox.docker.env.CEREBRAS_API_KEY","CEREBRAS_API_KEY", "envvar" ),
337+ ("agents.defaults.sandbox.docker.env.DEEPSEEK_API_KEY","DEEPSEEK_API_KEY", "envvar" ),
338+ ("agents.defaults.sandbox.docker.env.OPENCODE_API_KEY","OPENCODE_API_KEY", "envvar" ),
339+ ("agents.defaults.sandbox.docker.env.ZAI_API_KEY", "ZAI_API_KEY", "envvar" ),
340+ ("agents.defaults.sandbox.docker.env.TG_BOT_TOKEN", "TG_BOT_TOKEN", "envvar" ),
340341]
341342
342343def get_nested(obj, path):
@@ -356,13 +357,13 @@ def set_nested(obj, path, value):
356357 if keys[-1] in obj:
357358 obj[keys[-1]] = value
358359
359- is_ref = lambda v: isinstance(v, str) and re.match(r"^\$\{.+\}$", v)
360+ is_ref = lambda v: ( isinstance(v, str) and re.match(r"^\$\{.+\}$", v)) or (isinstance(v, dict) and "source" in v)
360361
361362# Phase 1: Collect secrets from known paths
362363env_vars = {} # VAR_NAME -> value
363364val_to_var = {} # value -> VAR_NAME (for dedup)
364365
365- for path, var_name in SENSITIVE_PATHS:
366+ for path, var_name, _ref_type in SENSITIVE_PATHS:
366367 val = get_nested(config, path)
367368 if val and isinstance(val, str) and not is_ref(val):
368369 if var_name not in env_vars:
@@ -384,6 +385,26 @@ if isinstance(skills, dict):
384385 env_vars[var_name] = api_key
385386 val_to_var[api_key] = var_name
386387
388+ # Phase 2b: Scan auth-profiles.json for plaintext keys/tokens
389+ ap_path = os.path.expanduser("~/.openclaw/agents/main/agent/auth-profiles.json")
390+ ap = None
391+ if os.path.exists(ap_path):
392+ with open(ap_path, "r") as f:
393+ ap = json.loads(strip_json5(f.read()))
394+ if isinstance(ap, dict):
395+ for provider, profile in ap.items():
396+ if not isinstance(profile, dict):
397+ continue
398+ for field, suffix in [("key", "_API_KEY"), ("token", "_TOKEN")]:
399+ val = profile.get(field)
400+ if val and isinstance(val, str) and not is_ref(val):
401+ if val in val_to_var:
402+ pass # reuse existing var, replacement handled in Phase 5
403+ else:
404+ var_name = provider.upper().replace("-", "_") + suffix
405+ env_vars[var_name] = val
406+ val_to_var[val] = var_name
407+
387408if not env_vars:
388409 print("no secrets found, writing minimal .env")
389410
@@ -409,24 +430,41 @@ with open(env_path, "w") as f:
409430
410431os.chmod(env_path, 0o600)
411432
412- # Phase 4: Replace inline secrets with ${VAR} references in config
413- for path, var_name in SENSITIVE_PATHS:
433+ # Phase 4: Replace inline secrets with refs in config
434+ for path, var_name, ref_type in SENSITIVE_PATHS:
414435 val = get_nested(config, path)
415436 if val and isinstance(val, str) and not is_ref(val) and var_name in env_vars:
416- set_nested(config, path, "${" + var_name + "}")
437+ if ref_type == "secretref":
438+ set_nested(config, path, {"source": "env", "provider": "default", "id": var_name})
439+ else:
440+ set_nested(config, path, "${" + var_name + "}")
417441
418442if isinstance(skills, dict):
419443 for skill_name, skill_cfg in skills.items():
420444 if isinstance(skill_cfg, dict) and "apiKey" in skill_cfg:
421445 api_key = skill_cfg["apiKey"]
422446 if api_key and isinstance(api_key, str) and not is_ref(api_key):
423447 if api_key in val_to_var:
424- skill_cfg["apiKey"] = "${" + val_to_var[api_key] + "}"
448+ skill_cfg["apiKey"] = {"source": "env", "provider": "default", "id": val_to_var[api_key]}
425449
426450with open(config_path, "w") as f:
427451 json.dump(config, f, indent=2)
428452
429- print(f"extracted {len(env_vars)} secret(s) to .env")
453+ # Phase 5: Replace plaintext secrets in auth-profiles.json
454+ ap_count = 0
455+ if ap and isinstance(ap, dict):
456+ for provider, profile in ap.items():
457+ if not isinstance(profile, dict):
458+ continue
459+ for field in ("key", "token"):
460+ val = profile.get(field)
461+ if val and isinstance(val, str) and not is_ref(val) and val in val_to_var:
462+ profile[field] = {"source": "env", "provider": "default", "id": val_to_var[val]}
463+ ap_count += 1
464+ with open(ap_path, "w") as f:
465+ json.dump(ap, f, indent=2)
466+
467+ print(f"extracted {len(env_vars)} secret(s) to .env, {ap_count} auth-profile ref(s) updated")
430468PYEOF'
431469
432470ok " $MSG_OK_ENV_EXTRACTED "
0 commit comments