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.
Files changed (251) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +5 -5
  3. package/.next/standalone/.next/prerender-manifest.json +5 -5
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +2 -2
  6. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  11. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
  12. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  13. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +2 -2
  15. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  16. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  17. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  19. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  20. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  21. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
  22. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  23. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/audit/invite/route_client-reference-manifest.js +1 -1
  25. package/.next/standalone/.next/server/app/api/audit/run/route.js +4 -4
  26. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/audit/run/route_client-reference-manifest.js +1 -1
  28. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/audit/status/route_client-reference-manifest.js +1 -1
  30. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/auth/login-request/route_client-reference-manifest.js +1 -1
  32. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/auth/login-verify/route_client-reference-manifest.js +1 -1
  34. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/auth/logout/route_client-reference-manifest.js +1 -1
  36. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/api/auth/status/route_client-reference-manifest.js +1 -1
  38. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route_client-reference-manifest.js +1 -1
  40. package/.next/standalone/.next/server/app/audit/page/build-manifest.json +2 -2
  41. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +37 -4
  42. package/.next/standalone/.next/server/app/audit/page.js +2 -2
  43. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  45. package/.next/standalone/.next/server/app/index.html +1 -1
  46. package/.next/standalone/.next/server/app/index.rsc +15 -15
  47. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
  48. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  49. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  50. package/.next/standalone/.next/server/app/page/build-manifest.json +2 -2
  51. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  52. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  53. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  54. package/.next/standalone/.next/server/app/policies/page/build-manifest.json +2 -2
  55. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  56. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  58. package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +2 -2
  59. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  60. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  61. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  62. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +2 -2
  63. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  64. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  65. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  66. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  67. package/.next/standalone/.next/server/app/projects/page/build-manifest.json +2 -2
  68. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  69. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  70. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  71. package/.next/standalone/.next/server/app/settings/page/build-manifest.json +2 -2
  72. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
  73. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  74. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  75. package/.next/standalone/.next/server/chunks/{[externals]__0-9ti7z._.js → [externals]__1lh7m5d._.js} +1 -1
  76. package/.next/standalone/.next/server/chunks/{[externals]__0u3wrvq._.js → [externals]__1rqkg_y._.js} +1 -1
  77. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
  78. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1adacul._.js +1 -1
  79. package/.next/standalone/.next/server/chunks/{_1-akg6w._.js → _08w6xzm._.js} +2 -2
  80. package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
  81. package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
  82. package/.next/standalone/.next/server/chunks/_1ek68ln._.js +4 -4
  83. package/.next/standalone/.next/server/chunks/lib_telemetry_ts_0dctyyw._.js +1 -1
  84. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_01r25oi._.js +2 -2
  85. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +2 -2
  86. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  87. package/.next/standalone/.next/server/chunks/src_hooks_fp-home_ts_09kv0bn._.js +1 -1
  88. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +2 -2
  89. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +2 -2
  90. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +2 -2
  91. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +2 -2
  92. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +2 -2
  93. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +1 -1
  94. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +2 -2
  95. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0rgu2r3._.js +3 -0
  96. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +2 -2
  97. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +2 -2
  98. package/.next/standalone/.next/server/chunks/ssr/_0-oij9d._.js +23 -0
  99. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  100. package/.next/standalone/.next/server/chunks/ssr/_0l2hi_d._.js +3 -0
  101. package/.next/standalone/.next/server/chunks/ssr/_0oq1dh6._.js +3 -0
  102. package/.next/standalone/.next/server/chunks/ssr/_1es2j7i._.js +47 -13
  103. package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +1 -1
  104. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +2 -2
  105. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -69
  106. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_rerun-button_tsx_0blihgw._.js +3 -0
  107. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  108. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
  109. package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
  110. package/.next/standalone/.next/server/chunks/ssr/node_modules_13n822a._.js +1 -1
  111. package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +2 -2
  112. package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +1 -1
  113. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589g._.js +1 -1
  114. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
  115. package/.next/standalone/.next/server/middleware-build-manifest.js +5 -5
  116. package/.next/standalone/.next/server/pages/404.html +1 -1
  117. package/.next/standalone/.next/server/pages/500.html +1 -1
  118. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  119. package/.next/standalone/.next/server/server-reference-manifest.json +54 -21
  120. package/.next/standalone/.next/static/chunks/010bv1w6j171t.js +1 -0
  121. package/.next/standalone/.next/static/chunks/04r6ch8uf_n8m.js +1 -0
  122. package/.next/standalone/.next/static/chunks/{29-iqnp9v_axt.css → 0o-hh5_turzlz.css} +1 -1
  123. package/.next/standalone/.next/static/chunks/{40nr6ehj4p3cw.css → 0qmsunv04x4na.css} +1 -1
  124. package/.next/standalone/.next/static/chunks/{0ollg-nnu3t3e.js → 0wz8yftk18ts2.js} +1 -1
  125. package/.next/standalone/.next/static/chunks/0zebh1n9jkfbt.js +1 -0
  126. package/.next/standalone/.next/static/chunks/1eyl7fzkmnkf_.js +1 -0
  127. package/.next/standalone/.next/static/chunks/1pb1oztsbwcss.js +1 -0
  128. package/.next/standalone/.next/static/chunks/{1-_zkmnm-jwei.js → 2aquitk72k2op.js} +1 -1
  129. package/.next/standalone/.next/static/chunks/2bi_1y0a_smt7.js +6 -0
  130. package/.next/standalone/.next/static/chunks/{42yfkhekx440s.js → 2ej3b8gk5ittu.js} +1 -1
  131. package/.next/standalone/.next/static/chunks/{354-n7p7labpn.js → 2zafgs90r_leq.js} +1 -1
  132. package/.next/standalone/.next/static/chunks/{2xm5mj1d21b4n.js → 32spub4wqjem-.js} +1 -1
  133. package/.next/standalone/.next/static/chunks/{1a6u7l3giejch.js → 3m4upvybtrexd.js} +1 -1
  134. package/.next/standalone/.next/static/chunks/{2k9f4tyv04809.css → 3uyhmm01g64k4.css} +1 -0
  135. package/.next/standalone/.next/static/chunks/{turbopack-2x-o099q64i8h.js → turbopack-3s3c-u_u_l0u9.js} +1 -1
  136. package/.next/standalone/.opencode/plugins/failproofai.mjs +51 -27
  137. package/.next/standalone/app/actions/get-leaks.ts +100 -0
  138. package/.next/standalone/app/audit/_components/audit-dashboard.tsx +112 -33
  139. package/.next/standalone/app/audit/_components/audit-poster.tsx +29 -10
  140. package/.next/standalone/app/audit/_components/come-back-better-section.tsx +6 -1
  141. package/.next/standalone/app/audit/_components/empty-state.tsx +4 -1
  142. package/.next/standalone/app/audit/_components/how-to-improve-section.tsx +19 -5
  143. package/.next/standalone/app/audit/_components/leak-section.tsx +179 -0
  144. package/.next/standalone/app/audit/_components/share-templates.ts +110 -44
  145. package/.next/standalone/app/audit/audit-styles.css +114 -1
  146. package/.next/standalone/fp-cloud-cli/CHANGELOG.md +26 -3
  147. package/.next/standalone/fp-cloud-cli/fp_cli/_click_compat.py +31 -8
  148. package/.next/standalone/fp-cloud-cli/fp_cli/permissions.py +1 -0
  149. package/.next/standalone/fp-cloud-cli/pyproject.toml +9 -2
  150. package/.next/standalone/fp-cloud-cli/tests/test_click_compat.py +54 -0
  151. package/.next/standalone/fp-cloud-cli/uv.lock +9 -12
  152. package/.next/standalone/lib/auth/api-server-client.ts +28 -0
  153. package/.next/standalone/lib/claude-sessions.ts +101 -27
  154. package/.next/standalone/node_modules/@next/env/package.json +1 -1
  155. package/.next/standalone/node_modules/next/dist/build/swc/index.js +1 -1
  156. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo-experimental.runtime.prod.js +1 -1
  157. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo.runtime.prod.js +1 -1
  158. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-route-turbo.runtime.prod.js +1 -1
  159. package/.next/standalone/node_modules/next/dist/compiled/next-server/pages-turbo.runtime.prod.js +5 -5
  160. package/.next/standalone/node_modules/next/dist/experimental/testmode/fetch.js +10 -2
  161. package/.next/standalone/node_modules/next/dist/lib/patch-incorrect-lockfile.js +3 -3
  162. package/.next/standalone/node_modules/next/dist/lib/typescript/runTypeScriptCli.js +2 -2
  163. package/.next/standalone/node_modules/next/dist/lib/verify-typescript-setup.js +3 -1
  164. package/.next/standalone/node_modules/next/dist/server/config.js +1 -1
  165. package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-turbopack.js +2 -2
  166. package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-webpack.js +1 -1
  167. package/.next/standalone/node_modules/next/dist/server/lib/app-info-log.js +1 -1
  168. package/.next/standalone/node_modules/next/dist/server/lib/incremental-cache/file-system-cache.js +17 -4
  169. package/.next/standalone/node_modules/next/dist/server/lib/start-server.js +1 -1
  170. package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/escape-path-delimiters.js +1 -1
  171. package/.next/standalone/node_modules/next/dist/telemetry/anonymous-meta.js +1 -1
  172. package/.next/standalone/node_modules/next/dist/telemetry/events/swc-load-failure.js +1 -1
  173. package/.next/standalone/node_modules/next/dist/telemetry/events/version.js +2 -2
  174. package/.next/standalone/node_modules/next/package.json +11 -11
  175. package/.next/standalone/package.json +14 -13
  176. package/.next/standalone/sdk/python/CHANGELOG.md +155 -0
  177. package/.next/standalone/sdk/python/README.md +8 -0
  178. package/.next/standalone/sdk/python/examples/evaluator_worker.py +121 -0
  179. package/.next/standalone/sdk/python/failproofai_sdk/_version.py +1 -1
  180. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/__init__.py +101 -0
  181. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/__main__.py +49 -0
  182. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/_sandbox_runner.py +63 -0
  183. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/authoring.py +404 -0
  184. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/client.py +299 -0
  185. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/protocol.py +754 -0
  186. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/runtime.py +936 -0
  187. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/source.py +684 -0
  188. package/.next/standalone/sdk/python/tests/fixtures/evaluator_v2/README.md +28 -0
  189. package/.next/standalone/sdk/python/tests/fixtures/evaluator_v2/contract.json +252 -0
  190. package/.next/standalone/sdk/python/tests/test_evaluator_authoring.py +124 -0
  191. package/.next/standalone/sdk/python/tests/test_evaluator_client.py +253 -0
  192. package/.next/standalone/sdk/python/tests/test_evaluator_example.py +35 -0
  193. package/.next/standalone/sdk/python/tests/test_evaluator_http_e2e.py +636 -0
  194. package/.next/standalone/sdk/python/tests/test_evaluator_main.py +47 -0
  195. package/.next/standalone/sdk/python/tests/test_evaluator_protocol.py +246 -0
  196. package/.next/standalone/sdk/python/tests/test_evaluator_review_fixes.py +164 -0
  197. package/.next/standalone/sdk/python/tests/test_evaluator_runtime.py +1103 -0
  198. package/.next/standalone/sdk/python/tests/test_evaluator_source.py +430 -0
  199. package/.next/standalone/sdk/python/tests/test_zero_dependencies.py +17 -0
  200. package/.next/standalone/sdk/python/uv.lock +29 -29
  201. package/.next/standalone/server.js +1 -1
  202. package/README.md +13 -58
  203. package/dist/cli.mjs +5368 -8756
  204. package/dist/worker.mjs +539 -312
  205. package/lib/auth/api-server-client.ts +28 -0
  206. package/lib/claude-sessions.ts +101 -27
  207. package/package.json +14 -13
  208. package/pi-extension/index.ts +42 -4
  209. package/scripts/changelog-open.py +115 -0
  210. package/src/audit/cli.ts +134 -0
  211. package/src/audit/desktop-notify.ts +420 -0
  212. package/src/audit/harm-report.ts +101 -0
  213. package/src/audit/index.ts +141 -13
  214. package/src/audit/leak-fingerprint.ts +200 -0
  215. package/src/audit/leak-notice.ts +161 -0
  216. package/src/audit/leak-record.ts +232 -0
  217. package/src/audit/leak-scan.ts +292 -0
  218. package/src/audit/leak-store.ts +217 -0
  219. package/src/audit/macos-notifier.ts +310 -0
  220. package/src/audit/redact-example.ts +266 -14
  221. package/src/audit/report-harm.ts +12 -1
  222. package/src/audit/report.ts +6 -3
  223. package/src/audit/schedule-cli.ts +37 -0
  224. package/src/audit/scoring.ts +49 -0
  225. package/src/audit/types.ts +41 -0
  226. package/src/hooks/builtin-policies.ts +67 -2
  227. package/src/hooks/configure-wizard.ts +20 -0
  228. package/src/hooks/fp-config.ts +71 -11
  229. package/src/hooks/fp-home.ts +27 -0
  230. package/src/hooks/handler.ts +59 -1
  231. package/src/hooks/integrations.ts +47 -13
  232. package/src/hooks/notice.ts +155 -0
  233. package/src/hooks/uninstall-cli.ts +15 -0
  234. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1b5jzxr._.js +0 -3
  235. package/.next/standalone/.next/server/chunks/ssr/_0j6gd_k._.js +0 -23
  236. package/.next/standalone/.next/server/chunks/ssr/_0v8eebd._.js +0 -3
  237. package/.next/standalone/.next/server/chunks/ssr/_1ylae7o._.js +0 -3
  238. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +0 -3
  239. package/.next/standalone/.next/server/chunks/ssr/node_modules_html-to-image_es_index_1ao30b1.js +0 -3
  240. package/.next/standalone/.next/static/chunks/03slu0c8_71y7.js +0 -1
  241. package/.next/standalone/.next/static/chunks/0nm11ysn0153h.js +0 -1
  242. package/.next/standalone/.next/static/chunks/0u64yey446k4f.css +0 -1
  243. package/.next/standalone/.next/static/chunks/1r9676mkvihto.js +0 -1
  244. package/.next/standalone/.next/static/chunks/30pct5s734jua.js +0 -6
  245. package/.next/standalone/.next/static/chunks/3pibxnbhjza7n.js +0 -69
  246. package/.next/standalone/.next/static/chunks/3qfoeqhy5l10x.js +0 -1
  247. package/.next/standalone/.next/static/chunks/3zkg2s2vzxc3d.js +0 -1
  248. package/.next/standalone/.next/static/chunks/41b5ptaocvi_l.js +0 -1
  249. /package/.next/standalone/.next/static/{JBYgkXkA1PS48m9P_hKNw → aKNv4Kmv98Xpwns0Xa-C-}/_buildManifest.js +0 -0
  250. /package/.next/standalone/.next/static/{JBYgkXkA1PS48m9P_hKNw → aKNv4Kmv98Xpwns0Xa-C-}/_clientMiddlewareManifest.js +0 -0
  251. /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")