graph-agents-cli 0.3.1__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 (291) hide show
  1. graph_agents_cli/__init__.py +26 -0
  2. graph_agents_cli/_api_policy.py +2145 -0
  3. graph_agents_cli/_approvals.py +400 -0
  4. graph_agents_cli/_build.py +186 -0
  5. graph_agents_cli/_build_info.json +7 -0
  6. graph_agents_cli/_chat_client.py +462 -0
  7. graph_agents_cli/_click.py +157 -0
  8. graph_agents_cli/_defaults.py +139 -0
  9. graph_agents_cli/_experiments.py +64 -0
  10. graph_agents_cli/_http.py +192 -0
  11. graph_agents_cli/_output.py +83 -0
  12. graph_agents_cli/_project.py +462 -0
  13. graph_agents_cli/_remote.py +220 -0
  14. graph_agents_cli/_response_schema.py +264 -0
  15. graph_agents_cli/_runner.py +319 -0
  16. graph_agents_cli/_skills_check.py +274 -0
  17. graph_agents_cli/_tools.py +189 -0
  18. graph_agents_cli/_trust.py +66 -0
  19. graph_agents_cli/api/__init__.py +15 -0
  20. graph_agents_cli/api/_changes.py +506 -0
  21. graph_agents_cli/api/_files.py +658 -0
  22. graph_agents_cli/api/cmd_api.py +2480 -0
  23. graph_agents_cli/deploy/__init__.py +15 -0
  24. graph_agents_cli/deploy/_config.py +171 -0
  25. graph_agents_cli/deploy/_image.py +128 -0
  26. graph_agents_cli/deploy/_kube.py +286 -0
  27. graph_agents_cli/deploy/_modes.py +234 -0
  28. graph_agents_cli/deploy/_preflight.py +370 -0
  29. graph_agents_cli/deploy/_values.py +168 -0
  30. graph_agents_cli/deploy/cmd_deploy.py +1866 -0
  31. graph_agents_cli/deploy/gitops.py +562 -0
  32. graph_agents_cli/deploy/local_load.py +273 -0
  33. graph_agents_cli/dev/__init__.py +13 -0
  34. graph_agents_cli/dev/cmd_build.py +131 -0
  35. graph_agents_cli/dev/cmd_install.py +78 -0
  36. graph_agents_cli/dev/cmd_lint.py +119 -0
  37. graph_agents_cli/dev/cmd_playground.py +297 -0
  38. graph_agents_cli/dev/policy_check.py +1287 -0
  39. graph_agents_cli/eval/__init__.py +22 -0
  40. graph_agents_cli/eval/_client.py +670 -0
  41. graph_agents_cli/eval/_common.py +177 -0
  42. graph_agents_cli/eval/_judge.py +168 -0
  43. graph_agents_cli/eval/_judge_runner.py +238 -0
  44. graph_agents_cli/eval/_paths.py +212 -0
  45. graph_agents_cli/eval/checks.py +581 -0
  46. graph_agents_cli/eval/cmd_analyze.py +278 -0
  47. graph_agents_cli/eval/cmd_compare.py +284 -0
  48. graph_agents_cli/eval/cmd_eval_group.py +80 -0
  49. graph_agents_cli/eval/cmd_generate.py +558 -0
  50. graph_agents_cli/eval/cmd_grade.py +466 -0
  51. graph_agents_cli/eval/cmd_metric.py +156 -0
  52. graph_agents_cli/eval/cmd_run.py +370 -0
  53. graph_agents_cli/eval/cmd_submit.py +400 -0
  54. graph_agents_cli/eval/config.py +435 -0
  55. graph_agents_cli/eval/dataset.py +350 -0
  56. graph_agents_cli/eval/gate.py +420 -0
  57. graph_agents_cli/eval/transcript.py +192 -0
  58. graph_agents_cli/extension/__init__.py +13 -0
  59. graph_agents_cli/extension/_compat.py +86 -0
  60. graph_agents_cli/extension/_loader.py +293 -0
  61. graph_agents_cli/extension/_manifest.py +135 -0
  62. graph_agents_cli/extension/_overrides.py +195 -0
  63. graph_agents_cli/extension/_paths.py +91 -0
  64. graph_agents_cli/extension/_refs.py +193 -0
  65. graph_agents_cli/extension/_resolver.py +453 -0
  66. graph_agents_cli/extension/_schema.py +106 -0
  67. graph_agents_cli/extension/_spec.py +253 -0
  68. graph_agents_cli/extension/_sync.py +102 -0
  69. graph_agents_cli/extension/_trust.py +58 -0
  70. graph_agents_cli/extension/cmd_extension_add.py +259 -0
  71. graph_agents_cli/extension/cmd_extension_group.py +57 -0
  72. graph_agents_cli/extension/cmd_extension_list.py +56 -0
  73. graph_agents_cli/extension/cmd_extension_remove.py +61 -0
  74. graph_agents_cli/extension/cmd_extension_update.py +195 -0
  75. graph_agents_cli/info/__init__.py +13 -0
  76. graph_agents_cli/info/cmd_info.py +222 -0
  77. graph_agents_cli/infra/__init__.py +15 -0
  78. graph_agents_cli/infra/checks.py +1169 -0
  79. graph_agents_cli/infra/cmd_infra.py +103 -0
  80. graph_agents_cli/main.py +591 -0
  81. graph_agents_cli/peer/__init__.py +15 -0
  82. graph_agents_cli/peer/_generate.py +254 -0
  83. graph_agents_cli/peer/cmd_peer.py +1151 -0
  84. graph_agents_cli/run/__init__.py +13 -0
  85. graph_agents_cli/run/_local_server.py +1157 -0
  86. graph_agents_cli/run/_signals.py +141 -0
  87. graph_agents_cli/run/cmd_approvals.py +530 -0
  88. graph_agents_cli/run/cmd_run.py +1421 -0
  89. graph_agents_cli/scaffold/__init__.py +19 -0
  90. graph_agents_cli/scaffold/agents/README.md +24 -0
  91. graph_agents_cli/scaffold/agents/empty_py/.template/templateconfig.yaml +22 -0
  92. graph_agents_cli/scaffold/agents/langgraph/.env.example +292 -0
  93. graph_agents_cli/scaffold/agents/langgraph/.template/templateconfig.yaml +28 -0
  94. graph_agents_cli/scaffold/agents/langgraph/Dockerfile +59 -0
  95. graph_agents_cli/scaffold/agents/langgraph/Dockerfile.langgraph-server +59 -0
  96. graph_agents_cli/scaffold/agents/langgraph/README.md +571 -0
  97. graph_agents_cli/scaffold/agents/langgraph/api-policy.yaml +60 -0
  98. graph_agents_cli/scaffold/agents/langgraph/app/__init__.py +20 -0
  99. graph_agents_cli/scaffold/agents/langgraph/app/agent.py +174 -0
  100. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/__init__.py +15 -0
  101. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/a2a.py +2162 -0
  102. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/a2a_client.py +1167 -0
  103. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/api_client.py +4220 -0
  104. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/approvals.py +1349 -0
  105. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/auth.py +1986 -0
  106. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/chat.py +2962 -0
  107. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/checkpointer.py +432 -0
  108. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/content.py +569 -0
  109. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/db.py +580 -0
  110. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/limits.py +203 -0
  111. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/metrics.py +231 -0
  112. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/middleware.py +361 -0
  113. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/model.py +611 -0
  114. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/playground.py +230 -0
  115. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/run_locks.py +459 -0
  116. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/structured.py +755 -0
  117. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/telemetry.py +681 -0
  118. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/threads.py +493 -0
  119. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/token_exchange.py +959 -0
  120. graph_agents_cli/scaffold/agents/langgraph/app/fast_api_app.py +770 -0
  121. graph_agents_cli/scaffold/agents/langgraph/app/policies/__init__.py +55 -0
  122. graph_agents_cli/scaffold/agents/langgraph/app/policies/custom.py +97 -0
  123. graph_agents_cli/scaffold/agents/langgraph/app/tools/__init__.py +46 -0
  124. graph_agents_cli/scaffold/agents/langgraph/app/tools/example_api.py +92 -0
  125. graph_agents_cli/scaffold/agents/langgraph/app/tools/weather.py +33 -0
  126. graph_agents_cli/scaffold/agents/langgraph/langgraph.json +14 -0
  127. graph_agents_cli/scaffold/agents/langgraph/pyproject.toml +78 -0
  128. graph_agents_cli/scaffold/agents/langgraph/tests/conftest.py +376 -0
  129. graph_agents_cli/scaffold/agents/langgraph/tests/eval/datasets/basic-dataset.json +53 -0
  130. graph_agents_cli/scaffold/agents/langgraph/tests/eval/eval_config.yaml +32 -0
  131. graph_agents_cli/scaffold/agents/langgraph/tests/integration/approval_graph.py +137 -0
  132. graph_agents_cli/scaffold/agents/langgraph/tests/integration/fake_issuer.py +216 -0
  133. graph_agents_cli/scaffold/agents/langgraph/tests/integration/fake_openai.py +357 -0
  134. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_a2a_outcomes.py +569 -0
  135. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_a2a_relay.py +479 -0
  136. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_api_surface.py +812 -0
  137. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_approvals.py +1367 -0
  138. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_approvals_server.py +794 -0
  139. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_cross_actor.py +497 -0
  140. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_cross_actor_server.py +247 -0
  141. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_history_repair.py +278 -0
  142. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_model_apis.py +242 -0
  143. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_postgres.py +637 -0
  144. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_resilience_postgres.py +770 -0
  145. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_runtime_guardrails.py +854 -0
  146. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_server_e2e.py +340 -0
  147. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_server_runtime.py +989 -0
  148. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_structured_answers.py +584 -0
  149. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_structured_server.py +222 -0
  150. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_token_exchange_issuer.py +650 -0
  151. graph_agents_cli/scaffold/agents/langgraph/tests/load_test/.results/.placeholder +0 -0
  152. graph_agents_cli/scaffold/agents/langgraph/tests/load_test/README.md +22 -0
  153. graph_agents_cli/scaffold/agents/langgraph/tests/load_test/conftest.py +21 -0
  154. graph_agents_cli/scaffold/agents/langgraph/tests/load_test/load_test.py +81 -0
  155. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_a2a_client.py +824 -0
  156. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_a2a_scoping.py +724 -0
  157. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_api_client.py +1214 -0
  158. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_api_client_hardening.py +716 -0
  159. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_api_policy_rpc.py +767 -0
  160. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_approval_ledger.py +1536 -0
  161. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_fake_model.py +115 -0
  162. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_jwt_policy.py +991 -0
  163. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_limits.py +310 -0
  164. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_logging.py +148 -0
  165. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_logging_hardening.py +271 -0
  166. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_policy.py +378 -0
  167. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_resilience.py +610 -0
  168. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_server_auth.py +702 -0
  169. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_structured.py +673 -0
  170. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_telemetry.py +404 -0
  171. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_thread_listing.py +255 -0
  172. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_threads.py +268 -0
  173. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_token_exchange.py +1320 -0
  174. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_untrusted_content.py +393 -0
  175. graph_agents_cli/scaffold/agents/langgraph/uv-fastapi.lock +2084 -0
  176. graph_agents_cli/scaffold/agents/langgraph/uv-langgraph-server.lock +2106 -0
  177. graph_agents_cli/scaffold/agents/langgraph/{{cookiecutter.agent_guidance_filename}} +129 -0
  178. graph_agents_cli/scaffold/base_templates/_shared/graph-agents-cli-manifest.yaml +36 -0
  179. graph_agents_cli/scaffold/base_templates/python/.dockerignore +32 -0
  180. graph_agents_cli/scaffold/base_templates/python/.github/CODEOWNERS +30 -0
  181. graph_agents_cli/scaffold/base_templates/python/.github/agent.env +7 -0
  182. graph_agents_cli/scaffold/base_templates/python/.github/workflows/pr_checks.yaml +214 -0
  183. graph_agents_cli/scaffold/base_templates/python/.gitignore +209 -0
  184. graph_agents_cli/scaffold/base_templates/python/tests/unit/test_dummy.py +23 -0
  185. graph_agents_cli/scaffold/base_templates/python/{{cookiecutter.agent_guidance_filename}} +35 -0
  186. graph_agents_cli/scaffold/cmd_scaffold_group.py +49 -0
  187. graph_agents_cli/scaffold/commands/__init__.py +13 -0
  188. graph_agents_cli/scaffold/commands/create.py +1424 -0
  189. graph_agents_cli/scaffold/commands/enhance.py +1652 -0
  190. graph_agents_cli/scaffold/commands/upgrade.py +570 -0
  191. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/.github/agent.env +12 -0
  192. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/.github/workflows/promote-to-prod.yaml +371 -0
  193. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/.github/workflows/staging.yaml +450 -0
  194. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/argocd/application-dev.yaml +43 -0
  195. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/argocd/application-prod.yaml +41 -0
  196. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/argocd/application-staging.yaml +43 -0
  197. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/.helmignore +14 -0
  198. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/Chart.yaml +21 -0
  199. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/examples/networkpolicy.yaml +103 -0
  200. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/NOTES.txt +48 -0
  201. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/_helpers.tpl +189 -0
  202. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/certificate.yaml +15 -0
  203. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/configmap.yaml +10 -0
  204. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/deployment.yaml +199 -0
  205. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/hpa.yaml +22 -0
  206. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/httproute.yaml +30 -0
  207. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/ingress.yaml +39 -0
  208. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/networkpolicy.yaml +48 -0
  209. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/pdb.yaml +13 -0
  210. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/postgresql-secret.yaml +37 -0
  211. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/service.yaml +15 -0
  212. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/serviceaccount.yaml +13 -0
  213. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/servicemonitor.yaml +42 -0
  214. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/values-dev.yaml +22 -0
  215. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/values-prod.yaml +45 -0
  216. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/values-staging.yaml +29 -0
  217. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/values.yaml +396 -0
  218. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/tests/integration/test_chart.py +269 -0
  219. graph_agents_cli/scaffold/deployment_targets/none/README.md +5 -0
  220. graph_agents_cli/scaffold/deployment_targets/none/python/README.md +6 -0
  221. graph_agents_cli/scaffold/utils/__init__.py +13 -0
  222. graph_agents_cli/scaffold/utils/backup.py +212 -0
  223. graph_agents_cli/scaffold/utils/build_record.py +257 -0
  224. graph_agents_cli/scaffold/utils/cli_options.py +184 -0
  225. graph_agents_cli/scaffold/utils/fs.py +83 -0
  226. graph_agents_cli/scaffold/utils/generate_locks.py +214 -0
  227. graph_agents_cli/scaffold/utils/generation_metadata.py +88 -0
  228. graph_agents_cli/scaffold/utils/keyedit.py +768 -0
  229. graph_agents_cli/scaffold/utils/keymerge.py +537 -0
  230. graph_agents_cli/scaffold/utils/language.py +138 -0
  231. graph_agents_cli/scaffold/utils/lock_utils.py +94 -0
  232. graph_agents_cli/scaffold/utils/logging.py +77 -0
  233. graph_agents_cli/scaffold/utils/manifest.py +292 -0
  234. graph_agents_cli/scaffold/utils/merge.py +970 -0
  235. graph_agents_cli/scaffold/utils/merge3.py +216 -0
  236. graph_agents_cli/scaffold/utils/openapi_seed.py +199 -0
  237. graph_agents_cli/scaffold/utils/remote_template.py +376 -0
  238. graph_agents_cli/scaffold/utils/template.py +1352 -0
  239. graph_agents_cli/scaffold/utils/upgrade.py +894 -0
  240. graph_agents_cli/scaffold/utils/version.py +438 -0
  241. graph_agents_cli/secrets/__init__.py +15 -0
  242. graph_agents_cli/secrets/_apply.py +954 -0
  243. graph_agents_cli/secrets/_required.py +188 -0
  244. graph_agents_cli/secrets/cmd_secrets.py +211 -0
  245. graph_agents_cli/setup/__init__.py +13 -0
  246. graph_agents_cli/setup/_antigravity.py +221 -0
  247. graph_agents_cli/setup/cmd_auth.py +1030 -0
  248. graph_agents_cli/setup/cmd_dev_token.py +513 -0
  249. graph_agents_cli/setup/cmd_setup.py +428 -0
  250. graph_agents_cli/setup/cmd_update.py +140 -0
  251. graph_agents_cli/skills/__init__.py +13 -0
  252. graph_agents_cli/skills/_bundle.py +65 -0
  253. graph_agents_cli/skills/data/README.md +19 -0
  254. graph_agents_cli/skills/data/graph-agents-cli-deploy/SKILL.md +357 -0
  255. graph_agents_cli/skills/data/graph-agents-cli-deploy/references/github-settings.md +113 -0
  256. graph_agents_cli/skills/data/graph-agents-cli-deploy/references/gitops.md +137 -0
  257. graph_agents_cli/skills/data/graph-agents-cli-deploy/references/kubernetes.md +315 -0
  258. graph_agents_cli/skills/data/graph-agents-cli-deploy/references/secrets.md +160 -0
  259. graph_agents_cli/skills/data/graph-agents-cli-eval/SKILL.md +303 -0
  260. graph_agents_cli/skills/data/graph-agents-cli-eval/references/dataset_schema.md +282 -0
  261. graph_agents_cli/skills/data/graph-agents-cli-eval/references/metrics-guide.md +143 -0
  262. graph_agents_cli/skills/data/graph-agents-cli-langgraph-code/SKILL.md +659 -0
  263. graph_agents_cli/skills/data/graph-agents-cli-langgraph-code/references/langchain-models.md +124 -0
  264. graph_agents_cli/skills/data/graph-agents-cli-langgraph-code/references/langgraph.md +235 -0
  265. graph_agents_cli/skills/data/graph-agents-cli-langgraph-code/references/template-contract.md +477 -0
  266. graph_agents_cli/skills/data/graph-agents-cli-observability/SKILL.md +231 -0
  267. graph_agents_cli/skills/data/graph-agents-cli-observability/references/langsmith.md +46 -0
  268. graph_agents_cli/skills/data/graph-agents-cli-observability/references/otel.md +59 -0
  269. graph_agents_cli/skills/data/graph-agents-cli-scaffold/SKILL.md +414 -0
  270. graph_agents_cli/skills/data/graph-agents-cli-scaffold/references/flags.md +134 -0
  271. graph_agents_cli/skills/data/graph-agents-cli-workflow/SKILL.md +478 -0
  272. graph_agents_cli/skills/data/graph-agents-cli-workflow/references/brainstorming.md +118 -0
  273. graph_agents_cli/skills/data/graph-agents-cli-workflow/references/commands.md +419 -0
  274. graph_agents_cli/skills/data/graph-agents-cli-workflow/references/extension.md +156 -0
  275. graph_agents_cli/skills/data/graph-agents-cli-workflow/references/internals.md +67 -0
  276. graph_agents_cli/skills/data/graph-agents-cli-workflow/references/spec-template.md +56 -0
  277. graph_agents_cli/skills/data/graph-agents-cli-workflow/references/terminology.md +119 -0
  278. graph_agents_cli/system/__init__.py +15 -0
  279. graph_agents_cli/system/_apply.py +519 -0
  280. graph_agents_cli/system/_checks.py +1023 -0
  281. graph_agents_cli/system/_deploy.py +215 -0
  282. graph_agents_cli/system/_model.py +363 -0
  283. graph_agents_cli/system/_system.py +664 -0
  284. graph_agents_cli/system/_views.py +208 -0
  285. graph_agents_cli/system/cmd_system.py +423 -0
  286. graph_agents_cli-0.3.1.dist-info/METADATA +162 -0
  287. graph_agents_cli-0.3.1.dist-info/RECORD +291 -0
  288. graph_agents_cli-0.3.1.dist-info/WHEEL +4 -0
  289. graph_agents_cli-0.3.1.dist-info/entry_points.txt +2 -0
  290. graph_agents_cli-0.3.1.dist-info/licenses/LICENSE +201 -0
  291. graph_agents_cli-0.3.1.dist-info/licenses/NOTICE +19 -0
