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,1169 @@
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
+ """Read-only prerequisite checks for ``infra check``: cluster, GitHub gates, disconnected profile.
15
+
16
+ Every check reports; nothing here creates or changes cluster or GitHub state.
17
+ Cluster checks shell out to ``kubectl`` with ``check=False`` so a missing
18
+ prerequisite is a row in the table, never an exception.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import base64
24
+ import binascii
25
+ import json
26
+ import os
27
+ import re
28
+ from collections.abc import Callable
29
+ from dataclasses import asdict, dataclass, field
30
+ from pathlib import Path
31
+ from typing import Any
32
+
33
+ from graph_agents_cli._defaults import REGISTRY_FIX_COMMAND, REGISTRY_FIX_EFFECT
34
+ from graph_agents_cli.deploy import _image, _kube, _modes, _preflight, gitops, local_load
35
+ from graph_agents_cli.deploy._config import DeploySettings
36
+ from graph_agents_cli.deploy._kube import Target, ToolFailed
37
+ from graph_agents_cli.deploy._values import load_chart_values
38
+ from graph_agents_cli.secrets import _apply as secrets_apply
39
+ from graph_agents_cli.secrets import _required
40
+
41
+ MIN_KUBERNETES = (1, 28)
42
+ DISCONNECTED = "disconnected"
43
+ PROFILES = (DISCONNECTED,)
44
+
45
+ OK = "ok"
46
+ MISSING = "missing"
47
+ WARN = "warn"
48
+ SKIP = "skip"
49
+ INFO = "info"
50
+
51
+ HOSTED_REGISTRY_HOSTS = ("ghcr.io", "docker.io", "index.docker.io", "gcr.io", "quay.io")
52
+ HOSTED_REGISTRY_SUFFIXES = (".amazonaws.com", ".pkg.dev", ".azurecr.io", ".gcr.io")
53
+
54
+
55
+ @dataclass
56
+ class Check:
57
+ name: str
58
+ status: str
59
+ required: bool
60
+ detail: str = ""
61
+ hint: str = ""
62
+
63
+ @property
64
+ def failed(self) -> bool:
65
+ return self.required and self.status == MISSING
66
+
67
+ def to_dict(self) -> dict[str, Any]:
68
+ return asdict(self)
69
+
70
+
71
+ @dataclass
72
+ class Report:
73
+ env: str | None
74
+ profile: str | None
75
+ mode: str | None
76
+ checks: list[Check] = field(default_factory=list)
77
+
78
+ @property
79
+ def ok(self) -> bool:
80
+ return not any(c.failed for c in self.checks)
81
+
82
+ def to_dict(self) -> dict[str, Any]:
83
+ return {
84
+ "env": self.env,
85
+ "profile": self.profile,
86
+ "mode": self.mode,
87
+ "ok": self.ok,
88
+ "checks": [c.to_dict() for c in self.checks],
89
+ }
90
+
91
+
92
+ def _kubectl(args: list[str], target: Target | None, *, namespaced: bool = False) -> Any:
93
+ """kubectl with a parsed JSON result: ``(returncode, data|None, stderr)``; never raises."""
94
+ try:
95
+ result = _kube.kubectl(
96
+ [*args, "-o", "json"], target, namespaced=namespaced, check=False, quiet=True
97
+ )
98
+ except ToolFailed as e:
99
+ return 1, None, str(e)
100
+ data = None
101
+ if result.returncode == 0 and result.stdout.strip():
102
+ try:
103
+ data = json.loads(result.stdout)
104
+ except json.JSONDecodeError:
105
+ data = None
106
+ return result.returncode, data, (result.stderr or "").strip()
107
+
108
+
109
+ def _names(listing: Any) -> list[str]:
110
+ if not isinstance(listing, dict):
111
+ return []
112
+ return [
113
+ str(item.get("metadata", {}).get("name", ""))
114
+ for item in listing.get("items", [])
115
+ if isinstance(item, dict)
116
+ ]
117
+
118
+
119
+ def _minor(version: str) -> int:
120
+ digits = re.match(r"\d+", version or "")
121
+ return int(digits.group()) if digits else 0
122
+
123
+
124
+ def _enabled(values: dict[str, Any], *path: str, default: bool = False) -> bool:
125
+ node: Any = values
126
+ for key in path:
127
+ if not isinstance(node, dict):
128
+ return default
129
+ node = node.get(key)
130
+ if node is None:
131
+ return default
132
+ return bool(node)
133
+
134
+
135
+ def _get(values: dict[str, Any], *path: str, default: Any = None) -> Any:
136
+ node: Any = values
137
+ for key in path:
138
+ if not isinstance(node, dict):
139
+ return default
140
+ node = node.get(key)
141
+ return default if node is None else node
142
+
143
+
144
+ # --------------------------------------------------------------------------- checks
145
+
146
+
147
+ def check_tools(settings: DeploySettings, mode: str) -> list[Check]:
148
+ checks: list[Check] = []
149
+ needs = {
150
+ "kubectl": True,
151
+ "helm": mode != _modes.ARGOCD,
152
+ "docker": mode in _modes.DIRECT_MODES or (mode == _modes.HELM_PUSH),
153
+ "gh": settings.cd != "skip",
154
+ }
155
+ hints = {
156
+ "kubectl": "https://kubernetes.io/docs/tasks/tools/",
157
+ "helm": "https://helm.sh/docs/intro/install/",
158
+ "docker": "install Docker (or a docker-compatible CLI) to build images locally",
159
+ "gh": "https://cli.github.com/",
160
+ }
161
+ for tool, required in needs.items():
162
+ present = _kube.tool_available(tool)
163
+ checks.append(
164
+ Check(
165
+ f"tool: {tool}",
166
+ OK if present else MISSING,
167
+ required,
168
+ "found" if present else "not on PATH",
169
+ "" if present else hints[tool],
170
+ )
171
+ )
172
+ if settings.cd == _modes.ARGOCD:
173
+ present = _kube.tool_available("argocd")
174
+ checks.append(
175
+ Check(
176
+ "tool: argocd",
177
+ OK if present else INFO,
178
+ False,
179
+ "found" if present else "not on PATH; `deploy --status` falls back to kubectl",
180
+ "" if present else "https://argo-cd.readthedocs.io/en/stable/cli_installation/",
181
+ )
182
+ )
183
+ return checks
184
+
185
+
186
+ def check_cluster(
187
+ settings: DeploySettings, env: str, target: Target, values: dict[str, Any]
188
+ ) -> list[Check]:
189
+ checks: list[Check] = []
190
+ rc, version, err = _kubectl(["version"], target)
191
+ server = version.get("serverVersion") if isinstance(version, dict) else None
192
+ if rc != 0 or not isinstance(server, dict):
193
+ checks.append(
194
+ Check(
195
+ "cluster reachable",
196
+ MISSING,
197
+ True,
198
+ err.splitlines()[0]
199
+ if err
200
+ else f"kubectl could not reach context {target.context or '(current)'}",
201
+ "check the kube context recorded in environments.<env>.context and your kubeconfig",
202
+ )
203
+ )
204
+ return checks
205
+ git_version = str(server.get("gitVersion", ""))
206
+ checks.append(
207
+ Check(
208
+ "cluster reachable",
209
+ OK,
210
+ True,
211
+ f"context {target.context or '(current)'}, server {git_version}",
212
+ )
213
+ )
214
+ major, minor = _minor(str(server.get("major", "0"))), _minor(str(server.get("minor", "0")))
215
+ new_enough = (major, minor) >= MIN_KUBERNETES
216
+ checks.append(
217
+ Check(
218
+ f"kubernetes >= {MIN_KUBERNETES[0]}.{MIN_KUBERNETES[1]}",
219
+ OK if new_enough else MISSING,
220
+ True,
221
+ f"server {major}.{minor}",
222
+ ""
223
+ if new_enough
224
+ else "upgrade the cluster; the chart uses gateway.networking.k8s.io/v1",
225
+ )
226
+ )
227
+
228
+ if _enabled(values, "gateway", "enabled", default=True):
229
+ checks += _gateway_checks(target, values)
230
+ else:
231
+ # Like cert-manager and metrics-server below: nothing to install, no hint.
232
+ checks.append(Check("gateway api", SKIP, False, "not needed: gateway.enabled is false"))
233
+
234
+ ingress_on = _enabled(values, "ingress", "enabled")
235
+ rc, listing, _ = _kubectl(["get", "ingressclass"], target)
236
+ ingress_classes = _names(listing) if rc == 0 else []
237
+ ingress_wanted = str(_get(values, "ingress", "className", default="") or "")
238
+ detail = ", ".join(ingress_classes) if ingress_classes else "no IngressClass in the cluster"
239
+ status = OK if ingress_classes else (MISSING if ingress_on else INFO)
240
+ if ingress_wanted and ingress_on:
241
+ status = OK if ingress_wanted in ingress_classes else MISSING
242
+ detail = f"{ingress_wanted} {'found' if status == OK else 'not found'}; available: {', '.join(ingress_classes) or 'none'}"
243
+ checks.append(
244
+ Check(
245
+ "ingress classes",
246
+ status,
247
+ ingress_on,
248
+ detail,
249
+ ""
250
+ if status == OK
251
+ else "select a maintained ingress controller and set ingress.className",
252
+ )
253
+ )
254
+
255
+ if _enabled(values, "tls", "certManager", "enabled"):
256
+ rc, _data, _ = _kubectl(["get", "crd", "certificates.cert-manager.io"], target)
257
+ checks.append(
258
+ Check(
259
+ "cert-manager",
260
+ OK if rc == 0 else MISSING,
261
+ True,
262
+ "certificates.cert-manager.io present" if rc == 0 else "CRD absent",
263
+ ""
264
+ if rc == 0
265
+ else "install cert-manager (https://cert-manager.io/docs/installation/) or set tls.certManager.enabled=false",
266
+ )
267
+ )
268
+ else:
269
+ checks.append(
270
+ Check("cert-manager", SKIP, False, "not needed: tls.certManager.enabled is false")
271
+ )
272
+
273
+ if _enabled(values, "hpa", "enabled"):
274
+ rc, api, _ = _kubectl(["get", "apiservice", "v1beta1.metrics.k8s.io"], target)
275
+ available = False
276
+ if rc == 0 and isinstance(api, dict):
277
+ for cond in api.get("status", {}).get("conditions", []):
278
+ if cond.get("type") == "Available" and str(cond.get("status")).lower() == "true":
279
+ available = True
280
+ checks.append(
281
+ Check(
282
+ "metrics-server",
283
+ OK if available else MISSING,
284
+ True,
285
+ "metrics API available" if available else "v1beta1.metrics.k8s.io unavailable",
286
+ ""
287
+ if available
288
+ else "install metrics-server (https://github.com/kubernetes-sigs/metrics-server) or set hpa.enabled=false",
289
+ )
290
+ )
291
+ else:
292
+ checks.append(Check("metrics-server", SKIP, False, "not needed: hpa.enabled is false"))
293
+
294
+ if settings.cd == _modes.ARGOCD:
295
+ rc_ns, _ns, _ = _kubectl(["get", "namespace", "argocd"], target)
296
+ rc_crd, _crd, _ = _kubectl(["get", "crd", "applications.argoproj.io"], target)
297
+ ok = rc_ns == 0 and rc_crd == 0
298
+ checks.append(
299
+ Check(
300
+ "argo cd",
301
+ OK if ok else MISSING,
302
+ True,
303
+ "namespace argocd and applications.argoproj.io present"
304
+ if ok
305
+ else f"namespace argocd {'ok' if rc_ns == 0 else 'missing'}, Application CRD {'ok' if rc_crd == 0 else 'missing'}",
306
+ ""
307
+ if ok
308
+ else "install Argo CD (https://argo-cd.readthedocs.io/en/stable/getting_started/)",
309
+ )
310
+ )
311
+ else:
312
+ checks.append(Check("argo cd", SKIP, False, f"not needed: cd is {settings.cd}"))
313
+
314
+ rc, _ns, _ = _kubectl(["get", "namespace", target.namespace], target)
315
+ checks.append(
316
+ Check(
317
+ f"namespace {target.namespace}",
318
+ OK if rc == 0 else WARN,
319
+ False,
320
+ "exists"
321
+ if rc == 0
322
+ else "absent (`deploy` and `secrets apply` create it; Argo CD with CreateNamespace)",
323
+ )
324
+ )
325
+
326
+ pull_secrets: list[str] = []
327
+ for item in _get(values, "imagePullSecrets", default=[]) or []:
328
+ if isinstance(item, dict) and item.get("name"):
329
+ pull_secrets.append(str(item["name"]))
330
+ elif isinstance(item, str):
331
+ pull_secrets.append(item)
332
+ if pull_secrets:
333
+ for name in pull_secrets:
334
+ rc, _s, _ = _kubectl(["get", "secret", name], target, namespaced=True)
335
+ checks.append(
336
+ Check(
337
+ f"image pull secret {name}",
338
+ OK if rc == 0 else MISSING,
339
+ True,
340
+ f"present in {target.namespace}"
341
+ if rc == 0
342
+ else f"absent from {target.namespace}",
343
+ ""
344
+ if rc == 0
345
+ else f"kubectl create secret docker-registry {name} -n {target.namespace} ... (operator prerequisite)",
346
+ )
347
+ )
348
+ else:
349
+ checks.append(Check("image pull secrets", SKIP, False, "none referenced in values"))
350
+
351
+ rc, secret, _ = _kubectl(["get", "secret", settings.secret_name], target, namespaced=True)
352
+ cd_mode = settings.cd != "skip"
353
+ required = _required.required_keys(settings, values)
354
+ present: set[str] = set()
355
+ if isinstance(secret, dict):
356
+ present = set(secret.get("data") or {}) | set(secret.get("stringData") or {})
357
+ missing = [k for k in required if k not in present]
358
+ provision = secrets_apply.provision_hint(env)
359
+ if rc != 0:
360
+ status, detail, hint = MISSING if cd_mode else WARN, "absent", provision
361
+ elif missing and isinstance(secret, dict):
362
+ status = MISSING if cd_mode else WARN
363
+ detail = f"present, missing required key(s): {', '.join(missing)}"
364
+ hint = provision
365
+ else:
366
+ status, detail, hint = (
367
+ OK,
368
+ "present" + (f" with the required key(s) {', '.join(required)}" if required else ""),
369
+ "",
370
+ )
371
+ # In cd skip, `deploy` applies the Secret from the env file itself, then
372
+ # refuses before helm when a required key is still missing.
373
+ checks.append(Check(f"app secret {settings.secret_name}", status, cd_mode, detail, hint))
374
+ checks += _jwt_checks(settings, env, values, present if rc == 0 else set())
375
+ checks += _database_tls_checks(settings, env, values, secret if rc == 0 else None)
376
+ checks += _metrics_token_checks(settings, env, target, values)
377
+ return checks
378
+
379
+
380
+ def _database_tls_checks(
381
+ settings: DeploySettings, env: str, values: dict[str, Any], secret: Any
382
+ ) -> list[Check]:
383
+ """Outside dev: whether the external database's DSN requires TLS (read, never printed)."""
384
+ if _modes.is_dev_env(env) or not isinstance(secret, dict):
385
+ return []
386
+ checks: list[Check] = []
387
+ data = secret.get("data") or {}
388
+ for key in _preflight.dsn_keys(settings, values):
389
+ raw = data.get(key)
390
+ if not raw:
391
+ continue
392
+ try:
393
+ dsn = base64.b64decode(str(raw)).decode("utf-8", errors="replace")
394
+ except (binascii.Error, ValueError):
395
+ continue
396
+ insecure = _preflight.dsn_without_tls(values, dsn)
397
+ checks.append(
398
+ Check(
399
+ f"database tls ({key})",
400
+ WARN if insecure else OK,
401
+ False,
402
+ "no sslmode=require/verify-ca/verify-full"
403
+ if insecure
404
+ else f"sslmode={_preflight.sslmode(dsn) or 'from PGSSLMODE'}",
405
+ "append ?sslmode=verify-full&sslrootcert=<CA file mounted into the pod> (or "
406
+ "sslrootcert=system) to the DSN, and connect as a least-privileged role"
407
+ if insecure
408
+ else "",
409
+ )
410
+ )
411
+ return checks
412
+
413
+
414
+ def _gateway_checks(target: Target, values: dict[str, Any]) -> list[Check]:
415
+ """Gateway API CRDs, a GatewayClass and the parentRef (only called while the gateway is on)."""
416
+ checks: list[Check] = []
417
+ gateway_on = True
418
+ rc, _data, _ = _kubectl(["get", "crd", "httproutes.gateway.networking.k8s.io"], target)
419
+ have_gateway_crd = rc == 0
420
+ checks.append(
421
+ Check(
422
+ "gateway api crds",
423
+ OK if have_gateway_crd else MISSING,
424
+ gateway_on,
425
+ "httproutes.gateway.networking.k8s.io present"
426
+ if have_gateway_crd
427
+ else "HTTPRoute CRD absent",
428
+ ""
429
+ if have_gateway_crd
430
+ else "install the Gateway API CRDs (https://gateway-api.sigs.k8s.io/guides/) or set ingress.enabled=true",
431
+ )
432
+ )
433
+ class_names: list[str] = []
434
+ if have_gateway_crd:
435
+ rc, listing, _ = _kubectl(["get", "gatewayclass"], target)
436
+ class_names = _names(listing) if rc == 0 else []
437
+ wanted = str(_get(values, "gateway", "className", default="") or "")
438
+ if wanted:
439
+ found = wanted in class_names
440
+ checks.append(
441
+ Check(
442
+ "gateway class",
443
+ OK if found else MISSING,
444
+ gateway_on,
445
+ f"{wanted} {'found' if found else 'not found'}; available: {', '.join(class_names) or 'none'}",
446
+ ""
447
+ if found
448
+ else "set gateway.className to one of the GatewayClasses in the cluster",
449
+ )
450
+ )
451
+ else:
452
+ checks.append(
453
+ Check(
454
+ "gateway classes",
455
+ OK if class_names else MISSING,
456
+ gateway_on,
457
+ ", ".join(class_names) if class_names else "no GatewayClass in the cluster",
458
+ ""
459
+ if class_names
460
+ else "install a Gateway API implementation (Envoy Gateway, Cilium, Istio, Traefik, NGINX Gateway Fabric, Kong) and set gateway.className",
461
+ )
462
+ )
463
+ # The chart's HTTPRoute marks gateway.parentRef.name as `required`, and `deploy`
464
+ # refuses (exit 3) before any tool runs when it is blank; report it here so the
465
+ # operator sees it before the first deploy.
466
+ parent_name = str(_get(values, "gateway", "parentRef", "name", default="") or "").strip()
467
+ checks.append(
468
+ Check(
469
+ "gateway parentRef",
470
+ OK if parent_name else MISSING,
471
+ gateway_on,
472
+ f"gateway.parentRef.name = {parent_name}"
473
+ if parent_name
474
+ else "gateway.parentRef.name is blank",
475
+ ""
476
+ if parent_name
477
+ else "set gateway.parentRef.name to the Gateway to attach to in values-<env>.yaml (or set gateway.enabled: false / ingress.enabled: true)",
478
+ )
479
+ )
480
+ return checks
481
+
482
+
483
+ def _jwt_checks(
484
+ settings: DeploySettings, env: str, values: dict[str, Any], secret_keys: set[str]
485
+ ) -> list[Check]:
486
+ """The ``jwt`` policy's verification settings (a key source; issuer and audience outside dev)."""
487
+ if _preflight.effective_auth_policy(settings, values) != "jwt":
488
+ return []
489
+ findings = _preflight.jwt_findings(settings, env, values, secret_keys)
490
+ name = "auth: jwt settings"
491
+ if not findings:
492
+ return [Check(name, OK, findings.strict, "verification key (and issuer and audience) set")]
493
+ hint = f"set them in {findings.where}"
494
+ if findings.strict:
495
+ detail = "; ".join(findings.problems) + " (the pods refuse to start)"
496
+ return [Check(name, MISSING, True, detail, hint)]
497
+ detail = "; ".join(findings.problems) + " (the pods answer every request with 503)"
498
+ return [Check(name, WARN, False, detail, hint)]
499
+
500
+
501
+ def _metrics_token_checks(
502
+ settings: DeploySettings, env: str, target: Target, values: dict[str, Any]
503
+ ) -> list[Check]:
504
+ """The ServiceMonitor's token Secret, when the ServiceMonitor sends one."""
505
+ monitor = _get(values, "metrics", "serviceMonitor", default={}) or {}
506
+ bearer = monitor.get("bearerToken") if isinstance(monitor, dict) else None
507
+ if not (
508
+ _enabled(values, "metrics", "serviceMonitor", "enabled")
509
+ and isinstance(bearer, dict)
510
+ and bearer.get("enabled")
511
+ ):
512
+ return []
513
+ name = str(bearer.get("secretName") or settings.metrics_secret_name)
514
+ key = str(bearer.get("key") or secrets_apply.METRICS_TOKEN_KEY)
515
+ rc, secret, _ = _kubectl(["get", "secret", name], target, namespaced=True)
516
+ present = set((secret or {}).get("data") or {}) if isinstance(secret, dict) else set()
517
+ ok = rc == 0 and key in present
518
+ hint = ""
519
+ if not ok:
520
+ hint = (
521
+ f"add {secrets_apply.METRICS_TOKEN_KEY} to secrets.keys and the env file, then "
522
+ f"{secrets_apply.provision_hint(env)} (it writes {settings.metrics_secret_name})"
523
+ if name == settings.metrics_secret_name
524
+ else f"create Secret {name} with the key {key} in {target.namespace}"
525
+ )
526
+ return [
527
+ Check(
528
+ f"metrics token secret {name}",
529
+ OK if ok else WARN,
530
+ False,
531
+ f"present with {key}"
532
+ if ok
533
+ else (f"absent from {target.namespace}" if rc != 0 else f"has no key {key}")
534
+ + ": the ServiceMonitor's scrapes get 401",
535
+ hint,
536
+ )
537
+ ]
538
+
539
+
540
+ def _gated_apis(policy_path: Path) -> list[str]:
541
+ """``name (approvers)`` of each API whose valid policy has an approval block.
542
+
543
+ With a list of approval rules, the approvers of every rule, each once.
544
+ """
545
+ from graph_agents_cli._api_policy import (
546
+ ApiPolicyFileError,
547
+ approval_rules,
548
+ load_policy_document,
549
+ )
550
+
551
+ if not policy_path.is_file():
552
+ return []
553
+ try:
554
+ document = load_policy_document(policy_path)
555
+ except ApiPolicyFileError:
556
+ return [] # `lint` reports an invalid policy
557
+ gated = []
558
+ for name, api in document["apis"].items():
559
+ rules = approval_rules(api) if isinstance(api, dict) else []
560
+ if rules:
561
+ approvers = dict.fromkeys(str(a) for rule in rules for a in rule["approvers"])
562
+ gated.append(f"{name} ({', '.join(approvers)})")
563
+ return gated
564
+
565
+
566
+ def check_approvals(
567
+ settings: DeploySettings, values: dict[str, Any], project_root: Path | None = None
568
+ ) -> list[Check]:
569
+ """Where pending approvals are kept, when api-policy.yaml gates any call.
570
+
571
+ A gated call pauses its run until an approver decides; the paused run
572
+ lives in the checkpointer and the approval in the ``approvals`` table of
573
+ the app database (``agent_approvals`` in ``DATABASE_URI`` under
574
+ langgraph-server). With the in-memory checkpointer both are lost when the
575
+ pod restarts, and another replica cannot resume them.
576
+ """
577
+ from graph_agents_cli._api_policy import POLICY_FILENAME
578
+
579
+ gated = _gated_apis((project_root or Path.cwd()) / POLICY_FILENAME)
580
+ if not gated:
581
+ return []
582
+ name = "approval gates"
583
+ apis = f"api-policy.yaml gates calls of {'; '.join(gated)}"
584
+ memory = (
585
+ settings.runtime != "langgraph-server"
586
+ and not _enabled(values, "postgresql", "enabled")
587
+ and str(_preflight.chart_env(values).get("CHECKPOINTER") or "postgres").strip().lower()
588
+ == "memory"
589
+ )
590
+ if memory:
591
+ return [
592
+ Check(
593
+ name,
594
+ WARN,
595
+ False,
596
+ f"{apis}; with CHECKPOINTER=memory a paused run and its pending approval live "
597
+ "in one pod's memory: a restart loses them and another replica cannot resume "
598
+ "them",
599
+ "set CHECKPOINTER=postgres: the paused runs and the approvals table then live "
600
+ "in the app database",
601
+ )
602
+ ]
603
+ server = settings.runtime == "langgraph-server"
604
+ where = "DATABASE_URI" if server else "POSTGRES_DSN"
605
+ table = "agent_approvals" if server else "approvals"
606
+ return [
607
+ Check(
608
+ name,
609
+ INFO,
610
+ False,
611
+ f"{apis}; pending and decided approvals are kept in the {table} table of the "
612
+ f"app database ({where}, beside the checkpoints): back it up with them, and keep "
613
+ "its retention in line with RETENTION_DAYS",
614
+ )
615
+ ]
616
+
617
+
618
+ def _codeowners_placeholders() -> list[str]:
619
+ path = Path(".github") / "CODEOWNERS"
620
+ if not path.is_file():
621
+ return []
622
+ lines = path.read_text(encoding="utf-8", errors="replace").splitlines()
623
+ return [
624
+ line.strip()
625
+ for line in lines
626
+ if line.strip() and not line.lstrip().startswith("#") and _image.has_placeholder(line)
627
+ ]
628
+
629
+
630
+ def _argocd_placeholders() -> list[str]:
631
+ """``deployment/argocd/*.yaml`` files whose ``repoURL`` still holds the placeholder."""
632
+ import yaml
633
+
634
+ found: list[str] = []
635
+ for path in sorted((Path("deployment") / "argocd").glob("*.yaml")):
636
+ try:
637
+ doc = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
638
+ except (OSError, yaml.YAMLError):
639
+ continue
640
+ spec = doc.get("spec") if isinstance(doc, dict) else None
641
+ if not isinstance(spec, dict):
642
+ continue
643
+ sources = [spec.get("source"), *(spec.get("sources") or [])]
644
+ urls = [str(s.get("repoURL") or "") for s in sources if isinstance(s, dict)]
645
+ if any(_image.has_placeholder(url) for url in urls):
646
+ found.append(str(path))
647
+ return found
648
+
649
+
650
+ def check_placeholders(
651
+ settings: DeploySettings, values: dict[str, Any], env: str | None = None
652
+ ) -> list[Check]:
653
+ """Scaffold placeholders (``CHANGE-ME``) that break a build, a rollout or the production gate.
654
+
655
+ A placeholder in the chart env is what ``deploy`` refuses outside dev and
656
+ warns about in dev (the pods would call it); ``env`` selects which.
657
+ """
658
+ checks: list[Check] = []
659
+ registry = settings.registry
660
+ problem = (
661
+ _image.reference_problem(
662
+ f"{registry.rstrip('/')}/{settings.project_name}", None, registry=registry
663
+ )
664
+ if registry
665
+ else "create_params.registry is empty"
666
+ )
667
+ checks.append(
668
+ Check(
669
+ "placeholder: registry",
670
+ MISSING if problem else OK,
671
+ True,
672
+ registry if not problem else problem.splitlines()[0],
673
+ ""
674
+ if not problem
675
+ else f"run `{REGISTRY_FIX_COMMAND}` ({REGISTRY_FIX_EFFECT}), or set "
676
+ "create_params.registry in graph-agents-cli-manifest.yaml (e.g. ghcr.io/<org>)",
677
+ )
678
+ )
679
+ repository = str(_get(values, "image", "repository", default="") or "")
680
+ argocd = settings.cd == _modes.ARGOCD
681
+ chart_placeholder = _image.has_placeholder(repository)
682
+ checks.append(
683
+ Check(
684
+ "placeholder: chart image.repository",
685
+ (MISSING if argocd else WARN) if chart_placeholder else OK,
686
+ argocd,
687
+ repository or "(unset)",
688
+ ""
689
+ if not chart_placeholder
690
+ else f"run `{REGISTRY_FIX_COMMAND}`, or set image.repository in the chart's "
691
+ "values.yaml (Argo CD renders it as is; `deploy` overrides it with --set)",
692
+ )
693
+ )
694
+ env_keys = _preflight.env_placeholders(values)
695
+ env_required = not (env and _modes.is_dev_env(env))
696
+ checks.append(
697
+ Check(
698
+ "placeholder: chart env",
699
+ (MISSING if env_required else WARN) if env_keys else OK,
700
+ env_required,
701
+ f"CHANGE-ME in env.{', env.'.join(env_keys)}" if env_keys else "none",
702
+ ""
703
+ if not env_keys
704
+ else "replace the placeholder URLs in values.yaml / values-<env>.yaml",
705
+ )
706
+ )
707
+ owners = _codeowners_placeholders()
708
+ gated = settings.cd != "skip"
709
+ checks.append(
710
+ Check(
711
+ "placeholder: CODEOWNERS",
712
+ (MISSING if gated else WARN) if owners else OK,
713
+ gated,
714
+ f"{len(owners)} rule(s) name a CHANGE-ME owner" if owners else "no placeholder owner",
715
+ ""
716
+ if not owners
717
+ else "name the team or users who approve production changes in .github/CODEOWNERS "
718
+ "(GitHub ignores unknown owners, so the code-owner review gate would require nobody)",
719
+ )
720
+ )
721
+ if argocd:
722
+ apps = _argocd_placeholders()
723
+ checks.append(
724
+ Check(
725
+ "placeholder: argocd repoURL",
726
+ MISSING if apps else OK,
727
+ True,
728
+ f"CHANGE-ME repoURL in {', '.join(apps)}" if apps else "set",
729
+ "" if not apps else "set spec.source.repoURL to this repository's git URL",
730
+ )
731
+ )
732
+ return checks
733
+
734
+
735
+ def _github_token_available() -> bool:
736
+ if gitops.github_token():
737
+ return True
738
+ if not _kube.tool_available("gh"):
739
+ return False
740
+ try:
741
+ result = _kube.run_cmd(["gh", "auth", "status"], check=False, quiet=True)
742
+ except ToolFailed:
743
+ return False
744
+ return result.returncode == 0
745
+
746
+
747
+ def _gh_api(path: str) -> tuple[int, Any]:
748
+ rc, data, _err = _gh_api_detail(path)
749
+ return rc, data
750
+
751
+
752
+ def _gh_api_detail(path: str) -> tuple[int, Any, str]:
753
+ """``gh api <path>`` (GET, read-only): ``(returncode, parsed JSON or None, stderr)``."""
754
+ try:
755
+ result = _kube.run_cmd(["gh", "api", path], check=False, quiet=True)
756
+ except ToolFailed as e:
757
+ return 1, None, str(e)
758
+ if result.returncode != 0:
759
+ return result.returncode, None, (result.stderr or "").strip()
760
+ try:
761
+ return 0, json.loads(result.stdout or "null"), ""
762
+ except json.JSONDecodeError:
763
+ return 0, None, ""
764
+
765
+
766
+ # `gh api` reports an HTTP error as e.g. "gh: Not Found (HTTP 404)".
767
+ _HTTP_STATUS = re.compile(r"\(HTTP (\d{3})\)")
768
+
769
+ FOUND = "found"
770
+ ABSENT = "absent"
771
+ DENIED = "denied"
772
+ UNKNOWN = "unknown"
773
+
774
+
775
+ def _gh_lookup(path: str) -> tuple[str, Any, str]:
776
+ """Classify a read: FOUND, ABSENT (HTTP 404), DENIED (401/403) or UNKNOWN (network, ...)."""
777
+ rc, data, err = _gh_api_detail(path)
778
+ if rc == 0:
779
+ return FOUND, data, ""
780
+ match = _HTTP_STATUS.search(err)
781
+ code = int(match.group(1)) if match else 0
782
+ if code == 404:
783
+ return ABSENT, None, err
784
+ if code in (401, 403):
785
+ return DENIED, None, err
786
+ return UNKNOWN, None, err
787
+
788
+
789
+ def _first_line(text: str) -> str:
790
+ return next((line.strip() for line in text.splitlines() if line.strip()), "")
791
+
792
+
793
+ # GitHub environments the helm-push workflows run in (staging.yaml, promote-to-prod.yaml)
794
+ # and the environment secret holding the cluster credentials there.
795
+ HELM_PUSH_ENVIRONMENTS = ("staging", "production")
796
+ KUBECONFIG_SECRET = "DEPLOY_KUBECONFIG"
797
+ REPO_KUBECONFIG_NAMES = ("KUBECONFIG", "DEPLOY_KUBECONFIG")
798
+
799
+
800
+ def check_kubeconfig_secrets(repo: str) -> list[Check]:
801
+ """helm-push: the kubeconfig must be an environment secret, never a repository one.
802
+
803
+ Only jobs that pass the environment's protection rules (required reviewers
804
+ for production, main only) can read an environment secret. A repository or
805
+ organization secret with the same name is readable by every workflow job of
806
+ the repository, and ``secrets.DEPLOY_KUBECONFIG`` falls back to it whenever
807
+ the environment secret is missing, which bypasses the production gate.
808
+ Only secret names are read (GitHub never returns values).
809
+ """
810
+ checks: list[Check] = []
811
+ for environment in HELM_PUSH_ENVIRONMENTS:
812
+ name = f"github secret: {KUBECONFIG_SECRET} ({environment})"
813
+ state, _data, err = _gh_lookup(
814
+ f"{repo}/environments/{environment}/secrets/{KUBECONFIG_SECRET}"
815
+ )
816
+ if state == FOUND:
817
+ checks.append(Check(name, OK, False, f"set in the {environment} environment"))
818
+ elif state == ABSENT:
819
+ checks.append(
820
+ Check(
821
+ name,
822
+ WARN,
823
+ False,
824
+ f"not set in the {environment} environment (or the environment does not "
825
+ "exist); the helm-push deploy job then stops at `Configure kubeconfig`, "
826
+ "or uses a repository or organization secret of that name",
827
+ f"gh secret set {KUBECONFIG_SECRET} --env {environment} < <kubeconfig of the "
828
+ f"{environment} cluster> (an environment secret, never a repository secret)",
829
+ )
830
+ )
831
+ elif state == DENIED:
832
+ checks.append(
833
+ Check(
834
+ name,
835
+ INFO,
836
+ False,
837
+ "not checked: listing environment secrets needs admin access (or the "
838
+ "secrets read permission) on the repository",
839
+ )
840
+ )
841
+ else:
842
+ checks.append(
843
+ Check(name, INFO, False, f"not checked: {_first_line(err) or 'gh api failed'}")
844
+ )
845
+
846
+ found: list[str] = []
847
+ unknown: list[str] = []
848
+ for secret in REPO_KUBECONFIG_NAMES:
849
+ state, _data, err = _gh_lookup(f"{repo}/actions/secrets/{secret}")
850
+ if state == FOUND:
851
+ found.append(f"repository secret {secret}")
852
+ elif state == DENIED:
853
+ unknown.append("reading repository secrets needs admin access on the repository")
854
+ elif state != ABSENT:
855
+ unknown.append(_first_line(err) or "gh api failed")
856
+ # Organization secrets shared with the repository resolve the same way (best effort).
857
+ state, listing, _err = _gh_lookup(f"{repo}/actions/organization-secrets?per_page=100")
858
+ org_checked = state == FOUND and isinstance(listing, dict)
859
+ if org_checked:
860
+ for item in listing.get("secrets") or []:
861
+ if isinstance(item, dict) and item.get("name") in REPO_KUBECONFIG_NAMES:
862
+ found.append(f"organization secret {item['name']}")
863
+ name = "github secret: repository-level kubeconfig"
864
+ if found:
865
+ checks.append(
866
+ Check(
867
+ name,
868
+ WARN,
869
+ False,
870
+ f"{', '.join(found)} exist(s): every workflow job of the repository can read "
871
+ f"it, and secrets.{KUBECONFIG_SECRET} falls back to it when an environment "
872
+ "secret is missing (bypassing the production gate)",
873
+ "delete it (`gh secret delete <name>`, or remove the organization secret's "
874
+ f"access to this repository) and keep the kubeconfig only as the "
875
+ f"{KUBECONFIG_SECRET} secret of the staging and production environments",
876
+ )
877
+ )
878
+ elif unknown:
879
+ checks.append(Check(name, INFO, False, f"not checked: {unknown[0]}"))
880
+ else:
881
+ scope = "repository or organization" if org_checked else "repository"
882
+ checks.append(
883
+ Check(
884
+ name,
885
+ OK,
886
+ False,
887
+ f"no {scope} secret named {' or '.join(REPO_KUBECONFIG_NAMES)}",
888
+ )
889
+ )
890
+ return checks
891
+
892
+
893
+ def check_github(settings: DeploySettings) -> list[Check]:
894
+ """Report (never enforce) the production environment and branch protection settings."""
895
+ if settings.cd == "skip":
896
+ return [Check("github protection", SKIP, False, "not needed: cd is skip")]
897
+ if not _github_token_available():
898
+ return [
899
+ Check(
900
+ "github protection",
901
+ INFO,
902
+ False,
903
+ "skipped: no GITHUB_TOKEN and gh is not authenticated",
904
+ )
905
+ ]
906
+ if not _kube.tool_available("gh"):
907
+ return [Check("github protection", INFO, False, "skipped: gh CLI not installed")]
908
+ try:
909
+ remote = gitops.parse_remote(gitops.origin_url())
910
+ except Exception as e:
911
+ return [Check("github protection", INFO, False, f"skipped: {e}")]
912
+ checks: list[Check] = []
913
+ repo = f"repos/{remote.owner}/{remote.repo}"
914
+
915
+ rc, prod, err = _gh_api_detail(f"{repo}/environments/production")
916
+ if rc != 0 and not _HTTP_STATUS.search(err):
917
+ # No HTTP answer at all (offline, DNS, proxy): nothing below can be checked.
918
+ return [
919
+ Check(
920
+ "github protection",
921
+ INFO,
922
+ False,
923
+ f"skipped: the GitHub API did not answer ({_first_line(err) or 'gh api failed'})",
924
+ )
925
+ ]
926
+ if rc != 0 or not isinstance(prod, dict):
927
+ checks.append(
928
+ Check(
929
+ "github env: production",
930
+ WARN,
931
+ False,
932
+ "environment not found",
933
+ "create the `production` environment with required reviewers, prevent self-review, branches restricted to main",
934
+ )
935
+ )
936
+ else:
937
+ rules = prod.get("protection_rules") or []
938
+ reviewers = next((r for r in rules if r.get("type") == "required_reviewers"), None)
939
+ has_reviewers = bool(reviewers and reviewers.get("reviewers"))
940
+ prevent_self = bool(reviewers and reviewers.get("prevent_self_review"))
941
+ policy = prod.get("deployment_branch_policy") or {}
942
+ restricted = bool(policy.get("protected_branches") or policy.get("custom_branch_policies"))
943
+ detail = (
944
+ f"reviewers: {'yes' if has_reviewers else 'no'}, prevent self-review: {'yes' if prevent_self else 'no'}, "
945
+ f"branches restricted: {'yes' if restricted else 'no'}"
946
+ )
947
+ ok = has_reviewers and prevent_self and restricted
948
+ checks.append(
949
+ Check(
950
+ "github env: production",
951
+ OK if ok else WARN,
952
+ False,
953
+ detail,
954
+ ""
955
+ if ok
956
+ else "at least one required reviewer, prevent self-review, deployment branches restricted to main",
957
+ )
958
+ )
959
+
960
+ rc, staging = _gh_api(f"{repo}/environments/staging")
961
+ if rc != 0 or not isinstance(staging, dict):
962
+ checks.append(
963
+ Check(
964
+ "github env: staging",
965
+ WARN,
966
+ False,
967
+ "environment not found",
968
+ "create the `staging` environment with deployment branches restricted to main",
969
+ )
970
+ )
971
+ else:
972
+ policy = staging.get("deployment_branch_policy") or {}
973
+ restricted = bool(policy.get("protected_branches") or policy.get("custom_branch_policies"))
974
+ checks.append(
975
+ Check(
976
+ "github env: staging",
977
+ OK if restricted else WARN,
978
+ False,
979
+ f"branches restricted: {'yes' if restricted else 'no'}",
980
+ "" if restricted else "restrict deployment branches to main",
981
+ )
982
+ )
983
+
984
+ rc, prot = _gh_api(f"{repo}/branches/main/protection")
985
+ if rc != 0 or not isinstance(prot, dict):
986
+ checks.append(
987
+ Check(
988
+ "github branch protection: main",
989
+ WARN,
990
+ False,
991
+ "no branch protection on main",
992
+ "require pull requests, code-owner review, dismiss stale approvals, pr_checks as a required status check",
993
+ )
994
+ )
995
+ else:
996
+ reviews = prot.get("required_pull_request_reviews") or {}
997
+ contexts = (prot.get("required_status_checks") or {}).get("contexts") or []
998
+ checks_list = [
999
+ c.get("context")
1000
+ for c in (prot.get("required_status_checks") or {}).get("checks") or []
1001
+ if isinstance(c, dict)
1002
+ ]
1003
+ has_pr_checks = any("pr_checks" in str(c) for c in [*contexts, *checks_list])
1004
+ code_owners = bool(reviews.get("require_code_owner_reviews"))
1005
+ dismiss = bool(reviews.get("dismiss_stale_reviews"))
1006
+ ok = bool(reviews) and code_owners and dismiss and has_pr_checks
1007
+ detail = (
1008
+ f"pull requests: {'yes' if reviews else 'no'}, code-owner review: {'yes' if code_owners else 'no'}, "
1009
+ f"dismiss stale: {'yes' if dismiss else 'no'}, pr_checks required: {'yes' if has_pr_checks else 'no'}"
1010
+ )
1011
+ checks.append(
1012
+ Check(
1013
+ "github branch protection: main",
1014
+ OK if ok else WARN,
1015
+ False,
1016
+ detail,
1017
+ "" if ok else "the branch protection settings the production gate relies on",
1018
+ )
1019
+ )
1020
+ if settings.cd == _modes.HELM_PUSH:
1021
+ checks += check_kubeconfig_secrets(repo)
1022
+ return checks
1023
+
1024
+
1025
+ def _hosted_registry(registry: str) -> bool:
1026
+ host = registry.split("/")[0].lower()
1027
+ return host in HOSTED_REGISTRY_HOSTS or host.endswith(HOSTED_REGISTRY_SUFFIXES)
1028
+
1029
+
1030
+ def check_disconnected(settings: DeploySettings, values: dict[str, Any]) -> list[Check]:
1031
+ """Disconnected profile: fail on every hosted dependency."""
1032
+ checks: list[Check] = []
1033
+ provider_ok = settings.model_provider == "openai-compatible"
1034
+ checks.append(
1035
+ Check(
1036
+ "disconnected: model provider",
1037
+ OK if provider_ok else MISSING,
1038
+ True,
1039
+ settings.model_provider,
1040
+ ""
1041
+ if provider_ok
1042
+ else "use MODEL_PROVIDER=openai-compatible with OPENAI_BASE_URL pointing at an on-network server",
1043
+ )
1044
+ )
1045
+ base_url = str(_get(values, "env", "OPENAI_BASE_URL", default="") or "")
1046
+ checks.append(
1047
+ Check(
1048
+ "disconnected: OPENAI_BASE_URL",
1049
+ OK if base_url else MISSING,
1050
+ True,
1051
+ base_url or "not set in chart env",
1052
+ ""
1053
+ if base_url
1054
+ else "set env.OPENAI_BASE_URL in values-<env>.yaml to the in-cluster model server",
1055
+ )
1056
+ )
1057
+ runtime_ok = settings.runtime == "fastapi"
1058
+ checks.append(
1059
+ Check(
1060
+ "disconnected: runtime",
1061
+ OK if runtime_ok else MISSING,
1062
+ True,
1063
+ settings.runtime,
1064
+ ""
1065
+ if runtime_ok
1066
+ else "langgraph-server needs a LangSmith license check (a hosted call); use runtime fastapi",
1067
+ )
1068
+ )
1069
+ hosted = bool(settings.registry) and _hosted_registry(settings.registry)
1070
+ checks.append(
1071
+ Check(
1072
+ "disconnected: registry",
1073
+ MISSING if hosted else OK,
1074
+ True,
1075
+ settings.registry or "(none)",
1076
+ ""
1077
+ if not hosted
1078
+ else "mirror images into an on-network registry (Harbor, registry:2) and set create_params.registry",
1079
+ )
1080
+ )
1081
+ tracing_on = _enabled(values, "tracing", "enabled")
1082
+ otlp = str(_get(values, "tracing", "otlpEndpoint", default="") or "")
1083
+ langsmith = tracing_on and not otlp
1084
+ checks.append(
1085
+ Check(
1086
+ "disconnected: tracing",
1087
+ MISSING if langsmith else OK,
1088
+ True,
1089
+ "LangSmith (hosted)" if langsmith else ("OTLP " + otlp if tracing_on else "off"),
1090
+ ""
1091
+ if not langsmith
1092
+ else "set tracing.otlpEndpoint to an in-cluster collector or disable tracing",
1093
+ )
1094
+ )
1095
+ if settings.cd == "skip":
1096
+ checks.append(
1097
+ Check(
1098
+ "disconnected: ci/cd",
1099
+ OK,
1100
+ True,
1101
+ "cd is skip: lifecycle covered by direct-mode deploy",
1102
+ )
1103
+ )
1104
+ elif gitops.enterprise_hosts():
1105
+ checks.append(
1106
+ Check(
1107
+ "disconnected: ci/cd",
1108
+ WARN,
1109
+ False,
1110
+ f"cd is {settings.cd} on GitHub Enterprise Server {', '.join(sorted(gitops.enterprise_hosts()))} (CD needs GitHub Actions on that server)",
1111
+ )
1112
+ )
1113
+ else:
1114
+ checks.append(
1115
+ Check(
1116
+ "disconnected: ci/cd",
1117
+ MISSING,
1118
+ True,
1119
+ f"cd is {settings.cd} on GitHub-hosted CI",
1120
+ "GitHub-hosted Actions are outside the profile; use cd: skip or an on-network GitHub Enterprise Server (set GH_HOST)",
1121
+ )
1122
+ )
1123
+ update_check = os.environ.get("GRAPH_AGENTS_CLI_NO_UPDATE_CHECK") == "1"
1124
+ checks.append(
1125
+ Check(
1126
+ "disconnected: update check",
1127
+ OK if update_check else WARN,
1128
+ False,
1129
+ "GRAPH_AGENTS_CLI_NO_UPDATE_CHECK=1"
1130
+ if update_check
1131
+ else "GRAPH_AGENTS_CLI_NO_UPDATE_CHECK is not set",
1132
+ "" if update_check else "export GRAPH_AGENTS_CLI_NO_UPDATE_CHECK=1",
1133
+ )
1134
+ )
1135
+ return checks
1136
+
1137
+
1138
+ def run_checks(
1139
+ settings: DeploySettings,
1140
+ env: str | None,
1141
+ profile: str | None = None,
1142
+ *,
1143
+ context_resolver: Callable[[DeploySettings, str], str | None] = _modes.resolve_context,
1144
+ ) -> Report:
1145
+ target: Target | None = None
1146
+ mode: str | None = None
1147
+ values: dict[str, Any] = load_chart_values(settings.chart_dir, env)
1148
+ if env:
1149
+ base = settings.target(env)
1150
+ target = Target(context=context_resolver(settings, env), namespace=base.namespace)
1151
+ local = None
1152
+ if settings.cd == "skip":
1153
+ # The same cluster-based detection `deploy` uses, not the context name alone.
1154
+ local = local_load.detect(target.context)[0] is not None
1155
+ mode = _modes.derive_mode(settings.cd, target.context, local=local)
1156
+ else:
1157
+ mode = _modes.derive_mode(settings.cd, None)
1158
+ report = Report(env=env, profile=profile, mode=mode)
1159
+ report.checks += check_tools(settings, mode)
1160
+ if target is not None:
1161
+ report.checks += check_cluster(settings, env or "", target, values)
1162
+ else:
1163
+ report.checks.append(Check("cluster", SKIP, False, "pass --env to check the cluster"))
1164
+ report.checks += check_placeholders(settings, values, env)
1165
+ report.checks += check_approvals(settings, values)
1166
+ report.checks += check_github(settings)
1167
+ if profile == DISCONNECTED:
1168
+ report.checks += check_disconnected(settings, values)
1169
+ return report