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,273 @@
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
+ """Load a locally built image into a single-node dev cluster (kind, k3d, k3s, minikube, ...).
15
+
16
+ A context name is only a label: a renamed kind context would otherwise make
17
+ ``deploy`` push a dev build to the real registry, and a production context that
18
+ happens to be called ``kind-...`` would be treated as a laptop cluster. So the
19
+ cluster is identified from the cluster itself: its nodes' ``providerID``, name
20
+ and labels (kind ``kind://<provider>/<cluster>/<node>``, k3d ``k3d-<cluster>-server-N``,
21
+ k3s ``k3s://``, minikube's ``minikube.k8s.io/name`` label, Docker Desktop and
22
+ OrbStack node names). A kind cluster is also confirmed with ``kind get
23
+ clusters``, since ``kind load`` only reaches clusters on this machine. Only
24
+ when the nodes cannot be read (no permission to list nodes, cluster down)
25
+ does the context name decide, and a ``kind-*`` name is still confirmed with
26
+ the kind CLI.
27
+
28
+ Only the ``docker`` CLI is supported for builds; Docker Desktop, Rancher
29
+ Desktop and OrbStack share the daemon with the cluster, so nothing needs
30
+ loading there.
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import json
36
+ import re
37
+ from dataclasses import dataclass
38
+ from pathlib import Path
39
+ from typing import Any
40
+
41
+ from graph_agents_cli.deploy import _kube
42
+
43
+ KIND = "kind"
44
+ K3D = "k3d"
45
+ K3S = "k3s"
46
+ MINIKUBE = "minikube"
47
+ SHARED_DAEMON = "shared-daemon"
48
+
49
+ _K3D_NODE = re.compile(r"^k3d-(?P<cluster>.+)-(?:server|agent)-\d+$")
50
+ _SHARED_DAEMON_NODES = ("docker-desktop", "orbstack")
51
+ NODE_READ_TIMEOUT = "10s"
52
+
53
+
54
+ @dataclass(frozen=True)
55
+ class LocalCluster:
56
+ """A single-node dev cluster the image can be loaded into."""
57
+
58
+ kind: str
59
+ name: str = ""
60
+ evidence: str = ""
61
+
62
+ def describe(self) -> str:
63
+ label = {
64
+ KIND: f"kind cluster {self.name!r}",
65
+ K3D: f"k3d cluster {self.name!r}",
66
+ K3S: "k3s",
67
+ MINIKUBE: f"minikube profile {self.name!r}",
68
+ SHARED_DAEMON: f"{self.name or 'local cluster'} (shares the docker daemon)",
69
+ }.get(self.kind, self.kind)
70
+ return f"{label}{f', {self.evidence}' if self.evidence else ''}"
71
+
72
+
73
+ def from_context_name(context: str | None) -> LocalCluster | None:
74
+ """The dev cluster a context name suggests (unverified), or ``None``."""
75
+ name = (context or "").strip()
76
+ if name.startswith("kind-"):
77
+ return LocalCluster(KIND, name[len("kind-") :], "from the context name")
78
+ if name.startswith("k3d-"):
79
+ return LocalCluster(K3D, name[len("k3d-") :], "from the context name")
80
+ if name.startswith("minikube"):
81
+ return LocalCluster(MINIKUBE, name, "from the context name")
82
+ if name.startswith("k3s"):
83
+ return LocalCluster(K3S, "", "from the context name")
84
+ if name in ("docker-desktop", "rancher-desktop", "orbstack"):
85
+ return LocalCluster(SHARED_DAEMON, name, "from the context name")
86
+ return None
87
+
88
+
89
+ def from_nodes(nodes: list[dict[str, Any]]) -> LocalCluster | None:
90
+ """The dev cluster the node objects identify, or ``None`` for any other cluster."""
91
+ for node in nodes:
92
+ meta = node.get("metadata") if isinstance(node.get("metadata"), dict) else {}
93
+ spec = node.get("spec") if isinstance(node.get("spec"), dict) else {}
94
+ labels = meta.get("labels") if isinstance(meta.get("labels"), dict) else {}
95
+ node_name = str(meta.get("name") or "")
96
+ provider_id = str(spec.get("providerID") or "")
97
+ if provider_id.startswith("kind://"):
98
+ # kind://<provider>/<cluster>/<node>
99
+ parts = provider_id[len("kind://") :].split("/")
100
+ if len(parts) >= 3 and parts[1]:
101
+ return LocalCluster(KIND, parts[1], f"node providerID {provider_id}")
102
+ if labels.get("minikube.k8s.io/name"):
103
+ profile = str(labels["minikube.k8s.io/name"])
104
+ return LocalCluster(MINIKUBE, profile, f"node label minikube.k8s.io/name={profile}")
105
+ if node_name in _SHARED_DAEMON_NODES or "rancher-desktop" in node_name:
106
+ return LocalCluster(SHARED_DAEMON, node_name, f"node {node_name}")
107
+ if provider_id.startswith("k3s://"):
108
+ match = _K3D_NODE.match(node_name)
109
+ if match:
110
+ return LocalCluster(K3D, match.group("cluster"), f"node {node_name}")
111
+ return LocalCluster(K3S, "", f"node providerID {provider_id}")
112
+ return None
113
+
114
+
115
+ def read_nodes(context: str | None) -> list[dict[str, Any]] | None:
116
+ """The cluster's Node objects, ``None`` when they cannot be read (never raises)."""
117
+ target = _kube.Target(context=context, namespace="")
118
+ try:
119
+ result = _kube.kubectl(
120
+ ["get", "nodes", "-o", "json", f"--request-timeout={NODE_READ_TIMEOUT}"],
121
+ target,
122
+ namespaced=False,
123
+ check=False,
124
+ quiet=True,
125
+ )
126
+ except _kube.ToolFailed:
127
+ return None
128
+ if result.returncode != 0 or not (result.stdout or "").strip():
129
+ return None
130
+ try:
131
+ data = json.loads(result.stdout)
132
+ except json.JSONDecodeError:
133
+ return None
134
+ items = data.get("items") if isinstance(data, dict) else None
135
+ if not isinstance(items, list) or not items:
136
+ return None
137
+ return [item for item in items if isinstance(item, dict)]
138
+
139
+
140
+ def _tool_output(cmd: list[str]) -> str | None:
141
+ """stdout of a local CLI, ``None`` when it is missing or fails (never raises)."""
142
+ try:
143
+ result = _kube.run_cmd(cmd, check=False, quiet=True)
144
+ except _kube.ToolFailed:
145
+ return None
146
+ if result.returncode != 0:
147
+ return None
148
+ return result.stdout or ""
149
+
150
+
151
+ def kind_clusters() -> set[str] | None:
152
+ """Names ``kind get clusters`` reports on this machine, ``None`` when kind is unavailable."""
153
+ out = _tool_output(["kind", "get", "clusters"])
154
+ if out is None:
155
+ return None
156
+ return {line.strip() for line in out.splitlines() if line.strip()}
157
+
158
+
159
+ def _json_or_none(text: str | None) -> Any:
160
+ if text is None:
161
+ return None
162
+ try:
163
+ return json.loads(text or "null")
164
+ except json.JSONDecodeError:
165
+ return None
166
+
167
+
168
+ def k3d_clusters() -> set[str] | None:
169
+ """Names ``k3d cluster list`` reports on this machine, ``None`` when k3d is unavailable."""
170
+ data = _json_or_none(_tool_output(["k3d", "cluster", "list", "-o", "json"]))
171
+ if not isinstance(data, list):
172
+ return None
173
+ return {str(c.get("name")) for c in data if isinstance(c, dict) and c.get("name")}
174
+
175
+
176
+ def minikube_profiles() -> set[str] | None:
177
+ """Valid profiles ``minikube profile list`` reports, ``None`` when minikube is unavailable."""
178
+ data = _json_or_none(_tool_output(["minikube", "profile", "list", "-o", "json"]))
179
+ valid = data.get("valid") if isinstance(data, dict) else None
180
+ if not isinstance(valid, list):
181
+ return None
182
+ return {str(p.get("Name")) for p in valid if isinstance(p, dict) and p.get("Name")}
183
+
184
+
185
+ def _local_hostnames() -> set[str]:
186
+ import socket
187
+
188
+ name = socket.gethostname().lower()
189
+ return {name, name.split(".")[0]}
190
+
191
+
192
+ _LISTINGS = {
193
+ KIND: ("kind get clusters", kind_clusters),
194
+ K3D: ("k3d cluster list", k3d_clusters),
195
+ MINIKUBE: ("minikube profile list", minikube_profiles),
196
+ }
197
+
198
+
199
+ def _verify(
200
+ cluster: LocalCluster, nodes: list[dict[str, Any]] | None
201
+ ) -> tuple[LocalCluster | None, str]:
202
+ """Confirm that ``cluster`` runs on this machine, where its load command can reach it."""
203
+ if cluster.kind in _LISTINGS:
204
+ command, listing = _LISTINGS[cluster.kind]
205
+ names = listing()
206
+ if names is None:
207
+ return None, (
208
+ f"{cluster.describe()}, but `{command}` is not available on this machine to "
209
+ "load images into it"
210
+ )
211
+ if cluster.name not in names:
212
+ return None, (
213
+ f"{cluster.describe()}, but `{command}` on this machine does not list "
214
+ f"{cluster.name!r}"
215
+ )
216
+ evidence = f"{cluster.evidence}; `{command}` lists it"
217
+ return LocalCluster(cluster.kind, cluster.name, evidence), ""
218
+ if cluster.kind == K3S and nodes is not None:
219
+ # k3s also runs production clusters: only this machine's own node can import images.
220
+ node_names = {
221
+ str(n["metadata"].get("name") or "").lower()
222
+ for n in nodes
223
+ if isinstance(n.get("metadata"), dict)
224
+ }
225
+ if not node_names & _local_hostnames():
226
+ return None, f"{cluster.describe()}, but none of its nodes is this machine"
227
+ return cluster, ""
228
+
229
+
230
+ def detect(context: str | None) -> tuple[LocalCluster | None, str]:
231
+ """Identify a local dev cluster behind ``context``: ``(cluster or None, reason)``.
232
+
233
+ ``reason`` explains, in one line, why the cluster is not treated as local.
234
+ """
235
+ nodes = read_nodes(context)
236
+ if nodes is not None:
237
+ cluster = from_nodes(nodes)
238
+ if cluster is None:
239
+ return None, "its nodes are not a local dev cluster"
240
+ else:
241
+ cluster = from_context_name(context)
242
+ if cluster is None:
243
+ return None, "its nodes cannot be read and the context name is not a dev cluster"
244
+ verified, why = _verify(cluster, nodes)
245
+ if verified is None:
246
+ # Docker Desktop can run its Kubernetes on kind nodes that the kind CLI does
247
+ # not list; the desktop apps share the docker daemon with the cluster, so
248
+ # their own context names still mean "no load needed" (never a push).
249
+ named = from_context_name(context)
250
+ if named is not None and named.kind == SHARED_DAEMON:
251
+ return named, ""
252
+ return verified, why
253
+
254
+
255
+ def local_load_commands(
256
+ cluster: LocalCluster, image: str, *, tar_path: Path | None = None
257
+ ) -> list[list[str]]:
258
+ """The commands that make ``image`` visible to ``cluster``.
259
+
260
+ An empty list means the cluster already sees the docker daemon's images.
261
+ """
262
+ if cluster.kind == KIND:
263
+ return [["kind", "load", "docker-image", image, "--name", cluster.name]]
264
+ if cluster.kind == K3D:
265
+ return [["k3d", "image", "import", image, "-c", cluster.name]]
266
+ if cluster.kind == MINIKUBE:
267
+ if cluster.name and cluster.name != "minikube":
268
+ return [["minikube", "image", "load", image, "-p", cluster.name]]
269
+ return [["minikube", "image", "load", image]]
270
+ if cluster.kind == K3S:
271
+ tar = str(tar_path or Path(".graph-agents-cli") / "image.tar")
272
+ return [["docker", "save", "-o", tar, image], ["k3s", "ctr", "images", "import", tar]]
273
+ return []
@@ -0,0 +1,13 @@
1
+ # Copyright 2026 Google LLC
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
+ # http://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.
@@ -0,0 +1,131 @@
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 build command: ``docker build`` on the project's Dockerfile.
17
+
18
+ One runtime-specific Dockerfile per project, selected at scaffold time;
19
+ ``build``, CI, and local-load all run ``docker build``. There is no experiment
20
+ gate. Exit codes: 0 ok, 2 when docker fails, 3 for a configuration error.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import shlex
26
+ from pathlib import Path
27
+
28
+ import click
29
+
30
+ from graph_agents_cli import _tools
31
+ from graph_agents_cli._project import chdir_project_root, read_project_config
32
+ from graph_agents_cli._runner import run
33
+ from graph_agents_cli.deploy._image import reference_problem as image_reference_problem
34
+
35
+ DEFAULT_TAG = "latest"
36
+ DOCKERFILE = "Dockerfile"
37
+ EXIT_TOOL_FAILURE = 2
38
+ EXIT_CONFIG_ERROR = 3
39
+
40
+
41
+ def image_name(*, project_name: str, registry: str | None) -> str:
42
+ """``<registry>/<project>`` when a registry is set, else ``<project>``."""
43
+ name = (project_name or "").strip()
44
+ if not name:
45
+ raise click.ClickException(
46
+ "The manifest has no project name; cannot derive an image name.\n"
47
+ " Set 'name' in graph-agents-cli-manifest.yaml."
48
+ )
49
+ reg = (registry or "").strip().rstrip("/")
50
+ return f"{reg}/{name}" if reg else name
51
+
52
+
53
+ def build_commands(
54
+ *, image: str, tag: str, push: bool, dockerfile: str = DOCKERFILE
55
+ ) -> list[list[str]]:
56
+ ref = f"{image}:{tag}"
57
+ commands = [["docker", "build", "-t", ref, "-f", dockerfile, "."]]
58
+ if push:
59
+ commands.append(["docker", "push", ref])
60
+ return commands
61
+
62
+
63
+ @click.command("build")
64
+ @click.option("--tag", default=DEFAULT_TAG, show_default=True, help="Image tag.")
65
+ @click.option("--registry", default=None, help="Override the manifest registry (e.g. ghcr.io/org).")
66
+ @click.option("--push", is_flag=True, default=False, help="Push the image after building.")
67
+ @click.option(
68
+ "--dry-run",
69
+ "dry_run",
70
+ is_flag=True,
71
+ default=False,
72
+ help="Print the docker commands without running them.",
73
+ )
74
+ @click.pass_context
75
+ def cmd_build(
76
+ ctx: click.Context, tag: str, registry: str | None, push: bool, dry_run: bool
77
+ ) -> None:
78
+ """Build the agent container image.
79
+
80
+ Runs `docker build -t <registry>/<name>:<tag> .` with the project's
81
+ runtime-specific Dockerfile. The registry defaults to the manifest's
82
+ `create_params.registry`; without one the image is named after the project.
83
+ """
84
+ chdir_project_root()
85
+ cfg = read_project_config()
86
+ project_root = Path.cwd()
87
+
88
+ if not (project_root / DOCKERFILE).is_file():
89
+ click.secho(
90
+ f"Error: No {DOCKERFILE} at the project root ({project_root}).\n"
91
+ " Add deployment support with: graph-agents-cli scaffold enhance",
92
+ fg="red",
93
+ err=True,
94
+ )
95
+ ctx.exit(EXIT_CONFIG_ERROR)
96
+
97
+ project_name = getattr(cfg, "project_name", "") or getattr(cfg, "name", "")
98
+ effective_registry = registry if registry is not None else getattr(cfg, "registry", "")
99
+ image = image_name(project_name=project_name, registry=effective_registry)
100
+ # Checked before docker runs (and under --dry-run): the scaffold's placeholder
101
+ # registry, or any malformed reference, would otherwise surface as a docker
102
+ # parser error that does not name the setting to change.
103
+ problem = image_reference_problem(image, tag, registry=effective_registry)
104
+ if problem:
105
+ # "Error:" like every other command's configuration error.
106
+ click.secho(f"Error: {problem}", fg="red", err=True)
107
+ ctx.exit(EXIT_CONFIG_ERROR)
108
+ commands = build_commands(image=image, tag=tag, push=push)
109
+
110
+ if dry_run:
111
+ click.echo("Dry run; would execute:")
112
+ for cmd in commands:
113
+ click.echo(f" {shlex.join(cmd)}")
114
+ return
115
+
116
+ _tools.require_tool(
117
+ "docker",
118
+ "Install Docker (https://docs.docker.com/get-docker/) and ensure it is in your PATH.",
119
+ )
120
+ for cmd in commands:
121
+ result = run(cmd, check=False)
122
+ if result.returncode != 0:
123
+ click.secho(
124
+ f"Error: {shlex.join(cmd[:2])} failed (exit code {result.returncode}).",
125
+ fg="red",
126
+ err=True,
127
+ )
128
+ ctx.exit(EXIT_TOOL_FAILURE)
129
+ click.echo(f"Built image: {image}:{tag}")
130
+ if push:
131
+ click.echo(f"Pushed image: {image}:{tag}")
@@ -0,0 +1,78 @@
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 install command: install project dependencies with uv."""
17
+
18
+ from __future__ import annotations
19
+
20
+ import logging
21
+ import shutil
22
+ from pathlib import Path
23
+
24
+ import click
25
+
26
+ from graph_agents_cli import _tools
27
+ from graph_agents_cli._project import chdir_project_root, find_project_root
28
+ from graph_agents_cli._runner import run
29
+
30
+
31
+ @click.command("install")
32
+ @click.option(
33
+ "--clean",
34
+ is_flag=True,
35
+ help="Delete and recreate the uv virtual environment (e.g. after moving the project folder).",
36
+ )
37
+ @click.option(
38
+ "--locked",
39
+ is_flag=True,
40
+ help="Assert that uv.lock is up to date with pyproject.toml; fail instead of updating it.",
41
+ )
42
+ def cmd_install(clean: bool, locked: bool) -> None:
43
+ """Install project dependencies."""
44
+ chdir_project_root()
45
+ # Re-materialize any missing extension working copies from their pinned
46
+ # SHAs before syncing: the loader tells a user with a missing copy to run
47
+ # this command. Never advances pins.
48
+ from graph_agents_cli.extension._sync import sync_extensions
49
+
50
+ sync_extensions(find_project_root(Path.cwd()))
51
+ install_python(clean=clean, locked=locked)
52
+
53
+
54
+ def install_python(*, clean: bool, locked: bool) -> None:
55
+ """Run ``uv sync`` (``--locked`` when asked), deleting ``.venv`` first under ``--clean``."""
56
+ # Resolve uv up front: --clean deletes the venv. Fail quickly here (before
57
+ # any destructive work) rather than when calling uv.
58
+ _tools.require_tool("uv")
59
+ if clean:
60
+ _delete_venv()
61
+ cmd = ["uv", "sync"]
62
+ if locked:
63
+ cmd.append("--locked")
64
+ run(cmd, check_err_msg="Failed to install dependencies")
65
+
66
+
67
+ def _delete_venv() -> None:
68
+ root = find_project_root()
69
+ if not root:
70
+ logging.warning("Could not find the project root; nothing to clean.")
71
+ return
72
+ venv_path = root / ".venv"
73
+ if not venv_path.exists():
74
+ return
75
+ try:
76
+ shutil.rmtree(venv_path)
77
+ except Exception as exc:
78
+ logging.warning("Failed to remove venv: %s", exc)
@@ -0,0 +1,119 @@
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 lint command: ruff, the API-policy check of every tool, the response schema."""
17
+
18
+ from __future__ import annotations
19
+
20
+ from pathlib import Path
21
+
22
+ import click
23
+
24
+ from graph_agents_cli import _response_schema
25
+ from graph_agents_cli._api_policy import POLICY_FILENAME, ensure_no_legacy_api_policy
26
+ from graph_agents_cli._project import chdir_project_root, read_project_config
27
+ from graph_agents_cli._runner import run
28
+ from graph_agents_cli.dev.policy_check import run_policy_check
29
+
30
+
31
+ @click.command("lint")
32
+ @click.option("--fix", is_flag=True, default=False, help="Auto-fix lint and formatting issues.")
33
+ @click.option(
34
+ "--policy-only",
35
+ "policy_only",
36
+ is_flag=True,
37
+ default=False,
38
+ help="Run only the API-policy check (skip ruff).",
39
+ )
40
+ def cmd_lint(fix: bool, policy_only: bool) -> None:
41
+ """Run code quality checks and the API-policy check.
42
+
43
+ \b
44
+ ruff check . (--fix applies fixes)
45
+ ruff format . --check (--fix reformats in place)
46
+ API-policy check api-policy.yaml must pass the strict schema, and
47
+ every API_CALLS entry of every tool must name a
48
+ declared API and be allowed by its rules (and
49
+ exist in its OpenAPI spec when one is named)
50
+ Response schema <agent directory>/response_schema.json, when the
51
+ project has one (structured final answers), must
52
+ be a schema the agent starts with; a warning when
53
+ agent.py does not pass response_format() to the
54
+ agent or has no StructuredAnswer() in its
55
+ middleware
56
+
57
+ \b
58
+ Exit codes:
59
+ 0 clean
60
+ 1 a refused call or an unreadable API_CALLS, or ruff failed
61
+ 3 configuration error: an invalid api-policy.yaml or response
62
+ schema, the retired product policy, or not in a project
63
+ """
64
+ chdir_project_root()
65
+ cfg = read_project_config()
66
+ ensure_no_legacy_api_policy(Path.cwd())
67
+ check_response_schema(Path.cwd(), cfg.agent_directory)
68
+ if not policy_only:
69
+ lint_python(fix=fix)
70
+ violations = run_policy_check(
71
+ Path.cwd(),
72
+ cfg.agent_directory,
73
+ policy_file=cfg.api_policy_file or POLICY_FILENAME,
74
+ runtime=cfg.runtime,
75
+ policy_declared=bool(cfg.api_policy_file),
76
+ auth_policy=cfg.auth_policy,
77
+ )
78
+ if violations:
79
+ raise click.ClickException(
80
+ f"API policy check failed: {violations} violation(s). Fix the tool declarations, "
81
+ "or change api-policy.yaml with `graph-agents-cli api` (the commands above) in a "
82
+ "reviewed pull request."
83
+ )
84
+
85
+
86
+ def check_response_schema(project_root: Path, agent_directory: str) -> None:
87
+ """The project's response schema, when it has one: refused (exit 3) unless the agent starts
88
+ with it; a warning when ``agent.py`` does not build the agent with ``response_format()`` or
89
+ lacks ``StructuredAnswer()`` in its middleware."""
90
+ path = _response_schema.schema_file(project_root, agent_directory)
91
+ if not path.is_file():
92
+ return
93
+ _response_schema.load_schema(path)
94
+ problems = _response_schema.wiring_problems(project_root, agent_directory)
95
+ if problems:
96
+ click.secho(
97
+ f"Warning: {agent_directory}/{_response_schema.SCHEMA_FILENAME} declares structured "
98
+ f"answers, but {'; and '.join(problems)}. Pass "
99
+ "response_format=response_format(model, tools) to create_agent and put "
100
+ "StructuredAnswer() last in middleware(); see the template's "
101
+ "app_utils/structured.py.",
102
+ fg="yellow",
103
+ err=True,
104
+ )
105
+ else:
106
+ click.echo(f"Response schema: {agent_directory}/{_response_schema.SCHEMA_FILENAME} OK")
107
+
108
+
109
+ def lint_python(*, fix: bool) -> None:
110
+ """Run ``ruff check`` and ``ruff format`` in the project."""
111
+ if fix:
112
+ run(["uv", "run", "ruff", "check", ".", "--fix"], check_err_msg="Ruff check --fix failed")
113
+ run(["uv", "run", "ruff", "format", "."], check_err_msg="Ruff format failed")
114
+ else:
115
+ run(["uv", "run", "ruff", "check", "."], check_err_msg="Ruff check failed")
116
+ run(
117
+ ["uv", "run", "ruff", "format", ".", "--check"],
118
+ check_err_msg="Ruff format check failed",
119
+ )