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,650 @@
|
|
|
1
|
+
"""What the ingest server promotes, and what this SDK must therefore emit.
|
|
2
|
+
|
|
3
|
+
The server pulls a fixed set of keys out of each event's payload and stores them
|
|
4
|
+
as real indexed columns; everything else stays in the opaque `payload` blob.
|
|
5
|
+
Those promoted columns are what every filter, facet and chart in the product
|
|
6
|
+
reads. The extraction is total in the worst way:
|
|
7
|
+
|
|
8
|
+
fn ps(payload, key) -> Option<&str> { payload.get(key).and_then(|v| v.as_str()) }
|
|
9
|
+
|
|
10
|
+
A missing key and a key holding the wrong JSON type are indistinguishable — both
|
|
11
|
+
yield `None`, both store NULL, and the row still ingests with `200 OK`. So an
|
|
12
|
+
event that spells `tool_name` as `toolName`, or sends it as a number, lands in
|
|
13
|
+
storage looking successful and is invisible to every tool-name filter forever.
|
|
14
|
+
|
|
15
|
+
This file pins the SDK's half. `PROMOTED` is frozen here because this repository
|
|
16
|
+
cannot see the server, and `test_promoted_columns_match_the_server` re-derives
|
|
17
|
+
the same list from a real AgentEye checkout when `FP_AGENTEYE_ROOT` points at
|
|
18
|
+
one, so the frozen copy cannot quietly rot.
|
|
19
|
+
"""
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import re
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
import pytest
|
|
26
|
+
|
|
27
|
+
from failproofai_sdk import _context, _events, _resolver, _schema
|
|
28
|
+
from failproofai_sdk._events import EventNamespace
|
|
29
|
+
|
|
30
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
31
|
+
# The promoted-column contract
|
|
32
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
#: Payload keys the server lifts into an indexed column via `ps()` (string).
|
|
35
|
+
PROMOTED_STRING = frozenset(
|
|
36
|
+
{
|
|
37
|
+
"tool_name",
|
|
38
|
+
"tool_call_id",
|
|
39
|
+
"hook_name",
|
|
40
|
+
"hook_id",
|
|
41
|
+
"input_id",
|
|
42
|
+
"pause_id",
|
|
43
|
+
"error_type",
|
|
44
|
+
"model",
|
|
45
|
+
}
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
#: Payload keys the server lifts via `pu32()` (unsigned, 32-bit).
|
|
49
|
+
PROMOTED_NUMERIC = frozenset({"duration_ms", "input_tokens", "output_tokens"})
|
|
50
|
+
|
|
51
|
+
PROMOTED = PROMOTED_STRING | PROMOTED_NUMERIC
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class _Recorder:
|
|
55
|
+
"""Stands in for the writer so a call can be inspected without touching disk."""
|
|
56
|
+
|
|
57
|
+
def __init__(self):
|
|
58
|
+
self.entries = []
|
|
59
|
+
|
|
60
|
+
def submit(self, entry):
|
|
61
|
+
self.entries.append(entry)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _emit_everything():
|
|
65
|
+
"""Call every public event method once, fully populated. Returns the payloads."""
|
|
66
|
+
recorder = _Recorder()
|
|
67
|
+
ns = EventNamespace(recorder)
|
|
68
|
+
|
|
69
|
+
ids = dict(session_id="s", agent_id="a")
|
|
70
|
+
ns.agent_start(**ids, goal="g", parent_id="p")
|
|
71
|
+
ns.agent_end(**ids, outcome="success", summary="s")
|
|
72
|
+
ns.agent_pause(**ids, pause_id="p1", reason="r", user_id="u")
|
|
73
|
+
ns.agent_resume(**ids, pause_id="p1", reason="r", user_id="u")
|
|
74
|
+
ns.tool_use(**ids, tool_name="bash", tool_call_id="tc", input={"cmd": "ls"})
|
|
75
|
+
ns.tool_result(**ids, tool_name="bash", tool_call_id="tc", output="o")
|
|
76
|
+
ns.model_request(**ids, model="m", messages=[], system=None, tools=[])
|
|
77
|
+
ns.model_response(**ids, model="m", stop_reason="end_turn", input_tokens=1, output_tokens=2, content="c", role="assistant")
|
|
78
|
+
ns.error(**ids, error_type="ValueError", message="m", traceback="t")
|
|
79
|
+
ns.hook_triggered(**ids, hook_name="h", hook_id="h1", trigger_event="PreToolUse", input={})
|
|
80
|
+
ns.hook_completed(**ids, hook_name="h", hook_id="h1", outcome="allow", output="o")
|
|
81
|
+
ns.human_wait(**ids, input_id="i1", prompt="p", options=["y"], reason="r")
|
|
82
|
+
ns.human_input(**ids, input_id="i1", response="y")
|
|
83
|
+
ns.human_pause(**ids, reason="r", user_id="u")
|
|
84
|
+
ns.human_interrupt(**ids, reason="r", user_id="u", at_step="1")
|
|
85
|
+
return recorder.entries
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_every_promoted_column_is_emitted_by_at_least_one_event():
|
|
89
|
+
"""A promoted column nothing ever populates is a dead column, silently."""
|
|
90
|
+
emitted = set()
|
|
91
|
+
for payload in _emit_everything():
|
|
92
|
+
emitted |= payload.keys()
|
|
93
|
+
|
|
94
|
+
missing = PROMOTED - emitted
|
|
95
|
+
assert not missing, (
|
|
96
|
+
f"no event emits {sorted(missing)}, so those indexed columns are "
|
|
97
|
+
"always NULL. Either an event method stopped sending the key, or the "
|
|
98
|
+
"server promotes something this SDK never produces."
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_promoted_values_have_the_json_type_the_extractor_accepts():
|
|
103
|
+
"""`ps()` needs a JSON string; `pu32()` needs a non-negative integer.
|
|
104
|
+
|
|
105
|
+
A type mismatch is NOT an error on either side — it is a NULL column next to
|
|
106
|
+
a payload that still visibly contains the value, which is the single most
|
|
107
|
+
confusing shape this pipeline can produce.
|
|
108
|
+
"""
|
|
109
|
+
for payload in _emit_everything():
|
|
110
|
+
for key in PROMOTED_STRING & payload.keys():
|
|
111
|
+
assert isinstance(payload[key], str), (
|
|
112
|
+
f"{payload['type']}.{key} is {type(payload[key]).__name__}; "
|
|
113
|
+
"ps() only reads JSON strings and would store NULL"
|
|
114
|
+
)
|
|
115
|
+
for key in PROMOTED_NUMERIC & payload.keys():
|
|
116
|
+
value = payload[key]
|
|
117
|
+
assert isinstance(value, int) and not isinstance(value, bool), (
|
|
118
|
+
f"{payload['type']}.{key} is {type(value).__name__}; emit a "
|
|
119
|
+
"whole integer so the value survives regardless of how the "
|
|
120
|
+
"server's numeric extractor is implemented"
|
|
121
|
+
)
|
|
122
|
+
assert value >= 0, f"{payload['type']}.{key} is negative; the column is unsigned"
|
|
123
|
+
assert value <= 0xFFFFFFFF, f"{payload['type']}.{key} overflows UInt32"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_promoted_keys_never_collide_with_a_reserved_name():
|
|
127
|
+
"""Reserved names are validated separately; a promoted key must not be one."""
|
|
128
|
+
assert not (PROMOTED & _events._RESERVED)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@pytest.mark.parametrize("event_type", ["tool_result", "hook_completed", "human_input", "agent_resume"])
|
|
132
|
+
def test_unpaired_events_omit_duration_rather_than_sending_zero(event_type):
|
|
133
|
+
"""No start seen means unknown, and unknown must not be recorded as `0ms`.
|
|
134
|
+
|
|
135
|
+
Zero is a real, plottable duration. Emitting it for "we never saw the start"
|
|
136
|
+
poisons every latency percentile with values that were never measured.
|
|
137
|
+
"""
|
|
138
|
+
recorder = _Recorder()
|
|
139
|
+
ns = EventNamespace(recorder)
|
|
140
|
+
ids = dict(session_id="s", agent_id="a")
|
|
141
|
+
|
|
142
|
+
if event_type == "tool_result":
|
|
143
|
+
ns.tool_result(**ids, tool_name="t", tool_call_id="never-started")
|
|
144
|
+
elif event_type == "hook_completed":
|
|
145
|
+
ns.hook_completed(**ids, hook_name="h", hook_id="never-started")
|
|
146
|
+
elif event_type == "human_input":
|
|
147
|
+
ns.human_input(**ids, input_id="never-started")
|
|
148
|
+
else:
|
|
149
|
+
ns.agent_resume(**ids, pause_id="never-started")
|
|
150
|
+
|
|
151
|
+
assert "duration_ms" not in recorder.entries[0]
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
155
|
+
# Reserved names — enforcement is SPLIT across two mechanisms, deliberately
|
|
156
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def test_reserved_set_is_exactly_the_five_ingest_reads_structurally():
|
|
160
|
+
assert _events._RESERVED == {"timestamp", "session_id", "agent_id", "type", "environment"}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@pytest.mark.parametrize("name", ["session_id", "agent_id"])
|
|
164
|
+
def test_identity_kwargs_raise_type_error_not_value_error(name):
|
|
165
|
+
"""These are explicit parameters, so Python rejects the duplicate first.
|
|
166
|
+
|
|
167
|
+
Two exception types for one apparent rule. Documented rather than unified,
|
|
168
|
+
because unifying it means shadowing a real signature parameter to raise a
|
|
169
|
+
nicer error, and that trades a loud failure for a subtler one.
|
|
170
|
+
"""
|
|
171
|
+
ns = EventNamespace(_Recorder())
|
|
172
|
+
kwargs = {"session_id": "s", "agent_id": "a"}
|
|
173
|
+
kwargs[name] = "duplicate"
|
|
174
|
+
with pytest.raises(TypeError):
|
|
175
|
+
ns.agent_start(**kwargs, **{name: "again"})
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@pytest.mark.parametrize("name", ["timestamp", "type", "environment"])
|
|
179
|
+
def test_stamped_fields_raise_value_error_from_the_validator(name):
|
|
180
|
+
"""These are not signature parameters, so they reach `_validate_fields`."""
|
|
181
|
+
ns = EventNamespace(_Recorder())
|
|
182
|
+
with pytest.raises(ValueError, match="Reserved field names"):
|
|
183
|
+
ns.agent_start(session_id="s", agent_id="a", **{name: "x"})
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@pytest.mark.parametrize(
|
|
187
|
+
"call",
|
|
188
|
+
[
|
|
189
|
+
lambda ns: ns.tool_result(session_id="s", agent_id="a", tool_name="t", tool_call_id="c", duration_ms=5),
|
|
190
|
+
lambda ns: ns.hook_completed(session_id="s", agent_id="a", hook_name="h", hook_id="i", duration_ms=5),
|
|
191
|
+
lambda ns: ns.human_input(session_id="s", agent_id="a", input_id="i", duration_ms=5),
|
|
192
|
+
lambda ns: ns.agent_resume(session_id="s", agent_id="a", pause_id="p", duration_ms=5),
|
|
193
|
+
],
|
|
194
|
+
)
|
|
195
|
+
def test_duration_ms_cannot_be_supplied_by_the_caller(call):
|
|
196
|
+
"""It is measured, not reported — a caller-supplied value would be unfalsifiable."""
|
|
197
|
+
ns = EventNamespace(_Recorder())
|
|
198
|
+
with pytest.raises(ValueError, match="auto-computed"):
|
|
199
|
+
call(ns)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def test_a_reserved_name_is_rejected_by_every_event_method():
|
|
203
|
+
"""One method forgetting `_validate_fields` would let a caller rewrite `type`."""
|
|
204
|
+
ns = EventNamespace(_Recorder())
|
|
205
|
+
methods = [m for m in dir(ns) if not m.startswith("_") and callable(getattr(ns, m))]
|
|
206
|
+
assert len(methods) == 15, f"expected 15 event methods, found {len(methods)}: {sorted(methods)}"
|
|
207
|
+
|
|
208
|
+
required = {
|
|
209
|
+
"tool_use": dict(tool_name="t", tool_call_id="c"),
|
|
210
|
+
"tool_result": dict(tool_name="t", tool_call_id="c"),
|
|
211
|
+
"hook_triggered": dict(hook_name="h", hook_id="i"),
|
|
212
|
+
"hook_completed": dict(hook_name="h", hook_id="i"),
|
|
213
|
+
"error": dict(error_type="E", message="m"),
|
|
214
|
+
"human_wait": dict(input_id="i"),
|
|
215
|
+
"human_input": dict(input_id="i"),
|
|
216
|
+
"agent_pause": dict(pause_id="p"),
|
|
217
|
+
"agent_resume": dict(pause_id="p"),
|
|
218
|
+
}
|
|
219
|
+
for method in methods:
|
|
220
|
+
with pytest.raises(ValueError, match="Reserved field names"):
|
|
221
|
+
getattr(ns, method)(
|
|
222
|
+
session_id="s", agent_id="a", **required.get(method, {}), type="hijacked"
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
227
|
+
# The strings the rename must never touch
|
|
228
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
#: Renaming any of these desynchronises the SDK from the daemon that reads its
|
|
231
|
+
#: spool, with no error on either side — events are simply written somewhere
|
|
232
|
+
#: nothing watches. The Python import name and the PyPI distribution name were
|
|
233
|
+
#: renamed to failproofai_sdk / failproofai-sdk; the wire and filesystem
|
|
234
|
+
#: contract deliberately was not, exactly as the `fp` CLI kept `X-AgentEye-Org`
|
|
235
|
+
#: and the `ae_session` cookie through its own rename.
|
|
236
|
+
FROZEN_STRINGS = {
|
|
237
|
+
"AGENTEYE_ENVIRONMENT": "the environment label, read at import time",
|
|
238
|
+
"FAILPROOFAI_HOME": "moves the umbrella root, mirrored in fp-home.ts",
|
|
239
|
+
"custom-agents": "the DEFAULT spool root, mirrored in fp-home.ts and config.rs",
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
#: Deliberately NOT frozen, and each for its own reason.
|
|
243
|
+
#:
|
|
244
|
+
#: ``AGENTEYE_SPOOL_TO_FAILPROOFAI`` was the opt-in that selected the umbrella
|
|
245
|
+
#: root. It also required that directory to already exist, and nothing ever
|
|
246
|
+
#: created it, so the branch never once fired. It is retired rather than frozen:
|
|
247
|
+
#: the umbrella is the default now, so anyone who exported it already has what
|
|
248
|
+
#: they were asking for.
|
|
249
|
+
#:
|
|
250
|
+
#: ``.agenteye`` is no longer a literal this package must contain. It survives
|
|
251
|
+
#: in prose and in `legacy_agenteye_dir()`, but freezing it would make this test
|
|
252
|
+
#: pass on a comment — which is exactly how it passed while the variable above
|
|
253
|
+
#: was being deleted.
|
|
254
|
+
#: ``AGENTEYE_HOME`` was frozen here as "the operator override both daemons
|
|
255
|
+
#: honour". It no longer resolves the spool in THIS package: it could aim the
|
|
256
|
+
#: SDK at any directory from the environment, including as an unintended side
|
|
257
|
+
#: effect of exporting it for `agenteye-collector`, which is the one component
|
|
258
|
+
#: that still reads it. `failproofaid` keeps watching `~/.agenteye/events`, so
|
|
259
|
+
#: nothing already written there is stranded — but this SDK writes to the
|
|
260
|
+
#: umbrella and nothing can redirect it off.
|
|
261
|
+
#:
|
|
262
|
+
#: It is RETIRED rather than dropped from the file, because the name still
|
|
263
|
+
#: appears in `_resolver`'s prose explaining why it went away, and a frozen
|
|
264
|
+
#: substring check would happily pass on that comment.
|
|
265
|
+
RETIRED_STRINGS = ("AGENTEYE_SPOOL_TO_FAILPROOFAI", "AGENTEYE_HOME")
|
|
266
|
+
|
|
267
|
+
PACKAGE_DIR = Path(_resolver.__file__).resolve().parent
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def test_the_spool_contract_strings_are_still_spelled_the_old_way():
|
|
271
|
+
"""A well-meaning rename sweep is the realistic threat here, not a redesign."""
|
|
272
|
+
source = "\n".join(
|
|
273
|
+
p.read_text(encoding="utf-8") for p in sorted(PACKAGE_DIR.glob("*.py"))
|
|
274
|
+
)
|
|
275
|
+
for literal, why in FROZEN_STRINGS.items():
|
|
276
|
+
assert literal in source, (
|
|
277
|
+
f"{literal!r} is gone from the package — {why}. This is a wire and "
|
|
278
|
+
"filesystem contract with a daemon that is released separately, so "
|
|
279
|
+
"renaming it here strands every event this SDK writes. If the "
|
|
280
|
+
"rename is genuinely intended, change the daemon FIRST and keep "
|
|
281
|
+
"reading the old name for at least one release."
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def test_the_retired_opt_in_is_not_read_by_any_module():
|
|
286
|
+
"""Freed, not merely unused — a leftover branch would contradict the docs.
|
|
287
|
+
|
|
288
|
+
Checked over `os.environ` lookups rather than the raw text, because the name
|
|
289
|
+
still appears in `_resolver`'s prose explaining why it went away, and a
|
|
290
|
+
substring check over source is how the frozen-strings test above was passing
|
|
291
|
+
for the wrong reason while the variable was being deleted.
|
|
292
|
+
"""
|
|
293
|
+
import re
|
|
294
|
+
|
|
295
|
+
for path in sorted(PACKAGE_DIR.glob("*.py")):
|
|
296
|
+
source = path.read_text(encoding="utf-8")
|
|
297
|
+
for retired in RETIRED_STRINGS:
|
|
298
|
+
reads = re.findall(rf"environ(?:\.get)?[\(\[]\s*[\"']{retired}", source)
|
|
299
|
+
assert not reads, f"{path.name} still reads the retired {retired}"
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def test_the_spool_layout_is_events_and_failed_under_the_base_dir():
|
|
303
|
+
"""The daemon watches `<base>/events` and quarantines to `<base>/failed`."""
|
|
304
|
+
writer_source = (PACKAGE_DIR / "_writer.py").read_text(encoding="utf-8")
|
|
305
|
+
assert '"events"' in writer_source
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def test_batches_are_published_by_atomic_rename_from_a_tmp_suffix():
|
|
309
|
+
"""`.tmp` -> `.jsonl` via os.replace is what stops a half-written read.
|
|
310
|
+
|
|
311
|
+
The daemon takes any `.jsonl` in the directory as complete. Writing straight
|
|
312
|
+
to the final name would hand it a truncated file whose unparseable lines
|
|
313
|
+
ingest counts as `skipped` — 200 OK, events gone.
|
|
314
|
+
"""
|
|
315
|
+
writer_source = (PACKAGE_DIR / "_writer.py").read_text(encoding="utf-8")
|
|
316
|
+
assert '.tmp' in writer_source
|
|
317
|
+
assert '.jsonl' in writer_source
|
|
318
|
+
assert "os.replace(tmp_path, final_path)" in writer_source, (
|
|
319
|
+
"the atomic publish is gone. shutil.move, Path.rename across devices, or "
|
|
320
|
+
"a plain write to the final name all reintroduce the torn-read window."
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
325
|
+
# Cross-check against a real AgentEye checkout, when one is available
|
|
326
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
327
|
+
|
|
328
|
+
_AGENTEYE_ROOT = os.environ.get("FP_AGENTEYE_ROOT")
|
|
329
|
+
_INGEST_RS = Path(_AGENTEYE_ROOT) / "server" / "src" / "routes" / "ingest.rs" if _AGENTEYE_ROOT else None
|
|
330
|
+
|
|
331
|
+
requires_agenteye_checkout = pytest.mark.skipif(
|
|
332
|
+
_INGEST_RS is None or not _INGEST_RS.is_file(),
|
|
333
|
+
reason="set FP_AGENTEYE_ROOT to an AgentEye checkout to verify against real ingest.rs",
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
@requires_agenteye_checkout
|
|
338
|
+
def test_promoted_columns_match_the_server():
|
|
339
|
+
"""Re-derive PROMOTED from ingest.rs so the frozen copy above cannot rot."""
|
|
340
|
+
source = _INGEST_RS.read_text(encoding="utf-8")
|
|
341
|
+
|
|
342
|
+
actual_strings = set(re.findall(r'ps\(payload_value,\s*"([a-z_]+)"\)', source))
|
|
343
|
+
actual_numeric = set(re.findall(r'pu32\(payload_value,\s*"([a-z_]+)"\)', source))
|
|
344
|
+
|
|
345
|
+
# `model_name` is a documented fallback spelling the server also accepts;
|
|
346
|
+
# this SDK only ever emits `model`, which is the preferred one.
|
|
347
|
+
actual_strings.discard("model_name")
|
|
348
|
+
|
|
349
|
+
assert actual_strings == PROMOTED_STRING, (
|
|
350
|
+
"the server's promoted string columns drifted from this SDK's frozen "
|
|
351
|
+
f"copy. server={sorted(actual_strings)} sdk={sorted(PROMOTED_STRING)}"
|
|
352
|
+
)
|
|
353
|
+
assert PROMOTED_NUMERIC >= actual_numeric, (
|
|
354
|
+
"the server promotes a numeric column this SDK does not know about: "
|
|
355
|
+
f"{sorted(actual_numeric - PROMOTED_NUMERIC)}"
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
360
|
+
# Promoted numeric columns — the type has to be right, not just the key
|
|
361
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
@pytest.mark.parametrize("name", sorted(_events._PROMOTED_NUMERIC))
|
|
365
|
+
@pytest.mark.parametrize("bad", ["many", 12.5, True, -1, 2**32, [1], {"n": 1}])
|
|
366
|
+
def test_a_promoted_numeric_is_refused_rather_than_stored_as_null(name, bad):
|
|
367
|
+
"""`pu32()` returns None on a mismatch, and None is written as NULL at 200 OK.
|
|
368
|
+
|
|
369
|
+
Nothing is logged and nothing is rejected — the row still arrives — so the
|
|
370
|
+
only symptom is a column that is populated for some events and not others.
|
|
371
|
+
`_RESERVED` never covered this: it blocks five structural keys and lets every
|
|
372
|
+
other custom field through untouched, whatever type it carries.
|
|
373
|
+
|
|
374
|
+
Rejecting rather than coercing, because a float is a mistake worth hearing
|
|
375
|
+
about: the server drops it whole rather than rounding it, and rounding it
|
|
376
|
+
here would hide that from the one person able to fix the source.
|
|
377
|
+
"""
|
|
378
|
+
ns = EventNamespace(_Recorder())
|
|
379
|
+
with pytest.raises(ValueError, match=name):
|
|
380
|
+
ns.agent_start(session_id="s", agent_id="a", **{name: bad})
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
@pytest.mark.parametrize("name", ["input_tokens", "output_tokens"])
|
|
384
|
+
def test_model_response_checks_its_own_two_token_counts(name):
|
|
385
|
+
"""They are named parameters, so they never reach `_validate_fields`.
|
|
386
|
+
|
|
387
|
+
They are also the likeliest of the three to arrive wrong — a caller reading
|
|
388
|
+
them straight off a provider's usage object gets whatever that object holds.
|
|
389
|
+
"""
|
|
390
|
+
ns = EventNamespace(_Recorder())
|
|
391
|
+
with pytest.raises(ValueError, match=name):
|
|
392
|
+
ns.model_response(session_id="s", agent_id="a", model="m", **{name: "1024"})
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
@pytest.mark.parametrize("name", sorted(_events._PROMOTED_NUMERIC))
|
|
396
|
+
def test_a_valid_promoted_numeric_still_goes_through_untouched(name):
|
|
397
|
+
recorder = _Recorder()
|
|
398
|
+
EventNamespace(recorder).agent_start(session_id="s", agent_id="a", **{name: 4096})
|
|
399
|
+
assert recorder.entries[0][name] == 4096
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
@pytest.mark.parametrize("name", sorted(_events._PROMOTED_NUMERIC))
|
|
403
|
+
def test_the_boundary_values_of_a_u32_are_accepted(name):
|
|
404
|
+
"""0 and 2**32 - 1 are valid, and an off-by-one here silently rejects real data."""
|
|
405
|
+
recorder = _Recorder()
|
|
406
|
+
ns = EventNamespace(recorder)
|
|
407
|
+
ns.agent_start(session_id="s", agent_id="a", **{name: 0})
|
|
408
|
+
ns.agent_start(session_id="s", agent_id="a", **{name: 2**32 - 1})
|
|
409
|
+
assert [e[name] for e in recorder.entries] == [0, 2**32 - 1]
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def test_none_is_not_treated_as_a_bad_promoted_numeric():
|
|
413
|
+
"""`model_response` passes its optionals straight through as None."""
|
|
414
|
+
recorder = _Recorder()
|
|
415
|
+
EventNamespace(recorder).model_response(
|
|
416
|
+
session_id="s", agent_id="a", model="m", input_tokens=None, output_tokens=None
|
|
417
|
+
)
|
|
418
|
+
assert "input_tokens" not in recorder.entries[0]
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def test_the_promoted_numeric_set_matches_the_one_ingest_lifts():
|
|
422
|
+
"""Two hand-maintained lists, one in the SDK and one in this file's header.
|
|
423
|
+
|
|
424
|
+
If they drift, the validator stops covering a column that ingest still
|
|
425
|
+
promotes, and this file's own assertions stop describing the server.
|
|
426
|
+
"""
|
|
427
|
+
assert _events._PROMOTED_NUMERIC == PROMOTED_NUMERIC
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
431
|
+
# The MEASURED duration is bound by the same u32 range a caller is held to
|
|
432
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
433
|
+
|
|
434
|
+
#: The four events that pair with an earlier one and compute `duration_ms`.
|
|
435
|
+
#: Each takes a start call and an end call against the same id.
|
|
436
|
+
PAIRED = {
|
|
437
|
+
"tool_result": (
|
|
438
|
+
lambda ns: ns.tool_use(session_id="s", agent_id="a", tool_name="t", tool_call_id="x"),
|
|
439
|
+
lambda ns: ns.tool_result(session_id="s", agent_id="a", tool_name="t", tool_call_id="x"),
|
|
440
|
+
),
|
|
441
|
+
"hook_completed": (
|
|
442
|
+
lambda ns: ns.hook_triggered(session_id="s", agent_id="a", hook_name="h", hook_id="x"),
|
|
443
|
+
lambda ns: ns.hook_completed(session_id="s", agent_id="a", hook_name="h", hook_id="x"),
|
|
444
|
+
),
|
|
445
|
+
"human_input": (
|
|
446
|
+
lambda ns: ns.human_wait(session_id="s", agent_id="a", input_id="x"),
|
|
447
|
+
lambda ns: ns.human_input(session_id="s", agent_id="a", input_id="x"),
|
|
448
|
+
),
|
|
449
|
+
"agent_resume": (
|
|
450
|
+
lambda ns: ns.agent_pause(session_id="s", agent_id="a", pause_id="x"),
|
|
451
|
+
lambda ns: ns.agent_resume(session_id="s", agent_id="a", pause_id="x"),
|
|
452
|
+
),
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def _emit_pair_with_gap(event_type, gap_seconds, monkeypatch):
|
|
457
|
+
"""Run one start/end pair with a controlled interval between them."""
|
|
458
|
+
import datetime as _dt
|
|
459
|
+
|
|
460
|
+
start_call, end_call = PAIRED[event_type]
|
|
461
|
+
recorder = _Recorder()
|
|
462
|
+
ns = EventNamespace(recorder)
|
|
463
|
+
|
|
464
|
+
base = _dt.datetime(2026, 1, 1, tzinfo=_dt.timezone.utc)
|
|
465
|
+
clock = {"now": base}
|
|
466
|
+
monkeypatch.setattr(EventNamespace, "_now", staticmethod(lambda: clock["now"]))
|
|
467
|
+
|
|
468
|
+
start_call(ns)
|
|
469
|
+
clock["now"] = base + _dt.timedelta(seconds=gap_seconds)
|
|
470
|
+
recorder.entries.clear()
|
|
471
|
+
end_call(ns)
|
|
472
|
+
return recorder.entries[0]
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
@pytest.mark.parametrize("event_type", sorted(PAIRED))
|
|
476
|
+
def test_a_pair_spanning_more_than_u32_milliseconds_omits_the_duration(event_type, monkeypatch):
|
|
477
|
+
"""~49.7 days is an ordinary lifetime for these pairs, not an abuse.
|
|
478
|
+
|
|
479
|
+
A `human_wait` answered after a long weekend, an `agent_pause` resumed a
|
|
480
|
+
month later. Over the range, `pu32()` stores NULL at 200 OK — so an
|
|
481
|
+
unbounded value is not an error anywhere, just an empty column.
|
|
482
|
+
"""
|
|
483
|
+
over = (2**32 / 1000) + 60 # comfortably past 2**32 ms
|
|
484
|
+
payload = _emit_pair_with_gap(event_type, over, monkeypatch)
|
|
485
|
+
assert "duration_ms" not in payload, (
|
|
486
|
+
f"{event_type} emitted {payload.get('duration_ms')}, which the server stores as NULL"
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
@pytest.mark.parametrize("event_type", sorted(PAIRED))
|
|
491
|
+
def test_a_backwards_clock_omits_the_duration_rather_than_going_negative(event_type, monkeypatch):
|
|
492
|
+
"""`datetime.now()` is wall-clock, so an NTP step back yields a negative gap.
|
|
493
|
+
|
|
494
|
+
`round()` keeps the sign, and a negative into an unsigned column is the same
|
|
495
|
+
silent NULL as an oversized one.
|
|
496
|
+
"""
|
|
497
|
+
payload = _emit_pair_with_gap(event_type, -5, monkeypatch)
|
|
498
|
+
assert "duration_ms" not in payload, f"{event_type} emitted a negative duration"
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
@pytest.mark.parametrize("event_type", sorted(PAIRED))
|
|
502
|
+
def test_an_ordinary_gap_still_produces_a_duration(event_type, monkeypatch):
|
|
503
|
+
"""The bound must not swallow the normal case it exists to protect."""
|
|
504
|
+
payload = _emit_pair_with_gap(event_type, 1.5, monkeypatch)
|
|
505
|
+
assert payload["duration_ms"] == 1500
|
|
506
|
+
assert isinstance(payload["duration_ms"], int)
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
@pytest.mark.parametrize("event_type", sorted(PAIRED))
|
|
510
|
+
def test_the_upper_boundary_itself_is_kept(event_type, monkeypatch):
|
|
511
|
+
"""Exactly 2**32 - 1 ms is representable; an off-by-one here drops real data."""
|
|
512
|
+
payload = _emit_pair_with_gap(event_type, (2**32 - 1) / 1000, monkeypatch)
|
|
513
|
+
assert payload["duration_ms"] == 2**32 - 1
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def test_the_measured_and_the_caller_supplied_paths_enforce_the_same_range():
|
|
517
|
+
"""One range, two entry points. They drifted once already.
|
|
518
|
+
|
|
519
|
+
`_validate_promoted_numeric` refused a caller anything outside 0..2**32-1
|
|
520
|
+
while the SDK's own computation was unbounded — the same field, held to two
|
|
521
|
+
different standards depending on who produced it.
|
|
522
|
+
"""
|
|
523
|
+
import datetime as _dt
|
|
524
|
+
|
|
525
|
+
base = _dt.datetime(2026, 1, 1, tzinfo=_dt.timezone.utc)
|
|
526
|
+
over = base + _dt.timedelta(milliseconds=2**32)
|
|
527
|
+
assert _events._measured_duration_ms(base, over) is None
|
|
528
|
+
with pytest.raises(ValueError):
|
|
529
|
+
_events._validate_promoted_numeric("duration_ms", 2**32)
|
|
530
|
+
|
|
531
|
+
ok = base + _dt.timedelta(milliseconds=2**32 - 1)
|
|
532
|
+
assert _events._measured_duration_ms(base, ok) == 2**32 - 1
|
|
533
|
+
_events._validate_promoted_numeric("duration_ms", 2**32 - 1)
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
537
|
+
# session_id / agent_id — on every event, and skipped silently when wrong
|
|
538
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
539
|
+
|
|
540
|
+
#: Every public event method, with the arguments it needs besides the two ids.
|
|
541
|
+
ALL_METHODS = [
|
|
542
|
+
("agent_start", {}), ("agent_end", {}),
|
|
543
|
+
("agent_pause", {"pause_id": "p"}), ("agent_resume", {"pause_id": "p"}),
|
|
544
|
+
("tool_use", {"tool_name": "t", "tool_call_id": "c"}),
|
|
545
|
+
("tool_result", {"tool_name": "t", "tool_call_id": "c"}),
|
|
546
|
+
("model_request", {}), ("model_response", {}),
|
|
547
|
+
("hook_triggered", {"hook_name": "h", "hook_id": "i"}),
|
|
548
|
+
("hook_completed", {"hook_name": "h", "hook_id": "i"}),
|
|
549
|
+
("error", {"error_type": "E", "message": "m"}),
|
|
550
|
+
("human_wait", {"input_id": "i"}), ("human_input", {"input_id": "i"}),
|
|
551
|
+
("human_pause", {}), ("human_interrupt", {}),
|
|
552
|
+
]
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def test_the_method_list_here_covers_every_public_event():
|
|
556
|
+
"""Or a method added later silently skips the check below."""
|
|
557
|
+
public = {
|
|
558
|
+
n for n in dir(EventNamespace)
|
|
559
|
+
if not n.startswith("_") and callable(getattr(EventNamespace, n))
|
|
560
|
+
}
|
|
561
|
+
assert {m for m, _ in ALL_METHODS} == public
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
@pytest.mark.parametrize("method,extra", ALL_METHODS, ids=[m for m, _ in ALL_METHODS])
|
|
565
|
+
@pytest.mark.parametrize("bad", [None, 123, {"x": 1}, ["a"], b"bytes"], ids=lambda v: type(v).__name__)
|
|
566
|
+
def test_a_non_string_session_id_is_refused_on_every_event(method, extra, bad):
|
|
567
|
+
"""Ingest SKIPS these and answers 200 — `{"accepted":0,"skipped":1}`.
|
|
568
|
+
|
|
569
|
+
Verified against the live server. Nothing upstream learns: the SDK reports
|
|
570
|
+
success, the collector deletes the batch, and the event is gone. `None` is
|
|
571
|
+
the realistic way in — an uninitialised variable, a lookup that missed.
|
|
572
|
+
"""
|
|
573
|
+
ns = EventNamespace(_Recorder())
|
|
574
|
+
with pytest.raises(TypeError, match="session_id"):
|
|
575
|
+
getattr(ns, method)(session_id=bad, agent_id="a", **extra)
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
@pytest.mark.parametrize("method,extra", ALL_METHODS, ids=[m for m, _ in ALL_METHODS])
|
|
579
|
+
def test_a_non_string_agent_id_is_refused_on_every_event(method, extra):
|
|
580
|
+
"""`None` is no longer invalid — it means "resolve from scope" (below).
|
|
581
|
+
|
|
582
|
+
`TypeError`, because the wrong TYPE was supplied — and because omitting a
|
|
583
|
+
required keyword argument was always a TypeError, so code catching one keeps
|
|
584
|
+
working now that identity is optional.
|
|
585
|
+
"""
|
|
586
|
+
ns = EventNamespace(_Recorder())
|
|
587
|
+
with pytest.raises(TypeError, match="agent_id"):
|
|
588
|
+
getattr(ns, method)(session_id="s", agent_id=12345, **extra)
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
@pytest.mark.parametrize("method,extra", ALL_METHODS, ids=[m for m, _ in ALL_METHODS])
|
|
592
|
+
def test_an_omitted_agent_id_resolves_rather_than_raising(method, extra):
|
|
593
|
+
"""Omitting it is the ergonomic path the scopes exist for, not an error.
|
|
594
|
+
|
|
595
|
+
With no `agent()` scope open it lands on `DEFAULT_AGENT_ID` — the convention
|
|
596
|
+
the skill already teaches — rather than raising. Inventing a *session* that
|
|
597
|
+
way would scatter one run across as many sessions as it has emit sites, so
|
|
598
|
+
only the agent has a default.
|
|
599
|
+
"""
|
|
600
|
+
recorder = _Recorder()
|
|
601
|
+
getattr(EventNamespace(recorder), method)(session_id="s", **extra)
|
|
602
|
+
assert recorder.entries[0]["agent_id"] == _context.DEFAULT_AGENT_ID
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def test_an_unresolvable_session_id_raises_rather_than_emitting():
|
|
606
|
+
"""Nothing passed and nothing bound must be loud.
|
|
607
|
+
|
|
608
|
+
Ingest skips an event whose `session_id` is not a JSON string and answers
|
|
609
|
+
`200 OK` with `{"accepted":0,"skipped":1}`, so emitting here would lose the
|
|
610
|
+
event with no error on either side — the exact failure the validation exists
|
|
611
|
+
to prevent, reached through the new ergonomic path instead of a bad argument.
|
|
612
|
+
"""
|
|
613
|
+
ns = EventNamespace(_Recorder())
|
|
614
|
+
with pytest.raises(TypeError, match="session_id is required"):
|
|
615
|
+
ns.agent_start()
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def test_an_ambient_scope_satisfies_both_ids():
|
|
619
|
+
import failproofai_sdk
|
|
620
|
+
|
|
621
|
+
recorder = _Recorder()
|
|
622
|
+
ns = EventNamespace(recorder)
|
|
623
|
+
with failproofai_sdk.session("sess-x"):
|
|
624
|
+
with failproofai_sdk.agent("planner"):
|
|
625
|
+
ns.agent_start(goal="from ambient scope")
|
|
626
|
+
entry = recorder.entries[-1]
|
|
627
|
+
assert entry["session_id"] == "sess-x"
|
|
628
|
+
assert entry["agent_id"] == "planner"
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
@pytest.mark.parametrize("blank", ["", " ", "\t\n"], ids=["empty", "spaces", "whitespace"])
|
|
632
|
+
def test_a_blank_identity_is_refused_although_the_server_accepts_it(blank):
|
|
633
|
+
"""The worse of the two outcomes, which is why it is refused too.
|
|
634
|
+
|
|
635
|
+
A skipped event is at least absent. A blank id is ACCEPTED by the server, so
|
|
636
|
+
every event sent that way lands and is silently grouped under one id — the
|
|
637
|
+
data looks present and is quietly merged across unrelated runs.
|
|
638
|
+
"""
|
|
639
|
+
ns = EventNamespace(_Recorder())
|
|
640
|
+
with pytest.raises(ValueError, match="empty"):
|
|
641
|
+
ns.agent_start(session_id=blank, agent_id="a")
|
|
642
|
+
with pytest.raises(ValueError, match="empty"):
|
|
643
|
+
ns.agent_start(session_id="s", agent_id=blank)
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def test_ordinary_identities_are_untouched():
|
|
647
|
+
recorder = _Recorder()
|
|
648
|
+
EventNamespace(recorder).agent_start(session_id="run-001", agent_id="planner")
|
|
649
|
+
assert recorder.entries[0]["session_id"] == "run-001"
|
|
650
|
+
assert recorder.entries[0]["agent_id"] == "planner"
|