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.
- agent_learning_kit-0.1.0.dist-info/METADATA +381 -0
- agent_learning_kit-0.1.0.dist-info/RECORD +642 -0
- agent_learning_kit-0.1.0.dist-info/WHEEL +4 -0
- agent_learning_kit-0.1.0.dist-info/entry_points.txt +5 -0
- agent_learning_kit-0.1.0.dist-info/licenses/LICENSE +173 -0
- agent_learning_kit-0.1.0.dist-info/licenses/NOTICE +7 -0
- fi/__init__.py +5 -0
- fi/alk/__init__.py +57 -0
- fi/alk/_facade.py +31 -0
- fi/alk/_module_alias.py +68 -0
- fi/alk/_paths.py +14 -0
- fi/alk/_schema.py +522 -0
- fi/alk/actions.py +727 -0
- fi/alk/bench/__init__.py +517 -0
- fi/alk/bench/_codeexec.py +213 -0
- fi/alk/bench/_coding.py +215 -0
- fi/alk/bench/_docker.py +237 -0
- fi/alk/bench/_grader.py +286 -0
- fi/alk/bench/_pull.py +212 -0
- fi/alk/bench/_voice.py +147 -0
- fi/alk/capabilities.py +627 -0
- fi/alk/cli.py +6396 -0
- fi/alk/config.py +130 -0
- fi/alk/cua_loop.py +562 -0
- fi/alk/evals.py +2351 -0
- fi/alk/extensions.py +163 -0
- fi/alk/harness/ARCHITECTURE.md +231 -0
- fi/alk/harness/DESIGN.md +246 -0
- fi/alk/harness/ENVIRONMENT_CONFORMANCE.md +127 -0
- fi/alk/harness/HOW-IT-WORKS.md +297 -0
- fi/alk/harness/IMPLEMENTATION_AND_VALIDATION_STATUS.md +229 -0
- fi/alk/harness/README.md +417 -0
- fi/alk/harness/__init__.py +77 -0
- fi/alk/harness/__main__.py +3 -0
- fi/alk/harness/amend.py +312 -0
- fi/alk/harness/artifacts.py +319 -0
- fi/alk/harness/authoring_entrypoint.py +189 -0
- fi/alk/harness/authoring_runtime_validation.py +267 -0
- fi/alk/harness/backends/README.md +43 -0
- fi/alk/harness/backends/__init__.py +122 -0
- fi/alk/harness/backends/base.py +241 -0
- fi/alk/harness/backends/claude.py +211 -0
- fi/alk/harness/backends/files.py +182 -0
- fi/alk/harness/backends/vertex_gemini.py +457 -0
- fi/alk/harness/background_noise.py +95 -0
- fi/alk/harness/build.py +385 -0
- fi/alk/harness/bundle.py +593 -0
- fi/alk/harness/bundle_author_v2.py +1831 -0
- fi/alk/harness/bundle_v2.py +719 -0
- fi/alk/harness/call_runner.py +1440 -0
- fi/alk/harness/callback_http_adapter.py +111 -0
- fi/alk/harness/catalogue.py +287 -0
- fi/alk/harness/chat.py +428 -0
- fi/alk/harness/chat_call_runner.py +506 -0
- fi/alk/harness/checks.py +136 -0
- fi/alk/harness/cli.py +1354 -0
- fi/alk/harness/config.py +338 -0
- fi/alk/harness/contract.py +718 -0
- fi/alk/harness/credentials.py +674 -0
- fi/alk/harness/data/persona_vocabulary.json +111 -0
- fi/alk/harness/environment.py +99 -0
- fi/alk/harness/environment_plan.py +168 -0
- fi/alk/harness/events.py +125 -0
- fi/alk/harness/executor.py +304 -0
- fi/alk/harness/folder.py +234 -0
- fi/alk/harness/generated_runtime.py +815 -0
- fi/alk/harness/github.py +72 -0
- fi/alk/harness/hosted_authoring_entrypoint.py +183 -0
- fi/alk/harness/hosted_entrypoint.py +2402 -0
- fi/alk/harness/hosted_scheduler.py +2218 -0
- fi/alk/harness/job.py +426 -0
- fi/alk/harness/judge.py +184 -0
- fi/alk/harness/livekit_source.py +50 -0
- fi/alk/harness/livekit_tool_trace_bootstrap.py +71 -0
- fi/alk/harness/observability.py +208 -0
- fi/alk/harness/outbound.py +3252 -0
- fi/alk/harness/packaging.py +515 -0
- fi/alk/harness/persona_guides.py +157 -0
- fi/alk/harness/platform.py +692 -0
- fi/alk/harness/process_preflight.py +764 -0
- fi/alk/harness/process_runtime.py +5670 -0
- fi/alk/harness/prove.py +425 -0
- fi/alk/harness/provider_import.py +703 -0
- fi/alk/harness/provider_lifecycle.py +392 -0
- fi/alk/harness/provision.py +2896 -0
- fi/alk/harness/reception.py +147 -0
- fi/alk/harness/retell_chat_call_runner.py +373 -0
- fi/alk/harness/run/__init__.py +296 -0
- fi/alk/harness/run/alk.py +184 -0
- fi/alk/harness/run/call.py +162 -0
- fi/alk/harness/run/conversation.py +264 -0
- fi/alk/harness/run/data/voices_by_language_and_gender.json +693 -0
- fi/alk/harness/run/evidence.py +195 -0
- fi/alk/harness/run/grade.py +598 -0
- fi/alk/harness/run/live.py +297 -0
- fi/alk/harness/run/models.py +56 -0
- fi/alk/harness/run/platform_evals.py +227 -0
- fi/alk/harness/run/sdk_voice.py +130 -0
- fi/alk/harness/run/simulation.py +1209 -0
- fi/alk/harness/run/stage.py +91 -0
- fi/alk/harness/run/targets.py +508 -0
- fi/alk/harness/run/tools.py +601 -0
- fi/alk/harness/run/voice.py +340 -0
- fi/alk/harness/runtime.py +172 -0
- fi/alk/harness/sandbox_server.py +2011 -0
- fi/alk/harness/sandbox_worker.py +44 -0
- fi/alk/harness/scenario.py +1048 -0
- fi/alk/harness/scenario_source.py +879 -0
- fi/alk/harness/scenario_tools.py +1143 -0
- fi/alk/harness/scenarios.py +915 -0
- fi/alk/harness/secrets.py +168 -0
- fi/alk/harness/service_catalog.py +97 -0
- fi/alk/harness/session.py +391 -0
- fi/alk/harness/sessions.py +372 -0
- fi/alk/harness/simulator.py +76 -0
- fi/alk/harness/simulator_voice.py +928 -0
- fi/alk/harness/skills/build-environment/SKILL.md +538 -0
- fi/alk/harness/skills/harness.md +131 -0
- fi/alk/harness/skills/kinds/chat.md +48 -0
- fi/alk/harness/skills/kinds/voice-voicemail.md +63 -0
- fi/alk/harness/skills/kinds/voice.md +59 -0
- fi/alk/harness/skills/plan-suite/SKILL.md +103 -0
- fi/alk/harness/skills/provision-environment/SKILL.md +136 -0
- fi/alk/harness/skills/run-scenarios/SKILL.md +112 -0
- fi/alk/harness/skills/understand-agent/SKILL.md +251 -0
- fi/alk/harness/skills/write-scenarios/SKILL.md +606 -0
- fi/alk/harness/skills/write-scenarios/references/refusals.md +28 -0
- fi/alk/harness/skills/write-scenarios/references/world-api.md +92 -0
- fi/alk/harness/source_data_invariants.py +444 -0
- fi/alk/harness/source_tool_evidence.py +79 -0
- fi/alk/harness/sources.py +253 -0
- fi/alk/harness/spend.py +140 -0
- fi/alk/harness/tool_trace_proxy.py +104 -0
- fi/alk/harness/tools.py +1018 -0
- fi/alk/harness/understand.py +169 -0
- fi/alk/harness/voicemail_audio.py +74 -0
- fi/alk/harness/world/__init__.py +33 -0
- fi/alk/harness/world/errors.py +68 -0
- fi/alk/harness/world/expectations.py +91 -0
- fi/alk/harness/world/handle.py +538 -0
- fi/alk/harness/world/kinds.py +196 -0
- fi/alk/harness/world/mutate.py +186 -0
- fi/alk/harness/world/probe.py +413 -0
- fi/alk/harness/world/provision.py +511 -0
- fi/alk/harness/world/provisioned.py +191 -0
- fi/alk/harness/world/runtime.py +616 -0
- fi/alk/harness/world/snapshot.py +288 -0
- fi/alk/harness/world/stores/__init__.py +305 -0
- fi/alk/harness/world/stores/container.py +215 -0
- fi/alk/harness/world/stores/inprocess.py +346 -0
- fi/alk/harness/world/stores/postgres.py +481 -0
- fi/alk/harness/world/stores/prove.py +202 -0
- fi/alk/harness/world/stores/sqlite.py +245 -0
- fi/alk/harness/world/stores/written.py +182 -0
- fi/alk/harness/world/tools.py +1516 -0
- fi/alk/harness/world/workspace.py +144 -0
- fi/alk/image_loop.py +453 -0
- fi/alk/image_perturb.py +241 -0
- fi/alk/improve.py +274 -0
- fi/alk/live/__init__.py +154 -0
- fi/alk/live/_attribution.py +184 -0
- fi/alk/live/_capture.py +264 -0
- fi/alk/live/_codec.py +391 -0
- fi/alk/live/_contract.py +134 -0
- fi/alk/live/_loopback.py +316 -0
- fi/alk/live/_perturb.py +449 -0
- fi/alk/live/_runner.py +386 -0
- fi/alk/live/_stats.py +561 -0
- fi/alk/live/_transcript.py +240 -0
- fi/alk/live/_workers/__init__.py +9 -0
- fi/alk/live/_workers/a2a_worker.py +316 -0
- fi/alk/live/_workers/langgraph_worker.py +217 -0
- fi/alk/live/_workers/livekit_worker.py +207 -0
- fi/alk/live/_workers/mcp_loopback_server.py +46 -0
- fi/alk/live/_workers/mcp_worker.py +158 -0
- fi/alk/live/_workers/pipecat_worker.py +189 -0
- fi/alk/live/a2a_lane.py +138 -0
- fi/alk/live/langgraph_lane.py +339 -0
- fi/alk/live/livekit_lane.py +376 -0
- fi/alk/live/mcp_lane.py +172 -0
- fi/alk/live/pipecat_lane.py +341 -0
- fi/alk/live/voice_redteam.py +494 -0
- fi/alk/loss.py +306 -0
- fi/alk/optimize.py +36260 -0
- fi/alk/practice/__init__.py +51 -0
- fi/alk/practice/_assess.py +103 -0
- fi/alk/practice/_budget.py +81 -0
- fi/alk/practice/_calibrate.py +69 -0
- fi/alk/practice/_capstone.py +86 -0
- fi/alk/practice/_contract.py +91 -0
- fi/alk/practice/_diagnose.py +79 -0
- fi/alk/practice/_drill.py +196 -0
- fi/alk/practice/_experiment.py +720 -0
- fi/alk/practice/_schedule.py +102 -0
- fi/alk/practice/_store.py +194 -0
- fi/alk/practice/_trainer.py +245 -0
- fi/alk/practice/_update.py +125 -0
- fi/alk/redteam.py +2621 -0
- fi/alk/rewardhack.py +237 -0
- fi/alk/simulate.py +10351 -0
- fi/alk/studio/__init__.py +82 -0
- fi/alk/studio/_bias.py +314 -0
- fi/alk/studio/_calibration.py +522 -0
- fi/alk/studio/_coverage.py +262 -0
- fi/alk/studio/_download.py +665 -0
- fi/alk/studio/_fidelity_attack.py +114 -0
- fi/alk/studio/_generate.py +652 -0
- fi/alk/studio/_library.py +370 -0
- fi/alk/studio/_scan.py +134 -0
- fi/alk/studio/_upgrade.py +42 -0
- fi/alk/studio/_vendor.py +172 -0
- fi/alk/suite.py +4200 -0
- fi/alk/tasks.py +828 -0
- fi/alk/telemetry/__init__.py +149 -0
- fi/alk/telemetry/_contract.py +141 -0
- fi/alk/telemetry/_emit.py +182 -0
- fi/alk/telemetry/_ledger.py +296 -0
- fi/alk/telemetry/_queue.py +127 -0
- fi/alk/telemetry/_row.py +294 -0
- fi/alk/telemetry/_run.py +233 -0
- fi/alk/telemetry/_sync.py +193 -0
- fi/alk/telemetry/_url.py +119 -0
- fi/alk/trinity.py +49397 -0
- fi/alk/voice_loop.py +174 -0
- fi/api/__init__.py +1 -0
- fi/api/auth.py +137 -0
- fi/api/types.py +29 -0
- fi/cli/__init__.py +9 -0
- fi/cli/assertions/__init__.py +25 -0
- fi/cli/assertions/conditions.py +76 -0
- fi/cli/assertions/evaluator.py +286 -0
- fi/cli/assertions/exit_codes.py +20 -0
- fi/cli/assertions/parser.py +131 -0
- fi/cli/assertions/reporter.py +194 -0
- fi/cli/commands/__init__.py +9 -0
- fi/cli/commands/config.py +165 -0
- fi/cli/commands/export.py +208 -0
- fi/cli/commands/init.py +112 -0
- fi/cli/commands/list_cmd.py +213 -0
- fi/cli/commands/run.py +486 -0
- fi/cli/commands/validate.py +173 -0
- fi/cli/commands/view.py +424 -0
- fi/cli/config/__init__.py +6 -0
- fi/cli/config/defaults.py +206 -0
- fi/cli/config/loader.py +155 -0
- fi/cli/config/schema.py +174 -0
- fi/cli/main.py +78 -0
- fi/cli/output/__init__.py +6 -0
- fi/cli/output/formatters.py +106 -0
- fi/cli/output/reporters.py +46 -0
- fi/cli/storage/__init__.py +5 -0
- fi/cli/storage/run_history.py +249 -0
- fi/cli/utils/__init__.py +5 -0
- fi/cli/utils/console.py +44 -0
- fi/evals/__init__.py +131 -0
- fi/evals/autoeval/__init__.py +137 -0
- fi/evals/autoeval/analyzer.py +211 -0
- fi/evals/autoeval/config.py +244 -0
- fi/evals/autoeval/export.py +213 -0
- fi/evals/autoeval/interactive.py +283 -0
- fi/evals/autoeval/pipeline.py +625 -0
- fi/evals/autoeval/prompts.py +139 -0
- fi/evals/autoeval/recommender.py +242 -0
- fi/evals/autoeval/rules.py +589 -0
- fi/evals/autoeval/templates.py +299 -0
- fi/evals/autoeval/types.py +232 -0
- fi/evals/core/__init__.py +16 -0
- fi/evals/core/cloud_registry.py +184 -0
- fi/evals/core/engines.py +368 -0
- fi/evals/core/evaluate.py +319 -0
- fi/evals/core/judge_prompt.py +90 -0
- fi/evals/core/prompt_generator.py +83 -0
- fi/evals/core/registry.py +57 -0
- fi/evals/core/result.py +55 -0
- fi/evals/evaluator.py +721 -0
- fi/evals/execution.py +168 -0
- fi/evals/feedback/__init__.py +32 -0
- fi/evals/feedback/calibrator.py +160 -0
- fi/evals/feedback/collector.py +214 -0
- fi/evals/feedback/hooks.py +81 -0
- fi/evals/feedback/retriever.py +128 -0
- fi/evals/feedback/store.py +272 -0
- fi/evals/feedback/types.py +99 -0
- fi/evals/framework/README.md +79 -0
- fi/evals/framework/__init__.py +267 -0
- fi/evals/framework/backends/Dockerfile.eval-runner +33 -0
- fi/evals/framework/backends/__init__.py +99 -0
- fi/evals/framework/backends/_container.py +141 -0
- fi/evals/framework/backends/_utils.py +145 -0
- fi/evals/framework/backends/base.py +223 -0
- fi/evals/framework/backends/celery_backend.py +417 -0
- fi/evals/framework/backends/celery_worker.py +78 -0
- fi/evals/framework/backends/kubernetes_backend.py +665 -0
- fi/evals/framework/backends/ray_backend.py +521 -0
- fi/evals/framework/backends/temporal.py +350 -0
- fi/evals/framework/backends/temporal_worker.py +126 -0
- fi/evals/framework/backends/thread_pool.py +286 -0
- fi/evals/framework/context.py +258 -0
- fi/evals/framework/enrichment.py +306 -0
- fi/evals/framework/evals/__init__.py +68 -0
- fi/evals/framework/evals/agentic.py +399 -0
- fi/evals/framework/evals/builder.py +609 -0
- fi/evals/framework/evals/semantic.py +142 -0
- fi/evals/framework/evaluator.py +647 -0
- fi/evals/framework/evaluators/__init__.py +22 -0
- fi/evals/framework/evaluators/blocking.py +347 -0
- fi/evals/framework/evaluators/non_blocking.py +577 -0
- fi/evals/framework/propagation.py +421 -0
- fi/evals/framework/protocols.py +385 -0
- fi/evals/framework/registry.py +370 -0
- fi/evals/framework/resilience/__init__.py +150 -0
- fi/evals/framework/resilience/circuit_breaker.py +309 -0
- fi/evals/framework/resilience/degradation.py +355 -0
- fi/evals/framework/resilience/health.py +505 -0
- fi/evals/framework/resilience/rate_limiter.py +228 -0
- fi/evals/framework/resilience/retry.py +274 -0
- fi/evals/framework/resilience/types.py +288 -0
- fi/evals/framework/resilience/wrapper.py +433 -0
- fi/evals/framework/types.py +218 -0
- fi/evals/guardrails/README.md +915 -0
- fi/evals/guardrails/__init__.py +96 -0
- fi/evals/guardrails/backends/__init__.py +43 -0
- fi/evals/guardrails/backends/azure.py +361 -0
- fi/evals/guardrails/backends/base.py +88 -0
- fi/evals/guardrails/backends/generic_llm.py +163 -0
- fi/evals/guardrails/backends/granite.py +216 -0
- fi/evals/guardrails/backends/llamaguard.py +221 -0
- fi/evals/guardrails/backends/local_base.py +479 -0
- fi/evals/guardrails/backends/openai.py +365 -0
- fi/evals/guardrails/backends/qwen.py +170 -0
- fi/evals/guardrails/backends/shieldgemma.py +154 -0
- fi/evals/guardrails/backends/turing.py +235 -0
- fi/evals/guardrails/backends/vllm_client.py +321 -0
- fi/evals/guardrails/backends/wildguard.py +188 -0
- fi/evals/guardrails/base.py +888 -0
- fi/evals/guardrails/config.py +221 -0
- fi/evals/guardrails/discovery.py +243 -0
- fi/evals/guardrails/gateway.py +437 -0
- fi/evals/guardrails/registry.py +231 -0
- fi/evals/guardrails/scanners/__init__.py +127 -0
- fi/evals/guardrails/scanners/base.py +191 -0
- fi/evals/guardrails/scanners/code_injection.py +243 -0
- fi/evals/guardrails/scanners/eval_delegate.py +574 -0
- fi/evals/guardrails/scanners/invisible_chars.py +351 -0
- fi/evals/guardrails/scanners/jailbreak.py +412 -0
- fi/evals/guardrails/scanners/language.py +288 -0
- fi/evals/guardrails/scanners/pipeline.py +260 -0
- fi/evals/guardrails/scanners/regex.py +311 -0
- fi/evals/guardrails/scanners/secrets.py +274 -0
- fi/evals/guardrails/scanners/topics.py +649 -0
- fi/evals/guardrails/scanners/urls.py +341 -0
- fi/evals/guardrails/types.py +96 -0
- fi/evals/llm/__init__.py +3 -0
- fi/evals/llm/base_llm_provider.py +35 -0
- fi/evals/llm/providers/litellm.py +70 -0
- fi/evals/local/__init__.py +90 -0
- fi/evals/local/evaluator.py +690 -0
- fi/evals/local/execution_mode.py +121 -0
- fi/evals/local/llm.py +489 -0
- fi/evals/local/metrics/__init__.py +19 -0
- fi/evals/local/registry.py +360 -0
- fi/evals/manager.py +1018 -0
- fi/evals/manager_types.py +362 -0
- fi/evals/metrics/__init__.py +185 -0
- fi/evals/metrics/agents/__init__.py +74 -0
- fi/evals/metrics/agents/metrics.py +693 -0
- fi/evals/metrics/agents/report.py +36463 -0
- fi/evals/metrics/agents/types.py +160 -0
- fi/evals/metrics/base_llm_metric.py +111 -0
- fi/evals/metrics/base_metric.py +138 -0
- fi/evals/metrics/code_security/__init__.py +305 -0
- fi/evals/metrics/code_security/analyzer.py +985 -0
- fi/evals/metrics/code_security/benchmarks/__init__.py +73 -0
- fi/evals/metrics/code_security/benchmarks/builtin.py +750 -0
- fi/evals/metrics/code_security/benchmarks/loader.py +580 -0
- fi/evals/metrics/code_security/benchmarks/types.py +308 -0
- fi/evals/metrics/code_security/detectors/__init__.py +186 -0
- fi/evals/metrics/code_security/detectors/base.py +394 -0
- fi/evals/metrics/code_security/detectors/cryptography.py +345 -0
- fi/evals/metrics/code_security/detectors/injection.py +744 -0
- fi/evals/metrics/code_security/detectors/secrets.py +287 -0
- fi/evals/metrics/code_security/detectors/serialization.py +192 -0
- fi/evals/metrics/code_security/joint_metrics.py +588 -0
- fi/evals/metrics/code_security/judges/__init__.py +83 -0
- fi/evals/metrics/code_security/judges/base.py +238 -0
- fi/evals/metrics/code_security/judges/dual_judge.py +534 -0
- fi/evals/metrics/code_security/judges/llm_judge.py +301 -0
- fi/evals/metrics/code_security/judges/pattern_judge.py +515 -0
- fi/evals/metrics/code_security/metrics.py +388 -0
- fi/evals/metrics/code_security/modes/__init__.py +63 -0
- fi/evals/metrics/code_security/modes/adversarial.py +284 -0
- fi/evals/metrics/code_security/modes/autocomplete.py +198 -0
- fi/evals/metrics/code_security/modes/base.py +283 -0
- fi/evals/metrics/code_security/modes/instruct.py +253 -0
- fi/evals/metrics/code_security/modes/repair.py +230 -0
- fi/evals/metrics/code_security/reports/__init__.py +57 -0
- fi/evals/metrics/code_security/reports/generator.py +404 -0
- fi/evals/metrics/code_security/reports/leaderboard.py +509 -0
- fi/evals/metrics/code_security/types.py +534 -0
- fi/evals/metrics/function_calling/__init__.py +34 -0
- fi/evals/metrics/function_calling/metrics.py +573 -0
- fi/evals/metrics/function_calling/types.py +87 -0
- fi/evals/metrics/hallucination/__init__.py +54 -0
- fi/evals/metrics/hallucination/detector.py +149 -0
- fi/evals/metrics/hallucination/metrics.py +390 -0
- fi/evals/metrics/hallucination/nli.py +253 -0
- fi/evals/metrics/hallucination/sentinel.py +106 -0
- fi/evals/metrics/hallucination/types.py +132 -0
- fi/evals/metrics/heuristics/aggregation_metrics.py +85 -0
- fi/evals/metrics/heuristics/json_metrics.py +87 -0
- fi/evals/metrics/heuristics/similarity_metrics.py +375 -0
- fi/evals/metrics/heuristics/string_metrics.py +391 -0
- fi/evals/metrics/llm_as_judges/__init__.py +17 -0
- fi/evals/metrics/llm_as_judges/custom_judge/metric.py +112 -0
- fi/evals/metrics/llm_as_judges/custom_judge/prompts.py +26 -0
- fi/evals/metrics/llm_as_judges/types.py +48 -0
- fi/evals/metrics/rag/__init__.py +111 -0
- fi/evals/metrics/rag/advanced/__init__.py +14 -0
- fi/evals/metrics/rag/advanced/multi_hop.py +283 -0
- fi/evals/metrics/rag/advanced/source_attribution.py +344 -0
- fi/evals/metrics/rag/generation/__init__.py +17 -0
- fi/evals/metrics/rag/generation/answer_relevancy.py +176 -0
- fi/evals/metrics/rag/generation/context_utilization.py +245 -0
- fi/evals/metrics/rag/generation/faithfulness.py +241 -0
- fi/evals/metrics/rag/generation/groundedness.py +131 -0
- fi/evals/metrics/rag/rag_score.py +277 -0
- fi/evals/metrics/rag/retrieval/__init__.py +20 -0
- fi/evals/metrics/rag/retrieval/context_entity_recall.py +124 -0
- fi/evals/metrics/rag/retrieval/context_precision.py +158 -0
- fi/evals/metrics/rag/retrieval/context_recall.py +106 -0
- fi/evals/metrics/rag/retrieval/noise_sensitivity.py +163 -0
- fi/evals/metrics/rag/retrieval/ranking.py +261 -0
- fi/evals/metrics/rag/types.py +100 -0
- fi/evals/metrics/rag/utils/__init__.py +62 -0
- fi/evals/metrics/rag/utils/claims.py +189 -0
- fi/evals/metrics/rag/utils/entities.py +244 -0
- fi/evals/metrics/rag/utils/nli.py +92 -0
- fi/evals/metrics/rag/utils/similarity.py +345 -0
- fi/evals/metrics/structured/__init__.py +114 -0
- fi/evals/metrics/structured/field_completeness.py +313 -0
- fi/evals/metrics/structured/hierarchy_score.py +366 -0
- fi/evals/metrics/structured/json_validation.py +190 -0
- fi/evals/metrics/structured/schema_compliance.py +280 -0
- fi/evals/metrics/structured/structured_output_score.py +298 -0
- fi/evals/metrics/structured/types.py +108 -0
- fi/evals/metrics/structured/validators/__init__.py +30 -0
- fi/evals/metrics/structured/validators/base.py +189 -0
- fi/evals/metrics/structured/validators/json_validator.py +196 -0
- fi/evals/metrics/structured/validators/pydantic_validator.py +178 -0
- fi/evals/metrics/structured/validators/yaml_validator.py +248 -0
- fi/evals/otel/__init__.py +266 -0
- fi/evals/otel/config.py +400 -0
- fi/evals/otel/conventions.py +463 -0
- fi/evals/otel/enrichment.py +371 -0
- fi/evals/otel/instrumentors/__init__.py +140 -0
- fi/evals/otel/instrumentors/anthropic.py +517 -0
- fi/evals/otel/instrumentors/base.py +382 -0
- fi/evals/otel/instrumentors/openai.py +673 -0
- fi/evals/otel/processors/__init__.py +36 -0
- fi/evals/otel/processors/base.py +473 -0
- fi/evals/otel/processors/cost.py +445 -0
- fi/evals/otel/processors/evaluation.py +559 -0
- fi/evals/otel/processors/llm.py +462 -0
- fi/evals/otel/tracer.py +506 -0
- fi/evals/otel/types.py +232 -0
- fi/evals/otel_utils.py +23 -0
- fi/evals/protect.py +671 -0
- fi/evals/protect_input_adapter.py +154 -0
- fi/evals/streaming/__init__.py +88 -0
- fi/evals/streaming/buffer.py +213 -0
- fi/evals/streaming/evaluator.py +551 -0
- fi/evals/streaming/policy.py +307 -0
- fi/evals/streaming/scorers.py +368 -0
- fi/evals/streaming/types.py +238 -0
- fi/evals/templates.py +472 -0
- fi/evals/types.py +156 -0
- fi/opt/__init__.py +221 -0
- fi/opt/_objective_scoring.py +85 -0
- fi/opt/base/__init__.py +11 -0
- fi/opt/base/base_generator.py +33 -0
- fi/opt/base/base_mapper.py +26 -0
- fi/opt/base/base_optimizer.py +45 -0
- fi/opt/base/evaluator.py +211 -0
- fi/opt/components.py +3095 -0
- fi/opt/datamappers/__init__.py +3 -0
- fi/opt/datamappers/basic_mapper.py +40 -0
- fi/opt/deployment.py +1021 -0
- fi/opt/evidence.py +4332 -0
- fi/opt/generators/__init__.py +3 -0
- fi/opt/generators/litellm.py +66 -0
- fi/opt/integrations/__init__.py +23 -0
- fi/opt/integrations/generative_suite.py +410 -0
- fi/opt/integrations/simulate.py +1313 -0
- fi/opt/mutations.py +771 -0
- fi/opt/observability.py +4639 -0
- fi/opt/optimizer_trace.py +889 -0
- fi/opt/optimizers/__init__.py +80 -0
- fi/opt/optimizers/agent.py +331 -0
- fi/opt/optimizers/agent_bandit.py +392 -0
- fi/opt/optimizers/agent_curriculum.py +635 -0
- fi/opt/optimizers/agent_evolution.py +894 -0
- fi/opt/optimizers/agent_feedback.py +1863 -0
- fi/opt/optimizers/agent_pareto.py +547 -0
- fi/opt/optimizers/agent_social_memory.py +1113 -0
- fi/opt/optimizers/agent_tpe.py +321 -0
- fi/opt/optimizers/bayesian_search.py +449 -0
- fi/opt/optimizers/council.py +2075 -0
- fi/opt/optimizers/futureagi_replay.py +799 -0
- fi/opt/optimizers/gepa.py +322 -0
- fi/opt/optimizers/metaprompt.py +243 -0
- fi/opt/optimizers/promptwizard.py +417 -0
- fi/opt/optimizers/protegi.py +329 -0
- fi/opt/optimizers/random_search.py +224 -0
- fi/opt/research.py +518 -0
- fi/opt/simulation.py +260 -0
- fi/opt/targets.py +232 -0
- fi/opt/types.py +66 -0
- fi/opt/utils/__init__.py +4 -0
- fi/opt/utils/early_stopping.py +266 -0
- fi/opt/utils/setup_logging.py +82 -0
- fi/simulate/__init__.py +540 -0
- fi/simulate/_hashing.py +35 -0
- fi/simulate/_logging.py +10 -0
- fi/simulate/adapters.py +87 -0
- fi/simulate/agent/__init__.py +120 -0
- fi/simulate/agent/browser.py +658 -0
- fi/simulate/agent/definition.py +587 -0
- fi/simulate/agent/frameworks.py +3528 -0
- fi/simulate/agent/generic.py +8286 -0
- fi/simulate/agent/import_probe.py +227 -0
- fi/simulate/agent/memory.py +905 -0
- fi/simulate/agent/mocks.py +101 -0
- fi/simulate/agent/multi_agent.py +361 -0
- fi/simulate/agent/orchestration.py +903 -0
- fi/simulate/agent/realtime.py +665 -0
- fi/simulate/agent/wrapper.py +99 -0
- fi/simulate/agent/wrappers/__init__.py +18 -0
- fi/simulate/agent/wrappers/anthropic.py +62 -0
- fi/simulate/agent/wrappers/gemini.py +65 -0
- fi/simulate/agent/wrappers/http.py +404 -0
- fi/simulate/agent/wrappers/langchain.py +80 -0
- fi/simulate/agent/wrappers/openai.py +75 -0
- fi/simulate/agent/wrappers/websocket.py +326 -0
- fi/simulate/artifacts/__init__.py +11 -0
- fi/simulate/artifacts/manifest.py +62 -0
- fi/simulate/cli.py +20560 -0
- fi/simulate/endpoints/__init__.py +45 -0
- fi/simulate/endpoints/_http_actor.py +73 -0
- fi/simulate/endpoints/actor_sources.py +243 -0
- fi/simulate/endpoints/base.py +107 -0
- fi/simulate/endpoints/builtins.py +10 -0
- fi/simulate/endpoints/callable.py +95 -0
- fi/simulate/endpoints/http.py +76 -0
- fi/simulate/endpoints/livekit.py +138 -0
- fi/simulate/endpoints/originators.py +132 -0
- fi/simulate/endpoints/profiles.py +348 -0
- fi/simulate/endpoints/retell.py +633 -0
- fi/simulate/endpoints/vapi.py +205 -0
- fi/simulate/endpoints/websocket.py +76 -0
- fi/simulate/environment.py +33026 -0
- fi/simulate/environments/__init__.py +11 -0
- fi/simulate/environments/base.py +73 -0
- fi/simulate/environments/chat.py +697 -0
- fi/simulate/environments/voice.py +212 -0
- fi/simulate/evaluation/__init__.py +4 -0
- fi/simulate/evaluation/ai_eval.py +227 -0
- fi/simulate/evidence/__init__.py +35 -0
- fi/simulate/evidence/base.py +59 -0
- fi/simulate/evidence/caller_observed.py +50 -0
- fi/simulate/evidence/livekit_instrumentation.py +51 -0
- fi/simulate/evidence/livekit_room.py +50 -0
- fi/simulate/evidence/otel.py +49 -0
- fi/simulate/evidence/providers/__init__.py +24 -0
- fi/simulate/evidence/providers/base.py +61 -0
- fi/simulate/evidence/providers/retell.py +376 -0
- fi/simulate/evidence/providers/vapi.py +426 -0
- fi/simulate/hosted/__init__.py +32 -0
- fi/simulate/hosted/child_entrypoint.py +306 -0
- fi/simulate/hosted/job.py +150 -0
- fi/simulate/hosted/targets.py +53 -0
- fi/simulate/instrumentation/__init__.py +5 -0
- fi/simulate/instrumentation/livekit/__init__.py +122 -0
- fi/simulate/manifest.py +1033 -0
- fi/simulate/matrix_cli.py +165 -0
- fi/simulate/realtime/__init__.py +40 -0
- fi/simulate/realtime/events.py +107 -0
- fi/simulate/realtime/media.py +61 -0
- fi/simulate/realtime/session.py +91 -0
- fi/simulate/recording/__init__.py +5 -0
- fi/simulate/recording/room_recorder.py +326 -0
- fi/simulate/registry.py +185 -0
- fi/simulate/results/__init__.py +9 -0
- fi/simulate/results/base.py +18 -0
- fi/simulate/results/filesystem.py +71 -0
- fi/simulate/results/futureagi.py +1340 -0
- fi/simulate/runtime/__init__.py +85 -0
- fi/simulate/runtime/capabilities.py +40 -0
- fi/simulate/runtime/events.py +63 -0
- fi/simulate/runtime/failures.py +25 -0
- fi/simulate/runtime/ids.py +34 -0
- fi/simulate/runtime/plan.py +70 -0
- fi/simulate/runtime/planner.py +102 -0
- fi/simulate/runtime/report.py +174 -0
- fi/simulate/runtime/run.py +75 -0
- fi/simulate/runtime/runner.py +333 -0
- fi/simulate/runtime/spec.py +186 -0
- fi/simulate/simulation/__init__.py +30 -0
- fi/simulate/simulation/behavior_policy.py +425 -0
- fi/simulate/simulation/bridge/__init__.py +9 -0
- fi/simulate/simulation/bridge/audio.py +29 -0
- fi/simulate/simulation/bridge/connector.py +46 -0
- fi/simulate/simulation/bridge/livekit.py +252 -0
- fi/simulate/simulation/bridge/retell.py +188 -0
- fi/simulate/simulation/bridge/vapi.py +177 -0
- fi/simulate/simulation/contract.py +419 -0
- fi/simulate/simulation/engines/__init__.py +12 -0
- fi/simulate/simulation/engines/base.py +21 -0
- fi/simulate/simulation/engines/cloud.py +517 -0
- fi/simulate/simulation/engines/livekit.py +4167 -0
- fi/simulate/simulation/engines/local_text.py +89 -0
- fi/simulate/simulation/fidelity.py +374 -0
- fi/simulate/simulation/gemini_tts_stream.py +110 -0
- fi/simulate/simulation/generator.py +91 -0
- fi/simulate/simulation/goal_machine.py +185 -0
- fi/simulate/simulation/livekit_models.py +467 -0
- fi/simulate/simulation/matrix.py +170 -0
- fi/simulate/simulation/models.py +279 -0
- fi/simulate/simulation/runner.py +153 -0
- fi/simulate/simulation/synthetic.py +880 -0
- fi/simulate/simulation/voice_prompt.py +502 -0
- fi/simulate/simulator/__init__.py +55 -0
- fi/simulate/simulator/builtins.py +53 -0
- fi/simulate/suite.py +1288 -0
- fi/simulate/utils/routes.py +164 -0
- fi/simulate/voice.py +225 -0
- fi/simulate/voice_cli.py +182 -0
- fi/utils/__init__.py +1 -0
- fi/utils/constants.py +14 -0
- fi/utils/errors.py +200 -0
- fi/utils/executor.py +26 -0
- fi/utils/routes.py +119 -0
- 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"]
|