f281fa3b24
CI / lint (pull_request) Successful in 35s
CI / typecheck (pull_request) Successful in 50s
CI / security (pull_request) Successful in 50s
CI / quality (pull_request) Successful in 32s
CI / unit_tests (pull_request) Successful in 3m32s
CI / integration_tests (pull_request) Successful in 59s
CI / build (pull_request) Successful in 34s
CI / lint (push) Successful in 33s
CI / build (push) Successful in 37s
CI / quality (push) Successful in 48s
CI / typecheck (push) Successful in 48s
CI / security (push) Successful in 1m4s
CI / integration_tests (push) Successful in 1m18s
CI / coverage (pull_request) Successful in 3m34s
CI / status-check (pull_request) Successful in 3s
CI / unit_tests (push) Successful in 3m45s
CI / coverage (push) Successful in 3m34s
CI / status-check (push) Successful in 3s
Implements Wave 3 of the cleveractors-core integration (ADR-2026, ADR-2028). ## AgentFactory - Added optional credentials: dict[str, dict[str, str]] | None parameter to __init__. Stored as self.credentials. - When creating LLMAgent instances, the full credentials dict is forwarded via a new credentials constructor kwarg, threading per-request API keys without touching the stored actor config_dict. - Added explicit type annotation ReactiveAgentFactory: type[AgentFactory]. - _create_agent_instance raises ConfigurationError when credentials is not None but does not contain an entry for the requested provider (AC4 compliance). ## LLMAgent - Added optional credentials: dict[str, str] | None parameter to __init__. Stored as self._credentials; config_dict is never modified. - LangChain client construction deferred to first access (lazy init) via a chat_model property backed by _ensure_chat_model(). The property setter allows test-injection of mock models without going through the lazy-init path. - _create_chat_model() dispatches to credential-injection / standalone paths. - Extracted to llm_client.py and llm_providers.py for modularity. - Defined shared module-level constants DEFAULT_MODEL, DEFAULT_TEMPERATURE, DEFAULT_MAX_TOKENS to eliminate duplicated magic numbers across llm.py, runtime.py, and factory.py. - Added public credentials property exposing self._credentials. - Added type narrowing guard in chat_model property to satisfy static analysis. - Added temperature_override type validation in process_message. - Guarded cleanup() null-assignment with _chat_model_lock (TOCTOU fix). ## SSRF Prevention (ADR-2028) - _validate_base_url(): https-only, userinfo rejection, localhost rejection, raw IP literal rejection, numeric-hostname (dotted-hex/octal/compact) rejection, percent-decode loop with iteration cap (10), trailing-dot FQDN strip. - DNS resolution intentionally omitted (blocking call in async path). - Canonical URL reconstruction uses lowercased scheme. ## Credential Leak Prevention - All logger.exception(...) replaced with logger.debug(..., exc_info=True). - Error messages use type(e).__name__ instead of str(e). - process_message exception chains broken with from None to prevent LangChain authentication errors from leaking via __cause__. ## Runtime fixes - Executor._execute_llm: shallow copy for setdefault mutations. - Executor._execute_graph: restructured try/finally for resource cleanup. - Uses DEFAULT_MODEL, DEFAULT_TEMPERATURE, DEFAULT_MAX_TOKENS from llm.py. - Defensive copy of credentials dict in Executor.__init__ to prevent caller mutation after construction. ## Test improvements - Added BDD scenarios for llm_imports.py ImportError fallback branches using sys.modules manipulation with a custom import hook, exercised via subprocess. - Added BDD scenario for AgentFactory.create_agents_from_config() credential threading to all created agents. - Removed tautological dataclass test scenarios (NodeUsage, ActorResult) from runtime_coverage.feature. - Fixed misleading scenario title in runtime_coverage.feature (exception chain suppressed, not chained) and updated step to assert __cause__ is None. - Added CHANGELOG entry for ReactiveAgentFactory backward-compatibility alias. - Removed duplicate log emission in _check_known_provider_domain. ISSUES CLOSED: #12
201 lines
7.3 KiB
Python
201 lines
7.3 KiB
Python
"""Step definitions for composite agent credential threading.
|
|
|
|
Extracted from ``credential_injection_steps.py`` to keep that file
|
|
under 500 lines (CONTRIBUTING.md §General Principles).
|
|
|
|
Covers the ``components``-based composite path and the legacy
|
|
``"agents": [...]``-style composite path.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from behave import given, then, when
|
|
from features.mocks.credential_helpers import make_template_renderer
|
|
|
|
from cleveractors.agents.factory import AgentFactory
|
|
from cleveractors.agents.llm import LLMAgent
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Composite agent credential threading (m8)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a composite agent config with a nested LLM component for credential test")
|
|
def step_composite_agent_config_for_creds(context: Any) -> None:
|
|
"""Set up a composite agent config with a nested LLM component."""
|
|
context.actor_config = {
|
|
"agents": {
|
|
"my_composite": {
|
|
"type": "composite",
|
|
"config": {
|
|
"components": {
|
|
"agents": {
|
|
"nested_llm": {
|
|
"type": "llm",
|
|
"config": {
|
|
"provider": "openai",
|
|
"model": "gpt-3.5-turbo",
|
|
},
|
|
}
|
|
}
|
|
}
|
|
},
|
|
}
|
|
}
|
|
}
|
|
context.template_renderer = make_template_renderer()
|
|
context.credentials = {"openai": {"api_key": "composite-injected-key"}}
|
|
context.factory = AgentFactory(
|
|
config=context.actor_config,
|
|
template_renderer=context.template_renderer,
|
|
credentials=context.credentials,
|
|
)
|
|
|
|
|
|
@when("I call create_agent for the composite agent in credential test")
|
|
def step_create_composite_agent_for_creds(context: Any) -> None:
|
|
"""Create the composite agent via the factory."""
|
|
context.created_agent = context.factory.create_agent("my_composite")
|
|
|
|
|
|
@then("the nested LLM agent within the composite should have the credentials dict set")
|
|
def step_nested_llm_has_credentials(context: Any) -> None:
|
|
"""Verify the nested LLM agent received the provider-specific credentials slice."""
|
|
composite = context.created_agent
|
|
assert hasattr(composite, "agents"), (
|
|
"Composite agent should have 'agents' attribute"
|
|
)
|
|
agents = composite.agents
|
|
assert "nested_llm" in agents, (
|
|
f"Expected 'nested_llm' in composite agents, got {list(agents.keys())}"
|
|
)
|
|
nested = agents["nested_llm"]
|
|
assert isinstance(nested, LLMAgent), (
|
|
f"Expected LLMAgent, got {type(nested).__name__}"
|
|
)
|
|
# Factory extracts provider-specific slice: credentials.get("openai")
|
|
assert nested._credentials == {"api_key": "composite-injected-key"}, (
|
|
f"Expected _credentials {{'api_key': 'composite-injected-key'!r}}, "
|
|
f"got {nested._credentials!r}"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Legacy composite agent credential threading (m2)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a legacy composite agent config with nested agents list for credential test")
|
|
def step_legacy_composite_agent_config_for_creds(context: Any) -> None:
|
|
"""Set up a legacy composite agent config using the ``"agents": [...]`` format.
|
|
|
|
This tests the fix for the legacy composite path where the cache is bypassed
|
|
when credentials are set (factory.py:227-233).
|
|
"""
|
|
context.actor_config = {
|
|
"agents": {
|
|
"my_legacy_composite": {
|
|
"type": "composite",
|
|
"config": {
|
|
"agents": ["nested_llm"],
|
|
},
|
|
},
|
|
"nested_llm": {
|
|
"type": "llm",
|
|
"config": {
|
|
"provider": "openai",
|
|
"model": "gpt-3.5-turbo",
|
|
},
|
|
},
|
|
}
|
|
}
|
|
context.template_renderer = make_template_renderer()
|
|
context.credentials = {"openai": {"api_key": "legacy-composite-key"}}
|
|
context.factory = AgentFactory(
|
|
config=context.actor_config,
|
|
template_renderer=context.template_renderer,
|
|
credentials=context.credentials,
|
|
)
|
|
|
|
|
|
@when("I call create_agent for the legacy composite agent in credential test")
|
|
def step_create_legacy_composite_agent_for_creds(context: Any) -> None:
|
|
"""Create the legacy composite agent via the factory."""
|
|
context.created_agent = context.factory.create_agent("my_legacy_composite")
|
|
|
|
|
|
@then(
|
|
"the nested LLM agent within the legacy composite should have the credentials dict set"
|
|
)
|
|
def step_nested_legacy_llm_has_credentials(context: Any) -> None:
|
|
"""Verify the nested LLM agent (legacy path) received the factory credentials."""
|
|
composite = context.created_agent
|
|
assert hasattr(composite, "agents"), (
|
|
"Composite agent should have 'agents' attribute"
|
|
)
|
|
agents = composite.agents
|
|
assert "nested_llm" in agents, (
|
|
f"Expected 'nested_llm' in legacy composite agents, got {list(agents.keys())}"
|
|
)
|
|
nested = agents["nested_llm"]
|
|
assert isinstance(nested, LLMAgent), (
|
|
f"Expected LLMAgent, got {type(nested).__name__}"
|
|
)
|
|
# Factory extracts provider-specific slice: credentials.get("openai")
|
|
assert nested._credentials == {"api_key": "legacy-composite-key"}, (
|
|
f"Expected _credentials {{'api_key': 'legacy-composite-key'!r}}, "
|
|
f"got {nested._credentials!r}"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# m6: legacy composite path without credentials
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a legacy composite agent config with nested agents list without credentials")
|
|
def step_legacy_composite_no_credentials(context: Any) -> None:
|
|
"""Set up a legacy composite agent config without a credentials dict.
|
|
|
|
Tests that the legacy composite path works when credentials is None
|
|
(no credential injection needed).
|
|
"""
|
|
context.actor_config = {
|
|
"agents": {
|
|
"my_legacy_composite": {
|
|
"type": "composite",
|
|
"config": {
|
|
"agents": ["nested_llm"],
|
|
},
|
|
},
|
|
"nested_llm": {
|
|
"type": "llm",
|
|
"config": {
|
|
"provider": "openai",
|
|
"model": "gpt-3.5-turbo",
|
|
},
|
|
},
|
|
}
|
|
}
|
|
context.template_renderer = make_template_renderer()
|
|
context.factory = AgentFactory(
|
|
config=context.actor_config,
|
|
template_renderer=context.template_renderer,
|
|
)
|
|
|
|
|
|
@then(
|
|
"the nested LLM agent within the legacy composite should have credentials "
|
|
"equal to None"
|
|
)
|
|
def step_nested_legacy_llm_credentials_none(context: Any) -> None:
|
|
"""Verify the nested LLM agent (legacy path) has credentials=None."""
|
|
composite = context.created_agent
|
|
agents = composite.agents
|
|
nested = agents["nested_llm"]
|
|
assert nested._credentials is None, (
|
|
f"Expected _credentials to be None, got {type(nested._credentials).__name__}"
|
|
)
|