Spaces:
Running
Running
Commit Β·
5d11752
1
Parent(s): 33980b3
Prune stale dataset files after sync upload
Browse files- .env.example +11 -1
- CHANGELOG.md +38 -0
- ENV_VARIABLES_FULL_LIST.md +254 -0
- README.md +2 -2
- multi-provider-key-rotator.cjs +13 -3
- openclaw-sync.py +65 -16
- start.sh +110 -2
.env.example
CHANGED
|
@@ -132,7 +132,10 @@ LLM_MODEL=anthropic/claude-sonnet-4-5
|
|
| 132 |
# have multiple accounts or want to spread rate-limit quota.
|
| 133 |
#
|
| 134 |
# Pattern: <PROVIDER>_API_KEYS=key1,key2,key3
|
| 135 |
-
# Fallback order: plural pool β singular key β LLM_API_KEY
|
|
|
|
|
|
|
|
|
|
| 136 |
#
|
| 137 |
# Uncomment and fill in only the providers you use:
|
| 138 |
#
|
|
@@ -172,6 +175,13 @@ LLM_MODEL=anthropic/claude-sonnet-4-5
|
|
| 172 |
# is also picked up by OpenClaw β that provider's models become
|
| 173 |
# available in the Control UI for manual selection.
|
| 174 |
# 3. Rotation pools (*_API_KEYS) work for every active provider
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
# independently and in parallel.
|
| 176 |
#
|
| 177 |
# EXAMPLE β default Anthropic, also use OpenAI and Groq:
|
|
|
|
| 132 |
# have multiple accounts or want to spread rate-limit quota.
|
| 133 |
#
|
| 134 |
# Pattern: <PROVIDER>_API_KEYS=key1,key2,key3
|
| 135 |
+
# Fallback order: plural pool β singular key β LLM_API_KEY (optional)
|
| 136 |
+
# Set false only if you want to disable global LLM_API_KEY fallback
|
| 137 |
+
# across providers.
|
| 138 |
+
LLM_API_KEY_FALLBACK_ENABLED=true
|
| 139 |
#
|
| 140 |
# Uncomment and fill in only the providers you use:
|
| 141 |
#
|
|
|
|
| 175 |
# is also picked up by OpenClaw β that provider's models become
|
| 176 |
# available in the Control UI for manual selection.
|
| 177 |
# 3. Rotation pools (*_API_KEYS) work for every active provider
|
| 178 |
+
#
|
| 179 |
+
# Optional: explicitly pin model lists per provider for Control UI visibility
|
| 180 |
+
# when provider keys are configured.
|
| 181 |
+
# Format: comma-separated model IDs
|
| 182 |
+
# NVIDIA_MODELS=meta/llama-3.1-70b-instruct,nvidia/llama-3.1-nemotron-70b-instruct
|
| 183 |
+
# OPENAI_MODELS=gpt-4o-mini,gpt-4.1
|
| 184 |
+
# GROQ_MODELS=llama-3.3-70b-versatile,deepseek-r1-distill-llama-70b
|
| 185 |
# independently and in parallel.
|
| 186 |
#
|
| 187 |
# EXAMPLE β default Anthropic, also use OpenAI and Groq:
|
CHANGELOG.md
CHANGED
|
@@ -2,6 +2,44 @@
|
|
| 2 |
|
| 3 |
All notable changes to this project will be documented in this file.
|
| 4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
## [1.4.0] - 2026-04-25
|
| 6 |
|
| 7 |
### Added
|
|
|
|
| 2 |
|
| 3 |
All notable changes to this project will be documented in this file.
|
| 4 |
|
| 5 |
+
## [1.5.0] - 2026-05-13
|
| 6 |
+
|
| 7 |
+
### Added
|
| 8 |
+
|
| 9 |
+
- **NVIDIA key-rotation support** β added `nvidia-key-rotator.cjs` wiring and startup integration so deployments can rotate NVIDIA credentials similarly to other provider key-rotation flows.
|
| 10 |
+
- **Cloudflare keep-alive automation** β added/expanded `cloudflare-keepalive-setup.py` flow and startup wiring to provision keep-alive through Cloudflare Worker automation instead of the older UptimeRobot-first approach.
|
| 11 |
+
- **Sync metadata marker model** β introduced a structured workspace marker `(file_count, total_size, newest_mtime, metadata_hash)` to support stronger change introspection in sync code.
|
| 12 |
+
|
| 13 |
+
### Changed
|
| 14 |
+
|
| 15 |
+
- **Workspace sync script rename finalized** β `workspace-sync.py` flow was migrated to `openclaw-sync.py` in Docker/startup/docs so restore/sync behavior is centralized under one script.
|
| 16 |
+
- **Sync trigger behavior hardened for config churn** β OpenClaw config sync now debounces until JSON settles before immediate sync, reducing false/partial syncs during rapid config writes.
|
| 17 |
+
- **Gateway restart flow now saves state first** β restart path was updated to run a pre-restart one-shot state sync so gateway reloads are less likely to drop recent state.
|
| 18 |
+
- **Shutdown backup now uses a two-step pass** β graceful shutdown now attempts `sync-once-settled` then a final `sync-once` pass to better capture last-second writes.
|
| 19 |
+
- **Telegram allowlist simplified** β consolidated Telegram allowlist into `TELEGRAM_ALLOWED_USERS` and aligned docs/examples.
|
| 20 |
+
- **Plugin startup behavior aligned** β startup-installed plugins are synced into `plugins.allow` before gateway launch so runtime-installed plugins are recognized cleanly.
|
| 21 |
+
- **Cloudflare proxy path matured** β multiple iterations improved fetch/proxy behavior (header handling, endpoint scoping, API root routing, URL parsing, and logging noise reduction), then simplified unstable undici patching paths.
|
| 22 |
+
- **Health dashboard polish** β sync timestamps now show local time, footer credits were corrected, and status rendering/docs were updated for the Cloudflare keep-alive model.
|
| 23 |
+
- **CI workflow churn documented** β GitHub workflow files for HF sync were added/renamed/cleaned multiple times as space/repo naming stabilized.
|
| 24 |
+
|
| 25 |
+
### Fixed
|
| 26 |
+
|
| 27 |
+
- **Missed rapid backup updates** β sync logic now relies on content fingerprint checks for no-op decisions so same-second or quick successive changes are less likely to be skipped.
|
| 28 |
+
- **Non-deterministic metadata hashing** β metadata hashing now iterates paths deterministically to avoid hash jitter from traversal order.
|
| 29 |
+
- **Transient file race sync failures** β sync fingerprinting/snapshot copy paths now tolerate transient `OSError` (file rotated/deleted mid-scan) instead of aborting the whole sync pass.
|
| 30 |
+
- **State restore migration edge cases** β restore flow includes migration/cleanup behavior for legacy hidden state paths and stale backup entries.
|
| 31 |
+
- **Startup/env robustness** β fixed shell export formatting/syntax issues (e.g., NVIDIA/XAI lines) and unbound-variable pitfalls in startup scripts.
|
| 32 |
+
- **Proxy runtime errors and noise** β fixed specific proxy runtime issues (including `UND_ERR_INVALID_ARG`, fetch duplex handling, and upstream error visibility) and reduced noisy stdout logs that interfered with clean process output.
|
| 33 |
+
- **HF workflow/repo reference mismatches** β corrected and later cleaned workflow repository references during repo migration/restructure.
|
| 34 |
+
|
| 35 |
+
### Docs
|
| 36 |
+
|
| 37 |
+
- README/.env/security docs were refreshed across multiple commits to reflect:
|
| 38 |
+
- Cloudflare keep-alive replacing UptimeRobot setup path,
|
| 39 |
+
- updated secrets and startup environment behavior,
|
| 40 |
+
- provider/key-rotation options,
|
| 41 |
+
- backup/sync behavior and troubleshooting guidance.
|
| 42 |
+
|
| 43 |
## [1.4.0] - 2026-04-25
|
| 44 |
|
| 45 |
### Added
|
ENV_VARIABLES_FULL_LIST.md
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ENV Variables Full List (Repo Scope)
|
| 2 |
+
|
| 3 |
+
> Internal reference file. Not linked from README/docs navigation.
|
| 4 |
+
|
| 5 |
+
## 1) User-settable (Space Secrets / `.env`)
|
| 6 |
+
|
| 7 |
+
These are the main variables you can set directly.
|
| 8 |
+
|
| 9 |
+
### Core startup/auth
|
| 10 |
+
- `LLM_API_KEY` β string API key. Example: `LLM_API_KEY=sk-ant-xxxx`
|
| 11 |
+
- `LLM_MODEL` β `provider/model-id` string. Examples:
|
| 12 |
+
- `LLM_MODEL=anthropic/claude-sonnet-4-5`
|
| 13 |
+
- `LLM_MODEL=nvidia/meta/llama-3.1-70b-instruct`
|
| 14 |
+
- `GATEWAY_TOKEN` β random secret string. Example: `GATEWAY_TOKEN=9d2f...`
|
| 15 |
+
- `OPENCLAW_PASSWORD` β plaintext password string. Example: `OPENCLAW_PASSWORD=MyStrongPass123`
|
| 16 |
+
|
| 17 |
+
### Provider fallback/selection
|
| 18 |
+
- `LLM_API_KEY_FALLBACK_ENABLED` β boolean-like:
|
| 19 |
+
- enabled: `true`, `1`, `yes`, `on`
|
| 20 |
+
- disabled: `false`, `0`, `no`, `off`
|
| 21 |
+
- default/example: `LLM_API_KEY_FALLBACK_ENABLED=true`
|
| 22 |
+
|
| 23 |
+
### Provider-specific keys (single-key form)
|
| 24 |
+
Use plain provider key strings. Examples:
|
| 25 |
+
- `NVIDIA_API_KEY=nvapi-xxxx`
|
| 26 |
+
- `OPENAI_API_KEY=sk-xxxx`
|
| 27 |
+
- `GEMINI_API_KEY=AIzaSyxxxx`
|
| 28 |
+
|
| 29 |
+
Full list:
|
| 30 |
+
- `ANTHROPIC_API_KEY`
|
| 31 |
+
- `OPENAI_API_KEY`
|
| 32 |
+
- `GEMINI_API_KEY`
|
| 33 |
+
- `DEEPSEEK_API_KEY`
|
| 34 |
+
- `OPENROUTER_API_KEY`
|
| 35 |
+
- `KILOCODE_API_KEY`
|
| 36 |
+
- `OPENCODE_API_KEY`
|
| 37 |
+
- `ZAI_API_KEY`
|
| 38 |
+
- `MOONSHOT_API_KEY`
|
| 39 |
+
- `KIMI_API_KEY`
|
| 40 |
+
- `MINIMAX_API_KEY`
|
| 41 |
+
- `MODELSTUDIO_API_KEY`
|
| 42 |
+
- `XIAOMI_API_KEY`
|
| 43 |
+
- `VOLCANO_ENGINE_API_KEY`
|
| 44 |
+
- `BYTEPLUS_API_KEY`
|
| 45 |
+
- `QIANFAN_API_KEY`
|
| 46 |
+
- `MISTRAL_API_KEY`
|
| 47 |
+
- `XAI_API_KEY`
|
| 48 |
+
- `NVIDIA_API_KEY`
|
| 49 |
+
- `COHERE_API_KEY`
|
| 50 |
+
- `GROQ_API_KEY`
|
| 51 |
+
- `TOGETHER_API_KEY`
|
| 52 |
+
- `CEREBRAS_API_KEY`
|
| 53 |
+
- `VENICE_API_KEY`
|
| 54 |
+
- `SYNTHETIC_API_KEY`
|
| 55 |
+
- `COPILOT_GITHUB_TOKEN`
|
| 56 |
+
- `HUGGINGFACE_HUB_TOKEN`
|
| 57 |
+
- `AI_GATEWAY_API_KEY`
|
| 58 |
+
|
| 59 |
+
### Provider key pools (comma-separated rotation)
|
| 60 |
+
Format: comma-separated keys, no spaces required. Examples:
|
| 61 |
+
- `NVIDIA_API_KEYS=nvapi-key1,nvapi-key2`
|
| 62 |
+
- `OPENAI_API_KEYS=sk-a,sk-b,sk-c`
|
| 63 |
+
|
| 64 |
+
Full list:
|
| 65 |
+
- `ANTHROPIC_API_KEYS`
|
| 66 |
+
- `OPENAI_API_KEYS`
|
| 67 |
+
- `GEMINI_API_KEYS`
|
| 68 |
+
- `DEEPSEEK_API_KEYS`
|
| 69 |
+
- `OPENROUTER_API_KEYS`
|
| 70 |
+
- `KILOCODE_API_KEYS`
|
| 71 |
+
- `OPENCODE_API_KEYS`
|
| 72 |
+
- `ZAI_API_KEYS`
|
| 73 |
+
- `MOONSHOT_API_KEYS`
|
| 74 |
+
- `MINIMAX_API_KEYS`
|
| 75 |
+
- `XIAOMI_API_KEYS`
|
| 76 |
+
- `VOLCANO_ENGINE_API_KEYS`
|
| 77 |
+
- `BYTEPLUS_API_KEYS`
|
| 78 |
+
- `QIANFAN_API_KEYS`
|
| 79 |
+
- `MODELSTUDIO_API_KEYS`
|
| 80 |
+
- `KIMI_API_KEYS`
|
| 81 |
+
- `MISTRAL_API_KEYS`
|
| 82 |
+
- `XAI_API_KEYS`
|
| 83 |
+
- `NVIDIA_API_KEYS`
|
| 84 |
+
- `GROQ_API_KEYS`
|
| 85 |
+
- `COHERE_API_KEYS`
|
| 86 |
+
- `TOGETHER_API_KEYS`
|
| 87 |
+
- `CEREBRAS_API_KEYS`
|
| 88 |
+
- `HUGGINGFACE_HUB_TOKENS`
|
| 89 |
+
|
| 90 |
+
### Provider model lists (optional, for Control UI visibility)
|
| 91 |
+
Format: comma-separated model IDs; used only when that provider has key(s) configured.
|
| 92 |
+
Examples:
|
| 93 |
+
- `NVIDIA_MODELS=meta/llama-3.1-70b-instruct,nvidia/llama-3.1-nemotron-70b-instruct`
|
| 94 |
+
- `OPENAI_MODELS=gpt-4o-mini,gpt-4.1`
|
| 95 |
+
- `GROQ_MODELS=llama-3.3-70b-versatile,deepseek-r1-distill-llama-70b`
|
| 96 |
+
|
| 97 |
+
Supported:
|
| 98 |
+
- `ANTHROPIC_MODELS`
|
| 99 |
+
- `OPENAI_MODELS`
|
| 100 |
+
- `GEMINI_MODELS`
|
| 101 |
+
- `DEEPSEEK_MODELS`
|
| 102 |
+
- `OPENROUTER_MODELS`
|
| 103 |
+
- `KILOCODE_MODELS`
|
| 104 |
+
- `OPENCODE_MODELS`
|
| 105 |
+
- `ZAI_MODELS`
|
| 106 |
+
- `MOONSHOT_MODELS`
|
| 107 |
+
- `KIMI_MODELS`
|
| 108 |
+
- `MINIMAX_MODELS`
|
| 109 |
+
- `MODELSTUDIO_MODELS`
|
| 110 |
+
- `XIAOMI_MODELS`
|
| 111 |
+
- `VOLCANO_ENGINE_MODELS`
|
| 112 |
+
- `BYTEPLUS_MODELS`
|
| 113 |
+
- `QIANFAN_MODELS`
|
| 114 |
+
- `GROQ_MODELS`
|
| 115 |
+
- `MISTRAL_MODELS`
|
| 116 |
+
- `XAI_MODELS`
|
| 117 |
+
- `NVIDIA_MODELS`
|
| 118 |
+
- `COHERE_MODELS`
|
| 119 |
+
- `TOGETHER_MODELS`
|
| 120 |
+
- `CEREBRAS_MODELS`
|
| 121 |
+
|
| 122 |
+
### Telegram / WhatsApp
|
| 123 |
+
- `TELEGRAM_BOT_TOKEN` β bot token string. Example: `TELEGRAM_BOT_TOKEN=123456:ABC...`
|
| 124 |
+
- `TELEGRAM_ALLOWED_USERS` β comma-separated numeric IDs. Example: `TELEGRAM_ALLOWED_USERS=12345,67890`
|
| 125 |
+
- `TELEGRAM_USER_ID` β legacy single numeric ID. Example: `TELEGRAM_USER_ID=12345`
|
| 126 |
+
- `TELEGRAM_USER_IDS` β legacy comma-separated IDs. Example: `TELEGRAM_USER_IDS=12345,67890`
|
| 127 |
+
- `WHATSAPP_ENABLED` β boolean-like (`true/false`, `1/0`, `yes/no`, `on/off`). Example: `WHATSAPP_ENABLED=true`
|
| 128 |
+
|
| 129 |
+
### Backup/sync
|
| 130 |
+
- `HF_TOKEN` β HF token string. Example: `HF_TOKEN=hf_xxxxx`
|
| 131 |
+
- `HF_USERNAME` β HF username string. Example: `HF_USERNAME=myhandle`
|
| 132 |
+
- `BACKUP_DATASET_NAME` β dataset slug string. Example: `BACKUP_DATASET_NAME=huggingclaw-backup`
|
| 133 |
+
- `SYNC_INTERVAL` β integer seconds. Example: `SYNC_INTERVAL=180`
|
| 134 |
+
- `SYNC_START_DELAY` β integer seconds. Example: `SYNC_START_DELAY=10`
|
| 135 |
+
- `SYNC_MAX_FILE_BYTES` β integer bytes. Example: `SYNC_MAX_FILE_BYTES=52428800`
|
| 136 |
+
- `OPENCLAW_CONFIG_WATCH_INTERVAL` β number (supports decimal seconds). Example: `OPENCLAW_CONFIG_WATCH_INTERVAL=1`
|
| 137 |
+
- `OPENCLAW_CONFIG_SETTLE_SECONDS` β number (supports decimal seconds). Example: `OPENCLAW_CONFIG_SETTLE_SECONDS=3`
|
| 138 |
+
|
| 139 |
+
### Gateway/runtime tuning
|
| 140 |
+
- `GATEWAY_HOST` β hostname/IP string. Example: `GATEWAY_HOST=127.0.0.1`
|
| 141 |
+
- `GATEWAY_PORT` β integer port. Example: `GATEWAY_PORT=3456`
|
| 142 |
+
- `GATEWAY_VERBOSE` β boolean-like. Example: `GATEWAY_VERBOSE=true`
|
| 143 |
+
- `GATEWAY_READY_TIMEOUT` β integer seconds. Example: `GATEWAY_READY_TIMEOUT=60`
|
| 144 |
+
- `GATEWAY_MAX_RESTARTS` β integer count. Example: `GATEWAY_MAX_RESTARTS=10`
|
| 145 |
+
- `GATEWAY_RESTART_DELAY` β integer seconds. Example: `GATEWAY_RESTART_DELAY=2`
|
| 146 |
+
- `TRUSTED_PROXIES` β comma-separated CIDR/IP list. Example: `TRUSTED_PROXIES=127.0.0.1,10.0.0.0/8`
|
| 147 |
+
- `ALLOWED_ORIGINS` β comma-separated origins. Example: `ALLOWED_ORIGINS=https://a.com,https://b.com`
|
| 148 |
+
- `PORT` β integer external port. Example: `PORT=7860`
|
| 149 |
+
|
| 150 |
+
### Browser/plugin controls
|
| 151 |
+
- `BROWSER_PLUGIN_MODE` β mode string (`auto`, `builtin`, `external`, etc. depending on runtime). Example: `BROWSER_PLUGIN_MODE=auto`
|
| 152 |
+
- `BROWSER_EXECUTABLE_PATH` β absolute path. Example: `BROWSER_EXECUTABLE_PATH=/usr/bin/chromium`
|
| 153 |
+
- `BROWSER_DISABLED` β boolean-like. Example: `BROWSER_DISABLED=false`
|
| 154 |
+
- `ACP_PLUGIN_MODE` β mode string. Example: `ACP_PLUGIN_MODE=auto`
|
| 155 |
+
- `ACPX_DISABLED` β boolean-like. Example: `ACPX_DISABLED=true`
|
| 156 |
+
|
| 157 |
+
### OpenClaw logging/behavior
|
| 158 |
+
- `OPENCLAW_VERSION` β image/app version string. Example: `OPENCLAW_VERSION=latest`
|
| 159 |
+
- `OPENCLAW_RUNTIME_VERSION` β runtime tag string. Example: `OPENCLAW_RUNTIME_VERSION=v1.2.3`
|
| 160 |
+
- `OPENCLAW_DISPLAY_VERSION` β display label string. Example: `OPENCLAW_DISPLAY_VERSION=Custom`
|
| 161 |
+
- `OPENCLAW_DISABLE_BONJOUR` β boolean-like. Example: `OPENCLAW_DISABLE_BONJOUR=true`
|
| 162 |
+
- `OPENCLAW_CONSOLE_LOG_LEVEL` β log level (`trace|debug|info|warn|error`). Example: `OPENCLAW_CONSOLE_LOG_LEVEL=warn`
|
| 163 |
+
- `OPENCLAW_CONSOLE_LOG_STYLE` β style string. Example: `OPENCLAW_CONSOLE_LOG_STYLE=pretty`
|
| 164 |
+
- `OPENCLAW_FILE_LOG_LEVEL` β log level (`trace|debug|info|warn|error`). Example: `OPENCLAW_FILE_LOG_LEVEL=info`
|
| 165 |
+
|
| 166 |
+
### Custom OpenAI-compatible provider
|
| 167 |
+
- `CUSTOM_PROVIDER_NAME` β unique provider prefix. Example: `CUSTOM_PROVIDER_NAME=modal`
|
| 168 |
+
- `CUSTOM_BASE_URL` β base URL only (not `/chat/completions`). Example: `CUSTOM_BASE_URL=https://api.modal.com/v1`
|
| 169 |
+
- `CUSTOM_MODEL_ID` β model id string. Example: `CUSTOM_MODEL_ID=zai-org/GLM-5.1-FP8`
|
| 170 |
+
- `CUSTOM_MODEL_NAME` β display name. Example: `CUSTOM_MODEL_NAME=GLM 5.1 FP8`
|
| 171 |
+
- `CUSTOM_API_KEY` β provider key string. Example: `CUSTOM_API_KEY=sk-xxxx`
|
| 172 |
+
- `CUSTOM_API_TYPE` β API mode string. Example: `CUSTOM_API_TYPE=openai-completions`
|
| 173 |
+
- `CUSTOM_CONTEXT_WINDOW` β integer token window. Example: `CUSTOM_CONTEXT_WINDOW=128000`
|
| 174 |
+
- `CUSTOM_MAX_TOKENS` β integer max tokens. Example: `CUSTOM_MAX_TOKENS=500`
|
| 175 |
+
|
| 176 |
+
### Cloudflare proxy/keepalive
|
| 177 |
+
- `CLOUDFLARE_WORKERS_TOKEN` β API token string. Example: `CLOUDFLARE_WORKERS_TOKEN=cf_xxx`
|
| 178 |
+
- `CLOUDFLARE_WORKER_NAME` β worker name string. Example: `CLOUDFLARE_WORKER_NAME=huggingclaw-proxy`
|
| 179 |
+
- `CLOUDFLARE_ACCOUNT_ID` β account id string. Example: `CLOUDFLARE_ACCOUNT_ID=abc123...`
|
| 180 |
+
- `CLOUDFLARE_PROXY_URL` β full proxy URL. Example: `CLOUDFLARE_PROXY_URL=https://my-worker.workers.dev`
|
| 181 |
+
- `CLOUDFLARE_PROXY_SECRET` β shared secret string. Example: `CLOUDFLARE_PROXY_SECRET=long-random-secret`
|
| 182 |
+
- `CLOUDFLARE_PROXY_DOMAINS` β comma-separated domains. Example: `CLOUDFLARE_PROXY_DOMAINS=api.openai.com,api.anthropic.com`
|
| 183 |
+
- `CLOUDFLARE_PROXY_DEBUG` β boolean-like. Example: `CLOUDFLARE_PROXY_DEBUG=false`
|
| 184 |
+
- `CLOUDFLARE_KEEPALIVE_ENABLED` β boolean-like. Example: `CLOUDFLARE_KEEPALIVE_ENABLED=true`
|
| 185 |
+
- `CLOUDFLARE_KEEPALIVE_URL` β URL string. Example: `CLOUDFLARE_KEEPALIVE_URL=https://space-url.hf.space`
|
| 186 |
+
- `CLOUDFLARE_KEEPALIVE_CRON` β cron string. Example: `CLOUDFLARE_KEEPALIVE_CRON=*/5 * * * *`
|
| 187 |
+
- `CLOUDFLARE_KEEPALIVE_WORKER_NAME` β worker name string. Example: `CLOUDFLARE_KEEPALIVE_WORKER_NAME=huggingclaw-keepalive`
|
| 188 |
+
|
| 189 |
+
### Startup command replay/install helpers
|
| 190 |
+
- `HUGGINGCLAW_RUN` β shell commands string. Example: `HUGGINGCLAW_RUN=apt-get update && apt-get install -y ffmpeg`
|
| 191 |
+
- `HUGGINGCLAW_APT_PACKAGES` β comma/space separated package names. Example: `HUGGINGCLAW_APT_PACKAGES=ffmpeg,git`
|
| 192 |
+
- `HUGGINGCLAW_PIP_PACKAGES` β pip package list string. Example: `HUGGINGCLAW_PIP_PACKAGES=yt-dlp,requests`
|
| 193 |
+
- `HUGGINGCLAW_NPM_PACKAGES` β npm package list string. Example: `HUGGINGCLAW_NPM_PACKAGES=sharp,axios`
|
| 194 |
+
- `HUGGINGCLAW_OPENCLAW_PLUGINS` β plugin ids list string. Example: `HUGGINGCLAW_OPENCLAW_PLUGINS=@openclaw/telegram,@openclaw/whatsapp`
|
| 195 |
+
- `HUGGINGCLAW_STARTUP_SCRIPT` β shell script string. Example: `HUGGINGCLAW_STARTUP_SCRIPT=echo hello`
|
| 196 |
+
- `HUGGINGCLAW_STARTUP_SCRIPT_B64` β base64 script content. Example: `HUGGINGCLAW_STARTUP_SCRIPT_B64=IyEvYmluL2Jhc2gKZWNobyBoaQ==`
|
| 197 |
+
- `HUGGINGCLAW_STARTUP_COMMANDS` β newline/semicolon commands string.
|
| 198 |
+
- `HUGGINGCLAW_STARTUP_STRICT` β boolean-like. Example: `HUGGINGCLAW_STARTUP_STRICT=true`
|
| 199 |
+
- `WEBHOOK_URL` β webhook endpoint URL. Example: `WEBHOOK_URL=https://hooks.example.com/hc`
|
| 200 |
+
|
| 201 |
+
---
|
| 202 |
+
|
| 203 |
+
## 2) Runtime/Internal envs (normally do **not** set manually)
|
| 204 |
+
|
| 205 |
+
These are mostly derived/temporary/process variables used by scripts:
|
| 206 |
+
|
| 207 |
+
- `APP_BASE`
|
| 208 |
+
- `BACKUP_DATASET`
|
| 209 |
+
- `BROWSER_SHOULD_ENABLE`
|
| 210 |
+
- `CF_PROXY_ENV_FILE`
|
| 211 |
+
- `CLEAN_TG_TOKEN`
|
| 212 |
+
- `CONFIG_JSON`
|
| 213 |
+
- `CUSTOM_BASE_URL_NORMALIZED`
|
| 214 |
+
- `CUSTOM_PROVIDER_NORMALIZED`
|
| 215 |
+
- `CUSTOM_PROVIDER_OK`
|
| 216 |
+
- `DEBIAN_FRONTEND`
|
| 217 |
+
- `ERRORS`
|
| 218 |
+
- `EXISTING_CONFIG`
|
| 219 |
+
- `GATEWAY_EXIT_CODE`
|
| 220 |
+
- `GATEWAY_PID`
|
| 221 |
+
- `GATEWAY_RESTART_COUNT`
|
| 222 |
+
- `GUARDIAN_PID`
|
| 223 |
+
- `HC_STARTUP_FAILURES`
|
| 224 |
+
- `HC_STARTUP_INDEX`
|
| 225 |
+
- `HC_STARTUP_STRICT_NORMALIZED`
|
| 226 |
+
- `HC_STARTUP_VAR`
|
| 227 |
+
- `HOME`
|
| 228 |
+
- `IDS_JSON`
|
| 229 |
+
- `INSTALLS`
|
| 230 |
+
- `LLM_PROVIDER`
|
| 231 |
+
- `NODE_OPTIONS`
|
| 232 |
+
- `NPM_CONFIG_PREFIX`
|
| 233 |
+
- `OPENCLAW_APP_DIR`
|
| 234 |
+
- `OPENCLAW_CONSOLE_LOG_LEVEL_CONFIGURED`
|
| 235 |
+
- `OPENCLAW_CONSOLE_LOG_STYLE_CONFIGURED`
|
| 236 |
+
- `OPENCLAW_FILE_LOG_LEVEL_CONFIGURED`
|
| 237 |
+
- `ORIGINS_JSON`
|
| 238 |
+
- `PATCHED`
|
| 239 |
+
- `PATH`
|
| 240 |
+
- `PLUGIN_ALLOW_JSON`
|
| 241 |
+
- `PROXIES_JSON`
|
| 242 |
+
- `PROXY_URL`
|
| 243 |
+
- `PYTHONUSERBASE`
|
| 244 |
+
- `RESET_MARKER_PATH`
|
| 245 |
+
- `SPACE_AUTHOR_NAME`
|
| 246 |
+
- `SPACE_HOST`
|
| 247 |
+
- `SPACE_REPO_NAME`
|
| 248 |
+
- `STARTUP_FILE`
|
| 249 |
+
- `SYNC_LOOP_PID`
|
| 250 |
+
- `VIRTUAL_ENV`
|
| 251 |
+
- `WEBHOOK_BODY`
|
| 252 |
+
- `WHATSAPP_CONFIG_ENABLED`
|
| 253 |
+
- `WHATSAPP_ENABLED_CONFIGURED`
|
| 254 |
+
- `WHATSAPP_ENABLED_NORMALIZED`
|
README.md
CHANGED
|
@@ -250,10 +250,10 @@ GEMINI_API_KEYS=AIza-key1,AIza-key2
|
|
| 250 |
**Fallback chain** (per provider):
|
| 251 |
1. `{PROVIDER}_API_KEYS` β comma-separated pool *(preferred)*
|
| 252 |
2. `{PROVIDER}_API_KEY` β single dedicated key
|
| 253 |
-
3. `LLM_API_KEY` β universal fallback *(default
|
| 254 |
|
| 255 |
> [!TIP]
|
| 256 |
-
>
|
| 257 |
|
| 258 |
Supported per-provider variables: `ANTHROPIC_API_KEYS`, `OPENAI_API_KEYS`, `GEMINI_API_KEYS`, `DEEPSEEK_API_KEYS`, `GROQ_API_KEYS`, `MISTRAL_API_KEYS`, `OPENROUTER_API_KEYS`, `XAI_API_KEYS`, `NVIDIA_API_KEYS`, `COHERE_API_KEYS`, `TOGETHER_API_KEYS`, `CEREBRAS_API_KEYS`, and more β see `.env.example` for the full list.
|
| 259 |
|
|
|
|
| 250 |
**Fallback chain** (per provider):
|
| 251 |
1. `{PROVIDER}_API_KEYS` β comma-separated pool *(preferred)*
|
| 252 |
2. `{PROVIDER}_API_KEY` β single dedicated key
|
| 253 |
+
3. `LLM_API_KEY` β universal fallback *(enabled by default; disable with `LLM_API_KEY_FALLBACK_ENABLED=false`)*
|
| 254 |
|
| 255 |
> [!TIP]
|
| 256 |
+
> By default, `LLM_API_KEY` fallback is enabled for compatibility. Set `LLM_API_KEY_FALLBACK_ENABLED=false` if you want strict provider-only activation.
|
| 257 |
|
| 258 |
Supported per-provider variables: `ANTHROPIC_API_KEYS`, `OPENAI_API_KEYS`, `GEMINI_API_KEYS`, `DEEPSEEK_API_KEYS`, `GROQ_API_KEYS`, `MISTRAL_API_KEYS`, `OPENROUTER_API_KEYS`, `XAI_API_KEYS`, `NVIDIA_API_KEYS`, `COHERE_API_KEYS`, `TOGETHER_API_KEYS`, `CEREBRAS_API_KEYS`, and more β see `.env.example` for the full list.
|
| 259 |
|
multi-provider-key-rotator.cjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
| 8 |
*
|
| 9 |
* For each provider you can supply a comma-separated pool:
|
| 10 |
* ANTHROPIC_API_KEYS=key1,key2,key3
|
| 11 |
-
* Falls back to the singular env var,
|
| 12 |
*
|
| 13 |
* Keys are rotated round-robin per provider independently.
|
| 14 |
*
|
|
@@ -30,7 +30,9 @@ const log = (...args) => console.error(...args);
|
|
| 30 |
// envPlural β env var that holds a comma-separated key pool (preferred)
|
| 31 |
// envSingular β env var that holds a single key (fallback)
|
| 32 |
//
|
| 33 |
-
// LLM_API_KEY
|
|
|
|
|
|
|
| 34 |
//
|
| 35 |
const PROVIDERS = [
|
| 36 |
{
|
|
@@ -180,6 +182,8 @@ function normalizeKeys(...inputs) {
|
|
| 180 |
|
| 181 |
// Build per-provider key pools + rotation indices
|
| 182 |
const providerState = PROVIDERS.map(p => {
|
|
|
|
|
|
|
| 183 |
const dedicatedKeys = normalizeKeys(
|
| 184 |
process.env[p.envPlural] || '',
|
| 185 |
process.env[p.envSingular] || '',
|
|
@@ -187,7 +191,11 @@ const providerState = PROVIDERS.map(p => {
|
|
| 187 |
const hasDedicated = dedicatedKeys.length > 0;
|
| 188 |
const keys = hasDedicated
|
| 189 |
? dedicatedKeys
|
| 190 |
-
:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
|
| 192 |
if (hasDedicated) {
|
| 193 |
log(`[key-rotator] ${p.name}: ${keys.length} key${keys.length === 1 ? '' : 's'}`);
|
|
@@ -208,6 +216,8 @@ const fallbackCount = providerState.filter(p => {
|
|
| 208 |
}).length;
|
| 209 |
if (fallbackCount > 0) {
|
| 210 |
log(`[key-rotator] ${fallbackCount} provider(s) using LLM_API_KEY fallback`);
|
|
|
|
|
|
|
| 211 |
}
|
| 212 |
|
| 213 |
// βββ Runtime helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 8 |
*
|
| 9 |
* For each provider you can supply a comma-separated pool:
|
| 10 |
* ANTHROPIC_API_KEYS=key1,key2,key3
|
| 11 |
+
* Falls back to the singular env var, and optionally to LLM_API_KEY.
|
| 12 |
*
|
| 13 |
* Keys are rotated round-robin per provider independently.
|
| 14 |
*
|
|
|
|
| 30 |
// envPlural β env var that holds a comma-separated key pool (preferred)
|
| 31 |
// envSingular β env var that holds a single key (fallback)
|
| 32 |
//
|
| 33 |
+
// LLM_API_KEY fallback can be controlled via:
|
| 34 |
+
// LLM_API_KEY_FALLBACK_ENABLED=true|false
|
| 35 |
+
// Default is enabled for backwards compatibility.
|
| 36 |
//
|
| 37 |
const PROVIDERS = [
|
| 38 |
{
|
|
|
|
| 182 |
|
| 183 |
// Build per-provider key pools + rotation indices
|
| 184 |
const providerState = PROVIDERS.map(p => {
|
| 185 |
+
const llmFallbackRaw = String(process.env.LLM_API_KEY_FALLBACK_ENABLED || '').trim().toLowerCase();
|
| 186 |
+
const llmFallbackEnabled = !/^(0|false|no|off)$/.test(llmFallbackRaw);
|
| 187 |
const dedicatedKeys = normalizeKeys(
|
| 188 |
process.env[p.envPlural] || '',
|
| 189 |
process.env[p.envSingular] || '',
|
|
|
|
| 191 |
const hasDedicated = dedicatedKeys.length > 0;
|
| 192 |
const keys = hasDedicated
|
| 193 |
? dedicatedKeys
|
| 194 |
+
: (
|
| 195 |
+
llmFallbackEnabled
|
| 196 |
+
? normalizeKeys(process.env.LLM_API_KEY || '')
|
| 197 |
+
: []
|
| 198 |
+
);
|
| 199 |
|
| 200 |
if (hasDedicated) {
|
| 201 |
log(`[key-rotator] ${p.name}: ${keys.length} key${keys.length === 1 ? '' : 's'}`);
|
|
|
|
| 216 |
}).length;
|
| 217 |
if (fallbackCount > 0) {
|
| 218 |
log(`[key-rotator] ${fallbackCount} provider(s) using LLM_API_KEY fallback`);
|
| 219 |
+
} else if (process.env.LLM_API_KEY && /^(0|false|no|off)$/i.test(String(process.env.LLM_API_KEY_FALLBACK_ENABLED || ''))) {
|
| 220 |
+
log('[key-rotator] LLM_API_KEY fallback disabled (set LLM_API_KEY_FALLBACK_ENABLED=true to re-enable)');
|
| 221 |
}
|
| 222 |
|
| 223 |
// βββ Runtime helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
openclaw-sync.py
CHANGED
|
@@ -18,6 +18,7 @@ import sys
|
|
| 18 |
import tempfile
|
| 19 |
import threading
|
| 20 |
import time
|
|
|
|
| 21 |
from pathlib import Path
|
| 22 |
|
| 23 |
os.environ.setdefault("HF_HUB_DISABLE_PROGRESS_BARS", "1")
|
|
@@ -69,6 +70,7 @@ EXCLUDED_STATE_NAMES = {
|
|
| 69 |
"openclaw-app",
|
| 70 |
"gateway.log",
|
| 71 |
"browser",
|
|
|
|
| 72 |
}
|
| 73 |
WHATSAPP_CREDS_DIR = OPENCLAW_HOME / "credentials" / "whatsapp" / "default"
|
| 74 |
WHATSAPP_BACKUP_DIR = STATE_DIR / "credentials" / "whatsapp" / "default"
|
|
@@ -76,6 +78,7 @@ RESET_MARKER = WORKSPACE / ".reset_credentials"
|
|
| 76 |
HF_API = HfApi(token=HF_TOKEN) if HF_TOKEN else None
|
| 77 |
STOP_EVENT = threading.Event()
|
| 78 |
_REPO_ID_CACHE: str | None = None
|
|
|
|
| 79 |
|
| 80 |
|
| 81 |
def write_status(status: str, message: str) -> None:
|
|
@@ -280,14 +283,15 @@ def file_marker(path: Path) -> tuple[int, int, int]:
|
|
| 280 |
return (1, int(stat.st_size), int(stat.st_mtime_ns))
|
| 281 |
|
| 282 |
|
| 283 |
-
def metadata_marker(root: Path) ->
|
| 284 |
if not root.exists():
|
| 285 |
-
return (0, 0, 0)
|
| 286 |
|
| 287 |
file_count = 0
|
| 288 |
total_size = 0
|
| 289 |
newest_mtime = 0
|
| 290 |
-
|
|
|
|
| 291 |
if not path.is_file():
|
| 292 |
continue
|
| 293 |
rel = path.relative_to(root).as_posix()
|
|
@@ -298,9 +302,17 @@ def metadata_marker(root: Path) -> tuple[int, int, int]:
|
|
| 298 |
except OSError:
|
| 299 |
continue
|
| 300 |
file_count += 1
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 304 |
|
| 305 |
|
| 306 |
def fingerprint_dir(root: Path) -> str:
|
|
@@ -313,9 +325,16 @@ def fingerprint_dir(root: Path) -> str:
|
|
| 313 |
if _should_exclude(rel, path):
|
| 314 |
continue
|
| 315 |
hasher.update(rel.encode("utf-8"))
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 319 |
return hasher.hexdigest()
|
| 320 |
|
| 321 |
|
|
@@ -331,10 +350,40 @@ def create_snapshot_dir(source_root: Path) -> Path:
|
|
| 331 |
target.mkdir(parents=True, exist_ok=True)
|
| 332 |
continue
|
| 333 |
target.parent.mkdir(parents=True, exist_ok=True)
|
| 334 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 335 |
return staging_root
|
| 336 |
|
| 337 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 338 |
def restore_workspace() -> bool:
|
| 339 |
if not HF_TOKEN:
|
| 340 |
write_status("disabled", "HF_TOKEN is not configured.")
|
|
@@ -396,16 +445,15 @@ def restore_workspace() -> bool:
|
|
| 396 |
|
| 397 |
def _sync_once_unlocked(
|
| 398 |
last_fingerprint: str | None = None,
|
| 399 |
-
last_marker:
|
| 400 |
-
) -> tuple[str,
|
| 401 |
if not HF_TOKEN:
|
| 402 |
write_status("disabled", "HF_TOKEN is not configured.")
|
| 403 |
-
return (last_fingerprint or "", last_marker or (0, 0, 0))
|
| 404 |
|
| 405 |
snapshot_state_into_workspace()
|
| 406 |
repo_id = ensure_repo_exists()
|
| 407 |
current_marker = metadata_marker(WORKSPACE)
|
| 408 |
-
|
| 409 |
if last_marker is not None and current_marker == last_marker:
|
| 410 |
write_status("synced", "No workspace changes detected.")
|
| 411 |
return (last_fingerprint or "", current_marker)
|
|
@@ -435,6 +483,7 @@ def _sync_once_unlocked(
|
|
| 435 |
commit_message=f"HuggingClaw sync {time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())}",
|
| 436 |
ignore_patterns=[".git/*", ".git"],
|
| 437 |
)
|
|
|
|
| 438 |
finally:
|
| 439 |
shutil.rmtree(snapshot_dir, ignore_errors=True)
|
| 440 |
|
|
@@ -444,8 +493,8 @@ def _sync_once_unlocked(
|
|
| 444 |
|
| 445 |
def sync_once(
|
| 446 |
last_fingerprint: str | None = None,
|
| 447 |
-
last_marker:
|
| 448 |
-
) -> tuple[str,
|
| 449 |
SYNC_LOCK_FILE.parent.mkdir(parents=True, exist_ok=True)
|
| 450 |
with SYNC_LOCK_FILE.open("w", encoding="utf-8") as lock_handle:
|
| 451 |
fcntl.flock(lock_handle, fcntl.LOCK_EX)
|
|
|
|
| 18 |
import tempfile
|
| 19 |
import threading
|
| 20 |
import time
|
| 21 |
+
from typing import TypeAlias
|
| 22 |
from pathlib import Path
|
| 23 |
|
| 24 |
os.environ.setdefault("HF_HUB_DISABLE_PROGRESS_BARS", "1")
|
|
|
|
| 70 |
"openclaw-app",
|
| 71 |
"gateway.log",
|
| 72 |
"browser",
|
| 73 |
+
"npm",
|
| 74 |
}
|
| 75 |
WHATSAPP_CREDS_DIR = OPENCLAW_HOME / "credentials" / "whatsapp" / "default"
|
| 76 |
WHATSAPP_BACKUP_DIR = STATE_DIR / "credentials" / "whatsapp" / "default"
|
|
|
|
| 78 |
HF_API = HfApi(token=HF_TOKEN) if HF_TOKEN else None
|
| 79 |
STOP_EVENT = threading.Event()
|
| 80 |
_REPO_ID_CACHE: str | None = None
|
| 81 |
+
WorkspaceMarker: TypeAlias = tuple[int, int, int, str]
|
| 82 |
|
| 83 |
|
| 84 |
def write_status(status: str, message: str) -> None:
|
|
|
|
| 283 |
return (1, int(stat.st_size), int(stat.st_mtime_ns))
|
| 284 |
|
| 285 |
|
| 286 |
+
def metadata_marker(root: Path) -> WorkspaceMarker:
|
| 287 |
if not root.exists():
|
| 288 |
+
return (0, 0, 0, "")
|
| 289 |
|
| 290 |
file_count = 0
|
| 291 |
total_size = 0
|
| 292 |
newest_mtime = 0
|
| 293 |
+
metadata_hasher = hashlib.sha256()
|
| 294 |
+
for path in sorted(root.rglob("*")):
|
| 295 |
if not path.is_file():
|
| 296 |
continue
|
| 297 |
rel = path.relative_to(root).as_posix()
|
|
|
|
| 302 |
except OSError:
|
| 303 |
continue
|
| 304 |
file_count += 1
|
| 305 |
+
size = int(stat.st_size)
|
| 306 |
+
mtime_ns = int(stat.st_mtime_ns)
|
| 307 |
+
total_size += size
|
| 308 |
+
newest_mtime = max(newest_mtime, mtime_ns)
|
| 309 |
+
metadata_hasher.update(rel.encode("utf-8"))
|
| 310 |
+
metadata_hasher.update(b"\0")
|
| 311 |
+
metadata_hasher.update(str(size).encode("ascii"))
|
| 312 |
+
metadata_hasher.update(b"\0")
|
| 313 |
+
metadata_hasher.update(str(mtime_ns).encode("ascii"))
|
| 314 |
+
metadata_hasher.update(b"\0")
|
| 315 |
+
return (file_count, total_size, newest_mtime, metadata_hasher.hexdigest())
|
| 316 |
|
| 317 |
|
| 318 |
def fingerprint_dir(root: Path) -> str:
|
|
|
|
| 325 |
if _should_exclude(rel, path):
|
| 326 |
continue
|
| 327 |
hasher.update(rel.encode("utf-8"))
|
| 328 |
+
try:
|
| 329 |
+
with path.open("rb") as handle:
|
| 330 |
+
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
| 331 |
+
hasher.update(chunk)
|
| 332 |
+
except (FileNotFoundError, IsADirectoryError, NotADirectoryError):
|
| 333 |
+
# Fingerprint must represent a complete view of the workspace.
|
| 334 |
+
# Retry next sync pass instead of silently hashing a partial tree.
|
| 335 |
+
raise RuntimeError(
|
| 336 |
+
f"Workspace changed while hashing {rel}; retrying next sync pass."
|
| 337 |
+
)
|
| 338 |
return hasher.hexdigest()
|
| 339 |
|
| 340 |
|
|
|
|
| 350 |
target.mkdir(parents=True, exist_ok=True)
|
| 351 |
continue
|
| 352 |
target.parent.mkdir(parents=True, exist_ok=True)
|
| 353 |
+
try:
|
| 354 |
+
shutil.copy2(path, target)
|
| 355 |
+
except (FileNotFoundError, IsADirectoryError, NotADirectoryError):
|
| 356 |
+
# Do not upload a partial snapshot; let caller retry on next loop.
|
| 357 |
+
raise RuntimeError(
|
| 358 |
+
f"Snapshot changed while copying {rel_posix}; retrying next sync pass."
|
| 359 |
+
)
|
| 360 |
return staging_root
|
| 361 |
|
| 362 |
|
| 363 |
+
def prune_remote_deleted_files(repo_id: str, snapshot_dir: Path) -> None:
|
| 364 |
+
if HF_API is None:
|
| 365 |
+
return
|
| 366 |
+
|
| 367 |
+
local_files = {
|
| 368 |
+
path.relative_to(snapshot_dir).as_posix()
|
| 369 |
+
for path in snapshot_dir.rglob("*")
|
| 370 |
+
if path.is_file()
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
remote_files = HF_API.list_repo_files(repo_id=repo_id, repo_type="dataset")
|
| 374 |
+
stale_files = [
|
| 375 |
+
path for path in remote_files
|
| 376 |
+
if path not in local_files and path not in {".gitattributes"}
|
| 377 |
+
]
|
| 378 |
+
for stale_path in stale_files:
|
| 379 |
+
HF_API.delete_file(
|
| 380 |
+
path_in_repo=stale_path,
|
| 381 |
+
repo_id=repo_id,
|
| 382 |
+
repo_type="dataset",
|
| 383 |
+
commit_message=f"Delete stale file {stale_path}",
|
| 384 |
+
)
|
| 385 |
+
|
| 386 |
+
|
| 387 |
def restore_workspace() -> bool:
|
| 388 |
if not HF_TOKEN:
|
| 389 |
write_status("disabled", "HF_TOKEN is not configured.")
|
|
|
|
| 445 |
|
| 446 |
def _sync_once_unlocked(
|
| 447 |
last_fingerprint: str | None = None,
|
| 448 |
+
last_marker: WorkspaceMarker | None = None,
|
| 449 |
+
) -> tuple[str, WorkspaceMarker]:
|
| 450 |
if not HF_TOKEN:
|
| 451 |
write_status("disabled", "HF_TOKEN is not configured.")
|
| 452 |
+
return (last_fingerprint or "", last_marker or (0, 0, 0, ""))
|
| 453 |
|
| 454 |
snapshot_state_into_workspace()
|
| 455 |
repo_id = ensure_repo_exists()
|
| 456 |
current_marker = metadata_marker(WORKSPACE)
|
|
|
|
| 457 |
if last_marker is not None and current_marker == last_marker:
|
| 458 |
write_status("synced", "No workspace changes detected.")
|
| 459 |
return (last_fingerprint or "", current_marker)
|
|
|
|
| 483 |
commit_message=f"HuggingClaw sync {time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())}",
|
| 484 |
ignore_patterns=[".git/*", ".git"],
|
| 485 |
)
|
| 486 |
+
prune_remote_deleted_files(repo_id, snapshot_dir)
|
| 487 |
finally:
|
| 488 |
shutil.rmtree(snapshot_dir, ignore_errors=True)
|
| 489 |
|
|
|
|
| 493 |
|
| 494 |
def sync_once(
|
| 495 |
last_fingerprint: str | None = None,
|
| 496 |
+
last_marker: WorkspaceMarker | None = None,
|
| 497 |
+
) -> tuple[str, WorkspaceMarker]:
|
| 498 |
SYNC_LOCK_FILE.parent.mkdir(parents=True, exist_ok=True)
|
| 499 |
with SYNC_LOCK_FILE.open("w", encoding="utf-8") as lock_handle:
|
| 500 |
fcntl.flock(lock_handle, fcntl.LOCK_EX)
|
start.sh
CHANGED
|
@@ -131,6 +131,48 @@ case "$LLM_PROVIDER" in
|
|
| 131 |
;;
|
| 132 |
esac
|
| 133 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
# ββ Setup directories ββ
|
| 135 |
mkdir -p /home/node/.openclaw/agents/main/sessions
|
| 136 |
mkdir -p /home/node/.openclaw/credentials
|
|
@@ -285,6 +327,67 @@ if [ -n "$CUSTOM_PROVIDER_NAME" ] || [ -n "$CUSTOM_BASE_URL" ] || [ -n "$CUSTOM_
|
|
| 285 |
fi
|
| 286 |
fi
|
| 287 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 288 |
# Browser configuration (managed local Chromium in HF/Docker)
|
| 289 |
BROWSER_EXECUTABLE_PATH=""
|
| 290 |
for candidate in /usr/bin/chromium /usr/bin/chromium-browser /snap/bin/chromium; do
|
|
@@ -540,10 +643,15 @@ fi
|
|
| 540 |
# ββ Trap SIGTERM for graceful shutdown ββ
|
| 541 |
graceful_shutdown() {
|
| 542 |
echo "Shutting down..."
|
| 543 |
-
if [ -f "/home/node/app/openclaw-sync.py" ]; then
|
| 544 |
echo "Saving state before exit..."
|
|
|
|
|
|
|
|
|
|
| 545 |
python3 /home/node/app/openclaw-sync.py sync-once || \
|
| 546 |
-
echo "Warning: could not complete shutdown sync"
|
|
|
|
|
|
|
| 547 |
fi
|
| 548 |
kill $(jobs -p) 2>/dev/null
|
| 549 |
exit 0
|
|
|
|
| 131 |
;;
|
| 132 |
esac
|
| 133 |
|
| 134 |
+
# Ensure OpenClaw provider discovery can see per-provider keys even when users
|
| 135 |
+
# configure only *_API_KEYS pools. Mirror first pool key into singular env.
|
| 136 |
+
promote_first_pool_key() {
|
| 137 |
+
local singular_var="$1"
|
| 138 |
+
local pool_var="$2"
|
| 139 |
+
local singular_val="${!singular_var:-}"
|
| 140 |
+
local pool_val="${!pool_var:-}"
|
| 141 |
+
|
| 142 |
+
[ -n "$singular_val" ] && return 0
|
| 143 |
+
[ -n "$pool_val" ] || return 0
|
| 144 |
+
|
| 145 |
+
local first
|
| 146 |
+
first=$(printf '%s' "$pool_val" | tr ',' '\n' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' | awk 'NF{print; exit}')
|
| 147 |
+
[ -n "$first" ] || return 0
|
| 148 |
+
export "${singular_var}=$first"
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
promote_first_pool_key "ANTHROPIC_API_KEY" "ANTHROPIC_API_KEYS"
|
| 152 |
+
promote_first_pool_key "OPENAI_API_KEY" "OPENAI_API_KEYS"
|
| 153 |
+
promote_first_pool_key "GEMINI_API_KEY" "GEMINI_API_KEYS"
|
| 154 |
+
promote_first_pool_key "DEEPSEEK_API_KEY" "DEEPSEEK_API_KEYS"
|
| 155 |
+
promote_first_pool_key "OPENROUTER_API_KEY" "OPENROUTER_API_KEYS"
|
| 156 |
+
promote_first_pool_key "KILOCODE_API_KEY" "KILOCODE_API_KEYS"
|
| 157 |
+
promote_first_pool_key "OPENCODE_API_KEY" "OPENCODE_API_KEYS"
|
| 158 |
+
promote_first_pool_key "ZAI_API_KEY" "ZAI_API_KEYS"
|
| 159 |
+
promote_first_pool_key "MOONSHOT_API_KEY" "MOONSHOT_API_KEYS"
|
| 160 |
+
promote_first_pool_key "MINIMAX_API_KEY" "MINIMAX_API_KEYS"
|
| 161 |
+
promote_first_pool_key "XIAOMI_API_KEY" "XIAOMI_API_KEYS"
|
| 162 |
+
promote_first_pool_key "VOLCANO_ENGINE_API_KEY" "VOLCANO_ENGINE_API_KEYS"
|
| 163 |
+
promote_first_pool_key "BYTEPLUS_API_KEY" "BYTEPLUS_API_KEYS"
|
| 164 |
+
promote_first_pool_key "QIANFAN_API_KEY" "QIANFAN_API_KEYS"
|
| 165 |
+
promote_first_pool_key "MODELSTUDIO_API_KEY" "MODELSTUDIO_API_KEYS"
|
| 166 |
+
promote_first_pool_key "KIMI_API_KEY" "KIMI_API_KEYS"
|
| 167 |
+
promote_first_pool_key "MISTRAL_API_KEY" "MISTRAL_API_KEYS"
|
| 168 |
+
promote_first_pool_key "XAI_API_KEY" "XAI_API_KEYS"
|
| 169 |
+
promote_first_pool_key "NVIDIA_API_KEY" "NVIDIA_API_KEYS"
|
| 170 |
+
promote_first_pool_key "GROQ_API_KEY" "GROQ_API_KEYS"
|
| 171 |
+
promote_first_pool_key "COHERE_API_KEY" "COHERE_API_KEYS"
|
| 172 |
+
promote_first_pool_key "TOGETHER_API_KEY" "TOGETHER_API_KEYS"
|
| 173 |
+
promote_first_pool_key "CEREBRAS_API_KEY" "CEREBRAS_API_KEYS"
|
| 174 |
+
promote_first_pool_key "HUGGINGFACE_HUB_TOKEN" "HUGGINGFACE_HUB_TOKENS"
|
| 175 |
+
|
| 176 |
# ββ Setup directories ββ
|
| 177 |
mkdir -p /home/node/.openclaw/agents/main/sessions
|
| 178 |
mkdir -p /home/node/.openclaw/credentials
|
|
|
|
| 327 |
fi
|
| 328 |
fi
|
| 329 |
|
| 330 |
+
# Optional: explicitly expose provider model lists in Control UI when
|
| 331 |
+
# provider keys are configured. Format:
|
| 332 |
+
# NVIDIA_MODELS=model1,model2
|
| 333 |
+
# OPENAI_MODELS=gpt-4o-mini,gpt-4.1
|
| 334 |
+
# This helps when provider auto-discovery does not populate models reliably.
|
| 335 |
+
inject_provider_models_from_env() {
|
| 336 |
+
local provider="$1"
|
| 337 |
+
local models_env="$2"
|
| 338 |
+
local key_env_single="$3"
|
| 339 |
+
local key_env_pool="$4"
|
| 340 |
+
local models_csv="${!models_env:-}"
|
| 341 |
+
local single_key="${!key_env_single:-}"
|
| 342 |
+
local pool_keys="${!key_env_pool:-}"
|
| 343 |
+
|
| 344 |
+
# Only inject when both:
|
| 345 |
+
# 1) provider has at least one configured key
|
| 346 |
+
# 2) explicit model list env is provided
|
| 347 |
+
if [ -z "$models_csv" ] || { [ -z "$single_key" ] && [ -z "$pool_keys" ]; }; then
|
| 348 |
+
return 0
|
| 349 |
+
fi
|
| 350 |
+
|
| 351 |
+
local models_json
|
| 352 |
+
models_json=$(printf '%s' "$models_csv" \
|
| 353 |
+
| tr ',' '\n' \
|
| 354 |
+
| sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' \
|
| 355 |
+
| awk 'NF' \
|
| 356 |
+
| jq -R . \
|
| 357 |
+
| jq -s 'map({id: ., name: .}) | unique_by(.id)')
|
| 358 |
+
|
| 359 |
+
CONFIG_JSON=$(jq \
|
| 360 |
+
--arg provider "$provider" \
|
| 361 |
+
--argjson models "$models_json" \
|
| 362 |
+
'.models.mode = "merge"
|
| 363 |
+
| .models.providers[$provider] = ((.models.providers[$provider] // {}) + {models: $models})' <<<"$CONFIG_JSON")
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
# Built-in provider model envs (optional)
|
| 367 |
+
inject_provider_models_from_env "anthropic" "ANTHROPIC_MODELS" "ANTHROPIC_API_KEY" "ANTHROPIC_API_KEYS"
|
| 368 |
+
inject_provider_models_from_env "openai" "OPENAI_MODELS" "OPENAI_API_KEY" "OPENAI_API_KEYS"
|
| 369 |
+
inject_provider_models_from_env "google" "GEMINI_MODELS" "GEMINI_API_KEY" "GEMINI_API_KEYS"
|
| 370 |
+
inject_provider_models_from_env "deepseek" "DEEPSEEK_MODELS" "DEEPSEEK_API_KEY" "DEEPSEEK_API_KEYS"
|
| 371 |
+
inject_provider_models_from_env "openrouter" "OPENROUTER_MODELS" "OPENROUTER_API_KEY" "OPENROUTER_API_KEYS"
|
| 372 |
+
inject_provider_models_from_env "kilocode" "KILOCODE_MODELS" "KILOCODE_API_KEY" "KILOCODE_API_KEYS"
|
| 373 |
+
inject_provider_models_from_env "opencode" "OPENCODE_MODELS" "OPENCODE_API_KEY" "OPENCODE_API_KEYS"
|
| 374 |
+
inject_provider_models_from_env "zai" "ZAI_MODELS" "ZAI_API_KEY" "ZAI_API_KEYS"
|
| 375 |
+
inject_provider_models_from_env "moonshot" "MOONSHOT_MODELS" "MOONSHOT_API_KEY" "MOONSHOT_API_KEYS"
|
| 376 |
+
inject_provider_models_from_env "kimi-coding" "KIMI_MODELS" "KIMI_API_KEY" "KIMI_API_KEYS"
|
| 377 |
+
inject_provider_models_from_env "minimax" "MINIMAX_MODELS" "MINIMAX_API_KEY" "MINIMAX_API_KEYS"
|
| 378 |
+
inject_provider_models_from_env "modelstudio" "MODELSTUDIO_MODELS" "MODELSTUDIO_API_KEY" "MODELSTUDIO_API_KEYS"
|
| 379 |
+
inject_provider_models_from_env "xiaomi" "XIAOMI_MODELS" "XIAOMI_API_KEY" "XIAOMI_API_KEYS"
|
| 380 |
+
inject_provider_models_from_env "volcengine" "VOLCANO_ENGINE_MODELS" "VOLCANO_ENGINE_API_KEY" "VOLCANO_ENGINE_API_KEYS"
|
| 381 |
+
inject_provider_models_from_env "byteplus" "BYTEPLUS_MODELS" "BYTEPLUS_API_KEY" "BYTEPLUS_API_KEYS"
|
| 382 |
+
inject_provider_models_from_env "qianfan" "QIANFAN_MODELS" "QIANFAN_API_KEY" "QIANFAN_API_KEYS"
|
| 383 |
+
inject_provider_models_from_env "groq" "GROQ_MODELS" "GROQ_API_KEY" "GROQ_API_KEYS"
|
| 384 |
+
inject_provider_models_from_env "mistral" "MISTRAL_MODELS" "MISTRAL_API_KEY" "MISTRAL_API_KEYS"
|
| 385 |
+
inject_provider_models_from_env "xai" "XAI_MODELS" "XAI_API_KEY" "XAI_API_KEYS"
|
| 386 |
+
inject_provider_models_from_env "nvidia" "NVIDIA_MODELS" "NVIDIA_API_KEY" "NVIDIA_API_KEYS"
|
| 387 |
+
inject_provider_models_from_env "cohere" "COHERE_MODELS" "COHERE_API_KEY" "COHERE_API_KEYS"
|
| 388 |
+
inject_provider_models_from_env "together" "TOGETHER_MODELS" "TOGETHER_API_KEY" "TOGETHER_API_KEYS"
|
| 389 |
+
inject_provider_models_from_env "cerebras" "CEREBRAS_MODELS" "CEREBRAS_API_KEY" "CEREBRAS_API_KEYS"
|
| 390 |
+
|
| 391 |
# Browser configuration (managed local Chromium in HF/Docker)
|
| 392 |
BROWSER_EXECUTABLE_PATH=""
|
| 393 |
for candidate in /usr/bin/chromium /usr/bin/chromium-browser /snap/bin/chromium; do
|
|
|
|
| 643 |
# ββ Trap SIGTERM for graceful shutdown ββ
|
| 644 |
graceful_shutdown() {
|
| 645 |
echo "Shutting down..."
|
| 646 |
+
if [ -f "/home/node/app/openclaw-sync.py" ] && [ -n "${HF_TOKEN:-}" ]; then
|
| 647 |
echo "Saving state before exit..."
|
| 648 |
+
timeout 8s python3 /home/node/app/openclaw-sync.py sync-once-settled || \
|
| 649 |
+
echo "Warning: could not complete settled shutdown sync"
|
| 650 |
+
sleep 1
|
| 651 |
python3 /home/node/app/openclaw-sync.py sync-once || \
|
| 652 |
+
echo "Warning: could not complete final shutdown sync"
|
| 653 |
+
elif [ -f "/home/node/app/openclaw-sync.py" ]; then
|
| 654 |
+
echo "HF_TOKEN not set; skipping shutdown backup sync."
|
| 655 |
fi
|
| 656 |
kill $(jobs -p) 2>/dev/null
|
| 657 |
exit 0
|