@@ -122,6 +122,15 @@ def test_llm_agent_settings_export_schema_groups_sections() -> None:
122122 is SettingProminence .CRITICAL
123123 )
124124
125+ # The critic API key must surface in the schema as a CRITICAL, secret
126+ # field that depends on critic_enabled — this is what the GUI uses to
127+ # render a masked input gated on the toggle.
128+ critic_api_key = v_fields ["verification.critic_api_key" ]
129+ assert critic_api_key .secret is True
130+ assert critic_api_key .value_type == "string"
131+ assert critic_api_key .prominence is SettingProminence .CRITICAL
132+ assert critic_api_key .depends_on == ["verification.critic_enabled" ]
133+
125134
126135def test_acp_agent_settings_export_schema_has_acp_section () -> None :
127136 schema = ACPAgentSettings .export_schema ()
@@ -554,6 +563,77 @@ def test_llm_create_agent_no_critic_without_api_key() -> None:
554563 assert agent .critic is None
555564
556565
566+ def test_llm_create_agent_critic_uses_explicit_api_key () -> None :
567+ """When ``verification.critic_api_key`` is set, the critic authenticates
568+ with it instead of the LLM key. The LLM's own key is preserved untouched
569+ so the main agent loop still talks to its provider."""
570+ settings = OpenHandsAgentSettings (
571+ llm = LLM (model = "m" , api_key = SecretStr ("llm-key" )),
572+ verification = VerificationSettings (
573+ critic_enabled = True ,
574+ critic_api_key = SecretStr ("critic-key" ),
575+ ),
576+ )
577+ agent = settings .create_agent ()
578+ assert isinstance (agent .critic , APIBasedCritic )
579+ assert isinstance (agent .critic .api_key , SecretStr )
580+ assert agent .critic .api_key .get_secret_value () == "critic-key"
581+ # LLM key unaffected.
582+ assert isinstance (agent .llm .api_key , SecretStr )
583+ assert agent .llm .api_key .get_secret_value () == "llm-key"
584+
585+
586+ def test_llm_create_agent_critic_falls_back_to_llm_api_key () -> None :
587+ """Without ``verification.critic_api_key``, the legacy behavior holds:
588+ the critic reuses the LLM key (auto-config path for the All-Hands proxy)."""
589+ settings = OpenHandsAgentSettings (
590+ llm = LLM (model = "m" , api_key = SecretStr ("llm-key" )),
591+ verification = VerificationSettings (critic_enabled = True ),
592+ )
593+ agent = settings .create_agent ()
594+ assert isinstance (agent .critic , APIBasedCritic )
595+ assert isinstance (agent .critic .api_key , SecretStr )
596+ assert agent .critic .api_key .get_secret_value () == "llm-key"
597+
598+
599+ def test_llm_create_agent_critic_with_only_critic_api_key () -> None :
600+ """If the LLM has no key but ``critic_api_key`` is supplied, the critic
601+ is still built — its credential is independent of the LLM's."""
602+ settings = OpenHandsAgentSettings (
603+ llm = LLM (model = "m" , api_key = None ),
604+ verification = VerificationSettings (
605+ critic_enabled = True ,
606+ critic_api_key = SecretStr ("critic-only-key" ),
607+ ),
608+ )
609+ agent = settings .create_agent ()
610+ assert isinstance (agent .critic , APIBasedCritic )
611+ assert isinstance (agent .critic .api_key , SecretStr )
612+ assert agent .critic .api_key .get_secret_value () == "critic-only-key"
613+
614+
615+ def test_verification_settings_critic_api_key_roundtrip () -> None :
616+ """``critic_api_key`` survives dump → validate when secrets are exposed,
617+ and validates from both plain strings and SecretStr inputs."""
618+ settings = VerificationSettings (
619+ critic_enabled = True ,
620+ critic_api_key = "plain-string-key" ,
621+ )
622+ assert isinstance (settings .critic_api_key , SecretStr )
623+ assert settings .critic_api_key .get_secret_value () == "plain-string-key"
624+
625+ dumped = settings .model_dump (context = {"expose_secrets" : "plaintext" })
626+ assert dumped ["critic_api_key" ] == "plain-string-key"
627+
628+ restored = VerificationSettings .model_validate (dumped )
629+ assert isinstance (restored .critic_api_key , SecretStr )
630+ assert restored .critic_api_key .get_secret_value () == "plain-string-key"
631+
632+ # Empty strings normalize to None (consistent with LLM.api_key handling).
633+ empty = VerificationSettings (critic_enabled = True , critic_api_key = "" )
634+ assert empty .critic_api_key is None
635+
636+
557637def test_llm_create_agent_critic_with_iterative_refinement () -> None :
558638 settings = OpenHandsAgentSettings (
559639 llm = LLM (model = "m" , api_key = SecretStr ("k" )),
0 commit comments