From 2472be63447e6171b0ed9cfc05cebb9e47539b14 Mon Sep 17 00:00:00 2001 From: Jer Miller Date: Wed, 20 May 2026 13:39:39 -0600 Subject: [PATCH] fix(logging): silence httpx INFO to stop Gemini API key leak into observer logs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit httpx logs the full request URL at INFO; the Gemini API authenticates via ?key=AIzaSy..., so describe.log and transcribe.log on Google-provider installs were getting live keys written in cleartext on every API call. Set the httpx logger to WARNING at package import time so the level survives every downstream basicConfig() call across cortex, supervisor, link, talents, and the observer CLIs that go through setup_cli. Bumps version 0.3.7 -> 0.3.8 and adds a non-routine CHANGELOG entry (security / data-handling — gated for founder review at publish time per cmo/brand/changelog-voice.md sensitive-changes rule). Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 3 +++ pyproject.toml | 2 +- solstone/__init__.py | 8 +++++++ tests/test_httpx_logger.py | 45 ++++++++++++++++++++++++++++++++++++++ uv.lock | 2 +- 5 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 tests/test_httpx_logger.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f0fcf01f..8af5e6bee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ Format adapted from [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), al ## [Unreleased] +### Fixed +- if you connected a Gemini API key to solstone, that key was being written into your local `describe.log` and `transcribe.log` files on every request to Google's API. the logs are local to your install, but the key was on disk in the clear — anyone you'd shared a log file with for debugging could have read it. install this release if you've used the Google provider, and rotate your Gemini key in your Google AI Studio console. + ### Changed - Google cogitate agents now run through the Google GenAI SDK instead of a separate Gemini CLI process. - Google tool use is handled inside solstone, with the same read/write policy boundaries. diff --git a/pyproject.toml b/pyproject.toml index 73d0c6ff3..89bc39666 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "solstone" -version = "0.3.7" +version = "0.3.8" description = "Navigate Life Intelligently" readme = "README.md" requires-python = ">=3.11" diff --git a/solstone/__init__.py b/solstone/__init__.py index 0ca446aa4..dc88dc43c 100644 --- a/solstone/__init__.py +++ b/solstone/__init__.py @@ -2,3 +2,11 @@ # Copyright (c) 2026 sol pbc """solstone namespace package.""" + +import logging + +# httpx logs the full request URL at INFO; the Gemini API authenticates via +# `?key=AIzaSy...`, so INFO leaks live keys into describe.log / transcribe.log. +# Set the level on the named logger so it survives later basicConfig() calls +# from individual CLI entry points. +logging.getLogger("httpx").setLevel(logging.WARNING) diff --git a/tests/test_httpx_logger.py b/tests/test_httpx_logger.py new file mode 100644 index 000000000..b24042013 --- /dev/null +++ b/tests/test_httpx_logger.py @@ -0,0 +1,45 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright (c) 2026 sol pbc + +"""Guard against regressing the httpx URL-leak silencing. + +httpx logs the full request URL at INFO. Gemini API keys ride in `?key=...` +on every call, so any path that lets httpx INFO records reach a file handler +leaks live credentials. The package import in `solstone/__init__.py` sets the +httpx logger to WARNING; these tests assert that contract and that it +survives a later `logging.basicConfig(level=INFO)` call. +""" + +import logging + +import solstone # noqa: F401 — import for its side effect (logger config) + + +def test_httpx_logger_silenced_at_import(): + assert logging.getLogger("httpx").level == logging.WARNING + + +def test_httpx_info_records_suppressed_after_basicconfig(): + logging.basicConfig(level=logging.INFO) + httpx_logger = logging.getLogger("httpx") + assert httpx_logger.getEffectiveLevel() >= logging.WARNING + + records: list[logging.LogRecord] = [] + + class _Capture(logging.Handler): + def emit(self, record: logging.LogRecord) -> None: + records.append(record) + + handler = _Capture(level=logging.DEBUG) + httpx_logger.addHandler(handler) + try: + httpx_logger.info( + "HTTP Request: GET https://generativelanguage.googleapis.com/v1beta/models?key=AIzaSyTEST" + ) + httpx_logger.warning("connection error") + finally: + httpx_logger.removeHandler(handler) + + levels = [r.levelno for r in records] + assert logging.INFO not in levels + assert logging.WARNING in levels diff --git a/uv.lock b/uv.lock index fa8310025..b645c5d55 100644 --- a/uv.lock +++ b/uv.lock @@ -3223,7 +3223,7 @@ wheels = [ [[package]] name = "solstone" -version = "0.3.7" +version = "0.3.8" source = { editable = "." } dependencies = [ { name = "anthropic" }, -- 2.51.2