Spaces:
Running
Running
Commit ·
c0ec1fe
1
Parent(s): 33980b3
Align env reference example with fallback default
Browse files- .env.example +4 -1
- CHANGELOG.md +38 -0
- ENV_VARIABLES_FULL_LIST.md +219 -0
- README.md +2 -2
- multi-provider-key-rotator.cjs +13 -3
- openclaw-sync.py +40 -16
- start.sh +7 -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 |
#
|
|
|
|
| 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 |
#
|
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,219 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
- `MISTRAL_API_KEYS`
|
| 79 |
+
- `XAI_API_KEYS`
|
| 80 |
+
- `NVIDIA_API_KEYS`
|
| 81 |
+
- `GROQ_API_KEYS`
|
| 82 |
+
- `COHERE_API_KEYS`
|
| 83 |
+
- `TOGETHER_API_KEYS`
|
| 84 |
+
- `CEREBRAS_API_KEYS`
|
| 85 |
+
- `HUGGINGFACE_HUB_TOKENS`
|
| 86 |
+
|
| 87 |
+
### Telegram / WhatsApp
|
| 88 |
+
- `TELEGRAM_BOT_TOKEN` — bot token string. Example: `TELEGRAM_BOT_TOKEN=123456:ABC...`
|
| 89 |
+
- `TELEGRAM_ALLOWED_USERS` — comma-separated numeric IDs. Example: `TELEGRAM_ALLOWED_USERS=12345,67890`
|
| 90 |
+
- `TELEGRAM_USER_ID` — legacy single numeric ID. Example: `TELEGRAM_USER_ID=12345`
|
| 91 |
+
- `TELEGRAM_USER_IDS` — legacy comma-separated IDs. Example: `TELEGRAM_USER_IDS=12345,67890`
|
| 92 |
+
- `WHATSAPP_ENABLED` — boolean-like (`true/false`, `1/0`, `yes/no`, `on/off`). Example: `WHATSAPP_ENABLED=true`
|
| 93 |
+
|
| 94 |
+
### Backup/sync
|
| 95 |
+
- `HF_TOKEN` — HF token string. Example: `HF_TOKEN=hf_xxxxx`
|
| 96 |
+
- `HF_USERNAME` — HF username string. Example: `HF_USERNAME=myhandle`
|
| 97 |
+
- `BACKUP_DATASET_NAME` — dataset slug string. Example: `BACKUP_DATASET_NAME=huggingclaw-backup`
|
| 98 |
+
- `SYNC_INTERVAL` — integer seconds. Example: `SYNC_INTERVAL=180`
|
| 99 |
+
- `SYNC_START_DELAY` — integer seconds. Example: `SYNC_START_DELAY=10`
|
| 100 |
+
- `SYNC_MAX_FILE_BYTES` — integer bytes. Example: `SYNC_MAX_FILE_BYTES=52428800`
|
| 101 |
+
- `OPENCLAW_CONFIG_WATCH_INTERVAL` — number (supports decimal seconds). Example: `OPENCLAW_CONFIG_WATCH_INTERVAL=1`
|
| 102 |
+
- `OPENCLAW_CONFIG_SETTLE_SECONDS` — number (supports decimal seconds). Example: `OPENCLAW_CONFIG_SETTLE_SECONDS=3`
|
| 103 |
+
|
| 104 |
+
### Gateway/runtime tuning
|
| 105 |
+
- `GATEWAY_HOST` — hostname/IP string. Example: `GATEWAY_HOST=127.0.0.1`
|
| 106 |
+
- `GATEWAY_PORT` — integer port. Example: `GATEWAY_PORT=3456`
|
| 107 |
+
- `GATEWAY_VERBOSE` — boolean-like. Example: `GATEWAY_VERBOSE=true`
|
| 108 |
+
- `GATEWAY_READY_TIMEOUT` — integer seconds. Example: `GATEWAY_READY_TIMEOUT=60`
|
| 109 |
+
- `GATEWAY_MAX_RESTARTS` — integer count. Example: `GATEWAY_MAX_RESTARTS=10`
|
| 110 |
+
- `GATEWAY_RESTART_DELAY` — integer seconds. Example: `GATEWAY_RESTART_DELAY=2`
|
| 111 |
+
- `TRUSTED_PROXIES` — comma-separated CIDR/IP list. Example: `TRUSTED_PROXIES=127.0.0.1,10.0.0.0/8`
|
| 112 |
+
- `ALLOWED_ORIGINS` — comma-separated origins. Example: `ALLOWED_ORIGINS=https://a.com,https://b.com`
|
| 113 |
+
- `PORT` — integer external port. Example: `PORT=7860`
|
| 114 |
+
|
| 115 |
+
### Browser/plugin controls
|
| 116 |
+
- `BROWSER_PLUGIN_MODE` — mode string (`auto`, `builtin`, `external`, etc. depending on runtime). Example: `BROWSER_PLUGIN_MODE=auto`
|
| 117 |
+
- `BROWSER_EXECUTABLE_PATH` — absolute path. Example: `BROWSER_EXECUTABLE_PATH=/usr/bin/chromium`
|
| 118 |
+
- `BROWSER_DISABLED` — boolean-like. Example: `BROWSER_DISABLED=false`
|
| 119 |
+
- `ACP_PLUGIN_MODE` — mode string. Example: `ACP_PLUGIN_MODE=auto`
|
| 120 |
+
- `ACPX_DISABLED` — boolean-like. Example: `ACPX_DISABLED=true`
|
| 121 |
+
|
| 122 |
+
### OpenClaw logging/behavior
|
| 123 |
+
- `OPENCLAW_VERSION` — image/app version string. Example: `OPENCLAW_VERSION=latest`
|
| 124 |
+
- `OPENCLAW_RUNTIME_VERSION` — runtime tag string. Example: `OPENCLAW_RUNTIME_VERSION=v1.2.3`
|
| 125 |
+
- `OPENCLAW_DISPLAY_VERSION` — display label string. Example: `OPENCLAW_DISPLAY_VERSION=Custom`
|
| 126 |
+
- `OPENCLAW_DISABLE_BONJOUR` — boolean-like. Example: `OPENCLAW_DISABLE_BONJOUR=true`
|
| 127 |
+
- `OPENCLAW_CONSOLE_LOG_LEVEL` — log level (`trace|debug|info|warn|error`). Example: `OPENCLAW_CONSOLE_LOG_LEVEL=warn`
|
| 128 |
+
- `OPENCLAW_CONSOLE_LOG_STYLE` — style string. Example: `OPENCLAW_CONSOLE_LOG_STYLE=pretty`
|
| 129 |
+
- `OPENCLAW_FILE_LOG_LEVEL` — log level (`trace|debug|info|warn|error`). Example: `OPENCLAW_FILE_LOG_LEVEL=info`
|
| 130 |
+
|
| 131 |
+
### Custom OpenAI-compatible provider
|
| 132 |
+
- `CUSTOM_PROVIDER_NAME` — unique provider prefix. Example: `CUSTOM_PROVIDER_NAME=modal`
|
| 133 |
+
- `CUSTOM_BASE_URL` — base URL only (not `/chat/completions`). Example: `CUSTOM_BASE_URL=https://api.modal.com/v1`
|
| 134 |
+
- `CUSTOM_MODEL_ID` — model id string. Example: `CUSTOM_MODEL_ID=zai-org/GLM-5.1-FP8`
|
| 135 |
+
- `CUSTOM_MODEL_NAME` — display name. Example: `CUSTOM_MODEL_NAME=GLM 5.1 FP8`
|
| 136 |
+
- `CUSTOM_API_KEY` — provider key string. Example: `CUSTOM_API_KEY=sk-xxxx`
|
| 137 |
+
- `CUSTOM_API_TYPE` — API mode string. Example: `CUSTOM_API_TYPE=openai-completions`
|
| 138 |
+
- `CUSTOM_CONTEXT_WINDOW` — integer token window. Example: `CUSTOM_CONTEXT_WINDOW=128000`
|
| 139 |
+
- `CUSTOM_MAX_TOKENS` — integer max tokens. Example: `CUSTOM_MAX_TOKENS=500`
|
| 140 |
+
|
| 141 |
+
### Cloudflare proxy/keepalive
|
| 142 |
+
- `CLOUDFLARE_WORKERS_TOKEN` — API token string. Example: `CLOUDFLARE_WORKERS_TOKEN=cf_xxx`
|
| 143 |
+
- `CLOUDFLARE_WORKER_NAME` — worker name string. Example: `CLOUDFLARE_WORKER_NAME=huggingclaw-proxy`
|
| 144 |
+
- `CLOUDFLARE_ACCOUNT_ID` — account id string. Example: `CLOUDFLARE_ACCOUNT_ID=abc123...`
|
| 145 |
+
- `CLOUDFLARE_PROXY_URL` — full proxy URL. Example: `CLOUDFLARE_PROXY_URL=https://my-worker.workers.dev`
|
| 146 |
+
- `CLOUDFLARE_PROXY_SECRET` — shared secret string. Example: `CLOUDFLARE_PROXY_SECRET=long-random-secret`
|
| 147 |
+
- `CLOUDFLARE_PROXY_DOMAINS` — comma-separated domains. Example: `CLOUDFLARE_PROXY_DOMAINS=api.openai.com,api.anthropic.com`
|
| 148 |
+
- `CLOUDFLARE_PROXY_DEBUG` — boolean-like. Example: `CLOUDFLARE_PROXY_DEBUG=false`
|
| 149 |
+
- `CLOUDFLARE_KEEPALIVE_ENABLED` — boolean-like. Example: `CLOUDFLARE_KEEPALIVE_ENABLED=true`
|
| 150 |
+
- `CLOUDFLARE_KEEPALIVE_URL` — URL string. Example: `CLOUDFLARE_KEEPALIVE_URL=https://space-url.hf.space`
|
| 151 |
+
- `CLOUDFLARE_KEEPALIVE_CRON` — cron string. Example: `CLOUDFLARE_KEEPALIVE_CRON=*/5 * * * *`
|
| 152 |
+
- `CLOUDFLARE_KEEPALIVE_WORKER_NAME` — worker name string. Example: `CLOUDFLARE_KEEPALIVE_WORKER_NAME=huggingclaw-keepalive`
|
| 153 |
+
|
| 154 |
+
### Startup command replay/install helpers
|
| 155 |
+
- `HUGGINGCLAW_RUN` — shell commands string. Example: `HUGGINGCLAW_RUN=apt-get update && apt-get install -y ffmpeg`
|
| 156 |
+
- `HUGGINGCLAW_APT_PACKAGES` — comma/space separated package names. Example: `HUGGINGCLAW_APT_PACKAGES=ffmpeg,git`
|
| 157 |
+
- `HUGGINGCLAW_PIP_PACKAGES` — pip package list string. Example: `HUGGINGCLAW_PIP_PACKAGES=yt-dlp,requests`
|
| 158 |
+
- `HUGGINGCLAW_NPM_PACKAGES` — npm package list string. Example: `HUGGINGCLAW_NPM_PACKAGES=sharp,axios`
|
| 159 |
+
- `HUGGINGCLAW_OPENCLAW_PLUGINS` — plugin ids list string. Example: `HUGGINGCLAW_OPENCLAW_PLUGINS=@openclaw/telegram,@openclaw/whatsapp`
|
| 160 |
+
- `HUGGINGCLAW_STARTUP_SCRIPT` — shell script string. Example: `HUGGINGCLAW_STARTUP_SCRIPT=echo hello`
|
| 161 |
+
- `HUGGINGCLAW_STARTUP_SCRIPT_B64` — base64 script content. Example: `HUGGINGCLAW_STARTUP_SCRIPT_B64=IyEvYmluL2Jhc2gKZWNobyBoaQ==`
|
| 162 |
+
- `HUGGINGCLAW_STARTUP_COMMANDS` — newline/semicolon commands string.
|
| 163 |
+
- `HUGGINGCLAW_STARTUP_STRICT` — boolean-like. Example: `HUGGINGCLAW_STARTUP_STRICT=true`
|
| 164 |
+
- `WEBHOOK_URL` — webhook endpoint URL. Example: `WEBHOOK_URL=https://hooks.example.com/hc`
|
| 165 |
+
|
| 166 |
+
---
|
| 167 |
+
|
| 168 |
+
## 2) Runtime/Internal envs (normally do **not** set manually)
|
| 169 |
+
|
| 170 |
+
These are mostly derived/temporary/process variables used by scripts:
|
| 171 |
+
|
| 172 |
+
- `APP_BASE`
|
| 173 |
+
- `BACKUP_DATASET`
|
| 174 |
+
- `BROWSER_SHOULD_ENABLE`
|
| 175 |
+
- `CF_PROXY_ENV_FILE`
|
| 176 |
+
- `CLEAN_TG_TOKEN`
|
| 177 |
+
- `CONFIG_JSON`
|
| 178 |
+
- `CUSTOM_BASE_URL_NORMALIZED`
|
| 179 |
+
- `CUSTOM_PROVIDER_NORMALIZED`
|
| 180 |
+
- `CUSTOM_PROVIDER_OK`
|
| 181 |
+
- `DEBIAN_FRONTEND`
|
| 182 |
+
- `ERRORS`
|
| 183 |
+
- `EXISTING_CONFIG`
|
| 184 |
+
- `GATEWAY_EXIT_CODE`
|
| 185 |
+
- `GATEWAY_PID`
|
| 186 |
+
- `GATEWAY_RESTART_COUNT`
|
| 187 |
+
- `GUARDIAN_PID`
|
| 188 |
+
- `HC_STARTUP_FAILURES`
|
| 189 |
+
- `HC_STARTUP_INDEX`
|
| 190 |
+
- `HC_STARTUP_STRICT_NORMALIZED`
|
| 191 |
+
- `HC_STARTUP_VAR`
|
| 192 |
+
- `HOME`
|
| 193 |
+
- `IDS_JSON`
|
| 194 |
+
- `INSTALLS`
|
| 195 |
+
- `LLM_PROVIDER`
|
| 196 |
+
- `NODE_OPTIONS`
|
| 197 |
+
- `NPM_CONFIG_PREFIX`
|
| 198 |
+
- `OPENCLAW_APP_DIR`
|
| 199 |
+
- `OPENCLAW_CONSOLE_LOG_LEVEL_CONFIGURED`
|
| 200 |
+
- `OPENCLAW_CONSOLE_LOG_STYLE_CONFIGURED`
|
| 201 |
+
- `OPENCLAW_FILE_LOG_LEVEL_CONFIGURED`
|
| 202 |
+
- `ORIGINS_JSON`
|
| 203 |
+
- `PATCHED`
|
| 204 |
+
- `PATH`
|
| 205 |
+
- `PLUGIN_ALLOW_JSON`
|
| 206 |
+
- `PROXIES_JSON`
|
| 207 |
+
- `PROXY_URL`
|
| 208 |
+
- `PYTHONUSERBASE`
|
| 209 |
+
- `RESET_MARKER_PATH`
|
| 210 |
+
- `SPACE_AUTHOR_NAME`
|
| 211 |
+
- `SPACE_HOST`
|
| 212 |
+
- `SPACE_REPO_NAME`
|
| 213 |
+
- `STARTUP_FILE`
|
| 214 |
+
- `SYNC_LOOP_PID`
|
| 215 |
+
- `VIRTUAL_ENV`
|
| 216 |
+
- `WEBHOOK_BODY`
|
| 217 |
+
- `WHATSAPP_CONFIG_ENABLED`
|
| 218 |
+
- `WHATSAPP_ENABLED_CONFIGURED`
|
| 219 |
+
- `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,7 +350,13 @@ 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 |
|
|
@@ -396,16 +421,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)
|
|
@@ -444,8 +468,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 |
|
|
|
|
| 421 |
|
| 422 |
def _sync_once_unlocked(
|
| 423 |
last_fingerprint: str | None = None,
|
| 424 |
+
last_marker: WorkspaceMarker | None = None,
|
| 425 |
+
) -> tuple[str, WorkspaceMarker]:
|
| 426 |
if not HF_TOKEN:
|
| 427 |
write_status("disabled", "HF_TOKEN is not configured.")
|
| 428 |
+
return (last_fingerprint or "", last_marker or (0, 0, 0, ""))
|
| 429 |
|
| 430 |
snapshot_state_into_workspace()
|
| 431 |
repo_id = ensure_repo_exists()
|
| 432 |
current_marker = metadata_marker(WORKSPACE)
|
|
|
|
| 433 |
if last_marker is not None and current_marker == last_marker:
|
| 434 |
write_status("synced", "No workspace changes detected.")
|
| 435 |
return (last_fingerprint or "", current_marker)
|
|
|
|
| 468 |
|
| 469 |
def sync_once(
|
| 470 |
last_fingerprint: str | None = None,
|
| 471 |
+
last_marker: WorkspaceMarker | None = None,
|
| 472 |
+
) -> tuple[str, WorkspaceMarker]:
|
| 473 |
SYNC_LOCK_FILE.parent.mkdir(parents=True, exist_ok=True)
|
| 474 |
with SYNC_LOCK_FILE.open("w", encoding="utf-8") as lock_handle:
|
| 475 |
fcntl.flock(lock_handle, fcntl.LOCK_EX)
|
start.sh
CHANGED
|
@@ -540,10 +540,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
|
|
|
|
| 540 |
# ── Trap SIGTERM for graceful shutdown ──
|
| 541 |
graceful_shutdown() {
|
| 542 |
echo "Shutting down..."
|
| 543 |
+
if [ -f "/home/node/app/openclaw-sync.py" ] && [ -n "${HF_TOKEN:-}" ]; then
|
| 544 |
echo "Saving state before exit..."
|
| 545 |
+
timeout 8s python3 /home/node/app/openclaw-sync.py sync-once-settled || \
|
| 546 |
+
echo "Warning: could not complete settled shutdown sync"
|
| 547 |
+
sleep 1
|
| 548 |
python3 /home/node/app/openclaw-sync.py sync-once || \
|
| 549 |
+
echo "Warning: could not complete final shutdown sync"
|
| 550 |
+
elif [ -f "/home/node/app/openclaw-sync.py" ]; then
|
| 551 |
+
echo "HF_TOKEN not set; skipping shutdown backup sync."
|
| 552 |
fi
|
| 553 |
kill $(jobs -p) 2>/dev/null
|
| 554 |
exit 0
|