Mask runtime .env values in agent output
Include usr/.env in the agent-facing SecretsManager sources so runtime credentials are masked in tool results, history, logs, and streamed output. Keep the writable default secrets manager scoped to usr/secrets.env. Add regression coverage for dotenv masking and document the source contract.
Alessandro committed
Jul 17, 2026 at 16:23 UTC
55456df29443b1fb62ba072a611e3a320f667a4c
3 files changed
+29
-5
helpers/secrets.py
+4
-4
@@ -7,7 +7,7 @@ from dataclasses import dataclass
7
from typing import Dict, Optional, List, Literal, Set, Callable, Tuple, TYPE_CHECKING
8
from dotenv.parser import parse_stream
9
from helpers.errors import RepairableException
10
-from helpers import files
10
+from helpers import dotenv, files
11
from helpers.extension import extensible
12
13
if TYPE_CHECKING:
@@ -509,8 +509,8 @@ class SecretsManager:
509
def get_secrets_manager(context: "AgentContext|None" = None) -> SecretsManager:
510
from helpers import projects
511
512
- # default secrets file
513
- secret_files = [DEFAULT_SECRETS_FILE]
512
+ # Agent-facing masking covers the secret store and runtime credentials in usr/.env.
513
+ secret_files = [DEFAULT_SECRETS_FILE, dotenv.get_dotenv_file_path()]
514
515
# use AgentContext from contextvars if no context provided
516
if not context:
@@ -542,4 +542,4 @@ def get_project_secrets_manager(project_name: str, merge_with_global: bool = Fal
542
543
@extensible
544
def get_default_secrets_manager() -> SecretsManager:
545
- return SecretsManager.get_instance()
\ No newline at end of file
545
+ return SecretsManager.get_instance()
helpers/secrets.py.dox.md
+2
-1
@@ -34,13 +34,14 @@
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.
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`.
41
42
## Key Concepts
43
43
-- Important called helpers/classes observed in the source: `key.upper`, `placeholder.format`, `SecretsManager.get_instance`, `self._replace_full_values`, `self._longest_suffix_prefix`, `threading.RLock`, `join`, `files.write_file`, `self._invalidate_all_caches`, `self.load_secrets`, `self.read_secrets_raw`, `self.parse_env_lines`, `self._serialize_env_lines`, `StreamingSecretsFilter`, `re.sub`, `self.parse_env_content`, `parse_stream`, `AgentContext.current`, `projects.get_context_project_name`, `files.get_abs_path`.
44
+- Important called helpers/classes observed in the source: `key.upper`, `placeholder.format`, `SecretsManager.get_instance`, `self._replace_full_values`, `self._longest_suffix_prefix`, `threading.RLock`, `join`, `files.write_file`, `self._invalidate_all_caches`, `self.load_secrets`, `self.read_secrets_raw`, `self.parse_env_lines`, `self._serialize_env_lines`, `StreamingSecretsFilter`, `re.sub`, `self.parse_env_content`, `parse_stream`, `AgentContext.current`, `projects.get_context_project_name`, `files.get_abs_path`, `dotenv.get_dotenv_file_path`.
45
- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
46
47
## Work Guidance
tests/test_secrets.py
new
+23
@@ -0,0 +1,23 @@
1
+from helpers import secrets
2
+
3
+
4
+class _Context:
5
+ def get_data(self, key: str):
6
+ return None
7
+
8
+
9
+def test_agent_secret_manager_masks_runtime_dotenv_values(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",
16
+ }
17
+ monkeypatch.setattr(secrets.files, "read_file", contents.__getitem__)
18
+
19
+ manager = secrets.get_secrets_manager(_Context())
20
+
21
+ assert manager.mask_values("key=llm-secret-value") == (
22
+ "key=§§secret(LLM_API_KEY)"
23
+ )