failproofai 1.0.1 → 1.0.2-beta.0

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 (389) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +6 -6
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  6. package/.next/standalone/.next/server/app/_global-error/page.js +2 -2
  7. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  11. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
  12. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  13. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page.js +2 -2
  16. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  17. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  19. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  20. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  21. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
  22. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  23. package/.next/standalone/.next/server/app/api/audit/invite/route.js +3 -2
  24. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/audit/run/route.js +5 -4
  26. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/auth/login-request/route.js +2 -2
  29. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js +4 -3
  31. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/auth/logout/route.js +4 -4
  33. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/auth/status/route.js +4 -4
  35. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  38. package/.next/standalone/.next/server/app/audit/page.js +2 -2
  39. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  41. package/.next/standalone/.next/server/app/index.html +1 -1
  42. package/.next/standalone/.next/server/app/index.rsc +15 -15
  43. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
  44. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  45. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  46. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  47. package/.next/standalone/.next/server/app/page.js +2 -2
  48. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  49. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  50. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +66 -22
  51. package/.next/standalone/.next/server/app/policies/page.js +5 -4
  52. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  53. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  54. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  55. package/.next/standalone/.next/server/app/project/[name]/page.js +2 -2
  56. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  58. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  59. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  60. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +2 -2
  61. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  62. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  63. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  64. package/.next/standalone/.next/server/app/projects/page.js +2 -2
  65. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  66. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  67. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
  68. package/.next/standalone/.next/server/app/settings/page.js +1 -1
  69. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  70. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  71. package/.next/standalone/.next/server/chunks/{[externals]__1qzqcio._.js → [externals]__0e32-7c._.js} +1 -1
  72. package/.next/standalone/.next/server/chunks/[externals]__1s462yl._.js +3 -0
  73. package/.next/standalone/.next/server/chunks/[root-of-the-server]__01eqny5._.js +3 -0
  74. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_0xu5z._.js +1 -1
  75. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1p8poxd._.js → [root-of-the-server]__0igxt32._.js} +2 -2
  76. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0neqlu-._.js +3 -0
  77. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +3 -0
  78. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0t9oqnn._.js +3 -0
  79. package/.next/standalone/.next/server/chunks/_0bs-v8p._.js +3 -0
  80. package/.next/standalone/.next/server/chunks/_0tovk6q._.js +3 -0
  81. package/.next/standalone/.next/server/chunks/_0trp3yc._.js +3 -0
  82. package/.next/standalone/.next/server/chunks/_185gyk9._.js +3 -0
  83. package/.next/standalone/.next/server/chunks/_1ek68ln._.js +5 -6
  84. package/.next/standalone/.next/server/chunks/lib_telemetry-id_ts_1k8gj9o._.js +3 -0
  85. package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_09d5c79._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_04op_ou._.js} +1 -1
  86. package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_1d2g73o._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_0dr20uz._.js} +1 -1
  87. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  88. package/.next/standalone/.next/server/chunks/src_hooks_0xbw3bu._.js +12 -0
  89. package/.next/standalone/.next/server/chunks/src_hooks_fp-home_ts_09kv0bn._.js +1 -1
  90. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +4 -0
  91. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +4 -0
  92. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +4 -0
  93. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0spkm68._.js → [root-of-the-server]__07gm3zl._.js} +21 -20
  94. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +4 -0
  95. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +4 -0
  96. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +1 -1
  97. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +4 -0
  98. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +6 -0
  99. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__07tbbf3._.js → [root-of-the-server]__0zig5sv._.js} +2 -2
  100. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__192nhvh._.js +5 -0
  101. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +4 -0
  102. package/.next/standalone/.next/server/chunks/ssr/{_0lvyuv4._.js → _00e9tnf._.js} +2 -2
  103. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  104. package/.next/standalone/.next/server/chunks/ssr/{_0u5zauv._.js → _12l4a_k._.js} +1 -1
  105. package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +1 -1
  106. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
  107. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +13 -13
  108. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  109. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
  110. package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
  111. package/.next/standalone/.next/server/chunks/ssr/src_hooks_08o7bqo._.js +10 -0
  112. package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +1 -1
  113. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
  114. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  115. package/.next/standalone/.next/server/middleware.js +1 -1
  116. package/.next/standalone/.next/server/pages/404.html +1 -1
  117. package/.next/standalone/.next/server/pages/500.html +1 -1
  118. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  119. package/.next/standalone/.next/server/server-reference-manifest.json +71 -27
  120. package/.next/standalone/.next/static/chunks/{37fcpc36x9gpe.js → 0eqky-7n7dgdm.js} +1 -1
  121. package/.next/standalone/.next/static/chunks/{1ide1wl2f7zi1.js → 0sa9v5mhc8861.js} +13 -13
  122. package/.next/standalone/.next/static/chunks/1mfzsja6ftim5.css +2 -0
  123. package/.next/standalone/.next/static/chunks/{3k7mxbp9hiom0.js → 1nuqytbta44af.js} +1 -1
  124. package/.next/standalone/.next/static/chunks/1sl2xmn9inx3y.js +6 -0
  125. package/.next/standalone/.next/static/chunks/{2_2lhuojq2xop.js → 2aawsjld9ied_.js} +1 -1
  126. package/.next/standalone/.next/static/chunks/2gn7ut4svofm2.js +1 -0
  127. package/.next/standalone/.next/static/chunks/{31s1smp-g7gks.js → 2hs7ycvsav7v2.js} +1 -1
  128. package/.next/standalone/.next/static/chunks/{0k5050ezjvv49.js → 2i2yt3hzjredm.js} +1 -1
  129. package/.next/standalone/.next/static/chunks/{3c808qrfcq0im.js → 35xg6retpe6rp.js} +1 -1
  130. package/.next/standalone/.next/static/chunks/{2zpp1we2jv6tb.js → 3y54nw0i7dsjl.js} +1 -1
  131. package/.next/standalone/SECURITY.md +5 -4
  132. package/.next/standalone/app/actions/get-hooks-config.ts +108 -17
  133. package/.next/standalone/app/actions/pack-actions.ts +153 -0
  134. package/.next/standalone/app/audit/_components/run-progress.tsx +1 -1
  135. package/.next/standalone/app/audit/_components/share-templates.ts +13 -13
  136. package/.next/standalone/app/policies/hooks-client.tsx +320 -14
  137. package/.next/standalone/components/navbar.tsx +2 -28
  138. package/.next/standalone/components/reach-developers.tsx +8 -1
  139. package/.next/standalone/docs-old/agenteye/python-sdk-skill.mdx +1 -1
  140. package/.next/standalone/docs-old/agenteye/python-sdk.mdx +59 -35
  141. package/.next/standalone/docs-old/i18n/README.ar.md +1 -0
  142. package/.next/standalone/docs-old/i18n/README.de.md +1 -0
  143. package/.next/standalone/docs-old/i18n/README.es.md +1 -0
  144. package/.next/standalone/docs-old/i18n/README.fr.md +1 -0
  145. package/.next/standalone/docs-old/i18n/README.he.md +1 -0
  146. package/.next/standalone/docs-old/i18n/README.hi.md +1 -0
  147. package/.next/standalone/docs-old/i18n/README.it.md +1 -0
  148. package/.next/standalone/docs-old/i18n/README.ja.md +1 -0
  149. package/.next/standalone/docs-old/i18n/README.ko.md +1 -0
  150. package/.next/standalone/docs-old/i18n/README.pt-br.md +1 -0
  151. package/.next/standalone/docs-old/i18n/README.ru.md +1 -0
  152. package/.next/standalone/docs-old/i18n/README.tr.md +1 -0
  153. package/.next/standalone/docs-old/i18n/README.vi.md +1 -0
  154. package/.next/standalone/docs-old/i18n/README.zh.md +1 -0
  155. package/.next/standalone/fp-cloud-cli/CHANGELOG.md +431 -0
  156. package/.next/standalone/fp-cloud-cli/LICENSE +42 -0
  157. package/.next/standalone/fp-cloud-cli/MANIFEST.in +10 -0
  158. package/.next/standalone/fp-cloud-cli/README.md +259 -0
  159. package/.next/standalone/fp-cloud-cli/fp_cli/__init__.py +10 -0
  160. package/.next/standalone/fp-cloud-cli/fp_cli/__main__.py +4 -0
  161. package/.next/standalone/fp-cloud-cli/fp_cli/_click_compat.py +64 -0
  162. package/.next/standalone/fp-cloud-cli/fp_cli/_context.py +332 -0
  163. package/.next/standalone/fp-cloud-cli/fp_cli/_version.py +1 -0
  164. package/.next/standalone/fp-cloud-cli/fp_cli/analytics.py +432 -0
  165. package/.next/standalone/fp-cloud-cli/fp_cli/analytics_config.py +77 -0
  166. package/.next/standalone/fp-cloud-cli/fp_cli/analytics_registry.py +83 -0
  167. package/.next/standalone/fp-cloud-cli/fp_cli/app.py +492 -0
  168. package/.next/standalone/fp-cloud-cli/fp_cli/auth.py +160 -0
  169. package/.next/standalone/fp-cloud-cli/fp_cli/client.py +1694 -0
  170. package/.next/standalone/fp-cloud-cli/fp_cli/commands/__init__.py +0 -0
  171. package/.next/standalone/fp-cloud-cli/fp_cli/commands/_write.py +214 -0
  172. package/.next/standalone/fp-cloud-cli/fp_cli/commands/agent_cmds.py +407 -0
  173. package/.next/standalone/fp-cloud-cli/fp_cli/commands/alerts_cmds.py +445 -0
  174. package/.next/standalone/fp-cloud-cli/fp_cli/commands/audits_cmds.py +1054 -0
  175. package/.next/standalone/fp-cloud-cli/fp_cli/commands/auth_cmds.py +512 -0
  176. package/.next/standalone/fp-cloud-cli/fp_cli/commands/errors_cmds.py +190 -0
  177. package/.next/standalone/fp-cloud-cli/fp_cli/commands/evals_cmds.py +161 -0
  178. package/.next/standalone/fp-cloud-cli/fp_cli/commands/events_cmds.py +159 -0
  179. package/.next/standalone/fp-cloud-cli/fp_cli/commands/fleet_cmds.py +416 -0
  180. package/.next/standalone/fp-cloud-cli/fp_cli/commands/guardrails_cmds.py +148 -0
  181. package/.next/standalone/fp-cloud-cli/fp_cli/commands/incidents_cmds.py +472 -0
  182. package/.next/standalone/fp-cloud-cli/fp_cli/commands/keys_cmds.py +407 -0
  183. package/.next/standalone/fp-cloud-cli/fp_cli/commands/list_cmds.py +63 -0
  184. package/.next/standalone/fp-cloud-cli/fp_cli/commands/orgs_cmds.py +319 -0
  185. package/.next/standalone/fp-cloud-cli/fp_cli/commands/policies_cmds.py +499 -0
  186. package/.next/standalone/fp-cloud-cli/fp_cli/commands/queries_cmds.py +378 -0
  187. package/.next/standalone/fp-cloud-cli/fp_cli/commands/sessions_cmds.py +151 -0
  188. package/.next/standalone/fp-cloud-cli/fp_cli/commands/settings_cmds.py +150 -0
  189. package/.next/standalone/fp-cloud-cli/fp_cli/commands/usage_cmds.py +35 -0
  190. package/.next/standalone/fp-cloud-cli/fp_cli/commands/users_cmds.py +404 -0
  191. package/.next/standalone/fp-cloud-cli/fp_cli/config.py +330 -0
  192. package/.next/standalone/fp-cloud-cli/fp_cli/dates.py +78 -0
  193. package/.next/standalone/fp-cloud-cli/fp_cli/enforcement.py +345 -0
  194. package/.next/standalone/fp-cloud-cli/fp_cli/errors.py +98 -0
  195. package/.next/standalone/fp-cloud-cli/fp_cli/models.py +891 -0
  196. package/.next/standalone/fp-cloud-cli/fp_cli/orgs.py +30 -0
  197. package/.next/standalone/fp-cloud-cli/fp_cli/output.py +6593 -0
  198. package/.next/standalone/fp-cloud-cli/fp_cli/permissions.py +208 -0
  199. package/.next/standalone/fp-cloud-cli/fp_cli/policy_check.py +290 -0
  200. package/.next/standalone/fp-cloud-cli/fp_cli/py.typed +0 -0
  201. package/.next/standalone/fp-cloud-cli/fp_cli/select.py +322 -0
  202. package/.next/standalone/fp-cloud-cli/fp_cli/theme.py +53 -0
  203. package/.next/standalone/fp-cloud-cli/pyproject.toml +106 -0
  204. package/.next/standalone/fp-cloud-cli/skill/SKILL.md +302 -0
  205. package/.next/standalone/fp-cloud-cli/skill/agents/openai.yaml +8 -0
  206. package/.next/standalone/fp-cloud-cli/skill/references/commands.md +254 -0
  207. package/.next/standalone/fp-cloud-cli/tests/__init__.py +0 -0
  208. package/.next/standalone/fp-cloud-cli/tests/conftest.py +92 -0
  209. package/.next/standalone/fp-cloud-cli/tests/test_alerting.py +644 -0
  210. package/.next/standalone/fp-cloud-cli/tests/test_analytics.py +385 -0
  211. package/.next/standalone/fp-cloud-cli/tests/test_audits.py +950 -0
  212. package/.next/standalone/fp-cloud-cli/tests/test_auth.py +98 -0
  213. package/.next/standalone/fp-cloud-cli/tests/test_auth_mode.py +272 -0
  214. package/.next/standalone/fp-cloud-cli/tests/test_click_compat.py +118 -0
  215. package/.next/standalone/fp-cloud-cli/tests/test_client.py +299 -0
  216. package/.next/standalone/fp-cloud-cli/tests/test_commands.py +814 -0
  217. package/.next/standalone/fp-cloud-cli/tests/test_config.py +99 -0
  218. package/.next/standalone/fp-cloud-cli/tests/test_dashboards_agent.py +340 -0
  219. package/.next/standalone/fp-cloud-cli/tests/test_dates.py +76 -0
  220. package/.next/standalone/fp-cloud-cli/tests/test_enforcement_logic.py +481 -0
  221. package/.next/standalone/fp-cloud-cli/tests/test_facets.py +170 -0
  222. package/.next/standalone/fp-cloud-cli/tests/test_failproofai_home.py +722 -0
  223. package/.next/standalone/fp-cloud-cli/tests/test_fp_home_contract.py +198 -0
  224. package/.next/standalone/fp-cloud-cli/tests/test_hardening.py +186 -0
  225. package/.next/standalone/fp-cloud-cli/tests/test_help_table_coverage.py +127 -0
  226. package/.next/standalone/fp-cloud-cli/tests/test_keys_queries.py +496 -0
  227. package/.next/standalone/fp-cloud-cli/tests/test_list.py +59 -0
  228. package/.next/standalone/fp-cloud-cli/tests/test_multivalue.py +183 -0
  229. package/.next/standalone/fp-cloud-cli/tests/test_no_customer_identifiers.py +168 -0
  230. package/.next/standalone/fp-cloud-cli/tests/test_operator.py +492 -0
  231. package/.next/standalone/fp-cloud-cli/tests/test_orgs.py +692 -0
  232. package/.next/standalone/fp-cloud-cli/tests/test_output.py +1732 -0
  233. package/.next/standalone/fp-cloud-cli/tests/test_policy_check.py +191 -0
  234. package/.next/standalone/fp-cloud-cli/tests/test_readme_matches_reality.py +125 -0
  235. package/.next/standalone/fp-cloud-cli/tests/test_review_fixes.py +161 -0
  236. package/.next/standalone/fp-cloud-cli/tests/test_telemetry_completeness.py +90 -0
  237. package/.next/standalone/fp-cloud-cli/tests/test_usage.py +115 -0
  238. package/.next/standalone/fp-cloud-cli/tests/test_v1_origin_diagnostic.py +70 -0
  239. package/.next/standalone/fp-cloud-cli/tests/test_v1_routing.py +214 -0
  240. package/.next/standalone/fp-cloud-cli/tests/test_whoami.py +104 -0
  241. package/.next/standalone/fp-cloud-cli/uv.lock +552 -0
  242. package/.next/standalone/package.json +29 -22
  243. package/.next/standalone/sdk/python/CHANGELOG.md +70 -0
  244. package/.next/standalone/sdk/python/LICENSE +42 -0
  245. package/.next/standalone/sdk/python/MANIFEST.in +21 -0
  246. package/.next/standalone/sdk/python/README.md +466 -0
  247. package/.next/standalone/sdk/python/docs/README.md +249 -0
  248. package/.next/standalone/sdk/python/docs/_shared/README.md +43 -0
  249. package/.next/standalone/sdk/python/docs/_shared/__init__.py +195 -0
  250. package/.next/standalone/sdk/python/docs/crewai/README.md +328 -0
  251. package/.next/standalone/sdk/python/docs/crewai/examples/quickstart.py +61 -0
  252. package/.next/standalone/sdk/python/docs/crewai/examples/research_crew.py +92 -0
  253. package/.next/standalone/sdk/python/docs/langgraph/README.md +481 -0
  254. package/.next/standalone/sdk/python/docs/langgraph/examples/quickstart.py +51 -0
  255. package/.next/standalone/sdk/python/docs/langgraph/examples/research_agent.py +86 -0
  256. package/.next/standalone/sdk/python/docs/langgraph/examples/supervisor_handoff.py +83 -0
  257. package/.next/standalone/sdk/python/docs/llama_index/README.md +332 -0
  258. package/.next/standalone/sdk/python/docs/llama_index/examples/quickstart.py +62 -0
  259. package/.next/standalone/sdk/python/docs/llama_index/examples/research_agent.py +93 -0
  260. package/.next/standalone/sdk/python/docs/manual/README.md +491 -0
  261. package/.next/standalone/sdk/python/docs/manual/examples/quickstart.py +57 -0
  262. package/.next/standalone/sdk/python/docs/manual/examples/research_agent.py +133 -0
  263. package/.next/standalone/sdk/python/docs/pydantic_ai/README.md +323 -0
  264. package/.next/standalone/sdk/python/docs/pydantic_ai/examples/quickstart.py +48 -0
  265. package/.next/standalone/sdk/python/docs/pydantic_ai/examples/research_agent.py +94 -0
  266. package/.next/standalone/sdk/python/failproofai_sdk/__init__.py +131 -0
  267. package/.next/standalone/sdk/python/failproofai_sdk/_context.py +218 -0
  268. package/.next/standalone/sdk/python/failproofai_sdk/_environment.py +78 -0
  269. package/.next/standalone/sdk/python/failproofai_sdk/_events.py +752 -0
  270. package/.next/standalone/sdk/python/failproofai_sdk/_resolver.py +118 -0
  271. package/.next/standalone/sdk/python/failproofai_sdk/_runtime.py +21 -0
  272. package/.next/standalone/sdk/python/failproofai_sdk/_schema.py +317 -0
  273. package/.next/standalone/sdk/python/failproofai_sdk/_scopes.py +422 -0
  274. package/.next/standalone/sdk/python/failproofai_sdk/_version.py +1 -0
  275. package/.next/standalone/sdk/python/failproofai_sdk/_writer.py +750 -0
  276. package/.next/standalone/sdk/python/failproofai_sdk/integrations/__init__.py +274 -0
  277. package/.next/standalone/sdk/python/failproofai_sdk/integrations/_compat.py +247 -0
  278. package/.next/standalone/sdk/python/failproofai_sdk/integrations/_core.py +1076 -0
  279. package/.next/standalone/sdk/python/failproofai_sdk/integrations/crewai.py +1669 -0
  280. package/.next/standalone/sdk/python/failproofai_sdk/integrations/langchain.py +2151 -0
  281. package/.next/standalone/sdk/python/failproofai_sdk/integrations/llama_index.py +1588 -0
  282. package/.next/standalone/sdk/python/failproofai_sdk/integrations/pydantic_ai.py +977 -0
  283. package/.next/standalone/sdk/python/failproofai_sdk/py.typed +0 -0
  284. package/.next/standalone/sdk/python/pyproject.toml +92 -0
  285. package/.next/standalone/sdk/python/skill/SKILL.md +399 -0
  286. package/.next/standalone/sdk/python/skill/agents/openai.yaml +8 -0
  287. package/.next/standalone/sdk/python/skill/references/events.md +234 -0
  288. package/.next/standalone/sdk/python/skill/references/frameworks.md +314 -0
  289. package/.next/standalone/sdk/python/skill/references/install.md +100 -0
  290. package/.next/standalone/sdk/python/skill/references/integration.md +352 -0
  291. package/.next/standalone/sdk/python/tests/__init__.py +0 -0
  292. package/.next/standalone/sdk/python/tests/conftest.py +169 -0
  293. package/.next/standalone/sdk/python/tests/integrations/__init__.py +6 -0
  294. package/.next/standalone/sdk/python/tests/integrations/test_crewai.py +1299 -0
  295. package/.next/standalone/sdk/python/tests/integrations/test_langchain.py +2338 -0
  296. package/.next/standalone/sdk/python/tests/integrations/test_llama_index.py +1576 -0
  297. package/.next/standalone/sdk/python/tests/integrations/test_pydantic_ai.py +1102 -0
  298. package/.next/standalone/sdk/python/tests/test_context.py +410 -0
  299. package/.next/standalone/sdk/python/tests/test_docs.py +411 -0
  300. package/.next/standalone/sdk/python/tests/test_durability.py +1352 -0
  301. package/.next/standalone/sdk/python/tests/test_encoding.py +571 -0
  302. package/.next/standalone/sdk/python/tests/test_integrations.py +1127 -0
  303. package/.next/standalone/sdk/python/tests/test_no_customer_identifiers.py +214 -0
  304. package/.next/standalone/sdk/python/tests/test_packaging.py +185 -0
  305. package/.next/standalone/sdk/python/tests/test_resolver_umbrella.py +178 -0
  306. package/.next/standalone/sdk/python/tests/test_scopes.py +487 -0
  307. package/.next/standalone/sdk/python/tests/test_sdk.py +923 -0
  308. package/.next/standalone/sdk/python/tests/test_server_contract.py +650 -0
  309. package/.next/standalone/sdk/python/tests/test_site_docs.py +371 -0
  310. package/.next/standalone/sdk/python/tests/test_skill_snippets.py +165 -0
  311. package/.next/standalone/sdk/python/tests/test_spool_contract.py +287 -0
  312. package/.next/standalone/sdk/python/tests/test_spool_creation.py +360 -0
  313. package/.next/standalone/sdk/python/tests/test_wire_format.py +279 -0
  314. package/.next/standalone/sdk/python/tests/test_zero_dependencies.py +378 -0
  315. package/.next/standalone/sdk/python/uv.lock +6005 -0
  316. package/.next/standalone/server.js +1 -1
  317. package/README.md +77 -21
  318. package/bin/failproofai.mjs +145 -11
  319. package/dist/cli.mjs +4482 -2205
  320. package/dist/index.js +5 -5
  321. package/dist/worker.mjs +2399 -1978
  322. package/package.json +29 -22
  323. package/policy-pack/SHA256SUMS +2 -0
  324. package/policy-pack/failproofai-pack.json +732 -0
  325. package/policy-pack/failproofai-pack.mjs +2017 -0
  326. package/scripts/build-policy-pack.mjs +121 -0
  327. package/scripts/changelog-section.py +77 -0
  328. package/scripts/prune-standalone.mjs +3 -0
  329. package/scripts/python-version.py +232 -0
  330. package/scripts/translate-docs/cli.ts +21 -0
  331. package/scripts/translate-docs/mintlify-nav.ts +85 -13
  332. package/scripts/translate-docs/translator.ts +66 -11
  333. package/src/audit/cache.ts +40 -3
  334. package/src/audit/cli.ts +1 -1
  335. package/src/audit/index.ts +1 -1
  336. package/src/audit/replay.ts +89 -3
  337. package/src/audit/report.ts +5 -5
  338. package/src/audit/schedule-cli.ts +49 -53
  339. package/src/hooks/builtin-policies.ts +217 -579
  340. package/src/hooks/cloud-enrollment-cli.ts +66 -27
  341. package/src/hooks/cloud-managed-policies.ts +6 -1
  342. package/src/hooks/configure-wizard.ts +1 -1
  343. package/src/hooks/custom-hooks-loader.ts +89 -6
  344. package/src/hooks/fp-home.ts +92 -4
  345. package/src/hooks/fp-reset.ts +26 -2
  346. package/src/hooks/handler.ts +181 -11
  347. package/src/hooks/harness-cli.ts +79 -25
  348. package/src/hooks/hook-activity-store.ts +13 -3
  349. package/src/hooks/install-prompt.ts +25 -3
  350. package/src/hooks/manager.ts +418 -172
  351. package/src/hooks/migrations.ts +2 -2
  352. package/src/hooks/pack-cli.ts +505 -0
  353. package/src/hooks/pack-failclosed.ts +189 -0
  354. package/src/hooks/pack-manifest.ts +344 -0
  355. package/src/hooks/pack-store.ts +930 -0
  356. package/src/hooks/policy-catalog.ts +535 -0
  357. package/src/hooks/policy-evaluator.ts +24 -16
  358. package/src/hooks/policy-presets.ts +4 -2
  359. package/src/hooks/policy-registry.ts +8 -2
  360. package/src/hooks/policy-types.ts +26 -0
  361. package/src/hooks/session-pause-cli.ts +33 -9
  362. package/src/hooks/tui.ts +570 -0
  363. package/src/hooks/types.ts +15 -1
  364. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_usx0j._.js +0 -3
  365. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_zk340._.js +0 -3
  366. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0agdw2a._.js +0 -3
  367. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0d1u_na._.js +0 -3
  368. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0oy4kxb._.js +0 -3
  369. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zf034w._.js +0 -3
  370. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1tvn4wf._.js +0 -3
  371. package/.next/standalone/.next/server/chunks/_0lxbzdq._.js +0 -3
  372. package/.next/standalone/.next/server/chunks/_1ger0a2._.js +0 -3
  373. package/.next/standalone/.next/server/chunks/_1zuiiy3._.js +0 -3
  374. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_1iiwm5e._.js +0 -3
  375. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0kc8_v5._.js +0 -4
  376. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0kgy9da._.js +0 -4
  377. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0mhqk5a._.js +0 -4
  378. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0pltpn8._.js +0 -4
  379. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qkcyf-._.js +0 -4
  380. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0w0x_ok._.js +0 -4
  381. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1_dfocn._.js +0 -4
  382. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1bfex62._.js +0 -5
  383. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1kzapg5._.js +0 -6
  384. package/.next/standalone/.next/static/chunks/36fw6yk1s6x_f.js +0 -1
  385. package/.next/standalone/.next/static/chunks/3tzq0260n17ae.js +0 -6
  386. package/.next/standalone/.next/static/chunks/3wq9x0c-7ld5d.css +0 -2
  387. /package/.next/standalone/.next/static/{ZklXxyA5ygsWSQz2u_zii → nwbyWT06Ud0XdXOrVJyvS}/_buildManifest.js +0 -0
  388. /package/.next/standalone/.next/static/{ZklXxyA5ygsWSQz2u_zii → nwbyWT06Ud0XdXOrVJyvS}/_clientMiddlewareManifest.js +0 -0
  389. /package/.next/standalone/.next/static/{ZklXxyA5ygsWSQz2u_zii → nwbyWT06Ud0XdXOrVJyvS}/_ssgManifest.js +0 -0
