gatewaysdk 0.3.2__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 (376) hide show
  1. gatewaysdk/__init__.py +540 -0
  2. gatewaysdk/adapter/__init__.py +15 -0
  3. gatewaysdk/adapter/base.py +94 -0
  4. gatewaysdk/adapter/messages.py +270 -0
  5. gatewaysdk/adapter/triplet.py +1026 -0
  6. gatewaysdk/algorithm/__init__.py +39 -0
  7. gatewaysdk/algorithm/apo/__init__.py +5 -0
  8. gatewaysdk/algorithm/apo/apo.py +898 -0
  9. gatewaysdk/algorithm/apo/prompts/apply_edit_variant01.poml +22 -0
  10. gatewaysdk/algorithm/apo/prompts/apply_edit_variant02.poml +18 -0
  11. gatewaysdk/algorithm/apo/prompts/text_gradient_variant01.poml +18 -0
  12. gatewaysdk/algorithm/apo/prompts/text_gradient_variant02.poml +16 -0
  13. gatewaysdk/algorithm/apo/prompts/text_gradient_variant03.poml +107 -0
  14. gatewaysdk/algorithm/base.py +262 -0
  15. gatewaysdk/algorithm/decorator.py +264 -0
  16. gatewaysdk/algorithm/evals/__init__.py +7 -0
  17. gatewaysdk/algorithm/evals/evals.py +217 -0
  18. gatewaysdk/algorithm/fast.py +250 -0
  19. gatewaysdk/algorithm/gepa/__init__.py +61 -0
  20. gatewaysdk/algorithm/gepa/adapter.py +495 -0
  21. gatewaysdk/algorithm/gepa/gepa.py +570 -0
  22. gatewaysdk/algorithm/gepa/lib/__init__.py +18 -0
  23. gatewaysdk/algorithm/gepa/lib/adapters/README.md +12 -0
  24. gatewaysdk/algorithm/gepa/lib/adapters/__init__.py +0 -0
  25. gatewaysdk/algorithm/gepa/lib/adapters/anymaths_adapter/README.md +341 -0
  26. gatewaysdk/algorithm/gepa/lib/adapters/anymaths_adapter/__init__.py +1 -0
  27. gatewaysdk/algorithm/gepa/lib/adapters/anymaths_adapter/anymaths_adapter.py +174 -0
  28. gatewaysdk/algorithm/gepa/lib/adapters/anymaths_adapter/requirements.txt +1 -0
  29. gatewaysdk/algorithm/gepa/lib/adapters/default_adapter/README.md +0 -0
  30. gatewaysdk/algorithm/gepa/lib/adapters/default_adapter/__init__.py +0 -0
  31. gatewaysdk/algorithm/gepa/lib/adapters/default_adapter/default_adapter.py +209 -0
  32. gatewaysdk/algorithm/gepa/lib/adapters/dspy_adapter/README.md +7 -0
  33. gatewaysdk/algorithm/gepa/lib/adapters/dspy_adapter/__init__.py +0 -0
  34. gatewaysdk/algorithm/gepa/lib/adapters/dspy_adapter/dspy_adapter.py +307 -0
  35. gatewaysdk/algorithm/gepa/lib/adapters/dspy_full_program_adapter/README.md +99 -0
  36. gatewaysdk/algorithm/gepa/lib/adapters/dspy_full_program_adapter/dspy_program_proposal_signature.py +137 -0
  37. gatewaysdk/algorithm/gepa/lib/adapters/dspy_full_program_adapter/full_program_adapter.py +268 -0
  38. gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/GEPA_RAG.md +621 -0
  39. gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/__init__.py +56 -0
  40. gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/evaluation_metrics.py +226 -0
  41. gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/generic_rag_adapter.py +496 -0
  42. gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/rag_pipeline.py +238 -0
  43. gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_store_interface.py +212 -0
  44. gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/__init__.py +2 -0
  45. gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/chroma_store.py +196 -0
  46. gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/lancedb_store.py +422 -0
  47. gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/milvus_store.py +409 -0
  48. gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/qdrant_store.py +368 -0
  49. gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/weaviate_store.py +418 -0
  50. gatewaysdk/algorithm/gepa/lib/adapters/mcp_adapter/README.md +552 -0
  51. gatewaysdk/algorithm/gepa/lib/adapters/mcp_adapter/__init__.py +37 -0
  52. gatewaysdk/algorithm/gepa/lib/adapters/mcp_adapter/mcp_adapter.py +699 -0
  53. gatewaysdk/algorithm/gepa/lib/adapters/mcp_adapter/mcp_client.py +364 -0
  54. gatewaysdk/algorithm/gepa/lib/adapters/terminal_bench_adapter/README.md +9 -0
  55. gatewaysdk/algorithm/gepa/lib/adapters/terminal_bench_adapter/__init__.py +0 -0
  56. gatewaysdk/algorithm/gepa/lib/adapters/terminal_bench_adapter/terminal_bench_adapter.py +217 -0
  57. gatewaysdk/algorithm/gepa/lib/api.py +382 -0
  58. gatewaysdk/algorithm/gepa/lib/core/__init__.py +0 -0
  59. gatewaysdk/algorithm/gepa/lib/core/adapter.py +180 -0
  60. gatewaysdk/algorithm/gepa/lib/core/data_loader.py +74 -0
  61. gatewaysdk/algorithm/gepa/lib/core/engine.py +379 -0
  62. gatewaysdk/algorithm/gepa/lib/core/result.py +233 -0
  63. gatewaysdk/algorithm/gepa/lib/core/state.py +636 -0
  64. gatewaysdk/algorithm/gepa/lib/examples/__init__.py +0 -0
  65. gatewaysdk/algorithm/gepa/lib/examples/aime.py +24 -0
  66. gatewaysdk/algorithm/gepa/lib/examples/anymaths-bench/eval_default.py +111 -0
  67. gatewaysdk/algorithm/gepa/lib/examples/anymaths-bench/prompt-templates/instruction_prompt.txt +9 -0
  68. gatewaysdk/algorithm/gepa/lib/examples/anymaths-bench/prompt-templates/optimal_prompt.txt +24 -0
  69. gatewaysdk/algorithm/gepa/lib/examples/anymaths-bench/train_anymaths.py +177 -0
  70. gatewaysdk/algorithm/gepa/lib/examples/dspy_full_program_evolution/arc_agi.ipynb +25705 -0
  71. gatewaysdk/algorithm/gepa/lib/examples/dspy_full_program_evolution/example.ipynb +348 -0
  72. gatewaysdk/algorithm/gepa/lib/examples/mcp_adapter/__init__.py +4 -0
  73. gatewaysdk/algorithm/gepa/lib/examples/mcp_adapter/mcp_optimization_example.py +456 -0
  74. gatewaysdk/algorithm/gepa/lib/examples/rag_adapter/RAG_GUIDE.md +613 -0
  75. gatewaysdk/algorithm/gepa/lib/examples/rag_adapter/__init__.py +9 -0
  76. gatewaysdk/algorithm/gepa/lib/examples/rag_adapter/rag_optimization.py +820 -0
  77. gatewaysdk/algorithm/gepa/lib/examples/rag_adapter/requirements-rag.txt +29 -0
  78. gatewaysdk/algorithm/gepa/lib/examples/terminal-bench/prompt-templates/instruction_prompt.txt +16 -0
  79. gatewaysdk/algorithm/gepa/lib/examples/terminal-bench/prompt-templates/terminus.txt +9 -0
  80. gatewaysdk/algorithm/gepa/lib/examples/terminal-bench/train_terminus.py +161 -0
  81. gatewaysdk/algorithm/gepa/lib/gepa_utils.py +117 -0
  82. gatewaysdk/algorithm/gepa/lib/logging/__init__.py +0 -0
  83. gatewaysdk/algorithm/gepa/lib/logging/experiment_tracker.py +187 -0
  84. gatewaysdk/algorithm/gepa/lib/logging/logger.py +75 -0
  85. gatewaysdk/algorithm/gepa/lib/logging/utils.py +103 -0
  86. gatewaysdk/algorithm/gepa/lib/proposer/__init__.py +0 -0
  87. gatewaysdk/algorithm/gepa/lib/proposer/base.py +31 -0
  88. gatewaysdk/algorithm/gepa/lib/proposer/merge.py +357 -0
  89. gatewaysdk/algorithm/gepa/lib/proposer/reflective_mutation/__init__.py +0 -0
  90. gatewaysdk/algorithm/gepa/lib/proposer/reflective_mutation/base.py +49 -0
  91. gatewaysdk/algorithm/gepa/lib/proposer/reflective_mutation/reflective_mutation.py +176 -0
  92. gatewaysdk/algorithm/gepa/lib/py.typed +0 -0
  93. gatewaysdk/algorithm/gepa/lib/strategies/__init__.py +0 -0
  94. gatewaysdk/algorithm/gepa/lib/strategies/batch_sampler.py +77 -0
  95. gatewaysdk/algorithm/gepa/lib/strategies/candidate_selector.py +50 -0
  96. gatewaysdk/algorithm/gepa/lib/strategies/component_selector.py +36 -0
  97. gatewaysdk/algorithm/gepa/lib/strategies/eval_policy.py +64 -0
  98. gatewaysdk/algorithm/gepa/lib/strategies/instruction_proposal.py +126 -0
  99. gatewaysdk/algorithm/gepa/lib/utils/__init__.py +10 -0
  100. gatewaysdk/algorithm/gepa/lib/utils/stop_condition.py +196 -0
  101. gatewaysdk/algorithm/gepa/tracing.py +105 -0
  102. gatewaysdk/algorithm/utils.py +177 -0
  103. gatewaysdk/algorithm/verl/__init__.py +5 -0
  104. gatewaysdk/algorithm/verl/interface.py +202 -0
  105. gatewaysdk/automations.py +111 -0
  106. gatewaysdk/benchmark_hub/README.md +88 -0
  107. gatewaysdk/benchmark_hub/__init__.py +119 -0
  108. gatewaysdk/benchmark_hub/_archive.py +189 -0
  109. gatewaysdk/benchmark_hub/_tar.py +6 -0
  110. gatewaysdk/benchmark_hub/client.py +992 -0
  111. gatewaysdk/benchmark_hub/dispatch_shard.py +34 -0
  112. gatewaysdk/benchmark_hub/eval_config.py +20 -0
  113. gatewaysdk/benchmark_hub/evals.py +351 -0
  114. gatewaysdk/benchmark_hub/harbor_adapter.py +673 -0
  115. gatewaysdk/benchmark_hub/path_utils.py +81 -0
  116. gatewaysdk/benchmark_hub/save_utils.py +101 -0
  117. gatewaysdk/benchmark_hub/verifiers_adapter.py +327 -0
  118. gatewaysdk/benchmark_hub/versioning.py +72 -0
  119. gatewaysdk/build.py +515 -0
  120. gatewaysdk/cli/__init__.py +58 -0
  121. gatewaysdk/cli/agent_runner.py +132 -0
  122. gatewaysdk/cli/http_client.py +115 -0
  123. gatewaysdk/cli/platform.py +4086 -0
  124. gatewaysdk/cli/prometheus.py +115 -0
  125. gatewaysdk/cli/release_gate.py +215 -0
  126. gatewaysdk/cli/store.py +131 -0
  127. gatewaysdk/cli/vllm.py +29 -0
  128. gatewaysdk/client.py +406 -0
  129. gatewaysdk/config.py +348 -0
  130. gatewaysdk/connectors/__init__.py +25 -0
  131. gatewaysdk/connectors/client.py +203 -0
  132. gatewaysdk/connectors/skill.py +25 -0
  133. gatewaysdk/connectors/template.py +106 -0
  134. gatewaysdk/context.py +606 -0
  135. gatewaysdk/emitter/__init__.py +43 -0
  136. gatewaysdk/emitter/annotation.py +370 -0
  137. gatewaysdk/emitter/exception.py +54 -0
  138. gatewaysdk/emitter/message.py +61 -0
  139. gatewaysdk/emitter/object.py +117 -0
  140. gatewaysdk/emitter/reward.py +320 -0
  141. gatewaysdk/env_var.py +156 -0
  142. gatewaysdk/environment/__init__.py +108 -0
  143. gatewaysdk/environment/_bundle.py +281 -0
  144. gatewaysdk/environment/_harbor.py +276 -0
  145. gatewaysdk/environment/_materialize.py +97 -0
  146. gatewaysdk/environment/_tar.py +33 -0
  147. gatewaysdk/environment/_world.py +346 -0
  148. gatewaysdk/environment/core.py +527 -0
  149. gatewaysdk/environment/errors.py +58 -0
  150. gatewaysdk/environment/runtime.py +150 -0
  151. gatewaysdk/environment/schema/__init__.py +35 -0
  152. gatewaysdk/environment/schema/__main__.py +225 -0
  153. gatewaysdk/environment/schema/_slack_fidelity.py +116 -0
  154. gatewaysdk/environment/schema/_slack_scenario.py +183 -0
  155. gatewaysdk/environment/schema/api.py +1628 -0
  156. gatewaysdk/environment/schema/batch.py +755 -0
  157. gatewaysdk/environment/schema/compiler.py +615 -0
  158. gatewaysdk/environment/schema/conform.py +428 -0
  159. gatewaysdk/environment/schema/connector.py +777 -0
  160. gatewaysdk/environment/schema/connectors/apple-business-manager/handlers.py +17 -0
  161. gatewaysdk/environment/schema/connectors/apple-business-manager/parity.json +28 -0
  162. gatewaysdk/environment/schema/connectors/apple-business-manager/provenance.json +233 -0
  163. gatewaysdk/environment/schema/connectors/apple-business-manager/scope.toml +112 -0
  164. gatewaysdk/environment/schema/connectors/apple-business-manager/world.json +1380 -0
  165. gatewaysdk/environment/schema/connectors/base.json +28 -0
  166. gatewaysdk/environment/schema/connectors/custom/handlers.py +18 -0
  167. gatewaysdk/environment/schema/connectors/custom/world.json +123 -0
  168. gatewaysdk/environment/schema/connectors/github/handlers.py +81 -0
  169. gatewaysdk/environment/schema/connectors/github/parity.json +37 -0
  170. gatewaysdk/environment/schema/connectors/github/provenance.json +134 -0
  171. gatewaysdk/environment/schema/connectors/github/scope.toml +158 -0
  172. gatewaysdk/environment/schema/connectors/github/world.json +5869 -0
  173. gatewaysdk/environment/schema/connectors/google-calendar/handlers.py +23 -0
  174. gatewaysdk/environment/schema/connectors/google-calendar/world.json +209 -0
  175. gatewaysdk/environment/schema/connectors/google-drive/handlers.py +18 -0
  176. gatewaysdk/environment/schema/connectors/google-drive/world.json +286 -0
  177. gatewaysdk/environment/schema/connectors/jamf/handlers.py +34 -0
  178. gatewaysdk/environment/schema/connectors/jamf/parity.json +49 -0
  179. gatewaysdk/environment/schema/connectors/jamf/provenance.json +181 -0
  180. gatewaysdk/environment/schema/connectors/jamf/scope.toml +128 -0
  181. gatewaysdk/environment/schema/connectors/jamf/world.json +2218 -0
  182. gatewaysdk/environment/schema/connectors/jira/handlers.py +28 -0
  183. gatewaysdk/environment/schema/connectors/jira/world.json +389 -0
  184. gatewaysdk/environment/schema/connectors/kandji/handlers.py +38 -0
  185. gatewaysdk/environment/schema/connectors/kandji/parity.json +29 -0
  186. gatewaysdk/environment/schema/connectors/kandji/provenance.json +152 -0
  187. gatewaysdk/environment/schema/connectors/kandji/scope.toml +120 -0
  188. gatewaysdk/environment/schema/connectors/kandji/world.json +765 -0
  189. gatewaysdk/environment/schema/connectors/linear/handlers.py +20 -0
  190. gatewaysdk/environment/schema/connectors/linear/world.json +363 -0
  191. gatewaysdk/environment/schema/connectors/microsoft-teams/fidelity.json +45 -0
  192. gatewaysdk/environment/schema/connectors/microsoft-teams/handlers.py +1 -0
  193. gatewaysdk/environment/schema/connectors/microsoft-teams/provenance.json +851 -0
  194. gatewaysdk/environment/schema/connectors/microsoft-teams/scope.md +231 -0
  195. gatewaysdk/environment/schema/connectors/microsoft-teams/teams-types.json +1710 -0
  196. gatewaysdk/environment/schema/connectors/microsoft-teams/world.json +858 -0
  197. gatewaysdk/environment/schema/connectors/netsuite/handlers.py +32 -0
  198. gatewaysdk/environment/schema/connectors/netsuite/parity.json +50 -0
  199. gatewaysdk/environment/schema/connectors/netsuite/provenance.json +137 -0
  200. gatewaysdk/environment/schema/connectors/netsuite/scope.toml +78 -0
  201. gatewaysdk/environment/schema/connectors/netsuite/world.json +16243 -0
  202. gatewaysdk/environment/schema/connectors/salesforce/handlers.py +806 -0
  203. gatewaysdk/environment/schema/connectors/salesforce/parity.json +46 -0
  204. gatewaysdk/environment/schema/connectors/salesforce/provenance.json +143 -0
  205. gatewaysdk/environment/schema/connectors/salesforce/scope.toml +174 -0
  206. gatewaysdk/environment/schema/connectors/salesforce/world.json +4695 -0
  207. gatewaysdk/environment/schema/connectors/slack/MODEL.md +236 -0
  208. gatewaysdk/environment/schema/connectors/slack/capabilities.json +1039 -0
  209. gatewaysdk/environment/schema/connectors/slack/handlers.py +762 -0
  210. gatewaysdk/environment/schema/connectors/slack/parity.json +65 -0
  211. gatewaysdk/environment/schema/connectors/slack/provenance.json +294 -0
  212. gatewaysdk/environment/schema/connectors/slack/scope.toml +44 -0
  213. gatewaysdk/environment/schema/connectors/slack/world.json +4993 -0
  214. gatewaysdk/environment/schema/connectors/workday/handlers.py +75 -0
  215. gatewaysdk/environment/schema/connectors/workday/parity.json +36 -0
  216. gatewaysdk/environment/schema/connectors/workday/provenance.json +179 -0
  217. gatewaysdk/environment/schema/connectors/workday/scope.toml +180 -0
  218. gatewaysdk/environment/schema/connectors/workday/world.json +806 -0
  219. gatewaysdk/environment/schema/data.py +593 -0
  220. gatewaysdk/environment/schema/extract.py +263 -0
  221. gatewaysdk/environment/schema/host.py +2437 -0
  222. gatewaysdk/environment/schema/host_surfaces.py +275 -0
  223. gatewaysdk/environment/schema/platform.py +1272 -0
  224. gatewaysdk/environment/schema/scaffold.py +250 -0
  225. gatewaysdk/environment/schema/skill.py +118 -0
  226. gatewaysdk/environment/schema/skills/agents/connector-template-author.md +202 -0
  227. gatewaysdk/environment/schema/skills/connector-schema-authoring/CONNECTORS.md +331 -0
  228. gatewaysdk/environment/schema/skills/connector-schema-authoring/CONVENTIONS.md +100 -0
  229. gatewaysdk/environment/schema/skills/connector-schema-authoring/SKILL.md +313 -0
  230. gatewaysdk/environment/schema/skills/world-data-ingestion/ROWS.md +223 -0
  231. gatewaysdk/environment/schema/skills/world-data-ingestion/SKILL.md +293 -0
  232. gatewaysdk/environment/schema/skills/worlds-getting-started/SKILL.md +390 -0
  233. gatewaysdk/environment/schema/slack.py +757 -0
  234. gatewaysdk/environment/schema/snapshot.py +479 -0
  235. gatewaysdk/environment/schema/store.py +904 -0
  236. gatewaysdk/environment/schema/tasks.py +525 -0
  237. gatewaysdk/environment/schema/tools_world.py +322 -0
  238. gatewaysdk/environment/schema/validation.py +577 -0
  239. gatewaysdk/environment/schema/worker.py +78 -0
  240. gatewaysdk/environment/schema/workers.py +500 -0
  241. gatewaysdk/environment/schema/world_tests.py +336 -0
  242. gatewaysdk/environments/__init__.py +24 -0
  243. gatewaysdk/environments/client.py +486 -0
  244. gatewaysdk/environments/types.py +318 -0
  245. gatewaysdk/execution/__init__.py +15 -0
  246. gatewaysdk/execution/base.py +64 -0
  247. gatewaysdk/execution/client_server.py +443 -0
  248. gatewaysdk/execution/events.py +69 -0
  249. gatewaysdk/execution/inter_process.py +16 -0
  250. gatewaysdk/execution/shared_memory.py +282 -0
  251. gatewaysdk/experiments/__init__.py +90 -0
  252. gatewaysdk/experiments/assignment.py +177 -0
  253. gatewaysdk/experiments/client.py +877 -0
  254. gatewaysdk/experiments/exposure.py +222 -0
  255. gatewaysdk/experiments/types.py +81 -0
  256. gatewaysdk/importers/__init__.py +49 -0
  257. gatewaysdk/importers/_normalize.py +91 -0
  258. gatewaysdk/importers/builder.py +173 -0
  259. gatewaysdk/importers/client.py +95 -0
  260. gatewaysdk/importers/langsmith.py +196 -0
  261. gatewaysdk/importers/recipes.py +249 -0
  262. gatewaysdk/instrumentation/__init__.py +300 -0
  263. gatewaysdk/instrumentation/agentops.py +314 -0
  264. gatewaysdk/instrumentation/agentops_langchain.py +45 -0
  265. gatewaysdk/instrumentation/base.py +119 -0
  266. gatewaysdk/instrumentation/litellm.py +83 -0
  267. gatewaysdk/instrumentation/registry.py +273 -0
  268. gatewaysdk/instrumentation/vllm.py +81 -0
  269. gatewaysdk/instrumentation/weave.py +500 -0
  270. gatewaysdk/integrations/__init__.py +15 -0
  271. gatewaysdk/integrations/gateway/__init__.py +11 -0
  272. gatewaysdk/integrations/gateway/client.py +171 -0
  273. gatewaysdk/integrations/tool_access.py +549 -0
  274. gatewaysdk/litagent/__init__.py +11 -0
  275. gatewaysdk/litagent/decorator.py +536 -0
  276. gatewaysdk/litagent/litagent.py +252 -0
  277. gatewaysdk/llm_proxy.py +1742 -0
  278. gatewaysdk/logging.py +370 -0
  279. gatewaysdk/memory.py +278 -0
  280. gatewaysdk/personas.py +186 -0
  281. gatewaysdk/platform/__init__.py +17 -0
  282. gatewaysdk/platform/builder.py +221 -0
  283. gatewaysdk/platform/compatibility.py +67 -0
  284. gatewaysdk/platform/manifest.py +185 -0
  285. gatewaysdk/platform/orchestrator.py +901 -0
  286. gatewaysdk/platform/registry.py +122 -0
  287. gatewaysdk/platform/worker.py +864 -0
  288. gatewaysdk/replay/__init__.py +1032 -0
  289. gatewaysdk/replay/pytest.py +56 -0
  290. gatewaysdk/reward.py +7 -0
  291. gatewaysdk/run.py +1781 -0
  292. gatewaysdk/runner/__init__.py +11 -0
  293. gatewaysdk/runner/agent.py +878 -0
  294. gatewaysdk/runner/base.py +182 -0
  295. gatewaysdk/runner/legacy.py +309 -0
  296. gatewaysdk/security.py +700 -0
  297. gatewaysdk/semconv.py +170 -0
  298. gatewaysdk/server.py +399 -0
  299. gatewaysdk/sessions.py +282 -0
  300. gatewaysdk/store/__init__.py +45 -0
  301. gatewaysdk/store/base.py +908 -0
  302. gatewaysdk/store/client_server.py +2093 -0
  303. gatewaysdk/store/collection/__init__.py +30 -0
  304. gatewaysdk/store/collection/base.py +587 -0
  305. gatewaysdk/store/collection/memory.py +970 -0
  306. gatewaysdk/store/collection/mongo.py +1412 -0
  307. gatewaysdk/store/collection_based.py +1823 -0
  308. gatewaysdk/store/gateway.py +983 -0
  309. gatewaysdk/store/gateway_listener.py +465 -0
  310. gatewaysdk/store/listener.py +58 -0
  311. gatewaysdk/store/memory.py +396 -0
  312. gatewaysdk/store/mongo.py +165 -0
  313. gatewaysdk/store/redis_stream.py +517 -0
  314. gatewaysdk/store/sqlite.py +3 -0
  315. gatewaysdk/store/threading.py +370 -0
  316. gatewaysdk/store/utils.py +142 -0
  317. gatewaysdk/tracer/__init__.py +14 -0
  318. gatewaysdk/tracer/base.py +286 -0
  319. gatewaysdk/tracer/dummy.py +106 -0
  320. gatewaysdk/tracer/otel.py +559 -0
  321. gatewaysdk/tracing/__init__.py +110 -0
  322. gatewaysdk/tracing/api.py +808 -0
  323. gatewaysdk/tracing/attributes.py +9 -0
  324. gatewaysdk/tracing/context.py +272 -0
  325. gatewaysdk/tracing/exporters/__init__.py +10 -0
  326. gatewaysdk/tracing/exporters/gateway.py +228 -0
  327. gatewaysdk/tracing/identity.py +288 -0
  328. gatewaysdk/tracing/init.py +620 -0
  329. gatewaysdk/tracing/instrumentors/__init__.py +15 -0
  330. gatewaysdk/tracing/instrumentors/claude_agent_sdk.py +766 -0
  331. gatewaysdk/tracing/instrumentors/instrumentation_principles.md +294 -0
  332. gatewaysdk/tracing/instrumentors/registry.py +352 -0
  333. gatewaysdk/tracing/mapping.py +729 -0
  334. gatewaysdk/tracing/processors.py +393 -0
  335. gatewaysdk/tracing/push.py +617 -0
  336. gatewaysdk/tracing/push_models.py +247 -0
  337. gatewaysdk/tracing/semconv.py +294 -0
  338. gatewaysdk/tracing/span_builder.py +356 -0
  339. gatewaysdk/trainer/__init__.py +6 -0
  340. gatewaysdk/trainer/init_utils.py +263 -0
  341. gatewaysdk/trainer/legacy.py +359 -0
  342. gatewaysdk/trainer/registry.py +12 -0
  343. gatewaysdk/trainer/trainer.py +638 -0
  344. gatewaysdk/types/__init__.py +63 -0
  345. gatewaysdk/types/core.py +556 -0
  346. gatewaysdk/types/resources.py +204 -0
  347. gatewaysdk/types/tracer.py +515 -0
  348. gatewaysdk/types/tracing.py +162 -0
  349. gatewaysdk/users.py +251 -0
  350. gatewaysdk/utils/__init__.py +1 -0
  351. gatewaysdk/utils/id.py +18 -0
  352. gatewaysdk/utils/metrics.py +1025 -0
  353. gatewaysdk/utils/otel.py +550 -0
  354. gatewaysdk/utils/otlp.py +556 -0
  355. gatewaysdk/utils/redact.py +22 -0
  356. gatewaysdk/utils/server_launcher.py +1045 -0
  357. gatewaysdk/utils/system_snapshot.py +90 -0
  358. gatewaysdk/verl/__init__.py +8 -0
  359. gatewaysdk/verl/__main__.py +6 -0
  360. gatewaysdk/verl/async_server.py +46 -0
  361. gatewaysdk/verl/config.yaml +27 -0
  362. gatewaysdk/verl/daemon.py +1154 -0
  363. gatewaysdk/verl/dataset.py +44 -0
  364. gatewaysdk/verl/entrypoint.py +248 -0
  365. gatewaysdk/verl/trainer.py +549 -0
  366. gatewaysdk/world_browser.py +748 -0
  367. gatewaysdk/world_data.py +453 -0
  368. gatewaysdk/world_sessions.py +978 -0
  369. gatewaysdk/world_tasks.py +285 -0
  370. gatewaysdk/world_tools.py +115 -0
  371. gatewaysdk/worlds.py +639 -0
  372. gatewaysdk-0.3.2.dist-info/METADATA +236 -0
  373. gatewaysdk-0.3.2.dist-info/RECORD +376 -0
  374. gatewaysdk-0.3.2.dist-info/WHEEL +4 -0
  375. gatewaysdk-0.3.2.dist-info/entry_points.txt +5 -0
  376. gatewaysdk-0.3.2.dist-info/licenses/LICENSE +19 -0
