From d5cef02e89ca6b5e00d2042b0967b46aa3f2f21e Mon Sep 17 00:00:00 2001 From: Don Garrett Date: Fri, 2 Oct 2026 22:41:01 +0000 Subject: [PATCH] Keep credentials out of reprs; count only live ssh-agents in tests A failing CI test printed a test key through SSHKey's default repr. Push credentials, AgentConfig's tokens and MatrixConfig's token are now excluded from their reprs, so no failure or log line can print them. In CI's container killed ssh-agents linger as zombies, so the tests count only live ones. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE --- nisse-dev/src/nisse_dev/config.py | 8 +++++--- nisse-dev/src/nisse_dev/git.py | 7 ++++--- nisse-dev/src/nisse_dev/matrix.py | 4 ++-- nisse-dev/tests/test_config.py | 7 +++++++ nisse-dev/tests/test_git.py | 17 ++++++++++++++++- 5 files changed, 34 insertions(+), 9 deletions(-) diff --git a/nisse-dev/src/nisse_dev/config.py b/nisse-dev/src/nisse_dev/config.py index e4d414c..a5c7919 100644 --- a/nisse-dev/src/nisse_dev/config.py +++ b/nisse-dev/src/nisse_dev/config.py @@ -233,12 +233,14 @@ class AgentConfig: workspace: Path project: str llm_base_url: str - llm_token: str + # Credentials never appear in a repr: a failing test or a log line + # would print them. + llm_token: str = field(repr=False) roles: RoleMap matrix: MatrixConfig operators: frozenset[str] gitea_url: str - gitea_token: str + gitea_token: str = field(repr=False) gitea_repo: str gitea_operators: frozenset[str] build_host: BuildHost @@ -248,7 +250,7 @@ class AgentConfig: # (push_user, push_token), or the path to a deploy key (push_key). push_key: Path | None = None push_user: str | None = None - push_token: str | None = None + push_token: str | None = field(default=None, repr=False) max_output_tokens: int = DEFAULT_MAX_OUTPUT_TOKENS request_timeout_s: float = DEFAULT_REQUEST_TIMEOUT_S tuning: Tuning = field(default_factory=Tuning) diff --git a/nisse-dev/src/nisse_dev/git.py b/nisse-dev/src/nisse_dev/git.py index 56f5596..8b0203b 100644 --- a/nisse-dev/src/nisse_dev/git.py +++ b/nisse-dev/src/nisse_dev/git.py @@ -18,7 +18,7 @@ Pull requests are opened from the agent's own host with its Gitea API token """ import shlex -from dataclasses import dataclass +from dataclasses import dataclass, field from pathlib import Path from nisse_dev.execution import HostUnavailable, SSHHost @@ -67,14 +67,15 @@ class HTTPSToken: """A push-only account's token, for HTTPS remotes (D4).""" username: str - token: str + # Never in a repr: a failing test or a log line would print it. + token: str = field(repr=False) @dataclass(frozen=True) class SSHKey: """A deploy key's private half, for SSH remotes (D4).""" - private_key: str + private_key: str = field(repr=False) PushCredential = HTTPSToken | SSHKey diff --git a/nisse-dev/src/nisse_dev/matrix.py b/nisse-dev/src/nisse_dev/matrix.py index ca94ce9..76ee235 100644 --- a/nisse-dev/src/nisse_dev/matrix.py +++ b/nisse-dev/src/nisse_dev/matrix.py @@ -16,7 +16,7 @@ import re import threading import uuid from collections.abc import Callable -from dataclasses import dataclass +from dataclasses import dataclass, field from urllib.parse import unquote import httpx @@ -40,7 +40,7 @@ class TransportError(RuntimeError): class MatrixConfig: homeserver: str user_id: str - token: str + token: str = field(repr=False) # never in a repr or a log line room_id: str diff --git a/nisse-dev/tests/test_config.py b/nisse-dev/tests/test_config.py index 4b3943f..596d5e2 100644 --- a/nisse-dev/tests/test_config.py +++ b/nisse-dev/tests/test_config.py @@ -360,3 +360,10 @@ def test_a_push_token_needs_its_user_and_excludes_a_key() -> None: build(gitea__push_user="pusher") with pytest.raises(ConfigError, match="not both"): build(gitea__push_user="p", gitea__push_token="t", gitea__push_key="~/k") + + +def test_no_credential_appears_in_the_config_repr() -> None: + config = build(gitea__push_user="pusher", gitea__push_token="${TEST_GITEA_TOKEN}") + text = repr(config) + for secret in SECRETS.values(): + assert secret not in text diff --git a/nisse-dev/tests/test_git.py b/nisse-dev/tests/test_git.py index 369b23a..1a81524 100644 --- a/nisse-dev/tests/test_git.py +++ b/nisse-dev/tests/test_git.py @@ -135,10 +135,25 @@ def test_a_failed_push_still_kills_its_agent( def _agents() -> set[str]: + """Live ssh-agents. A killed one lingers as a zombie in a CI container, + where nothing reaps orphans, so zombies don't count.""" out = subprocess.run( ["pgrep", "-x", "ssh-agent"], capture_output=True, text=True, check=False ).stdout - return set(out.split()) + live = set() + for pid in out.split(): + try: + stat = Path(f"/proc/{pid}/stat").read_text() + except FileNotFoundError: + continue + if stat.rsplit(")", 1)[1].split()[0] != "Z": + live.add(pid) + return live + + +def test_credentials_never_appear_in_a_repr() -> None: + assert "s3cr3t" not in repr(HTTPSToken("pusher", "s3cr3t")) + assert "PRIVATE" not in repr(SSHKey("-----BEGIN OPENSSH PRIVATE KEY-----")) def test_a_remote_url_is_quoted_not_interpreted(repo: GitRepo, tmp_path: Path) -> None: