diff --git a/tests/tools/test_skills_tool.py b/tests/tools/test_skills_tool.py index 506a47eeb6..07b6b58e01 100644 --- a/tests/tools/test_skills_tool.py +++ b/tests/tools/test_skills_tool.py @@ -150,6 +150,31 @@ class TestRequiredEnvironmentVariablesNormalization: assert _is_env_var_persisted("EMPTY_HOST_KEY", {}) is False assert _is_env_var_persisted("FILLED_KEY", {}) is True + def test_active_profile_secret_scope_satisfies_requirement(self): + """Cron workers must accept a value hydrated from this profile's vault.""" + from agent import secret_scope + from tools.skills_tool import _is_env_var_persisted + + secret_scope.set_multiplex_active(True) + token = secret_scope.set_secret_scope({"VAULT_SKILL_API_KEY": "vault-value"}) + try: + assert _is_env_var_persisted("VAULT_SKILL_API_KEY", {}) is True + finally: + secret_scope.reset_secret_scope(token) + secret_scope.set_multiplex_active(False) + + def test_unscoped_multiplex_requirement_does_not_read_process_environment(self, monkeypatch): + """A worker without a profile scope must keep the fail-closed boundary.""" + from agent import secret_scope + from tools.skills_tool import _is_env_var_persisted + + monkeypatch.setenv("OTHER_PROFILE_SKILL_API_KEY", "other-profile-value") + secret_scope.set_multiplex_active(True) + try: + assert _is_env_var_persisted("OTHER_PROFILE_SKILL_API_KEY", {}) is False + finally: + secret_scope.set_multiplex_active(False) + # --------------------------------------------------------------------------- # _get_category_from_path diff --git a/tools/skills_tool_setup.py b/tools/skills_tool_setup.py index 243f7b0eb6..17d6dcf6ac 100644 --- a/tools/skills_tool_setup.py +++ b/tools/skills_tool_setup.py @@ -3,7 +3,6 @@ ``load_env``) stays there and is read lazily at call time so origin-module patches are honored.""" import logging -import os import re from enum import Enum from typing import Any, Dict, List @@ -126,8 +125,20 @@ def _is_gateway_surface() -> bool: def _is_env_var_persisted(var_name: str, env_snapshot: Dict[str, str]) -> bool: - """Set (non-empty) in the .env snapshot, else in the process environment.""" - return bool(env_snapshot[var_name] if var_name in env_snapshot else os.getenv(var_name)) + """Return whether a requirement is present in this profile's secret sources. + + The snapshot keeps the existing ``.env`` precedence. A miss must use + ``get_secret`` rather than reading ``os.environ`` directly so an active + multiplex worker can see its hydrated external secrets without seeing a + different profile's process environment. + """ + if var_name in env_snapshot: + return bool(env_snapshot[var_name]) + try: + from agent.secret_scope import UnscopedSecretError, get_secret + return bool(get_secret(var_name)) + except UnscopedSecretError: + return False def _build_setup_note(