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,479 @@
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
+ """An agent asks another for the user, and relays the user's approval (end to end).
16
+
17
+ One app plays both agents, in process: the concierge (alice talks to it over
18
+ `/chat` with her own token; its tools are `peer_tools`, calling the other agent
19
+ over the ASGI transport, with a token the fake issuer mints in exchange for
20
+ alice's) and orders (its A2A endpoint, and a `cancelOrder` call to a backend
21
+ that records every request, gated so that the concierge may relay alice's
22
+ decision). The loop check's idea of "this agent" is pinned to the concierge
23
+ (both agents are one process here).
24
+
25
+ * The relay: alice asks the concierge to cancel; orders pauses for approval;
26
+ alice approves at the concierge, seeing orders' call as the `effect`; the
27
+ decision reaches orders once, the order is cancelled once, orders records
28
+ `decided_via: concierge`, its waiting task follows, and the token was
29
+ exchanged once for the pair (alice, orders).
30
+ * When orders checks the order against the user's own words, the run alice's
31
+ relayed approval resumes there checks the words she asked with, not those she
32
+ said when approving at the concierge.
33
+ * When orders lost the task before alice decided, the relay reads its approvals
34
+ ledger instead and still sends exactly the decision she approved.
35
+ * With orders' gate at `decide_with: direct`, the concierge relays nothing: it
36
+ says alice must approve at orders, and she does, with her own token there.
37
+
38
+ Every test runs with the in-memory checkpointer, and on Postgres when
39
+ `TEST_POSTGRES_DSN` is set (its sessions in a time zone other than UTC).
40
+ """
41
+
42
+ from __future__ import annotations
43
+
44
+ import asyncio
45
+ import json
46
+ import os
47
+ import re
48
+ import time
49
+ import uuid
50
+ from collections.abc import AsyncIterator, Iterator
51
+ from pathlib import Path
52
+ from typing import Any
53
+ from urllib.parse import urlsplit
54
+
55
+ # The environment must be in place before the app (and the graph) is imported.
56
+ os.environ.update(
57
+ {
58
+ "MODEL_PROVIDER": "fake",
59
+ "MODEL_NAME": "fake",
60
+ "CHECKPOINTER": "memory",
61
+ "AUTH_POLICY": "shared-bearer",
62
+ "API_KEY": "test-key",
63
+ "APP_ENV": "dev",
64
+ "TRACING_ENABLED": "false",
65
+ "RUNTIME": "fastapi",
66
+ "APP_URL": "http://testserver",
67
+ }
68
+ )
69
+
70
+ import httpx
71
+ import jwt
72
+ import pytest
73
+ from fake_issuer import FakeIssuer
74
+ from langchain.tools import ToolRuntime
75
+ from langchain_core.tools import tool
76
+
77
+ from {{cookiecutter.agent_directory}}.app_utils import a2a as a2a_module
78
+ from {{cookiecutter.agent_directory}}.app_utils import token_exchange
79
+ from {{cookiecutter.agent_directory}}.app_utils.a2a_client import (
80
+ context_id_for,
81
+ peer_tools,
82
+ reset_a2a_client,
83
+ )
84
+ from {{cookiecutter.agent_directory}}.app_utils.api_client import (
85
+ get_client,
86
+ require_user_mentioned,
87
+ )
88
+ from {{cookiecutter.agent_directory}}.app_utils.api_client import (
89
+ reset_policy_cache as reset_api_policy,
90
+ )
91
+ from {{cookiecutter.agent_directory}}.app_utils.auth import reset_policy_cache
92
+ from {{cookiecutter.agent_directory}}.fast_api_app import app
93
+
94
+ A2A_PATH = "/a2a/{{cookiecutter.agent_directory}}"
95
+ SECRET = "s3cret"
96
+
97
+ POLICY = """
98
+ apis:
99
+ orders_agent:
100
+ description: "Orders agent: cancels the caller's orders after approval."
101
+ protocol: a2a
102
+ a2a: {path: /a2a/{{cookiecutter.agent_directory}}}
103
+ base_url_env: ORDERS_AGENT_URL
104
+ auth: exchange
105
+ exchange: {audience: orders}
106
+ allowed_methods: [GET, POST]
107
+ allowed_operations:
108
+ - operationId: getAgentCard
109
+ methods: [GET]
110
+ path: /a2a/{{cookiecutter.agent_directory}}/.well-known/agent-card.json
111
+ - {rpc_method: SendMessage, methods: [POST], path: /a2a/{{cookiecutter.agent_directory}}}
112
+ - {rpc_method: GetTask, methods: [POST], path: /a2a/{{cookiecutter.agent_directory}}}
113
+ - {operationId: listContextApprovals, methods: [GET], path: "/threads/{context_id}/approvals"}
114
+ approval:
115
+ - required_for: {operations: [{a2a_operation: approve}]}
116
+ approvers: [requester]
117
+ timeout_s: 900
118
+ limits: {max_calls_per_run: 12, max_response_bytes: 1048576}
119
+ timeouts_ms: {connect: 2000, read: 120000}
120
+ shop:
121
+ base_url_env: SHOP_API_BASE_URL
122
+ auth: none
123
+ allowed_methods: [GET, POST]
124
+ approval:
125
+ required_for:
126
+ operations:
127
+ - operationId: cancelOrder
128
+ path: /orders/{order_id}/cancel
129
+ methods: [POST]
130
+ approvers: [requester]
131
+ timeout_s: 60
132
+ """
133
+ RELAYED = " decide_with: relayed\n relayers: [concierge]\n"
134
+ PEERS = {
135
+ "orders": {
136
+ "api": "orders_agent",
137
+ "approvals": "relay",
138
+ "description": "Orders agent: cancels the caller's orders after approval.",
139
+ }
140
+ }
141
+
142
+ # What orders' cancel sends: an integer a double cannot hold, which the person must see exactly.
143
+ CANCEL_BODY = {"reason": "the customer asked", "amount": 12345678901234567}
144
+ # What the backend received.
145
+ SENT: list[httpx.Request] = []
146
+ # When it holds True, orders cancels only an order the user's own words name.
147
+ CHECK_MENTION: list[bool] = []
148
+
149
+
150
+ def _backend(request: httpx.Request) -> httpx.Response:
151
+ SENT.append(request)
152
+ return httpx.Response(200, json={"cancelled": request.url.path})
153
+
154
+
155
+ @tool
156
+ async def cancel_order(order_id: int, runtime: ToolRuntime[Any]) -> str:
157
+ """Cancel an order by its number."""
158
+ if CHECK_MENTION:
159
+ require_user_mentioned(order_id, runtime)
160
+ context = getattr(runtime, "context", None)
161
+ client = get_client("shop", context=context, transport=httpx.MockTransport(_backend))
162
+ data = await client.post(
163
+ "/orders/{order_id}/cancel",
164
+ operation_id="cancelOrder",
165
+ path_params={"order_id": order_id},
166
+ json_body=CANCEL_BODY,
167
+ )
168
+ return json.dumps(data)
169
+
170
+
171
+ @pytest.fixture(scope="module")
172
+ def issuer() -> Iterator[FakeIssuer]:
173
+ server = FakeIssuer({"concierge": (SECRET, {"orders"})})
174
+ try:
175
+ yield server
176
+ finally:
177
+ server.close()
178
+
179
+
180
+ ADMIN_DSN = os.environ.get("TEST_POSTGRES_DSN", "")
181
+
182
+
183
+ @pytest.fixture(params=["memory", "postgres"])
184
+ async def database(
185
+ request: pytest.FixtureRequest, monkeypatch: pytest.MonkeyPatch
186
+ ) -> AsyncIterator[str]:
187
+ if request.param == "memory":
188
+ yield "memory"
189
+ return
190
+ if not ADMIN_DSN:
191
+ pytest.skip("TEST_POSTGRES_DSN is not set")
192
+ import psycopg
193
+
194
+ name = f"gac_test_{uuid.uuid4().hex[:12]}"
195
+ async with await psycopg.AsyncConnection.connect(ADMIN_DSN, autocommit=True) as admin:
196
+ await admin.execute(f'CREATE DATABASE "{name}"')
197
+ # Sessions answer in a zone other than UTC (as a server set up on a laptop does).
198
+ await admin.execute(f"ALTER DATABASE \"{name}\" SET timezone TO 'America/New_York'")
199
+ monkeypatch.setenv("CHECKPOINTER", "postgres")
200
+ monkeypatch.setenv("POSTGRES_DSN", urlsplit(ADMIN_DSN)._replace(path=f"/{name}").geturl())
201
+ yield "postgres"
202
+ async with await psycopg.AsyncConnection.connect(ADMIN_DSN, autocommit=True) as admin:
203
+ await admin.execute(f'DROP DATABASE IF EXISTS "{name}" WITH (FORCE)')
204
+
205
+
206
+ async def _agents(
207
+ issuer: FakeIssuer,
208
+ tmp_path: Path,
209
+ monkeypatch: pytest.MonkeyPatch,
210
+ use_test_tools: Any,
211
+ decide: str,
212
+ ) -> AsyncIterator[dict[str, Any]]:
213
+ policy = tmp_path / "api-policy.yaml"
214
+ policy.write_text(POLICY + decide, encoding="utf-8")
215
+ for name, value in {
216
+ "API_POLICY_PATH": str(policy),
217
+ "SHOP_API_BASE_URL": "http://shop.test",
218
+ "ORDERS_AGENT_URL": "http://testserver",
219
+ "AUTH_POLICY": "jwt",
220
+ "AUTH_JWT_JWKS_URL": issuer.jwks_url,
221
+ "AUTH_JWT_ISSUER": issuer.issuer,
222
+ "AUTH_JWT_AUDIENCE": "concierge,orders",
223
+ "AUTH_ALLOWED_ACTORS": "concierge",
224
+ "TOKEN_EXCHANGE_URL": issuer.token_url,
225
+ "TOKEN_EXCHANGE_CLIENT_ID": "concierge",
226
+ "TOKEN_EXCHANGE_CLIENT_SECRET": SECRET,
227
+ }.items():
228
+ monkeypatch.setenv(name, value)
229
+ for name in ("AUTH_JWT_PUBLIC_KEY", "TRACE_CAPTURE", "PRINCIPAL_HASH_SALT", "A2A_NAME"):
230
+ monkeypatch.delenv(name, raising=False)
231
+ # One process plays both agents: "this agent", for the loop check, is the concierge.
232
+ monkeypatch.setattr(token_exchange, "own_names", lambda env=None: {"concierge"})
233
+ reset_policy_cache()
234
+ reset_api_policy()
235
+ reset_a2a_client()
236
+ token_exchange.reset_token_exchange()
237
+ SENT.clear()
238
+ CHECK_MENTION.clear()
239
+ issuer.requests.clear()
240
+ user = f"alice-{uuid.uuid4().hex[:8]}"
241
+ async with app.router.lifespan_context(app):
242
+ transport = httpx.ASGITransport(app=app, raise_app_exceptions=False)
243
+ use_test_tools(*reversed(peer_tools(PEERS, transport=transport)), cancel_order)
244
+ async with httpx.AsyncClient(
245
+ transport=transport, base_url="http://testserver", timeout=60
246
+ ) as client:
247
+ started = asyncio.get_running_loop().time()
248
+ while (await client.get("/ready")).status_code != 200: # postgres: the schema
249
+ assert asyncio.get_running_loop().time() - started < 30, "not ready"
250
+ await asyncio.sleep(0.1)
251
+ yield {
252
+ "http": client,
253
+ "issuer": issuer,
254
+ "at_concierge": issuer.login(user, audience="concierge"),
255
+ "at_orders": issuer.login(user, audience="orders"),
256
+ }
257
+ reset_policy_cache()
258
+ reset_api_policy()
259
+ reset_a2a_client()
260
+
261
+
262
+ @pytest.fixture
263
+ async def relayed(
264
+ database: str,
265
+ issuer: FakeIssuer,
266
+ tmp_path: Path,
267
+ monkeypatch: pytest.MonkeyPatch,
268
+ use_test_tools: Any,
269
+ ) -> AsyncIterator[dict[str, Any]]:
270
+ """orders' cancel gate relays the concierge's decisions (`decide_with: relayed`)."""
271
+ async for agents in _agents(issuer, tmp_path, monkeypatch, use_test_tools, RELAYED):
272
+ yield {**agents, "database": database}
273
+
274
+
275
+ @pytest.fixture
276
+ async def direct(
277
+ database: str,
278
+ issuer: FakeIssuer,
279
+ tmp_path: Path,
280
+ monkeypatch: pytest.MonkeyPatch,
281
+ use_test_tools: Any,
282
+ ) -> AsyncIterator[dict[str, Any]]:
283
+ """orders' cancel gate at the default, `decide_with: direct`."""
284
+ async for agents in _agents(issuer, tmp_path, monkeypatch, use_test_tools, ""):
285
+ yield agents
286
+
287
+
288
+ def _events(text: str) -> list[tuple[str, dict[str, Any]]]:
289
+ events = []
290
+ for block in text.split("\n\n"):
291
+ lines = dict(line.split(": ", 1) for line in block.splitlines() if ": " in line)
292
+ if "event" in lines and "data" in lines:
293
+ events.append((lines["event"], json.loads(lines["data"])))
294
+ return events
295
+
296
+
297
+ async def _chat(agents: dict[str, Any], message: str, thread_id: str | None = None) -> Any:
298
+ body: dict[str, Any] = {"message": message}
299
+ if thread_id:
300
+ body["thread_id"] = thread_id
301
+ r = await agents["http"].post(
302
+ "/chat", json=body, headers={"Authorization": f"Bearer {agents['at_concierge']}"}
303
+ )
304
+ assert r.status_code == 200, r.text
305
+ return _events(r.text)
306
+
307
+
308
+ def _end(events: list[tuple[str, dict[str, Any]]]) -> dict[str, Any]:
309
+ ends = [data for event, data in events if event == "message.end"]
310
+ assert ends, events
311
+ return ends[-1]
312
+
313
+
314
+ def _results(events: list[tuple[str, dict[str, Any]]]) -> str:
315
+ return json.dumps([data for event, data in events if event == "tool.result"])
316
+
317
+
318
+ async def _ask(
319
+ agents: dict[str, Any], status: str, message: str = "Ask orders about cancelling the order"
320
+ ) -> tuple[str, str]:
321
+ """alice asks the concierge to cancel; orders waits for approval. (thread, orders task)"""
322
+ events = await _chat(agents, message)
323
+ end = _end(events)
324
+ assert end["status"] == "ok", events
325
+ assert f'\\"status\\": \\"{status}\\"' in _results(events), _results(events)
326
+ found = re.search(r'\\"task_id\\": \\"([0-9a-f-]+)\\"', _results(events))
327
+ assert found, _results(events)
328
+ assert SENT == []
329
+ return end["thread_id"], found.group(1)
330
+
331
+
332
+ async def test_the_concierge_relays_the_persons_approval(relayed: dict[str, Any]) -> None:
333
+ agents = relayed
334
+ thread, task_id = await _ask(agents, "needs_user_approval")
335
+ # alice asks the concierge to approve: it pauses for her, showing what happens at orders.
336
+ paused = _end(await _chat(agents, f"approve_agent_action for {task_id}", thread))
337
+ assert paused["status"] == "awaiting_approval"
338
+ approval = paused["approval"]
339
+ assert (approval["api"], approval["a2a_operation"]) == ("orders_agent", "approve")
340
+ effect = approval["effect"]
341
+ assert (effect["agent"], effect["via"], effect["method"], effect["path"]) == (
342
+ "orders",
343
+ ["orders"],
344
+ "POST",
345
+ "/orders/1/cancel",
346
+ )
347
+ assert approval["nested"]["decide_with"] == "relayed" and SENT == []
348
+ # Exactly what orders will send (read from its approval_json, not the protobuf Struct,
349
+ # whose numbers are doubles).
350
+ assert effect["body"] == CANCEL_BODY and approval["nested"]["call"]["body"] == CANCEL_BODY
351
+ # She approves: the decision reaches orders once, and the order is cancelled once.
352
+ r = await agents["http"].post(
353
+ f"/threads/{thread}/approvals/{approval['approval_id']}",
354
+ json={"decision": "approve"},
355
+ headers={"Authorization": f"Bearer {agents['at_concierge']}"},
356
+ )
357
+ assert r.status_code == 200, r.text
358
+ resumed = _events(r.text)
359
+ assert _end(resumed)["status"] == "ok", resumed
360
+ assert "cancelled" in _results(resumed)
361
+ assert [(s.method, s.url.path) for s in SENT] == [("POST", "/orders/1/cancel")]
362
+ # At orders (alice's own token): its approval names the concierge as the relayer, and the
363
+ # task that waited follows the run the decision resumed.
364
+ subject = jwt.decode(agents["at_orders"], options={"verify_signature": False})["sub"]
365
+ context = context_id_for(thread, "orders_agent", subject)
366
+ mine = {"Authorization": f"Bearer {agents['at_orders']}"}
367
+ [record] = (await agents["http"].get(f"/threads/{context}/approvals", headers=mine)).json()
368
+ assert (record["status"], record["decided_via"], record["requester_actor"]) == (
369
+ "approved",
370
+ "concierge",
371
+ "concierge",
372
+ )
373
+ got = await agents["http"].post(
374
+ A2A_PATH,
375
+ json={"jsonrpc": "2.0", "id": "1", "method": "GetTask", "params": {"id": task_id}},
376
+ headers={**mine, "A2A-Version": "1.0"},
377
+ )
378
+ waited = got.json()["result"]
379
+ assert waited["status"]["state"] == "TASK_STATE_COMPLETED"
380
+ assert waited["status"]["message"]["parts"][0]["text"].startswith("Continued in task ")
381
+ # Exchanged once for the pair (alice, orders), then reused (the card, the ask, the reads
382
+ # and the decision all went out with it).
383
+ exchanges = [r for r in agents["issuer"].requests if r["form"].get("audience") == "orders"]
384
+ assert len(exchanges) == 1
385
+
386
+
387
+ async def _approve_relayed(agents: dict[str, Any], thread: str, task_id: str) -> list[Any]:
388
+ """alice has the concierge relay orders' approval, and approves it there: the resumed run."""
389
+ paused = _end(await _chat(agents, f"approve_agent_action for {task_id}", thread))
390
+ assert paused["status"] == "awaiting_approval", paused
391
+ r = await agents["http"].post(
392
+ f"/threads/{thread}/approvals/{paused['approval']['approval_id']}",
393
+ json={"decision": "approve"},
394
+ headers={"Authorization": f"Bearer {agents['at_concierge']}"},
395
+ )
396
+ assert r.status_code == 200, r.text
397
+ return _events(r.text)
398
+
399
+
400
+ async def test_the_run_alice_approves_checks_the_words_she_asked_with(
401
+ relayed: dict[str, Any],
402
+ ) -> None:
403
+ """orders cancels only an order the user's own words name (`require_user_mentioned`).
404
+ alice's words reach it with the request; when she approves at the concierge, having
405
+ said "approve_agent_action for <task>" there, the run the relayed decision resumes at
406
+ orders checks the words she asked with, and the order is cancelled once."""
407
+ agents = relayed
408
+ CHECK_MENTION.append(True)
409
+ thread, task_id = await _ask(
410
+ agents, "needs_user_approval", "Ask orders about cancelling order 1"
411
+ )
412
+ resumed = await _approve_relayed(agents, thread, task_id)
413
+ assert _end(resumed)["status"] == "ok", resumed
414
+ assert "refused" not in _results(resumed), _results(resumed)
415
+ assert [(s.method, s.url.path) for s in SENT] == [("POST", "/orders/1/cancel")]
416
+
417
+
418
+ async def test_when_orders_lost_the_task_the_relay_reads_its_ledger_and_sends_once(
419
+ relayed: dict[str, Any], monkeypatch: pytest.MonkeyPatch
420
+ ) -> None:
421
+ """orders loses the task between the relay and alice's decision (A2A_TASK_TTL_S is far
422
+ shorter than an approval may wait): the resumed relay reads what orders waits on from
423
+ its approvals ledger instead, which gives the times the task gave, whatever the
424
+ database's time zone (America/New_York here), so the decision is the one alice approved
425
+ and the order is cancelled once."""
426
+ agents = relayed
427
+ thread, task_id = await _ask(agents, "needs_user_approval")
428
+ paused = _end(await _chat(agents, f"approve_agent_action for {task_id}", thread))
429
+ assert paused["status"] == "awaiting_approval", paused
430
+ if agents["database"] == "postgres":
431
+ import psycopg
432
+
433
+ async with await psycopg.AsyncConnection.connect(os.environ["POSTGRES_DSN"]) as conn:
434
+ cursor = await conn.execute("DELETE FROM a2a_tasks WHERE task_id = %s", (task_id,))
435
+ assert cursor.rowcount == 1
436
+ await conn.commit()
437
+ else:
438
+ # The tasks in process memory: past A2A_TASK_TTL_S, as if the hour went by.
439
+ for store in a2a_module._STORES:
440
+ later = time.monotonic() + store.memory.ttl_s + 1
441
+ monkeypatch.setattr(store.memory, "_clock", lambda later=later: later)
442
+ gone = await agents["http"].post( # alice's own token reads her agents' tasks
443
+ A2A_PATH,
444
+ json={"jsonrpc": "2.0", "id": "1", "method": "GetTask", "params": {"id": task_id}},
445
+ headers={"Authorization": f"Bearer {agents['at_orders']}", "A2A-Version": "1.0"},
446
+ )
447
+ assert gone.json()["error"]["code"] == -32001, gone.text
448
+ r = await agents["http"].post(
449
+ f"/threads/{thread}/approvals/{paused['approval']['approval_id']}",
450
+ json={"decision": "approve"},
451
+ headers={"Authorization": f"Bearer {agents['at_concierge']}"},
452
+ )
453
+ assert r.status_code == 200, r.text
454
+ resumed = _events(r.text)
455
+ assert "differs from the request that was approved" not in _results(resumed), resumed
456
+ assert [(s.method, s.url.path) for s in SENT] == [("POST", "/orders/1/cancel")]
457
+
458
+
459
+ async def test_under_direct_the_concierge_relays_nothing_and_alice_approves_at_orders(
460
+ direct: dict[str, Any],
461
+ ) -> None:
462
+ agents = direct
463
+ thread, task_id = await _ask(agents, "needs_direct_approval")
464
+ events = await _chat(agents, f"approve_agent_action for {task_id}", thread)
465
+ assert _end(events)["status"] == "ok" # no approval asked at the concierge
466
+ result = _results(events)
467
+ assert '\\"status\\": \\"needs_direct_approval\\"' in result
468
+ assert "/orders/1/cancel" in result and SENT == []
469
+ subject = jwt.decode(agents["at_orders"], options={"verify_signature": False})["sub"]
470
+ context = context_id_for(thread, "orders_agent", subject)
471
+ mine = {"Authorization": f"Bearer {agents['at_orders']}"}
472
+ [record] = (await agents["http"].get(f"/threads/{context}/approvals", headers=mine)).json()
473
+ r = await agents["http"].post(
474
+ f"/threads/{context}/approvals/{record['approval_id']}",
475
+ json={"decision": "approve"},
476
+ headers=mine,
477
+ )
478
+ assert r.status_code == 200, r.text
479
+ assert [(s.method, s.url.path) for s in SENT] == [("POST", "/orders/1/cancel")]