@@ -0,0 +1,1127 @@
1
+ """The integrations scaffolding, exercised without any framework installed.
2
+
3
+ Everything here runs against a **fake** adapter registered into the real
4
+ registry, and `sys.modules` stubs for the patch mechanics. That is deliberate:
5
+ the registry, the failure policy and the install/uninstall discipline are the
6
+ parts that must be reviewable on their own, before any framework-specific code
7
+ exists to hide behind.
8
+
9
+ Two properties are only testable because `FAILPROOFAI_SDK_STRICT` exists. Without it
10
+ you can prove "the customer's call still worked", but never "we swallowed the
11
+ right thing" — and an adapter that swallows `BaseException` passes the first
12
+ test while silently breaking cancellation in every async app that installs it.
13
+ """
14
+
15
+ import dataclasses
16
+ import collections
17
+ import logging
18
+ import sys
19
+ import threading
20
+ import types
21
+ import warnings
22
+
23
+ import pytest
24
+
25
+ import failproofai_sdk
26
+ from failproofai_sdk import integrations
27
+ from failproofai_sdk.integrations import _compat, _core
28
+
29
+ INTEGRATIONS_LOGGER = "failproofai_sdk.integrations"
30
+
31
+
32
+ @pytest.fixture(autouse=True)
33
+ def _integration_state(monkeypatch):
34
+ """Both strict flags off, no degraded sites, no already-shown warnings.
35
+
36
+ All of this is process-global by design (a degraded hook must stay degraded
37
+ for the life of the process), so it has to be reset around every test or the
38
+ order of the file changes its result.
39
+ """
40
+ monkeypatch.delenv("FAILPROOFAI_SDK_STRICT", raising=False)
41
+ monkeypatch.delenv("FAILPROOFAI_SDK_STRICT_INTEGRATIONS", raising=False)
42
+ _reset_flags()
43
+ yield
44
+ _reset_flags()
45
+
46
+
47
+ def _reset_flags():
48
+ _core.set_strict(None)
49
+ _compat.set_strict_integrations(None)
50
+ _core.reset_failures()
51
+ _compat.reset_warnings()
52
+
53
+
54
+ def strict_on(monkeypatch, var="FAILPROOFAI_SDK_STRICT"):
55
+ monkeypatch.setenv(var, "1")
56
+ _core.set_strict(None)
57
+ _compat.set_strict_integrations(None)
58
+
59
+
60
+ class FakeAdapter:
61
+ """The shape `failproofai_sdk/integrations/<framework>.py` has to implement."""
62
+
63
+ def __init__(self, name="fake", module="fakeframework", fail_install=False):
64
+ self.name = name
65
+ self.module = module
66
+ self.fail_install = fail_install
67
+ self.installs = 0
68
+ self.uninstalls = 0
69
+ self.options = None
70
+
71
+ def install(self, **options):
72
+ self.installs += 1
73
+ self.options = options
74
+ if self.fail_install:
75
+ raise RuntimeError("adapter install exploded")
76
+
77
+ def uninstall(self):
78
+ self.uninstalls += 1
79
+
80
+
81
+ def register(monkeypatch, adapter, *, name=None, detect=None):
82
+ """Put an adapter into the real registry for the duration of one test."""
83
+ name = name or adapter.name
84
+ module_path = f"agenteye_fake_{name}"
85
+ module = types.ModuleType(module_path)
86
+ module.adapter = adapter
87
+ monkeypatch.setitem(sys.modules, module_path, module)
88
+ monkeypatch.setitem(integrations._REGISTRY, name, module_path)
89
+ monkeypatch.setitem(integrations._DETECT, name, detect or (adapter.module,))
90
+ return adapter
91
+
92
+
93
+ @pytest.fixture()
94
+ def fake(monkeypatch):
95
+ adapter = register(monkeypatch, FakeAdapter())
96
+ yield adapter
97
+ # Before monkeypatch unwinds the registry, so _ACTIVE cannot keep a
98
+ # reference to a test-scoped adapter.
99
+ integrations.uninstrument()
100
+
101
+
102
+ # ---------------------------------------------------------------------------
103
+ # registry
104
+ # ---------------------------------------------------------------------------
105
+
106
+ def test_the_four_adapters_are_pre_registered():
107
+ # Registered before their modules exist so that adding an adapter is one new
108
+ # file, never an edit to the registry (which four agents would conflict on).
109
+ assert integrations.available() == ("crewai", "langchain", "llama_index", "pydantic_ai")
110
+
111
+
112
+ @pytest.mark.parametrize(
113
+ "spelling,expected",
114
+ [
115
+ ("langgraph", "langchain"),
116
+ ("LangGraph", "langchain"),
117
+ ("llamaindex", "llama_index"),
118
+ ("llama-index", "llama_index"),
119
+ ("pydantic-ai", "pydantic_ai"),
120
+ (" crewai ", "crewai"),
121
+ ],
122
+ )
123
+ def test_aliases_resolve(spelling, expected):
124
+ assert integrations._canonical(spelling) == expected
125
+
126
+
127
+ def test_unknown_name_raises_valueerror_listing_the_valid_ones():
128
+ # A typo that silently records nothing is the worst outcome available.
129
+ with pytest.raises(ValueError) as excinfo:
130
+ integrations.instrument("langhcain")
131
+ message = str(excinfo.value)
132
+ assert "langhcain" in message
133
+ for name in ("langchain", "crewai", "llama_index", "pydantic_ai"):
134
+ assert name in message
135
+
136
+
137
+ def test_instrument_installs_and_reports_the_name(fake):
138
+ assert integrations.instrument("fake") == ("fake",)
139
+ assert fake.installs == 1
140
+ assert integrations.active() == ("fake",)
141
+
142
+
143
+ def test_the_public_facade_reaches_the_registry(fake):
144
+ # `failproofai_sdk.instrument` imports this package inside the function body, so
145
+ # this is also the test that the lazy wiring is connected at all.
146
+ assert failproofai_sdk.instrument("fake") == ("fake",)
147
+ assert failproofai_sdk.uninstrument("fake") == ("fake",)
148
+ with pytest.raises(ValueError):
149
+ failproofai_sdk.instrument("bogus")
150
+
151
+
152
+ def test_options_reach_the_adapter(fake):
153
+ integrations.instrument("fake", session_id="s-1", capture_inputs=False)
154
+ assert fake.options == {"session_id": "s-1", "capture_inputs": False}
155
+
156
+
157
+ def test_instrumenting_twice_is_a_noop(fake):
158
+ assert integrations.instrument("fake") == ("fake",)
159
+ assert integrations.instrument("fake") == ()
160
+ assert fake.installs == 1
161
+
162
+
163
+ def test_uninstrument_calls_uninstall_once(fake):
164
+ integrations.instrument("fake")
165
+ assert integrations.uninstrument("fake") == ("fake",)
166
+ assert fake.uninstalls == 1
167
+ assert integrations.active() == ()
168
+
169
+
170
+ def test_uninstrument_of_something_never_instrumented_is_a_noop(fake):
171
+ assert integrations.uninstrument("fake") == ()
172
+ assert fake.uninstalls == 0
173
+
174
+
175
+ def test_uninstrument_of_an_unknown_name_does_not_raise(caplog):
176
+ # Teardown that can fail is teardown people stop calling.
177
+ with caplog.at_level(logging.WARNING, logger=INTEGRATIONS_LOGGER):
178
+ assert integrations.uninstrument("nope") == ()
179
+ assert "nope" in caplog.text
180
+
181
+
182
+ def test_bare_instrument_with_nothing_imported_warns_rather_than_going_quiet(
183
+ monkeypatch, caplog
184
+ ):
185
+ # The import-order mistake — calling instrument() above the `import
186
+ # langchain` line — instruments nothing and is otherwise indistinguishable
187
+ # from working: no exception, no events, adapter "installed". The message
188
+ # naming the fix existed at debug level, where no default logging config
189
+ # shows it, so the one mistake that costs a user all of their telemetry was
190
+ # the one mistake the SDK said nothing about.
191
+ #
192
+ # Detection is pointed at modules that cannot be imported, rather than the
193
+ # registry being emptied: tests/integrations/ runs first and imports all
194
+ # four frameworks, so without this `instrument()` would install them for
195
+ # real and leak `_ACTIVE` into every test after this one — but an empty
196
+ # registry would also render the message's list of valid names as nothing,
197
+ # which is the half of it most worth asserting.
198
+ monkeypatch.setattr(
199
+ integrations,
200
+ "_DETECT",
201
+ {name: ("failproofai_sdk_no_such_framework",) for name in integrations._REGISTRY},
202
+ )
203
+ with caplog.at_level(logging.WARNING, logger=INTEGRATIONS_LOGGER):
204
+ assert integrations.instrument() == ()
205
+ assert "NOTHING was instrumented" in caplog.text
206
+ assert [r for r in caplog.records if r.levelno >= logging.WARNING]
207
+ # It must name what this call WOULD have accepted. Suggesting one hardcoded
208
+ # framework leaves a reader who is not using it unsure whether the message
209
+ # is a suggestion or a diagnosis.
210
+ for name in integrations._REGISTRY:
211
+ assert f"instrument({name!r})" in caplog.text
212
+
213
+
214
+ def test_uninstrument_all_removes_everything(monkeypatch):
215
+ one = register(monkeypatch, FakeAdapter(name="fake1", module="fw1"))
216
+ two = register(monkeypatch, FakeAdapter(name="fake2", module="fw2"))
217
+ integrations.instrument("fake1")
218
+ integrations.instrument("fake2")
219
+ assert sorted(integrations.uninstrument()) == ["fake1", "fake2"]
220
+ assert one.uninstalls == 1 and two.uninstalls == 1
221
+ assert integrations.active() == ()
222
+
223
+
224
+ def test_a_failing_install_is_skipped_and_the_others_still_install(monkeypatch, caplog):
225
+ broken = register(monkeypatch, FakeAdapter(name="broken", module="fw1", fail_install=True))
226
+ good = register(monkeypatch, FakeAdapter(name="good", module="fw2"))
227
+ try:
228
+ with caplog.at_level(logging.WARNING, logger=INTEGRATIONS_LOGGER):
229
+ assert integrations.instrument("broken") == ()
230
+ assert integrations.instrument("good") == ("good",)
231
+ assert broken.installs == 1 and good.installs == 1
232
+ assert integrations.active() == ("good",)
233
+ assert "broken" in caplog.text
234
+ finally:
235
+ integrations.uninstrument()
236
+
237
+
238
+ def test_a_failing_install_raises_under_strict(monkeypatch):
239
+ register(monkeypatch, FakeAdapter(name="broken", module="fw1", fail_install=True))
240
+ strict_on(monkeypatch)
241
+ with pytest.raises(RuntimeError, match="exploded"):
242
+ integrations.instrument("broken")
243
+ assert integrations.active() == ()
244
+
245
+
246
+ def test_an_adapter_missing_install_is_rejected(monkeypatch, caplog):
247
+ register(monkeypatch, types.SimpleNamespace(name="halfbaked", module="fw"), name="halfbaked")
248
+ with caplog.at_level(logging.WARNING, logger=INTEGRATIONS_LOGGER):
249
+ assert integrations.instrument("halfbaked") == ()
250
+ assert "halfbaked" in caplog.text
251
+
252
+
253
+ # ---------------------------------------------------------------------------
254
+ # auto-detection
255
+ # ---------------------------------------------------------------------------
256
+
257
+ def test_autodetect_ignores_a_framework_that_is_not_imported(fake):
258
+ assert "fakeframework" not in sys.modules
259
+ assert "fake" not in integrations.instrument()
260
+
261
+
262
+ def test_autodetect_picks_up_a_framework_that_is_imported(fake, monkeypatch):
263
+ monkeypatch.setitem(sys.modules, "fakeframework", types.ModuleType("fakeframework"))
264
+ assert "fake" in integrations.instrument()
265
+ assert fake.installs == 1
266
+
267
+
268
+ def test_autodetect_ignores_an_installed_but_unimported_framework(fake, monkeypatch, tmp_path):
269
+ """The test that actually separates `sys.modules` from `find_spec`.
270
+
271
+ A framework merely *installed* must not be instrumented — the earlier test
272
+ passes against a `find_spec` implementation too, because a stub in
273
+ `sys.modules` has no spec to find. Here the module is genuinely importable
274
+ and genuinely not imported, which is the common case on any machine with
275
+ more than one framework in its virtualenv.
276
+ """
277
+ import importlib.util
278
+
279
+ (tmp_path / "unimported_framework.py").write_text("raise AssertionError('imported!')\n")
280
+ monkeypatch.syspath_prepend(str(tmp_path))
281
+ monkeypatch.setitem(integrations._DETECT, "fake", ("unimported_framework",))
282
+
283
+ assert importlib.util.find_spec("unimported_framework") is not None
284
+ assert "unimported_framework" not in sys.modules
285
+
286
+ assert "fake" not in integrations.instrument()
287
+ assert fake.installs == 0
288
+ assert "unimported_framework" not in sys.modules
289
+
290
+
291
+ def test_autodetect_never_imports_the_framework(fake, monkeypatch):
292
+ """`sys.modules`, not `find_spec`.
293
+
294
+ `find_spec` would say "installed" for a framework the user has not imported,
295
+ and acting on that means charging 200ms of transitive imports to a library
296
+ that was supposed to be invisible.
297
+ """
298
+ imported = []
299
+ real_import_module = integrations.importlib.import_module
300
+
301
+ def spy(name, *args, **kwargs):
302
+ imported.append(name)
303
+ return real_import_module(name, *args, **kwargs)
304
+
305
+ monkeypatch.setattr(integrations.importlib, "import_module", spy)
306
+ integrations.instrument()
307
+ assert "fakeframework" not in imported
308
+
309
+
310
+ # ---------------------------------------------------------------------------
311
+ # install / uninstall discipline
312
+ # ---------------------------------------------------------------------------
313
+
314
+ def original_function(x):
315
+ return x * 2
316
+
317
+
318
+ def test_uninstall_restores_the_exact_original_object():
319
+ # `is`, not `==`. Restoring by re-importing hands back whatever the current
320
+ # value is, which is how two instrumentation libraries un-patch each other.
321
+ holder = types.SimpleNamespace(fn=original_function)
322
+ patcher = _core.Patcher()
323
+ patcher.patch(holder, "fn", _core.wrap_callable(holder.fn, before=lambda *a, **k: None))
324
+ assert holder.fn is not original_function
325
+ assert holder.fn(3) == 6
326
+ patcher.restore_all()
327
+ assert holder.fn is original_function
328
+
329
+
330
+ def test_patching_an_absent_attribute_removes_it_again():
331
+ holder = types.SimpleNamespace()
332
+ patcher = _core.Patcher()
333
+ patcher.patch(holder, "hook", lambda: None)
334
+ assert hasattr(holder, "hook")
335
+ patcher.restore_all()
336
+ assert not hasattr(holder, "hook")
337
+
338
+
339
+ def test_uninstall_declines_when_a_third_party_patched_on_top(caplog):
340
+ holder = types.SimpleNamespace(fn=original_function)
341
+ patcher = _core.Patcher()
342
+ patcher.patch(holder, "fn", _core.wrap_callable(holder.fn, before=lambda *a, **k: None))
343
+
344
+ def third_party(x):
345
+ return x
346
+
347
+ holder.fn = third_party # somebody else instrumented after us
348
+
349
+ with caplog.at_level(logging.WARNING, logger=INTEGRATIONS_LOGGER):
350
+ patcher.restore_all()
351
+
352
+ # Restoring here would silently delete their patch.
353
+ assert holder.fn is third_party
354
+ assert "not restoring" in caplog.text
355
+
356
+
357
+ def test_every_wrapper_carries_the_original():
358
+ wrapped = _core.wrap_callable(original_function, before=lambda *a, **k: None)
359
+ assert _core.is_wrapped(wrapped)
360
+ assert _core.unwrap(wrapped) is original_function
361
+ assert wrapped.__name__ == "original_function"
362
+ assert _core.unwrap(original_function) is original_function
363
+
364
+
365
+ # ---------------------------------------------------------------------------
366
+ # failure policy
367
+ # ---------------------------------------------------------------------------
368
+
369
+ def test_a_raising_hook_leaves_the_call_untouched_and_logs_once(caplog):
370
+ def before(*args, **kwargs):
371
+ raise RuntimeError("hook boom")
372
+
373
+ wrapped = _core.wrap_callable(original_function, before=before)
374
+ with caplog.at_level(logging.WARNING, logger=INTEGRATIONS_LOGGER):
375
+ assert wrapped(21) == 42
376
+ warnings_logged = [r for r in caplog.records if r.levelno == logging.WARNING]
377
+ assert len(warnings_logged) == 1
378
+ assert warnings_logged[0].exc_info is not None
379
+
380
+
381
+ def test_a_raising_hook_reraises_under_strict(monkeypatch):
382
+ def before(*args, **kwargs):
383
+ raise RuntimeError("hook boom")
384
+
385
+ wrapped = _core.wrap_callable(original_function, before=before)
386
+ strict_on(monkeypatch)
387
+ with pytest.raises(RuntimeError, match="hook boom"):
388
+ wrapped(21)
389
+
390
+
391
+ def test_a_hot_failing_hook_is_logged_once_then_disabled(caplog):
392
+ calls = []
393
+
394
+ def before(*args, **kwargs):
395
+ calls.append(1)
396
+ raise RuntimeError("boom")
397
+
398
+ wrapped = _core.wrap_callable(original_function, before=before)
399
+ with caplog.at_level(logging.DEBUG, logger=INTEGRATIONS_LOGGER):
400
+ for _ in range(10):
401
+ assert wrapped(1) == 2
402
+
403
+ # Stops being called at all: a broken adapter costs one log line, not 40%
404
+ # of the process.
405
+ assert len(calls) == _core._MAX_FAILURES
406
+ assert len([r for r in caplog.records if r.levelno == logging.WARNING]) == 1
407
+ assert len([r for r in caplog.records if r.levelno == logging.ERROR]) == 1
408
+
409
+
410
+ def test_safe_does_not_swallow_baseexception():
411
+ """CancelledError is a BaseException, and eating it breaks cancellation.
412
+
413
+ A `safe()` that caught `BaseException` would pass every other test in this
414
+ file while silently making every instrumented async application unkillable.
415
+ """
416
+
417
+ @_core.safe
418
+ def handler():
419
+ raise KeyboardInterrupt("ctrl-c")
420
+
421
+ with pytest.raises(KeyboardInterrupt):
422
+ handler()
423
+
424
+
425
+ def test_safe_swallows_exception_and_returns_none(caplog):
426
+ @_core.safe
427
+ def handler():
428
+ raise ValueError("nope")
429
+
430
+ with caplog.at_level(logging.WARNING, logger=INTEGRATIONS_LOGGER):
431
+ assert handler() is None
432
+ assert caplog.records
433
+
434
+
435
+ def test_the_structural_guarantee_return_value():
436
+ """before AND after both raising still returns the original's value."""
437
+ sentinel = object()
438
+
439
+ def boom(*args, **kwargs):
440
+ raise RuntimeError("hook")
441
+
442
+ wrapped = _core.wrap_callable(lambda: sentinel, before=boom, after=boom)
443
+ assert wrapped() is sentinel
444
+
445
+
446
+ def test_the_structural_guarantee_exception_identity():
447
+ """The original exception object comes back out unchanged."""
448
+ original_exc = ValueError("the real failure")
449
+
450
+ def raiser():
451
+ raise original_exc
452
+
453
+ def boom(*args, **kwargs):
454
+ raise RuntimeError("hook")
455
+
456
+ wrapped = _core.wrap_callable(raiser, before=boom, after=boom, on_error=boom)
457
+ with pytest.raises(ValueError) as excinfo:
458
+ wrapped()
459
+ # `is`, not a message match: a wrapper that re-raised a copy would break
460
+ # `except MyError as e: e.retry_after`.
461
+ assert excinfo.value is original_exc
462
+
463
+
464
+ def test_on_error_sees_the_exception_and_the_before_context():
465
+ seen = {}
466
+
467
+ def before(*args, **kwargs):
468
+ return {"args": args}
469
+
470
+ def on_error(ctx, exc):
471
+ seen["ctx"] = ctx
472
+ seen["exc"] = exc
473
+
474
+ failure = KeyError("k")
475
+
476
+ def raiser(x):
477
+ raise failure
478
+
479
+ wrapped = _core.wrap_callable(raiser, before=before, on_error=on_error)
480
+ with pytest.raises(KeyError):
481
+ wrapped(7)
482
+ assert seen["ctx"] == {"args": (7,)}
483
+ assert seen["exc"] is failure
484
+
485
+
486
+ def test_after_sees_the_result():
487
+ seen = {}
488
+ wrapped = _core.wrap_callable(
489
+ original_function,
490
+ before=lambda *a, **k: "ctx",
491
+ after=lambda ctx, result: seen.update(ctx=ctx, result=result),
492
+ )
493
+ assert wrapped(4) == 8
494
+ assert seen == {"ctx": "ctx", "result": 8}
495
+
496
+
497
+ # ---------------------------------------------------------------------------
498
+ # payload discipline
499
+ # ---------------------------------------------------------------------------
500
+
501
+ def test_truncate_marks_and_bounds_a_long_string():
502
+ out = _core.truncate("x" * 20_000, 100)
503
+ assert len(out) == 100
504
+ assert out.endswith(_core.TRUNCATION_MARKER)
505
+
506
+
507
+ def test_truncate_recurses_into_containers():
508
+ out = _core.truncate({"a": ["y" * 500]}, 50)
509
+ assert out["a"][0].endswith(_core.TRUNCATION_MARKER)
510
+
511
+
512
+ def test_truncate_leaves_small_values_alone():
513
+ value = {"a": 1, "b": "short", "c": None, "d": True}
514
+ assert _core.truncate(value) == value
515
+
516
+
517
+ def test_truncate_expands_a_mapping_that_is_not_a_dict():
518
+ # `isinstance(value, dict)` missed every mapping a framework actually hands
519
+ # us that is not literally a dict, and those fell through to the repr
520
+ # branch. A tool's JSON schema then reached the events store as the STRING
521
+ # "mappingproxy({'title': 'From Unit', 'type': 'string'})" — valid JSON
522
+ # holding a Python repr, so `JSONExtract` over it finds nothing and the
523
+ # field is unqueryable rather than merely ugly. Seen in a real stored row:
524
+ # a crewai `model_request.tools[0]…properties.from_unit`.
525
+ # `MappingProxyType` is what `model_json_schema()` and any frozen config
526
+ # hands back, so this is the common case, not an exotic one.
527
+ schema = types.MappingProxyType({"title": "From Unit", "type": "string"})
528
+ assert _core.truncate({"from_unit": schema}) == {
529
+ "from_unit": {"title": "From Unit", "type": "string"}
530
+ }
531
+ assert _core.truncate({"p": collections.ChainMap({"a": 1})}) == {"p": {"a": 1}}
532
+
533
+
534
+ def test_truncate_expands_a_dataclass_and_a_pydantic_model():
535
+ # Every framework hands us these — a tool's argument model, its structured
536
+ # return, a settings object on a request — and they have no JSON shape by
537
+ # the container checks, so they were rendered: `Point(x=1, y=2)`, a Python
538
+ # repr inside a JSON string that `JSONExtract` cannot read and the dashboard
539
+ # cannot filter on.
540
+ @dataclasses.dataclass
541
+ class Point:
542
+ x: int
543
+ y: int
544
+
545
+ class Model: # duck-typed pydantic v2: what matters is `model_dump`
546
+ def model_dump(self):
547
+ return {"city": "Faro", "celsius": 21}
548
+
549
+ assert _core.truncate({"p": Point(1, 2)}) == {"p": {"x": 1, "y": 2}}
550
+ assert _core.truncate({"m": Model()}) == {"m": {"city": "Faro", "celsius": 21}}
551
+ # and nested inside a container, which is where they actually arrive
552
+ assert _core.truncate([Point(1, 2)]) == [{"x": 1, "y": 2}]
553
+
554
+
555
+ def test_truncate_falls_back_to_repr_when_unwrapping_raises():
556
+ # `model_dump` and `getattr` both run the CALLER'S code — a validator, a
557
+ # property that touches the network. A telemetry library must not turn that
558
+ # into an exception in someone's agent loop, and `repr` is exactly what
559
+ # would have happened before any of this existed.
560
+ class Exploding:
561
+ def model_dump(self):
562
+ raise RuntimeError("boom")
563
+
564
+ def __repr__(self):
565
+ return "<Exploding>"
566
+
567
+ assert _core.truncate({"e": Exploding()}) == {"e": "<Exploding>"}
568
+
569
+
570
+ def test_truncate_does_not_mistake_a_class_for_an_instance():
571
+ # `dataclasses.is_dataclass` is true of the CLASS as well as its instances,
572
+ # and `fields()` on the class would render a type as though it were data.
573
+ @dataclasses.dataclass
574
+ class Point:
575
+ x: int
576
+
577
+ out = _core.truncate({"c": Point})
578
+ assert isinstance(out["c"], str) and "class" in out["c"]
579
+
580
+
581
+ def test_truncate_still_reprs_an_object_with_no_json_shape():
582
+ # The counterweight: widening to the ABCs must not turn the repr branch
583
+ # into dead code. An object that is neither a mapping nor a sequence has no
584
+ # JSON shape and rendering it is the only thing left to do.
585
+ class Opaque:
586
+ def __repr__(self):
587
+ return "<Opaque>"
588
+
589
+ assert _core.truncate({"o": Opaque()}) == {"o": "<Opaque>"}
590
+ # And a str is a Sequence — it must keep taking the string path, not be
591
+ # exploded into a list of characters.
592
+ assert _core.truncate("hello") == "hello"
593
+ assert _core.truncate(b"raw") == "raw"
594
+
595
+
596
+ def test_payload_flags_truncation():
597
+ out = _core.payload({"fw_prompt": "z" * 100_000})
598
+ assert out["fw_truncated"] is True
599
+ assert len(out["fw_prompt"]) <= _core.FIELD_LIMIT
600
+
601
+
602
+ def test_payload_enforces_the_event_budget():
603
+ fields = {f"fw_{i}": "q" * _core.FIELD_LIMIT for i in range(20)}
604
+ out = _core.payload(fields)
605
+ assert out["fw_truncated"] is True
606
+ total = sum(len(v) for v in out.values() if isinstance(v, str))
607
+ assert total <= _core.EVENT_BUDGET
608
+
609
+
610
+ def test_payload_leaves_a_small_event_alone():
611
+ assert _core.payload({"fw_node": "retrieve"}) == {"fw_node": "retrieve"}
612
+
613
+
614
+ def test_fw_fields_namespaces_and_drops_none():
615
+ assert _core.fw_fields(run_id="r1", node="retrieve", tags=None) == {
616
+ "fw_run_id": "r1",
617
+ "fw_node": "retrieve",
618
+ }
619
+
620
+
621
+ def test_fw_fields_namespaces_a_name_that_would_shadow_a_column():
622
+ # `_schema._build()` merges extras last, so a bare `tool_name` extra would
623
+ # overwrite the declared one and change the promoted column.
624
+ assert _core.fw_fields(tool_name="sneaky") == {"fw_tool_name": "sneaky"}
625
+
626
+
627
+ def test_fw_fields_passes_the_deliberate_top_level_names_through():
628
+ out = _core.fw_fields(duration_ms=12, request_id="req-1", usage={"total_tokens": 3})
629
+ assert out == {"duration_ms": 12, "request_id": "req-1", "usage": {"total_tokens": 3}}
630
+
631
+
632
+ def test_forbidden_extras_is_derived_from_the_schema():
633
+ # Derived, not hand-listed, so adding a field to _schema.py cannot leave a
634
+ # stale copy here.
635
+ for name in ("tool_name", "model", "outcome", "input_tokens", "error", "session_id"):
636
+ assert name in _core.FORBIDDEN_EXTRAS
637
+ for name in ("request_id", "duration_ms", "usage", "framework"):
638
+ assert name not in _core.FORBIDDEN_EXTRAS
639
+
640
+
641
+ def test_guard_extras_strips_a_shadowing_name(caplog):
642
+ with caplog.at_level(logging.WARNING, logger=INTEGRATIONS_LOGGER):
643
+ out = _core.guard_extras({"tool_name": "sneaky", "fw_node": "ok"})
644
+ assert out == {"fw_node": "ok"}
645
+ assert "tool_name" in caplog.text
646
+
647
+
648
+ def test_guard_extras_raises_under_strict(monkeypatch):
649
+ strict_on(monkeypatch)
650
+ with pytest.raises(ValueError, match="tool_name"):
651
+ _core.guard_extras({"tool_name": "sneaky"})
652
+
653
+
654
+ @pytest.mark.parametrize(
655
+ "raw,expected",
656
+ [
657
+ (" retrieve\n documents ", "retrieve documents"),
658
+ ("550e8400-e29b-41d4-a716-446655440000", "main"),
659
+ ("550e8400e29b41d4a716446655440000", "main"),
660
+ ("deadbeefdeadbeefdead", "main"),
661
+ ("", "main"),
662
+ (" ", "main"),
663
+ (None, "main"),
664
+ ("Researcher", "Researcher"),
665
+ ("node_" * 40, ("node_" * 40)[:64]),
666
+ # A readable name carrying a per-run id. `_looks_like_id` fires only on
667
+ # a value that is an id all the way through, so these went in untouched
668
+ # — one distinct value per run, into the LowCardinality column that is
669
+ # the primary dashboard facet. That is the same poisoning the
670
+ # whole-string guard exists to prevent, reached by the shape frameworks
671
+ # actually produce and the docs already warn about.
672
+ ("agent-550e8400e29b41d4a716446655440000", "agent"),
673
+ ("task-550e8400-e29b-41d4-a716-446655440000", "task"),
674
+ ("crew_550e8400-e29b-41d4-a716-446655440000_worker", "crew worker"),
675
+ # ...and the counterweight: stripping must not eat a readable name that
676
+ # merely contains short hex-ish or numeric segments.
677
+ ("agent-v2", "agent-v2"),
678
+ ("step-3", "step-3"),
679
+ ("node_a1b2", "node_a1b2"),
680
+ ("deadbeef", "deadbeef"),
681
+ ],
682
+ )
683
+ def test_normalize_agent_id(raw, expected):
684
+ # agent_id is LowCardinality(String) and the primary dashboard facet; a UUID
685
+ # in it poisons that facet for every session in the project.
686
+ assert _core.normalize_agent_id(raw, "main") == expected
687
+
688
+
689
+ def test_ms_is_always_an_int():
690
+ # The server stores duration_ms as u32 and its JSON parser drops floats,
691
+ # which silently NULLs the column.
692
+ from datetime import timedelta
693
+
694
+ assert _core.ms(1.2345) == 1234
695
+ assert isinstance(_core.ms(0.5), int)
696
+ assert _core.ms(timedelta(milliseconds=250)) == 250
697
+ assert _core.ms(-3.0) == 0
698
+
699
+
700
+ def test_framework_fields():
701
+ out = _core.framework_fields("langchain", "definitely-not-installed")
702
+ assert out["framework"] == "langchain"
703
+ assert out["integration_version"] == failproofai_sdk.__version__
704
+ assert "framework_version" not in out
705
+
706
+
707
+ # ---------------------------------------------------------------------------
708
+ # RunTracker
709
+ # ---------------------------------------------------------------------------
710
+
711
+ def test_run_tracker_brackets_an_agent(events):
712
+ tracker = _core.RunTracker("fake")
713
+ tracker.start_agent("run-1", agent_id="researcher", session_id="s-1", goal="find it")
714
+ tracker.end_agent("run-1", outcome="success")
715
+ assert events.types() == ["agent_start", "agent_end"]
716
+ start, end = events.entries
717
+ assert start["session_id"] == "s-1"
718
+ assert start["agent_id"] == "researcher"
719
+ assert start["goal"] == "find it"
720
+ assert "parent_id" not in start
721
+ assert end["outcome"] == "success"
722
+
723
+
724
+ def test_run_tracker_uses_a_uuid_run_id_as_a_key_not_as_an_agent_id(events):
725
+ tracker = _core.RunTracker("fake")
726
+ tracker.start_agent(
727
+ "run-1", agent_id="550e8400-e29b-41d4-a716-446655440000", session_id="s-1"
728
+ )
729
+ assert events.last()["agent_id"] == "main"
730
+
731
+
732
+ def test_run_tracker_nests_on_the_parent_key(events):
733
+ tracker = _core.RunTracker("fake")
734
+ tracker.start_agent("root", agent_id="crew", session_id="s-1")
735
+ tracker.start_agent("child", agent_id="researcher", parent_key="root")
736
+ child = events.entries[-1]
737
+ assert child["parent_id"] == "crew"
738
+ assert child["session_id"] == "s-1"
739
+
740
+
741
+ def test_run_tracker_walks_a_chain_of_non_agent_runs(events):
742
+ """The framework's parent_run_id chain is a better parent than a contextvar
743
+ stack: it survives task hops and thread pools."""
744
+ tracker = _core.RunTracker("fake")
745
+ tracker.start_agent("agent-run", agent_id="graph", session_id="s-1")
746
+ tracker.link("chain-run", "agent-run")
747
+ tracker.link("inner-run", "chain-run")
748
+ tracker.emit(
749
+ "tool_use", "tool-run", parent_key="inner-run", tool_name="search", tool_call_id="t1"
750
+ )
751
+ tool = events.entries[-1]
752
+ assert tool["type"] == "tool_use"
753
+ assert tool["agent_id"] == "graph"
754
+ assert tool["session_id"] == "s-1"
755
+
756
+
757
+ def test_run_tracker_joins_a_hand_written_scope(events):
758
+ """Step 3 of `identity()` — the whole interop story.
759
+
760
+ An adapter running inside a hand-written `with failproofai_sdk.agent("planner")`
761
+ must land in the SAME session with parent_id="planner", or the customer gets
762
+ two disconnected trees for one run.
763
+ """
764
+ tracker = _core.RunTracker("fake")
765
+ with failproofai_sdk.agent("planner", session_id="s-outer", goal="q"):
766
+ tracker.start_agent("run-1", agent_id="researcher")
767
+ tracker.emit("tool_use", "run-1", tool_name="search", tool_call_id="t1")
768
+ tracker.end_agent("run-1")
769
+ # A run the tracker has never seen — a callback that arrived without a
770
+ # start, which every framework produces eventually — still lands on the
771
+ # open scope rather than being dropped or given a session of its own.
772
+ tracker.emit("tool_use", "never-registered", tool_name="lookup", tool_call_id="t2")
773
+
774
+ assert events.types() == [
775
+ "agent_start",
776
+ "agent_start",
777
+ "tool_use",
778
+ "agent_end",
779
+ "tool_use",
780
+ "agent_end",
781
+ ]
782
+ assert all(e["session_id"] == "s-outer" for e in events.entries)
783
+ researcher = events.entries[1]
784
+ assert researcher["agent_id"] == "researcher"
785
+ assert researcher["parent_id"] == "planner"
786
+ assert events.entries[2]["agent_id"] == "researcher"
787
+ assert events.entries[4]["agent_id"] == "planner"
788
+
789
+
790
+ def test_run_tracker_does_not_invent_a_parent_inside_a_bare_session(events):
791
+ """A `session()` scope binds no agent, so an adapter agent there is a root.
792
+
793
+ Claiming `parent_id="main"` would point at an agent that never emitted an
794
+ `agent_start`, and the dashboard answers that by synthesizing a permanent
795
+ extra lane that is `ongoing` forever.
796
+ """
797
+ tracker = _core.RunTracker("fake")
798
+ with failproofai_sdk.session("s-outer"):
799
+ tracker.start_agent("run-1", agent_id="researcher")
800
+ start = events.entries[0]
801
+ assert start["session_id"] == "s-outer"
802
+ assert "parent_id" not in start
803
+
804
+
805
+ def test_run_tracker_drops_unresolvable_events_and_warns_once(events, caplog):
806
+ tracker = _core.RunTracker("fake")
807
+ with caplog.at_level(logging.WARNING, logger=INTEGRATIONS_LOGGER):
808
+ for i in range(5):
809
+ tracker.emit("tool_use", f"unknown-{i}", tool_name="t", tool_call_id="x")
810
+ assert events.entries == []
811
+ # Dropped rather than given a synthesized session id: a synthetic session
812
+ # splits one run into many, which is a silent wrong answer.
813
+ resolution_warnings = [r for r in caplog.records if "could not resolve" in r.getMessage()]
814
+ assert len(resolution_warnings) == 1
815
+
816
+
817
+ def test_run_tracker_is_bounded(events):
818
+ tracker = _core.RunTracker("fake", max_open=10)
819
+ for i in range(200):
820
+ tracker.start_agent(f"run-{i}", agent_id="worker", session_id="s-1")
821
+ assert len(tracker.open_agents()) <= 10
822
+
823
+
824
+ def test_run_tracker_survives_concurrent_start_and_end(events):
825
+ """CrewAI dispatches its handlers on a ten-worker pool."""
826
+ tracker = _core.RunTracker("fake")
827
+ errors = []
828
+ barrier = threading.Barrier(8)
829
+
830
+ def worker(n):
831
+ try:
832
+ barrier.wait(timeout=10)
833
+ for i in range(50):
834
+ key = f"{n}-{i}"
835
+ tracker.start_agent(key, agent_id="worker", session_id="s-1")
836
+ tracker.emit("tool_use", key, tool_name="t", tool_call_id=key)
837
+ tracker.end_agent(key)
838
+ except BaseException as exc: # noqa: BLE001 - reported, not swallowed
839
+ errors.append(exc)
840
+
841
+ threads = [threading.Thread(target=worker, args=(n,)) for n in range(8)]
842
+ for thread in threads:
843
+ thread.start()
844
+ for thread in threads:
845
+ thread.join(timeout=30)
846
+
847
+ assert errors == []
848
+ assert tracker.open_agents() == ()
849
+ assert len(events.entries) == 8 * 50 * 3
850
+
851
+
852
+ def test_run_tracker_close_open_agents(events):
853
+ tracker = _core.RunTracker("fake")
854
+ tracker.start_agent("a", agent_id="one", session_id="s-1")
855
+ tracker.start_agent("b", agent_id="two", parent_key="a")
856
+ tracker.close_open_agents()
857
+ # A session that dies with an open agent_start renders `ongoing` forever.
858
+ assert events.types() == ["agent_start", "agent_start", "agent_end", "agent_end"]
859
+ assert [e["agent_id"] for e in events.entries[2:]] == ["two", "one"]
860
+ assert all(e["outcome"] == "cancelled" for e in events.entries[2:])
861
+
862
+
863
+ def test_run_tracker_stamps_the_framework_fields(events):
864
+ tracker = _core.RunTracker("fake", base_fields=_core.framework_fields("fake"))
865
+ tracker.start_agent("run-1", agent_id="worker", session_id="s-1")
866
+ assert events.last()["framework"] == "fake"
867
+ assert events.last()["integration_version"] == failproofai_sdk.__version__
868
+
869
+
870
+ def test_run_tracker_base_fields_cannot_shadow_a_declared_field(events, caplog):
871
+ tracker = _core.RunTracker("fake", base_fields={"tool_name": "hijacked"})
872
+ tracker.start_agent("run-1", agent_id="worker", session_id="s-1")
873
+ with caplog.at_level(logging.WARNING, logger=INTEGRATIONS_LOGGER):
874
+ tracker.emit("tool_use", "run-1", tool_name="real", tool_call_id="t1")
875
+ assert events.last()["tool_name"] == "real"
876
+
877
+
878
+ def test_run_tracker_truncates_a_huge_payload(events):
879
+ tracker = _core.RunTracker("fake")
880
+ tracker.start_agent("run-1", agent_id="worker", session_id="s-1")
881
+ tracker.emit(
882
+ "tool_use", "run-1", tool_name="t", tool_call_id="t1", input={"q": "x" * 100_000}
883
+ )
884
+ assert len(events.last()["input"]["q"]) <= _core.FIELD_LIMIT
885
+
886
+
887
+ def test_run_tracker_never_raises_into_the_caller(events, caplog):
888
+ tracker = _core.RunTracker("fake")
889
+ tracker.start_agent("run-1", agent_id="worker", session_id="s-1")
890
+ with caplog.at_level(logging.WARNING, logger=INTEGRATIONS_LOGGER):
891
+ # `timestamp` is reserved, so the emit raises inside the SDK.
892
+ tracker.emit("tool_use", "run-1", tool_name="t", tool_call_id="t1", timestamp="nope")
893
+ assert "failproofai_sdk" in caplog.text
894
+
895
+
896
+ def test_run_tracker_raises_into_the_caller_under_strict(events, monkeypatch):
897
+ tracker = _core.RunTracker("fake")
898
+ tracker.start_agent("run-1", agent_id="worker", session_id="s-1")
899
+ strict_on(monkeypatch)
900
+ with pytest.raises(ValueError):
901
+ tracker.emit("tool_use", "run-1", tool_name="t", tool_call_id="t1", timestamp="nope")
902
+
903
+
904
+ def test_run_tracker_does_not_touch_contextvars(events):
905
+ """Shape B must never bind identity onto the context.
906
+
907
+ `ContextVar.reset(token)` raises across asyncio tasks as well as threads, so
908
+ a token cannot be held between two callbacks — and a scope entered in
909
+ `on_tool_start` that is never exited misattributes every later event in the
910
+ process.
911
+ """
912
+ from failproofai_sdk import _context
913
+
914
+ tracker = _core.RunTracker("fake")
915
+ tracker.start_agent("run-1", agent_id="worker", session_id="s-1")
916
+ assert _context.snapshot() == (None, ())
917
+ tracker.emit("tool_use", "run-1", tool_name="t", tool_call_id="t1")
918
+ assert _context.snapshot() == (None, ())
919
+ tracker.end_agent("run-1")
920
+ assert _context.snapshot() == (None, ())
921
+
922
+
923
+ # ---------------------------------------------------------------------------
924
+ # _compat
925
+ # ---------------------------------------------------------------------------
926
+
927
+ @pytest.mark.parametrize(
928
+ "text,expected",
929
+ [
930
+ ("1.5.2", (1, 5, 2)),
931
+ ("2.0.0b1", (2, 0, 0)),
932
+ ("0.14.23.post1", (0, 14, 23)),
933
+ ("1.2.dev0", (1, 2)),
934
+ ("1", (1,)),
935
+ ("", ()),
936
+ ("nonsense", ()),
937
+ ],
938
+ )
939
+ def test_parse_version_is_naive_on_purpose(text, expected):
940
+ # No `packaging`: `import failproofai_sdk` is zero-dependency.
941
+ assert _compat.parse_version(text) == expected
942
+
943
+
944
+ def test_version_comparison_orders_as_expected():
945
+ assert _compat.parse_version("1.5") >= _compat.parse_version("1.4.7")
946
+ assert _compat.parse_version("1.5.2") < _compat.parse_version("2")
947
+
948
+
949
+ def test_missing_framework_raises_with_the_install_command():
950
+ # Tier 1. Instrumenting is an explicit user action, so silence is never right.
951
+ with pytest.raises(ImportError) as excinfo:
952
+ _compat.require_module("definitely_not_a_module", dist="nope", extra="langchain")
953
+ assert "pip install 'failproofai_sdk[langchain]'" in str(excinfo.value)
954
+
955
+
956
+ def test_require_module_returns_the_module():
957
+ assert _compat.require_module("json", dist="json", extra="langchain").__name__ == "json"
958
+
959
+
960
+ def test_version_in_range_is_silent(recwarn):
961
+ assert _compat.check_version("self", "failproofai_sdk", minimum="0.0.1", below="99") is True
962
+ assert not [w for w in recwarn if issubclass(w.category, _compat.FailproofAICompatWarning)]
963
+
964
+
965
+ def test_version_out_of_range_warns_once():
966
+ # Tier 2: warn, then best effort. A ceiling is what stops a future major
967
+ # from making the adapter stop recording while raising nothing.
968
+ with pytest.warns(_compat.FailproofAICompatWarning, match="newer"):
969
+ assert _compat.check_version("self", "failproofai_sdk", below="0.0.1") is False
970
+
971
+ # Deduplicated: these fire from install() *and* from hot callbacks, so a
972
+ # per-call warning on a chatty framework is its own outage.
973
+ with warnings.catch_warnings(record=True) as second:
974
+ warnings.simplefilter("always")
975
+ _compat.check_version("self", "failproofai_sdk", below="0.0.1")
976
+ assert not [w for w in second if issubclass(w.category, _compat.FailproofAICompatWarning)]
977
+
978
+
979
+ def test_an_uninstallable_distribution_is_best_effort():
980
+ assert _compat.check_version("x", "definitely-not-installed", minimum="99") is True
981
+
982
+
983
+ def test_strict_integrations_promotes_a_warning_to_an_exception(monkeypatch):
984
+ strict_on(monkeypatch, "FAILPROOFAI_SDK_STRICT_INTEGRATIONS")
985
+ with pytest.raises(_compat.FailproofAICompatWarning):
986
+ _compat.check_version("self", "failproofai_sdk", minimum="99")
987
+
988
+
989
+ def test_a_failing_capability_probe_disables_one_hook_only():
990
+ # Tier 3.
991
+ with pytest.warns(_compat.FailproofAICompatWarning, match="on_interrupt"):
992
+ assert _compat.probe("langchain", "on_interrupt", lambda: 1 / 0) is False
993
+ assert _compat.probe("langchain", "on_tool_start", lambda: True) is True
994
+
995
+
996
+ def test_a_probe_returning_false_warns():
997
+ with pytest.warns(_compat.FailproofAICompatWarning, match="does not provide"):
998
+ assert _compat.probe("langchain", "on_resume", lambda: None) is False
999
+
1000
+
1001
+ # ─────────────────────────────────────────────────────────────────────────────
1002
+ # Capture fidelity: the field limit, and the budget that has to follow it
1003
+ # ─────────────────────────────────────────────────────────────────────────────
1004
+ #
1005
+ # `payload()` does not shorten the field that overruns the budget — it OMITS THE
1006
+ # KEY (`remaining <= 0` -> `continue`). So the per-event budget and the per-field
1007
+ # limit are one setting in two halves: raising the limit alone converts shortened
1008
+ # values into missing ones, and the event stops recording that anything is gone.
1009
+ #
1010
+ # The budget is therefore derived, and these tests pin that it stays derived.
1011
+
1012
+
1013
+ def test_the_event_budget_is_derived_from_the_field_limit():
1014
+ assert _core.EVENT_BUDGET == _core.FIELD_LIMIT * _core._FIELDS_PER_EVENT
1015
+
1016
+
1017
+ def test_raising_the_field_limit_keeps_the_same_headroom():
1018
+ """The property that makes raising it safe: same max-size field count."""
1019
+ for limit in (2048, 8192, 32768):
1020
+ tracker = _core.RunTracker("probe", field_limit=limit)
1021
+ assert tracker._budget // limit == _core._FIELDS_PER_EVENT
1022
+
1023
+
1024
+ def test_a_tracker_field_limit_reaches_the_fw_extras():
1025
+ tracker = _core.RunTracker("probe", field_limit=64)
1026
+ out = _core.payload({"fw_prompt": "z" * 5_000}, limit=tracker._field_limit,
1027
+ budget=tracker._budget)
1028
+ assert len(out["fw_prompt"]) == 64
1029
+
1030
+
1031
+ def test_a_tracker_without_a_field_limit_keeps_the_core_default():
1032
+ assert _core.RunTracker("probe")._field_limit == _core.FIELD_LIMIT
1033
+
1034
+
1035
+ def test_the_budget_omits_keys_rather_than_shortening_them():
1036
+ """The reason the two settings cannot move independently.
1037
+
1038
+ Documented here because it is the non-obvious half: past the budget a field
1039
+ does not arrive short, it does not arrive at all, and `fw_truncated` names
1040
+ none of the keys that went.
1041
+ """
1042
+ fields = {f"fw_{i:02d}": "q" * _core.FIELD_LIMIT for i in range(40)}
1043
+ out = _core.payload(fields)
1044
+ survivors = [k for k in out if k.startswith("fw_")]
1045
+ assert len(survivors) < len(fields), "nothing was dropped; fixture too small"
1046
+ assert out["fw_truncated"] is True
1047
+ # The flag is the only signal — it does not enumerate what it removed.
1048
+ assert not any("fw_00" in str(v) for k, v in out.items() if k == "fw_truncated")
1049
+
1050
+
1051
+ class _HalfInstallingAdapter:
1052
+ """An adapter whose `install()` does its global side effects, then raises.
1053
+
1054
+ That ordering is not contrived — it is what `langchain.install()` does: it
1055
+ sets `_STATE.enabled`, registers the append-only configure hook, builds the
1056
+ tracer and exports the env var, and only THEN probes a capability that can
1057
+ raise.
1058
+ """
1059
+
1060
+ NAME = "half"
1061
+
1062
+ def __init__(self, boom):
1063
+ self.state = {"patched": False}
1064
+ self._boom = boom
1065
+
1066
+ def install(self, **options):
1067
+ self.state["patched"] = True
1068
+ raise self._boom
1069
+
1070
+ def uninstall(self):
1071
+ self.state["patched"] = False
1072
+ return ()
1073
+
1074
+
1075
+ def _with_fake_adapter(monkeypatch, adapter):
1076
+ monkeypatch.setitem(integrations._REGISTRY, "half", lambda: adapter)
1077
+ monkeypatch.setattr(integrations, "_load", lambda name: adapter)
1078
+ integrations._ACTIVE.pop("half", None)
1079
+
1080
+
1081
+ def test_an_install_that_raises_part_way_is_rolled_back(monkeypatch):
1082
+ """`instrument()` treated `install()` as atomic. It is not.
1083
+
1084
+ A failure part-way through left the adapter FULLY patched and never recorded
1085
+ in `_ACTIVE` — so `active()` denied it existed and `uninstrument()`, which
1086
+ iterates `_ACTIVE`, could never undo it. It recorded for the life of the
1087
+ process and could not be removed, while the log said the process was
1088
+ unaffected.
1089
+ """
1090
+ adapter = _HalfInstallingAdapter(RuntimeError("framework internals moved"))
1091
+ _with_fake_adapter(monkeypatch, adapter)
1092
+ _core.set_strict(False)
1093
+ _compat.set_strict_integrations(False)
1094
+ try:
1095
+ assert integrations.instrument("half") == ()
1096
+ assert "half" not in integrations.active()
1097
+ assert adapter.state["patched"] is False, (
1098
+ "install failed but its global side effects were left in place"
1099
+ )
1100
+ finally:
1101
+ _core.set_strict(None)
1102
+ _compat.set_strict_integrations(None)
1103
+ integrations._ACTIVE.pop("half", None)
1104
+
1105
+
1106
+ def test_strict_integrations_raises_instead_of_silently_disabling_the_adapter(monkeypatch):
1107
+ """The flag promised loud, and delivered silent.
1108
+
1109
+ `FAILPROOFAI_SDK_STRICT_INTEGRATIONS=1` makes `_compat.warn` raise, and
1110
+ `instrument()` caught it — because its own strict gate is a DIFFERENT
1111
+ variable. So the operator got neither the raise the flag promises nor the
1112
+ best-effort instrumentation the warning text promises ("Instrumenting
1113
+ anyway"): the adapter was skipped entirely and recorded nothing.
1114
+ """
1115
+ adapter = _HalfInstallingAdapter(_compat.FailproofAICompatWarning("too new"))
1116
+ _with_fake_adapter(monkeypatch, adapter)
1117
+ _core.set_strict(False)
1118
+ _compat.set_strict_integrations(True)
1119
+ try:
1120
+ with pytest.raises(_compat.FailproofAICompatWarning):
1121
+ integrations.instrument("half")
1122
+ # And it is still rolled back on the way out.
1123
+ assert adapter.state["patched"] is False
1124
+ finally:
1125
+ _core.set_strict(None)
1126
+ _compat.set_strict_integrations(None)
1127
+ integrations._ACTIVE.pop("half", None)