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,692 @@
1
+ """Multi-tenancy: org selection at login, --org header injection, orgs/org commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+
7
+ import httpx
8
+ import respx
9
+
10
+ from fp_cli import config
11
+ from fp_cli.app import app
12
+
13
+ BASE = "http://dash.test"
14
+
15
+
16
+ def _session(memberships, *, is_admin=False, id="u1", email="me@test"):
17
+ return {"id": id, "email": email, "is_instance_admin": is_admin, "memberships": memberships}
18
+
19
+
20
+ _ACME = {"org_id": "o1", "org_slug": "acme", "org_name": "Acme", "permissions": ["events:read"], "permission_set": "standard"}
21
+ _GLOBEX = {"org_id": "o2", "org_slug": "globex", "org_name": "Globex", "permissions": ["events:read", "keys:create"], "permission_set": "admin"}
22
+
23
+
24
+ def _otp_routes(user):
25
+ respx.post(f"{BASE}/api/auth/otp/request").mock(return_value=httpx.Response(200, json={"ok": True}))
26
+ # The OTP-verify payload is intentionally slim (no memberships); login reads the
27
+ # authoritative memberships from GET /api/auth/session, so mock that too.
28
+ respx.post(f"{BASE}/api/auth/otp/verify").mock(
29
+ return_value=httpx.Response(
30
+ 200,
31
+ json={"user": {"id": user["id"], "email": user["email"]}, "expires_in_secs": 3600},
32
+ headers={"set-cookie": "ae_session=tok; Path=/"},
33
+ )
34
+ )
35
+ respx.get(f"{BASE}/api/auth/session").mock(return_value=httpx.Response(200, json=user))
36
+
37
+
38
+ # --- login org selection ----------------------------------------------------
39
+
40
+
41
+ @respx.mock
42
+ def test_login_single_org_auto_selects(home, runner):
43
+ _otp_routes(_session([_ACME]))
44
+ result = runner.invoke(app, ["--base-url", BASE, "login", "--email", "me@test"], input="123456\n")
45
+ assert result.exit_code == 0, result.output
46
+ assert config.load_config().org == "acme"
47
+
48
+
49
+ @respx.mock
50
+ def test_login_org_flag_persists(home, runner):
51
+ _otp_routes(_session([_ACME, _GLOBEX]))
52
+ result = runner.invoke(
53
+ app, ["--base-url", BASE, "login", "--email", "me@test", "--org", "globex"], input="123456\n"
54
+ )
55
+ assert result.exit_code == 0, result.output
56
+ assert config.load_config().org == "globex"
57
+
58
+
59
+ @respx.mock
60
+ def test_login_org_not_a_member_is_usage_error(home, runner):
61
+ _otp_routes(_session([_ACME]))
62
+ result = runner.invoke(
63
+ app, ["--base-url", BASE, "login", "--email", "me@test", "--org", "globex"], input="123456\n"
64
+ )
65
+ assert result.exit_code == 2 # BadParameter: not a member of globex
66
+
67
+
68
+ @respx.mock
69
+ def test_login_multi_org_no_selection_exits_nonzero_json(home, runner):
70
+ # Non-interactive (CliRunner) multi-org login without --org: must not block.
71
+ _otp_routes(_session([_ACME, _GLOBEX]))
72
+ result = runner.invoke(app, ["--base-url", BASE, "--json", "login", "--email", "me@test"], input="123456\n")
73
+ assert result.exit_code == 2, result.output
74
+ # CliRunner echoes the typed OTP back onto stdout (its fake `input()` does what a terminal
75
+ # normally does), so stdout here is "<echo>\n<envelope>". That prefix is a harness artifact,
76
+ # not part of the --json contract — a real run echoes nothing — so parse from the envelope's
77
+ # opening brace and keep asserting that the envelope itself lands on stdout.
78
+ payload = json.loads(result.stdout[result.stdout.index("{") :])
79
+ assert payload["needs_org_selection"] is True
80
+ assert set(payload["orgs"]) == {"acme", "globex"}
81
+ # token saved so the user can `orgs switch` without re-doing OTP
82
+ assert config.load_config().session_token == "tok"
83
+
84
+
85
+ # --- interactive org picker (multi-tenant) ----------------------------------
86
+ #
87
+ # The CliRunner's stdin is not a TTY, so the picker is gated behind
88
+ # `_stdin_is_tty()` which we stub True. The OTP code is the first stdin line;
89
+ # the picker choice (slug / number / Enter-for-default) is the next.
90
+
91
+
92
+ def _tty(monkeypatch):
93
+ monkeypatch.setattr("fp_cli.select.stdin_is_tty", lambda: True)
94
+
95
+
96
+ @respx.mock
97
+ def test_login_multi_org_picker_by_slug(home, runner, monkeypatch):
98
+ _otp_routes(_session([_ACME, _GLOBEX]))
99
+ _tty(monkeypatch)
100
+ result = runner.invoke(
101
+ app, ["--base-url", BASE, "login", "--email", "me@test"], input="123456\nglobex\n"
102
+ )
103
+ assert result.exit_code == 0, result.output
104
+ assert config.load_config().org == "globex"
105
+
106
+
107
+ @respx.mock
108
+ def test_login_multi_org_picker_by_number(home, runner, monkeypatch):
109
+ # memberships order is acme, globex → "2" selects globex
110
+ _otp_routes(_session([_ACME, _GLOBEX]))
111
+ _tty(monkeypatch)
112
+ result = runner.invoke(
113
+ app, ["--base-url", BASE, "login", "--email", "me@test"], input="123456\n2\n"
114
+ )
115
+ assert result.exit_code == 0, result.output
116
+ assert config.load_config().org == "globex"
117
+
118
+
119
+ @respx.mock
120
+ def test_login_multi_org_picker_rejects_invalid_then_accepts(home, runner, monkeypatch):
121
+ # A non-member / garbage choice must NOT be accepted — it re-prompts (no leak).
122
+ _otp_routes(_session([_ACME, _GLOBEX]))
123
+ _tty(monkeypatch)
124
+ result = runner.invoke(
125
+ app, ["--base-url", BASE, "login", "--email", "me@test"], input="123456\nnope\n9\nacme\n"
126
+ )
127
+ assert result.exit_code == 0, result.output
128
+ assert config.load_config().org == "acme"
129
+ assert "not one of your orgs" in (result.stderr or "")
130
+
131
+
132
+ @respx.mock
133
+ def test_login_saved_org_does_not_bypass_picker(home, runner, monkeypatch):
134
+ # THE FIX: a previously-saved tenant must not silently re-activate — the picker
135
+ # still runs and a different org can be chosen.
136
+ config.save_config(config.CliConfig(base_url=BASE, org="acme"))
137
+ _otp_routes(_session([_ACME, _GLOBEX]))
138
+ _tty(monkeypatch)
139
+ result = runner.invoke(
140
+ app, ["--base-url", BASE, "login", "--email", "me@test"], input="123456\nglobex\n"
141
+ )
142
+ assert result.exit_code == 0, result.output
143
+ assert config.load_config().org == "globex" # chose globex despite saved acme
144
+
145
+
146
+ @respx.mock
147
+ def test_login_saved_org_is_picker_default_on_enter(home, runner, monkeypatch):
148
+ # The saved tenant is offered as the Enter-to-keep default.
149
+ config.save_config(config.CliConfig(base_url=BASE, org="acme"))
150
+ _otp_routes(_session([_ACME, _GLOBEX]))
151
+ _tty(monkeypatch)
152
+ result = runner.invoke(
153
+ app, ["--base-url", BASE, "login", "--email", "me@test"], input="123456\n\n"
154
+ )
155
+ assert result.exit_code == 0, result.output
156
+ assert config.load_config().org == "acme"
157
+
158
+
159
+ @respx.mock
160
+ def test_login_stale_saved_org_not_offered_as_default(home, runner, monkeypatch):
161
+ # A saved org the user is no longer a member of must not be the default; a bare
162
+ # Enter then has no default → re-prompt (we follow with a valid pick).
163
+ config.save_config(config.CliConfig(base_url=BASE, org="zombie"))
164
+ _otp_routes(_session([_ACME, _GLOBEX]))
165
+ _tty(monkeypatch)
166
+ result = runner.invoke(
167
+ app, ["--base-url", BASE, "login", "--email", "me@test"], input="123456\nacme\n"
168
+ )
169
+ assert result.exit_code == 0, result.output
170
+ assert config.load_config().org == "acme"
171
+
172
+
173
+ @respx.mock
174
+ def test_login_global_org_flag_skips_picker(home, runner, monkeypatch):
175
+ # An explicit global --org (before the command) is a deliberate choice → no picker.
176
+ _otp_routes(_session([_ACME, _GLOBEX]))
177
+ _tty(monkeypatch)
178
+ result = runner.invoke(
179
+ app, ["--base-url", BASE, "--org", "globex", "login", "--email", "me@test"], input="123456\n"
180
+ )
181
+ assert result.exit_code == 0, result.output
182
+ assert config.load_config().org == "globex"
183
+
184
+
185
+ @respx.mock
186
+ def test_login_env_org_skips_picker(home, runner, monkeypatch):
187
+ # FP_ORG is an explicit choice → no picker.
188
+ monkeypatch.setenv("FP_ORG", "globex")
189
+ _otp_routes(_session([_ACME, _GLOBEX]))
190
+ _tty(monkeypatch)
191
+ result = runner.invoke(
192
+ app, ["--base-url", BASE, "login", "--email", "me@test"], input="123456\n"
193
+ )
194
+ assert result.exit_code == 0, result.output
195
+ assert config.load_config().org == "globex"
196
+
197
+
198
+ @respx.mock
199
+ def test_login_admin_can_select_existing_nonmember_org_via_flag(home, runner):
200
+ # An instance admin may activate a non-member org via --org ONLY if it exists
201
+ # and is accessible — the server probe (/api/access-granters) returns 200.
202
+ _otp_routes(_session([_ACME], is_admin=True))
203
+ respx.get(f"{BASE}/api/access-granters").mock(
204
+ return_value=httpx.Response(200, json=["dev"])
205
+ )
206
+ result = runner.invoke(
207
+ app, ["--base-url", BASE, "login", "--email", "me@test", "--org", "globex"], input="123456\n"
208
+ )
209
+ assert result.exit_code == 0, result.output
210
+ assert config.load_config().org == "globex"
211
+
212
+
213
+ @respx.mock
214
+ def test_login_admin_nonexistent_org_rejected(home, runner):
215
+ # THE BUG FIX: an instance admin requesting a NON-EXISTENT org must be rejected,
216
+ # not silently accepted+saved. The probe returns 403 (no such org / no access).
217
+ _otp_routes(_session([_ACME], is_admin=True))
218
+ respx.get(f"{BASE}/api/access-granters").mock(
219
+ return_value=httpx.Response(403, json={})
220
+ )
221
+ result = runner.invoke(
222
+ app, ["--base-url", BASE, "login", "--email", "me@test", "--org", "fp"], input="123456\n"
223
+ )
224
+ assert result.exit_code == 2, result.output
225
+ assert config.load_config().org is None # nothing bogus persisted
226
+
227
+
228
+ @respx.mock
229
+ def test_login_env_nonexistent_org_rejected(home, runner, monkeypatch):
230
+ # FP_ORG follows the SAME validation as --org (both feed `requested`).
231
+ monkeypatch.setenv("FP_ORG", "fp")
232
+ _otp_routes(_session([_ACME], is_admin=True))
233
+ respx.get(f"{BASE}/api/access-granters").mock(
234
+ return_value=httpx.Response(403, json={})
235
+ )
236
+ result = runner.invoke(app, ["--base-url", BASE, "login", "--email", "me@test"], input="123456\n")
237
+ assert result.exit_code == 2, result.output
238
+ assert config.load_config().org is None
239
+
240
+
241
+ @respx.mock
242
+ def test_login_nonadmin_nonmember_rejected_without_probe(home, runner):
243
+ # A regular user's non-member org is rejected by the membership check alone —
244
+ # no server probe is made (the membership list is conclusive for non-admins).
245
+ _otp_routes(_session([_ACME], is_admin=False))
246
+ probe = respx.get(f"{BASE}/api/access-granters").mock(
247
+ return_value=httpx.Response(200, json=["dev"])
248
+ )
249
+ result = runner.invoke(
250
+ app, ["--base-url", BASE, "login", "--email", "me@test", "--org", "globex"], input="123456\n"
251
+ )
252
+ assert result.exit_code == 2, result.output
253
+ assert not probe.called
254
+
255
+
256
+ @respx.mock
257
+ def test_login_multi_org_noninteractive_reuses_saved(home, runner):
258
+ # Non-interactive (CliRunner stdin is not a TTY), no --json: a still-valid saved
259
+ # tenant is reused without prompting (back-compat for piped re-login).
260
+ config.save_config(config.CliConfig(base_url=BASE, org="acme"))
261
+ _otp_routes(_session([_ACME, _GLOBEX]))
262
+ result = runner.invoke(
263
+ app, ["--base-url", BASE, "login", "--email", "me@test"], input="123456\n"
264
+ )
265
+ assert result.exit_code == 0, result.output
266
+ assert config.load_config().org == "acme"
267
+
268
+
269
+ @respx.mock
270
+ def test_login_multi_org_noninteractive_stale_saved_needs_selection(home, runner):
271
+ # Non-interactive with a saved org the user is NO LONGER a member of → must not
272
+ # silently reuse it; falls through to needs-selection (exit 2).
273
+ config.save_config(config.CliConfig(base_url=BASE, org="zombie"))
274
+ _otp_routes(_session([_ACME, _GLOBEX]))
275
+ result = runner.invoke(
276
+ app, ["--base-url", BASE, "login", "--email", "me@test"], input="123456\n"
277
+ )
278
+ assert result.exit_code == 2, result.output
279
+ assert config.load_config().org is None
280
+
281
+
282
+ @respx.mock
283
+ def test_login_noninteractive_multi_org_no_saved_needs_selection_plain(home, runner):
284
+ # Non-interactive, NON-json, multi-org, nothing saved → needs selection, token kept.
285
+ _otp_routes(_session([_ACME, _GLOBEX]))
286
+ result = runner.invoke(
287
+ app, ["--base-url", BASE, "login", "--email", "me@test"], input="123456\n"
288
+ )
289
+ assert result.exit_code == 2, result.output
290
+ assert config.load_config().session_token == "tok"
291
+ assert config.load_config().org is None
292
+
293
+
294
+ # --- X-AgentEye-Org header injection ----------------------------------------
295
+
296
+
297
+ @respx.mock
298
+ def test_org_flag_sets_header(logged_in, runner):
299
+ route = respx.get(f"{BASE}/api/sessions").mock(
300
+ return_value=httpx.Response(200, json={"sessions": [], "next_cursor": None})
301
+ )
302
+ result = runner.invoke(app, ["--org", "acme", "sessions"])
303
+ assert result.exit_code == 0, result.output
304
+ assert route.calls.last.request.headers.get("X-AgentEye-Org") == "acme"
305
+
306
+
307
+ @respx.mock
308
+ def test_saved_org_sets_header(home, runner):
309
+ config.save_config(
310
+ config.CliConfig(base_url=BASE, session_token="tok", expires_at="2999-01-01T00:00:00Z", org="globex")
311
+ )
312
+ route = respx.get(f"{BASE}/api/sessions").mock(
313
+ return_value=httpx.Response(200, json={"sessions": [], "next_cursor": None})
314
+ )
315
+ result = runner.invoke(app, ["sessions"])
316
+ assert result.exit_code == 0, result.output
317
+ assert route.calls.last.request.headers.get("X-AgentEye-Org") == "globex"
318
+
319
+
320
+ def test_bad_org_slug_is_usage_error(logged_in, runner):
321
+ result = runner.invoke(app, ["--org", "Bad_Slug!", "sessions"])
322
+ assert result.exit_code == 2
323
+
324
+
325
+ # --- whoami multi-tenant shape ----------------------------------------------
326
+
327
+
328
+ @respx.mock
329
+ def test_whoami_shows_memberships_and_active_org(logged_in, runner):
330
+ respx.get(f"{BASE}/api/auth/session").mock(
331
+ return_value=httpx.Response(200, json=_session([_ACME, _GLOBEX], is_admin=True))
332
+ )
333
+ result = runner.invoke(app, ["--org", "globex", "--json", "whoami"])
334
+ assert result.exit_code == 0, result.output
335
+ data = json.loads(result.stdout)
336
+ assert data["active_org"] == "globex"
337
+ assert data["is_instance_admin"] is True
338
+ assert data["permissions"] == ["events:read", "keys:create"] # globex's grants
339
+ assert {m["org_slug"] for m in data["memberships"]} == {"acme", "globex"}
340
+
341
+
342
+ # --- orgs list / orgs current -----------------------------------------------
343
+ # (org selection/persistence/validation is covered by the orgs switch tests below;
344
+ # `orgs use` was removed in favour of `orgs switch`.)
345
+
346
+
347
+ @respx.mock
348
+ def test_orgs_list_json(logged_in, runner):
349
+ respx.get(f"{BASE}/api/auth/session").mock(
350
+ return_value=httpx.Response(200, json=_session([_ACME, _GLOBEX]))
351
+ )
352
+ result = runner.invoke(app, ["--org", "acme", "--json", "orgs", "list"])
353
+ assert result.exit_code == 0, result.output
354
+ data = json.loads(result.stdout)
355
+ assert data["active_org"] == "acme"
356
+ assert {o["org_slug"] for o in data["orgs"]} == {"acme", "globex"}
357
+
358
+
359
+ @respx.mock
360
+ def test_orgs_current_json(logged_in, runner):
361
+ respx.get(f"{BASE}/api/auth/session").mock(
362
+ return_value=httpx.Response(200, json=_session([_ACME, _GLOBEX]))
363
+ )
364
+ result = runner.invoke(app, ["--org", "globex", "--json", "orgs", "current"])
365
+ assert result.exit_code == 0, result.output
366
+ data = json.loads(result.stdout)
367
+ assert data["slug"] == "globex"
368
+ assert data["name"] == "Globex" and data["role"] == "admin"
369
+ assert data["permission_count"] == 2 # globex's two grants
370
+ assert data["user_email"] # reports who you're signed in as
371
+
372
+
373
+ @respx.mock
374
+ def test_orgs_perms_json(logged_in, runner):
375
+ respx.get(f"{BASE}/api/auth/session").mock(
376
+ return_value=httpx.Response(200, json=_session([_ACME, _GLOBEX]))
377
+ )
378
+ result = runner.invoke(app, ["--org", "globex", "--json", "orgs", "perms"])
379
+ assert result.exit_code == 0, result.output
380
+ data = json.loads(result.stdout)
381
+ assert data["slug"] == "globex" and data["role"] == "admin"
382
+ assert data["permissions"] == ["events:read", "keys:create"] # globex's flat grants
383
+ assert data["permission_count"] == 2
384
+
385
+
386
+ @respx.mock
387
+ def test_orgs_perms_table(logged_in, runner):
388
+ respx.get(f"{BASE}/api/auth/session").mock(
389
+ return_value=httpx.Response(200, json=_session([_ACME, _GLOBEX]))
390
+ )
391
+ result = runner.invoke(app, ["--org", "globex", "orgs", "perms"])
392
+ assert result.exit_code == 0, result.output
393
+ # identity header (name · slug · role) + the shared grouped permissions panel, with the
394
+ # org NAME in the panel's border title.
395
+ assert "Globex" in result.stdout and "globex" in result.stdout and "role admin" in result.stdout
396
+ assert "permissions · 2 · Globex" in result.stdout
397
+ assert "events" in result.stdout and "keys" in result.stdout
398
+
399
+
400
+ @respx.mock
401
+ def test_org_use_admin_nonexistent_org_rejected(logged_in, runner):
402
+ # Instance admin → a NON-EXISTENT org (probe 403) is rejected, not persisted.
403
+ respx.get(f"{BASE}/api/auth/session").mock(
404
+ return_value=httpx.Response(200, json=_session([_ACME], is_admin=True))
405
+ )
406
+ respx.get(f"{BASE}/api/access-granters").mock(
407
+ return_value=httpx.Response(403, json={})
408
+ )
409
+ result = runner.invoke(app, ["orgs", "use", "fp"])
410
+ assert result.exit_code == 2
411
+ assert config.load_config().org is None
412
+
413
+
414
+ @respx.mock
415
+ def test_logout_clears_active_org_and_identity(logged_in, runner):
416
+ # Logout must not leave a remembered org/identity in cli.json — only base_url
417
+ # (and prefs) survive, so the next login starts fresh.
418
+ cfg = config.load_config()
419
+ cfg.org = "acme"
420
+ config.save_config(cfg)
421
+ respx.post(f"{BASE}/api/auth/logout").mock(return_value=httpx.Response(200, json={}))
422
+ result = runner.invoke(app, ["logout"])
423
+ assert result.exit_code == 0, result.output
424
+ after = config.load_config()
425
+ assert after.session_token is None
426
+ assert after.org is None
427
+ assert after.email is None
428
+ assert after.user_id is None
429
+ assert after.base_url == BASE # kept so the next login needs no --base-url
430
+
431
+
432
+ def test_logout_when_not_signed_in_is_noop(home, runner):
433
+ # No session → logout must NOT claim a sign-out happened (no server call either).
434
+ result = runner.invoke(app, ["--base-url", BASE, "logout"])
435
+ assert result.exit_code == 0, result.output
436
+ assert "already signed out" in (result.stderr or "").lower()
437
+
438
+
439
+ def test_logout_when_not_signed_in_json(home, runner):
440
+ result = runner.invoke(app, ["--base-url", BASE, "--json", "logout"])
441
+ assert result.exit_code == 0, result.output
442
+ payload = json.loads(result.stdout)
443
+ assert payload["already_signed_out"] is True
444
+
445
+
446
+ # --- org list (and the orgs-list alias) -------------------------------------
447
+
448
+
449
+ @respx.mock
450
+ def test_org_list_json_shows_role_and_active(logged_in, runner):
451
+ respx.get(f"{BASE}/api/auth/session").mock(
452
+ return_value=httpx.Response(200, json=_session([_ACME, _GLOBEX], is_admin=True))
453
+ )
454
+ result = runner.invoke(app, ["--org", "globex", "--json", "orgs", "list"])
455
+ assert result.exit_code == 0, result.output
456
+ data = json.loads(result.stdout)
457
+ assert data["active_org"] == "globex"
458
+ assert data["is_instance_admin"] is True
459
+ byslug = {o["org_slug"]: o for o in data["orgs"]}
460
+ assert byslug["globex"]["active"] is True and byslug["acme"]["active"] is False
461
+ assert byslug["acme"]["permission_set"] == "standard" # your role in that org
462
+
463
+
464
+ @respx.mock
465
+ def test_org_list_table_lists_all_orgs(logged_in, runner):
466
+ respx.get(f"{BASE}/api/auth/session").mock(
467
+ return_value=httpx.Response(200, json=_session([_ACME, _GLOBEX]))
468
+ )
469
+ result = runner.invoke(app, ["--org", "acme", "orgs", "list"])
470
+ assert result.exit_code == 0, result.output
471
+ assert "acme" in result.stdout and "globex" in result.stdout
472
+
473
+
474
+ @respx.mock
475
+ def test_orgs_list_alias_still_works(logged_in, runner):
476
+ # `orgs list` lists your orgs (merged single `orgs` group).
477
+ respx.get(f"{BASE}/api/auth/session").mock(
478
+ return_value=httpx.Response(200, json=_session([_ACME]))
479
+ )
480
+ result = runner.invoke(app, ["--org", "acme", "--json", "orgs", "list"])
481
+ assert result.exit_code == 0, result.output
482
+ assert json.loads(result.stdout)["active_org"] == "acme"
483
+
484
+
485
+ # --- org switch -------------------------------------------------------------
486
+
487
+
488
+ @respx.mock
489
+ def test_org_switch_to_member(logged_in, runner):
490
+ respx.get(f"{BASE}/api/auth/session").mock(
491
+ return_value=httpx.Response(200, json=_session([_ACME, _GLOBEX]))
492
+ )
493
+ result = runner.invoke(app, ["orgs", "switch", "globex"])
494
+ assert result.exit_code == 0, result.output
495
+ assert config.load_config().org == "globex"
496
+
497
+
498
+ @respx.mock
499
+ def test_org_switch_non_member_rejected_without_probe(logged_in, runner):
500
+ respx.get(f"{BASE}/api/auth/session").mock(
501
+ return_value=httpx.Response(200, json=_session([_ACME]))
502
+ )
503
+ probe = respx.get(f"{BASE}/api/access-granters").mock(
504
+ return_value=httpx.Response(200, json=["dev"])
505
+ )
506
+ result = runner.invoke(app, ["orgs", "switch", "globex"])
507
+ assert result.exit_code == 2
508
+ assert not probe.called # non-admin → membership check is conclusive
509
+ assert config.load_config().org is None
510
+
511
+
512
+ @respx.mock
513
+ def test_org_switch_admin_existing_nonmember_allowed(logged_in, runner):
514
+ respx.get(f"{BASE}/api/auth/session").mock(
515
+ return_value=httpx.Response(200, json=_session([_ACME], is_admin=True))
516
+ )
517
+ respx.get(f"{BASE}/api/access-granters").mock(
518
+ return_value=httpx.Response(200, json=["dev"])
519
+ )
520
+ result = runner.invoke(app, ["orgs", "switch", "globex"])
521
+ assert result.exit_code == 0, result.output
522
+ assert config.load_config().org == "globex"
523
+
524
+
525
+ @respx.mock
526
+ def test_org_switch_admin_nonexistent_rejected(logged_in, runner):
527
+ respx.get(f"{BASE}/api/auth/session").mock(
528
+ return_value=httpx.Response(200, json=_session([_ACME], is_admin=True))
529
+ )
530
+ respx.get(f"{BASE}/api/access-granters").mock(
531
+ return_value=httpx.Response(403, json={})
532
+ )
533
+ result = runner.invoke(app, ["orgs", "switch", "fp"])
534
+ assert result.exit_code == 2
535
+ assert config.load_config().org is None
536
+
537
+
538
+ @respx.mock
539
+ def test_org_switch_no_arg_single_org_auto_selects(logged_in, runner):
540
+ respx.get(f"{BASE}/api/auth/session").mock(
541
+ return_value=httpx.Response(200, json=_session([_ACME]))
542
+ )
543
+ result = runner.invoke(app, ["orgs", "switch"])
544
+ assert result.exit_code == 0, result.output
545
+ assert config.load_config().org == "acme"
546
+
547
+
548
+ @respx.mock
549
+ def test_org_switch_no_arg_interactive_picker(logged_in, runner, monkeypatch):
550
+ respx.get(f"{BASE}/api/auth/session").mock(
551
+ return_value=httpx.Response(200, json=_session([_ACME, _GLOBEX]))
552
+ )
553
+ _tty(monkeypatch)
554
+ result = runner.invoke(app, ["orgs", "switch"], input="globex\n")
555
+ assert result.exit_code == 0, result.output
556
+ assert config.load_config().org == "globex"
557
+
558
+
559
+ @respx.mock
560
+ def test_org_switch_no_arg_noninteractive_requires_slug(logged_in, runner):
561
+ # CliRunner stdin is not a TTY and no slug given → must not hang; error out.
562
+ respx.get(f"{BASE}/api/auth/session").mock(
563
+ return_value=httpx.Response(200, json=_session([_ACME, _GLOBEX]))
564
+ )
565
+ result = runner.invoke(app, ["orgs", "switch"])
566
+ assert result.exit_code == 2
567
+ assert config.load_config().org is None
568
+
569
+
570
+ @respx.mock
571
+ def test_org_switch_to_different_renders_switched_card(logged_in, runner):
572
+ # Positional switch to a DIFFERENT org → the boxed 'switched org' card (was <prev>).
573
+ respx.get(f"{BASE}/api/auth/session").mock(
574
+ return_value=httpx.Response(200, json=_session([_ACME, _GLOBEX]))
575
+ )
576
+ result = runner.invoke(app, ["--org", "acme", "orgs", "switch", "globex"])
577
+ assert result.exit_code == 0, result.output
578
+ assert config.load_config().org == "globex"
579
+ assert "switched org" in result.stderr and "globex" in result.stderr and "was acme" in result.stderr
580
+
581
+
582
+ @respx.mock
583
+ def test_org_switch_to_current_is_noop(logged_in, runner):
584
+ # Positional switch to the org you're already on → calm 'already on' no-op, not the card.
585
+ respx.get(f"{BASE}/api/auth/session").mock(
586
+ return_value=httpx.Response(200, json=_session([_ACME, _GLOBEX]))
587
+ )
588
+ result = runner.invoke(app, ["--org", "acme", "orgs", "switch", "acme"])
589
+ assert result.exit_code == 0, result.output
590
+ assert "already on acme" in result.stderr
591
+ assert "switched org" not in result.stderr
592
+
593
+
594
+ @respx.mock
595
+ def test_org_switch_not_found_did_you_mean(logged_in, runner):
596
+ # A near-miss slug → styled not-found + did-you-mean + hint, non-zero exit.
597
+ respx.get(f"{BASE}/api/auth/session").mock(
598
+ return_value=httpx.Response(200, json=_session([_ACME, _GLOBEX]))
599
+ )
600
+ result = runner.invoke(app, ["orgs", "switch", "acm"])
601
+ assert result.exit_code == 2
602
+ assert "no org named acm" in result.stderr
603
+ assert "did you mean acme" in result.stderr
604
+ assert config.load_config().org is None
605
+
606
+
607
+ @respx.mock
608
+ def test_org_switch_single_org_says_only_org(logged_in, runner):
609
+ # One org → no picker; calm 'only org' line; still persisted as active.
610
+ respx.get(f"{BASE}/api/auth/session").mock(
611
+ return_value=httpx.Response(200, json=_session([_ACME]))
612
+ )
613
+ result = runner.invoke(app, ["orgs", "switch"])
614
+ assert result.exit_code == 0, result.output
615
+ assert "only org" in result.stderr and "acme" in result.stderr
616
+ assert config.load_config().org == "acme"
617
+
618
+
619
+ @respx.mock
620
+ def test_login_already_signed_in_switches_org_via_flag(home, runner):
621
+ # A valid session is active on org acme; `login --org globex` must honor the explicit
622
+ # selector and switch the active tenant (no re-auth), not drop it and report "already
623
+ # signed in" on the old org (the bug).
624
+ config.save_config(config.CliConfig(
625
+ base_url=BASE, session_token="tok", expires_at="2999-01-01T00:00:00Z",
626
+ email="me@test", user_id="u1", org="acme",
627
+ ))
628
+ respx.get(f"{BASE}/api/auth/session").mock(
629
+ return_value=httpx.Response(200, json=_session([_ACME, _GLOBEX]))
630
+ )
631
+ result = runner.invoke(app, ["--json", "login", "--org", "globex"])
632
+ assert result.exit_code == 0, result.output
633
+ data = json.loads(result.stdout)
634
+ assert data["org"] == "globex" and data.get("switched_org") is True
635
+ assert config.load_config().org == "globex"
636
+
637
+
638
+ # --- membership discovery that FAILS is not "no memberships" ----------------
639
+
640
+
641
+ @respx.mock
642
+ def test_a_membership_fetch_failure_is_reported_not_silently_emptied(home, runner):
643
+ """`except Exception: pass` turned every discovery failure into an empty list.
644
+
645
+ A timeout, a 500, a malformed body or a permissions problem all collapsed
646
+ into "this user belongs to no orgs" — which then clears a previously chosen
647
+ tenant and reads, to anyone looking, like a successful single-org login.
648
+ """
649
+ respx.post(f"{BASE}/api/auth/otp/request").mock(return_value=httpx.Response(200, json={"ok": True}))
650
+ respx.post(f"{BASE}/api/auth/otp/verify").mock(
651
+ return_value=httpx.Response(
652
+ 200,
653
+ json={"user": {"id": "u1", "email": "me@test"}, "expires_in_secs": 3600},
654
+ headers={"set-cookie": "ae_session=tok; Path=/"},
655
+ )
656
+ )
657
+ respx.get(f"{BASE}/api/auth/session").mock(return_value=httpx.Response(503, json={"error": "upstream down"}))
658
+
659
+ result = runner.invoke(app, ["--base-url", BASE, "login", "--email", "me@test"], input="123456\n")
660
+
661
+ # The token IS kept — losing a good session to a discovery blip would be worse.
662
+ assert config.load_config().session_token == "tok"
663
+ # ...but the failure is said out loud rather than presented as "no orgs".
664
+ assert "could not read your organisation memberships" in result.stderr, result.stderr
665
+
666
+
667
+ @respx.mock
668
+ def test_an_explicit_org_is_not_rejected_on_a_check_that_never_ran(home, runner):
669
+ """"You are not a member of org 'X'. Your orgs: (none)." was a confident lie.
670
+
671
+ With discovery failed, `slugs` is empty because nothing answered — not
672
+ because the user belongs to nothing. Exiting 2 on a tenant they may well
673
+ have, citing a membership list that was never fetched, is the worst of the
674
+ available answers.
675
+ """
676
+ respx.post(f"{BASE}/api/auth/otp/request").mock(return_value=httpx.Response(200, json={"ok": True}))
677
+ respx.post(f"{BASE}/api/auth/otp/verify").mock(
678
+ return_value=httpx.Response(
679
+ 200,
680
+ json={"user": {"id": "u1", "email": "me@test"}, "expires_in_secs": 3600},
681
+ headers={"set-cookie": "ae_session=tok; Path=/"},
682
+ )
683
+ )
684
+ respx.get(f"{BASE}/api/auth/session").mock(return_value=httpx.Response(500, json={"error": "boom"}))
685
+ # The direct check is the only thing that can still answer, so it is consulted.
686
+ respx.get(f"{BASE}/api/access-granters").mock(return_value=httpx.Response(200, json=[]))
687
+
688
+ result = runner.invoke(
689
+ app, ["--base-url", BASE, "login", "--email", "me@test", "--org", "globex"], input="123456\n"
690
+ )
691
+ assert result.exit_code == 0, result.output + result.stderr
692
+ assert config.load_config().org == "globex"