-
Notifications
You must be signed in to change notification settings - Fork 0
Add opt-in diagnostic logging behind -v/--verbose flag #125
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| """Opt-in diagnostic logging behind the root ``-v/--verbose`` flag. | ||
|
|
||
| The CLI normally configures no logging at all, and the realtime paths actively | ||
| *silence* library loggers so stderr stays clean next to the CLI's normalized | ||
| errors (``aai_cli.ws``, ``aai_cli.streaming.diagnostics``). Verbose mode is the | ||
| inverse switch: ``enable`` installs one stderr handler so library logs become | ||
| visible — ``-v`` surfaces request-level lines (httpx and friends at INFO), | ||
| ``-vv`` wire-level detail (websockets frames, httpcore events at DEBUG) — and | ||
| the silencers stand down while it is ``active``. | ||
|
|
||
| Secrets never print in clear: ``register_secret`` records sensitive values as | ||
| they are resolved (API key, session JWT) and the handler's formatter masks them | ||
| in every rendered record. Masking must live in the formatter, not at call | ||
| sites, because the leak comes from *library* logs — websockets logs the raw | ||
| Authorization header at DEBUG during the handshake. | ||
|
|
||
| Stdlib-only on purpose: ``config`` (a Rich-free library layer) registers | ||
| secrets here, so this module must not pull in Rich via ``output``/``theme``. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import logging | ||
| import sys | ||
|
|
||
| _MASK = "[redacted]" | ||
|
|
||
| _verbosity = 0 | ||
| _secrets: set[str] = set() | ||
|
|
||
|
|
||
| class _RedactingFormatter(logging.Formatter): | ||
| """Formats records normally, then masks every registered secret.""" | ||
|
|
||
| def format(self, record: logging.LogRecord) -> str: | ||
| text = super().format(record) | ||
| for secret in _secrets: | ||
| text = text.replace(secret, _MASK) | ||
| return text | ||
|
|
||
|
|
||
| def register_secret(value: str | None) -> None: | ||
| """Record a sensitive value so verbose output masks it. Empty values are | ||
| ignored (replacing "" would shred every record).""" | ||
| if value: | ||
| _secrets.add(value) | ||
|
|
||
|
|
||
| def active() -> bool: | ||
| """Whether verbose logging is on — the realtime silencers stand down then.""" | ||
| return _verbosity > 0 | ||
|
|
||
|
|
||
| def enable(verbosity: int) -> None: | ||
| """Install the stderr diagnostics handler: ``-v`` (1) at INFO, ``-vv``+ at DEBUG. | ||
|
|
||
| Zero is the everyday no-op — no handler, the CLI stays log-silent. The | ||
| handler goes on the root logger so third-party loggers (httpx, websockets, | ||
| the assemblyai SDK) are covered without naming each one; stderr keeps the | ||
| errors-to-stderr / data-to-stdout split intact for pipelines. | ||
| """ | ||
| global _verbosity | ||
| if verbosity <= 0: | ||
| return | ||
| _verbosity = verbosity | ||
| handler = logging.StreamHandler(sys.stderr) | ||
| handler.setFormatter(_RedactingFormatter("[%(name)s] %(message)s")) | ||
| root = logging.getLogger() | ||
| root.addHandler(handler) | ||
| root.setLevel(logging.INFO if verbosity == 1 else logging.DEBUG) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| """Tests for the opt-in diagnostic logging behind the root -v/--verbose flag | ||
| (aai_cli/debuglog.py): the stderr handler, secret redaction, and the places | ||
| that register secrets (config.resolve_api_key, AppState.resolve_session). | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import logging | ||
|
|
||
| import pytest | ||
| from typer.testing import CliRunner | ||
|
|
||
| from aai_cli import config, debuglog | ||
| from aai_cli import ws as wsutil | ||
| from aai_cli.context import AppState | ||
| from aai_cli.main import app | ||
|
|
||
| runner = CliRunner() | ||
|
|
||
|
|
||
| @pytest.fixture(autouse=True) | ||
| def reset_debuglog(monkeypatch): | ||
| # enable() mutates process-global state (the root logger and the module's | ||
| # verbosity/secret registries); snapshot and restore so pytest-randomly | ||
| # ordering can't leak a verbose run into unrelated tests. | ||
| root = logging.getLogger() | ||
| previous_handlers = list(root.handlers) | ||
| previous_level = root.level | ||
| # Logger levels are process-global too: any earlier test that exercised the | ||
| # realtime silencers left the websockets loggers clamped at CRITICAL, which | ||
| # would swallow the wire-level records asserted here. Reset them so these | ||
| # tests are order-independent under pytest-randomly, then restore. | ||
| wire_loggers = [logging.getLogger(name) for name in wsutil.WEBSOCKETS_LOGGERS] | ||
| previous_wire_levels = [logger.level for logger in wire_loggers] | ||
| for logger in wire_loggers: | ||
| logger.setLevel(logging.NOTSET) | ||
| monkeypatch.setattr(debuglog, "_verbosity", 0) | ||
| monkeypatch.setattr(debuglog, "_secrets", set()) | ||
| yield | ||
| root.handlers[:] = previous_handlers | ||
| root.setLevel(previous_level) | ||
| for logger, level in zip(wire_loggers, previous_wire_levels, strict=True): | ||
| logger.setLevel(level) | ||
|
|
||
|
|
||
| def test_enable_zero_is_the_everyday_no_op(): | ||
| root = logging.getLogger() | ||
| handlers_before = list(root.handlers) | ||
| level_before = root.level | ||
| debuglog.enable(0) | ||
| assert not debuglog.active() | ||
| assert root.handlers == handlers_before | ||
| assert root.level == level_before | ||
|
|
||
|
|
||
| def test_enable_one_logs_info_but_not_debug_to_stderr(capsys): | ||
| debuglog.enable(1) | ||
| assert debuglog.active() | ||
| assert logging.getLogger().level == logging.INFO | ||
| probe = logging.getLogger("aai_cli.test_probe") | ||
| probe.info("hello request log") | ||
| probe.debug("wire frame detail") | ||
| err = capsys.readouterr().err | ||
| assert "[aai_cli.test_probe] hello request log" in err | ||
| assert "wire frame detail" not in err | ||
|
|
||
|
|
||
| def test_enable_two_logs_wire_level_debug(capsys): | ||
| debuglog.enable(2) | ||
| assert logging.getLogger().level == logging.DEBUG | ||
| logging.getLogger("websockets.client").debug("> TEXT frame") | ||
| assert "[websockets.client] > TEXT frame" in capsys.readouterr().err | ||
|
|
||
|
|
||
| def test_registered_secrets_are_masked_in_every_record(capsys): | ||
| debuglog.enable(2) | ||
| debuglog.register_secret("sk_super_secret_value") | ||
| logging.getLogger("websockets.client").debug("authorization: sk_super_secret_value") | ||
| err = capsys.readouterr().err | ||
| assert "sk_super_secret_value" not in err | ||
| assert "authorization: [redacted]" in err | ||
|
|
||
|
|
||
| def test_register_secret_ignores_empty_values(): | ||
| debuglog.register_secret(None) | ||
| debuglog.register_secret("") | ||
| assert debuglog._secrets == set() | ||
|
|
||
|
|
||
| def test_resolve_api_key_registers_the_key_for_redaction(monkeypatch): | ||
| monkeypatch.setenv("ASSEMBLYAI_API_KEY", "sk_env_key_for_redaction") | ||
| assert config.resolve_api_key() == "sk_env_key_for_redaction" | ||
| assert "sk_env_key_for_redaction" in debuglog._secrets | ||
|
|
||
|
|
||
| def test_resolve_session_registers_the_jwt_for_redaction(monkeypatch): | ||
| monkeypatch.setattr(config, "get_session", lambda profile: {"jwt": "jwt_secret", "token": "t"}) | ||
| monkeypatch.setattr(config, "get_account_id", lambda profile: 42) | ||
| assert AppState().resolve_session() == (42, "jwt_secret") | ||
| assert "jwt_secret" in debuglog._secrets | ||
|
|
||
|
|
||
| def test_root_vv_flag_enables_wire_level_diagnostics(): | ||
| result = runner.invoke(app, ["-vv"]) # bare invocation: prints help and exits 0 | ||
| assert result.exit_code == 0 | ||
| assert debuglog.active() | ||
| assert logging.getLogger().level == logging.DEBUG | ||
|
|
||
|
|
||
| def test_root_v_flag_sets_info_and_logs_the_environment(caplog): | ||
| caplog.set_level(logging.DEBUG, logger="aai_cli") | ||
| result = runner.invoke(app, ["-v"]) | ||
| assert result.exit_code == 0 | ||
| assert logging.getLogger().level == logging.INFO | ||
| assert "environment" in caplog.text | ||
| assert "production" in caplog.text |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_secrets is a module-level mutable set that stores per-session/API secrets; it persists for the process lifetime and can leak sensitive values between requests or sessions.
Details
✨ AI Reasoning
A mutable module-level set is being used to hold sensitive values that are registered during normal command execution. In long-running processes or servers, that state will persist across independent requests or sessions and can accumulate or leak secrets between contexts, causing data leakage and cross-request coupling. The concern focuses on request/session-scoped secrets being stored in process-global mutable state.
🔧 How do I fix it?
Avoid storing request-specific data in module-level variables. Use request-scoped variables or explicitly mark shared caches as intentional.
Reply
@AikidoSec feedback: [FEEDBACK]to get better review comments in the future.Reply
@AikidoSec ignore: [REASON]to ignore this issue.More info
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@AikidoSec ignore: This is a short-lived single-user CLI process, not a long-running server — there are no cross-request or cross-session contexts to leak between. The set holds only the current invocation's own credentials (API key, session JWT), registered precisely so the logging formatter can redact them from
-v/--verbosediagnostic output, and it dies with the process. The process-global scope is intentional and mirrors the existingenvironments.pypattern in this codebase.Generated by Claude Code
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✅ Based on your feedback, we ignored this issue because of the following reason:
Generated by Claude Code