From 7b8cc30172d5f05934ffd03bef913b0ed4c5db9c Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Sat, 6 Jun 2026 16:21:36 +0200 Subject: [PATCH 1/4] feat(aggregator): add secubox-aggregator-migrate helper (ref #498) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automates the Phase 7 cutover applied live on gk2 : 1. Walk /usr/lib/secubox//api/main.py to discover installable modules and rewrite /etc/secubox/aggregator.toml accordingly 2. Restart secubox-aggregator and read /health to find out which modules actually mounted (filters out modules with relative-import bugs that cannot be loaded from a file path) 3. For each mounted module : back up + patch the matching /etc/nginx/secubox-routes.d/.conf so proxy_pass targets /run/secubox/aggregator.sock instead of the now-defunct /run/secubox/.sock 4. nginx -t + reload 5. systemctl stop + disable secubox-.service for migrated modules (frees ~30 MB Python interpreter each) 6. Final report with rollback recipe Idempotent : second run is a no-op (modules already pointed at aggregator socket, services already disabled). Operators re-run it after installing a new secubox-* package to fold it into the aggregator. Wires the script under /usr/sbin/ via debian/rules and prints a one-line hint in postinst pointing operators at it. Mass migration on gk2 was 110/121 modules → 1515 MB RAM freed (3499→1984 MB uvicorn), 153 MB → 2.7 GiB free. --- packages/secubox-aggregator/debian/postinst | 4 + packages/secubox-aggregator/debian/rules | 3 + .../sbin/secubox-aggregator-migrate | 142 ++++++++++++++++++ 3 files changed, 149 insertions(+) create mode 100755 packages/secubox-aggregator/sbin/secubox-aggregator-migrate diff --git a/packages/secubox-aggregator/debian/postinst b/packages/secubox-aggregator/debian/postinst index 9d708cbe..3124c926 100755 --- a/packages/secubox-aggregator/debian/postinst +++ b/packages/secubox-aggregator/debian/postinst @@ -16,6 +16,10 @@ case "$1" in systemctl daemon-reload systemctl enable secubox-aggregator.service systemctl start secubox-aggregator.service || true + + echo "secubox-aggregator: to migrate all installed SecuBox modules into" + echo " the aggregator (replaces per-module uvicorn processes) run :" + echo " sudo /usr/sbin/secubox-aggregator-migrate" ;; esac #DEBHELPER# diff --git a/packages/secubox-aggregator/debian/rules b/packages/secubox-aggregator/debian/rules index 374106be..a829047e 100644 --- a/packages/secubox-aggregator/debian/rules +++ b/packages/secubox-aggregator/debian/rules @@ -14,3 +14,6 @@ override_dh_auto_install: install -d $(CURDIR)/debian/secubox-aggregator/lib/systemd/system install -m 644 systemd/secubox-aggregator.service \ $(CURDIR)/debian/secubox-aggregator/lib/systemd/system/ + install -d $(CURDIR)/debian/secubox-aggregator/usr/sbin + install -m 755 sbin/secubox-aggregator-migrate \ + $(CURDIR)/debian/secubox-aggregator/usr/sbin/ diff --git a/packages/secubox-aggregator/sbin/secubox-aggregator-migrate b/packages/secubox-aggregator/sbin/secubox-aggregator-migrate new file mode 100755 index 00000000..6074b899 --- /dev/null +++ b/packages/secubox-aggregator/sbin/secubox-aggregator-migrate @@ -0,0 +1,142 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LicenseRef-CMSD-1.0 +# Copyright (c) 2026 CyberMind — Gérald Kerma + +# SecuBox-Deb :: secubox-aggregator-migrate +# Phase 7 (#496) — automate cutover from per-module uvicorn to aggregator. +# +# Steps : +# 1. Discover all /usr/lib/secubox//api/main.py modules +# 2. Generate /etc/secubox/aggregator.toml with the full list +# 3. Restart aggregator and capture which modules mounted successfully +# 4. For each mounted module : +# - Switch nginx proxy_pass to /run/secubox/aggregator.sock +# - Stop + disable secubox-.service +# 5. Reload nginx +# 6. Report savings +# +# Idempotent. Operator can re-run after package upgrades that add modules. + +set -euo pipefail +readonly MODULE="secubox-aggregator-migrate" +readonly VERSION="0.1.0" + +log() { printf '[%s] %s\n' "$MODULE" "$*" >&2; } +err() { printf '[%s] ERROR: %s\n' "$MODULE" "$*" >&2; exit 1; } + +[ -d /usr/lib/secubox ] || err "no /usr/lib/secubox — SecuBox modules not installed" +[ -d /run/secubox ] || err "no /run/secubox runtime dir — secubox-core required" +command -v curl >/dev/null || err "curl required" + +log "Phase 7 ASGI migration — v${VERSION}" + +# ── Step 1+2 : Discover modules + write aggregator.toml ────────────────── +TOML_TMP=$(mktemp) +{ + echo "# Phase 7 (#496) — auto-generated by ${MODULE}" + echo "# Run secubox-aggregator-migrate to refresh after installing new modules." + echo "modules = [" + for f in /usr/lib/secubox/*/api/main.py; do + [ -f "$f" ] || continue + m=$(basename "$(dirname "$(dirname "$f")")") + printf ' "%s",\n' "$m" + done + echo "]" +} > "$TOML_TMP" + +DISCOVERED=$(grep -c '^ "' "$TOML_TMP" || echo 0) +log "discovered ${DISCOVERED} modules" + +install -d -m 0755 /etc/secubox +install -m 0644 "$TOML_TMP" /etc/secubox/aggregator.toml +rm -f "$TOML_TMP" + +# ── Step 3 : Restart aggregator + wait for mount completion ────────────── +systemctl restart secubox-aggregator +sleep 8 + +# ── Step 4 : Grab mounted module list from /health ─────────────────────── +HEALTH=$(curl -s --unix-socket /run/secubox/aggregator.sock http://localhost/health 2>&1) || \ + err "aggregator /health unreachable" + +MOUNTED=$(printf '%s' "$HEALTH" | python3 -c " +import sys, json +try: + d = json.loads(sys.stdin.read()) + print(' '.join(d.get('mounted', []))) +except Exception as e: + print('', end='') + sys.stderr.write(f'parse: {e}\n') +") + +if [ -z "$MOUNTED" ]; then + err "no modules mounted in aggregator — check 'journalctl -u secubox-aggregator'" +fi + +MOUNTED_COUNT=$(printf '%s' "$MOUNTED" | wc -w) +log "aggregator mounted ${MOUNTED_COUNT} modules — proceeding with cutover" + +# ── Step 5 : nginx proxy_pass switch ───────────────────────────────────── +SWITCHED=0 +NGINX_SKIPPED=0 +for name in $MOUNTED; do + cfg=/etc/nginx/secubox-routes.d/${name}.conf + if [ ! -f "$cfg" ]; then + NGINX_SKIPPED=$((NGINX_SKIPPED + 1)) + continue + fi + # First-run backup + [ -f "${cfg}.bak.phase7" ] || cp "$cfg" "${cfg}.bak.phase7" + sed -i "s|unix:/run/secubox/${name}\.sock|unix:/run/secubox/aggregator.sock|g" "$cfg" + SWITCHED=$((SWITCHED + 1)) +done +log "nginx routes switched : ${SWITCHED} (skipped ${NGINX_SKIPPED} with no route file)" + +# Validate + reload nginx +if ! nginx -t 2>/dev/null; then + err "nginx config check failed — review /etc/nginx/secubox-routes.d/" +fi +systemctl reload nginx +log "nginx reloaded" + +# ── Step 6 : Disable migrated per-module systemd units ─────────────────── +STOPPED=0 +for name in $MOUNTED; do + svc=secubox-${name}.service + systemctl is-active "$svc" >/dev/null 2>&1 || continue + systemctl stop "$svc" 2>/dev/null || true + systemctl disable "$svc" 2>/dev/null || true + STOPPED=$((STOPPED + 1)) +done +log "stopped + disabled : ${STOPPED} per-module services" + +# ── Step 7 : Final report ─────────────────────────────────────────────── +cat <.conf.bak.phase7 \\ + /etc/nginx/secubox-routes.d/.conf + systemctl enable --now secubox-.service + systemctl reload nginx +──────────────────────────────────────────────────────────────────── +REPORT From b1ee9427a3b529e1c8390b942fc2b963d3fe5435 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Sat, 6 Jun 2026 16:33:48 +0200 Subject: [PATCH 2/4] fix(aggregator): rewrite nginx prefix-preservation for all 3 locations (ref #498) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First version of secubox-aggregator-migrate only touched /etc/nginx/secubox-routes.d/.conf and used a simple socket-name sed (replace `.sock` with `aggregator.sock`). On gk2 this left the navbar empty after migration because : 1. Most legacy routes live in /etc/nginx/secubox.d/.conf — that directory was untouched, so `/api/v1//` still targeted the now-defunct per-module socket. 2. webui.conf carries inline `location /api/v1//` blocks for the stats/auth modules (auth, certs, metrics, waf, soc, ...) plus a catch-all `location /api/ { proxy_pass http://127.0.0.1:8001; }` pointing at hub's old TCP port. Both untouched. 3. The naive sed of `:/` (strip-prefix rewrite) sent `/foo` upstream instead of `/api/v1//foo`. The aggregator mounts each sub-app under `/api/v1/` and so cannot find anything when the prefix is stripped — every route returned 404 or 502. 4. Hub used TCP 127.0.0.1:8001, not a unix socket, so the socket-name sed never matched. This rewrite : - Walks BOTH /etc/nginx/secubox.d/ and /etc/nginx/secubox-routes.d/ - Collapses every `proxy_pass http://unix:/run/secubox/.sock(:/...)?` variant to `proxy_pass http://unix:.../aggregator.sock:/api/v1//` so the full prefix arrives at the aggregator - Handles the same rewrite inline inside sites-enabled/webui.conf - Special-cases hub TCP 127.0.0.1:8001 → aggregator unix - Rewrites the /api/ catch-all to forward the full URI unchanged - Backs up the inline-block file under /root (NOT next to webui.conf, because nginx auto-includes sites-enabled/* and would crash on a duplicate default_server) Verified live on gk2 : 106/110 mounted modules now return 200 via nginx, free RAM 4.5 GiB. --- .../sbin/secubox-aggregator-migrate | 79 +++++++++++++++++-- 1 file changed, 72 insertions(+), 7 deletions(-) diff --git a/packages/secubox-aggregator/sbin/secubox-aggregator-migrate b/packages/secubox-aggregator/sbin/secubox-aggregator-migrate index 6074b899..f576e8d8 100755 --- a/packages/secubox-aggregator/sbin/secubox-aggregator-migrate +++ b/packages/secubox-aggregator/sbin/secubox-aggregator-migrate @@ -77,24 +77,85 @@ MOUNTED_COUNT=$(printf '%s' "$MOUNTED" | wc -w) log "aggregator mounted ${MOUNTED_COUNT} modules — proceeding with cutover" # ── Step 5 : nginx proxy_pass switch ───────────────────────────────────── +# +# Per-module FastAPIs used to listen on /run/secubox/.sock and the +# per-module nginx route did : +# proxy_pass http://unix:.../.sock:/; +# The trailing `:/` strips the matching location prefix, so the upstream +# saw a bare `/foo` URI and matched its top-level routes. +# +# The aggregator mounts each sub-app under /api/v1/, so it expects +# the FULL prefix to arrive intact. Rewrite must : +# 1. Point at /run/secubox/aggregator.sock +# 2. Preserve the /api/v1// prefix on the upstream URI +# +# Three locations need patching : +# /etc/nginx/secubox.d/.conf ← legacy module-side drop-ins +# /etc/nginx/secubox-routes.d/.conf ← newer per-module route files +# /etc/nginx/sites-enabled/webui.conf ← inline per-module location blocks +# + the /api/ catch-all fallback +# Plus one special case : hub used to listen on TCP 127.0.0.1:8001. SWITCHED=0 NGINX_SKIPPED=0 + +_rewrite_file() { + local f="$1" name="$2" + [ -f "$f" ] || return 0 + case "$f" in *.bak*|*.dpkg*|*.disabled*) return 0 ;; esac + # First-run backup + [ -f "${f}.bak.phase7" ] || cp "$f" "${f}.bak.phase7" + # Every per-module unix socket → aggregator + /api/v1// prefix + sed -i -E \ + -e "s|proxy_pass http://unix:/run/secubox/${name}\.sock(:/[^;]*)?;|proxy_pass http://unix:/run/secubox/aggregator.sock:/api/v1/${name}/;|g" \ + -e "s|proxy_pass http://unix:/run/secubox/aggregator\.sock:/;|proxy_pass http://unix:/run/secubox/aggregator.sock:/api/v1/${name}/;|g" \ + "$f" +} + for name in $MOUNTED; do - cfg=/etc/nginx/secubox-routes.d/${name}.conf - if [ ! -f "$cfg" ]; then + found_any=0 + for dir in /etc/nginx/secubox.d /etc/nginx/secubox-routes.d; do + cfg="${dir}/${name}.conf" + if [ -f "$cfg" ]; then + _rewrite_file "$cfg" "$name" + found_any=1 + fi + done + if [ "$found_any" -eq 0 ]; then NGINX_SKIPPED=$((NGINX_SKIPPED + 1)) continue fi - # First-run backup - [ -f "${cfg}.bak.phase7" ] || cp "$cfg" "${cfg}.bak.phase7" - sed -i "s|unix:/run/secubox/${name}\.sock|unix:/run/secubox/aggregator.sock|g" "$cfg" SWITCHED=$((SWITCHED + 1)) done + +# Hub special case : it used TCP 127.0.0.1:8001 (not a unix socket). Replace +# every such line in secubox.d/hub.conf and in webui.conf inline blocks. +HUB_CFG=/etc/nginx/secubox.d/hub.conf +if [ -f "$HUB_CFG" ]; then + [ -f "${HUB_CFG}.bak.phase7" ] || cp "$HUB_CFG" "${HUB_CFG}.bak.phase7" + sed -i -E "s|proxy_pass http://127\.0\.0\.1:8001/?;|proxy_pass http://unix:/run/secubox/aggregator.sock:/api/v1/hub/;|g" "$HUB_CFG" +fi + +# webui.conf : inline `location /api/v1//` blocks + `/api/` catch-all +WEBUI=/etc/nginx/sites-enabled/webui.conf +if [ -f "$WEBUI" ]; then + [ -f "/root/webui.conf.bak.phase7" ] || cp "$WEBUI" /root/webui.conf.bak.phase7 + # NOTE: backup stored under /root, not next to $WEBUI, because nginx + # auto-includes anything matching sites-enabled/* and would crash on + # duplicate-default-server. + for name in $MOUNTED; do + sed -i -E \ + -e "s|proxy_pass http://unix:/run/secubox/${name}\.sock(:/[^;]*)?;|proxy_pass http://unix:/run/secubox/aggregator.sock:/api/v1/${name}/;|g" \ + "$WEBUI" + done + # Catch-all : keep the full URI by removing any rewrite on /api/ + sed -i -E "/location \/api\/ \{/,/\}/ s|proxy_pass http://127\.0\.0\.1:8001;|proxy_pass http://unix:/run/secubox/aggregator.sock;|" "$WEBUI" +fi + log "nginx routes switched : ${SWITCHED} (skipped ${NGINX_SKIPPED} with no route file)" # Validate + reload nginx if ! nginx -t 2>/dev/null; then - err "nginx config check failed — review /etc/nginx/secubox-routes.d/" + err "nginx config check failed — review /etc/nginx/{secubox.d,secubox-routes.d,sites-enabled/webui.conf}" fi systemctl reload nginx log "nginx reloaded" @@ -134,8 +195,12 @@ Failed mount reasons (relative imports etc.) : Rollback per module if needed : + # restore whichever of these exist + cp /etc/nginx/secubox.d/.conf.bak.phase7 \\ + /etc/nginx/secubox.d/.conf 2>/dev/null cp /etc/nginx/secubox-routes.d/.conf.bak.phase7 \\ - /etc/nginx/secubox-routes.d/.conf + /etc/nginx/secubox-routes.d/.conf 2>/dev/null + cp /root/webui.conf.bak.phase7 /etc/nginx/sites-enabled/webui.conf systemctl enable --now secubox-.service systemctl reload nginx ──────────────────────────────────────────────────────────────────── From 705476cf0f73e27b374ad6384d3036fa4dea6924 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Sat, 6 Jun 2026 16:39:56 +0200 Subject: [PATCH 3/4] fix(aggregator): synthetic-package loader handles relative imports + api shadow (ref #498) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first loader iteration loaded api/main.py as a flat module sbx_mod_ with sys.path.insert of /. Two patterns broke across 11 of 121 modules : 1. Relative imports : modules that do `from .deps import X` failed with "attempted relative import with no known parent package" because the module had no __package__ set (just a flat name with no dots). 2. Absolute api.X imports : a request that loaded auth first inserted auth's `api`, `api.deps`, `api.webui_identity`, ... into sys.modules. When haproxy then ran `from api.webui_identity import X`, Python returned auth's cached `api.webui_identity` — usually missing the name haproxy expected, so it raised ImportError. (Hit haproxy because it loaded after auth alphabetically.) The new loader : - Builds a synthetic parent package `sbx_pkg_` whose __path__ is `/api`. Loads api/main.py AS `.main` with __package__ = pkg, so `from .deps import X` resolves through the submodule_search_locations and finds the right deps.py. - Drops every `api*` entry from sys.modules BEFORE each module load, and again AFTER, so each module gets a clean api namespace and one module's loaded api.X cannot shadow another module's api.X. - Keeps the temporary sys.path entry so absolute `from api.X` still works during exec_module (until cleanup). Verified live on gk2 : 119 of 121 modules now mount (was 110). The remaining 2 are real module bugs unrelated to the loader : - magicmirror : ships api/main.py + api/__init__.py but no api/routers/ — `from .routers import mmpm` always failed (this module never started cleanly under its own systemd unit either) - openclaw : /var/lib/secubox/openclaw is 0750 root:root so the aggregator (running as `secubox`) can't traverse it during api/main.py's directory-creation block. Fix is in the openclaw postinst (chown to secubox or add a group), not here. --- .../secubox-aggregator/aggregator/main.py | 93 +++++++++++++++---- 1 file changed, 77 insertions(+), 16 deletions(-) diff --git a/packages/secubox-aggregator/aggregator/main.py b/packages/secubox-aggregator/aggregator/main.py index e8254b53..adba2ea9 100644 --- a/packages/secubox-aggregator/aggregator/main.py +++ b/packages/secubox-aggregator/aggregator/main.py @@ -74,14 +74,31 @@ SECUBOX_LIB = Path("/usr/lib/secubox") def _load_app_from_path(name: str) -> FastAPI | None: """Locate the module's FastAPI by walking /usr/lib/secubox//. - SecuBox modules ship their FastAPI as `api/main.py` (and the per-module - systemd unit runs `uvicorn api.main:app` from cwd=/usr/lib/secubox/). - They are not installed as Python packages, so importlib.import_module - can't find them — we load by absolute path via spec_from_file_location. + SecuBox modules ship their FastAPI as `api/main.py`. They are not + installed as Python packages — the per-module systemd unit runs + `uvicorn api.main:app` from cwd=/usr/lib/secubox/, so module + code typically uses one of these import styles : - The aggregator adds the module's directory to sys.path TEMPORARILY so - relative imports inside `api/main.py` work (e.g. `from .deps import ...`). - The path is popped after loading to avoid cross-module shadowing. + from .deps import X # relative — needs __package__ set + from api.deps import X # absolute — needs cwd on sys.path + from api import deps # mixed + import api.deps # bare absolute + + To make all four work for many modules in the SAME interpreter we : + + 1. Synthesise a per-module parent package `sbx_pkg_` whose + __path__ is `/api`. Loading api/main.py as `.main` + inside that synthetic package makes `from .deps` resolve to + `sbx_pkg_.deps`, served from the right `api/` directory. + 2. Pop any cached `sys.modules["api"]` / `sys.modules["api.*"]` left + behind by previously-loaded modules and put on sys.path — + so absolute `from api.X` resolves freshly to THIS module's `api/`. + 3. After exec, drop the freshly-loaded `api*` entries again so the + next module starts with a clean slate (and remove the sys.path + entry to prevent cross-module bleed). + + Both styles now work per-module without one module's `api/` + shadowing another's — the bug that hit haproxy after auth had loaded. """ import importlib.util as _util @@ -91,21 +108,60 @@ def _load_app_from_path(name: str) -> FastAPI | None: ] target = next((p for p in candidates if p.exists()), None) if target is None: - _LOAD_ERRORS[name] = f"main.py not found under {SECUBOX_LIB}/{{{name},{name.replace('-','_')}}}/api/" + _LOAD_ERRORS[name] = ( + f"main.py not found under " + f"{SECUBOX_LIB}/{{{name},{name.replace('-', '_')}}}/api/" + ) return None - mod_dir = str(target.parent.parent) # /usr/lib/secubox/ - spec = _util.spec_from_file_location(f"sbx_mod_{name.replace('-', '_')}", target) + api_dir = target.parent # /usr/lib/secubox//api + mod_dir = str(api_dir.parent) # /usr/lib/secubox/ + safe = name.replace("-", "_") + pkg_name = f"sbx_pkg_{safe}" + main_name = f"{pkg_name}.main" + + # ── (1) Synthetic parent package so `from .deps import X` works ── + pkg_init = api_dir / "__init__.py" + if pkg_init.exists(): + pkg_spec = _util.spec_from_file_location( + pkg_name, pkg_init, + submodule_search_locations=[str(api_dir)], + ) + else: + pkg_spec = _util.spec_from_loader(pkg_name, loader=None, is_package=True) + pkg_spec.submodule_search_locations = [str(api_dir)] + if pkg_spec is None: + _LOAD_ERRORS[name] = "could not build synthetic parent package spec" + return None + pkg_mod = _util.module_from_spec(pkg_spec) + pkg_mod.__path__ = [str(api_dir)] + + # ── (2) Isolate the `api` namespace from prior module loads ── + for k in list(sys.modules): + if k == "api" or k.startswith("api."): + del sys.modules[k] + sys.path.insert(0, mod_dir) + sys.modules[pkg_name] = pkg_mod + if pkg_init.exists() and pkg_spec.loader is not None: + try: + pkg_spec.loader.exec_module(pkg_mod) + except Exception as e: + _LOAD_ERRORS[name] = f"exec_module(parent): {type(e).__name__}: {e}" + sys.modules.pop(pkg_name, None) + try: + sys.path.remove(mod_dir) + except ValueError: + pass + return None + + # ── (3) Load api/main.py as `.main` so it inherits __package__ ── + spec = _util.spec_from_file_location(main_name, target) if spec is None or spec.loader is None: _LOAD_ERRORS[name] = f"spec_from_file_location returned None for {target}" return None - mod = _util.module_from_spec(spec) - # Inject into sys.modules under a unique name so reloading/dependent - # imports inside the module find it. - sys.modules[spec.name] = mod - # Add the module's parent dir to sys.path so its relative imports work. - sys.path.insert(0, mod_dir) + mod.__package__ = pkg_name + sys.modules[main_name] = mod try: spec.loader.exec_module(mod) except Exception as e: @@ -116,6 +172,11 @@ def _load_app_from_path(name: str) -> FastAPI | None: sys.path.remove(mod_dir) except ValueError: pass + # Drop any `api*` cache this load produced so the next module gets + # its own fresh `api` namespace. + for k in list(sys.modules): + if k == "api" or k.startswith("api."): + del sys.modules[k] sub_app = getattr(mod, "app", None) if not isinstance(sub_app, FastAPI): From 0cc99d5187d0ac8f2fc7a66e00601fa0e313ae47 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Sat, 6 Jun 2026 16:48:38 +0200 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20Phase=207=20follow-up=20bugs=20?= =?UTF-8?q?=E2=80=94=20magicmirror,=20openclaw,=20ghost=20dirs=20(ref=20#4?= =?UTF-8?q?98)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three small bugs surfaced when auditing the aggregator load after mass migration on gk2 : magicmirror : debian/rules installed only api/main.py + an empty __init__.py, so the running module's `from .routers import mmpm` had no routers/ package to find. Was already broken under its own per-module systemd unit. Now ships api/__init__.py from source + api/routers/{__init__.py,mmpm.py}. openclaw : postinst chown'd /var/lib/secubox/openclaw to root:root with 0750. The Phase 7.D aggregator runs as `secubox`, which couldn't traverse the parent dir during the module's import-time mkdir of scans/. Now chown to secubox:secubox (when the user exists), 0755 on parent, 0750 on scans/ — same containment, traversable by the aggregator. secubox-aggregator-migrate : older packaging shipped some modules under /usr/lib/secubox/secubox-/ in addition to the canonical /usr/lib/secubox//. On upgraded systems both coexist and the discovery walk picked up duplicates that mounted as empty (no paths) sub-apps under /api/v1/secubox-/. Now skipped when an unprefixed twin exists. Live on gk2 : 117 mounted, 0 failed, 4.4 GiB free RAM. magicmirror and openclaw both return HTTP 200 via nginx. --- .../sbin/secubox-aggregator-migrate | 17 +++++++++++++++++ packages/secubox-magicmirror/debian/rules | 5 ++++- packages/secubox-openclaw/debian/postinst | 13 ++++++++++--- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/packages/secubox-aggregator/sbin/secubox-aggregator-migrate b/packages/secubox-aggregator/sbin/secubox-aggregator-migrate index f576e8d8..87f1a063 100755 --- a/packages/secubox-aggregator/sbin/secubox-aggregator-migrate +++ b/packages/secubox-aggregator/sbin/secubox-aggregator-migrate @@ -31,6 +31,13 @@ command -v curl >/dev/null || err "curl required" log "Phase 7 ASGI migration — v${VERSION}" # ── Step 1+2 : Discover modules + write aggregator.toml ────────────────── +# +# Older packaging variants installed under /usr/lib/secubox/secubox-/ +# (with the package-name prefix) before being superseded by the canonical +# unprefixed /usr/lib/secubox//. Both directories often coexist on +# upgraded systems. We filter out the `secubox-*` prefixed siblings here : +# their FastAPIs are byte-identical with the unprefixed ones, so mounting +# both would just waste memory and create ghost openapi.json entries. TOML_TMP=$(mktemp) { echo "# Phase 7 (#496) — auto-generated by ${MODULE}" @@ -39,6 +46,16 @@ TOML_TMP=$(mktemp) for f in /usr/lib/secubox/*/api/main.py; do [ -f "$f" ] || continue m=$(basename "$(dirname "$(dirname "$f")")") + case "$m" in + secubox-*) + # legacy prefixed dir — canonical unprefixed twin wins + stripped="${m#secubox-}" + if [ -f "/usr/lib/secubox/${stripped}/api/main.py" ]; then + log "skip ${m} (duplicate of ${stripped})" + continue + fi + ;; + esac printf ' "%s",\n' "$m" done echo "]" diff --git a/packages/secubox-magicmirror/debian/rules b/packages/secubox-magicmirror/debian/rules index b0f0e449..83d038b5 100755 --- a/packages/secubox-magicmirror/debian/rules +++ b/packages/secubox-magicmirror/debian/rules @@ -5,7 +5,10 @@ override_dh_auto_install: install -d $(CURDIR)/debian/secubox-magicmirror/usr/lib/secubox/magicmirror/api install -m 644 api/main.py $(CURDIR)/debian/secubox-magicmirror/usr/lib/secubox/magicmirror/api/ - touch $(CURDIR)/debian/secubox-magicmirror/usr/lib/secubox/magicmirror/api/__init__.py + install -m 644 api/__init__.py $(CURDIR)/debian/secubox-magicmirror/usr/lib/secubox/magicmirror/api/ + install -d $(CURDIR)/debian/secubox-magicmirror/usr/lib/secubox/magicmirror/api/routers + install -m 644 api/routers/__init__.py $(CURDIR)/debian/secubox-magicmirror/usr/lib/secubox/magicmirror/api/routers/ + install -m 644 api/routers/mmpm.py $(CURDIR)/debian/secubox-magicmirror/usr/lib/secubox/magicmirror/api/routers/ install -d $(CURDIR)/debian/secubox-magicmirror/usr/share/secubox/www/magicmirror install -m 644 www/magicmirror/index.html $(CURDIR)/debian/secubox-magicmirror/usr/share/secubox/www/magicmirror/ install -d $(CURDIR)/debian/secubox-magicmirror/etc/nginx/secubox.d diff --git a/packages/secubox-openclaw/debian/postinst b/packages/secubox-openclaw/debian/postinst index 09df7781..e2ef4935 100755 --- a/packages/secubox-openclaw/debian/postinst +++ b/packages/secubox-openclaw/debian/postinst @@ -1,11 +1,18 @@ #!/bin/sh set -e if [ "$1" = "configure" ]; then - # Create data directories + # Create data directories. The openclaw FastAPI imports a module that + # touches /var/lib/secubox/openclaw/scans at import time, so the user + # that loads it MUST be able to traverse the parent dir. Under + # Phase 7.D (secubox-aggregator) the loader runs as `secubox`, not + # root — so the parent dir is 0755 and owned by `secubox:secubox`. mkdir -p /var/lib/secubox/openclaw/scans mkdir -p /var/cache/secubox/openclaw - chown -R root:root /var/lib/secubox/openclaw - chmod 750 /var/lib/secubox/openclaw + if getent passwd secubox >/dev/null; then + chown -R secubox:secubox /var/lib/secubox/openclaw /var/cache/secubox/openclaw + fi + chmod 0755 /var/lib/secubox/openclaw + chmod 0750 /var/lib/secubox/openclaw/scans # Enable and start service systemctl daemon-reload