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,1054 @@
1
+ """Audits: audits list/show/create/edit/delete/run/runs + findings/finding + triage.
2
+
3
+ An **audit** is a scheduled sweep over a window of agent activity (`audits create`), and what
4
+ it produces are **findings** — recurring patterns carried across runs by a fingerprint. So the
5
+ group has two handles: an audit is referenced by its **name** (unique per org, like `alerts`),
6
+ while a finding is referenced by its **id** (findings have no human name). The triage verbs
7
+ (`ack`/`mute`/`dismiss`/`resolve`/`reopen`/`assign`) act on a finding id and all post the same
8
+ status endpoint.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import re
15
+ from datetime import datetime, timezone
16
+ from typing import Any, Dict, List, Optional
17
+
18
+ import typer
19
+
20
+ from .. import _click_compat as click # the Click Typer is running; see _click_compat
21
+ from .. import client as api
22
+ from .. import output
23
+ from .._context import GLOBALS_EPILOG, AppState, collect_multi, require_auth, validate_limit
24
+ from ..errors import ApiError, ForbiddenError, NotFoundError
25
+ from ..models import Audit
26
+ from . import _write
27
+
28
+ _WINDOW_MODES = ("fixed", "since_last")
29
+ _SENSITIVITIES = ("low", "medium", "high")
30
+ _FINDING_STATUSES = ("open", "recurring", "resolved", "dismissed", "muted")
31
+ _TRIAGE_ACTIONS = ("ack", "mute", "dismiss", "resolve", "reopen", "assign")
32
+ # Triage actions that suppress or close a finding — those confirm first (`--yes` skips).
33
+ # ack / reopen / assign are calm, reversible bookkeeping, so they act immediately.
34
+ _CONFIRMING_ACTIONS = ("mute", "dismiss", "resolve")
35
+ # The server's accepted ranges (mirrored client-side so a bad value is a clean exit 2
36
+ # instead of an HTTP 422 round-trip).
37
+ _INTERVAL_MIN, _INTERVAL_MAX = 3_600, 604_800
38
+ _LOOKBACK_MIN, _LOOKBACK_MAX = 3_600, 7_776_000
39
+ _UUID_RE = re.compile(r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")
40
+
41
+
42
+ def _validate_statuses(value: Optional[str]) -> None:
43
+ """Reject an unknown ``--status`` value up front (exit 2) rather than letting the server answer
44
+ with a 422 (or, worse, silently drop it). Accepts a CSV of the five finding statuses."""
45
+ if value is None:
46
+ return
47
+ for s in value.split(","):
48
+ s = s.strip()
49
+ if s and s not in _FINDING_STATUSES:
50
+ raise typer.BadParameter(
51
+ f"'{s}' is not a valid status. Choose from: {', '.join(_FINDING_STATUSES)} (CSV).",
52
+ param_hint="--status",
53
+ )
54
+
55
+
56
+ def _validate_action(action: str) -> str:
57
+ """Guard the triage action the subcommands hand to the status endpoint. Each triage command
58
+ passes its own fixed verb, so this only trips on a coding error — but it keeps an unknown
59
+ action a clean usage error (exit 2) instead of an HTTP 422."""
60
+ if action not in _TRIAGE_ACTIONS:
61
+ raise typer.BadParameter(
62
+ f"'{action}' is not a valid triage action. Choose from: {', '.join(_TRIAGE_ACTIONS)}.",
63
+ param_hint="--action",
64
+ )
65
+ return action
66
+
67
+
68
+ def _parse_anchor(value: str) -> Optional[str]:
69
+ """Normalize a ``--schedule-anchor`` to an RFC3339 string the server's
70
+ ``DateTime<Utc>`` deserializer accepts, or ``None`` if it isn't a timestamp.
71
+
72
+ Accepts a trailing ``Z`` (which ``datetime.fromisoformat`` rejects before 3.11)
73
+ and a naive timestamp, which is read as UTC — audits are UTC end to end and
74
+ there is no timezone anywhere in this system."""
75
+ raw = value.strip()
76
+ if not raw:
77
+ return None
78
+ try:
79
+ parsed = datetime.fromisoformat(raw.replace("Z", "+00:00"))
80
+ except ValueError:
81
+ return None
82
+ if parsed.tzinfo is None:
83
+ parsed = parsed.replace(tzinfo=timezone.utc)
84
+ return parsed.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
85
+
86
+
87
+ def _validate_audit(body: Dict[str, Any], *, require_core: bool) -> None:
88
+ """Client-side validation of an audit body (exit 2 on a bad value), mirroring the server's
89
+ accepted ranges/enums so a typo never costs a round-trip or surfaces as a raw 422.
90
+
91
+ Also normalizes ``schedule_anchor`` in place to RFC3339 — see the comment there."""
92
+ if require_core and not str(body.get("name") or "").strip():
93
+ raise typer.BadParameter("audit 'name' is required.")
94
+ interval = body.get("schedule_interval_secs")
95
+ if isinstance(interval, int) and not (_INTERVAL_MIN <= interval <= _INTERVAL_MAX):
96
+ raise typer.BadParameter(
97
+ f"schedule_interval_secs must be between {_INTERVAL_MIN} (1h) and {_INTERVAL_MAX} (7d).",
98
+ param_hint="--schedule-interval-secs",
99
+ )
100
+ lookback = body.get("lookback_window_secs")
101
+ if isinstance(lookback, int) and not (_LOOKBACK_MIN <= lookback <= _LOOKBACK_MAX):
102
+ raise typer.BadParameter(
103
+ f"lookback_window_secs must be between {_LOOKBACK_MIN} (1h) and {_LOOKBACK_MAX} (90d).",
104
+ param_hint="--lookback-window-secs",
105
+ )
106
+ mode = body.get("window_mode")
107
+ if mode and mode not in _WINDOW_MODES:
108
+ raise typer.BadParameter(
109
+ f"window_mode must be one of: {', '.join(_WINDOW_MODES)}.", param_hint="--window-mode"
110
+ )
111
+ anchor = body.get("schedule_anchor")
112
+ if anchor is not None:
113
+ normalized = _parse_anchor(str(anchor))
114
+ if not normalized:
115
+ raise typer.BadParameter(
116
+ "schedule_anchor must be an ISO 8601 timestamp, e.g. 2026-07-22T09:00:00Z.",
117
+ param_hint="--schedule-anchor",
118
+ )
119
+ # Normalized in place, so every write path (flags, --file, the edit
120
+ # read-merge) sends the RFC3339 form the server's DateTime<Utc>
121
+ # deserializer accepts. Rewriting it here rather than in the override
122
+ # builder keeps an unparseable value reaching the check above instead of
123
+ # being silently dropped as "not supplied".
124
+ body["schedule_anchor"] = normalized
125
+ sensitivity = body.get("sensitivity")
126
+ if sensitivity and sensitivity not in _SENSITIVITIES:
127
+ raise typer.BadParameter(
128
+ f"sensitivity must be one of: {', '.join(_SENSITIVITIES)}.", param_hint="--sensitivity"
129
+ )
130
+ top_k = body.get("top_k")
131
+ if isinstance(top_k, int) and top_k < 1:
132
+ raise typer.BadParameter("top_k must be at least 1.", param_hint="--top-k")
133
+
134
+
135
+ def _fail(state: AppState, exc: Exception, *, finding_id: str = "") -> None:
136
+ """Re-raise as a typed error for the central chokepoint to render (JSON envelope under
137
+ ``--json`` on stdout, red box otherwise). A 404 — or a **malformed (non-UUID) finding id**,
138
+ which the server answers with a 400 from its path extractor rather than a 404 — becomes the
139
+ friendlier ``no finding <id>`` (exit 6); a permission denial and every other error keep the
140
+ server's message and exit code."""
141
+ if isinstance(exc, ForbiddenError):
142
+ raise exc # a 403 reached the handler with a usable id — surface it as-is
143
+ status = getattr(exc, "status", None) or 0
144
+ malformed = bool(finding_id) and not _UUID_RE.match(finding_id) and status >= 400
145
+ if finding_id and (malformed or isinstance(exc, NotFoundError)):
146
+ raise NotFoundError(
147
+ f"no finding {finding_id}",
148
+ hint="run `fp audits findings` to list findings",
149
+ )
150
+ raise exc
151
+
152
+
153
+ def _parse_json_opt(value: Optional[str], hint: str) -> Any:
154
+ if value is None:
155
+ return None
156
+ try:
157
+ return json.loads(value)
158
+ except json.JSONDecodeError as exc:
159
+ raise typer.BadParameter(f"{hint} is not valid JSON: {exc}", param_hint=hint)
160
+
161
+
162
+ # Mirrors MAX_CONTEXT_CHARS / MAX_REFERENCE_URLS in server/src/audits/context.rs.
163
+ # Checked here so an over-long brief is a usage error at exit 2, not a 422 after
164
+ # the request went out.
165
+ _MAX_CONTEXT_CHARS = 8192
166
+ _MAX_REFERENCE_URLS = 5
167
+
168
+
169
+ def _context_text(text: Optional[str], text_file: Optional[str]) -> Optional[str]:
170
+ """The brief from ``--text`` or ``--text-file``. ``None`` means neither was given.
171
+
172
+ Shared by ``audits create`` and ``audits context-set`` so the two flags mean the same
173
+ thing on both, and the cap is stated once."""
174
+ if text is not None and text_file:
175
+ raise typer.BadParameter("pass --text or --text-file, not both.", param_hint="--text-file")
176
+ body_text = text
177
+ if text_file:
178
+ try:
179
+ with open(text_file, "r", encoding="utf-8") as fh:
180
+ body_text = fh.read()
181
+ except OSError as exc:
182
+ raise typer.BadParameter(f"could not read {text_file}: {exc}", param_hint="--text-file")
183
+ if body_text is not None and len(body_text) > _MAX_CONTEXT_CHARS:
184
+ raise typer.BadParameter(
185
+ f"the brief is limited to {_MAX_CONTEXT_CHARS} characters.", param_hint="--text"
186
+ )
187
+ return body_text
188
+
189
+
190
+ def _context_urls(url: Optional[List[str]]) -> List[str]:
191
+ """The ``--url`` list, capped where the server caps it."""
192
+ urls = list(url or [])
193
+ if len(urls) > _MAX_REFERENCE_URLS:
194
+ raise typer.BadParameter(
195
+ f"an audit may reference at most {_MAX_REFERENCE_URLS} URLs.", param_hint="--url"
196
+ )
197
+ return urls
198
+
199
+
200
+ # Keys `audits show --json` emits that the definition endpoint will not accept.
201
+ #
202
+ # Server-derived state the server simply ignores, plus the two context keys it
203
+ # actively 422s (they are written through `PUT /audits/{id}/context`). Dropping
204
+ # them here is what makes the documented `audits show --json > f && audits edit
205
+ # --file f` round-trip work: without it the file carries `additional_context`,
206
+ # the server rejects the whole request, and the edit fails for a field the
207
+ # operator never touched.
208
+ _READ_ONLY_AUDIT_KEYS = frozenset({
209
+ "id", "created_by", "created_at", "updated_at",
210
+ "open_findings", "run_count",
211
+ "last_run_status", "last_run_finished_at", "last_attempted_at",
212
+ "next_attempt_at", "last_error",
213
+ # Written through the context sub-resource, not here.
214
+ "additional_context", "reference_urls", "reference_url_count",
215
+ })
216
+
217
+
218
+ def _load_file(file: Optional[str]) -> Dict[str, Any]:
219
+ if file is None:
220
+ return {}
221
+ raw = _write.read_text_arg(file)
222
+ try:
223
+ parsed = json.loads(raw)
224
+ except json.JSONDecodeError as exc:
225
+ raise typer.BadParameter(f"--file is not valid JSON: {exc}", param_hint="--file")
226
+ if not isinstance(parsed, dict):
227
+ raise typer.BadParameter("--file must contain a JSON object.", param_hint="--file")
228
+ return {k: v for k, v in parsed.items() if k not in _READ_ONLY_AUDIT_KEYS}
229
+
230
+
231
+ def _apply_overrides(body: Dict[str, Any], **overrides: Any) -> Dict[str, Any]:
232
+ for key, value in overrides.items():
233
+ if value is not None:
234
+ body[key] = value
235
+ return body
236
+
237
+
238
+ def _audit_to_body(audit: Audit) -> Dict[str, Any]:
239
+ """The writable fields of an existing audit, used as the merge base for a flag-only edit.
240
+
241
+ The server's ``PUT /api/audits/{id}`` replaces the definition (and ``name`` is mandatory on
242
+ it), so a single-field change must re-send the whole audit. Server-derived state
243
+ (``open_findings``, the last-run columns) is deliberately excluded — it is read-only."""
244
+ return {
245
+ "name": audit.name,
246
+ "description": audit.description,
247
+ "enabled": audit.enabled,
248
+ "schedule_interval_secs": audit.schedule_interval_secs,
249
+ "schedule_anchor": audit.schedule_anchor,
250
+ "window_mode": audit.window_mode,
251
+ "lookback_window_secs": audit.lookback_window_secs,
252
+ "scope": audit.scope,
253
+ "ignore_error_types": audit.ignore_error_types,
254
+ "llm_enabled": audit.llm_enabled,
255
+ "top_k": audit.top_k,
256
+ "sensitivity": audit.sensitivity,
257
+ "channels": audit.channels,
258
+ }
259
+
260
+
261
+ def _audit_from_body(body: Dict[str, Any], base: Optional[Audit] = None) -> Audit:
262
+ """Build an ``Audit`` view from a create/edit request body — the fallback when the canonical
263
+ re-fetch misses. ``base`` (the pre-edit audit) supplies id/created/derived context; a create
264
+ has none, so those default to empty."""
265
+ merged: Dict[str, Any] = dict(_audit_to_body(base)) if base else {}
266
+ merged.update({k: v for k, v in body.items() if v is not None})
267
+ merged.setdefault("id", base.id if base else "")
268
+ return Audit.from_dict({
269
+ **merged,
270
+ "id": base.id if base else "",
271
+ "created_by": base.created_by if base else "",
272
+ "created_at": base.created_at if base else "",
273
+ "open_findings": base.open_findings if base else 0,
274
+ "last_run_status": base.last_run_status if base else None,
275
+ "last_run_finished_at": base.last_run_finished_at if base else None,
276
+ })
277
+
278
+
279
+ def _refetch_audit(cctx, name: Optional[str]) -> Optional[Audit]:
280
+ """Re-read the canonical stored audit by name after a create/edit (the server busts the list
281
+ cache on write, so this reflects the saved state incl. server-applied defaults). Returns
282
+ ``None`` on any miss so the caller falls back to rendering from the request body."""
283
+ if not name:
284
+ return None
285
+ try:
286
+ for a in api.list_audits(cctx):
287
+ if a.name == name:
288
+ return a
289
+ except Exception:
290
+ return None
291
+ return None
292
+
293
+
294
+ def _resolve_audit_or_exit(audits, handle: str) -> Audit:
295
+ """Resolve an audit by **name** (primary), falling back to an exact id match, raising a typed
296
+ error the central chokepoint renders: none → exit 6, several → exit 2."""
297
+ return _write.resolve_one(audits, handle, kind="audit", list_cmd="audits list")
298
+
299
+
300
+ def audits_list(
301
+ ctx: typer.Context,
302
+ enabled_only: bool = typer.Option(False, "--enabled-only", help="Only audits that are switched on."),
303
+ show_id: bool = typer.Option(False, "--show-id", help="Prepend a short audit-id column (the full id is always in --json)."),
304
+ ) -> None:
305
+ """List audit definitions in a boxed table, newest first.
306
+
307
+ Shows `created · name · by · every · findings · status · last run` — `every` is the humanized
308
+ schedule interval, `findings` the open-finding count (pink when there's something to triage),
309
+ and `last run` the age of the last run tinted by its outcome (`never` if it hasn't run yet).
310
+ Disabled audits are dimmed; the on/off split is in the footer. `name` is the handle the other
311
+ subcommands take; the raw id is hidden unless `--show-id`. Needs `audits:read`. With `--json`:
312
+ `{"audits": [{id, name, enabled, schedule_interval_secs, window_mode, scope, open_findings,
313
+ last_run_status, created_by, created_at, ...}]}`.
314
+
315
+ Example:
316
+
317
+ * `fp audits list --enabled-only`
318
+ """
319
+ state: AppState = ctx.obj
320
+ cctx = require_auth(state)
321
+ audits = api.list_audits(cctx)
322
+ if enabled_only:
323
+ audits = [a for a in audits if a.enabled]
324
+ if state.json:
325
+ output.emit_json({"audits": audits})
326
+ return
327
+ output.render_audits(audits, show_id=show_id)
328
+ output.audits_footer(audits)
329
+
330
+
331
+ def audits_show(
332
+ ctx: typer.Context,
333
+ name: str = typer.Argument(..., metavar="NAME", help="Audit name (or id)."),
334
+ ) -> None:
335
+ """Show one audit as a stack of cards — identity, schedule, scope, analysis, channels.
336
+
337
+ Referenced by audit **name** (a UUID-shaped id is also accepted). The identity card carries the
338
+ on/off state, the cadence and the open-finding count; `scope` shows what activity the audit
339
+ covers, `analysis` the LLM/sensitivity settings, `channels` default-vs-custom delivery.
340
+ Not-found → red `✗ no audit named "…"`, exit 6. Needs `audits:read`. With `--json`: the full
341
+ raw `Audit` (untouched `scope` + `channels`).
342
+
343
+ Example:
344
+
345
+ * `fp audits show weekly-failure-audit`
346
+ """
347
+ state: AppState = ctx.obj
348
+ cctx = require_auth(state)
349
+ audit = _resolve_audit_or_exit(api.list_audits(cctx), name)
350
+ if state.json:
351
+ output.emit_json(audit)
352
+ return
353
+ output.render_audit_show(audit)
354
+
355
+
356
+ def _definition_overrides(
357
+ name, description, enabled, schedule_interval_secs, schedule_anchor, window_mode,
358
+ lookback_window_secs, scope, ignore_error_type, llm_enabled, top_k, sensitivity, channels,
359
+ ) -> Dict[str, Any]:
360
+ """The discrete definition flags → an overrides dict (``None`` = "not supplied", so it never
361
+ clobbers a value carried over from `--file` or the existing audit)."""
362
+ return dict(
363
+ name=name,
364
+ description=description,
365
+ enabled=enabled,
366
+ schedule_interval_secs=schedule_interval_secs,
367
+ # Raw here on purpose — `_validate_audit` parses/normalizes it. Parsing at
368
+ # this point would turn a malformed anchor into None, i.e. "not supplied",
369
+ # and silently ignore the flag instead of erroring.
370
+ schedule_anchor=schedule_anchor,
371
+ window_mode=window_mode,
372
+ lookback_window_secs=lookback_window_secs,
373
+ scope=_parse_json_opt(scope, "--scope"),
374
+ ignore_error_types=collect_multi(ignore_error_type),
375
+ llm_enabled=llm_enabled,
376
+ top_k=top_k,
377
+ sensitivity=sensitivity,
378
+ channels=_parse_json_opt(channels, "--channels"),
379
+ )
380
+
381
+
382
+ def audits_create(
383
+ ctx: typer.Context,
384
+ name: str = typer.Argument(..., metavar="NAME", help="Audit name (unique per org)."),
385
+ file: Optional[str] = typer.Option(None, "--file", help="Full audit JSON to base it on, or `-` for stdin."),
386
+ description: Optional[str] = typer.Option(None, "--description", help="What this audit is for."),
387
+ enabled: Optional[bool] = typer.Option(None, "--enabled/--disabled", help="Start it on or off (default: on)."),
388
+ schedule_interval_secs: Optional[int] = typer.Option(None, "--schedule-interval-secs", help=f"How often it runs, in seconds ({_INTERVAL_MIN}–{_INTERVAL_MAX})."),
389
+ schedule_anchor: Optional[str] = typer.Option(None, "--schedule-anchor", help="Fixed UTC slot the schedule is phased to, ISO 8601 (default: next 09:00 UTC)."),
390
+ window_mode: Optional[str] = typer.Option(None, "--window-mode", help=f"Which window each run sweeps: {', '.join(_WINDOW_MODES)}."),
391
+ lookback_window_secs: Optional[int] = typer.Option(None, "--lookback-window-secs", help=f"How far back a run looks, in seconds ({_LOOKBACK_MIN}–{_LOOKBACK_MAX})."),
392
+ scope: Optional[str] = typer.Option(None, "--scope", help="Scope filter as JSON, e.g. `{\"environments\":[\"prod\"]}` (omit to cover everything)."),
393
+ ignore_error_type: Optional[List[str]] = typer.Option(None, "--ignore-error-type", help="Error type to exclude (repeatable, or CSV)."),
394
+ llm_enabled: Optional[bool] = typer.Option(None, "--llm/--no-llm", help="Use the LLM analysis pass (default: on)."),
395
+ top_k: Optional[int] = typer.Option(None, "--top-k", help="Max findings a run keeps."),
396
+ sensitivity: Optional[str] = typer.Option(None, "--sensitivity", help=f"How eagerly it flags: {', '.join(_SENSITIVITIES)}."),
397
+ channels: Optional[str] = typer.Option(None, "--channels", help="Channels as a JSON array, e.g. `[{\"kind\":\"slack\"}]` (omit for the org defaults)."),
398
+ text: Optional[str] = typer.Option(
399
+ None, "--text", help="Operator brief for the analysis prompt (max 8192 chars)."
400
+ ),
401
+ text_file: Optional[str] = typer.Option(
402
+ None, "--text-file", help="Read the brief from a file instead of --text."
403
+ ),
404
+ url: Optional[List[str]] = typer.Option(
405
+ None, "--url", help="Reference URL; repeat up to 5 times. Public https:// only."
406
+ ),
407
+ ) -> None:
408
+ """Create an audit and show it rendered as parsed cards.
409
+
410
+ Give it a **name** (positional), then define it with the flags or base it on a full JSON
411
+ payload via `--file` (flags layer on top). Everything except the name has a server default —
412
+ a bare `fp audits create nightly` gives you a daily, LLM-backed audit over all activity.
413
+ On success it renders exactly the way `audits show` does, in a green "audit created" card.
414
+ Creating isn't destructive, so there's no confirm; a name collision is rejected up front
415
+ (exit 2). New audits start **enabled** unless you pass `--disabled`. Needs `audits:write`.
416
+ With `--json`: `{id, created_at, sources}`.
417
+
418
+ `--text`/`--text-file`/`--url` attach reference context in the SAME request, which is
419
+ also the only way to be sure the first run has it: a new enabled audit is due
420
+ immediately, so context set afterwards can miss it. A URL the guard refuses fails the
421
+ whole create — no half-made audit is left behind. Change it later with
422
+ `audits context-set`.
423
+
424
+ Examples:
425
+
426
+ * `fp audits create nightly-prod --scope '{"environments":["prod"]}' --schedule-interval-secs 86400`
427
+ * `fp audits create nightly-prod --text "checkout agent for a retail store" --url https://docs.example.com/runbook`
428
+ * `fp audits create weekly --file audit.json` — base it on a saved payload
429
+ * `fp audits create weekly --file audit.json --sensitivity high` — file + an override
430
+ """
431
+ state: AppState = ctx.obj
432
+ body = _load_file(file)
433
+ _apply_overrides(body, **_definition_overrides(
434
+ name, description, enabled, schedule_interval_secs, schedule_anchor, window_mode,
435
+ lookback_window_secs, scope, ignore_error_type, llm_enabled, top_k, sensitivity, channels,
436
+ ))
437
+ # Context travels WITH the definition — the server writes both in one
438
+ # transaction, so the run it queues cannot start before the material lands.
439
+ # Flags layer over `--file` here too; neither flag leaves whatever the file
440
+ # carried alone.
441
+ brief, urls = _context_text(text, text_file), _context_urls(url)
442
+ if brief is not None or urls:
443
+ body["context"] = {"text": brief or "", "urls": urls}
444
+ _validate_audit(body, require_core=True)
445
+ cctx = require_auth(state)
446
+ if any(a.name == body.get("name") for a in api.list_audits(cctx)): # names are the handle → unique
447
+ raise click.UsageError(f'an audit named "{body.get("name")}" already exists')
448
+ result = api.create_audit(cctx, body)
449
+ _write.record_action("audit_created", resource="audit", success=True)
450
+ if body.get("context"):
451
+ # Same event and the same allowlisted properties as `context-set`, so
452
+ # "was context filled in at creation?" is answerable across surfaces.
453
+ # `via` and `url_count` are both on _SAFE_PROP_KEYS; shape only, never a URL.
454
+ _write.record_action(
455
+ "audit_context_saved", resource="audit", success=True, via="cli",
456
+ url_count=len(body["context"]["urls"]),
457
+ )
458
+ if state.json:
459
+ output.emit_json(result)
460
+ return
461
+ # Re-read the canonical stored audit (server defaults applied) for the rendered cards.
462
+ audit = _refetch_audit(cctx, body.get("name")) or _audit_from_body(body)
463
+ output.render_audit_created(audit)
464
+
465
+
466
+ def audits_edit(
467
+ ctx: typer.Context,
468
+ name: str = typer.Argument(..., metavar="NAME", help="Audit name (or id) to edit."),
469
+ new_name: Optional[str] = typer.Option(None, "--name", help="Rename the audit."),
470
+ file: Optional[str] = typer.Option(None, "--file", help="Full audit JSON to replace it with, or `-` for stdin."),
471
+ description: Optional[str] = typer.Option(None, "--description", help="New description."),
472
+ enabled: Optional[bool] = typer.Option(None, "--enabled/--disabled", help="Switch the audit on or off."),
473
+ schedule_interval_secs: Optional[int] = typer.Option(None, "--schedule-interval-secs", help=f"New run interval, in seconds ({_INTERVAL_MIN}–{_INTERVAL_MAX})."),
474
+ schedule_anchor: Optional[str] = typer.Option(None, "--schedule-anchor", help="New fixed UTC slot the schedule is phased to, ISO 8601."),
475
+ window_mode: Optional[str] = typer.Option(None, "--window-mode", help=f"New window mode: {', '.join(_WINDOW_MODES)}."),
476
+ lookback_window_secs: Optional[int] = typer.Option(None, "--lookback-window-secs", help=f"New lookback, in seconds ({_LOOKBACK_MIN}–{_LOOKBACK_MAX})."),
477
+ scope: Optional[str] = typer.Option(None, "--scope", help="New scope filter as JSON."),
478
+ ignore_error_type: Optional[List[str]] = typer.Option(None, "--ignore-error-type", help="Replace the ignored error types (repeatable, or CSV)."),
479
+ llm_enabled: Optional[bool] = typer.Option(None, "--llm/--no-llm", help="Turn the LLM analysis pass on or off."),
480
+ top_k: Optional[int] = typer.Option(None, "--top-k", help="New max findings per run."),
481
+ sensitivity: Optional[str] = typer.Option(None, "--sensitivity", help=f"New sensitivity: {', '.join(_SENSITIVITIES)}."),
482
+ channels: Optional[str] = typer.Option(None, "--channels", help="New channels as a JSON array."),
483
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip the confirmation prompt. The prompt only appears on an interactive terminal: under --json, or with stdin redirected, this command proceeds without asking."),
484
+ ) -> None:
485
+ """Edit an audit, referenced by **name** (or a UUID-shaped id). Two ways to change it:
486
+
487
+ * **Tweak fields** with the override flags (e.g. `--sensitivity high`, `--disabled`). Because
488
+ the server replaces the whole definition, the CLI re-sends the current audit with your
489
+ changes applied — so a flag-only edit needs `audits:read` **and** `audits:write`.
490
+ * **Replace wholesale** with `--file` (a complete audit payload), optionally layering a few
491
+ override flags on top.
492
+
493
+ It confirms first (default no; `--yes` skips) and renders the new state as a green "audit
494
+ updated" card. A rename onto an existing name is rejected (exit 2). Needs `audits:write`.
495
+ With `--json`: `{id, updated}`.
496
+
497
+ Examples:
498
+
499
+ * `fp audits edit nightly-prod --sensitivity high` — change one field
500
+ * `fp audits edit nightly-prod --disabled --yes` — pause it, no prompt
501
+ * `fp audits edit nightly-prod --name nightly --yes` — rename
502
+ """
503
+ state: AppState = ctx.obj
504
+ overrides = _definition_overrides(
505
+ new_name, description, enabled, schedule_interval_secs, schedule_anchor, window_mode,
506
+ lookback_window_secs, scope, ignore_error_type, llm_enabled, top_k, sensitivity, channels,
507
+ )
508
+ cctx = require_auth(state)
509
+ existing = api.list_audits(cctx)
510
+ audit = _resolve_audit_or_exit(existing, name)
511
+ if file is not None:
512
+ # An explicit full body is a straight replace.
513
+ body = _load_file(file)
514
+ _apply_overrides(body, **overrides)
515
+ body.setdefault("name", audit.name)
516
+ else:
517
+ # Flag-only edit: the server replaces the definition, so seed the body from the resolved
518
+ # audit and overlay just the changed fields (read-merge, like `alerts update`).
519
+ body = _audit_to_body(audit)
520
+ _apply_overrides(body, **overrides)
521
+ _validate_audit(body, require_core=True)
522
+ final_name = body.get("name") or audit.name
523
+ if final_name != audit.name and any(a.name == final_name and a.id != audit.id for a in existing):
524
+ raise click.UsageError(f'an audit named "{final_name}" already exists')
525
+ if _write.should_prompt(state, yes):
526
+ if not output.confirm_audit_edit(audit.name):
527
+ if state.json:
528
+ output.emit_json({"cancelled": True})
529
+ else:
530
+ output.cancelled_plain("nothing changed")
531
+ return
532
+ result = api.update_audit(cctx, audit.id, body)
533
+ _write.record_action("audit_updated", resource="audit", success=True)
534
+ if state.json:
535
+ output.emit_json(result)
536
+ return
537
+ updated = _refetch_audit(cctx, final_name) or _audit_from_body(body, base=audit)
538
+ output.render_audit_updated(updated, old_name=audit.name)
539
+
540
+
541
+ def audits_delete(
542
+ ctx: typer.Context,
543
+ name: str = typer.Argument(..., metavar="NAME", help="Audit name (or id) to delete."),
544
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip the confirmation prompt. The prompt only appears on an interactive terminal: under --json, or with stdin redirected, this command proceeds without asking."),
545
+ ) -> None:
546
+ """Delete an audit, referenced by **name** (or a UUID-shaped id). This cannot be undone.
547
+
548
+ Shows an amber preview of the audit (incl. its open-finding count — the findings and the run
549
+ history go with it) then confirms. Needs `audits:write`. With `--json`: `{"deleted": true, "id",
550
+ "name"}` (or `{"cancelled": true}` on a declined prompt).
551
+
552
+ Example:
553
+
554
+ * `fp audits delete old-experiment --yes`
555
+ """
556
+ state: AppState = ctx.obj
557
+ cctx = require_auth(state)
558
+ audit = _resolve_audit_or_exit(api.list_audits(cctx), name)
559
+ if _write.should_prompt(state, yes):
560
+ output.render_audit_delete_preview(audit) # amber preview of what's about to go
561
+ if not output.confirm_audit_delete(audit.open_findings):
562
+ if state.json:
563
+ output.emit_json({"cancelled": True})
564
+ else:
565
+ output.cancelled_plain("nothing deleted")
566
+ return
567
+ api.delete_audit(cctx, audit.id)
568
+ _write.record_action("audit_deleted", resource="audit", success=True, destructive=True)
569
+ if state.json:
570
+ output.emit_json({"deleted": True, "id": audit.id, "name": audit.name})
571
+ else:
572
+ output.audit_deleted(audit.name)
573
+
574
+
575
+ def audits_run(
576
+ ctx: typer.Context,
577
+ name: str = typer.Argument(..., metavar="NAME", help="Audit name (or id) to run now."),
578
+ ) -> None:
579
+ """Queue an audit to run now, ahead of its schedule.
580
+
581
+ This makes the audit **due**; the dispatcher picks it up on its next tick, so success here
582
+ means "queued", not "finished" — follow it with `fp audits runs <name>`. An audit that
583
+ is disabled, or that already has a run in progress, is refused with the server's explanation
584
+ (exit 1) rather than silently double-queued. Needs `audits:write`. With `--json`:
585
+ `{"queued": true}`.
586
+
587
+ Example:
588
+
589
+ * `fp audits run nightly-prod`
590
+ """
591
+ state: AppState = ctx.obj
592
+ cctx = require_auth(state)
593
+ audit = _resolve_audit_or_exit(api.list_audits(cctx), name)
594
+ try:
595
+ result = api.run_audit(cctx, audit.id)
596
+ except ApiError as exc:
597
+ if getattr(exc, "status", None) == 409:
598
+ # A run is already in progress (or the audit is disabled) — the server's message says
599
+ # which; add the "what now" pointer the bare message lacks.
600
+ raise ApiError(
601
+ exc.message,
602
+ status=exc.status,
603
+ request_id=exc.request_id,
604
+ hint=f"check it with `fp audits runs {audit.name}`",
605
+ )
606
+ raise
607
+ _write.record_action("audit_run_queued", resource="audit", success=True)
608
+ if state.json:
609
+ output.emit_json(result)
610
+ else:
611
+ output.audit_run_queued(audit.name)
612
+
613
+
614
+ def audits_context_show(
615
+ ctx: typer.Context,
616
+ name: str = typer.Argument(..., metavar="NAME", help="Audit name (or id)."),
617
+ ) -> None:
618
+ """Show the reference context an audit sends to the analysis prompt.
619
+
620
+ Prints the operator brief plus every reference URL with its fetch state: how many
621
+ characters were stored, whether the snapshot was truncated, how many secret-shaped
622
+ values were masked, and whether the page contains phrases that read as instructions
623
+ to an AI (which is worth reading before the next run). Needs `audits:read`.
624
+
625
+ Example:
626
+
627
+ * `fp audits context-show nightly-prod`
628
+ """
629
+ state: AppState = ctx.obj
630
+ cctx = require_auth(state)
631
+ audit = _resolve_audit_or_exit(api.list_audits(cctx), name)
632
+ result = api.get_audit_context(cctx, audit.id)
633
+ if state.json:
634
+ output.emit_json(result)
635
+ else:
636
+ output.audit_context(audit.name, result)
637
+
638
+
639
+ def audits_context_set(
640
+ ctx: typer.Context,
641
+ name: str = typer.Argument(..., metavar="NAME", help="Audit name (or id)."),
642
+ text: Optional[str] = typer.Option(
643
+ None, "--text", help="Operator brief (max 8192 chars). Pass an empty string to clear it."
644
+ ),
645
+ text_file: Optional[str] = typer.Option(
646
+ None, "--text-file", help="Read the brief from a file instead of --text."
647
+ ),
648
+ url: Optional[List[str]] = typer.Option(
649
+ None, "--url", help="Reference URL; repeat up to 5 times. Public https:// only. "
650
+ "Replaces the existing list; omit to leave it unchanged."
651
+ ),
652
+ clear_urls: bool = typer.Option(
653
+ False, "--clear-urls", help="Remove every reference URL and its stored snapshot."
654
+ ),
655
+ ) -> None:
656
+ """Update an audit's reference context.
657
+
658
+ Each half is independent: pass `--text`/`--text-file` to change the brief, `--url`
659
+ to replace the URL list, or both. **Whatever you omit is left alone.** Removing
660
+ something is always explicit — `--text ""` clears the brief, `--clear-urls` drops
661
+ every URL and its stored snapshot.
662
+
663
+ Omission used to mean "delete" for URLs but "keep" for the brief, so a routine
664
+ `--text` edit silently threw away every reference page.
665
+
666
+ URLs are validated immediately (public `https://` only; private, loopback and
667
+ cloud-metadata addresses are refused) and fetched in the background, so a slow site
668
+ never blocks the save and never blocks a run. Needs `audits:write`.
669
+
670
+ Examples:
671
+
672
+ * `fp audits context-set nightly-prod --text "checkout agent for a retail store"`
673
+ * `fp audits context-set nightly-prod --url https://docs.example.com/runbook`
674
+ * `fp audits context-set nightly-prod --text ""` — clear the brief
675
+ * `fp audits context-set nightly-prod --clear-urls` — drop the pages
676
+ """
677
+ state: AppState = ctx.obj
678
+ cctx = require_auth(state)
679
+ body_text = _context_text(text, text_file)
680
+ urls = _context_urls(url)
681
+ if clear_urls and urls:
682
+ raise typer.BadParameter("pass --url or --clear-urls, not both.", param_hint="--clear-urls")
683
+ if body_text is None and not urls and not clear_urls:
684
+ raise typer.BadParameter(
685
+ "nothing to change — pass --text, --text-file, --url or --clear-urls.",
686
+ param_hint="--text",
687
+ )
688
+
689
+ audit = _resolve_audit_or_exit(api.list_audits(cctx), name)
690
+ # The endpoint is a FULL replacement, so anything the caller did not name has
691
+ # to be read back and re-sent. Both halves, symmetrically: this used to merge
692
+ # the brief and not the URLs, which made `--text` alone a silent delete of
693
+ # every reference page — the read-merge-allowlist class recorded in
694
+ # models.py:116, in miniature.
695
+ if body_text is None or not (urls or clear_urls):
696
+ current = api.get_audit_context(cctx, audit.id)
697
+ if body_text is None:
698
+ body_text = current.get("text", "")
699
+ if not urls and not clear_urls:
700
+ urls = [str(s.get("url") or "") for s in (current.get("sources") or [])]
701
+ urls = [u for u in urls if u]
702
+ result = api.put_audit_context(cctx, audit.id, {"text": body_text, "urls": urls})
703
+ # Shape only — how many URLs, never which. The name is `url_count` because that is
704
+ # what the dashboard sends for this same quantity (the API field is
705
+ # `reference_url_count`); the CLI used to send `count`, so one event carried two
706
+ # property names and neither answered it alone. It is on _SAFE_PROP_KEYS —
707
+ # anything not on that allowlist is dropped silently, so a renamed property needs
708
+ # its entry there in the same change or it never existed.
709
+ # `via` names the surface this was written from, so the one series splits
710
+ # into create / settings / cli rather than answering "somebody saved
711
+ # context" and nothing more. The dashboard emits the other two values.
712
+ # Already on _SAFE_PROP_KEYS, so it survives the allowlist.
713
+ _write.record_action(
714
+ "audit_context_saved", resource="audit", success=True, via="cli", url_count=len(urls)
715
+ )
716
+ if state.json:
717
+ output.emit_json(result)
718
+ else:
719
+ output.audit_context_saved(audit.name, result)
720
+
721
+
722
+ def audits_context_refresh(
723
+ ctx: typer.Context,
724
+ name: str = typer.Argument(..., metavar="NAME", help="Audit name (or id)."),
725
+ ) -> None:
726
+ """Re-fetch every reference URL on an audit.
727
+
728
+ Snapshots refresh on their own weekly, so this is for when you know a page changed
729
+ and want it picked up now. URLs the guard refused are not retried — nothing about
730
+ them can change until the URL itself does. Needs `audits:write`.
731
+
732
+ Example:
733
+
734
+ * `fp audits context-refresh nightly-prod`
735
+ """
736
+ state: AppState = ctx.obj
737
+ cctx = require_auth(state)
738
+ audit = _resolve_audit_or_exit(api.list_audits(cctx), name)
739
+ result = api.refresh_audit_context(cctx, audit.id)
740
+ # `url_count` is how many URLs the server queued, under the same property name the
741
+ # dashboard sends — see the note in `audits_context_set`.
742
+ _write.record_action(
743
+ "audit_context_refreshed", resource="audit", success=True,
744
+ url_count=int(result.get("queued") or 0),
745
+ )
746
+ if state.json:
747
+ output.emit_json(result)
748
+ else:
749
+ output.audit_context_refreshed(audit.name, result)
750
+
751
+
752
+ def audits_runs(
753
+ ctx: typer.Context,
754
+ name: str = typer.Argument(..., metavar="NAME", help="Audit name (or id)."),
755
+ limit: int = typer.Option(50, "--limit", "-n", help="Max runs to show (the server returns the 50 most recent)."),
756
+ show_id: bool = typer.Option(False, "--show-id", help="Prepend a short run-id column (the full id is always in --json)."),
757
+ ) -> None:
758
+ """List an audit's run history in a boxed table, newest first.
759
+
760
+ Shows `started · status · trigger · findings · new · took` — `findings` is the run's total,
761
+ `new` the count first seen in that run, `took` the wall time (`-` while a run is still going).
762
+ A failed run's `error` and each run's `stats`/`report` live in `--json`. Needs `audits:read`.
763
+ With `--json`: `{"runs": [{id, status, trigger_kind, window_from, window_to, started_at,
764
+ finished_at, stats, findings_count, new_findings_count, report, error}]}`.
765
+
766
+ Example:
767
+
768
+ * `fp audits runs nightly-prod --limit 10`
769
+ """
770
+ state: AppState = ctx.obj
771
+ validate_limit(limit)
772
+ cctx = require_auth(state)
773
+ audit = _resolve_audit_or_exit(api.list_audits(cctx), name)
774
+ runs = api.list_audit_runs(cctx, audit.id)[:limit]
775
+ if state.json:
776
+ output.emit_json({"runs": runs})
777
+ return
778
+ output.render_audit_runs(runs, name=audit.name, show_id=show_id)
779
+ output.audit_runs_footer(runs)
780
+
781
+
782
+ def audits_findings(
783
+ ctx: typer.Context,
784
+ audit: Optional[str] = typer.Option(None, "--audit", help="Only findings from this audit (name, or id)."),
785
+ run_id: Optional[str] = typer.Option(None, "--run-id", help="Only findings produced or updated by this run."),
786
+ status: Optional[str] = typer.Option(None, "--status", help=f"Filter by status: {', '.join(_FINDING_STATUSES)} (CSV; default: open + recurring)."),
787
+ limit: int = typer.Option(100, "--limit", "-n", help="Max findings to return (server caps at 500)."),
788
+ offset: int = typer.Option(0, "--offset", help="Skip this many findings (paging)."),
789
+ show_id: bool = typer.Option(False, "--show-id", help="Show full finding ids instead of the short form (always full in --json)."),
790
+ ) -> None:
791
+ """List findings across audits in a boxed table, highest priority first.
792
+
793
+ Shows `id · title · severity · status · kind · seen · last` — the id is the handle the triage
794
+ commands take (short by default, full with `--show-id`); `kind` separates a `failure` from a
795
+ `policy` violation or an `improvement`; `seen` is the occurrence count and `last` the age of the
796
+ most recent sighting. With no `--status` the server returns the live set (open + recurring).
797
+ Suppressed findings are dimmed; a status/severity breakdown is in the footer. `--audit` takes an
798
+ audit **name**. Needs `audits:read`. With `--json`: `{"findings": [{id, audit_id, audit_name,
799
+ title, severity, status, kind, priority, occurrences, last_seen_at, recommendation, ...}]}`.
800
+
801
+ Examples:
802
+
803
+ * `fp audits findings --status open --limit 20`
804
+ * `fp audits findings --audit nightly-prod`
805
+ """
806
+ state: AppState = ctx.obj
807
+ _validate_statuses(status)
808
+ validate_limit(limit)
809
+ if offset < 0:
810
+ raise typer.BadParameter("must be zero or a positive integer.", param_hint="--offset")
811
+ cctx = require_auth(state)
812
+ audit_id = None
813
+ if audit:
814
+ audit_id = _resolve_audit_or_exit(api.list_audits(cctx), audit).id
815
+ findings = api.list_audit_findings(
816
+ cctx, audit_id=audit_id, run_id=run_id, status=status, limit=limit, offset=offset
817
+ )
818
+ if state.json:
819
+ output.emit_json({"findings": findings})
820
+ return
821
+ output.render_findings(findings, show_id=show_id)
822
+ output.findings_footer(findings)
823
+
824
+
825
+ def audits_finding(
826
+ ctx: typer.Context,
827
+ finding_id: str = typer.Argument(..., metavar="FINDING_ID", help="Finding id."),
828
+ ) -> None:
829
+ """Show one finding in full — a stack of cards: identity, analysis, recommendation, evidence.
830
+
831
+ The identity card carries severity · status · kind · magnitude, how often it's been seen, the
832
+ owning audit and any assignee; `analysis` holds the description and the root-cause hypothesis,
833
+ `recommendation` the suggested fix with its expected impact and effort. Empty sections are
834
+ omitted. Not-found (or a malformed id) → `✗ no finding <id>`, exit 6. Needs `audits:read`.
835
+ With `--json`: the full `AuditFinding` (untouched `evidence`/`evidence_queries`/`scope`).
836
+
837
+ Example:
838
+
839
+ * `fp audits finding <id>`
840
+ """
841
+ state: AppState = ctx.obj
842
+ cctx = require_auth(state)
843
+ try:
844
+ finding = api.get_audit_finding(cctx, finding_id)
845
+ except (ApiError, ForbiddenError, NotFoundError) as exc:
846
+ _fail(state, exc, finding_id=finding_id)
847
+ if state.json:
848
+ output.emit_json(finding)
849
+ return
850
+ output.render_finding_show(finding)
851
+
852
+
853
+ def _triage(
854
+ ctx: typer.Context,
855
+ action: str,
856
+ finding_id: str,
857
+ *,
858
+ reason: Optional[str] = None,
859
+ assigned_to: Optional[str] = None,
860
+ assume_yes: bool = True,
861
+ ) -> None:
862
+ """The shared body behind every triage verb: validate → (confirm) → POST the status action →
863
+ record → render. The suppressing/closing actions (mute/dismiss/resolve) confirm first; ack,
864
+ reopen and assign act immediately (they're calm and reversible)."""
865
+ state: AppState = ctx.obj
866
+ _validate_action(action)
867
+ cctx = require_auth(state)
868
+ if action in _CONFIRMING_ACTIONS and _write.should_prompt(state, assume_yes):
869
+ title = None
870
+ try:
871
+ title = api.get_audit_finding(cctx, finding_id).title
872
+ except NotFoundError as exc:
873
+ _fail(state, exc, finding_id=finding_id)
874
+ except (ApiError, ForbiddenError):
875
+ title = None
876
+ if not output.confirm_finding_action(action, finding_id, title=title):
877
+ if state.json:
878
+ output.emit_json({"cancelled": True})
879
+ else:
880
+ output.cancelled_plain("nothing changed")
881
+ return
882
+ try:
883
+ result = api.set_finding_status(
884
+ cctx, finding_id, action=action, reason=reason, assigned_to=assigned_to
885
+ )
886
+ except (ApiError, ForbiddenError, NotFoundError) as exc:
887
+ _fail(state, exc, finding_id=finding_id)
888
+ _write.record_action(f"finding_{action}", resource="finding", success=True)
889
+ if state.json:
890
+ output.emit_json(result)
891
+ else:
892
+ output.finding_triaged(action, finding_id, assigned_to=assigned_to)
893
+
894
+
895
+ def findings_ack(
896
+ ctx: typer.Context,
897
+ finding_id: str = typer.Argument(..., metavar="FINDING_ID", help="Finding id."),
898
+ reason: Optional[str] = typer.Option(None, "--reason", help="Why you're acknowledging it (kept as durable feedback)."),
899
+ ) -> None:
900
+ """Acknowledge a finding — you've seen it and it stays visible, just deprioritized.
901
+
902
+ The gentlest triage verb: the status doesn't change, but the acknowledgement is recorded as
903
+ durable feedback so later runs rank the pattern lower. No confirm (it isn't destructive).
904
+ Needs `audits:write`. With `--json`: `{id, action, ok}`.
905
+
906
+ Example:
907
+
908
+ * `fp audits ack <finding-id> --reason "known, fix is queued"`
909
+ """
910
+ _triage(ctx, "ack", finding_id, reason=reason)
911
+
912
+
913
+ def findings_mute(
914
+ ctx: typer.Context,
915
+ finding_id: str = typer.Argument(..., metavar="FINDING_ID", help="Finding id."),
916
+ reason: Optional[str] = typer.Option(None, "--reason", help="Why you're muting it (kept as durable feedback)."),
917
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip the confirmation prompt. The prompt only appears on an interactive terminal: under --json, or with stdin redirected, this command proceeds without asking."),
918
+ ) -> None:
919
+ """Mute a finding — stop future runs surfacing this pattern at all.
920
+
921
+ The strongest "don't show me this again" action: the finding goes to `muted` and the
922
+ suppression is durable, so a re-detection of the same fingerprint stays hidden until you
923
+ `reopen` it. Confirms first (`--yes` skips). Needs `audits:write`. With `--json`:
924
+ `{id, action, ok}` (or `{"cancelled": true}` on a declined prompt).
925
+
926
+ Example:
927
+
928
+ * `fp audits mute <finding-id> --reason "expected in staging" --yes`
929
+ """
930
+ _triage(ctx, "mute", finding_id, reason=reason, assume_yes=yes)
931
+
932
+
933
+ def findings_dismiss(
934
+ ctx: typer.Context,
935
+ finding_id: str = typer.Argument(..., metavar="FINDING_ID", help="Finding id."),
936
+ reason: Optional[str] = typer.Option(None, "--reason", help="Why you're dismissing it (kept as durable feedback)."),
937
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip the confirmation prompt. The prompt only appears on an interactive terminal: under --json, or with stdin redirected, this command proceeds without asking."),
938
+ ) -> None:
939
+ """Dismiss a finding — it's not worth acting on.
940
+
941
+ Sets the status to `dismissed` and records durable feedback so the pattern is suppressed in
942
+ later runs (like `mute`, but the label says "judged not a problem" rather than "hide this").
943
+ Confirms first (`--yes` skips). Needs `audits:write`. With `--json`: `{id, action, ok}` (or
944
+ `{"cancelled": true}` on a declined prompt).
945
+
946
+ Example:
947
+
948
+ * `fp audits dismiss <finding-id> --reason "false positive" --yes`
949
+ """
950
+ _triage(ctx, "dismiss", finding_id, reason=reason, assume_yes=yes)
951
+
952
+
953
+ def findings_resolve(
954
+ ctx: typer.Context,
955
+ finding_id: str = typer.Argument(..., metavar="FINDING_ID", help="Finding id."),
956
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip the confirmation prompt. The prompt only appears on an interactive terminal: under --json, or with stdin redirected, this command proceeds without asking."),
957
+ ) -> None:
958
+ """Resolve a finding — you fixed it.
959
+
960
+ Sets the status to `resolved` and leaves **no** suppression behind, deliberately: if the
961
+ pattern genuinely comes back, the next run should raise it as new. Confirms first (`--yes`
962
+ skips). Needs `audits:write`. With `--json`: `{id, action, ok}` (or `{"cancelled": true}` on a
963
+ declined prompt).
964
+
965
+ Example:
966
+
967
+ * `fp audits resolve <finding-id> --yes`
968
+ """
969
+ _triage(ctx, "resolve", finding_id, assume_yes=yes)
970
+
971
+
972
+ def findings_reopen(
973
+ ctx: typer.Context,
974
+ finding_id: str = typer.Argument(..., metavar="FINDING_ID", help="Finding id."),
975
+ ) -> None:
976
+ """Re-open a finding — put it back in the live queue.
977
+
978
+ Sets the status back to `open` **and clears any mute/dismiss suppression**, so the pattern can
979
+ rank and resurface normally. The undo for `mute`/`dismiss`/`resolve`. No confirm (it only makes
980
+ things more visible). Needs `audits:write`. With `--json`: `{id, action, ok}`.
981
+
982
+ Example:
983
+
984
+ * `fp audits reopen <finding-id>`
985
+ """
986
+ _triage(ctx, "reopen", finding_id)
987
+
988
+
989
+ def findings_assign(
990
+ ctx: typer.Context,
991
+ finding_id: str = typer.Argument(..., metavar="FINDING_ID", help="Finding id."),
992
+ assignee: str = typer.Option(..., "--to", help="Email of the person who owns this finding."),
993
+ ) -> None:
994
+ """Assign a finding to someone (sets its owner; the status is untouched).
995
+
996
+ `--to` is required — the server rejects an assign without one. Re-running it reassigns.
997
+ No confirm (it isn't destructive). Needs `audits:write`. With `--json`: `{id, action, ok}`.
998
+
999
+ Example:
1000
+
1001
+ * `fp audits assign <finding-id> --to alice@example.com`
1002
+ """
1003
+ _triage(ctx, "assign", finding_id, assigned_to=assignee)
1004
+
1005
+
1006
+ _AUDITS_GROUP_HELP = """Schedule audits over your agent activity and triage the findings they produce.
1007
+
1008
+ An **audit** runs on a schedule and sweeps a window of activity; what it produces are **findings** —
1009
+ recurring patterns carried across runs. Audits are referenced by **name**, findings by **id**.
1010
+
1011
+ **Subcommands:** `list` · `show` · `create` · `edit` · `delete` · `run` · `runs` · `findings` ·
1012
+ `finding` · `ack` · `mute` · `dismiss` · `resolve` · `reopen` · `assign`
1013
+
1014
+ **Examples:**
1015
+
1016
+ * `fp audits list` — all audits, newest first
1017
+ * `fp audits show nightly-prod` — one audit's schedule / scope / analysis / channels
1018
+ * `fp audits create nightly-prod --scope '{"environments":["prod"]}'`
1019
+ * `fp audits run nightly-prod` — queue a run now, ahead of schedule
1020
+ * `fp audits findings --status open` — the live triage queue, highest priority first
1021
+ * `fp audits finding <id>` — one finding in full, with its recommendation
1022
+ * `fp audits resolve <id>` — you fixed it · `mute` / `dismiss` to suppress · `reopen` to undo
1023
+ """
1024
+
1025
+
1026
+ def register(app: typer.Typer) -> None:
1027
+ audits_app = typer.Typer(
1028
+ no_args_is_help=True,
1029
+ rich_markup_mode="markdown",
1030
+ context_settings={"help_option_names": ["-h", "--help"]},
1031
+ help=_AUDITS_GROUP_HELP,
1032
+ )
1033
+ audits_app.command("list", epilog=GLOBALS_EPILOG)(audits_list)
1034
+ audits_app.command("show", epilog=GLOBALS_EPILOG)(audits_show)
1035
+ audits_app.command("create", epilog=GLOBALS_EPILOG)(audits_create)
1036
+ audits_app.command("edit", epilog=GLOBALS_EPILOG)(audits_edit)
1037
+ audits_app.command("delete", epilog=GLOBALS_EPILOG)(audits_delete)
1038
+ audits_app.command("run", epilog=GLOBALS_EPILOG)(audits_run)
1039
+ audits_app.command("runs", epilog=GLOBALS_EPILOG)(audits_runs)
1040
+ # Hyphenated rather than a nested `context` sub-group: the group's help is a
1041
+ # hand-maintained table (output.py `_TOP_LEVEL_GROUPS`), and a third level
1042
+ # would not render in it.
1043
+ audits_app.command("context-show", epilog=GLOBALS_EPILOG)(audits_context_show)
1044
+ audits_app.command("context-set", epilog=GLOBALS_EPILOG)(audits_context_set)
1045
+ audits_app.command("context-refresh", epilog=GLOBALS_EPILOG)(audits_context_refresh)
1046
+ audits_app.command("findings", epilog=GLOBALS_EPILOG)(audits_findings)
1047
+ audits_app.command("finding", epilog=GLOBALS_EPILOG)(audits_finding)
1048
+ audits_app.command("ack", epilog=GLOBALS_EPILOG)(findings_ack)
1049
+ audits_app.command("mute", epilog=GLOBALS_EPILOG)(findings_mute)
1050
+ audits_app.command("dismiss", epilog=GLOBALS_EPILOG)(findings_dismiss)
1051
+ audits_app.command("resolve", epilog=GLOBALS_EPILOG)(findings_resolve)
1052
+ audits_app.command("reopen", epilog=GLOBALS_EPILOG)(findings_reopen)
1053
+ audits_app.command("assign", epilog=GLOBALS_EPILOG)(findings_assign)
1054
+ app.add_typer(audits_app, name="audits")