diff --git a/openhands-sdk/openhands/sdk/agent/base.py b/openhands-sdk/openhands/sdk/agent/base.py index c2a33a69ad..16736a84d2 100644 --- a/openhands-sdk/openhands/sdk/agent/base.py +++ b/openhands-sdk/openhands/sdk/agent/base.py @@ -144,13 +144,14 @@ class AgentBase(DiscriminatedUnionMixin, ABC): "- An absolute path (e.g., '/path/to/custom_prompt.j2')" ), ) - security_policy_filename: str = Field( + security_policy_filename: str | None = Field( default="security_policy.j2", description=( - "Security policy template filename. Can be either:\n" + "Security policy template filename. Can be:\n" "- A relative filename (e.g., 'security_policy.j2') loaded from the " "agent's prompts directory\n" - "- An absolute path (e.g., '/path/to/custom_security_policy.j2')" + "- An absolute path (e.g., '/path/to/custom_security_policy.j2')\n" + "- Empty string or None to disable security policy" ), ) system_prompt_kwargs: dict[str, object] = Field( diff --git a/openhands-sdk/openhands/sdk/agent/prompts/system_prompt.j2 b/openhands-sdk/openhands/sdk/agent/prompts/system_prompt.j2 index fdd4626c39..220d19c29a 100644 --- a/openhands-sdk/openhands/sdk/agent/prompts/system_prompt.j2 +++ b/openhands-sdk/openhands/sdk/agent/prompts/system_prompt.j2 @@ -76,7 +76,9 @@ You are OpenHands agent, a helpful AI assistant that can interact with a compute +{% if security_policy_filename %} {% include security_policy_filename %} +{% endif %} {% if llm_security_analyzer %} diff --git a/openhands-sdk/openhands/sdk/conversation/impl/remote_conversation.py b/openhands-sdk/openhands/sdk/conversation/impl/remote_conversation.py index 7398ec7ab7..2dec73d91d 100644 --- a/openhands-sdk/openhands/sdk/conversation/impl/remote_conversation.py +++ b/openhands-sdk/openhands/sdk/conversation/impl/remote_conversation.py @@ -1169,7 +1169,11 @@ def set_confirmation_policy(self, policy: ConfirmationPolicyBase) -> None: def set_security_analyzer(self, analyzer: SecurityAnalyzerBase | None) -> None: """Set the security analyzer for the remote conversation.""" - payload = {"security_analyzer": analyzer.model_dump() if analyzer else analyzer} + payload = { + "security_analyzer": analyzer.model_dump(mode="json") + if analyzer + else analyzer + } _send_request( self._client, "POST", diff --git a/openhands-sdk/openhands/sdk/security/grayswan/analyzer.py b/openhands-sdk/openhands/sdk/security/grayswan/analyzer.py index a4ce518f3e..47d4af2d73 100644 --- a/openhands-sdk/openhands/sdk/security/grayswan/analyzer.py +++ b/openhands-sdk/openhands/sdk/security/grayswan/analyzer.py @@ -91,34 +91,25 @@ def validate_thresholds(self) -> GraySwanAnalyzer: def model_post_init(self, __context: Any) -> None: """Initialize the analyzer after model creation.""" - # Resolve API key from environment if not provided - if self.api_key is None: - env_key = os.getenv("GRAYSWAN_API_KEY") - if env_key: - self.api_key = SecretStr(env_key) - logger.debug( - "API key resolved from GRAYSWAN_API_KEY environment variable" - ) - - if not self.api_key: - # Design choice: Graceful degradation instead of fail-fast. - # This allows deployment without API key configured, returning UNKNOWN - # risk for all analyses. This is intentional to support environments - # where security analysis is optional or configured later. + # ALWAYS prefer environment variable - this ensures Docker gets the correct key + # even if serialization didn't work properly + env_key = os.getenv("GRAYSWAN_API_KEY") + if env_key: + self.api_key = SecretStr(env_key) + logger.info("Using GraySwan API key from environment") + elif not self.api_key or not self.api_key.get_secret_value(): logger.warning( "GRAYSWAN_API_KEY not set. GraySwanAnalyzer will return UNKNOWN risk." ) - # Resolve policy ID from environment if not provided - if self.policy_id is None: - self.policy_id = os.getenv("GRAYSWAN_POLICY_ID") - - if not self.policy_id: - # Use GraySwan default coding agent policy + # Always prefer environment variable for policy ID too + env_policy = os.getenv("GRAYSWAN_POLICY_ID") + if env_policy: + self.policy_id = env_policy + logger.info(f"Using GraySwan policy ID from environment: {self.policy_id}") + elif not self.policy_id: self.policy_id = "689ca4885af3538a39b2ba04" logger.info(f"Using default GraySwan policy ID: {self.policy_id}") - else: - logger.info(f"Using GraySwan policy ID from environment: {self.policy_id}") logger.info( f"GraySwanAnalyzer initialized with history_limit={self.history_limit}, "