failproofai 1.0.3 → 1.0.4-beta.2
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 +5 -5
- package/.next/standalone/.next/prerender-manifest.json +5 -5
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +2 -2
- 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.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/build-manifest.json +2 -2
- 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.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.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/invite/route_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js +4 -4
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/route_client-reference-manifest.js +1 -1
- 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-request/route_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/audit/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +37 -4
- 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/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- 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/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
- 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/build-manifest.json +2 -2
- 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.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/build-manifest.json +2 -2
- 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.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/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- 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/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
- 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]__0-9ti7z._.js → [externals]__1lh7m5d._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{[externals]__0u3wrvq._.js → [externals]__1rqkg_y._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1adacul._.js +1 -1
- package/.next/standalone/.next/server/chunks/{_1-akg6w._.js → _08w6xzm._.js} +2 -2
- package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1ek68ln._.js +4 -4
- package/.next/standalone/.next/server/chunks/lib_telemetry_ts_0dctyyw._.js +1 -1
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_01r25oi._.js +2 -2
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +2 -2
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- 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 +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +2 -2
- 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 +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0rgu2r3._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_0-oij9d._.js +23 -0
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0l2hi_d._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0oq1dh6._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1es2j7i._.js +47 -13
- package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -69
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_rerun-button_tsx_0blihgw._.js +3 -0
- 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 +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_13n822a._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +2 -2
- 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-config_ts_04t589g._.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 +5 -5
- 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 +54 -21
- package/.next/standalone/.next/static/chunks/010bv1w6j171t.js +1 -0
- package/.next/standalone/.next/static/chunks/04r6ch8uf_n8m.js +1 -0
- package/.next/standalone/.next/static/chunks/{29-iqnp9v_axt.css → 0o-hh5_turzlz.css} +1 -1
- package/.next/standalone/.next/static/chunks/{40nr6ehj4p3cw.css → 0qmsunv04x4na.css} +1 -1
- package/.next/standalone/.next/static/chunks/{0ollg-nnu3t3e.js → 0wz8yftk18ts2.js} +1 -1
- package/.next/standalone/.next/static/chunks/0zebh1n9jkfbt.js +1 -0
- package/.next/standalone/.next/static/chunks/1eyl7fzkmnkf_.js +1 -0
- package/.next/standalone/.next/static/chunks/1pb1oztsbwcss.js +1 -0
- package/.next/standalone/.next/static/chunks/{1-_zkmnm-jwei.js → 2aquitk72k2op.js} +1 -1
- package/.next/standalone/.next/static/chunks/2bi_1y0a_smt7.js +6 -0
- package/.next/standalone/.next/static/chunks/{42yfkhekx440s.js → 2ej3b8gk5ittu.js} +1 -1
- package/.next/standalone/.next/static/chunks/{354-n7p7labpn.js → 2zafgs90r_leq.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2xm5mj1d21b4n.js → 32spub4wqjem-.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1a6u7l3giejch.js → 3m4upvybtrexd.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2k9f4tyv04809.css → 3uyhmm01g64k4.css} +1 -0
- package/.next/standalone/.next/static/chunks/{turbopack-2x-o099q64i8h.js → turbopack-3s3c-u_u_l0u9.js} +1 -1
- package/.next/standalone/.opencode/plugins/failproofai.mjs +51 -27
- package/.next/standalone/app/actions/get-leaks.ts +100 -0
- package/.next/standalone/app/audit/_components/audit-dashboard.tsx +112 -33
- package/.next/standalone/app/audit/_components/audit-poster.tsx +29 -10
- package/.next/standalone/app/audit/_components/come-back-better-section.tsx +6 -1
- package/.next/standalone/app/audit/_components/empty-state.tsx +4 -1
- package/.next/standalone/app/audit/_components/how-to-improve-section.tsx +19 -5
- package/.next/standalone/app/audit/_components/leak-section.tsx +179 -0
- package/.next/standalone/app/audit/_components/share-templates.ts +110 -44
- package/.next/standalone/app/audit/audit-styles.css +114 -1
- package/.next/standalone/fp-cloud-cli/CHANGELOG.md +26 -3
- package/.next/standalone/fp-cloud-cli/fp_cli/_click_compat.py +31 -8
- package/.next/standalone/fp-cloud-cli/fp_cli/permissions.py +1 -0
- package/.next/standalone/fp-cloud-cli/pyproject.toml +9 -2
- package/.next/standalone/fp-cloud-cli/tests/test_click_compat.py +54 -0
- package/.next/standalone/fp-cloud-cli/uv.lock +9 -12
- package/.next/standalone/lib/auth/api-server-client.ts +28 -0
- package/.next/standalone/lib/claude-sessions.ts +101 -27
- package/.next/standalone/node_modules/@next/env/package.json +1 -1
- package/.next/standalone/node_modules/next/dist/build/swc/index.js +1 -1
- package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo-experimental.runtime.prod.js +1 -1
- package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo.runtime.prod.js +1 -1
- package/.next/standalone/node_modules/next/dist/compiled/next-server/app-route-turbo.runtime.prod.js +1 -1
- package/.next/standalone/node_modules/next/dist/compiled/next-server/pages-turbo.runtime.prod.js +5 -5
- package/.next/standalone/node_modules/next/dist/experimental/testmode/fetch.js +10 -2
- package/.next/standalone/node_modules/next/dist/lib/patch-incorrect-lockfile.js +3 -3
- package/.next/standalone/node_modules/next/dist/lib/typescript/runTypeScriptCli.js +2 -2
- package/.next/standalone/node_modules/next/dist/lib/verify-typescript-setup.js +3 -1
- package/.next/standalone/node_modules/next/dist/server/config.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-turbopack.js +2 -2
- package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-webpack.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/lib/app-info-log.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/lib/incremental-cache/file-system-cache.js +17 -4
- package/.next/standalone/node_modules/next/dist/server/lib/start-server.js +1 -1
- package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/escape-path-delimiters.js +1 -1
- package/.next/standalone/node_modules/next/dist/telemetry/anonymous-meta.js +1 -1
- package/.next/standalone/node_modules/next/dist/telemetry/events/swc-load-failure.js +1 -1
- package/.next/standalone/node_modules/next/dist/telemetry/events/version.js +2 -2
- package/.next/standalone/node_modules/next/package.json +11 -11
- package/.next/standalone/package.json +14 -13
- package/.next/standalone/sdk/python/CHANGELOG.md +155 -0
- package/.next/standalone/sdk/python/README.md +8 -0
- package/.next/standalone/sdk/python/examples/evaluator_worker.py +121 -0
- package/.next/standalone/sdk/python/failproofai_sdk/_version.py +1 -1
- package/.next/standalone/sdk/python/failproofai_sdk/evaluator/__init__.py +101 -0
- package/.next/standalone/sdk/python/failproofai_sdk/evaluator/__main__.py +49 -0
- package/.next/standalone/sdk/python/failproofai_sdk/evaluator/_sandbox_runner.py +63 -0
- package/.next/standalone/sdk/python/failproofai_sdk/evaluator/authoring.py +404 -0
- package/.next/standalone/sdk/python/failproofai_sdk/evaluator/client.py +299 -0
- package/.next/standalone/sdk/python/failproofai_sdk/evaluator/protocol.py +754 -0
- package/.next/standalone/sdk/python/failproofai_sdk/evaluator/runtime.py +936 -0
- package/.next/standalone/sdk/python/failproofai_sdk/evaluator/source.py +684 -0
- package/.next/standalone/sdk/python/tests/fixtures/evaluator_v2/README.md +28 -0
- package/.next/standalone/sdk/python/tests/fixtures/evaluator_v2/contract.json +252 -0
- package/.next/standalone/sdk/python/tests/test_evaluator_authoring.py +124 -0
- package/.next/standalone/sdk/python/tests/test_evaluator_client.py +253 -0
- package/.next/standalone/sdk/python/tests/test_evaluator_example.py +35 -0
- package/.next/standalone/sdk/python/tests/test_evaluator_http_e2e.py +636 -0
- package/.next/standalone/sdk/python/tests/test_evaluator_main.py +47 -0
- package/.next/standalone/sdk/python/tests/test_evaluator_protocol.py +246 -0
- package/.next/standalone/sdk/python/tests/test_evaluator_review_fixes.py +164 -0
- package/.next/standalone/sdk/python/tests/test_evaluator_runtime.py +1103 -0
- package/.next/standalone/sdk/python/tests/test_evaluator_source.py +430 -0
- package/.next/standalone/sdk/python/tests/test_zero_dependencies.py +17 -0
- package/.next/standalone/sdk/python/uv.lock +29 -29
- package/.next/standalone/server.js +1 -1
- package/README.md +13 -58
- package/dist/cli.mjs +5368 -8756
- package/dist/worker.mjs +539 -312
- package/lib/auth/api-server-client.ts +28 -0
- package/lib/claude-sessions.ts +101 -27
- package/package.json +14 -13
- package/pi-extension/index.ts +42 -4
- package/scripts/changelog-open.py +115 -0
- package/src/audit/cli.ts +134 -0
- package/src/audit/desktop-notify.ts +420 -0
- package/src/audit/harm-report.ts +101 -0
- package/src/audit/index.ts +141 -13
- package/src/audit/leak-fingerprint.ts +200 -0
- package/src/audit/leak-notice.ts +161 -0
- package/src/audit/leak-record.ts +232 -0
- package/src/audit/leak-scan.ts +292 -0
- package/src/audit/leak-store.ts +217 -0
- package/src/audit/macos-notifier.ts +310 -0
- package/src/audit/redact-example.ts +266 -14
- package/src/audit/report-harm.ts +12 -1
- package/src/audit/report.ts +6 -3
- package/src/audit/schedule-cli.ts +37 -0
- package/src/audit/scoring.ts +49 -0
- package/src/audit/types.ts +41 -0
- package/src/hooks/builtin-policies.ts +67 -2
- package/src/hooks/configure-wizard.ts +20 -0
- package/src/hooks/fp-config.ts +71 -11
- package/src/hooks/fp-home.ts +27 -0
- package/src/hooks/handler.ts +59 -1
- package/src/hooks/integrations.ts +47 -13
- package/src/hooks/notice.ts +155 -0
- package/src/hooks/uninstall-cli.ts +15 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1b5jzxr._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0j6gd_k._.js +0 -23
- package/.next/standalone/.next/server/chunks/ssr/_0v8eebd._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_1ylae7o._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/node_modules_html-to-image_es_index_1ao30b1.js +0 -3
- package/.next/standalone/.next/static/chunks/03slu0c8_71y7.js +0 -1
- package/.next/standalone/.next/static/chunks/0nm11ysn0153h.js +0 -1
- package/.next/standalone/.next/static/chunks/0u64yey446k4f.css +0 -1
- package/.next/standalone/.next/static/chunks/1r9676mkvihto.js +0 -1
- package/.next/standalone/.next/static/chunks/30pct5s734jua.js +0 -6
- package/.next/standalone/.next/static/chunks/3pibxnbhjza7n.js +0 -69
- package/.next/standalone/.next/static/chunks/3qfoeqhy5l10x.js +0 -1
- package/.next/standalone/.next/static/chunks/3zkg2s2vzxc3d.js +0 -1
- package/.next/standalone/.next/static/chunks/41b5ptaocvi_l.js +0 -1
- /package/.next/standalone/.next/static/{JBYgkXkA1PS48m9P_hKNw → aKNv4Kmv98Xpwns0Xa-C-}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{JBYgkXkA1PS48m9P_hKNw → aKNv4Kmv98Xpwns0Xa-C-}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{JBYgkXkA1PS48m9P_hKNw → aKNv4Kmv98Xpwns0Xa-C-}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Evaluator v2 contract fixtures
|
|
2
|
+
|
|
3
|
+
`contract.json` is the Checkpoint 0 wire contract shared by the Rust server and
|
|
4
|
+
the zero-dependency Python SDK. The matching copy lives at
|
|
5
|
+
`server/tests/fixtures/evaluator_v2/contract.json` in the `agenteye` repository.
|
|
6
|
+
Change both copies together.
|
|
7
|
+
|
|
8
|
+
Contract rules:
|
|
9
|
+
|
|
10
|
+
- The only accepted protocol major is the exact string `"2"`. Unsupported
|
|
11
|
+
majors return `426 unsupported_protocol_version`.
|
|
12
|
+
- Unknown JSON fields are ignored so either side may add optional fields within
|
|
13
|
+
major version 2. Removing, renaming, or changing the meaning of a field needs
|
|
14
|
+
a new major version.
|
|
15
|
+
- Worker payloads never carry authoritative tenant or evaluator-instance
|
|
16
|
+
identity. The server derives those from the credential and leased record.
|
|
17
|
+
- `lease_generation` is the fencing token. `409 lease_lost` is terminal for the
|
|
18
|
+
affected local execution; the SDK must stop heartbeating or submitting it.
|
|
19
|
+
- `submission_id` is an idempotency key. Replaying identical content succeeds;
|
|
20
|
+
reusing it for different content returns `409 submission_conflict`.
|
|
21
|
+
- Transcript overflow is terminal in v2 (`413 transcript_too_large`); the server
|
|
22
|
+
never silently truncates the evaluated input.
|
|
23
|
+
- Only errors marked `retryable` may be retried automatically. HTTP method alone
|
|
24
|
+
is not enough to decide whether a protocol operation is safe to replay.
|
|
25
|
+
|
|
26
|
+
The timing and payload limits in the fixture are normative defaults. A register
|
|
27
|
+
response may lower the worker's effective concurrency, heartbeat interval, or
|
|
28
|
+
lease duration, but may not raise a client-side payload bound.
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
{
|
|
2
|
+
"fixture_revision": "evaluator-v2-2026-08-28.3",
|
|
3
|
+
"protocol": {
|
|
4
|
+
"supported_major_versions": ["2"],
|
|
5
|
+
"transcript_schema_version": "2",
|
|
6
|
+
"result_schema_version": "2"
|
|
7
|
+
},
|
|
8
|
+
"http": {
|
|
9
|
+
"register": "/v1/evaluator/workers/register",
|
|
10
|
+
"claim": "/v1/evaluator/assignments/claim",
|
|
11
|
+
"transcript": "/v1/evaluator/assignments/{assignment_id}/transcript",
|
|
12
|
+
"definitions": "/v1/evaluator/assignments/{assignment_id}/definitions",
|
|
13
|
+
"plan": "/v1/evaluator/assignments/{assignment_id}/plan",
|
|
14
|
+
"heartbeat": "/v1/evaluator/runs/heartbeat",
|
|
15
|
+
"result": "/v1/evaluator/runs/{evaluation_run_id}/result",
|
|
16
|
+
"worker_id_header": "X-FailproofAI-Worker-Id",
|
|
17
|
+
"lease_generation_header": "X-FailproofAI-Lease-Generation"
|
|
18
|
+
},
|
|
19
|
+
"timing": {
|
|
20
|
+
"heartbeat_interval_seconds": 30,
|
|
21
|
+
"lease_duration_seconds": 120,
|
|
22
|
+
"poll_interval_seconds": 10,
|
|
23
|
+
"max_attempts": 5
|
|
24
|
+
},
|
|
25
|
+
"limits": {
|
|
26
|
+
"max_catalog_definitions": 100,
|
|
27
|
+
"max_claim_capacity": 32,
|
|
28
|
+
"max_transcript_bytes": 26214400,
|
|
29
|
+
"max_results_per_run": 25,
|
|
30
|
+
"max_eval_key_bytes": 128,
|
|
31
|
+
"max_display_name_bytes": 128,
|
|
32
|
+
"max_version_bytes": 128,
|
|
33
|
+
"max_worker_id_bytes": 128,
|
|
34
|
+
"max_label_bytes": 64,
|
|
35
|
+
"max_labels_per_result": 20,
|
|
36
|
+
"max_summary_bytes": 4096,
|
|
37
|
+
"max_reasoning_bytes": 16384,
|
|
38
|
+
"max_unit_bytes": 64,
|
|
39
|
+
"max_display_value_bytes": 256,
|
|
40
|
+
"max_description_bytes": 1000,
|
|
41
|
+
"max_error_code_bytes": 64,
|
|
42
|
+
"max_error_message_bytes": 4096
|
|
43
|
+
},
|
|
44
|
+
"errors": {
|
|
45
|
+
"invalid_credentials": {"http_status": 401, "retryable": false},
|
|
46
|
+
"instance_disabled": {"http_status": 403, "retryable": false},
|
|
47
|
+
"insufficient_permissions": {"http_status": 403, "retryable": false},
|
|
48
|
+
"assignment_not_found": {"http_status": 404, "retryable": false},
|
|
49
|
+
"run_not_found": {"http_status": 404, "retryable": false},
|
|
50
|
+
"catalog_mismatch": {"http_status": 409, "retryable": false},
|
|
51
|
+
"lease_lost": {"http_status": 409, "retryable": false},
|
|
52
|
+
"plan_conflict": {"http_status": 409, "retryable": false},
|
|
53
|
+
"submission_conflict": {"http_status": 409, "retryable": false},
|
|
54
|
+
"retry_budget_exhausted": {"http_status": 409, "retryable": false},
|
|
55
|
+
"transcript_too_large": {"http_status": 413, "retryable": false},
|
|
56
|
+
"invalid_request": {"http_status": 422, "retryable": false},
|
|
57
|
+
"invalid_catalog": {"http_status": 422, "retryable": false},
|
|
58
|
+
"incomplete_plan": {"http_status": 422, "retryable": false},
|
|
59
|
+
"unsupported_protocol_version": {"http_status": 426, "retryable": false},
|
|
60
|
+
"internal_error": {"http_status": 500, "retryable": true}
|
|
61
|
+
},
|
|
62
|
+
"samples": {
|
|
63
|
+
"register_request": {
|
|
64
|
+
"protocol_version": "2",
|
|
65
|
+
"worker_id": "pod-7f8c9",
|
|
66
|
+
"sdk_version": "0.0.1b2",
|
|
67
|
+
"catalog_revision": "sha256:b4e2c077aa9f91b5de1f3184ebf96811bce07eec00c7f417896ab269c67c88fb",
|
|
68
|
+
"max_concurrency": 4,
|
|
69
|
+
"definitions": [
|
|
70
|
+
{
|
|
71
|
+
"eval_key": "tool_efficiency",
|
|
72
|
+
"display_name": "Tool efficiency",
|
|
73
|
+
"eval_version": "1.2.0",
|
|
74
|
+
"result_kind": "score",
|
|
75
|
+
"labels": ["tools", "deterministic"]
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
},
|
|
79
|
+
"register_response": {
|
|
80
|
+
"protocol_version": "2",
|
|
81
|
+
"evaluator_instance_id": "018f47a8-7c1d-7e21-a22a-79f7a4d23101",
|
|
82
|
+
"evaluator_kind": "customer",
|
|
83
|
+
"heartbeat_interval_seconds": 30,
|
|
84
|
+
"lease_duration_seconds": 120,
|
|
85
|
+
"poll_interval_seconds": 10,
|
|
86
|
+
"claim_limit": 4,
|
|
87
|
+
"disabled_definitions": []
|
|
88
|
+
},
|
|
89
|
+
"claim_request": {
|
|
90
|
+
"protocol_version": "2",
|
|
91
|
+
"worker_id": "pod-7f8c9",
|
|
92
|
+
"catalog_revision": "sha256:b4e2c077aa9f91b5de1f3184ebf96811bce07eec00c7f417896ab269c67c88fb",
|
|
93
|
+
"capacity": 2
|
|
94
|
+
},
|
|
95
|
+
"claim_response": {
|
|
96
|
+
"protocol_version": "2",
|
|
97
|
+
"assignments": [
|
|
98
|
+
{
|
|
99
|
+
"assignment_id": "018f47a8-7c1d-7e21-a22a-79f7a4d23102",
|
|
100
|
+
"lease_generation": 3,
|
|
101
|
+
"lease_expires_at": "2026-08-28T12:02:00.000000Z",
|
|
102
|
+
"session_id": "session-42",
|
|
103
|
+
"session_revision_id": "evt-agent-end-42",
|
|
104
|
+
"agent_id": "support-agent",
|
|
105
|
+
"environment": "production",
|
|
106
|
+
"trigger_reason": "agent_end",
|
|
107
|
+
"event_count": 42,
|
|
108
|
+
"transcript_url": "/v1/evaluator/assignments/018f47a8-7c1d-7e21-a22a-79f7a4d23102/transcript",
|
|
109
|
+
"definitions_url": "/v1/evaluator/assignments/018f47a8-7c1d-7e21-a22a-79f7a4d23102/definitions"
|
|
110
|
+
}
|
|
111
|
+
]
|
|
112
|
+
},
|
|
113
|
+
"definitions_response": {
|
|
114
|
+
"protocol_version": "2",
|
|
115
|
+
"assignment_id": "018f47a8-7c1d-7e21-a22a-79f7a4d23102",
|
|
116
|
+
"catalog_revision": "sha256:b4e2c077aa9f91b5de1f3184ebf96811bce07eec00c7f417896ab269c67c88fb",
|
|
117
|
+
"definitions": [
|
|
118
|
+
{
|
|
119
|
+
"eval_key": "tool_efficiency",
|
|
120
|
+
"display_name": "Tool efficiency",
|
|
121
|
+
"eval_version": "1.2.0",
|
|
122
|
+
"result_kind": "score",
|
|
123
|
+
"labels": ["tools", "deterministic"],
|
|
124
|
+
"execution_mode": "python",
|
|
125
|
+
"condition_source": null,
|
|
126
|
+
"source_checksum": "sha256:da6cf174ea9199dd8412af4abebd40bd27dea482f738cb8c28523076472501ea",
|
|
127
|
+
"timeout_seconds": 30.0
|
|
128
|
+
}
|
|
129
|
+
]
|
|
130
|
+
},
|
|
131
|
+
"transcript_response": {
|
|
132
|
+
"schema_version": "2",
|
|
133
|
+
"assignment_id": "018f47a8-7c1d-7e21-a22a-79f7a4d23102",
|
|
134
|
+
"session_id": "session-42",
|
|
135
|
+
"session_revision_id": "evt-agent-end-42",
|
|
136
|
+
"agent_id": "support-agent",
|
|
137
|
+
"environment": "production",
|
|
138
|
+
"started_at": "2026-08-28T11:58:00.000000Z",
|
|
139
|
+
"ended_at": "2026-08-28T12:00:00.000000Z",
|
|
140
|
+
"event_count": 2,
|
|
141
|
+
"events": [
|
|
142
|
+
{
|
|
143
|
+
"id": "evt-tool-1",
|
|
144
|
+
"ts": "2026-08-28T11:59:00.000000Z",
|
|
145
|
+
"event_type": "tool_use",
|
|
146
|
+
"payload": {"tool_name": "search"}
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"id": "evt-end-1",
|
|
150
|
+
"ts": "2026-08-28T12:00:00.000000Z",
|
|
151
|
+
"event_type": "agent_end",
|
|
152
|
+
"payload": {"summary": "Done"}
|
|
153
|
+
}
|
|
154
|
+
]
|
|
155
|
+
},
|
|
156
|
+
"plan_request": {
|
|
157
|
+
"protocol_version": "2",
|
|
158
|
+
"worker_id": "pod-7f8c9",
|
|
159
|
+
"lease_generation": 3,
|
|
160
|
+
"selected": [
|
|
161
|
+
{"eval_key": "tool_efficiency", "eval_version": "1.2.0"}
|
|
162
|
+
],
|
|
163
|
+
"skipped": [
|
|
164
|
+
{
|
|
165
|
+
"eval_key": "answer_groundedness",
|
|
166
|
+
"eval_version": "2.1.0",
|
|
167
|
+
"reason_code": "no_retrieval_events"
|
|
168
|
+
}
|
|
169
|
+
]
|
|
170
|
+
},
|
|
171
|
+
"plan_response": {
|
|
172
|
+
"protocol_version": "2",
|
|
173
|
+
"assignment_id": "018f47a8-7c1d-7e21-a22a-79f7a4d23102",
|
|
174
|
+
"assignment_status": "planned",
|
|
175
|
+
"idempotent_replay": false,
|
|
176
|
+
"runs": [
|
|
177
|
+
{
|
|
178
|
+
"evaluation_run_id": "018f47a8-7c1d-7e21-a22a-79f7a4d23103",
|
|
179
|
+
"eval_key": "tool_efficiency",
|
|
180
|
+
"eval_version": "1.2.0",
|
|
181
|
+
"execution_mode": "python",
|
|
182
|
+
"evaluator_source": "EvalResult(score=Score(1.0))",
|
|
183
|
+
"source_checksum": "sha256:da6cf174ea9199dd8412af4abebd40bd27dea482f738cb8c28523076472501ea",
|
|
184
|
+
"timeout_seconds": 30.0
|
|
185
|
+
}
|
|
186
|
+
]
|
|
187
|
+
},
|
|
188
|
+
"heartbeat_request": {
|
|
189
|
+
"protocol_version": "2",
|
|
190
|
+
"worker_id": "pod-7f8c9",
|
|
191
|
+
"lease_generation": 3,
|
|
192
|
+
"runs": [
|
|
193
|
+
{
|
|
194
|
+
"evaluation_run_id": "018f47a8-7c1d-7e21-a22a-79f7a4d23103",
|
|
195
|
+
"state": "running",
|
|
196
|
+
"progress": 0.5
|
|
197
|
+
}
|
|
198
|
+
]
|
|
199
|
+
},
|
|
200
|
+
"heartbeat_response": {
|
|
201
|
+
"protocol_version": "2",
|
|
202
|
+
"lease_expires_at": "2026-08-28T12:02:30.000000Z",
|
|
203
|
+
"accepted_run_ids": ["018f47a8-7c1d-7e21-a22a-79f7a4d23103"]
|
|
204
|
+
},
|
|
205
|
+
"result_request": {
|
|
206
|
+
"protocol_version": "2",
|
|
207
|
+
"result_schema_version": "2",
|
|
208
|
+
"submission_id": "018f47a8-7c1d-7e21-a22a-79f7a4d23104",
|
|
209
|
+
"worker_id": "pod-7f8c9",
|
|
210
|
+
"lease_generation": 3,
|
|
211
|
+
"status": "succeeded",
|
|
212
|
+
"started_at": "2026-08-28T12:00:10.000000Z",
|
|
213
|
+
"finished_at": "2026-08-28T12:00:10.812000Z",
|
|
214
|
+
"duration_ms": 812,
|
|
215
|
+
"summary": "Used a compact tool set without retries.",
|
|
216
|
+
"results": [
|
|
217
|
+
{
|
|
218
|
+
"result_key": "tool_efficiency",
|
|
219
|
+
"result_kind": "score",
|
|
220
|
+
"numeric_value": 0.92,
|
|
221
|
+
"bool_value": true,
|
|
222
|
+
"text_value": null,
|
|
223
|
+
"unit": "ratio",
|
|
224
|
+
"display_value": "92%",
|
|
225
|
+
"description": "Distinct tools divided by total tool calls",
|
|
226
|
+
"reasoning": "3 distinct tools across 3 calls",
|
|
227
|
+
"labels": ["tools", "deterministic"]
|
|
228
|
+
}
|
|
229
|
+
],
|
|
230
|
+
"error_code": null,
|
|
231
|
+
"error_message": null
|
|
232
|
+
},
|
|
233
|
+
"result_response": {
|
|
234
|
+
"protocol_version": "2",
|
|
235
|
+
"evaluation_run_id": "018f47a8-7c1d-7e21-a22a-79f7a4d23103",
|
|
236
|
+
"submission_id": "018f47a8-7c1d-7e21-a22a-79f7a4d23104",
|
|
237
|
+
"status": "committed",
|
|
238
|
+
"idempotent_replay": false,
|
|
239
|
+
"result_count": 1,
|
|
240
|
+
"result_checksum": "sha256:8cbd34f2d95d"
|
|
241
|
+
},
|
|
242
|
+
"error_response": {
|
|
243
|
+
"protocol_version": "2",
|
|
244
|
+
"error": {
|
|
245
|
+
"code": "lease_lost",
|
|
246
|
+
"message": "The assignment lease is no longer owned by this worker.",
|
|
247
|
+
"retryable": false,
|
|
248
|
+
"request_id": "req-018f47a8"
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import math
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from failproofai_sdk.evaluator import (
|
|
9
|
+
Assertion,
|
|
10
|
+
EvalResult,
|
|
11
|
+
Evaluator,
|
|
12
|
+
Metric,
|
|
13
|
+
ResultKind,
|
|
14
|
+
Score,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_catalog_is_stable_across_registration_order():
|
|
19
|
+
first = Evaluator(name="acme", version="2026.08.1")
|
|
20
|
+
second = Evaluator(name="acme", version="2026.08.1")
|
|
21
|
+
|
|
22
|
+
@first.eval("zeta_check", version="1", labels=["z", "a"])
|
|
23
|
+
def first_zeta(session):
|
|
24
|
+
return EvalResult(score=Score(1))
|
|
25
|
+
|
|
26
|
+
@first.eval("alpha_check", version="1")
|
|
27
|
+
def first_alpha(session):
|
|
28
|
+
return EvalResult(score=Score(1))
|
|
29
|
+
|
|
30
|
+
@second.eval("alpha_check", version="1")
|
|
31
|
+
def second_alpha(session):
|
|
32
|
+
return EvalResult(score=Score(1))
|
|
33
|
+
|
|
34
|
+
@second.eval("zeta_check", version="1", labels=["a", "z"])
|
|
35
|
+
def second_zeta(session):
|
|
36
|
+
return EvalResult(score=Score(1))
|
|
37
|
+
|
|
38
|
+
assert first.catalog_revision == second.catalog_revision
|
|
39
|
+
assert [item.eval_key for item in first.catalog()] == ["alpha_check", "zeta_check"]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_duplicate_eval_keys_are_rejected_even_when_versions_differ():
|
|
43
|
+
evaluator = Evaluator(name="acme", version="1")
|
|
44
|
+
|
|
45
|
+
@evaluator.eval("quality", version="1")
|
|
46
|
+
def quality_v1(session):
|
|
47
|
+
return EvalResult(score=Score(1))
|
|
48
|
+
|
|
49
|
+
with pytest.raises(ValueError, match="duplicate eval key"):
|
|
50
|
+
|
|
51
|
+
@evaluator.eval("quality", version="2")
|
|
52
|
+
def quality_v2(session):
|
|
53
|
+
return EvalResult(score=Score(1))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@pytest.mark.parametrize("value", [-0.01, 1.01, math.nan, math.inf])
|
|
57
|
+
def test_scores_are_finite_ratios(value):
|
|
58
|
+
with pytest.raises(ValueError):
|
|
59
|
+
Score(value)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_result_presentation_fields_are_bounded_before_networking():
|
|
63
|
+
with pytest.raises(ValueError, match="unit is 65 bytes"):
|
|
64
|
+
Metric(1, unit="u" * 65)
|
|
65
|
+
with pytest.raises(ValueError, match="display value is 257 bytes"):
|
|
66
|
+
Score(1, display_value="x" * 257)
|
|
67
|
+
with pytest.raises(ValueError, match="description is 1001 bytes"):
|
|
68
|
+
Assertion(True, description="x" * 1001)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_eval_result_expands_to_typed_long_form_rows():
|
|
72
|
+
result = EvalResult(
|
|
73
|
+
score=Score(0.75, passed=True, unit="ratio"),
|
|
74
|
+
metrics={"call_count": Metric(4, unit="calls")},
|
|
75
|
+
assertions={"had_output": Assertion(True)},
|
|
76
|
+
reasoning="Three useful calls out of four.",
|
|
77
|
+
labels=("tools",),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
items = result.result_items("tool_efficiency")
|
|
81
|
+
assert [item.result_kind for item in items] == [
|
|
82
|
+
ResultKind.SCORE,
|
|
83
|
+
ResultKind.METRIC,
|
|
84
|
+
ResultKind.ASSERTION,
|
|
85
|
+
]
|
|
86
|
+
assert items[0].reasoning == "Three useful calls out of four."
|
|
87
|
+
assert items[1].numeric_value == 4
|
|
88
|
+
assert items[2].bool_value is True
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_empty_eval_result_is_rejected_when_serialized():
|
|
92
|
+
with pytest.raises(ValueError, match="must contain"):
|
|
93
|
+
EvalResult().result_items("quality")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_result_keys_must_be_unique_across_kinds():
|
|
97
|
+
result = EvalResult(score=Score(1), metrics={"quality": 1})
|
|
98
|
+
with pytest.raises(ValueError, match="result keys must be unique"):
|
|
99
|
+
result.result_items("quality")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_sync_and_async_functions_share_one_call_path():
|
|
103
|
+
async def async_eval(session):
|
|
104
|
+
return EvalResult(score=Score(1))
|
|
105
|
+
|
|
106
|
+
def sync_eval(session):
|
|
107
|
+
return EvalResult(score=Score(0.5))
|
|
108
|
+
|
|
109
|
+
async def exercise():
|
|
110
|
+
sync_result = await Evaluator.call(sync_eval, None)
|
|
111
|
+
async_result = await Evaluator.call(async_eval, None)
|
|
112
|
+
return sync_result, async_result
|
|
113
|
+
|
|
114
|
+
sync_result, async_result = asyncio.run(exercise())
|
|
115
|
+
assert sync_result.score.value == 0.5
|
|
116
|
+
assert async_result.score.value == 1
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_keys_are_machine_safe_and_versions_are_explicit():
|
|
120
|
+
evaluator = Evaluator(name="acme", version="1")
|
|
121
|
+
with pytest.raises(ValueError, match="must match"):
|
|
122
|
+
evaluator.eval("Not Safe", version="1")
|
|
123
|
+
with pytest.raises(ValueError, match="must not be empty"):
|
|
124
|
+
evaluator.eval("safe", version="")
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
import json
|
|
5
|
+
import threading
|
|
6
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from urllib.error import HTTPError, URLError
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
from failproofai_sdk.evaluator import (
|
|
13
|
+
Assignment,
|
|
14
|
+
ClaimRequest,
|
|
15
|
+
EvaluatorAPIError,
|
|
16
|
+
EvaluatorClient,
|
|
17
|
+
ResultRequest,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
FIXTURE = Path(__file__).parent / "fixtures" / "evaluator_v2" / "contract.json"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _samples():
|
|
24
|
+
return json.loads(FIXTURE.read_text(encoding="utf-8"))["samples"]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Response:
|
|
28
|
+
def __init__(self, body):
|
|
29
|
+
self.body = json.dumps(body).encode()
|
|
30
|
+
|
|
31
|
+
def __enter__(self):
|
|
32
|
+
return self
|
|
33
|
+
|
|
34
|
+
def __exit__(self, *args):
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
def read(self, amount):
|
|
38
|
+
return self.body[:amount]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class RawResponse(Response):
|
|
42
|
+
def __init__(self, body: bytes):
|
|
43
|
+
self.body = body
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_claim_sends_bearer_auth_and_does_not_retry():
|
|
47
|
+
calls = []
|
|
48
|
+
|
|
49
|
+
def opener(request, timeout):
|
|
50
|
+
calls.append((request, timeout))
|
|
51
|
+
raise URLError("offline")
|
|
52
|
+
|
|
53
|
+
client = EvaluatorClient(
|
|
54
|
+
base_url="https://cloud.example/api/",
|
|
55
|
+
credential="secret",
|
|
56
|
+
opener=opener,
|
|
57
|
+
sleeper=lambda _: None,
|
|
58
|
+
)
|
|
59
|
+
with pytest.raises(EvaluatorAPIError, match="transport_error"):
|
|
60
|
+
client.claim(ClaimRequest("worker", "sha256:x", 1, 20))
|
|
61
|
+
|
|
62
|
+
assert len(calls) == 1
|
|
63
|
+
request, timeout = calls[0]
|
|
64
|
+
assert request.full_url == "https://cloud.example/v1/evaluator/assignments/claim"
|
|
65
|
+
assert request.get_header("Authorization") == "Bearer secret"
|
|
66
|
+
assert timeout == 30
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_idempotent_result_submission_retries_transport_failure():
|
|
70
|
+
samples = _samples()
|
|
71
|
+
calls = 0
|
|
72
|
+
|
|
73
|
+
def opener(request, timeout):
|
|
74
|
+
nonlocal calls
|
|
75
|
+
calls += 1
|
|
76
|
+
if calls == 1:
|
|
77
|
+
raise URLError("reset")
|
|
78
|
+
return Response(samples["result_response"])
|
|
79
|
+
|
|
80
|
+
client = EvaluatorClient(
|
|
81
|
+
base_url="https://cloud.example/",
|
|
82
|
+
credential="secret",
|
|
83
|
+
opener=opener,
|
|
84
|
+
sleeper=lambda _: None,
|
|
85
|
+
)
|
|
86
|
+
response = client.submit_result(
|
|
87
|
+
samples["result_response"]["evaluation_run_id"],
|
|
88
|
+
ResultRequest.from_wire(samples["result_request"]),
|
|
89
|
+
)
|
|
90
|
+
assert response.status == "committed"
|
|
91
|
+
assert calls == 2
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_transcript_url_cannot_exfiltrate_the_worker_credential():
|
|
95
|
+
sample = _samples()["claim_response"]["assignments"][0]
|
|
96
|
+
assignment = Assignment.from_wire(
|
|
97
|
+
{**sample, "transcript_url": "https://evil.test/read"}
|
|
98
|
+
)
|
|
99
|
+
client = EvaluatorClient(
|
|
100
|
+
base_url="https://cloud.example/",
|
|
101
|
+
credential="secret",
|
|
102
|
+
opener=lambda *_args, **_kwargs: pytest.fail("network must not be reached"),
|
|
103
|
+
)
|
|
104
|
+
with pytest.raises(EvaluatorAPIError, match="outside the configured API origin"):
|
|
105
|
+
client.transcript(assignment, worker_id="worker")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_machine_error_envelope_controls_retryability():
|
|
109
|
+
body = json.dumps(_samples()["error_response"]).encode()
|
|
110
|
+
|
|
111
|
+
def opener(request, timeout):
|
|
112
|
+
raise HTTPError(request.full_url, 409, "Conflict", {}, io.BytesIO(body))
|
|
113
|
+
|
|
114
|
+
client = EvaluatorClient(
|
|
115
|
+
base_url="https://cloud.example/", credential="secret", opener=opener
|
|
116
|
+
)
|
|
117
|
+
with pytest.raises(EvaluatorAPIError) as caught:
|
|
118
|
+
client.claim(ClaimRequest("worker", "sha256:x", 1, 20))
|
|
119
|
+
assert caught.value.code == "lease_lost"
|
|
120
|
+
assert caught.value.status == 409
|
|
121
|
+
assert caught.value.retryable is False
|
|
122
|
+
assert caught.value.request_id == "req-018f47a8"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@pytest.mark.parametrize(
|
|
126
|
+
("body", "code"),
|
|
127
|
+
[
|
|
128
|
+
(b"not-json", "invalid_response"),
|
|
129
|
+
(b"[]", "invalid_response"),
|
|
130
|
+
(
|
|
131
|
+
b"{" + b'"padding":"' + b"x" * (2 * 1024 * 1024) + b'"}',
|
|
132
|
+
"response_too_large",
|
|
133
|
+
),
|
|
134
|
+
],
|
|
135
|
+
)
|
|
136
|
+
def test_malformed_or_oversized_server_responses_fail_closed(body, code):
|
|
137
|
+
client = EvaluatorClient(
|
|
138
|
+
base_url="https://cloud.example/",
|
|
139
|
+
credential="secret",
|
|
140
|
+
opener=lambda request, timeout: RawResponse(body),
|
|
141
|
+
)
|
|
142
|
+
with pytest.raises(EvaluatorAPIError) as caught:
|
|
143
|
+
client.claim(ClaimRequest("worker", "sha256:x", 1, 20))
|
|
144
|
+
assert caught.value.code == code
|
|
145
|
+
assert caught.value.retryable is False
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_transcript_identity_is_sent_as_fencing_headers():
|
|
149
|
+
samples = _samples()
|
|
150
|
+
assignment = Assignment.from_wire(samples["claim_response"]["assignments"][0])
|
|
151
|
+
captured = None
|
|
152
|
+
|
|
153
|
+
def opener(request, timeout):
|
|
154
|
+
nonlocal captured
|
|
155
|
+
captured = request
|
|
156
|
+
return Response(samples["transcript_response"])
|
|
157
|
+
|
|
158
|
+
client = EvaluatorClient(
|
|
159
|
+
base_url="https://cloud.example/", credential="secret", opener=opener
|
|
160
|
+
)
|
|
161
|
+
client.transcript(assignment, worker_id="worker-7")
|
|
162
|
+
assert captured.get_header("X-failproofai-worker-id") == "worker-7"
|
|
163
|
+
assert captured.get_header("X-failproofai-lease-generation") == "3"
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def test_definitions_use_the_server_supplied_path_and_fencing_headers():
|
|
167
|
+
samples = _samples()
|
|
168
|
+
assignment = Assignment.from_wire(samples["claim_response"]["assignments"][0])
|
|
169
|
+
captured = None
|
|
170
|
+
|
|
171
|
+
def opener(request, timeout):
|
|
172
|
+
nonlocal captured
|
|
173
|
+
captured = request
|
|
174
|
+
return Response(samples["definitions_response"])
|
|
175
|
+
|
|
176
|
+
client = EvaluatorClient(
|
|
177
|
+
base_url="https://cloud.example/", credential="secret", opener=opener
|
|
178
|
+
)
|
|
179
|
+
response = client.definitions(assignment, worker_id="worker-7")
|
|
180
|
+
|
|
181
|
+
assert response.assignment_id == assignment.assignment_id
|
|
182
|
+
assert response.definitions[0].execution_mode.value == "python"
|
|
183
|
+
assert captured.full_url.endswith(assignment.definitions_url)
|
|
184
|
+
assert captured.get_header("X-failproofai-worker-id") == "worker-7"
|
|
185
|
+
assert captured.get_header("X-failproofai-lease-generation") == "3"
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def test_constructor_rejects_unsafe_or_incomplete_configuration():
|
|
189
|
+
with pytest.raises(ValueError, match="absolute"):
|
|
190
|
+
EvaluatorClient(base_url="localhost:8080", credential="secret")
|
|
191
|
+
with pytest.raises(ValueError, match="credential"):
|
|
192
|
+
EvaluatorClient(base_url="https://cloud.example", credential="")
|
|
193
|
+
with pytest.raises(ValueError, match="control characters"):
|
|
194
|
+
EvaluatorClient(base_url="https://cloud.example", credential="secret\nleak")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def test_private_cluster_http_requires_an_explicit_opt_in():
|
|
198
|
+
with pytest.raises(ValueError, match="must use https"):
|
|
199
|
+
EvaluatorClient(base_url="http://server:8080", credential="secret")
|
|
200
|
+
EvaluatorClient(
|
|
201
|
+
base_url="http://server:8080",
|
|
202
|
+
credential="secret",
|
|
203
|
+
allow_insecure_http=True,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def test_protocol_redirect_does_not_forward_the_bearer_credential():
|
|
208
|
+
exfiltration_attempts = []
|
|
209
|
+
|
|
210
|
+
class Sink(BaseHTTPRequestHandler):
|
|
211
|
+
def do_POST(self):
|
|
212
|
+
exfiltration_attempts.append(self.headers.get("Authorization"))
|
|
213
|
+
self.send_response(200)
|
|
214
|
+
self.end_headers()
|
|
215
|
+
|
|
216
|
+
def log_message(self, format, *args):
|
|
217
|
+
return
|
|
218
|
+
|
|
219
|
+
sink = ThreadingHTTPServer(("127.0.0.1", 0), Sink)
|
|
220
|
+
sink_thread = threading.Thread(target=sink.serve_forever, daemon=True)
|
|
221
|
+
sink_thread.start()
|
|
222
|
+
|
|
223
|
+
location = f"http://127.0.0.1:{sink.server_address[1]}/steal"
|
|
224
|
+
|
|
225
|
+
class Redirector(BaseHTTPRequestHandler):
|
|
226
|
+
def do_POST(self):
|
|
227
|
+
self.send_response(307)
|
|
228
|
+
self.send_header("Location", location)
|
|
229
|
+
self.end_headers()
|
|
230
|
+
|
|
231
|
+
def log_message(self, format, *args):
|
|
232
|
+
return
|
|
233
|
+
|
|
234
|
+
redirector = ThreadingHTTPServer(("127.0.0.1", 0), Redirector)
|
|
235
|
+
redirector_thread = threading.Thread(target=redirector.serve_forever, daemon=True)
|
|
236
|
+
redirector_thread.start()
|
|
237
|
+
try:
|
|
238
|
+
client = EvaluatorClient(
|
|
239
|
+
base_url=f"http://127.0.0.1:{redirector.server_address[1]}",
|
|
240
|
+
credential="must-not-leak",
|
|
241
|
+
max_retries=0,
|
|
242
|
+
)
|
|
243
|
+
with pytest.raises(EvaluatorAPIError) as caught:
|
|
244
|
+
client.claim(ClaimRequest("worker", "sha256:x", 1, 0))
|
|
245
|
+
assert caught.value.status == 307
|
|
246
|
+
assert exfiltration_attempts == []
|
|
247
|
+
finally:
|
|
248
|
+
redirector.shutdown()
|
|
249
|
+
redirector.server_close()
|
|
250
|
+
redirector_thread.join(timeout=5)
|
|
251
|
+
sink.shutdown()
|
|
252
|
+
sink.server_close()
|
|
253
|
+
sink_thread.join(timeout=5)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from failproofai_sdk.evaluator import ConditionResult
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _example_module():
|
|
12
|
+
path = Path(__file__).parents[1] / "examples" / "evaluator_worker.py"
|
|
13
|
+
spec = importlib.util.spec_from_file_location("evaluator_worker_example", path)
|
|
14
|
+
module = importlib.util.module_from_spec(spec)
|
|
15
|
+
assert spec.loader is not None
|
|
16
|
+
spec.loader.exec_module(module)
|
|
17
|
+
return module
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_example_registers_deterministic_and_async_evals(monkeypatch):
|
|
21
|
+
monkeypatch.delenv("EXAMPLE_JUDGE_URL", raising=False)
|
|
22
|
+
module = _example_module()
|
|
23
|
+
definitions = {item.eval_key: item for item in module.app.definitions}
|
|
24
|
+
assert set(definitions) == {"answer_relevance", "tool_efficiency"}
|
|
25
|
+
assert definitions["answer_relevance"].eval_version == "judge-api-v1"
|
|
26
|
+
|
|
27
|
+
skipped = definitions["answer_relevance"].condition(None)
|
|
28
|
+
assert skipped == ConditionResult(False, "judge_not_configured")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_example_rejects_non_http_judge_urls(monkeypatch):
|
|
32
|
+
module = _example_module()
|
|
33
|
+
monkeypatch.setenv("EXAMPLE_JUDGE_URL", "file:///etc/passwd")
|
|
34
|
+
with pytest.raises(ValueError, match="absolute http"):
|
|
35
|
+
module._call_judge("question", "answer")
|