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,462 @@
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
+ """Project manifest reader (``graph-agents-cli-manifest.yaml``) and prerequisite guards.
17
+
18
+ The manifest fields are defined by the ProjectInfo model below. There is no legacy
19
+ ``pyproject.toml`` fallback: the manifest is the only project marker.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import logging
25
+ import os
26
+ from collections.abc import Mapping
27
+ from dataclasses import dataclass, field
28
+ from pathlib import Path
29
+ from typing import Any
30
+
31
+ import click
32
+ import yaml
33
+
34
+ from graph_agents_cli._api_policy import ApiPolicyConfigError, manifest_policy_file_problem
35
+ from graph_agents_cli._defaults import (
36
+ DEFAULT_AGENT_GUIDANCE_FILENAME,
37
+ DEFAULT_CD,
38
+ DEFAULT_MODEL_PROVIDER,
39
+ DEFAULT_MODELS,
40
+ DEFAULT_RUNTIME,
41
+ ENVIRONMENTS,
42
+ PROVIDER_KEY_VARS,
43
+ auth_policy_implemented_default,
44
+ default_secret_keys,
45
+ normalize_auth_policy,
46
+ )
47
+
48
+ MANIFEST_FILENAME = "graph-agents-cli-manifest.yaml"
49
+ API_POLICY_FILENAME = "api-policy.yaml"
50
+
51
+ # Exit code of a configuration error (0 ok, 1 refused or failed gate, 2 tool
52
+ # failure, 3 configuration error).
53
+ EXIT_CONFIG_ERROR = 3
54
+
55
+
56
+ class ManifestError(click.ClickException):
57
+ """The project manifest cannot be read or has the wrong shape (exit 3)."""
58
+
59
+ exit_code = EXIT_CONFIG_ERROR
60
+
61
+
62
+ class NotInProjectError(click.ClickException):
63
+ """The command needs a project and none encloses the working directory (exit 3)."""
64
+
65
+ exit_code = EXIT_CONFIG_ERROR
66
+
67
+
68
+ @dataclass
69
+ class SecretsConfig:
70
+ """The ``secrets:`` block: allow-listed Secret keys and their owner."""
71
+
72
+ keys: list[str] = field(default_factory=list)
73
+ owner: str = ""
74
+
75
+
76
+ @dataclass
77
+ class ApiPolicyConfig:
78
+ """The ``api_policy:`` block. ``policy_file`` is None when no policy is declared."""
79
+
80
+ policy_file: str | None = None
81
+
82
+
83
+ @dataclass
84
+ class ProjectConfig:
85
+ """Configuration read from ``graph-agents-cli-manifest.yaml``.
86
+
87
+ Every manifest key is an attribute. ``create_params`` is kept as the raw
88
+ dict (for ``generation_metadata``) and also exposed through typed
89
+ properties with the defaults of ``graph_agents_cli._defaults``.
90
+ """
91
+
92
+ project_name: str = ""
93
+ cli_version: str = ""
94
+ # The ``cli_build`` block as written (``scaffold.utils.build_record`` parses
95
+ # and validates it): the build that rendered the project. None when absent.
96
+ cli_build: Any = None
97
+ agent_directory: str = "app"
98
+ base_template: str = "langgraph"
99
+ generated_at: str = ""
100
+ language: str = "python"
101
+ create_params: dict[str, Any] = field(default_factory=dict)
102
+ environments: dict[str, dict[str, str]] = field(default_factory=dict)
103
+ secrets: SecretsConfig = field(default_factory=SecretsConfig)
104
+ api_policy: ApiPolicyConfig = field(default_factory=ApiPolicyConfig)
105
+ process: str | None = None
106
+ # True when the manifest still has the retired ``product_api:`` block;
107
+ # create/enhance/upgrade/lint stop with a migration message (exit 3).
108
+ has_legacy_product_api: bool = False
109
+
110
+ # -- manifest aliases -------------------------------------------------
111
+
112
+ @property
113
+ def name(self) -> str:
114
+ """The manifest's ``name`` key (alias of ``project_name``)."""
115
+ return self.project_name
116
+
117
+ # -- create_params, typed ----------------------------------------------
118
+
119
+ @property
120
+ def deployment_target(self) -> str:
121
+ return str(self.create_params.get("deployment_target") or "none")
122
+
123
+ @property
124
+ def runtime(self) -> str:
125
+ return str(self.create_params.get("runtime") or DEFAULT_RUNTIME)
126
+
127
+ @property
128
+ def model_provider(self) -> str:
129
+ return str(self.create_params.get("model_provider") or DEFAULT_MODEL_PROVIDER)
130
+
131
+ @property
132
+ def model(self) -> str:
133
+ recorded = self.create_params.get("model")
134
+ if recorded:
135
+ return str(recorded)
136
+ return DEFAULT_MODELS.get(self.model_provider, "")
137
+
138
+ @property
139
+ def checkpointer(self) -> str:
140
+ recorded = self.create_params.get("checkpointer")
141
+ if recorded:
142
+ return str(recorded)
143
+ # A deployed project defaults to postgres; a local-only project to memory.
144
+ return "postgres" if self.deployment_target == "kubernetes" else "memory"
145
+
146
+ @property
147
+ def registry(self) -> str:
148
+ return str(self.create_params.get("registry") or "")
149
+
150
+ @property
151
+ def cd(self) -> str:
152
+ return str(self.create_params.get("cd") or DEFAULT_CD)
153
+
154
+ @property
155
+ def auth_policy(self) -> str:
156
+ """The recorded policy; the retired ``product-session`` reads as ``custom`` (with a warning)."""
157
+ return normalize_auth_policy(self.create_params.get("auth_policy"))
158
+
159
+ @property
160
+ def auth_policy_implemented(self) -> bool:
161
+ recorded = self.create_params.get("auth_policy_implemented")
162
+ if recorded is None:
163
+ return auth_policy_implemented_default(self.auth_policy)
164
+ return bool(recorded)
165
+
166
+ @property
167
+ def agent_guidance_filename(self) -> str:
168
+ return str(
169
+ self.create_params.get("agent_guidance_filename") or DEFAULT_AGENT_GUIDANCE_FILENAME
170
+ )
171
+
172
+ # -- convenience --------------------------------------------------------
173
+
174
+ @property
175
+ def secret_keys(self) -> list[str]:
176
+ return list(self.secrets.keys)
177
+
178
+ @property
179
+ def api_policy_file(self) -> str | None:
180
+ return self.api_policy.policy_file
181
+
182
+ def provider_key_var(self) -> str:
183
+ """The Secret key that carries the model provider's API key."""
184
+ return PROVIDER_KEY_VARS.get(self.model_provider, "MODEL_API_KEY")
185
+
186
+ def environment(self, env: str) -> tuple[str, str]:
187
+ """Return ``(context, namespace)`` for ``env``.
188
+
189
+ The manifest's ``environments:`` block wins. An environment the block
190
+ does not name but the CLI knows (dev, staging, prod) falls back to an
191
+ empty context (the current kube context) and the ``<name>-<env>``
192
+ namespace convention. Anything else is a usage error.
193
+ """
194
+ recorded = self.environments.get(env)
195
+ if recorded is not None:
196
+ context = str(recorded.get("context") or "")
197
+ namespace = str(recorded.get("namespace") or f"{self.project_name}-{env}")
198
+ return context, namespace
199
+ if env in ENVIRONMENTS:
200
+ return "", f"{self.project_name}-{env}"
201
+ known = ", ".join(sorted(set(self.environments) | set(ENVIRONMENTS)))
202
+ raise click.UsageError(
203
+ f"Unknown environment '{env}'. Known environments: {known}.\n"
204
+ f" Add it under environments: in {MANIFEST_FILENAME}."
205
+ )
206
+
207
+ @classmethod
208
+ def from_dict(
209
+ cls,
210
+ data: Mapping[str, Any] | Any,
211
+ filename: str = MANIFEST_FILENAME,
212
+ ) -> ProjectConfig:
213
+ """Create a ProjectConfig from a raw manifest mapping."""
214
+ if not isinstance(data, Mapping):
215
+ raise ManifestError(f"malformed {filename}")
216
+
217
+ cfg = cls()
218
+ cfg.project_name = str(data.get("name") or cfg.project_name)
219
+ cfg.cli_version = str(data.get("cli_version") or cfg.cli_version)
220
+ cfg.cli_build = data.get("cli_build")
221
+ cfg.agent_directory = str(data.get("agent_directory") or cfg.agent_directory)
222
+ cfg.base_template = str(data.get("base_template") or cfg.base_template)
223
+ cfg.generated_at = str(data.get("generated_at") or cfg.generated_at)
224
+ cfg.language = str(data.get("language") or cfg.language)
225
+
226
+ create_params = data.get("create_params")
227
+ if create_params is None:
228
+ create_params = {}
229
+ elif not isinstance(create_params, Mapping):
230
+ raise ManifestError(f"malformed create_params in {filename}")
231
+ cfg.create_params = dict(create_params)
232
+
233
+ environments = data.get("environments") or {}
234
+ if not isinstance(environments, Mapping):
235
+ raise ManifestError(f"malformed environments in {filename}")
236
+ parsed_envs: dict[str, dict[str, str]] = {}
237
+ for env_name, env_data in environments.items():
238
+ if env_data is None:
239
+ env_data = {}
240
+ if not isinstance(env_data, Mapping):
241
+ raise ManifestError(f"malformed environments.{env_name} in {filename}")
242
+ parsed_envs[str(env_name)] = {
243
+ "context": str(env_data.get("context") or ""),
244
+ "namespace": str(env_data.get("namespace") or f"{cfg.project_name}-{env_name}"),
245
+ }
246
+ cfg.environments = parsed_envs
247
+
248
+ secrets = data.get("secrets") or {}
249
+ if not isinstance(secrets, Mapping):
250
+ raise ManifestError(f"malformed secrets in {filename}")
251
+ raw_keys = secrets.get("keys")
252
+ if isinstance(raw_keys, str):
253
+ raw_keys = [raw_keys]
254
+ keys = [str(k) for k in raw_keys] if raw_keys else []
255
+ if not keys:
256
+ # No recorded allow-list: the default follows the provider and runtime.
257
+ keys = default_secret_keys(cfg.model_provider, cfg.runtime, auth_policy=cfg.auth_policy)
258
+ cfg.secrets = SecretsConfig(keys=keys, owner=str(secrets.get("owner") or ""))
259
+
260
+ api_policy = data.get("api_policy") or {}
261
+ if not isinstance(api_policy, Mapping):
262
+ raise ManifestError(f"malformed api_policy in {filename}")
263
+ policy_file = api_policy.get("policy_file")
264
+ problem = manifest_policy_file_problem(policy_file, filename)
265
+ if problem:
266
+ raise ApiPolicyConfigError(problem)
267
+ cfg.api_policy = ApiPolicyConfig(policy_file=API_POLICY_FILENAME if policy_file else None)
268
+ cfg.has_legacy_product_api = "product_api" in data
269
+
270
+ process = data.get("process")
271
+ cfg.process = str(process) if process else None
272
+
273
+ return cfg
274
+
275
+
276
+ def _read_project_config_from_manifest(manifest_path: Path) -> ProjectConfig:
277
+ """Read project configuration from a manifest file (unreadable or invalid YAML: exit 3)."""
278
+ try:
279
+ with open(manifest_path, encoding="utf-8") as f:
280
+ data = yaml.safe_load(f)
281
+ except yaml.YAMLError as exc:
282
+ where = ""
283
+ mark = getattr(exc, "problem_mark", None)
284
+ if mark is not None:
285
+ where = f" (line {mark.line + 1}, column {mark.column + 1})"
286
+ problem = getattr(exc, "problem", None) or str(exc).splitlines()[0]
287
+ raise ManifestError(
288
+ f"{manifest_path} is not valid YAML{where}: {problem}.\n"
289
+ " Fix the file, or restore it from version control."
290
+ ) from None
291
+ except (OSError, UnicodeDecodeError) as exc:
292
+ raise ManifestError(f"Cannot read {manifest_path}: {exc}") from None
293
+ if data is None:
294
+ data = {}
295
+ return ProjectConfig.from_dict(data, filename=manifest_path.name)
296
+
297
+
298
+ def read_project_config(project_dir: str | None = None) -> ProjectConfig:
299
+ """Read project metadata from ``graph-agents-cli-manifest.yaml``.
300
+
301
+ Returns a default ``ProjectConfig`` when the directory holds no manifest.
302
+
303
+ Args:
304
+ project_dir: Directory containing the manifest. Defaults to the
305
+ current working directory.
306
+ """
307
+ root = Path(project_dir) if project_dir else Path.cwd()
308
+ manifest_path = root / MANIFEST_FILENAME
309
+ if manifest_path.exists():
310
+ return _read_project_config_from_manifest(manifest_path)
311
+ return ProjectConfig()
312
+
313
+
314
+ def _parse_version(v: str) -> tuple[int, ...]:
315
+ """Parse a dotted numeric version into a comparable tuple.
316
+
317
+ Raises ``ValueError`` on non-numeric components (e.g. a ``0.0.0-dev``
318
+ source build), which callers treat as "version unknown, don't compare".
319
+ """
320
+ return tuple(int(x) for x in v.split("."))
321
+
322
+
323
+ def scaffold_older_than(cfg: ProjectConfig, version: str) -> bool:
324
+ """True when the version that scaffolded ``cfg`` is older than ``version``.
325
+
326
+ Best-effort: returns False when the scaffold version is missing or
327
+ non-numeric, so callers fall back to generic guidance instead of guessing.
328
+ """
329
+ if not cfg.cli_version:
330
+ return False
331
+ try:
332
+ return _parse_version(cfg.cli_version) < _parse_version(version)
333
+ except ValueError:
334
+ return False
335
+
336
+
337
+ def check_cli_version(cfg: ProjectConfig) -> None:
338
+ """Warn if the running CLI version differs from the one that scaffolded the project.
339
+
340
+ Emits a warning with upgrade guidance on a mismatch; never blocks execution.
341
+ """
342
+ cli_version = cfg.cli_version
343
+ if not cli_version:
344
+ return
345
+
346
+ from graph_agents_cli import __version__
347
+
348
+ try:
349
+ project_ver = _parse_version(cli_version)
350
+ cli_ver = _parse_version(__version__)
351
+ except ValueError:
352
+ return
353
+
354
+ if cli_ver < project_ver:
355
+ from graph_agents_cli.scaffold.utils.version import install_command
356
+
357
+ click.echo(
358
+ f"\n⚠️ Version mismatch: project was scaffolded with graph-agents-cli {cli_version},"
359
+ f" running {__version__}.\n"
360
+ f" Upgrade the CLI: {install_command(version=cli_version)}\n",
361
+ err=True,
362
+ )
363
+ elif cli_ver > project_ver:
364
+ click.echo(
365
+ f"\n⚠️ Version mismatch: project was scaffolded with graph-agents-cli {cli_version},"
366
+ f" running {__version__}.\n"
367
+ " Upgrade the project: graph-agents-cli scaffold upgrade\n",
368
+ err=True,
369
+ )
370
+
371
+
372
+ def find_project_root(dir: Path | None = None) -> Path | None:
373
+ """Find the project root by walking up looking for the manifest."""
374
+ if dir is None:
375
+ dir = Path.cwd()
376
+ for parent in [dir, *dir.parents]:
377
+ if (parent / MANIFEST_FILENAME).exists():
378
+ return parent
379
+ return None
380
+
381
+
382
+ def is_project_moved() -> bool:
383
+ """Check if the project has been moved by comparing the current path with .venv/bin/activate."""
384
+ root = find_project_root(Path.cwd())
385
+ if not root:
386
+ return False
387
+
388
+ venv_dir = root / ".venv"
389
+ # Support both Unix-style (bin) and Windows-style (Scripts) virtualenvs
390
+ activate_script = venv_dir / "bin" / "activate"
391
+ if not activate_script.exists():
392
+ activate_script = venv_dir / "Scripts" / "activate"
393
+
394
+ if not activate_script.exists():
395
+ return False
396
+
397
+ try:
398
+ with open(activate_script, encoding="utf-8") as f:
399
+ for line in f:
400
+ if line.startswith("VIRTUAL_ENV="):
401
+ stored_path_str = line.split("=", 1)[1].strip().strip("'\"")
402
+ stored_path = Path(stored_path_str).resolve()
403
+ current_path = venv_dir.resolve()
404
+ return stored_path != current_path
405
+ except Exception as e:
406
+ logging.warning(f"Error checking if project moved: {e}")
407
+ return False
408
+
409
+
410
+ # ---------------------------------------------------------------------------
411
+ # Prerequisite guards, reusable checks for CLI commands
412
+ # ---------------------------------------------------------------------------
413
+
414
+
415
+ def chdir_project_root(dir: Path | None = None) -> None:
416
+ """Locate the project root relative to ``dir`` and chdir to it; raise if none."""
417
+ if dir is None:
418
+ dir = Path.cwd()
419
+ root = find_project_root(dir)
420
+ if not root:
421
+ raise NotInProjectError(
422
+ f"No {MANIFEST_FILENAME} found in the current directory or its parents.\n"
423
+ " Run this command from your project root, or create a project first:\n"
424
+ " graph-agents-cli create my-agent"
425
+ )
426
+ # Only announce the root when we actually move (i.e. run from a subdir).
427
+ if root.resolve() != Path.cwd().resolve():
428
+ click.echo(f"Using project root directory: {root}", err=True)
429
+ os.chdir(root)
430
+
431
+
432
+ def require_agent_directory(cfg: ProjectConfig) -> None:
433
+ """Raise if the configured agent_directory doesn't exist. Assumes cwd is the project root."""
434
+ agent_path = Path(cfg.agent_directory)
435
+ if not agent_path.is_dir():
436
+ raise click.ClickException(
437
+ f"Agent directory '{cfg.agent_directory}' not found.\n"
438
+ " Ensure you're in the project root and that the directory exists.\n"
439
+ f" The agent_directory is configured in {MANIFEST_FILENAME}."
440
+ )
441
+
442
+
443
+ def require_deployment_target(cfg: ProjectConfig) -> None:
444
+ """Raise a UsageError when the project has no deployment target (``none``)."""
445
+ if cfg.deployment_target in ("none", ""):
446
+ raise click.UsageError(
447
+ "No deployment target configured (deployment_target is 'none').\n"
448
+ f" Set create_params.deployment_target in {MANIFEST_FILENAME},\n"
449
+ " or add deployment support to your project:\n"
450
+ " graph-agents-cli scaffold enhance --deployment-target kubernetes"
451
+ )
452
+
453
+
454
+ def find_project_config(project_dir: Path | None = None) -> ProjectConfig | None:
455
+ """Read the manifest after resolving the project root from ``project_dir``.
456
+
457
+ Returns None when no project root is found.
458
+ """
459
+ project_root_dir = find_project_root(project_dir)
460
+ if project_root_dir is None:
461
+ return None
462
+ return read_project_config(str(project_root_dir))
@@ -0,0 +1,220 @@
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
+ """Request headers and URL classification for talking to a remote agent.
17
+
18
+ Transport-neutral so every caller (``run``, ``eval generate``) agrees on which
19
+ credentials a ``--url`` gets and where the chat or A2A surface lives.
20
+ Credentials follow the project's auth policy:
21
+
22
+ * ``--header 'Authorization: Bearer ...'`` or ``GRAPH_AGENTS_CLI_API_KEY``
23
+ for ``shared-bearer`` (and a JWT in the same header for ``jwt``);
24
+ * ``--header 'Name: value'`` or ``--cookie name=value`` for a ``custom``
25
+ policy. The hidden ``--session-token`` option of ``run`` and ``eval`` is a
26
+ deprecated alias of ``--header 'X-Session-Token: ...'``: it still works, prints
27
+ a one-line deprecation warning, and is folded into the headers on entry.
28
+
29
+ No cloud SDK is consulted; the CLI stores no credentials.
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ import os
35
+ from collections.abc import Mapping, Sequence
36
+ from typing import NamedTuple
37
+
38
+ import click
39
+ import httpx
40
+
41
+ from graph_agents_cli import _http
42
+
43
+ API_KEY_ENV = "GRAPH_AGENTS_CLI_API_KEY"
44
+ SESSION_TOKEN_HEADER = "X-Session-Token"
45
+ AGENT_CARD_PATH = "/.well-known/agent-card.json"
46
+
47
+ RUN_MODES: tuple[str, ...] = ("chat", "a2a")
48
+ DEFAULT_RUN_MODE = "chat"
49
+
50
+
51
+ SESSION_TOKEN_DEPRECATION = (
52
+ "Warning: --session-token is deprecated and will be removed; "
53
+ "pass --header 'X-Session-Token: <token>' instead."
54
+ )
55
+
56
+
57
+ def deprecated_session_token(
58
+ _ctx: click.Context, _param: click.Parameter, value: str | None
59
+ ) -> str | None:
60
+ """Click callback of the hidden ``--session-token`` alias: warn once when it is used."""
61
+ if value:
62
+ click.echo(SESSION_TOKEN_DEPRECATION, err=True)
63
+ return value
64
+
65
+
66
+ def fold_session_token(header: Sequence[str], session_token: str | None) -> tuple[str, ...]:
67
+ """``header`` plus the deprecated ``--session-token`` value as its ``X-Session-Token`` header.
68
+
69
+ Folding it into the ``--header`` values once, on entry, means everything
70
+ downstream (resume hints, extension overrides) only ever sees ``--header``.
71
+ """
72
+ if not session_token:
73
+ return tuple(header)
74
+ return (*header, f"{SESSION_TOKEN_HEADER}: {session_token}")
75
+
76
+
77
+ def parse_header(value: str) -> tuple[str, str]:
78
+ """Parse one ``Key: Value`` header string."""
79
+ if ":" not in value:
80
+ raise click.BadParameter(f"Invalid header format (expected 'Key: Value'): {value}")
81
+ key, _, val = value.partition(":")
82
+ key = key.strip()
83
+ if not key:
84
+ raise click.BadParameter(f"Invalid header format (empty name): {value}")
85
+ return key, val.strip()
86
+
87
+
88
+ def parse_cookie(value: str) -> tuple[str, str]:
89
+ """Parse one ``name=value`` cookie string."""
90
+ if "=" not in value:
91
+ raise click.BadParameter(f"Invalid cookie format (expected 'name=value'): {value}")
92
+ name, _, val = value.partition("=")
93
+ name = name.strip()
94
+ if not name:
95
+ raise click.BadParameter(f"Invalid cookie format (empty name): {value}")
96
+ return name, val.strip()
97
+
98
+
99
+ def _has_header(headers: Mapping[str, str], name: str) -> bool:
100
+ lname = name.lower()
101
+ return any(k.lower() == lname for k in headers)
102
+
103
+
104
+ def build_headers(
105
+ header: Sequence[str] = (),
106
+ cookie: Sequence[str] = (),
107
+ session_token: str | None = None,
108
+ env: Mapping[str, str] | None = None,
109
+ ) -> dict[str, str]:
110
+ """Build the request headers for a chat or A2A call.
111
+
112
+ Precedence: explicit ``--header`` values are applied verbatim; ``--cookie``
113
+ pairs are folded into one ``Cookie`` header (appended to an explicit
114
+ ``Cookie`` header if one was given); ``--session-token`` becomes
115
+ ``X-Session-Token``; and when no ``Authorization`` header was supplied,
116
+ ``GRAPH_AGENTS_CLI_API_KEY`` from ``env`` (default ``os.environ``) becomes
117
+ ``Authorization: Bearer <key>``.
118
+ """
119
+ environ = os.environ if env is None else env
120
+ headers: dict[str, str] = {}
121
+ for raw in header:
122
+ key, val = parse_header(raw)
123
+ headers[key] = val
124
+
125
+ pairs = [parse_cookie(c) for c in cookie]
126
+ if pairs:
127
+ rendered = "; ".join(f"{k}={v}" for k, v in pairs)
128
+ existing_key = next((k for k in headers if k.lower() == "cookie"), None)
129
+ if existing_key is not None and headers[existing_key]:
130
+ headers[existing_key] = f"{headers[existing_key]}; {rendered}"
131
+ else:
132
+ headers[existing_key or "Cookie"] = rendered
133
+
134
+ if session_token:
135
+ headers[SESSION_TOKEN_HEADER] = session_token
136
+
137
+ if not _has_header(headers, "Authorization"):
138
+ api_key = environ.get(API_KEY_ENV, "")
139
+ if api_key:
140
+ headers["Authorization"] = f"Bearer {api_key}"
141
+
142
+ return headers
143
+
144
+
145
+ class RemoteTarget(NamedTuple):
146
+ """Where a query goes: the chat base URL or the resolved A2A base URL."""
147
+
148
+ mode: str
149
+ base_url: str
150
+ # For ``a2a``: the base under which the agent card was found. ``None``
151
+ # for ``chat``.
152
+ a2a_base: str | None = None
153
+ card: dict | None = None
154
+
155
+
156
+ def a2a_candidates(base_url: str, agent_directory: str | None) -> list[str]:
157
+ """A2A base URLs to probe for a card, in order.
158
+
159
+ The scaffolded app serves the card at ``/a2a/<agent_directory>``; the bare URL is tried second so a ``--url`` that already points
160
+ at the A2A base, or a spec-canonical root card, still works.
161
+ """
162
+ base = base_url.rstrip("/")
163
+ candidates: list[str] = []
164
+ if agent_directory:
165
+ candidates.append(f"{base}/a2a/{agent_directory}")
166
+ candidates.append(base)
167
+ return candidates
168
+
169
+
170
+ def classify_url(
171
+ url: str,
172
+ *,
173
+ mode: str = DEFAULT_RUN_MODE,
174
+ agent_directory: str | None = None,
175
+ headers: Mapping[str, str] | None = None,
176
+ timeout: float = 30.0,
177
+ ) -> RemoteTarget:
178
+ """Resolve a ``--url`` into a :class:`RemoteTarget` for ``mode``.
179
+
180
+ ``chat`` needs no probing: the chat API lives at ``<url>/chat``. ``a2a``
181
+ probes ``<url>/a2a/<agent_directory>/.well-known/agent-card.json`` and then
182
+ ``<url>/.well-known/agent-card.json``; the first 200 wins.
183
+
184
+ Raises :class:`click.ClickException` when no card is found (listing each
185
+ probe) or :class:`click.BadParameter` for an unknown mode.
186
+ """
187
+ base = url.rstrip("/")
188
+ if mode == "chat":
189
+ return RemoteTarget(mode="chat", base_url=base)
190
+ if mode != "a2a":
191
+ raise click.BadParameter(f"Unknown mode {mode!r}; choose from {', '.join(RUN_MODES)}")
192
+
193
+ failures: list[tuple[str, str]] = []
194
+ for candidate in a2a_candidates(base, agent_directory):
195
+ card_url = f"{candidate}{AGENT_CARD_PATH}"
196
+ try:
197
+ resp = _http.get(card_url, headers=dict(headers or {}), timeout=timeout)
198
+ except httpx.TransportError as exc:
199
+ failures.append((card_url, f"unreachable: {exc}"))
200
+ continue
201
+ if resp.status_code == 200:
202
+ try:
203
+ card = resp.json()
204
+ except ValueError:
205
+ card = None
206
+ return RemoteTarget(
207
+ mode="a2a",
208
+ base_url=base,
209
+ a2a_base=candidate,
210
+ card=card if isinstance(card, dict) else None,
211
+ )
212
+ failures.append((card_url, f"HTTP {resp.status_code}: {resp.text[:200]}"))
213
+
214
+ detail = "\n".join(f" {card_url}\n {why}" for card_url, why in failures)
215
+ hint = ""
216
+ if base.endswith("/a2a"):
217
+ hint += "\n Pass the base service URL without the '/a2a' suffix."
218
+ if any("HTTP 404" in why or "HTTP 405" in why for _, why in failures):
219
+ hint += "\n If this agent only exposes the chat API, try --mode chat instead."
220
+ raise click.ClickException(f"Failed to fetch an A2A agent card:\n{detail}{hint}")