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