agent-learning-kit 0.1.0__py3-none-any.whl

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 (642) hide show
  1. agent_learning_kit-0.1.0.dist-info/METADATA +381 -0
  2. agent_learning_kit-0.1.0.dist-info/RECORD +642 -0
  3. agent_learning_kit-0.1.0.dist-info/WHEEL +4 -0
  4. agent_learning_kit-0.1.0.dist-info/entry_points.txt +5 -0
  5. agent_learning_kit-0.1.0.dist-info/licenses/LICENSE +173 -0
  6. agent_learning_kit-0.1.0.dist-info/licenses/NOTICE +7 -0
  7. fi/__init__.py +5 -0
  8. fi/alk/__init__.py +57 -0
  9. fi/alk/_facade.py +31 -0
  10. fi/alk/_module_alias.py +68 -0
  11. fi/alk/_paths.py +14 -0
  12. fi/alk/_schema.py +522 -0
  13. fi/alk/actions.py +727 -0
  14. fi/alk/bench/__init__.py +517 -0
  15. fi/alk/bench/_codeexec.py +213 -0
  16. fi/alk/bench/_coding.py +215 -0
  17. fi/alk/bench/_docker.py +237 -0
  18. fi/alk/bench/_grader.py +286 -0
  19. fi/alk/bench/_pull.py +212 -0
  20. fi/alk/bench/_voice.py +147 -0
  21. fi/alk/capabilities.py +627 -0
  22. fi/alk/cli.py +6396 -0
  23. fi/alk/config.py +130 -0
  24. fi/alk/cua_loop.py +562 -0
  25. fi/alk/evals.py +2351 -0
  26. fi/alk/extensions.py +163 -0
  27. fi/alk/harness/ARCHITECTURE.md +231 -0
  28. fi/alk/harness/DESIGN.md +246 -0
  29. fi/alk/harness/ENVIRONMENT_CONFORMANCE.md +127 -0
  30. fi/alk/harness/HOW-IT-WORKS.md +297 -0
  31. fi/alk/harness/IMPLEMENTATION_AND_VALIDATION_STATUS.md +229 -0
  32. fi/alk/harness/README.md +417 -0
  33. fi/alk/harness/__init__.py +77 -0
  34. fi/alk/harness/__main__.py +3 -0
  35. fi/alk/harness/amend.py +312 -0
  36. fi/alk/harness/artifacts.py +319 -0
  37. fi/alk/harness/authoring_entrypoint.py +189 -0
  38. fi/alk/harness/authoring_runtime_validation.py +267 -0
  39. fi/alk/harness/backends/README.md +43 -0
  40. fi/alk/harness/backends/__init__.py +122 -0
  41. fi/alk/harness/backends/base.py +241 -0
  42. fi/alk/harness/backends/claude.py +211 -0
  43. fi/alk/harness/backends/files.py +182 -0
  44. fi/alk/harness/backends/vertex_gemini.py +457 -0
  45. fi/alk/harness/background_noise.py +95 -0
  46. fi/alk/harness/build.py +385 -0
  47. fi/alk/harness/bundle.py +593 -0
  48. fi/alk/harness/bundle_author_v2.py +1831 -0
  49. fi/alk/harness/bundle_v2.py +719 -0
  50. fi/alk/harness/call_runner.py +1440 -0
  51. fi/alk/harness/callback_http_adapter.py +111 -0
  52. fi/alk/harness/catalogue.py +287 -0
  53. fi/alk/harness/chat.py +428 -0
  54. fi/alk/harness/chat_call_runner.py +506 -0
  55. fi/alk/harness/checks.py +136 -0
  56. fi/alk/harness/cli.py +1354 -0
  57. fi/alk/harness/config.py +338 -0
  58. fi/alk/harness/contract.py +718 -0
  59. fi/alk/harness/credentials.py +674 -0
  60. fi/alk/harness/data/persona_vocabulary.json +111 -0
  61. fi/alk/harness/environment.py +99 -0
  62. fi/alk/harness/environment_plan.py +168 -0
  63. fi/alk/harness/events.py +125 -0
  64. fi/alk/harness/executor.py +304 -0
  65. fi/alk/harness/folder.py +234 -0
  66. fi/alk/harness/generated_runtime.py +815 -0
  67. fi/alk/harness/github.py +72 -0
  68. fi/alk/harness/hosted_authoring_entrypoint.py +183 -0
  69. fi/alk/harness/hosted_entrypoint.py +2402 -0
  70. fi/alk/harness/hosted_scheduler.py +2218 -0
  71. fi/alk/harness/job.py +426 -0
  72. fi/alk/harness/judge.py +184 -0
  73. fi/alk/harness/livekit_source.py +50 -0
  74. fi/alk/harness/livekit_tool_trace_bootstrap.py +71 -0
  75. fi/alk/harness/observability.py +208 -0
  76. fi/alk/harness/outbound.py +3252 -0
  77. fi/alk/harness/packaging.py +515 -0
  78. fi/alk/harness/persona_guides.py +157 -0
  79. fi/alk/harness/platform.py +692 -0
  80. fi/alk/harness/process_preflight.py +764 -0
  81. fi/alk/harness/process_runtime.py +5670 -0
  82. fi/alk/harness/prove.py +425 -0
  83. fi/alk/harness/provider_import.py +703 -0
  84. fi/alk/harness/provider_lifecycle.py +392 -0
  85. fi/alk/harness/provision.py +2896 -0
  86. fi/alk/harness/reception.py +147 -0
  87. fi/alk/harness/retell_chat_call_runner.py +373 -0
  88. fi/alk/harness/run/__init__.py +296 -0
  89. fi/alk/harness/run/alk.py +184 -0
  90. fi/alk/harness/run/call.py +162 -0
  91. fi/alk/harness/run/conversation.py +264 -0
  92. fi/alk/harness/run/data/voices_by_language_and_gender.json +693 -0
  93. fi/alk/harness/run/evidence.py +195 -0
  94. fi/alk/harness/run/grade.py +598 -0
  95. fi/alk/harness/run/live.py +297 -0
  96. fi/alk/harness/run/models.py +56 -0
  97. fi/alk/harness/run/platform_evals.py +227 -0
  98. fi/alk/harness/run/sdk_voice.py +130 -0
  99. fi/alk/harness/run/simulation.py +1209 -0
  100. fi/alk/harness/run/stage.py +91 -0
  101. fi/alk/harness/run/targets.py +508 -0
  102. fi/alk/harness/run/tools.py +601 -0
  103. fi/alk/harness/run/voice.py +340 -0
  104. fi/alk/harness/runtime.py +172 -0
  105. fi/alk/harness/sandbox_server.py +2011 -0
  106. fi/alk/harness/sandbox_worker.py +44 -0
  107. fi/alk/harness/scenario.py +1048 -0
  108. fi/alk/harness/scenario_source.py +879 -0
  109. fi/alk/harness/scenario_tools.py +1143 -0
  110. fi/alk/harness/scenarios.py +915 -0
  111. fi/alk/harness/secrets.py +168 -0
  112. fi/alk/harness/service_catalog.py +97 -0
  113. fi/alk/harness/session.py +391 -0
  114. fi/alk/harness/sessions.py +372 -0
  115. fi/alk/harness/simulator.py +76 -0
  116. fi/alk/harness/simulator_voice.py +928 -0
  117. fi/alk/harness/skills/build-environment/SKILL.md +538 -0
  118. fi/alk/harness/skills/harness.md +131 -0
  119. fi/alk/harness/skills/kinds/chat.md +48 -0
  120. fi/alk/harness/skills/kinds/voice-voicemail.md +63 -0
  121. fi/alk/harness/skills/kinds/voice.md +59 -0
  122. fi/alk/harness/skills/plan-suite/SKILL.md +103 -0
  123. fi/alk/harness/skills/provision-environment/SKILL.md +136 -0
  124. fi/alk/harness/skills/run-scenarios/SKILL.md +112 -0
  125. fi/alk/harness/skills/understand-agent/SKILL.md +251 -0
  126. fi/alk/harness/skills/write-scenarios/SKILL.md +606 -0
  127. fi/alk/harness/skills/write-scenarios/references/refusals.md +28 -0
  128. fi/alk/harness/skills/write-scenarios/references/world-api.md +92 -0
  129. fi/alk/harness/source_data_invariants.py +444 -0
  130. fi/alk/harness/source_tool_evidence.py +79 -0
  131. fi/alk/harness/sources.py +253 -0
  132. fi/alk/harness/spend.py +140 -0
  133. fi/alk/harness/tool_trace_proxy.py +104 -0
  134. fi/alk/harness/tools.py +1018 -0
  135. fi/alk/harness/understand.py +169 -0
  136. fi/alk/harness/voicemail_audio.py +74 -0
  137. fi/alk/harness/world/__init__.py +33 -0
  138. fi/alk/harness/world/errors.py +68 -0
  139. fi/alk/harness/world/expectations.py +91 -0
  140. fi/alk/harness/world/handle.py +538 -0
  141. fi/alk/harness/world/kinds.py +196 -0
  142. fi/alk/harness/world/mutate.py +186 -0
  143. fi/alk/harness/world/probe.py +413 -0
  144. fi/alk/harness/world/provision.py +511 -0
  145. fi/alk/harness/world/provisioned.py +191 -0
  146. fi/alk/harness/world/runtime.py +616 -0
  147. fi/alk/harness/world/snapshot.py +288 -0
  148. fi/alk/harness/world/stores/__init__.py +305 -0
  149. fi/alk/harness/world/stores/container.py +215 -0
  150. fi/alk/harness/world/stores/inprocess.py +346 -0
  151. fi/alk/harness/world/stores/postgres.py +481 -0
  152. fi/alk/harness/world/stores/prove.py +202 -0
  153. fi/alk/harness/world/stores/sqlite.py +245 -0
  154. fi/alk/harness/world/stores/written.py +182 -0
  155. fi/alk/harness/world/tools.py +1516 -0
  156. fi/alk/harness/world/workspace.py +144 -0
  157. fi/alk/image_loop.py +453 -0
  158. fi/alk/image_perturb.py +241 -0
  159. fi/alk/improve.py +274 -0
  160. fi/alk/live/__init__.py +154 -0
  161. fi/alk/live/_attribution.py +184 -0
  162. fi/alk/live/_capture.py +264 -0
  163. fi/alk/live/_codec.py +391 -0
  164. fi/alk/live/_contract.py +134 -0
  165. fi/alk/live/_loopback.py +316 -0
  166. fi/alk/live/_perturb.py +449 -0
  167. fi/alk/live/_runner.py +386 -0
  168. fi/alk/live/_stats.py +561 -0
  169. fi/alk/live/_transcript.py +240 -0
  170. fi/alk/live/_workers/__init__.py +9 -0
  171. fi/alk/live/_workers/a2a_worker.py +316 -0
  172. fi/alk/live/_workers/langgraph_worker.py +217 -0
  173. fi/alk/live/_workers/livekit_worker.py +207 -0
  174. fi/alk/live/_workers/mcp_loopback_server.py +46 -0
  175. fi/alk/live/_workers/mcp_worker.py +158 -0
  176. fi/alk/live/_workers/pipecat_worker.py +189 -0
  177. fi/alk/live/a2a_lane.py +138 -0
  178. fi/alk/live/langgraph_lane.py +339 -0
  179. fi/alk/live/livekit_lane.py +376 -0
  180. fi/alk/live/mcp_lane.py +172 -0
  181. fi/alk/live/pipecat_lane.py +341 -0
  182. fi/alk/live/voice_redteam.py +494 -0
  183. fi/alk/loss.py +306 -0
  184. fi/alk/optimize.py +36260 -0
  185. fi/alk/practice/__init__.py +51 -0
  186. fi/alk/practice/_assess.py +103 -0
  187. fi/alk/practice/_budget.py +81 -0
  188. fi/alk/practice/_calibrate.py +69 -0
  189. fi/alk/practice/_capstone.py +86 -0
  190. fi/alk/practice/_contract.py +91 -0
  191. fi/alk/practice/_diagnose.py +79 -0
  192. fi/alk/practice/_drill.py +196 -0
  193. fi/alk/practice/_experiment.py +720 -0
  194. fi/alk/practice/_schedule.py +102 -0
  195. fi/alk/practice/_store.py +194 -0
  196. fi/alk/practice/_trainer.py +245 -0
  197. fi/alk/practice/_update.py +125 -0
  198. fi/alk/redteam.py +2621 -0
  199. fi/alk/rewardhack.py +237 -0
  200. fi/alk/simulate.py +10351 -0
  201. fi/alk/studio/__init__.py +82 -0
  202. fi/alk/studio/_bias.py +314 -0
  203. fi/alk/studio/_calibration.py +522 -0
  204. fi/alk/studio/_coverage.py +262 -0
  205. fi/alk/studio/_download.py +665 -0
  206. fi/alk/studio/_fidelity_attack.py +114 -0
  207. fi/alk/studio/_generate.py +652 -0
  208. fi/alk/studio/_library.py +370 -0
  209. fi/alk/studio/_scan.py +134 -0
  210. fi/alk/studio/_upgrade.py +42 -0
  211. fi/alk/studio/_vendor.py +172 -0
  212. fi/alk/suite.py +4200 -0
  213. fi/alk/tasks.py +828 -0
  214. fi/alk/telemetry/__init__.py +149 -0
  215. fi/alk/telemetry/_contract.py +141 -0
  216. fi/alk/telemetry/_emit.py +182 -0
  217. fi/alk/telemetry/_ledger.py +296 -0
  218. fi/alk/telemetry/_queue.py +127 -0
  219. fi/alk/telemetry/_row.py +294 -0
  220. fi/alk/telemetry/_run.py +233 -0
  221. fi/alk/telemetry/_sync.py +193 -0
  222. fi/alk/telemetry/_url.py +119 -0
  223. fi/alk/trinity.py +49397 -0
  224. fi/alk/voice_loop.py +174 -0
  225. fi/api/__init__.py +1 -0
  226. fi/api/auth.py +137 -0
  227. fi/api/types.py +29 -0
  228. fi/cli/__init__.py +9 -0
  229. fi/cli/assertions/__init__.py +25 -0
  230. fi/cli/assertions/conditions.py +76 -0
  231. fi/cli/assertions/evaluator.py +286 -0
  232. fi/cli/assertions/exit_codes.py +20 -0
  233. fi/cli/assertions/parser.py +131 -0
  234. fi/cli/assertions/reporter.py +194 -0
  235. fi/cli/commands/__init__.py +9 -0
  236. fi/cli/commands/config.py +165 -0
  237. fi/cli/commands/export.py +208 -0
  238. fi/cli/commands/init.py +112 -0
  239. fi/cli/commands/list_cmd.py +213 -0
  240. fi/cli/commands/run.py +486 -0
  241. fi/cli/commands/validate.py +173 -0
  242. fi/cli/commands/view.py +424 -0
  243. fi/cli/config/__init__.py +6 -0
  244. fi/cli/config/defaults.py +206 -0
  245. fi/cli/config/loader.py +155 -0
  246. fi/cli/config/schema.py +174 -0
  247. fi/cli/main.py +78 -0
  248. fi/cli/output/__init__.py +6 -0
  249. fi/cli/output/formatters.py +106 -0
  250. fi/cli/output/reporters.py +46 -0
  251. fi/cli/storage/__init__.py +5 -0
  252. fi/cli/storage/run_history.py +249 -0
  253. fi/cli/utils/__init__.py +5 -0
  254. fi/cli/utils/console.py +44 -0
  255. fi/evals/__init__.py +131 -0
  256. fi/evals/autoeval/__init__.py +137 -0
  257. fi/evals/autoeval/analyzer.py +211 -0
  258. fi/evals/autoeval/config.py +244 -0
  259. fi/evals/autoeval/export.py +213 -0
  260. fi/evals/autoeval/interactive.py +283 -0
  261. fi/evals/autoeval/pipeline.py +625 -0
  262. fi/evals/autoeval/prompts.py +139 -0
  263. fi/evals/autoeval/recommender.py +242 -0
  264. fi/evals/autoeval/rules.py +589 -0
  265. fi/evals/autoeval/templates.py +299 -0
  266. fi/evals/autoeval/types.py +232 -0
  267. fi/evals/core/__init__.py +16 -0
  268. fi/evals/core/cloud_registry.py +184 -0
  269. fi/evals/core/engines.py +368 -0
  270. fi/evals/core/evaluate.py +319 -0
  271. fi/evals/core/judge_prompt.py +90 -0
  272. fi/evals/core/prompt_generator.py +83 -0
  273. fi/evals/core/registry.py +57 -0
  274. fi/evals/core/result.py +55 -0
  275. fi/evals/evaluator.py +721 -0
  276. fi/evals/execution.py +168 -0
  277. fi/evals/feedback/__init__.py +32 -0
  278. fi/evals/feedback/calibrator.py +160 -0
  279. fi/evals/feedback/collector.py +214 -0
  280. fi/evals/feedback/hooks.py +81 -0
  281. fi/evals/feedback/retriever.py +128 -0
  282. fi/evals/feedback/store.py +272 -0
  283. fi/evals/feedback/types.py +99 -0
  284. fi/evals/framework/README.md +79 -0
  285. fi/evals/framework/__init__.py +267 -0
  286. fi/evals/framework/backends/Dockerfile.eval-runner +33 -0
  287. fi/evals/framework/backends/__init__.py +99 -0
  288. fi/evals/framework/backends/_container.py +141 -0
  289. fi/evals/framework/backends/_utils.py +145 -0
  290. fi/evals/framework/backends/base.py +223 -0
  291. fi/evals/framework/backends/celery_backend.py +417 -0
  292. fi/evals/framework/backends/celery_worker.py +78 -0
  293. fi/evals/framework/backends/kubernetes_backend.py +665 -0
  294. fi/evals/framework/backends/ray_backend.py +521 -0
  295. fi/evals/framework/backends/temporal.py +350 -0
  296. fi/evals/framework/backends/temporal_worker.py +126 -0
  297. fi/evals/framework/backends/thread_pool.py +286 -0
  298. fi/evals/framework/context.py +258 -0
  299. fi/evals/framework/enrichment.py +306 -0
  300. fi/evals/framework/evals/__init__.py +68 -0
  301. fi/evals/framework/evals/agentic.py +399 -0
  302. fi/evals/framework/evals/builder.py +609 -0
  303. fi/evals/framework/evals/semantic.py +142 -0
  304. fi/evals/framework/evaluator.py +647 -0
  305. fi/evals/framework/evaluators/__init__.py +22 -0
  306. fi/evals/framework/evaluators/blocking.py +347 -0
  307. fi/evals/framework/evaluators/non_blocking.py +577 -0
  308. fi/evals/framework/propagation.py +421 -0
  309. fi/evals/framework/protocols.py +385 -0
  310. fi/evals/framework/registry.py +370 -0
  311. fi/evals/framework/resilience/__init__.py +150 -0
  312. fi/evals/framework/resilience/circuit_breaker.py +309 -0
  313. fi/evals/framework/resilience/degradation.py +355 -0
  314. fi/evals/framework/resilience/health.py +505 -0
  315. fi/evals/framework/resilience/rate_limiter.py +228 -0
  316. fi/evals/framework/resilience/retry.py +274 -0
  317. fi/evals/framework/resilience/types.py +288 -0
  318. fi/evals/framework/resilience/wrapper.py +433 -0
  319. fi/evals/framework/types.py +218 -0
  320. fi/evals/guardrails/README.md +915 -0
  321. fi/evals/guardrails/__init__.py +96 -0
  322. fi/evals/guardrails/backends/__init__.py +43 -0
  323. fi/evals/guardrails/backends/azure.py +361 -0
  324. fi/evals/guardrails/backends/base.py +88 -0
  325. fi/evals/guardrails/backends/generic_llm.py +163 -0
  326. fi/evals/guardrails/backends/granite.py +216 -0
  327. fi/evals/guardrails/backends/llamaguard.py +221 -0
  328. fi/evals/guardrails/backends/local_base.py +479 -0
  329. fi/evals/guardrails/backends/openai.py +365 -0
  330. fi/evals/guardrails/backends/qwen.py +170 -0
  331. fi/evals/guardrails/backends/shieldgemma.py +154 -0
  332. fi/evals/guardrails/backends/turing.py +235 -0
  333. fi/evals/guardrails/backends/vllm_client.py +321 -0
  334. fi/evals/guardrails/backends/wildguard.py +188 -0
  335. fi/evals/guardrails/base.py +888 -0
  336. fi/evals/guardrails/config.py +221 -0
  337. fi/evals/guardrails/discovery.py +243 -0
  338. fi/evals/guardrails/gateway.py +437 -0
  339. fi/evals/guardrails/registry.py +231 -0
  340. fi/evals/guardrails/scanners/__init__.py +127 -0
  341. fi/evals/guardrails/scanners/base.py +191 -0
  342. fi/evals/guardrails/scanners/code_injection.py +243 -0
  343. fi/evals/guardrails/scanners/eval_delegate.py +574 -0
  344. fi/evals/guardrails/scanners/invisible_chars.py +351 -0
  345. fi/evals/guardrails/scanners/jailbreak.py +412 -0
  346. fi/evals/guardrails/scanners/language.py +288 -0
  347. fi/evals/guardrails/scanners/pipeline.py +260 -0
  348. fi/evals/guardrails/scanners/regex.py +311 -0
  349. fi/evals/guardrails/scanners/secrets.py +274 -0
  350. fi/evals/guardrails/scanners/topics.py +649 -0
  351. fi/evals/guardrails/scanners/urls.py +341 -0
  352. fi/evals/guardrails/types.py +96 -0
  353. fi/evals/llm/__init__.py +3 -0
  354. fi/evals/llm/base_llm_provider.py +35 -0
  355. fi/evals/llm/providers/litellm.py +70 -0
  356. fi/evals/local/__init__.py +90 -0
  357. fi/evals/local/evaluator.py +690 -0
  358. fi/evals/local/execution_mode.py +121 -0
  359. fi/evals/local/llm.py +489 -0
  360. fi/evals/local/metrics/__init__.py +19 -0
  361. fi/evals/local/registry.py +360 -0
  362. fi/evals/manager.py +1018 -0
  363. fi/evals/manager_types.py +362 -0
  364. fi/evals/metrics/__init__.py +185 -0
  365. fi/evals/metrics/agents/__init__.py +74 -0
  366. fi/evals/metrics/agents/metrics.py +693 -0
  367. fi/evals/metrics/agents/report.py +36463 -0
  368. fi/evals/metrics/agents/types.py +160 -0
  369. fi/evals/metrics/base_llm_metric.py +111 -0
  370. fi/evals/metrics/base_metric.py +138 -0
  371. fi/evals/metrics/code_security/__init__.py +305 -0
  372. fi/evals/metrics/code_security/analyzer.py +985 -0
  373. fi/evals/metrics/code_security/benchmarks/__init__.py +73 -0
  374. fi/evals/metrics/code_security/benchmarks/builtin.py +750 -0
  375. fi/evals/metrics/code_security/benchmarks/loader.py +580 -0
  376. fi/evals/metrics/code_security/benchmarks/types.py +308 -0
  377. fi/evals/metrics/code_security/detectors/__init__.py +186 -0
  378. fi/evals/metrics/code_security/detectors/base.py +394 -0
  379. fi/evals/metrics/code_security/detectors/cryptography.py +345 -0
  380. fi/evals/metrics/code_security/detectors/injection.py +744 -0
  381. fi/evals/metrics/code_security/detectors/secrets.py +287 -0
  382. fi/evals/metrics/code_security/detectors/serialization.py +192 -0
  383. fi/evals/metrics/code_security/joint_metrics.py +588 -0
  384. fi/evals/metrics/code_security/judges/__init__.py +83 -0
  385. fi/evals/metrics/code_security/judges/base.py +238 -0
  386. fi/evals/metrics/code_security/judges/dual_judge.py +534 -0
  387. fi/evals/metrics/code_security/judges/llm_judge.py +301 -0
  388. fi/evals/metrics/code_security/judges/pattern_judge.py +515 -0
  389. fi/evals/metrics/code_security/metrics.py +388 -0
  390. fi/evals/metrics/code_security/modes/__init__.py +63 -0
  391. fi/evals/metrics/code_security/modes/adversarial.py +284 -0
  392. fi/evals/metrics/code_security/modes/autocomplete.py +198 -0
  393. fi/evals/metrics/code_security/modes/base.py +283 -0
  394. fi/evals/metrics/code_security/modes/instruct.py +253 -0
  395. fi/evals/metrics/code_security/modes/repair.py +230 -0
  396. fi/evals/metrics/code_security/reports/__init__.py +57 -0
  397. fi/evals/metrics/code_security/reports/generator.py +404 -0
  398. fi/evals/metrics/code_security/reports/leaderboard.py +509 -0
  399. fi/evals/metrics/code_security/types.py +534 -0
  400. fi/evals/metrics/function_calling/__init__.py +34 -0
  401. fi/evals/metrics/function_calling/metrics.py +573 -0
  402. fi/evals/metrics/function_calling/types.py +87 -0
  403. fi/evals/metrics/hallucination/__init__.py +54 -0
  404. fi/evals/metrics/hallucination/detector.py +149 -0
  405. fi/evals/metrics/hallucination/metrics.py +390 -0
  406. fi/evals/metrics/hallucination/nli.py +253 -0
  407. fi/evals/metrics/hallucination/sentinel.py +106 -0
  408. fi/evals/metrics/hallucination/types.py +132 -0
  409. fi/evals/metrics/heuristics/aggregation_metrics.py +85 -0
  410. fi/evals/metrics/heuristics/json_metrics.py +87 -0
  411. fi/evals/metrics/heuristics/similarity_metrics.py +375 -0
  412. fi/evals/metrics/heuristics/string_metrics.py +391 -0
  413. fi/evals/metrics/llm_as_judges/__init__.py +17 -0
  414. fi/evals/metrics/llm_as_judges/custom_judge/metric.py +112 -0
  415. fi/evals/metrics/llm_as_judges/custom_judge/prompts.py +26 -0
  416. fi/evals/metrics/llm_as_judges/types.py +48 -0
  417. fi/evals/metrics/rag/__init__.py +111 -0
  418. fi/evals/metrics/rag/advanced/__init__.py +14 -0
  419. fi/evals/metrics/rag/advanced/multi_hop.py +283 -0
  420. fi/evals/metrics/rag/advanced/source_attribution.py +344 -0
  421. fi/evals/metrics/rag/generation/__init__.py +17 -0
  422. fi/evals/metrics/rag/generation/answer_relevancy.py +176 -0
  423. fi/evals/metrics/rag/generation/context_utilization.py +245 -0
  424. fi/evals/metrics/rag/generation/faithfulness.py +241 -0
  425. fi/evals/metrics/rag/generation/groundedness.py +131 -0
  426. fi/evals/metrics/rag/rag_score.py +277 -0
  427. fi/evals/metrics/rag/retrieval/__init__.py +20 -0
  428. fi/evals/metrics/rag/retrieval/context_entity_recall.py +124 -0
  429. fi/evals/metrics/rag/retrieval/context_precision.py +158 -0
  430. fi/evals/metrics/rag/retrieval/context_recall.py +106 -0
  431. fi/evals/metrics/rag/retrieval/noise_sensitivity.py +163 -0
  432. fi/evals/metrics/rag/retrieval/ranking.py +261 -0
  433. fi/evals/metrics/rag/types.py +100 -0
  434. fi/evals/metrics/rag/utils/__init__.py +62 -0
  435. fi/evals/metrics/rag/utils/claims.py +189 -0
  436. fi/evals/metrics/rag/utils/entities.py +244 -0
  437. fi/evals/metrics/rag/utils/nli.py +92 -0
  438. fi/evals/metrics/rag/utils/similarity.py +345 -0
  439. fi/evals/metrics/structured/__init__.py +114 -0
  440. fi/evals/metrics/structured/field_completeness.py +313 -0
  441. fi/evals/metrics/structured/hierarchy_score.py +366 -0
  442. fi/evals/metrics/structured/json_validation.py +190 -0
  443. fi/evals/metrics/structured/schema_compliance.py +280 -0
  444. fi/evals/metrics/structured/structured_output_score.py +298 -0
  445. fi/evals/metrics/structured/types.py +108 -0
  446. fi/evals/metrics/structured/validators/__init__.py +30 -0
  447. fi/evals/metrics/structured/validators/base.py +189 -0
  448. fi/evals/metrics/structured/validators/json_validator.py +196 -0
  449. fi/evals/metrics/structured/validators/pydantic_validator.py +178 -0
  450. fi/evals/metrics/structured/validators/yaml_validator.py +248 -0
  451. fi/evals/otel/__init__.py +266 -0
  452. fi/evals/otel/config.py +400 -0
  453. fi/evals/otel/conventions.py +463 -0
  454. fi/evals/otel/enrichment.py +371 -0
  455. fi/evals/otel/instrumentors/__init__.py +140 -0
  456. fi/evals/otel/instrumentors/anthropic.py +517 -0
  457. fi/evals/otel/instrumentors/base.py +382 -0
  458. fi/evals/otel/instrumentors/openai.py +673 -0
  459. fi/evals/otel/processors/__init__.py +36 -0
  460. fi/evals/otel/processors/base.py +473 -0
  461. fi/evals/otel/processors/cost.py +445 -0
  462. fi/evals/otel/processors/evaluation.py +559 -0
  463. fi/evals/otel/processors/llm.py +462 -0
  464. fi/evals/otel/tracer.py +506 -0
  465. fi/evals/otel/types.py +232 -0
  466. fi/evals/otel_utils.py +23 -0
  467. fi/evals/protect.py +671 -0
  468. fi/evals/protect_input_adapter.py +154 -0
  469. fi/evals/streaming/__init__.py +88 -0
  470. fi/evals/streaming/buffer.py +213 -0
  471. fi/evals/streaming/evaluator.py +551 -0
  472. fi/evals/streaming/policy.py +307 -0
  473. fi/evals/streaming/scorers.py +368 -0
  474. fi/evals/streaming/types.py +238 -0
  475. fi/evals/templates.py +472 -0
  476. fi/evals/types.py +156 -0
  477. fi/opt/__init__.py +221 -0
  478. fi/opt/_objective_scoring.py +85 -0
  479. fi/opt/base/__init__.py +11 -0
  480. fi/opt/base/base_generator.py +33 -0
  481. fi/opt/base/base_mapper.py +26 -0
  482. fi/opt/base/base_optimizer.py +45 -0
  483. fi/opt/base/evaluator.py +211 -0
  484. fi/opt/components.py +3095 -0
  485. fi/opt/datamappers/__init__.py +3 -0
  486. fi/opt/datamappers/basic_mapper.py +40 -0
  487. fi/opt/deployment.py +1021 -0
  488. fi/opt/evidence.py +4332 -0
  489. fi/opt/generators/__init__.py +3 -0
  490. fi/opt/generators/litellm.py +66 -0
  491. fi/opt/integrations/__init__.py +23 -0
  492. fi/opt/integrations/generative_suite.py +410 -0
  493. fi/opt/integrations/simulate.py +1313 -0
  494. fi/opt/mutations.py +771 -0
  495. fi/opt/observability.py +4639 -0
  496. fi/opt/optimizer_trace.py +889 -0
  497. fi/opt/optimizers/__init__.py +80 -0
  498. fi/opt/optimizers/agent.py +331 -0
  499. fi/opt/optimizers/agent_bandit.py +392 -0
  500. fi/opt/optimizers/agent_curriculum.py +635 -0
  501. fi/opt/optimizers/agent_evolution.py +894 -0
  502. fi/opt/optimizers/agent_feedback.py +1863 -0
  503. fi/opt/optimizers/agent_pareto.py +547 -0
  504. fi/opt/optimizers/agent_social_memory.py +1113 -0
  505. fi/opt/optimizers/agent_tpe.py +321 -0
  506. fi/opt/optimizers/bayesian_search.py +449 -0
  507. fi/opt/optimizers/council.py +2075 -0
  508. fi/opt/optimizers/futureagi_replay.py +799 -0
  509. fi/opt/optimizers/gepa.py +322 -0
  510. fi/opt/optimizers/metaprompt.py +243 -0
  511. fi/opt/optimizers/promptwizard.py +417 -0
  512. fi/opt/optimizers/protegi.py +329 -0
  513. fi/opt/optimizers/random_search.py +224 -0
  514. fi/opt/research.py +518 -0
  515. fi/opt/simulation.py +260 -0
  516. fi/opt/targets.py +232 -0
  517. fi/opt/types.py +66 -0
  518. fi/opt/utils/__init__.py +4 -0
  519. fi/opt/utils/early_stopping.py +266 -0
  520. fi/opt/utils/setup_logging.py +82 -0
  521. fi/simulate/__init__.py +540 -0
  522. fi/simulate/_hashing.py +35 -0
  523. fi/simulate/_logging.py +10 -0
  524. fi/simulate/adapters.py +87 -0
  525. fi/simulate/agent/__init__.py +120 -0
  526. fi/simulate/agent/browser.py +658 -0
  527. fi/simulate/agent/definition.py +587 -0
  528. fi/simulate/agent/frameworks.py +3528 -0
  529. fi/simulate/agent/generic.py +8286 -0
  530. fi/simulate/agent/import_probe.py +227 -0
  531. fi/simulate/agent/memory.py +905 -0
  532. fi/simulate/agent/mocks.py +101 -0
  533. fi/simulate/agent/multi_agent.py +361 -0
  534. fi/simulate/agent/orchestration.py +903 -0
  535. fi/simulate/agent/realtime.py +665 -0
  536. fi/simulate/agent/wrapper.py +99 -0
  537. fi/simulate/agent/wrappers/__init__.py +18 -0
  538. fi/simulate/agent/wrappers/anthropic.py +62 -0
  539. fi/simulate/agent/wrappers/gemini.py +65 -0
  540. fi/simulate/agent/wrappers/http.py +404 -0
  541. fi/simulate/agent/wrappers/langchain.py +80 -0
  542. fi/simulate/agent/wrappers/openai.py +75 -0
  543. fi/simulate/agent/wrappers/websocket.py +326 -0
  544. fi/simulate/artifacts/__init__.py +11 -0
  545. fi/simulate/artifacts/manifest.py +62 -0
  546. fi/simulate/cli.py +20560 -0
  547. fi/simulate/endpoints/__init__.py +45 -0
  548. fi/simulate/endpoints/_http_actor.py +73 -0
  549. fi/simulate/endpoints/actor_sources.py +243 -0
  550. fi/simulate/endpoints/base.py +107 -0
  551. fi/simulate/endpoints/builtins.py +10 -0
  552. fi/simulate/endpoints/callable.py +95 -0
  553. fi/simulate/endpoints/http.py +76 -0
  554. fi/simulate/endpoints/livekit.py +138 -0
  555. fi/simulate/endpoints/originators.py +132 -0
  556. fi/simulate/endpoints/profiles.py +348 -0
  557. fi/simulate/endpoints/retell.py +633 -0
  558. fi/simulate/endpoints/vapi.py +205 -0
  559. fi/simulate/endpoints/websocket.py +76 -0
  560. fi/simulate/environment.py +33026 -0
  561. fi/simulate/environments/__init__.py +11 -0
  562. fi/simulate/environments/base.py +73 -0
  563. fi/simulate/environments/chat.py +697 -0
  564. fi/simulate/environments/voice.py +212 -0
  565. fi/simulate/evaluation/__init__.py +4 -0
  566. fi/simulate/evaluation/ai_eval.py +227 -0
  567. fi/simulate/evidence/__init__.py +35 -0
  568. fi/simulate/evidence/base.py +59 -0
  569. fi/simulate/evidence/caller_observed.py +50 -0
  570. fi/simulate/evidence/livekit_instrumentation.py +51 -0
  571. fi/simulate/evidence/livekit_room.py +50 -0
  572. fi/simulate/evidence/otel.py +49 -0
  573. fi/simulate/evidence/providers/__init__.py +24 -0
  574. fi/simulate/evidence/providers/base.py +61 -0
  575. fi/simulate/evidence/providers/retell.py +376 -0
  576. fi/simulate/evidence/providers/vapi.py +426 -0
  577. fi/simulate/hosted/__init__.py +32 -0
  578. fi/simulate/hosted/child_entrypoint.py +306 -0
  579. fi/simulate/hosted/job.py +150 -0
  580. fi/simulate/hosted/targets.py +53 -0
  581. fi/simulate/instrumentation/__init__.py +5 -0
  582. fi/simulate/instrumentation/livekit/__init__.py +122 -0
  583. fi/simulate/manifest.py +1033 -0
  584. fi/simulate/matrix_cli.py +165 -0
  585. fi/simulate/realtime/__init__.py +40 -0
  586. fi/simulate/realtime/events.py +107 -0
  587. fi/simulate/realtime/media.py +61 -0
  588. fi/simulate/realtime/session.py +91 -0
  589. fi/simulate/recording/__init__.py +5 -0
  590. fi/simulate/recording/room_recorder.py +326 -0
  591. fi/simulate/registry.py +185 -0
  592. fi/simulate/results/__init__.py +9 -0
  593. fi/simulate/results/base.py +18 -0
  594. fi/simulate/results/filesystem.py +71 -0
  595. fi/simulate/results/futureagi.py +1340 -0
  596. fi/simulate/runtime/__init__.py +85 -0
  597. fi/simulate/runtime/capabilities.py +40 -0
  598. fi/simulate/runtime/events.py +63 -0
  599. fi/simulate/runtime/failures.py +25 -0
  600. fi/simulate/runtime/ids.py +34 -0
  601. fi/simulate/runtime/plan.py +70 -0
  602. fi/simulate/runtime/planner.py +102 -0
  603. fi/simulate/runtime/report.py +174 -0
  604. fi/simulate/runtime/run.py +75 -0
  605. fi/simulate/runtime/runner.py +333 -0
  606. fi/simulate/runtime/spec.py +186 -0
  607. fi/simulate/simulation/__init__.py +30 -0
  608. fi/simulate/simulation/behavior_policy.py +425 -0
  609. fi/simulate/simulation/bridge/__init__.py +9 -0
  610. fi/simulate/simulation/bridge/audio.py +29 -0
  611. fi/simulate/simulation/bridge/connector.py +46 -0
  612. fi/simulate/simulation/bridge/livekit.py +252 -0
  613. fi/simulate/simulation/bridge/retell.py +188 -0
  614. fi/simulate/simulation/bridge/vapi.py +177 -0
  615. fi/simulate/simulation/contract.py +419 -0
  616. fi/simulate/simulation/engines/__init__.py +12 -0
  617. fi/simulate/simulation/engines/base.py +21 -0
  618. fi/simulate/simulation/engines/cloud.py +517 -0
  619. fi/simulate/simulation/engines/livekit.py +4167 -0
  620. fi/simulate/simulation/engines/local_text.py +89 -0
  621. fi/simulate/simulation/fidelity.py +374 -0
  622. fi/simulate/simulation/gemini_tts_stream.py +110 -0
  623. fi/simulate/simulation/generator.py +91 -0
  624. fi/simulate/simulation/goal_machine.py +185 -0
  625. fi/simulate/simulation/livekit_models.py +467 -0
  626. fi/simulate/simulation/matrix.py +170 -0
  627. fi/simulate/simulation/models.py +279 -0
  628. fi/simulate/simulation/runner.py +153 -0
  629. fi/simulate/simulation/synthetic.py +880 -0
  630. fi/simulate/simulation/voice_prompt.py +502 -0
  631. fi/simulate/simulator/__init__.py +55 -0
  632. fi/simulate/simulator/builtins.py +53 -0
  633. fi/simulate/suite.py +1288 -0
  634. fi/simulate/utils/routes.py +164 -0
  635. fi/simulate/voice.py +225 -0
  636. fi/simulate/voice_cli.py +182 -0
  637. fi/utils/__init__.py +1 -0
  638. fi/utils/constants.py +14 -0
  639. fi/utils/errors.py +200 -0
  640. fi/utils/executor.py +26 -0
  641. fi/utils/routes.py +119 -0
  642. fi/utils/utils.py +17 -0
