Keep credentials out of reprs; count only live ssh-agents in tests
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m43s

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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
This commit is contained in:
dongarandClaude Opus 5.5 committed 2026-10-02 22:41:01 +00:00
1 parent e02bac110d
commit d5cef02e89
5 files changed
+34 -9

No files matched your search

+5 -3
View File
@@ -233,12 +233,14 @@ class AgentConfig:
workspace: Path workspace: Path
project: str project: str
llm_base_url: 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 roles: RoleMap
matrix: MatrixConfig matrix: MatrixConfig
operators: frozenset[str] operators: frozenset[str]
gitea_url: str gitea_url: str
gitea_token: str gitea_token: str = field(repr=False)
gitea_repo: str gitea_repo: str
gitea_operators: frozenset[str] gitea_operators: frozenset[str]
build_host: BuildHost build_host: BuildHost
@@ -248,7 +250,7 @@ class AgentConfig:
# (push_user, push_token), or the path to a deploy key (push_key). # (push_user, push_token), or the path to a deploy key (push_key).
push_key: Path | None = None push_key: Path | None = None
push_user: str | 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 max_output_tokens: int = DEFAULT_MAX_OUTPUT_TOKENS
request_timeout_s: float = DEFAULT_REQUEST_TIMEOUT_S request_timeout_s: float = DEFAULT_REQUEST_TIMEOUT_S
tuning: Tuning = field(default_factory=Tuning) tuning: Tuning = field(default_factory=Tuning)
+4 -3
View File
@@ -18,7 +18,7 @@ Pull requests are opened from the agent's own host with its Gitea API token
""" """
import shlex import shlex
from dataclasses import dataclass from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from nisse_dev.execution import HostUnavailable, SSHHost from nisse_dev.execution import HostUnavailable, SSHHost
@@ -67,14 +67,15 @@ class HTTPSToken:
"""A push-only account's token, for HTTPS remotes (D4).""" """A push-only account's token, for HTTPS remotes (D4)."""
username: str 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) @dataclass(frozen=True)
class SSHKey: class SSHKey:
"""A deploy key's private half, for SSH remotes (D4).""" """A deploy key's private half, for SSH remotes (D4)."""
private_key: str private_key: str = field(repr=False)
PushCredential = HTTPSToken | SSHKey PushCredential = HTTPSToken | SSHKey
+2 -2
View File
@@ -16,7 +16,7 @@ import re
import threading import threading
import uuid import uuid
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass, field
from urllib.parse import unquote from urllib.parse import unquote
import httpx import httpx
@@ -40,7 +40,7 @@ class TransportError(RuntimeError):
class MatrixConfig: class MatrixConfig:
homeserver: str homeserver: str
user_id: str user_id: str
token: str token: str = field(repr=False) # never in a repr or a log line
room_id: str room_id: str
+7
View File
@@ -360,3 +360,10 @@ def test_a_push_token_needs_its_user_and_excludes_a_key() -> None:
build(gitea__push_user="pusher") build(gitea__push_user="pusher")
with pytest.raises(ConfigError, match="not both"): with pytest.raises(ConfigError, match="not both"):
build(gitea__push_user="p", gitea__push_token="t", gitea__push_key="~/k") 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
+16 -1
View File
@@ -135,10 +135,25 @@ def test_a_failed_push_still_kills_its_agent(
def _agents() -> set[str]: 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( out = subprocess.run(
["pgrep", "-x", "ssh-agent"], capture_output=True, text=True, check=False ["pgrep", "-x", "ssh-agent"], capture_output=True, text=True, check=False
).stdout ).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: def test_a_remote_url_is_quoted_not_interpreted(repo: GitRepo, tmp_path: Path) -> None: