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,702 @@
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
+ """The LangGraph Server authorization rules (`auth.build_sdk_auth`), without a server.
16
+
17
+ Handlers are resolved with the server's documented precedence (resource and
18
+ action, then resource, then action, then the global handler).
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import asyncio
24
+ from collections.abc import Iterator
25
+ from types import SimpleNamespace
26
+ from typing import Any
27
+
28
+ import httpx
29
+ import pytest
30
+ from langgraph_sdk import Auth
31
+ from starlette.requests import Request
32
+
33
+ from {{cookiecutter.agent_directory}}.app_utils.auth import (
34
+ Principal,
35
+ build_sdk_auth,
36
+ reset_policy_cache,
37
+ )
38
+ from {{cookiecutter.agent_directory}}.app_utils.middleware import AuthErrorMiddleware
39
+
40
+ RESOURCE_ACTIONS = {
41
+ "threads": ("create", "read", "update", "delete", "search", "create_run"),
42
+ "assistants": ("create", "read", "update", "delete", "search"),
43
+ "crons": ("create", "read", "update", "delete", "search"),
44
+ "store": ("put", "get", "search", "list_namespaces", "delete"),
45
+ }
46
+ READS = {
47
+ ("assistants", "read"),
48
+ ("assistants", "search"),
49
+ ("crons", "read"),
50
+ ("crons", "search"),
51
+ ("store", "get"),
52
+ ("store", "search"),
53
+ ("store", "list_namespaces"),
54
+ }
55
+ WRITES = {
56
+ (resource, action)
57
+ for resource in ("assistants", "crons", "store")
58
+ for action in RESOURCE_ACTIONS[resource]
59
+ if (resource, action) not in READS
60
+ }
61
+
62
+
63
+ SOURCE = "11111111-1111-1111-1111-111111111111"
64
+ COPY = "22222222-2222-2222-2222-222222222222"
65
+ INPUT = {"messages": [{"role": "user", "content": "hello"}]}
66
+
67
+
68
+ def _no_approvals(monkeypatch: pytest.MonkeyPatch, refusal: str | None = None) -> list[str]:
69
+ """The chat runtime as the hook sees it: no pending approval; `refusal` for a replay
70
+ or a copy. Returns the threads whose replay or copy was checked.
71
+ """
72
+ from {{cookiecutter.agent_directory}}.app_utils.chat import RUNTIME
73
+
74
+ checked: list[str] = []
75
+
76
+ async def pending(thread_id: str) -> list[Any]:
77
+ return []
78
+
79
+ async def replay_refusal(thread_id: str) -> str | None:
80
+ checked.append(thread_id)
81
+ return refusal
82
+
83
+ monkeypatch.setattr(RUNTIME, "pending_approvals", pending)
84
+ monkeypatch.setattr(RUNTIME, "replay_refusal", replay_refusal)
85
+ monkeypatch.setattr(RUNTIME, "copy_refusal", replay_refusal)
86
+ return checked
87
+
88
+
89
+ @pytest.fixture
90
+ def auth(monkeypatch: pytest.MonkeyPatch) -> Iterator[Any]:
91
+ monkeypatch.setenv("AUTH_POLICY", "shared-bearer")
92
+ monkeypatch.setenv("API_KEY", "k")
93
+ monkeypatch.delenv("PRINCIPAL_HASH_SALT", raising=False)
94
+ monkeypatch.setenv("AUTH_ADMIN_ROLES", "admin, platform")
95
+ monkeypatch.setenv("AUTH_READ_ACROSS_ROLES", "support")
96
+ monkeypatch.setenv("APP_ENV", "prod")
97
+ reset_policy_cache()
98
+ yield build_sdk_auth()
99
+ reset_policy_cache()
100
+
101
+
102
+ def _user(identity: str, *roles: str) -> Any:
103
+ return SimpleNamespace(
104
+ identity=identity,
105
+ display_name=identity,
106
+ is_authenticated=True,
107
+ permissions=["chat.send", *(f"role:{r}" for r in roles)],
108
+ )
109
+
110
+
111
+ def _http(method: str, path: str, authorization: str | None = "Bearer k") -> Request:
112
+ headers = [(b"authorization", authorization.encode())] if authorization else []
113
+ return Request(
114
+ {"type": "http", "method": method, "path": path, "headers": headers, "query_string": b""}
115
+ )
116
+
117
+
118
+ async def _authenticate(auth: Any, method: str, path: str) -> None:
119
+ """What the server does first for every native API request (per-request state included)."""
120
+ await auth._authenticate_handler(request=_http(method, path))
121
+
122
+
123
+ def _handler(auth: Any, resource: str, action: str) -> Any:
124
+ for key in ((resource, action), (resource, "*"), ("*", action), ("*", "*")):
125
+ if key in auth._handlers:
126
+ return auth._handlers[key][-1]
127
+ return auth._global_handlers[-1] if auth._global_handlers else None
128
+
129
+
130
+ async def _dispatch(auth: Any, user: Any, resource: str, action: str, value: Any = None) -> Any:
131
+ ctx = Auth.types.AuthContext(
132
+ user=user, permissions=list(user.permissions), resource=resource, action=action
133
+ )
134
+ handler = _handler(auth, resource, action)
135
+ assert handler is not None, f"{resource}.{action} would be allowed without a rule"
136
+ return await handler(ctx=ctx, value={} if value is None else value)
137
+
138
+
139
+ def _sdk_resource_actions(auth: Any) -> set[tuple[str, str]]:
140
+ """Every (resource, action) the installed SDK can register a handler for.
141
+
142
+ The same walk LangGraph Server does at startup to warn about paths that
143
+ would be allowed without a rule, so a newer SDK action shows up here.
144
+ """
145
+ pairs: set[tuple[str, str]] = set()
146
+ for slot in getattr(type(auth.on), "__slots__", ()) or ():
147
+ namespace = getattr(auth.on, slot, None)
148
+ if slot.startswith("_") or namespace is None:
149
+ continue
150
+ for name in dir(namespace):
151
+ action = getattr(getattr(namespace, name, None), "action", None)
152
+ if not name.startswith("_") and isinstance(action, str):
153
+ pairs.add((slot, action))
154
+ return pairs
155
+
156
+
157
+ def test_every_dispatch_path_has_a_rule_and_the_rest_is_denied(auth: Any) -> None:
158
+ assert auth._global_handlers, "no default-deny handler"
159
+ known = {(r, a) for r, actions in RESOURCE_ACTIONS.items() for a in actions}
160
+ assert known <= _sdk_resource_actions(auth)
161
+ for resource, action in known:
162
+ assert (resource, action) in auth._handlers, f"{resource}.{action}"
163
+ # A pair a newer SDK adds falls to the default-deny handler (checked below).
164
+
165
+
166
+ @pytest.mark.parametrize(("resource", "action"), sorted(READS))
167
+ async def test_reads_are_open_to_every_authenticated_principal(
168
+ auth: Any, resource: str, action: str
169
+ ) -> None:
170
+ assert await _dispatch(auth, _user("bob"), resource, action) is True
171
+
172
+
173
+ @pytest.mark.parametrize(("resource", "action"), sorted(WRITES))
174
+ async def test_writes_need_an_admin_role(auth: Any, resource: str, action: str) -> None:
175
+ with pytest.raises(Auth.exceptions.HTTPException) as exc:
176
+ await _dispatch(auth, _user("bob", "support"), resource, action)
177
+ assert exc.value.status_code == 403 and "AUTH_ADMIN_ROLES" in str(exc.value.detail)
178
+ assert await _dispatch(auth, _user("carol", "admin"), resource, action) is True
179
+ assert await _dispatch(auth, _user("dan", "viewer", "platform"), resource, action) is True
180
+
181
+
182
+ async def test_no_admin_roles_means_nobody_may_change_shared_resources(
183
+ auth: Any, monkeypatch: pytest.MonkeyPatch
184
+ ) -> None:
185
+ monkeypatch.setenv("AUTH_ADMIN_ROLES", "")
186
+ with pytest.raises(Auth.exceptions.HTTPException):
187
+ await _dispatch(auth, _user("carol", "admin"), "assistants", "update")
188
+
189
+
190
+ async def test_anything_without_a_rule_is_denied_even_for_admins(auth: Any) -> None:
191
+ for resource, action in (("runs", "read"), ("threads", "prune"), ("profiles", "create")):
192
+ with pytest.raises(Auth.exceptions.HTTPException) as exc:
193
+ await _dispatch(auth, _user("carol", "admin"), resource, action)
194
+ assert exc.value.status_code == 403
195
+
196
+
197
+ async def test_studio_user_is_trusted_only_under_dev(
198
+ auth: Any, monkeypatch: pytest.MonkeyPatch
199
+ ) -> None:
200
+ studio = Auth.types.StudioUser("langgraph-studio-user")
201
+ with pytest.raises(Auth.exceptions.HTTPException):
202
+ await _dispatch(auth, studio, "assistants", "update")
203
+ monkeypatch.setenv("APP_ENV", "dev")
204
+ assert await _dispatch(auth, studio, "assistants", "update") is True
205
+
206
+
207
+ async def test_thread_rules_are_owner_scoped(auth: Any) -> None:
208
+ alice, support = _user("alice"), _user("sam", "support")
209
+ value: dict[str, Any] = {"metadata": {"principal_id": "mallory", "topic": "x"}}
210
+ assert await _dispatch(auth, alice, "threads", "create", value) == {"principal_id": "alice"}
211
+ assert value["metadata"] == {"principal_id": "alice", "topic": "x", "tenant": None}
212
+ for action in ("read", "search"):
213
+ assert await _dispatch(auth, alice, "threads", action) == {"principal_id": "alice"}
214
+ assert await _dispatch(auth, support, "threads", action) is None # read-across
215
+ for action in ("update", "delete", "create_run"):
216
+ # Read-across never extends to changes.
217
+ assert await _dispatch(auth, support, "threads", action) == {"principal_id": "sam"}
218
+ run: dict[str, Any] = {"metadata": {"principal_id": "mallory", "tenant": "acme"}}
219
+ await _dispatch(auth, alice, "threads", "create_run", run)
220
+ assert run["metadata"] == {"principal_id": "alice", "tenant": None}
221
+ # A native caller cannot vouch for a tenant.
222
+ claimed: dict[str, Any] = {"metadata": {"tenant": "acme"}}
223
+ await _dispatch(auth, alice, "threads", "create", claimed)
224
+ assert claimed["metadata"] == {"principal_id": "alice", "tenant": None}
225
+ # Ownership that cannot be stamped is refused, not skipped.
226
+ for action in ("create", "create_run"):
227
+ with pytest.raises(Auth.exceptions.HTTPException) as exc:
228
+ await _dispatch(auth, alice, "threads", action, {"metadata": None})
229
+ assert exc.value.status_code == 403
230
+
231
+
232
+ async def test_an_owner_cannot_hand_a_thread_to_another_principal(auth: Any) -> None:
233
+ value: dict[str, Any] = {
234
+ "thread_id": "t1",
235
+ "metadata": {"principal_id": "bob", "tenant": "t-bob", "topic": "kept"},
236
+ }
237
+ result = await _dispatch(auth, _user("alice"), "threads", "update", value)
238
+ assert result == {"principal_id": "alice"}
239
+ assert value["metadata"] == {"principal_id": "alice", "topic": "kept"}
240
+ # An update without metadata (for example a run cancellation) passes through.
241
+ assert await _dispatch(auth, _user("alice"), "threads", "update", {"thread_id": "t1"}) == {
242
+ "principal_id": "alice"
243
+ }
244
+
245
+
246
+ async def test_a_copy_is_a_write_even_for_read_across_roles(
247
+ auth: Any, monkeypatch: pytest.MonkeyPatch
248
+ ) -> None:
249
+ """The server's copy reads its source with the read filter, then authorizes a create
250
+ without metadata (the copy keeps the source's metadata, owner included)."""
251
+ _no_approvals(monkeypatch)
252
+ support = _user("sam", "support")
253
+
254
+ async def copy_as(user: Any) -> Any:
255
+ await _authenticate(auth, "POST", f"/threads/{SOURCE}/copy")
256
+ source_filter = await _dispatch(auth, user, "threads", "read", {"thread_id": SOURCE})
257
+ await _dispatch(auth, user, "threads", "create", {"thread_id": COPY})
258
+ return source_filter
259
+
260
+ # The source is limited to the caller's own threads: alice's is "not found" to sam.
261
+ assert await copy_as(support) == {"principal_id": "sam"}
262
+ assert await copy_as(_user("alice")) == {"principal_id": "alice"}
263
+ # Everywhere else sam still reads across.
264
+ await _authenticate(auth, "GET", f"/threads/{SOURCE}")
265
+ assert await _dispatch(auth, support, "threads", "read", {"thread_id": SOURCE}) is None
266
+ await _authenticate(auth, "POST", "/threads/search")
267
+ assert await _dispatch(auth, support, "threads", "search", {}) is None
268
+ # A metadata-less create the handler cannot tell apart from a copy fails
269
+ # closed for read-across roles (the source filter may have been lifted).
270
+ await _authenticate(auth, "POST", f"/threads/{SOURCE}/fork")
271
+ with pytest.raises(Auth.exceptions.HTTPException) as exc:
272
+ await _dispatch(auth, support, "threads", "create", {"thread_id": COPY})
273
+ assert exc.value.status_code == 403
274
+ assert await _dispatch(auth, _user("bob"), "threads", "create", {"thread_id": COPY}) == {
275
+ "principal_id": "bob"
276
+ }
277
+
278
+
279
+ async def test_the_copy_flag_is_per_request(auth: Any, monkeypatch: pytest.MonkeyPatch) -> None:
280
+ """Concurrent requests (separate tasks, like the server's) never see each other's flag."""
281
+ _no_approvals(monkeypatch)
282
+ support = _user("sam", "support")
283
+ gate = asyncio.Event()
284
+
285
+ async def copying() -> Any:
286
+ await _authenticate(auth, "POST", f"/threads/{SOURCE}/copy")
287
+ await gate.wait()
288
+ return await _dispatch(auth, support, "threads", "read", {"thread_id": SOURCE})
289
+
290
+ async def reading() -> Any:
291
+ await _authenticate(auth, "GET", f"/threads/{SOURCE}")
292
+ gate.set()
293
+ return await _dispatch(auth, support, "threads", "read", {"thread_id": SOURCE})
294
+
295
+ assert await asyncio.gather(copying(), reading()) == [{"principal_id": "sam"}, None]
296
+
297
+
298
+ async def test_a_thread_with_gated_calls_is_not_copied(
299
+ auth: Any, monkeypatch: pytest.MonkeyPatch
300
+ ) -> None:
301
+ """A copy would carry the thread's tool calls without their approvals."""
302
+ checked = _no_approvals(monkeypatch, "the thread has approvals of gated API calls")
303
+ await _authenticate(auth, "POST", f"/threads/{SOURCE}/copy")
304
+ with pytest.raises(Auth.exceptions.HTTPException) as exc:
305
+ await _dispatch(auth, _user("alice"), "threads", "read", {"thread_id": SOURCE})
306
+ assert exc.value.status_code == 403
307
+ assert "cannot be copied: the thread has approvals of gated API calls" in str(exc.value.detail)
308
+ # Reading it is not copying it.
309
+ await _authenticate(auth, "GET", f"/threads/{SOURCE}")
310
+ assert await _dispatch(auth, _user("alice"), "threads", "read", {"thread_id": SOURCE}) == {
311
+ "principal_id": "alice"
312
+ }
313
+ assert checked == [SOURCE]
314
+
315
+
316
+ async def test_native_runs_on_existing_threads_carry_the_hashed_owner_id(
317
+ auth: Any, monkeypatch: pytest.MonkeyPatch
318
+ ) -> None:
319
+ monkeypatch.setenv("PRINCIPAL_HASH_SALT", "pepper")
320
+ _no_approvals(monkeypatch)
321
+ alice = _user("alice@example.com")
322
+ hashed = Principal(id="alice@example.com").hashed_id()
323
+ run: dict[str, Any] = {
324
+ "thread_id": SOURCE,
325
+ "if_not_exists": "reject",
326
+ "metadata": {},
327
+ "kwargs": {"input": INPUT},
328
+ }
329
+ assert await _dispatch(auth, alice, "threads", "create_run", run) == {
330
+ "principal_id": "alice@example.com" # the filter still checks the thread's raw stamp
331
+ }
332
+ assert run["metadata"] == {"principal_id": hashed, "tenant": None}
333
+ # A run that may create its thread stamps the raw id: it becomes the thread's owner.
334
+ for creating in (
335
+ {
336
+ "thread_id": SOURCE,
337
+ "if_not_exists": "create",
338
+ "metadata": {},
339
+ "kwargs": {"input": INPUT},
340
+ },
341
+ {"thread_id": None, "metadata": {"principal_id": "mallory"}},
342
+ ):
343
+ await _dispatch(auth, alice, "threads", "create_run", creating)
344
+ assert creating["metadata"]["principal_id"] == "alice@example.com"
345
+
346
+
347
+ @pytest.mark.parametrize(
348
+ "kwargs",
349
+ [
350
+ {"input": None},
351
+ {},
352
+ {"input": INPUT, "config": {"configurable": {"checkpoint_id": "cp-1"}}},
353
+ {"input": INPUT, "config": {"configurable": {"checkpoint": {"checkpoint_id": "cp-1"}}}},
354
+ {"input": None, "context": {"checkpoint_id": "cp-1"}},
355
+ {"input": INPUT, "config": {"configurable": {"checkpoint_map": {"": "cp-1"}}}},
356
+ ],
357
+ ids=["input null", "no input", "checkpoint_id", "checkpoint", "context", "checkpoint_map"],
358
+ )
359
+ async def test_a_native_run_that_reruns_a_paused_step_is_refused_where_calls_were_gated(
360
+ auth: Any, monkeypatch: pytest.MonkeyPatch, kwargs: dict[str, Any]
361
+ ) -> None:
362
+ """Continued without input, or replayed from a checkpoint, a step runs its tool calls again."""
363
+ checked = _no_approvals(monkeypatch, "the thread has approvals of gated API calls")
364
+ run: dict[str, Any] = {"thread_id": SOURCE, "metadata": {}, "kwargs": kwargs}
365
+ with pytest.raises(Auth.exceptions.HTTPException) as exc:
366
+ await _dispatch(auth, _user("alice"), "threads", "create_run", run)
367
+ assert exc.value.status_code == 403
368
+ assert "the thread has approvals of gated API calls" in str(exc.value.detail)
369
+ assert checked == [SOURCE]
370
+
371
+
372
+ async def test_a_native_run_with_input_or_on_a_thread_without_approvals_goes_ahead(
373
+ auth: Any, monkeypatch: pytest.MonkeyPatch
374
+ ) -> None:
375
+ checked = _no_approvals(monkeypatch, "the thread has approvals of gated API calls")
376
+ run: dict[str, Any] = {
377
+ "thread_id": SOURCE,
378
+ "metadata": {},
379
+ "kwargs": {"input": INPUT, "config": {"configurable": {"checkpoint_ns": ""}}},
380
+ }
381
+ assert await _dispatch(auth, _user("alice"), "threads", "create_run", run) == {
382
+ "principal_id": "alice"
383
+ }
384
+ assert checked == [] # a new message is not a replay: not even checked
385
+ checked = _no_approvals(monkeypatch, None)
386
+ replay: dict[str, Any] = {"thread_id": SOURCE, "metadata": {}, "kwargs": {"input": None}}
387
+ assert await _dispatch(auth, _user("alice"), "threads", "create_run", replay) == {
388
+ "principal_id": "alice"
389
+ }
390
+ assert checked == [SOURCE]
391
+
392
+
393
+ async def test_the_native_api_gets_the_policys_challenge_and_status(
394
+ auth: Any, monkeypatch: pytest.MonkeyPatch
395
+ ) -> None:
396
+ """LangGraph Server keeps only the detail of a 401 and turns a 503 into a 500.
397
+
398
+ The fake below behaves like its auth middleware: it calls the authenticate
399
+ handler, answers 401/403 bare and lets any other status escape.
400
+ """
401
+
402
+ async def server(scope: Any, receive: Any, send: Any) -> None:
403
+ try:
404
+ await auth._authenticate_handler(request=Request(scope, receive))
405
+ except Auth.exceptions.HTTPException as exc:
406
+ if exc.status_code not in (401, 403):
407
+ raise
408
+ await send({"type": "http.response.start", "status": exc.status_code, "headers": []})
409
+ await send({"type": "http.response.body", "body": b'{"detail": "bare"}'})
410
+ return
411
+ await send({"type": "http.response.start", "status": 200, "headers": []})
412
+ await send({"type": "http.response.body", "body": b"{}"})
413
+
414
+ client = httpx.AsyncClient(
415
+ transport=httpx.ASGITransport(app=AuthErrorMiddleware(server)), base_url="http://server"
416
+ )
417
+ async with client:
418
+ r = await client.get("/assistants/search", headers={"Authorization": "Bearer wrong"})
419
+ assert r.status_code == 401 and r.headers["www-authenticate"] == "Bearer"
420
+ r = await client.get("/assistants/search", headers={"Authorization": "Bearer k"})
421
+ assert r.status_code == 200 and "www-authenticate" not in r.headers
422
+ monkeypatch.delenv("API_KEY") # a misconfigured policy answers 503, not 500
423
+ r = await client.get("/assistants/search", headers={"Authorization": "Bearer k"})
424
+ assert r.status_code == 503 and "API_KEY is not configured" in r.json()["detail"]
425
+
426
+
427
+ async def test_the_auth_error_middleware_leaves_other_errors_alone() -> None:
428
+ async def failing(scope: Any, receive: Any, send: Any) -> None:
429
+ raise RuntimeError("not an auth error")
430
+
431
+ async def late(scope: Any, receive: Any, send: Any) -> None:
432
+ await send({"type": "http.response.start", "status": 200, "headers": []})
433
+ raise Auth.exceptions.HTTPException(status_code=503)
434
+
435
+ for app in (failing, late):
436
+ transport = httpx.ASGITransport(app=AuthErrorMiddleware(app))
437
+ async with httpx.AsyncClient(transport=transport, base_url="http://server") as client:
438
+ with pytest.raises((RuntimeError, Auth.exceptions.HTTPException)):
439
+ await client.get("/threads")
440
+
441
+
442
+ # --- agents calling for a user (0.3): the `actor:` permission -------------------------------
443
+
444
+
445
+ def _agent(identity: str, actor: str, *roles: str) -> Any:
446
+ user = _user(identity, *roles)
447
+ user.permissions.append(f"actor:{actor}")
448
+ return user
449
+
450
+
451
+ async def test_a_delegated_callers_filters_name_its_actor(auth: Any) -> None:
452
+ concierge = _agent("alice", "concierge")
453
+ both = {"principal_id": "alice", "actor": "concierge"}
454
+ for action in ("read", "search", "update", "delete", "create_run"):
455
+ assert await _dispatch(auth, concierge, "threads", action) == both
456
+ # The person's own (direct) filter is a containment filter: it matches the threads its
457
+ # agents started for it too, while an agent's never matches a thread without `actor`
458
+ # (a direct one, or one created before 0.3).
459
+ for action in ("read", "search", "update", "delete", "create_run"):
460
+ assert await _dispatch(auth, _user("alice"), "threads", action) == {"principal_id": "alice"}
461
+
462
+
463
+ async def test_threads_are_stamped_with_the_actor_and_nobody_chooses_it(auth: Any) -> None:
464
+ created: dict[str, Any] = {"metadata": {"actor": "billing", "topic": "x"}}
465
+ await _dispatch(auth, _agent("alice", "concierge"), "threads", "create", created)
466
+ assert created["metadata"] == {
467
+ "actor": "concierge",
468
+ "topic": "x",
469
+ "principal_id": "alice",
470
+ "tenant": None,
471
+ }
472
+ # A direct caller cannot create a thread that looks like an agent's.
473
+ forged: dict[str, Any] = {"metadata": {"actor": "concierge"}}
474
+ await _dispatch(auth, _user("alice"), "threads", "create", forged)
475
+ assert forged["metadata"] == {"principal_id": "alice", "tenant": None}
476
+ run: dict[str, Any] = {"metadata": {"actor": "billing"}}
477
+ await _dispatch(auth, _agent("alice", "concierge"), "threads", "create_run", run)
478
+ assert run["metadata"]["actor"] == "concierge"
479
+ direct_run: dict[str, Any] = {"metadata": {"actor": "concierge"}}
480
+ await _dispatch(auth, _user("alice"), "threads", "create_run", direct_run)
481
+ assert "actor" not in direct_run["metadata"]
482
+
483
+
484
+ async def test_an_update_cannot_adopt_or_strip_an_actor(auth: Any) -> None:
485
+ # The person (direct) updating its agent's thread: the thread keeps its actor (the
486
+ # server merges the update's metadata, and the update carries none).
487
+ by_person: dict[str, Any] = {"thread_id": "t1", "metadata": {"actor": "", "topic": "y"}}
488
+ await _dispatch(auth, _user("alice"), "threads", "update", by_person)
489
+ assert by_person["metadata"] == {"principal_id": "alice", "topic": "y"}
490
+ # An agent cannot move a thread to another agent.
491
+ by_agent: dict[str, Any] = {"thread_id": "t1", "metadata": {"actor": "billing"}}
492
+ result = await _dispatch(auth, _agent("alice", "concierge"), "threads", "update", by_agent)
493
+ assert result == {"principal_id": "alice", "actor": "concierge"}
494
+ assert by_agent["metadata"] == {"principal_id": "alice", "actor": "concierge"}
495
+
496
+
497
+ async def test_a_delegated_callers_roles_never_read_across_or_administer(auth: Any) -> None:
498
+ agent = _agent("alice", "concierge", "support", "admin")
499
+ for action in ("read", "search"):
500
+ assert await _dispatch(auth, agent, "threads", action) == {
501
+ "principal_id": "alice",
502
+ "actor": "concierge",
503
+ }
504
+ with pytest.raises(Auth.exceptions.HTTPException) as exc:
505
+ await _dispatch(auth, agent, "assistants", "update")
506
+ assert exc.value.status_code == 403
507
+
508
+
509
+ async def test_the_authenticate_handler_publishes_only_the_policys_actor(
510
+ auth: Any, monkeypatch: pytest.MonkeyPatch
511
+ ) -> None:
512
+ from {{cookiecutter.agent_directory}}.app_utils import auth as auth_module
513
+ from {{cookiecutter.agent_directory}}.app_utils.auth import ACTIONS, Actor
514
+
515
+ monkeypatch.setenv("AUTH_ALLOWED_ACTORS", "concierge")
516
+
517
+ class _Delegating:
518
+ principal = Principal(
519
+ id="alice",
520
+ permissions={*ACTIONS, "actor:billing"}, # a policy's own actor: permission
521
+ actor=Actor(id="concierge"),
522
+ )
523
+
524
+ async def authenticate(self, request: Any) -> Principal:
525
+ return self.principal
526
+
527
+ async def authorize(self, *args: Any) -> None:
528
+ return None
529
+
530
+ monkeypatch.setattr(auth_module, "get_policy", lambda: _Delegating())
531
+ user = await auth._authenticate_handler(request=_http("GET", "/threads"))
532
+ assert user["identity"] == "alice"
533
+ assert [p for p in user["permissions"] if p.startswith("actor:")] == ["actor:concierge"]
534
+ monkeypatch.setenv("AUTH_ALLOWED_ACTORS", "")
535
+ with pytest.raises(Auth.exceptions.HTTPException) as exc:
536
+ await auth._authenticate_handler(request=_http("GET", "/threads"))
537
+ assert exc.value.status_code == 403
538
+
539
+
540
+ # --- a native run's context: the caller's own, whatever the request sends ----------------
541
+
542
+
543
+ class _ServerUser(dict): # type: ignore[type-arg]
544
+ """The server's user as its handlers get it: the authenticate handler's dict, whose
545
+ fields read as keys (`get`) and as attributes."""
546
+
547
+ def __getattr__(self, name: str) -> Any:
548
+ try:
549
+ return self[name]
550
+ except KeyError as exc:
551
+ raise AttributeError(name) from exc
552
+
553
+
554
+ async def _server_user(auth: Any, monkeypatch: pytest.MonkeyPatch, principal: Principal) -> Any:
555
+ """The user the server builds from the authenticate handler for a policy's `principal`."""
556
+ from {{cookiecutter.agent_directory}}.app_utils import auth as auth_module
557
+
558
+ class _Policy:
559
+ async def authenticate(self, request: Any) -> Principal:
560
+ return principal
561
+
562
+ async def authorize(self, *args: Any) -> None:
563
+ return None
564
+
565
+ monkeypatch.setattr(auth_module, "get_policy", lambda: _Policy())
566
+ return _ServerUser(await auth._authenticate_handler(request=_http("POST", "/threads")))
567
+
568
+
569
+ def _principal(*roles: str, actor: Any = None) -> Principal:
570
+ from {{cookiecutter.agent_directory}}.app_utils.auth import ACTIONS
571
+
572
+ return Principal(
573
+ id="alice",
574
+ roles=list(roles),
575
+ permissions=set(ACTIONS),
576
+ attributes={"tenant": "acme", "credentials": {"shop": "secret-token"}},
577
+ actor=actor,
578
+ )
579
+
580
+
581
+ async def test_a_native_run_gets_the_callers_own_run_context_whatever_it_sends(
582
+ auth: Any, monkeypatch: pytest.MonkeyPatch
583
+ ) -> None:
584
+ _no_approvals(monkeypatch)
585
+ alice = await _server_user(auth, monkeypatch, _principal("user", "ops"))
586
+ own = {"principal_id": "alice", "roles": ["user", "ops"], "attributes": {"tenant": "acme"}}
587
+ assert "secret-token" not in repr(dict(alice)) # published without credentials
588
+ forged = {
589
+ "principal_id": "bob",
590
+ "roles": ["admin"],
591
+ "attributes": {"@actor": {"id": "concierge"}, "credentials": {"shop": "forged"}},
592
+ }
593
+ run: dict[str, Any] = {
594
+ "thread_id": SOURCE,
595
+ "metadata": {},
596
+ "kwargs": {
597
+ "input": INPUT,
598
+ # The server copies the run context into `config.configurable` (and back).
599
+ "context": {**forged, "locale": "fr"},
600
+ "config": {"configurable": {**forged, "thread_id": SOURCE}},
601
+ },
602
+ }
603
+ assert await _dispatch(auth, alice, "threads", "create_run", run) == {"principal_id": "alice"}
604
+ assert run["kwargs"]["context"] == {**own, "locale": "fr"} # other settings are kept
605
+ assert run["kwargs"]["config"]["configurable"] == {**own, "thread_id": SOURCE}
606
+ # Nothing sent: the run still acts for the caller (a run context the server keeps).
607
+ bare: dict[str, Any] = {"thread_id": None, "metadata": {}, "kwargs": {"input": INPUT}}
608
+ await _dispatch(auth, alice, "threads", "create_run", bare)
609
+ assert bare["kwargs"]["context"] == own
610
+ bare["kwargs"]["context"]["attributes"]["tenant"] = "changed" # a copy, not the user's
611
+ assert alice["run_context"] == own
612
+
613
+
614
+ async def test_a_delegated_native_run_keeps_its_actor_and_only_lent_roles(
615
+ auth: Any, monkeypatch: pytest.MonkeyPatch
616
+ ) -> None:
617
+ from {{cookiecutter.agent_directory}}.app_utils.auth import Actor
618
+
619
+ monkeypatch.setenv("AUTH_ALLOWED_ACTORS", "billing")
620
+ _no_approvals(monkeypatch)
621
+ actor = Actor(id="billing", chain=("billing", "concierge"), client="billing-app")
622
+ billing = await _server_user(auth, monkeypatch, _principal("ops", actor=actor))
623
+ for sent in (
624
+ None,
625
+ {"principal_id": "alice", "roles": ["ops"], "attributes": {}},
626
+ {"principal_id": "alice", "attributes": {"@actor": {"id": "concierge"}}},
627
+ ):
628
+ run: dict[str, Any] = {"thread_id": SOURCE, "metadata": {}, "kwargs": {"input": INPUT}}
629
+ if sent is not None:
630
+ run["kwargs"]["context"] = sent
631
+ await _dispatch(auth, billing, "threads", "create_run", run)
632
+ assert run["kwargs"]["context"] == {
633
+ "principal_id": "alice",
634
+ "roles": [], # AUTH_DELEGATED_ROLES lends none
635
+ "attributes": {
636
+ "tenant": "acme",
637
+ "@actor": {
638
+ "id": "billing",
639
+ "chain": ["billing", "concierge"],
640
+ "client": "billing-app",
641
+ },
642
+ },
643
+ }
644
+ assert run["metadata"]["actor"] == "billing"
645
+
646
+
647
+ async def test_a_run_context_comes_from_the_verified_user_never_from_the_request(
648
+ auth: Any, monkeypatch: pytest.MonkeyPatch
649
+ ) -> None:
650
+ """A user without a published run context (or with another caller's) gets one built
651
+ from its identity and permissions."""
652
+ _no_approvals(monkeypatch)
653
+
654
+ async def context_of(user: Any) -> Any:
655
+ run: dict[str, Any] = {
656
+ "thread_id": SOURCE,
657
+ "metadata": {},
658
+ "kwargs": {"input": INPUT, "context": {"principal_id": "bob", "roles": ["admin"]}},
659
+ }
660
+ await _dispatch(auth, user, "threads", "create_run", run)
661
+ return run["kwargs"]["context"]
662
+
663
+ concierge = {"@actor": {"id": "concierge", "chain": ["concierge"], "client": None}}
664
+ assert await context_of(_user("alice", "ops")) == {
665
+ "principal_id": "alice",
666
+ "roles": ["ops"],
667
+ "attributes": {},
668
+ }
669
+ assert await context_of(_agent("alice", "concierge")) == {
670
+ "principal_id": "alice",
671
+ "roles": [],
672
+ "attributes": concierge,
673
+ }
674
+ for published in (
675
+ {"principal_id": "bob", "roles": ["admin"], "attributes": {}},
676
+ {"principal_id": "alice", "roles": ["admin"], "attributes": {}}, # drops the actor
677
+ {"principal_id": "alice", "roles": [], "attributes": {"@actor": {"id": "billing"}}},
678
+ ):
679
+ user = _ServerUser(
680
+ identity="alice",
681
+ permissions=["chat.send", "actor:concierge"],
682
+ run_context=published,
683
+ )
684
+ assert await context_of(user) == {
685
+ "principal_id": "alice",
686
+ "roles": [],
687
+ "attributes": concierge,
688
+ }
689
+
690
+
691
+ async def test_a_studio_run_keeps_its_context_and_a_run_without_settings_is_refused(
692
+ auth: Any, monkeypatch: pytest.MonkeyPatch
693
+ ) -> None:
694
+ monkeypatch.setenv("APP_ENV", "dev")
695
+ studio = Auth.types.StudioUser("langgraph-studio-user")
696
+ context = {"principal_id": "tester", "roles": ["ops"], "attributes": {}}
697
+ run: dict[str, Any] = {"metadata": {}, "kwargs": {"input": INPUT, "context": dict(context)}}
698
+ await _dispatch(auth, studio, "threads", "create_run", run)
699
+ assert run["kwargs"]["context"] == context # the developer's, under `langgraph dev` only
700
+ with pytest.raises(Auth.exceptions.HTTPException) as exc:
701
+ await _dispatch(auth, _user("alice"), "threads", "create_run", {"kwargs": "input"})
702
+ assert exc.value.status_code == 403