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,453 @@
1
+ # Copyright 2026 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # 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
+ """Resolve extension references to commit SHAs and vendor extension directories."""
16
+
17
+ from __future__ import annotations
18
+
19
+ import hashlib
20
+ import logging
21
+ import os
22
+ import re
23
+ import shutil
24
+ import subprocess
25
+ from collections.abc import Callable
26
+ from pathlib import Path
27
+ from typing import Any
28
+
29
+ import yaml
30
+ from filelock import FileLock, Timeout
31
+
32
+ from graph_agents_cli._runner import run_resolved
33
+ from graph_agents_cli.extension._paths import git_cache_dir_name, git_cache_root
34
+ from graph_agents_cli.extension._refs import ExtensionRef
35
+ from graph_agents_cli.extension._spec import EXTENSION_FILE
36
+ from graph_agents_cli.scaffold.utils.fs import source_ignore_patterns
37
+
38
+ # Only a full 40-char commit SHA is unambiguous. A short/partial hex string
39
+ # could instead be a branch or tag named like hex (e.g. "cafe123"), so anything
40
+ # shorter than 40 goes through ls-remote ref resolution rather than being
41
+ # treated as an already-resolved commit.
42
+ _SHA_RE = re.compile(r"^[0-9a-f]{40}$")
43
+ # An extension name becomes a directory component under the vendoring root, so it
44
+ # must not contain path separators or traversal — reject anything else.
45
+ _SAFE_NAME_RE = re.compile(r"^[A-Za-z0-9._-]+$")
46
+
47
+
48
+ def validate_extension_name(name: str) -> str:
49
+ """Return ``name`` if safe as a single path component, else raise.
50
+
51
+ Guards the vendoring path (``dest_root / name``, which ``_replace_tree``
52
+ ``rmtree``s and recreates) against a malicious ``name:`` (e.g. ``../../x``)
53
+ read from an upstream extension file or a committed manifest.
54
+ """
55
+ if name in (".", "..") or not _SAFE_NAME_RE.match(name):
56
+ raise ResolverError(
57
+ f"unsafe extension name {name!r}: use only letters, digits, '.', '_', '-'."
58
+ )
59
+ return name
60
+
61
+
62
+ # Seconds to wait for the shared git-cache lock before giving up.
63
+ _CACHE_LOCK_TIMEOUT_SECONDS = 120
64
+
65
+ # Records what a vendored copy was materialized from, so sync can detect a
66
+ # stale or partial copy (dir exists but doesn't match its source) instead of
67
+ # trusting mere directory existence.
68
+ STAMP_FILE = ".graph-agents-cli-extension-sha"
69
+
70
+ # Recorded as the pin of a `local@` extension, which has no commit to point at.
71
+ LOCAL_SHA = "local"
72
+
73
+
74
+ def _git_ignored(root: Path) -> frozenset[str]:
75
+ """POSIX paths, relative to ``root``, that git considers ignored there.
76
+
77
+ Asked of git rather than parsed out of ``.gitignore``, which buys four
78
+ things no pattern match of our own would get right: nested ignore files and
79
+ their negations, ``.git/info/exclude`` and the user's global
80
+ ``core.excludesFile``, correct results when ``root`` is a subdirectory of
81
+ the repository whose rules apply (a ``local@…#selector`` ref), and — the one
82
+ that is a correctness bug rather than a gap — force-added files. Those are
83
+ tracked, so they are part of the extension however the patterns read, and
84
+ ``--others`` never lists them.
85
+
86
+ Empty when ``root`` is not in a work tree, which is a perfectly ordinary way
87
+ to hold an extension source — and empty, rather than an error, when there is
88
+ no git at all: vendoring a local source has never needed one, and gaining a
89
+ hard dependency on it here would be a regression, not a feature.
90
+ """
91
+ try:
92
+ result = _git(
93
+ "-C",
94
+ str(root),
95
+ "ls-files",
96
+ "--others",
97
+ "--ignored",
98
+ "--exclude-standard",
99
+ # Collapse a wholly-ignored directory to its own name: the point is
100
+ # to skip `.venv/` without first enumerating what is inside it.
101
+ "--directory",
102
+ "-z",
103
+ )
104
+ except OSError as e:
105
+ logging.debug("Could not ask git what %s ignores: %s", root, e)
106
+ return frozenset()
107
+ if result.returncode != 0:
108
+ logging.debug("No git ignore rules apply to %s: %s", root, result.stderr.strip())
109
+ return frozenset()
110
+ return frozenset(path.rstrip("/") for path in result.stdout.split("\0") if path)
111
+
112
+
113
+ class _SourceExclusions:
114
+ """What to leave behind when reading an extension source.
115
+
116
+ One instance answers for both the fingerprint and the copy, deliberately. If
117
+ the hash counted something the copy leaves out, every change to it would
118
+ stamp a mismatch and provoke a re-copy that produces a byte-identical tree —
119
+ reporting a repair that did not happen.
120
+ """
121
+
122
+ def __init__(self, root: Path, *, use_gitignore: bool):
123
+ self._root = str(root)
124
+ # Only for a working directory. A git checkout holds tracked files and
125
+ # nothing else, so there is nothing ignored in it to find — and asking
126
+ # anyway would drop a force-added file that the commit genuinely
127
+ # contains.
128
+ self._ignored = _git_ignored(root) if use_gitignore else frozenset()
129
+
130
+ def __call__(self, directory: str, names: list[str]) -> set[str]:
131
+ excluded = source_ignore_patterns(directory, names)
132
+ if not self._ignored:
133
+ return excluded
134
+ rel = os.path.relpath(directory, self._root)
135
+ prefix = "" if rel == os.curdir else f"{Path(rel).as_posix()}/"
136
+ return excluded | {n for n in names if f"{prefix}{n}" in self._ignored}
137
+
138
+
139
+ class ResolverError(Exception):
140
+ """Raised when an extension cannot be resolved or vendored."""
141
+
142
+
143
+ def read_stamp(extension_dir: Path) -> str | None:
144
+ """Return what an extension dir was materialized from, or None if unstamped."""
145
+ stamp = extension_dir / STAMP_FILE
146
+ if not stamp.exists():
147
+ return None
148
+ return stamp.read_text(encoding="utf-8").strip() or None
149
+
150
+
151
+ def expected_stamp(ref: ExtensionRef, sha: str) -> str:
152
+ """Return the stamp a freshly materialized copy of ``ref`` would carry.
153
+
154
+ For a git ref that is the pinned SHA. A ``local@`` ref has no commit to pin —
155
+ its recorded pin is the constant ``LOCAL_SHA`` — so its copy is stamped with a
156
+ hash of the source tree instead. Comparing that hash is what lets sync notice
157
+ edits to a local source; comparing the pin would only match the sentinel
158
+ against itself and never refresh.
159
+ """
160
+ if ref.kind != "local":
161
+ return sha
162
+ src = _local_source_dir(ref)
163
+ return _fingerprint(src, _SourceExclusions(src, use_gitignore=True))
164
+
165
+
166
+ def _fingerprint(root: Path, exclude: _SourceExclusions) -> str:
167
+ """Return a hash of the name and contents of every file under ``root``.
168
+
169
+ Walked in sorted order and keyed on POSIX-style relative paths, so a given
170
+ tree hashes identically across runs and platforms.
171
+
172
+ Hashes exactly what the copy would contain: symlinks are skipped (see
173
+ ``_skip_symlinks``) and so is everything ``exclude`` drops, files included —
174
+ ``.env`` is a file, and hashing one the copy omits would make every edit to
175
+ it force a re-copy that changes nothing.
176
+
177
+ Scanned with ``os.scandir``, whose entries answer "directory or symlink?"
178
+ from the directory read itself: one syscall per directory rather than a
179
+ ``stat`` per name. An unreadable directory raises rather than being skipped,
180
+ since a tree walked only in part hashes the same after a change inside the
181
+ part that was missed.
182
+ """
183
+ digest = hashlib.sha256()
184
+ # (directory, its POSIX path relative to root). A stack, so a deep source
185
+ # tree cannot exhaust the interpreter's recursion limit.
186
+ stack: list[tuple[str, str]] = [(str(root), "")]
187
+ try:
188
+ while stack:
189
+ directory, rel_dir = stack.pop()
190
+ with os.scandir(directory) as scan:
191
+ entries = [entry for entry in scan if not entry.is_symlink()]
192
+ excluded = exclude(directory, [entry.name for entry in entries])
193
+ subdirs: list[os.DirEntry[str]] = []
194
+ files: list[os.DirEntry[str]] = []
195
+ for entry in entries:
196
+ if entry.name in excluded:
197
+ continue
198
+ if entry.is_dir(follow_symlinks=False):
199
+ subdirs.append(entry)
200
+ else:
201
+ files.append(entry)
202
+ prefix = f"{rel_dir}/" if rel_dir else ""
203
+ for entry in sorted(files, key=lambda e: e.name):
204
+ with open(entry.path, "rb") as f:
205
+ contents = hashlib.file_digest(f, "sha256").hexdigest()
206
+ digest.update(f"{prefix}{entry.name}\0{contents}\0".encode())
207
+ # Reversed, so popping visits the subdirectories in sorted order.
208
+ for entry in sorted(subdirs, key=lambda e: e.name, reverse=True):
209
+ stack.append((entry.path, f"{prefix}{entry.name}"))
210
+ except OSError as e:
211
+ raise ResolverError(f"could not read local extension source {root}: {e}") from e
212
+ return digest.hexdigest()
213
+
214
+
215
+ def _local_source_dir(ref: ExtensionRef) -> Path:
216
+ """Return the directory a ``local@`` ref selects, or raise if it is unusable."""
217
+ src = ref.local_path
218
+ if src is None or not src.exists():
219
+ raise ResolverError(f"local extension path not found: {ref.local_path}.")
220
+ sub = src / ref.selector if ref.selector else src
221
+ if not (sub / EXTENSION_FILE).exists():
222
+ raise ResolverError(f"{sub} has no {EXTENSION_FILE}.")
223
+ return sub
224
+
225
+
226
+ def _https_url(repo: str) -> str:
227
+ return f"https://github.com/{repo}.git"
228
+
229
+
230
+ def _clone_url(ref: ExtensionRef) -> str:
231
+ """Return the URL git should clone or query for a non-local ref."""
232
+ if ref.url is not None:
233
+ return ref.url
234
+ if ref.repo is None:
235
+ raise ResolverError(f"cannot resolve ref {ref.raw!r}: repo is not set.")
236
+ return _https_url(ref.repo)
237
+
238
+
239
+ def _git(*args: str) -> subprocess.CompletedProcess:
240
+ """Run a git command with output captured (no shell, no PATH resolution)."""
241
+ return run_resolved(
242
+ ["git", *args],
243
+ resolve_executable=False,
244
+ capture_output=True,
245
+ text=True,
246
+ )
247
+
248
+
249
+ def resolve_sha(ref: ExtensionRef) -> str:
250
+ """Resolve an ExtensionRef to an exact commit SHA.
251
+
252
+ For local refs returns the sentinel ``LOCAL_SHA`` (unpinned).
253
+ For every other ref runs ``git ls-remote`` unless the caller
254
+ already supplied a full hex SHA.
255
+ """
256
+ if ref.kind == "local":
257
+ return LOCAL_SHA
258
+ if ref.ref and _SHA_RE.match(ref.ref):
259
+ return ref.ref
260
+ target = ref.ref or "HEAD"
261
+ result = _git("ls-remote", _clone_url(ref), target)
262
+ if result.returncode != 0 or not result.stdout.strip():
263
+ raise ResolverError(
264
+ f"could not resolve {target!r} in {ref.repo!r}; check the repo and ref."
265
+ )
266
+ return result.stdout.split()[0]
267
+
268
+
269
+ def _extension_name_from_dir(extension_dir: Path, fallback: str) -> str:
270
+ spec_path = extension_dir / EXTENSION_FILE
271
+ if spec_path.exists():
272
+ try:
273
+ with open(spec_path, encoding="utf-8") as f:
274
+ data: Any = yaml.safe_load(f) or {}
275
+ if isinstance(data, dict) and isinstance(data.get("name"), str):
276
+ return str(data["name"])
277
+ except (OSError, RecursionError, UnicodeDecodeError, yaml.YAMLError):
278
+ # Best-effort name detection only: fall back to the caller's name if
279
+ # the file is unreadable/malformed. The loader reports it properly —
280
+ # warned about and skipped on load, fatal for `add`/`update`.
281
+ pass
282
+ return fallback
283
+
284
+
285
+ def _skip_symlinks(directory: str, names: list[str]) -> set[str]:
286
+ """Names in ``directory`` that are symlinks, for ``copytree(ignore=...)``."""
287
+ skipped = {n for n in names if Path(directory, n).is_symlink()}
288
+ for name in sorted(skipped):
289
+ logging.warning("Skipping symlink in extension source: %s", Path(directory, name))
290
+ return skipped
291
+
292
+
293
+ def _ignored_names(
294
+ exclude: _SourceExclusions,
295
+ ) -> Callable[[str, list[str]], set[str]]:
296
+ """Return the ``copytree(ignore=...)`` callback for an extension source."""
297
+
298
+ def ignore(directory: str, names: list[str]) -> set[str]:
299
+ return _skip_symlinks(directory, names) | exclude(directory, names)
300
+
301
+ return ignore
302
+
303
+
304
+ def _replace_tree(src: Path, dest: Path, exclude: _SourceExclusions) -> None:
305
+ """Replace ``dest`` with a copy of ``src``, leaving it alone if the copy fails."""
306
+ # Copy alongside and swap, so an unreadable source cannot delete the working
307
+ # copy on its way to failing. shutil.Error is an OSError.
308
+ staged = dest.parent / f".{dest.name}.incoming"
309
+ try:
310
+ shutil.rmtree(staged, ignore_errors=True)
311
+ # Never follow symlinks out of an extension: copytree would otherwise
312
+ # dereference `notes.md -> ~/.ssh/id_rsa` into a real file in the
313
+ # project, where it gets committed (CWE-59).
314
+ shutil.copytree(src, staged, ignore=_ignored_names(exclude))
315
+ if dest.exists():
316
+ shutil.rmtree(dest)
317
+ staged.rename(dest)
318
+ except OSError as e:
319
+ shutil.rmtree(staged, ignore_errors=True)
320
+ raise ResolverError(f"could not copy {src} into {dest}: {e}") from e
321
+
322
+
323
+ def _write_stamp(extension_dir: Path, sha: str) -> None:
324
+ """Record the SHA the copy was materialized from (written last, post-copy)."""
325
+ (extension_dir / STAMP_FILE).write_text(sha, encoding="utf-8")
326
+
327
+
328
+ def materialize(
329
+ ref: ExtensionRef,
330
+ sha: str,
331
+ dest_root: Path,
332
+ *,
333
+ name: str | None = None,
334
+ ) -> tuple[str, Path]:
335
+ """Vendor an extension directory into ``dest_root/<extension_name>``.
336
+
337
+ Returns ``(extension_name, extension_dir)``.
338
+ For ``local@`` refs, copies from the local path.
339
+ For git refs, clones/reuses the cache and checks out ``sha``.
340
+
341
+ When ``name`` is provided the vendored directory is placed at
342
+ ``dest_root/<name>`` (the caller's stable id) and that name is returned,
343
+ regardless of what the upstream ``graph-agents-cli-extension.yaml`` declares. This
344
+ prevents an extension rename upstream from silently relocating (and losing) the
345
+ vendored copy.
346
+ """
347
+ dest_root.mkdir(parents=True, exist_ok=True)
348
+
349
+ if ref.kind == "local":
350
+ sub = _local_source_dir(ref)
351
+ spec_name = _extension_name_from_dir(sub, ref.selector or sub.name)
352
+ resolved_name = validate_extension_name(name if name is not None else spec_name)
353
+ extension_dir = dest_root / resolved_name
354
+ exclude = _SourceExclusions(sub, use_gitignore=True)
355
+ # Fingerprinted before the copy, so a source edited while it is being
356
+ # copied leaves the older stamp and the next sync redoes the copy,
357
+ # rather than blessing a tree that is half of each.
358
+ stamp = _fingerprint(sub, exclude)
359
+ _replace_tree(sub, extension_dir, exclude)
360
+ _write_stamp(extension_dir, stamp)
361
+ return resolved_name, extension_dir
362
+
363
+ checkout = _checkout(ref, sha)
364
+ if ref.kind == "first_party":
365
+ # First-party extensions live under extensions/<selector>/ in the repo.
366
+ sub = checkout / "extensions" / ref.selector if ref.selector else checkout
367
+ else:
368
+ sub = checkout / ref.selector if ref.selector else checkout
369
+ if not (sub / EXTENSION_FILE).exists():
370
+ raise ResolverError(
371
+ f"{ref.repo}#{ref.selector or ''} has no {EXTENSION_FILE} at the pinned commit."
372
+ )
373
+ spec_name = _extension_name_from_dir(
374
+ sub, ref.selector or (ref.repo.split("/")[-1] if ref.repo else "extension")
375
+ )
376
+ resolved_name = validate_extension_name(name if name is not None else spec_name)
377
+ extension_dir = dest_root / resolved_name
378
+ _replace_tree(sub, extension_dir, _SourceExclusions(sub, use_gitignore=False))
379
+ _write_stamp(extension_dir, sha)
380
+ return resolved_name, extension_dir
381
+
382
+
383
+ def _checkout(ref: ExtensionRef, sha: str) -> Path:
384
+ """Clone (or reuse) the repo in the shared cache and check out ``sha``.
385
+
386
+ Returns the worktree root path.
387
+ """
388
+ cache = git_cache_root()
389
+ cache.mkdir(parents=True, exist_ok=True)
390
+ if ref.repo is None:
391
+ raise ResolverError(f"cannot checkout ref {ref.raw!r}: repo is not set.")
392
+ repo_dir = cache / git_cache_dir_name(ref.repo, ref.url)
393
+ url = _clone_url(ref)
394
+
395
+ # The cache is shared across processes and checkout mutates a single working
396
+ # tree, so serialize all git operations on this repo with a per-repo lock.
397
+ try:
398
+ lock = FileLock(
399
+ str(cache / f"{repo_dir.name}.lock"),
400
+ timeout=_CACHE_LOCK_TIMEOUT_SECONDS,
401
+ )
402
+ with lock:
403
+ return _checkout_locked(repo_dir, url, ref.repo, sha)
404
+ except Timeout as e:
405
+ raise ResolverError(
406
+ f"timed out waiting for the extension cache lock for {ref.repo}; "
407
+ "another graph-agents-cli process may be resolving extensions."
408
+ ) from e
409
+
410
+
411
+ def _clone(repo_dir: Path, url: str, repo: str) -> None:
412
+ """Clone ``url`` into ``repo_dir``, fetching as little as possible.
413
+
414
+ Only the pinned commit is ever checked out, so the default branch's file
415
+ contents are pure waste: ``--no-checkout`` skips materializing them and
416
+ ``--filter=blob:none`` skips downloading them, leaving git to fetch blobs
417
+ lazily for the commit that is actually wanted.
418
+
419
+ ``--depth=1`` is deliberately not used. The caller checks out an arbitrary
420
+ SHA, which a shallow clone can only reach if the server allows
421
+ ``uploadpack.allowReachableSHA1InWant``, and the existing ``fetch --all``
422
+ fallback does not unshallow.
423
+ """
424
+ base = ["clone", "--quiet", "--no-checkout"]
425
+ res = _git(*base, "--filter=blob:none", url, str(repo_dir))
426
+ if res.returncode == 0:
427
+ return
428
+
429
+ # A server that does not set uploadpack.allowFilter needs no handling here:
430
+ # measured against git 2.51, the client warns ("filtering not recognized by
431
+ # server, ignoring") and completes a full clone, exit 0. What does fail is a
432
+ # client older than the flag (git < 2.19), which rejects it as an unknown
433
+ # option — so retry without it rather than breaking `extension add` outright.
434
+ logging.debug("Blobless clone of %s failed, retrying in full: %s", repo, res.stderr)
435
+ shutil.rmtree(repo_dir, ignore_errors=True)
436
+ res = _git(*base, url, str(repo_dir))
437
+ if res.returncode != 0:
438
+ raise ResolverError(f"git clone failed for {repo}: {res.stderr.strip()}")
439
+
440
+
441
+ def _checkout_locked(repo_dir: Path, url: str, repo: str, sha: str) -> Path:
442
+ if not (repo_dir / ".git").exists():
443
+ _clone(repo_dir, url, repo)
444
+
445
+ fetch = _git("-C", str(repo_dir), "fetch", "--quiet", "origin", sha)
446
+ # Some hosts reject fetching an arbitrary SHA; fall back to a full fetch.
447
+ if fetch.returncode != 0:
448
+ _git("-C", str(repo_dir), "fetch", "--quiet", "--all")
449
+
450
+ checkout = _git("-C", str(repo_dir), "checkout", "--quiet", sha)
451
+ if checkout.returncode != 0:
452
+ raise ResolverError(f"git checkout {sha} failed: {checkout.stderr.strip()}")
453
+ return repo_dir
@@ -0,0 +1,106 @@
1
+ # Copyright 2026 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # 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
+ """JSON Schema for graph-agents-cli-extension.yaml, generated from the wire models.
16
+
17
+ ``uv run python -m graph_agents_cli.extension._schema`` rewrites the checked-in
18
+ copy (``schemas/`` at the repository root); a unit test fails if the two disagree.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import json
24
+ from pathlib import Path
25
+ from typing import Any
26
+
27
+ from pydantic import BaseModel
28
+
29
+ from graph_agents_cli.extension._spec import V1ALPHA1, ExtensionManifest
30
+
31
+
32
+ def _models(model: type[BaseModel], seen: set[type[BaseModel]] | None = None) -> set:
33
+ """Every model reachable from `model`, so nothing has to be registered by hand."""
34
+ seen = seen if seen is not None else set()
35
+ if model in seen:
36
+ return seen
37
+ seen.add(model)
38
+ for field in model.model_fields.values():
39
+ stack = [field.annotation]
40
+ while stack:
41
+ current = stack.pop()
42
+ if isinstance(current, type) and issubclass(current, BaseModel):
43
+ _models(current, seen)
44
+ stack.extend(getattr(current, "__args__", ()))
45
+ return seen
46
+
47
+
48
+ SCHEMA_FILE = "graph-agents-cli-extension-v1alpha1.schema.json"
49
+ # Repo-only: generated for editors and the published contract, not read at runtime.
50
+ # <repo>/src/graph_agents_cli/extension/_schema.py -> <repo>/schemas/<file>
51
+ SCHEMA_PATH = Path(__file__).resolve().parents[3] / "schemas" / SCHEMA_FILE
52
+ SCHEMA_ID = f"https://raw.githubusercontent.com/ss7172/graph-agents-cli/main/schemas/{SCHEMA_FILE}"
53
+
54
+
55
+ def _allow_empty_keys(schema: dict[str, Any]) -> None:
56
+ """Let any optional property be null, matching how the loader reads YAML.
57
+
58
+ A key written with nothing under it parses as null and the loader treats it
59
+ as absent, so the document has to accept it or an editor flags something
60
+ that installs fine. Driven off the models because pydantic emits no
61
+ `default` for a `default_factory` field.
62
+ """
63
+ defs = schema.get("$defs", {})
64
+ for model in _models(ExtensionManifest):
65
+ block = schema if model is ExtensionManifest else defs.get(model.__name__)
66
+ if not block:
67
+ continue
68
+ for name, field in model.model_fields.items():
69
+ if field.is_required():
70
+ continue
71
+ prop = block.get("properties", {}).get(field.alias or name)
72
+ if prop is None or any(v.get("type") == "null" for v in prop.get("anyOf", [])):
73
+ continue
74
+ keep = {k: prop.pop(k) for k in list(prop) if k in ("default", "title")}
75
+ inner = prop.pop("anyOf", None) or [dict(prop)]
76
+ prop.clear()
77
+ prop.update(keep)
78
+ prop["anyOf"] = [*inner, {"type": "null"}]
79
+
80
+
81
+ def build_json_schema() -> dict[str, Any]:
82
+ """The manifest schema as a JSON Schema 2020-12 document."""
83
+ schema = ExtensionManifest.model_json_schema(by_alias=True)
84
+ _allow_empty_keys(schema)
85
+ # Our header wins over pydantic's class-derived title/description.
86
+ return {
87
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
88
+ "$id": SCHEMA_ID,
89
+ **schema,
90
+ "title": f"graph-agents-cli extension manifest ({V1ALPHA1})",
91
+ "description": (
92
+ "Manifest for an graph-agents-cli extension. Generated from the CLI's own "
93
+ "models - edit those, not this file. Experimental: this format may "
94
+ "still change in a breaking way."
95
+ ),
96
+ }
97
+
98
+
99
+ def render() -> str:
100
+ """The document as it is written to disk (trailing newline included)."""
101
+ return json.dumps(build_json_schema(), indent=2, sort_keys=False) + "\n"
102
+
103
+
104
+ if __name__ == "__main__":
105
+ SCHEMA_PATH.write_text(render(), encoding="utf-8")
106
+ print(f"wrote {SCHEMA_PATH}")