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,400 @@
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
+ """Client side of the human approval gate: the paused call, shown safely, and how to decide it.
16
+
17
+ An API's ``approval`` block in ``api-policy.yaml`` makes the agent pause
18
+ before sending a gated call. The chat API then ends the run's stream with
19
+ ``message.end`` whose ``status`` is ``awaiting_approval`` and whose
20
+ ``approval`` describes the call (``approval_id``, ``api``, ``method``,
21
+ ``path``, ``query``, ``body``, ``operation_id``, ``reason``, ``approvers``,
22
+ ``expires_at``). ``GET /threads/{thread_id}/approvals`` lists a thread's
23
+ approvals and ``POST /threads/{thread_id}/approvals/{approval_id}`` decides
24
+ one. ``run``, ``approvals`` and ``eval generate`` share this module. A decision
25
+ this agent relays to another agent carries ``effect`` (the call that will
26
+ actually happen there) and ``nested`` (the approvals it passes through): they
27
+ are printed first.
28
+
29
+ Everything in an approval except its id came, one way or another, from the
30
+ model (the path's ids, the body, the stated reason), and the model may have
31
+ read text an attacker wrote. A human decides from what is printed here, so
32
+ it is printed in full (never cut) and with every control, format and
33
+ separator character escaped: no text can move the cursor, recolour the
34
+ screen, reorder characters or fake a line of this output.
35
+ """
36
+
37
+ from __future__ import annotations
38
+
39
+ import json
40
+ import re
41
+ import shlex
42
+ import unicodedata
43
+ from collections.abc import Mapping, Sequence
44
+ from dataclasses import dataclass, field
45
+ from typing import Any
46
+
47
+ from graph_agents_cli._api_policy import (
48
+ HTTP_METHODS,
49
+ REQUESTER_APPROVER,
50
+ path_matches,
51
+ path_template_problem,
52
+ )
53
+ from graph_agents_cli._chat_client import DECISION_APPROVE, DECISION_REJECT
54
+
55
+ # Approval statuses the server reports.
56
+ STATUS_PENDING = "pending"
57
+ STATUS_APPROVED = "approved"
58
+ STATUS_REJECTED = "rejected"
59
+ STATUS_EXPIRED = "expired"
60
+
61
+ # What an eval case's `approvals` instructions and `expect.approvals` match a gate on.
62
+ MATCH_KEYS = ("api", "operation_id", "method", "path")
63
+
64
+ # The refusals of `POST /threads/{thread_id}/approvals/{approval_id}`.
65
+ DECISION_REFUSALS: dict[int, str] = {
66
+ 403: (
67
+ "you are not an allowed approver of this call. requester is the principal who "
68
+ "started the run; role:<name> is any other principal holding that role (a requester "
69
+ "decides their own call only when requester is listed)"
70
+ ),
71
+ 404: "no such approval on that thread (a wrong id, or the thread was deleted)",
72
+ 409: "it is not pending any more: it was already decided (an approval is single-use)",
73
+ 410: "it expired, which rejected the call: nothing was sent; ask the agent again",
74
+ }
75
+
76
+ _ESCAPED_CATEGORIES = frozenset({"Cc", "Cf", "Zl", "Zp", "Cs", "Co", "Cn"})
77
+
78
+
79
+ def safe_text(value: Any) -> str:
80
+ """``value`` as one line of terminal-safe text.
81
+
82
+ Every control (C0, DEL, C1), format (bidi overrides, zero-width marks),
83
+ line/paragraph separator, surrogate, private-use or unassigned character is
84
+ written as its ``\\x``/``\\u``/``\\U`` escape, so what the terminal shows
85
+ is exactly what the value holds.
86
+ """
87
+ text = value if isinstance(value, str) else str(value)
88
+ out: list[str] = []
89
+ for char in text:
90
+ if unicodedata.category(char) in _ESCAPED_CATEGORIES:
91
+ code = ord(char)
92
+ if code <= 0xFF:
93
+ out.append(f"\\x{code:02x}")
94
+ elif code <= 0xFFFF:
95
+ out.append(f"\\u{code:04x}")
96
+ else:
97
+ out.append(f"\\U{code:08x}")
98
+ else:
99
+ out.append(char)
100
+ return "".join(out)
101
+
102
+
103
+ # What streamed agent text may not send to the terminal: C0 controls but tab and
104
+ # newline (ESC starts every cursor, colour and conceal sequence), DEL, C1 controls
105
+ # (0x9b is a one-byte CSI) and the bidi embedding, override and isolate marks.
106
+ _TERMINAL_CONTROLS = re.compile("[\\x00-\\x08\\x0b-\\x1f\\x7f-\\x9f\\u202a-\\u202e\\u2066-\\u2069]")
107
+
108
+
109
+ def terminal_text(text: str) -> str:
110
+ """Streamed agent text with the characters that steer a terminal escaped.
111
+
112
+ Line breaks and tabs stay (a reply has paragraphs), so the text reads as
113
+ written; nothing in it can hide, recolour or overwrite the lines around
114
+ it, such as the approval shown before "Approve? [y/N]".
115
+ """
116
+ return _TERMINAL_CONTROLS.sub(lambda m: safe_text(m.group(0)), text)
117
+
118
+
119
+ def json_lines(value: Any) -> list[str]:
120
+ """``value`` as indented JSON, one terminal-safe line per list item.
121
+
122
+ ``json.dumps`` escapes the newlines inside strings, so the line breaks left
123
+ are the ones indentation adds; each line is then passed through ``safe_text``.
124
+ """
125
+ try:
126
+ text = json.dumps(value, indent=2, ensure_ascii=False, sort_keys=True, default=str)
127
+ except (TypeError, ValueError):
128
+ text = repr(value)
129
+ return [safe_text(line) for line in text.split("\n")]
130
+
131
+
132
+ def _opt_str(value: Any) -> str | None:
133
+ return value if isinstance(value, str) and value else None
134
+
135
+
136
+ @dataclass(frozen=True)
137
+ class Approval:
138
+ """One approval: the paused call and who may decide it."""
139
+
140
+ approval_id: str
141
+ thread_id: str | None = None
142
+ api: str | None = None
143
+ method: str | None = None
144
+ path: str | None = None
145
+ query: Any = None
146
+ body: Any = None
147
+ operation_id: str | None = None
148
+ reason: str | None = None
149
+ approvers: tuple[str, ...] = ()
150
+ expires_at: str | None = None
151
+ # pending / approved / rejected / expired (listings); None in a paused run's payload.
152
+ status: str | None = None
153
+ decided_at: str | None = None
154
+ comment: str | None = None
155
+ raw: Mapping[str, Any] = field(default_factory=dict, compare=False, repr=False)
156
+
157
+ @classmethod
158
+ def from_payload(cls, payload: Any, *, thread_id: str | None = None) -> Approval | None:
159
+ """An approval from the server's JSON (a paused run's or a listing row); None without an id."""
160
+ if not isinstance(payload, Mapping):
161
+ return None
162
+ approval_id = _opt_str(payload.get("approval_id")) or _opt_str(payload.get("id"))
163
+ if approval_id is None:
164
+ return None
165
+ approvers = payload.get("approvers")
166
+ method = _opt_str(payload.get("method"))
167
+ return cls(
168
+ approval_id=approval_id,
169
+ thread_id=_opt_str(payload.get("thread_id")) or thread_id,
170
+ api=_opt_str(payload.get("api")),
171
+ method=method.upper() if method else None,
172
+ path=_opt_str(payload.get("path")),
173
+ query=payload.get("query"),
174
+ body=payload.get("body"),
175
+ operation_id=_opt_str(payload.get("operation_id")),
176
+ reason=_opt_str(payload.get("reason")),
177
+ approvers=tuple(str(a) for a in approvers) if isinstance(approvers, list) else (),
178
+ expires_at=_opt_str(payload.get("expires_at")),
179
+ status=_opt_str(payload.get("status")),
180
+ decided_at=_opt_str(payload.get("decided_at")),
181
+ comment=_opt_str(payload.get("comment")),
182
+ raw=dict(payload),
183
+ )
184
+
185
+ @property
186
+ def pending(self) -> bool:
187
+ """True unless a listing says it was decided or expired."""
188
+ return self.status in (None, STATUS_PENDING)
189
+
190
+ @property
191
+ def requester_may_decide(self) -> bool:
192
+ """Whether the principal who started the run is one of the approvers."""
193
+ return REQUESTER_APPROVER in self.approvers
194
+
195
+ def call_line(self) -> str:
196
+ """``POST /orders/ORD-1/cancel (api orders, operation cancelOrder)``, terminal-safe."""
197
+ call = f"{safe_text(self.method or '?')} {safe_text(self.path or '?')}"
198
+ details = []
199
+ if self.api:
200
+ details.append(f"api {safe_text(self.api)}")
201
+ if self.operation_id:
202
+ details.append(f"operation {safe_text(self.operation_id)}")
203
+ return f"{call} ({', '.join(details)})" if details else call
204
+
205
+ def effect_lines(self) -> list[str]:
206
+ """What a relayed decision will do (``effect``) and through which approvals (``nested``).
207
+
208
+ ``orders (via billing) will POST /orders/ORD-1002/cancel (cancelOrder), as reported
209
+ by orders``, its body, and one ``via`` line per agent; empty for any other approval.
210
+ """
211
+ effect = self.raw.get("effect")
212
+ if not isinstance(effect, Mapping):
213
+ return []
214
+ via = [str(v) for v in effect.get("via") or [] if v]
215
+ agent = str(effect.get("agent") or "?")
216
+ hops = via[:-1] if via and via[-1] == agent else via
217
+ who = safe_text(agent) + (f" (via {', '.join(safe_text(h) for h in hops)})" if hops else "")
218
+ what = f"{safe_text(effect.get('method') or '?')} {safe_text(effect.get('path') or '?')}"
219
+ if effect.get("operation_id"):
220
+ what += f" ({safe_text(effect['operation_id'])})"
221
+ reported = f", as reported by {safe_text(via[-1])}" if via else ""
222
+ lines = [f" effect: {who} will {what}{reported}"]
223
+ for key in ("query", "body"):
224
+ if effect.get(key) not in (None, {}, [], ""):
225
+ shown = json_lines(effect[key])
226
+ lines.append(f" {key + ':':<11}{shown[0]}")
227
+ lines.extend(f" {line}" for line in shown[1:])
228
+ if effect.get("expires_at"):
229
+ lines.append(
230
+ f" expires: {safe_text(effect['expires_at'])} (at {safe_text(agent)})"
231
+ )
232
+ level, depth = self.raw.get("nested"), 0
233
+ while isinstance(level, Mapping) and depth < 8:
234
+ decided = safe_text(level.get("decide_with") or "?")
235
+ lines.append(
236
+ f" via: {safe_text(level.get('agent') or '?')} approval "
237
+ f"{safe_text(level.get('approval_id') or '?')} (decided there: {decided})"
238
+ )
239
+ level, depth = level.get("nested"), depth + 1
240
+ return lines
241
+
242
+ def lines(self) -> list[str]:
243
+ """The approval in full, as the human deciding it must see it (what it will do first)."""
244
+ lines = [*self.effect_lines(), f" call: {self.call_line()}"]
245
+ if self.query not in (None, {}, [], ""):
246
+ query = json_lines(self.query)
247
+ lines.append(f" query: {query[0]}")
248
+ lines.extend(f" {line}" for line in query[1:])
249
+ if self.body is not None:
250
+ body = json_lines(self.body)
251
+ lines.append(f" body: {body[0]}")
252
+ lines.extend(f" {line}" for line in body[1:])
253
+ else:
254
+ lines.append(" body: (none)")
255
+ if self.reason:
256
+ lines.append(f" reason: {safe_text(self.reason)}")
257
+ approvers = ", ".join(safe_text(a) for a in self.approvers) or "(not stated)"
258
+ lines.append(f" approvers: {approvers}")
259
+ if self.expires_at:
260
+ lines.append(f" expires at: {safe_text(self.expires_at)}")
261
+ if self.status:
262
+ status = safe_text(self.status)
263
+ if self.decided_at:
264
+ status += f" at {safe_text(self.decided_at)}"
265
+ lines.append(f" status: {status}")
266
+ if self.comment:
267
+ lines.append(f" comment: {safe_text(self.comment)}")
268
+ lines.append(f" approval id: {safe_text(self.approval_id)}")
269
+ if self.thread_id:
270
+ lines.append(f" thread: {safe_text(self.thread_id)}")
271
+ return lines
272
+
273
+
274
+ # The marker `run` puts where it leaves a credential out of a printed command.
275
+ REDACTED_MARK = "<redacted>"
276
+
277
+
278
+ def decide_commands(
279
+ approval: Approval,
280
+ thread_id: str,
281
+ flags: str = "",
282
+ *,
283
+ verbs: Sequence[str] = ("approve",),
284
+ options: str = "",
285
+ ) -> list[str]:
286
+ """The ``graph-agents-cli approvals approve|reject ...`` commands that decide ``approval``.
287
+
288
+ Ids come from the server: they are shell-quoted so a pasted command runs
289
+ exactly one ``graph-agents-cli`` invocation, and an id that starts with
290
+ ``-`` goes after ``--`` so it cannot be read as an option (``--url=...``
291
+ would send the decision elsewhere). ``flags`` (``--url``, headers with
292
+ credentials redacted) and ``options`` (``--comment ...``) start with a
293
+ space when not empty.
294
+ """
295
+ approval_id = shlex.quote(approval.approval_id)
296
+ where = f"--thread-id {shlex.quote(thread_id)}{flags}{options}"
297
+ commands = []
298
+ for verb in verbs:
299
+ if approval.approval_id.startswith("-"):
300
+ commands.append(f"graph-agents-cli approvals {verb} {where} -- {approval_id}")
301
+ else:
302
+ commands.append(f"graph-agents-cli approvals {verb} {approval_id} {where}")
303
+ return commands
304
+
305
+
306
+ def awaiting_lines(approval: Approval, thread_id: str | None, flags: str = "") -> list[str]:
307
+ """What a paused run prints: who decides, and the exact commands to decide it."""
308
+ who = ", ".join(safe_text(a) for a in approval.approvers) or "an allowed approver"
309
+ lines = [
310
+ f"Awaiting approval by {who}: the call was not sent; the run is paused until it is decided."
311
+ ]
312
+ if thread_id:
313
+ (approve,) = decide_commands(approval, thread_id, flags, verbs=(DECISION_APPROVE,))
314
+ (reject,) = decide_commands(
315
+ approval, thread_id, flags, verbs=(DECISION_REJECT,), options=' --comment "<why>"'
316
+ )
317
+ lines.append(f" Approve: {approve}")
318
+ lines.append(f" Reject: {reject}")
319
+ if REDACTED_MARK in flags:
320
+ lines.append(" (re-supply the redacted credential values)")
321
+ if not approval.approvers:
322
+ lines.append(
323
+ " The server did not say who may decide it: an allowed approver runs these "
324
+ "commands with their own credentials."
325
+ )
326
+ elif not approval.requester_may_decide:
327
+ lines.append(
328
+ " Only another principal can decide it (requester is not an approver): send "
329
+ "them these commands; they run them with their own credentials."
330
+ )
331
+ if approval.expires_at:
332
+ lines.append(f" It expires (rejected, nothing sent) at {safe_text(approval.expires_at)}.")
333
+ return lines
334
+
335
+
336
+ # ---------------------------------------------------------------------------
337
+ # Eval: matching a gate
338
+ # ---------------------------------------------------------------------------
339
+
340
+
341
+ def match_problem(match: Any) -> str | None:
342
+ """Why an eval ``match`` object cannot name a gated call, or None.
343
+
344
+ ``{"operation_id": ...}`` or ``{"method": ..., "path": ...}`` (a path
345
+ template: ``{name}`` matches one segment), or all three; ``api`` narrows
346
+ it to one API.
347
+ """
348
+ if not isinstance(match, Mapping) or not match:
349
+ return 'must be an object like {"operation_id": "..."} or {"method": "...", "path": "..."}'
350
+ unknown = sorted(set(match) - set(MATCH_KEYS), key=str)
351
+ if unknown:
352
+ return f"unknown key(s) {', '.join(map(str, unknown))} (known: {', '.join(MATCH_KEYS)})"
353
+ for key in MATCH_KEYS:
354
+ if key in match and not (isinstance(match[key], str) and match[key].strip()):
355
+ return f"{key} must be a non-empty string"
356
+ has_endpoint = "method" in match and "path" in match
357
+ if "operation_id" not in match and not has_endpoint:
358
+ return "needs operation_id, or method and path"
359
+ if ("method" in match) != ("path" in match):
360
+ return "method and path go together"
361
+ if "method" in match and match["method"].upper() not in HTTP_METHODS:
362
+ return f"unknown HTTP method {match['method']!r} (allowed: {', '.join(HTTP_METHODS)})"
363
+ if "path" in match:
364
+ problem = path_template_problem(match["path"])
365
+ if problem:
366
+ return f"path {problem}"
367
+ return None
368
+
369
+
370
+ def call_matches(match: Mapping[str, Any], call: Mapping[str, Any] | Approval) -> bool:
371
+ """Whether a valid ``match`` names the call: every key it gives must agree."""
372
+ if isinstance(call, Approval):
373
+ call = {
374
+ "api": call.api,
375
+ "operation_id": call.operation_id,
376
+ "method": call.method,
377
+ "path": call.path,
378
+ }
379
+ if "api" in match and call.get("api") != match["api"]:
380
+ return False
381
+ if "operation_id" in match and call.get("operation_id") != match["operation_id"]:
382
+ return False
383
+ if "method" in match and str(call.get("method") or "").upper() != match["method"].upper():
384
+ return False
385
+ if "path" in match:
386
+ path = call.get("path")
387
+ if not isinstance(path, str) or not path_matches(match["path"], path):
388
+ return False
389
+ return True
390
+
391
+
392
+ def describe_match(match: Mapping[str, Any]) -> str:
393
+ """``cancelOrder`` / ``POST /orders/{id}/cancel`` / ``orders: cancelOrder POST /...``."""
394
+ parts = []
395
+ if match.get("operation_id"):
396
+ parts.append(str(match["operation_id"]))
397
+ if match.get("method"):
398
+ parts.append(f"{str(match['method']).upper()} {match.get('path')}")
399
+ text = " ".join(parts)
400
+ return f"{match['api']}: {text}" if match.get("api") else text
@@ -0,0 +1,186 @@
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
+ """Which build of graph-agents-cli is running.
16
+
17
+ A release and the builds made between two releases share one version string,
18
+ so the version alone cannot tell them apart. Every wheel therefore records the
19
+ source it was built from in ``graph_agents_cli/_build_info.json``, written by
20
+ ``hatch_build.py`` at the repository root: the git commit, whether the files
21
+ that go into the wheel had uncommitted changes, and whether the commit carries
22
+ the release tag ``v<version>``. A source checkout (an editable install, ``uv
23
+ run`` in the repository) reads the same facts from git when asked.
24
+
25
+ The build id is the version for a release build, ``<version>+g<commit7>`` for
26
+ any other commit and ``<version>+g<commit7>.dirty`` when the tree had
27
+ uncommitted changes. A build whose source is unknown (built from a tree without
28
+ git and without the file) is identified by its version only and has no commit.
29
+
30
+ The build hook loads this module from the source tree by path, before the
31
+ package's dependencies exist, so it uses the standard library only.
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ import functools
37
+ import json
38
+ import os
39
+ import re
40
+ import subprocess
41
+ from dataclasses import dataclass
42
+ from pathlib import Path
43
+
44
+ PACKAGE_NAME = "graph-agents-cli"
45
+ BUILD_INFO_FILENAME = "_build_info.json"
46
+ BUILD_INFO_FORMAT = 1
47
+ # The paths whose content goes into the wheel (relative to the repository root):
48
+ # an uncommitted change under one of them makes the build "dirty".
49
+ SOURCE_PATHS = ("src", "pyproject.toml", "hatch_build.py")
50
+ _COMMIT_RE = re.compile(r"[0-9a-f]{40}([0-9a-f]{24})?") # SHA-1 or SHA-256 object names
51
+ _GIT_TIMEOUT_S = 15
52
+ # Variables that would point git at another repository than the one asked about.
53
+ _GIT_REDIRECTS = ("GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE", "GIT_OBJECT_DIRECTORY")
54
+
55
+
56
+ @dataclass(frozen=True)
57
+ class BuildInfo:
58
+ """The version plus the source facts of one build."""
59
+
60
+ version: str
61
+ commit: str | None = None
62
+ dirty: bool = False
63
+ release: bool = False
64
+
65
+ @property
66
+ def id(self) -> str:
67
+ """``0.2.0`` (release or unknown source), ``0.2.0+g1a2b3c4`` or ``0.2.0+g1a2b3c4.dirty``."""
68
+ if self.commit is None or (self.release and not self.dirty):
69
+ return self.version
70
+ return f"{self.version}+g{self.commit[:7]}{'.dirty' if self.dirty else ''}"
71
+
72
+ @property
73
+ def is_release(self) -> bool:
74
+ """Built from the commit the release tag ``v<version>`` names, with no local change."""
75
+ return self.commit is not None and self.release and not self.dirty
76
+
77
+ def describe(self) -> str:
78
+ """One line for ``info``: the id and where it comes from."""
79
+ if self.commit is None:
80
+ return f"{self.id} (source unknown: not built from a git checkout)"
81
+ if self.is_release:
82
+ return f"{self.id} (release, commit {self.commit[:12]})"
83
+ if self.dirty:
84
+ return f"{self.id} (commit {self.commit[:12]} with uncommitted changes)"
85
+ return f"{self.id} (commit {self.commit[:12]}, not a release)"
86
+
87
+
88
+ def _git(root: Path, *args: str) -> str | None:
89
+ """``git -C root <args>`` stdout (stripped), or None when git fails or is missing."""
90
+ env = {k: v for k, v in os.environ.items() if k not in _GIT_REDIRECTS}
91
+ env["GIT_OPTIONAL_LOCKS"] = "0" # a status must not rewrite the index
92
+ try:
93
+ proc = subprocess.run(
94
+ ["git", "-C", str(root), *args],
95
+ capture_output=True,
96
+ text=True,
97
+ timeout=_GIT_TIMEOUT_S,
98
+ env=env,
99
+ check=False,
100
+ )
101
+ except (OSError, subprocess.SubprocessError):
102
+ return None
103
+ if proc.returncode != 0:
104
+ return None
105
+ return proc.stdout.strip()
106
+
107
+
108
+ def git_facts(root: str | os.PathLike[str], version: str) -> dict[str, object] | None:
109
+ """The build facts of the git checkout whose top level is ``root``; None when it is not one.
110
+
111
+ ``dirty`` is true when a file under :data:`SOURCE_PATHS` differs from the
112
+ commit (untracked files included, ignored ones not); when git cannot say,
113
+ the build counts as dirty. ``release`` is true only for a clean tree whose
114
+ commit carries the tag ``v<version>``.
115
+ """
116
+ root_path = Path(root).resolve()
117
+ top = _git(root_path, "rev-parse", "--show-toplevel")
118
+ if not top or Path(top).resolve() != root_path:
119
+ return None # not a checkout, or a directory inside another repository
120
+ commit = _git(root_path, "rev-parse", "--verify", "HEAD^{commit}")
121
+ if not commit or _COMMIT_RE.fullmatch(commit) is None:
122
+ return None
123
+ status = _git(root_path, "status", "--porcelain", "--untracked-files=all", "--", *SOURCE_PATHS)
124
+ dirty = status is None or bool(status)
125
+ tags = (_git(root_path, "tag", "--points-at", "HEAD") or "").split()
126
+ return {
127
+ "format": BUILD_INFO_FORMAT,
128
+ "version": version,
129
+ "commit": commit,
130
+ "dirty": dirty,
131
+ "release": not dirty and f"v{version}" in tags,
132
+ }
133
+
134
+
135
+ def _from_facts(version: str, facts: object) -> BuildInfo | None:
136
+ if not isinstance(facts, dict):
137
+ return None
138
+ commit = facts.get("commit")
139
+ if not isinstance(commit, str) or _COMMIT_RE.fullmatch(commit) is None:
140
+ return None
141
+ dirty = facts.get("dirty") is not False # anything but an explicit false counts as dirty
142
+ return BuildInfo(
143
+ version=version,
144
+ commit=commit,
145
+ dirty=dirty,
146
+ release=facts.get("release") is True and not dirty,
147
+ )
148
+
149
+
150
+ def _installed_version() -> str:
151
+ from importlib.metadata import PackageNotFoundError
152
+ from importlib.metadata import version as dist_version
153
+
154
+ try:
155
+ return dist_version(PACKAGE_NAME)
156
+ except PackageNotFoundError:
157
+ return "0.0.0" # the unknown-version sentinel of scaffold.utils.version
158
+
159
+
160
+ def build_of_package(package_dir: Path, version: str) -> BuildInfo:
161
+ """The build of the package at ``package_dir``.
162
+
163
+ The recorded ``_build_info.json`` wins (an installed wheel); a package that
164
+ lives at ``<checkout>/src/graph_agents_cli`` reads the checkout's git facts;
165
+ anything else is a build of unknown source.
166
+ """
167
+ info_file = package_dir / BUILD_INFO_FILENAME
168
+ if info_file.is_file():
169
+ try:
170
+ recorded = _from_facts(version, json.loads(info_file.read_text(encoding="utf-8")))
171
+ except (OSError, ValueError):
172
+ recorded = None
173
+ if recorded is not None:
174
+ return recorded
175
+ root = package_dir.parent.parent
176
+ if package_dir.parent.name == "src" and (root / "pyproject.toml").is_file():
177
+ from_git = _from_facts(version, git_facts(root, version))
178
+ if from_git is not None:
179
+ return from_git
180
+ return BuildInfo(version=version)
181
+
182
+
183
+ @functools.cache
184
+ def current_build() -> BuildInfo:
185
+ """The running build (computed once per process)."""
186
+ return build_of_package(Path(__file__).resolve().parent, _installed_version())
@@ -0,0 +1,7 @@
1
+ {
2
+ "commit": "01c0236b8b6fada8d22673e4608c9cb49b6bcf2e",
3
+ "dirty": false,
4
+ "format": 1,
5
+ "release": true,
6
+ "version": "0.3.1"
7
+ }