agentprobe-testing 1.5.0__tar.gz → 1.5.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. {agentprobe_testing-1.5.0/agentprobe_testing.egg-info → agentprobe_testing-1.5.1}/PKG-INFO +1 -1
  2. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/__init__.py +1 -1
  3. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/agents/target_agent.py +3 -1
  4. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/classifier.py +3 -1
  5. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/domains/access_control/agent.py +3 -1
  6. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/domains/access_control/complex_agent.py +3 -1
  7. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/domains/tau_retail/user_sim.py +2 -1
  8. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/domains/web_browsing/agent.py +3 -1
  9. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/injector.py +3 -1
  10. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/llm.py +32 -0
  11. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/registry.py +13 -4
  12. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/summarize.py +2 -1
  13. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/validate_scenarios.py +3 -1
  14. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1/agentprobe_testing.egg-info}/PKG-INFO +1 -1
  15. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/pyproject.toml +1 -1
  16. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/tests/test_llm.py +44 -0
  17. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/tests/test_registry.py +15 -0
  18. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/LICENSE +0 -0
  19. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/README.md +0 -0
  20. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/agents/__init__.py +0 -0
  21. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/agents/base.py +0 -0
  22. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/agents/browser_agent.py +0 -0
  23. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/agents/rule_based.py +0 -0
  24. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/agents/scripted.py +0 -0
  25. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/agreement.py +0 -0
  26. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/cli.py +0 -0
  27. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/diff.py +0 -0
  28. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/domain.py +0 -0
  29. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/domains/__init__.py +0 -0
  30. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/domains/access_control/__init__.py +0 -0
  31. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/domains/access_control/clean.py +0 -0
  32. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/domains/access_control/decoy.py +0 -0
  33. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/domains/access_control/domain.py +0 -0
  34. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/domains/access_control/entities.py +0 -0
  35. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/domains/access_control/injector_prompt.py +0 -0
  36. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/domains/access_control/rule_based_agent.py +0 -0
  37. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/domains/access_control/scenarios.py +0 -0
  38. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/domains/access_control/split.py +0 -0
  39. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/domains/access_control/tools.py +0 -0
  40. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/domains/access_control/trap.py +0 -0
  41. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/domains/tau_retail/__init__.py +0 -0
  42. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/domains/tau_retail/chat_agent.py +0 -0
  43. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/domains/tau_retail/domain.py +0 -0
  44. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/domains/tau_retail/injector_prompt.py +0 -0
  45. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/domains/tau_retail/loader.py +0 -0
  46. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/domains/tau_retail/openai_compat_agent.py +0 -0
  47. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/domains/tau_retail/oracle.py +0 -0
  48. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/domains/tau_retail/tools.py +0 -0
  49. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/domains/tau_retail/world.py +0 -0
  50. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/domains/web_browsing/__init__.py +0 -0
  51. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/domains/web_browsing/clean.py +0 -0
  52. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/domains/web_browsing/domain.py +0 -0
  53. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/domains/web_browsing/entities.py +0 -0
  54. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/domains/web_browsing/injector_prompt.py +0 -0
  55. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/domains/web_browsing/rule_based_agent.py +0 -0
  56. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/domains/web_browsing/scenarios.py +0 -0
  57. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/domains/web_browsing/tools.py +0 -0
  58. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/domains/web_browsing/trap.py +0 -0
  59. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/feedback.py +0 -0
  60. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/generic_world.py +0 -0
  61. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/injection.py +0 -0
  62. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/playbook.py +0 -0
  63. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/priority.py +0 -0
  64. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/quickstart.py +0 -0
  65. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/reachability.py +0 -0
  66. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/report.py +0 -0
  67. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/runner.py +0 -0
  68. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/savings.py +0 -0
  69. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/scenario.py +0 -0
  70. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/scenarios/__init__.py +0 -0
  71. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/scenarios/clean.py +0 -0
  72. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/scenarios/decoy.py +0 -0
  73. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/scenarios/registry.py +0 -0
  74. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/scenarios/split.py +0 -0
  75. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/scenarios/trap.py +0 -0
  76. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/termui.py +0 -0
  77. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/tools.py +0 -0
  78. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/trajectory.py +0 -0
  79. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/triage.py +0 -0
  80. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe/world.py +0 -0
  81. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe_testing.egg-info/SOURCES.txt +0 -0
  82. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe_testing.egg-info/dependency_links.txt +0 -0
  83. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe_testing.egg-info/entry_points.txt +0 -0
  84. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe_testing.egg-info/requires.txt +0 -0
  85. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/agentprobe_testing.egg-info/top_level.txt +0 -0
  86. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/setup.cfg +0 -0
  87. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/tests/test_access_control_agent.py +0 -0
  88. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/tests/test_access_control_domain.py +0 -0
  89. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/tests/test_access_control_rule_based_agent.py +0 -0
  90. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/tests/test_access_control_scenarios.py +0 -0
  91. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/tests/test_access_control_tools.py +0 -0
  92. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/tests/test_agreement.py +0 -0
  93. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/tests/test_browser_agent.py +0 -0
  94. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/tests/test_classifier.py +0 -0
  95. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/tests/test_cli.py +0 -0
  96. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/tests/test_complex_access_control_agent.py +0 -0
  97. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/tests/test_diff.py +0 -0
  98. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/tests/test_domain.py +0 -0
  99. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/tests/test_failures.py +0 -0
  100. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/tests/test_feedback.py +0 -0
  101. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/tests/test_generic_world.py +0 -0
  102. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/tests/test_injection.py +0 -0
  103. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/tests/test_injection_mode.py +0 -0
  104. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/tests/test_injector.py +0 -0
  105. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/tests/test_package_api.py +0 -0
  106. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/tests/test_playbook.py +0 -0
  107. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/tests/test_priority.py +0 -0
  108. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/tests/test_quickstart.py +0 -0
  109. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/tests/test_reachability.py +0 -0
  110. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/tests/test_report.py +0 -0
  111. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/tests/test_rule_based_agent.py +0 -0
  112. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/tests/test_runner.py +0 -0
  113. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/tests/test_runner_timeouts.py +0 -0
  114. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/tests/test_savings.py +0 -0
  115. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/tests/test_scenarios.py +0 -0
  116. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/tests/test_target_agent.py +0 -0
  117. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/tests/test_tau_retail.py +0 -0
  118. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/tests/test_termui.py +0 -0
  119. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/tests/test_tools.py +0 -0
  120. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/tests/test_trajectory.py +0 -0
  121. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/tests/test_triage.py +0 -0
  122. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/tests/test_validate_scenarios.py +0 -0
  123. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/tests/test_web_browsing_domain.py +0 -0
  124. {agentprobe_testing-1.5.0 → agentprobe_testing-1.5.1}/tests/test_world.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentprobe-testing
