Compare commits

...
Author SHA1 Message Date
root 971e4bc8e0 chore(build): bump add-on runtime to Node 24 2026-07-17 00:34:09 +03:00
root 883f5b8778 Merge pull request #162 from techartdev/fix/issues-159-150-release 2026-07-17 00:12:08 +03:00
root 7608357297 fix(addon): repair runtime downgrade and lan certs 2026-07-15 13:08:13 +03:00
TechArtDev 9235903ef2 Merge pull request #161 from techartdev/bot/openclaw-bump-2026.7.1
Bump OpenClaw to 2026.7.1
2026-07-14 18:14:19 +03:00
root d5f3831131 Bump OpenClaw to 2026.7.1 (add-on 0.5.81) 2026-07-14 09:00:42 +03:00
TechArtDev b45e665bca Merge pull request #157 from techartdev/bot/openclaw-bump-2026.6.10
Bump OpenClaw to 2026.6.10
2026-06-26 17:51:42 +03:00
root 0b799f5fb1 Bump OpenClaw to 2026.6.10 (add-on 0.5.80) 2026-06-26 09:00:44 +03:00
TechArtDev 6129b62512 Merge pull request #156 from techartdev/bot/openclaw-bump-2026.6.9
Bump OpenClaw to 2026.6.9
2026-06-24 00:48:22 +03:00
root fe082864ec Bump OpenClaw to 2026.6.9 (add-on 0.5.79) 2026-06-23 09:00:40 +03:00
TechArtDev 4d8fa8996a Merge pull request #153 from techartdev/bot/openclaw-bump-2026.6.6
Bump OpenClaw to 2026.6.6
2026-06-16 23:54:44 +03:00
root 45d4066d2a Bump OpenClaw to 2026.6.6 (add-on 0.5.78) 2026-06-16 09:00:32 +03:00
TechArtDev 28574b28ef Merge pull request #149 from techartdev/bot/openclaw-bump-2026.5.28
Bump OpenClaw to 2026.5.28
2026-05-31 10:50:08 +03:00
root 089594a95b Bump OpenClaw to 2026.5.28 (add-on 0.5.77) 2026-05-31 09:00:39 +03:00
TechArtDev 08bc68b9e7 Merge pull request #148 from techartdev/bot/openclaw-bump-2026.5.27
Bump OpenClaw to 2026.5.27
2026-05-29 13:22:21 +03:00
TechArtDev 3a06c4604f Merge branch 'main' into bot/openclaw-bump-2026.5.27 2026-05-29 13:22:09 +03:00
TechArtDev 661085afc3 Bump version to 0.5.76 in config.yaml 2026-05-29 13:21:32 +03:00
TechArtDev ccde8598d7 Merge pull request #146 from techartdev/fix/backup-friendly-persistence-defaults
fix(addon): make heavy tool persistence opt-in
2026-05-29 13:15:28 +03:00
root 80f86ae030 Bump OpenClaw to 2026.5.27 (add-on 0.5.75) 2026-05-29 09:01:20 +03:00
root 082c2c74b2 fix(addon): keep backups lean by default 2026-05-28 01:47:06 +03:00
TechArtDev d070dbe1e1 Merge pull request #145 from techartdev/fix/issue-136-local-embeddings-addon
fix(addon): bundle node-llama-cpp for local embeddings
2026-05-27 23:43:19 +03:00
root 9824f7c138 fix: bundle local embeddings runtime in addon 2026-05-27 23:28:33 +03:00
TechArtDev 71e3fbe410 Merge pull request #144 from techartdev/bot/openclaw-bump-2026.5.22
Bump OpenClaw to 2026.5.22
2026-05-26 14:11:23 +03:00
root c2398c84a4 Bump OpenClaw to 2026.5.22 (add-on 0.5.73) 2026-05-26 09:00:38 +03:00
techartdev 3e5e0877b1 Bump OpenClaw to version 0.5.72 and add repair for known invalid config settings 2026-05-04 10:19:53 +03:00
TechArtDev c769cb88e1 Merge pull request #127 from techartdev/bot/openclaw-bump-2026.5.2
Bump OpenClaw to 2026.5.2
2026-05-03 12:01:49 +03:00
root 865ca08873 Bump OpenClaw to 2026.5.2 (add-on 0.5.71) 2026-05-03 09:01:16 +03:00
TechArtDev f0a465bf7c Update OpenClaw to version 2026.4.27
Bump OpenClaw version to 2026.4.27.
2026-04-30 04:22:52 +03:00
TechArtDev 80ef635315 Merge pull request #124 from techartdev/bot/openclaw-bump-2026.4.26
Bump OpenClaw to 2026.4.26
2026-04-30 04:19:20 +03:00
TechArtDev 2acfc77a3f Update OpenClaw version to 2026.4.27 2026-04-30 04:19:09 +03:00
root 91a2ddd01f Bump OpenClaw to 2026.4.26 (add-on 0.5.70) 2026-04-29 09:01:24 +03:00
TechArtDev f66c574611 Merge pull request #122 from techartdev/bot/openclaw-bump-2026.4.24
Bump OpenClaw to 2026.4.24
2026-04-27 14:33:27 +03:00
root 57a8bf3868 Bump OpenClaw to 2026.4.24 (add-on 0.5.69) 2026-04-27 09:01:16 +03:00
TechArtDev 2b6c875b0e Merge pull request #121 from techartdev/bot/openclaw-bump-2026.4.22
Bump OpenClaw to 2026.4.22
2026-04-25 00:38:13 +03:00
root ff5e1eba1e Bump OpenClaw to 2026.4.22 (add-on 0.5.68) 2026-04-24 09:01:21 +03:00
TechArtDev 9a65e66b75 Merge pull request #116 from techartdev/bot/openclaw-bump-2026.4.12
Bump OpenClaw to 2026.4.12
2026-04-14 12:09:42 +03:00
root 9b7b701d5b Bump OpenClaw to 2026.4.12 (add-on 0.5.67) 2026-04-14 09:00:41 +03:00
TechArtDev 505115e06f Merge pull request #107 from techartdev/fix/landing-token-post-onboard
fix: re-render landing page with gateway token after onboard (issue #102)
2026-04-04 16:17:54 +03:00
root 72495984ad fix: re-render landing page with token after onboard (issue #102)
On first boot the gateway token doesn't exist yet when nginx/landing are
first rendered, so the 'Open Gateway Web UI' button opens the Control UI
without a token and the user sees Unauthorized.

Fix: extract render logic into render_landing(), call it once at startup
(as before) and spawn a background poller that re-renders and HUPs nginx
as soon as the token appears in openclaw.json — no add-on restart needed.

Bumps add-on to 0.5.66.
2026-04-04 15:03:26 +03:00
TechArtDev ba8fd59571 Merge pull request #106 from techartdev/bot/openclaw-bump-2026.4.2
chore: bump OpenClaw to 2026.4.2
2026-04-04 03:09:45 +03:00
root dab02d2e22 chore: bump OpenClaw to 2026.4.2 2026-04-04 03:09:20 +03:00
techartdev 38e104d663 Merge branch 'main' of https://github.com/techartdev/OpenClawHomeAssistant 2026-03-18 14:21:27 +02:00
techartdev 991cc3bcfc feat: enhance gateway daemon detection and restart handling 2026-03-18 14:21:19 +02:00
techartdev 1a4e23e8c3 feat: update version to 0.5.63 and bump OpenClaw to 2026.3.13 2026-03-14 20:14:44 +02:00
techartdev 4f56a16b91 feat: update version to 0.5.62 and fix gateway restart loop issues 2026-03-10 23:31:04 +02:00
techartdev 6a08c66128 feat: update version to 0.5.61 and fix gateway restart loop in tailnet mode 2026-03-10 23:05:32 +02:00
techartdev 02814db080 feat: update version to 0.5.60 and enhance session lock cleanup for non-default agents 2026-03-10 22:44:51 +02:00
13 changed files with 884 additions and 152 deletions
+51 -18
View File
@@ -43,10 +43,10 @@ When you open the add-on page in Home Assistant, nginx serves a landing page wit
| `/config/` | Yes | All user data — survives add-on updates and rebuilds |
| `/config/.openclaw/` | Yes | OpenClaw configuration (`openclaw.json`), skills, agent data |
| `/config/clawd/` | Yes | Agent workspace (ClawHub-installed skills, files) |
| `/config/.node_global/` | Yes | User-installed npm packages (skills installed via dashboard) |
| `/config/.node_global/` | Optional | User-installed npm packages when `persist_node_global=true` |
| `/config/secrets/` | Yes | Tokens (e.g., `homeassistant.token`) |
| `/config/keys/` | Yes | SSH keys (e.g., router SSH key) |
| `/config/.linuxbrew/` | Yes | Homebrew install and brew-installed CLI tools |
| `/config/.linuxbrew/` | Optional | Homebrew install and brew-installed CLI tools when `persist_brew_tools=true` |
| `/config/gogcli/` | Yes | gog OAuth credentials for Google APIs |
| `/usr/lib/node_modules/openclaw/` | No | OpenClaw installation (rebuilt with each image update) |
@@ -113,8 +113,9 @@ Save this token — you'll need it to access the Gateway Web UI and for API inte
1. In the terminal, confirm the gateway is running:
```sh
openclaw gateway status
oc-gateway status
```
> In this add-on, the gateway is supervised by `run.sh` (not systemd), so `openclaw gateway status` can show misleading service-manager warnings.
2. Click the **Open Gateway Web UI** button on the landing page
3. If prompted for a token, paste the one from Step 2 or go to the Overview tab, paste the token in the 'Gateway Token' field and press Connect.
@@ -313,7 +314,10 @@ To provide the SSH key: place the private key file in the add-on config director
| Option | Type | Default | Description |
|---|---|---|---|
| `clean_session_locks_on_start` | bool | `true` | Remove stale session lock files on startup (safe — only removes locks when gateway isn't running) |
| `clean_session_locks_on_exit` | bool | `true` | Remove session lock files on clean shutdown || `auto_configure_mcp` | bool | `false` | Auto-register Home Assistant as an MCP server on startup (requires `homeassistant_token`) |
| `clean_session_locks_on_exit` | bool | `true` | Remove session lock files on clean shutdown |
| `persist_node_global` | bool | `false` | Persist user-installed npm global skills/packages in `/config/.node_global/`. Turn on only if you want those installs to survive add-on rebuilds. |
| `persist_brew_tools` | bool | `false` | Persist Homebrew and brew-installed CLI tools in `/config/.linuxbrew/`. Turn on only if you want those installs to survive add-on rebuilds. |
| `auto_configure_mcp` | bool | `false` | Auto-register Home Assistant as an MCP server on startup (requires `homeassistant_token`) |
---
## 6. Use Case Guides
@@ -647,10 +651,10 @@ You should see your account listed with the `sheets` service.
| Built-in skills | `/config/.openclaw/skills/` | Yes |
| Agent sessions & data | `/config/.openclaw/agents/` | Yes |
| ClawHub workspace | `/config/clawd/` | Yes |
| User-installed npm skills | `/config/.node_global/` | Yes |
| User-installed npm skills | `/config/.node_global/` | Optional (`persist_node_global=true`) |
| SSH keys | `/config/keys/` | Yes |
| Tokens | `/config/secrets/` | Yes |
| Homebrew & brew-installed tools | `/config/.linuxbrew/` | Yes (synced on startup) |
| Homebrew & brew-installed tools | `/config/.linuxbrew/` | Optional (`persist_brew_tools=true`) |
| gog OAuth credentials | `/config/gogcli/` | Yes |
| TLS certificates (lan_https) | `/config/certs/` | Yes (CA persists; server cert regenerated if IP changes) |
| OpenClaw binary | `/usr/lib/node_modules/openclaw/` | **No** — reinstalled from image |
@@ -667,19 +671,21 @@ This means built-in skills survive image rebuilds, and any customizations you ma
### How user-installed skills work
When you install a skill via the OpenClaw dashboard or `npm install -g`, the add-on redirects global npm installs to `/config/.node_global/`. This directory persists across updates.
By default, user-installed npm skills and global packages are **ephemeral** to keep Home Assistant backups small.
The add-on also configures `pnpm` global directory to persistent storage at `/config/.node_global/pnpm/`.
If you enable `persist_node_global`, the add-on redirects global npm installs to `/config/.node_global/` so dashboard-installed skills survive add-on rebuilds. The add-on also configures the pnpm global directory at `/config/.node_global/pnpm/` in that mode.
### Homebrew-installed tools
Homebrew (Linuxbrew) and all brew-installed CLI tools (e.g., `gemini`, `aider`, `gh`, `bw`) are now **persisted** across add-on updates. On each startup, the add-on:
By default, Homebrew (Linuxbrew) and brew-installed CLI tools are **ephemeral** to keep Home Assistant backups small.
If you enable `persist_brew_tools`, the add-on:
1. Syncs the image's Homebrew install to `/config/.linuxbrew/`
2. Creates a symlink from `/home/linuxbrew/.linuxbrew/` to the persistent copy
3. On subsequent boots, only newer files are synced (user-installed packages are preserved)
This means `brew install` packages survive image rebuilds.
This means `brew install` packages survive image rebuilds only when persistence is enabled.
---
@@ -705,6 +711,7 @@ The add-on image includes these tools, available in the terminal:
| Chromium | `chromium` | Headless browser for automation |
| SSH | `ssh` | Remote access |
| oc-cleanup | `oc-cleanup` | Interactive disk space monitor & cache cleanup helper |
| oc-gateway | `oc-gateway status` / `oc-gateway restart` | Add-on-native gateway status/restart helper (`run.sh` supervised, no systemd) |
### oc-cleanup
@@ -740,7 +747,7 @@ Home Assistant checks for add-on updates automatically. When an update is availa
**What happens during an update**:
- The container is destroyed and recreated from the new image
- Everything under `/config/` is preserved (config, skills, workspace, keys)
- Homebrew and brew-installed packages are preserved (synced to `/config/.linuxbrew/`)
- Homebrew and npm global packages are preserved only if you explicitly enable `persist_brew_tools` / `persist_node_global`
- The OpenClaw binary is updated to the version in the new image
### Checking your version
@@ -753,7 +760,20 @@ openclaw --version
### Backup
Home Assistant's built-in backup system automatically includes add-on configuration data (`/config/`). This covers all persistent data: OpenClaw config, skills, workspace, keys, and tokens.
Home Assistant's built-in backup system automatically includes add-on configuration data (`/config/`). By default this covers the important user state: OpenClaw config, skills, workspace, keys, and tokens — without large optional toolchains.
### Backup-friendly defaults (v0.5.75+)
Starting with v0.5.75, the add-on keeps large optional toolchains out of backups by default:
- `persist_node_global: false` → `/config/.node_global/` is not used unless you opt in
- `persist_brew_tools: false` → `/config/.linuxbrew/` is not used unless you opt in
Turn these on only if you specifically want user-installed npm global skills or brew-installed CLI tools to survive add-on rebuilds.
### Migration note for older installs
If you used an older add-on version, you may already have legacy directories such as `/config/.node_global/` or `/config/.linuxbrew/` from previous persistent behavior. Disabling persistence stops future growth, but those directories still count toward backup size until you remove or archive them manually.
**To create a backup**: Go to **Settings → System → Backups → Create Backup**
@@ -762,7 +782,8 @@ Home Assistant's built-in backup system automatically includes add-on configurat
# Key paths to back up:
# /config/.openclaw/ - OpenClaw config, skills, agent data
# /config/clawd/ - ClawHub workspace
# /config/.node_global/ - User-installed npm skills
# /config/.node_global/ - User-installed npm skills (only if persist_node_global=true)
# /config/.linuxbrew/ - Homebrew tools (only if persist_brew_tools=true)
# /config/keys/ - SSH keys
# /config/secrets/ - Tokens
```
@@ -772,12 +793,12 @@ Home Assistant's built-in backup system automatically includes add-on configurat
To reset the add-on to a clean state, remove the persistent data:
```sh
rm -rf /config/.openclaw /config/clawd /config/.node_global
rm -rf /config/.openclaw /config/clawd /config/.node_global /config/.linuxbrew
```
Then restart the add-on. It will re-bootstrap a fresh configuration.
> **Warning**: This deletes all your OpenClaw configuration, skills, and workspace data. Back up first if needed.
> **Warning**: This deletes all your OpenClaw configuration, skills, workspace data, and any optionally persisted tool installations. Back up first if needed.
---
@@ -806,11 +827,23 @@ Go to **Settings → Add-ons → OpenClaw Assistant → Log** tab. Logs show sta
**Symptom**: Browser shows connection refused when opening the Gateway Web UI.
**Checks**:
1. Is the gateway running? In the terminal: `openclaw gateway status`
1. Is the gateway running? In the terminal: `oc-gateway status`
2. Is the bind mode correct? `openclaw config get gateway.bind` — must be `lan` for direct LAN access, or `loopback` if using `lan_https` mode
3. Is the port correct? `openclaw config get gateway.port`
4. Is the firewall blocking the port? Check your HA host firewall rules
### Gateway restart loop: `web_search provider is not available: brave`
**Symptom**: Logs repeat `Invalid config at /config/.openclaw/openclaw.json` and `tools.web.search.provider: web_search provider is not available: brave`.
**Cause**: The persisted OpenClaw config selects the Brave web search provider, but that provider plugin is not currently installed or enabled in the add-on runtime.
**Fix**: In v0.5.72+ the add-on clears that unavailable provider automatically during startup. On older versions, run this in the add-on terminal, then restart:
```sh
jq 'del(.tools.web.search.provider)' /config/.openclaw/openclaw.json > /tmp/openclaw.json && mv /tmp/openclaw.json /config/.openclaw/openclaw.json
```
### "disconnected (1008): control ui requires device identity" / "requires HTTPS or localhost"
**Symptom**: Gateway UI shows error 1008 or "requires secure context / device identity".
@@ -860,7 +893,7 @@ Go to **Settings → Add-ons → OpenClaw Assistant → Log** tab. Logs show sta
"allowedOrigins": ["https://YOUR_IP:18789"]
}
```
Then restart the gateway: `openclaw gateway restart`
Then restart only the gateway process: `oc-gateway restart`
3. Alternatively, approve devices individually without disabling auth:
```sh
openclaw devices list # show pending pairing requests
@@ -934,7 +967,7 @@ Built-in skills are synced to persistent storage on each startup. If skills are
1. Check logs for `INFO: Synced built-in skills to persistent storage` — this confirms the sync ran
2. If you see `WARN: Built-in skills directory not found`, the OpenClaw installation may be corrupted. Try reinstalling the add-on.
3. User-installed skills (via dashboard) are stored in `/config/.node_global/` and should survive updates
3. User-installed skills (via dashboard) survive updates only when `persist_node_global` is enabled. With the default backup-friendly setting (`false`), they are ephemeral and may need to be reinstalled after an add-on rebuild.
### Homebrew errors / CPU compatibility
+112 -2
View File
@@ -2,12 +2,122 @@
All notable changes to the OpenClaw Assistant Home Assistant Add-on will be documented in this file.
## [0.5.59] - 2026-03-10
## [0.5.82] - 2026-07-15
### Fixed
- **Gateway restart loop** (issue #95): when the agent or user ran `openclaw gateway restart`, the supervisor loop detected the old PID exiting and immediately spawned a second gateway instance, which collided with the already-restarted one and looped with "another gateway instance is already listening". The supervisor now detects a self-restart (new PID already running on the same port) and re-tracks it instead of spawning a duplicate.
- Repair add-on startup automatically when the bundled OpenClaw CLI is older than the persisted `/config/.openclaw/openclaw.json` format version. On mismatch, the add-on now restores the newer runtime before launching the gateway instead of silently coming up broken after a Home Assistant OS update or add-on rebuild.
- Regenerate malformed `lan_https` CA/server certificates with proper X.509 extensions (`basicConstraints`, `keyUsage`, `extendedKeyUsage`) so Python/OpenSSL strict verification accepts the built-in HTTPS proxy certificates.
## [0.5.81] - 2026-07-14
### Changed
- Bump OpenClaw to `2026.7.1`.
## [0.5.80] - 2026-06-26
### Changed
- Bump OpenClaw to `2026.6.10`.
## [0.5.78] - 2026-06-16
### Changed
- Bump OpenClaw through the `2026.5.28` and `2026.6.6` upstream releases.
## [0.5.76] - 2026-05-29
### Changed
- Bump OpenClaw to `2026.5.27`.
## [0.5.75] - 2026-05-28
### Changed
- **Backup-friendly persistence defaults**: new add-on options `persist_node_global` and `persist_brew_tools`, both defaulting to `false` so large optional toolchains are no longer persisted into Home Assistant backups unless users explicitly opt in.
- `run.sh` now keeps npm global installs and Homebrew ephemeral by default, while preserving the old rebuild-survival behavior when the new toggles are enabled.
### Added
- Migration notes and documentation for older installs that already have legacy `/config/.node_global/` or `/config/.linuxbrew/` directories contributing to backup size.
## [0.5.74] - 2026-05-27
### Fixed
- Bundle `node-llama-cpp` inside the add-on image so the default local memory/embeddings provider works in HAOS without manual package installs.
- Add `cmake` to the image so `node-llama-cpp` can fall back to a source build when a prebuilt binary is unavailable for the target architecture.
## [0.5.73] - 2026-05-26
### Added
- New add-on-native `oc-gateway` helper for container-supervised runtime management:
- `oc-gateway status` shows gateway state in the HA add-on model (`run.sh` supervisor, not systemd)
- `oc-gateway restart` requests gateway self-restart via `SIGUSR1` without full add-on restart
### Changed
- Troubleshooting and setup docs now use `oc-gateway status` / `oc-gateway restart` in add-on contexts to avoid confusing systemd-related CLI output.
## [0.5.72] - 2026-05-04
### Fixed
- Repair startup when a persisted OpenClaw config still selects the unavailable `tools.web.search.provider=brave` provider. The add-on now clears that provider before launching the gateway so OpenClaw can start; users can reinstall/enable the Brave provider later if they want web search through Brave.
## [0.5.71] - 2026-05-03
### Changed
- Bump OpenClaw through the 2026.4.29 and 2026.5.2 upstream releases.
## [0.5.70] - 2026-04-30
### Changed
- Bump OpenClaw to 2026.4.27.
## [0.5.69] - 2026-04-27
### Changed
- Bump OpenClaw through the 2026.4.23 and 2026.4.24 upstream releases.
## [0.5.68] - 2026-04-25
### Changed
- Bump OpenClaw through the 2026.4.14, 2026.4.15, 2026.4.21, and 2026.4.22 upstream releases.
## [0.5.67] - 2026-04-25
### Changed
- Bump OpenClaw through the 2026.4.5, 2026.4.8, 2026.4.9, 2026.4.10, 2026.4.11, and 2026.4.12 upstream releases.
## [0.5.66] - 2026-04-04
### Fixed
- **"Open Gateway Web UI" button missing token on first boot / post-onboard** (issue #102): the gateway token was read once at startup, before `openclaw onboard` had a chance to write `openclaw.json`. The landing page now re-renders automatically in the background (up to ~2 min after startup) once the token appears in `openclaw.json`, and nginx is reloaded with SIGHUP — no add-on restart required. Existing installs with a token already present are unaffected.
## [0.5.65] - 2026-04-04
### Changed
- Bump OpenClaw to 2026.4.2.
## [0.5.63] - 2026-03-14
### Changed
- Bump OpenClaw to 2026.3.13.
## [0.5.62] - 2026-03-10
### Fixed
- **Gateway restart loop** (issue #95): `openclaw gateway run` is a thin wrapper that spawns `openclaw-gateway` as a long-running daemon then exits immediately. On self-restart (SIGUSR1 / `openclaw gateway restart`), the old daemon forks a new one and exits — the new PID is not a child of run.sh. The supervisor now uses a 3-tier daemon detection function (`find_gateway_daemon_pid`): (1) port ownership via `ss -tlnp`, (2) process title via `pgrep -f "openclaw-gateway"`, (3) `/proc/*/cmdline` scan for "openclaw" (catches the daemon immediately after fork, even before process.title or port bind — critical on Pi/eMMC where initialization takes 20-30 s). Detection retries up to 10 times with a final port-occupancy guard before any supervisor-initiated restart. Non-child PIDs are monitored with `kill -0` polling instead of `wait`. The loopback relay (tailnet mode) is stopped/restarted around gateway restarts to prevent port conflicts.
## [0.5.61] - 2026-03-10
### Fixed
- **Gateway restart loop** (issue #95): stop the tailnet loopback relay before supervisor-initiated gateway restarts and start it again after the new daemon is launched, preventing the relay from holding the local port and trapping the add-on in an `already listening` restart loop.
## [0.5.60] - 2026-03-10
### Fixed
- **Session lock cleanup ignored non-default agents**: `cleanup_session_locks` was hardcoded to `agents/main/sessions`, skipping stale locks for any agent with a custom `forcedAgentId`. Stale locks could block the gateway from opening sessions for those agents, causing silent fallback to `main`. Cleanup now scans all `agents/*/sessions/` directories.
## [0.5.59] - 2026-03-10
- **Remote mode URL not propagated** (issue #93): `start_openclaw_runtime` was reading `gateway.remote.url` back via `openclaw config get`, which can time out (2 s limit at startup) or return an empty/redacted result. The function now uses `$GATEWAY_REMOTE_URL` directly from the already-parsed add-on options, which is the same value the config helper writes to `openclaw.json`.
- **Terminal CLI unreachable in tailnet mode** (issue #90): when `gateway_bind_mode=tailnet` (or `access_mode=tailnet_https`), the gateway binds only to the Tailscale IP. The local CLI always connects via `ws://127.0.0.1:PORT`, causing "Gateway not running" inside the add-on terminal. A lightweight loopback relay (Node.js) is now started automatically to forward `127.0.0.1:PORT → TAILSCALE_IP:PORT`, making all terminal CLI commands work normally. Token auth is still enforced end-to-end by the gateway.
- **Session lock cleanup ignored non-default agents**: `cleanup_session_locks` was hardcoded to `agents/main/sessions`, skipping stale locks for any agent with a custom `forcedAgentId`. Stale locks could block the gateway from opening sessions for those agents, causing silent fallback to `main`. Cleanup now scans all `agents/*/sessions/` directories.
### Added
- **MCP auto-configuration for Home Assistant**: new option `auto_configure_mcp` (default: `false`). When enabled and `homeassistant_token` is set, the add-on automatically registers Home Assistant as an MCP server (`mcporter config add HA ...`) on startup. Auto-detects the HA API URL (supervisor proxy or localhost:8123). Re-configures only when the token changes.
+13 -7
View File
@@ -4,7 +4,7 @@ FROM ${BUILD_FROM}
# Base image is Debian Bookworm (glibc). This avoids musl-related native module issues
# that occur on Alpine (e.g. clipboard, node-llama-cpp).
# Install base packages (without nodejs/npm - we'll get Node 22 from NodeSource)
# Install base packages (without nodejs/npm - we'll get Node 24 from NodeSource)
# build-essential provides gcc/cc needed by Homebrew for OpenClaw's brew-managed dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
bash \
@@ -18,6 +18,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
xz-utils \
file \
python3 \
cmake \
nginx \
gnupg \
build-essential \
@@ -33,19 +34,21 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Install Node.js 22 LTS from NodeSource (Debian Bookworm ships Node 18, but OpenClaw requires 20+)
# Use explicit keyring + apt source instead of setup_22.x pipe script for deterministic builds.
# Install Node.js 24 LTS from NodeSource.
# OpenClaw supports Node >=24.15.0, and this keeps the image compatible with the
# current mcporter releases without relying on an older pinned CLI line.
# Use explicit keyring + apt source instead of setup_24.x pipe script for deterministic builds.
RUN mkdir -p /etc/apt/keyrings \
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \
| gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
&& chmod 644 /etc/apt/keyrings/nodesource.gpg \
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" \
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_24.x nodistro main" \
> /etc/apt/sources.list.d/nodesource.list \
&& apt-get update \
&& apt-get install -y --no-install-recommends nodejs \
&& NODE_VERSION="$(node -v)" \
&& echo "$NODE_VERSION" \
&& echo "$NODE_VERSION" | grep -E '^v22\.' \
&& echo "$NODE_VERSION" | grep -E '^v24\.' \
&& rm -f /etc/apt/sources.list.d/nodesource.list /etc/apt/keyrings/nodesource.gpg \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
@@ -113,8 +116,10 @@ RUN if [ -x /home/linuxbrew/.linuxbrew/bin/brew ]; then \
USER root
# Install OpenClaw globally
# Bundle node-llama-cpp in the image so the default local memory/embeddings
# provider works in HAOS without requiring manual npm installs in /usr/lib.
RUN npm config set fund false && npm config set audit false \
&& npm install -g openclaw@2026.3.8
&& npm install -g openclaw@2026.7.1 node-llama-cpp@3.18.1
# Shell aliases and color options for interactive use
RUN tee -a /etc/bash.bashrc <<'EOF'
@@ -138,10 +143,11 @@ COPY run.sh /run.sh
COPY oc_config_helper.py /oc_config_helper.py
COPY render_nginx.py /render_nginx.py
COPY oc-cleanup.sh /usr/local/bin/oc-cleanup
COPY oc-gateway /usr/local/bin/oc-gateway
COPY openclaw-proxy-shim.cjs /usr/local/lib/openclaw-proxy-shim.cjs
COPY nginx.conf.tpl /etc/nginx/nginx.conf.tpl
COPY landing.html.tpl /etc/nginx/landing.html.tpl
RUN chmod +x /run.sh /oc_config_helper.py /render_nginx.py /usr/local/bin/oc-cleanup \
RUN chmod +x /run.sh /oc_config_helper.py /render_nginx.py /usr/local/bin/oc-cleanup /usr/local/bin/oc-gateway \
&& mkdir -p /run/nginx
CMD [ "/run.sh" ]
+8 -1
View File
@@ -1,5 +1,5 @@
name: OpenClaw Assistant
version: "0.5.59"
version: "0.5.82"
slug: openclaw_assistant
description: Run OpenClaw Assistant (OpenClaw-compatible) as a Home Assistant add-on.
url: https://github.com/techartdev/OpenClawHomeAssistant
@@ -56,6 +56,11 @@ options:
clean_session_locks_on_start: true
clean_session_locks_on_exit: true
# Persist heavy optional tooling across add-on rebuilds.
# Disabled by default to keep Home Assistant backups small.
persist_node_global: false
persist_brew_tools: false
# Gateway mode:
# - local: Run gateway locally (recommended for most users)
# - remote: Connect to a remote gateway
@@ -153,6 +158,8 @@ schema:
clean_session_locks_on_start: bool?
clean_session_locks_on_exit: bool?
persist_node_global: bool?
persist_brew_tools: bool?
gateway_mode: list(local|remote)?
gateway_remote_url: str?
gateway_bind_mode: list(loopback|lan|tailnet)?
+133
View File
@@ -0,0 +1,133 @@
#!/usr/bin/env bash
set -euo pipefail
cmd="${1:-status}"
shift || true
find_gateway_pid() {
local port="${1:-18789}"
local pid=""
# 1) Port owner (most accurate)
if command -v ss >/dev/null 2>&1; then
pid=$(ss -tlnp 2>/dev/null \
| grep ":${port} " \
| sed -n 's/.*pid=\([0-9]*\).*/\1/p' \
| head -1 || true)
if [ -n "$pid" ] && [ -r "/proc/$pid/cmdline" ]; then
local cmdline
cmdline=$(tr '\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null || true)
if echo "$cmdline" | grep -qi "openclaw\|node"; then
echo "$pid"
return 0
fi
fi
fi
# 2) Process title
pid=$(pgrep -f "openclaw-gateway" 2>/dev/null | head -1 || true)
if [ -n "$pid" ]; then
echo "$pid"
return 0
fi
# 3) /proc cmdline fallback
for f in /proc/[0-9]*/cmdline; do
[ -r "$f" ] || continue
if tr '\0' ' ' < "$f" 2>/dev/null | grep -qi "openclaw"; then
echo "${f#/proc/}" | cut -d/ -f1
return 0
fi
done
return 1
}
get_gateway_port() {
python3 - <<'PY'
import json, os
p = os.environ.get('OPENCLAW_CONFIG_PATH', '/config/.openclaw/openclaw.json')
try:
with open(p, 'r', encoding='utf-8') as f:
cfg = json.load(f)
print(int(cfg.get('gateway', {}).get('port', 18789)))
except Exception:
print(18789)
PY
}
status_cmd() {
local port pid
port="$(get_gateway_port)"
pid="$(find_gateway_pid "$port" || true)"
echo "OpenClaw add-on gateway status"
echo "Supervisor: run.sh (not systemd)"
echo "Configured port: ${port}"
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
echo "Gateway process: running (PID ${pid})"
if command -v ss >/dev/null 2>&1 && ss -tlnp 2>/dev/null | grep -q ":${port} "; then
echo "Listener: active on :${port}"
else
echo "Listener: process found, port bind not detected yet"
fi
exit 0
fi
echo "Gateway process: not detected"
echo "Tip: Check add-on logs in Home Assistant for startup errors."
exit 1
}
restart_cmd() {
local port pid
port="$(get_gateway_port)"
pid="$(find_gateway_pid "$port" || true)"
if [ -z "$pid" ] || ! kill -0 "$pid" 2>/dev/null; then
echo "No running gateway process found to restart."
echo "Tip: If startup failed, inspect add-on logs."
exit 1
fi
echo "Requesting gateway self-restart via SIGUSR1 (PID ${pid})..."
kill -USR1 "$pid"
for _ in $(seq 1 20); do
sleep 1
local new_pid
new_pid="$(find_gateway_pid "$port" || true)"
if [ -n "$new_pid" ] && kill -0 "$new_pid" 2>/dev/null; then
echo "Gateway active (PID ${new_pid})"
exit 0
fi
done
echo "Restart signal sent; gateway may still be reinitializing."
echo "Run 'oc-gateway status' again in a few seconds."
exit 0
}
case "$cmd" in
status)
status_cmd
;;
restart|reload)
restart_cmd
;;
help|-h|--help)
cat <<'EOF'
Usage: oc-gateway <status|restart|reload>
status Show add-on-native gateway status (run.sh-supervised)
restart Request gateway self-restart via SIGUSR1
reload Alias of restart
EOF
;;
*)
echo "Unknown command: $cmd" >&2
echo "Run: oc-gateway help" >&2
exit 2
;;
esac
+44
View File
@@ -256,6 +256,43 @@ def set_control_ui_origins(origins_csv: str, additional_origins_csv: str = "", d
return False
def repair_known_invalid_settings():
"""Repair known config values that prevent OpenClaw from starting."""
cfg = read_config()
if cfg is None:
return True
tools = cfg.get("tools")
if not isinstance(tools, dict):
return True
web = tools.get("web")
if not isinstance(web, dict):
return True
search = web.get("search")
if not isinstance(search, dict):
return True
provider = search.get("provider")
changes = []
if provider == "brave":
del search["provider"]
changes.append("removed unavailable tools.web.search.provider=brave")
if not changes:
print("INFO: No known invalid OpenClaw config settings found")
return True
if write_config(cfg):
print(f"INFO: Repaired OpenClaw config: {', '.join(changes)}")
return True
print("ERROR: Failed to write config")
return False
def main():
"""CLI entry point for use by run.sh"""
if len(sys.argv) < 2:
@@ -300,6 +337,13 @@ def main():
success = set_control_ui_origins(origins_csv, additional_origins_csv, disable_device_auth)
sys.exit(0 if success else 1)
elif cmd == "repair-known-invalid-settings":
if len(sys.argv) != 2:
print("Usage: oc_config_helper.py repair-known-invalid-settings")
sys.exit(1)
success = repair_known_invalid_settings()
sys.exit(0 if success else 1)
elif cmd == "set":
if len(sys.argv) != 4:
print("Usage: oc_config_helper.py set <key> <value>")
+475 -124
View File
@@ -44,6 +44,8 @@ ROUTER_KEY=$(jq -r '.router_ssh_key_path // "/data/keys/router_ssh"' "$OPTIONS_F
# Optional: allow disabling lock cleanup if you ever need to debug
CLEAN_LOCKS_ON_START=$(jq -r '.clean_session_locks_on_start // true' "$OPTIONS_FILE")
CLEAN_LOCKS_ON_EXIT=$(jq -r '.clean_session_locks_on_exit // true' "$OPTIONS_FILE")
PERSIST_NODE_GLOBAL=$(jq -r '.persist_node_global // false' "$OPTIONS_FILE")
PERSIST_BREW_TOOLS=$(jq -r '.persist_brew_tools // false' "$OPTIONS_FILE")
# Gateway configuration
GATEWAY_MODE=$(jq -r '.gateway_mode // "local"' "$OPTIONS_FILE")
@@ -149,6 +151,15 @@ export XDG_CONFIG_HOME=/config
mkdir -p /config/.openclaw /config/.openclaw/identity /config/clawd /config/keys /config/secrets
warn_legacy_persistent_dir() {
local path="$1"
local label="$2"
if [ -e "$path" ]; then
echo "WARN: Found legacy persistent ${label} at ${path}, but persistence is disabled."
echo "WARN: It will still inflate Home Assistant backups until you remove or archive it manually."
fi
}
# ------------------------------------------------------------------------------
# Sync built-in OpenClaw skills from image to persistent storage
# On each startup, copy new/updated built-in skills so they survive rebuilds.
@@ -180,21 +191,33 @@ else
fi
# ------------------------------------------------------------------------------
# Persist user-installed node skills across Docker image rebuilds
# Redirect npm/pnpm global installs to /config/.node_global (persistent storage)
# so that skills installed via the dashboard survive container rebuilds.
# Optional persistence for user-installed node skills across Docker image rebuilds.
# When enabled, redirect npm/pnpm global installs to /config/.node_global so
# dashboard-installed skills survive image updates. Disabled by default to keep
# Home Assistant backups smaller.
# NOTE: This MUST come after the skills sync above (which needs the original npm root -g).
# ------------------------------------------------------------------------------
PERSISTENT_NODE_GLOBAL="/config/.node_global"
mkdir -p "$PERSISTENT_NODE_GLOBAL"
npm config set prefix "$PERSISTENT_NODE_GLOBAL" 2>/dev/null || true
export PATH="${PERSISTENT_NODE_GLOBAL}/bin:${PATH}"
export NODE_PATH="${PERSISTENT_NODE_GLOBAL}/lib/node_modules:${NODE_PATH:-}"
if [ "$PERSIST_NODE_GLOBAL" = "true" ] || [ "$PERSIST_NODE_GLOBAL" = "1" ]; then
mkdir -p "$PERSISTENT_NODE_GLOBAL"
npm config set prefix "$PERSISTENT_NODE_GLOBAL" 2>/dev/null || true
export PATH="${PERSISTENT_NODE_GLOBAL}/bin:${PATH}"
export NODE_PATH="${PERSISTENT_NODE_GLOBAL}/lib/node_modules:${NODE_PATH:-}"
# Also configure pnpm global dir to persistent storage
export PNPM_HOME="${PERSISTENT_NODE_GLOBAL}/pnpm"
mkdir -p "$PNPM_HOME"
export PATH="${PNPM_HOME}:${PATH}"
# Also configure pnpm global dir to persistent storage
export PNPM_HOME="${PERSISTENT_NODE_GLOBAL}/pnpm"
mkdir -p "$PNPM_HOME"
export PATH="${PNPM_HOME}:${PATH}"
echo "INFO: persist_node_global=true; user-installed npm skills will survive add-on rebuilds."
else
npm config delete prefix 2>/dev/null || true
export npm_config_prefix="/usr/local"
export PNPM_HOME="/tmp/.pnpm-home"
mkdir -p "$PNPM_HOME"
export PATH="${PNPM_HOME}:${PATH}"
warn_legacy_persistent_dir "$PERSISTENT_NODE_GLOBAL" "node global tool/skill data"
echo "INFO: persist_node_global=false; npm/pnpm global installs are ephemeral and excluded from HA backups."
fi
# Protect critical runtime variables from accidental override via gateway_env_vars.
is_reserved_gateway_env_var() {
@@ -331,42 +354,48 @@ elif [ "$GW_ENV_VARS_TYPE" != "null" ]; then
fi
# ------------------------------------------------------------------------------
# Persist Linuxbrew/Homebrew across Docker image rebuilds
# Homebrew installs to /home/linuxbrew/.linuxbrew/ which is ephemeral.
# We sync it to /config/.linuxbrew and symlink back so brew-installed CLI
# tools (gog, gh, bw, etc.) survive add-on updates.
# Optional persistence for Linuxbrew/Homebrew across Docker image rebuilds.
# When enabled, sync /home/linuxbrew/.linuxbrew to /config/.linuxbrew so
# brew-installed CLI tools survive image updates. Disabled by default to keep
# Home Assistant backups smaller.
# ------------------------------------------------------------------------------
IMAGE_BREW_DIR="/home/linuxbrew/.linuxbrew"
PERSISTENT_BREW_DIR="/config/.linuxbrew"
if [ -d "$IMAGE_BREW_DIR" ] && [ ! -L "$IMAGE_BREW_DIR" ]; then
# Image has a real Homebrew install — sync to persistent storage
if [ -d "$PERSISTENT_BREW_DIR" ]; then
# Persistent copy exists: sync new/updated files from image (upgrades),
# but preserve user-installed packages already in persistent storage.
if command -v rsync >/dev/null 2>&1; then
rsync -a --update "$IMAGE_BREW_DIR/" "$PERSISTENT_BREW_DIR/" 2>/dev/null || true
if [ "$PERSIST_BREW_TOOLS" = "true" ] || [ "$PERSIST_BREW_TOOLS" = "1" ]; then
if [ -d "$IMAGE_BREW_DIR" ] && [ ! -L "$IMAGE_BREW_DIR" ]; then
# Image has a real Homebrew install — sync to persistent storage
if [ -d "$PERSISTENT_BREW_DIR" ]; then
# Persistent copy exists: sync new/updated files from image (upgrades),
# but preserve user-installed packages already in persistent storage.
if command -v rsync >/dev/null 2>&1; then
rsync -a --update "$IMAGE_BREW_DIR/" "$PERSISTENT_BREW_DIR/" 2>/dev/null || true
else
cp -ru "$IMAGE_BREW_DIR/"* "$PERSISTENT_BREW_DIR/" 2>/dev/null || true
fi
echo "INFO: Synced Homebrew updates to persistent storage"
else
cp -ru "$IMAGE_BREW_DIR/"* "$PERSISTENT_BREW_DIR/" 2>/dev/null || true
# First time: copy entire Homebrew install to persistent storage
cp -a "$IMAGE_BREW_DIR" "$PERSISTENT_BREW_DIR" 2>/dev/null || true
echo "INFO: Copied Homebrew to persistent storage at $PERSISTENT_BREW_DIR"
fi
echo "INFO: Synced Homebrew updates to persistent storage"
# Replace image dir with symlink to persistent copy
rm -rf "$IMAGE_BREW_DIR"
ln -sf "$PERSISTENT_BREW_DIR" "$IMAGE_BREW_DIR"
elif [ -L "$IMAGE_BREW_DIR" ]; then
echo "INFO: Homebrew already linked to persistent storage"
elif [ -d "$PERSISTENT_BREW_DIR" ]; then
# Image doesn't have Homebrew (failed install?) but persistent copy exists
mkdir -p "$(dirname "$IMAGE_BREW_DIR")"
ln -sf "$PERSISTENT_BREW_DIR" "$IMAGE_BREW_DIR"
echo "INFO: Restored Homebrew symlink from persistent storage"
else
# First time: copy entire Homebrew install to persistent storage
cp -a "$IMAGE_BREW_DIR" "$PERSISTENT_BREW_DIR" 2>/dev/null || true
echo "INFO: Copied Homebrew to persistent storage at $PERSISTENT_BREW_DIR"
echo "INFO: Homebrew not available (install may have failed during image build)"
fi
# Replace image dir with symlink to persistent copy
rm -rf "$IMAGE_BREW_DIR"
ln -sf "$PERSISTENT_BREW_DIR" "$IMAGE_BREW_DIR"
elif [ -L "$IMAGE_BREW_DIR" ]; then
echo "INFO: Homebrew already linked to persistent storage"
elif [ -d "$PERSISTENT_BREW_DIR" ]; then
# Image doesn't have Homebrew (failed install?) but persistent copy exists
mkdir -p "$(dirname "$IMAGE_BREW_DIR")"
ln -sf "$PERSISTENT_BREW_DIR" "$IMAGE_BREW_DIR"
echo "INFO: Restored Homebrew symlink from persistent storage"
echo "INFO: persist_brew_tools=true; brew-installed tools will survive add-on rebuilds."
else
echo "INFO: Homebrew not available (install may have failed during image build)"
warn_legacy_persistent_dir "$PERSISTENT_BREW_DIR" "Homebrew data"
echo "INFO: persist_brew_tools=false; Homebrew installs stay ephemeral and excluded from HA backups."
fi
# Back-compat: some docs/scripts assume /data; point it at /config.
@@ -374,8 +403,9 @@ if [ ! -e /data ]; then
ln -s /config /data || true
fi
# Ensure these exist so cleanup doesn't fail
mkdir -p /config/.openclaw/agents/main/sessions || true
# Ensure the agents base directory exists so cleanup scans work even before first run.
# Do NOT pre-create agent-specific directories; OpenClaw creates them as needed.
mkdir -p /config/.openclaw/agents || true
# ------------------------------------------------------------------------------
# SINGLE-INSTANCE GUARD (prevents multiple gateway runs racing each other)
@@ -393,30 +423,43 @@ fi
# ------------------------------------------------------------------------------
gateway_running() {
pgrep -f "openclaw.*gateway.*run" >/dev/null 2>&1
pgrep -f "openclaw-gateway" >/dev/null 2>&1
}
cleanup_session_locks() {
local sessions_dir="/config/.openclaw/agents/main/sessions"
local glob1="${sessions_dir}"/*.jsonl.lock
local agents_dir="/config/.openclaw/agents"
local total_locks=0
local cleaned_dirs=()
# Scan all agent session directories, not just 'main'.
# This is needed for users who have gateway.forcedAgentId set to a non-default agent.
shopt -s nullglob
local locks=( $glob1 )
local all_locks=()
for agent_sessions_dir in "${agents_dir}"/*/sessions; do
local agent_locks=( "${agent_sessions_dir}"/*.jsonl.lock )
if [ ${#agent_locks[@]} -gt 0 ]; then
all_locks+=( "${agent_locks[@]}" )
cleaned_dirs+=( "$agent_sessions_dir" )
total_locks=$(( total_locks + ${#agent_locks[@]} ))
fi
done
shopt -u nullglob
if [ ${#locks[@]} -eq 0 ]; then
if [ "$total_locks" -eq 0 ]; then
return 0
fi
# If gateway is running, do NOT remove locks automatically (could be real).
if gateway_running; then
echo "INFO: Gateway appears to be running; leaving session lock files untouched."
echo "INFO: Locks present: ${#locks[@]}"
echo "INFO: Locks present: $total_locks"
return 0
fi
echo "INFO: Removing stale session lock files (${#locks[@]}) from ${sessions_dir}"
rm -f "${sessions_dir}"/*.jsonl.lock || true
echo "INFO: Removing stale session lock files ($total_locks) across agents: ${cleaned_dirs[*]}"
for agent_sessions_dir in "${cleaned_dirs[@]}"; do
rm -f "${agent_sessions_dir}"/*.jsonl.lock || true
done
}
if [ "$CLEAN_LOCKS_ON_START" = "true" ]; then
@@ -475,13 +518,17 @@ shutdown() {
if [ -n "${GW_PID}" ] && kill -0 "${GW_PID}" >/dev/null 2>&1; then
kill -TERM "${GW_PID}" >/dev/null 2>&1 || true
wait "${GW_PID}" || true
# wait reaps child PIDs; for non-child (re-tracked) PIDs it fails instantly,
# so fall back to a timed kill -0 poll to let the gateway finish cleanly.
if ! wait "${GW_PID}" 2>/dev/null; then
for _i in 1 2 3 4 5; do
kill -0 "${GW_PID}" 2>/dev/null || break
sleep 1
done
fi
fi
if [ -n "${GW_RELAY_PID}" ] && kill -0 "${GW_RELAY_PID}" >/dev/null 2>&1; then
kill -TERM "${GW_RELAY_PID}" >/dev/null 2>&1 || true
wait "${GW_RELAY_PID}" || true
fi
stop_gw_relay
if [ "$CLEAN_LOCKS_ON_EXIT" = "true" ]; then
cleanup_session_locks || true
@@ -495,6 +542,69 @@ if ! command -v openclaw >/dev/null 2>&1; then
exit 1
fi
get_openclaw_version() {
local raw
raw="$(openclaw --version 2>/dev/null | head -n 1 || true)"
printf '%s\n' "$raw" | grep -oE '[0-9]{4}\.[0-9]+\.[0-9]+' | head -n 1 || true
}
version_is_less_than() {
local left="$1"
local right="$2"
[ -n "$left" ] && [ -n "$right" ] && [ "$left" != "$right" ] && \
[ "$(printf '%s\n%s\n' "$left" "$right" | sort -V | head -n 1)" = "$left" ]
}
repair_runtime_version_mismatch() {
local config_path="/config/.openclaw/openclaw.json"
local runtime_version persisted_version refreshed_version
if [ ! -f "$config_path" ]; then
return 0
fi
runtime_version="$(get_openclaw_version)"
persisted_version="$(python3 - "$config_path" <<'PY'
import json
import sys
from pathlib import Path
path = Path(sys.argv[1])
try:
data = json.loads(path.read_text(encoding="utf-8"))
except Exception:
print("", end="")
raise SystemExit(0)
value = data.get("meta", {}).get("lastTouchedVersion", "")
print(value if isinstance(value, str) else "", end="")
PY
)"
if [ -z "$runtime_version" ] || [ -z "$persisted_version" ]; then
return 0
fi
if ! version_is_less_than "$runtime_version" "$persisted_version"; then
return 0
fi
echo "WARN: Persisted OpenClaw config was last written by newer version $persisted_version, but bundled runtime is $runtime_version."
echo "WARN: Attempting one-time runtime repair so the gateway can start after add-on rebuilds or Home Assistant OS updates."
if npm install -g "openclaw@${persisted_version}" >/tmp/openclaw-runtime-repair.log 2>&1; then
refreshed_version="$(get_openclaw_version)"
echo "INFO: OpenClaw runtime repair succeeded (${runtime_version} -> ${refreshed_version:-$persisted_version})."
return 0
fi
echo "ERROR: Automatic runtime repair failed; startup will continue with bundled OpenClaw $runtime_version."
echo "ERROR: Review /tmp/openclaw-runtime-repair.log and rerun 'openclaw update' or 'npm install -g openclaw@${persisted_version}' once connectivity is available."
return 0
}
repair_runtime_version_mismatch
# Bootstrap minimal OpenClaw config ONLY if missing.
# We do not overwrite or patch existing configs; onboarding owns everything else.
OPENCLAW_CONFIG_PATH="/config/.openclaw/openclaw.json"
@@ -544,9 +654,20 @@ fi
if [ -f "$OPENCLAW_CONFIG_PATH" ]; then
if [ -f "$HELPER_PATH" ]; then
if python3 "$HELPER_PATH" repair-known-invalid-settings; then
:
else
rc=$?
echo "ERROR: Failed to repair known invalid OpenClaw config settings via oc_config_helper.py (exit code ${rc})."
echo "ERROR: Gateway configuration may be invalid; aborting startup."
exit "${rc}"
fi
# In lan_https mode the gateway uses an internal port; nginx owns the external one.
EFFECTIVE_GW_PORT="$GATEWAY_INTERNAL_PORT"
if ! python3 "$HELPER_PATH" apply-gateway-settings "$GATEWAY_MODE" "$GATEWAY_REMOTE_URL" "$GATEWAY_BIND_MODE" "$EFFECTIVE_GW_PORT" "$ENABLE_OPENAI_API" "$GATEWAY_AUTH_MODE" "$GATEWAY_TRUSTED_PROXIES"; then
if python3 "$HELPER_PATH" apply-gateway-settings "$GATEWAY_MODE" "$GATEWAY_REMOTE_URL" "$GATEWAY_BIND_MODE" "$EFFECTIVE_GW_PORT" "$ENABLE_OPENAI_API" "$GATEWAY_AUTH_MODE" "$GATEWAY_TRUSTED_PROXIES"; then
:
else
rc=$?
echo "ERROR: Failed to apply gateway settings via oc_config_helper.py (exit code ${rc})."
echo "ERROR: Gateway configuration may be incorrect; aborting startup."
@@ -577,6 +698,71 @@ if [ "$ENABLE_HTTPS_PROXY" = "true" ]; then
CERT_DIR="/config/certs"
mkdir -p "$CERT_DIR"
cert_ext_contains() {
local cert_path="$1"
local extension_name="$2"
local expected="$3"
openssl x509 -in "$cert_path" -noout -ext "$extension_name" 2>/dev/null | grep -Fq "$expected"
}
local_ca_cert_is_valid() {
local cert_path="$1"
[ -f "$cert_path" ] && \
cert_ext_contains "$cert_path" basicConstraints "CA:TRUE" && \
cert_ext_contains "$cert_path" keyUsage "Certificate Sign, CRL Sign"
}
gateway_server_cert_is_valid() {
local cert_path="$1"
[ -f "$cert_path" ] && \
cert_ext_contains "$cert_path" extendedKeyUsage "TLS Web Server Authentication" && \
cert_ext_contains "$cert_path" subjectAltName "DNS:localhost"
}
generate_local_ca_cert() {
cat > "$CERT_DIR/_ca.ext" <<'CAEOF'
[v3_ca]
basicConstraints=critical,CA:TRUE
keyUsage=critical,keyCertSign,cRLSign
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid:always,issuer
CAEOF
openssl genrsa -out "$CERT_DIR/ca.key" 2048 2>/dev/null
openssl req -new -key "$CERT_DIR/ca.key" -out "$CERT_DIR/ca.csr" \
-subj "/CN=OpenClaw Local CA" 2>/dev/null
openssl x509 -req -in "$CERT_DIR/ca.csr" -signkey "$CERT_DIR/ca.key" \
-out "$CERT_DIR/ca.crt" -days 3650 \
-extfile "$CERT_DIR/_ca.ext" -extensions v3_ca 2>/dev/null
rm -f "$CERT_DIR/ca.csr" "$CERT_DIR/_ca.ext"
chmod 600 "$CERT_DIR/ca.key"
}
generate_gateway_server_cert() {
openssl genrsa -out "$CERT_DIR/gateway.key" 2048 2>/dev/null
openssl req -new -key "$CERT_DIR/gateway.key" -out "$CERT_DIR/gateway.csr" \
-subj "/CN=OpenClaw Gateway" 2>/dev/null
cat > "$CERT_DIR/_server.ext" <<SERVER_EOF
[v3_server]
basicConstraints=critical,CA:FALSE
keyUsage=critical,digitalSignature,keyEncipherment
extendedKeyUsage=serverAuth
subjectAltName=IP:${LAN_IP:-127.0.0.1},IP:127.0.0.1,DNS:localhost,DNS:homeassistant,DNS:homeassistant.local${EXTRA_SANS:+,${EXTRA_SANS}}
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer
SERVER_EOF
openssl x509 -req -in "$CERT_DIR/gateway.csr" \
-CA "$CERT_DIR/ca.crt" -CAkey "$CERT_DIR/ca.key" -CAcreateserial \
-out "$CERT_DIR/gateway.crt" -days 3650 \
-extfile "$CERT_DIR/_server.ext" -extensions v3_server 2>/dev/null
rm -f "$CERT_DIR/gateway.csr" "$CERT_DIR/_server.ext" "$CERT_DIR/ca.srl"
chmod 600 "$CERT_DIR/gateway.key"
}
# Detect primary LAN IP
LAN_IP=$(hostname -I 2>/dev/null | awk '{print $1}')
STORED_IP=$(cat "$CERT_DIR/.cert_ip" 2>/dev/null || echo "")
@@ -584,12 +770,17 @@ if [ "$ENABLE_HTTPS_PROXY" = "true" ]; then
# --- Local CA (generated once, persists across restarts) ---
if [ ! -f "$CERT_DIR/ca.key" ] || [ ! -f "$CERT_DIR/ca.crt" ]; then
echo "INFO: Generating local CA certificate (one-time)..."
openssl genrsa -out "$CERT_DIR/ca.key" 2048 2>/dev/null
openssl req -new -x509 -key "$CERT_DIR/ca.key" -out "$CERT_DIR/ca.crt" \
-days 3650 -nodes -subj "/CN=OpenClaw Local CA" 2>/dev/null
chmod 600 "$CERT_DIR/ca.key"
generate_local_ca_cert
STORED_IP="" # force server cert regeneration
echo "INFO: Local CA created at $CERT_DIR/ca.crt"
elif ! local_ca_cert_is_valid "$CERT_DIR/ca.crt"; then
echo "WARN: Existing local CA certificate is missing required X.509 CA extensions."
echo "WARN: Regenerating CA/server certificates for OpenSSL and Python strict verification compatibility."
echo "WARN: Devices that trusted the old CA need the new /cert/ca.crt installed again."
rm -f "$CERT_DIR/ca.key" "$CERT_DIR/ca.crt" "$CERT_DIR/gateway.key" "$CERT_DIR/gateway.crt"
generate_local_ca_cert
STORED_IP=""
echo "INFO: Local CA rotated at $CERT_DIR/ca.crt"
fi
# --- Extra SANs from gateway_additional_allowed_origins + gateway_public_url ---
@@ -623,24 +814,9 @@ PY
STORED_EXTRA_SANS=$(cat "$CERT_DIR/.cert_extra_sans" 2>/dev/null || echo "")
# --- Server cert (regenerated when LAN IP or SANs change) ---
if [ ! -f "$CERT_DIR/gateway.crt" ] || [ ! -f "$CERT_DIR/gateway.key" ] || [ "$LAN_IP" != "$STORED_IP" ] || [ "$EXTRA_SANS" != "$STORED_EXTRA_SANS" ]; then
if [ ! -f "$CERT_DIR/gateway.crt" ] || [ ! -f "$CERT_DIR/gateway.key" ] || [ "$LAN_IP" != "$STORED_IP" ] || [ "$EXTRA_SANS" != "$STORED_EXTRA_SANS" ] || ! gateway_server_cert_is_valid "$CERT_DIR/gateway.crt"; then
echo "INFO: Generating server TLS certificate for IP: ${LAN_IP:-unknown}..."
openssl genrsa -out "$CERT_DIR/gateway.key" 2048 2>/dev/null
openssl req -new -key "$CERT_DIR/gateway.key" -out "$CERT_DIR/gateway.csr" \
-subj "/CN=OpenClaw Gateway" 2>/dev/null
# SAN extension — include LAN IP, loopback, common mDNS names + user extras
cat > "$CERT_DIR/_san.ext" <<SANEOF
subjectAltName=IP:${LAN_IP:-127.0.0.1},IP:127.0.0.1,DNS:localhost,DNS:homeassistant,DNS:homeassistant.local${EXTRA_SANS:+,${EXTRA_SANS}}
SANEOF
openssl x509 -req -in "$CERT_DIR/gateway.csr" \
-CA "$CERT_DIR/ca.crt" -CAkey "$CERT_DIR/ca.key" -CAcreateserial \
-out "$CERT_DIR/gateway.crt" -days 3650 \
-extfile "$CERT_DIR/_san.ext" 2>/dev/null
rm -f "$CERT_DIR/gateway.csr" "$CERT_DIR/_san.ext" "$CERT_DIR/ca.srl"
chmod 600 "$CERT_DIR/gateway.key"
generate_gateway_server_cert
printf '%s' "$LAN_IP" > "$CERT_DIR/.cert_ip"
printf '%s' "$EXTRA_SANS" > "$CERT_DIR/.cert_extra_sans"
echo "INFO: Server TLS certificate generated (SAN: IP:${LAN_IP:-127.0.0.1}${EXTRA_SANS:+,${EXTRA_SANS}})"
@@ -797,25 +973,27 @@ PY
return 0
}
if ! start_openclaw_runtime; then
exit 1
fi
# --- Loopback relay for tailnet bind mode (issue #90) ---
# --- Loopback relay helpers for tailnet bind mode (issue #90) ---
# When gateway.bind=tailnet the gateway only listens on the Tailscale IP.
# The local CLI always tries ws://127.0.0.1:PORT and fails with
# "Gateway not running" even though the gateway is healthy.
# A lightweight Node.js relay (loopback-only) forwards those connections
# to the Tailscale IP so terminal CLI commands work normally.
# Token auth is still enforced end-to-end by the gateway.
if [ "$GATEWAY_BIND_MODE" = "tailnet" ]; then
TAILSCALE_IP=$(ip -4 addr show tailscale0 2>/dev/null \
# These functions start/stop a lightweight Node.js TCP relay on
# 127.0.0.1:PORT -> TAILSCALE_IP:PORT so terminal CLI commands work.
# IMPORTANT: stop_gw_relay must be called before restarting the gateway;
# otherwise the relay holds the loopback port and the new gateway instance
# detects it as "already listening" and exits with code 1.
start_gw_relay() {
if [ "$GATEWAY_BIND_MODE" != "tailnet" ]; then
return 0
fi
local ts_ip
ts_ip=$(ip -4 addr show tailscale0 2>/dev/null \
| awk '/inet /{gsub(/\/.*/,"",$2); print $2; exit}' || true)
if [[ "${TAILSCALE_IP:-}" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "INFO: Starting loopback relay for tailnet gateway (127.0.0.1:${GATEWAY_PORT} -> ${TAILSCALE_IP}:${GATEWAY_PORT})"
if [[ "${ts_ip:-}" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "INFO: Starting loopback relay for tailnet gateway (127.0.0.1:${GATEWAY_PORT} -> ${ts_ip}:${GATEWAY_PORT})"
node -e "
const net = require('net');
const TARGET_HOST = '${TAILSCALE_IP}';
const TARGET_HOST = '${ts_ip}';
const TARGET_PORT = ${GATEWAY_PORT};
const server = net.createServer(function(c) {
const t = net.createConnection(TARGET_PORT, TARGET_HOST);
@@ -830,8 +1008,69 @@ server.listen(TARGET_PORT, '127.0.0.1');" &
echo "WARN: tailnet bind mode active but Tailscale IP not found on tailscale0 interface."
echo "WARN: Terminal CLI may show gateway as unreachable. Ensure Tailscale is running and restart."
fi
}
stop_gw_relay() {
if [ -n "${GW_RELAY_PID}" ] && kill -0 "${GW_RELAY_PID}" >/dev/null 2>&1; then
kill -TERM "${GW_RELAY_PID}" >/dev/null 2>&1 || true
wait "${GW_RELAY_PID}" 2>/dev/null || true
GW_RELAY_PID=""
fi
}
# Find a running gateway daemon's PID using multiple detection methods.
# Used by the supervisor loop to detect self-restarts (SIGUSR1) without
# spawning duplicate gateway instances that collide on the port.
#
# Three tiers, tried in order of reliability:
# 1. Port ownership via `ss -tlnp` — authoritative, but only works once
# the daemon has bound the port (can take 20+ s on Pi hardware).
# 2. Process title via `pgrep -f openclaw-gateway` — works after Node.js
# sets process.title, which also happens late during init.
# 3. /proc cmdline scan — catches the daemon IMMEDIATELY after fork,
# before title or port bind, by matching "openclaw" in the cmdline.
# Excludes known PIDs (nginx, ttyd, relay, our shell, old GW_PID).
#
# Returns the PID on stdout and exit 0, or exits with code 1 if nothing found.
find_gateway_daemon_pid() {
local pid=""
# Tier 1: port ownership (authoritative once port is bound)
pid=$(ss -tlnp 2>/dev/null \
| grep ":${GATEWAY_INTERNAL_PORT} " \
| sed -n 's/.*pid=\([0-9]*\).*/\1/p' \
| head -1)
[ -n "$pid" ] && { echo "$pid"; return 0; }
# Tier 2: process title (after Node sets process.title)
pid=$(pgrep -f "openclaw-gateway" 2>/dev/null | head -1)
[ -n "$pid" ] && { echo "$pid"; return 0; }
# Tier 3: scan /proc for any openclaw process we don't already know about.
# The daemon's cmdline (e.g. node /usr/.../openclaw/...) contains "openclaw"
# from the moment it is forked, even before process.title is set.
local known=" ${NGINX_PID:-0} ${TTYD_PID:-0} ${GW_RELAY_PID:-0} ${GW_PID:-0} $$ "
local f cand
for f in /proc/[0-9]*/cmdline; do
[ -r "$f" ] || continue
if tr '\0' ' ' < "$f" 2>/dev/null | grep -q "openclaw"; then
cand="${f#/proc/}"
cand="${cand%%/*}"
case "$known" in *" $cand "*) continue ;; esac
echo "$cand"
return 0
fi
done
return 1
}
if ! start_openclaw_runtime; then
exit 1
fi
start_gw_relay
# Start web terminal (optional)
TTYD_PID_FILE="/var/run/openclaw-ttyd.pid"
@@ -898,38 +1137,61 @@ if command -v ss >/dev/null 2>&1 && ss -tlnp 2>/dev/null | grep -q ':48099 '; th
echo "WARN: Port 48099 still in use after cleanup; nginx may fail to start"
fi
# Render nginx config from template.
# The gateway token is NOT managed by the add-on; OpenClaw will generate/store it.
# Read directly from config file — the CLI redacts secrets since v2026.2.22+.
GW_TOKEN="$(python3 -c "
# ------------------------------------------------------------------------------
# render_landing: (re-)render the nginx config + landing page HTML.
#
# Called once before nginx starts (token may be empty on first boot/pre-onboard)
# and again in the background after the gateway comes up so a freshly-generated
# token is immediately reflected in the "Open Gateway Web UI" button.
# nginx is sent SIGHUP to reload the updated config without restarting.
# ------------------------------------------------------------------------------
render_landing() {
local label="${1:-startup}"
# Read gateway token directly from openclaw.json (CLI redacts secrets v2026.2.22+)
local token
token="$(python3 -c "
import json, os
p = os.environ.get('OPENCLAW_CONFIG_PATH', '/config/.openclaw/openclaw.json')
print(json.load(open(p)).get('gateway',{}).get('auth',{}).get('token',''), end='')
" 2>/dev/null || true)"
# Collect disk usage for landing page status card
DISK_TOTAL="" DISK_USED="" DISK_AVAIL="" DISK_PCT=""
if df -h /config >/dev/null 2>&1; then
DISK_TOTAL=$(df -h /config | awk 'NR==2{print $2}')
DISK_USED=$(df -h /config | awk 'NR==2{print $3}')
DISK_AVAIL=$(df -h /config | awk 'NR==2{print $4}')
DISK_PCT=$(df -h /config | awk 'NR==2{print $5}')
echo "INFO: Disk usage: ${DISK_USED}/${DISK_TOTAL} (${DISK_PCT} used, ${DISK_AVAIL} free)"
# Warn early if disk is getting full
DISK_PCT_NUM=${DISK_PCT//%/}
if [ "$DISK_PCT_NUM" -ge 90 ] 2>/dev/null; then
echo "WARNING: Disk is ${DISK_PCT} full! Add-on updates may fail. Run 'oc-cleanup' in the terminal."
elif [ "$DISK_PCT_NUM" -ge 75 ] 2>/dev/null; then
echo "NOTICE: Disk is ${DISK_PCT} full. Consider running 'oc-cleanup' in the terminal."
local disk_total="" disk_used="" disk_avail="" disk_pct=""
if df -h /config >/dev/null 2>&1; then
disk_total=$(df -h /config | awk 'NR==2{print $2}')
disk_used=$(df -h /config | awk 'NR==2{print $3}')
disk_avail=$(df -h /config | awk 'NR==2{print $4}')
disk_pct=$(df -h /config | awk 'NR==2{print $5}')
if [ "$label" = "startup" ]; then
echo "INFO: Disk usage: ${disk_used}/${disk_total} (${disk_pct} used, ${disk_avail} free)"
local pct_num=${disk_pct//%/}
if [ "$pct_num" -ge 90 ] 2>/dev/null; then
echo "WARNING: Disk is ${disk_pct} full! Add-on updates may fail. Run 'oc-cleanup' in the terminal."
elif [ "$pct_num" -ge 75 ] 2>/dev/null; then
echo "NOTICE: Disk is ${disk_pct} full. Consider running 'oc-cleanup' in the terminal."
fi
fi
fi
fi
GW_PUBLIC_URL="$GW_PUBLIC_URL" GW_TOKEN="$GW_TOKEN" TERMINAL_PORT="$TERMINAL_PORT" \
ENABLE_HTTPS_PROXY="$ENABLE_HTTPS_PROXY" HTTPS_PROXY_PORT="$GATEWAY_PORT" \
GATEWAY_INTERNAL_PORT="$GATEWAY_INTERNAL_PORT" ACCESS_MODE="$ACCESS_MODE" \
DISK_TOTAL="$DISK_TOTAL" DISK_USED="$DISK_USED" DISK_AVAIL="$DISK_AVAIL" DISK_PCT="$DISK_PCT" \
NGINX_LOG_LEVEL="$NGINX_LOG_LEVEL" \
python3 /render_nginx.py
GW_PUBLIC_URL="$GW_PUBLIC_URL" GW_TOKEN="$token" TERMINAL_PORT="$TERMINAL_PORT" \
ENABLE_HTTPS_PROXY="$ENABLE_HTTPS_PROXY" HTTPS_PROXY_PORT="$GATEWAY_PORT" \
GATEWAY_INTERNAL_PORT="$GATEWAY_INTERNAL_PORT" ACCESS_MODE="$ACCESS_MODE" \
DISK_TOTAL="$disk_total" DISK_USED="$disk_used" DISK_AVAIL="$disk_avail" DISK_PCT="$disk_pct" \
NGINX_LOG_LEVEL="$NGINX_LOG_LEVEL" \
python3 /render_nginx.py
if [ "$label" != "startup" ]; then
# Signal nginx to reload config/landing HTML without dropping connections.
local nginx_pid
nginx_pid=$(cat "${NGINX_PID_FILE:-/var/run/openclaw-nginx.pid}" 2>/dev/null || true)
if [ -n "$nginx_pid" ] && kill -0 "$nginx_pid" 2>/dev/null; then
kill -HUP "$nginx_pid" 2>/dev/null || true
echo "INFO: Landing page re-rendered with gateway token (nginx reloaded)."
fi
fi
}
# Initial render (token may be absent if openclaw.json does not exist yet)
render_landing startup
echo "Starting ingress proxy (nginx) on :48099 ..."
nginx -g 'daemon off;' &
@@ -942,34 +1204,123 @@ else
echo "WARN: nginx failed to start (PID $NGINX_PID exited); ingress UI may be unavailable"
fi
# If the token was not available at startup (first boot / pre-onboard), schedule
# a background re-render so the "Open Gateway Web UI" button gets the real token
# once openclaw onboard writes openclaw.json (typically within 30-90 s).
(
CONFIG_PATH="${OPENCLAW_CONFIG_PATH:-/config/.openclaw/openclaw.json}"
for _i in $(seq 1 24); do
sleep 5
token=$(python3 -c "
import json, os
p='$CONFIG_PATH'
try:
print(json.load(open(p)).get('gateway',{}).get('auth',{}).get('token',''), end='')
except Exception:
pass
" 2>/dev/null || true)
if [ -n "$token" ]; then
render_landing post-onboard
break
fi
done
) &
# Keep add-on alive even if gateway/node runtime restarts itself (e.g. during onboarding).
# If runtime exits unexpectedly, restart it while nginx/ttyd stay up.
#
# Design notes (issue #95):
# `openclaw gateway run` is a thin wrapper that spawns `openclaw-gateway` as a
# long-running daemon and then exits. When the gateway self-restarts (SIGUSR1 /
# `openclaw gateway restart`), the old daemon exits and a NEW daemon is forked —
# the new PID is NOT a child of this shell so `wait` cannot block on it.
#
# The new daemon can take 20-30 seconds to initialise on low-power hardware
# (Pi / eMMC). During that time its process.title and port binding are not yet
# visible, but the process itself exists in /proc with "openclaw" in its cmdline.
#
# Strategy:
# 1. `wait` for our child (the wrapper). After it exits, use
# `find_gateway_daemon_pid` (port → pgrep → /proc scan) with retries
# to find the daemon. If found → re-track and poll with `kill -0`.
# 2. When the re-tracked daemon eventually exits (crash or another restart),
# `kill -0` fails, we check again for a live daemon to re-track.
# 3. Before any supervisor-initiated restart, do a final port-occupancy
# guard to prevent launching a duplicate.
GW_IS_CHILD=true # true only when GW_PID was started by us (can use `wait`)
while true; do
GW_EXIT_CODE=0
wait "${GW_PID}" || GW_EXIT_CODE=$?
if [ "$GW_IS_CHILD" = "true" ]; then
# Efficient blocking wait on our child process.
GW_EXIT_CODE=0
wait "${GW_PID}" 2>/dev/null || GW_EXIT_CODE=$?
else
# GW_PID is NOT our child (re-tracked after a self-restart).
# Poll with kill -0 until it exits.
while kill -0 "$GW_PID" 2>/dev/null; do
if [ "$SHUTTING_DOWN" = "true" ]; then break 2; fi
sleep 5
done
GW_EXIT_CODE=0
fi
if [ "$SHUTTING_DOWN" = "true" ]; then
break
fi
# Detect agent/user-initiated self-restart (e.g. 'openclaw gateway restart').
# When the gateway restarts itself, the old PID exits but a new process immediately
# binds the same port. Without this check the supervisor would spawn a second
# instance, which fails with "already listening" and loops forever.
# Give the new process a moment to start, then re-track it instead of spawning a duplicate.
sleep 1
RESTARTED_PID=$(pgrep -f "openclaw.*(gateway|node).*run" 2>/dev/null | head -1 || true)
if [ -n "$RESTARTED_PID" ] && [ "$RESTARTED_PID" != "$GW_PID" ]; then
echo "INFO: OpenClaw runtime restarted itself (new PID $RESTARTED_PID); re-tracking."
# --- Detect self-restart ---------------------------------------------------
# Try up to 10 times (≈ 20 s) using all 3 tiers of find_gateway_daemon_pid.
# Tier 3 (/proc scan) usually finds the daemon on the very first attempt
# because the process exists immediately after fork, even before port bind
# or process.title. The retries cover edge cases on extremely slow I/O.
RESTARTED_PID=""
if [ "$GATEWAY_MODE" != "remote" ]; then
for _attempt in 1 2 3 4 5 6 7 8 9 10; do
RESTARTED_PID=$(find_gateway_daemon_pid 2>/dev/null || true)
[ -n "$RESTARTED_PID" ] && break
sleep 2
done
else
sleep 2
RESTARTED_PID=$(pgrep -f "openclaw.*node.*run" 2>/dev/null | head -1 || true)
fi
if [ -n "$RESTARTED_PID" ]; then
echo "INFO: OpenClaw runtime active (PID $RESTARTED_PID); monitoring."
GW_PID="$RESTARTED_PID"
GW_IS_CHILD=false
continue
fi
# --- Final port guard ------------------------------------------------------
# Even if all detection methods missed the daemon during the loop above,
# the port may now be bound (the daemon finished initialising while we slept).
# Never launch a duplicate if the port is occupied.
if [ "$GATEWAY_MODE" != "remote" ] && \
ss -tlnp 2>/dev/null | grep -q ":${GATEWAY_INTERNAL_PORT} "; then
PORT_PID=$(ss -tlnp 2>/dev/null \
| grep ":${GATEWAY_INTERNAL_PORT} " \
| sed -n 's/.*pid=\([0-9]*\).*/\1/p' \
| head -1 || true)
echo "INFO: Gateway port ${GATEWAY_INTERNAL_PORT} occupied by PID ${PORT_PID:-unknown}; monitoring."
GW_PID="${PORT_PID:-$GW_PID}"
GW_IS_CHILD=false
continue
fi
echo "WARN: OpenClaw runtime exited with code ${GW_EXIT_CODE}. Restarting in 2s..."
sleep 2
# Stop the loopback relay BEFORE restarting the gateway (tailnet mode only).
# The relay holds 127.0.0.1:GATEWAY_PORT — leaving it up causes the new gateway
# to detect the port as occupied and exit with code 1, re-entering the loop.
stop_gw_relay
if ! start_openclaw_runtime; then
echo "ERROR: Failed to restart OpenClaw runtime; retrying in 5s..."
sleep 5
else
GW_IS_CHILD=true
start_gw_relay
fi
done
+8
View File
@@ -42,6 +42,14 @@ configuration:
clean_session_locks_on_exit:
name: Изчистване на заключвания при изход
description: Изчистване на заключващи файлове на сесии при нормално спиране на добавката
persist_node_global:
name: Запазване на npm глобални умения
description: Когато е ВКЛ., съхранява инсталираните от потребителя npm умения и глобални пакети в /config/.node_global, за да оцелеят при преизграждане на добавката. По подразбиране е ИЗКЛ. за по-малки HA архиви.
persist_brew_tools:
name: Запазване на Homebrew инструменти
description: Когато е ВКЛ., съхранява Homebrew и инсталираните с brew CLI инструменти в /config/.linuxbrew, за да оцелеят при преизграждане на добавката. По подразбиране е ИЗКЛ. за по-малки HA архиви.
gateway_mode:
name: Режим на Gateway
+8
View File
@@ -42,6 +42,14 @@ configuration:
clean_session_locks_on_exit:
name: Sitzungssperren beim Beenden bereinigen
description: Sitzungssperrdateien beim ordnungsgemäßen Stoppen des Add-ons bereinigen
persist_node_global:
name: npm-Globale Skills dauerhaft speichern
description: Wenn EIN, werden benutzerinstallierte npm-Skills und globale Pakete unter /config/.node_global gespeichert und über Add-on-Neubuilds hinweg behalten. Standard ist AUS, um Home-Assistant-Backups klein zu halten.
persist_brew_tools:
name: Homebrew-Tools dauerhaft speichern
description: Wenn EIN, werden Homebrew und per brew installierte CLI-Tools unter /config/.linuxbrew gespeichert und über Add-on-Neubuilds hinweg behalten. Standard ist AUS, um Home-Assistant-Backups klein zu halten.
gateway_mode:
name: Gateway-Modus
+8
View File
@@ -42,6 +42,14 @@ configuration:
clean_session_locks_on_exit:
name: Clean Session Locks on Exit
description: Cleanup session lock files when add-on stops gracefully
persist_node_global:
name: Persist npm Global Skills
description: When ON, store user-installed npm skills and global packages under /config/.node_global so they survive add-on rebuilds. Default OFF to keep Home Assistant backups smaller.
persist_brew_tools:
name: Persist Homebrew Tools
description: When ON, store Homebrew and brew-installed CLI tools under /config/.linuxbrew so they survive add-on rebuilds. Default OFF to keep Home Assistant backups smaller.
gateway_mode:
name: Gateway Mode
+8
View File
@@ -42,6 +42,14 @@ configuration:
clean_session_locks_on_exit:
name: Limpiar bloqueos de sesión al salir
description: Limpiar archivos de bloqueo de sesión cuando el complemento se detiene correctamente
persist_node_global:
name: Conservar habilidades globales de npm
description: Cuando está activado, guarda las habilidades npm instaladas por el usuario y los paquetes globales en /config/.node_global para que sobrevivan a reconstrucciones del complemento. Por defecto está desactivado para mantener pequeñas las copias de seguridad de Home Assistant.
persist_brew_tools:
name: Conservar herramientas de Homebrew
description: Cuando está activado, guarda Homebrew y las herramientas CLI instaladas con brew en /config/.linuxbrew para que sobrevivan a reconstrucciones del complemento. Por defecto está desactivado para mantener pequeñas las copias de seguridad de Home Assistant.
gateway_mode:
name: Modo del Gateway
+8
View File
@@ -42,6 +42,14 @@ configuration:
clean_session_locks_on_exit:
name: Wyczyść blokady sesji przy wyjściu
description: Usuń pliki blokad sesji gdy dodatek zatrzymuje się poprawnie
persist_node_global:
name: Zachowuj globalne umiejętności npm
description: Po włączeniu zapisuje umiejętności npm instalowane przez użytkownika i pakiety globalne w /config/.node_global, aby przetrwały przebudowę dodatku. Domyślnie wyłączone, aby kopie zapasowe Home Assistant były mniejsze.
persist_brew_tools:
name: Zachowuj narzędzia Homebrew
description: Po włączeniu zapisuje Homebrew i narzędzia CLI instalowane przez brew w /config/.linuxbrew, aby przetrwały przebudowę dodatku. Domyślnie wyłączone, aby kopie zapasowe Home Assistant były mniejsze.
gateway_remote_url:
name: URL zdalnego Gateway
@@ -42,6 +42,14 @@ configuration:
clean_session_locks_on_exit:
name: Limpar Bloqueios de Sessão ao Sair
description: Limpar arquivos de bloqueio de sessão quando o add-on parar normalmente
persist_node_global:
name: Persistir skills globais do npm
description: Quando ativado, armazena skills npm instaladas pelo usuário e pacotes globais em /config/.node_global para sobreviverem a reconstruções do add-on. O padrão é desativado para manter menores os backups do Home Assistant.
persist_brew_tools:
name: Persistir ferramentas do Homebrew
description: Quando ativado, armazena o Homebrew e ferramentas CLI instaladas via brew em /config/.linuxbrew para sobreviverem a reconstruções do add-on. O padrão é desativado para manter menores os backups do Home Assistant.
gateway_mode:
name: Modo do Gateway