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,234 @@
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
+ """Deployment mode derivation (direct, argocd, helm-push) and kube context resolution.
15
+
16
+ The kube context decides which cluster every kubectl and helm call reaches.
17
+ Taking it silently from the kubeconfig's current context means a developer
18
+ whose terminal points at production can deploy there by accident (and a prod
19
+ rollout can land on a laptop cluster). So only ``dev`` may use the current
20
+ context without asking: every other environment needs the context recorded in
21
+ the manifest (``environments.<env>.context``) or passed with ``--context``, or
22
+ an explicit confirmation (a prompt at a terminal, ``--yes`` otherwise). The
23
+ resolved context and its API server are printed before anything happens.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import sys
29
+ from dataclasses import dataclass
30
+
31
+ import click
32
+
33
+ from graph_agents_cli._output import Console
34
+ from graph_agents_cli.deploy import _kube
35
+ from graph_agents_cli.deploy._config import DeploySettings
36
+ from graph_agents_cli.deploy._kube import ConfigError, Refused
37
+
38
+ LOCAL_LOAD = "local-load"
39
+ REGISTRY = "registry"
40
+ HELM_PUSH = "helm-push"
41
+ ARGOCD = "argocd"
42
+
43
+ DIRECT_MODES = (LOCAL_LOAD, REGISTRY)
44
+
45
+ # Environments with policy gates (auth policy implemented, helm-push CI-only deploys).
46
+ PROTECTED_ENVS = ("staging", "prod")
47
+ # The one environment that may fall back to `.env` and the current kube context.
48
+ DEV_ENV = "dev"
49
+
50
+ # Kube context names that suggest a single-node dev cluster (kind, k3s, minikube, ...).
51
+ DEV_CONTEXT_PREFIXES = ("kind-", "k3d-", "minikube", "k3s")
52
+ DEV_CONTEXT_NAMES = ("minikube", "docker-desktop", "rancher-desktop", "orbstack", "k3s")
53
+
54
+ FLAG = "flag"
55
+ MANIFEST = "manifest"
56
+ CURRENT = "current"
57
+ NONE = "none"
58
+
59
+
60
+ def is_dev_cluster(context: str | None) -> bool:
61
+ """Whether a context *name* looks like a dev cluster (a hint; see ``local_load.detect``)."""
62
+ if not context:
63
+ return False
64
+ name = context.strip()
65
+ return name in DEV_CONTEXT_NAMES or name.startswith(DEV_CONTEXT_PREFIXES)
66
+
67
+
68
+ def is_dev_env(env: str) -> bool:
69
+ return env == DEV_ENV
70
+
71
+
72
+ @dataclass(frozen=True)
73
+ class ResolvedContext:
74
+ """The kube context for one command and where it came from."""
75
+
76
+ name: str | None
77
+ source: str # FLAG, MANIFEST, CURRENT or NONE
78
+
79
+ @property
80
+ def explicit(self) -> bool:
81
+ return self.source in (FLAG, MANIFEST)
82
+
83
+ def describe(self, env: str) -> str:
84
+ return {
85
+ FLAG: "from --context",
86
+ MANIFEST: f"from environments.{env}.context",
87
+ CURRENT: "the kubeconfig's current context",
88
+ NONE: "no context recorded and no current context",
89
+ }[self.source]
90
+
91
+
92
+ def resolve(settings: DeploySettings, env: str, override: str | None = None) -> ResolvedContext:
93
+ """``--context``, else the manifest's ``environments.<env>.context``, else the current one."""
94
+ if override and override.strip():
95
+ return ResolvedContext(override.strip(), FLAG)
96
+ recorded = settings.environment(env).get("context")
97
+ if recorded:
98
+ return ResolvedContext(recorded, MANIFEST)
99
+ current = _kube.current_context()
100
+ if current:
101
+ return ResolvedContext(current, CURRENT)
102
+ return ResolvedContext(None, NONE)
103
+
104
+
105
+ def resolve_context(settings: DeploySettings, env: str) -> str | None:
106
+ """The environment's recorded context, else the kubeconfig's current context."""
107
+ return resolve(settings, env).name
108
+
109
+
110
+ def _interactive() -> bool:
111
+ try:
112
+ return sys.stdin.isatty()
113
+ except (AttributeError, ValueError):
114
+ return False
115
+
116
+
117
+ def announce(
118
+ env: str, resolved: ResolvedContext, *, console: Console, show_server: bool = True
119
+ ) -> None:
120
+ """Print the context (and its API server) every cluster-touching command will use."""
121
+ server = _kube.server_url(resolved.name) if show_server and resolved.name else None
122
+ where = f"; server {server}" if server else ""
123
+ console.print(
124
+ f"Kube context: {resolved.name or '(none)'} ({resolved.describe(env)}{where})",
125
+ highlight=False,
126
+ markup=False,
127
+ )
128
+
129
+
130
+ def require_known_context(
131
+ env: str, resolved: ResolvedContext, *, dry_run: bool, console: Console
132
+ ) -> None:
133
+ """Exit 3 when an explicit context (``--context`` or the manifest) is not in the kubeconfig.
134
+
135
+ A typo is a configuration error, reported the same way by every command
136
+ (read-only ones included), not a kubectl failure (exit 2) that a gate script
137
+ could mistake for an unreachable cluster. ``--dry-run`` only reports it.
138
+ """
139
+ if not (resolved.explicit and resolved.name):
140
+ return
141
+ names = _kube.context_names()
142
+ if not names or resolved.name in names:
143
+ return
144
+ message = (
145
+ f"Kube context {resolved.name!r} ({resolved.describe(env)}) is not in the "
146
+ f"kubeconfig. Known contexts: {', '.join(sorted(names)[:10])}."
147
+ )
148
+ if not dry_run:
149
+ raise ConfigError(message)
150
+ console.print(f" [dry-run] {message}", style="yellow", markup=False)
151
+
152
+
153
+ def confirm(
154
+ env: str,
155
+ resolved: ResolvedContext,
156
+ *,
157
+ yes: bool,
158
+ dry_run: bool,
159
+ console: Console,
160
+ action: str,
161
+ ) -> None:
162
+ """Refuse or confirm an implicit context for any environment other than ``dev``.
163
+
164
+ ``dev`` and an explicit context (``--context`` or the manifest) pass, once
165
+ the kubeconfig is known to hold that context (checked here, so a typo stops
166
+ the command before anything is built). For another environment an implicit
167
+ current context needs ``--yes`` or a ``y`` at the prompt; with no context at
168
+ all it is a configuration error. ``--dry-run`` never prompts (nothing is
169
+ changed) and only reports.
170
+ """
171
+ require_known_context(env, resolved, dry_run=dry_run, console=console)
172
+ if is_dev_env(env) or resolved.explicit:
173
+ return
174
+ how = (
175
+ f"Record it as environments.{env}.context in graph-agents-cli-manifest.yaml or pass "
176
+ "--context <name>"
177
+ )
178
+ if resolved.name is None:
179
+ if dry_run:
180
+ console.print(
181
+ f" [dry-run] no kube context for {env}; the real run stops here. {how}.",
182
+ style="yellow",
183
+ markup=False,
184
+ )
185
+ return
186
+ raise ConfigError(f"No kube context for {env}. {how}.")
187
+ if dry_run:
188
+ console.print(
189
+ f" [dry-run] {env} has no recorded context; the real run asks to confirm "
190
+ f"{resolved.name!r} (or needs --yes).",
191
+ style="yellow",
192
+ markup=False,
193
+ )
194
+ return
195
+ if yes:
196
+ console.print(
197
+ f" Using the current context {resolved.name!r} for {env} (--yes).",
198
+ style="yellow",
199
+ markup=False,
200
+ )
201
+ return
202
+ if not _interactive():
203
+ raise Refused(
204
+ f"Refusing to {action} {env} on the kubeconfig's current context {resolved.name!r} "
205
+ f"without confirmation.\n {how}, or pass --yes to accept the current context."
206
+ )
207
+ # Only the first letter changes: `str.capitalize` would lowercase "Secret".
208
+ if not click.confirm(
209
+ f"{action[:1].upper()}{action[1:]} {env} on context {resolved.name!r}?", default=False
210
+ ):
211
+ raise Refused(f"Aborted: {env} was not changed. {how}.")
212
+
213
+
214
+ def derive_mode(cd: str, context: str | None, *, local: bool | None = None) -> str:
215
+ """The deploy mode for ``cd``; ``local`` (when known) overrides the context-name guess."""
216
+ if cd == "skip":
217
+ is_local = is_dev_cluster(context) if local is None else local
218
+ return LOCAL_LOAD if is_local else REGISTRY
219
+ if cd == HELM_PUSH:
220
+ return HELM_PUSH
221
+ if cd == ARGOCD:
222
+ return ARGOCD
223
+ raise ConfigError(
224
+ f"Unknown cd value {cd!r} in the manifest (expected argocd, helm-push or skip)."
225
+ )
226
+
227
+
228
+ def describe(mode: str) -> str:
229
+ return {
230
+ LOCAL_LOAD: "direct, local-load (build and load the image into the local cluster)",
231
+ REGISTRY: "direct, registry (build, push, and helm upgrade)",
232
+ HELM_PUSH: "helm-push (CI builds and pushes; deploy runs helm)",
233
+ ARGOCD: "argocd (deploy writes desired state; Argo CD reconciles from main)",
234
+ }.get(mode, mode)
@@ -0,0 +1,370 @@
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
+ """Settings checks shared by ``deploy`` and ``infra check``: what the pods would see.
15
+
16
+ A pod's environment is the app Secret (``envFrom``), then the chart's ConfigMap
17
+ (``env`` in the merged values, also ``envFrom`` and listed after the Secret, so
18
+ it wins, even with an empty value), then the variables the chart sets itself.
19
+ These checks read that environment the same way, from the merged chart values
20
+ and the names of the keys the Secret holds (values are never needed):
21
+
22
+ - ``env_placeholders``: a scaffold placeholder (``CHANGE-ME``) still in the
23
+ chart env, for example an API base URL: the pods would call it.
24
+ - ``jwt_findings``: the ``jwt`` policy without a verification key, issuer or
25
+ audience. Outside ``APP_ENV=dev`` the pod refuses to start without them (after
26
+ the image was built and the rollout waited); in dev it starts, reports Ready
27
+ and answers every request with 503.
28
+ - ``dsn_without_tls``: a database connection string for an external database
29
+ that does not require TLS.
30
+ - ``peer_findings``: the other agents the project calls (``protocol: a2a`` APIs):
31
+ a peer reached with a credential over plain http to a host that is not
32
+ cluster-internal, an ``auth: exchange`` API without the issuer's token URL
33
+ or this agent's client id (or a plain-http token URL), and what is missing
34
+ from ``secrets.keys``.
35
+ """
36
+
37
+ from __future__ import annotations
38
+
39
+ import ipaddress
40
+ import re
41
+ import urllib.parse
42
+ from collections.abc import Mapping, Set
43
+ from dataclasses import dataclass, field
44
+ from typing import Any
45
+
46
+ from graph_agents_cli._defaults import normalize_auth_policy
47
+ from graph_agents_cli.deploy import _image, _modes
48
+ from graph_agents_cli.deploy._config import DeploySettings
49
+
50
+ JWT_KEY_SOURCES = ("AUTH_JWT_JWKS_URL", "AUTH_JWT_PUBLIC_KEY")
51
+ JWT_REQUIRED_OUTSIDE_DEV = ("AUTH_JWT_ISSUER", "AUTH_JWT_AUDIENCE")
52
+ _DEFAULT_JWT_ALGORITHMS = "RS256,ES256"
53
+ _HMAC = frozenset({"HS256", "HS384", "HS512"})
54
+ # libpq modes that refuse a connection without TLS.
55
+ TLS_SSLMODES = frozenset({"require", "verify-ca", "verify-full"})
56
+ # URL (`?sslmode=x`) or keyword/value form, where libpq allows spaces around `=`
57
+ # and a quoted value (`sslmode = 'verify-full'`).
58
+ _SSLMODE = re.compile(r"(?:^|[?&\s])sslmode\s*=\s*'?([A-Za-z-]+)")
59
+
60
+
61
+ def chart_env(values: Mapping[str, Any]) -> dict[str, Any]:
62
+ env = values.get("env")
63
+ return dict(env) if isinstance(env, Mapping) else {}
64
+
65
+
66
+ def env_placeholders(values: Mapping[str, Any]) -> list[str]:
67
+ """Chart ``env`` keys whose value still holds the scaffold placeholder (``CHANGE-ME``)."""
68
+ return sorted(
69
+ k for k, v in chart_env(values).items() if isinstance(v, str) and _image.has_placeholder(v)
70
+ )
71
+
72
+
73
+ def effective_auth_policy(settings: DeploySettings, values: Mapping[str, Any]) -> str:
74
+ """``env.AUTH_POLICY`` from the chart values, else the manifest's policy."""
75
+ raw = str(chart_env(values).get("AUTH_POLICY") or "").strip()
76
+ return normalize_auth_policy(raw or settings.auth_policy, warn=False)
77
+
78
+
79
+ def app_env(values: Mapping[str, Any]) -> str:
80
+ """``APP_ENV`` as the pod reads it (the app's dev test is exactly ``dev``)."""
81
+ value = chart_env(values).get("APP_ENV")
82
+ return "" if value is None else str(value)
83
+
84
+
85
+ @dataclass
86
+ class JwtFindings:
87
+ """What keeps the ``jwt`` policy from verifying tokens; ``strict`` outside dev."""
88
+
89
+ env_name: str = ""
90
+ strict: bool = False
91
+ problems: list[str] = field(default_factory=list)
92
+ # What cannot be told before the Secret is read; mentioned only next to problems.
93
+ unverified: list[str] = field(default_factory=list)
94
+
95
+ def __bool__(self) -> bool:
96
+ return bool(self.problems)
97
+
98
+ @property
99
+ def where(self) -> str:
100
+ return f"values-{self.env_name}.yaml (or values.yaml) under env:"
101
+
102
+ def error(self) -> str | None:
103
+ """The refusal (exit 3) outside dev: the pods would refuse to start."""
104
+ if not (self.problems and self.strict):
105
+ return None
106
+ also = f"\n Also check: {'; '.join(self.unverified)}." if self.unverified else ""
107
+ return (
108
+ f"The jwt auth policy cannot verify tokens in {self.env_name}: "
109
+ f"{'; '.join(self.problems)}.\n Set them in {self.where} (a setting the chart env "
110
+ "lists overrides the Secret, even when empty). Outside APP_ENV=dev the pods refuse "
111
+ f"to start without them.{also}"
112
+ )
113
+
114
+ def warning(self) -> str | None:
115
+ """The dev warning: the pods start, report Ready and answer every request with 503."""
116
+ if not self.problems or self.strict:
117
+ return None
118
+ return (
119
+ f"jwt: {'; '.join(self.problems)}. The {self.env_name} pods start and report Ready "
120
+ f"but answer every request with 503 until it is set in {self.where}"
121
+ )
122
+
123
+
124
+ def _setting(env: Mapping[str, Any], key: str, secret_keys: Set[str] | None) -> str | None:
125
+ """What the pod sees for ``key``: the chart env's value when it lists the key (the
126
+ ConfigMap wins, even when empty), else ``"<secret>"`` when the Secret holds it, else
127
+ ``""``. ``None`` when that cannot be known (the Secret was not read)."""
128
+ if key in env:
129
+ value = env.get(key)
130
+ return "" if value is None else str(value).strip()
131
+ if secret_keys is None:
132
+ return None
133
+ return "<secret>" if key in secret_keys else ""
134
+
135
+
136
+ def jwt_findings(
137
+ settings: DeploySettings,
138
+ env_name: str,
139
+ values: Mapping[str, Any],
140
+ secret_keys: Set[str] | None,
141
+ ) -> JwtFindings:
142
+ """What keeps the ``jwt`` policy from verifying tokens in ``env_name`` (empty otherwise).
143
+
144
+ ``secret_keys`` are the keys the app Secret holds (or will hold after this
145
+ deploy); ``None`` when unknown (argocd mode never reads the cluster), and a
146
+ setting the chart values do not list is then assumed to come from the
147
+ Secret. Strict outside dev (``env_name`` other than ``dev``, or ``APP_ENV``
148
+ not exactly ``dev``), where the pod would refuse to start.
149
+ """
150
+ findings = JwtFindings(env_name=env_name)
151
+ if effective_auth_policy(settings, values) != "jwt":
152
+ return findings
153
+ env = chart_env(values)
154
+ findings.strict = not _modes.is_dev_env(env_name) or app_env(values) != "dev"
155
+ algorithms = str(env.get("AUTH_JWT_ALGORITHMS") or _DEFAULT_JWT_ALGORITHMS)
156
+ asymmetric = [
157
+ a.strip().upper()
158
+ for a in algorithms.split(",")
159
+ if a.strip() and a.strip().upper() not in _HMAC and a.strip().lower() != "none"
160
+ ]
161
+ if asymmetric:
162
+ sources = [_setting(env, key, secret_keys) for key in JWT_KEY_SOURCES]
163
+ if None in sources and not any(sources):
164
+ unknown = [k for k, v in zip(JWT_KEY_SOURCES, sources, strict=True) if v is None]
165
+ findings.unverified.append(
166
+ "no verification key in the chart values, so "
167
+ + " or ".join(unknown)
168
+ + " must come from the Secret"
169
+ )
170
+ if all(value == "" for value in sources):
171
+ findings.problems.append(
172
+ "no verification key (set AUTH_JWT_JWKS_URL to the issuer's JWKS, or "
173
+ "AUTH_JWT_PUBLIC_KEY)"
174
+ )
175
+ elif all(value for value in sources):
176
+ findings.problems.append(
177
+ "both AUTH_JWT_JWKS_URL and AUTH_JWT_PUBLIC_KEY are set (set one)"
178
+ )
179
+ if findings.strict:
180
+ for key in JWT_REQUIRED_OUTSIDE_DEV:
181
+ if _setting(env, key, secret_keys) == "":
182
+ findings.problems.append(f"{key} is empty")
183
+ return findings
184
+
185
+
186
+ def sslmode(dsn: str) -> str | None:
187
+ """The ``sslmode`` a libpq connection string (URL or ``key=value`` form) asks for."""
188
+ match = _SSLMODE.search(dsn or "")
189
+ return match.group(1).lower() if match else None
190
+
191
+
192
+ def dsn_without_tls(values: Mapping[str, Any], dsn: str) -> bool:
193
+ """The DSN does not require TLS (and the chart env sets no ``PGSSLMODE`` that does)."""
194
+ mode = sslmode(dsn)
195
+ if mode is None:
196
+ mode = str(chart_env(values).get("PGSSLMODE") or "").strip().lower() or None
197
+ return mode not in TLS_SSLMODES
198
+
199
+
200
+ def _enabled(values: Mapping[str, Any], section: str) -> bool:
201
+ block = values.get(section)
202
+ value = block.get("enabled") if isinstance(block, Mapping) else None
203
+ return value is True or (
204
+ isinstance(value, str) and value.strip().lower() in ("true", "yes", "1")
205
+ )
206
+
207
+
208
+ def dsn_keys(settings: DeploySettings, values: Mapping[str, Any]) -> list[str]:
209
+ """Secret keys that hold an external database's connection string.
210
+
211
+ None when the bundled dev subchart provides the database (the chart builds
212
+ that DSN itself) or the fastapi runtime keeps its checkpoints in memory.
213
+ """
214
+ if _enabled(values, "postgresql"):
215
+ return []
216
+ if settings.runtime == "langgraph-server":
217
+ return ["DATABASE_URI"]
218
+ if str(chart_env(values).get("CHECKPOINTER") or "postgres").strip().lower() == "memory":
219
+ return []
220
+ return ["POSTGRES_DSN"]
221
+
222
+
223
+ def dsn_tls_warning(key: str) -> str:
224
+ return (
225
+ f"{key} does not require TLS (no sslmode=require, verify-ca or verify-full): the "
226
+ "database password and every checkpoint cross the network unencrypted. Append "
227
+ "?sslmode=verify-full&sslrootcert=<CA file mounted into the pod> (or "
228
+ "sslrootcert=system for a publicly trusted certificate), or set PGSSLMODE in the "
229
+ "chart env."
230
+ )
231
+
232
+
233
+ # ---------------------------------------------------------------------------
234
+ # Peers (protocol: a2a APIs): the rules the runtime applies when it calls them
235
+ # ---------------------------------------------------------------------------
236
+
237
+ # A credential-carrying call to another agent may use plain http outside APP_ENV=dev only to
238
+ # these hosts: the runtime's `internal_host` (app_utils/api_client.py; a test keeps the two
239
+ # in step).
240
+ INTERNAL_SUFFIXES = (".svc", ".svc.cluster.local")
241
+ CREDENTIAL_AUTH_MODES = ("bearer", "forward", "exchange")
242
+ TOKEN_EXCHANGE_URL = "TOKEN_EXCHANGE_URL"
243
+ TOKEN_EXCHANGE_CLIENT_ID = "TOKEN_EXCHANGE_CLIENT_ID"
244
+ TOKEN_EXCHANGE_CLIENT_SECRET = "TOKEN_EXCHANGE_CLIENT_SECRET"
245
+ PRINCIPAL_HASH_SALT = "PRINCIPAL_HASH_SALT"
246
+ _TRUE = ("1", "true", "yes", "on")
247
+
248
+
249
+ def internal_host(host: str) -> bool:
250
+ """Whether `host` is loopback, a single-label name or a cluster-internal (`.svc`) name."""
251
+ host = host.strip("[]").lower().rstrip(".")
252
+ if host == "localhost" or ("." not in host and ":" not in host):
253
+ return True
254
+ if host.endswith(INTERNAL_SUFFIXES):
255
+ return True
256
+ try:
257
+ return ipaddress.ip_address(host).is_loopback
258
+ except ValueError:
259
+ return False
260
+
261
+
262
+ def _loopback(host: str) -> bool:
263
+ host = host.strip("[]").lower()
264
+ if host == "localhost":
265
+ return True
266
+ try:
267
+ return ipaddress.ip_address(host).is_loopback
268
+ except ValueError:
269
+ return False
270
+
271
+
272
+ @dataclass
273
+ class PeerFindings:
274
+ """What keeps the agent from calling its peers in an environment (``strict`` outside dev:
275
+ the runtime refuses those calls, or refuses to start)."""
276
+
277
+ env_name: str = ""
278
+ strict: bool = False
279
+ problems: list[str] = field(default_factory=list)
280
+ warnings: list[str] = field(default_factory=list)
281
+
282
+ def error(self) -> str | None:
283
+ """The refusal (exit 3) outside dev."""
284
+ if not (self.problems and self.strict):
285
+ return None
286
+ lines = "\n".join(f" - {problem}" for problem in self.problems)
287
+ return (
288
+ f"The agent could not call the agents in api-policy.yaml in {self.env_name}:\n{lines}"
289
+ f"\n Set them in values-{self.env_name}.yaml (or values.yaml) under env:."
290
+ )
291
+
292
+ def notes(self) -> list[str]:
293
+ """The warnings, and in dev the problems too (they warn there)."""
294
+ return [*([] if self.strict else self.problems), *self.warnings]
295
+
296
+
297
+ def peer_findings(
298
+ settings: DeploySettings,
299
+ env_name: str,
300
+ values: Mapping[str, Any],
301
+ document: Mapping[str, Any] | None,
302
+ ) -> PeerFindings:
303
+ """The peer rules (``deploy``'s pre-checks; design 7.4) for ``env_name``.
304
+
305
+ Refused outside dev: a credential-carrying peer URL over http to a host that
306
+ is not internal (loopback, a single label, ``.svc``); an ``auth: exchange`` API
307
+ without ``TOKEN_EXCHANGE_URL`` or ``TOKEN_EXCHANGE_CLIENT_ID``, or with an http
308
+ token URL to a host that is not loopback (unless ``TOKEN_EXCHANGE_ALLOW_HTTP``).
309
+ A peer URL still at ``CHANGE-ME`` is refused by ``env_placeholders`` already.
310
+ Warned: an unset peer URL, and what ``secrets.keys`` lacks (the exchange
311
+ client secret, ``PRINCIPAL_HASH_SALT``).
312
+ """
313
+ findings = PeerFindings(env_name=env_name)
314
+ findings.strict = not _modes.is_dev_env(env_name) or app_env(values) != "dev"
315
+ apis = (document or {}).get("apis") or {}
316
+ peers = {
317
+ str(n): a for n, a in apis.items() if isinstance(a, Mapping) and a.get("protocol") == "a2a"
318
+ }
319
+ env = chart_env(values)
320
+ for name, api in peers.items():
321
+ variable = str(api.get("base_url_env") or "")
322
+ url = str(env.get(variable) or "").strip()
323
+ if not url:
324
+ findings.warnings.append(
325
+ f"{variable} (the URL of the agent behind {name}) is not in the chart env: set it "
326
+ f"in values-{env_name}.yaml, or the calls to {name} fail"
327
+ )
328
+ continue
329
+ if _image.has_placeholder(url) or api.get("auth") not in CREDENTIAL_AUTH_MODES:
330
+ continue
331
+ parts = urllib.parse.urlsplit(url)
332
+ if parts.scheme == "http" and not internal_host(parts.hostname or ""):
333
+ findings.problems.append(
334
+ f"{variable} ({url}) must use https outside APP_ENV=dev to carry credentials "
335
+ f"(auth: {api.get('auth')}); plain http is for loopback and cluster-internal "
336
+ "names only"
337
+ )
338
+ keys = set(settings.secret_keys)
339
+ if any(api.get("auth") == "exchange" for api in apis.values() if isinstance(api, Mapping)):
340
+ for variable in (TOKEN_EXCHANGE_URL, TOKEN_EXCHANGE_CLIENT_ID):
341
+ if not str(env.get(variable) or "").strip():
342
+ findings.problems.append(
343
+ f"{variable} is not set, and auth: exchange APIs need it (the issuer's token "
344
+ "endpoint and this agent's client there)"
345
+ )
346
+ token = str(env.get(TOKEN_EXCHANGE_URL) or "").strip()
347
+ allow_http = str(env.get("TOKEN_EXCHANGE_ALLOW_HTTP") or "").strip().lower() in _TRUE
348
+ parts = urllib.parse.urlsplit(token)
349
+ if (
350
+ token
351
+ and not _image.has_placeholder(token)
352
+ and parts.scheme == "http"
353
+ and not (_loopback(parts.hostname or "") or allow_http)
354
+ ):
355
+ findings.problems.append(
356
+ f"{TOKEN_EXCHANGE_URL} ({token}) must use https outside APP_ENV=dev (it carries "
357
+ 'the users\' tokens and the client secret); set TOKEN_EXCHANGE_ALLOW_HTTP: "true" '
358
+ "only for a trusted in-cluster issuer"
359
+ )
360
+ if TOKEN_EXCHANGE_CLIENT_SECRET not in keys:
361
+ findings.warnings.append(
362
+ f"{TOKEN_EXCHANGE_CLIENT_SECRET} is not in secrets.keys: the pods never get it, so "
363
+ "every token exchange fails"
364
+ )
365
+ if peers and PRINCIPAL_HASH_SALT not in keys:
366
+ findings.warnings.append(
367
+ f"{PRINCIPAL_HASH_SALT} is not in secrets.keys: the conversation ids sent to other "
368
+ "agents are keyed without a secret"
369
+ )
370
+ return findings