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,722 @@
1
+ """The CLI's config lives inside a home another product owns.
2
+
3
+ `~/.failproofai/` belongs to the Enforcement CLI. Its layout is declared in
4
+ `src/hooks/fp-home.ts`, mirrored for the daemon in
5
+ `crates/failproofaid/src/paths.rs`, and a reset walks it with
6
+ `rmSync(recursive)`. So the two things that matter here are not "can we write a
7
+ file" — they are:
8
+
9
+ * we CREATE and never destroy. Anything already in that home survives every
10
+ path this module can take, including the failure paths.
11
+ * we are visible to the layout register, so a future migration knows a
12
+ credential lives under `fpcli/`. That half is asserted on the TS side
13
+ (`HOME_CLASSES`); what is asserted here is the Python half agreeing about
14
+ where the file goes.
15
+
16
+ The legacy `~/.fp/cli.json` is read once and never deleted: `load_config`
17
+ adopts a pre-move session and copies it to the new path, leaving the original
18
+ so a downgrade still finds it. Both halves are tested below.
19
+
20
+ This paragraph claimed the file was "neither read nor deleted" and that the
21
+ move forced a re-login. Adoption landed after the move, the tests 200 lines
22
+ down were updated for it, and this header was not — which is how a docstring
23
+ ends up contradicting the assertions in its own file.
24
+ """
25
+ from __future__ import annotations
26
+
27
+ import errno
28
+ import json
29
+ import os
30
+ import stat
31
+ from pathlib import Path
32
+
33
+ import pytest
34
+
35
+ from fp_cli import config as cfg
36
+
37
+
38
+ @pytest.fixture
39
+ def clean_env(monkeypatch, tmp_path):
40
+ """No inherited home vars, and `Path.home()` pinned inside the tmp dir."""
41
+ for var in ("FP_HOME", "FAILPROOFAI_HOME"):
42
+ monkeypatch.delenv(var, raising=False)
43
+ fake_home = tmp_path / "home"
44
+ fake_home.mkdir()
45
+ monkeypatch.setattr(Path, "home", staticmethod(lambda: fake_home))
46
+ return fake_home
47
+
48
+
49
+ def _child_writer(n: int, home: str) -> None: # pragma: no cover - child process
50
+ """Runs in a spawned interpreter, so it must be importable by name."""
51
+ os.environ["FP_HOME"] = home
52
+ os.environ.pop("FAILPROOFAI_HOME", None)
53
+ from fp_cli import config as c
54
+
55
+ for _ in range(15):
56
+ c.save_config(c.CliConfig(session_token=f"proc-{n}"))
57
+
58
+
59
+ # ── Where the file resolves ──────────────────────────────────────────────────
60
+
61
+
62
+ def test_default_is_under_the_failproofai_home(clean_env):
63
+ assert cfg.config_path() == clean_env / ".failproofai" / "fpcli" / "cli-auth.json"
64
+
65
+
66
+ def test_failproofai_home_env_appends_the_subdir(clean_env, monkeypatch, tmp_path):
67
+ """`FAILPROOFAI_HOME` names the HOME ROOT, so `fpcli/` is appended to it."""
68
+ monkeypatch.setenv("FAILPROOFAI_HOME", str(tmp_path / "elsewhere"))
69
+ assert cfg.config_path() == tmp_path / "elsewhere" / "fpcli" / "cli-auth.json"
70
+
71
+
72
+ def test_fp_home_is_used_as_is(clean_env, monkeypatch, tmp_path):
73
+ """`FP_HOME` names the CLI's OWN directory — no subdir appended.
74
+
75
+ That is what it meant before the move, so an existing export keeps
76
+ addressing the same directory it always did.
77
+ """
78
+ monkeypatch.setenv("FP_HOME", str(tmp_path / "mine"))
79
+ assert cfg.config_path() == tmp_path / "mine" / "cli-auth.json"
80
+
81
+
82
+ def test_fp_home_wins_over_failproofai_home(clean_env, monkeypatch, tmp_path):
83
+ monkeypatch.setenv("FP_HOME", str(tmp_path / "mine"))
84
+ monkeypatch.setenv("FAILPROOFAI_HOME", str(tmp_path / "theirs"))
85
+ assert cfg.config_path() == tmp_path / "mine" / "cli-auth.json"
86
+
87
+
88
+ @pytest.mark.parametrize("var", ["FP_HOME", "FAILPROOFAI_HOME"])
89
+ def test_an_empty_env_var_is_not_a_path(clean_env, monkeypatch, var):
90
+ """`FP_HOME=` must fall through, not resolve the config to `/cli-auth.json`."""
91
+ monkeypatch.setenv(var, "")
92
+ assert cfg.config_path() == clean_env / ".failproofai" / "fpcli" / "cli-auth.json"
93
+
94
+
95
+ # ── Create, never destroy ────────────────────────────────────────────────────
96
+
97
+
98
+ def test_creates_the_whole_chain_when_nothing_exists(clean_env):
99
+ assert not (clean_env / ".failproofai").exists()
100
+ cfg.save_config(cfg.CliConfig(session_token="t"))
101
+ assert cfg.config_path().is_file()
102
+
103
+
104
+ def test_a_populated_failproofai_home_is_left_intact(clean_env):
105
+ """The case this whole design is about: never wipe somebody else's home."""
106
+ home = clean_env / ".failproofai"
107
+ (home / "policies").mkdir(parents=True)
108
+ (home / "policies" / "mine.mjs").write_text("// hand-written")
109
+ (home / "credentials.json").write_text('{"token": "enforcement-cli"}')
110
+ (home / "VERSION").write_text('{"layout": 4}')
111
+ (home / "state").mkdir()
112
+ (home / "state" / "spool").mkdir()
113
+
114
+ cfg.save_config(cfg.CliConfig(session_token="t"))
115
+
116
+ assert (home / "policies" / "mine.mjs").read_text() == "// hand-written"
117
+ assert json.loads((home / "credentials.json").read_text())["token"] == "enforcement-cli"
118
+ assert json.loads((home / "VERSION").read_text())["layout"] == 4
119
+ assert (home / "state" / "spool").is_dir()
120
+ assert cfg.config_path().is_file()
121
+
122
+
123
+ def test_other_files_in_fpcli_survive_a_save(clean_env):
124
+ """We own `cli-auth.json`, not the directory it sits in."""
125
+ fpcli = clean_env / ".failproofai" / "fpcli"
126
+ fpcli.mkdir(parents=True)
127
+ (fpcli / "unrelated.json").write_text("{}")
128
+ cfg.save_config(cfg.CliConfig(session_token="t"))
129
+ assert (fpcli / "unrelated.json").is_file()
130
+
131
+
132
+ def test_an_existing_auth_file_is_replaced_not_merged(clean_env):
133
+ """A stale session must not leak fields into the new one."""
134
+ cfg.save_config(cfg.CliConfig(session_token="old", email="old@x", org="old-org"))
135
+ cfg.save_config(cfg.CliConfig(session_token="new"))
136
+ loaded = cfg.load_config()
137
+ assert loaded.session_token == "new"
138
+ assert loaded.email is None
139
+ assert loaded.org is None
140
+
141
+
142
+ def test_save_is_idempotent(clean_env):
143
+ for _ in range(3):
144
+ cfg.save_config(cfg.CliConfig(session_token="t"))
145
+ assert cfg.load_config().session_token == "t"
146
+
147
+
148
+ # ── Hostile filesystem ───────────────────────────────────────────────────────
149
+
150
+
151
+ def test_home_occupied_by_a_regular_file_raises_not_corrupts(clean_env):
152
+ """`~/.failproofai` as a FILE must fail loudly and leave it byte-identical."""
153
+ home = clean_env / ".failproofai"
154
+ home.write_text("not a directory")
155
+ with pytest.raises(OSError):
156
+ cfg.save_config(cfg.CliConfig(session_token="t"))
157
+ assert home.read_text() == "not a directory"
158
+
159
+
160
+ def test_fpcli_occupied_by_a_regular_file_raises(clean_env):
161
+ fpcli = clean_env / ".failproofai" / "fpcli"
162
+ fpcli.parent.mkdir(parents=True)
163
+ fpcli.write_text("not a directory")
164
+ with pytest.raises(OSError):
165
+ cfg.save_config(cfg.CliConfig(session_token="t"))
166
+ assert fpcli.read_text() == "not a directory"
167
+
168
+
169
+ @pytest.mark.skipif(os.geteuid() == 0, reason="root ignores directory permissions")
170
+ def test_a_read_only_home_raises_and_changes_nothing(clean_env):
171
+ home = clean_env / ".failproofai"
172
+ home.mkdir()
173
+ (home / "credentials.json").write_text("keep me")
174
+ home.chmod(0o500)
175
+ try:
176
+ with pytest.raises(OSError):
177
+ cfg.save_config(cfg.CliConfig(session_token="t"))
178
+ assert (home / "credentials.json").read_text() == "keep me"
179
+ finally:
180
+ home.chmod(0o700) # so tmp_path cleanup can run
181
+
182
+
183
+ def test_a_symlinked_home_is_followed(clean_env, tmp_path):
184
+ """Some setups symlink the home onto another volume."""
185
+ real = tmp_path / "real-home"
186
+ real.mkdir()
187
+ (clean_env / ".failproofai").symlink_to(real, target_is_directory=True)
188
+ cfg.save_config(cfg.CliConfig(session_token="t"))
189
+ assert (real / "fpcli" / "cli-auth.json").is_file()
190
+
191
+
192
+ def test_unreadable_config_reads_as_absent_rather_than_raising(clean_env):
193
+ """A corrupt file must not crash every command — it means "logged out"."""
194
+ cfg.save_config(cfg.CliConfig(session_token="t"))
195
+ cfg.config_path().write_text("{ not json")
196
+ assert cfg.load_config().session_token is None
197
+
198
+
199
+ # ── Permissions ──────────────────────────────────────────────────────────────
200
+
201
+
202
+ def test_auth_file_is_owner_only(clean_env):
203
+ cfg.save_config(cfg.CliConfig(session_token="t"))
204
+ assert stat.S_IMODE(os.stat(cfg.config_path()).st_mode) == 0o600
205
+
206
+
207
+ def test_rewriting_a_loosened_file_restores_0600(clean_env):
208
+ cfg.save_config(cfg.CliConfig(session_token="t"))
209
+ cfg.config_path().chmod(0o644)
210
+ cfg.save_config(cfg.CliConfig(session_token="t2"))
211
+ assert stat.S_IMODE(os.stat(cfg.config_path()).st_mode) == 0o600
212
+
213
+
214
+ # ── The legacy ~/.fp/cli.json ────────────────────────────────────────────────
215
+
216
+
217
+ def _plant_legacy(home: Path) -> Path:
218
+ old = home / ".fp"
219
+ old.mkdir()
220
+ path = old / "cli.json"
221
+ path.write_text(json.dumps({"session_token": "legacy", "email": "old@x"}))
222
+ return path
223
+
224
+
225
+ def test_a_legacy_session_is_adopted(clean_env):
226
+ """Nobody is signed out by the move. The old session is picked up as-is."""
227
+ _plant_legacy(clean_env)
228
+ loaded = cfg.load_config()
229
+ assert loaded.session_token == "legacy"
230
+ assert loaded.email == "old@x"
231
+
232
+
233
+ def test_adoption_writes_the_session_to_the_new_location(clean_env):
234
+ _plant_legacy(clean_env)
235
+ assert not cfg.config_path().exists()
236
+ cfg.load_config()
237
+ assert json.loads(cfg.config_path().read_text())["session_token"] == "legacy"
238
+
239
+
240
+ def test_adoption_is_a_copy_so_a_downgrade_still_works(clean_env):
241
+ """The old `fp` must still find its session if someone rolls back."""
242
+ legacy = _plant_legacy(clean_env)
243
+ cfg.load_config()
244
+ assert json.loads(legacy.read_text())["session_token"] == "legacy"
245
+
246
+
247
+ def test_adoption_does_not_happen_when_a_current_session_exists(clean_env):
248
+ _plant_legacy(clean_env)
249
+ cfg.save_config(cfg.CliConfig(session_token="current"))
250
+ assert cfg.load_config().session_token == "current"
251
+
252
+
253
+ def test_a_corrupt_legacy_file_is_skipped_not_adopted(clean_env):
254
+ old = clean_env / ".fp"
255
+ old.mkdir()
256
+ (old / "cli.json").write_text("{ not json")
257
+ assert cfg.load_config().session_token is None
258
+
259
+
260
+ @pytest.mark.skipif(os.geteuid() == 0, reason="root ignores directory permissions")
261
+ def test_an_unwritable_target_still_hands_back_the_session(clean_env):
262
+ """Our housekeeping must never be the reason someone is logged out."""
263
+ _plant_legacy(clean_env)
264
+ home = clean_env / ".failproofai"
265
+ home.mkdir()
266
+ home.chmod(0o500)
267
+ try:
268
+ assert cfg.load_config().session_token == "legacy"
269
+ finally:
270
+ home.chmod(0o700)
271
+
272
+
273
+ def test_a_relocated_legacy_session_is_adopted(clean_env, monkeypatch, tmp_path):
274
+ """`FP_HOME` users are carried across too — their old file is beside the new."""
275
+ relocated = tmp_path / "custom"
276
+ relocated.mkdir()
277
+ (relocated / "cli.json").write_text(json.dumps({"session_token": "relocated"}))
278
+ monkeypatch.setenv("FP_HOME", str(relocated))
279
+ assert cfg.load_config().session_token == "relocated"
280
+
281
+
282
+ def test_fp_home_does_not_reach_into_the_home_directory(clean_env, monkeypatch, tmp_path):
283
+ """Someone who redirected the config said where it lives. Respect that.
284
+
285
+ Reaching past `FP_HOME` into `~/.fp` would adopt a session from a different
286
+ context — another tenant, or another user's leftovers on a shared box.
287
+ """
288
+ _plant_legacy(clean_env) # a session at ~/.fp/cli.json
289
+ empty = tmp_path / "empty"
290
+ empty.mkdir()
291
+ monkeypatch.setenv("FP_HOME", str(empty))
292
+ assert cfg.load_config().session_token is None
293
+
294
+
295
+ def test_a_legacy_session_is_never_deleted(clean_env):
296
+ """Deleting a file the user did not ask us to touch is not ours to do."""
297
+ legacy = _plant_legacy(clean_env)
298
+ cfg.save_config(cfg.CliConfig(session_token="new"))
299
+ assert legacy.is_file()
300
+ assert json.loads(legacy.read_text())["session_token"] == "legacy"
301
+
302
+
303
+ def test_a_relocated_legacy_session_is_detected_too(clean_env, monkeypatch, tmp_path):
304
+ """The group most likely to be broken silently: `FP_HOME` users.
305
+
306
+ The move changed the FILENAME as well as the directory, so somebody who
307
+ exported `FP_HOME` has their old session at `$FP_HOME/cli.json` and may own
308
+ no `~/.fp` at all. Checking only the default hands exactly those users an
309
+ unexplained logout.
310
+ """
311
+ relocated = tmp_path / "custom"
312
+ relocated.mkdir()
313
+ (relocated / "cli.json").write_text('{"session_token": "old"}')
314
+ monkeypatch.setenv("FP_HOME", str(relocated))
315
+
316
+ assert cfg.legacy_install_detected() is True
317
+ assert cfg.legacy_config_path() == relocated / "cli.json"
318
+
319
+
320
+ def test_the_relocated_path_is_named_before_the_default(clean_env, monkeypatch, tmp_path):
321
+ """When both exist, name the one THIS invocation would have read.
322
+
323
+ Naming the default instead tells an `FP_HOME` user to delete an unrelated
324
+ file — on a machine they may share.
325
+ """
326
+ _plant_legacy(clean_env) # the default, ~/.fp/cli.json
327
+ relocated = tmp_path / "custom"
328
+ relocated.mkdir()
329
+ (relocated / "cli.json").write_text('{"session_token": "old"}')
330
+ monkeypatch.setenv("FP_HOME", str(relocated))
331
+
332
+ assert cfg.legacy_config_path() == relocated / "cli.json"
333
+
334
+
335
+ def test_no_legacy_anywhere_reports_none(clean_env):
336
+ assert cfg.legacy_config_path() is None
337
+ assert cfg.legacy_install_detected() is False
338
+
339
+
340
+ def test_legacy_install_detected_only_before_the_first_login(clean_env):
341
+ _plant_legacy(clean_env)
342
+ assert cfg.legacy_install_detected() is True
343
+ cfg.save_config(cfg.CliConfig(session_token="new"))
344
+ assert cfg.legacy_install_detected() is False
345
+
346
+
347
+ def test_no_legacy_file_means_no_notice(clean_env):
348
+ assert cfg.legacy_install_detected() is False
349
+
350
+
351
+ # ── The neighbouring roots stay separate ─────────────────────────────────────
352
+
353
+
354
+ def test_the_sdk_spool_root_is_untouched(clean_env, monkeypatch):
355
+ """`~/.agenteye` is a wire contract with the collector, not a preference.
356
+
357
+ A save must not create, move or read it — renaming it from this side would
358
+ write events into a directory nothing watches.
359
+ """
360
+ monkeypatch.setenv("AGENTEYE_HOME", str(clean_env / ".agenteye"))
361
+ cfg.save_config(cfg.CliConfig(session_token="t"))
362
+ assert not (clean_env / ".agenteye").exists()
363
+
364
+
365
+ # ── Symlinks: we write into a directory full of another product's secrets ────
366
+
367
+
368
+ def test_a_symlinked_config_never_writes_through_to_a_neighbour(clean_env):
369
+ """The bug the move created.
370
+
371
+ `O_TRUNC` follows symlinks. A link at `cli-auth.json` pointing at
372
+ `../credentials.json` therefore made `fp login` truncate the Enforcement
373
+ CLI's token and write ours over it — no error, no trace. Harmless while the
374
+ CLI owned `~/.fp` outright; not harmless in a shared home.
375
+ """
376
+ home = clean_env / ".failproofai"
377
+ (home / "fpcli").mkdir(parents=True)
378
+ victim = home / "credentials.json"
379
+ victim.write_text('{"token": "enforcement-cli"}')
380
+ cfg.config_path().symlink_to(victim)
381
+
382
+ with pytest.raises(OSError, match="symbolic link"):
383
+ cfg.save_config(cfg.CliConfig(session_token="ours"))
384
+
385
+ assert json.loads(victim.read_text())["token"] == "enforcement-cli"
386
+
387
+
388
+ def test_a_dangling_symlinked_config_is_refused_too(clean_env):
389
+ """Not just "does the target matter" — following at all is the bug."""
390
+ (clean_env / ".failproofai" / "fpcli").mkdir(parents=True)
391
+ cfg.config_path().symlink_to(clean_env / "nowhere.json")
392
+ with pytest.raises(OSError, match="symbolic link"):
393
+ cfg.save_config(cfg.CliConfig(session_token="t"))
394
+ assert not (clean_env / "nowhere.json").exists()
395
+
396
+
397
+ def test_the_refusal_does_not_delete_the_link(clean_env):
398
+ """A symlink is something a person put there. Refuse; never clean up."""
399
+ (clean_env / ".failproofai" / "fpcli").mkdir(parents=True)
400
+ target = clean_env / "target.json"
401
+ target.write_text("{}")
402
+ cfg.config_path().symlink_to(target)
403
+ with pytest.raises(OSError):
404
+ cfg.save_config(cfg.CliConfig(session_token="t"))
405
+ assert cfg.config_path().is_symlink()
406
+
407
+
408
+ def test_a_symlinked_fpcli_directory_is_followed(clean_env, tmp_path):
409
+ """Only the FINAL component is guarded. A relocated directory is fine."""
410
+ real = tmp_path / "real-fpcli"
411
+ real.mkdir()
412
+ (clean_env / ".failproofai").mkdir()
413
+ (clean_env / ".failproofai" / "fpcli").symlink_to(real, target_is_directory=True)
414
+ cfg.save_config(cfg.CliConfig(session_token="t"))
415
+ assert (real / "cli-auth.json").is_file()
416
+
417
+
418
+ def test_a_config_path_occupied_by_a_directory_raises(clean_env):
419
+ (clean_env / ".failproofai" / "fpcli").mkdir(parents=True)
420
+ cfg.config_path().mkdir()
421
+ with pytest.raises(OSError):
422
+ cfg.save_config(cfg.CliConfig(session_token="t"))
423
+ assert cfg.config_path().is_dir()
424
+
425
+
426
+ def test_a_broken_symlinked_home_raises_rather_than_writing_elsewhere(clean_env, tmp_path):
427
+ (clean_env / ".failproofai").symlink_to(tmp_path / "does-not-exist", target_is_directory=True)
428
+ with pytest.raises(OSError):
429
+ cfg.save_config(cfg.CliConfig(session_token="t"))
430
+
431
+
432
+ # ── Permissions of what we create ────────────────────────────────────────────
433
+
434
+
435
+ @pytest.mark.skipif(os.geteuid() == 0, reason="root ignores directory permissions")
436
+ def test_created_directories_are_not_group_or_world_writable(clean_env):
437
+ """`fpcli/` holds a credential, so it is created `0700` regardless of umask.
438
+
439
+ A group-writable directory does not expose the `0600` file inside it, but it
440
+ does let anyone in the group replace that file — which is a session swap.
441
+
442
+ The shared parent is deliberately NOT asserted on: when we are the first to
443
+ create `~/.failproofai` we leave it to the umask, exactly as the Enforcement
444
+ CLI would, and when it already exists we must not re-permission a home we do
445
+ not own.
446
+ """
447
+ cfg.save_config(cfg.CliConfig(session_token="t"))
448
+ mode = stat.S_IMODE(os.stat(clean_env / ".failproofai" / "fpcli").st_mode)
449
+ assert not mode & stat.S_IWGRP, f"fpcli/ is group-writable ({oct(mode)})"
450
+ assert not mode & stat.S_IWOTH, f"fpcli/ is world-writable ({oct(mode)})"
451
+ assert not mode & stat.S_IRGRP and not mode & stat.S_IROTH, oct(mode)
452
+
453
+
454
+ def test_an_existing_shared_home_is_never_re_permissioned(clean_env):
455
+ """We hardened `fpcli/`; doing the same to a home we do not own is not ours."""
456
+ home = clean_env / ".failproofai"
457
+ home.mkdir(mode=0o755)
458
+ before = stat.S_IMODE(os.stat(home).st_mode)
459
+ cfg.save_config(cfg.CliConfig(session_token="t"))
460
+ assert stat.S_IMODE(os.stat(home).st_mode) == before
461
+
462
+
463
+ def test_an_existing_fpcli_dir_keeps_its_mode(clean_env):
464
+ """`exist_ok=True` must not chmod — a user who widened it chose that."""
465
+ fpcli = clean_env / ".failproofai" / "fpcli"
466
+ fpcli.mkdir(parents=True, mode=0o755)
467
+ before = stat.S_IMODE(os.stat(fpcli).st_mode)
468
+ cfg.save_config(cfg.CliConfig(session_token="t"))
469
+ assert stat.S_IMODE(os.stat(fpcli).st_mode) == before
470
+
471
+
472
+ @pytest.mark.skipif(os.geteuid() == 0, reason="root ignores directory permissions")
473
+ def test_an_untraversable_home_raises_and_keeps_its_contents(clean_env):
474
+ home = clean_env / ".failproofai"
475
+ (home / "fpcli").mkdir(parents=True)
476
+ (home / "credentials.json").write_text("keep me")
477
+ home.chmod(0o000)
478
+ try:
479
+ with pytest.raises(OSError):
480
+ cfg.save_config(cfg.CliConfig(session_token="t"))
481
+ finally:
482
+ home.chmod(0o700)
483
+ assert (home / "credentials.json").read_text() == "keep me"
484
+
485
+
486
+ @pytest.mark.skipif(os.geteuid() == 0, reason="root can read any file")
487
+ def test_an_unreadable_config_reads_as_logged_out(clean_env):
488
+ """EACCES on read must not crash every command."""
489
+ cfg.save_config(cfg.CliConfig(session_token="t"))
490
+ cfg.config_path().chmod(0o000)
491
+ try:
492
+ assert cfg.load_config().session_token is None
493
+ finally:
494
+ cfg.config_path().chmod(0o600)
495
+
496
+
497
+ # ── Env var shapes ───────────────────────────────────────────────────────────
498
+
499
+
500
+ def test_a_trailing_slash_resolves_to_the_same_file(clean_env, monkeypatch, tmp_path):
501
+ monkeypatch.setenv("FAILPROOFAI_HOME", f"{tmp_path / 'h'}/")
502
+ assert cfg.config_path() == tmp_path / "h" / "fpcli" / "cli-auth.json"
503
+
504
+
505
+ def test_a_relative_env_path_is_taken_relative_to_cwd(clean_env, monkeypatch, tmp_path):
506
+ """Documented behaviour rather than an accident: `Path` does not absolutise."""
507
+ monkeypatch.chdir(tmp_path)
508
+ monkeypatch.setenv("FP_HOME", "rel-home")
509
+ cfg.save_config(cfg.CliConfig(session_token="t"))
510
+ assert (tmp_path / "rel-home" / "cli-auth.json").is_file()
511
+
512
+
513
+ def test_a_deeply_nested_home_creates_every_parent(clean_env, monkeypatch, tmp_path):
514
+ monkeypatch.setenv("FAILPROOFAI_HOME", str(tmp_path / "a" / "b" / "c" / "d"))
515
+ cfg.save_config(cfg.CliConfig(session_token="t"))
516
+ assert cfg.config_path().is_file()
517
+
518
+
519
+ # ── Logout, and concurrency ──────────────────────────────────────────────────
520
+
521
+
522
+ def test_logout_clears_the_session_without_touching_the_shared_home(clean_env):
523
+ home = clean_env / ".failproofai"
524
+ home.mkdir()
525
+ (home / "credentials.json").write_text('{"token": "enforcement-cli"}')
526
+ cfg.save_config(cfg.CliConfig(session_token="t", email="a@b", org="acme",
527
+ base_url="https://x", anonymous_id="anon"))
528
+ out = cfg.clear_token(cfg.load_config())
529
+
530
+ assert out.session_token is None and out.email is None and out.org is None
531
+ # kept on purpose, so the next login needs no re-configuring
532
+ assert out.base_url == "https://x" and out.anonymous_id == "anon"
533
+ assert json.loads((home / "credentials.json").read_text())["token"] == "enforcement-cli"
534
+ assert cfg.config_path().is_file()
535
+
536
+
537
+ def test_concurrent_saves_leave_valid_json(clean_env):
538
+ """Two `fp` processes can race. Last write wins; a torn file does not."""
539
+ import threading
540
+
541
+ errors: list = []
542
+
543
+ def writer(n: int) -> None:
544
+ try:
545
+ for _ in range(20):
546
+ cfg.save_config(cfg.CliConfig(session_token=f"tok-{n}"))
547
+ except Exception as exc: # pragma: no cover - surfaced via `errors`
548
+ errors.append(exc)
549
+
550
+ threads = [threading.Thread(target=writer, args=(i,)) for i in range(8)]
551
+ for t in threads:
552
+ t.start()
553
+ for t in threads:
554
+ t.join()
555
+
556
+ assert not errors
557
+ assert cfg.load_config().session_token.startswith("tok-")
558
+
559
+
560
+ # ── Hard links, atomicity, and the temp file ─────────────────────────────────
561
+
562
+
563
+ def test_a_hard_linked_config_does_not_clobber_its_twin(clean_env):
564
+ """`O_NOFOLLOW` says nothing about hard links — they are not links.
565
+
566
+ A hard link is a second NAME for one inode, so an in-place write went
567
+ straight through it into the neighbour's file. `os.replace` swaps the
568
+ directory entry instead, leaving the other name on the old inode.
569
+ """
570
+ home = clean_env / ".failproofai"
571
+ (home / "fpcli").mkdir(parents=True)
572
+ victim = home / "credentials.json"
573
+ victim.write_text('{"token": "enforcement-cli"}')
574
+ os.link(victim, cfg.config_path())
575
+
576
+ cfg.save_config(cfg.CliConfig(session_token="ours"))
577
+
578
+ assert json.loads(victim.read_text())["token"] == "enforcement-cli"
579
+ assert cfg.load_config().session_token == "ours"
580
+
581
+
582
+ def test_no_temp_file_survives_a_successful_save(clean_env):
583
+ cfg.save_config(cfg.CliConfig(session_token="t"))
584
+ leftovers = [p.name for p in cfg.config_path().parent.iterdir() if p.name.endswith(".tmp")]
585
+ assert leftovers == []
586
+
587
+
588
+ def test_no_temp_file_survives_a_failed_save(clean_env, monkeypatch):
589
+ """A disk-full or killed write must not strand a credential beside the real one."""
590
+ cfg.save_config(cfg.CliConfig(session_token="original"))
591
+ real_replace = os.replace
592
+
593
+ def boom(src, dst):
594
+ raise OSError(errno.ENOSPC, "No space left on device")
595
+
596
+ monkeypatch.setattr(os, "replace", boom)
597
+ with pytest.raises(OSError):
598
+ cfg.save_config(cfg.CliConfig(session_token="doomed"))
599
+ monkeypatch.setattr(os, "replace", real_replace)
600
+
601
+ leftovers = [p.name for p in cfg.config_path().parent.iterdir() if p.name.endswith(".tmp")]
602
+ assert leftovers == []
603
+ # and the previous session is still readable — the write never landed
604
+ assert cfg.load_config().session_token == "original"
605
+
606
+
607
+ def test_a_reader_never_sees_a_half_written_file(clean_env):
608
+ """Atomicity, asserted through the only observable that matters.
609
+
610
+ The rename is the whole point: a reader either gets the old session or the
611
+ new one, never a splice. Simulated by failing between write and rename,
612
+ which is exactly the window an in-place write left open.
613
+ """
614
+ cfg.save_config(cfg.CliConfig(session_token="v1", email="a@b"))
615
+ before = cfg.config_path().read_text()
616
+
617
+ import unittest.mock as mock
618
+
619
+ with mock.patch.object(os, "replace", side_effect=OSError(errno.EIO, "io")):
620
+ with pytest.raises(OSError):
621
+ cfg.save_config(cfg.CliConfig(session_token="v2"))
622
+
623
+ assert cfg.config_path().read_text() == before
624
+ assert cfg.load_config().session_token == "v1"
625
+
626
+
627
+ def test_concurrent_processes_leave_one_whole_session(clean_env):
628
+ """Threads share a pid; processes do not. Both must land on a valid file."""
629
+ import multiprocessing as mp
630
+
631
+ target = str(cfg.config_path().parent)
632
+ ctx = mp.get_context("spawn")
633
+ procs = [ctx.Process(target=_child_writer, args=(i, target)) for i in range(4)]
634
+ for p in procs:
635
+ p.start()
636
+ for p in procs:
637
+ p.join(timeout=60)
638
+
639
+ assert all(p.exitcode == 0 for p in procs), [p.exitcode for p in procs]
640
+ assert cfg.load_config().session_token.startswith("proc-")
641
+ leftovers = [p.name for p in cfg.config_path().parent.iterdir() if p.name.endswith(".tmp")]
642
+ assert leftovers == []
643
+
644
+
645
+ # ── Exotic paths ─────────────────────────────────────────────────────────────
646
+
647
+
648
+ def test_a_path_with_spaces_and_unicode(clean_env, monkeypatch, tmp_path):
649
+ monkeypatch.setenv("FAILPROOFAI_HOME", str(tmp_path / "my home ünïcode 目录"))
650
+ cfg.save_config(cfg.CliConfig(session_token="t"))
651
+ assert cfg.load_config().session_token == "t"
652
+
653
+
654
+ def test_a_fifo_in_the_config_position_is_refused(clean_env):
655
+ """A FIFO in the config position must not hang the CLI.
656
+
657
+ Measured, not theorised: with the previous in-place `os.open(..., O_TRUNC)`
658
+ this test does not fail, it HANGS — opening a FIFO for writing blocks until
659
+ a reader appears, so `fp login` waits forever with no output. A mutation run
660
+ that removed the temp-and-rename sat here for ten minutes before being
661
+ killed. Writing to a fresh temp file and renaming over the FIFO cannot
662
+ block, because the open never touches the FIFO at all.
663
+ """
664
+ (clean_env / ".failproofai" / "fpcli").mkdir(parents=True)
665
+ try:
666
+ os.mkfifo(cfg.config_path())
667
+ except (AttributeError, OSError): # pragma: no cover - platform without FIFOs
668
+ pytest.skip("mkfifo unavailable")
669
+ # The rename replaces the directory entry; the FIFO must not be written into.
670
+ cfg.save_config(cfg.CliConfig(session_token="t"))
671
+ assert not stat.S_ISFIFO(os.lstat(cfg.config_path()).st_mode)
672
+ assert cfg.load_config().session_token == "t"
673
+
674
+
675
+ # ── The shipped text must not name the old location ──────────────────────────
676
+
677
+
678
+ def test_no_shipped_module_names_the_pre_move_path():
679
+ """Help text is a docstring, so a stale path compiles, ships and passes.
680
+
681
+ This is the same failure the `fp.events` example was: prose that describes
682
+ the product, wrong, with nothing to catch it. Four modules named
683
+ `~/.fp/cli.json` after the move — `login`, `logout` and `orgs switch` all
684
+ print theirs to the user.
685
+
686
+ `config.py` is exempt: it is where the legacy path is deliberately named, to
687
+ recognise a pre-move install and say so.
688
+ """
689
+ import fp_cli
690
+
691
+ pkg = Path(fp_cli.__file__).parent
692
+ offenders = []
693
+ for path in sorted(pkg.rglob("*.py")):
694
+ if path.name == "config.py":
695
+ continue
696
+ if "~/.fp/cli.json" in path.read_text(encoding="utf-8"):
697
+ offenders.append(str(path.relative_to(pkg)))
698
+ assert offenders == [], f"these still name the pre-move config path: {offenders}"
699
+
700
+
701
+ def test_logout_is_not_undone_by_adoption(clean_env):
702
+ """The sharp edge of adopting: it must not resurrect a session on purpose.
703
+
704
+ `logout` writes a config with no token rather than deleting the file, so
705
+ adoption has to key off the file being ABSENT or unreadable — not off "there
706
+ is no token here". Keying off the token would make every command after a
707
+ logout re-adopt `~/.fp/cli.json` and sign the user back in, which is worse
708
+ than the problem adoption solves.
709
+ """
710
+ _plant_legacy(clean_env)
711
+ assert cfg.load_config().session_token == "legacy" # adopted
712
+ cfg.clear_token(cfg.load_config()) # `fp logout`
713
+ assert cfg.load_config().session_token is None # and it stays out
714
+
715
+
716
+ def test_a_current_config_without_a_token_blocks_adoption(clean_env):
717
+ """The same invariant stated directly, without going through logout."""
718
+ _plant_legacy(clean_env)
719
+ cfg.save_config(cfg.CliConfig(base_url="https://x")) # no token
720
+ loaded = cfg.load_config()
721
+ assert loaded.session_token is None
722
+ assert loaded.base_url == "https://x"