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,184 @@
1
+ """Wall-clock-derived virtual clock for the env gateway (urn:agentenv:clock/v1).
2
+
3
+ Reads compute t0 + real_elapsed * rate, so virtual time advances on its own (independent of
4
+ tool calls) and reads never mutate it. Off by default: an unarmed clock 404s.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import math
9
+ import re
10
+ import threading
11
+ import time
12
+ from datetime import datetime, timedelta, timezone
13
+
14
+
15
+ class ClockError(ValueError):
16
+ """Invalid clock configuration (surfaced as HTTP 400)."""
17
+
18
+
19
+ MAX_RATE = 86400.0 # 1 real second = 1 virtual day (the max virtual-time speed)
20
+
21
+ # Fast-fail a t0 that would overflow within a real day of advancement at `rate` (frozen clocks
22
+ # exempt). Not the overflow guarantee — reads still saturate (_advance) if the gateway outlives it.
23
+ _OVERFLOW_HORIZON_S = 86400.0
24
+ _MAX_T0 = datetime.max.replace(tzinfo=timezone.utc)
25
+
26
+
27
+ def _parse_rate(rate) -> float:
28
+ if isinstance(rate, bool) or not isinstance(rate, (int, float)):
29
+ raise ClockError(f"rate must be a number, got {type(rate).__name__}")
30
+ # NaN passes both ordered bounds below, then poisons every later read with an uncatchable 500.
31
+ if not math.isfinite(rate):
32
+ raise ClockError(f"rate must be a finite number, got {rate}")
33
+ if rate < 0:
34
+ raise ClockError("rate must be non-negative")
35
+ if rate > MAX_RATE:
36
+ raise ClockError(f"rate {rate:g} exceeds max {MAX_RATE:g} (1 real s = 1 virtual day)")
37
+ return float(rate)
38
+
39
+
40
+ # fromisoformat is looser than RFC3339 (accepts basic format, week/ordinal dates, sub-minute
41
+ # offsets), so gate on the grammar first and let it do only the arithmetic. A space separator is ok.
42
+ _RFC3339_RE = re.compile(
43
+ r"""^\d{4}-\d{2}-\d{2}
44
+ [Tt ]
45
+ \d{2}:\d{2}:\d{2} # seconds are mandatory
46
+ (?:\.\d+)?
47
+ (?:[Zz]|[+-]\d{2}:\d{2})$ # offset is hours:minutes only, or Z
48
+ """,
49
+ re.VERBOSE,
50
+ )
51
+ _RFC3339_SHAPE = "YYYY-MM-DDThh:mm:ss[.fff](Z|+hh:mm)"
52
+
53
+
54
+ def _parse_rfc3339(text: str) -> datetime:
55
+ if not isinstance(text, str):
56
+ raise ClockError(f"virtual_time must be an RFC3339 string, got {type(text).__name__}")
57
+ # Don't strip: a whitespace-wrapped value would slip through the anchored grammar (a 200 the contract calls a 400).
58
+ raw = text
59
+ if not _RFC3339_RE.match(raw):
60
+ raise ClockError(f"invalid RFC3339 virtual_time {text!r}: expected {_RFC3339_SHAPE}")
61
+ if raw.endswith(("Z", "z")):
62
+ raw = raw[:-1] + "+00:00"
63
+ try:
64
+ dt = datetime.fromisoformat(raw)
65
+ except ValueError as e:
66
+ raise ClockError(f"invalid RFC3339 virtual_time {text!r}: {e}")
67
+ try:
68
+ return dt.astimezone(timezone.utc)
69
+ except OverflowError:
70
+ raise ClockError(f"invalid RFC3339 virtual_time {text!r}: out of representable range")
71
+
72
+
73
+ def _iso(dt: datetime) -> str:
74
+ return dt.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
75
+
76
+
77
+ # Calendar Y/M are absent on purpose: a month is not a fixed span. Empty ("P"/"PT") is caught by the
78
+ # any-group check in _parse_duration, not here.
79
+ _ISO8601_DURATION_RE = re.compile(
80
+ r"""^P (?:(\d+)W)? (?:(\d+)D)? # weeks / days
81
+ (?: T (?:(\d+)H)? (?:(\d+)M)? # hours / minutes ('M' is months before T, minutes after)
82
+ (?:(\d+(?:\.\d+)?)S)? )? $ # seconds, optionally fractional
83
+ """,
84
+ re.VERBOSE,
85
+ )
86
+
87
+
88
+ def _parse_duration(text) -> timedelta:
89
+ if not isinstance(text, str):
90
+ raise ClockError(f"duration must be an ISO-8601 string, got {type(text).__name__}")
91
+ m = _ISO8601_DURATION_RE.match(text)
92
+ if m is None or not any(m.groups()):
93
+ raise ClockError(f"invalid ISO-8601 duration {text!r}: expected e.g. 'PT24H', 'P1DT6H', 'PT30M'")
94
+ weeks, days, hours, minutes, seconds = m.groups()
95
+ try:
96
+ return timedelta(weeks=int(weeks or 0), days=int(days or 0), hours=int(hours or 0),
97
+ minutes=int(minutes or 0), seconds=float(seconds or 0))
98
+ except (OverflowError, ValueError):
99
+ raise ClockError(f"invalid ISO-8601 duration {text!r}: out of representable range")
100
+
101
+
102
+ def _advance(t0: datetime, seconds: float) -> datetime:
103
+ """t0 + seconds, saturating at datetime.min/max instead of raising.
104
+
105
+ set_time's horizon check can't bound a gateway that outlives _OVERFLOW_HORIZON_S, so reads
106
+ saturate rather than 500 — an armed clock always returns a (monotonic) time.
107
+ """
108
+ try:
109
+ return t0 + timedelta(seconds=seconds)
110
+ except (OverflowError, ValueError, OSError):
111
+ return _MAX_T0 if seconds > 0 else datetime.min.replace(tzinfo=timezone.utc)
112
+
113
+
114
+ class Clock:
115
+ """Gateway-owned virtual clock — wall-clock-driven, monotonic, threadsafe; `now_fn` injectable for tests."""
116
+
117
+ def __init__(self, now_fn=time.monotonic):
118
+ self._now = now_fn
119
+ self._lock = threading.Lock()
120
+ self._armed = False
121
+ self._t0: datetime | None = None
122
+ self._anchor: float | None = None
123
+ self._rate = 1.0
124
+ self._generation = 0
125
+
126
+ @property
127
+ def armed(self) -> bool:
128
+ return self._armed
129
+
130
+ @property
131
+ def generation(self) -> int:
132
+ """Bumped on every arm/clear — lets consumers detect a re-anchor and drop stale derived state."""
133
+ return self._generation
134
+
135
+ def t0(self) -> datetime | None:
136
+ """The armed anchor virtual time (None when unarmed) — for time-trigger mark resolution."""
137
+ with self._lock:
138
+ return self._t0
139
+
140
+ def set_time(self, virtual_time: str, rate=1.0) -> dict:
141
+ """Arm/re-arm: anchor t0=virtual_time to now, advancing at `rate` virtual s per real s."""
142
+ t0 = _parse_rfc3339(virtual_time)
143
+ r = _parse_rate(1.0 if rate is None else rate)
144
+ if t0 > _MAX_T0 - timedelta(seconds=_OVERFLOW_HORIZON_S * r):
145
+ raise ClockError(f"virtual_time {virtual_time!r} at rate {r:g} would overflow datetime near the max")
146
+ with self._lock:
147
+ self._t0 = t0
148
+ self._anchor = self._now()
149
+ self._rate = r
150
+ self._armed = True
151
+ self._generation += 1
152
+ return self.state()
153
+
154
+ def clear(self) -> dict:
155
+ """Disarm (idempotent). Afterwards reads 404."""
156
+ with self._lock:
157
+ self._armed = False
158
+ self._t0 = None
159
+ self._anchor = None
160
+ self._rate = 1.0
161
+ self._generation += 1
162
+ return {"ok": True, "armed": False}
163
+
164
+ def now(self) -> datetime | None:
165
+ with self._lock:
166
+ if not self._armed or self._t0 is None:
167
+ return None
168
+ elapsed = self._now() - self._anchor
169
+ return _advance(self._t0, elapsed * self._rate)
170
+
171
+ def read(self) -> dict:
172
+ """The frozen consumer contract: {"virtual_time": "<RFC3339>"} — pure read, never advances."""
173
+ current = self.now()
174
+ if current is None:
175
+ raise ClockError("clock is not armed")
176
+ return {"virtual_time": _iso(current)}
177
+
178
+ def state(self) -> dict:
179
+ with self._lock:
180
+ if not self._armed or self._t0 is None:
181
+ return {"armed": False}
182
+ elapsed = self._now() - self._anchor
183
+ return {"armed": True, "t0": _iso(self._t0), "virtual_seconds_per_real_second": self._rate,
184
+ "virtual_time": _iso(_advance(self._t0, elapsed * self._rate))}
@@ -0,0 +1,190 @@
1
+ """Gateway constants shared with the data-plane client call sites.
2
+
3
+ Lives inside the gateway package so gateway.py can reach it with a relative
4
+ import — the gateway runs as a standalone container with no `agent_env` package.
5
+ Client call sites (MCPServerEnv/WebsiteEnv) import it by absolute path. One
6
+ source of truth keeps the layers ordered: client add_data >= gateway REST
7
+ proxy >= actual load time.
8
+ """
9
+
10
+ import os
11
+ from enum import Enum
12
+ from uuid import uuid4
13
+ import secrets
14
+
15
+ # Max end-to-end time for a single service's data load (seconds).
16
+ #
17
+ # The previous value of 600 was sized above a then-slowest prod load of ~430s.
18
+ # That is no longer the ceiling: a large universe's gmail component now takes 498s and slack
19
+ # 499s, leaving under 20% headroom on a number chosen to have plenty.
20
+ #
21
+ # The margin matters more than the average because of how a load fails. A
22
+ # component that exceeds this does not retry — the environment comes up, reports
23
+ # healthy, and serves zero rows, which is indistinguishable from a component that
24
+ # is legitimately empty. Paying a few extra minutes on a genuine hang is much
25
+ # cheaper than that.
26
+ #
27
+ # The real fix is capacity, not patience: a universe loads all its components
28
+ # through one asyncio.gather onto a VM defaulting to a single vCPU, and the
29
+ # loaders are CPU-bound. This value should come back down once that is sized
30
+ # properly.
31
+ DATA_PLANE_LOAD_TIMEOUT_S = 900
32
+
33
+ # Extra seconds granted per GB of payload, on top of the floor.
34
+ #
35
+ # Explicitly a heuristic, not a model. Measured load cost tracks RECORD COUNT, not bytes:
36
+ # github moved 3.5GB in 89s while gmail took 206s for 409MB and slack 207s for 86.6MB (~20x
37
+ # and ~94x worse per byte). So size is a weak proxy -- it over-grants to the big-blob
38
+ # services that never needed it and under-grants to the small record-heavy ones that do.
39
+ # It is still strictly better than a flat cap, because the flat cap's failure mode was
40
+ # killing healthy work. A real fix is a stall timeout (fail on no progress rather than on
41
+ # elapsed time), which needs the server to report progress mid-ingest.
42
+ DATA_PLANE_LOAD_TIMEOUT_PER_GB_S = 1200
43
+
44
+ # Ceiling, so a genuinely hung load on a huge payload still surfaces in bounded time
45
+ # instead of holding a worker slot and a billable VM for hours.
46
+ DATA_PLANE_LOAD_TIMEOUT_MAX_S = 3600
47
+
48
+
49
+ def data_plane_load_timeout_s(size_bytes: int | None) -> int:
50
+ """Read timeout for one service's data load, scaled by payload size.
51
+
52
+ ``None``/0 (size unknown) yields the floor, which is today's behaviour -- an
53
+ unmeasurable payload must not silently get an unbounded wait.
54
+ """
55
+ if not size_bytes or size_bytes <= 0:
56
+ return DATA_PLANE_LOAD_TIMEOUT_S
57
+ gb = size_bytes / 1_000_000_000
58
+ return int(min(
59
+ DATA_PLANE_LOAD_TIMEOUT_MAX_S,
60
+ DATA_PLANE_LOAD_TIMEOUT_S + gb * DATA_PLANE_LOAD_TIMEOUT_PER_GB_S,
61
+ ))
62
+
63
+ # Hand-rolled mirror of agentenv_protocol.types — the gateway container has no
64
+ # agentenv_protocol dependency. Keep byte-identical to that module.
65
+ WELL_KNOWN_PATH = "/.well-known/agent-env.json"
66
+ RPC_PATH = "/agentenv"
67
+ PROTOCOL_VERSION = "1.0"
68
+ METHOD_RESET = "data/reset"
69
+ METHOD_ADD = "data/add"
70
+ METHOD_GET = "data/get"
71
+ MCP_TRANSPORT = "mcp"
72
+
73
+ # Env/gateway-level extensions the gateway itself owns (tool gating + triggers).
74
+ EXT_DISABLE_TOOL_URI = "urn:agentenv:disable-tool/v1"
75
+ EXT_ENABLE_TOOL_URI = "urn:agentenv:enable-tool/v1"
76
+ EXT_TRIGGERS_URI = "urn:agentenv:triggers/v1"
77
+ EXT_CLOCK_URI = "urn:agentenv:clock/v1"
78
+ EXT_TRAJECTORY_URI = "urn:agentenv:trajectory/v1"
79
+ EXT_STEP_URI = "urn:agentenv:step/v1"
80
+ EXT_STATE_URI = "urn:agentenv:state/v1"
81
+
82
+ # Gateway's compose service name — how backing servers reach it to build env_get_time_url (compose path).
83
+ GATEWAY_COMPOSE_HOST = "gateway"
84
+
85
+ # Per-child card fetch budget (seconds) when composing the env card.
86
+ CARD_FETCH_TIMEOUT_S = 5.0
87
+
88
+ # Trigger status vocabulary, part of the GET /triggers/state contract; `state` asserts against it.
89
+ TRIGGER_STATUSES = ("armed", "firing", "queued", "fired", "failed")
90
+
91
+ # A fire is in flight; `queued` also has further provoking calls already waiting to be drained.
92
+ TRIGGER_IN_FLIGHT_STATUSES = ("firing", "queued")
93
+
94
+ GATEWAY_TRAJECTORY_FILE = os.environ.get(
95
+ "GATEWAY_TRAJECTORY_FILE",
96
+ f"/tmp/agentenv/{uuid4().hex[:8]}-trajectory.jsonl",
97
+ )
98
+
99
+ AGENT_ENV_ROLE_HEADER = "AgentEnv-Role"
100
+ DEFAULT_ROLE = "default"
101
+ WILDCARD = "*"
102
+ TOOL_DISABLE_ACTION = "disable"
103
+ TOOL_ENABLE_ACTION = "enable"
104
+
105
+
106
+ class GatewayMode(str, Enum):
107
+ PERFORMANCE = "performance"
108
+ CONSISTENT = "consistent"
109
+
110
+
111
+ DEFAULT_MCP_SERVER_NAME = "env"
112
+
113
+
114
+ def random_mcp_server_name() -> str:
115
+ """`env` + 4 random digits: undeclared envs on one agent never collide; declare a MultiEnv name for a stable one."""
116
+ return f"env{secrets.randbelow(10000):04d}"
117
+
118
+
119
+ # The gateway's own extensions, as its env card advertises them; here so kernel code can read them without importing gateway.py.
120
+ _TOOLS_REQUEST = {
121
+ "type": "object",
122
+ "properties": {
123
+ "role": {"type": "string"},
124
+ "tools": {"anyOf": [{"type": "array", "items": {"type": "string"}}, {"const": WILDCARD}]},
125
+ },
126
+ "required": ["role", "tools"],
127
+ }
128
+ # call_tool also needs a tool_name; if/then rather than oneOf keeps the fields top-level, where card viewers read them
129
+ _STEP_REQUEST = {
130
+ "type": "object",
131
+ "properties": {"action": {"enum": ["list_tools", "call_tool"]}, "tool_name": {"type": "string", "minLength": 1}, "arguments": {"type": "object"}},
132
+ "required": ["action"],
133
+ "if": {"properties": {"action": {"const": "call_tool"}}},
134
+ "then": {"required": ["tool_name"]},
135
+ }
136
+ GATEWAY_EXTENSIONS = [
137
+ {
138
+ "uri": EXT_DISABLE_TOOL_URI,
139
+ "description": "Disable tools for a role.",
140
+ "params": {"endpoint": f"/tools/{TOOL_DISABLE_ACTION}", "methods": {TOOL_DISABLE_ACTION: {"method": "POST", "request": _TOOLS_REQUEST}}},
141
+ "required": False,
142
+ },
143
+ {
144
+ "uri": EXT_ENABLE_TOOL_URI,
145
+ "description": "Enable tools for a role.",
146
+ "params": {"endpoint": f"/tools/{TOOL_ENABLE_ACTION}", "methods": {TOOL_ENABLE_ACTION: {"method": "POST", "request": _TOOLS_REQUEST}}},
147
+ "required": False,
148
+ },
149
+ {
150
+ "uri": EXT_TRIGGERS_URI,
151
+ "description": "Register (additive add-by-id), remove, clear, or read (state) typed Action/State triggers + the firing log (harness control path).",
152
+ "params": {"endpoint": "/triggers/register", "methods": {
153
+ "register": {"method": "POST", "endpoint": "/triggers/register", "request": {"type": "object", "properties": {"watch_roles": {"type": "array", "items": {"type": "string"}}, "executor": {"type": "object"}, "triggers": {"type": "array"}}, "required": ["triggers"]}},
154
+ "remove": {"method": "POST", "endpoint": "/triggers/remove", "request": {"type": "object", "properties": {"ids": {"type": "array", "items": {"type": "string"}}}, "required": ["ids"]}},
155
+ "clear": {"method": "POST", "endpoint": "/triggers/clear", "request": {"type": "object", "properties": {}}},
156
+ "state": {"method": "GET", "endpoint": "/triggers/state", "request": {"type": "object", "properties": {}}}}},
157
+ "required": False,
158
+ },
159
+ {
160
+ "uri": EXT_CLOCK_URI,
161
+ "description": "Virtual clock (urn:agentenv:clock/v1): read the gateway's virtual time (wall-clock-derived, rate-scaled) via get_time; set-time/clear are the harness control path.",
162
+ "params": {"endpoint": "/clock/time", "methods": {
163
+ "get_time": {"method": "GET", "endpoint": "/clock/time"},
164
+ "set_time": {"method": "PUT", "endpoint": "/clock/set-time", "request": {"type": "object", "properties": {"virtual_time": {"type": "string"}, "virtual_seconds_per_real_second": {"type": "number"}}, "required": ["virtual_time"]}},
165
+ "clear": {"method": "POST", "endpoint": "/clock/clear", "request": {"type": "object", "properties": {}}},
166
+ "state": {"method": "GET", "endpoint": "/clock/state", "request": {"type": "object", "properties": {}}}}},
167
+ "required": False,
168
+ },
169
+ {
170
+ "uri": EXT_TRAJECTORY_URI,
171
+ "description": "Env trajectory (urn:agentenv:trajectory/v1): the env's append-only JSONL record of tool calls, results, and trigger firings, virtual_time-stamped while the clock is armed.",
172
+ "params": {"endpoint": "/trajectory", "methods": {
173
+ "get": {"method": "GET", "endpoint": "/trajectory"}}},
174
+ "required": False,
175
+ },
176
+ {
177
+ "uri": EXT_STEP_URI,
178
+ "description": "Step (urn:agentenv:step/v1): list or call the env's tools over REST, filtered by the AgentEnv-Role header.",
179
+ "params": {"endpoint": "/step", "methods": {
180
+ "step": {"method": "POST", "endpoint": "/step", "request": _STEP_REQUEST}}},
181
+ "required": False,
182
+ },
183
+ {
184
+ "uri": EXT_STATE_URI,
185
+ "description": "State (urn:agentenv:state/v1): the env's MCP servers and their tools, its changelog id and its role rules.",
186
+ "params": {"endpoint": "/state", "methods": {
187
+ "get": {"method": "GET", "endpoint": "/state"}}},
188
+ "required": False,
189
+ },
190
+ ]
@@ -0,0 +1,156 @@
1
+ """Entry point for the Gateway container."""
2
+ import asyncio
3
+ import logging
4
+ import os
5
+ import time
6
+
7
+ import httpx
8
+
9
+ from . import AGENT_ENV_GATEWAY_MCP_PORT, GatewayMode, InternalMCPServer
10
+ from .constants import DEFAULT_MCP_SERVER_NAME
11
+ from .gateway import Gateway
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ def parse_internal_servers(spec: str, port: int) -> list[InternalMCPServer]:
17
+ servers: list[InternalMCPServer] = []
18
+ seen_names: set[str] = set()
19
+ if not spec.strip():
20
+ return servers
21
+ for entry in spec.split(","):
22
+ entry = entry.strip()
23
+ if not entry:
24
+ continue
25
+ if "=" in entry:
26
+ name, url = entry.split("=", 1)
27
+ name = name.strip()
28
+ url = url.strip()
29
+ if not name or not url:
30
+ raise ValueError(f"Invalid INTERNAL_MCP_SERVERS entry: '{entry}' (expected 'name=url' or 'name')")
31
+ else:
32
+ name = entry
33
+ url = f"http://{name}:{port}/mcp"
34
+ if name in seen_names:
35
+ raise ValueError(f"Duplicate internal server name: '{name}'")
36
+ seen_names.add(name)
37
+ servers.append(InternalMCPServer(name=name, mcp_url=url))
38
+ return servers
39
+
40
+
41
+ # How long to wait for each internal MCP server to become ready before giving
42
+ # up. Generous + configurable so a slow-starting server (image pull, DB
43
+ # migration, warmup) doesn't trip a premature failure that crash-loops the
44
+ # gateway. In Modal/container mode this is the *only* readiness gate (there's no
45
+ # docker-compose `depends_on`), so the old 30s ceiling was far too short.
46
+ _INTERNAL_SERVER_TIMEOUT_S = int(os.environ.get("GATEWAY_INTERNAL_SERVER_TIMEOUT_S", "180"))
47
+ _INTERNAL_SERVER_POLL_S = 2
48
+
49
+
50
+ def _ready_url(mcp_url: str) -> str:
51
+ """The server's /ready URL (root sibling of /mcp)."""
52
+ base = mcp_url.rstrip("/")
53
+ if base.endswith("/mcp"):
54
+ base = base[: -len("/mcp")]
55
+ return f"{base}/ready"
56
+
57
+
58
+ async def _wait_for_server(name: str, mcp_url: str, timeout_s: int = _INTERNAL_SERVER_TIMEOUT_S) -> bool:
59
+ """Wait until an internal MCP server is ready.
60
+
61
+ Prefer the server's ``/ready`` endpoint (HTTP 200 = fully initialised). If
62
+ the server has no ``/ready`` (older image → 404), fall back to the ``/mcp``
63
+ endpoint merely responding (<500). Poll up to ``timeout_s`` so a slow server
64
+ doesn't make the gateway give up and crash-loop.
65
+ """
66
+ ready_url = _ready_url(mcp_url)
67
+ deadline = time.monotonic() + timeout_s
68
+ last = "no response"
69
+ async with httpx.AsyncClient() as client:
70
+ while time.monotonic() < deadline:
71
+ try:
72
+ r = await client.get(ready_url, timeout=5)
73
+ if r.status_code == 200:
74
+ logger.info(f" {name} ready via /ready (200)")
75
+ return True
76
+ # No usable /ready: 404 = no such route; some MCP servers (e.g.
77
+ # @playwright/mcp, used by the website-browser companion) answer
78
+ # 400 to a plain GET /ready. In every non-200 case fall back to
79
+ # /mcp liveness and keep waiting.
80
+ m = await client.get(mcp_url, timeout=5)
81
+ if m.status_code < 500:
82
+ logger.info(f" {name} ready via /mcp fallback (/ready={r.status_code}, /mcp={m.status_code})")
83
+ return True
84
+ last = f"/ready={r.status_code} /mcp={m.status_code}"
85
+ except Exception as e:
86
+ last = type(e).__name__
87
+ logger.info(f" {name} not ready yet ({last}); retrying...")
88
+ await asyncio.sleep(_INTERNAL_SERVER_POLL_S)
89
+ logger.error(f"{name} not ready after {timeout_s}s (last: {last})")
90
+ return False
91
+
92
+
93
+ async def wait_for_internal_servers(servers: list[InternalMCPServer]) -> None:
94
+ """Wait for all internal servers to be ready. Raises if any never come up."""
95
+ t0 = time.monotonic()
96
+ for server in servers:
97
+ logger.info(f"Waiting for {server.name} at {server.mcp_url} (timeout {_INTERNAL_SERVER_TIMEOUT_S}s)...")
98
+ results = await asyncio.gather(*[_wait_for_server(s.name, s.mcp_url) for s in servers])
99
+ for server, ready in zip(servers, results):
100
+ if not ready:
101
+ raise RuntimeError(f"Internal server '{server.name}' not ready at {server.mcp_url}")
102
+ logger.info(f"Internal MCP server {server.name} is ready")
103
+ logger.info(f"All {len(servers)} internal MCP server(s) ready in {time.monotonic() - t0:.1f}s")
104
+
105
+
106
+ def parse_name_url_map(env_str: str) -> dict[str, str]:
107
+ # Expected format: 'slack=http://slack-website-backend:8000,email=http://email-website-backend:8000'
108
+ urls: dict[str, str] = {}
109
+ if not env_str.strip():
110
+ return urls
111
+ for entry in env_str.split(","):
112
+ entry = entry.strip()
113
+ if not entry:
114
+ continue
115
+ if "=" not in entry:
116
+ raise ValueError(f"Invalid REST_PROXY_URLS entry: '{entry}' (expected 'name=url')")
117
+ name, url = entry.split("=", 1)
118
+ name = name.strip()
119
+ url = url.strip()
120
+ if not name:
121
+ raise ValueError(f"Empty service name in REST_PROXY_URLS entry: '{entry}'")
122
+ if not url.startswith(("http://", "https://")):
123
+ raise ValueError(f"Invalid URL in REST_PROXY_URLS: '{url}' (must start with http:// or https://)")
124
+ if name in urls:
125
+ raise ValueError(f"Duplicate service name in REST_PROXY_URLS: '{name}'")
126
+ urls[name] = url
127
+ return urls
128
+
129
+
130
+ if __name__ == "__main__":
131
+ logging.basicConfig(
132
+ level=logging.INFO,
133
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
134
+ )
135
+
136
+ internal_mcp_servers = parse_internal_servers(
137
+ spec=os.environ.get("INTERNAL_MCP_SERVERS", ""),
138
+ port=int(os.environ.get("INTERNAL_MCP_PORT", str(AGENT_ENV_GATEWAY_MCP_PORT))),
139
+ )
140
+ asyncio.run(wait_for_internal_servers(internal_mcp_servers))
141
+
142
+ website_urls = parse_name_url_map(os.environ.get("WEBSITE_URLS", ""))
143
+ rest_proxy_urls = parse_name_url_map(os.environ.get("REST_PROXY_URLS", ""))
144
+ gateway_mode = GatewayMode(os.environ.get("GATEWAY_MODE", GatewayMode.PERFORMANCE.value))
145
+ service_db_url = os.environ.get("SERVICE_DB_URL")
146
+ gateway = Gateway(
147
+ host=os.environ.get("MCP_HOST", "0.0.0.0"),
148
+ port=int(os.environ.get("MCP_PORT", str(AGENT_ENV_GATEWAY_MCP_PORT))),
149
+ server_name=os.environ.get("MCP_SERVER_NAME") or DEFAULT_MCP_SERVER_NAME,
150
+ internal_mcp_servers=internal_mcp_servers,
151
+ website_urls=website_urls,
152
+ rest_proxy_urls=rest_proxy_urls,
153
+ gateway_mode=gateway_mode,
154
+ service_db_url=service_db_url,
155
+ )
156
+ gateway.run()