cm_bot_v2/docs/superpowers/plans/2026-05-02-debug-mode-hotfix.md
yiekheng 40c3a76c13 Add implementation plan for debug-mode hotfix
Bite-sized TDD-style plan: failing helper test, two implementation
tasks (web then api), compose plumbing, doc updates, integration
verification. Uses unittest stdlib so no new deps.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 16:20:50 +08:00

20 KiB

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


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:

  • Step 2: Write the failing test

Create tests/test_debug_enabled.py:

"""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:

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:

/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
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:

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 getenvos.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:

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:

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:

    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
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
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):

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:

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):

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):

    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:

    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
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()
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
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:

    environment:
      PYTHONUNBUFFERED: "1"
      DB_HOST: ${DB_HOST}

(under api-server:, currently lines 40-42). Insert CM_DEBUG directly after PYTHONUNBUFFERED:

    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):

    environment:
      PYTHONUNBUFFERED: "1"
      API_BASE_URL: http://api-server:3000
      CM_PREFIX_PATTERN: ${CM_PREFIX_PATTERN}

Insert CM_DEBUG after PYTHONUNBUFFERED:

    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
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
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
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
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
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
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:

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:

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:

docker compose down
  • Step 2: Local dev with CM_DEBUG=false (default) — debug OFF path

Unset or set CM_DEBUG=false in .env, then:

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:

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:

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:

docker compose down
  • Step 3: Override path regression check

From the repo root:

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
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)