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,923 @@
1
+ """
2
+ Tests for the failproofai_sdk SDK.
3
+
4
+ Unit tests use EventNamespace with a mock writer (no disk I/O).
5
+ Integration tests use configure() + real tmpdir + flush_now().
6
+ """
7
+
8
+ import json
9
+ import time
10
+ from datetime import datetime, timezone
11
+ from pathlib import Path
12
+
13
+ import pytest
14
+
15
+ import failproofai_sdk
16
+ from failproofai_sdk._events import EventNamespace
17
+ from failproofai_sdk._schema import (
18
+ AgentEndEvent,
19
+ AgentPauseEvent,
20
+ AgentResumeEvent,
21
+ AgentStartEvent,
22
+ ErrorEvent,
23
+ HookCompletedEvent,
24
+ HookTriggeredEvent,
25
+ HumanInputEvent,
26
+ HumanInterruptEvent,
27
+ HumanPauseEvent,
28
+ HumanWaitEvent,
29
+ ModelRequestEvent,
30
+ ModelResponseEvent,
31
+ ToolResultEvent,
32
+ ToolUseEvent,
33
+ )
34
+ from failproofai_sdk._writer import EventWriter
35
+
36
+
37
+ # ---------------------------------------------------------------------------
38
+ # Helpers
39
+ # ---------------------------------------------------------------------------
40
+
41
+ class MockWriter:
42
+ """Collects submitted entries without touching disk."""
43
+
44
+ def __init__(self):
45
+ self.entries: list[dict] = []
46
+
47
+ def submit(self, entry: dict) -> None:
48
+ self.entries.append(entry)
49
+
50
+ def last(self) -> dict:
51
+ return self.entries[-1]
52
+
53
+
54
+ @pytest.fixture()
55
+ def mock_writer():
56
+ return MockWriter()
57
+
58
+
59
+ @pytest.fixture()
60
+ def ns(mock_writer):
61
+ return EventNamespace(mock_writer)
62
+
63
+
64
+ # ---------------------------------------------------------------------------
65
+ # Reserved field validation
66
+ # ---------------------------------------------------------------------------
67
+
68
+ @pytest.mark.parametrize("reserved", ["timestamp", "type"])
69
+ def test_reserved_fields_raise_via_extra(ns, reserved):
70
+ # timestamp and type aren't in the explicit signature so they land in **fields and hit our validator
71
+ with pytest.raises(ValueError, match="Reserved field"):
72
+ ns.agent_start(session_id="s1", agent_id="a1", **{reserved: "bad"})
73
+
74
+
75
+ @pytest.mark.parametrize("reserved", ["session_id", "agent_id"])
76
+ def test_reserved_fields_blocked_by_signature(ns, reserved):
77
+ # session_id and agent_id are explicit params; Python raises TypeError on duplicate keyword
78
+ with pytest.raises(TypeError):
79
+ ns.agent_start(session_id="s1", agent_id="a1", **{reserved: "bad"})
80
+
81
+
82
+ # ---------------------------------------------------------------------------
83
+ # duration_ms rejection on paired end events
84
+ # ---------------------------------------------------------------------------
85
+
86
+ def test_tool_result_rejects_duration_ms(ns):
87
+ with pytest.raises(ValueError, match="duration_ms"):
88
+ ns.tool_result(session_id="s1", agent_id="a1", tool_name="t", tool_call_id="tc1", duration_ms=99)
89
+
90
+
91
+ def test_hook_completed_rejects_duration_ms(ns):
92
+ with pytest.raises(ValueError, match="duration_ms"):
93
+ ns.hook_completed(session_id="s1", agent_id="a1", hook_name="h", hook_id="hid1", duration_ms=99)
94
+
95
+
96
+ # ---------------------------------------------------------------------------
97
+ # Null field omission
98
+ # ---------------------------------------------------------------------------
99
+
100
+ def test_null_fields_omitted_tool_use(ns, mock_writer):
101
+ ns.tool_use(session_id="s1", agent_id="a1", tool_name="search", tool_call_id="tc1")
102
+ d = mock_writer.last()
103
+ assert "input" not in d
104
+
105
+
106
+ def test_null_fields_omitted_model_request(ns, mock_writer):
107
+ ns.model_request(session_id="s1", agent_id="a1")
108
+ d = mock_writer.last()
109
+ for f in ("model", "messages", "system", "tools"):
110
+ assert f not in d
111
+
112
+
113
+ def test_null_fields_omitted_model_response(ns, mock_writer):
114
+ ns.model_response(session_id="s1", agent_id="a1")
115
+ d = mock_writer.last()
116
+ for f in ("model", "stop_reason", "input_tokens", "output_tokens", "content", "role"):
117
+ assert f not in d
118
+
119
+
120
+ def test_model_request_fields_captured(ns, mock_writer):
121
+ messages = [{"role": "user", "content": "hello"}]
122
+ tools = [{"name": "search", "input_schema": {"type": "object"}}]
123
+ ns.model_request(
124
+ session_id="s1", agent_id="a1", model="claude-opus-4-6",
125
+ messages=messages, system="You are helpful.", tools=tools,
126
+ )
127
+ d = mock_writer.last()
128
+ assert d["messages"] == messages
129
+ assert d["system"] == "You are helpful."
130
+ assert d["tools"] == tools
131
+
132
+
133
+ def test_model_response_content_as_string(ns, mock_writer):
134
+ ns.model_response(
135
+ session_id="s1", agent_id="a1",
136
+ content="plain text completion", role="assistant",
137
+ )
138
+ d = mock_writer.last()
139
+ assert d["content"] == "plain text completion"
140
+ assert d["role"] == "assistant"
141
+
142
+
143
+ def test_model_response_content_as_blocks(ns, mock_writer):
144
+ blocks = [
145
+ {"type": "text", "text": "I'll search..."},
146
+ {"type": "tool_use", "id": "toolu_01", "name": "search", "input": {"q": "x"}},
147
+ ]
148
+ ns.model_response(session_id="s1", agent_id="a1", content=blocks, role="assistant")
149
+ d = mock_writer.last()
150
+ assert d["content"] == blocks
151
+ assert d["role"] == "assistant"
152
+
153
+
154
+ def test_null_fields_omitted_agent_start(ns, mock_writer):
155
+ ns.agent_start(session_id="s1", agent_id="a1")
156
+ d = mock_writer.last()
157
+ assert "goal" not in d
158
+ assert "parent_id" not in d
159
+
160
+
161
+ def test_null_fields_omitted_error(ns, mock_writer):
162
+ ns.error(session_id="s1", agent_id="a1", error_type="ValueError", message="oops")
163
+ d = mock_writer.last()
164
+ assert "traceback" not in d
165
+
166
+
167
+ # ---------------------------------------------------------------------------
168
+ # Field ordering
169
+ # ---------------------------------------------------------------------------
170
+
171
+ def test_field_ordering(ns, mock_writer):
172
+ ns.agent_start(session_id="s1", agent_id="a1", goal="do stuff")
173
+ d = mock_writer.last()
174
+ keys = list(d.keys())
175
+ assert keys[0] == "timestamp"
176
+ assert keys[1] == "session_id"
177
+ assert keys[2] == "agent_id"
178
+ assert keys[3] == "type"
179
+
180
+
181
+ def test_custom_fields_at_end(ns, mock_writer):
182
+ ns.agent_end(session_id="s1", agent_id="a1", outcome="success", message="done", custom_key="val")
183
+ d = mock_writer.last()
184
+ keys = list(d.keys())
185
+ assert keys.index("message") > keys.index("outcome")
186
+ assert keys.index("custom_key") > keys.index("outcome")
187
+ assert d["message"] == "done"
188
+ assert d["custom_key"] == "val"
189
+
190
+
191
+ # ---------------------------------------------------------------------------
192
+ # Auto duration computation
193
+ # ---------------------------------------------------------------------------
194
+
195
+ def test_tool_use_result_duration(ns, mock_writer):
196
+ ns.tool_use(session_id="s1", agent_id="a1", tool_name="search", tool_call_id="tc1")
197
+ time.sleep(0.01)
198
+ ns.tool_result(session_id="s1", agent_id="a1", tool_name="search", tool_call_id="tc1", output="ok")
199
+ d = mock_writer.last()
200
+ assert "duration_ms" in d
201
+ assert d["duration_ms"] >= 10.0 # at least 10ms
202
+
203
+
204
+ def test_hook_triggered_completed_duration(ns, mock_writer):
205
+ ns.hook_triggered(session_id="s1", agent_id="a1", hook_name="pre_tool_use", hook_id="hid1")
206
+ time.sleep(0.01)
207
+ ns.hook_completed(session_id="s1", agent_id="a1", hook_name="pre_tool_use", hook_id="hid1", outcome="success")
208
+ d = mock_writer.last()
209
+ assert "duration_ms" in d
210
+ assert d["duration_ms"] >= 10.0
211
+
212
+
213
+ # ---------------------------------------------------------------------------
214
+ # Missing correlation ID → duration_ms absent
215
+ # ---------------------------------------------------------------------------
216
+
217
+ def test_tool_result_without_prior_tool_use(ns, mock_writer):
218
+ ns.tool_result(session_id="s1", agent_id="a1", tool_name="search", tool_call_id="no-prior")
219
+ d = mock_writer.last()
220
+ assert "duration_ms" not in d
221
+
222
+
223
+ def test_hook_completed_without_prior_triggered(ns, mock_writer):
224
+ ns.hook_completed(session_id="s1", agent_id="a1", hook_name="h", hook_id="no-prior", outcome="success")
225
+ d = mock_writer.last()
226
+ assert "duration_ms" not in d
227
+
228
+
229
+ # ---------------------------------------------------------------------------
230
+ # All 9 event types — type field smoke test
231
+ # ---------------------------------------------------------------------------
232
+
233
+ @pytest.mark.parametrize("method,kwargs,expected_type", [
234
+ ("tool_use", {"tool_name": "t", "tool_call_id": "c1"}, "tool_use"),
235
+ ("tool_result", {"tool_name": "t", "tool_call_id": "c2"}, "tool_result"),
236
+ ("model_request", {}, "model_request"),
237
+ ("model_response", {}, "model_response"),
238
+ ("agent_start", {}, "agent_start"),
239
+ ("agent_end", {}, "agent_end"),
240
+ ("hook_triggered", {"hook_name": "h", "hook_id": "hid"}, "hook_triggered"),
241
+ ("hook_completed", {"hook_name": "h", "hook_id": "hid2"}, "hook_completed"),
242
+ ("error", {"error_type": "ValueError", "message": "oops"}, "error"),
243
+ ("human_wait", {"input_id": "inp1"}, "human_wait"),
244
+ ("human_input", {"input_id": "inp2"}, "human_input"),
245
+ ("human_pause", {}, "human_pause"),
246
+ ("human_interrupt", {}, "human_interrupt"),
247
+ ("agent_pause", {"pause_id": "p1"}, "agent_pause"),
248
+ ("agent_resume", {"pause_id": "p2"}, "agent_resume"),
249
+ ])
250
+ def test_event_types(ns, mock_writer, method, kwargs, expected_type):
251
+ getattr(ns, method)(session_id="s1", agent_id="a1", **kwargs)
252
+ assert mock_writer.last()["type"] == expected_type
253
+
254
+
255
+ # ---------------------------------------------------------------------------
256
+ # Schema to_dict directly — field values
257
+ # ---------------------------------------------------------------------------
258
+
259
+ def test_tool_use_schema():
260
+ e = ToolUseEvent(
261
+ timestamp="2026-03-31T14:22:01.123456Z",
262
+ session_id="run-abc",
263
+ agent_id="planner",
264
+ tool_name="web_search",
265
+ tool_call_id="toolu_01",
266
+ input={"query": "test"},
267
+ )
268
+ d = e.to_dict()
269
+ assert d["type"] == "tool_use"
270
+ assert d["tool_name"] == "web_search"
271
+ assert d["input"] == {"query": "test"}
272
+
273
+
274
+ def test_tool_result_with_duration_schema():
275
+ e = ToolResultEvent(
276
+ timestamp="2026-03-31T14:22:02.456789Z",
277
+ session_id="run-abc",
278
+ agent_id="planner",
279
+ tool_name="web_search",
280
+ tool_call_id="toolu_01",
281
+ output={"results": ["x"]},
282
+ duration_ms=1333.6,
283
+ )
284
+ d = e.to_dict()
285
+ assert d["duration_ms"] == 1333.6
286
+ assert d["output"] == {"results": ["x"]}
287
+ assert "error" not in d
288
+
289
+
290
+ def test_error_schema():
291
+ e = ErrorEvent(
292
+ timestamp="2026-03-31T14:22:08.500000Z",
293
+ session_id="run-abc",
294
+ agent_id="planner",
295
+ error_type="TimeoutError",
296
+ message="timed out",
297
+ traceback="Traceback...",
298
+ )
299
+ d = e.to_dict()
300
+ assert d["error_type"] == "TimeoutError"
301
+ assert d["traceback"] == "Traceback..."
302
+
303
+
304
+ # ---------------------------------------------------------------------------
305
+ # Human-in-the-loop event tests
306
+ # ---------------------------------------------------------------------------
307
+
308
+ def test_human_input_rejects_duration_ms(ns):
309
+ with pytest.raises(ValueError, match="duration_ms"):
310
+ ns.human_input(session_id="s1", agent_id="a1", input_id="inp1", duration_ms=99)
311
+
312
+
313
+ def test_human_wait_input_duration(ns, mock_writer):
314
+ ns.human_wait(session_id="s1", agent_id="a1", input_id="inp1")
315
+ time.sleep(0.01)
316
+ ns.human_input(session_id="s1", agent_id="a1", input_id="inp1", response="approved")
317
+ d = mock_writer.last()
318
+ assert "duration_ms" in d
319
+ assert d["duration_ms"] >= 10.0
320
+ assert d["response"] == "approved"
321
+
322
+
323
+ def test_human_input_without_prior_wait(ns, mock_writer):
324
+ ns.human_input(session_id="s1", agent_id="a1", input_id="no-prior")
325
+ d = mock_writer.last()
326
+ assert "duration_ms" not in d
327
+
328
+
329
+ # ---------------------------------------------------------------------------
330
+ # Agent pause / resume event tests
331
+ # ---------------------------------------------------------------------------
332
+
333
+ def test_agent_resume_rejects_duration_ms(ns):
334
+ with pytest.raises(ValueError, match="duration_ms"):
335
+ ns.agent_resume(session_id="s1", agent_id="a1", pause_id="p1", duration_ms=99)
336
+
337
+
338
+ def test_agent_pause_resume_duration(ns, mock_writer):
339
+ ns.agent_pause(session_id="s1", agent_id="a1", pause_id="p1", reason="waiting_for_user")
340
+ time.sleep(0.01)
341
+ ns.agent_resume(session_id="s1", agent_id="a1", pause_id="p1")
342
+ d = mock_writer.last()
343
+ assert d["type"] == "agent_resume"
344
+ assert d["pause_id"] == "p1"
345
+ assert "duration_ms" in d
346
+ assert d["duration_ms"] >= 10.0
347
+
348
+
349
+ def test_agent_resume_without_prior_pause(ns, mock_writer):
350
+ ns.agent_resume(session_id="s1", agent_id="a1", pause_id="no-prior")
351
+ d = mock_writer.last()
352
+ assert d["type"] == "agent_resume"
353
+ assert d["pause_id"] == "no-prior"
354
+ assert "duration_ms" not in d
355
+
356
+
357
+ def test_agent_pause_fields(ns, mock_writer):
358
+ ns.agent_pause(session_id="s1", agent_id="a1", pause_id="p1", reason="user_requested", user_id="usr_9")
359
+ d = mock_writer.last()
360
+ assert d["type"] == "agent_pause"
361
+ assert d["pause_id"] == "p1"
362
+ assert d["reason"] == "user_requested"
363
+ assert d["user_id"] == "usr_9"
364
+
365
+
366
+ def test_agent_pause_null_fields_omitted(ns, mock_writer):
367
+ ns.agent_pause(session_id="s1", agent_id="a1", pause_id="p1")
368
+ d = mock_writer.last()
369
+ assert d["pause_id"] == "p1"
370
+ assert "reason" not in d
371
+ assert "user_id" not in d
372
+
373
+
374
+ def test_agent_pause_resume_cross_namespace_link():
375
+ # Pause in one EventNamespace ("process"), resume in another: the in-memory
376
+ # _pending map does not carry over, so duration_ms is absent — but the
377
+ # shared pause_id still links the two events for downstream pairing.
378
+ w1, w2 = MockWriter(), MockWriter()
379
+ EventNamespace(w1).agent_pause(session_id="s1", agent_id="a1", pause_id="cross-1")
380
+ EventNamespace(w2).agent_resume(session_id="s1", agent_id="a1", pause_id="cross-1")
381
+ pause_d, resume_d = w1.last(), w2.last()
382
+ assert pause_d["type"] == "agent_pause" and pause_d["pause_id"] == "cross-1"
383
+ assert resume_d["type"] == "agent_resume" and resume_d["pause_id"] == "cross-1"
384
+ assert "duration_ms" not in resume_d # no in-process pending → no auto-duration
385
+
386
+
387
+ def test_agent_pause_id_isolated_from_tool_call_id(ns, mock_writer):
388
+ # A pause_id equal to a tool_call_id must NOT cross-link in the shared
389
+ # _pending map (pause keys are namespaced), else resume would steal the
390
+ # tool's start time and emit a bogus duration.
391
+ ns.tool_use(session_id="s1", agent_id="a1", tool_name="t", tool_call_id="x1")
392
+ ns.agent_resume(session_id="s1", agent_id="a1", pause_id="x1")
393
+ d = mock_writer.last()
394
+ assert d["type"] == "agent_resume"
395
+ assert "duration_ms" not in d
396
+
397
+
398
+ def test_agent_pause_resume_schema():
399
+ p = AgentPauseEvent(
400
+ timestamp="2026-07-15T10:00:00.000000Z", session_id="s", agent_id="a",
401
+ pause_id="p1", reason="waiting_for_user",
402
+ )
403
+ pd = p.to_dict()
404
+ assert pd["type"] == "agent_pause"
405
+ assert pd["pause_id"] == "p1"
406
+ assert pd["reason"] == "waiting_for_user"
407
+ assert "user_id" not in pd # None-dropped
408
+
409
+ r = AgentResumeEvent(
410
+ timestamp="2026-07-15T10:05:00.000000Z", session_id="s", agent_id="a",
411
+ pause_id="p1", duration_ms=300000.0,
412
+ )
413
+ rd = r.to_dict()
414
+ assert rd["type"] == "agent_resume"
415
+ assert rd["pause_id"] == "p1"
416
+ assert rd["duration_ms"] == 300000.0
417
+
418
+
419
+ def test_human_wait_options_in_output(ns, mock_writer):
420
+ ns.human_wait(
421
+ session_id="s1", agent_id="a1", input_id="inp1",
422
+ prompt="Choose an action", options=["approve", "reject", "defer"],
423
+ )
424
+ d = mock_writer.last()
425
+ assert d["options"] == ["approve", "reject", "defer"]
426
+ assert d["prompt"] == "Choose an action"
427
+ assert d["input_id"] == "inp1"
428
+
429
+
430
+ def test_human_wait_no_options_omitted(ns, mock_writer):
431
+ ns.human_wait(session_id="s1", agent_id="a1", input_id="inp1")
432
+ d = mock_writer.last()
433
+ assert "options" not in d
434
+ assert "prompt" not in d
435
+ assert "reason" not in d
436
+
437
+
438
+ def test_null_fields_omitted_human_pause(ns, mock_writer):
439
+ ns.human_pause(session_id="s1", agent_id="a1")
440
+ d = mock_writer.last()
441
+ assert "reason" not in d
442
+ assert "user_id" not in d
443
+
444
+
445
+ def test_null_fields_omitted_human_interrupt(ns, mock_writer):
446
+ ns.human_interrupt(session_id="s1", agent_id="a1")
447
+ d = mock_writer.last()
448
+ assert "reason" not in d
449
+ assert "user_id" not in d
450
+ assert "at_step" not in d
451
+
452
+
453
+ def test_human_pause_fields(ns, mock_writer):
454
+ ns.human_pause(session_id="s1", agent_id="a1", reason="user_requested", user_id="usr_42")
455
+ d = mock_writer.last()
456
+ assert d["type"] == "human_pause"
457
+ assert d["reason"] == "user_requested"
458
+ assert d["user_id"] == "usr_42"
459
+
460
+
461
+ def test_human_interrupt_fields(ns, mock_writer):
462
+ ns.human_interrupt(
463
+ session_id="s1", agent_id="a1",
464
+ reason="output_incorrect", user_id="usr_42", at_step="tool_use:web_search",
465
+ )
466
+ d = mock_writer.last()
467
+ assert d["type"] == "human_interrupt"
468
+ assert d["reason"] == "output_incorrect"
469
+ assert d["at_step"] == "tool_use:web_search"
470
+ assert d["user_id"] == "usr_42"
471
+
472
+
473
+ def test_human_wait_schema():
474
+ e = HumanWaitEvent(
475
+ timestamp="2026-05-11T10:00:00.000000Z",
476
+ session_id="run-abc",
477
+ agent_id="planner",
478
+ input_id="inp-01",
479
+ prompt="Do you approve?",
480
+ options=["approve", "reject"],
481
+ )
482
+ d = e.to_dict()
483
+ assert d["type"] == "human_wait"
484
+ assert d["input_id"] == "inp-01"
485
+ assert d["prompt"] == "Do you approve?"
486
+ assert d["options"] == ["approve", "reject"]
487
+ assert "reason" not in d
488
+
489
+
490
+ def test_human_input_schema():
491
+ e = HumanInputEvent(
492
+ timestamp="2026-05-11T10:00:05.000000Z",
493
+ session_id="run-abc",
494
+ agent_id="planner",
495
+ input_id="inp-01",
496
+ response="approve",
497
+ duration_ms=5000.0,
498
+ )
499
+ d = e.to_dict()
500
+ assert d["type"] == "human_input"
501
+ assert d["input_id"] == "inp-01"
502
+ assert d["response"] == "approve"
503
+ assert d["duration_ms"] == 5000.0
504
+
505
+
506
+ def test_human_wait_input_id_in_base_dict(ns, mock_writer):
507
+ # input_id must appear before environment in field ordering
508
+ ns.human_wait(session_id="s1", agent_id="a1", input_id="inp1")
509
+ keys = list(mock_writer.last().keys())
510
+ assert keys.index("input_id") < keys.index("environment")
511
+
512
+
513
+ def test_human_input_input_id_in_base_dict(ns, mock_writer):
514
+ ns.human_input(session_id="s1", agent_id="a1", input_id="inp1")
515
+ keys = list(mock_writer.last().keys())
516
+ assert keys.index("input_id") < keys.index("environment")
517
+
518
+
519
+ def test_human_wait_custom_fields(ns, mock_writer):
520
+ ns.human_wait(session_id="s1", agent_id="a1", input_id="inp1", tenant="acme")
521
+ d = mock_writer.last()
522
+ assert d["tenant"] == "acme"
523
+
524
+
525
+ def test_human_interrupt_pending_cleared_after_human_input(ns, mock_writer):
526
+ # Emit wait then input twice with same input_id — second input should have no duration
527
+ ns.human_wait(session_id="s1", agent_id="a1", input_id="inp1")
528
+ ns.human_input(session_id="s1", agent_id="a1", input_id="inp1")
529
+ ns.human_input(session_id="s1", agent_id="a1", input_id="inp1")
530
+ d = mock_writer.last()
531
+ assert "duration_ms" not in d
532
+
533
+
534
+ def test_environment_in_human_events(ns, mock_writer):
535
+ import failproofai_sdk._environment as env_mod
536
+ env_mod.set_environment("production")
537
+ ns.human_wait(session_id="s1", agent_id="a1", input_id="inp1")
538
+ ns.human_input(session_id="s1", agent_id="a1", input_id="inp1")
539
+ ns.human_pause(session_id="s1", agent_id="a1")
540
+ ns.human_interrupt(session_id="s1", agent_id="a1")
541
+ for entry in mock_writer.entries[-4:]:
542
+ assert entry["environment"] == "production"
543
+
544
+
545
+ def test_pending_dict_is_bounded(ns):
546
+ """Orphaned tool_use calls (no matching tool_result) must not grow
547
+ `_pending` without bound — at the cap, the oldest entry is evicted FIFO."""
548
+ from failproofai_sdk._events import _PENDING_CAP, _tool_key
549
+
550
+ for i in range(_PENDING_CAP + 50):
551
+ ns.tool_use(
552
+ session_id="s1",
553
+ agent_id="a1",
554
+ tool_name="t",
555
+ tool_call_id=f"call-{i}",
556
+ )
557
+ assert len(ns._pending) == _PENDING_CAP
558
+ # Earliest IDs were evicted.
559
+ assert _tool_key("s1", "call-0") not in ns._pending
560
+ assert _tool_key("s1", "call-49") not in ns._pending
561
+ # Most-recent IDs are still tracked.
562
+ assert _tool_key("s1", f"call-{_PENDING_CAP + 49}") in ns._pending
563
+
564
+
565
+ # ---------------------------------------------------------------------------
566
+ # EventWriter — file writing integration tests
567
+ # ---------------------------------------------------------------------------
568
+
569
+ def test_writer_creates_jsonl_file(tmp_path):
570
+ import failproofai_sdk._resolver as resolver
571
+ original = resolver._base_dir
572
+ try:
573
+ resolver.set_base_dir(tmp_path)
574
+ writer = EventWriter(flush_interval=60) # won't auto-flush during test
575
+ writer.submit({"timestamp": "t", "session_id": "s1", "agent_id": "a1", "type": "agent_start"})
576
+ writer.flush_now()
577
+
578
+ events_dir = tmp_path / "events"
579
+ jsonl_files = list(events_dir.glob("*.jsonl"))
580
+ assert len(jsonl_files) == 1
581
+
582
+ lines = jsonl_files[0].read_text().strip().splitlines()
583
+ assert len(lines) == 1
584
+ parsed = json.loads(lines[0])
585
+ assert parsed["type"] == "agent_start"
586
+ finally:
587
+ resolver.set_base_dir(original)
588
+
589
+
590
+ def test_writer_no_tmp_files_after_flush(tmp_path):
591
+ import failproofai_sdk._resolver as resolver
592
+ original = resolver._base_dir
593
+ try:
594
+ resolver.set_base_dir(tmp_path)
595
+ writer = EventWriter(flush_interval=60)
596
+ writer.submit({"timestamp": "t", "session_id": "s1", "agent_id": "a1", "type": "tool_use"})
597
+ writer.flush_now()
598
+
599
+ events_dir = tmp_path / "events"
600
+ tmp_files = list(events_dir.glob("*.tmp"))
601
+ assert tmp_files == []
602
+ finally:
603
+ resolver.set_base_dir(original)
604
+
605
+
606
+ def test_writer_multiple_events_in_one_file(tmp_path):
607
+ import failproofai_sdk._resolver as resolver
608
+ original = resolver._base_dir
609
+ try:
610
+ resolver.set_base_dir(tmp_path)
611
+ writer = EventWriter(flush_interval=60)
612
+ for i in range(5):
613
+ writer.submit({"timestamp": "t", "session_id": "s1", "agent_id": "a1", "type": "model_request", "i": i})
614
+ writer.flush_now()
615
+
616
+ events_dir = tmp_path / "events"
617
+ jsonl_files = list(events_dir.glob("*.jsonl"))
618
+ assert len(jsonl_files) == 1
619
+ lines = jsonl_files[0].read_text().strip().splitlines()
620
+ assert len(lines) == 5
621
+ finally:
622
+ resolver.set_base_dir(original)
623
+
624
+
625
+ def test_writer_coerces_unserializable_payload_values(tmp_path):
626
+ import failproofai_sdk._resolver as resolver
627
+ original = resolver._base_dir
628
+ try:
629
+ resolver.set_base_dir(tmp_path)
630
+ writer = EventWriter(flush_interval=60)
631
+ value = datetime(2026, 7, 17, 12, 34, 56, tzinfo=timezone.utc)
632
+ writer.submit({"timestamp": "t", "session_id": "s1", "agent_id": "a1", "type": "tool_result", "output": value})
633
+ writer.flush_now()
634
+
635
+ path = next((tmp_path / "events").glob("*.jsonl"))
636
+ assert json.loads(path.read_text())["output"] == str(value)
637
+ assert writer._thread.is_alive()
638
+ finally:
639
+ resolver.set_base_dir(original)
640
+
641
+
642
+ def test_writer_requeues_batch_after_write_failure(monkeypatch):
643
+ writer = EventWriter(flush_interval=60)
644
+ entries = [{"i": 1}, {"i": 2}]
645
+ for entry in entries:
646
+ writer.submit(entry)
647
+
648
+ monkeypatch.setattr(writer, "_write_batch", lambda _entries: (_ for _ in ()).throw(OSError("disk full")))
649
+ with pytest.raises(OSError, match="disk full"):
650
+ writer.flush_now()
651
+ assert list(writer._queue) == entries
652
+
653
+
654
+ def test_writer_no_flush_when_empty(tmp_path):
655
+ import failproofai_sdk._resolver as resolver
656
+ original = resolver._base_dir
657
+ try:
658
+ resolver.set_base_dir(tmp_path)
659
+ writer = EventWriter(flush_interval=60)
660
+ writer.flush_now() # nothing in queue
661
+
662
+ events_dir = tmp_path / "events"
663
+ assert not events_dir.exists() or list(events_dir.glob("*")) == []
664
+ finally:
665
+ resolver.set_base_dir(original)
666
+
667
+
668
+ # ---------------------------------------------------------------------------
669
+ # configure() integration
670
+ # ---------------------------------------------------------------------------
671
+
672
+ def test_configure_custom_base_dir(tmp_path):
673
+ failproofai_sdk.configure(base_dir=tmp_path, flush_interval=60)
674
+ failproofai_sdk.event.agent_start(session_id="s1", agent_id="a1", goal="test configure")
675
+ failproofai_sdk._writer.flush_now()
676
+
677
+ events_dir = tmp_path / "events"
678
+ jsonl_files = list(events_dir.glob("*.jsonl"))
679
+ assert len(jsonl_files) == 1
680
+ parsed = json.loads(jsonl_files[0].read_text().strip().splitlines()[0])
681
+ assert parsed["goal"] == "test configure"
682
+
683
+
684
+ # ---------------------------------------------------------------------------
685
+ # atexit flush — events must survive process exit without explicit flush_now()
686
+ # ---------------------------------------------------------------------------
687
+
688
+ def test_atexit_flushes_on_process_exit(tmp_path):
689
+ import subprocess
690
+ import sys
691
+ script = f"""
692
+ import failproofai_sdk
693
+ import failproofai_sdk._resolver as resolver
694
+ from pathlib import Path
695
+ resolver.set_base_dir(Path(r'{tmp_path}'))
696
+ failproofai_sdk.event.agent_start(session_id='s1', agent_id='a1')
697
+ # intentionally no flush_now() — atexit must handle it
698
+ """
699
+ result = subprocess.run([sys.executable, "-c", script], capture_output=True)
700
+ assert result.returncode == 0, result.stderr.decode()
701
+ events_dir = tmp_path / "events"
702
+ jsonl_files = list(events_dir.glob("*.jsonl"))
703
+ assert len(jsonl_files) == 1
704
+ parsed = json.loads(jsonl_files[0].read_text().strip().splitlines()[0])
705
+ assert parsed["type"] == "agent_start"
706
+
707
+
708
+ # ---------------------------------------------------------------------------
709
+ # Timestamp format
710
+ # ---------------------------------------------------------------------------
711
+
712
+ def test_timestamp_format(ns, mock_writer):
713
+ ns.agent_start(session_id="s1", agent_id="a1")
714
+ d = mock_writer.last()
715
+ ts = d["timestamp"]
716
+ # ISO 8601 with microsecond precision, UTC: 2026-04-01T12:34:56.789012Z
717
+ assert ts.endswith("Z")
718
+ assert "T" in ts
719
+ # Must be parseable
720
+ from datetime import datetime, timezone
721
+ dt = datetime.fromisoformat(ts.rstrip("Z") + "+00:00")
722
+ assert dt.tzinfo is not None
723
+
724
+
725
+ # ---------------------------------------------------------------------------
726
+ # Base dir resolution — set_base_dir() > ~/.failproofai/custom-agents
727
+ # ---------------------------------------------------------------------------
728
+
729
+ def test_base_dir_from_env_var_is_ignored(monkeypatch, tmp_path):
730
+ """`AGENTEYE_HOME` no longer resolves the spool; see `_resolver`."""
731
+ import failproofai_sdk._resolver as resolver
732
+ from pathlib import Path
733
+ monkeypatch.setattr(resolver, "_base_dir", None)
734
+ monkeypatch.setattr(Path, "home", staticmethod(lambda: tmp_path))
735
+ monkeypatch.delenv("FAILPROOFAI_HOME", raising=False)
736
+ monkeypatch.setenv("AGENTEYE_HOME", str(tmp_path / "ignored"))
737
+ assert resolver.get_base_dir() == tmp_path / ".failproofai" / "custom-agents"
738
+
739
+
740
+ def test_base_dir_from_failproofai_home(monkeypatch, tmp_path):
741
+ """The one variable that moves it — and it stays inside the umbrella."""
742
+ import failproofai_sdk._resolver as resolver
743
+ monkeypatch.setattr(resolver, "_base_dir", None)
744
+ monkeypatch.setenv("FAILPROOFAI_HOME", str(tmp_path))
745
+ assert resolver.get_base_dir() == tmp_path / "custom-agents"
746
+
747
+
748
+ def test_set_base_dir_overrides_env_var(monkeypatch, tmp_path):
749
+ import failproofai_sdk._resolver as resolver
750
+ other = tmp_path / "explicit"
751
+ monkeypatch.setattr(resolver, "_base_dir", None)
752
+ monkeypatch.setenv("FAILPROOFAI_HOME", str(tmp_path / "from-env"))
753
+ resolver.set_base_dir(other)
754
+ try:
755
+ assert resolver.get_base_dir() == other
756
+ finally:
757
+ resolver.set_base_dir(None)
758
+
759
+
760
+ def test_default_when_neither_set(monkeypatch):
761
+ import failproofai_sdk._resolver as resolver
762
+ monkeypatch.setattr(resolver, "_base_dir", None)
763
+ monkeypatch.delenv("AGENTEYE_HOME", raising=False)
764
+ monkeypatch.delenv("FAILPROOFAI_HOME", raising=False)
765
+ assert resolver.get_base_dir() == Path.home() / ".failproofai" / "custom-agents"
766
+
767
+
768
+ def test_writer_uses_failproofai_home_base_dir(monkeypatch, tmp_path):
769
+ """End-to-end: the writer spools under $FAILPROOFAI_HOME/custom-agents."""
770
+ import failproofai_sdk._resolver as resolver
771
+ monkeypatch.setattr(resolver, "_base_dir", None)
772
+ monkeypatch.setenv("FAILPROOFAI_HOME", str(tmp_path))
773
+ writer = EventWriter(flush_interval=60)
774
+ writer.submit({"timestamp": "t", "session_id": "s1", "agent_id": "a1", "type": "agent_start"})
775
+ writer.flush_now()
776
+
777
+ jsonl_files = list((tmp_path / "custom-agents" / "events").glob("*.jsonl"))
778
+ assert len(jsonl_files) == 1
779
+
780
+
781
+ def test_writer_ignores_agenteye_home(monkeypatch, tmp_path):
782
+ """End-to-end counterpart: the retired variable moves nothing on disk.
783
+
784
+ Asserting the batch's real location, not just what `get_base_dir()` returns
785
+ — the resolver could be right while the writer still resolved separately.
786
+ """
787
+ import failproofai_sdk._resolver as resolver
788
+ monkeypatch.setattr(resolver, "_base_dir", None)
789
+ monkeypatch.setenv("FAILPROOFAI_HOME", str(tmp_path / "umbrella"))
790
+ monkeypatch.setenv("AGENTEYE_HOME", str(tmp_path / "legacy"))
791
+ writer = EventWriter(flush_interval=60)
792
+ writer.submit({"timestamp": "t", "session_id": "s1", "agent_id": "a1", "type": "agent_start"})
793
+ writer.flush_now()
794
+
795
+ assert not (tmp_path / "legacy").exists(), "the retired variable still steered the writer"
796
+ assert len(list((tmp_path / "umbrella" / "custom-agents" / "events").glob("*.jsonl"))) == 1
797
+
798
+
799
+ # ---------------------------------------------------------------------------
800
+ # Environment
801
+ # ---------------------------------------------------------------------------
802
+
803
+ @pytest.fixture(autouse=True)
804
+ def _reset_environment():
805
+ """Reset the global environment state between every test."""
806
+ import failproofai_sdk._environment as env_mod
807
+ original = env_mod._environment
808
+ yield
809
+ env_mod._environment = original
810
+
811
+
812
+ def test_environment_defaults_to_dev(ns, mock_writer):
813
+ import failproofai_sdk._environment as env_mod
814
+ env_mod._environment = None
815
+ ns.agent_start(session_id="s1", agent_id="a1")
816
+ assert mock_writer.last()["environment"] == "dev"
817
+
818
+
819
+ def test_environment_from_configure(tmp_path, mock_writer):
820
+ import failproofai_sdk._environment as env_mod
821
+ env_mod.set_environment("staging")
822
+ ns = EventNamespace(mock_writer)
823
+ ns.agent_start(session_id="s1", agent_id="a1")
824
+ assert mock_writer.last()["environment"] == "staging"
825
+
826
+
827
+ def test_environment_from_env_var(ns, mock_writer, monkeypatch):
828
+ import failproofai_sdk._environment as env_mod
829
+ env_mod._environment = None
830
+ monkeypatch.setenv("AGENTEYE_ENVIRONMENT", "production")
831
+ ns.agent_start(session_id="s1", agent_id="a1")
832
+ assert mock_writer.last()["environment"] == "production"
833
+
834
+
835
+ def test_configure_overrides_env_var(ns, mock_writer, monkeypatch):
836
+ import failproofai_sdk._environment as env_mod
837
+ monkeypatch.setenv("AGENTEYE_ENVIRONMENT", "production")
838
+ env_mod.set_environment("staging")
839
+ ns.agent_start(session_id="s1", agent_id="a1")
840
+ assert mock_writer.last()["environment"] == "staging"
841
+
842
+
843
+ def test_a_comma_in_configure_environment_is_refused_rather_than_dropped(
844
+ ns, mock_writer
845
+ ):
846
+ # Ingest splits `environment` on commas to build its facets, so a line whose
847
+ # environment contains one is discarded WHOLE: 200 with
848
+ # `{"accepted":0,"skipped":N}`, the daemon deletes the delivered batch, and
849
+ # the run is simply never in the dashboard. Measured against the running
850
+ # stack before this check existed. `failproofaid` already refuses a comma in
851
+ # `collector.environment`; the SDK writes the same field and did not.
852
+ import failproofai_sdk._environment as env_mod
853
+
854
+ with pytest.raises(ValueError, match="comma"):
855
+ env_mod.set_environment("prod,eu")
856
+ # And the rejected call must not have taken effect.
857
+ env_mod._environment = None
858
+ ns.agent_start(session_id="s1", agent_id="a1")
859
+ assert "," not in mock_writer.last()["environment"]
860
+
861
+
862
+ def test_a_comma_in_the_env_var_warns_and_falls_back_instead_of_raising(
863
+ ns, mock_writer, monkeypatch, caplog
864
+ ):
865
+ # An env var is read lazily, inside `to_dict()` on whatever event happens to
866
+ # be next — raising there would take the caller's agent down from a line of
867
+ # telemetry, which a library in someone else's process must not do. Warn,
868
+ # and land the events under a visibly wrong environment rather than nowhere.
869
+ import logging
870
+
871
+ import failproofai_sdk._environment as env_mod
872
+
873
+ env_mod._environment = None
874
+ monkeypatch.setenv("AGENTEYE_ENVIRONMENT", "prod,eu")
875
+ with caplog.at_level(logging.WARNING, logger="failproofai_sdk"):
876
+ ns.agent_start(session_id="s1", agent_id="a1")
877
+ assert mock_writer.last()["environment"] == "dev"
878
+ assert "comma" in caplog.text
879
+
880
+
881
+ def test_environment_is_reserved(ns):
882
+ with pytest.raises(ValueError, match="Reserved field"):
883
+ ns.agent_start(session_id="s1", agent_id="a1", environment="bad")
884
+
885
+
886
+ def test_environment_field_ordering(ns, mock_writer):
887
+ import failproofai_sdk._environment as env_mod
888
+ env_mod.set_environment("qa")
889
+ ns.agent_start(session_id="s1", agent_id="a1", goal="test")
890
+ keys = list(mock_writer.last().keys())
891
+ assert keys.index("environment") > keys.index("type")
892
+ assert keys.index("environment") < keys.index("goal")
893
+
894
+
895
+ def test_environment_in_all_event_types(ns, mock_writer):
896
+ import failproofai_sdk._environment as env_mod
897
+ env_mod.set_environment("canary")
898
+ ns.agent_start(session_id="s1", agent_id="a1")
899
+ ns.tool_use(session_id="s1", agent_id="a1", tool_name="t", tool_call_id="tc1")
900
+ ns.error(session_id="s1", agent_id="a1", error_type="E", message="m")
901
+ for entry in mock_writer.entries:
902
+ assert entry["environment"] == "canary"
903
+
904
+
905
+ def test_environment_integration(tmp_path):
906
+ import failproofai_sdk._resolver as resolver
907
+ import failproofai_sdk._environment as env_mod
908
+ original_dir = resolver._base_dir
909
+ try:
910
+ resolver.set_base_dir(tmp_path)
911
+ env_mod.set_environment("integration-test")
912
+ writer = EventWriter(flush_interval=60)
913
+ ns = EventNamespace(writer)
914
+ ns.agent_start(session_id="s1", agent_id="a1")
915
+ writer.flush_now()
916
+
917
+ events_dir = tmp_path / "events"
918
+ jsonl_files = list(events_dir.glob("*.jsonl"))
919
+ assert len(jsonl_files) == 1
920
+ parsed = json.loads(jsonl_files[0].read_text().strip().splitlines()[0])
921
+ assert parsed["environment"] == "integration-test"
922
+ finally:
923
+ resolver.set_base_dir(original_dir)