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,404 @@
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
+ """Capture policy: under `metadata` no error message or stack trace leaves the process."""
16
+
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ import logging
21
+ from typing import Any
22
+
23
+ import pytest
24
+
25
+ from {{cookiecutter.agent_directory}}.app_utils import telemetry
26
+ from {{cookiecutter.agent_directory}}.app_utils.limits import SettingsError
27
+
28
+ SECRET = "SECRET-PROMPT-TEXT"
29
+
30
+
31
+ @pytest.fixture
32
+ def tracing_env(monkeypatch: pytest.MonkeyPatch):
33
+ # Every variable setup_telemetry() writes goes through monkeypatch so the
34
+ # environment (and LangChain's tracing switch) is restored for later tests.
35
+ for var in (
36
+ "LANGSMITH_TRACING",
37
+ "LANGCHAIN_TRACING_V2",
38
+ "LANGSMITH_HIDE_INPUTS",
39
+ "LANGSMITH_HIDE_OUTPUTS",
40
+ ):
41
+ monkeypatch.setenv(var, "false")
42
+ monkeypatch.setenv("LANGSMITH_ENDPOINT", "http://127.0.0.1:9") # never a real upload
43
+ monkeypatch.setenv("TRACING_ENABLED", "true")
44
+ monkeypatch.setenv("TRACE_CAPTURE", "metadata")
45
+ monkeypatch.setenv("LANGSMITH_API_KEY", "lsv2_test")
46
+ monkeypatch.setenv("LANGSMITH_PROJECT", "test-project")
47
+ monkeypatch.setattr(telemetry, "_initialized", False)
48
+ yield
49
+ import langsmith
50
+
51
+ langsmith.configure(client=None)
52
+ monkeypatch.setattr(telemetry, "_initialized", False)
53
+
54
+
55
+ def test_redact_error_keeps_only_the_exception_class() -> None:
56
+ error = f"ValueError('unknown item {SECRET}')Traceback (most recent call last):\n File ..."
57
+ assert telemetry.redact_error({"error": error}) == {"error": "ValueError: <redacted>"}
58
+ # Metadata and other payloads pass through untouched (the client runs the
59
+ # anonymizer over run metadata as well).
60
+ assert telemetry.redact_error({"error": "x", "other": 1}) == {"error": "x", "other": 1}
61
+ assert telemetry.redact_error({"thread_id": "t"}) == {"thread_id": "t"}
62
+ assert telemetry.redact_error(["not", "a", "dict"]) == ["not", "a", "dict"]
63
+
64
+
65
+ async def test_langsmith_uploads_no_error_message_under_metadata_capture(
66
+ tracing_env, monkeypatch: pytest.MonkeyPatch
67
+ ) -> None:
68
+ from langchain_core.runnables import RunnableLambda
69
+ from langchain_core.tracers.langchain import LangChainTracer
70
+ from langsmith import Client
71
+
72
+ bodies: list[bytes] = []
73
+
74
+ def fake_request(self: Any, method: str, pathname: str, *, request_kwargs: Any, **_: Any):
75
+ content = request_kwargs.get("content") or request_kwargs.get("data") or b""
76
+ if isinstance(content, str):
77
+ content = content.encode()
78
+ if content:
79
+ bodies.append(content)
80
+
81
+ class _Resp:
82
+ status_code = 200
83
+
84
+ def json(self) -> dict:
85
+ return {}
86
+
87
+ def raise_for_status(self) -> None:
88
+ pass
89
+
90
+ return _Resp()
91
+
92
+ monkeypatch.setattr(Client, "request_with_retries", fake_request)
93
+ telemetry.setup_telemetry()
94
+
95
+ import langsmith
96
+
97
+ client = langsmith.run_trees.get_cached_client()
98
+ assert client._hide_inputs is True and client._hide_outputs is True
99
+ tracer = LangChainTracer(client=client, project_name="test-project")
100
+
101
+ def _boom(_x: Any) -> Any:
102
+ raise ValueError(f"unknown item {SECRET}")
103
+
104
+ with pytest.raises(ValueError):
105
+ RunnableLambda(_boom).invoke("hi", config={"callbacks": [tracer]})
106
+ tracer.wait_for_futures()
107
+ client.flush()
108
+
109
+ payloads = b"".join(bodies).decode("utf-8", errors="replace")
110
+ assert payloads, "nothing was uploaded"
111
+ assert SECRET not in payloads
112
+ assert "Traceback" not in payloads
113
+ assert "ValueError: <redacted>" in payloads
114
+
115
+
116
+ def test_langsmith_full_capture_keeps_the_message(
117
+ tracing_env, monkeypatch: pytest.MonkeyPatch
118
+ ) -> None:
119
+ monkeypatch.setenv("TRACE_CAPTURE", "full")
120
+ telemetry.setup_telemetry()
121
+ import langsmith
122
+
123
+ client = langsmith.run_trees.get_cached_client()
124
+ assert client._hide_inputs is False and client._anonymizer is None
125
+ error = {"error": f"ValueError('{SECRET}')"}
126
+ assert client._hide_run_error(error) == error
127
+
128
+
129
+ def _encode(spans: list) -> dict:
130
+ from google.protobuf.json_format import MessageToDict
131
+ from opentelemetry.exporter.otlp.proto.common._internal.trace_encoder import encode_spans
132
+
133
+ return MessageToDict(encode_spans(spans))
134
+
135
+
136
+ def test_otlp_exporter_strips_error_text_from_events_and_status() -> None:
137
+ from opentelemetry.sdk.trace import TracerProvider
138
+ from opentelemetry.sdk.trace.export import SimpleSpanProcessor
139
+ from opentelemetry.trace import Status, StatusCode
140
+
141
+ class _Sink:
142
+ def __init__(self) -> None:
143
+ self.spans: list = []
144
+
145
+ def export(self, spans: Any) -> Any:
146
+ self.spans.extend(spans)
147
+
148
+ def shutdown(self) -> None:
149
+ pass
150
+
151
+ def force_flush(self, timeout_millis: int = 30000) -> bool:
152
+ return True
153
+
154
+ sink = _Sink()
155
+ provider = TracerProvider()
156
+ provider.add_span_processor(SimpleSpanProcessor(telemetry._MetadataOnlyExporter(sink)))
157
+ tracer = provider.get_tracer("test")
158
+ message = f"ValueError('{SECRET}')\nTraceback (most recent call last): ..."
159
+ with tracer.start_as_current_span("with-events") as span:
160
+ span.add_event(
161
+ "exception",
162
+ {
163
+ "exception.type": "ValueError",
164
+ "exception.message": SECRET,
165
+ "exception.stacktrace": "tb",
166
+ },
167
+ )
168
+ span.set_status(Status(StatusCode.ERROR, message))
169
+ with tracer.start_as_current_span("status-only") as span:
170
+ span.set_status(Status(StatusCode.ERROR, message))
171
+ provider.force_flush()
172
+
173
+ encoded = json.dumps(_encode(sink.spans))
174
+ assert SECRET not in encoded and "Traceback" not in encoded
175
+ spans = _encode(sink.spans)["resourceSpans"][0]["scopeSpans"][0]["spans"]
176
+ by_name = {s["name"]: s for s in spans}
177
+ for name in ("with-events", "status-only"):
178
+ assert by_name[name]["status"]["code"] == "STATUS_CODE_ERROR"
179
+ assert "message" not in by_name[name]["status"]
180
+ event_attrs = {a["key"] for a in by_name["with-events"]["events"][0]["attributes"]}
181
+ assert event_attrs == {"exception.type"}
182
+
183
+
184
+ # --- correlation across services ---------------------------------------------------------
185
+
186
+ TRACE_ID = "4bf92f3577b34da6a3ce929d0e0e4736"
187
+ TRACEPARENT = f"00-{TRACE_ID}-00f067aa0ba902b7-01"
188
+
189
+
190
+ def test_outbound_headers_carry_the_request_id(monkeypatch: pytest.MonkeyPatch) -> None:
191
+ monkeypatch.setattr(telemetry, "_otlp_active", False)
192
+ monkeypatch.delenv("PROPAGATE_TRACE_HEADERS", raising=False)
193
+ token = telemetry.LOG_CONTEXT["request_id"].set("req-9")
194
+ try:
195
+ # Without OTLP tracing only the request id is passed on.
196
+ assert telemetry.outbound_trace_headers() == {"X-Request-ID": "req-9"}
197
+ monkeypatch.setenv("PROPAGATE_TRACE_HEADERS", "false")
198
+ assert telemetry.outbound_trace_headers() == {}
199
+ finally:
200
+ telemetry.LOG_CONTEXT["request_id"].reset(token)
201
+
202
+
203
+ def test_an_incoming_trace_is_continued_and_passed_on(monkeypatch: pytest.MonkeyPatch) -> None:
204
+ from opentelemetry.sdk.trace import TracerProvider
205
+
206
+ monkeypatch.setattr(telemetry, "_otlp_active", True)
207
+ monkeypatch.delenv("PROPAGATE_TRACE_HEADERS", raising=False)
208
+ tracer = TracerProvider().get_tracer("test")
209
+ incoming = [(b"traceparent", TRACEPARENT.encode()), (b"baggage", b"user=alice")]
210
+ token = telemetry.attach_trace_context(incoming, "/a2a/app")
211
+ assert token is not None
212
+ try:
213
+ with tracer.start_as_current_span("run") as span:
214
+ outbound = telemetry.outbound_trace_headers()
215
+ finally:
216
+ telemetry.detach_trace_context(token)
217
+ context = span.get_span_context()
218
+ # The run's span joined the caller's trace, and the next hop continues it from the run.
219
+ assert format(context.trace_id, "032x") == TRACE_ID
220
+ _, trace_id, parent_id, _ = outbound["traceparent"].split("-")
221
+ assert (trace_id, parent_id) == (TRACE_ID, format(context.span_id, "016x"))
222
+ assert "baggage" not in outbound # W3C trace context only
223
+ # Nothing is attached without a traceparent, without OTLP tracing, or when switched off.
224
+ assert telemetry.attach_trace_context([(b"x-request-id", b"r")], "/a2a/app") is None
225
+ monkeypatch.setattr(telemetry, "_otlp_active", False)
226
+ assert telemetry.attach_trace_context(incoming, "/a2a/app") is None
227
+ monkeypatch.setattr(telemetry, "_otlp_active", True)
228
+ for off in ("false", "off", "0"):
229
+ monkeypatch.setenv("PROPAGATE_TRACE_HEADERS", off)
230
+ assert telemetry.attach_trace_context(incoming, "/a2a/app") is None
231
+ # Under peers (the default, and what true reads as), only on the A2A routes.
232
+ for peers in ("", "peers", "true", "TRUE"):
233
+ monkeypatch.setenv("PROPAGATE_TRACE_HEADERS", peers)
234
+ for path in ("/chat", "/threads/t1/runs", "/a2a", "/A2A/app", None):
235
+ assert telemetry.attach_trace_context(incoming, path) is None
236
+ # Under all, on every path (a tracing gateway in front of the agent).
237
+ monkeypatch.setenv("PROPAGATE_TRACE_HEADERS", "all")
238
+ token = telemetry.attach_trace_context(incoming, "/chat")
239
+ assert token is not None
240
+ telemetry.detach_trace_context(token)
241
+
242
+
243
+ async def test_the_request_middleware_continues_the_callers_trace(
244
+ monkeypatch: pytest.MonkeyPatch,
245
+ ) -> None:
246
+ import httpx
247
+ from opentelemetry import trace
248
+ from starlette.applications import Starlette
249
+ from starlette.requests import Request
250
+ from starlette.responses import JSONResponse
251
+ from starlette.routing import Route
252
+
253
+ from {{cookiecutter.agent_directory}}.app_utils.middleware import RequestContextMiddleware
254
+
255
+ monkeypatch.setattr(telemetry, "_otlp_active", True)
256
+ monkeypatch.delenv("PROPAGATE_TRACE_HEADERS", raising=False)
257
+
258
+ async def endpoint(request: Request) -> JSONResponse:
259
+ context = trace.get_current_span().get_span_context()
260
+ return JSONResponse(
261
+ {
262
+ "trace_id": format(context.trace_id, "032x"),
263
+ "out": telemetry.outbound_trace_headers(),
264
+ }
265
+ )
266
+
267
+ routes = [Route("/a2a/app", endpoint, methods=["POST"]), Route("/chat", endpoint)]
268
+ app = RequestContextMiddleware(Starlette(routes=routes))
269
+ caller = {"traceparent": TRACEPARENT, "X-Request-ID": "req-7"}
270
+ async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://t") as c:
271
+ body = (await c.post("/a2a/app", headers=caller)).json()
272
+ other = (await c.post("/a2a/app")).json()
273
+ public = (await c.get("/chat", headers=caller)).json()
274
+ monkeypatch.setenv("PROPAGATE_TRACE_HEADERS", "all")
275
+ gateway = (await c.get("/chat", headers=caller)).json()
276
+ assert body["trace_id"] == TRACE_ID
277
+ assert body["out"]["X-Request-ID"] == "req-7"
278
+ assert body["out"]["traceparent"].split("-")[1] == TRACE_ID
279
+ # The context is detached after the request: the next one starts on its own.
280
+ assert other["trace_id"] == "0" * 32 and "traceparent" not in other["out"]
281
+ # A caller of a public route cannot choose this agent's trace ids (the request id is
282
+ # still taken and echoed); behind a tracing gateway, `all` continues it there too.
283
+ assert public["trace_id"] == "0" * 32 and public["out"]["X-Request-ID"] == "req-7"
284
+ assert gateway["trace_id"] == TRACE_ID
285
+
286
+
287
+ @pytest.mark.parametrize(
288
+ ("value", "scope"),
289
+ [
290
+ (None, "peers"),
291
+ ("", "peers"),
292
+ ("peers", "peers"),
293
+ ("true", "peers"),
294
+ ("1", "peers"),
295
+ ("Yes", "peers"),
296
+ ("on", "peers"),
297
+ ("all", "all"),
298
+ ("ALL", "all"),
299
+ ("off", "off"),
300
+ ("false", "off"),
301
+ ("0", "off"),
302
+ ("no", "off"),
303
+ ],
304
+ )
305
+ def test_the_trace_scope(monkeypatch: pytest.MonkeyPatch, value: str | None, scope: str) -> None:
306
+ if value is None:
307
+ monkeypatch.delenv("PROPAGATE_TRACE_HEADERS", raising=False)
308
+ else:
309
+ monkeypatch.setenv("PROPAGATE_TRACE_HEADERS", value)
310
+ assert telemetry.trace_scope() == scope
311
+ assert telemetry.propagate_trace_headers() is (scope != "off")
312
+ assert telemetry.propagate_to_every_api() is (scope == "all")
313
+
314
+
315
+ def test_true_is_read_as_peers_and_said_once(
316
+ monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
317
+ ) -> None:
318
+ monkeypatch.setattr(telemetry, "_said_true_is_peers", False)
319
+ monkeypatch.setenv("PROPAGATE_TRACE_HEADERS", "true")
320
+ with caplog.at_level(logging.INFO, logger=telemetry.__name__):
321
+ assert telemetry.trace_scope() == telemetry.trace_scope() == "peers"
322
+ assert caplog.text.count("PROPAGATE_TRACE_HEADERS=true reads as peers") == 1
323
+
324
+
325
+ async def test_a_bad_trace_scope_stops_startup(monkeypatch: pytest.MonkeyPatch) -> None:
326
+ from {{cookiecutter.agent_directory}} import fast_api_app
327
+
328
+ monkeypatch.setenv("PROPAGATE_TRACE_HEADERS", "everywhere")
329
+ with pytest.raises(SettingsError, match="PROPAGATE_TRACE_HEADERS must be peers, all or off"):
330
+ async with fast_api_app.lifespan(fast_api_app.app):
331
+ pass
332
+ # At runtime a value startup refuses propagates nothing.
333
+ assert not telemetry.propagate_trace_headers() and not telemetry.propagate_to_every_api()
334
+
335
+
336
+ @pytest.mark.parametrize(
337
+ ("value", "receive"),
338
+ [
339
+ ("peers", {"peer", "forward", "exchange"}),
340
+ ("all", {"peer", "forward", "exchange", "bearer", "none", "jsonrpc"}),
341
+ ("off", set()),
342
+ ],
343
+ )
344
+ def test_which_apis_get_the_headers(
345
+ monkeypatch: pytest.MonkeyPatch, value: str, receive: set[str]
346
+ ) -> None:
347
+ from {{cookiecutter.agent_directory}}.app_utils import api_client
348
+
349
+ apis = {
350
+ "peer": {"protocol": "a2a", "auth": "bearer"},
351
+ "forward": {"auth": "forward"},
352
+ "exchange": {"auth": "exchange"},
353
+ "bearer": {"auth": "bearer"},
354
+ "none": {"auth": "none"},
355
+ "jsonrpc": {"protocol": "jsonrpc", "auth": "bearer"},
356
+ }
357
+ monkeypatch.setattr(telemetry, "_otlp_active", False)
358
+ monkeypatch.setenv("PROPAGATE_TRACE_HEADERS", value)
359
+ api_client.set_outbound_headers(
360
+ telemetry.outbound_trace_headers, everywhere=telemetry.propagate_to_every_api
361
+ )
362
+ token = telemetry.LOG_CONTEXT["request_id"].set("req-3")
363
+ try:
364
+ got = {name for name, api in apis.items() if api_client.outbound_headers(api)}
365
+ finally:
366
+ telemetry.LOG_CONTEXT["request_id"].reset(token)
367
+ api_client.set_outbound_headers(None)
368
+ assert got == receive
369
+
370
+
371
+ async def test_a_tool_passes_its_own_span_on_without_a_current_otel_span(
372
+ monkeypatch: pytest.MonkeyPatch,
373
+ ) -> None:
374
+ """A `/chat` run with no incoming trace: OpenInference does not make its spans current,
375
+ so the headers must come from the tool's own span, not from the (empty) OTel context."""
376
+ from langchain_core.tools import tool
377
+ from openinference.instrumentation.langchain import LangChainInstrumentor
378
+ from opentelemetry import trace
379
+ from opentelemetry.sdk.trace import TracerProvider
380
+ from opentelemetry.sdk.trace.export import SimpleSpanProcessor
381
+ from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
382
+
383
+ monkeypatch.setattr(telemetry, "_otlp_active", True)
384
+ monkeypatch.delenv("PROPAGATE_TRACE_HEADERS", raising=False)
385
+ exporter = InMemorySpanExporter()
386
+ provider = TracerProvider()
387
+ provider.add_span_processor(SimpleSpanProcessor(exporter))
388
+
389
+ @tool
390
+ async def ask_peer(question: str) -> str:
391
+ """Returns the correlation headers an outbound call would carry."""
392
+ assert not trace.get_current_span().get_span_context().is_valid
393
+ return json.dumps(telemetry.outbound_trace_headers())
394
+
395
+ instrumentor = LangChainInstrumentor()
396
+ instrumentor.instrument(tracer_provider=provider)
397
+ try:
398
+ headers = json.loads(await ask_peer.ainvoke({"question": "q"}))
399
+ finally:
400
+ instrumentor.uninstrument()
401
+ (span,) = [s for s in exporter.get_finished_spans() if s.name == "ask_peer"]
402
+ _, trace_id, parent_id, _ = headers["traceparent"].split("-")
403
+ assert trace_id == format(span.context.trace_id, "032x")
404
+ assert parent_id == format(span.context.span_id, "016x")
@@ -0,0 +1,255 @@
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
+ """Thread listing scopes, owners shown hashed, and thread-delete listeners."""
16
+
17
+ from __future__ import annotations
18
+
19
+ import os
20
+ import uuid
21
+ from collections.abc import AsyncIterator
22
+ from typing import Any
23
+
24
+ import pytest
25
+ from fastapi import HTTPException
26
+
27
+ from {{cookiecutter.agent_directory}}.app_utils.auth import Actor, Principal
28
+ from {{cookiecutter.agent_directory}}.app_utils.checkpointer import POSTGRES
29
+ from {{cookiecutter.agent_directory}}.app_utils.db import Database
30
+ from {{cookiecutter.agent_directory}}.app_utils.threads import (
31
+ DELETE_LISTENERS,
32
+ SCOPE_ALL,
33
+ SCOPE_OWN,
34
+ ThreadStore,
35
+ check_scope,
36
+ own_view,
37
+ search_server_threads,
38
+ )
39
+
40
+ OWNER = Principal(id="alice@example.com", roles=["viewer"])
41
+ AUDITOR = Principal(id="carol", roles=["auditor", "viewer"], attributes={"credentials": {"x": "s"}})
42
+
43
+
44
+ @pytest.fixture
45
+ def store(monkeypatch: pytest.MonkeyPatch) -> ThreadStore:
46
+ monkeypatch.setenv("AUTH_READ_ACROSS_ROLES", "auditor")
47
+ return ThreadStore(Database("memory"))
48
+
49
+
50
+ async def test_a_read_across_role_lists_its_own_threads_unless_it_asks_for_all(
51
+ store: ThreadStore,
52
+ ) -> None:
53
+ await store.create("t-alice", OWNER)
54
+ await store.create("t-carol", AUDITOR)
55
+ assert [r.thread_id for r in await store.list_for(AUDITOR, scope=SCOPE_OWN)] == ["t-carol"]
56
+ everything = await store.list_for(AUDITOR, scope=SCOPE_ALL)
57
+ assert {r.thread_id for r in everything} == {"t-alice", "t-carol"}
58
+ rows = {r.thread_id: r.public() for r in everything}
59
+ assert rows["t-alice"]["owner"] == OWNER.hashed_id()
60
+ assert "alice@example.com" not in repr(rows)
61
+ # Someone without the role gets its own threads whatever it asks for.
62
+ assert [r.thread_id for r in await store.list_for(OWNER, scope=SCOPE_ALL)] == ["t-alice"]
63
+
64
+
65
+ def test_scope_all_needs_a_read_across_role(store: ThreadStore) -> None:
66
+ assert check_scope(AUDITOR, SCOPE_ALL) == SCOPE_ALL
67
+ assert check_scope(OWNER, SCOPE_OWN) == SCOPE_OWN
68
+ with pytest.raises(HTTPException) as exc:
69
+ check_scope(OWNER, SCOPE_ALL)
70
+ assert exc.value.status_code == 403
71
+ with pytest.raises(HTTPException) as exc:
72
+ check_scope(AUDITOR, "everyone")
73
+ assert exc.value.status_code == 422
74
+
75
+
76
+ def test_own_view_drops_the_read_across_roles_and_the_credentials(store: ThreadStore) -> None:
77
+ view = own_view(AUDITOR)
78
+ assert view.id == "carol" and view.roles == ["viewer"]
79
+ assert "credentials" not in view.attributes
80
+
81
+
82
+ class _Threads:
83
+ def __init__(self, threads: list[dict[str, Any]]) -> None:
84
+ self.threads = threads
85
+ self.searches: list[dict[str, Any]] = []
86
+
87
+ async def search(self, **kwargs: Any) -> list[dict[str, Any]]:
88
+ self.searches.append(kwargs)
89
+ return self.threads
90
+
91
+
92
+ class _Client:
93
+ def __init__(self, threads: list[dict[str, Any]]) -> None:
94
+ self.threads = _Threads(threads)
95
+
96
+
97
+ async def test_server_listing_filters_by_owner_and_hashes_it() -> None:
98
+ client = _Client(
99
+ [
100
+ {"thread_id": "1", "metadata": {"principal_id": "alice@example.com"}},
101
+ {"thread_id": "2", "metadata": {}},
102
+ "not a thread",
103
+ ]
104
+ )
105
+ rows = await search_server_threads(client, AUDITOR, scope=SCOPE_ALL, limit=5, offset=0)
106
+ assert client.threads.searches[-1] == {
107
+ "limit": 5,
108
+ "offset": 0,
109
+ "sort_by": "updated_at",
110
+ "sort_order": "desc",
111
+ }
112
+ assert [(r["thread_id"], r["owner"]) for r in rows] == [("1", OWNER.hashed_id()), ("2", None)]
113
+ await search_server_threads(client, OWNER, scope=SCOPE_OWN, limit=5, offset=10)
114
+ assert client.threads.searches[-1]["metadata"] == {"principal_id": "alice@example.com"}
115
+
116
+
117
+ async def test_deleting_a_thread_tells_the_listeners(store: ThreadStore) -> None:
118
+ told: list[str] = []
119
+
120
+ async def listener(thread_id: str) -> None:
121
+ told.append(thread_id)
122
+
123
+ async def broken(thread_id: str) -> None:
124
+ raise RuntimeError("listener down")
125
+
126
+ DELETE_LISTENERS.extend([broken, listener])
127
+ try:
128
+ await store.create("t1", OWNER)
129
+ await store.delete("t1") # a failing listener never fails the delete
130
+ assert told == ["t1"] and await store.get("t1") is None
131
+ finally:
132
+ DELETE_LISTENERS.remove(broken)
133
+ DELETE_LISTENERS.remove(listener)
134
+
135
+
136
+ # --- the same rules on the Postgres `threads` table (opt-in: TEST_POSTGRES_DSN) ------------
137
+
138
+ ADMIN_DSN = os.environ.get("TEST_POSTGRES_DSN", "")
139
+
140
+
141
+ @pytest.fixture
142
+ async def pg_store(monkeypatch: pytest.MonkeyPatch) -> AsyncIterator[ThreadStore]:
143
+ if not ADMIN_DSN:
144
+ pytest.skip("TEST_POSTGRES_DSN is not set")
145
+ from urllib.parse import urlsplit
146
+
147
+ import psycopg
148
+
149
+ monkeypatch.setenv("AUTH_READ_ACROSS_ROLES", "auditor")
150
+ name = f"gac_test_{uuid.uuid4().hex[:12]}"
151
+ async with await psycopg.AsyncConnection.connect(ADMIN_DSN, autocommit=True) as admin:
152
+ await admin.execute(f'CREATE DATABASE "{name}"')
153
+ db = Database(POSTGRES, urlsplit(ADMIN_DSN)._replace(path=f"/{name}").geturl())
154
+ await db.open()
155
+ try:
156
+ yield ThreadStore(db)
157
+ finally:
158
+ await db.close()
159
+ async with await psycopg.AsyncConnection.connect(ADMIN_DSN, autocommit=True) as admin:
160
+ await admin.execute(f'DROP DATABASE IF EXISTS "{name}" WITH (FORCE)')
161
+
162
+
163
+ async def test_postgres_listing_scopes_and_delete_listeners(pg_store: ThreadStore) -> None:
164
+ await pg_store.create("t-alice", OWNER)
165
+ await pg_store.create("t-carol", AUDITOR)
166
+ own = await pg_store.list_for(own_view(AUDITOR))
167
+ assert [r.thread_id for r in own] == ["t-carol"]
168
+ assert [r.thread_id for r in await pg_store.list_for(AUDITOR, scope=SCOPE_OWN)] == ["t-carol"]
169
+ everything = await pg_store.list_for(AUDITOR, scope=SCOPE_ALL)
170
+ assert {r.public()["owner"] for r in everything} == {OWNER.hashed_id(), AUDITOR.hashed_id()}
171
+ told: list[str] = []
172
+
173
+ async def listener(thread_id: str) -> None:
174
+ told.append(thread_id)
175
+
176
+ DELETE_LISTENERS.append(listener)
177
+ try:
178
+ await pg_store.delete("t-alice")
179
+ finally:
180
+ DELETE_LISTENERS.remove(listener)
181
+ assert told == ["t-alice"] and await pg_store.get("t-alice") is None
182
+
183
+
184
+ # --- threads an agent started for its user (0.3) ------------------------------------------
185
+
186
+ AGENT = Principal(id="alice@example.com", actor=Actor(id="concierge", chain=("concierge",)))
187
+ OTHER_AGENT = Principal(id="alice@example.com", actor=Actor(id="billing", chain=("billing",)))
188
+
189
+
190
+ async def test_listing_follows_the_owner_key(store: ThreadStore) -> None:
191
+ await store.create("t-direct", OWNER)
192
+ await store.create("t-concierge", AGENT)
193
+ await store.create("t-billing", OTHER_AGENT)
194
+ assert {r.thread_id for r in await store.list_for(OWNER)} == {
195
+ "t-direct",
196
+ "t-concierge",
197
+ "t-billing",
198
+ }
199
+ assert [r.thread_id for r in await store.list_for(AGENT)] == ["t-concierge"]
200
+ assert [r.thread_id for r in await store.list_for(own_view(OTHER_AGENT))] == ["t-billing"]
201
+ assert own_view(AGENT).actor == AGENT.actor
202
+ # The rows keep their v0.2 shape: the owner (hashed subject), never the actor.
203
+ row = (await store.list_for(AGENT))[0].public()
204
+ assert set(row) == {"thread_id", "owner", "created_at", "updated_at"}
205
+
206
+
207
+ async def test_server_listing_of_an_agent_filters_by_its_actor() -> None:
208
+ client = _Client([])
209
+ await search_server_threads(client, AGENT, scope=SCOPE_OWN, limit=5, offset=0)
210
+ assert client.threads.searches[-1]["metadata"] == {
211
+ "principal_id": "alice@example.com",
212
+ "actor": "concierge",
213
+ }
214
+ await search_server_threads(client, OWNER, scope=SCOPE_OWN, limit=5, offset=0)
215
+ assert client.threads.searches[-1]["metadata"] == {"principal_id": "alice@example.com"}
216
+
217
+
218
+ async def test_postgres_listing_follows_the_owner_key(pg_store: ThreadStore) -> None:
219
+ await pg_store.create("t-direct", OWNER)
220
+ await pg_store.create("t-concierge", AGENT)
221
+ await pg_store.create("t-billing", OTHER_AGENT)
222
+ assert {r.thread_id for r in await pg_store.list_for(OWNER)} == {
223
+ "t-direct",
224
+ "t-concierge",
225
+ "t-billing",
226
+ }
227
+ assert [r.thread_id for r in await pg_store.list_for(AGENT)] == ["t-concierge"]
228
+ stored = await pg_store.get("t-concierge")
229
+ assert stored is not None and stored.actor == "concierge"
230
+ assert (await pg_store.ensure("t-concierge", OWNER)).actor == "concierge"
231
+ with pytest.raises(HTTPException) as exc:
232
+ await pg_store.ensure("t-concierge", OTHER_AGENT)
233
+ assert exc.value.status_code == 403
234
+
235
+
236
+ async def test_a_02_threads_table_is_upgraded_in_place(pg_store: ThreadStore) -> None:
237
+ """A v0.2 table (no `actor` column) gains it at startup; its rows read as direct."""
238
+ db = pg_store.db
239
+ await db.execute("DROP TABLE threads")
240
+ await db.execute(
241
+ "CREATE TABLE threads (thread_id TEXT PRIMARY KEY, principal_id TEXT NOT NULL, "
242
+ "tenant TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), "
243
+ "updated_at TIMESTAMPTZ NOT NULL DEFAULT now())"
244
+ )
245
+ await db.execute(
246
+ "INSERT INTO threads (thread_id, principal_id) VALUES (%s, %s)",
247
+ ("t-old", OWNER.id),
248
+ )
249
+ await db.setup()
250
+ legacy = await pg_store.get("t-old")
251
+ assert legacy is not None and legacy.actor == ""
252
+ assert [r.thread_id for r in await pg_store.list_for(OWNER)] == ["t-old"]
253
+ assert await pg_store.list_for(AGENT) == []
254
+ with pytest.raises(HTTPException):
255
+ await pg_store.ensure("t-old", AGENT)