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