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,141 @@
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
+
15
+ """Turn SIGTERM and SIGHUP into the same clean shutdown as Ctrl-C.
16
+
17
+ Python's default SIGTERM action ends the interpreter on the spot: no
18
+ ``finally``, no ``except BaseException``. The commands that start servers
19
+ (``run``, ``eval generate``, ``playground``) rely on exactly that cleanup to stop
20
+ the server they started and remove its pid file, so an IDE stop button, a CI
21
+ job timeout or a plain ``kill`` used to leave a detached server behind. Inside
22
+ :func:`terminate_like_interrupt` those signals raise :class:`TerminationSignal`
23
+ (a ``KeyboardInterrupt``), so every existing Ctrl-C cleanup path runs, and the
24
+ CLI exits with ``128 + signal number`` (143 for SIGTERM).
25
+
26
+ The cleanup itself runs under :func:`shielded`: a signal that arrives while a
27
+ server is being stopped (the first one, during a normal teardown) is held until
28
+ the pid file is gone, then handled, so no stale record is left behind.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ import contextlib
34
+ import signal
35
+ import threading
36
+ from collections.abc import Iterator
37
+ from types import FrameType
38
+
39
+
40
+ class TerminationSignal(KeyboardInterrupt):
41
+ """SIGTERM/SIGHUP received: unwinds like Ctrl-C, exits with ``128 + signum``."""
42
+
43
+ def __init__(self, signum: int) -> None:
44
+ super().__init__(signum)
45
+ self.signum = signum
46
+
47
+ @property
48
+ def exit_code(self) -> int:
49
+ return 128 + self.signum
50
+
51
+
52
+ def _signals() -> list[int]:
53
+ return [
54
+ sig for sig in (getattr(signal, "SIGTERM", None), getattr(signal, "SIGHUP", None)) if sig
55
+ ]
56
+
57
+
58
+ def _raise(signum: int, _frame: FrameType | None) -> None:
59
+ # The cleanup this starts (stopping a server, removing its pid file) must
60
+ # not be cut short by a second signal: ignore them until the block exits.
61
+ for sig in _signals():
62
+ with contextlib.suppress(ValueError, OSError):
63
+ signal.signal(sig, signal.SIG_IGN)
64
+ raise TerminationSignal(signum)
65
+
66
+
67
+ def _deliver(signum: int, handler: object) -> None:
68
+ """Act on a signal held by :func:`shielded` as the handler it was held from would."""
69
+ if handler is signal.SIG_IGN:
70
+ return
71
+ if handler is None or handler is signal.SIG_DFL:
72
+ # The default action (end the process), now that the cleanup is done.
73
+ signal.raise_signal(signum)
74
+ return
75
+ if callable(handler):
76
+ handler(signum, None) # raises TerminationSignal / KeyboardInterrupt
77
+
78
+
79
+ @contextlib.contextmanager
80
+ def shielded() -> Iterator[None]:
81
+ """Hold SIGINT, SIGTERM and SIGHUP until the block ends, then deliver the first one.
82
+
83
+ For cleanup that must not stop halfway: stopping a server and removing its
84
+ pid file. Unlike :func:`terminate_like_interrupt`, which turns the signal
85
+ into an exception wherever the main thread is (inside ``psutil.wait_procs``
86
+ of a teardown, say), a signal arriving here is only recorded; once the
87
+ block is done the previous handlers are back and the signal is handled as
88
+ they would have (a :class:`TerminationSignal`, a ``KeyboardInterrupt``, or
89
+ the default action). A no-op outside the main thread.
90
+ """
91
+ if threading.current_thread() is not threading.main_thread():
92
+ yield
93
+ return
94
+ held: list[int] = []
95
+
96
+ def _hold(signum: int, _frame: FrameType | None) -> None:
97
+ if not held:
98
+ held.append(signum)
99
+
100
+ previous: dict[int, object] = {}
101
+ for sig in (signal.SIGINT, *_signals()):
102
+ try:
103
+ previous[sig] = signal.signal(sig, _hold)
104
+ except (ValueError, OSError): # not supported on this platform
105
+ continue
106
+ try:
107
+ yield
108
+ finally:
109
+ for sig, handler in previous.items():
110
+ restore = signal.SIG_DFL if handler is None else handler
111
+ with contextlib.suppress(ValueError, OSError, TypeError):
112
+ signal.signal(sig, restore) # type: ignore[arg-type]
113
+ if held:
114
+ _deliver(held[0], previous.get(held[0]))
115
+
116
+
117
+ @contextlib.contextmanager
118
+ def terminate_like_interrupt() -> Iterator[None]:
119
+ """Within the block, SIGTERM and SIGHUP raise :class:`TerminationSignal`.
120
+
121
+ A no-op outside the main thread (only it may install handlers). The
122
+ previous handlers are restored on exit; once one signal has arrived, further
123
+ ones are ignored so the cleanup it triggers runs to the end.
124
+ """
125
+ if threading.current_thread() is not threading.main_thread():
126
+ yield
127
+ return
128
+ previous: dict[int, object] = {}
129
+ for sig in _signals():
130
+ try:
131
+ previous[sig] = signal.signal(sig, _raise)
132
+ except (ValueError, OSError): # not supported on this platform
133
+ continue
134
+ try:
135
+ yield
136
+ finally:
137
+ for sig, handler in previous.items():
138
+ # None: the previous handler was not installed from Python.
139
+ restore = signal.SIG_DFL if handler is None else handler
140
+ with contextlib.suppress(ValueError, OSError, TypeError):
141
+ signal.signal(sig, restore) # type: ignore[arg-type]
@@ -0,0 +1,530 @@
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
+
15
+ """graph-agents-cli approvals commands: list and decide the calls agent runs wait on.
16
+
17
+ A call an API's ``approval`` block gates is not sent until an allowed
18
+ approver decides it: ``requester`` (the principal who started the run) and/or
19
+ ``role:<name>`` (another principal holding that role). These commands are the
20
+ client of the chat API's approval routes, locally or against a deployed agent
21
+ (``--url``), with the credentials ``run`` sends:
22
+
23
+ * ``GET /threads/{thread_id}/approvals`` lists a thread's approvals;
24
+ * ``GET /approvals`` lists, across threads, the ones the caller may see (its
25
+ own, the ones its roles may decide, every one for a read-across role);
26
+ against an agent without it, the caller's own threads are searched;
27
+ * ``POST /threads/{thread_id}/approvals/{approval_id}`` decides one and
28
+ streams the resumed run (the same events as ``POST /chat``).
29
+
30
+ The decision carries only ``decision`` and ``comment``: the server sends
31
+ exactly the call it recorded, once, or nothing.
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ import json
37
+ import os
38
+ from collections.abc import Callable, Iterator
39
+ from contextlib import contextmanager
40
+ from dataclasses import replace
41
+ from pathlib import Path
42
+ from typing import Any, NamedTuple
43
+
44
+ import click
45
+ import httpx
46
+
47
+ from graph_agents_cli import _chat_client
48
+ from graph_agents_cli._approvals import STATUS_PENDING, Approval, awaiting_lines, safe_text
49
+ from graph_agents_cli._chat_client import (
50
+ DECISION_APPROVE,
51
+ DECISION_REJECT,
52
+ STATUS_AWAITING_APPROVAL,
53
+ ChatClientError,
54
+ ChatHTTPError,
55
+ )
56
+ from graph_agents_cli._click import LazyGroup
57
+ from graph_agents_cli._project import (
58
+ chdir_project_root,
59
+ read_project_config,
60
+ require_agent_directory,
61
+ )
62
+ from graph_agents_cli._remote import API_KEY_ENV, build_headers
63
+ from graph_agents_cli.run import _local_server
64
+ from graph_agents_cli.run._signals import terminate_like_interrupt
65
+ from graph_agents_cli.run.cmd_run import (
66
+ AgentError,
67
+ DecisionRefused,
68
+ _auth_hint,
69
+ _build_resume_flags,
70
+ _ChatRenderer,
71
+ _local_api_key,
72
+ _project_auth_policy,
73
+ _TransportFailure,
74
+ decision_refused_message,
75
+ print_approval,
76
+ render_chat_events,
77
+ )
78
+
79
+ # What `list` / a decision without --thread-id look through: GET /approvals
80
+ # pages of 100 (or, against an agent without that route, the caller's most
81
+ # recently active threads, GET /threads pages of 100).
82
+ THREAD_PAGE = 100
83
+ MAX_THREAD_PAGES = 5
84
+
85
+
86
+ class NoPendingRun(click.ClickException):
87
+ """Nothing can be pending where the command looked (exit 1)."""
88
+
89
+ exit_code = 1
90
+
91
+
92
+ class _Target(NamedTuple):
93
+ base_url: str
94
+ headers: dict[str, str]
95
+ remote: bool
96
+ flags: str # repeated in printed commands (credentials redacted)
97
+
98
+
99
+ def _local_checkpointer(project_root: Path, configured: str) -> str:
100
+ """The checkpointer a local server would run with (the environment, then .env, then the manifest)."""
101
+ env = {**_local_server.dotenv_settings(project_root / ".env"), **os.environ}
102
+ return (env.get("CHECKPOINTER") or configured or "memory").strip().lower()
103
+
104
+
105
+ @contextmanager
106
+ def _target(url: str | None, header: tuple[str, ...], cookie: tuple[str, ...]) -> Iterator[_Target]:
107
+ """Where the approvals live: ``--url``, or the project's local server.
108
+
109
+ Locally, the running server (``run --start-server``, or the one a paused
110
+ run kept) is used. With none running, a temporary server is started (and
111
+ stopped afterwards) only where a paused run and its approval outlive
112
+ their server: ``fastapi`` with a postgres checkpointer, and
113
+ ``langgraph-server``, whose ``langgraph dev`` keeps its threads and the
114
+ app's approvals in ``.langgraph_api/`` of the project. With the fastapi
115
+ runtime's in-memory checkpointer, nothing can be pending.
116
+ """
117
+ headers = build_headers(header, cookie)
118
+ flags = _build_resume_flags(url, "chat", header, cookie, None)
119
+ if url:
120
+ yield _Target(url.rstrip("/"), headers, True, flags)
121
+ return
122
+ chdir_project_root()
123
+ cfg = read_project_config()
124
+ require_agent_directory(cfg)
125
+ root = Path.cwd()
126
+ if not any(k.lower() == "authorization" for k in headers):
127
+ api_key = _local_api_key(root)
128
+ if api_key:
129
+ headers["Authorization"] = f"Bearer {api_key}"
130
+ port = _local_server.get_server_port(root)
131
+ if port is not None:
132
+ _local_server.touch_activity(root)
133
+ yield _Target(f"http://127.0.0.1:{port}", headers, False, flags)
134
+ return
135
+ runtime = getattr(cfg, "runtime", "fastapi") or "fastapi"
136
+ checkpointer = _local_checkpointer(root, getattr(cfg, "checkpointer", "memory") or "memory")
137
+ if runtime == "fastapi" and checkpointer != "postgres":
138
+ raise NoPendingRun(
139
+ "No local server is running, and with the in-memory checkpointer "
140
+ f"(CHECKPOINTER={checkpointer}) a paused run lives only in the server that ran it: "
141
+ "nothing is pending. A run that pauses keeps its one-off server running; "
142
+ "`run --start-server` keeps one explicitly. Pass --url for a deployed agent."
143
+ )
144
+ with terminate_like_interrupt():
145
+ server = _local_server.ensure_server(
146
+ root, cfg.agent_directory, runtime=runtime, checkpointer=checkpointer
147
+ )
148
+ try:
149
+ yield _Target(server.base_url, headers, False, flags)
150
+ finally:
151
+ if server.started:
152
+ try:
153
+ _local_server.stop_server(root, pid=server.pid)
154
+ except _local_server.ServerStopError as exc: # never replaces the result
155
+ _local_server.warn_not_stopped(exc)
156
+
157
+
158
+ def _http_failure(exc: ChatHTTPError, target: _Target, what: str) -> click.ClickException:
159
+ if exc.status_code == 401:
160
+ hint = _auth_hint(_project_auth_policy(remote=target.remote), remote=target.remote)
161
+ return click.ClickException(f"{what} failed (HTTP 401): not authenticated.{hint}")
162
+ if exc.status_code == 403:
163
+ return click.ClickException(
164
+ f"{what} failed (HTTP 403): you may not see this thread's approvals (its owner, its "
165
+ "approvers and read-across roles may)."
166
+ )
167
+ if exc.status_code == 404:
168
+ return click.ClickException(
169
+ f"{what} failed (HTTP 404): no such thread (a wrong id, a deleted thread, or an "
170
+ "agent without approval routes: check --url)."
171
+ )
172
+ return click.ClickException(f"{what} failed (HTTP {exc.status_code}):\n {exc.body}")
173
+
174
+
175
+ def _transport_failure(exc: httpx.TransportError, target: _Target) -> _TransportFailure:
176
+ where = target.base_url if target.remote else "the local server"
177
+ return _TransportFailure(f"Could not reach {where}: {exc}")
178
+
179
+
180
+ def _thread_approvals(target: _Target, thread_id: str) -> list[Approval]:
181
+ """A thread's approvals; each is decided on the thread it was listed for."""
182
+ rows = _chat_client.list_approvals(target.base_url, thread_id, headers=target.headers)
183
+ found = [Approval.from_payload(row, thread_id=thread_id) for row in rows]
184
+ return [replace(a, thread_id=thread_id) for a in found if a is not None]
185
+
186
+
187
+ def _own_thread_ids(target: _Target) -> tuple[list[str], bool]:
188
+ """The caller's thread ids (most recent first, as the server lists them); True when cut."""
189
+ ids: list[str] = []
190
+ for page in range(MAX_THREAD_PAGES):
191
+ rows = _chat_client.list_threads(
192
+ target.base_url, headers=target.headers, limit=THREAD_PAGE, offset=page * THREAD_PAGE
193
+ )
194
+ ids.extend(str(r["thread_id"]) for r in rows if isinstance(r.get("thread_id"), str))
195
+ if len(rows) < THREAD_PAGE:
196
+ return ids, False
197
+ return ids, True
198
+
199
+
200
+ def _visible_approvals(
201
+ target: _Target, *, pending_only: bool
202
+ ) -> tuple[list[Approval], bool] | None:
203
+ """GET /approvals, page by page: every approval the caller may see; True when cut.
204
+
205
+ None when the agent has no such route (404/405 on the first page).
206
+ """
207
+ found: list[Approval] = []
208
+ for page in range(MAX_THREAD_PAGES):
209
+ try:
210
+ rows = _chat_client.list_visible_approvals(
211
+ target.base_url,
212
+ status=STATUS_PENDING if pending_only else None,
213
+ headers=target.headers,
214
+ limit=THREAD_PAGE,
215
+ offset=page * THREAD_PAGE,
216
+ )
217
+ except ChatHTTPError as exc:
218
+ if page == 0 and exc.status_code in (404, 405):
219
+ return None
220
+ raise
221
+ for row in rows:
222
+ approval = Approval.from_payload(row)
223
+ if approval is not None and approval.thread_id:
224
+ found.append(approval)
225
+ if len(rows) < THREAD_PAGE:
226
+ return found, False
227
+ return found, True
228
+
229
+
230
+ class _Collected(NamedTuple):
231
+ approvals: list[Approval]
232
+ cut: bool # more than the pages looked through
233
+ own_threads_only: bool # the fallback scan: another principal's thread needs --thread-id
234
+
235
+
236
+ def _collect(target: _Target, thread_id: str | None, *, pending_only: bool = False) -> _Collected:
237
+ """Approvals of one thread, or every one the caller may see (see the module doc)."""
238
+ if thread_id:
239
+ return _Collected(_thread_approvals(target, thread_id), False, False)
240
+ visible = _visible_approvals(target, pending_only=pending_only)
241
+ if visible is not None:
242
+ return _Collected(visible[0], visible[1], False)
243
+ thread_ids, cut = _own_thread_ids(target)
244
+ approvals: list[Approval] = []
245
+ for tid in thread_ids:
246
+ try:
247
+ approvals.extend(_thread_approvals(target, tid))
248
+ except ChatHTTPError as exc:
249
+ if exc.status_code in (403, 404): # deleted meanwhile, or not visible: skip it
250
+ continue
251
+ raise
252
+ return _Collected(approvals, cut, True)
253
+
254
+
255
+ def _common_options(function: Callable[..., Any]) -> Callable[..., Any]:
256
+ function = click.option(
257
+ "--cookie",
258
+ "cookie",
259
+ multiple=True,
260
+ help="Cookie ('name=value'), for a custom auth policy that reads cookies. Repeatable.",
261
+ )(function)
262
+ function = click.option(
263
+ "--header",
264
+ "-H",
265
+ "header",
266
+ multiple=True,
267
+ help=(
268
+ "Custom HTTP header ('Key: Value'). Repeatable. An Authorization header overrides "
269
+ f"{API_KEY_ENV}; prefer the variable for bearer credentials."
270
+ ),
271
+ )(function)
272
+ function = click.option(
273
+ "--url",
274
+ default=None,
275
+ help="Base URL of a deployed agent. Without it, the project's local server.",
276
+ )(function)
277
+ return function
278
+
279
+
280
+ @click.group("approvals", cls=LazyGroup)
281
+ def approvals_group() -> None:
282
+ """List and decide the gated API calls agent runs are waiting on.
283
+
284
+ A call gated by an API's approval block in api-policy.yaml is not sent
285
+ until an approver decides it: requester (the principal who started the
286
+ run) and/or role:<name> (a principal holding that role; a requester
287
+ decides their own call only when requester is listed). Approving sends
288
+ exactly the call shown, once; rejecting, or letting it expire, sends
289
+ nothing and the agent is told it was not approved.
290
+
291
+ \b
292
+ Credentials follow the project's auth policy, as for `run`: put a bearer
293
+ credential in GRAPH_AGENTS_CLI_API_KEY (locally, the API_KEY in .env is
294
+ used when it is unset), or pass --header / --cookie. Each approver uses
295
+ their own credential.
296
+
297
+ \b
298
+ Exit codes:
299
+ 0 listed, or decided (the resumed run was shown)
300
+ 1 refused (not an allowed approver, already decided, expired, not
301
+ found), or the resumed run ended with an error
302
+ 2 the agent could not be reached
303
+ 3 configuration error (not in a project without --url)
304
+ """
305
+
306
+
307
+ @approvals_group.command("list")
308
+ @_common_options
309
+ @click.option(
310
+ "--thread-id",
311
+ default=None,
312
+ help=(
313
+ "The thread to list. Without it: every approval you may see (your own, the ones "
314
+ "your roles may decide, all of them for a read-across role; the newest "
315
+ f"{THREAD_PAGE * MAX_THREAD_PAGES})."
316
+ ),
317
+ )
318
+ @click.option(
319
+ "--all", "show_all", is_flag=True, default=False, help="Also list decided and expired ones."
320
+ )
321
+ @click.option("--json", "as_json", is_flag=True, default=False, help="Print JSON.")
322
+ def cmd_list(
323
+ url: str | None,
324
+ header: tuple[str, ...],
325
+ cookie: tuple[str, ...],
326
+ thread_id: str | None,
327
+ show_all: bool,
328
+ as_json: bool,
329
+ ) -> None:
330
+ """List pending approvals (a thread's, or every one you may see)."""
331
+ try:
332
+ with _target(url, header, cookie) as target:
333
+ flags = target.flags
334
+ try:
335
+ approvals, cut, own_threads_only = _collect(
336
+ target, thread_id, pending_only=not show_all
337
+ )
338
+ except ChatHTTPError as exc:
339
+ raise _http_failure(exc, target, "Listing approvals") from exc
340
+ except httpx.TransportError as exc:
341
+ raise _transport_failure(exc, target) from exc
342
+ except ChatClientError as exc:
343
+ raise click.ClickException(f"Listing approvals failed: {exc}") from exc
344
+ except NoPendingRun as exc:
345
+ if as_json:
346
+ click.echo(json.dumps({"approvals": [], "note": exc.message}, indent=2))
347
+ else:
348
+ click.echo(exc.message)
349
+ return
350
+ shown = [a for a in approvals if show_all or a.pending]
351
+ if as_json:
352
+ payload = {"approvals": [dict(a.raw, thread_id=a.thread_id) for a in shown]}
353
+ if cut:
354
+ payload["truncated"] = True
355
+ # ASCII-escaped: the call's text came from the model.
356
+ click.echo(json.dumps(payload, indent=2))
357
+ return
358
+ what = "approvals" if show_all else "pending approvals"
359
+ if thread_id:
360
+ where = f"thread {safe_text(thread_id)}"
361
+ else:
362
+ where = "your threads" if own_threads_only else "the threads you may see"
363
+ if not shown:
364
+ click.echo(f"No {what} on {where}.")
365
+ for approval in shown:
366
+ click.echo()
367
+ for line in approval.lines():
368
+ click.echo(line)
369
+ if approval.pending and approval.thread_id:
370
+ for line in awaiting_lines(approval, approval.thread_id, flags)[1:3]:
371
+ click.echo(line)
372
+ if cut:
373
+ searched = (
374
+ f"your {THREAD_PAGE * MAX_THREAD_PAGES} most recent threads"
375
+ if own_threads_only
376
+ else f"the newest {THREAD_PAGE * MAX_THREAD_PAGES} approvals"
377
+ )
378
+ click.secho(
379
+ f"Only {searched} were searched; pass --thread-id for an older one.", fg="yellow"
380
+ )
381
+ if own_threads_only:
382
+ click.secho(
383
+ "This agent lists approvals per thread only: one on another principal's thread "
384
+ "(a role: approver's) needs --thread-id.",
385
+ dim=True,
386
+ )
387
+
388
+
389
+ def _find(target: _Target, approval_id: str, thread_id: str | None) -> Approval:
390
+ approvals, cut, own_threads_only = _collect(target, thread_id)
391
+ for approval in approvals:
392
+ if approval.approval_id == approval_id:
393
+ return approval
394
+ if thread_id:
395
+ where, extra = f"thread {safe_text(thread_id)}", ""
396
+ elif own_threads_only:
397
+ where = "your threads"
398
+ extra = (
399
+ "; an approval on another principal's thread needs --thread-id (the requester's "
400
+ "`run` printed it)"
401
+ )
402
+ else:
403
+ where, extra = "the threads you may see", ""
404
+ if cut:
405
+ extra += "; only the most recent ones were searched (pass --thread-id)"
406
+ raise NoPendingRun(f"No approval {safe_text(approval_id)} on {where}{extra}.")
407
+
408
+
409
+ def _decide(
410
+ decision: str,
411
+ approval_id: str,
412
+ *,
413
+ url: str | None,
414
+ header: tuple[str, ...],
415
+ cookie: tuple[str, ...],
416
+ thread_id: str | None,
417
+ comment: str | None,
418
+ verbose: bool,
419
+ ) -> None:
420
+ with _target(url, header, cookie) as target:
421
+ try:
422
+ approval = _find(target, approval_id, thread_id)
423
+ except ChatHTTPError as exc:
424
+ raise _http_failure(exc, target, "Looking up the approval") from exc
425
+ except httpx.TransportError as exc:
426
+ raise _transport_failure(exc, target) from exc
427
+ except ChatClientError as exc:
428
+ raise click.ClickException(f"Looking up the approval failed: {exc}") from exc
429
+ # The decider sees the call before it is decided, whoever started the run.
430
+ print_approval(approval)
431
+ if not approval.pending:
432
+ raise DecisionRefused(
433
+ f"Approval {safe_text(approval_id)} is {safe_text(approval.status)}, not pending: "
434
+ "an approval is decided once."
435
+ )
436
+ assert approval.thread_id is not None
437
+ verb = "Approving" if decision == DECISION_APPROVE else "Rejecting"
438
+ click.secho(
439
+ f"{verb}; the resumed run follows.",
440
+ fg="green" if decision == DECISION_APPROVE else "yellow",
441
+ )
442
+ renderer = _ChatRenderer(verbose=verbose)
443
+ try:
444
+ events = _chat_client.decide_approval(
445
+ target.base_url,
446
+ approval.thread_id,
447
+ approval.approval_id,
448
+ decision,
449
+ comment=comment,
450
+ headers=target.headers,
451
+ )
452
+ outcome = render_chat_events(events, renderer=renderer)
453
+ except ChatHTTPError as exc:
454
+ raise DecisionRefused(
455
+ decision_refused_message(
456
+ exc,
457
+ approval_id,
458
+ remote=target.remote,
459
+ policy=_project_auth_policy(remote=target.remote),
460
+ )
461
+ ) from exc
462
+ except AgentError as exc:
463
+ click.secho(f"[error: {exc.code}]: {exc.message}", fg="red")
464
+ raise click.exceptions.Exit(1) from exc
465
+ except httpx.TransportError as exc:
466
+ raise _TransportFailure(
467
+ f"The connection dropped while the resumed run streamed: {exc}\n"
468
+ " The decision was recorded if the server answered; check with "
469
+ f"`graph-agents-cli approvals list --all --thread-id {approval.thread_id}`."
470
+ ) from exc
471
+ if outcome.status == STATUS_AWAITING_APPROVAL:
472
+ nxt = Approval.from_payload(outcome.approval, thread_id=approval.thread_id)
473
+ if nxt is not None:
474
+ nxt = replace(nxt, thread_id=approval.thread_id)
475
+ print_approval(nxt)
476
+ click.echo()
477
+ lines = awaiting_lines(nxt, nxt.thread_id, target.flags)
478
+ click.secho(lines[0], fg="yellow", bold=True)
479
+ for line in lines[1:]:
480
+ click.secho(line, fg="yellow")
481
+
482
+
483
+ def _decision_command(decision: str) -> click.Command:
484
+ approve = decision == DECISION_APPROVE
485
+ summary = (
486
+ "Approve a pending call: it is sent exactly as shown, once, and the run resumes."
487
+ if approve
488
+ else "Reject a pending call: it is never sent, and the run resumes without it."
489
+ )
490
+
491
+ @click.command(decision, help=summary, short_help=summary)
492
+ @click.argument("approval_id")
493
+ @_common_options
494
+ @click.option(
495
+ "--thread-id",
496
+ default=None,
497
+ help=(
498
+ "The approval's thread (printed with the approval). Without it, every approval "
499
+ "you may see is searched."
500
+ ),
501
+ )
502
+ @click.option("--comment", default=None, help="Why; recorded with the decision.")
503
+ @click.option(
504
+ "--verbose", "-v", is_flag=True, default=False, help="Print each event of the resumed run."
505
+ )
506
+ def command(
507
+ approval_id: str,
508
+ url: str | None,
509
+ header: tuple[str, ...],
510
+ cookie: tuple[str, ...],
511
+ thread_id: str | None,
512
+ comment: str | None,
513
+ verbose: bool,
514
+ ) -> None:
515
+ _decide(
516
+ decision,
517
+ approval_id,
518
+ url=url,
519
+ header=header,
520
+ cookie=cookie,
521
+ thread_id=thread_id,
522
+ comment=comment,
523
+ verbose=verbose,
524
+ )
525
+
526
+ return command
527
+
528
+
529
+ approvals_group.add_command(_decision_command(DECISION_APPROVE))
530
+ approvals_group.add_command(_decision_command(DECISION_REJECT))