Skip to content

Commit 3fe9bf3

Browse files
aaajiaoclaude
andcommitted
feat: upgrade secret extraction to SecretRef format + auth-profiles.json
Channel tokens and skill apiKeys now use SecretRef objects ({"source":"env","provider":"default","id":"VAR"}) instead of ${VAR} string interpolation, matching what `openclaw secrets audit` expects. Also scans auth-profiles.json after onboard and replaces plaintext key/token fields with SecretRef objects. Gateway auth token and sandbox env vars keep ${VAR} format (Gateway does its own interpolation). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 77324d7 commit 3fe9bf3

1 file changed

Lines changed: 64 additions & 26 deletions

File tree

openclaw-orbstack-setup.sh

Lines changed: 64 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -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
320321
SENSITIVE_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
342343
def 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
362363
env_vars = {} # VAR_NAME -> value
363364
val_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+
387408
if not env_vars:
388409
print("no secrets found, writing minimal .env")
389410
@@ -409,24 +430,41 @@ with open(env_path, "w") as f:
409430
410431
os.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
418442
if 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
426450
with 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")
430468
PYEOF'
431469

432470
ok "$MSG_OK_ENV_EXTRACTED"

0 commit comments

Comments
 (0)