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.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +6 -6
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js +2 -2
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js +2 -2
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/audit/invite/route.js +3 -2
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js +5 -4
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js +2 -2
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js +4 -3
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js +4 -4
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js +4 -4
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js +2 -2
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js +2 -2
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +66 -22
- package/.next/standalone/.next/server/app/policies/page.js +5 -4
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js +2 -2
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js +2 -2
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
- package/.next/standalone/.next/server/app/settings/page.js +1 -1
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/{[externals]__1qzqcio._.js → [externals]__0e32-7c._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[externals]__1s462yl._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__01eqny5._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_0xu5z._.js +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1p8poxd._.js → [root-of-the-server]__0igxt32._.js} +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0neqlu-._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0t9oqnn._.js +3 -0
- package/.next/standalone/.next/server/chunks/_0bs-v8p._.js +3 -0
- package/.next/standalone/.next/server/chunks/_0tovk6q._.js +3 -0
- package/.next/standalone/.next/server/chunks/_0trp3yc._.js +3 -0
- package/.next/standalone/.next/server/chunks/_185gyk9._.js +3 -0
- package/.next/standalone/.next/server/chunks/_1ek68ln._.js +5 -6
- package/.next/standalone/.next/server/chunks/lib_telemetry-id_ts_1k8gj9o._.js +3 -0
- 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
- 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
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/src_hooks_0xbw3bu._.js +12 -0
- package/.next/standalone/.next/server/chunks/src_hooks_fp-home_ts_09kv0bn._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0spkm68._.js → [root-of-the-server]__07gm3zl._.js} +21 -20
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +6 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__07tbbf3._.js → [root-of-the-server]__0zig5sv._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__192nhvh._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{_0lvyuv4._.js → _00e9tnf._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_0u5zauv._.js → _12l4a_k._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +13 -13
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_08o7bqo._.js +10 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/middleware.js +1 -1
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +71 -27
- package/.next/standalone/.next/static/chunks/{37fcpc36x9gpe.js → 0eqky-7n7dgdm.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1ide1wl2f7zi1.js → 0sa9v5mhc8861.js} +13 -13
- package/.next/standalone/.next/static/chunks/1mfzsja6ftim5.css +2 -0
- package/.next/standalone/.next/static/chunks/{3k7mxbp9hiom0.js → 1nuqytbta44af.js} +1 -1
- package/.next/standalone/.next/static/chunks/1sl2xmn9inx3y.js +6 -0
- package/.next/standalone/.next/static/chunks/{2_2lhuojq2xop.js → 2aawsjld9ied_.js} +1 -1
- package/.next/standalone/.next/static/chunks/2gn7ut4svofm2.js +1 -0
- package/.next/standalone/.next/static/chunks/{31s1smp-g7gks.js → 2hs7ycvsav7v2.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0k5050ezjvv49.js → 2i2yt3hzjredm.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3c808qrfcq0im.js → 35xg6retpe6rp.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2zpp1we2jv6tb.js → 3y54nw0i7dsjl.js} +1 -1
- package/.next/standalone/SECURITY.md +5 -4
- package/.next/standalone/app/actions/get-hooks-config.ts +108 -17
- package/.next/standalone/app/actions/pack-actions.ts +153 -0
- package/.next/standalone/app/audit/_components/run-progress.tsx +1 -1
- package/.next/standalone/app/audit/_components/share-templates.ts +13 -13
- package/.next/standalone/app/policies/hooks-client.tsx +320 -14
- package/.next/standalone/components/navbar.tsx +2 -28
- package/.next/standalone/components/reach-developers.tsx +8 -1
- package/.next/standalone/docs-old/agenteye/python-sdk-skill.mdx +1 -1
- package/.next/standalone/docs-old/agenteye/python-sdk.mdx +59 -35
- package/.next/standalone/docs-old/i18n/README.ar.md +1 -0
- package/.next/standalone/docs-old/i18n/README.de.md +1 -0
- package/.next/standalone/docs-old/i18n/README.es.md +1 -0
- package/.next/standalone/docs-old/i18n/README.fr.md +1 -0
- package/.next/standalone/docs-old/i18n/README.he.md +1 -0
- package/.next/standalone/docs-old/i18n/README.hi.md +1 -0
- package/.next/standalone/docs-old/i18n/README.it.md +1 -0
- package/.next/standalone/docs-old/i18n/README.ja.md +1 -0
- package/.next/standalone/docs-old/i18n/README.ko.md +1 -0
- package/.next/standalone/docs-old/i18n/README.pt-br.md +1 -0
- package/.next/standalone/docs-old/i18n/README.ru.md +1 -0
- package/.next/standalone/docs-old/i18n/README.tr.md +1 -0
- package/.next/standalone/docs-old/i18n/README.vi.md +1 -0
- package/.next/standalone/docs-old/i18n/README.zh.md +1 -0
- package/.next/standalone/fp-cloud-cli/CHANGELOG.md +431 -0
- package/.next/standalone/fp-cloud-cli/LICENSE +42 -0
- package/.next/standalone/fp-cloud-cli/MANIFEST.in +10 -0
- package/.next/standalone/fp-cloud-cli/README.md +259 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/__init__.py +10 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/__main__.py +4 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/_click_compat.py +64 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/_context.py +332 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/_version.py +1 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/analytics.py +432 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/analytics_config.py +77 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/analytics_registry.py +83 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/app.py +492 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/auth.py +160 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/client.py +1694 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/__init__.py +0 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/_write.py +214 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/agent_cmds.py +407 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/alerts_cmds.py +445 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/audits_cmds.py +1054 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/auth_cmds.py +512 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/errors_cmds.py +190 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/evals_cmds.py +161 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/events_cmds.py +159 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/fleet_cmds.py +416 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/guardrails_cmds.py +148 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/incidents_cmds.py +472 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/keys_cmds.py +407 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/list_cmds.py +63 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/orgs_cmds.py +319 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/policies_cmds.py +499 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/queries_cmds.py +378 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/sessions_cmds.py +151 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/settings_cmds.py +150 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/usage_cmds.py +35 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/users_cmds.py +404 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/config.py +330 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/dates.py +78 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/enforcement.py +345 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/errors.py +98 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/models.py +891 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/orgs.py +30 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/output.py +6593 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/permissions.py +208 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/policy_check.py +290 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/py.typed +0 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/select.py +322 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/theme.py +53 -0
- package/.next/standalone/fp-cloud-cli/pyproject.toml +106 -0
- package/.next/standalone/fp-cloud-cli/skill/SKILL.md +302 -0
- package/.next/standalone/fp-cloud-cli/skill/agents/openai.yaml +8 -0
- package/.next/standalone/fp-cloud-cli/skill/references/commands.md +254 -0
- package/.next/standalone/fp-cloud-cli/tests/__init__.py +0 -0
- package/.next/standalone/fp-cloud-cli/tests/conftest.py +92 -0
- package/.next/standalone/fp-cloud-cli/tests/test_alerting.py +644 -0
- package/.next/standalone/fp-cloud-cli/tests/test_analytics.py +385 -0
- package/.next/standalone/fp-cloud-cli/tests/test_audits.py +950 -0
- package/.next/standalone/fp-cloud-cli/tests/test_auth.py +98 -0
- package/.next/standalone/fp-cloud-cli/tests/test_auth_mode.py +272 -0
- package/.next/standalone/fp-cloud-cli/tests/test_click_compat.py +118 -0
- package/.next/standalone/fp-cloud-cli/tests/test_client.py +299 -0
- package/.next/standalone/fp-cloud-cli/tests/test_commands.py +814 -0
- package/.next/standalone/fp-cloud-cli/tests/test_config.py +99 -0
- package/.next/standalone/fp-cloud-cli/tests/test_dashboards_agent.py +340 -0
- package/.next/standalone/fp-cloud-cli/tests/test_dates.py +76 -0
- package/.next/standalone/fp-cloud-cli/tests/test_enforcement_logic.py +481 -0
- package/.next/standalone/fp-cloud-cli/tests/test_facets.py +170 -0
- package/.next/standalone/fp-cloud-cli/tests/test_failproofai_home.py +722 -0
- package/.next/standalone/fp-cloud-cli/tests/test_fp_home_contract.py +198 -0
- package/.next/standalone/fp-cloud-cli/tests/test_hardening.py +186 -0
- package/.next/standalone/fp-cloud-cli/tests/test_help_table_coverage.py +127 -0
- package/.next/standalone/fp-cloud-cli/tests/test_keys_queries.py +496 -0
- package/.next/standalone/fp-cloud-cli/tests/test_list.py +59 -0
- package/.next/standalone/fp-cloud-cli/tests/test_multivalue.py +183 -0
- package/.next/standalone/fp-cloud-cli/tests/test_no_customer_identifiers.py +168 -0
- package/.next/standalone/fp-cloud-cli/tests/test_operator.py +492 -0
- package/.next/standalone/fp-cloud-cli/tests/test_orgs.py +692 -0
- package/.next/standalone/fp-cloud-cli/tests/test_output.py +1732 -0
- package/.next/standalone/fp-cloud-cli/tests/test_policy_check.py +191 -0
- package/.next/standalone/fp-cloud-cli/tests/test_readme_matches_reality.py +125 -0
- package/.next/standalone/fp-cloud-cli/tests/test_review_fixes.py +161 -0
- package/.next/standalone/fp-cloud-cli/tests/test_telemetry_completeness.py +90 -0
- package/.next/standalone/fp-cloud-cli/tests/test_usage.py +115 -0
- package/.next/standalone/fp-cloud-cli/tests/test_v1_origin_diagnostic.py +70 -0
- package/.next/standalone/fp-cloud-cli/tests/test_v1_routing.py +214 -0
- package/.next/standalone/fp-cloud-cli/tests/test_whoami.py +104 -0
- package/.next/standalone/fp-cloud-cli/uv.lock +552 -0
- package/.next/standalone/package.json +29 -22
- package/.next/standalone/sdk/python/CHANGELOG.md +70 -0
- package/.next/standalone/sdk/python/LICENSE +42 -0
- package/.next/standalone/sdk/python/MANIFEST.in +21 -0
- package/.next/standalone/sdk/python/README.md +466 -0
- package/.next/standalone/sdk/python/docs/README.md +249 -0
- package/.next/standalone/sdk/python/docs/_shared/README.md +43 -0
- package/.next/standalone/sdk/python/docs/_shared/__init__.py +195 -0
- package/.next/standalone/sdk/python/docs/crewai/README.md +328 -0
- package/.next/standalone/sdk/python/docs/crewai/examples/quickstart.py +61 -0
- package/.next/standalone/sdk/python/docs/crewai/examples/research_crew.py +92 -0
- package/.next/standalone/sdk/python/docs/langgraph/README.md +481 -0
- package/.next/standalone/sdk/python/docs/langgraph/examples/quickstart.py +51 -0
- package/.next/standalone/sdk/python/docs/langgraph/examples/research_agent.py +86 -0
- package/.next/standalone/sdk/python/docs/langgraph/examples/supervisor_handoff.py +83 -0
- package/.next/standalone/sdk/python/docs/llama_index/README.md +332 -0
- package/.next/standalone/sdk/python/docs/llama_index/examples/quickstart.py +62 -0
- package/.next/standalone/sdk/python/docs/llama_index/examples/research_agent.py +93 -0
- package/.next/standalone/sdk/python/docs/manual/README.md +491 -0
- package/.next/standalone/sdk/python/docs/manual/examples/quickstart.py +57 -0
- package/.next/standalone/sdk/python/docs/manual/examples/research_agent.py +133 -0
- package/.next/standalone/sdk/python/docs/pydantic_ai/README.md +323 -0
- package/.next/standalone/sdk/python/docs/pydantic_ai/examples/quickstart.py +48 -0
- package/.next/standalone/sdk/python/docs/pydantic_ai/examples/research_agent.py +94 -0
- package/.next/standalone/sdk/python/failproofai_sdk/__init__.py +131 -0
- package/.next/standalone/sdk/python/failproofai_sdk/_context.py +218 -0
- package/.next/standalone/sdk/python/failproofai_sdk/_environment.py +78 -0
- package/.next/standalone/sdk/python/failproofai_sdk/_events.py +752 -0
- package/.next/standalone/sdk/python/failproofai_sdk/_resolver.py +118 -0
- package/.next/standalone/sdk/python/failproofai_sdk/_runtime.py +21 -0
- package/.next/standalone/sdk/python/failproofai_sdk/_schema.py +317 -0
- package/.next/standalone/sdk/python/failproofai_sdk/_scopes.py +422 -0
- package/.next/standalone/sdk/python/failproofai_sdk/_version.py +1 -0
- package/.next/standalone/sdk/python/failproofai_sdk/_writer.py +750 -0
- package/.next/standalone/sdk/python/failproofai_sdk/integrations/__init__.py +274 -0
- package/.next/standalone/sdk/python/failproofai_sdk/integrations/_compat.py +247 -0
- package/.next/standalone/sdk/python/failproofai_sdk/integrations/_core.py +1076 -0
- package/.next/standalone/sdk/python/failproofai_sdk/integrations/crewai.py +1669 -0
- package/.next/standalone/sdk/python/failproofai_sdk/integrations/langchain.py +2151 -0
- package/.next/standalone/sdk/python/failproofai_sdk/integrations/llama_index.py +1588 -0
- package/.next/standalone/sdk/python/failproofai_sdk/integrations/pydantic_ai.py +977 -0
- package/.next/standalone/sdk/python/failproofai_sdk/py.typed +0 -0
- package/.next/standalone/sdk/python/pyproject.toml +92 -0
- package/.next/standalone/sdk/python/skill/SKILL.md +399 -0
- package/.next/standalone/sdk/python/skill/agents/openai.yaml +8 -0
- package/.next/standalone/sdk/python/skill/references/events.md +234 -0
- package/.next/standalone/sdk/python/skill/references/frameworks.md +314 -0
- package/.next/standalone/sdk/python/skill/references/install.md +100 -0
- package/.next/standalone/sdk/python/skill/references/integration.md +352 -0
- package/.next/standalone/sdk/python/tests/__init__.py +0 -0
- package/.next/standalone/sdk/python/tests/conftest.py +169 -0
- package/.next/standalone/sdk/python/tests/integrations/__init__.py +6 -0
- package/.next/standalone/sdk/python/tests/integrations/test_crewai.py +1299 -0
- package/.next/standalone/sdk/python/tests/integrations/test_langchain.py +2338 -0
- package/.next/standalone/sdk/python/tests/integrations/test_llama_index.py +1576 -0
- package/.next/standalone/sdk/python/tests/integrations/test_pydantic_ai.py +1102 -0
- package/.next/standalone/sdk/python/tests/test_context.py +410 -0
- package/.next/standalone/sdk/python/tests/test_docs.py +411 -0
- package/.next/standalone/sdk/python/tests/test_durability.py +1352 -0
- package/.next/standalone/sdk/python/tests/test_encoding.py +571 -0
- package/.next/standalone/sdk/python/tests/test_integrations.py +1127 -0
- package/.next/standalone/sdk/python/tests/test_no_customer_identifiers.py +214 -0
- package/.next/standalone/sdk/python/tests/test_packaging.py +185 -0
- package/.next/standalone/sdk/python/tests/test_resolver_umbrella.py +178 -0
- package/.next/standalone/sdk/python/tests/test_scopes.py +487 -0
- package/.next/standalone/sdk/python/tests/test_sdk.py +923 -0
- package/.next/standalone/sdk/python/tests/test_server_contract.py +650 -0
- package/.next/standalone/sdk/python/tests/test_site_docs.py +371 -0
- package/.next/standalone/sdk/python/tests/test_skill_snippets.py +165 -0
- package/.next/standalone/sdk/python/tests/test_spool_contract.py +287 -0
- package/.next/standalone/sdk/python/tests/test_spool_creation.py +360 -0
- package/.next/standalone/sdk/python/tests/test_wire_format.py +279 -0
- package/.next/standalone/sdk/python/tests/test_zero_dependencies.py +378 -0
- package/.next/standalone/sdk/python/uv.lock +6005 -0
- package/.next/standalone/server.js +1 -1
- package/README.md +77 -21
- package/bin/failproofai.mjs +145 -11
- package/dist/cli.mjs +4482 -2205
- package/dist/index.js +5 -5
- package/dist/worker.mjs +2399 -1978
- package/package.json +29 -22
- package/policy-pack/SHA256SUMS +2 -0
- package/policy-pack/failproofai-pack.json +732 -0
- package/policy-pack/failproofai-pack.mjs +2017 -0
- package/scripts/build-policy-pack.mjs +121 -0
- package/scripts/changelog-section.py +77 -0
- package/scripts/prune-standalone.mjs +3 -0
- package/scripts/python-version.py +232 -0
- package/scripts/translate-docs/cli.ts +21 -0
- package/scripts/translate-docs/mintlify-nav.ts +85 -13
- package/scripts/translate-docs/translator.ts +66 -11
- package/src/audit/cache.ts +40 -3
- package/src/audit/cli.ts +1 -1
- package/src/audit/index.ts +1 -1
- package/src/audit/replay.ts +89 -3
- package/src/audit/report.ts +5 -5
- package/src/audit/schedule-cli.ts +49 -53
- package/src/hooks/builtin-policies.ts +217 -579
- package/src/hooks/cloud-enrollment-cli.ts +66 -27
- package/src/hooks/cloud-managed-policies.ts +6 -1
- package/src/hooks/configure-wizard.ts +1 -1
- package/src/hooks/custom-hooks-loader.ts +89 -6
- package/src/hooks/fp-home.ts +92 -4
- package/src/hooks/fp-reset.ts +26 -2
- package/src/hooks/handler.ts +181 -11
- package/src/hooks/harness-cli.ts +79 -25
- package/src/hooks/hook-activity-store.ts +13 -3
- package/src/hooks/install-prompt.ts +25 -3
- package/src/hooks/manager.ts +418 -172
- package/src/hooks/migrations.ts +2 -2
- package/src/hooks/pack-cli.ts +505 -0
- package/src/hooks/pack-failclosed.ts +189 -0
- package/src/hooks/pack-manifest.ts +344 -0
- package/src/hooks/pack-store.ts +930 -0
- package/src/hooks/policy-catalog.ts +535 -0
- package/src/hooks/policy-evaluator.ts +24 -16
- package/src/hooks/policy-presets.ts +4 -2
- package/src/hooks/policy-registry.ts +8 -2
- package/src/hooks/policy-types.ts +26 -0
- package/src/hooks/session-pause-cli.ts +33 -9
- package/src/hooks/tui.ts +570 -0
- package/src/hooks/types.ts +15 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_usx0j._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_zk340._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0agdw2a._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0d1u_na._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0oy4kxb._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zf034w._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1tvn4wf._.js +0 -3
- package/.next/standalone/.next/server/chunks/_0lxbzdq._.js +0 -3
- package/.next/standalone/.next/server/chunks/_1ger0a2._.js +0 -3
- package/.next/standalone/.next/server/chunks/_1zuiiy3._.js +0 -3
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_1iiwm5e._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0kc8_v5._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0kgy9da._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0mhqk5a._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0pltpn8._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qkcyf-._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0w0x_ok._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1_dfocn._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1bfex62._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1kzapg5._.js +0 -6
- package/.next/standalone/.next/static/chunks/36fw6yk1s6x_f.js +0 -1
- package/.next/standalone/.next/static/chunks/3tzq0260n17ae.js +0 -6
- package/.next/standalone/.next/static/chunks/3wq9x0c-7ld5d.css +0 -2
- /package/.next/standalone/.next/static/{ZklXxyA5ygsWSQz2u_zii → nwbyWT06Ud0XdXOrVJyvS}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{ZklXxyA5ygsWSQz2u_zii → nwbyWT06Ud0XdXOrVJyvS}/_clientMiddlewareManifest.js +0 -0
- /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")
|