@@ -0,0 +1,593 @@
1
+ """Portable, immutable environment bundles shared by local and hosted ALK.
2
+
3
+ The harness may discover an environment in a repository or generate missing pieces, but every
4
+ runtime receives the same sealed representation. A bundle contains no resolved customer
5
+ secrets. Runtime credentials are injected from ``SecretRef`` values when a job starts.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import hashlib
11
+ import json
12
+ import os
13
+ import re
14
+ import shutil
15
+ import tempfile
16
+ from enum import Enum
17
+ from pathlib import Path, PurePosixPath
18
+ from typing import Any
19
+
20
+ from pydantic import BaseModel, Field, JsonValue, model_validator
21
+
22
+ BUNDLE_SCHEMA_VERSION = "futureagi.environment-bundle.v1"
23
+ BUNDLE_MANIFEST = "manifest.json"
24
+
25
+
26
+ class BundleError(RuntimeError):
27
+ """A bundle is unsafe, incomplete, or no longer matches its digest."""
28
+
29
+
30
+ class RuntimeKind(str, Enum):
31
+ COMPOSE = "compose"
32
+ PROCESS = "process"
33
+ EXTERNAL = "external"
34
+
35
+
36
+ class CapabilityProtocol(str, Enum):
37
+ HTTP = "http"
38
+ MCP = "mcp"
39
+ POSTGRES = "postgres"
40
+ SQLITE = "sqlite"
41
+ LIVEKIT = "livekit"
42
+ TCP = "tcp"
43
+ CLICKHOUSE = "clickhouse"
44
+ REDIS = "redis"
45
+ MONGODB = "mongodb"
46
+ MYSQL = "mysql"
47
+ AMQP = "amqp"
48
+ KAFKA = "kafka"
49
+ NATS = "nats"
50
+ S3 = "s3"
51
+ GRPC = "grpc"
52
+ BOLT = "bolt"
53
+
54
+
55
+ class BundleRuntime(BaseModel):
56
+ kind: RuntimeKind
57
+ document: str | None = None
58
+ control_service: str | None = None
59
+ command: list[str] = Field(default_factory=list)
60
+
61
+ @model_validator(mode="after")
62
+ def _has_entrypoint(self) -> "BundleRuntime":
63
+ if self.kind is RuntimeKind.COMPOSE and not self.document:
64
+ raise ValueError("compose runtime requires a document")
65
+ if self.kind is RuntimeKind.PROCESS and not self.command:
66
+ raise ValueError("process runtime requires a command")
67
+ if self.document:
68
+ _safe_relative(self.document)
69
+ return self
70
+
71
+
72
+ class Capability(BaseModel):
73
+ protocol: CapabilityProtocol
74
+ service: str | None = None
75
+ container_port: int | None = Field(default=None, ge=1, le=65535)
76
+ path: str | None = None
77
+ configuration_name: str | None = None
78
+ metadata: dict[str, JsonValue] = Field(default_factory=dict)
79
+
80
+
81
+ class ReadinessProbe(BaseModel):
82
+ capability: str
83
+ path: str | None = None
84
+ timeout_seconds: float = Field(default=120.0, gt=0, le=1800)
85
+ interval_seconds: float = Field(default=1.0, gt=0, le=60)
86
+
87
+
88
+ class BundleProvenance(BaseModel):
89
+ source_kind: str
90
+ repository: str | None = None
91
+ commit: str | None = None
92
+ source_digest: str
93
+ generator: str = "fi.alk.harness"
94
+ generator_version: str = "1"
95
+ adopted_files: list[str] = Field(default_factory=list)
96
+ generated_files: list[str] = Field(default_factory=list)
97
+
98
+
99
+ class BundleFile(BaseModel):
100
+ path: str
101
+ sha256: str
102
+ size: int = Field(ge=0)
103
+
104
+ @model_validator(mode="after")
105
+ def _valid_path(self) -> "BundleFile":
106
+ _safe_relative(self.path)
107
+ return self
108
+
109
+
110
+ class EnvironmentBundle(BaseModel):
111
+ schema_version: str = BUNDLE_SCHEMA_VERSION
112
+ name: str
113
+ digest: str
114
+ runtime: BundleRuntime
115
+ services: list[str] = Field(default_factory=list)
116
+ capabilities: dict[str, Capability] = Field(default_factory=dict)
117
+ readiness: list[ReadinessProbe] = Field(default_factory=list)
118
+ provenance: BundleProvenance
119
+ files: list[BundleFile] = Field(default_factory=list)
120
+ metadata: dict[str, JsonValue] = Field(default_factory=dict)
121
+
122
+ @model_validator(mode="after")
123
+ def _validate_manifest(self) -> "EnvironmentBundle":
124
+ if self.schema_version != BUNDLE_SCHEMA_VERSION:
125
+ raise ValueError(f"bundle_schema_unsupported: {self.schema_version}")
126
+ if not re.fullmatch(r"sha256:[0-9a-f]{64}", self.digest):
127
+ raise ValueError("bundle_digest_invalid")
128
+ missing = [
129
+ p.capability
130
+ for p in self.readiness
131
+ if p.capability not in self.capabilities
132
+ ]
133
+ if missing:
134
+ raise ValueError(
135
+ "readiness_capability_missing: " + ", ".join(sorted(set(missing)))
136
+ )
137
+ _reject_secret_values(self.model_dump(exclude={"digest"}))
138
+ return self
139
+
140
+
141
+ _SECRET_FIELD = re.compile(
142
+ r"(^|_)(api_?key|api_?secret|authorization|credential|password|private_?key|secret|token)(_|$)",
143
+ re.IGNORECASE,
144
+ )
145
+ _SECRET_FILES = {".env", ".env.local", ".npmrc", ".pypirc", "credentials", "id_rsa"}
146
+ _SECRET_CONTENT = (
147
+ re.compile(rb"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----"),
148
+ re.compile(rb"\b(?:ghp|github_pat)_[A-Za-z0-9_]{20,}\b"),
149
+ re.compile(rb"\bAKIA[0-9A-Z]{16}\b"),
150
+ re.compile(rb"\bsk-[A-Za-z0-9_-]{20,}\b"),
151
+ )
152
+
153
+
154
+ def _safe_relative(value: str) -> PurePosixPath:
155
+ path = PurePosixPath(value)
156
+ if path.is_absolute() or not value or ".." in path.parts:
157
+ raise ValueError(f"bundle_path_unsafe: {value}")
158
+ return path
159
+
160
+
161
+ def _reject_secret_values(value: Any, path: tuple[str, ...] = ()) -> None:
162
+ if isinstance(value, dict):
163
+ for key, item in value.items():
164
+ current = (*path, str(key))
165
+ # References and configuration variable names are identifiers, never values.
166
+ if str(key) in {"secret_refs", "configuration_name"}:
167
+ continue
168
+ if _SECRET_FIELD.search(str(key)) and item not in (None, "", {}, []):
169
+ raise ValueError("resolved_secret_forbidden: " + ".".join(current))
170
+ _reject_secret_values(item, current)
171
+ elif isinstance(value, list):
172
+ for index, item in enumerate(value):
173
+ _reject_secret_values(item, (*path, str(index)))
174
+
175
+
176
+ def _iter_files(root: Path) -> list[Path]:
177
+ files: list[Path] = []
178
+ for path in sorted(root.rglob("*")):
179
+ if path.name == BUNDLE_MANIFEST or path.is_dir():
180
+ continue
181
+ if path.is_symlink():
182
+ raise BundleError(f"bundle_symlink_forbidden: {path.relative_to(root)}")
183
+ if not path.is_file():
184
+ raise BundleError(f"bundle_entry_unsupported: {path.relative_to(root)}")
185
+ files.append(path)
186
+ return files
187
+
188
+
189
+ def _inspect_file(root: Path, path: Path) -> BundleFile:
190
+ relative = path.relative_to(root).as_posix()
191
+ _safe_relative(relative)
192
+ if path.name in _SECRET_FILES or path.suffix.lower() in {
193
+ ".pem",
194
+ ".key",
195
+ ".p12",
196
+ ".pfx",
197
+ }:
198
+ raise BundleError(f"bundle_secret_file_forbidden: {relative}")
199
+ digest = hashlib.sha256()
200
+ size = 0
201
+ with path.open("rb") as stream:
202
+ while chunk := stream.read(1024 * 1024):
203
+ size += len(chunk)
204
+ digest.update(chunk)
205
+ if any(pattern.search(chunk) for pattern in _SECRET_CONTENT):
206
+ raise BundleError(f"bundle_secret_material_detected: {relative}")
207
+ return BundleFile(path=relative, sha256=digest.hexdigest(), size=size)
208
+
209
+
210
+ def _bundle_digest(manifest: dict[str, Any], files: list[BundleFile]) -> str:
211
+ core = dict(manifest)
212
+ core.pop("digest", None)
213
+ core.pop("files", None)
214
+ digest = hashlib.sha256(
215
+ json.dumps(
216
+ core, sort_keys=True, separators=(",", ":"), ensure_ascii=False
217
+ ).encode()
218
+ )
219
+ for record in files:
220
+ encoded = record.model_dump_json().encode()
221
+ digest.update(len(encoded).to_bytes(8, "big"))
222
+ digest.update(encoded)
223
+ return "sha256:" + digest.hexdigest()
224
+
225
+
226
+ def seal_bundle(
227
+ root: str | Path, manifest: EnvironmentBundle | dict[str, Any]
228
+ ) -> EnvironmentBundle:
229
+ """Validate and seal a prepared bundle directory with a content digest.
230
+
231
+ The operation is idempotent: sealing unchanged contents produces the same manifest and
232
+ digest. ``manifest.json`` is replaced atomically only after all validation succeeds.
233
+ """
234
+ root = Path(root).expanduser().resolve()
235
+ if not root.is_dir():
236
+ raise BundleError(f"bundle_root_missing: {root}")
237
+ raw = (
238
+ manifest.model_dump(mode="json")
239
+ if isinstance(manifest, EnvironmentBundle)
240
+ else dict(manifest)
241
+ )
242
+ raw.setdefault("schema_version", BUNDLE_SCHEMA_VERSION)
243
+ raw["digest"] = "sha256:" + "0" * 64
244
+ raw["files"] = []
245
+ files = [_inspect_file(root, path) for path in _iter_files(root)]
246
+ raw["files"] = [record.model_dump(mode="json") for record in files]
247
+ # Hash the fully normalized representation. Optional/default fields must participate in the
248
+ # same way both before and after a JSON round trip or verification will disagree with seal.
249
+ normalized = EnvironmentBundle.model_validate(raw).model_dump(mode="json")
250
+ normalized["digest"] = _bundle_digest(normalized, files)
251
+ sealed = EnvironmentBundle.model_validate(normalized)
252
+ target = root / BUNDLE_MANIFEST
253
+ temporary = root / f".{BUNDLE_MANIFEST}.tmp-{os.getpid()}"
254
+ temporary.write_text(sealed.model_dump_json(indent=2) + "\n", encoding="utf-8")
255
+ temporary.replace(target)
256
+ return sealed
257
+
258
+
259
+ def load_bundle(root: str | Path, *, verify: bool = True) -> EnvironmentBundle:
260
+ root = Path(root).expanduser().resolve()
261
+ target = root / BUNDLE_MANIFEST
262
+ if not target.is_file():
263
+ raise BundleError(f"bundle_manifest_missing: {target}")
264
+ try:
265
+ bundle = EnvironmentBundle.model_validate_json(
266
+ target.read_text(encoding="utf-8")
267
+ )
268
+ except (OSError, ValueError) as exc:
269
+ raise BundleError(f"bundle_manifest_invalid: {exc}") from exc
270
+ if verify:
271
+ actual = [_inspect_file(root, path) for path in _iter_files(root)]
272
+ expected = [item.model_dump() for item in bundle.files]
273
+ if [item.model_dump() for item in actual] != expected:
274
+ raise BundleError("bundle_files_changed")
275
+ digest = _bundle_digest(bundle.model_dump(mode="json"), actual)
276
+ if digest != bundle.digest:
277
+ raise BundleError("bundle_digest_mismatch")
278
+ return bundle
279
+
280
+
281
+ _COPY_IGNORED = {
282
+ ".git",
283
+ ".mypy_cache",
284
+ ".pytest_cache",
285
+ ".ruff_cache",
286
+ ".venv",
287
+ "__pycache__",
288
+ "artifacts",
289
+ "build",
290
+ "dist",
291
+ "node_modules",
292
+ }
293
+
294
+
295
+ def _copy_generated_runtime_context(
296
+ session: Path, staging: Path, generated: list[str]
297
+ ) -> Path | None:
298
+ source = session / "generated-runtime-context"
299
+ if not source.is_dir():
300
+ return None
301
+ target = staging / "services" / "generated-runtime"
302
+ shutil.copytree(source, target)
303
+ for path in sorted(target.rglob("*")):
304
+ if path.is_file():
305
+ generated.append(path.relative_to(staging).as_posix())
306
+ plan = session / "generated-runtime.json"
307
+ if plan.is_file():
308
+ try:
309
+ portable_plan = json.loads(plan.read_text(encoding="utf-8"))
310
+ except (OSError, json.JSONDecodeError) as exc:
311
+ raise BundleError(f"bundle_generated_runtime_plan_invalid: {exc}") from exc
312
+ portable_plan["context_directory"] = "services/generated-runtime"
313
+ (staging / plan.name).write_text(
314
+ json.dumps(portable_plan, indent=2), encoding="utf-8"
315
+ )
316
+ generated.append(plan.name)
317
+ return target
318
+
319
+
320
+ def _portable_managed_compose(
321
+ compose: Path,
322
+ *,
323
+ source: Path,
324
+ session: Path,
325
+ generated_context: Path | None,
326
+ ) -> dict[str, Any]:
327
+ """Rewrite harness-generated host paths to paths inside the sealed bundle."""
328
+ try:
329
+ document = json.loads(compose.read_text(encoding="utf-8"))
330
+ except (OSError, json.JSONDecodeError) as exc:
331
+ raise BundleError(f"bundle_managed_compose_invalid: {exc}") from exc
332
+ generated_source = session / "generated-runtime-context"
333
+
334
+ def portable_path(raw: str) -> str:
335
+ value = Path(raw)
336
+ resolved = (
337
+ value.resolve()
338
+ if value.is_absolute()
339
+ else (compose.parent / value).resolve()
340
+ )
341
+ try:
342
+ relative = resolved.relative_to(source)
343
+ except ValueError:
344
+ relative = None
345
+ if relative is not None:
346
+ return (Path("services/source") / relative).as_posix()
347
+ if generated_context is not None and resolved == generated_source.resolve():
348
+ return "services/generated-runtime"
349
+ raise BundleError(f"bundle_build_or_mount_path_outside_submission: {raw}")
350
+
351
+ services = document.get("services", {}) if isinstance(document, dict) else {}
352
+ for service in services.values() if isinstance(services, dict) else []:
353
+ if not isinstance(service, dict):
354
+ continue
355
+ build = service.get("build")
356
+ if isinstance(build, str):
357
+ service["build"] = portable_path(build)
358
+ elif isinstance(build, dict) and build.get("context"):
359
+ build["context"] = portable_path(str(build["context"]))
360
+ volumes = service.get("volumes", [])
361
+ rewritten: list[Any] = []
362
+ for volume in volumes if isinstance(volumes, list) else []:
363
+ if isinstance(volume, str):
364
+ parts = volume.split(":", 2)
365
+ if parts and Path(parts[0]).is_absolute():
366
+ parts[0] = portable_path(parts[0])
367
+ rewritten.append(":".join(parts))
368
+ elif isinstance(volume, dict) and volume.get("type") == "bind":
369
+ item = dict(volume)
370
+ item["source"] = portable_path(str(item.get("source", "")))
371
+ rewritten.append(item)
372
+ else:
373
+ rewritten.append(volume)
374
+ if isinstance(volumes, list):
375
+ service["volumes"] = rewritten
376
+ return document
377
+
378
+
379
+ def export_session_bundle(
380
+ source: str | Path,
381
+ session: str | Path,
382
+ *,
383
+ name: str,
384
+ ) -> tuple[Path, EnvironmentBundle]:
385
+ """Export the environment discovered/generated for a harness session.
386
+
387
+ The source is copied into the internal bundle because Compose build contexts must survive
388
+ deletion of a hosted executor's repository clone. Customer dotenv files, VCS metadata,
389
+ caches, and prior artifacts are excluded. The secret scanner then validates every retained
390
+ byte before the staging directory is atomically promoted.
391
+ """
392
+ from .provision import ProvisionedEnvironment, compose_file, source_fingerprint
393
+
394
+ source = Path(source).expanduser().resolve()
395
+ session = Path(session).expanduser().resolve()
396
+ if not source.is_dir():
397
+ raise BundleError(f"bundle_source_missing: {source}")
398
+ session.mkdir(parents=True, exist_ok=True)
399
+ final = session / "environment-bundle"
400
+ staging_parent = Path(tempfile.mkdtemp(prefix=".bundle-", dir=session))
401
+ staging = staging_parent / "environment-bundle"
402
+ staged_source = staging / "services" / "source"
403
+ adopted: list[str] = []
404
+ generated: list[str] = []
405
+ try:
406
+ for path in sorted(source.rglob("*")):
407
+ relative = path.relative_to(source)
408
+ if any(part in _COPY_IGNORED for part in relative.parts):
409
+ continue
410
+ if (
411
+ path.name in _SECRET_FILES
412
+ or path.name.startswith(".env.")
413
+ and path.name != ".env.example"
414
+ ):
415
+ continue
416
+ target = staged_source / relative
417
+ if path.is_symlink():
418
+ raise BundleError(f"bundle_source_symlink_forbidden: {relative}")
419
+ if path.is_dir():
420
+ target.mkdir(parents=True, exist_ok=True)
421
+ elif path.is_file():
422
+ target.parent.mkdir(parents=True, exist_ok=True)
423
+ shutil.copy2(path, target)
424
+ adopted.append((Path("services/source") / relative).as_posix())
425
+
426
+ for artifact in (
427
+ "contract.json",
428
+ "sub_goals.json",
429
+ "simulator_prompt.md",
430
+ "world.json",
431
+ "world.py",
432
+ ):
433
+ origin = session / artifact
434
+ if not origin.is_file():
435
+ continue
436
+ target = staging / artifact
437
+ target.parent.mkdir(parents=True, exist_ok=True)
438
+ shutil.copy2(origin, target)
439
+ generated.append(artifact)
440
+
441
+ provisioned = ProvisionedEnvironment.load(session)
442
+ generated_context = (
443
+ _copy_generated_runtime_context(session, staging, generated)
444
+ if provisioned and provisioned.generated_runtime_plan
445
+ else None
446
+ )
447
+ services = list(provisioned.services) if provisioned else []
448
+ capabilities: dict[str, Capability] = {}
449
+ readiness: list[dict[str, Any]] = []
450
+ if provisioned:
451
+ configured = set(provisioned.overrides)
452
+ for endpoint in provisioned.service_endpoints:
453
+ key = (
454
+ f"{endpoint['service']}_{endpoint['kind']}_"
455
+ f"{endpoint['container_port']}"
456
+ ).replace("-", "_")
457
+ names = [
458
+ name
459
+ for name in endpoint.get("configuration_names", [])
460
+ if name in configured
461
+ ]
462
+ capabilities[key] = Capability(
463
+ protocol=CapabilityProtocol(str(endpoint["protocol"])),
464
+ service=str(endpoint["service"]),
465
+ container_port=int(endpoint["container_port"]),
466
+ configuration_name=names[0] if len(names) == 1 else None,
467
+ metadata={"kind": str(endpoint["kind"])},
468
+ )
469
+ readiness.append(
470
+ {
471
+ "capability": key,
472
+ "path": str(endpoint.get("readiness_path") or "") or None,
473
+ }
474
+ )
475
+ compose = compose_file(source)
476
+ if compose is None and provisioned and provisioned.compose_file:
477
+ compose = Path(provisioned.compose_file)
478
+ if compose is None:
479
+ runtime = BundleRuntime(kind=RuntimeKind.EXTERNAL)
480
+ else:
481
+ try:
482
+ document = (
483
+ Path("services/source") / compose.relative_to(source)
484
+ ).as_posix()
485
+ except ValueError:
486
+ # Managed Compose is generated into the session, not the repository.
487
+ document = "compose.json"
488
+ portable = _portable_managed_compose(
489
+ compose,
490
+ source=source,
491
+ session=session,
492
+ generated_context=generated_context,
493
+ )
494
+ (staging / document).write_text(
495
+ json.dumps(portable, indent=2), encoding="utf-8"
496
+ )
497
+ generated.append(document)
498
+ runtime = BundleRuntime(kind=RuntimeKind.COMPOSE, document=document)
499
+
500
+ repository = source.name
501
+ commit = None
502
+ job_path = session / "job.json"
503
+ if job_path.is_file():
504
+ try:
505
+ saved_job = json.loads(job_path.read_text(encoding="utf-8"))
506
+ saved_source = saved_job.get("source") or {}
507
+ repository = str(saved_source.get("repository") or repository)
508
+ commit = saved_source.get("commit_sha") or None
509
+ except (OSError, json.JSONDecodeError, AttributeError):
510
+ # The source digest still gives exact provenance for legacy/local sessions.
511
+ pass
512
+
513
+ raw = {
514
+ "schema_version": BUNDLE_SCHEMA_VERSION,
515
+ "name": name,
516
+ "digest": "sha256:" + "0" * 64,
517
+ "runtime": runtime.model_dump(mode="json"),
518
+ "services": services,
519
+ "capabilities": {
520
+ key: value.model_dump(mode="json", exclude_none=True)
521
+ for key, value in capabilities.items()
522
+ },
523
+ "readiness": readiness,
524
+ "provenance": {
525
+ "source_kind": "repository",
526
+ "repository": repository,
527
+ "commit": commit,
528
+ "source_digest": source_fingerprint(source),
529
+ "adopted_files": adopted,
530
+ "generated_files": generated,
531
+ },
532
+ }
533
+ # Freeze the provisioning decision inside the sealed payload. This plan excludes
534
+ # ephemeral ports/project names and resolved values, so equal admitted inputs produce
535
+ # an equal plan digest across retries and execution providers.
536
+ from .environment_plan import (
537
+ ENVIRONMENT_PLAN_FILE,
538
+ PlanSource,
539
+ create_environment_plan,
540
+ write_environment_plan,
541
+ )
542
+
543
+ plan = create_environment_plan(
544
+ source=PlanSource(
545
+ kind="repository",
546
+ repository=repository,
547
+ commit_sha=commit,
548
+ source_digest=str(raw["provenance"]["source_digest"]),
549
+ ),
550
+ runtime=runtime,
551
+ services=services,
552
+ capabilities=capabilities,
553
+ readiness=[ReadinessProbe.model_validate(item) for item in readiness],
554
+ metadata={
555
+ "managed": bool(provisioned and provisioned.managed),
556
+ "generated_runtime_fingerprint": (
557
+ provisioned.runtime_fingerprint if provisioned else ""
558
+ ),
559
+ },
560
+ )
561
+ write_environment_plan(staging, plan)
562
+ generated.append(ENVIRONMENT_PLAN_FILE)
563
+ raw["provenance"]["generated_files"] = generated
564
+ raw["metadata"] = {"environment_plan_digest": plan.digest}
565
+ seal_bundle(staging, raw)
566
+ if final.exists():
567
+ shutil.rmtree(final)
568
+ staging.replace(final)
569
+ return final, load_bundle(final)
570
+ except Exception:
571
+ shutil.rmtree(staging_parent, ignore_errors=True)
572
+ raise
573
+ finally:
574
+ if staging_parent.exists():
575
+ shutil.rmtree(staging_parent, ignore_errors=True)
576
+
577
+
578
+ __all__ = [
579
+ "BUNDLE_MANIFEST",
580
+ "BUNDLE_SCHEMA_VERSION",
581
+ "BundleError",
582
+ "BundleFile",
583
+ "BundleProvenance",
584
+ "BundleRuntime",
585
+ "Capability",
586
+ "CapabilityProtocol",
587
+ "EnvironmentBundle",
588
+ "ReadinessProbe",
589
+ "RuntimeKind",
590
+ "load_bundle",
591
+ "export_session_bundle",
592
+ "seal_bundle",
593
+ ]