@@ -0,0 +1,1352 @@
1
+ # Copyright 2026 Google LLC
2
+ # Modifications Copyright 2026 graph-agents-cli contributors
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # https://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ """Template engine: four-layer cookiecutter rendering and conditional files.
17
+
18
+ Layers, later ones overwriting earlier ones: ``base_templates/_shared`` ->
19
+ ``base_templates/python`` -> ``deployment_targets/<target>/{_shared,python}``
20
+ -> ``agents/<name>`` overlay. Runtime-specific files are selected with
21
+ ``CONDITIONAL_FILES`` and ``select_runtime_files``, not with a fifth layer.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import json
27
+ import logging
28
+ import os
29
+ import pathlib
30
+ import re
31
+ import shlex
32
+ import shutil
33
+ import subprocess
34
+ import sys
35
+ import tempfile
36
+ from datetime import UTC, datetime
37
+ from typing import Any
38
+
39
+ import yaml
40
+ from cookiecutter.main import cookiecutter
41
+ from rich.prompt import Confirm, IntPrompt
42
+
43
+ from graph_agents_cli import _defaults
44
+ from graph_agents_cli._api_policy import POLICY_FILENAME as API_POLICY_FILENAME
45
+ from graph_agents_cli._api_policy import ApiSummary, ExampleCall, secret_envs
46
+ from graph_agents_cli._defaults import (
47
+ DEFAULT_AGENT_GUIDANCE_FILENAME,
48
+ DEFAULT_AUTH_POLICY,
49
+ DEFAULT_CD,
50
+ DEFAULT_MODEL_PROVIDER,
51
+ DEFAULT_MODELS,
52
+ PROVIDER_KEY_VARS,
53
+ auth_policy_implemented_default,
54
+ default_secret_keys,
55
+ )
56
+ from graph_agents_cli._output import Console
57
+
58
+ from .lock_utils import LOCK_FILENAMES, lock_filename, replace_lock_project_name
59
+ from .remote_template import get_base_template_name
60
+ from .version import cli_install_spec, get_current_version
61
+
62
+ # Root of the scaffold package: agents/, base_templates/, deployment_targets/.
63
+ # Module-level so tests can point the engine at a scratch tree.
64
+ SCAFFOLD_ROOT = pathlib.Path(__file__).resolve().parent.parent
65
+
66
+ TEMPLATE_CONFIG_FILE = "templateconfig.yaml"
67
+ MANIFEST_FILENAME = "graph-agents-cli-manifest.yaml"
68
+ EXAMPLE_API_TOOL = "{agent_directory}/tools/example_api.py"
69
+ LANGGRAPH_SERVER_DOCKERFILE = "Dockerfile.langgraph-server"
70
+
71
+
72
+ def agents_dir() -> pathlib.Path:
73
+ return SCAFFOLD_ROOT / "agents"
74
+
75
+
76
+ def base_templates_dir() -> pathlib.Path:
77
+ return SCAFFOLD_ROOT / "base_templates"
78
+
79
+
80
+ def deployment_targets_dir() -> pathlib.Path:
81
+ return SCAFFOLD_ROOT / "deployment_targets"
82
+
83
+
84
+ # =============================================================================
85
+ # Choice tables (values from graph_agents_cli._defaults)
86
+ # =============================================================================
87
+
88
+ DEPLOYMENT_TARGETS: dict[str, dict[str, str]] = {
89
+ "kubernetes": {
90
+ "display_name": "kubernetes",
91
+ "description": "Helm chart on your Kubernetes cluster",
92
+ },
93
+ "none": {
94
+ "display_name": "none",
95
+ "description": "No deployment (local development only)",
96
+ },
97
+ }
98
+ assert tuple(DEPLOYMENT_TARGETS) == _defaults.DEPLOYMENT_TARGETS
99
+
100
+ RUNTIMES: dict[str, dict[str, str]] = {
101
+ "fastapi": {
102
+ "display_name": "fastapi",
103
+ "description": "uvicorn + FastAPI; checkpointer bound in the app; no license",
104
+ },
105
+ "langgraph-server": {
106
+ "display_name": "langgraph-server",
107
+ "description": "LangGraph Server image; native Assistants/Threads API; needs Redis",
108
+ },
109
+ }
110
+ assert tuple(RUNTIMES) == _defaults.RUNTIMES
111
+
112
+ MODEL_PROVIDERS: dict[str, dict[str, str]] = {
113
+ "openai": {"display_name": "openai", "description": "OpenAI API (OPENAI_API_KEY)"},
114
+ "anthropic": {"display_name": "anthropic", "description": "Anthropic API (ANTHROPIC_API_KEY)"},
115
+ "gemini": {"display_name": "gemini", "description": "Google AI Studio (GOOGLE_API_KEY)"},
116
+ "openai-compatible": {
117
+ "display_name": "openai-compatible",
118
+ "description": "Ollama, vLLM, TGI... (MODEL_API_KEY, OPENAI_BASE_URL)",
119
+ },
120
+ }
121
+ assert tuple(MODEL_PROVIDERS) == _defaults.MODEL_PROVIDERS
122
+
123
+ CHECKPOINTERS: dict[str, dict[str, str]] = {
124
+ "memory": {"display_name": "memory", "description": "In-process; state lost on restart"},
125
+ "postgres": {
126
+ "display_name": "postgres",
127
+ "description": "Postgres in the cluster (deployed default)",
128
+ },
129
+ }
130
+ assert tuple(CHECKPOINTERS) == _defaults.CHECKPOINTERS
131
+
132
+ CD_MODES: dict[str, dict[str, str]] = {
133
+ "argocd": {
134
+ "display_name": "argocd",
135
+ "description": "GitOps: CI commits the image tag, Argo CD syncs",
136
+ },
137
+ "helm-push": {
138
+ "display_name": "helm-push",
139
+ "description": "CI runs helm upgrade from a self-hosted runner",
140
+ },
141
+ "skip": {"display_name": "skip", "description": "No CD; deploy from a workstation"},
142
+ }
143
+ assert tuple(CD_MODES) == _defaults.CD_MODES
144
+
145
+ AUTH_POLICIES: dict[str, dict[str, str]] = {
146
+ "shared-bearer": {
147
+ "display_name": "shared-bearer",
148
+ "description": "Authorization: Bearer <API_KEY>",
149
+ },
150
+ "jwt": {
151
+ "display_name": "jwt",
152
+ "description": "Per-user principals from a verified OIDC/JWT bearer token",
153
+ },
154
+ "custom": {
155
+ "display_name": "custom",
156
+ "description": "Your own policy in app/policies/custom.py (fail-closed stub until implemented)",
157
+ },
158
+ }
159
+ assert tuple(AUTH_POLICIES) == _defaults.AUTH_POLICIES
160
+
161
+
162
+ # =============================================================================
163
+ # Combination table (runtime x checkpointer x target)
164
+ # =============================================================================
165
+
166
+ # (runtime, checkpointer, deployment_target) -> (valid, note). An in-memory
167
+ # checkpointer is refused on kubernetes: replicas and restarts would lose state.
168
+ COMBINATIONS: dict[tuple[str, str, str], tuple[bool, str]] = {
169
+ ("fastapi", "memory", "none"): (True, "local dev under uvicorn; state lost on restart"),
170
+ ("fastapi", "memory", "kubernetes"): (
171
+ False,
172
+ "refused; multi-replica and restarts lose state",
173
+ ),
174
+ ("fastapi", "postgres", "none"): (True, "local dev against a local or docker Postgres"),
175
+ ("fastapi", "postgres", "kubernetes"): (True, "default for kubernetes"),
176
+ ("langgraph-server", "memory", "none"): (
177
+ True,
178
+ "`langgraph dev` in-memory server only; not deployable",
179
+ ),
180
+ ("langgraph-server", "memory", "kubernetes"): (
181
+ False,
182
+ "refused; the deployed LangGraph Server keeps runs and threads in Postgres "
183
+ "(with Redis), an in-memory server loses them on restart and cannot run replicas",
184
+ ),
185
+ ("langgraph-server", "postgres", "kubernetes"): (
186
+ True,
187
+ "chart adds Redis; server owns persistence",
188
+ ),
189
+ ("langgraph-server", "postgres", "none"): (
190
+ True,
191
+ "`run` and `playground` use `langgraph dev` (in-memory) locally; postgres is only "
192
+ "the recorded deployed default",
193
+ ),
194
+ }
195
+
196
+
197
+ def default_checkpointer(deployment_target: str) -> str:
198
+ """``none`` (local only) defaults to memory, kubernetes to postgres."""
199
+ return "postgres" if deployment_target == "kubernetes" else "memory"
200
+
201
+
202
+ def validate_combination(
203
+ runtime: str,
204
+ checkpointer: str,
205
+ deployment_target: str,
206
+ cd: str | None = None,
207
+ ) -> None:
208
+ """Enforce the combination table and the ``--cd`` rule; raise ValueError with the reason."""
209
+ key = (runtime, checkpointer, deployment_target)
210
+ entry = COMBINATIONS.get(key)
211
+ if entry is None:
212
+ raise ValueError(
213
+ f"Unknown combination runtime={runtime} checkpointer={checkpointer} "
214
+ f"deployment_target={deployment_target}."
215
+ )
216
+ valid, note = entry
217
+ if not valid:
218
+ raise ValueError(
219
+ f"Invalid combination: runtime={runtime} checkpointer={checkpointer} "
220
+ f"deployment_target={deployment_target} ({note}).\n"
221
+ " Valid: fastapi or langgraph-server with postgres on kubernetes; any "
222
+ "checkpointer with --deployment-target none."
223
+ )
224
+ if cd is not None and cd != "skip" and deployment_target != "kubernetes":
225
+ raise ValueError(
226
+ f"--cd {cd} requires --deployment-target kubernetes "
227
+ f"(got deployment_target={deployment_target})."
228
+ )
229
+
230
+
231
+ # =============================================================================
232
+ # Conditional files
233
+ # =============================================================================
234
+ # Maps a path in the rendered project to its inclusion condition. Paths that
235
+ # fail their condition are renamed to unused_* and removed afterwards.
236
+ #
237
+ # The config dict carries: deployment_target, runtime, cd, has_api_policy,
238
+ # has_example_api (the policy's first API allows an operation the example can make).
239
+ # `deployment/argocd` is listed before `deployment` so its own rule is applied
240
+ # even when the whole directory is kept.
241
+
242
+ CONDITIONAL_FILES: dict[str, Any] = {
243
+ ".github/workflows/staging.yaml": lambda c: c.get("cd", "skip") != "skip",
244
+ ".github/workflows/promote-to-prod.yaml": lambda c: c.get("cd", "skip") != "skip",
245
+ "deployment/argocd": lambda c: c.get("cd") == "argocd",
246
+ "deployment": lambda c: c.get("deployment_target") == "kubernetes",
247
+ API_POLICY_FILENAME: lambda c: bool(c.get("has_api_policy", False)),
248
+ EXAMPLE_API_TOOL: lambda c: bool(c.get("has_example_api", False)),
249
+ }
250
+
251
+
252
+ def apply_conditional_files(
253
+ project_path: pathlib.Path,
254
+ config: dict[str, Any],
255
+ agent_directory: str = "app",
256
+ ) -> None:
257
+ """Rename every conditional path whose condition is False to ``unused_*``.
258
+
259
+ Args:
260
+ project_path: Path to the generated project directory
261
+ config: dict with deployment_target, runtime, cd, has_api_policy, has_example_api
262
+ agent_directory: replaces the ``{agent_directory}`` placeholder in paths
263
+ """
264
+ for rel_path_template, condition_fn in CONDITIONAL_FILES.items():
265
+ rel_path = rel_path_template.replace("{agent_directory}", agent_directory)
266
+ file_path = project_path / rel_path
267
+
268
+ if not file_path.exists():
269
+ continue
270
+
271
+ if condition_fn(config):
272
+ logging.debug("Conditional file '%s' condition True, keeping", rel_path)
273
+ continue
274
+
275
+ unused_path = file_path.parent / f"unused_{file_path.name}"
276
+ logging.debug(
277
+ "Conditional file '%s' condition False, renaming to %s", rel_path, unused_path.name
278
+ )
279
+ if unused_path.exists():
280
+ if unused_path.is_dir():
281
+ shutil.rmtree(unused_path)
282
+ else:
283
+ unused_path.unlink()
284
+ file_path.rename(unused_path)
285
+
286
+
287
+ def _remove_unused_paths(project_path: pathlib.Path) -> None:
288
+ """Delete the ``unused_*`` files and directories left by conditional templates.
289
+
290
+ ``rglob`` (not ``glob.glob``) so entries under dot-directories such as
291
+ ``.github/workflows`` are found too.
292
+ """
293
+ # Longest paths first so a directory is removed after anything inside it.
294
+ for unused_path in sorted(project_path.rglob("unused_*"), key=lambda p: -len(p.parts)):
295
+ if unused_path.is_dir():
296
+ shutil.rmtree(unused_path)
297
+ logging.debug("Deleted unused directory: %s", unused_path)
298
+ elif unused_path.exists():
299
+ unused_path.unlink()
300
+ logging.debug("Deleted unused file: %s", unused_path)
301
+
302
+
303
+ def select_runtime_files(project_path: pathlib.Path, runtime: str, project_name: str) -> None:
304
+ """Pick the runtime's Dockerfile and bundled lock.
305
+
306
+ The template ships ``Dockerfile`` (fastapi) and ``Dockerfile.langgraph-server``;
307
+ under ``langgraph-server`` the latter replaces the former, otherwise it is
308
+ deleted. Of ``uv-fastapi.lock`` and ``uv-langgraph-server.lock`` the matching
309
+ one becomes ``uv.lock`` (project-name placeholder filled in) and the other is
310
+ deleted. A template that ships neither bundled lock gets a warning.
311
+ """
312
+ console = Console()
313
+ dockerfile = project_path / "Dockerfile"
314
+ server_dockerfile = project_path / LANGGRAPH_SERVER_DOCKERFILE
315
+
316
+ if runtime == "langgraph-server":
317
+ if server_dockerfile.exists():
318
+ if dockerfile.exists():
319
+ dockerfile.unlink()
320
+ server_dockerfile.rename(dockerfile)
321
+ logging.debug("Selected %s as Dockerfile", LANGGRAPH_SERVER_DOCKERFILE)
322
+ else:
323
+ console.print(
324
+ f"⚠️ The template ships no {LANGGRAPH_SERVER_DOCKERFILE}; keeping Dockerfile as is.",
325
+ style="yellow",
326
+ )
327
+ elif server_dockerfile.exists():
328
+ server_dockerfile.unlink()
329
+ logging.debug("Removed %s (runtime %s)", LANGGRAPH_SERVER_DOCKERFILE, runtime)
330
+
331
+ wanted = lock_filename(runtime)
332
+ wanted_path = project_path / wanted
333
+ uv_lock = project_path / "uv.lock"
334
+ if wanted_path.exists():
335
+ uv_lock.write_text(
336
+ replace_lock_project_name(wanted_path.read_text(encoding="utf-8"), project_name),
337
+ encoding="utf-8",
338
+ )
339
+ wanted_path.unlink()
340
+ logging.debug("Selected %s as uv.lock", wanted)
341
+ elif uv_lock.exists():
342
+ logging.debug("Template ships its own uv.lock; no bundled runtime lock to select")
343
+ else:
344
+ console.print(
345
+ f"⚠️ No bundled lock ({wanted}) in the template; run `uv lock` in the project.",
346
+ style="yellow",
347
+ )
348
+ for other in LOCK_FILENAMES.values():
349
+ if other != wanted:
350
+ (project_path / other).unlink(missing_ok=True)
351
+
352
+
353
+ # =============================================================================
354
+ # Dependencies helper (remote templates inheriting base-template deps)
355
+ # =============================================================================
356
+
357
+
358
+ def _add_dependencies(
359
+ project_path: pathlib.Path,
360
+ dependencies: list[str],
361
+ success_message: str,
362
+ auto_approve: bool = False,
363
+ interactive: bool = False,
364
+ ) -> bool:
365
+ """Add dependencies with ``uv add``, confirming first in interactive mode."""
366
+ if not dependencies:
367
+ return True
368
+
369
+ console = Console()
370
+ deps_str = shlex.join(dependencies)
371
+
372
+ should_add = True
373
+ if interactive:
374
+ should_add = Confirm.ask("\n? Add these dependencies automatically?", default=True)
375
+
376
+ if not should_add:
377
+ console.print("\n⚠️ Skipped dependency installation.", style="yellow")
378
+ console.print(" To add them manually later, run:", style="dim")
379
+ console.print(f" cd {project_path.name}", style="dim")
380
+ console.print(f" uv add {deps_str}\n", style="dim")
381
+ return False
382
+
383
+ try:
384
+ if auto_approve:
385
+ console.print(
386
+ f"✓ Auto-installing dependencies: {', '.join(dependencies)}",
387
+ style="bold cyan",
388
+ )
389
+ else:
390
+ console.print(f"\n✓ Running: uv add {deps_str}", style="bold cyan")
391
+
392
+ from graph_agents_cli._runner import run_resolved
393
+ from graph_agents_cli._tools import ToolNotFoundError
394
+
395
+ cmd = ["uv", "add", *dependencies]
396
+ result = run_resolved(
397
+ cmd,
398
+ cwd=project_path,
399
+ capture_output=True,
400
+ text=True,
401
+ check=True,
402
+ )
403
+
404
+ if not auto_approve:
405
+ output_lines = result.stderr.strip().split("\n")
406
+ for line in output_lines:
407
+ if "Resolved" in line or "Installed" in line:
408
+ console.print(f" {line}", style="dim")
409
+ break
410
+
411
+ console.print(f"✓ {success_message}\n", style="bold green")
412
+ return True
413
+
414
+ except subprocess.CalledProcessError as e:
415
+ console.print(f"\n✗ Failed to add dependencies: {e.stderr.strip()}", style="bold red")
416
+ console.print(" You can add them manually:", style="yellow")
417
+ console.print(f" cd {project_path.name}", style="dim")
418
+ console.print(f" uv add {deps_str}\n", style="dim")
419
+ return False
420
+ except ToolNotFoundError:
421
+ console.print("\n✗ uv command not found. Please install uv first.", style="bold red")
422
+ console.print(" Install from: https://docs.astral.sh/uv/", style="dim")
423
+ console.print("\n To add dependencies manually:", style="yellow")
424
+ console.print(f" cd {project_path.name}", style="dim")
425
+ console.print(f" uv add {deps_str}\n", style="dim")
426
+ return False
427
+
428
+
429
+ def add_base_template_dependencies(
430
+ project_path: pathlib.Path,
431
+ base_dependencies: list[str],
432
+ base_template_name: str,
433
+ auto_approve: bool = False,
434
+ interactive: bool = False,
435
+ ) -> bool:
436
+ """Add a base template's ``extra_dependencies`` to a project rendered from a remote template."""
437
+ if not base_dependencies:
438
+ return True
439
+
440
+ console = Console()
441
+ console.print(
442
+ f"\n✓ Ensuring base template '{base_template_name}' dependencies",
443
+ style="bold cyan",
444
+ )
445
+ console.print(" Adding the following dependencies:", style="white")
446
+ for dep in base_dependencies:
447
+ console.print(f" • {dep}", style="yellow")
448
+
449
+ return _add_dependencies(
450
+ project_path=project_path,
451
+ dependencies=base_dependencies,
452
+ success_message="Dependencies added successfully",
453
+ auto_approve=auto_approve,
454
+ interactive=interactive,
455
+ )
456
+
457
+
458
+ # =============================================================================
459
+ # Agent directory validation (Python identifiers)
460
+ # =============================================================================
461
+
462
+
463
+ def validate_agent_directory_name(
464
+ agent_dir: str, allow_dot: bool = False, language: str = "python"
465
+ ) -> None:
466
+ """Validate that an agent directory name is a valid Python module path.
467
+
468
+ Args:
469
+ agent_dir: The agent directory name to validate
470
+ allow_dot: If True, allows "." as a special value indicating flat structure
471
+ language: Kept for call compatibility; only ``python`` exists
472
+
473
+ Raises:
474
+ ValueError: If the agent directory name is not valid
475
+ """
476
+ if agent_dir == ".":
477
+ if allow_dot:
478
+ return
479
+ raise ValueError(
480
+ "Agent directory '.' is not valid in this context. "
481
+ "Use '.' only to indicate flat structure templates."
482
+ )
483
+ # Allowlist: one or more path components of letters, digits, hyphens and
484
+ # underscores. This blocks absolute paths, dot-dot, backslashes, tilde,
485
+ # Windows drives, and any other path-traversal payload.
486
+ if not re.match(r"^[a-zA-Z0-9_-]+(?:/[a-zA-Z0-9_-]+)*$", agent_dir):
487
+ raise ValueError(
488
+ f"Invalid agent directory name '{agent_dir}'. It can only contain "
489
+ "letters, numbers, hyphens, underscores, and forward slashes (no "
490
+ "absolute paths or dot-dot components)."
491
+ )
492
+ if language != "python":
493
+ raise ValueError(f"Unsupported language '{language}': only python templates exist.")
494
+ if "-" in agent_dir:
495
+ raise ValueError(
496
+ f"Agent directory '{agent_dir}' contains hyphens (-) which are not allowed. "
497
+ "Agent directories must be valid Python identifiers since they're used as module names. "
498
+ "Please use underscores (_) or lowercase letters instead."
499
+ )
500
+ for component in agent_dir.split("/"):
501
+ if not component.isidentifier():
502
+ raise ValueError(
503
+ f"Agent directory '{agent_dir}' is not a valid Python identifier. "
504
+ "Agent directories must be valid Python identifiers since they're used as module names. "
505
+ "Please use only lowercase letters, numbers, and underscores, and don't start with a number."
506
+ )
507
+
508
+
509
+ # =============================================================================
510
+ # Template discovery
511
+ # =============================================================================
512
+
513
+
514
+ def get_available_agents(
515
+ deployment_target: str | None = None, include_hidden: bool = False
516
+ ) -> dict:
517
+ """Load the bundled agent templates, numbered from 1 for display.
518
+
519
+ Each agent dict includes: name, display_name, description, language, framework.
520
+
521
+ Args:
522
+ deployment_target: Optional deployment target to filter agents
523
+ include_hidden: If True, include agents marked as hidden in templateconfig
524
+ """
525
+ PRIORITY_ORDER = {"langgraph": 0}
526
+
527
+ agents_list = []
528
+ agents_root = agents_dir()
529
+ if not agents_root.is_dir():
530
+ return {}
531
+
532
+ for agent_dir in sorted(agents_root.iterdir()):
533
+ if not agent_dir.is_dir() or agent_dir.name.startswith("__"):
534
+ continue
535
+ template_config_path = agent_dir / ".template" / TEMPLATE_CONFIG_FILE
536
+ if not template_config_path.exists():
537
+ continue
538
+ try:
539
+ with open(template_config_path, encoding="utf-8") as f:
540
+ config = yaml.safe_load(f) or {}
541
+ agent_name = agent_dir.name
542
+ settings = config.get("settings", {})
543
+
544
+ if not include_hidden and config.get("hidden", False):
545
+ continue
546
+
547
+ if deployment_target:
548
+ targets = settings.get("deployment_targets", [])
549
+ if isinstance(targets, str):
550
+ targets = [targets]
551
+ if deployment_target not in targets:
552
+ continue
553
+
554
+ language = settings.get("language", "python")
555
+ tags = settings.get("tags", [])
556
+ framework = "langgraph" if "langgraph" in tags else "other"
557
+
558
+ agents_list.append(
559
+ {
560
+ "name": agent_name,
561
+ "display_name": config.get("display_name", agent_name),
562
+ "description": config.get("description", "No description available"),
563
+ "language": language,
564
+ "framework": framework,
565
+ "priority": PRIORITY_ORDER.get(agent_name, 100),
566
+ }
567
+ )
568
+ except Exception as e:
569
+ logging.warning(f"Could not load agent from {agent_dir}: {e}")
570
+
571
+ agents_list.sort(key=lambda agent: (agent["priority"], agent["name"]))
572
+ return {i + 1: agent for i, agent in enumerate(agents_list)}
573
+
574
+
575
+ def get_available_base_templates() -> list[str]:
576
+ """Names of the bundled templates a remote template may inherit from."""
577
+ agents = get_available_agents(include_hidden=True)
578
+ return sorted(agent_info["name"] for agent_info in agents.values())
579
+
580
+
581
+ def validate_base_template(base_template: str) -> bool:
582
+ """True when ``base_template`` names a bundled template."""
583
+ return base_template in get_available_base_templates()
584
+
585
+
586
+ def load_template_config(template_dir: pathlib.Path) -> dict[str, Any]:
587
+ """Read ``templateconfig.yaml`` from a ``.template`` directory ({} if absent)."""
588
+ config_file = template_dir / TEMPLATE_CONFIG_FILE
589
+ if not config_file.exists():
590
+ return {}
591
+
592
+ try:
593
+ with open(config_file, encoding="utf-8") as f:
594
+ config = yaml.safe_load(f)
595
+ return config if config else {}
596
+ except Exception as e:
597
+ logging.error(f"Error loading template config: {e}")
598
+ return {}
599
+
600
+
601
+ def get_agent_language(agent_name: str, remote_config: dict[str, Any] | None = None) -> str:
602
+ """The template language: always ``python``; anything else is refused."""
603
+ if remote_config:
604
+ config = remote_config
605
+ else:
606
+ config = load_template_config(agents_dir() / agent_name / ".template")
607
+
608
+ language = (config or {}).get("settings", {}).get("language", "python")
609
+ if language != "python":
610
+ raise ValueError(
611
+ f"Template '{agent_name}' declares language '{language}'; only python templates "
612
+ "are supported."
613
+ )
614
+ return "python"
615
+
616
+
617
+ def get_deployment_targets(agent_name: str, remote_config: dict[str, Any] | None = None) -> list:
618
+ """Get available deployment targets for the selected agent."""
619
+ if remote_config:
620
+ config = remote_config
621
+ else:
622
+ config = load_template_config(agents_dir() / agent_name / ".template")
623
+
624
+ if not config:
625
+ return []
626
+
627
+ targets = config.get("settings", {}).get("deployment_targets", [])
628
+ return targets if isinstance(targets, list) else [targets]
629
+
630
+
631
+ def get_template_path(agent_name: str) -> pathlib.Path:
632
+ """Get the absolute path to a bundled agent's ``.template`` directory."""
633
+ template_path = agents_dir() / agent_name / ".template"
634
+ logging.debug("Looking for template in: %s", template_path)
635
+ if not template_path.exists():
636
+ raise ValueError(f"Template directory not found at {template_path}")
637
+ return template_path
638
+
639
+
640
+ # =============================================================================
641
+ # Interactive prompts
642
+ # =============================================================================
643
+
644
+
645
+ def prompt_choice(
646
+ title: str,
647
+ choices: dict[str, dict[str, str]],
648
+ default_value: str | None = None,
649
+ *,
650
+ header: str | None = None,
651
+ prompt: str = "Enter the number of your choice",
652
+ ) -> str:
653
+ """Numbered menu over ``choices`` (key -> {display_name, description})."""
654
+ console = Console()
655
+ keys = list(choices)
656
+ default_idx = 1
657
+ if default_value and default_value in keys:
658
+ default_idx = keys.index(default_value) + 1
659
+
660
+ console.print(f"\n> {title}")
661
+ if header:
662
+ console.print(f"\n [bold cyan]{header}[/]")
663
+ for idx, key in enumerate(keys, 1):
664
+ info = choices[key]
665
+ name_padded = info.get("display_name", key).ljust(18)
666
+ description = info.get("description", "")
667
+ current = " [dim cyan](current)[/]" if key == default_value else ""
668
+ console.print(f" {idx}. [bold]{name_padded}[/] [dim]{description}[/]{current}")
669
+
670
+ while True:
671
+ choice = IntPrompt.ask(f"\n{prompt}", default=default_idx, show_default=True)
672
+ if 1 <= choice <= len(keys):
673
+ return keys[choice - 1]
674
+ console.print(f"Please enter a number between 1 and {len(keys)}.", style="yellow")
675
+
676
+
677
+ def prompt_deployment_target(
678
+ agent_name: str,
679
+ remote_config: dict[str, Any] | None = None,
680
+ default_value: str | None = None,
681
+ ) -> str:
682
+ """Ask the user to select a deployment target the agent supports."""
683
+ targets = get_deployment_targets(agent_name, remote_config=remote_config)
684
+ if not targets:
685
+ return ""
686
+ choices = {
687
+ t: DEPLOYMENT_TARGETS.get(t, {"display_name": t, "description": ""}) for t in targets
688
+ }
689
+ return prompt_choice(
690
+ "Please select a deployment target:",
691
+ choices,
692
+ default_value,
693
+ header="☁️ Deployment Targets",
694
+ prompt="Enter the number of your deployment target choice",
695
+ )
696
+
697
+
698
+ def prompt_runtime(default_value: str | None = None) -> str:
699
+ return prompt_choice("Please select a runtime:", RUNTIMES, default_value, header="Runtimes")
700
+
701
+
702
+ def prompt_model_provider(default_value: str | None = None) -> str:
703
+ return prompt_choice(
704
+ "Please select a model provider:", MODEL_PROVIDERS, default_value, header="Providers"
705
+ )
706
+
707
+
708
+ def prompt_checkpointer(default_value: str | None = None) -> str:
709
+ return prompt_choice(
710
+ "Please select the deployed checkpointer:",
711
+ CHECKPOINTERS,
712
+ default_value,
713
+ header="Checkpointers",
714
+ )
715
+
716
+
717
+ def prompt_cd(default_value: str | None = None) -> str:
718
+ return prompt_choice("Please select a CD mode:", CD_MODES, default_value, header="CD Modes")
719
+
720
+
721
+ def prompt_auth_policy(default_value: str | None = None) -> str:
722
+ return prompt_choice(
723
+ "Please select an authentication policy:",
724
+ AUTH_POLICIES,
725
+ default_value,
726
+ header="Auth Policies",
727
+ )
728
+
729
+
730
+ # =============================================================================
731
+ # Cookiecutter context
732
+ # =============================================================================
733
+
734
+ COPY_WITHOUT_RENDER: list[str] = [
735
+ "deployment/helm/*/templates/*",
736
+ "deployment/helm/*/templates/**/*",
737
+ "*.tpl",
738
+ ".github/workflows/*",
739
+ "*.lock",
740
+ "*.ipynb",
741
+ "node_modules/**",
742
+ ".venv/**",
743
+ "__pycache__/**",
744
+ ".git/*",
745
+ ]
746
+
747
+
748
+ def build_cookiecutter_context(
749
+ *,
750
+ project_name: str,
751
+ agent_name: str,
752
+ deployment_target: str,
753
+ runtime: str,
754
+ model_provider: str = DEFAULT_MODEL_PROVIDER,
755
+ model: str | None = None,
756
+ checkpointer: str | None = None,
757
+ registry: str = "",
758
+ cd: str = DEFAULT_CD,
759
+ auth_policy: str = DEFAULT_AUTH_POLICY,
760
+ has_api_policy: bool = False,
761
+ apis: tuple[ApiSummary, ...] | list[ApiSummary] = (),
762
+ example_call: ExampleCall | None = None,
763
+ process: str | None = None,
764
+ agent_guidance_filename: str = DEFAULT_AGENT_GUIDANCE_FILENAME,
765
+ agent_directory: str = "app",
766
+ template_config: dict[str, Any] | None = None,
767
+ recorded_base_template: str | None = None,
768
+ generated_at: str | None = None,
769
+ auth_policy_implemented: bool | None = None,
770
+ ) -> dict[str, Any]:
771
+ """The variables every template may use.
772
+
773
+ List-valued variables are wrapped in a one-element list because cookiecutter
774
+ treats a bare list as a choice and would keep only its first item. ``apis``
775
+ summarises the declared APIs of ``api-policy.yaml`` (name, base_url_env,
776
+ auth, token_env) and every ``auth: bearer`` API's ``token_env`` joins
777
+ ``secret_keys``. ``example_api`` is the call ``tools/example_api.py`` makes
778
+ (``dev.policy_check.example_call``: the first operation the policy's first
779
+ API allows, any method), empty when that API allows none; the example is
780
+ then not rendered.
781
+ ``auth_policy_implemented`` is derived from ``auth_policy`` unless a
782
+ recorded value is passed (an in-folder re-render keeps the developer's flip).
783
+ """
784
+ settings = (template_config or {}).get("settings", {})
785
+ tags = settings.get("tags", []) or []
786
+ model = model or DEFAULT_MODELS.get(model_provider, "")
787
+ checkpointer = checkpointer or default_checkpointer(deployment_target)
788
+ api_summaries = list(apis) if has_api_policy else []
789
+ return {
790
+ "project_name": project_name,
791
+ "agent_name": agent_name,
792
+ "package_version": get_current_version(),
793
+ "generated_at": generated_at or datetime.now(tz=UTC).isoformat(),
794
+ "agent_directory": agent_directory,
795
+ "language": "python",
796
+ "deployment_target": deployment_target,
797
+ "runtime": runtime,
798
+ "model_provider": model_provider,
799
+ "model": model,
800
+ "provider_key_var": PROVIDER_KEY_VARS.get(model_provider, "MODEL_API_KEY"),
801
+ "checkpointer": checkpointer,
802
+ "registry": registry or "",
803
+ "cd": cd,
804
+ "auth_policy": auth_policy,
805
+ # Provided so the manifest template need not derive it.
806
+ "auth_policy_implemented": (
807
+ auth_policy_implemented_default(auth_policy)
808
+ if auth_policy_implemented is None
809
+ else bool(auth_policy_implemented)
810
+ ),
811
+ "agent_guidance_filename": agent_guidance_filename,
812
+ "process": process or "",
813
+ "has_api_policy": bool(has_api_policy),
814
+ "apis": [[summary.as_context() for summary in api_summaries]],
815
+ "example_api": (
816
+ example_call.as_context() if has_api_policy and example_call is not None else {}
817
+ ),
818
+ "secret_keys": [
819
+ default_secret_keys(
820
+ model_provider,
821
+ runtime,
822
+ secret_envs(api_summaries),
823
+ auth_policy=auth_policy,
824
+ )
825
+ ],
826
+ "default_judge_model": model,
827
+ "cli_install_spec": cli_install_spec(),
828
+ "tags": [list(tags)],
829
+ "settings": settings,
830
+ "recorded_base_template": recorded_base_template or agent_name,
831
+ "_copy_without_render": list(COPY_WITHOUT_RENDER),
832
+ }
833
+
834
+
835
+ # =============================================================================
836
+ # Rendering
837
+ # =============================================================================
838
+
839
+
840
+ def _resolve_agent_directory(
841
+ template_config: dict[str, Any],
842
+ cli_overrides: dict[str, Any] | None,
843
+ remote_template_path: pathlib.Path | None,
844
+ ) -> str:
845
+ """Agent directory from CLI override or template settings; ``.`` means flat structure."""
846
+ agent_dir = None
847
+ if (
848
+ cli_overrides
849
+ and "settings" in cli_overrides
850
+ and "agent_directory" in cli_overrides["settings"]
851
+ ):
852
+ agent_dir = cli_overrides["settings"]["agent_directory"]
853
+ else:
854
+ agent_dir = template_config.get("settings", {}).get("agent_directory", "app")
855
+
856
+ if agent_dir == ".":
857
+ if remote_template_path:
858
+ folder_name = remote_template_path.name.replace("-", "_")
859
+ logging.debug(
860
+ "Flat structure (-dir .): deriving target '%s' from folder name", folder_name
861
+ )
862
+ agent_dir = folder_name
863
+ else:
864
+ logging.debug("Flat structure (-dir .): using 'app' as fallback")
865
+ agent_dir = "app"
866
+
867
+ validate_agent_directory_name(agent_dir)
868
+ return agent_dir
869
+
870
+
871
+ def process_template(
872
+ *,
873
+ agent_name: str,
874
+ template_dir: pathlib.Path,
875
+ project_name: str,
876
+ deployment_target: str,
877
+ runtime: str,
878
+ model_provider: str = DEFAULT_MODEL_PROVIDER,
879
+ model: str | None = None,
880
+ checkpointer: str | None = None,
881
+ registry: str = "",
882
+ cd: str = DEFAULT_CD,
883
+ auth_policy: str = DEFAULT_AUTH_POLICY,
884
+ has_api_policy: bool = False,
885
+ apis: tuple[ApiSummary, ...] | list[ApiSummary] = (),
886
+ example_call: ExampleCall | None = None,
887
+ process: str | None = None,
888
+ output_dir: pathlib.Path | None = None,
889
+ remote_template_path: pathlib.Path | None = None,
890
+ remote_config: dict[str, Any] | None = None,
891
+ in_folder: bool = False,
892
+ overlay_is_project: bool = False,
893
+ cli_overrides: dict[str, Any] | None = None,
894
+ remote_spec: Any | None = None,
895
+ recorded_base_template: str | None = None,
896
+ agent_guidance_filename: str = DEFAULT_AGENT_GUIDANCE_FILENAME,
897
+ auth_policy_implemented: bool | None = None,
898
+ ) -> pathlib.Path:
899
+ """Render the template layers into a new project and return its path.
900
+
901
+ Args:
902
+ agent_name: Name of the agent template to use
903
+ template_dir: The ``.template`` directory of the selected template
904
+ project_name: Name of the project to create
905
+ deployment_target: ``kubernetes`` or ``none``
906
+ runtime, model_provider, model, checkpointer, registry, cd, auth_policy:
907
+ the create parameters (validated by the caller)
908
+ has_api_policy: keep ``api-policy.yaml``
909
+ apis: the APIs the policy declares (see ``build_cookiecutter_context``)
910
+ example_call: the call the example API tool makes; None leaves the tool out
911
+ process: governing process document (path or string), recorded verbatim
912
+ output_dir: Optional output directory path, defaults to current directory
913
+ remote_template_path: Optional path to remote template for overlay
914
+ remote_config: Optional remote template configuration
915
+ in_folder: Render directly into ``output_dir`` instead of a subdirectory
916
+ overlay_is_project: True when the overlay source is the output project
917
+ itself (``--agent local@.``), so its manifest is the project's own
918
+ and is copied. False for any other source, whose manifest describes
919
+ the template and is skipped.
920
+ cli_overrides: CLI override values that take precedence over template config
921
+ remote_spec: the parsed remote spec, when any (unused, kept for callers)
922
+ recorded_base_template: what the manifest records as base_template
923
+ agent_guidance_filename: name of the root guidance file
924
+ auth_policy_implemented: recorded flag to keep on an in-folder re-render (None derives it)
925
+ """
926
+ logging.debug("Processing template from %s", template_dir)
927
+ logging.debug("Project name: %s", project_name)
928
+ logging.debug("Output directory: %s", output_dir)
929
+
930
+ is_remote = remote_template_path is not None
931
+
932
+ if is_remote:
933
+ base_template_name = get_base_template_name(remote_config or {})
934
+ agent_path = agents_dir() / base_template_name
935
+ logging.debug("Remote template using base: %s", base_template_name)
936
+ elif cli_overrides and cli_overrides.get("base_template"):
937
+ base_template_name = cli_overrides["base_template"]
938
+ agent_path = agents_dir() / base_template_name
939
+ logging.debug("Using base template override: %s", base_template_name)
940
+ else:
941
+ base_template_name = agent_name
942
+ agent_path = pathlib.Path(template_dir).parent
943
+
944
+ logging.debug("agent path: %s", agent_path)
945
+ if not agent_path.exists():
946
+ # Fail here rather than carry on: an unresolvable base silently copies
947
+ # no agent layer and produces a half-built project.
948
+ raise ValueError(
949
+ f"Base template '{base_template_name}' not found at {agent_path}. "
950
+ "A template declares its base in .template/templateconfig.yaml; "
951
+ f"'{base_template_name}' is not one this CLI version provides."
952
+ )
953
+
954
+ template_config = (
955
+ remote_config if remote_config else load_template_config(pathlib.Path(template_dir))
956
+ )
957
+ if not template_config:
958
+ raise ValueError("Could not load template config")
959
+
960
+ get_agent_language(agent_name, template_config)
961
+ agent_directory = _resolve_agent_directory(template_config, cli_overrides, remote_template_path)
962
+
963
+ available_targets = template_config.get("settings", {}).get("deployment_targets", [])
964
+ if isinstance(available_targets, str):
965
+ available_targets = [available_targets]
966
+ if deployment_target not in available_targets:
967
+ raise ValueError(
968
+ f"Invalid deployment target '{deployment_target}'. Available targets: {available_targets}"
969
+ )
970
+
971
+ destination_dir = output_dir if output_dir else pathlib.Path.cwd()
972
+ destination_dir.mkdir(parents=True, exist_ok=True)
973
+
974
+ with tempfile.TemporaryDirectory() as temp_dir:
975
+ temp_path = pathlib.Path(temp_dir)
976
+ original_dir = pathlib.Path.cwd()
977
+
978
+ try:
979
+ os.chdir(temp_path)
980
+
981
+ cookiecutter_template = temp_path / "template"
982
+ project_template = cookiecutter_template / "{{cookiecutter.project_name}}"
983
+ project_template.mkdir(parents=True)
984
+
985
+ # 1a. Shared base template files
986
+ shared_base_path = base_templates_dir() / "_shared"
987
+ if shared_base_path.exists():
988
+ copy_files(shared_base_path, project_template, agent_name, overwrite=True)
989
+ logging.debug("1a. Copied shared base template from %s", shared_base_path)
990
+
991
+ # 1b. Python base template files
992
+ language_base_path = base_templates_dir() / "python"
993
+ if language_base_path.exists():
994
+ copy_files(language_base_path, project_template, agent_name, overwrite=True)
995
+ logging.debug("1b. Copied python base template from %s", language_base_path)
996
+ else:
997
+ raise FileNotFoundError(f"Language base template not found: {language_base_path}")
998
+
999
+ # 2. Deployment target layer
1000
+ if deployment_target in DEPLOYMENT_TARGETS:
1001
+ for layer in ("_shared", "python"):
1002
+ layer_path = deployment_targets_dir() / deployment_target / layer
1003
+ if layer_path.exists():
1004
+ copy_files(
1005
+ layer_path, project_template, agent_name=agent_name, overwrite=True
1006
+ )
1007
+ logging.debug("2. Copied deployment files from %s", layer_path)
1008
+
1009
+ # 3. Agent overlay: app/, tests/, deployment/, then the remaining
1010
+ # top-level items so a framework template can own the whole project.
1011
+ if is_remote or (cli_overrides and cli_overrides.get("base_template")):
1012
+ template_agent_directory = "app"
1013
+ else:
1014
+ template_agent_directory = template_config.get("settings", {}).get(
1015
+ "agent_directory", "app"
1016
+ )
1017
+
1018
+ source_agent_folder = agent_path / template_agent_directory
1019
+ if source_agent_folder.exists():
1020
+ logging.debug(
1021
+ "3. Copying agent folder %s -> %s", template_agent_directory, agent_directory
1022
+ )
1023
+ copy_files(
1024
+ source_agent_folder,
1025
+ project_template / agent_directory,
1026
+ agent_name,
1027
+ overwrite=True,
1028
+ )
1029
+
1030
+ other_folders = ["tests", "deployment"]
1031
+ for folder in other_folders:
1032
+ agent_folder = agent_path / folder
1033
+ if agent_folder.exists():
1034
+ logging.debug("3. Copying %s folder with override", folder)
1035
+ copy_files(agent_folder, project_template / folder, agent_name, overwrite=True)
1036
+
1037
+ already_copied = {
1038
+ ".template",
1039
+ "__pycache__",
1040
+ template_agent_directory.split("/")[0],
1041
+ *other_folders,
1042
+ }
1043
+ for item in agent_path.iterdir():
1044
+ if item.name in already_copied:
1045
+ continue
1046
+ logging.debug("3c. Overlaying agent-owned %s", item.name)
1047
+ copy_files(item, project_template / item.name, agent_name, overwrite=True)
1048
+
1049
+ # 4. cookiecutter.json
1050
+ cookiecutter_config = build_cookiecutter_context(
1051
+ project_name=project_name,
1052
+ agent_name=agent_name,
1053
+ deployment_target=deployment_target,
1054
+ runtime=runtime,
1055
+ model_provider=model_provider,
1056
+ model=model,
1057
+ checkpointer=checkpointer,
1058
+ registry=registry,
1059
+ cd=cd,
1060
+ auth_policy=auth_policy,
1061
+ has_api_policy=has_api_policy,
1062
+ apis=apis,
1063
+ example_call=example_call,
1064
+ process=process,
1065
+ agent_guidance_filename=agent_guidance_filename,
1066
+ agent_directory=agent_directory,
1067
+ template_config=template_config,
1068
+ recorded_base_template=recorded_base_template,
1069
+ auth_policy_implemented=auth_policy_implemented,
1070
+ )
1071
+ with open(
1072
+ cookiecutter_template / "cookiecutter.json", "w", encoding="utf-8"
1073
+ ) as json_file:
1074
+ json.dump(cookiecutter_config, json_file, indent=4)
1075
+
1076
+ logging.debug("Template structure created at %s", cookiecutter_template)
1077
+
1078
+ cookiecutter(
1079
+ str(cookiecutter_template),
1080
+ no_input=True,
1081
+ overwrite_if_exists=True,
1082
+ extra_context={"project_name": project_name, "agent_name": agent_name},
1083
+ )
1084
+ logging.debug("Template processing completed successfully")
1085
+
1086
+ generated_project_dir = temp_path / project_name
1087
+
1088
+ # 4b. Without a call to show, the example API tool renders empty. Drop it
1089
+ # before anything is copied: an in-folder render (enhance --force) must
1090
+ # neither blank nor delete an example_api.py the project already has.
1091
+ has_example_api = has_api_policy and example_call is not None
1092
+ if not has_example_api:
1093
+ example_tool = generated_project_dir / EXAMPLE_API_TOOL.replace(
1094
+ "{agent_directory}", agent_directory
1095
+ )
1096
+ example_tool.unlink(missing_ok=True)
1097
+
1098
+ # 5. Remote overlay (after cookiecutter, so its files are never rendered)
1099
+ if is_remote and remote_template_path:
1100
+ logging.debug(
1101
+ "Copying remote template files from %s to %s",
1102
+ remote_template_path,
1103
+ generated_project_dir,
1104
+ )
1105
+ cli_agent_dir = (
1106
+ cli_overrides.get("settings", {}).get("agent_directory")
1107
+ if cli_overrides
1108
+ else None
1109
+ )
1110
+ is_flat_structure = (cli_agent_dir == ".") or bool(
1111
+ remote_config and remote_config.get("is_flat_structure", False)
1112
+ )
1113
+ if is_flat_structure:
1114
+ copy_flat_structure_agent_files(
1115
+ remote_template_path, generated_project_dir, agent_directory
1116
+ )
1117
+ else:
1118
+ copy_files(
1119
+ remote_template_path,
1120
+ generated_project_dir,
1121
+ agent_name=agent_name,
1122
+ overwrite=True,
1123
+ skip_manifest=not overlay_is_project,
1124
+ guidance_filename=agent_guidance_filename,
1125
+ )
1126
+ logging.debug("Remote template files copied successfully")
1127
+
1128
+ # 6. Move to the final destination
1129
+ if in_folder:
1130
+ final_destination = destination_dir
1131
+ if generated_project_dir.exists():
1132
+ for item in generated_project_dir.iterdir():
1133
+ dest_item = final_destination / item.name
1134
+ if item.is_dir():
1135
+ if dest_item.exists():
1136
+ shutil.rmtree(dest_item)
1137
+ shutil.copytree(item, dest_item, dirs_exist_ok=True)
1138
+ else:
1139
+ shutil.copy2(item, dest_item)
1140
+ logging.debug("Project files copied to %s", final_destination)
1141
+ else:
1142
+ final_destination = destination_dir / project_name
1143
+ if generated_project_dir.exists():
1144
+ if final_destination.exists():
1145
+ shutil.rmtree(final_destination)
1146
+ shutil.copytree(generated_project_dir, final_destination, dirs_exist_ok=True)
1147
+ logging.debug("Project created at %s", final_destination)
1148
+
1149
+ if not final_destination.exists():
1150
+ raise FileNotFoundError(
1151
+ f"Final destination directory not found at {final_destination}"
1152
+ )
1153
+
1154
+ # 7. Conditional files, then runtime-specific Dockerfile and lock
1155
+ conditional_config = {
1156
+ "agent_name": agent_name,
1157
+ "deployment_target": deployment_target,
1158
+ "runtime": runtime,
1159
+ "cd": cd,
1160
+ "has_api_policy": has_api_policy,
1161
+ # In folder, an example_api.py still present is the project's own (4b).
1162
+ "has_example_api": has_example_api or in_folder,
1163
+ }
1164
+ apply_conditional_files(final_destination, conditional_config, agent_directory)
1165
+ _remove_unused_paths(final_destination)
1166
+ select_runtime_files(final_destination, runtime, project_name)
1167
+
1168
+ except Exception as e:
1169
+ logging.error(f"Failed to process template: {e!s}")
1170
+ raise
1171
+ finally:
1172
+ os.chdir(original_dir)
1173
+
1174
+ return final_destination
1175
+
1176
+
1177
+ # =============================================================================
1178
+ # File copying
1179
+ # =============================================================================
1180
+
1181
+ _TOOL_CACHES = frozenset({"__pycache__", ".ruff_cache", ".pytest_cache", ".mypy_cache", ".venv"})
1182
+
1183
+
1184
+ def copy_files(
1185
+ src: pathlib.Path,
1186
+ dst: pathlib.Path,
1187
+ agent_name: str | None = None,
1188
+ overwrite: bool = False,
1189
+ *,
1190
+ skip_manifest: bool = False,
1191
+ guidance_filename: str | None = None,
1192
+ ) -> None:
1193
+ """Copy files with configurable behavior for exclusions and overwrites.
1194
+
1195
+ Args:
1196
+ src: Source path
1197
+ dst: Destination path
1198
+ agent_name: Name of the agent (kept for call compatibility)
1199
+ overwrite: Whether to overwrite existing files (True) or skip them (False)
1200
+ guidance_filename: Write a root AGENTS.md under this name instead, so a
1201
+ template's guide replaces the base one whatever the project calls it.
1202
+ skip_manifest: Skip a root graph-agents-cli-manifest.yaml. Set when copying a
1203
+ fetched template, whose manifest describes the template rather than
1204
+ the project and has already been read for config.
1205
+ """
1206
+
1207
+ def should_skip(path: pathlib.Path) -> bool:
1208
+ """Symlinks are never followed (CWE-59): a template could point one at
1209
+ a sensitive host file and the project would end up with its contents."""
1210
+ if path.is_symlink():
1211
+ logging.warning(
1212
+ f"Skipping symlink in template source (symlinks are not allowed): {path}"
1213
+ )
1214
+ return True
1215
+ if path.suffix in [".pyc"]:
1216
+ return True
1217
+ if "__pycache__" in str(path) or path.name in _TOOL_CACHES:
1218
+ return True
1219
+ if ".git" in path.parts:
1220
+ return True
1221
+ if path.is_dir() and path.name == ".template":
1222
+ return True
1223
+ if skip_manifest and path.name == MANIFEST_FILENAME:
1224
+ return True
1225
+ return False
1226
+
1227
+ def log_windows_path_warning(path: pathlib.Path) -> None:
1228
+ if sys.platform == "win32":
1229
+ path_str = str(path.absolute())
1230
+ if len(path_str) >= 260:
1231
+ logging.error(
1232
+ f"Path length ({len(path_str)} chars) may exceed Windows limit. "
1233
+ "Try using a shorter output directory."
1234
+ )
1235
+
1236
+ if src.is_dir():
1237
+ if not dst.exists():
1238
+ try:
1239
+ dst.mkdir(parents=True)
1240
+ logging.debug("Created directory: %s", dst)
1241
+ except OSError as e:
1242
+ logging.error(f"Failed to create directory: {dst}")
1243
+ logging.error(f"Error: {e}")
1244
+ raise
1245
+ for item in src.iterdir():
1246
+ if should_skip(item):
1247
+ logging.debug("Skipping file/directory: %s", item)
1248
+ continue
1249
+
1250
+ # Root only: nested AGENTS.md files are the template's own docs.
1251
+ if guidance_filename and item.is_file() and item.name == "AGENTS.md":
1252
+ d = dst / guidance_filename
1253
+ else:
1254
+ d = dst / item.name
1255
+ if item.is_dir():
1256
+ copy_files(item, d, agent_name, overwrite)
1257
+ else:
1258
+ if overwrite or not d.exists():
1259
+ try:
1260
+ d.parent.mkdir(parents=True, exist_ok=True)
1261
+ logging.debug("Copying file: %s -> %s", item, d)
1262
+ shutil.copy2(item, d)
1263
+ except OSError:
1264
+ logging.error(f"Failed to copy: {item} -> {d}")
1265
+ log_windows_path_warning(d)
1266
+ raise
1267
+ else:
1268
+ logging.debug("Skipping existing file: %s", d)
1269
+ else:
1270
+ if not should_skip(src):
1271
+ if overwrite or not dst.exists():
1272
+ try:
1273
+ dst.parent.mkdir(parents=True, exist_ok=True)
1274
+ logging.debug("Copying file: %s -> %s", src, dst)
1275
+ shutil.copy2(src, dst)
1276
+ except OSError:
1277
+ logging.error(f"Failed to copy: {src} -> {dst}")
1278
+ log_windows_path_warning(dst)
1279
+ raise
1280
+
1281
+
1282
+ def _assert_path_within(candidate: pathlib.Path, root: pathlib.Path) -> None:
1283
+ """Raise ValueError if *candidate* is not contained within *root*.
1284
+
1285
+ Both paths are resolved to their real absolute forms before the check so
1286
+ that symbolic links and ``..`` components cannot bypass the boundary.
1287
+ """
1288
+ try:
1289
+ candidate.resolve().relative_to(root.resolve())
1290
+ except ValueError as e:
1291
+ raise ValueError(
1292
+ f"Security check failed: '{candidate}' would be written "
1293
+ f"outside the project directory '{root}'. "
1294
+ "Aborting to prevent path-traversal exploitation."
1295
+ ) from e
1296
+
1297
+
1298
+ def _skip_symlinks(directory: str, names: list[str]) -> set[str]:
1299
+ """Ignore callback for shutil.copytree to skip symlinks at every nesting level."""
1300
+ return {n for n in names if pathlib.Path(directory, n).is_symlink()}
1301
+
1302
+
1303
+ def copy_flat_structure_agent_files(
1304
+ src: pathlib.Path,
1305
+ dst: pathlib.Path,
1306
+ agent_directory: str,
1307
+ ) -> None:
1308
+ """Copy agent files from a flat structure template to the agent directory.
1309
+
1310
+ Python files (*.py) in the template root go to the agent directory; other
1311
+ files go to the project root. Symlinks are never followed and every
1312
+ destination is verified to be inside *dst* before any write.
1313
+ """
1314
+ agent_dst = dst / agent_directory
1315
+ _assert_path_within(agent_dst, dst)
1316
+ agent_dst.mkdir(parents=True, exist_ok=True)
1317
+
1318
+ agent_file_extensions = {".py"}
1319
+ skip_files = {"pyproject.toml", "uv.lock", "README.md", ".gitignore"}
1320
+
1321
+ for item in src.iterdir():
1322
+ if item.name.startswith(".") or item.name in skip_files:
1323
+ continue
1324
+ if item.name == "__pycache__":
1325
+ continue
1326
+ if item.is_symlink():
1327
+ logging.warning(
1328
+ f"Skipping symlink in flat-structure template source "
1329
+ f"(symlinks are not allowed): {item}"
1330
+ )
1331
+ continue
1332
+
1333
+ if item.is_file():
1334
+ if item.suffix in agent_file_extensions:
1335
+ dest_file = agent_dst / item.name
1336
+ _assert_path_within(dest_file, dst)
1337
+ logging.debug(
1338
+ "Flat structure: copying %s -> %s/%s", item.name, agent_directory, item.name
1339
+ )
1340
+ shutil.copy2(item, dest_file)
1341
+ else:
1342
+ dest_file = dst / item.name
1343
+ _assert_path_within(dest_file, dst)
1344
+ logging.debug("Flat structure: copying %s -> %s", item.name, item.name)
1345
+ shutil.copy2(item, dest_file)
1346
+ elif item.is_dir():
1347
+ dest_dir = dst / item.name
1348
+ _assert_path_within(dest_dir, dst)
1349
+ logging.debug("Flat structure: copying directory %s", item.name)
1350
+ if dest_dir.exists():
1351
+ shutil.rmtree(dest_dir)
1352
+ shutil.copytree(item, dest_dir, ignore=_skip_symlinks)