agentenv-framework 0.9.1254__py3-none-any.whl

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 (312) hide show
  1. agent_env/__init__.py +0 -0
  2. agent_env/a2a_agent/__init__.py +23 -0
  3. agent_env/a2a_agent/a2a_agent.py +600 -0
  4. agent_env/a2a_agent/conversation_store.py +356 -0
  5. agent_env/a2a_agent/object_transfer.py +609 -0
  6. agent_env/a2a_agent/protocol.py +238 -0
  7. agent_env/a2a_agent/store.py +201 -0
  8. agent_env/a2a_agent/validator.py +901 -0
  9. agent_env/artifact/__init__.py +48 -0
  10. agent_env/artifact/artifact.py +74 -0
  11. agent_env/artifact/artifacts/__init__.py +0 -0
  12. agent_env/artifact/artifacts/cli.py +83 -0
  13. agent_env/artifact/artifacts/docker_image.py +457 -0
  14. agent_env/artifact/artifacts/environment.py +89 -0
  15. agent_env/artifact/artifacts/environment_universe.py +156 -0
  16. agent_env/artifact/artifacts/file.py +214 -0
  17. agent_env/artifact/artifacts/file_artifact_universe.py +206 -0
  18. agent_env/artifact/artifacts/skill.py +206 -0
  19. agent_env/artifact/artifacts/vm_image.py +95 -0
  20. agent_env/artifact/ref.py +15 -0
  21. agent_env/artifact/registry.py +251 -0
  22. agent_env/artifact/store.py +295 -0
  23. agent_env/artifact/universe.py +38 -0
  24. agent_env/attribution.py +41 -0
  25. agent_env/bundle/__init__.py +4 -0
  26. agent_env/bundle/_fs.py +128 -0
  27. agent_env/bundle/authoring.py +239 -0
  28. agent_env/bundle/installed.py +175 -0
  29. agent_env/bundle/ledger.py +269 -0
  30. agent_env/bundle/materialize.py +204 -0
  31. agent_env/bundle/parse.py +594 -0
  32. agent_env/bundle/plan.py +414 -0
  33. agent_env/bundle/resolve.py +404 -0
  34. agent_env/bundle/run.py +361 -0
  35. agent_env/cli/__init__.py +69 -0
  36. agent_env/cli/__main__.py +10 -0
  37. agent_env/cli/_installers.py +576 -0
  38. agent_env/cli/_plugin_changes.py +947 -0
  39. agent_env/cli/a2a_agent/__init__.py +22 -0
  40. agent_env/cli/a2a_agent/add_skill.py +97 -0
  41. agent_env/cli/a2a_agent/deploy.py +60 -0
  42. agent_env/cli/a2a_agent/get.py +15 -0
  43. agent_env/cli/a2a_agent/get_instance.py +26 -0
  44. agent_env/cli/a2a_agent/put.py +78 -0
  45. agent_env/cli/a2a_agent/validate.py +56 -0
  46. agent_env/cli/artifact/__init__.py +20 -0
  47. agent_env/cli/artifact/cli.py +83 -0
  48. agent_env/cli/artifact/environment.py +44 -0
  49. agent_env/cli/artifact/environment_universe.py +163 -0
  50. agent_env/cli/artifact/file_artifact_universe.py +281 -0
  51. agent_env/cli/artifact/skill.py +128 -0
  52. agent_env/cli/banner.py +25 -0
  53. agent_env/cli/config.py +263 -0
  54. agent_env/cli/env/__init__.py +31 -0
  55. agent_env/cli/env/deploy.py +112 -0
  56. agent_env/cli/env/gateway.py +57 -0
  57. agent_env/cli/env/get_instance.py +40 -0
  58. agent_env/cli/env/mcp_server.py +410 -0
  59. agent_env/cli/env/multi.py +282 -0
  60. agent_env/cli/env/service_db.py +99 -0
  61. agent_env/cli/env/snapshot.py +35 -0
  62. agent_env/cli/env/state/__init__.py +6 -0
  63. agent_env/cli/env/state/init.py +108 -0
  64. agent_env/cli/env/state/teardown.py +44 -0
  65. agent_env/cli/env/website.py +216 -0
  66. agent_env/cli/env/website_browser.py +67 -0
  67. agent_env/cli/eval/__init__.py +16 -0
  68. agent_env/cli/eval/add_tasks.py +63 -0
  69. agent_env/cli/eval/create.py +49 -0
  70. agent_env/cli/eval/run.py +194 -0
  71. agent_env/cli/identity/__init__.py +5 -0
  72. agent_env/cli/identity/client_identity.py +37 -0
  73. agent_env/cli/plugin.py +438 -0
  74. agent_env/cli/run.py +285 -0
  75. agent_env/cli/task/__init__.py +21 -0
  76. agent_env/cli/task/create.py +109 -0
  77. agent_env/cli/task/get.py +17 -0
  78. agent_env/cli/task/get_instance.py +34 -0
  79. agent_env/cli/task/run.py +615 -0
  80. agent_env/cli/task/validate.py +31 -0
  81. agent_env/cli/up.py +123 -0
  82. agent_env/cli/utils.py +183 -0
  83. agent_env/config/__init__.py +50 -0
  84. agent_env/config/describe.py +1067 -0
  85. agent_env/config/errors.py +13 -0
  86. agent_env/config/loader.py +245 -0
  87. agent_env/config/model.py +154 -0
  88. agent_env/config/paths.py +25 -0
  89. agent_env/config/plugin_tables.py +101 -0
  90. agent_env/config/provenance.py +134 -0
  91. agent_env/config/runtime.py +1064 -0
  92. agent_env/config/snapshot.py +81 -0
  93. agent_env/entity_refs.py +186 -0
  94. agent_env/env/__init__.py +49 -0
  95. agent_env/env/env.py +433 -0
  96. agent_env/env/env_artifact_store.py +87 -0
  97. agent_env/env/envs/__init__.py +7 -0
  98. agent_env/env/envs/_deployment.py +281 -0
  99. agent_env/env/envs/gateway_server.py +32 -0
  100. agent_env/env/envs/mcp_server.py +387 -0
  101. agent_env/env/envs/multi_env.py +780 -0
  102. agent_env/env/envs/service_db/Dockerfile +20 -0
  103. agent_env/env/envs/service_db/Dockerfile.db-mcp +42 -0
  104. agent_env/env/envs/service_db/Dockerfile.db-web +8 -0
  105. agent_env/env/envs/service_db.py +92 -0
  106. agent_env/env/envs/website.py +294 -0
  107. agent_env/env/envs/website_browser/Dockerfile +40 -0
  108. agent_env/env/envs/website_browser/__init__.py +17 -0
  109. agent_env/env/envs/website_browser/entrypoint.sh +13 -0
  110. agent_env/env/gateway/Dockerfile +14 -0
  111. agent_env/env/gateway/__init__.py +37 -0
  112. agent_env/env/gateway/clock.py +184 -0
  113. agent_env/env/gateway/constants.py +190 -0
  114. agent_env/env/gateway/entrypoint.py +156 -0
  115. agent_env/env/gateway/gateway.py +1095 -0
  116. agent_env/env/gateway/get_time.py +60 -0
  117. agent_env/env/gateway/requirements.txt +4 -0
  118. agent_env/env/gateway/triggers.py +1193 -0
  119. agent_env/env/legacy_protocol.py +146 -0
  120. agent_env/env/registry.py +93 -0
  121. agent_env/env/snapshot_store.py +372 -0
  122. agent_env/env/store.py +411 -0
  123. agent_env/eval/__init__.py +14 -0
  124. agent_env/eval/eval.py +68 -0
  125. agent_env/eval/store.py +116 -0
  126. agent_env/examples/hello/README.md +9 -0
  127. agent_env/examples/hello/artifacts/greeting/check.sh +1 -0
  128. agent_env/examples/hello/artifacts/greeting/hello.txt +1 -0
  129. agent_env/examples/hello/tasks/hello.json +10 -0
  130. agent_env/explorer/__init__.py +12 -0
  131. agent_env/explorer/app.py +277 -0
  132. agent_env/explorer/openapi_docs.py +242 -0
  133. agent_env/explorer/plugin.py +95 -0
  134. agent_env/explorer/routers/__init__.py +1 -0
  135. agent_env/explorer/routers/common.py +174 -0
  136. agent_env/explorer/routers/conversations.py +25 -0
  137. agent_env/explorer/routers/objects.py +112 -0
  138. agent_env/explorer/routers/runs.py +475 -0
  139. agent_env/plugins/__init__.py +59 -0
  140. agent_env/plugins/_cli.py +244 -0
  141. agent_env/plugins/_discovery.py +97 -0
  142. agent_env/plugins/_inventory.py +262 -0
  143. agent_env/plugins/_registration.py +268 -0
  144. agent_env/plugins/_report.py +47 -0
  145. agent_env/plugins/_requirements.py +55 -0
  146. agent_env/providers/__init__.py +37 -0
  147. agent_env/providers/env_providers/__init__.py +12 -0
  148. agent_env/providers/env_providers/constants.py +13 -0
  149. agent_env/providers/env_providers/env_gateway_provider.py +1354 -0
  150. agent_env/providers/env_providers/env_provider.py +237 -0
  151. agent_env/providers/env_providers/env_server_provider.py +133 -0
  152. agent_env/providers/env_state/__init__.py +49 -0
  153. agent_env/providers/env_state/env_state_provider.py +554 -0
  154. agent_env/providers/env_state/local_postgres.py +390 -0
  155. agent_env/providers/env_state/store.py +112 -0
  156. agent_env/providers/sandbox_providers/__init__.py +29 -0
  157. agent_env/providers/sandbox_providers/chained_sandbox_provider.py +93 -0
  158. agent_env/providers/sandbox_providers/e2b/__init__.py +6 -0
  159. agent_env/providers/sandbox_providers/e2b/provider.py +376 -0
  160. agent_env/providers/sandbox_providers/e2b/sandbox.py +342 -0
  161. agent_env/providers/sandbox_providers/e2b/template.py +158 -0
  162. agent_env/providers/sandbox_providers/local_sandbox.py +408 -0
  163. agent_env/providers/sandbox_providers/modal_sandbox.py +500 -0
  164. agent_env/providers/sandbox_providers/modal_vm_sandbox.py +466 -0
  165. agent_env/providers/sandbox_providers/sandbox.py +387 -0
  166. agent_env/providers/sandbox_providers/sandbox_provider.py +614 -0
  167. agent_env/py.typed +0 -0
  168. agent_env/runner/__init__.py +16 -0
  169. agent_env/runner/local_runner.py +190 -0
  170. agent_env/runner/runner.py +127 -0
  171. agent_env/runner/store.py +94 -0
  172. agent_env/store/__init__.py +126 -0
  173. agent_env/store/_google.py +68 -0
  174. agent_env/store/base.py +33 -0
  175. agent_env/store/document_store/__init__.py +60 -0
  176. agent_env/store/document_store/document_store.py +519 -0
  177. agent_env/store/document_store/dynamodb_document_store.py +306 -0
  178. agent_env/store/document_store/evaluation.py +199 -0
  179. agent_env/store/document_store/firestore_mongo_document_store.py +242 -0
  180. agent_env/store/document_store/mongo_document_store.py +259 -0
  181. agent_env/store/document_store/sqlite_document_store.py +314 -0
  182. agent_env/store/ids.py +123 -0
  183. agent_env/store/image_store/__init__.py +34 -0
  184. agent_env/store/image_store/ecr_image_store.py +131 -0
  185. agent_env/store/image_store/google_credentials.py +113 -0
  186. agent_env/store/image_store/image_store.py +113 -0
  187. agent_env/store/image_store/local_registry_image_store.py +106 -0
  188. agent_env/store/image_store/oci_registry_credentials.py +196 -0
  189. agent_env/store/local_state.py +27 -0
  190. agent_env/store/object_store/__init__.py +24 -0
  191. agent_env/store/object_store/gcs_object_store.py +415 -0
  192. agent_env/store/object_store/local_object_store.py +143 -0
  193. agent_env/store/object_store/object_store.py +198 -0
  194. agent_env/store/object_store/s3_object_store.py +354 -0
  195. agent_env/store/query.py +112 -0
  196. agent_env/store/routing.py +609 -0
  197. agent_env/store/secret_store/__init__.py +19 -0
  198. agent_env/store/secret_store/aws_secrets_manager_secret_store.py +293 -0
  199. agent_env/store/secret_store/gcp_secret_manager_secret_store.py +319 -0
  200. agent_env/store/secret_store/local_secret_store.py +47 -0
  201. agent_env/store/secret_store/secret_store.py +19 -0
  202. agent_env/task/__init__.py +27 -0
  203. agent_env/task/interrupts.py +109 -0
  204. agent_env/task/registry.py +27 -0
  205. agent_env/task/step_journal.py +99 -0
  206. agent_env/task/store.py +940 -0
  207. agent_env/task/task.py +815 -0
  208. agent_env/task/teardown.py +142 -0
  209. agent_env/task_step/__init__.py +47 -0
  210. agent_env/task_step/context.py +195 -0
  211. agent_env/task_step/context_ops.py +224 -0
  212. agent_env/task_step/registry.py +182 -0
  213. agent_env/task_step/review_store.py +118 -0
  214. agent_env/task_step/snapshot_utils/__init__.py +2 -0
  215. agent_env/task_step/snapshot_utils/agent_state_capture.py +261 -0
  216. agent_env/task_step/snapshot_utils/snapshot_series.py +631 -0
  217. agent_env/task_step/store.py +129 -0
  218. agent_env/task_step/task_step.py +173 -0
  219. agent_env/task_step/task_steps/__init__.py +27 -0
  220. agent_env/task_step/task_steps/a2a_agent_validator/__init__.py +31 -0
  221. agent_env/task_step/task_steps/a2a_agent_validator/fixtures/README.md +158 -0
  222. agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.m4a +0 -0
  223. agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.mp3 +0 -0
  224. agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.mp4 +0 -0
  225. agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.ogg +0 -0
  226. agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.wav +0 -0
  227. agent_env/task_step/task_steps/a2a_agent_validator/fixtures/document.pdf +0 -0
  228. agent_env/task_step/task_steps/a2a_agent_validator/fixtures/red.gif +0 -0
  229. agent_env/task_step/task_steps/a2a_agent_validator/fixtures/red.jpg +0 -0
  230. agent_env/task_step/task_steps/a2a_agent_validator/fixtures/red.png +0 -0
  231. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_agent_card.py +103 -0
  232. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_agent_config_identity.py +69 -0
  233. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_agent_mcp.py +178 -0
  234. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_core_protocol.py +67 -0
  235. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_install.py +115 -0
  236. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_litellm_attribution.py +124 -0
  237. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_litellm_attribution_runtime.py +173 -0
  238. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_modalities.py +270 -0
  239. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_peer_agents.py +102 -0
  240. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_role.py +151 -0
  241. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_skill_config.py +194 -0
  242. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_snapshot.py +111 -0
  243. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_system_prompt.py +215 -0
  244. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_trajectory.py +164 -0
  245. agent_env/task_step/task_steps/add_skills.py +240 -0
  246. agent_env/task_step/task_steps/apply_server_config.py +288 -0
  247. agent_env/task_step/task_steps/collect_artifacts.py +940 -0
  248. agent_env/task_step/task_steps/deploy_agent.py +626 -0
  249. agent_env/task_step/task_steps/deploy_env.py +197 -0
  250. agent_env/task_step/task_steps/deploy_human_agent.py +100 -0
  251. agent_env/task_step/task_steps/deploy_sandbox.py +170 -0
  252. agent_env/task_step/task_steps/env_card_validator/__init__.py +4 -0
  253. agent_env/task_step/task_steps/env_card_validator/verify_env_card.py +109 -0
  254. agent_env/task_step/task_steps/env_card_validator/verify_env_core_protocol.py +108 -0
  255. agent_env/task_step/task_steps/install_agent.py +376 -0
  256. agent_env/task_step/task_steps/load_artifact.py +775 -0
  257. agent_env/task_step/task_steps/mcp_cli_builder/__init__.py +4 -0
  258. agent_env/task_step/task_steps/mcp_cli_builder/build_mcp_cli.py +201 -0
  259. agent_env/task_step/task_steps/mcp_cli_builder/codegen.py +627 -0
  260. agent_env/task_step/task_steps/mcp_env_validator/__init__.py +44 -0
  261. agent_env/task_step/task_steps/mcp_env_validator/validation_gate_aggregator.py +102 -0
  262. agent_env/task_step/task_steps/mcp_env_validator/verify_mcp_env_assessment.py +78 -0
  263. agent_env/task_step/task_steps/mcp_env_validator/verify_mcp_tool_schema.py +152 -0
  264. agent_env/task_step/task_steps/mcp_env_validator/verify_spec_conformance.py +486 -0
  265. agent_env/task_step/task_steps/modify_env_tool_access.py +80 -0
  266. agent_env/task_step/task_steps/multienv_validator/__init__.py +3 -0
  267. agent_env/task_step/task_steps/multienv_validator/combine_universe_verdicts.py +107 -0
  268. agent_env/task_step/task_steps/multienv_validator/universe_comparison.py +253 -0
  269. agent_env/task_step/task_steps/multienv_validator/verify_universe_agent_judge.py +180 -0
  270. agent_env/task_step/task_steps/multienv_validator/verify_universe_roundtrip.py +241 -0
  271. agent_env/task_step/task_steps/peer_agents.py +92 -0
  272. agent_env/task_step/task_steps/prompt_agent.py +900 -0
  273. agent_env/task_step/task_steps/register_agent_triggers.py +113 -0
  274. agent_env/task_step/task_steps/register_env_triggers.py +97 -0
  275. agent_env/task_step/task_steps/reset_env.py +71 -0
  276. agent_env/task_step/task_steps/review.py +152 -0
  277. agent_env/task_step/task_steps/run_code.py +461 -0
  278. agent_env/task_step/task_steps/run_code_runner.py +52 -0
  279. agent_env/task_step/task_steps/run_docker_container.py +380 -0
  280. agent_env/task_step/task_steps/sandbox_utils/__init__.py +0 -0
  281. agent_env/task_step/task_steps/sandbox_utils/sandbox_utils.py +79 -0
  282. agent_env/task_step/task_steps/snapshot_agent_state.py +205 -0
  283. agent_env/task_step/task_steps/snapshot_env.py +637 -0
  284. agent_env/task_step/task_steps/sync_env_clock.py +132 -0
  285. agent_env/task_step/task_steps/teardown_sandboxes.py +170 -0
  286. agent_env/task_step/task_steps/verifiers/__init__.py +23 -0
  287. agent_env/task_step/task_steps/verifiers/agent_prompt_response_verifier.py +166 -0
  288. agent_env/task_step/task_steps/verifiers/aggregate_verifiers.py +90 -0
  289. agent_env/task_step/task_steps/verifiers/env_outcome_verifier.py +127 -0
  290. agent_env/task_step/task_steps/verifiers/judge_utils/__init__.py +2 -0
  291. agent_env/task_step/task_steps/verifiers/judge_utils/frame_selection.py +377 -0
  292. agent_env/task_step/task_steps/verifiers/judge_utils/judge_output_format.py +1293 -0
  293. agent_env/task_step/task_steps/verifiers/judge_utils/trajectory_filter.py +514 -0
  294. agent_env/task_step/task_steps/verifiers/rubrics_verifier.py +1318 -0
  295. agent_env/task_step/task_steps/verifiers/run_container_unit_tests_verifier.py +434 -0
  296. agent_env/task_step/task_steps/verifiers/scoring.py +40 -0
  297. agent_env/task_step/task_steps/verifiers/verify_sandbox.py +308 -0
  298. agent_env/task_step/thread_work.py +61 -0
  299. agent_env/utils/__init__.py +0 -0
  300. agent_env/utils/card_naming.py +108 -0
  301. agent_env/utils/deprecation.py +109 -0
  302. agent_env/utils/docker_build.py +29 -0
  303. agent_env/utils/exec_retry.py +77 -0
  304. agent_env/utils/litellm_attribution.py +59 -0
  305. agent_env/utils/paths.py +20 -0
  306. agentenv_framework-0.9.1254.dist-info/METADATA +394 -0
  307. agentenv_framework-0.9.1254.dist-info/RECORD +312 -0
  308. agentenv_framework-0.9.1254.dist-info/WHEEL +4 -0
  309. agentenv_framework-0.9.1254.dist-info/entry_points.txt +5 -0
  310. agentenv_framework-0.9.1254.dist-info/licenses/LICENSE +202 -0
  311. agentenv_framework-0.9.1254.dist-info/licenses/NOTICE +4 -0
  312. agentenv_framework-0.9.1254.dist-info/licenses/THIRD_PARTY_NOTICES.md +7665 -0
