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,1030 @@
1
+ # Copyright 2026 Google LLC
2
+ # Modifications Copyright 2026 graph-agents-cli contributors
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # https://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ """graph-agents-cli login — preflight for provider keys, tracing, and kubeconfig.
17
+
18
+ The CLI stores no credentials, so ``login`` is a preflight check. The
19
+ command reads the project manifest when run inside a project, inspects the
20
+ process environment and the project's ``.env``, and reports whether:
21
+
22
+ * the model provider key variable (``_defaults.PROVIDER_KEY_VARS``) is set, or,
23
+ for ``openai-compatible``, whether ``OPENAI_BASE_URL`` is set and answers
24
+ ``GET /models`` (3 s timeout; reachability is advisory);
25
+ * ``LANGSMITH_API_KEY`` (or an OTLP endpoint) is present when
26
+ ``TRACING_ENABLED=true``;
27
+ * ``kubectl config current-context`` resolves (and, with ``--cluster``,
28
+ ``kubectl cluster-info`` succeeds);
29
+ * under the ``shared-bearer`` auth policy, ``API_KEY`` is set (the local
30
+ server answers 503 to every request without it);
31
+ * under ``jwt``, a verification key is set (``AUTH_JWT_JWKS_URL`` or
32
+ ``AUTH_JWT_PUBLIC_KEY``; ``auth dev-token`` makes a local one) and
33
+ ``GRAPH_AGENTS_CLI_API_KEY`` holds the token ``run`` and ``eval`` send;
34
+ * ``.env`` is not open to other users;
35
+ * under ``--profile disconnected``, nothing hosted is configured: no
36
+ hosted model provider, no LangSmith, no GitHub-hosted CI, ``fastapi`` runtime.
37
+
38
+ ``--write-env`` prompts for the missing keys and writes them to ``.env``
39
+ without echoing the values (a blank ``KEY=`` line copied from ``.env.example``
40
+ is filled in place, anything else is appended; the file is kept at mode 0600);
41
+ inside a ``shared-bearer`` project it also generates a missing ``API_KEY`` (as
42
+ ``secrets apply`` does). It always leaves ``.env`` at mode 0600, even when
43
+ nothing is missing. The CLI itself stores nothing.
44
+ """
45
+
46
+ from __future__ import annotations
47
+
48
+ import contextlib
49
+ import json
50
+ import os
51
+ import re
52
+ import subprocess
53
+ import tempfile
54
+ from collections.abc import Iterator
55
+ from dataclasses import asdict, dataclass, field
56
+ from pathlib import Path
57
+ from typing import Any
58
+
59
+ import click
60
+ import yaml
61
+
62
+ from graph_agents_cli._defaults import (
63
+ DEFAULT_AUTH_POLICY,
64
+ DEFAULT_MODEL_PROVIDER,
65
+ DEFAULT_RUNTIME,
66
+ FAKE_PROVIDER,
67
+ MODEL_PROVIDERS,
68
+ PROVIDER_KEY_VARS,
69
+ normalize_auth_policy,
70
+ )
71
+ from graph_agents_cli._output import Console
72
+ from graph_agents_cli._project import ManifestError, find_project_root
73
+ from graph_agents_cli._remote import API_KEY_ENV
74
+ from graph_agents_cli._runner import run_resolved
75
+ from graph_agents_cli._skills_check import NO_UPDATE_CHECK_ENV
76
+ from graph_agents_cli._tools import ToolNotFoundError, install_hint
77
+
78
+ MANIFEST_FILENAME = "graph-agents-cli-manifest.yaml"
79
+ PROFILES = ("default", "disconnected")
80
+ HOSTED_PROVIDERS = frozenset(p for p in MODEL_PROVIDERS if p != "openai-compatible")
81
+
82
+ # Reachability probe for openai-compatible servers (self-hosted models).
83
+ MODELS_PROBE_TIMEOUT_S = 3.0
84
+ KUBECTL_TIMEOUT_S = 5
85
+ CLUSTER_INFO_TIMEOUT_S = 15
86
+
87
+ _TRUE_VALUES = frozenset({"1", "true", "yes", "on"})
88
+ # GitHub-hosted runner labels (disconnected: only an on-network GHES keeps CD in profile).
89
+ _HOSTED_RUNNER_RE = re.compile(
90
+ r"runs-on:\s*\[?\s*['\"]?(ubuntu|windows|macos)-(latest|\d+(\.\d+)?)", re.IGNORECASE
91
+ )
92
+
93
+ # Status values, ordered by severity for the summary.
94
+ OK, WARN, FAIL, SKIP = "ok", "warn", "fail", "skip"
95
+
96
+
97
+ @dataclass
98
+ class Check:
99
+ """One preflight result. ``detail`` never contains a secret value."""
100
+
101
+ name: str
102
+ status: str
103
+ detail: str
104
+ hint: str = ""
105
+
106
+
107
+ @dataclass
108
+ class ProjectInfo:
109
+ """What `login` learned from the manifest (all fields optional)."""
110
+
111
+ root: str | None = None
112
+ name: str | None = None
113
+ provider: str = DEFAULT_MODEL_PROVIDER
114
+ provider_source: str = "default"
115
+ runtime: str = DEFAULT_RUNTIME
116
+ deployment_target: str = "kubernetes"
117
+ cd: str = "skip"
118
+ auth_policy: str = DEFAULT_AUTH_POLICY
119
+ environments: dict[str, dict[str, str]] = field(default_factory=dict)
120
+
121
+
122
+ @dataclass
123
+ class EnvView:
124
+ """Merged view of the process environment and the project's .env.
125
+
126
+ The process environment wins over the file. ``sources`` records where each
127
+ present key came from so the report can say "set (environment)" without
128
+ printing the value.
129
+ """
130
+
131
+ values: dict[str, str]
132
+ sources: dict[str, str]
133
+ env_file: Path
134
+
135
+ def get(self, key: str) -> str:
136
+ return self.values.get(key, "")
137
+
138
+ def has(self, key: str) -> bool:
139
+ return bool(self.values.get(key, "").strip())
140
+
141
+ def where(self, key: str) -> str:
142
+ return self.sources.get(key, "unset")
143
+
144
+ def truthy(self, key: str) -> bool:
145
+ return self.get(key).strip().lower() in _TRUE_VALUES
146
+
147
+
148
+ # ── Inputs ──────────────────────────────────────────────────────────────────
149
+
150
+
151
+ def _read_manifest(root: Path) -> dict[str, Any]:
152
+ path = root / MANIFEST_FILENAME
153
+ try:
154
+ data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
155
+ except (OSError, yaml.YAMLError) as e:
156
+ raise ManifestError(f"Could not read {path}: {e}") from e
157
+ if not isinstance(data, dict):
158
+ raise ManifestError(f"malformed {MANIFEST_FILENAME}")
159
+ return data
160
+
161
+
162
+ def load_project_info(cwd: Path | None = None) -> ProjectInfo:
163
+ """Read the manifest of the enclosing project, if any."""
164
+ root = find_project_root(cwd)
165
+ if root is None:
166
+ return ProjectInfo()
167
+ data = _read_manifest(root)
168
+ params = data.get("create_params") or {}
169
+ if not isinstance(params, dict):
170
+ params = {}
171
+ info = ProjectInfo(root=str(root), name=data.get("name"))
172
+ provider = params.get("model_provider")
173
+ if provider:
174
+ info.provider = str(provider)
175
+ info.provider_source = "manifest"
176
+ info.runtime = str(params.get("runtime") or DEFAULT_RUNTIME)
177
+ info.deployment_target = str(params.get("deployment_target") or "kubernetes")
178
+ info.cd = str(params.get("cd") or "skip")
179
+ info.auth_policy = normalize_auth_policy(params.get("auth_policy"), warn=False)
180
+ envs = data.get("environments") or {}
181
+ if isinstance(envs, dict):
182
+ info.environments = {
183
+ str(k): {str(a): str(b or "") for a, b in (v or {}).items()}
184
+ for k, v in envs.items()
185
+ if isinstance(v, dict)
186
+ }
187
+ return info
188
+
189
+
190
+ def load_env(env_file: Path) -> EnvView:
191
+ """Merge ``env_file`` (when present) under the process environment."""
192
+ from dotenv import dotenv_values
193
+
194
+ values: dict[str, str] = {}
195
+ sources: dict[str, str] = {}
196
+ if env_file.is_file():
197
+ try:
198
+ for key, value in dotenv_values(env_file).items():
199
+ if value is None:
200
+ continue
201
+ values[key] = value
202
+ sources[key] = env_file.name
203
+ except Exception as e: # dotenv raises plain exceptions on bad files
204
+ raise click.ClickException(f"Could not parse {env_file}: {e}") from e
205
+ for key, value in os.environ.items():
206
+ if value:
207
+ values[key] = value
208
+ sources[key] = "environment"
209
+ return EnvView(values=values, sources=sources, env_file=env_file)
210
+
211
+
212
+ def resolve_provider(info: ProjectInfo, env: EnvView) -> tuple[str, str]:
213
+ """Provider precedence: MODEL_PROVIDER (environment or .env), then the manifest, then the default.
214
+
215
+ The manifest records the provider chosen at scaffold time; the app itself
216
+ reads ``MODEL_PROVIDER`` at runtime, so a value in the
217
+ environment or ``.env`` (for example ``fake`` for a local check) is what the
218
+ preflight must judge.
219
+ """
220
+ if env.has("MODEL_PROVIDER"):
221
+ return env.get("MODEL_PROVIDER").strip(), env.where("MODEL_PROVIDER")
222
+ if info.provider_source == "manifest":
223
+ return info.provider, "manifest"
224
+ return DEFAULT_MODEL_PROVIDER, "default"
225
+
226
+
227
+ # ── Checks ──────────────────────────────────────────────────────────────────
228
+
229
+
230
+ def _models_url(base_url: str) -> str:
231
+ return base_url.rstrip("/") + "/models"
232
+
233
+
234
+ def probe_openai_compatible(base_url: str, api_key: str = "") -> Check:
235
+ """GET ``<OPENAI_BASE_URL>/models`` with a short timeout. Advisory only."""
236
+ import httpx
237
+
238
+ from graph_agents_cli import _http
239
+
240
+ url = _models_url(base_url)
241
+ headers = {"Authorization": f"Bearer {api_key}"} if api_key else {}
242
+ try:
243
+ response = _http.get(url, headers=headers, timeout=MODELS_PROBE_TIMEOUT_S)
244
+ except _http.ProxyConfigError as e:
245
+ return Check(
246
+ "openai_base_url",
247
+ WARN,
248
+ f"OPENAI_BASE_URL set but {url} was not probed: {e.message}",
249
+ "Fix the proxy setting it names; the check is advisory.",
250
+ )
251
+ except httpx.HTTPError as e:
252
+ return Check(
253
+ "openai_base_url",
254
+ WARN,
255
+ f"OPENAI_BASE_URL set but {url} is unreachable ({type(e).__name__})",
256
+ "Start the on-network model server or fix OPENAI_BASE_URL; the check is advisory.",
257
+ )
258
+ if response.status_code in (401, 403):
259
+ return Check(
260
+ "openai_base_url",
261
+ WARN,
262
+ f"{url} reachable but rejected the credentials (HTTP {response.status_code})",
263
+ "Set MODEL_API_KEY to a key the server accepts.",
264
+ )
265
+ if response.status_code >= 400:
266
+ return Check(
267
+ "openai_base_url",
268
+ WARN,
269
+ f"{url} answered HTTP {response.status_code}",
270
+ "Check that OPENAI_BASE_URL points at the API root (ending in /v1 for most servers).",
271
+ )
272
+ count = ""
273
+ try:
274
+ data = response.json().get("data")
275
+ if isinstance(data, list):
276
+ count = f", {len(data)} model(s) listed"
277
+ except (ValueError, AttributeError):
278
+ pass
279
+ return Check("openai_base_url", OK, f"{url} reachable (HTTP {response.status_code}{count})")
280
+
281
+
282
+ def check_provider(provider: str, source: str, env: EnvView, *, profile: str) -> list[Check]:
283
+ checks: list[Check] = []
284
+ if provider == FAKE_PROVIDER:
285
+ # Test-only deterministic model: no key, no network,
286
+ # allowed under every profile. Never offered by `create`.
287
+ checks.append(
288
+ Check(
289
+ "provider",
290
+ WARN,
291
+ f"model provider {FAKE_PROVIDER} ({source}) is the test-only fake model",
292
+ "Set MODEL_PROVIDER to a real provider before deploying.",
293
+ )
294
+ )
295
+ checks.append(Check("provider_key", SKIP, "no key needed for the fake provider"))
296
+ return checks
297
+ if provider not in MODEL_PROVIDERS:
298
+ checks.append(
299
+ Check(
300
+ "provider",
301
+ FAIL,
302
+ f"unknown model provider {provider!r} ({source})",
303
+ f"Use one of: {', '.join(MODEL_PROVIDERS)}.",
304
+ )
305
+ )
306
+ return checks
307
+
308
+ if profile == "disconnected" and provider in HOSTED_PROVIDERS:
309
+ checks.append(
310
+ Check(
311
+ "provider",
312
+ FAIL,
313
+ f"model provider {provider} ({source}) is hosted; the disconnected profile "
314
+ "requires openai-compatible",
315
+ "Set MODEL_PROVIDER=openai-compatible and OPENAI_BASE_URL to an on-network server.",
316
+ )
317
+ )
318
+ else:
319
+ checks.append(Check("provider", OK, f"model provider {provider} ({source})"))
320
+
321
+ key_var = PROVIDER_KEY_VARS[provider]
322
+ if env.has(key_var):
323
+ checks.append(Check("provider_key", OK, f"{key_var} set ({env.where(key_var)})"))
324
+ elif provider == "openai-compatible":
325
+ checks.append(
326
+ Check(
327
+ "provider_key",
328
+ WARN,
329
+ f"{key_var} not set (optional for servers that do not require a key)",
330
+ f"Set {key_var} in .env or run 'graph-agents-cli login --write-env'.",
331
+ )
332
+ )
333
+ else:
334
+ checks.append(
335
+ Check(
336
+ "provider_key",
337
+ FAIL,
338
+ f"{key_var} not set",
339
+ f"Export {key_var}, add it to .env, or run 'graph-agents-cli login --write-env'.",
340
+ )
341
+ )
342
+
343
+ if provider == "openai-compatible":
344
+ if env.has("OPENAI_BASE_URL"):
345
+ checks.append(probe_openai_compatible(env.get("OPENAI_BASE_URL"), env.get(key_var)))
346
+ else:
347
+ checks.append(
348
+ Check(
349
+ "openai_base_url",
350
+ FAIL,
351
+ "OPENAI_BASE_URL not set (required for openai-compatible)",
352
+ "Set OPENAI_BASE_URL to the server's API root, e.g. http://ollama:11434/v1.",
353
+ )
354
+ )
355
+ return checks
356
+
357
+
358
+ def check_judge(env: EnvView, *, profile: str) -> Check:
359
+ judge_provider = env.get("JUDGE_MODEL_PROVIDER").strip()
360
+ if not judge_provider:
361
+ return Check("judge", SKIP, "judge defaults to the agent's provider and key")
362
+ if judge_provider == FAKE_PROVIDER:
363
+ return Check("judge", OK, f"judge provider {FAKE_PROVIDER} (test-only; no key needed)")
364
+ if judge_provider not in MODEL_PROVIDERS:
365
+ return Check(
366
+ "judge",
367
+ FAIL,
368
+ f"unknown JUDGE_MODEL_PROVIDER {judge_provider!r}",
369
+ f"Use one of: {', '.join(MODEL_PROVIDERS)}.",
370
+ )
371
+ if profile == "disconnected" and judge_provider in HOSTED_PROVIDERS:
372
+ return Check(
373
+ "judge",
374
+ FAIL,
375
+ f"JUDGE_MODEL_PROVIDER={judge_provider} is hosted",
376
+ "Point JUDGE_MODEL_PROVIDER=openai-compatible and JUDGE_BASE_URL at an on-network server.",
377
+ )
378
+ key_var = PROVIDER_KEY_VARS[judge_provider]
379
+ if env.has("JUDGE_API_KEY") or env.has(key_var):
380
+ return Check("judge", OK, f"judge provider {judge_provider}, key present")
381
+ if judge_provider == "openai-compatible":
382
+ return Check("judge", OK, f"judge provider {judge_provider} (no key; optional)")
383
+ return Check(
384
+ "judge",
385
+ WARN,
386
+ f"judge provider {judge_provider} but neither JUDGE_API_KEY nor {key_var} is set",
387
+ "Set JUDGE_API_KEY (defaults to the provider key when the providers match).",
388
+ )
389
+
390
+
391
+ def check_tracing(env: EnvView, *, profile: str) -> Check:
392
+ enabled = env.truthy("TRACING_ENABLED")
393
+ has_langsmith = env.has("LANGSMITH_API_KEY")
394
+ has_otlp = env.has("OTEL_EXPORTER_OTLP_ENDPOINT")
395
+
396
+ if profile == "disconnected" and has_langsmith:
397
+ return Check(
398
+ "tracing",
399
+ FAIL,
400
+ f"LANGSMITH_API_KEY set ({env.where('LANGSMITH_API_KEY')}); LangSmith is a hosted dependency",
401
+ "Unset LANGSMITH_API_KEY and export traces over OTLP to an in-cluster collector, or disable tracing.",
402
+ )
403
+ if not enabled:
404
+ return Check("tracing", SKIP, "TRACING_ENABLED is not true; tracing off")
405
+ if profile == "disconnected":
406
+ if has_otlp:
407
+ return Check("tracing", OK, "tracing enabled with OTEL_EXPORTER_OTLP_ENDPOINT")
408
+ return Check(
409
+ "tracing",
410
+ FAIL,
411
+ "TRACING_ENABLED=true without OTEL_EXPORTER_OTLP_ENDPOINT",
412
+ "Set OTEL_EXPORTER_OTLP_ENDPOINT to an on-network collector or set TRACING_ENABLED=false.",
413
+ )
414
+ if has_langsmith:
415
+ return Check(
416
+ "tracing",
417
+ OK,
418
+ f"tracing enabled; LANGSMITH_API_KEY set ({env.where('LANGSMITH_API_KEY')})",
419
+ )
420
+ if has_otlp:
421
+ return Check(
422
+ "tracing", OK, "tracing enabled with OTEL_EXPORTER_OTLP_ENDPOINT (no LangSmith)"
423
+ )
424
+ return Check(
425
+ "tracing",
426
+ FAIL,
427
+ "TRACING_ENABLED=true but neither LANGSMITH_API_KEY nor OTEL_EXPORTER_OTLP_ENDPOINT is set",
428
+ "Set LANGSMITH_API_KEY (or an OTLP endpoint), or set TRACING_ENABLED=false.",
429
+ )
430
+
431
+
432
+ def _kubectl(args: list[str], timeout: int) -> subprocess.CompletedProcess:
433
+ return run_resolved(
434
+ ["kubectl", *args],
435
+ capture_output=True,
436
+ text=True,
437
+ encoding="utf-8",
438
+ errors="replace",
439
+ timeout=timeout,
440
+ )
441
+
442
+
443
+ def check_kubeconfig(info: ProjectInfo, *, cluster: bool) -> list[Check]:
444
+ if info.root is not None and info.deployment_target == "none":
445
+ return [Check("kubeconfig", SKIP, "deployment target is none")]
446
+
447
+ severity = FAIL if cluster else WARN
448
+ try:
449
+ result = _kubectl(["config", "current-context"], KUBECTL_TIMEOUT_S)
450
+ except ToolNotFoundError:
451
+ return [Check("kubeconfig", severity, "kubectl not found on PATH", install_hint("kubectl"))]
452
+ except (OSError, subprocess.SubprocessError) as e:
453
+ return [Check("kubeconfig", severity, f"kubectl failed: {e}")]
454
+
455
+ if result.returncode != 0:
456
+ return [
457
+ Check(
458
+ "kubeconfig",
459
+ severity,
460
+ "no current kube context (kubectl config current-context failed)",
461
+ "Set a context with 'kubectl config use-context <name>' or point KUBECONFIG at a file.",
462
+ )
463
+ ]
464
+ context = (result.stdout or "").strip()
465
+ detail = f"current context: {context}"
466
+ expected = {
467
+ env_name: cfg.get("context", "")
468
+ for env_name, cfg in info.environments.items()
469
+ if cfg.get("context")
470
+ }
471
+ matches = [env_name for env_name, ctx in expected.items() if ctx == context]
472
+ if matches:
473
+ detail += f" (manifest environment: {', '.join(matches)})"
474
+ checks = [Check("kubeconfig", OK, detail)]
475
+
476
+ if cluster:
477
+ try:
478
+ result = _kubectl(["cluster-info"], CLUSTER_INFO_TIMEOUT_S)
479
+ except (OSError, subprocess.SubprocessError) as e:
480
+ checks.append(Check("cluster", FAIL, f"kubectl cluster-info failed: {e}"))
481
+ return checks
482
+ if result.returncode == 0:
483
+ checks.append(Check("cluster", OK, f"cluster reachable via context {context}"))
484
+ else:
485
+ err = (result.stderr or result.stdout or "").strip().splitlines()
486
+ checks.append(
487
+ Check(
488
+ "cluster",
489
+ FAIL,
490
+ f"cluster unreachable via context {context}: {err[0] if err else 'unknown error'}",
491
+ "Check VPN / kubeconfig credentials; 'kubectl cluster-info' must succeed before deploy.",
492
+ )
493
+ )
494
+ return checks
495
+
496
+
497
+ def _hosted_ci_indicators(root: Path | None, env: EnvView) -> list[str]:
498
+ """Return what makes this project depend on GitHub-hosted CI."""
499
+ indicators: list[str] = []
500
+ if env.truthy("GITHUB_ACTIONS"):
501
+ indicators.append("running under GITHUB_ACTIONS")
502
+ if root is None:
503
+ return indicators
504
+ workflows = root / ".github" / "workflows"
505
+ if not workflows.is_dir():
506
+ return indicators
507
+ for path in sorted(workflows.iterdir()):
508
+ if path.suffix not in (".yml", ".yaml"):
509
+ continue
510
+ try:
511
+ text = path.read_text(encoding="utf-8")
512
+ except OSError:
513
+ continue
514
+ if _HOSTED_RUNNER_RE.search(text):
515
+ indicators.append(f"{path.relative_to(root)} uses a GitHub-hosted runner")
516
+ return indicators
517
+
518
+
519
+ def check_disconnected_profile(info: ProjectInfo, env: EnvView) -> list[Check]:
520
+ """Extra checks that only apply under ``--profile disconnected``."""
521
+ checks: list[Check] = []
522
+
523
+ if info.root is not None:
524
+ if info.runtime == "fastapi":
525
+ checks.append(Check("profile.runtime", OK, "runtime fastapi"))
526
+ else:
527
+ checks.append(
528
+ Check(
529
+ "profile.runtime",
530
+ FAIL,
531
+ f"runtime {info.runtime} needs a LangSmith license check at startup",
532
+ "Scaffold with --runtime fastapi for the disconnected profile.",
533
+ )
534
+ )
535
+
536
+ indicators = _hosted_ci_indicators(Path(info.root) if info.root else None, env)
537
+ if indicators:
538
+ checks.append(
539
+ Check(
540
+ "profile.ci",
541
+ FAIL,
542
+ "GitHub-hosted CI detected: " + "; ".join(indicators),
543
+ "CI/CD is outside the disconnected profile unless an on-network GitHub Enterprise "
544
+ "Server hosts Actions; use 'cd: skip' with direct-mode deploy.",
545
+ )
546
+ )
547
+ elif info.root is not None and info.cd != "skip":
548
+ checks.append(
549
+ Check(
550
+ "profile.ci",
551
+ WARN,
552
+ f"cd: {info.cd} needs an on-network GitHub Enterprise Server",
553
+ "Confirm the Actions host is on-network, or switch to 'cd: skip'.",
554
+ )
555
+ )
556
+ else:
557
+ checks.append(Check("profile.ci", OK, "no GitHub-hosted CI dependency"))
558
+
559
+ if os.environ.get(NO_UPDATE_CHECK_ENV) == "1":
560
+ checks.append(Check("profile.update_check", OK, f"{NO_UPDATE_CHECK_ENV}=1"))
561
+ else:
562
+ checks.append(
563
+ Check(
564
+ "profile.update_check",
565
+ WARN,
566
+ f"{NO_UPDATE_CHECK_ENV} is not 1; the CLI will try the GitHub release check "
567
+ "and npx on each run",
568
+ f"Export {NO_UPDATE_CHECK_ENV}=1.",
569
+ )
570
+ )
571
+ return checks
572
+
573
+
574
+ def needs_api_key(info: ProjectInfo, env: EnvView) -> bool:
575
+ """True inside a project whose effective auth policy is shared-bearer and API_KEY is unset.
576
+
577
+ ``AUTH_POLICY`` from the environment or ``.env`` (what the app reads at
578
+ runtime) wins over the manifest's ``create_params.auth_policy``.
579
+ """
580
+ if info.root is None or env.has("API_KEY"):
581
+ return False
582
+ policy = env.get("AUTH_POLICY").strip() or info.auth_policy
583
+ return policy == "shared-bearer"
584
+
585
+
586
+ def check_api_key(info: ProjectInfo, env: EnvView) -> Check | None:
587
+ if info.root is None:
588
+ return None
589
+ if needs_api_key(info, env):
590
+ return Check(
591
+ "api_key",
592
+ WARN,
593
+ "API_KEY unset; under AUTH_POLICY=shared-bearer the local server answers 503 "
594
+ "to every request (run, eval, playground)",
595
+ "Run 'graph-agents-cli login --write-env' to generate one into .env.",
596
+ )
597
+ if env.has("API_KEY"):
598
+ return Check("api_key", OK, f"API_KEY set ({env.where('API_KEY')})")
599
+ return None
600
+
601
+
602
+ def effective_auth_policy(info: ProjectInfo, env: EnvView) -> str | None:
603
+ """The policy the local server runs: ``AUTH_POLICY`` (environment or .env), else the manifest."""
604
+ if info.root is None:
605
+ return None
606
+ return normalize_auth_policy(env.get("AUTH_POLICY").strip() or info.auth_policy, warn=False)
607
+
608
+
609
+ DEV_TOKEN_EXPORT = (
610
+ 'export GRAPH_AGENTS_CLI_API_KEY="$(graph-agents-cli auth dev-token --sub <user>)"'
611
+ )
612
+
613
+
614
+ def check_jwt(info: ProjectInfo, env: EnvView) -> list[Check]:
615
+ """Under ``jwt``: can the local server verify tokens, and will run/eval send one?"""
616
+ if effective_auth_policy(info, env) != "jwt":
617
+ return []
618
+ checks: list[Check] = []
619
+ key_vars = [name for name in ("AUTH_JWT_JWKS_URL", "AUTH_JWT_PUBLIC_KEY") if env.has(name)]
620
+ if not key_vars:
621
+ checks.append(
622
+ Check(
623
+ "jwt_key",
624
+ WARN,
625
+ "AUTH_POLICY=jwt without AUTH_JWT_JWKS_URL or AUTH_JWT_PUBLIC_KEY: the local "
626
+ "server answers 503 to every request (run, eval, playground)",
627
+ "For local runs: 'graph-agents-cli auth dev-token --sub <user>' writes a dev "
628
+ "key, issuer and audience to .env and prints a token. Or set AUTH_JWT_JWKS_URL "
629
+ "to your issuer's keys.",
630
+ )
631
+ )
632
+ else:
633
+ sources = ", ".join(f"{name} ({env.where(name)})" for name in key_vars)
634
+ checks.append(Check("jwt_key", OK, f"verification key: {sources}"))
635
+ missing = [n for n in ("AUTH_JWT_ISSUER", "AUTH_JWT_AUDIENCE") if not env.has(n)]
636
+ if missing and env.get("APP_ENV") != "dev":
637
+ checks.append(
638
+ Check(
639
+ "jwt_claims",
640
+ FAIL,
641
+ f"{' and '.join(missing)} unset: outside APP_ENV=dev the server refuses to "
642
+ "start without them",
643
+ "Set them in .env (APP_ENV=dev relaxes this for local runs).",
644
+ )
645
+ )
646
+ # run and eval read the token from the process environment only (never .env).
647
+ if os.environ.get(API_KEY_ENV, "").strip():
648
+ checks.append(
649
+ Check("jwt_token", OK, f"{API_KEY_ENV} set (environment): run and eval send it")
650
+ )
651
+ else:
652
+ checks.append(
653
+ Check(
654
+ "jwt_token",
655
+ WARN,
656
+ f"{API_KEY_ENV} is not set: a local run or eval sends no token (401 Missing "
657
+ "bearer token)",
658
+ f"Put a token there (kept out of argv and shell history): {DEV_TOKEN_EXPORT}",
659
+ )
660
+ )
661
+ return checks
662
+
663
+
664
+ def env_file_mode_problem(path: Path) -> str | None:
665
+ """Why ``path`` is open to other users (group/world bits), or None."""
666
+ if os.name == "nt" or not path.is_file():
667
+ return None
668
+ mode = path.stat().st_mode & 0o777
669
+ if mode & 0o077:
670
+ return f"{path.name} is readable or writable by other users (mode {mode:04o})"
671
+ return None
672
+
673
+
674
+ def check_env_file(env: EnvView) -> Check | None:
675
+ problem = env_file_mode_problem(env.env_file)
676
+ if problem is None:
677
+ return None
678
+ return Check(
679
+ "env_file",
680
+ WARN,
681
+ f"{problem}; it holds keys",
682
+ f"chmod 600 {env.env_file.name} (or run 'graph-agents-cli login --write-env', which "
683
+ "keeps it 0600).",
684
+ )
685
+
686
+
687
+ def run_preflight(info: ProjectInfo, env: EnvView, *, profile: str, cluster: bool) -> list[Check]:
688
+ provider, source = resolve_provider(info, env)
689
+ checks = check_provider(provider, source, env, profile=profile)
690
+ api_key = check_api_key(info, env)
691
+ if api_key is not None:
692
+ checks.append(api_key)
693
+ checks.extend(check_jwt(info, env))
694
+ env_file = check_env_file(env)
695
+ if env_file is not None:
696
+ checks.append(env_file)
697
+ checks.append(check_judge(env, profile=profile))
698
+ checks.append(check_tracing(env, profile=profile))
699
+ checks.extend(check_kubeconfig(info, cluster=cluster))
700
+ if profile == "disconnected":
701
+ checks.extend(check_disconnected_profile(info, env))
702
+ return checks
703
+
704
+
705
+ # ── --write-env ─────────────────────────────────────────────────────────────
706
+
707
+
708
+ def missing_env_keys(info: ProjectInfo, env: EnvView, *, profile: str) -> list[tuple[str, bool]]:
709
+ """Keys ``--write-env`` should ask for: ``(name, is_secret)`` in prompt order."""
710
+ provider, _ = resolve_provider(info, env)
711
+ wanted: list[tuple[str, bool]] = []
712
+ if provider in PROVIDER_KEY_VARS:
713
+ if provider == "openai-compatible":
714
+ wanted.append(("OPENAI_BASE_URL", False))
715
+ wanted.append((PROVIDER_KEY_VARS[provider], True))
716
+ if env.truthy("TRACING_ENABLED") and profile != "disconnected":
717
+ if not env.has("OTEL_EXPORTER_OTLP_ENDPOINT"):
718
+ wanted.append(("LANGSMITH_API_KEY", True))
719
+ return [(name, secret) for name, secret in wanted if not env.has(name)]
720
+
721
+
722
+ _ENV_ASSIGNMENT = re.compile(r"^(?P<lead>\s*(?:export\s+)?)(?P<key>[A-Za-z_][A-Za-z0-9_]*)\s*=")
723
+ _BLANK_VALUES = frozenset({"", '""', "''"})
724
+ ENV_FILE_MODE = 0o600
725
+
726
+
727
+ def write_env(env_file: Path, entries: dict[str, str]) -> None:
728
+ """Set ``KEY=value`` in ``env_file``: fill blank ``KEY=`` lines in place, append the rest.
729
+
730
+ ``cp .env.example .env`` leaves ``API_KEY=`` (and a blank provider key)
731
+ in the file; appending a second line would leave two assignments. Every
732
+ blank assignment of a key gets the value (a later blank one would
733
+ otherwise win), and a key the file does not assign is appended. A key the
734
+ file already sets (another run got there first) is left as it is, never
735
+ assigned twice. The file holds credentials: it is written atomically with
736
+ mode 0600 (a symlinked ``.env`` is written through to its target), and
737
+ concurrent writers take turns (the file is read again under the lock).
738
+ """
739
+ if not entries:
740
+ return
741
+ target = env_file.resolve() if env_file.is_symlink() else env_file
742
+ target.parent.mkdir(parents=True, exist_ok=True)
743
+ with _env_file_lock(target):
744
+ existing = ""
745
+ if target.is_file():
746
+ # newline="": keep CRLF files CRLF (no universal-newline translation).
747
+ with target.open(encoding="utf-8", newline="") as handle:
748
+ existing = handle.read()
749
+ lines = existing.splitlines(keepends=True)
750
+ pending = dict(entries)
751
+ filled: set[str] = set()
752
+ already_set: set[str] = set()
753
+ for index, line in enumerate(lines):
754
+ match = _ENV_ASSIGNMENT.match(line)
755
+ if not match or match.group("key") not in pending:
756
+ continue
757
+ key = match.group("key")
758
+ value = line[match.end() :].strip()
759
+ # Blank as python-dotenv (every reader) sees it: `KEY=`, `KEY="" # note`.
760
+ # An unquoted `KEY= # note` is the value "# note" to dotenv, so it is set.
761
+ if value.split(" #", 1)[0].strip() not in _BLANK_VALUES:
762
+ already_set.add(key)
763
+ continue
764
+ ending = line[len(line.rstrip("\r\n")) :] # keep the file's own line ending
765
+ lines[index] = f"{match.group('lead')}{key}={pending[key]}{ending}"
766
+ filled.add(key)
767
+ text = "".join(lines)
768
+ appended = [
769
+ f"{k}={v}" for k, v in pending.items() if k not in filled and k not in already_set
770
+ ]
771
+ if appended:
772
+ if text and not text.endswith("\n"):
773
+ text += "\n"
774
+ text += "\n".join(appended) + "\n"
775
+ _write_private(target, text)
776
+
777
+
778
+ @contextlib.contextmanager
779
+ def _env_file_lock(target: Path) -> Iterator[None]:
780
+ """Serialise read-modify-write of ``target`` between processes (POSIX; else no lock).
781
+
782
+ The lock is a file in the project's state directory beside it
783
+ (``.graph-agents-cli/<name>.lock``, git and docker ignored, left in place),
784
+ never the env file itself, which a write replaces.
785
+ """
786
+ try:
787
+ import fcntl
788
+ except ImportError: # Windows: writes are still atomic, only not serialised
789
+ yield
790
+ return
791
+ lock_dir = target.parent / ".graph-agents-cli"
792
+ lock_dir.mkdir(exist_ok=True)
793
+ lock_path = lock_dir / f"{target.name.lstrip('.') or 'env'}.lock"
794
+ fd = os.open(lock_path, os.O_RDWR | os.O_CREAT, ENV_FILE_MODE)
795
+ try:
796
+ fcntl.flock(fd, fcntl.LOCK_EX)
797
+ yield
798
+ finally:
799
+ os.close(fd) # closing releases the lock
800
+
801
+
802
+ def _keep_private(path: Path, console: Console) -> None:
803
+ """Make an existing env file owner-only (0600), as a write would leave it."""
804
+ problem = env_file_mode_problem(path)
805
+ if problem is None:
806
+ return
807
+ target = path.resolve() if path.is_symlink() else path
808
+ try:
809
+ target.chmod(ENV_FILE_MODE)
810
+ except OSError as exc:
811
+ console.print(f" {problem}; could not make it 0600: {exc}", style="yellow")
812
+ return
813
+ console.print(f" {problem}: now 0600 (owner only).", style="green", highlight=False)
814
+
815
+
816
+ def _write_private(path: Path, text: str) -> None:
817
+ """Replace ``path`` with ``text``, readable by the owner only (0600)."""
818
+ fd, tmp_name = tempfile.mkstemp(prefix=f".{path.name}.", suffix=".tmp", dir=path.parent)
819
+ try:
820
+ with os.fdopen(fd, "w", encoding="utf-8", newline="") as handle:
821
+ handle.write(text)
822
+ os.chmod(tmp_name, ENV_FILE_MODE)
823
+ os.replace(tmp_name, path)
824
+ except BaseException:
825
+ with contextlib.suppress(OSError):
826
+ os.unlink(tmp_name)
827
+ raise
828
+
829
+
830
+ def prompt_and_write_env(info: ProjectInfo, env: EnvView, *, profile: str, console: Console) -> int:
831
+ """Prompt for each missing key and write the answers to ``env.env_file`` (see ``write_env``).
832
+
833
+ Secret values are read with ``hide_input`` and never printed. An empty
834
+ answer skips that key. A missing ``API_KEY`` under the shared-bearer policy
835
+ is generated rather than prompted for. When stdin runs out (a CI job, a
836
+ pipe), prompting stops and whatever was collected, the generated key
837
+ included, is still written. Returns the number of keys written.
838
+ """
839
+ from graph_agents_cli.secrets._apply import GENERATED_KEY, generate_api_key
840
+
841
+ missing = missing_env_keys(info, env, profile=profile)
842
+ generate = needs_api_key(info, env)
843
+ if not missing and not generate:
844
+ console.print(" Nothing to write: every required key is already set.", style="dim")
845
+ _keep_private(env.env_file, console)
846
+ return 0
847
+ console.print()
848
+ console.print(f" Writing missing keys to {env.env_file} (leave blank to skip).", style="bold")
849
+ entries: dict[str, str] = {}
850
+ if generate:
851
+ entries[GENERATED_KEY] = generate_api_key()
852
+ console.print(
853
+ f" Generated {GENERATED_KEY} for AUTH_POLICY=shared-bearer (value not shown).",
854
+ style="green",
855
+ )
856
+ skipped: list[str] = []
857
+ for position, (name, secret) in enumerate(missing):
858
+ try:
859
+ value = click.prompt(
860
+ f" {name}",
861
+ default="",
862
+ show_default=False,
863
+ hide_input=secret,
864
+ ).strip()
865
+ except click.Abort:
866
+ # EOF on stdin: nothing more to read. Keep what was collected.
867
+ skipped = [n for n, _ in missing[position:]]
868
+ click.echo("", err=True)
869
+ break
870
+ if value:
871
+ entries[name] = value
872
+ write_env(env.env_file, entries)
873
+ if skipped:
874
+ console.print(
875
+ f" No input for {', '.join(skipped)} (stdin closed); set them in "
876
+ f"{env.env_file} or rerun `graph-agents-cli login --write-env` in a terminal.",
877
+ style="yellow",
878
+ )
879
+ if entries:
880
+ console.print(
881
+ f" Wrote {len(entries)} key(s) to {env.env_file} (values not shown).", style="green"
882
+ )
883
+ else:
884
+ console.print(" No keys written.", style="dim")
885
+ return len(entries)
886
+
887
+
888
+ # ── Report ──────────────────────────────────────────────────────────────────
889
+
890
+ _ICONS = {OK: ("✓", "green"), WARN: ("!", "yellow"), FAIL: ("✗", "red"), SKIP: ("-", "dim")}
891
+
892
+
893
+ def summarize(checks: list[Check]) -> dict[str, int]:
894
+ summary = {OK: 0, WARN: 0, FAIL: 0, SKIP: 0}
895
+ for check in checks:
896
+ summary[check.status] += 1
897
+ return summary
898
+
899
+
900
+ def build_report(
901
+ info: ProjectInfo, env: EnvView, checks: list[Check], *, profile: str
902
+ ) -> dict[str, Any]:
903
+ provider, source = resolve_provider(info, env)
904
+ summary = summarize(checks)
905
+ return {
906
+ "profile": profile,
907
+ "project": {
908
+ "root": info.root,
909
+ "name": info.name,
910
+ "provider": provider,
911
+ "provider_source": source,
912
+ "runtime": info.runtime if info.root else None,
913
+ "deployment_target": info.deployment_target if info.root else None,
914
+ "cd": info.cd if info.root else None,
915
+ },
916
+ "env_file": str(env.env_file),
917
+ "checks": [asdict(c) for c in checks],
918
+ "summary": summary,
919
+ "ok": summary[FAIL] == 0,
920
+ }
921
+
922
+
923
+ def print_report(report: dict[str, Any], console: Console) -> None:
924
+ project = report["project"]
925
+ console.print()
926
+ console.print("Preflight", style="cyan bold")
927
+ console.print()
928
+ if project["root"]:
929
+ console.print(f" Project: {project['name'] or '?'} ({project['root']})")
930
+ console.print(
931
+ f" Runtime: {project['runtime']} target: {project['deployment_target']}"
932
+ f" cd: {project['cd']}"
933
+ )
934
+ else:
935
+ console.print(" Project: none (not inside a graph-agents-cli project)", style="dim")
936
+ console.print(f" Provider: {project['provider']} ({project['provider_source']})")
937
+ console.print(f" Env file: {report['env_file']}")
938
+ console.print(f" Profile: {report['profile']}")
939
+ console.print()
940
+ for check in report["checks"]:
941
+ icon, style = _ICONS[check["status"]]
942
+ console.print(f" [{style}]{icon}[/] {check['name']}: {check['detail']}", highlight=False)
943
+ if check["hint"] and check["status"] in (WARN, FAIL):
944
+ console.print(f" {check['hint']}", style="dim", highlight=False)
945
+ console.print()
946
+ s = report["summary"]
947
+ console.print(
948
+ f" {s[OK]} ok, {s[WARN]} warning(s), {s[FAIL]} failed, {s[SKIP]} skipped.",
949
+ style="green" if report["ok"] else "red",
950
+ )
951
+ console.print(" Nothing is stored by the CLI.", style="dim")
952
+ console.print()
953
+
954
+
955
+ # ── Command ─────────────────────────────────────────────────────────────────
956
+
957
+
958
+ @click.command("login")
959
+ @click.option(
960
+ "--profile",
961
+ type=click.Choice(PROFILES),
962
+ default="default",
963
+ show_default=True,
964
+ help="'disconnected' fails on any hosted dependency.",
965
+ )
966
+ @click.option(
967
+ "--cluster",
968
+ is_flag=True,
969
+ default=False,
970
+ help="Also run 'kubectl cluster-info' against the current context (fails when unreachable).",
971
+ )
972
+ @click.option(
973
+ "--write-env",
974
+ is_flag=True,
975
+ default=False,
976
+ help=(
977
+ "Prompt for missing keys and write them to .env (blank KEY= lines are filled in "
978
+ "place; the file is kept 0600; values are never echoed)."
979
+ ),
980
+ )
981
+ @click.option(
982
+ "--env-file",
983
+ type=click.Path(dir_okay=False, path_type=Path),
984
+ default=None,
985
+ help="The .env file to read (and write with --write-env). Default: <project>/.env or ./.env.",
986
+ )
987
+ @click.option(
988
+ "--status",
989
+ is_flag=True,
990
+ default=False,
991
+ help="Print the report and exit 0 even when a check fails.",
992
+ )
993
+ @click.option(
994
+ "--json",
995
+ "as_json",
996
+ is_flag=True,
997
+ default=False,
998
+ help="Emit the report as JSON.",
999
+ )
1000
+ def cmd_login(*, profile, cluster, write_env, env_file, status, as_json):
1001
+ """Check model provider keys, LangSmith, and kubeconfig; optionally write .env.
1002
+
1003
+ A preflight, not an authentication: nothing is stored. Exit code 1 when
1004
+ any check fails (0 with --status).
1005
+ """
1006
+ if write_env and as_json:
1007
+ raise click.UsageError(
1008
+ "--write-env prompts interactively and cannot be combined with --json."
1009
+ )
1010
+
1011
+ console = Console()
1012
+ info = load_project_info()
1013
+ base = Path(info.root) if info.root else Path.cwd()
1014
+ env_path = env_file if env_file is not None else base / ".env"
1015
+ env = load_env(env_path)
1016
+
1017
+ if write_env:
1018
+ if prompt_and_write_env(info, env, profile=profile, console=console):
1019
+ env = load_env(env_path)
1020
+
1021
+ checks = run_preflight(info, env, profile=profile, cluster=cluster)
1022
+ report = build_report(info, env, checks, profile=profile)
1023
+
1024
+ if as_json:
1025
+ click.echo(json.dumps(report, indent=2))
1026
+ else:
1027
+ print_report(report, console)
1028
+
1029
+ if not report["ok"] and not status:
1030
+ click.get_current_context().exit(1)