fix(followups): F2 - Python entry points report an unreadable env file in one line

A .mam.env that exists but cannot be read or decoded stopped the Python
entry points with an EnvFileError traceback, while the wrapper printed one
`ERROR:` line and exited 1 (PR #43 review claude S1). The mains that load the
env file - publish_event, registry, job_subscriber, cli, bench, heartbeat and
remote - now call mqtt_common.load_env_or_report(), which prints
`ERROR: <path>: cannot read (<exception type>)` and lets the main return 1.
No value or file content is printed. ensure_env_loaded() still raises: no
library function exits the process.

Test: every entry point, for a 000 file and an undecodable one, ends with
exactly that line and rc 1 and creates nothing (14 cases; all 14 fail at
0051fd6 with a traceback).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HCGpJXhSZasJLU38ksDghe
This commit is contained in:
GodopuandClaude Opus 5.5 committed 2026-10-02 08:09:46 +09:00
1 parent 75936f11c6
commit 5edf913d22
9 files changed
+73 -9

No files matched your search

+2 -1
View File
@@ -163,7 +163,8 @@ def run_benchmark(n=50, transport="fake", connect_latency_ms=50.0,
def main(argv: Optional[List[str]] = None) -> int:
mqtt_common.ensure_env_loaded()
if not mqtt_common.load_env_or_report():
return 1
parser = argparse.ArgumentParser(prog="lib_py.delegation.bench")
parser.add_argument("--n", type=int, default=50)
parser.add_argument("--transport", default="fake", choices=["fake", "broker"])
+2 -1
View File
@@ -57,7 +57,8 @@ def _build_parser() -> argparse.ArgumentParser:
def main(argv: Optional[List[str]] = None) -> int:
mqtt_common.ensure_env_loaded()
if not mqtt_common.load_env_or_report():
return 1
args = _build_parser().parse_args(argv)
if args.command == "submit":
@@ -493,7 +493,8 @@ def main(argv=None) -> int:
help="stop once this process (the agent) is gone")
parser.add_argument("-v", "--verbose", action="store_true")
args = parser.parse_args(argv)
mqtt_common.ensure_env_loaded()
if not mqtt_common.load_env_or_report():
return 1
mqtt_common.setup_logging(logging.DEBUG if args.verbose else logging.INFO)
return run(args.job, args.registry_dir, args.interval, args.expect_dispatch_token,
args.attempt_id, args.max_lifetime, args.watch_pid)
@@ -587,7 +587,8 @@ def main(argv=None) -> int:
parser.add_argument("-v", "--verbose", action="store_true")
args = parser.parse_args(argv)
mqtt_common.ensure_env_loaded()
if not mqtt_common.load_env_or_report():
return 1
mqtt_common.setup_logging(logging.DEBUG if args.verbose else logging.WARNING)
try:
@@ -21,6 +21,7 @@ import hmac
import json
import logging
import os
import sys
import tempfile
import time
import uuid
@@ -110,9 +111,10 @@ def ensure_env_loaded(workspace_dir: Optional[str] = None) -> None:
"""Load the workspace env file exactly once per process (D6, issue #5 phase 1).
This used to run at import time. Importing a library must not mutate
``os.environ``, so every *process entry point* calls this instead:
``os.environ``, so every *process entry point* calls this instead -
``publish_event.main``, ``registry.main``, ``job_subscriber.main``,
``cli.main``, ``bench.main``, and reconcile.sh's embedded subscriber.
``cli.main``, ``bench.main``, ``heartbeat.main`` and ``remote.main``
through :func:`load_env_or_report`, and reconcile.sh's embedded subscriber.
Production semantics are unchanged because every production path passes
through one of those, and OS env still wins over the file
(see :func:`_parse_env_file`).
@@ -123,6 +125,20 @@ def ensure_env_loaded(workspace_dir: Optional[str] = None) -> None:
_env_loaded = True
_load_dotenv(workspace_dir)
def load_env_or_report() -> bool:
"""ensure_env_loaded() for a process entry point: an env file that exists
but cannot be read ends the process the way the wrapper does - one
`ERROR: <path>: cannot read (<exception type>)` line, no traceback, no
value - and the caller returns 1. False after printing; library code keeps
calling ensure_env_loaded() and never exits."""
try:
ensure_env_loaded()
except envfile.EnvFileError as e:
print(f"ERROR: {e}", file=sys.stderr)
return False
return True
# --------------------------------------------------------------------------
# Constants
# --------------------------------------------------------------------------
@@ -334,7 +334,8 @@ def main(argv=None, _beat: bool = False) -> int:
logger.error("--heartbeat needs a positive period and a non-terminal event")
return 1
mqtt_common.ensure_env_loaded()
if not mqtt_common.load_env_or_report():
return 1
mqtt_common.setup_logging(logging.DEBUG if args.verbose else logging.WARNING)
# --- parse optional data JSON (parameter error -> exit 1) ---
+2 -1
View File
@@ -533,7 +533,8 @@ def _prompt_from_args(parser: argparse.ArgumentParser, args: argparse.Namespace)
def main(argv: Optional[List[str]] = None) -> int:
mqtt_common.ensure_env_loaded()
if not mqtt_common.load_env_or_report():
return 1
mqtt_common.setup_logging(logging.INFO)
parser = _build_parser()
args = parser.parse_args(argv)
+2 -1
View File
@@ -460,7 +460,8 @@ def main(argv=None) -> int:
p_r.add_argument("--python", default=None)
parser.add_argument("-v", "--verbose", action="store_true")
args = parser.parse_args(argv)
mqtt_common.ensure_env_loaded()
if not mqtt_common.load_env_or_report():
return 1
mqtt_common.setup_logging(logging.DEBUG if args.verbose else logging.INFO)
if args.command == "dispatch":
try: