From 665900cbfe1b3a2b03e9821ea0939c14156f1d6a Mon Sep 17 00:00:00 2001 From: Edgar Arakelyan Date: Mon, 12 Oct 2026 08:58:32 -0700 Subject: [PATCH 1/2] fix(cli): -v logs agent-env's debug messages and only other libraries' warnings `agent-env -v` set the root logger to DEBUG, so every library logged its debug messages, and hpack logs each HTTP/2 header it encodes: a verbose run's output carried Modal's x-modal-token-id and x-modal-token-secret hundreds of times. -v now sets DEBUG on agent-env's own loggers (agent_env, agentenv_protocol), and other libraries log warnings. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/agent_env/cli/__init__.py | 13 +++++++++++-- tst/unit/cli/verbose_logging_test.py | 25 +++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 tst/unit/cli/verbose_logging_test.py diff --git a/src/agent_env/cli/__init__.py b/src/agent_env/cli/__init__.py index e39688d4..5389b8ef 100644 --- a/src/agent_env/cli/__init__.py +++ b/src/agent_env/cli/__init__.py @@ -43,14 +43,23 @@ def invoke(self, ctx: click.Context): raise click.ClickException("\n".join([str(e), *getattr(e, "__notes__", ())])) from e +def _verbose_logging() -> None: + """Log agent-env's own DEBUG messages, and only warnings from other libraries: their debug logs can carry + credentials, as hpack's do, which log every HTTP/2 header Modal's client sends, its token among them.""" + logging.basicConfig(level=logging.WARNING, format="%(asctime)s %(name)s %(message)s") + for name in ("agent_env", "agentenv_protocol"): + logging.getLogger(name).setLevel(logging.DEBUG) + + @click.group(cls=_UserErrorsAreNotCrashes) @click.version_option(package_name="agentenv-framework", prog_name="agent-env", message="%(prog)s %(version)s") -@click.option("--verbose", "-v", is_flag=True, help="Enable verbose logging (DEBUG level)") +@click.option("--verbose", "-v", is_flag=True, + help="Enable agent-env's DEBUG logging (other libraries log warnings only)") @click.pass_context def cli(ctx: click.Context, verbose: bool): """Agent environment CLI.""" if verbose: - logging.basicConfig(level=logging.DEBUG, format="%(asctime)s %(name)s %(message)s") + _verbose_logging() # For this command only, so a CLI invoked in-process leaves the caller's stores as they were. ctx.with_resource(namespace_routing()) diff --git a/tst/unit/cli/verbose_logging_test.py b/tst/unit/cli/verbose_logging_test.py new file mode 100644 index 00000000..db9dcbbe --- /dev/null +++ b/tst/unit/cli/verbose_logging_test.py @@ -0,0 +1,25 @@ +"""``agent-env -v`` logs agent-env's own DEBUG messages, and only warnings from other libraries, whose debug logs can +carry credentials: hpack logs every HTTP/2 header Modal's client sends, its token among them.""" + +import subprocess +import sys + +# In a fresh interpreter, as the CLI runs: pytest's own handlers on the root logger would make basicConfig a no-op here. +_LOGS = """ +import logging +from agent_env.cli import _verbose_logging +_verbose_logging() +logging.getLogger("agent_env.providers.sandbox_providers.modal_sandbox").debug("creating a sandbox") +logging.getLogger("agentenv_protocol.client").debug("reading a card") +logging.getLogger("hpack.hpack").debug("Adding b'x-modal-token-secret'=b'as-0000' to the header table") +logging.getLogger("hpack.hpack").warning("a header table warning") +""" + + +def test_verbose_logs_agent_envs_debug_messages_and_only_warnings_from_other_libraries(): + done = subprocess.run([sys.executable, "-c", _LOGS], capture_output=True, text=True, timeout=120) + + assert done.returncode == 0, done.stderr + assert "creating a sandbox" in done.stderr and "reading a card" in done.stderr + assert "a header table warning" in done.stderr + assert "x-modal-token-secret" not in done.stderr From 4fca0329d2871b2c6a8b243363803555ef1ea94a Mon Sep 17 00:00:00 2001 From: Edgar Arakelyan Date: Mon, 12 Oct 2026 09:07:43 -0700 Subject: [PATCH 2/2] fix(cli): -v puts the loggers' levels back when its command ends The first commit left agent_env's logger at DEBUG after an in-process `-v` command, so later commands in that process, and the unit tests after one (plugin check's own report, with pytest's live logging on), saw it. -v's levels are now a resource of the command's context, as its namespace routing is. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/agent_env/cli/__init__.py | 23 +++++++++++++++++------ tst/unit/cli/verbose_logging_test.py | 26 +++++++++++++++++++++----- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/src/agent_env/cli/__init__.py b/src/agent_env/cli/__init__.py index 5389b8ef..aad88a4a 100644 --- a/src/agent_env/cli/__init__.py +++ b/src/agent_env/cli/__init__.py @@ -1,4 +1,6 @@ import logging +from collections.abc import Iterator +from contextlib import contextmanager import click @@ -43,12 +45,21 @@ def invoke(self, ctx: click.Context): raise click.ClickException("\n".join([str(e), *getattr(e, "__notes__", ())])) from e -def _verbose_logging() -> None: - """Log agent-env's own DEBUG messages, and only warnings from other libraries: their debug logs can carry - credentials, as hpack's do, which log every HTTP/2 header Modal's client sends, its token among them.""" +@contextmanager +def _verbose_logging() -> Iterator[None]: + """Log agent-env's own DEBUG messages, and only warnings from other libraries, while a command runs: their debug + logs can carry credentials, as hpack's do, which log every HTTP/2 header Modal's client sends, its token among + them. The loggers' levels are put back after it, so a CLI invoked in-process leaves the caller's logging as it was.""" logging.basicConfig(level=logging.WARNING, format="%(asctime)s %(name)s %(message)s") - for name in ("agent_env", "agentenv_protocol"): - logging.getLogger(name).setLevel(logging.DEBUG) + loggers = [logging.getLogger(name) for name in ("agent_env", "agentenv_protocol")] + levels = [logger.level for logger in loggers] + for logger in loggers: + logger.setLevel(logging.DEBUG) + try: + yield + finally: + for logger, level in zip(loggers, levels): + logger.setLevel(level) @click.group(cls=_UserErrorsAreNotCrashes) @@ -59,7 +70,7 @@ def _verbose_logging() -> None: def cli(ctx: click.Context, verbose: bool): """Agent environment CLI.""" if verbose: - _verbose_logging() + ctx.with_resource(_verbose_logging()) # For this command only, so a CLI invoked in-process leaves the caller's stores as they were. ctx.with_resource(namespace_routing()) diff --git a/tst/unit/cli/verbose_logging_test.py b/tst/unit/cli/verbose_logging_test.py index db9dcbbe..d7b4b3b1 100644 --- a/tst/unit/cli/verbose_logging_test.py +++ b/tst/unit/cli/verbose_logging_test.py @@ -1,18 +1,23 @@ """``agent-env -v`` logs agent-env's own DEBUG messages, and only warnings from other libraries, whose debug logs can carry credentials: hpack logs every HTTP/2 header Modal's client sends, its token among them.""" +import logging import subprocess import sys +from click.testing import CliRunner + +from agent_env.cli import cli + # In a fresh interpreter, as the CLI runs: pytest's own handlers on the root logger would make basicConfig a no-op here. _LOGS = """ import logging from agent_env.cli import _verbose_logging -_verbose_logging() -logging.getLogger("agent_env.providers.sandbox_providers.modal_sandbox").debug("creating a sandbox") -logging.getLogger("agentenv_protocol.client").debug("reading a card") -logging.getLogger("hpack.hpack").debug("Adding b'x-modal-token-secret'=b'as-0000' to the header table") -logging.getLogger("hpack.hpack").warning("a header table warning") +with _verbose_logging(): + logging.getLogger("agent_env.providers.sandbox_providers.modal_sandbox").debug("creating a sandbox") + logging.getLogger("agentenv_protocol.client").debug("reading a card") + logging.getLogger("hpack.hpack").debug("Adding b'x-modal-token-secret'=b'as-0000' to the header table") + logging.getLogger("hpack.hpack").warning("a header table warning") """ @@ -23,3 +28,14 @@ def test_verbose_logs_agent_envs_debug_messages_and_only_warnings_from_other_lib assert "creating a sandbox" in done.stderr and "reading a card" in done.stderr assert "a header table warning" in done.stderr assert "x-modal-token-secret" not in done.stderr + + +def test_a_verbose_command_run_in_process_leaves_the_callers_logging_as_it_was(): + """Run in a caller's process, as the tests and any embedding code run it, -v lasts only for its command.""" + loggers = [logging.getLogger(name) for name in ("agent_env", "agentenv_protocol")] + before = [logger.level for logger in loggers] + + result = CliRunner().invoke(cli, ["-v", "config", "--help"]) + + assert result.exit_code == 0, result.output + assert [logger.level for logger in loggers] == before