@@ -0,0 +1,1272 @@
1
+ """Credential-free platform bridge for authored schema bundles.
2
+
3
+ Validation parses Python syntax without importing it. Authored code executes only
4
+ inside the hosted runner or an isolated workbench, never the publication server.
5
+ The stdio protocol deliberately contains bundle bytes and mapping context only.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import ast
11
+ import base64
12
+ import binascii
13
+ import gzip
14
+ import io
15
+ import json
16
+ import shutil
17
+ import sys
18
+ import tarfile
19
+ import tempfile
20
+ from pathlib import Path, PurePosixPath
21
+ from typing import Any
22
+
23
+ from .._materialize import materialize_template
24
+ from . import ContractError, SQLiteStore, bundle_mode, check_bundle, compile_bundle
25
+ from . import snapshot as snapshots
26
+ from .validation import (
27
+ digest,
28
+ encoded,
29
+ fail,
30
+ journal_paths,
31
+ members,
32
+ nonempty,
33
+ read_json,
34
+ )
35
+
36
+ MAX_BYTES = 64 * 1024 * 1024
37
+ MAX_FILES = 2000
38
+ COMMANDS = (
39
+ "validate",
40
+ "validate-archive",
41
+ "init-template",
42
+ "init-template-files",
43
+ "template-summary",
44
+ "describe",
45
+ "compile",
46
+ "apply-batch",
47
+ "build-slack",
48
+ "refresh-slack",
49
+ "capture",
50
+ "import-data",
51
+ "ingest",
52
+ "conform",
53
+ "counts",
54
+ "api-bundle",
55
+ "templates",
56
+ "validate-rows",
57
+ "snapshot-build",
58
+ "snapshot-default",
59
+ "run-tests",
60
+ )
61
+ PLATFORM_MAPPING = {"id": "platform-agent", "version": "1"}
62
+
63
+
64
+ def compile_world(root: Path) -> dict:
65
+ """Authored files -> generated artifacts, on an arbitrary schema world.
66
+
67
+ The platform stores compiled trees, so a world whose authored inputs changed
68
+ (``schema/world.json``, ``gateway-env.toml``, handlers) is recompiled here
69
+ rather than rejected with ``lock_mismatch``. Only a journal that holds no
70
+ state (``mutations == []``) of the same domain ``$id`` follows the contract to
71
+ its new identity; a journal with rows is refused, never migrated silently —
72
+ state migration stays manual (re-import the rows against the new schema).
73
+ Journals from a different domain are refused outright. The snapshots are then
74
+ rebuilt loudly: data the contract refuses is a rejection, not a dropped snapshot.
75
+ """
76
+ from .snapshot import build_all
77
+
78
+ if bundle_mode(root) is None:
79
+ fail(
80
+ "mode",
81
+ "Expected a schema-backed mock-tools world (gateway-env.toml with [schema] language)",
82
+ )
83
+ added = []
84
+ base = root / "schema/base.json"
85
+ if not base.exists():
86
+ from .scaffold import TEMPLATES
87
+
88
+ base.parent.mkdir(parents=True, exist_ok=True)
89
+ base.write_bytes((TEMPLATES / "base.json").read_bytes())
90
+ added.append("schema/base.json")
91
+ contract = compile_bundle(root, snapshots=False)
92
+ initial = root / "data/initial.json"
93
+ if not initial.exists():
94
+ initial.parent.mkdir(parents=True, exist_ok=True)
95
+ initial.write_bytes(
96
+ encoded(
97
+ {
98
+ "id": f"{contract.metadata['package']['id']}.initial.v1",
99
+ "target": contract.target,
100
+ "mapping": {"id": "empty", "version": "1"},
101
+ "coverage": "partial",
102
+ "mutations": [],
103
+ }
104
+ )
105
+ )
106
+ added.append("data/initial.json")
107
+ retargeted = []
108
+ for path in journal_paths(root):
109
+ journal = read_json(path)
110
+ location = str(path.relative_to(root))
111
+ if not isinstance(journal, dict) or not isinstance(journal.get("target"), dict):
112
+ fail("batch", "Journal must be a batch object with a target", location)
113
+ if journal["target"] == contract.target:
114
+ continue
115
+ if journal["target"].get("id") != contract.target["id"]:
116
+ fail(
117
+ "batch_target",
118
+ "Journal belongs to a different domain; remove it or restore the schema's $id",
119
+ location,
120
+ )
121
+ if journal.get("mutations") != []:
122
+ fail(
123
+ "batch_target",
124
+ "Batch targets a different domain identity/version/hash; a journal that holds "
125
+ "state is not migrated to an edited schema — re-import its rows against the new "
126
+ "contract (or restore the schema it was written against)",
127
+ location,
128
+ )
129
+ journal["target"] = contract.target
130
+ path.write_bytes(encoded(journal))
131
+ retargeted.append(location)
132
+ build_all(root, contract)
133
+ return {"added": added, "retargeted": retargeted}
134
+
135
+
136
+ def append_batch(root: Path, batch: dict, *, source: str | None = None) -> dict:
137
+ """Validate then append one platform batch as an immutable refresh journal.
138
+
139
+ ``target`` is filled from the compiled contract (a caller-supplied one must
140
+ match); ``mapping`` and ``coverage`` default to the platform mapping and
141
+ ``partial``. Mutations without ``source`` take ``{"record": "<source>#<n>"}``
142
+ when a source prefix is given, otherwise provenance is required per mutation.
143
+ Replaying every prior journal first makes a repeated batch id idempotent and
144
+ a reused id with different content a conflict, exactly as in the store.
145
+ """
146
+ contract = check_bundle(root)
147
+ members(
148
+ batch,
149
+ {"id", "target", "mapping", "coverage", "mutations"},
150
+ {"id", "mutations"},
151
+ "/batch",
152
+ )
153
+ nonempty(batch["id"], "/batch/id")
154
+ if "target" in batch and batch["target"] != contract.target:
155
+ fail(
156
+ "batch_target",
157
+ "Batch targets a different domain identity/version/hash",
158
+ "/batch/target",
159
+ )
160
+ if not isinstance(batch["mutations"], list) or not batch["mutations"]:
161
+ fail("batch", "mutations must be a non-empty array", "/batch/mutations")
162
+ mutations = []
163
+ for index, mutation in enumerate(batch["mutations"]):
164
+ if not isinstance(mutation, dict):
165
+ fail(
166
+ "batch", "Each mutation must be an object", f"/batch/mutations/{index}"
167
+ )
168
+ if "source" not in mutation:
169
+ if source is None:
170
+ fail(
171
+ "provenance",
172
+ "Mutation has no source; pass a source prefix or a source.record per mutation",
173
+ f"/batch/mutations/{index}/source",
174
+ )
175
+ mutation = {**mutation, "source": {"record": f"{source}#{index}"}}
176
+ mutations.append(mutation)
177
+ resolved = {
178
+ "id": batch["id"],
179
+ "target": contract.target,
180
+ "mapping": batch.get("mapping", PLATFORM_MAPPING),
181
+ "coverage": batch.get("coverage", "partial"),
182
+ "mutations": mutations,
183
+ }
184
+ journals = journal_paths(root)
185
+ with (
186
+ tempfile.TemporaryDirectory(prefix="gateway-apply-batch-") as temporary,
187
+ SQLiteStore.create(Path(temporary) / "state.sqlite", contract) as store,
188
+ ):
189
+ for path in journals:
190
+ store.apply_batch(read_json(path))
191
+ result = store.apply_batch(resolved)
192
+ store.audit()
193
+ if result["status"] == "already_applied":
194
+ return result
195
+ batches = root / "data/batches"
196
+ batches.mkdir(exist_ok=True)
197
+ existing = len(journals) - 1
198
+ target = batches / f"{existing + 1:06d}-{digest(batch['id'].encode())[:16]}.json"
199
+ target.write_bytes(encoded(resolved))
200
+ if snapshots.default_snapshot(root).is_file():
201
+ # The journals grew; a snapshot built before this batch would be served
202
+ # over them. Rebuild so the tree stays one thing (default + scenarios).
203
+ from .compiler import build
204
+ from .snapshot import build_all
205
+
206
+ build_all(root, build(root))
207
+ return {**result, "journal": str(target.relative_to(root))}
208
+
209
+
210
+ def _pinned_state(bundle: Path, contract) -> dict[str, int] | None:
211
+ """The entity counts of a data-only version whose hosted snapshot is not in the
212
+ tree: ``db/snapshot.json`` alone. The platform audited that snapshot through
213
+ this contract when it built it, so the pin stands in for the state check —
214
+ provided it names this contract; a snapshot built through another contract
215
+ is refused, since the tree's data files cannot reproduce it either. A tree
216
+ that carries its snapshot, or no pin at all, materializes as usual."""
217
+ from .snapshot import contract_hash, current_default, pinned_default
218
+
219
+ pin = pinned_default(bundle)
220
+ if pin is None or current_default(bundle, contract) is not None:
221
+ return None
222
+ if pin.get("contractHash") != contract_hash(contract):
223
+ fail(
224
+ "snapshot_pinned",
225
+ "db/snapshot.json pins a hosted snapshot built through a different contract; "
226
+ "re-import its rows against the current schema (import_world_data, "
227
+ "gateway worlds data import) or restore the schema it was built against",
228
+ "/db/snapshot.json",
229
+ )
230
+ counts = pin.get("entityCounts")
231
+ if not isinstance(counts, dict) or any(
232
+ not isinstance(value, int) or isinstance(value, bool)
233
+ for value in counts.values()
234
+ ):
235
+ fail(
236
+ "snapshot_pin",
237
+ "db/snapshot.json must carry the hosted snapshot's entityCounts",
238
+ "/db/snapshot.json",
239
+ )
240
+ return {entity: int(counts.get(entity, 0)) for entity in contract.entities}
241
+
242
+
243
+ def validate_bundle(bundle: str | Path) -> dict:
244
+ bundle = Path(bundle)
245
+ if bundle_mode(bundle) is None:
246
+ fail("mode", "Expected a schema-backed mock-tools world")
247
+ contract = check_bundle(bundle)
248
+ declared = set(contract.tools)
249
+ found: dict[str, str] = {}
250
+ for path in sorted((bundle / "tools").glob("*.py")):
251
+ if path.is_symlink():
252
+ fail("path", "Tool handlers must be regular files")
253
+ try:
254
+ tree = ast.parse(path.read_text(encoding="utf-8"), filename=path.name)
255
+ except (SyntaxError, UnicodeError):
256
+ fail("handler", f"Invalid Python handler syntax: {path.name}")
257
+ for node in tree.body:
258
+ if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
259
+ continue
260
+ names = []
261
+ for decorator in node.decorator_list:
262
+ target = (
263
+ decorator.func if isinstance(decorator, ast.Call) else decorator
264
+ )
265
+ is_tool = (isinstance(target, ast.Name) and target.id == "tool") or (
266
+ isinstance(target, ast.Attribute) and target.attr == "tool"
267
+ )
268
+ if not is_tool:
269
+ continue
270
+ name = node.name
271
+ if isinstance(decorator, ast.Call):
272
+ for keyword in decorator.keywords:
273
+ if keyword.arg == "name":
274
+ if not isinstance(
275
+ keyword.value, ast.Constant
276
+ ) or not isinstance(keyword.value.value, str):
277
+ fail(
278
+ "handler", "Publication requires literal tool names"
279
+ )
280
+ name = keyword.value.value
281
+ names.append(name)
282
+ if not names and node.name in declared:
283
+ names.append(node.name)
284
+ if not names and node.name == "run" and path.stem in declared:
285
+ names.append(path.stem)
286
+ for name in names:
287
+ if name in found:
288
+ fail("handler", f"Duplicate handler for {name}")
289
+ found[name] = path.name
290
+ if set(found) != declared:
291
+ fail("handler", "Declared tools and Python handlers must match exactly")
292
+ pinned = _pinned_state(bundle, contract)
293
+ if pinned is not None:
294
+ counts, initial_state = pinned, "pinned"
295
+ else:
296
+ initial_state = "validated"
297
+ with tempfile.TemporaryDirectory(prefix="gateway-state-check-") as staging:
298
+ database = Path(staging) / "world.db"
299
+ materialize_template(bundle, database)
300
+ with SQLiteStore.open(database, contract) as store:
301
+ store.audit()
302
+ counts = {
303
+ entity: len(store.list(entity)) for entity in contract.entities
304
+ }
305
+ return {
306
+ "status": "ok",
307
+ "domain": contract.target,
308
+ "package": contract.metadata["package"],
309
+ "initial_state": initial_state,
310
+ "handler_execution": "skipped",
311
+ "entity_counts": counts,
312
+ "tools": sorted(declared),
313
+ }
314
+
315
+
316
+ API_BUNDLE = "gateway-api-world.toml"
317
+ BUNDLED_WORLD = "server/world"
318
+
319
+
320
+ def world_root(root: Path) -> Path:
321
+ """The world inside the tree the platform holds. ``bench push`` uploads a schema
322
+ world as its api bundle, so a version's tree is ``gateway-api-world.toml`` +
323
+ ``seed/`` + ``scenarios/`` + ``server/world/<the authoring world>``: that nested
324
+ world is what data commands operate on. A plain tree is its own root."""
325
+ nested = root / BUNDLED_WORLD
326
+ if (root / API_BUNDLE).is_file() and (
327
+ (nested / "connector.toml").is_file()
328
+ or (nested / "schema/world.json").is_file()
329
+ ):
330
+ return nested
331
+ return root
332
+
333
+
334
+ def _write_files(root: Path, entries: list) -> None:
335
+ if not isinstance(entries, list) or len(entries) > MAX_FILES:
336
+ fail("size", "Invalid bundle file count")
337
+ seen = set()
338
+ size = 0
339
+ for entry in entries:
340
+ if not isinstance(entry, dict) or set(entry) != {"path", "content"}:
341
+ fail("input", "Expected path and base64 content")
342
+ name = entry["path"]
343
+ if not isinstance(name, str) or not name or "\\" in name or "\x00" in name:
344
+ fail("path", "Invalid bundle path")
345
+ path = PurePosixPath(name)
346
+ if (
347
+ path.is_absolute()
348
+ or any(p in ("", ".", "..") for p in name.split("/"))
349
+ or name in seen
350
+ ):
351
+ fail("path", "Bundle paths must be unique relative file paths")
352
+ seen.add(name)
353
+ try:
354
+ data = base64.b64decode(entry["content"], validate=True)
355
+ except (ValueError, TypeError):
356
+ fail("input", "Invalid base64 bundle content")
357
+ size += len(data)
358
+ if size > MAX_BYTES:
359
+ fail("size", "Bundle exceeds 64 MiB")
360
+ target = root.joinpath(*path.parts)
361
+ target.parent.mkdir(parents=True, exist_ok=True)
362
+ target.write_bytes(data)
363
+
364
+
365
+ def _archive_files(encoded: str) -> list:
366
+ try:
367
+ archive = base64.b64decode(encoded, validate=True)
368
+ if len(archive) > MAX_BYTES:
369
+ fail("size", "Schema archive exceeds 64 MiB")
370
+ with gzip.GzipFile(fileobj=io.BytesIO(archive)) as stream:
371
+ data = stream.read(MAX_BYTES + 1)
372
+ if len(data) > MAX_BYTES:
373
+ fail("size", "Expanded schema archive exceeds 64 MiB")
374
+ files = {}
375
+ with tarfile.open(fileobj=io.BytesIO(data), mode="r:") as tar:
376
+ if tar.pax_headers:
377
+ fail("archive", "Global archive metadata is not supported")
378
+ for member in tar:
379
+ if len(files) > MAX_FILES:
380
+ fail("size", "Too many archive files")
381
+ if (
382
+ member.issym()
383
+ or member.islnk()
384
+ or not (member.isfile() or member.isdir())
385
+ ):
386
+ fail(
387
+ "archive",
388
+ "Archive may contain only ordinary files and directories",
389
+ )
390
+ if any(
391
+ key
392
+ not in (
393
+ "path",
394
+ "mtime",
395
+ "atime",
396
+ "ctime",
397
+ "uid",
398
+ "gid",
399
+ "uname",
400
+ "gname",
401
+ )
402
+ for key in member.pax_headers
403
+ ):
404
+ fail("archive", "Unsupported archive metadata")
405
+ name = member.name
406
+ while name.startswith("./"):
407
+ name = name[2:]
408
+ path = PurePosixPath(name)
409
+ if (
410
+ path.is_absolute()
411
+ or ".." in path.parts
412
+ or not name
413
+ or chr(92) in name
414
+ ):
415
+ fail("path", "Archive path escapes the bundle")
416
+ if member.isdir():
417
+ continue
418
+ stream = tar.extractfile(member)
419
+ if stream is None:
420
+ fail("archive", "Unreadable archive member")
421
+ content = stream.read(MAX_BYTES + 1)
422
+ if len(content) != member.size:
423
+ fail("archive", "Truncated archive member")
424
+ files[str(path)] = base64.b64encode(content).decode("ascii")
425
+ return [{"path": path, "content": content} for path, content in files.items()]
426
+ except (tarfile.TarError, EOFError, OSError, ValueError, TypeError):
427
+ fail("archive", "Invalid world archive")
428
+
429
+
430
+ def _capture(request: dict) -> dict:
431
+ """``capture``: run one connection operation for the platform, from the pod whose
432
+ egress the vendor allow-listed. The spec arrives as text, the credential as the
433
+ environment variable it names (set by the caller, never in the request), and every
434
+ recorded page comes back redacted the way the CLI would have written it."""
435
+ from .connector import CAPTURES, MANIFEST, capture, parse_spec
436
+
437
+ spec_text = request.get("spec")
438
+ op = request.get("op")
439
+ args = request.get("args") or {}
440
+ params = request.get("params") or {}
441
+ max_pages = request.get("max_pages")
442
+ if not isinstance(spec_text, str) or not isinstance(op, str):
443
+ fail("input", "Expected the connector.toml text and an operation name")
444
+ if not all(
445
+ isinstance(m, dict)
446
+ and all(isinstance(k, str) and isinstance(v, str) for k, v in m.items())
447
+ for m in (args, params)
448
+ ):
449
+ fail("input", "args and params map names to strings")
450
+ if max_pages is not None and (
451
+ isinstance(max_pages, bool) or not isinstance(max_pages, int) or max_pages < 1
452
+ ):
453
+ fail("input", "max_pages must be a positive integer")
454
+ parse_spec(spec_text)
455
+ with tempfile.TemporaryDirectory(prefix="gateway-capture-") as directory:
456
+ root = Path(directory) / "world"
457
+ root.mkdir()
458
+ (root / "connector.toml").write_text(spec_text, encoding="utf-8")
459
+ written = capture(root, op, args, params, max_pages=max_pages)
460
+ manifest = json.loads((root / CAPTURES / MANIFEST).read_text(encoding="utf-8"))
461
+ return {
462
+ "status": "ok",
463
+ "files": [
464
+ {
465
+ "path": path.relative_to(root).as_posix(),
466
+ "content": base64.b64encode(path.read_bytes()).decode("ascii"),
467
+ }
468
+ for path in written
469
+ ],
470
+ "manifest": manifest,
471
+ }
472
+
473
+
474
+ def _template_files(request: dict) -> dict[str, bytes]:
475
+ listed = request.get("files")
476
+ if not isinstance(listed, list) or not listed:
477
+ fail("input", "Expected the template's files as [{path, content}]")
478
+ files: dict[str, bytes] = {}
479
+ for entry in listed:
480
+ if (
481
+ not isinstance(entry, dict)
482
+ or not isinstance(entry.get("path"), str)
483
+ or not isinstance(entry.get("content"), str)
484
+ ):
485
+ fail("input", "Each template file is {path, content} with base64 content")
486
+ try:
487
+ files[entry["path"]] = base64.b64decode(entry["content"], validate=True)
488
+ except (ValueError, binascii.Error):
489
+ fail("input", "Template file content must be base64")
490
+ return files
491
+
492
+
493
+ def _template_summary(files: dict[str, bytes]) -> dict:
494
+ """What a workspace connector is, read from its files: the vendor connection
495
+ (connector.toml) and the data model (world.json), plus the digest the platform
496
+ pins worlds to. Never runs the template's Python."""
497
+ from .connector import parse_spec
498
+ from .scaffold import template_digest
499
+
500
+ # A template keeps world.json at its root; an initialized world keeps it under schema/.
501
+ world_path = next(
502
+ (path for path in ("world.json", "schema/world.json") if path in files), None
503
+ )
504
+ if world_path is None:
505
+ fail(
506
+ "template",
507
+ "No world.json (template) or schema/world.json (world) among the files",
508
+ )
509
+ try:
510
+ world = json.loads(files[world_path].decode("utf-8"))
511
+ except (UnicodeDecodeError, json.JSONDecodeError) as error:
512
+ fail("template", f"{world_path} is not valid JSON: {error}")
513
+ gateway = world.get("x-gateway") if isinstance(world, dict) else None
514
+ if not isinstance(gateway, dict):
515
+ fail("template", f"{world_path} is not a gateway-world document")
516
+ entities = []
517
+ for name, entity in sorted((gateway.get("entities") or {}).items()):
518
+ fields: list = []
519
+ schema = entity.get("schema") if isinstance(entity, dict) else None
520
+ ref = schema.get("$ref") if isinstance(schema, dict) else None
521
+ definition = None
522
+ if isinstance(ref, str) and ref.startswith("#/$defs/"):
523
+ definition = (world.get("$defs") or {}).get(ref[len("#/$defs/") :])
524
+ properties = (
525
+ (definition or {}).get("properties") or {}
526
+ if isinstance(definition, dict)
527
+ else {}
528
+ )
529
+ required = (
530
+ set((definition or {}).get("required") or [])
531
+ if isinstance(definition, dict)
532
+ else set()
533
+ )
534
+ for field_name, field_schema in properties.items():
535
+ kind = field_schema.get("type") if isinstance(field_schema, dict) else None
536
+ if isinstance(kind, list):
537
+ # A nullable field is `["string", "null"]` in JSON Schema; the page
538
+ # shows it as one readable type, not the list's concatenation.
539
+ kind = " | ".join(str(part) for part in kind)
540
+ if (
541
+ kind is None
542
+ and isinstance(field_schema, dict)
543
+ and isinstance(field_schema.get("$ref"), str)
544
+ ):
545
+ kind = field_schema["$ref"].rsplit("/", 1)[-1]
546
+ fields.append(
547
+ {"name": field_name, "type": kind, "required": field_name in required}
548
+ )
549
+ entities.append(
550
+ {
551
+ "name": name,
552
+ "primaryKey": list(entity.get("primary_key") or [])
553
+ if isinstance(entity, dict)
554
+ else [],
555
+ "unique": list(entity.get("unique") or [])
556
+ if isinstance(entity, dict)
557
+ else [],
558
+ "relationships": list(entity.get("relationships") or [])
559
+ if isinstance(entity, dict)
560
+ else [],
561
+ "fields": fields,
562
+ }
563
+ )
564
+ connection = None
565
+ if "connector.toml" in files:
566
+ spec = parse_spec(files["connector.toml"].decode("utf-8"))
567
+ connection = {
568
+ "slug": spec.slug,
569
+ "baseUrl": spec.base_url,
570
+ "auth": spec.scheme,
571
+ "operations": [
572
+ {
573
+ "name": op.name,
574
+ "method": op.method,
575
+ "path": op.path,
576
+ "params": list(op.params),
577
+ "entity": op.entity,
578
+ "results": op.results,
579
+ }
580
+ for op in spec.operations.values()
581
+ ],
582
+ }
583
+ package = gateway.get("package") or {}
584
+ return {
585
+ "status": "ok",
586
+ "summary": {
587
+ "packageVersion": package.get("version"),
588
+ "domainVersion": gateway.get("domain_version"),
589
+ "domainId": world.get("$id"),
590
+ "digest": template_digest(files),
591
+ "entities": entities,
592
+ "connection": connection,
593
+ "tools": sorted((gateway.get("tools") or {}).keys()),
594
+ "files": sorted(files),
595
+ },
596
+ }
597
+
598
+
599
+ COMPILED_MARKERS = ("schema/world.json", "tool-schemas.json")
600
+ # The compiled world's files a description is read from; the rest of a bundle
601
+ # (seed data, verifiers, tool sources) says nothing about the contract.
602
+ DESCRIBE_PATHS = (
603
+ "schema/world.json",
604
+ "tool-schemas.json",
605
+ "connector.toml",
606
+ "api/handlers.py",
607
+ )
608
+
609
+
610
+ def _describe(request: dict) -> dict:
611
+ """One description for the connector page and a world's Schema tab: the data
612
+ model, the tools with their schemas, and — when the connector declares a vendor
613
+ connection — the OpenAPI document the served world answers to.
614
+
615
+ The connector is compiled exactly as a world build compiles it (an installed
616
+ template by slug, or a template's files), so the page shows what a world gets;
617
+ a compiled world's own files (``schema/world.json`` + ``tool-schemas.json``) pass
618
+ straight through. Never runs the connector's Python."""
619
+ from .connector import parse_spec
620
+ from .scaffold import (
621
+ TEMPLATES,
622
+ init_bundle,
623
+ init_bundle_from_files,
624
+ template_digest,
625
+ )
626
+
627
+ connector = request.get("connector")
628
+ with tempfile.TemporaryDirectory(prefix="gateway-schema-") as directory:
629
+ root = Path(directory) / "bundle"
630
+ if connector is not None:
631
+ if not isinstance(connector, str):
632
+ fail("input", "Expected an installed connector template slug")
633
+ init_bundle(root, connector=connector)
634
+ template_dir = TEMPLATES / connector
635
+ authored = {
636
+ str(path.relative_to(template_dir)): path.read_bytes()
637
+ for path in sorted(template_dir.rglob("*"))
638
+ if path.is_file() and "__pycache__" not in path.parts
639
+ }
640
+ else:
641
+ authored = _template_files(request)
642
+ if all(marker in authored for marker in COMPILED_MARKERS):
643
+ root.mkdir()
644
+ for path, data in authored.items():
645
+ if path in DESCRIBE_PATHS:
646
+ target = root / path
647
+ target.parent.mkdir(parents=True, exist_ok=True)
648
+ target.write_bytes(data)
649
+ else:
650
+ slug = (
651
+ request.get("slug")
652
+ if isinstance(request.get("slug"), str)
653
+ else None
654
+ )
655
+ if slug is None and "connector.toml" in authored:
656
+ slug = parse_spec(authored["connector.toml"].decode("utf-8")).slug
657
+ init_bundle_from_files(root, authored, connector=slug or "custom")
658
+ summary = _template_summary(
659
+ {"world.json": (root / "schema/world.json").read_bytes()}
660
+ )["summary"]
661
+ summary["digest"] = template_digest(authored)
662
+ summary["files"] = sorted(authored)
663
+ tool_schemas = {}
664
+ if (root / "tool-schemas.json").is_file():
665
+ tool_schemas = (
666
+ json.loads(
667
+ (root / "tool-schemas.json").read_text(encoding="utf-8")
668
+ ).get("tools")
669
+ or {}
670
+ )
671
+ summary["tools"] = [
672
+ {
673
+ "name": name,
674
+ "description": str(entry.get("description") or ""),
675
+ "inputSchema": entry.get("input_schema"),
676
+ "outputSchema": entry.get("output_schema"),
677
+ }
678
+ for name, entry in sorted(tool_schemas.items())
679
+ ]
680
+ summary["connection"] = None
681
+ summary["openapi"] = None
682
+ if (root / "connector.toml").is_file():
683
+ from .api import load_api_settings, openapi_document
684
+
685
+ spec = parse_spec((root / "connector.toml").read_text(encoding="utf-8"))
686
+ try:
687
+ settings = load_api_settings(root, spec)
688
+ except ContractError:
689
+ # A template without its handlers still has a shape worth showing;
690
+ # the routes render with the default action and status.
691
+ settings = None
692
+ summary["connection"] = {
693
+ "slug": spec.slug,
694
+ "baseUrl": spec.base_url,
695
+ "auth": spec.scheme,
696
+ "operations": [
697
+ {
698
+ "name": op.name,
699
+ "method": op.method,
700
+ "path": op.path,
701
+ "params": list(op.params),
702
+ "entity": op.entity,
703
+ "results": op.results,
704
+ "action": settings.for_operation(op.name).action
705
+ if settings
706
+ else None,
707
+ "status": settings.for_operation(op.name).status
708
+ if settings
709
+ else None,
710
+ }
711
+ for op in spec.operations.values()
712
+ ],
713
+ }
714
+ summary["openapi"] = openapi_document(
715
+ spec, summary.get("domainId") or spec.slug, settings
716
+ )
717
+ return {"status": "ok", "description": summary}
718
+
719
+
720
+ def _import_data(request: dict) -> dict:
721
+ """``import-data``: rows into a world the platform holds as files. The tree arrives
722
+ as ``[{path, content}]``, the rows as ``{entity: [row, …]}`` with a mode; the world is
723
+ rebuilt in a temporary directory, the rows go in through :func:`data.import_rows`
724
+ (the same gate the CLI uses, scratch copy first), and every file of the updated
725
+ tree comes back the way ``init-template`` returns one, so the platform commits a
726
+ version from it without knowing which file changed."""
727
+ from .data import KIND_SCHEMA, MODES, import_rows
728
+
729
+ rows = request.get("rows")
730
+ mode = request.get("mode", "append")
731
+ if not isinstance(rows, dict) or not all(
732
+ isinstance(v, list) for v in rows.values()
733
+ ):
734
+ fail("input", "Expected rows as {entity: [row, …]}")
735
+ if mode not in MODES:
736
+ fail("input", f"mode must be one of {', '.join(MODES)}")
737
+ with tempfile.TemporaryDirectory(prefix="gateway-import-") as directory:
738
+ root = Path(directory) / "bundle"
739
+ root.mkdir()
740
+ _write_files(root, request.get("files"))
741
+ world = world_root(root)
742
+ result = import_rows(world, rows, mode=mode)
743
+ if result["kind"] == KIND_SCHEMA and snapshots.default_snapshot(world).is_file():
744
+ # The journals grew; a snapshot built before this import would be
745
+ # served over them. Rebuild so the tree that travels back is one thing.
746
+ from .compiler import build
747
+ from .snapshot import build_all
748
+
749
+ build_all(world, build(world)) # build(): the contract, no snapshot audit
750
+ if world != root and result["kind"] == KIND_SCHEMA:
751
+ # The bundle's base seed is derived from the world's data; older runners post it.
752
+ from .api import write_base_seed
753
+
754
+ write_base_seed(world, root)
755
+ return {
756
+ "status": "ok",
757
+ "kind": result["kind"],
758
+ "entity_counts": result["entity_counts"],
759
+ "changed": result["changed"],
760
+ "files": _tree(root),
761
+ }
762
+
763
+
764
+ # ---------------------------------------------------------------- batches
765
+
766
+
767
+ def _workdir(request: dict) -> Path:
768
+ """The absolute directory the caller prepared; every path in the request is under it."""
769
+ workdir = request.get("workdir")
770
+ if (
771
+ not isinstance(workdir, str)
772
+ or not workdir
773
+ or not Path(workdir).is_absolute()
774
+ or not Path(workdir).is_dir()
775
+ ):
776
+ fail(
777
+ "input",
778
+ "Expected workdir: an absolute path to a directory the caller prepared",
779
+ )
780
+ return Path(workdir).resolve()
781
+
782
+
783
+ def _under(workdir: Path, name: Any, what: str) -> Path:
784
+ if not isinstance(name, str) or not name or "\x00" in name:
785
+ fail("path", f"{what} must be a path under workdir")
786
+ candidate = (
787
+ (workdir / name).resolve()
788
+ if not Path(name).is_absolute()
789
+ else Path(name).resolve()
790
+ )
791
+ if not candidate.is_relative_to(workdir):
792
+ fail("path", f"{what} must stay under workdir")
793
+ return candidate
794
+
795
+
796
+ def _chunk_specs(workdir: Path, request: dict) -> list:
797
+ from .batch import ChunkSpec
798
+
799
+ listed = request.get("chunks")
800
+ if not isinstance(listed, list) or not listed:
801
+ fail("input", "Expected chunks as [{ordinal, path, entity?}]")
802
+ specs = []
803
+ seen = set()
804
+ for entry in listed:
805
+ if (
806
+ not isinstance(entry, dict)
807
+ or not isinstance(entry.get("ordinal"), int)
808
+ or isinstance(entry.get("ordinal"), bool)
809
+ ):
810
+ fail("input", "Each chunk is {ordinal, path, entity?}")
811
+ entity = entry.get("entity")
812
+ if entity is not None and not isinstance(entity, str):
813
+ fail("input", "chunk entity must be a string")
814
+ if entry["ordinal"] in seen:
815
+ fail("input", f"chunk ordinal {entry['ordinal']} is listed twice")
816
+ seen.add(entry["ordinal"])
817
+ path = _under(workdir, entry.get("path"), "chunk path")
818
+ if not path.is_file():
819
+ fail("input", f"chunk {entry['ordinal']} is missing under workdir")
820
+ specs.append(ChunkSpec(entry["ordinal"], path, entity))
821
+ return sorted(specs, key=lambda spec: spec.ordinal)
822
+
823
+
824
+ def _tree(root: Path) -> list:
825
+ """Every file under ``root`` as ``[{path, content}]``, the encoding ``init-template``
826
+ returns, so the platform commits a version without knowing which file changed."""
827
+ return [
828
+ {
829
+ "path": path.relative_to(root).as_posix(),
830
+ "content": base64.b64encode(path.read_bytes()).decode("ascii"),
831
+ }
832
+ for path in sorted(root.rglob("*"))
833
+ if path.is_file() and "__pycache__" not in path.parts
834
+ ]
835
+
836
+
837
+ def _write_tree(workdir: Path, request: dict) -> Path:
838
+ """The world tree from ``files``, written under ``workdir/tree`` and compiled (the
839
+ contract alone is enough: generated artifacts are regenerated, snapshots are not)."""
840
+ from .compiler import compile_bundle
841
+
842
+ root = workdir / "tree"
843
+ if root.exists():
844
+ shutil.rmtree(root)
845
+ root.mkdir()
846
+ _write_files(root, request.get("files"))
847
+ world = world_root(root)
848
+ compile_bundle(world, snapshots=False)
849
+ return world
850
+
851
+
852
+ def _flag(request: dict, name: str, default: bool) -> bool:
853
+ value = request.get(name, default)
854
+ if not isinstance(value, bool):
855
+ fail("input", f"{name} must be true or false")
856
+ return value
857
+
858
+
859
+ def _validate_rows(request: dict) -> dict:
860
+ """``validate-rows``: shape-only validation of every row in the chunks (type, bounds,
861
+ required, unknown property, primary key present); no reference checks."""
862
+ from .batch import validate_chunks
863
+ from .compiler import check_bundle
864
+ from .data import MODES
865
+
866
+ workdir = _workdir(request)
867
+ if request.get("mode", "append") not in MODES:
868
+ fail("input", f"mode must be one of {', '.join(MODES)}")
869
+ root = _write_tree(workdir, request)
870
+ report = validate_chunks(check_bundle(root), _chunk_specs(workdir, request))
871
+ return {"status": "ok", **report}
872
+
873
+
874
+ def _snapshot_build(request: dict) -> dict:
875
+ """``snapshot-build``: the parent snapshot (or the tree's default) with the chunks
876
+ applied in dependency order, one transaction (atomic) or one per chunk; the new
877
+ snapshot saved under workdir with its sidecar, or nothing on dryRun or an atomic
878
+ refusal."""
879
+ from .batch import build_batch
880
+ from .compiler import check_bundle
881
+ from .data import MERGES, MODES
882
+ from .snapshot import EMPTY_DIGEST, build_default, default_snapshot
883
+
884
+ workdir = _workdir(request)
885
+ mode = request.get("mode", "append")
886
+ merge = request.get("merge", "upsert")
887
+ if mode not in MODES:
888
+ fail("input", f"mode must be one of {', '.join(MODES)}")
889
+ if merge not in MERGES:
890
+ fail("input", f"merge must be one of {', '.join(MERGES)}")
891
+ batch_id = request.get("batchId")
892
+ parent_digest = request.get("parentBatchDigest", EMPTY_DIGEST)
893
+ if (
894
+ not isinstance(batch_id, str)
895
+ or not batch_id
896
+ or "/" in batch_id
897
+ or batch_id in (".", "..")
898
+ ):
899
+ fail("input", "batchId must be a non-empty string without slashes")
900
+ if not isinstance(parent_digest, str) or len(parent_digest) != 64:
901
+ fail("input", "parentBatchDigest must be a 64-hex batch digest")
902
+ atomic = _flag(request, "atomic", True)
903
+ dry_run = _flag(request, "dryRun", False)
904
+ root = _write_tree(workdir, request)
905
+ contract = check_bundle(root)
906
+ chunks = _chunk_specs(workdir, request)
907
+ # One directory per batch, so a parent built by an earlier call in the same
908
+ # workdir is never the file being written.
909
+ out_dir = workdir / "snapshot" / batch_id
910
+ out_dir.mkdir(parents=True, exist_ok=True)
911
+ out = out_dir / "state.sqlite"
912
+ out.unlink(missing_ok=True)
913
+ out.with_name("state.json").unlink(missing_ok=True)
914
+ if request.get("parentSnapshot") is not None:
915
+ parent = _under(workdir, request["parentSnapshot"], "parentSnapshot")
916
+ if not parent.is_file():
917
+ fail("input", "parentSnapshot is missing under workdir")
918
+ shutil.copyfile(parent, out)
919
+ else:
920
+ build_default(root, contract)
921
+ shutil.copyfile(default_snapshot(root), out)
922
+ result = build_batch(
923
+ contract,
924
+ out,
925
+ chunks,
926
+ mode=mode,
927
+ merge=merge,
928
+ atomic=atomic,
929
+ batch_id=batch_id,
930
+ parent_digest=parent_digest,
931
+ dry_run=dry_run,
932
+ )
933
+ if "snapshot" in result:
934
+ # Absolute, under workdir: the caller opens it as it is.
935
+ result["snapshot"] = str(result["snapshot"])
936
+ return {"status": "ok", **result}
937
+
938
+
939
+ def _snapshot_default(request: dict) -> dict:
940
+ """``snapshot-default``: the tree's default snapshot and every scenario snapshot,
941
+ built under ``workdir/tree``; the sidecars come back with absolute paths."""
942
+ from .compiler import check_bundle
943
+ from .snapshot import build_all, default_snapshot, scenario_snapshot
944
+
945
+ workdir = _workdir(request)
946
+ root = _write_tree(workdir, request)
947
+ built = build_all(root, check_bundle(root))
948
+ # Paths are absolute, under workdir; the sidecar sits both at the top level and
949
+ # keyed by its tree path, so a reader may take either.
950
+ return {
951
+ "status": "ok",
952
+ "snapshot": str(default_snapshot(root)),
953
+ "sidecar": built["default"],
954
+ "sidecars": {"db/state.json": built["default"]},
955
+ "scenarios": {
956
+ name: {"snapshot": str(scenario_snapshot(root, name)), "sidecar": sidecar}
957
+ for name, sidecar in built["scenarios"].items()
958
+ },
959
+ }
960
+
961
+
962
+ def _api_bundle(request: dict) -> dict:
963
+ """The api-kind bundle that hosts a world's API, built from the world's files:
964
+ what `gateway worlds schema api-bundle` writes locally, for a CLI with no Python."""
965
+ from .api import build_api_bundle
966
+
967
+ files = _template_files(request)
968
+ with tempfile.TemporaryDirectory(prefix="gateway-schema-") as directory:
969
+ root = Path(directory) / "world"
970
+ root.mkdir()
971
+ for path, data in files.items():
972
+ target = root / path
973
+ target.parent.mkdir(parents=True, exist_ok=True)
974
+ target.write_bytes(data)
975
+ out = Path(directory) / "api"
976
+ build_api_bundle(root, out)
977
+ return {"status": "ok", "files": _tree(out)}
978
+
979
+
980
+ def _ingest(request: dict) -> dict:
981
+ """``ingest``: the world's authored tree, its ``captures/`` and manifest included,
982
+ seeded from those captures through the contract exactly as the CLI does, and the
983
+ whole updated tree handed back. Runs no Python of the world's own."""
984
+ from .connector import ingest
985
+
986
+ batch_id = request.get("batch_id")
987
+ if batch_id is not None and not isinstance(batch_id, str):
988
+ fail("input", "batch_id must be a string")
989
+ with tempfile.TemporaryDirectory(prefix="gateway-ingest-") as directory:
990
+ root = Path(directory) / "world"
991
+ root.mkdir()
992
+ _write_files(root, request.get("files"))
993
+ result = ingest(world_root(root), batch_id=batch_id)
994
+ return {"status": "ok", "result": result, "files": _tree(root)}
995
+
996
+
997
+ def _conform(request: dict) -> dict:
998
+ """``conform``: serve the world in the bridge and check every operation's answer
999
+ against the vendor's OpenAPI documents. The report is the one ``connector conform
1000
+ --out`` writes; ``world`` names the bundle, never the bridge's scratch path."""
1001
+ from .conform import conform
1002
+
1003
+ documents = request.get("openapi")
1004
+ if not isinstance(documents, list) or not documents:
1005
+ fail("input", "Expected one or more OpenAPI documents as [{path, content}]")
1006
+ max_operations = request.get("max_operations")
1007
+ if max_operations is not None and (
1008
+ isinstance(max_operations, bool)
1009
+ or not isinstance(max_operations, int)
1010
+ or max_operations < 1
1011
+ ):
1012
+ fail("input", "max_operations must be a positive integer")
1013
+ with tempfile.TemporaryDirectory(prefix="gateway-conform-") as directory:
1014
+ root = Path(directory) / "world"
1015
+ root.mkdir()
1016
+ _write_files(root, request.get("files"))
1017
+ openapi = Path(directory) / "openapi"
1018
+ openapi.mkdir()
1019
+ _write_files(openapi, documents)
1020
+ paths = [
1021
+ openapi.joinpath(*PurePosixPath(entry["path"]).parts) for entry in documents
1022
+ ]
1023
+ report = conform(
1024
+ world_root(root), paths, max_operations=max_operations
1025
+ ).to_json()
1026
+ report["world"] = root.name
1027
+ return {"status": "ok", "report": report}
1028
+
1029
+
1030
+ def _run_tests(request: dict) -> dict:
1031
+ """``run-tests``: serve the world at these files in the bridge and run the tests it
1032
+ ships under ``tests/`` (see :mod:`world_tests`). ``scenario`` starts the world in one
1033
+ of its scenarios; ``snapshot`` ({url, sha256, digest}) lays a pinned data snapshot
1034
+ into the tree first, so a data-only version is tested with its rows."""
1035
+ from .snapshot import fetch as fetch_snapshot
1036
+ from .world_tests import run
1037
+
1038
+ scenario = request.get("scenario")
1039
+ if scenario is not None and not isinstance(scenario, str):
1040
+ fail("input", "scenario must be a string")
1041
+ snapshot = request.get("snapshot")
1042
+ if snapshot is not None and not (
1043
+ isinstance(snapshot, dict) and all(isinstance(snapshot.get(k), str) for k in ("url", "sha256", "digest"))
1044
+ ):
1045
+ fail("input", "snapshot must be {url, sha256, digest}")
1046
+ with tempfile.TemporaryDirectory(prefix="gateway-tests-") as directory:
1047
+ root = Path(directory) / "world"
1048
+ root.mkdir()
1049
+ _write_files(root, request.get("files"))
1050
+ root = world_root(root)
1051
+ if snapshot is not None:
1052
+ fetch_snapshot(root, check_bundle(root), snapshot["url"], snapshot["sha256"], snapshot["digest"])
1053
+ report = run(root, scenario=scenario).to_json()
1054
+ report["world"] = root.name
1055
+ return {"status": "ok", "report": report}
1056
+
1057
+
1058
+ def _counts(request: dict) -> dict:
1059
+ """``counts``: what the world at these files seeds with. A kind whose seed lives
1060
+ elsewhere (a harbor tree, an unknown layout) answers with the reason instead of
1061
+ a rejection, so the caller can show it."""
1062
+ from .data import entity_counts, entity_rows, world_kind
1063
+
1064
+ entity = request.get("entity")
1065
+ if entity is not None and not isinstance(entity, str):
1066
+ fail("input", "entity must be a string")
1067
+ with tempfile.TemporaryDirectory(prefix="gateway-counts-") as directory:
1068
+ root = Path(directory) / "world"
1069
+ root.mkdir()
1070
+ _write_files(root, request.get("files"))
1071
+ root = world_root(root)
1072
+ kind = world_kind(root)
1073
+ try:
1074
+ counts = entity_counts(root)
1075
+ except ContractError as error:
1076
+ if error.detail.get("code") != "kind":
1077
+ raise
1078
+ return {
1079
+ "status": "ok",
1080
+ "kind": kind,
1081
+ "entity_counts": None,
1082
+ "reason": error.detail["message"],
1083
+ }
1084
+ report = {"status": "ok", "kind": kind, "entity_counts": counts}
1085
+ if entity is not None:
1086
+ report["rows"] = entity_rows(root, entity)
1087
+ return report
1088
+
1089
+
1090
+ def _templates(request: dict) -> dict:
1091
+ """``templates``: every installed connector template, read from its ``world.json``
1092
+ (name and description when the document carries them) and whether it declares a
1093
+ vendor connection. Never runs a template's Python."""
1094
+ from .scaffold import TEMPLATES, list_templates
1095
+
1096
+ templates = []
1097
+ for slug in list_templates():
1098
+ world = json.loads(
1099
+ (TEMPLATES / slug / "world.json").read_text(encoding="utf-8")
1100
+ )
1101
+ gateway = world.get("x-gateway") if isinstance(world, dict) else None
1102
+ if not isinstance(gateway, dict):
1103
+ fail("template", f"{slug}/world.json is not a gateway-world document")
1104
+ templates.append(
1105
+ {
1106
+ "slug": slug,
1107
+ "name": world.get("title")
1108
+ if isinstance(world.get("title"), str)
1109
+ else None,
1110
+ "description": world.get("description")
1111
+ if isinstance(world.get("description"), str)
1112
+ else None,
1113
+ "entities": sorted(gateway.get("entities") or {}),
1114
+ "tools": sorted(gateway.get("tools") or {}),
1115
+ "hasConnection": (TEMPLATES / slug / "connector.toml").is_file(),
1116
+ }
1117
+ )
1118
+ return {"status": "ok", "templates": templates}
1119
+
1120
+
1121
+ def execute(request: dict) -> dict:
1122
+ if not isinstance(request, dict) or request.get("command") not in COMMANDS:
1123
+ fail(
1124
+ "command",
1125
+ "Expected one of " + ", ".join(COMMANDS[:-1]) + " or " + COMMANDS[-1],
1126
+ )
1127
+ if request["command"] == "validate-rows":
1128
+ return _validate_rows(request)
1129
+ if request["command"] == "snapshot-build":
1130
+ return _snapshot_build(request)
1131
+ if request["command"] == "snapshot-default":
1132
+ return _snapshot_default(request)
1133
+ if request["command"] == "capture":
1134
+ return _capture(request)
1135
+ if request["command"] == "import-data":
1136
+ return _import_data(request)
1137
+ if request["command"] == "ingest":
1138
+ return _ingest(request)
1139
+ if request["command"] == "conform":
1140
+ return _conform(request)
1141
+ if request["command"] == "run-tests":
1142
+ return _run_tests(request)
1143
+ if request["command"] == "counts":
1144
+ return _counts(request)
1145
+ if request["command"] == "templates":
1146
+ return _templates(request)
1147
+ if request["command"] == "api-bundle":
1148
+ return _api_bundle(request)
1149
+ if request["command"] == "template-summary":
1150
+ return _template_summary(_template_files(request))
1151
+ if request["command"] == "describe":
1152
+ return _describe(request)
1153
+ extra: dict = {}
1154
+ with tempfile.TemporaryDirectory(prefix="gateway-schema-") as directory:
1155
+ root = Path(directory) / "bundle"
1156
+ root.mkdir()
1157
+ if request["command"] == "validate-archive":
1158
+ _write_files(root, _archive_files(request.get("archive")))
1159
+ # A stored version may be the api bundle of a schema world: the
1160
+ # world lives under server/world, and that is what gets validated.
1161
+ root = world_root(root)
1162
+ if bundle_mode(root) is None:
1163
+ return {"status": "ok", "mode": "legacy"}
1164
+ if request["command"] in (
1165
+ "validate",
1166
+ "compile",
1167
+ "apply-batch",
1168
+ "refresh-slack",
1169
+ ):
1170
+ _write_files(root, request.get("files"))
1171
+ # The tree the platform sent may be a schema world's api bundle; the
1172
+ # world itself sits under server/world and is what compiles, takes a
1173
+ # batch and validates. The whole tree still travels back.
1174
+ world = world_root(root)
1175
+ if request["command"] == "compile":
1176
+ # Arbitrary custom worlds: the agent authors schema/world.json (+ manifest,
1177
+ # handlers, journals); the trusted compiler owns every generated artifact.
1178
+ extra["compile"] = compile_world(world)
1179
+ if world != root and (world / "data/initial.json").is_file():
1180
+ from .api import write_base_seed
1181
+
1182
+ write_base_seed(world, root)
1183
+ if request["command"] == "apply-batch":
1184
+ batch, source = request.get("batch"), request.get("source")
1185
+ if not isinstance(batch, dict) or (
1186
+ source is not None and not isinstance(source, str)
1187
+ ):
1188
+ fail(
1189
+ "input",
1190
+ "Expected a batch object and an optional source prefix string",
1191
+ )
1192
+ extra["batch"] = append_batch(world, batch, source=source)
1193
+ if request["command"] == "init-template":
1194
+ # The platform's "create world" for every connector template: the same
1195
+ # init + compile the CLI runs, on a fresh empty tree, with nothing seeded.
1196
+ from .scaffold import init_bundle
1197
+
1198
+ connector = request.get("connector")
1199
+ name = request.get("name")
1200
+ if not isinstance(connector, str) or (
1201
+ name is not None and not isinstance(name, str)
1202
+ ):
1203
+ fail("input", "Expected a connector template slug and an optional name")
1204
+ init_bundle(root, connector=connector, name=name)
1205
+ if request["command"] == "init-template-files":
1206
+ # A workspace connector: the template arrives as files, never as a path
1207
+ # the request chose, and the world is built exactly as from an installed one.
1208
+ from .scaffold import init_bundle_from_files
1209
+
1210
+ connector = request.get("connector")
1211
+ name = request.get("name")
1212
+ if not isinstance(connector, str) or (
1213
+ name is not None and not isinstance(name, str)
1214
+ ):
1215
+ fail("input", "Expected the connector slug and an optional name")
1216
+ init_bundle_from_files(
1217
+ root, _template_files(request), connector=connector, name=name
1218
+ )
1219
+ if request["command"] == "build-slack":
1220
+ from .slack import build_slack_bundle
1221
+
1222
+ build_slack_bundle(
1223
+ root, evidence=request.get("evidence"), name=request.get("name")
1224
+ )
1225
+ if request["command"] == "refresh-slack":
1226
+ from .slack import refresh_slack_bundle
1227
+
1228
+ refresh_slack_bundle(
1229
+ root, request.get("evidence"), batch_id=request.get("batch_id")
1230
+ )
1231
+ report = {**validate_bundle(world_root(root)), **extra}
1232
+ if request["command"] not in ("validate", "validate-archive"):
1233
+ report["files"] = [
1234
+ {
1235
+ "path": str(path.relative_to(root)),
1236
+ "content": base64.b64encode(path.read_bytes()).decode("ascii"),
1237
+ }
1238
+ for path in sorted(root.rglob("*"))
1239
+ if path.is_file()
1240
+ ]
1241
+ return report
1242
+
1243
+
1244
+ def main():
1245
+ try:
1246
+ raw = sys.stdin.buffer.read(2 * MAX_BYTES + 1)
1247
+ if len(raw) > 2 * MAX_BYTES:
1248
+ fail("size", "Bridge request is too large")
1249
+ result = execute(json.loads(raw))
1250
+ print(json.dumps(result, sort_keys=True))
1251
+ except ContractError as error:
1252
+ print(json.dumps({"status": "rejected", "error": error.detail}, sort_keys=True))
1253
+ return 1
1254
+ except (OSError, ValueError, TypeError):
1255
+ # Never dump request content or provider records in process diagnostics.
1256
+ print(
1257
+ json.dumps(
1258
+ {
1259
+ "status": "rejected",
1260
+ "error": {
1261
+ "code": "input",
1262
+ "message": "Invalid schema bridge input",
1263
+ },
1264
+ }
1265
+ )
1266
+ )
1267
+ return 1
1268
+ return 0
1269
+
1270
+
1271
+ if __name__ == "__main__":
1272
+ raise SystemExit(main())