diff --git a/docs/superpowers/plans/2026-05-02-debug-mode-hotfix.md b/docs/superpowers/plans/2026-05-02-debug-mode-hotfix.md new file mode 100644 index 0000000..13e8288 --- /dev/null +++ b/docs/superpowers/plans/2026-05-02-debug-mode-hotfix.md @@ -0,0 +1,621 @@ +# Debug-Mode Hotfix Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace hardcoded `debug=True` in both Flask entrypoints with an env-driven `CM_DEBUG` toggle so the Werkzeug debugger is off by default in rex/siong containers and only enabled when an operator opts in. + +**Architecture:** Add a small `_debug_enabled()` helper to `app/cm_api.py` and `app/cm_web_view.py` that reads `CM_DEBUG` from the environment and accepts `1`/`true`/`yes` (case-insensitive) as truthy. Wire `CM_DEBUG: ${CM_DEBUG:-false}` into the Flask service `environment:` blocks in `docker-compose.yml`. Document the variable in `.env.example` and `AGENTS.md`. Gate against regressions with a `unittest`-based test that exercises both copies of the helper. + +**Tech Stack:** Python 3.9 (containers), Python 3.12 (local venv), Flask 2.3.3, Docker Compose v2, `unittest` (stdlib — no new dependency). + +**Spec:** [docs/superpowers/specs/2026-05-02-debug-mode-hotfix-design.md](../specs/2026-05-02-debug-mode-hotfix-design.md) + +--- + +## File Map + +| File | Operation | Purpose | +|---|---|---| +| `tests/__init__.py` | Create | Make `tests/` a package so the test runner can find it. | +| `tests/test_debug_enabled.py` | Create | unittest-based regression tests for the `_debug_enabled` helper in both Flask modules; parametrized so the two copies can't drift. | +| `app/cm_web_view.py` | Modify | Add `_debug_enabled()`; replace `debug=True` in `app.run(...)` at line 748. | +| `app/cm_api.py` | Modify | Add `import os`; add `_debug_enabled()`; change `run()` default to `debug=None` with env resolution. | +| `docker-compose.yml` | Modify | Plumb `CM_DEBUG: ${CM_DEBUG:-false}` into `api-server` and `web-view` env blocks. | +| `.env.example` | Modify | New `Runtime` section documenting `CM_DEBUG`. | +| `AGENTS.md` | Modify | One-line note about `CM_DEBUG` under Security & Configuration Tips. | + +The `_debug_enabled` helper is intentionally duplicated rather than placed in a shared module — only two call sites, the parser is six lines, and `app/__init__.py` is just a package marker. The test file enforces parity. + +--- + +## Task 1: Test infrastructure + first failing test for `cm_web_view._debug_enabled` + +**Files:** +- Create: `tests/__init__.py` +- Create: `tests/test_debug_enabled.py` + +- [ ] **Step 1: Create the test package marker** + +Create `tests/__init__.py` empty: + +```python +``` + +- [ ] **Step 2: Write the failing test** + +Create `tests/test_debug_enabled.py`: + +```python +"""Regression tests for the _debug_enabled helper. + +Both app.cm_api and app.cm_web_view define a private _debug_enabled() +function that parses the CM_DEBUG environment variable. They are +intentionally duplicated (only two call sites; no shared utility module +exists). This test runs the same parametrized cases against every copy +to catch drift if one is updated without the other. +""" + +import os +import unittest +from unittest import mock + +# Import the modules at top-level (before any mock.patch.dict with +# clear=True), so module-load-time os.getenv() reads see the real +# environment. The patches inside individual tests then only affect the +# helper's runtime read of CM_DEBUG. +import app.cm_api +import app.cm_web_view + + +# Modules expected to expose a private _debug_enabled() helper. +# Add new entries here if more Flask entrypoints adopt the same toggle. +HELPER_MODULES = ( + app.cm_web_view, + app.cm_api, +) + + +# (env_value, expected_result). env_value=None means CM_DEBUG is unset. +CASES = ( + (None, False), + ("", False), + ("false", False), + ("False", False), + ("FALSE", False), + ("0", False), + ("no", False), + ("anything-else", False), + ("true", True), + ("True", True), + ("TRUE", True), + ("1", True), + ("yes", True), + ("YES", True), + (" true ", True), # whitespace tolerated +) + + +class DebugEnabledTests(unittest.TestCase): + def _resolve(self, module): + return getattr(module, "_debug_enabled", None) + + def test_helper_exists_on_every_module(self): + for module in HELPER_MODULES: + with self.subTest(module=module.__name__): + helper = self._resolve(module) + self.assertTrue( + callable(helper), + f"{module.__name__}._debug_enabled must be callable", + ) + + def test_parses_cm_debug_consistently(self): + for module in HELPER_MODULES: + helper = self._resolve(module) + if helper is None: + self.fail( + f"{module.__name__}._debug_enabled is missing — " + "make test_helper_exists_on_every_module pass first" + ) + for env_value, expected in CASES: + with self.subTest(module=module.__name__, env=env_value): + env = {} if env_value is None else {"CM_DEBUG": env_value} + with mock.patch.dict(os.environ, env, clear=True): + self.assertEqual( + helper(), + expected, + f"{module.__name__}._debug_enabled() should be " + f"{expected!r} for CM_DEBUG={env_value!r}", + ) + + +if __name__ == "__main__": + unittest.main() +``` + +- [ ] **Step 3: Run the test to verify it fails** + +Run from repo root: + +```bash +cd /home/yiekheng/projects/cm_bot_v2 && \ +.venv/bin/python -m unittest tests.test_debug_enabled -v +``` + +Expected: import error or `AttributeError: module 'app.cm_web_view' has no attribute '_debug_enabled'`. The failure mode may be that importing `app.cm_web_view` itself fails because Flask is needed — that is fine, see Task 2 for the venv prerequisite. + +- [ ] **Step 4: Install runtime deps into the venv if Flask import fails** + +Only if the previous step failed at import time with `ModuleNotFoundError: No module named 'flask'`, run: + +```bash +/home/yiekheng/projects/cm_bot_v2/.venv/bin/pip install -r /home/yiekheng/projects/cm_bot_v2/requirements.txt +``` + +Then re-run Step 3. Expected after install: the test fails with the missing `_debug_enabled` attribute, not an import error. + +- [ ] **Step 5: Commit the failing test** + +```bash +cd /home/yiekheng/projects/cm_bot_v2 && \ +git add tests/__init__.py tests/test_debug_enabled.py && \ +git -c user.name='yiekheng' -c user.email='yiekheng@04080616.xyz' \ + commit -m "test: add CM_DEBUG helper parity test (failing)" +``` + +--- + +## Task 2: Implement `_debug_enabled` in `cm_web_view.py` and flip `app.run` + +**Files:** +- Modify: `app/cm_web_view.py` (top of file — verify `import os` already present at line 10; bottom of file — `__main__` block currently at lines 744-748) + +- [ ] **Step 1: Verify `os` is already imported** + +Run: + +```bash +grep -n "^import os" /home/yiekheng/projects/cm_bot_v2/app/cm_web_view.py +``` + +Expected: `10:import os`. If absent, add `import os` to the imports section. Do not add `from os import getenv` — `os.getenv` is what the helper uses. + +- [ ] **Step 2: Add the helper near the top of the module** + +Insert after the imports section (before `app = Flask(...)`). Find the first blank line after the imports and insert: + +```python +def _debug_enabled() -> bool: + """Return True iff CM_DEBUG env var is set to a truthy value. + + Truthy: '1', 'true', 'yes' (case-insensitive, whitespace-trimmed). + Anything else, including unset, is False. Default-off so the + Werkzeug debugger is never reachable in production containers. + """ + return os.getenv("CM_DEBUG", "false").strip().lower() in ("1", "true", "yes") +``` + +- [ ] **Step 3: Replace `debug=True` in the `__main__` block** + +Find: + +```python +if __name__ == '__main__': + print("Starting CM Web View...") + print("Web interface will be available at: http://localhost:8000") + print("Make sure the API server is running on port 3000") + app.run(host='0.0.0.0', port=8000, debug=True) +``` + +Replace the last line with: + +```python + app.run(host='0.0.0.0', port=8000, debug=_debug_enabled()) +``` + +- [ ] **Step 4: Run the test — `cm_web_view` cases should now pass, `cm_api` cases should still fail** + +```bash +cd /home/yiekheng/projects/cm_bot_v2 && \ +.venv/bin/python -m unittest tests.test_debug_enabled -v +``` + +Expected: `test_helper_exists_on_every_module` and `test_parses_cm_debug_consistently` still fail because `app.cm_api._debug_enabled` doesn't exist yet. The `app.cm_web_view` subTest entries should pass. Confirm by reading the verbose subtest output. + +- [ ] **Step 5: Commit** + +```bash +cd /home/yiekheng/projects/cm_bot_v2 && \ +git add app/cm_web_view.py && \ +git -c user.name='yiekheng' -c user.email='yiekheng@04080616.xyz' \ + commit -m "feat(web): make Werkzeug debug opt-in via CM_DEBUG" +``` + +--- + +## Task 3: Implement `_debug_enabled` in `cm_api.py` and switch `run()` to env-resolved default + +**Files:** +- Modify: `app/cm_api.py` (top of file — currently no `import os`; class `CM_API.run` method around line 160) + +- [ ] **Step 1: Add `import os` at the top** + +Find the existing import block (currently lines 1-4): + +```python +import threading +from flask import Flask, jsonify, request +from flask_cors import CORS +from .db import DB +``` + +Insert `import os` as the very first line so the imports remain stdlib-then-third-party: + +```python +import os +import threading +from flask import Flask, jsonify, request +from flask_cors import CORS +from .db import DB +``` + +- [ ] **Step 2: Add the helper above the `CM_API` class** + +Insert between the imports and `class CM_API:` (around line 6): + +```python +def _debug_enabled() -> bool: + """Return True iff CM_DEBUG env var is set to a truthy value. + + Truthy: '1', 'true', 'yes' (case-insensitive, whitespace-trimmed). + Anything else, including unset, is False. Default-off so the + Werkzeug debugger is never reachable in production containers. + """ + return os.getenv("CM_DEBUG", "false").strip().lower() in ("1", "true", "yes") +``` + +This text is identical to the helper in `cm_web_view.py` — the parametrized test enforces it stays that way. + +- [ ] **Step 3: Change the `run` method signature and resolve when unset** + +Find the existing method (currently around line 160): + +```python + def run(self, port=3000, debug=True): + # Test database connection before starting server + test_db = self._get_database_connection() + if test_db is None: + print("Cannot start server: Database not available") + exit(1) + self._close_database_connection(test_db) + + print(f'CM Bot DB API Listening at Port : {port}') + self.app.run(host='0.0.0.0', port=port, debug=debug) +``` + +Replace with: + +```python + def run(self, port=3000, debug=None): + if debug is None: + debug = _debug_enabled() + # Test database connection before starting server + test_db = self._get_database_connection() + if test_db is None: + print("Cannot start server: Database not available") + exit(1) + self._close_database_connection(test_db) + + print(f'CM Bot DB API Listening at Port : {port}') + self.app.run(host='0.0.0.0', port=port, debug=debug) +``` + +Do **not** touch `run_in_thread` — its `debug=False` default is already safe and used internally. + +- [ ] **Step 4: Run the test — both modules should now pass** + +```bash +cd /home/yiekheng/projects/cm_bot_v2 && \ +.venv/bin/python -m unittest tests.test_debug_enabled -v +``` + +Expected: `OK` with all subTests passing for both `app.cm_web_view` and `app.cm_api`. + +- [ ] **Step 5: Confirm no caller passes `debug` positionally to `run()`** + +```bash +grep -rn "\.run(" /home/yiekheng/projects/cm_bot_v2/app/ | grep -v "self\.app\.run\|app\.run(" +``` + +Expected: only `api.run(port = 3000)` in `cm_api.py:191`. If anything else appears that passes a positional second arg to `CM_API.run`, change it to a keyword argument before continuing — the signature change went from `debug=True` to `debug=None`, so a caller passing `True` positionally would now incorrectly enable debug. + +- [ ] **Step 6: Commit** + +```bash +cd /home/yiekheng/projects/cm_bot_v2 && \ +git add app/cm_api.py && \ +git -c user.name='yiekheng' -c user.email='yiekheng@04080616.xyz' \ + commit -m "feat(api): make Werkzeug debug opt-in via CM_DEBUG" +``` + +--- + +## Task 4: Plumb `CM_DEBUG` into `docker-compose.yml` + +**Files:** +- Modify: `docker-compose.yml` (the `api-server` `environment:` block at lines 40-49 and the `web-view` `environment:` block at lines 63-66) + +- [ ] **Step 1: Add `CM_DEBUG` to the `api-server` environment** + +Find: + +```yaml + environment: + PYTHONUNBUFFERED: "1" + DB_HOST: ${DB_HOST} +``` + +(under `api-server:`, currently lines 40-42). Insert `CM_DEBUG` directly after `PYTHONUNBUFFERED`: + +```yaml + environment: + PYTHONUNBUFFERED: "1" + CM_DEBUG: ${CM_DEBUG:-false} + DB_HOST: ${DB_HOST} +``` + +The `${CM_DEBUG:-false}` form guarantees the variable is defined inside the container even if the operator did not set it in their `.env`. + +- [ ] **Step 2: Add `CM_DEBUG` to the `web-view` environment** + +Find (under `web-view:`, currently lines 63-66): + +```yaml + environment: + PYTHONUNBUFFERED: "1" + API_BASE_URL: http://api-server:3000 + CM_PREFIX_PATTERN: ${CM_PREFIX_PATTERN} +``` + +Insert `CM_DEBUG` after `PYTHONUNBUFFERED`: + +```yaml + environment: + PYTHONUNBUFFERED: "1" + CM_DEBUG: ${CM_DEBUG:-false} + API_BASE_URL: http://api-server:3000 + CM_PREFIX_PATTERN: ${CM_PREFIX_PATTERN} +``` + +Do **not** add it to `telegram-bot` or `transfer-bot` — neither runs a Flask server. + +- [ ] **Step 3: Validate the compose file parses** + +```bash +cd /home/yiekheng/projects/cm_bot_v2 && \ +docker compose -f docker-compose.yml config >/dev/null && echo OK +``` + +Expected: `OK`. If the command errors with a YAML or interpolation problem, fix the indentation around the new lines. + +- [ ] **Step 4: Confirm the variable reaches both services in the rendered config** + +```bash +cd /home/yiekheng/projects/cm_bot_v2 && \ +docker compose -f docker-compose.yml config | grep -E "CM_DEBUG" +``` + +Expected: two matching lines, one each under `api-server` and `web-view`, value rendered as `false` (or whatever `CM_DEBUG` resolves to in the current shell env). + +- [ ] **Step 5: Commit** + +```bash +cd /home/yiekheng/projects/cm_bot_v2 && \ +git add docker-compose.yml && \ +git -c user.name='yiekheng' -c user.email='yiekheng@04080616.xyz' \ + commit -m "chore(compose): pass CM_DEBUG into api-server and web-view" +``` + +--- + +## Task 5: Document `CM_DEBUG` in `.env.example` + +**Files:** +- Modify: `.env.example` (currently 32 lines; the existing `=== Deployment Identity ===` section is at the top) + +- [ ] **Step 1: Insert a new `Runtime` section near the top** + +Find the first three lines of `.env.example`: + +``` +# === Deployment Identity === +# Unique name prefix for containers and network (avoid conflicts on same host) +CM_DEPLOY_NAME=rex-cm +``` + +Add a new section above them: + +``` +# === Runtime === +# Set to true ONLY in local dev. Werkzeug debugger = RCE if exposed. +CM_DEBUG=false + +# === Deployment Identity === +# Unique name prefix for containers and network (avoid conflicts on same host) +CM_DEPLOY_NAME=rex-cm +``` + +- [ ] **Step 2: Confirm the file still parses as a valid env file** + +```bash +cd /home/yiekheng/projects/cm_bot_v2 && \ +grep -E "^[A-Z_]+=" .env.example | wc -l +``` + +Expected: one more line than before the change (run before/after to verify if curious; the absolute count is implementation-dependent). + +- [ ] **Step 3: Commit** + +```bash +cd /home/yiekheng/projects/cm_bot_v2 && \ +git add .env.example && \ +git -c user.name='yiekheng' -c user.email='yiekheng@04080616.xyz' \ + commit -m "docs(env): document CM_DEBUG in .env.example" +``` + +--- + +## Task 6: Add a one-line note to `AGENTS.md` + +**Files:** +- Modify: `AGENTS.md` (the `## Security & Configuration Tips` section at the bottom) + +- [ ] **Step 1: Append a bullet to the Security section** + +Find the existing section (currently the last block in the file): + +``` +## Security & Configuration Tips +- Never commit real secrets in `.env`. +- `app/cm_bot_hal.py` currently contains hardcoded agent credentials/pin; move these to env vars before production use. +- Keep container clocks mounted (`/etc/timezone`, `/etc/localtime`) as compose currently defines to avoid schedule drift. +``` + +Add a new bullet above the `cm_bot_hal.py` line: + +``` +## Security & Configuration Tips +- Never commit real secrets in `.env`. +- `CM_DEBUG` defaults to `false` for both Flask services. Set it to `true` only in local development; rex/siong production env files must leave it unset (the Werkzeug debugger is RCE if reachable). +- `app/cm_bot_hal.py` currently contains hardcoded agent credentials/pin; move these to env vars before production use. +- Keep container clocks mounted (`/etc/timezone`, `/etc/localtime`) as compose currently defines to avoid schedule drift. +``` + +- [ ] **Step 2: Commit** + +```bash +cd /home/yiekheng/projects/cm_bot_v2 && \ +git add AGENTS.md && \ +git -c user.name='yiekheng' -c user.email='yiekheng@04080616.xyz' \ + commit -m "docs(agents): note CM_DEBUG default and intent" +``` + +--- + +## Task 7: Integration verification (manual run) + +This task corresponds to the four verification scenarios in the spec. No commit — these are smoke checks. If any fails, do not declare done; debug and fix. + +**Files:** none modified. + +- [ ] **Step 1: Local dev with `CM_DEBUG=true` — debug ON path** + +In a scratch repo-root `.env` (or temporarily setting in shell), set: + +``` +CM_DEBUG=true +``` + +Then bring up the local stack: + +```bash +cd /home/yiekheng/projects/cm_bot_v2 && \ +docker compose -f docker-compose.yml -f docker-compose.override.yml up --build -d api-server web-view +``` + +Inspect logs: + +```bash +docker compose logs --no-color api-server web-view | grep -E "Debug mode|Debugger PIN" +``` + +Expected: at least one `* Debug mode: on` line and at least one `Debugger PIN:` line. + +Tear down: + +```bash +docker compose down +``` + +- [ ] **Step 2: Local dev with `CM_DEBUG=false` (default) — debug OFF path** + +Unset or set `CM_DEBUG=false` in `.env`, then: + +```bash +cd /home/yiekheng/projects/cm_bot_v2 && \ +docker compose -f docker-compose.yml -f docker-compose.override.yml up --build -d api-server web-view +``` + +Inspect logs: + +```bash +docker compose logs --no-color api-server web-view | grep -E "Debug mode|Debugger PIN" || echo "no debug lines (expected)" +``` + +Expected: prints `no debug lines (expected)` because neither pattern matches. + +Sanity-check both endpoints still serve: + +```bash +curl -s -o /dev/null -w "api %{http_code}\n" http://localhost:3000/acc/ ; \ +curl -s -o /dev/null -w "web %{http_code}\n" "http://localhost:${CM_WEB_HOST_PORT:-8001}/api/acc/" +``` + +Expected: `api 200` and `web 200` (assuming the database is reachable; if the api shows 5xx because of DB, that is unrelated to this change — the absence of debug lines above is the success criterion for this task). + +Tear down: + +```bash +docker compose down +``` + +- [ ] **Step 3: Override path regression check** + +From the repo root: + +```bash +cd /home/yiekheng/projects/cm_bot_v2 && \ +.venv/bin/python -c " +import os +os.environ['CM_DEBUG'] = 'false' +from app.cm_api import _debug_enabled +print('helper says:', _debug_enabled()) +import inspect +sig = inspect.signature(__import__('app.cm_api', fromlist=['CM_API']).CM_API.run) +print('run() signature:', sig) +" +``` + +Expected output: + +``` +helper says: False +run() signature: (self, port=3000, debug=None) +``` + +- [ ] **Step 4: Re-run the full unit test suite** + +```bash +cd /home/yiekheng/projects/cm_bot_v2 && \ +.venv/bin/python -m unittest tests.test_debug_enabled -v +``` + +Expected: `OK` with all subTests passing. + +--- + +## Spec Coverage Check (self-review) + +| Spec requirement | Task | +|---|---| +| `_debug_enabled()` helper definition | Task 2 (web), Task 3 (api) | +| `cm_web_view.py:748` debug flag swap | Task 2 | +| `cm_api.py:160` signature change to `debug=None` | Task 3 | +| `import os` added to `cm_api.py` | Task 3, Step 1 | +| `run_in_thread` left untouched | Task 3, Step 3 (explicit "Do not touch") | +| `docker-compose.yml` plumbing for both Flask services | Task 4 | +| `.env.example` Runtime section | Task 5 | +| `AGENTS.md` one-line note | Task 6 | +| Verification: debug-on local | Task 7, Step 1 | +| Verification: debug-off local + endpoints serve | Task 7, Step 2 | +| Verification: explicit override path | Task 7, Step 3 | +| Regression test for helper parity | Task 1 (write), Tasks 2 & 3 (make pass), Task 7 Step 4 (re-run) |