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,271 @@
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
+ """What the logs never hold: query strings, outbound URLs, raw warnings, a run
16
+ context's credential."""
17
+
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ import logging
22
+ import os
23
+ import subprocess
24
+ import sys
25
+ from collections.abc import Iterator
26
+ from pathlib import Path
27
+
28
+ import pytest
29
+
30
+ from {{cookiecutter.agent_directory}}.app_utils import telemetry
31
+
32
+ PROJECT = Path(__file__).resolve().parents[2]
33
+
34
+
35
+ @pytest.fixture
36
+ def json_logging(monkeypatch: pytest.MonkeyPatch) -> Iterator[None]:
37
+ root = logging.getLogger()
38
+ level, handlers = root.level, list(root.handlers)
39
+ quiet = {name: logging.getLogger(name).level for name in telemetry.QUIET_LOGGERS}
40
+ monkeypatch.setenv("LOG_FORMAT", "json")
41
+ monkeypatch.setenv("LOG_LEVEL", "INFO")
42
+ telemetry.setup_logging()
43
+ yield
44
+ logging.captureWarnings(False)
45
+ root.handlers = handlers
46
+ root.setLevel(level)
47
+ for name, value in quiet.items():
48
+ logging.getLogger(name).setLevel(value)
49
+
50
+
51
+ def _records(err: str) -> list[dict]:
52
+ return [json.loads(line) for line in err.splitlines() if line.startswith("{")]
53
+
54
+
55
+ def test_access_lines_keep_the_path_and_drop_the_query(json_logging, capsys) -> None:
56
+ access = logging.getLogger("uvicorn.access")
57
+ access.info(
58
+ '%s - "%s %s HTTP/%s" %d',
59
+ "127.0.0.1:5000",
60
+ "GET",
61
+ "/threads?access_token=eyJ.canary-token-4411&limit=1",
62
+ "1.1",
63
+ 401,
64
+ )
65
+ access.info('%s - "%s %s HTTP/%s" %d', "127.0.0.1:5000", "GET", "/health", "1.1", 200)
66
+ err = capsys.readouterr().err
67
+ assert "canary-token-4411" not in err and "limit=1" not in err
68
+ messages = [r["message"] for r in _records(err)]
69
+ assert messages == [
70
+ '127.0.0.1:5000 - "GET /threads HTTP/1.1" 401',
71
+ '127.0.0.1:5000 - "GET /health HTTP/1.1" 200',
72
+ ]
73
+
74
+
75
+ def test_http_client_url_lines_stay_out_of_info_logs(json_logging, capsys) -> None:
76
+ logging.getLogger("httpx").info(
77
+ 'HTTP Request: GET http://orders.test/orders?customer=alice "HTTP/1.1 200 OK"'
78
+ )
79
+ logging.getLogger("httpcore.connection").info("connect_tcp.started host='orders.test'")
80
+ logging.getLogger("httpx2").info('HTTP Request: POST http://model.test/v1/chat?k=v "200"')
81
+ logging.getLogger("httpx").warning("kept: a warning")
82
+ err = capsys.readouterr().err
83
+ assert "customer=alice" not in err and "connect_tcp" not in err and "k=v" not in err
84
+ assert [r["message"] for r in _records(err)] == ["kept: a warning"]
85
+
86
+
87
+ def test_setup_is_idempotent(json_logging) -> None:
88
+ telemetry.setup_logging()
89
+ access = logging.getLogger("uvicorn.access")
90
+ assert sum(isinstance(f, telemetry.AccessLogFilter) for f in access.filters) == 1
91
+ warns = logging.getLogger("py.warnings")
92
+ assert sum(isinstance(f, telemetry.WarningRedactionFilter) for f in warns.filters) == 1
93
+
94
+
95
+ def test_the_value_a_serializer_warning_echoes_is_redacted() -> None:
96
+ record = logging.LogRecord(
97
+ "py.warnings",
98
+ logging.WARNING,
99
+ __file__,
100
+ 1,
101
+ "%s",
102
+ (
103
+ "pydantic/main.py:475: UserWarning: Pydantic serializer warnings:\n"
104
+ " PydanticSerializationUnexpectedValue(Expected `none` - serialized value may "
105
+ "not be as expected [field_name='context', input_value=AgentContext(principal_id"
106
+ "...'secret-TAIL', input_type=AgentContext], input_type=AgentContext])\n"
107
+ " return self.__pydantic_serializer__.to_python(\n",
108
+ ),
109
+ None,
110
+ )
111
+ assert telemetry.WarningRedactionFilter().filter(record)
112
+ message = record.getMessage()
113
+ assert "secret-TAIL" not in message and "principal_id" not in message
114
+ assert "input_value=<redacted>, input_type=AgentContext]" in message
115
+
116
+
117
+ def test_the_api_client_quiets_the_same_loggers() -> None:
118
+ """`api_client` quiets them itself (a graph process may never load the app)."""
119
+ import inspect
120
+
121
+ from {{cookiecutter.agent_directory}}.app_utils import api_client
122
+
123
+ names = str(telemetry.QUIET_LOGGERS).replace("'", '"')
124
+ assert names in inspect.getsource(api_client)
125
+
126
+
127
+ # --- langgraph-server: the server's own lines follow the same rules ------------------------
128
+
129
+
130
+ @pytest.fixture
131
+ def server_logging() -> Iterator[logging.Logger]:
132
+ access = logging.getLogger(telemetry.SERVER_ACCESS_LOGGER)
133
+ saved_filters = list(access.filters)
134
+ quiet = {name: logging.getLogger(name).level for name in telemetry.QUIET_LOGGERS}
135
+ for name in telemetry.QUIET_LOGGERS:
136
+ logging.getLogger(name).setLevel(logging.NOTSET)
137
+ telemetry.setup_server_logging()
138
+ yield access
139
+ logging.captureWarnings(False)
140
+ access.filters = saved_filters
141
+ for name, value in quiet.items():
142
+ logging.getLogger(name).setLevel(value)
143
+
144
+
145
+ def test_the_server_access_line_loses_its_query_string(server_logging) -> None:
146
+ """LangGraph Server logs a structlog event dict per request, query string included."""
147
+ event = {
148
+ "event": "GET /threads 401 3ms",
149
+ "path": "/threads",
150
+ "status": 401,
151
+ "query_string": "access_token=LGQSCANARY-1&x=1",
152
+ }
153
+ record = logging.LogRecord(
154
+ telemetry.SERVER_ACCESS_LOGGER, logging.WARNING, __file__, 1, event, None, None
155
+ )
156
+ assert all(f.filter(record) for f in server_logging.filters)
157
+ assert record.msg == {k: v for k, v in event.items() if k != "query_string"}
158
+ assert "query_string" in event # the server's own dict is left alone
159
+ telemetry.setup_server_logging() # idempotent
160
+ assert sum(isinstance(f, telemetry.ServerAccessLogFilter) for f in server_logging.filters) == 1
161
+ for name in ("httpx", "httpcore", "httpx2", "httpcore2"):
162
+ assert logging.getLogger(name).getEffectiveLevel() == logging.WARNING
163
+
164
+
165
+ def test_the_rendered_server_line_holds_no_query_value(server_logging) -> None:
166
+ """End to end with structlog configured the way LangGraph Server configures it."""
167
+ structlog = pytest.importorskip("structlog")
168
+ import io
169
+
170
+ stream = io.StringIO()
171
+ handler = logging.StreamHandler(stream)
172
+ handler.setFormatter(
173
+ structlog.stdlib.ProcessorFormatter(
174
+ processors=[
175
+ structlog.stdlib.ProcessorFormatter.remove_processors_meta,
176
+ structlog.processors.JSONRenderer(),
177
+ ],
178
+ )
179
+ )
180
+ server_logging.addHandler(handler)
181
+ try:
182
+ log = structlog.wrap_logger(
183
+ server_logging,
184
+ processors=[structlog.stdlib.ProcessorFormatter.wrap_for_formatter],
185
+ wrapper_class=structlog.stdlib.BoundLogger,
186
+ )
187
+ log.warning("GET /threads 401 3ms", path="/threads", query_string="access_token=CANARY-9")
188
+ finally:
189
+ server_logging.removeHandler(handler)
190
+ line = json.loads(stream.getvalue())
191
+ assert line["event"] == "GET /threads 401 3ms" and line["path"] == "/threads"
192
+ assert "CANARY-9" not in stream.getvalue() and "query_string" not in line
193
+
194
+
195
+ # A tool with the unparameterised `ToolRuntime` makes pydantic warn with the run
196
+ # context's repr on every call. The run context holds the caller's forwarded
197
+ # credential; the process's stderr must stay JSON and must not hold it.
198
+ _TOOL_RUN = """
199
+ import asyncio
200
+ from dataclasses import dataclass, field
201
+ from typing import Any
202
+
203
+ from langchain.agents import create_agent
204
+ from langchain.tools import ToolRuntime
205
+ from langchain_core.language_models.fake_chat_models import FakeMessagesListChatModel
206
+ from langchain_core.messages import AIMessage
207
+ from langchain_core.tools import tool
208
+
209
+ from {{cookiecutter.agent_directory}}.app_utils import telemetry
210
+
211
+
212
+ @dataclass
213
+ class Context:
214
+ principal_id: str = "anonymous"
215
+ roles: list[str] = field(default_factory=list)
216
+ attributes: dict[str, Any] = field(default_factory=dict)
217
+
218
+
219
+ class Scripted(FakeMessagesListChatModel):
220
+ def bind_tools(self, tools, **kwargs):
221
+ return self
222
+
223
+
224
+ @tool
225
+ async def lookup(item: str, runtime: ToolRuntime) -> str:
226
+ \"\"\"Look up ITEM.\"\"\"
227
+ return "found " + item
228
+
229
+
230
+ async def main() -> None:
231
+ telemetry.setup_logging()
232
+ model = Scripted(responses=[
233
+ AIMessage(content="", tool_calls=[{"name": "lookup", "args": {"item": "x"}, "id": "c1"}]),
234
+ AIMessage(content="done"),
235
+ ])
236
+ graph = create_agent(model=model, tools=[lookup], context_schema=Context)
237
+ context = Context(
238
+ principal_id="alice@example.com",
239
+ roles=["user"],
240
+ attributes={"credentials": {"orders": "FWD-SECRET-abcdefghijklmnop-TAILMARK"}},
241
+ )
242
+ async for _ in graph.astream(
243
+ {"messages": [{"role": "user", "content": "go"}]},
244
+ context=context,
245
+ stream_mode=["updates"],
246
+ ):
247
+ pass
248
+
249
+
250
+ asyncio.run(main())
251
+ """
252
+
253
+
254
+ def test_warnings_are_json_records_without_the_context_repr() -> None:
255
+ env = dict(os.environ)
256
+ env.update({"LOG_FORMAT": "json", "LOG_LEVEL": "INFO", "PYTHONWARNINGS": "default"})
257
+ result = subprocess.run(
258
+ [sys.executable, "-c", _TOOL_RUN],
259
+ env=env,
260
+ cwd=PROJECT,
261
+ capture_output=True,
262
+ text=True,
263
+ timeout=120,
264
+ )
265
+ assert result.returncode == 0, result.stderr
266
+ assert "TAILMARK" not in result.stderr and "alice@example.com" not in result.stderr
267
+ lines = [line for line in result.stderr.splitlines() if line.strip()]
268
+ records = [json.loads(line) for line in lines] # every line is one JSON record
269
+ # The warning itself is kept (a JSON record); only the value it echoes goes.
270
+ warned = [r["message"] for r in records if r["logger"] == "py.warnings"]
271
+ assert warned and all("input_value=<redacted>" in m for m in warned if "input_value" in m)
@@ -0,0 +1,378 @@
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
+ """Unit tests for the auth policies: shared-bearer, the custom stub and policy selection.
16
+
17
+ The jwt policy has its own file (test_jwt_policy.py). No network, no model key.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import pytest
23
+ from fastapi import HTTPException
24
+ from starlette.requests import Request
25
+
26
+ from {{cookiecutter.agent_directory}}.app_utils import auth as auth_module
27
+ from {{cookiecutter.agent_directory}}.app_utils.auth import (
28
+ ACTIONS,
29
+ Actor,
30
+ JwtPolicy,
31
+ Principal,
32
+ SharedBearerPolicy,
33
+ actor_from_claims,
34
+ admin_roles,
35
+ authenticate_and_authorize,
36
+ check_startup,
37
+ delegation_settings,
38
+ dev_mode,
39
+ finalize_principal,
40
+ get_policy,
41
+ keep_subject_token,
42
+ policy_name,
43
+ read_across_roles,
44
+ require,
45
+ reset_policy_cache,
46
+ )
47
+ from {{cookiecutter.agent_directory}}.app_utils.limits import SettingsError
48
+ from {{cookiecutter.agent_directory}}.policies import build_policy
49
+ from {{cookiecutter.agent_directory}}.policies.custom import NOT_IMPLEMENTED, CustomPolicy
50
+
51
+
52
+ def _request(headers: dict[str, str] | None = None) -> Request:
53
+ raw = [(k.lower().encode(), v.encode()) for k, v in (headers or {}).items()]
54
+ return Request(
55
+ {"type": "http", "method": "POST", "path": "/chat", "headers": raw, "query_string": b""}
56
+ )
57
+
58
+
59
+ # --- SharedBearerPolicy -------------------------------------------------------
60
+
61
+
62
+ async def test_shared_bearer_accepts_the_configured_key() -> None:
63
+ policy = SharedBearerPolicy(api_key="s3cret")
64
+ principal = await policy.authenticate(_request({"Authorization": "Bearer s3cret"}))
65
+ assert principal.id == "shared"
66
+ assert principal.permissions == set(ACTIONS)
67
+ assert len(principal.hashed_id()) == 16
68
+ for action in ACTIONS:
69
+ await policy.authorize(principal, action, None)
70
+
71
+
72
+ @pytest.mark.parametrize(
73
+ "headers",
74
+ [
75
+ {},
76
+ {"Authorization": "Bearer wrong"},
77
+ {"Authorization": "Basic s3cret"},
78
+ {"Authorization": "Bearer"},
79
+ ],
80
+ )
81
+ async def test_shared_bearer_rejects_missing_or_wrong_key(headers: dict[str, str]) -> None:
82
+ policy = SharedBearerPolicy(api_key="s3cret")
83
+ with pytest.raises(HTTPException) as exc:
84
+ await policy.authenticate(_request(headers))
85
+ assert exc.value.status_code == 401
86
+ assert exc.value.headers == {"WWW-Authenticate": "Bearer"}
87
+
88
+
89
+ async def test_shared_bearer_fails_closed_without_a_configured_key(
90
+ monkeypatch: pytest.MonkeyPatch,
91
+ ) -> None:
92
+ monkeypatch.delenv("API_KEY", raising=False)
93
+ policy = SharedBearerPolicy()
94
+ with pytest.raises(HTTPException) as exc:
95
+ await policy.authenticate(_request({"Authorization": "Bearer anything"}))
96
+ assert exc.value.status_code == 503
97
+
98
+
99
+ async def test_shared_bearer_reads_api_key_from_env(monkeypatch: pytest.MonkeyPatch) -> None:
100
+ monkeypatch.setenv("API_KEY", "from-env")
101
+ principal = await SharedBearerPolicy().authenticate(
102
+ _request({"Authorization": "Bearer from-env"})
103
+ )
104
+ assert principal.id == "shared"
105
+
106
+
107
+ async def test_unknown_action_is_forbidden() -> None:
108
+ policy = SharedBearerPolicy(api_key="k")
109
+ with pytest.raises(HTTPException) as exc:
110
+ await policy.authorize(Principal(id="shared"), "thread.export", None)
111
+ assert exc.value.status_code == 403
112
+
113
+
114
+ # --- CustomPolicy stub and an unconfigured jwt policy: fail closed ---------------
115
+
116
+
117
+ @pytest.fixture
118
+ def custom_policy(monkeypatch: pytest.MonkeyPatch):
119
+ monkeypatch.setenv("AUTH_POLICY", "custom")
120
+ reset_policy_cache()
121
+ yield
122
+ reset_policy_cache()
123
+
124
+
125
+ async def test_custom_stub_fails_closed_on_every_action(custom_policy) -> None:
126
+ assert isinstance(build_policy("custom"), CustomPolicy)
127
+ assert isinstance(get_policy(), CustomPolicy)
128
+ with pytest.raises(HTTPException) as exc:
129
+ await require("chat.send")(_request({"Authorization": "Bearer test-key"}))
130
+ assert exc.value.status_code == 503 and exc.value.detail == NOT_IMPLEMENTED
131
+ for action in ACTIONS:
132
+ with pytest.raises(HTTPException) as exc:
133
+ await authenticate_and_authorize(_request(), action)
134
+ assert exc.value.status_code == 503
135
+ # A stub that only failed authenticate would still be caught here.
136
+ with pytest.raises(HTTPException) as exc:
137
+ await CustomPolicy().authorize(Principal(id="u1"), "chat.send", None)
138
+ assert exc.value.status_code == 503
139
+
140
+
141
+ async def test_unconfigured_jwt_fails_closed(monkeypatch: pytest.MonkeyPatch) -> None:
142
+ monkeypatch.setenv("AUTH_POLICY", "jwt")
143
+ monkeypatch.setenv("APP_ENV", "dev")
144
+ for name in ("AUTH_JWT_JWKS_URL", "AUTH_JWT_PUBLIC_KEY", "AUTH_JWT_ALGORITHMS"):
145
+ monkeypatch.delenv(name, raising=False)
146
+ reset_policy_cache()
147
+ try:
148
+ assert isinstance(get_policy(), JwtPolicy)
149
+ for action in ACTIONS:
150
+ with pytest.raises(HTTPException) as exc:
151
+ await authenticate_and_authorize(
152
+ _request({"Authorization": "Bearer x.y.z"}), action
153
+ )
154
+ assert exc.value.status_code == 503
155
+ assert "AUTH_POLICY=jwt" in exc.value.detail
156
+ finally:
157
+ reset_policy_cache()
158
+
159
+
160
+ # --- startup check and shared settings ------------------------------------------------
161
+
162
+
163
+ def test_check_startup(monkeypatch: pytest.MonkeyPatch) -> None:
164
+ for app_env in ("dev", "prod"):
165
+ monkeypatch.setenv("APP_ENV", app_env)
166
+ # shared-bearer and the custom stub have nothing to check at startup.
167
+ for name in ("shared-bearer", "custom", "product-session"):
168
+ monkeypatch.setenv("AUTH_POLICY", name)
169
+ reset_policy_cache()
170
+ check_startup()
171
+ # A typo in AUTH_POLICY never starts, in any environment.
172
+ monkeypatch.setenv("AUTH_POLICY", "jtw")
173
+ reset_policy_cache()
174
+ with pytest.raises(RuntimeError, match="Unknown AUTH_POLICY"):
175
+ check_startup()
176
+ reset_policy_cache()
177
+
178
+
179
+ def test_dev_mode_needs_an_explicit_app_env(monkeypatch: pytest.MonkeyPatch) -> None:
180
+ # Only exactly `dev`: dev relaxes checks, so a near miss is a deployed environment.
181
+ for value, expected in (
182
+ ("dev", True),
183
+ (" DEV ", False),
184
+ ("Dev", False),
185
+ ("dev ", False),
186
+ ("development", False),
187
+ ("prod", False),
188
+ ("", False),
189
+ ):
190
+ monkeypatch.setenv("APP_ENV", value)
191
+ assert dev_mode() is expected, value
192
+ monkeypatch.delenv("APP_ENV")
193
+ assert dev_mode() is False
194
+
195
+
196
+ def test_role_lists(monkeypatch: pytest.MonkeyPatch) -> None:
197
+ monkeypatch.delenv("AUTH_ADMIN_ROLES", raising=False)
198
+ assert admin_roles() == set() # empty = nobody
199
+ monkeypatch.setenv("AUTH_ADMIN_ROLES", " admin, ,platform ")
200
+ monkeypatch.setenv("AUTH_READ_ACROSS_ROLES", "support")
201
+ assert admin_roles() == {"admin", "platform"}
202
+ assert read_across_roles() == {"support"}
203
+
204
+
205
+ def test_retired_policy_name_reads_as_custom(
206
+ monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
207
+ ) -> None:
208
+ monkeypatch.setenv("AUTH_POLICY", "product-session")
209
+ monkeypatch.setattr(auth_module, "_warned_aliases", set())
210
+ reset_policy_cache()
211
+ try:
212
+ with caplog.at_level("WARNING"):
213
+ assert policy_name() == "custom"
214
+ assert policy_name() == "custom"
215
+ assert isinstance(get_policy(), CustomPolicy)
216
+ warnings = [r for r in caplog.records if "deprecated" in r.getMessage()]
217
+ assert len(warnings) == 1
218
+ finally:
219
+ reset_policy_cache()
220
+
221
+
222
+ def test_public_attributes_never_carry_credentials() -> None:
223
+ principal = Principal(
224
+ id="u1", attributes={"tenant": "t1", "credentials": {"billing": "Bearer secret"}}
225
+ )
226
+ assert principal.public_attributes() == {"tenant": "t1"}
227
+ assert "credentials" in principal.attributes # the original is untouched
228
+
229
+
230
+ def test_hashed_id_is_salted_with_principal_hash_salt(monkeypatch: pytest.MonkeyPatch) -> None:
231
+ import hashlib
232
+ import hmac
233
+
234
+ principal = Principal(id="alice@example.com")
235
+ monkeypatch.delenv("PRINCIPAL_HASH_SALT", raising=False)
236
+ unsalted = principal.hashed_id()
237
+ assert unsalted == hashlib.sha256(b"alice@example.com").hexdigest()[:16]
238
+ monkeypatch.setenv("PRINCIPAL_HASH_SALT", " ") # blank = not set
239
+ assert principal.hashed_id() == unsalted
240
+ monkeypatch.setenv("PRINCIPAL_HASH_SALT", "pepper-1")
241
+ salted = principal.hashed_id()
242
+ expected = hmac.new(b"pepper-1", b"alice@example.com", hashlib.sha256).hexdigest()[:16]
243
+ assert salted == expected and salted != unsalted and len(salted) == 16
244
+ assert Principal(id="bob").hashed_id() != salted
245
+ monkeypatch.setenv("PRINCIPAL_HASH_SALT", "pepper-2")
246
+ assert principal.hashed_id() not in (salted, unsalted)
247
+
248
+
249
+ def test_unknown_policy_name_is_an_error() -> None:
250
+ with pytest.raises(RuntimeError, match="Unknown AUTH_POLICY"):
251
+ build_policy("bogus")
252
+
253
+
254
+ async def test_not_implemented_error_from_a_policy_maps_to_503(
255
+ monkeypatch: pytest.MonkeyPatch,
256
+ ) -> None:
257
+ assert auth_module._as_http_exception(NotImplementedError("x")).status_code == 503
258
+ assert auth_module._as_http_exception(NotImplementedError()).detail.startswith("The configured")
259
+ kept = HTTPException(status_code=401)
260
+ assert auth_module._as_http_exception(kept) is kept
261
+ with pytest.raises(ValueError):
262
+ auth_module._as_http_exception(ValueError("not mapped"))
263
+
264
+ class _Unimplemented:
265
+ async def authenticate(self, request: Request) -> Principal:
266
+ raise NotImplementedError("consumer policy pending")
267
+
268
+ async def authorize(self, principal: Principal, action: str, resource: str | None) -> None:
269
+ raise NotImplementedError
270
+
271
+ monkeypatch.setattr(auth_module, "get_policy", lambda: _Unimplemented())
272
+ with pytest.raises(HTTPException) as exc:
273
+ await require("chat.send")(_request())
274
+ assert exc.value.status_code == 503 and "pending" in str(exc.value.detail)
275
+ with pytest.raises(HTTPException) as exc:
276
+ await authenticate_and_authorize(_request(), "card.read")
277
+ assert exc.value.status_code == 503
278
+
279
+
280
+ # --- finalize_principal: one rule set for every policy (0.3) --------------------------------
281
+
282
+
283
+ class _FixedPolicy:
284
+ """A custom policy that returns one principal, as a project's own policy would."""
285
+
286
+ def __init__(self, principal: Principal) -> None:
287
+ self.principal = principal
288
+
289
+ async def authenticate(self, request: Request) -> Principal:
290
+ return self.principal
291
+
292
+ async def authorize(self, principal: Principal, action: str, resource: str | None) -> None:
293
+ return None
294
+
295
+
296
+ async def test_shared_bearer_principals_pass_unchanged(monkeypatch: pytest.MonkeyPatch) -> None:
297
+ monkeypatch.setenv("AUTH_POLICY", "shared-bearer")
298
+ monkeypatch.setenv("API_KEY", "k")
299
+ reset_policy_cache()
300
+ try:
301
+ principal = await require("chat.send")(_request({"Authorization": "Bearer k"}))
302
+ finally:
303
+ reset_policy_cache()
304
+ assert (principal.id, principal.roles, principal.actor) == ("shared", ["shared"], None)
305
+ assert principal.owner_key() == "shared" and "@actor" not in principal.attributes
306
+
307
+
308
+ @pytest.mark.parametrize("bad_id", ["", "a\x00b", "tab\there", "x" * 257])
309
+ async def test_a_custom_policys_invalid_ids_are_refused_as_its_bug(
310
+ monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, bad_id: str
311
+ ) -> None:
312
+ monkeypatch.setenv("AUTH_POLICY", "custom")
313
+ for principal in (
314
+ Principal(id=bad_id),
315
+ Principal(id="alice", actor=Actor(id=bad_id)),
316
+ Principal(id="alice", actor=Actor(id="concierge", chain=("concierge", bad_id))),
317
+ ):
318
+ monkeypatch.setattr(auth_module, "get_policy", lambda p=principal: _FixedPolicy(p))
319
+ with pytest.raises(HTTPException) as exc:
320
+ await authenticate_and_authorize(_request(), "a2a.invoke")
321
+ assert exc.value.status_code == 500
322
+ assert "a bug in the policy" in caplog.text
323
+
324
+
325
+ async def test_a_custom_policy_marks_delegation_with_actor(monkeypatch: pytest.MonkeyPatch) -> None:
326
+ monkeypatch.setenv("AUTH_POLICY", "custom")
327
+ delegated = Principal(
328
+ id="alice", roles=["support"], actor=Actor(id="concierge"), permissions=set(ACTIONS)
329
+ )
330
+ monkeypatch.setattr(auth_module, "get_policy", lambda: _FixedPolicy(delegated))
331
+ with pytest.raises(HTTPException) as exc:
332
+ await require("chat.send")(_request())
333
+ assert exc.value.status_code == 403
334
+ monkeypatch.setenv("AUTH_ALLOWED_ACTORS", "concierge")
335
+ principal = await require("chat.send")(_request())
336
+ assert principal.actor == Actor(id="concierge", chain=("concierge",))
337
+ assert principal.roles == [] and principal.owner_key() == "alice\x1fconcierge"
338
+ # A chain that does not start with the actor is a bug in the policy.
339
+ broken = Principal(id="alice", actor=Actor(id="concierge", chain=("billing",)))
340
+ monkeypatch.setattr(auth_module, "get_policy", lambda: _FixedPolicy(broken))
341
+ with pytest.raises(HTTPException) as exc:
342
+ await require("chat.send")(_request())
343
+ assert exc.value.status_code == 500
344
+
345
+
346
+ def test_the_jwt_helpers_are_exported_for_custom_policies() -> None:
347
+ actor = actor_from_claims({"sub": "alice", "act": {"sub": "concierge"}})
348
+ assert actor == Actor(id="concierge", chain=("concierge",), client=None)
349
+ principal = keep_subject_token(Principal(id="alice"), "tok", ["a", "b"])
350
+ assert principal.attributes["credentials"] == {
351
+ "@subject_token": "tok",
352
+ "@subject_aud": ("a", "b"),
353
+ }
354
+ assert principal.public_attributes() == {}
355
+
356
+
357
+ def test_delegation_settings_are_checked(monkeypatch: pytest.MonkeyPatch) -> None:
358
+ defaults = delegation_settings({})
359
+ assert (defaults.max_depth, defaults.allowed_actors, defaults.delegated_roles) == (
360
+ 3,
361
+ frozenset(),
362
+ frozenset(),
363
+ )
364
+ assert not defaults.allows("concierge")
365
+ assert delegation_settings({"AUTH_ALLOWED_ACTORS": "*"}).allows("anyone")
366
+ for env in (
367
+ {"AUTH_MAX_DELEGATION_DEPTH": "0"},
368
+ {"AUTH_MAX_DELEGATION_DEPTH": "9"},
369
+ {"AUTH_MAX_DELEGATION_DEPTH": "three"},
370
+ {"AUTH_ALLOWED_ACTORS": "concierge, bad\x01id"},
371
+ ):
372
+ with pytest.raises(SettingsError):
373
+ delegation_settings(env)
374
+ # A bad value reached at request time (the startup check refuses it) fails closed.
375
+ monkeypatch.setenv("AUTH_MAX_DELEGATION_DEPTH", "99")
376
+ with pytest.raises(HTTPException) as exc:
377
+ finalize_principal(Principal(id="alice"))
378
+ assert exc.value.status_code == 503