@@ -0,0 +1,288 @@
1
+ """Task step that applies a task's behavior config to deployed servers.
2
+
3
+ At setup (after the env is deployed + universe loaded, before the agent runs)
4
+ this step invokes each backing MCP server's ``set_*`` config extensions over the
5
+ gateway. Extensions are advertised on the server's ``EnvironmentCard`` (served at
6
+ ``/.well-known/agent-env.json``) under ``capabilities.extensions[]`` and invoked
7
+ at their advertised REST endpoint (e.g. ``/agentenv/ext/set_errors``).
8
+
9
+ Config lives on the backing server, not on the gateway itself. Each server's card
10
+ comes from the env card stored on the deployed record, whose extension endpoints the
11
+ gateway has already rewritten to its ``/svc/mcp-{service}`` proxy; a record without a
12
+ stored card reads it live there. The agent never sees the card or these endpoints;
13
+ only this harness step invokes them.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import logging
19
+ from typing import Any, ClassVar, Optional
20
+
21
+ import httpx
22
+ from agentenv_protocol import client as protocol_v1
23
+
24
+ from agent_env.task_step.context import TaskStepContext
25
+ from agent_env.entity_refs import EntityRef
26
+ from agent_env.task_step.task_step import TaskStep, TaskStepDependency
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+
31
+ class ApplyServerConfigError(RuntimeError):
32
+ """Raised when a config directive cannot be applied (bad target / extension
33
+ not advertised / server rejected the args)."""
34
+
35
+
36
+ class ConfigDirective:
37
+ """A single config extension invocation against one backing server.
38
+
39
+ - ``service``: backing MCP server name (the env's ``environment_name``); the
40
+ gateway proxy key is ``mcp-<service>``.
41
+ - ``uri``: the extension's stable card URI (e.g. ``urn:agentenv:set-errors/v1``).
42
+ This is the discovery key on the card, NOT a fetchable URL.
43
+ - ``args``: the extension's POST body (e.g. ``{"tool_name", "error_rate",
44
+ "error_type"}`` for set_errors).
45
+ """
46
+
47
+ def __init__(self, service: str, uri: str, args: dict[str, Any]):
48
+ if not isinstance(service, str) or not service:
49
+ raise ValueError("directive.service must be a non-empty string")
50
+ if not isinstance(uri, str) or not uri:
51
+ raise ValueError("directive.uri must be a non-empty string")
52
+ if not isinstance(args, dict):
53
+ raise ValueError("directive.args must be a dict")
54
+ self.service = service
55
+ self.uri = uri
56
+ self.args = args
57
+
58
+ def to_dict(self) -> dict:
59
+ # Dual-write. This feeds the `tasks` and `task_steps`
60
+ # documents — NOT the delivered bundle. The bundle mints its own dict
61
+ # from these attributes in export/exporter.py, so the permanently frozen
62
+ # `{'service': ...}` Harbor applier input is unaffected by the twin here.
63
+ return {"service": self.service, "environment": self.service, "uri": self.uri, "args": self.args}
64
+
65
+ @classmethod
66
+ def from_dict(cls, data: dict) -> "ConfigDirective":
67
+ # Reads both spellings. Legacy-first: it is the spelling every
68
+ # SDK version has always written, so it is never the stale half of a pair.
69
+ service = data["service"] if "service" in data else data.get("environment")
70
+ if service is None:
71
+ raise ValueError("directive requires a 'service' (or 'environment') key")
72
+ return cls(service=service, uri=data["uri"], args=data.get("args") or {})
73
+
74
+
75
+ class ApplyServerConfigStep(TaskStep):
76
+ type: ClassVar[str] = "apply_server_config"
77
+ entity_refs = (EntityRef.env("env_id"),)
78
+
79
+ # A directive whose ``service`` is this sentinel is a broadcast: at execution
80
+ # it expands to one directive per MCP server in the env (same uri + args).
81
+ # Lets a caller apply an env-wide config (e.g. set_acting_user across every
82
+ # server) without enumerating services itself. Pairs naturally with
83
+ # ``tolerate_unadvertised`` so servers that don't opt in are simply skipped.
84
+ _BROADCAST: ClassVar[str] = "*"
85
+
86
+ def __init__(
87
+ self,
88
+ id: str,
89
+ version: Optional[int],
90
+ env_id: str,
91
+ directives: list,
92
+ depends_on: Optional[list[TaskStepDependency]] = None,
93
+ fail_task_on_error: bool = True,
94
+ timeout_seconds: int = 30,
95
+ tolerate_unadvertised: bool = False,
96
+ ):
97
+ super().__init__(id, version, depends_on=depends_on, fail_task_on_error=fail_task_on_error)
98
+ if not isinstance(env_id, str) or not env_id:
99
+ raise ValueError("env_id must be a non-empty string")
100
+ if not isinstance(directives, list) or not directives:
101
+ raise ValueError("directives must be a non-empty list")
102
+ self.env_id = env_id
103
+ self.directives = [
104
+ d if isinstance(d, ConfigDirective) else ConfigDirective.from_dict(d)
105
+ for d in directives
106
+ ]
107
+ self.timeout_seconds = timeout_seconds
108
+ # When True, a directive whose extension isn't advertised on the target
109
+ # server's card is skipped (recorded) instead of failing the step. Use
110
+ # for broadcast directives applied across every service in an env (e.g.
111
+ # set_acting_user), where some services may not opt into the extension.
112
+ # A failing live card read or an extension that IS advertised but rejects
113
+ # the args (e.g. unresolvable persona) still fails loud regardless; a child
114
+ # env missing from the stored card is skipped (no_env_card).
115
+ self.tolerate_unadvertised = tolerate_unadvertised
116
+
117
+ def to_dict(self) -> dict:
118
+ base = super().to_dict()
119
+ base["env_id"] = self.env_id
120
+ base["directives"] = [d.to_dict() for d in self.directives]
121
+ base["timeout_seconds"] = self.timeout_seconds
122
+ base["tolerate_unadvertised"] = self.tolerate_unadvertised
123
+ return base
124
+
125
+ @classmethod
126
+ def from_dict(cls, data: dict) -> "ApplyServerConfigStep":
127
+ return cls(
128
+ **cls._base_from_dict(data),
129
+ env_id=data["env_id"],
130
+ directives=data["directives"],
131
+ timeout_seconds=data.get("timeout_seconds", 30),
132
+ tolerate_unadvertised=data.get("tolerate_unadvertised", False),
133
+ )
134
+
135
+ def _expand_directives(self, deployed) -> list[ConfigDirective]:
136
+ """Expand any broadcast directive (service == ``*``) into one directive
137
+ per MCP server in the env, resolved from the env definition. The env's
138
+ child service names aren't otherwise enumerable here (the deployed card
139
+ list carries no roster), so we read them from ``Env.get`` — matching how
140
+ deploy_env / load_artifact resolve the env. Non-broadcast directives
141
+ pass through untouched."""
142
+ if not any(d.service == self._BROADCAST for d in self.directives):
143
+ return self.directives
144
+
145
+ from agent_env.env.env import Env
146
+
147
+ env = Env.get(deployed.env_id, deployed.env_version)
148
+ environment_names = [
149
+ e.environment_name for e in (getattr(env, "mcp_server_envs", None) or [])
150
+ ]
151
+ # A single-server env (a bare MCPServerEnv) exposes `environment_name` and no
152
+ # `mcp_server_envs`; mirror snapshot_env / exporter enumeration and fall
153
+ # back to it, else a broadcast silently matches nothing on single-server envs.
154
+ if not environment_names and getattr(env, "environment_name", None):
155
+ environment_names = [env.environment_name]
156
+ # A broadcast that resolves to zero services would apply nothing while the
157
+ # step reports success: the exact silent misconfig this step prevents. Fail
158
+ # loud (tolerate_unadvertised governs advertisement, not missing servers).
159
+ if not environment_names:
160
+ raise ApplyServerConfigError(
161
+ f"apply_server_config: broadcast directive matched no MCP servers "
162
+ f"in env {self.env_id!r} (env exposes neither mcp_server_envs nor a "
163
+ f"environment_name); nothing to apply, likely a task authoring error"
164
+ )
165
+ expanded: list[ConfigDirective] = []
166
+ for d in self.directives:
167
+ if d.service != self._BROADCAST:
168
+ expanded.append(d)
169
+ continue
170
+ for svc in environment_names:
171
+ expanded.append(ConfigDirective(service=svc, uri=d.uri, args=d.args))
172
+ return expanded
173
+
174
+ async def execute(self, context: TaskStepContext) -> TaskStepContext:
175
+ deployed = next((d for d in context.deployed_envs if d.env_id == self.env_id), None)
176
+ if deployed is None:
177
+ raise ApplyServerConfigError(f"Env '{self.env_id}' not found in context.deployed_envs")
178
+
179
+ # Record each directive in context metadata the moment it's accepted, so a
180
+ # later failure still leaves an accurate audit trail of what the server was
181
+ # actually armed with — a partial apply must not look like a no-op.
182
+ changes = context.metadata.setdefault("server_config_changes", [])
183
+ # Directives skipped because the target server doesn't advertise the
184
+ # extension (only when tolerate_unadvertised). Kept separate from
185
+ # `changes` so the audit trail never shows a skip as an applied change.
186
+ skipped = context.metadata.setdefault("server_config_skipped", [])
187
+ # Expand broadcast (service="*") directives to one-per-env-service.
188
+ # Env.get inside _expand_directives can raise (missing env / version /
189
+ # network), so wrap it to keep the broadcast path's failure contract the
190
+ # same ApplyServerConfigError as the per-directive HTTP calls below.
191
+ try:
192
+ directives = self._expand_directives(deployed)
193
+ except ApplyServerConfigError:
194
+ raise
195
+ except Exception as e:
196
+ raise ApplyServerConfigError(
197
+ f"Failed to expand broadcast directives for env {self.env_id!r}: "
198
+ f"{type(e).__name__}: {e}"
199
+ ) from e
200
+ from agent_env.env import legacy_protocol
201
+ from agent_env.env.env import gateway_url_of
202
+
203
+ # One card per service — a task typically sets several directives (e.g. errors on
204
+ # multiple tools) against the same server. A None card marks a child env with no
205
+ # card: a server predating env-card support, one whose card name differs, or one
206
+ # whose card the deploy couldn't read.
207
+ cards: dict[str, tuple[str, Optional[dict]]] = {}
208
+ for directive in directives:
209
+ try:
210
+ if directive.service not in cards:
211
+ cards[directive.service] = await legacy_protocol.child_env_card(
212
+ deployed, gateway_url_of(deployed), directive.service, timeout=self.timeout_seconds
213
+ )
214
+ base_url, card = cards[directive.service]
215
+ if card is None:
216
+ if not self.tolerate_unadvertised:
217
+ raise ApplyServerConfigError(
218
+ f"Service {directive.service!r} has no env card (env={self.env_id}), so {directive.uri!r} "
219
+ f"can't be applied: it serves none, its card isn't named {directive.service!r}, or the deploy couldn't read it"
220
+ )
221
+ logger.warning(
222
+ f"apply_server_config: service {directive.service!r} serves no "
223
+ f"environment card (env={self.env_id}); skipping {directive.uri!r} "
224
+ f"(tolerate_unadvertised)"
225
+ )
226
+ skipped.append({
227
+ "step_id": self.id,
228
+ "env_id": self.env_id,
229
+ "service": directive.service,
230
+ "environment": directive.service,
231
+ "uri": directive.uri,
232
+ "reason": "no_env_card",
233
+ })
234
+ continue
235
+ ext = protocol_v1.find_extension(card, directive.uri)
236
+ if ext is None:
237
+ if self.tolerate_unadvertised:
238
+ logger.warning(
239
+ f"apply_server_config: {directive.uri!r} not advertised on "
240
+ f"service {directive.service!r} (env={self.env_id}); skipping "
241
+ f"(tolerate_unadvertised)"
242
+ )
243
+ skipped.append({
244
+ "step_id": self.id,
245
+ "env_id": self.env_id,
246
+ "service": directive.service,
247
+ "environment": directive.service,
248
+ "uri": directive.uri,
249
+ "reason": "extension_not_advertised",
250
+ })
251
+ continue
252
+ raise ApplyServerConfigError(
253
+ f"Extension {directive.uri!r} not advertised on card for service "
254
+ f"{directive.service!r} (env={self.env_id}). "
255
+ f"Advertised: {[e.get('uri') for e in (card.get('capabilities') or {}).get('extensions') or []]}"
256
+ )
257
+ result = await protocol_v1.invoke_extension(
258
+ base_url, card, directive.uri, params=directive.args,
259
+ timeout=self.timeout_seconds,
260
+ )
261
+ except ApplyServerConfigError:
262
+ raise
263
+ except httpx.HTTPStatusError as e:
264
+ raise ApplyServerConfigError(
265
+ f"Applying {directive.uri!r} on service {directive.service!r} "
266
+ f"(env={self.env_id}) failed: HTTP {e.response.status_code} {e.response.text}"
267
+ ) from e
268
+ except Exception as e:
269
+ raise ApplyServerConfigError(
270
+ f"Applying {directive.uri!r} on service {directive.service!r} "
271
+ f"(env={self.env_id}) failed: {type(e).__name__}: {e}"
272
+ ) from e
273
+
274
+ logger.info(
275
+ f"apply_server_config: applied {directive.uri} on service={directive.service} "
276
+ f"env={self.env_id} args={directive.args}: {result}"
277
+ )
278
+ changes.append({
279
+ "step_id": self.id,
280
+ "env_id": self.env_id,
281
+ "service": directive.service,
282
+ "environment": directive.service,
283
+ "uri": directive.uri,
284
+ "args": directive.args,
285
+ "result": result,
286
+ })
287
+
288
+ return context