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,764 @@
1
+ """The §2e pre-provision checklist — `hosted-execution-seams.md` v1.9 — as a single gate the
2
+ in-sandbox provisioner runs before starting anything.
3
+
4
+ `bundle_v2.py` validates everything decidable from the manifest's own field values alone; this
5
+ module covers what its docstring names as deferred: the bundle's actual files on disk (digest and
6
+ per-file hashes, symlinks, path escapes, secret content), the pydantic `extra="forbid"` ->
7
+ `unknown_field` translation, and every rule that needs the job the bundle will run under
8
+ (placeholder vocabulary, secret purposes against the job's `secret_refs`, the `depends_on` graph,
9
+ the engine catalog, `seed_missing`, `inputs_digest` verification, reserved-name content scanning,
10
+ `no_sql_store`, and resource sanity). `seed_strategy_unsupported`, `sentinel_shape_mismatch`,
11
+ `capability_unresolved`, `configuration_name_duplicate`, `user_assignment_invalid`, and
12
+ `capability_engine_mismatch` are already enforced by the model layer and are not repeated here.
13
+
14
+ A missing interpreter (§0, v1.7) is a BUILD-time failure, not a preflight one — no manifest field
15
+ carries an interpreter demand, so this module has nothing to check and does not attempt to.
16
+
17
+ `preflight_bundle` runs the checklist in the contract's own order and raises on the first
18
+ violation, never a crash — every failure is a `PreflightError` carrying a code from §2e's
19
+ failure-code table (v1.7). The caller (the provisioner) is responsible for mapping that into a
20
+ FAILED terminal state with `FailureDomain.ENVIRONMENT` in `HarnessStage.VALIDATING_ENVIRONMENT`,
21
+ per §2e's closing rule.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import hashlib
27
+ import json
28
+ import re
29
+ from pathlib import Path, PurePosixPath
30
+
31
+ from pydantic import ValidationError
32
+
33
+ from .artifacts import _SECRET_CONTENT, _SECRET_FILES
34
+ from .bundle import CapabilityProtocol
35
+ from .bundle_v2 import (
36
+ BUNDLE_V2_MANIFEST,
37
+ BUNDLE_V2_SCHEMA_VERSION,
38
+ BundleFileV2,
39
+ EnvironmentBundleV2,
40
+ ManagedEngine,
41
+ ManagedProcess,
42
+ RuntimeKindV2,
43
+ SecretPurpose,
44
+ SourceProcess,
45
+ compute_inputs_digest,
46
+ seal_bundle_v2,
47
+ )
48
+ from .provider_import import ProviderImportSpec
49
+ from .provider_lifecycle import ProviderLifecycleSpec, ProviderScope
50
+
51
+ _SECRET_PURPOSE_VALUES = {member.value for member in SecretPurpose}
52
+
53
+
54
+ class PreflightError(RuntimeError):
55
+ """A §2e checklist rule rejected the bundle.
56
+
57
+ ``code`` is one of §2e's failure-code table (v1.9): "contract-rule" codes, each named by a
58
+ numbered checklist item's prose, and "mechanical" codes for plumbing failures the contract
59
+ describes but does not formalize as a rule (a missing bundle file, an out-of-range
60
+ ``parallelism``). Every code this module raises is in that table — including
61
+ ``fixed_port_reserved`` (F11, p5-round1-review; added to the table by v1.9), which guards
62
+ against a `fixed_port` aliasing the provisioner's own port-formula bands.
63
+ """
64
+
65
+ def __init__(self, code: str, message: str) -> None:
66
+ self.code = code
67
+ self.message = message
68
+ super().__init__(f"{code}: {message}")
69
+
70
+
71
+ _SECRET_SUFFIXES = {".pem", ".key", ".p12", ".pfx"}
72
+
73
+ # §2b's catalog table. `ManagedEngine` already closes which *engines* exist; this closes which
74
+ # *version* of each is the one the snapshot actually ships.
75
+ _ENGINE_CATALOG_VERSION: dict[ManagedEngine, str] = {
76
+ ManagedEngine.POSTGRES: "16",
77
+ ManagedEngine.REDIS: "7",
78
+ ManagedEngine.RABBITMQ: "3.13",
79
+ }
80
+
81
+ # §0 (v1.7): a repo needing an interpreter the snapshot lacks fails at BUILD time, reported
82
+ # `runtime_unsupported` there — not here. The manifest carries no interpreter-demand field (the
83
+ # source tree isn't embedded in the bundle, so preflight can't see `.python-version`/`engines`
84
+ # even if it wanted to), so this module has no interpreter check to run.
85
+
86
+ # §2c: migrations/seeds must not create these — checked as a source-content scan, not a manifest
87
+ # field, since the identifier lives inside SQL/scripts the model layer never parses.
88
+ # `re.IGNORECASE`: postgres folds an unquoted identifier to lower case, so `CREATE TABLE
89
+ # _ALK_CONFORMANCE` creates the reserved table under its lower-case name — case-insensitive
90
+ # matching is the only way to catch that (F9, p4-round1-review). This is slightly over-broad for
91
+ # redis/rabbitmq, whose names are case-sensitive, but over-broad on a reserved-name check is the
92
+ # safe direction. Known false-positive surface, left as-is (documented rather than fixed): the scan
93
+ # reads whole file bytes with no lexical awareness beyond stripped `--`/`/* */` comments below, so
94
+ # a quoted string literal containing the reserved name (e.g. as inserted *data*) still trips it.
95
+ # The stripping below is a false-NEGATIVE surface in the opposite direction, equally lexer-free and
96
+ # equally left as-is: a `--` or `/*` inside a string literal (not a comment) deletes real content
97
+ # up to the next line-end or `*/`, which can delete a reserved-name definition that follows it on
98
+ # the same statement (N7, p4-round2-review).
99
+ _RESERVED_NAME = "_alk_conformance"
100
+ _RESERVED_NAME_PATTERN = re.compile(
101
+ r"(?<![A-Za-z0-9_])" + re.escape(_RESERVED_NAME) + r"(?![A-Za-z0-9_])",
102
+ re.IGNORECASE,
103
+ )
104
+ _SQL_LINE_COMMENT = re.compile(r"--[^\n]*")
105
+ _SQL_BLOCK_COMMENT = re.compile(r"/\*.*?\*/", re.DOTALL)
106
+
107
+ # §2b closed placeholder vocabulary.
108
+ _PLACEHOLDER = re.compile(r"\{\{([^{}]+)\}\}")
109
+ _FIXED_PLACEHOLDERS = {"JOB_ID", "WORLD_INDEX", "WORLD_DIR", "DB_NAME"}
110
+ _NAMED_PLACEHOLDER = re.compile(r"^(PORT|HOST)_(.+)$")
111
+
112
+ # §2a: Dockerfile-style install lines requiring root privileges have no process-copy equivalent —
113
+ # the provisioner never runs as root and never will (§0's guest is unprivileged throughout).
114
+ _ROOT_BUILD_COMMANDS = {
115
+ "apt-get",
116
+ "apt",
117
+ "apt-cache",
118
+ "dpkg",
119
+ "yum",
120
+ "dnf",
121
+ "apk",
122
+ "pacman",
123
+ "sudo",
124
+ }
125
+
126
+ _MAX_PROCESSES = 100
127
+ _MIN_PARALLELISM = 1
128
+ _MAX_PARALLELISM = 8
129
+
130
+ # §2b's own port formulas (`process_runtime.plan_ports`): job-shared `14000 + ordinal`
131
+ # (ordinal <= 99, §2e item 7's process cap) and per-world `15000 + 100*world_index + ordinal`
132
+ # (world_index <= 7, §1's parallelism cap). A `fixed_port` landing inside either band can alias a
133
+ # formula port the provisioner is about to hand to a *different* process — F11, p5-round1-review.
134
+ # `fixed_port` forces W=1, so the collision surface is small, but the failure mode is a bind
135
+ # error inside a customer process, not a bundle rejection, which is strictly worse. Mirrored here
136
+ # rather than imported from `process_runtime.py`: preflight has no business depending on the
137
+ # execution module, and both bands are fixed by the contract, not by any runtime state.
138
+ _JOB_SHARED_PORT_BAND = range(14000, 14100)
139
+ _PER_WORLD_PORT_BAND = range(15000, 15800)
140
+
141
+ # `process_runtime.py`'s own `_rabbitmq_management_port` formula (`amqp_port + 10000`) —
142
+ # mirrored here for the same reason as the two bands above: preflight has no business depending
143
+ # on the execution module. The rabbitmq catalog entry supports `datadir_copy` only (no
144
+ # `template_database`), so its amqp port is always drawn from the PER-WORLD band in practice
145
+ # today; the job-shared shift is reserved too, defensively, since the formula itself is generic
146
+ # and nothing about this band's math depends on which base band it is applied to.
147
+ _RABBITMQ_MANAGEMENT_PORT_OFFSET = 10000
148
+ _JOB_SHARED_RABBITMQ_MANAGEMENT_BAND = range(
149
+ _JOB_SHARED_PORT_BAND.start + _RABBITMQ_MANAGEMENT_PORT_OFFSET,
150
+ _JOB_SHARED_PORT_BAND.stop + _RABBITMQ_MANAGEMENT_PORT_OFFSET,
151
+ )
152
+ _PER_WORLD_RABBITMQ_MANAGEMENT_BAND = range(
153
+ _PER_WORLD_PORT_BAND.start + _RABBITMQ_MANAGEMENT_PORT_OFFSET,
154
+ _PER_WORLD_PORT_BAND.stop + _RABBITMQ_MANAGEMENT_PORT_OFFSET,
155
+ )
156
+
157
+
158
+ def preflight_bundle(
159
+ bundle_dir: Path,
160
+ manifest: EnvironmentBundleV2,
161
+ *,
162
+ parallelism: int,
163
+ secret_refs: dict[str, str],
164
+ ) -> None:
165
+ """Run the complete §2e checklist against a sealed v2 bundle directory, in the contract's own
166
+ numbered order. Raises ``PreflightError`` on the first violation; returns ``None`` when clean.
167
+
168
+ ``manifest`` is the already-parsed model the caller obtained from ``load_bundle_v2`` — item 4
169
+ (the pydantic ``extra_forbidden`` -> ``unknown_field`` translation bundle_v2's own docstring
170
+ defers here) is implemented by re-validating the bytes on disk, which is also where this
171
+ function's own read of ``manifest.json`` for step 1 comes from; a caller that already trusts
172
+ ``manifest`` still gets a genuine check that the file backing it hasn't drifted since.
173
+
174
+ ``secret_refs`` maps each job secret alias to its ``SecretRef.purpose`` value (§1) — item 5's
175
+ ``secret_unclaimed``/``secret_missing`` pair needs it and the contract's own entrypoint
176
+ signature (§2e's charter) does not carry it. Required, not optional: §4's provider port hands
177
+ the provisioner ``work_directory``, and `/work/job.json` is readable from it, so every real
178
+ caller has the job's resolved refs — there is no legitimate caller that cannot supply this.
179
+ Pass ``{}`` explicitly for a job with no secret refs at all, rather than omitting the argument:
180
+ an optional default silently both under- and over-enforced the check it exists for (F2,
181
+ p4-round1-review), which required-and-explicit closes. Every value must be a ``SecretPurpose``
182
+ value; anything else raises ``ValueError`` immediately, before any bundle content is checked.
183
+ """
184
+ bundle_dir = Path(bundle_dir)
185
+ for alias, purpose in secret_refs.items():
186
+ # `isinstance` first: §1's raw `agent.secret_refs` shape is `{alias: {manager, key,
187
+ # version, purpose}}`, a dict — an unhashable value would otherwise raise TypeError against
188
+ # the `in` check below instead of the ValueError this docstring promises (N8, p4-round2-
189
+ # review).
190
+ if not isinstance(purpose, str) or purpose not in _SECRET_PURPOSE_VALUES:
191
+ raise ValueError(
192
+ f"secret_refs[{alias!r}] = {purpose!r} is not a SecretPurpose value"
193
+ )
194
+
195
+ if manifest.runtime.kind is RuntimeKindV2.COMPOSE:
196
+ # §2a: "a hosted job with kind: compose fails preflight" — not one of §2e's seven numbered
197
+ # items, so ahead of item 1 rather than slotted between them: every item below assumes
198
+ # v2's processes/seed shape, which a compose bundle need not carry, and a compose bundle's
199
+ # own files (its document, e.g.) carry no obligation to be exhaustively listed in files[]
200
+ # the way a hosted bundle's do — checking file-listing first mis-reported that case as
201
+ # bundle_file_unlisted instead of compose_not_hosted (N1, p4-round2-review).
202
+ raise PreflightError(
203
+ "compose_not_hosted", "kind: compose is not a legal hosted runtime"
204
+ )
205
+
206
+ files = _verify_digest(bundle_dir, manifest) # 1
207
+ walked_files = _verify_path_safety(bundle_dir, files) # 2
208
+ _scan_bundle_files_for_secrets(bundle_dir, walked_files) # 3
209
+ _verify_unknown_fields(bundle_dir, manifest) # 4
210
+
211
+ if manifest.runtime.kind is RuntimeKindV2.PROCESS:
212
+ _verify_placeholder_vocabulary(manifest) # 5
213
+ _verify_no_root_build_commands(manifest) # 5 (§2a)
214
+ _verify_secret_purposes(manifest, secret_refs) # 5
215
+ _verify_provider_lifecycle(manifest) # 5
216
+ _verify_provider_import(manifest) # 5
217
+ _verify_depends_on(manifest) # 5
218
+ _verify_engine_catalog(manifest) # 5
219
+ _verify_fixed_port_not_reserved(manifest) # 5 / §2b
220
+ _verify_seed_missing(manifest) # 5 / §2c
221
+ _verify_reserved_names(bundle_dir, manifest) # 5
222
+ _verify_seed_files_on_disk_and_listed(bundle_dir, manifest, files) # 5
223
+
224
+ _verify_no_sql_store(manifest) # 6
225
+ _verify_resource_sanity(manifest, parallelism=parallelism) # 7
226
+
227
+
228
+ # --- item 1: digest verification -------------------------------------------------------------
229
+
230
+
231
+ def _verify_digest(
232
+ bundle_dir: Path, manifest: EnvironmentBundleV2
233
+ ) -> list[BundleFileV2]:
234
+ root = bundle_dir.resolve()
235
+ try:
236
+ raw = json.loads((root / BUNDLE_V2_MANIFEST).read_text(encoding="utf-8"))
237
+ except (OSError, json.JSONDecodeError) as exc:
238
+ raise PreflightError("bundle_manifest_invalid", str(exc)) from exc
239
+ on_disk_schema_version = (
240
+ raw.get("schema_version") if isinstance(raw, dict) else None
241
+ )
242
+ if on_disk_schema_version != BUNDLE_V2_SCHEMA_VERSION:
243
+ # §2e item 1 opens with "schema_version is …bundle.v2" — checked here, at item 1,
244
+ # rather than left to surface three items late through item 4's re-validation fallback
245
+ # (F11, p4-round1-review).
246
+ raise PreflightError("bundle_schema_unsupported", str(on_disk_schema_version))
247
+ for record in manifest.files:
248
+ path = root / record.path
249
+ if not path.is_file():
250
+ raise PreflightError("bundle_file_missing", record.path)
251
+ digest = hashlib.sha256()
252
+ size = 0
253
+ with path.open("rb") as stream:
254
+ while chunk := stream.read(1024 * 1024):
255
+ size += len(chunk)
256
+ digest.update(chunk)
257
+ if digest.hexdigest() != record.sha256 or size != record.size:
258
+ raise PreflightError("bundle_file_changed", record.path)
259
+ recomputed = seal_bundle_v2(manifest)
260
+ if recomputed != manifest.digest:
261
+ raise PreflightError(
262
+ "bundle_digest_mismatch",
263
+ f"expected {manifest.digest}, computed {recomputed}",
264
+ )
265
+ return manifest.files
266
+
267
+
268
+ # --- item 2: path safety on the filesystem itself ----------------------------------------------
269
+
270
+
271
+ def _verify_path_safety(bundle_dir: Path, files: list[BundleFileV2]) -> list[Path]:
272
+ """The model already rejects unsafe strings in `files[].path` (`_safe_relative`); this walks
273
+ the actual filesystem, which a string check cannot: a symlinked directory can make an
274
+ innocent-looking relative path resolve outside the bundle root.
275
+
276
+ Every non-directory entry except the bundle root's own `manifest.json` must be recorded in
277
+ `files[]` (`bundle_file_unlisted`) — a file physically present but never listed was invisible
278
+ to both the digest check above and the secret scan that follows, which is exactly what let an
279
+ unlisted `.env` through undetected (F1, p4-round1-review). The `manifest.json` exemption is by
280
+ exact root path, not by basename (F10, p4-round1-review): a nested `db/manifest.json` gets no
281
+ special treatment, only `bundle_dir/manifest.json` itself. The exemption covers only the
282
+ listing check, not the symlink check — a symlinked root `manifest.json` would otherwise be
283
+ waved through here and then read straight through by `_verify_digest`/`_verify_unknown_fields`,
284
+ the very item whose job is to stop path escapes (N3, p4-round2-review).
285
+
286
+ Returns the walked file paths so the secret scan (item 3) can run against what the filesystem
287
+ actually contains rather than against `files[]` again.
288
+ """
289
+ root = bundle_dir.resolve()
290
+ manifest_path = root / BUNDLE_V2_MANIFEST
291
+ listed = {record.path for record in files}
292
+ walked: list[Path] = []
293
+ for entry in root.rglob("*"):
294
+ if entry.is_symlink():
295
+ raise PreflightError(
296
+ "bundle_symlink_forbidden", str(entry.relative_to(root))
297
+ )
298
+ if entry == manifest_path:
299
+ continue
300
+ if entry.is_dir():
301
+ continue
302
+ relative = entry.relative_to(root).as_posix()
303
+ if relative not in listed:
304
+ raise PreflightError("bundle_file_unlisted", relative)
305
+ walked.append(entry)
306
+ return walked
307
+
308
+
309
+ # --- item 3: secret material in the bundle's own files ------------------------------------------
310
+
311
+
312
+ def _scan_bundle_files_for_secrets(bundle_dir: Path, walked_files: list[Path]) -> None:
313
+ """Reuses `artifacts.py`'s own file-name and content secret scan unchanged — the same
314
+ high-entropy-token regexes and credential-file-name set this codebase already applies to
315
+ sealed run artifacts, applied here to a sealed bundle's files instead.
316
+
317
+ Scoped to every file item 2's filesystem walk actually found, not to `files[]` (F1,
318
+ p4-round1-review) — an unlisted secret file is already rejected by item 2's own
319
+ `bundle_file_unlisted` check, but this scan must not depend on that running first to be
320
+ correct on its own terms.
321
+ """
322
+ root = bundle_dir.resolve()
323
+ for path in walked_files:
324
+ relative = path.relative_to(root).as_posix()
325
+ posix_path = PurePosixPath(relative)
326
+ if (
327
+ posix_path.name in _SECRET_FILES
328
+ or posix_path.suffix.lower() in _SECRET_SUFFIXES
329
+ ):
330
+ raise PreflightError(
331
+ "secret_in_bundle", f"{relative}: forbidden secret-shaped file"
332
+ )
333
+ with path.open("rb") as stream:
334
+ while chunk := stream.read(1024 * 1024):
335
+ if any(pattern.search(chunk) for pattern in _SECRET_CONTENT):
336
+ raise PreflightError(
337
+ "secret_in_bundle", f"{relative}: high-entropy secret-scan hit"
338
+ )
339
+
340
+
341
+ # --- item 4: unknown-field translation ----------------------------------------------------------
342
+
343
+
344
+ def _verify_unknown_fields(bundle_dir: Path, manifest: EnvironmentBundleV2) -> None:
345
+ target = bundle_dir / BUNDLE_V2_MANIFEST
346
+ try:
347
+ raw = json.loads(target.read_text(encoding="utf-8"))
348
+ except (OSError, json.JSONDecodeError) as exc:
349
+ raise PreflightError("bundle_manifest_invalid", str(exc)) from exc
350
+ try:
351
+ revalidated = EnvironmentBundleV2.model_validate(raw)
352
+ except ValidationError as exc:
353
+ raise _translate_validation_error(exc) from exc
354
+ if revalidated.model_dump(mode="json") != manifest.model_dump(mode="json"):
355
+ # Re-validating catches drift that makes the file *invalid*; it says nothing about drift
356
+ # that leaves it valid (a changed `run_command`, a flipped `user`) unless the two dumps are
357
+ # actually compared (F12, p4-round1-review).
358
+ raise PreflightError(
359
+ "bundle_manifest_drifted",
360
+ "manifest.json on disk no longer matches manifest argument",
361
+ )
362
+
363
+
364
+ def _translate_validation_error(exc: ValidationError) -> PreflightError:
365
+ """§2b: "unknown keys in a process entry are a preflight error (`unknown_field`)" — the model
366
+ layer's docstring defers this exact translation here, since pydantic's own `extra_forbidden`
367
+ carries no contract vocabulary of its own. Every other model-layer rejection already embeds
368
+ its own snake_case code as the leading token of its message (see `bundle_v2.py`'s
369
+ `model_validator`s); that code is preserved rather than collapsed into a generic one.
370
+ """
371
+ for error in exc.errors():
372
+ if error.get("type") == "extra_forbidden":
373
+ location = ".".join(str(part) for part in error["loc"])
374
+ return PreflightError("unknown_field", f"{location}: unknown field")
375
+ # `(?::|$)`, not just `:` (F13, p4-round1-review): a bare code with no trailing detail (e.g.
376
+ # `bundle_digest_invalid`) is the entire message, with nothing after it to require a colon
377
+ # before. Scans every error, not just the first, since pydantic's own ordering is not the
378
+ # contract's priority — the first message that yields a recognizable code wins.
379
+ for error in exc.errors():
380
+ message = str(error.get("msg", ""))
381
+ matched = re.match(r"(?:Value error, )?([a-z][a-z0-9_]*)(?::|$)", message)
382
+ if matched:
383
+ return PreflightError(matched.group(1), message)
384
+ return PreflightError("bundle_manifest_invalid", str(exc))
385
+
386
+
387
+ # --- item 5: everything the model layer needs the job or the files for ------------------------
388
+
389
+
390
+ def _verify_placeholder_vocabulary(manifest: EnvironmentBundleV2) -> None:
391
+ """§2b's closed `{{...}}` vocabulary, checked in `environment`. `build_environment` takes NO
392
+ placeholders at all (§2b) — any `{{...}}` match there is rejected outright, never resolved
393
+ against the vocabulary below (F6, p4-round1-review).
394
+
395
+ `{{<CONFIGURATION_NAME>}}` can only ever resolve to a capability whose `configuration_name`
396
+ is non-null — a capability left null is therefore structurally unreachable by any placeholder,
397
+ which is what makes this scan also enforce §2d's "non-null whenever referenced by any process
398
+ `environment`... entry" without a second pass. When the unmatched token is exactly a declared
399
+ capability's slug and that capability's `configuration_name` is null, the real problem is the
400
+ missing name, not the token — reported `capability_unresolved` naming the capability, rather
401
+ than the generic `unknown_placeholder` every other unmatched token gets (F15, p4-round1-review;
402
+ a deliberate resolution — §2d names no other string a producer could have meant).
403
+ """
404
+ known_names = {process.name for process in manifest.processes}
405
+ known_configuration_names = {
406
+ capability.configuration_name
407
+ for capability in manifest.capabilities.values()
408
+ if capability.configuration_name
409
+ }
410
+ unresolved_capability_slugs = {
411
+ slug
412
+ for slug, capability in manifest.capabilities.items()
413
+ if not capability.configuration_name
414
+ }
415
+ for process in manifest.processes:
416
+ if not isinstance(process, SourceProcess):
417
+ continue
418
+ for key, value in (process.build_environment or {}).items():
419
+ match = _PLACEHOLDER.search(value)
420
+ if match:
421
+ raise PreflightError(
422
+ "unknown_placeholder",
423
+ f"{process.name}.build_environment.{key}: {{{{{match.group(1)}}}}} — "
424
+ "build_environment takes no placeholders",
425
+ )
426
+ for key, value in process.environment.items():
427
+ for match in _PLACEHOLDER.finditer(value):
428
+ token = match.group(1)
429
+ if token in _FIXED_PLACEHOLDERS:
430
+ continue
431
+ named = _NAMED_PLACEHOLDER.match(token)
432
+ if named:
433
+ _, name = named.groups()
434
+ if name in known_names:
435
+ continue
436
+ raise PreflightError(
437
+ "unknown_placeholder",
438
+ f"{process.name}.environment.{key}: {{{{{token}}}}} names an unknown "
439
+ "process",
440
+ )
441
+ if token in known_configuration_names:
442
+ continue
443
+ if token in unresolved_capability_slugs:
444
+ raise PreflightError(
445
+ "capability_unresolved",
446
+ f"{process.name}.environment.{key}: {{{{{token}}}}} names capability "
447
+ f"{token!r}, which has no configuration_name",
448
+ )
449
+ raise PreflightError(
450
+ "unknown_placeholder",
451
+ f"{process.name}.environment.{key}: {{{{{token}}}}} is not in the closed "
452
+ "placeholder vocabulary",
453
+ )
454
+
455
+
456
+ def _verify_no_root_build_commands(manifest: EnvironmentBundleV2) -> None:
457
+ for process in manifest.processes:
458
+ if not isinstance(process, SourceProcess):
459
+ continue
460
+ for step in process.build_commands:
461
+ if step[0] in _ROOT_BUILD_COMMANDS or "sudo" in step:
462
+ raise PreflightError(
463
+ "build_requires_root",
464
+ f"{process.name}: build step {step!r} requires root",
465
+ )
466
+
467
+
468
+ def _verify_secret_purposes(
469
+ manifest: EnvironmentBundleV2, secret_refs: dict[str, str]
470
+ ) -> None:
471
+ """§2b: both directions, scoped to `target_provider` only — `source_checkout` and any other
472
+ gateway-only purpose never crosses into the guest (§0 step 3) and has nothing to claim here."""
473
+ simulator_claimants = [
474
+ process.name
475
+ for process in manifest.processes
476
+ if isinstance(process, SourceProcess)
477
+ and SecretPurpose.SIMULATOR_PROVIDER in process.secret_purposes
478
+ ]
479
+ if simulator_claimants:
480
+ raise PreflightError(
481
+ "secret_purpose_forbidden",
482
+ "customer processes cannot claim simulator_provider credentials: "
483
+ + ", ".join(sorted(simulator_claimants)),
484
+ )
485
+
486
+ ref_has_target_provider = any(
487
+ purpose == SecretPurpose.TARGET_PROVIDER.value
488
+ for purpose in secret_refs.values()
489
+ )
490
+ process_claims_target_provider = any(
491
+ SecretPurpose.TARGET_PROVIDER in process.secret_purposes
492
+ for process in manifest.processes
493
+ if isinstance(process, SourceProcess)
494
+ )
495
+ lifecycle = manifest.metadata.get("provider_lifecycle")
496
+ lifecycle_claims_target_provider = bool(
497
+ isinstance(lifecycle, dict) and lifecycle.get("required_secrets")
498
+ )
499
+ provider_import_claims_target_provider = isinstance(
500
+ manifest.metadata.get("provider_import"), dict
501
+ )
502
+ connect_only_claims_target_provider = isinstance(
503
+ manifest.metadata.get("provider_connect_only"), dict
504
+ )
505
+ guest_claims_target_provider = (
506
+ process_claims_target_provider
507
+ or lifecycle_claims_target_provider
508
+ or provider_import_claims_target_provider
509
+ or connect_only_claims_target_provider
510
+ )
511
+ if ref_has_target_provider and not guest_claims_target_provider:
512
+ raise PreflightError(
513
+ "secret_unclaimed",
514
+ "a target_provider secret ref is not listed by any process",
515
+ )
516
+ if guest_claims_target_provider and not ref_has_target_provider:
517
+ raise PreflightError(
518
+ "secret_missing",
519
+ "a process or provider lifecycle requires target_provider secrets but the job "
520
+ "supplies no such ref",
521
+ )
522
+
523
+
524
+ def _verify_provider_lifecycle(manifest: EnvironmentBundleV2) -> None:
525
+ raw = manifest.metadata.get("provider_lifecycle")
526
+ if raw is None:
527
+ return
528
+ try:
529
+ spec = ProviderLifecycleSpec.model_validate(raw)
530
+ except ValueError as exc:
531
+ raise PreflightError("bundle_manifest_invalid", str(exc)) from exc
532
+ if spec.scope is ProviderScope.ATTEMPT:
533
+ raise PreflightError(
534
+ "bundle_manifest_invalid",
535
+ "attempt-scoped targets require a routing service; use scope: world for now",
536
+ )
537
+ capability = manifest.capabilities.get(spec.public_capability)
538
+ if capability is None or capability.protocol is not CapabilityProtocol.HTTP:
539
+ raise PreflightError(
540
+ "capability_unresolved",
541
+ f"{spec.public_capability!r} must name an HTTP capability",
542
+ )
543
+ process_name = spec.process or manifest.runtime.control_service
544
+ if not any(
545
+ isinstance(process, SourceProcess) and process.name == process_name
546
+ for process in manifest.processes
547
+ ):
548
+ raise PreflightError(
549
+ "service_unresolved",
550
+ f"{process_name!r} must name a source process",
551
+ )
552
+
553
+
554
+ def _verify_provider_import(manifest: EnvironmentBundleV2) -> None:
555
+ raw = manifest.metadata.get("provider_import")
556
+ if raw is None:
557
+ return
558
+ try:
559
+ spec = ProviderImportSpec.model_validate(raw)
560
+ except ValueError as exc:
561
+ raise PreflightError("bundle_manifest_invalid", str(exc)) from exc
562
+ capability = manifest.capabilities.get(spec.public_capability)
563
+ if capability is None or capability.protocol is not CapabilityProtocol.HTTP:
564
+ raise PreflightError(
565
+ "capability_unresolved",
566
+ f"{spec.public_capability!r} must name an HTTP capability",
567
+ )
568
+
569
+
570
+ def _verify_depends_on(manifest: EnvironmentBundleV2) -> None:
571
+ graph = {process.name: list(process.depends_on) for process in manifest.processes}
572
+ for name, deps in graph.items():
573
+ unknown = sorted(dep for dep in deps if dep not in graph)
574
+ if unknown:
575
+ raise PreflightError(
576
+ "depends_on_unresolved",
577
+ f"{name} depends_on unknown process(es): {', '.join(unknown)}",
578
+ )
579
+
580
+ unvisited, in_progress, done = 0, 1, 2
581
+ state = {name: unvisited for name in graph}
582
+
583
+ def visit(name: str, stack: list[str]) -> None:
584
+ state[name] = in_progress
585
+ stack.append(name)
586
+ for dep in graph[name]:
587
+ if state[dep] == in_progress:
588
+ cycle = stack[stack.index(dep) :] + [dep]
589
+ raise PreflightError("depends_on_cycle", " -> ".join(cycle))
590
+ if state[dep] == unvisited:
591
+ visit(dep, stack)
592
+ stack.pop()
593
+ state[name] = done
594
+
595
+ for name in sorted(graph):
596
+ if state[name] == unvisited:
597
+ visit(name, [])
598
+
599
+
600
+ def _verify_engine_catalog(manifest: EnvironmentBundleV2) -> None:
601
+ for process in manifest.processes:
602
+ if not isinstance(process, ManagedProcess):
603
+ continue
604
+ pinned = _ENGINE_CATALOG_VERSION[process.engine]
605
+ if process.version != pinned:
606
+ raise PreflightError(
607
+ "engine_unsupported",
608
+ f"{process.name}: {process.engine.value} {process.version} is not supported; "
609
+ f"the snapshot ships {process.engine.value} {pinned}",
610
+ )
611
+
612
+
613
+ def _verify_fixed_port_not_reserved(manifest: EnvironmentBundleV2) -> None:
614
+ for process in manifest.processes:
615
+ if not isinstance(process, SourceProcess) or process.fixed_port is None:
616
+ continue
617
+ if (
618
+ process.fixed_port in _JOB_SHARED_PORT_BAND
619
+ or process.fixed_port in _PER_WORLD_PORT_BAND
620
+ or process.fixed_port in _JOB_SHARED_RABBITMQ_MANAGEMENT_BAND
621
+ or process.fixed_port in _PER_WORLD_RABBITMQ_MANAGEMENT_BAND
622
+ ):
623
+ raise PreflightError(
624
+ "fixed_port_reserved",
625
+ f"{process.name}: fixed_port {process.fixed_port} falls inside the provisioner's "
626
+ "own port-formula bands (14000-14099 job-shared, 15000-15799 per-world, "
627
+ "24000-24099/25000-25799 rabbitmq management)",
628
+ )
629
+
630
+
631
+ def _verify_seed_missing(manifest: EnvironmentBundleV2) -> None:
632
+ covered = {
633
+ store.capability for store in (manifest.seed.stores if manifest.seed else [])
634
+ }
635
+ missing = sorted(
636
+ slug
637
+ for slug, capability in manifest.capabilities.items()
638
+ if capability.protocol is CapabilityProtocol.POSTGRES and slug not in covered
639
+ )
640
+ if missing:
641
+ raise PreflightError(
642
+ "seed_missing",
643
+ "postgres-protocol capability with no store entry: " + ", ".join(missing),
644
+ )
645
+
646
+
647
+ def _verify_reserved_names(bundle_dir: Path, manifest: EnvironmentBundleV2) -> None:
648
+ if manifest.seed is None:
649
+ return
650
+ root = bundle_dir.resolve()
651
+ for store in manifest.seed.stores:
652
+ for relative_path in (*store.migrations, *store.seed_files):
653
+ path = root / relative_path
654
+ if not path.is_file():
655
+ continue # reported by `_verify_seed_files_on_disk_and_listed`
656
+ text = path.read_text(encoding="utf-8", errors="replace")
657
+ # Strip `--`-to-EOL and `/* ... */` comments before scanning (F9, p4-round1-review) —
658
+ # a generated seed file's own note about the reservation ("-- never create
659
+ # _alk_conformance here") would otherwise trip the scan on prose, not on an identifier
660
+ # it defines. Quoted string literals containing the name as *data* remain a known
661
+ # false-positive surface: the scan has no lexer, only comment-stripping. The stripping
662
+ # is a false-NEGATIVE surface in the opposite direction, equally lexer-free and equally
663
+ # left as-is (N7, p4-round2-review; B4, p4-round3-review): a `--` or `/*` inside a
664
+ # string literal (not a comment) deletes real content up to the next line-end or `*/`,
665
+ # which can delete a reserved-name definition that follows it on the same statement.
666
+ code = _SQL_BLOCK_COMMENT.sub("", _SQL_LINE_COMMENT.sub("", text))
667
+ if _RESERVED_NAME_PATTERN.search(code):
668
+ raise PreflightError(
669
+ "reserved_name",
670
+ f"{relative_path} defines the reserved conformance-canary identifier "
671
+ f"{_RESERVED_NAME!r}",
672
+ )
673
+
674
+
675
+ def _verify_seed_files_on_disk_and_listed(
676
+ bundle_dir: Path, manifest: EnvironmentBundleV2, files: list[BundleFileV2]
677
+ ) -> None:
678
+ """Digest verification (item 1) already guarantees every ``files[]``-listed path exists, so a
679
+ path missing from disk entirely is ``seed_file_missing`` regardless of whether it was ever
680
+ listed. ``seed_file_unlisted`` stays here as a second, store-scoped statement of the same
681
+ "listed" rule item 2's own walk now enforces bundle-wide (F1, p4-round1-review) — through
682
+ `preflight_bundle`'s full sequence item 2's ``bundle_file_unlisted`` always fires first for any
683
+ file the walk visits; the root ``manifest.json`` is exempt from that walk, so a store path
684
+ naming it still reaches here (N2, p4-round2-review).
685
+
686
+ Once every migration/seed file for a store is confirmed present and listed, its recorded
687
+ ``inputs_digest`` is recomputed and compared (F14, p4-round1-review; §2c makes it the baseline
688
+ identity attempt-retry reuse trusts absolutely, and nothing else on either side of the seam
689
+ ever validated it). ``engine``/``version`` come from the store's capability's own backing
690
+ ``ManagedProcess`` — guaranteed to exist by `bundle_v2`'s ``store_service_not_managed`` check;
691
+ a non-``ManagedProcess`` backing here would mean that guarantee broke, raised as a typed
692
+ ``PreflightError`` rather than asserted, since this module's charter is a rejection on every
693
+ path, never a crash (N8, p4-round2-review).
694
+ """
695
+ if manifest.seed is None:
696
+ return
697
+ listed = {record.path for record in files}
698
+ root = bundle_dir.resolve()
699
+ processes_by_name = {process.name: process for process in manifest.processes}
700
+ for store in manifest.seed.stores:
701
+ for relative_path in (*store.migrations, *store.seed_files):
702
+ if not (root / relative_path).is_file():
703
+ raise PreflightError(
704
+ "seed_file_missing", f"{relative_path} does not exist on disk"
705
+ )
706
+ if relative_path not in listed:
707
+ raise PreflightError(
708
+ "seed_file_unlisted", f"{relative_path} is not listed in files[]"
709
+ )
710
+ capability = manifest.capabilities[store.capability]
711
+ engine_process = processes_by_name[capability.service]
712
+ if not isinstance(engine_process, ManagedProcess):
713
+ raise PreflightError(
714
+ "store_service_not_managed",
715
+ f"{store.capability}: service {capability.service!r} is not a managed engine",
716
+ )
717
+ recomputed = compute_inputs_digest(
718
+ root,
719
+ store.migrations,
720
+ store.seed_files,
721
+ engine=engine_process.engine,
722
+ version=engine_process.version,
723
+ )
724
+ if recomputed != store.baseline.inputs_digest:
725
+ raise PreflightError(
726
+ "inputs_digest_mismatch",
727
+ f"{store.capability}: expected {store.baseline.inputs_digest}, computed "
728
+ f"{recomputed}",
729
+ )
730
+
731
+
732
+ # --- item 6: no_sql_store ------------------------------------------------------------------------
733
+
734
+
735
+ def _verify_no_sql_store(manifest: EnvironmentBundleV2) -> None:
736
+ if manifest.runtime.kind is not RuntimeKindV2.PROCESS:
737
+ return
738
+ if not any(
739
+ capability.protocol is CapabilityProtocol.POSTGRES
740
+ for capability in manifest.capabilities.values()
741
+ ):
742
+ raise PreflightError(
743
+ "no_sql_store",
744
+ "kind: process requires at least one postgres-protocol capability",
745
+ )
746
+
747
+
748
+ # --- item 7: resource sanity ----------------------------------------------------------------------
749
+
750
+
751
+ def _verify_resource_sanity(manifest: EnvironmentBundleV2, *, parallelism: int) -> None:
752
+ if len(manifest.processes) > _MAX_PROCESSES:
753
+ raise PreflightError(
754
+ "process_count_exceeded",
755
+ f"{len(manifest.processes)} processes exceeds the {_MAX_PROCESSES} cap",
756
+ )
757
+ if not (_MIN_PARALLELISM <= parallelism <= _MAX_PARALLELISM):
758
+ raise PreflightError(
759
+ "parallelism_out_of_range",
760
+ f"parallelism={parallelism} is outside {_MIN_PARALLELISM}..{_MAX_PARALLELISM}",
761
+ )
762
+
763
+
764
+ __all__ = ["PreflightError", "preflight_bundle"]