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,519 @@
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
+ """``system apply``: make every project of a system agree with the file.
16
+
17
+ One ``Plan`` per project, built from the project's files as they are (so a
18
+ second run finds nothing to do):
19
+
20
+ - **as a caller**, for each edge, what ``peer add`` writes (``plan_add``, with the
21
+ path from the called agent's A2A name and the audience from its name), and per
22
+ environment the called agent's URL (``<PEER>_AGENT_URL``), the issuer's token
23
+ endpoint (``TOKEN_EXCHANGE_URL``) and ``networkPolicy.egressTo`` to the called
24
+ agent's pods; ``TOKEN_EXCHANGE_CLIENT_ID`` is the file's ``client_id``. A peer
25
+ that is an agent of the file and has left ``calls`` is removed
26
+ (``plan_remove``); other peers are never touched.
27
+ - **as a called agent**: ``appUrl`` per environment (the URL callers dial, so
28
+ their card check passes), ``AUTH_JWT_AUDIENCE`` when it is empty, the callers'
29
+ client ids added to ``AUTH_ALLOWED_ACTORS``, and per environment
30
+ ``networkPolicy.ingressFrom`` from the callers' pods, plus the Gateway's
31
+ namespace while the route publishes anything (the Gateway must still reach
32
+ ``/chat`` and whatever else it routes once the policy is on).
33
+
34
+ Never written: approval gates (a called agent's ``decide_with: relayed`` stays a
35
+ reviewed ``api approval`` command, printed), secrets, ``.env``, and anything for
36
+ local environments (their lines are printed). An existing peer's entry is
37
+ rewritten only where it differs from the file; its limits, timeouts, approval
38
+ timeout, resource indicator and description stay (``plan_add(keep_tuning=True)``).
39
+ """
40
+
41
+ from __future__ import annotations
42
+
43
+ import copy
44
+ from collections.abc import Callable
45
+ from dataclasses import dataclass, field
46
+ from pathlib import Path
47
+ from typing import Any
48
+
49
+ import click
50
+
51
+ from graph_agents_cli._api_policy import (
52
+ TOKEN_EXCHANGE_CLIENT_ID_ENV,
53
+ TOKEN_EXCHANGE_URL_ENV,
54
+ )
55
+ from graph_agents_cli.api._files import Plan, read_text, values_env_set
56
+ from graph_agents_cli.peer import _generate as gen
57
+ from graph_agents_cli.peer.cmd_peer import plan_add, plan_remove
58
+ from graph_agents_cli.scaffold.utils.keyedit import EditError, YamlText
59
+ from graph_agents_cli.system._system import CLUSTER, LOCAL, Link, Node, System
60
+
61
+ NS_LABEL = "kubernetes.io/metadata.name"
62
+ INGRESS_PATH = ("networkPolicy", "ingressFrom")
63
+ EGRESS_PATH = ("networkPolicy", "egressTo")
64
+ ALLOWED_ACTORS_ENV = "AUTH_ALLOWED_ACTORS"
65
+ AUDIENCE_ENV = "AUTH_JWT_AUDIENCE"
66
+ ALLOWED_ACTORS_COMMENT = (
67
+ "Agents that may call this agent for a user: their actor ids (graph-agents-cli system apply "
68
+ "adds its callers)."
69
+ )
70
+ APP_URL_COMMENT = "The URL other agents call this agent at (graph-agents-cli system apply)."
71
+
72
+
73
+ class SystemApplyError(click.ClickException):
74
+ """A project cannot take the file's edges (exit 3); nothing was written."""
75
+
76
+ exit_code = 3
77
+
78
+
79
+ @dataclass
80
+ class ProjectPlan:
81
+ """What ``system apply`` changes in one project, and what it leaves to the user."""
82
+
83
+ node: Node
84
+ plan: Plan
85
+ notes: list[str] = field(default_factory=list)
86
+ todos: list[str] = field(default_factory=list)
87
+ document: dict[str, Any] | None = None # the policy as it will be
88
+
89
+ def note(self, text: str) -> None:
90
+ if text not in self.notes:
91
+ self.notes.append(text)
92
+
93
+ def todo(self, text: str) -> None:
94
+ if text not in self.todos:
95
+ self.todos.append(text)
96
+
97
+
98
+ def pod_peer(namespace: str, labels: dict[str, str]) -> dict[str, Any]:
99
+ """A NetworkPolicy peer: the agent pods (``labels``, the chart's selector labels) in
100
+ ``namespace``; both must match."""
101
+ return {
102
+ "namespaceSelector": {"matchLabels": {NS_LABEL: namespace}},
103
+ "podSelector": {"matchLabels": dict(labels)},
104
+ }
105
+
106
+
107
+ def egress_rule(namespace: str, labels: dict[str, str], port: int | str) -> dict[str, Any]:
108
+ """An egress rule to the agent pods, on their http port (the pod's port, not the
109
+ Service's: a NetworkPolicy sees the connection after the Service has translated it)."""
110
+ return {"to": [pod_peer(namespace, labels)], "ports": [{"port": port, "protocol": "TCP"}]}
111
+
112
+
113
+ def gateway_peer(namespace: str) -> dict[str, Any]:
114
+ return {"namespaceSelector": {"matchLabels": {NS_LABEL: namespace}}}
115
+
116
+
117
+ def _rel(node: Node, path: Path) -> str:
118
+ return path.relative_to(node.root).as_posix()
119
+
120
+
121
+ def _edit(result: ProjectPlan, rel: str, action: Callable[[YamlText], None], what: str) -> None:
122
+ """Apply ``action`` to the planned text of the values file ``rel``; a todo when it cannot."""
123
+ plan = result.plan
124
+ before = plan.current(rel)
125
+ if before is None:
126
+ result.todo(f"{rel}: {what} (the file does not exist)")
127
+ return
128
+ try:
129
+ values = YamlText(before)
130
+ action(values)
131
+ except EditError as exc:
132
+ result.todo(f"{rel}: {what} (not edited: {exc})")
133
+ return
134
+ plan.set_text(rel, read_text(plan.root / rel), values.text)
135
+
136
+
137
+ def _set_env(values: YamlText, key: str, value: str, *, comment: str | None = None) -> None:
138
+ env = values.get(("env",))
139
+ if env is None:
140
+ values.set(("env",), {key: value})
141
+ elif not isinstance(env, dict):
142
+ raise EditError("env: is not a mapping")
143
+ elif key not in env:
144
+ values.set(("env", key), value, after="AUTH_ADMIN_ROLES", comment=comment)
145
+ elif env.get(key) != value:
146
+ values.set(("env", key), value)
147
+
148
+
149
+ def _sync_list(
150
+ values: YamlText,
151
+ path: tuple[str, ...],
152
+ base: list[Any],
153
+ desired: list[dict[str, Any]],
154
+ managed: list[dict[str, Any]],
155
+ ) -> None:
156
+ """Make the list at ``path`` hold every ``desired`` entry and no ``managed`` one that is
157
+ not desired; other entries stay. A values-<env>.yaml without the list starts from the
158
+ base values' list (Helm replaces a list whole, so the base entries must be repeated)."""
159
+ current = values.get(path)
160
+ if current is None:
161
+ start = [e for e in base if not (e in managed and e not in desired)]
162
+ new = start + [d for d in desired if d not in start]
163
+ if new != base:
164
+ values.set(path, new, block=True)
165
+ return
166
+ if not isinstance(current, list):
167
+ raise EditError(f"{'.'.join(path)} is not a list")
168
+ for entry in desired:
169
+ if entry not in values.get(path):
170
+ values.append(path, entry)
171
+ stale = [i for i, e in enumerate(values.get(path)) if e in managed and e not in desired]
172
+ for index in reversed(stale):
173
+ values.remove_item(path, index)
174
+
175
+
176
+ def _base_list(node: Node, path: tuple[str, ...]) -> list[Any]:
177
+ found: Any = node.values(None)
178
+ for part in path:
179
+ found = found.get(part) if isinstance(found, dict) else None
180
+ return copy.deepcopy(found) if isinstance(found, list) else []
181
+
182
+
183
+ def _network_enabled(node: Node, env: str) -> tuple[bool, bool]:
184
+ policy = node.values(env).get("networkPolicy")
185
+ policy = policy if isinstance(policy, dict) else {}
186
+ return policy.get("enabled") is True, policy.get("restrictEgress") is True
187
+
188
+
189
+ def plan_system(system: System, envs: list[str]) -> list[ProjectPlan]:
190
+ """A plan per project (file order); ``SystemApplyError`` before anything is written."""
191
+ results = []
192
+ for node in system.nodes.values():
193
+ result = ProjectPlan(node, Plan(node.root), document=node.document)
194
+ _plan_caller(system, node, envs, result)
195
+ _plan_callee(system, node, envs, result)
196
+ for item in result.plan.left_for_you:
197
+ result.todo(item)
198
+ results.append(result)
199
+ return results
200
+
201
+
202
+ def _first_local(system: System, envs: list[str]) -> str | None:
203
+ return next((e for e in envs if system.envs[e].kind == LOCAL), None)
204
+
205
+
206
+ def _plan_caller(system: System, node: Node, envs: list[str], result: ProjectPlan) -> None:
207
+ links = system.calls(node)
208
+ project = node.project
209
+ names: dict[str, str] = {}
210
+ local = _first_local(system, envs)
211
+ for link in links:
212
+ options = link.options(local_url=system.url(link.callee, local) if local else None)
213
+ try:
214
+ added = plan_add(project, result.plan, options, names=names, keep_tuning=True)
215
+ except click.ClickException as exc:
216
+ raise SystemApplyError(
217
+ f"{link.describe()}: {node.name} ({node.root}) cannot take this edge; nothing was "
218
+ f"written:\n {exc.format_message()}"
219
+ ) from None
220
+ project, names = added.project, added.names
221
+ if added.api != link.entry: # peer add's notes, for an entry this run writes
222
+ for note in added.notes:
223
+ result.note(note)
224
+ wanted = {link.callee.name for link in links}
225
+ current = gen.peers_of(project.document, {**node.module_names, **names})
226
+ for peer in current:
227
+ if peer.name in system.nodes and peer.name not in wanted:
228
+ try:
229
+ removed = plan_remove(project, result.plan, peer.name, names=names)
230
+ except click.ClickException as exc:
231
+ raise SystemApplyError(
232
+ f"{node.name}: the peer {peer.name} left its calls but cannot be removed; "
233
+ f"nothing was written:\n {exc.format_message()}"
234
+ ) from None
235
+ project = removed.project
236
+ result.note(f"{peer.name} is no longer in {node.name}'s calls: its peer entry goes")
237
+ for note in removed.notes:
238
+ result.note(note)
239
+ result.document = project.document
240
+ charted = [e for e in envs if system.envs[e].charted]
241
+ if not links:
242
+ # No edge left: only the egress rules to agents of the file go.
243
+ for env in charted:
244
+ if node.has_chart and system.envs[env].kind == CLUSTER:
245
+ _plan_egress(system, node, env, [], result)
246
+ return
247
+ exchange = any(link.auth == "exchange" for link in links)
248
+ if node.has_chart:
249
+ for link in links:
250
+ urls = {env: system.url(link.callee, env) for env in charted}
251
+ values_env_set(result.plan, node.config, link.url_env, urls)
252
+ if exchange:
253
+ tokens: dict[str, str] = {}
254
+ for env in charted:
255
+ token = system.token_url(env)
256
+ if token:
257
+ tokens[env] = token
258
+ values_env_set(result.plan, node.config, TOKEN_EXCHANGE_URL_ENV, tokens)
259
+ missing = [e for e in charted if e not in tokens]
260
+ if missing:
261
+ result.todo(
262
+ f"set {TOKEN_EXCHANGE_URL_ENV} for {', '.join(missing)} in "
263
+ "values-<env>.yaml (identity.token_url names none)"
264
+ )
265
+ _set_client_id(node, result)
266
+ for env in charted:
267
+ if system.envs[env].kind == CLUSTER:
268
+ _plan_egress(system, node, env, links, result)
269
+ for env in envs:
270
+ if system.envs[env].kind != LOCAL:
271
+ continue
272
+ lines = [f"{link.url_env}={system.url(link.callee, env)}" for link in links]
273
+ if exchange:
274
+ token = system.token_url(env)
275
+ lines.append(f"{TOKEN_EXCHANGE_URL_ENV}={token or '<the issuer token endpoint>'}")
276
+ lines.append(f"{TOKEN_EXCHANGE_CLIENT_ID_ENV}={node.client_id}")
277
+ result.todo(f"in {node.name}'s .env for {env} (never written): {', '.join(lines)}")
278
+
279
+
280
+ def _set_client_id(node: Node, result: ProjectPlan) -> None:
281
+ """``TOKEN_EXCHANGE_CLIENT_ID`` is the file's client id: in values.yaml (where `peer add`
282
+ put the project's name), and in each values-<env>.yaml that sets its own."""
283
+ files = node.chart_files
284
+ for index, path in enumerate(files):
285
+ rel = _rel(node, path)
286
+ text = result.plan.current(rel) or ""
287
+ try:
288
+ env = YamlText(text).get(("env",))
289
+ except EditError:
290
+ env = None
291
+ if index > 0 and not (isinstance(env, dict) and TOKEN_EXCHANGE_CLIENT_ID_ENV in env):
292
+ continue
293
+ _edit(
294
+ result,
295
+ rel,
296
+ lambda v: _set_env(v, TOKEN_EXCHANGE_CLIENT_ID_ENV, node.client_id),
297
+ f"set env.{TOKEN_EXCHANGE_CLIENT_ID_ENV}: {node.client_id}",
298
+ )
299
+
300
+
301
+ def _plan_egress(
302
+ system: System, node: Node, env: str, links: list[Link], result: ProjectPlan
303
+ ) -> None:
304
+ values_file = node.values_file(env)
305
+ if values_file is None:
306
+ if links:
307
+ result.todo(f"values-{env}.yaml does not exist: no egress rules written for {env}")
308
+ return
309
+ desired = [
310
+ egress_rule(
311
+ link.callee.namespace(env), link.callee.pod_labels, link.callee.target_port(env)
312
+ )
313
+ for link in links
314
+ ]
315
+ managed = [
316
+ egress_rule(other.namespace(env), other.pod_labels, other.target_port(env))
317
+ for other in system.nodes.values()
318
+ ]
319
+ base = _base_list(node, EGRESS_PATH)
320
+ _edit(
321
+ result,
322
+ _rel(node, values_file),
323
+ lambda v: _sync_list(v, EGRESS_PATH, base, desired, managed),
324
+ f"networkPolicy.egressTo: allow the pods of {', '.join(lk.callee.name for lk in links)}",
325
+ )
326
+ enabled, restricted = _network_enabled(node, env)
327
+ if not links:
328
+ return
329
+ if not (enabled and restricted):
330
+ result.note(
331
+ f"values-{env}.yaml: the egress rules take effect with networkPolicy.enabled and "
332
+ "restrictEgress (both off now)"
333
+ )
334
+ elif any(link.auth == "exchange" for link in links):
335
+ result.todo(
336
+ f"values-{env}.yaml: with restrictEgress, also allow the token issuer "
337
+ f"({TOKEN_EXCHANGE_URL_ENV}) in networkPolicy.egressTo"
338
+ )
339
+
340
+
341
+ def _stale_actors(system: System, node: Node, actors: list[str]) -> list[str]:
342
+ """Agents of the file ``node``'s AUTH_ALLOWED_ACTORS still lists that no longer call it."""
343
+ clients = {n.client_id for n in system.nodes.values()}
344
+ known = clients | {n.actor_id for n in system.nodes.values()}
345
+ known |= {f"client:{client}" for client in clients}
346
+ listed = node.allowed_actors() if node.has_chart else []
347
+ return [a for a in listed if a in known and a not in actors]
348
+
349
+
350
+ def _plan_callee(system: System, node: Node, envs: list[str], result: ProjectPlan) -> None:
351
+ links = system.callers(node)
352
+ charted = [e for e in envs if system.envs[e].charted]
353
+ stale = _stale_actors(system, node, [link.actor for link in links if link.carries_user])
354
+ if stale:
355
+ result.note(
356
+ f"AUTH_ALLOWED_ACTORS still lists {', '.join(stale)}, which no longer call "
357
+ f"{node.name} for a user: remove them if nothing else does (apply never takes access "
358
+ "away)"
359
+ )
360
+ if not links:
361
+ # Nobody calls it any more: only the ingress rules for agents of the file go (its
362
+ # appUrl, audience and allowed actors stay: apply never takes access away).
363
+ for env in charted:
364
+ if node.has_chart and system.envs[env].kind == CLUSTER:
365
+ _plan_ingress(system, node, env, [], result)
366
+ return
367
+ identity = [link for link in links if link.carries_user]
368
+ actors = list(dict.fromkeys(link.actor for link in identity))
369
+ jwt = node.auth_policy() == "jwt"
370
+ if node.has_chart:
371
+ for env in charted:
372
+ values_file = node.values_file(env)
373
+ if values_file is None:
374
+ result.todo(f"values-{env}.yaml does not exist: set appUrl for {env} there")
375
+ continue
376
+ url = system.url(node, env)
377
+ _edit(
378
+ result,
379
+ _rel(node, values_file),
380
+ lambda v, url=url: v.set(("appUrl",), url, comment=APP_URL_COMMENT),
381
+ f"set appUrl: {url}",
382
+ )
383
+ if identity and jwt:
384
+ _set_audience(node, result)
385
+ if actors:
386
+ _add_actors(node, actors, result)
387
+ for env in charted:
388
+ if system.envs[env].kind == CLUSTER:
389
+ _plan_ingress(system, node, env, links, result)
390
+ for env in envs:
391
+ if system.envs[env].kind != LOCAL:
392
+ continue
393
+ port = system.url(node, env).rsplit(":", 1)[-1]
394
+ lines = [f"port {port} (APP_URL=http://127.0.0.1:{port})"]
395
+ if identity and jwt:
396
+ lines.append(f"{AUDIENCE_ENV} including {node.name}")
397
+ if actors:
398
+ lines.append(f"{ALLOWED_ACTORS_ENV} including {','.join(actors)}")
399
+ result.todo(f"run {node.name} for {env} on {', '.join(lines)} (.env is never written)")
400
+ if not jwt and identity:
401
+ result.note(
402
+ f"{node.name} uses the {node.auth_policy()} auth policy: its policy must accept the "
403
+ f"callers' credentials and set their actor ({', '.join(actors)})"
404
+ )
405
+
406
+
407
+ def _set_audience(node: Node, result: ProjectPlan) -> None:
408
+ """``AUTH_JWT_AUDIENCE`` becomes the agent's name where it is empty (values.yaml, and each
409
+ values-<env>.yaml that sets it empty); a set audience is never changed (SC04 checks it)."""
410
+ for index, path in enumerate(node.chart_files):
411
+ rel = _rel(node, path)
412
+ try:
413
+ env = YamlText(result.plan.current(rel) or "").get(("env",))
414
+ except EditError:
415
+ continue
416
+ env = env if isinstance(env, dict) else {}
417
+ if index > 0 and AUDIENCE_ENV not in env:
418
+ continue
419
+ if str(env.get(AUDIENCE_ENV) or "").strip():
420
+ continue
421
+ _edit(
422
+ result,
423
+ rel,
424
+ lambda v: _set_env(v, AUDIENCE_ENV, node.name),
425
+ f"set env.{AUDIENCE_ENV}: {node.name}",
426
+ )
427
+
428
+
429
+ def _add_actors(node: Node, actors: list[str], result: ProjectPlan) -> None:
430
+ """The callers' actor ids join ``AUTH_ALLOWED_ACTORS`` (values.yaml, and each
431
+ values-<env>.yaml that sets its own). Ids already there, and ``*``, stay; none goes."""
432
+ for index, path in enumerate(node.chart_files):
433
+ rel = _rel(node, path)
434
+ try:
435
+ env = YamlText(result.plan.current(rel) or "").get(("env",))
436
+ except EditError:
437
+ continue
438
+ env = env if isinstance(env, dict) else {}
439
+ if index > 0 and ALLOWED_ACTORS_ENV not in env:
440
+ continue
441
+ present = [a.strip() for a in str(env.get(ALLOWED_ACTORS_ENV) or "").split(",")]
442
+ present = [a for a in present if a]
443
+ if "*" in present:
444
+ continue
445
+ wanted = present + [a for a in actors if a not in present]
446
+ if wanted == present:
447
+ continue
448
+ value = ",".join(wanted)
449
+ _edit(
450
+ result,
451
+ rel,
452
+ lambda v, value=value: _set_env(
453
+ v, ALLOWED_ACTORS_ENV, value, comment=ALLOWED_ACTORS_COMMENT
454
+ ),
455
+ f"set env.{ALLOWED_ACTORS_ENV}: {value}",
456
+ )
457
+
458
+
459
+ def _plan_ingress(
460
+ system: System, node: Node, env: str, links: list[Link], result: ProjectPlan
461
+ ) -> None:
462
+ values_file = node.values_file(env)
463
+ if values_file is None:
464
+ return
465
+ desired = [pod_peer(link.caller.namespace(env), link.caller.pod_labels) for link in links]
466
+ managed = [pod_peer(o.namespace(env), o.pod_labels) for o in system.nodes.values()]
467
+ if links and node.public_paths(env):
468
+ values = node.values(env)
469
+ gateway = values.get("gateway") if isinstance(values.get("gateway"), dict) else {}
470
+ parent = gateway.get("parentRef") if isinstance(gateway.get("parentRef"), dict) else {}
471
+ namespace = str(parent.get("namespace") or "").strip()
472
+ if gateway.get("enabled") is True and namespace:
473
+ desired.append(gateway_peer(namespace))
474
+ else:
475
+ result.todo(
476
+ f"values-{env}.yaml: the route publishes paths, so networkPolicy.ingressFrom must "
477
+ "also admit the namespace of the Gateway's (or ingress controller's) pods: add it"
478
+ )
479
+ base = _base_list(node, INGRESS_PATH)
480
+ _edit(
481
+ result,
482
+ _rel(node, values_file),
483
+ lambda v: _sync_list(v, INGRESS_PATH, base, desired, managed),
484
+ f"networkPolicy.ingressFrom: admit {', '.join(lk.caller.name for lk in links)}",
485
+ )
486
+ enabled, _restricted = _network_enabled(node, env)
487
+ if not links:
488
+ return
489
+ if not enabled:
490
+ result.note(
491
+ f"values-{env}.yaml: the ingress rules take effect with networkPolicy.enabled (off now)"
492
+ )
493
+ metrics = node.values(env).get("metrics")
494
+ metrics = metrics if isinstance(metrics, dict) else {}
495
+ monitor = (
496
+ metrics.get("serviceMonitor") if isinstance(metrics.get("serviceMonitor"), dict) else {}
497
+ )
498
+ if metrics.get("scrapeAnnotations") is True or monitor.get("enabled") is True:
499
+ result.todo(
500
+ f"values-{env}.yaml: Prometheus scrapes /metrics: admit its namespace in "
501
+ "networkPolicy.ingressFrom too"
502
+ )
503
+
504
+
505
+ def relay_commands(
506
+ system: System, results: list[ProjectPlan] | None = None
507
+ ) -> list[tuple[Node, str, str]]:
508
+ """``(called agent, why, command)`` for each relay edge a called agent's gates refuse, the
509
+ policies as ``results`` leave them (a caller's own relay gates come with its peers)."""
510
+ documents = {r.node.name: r.document for r in results or []}
511
+ found = []
512
+ for link in system.links:
513
+ for gate in system.relay_gates(link, documents):
514
+ why = (
515
+ f"relays from {link.caller.name} to {link.callee.name} need the person to approve "
516
+ f"at {link.callee.name}"
517
+ )
518
+ found.append((link.callee, why, gate.command(link.actor)))
519
+ return found