Limit runtime secret redaction to credentials
Filter usr/.env masking to API keys and login/password credentials so ordinary settings cannot corrupt chat text. Keep global and project secrets protected, and cover full-response, prompt, and streaming behavior with a regression test.
Alessandro committed
Jul 19, 2026 at 13:22 UTC
fd795bda82167b2a3bf6ad4705218987073cd0d7
3 files changed
+35
-6
helpers/secrets.py
+14
@@ -17,6 +17,12 @@ if TYPE_CHECKING:
17
# New alias-based placeholder format §§secret(KEY)
18
ALIAS_PATTERN = r"§§secret\(([A-Za-z_][A-Za-z0-9_]*)\)"
19
DEFAULT_SECRETS_FILE = "usr/secrets.env"
20
+_RUNTIME_CREDENTIAL_KEYS = {
21
+ dotenv.KEY_AUTH_LOGIN,
22
+ dotenv.KEY_AUTH_PASSWORD,
23
+ dotenv.KEY_RFC_PASSWORD,
24
+ dotenv.KEY_ROOT_PASSWORD,
25
+}
26
27
28
def alias_for_key(key: str, placeholder: str = "§§secret({key})") -> str:
@@ -160,6 +166,14 @@ class SecretsManager:
166
content = ""
167
168
self._raw_snapshots[path] = content
169
+ if path == dotenv.get_dotenv_file_path():
170
+ content = "\n".join(
171
+ line.raw
172
+ for line in self.parse_env_lines(content)
173
+ if line.type != "pair"
174
+ or (line.key or "").upper().startswith("API_KEY_")
175
+ or (line.key or "").upper() in _RUNTIME_CREDENTIAL_KEYS
176
+ )
177
parts.append(content)
178
179
combined = "\n".join(parts)
helpers/secrets.py.dox.md
+3
-2
@@ -29,12 +29,12 @@
29
- `get_secrets_manager(context: 'AgentContext|None'=...) -> SecretsManager`
30
- `get_project_secrets_manager(project_name: str, merge_with_global: bool=...) -> SecretsManager`
31
- `get_default_secrets_manager() -> SecretsManager`
32
-- Notable constants/configuration names: `ALIAS_PATTERN`, `DEFAULT_SECRETS_FILE`.
32
+- Notable constants/configuration names: `ALIAS_PATTERN`, `DEFAULT_SECRETS_FILE`, `_RUNTIME_CREDENTIAL_KEYS`.
33
34
## Runtime Contracts
35
36
- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
37
-- The agent-facing `get_secrets_manager` masks and unpacks values from `usr/.env`, the global `usr/secrets.env`, and the active project's `secrets.env`; `get_default_secrets_manager` remains scoped to the single writable `usr/secrets.env` file.
37
+- The agent-facing `get_secrets_manager` masks and unpacks `API_KEY_*` and login/password credentials from `usr/.env`, every value from the global `usr/secrets.env`, and every value from the active project's `secrets.env`; ordinary runtime settings are not treated as secrets. `get_default_secrets_manager` remains scoped to the single writable `usr/secrets.env` file.
38
- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
39
- Observed side-effect areas: filesystem reads, filesystem writes, filesystem deletion, WebSocket state, settings/state persistence, secret handling.
40
- Imported dependency areas include: `dataclasses`, `dotenv.parser`, `helpers`, `helpers.errors`, `helpers.extension`, `io`, `os`, `re`, `threading`, `time`, `typing`.
@@ -54,6 +54,7 @@
54
55
- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
56
- Related tests observed by source search:
57
+ - `tests/test_secrets.py`
58
- `tests/test_plugin_scan_prompt.py`
59
- `tests/test_print_style.py`
60
- `tests/test_time_travel.py`
tests/test_secrets.py
+18
-4
@@ -6,18 +6,32 @@ class _Context:
6
return None
7
8
9
-def test_agent_secret_manager_masks_runtime_dotenv_values(monkeypatch):
9
+def test_agent_secret_manager_masks_runtime_credentials_only(monkeypatch):
10
monkeypatch.setattr(secrets.SecretsManager, "_instances", {})
11
monkeypatch.setattr(secrets.dotenv, "get_dotenv_file_path", lambda: "usr/.env")
12
13
contents = {
14
"usr/secrets.env": "PROJECT_SECRET=project-value\n",
15
- "usr/.env": "LLM_API_KEY=llm-secret-value\n",
15
+ "usr/.env": (
16
+ "API_KEY_OPENAI=llm-secret-value\n"
17
+ "ANONYMIZED_TELEMETRY=false\n"
18
+ "DEFAULT_USER_TIMEZONE=Europe/Rome\n"
19
+ ),
20
}
21
monkeypatch.setattr(secrets.files, "read_file", contents.__getitem__)
22
23
manager = secrets.get_secrets_manager(_Context())
24
21
- assert manager.mask_values("key=llm-secret-value") == (
22
- "key=§§secret(LLM_API_KEY)"
25
+ assert manager.mask_values(
26
+ "project-value; key=llm-secret-value; avoid falsely accusing a utility"
27
+ ) == (
28
+ "§§secret(PROJECT_SECRET); key=§§secret(API_KEY_OPENAI); "
29
+ "avoid falsely accusing a utility"
30
)
31
+ assert "ANONYMIZED_TELEMETRY" not in manager.get_secrets_for_prompt()
32
+
33
+ stream_filter = manager.create_streaming_filter()
34
+ assert stream_filter.process_chunk("avoid falsely accusing a utility") == (
35
+ "avoid falsely accusing a utility"
36
+ )
37
+ assert stream_filter.finalize() == ""