3
- Version: 1.5.0
3
+ Version: 1.5.1
4
4
  Summary: Adaptive chaos-testing for LLM agents: a live model-driven Injector that reads an agent's real trajectory and decides where to break something, instead of scripting perturbations in advance.
5
5
  License: Business Source License 1.1
6
6
 
@@ -61,7 +61,7 @@ from agentprobe.scenario import CommitPattern, FactPattern, GoalSpec, Scenario
61
61
  from agentprobe.scenarios.registry import ALL_SCENARIOS as TICKET_SCENARIOS
62
62
  from agentprobe.scenarios.registry import BY_ID as TICKET_SCENARIOS_BY_ID
63
63
 
64
- __version__ = "1.5.0"
64
+ __version__ = "1.5.1"
65
65
 
66
66
  __all__ = [
67
67
  "FixedFaultInjector",
@@ -16,6 +16,8 @@ from agentprobe.llm import cacheable_system, create_deterministic, response_cost
16
16
 
17
17
  import anthropic
18
18
 
19
+
20
+ from agentprobe.llm import make_client
19
21
  from agentprobe.agents.base import Agent, AgentAction
20
22
 
21
23
  SYSTEM_PROMPT = """You are a customer support agent. You have read tools \
@@ -44,7 +46,7 @@ class TargetAgent(Agent):
44
46
  max_tokens: int = 1024,
45
47
  system_prompt: Optional[str] = None,
46
48
  ):
47
- self._client = anthropic.Anthropic()
49
+ self._client = make_client()
48
50
  self._model = model
49
51
  self._max_tokens = max_tokens
50
52
  self._system_prompt = system_prompt if system_prompt is not None else SYSTEM_PROMPT
@@ -11,6 +11,8 @@ from enum import Enum
11
11
 
12
12
  import anthropic
13
13
 
14
+
15
+ from agentprobe.llm import make_client
14
16
  from agentprobe.injection import AppliedInjection
15
17
  from agentprobe.llm import cacheable_system, create_deterministic, response_cost_usd
16
18
  from agentprobe.trajectory import Step
@@ -126,7 +128,7 @@ def classify_response(
126
128
  ) -> ClassifiedResponse:
127
129
  """`steps` is the full trajectory, not pre-filtered to post-injection
128
130
  steps -- see _render_classification_prompt for why."""
129
- client = anthropic.Anthropic()
131
+ client = make_client()
130
132
  response = create_deterministic(
131
133
  client,
132
134
  model=model,
@@ -15,6 +15,8 @@ from agentprobe.llm import cacheable_system, create_deterministic, response_cost
15
15
 
16
16
  import anthropic
17
17
 
18
+
19
+ from agentprobe.llm import make_client
18
20
  from agentprobe.agents.base import Agent, AgentAction
19
21
 
20
22
  SYSTEM_PROMPT = """You are an IT access-control agent. You have read tools \
@@ -29,7 +31,7 @@ reply with a short final summary of what you did, in plain text."""
29
31
 
30
32
  class AccessControlAgent(Agent):
31
33
  def __init__(self, model: str = "claude-haiku-4-5-20251001", max_tokens: int = 1024):
32
- self._client = anthropic.Anthropic()
34
+ self._client = make_client()
33
35
  self._model = model
34
36
  self._max_tokens = max_tokens
35
37
  self._messages: list[dict[str, Any]] = []
@@ -17,6 +17,8 @@ from agentprobe.llm import cacheable_system, create_deterministic, response_cost
17
17
 
18
18
  import anthropic
19
19
 
20
+
21
+ from agentprobe.llm import make_client
20
22
  from agentprobe.agents.base import Agent, AgentAction
21
23
 
22
24
  COMPLEX_SYSTEM_PROMPT = """You are a senior IT access-control specialist handling access requests \
@@ -62,7 +64,7 @@ summary of what you decided and why."""
62
64
 
63
65
  class ComplexAccessControlAgent(Agent):
64
66
  def __init__(self, model: str = "claude-haiku-4-5-20251001", max_tokens: int = 1536):
65
- self._client = anthropic.Anthropic()
67
+ self._client = make_client()
66
68
  self._model = model
67
69
  self._max_tokens = max_tokens
68
70
  self._messages: list[dict[str, Any]] = []
@@ -41,10 +41,11 @@ class LLMUser:
41
41
  def _anthropic_complete(self, system: str, messages: list) -> tuple[str, float]:
42
42
  import anthropic
43
43
 
44
+ from agentprobe.llm import make_client
44
45
  from agentprobe.llm import response_cost_usd
45
46
 
46
47
  if self._client is None:
47
- self._client = anthropic.Anthropic()
48
+ self._client = make_client()
48
49
  r = self._client.messages.create(model=self.model, max_tokens=512, system=system, messages=messages)
49
50
  return "".join(b.text for b in r.content if b.type == "text"), response_cost_usd(self.model, r)
50
51
 
@@ -17,6 +17,8 @@ from agentprobe.llm import cacheable_system, create_deterministic, response_cost
17
17
 
18
18
  import anthropic
19
19
 
20
+
21
+ from agentprobe.llm import make_client
20
22
  from agentprobe.agents.base import Agent, AgentAction
21
23
 
22
24
  SYSTEM_PROMPT = """You are a customer-support agent working the store's own web \
@@ -34,7 +36,7 @@ with a short final summary of what you did, in plain text."""
34
36
 
35
37
  class WebBrowsingAgent(Agent):
36
38
  def __init__(self, model: str = "claude-haiku-4-5-20251001", max_tokens: int = 1024):
37
- self._client = anthropic.Anthropic()
39
+ self._client = make_client()
38
40
  self._model = model
39
41
  self._max_tokens = max_tokens
40
42
  self._messages: list[dict[str, Any]] = []
@@ -12,6 +12,8 @@ from typing import TYPE_CHECKING, Optional
12
12
 
13
13
  import anthropic
14
14
 
15
+
16
+ from agentprobe.llm import make_client
15
17
  from agentprobe.injection import (
16
18
  AppliedInjection,
17
19
  ArmedInjection,
@@ -856,7 +858,7 @@ class ModelInjector(Injector):
856
858
  example."""
857
859
  if policy not in KIND_POLICIES:
858
860
  raise ValueError(f"unknown kind policy {policy!r}, must be one of {KIND_POLICIES}")
859
- self._client = anthropic.Anthropic()
861
+ self._client = make_client()
860
862
  self._model = model
861
863
  self._kind_order = list(kind_order) if kind_order is not None else list(_DEFAULT_KIND_ORDER)
862
864
  self._policy = policy
@@ -15,6 +15,7 @@ from __future__ import annotations
15
15
 
16
16
  from typing import Any
17
17
 
18
+ import anthropic
18
19
  import anthropic._streaming as _anthropic_streaming
19
20
 
20
21
  _TEMPERATURE_SUPPORTED: dict[str, bool] = {}
@@ -110,6 +111,37 @@ def create_deterministic(client: Any, **kwargs: Any) -> Any:
110
111
  raise
111
112
 
112
113
 
114
+ # ---- per-request API key ------------------------------------------------------
115
+ #
116
+ # A hosted runner serves many customers from one process, so a customer's Anthropic key must
117
+ # not be put in the process-wide ANTHROPIC_API_KEY variable: two overlapping requests would
118
+ # overwrite each other's key and one customer's usage could be billed to another. The key is
119
+ # held in a context variable instead, which is isolated per request/task.
120
+
121
+ import contextlib as _contextlib
122
+ import contextvars as _contextvars
123
+
124
+ _api_key_var: "_contextvars.ContextVar[Any]" = _contextvars.ContextVar("agentprobe_api_key", default=None)
125
+
126
+
127
+ @_contextlib.contextmanager
128
+ def api_key_scope(api_key: str):
129
+ """Everything inside the `with` block (in this task/thread context) that builds an Anthropic
130
+ client uses `api_key`; other concurrent requests are unaffected."""
131
+ token = _api_key_var.set(api_key)
132
+ try:
133
+ yield
134
+ finally:
135
+ _api_key_var.reset(token)
136
+
137
+
138
+ def make_client() -> Any:
139
+ """The Anthropic client every agentprobe component uses: the key from api_key_scope() if one
140
+ is active, otherwise the usual ANTHROPIC_API_KEY from the environment."""
141
+ key = _api_key_var.get()
142
+ return anthropic.Anthropic(api_key=key) if key else anthropic.Anthropic()
143
+
144
+
113
145
  _PRICE_PER_TOKEN: dict[str, tuple[float, float]] = {
114
146
  # (input, output) USD per token, from the pricing table this was
115
147
  # checked against (2026-08). Keyed by the exact model ids this
@@ -43,26 +43,35 @@ class FetchedDomain:
43
43
  review_url: Optional[str] = None
44
44
 
45
45
 
46
- def _default_fetcher(url: str) -> dict:
46
+ def _default_fetcher(url: str, api_key: Optional[str] = None) -> dict:
47
47
  # Cloudflare's default bot protection blocks urllib's stock User-Agent
48
48
  # ("Python-urllib/3.x") outright -- a real client identifier, same idea
49
49
  # as any other SDK sending its own name/version, gets through.
50
- request = urllib.request.Request(url, headers={"User-Agent": "agentprobe-client/0.5.0"})
50
+ headers = {"User-Agent": "agentprobe-client/0.5.0"}
51
+ if api_key:
52
+ headers["Authorization"] = f"Bearer {api_key}"
53
+ request = urllib.request.Request(url, headers=headers)
51
54
  with urllib.request.urlopen(request, timeout=10) as resp: # noqa: S310 -- https enforced by fetch_domain below
52
55
  return json.loads(resp.read().decode("utf-8"))
53
56
 
54
57
 
55
- def fetch_domain(key: str, base_url: str = DEFAULT_BASE_URL, fetcher: Optional[Fetcher] = None) -> FetchedDomain:
58
+ def fetch_domain(
59
+ key: str, base_url: str = DEFAULT_BASE_URL, fetcher: Optional[Fetcher] = None, api_key: Optional[str] = None
60
+ ) -> FetchedDomain:
56
61
  """Fetch and load the Domain stored under `key`. Pass `fetcher` (a
57
62
  callable url -> dict) in tests instead of hitting the real network --
58
63
  see tests/test_registry.py.
64
+
65
+ A domain generated while signed in (with your API key) belongs to your account and can
66
+ only be fetched with that account's `api_key`; one generated anonymously stays fetchable
67
+ by anyone who has its key.
59
68
  """
60
69
  if not (base_url.startswith("https://") or base_url.startswith("http://localhost")):
61
70
  raise ValueError("fetch_domain requires an https:// base_url (http://localhost is allowed for local testing)")
62
71
  if not key:
63
72
  raise ValueError("fetch_domain requires a non-empty key")
64
73
 
65
- fetcher = fetcher if fetcher is not None else _default_fetcher
74
+ fetcher = fetcher if fetcher is not None else (lambda url: _default_fetcher(url, api_key))
66
75
  payload = fetcher(f"{base_url}/v1/domains/{key}")
67
76
 
68
77
  if "source" not in payload:
@@ -61,7 +61,8 @@ def summarize_failures(
61
61
  if client is None:
62
62
  import anthropic
63
63
 
64
- client = anthropic.Anthropic()
64
+ from agentprobe.llm import make_client
65
+ client = make_client()
65
66
  response = create_deterministic(
66
67
  client,
67
68
  model=model,
@@ -31,6 +31,8 @@ from typing import Any
31
31
 
32
32
  import anthropic
33
33
 
34
+
35
+ from agentprobe.llm import make_client
34
36
  from agentprobe.domain import Domain, TICKET_DOMAIN
35
37
  from agentprobe.llm import create_deterministic
36
38
  from agentprobe.reachability import reachable
@@ -392,7 +394,7 @@ judgment call. Call verify_value_mismatch exactly once."""
392
394
  def _verify_value_mismatch_with_model(
393
395
  order_amount: float, required_amount: float, policies: dict[str, str], model: str = "claude-sonnet-5"
394
396
  ) -> str:
395
- client = anthropic.Anthropic()
397
+ client = make_client()
396
398
  policy_text = "\n".join(f"{name}: {text}" for name, text in policies.items()) or "(no policies in this scenario)"
397
399
  prompt = (
398
400
  f"Order amount: {order_amount}\n"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentprobe-testing
3
- Version: 1.5.0
3
+ Version: 1.5.1
4
4
  Summary: Adaptive chaos-testing for LLM agents: a live model-driven Injector that reads an agent's real trajectory and decides where to break something, instead of scripting perturbations in advance.
5
5
  License: Business Source License 1.1
6
6
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agentprobe-testing"
3
- version = "1.5.0"
3
+ version = "1.5.1"
4
4
  description = "Adaptive chaos-testing for LLM agents: a live model-driven Injector that reads an agent's real trajectory and decides where to break something, instead of scripting perturbations in advance."
5
5
  requires-python = ">=3.11"
6
6
  dependencies = ["anthropic>=1.0.0", "python-dotenv>=1.0.0"]
@@ -241,3 +241,47 @@ def test_cache_is_per_model():
241
241
  accepting = FakeClient(reject_temperature=False)
242
242
  create_deterministic(accepting, model="claude-haiku-4-5-20251001", max_tokens=10)
243
243
  assert accepting.messages.calls[0]["extra_body"] == {"temperature": 0}
244
+
245
+
246
+ def test_api_key_scope_gives_each_context_its_own_key_and_restores_afterwards(monkeypatch):
247
+ import threading
248
+
249
+ import anthropic
250
+
251
+ from agentprobe.llm import api_key_scope, make_client
252
+
253
+ seen = []
254
+
255
+ class FakeClient:
256
+ def __init__(self, api_key=None):
257
+ self.api_key = api_key
258
+
259
+ monkeypatch.setattr(anthropic, "Anthropic", FakeClient)
260
+ assert make_client().api_key is None # no scope: falls back to the environment's key
261
+ with api_key_scope("sk-a"):
262
+ assert make_client().api_key == "sk-a"
263
+ # another thread/context never sees this request's key
264
+ t = threading.Thread(target=lambda: seen.append(make_client().api_key))
265
+ t.start()
266
+ t.join()
267
+ with api_key_scope("sk-b"):
268
+ assert make_client().api_key == "sk-b"
269
+ assert make_client().api_key == "sk-a"
270
+ assert make_client().api_key is None
271
+ assert seen == [None]
272
+
273
+
274
+ def test_overlapping_async_requests_keep_their_own_keys():
275
+ import asyncio
276
+
277
+ from agentprobe.llm import _api_key_var, api_key_scope
278
+
279
+ async def request(key, delay):
280
+ with api_key_scope(key):
281
+ await asyncio.sleep(delay) # another request runs while this one waits
282
+ return _api_key_var.get()
283
+
284
+ async def main():
285
+ return await asyncio.gather(request("sk-a", 0.02), request("sk-b", 0.0), request("sk-c", 0.01))
286
+
287
+ assert asyncio.run(main()) == ["sk-a", "sk-b", "sk-c"]
@@ -417,3 +417,18 @@ def test_replay_run_round_trip_replays_the_exact_recorded_sequence():
417
417
 
418
418
  assert replayed.injector_model == "replay:42"
419
419
  assert replayed._compute()["total"] == 1 # the same recorded injection fired again
420
+
421
+
422
+ def test_fetch_domain_sends_the_api_key_when_given(monkeypatch):
423
+ import agentprobe.registry as registry
424
+
425
+ seen = {}
426
+
427
+ def fake_default(url, api_key=None):
428
+ seen["url"], seen["key"] = url, api_key
429
+ return {"source": "raise SystemExit", "review_url": None}
430
+
431
+ monkeypatch.setattr(registry, "_default_fetcher", fake_default)
432
+ with pytest.raises(BaseException): # the stub source isn't a real domain; only the request matters
433
+ registry.fetch_domain("k_abc", base_url="https://sampact.com", api_key="ap_secret")
434
+ assert seen == {"url": "https://sampact.com/v1/domains/k_abc", "key": "ap_secret"}