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,627 @@
1
+ """Generate a Click-based CLI script from a manifest and live tools.
2
+
3
+ When an Interface Manifest is available, it supplies entity/action structure
4
+ while the gateway's live tool schemas supply invocation details. Servers
5
+ without a manifest retain the legacy flat tool CLI. Both modes query
6
+ `/step list_tools` on each invocation so role filtering stays live.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import copy
12
+ import re
13
+ from typing import Any
14
+
15
+ from agentenv_protocol import MANIFEST_VERSION, manifest_compatible
16
+
17
+ from agent_env.env.gateway import AGENT_ENV_ROLE_HEADER
18
+
19
+ GATEWAY_URL_ENV_VAR = "AGENT_ENV_GATEWAY_URL"
20
+ ROLE_ENV_VAR = "AGENT_ENV_ROLE"
21
+ DEFAULT_CLI_ROLE = "cli"
22
+ _COMMAND_NAME_RE = re.compile(r"^[a-zA-Z][a-zA-Z0-9_-]*$")
23
+ _WORD_RE = re.compile(r"[A-Z]+(?=[A-Z][a-z]|$)|[A-Z]?[a-z]+|[0-9]+")
24
+ # Root subcommands the renderer owns; an entity must not normalize to one.
25
+ _RESERVED_ENTITY_NAMES = frozenset({"actions"})
26
+
27
+
28
+ def _entity_command_name(entity_name: str, environment_name: str) -> str:
29
+ """The entity's group name, or "" when it is its service's namesake.
30
+
31
+ A namesake entity (Apartment in apartment) strips to nothing; returning ""
32
+ tells the caller to render its verbs at the root rather than repeat the token.
33
+ """
34
+ words = _WORD_RE.findall(entity_name)
35
+ service_words = [
36
+ part.lower()
37
+ for part in re.split(r"[-_]+", environment_name)
38
+ if part
39
+ ]
40
+ lowered = [word.lower() for word in words]
41
+ if service_words and lowered[: len(service_words)] == service_words:
42
+ lowered = lowered[len(service_words):]
43
+ return "-".join(lowered)
44
+
45
+
46
+ def _action_command_name(tool_name: str, environment_name: str) -> str:
47
+ prefix = f"{environment_name}_"
48
+ name = tool_name[len(prefix):] if tool_name.startswith(prefix) else tool_name
49
+ return re.sub(r"_+", "-", name).strip("-").lower()
50
+
51
+
52
+ def _validate_params(container: Any, label: str) -> None:
53
+ """Reject a malformed params projection at build time, not CLI-invocation time."""
54
+ if not isinstance(container, dict):
55
+ raise ValueError(f"Manifest {label} must be an object")
56
+ params = container.get("params")
57
+ if params is None:
58
+ return
59
+ if not isinstance(params, list):
60
+ raise ValueError(f"Manifest {label} params must be a list")
61
+ for param in params:
62
+ if not isinstance(param, dict):
63
+ raise ValueError(f"Manifest {label} params entries must be objects")
64
+ name = param.get("name")
65
+ if not isinstance(name, str) or not name:
66
+ raise ValueError(f"Manifest {label} has a param with no usable name")
67
+
68
+
69
+ def _prepare_interface_manifest(
70
+ interface_manifest: dict[str, Any] | None,
71
+ ) -> dict[str, Any] | None:
72
+ if interface_manifest is None:
73
+ return None
74
+ if not isinstance(interface_manifest, dict):
75
+ raise ValueError("interface_manifest must be a JSON object")
76
+ version = interface_manifest.get("manifest_version")
77
+ if not manifest_compatible(version):
78
+ raise ValueError(
79
+ f"Unsupported Interface Manifest version {version!r}; this renderer "
80
+ f"supports {MANIFEST_VERSION!r} (same major, and same minor while 0.x)"
81
+ )
82
+ interface = interface_manifest.get("interface")
83
+ if interface not in (None, "cli"):
84
+ raise ValueError(
85
+ f"Expected a CLI Interface Manifest, got interface {interface!r}"
86
+ )
87
+ environment_name = interface_manifest.get("service")
88
+ if not isinstance(environment_name, str) or not environment_name:
89
+ raise ValueError("Interface Manifest must include a non-empty service")
90
+
91
+ prepared = copy.deepcopy(interface_manifest)
92
+ entity_names: set[str] = set()
93
+ root_verbs: set[str] = set()
94
+ rooted_entity: str | None = None
95
+ for entity in prepared.get("entities") or []:
96
+ raw_name = entity.get("entity") or ""
97
+ if not _WORD_RE.findall(raw_name):
98
+ raise ValueError(f"Entity {raw_name!r} has no usable CLI command name")
99
+
100
+ # In the CLI projection, commands are keyed by verb (list/get/...); the
101
+ # key is the subcommand name, so there is nothing further to assign.
102
+ commands = entity.get("commands")
103
+ if commands is not None and not isinstance(commands, dict):
104
+ raise ValueError(f"Entity {raw_name!r} commands must be an object")
105
+ for verb, command in (commands or {}).items():
106
+ _validate_params(command, f"entity {raw_name!r} command {verb!r}")
107
+
108
+ command_name = _entity_command_name(raw_name, environment_name)
109
+ if not command_name:
110
+ # Namesake entity: `apartment list`, not `apartment apartment list`.
111
+ if rooted_entity is not None:
112
+ raise ValueError(
113
+ f"Entities {rooted_entity!r} and {raw_name!r} both match the "
114
+ f"service name; only one can supply root commands"
115
+ )
116
+ rooted_entity = raw_name
117
+ root_verbs = set(commands or {})
118
+ entity["_command_name"] = None
119
+ continue
120
+ if command_name in _RESERVED_ENTITY_NAMES:
121
+ raise ValueError(
122
+ f"Entity CLI command {command_name!r} collides with a reserved "
123
+ f"root command; rename the entity or its service prefix"
124
+ )
125
+ if command_name in entity_names:
126
+ raise ValueError(f"Duplicate entity CLI command {command_name!r}")
127
+ entity_names.add(command_name)
128
+ entity["_command_name"] = command_name
129
+
130
+ collisions = sorted(root_verbs & (entity_names | _RESERVED_ENTITY_NAMES))
131
+ if collisions:
132
+ raise ValueError(
133
+ f"Root command {collisions[0]!r} from entity {rooted_entity!r} collides "
134
+ f"with another root command"
135
+ )
136
+
137
+ action_names: set[str] = set()
138
+ for action in prepared.get("actions") or []:
139
+ command_name = _action_command_name(action.get("tool") or "", environment_name)
140
+ if not command_name:
141
+ raise ValueError(
142
+ f"Action {action.get('name') or action.get('tool')!r} has no usable "
143
+ f"CLI command name"
144
+ )
145
+ if command_name in action_names:
146
+ raise ValueError(f"Duplicate action CLI command {command_name!r}")
147
+ action_names.add(command_name)
148
+ action["_command_name"] = command_name
149
+ _validate_params(action, f"action {command_name!r}")
150
+ return prepared
151
+
152
+
153
+ def generate_cli_script(
154
+ command_name: str,
155
+ interface_manifest: dict[str, Any] | None = None,
156
+ environment_name: str | None = None,
157
+ ) -> str:
158
+ if not _COMMAND_NAME_RE.match(command_name):
159
+ raise ValueError(f"command_name {command_name!r} must match {_COMMAND_NAME_RE.pattern!r}")
160
+ prepared_manifest = _prepare_interface_manifest(interface_manifest)
161
+ return _SCRIPT.format(
162
+ command_name=command_name,
163
+ environment_name=repr(environment_name or command_name),
164
+ interface_manifest_literal=repr(prepared_manifest),
165
+ gateway_env_var=GATEWAY_URL_ENV_VAR,
166
+ role_env_var=ROLE_ENV_VAR,
167
+ default_role=DEFAULT_CLI_ROLE,
168
+ role_header=AGENT_ENV_ROLE_HEADER,
169
+ )
170
+
171
+
172
+ _SCRIPT = '''#!/usr/bin/env python3
173
+ """Auto-generated CLI for MCP env {command_name}. Discovers tools at runtime."""
174
+
175
+ import functools
176
+ import json
177
+ import keyword
178
+ import os
179
+ import re
180
+ import sys
181
+
182
+ import click
183
+ import httpx
184
+
185
+ GATEWAY_URL_ENV = "{gateway_env_var}"
186
+ ROLE_ENV = "{role_env_var}"
187
+ DEFAULT_ROLE = "{default_role}"
188
+ ROLE_HEADER = "{role_header}"
189
+ SERVICE_NAME = {environment_name}
190
+ INTERFACE_MANIFEST = {interface_manifest_literal}
191
+ _CONFIG_FILE_NAME = ".env"
192
+
193
+
194
+ def _config_file_path():
195
+ return os.path.join(os.path.dirname(os.path.realpath(__file__)), _CONFIG_FILE_NAME)
196
+
197
+
198
+ def _load_sibling_env():
199
+ """Populate os.environ from <bin_dir>/.env (KEY=VALUE per line). Shell env wins."""
200
+ sibling = _config_file_path()
201
+ if not os.path.isfile(sibling):
202
+ return
203
+ for line in open(sibling):
204
+ line = line.strip()
205
+ if not line or line.startswith("#"):
206
+ continue
207
+ k, _, v = line.partition("=")
208
+ k = k.strip()
209
+ v = v.strip().strip('"').strip("'")
210
+ if k and k not in os.environ:
211
+ os.environ[k] = v
212
+
213
+
214
+ _load_sibling_env()
215
+
216
+
217
+ def _gateway_url():
218
+ url = os.environ.get(GATEWAY_URL_ENV)
219
+ if not url:
220
+ click.echo(f"error: set {{GATEWAY_URL_ENV}} or write KEY=VALUE pairs to {{_config_file_path()}}", err=True)
221
+ sys.exit(2)
222
+ return url.rstrip("/")
223
+
224
+
225
+ def _role():
226
+ return os.environ.get(ROLE_ENV, DEFAULT_ROLE)
227
+
228
+
229
+ def _post_step(payload):
230
+ response = httpx.post(
231
+ f"{{_gateway_url()}}/step",
232
+ json=payload,
233
+ headers={{ROLE_HEADER: _role()}},
234
+ timeout=120,
235
+ )
236
+ response.raise_for_status()
237
+ return response.json()
238
+
239
+
240
+ @functools.lru_cache(maxsize=1)
241
+ def _list_tools():
242
+ """Tools for SERVICE_NAME that the current role can access.
243
+
244
+ Combines /step list_tools (role-filtered, flat) with /state (server-grouped)
245
+ to scope the listing to this service's tools only.
246
+ """
247
+ role_filtered = _post_step({{"action": "list_tools"}})
248
+ allowed_names = {{t.get("name") for t in (role_filtered.get("tools") or []) if t.get("name")}}
249
+ state_resp = httpx.get(
250
+ f"{{_gateway_url()}}/state",
251
+ headers={{ROLE_HEADER: _role()}},
252
+ timeout=120,
253
+ )
254
+ state_resp.raise_for_status()
255
+ for server in (state_resp.json().get("mcp_servers") or []):
256
+ if server.get("name") == SERVICE_NAME:
257
+ return tuple(t for t in (server.get("tools") or []) if t.get("name") in allowed_names)
258
+ return ()
259
+
260
+
261
+ def _decode_json_flag(raw, schema_type):
262
+ if raw is None:
263
+ return None
264
+ if schema_type in ("array", "object", None):
265
+ try:
266
+ return json.loads(raw)
267
+ except json.JSONDecodeError:
268
+ click.echo(f"error: expected JSON for value, got {{raw!r}}", err=True)
269
+ sys.exit(2)
270
+ return raw
271
+
272
+
273
+ def _call_tool(tool_name, arguments):
274
+ body = _post_step({{"action": "call_tool", "tool_name": tool_name, "arguments": arguments}})
275
+ if "error" in body:
276
+ click.echo(f"error: {{body['error']}}", err=True)
277
+ sys.exit(1)
278
+ if body.get("isError"):
279
+ text_blocks = [b.get("text", "") for b in body.get("content", []) if b.get("type") == "text"]
280
+ click.echo(f"error: {{''.join(text_blocks) or 'tool returned isError=True'}}", err=True)
281
+ sys.exit(1)
282
+ text = "".join(b.get("text", "") for b in body.get("content", []) if b.get("type") == "text")
283
+ try:
284
+ click.echo(json.dumps(json.loads(text), indent=2))
285
+ except json.JSONDecodeError:
286
+ click.echo(text)
287
+
288
+
289
+ def _subcommand_name(tool_name):
290
+ return re.sub(r"_+", "-", tool_name).strip("-")
291
+
292
+
293
+ def _python_identifier(name):
294
+ sanitized = re.sub(r"[^0-9a-zA-Z_]", "_", name)
295
+ if sanitized and sanitized[0].isdigit():
296
+ sanitized = f"_{{sanitized}}"
297
+ if not sanitized:
298
+ sanitized = "_arg"
299
+ if keyword.iskeyword(sanitized):
300
+ sanitized = f"{{sanitized}}_"
301
+ return sanitized
302
+
303
+
304
+ def _normalize_schema(param_schema):
305
+ schema = param_schema
306
+ schema_type = schema.get("type")
307
+ if isinstance(schema_type, list):
308
+ non_null = [t for t in schema_type if t != "null"]
309
+ if len(non_null) == 1:
310
+ schema = {{**schema, "type": non_null[0]}}
311
+ schema_type = non_null[0]
312
+ else:
313
+ schema_type = None
314
+ any_of = schema.get("anyOf") or schema.get("oneOf")
315
+ if any_of and schema_type is None:
316
+ non_null = [s for s in any_of if s.get("type") != "null"]
317
+ if len(non_null) == 1:
318
+ inner = non_null[0]
319
+ schema = {{**inner, "description": schema.get("description") or inner.get("description"), "default": schema.get("default", inner.get("default"))}}
320
+ schema_type = inner.get("type")
321
+ return schema_type, schema
322
+
323
+
324
+ def _click_type_for_schema(param_schema):
325
+ """Return (effective_json_type, click_type_obj)."""
326
+ schema_type, normalized = _normalize_schema(param_schema)
327
+ if schema_type == "string":
328
+ enum_values = normalized.get("enum") or []
329
+ if enum_values:
330
+ return "string", click.Choice([str(v) for v in enum_values])
331
+ return "string", str
332
+ if schema_type == "integer":
333
+ return "integer", int
334
+ if schema_type == "number":
335
+ return "number", float
336
+ if schema_type == "boolean":
337
+ return "boolean", bool
338
+ return schema_type, str
339
+
340
+
341
+ def _format_help(text, schema_type):
342
+ base = (text or "").strip().replace("\\n", " ")
343
+ if schema_type in ("array", "object", None):
344
+ suffix = " (JSON-encoded)"
345
+ return (base + suffix) if base else suffix.lstrip()
346
+ return base
347
+
348
+
349
+ def _build_command(
350
+ tool,
351
+ command_name=None,
352
+ parameter_order=None,
353
+ parameter_overrides=None,
354
+ required_override=None,
355
+ help_override=None,
356
+ ):
357
+ tool_name = tool["name"]
358
+ description = (help_override or tool.get("description") or "").strip().replace("\\n", " ")
359
+ schema = tool.get("parameters") or {{}}
360
+ properties = schema.get("properties") or {{}}
361
+ required_set = set(
362
+ schema.get("required") or []
363
+ if required_override is None
364
+ else required_override
365
+ )
366
+ parameter_overrides = parameter_overrides or {{}}
367
+
368
+ options = []
369
+ arg_meta = []
370
+ seen_py_names = set()
371
+
372
+ ordered_names = []
373
+ for original_name in (parameter_order or []):
374
+ if original_name in properties and original_name not in ordered_names:
375
+ ordered_names.append(original_name)
376
+ ordered_names.extend(name for name in properties if name not in ordered_names)
377
+
378
+ # Both sides derive from the same OpenAPI spec, so drift means the repos are
379
+ # out of sync. Warn rather than fail: the call still uses the live schema.
380
+ if required_override is not None:
381
+ for name in (parameter_order or []):
382
+ if name not in properties:
383
+ click.echo(
384
+ f"warning: {{tool_name}}: manifest param {{name!r}} is absent from the "
385
+ f"live tool schema and was dropped",
386
+ err=True,
387
+ )
388
+ for name in (schema.get("required") or []):
389
+ if name not in required_set:
390
+ click.echo(
391
+ f"warning: {{tool_name}}: live tool requires {{name!r}} but the manifest "
392
+ f"does not; rendering it as optional",
393
+ err=True,
394
+ )
395
+
396
+ for original_name in ordered_names:
397
+ param_schema = {{
398
+ **properties[original_name],
399
+ **parameter_overrides.get(original_name, {{}}),
400
+ }}
401
+ flag = "--" + re.sub(r"_+", "-", original_name).strip("-").lower()
402
+ py_name = _python_identifier(original_name)
403
+ candidate, idx = py_name, 1
404
+ while candidate in seen_py_names:
405
+ idx += 1
406
+ candidate = f"{{py_name}}{{idx}}"
407
+ py_name = candidate
408
+ seen_py_names.add(py_name)
409
+ required = original_name in required_set
410
+
411
+ effective_type, click_type = _click_type_for_schema(param_schema)
412
+ help_text = _format_help(param_schema.get("description"), effective_type)
413
+
414
+ if effective_type == "boolean":
415
+ if "default" in param_schema:
416
+ opt = click.Option(
417
+ [f"{{flag}}/--no-{{flag.lstrip('-')}}", py_name],
418
+ default=bool(param_schema["default"]),
419
+ help=help_text,
420
+ )
421
+ else:
422
+ opt = click.Option(
423
+ [flag, py_name],
424
+ is_flag=True,
425
+ default=False,
426
+ help=help_text,
427
+ )
428
+ else:
429
+ opt = click.Option(
430
+ [flag, py_name],
431
+ type=click_type,
432
+ required=required,
433
+ default=param_schema.get("default") if not required else None,
434
+ help=help_text,
435
+ )
436
+ options.append(opt)
437
+ arg_meta.append((py_name, original_name, effective_type))
438
+
439
+ def _callback(**kwargs):
440
+ arguments = {{}}
441
+ for py_name, original_name, effective_type in arg_meta:
442
+ value = kwargs.get(py_name)
443
+ if effective_type == "boolean":
444
+ arguments[original_name] = value
445
+ elif value is not None:
446
+ if effective_type in ("array", "object", None):
447
+ arguments[original_name] = _decode_json_flag(value, effective_type)
448
+ else:
449
+ arguments[original_name] = value
450
+ _call_tool(tool_name, arguments)
451
+
452
+ return click.Command(
453
+ name=command_name or _subcommand_name(tool_name),
454
+ params=options,
455
+ callback=_callback,
456
+ help=description,
457
+ )
458
+
459
+
460
+ def _command_rendering(command):
461
+ # The CLI projection already carries each command's final params (required +
462
+ # optional, enums included); the live tool schema fills in the rest.
463
+ params = command.get("params") or []
464
+ ordered = [p.get("name") for p in params if p.get("name")]
465
+ required = [p.get("name") for p in params if p.get("required") and p.get("name")]
466
+ overrides = {{}}
467
+ for p in params:
468
+ name = p.get("name")
469
+ if not name:
470
+ continue
471
+ override = {{key: p[key] for key in ("type", "enum") if p.get(key) is not None}}
472
+ if override:
473
+ overrides[name] = override
474
+ return ordered, overrides, required
475
+
476
+
477
+ def _build_entity_group(entity, tools_by_name):
478
+ commands = {{}}
479
+ for verb, command in (entity.get("commands") or {{}}).items():
480
+ tool = tools_by_name.get(command.get("tool"))
481
+ if tool is None:
482
+ continue
483
+ ordered, overrides, required = _command_rendering(command)
484
+ commands[verb] = _build_command(
485
+ tool,
486
+ command_name=verb,
487
+ parameter_order=ordered,
488
+ parameter_overrides=overrides,
489
+ required_override=required,
490
+ help_override=command.get("summary") or command.get("description"),
491
+ )
492
+ return click.Group(
493
+ name=entity["_command_name"],
494
+ commands=commands,
495
+ help=f"Commands for {{entity.get('entity')}}.",
496
+ )
497
+
498
+
499
+ def _build_actions_group(actions, tools_by_name):
500
+ commands = {{}}
501
+ for action in actions:
502
+ tool = tools_by_name.get(action.get("tool"))
503
+ if tool is None:
504
+ continue
505
+ name = action["_command_name"]
506
+ ordered, overrides, required = _command_rendering(action)
507
+ commands[name] = _build_command(
508
+ tool,
509
+ command_name=name,
510
+ parameter_order=ordered,
511
+ parameter_overrides=overrides,
512
+ required_override=required,
513
+ help_override=action.get("description"),
514
+ )
515
+ return click.Group(
516
+ name="actions",
517
+ commands=commands,
518
+ help="Non-CRUD service actions.",
519
+ )
520
+
521
+
522
+ class _DynamicGroup(click.Group):
523
+ def list_commands(self, ctx):
524
+ try:
525
+ tools = _list_tools()
526
+ except Exception as e:
527
+ click.echo(f"warning: could not fetch tools from gateway: {{e}}", err=True)
528
+ return []
529
+ names = []
530
+ seen = set()
531
+ for t in tools:
532
+ tname = t.get("name")
533
+ if not tname:
534
+ continue
535
+ sub = _subcommand_name(tname)
536
+ if sub in seen:
537
+ continue
538
+ seen.add(sub)
539
+ names.append(sub)
540
+ return sorted(names)
541
+
542
+ def get_command(self, ctx, name):
543
+ try:
544
+ tools = _list_tools()
545
+ except Exception as e:
546
+ click.echo(f"error: could not fetch tools from gateway: {{e}}", err=True)
547
+ return None
548
+ for t in tools:
549
+ tname = t.get("name")
550
+ if tname and _subcommand_name(tname) == name:
551
+ return _build_command(t)
552
+ return None
553
+
554
+
555
+ class _ManifestGroup(click.Group):
556
+ def _tools_by_name(self):
557
+ return {{tool["name"]: tool for tool in _list_tools() if tool.get("name")}}
558
+
559
+ def list_commands(self, ctx):
560
+ try:
561
+ tools_by_name = self._tools_by_name()
562
+ except Exception as e:
563
+ click.echo(f"warning: could not fetch tools from gateway: {{e}}", err=True)
564
+ return []
565
+ names = []
566
+ for entity in (INTERFACE_MANIFEST.get("entities") or []):
567
+ commands = entity.get("commands") or {{}}
568
+ live = {{
569
+ verb: cmd
570
+ for verb, cmd in commands.items()
571
+ if cmd.get("tool") in tools_by_name
572
+ }}
573
+ if not live:
574
+ continue
575
+ # A namesake entity has no group name; its verbs sit at the root.
576
+ if entity.get("_command_name") is None:
577
+ names.extend(live)
578
+ else:
579
+ names.append(entity["_command_name"])
580
+ if any(
581
+ action.get("tool") in tools_by_name
582
+ for action in (INTERFACE_MANIFEST.get("actions") or [])
583
+ ):
584
+ names.append("actions")
585
+ return sorted(names)
586
+
587
+ def get_command(self, ctx, name):
588
+ try:
589
+ tools_by_name = self._tools_by_name()
590
+ except Exception as e:
591
+ click.echo(f"error: could not fetch tools from gateway: {{e}}", err=True)
592
+ return None
593
+ if name == "actions":
594
+ return _build_actions_group(
595
+ INTERFACE_MANIFEST.get("actions") or [],
596
+ tools_by_name,
597
+ )
598
+ for entity in INTERFACE_MANIFEST.get("entities") or []:
599
+ if entity.get("_command_name") is None:
600
+ command = (entity.get("commands") or {{}}).get(name)
601
+ tool = tools_by_name.get((command or {{}}).get("tool"))
602
+ if tool is not None:
603
+ ordered, overrides, required = _command_rendering(command)
604
+ return _build_command(
605
+ tool,
606
+ command_name=name,
607
+ parameter_order=ordered,
608
+ parameter_overrides=overrides,
609
+ required_override=required,
610
+ help_override=command.get("summary") or command.get("description"),
611
+ )
612
+ elif entity.get("_command_name") == name:
613
+ return _build_entity_group(entity, tools_by_name)
614
+ return None
615
+
616
+
617
+ _ROOT_GROUP = _ManifestGroup if INTERFACE_MANIFEST is not None else _DynamicGroup
618
+
619
+
620
+ @click.command(cls=_ROOT_GROUP, help="Auto-generated CLI for {command_name}")
621
+ def cli():
622
+ pass
623
+
624
+
625
+ if __name__ == "__main__":
626
+ cli()
627
+ '''
@@ -0,0 +1,44 @@
1
+ TOOL_CORRECTNESS_OUTPUT_FORMAT = {
2
+ "type": "json_schema",
3
+ "schema": {
4
+ "type": "object",
5
+ "properties": {
6
+ "results": {
7
+ "type": "array",
8
+ "items": {
9
+ "type": "object",
10
+ "properties": {
11
+ "tool_name": {"type": "string"},
12
+ "passed": {"type": "boolean"},
13
+ "error": {"type": "string"},
14
+ "justification": {"type": "string"},
15
+ },
16
+ "required": ["tool_name", "passed", "error", "justification"],
17
+ },
18
+ },
19
+ },
20
+ "required": ["results"],
21
+ },
22
+ }
23
+
24
+ TOOL_CORRECTNESS_PROMPT = """You have access to MCP tools. Your job is to validate that each tool works correctly.
25
+
26
+ Assume that some data may already exist in the system.
27
+
28
+ Follow this order:
29
+
30
+ 1. **Create test data first**: Use write/create tools to build up test data. Some writes may depend on other writes (e.g. creating a record may require first creating a table, posting a message may require first having a channel). Figure out the dependency order and create prerequisite data before testing dependent tools.
31
+
32
+ 2. **Read-back verification**: After creating data, use read/get/list tools to verify the data was persisted and matches what was written.
33
+
34
+ 3. **Update verification**: If update tools exist, modify an entity you created and verify the changes are reflected when reading it back.
35
+
36
+ 4. **Delete verification**: If delete tools exist, delete an entity and verify it is no longer returned by read/list tools.
37
+
38
+ 5. **Search verification**: If search tools exist, verify they can find data you created.
39
+
40
+ A tool passes if it behaves correctly — including returning a well-structured error for invalid inputs. A tool fails only if it has an actual defect: crashes, returns malformed data, silently drops writes, or behaves inconsistently with its description.
41
+
42
+ If a tool cannot be tested because there is no way to create its prerequisites through the available toolset (e.g. no create tool exists for a required parent entity), mark it as passed with a justification noting the limitation.
43
+
44
+ Report your findings as structured JSON. For each tool, set passed=true if it works correctly, or passed=false with an error description if it fails."""