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,664 @@
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
+ """A system file resolved against its projects: agents, edges, environments and URLs.
16
+
17
+ ``resolve`` reads every project the file names (its manifest, ``api-policy.yaml``,
18
+ the generated peers module and the chart's values) and turns each ``calls`` entry
19
+ into a ``Link`` with what ``peer add`` would be given for it. What makes the file
20
+ unusable is a ``SystemFileError`` (exit 3): a project that does not exist or that
21
+ two agents name, an edge to an unknown agent or to the agent itself, the same agent called twice by
22
+ one agent, two agents with one client id, an ``auth: exchange`` edge without
23
+ ``identity``, an environment a project's manifest does not know. Everything else
24
+ is a finding of ``system check``.
25
+
26
+ The URL an agent is called at in an environment:
27
+
28
+ - ``port_base`` (local processes): ``http://127.0.0.1:<port_base + i>``, i its
29
+ position in ``agents``;
30
+ - ``url``: the template with ``{agent}``, ``{project}`` and ``{env}`` filled;
31
+ - otherwise in the cluster: ``http://<chart fullname>.<namespace>.svc.cluster.local``
32
+ (``:<service.port>`` unless 80), the namespace from the called project's
33
+ manifest (``environments.<env>.namespace``, default ``<project>-<env>``) and the
34
+ fullname the release name, which ``deploy`` sets to the project's name.
35
+ """
36
+
37
+ from __future__ import annotations
38
+
39
+ import copy
40
+ from dataclasses import dataclass, field
41
+ from functools import cached_property
42
+ from pathlib import Path
43
+ from typing import Any
44
+
45
+ import click
46
+
47
+ from graph_agents_cli._api_policy import (
48
+ DECIDE_DIRECT,
49
+ DECIDE_RELAYED,
50
+ REQUESTER_APPROVER,
51
+ approval_rules,
52
+ )
53
+ from graph_agents_cli._defaults import normalize_auth_policy
54
+ from graph_agents_cli._project import MANIFEST_FILENAME, ProjectConfig
55
+ from graph_agents_cli.api._files import chart_values_files, read_text
56
+ from graph_agents_cli.api.cmd_api import _Project, load_project_at
57
+ from graph_agents_cli.deploy._values import deep_merge, load_yaml
58
+ from graph_agents_cli.peer import _generate as gen
59
+ from graph_agents_cli.peer.cmd_peer import AUTH_BY_POLICY, PeerOptions, runtime_problem
60
+ from graph_agents_cli.system._model import (
61
+ DENY,
62
+ RELAY,
63
+ Edge,
64
+ SystemFile,
65
+ SystemFileError,
66
+ load,
67
+ )
68
+
69
+ LOCAL = "local"
70
+ URL = "url"
71
+ CLUSTER = "cluster"
72
+ DEFAULT_SERVICE_PORT = 80
73
+ DEFAULT_TARGET_PORT = 8000
74
+ # `AUTH_MAX_DELEGATION_DEPTH` when a project does not set it (the template's default).
75
+ DEFAULT_MAX_DELEGATION_DEPTH = 3
76
+ # `DB_POOL_MAX_SIZE` when unset (the template's default), and LangGraph Server's own pool
77
+ # (`LANGGRAPH_POSTGRES_POOL_MAX_SIZE`, 150 in langgraph-api 0.14), which a langgraph-server
78
+ # replica opens beside the app's.
79
+ DEFAULT_DB_POOL_MAX_SIZE = 10
80
+ DEFAULT_LANGGRAPH_POOL_MAX_SIZE = 150
81
+ CLIENT_ACTOR_PREFIX = "client:"
82
+
83
+
84
+ @dataclass(frozen=True)
85
+ class Env:
86
+ """An environment of the file: where the agents run, so how they are called."""
87
+
88
+ name: str
89
+ kind: str # LOCAL, URL or CLUSTER
90
+ port_base: int | None = None
91
+ url: str | None = None
92
+
93
+ @property
94
+ def charted(self) -> bool:
95
+ """Whether the agents run from their charts here (values-<env>.yaml)."""
96
+ return self.kind != LOCAL
97
+
98
+
99
+ @dataclass
100
+ class Node:
101
+ """One agent of the system and what its project says."""
102
+
103
+ name: str
104
+ index: int
105
+ root: Path
106
+ project: _Project
107
+ client_id: str
108
+ # The actor id the agents it calls see (act.sub): the file's actor_id, else client_id.
109
+ actor_id: str = ""
110
+ _values: dict[str | None, dict[str, Any]] = field(default_factory=dict, repr=False)
111
+
112
+ @property
113
+ def config(self) -> ProjectConfig:
114
+ return self.project.config
115
+
116
+ @property
117
+ def document(self) -> dict[str, Any] | None:
118
+ return self.project.document
119
+
120
+ @property
121
+ def release(self) -> str:
122
+ """The Helm release (and chart fullname): the project's name, as ``deploy`` sets it."""
123
+ return self.config.project_name[:63].rstrip("-")
124
+
125
+ @property
126
+ def chart_name(self) -> str:
127
+ """The chart's ``agent.name``: ``nameOverride``, else the chart's name (the project's)."""
128
+ override = self.values(None).get("nameOverride")
129
+ name = str(override) if override else self.config.project_name
130
+ return name[:63].rstrip("-")
131
+
132
+ @property
133
+ def pod_labels(self) -> dict[str, str]:
134
+ """The agent pods' selector labels (the chart's ``agent.selectorLabels``): the name and
135
+ the release. The bundled database's pods share the release label, not the name."""
136
+ return {
137
+ "app.kubernetes.io/name": self.chart_name,
138
+ "app.kubernetes.io/instance": self.release,
139
+ }
140
+
141
+ @property
142
+ def runtime_problem(self) -> str | None:
143
+ return runtime_problem(self.root, self.config.agent_directory)
144
+
145
+ @cached_property
146
+ def chart_files(self) -> list[Path]:
147
+ return chart_values_files(self.root, self.config)
148
+
149
+ @property
150
+ def has_chart(self) -> bool:
151
+ return bool(self.chart_files)
152
+
153
+ def values_file(self, env: str) -> Path | None:
154
+ """``values-<env>.yaml`` when the chart has it."""
155
+ for path in self.chart_files[1:]:
156
+ if path.stem == f"values-{env}":
157
+ return path
158
+ return None
159
+
160
+ def values(self, env: str | None = None) -> dict[str, Any]:
161
+ """The chart's values merged as ``deploy --env`` merges them (``values.yaml`` alone
162
+ for None); empty without a chart. Unreadable values read as empty (``deploy``
163
+ reports them)."""
164
+ if env not in self._values:
165
+ merged: dict[str, Any] = {}
166
+ if self.chart_files:
167
+ try:
168
+ merged = load_yaml(self.chart_files[0])
169
+ extra = self.values_file(env) if env else None
170
+ if extra is not None:
171
+ merged = deep_merge(merged, load_yaml(extra))
172
+ except click.ClickException:
173
+ merged = {}
174
+ self._values[env] = merged
175
+ return copy.deepcopy(self._values[env])
176
+
177
+ def chart_env(self, env: str | None = None) -> dict[str, Any]:
178
+ found = self.values(env).get("env")
179
+ return dict(found) if isinstance(found, dict) else {}
180
+
181
+ def setting(self, env: str | None, key: str) -> str:
182
+ """A chart ``env`` setting as a string ('' when unset or null)."""
183
+ value = self.chart_env(env).get(key)
184
+ return "" if value is None else str(value).strip()
185
+
186
+ def auth_policy(self, env: str | None = None) -> str:
187
+ """``env.AUTH_POLICY`` of the chart values, else the manifest's policy."""
188
+ raw = self.setting(env, "AUTH_POLICY")
189
+ return normalize_auth_policy(raw or self.config.auth_policy, warn=False)
190
+
191
+ def a2a_name(self, env: str | None = None) -> str:
192
+ return self.setting(env, "A2A_NAME") or self.config.agent_directory
193
+
194
+ def mount(self, env: str | None = None) -> str:
195
+ """Where the agent serves A2A: ``/a2a/<A2A_NAME>``."""
196
+ return f"/a2a/{self.a2a_name(env)}"
197
+
198
+ def namespace(self, env: str) -> str:
199
+ return self.config.environment(env)[1]
200
+
201
+ def context(self, env: str) -> str | None:
202
+ return self.config.environment(env)[0] or None
203
+
204
+ def _port(self, env: str, key: str, default: int) -> int | str:
205
+ service = self.values(env).get("service")
206
+ value = service.get(key) if isinstance(service, dict) else None
207
+ if isinstance(value, int) and not isinstance(value, bool):
208
+ return value
209
+ if isinstance(value, str) and value.strip():
210
+ return int(value) if value.strip().isdigit() else value.strip()
211
+ return default
212
+
213
+ def service_port(self, env: str) -> int | str:
214
+ return self._port(env, "port", DEFAULT_SERVICE_PORT)
215
+
216
+ def target_port(self, env: str) -> int | str:
217
+ """The pods' http port (``service.targetPort``): what a NetworkPolicy matches."""
218
+ return self._port(env, "targetPort", DEFAULT_TARGET_PORT)
219
+
220
+ def replicas(self, env: str) -> tuple[int, bool]:
221
+ """``(replicas, autoscaled)``: ``hpa.maxReplicas`` while the HPA is on."""
222
+ values = self.values(env)
223
+ hpa = values.get("hpa") if isinstance(values.get("hpa"), dict) else {}
224
+ if _true(hpa.get("enabled")):
225
+ return _int(hpa.get("maxReplicas"), 1), True
226
+ return _int(values.get("replicaCount"), 1), False
227
+
228
+ def task_store(self, env: str | None = None) -> str:
229
+ """Where the agent keeps A2A tasks: ``postgres`` (shared by replicas) or ``memory``."""
230
+ if self.config.runtime == "langgraph-server":
231
+ return "postgres"
232
+ if self.has_chart:
233
+ checkpointer = self.setting(env, "CHECKPOINTER") or "postgres"
234
+ else:
235
+ checkpointer = self.config.checkpointer
236
+ return "memory" if checkpointer.lower() == "memory" else "postgres"
237
+
238
+ def app_url(self, env: str) -> str:
239
+ """The base URL the agent's card advertises (the chart's ``agent.appUrl``)."""
240
+ values = self.values(env)
241
+ if values.get("appUrl"):
242
+ return str(values["appUrl"])
243
+ if self.setting(env, "APP_URL"):
244
+ return self.setting(env, "APP_URL")
245
+ gateway = values.get("gateway") if isinstance(values.get("gateway"), dict) else {}
246
+ if _true(gateway.get("enabled")) and gateway.get("hostname"):
247
+ return f"https://{gateway['hostname']}"
248
+ ingress = values.get("ingress") if isinstance(values.get("ingress"), dict) else {}
249
+ if _true(ingress.get("enabled")) and ingress.get("hostname"):
250
+ tls = values.get("tls") if isinstance(values.get("tls"), dict) else {}
251
+ manager = tls.get("certManager") if isinstance(tls.get("certManager"), dict) else {}
252
+ secure = bool(tls.get("existingSecret")) or _true(manager.get("enabled"))
253
+ return f"{'https' if secure else 'http'}://{ingress['hostname']}"
254
+ return ""
255
+
256
+ def route_enabled(self, env: str) -> bool:
257
+ values = self.values(env)
258
+ return any(
259
+ _true((values.get(key) or {}).get("enabled"))
260
+ for key in ("gateway", "ingress")
261
+ if isinstance(values.get(key), dict)
262
+ )
263
+
264
+ def public_paths(self, env: str) -> list[dict[str, Any]]:
265
+ """What the route publishes (``route.publicPaths``; empty with no route)."""
266
+ if not self.route_enabled(env):
267
+ return []
268
+ route = self.values(env).get("route")
269
+ paths = route.get("publicPaths") if isinstance(route, dict) else None
270
+ return [p for p in paths or [] if isinstance(p, dict)]
271
+
272
+ def max_delegation_depth(self, env: str | None = None) -> int:
273
+ raw = self.setting(env, "AUTH_MAX_DELEGATION_DEPTH")
274
+ return int(raw) if raw.isdigit() else DEFAULT_MAX_DELEGATION_DEPTH
275
+
276
+ def allowed_actors(self, env: str | None = None) -> list[str]:
277
+ return [a.strip() for a in self.setting(env, "AUTH_ALLOWED_ACTORS").split(",") if a.strip()]
278
+
279
+ @cached_property
280
+ def module_text(self) -> str | None:
281
+ return read_text(gen.module_path(self.root, self.config.agent_directory))
282
+
283
+ @property
284
+ def module_names(self) -> dict[str, str]:
285
+ return gen.names_in(self.module_text)
286
+
287
+ @cached_property
288
+ def peers(self) -> dict[str, gen.Peer]:
289
+ """The peers the project declares, by peer name."""
290
+ return {p.name: p for p in gen.peers_of(self.document, self.module_names)}
291
+
292
+ def entry(self, api: str) -> dict[str, Any] | None:
293
+ found = ((self.document or {}).get("apis") or {}).get(api)
294
+ return found if isinstance(found, dict) else None
295
+
296
+ def requester_gates(self, document: dict[str, Any] | None = None) -> list[Gate]:
297
+ """Every approval rule a requester decides, in the policy (what a relay reaches);
298
+ ``document``: the policy as a plan will leave it (default: as it is)."""
299
+ gates: list[Gate] = []
300
+ policy = self.document if document is None else document
301
+ for api_name, api in ((policy or {}).get("apis") or {}).items():
302
+ rules = approval_rules(api) if isinstance(api, dict) else []
303
+ listed = isinstance(api.get("approval"), list)
304
+ for index, rule in enumerate(rules):
305
+ if REQUESTER_APPROVER not in [str(a) for a in rule.get("approvers") or []]:
306
+ continue
307
+ gates.append(
308
+ Gate(
309
+ api=str(api_name),
310
+ rule=index if listed else None,
311
+ decide_with=str(rule.get("decide_with") or DECIDE_DIRECT),
312
+ relayers=[str(r) for r in rule.get("relayers") or []],
313
+ )
314
+ )
315
+ return gates
316
+
317
+
318
+ @dataclass(frozen=True)
319
+ class Gate:
320
+ """A requester gate of a called agent, as `api approval` names it."""
321
+
322
+ api: str
323
+ rule: int | None # --rule N for a list of rules
324
+ decide_with: str
325
+ relayers: list[str]
326
+
327
+ def relays(self, actor: str) -> bool:
328
+ return self.decide_with == DECIDE_RELAYED and actor in self.relayers
329
+
330
+ def command(self, actor: str) -> str:
331
+ """The `api approval` line that lets ``actor`` relay the requester's decision here."""
332
+ relayers = [*self.relayers, actor] if actor not in self.relayers else self.relayers
333
+ rule = f" --rule {self.rule}" if self.rule is not None else ""
334
+ return (
335
+ f"graph-agents-cli api approval {self.api}{rule} --decide-with relayed "
336
+ f"--relayers {','.join(relayers)}"
337
+ )
338
+
339
+
340
+ @dataclass
341
+ class Link:
342
+ """One edge: ``caller`` calls ``callee``, as the file says."""
343
+
344
+ caller: Node
345
+ callee: Node
346
+ edge: Edge
347
+
348
+ @property
349
+ def auth(self) -> str:
350
+ return self.edge.auth or AUTH_BY_POLICY.get(self.caller.config.auth_policy, "bearer")
351
+
352
+ @property
353
+ def peer(self) -> str:
354
+ return self.callee.name
355
+
356
+ @property
357
+ def existing(self) -> gen.Peer | None:
358
+ """The caller's peer of the callee's name, when it has one (its API and variables are
359
+ kept: a peer added with --api-name or --url-env keeps them)."""
360
+ return self.caller.peers.get(self.callee.name)
361
+
362
+ @property
363
+ def entry(self) -> dict[str, Any] | None:
364
+ peer = self.existing
365
+ return self.caller.entry(peer.api) if peer else None
366
+
367
+ @property
368
+ def api_name(self) -> str:
369
+ peer = self.existing
370
+ return peer.api if peer else f"{self.callee.name}{gen.AGENT_SUFFIX}"
371
+
372
+ @property
373
+ def url_env(self) -> str:
374
+ entry = self.entry
375
+ if entry and entry.get("base_url_env"):
376
+ return str(entry["base_url_env"])
377
+ return f"{self.callee.name.upper()}_AGENT_URL"
378
+
379
+ @property
380
+ def token_env(self) -> str | None:
381
+ if self.auth != "bearer":
382
+ return None
383
+ entry = self.entry
384
+ if entry and entry.get("auth") == "bearer" and entry.get("token_env"):
385
+ return str(entry["token_env"])
386
+ return f"{self.callee.name.upper()}_AGENT_KEY"
387
+
388
+ @property
389
+ def audience(self) -> str:
390
+ """What the callee's AUTH_JWT_AUDIENCE must hold: the callee's name."""
391
+ return self.callee.name
392
+
393
+ @property
394
+ def actor(self) -> str:
395
+ """The actor id the callee sees for the caller (what AUTH_ALLOWED_ACTORS lists).
396
+
397
+ The act.sub of the caller's exchanged tokens (the file's ``actor_id``, else its
398
+ client id); ``client:<client id>`` for tokens that name no actor.
399
+ """
400
+ if self.edge.allow_actorless:
401
+ return f"{CLIENT_ACTOR_PREFIX}{self.caller.client_id}"
402
+ return self.caller.actor_id or self.caller.client_id
403
+
404
+ @property
405
+ def carries_user(self) -> bool:
406
+ """Whether the call carries the user's identity (exchange, forward)."""
407
+ return self.auth in ("exchange", "forward")
408
+
409
+ def describe(self) -> str:
410
+ return f"{self.caller.name} -> {self.callee.name}"
411
+
412
+ def options(self, *, local_url: str | None = None) -> PeerOptions:
413
+ """What `peer add` is given for this edge."""
414
+ description = self.edge.description or self.callee.setting(None, "A2A_DESCRIPTION")
415
+ options = PeerOptions(
416
+ name=self.peer,
417
+ api_name=self.api_name,
418
+ url_env=self.url_env,
419
+ token_env=self.token_env,
420
+ path=self.callee.mount(),
421
+ auth=self.auth,
422
+ audience=self.audience if self.auth in ("exchange", "forward") else None,
423
+ scope=self.edge.scope,
424
+ allow_actorless=self.edge.allow_actorless,
425
+ description=description or None,
426
+ approvals=self.edge.approvals,
427
+ )
428
+ if self.edge.calls is not None:
429
+ options.calls = tuple(self.edge.calls)
430
+ if local_url:
431
+ options.local_url = local_url
432
+ return options
433
+
434
+
435
+ @dataclass
436
+ class System:
437
+ """A system file resolved against its projects."""
438
+
439
+ path: Path
440
+ file: SystemFile
441
+ nodes: dict[str, Node]
442
+ links: list[Link]
443
+ envs: dict[str, Env]
444
+
445
+ @property
446
+ def root(self) -> Path:
447
+ return self.path.parent
448
+
449
+ def calls(self, node: Node) -> list[Link]:
450
+ return [link for link in self.links if link.caller is node]
451
+
452
+ def callers(self, node: Node) -> list[Link]:
453
+ return [link for link in self.links if link.callee is node]
454
+
455
+ def url(self, node: Node, env: str) -> str:
456
+ """The base URL ``node`` is called at in ``env``."""
457
+ spec = self.envs[env]
458
+ if spec.kind == LOCAL:
459
+ return f"http://127.0.0.1:{(spec.port_base or 0) + node.index}"
460
+ if spec.kind == URL:
461
+ return (
462
+ (spec.url or "")
463
+ .replace("{agent}", node.name)
464
+ .replace("{project}", node.config.project_name)
465
+ .replace("{env}", env)
466
+ .rstrip("/")
467
+ )
468
+ port = node.service_port(env)
469
+ suffix = "" if port == DEFAULT_SERVICE_PORT else f":{port}"
470
+ return f"http://{node.release}.{node.namespace(env)}.svc.cluster.local{suffix}"
471
+
472
+ def token_url(self, env: str) -> str | None:
473
+ identity = self.file.identity
474
+ if identity is None or identity.token_url is None:
475
+ return None
476
+ if isinstance(identity.token_url, str):
477
+ return identity.token_url
478
+ return identity.token_url.get(env)
479
+
480
+ def select(self, names: list[str] | tuple[str, ...] | None) -> list[str]:
481
+ """The environments to look at: ``names`` (each must be in the file), else all."""
482
+ if not names:
483
+ return list(self.envs)
484
+ unknown = [n for n in names if n not in self.envs]
485
+ if unknown:
486
+ known = ", ".join(self.envs) or "none"
487
+ raise click.UsageError(
488
+ f"environment {', '.join(unknown)} is not in {self.path.name} (environments: "
489
+ f"{known})"
490
+ )
491
+ return list(dict.fromkeys(names))
492
+
493
+ def local_url(self, node: Node) -> str | None:
494
+ """The URL of ``node`` in the first local environment (for .env.example)."""
495
+ for env in self.envs.values():
496
+ if env.kind == LOCAL:
497
+ return self.url(node, env.name)
498
+ return None
499
+
500
+ def relay_gates(
501
+ self, link: Link, documents: dict[str, dict[str, Any] | None] | None = None
502
+ ) -> list[Gate]:
503
+ """The callee's requester gates that ``link``'s caller cannot relay to (SC07);
504
+ ``documents``: each agent's policy as a plan will leave it."""
505
+ if link.edge.approvals != RELAY:
506
+ return []
507
+ document = (documents or {}).get(link.callee.name)
508
+ gates = link.callee.requester_gates(document)
509
+ return [gate for gate in gates if not gate.relays(link.actor)]
510
+
511
+
512
+ def _true(value: Any) -> bool:
513
+ return value is True or (
514
+ isinstance(value, str) and value.strip().lower() in ("true", "yes", "1", "on")
515
+ )
516
+
517
+
518
+ def _int(value: Any, default: int) -> int:
519
+ if isinstance(value, bool):
520
+ return default
521
+ if isinstance(value, int):
522
+ return value
523
+ if isinstance(value, str) and value.strip().isdigit():
524
+ return int(value.strip())
525
+ return default
526
+
527
+
528
+ def resolve(path: Path) -> System:
529
+ """Read ``path`` and every project it names; ``SystemFileError`` when it cannot be used."""
530
+ data = load(path)
531
+ problems: list[str] = []
532
+ nodes: dict[str, Node] = {}
533
+ roots: dict[Path, str] = {}
534
+ for index, (name, agent) in enumerate(data.agents.items()):
535
+ root = (path.parent / agent.project).resolve()
536
+ if root in roots:
537
+ problems.append(
538
+ f"agents.{name}.project: {agent.project} is also agents.{roots[root]}'s project "
539
+ "(one project is one agent: its release, namespace and card)"
540
+ )
541
+ continue
542
+ roots[root] = name
543
+ if not (root / MANIFEST_FILENAME).is_file():
544
+ problems.append(
545
+ f"agents.{name}.project: {agent.project} is not a graph-agents-cli project (no "
546
+ f"{MANIFEST_FILENAME} in {root})"
547
+ )
548
+ continue
549
+ try:
550
+ project = load_project_at(root)
551
+ except click.ClickException as exc: # an invalid manifest or policy, a legacy file
552
+ problems.append(f"agents.{name} ({agent.project}): {exc.format_message()}")
553
+ continue
554
+ client_id = agent.client_id or name
555
+ nodes[name] = Node(
556
+ name, index, root, project, client_id, actor_id=agent.actor_id or client_id
557
+ )
558
+ clients: dict[str, str] = {}
559
+ actors: dict[str, str] = {}
560
+ for node in nodes.values():
561
+ if node.client_id in clients:
562
+ problems.append(
563
+ f"agents.{node.name}.client_id: {node.client_id} is also agents."
564
+ f"{clients[node.client_id]}'s: each agent needs its own client id"
565
+ )
566
+ clients.setdefault(node.client_id, node.name)
567
+ if node.actor_id in actors and actors[node.actor_id] != node.name:
568
+ problems.append(
569
+ f"agents.{node.name}.actor_id: {node.actor_id} is also agents."
570
+ f"{actors[node.actor_id]}'s actor id: the agents they call could not tell them "
571
+ "apart"
572
+ )
573
+ actors.setdefault(node.actor_id, node.name)
574
+ links: list[Link] = []
575
+ for name in data.agents:
576
+ seen: set[str] = set()
577
+ for position, edge in enumerate(data.edges(name)):
578
+ where = f"agents.{name}.calls[{position}]"
579
+ if edge.agent == name:
580
+ problems.append(f"{where}: an agent cannot call itself ({name})")
581
+ elif edge.agent not in data.agents:
582
+ problems.append(f"{where}: {edge.agent} is not an agent of this file")
583
+ elif edge.agent in seen:
584
+ problems.append(f"{where}: {name} already calls {edge.agent} (one edge per pair)")
585
+ elif name in nodes and edge.agent in nodes:
586
+ links.append(Link(nodes[name], nodes[edge.agent], edge))
587
+ seen.add(edge.agent)
588
+ if data.identity is None:
589
+ exchanges = [link.describe() for link in links if link.auth == "exchange"]
590
+ if exchanges:
591
+ problems.append(
592
+ f"identity: required, since these edges use auth: exchange: {', '.join(exchanges)}"
593
+ )
594
+ envs: dict[str, Env] = {}
595
+ for env_name in data.environments:
596
+ spec = data.environment(env_name)
597
+ kind = LOCAL if spec.port_base is not None else URL if spec.url is not None else CLUSTER
598
+ envs[env_name] = Env(env_name, kind, spec.port_base, spec.url)
599
+ if kind == LOCAL:
600
+ continue
601
+ for node in nodes.values():
602
+ try:
603
+ node.config.environment(env_name)
604
+ except click.UsageError:
605
+ problems.append(
606
+ f"environments.{env_name}: {node.name}'s manifest does not know this "
607
+ f"environment (add it under environments: in {node.root / MANIFEST_FILENAME})"
608
+ )
609
+ for label, keys in (
610
+ (
611
+ "identity.token_url",
612
+ list(data.identity.token_url)
613
+ if data.identity and isinstance(data.identity.token_url, dict)
614
+ else [],
615
+ ),
616
+ ("database.max_connections", list(data.database.max_connections) if data.database else []),
617
+ ):
618
+ for key in keys:
619
+ if key not in envs:
620
+ problems.append(f"{label}.{key}: not an environment of this file")
621
+ for env in envs.values():
622
+ if env.kind == URL:
623
+ leftover = (env.url or "").replace("{agent}", "").replace("{project}", "")
624
+ leftover = leftover.replace("{env}", "")
625
+ if "{" in leftover or "}" in leftover:
626
+ problems.append(
627
+ f"environments.{env.name}.url: only {{agent}}, {{project}} and {{env}} may be "
628
+ "filled in"
629
+ )
630
+ if problems:
631
+ lines = "\n".join(f" - {problem}" for problem in problems)
632
+ raise SystemFileError(f"{path} cannot be used:\n{lines}")
633
+ return System(path, data, nodes, links, envs)
634
+
635
+
636
+ def locate(file: str | None) -> Path:
637
+ """The system file: ``--file``, else ``graph-agents-system.yaml`` here or above."""
638
+ from graph_agents_cli.system._model import SYSTEM_FILENAME, find
639
+
640
+ if file:
641
+ path = Path(file)
642
+ if not path.is_file():
643
+ raise SystemFileError(f"{file}: no such file")
644
+ return path.resolve()
645
+ found = find()
646
+ if found is None:
647
+ raise SystemFileError(f"no {SYSTEM_FILENAME} in this directory or its parents; pass --file")
648
+ return found
649
+
650
+
651
+ __all__ = [
652
+ "CLUSTER",
653
+ "DENY",
654
+ "LOCAL",
655
+ "RELAY",
656
+ "URL",
657
+ "Env",
658
+ "Gate",
659
+ "Link",
660
+ "Node",
661
+ "System",
662
+ "locate",
663
+ "resolve",
664
+ ]