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,658 @@
1
+ # Copyright 2026 graph-agents-cli contributors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """The files an ``api`` command changes: planned as text, shown as a diff, written atomically.
16
+
17
+ Besides ``api-policy.yaml`` a change keeps the rest of the project in step:
18
+ the manifest (``api_policy.policy_file``, and in ``secrets.keys`` the bearer
19
+ ``token_env`` and, for ``auth: exchange``, ``TOKEN_EXCHANGE_CLIENT_SECRET``),
20
+ ``.env.example`` and the chart's ``values.yaml`` ``env`` map (each API's
21
+ ``base_url_env``; for ``auth: exchange``, ``TOKEN_EXCHANGE_URL`` and
22
+ ``TOKEN_EXCHANGE_CLIENT_ID`` once). Every edit keeps comments and formatting
23
+ (:mod:`graph_agents_cli.scaffold.utils.keyedit`); one that cannot be made
24
+ safely becomes a "Left for you" item instead.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import difflib
30
+ import os
31
+ import re
32
+ import tempfile
33
+ from collections.abc import Mapping
34
+ from dataclasses import dataclass, field
35
+ from pathlib import Path
36
+ from typing import Any
37
+
38
+ import click
39
+
40
+ from graph_agents_cli._api_policy import (
41
+ POLICY_FILENAME,
42
+ TOKEN_EXCHANGE_CLIENT_ID_ENV,
43
+ TOKEN_EXCHANGE_URL_ENV,
44
+ secret_envs,
45
+ summarize,
46
+ )
47
+ from graph_agents_cli._defaults import default_secret_keys
48
+ from graph_agents_cli._project import MANIFEST_FILENAME, ProjectConfig
49
+ from graph_agents_cli.scaffold.utils.keyedit import (
50
+ EditError,
51
+ YamlText,
52
+ env_insert,
53
+ env_names,
54
+ env_remove,
55
+ )
56
+
57
+ ENV_EXAMPLE = ".env.example"
58
+ ENV_SECTION = re.compile(r"^# --- Outbound APIs")
59
+ ENV_BEFORE = re.compile(r"^# Path of the policy file")
60
+ # The note a project without a policy carries in .env.example (older and current wording).
61
+ ENV_NO_POLICY_NOTES = [
62
+ "# No api-policy.yaml is declared, so every outbound API call is refused. Declare",
63
+ "# the APIs tools may call in api-policy.yaml (graph-agents-cli create --api-policy).",
64
+ "# the APIs tools may call with `graph-agents-cli api add` (see README.md).",
65
+ ]
66
+ # The current wording, as a fresh project without a policy has it.
67
+ ENV_NO_POLICY_NOTE = [ENV_NO_POLICY_NOTES[0], ENV_NO_POLICY_NOTES[2]]
68
+ # The line above an API's variables (written by `create`, `api add` and `peer add`, which
69
+ # names the peer: `# orders_agent (A2A peer orders; auth: exchange)`).
70
+ ENV_API_HEADER = re.compile(
71
+ r"^# ([a-z][a-z0-9_]{0,31}) \((?:A2A peer [a-z][a-z0-9_]{0,31}; )?auth: [a-z]+\)$"
72
+ )
73
+ LOCAL_BASE_URL = "http://localhost:9000"
74
+ CHART_BASE_URL = "http://CHANGE-ME"
75
+ VALUES_COMMENT = "Base URLs of the APIs in api-policy.yaml (tokens come from the Secret)."
76
+ # `auth: exchange`: the issuer's token endpoint and this agent's client there, once for every
77
+ # exchange API. `.env.example` lists them under this note (the secret commented out: it goes
78
+ # in .env and reaches the Secret through secrets.keys); `create` renders the same lines, with
79
+ # the optional settings (`EXCHANGE_ENV_OPTIONAL`) between the note and the secret.
80
+ EXCHANGE_ENV_NOTE = [
81
+ "# Token exchange (RFC 8693) for the auth: exchange APIs: the issuer's token endpoint",
82
+ "# and this agent's client there. Put the client secret in .env (it is in secrets.keys).",
83
+ ]
84
+ EXCHANGE_ENV_OPTIONAL = [
85
+ "# Optional: client authentication (or client_secret_post), the subject token type",
86
+ "# sent, the exchange's deadline, how long an exchanged token is reused (at most",
87
+ "# 300 s), how long a refusal or a failing issuer is left alone, tokens kept per",
88
+ "# process, and a plain-http token URL outside dev (a trusted in-cluster issuer only).",
89
+ "# TOKEN_EXCHANGE_CLIENT_AUTH=client_secret_basic",
90
+ "# TOKEN_EXCHANGE_SUBJECT_TOKEN_TYPE=urn:ietf:params:oauth:token-type:access_token",
91
+ "# TOKEN_EXCHANGE_TIMEOUT_MS=2000",
92
+ "# TOKEN_EXCHANGE_MAX_TTL_S=300",
93
+ "# TOKEN_EXCHANGE_FAILURE_TTL_S=10",
94
+ "# TOKEN_EXCHANGE_CACHE_MAX=10000",
95
+ "# TOKEN_EXCHANGE_ALLOW_HTTP=false",
96
+ ]
97
+ EXCHANGE_ENV_SECRET = "# TOKEN_EXCHANGE_CLIENT_SECRET="
98
+ EXCHANGE_VARIABLES = (TOKEN_EXCHANGE_URL_ENV, TOKEN_EXCHANGE_CLIENT_ID_ENV)
99
+ EXCHANGE_VALUES_COMMENT = (
100
+ "Token exchange (RFC 8693) for the auth: exchange APIs (the client secret comes from the "
101
+ "Secret)."
102
+ )
103
+
104
+
105
+ @dataclass
106
+ class FileChange:
107
+ """One file's text before and after (None: the file does not exist).
108
+
109
+ ``summary`` replaces the file's diff in the output (a copied spec is
110
+ hundreds of lines nobody needs to review line by line here).
111
+ """
112
+
113
+ path: str # relative to the project root
114
+ before: str | None
115
+ after: str | None
116
+ summary: str | None = None
117
+
118
+
119
+ @dataclass
120
+ class Plan:
121
+ """Every file a command changes, plus what it could not do and what to know."""
122
+
123
+ root: Path
124
+ changes: list[FileChange] = field(default_factory=list)
125
+ left_for_you: list[str] = field(default_factory=list)
126
+
127
+ def set_text(
128
+ self, path: str, before: str | None, after: str | None, *, summary: str | None = None
129
+ ) -> None:
130
+ for change in self.changes:
131
+ if change.path == path:
132
+ change.after = after
133
+ change.summary = summary or change.summary
134
+ return
135
+ if before != after:
136
+ self.changes.append(FileChange(path, before, after, summary))
137
+
138
+ def current(self, path: str) -> str | None:
139
+ """``path``'s text after the changes planned so far (its stored text when none is).
140
+
141
+ Every edit starts from this text, so several edits of one file in one plan
142
+ (``system apply`` adds many peers at once) build on each other.
143
+ """
144
+ for change in self.changes:
145
+ if change.path == path:
146
+ return change.after
147
+ return read_text(self.root / path)
148
+
149
+ @property
150
+ def effective(self) -> list[FileChange]:
151
+ return [c for c in self.changes if c.before != c.after]
152
+
153
+ def summaries(self) -> list[str]:
154
+ """One line per summarised change (shown after the diff of the others)."""
155
+ return [f"{c.path}: {c.summary}" for c in self.effective if c.summary]
156
+
157
+ def diff(self) -> str:
158
+ """The unified diff of every change that is not summarised."""
159
+ chunks = []
160
+ for change in self.effective:
161
+ if change.summary:
162
+ continue
163
+ before = (change.before or "").splitlines(keepends=True)
164
+ after = (change.after or "").splitlines(keepends=True)
165
+ chunks.extend(
166
+ _terminated(
167
+ difflib.unified_diff(
168
+ before,
169
+ after,
170
+ fromfile="/dev/null" if change.before is None else f"a/{change.path}",
171
+ tofile="/dev/null" if change.after is None else f"b/{change.path}",
172
+ )
173
+ )
174
+ )
175
+ return "".join(chunks)
176
+
177
+ def write(self) -> None:
178
+ """Write every change atomically (a temporary file renamed over the old one).
179
+
180
+ An existing file keeps its mode. A new one gets the mode any new file
181
+ gets (0666 less the umask), not the 0600 of the temporary file: the
182
+ images copy the project's files as they are and run as uid 1000, so a
183
+ private api-policy.yaml would be unreadable there and the agent would
184
+ refuse every call.
185
+ """
186
+ for change in self.effective:
187
+ target = self.root / change.path
188
+ if change.after is None:
189
+ target.unlink(missing_ok=True)
190
+ continue
191
+ target.parent.mkdir(parents=True, exist_ok=True)
192
+ mode = target.stat().st_mode & 0o777 if target.exists() else _new_file_mode()
193
+ handle, temporary = tempfile.mkstemp(
194
+ prefix=f".{target.name}.", suffix=".tmp", dir=target.parent
195
+ )
196
+ try:
197
+ with os.fdopen(handle, "w", encoding="utf-8", newline="") as stream:
198
+ stream.write(change.after)
199
+ os.chmod(temporary, mode)
200
+ os.replace(temporary, target)
201
+ except BaseException:
202
+ Path(temporary).unlink(missing_ok=True)
203
+ raise
204
+
205
+
206
+ def _new_file_mode() -> int:
207
+ """The mode ``open()`` gives a new file: 0666 less the process umask."""
208
+ umask = os.umask(0o022) # reading the umask means setting it; restored at once
209
+ os.umask(umask)
210
+ return 0o666 & ~umask
211
+
212
+
213
+ def _terminated(lines: Any) -> list[str]:
214
+ out = []
215
+ for line in lines:
216
+ out.append(line if line.endswith("\n") else line + "\n\\n")
217
+ return out
218
+
219
+
220
+ def print_diff(text: str) -> None:
221
+ for line in text.splitlines():
222
+ if line.startswith(("+++", "---")):
223
+ click.secho(line, bold=True)
224
+ elif line.startswith("+"):
225
+ click.secho(line, fg="green")
226
+ elif line.startswith("-"):
227
+ click.secho(line, fg="red")
228
+ elif line.startswith("@@"):
229
+ click.secho(line, fg="cyan")
230
+ else:
231
+ click.echo(line)
232
+
233
+
234
+ def read_text(path: Path) -> str | None:
235
+ """The file's text as stored: line endings are kept (a CRLF file stays CRLF)."""
236
+ try:
237
+ with path.open(encoding="utf-8", newline="") as stream:
238
+ return stream.read()
239
+ except FileNotFoundError:
240
+ return None
241
+
242
+
243
+ # ---------------------------------------------------------------------------
244
+ # The manifest
245
+ # ---------------------------------------------------------------------------
246
+
247
+
248
+ def sync_manifest(
249
+ plan: Plan,
250
+ config: ProjectConfig,
251
+ *,
252
+ document: dict[str, Any] | None,
253
+ previous: dict[str, Any] | None,
254
+ ) -> None:
255
+ """``api_policy`` present exactly when there is a policy; bearer tokens in ``secrets.keys``.
256
+
257
+ A token variable goes when the last API using it goes, unless it is one of
258
+ the project's default keys (the provider key, API_KEY, ...).
259
+ """
260
+ stored = read_text(plan.root / MANIFEST_FILENAME)
261
+ before = plan.current(MANIFEST_FILENAME)
262
+ if before is None:
263
+ plan.left_for_you.append(f"{MANIFEST_FILENAME} not found; nothing recorded there")
264
+ return
265
+ try:
266
+ manifest = YamlText(before)
267
+ except EditError as exc:
268
+ plan.left_for_you.append(f"{MANIFEST_FILENAME} could not be edited safely ({exc})")
269
+ return
270
+ new_tokens = secret_envs(summarize(document)) if document else []
271
+ old_tokens = secret_envs(summarize(previous)) if previous else []
272
+ defaults = default_secret_keys(
273
+ config.model_provider, config.runtime, auth_policy=config.auth_policy
274
+ )
275
+ steps: list[tuple[str, Any]] = []
276
+ if document is not None:
277
+ if manifest.get(("api_policy", "policy_file")) != POLICY_FILENAME:
278
+ steps.append(("policy", {"policy_file": POLICY_FILENAME}))
279
+ elif manifest.get(("api_policy",)) is not None:
280
+ steps.append(("no-policy", None))
281
+ keys = manifest.get(("secrets", "keys"))
282
+ if isinstance(keys, list):
283
+ for token in new_tokens:
284
+ if token not in keys:
285
+ steps.append(("add-key", token))
286
+ for token in old_tokens:
287
+ if token not in new_tokens and token not in defaults and token in keys:
288
+ steps.append(("remove-key", token))
289
+ elif new_tokens:
290
+ steps.append(("keys", [*defaults, *(t for t in new_tokens if t not in defaults)]))
291
+ for kind, value in steps:
292
+ try:
293
+ if kind == "policy":
294
+ manifest.set(("api_policy",), value, after="secrets", block=True)
295
+ elif kind == "no-policy":
296
+ manifest.delete(("api_policy",))
297
+ elif kind == "add-key":
298
+ manifest.append(("secrets", "keys"), value)
299
+ elif kind == "remove-key":
300
+ current = manifest.get(("secrets", "keys"))
301
+ manifest.remove_item(("secrets", "keys"), current.index(value))
302
+ else:
303
+ manifest.set(("secrets", "keys"), value)
304
+ except EditError as exc:
305
+ plan.left_for_you.append(_manifest_todo(kind, value, exc))
306
+ plan.set_text(MANIFEST_FILENAME, stored, manifest.text)
307
+
308
+
309
+ def _manifest_todo(kind: str, value: Any, exc: EditError) -> str:
310
+ what = {
311
+ "policy": f"set api_policy: {{policy_file: {POLICY_FILENAME}}}",
312
+ "no-policy": "remove the api_policy block",
313
+ "add-key": f"add {value} to secrets.keys",
314
+ "remove-key": f"remove {value} from secrets.keys",
315
+ "keys": f"set secrets.keys to {value}",
316
+ }[kind]
317
+ return f"{MANIFEST_FILENAME}: {what} (not edited automatically: {exc})"
318
+
319
+
320
+ # ---------------------------------------------------------------------------
321
+ # .env.example
322
+ # ---------------------------------------------------------------------------
323
+
324
+
325
+ def _env_comment(name: str, api: dict[str, Any], peer: str | None = None) -> str:
326
+ if peer is not None:
327
+ return f"# {name} (A2A peer {peer}; auth: {api['auth']})"
328
+ return f"# {name} (auth: {api['auth']})"
329
+
330
+
331
+ def env_example_add(
332
+ plan: Plan,
333
+ name: str,
334
+ api: dict[str, Any],
335
+ *,
336
+ peer: str | None = None,
337
+ base_url: str = LOCAL_BASE_URL,
338
+ ) -> None:
339
+ """Document an API's variables in ``.env.example`` (``peer``: the A2A peer it is)."""
340
+ stored = read_text(plan.root / ENV_EXAMPLE)
341
+ before = plan.current(ENV_EXAMPLE)
342
+ if before is None:
343
+ return
344
+ variables = [(api["base_url_env"], base_url)]
345
+ if api["auth"] == "bearer":
346
+ variables.append((api["token_env"], ""))
347
+ exchange = api["auth"] == "exchange"
348
+ try:
349
+ present = env_names(before)
350
+ missing = [(var, value) for var, value in variables if var not in present]
351
+ after = before
352
+ if missing:
353
+ lines = [
354
+ _env_comment(name, api, peer),
355
+ *(f"{var}={value}" for var, value in missing),
356
+ ]
357
+ after = env_insert(
358
+ after, lines, section=ENV_SECTION, before=ENV_BEFORE, drop=ENV_NO_POLICY_NOTES
359
+ )
360
+ if exchange and not set(EXCHANGE_VARIABLES) & present:
361
+ lines = [
362
+ *EXCHANGE_ENV_NOTE,
363
+ EXCHANGE_ENV_SECRET,
364
+ *(f"{v}=" for v in EXCHANGE_VARIABLES),
365
+ ]
366
+ after = env_insert(after, lines, section=ENV_SECTION, before=ENV_BEFORE)
367
+ except (EditError, ValueError) as exc:
368
+ wanted = [var for var, _ in variables] + (list(EXCHANGE_VARIABLES) if exchange else [])
369
+ plan.left_for_you.append(f"{ENV_EXAMPLE}: document {', '.join(wanted)} (not edited: {exc})")
370
+ return
371
+ plan.set_text(ENV_EXAMPLE, stored, after)
372
+
373
+
374
+ def _api_variables(api: Mapping[str, Any]) -> set[str]:
375
+ """The variables an API puts in ``.env.example``: its base URL, and its token under bearer."""
376
+ names = {str(api["base_url_env"])}
377
+ if api["auth"] == "bearer":
378
+ names.add(str(api["token_env"]))
379
+ if api["auth"] == "exchange":
380
+ names.update(EXCHANGE_VARIABLES)
381
+ return names
382
+
383
+
384
+ def env_example_remove(
385
+ plan: Plan,
386
+ name: str,
387
+ api: dict[str, Any],
388
+ keep: set[str],
389
+ remaining: Mapping[str, Mapping[str, Any]],
390
+ ) -> None:
391
+ """Drop the variables of API ``name`` that no other API uses (``keep``).
392
+
393
+ ``remaining``: the APIs left, by name. The header line of an API that is
394
+ gone goes once none of its variables remain; when a variable another API
395
+ shares keeps it, it is renamed to that API. Without any API left the
396
+ fresh project's "no policy" note comes back.
397
+ """
398
+ stored = read_text(plan.root / ENV_EXAMPLE)
399
+ before = plan.current(ENV_EXAMPLE)
400
+ if before is None:
401
+ return
402
+ names = [api["base_url_env"]] + ([api["token_env"]] if api["auth"] == "bearer" else [])
403
+ names = [n for n in names if n not in keep]
404
+ no_exchange = not any(other.get("auth") == "exchange" for other in remaining.values())
405
+ try:
406
+ names = [n for n in names if n in env_names(before)]
407
+ after = before
408
+ if names:
409
+ headers = [_env_comment(name, api), *_peer_comments(before, name)]
410
+ after = env_remove(before, names, comments=headers)
411
+ exchange_names = [n for n in EXCHANGE_VARIABLES if n in env_names(after)]
412
+ if no_exchange and exchange_names:
413
+ after = env_remove(
414
+ after,
415
+ exchange_names,
416
+ comments=[*EXCHANGE_ENV_NOTE, *EXCHANGE_ENV_OPTIONAL, EXCHANGE_ENV_SECRET],
417
+ )
418
+ after = _fix_orphan_headers(after, remaining)
419
+ if not remaining:
420
+ after = _restore_no_policy_note(after)
421
+ except (EditError, ValueError) as exc:
422
+ plan.left_for_you.append(f"{ENV_EXAMPLE}: remove {', '.join(names)} (not edited: {exc})")
423
+ return
424
+ plan.set_text(ENV_EXAMPLE, stored, after)
425
+
426
+
427
+ def _peer_comments(text: str, name: str) -> list[str]:
428
+ """The header lines ``peer add`` wrote for API ``name`` (whichever peer and auth)."""
429
+ found = []
430
+ for line in text.splitlines():
431
+ stripped = line.strip()
432
+ match = ENV_API_HEADER.match(stripped)
433
+ if match and match.group(1) == name and "(A2A peer " in stripped:
434
+ found.append(stripped)
435
+ return found
436
+
437
+
438
+ def _fix_orphan_headers(text: str, remaining: Mapping[str, Mapping[str, Any]]) -> str:
439
+ """``text`` with the headers of APIs that are gone dropped or handed on.
440
+
441
+ A header that no longer heads a variable goes. One that still heads a
442
+ variable a remaining API uses names that API (the first in the policy)
443
+ instead, so the file never credits a variable to an API that is gone.
444
+ """
445
+ lines = text.splitlines(keepends=True)
446
+ kept = []
447
+ for index, line in enumerate(lines):
448
+ stripped = line.rstrip("\r\n")
449
+ match = ENV_API_HEADER.match(stripped)
450
+ if match and match.group(1) not in remaining:
451
+ headed = _headed_variables(lines, index)
452
+ if not headed:
453
+ continue
454
+ owner = next((n for n, a in remaining.items() if headed & _api_variables(a)), None)
455
+ if owner is not None:
456
+ line = _env_comment(owner, dict(remaining[owner])) + line[len(stripped) :]
457
+ kept.append(line)
458
+ return "".join(kept)
459
+
460
+
461
+ def _headed_variables(lines: list[str], index: int) -> set[str]:
462
+ """The variables that follow line ``index`` before a blank line or another header."""
463
+ names: set[str] = set()
464
+ for line in lines[index + 1 :]:
465
+ stripped = line.strip()
466
+ if not stripped or stripped.startswith("# ---") or ENV_API_HEADER.match(stripped):
467
+ break
468
+ names |= env_names(line)
469
+ return names
470
+
471
+
472
+ def _restore_no_policy_note(text: str) -> str:
473
+ """The Outbound APIs section says again that no policy is declared (when it has none)."""
474
+ if not any(ENV_SECTION.match(line) for line in text.splitlines()):
475
+ return text
476
+ present = {line.strip() for line in text.splitlines()}
477
+ if ENV_NO_POLICY_NOTE[0] in present:
478
+ return text
479
+ return env_insert(text, ENV_NO_POLICY_NOTE, section=ENV_SECTION, before=ENV_BEFORE)
480
+
481
+
482
+ # ---------------------------------------------------------------------------
483
+ # The chart's values
484
+ # ---------------------------------------------------------------------------
485
+
486
+
487
+ def chart_values_files(root: Path, config: ProjectConfig) -> list[Path]:
488
+ """``values.yaml`` then ``values-<env>.yaml`` of the project's chart (empty without one)."""
489
+ if config.deployment_target != "kubernetes":
490
+ return []
491
+ chart = root / "deployment" / "helm" / (config.project_name or "")
492
+ if not (chart / "values.yaml").is_file():
493
+ return []
494
+ return [chart / "values.yaml", *sorted(chart.glob("values-*.yaml"))]
495
+
496
+
497
+ def values_add(plan: Plan, config: ProjectConfig, document: dict[str, Any], name: str) -> None:
498
+ files = chart_values_files(plan.root, config)
499
+ if not files:
500
+ return
501
+ path = files[0]
502
+ rel = path.relative_to(plan.root).as_posix()
503
+ before = plan.current(rel)
504
+ api = document["apis"][name]
505
+ variable = api["base_url_env"]
506
+ try:
507
+ values = YamlText(before or "")
508
+ env = values.get(("env",))
509
+ if not isinstance(env, dict):
510
+ raise EditError("values.yaml has no env: mapping")
511
+ if variable in env:
512
+ return
513
+ others = [
514
+ str(other["base_url_env"])
515
+ for other_name, other in document["apis"].items()
516
+ if other_name != name and str(other["base_url_env"]) in env
517
+ ]
518
+ if others:
519
+ values.set(("env", variable), CHART_BASE_URL, after=others[-1])
520
+ else:
521
+ comment = None if VALUES_COMMENT in (before or "") else VALUES_COMMENT
522
+ values.set(("env", variable), CHART_BASE_URL, after="AUTH_ADMIN_ROLES", comment=comment)
523
+ except EditError as exc:
524
+ plan.left_for_you.append(
525
+ f"{rel}: add {variable}: <the API's base URL> under env: (not edited: {exc})"
526
+ )
527
+ return
528
+ plan.set_text(rel, read_text(path), values.text)
529
+
530
+
531
+ def values_exchange_add(
532
+ plan: Plan, config: ProjectConfig, document: dict[str, Any], name: str
533
+ ) -> None:
534
+ """``TOKEN_EXCHANGE_URL`` (a placeholder) and ``TOKEN_EXCHANGE_CLIENT_ID`` (the project's
535
+ name) in the chart's ``values.yaml`` ``env``, once, after the new exchange API's base URL."""
536
+ files = chart_values_files(plan.root, config)
537
+ if not files or document["apis"][name]["auth"] != "exchange":
538
+ return
539
+ path = files[0]
540
+ rel = path.relative_to(plan.root).as_posix()
541
+ before = plan.current(rel)
542
+ try:
543
+ values = YamlText(before or "")
544
+ env = values.get(("env",))
545
+ if not isinstance(env, dict):
546
+ raise EditError("values.yaml has no env: mapping")
547
+ if TOKEN_EXCHANGE_URL_ENV in env and TOKEN_EXCHANGE_CLIENT_ID_ENV in env:
548
+ return
549
+ after = str(document["apis"][name]["base_url_env"])
550
+ if TOKEN_EXCHANGE_URL_ENV not in env:
551
+ values.set(
552
+ ("env", TOKEN_EXCHANGE_URL_ENV),
553
+ CHART_BASE_URL,
554
+ after=after if after in env else None,
555
+ comment=EXCHANGE_VALUES_COMMENT,
556
+ )
557
+ if TOKEN_EXCHANGE_CLIENT_ID_ENV not in values.get(("env",)):
558
+ values.set(
559
+ ("env", TOKEN_EXCHANGE_CLIENT_ID_ENV),
560
+ config.project_name or "CHANGE-ME",
561
+ after=TOKEN_EXCHANGE_URL_ENV,
562
+ )
563
+ except EditError as exc:
564
+ plan.left_for_you.append(
565
+ f"{rel}: add {TOKEN_EXCHANGE_URL_ENV}: <the issuer's token endpoint> and "
566
+ f"{TOKEN_EXCHANGE_CLIENT_ID_ENV}: <this agent's client id> under env: (not edited: "
567
+ f"{exc})"
568
+ )
569
+ return
570
+ plan.set_text(rel, read_text(path), values.text)
571
+
572
+
573
+ def values_exchange_remove(plan: Plan, config: ProjectConfig) -> None:
574
+ """Drop ``TOKEN_EXCHANGE_URL`` and ``TOKEN_EXCHANGE_CLIENT_ID`` from the chart's env maps
575
+ (the last exchange API went), with their comment line."""
576
+ for path in chart_values_files(plan.root, config):
577
+ rel = path.relative_to(plan.root).as_posix()
578
+ stored = read_text(path)
579
+ before = plan.current(rel)
580
+ try:
581
+ values = YamlText(before or "")
582
+ env = values.get(("env",))
583
+ if not isinstance(env, dict) or not set(EXCHANGE_VARIABLES) & set(env):
584
+ continue
585
+ for variable in EXCHANGE_VARIABLES:
586
+ if variable in values.get(("env",)):
587
+ values.delete(("env", variable))
588
+ text = values.text
589
+ lines = text.splitlines(keepends=True)
590
+ kept = [line for line in lines if line.strip() != f"# {EXCHANGE_VALUES_COMMENT}"]
591
+ if len(kept) != len(lines) and YamlText("".join(kept)).data == values.data:
592
+ text = "".join(kept)
593
+ except EditError as exc:
594
+ plan.left_for_you.append(
595
+ f"{rel}: remove {', '.join(EXCHANGE_VARIABLES)} from env: (not edited: {exc})"
596
+ )
597
+ continue
598
+ plan.set_text(rel, stored, text)
599
+
600
+
601
+ def values_env_set(
602
+ plan: Plan, config: ProjectConfig, variable: str, template: str | Mapping[str, str]
603
+ ) -> list[str]:
604
+ """Set ``variable`` in each ``values-<env>.yaml``'s ``env`` to ``template`` with ``{env}``
605
+ filled, or to the value a mapping gives each environment (the files of the others are
606
+ left alone); the environments it was set for."""
607
+ done = []
608
+ for path in chart_values_files(plan.root, config)[1:]:
609
+ environment = path.stem.removeprefix("values-")
610
+ if isinstance(template, Mapping):
611
+ if environment not in template:
612
+ continue
613
+ value = template[environment]
614
+ else:
615
+ value = template.replace("{env}", environment)
616
+ rel = path.relative_to(plan.root).as_posix()
617
+ before = plan.current(rel)
618
+ try:
619
+ values = YamlText(before or "")
620
+ env = values.get(("env",))
621
+ if env is None:
622
+ values.set(("env",), {variable: value})
623
+ elif not isinstance(env, dict):
624
+ raise EditError(f"{rel} has an env: that is not a mapping")
625
+ elif env.get(variable) != value:
626
+ values.set(("env", variable), value)
627
+ except EditError as exc:
628
+ plan.left_for_you.append(f"{rel}: set env.{variable}: {value} (not edited: {exc})")
629
+ continue
630
+ plan.set_text(rel, read_text(path), values.text)
631
+ done.append(environment)
632
+ return done
633
+
634
+
635
+ def values_remove(plan: Plan, config: ProjectConfig, variable: str, remaining: set[str]) -> None:
636
+ """Drop ``variable`` from the chart's env maps; ``remaining``: the other APIs' base URLs.
637
+
638
+ When no API base URL is left in ``values.yaml``, its heading comment goes too.
639
+ """
640
+ for path in chart_values_files(plan.root, config):
641
+ rel = path.relative_to(plan.root).as_posix()
642
+ before = plan.current(rel)
643
+ try:
644
+ values = YamlText(before or "")
645
+ env = values.get(("env",))
646
+ if not isinstance(env, dict) or variable not in env:
647
+ continue
648
+ values.delete(("env", variable))
649
+ text = values.text
650
+ if not remaining & set(env):
651
+ lines = text.splitlines(keepends=True)
652
+ kept = [line for line in lines if line.strip() != f"# {VALUES_COMMENT}"]
653
+ if len(kept) == len(lines) - 1 and YamlText("".join(kept)).data == values.data:
654
+ text = "".join(kept)
655
+ except EditError as exc:
656
+ plan.left_for_you.append(f"{rel}: remove {variable} from env: (not edited: {exc})")
657
+ continue
658
+ plan.set_text(rel, read_text(path), text)