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,815 @@
|
|
|
1
|
+
"""Deterministic container packaging for repositories that ship no container metadata.
|
|
2
|
+
|
|
3
|
+
The adapter packages a submitted process; it never generates application behavior. It accepts
|
|
4
|
+
only dependency and entrypoint conventions that can be proven from files in the repository, and
|
|
5
|
+
stages a sanitized build context outside the submitted checkout so generated files and secret
|
|
6
|
+
configuration never mutate or enter the customer's source tree.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
import json
|
|
13
|
+
import re
|
|
14
|
+
import shlex
|
|
15
|
+
import shutil
|
|
16
|
+
import stat
|
|
17
|
+
from dataclasses import asdict, dataclass
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
GENERATED_DOCKERFILE = ".alk-generated.Dockerfile"
|
|
23
|
+
GENERATED_PLAN = "generated-runtime.json"
|
|
24
|
+
_MAX_FILES = 20_000
|
|
25
|
+
_MAX_FILE_BYTES = 64 * 1024 * 1024
|
|
26
|
+
_MAX_CONTEXT_BYTES = 512 * 1024 * 1024
|
|
27
|
+
_IGNORED_DIRECTORIES = {
|
|
28
|
+
".git",
|
|
29
|
+
".hg",
|
|
30
|
+
".mypy_cache",
|
|
31
|
+
".pytest_cache",
|
|
32
|
+
".ruff_cache",
|
|
33
|
+
".svn",
|
|
34
|
+
".tox",
|
|
35
|
+
".venv",
|
|
36
|
+
"__pycache__",
|
|
37
|
+
"artifacts",
|
|
38
|
+
"build",
|
|
39
|
+
"coverage",
|
|
40
|
+
"dist",
|
|
41
|
+
"node_modules",
|
|
42
|
+
"target",
|
|
43
|
+
"vendor",
|
|
44
|
+
}
|
|
45
|
+
_SECRET_NAMES = {
|
|
46
|
+
".env",
|
|
47
|
+
".npmrc",
|
|
48
|
+
".pypirc",
|
|
49
|
+
"credentials",
|
|
50
|
+
"credentials.json",
|
|
51
|
+
"id_rsa",
|
|
52
|
+
"id_ed25519",
|
|
53
|
+
}
|
|
54
|
+
_SECRET_SUFFIXES = {".key", ".p12", ".pem", ".pfx"}
|
|
55
|
+
_SECRET_CONTENT = (
|
|
56
|
+
re.compile(rb"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----"),
|
|
57
|
+
re.compile(rb"\b(?:ghp|github_pat)_[A-Za-z0-9_]{20,}\b"),
|
|
58
|
+
re.compile(rb"\bAKIA[0-9A-Z]{16}\b"),
|
|
59
|
+
re.compile(rb"\bsk-[A-Za-z0-9_-]{20,}\b"),
|
|
60
|
+
)
|
|
61
|
+
_PYTHON_ENTRYPOINTS = ("agent.py", "main.py", "app.py", "bot.py", "server.py")
|
|
62
|
+
_NODE_ENTRYPOINTS = ("agent.js", "index.js", "main.js", "server.js")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class GeneratedRuntimeError(ValueError):
|
|
66
|
+
"""The repository cannot be packaged without unsafe or ambiguous guessing."""
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True)
|
|
70
|
+
class GeneratedRuntimePlan:
|
|
71
|
+
language: str
|
|
72
|
+
component: str
|
|
73
|
+
version: str
|
|
74
|
+
dependency_file: str
|
|
75
|
+
install_strategy: str
|
|
76
|
+
extras: tuple[str, ...]
|
|
77
|
+
command: tuple[str, ...]
|
|
78
|
+
context_excludes: tuple[str, ...]
|
|
79
|
+
dockerfile: str
|
|
80
|
+
context_directory: str = ""
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def fingerprint(self) -> str:
|
|
84
|
+
payload = asdict(self)
|
|
85
|
+
payload.pop("context_directory", None)
|
|
86
|
+
return hashlib.sha256(
|
|
87
|
+
json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()
|
|
88
|
+
).hexdigest()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def can_generate_runtime(source: str | Path, runtime: Any | None = None) -> bool:
|
|
92
|
+
"""Whether a deterministic language manifest exists; this performs no writes."""
|
|
93
|
+
try:
|
|
94
|
+
detect_generated_runtime(source, runtime)
|
|
95
|
+
except GeneratedRuntimeError:
|
|
96
|
+
return False
|
|
97
|
+
return True
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def detect_generated_runtime(
|
|
101
|
+
source: str | Path, runtime: Any | None = None
|
|
102
|
+
) -> GeneratedRuntimePlan:
|
|
103
|
+
"""Return an auditable runtime plan, or one actionable admission error."""
|
|
104
|
+
root = Path(source).expanduser().resolve()
|
|
105
|
+
component = _component_root(root, str(getattr(runtime, "workdir", "") or ""))
|
|
106
|
+
language = str(getattr(runtime, "language", "") or "").strip().lower()
|
|
107
|
+
python_markers = _python_markers(component)
|
|
108
|
+
node_marker = component / "package.json"
|
|
109
|
+
|
|
110
|
+
if language in {"python", "py"}:
|
|
111
|
+
if not python_markers:
|
|
112
|
+
raise GeneratedRuntimeError(
|
|
113
|
+
"generated_runtime_dependency_manifest_missing: expected pyproject.toml, "
|
|
114
|
+
"requirements.txt, or a Python lock file"
|
|
115
|
+
)
|
|
116
|
+
language = "python"
|
|
117
|
+
elif language in {"node", "nodejs", "javascript", "typescript", "js", "ts"}:
|
|
118
|
+
if not node_marker.is_file():
|
|
119
|
+
raise GeneratedRuntimeError(
|
|
120
|
+
"generated_runtime_dependency_manifest_missing: expected package.json"
|
|
121
|
+
)
|
|
122
|
+
language = "node"
|
|
123
|
+
elif python_markers and not node_marker.is_file():
|
|
124
|
+
language = "python"
|
|
125
|
+
elif node_marker.is_file() and not python_markers:
|
|
126
|
+
language = "node"
|
|
127
|
+
elif python_markers and node_marker.is_file():
|
|
128
|
+
raise GeneratedRuntimeError(
|
|
129
|
+
"generated_runtime_language_ambiguous: both Python and Node manifests exist; "
|
|
130
|
+
"set runtime.language and runtime.workdir to the intended component"
|
|
131
|
+
)
|
|
132
|
+
else:
|
|
133
|
+
nested = _nested_components(component)
|
|
134
|
+
if len(nested) == 1 and component == root:
|
|
135
|
+
component = nested[0]
|
|
136
|
+
return detect_generated_runtime(
|
|
137
|
+
root,
|
|
138
|
+
_runtime_override(
|
|
139
|
+
runtime, workdir=component.relative_to(root).as_posix()
|
|
140
|
+
),
|
|
141
|
+
)
|
|
142
|
+
if len(nested) > 1:
|
|
143
|
+
choices = ", ".join(
|
|
144
|
+
path.relative_to(root).as_posix() for path in nested[:12]
|
|
145
|
+
)
|
|
146
|
+
raise GeneratedRuntimeError(
|
|
147
|
+
"generated_runtime_component_ambiguous: multiple runnable components found: "
|
|
148
|
+
f"{choices}; set runtime.workdir"
|
|
149
|
+
)
|
|
150
|
+
raise GeneratedRuntimeError(
|
|
151
|
+
"generated_runtime_dependency_manifest_missing: no supported Python or Node "
|
|
152
|
+
"dependency manifest was found"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
explicit_command = _explicit_command(runtime)
|
|
156
|
+
relative_component = component.relative_to(root).as_posix()
|
|
157
|
+
if relative_component == ".":
|
|
158
|
+
relative_component = ""
|
|
159
|
+
if language == "python":
|
|
160
|
+
version = _python_language_version(
|
|
161
|
+
component, str(getattr(runtime, "version", "") or "")
|
|
162
|
+
)
|
|
163
|
+
command = explicit_command or _python_command(component)
|
|
164
|
+
dependency, strategy, install, extras = _python_install(
|
|
165
|
+
component, runtime, command
|
|
166
|
+
)
|
|
167
|
+
dockerfile = _python_dockerfile(version, dependency, strategy, install, command)
|
|
168
|
+
else:
|
|
169
|
+
version = _language_version(str(getattr(runtime, "version", "") or ""), "22")
|
|
170
|
+
dependency, strategy, install, runner = _node_install(component)
|
|
171
|
+
command = explicit_command or _node_command(component, runner)
|
|
172
|
+
install.extend(_node_build_steps(component, runner, command))
|
|
173
|
+
dockerfile = _node_dockerfile(version, dependency, install, command)
|
|
174
|
+
extras = []
|
|
175
|
+
return GeneratedRuntimePlan(
|
|
176
|
+
language=language,
|
|
177
|
+
component=relative_component,
|
|
178
|
+
version=version,
|
|
179
|
+
dependency_file=dependency,
|
|
180
|
+
install_strategy=strategy,
|
|
181
|
+
extras=tuple(extras),
|
|
182
|
+
command=tuple(command),
|
|
183
|
+
context_excludes=_context_excludes(component, runtime, dependency, command),
|
|
184
|
+
dockerfile=dockerfile,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def prepare_generated_runtime(
|
|
189
|
+
source: str | Path, destination: str | Path, runtime: Any | None = None
|
|
190
|
+
) -> GeneratedRuntimePlan:
|
|
191
|
+
"""Detect the runtime and create its sanitized, session-owned build context."""
|
|
192
|
+
source_root = Path(source).expanduser().resolve()
|
|
193
|
+
destination_root = Path(destination).expanduser().resolve()
|
|
194
|
+
try:
|
|
195
|
+
destination_root.relative_to(source_root)
|
|
196
|
+
except ValueError:
|
|
197
|
+
pass
|
|
198
|
+
else:
|
|
199
|
+
raise GeneratedRuntimeError(
|
|
200
|
+
"generated_runtime_destination_inside_source: session artifacts must be outside "
|
|
201
|
+
"the submitted repository"
|
|
202
|
+
)
|
|
203
|
+
plan = detect_generated_runtime(source_root, runtime)
|
|
204
|
+
component = source_root / plan.component if plan.component else source_root
|
|
205
|
+
context = destination_root / "generated-runtime-context"
|
|
206
|
+
if context.exists():
|
|
207
|
+
shutil.rmtree(context)
|
|
208
|
+
context.mkdir(parents=True)
|
|
209
|
+
_copy_sanitized_context(component, context, plan.context_excludes)
|
|
210
|
+
context.joinpath(GENERATED_DOCKERFILE).write_text(plan.dockerfile, encoding="utf-8")
|
|
211
|
+
prepared = GeneratedRuntimePlan(
|
|
212
|
+
**{**asdict(plan), "context_directory": str(context)}
|
|
213
|
+
)
|
|
214
|
+
destination_root.mkdir(parents=True, exist_ok=True)
|
|
215
|
+
destination_root.joinpath(GENERATED_PLAN).write_text(
|
|
216
|
+
json.dumps(
|
|
217
|
+
{
|
|
218
|
+
**asdict(prepared),
|
|
219
|
+
"fingerprint": prepared.fingerprint,
|
|
220
|
+
"generated_files": [
|
|
221
|
+
f"generated-runtime-context/{GENERATED_DOCKERFILE}",
|
|
222
|
+
GENERATED_PLAN,
|
|
223
|
+
],
|
|
224
|
+
},
|
|
225
|
+
indent=2,
|
|
226
|
+
),
|
|
227
|
+
encoding="utf-8",
|
|
228
|
+
)
|
|
229
|
+
return prepared
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _runtime_override(runtime: Any | None, **values: str) -> Any:
|
|
233
|
+
payload = {
|
|
234
|
+
"language": str(getattr(runtime, "language", "") or ""),
|
|
235
|
+
"version": str(getattr(runtime, "version", "") or ""),
|
|
236
|
+
"workdir": str(getattr(runtime, "workdir", "") or ""),
|
|
237
|
+
"command": list(getattr(runtime, "command", None) or []),
|
|
238
|
+
"extras": list(getattr(runtime, "extras", None) or []),
|
|
239
|
+
"context_excludes": list(
|
|
240
|
+
getattr(runtime, "context_excludes", None) or []
|
|
241
|
+
),
|
|
242
|
+
}
|
|
243
|
+
payload.update(values)
|
|
244
|
+
return type("RuntimeHints", (), payload)()
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _component_root(root: Path, configured: str) -> Path:
|
|
248
|
+
# Understanding occasionally records ``/`` to mean "the repository root". A generated
|
|
249
|
+
# runtime can never use the sandbox filesystem root as its source component; its hard
|
|
250
|
+
# boundary is the submitted checkout. Normalize only this root sentinel while continuing
|
|
251
|
+
# to reject every unrelated absolute path below.
|
|
252
|
+
if configured.strip() == "/":
|
|
253
|
+
return root
|
|
254
|
+
# Contracts produced for a selected monorepo component sometimes retain the
|
|
255
|
+
# repository-relative path with a leading slash (for example
|
|
256
|
+
# ``/examples/drive_thru``). The submitted ``root`` is already the hard
|
|
257
|
+
# filesystem boundary and the selected component, so an absolute hint whose
|
|
258
|
+
# final component names that root means "the selected root". Never resolve
|
|
259
|
+
# or read the absolute path itself.
|
|
260
|
+
configured_path = Path(configured) if configured else None
|
|
261
|
+
if (
|
|
262
|
+
configured_path is not None
|
|
263
|
+
and configured_path.is_absolute()
|
|
264
|
+
and configured_path.name == root.name
|
|
265
|
+
):
|
|
266
|
+
return root
|
|
267
|
+
# The same thing is common without a leading slash: understanding runs at
|
|
268
|
+
# a selected component but records its path relative to the original
|
|
269
|
+
# monorepo (``examples/hotel_receptionist``). If the submitted root already
|
|
270
|
+
# ends in that exact path, it is provenance for the selected root rather
|
|
271
|
+
# than a request to descend into it a second time.
|
|
272
|
+
if configured_path is not None and not configured_path.is_absolute():
|
|
273
|
+
configured_parts = tuple(part for part in configured_path.parts if part != ".")
|
|
274
|
+
if (
|
|
275
|
+
configured_parts
|
|
276
|
+
and tuple(root.parts[-len(configured_parts) :]) == configured_parts
|
|
277
|
+
):
|
|
278
|
+
return root
|
|
279
|
+
component = (root / configured).resolve() if configured else root
|
|
280
|
+
try:
|
|
281
|
+
component.relative_to(root)
|
|
282
|
+
except ValueError as exc:
|
|
283
|
+
raise GeneratedRuntimeError(
|
|
284
|
+
"generated_runtime_workdir_escapes_repository"
|
|
285
|
+
) from exc
|
|
286
|
+
if not component.is_dir():
|
|
287
|
+
raise GeneratedRuntimeError(
|
|
288
|
+
f"generated_runtime_workdir_missing: {configured or '.'}"
|
|
289
|
+
)
|
|
290
|
+
return component
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _python_markers(root: Path) -> list[Path]:
|
|
294
|
+
return [
|
|
295
|
+
root / name
|
|
296
|
+
for name in (
|
|
297
|
+
"pyproject.toml",
|
|
298
|
+
"requirements.txt",
|
|
299
|
+
"uv.lock",
|
|
300
|
+
"poetry.lock",
|
|
301
|
+
"setup.py",
|
|
302
|
+
)
|
|
303
|
+
if root.joinpath(name).is_file()
|
|
304
|
+
]
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _nested_components(root: Path) -> list[Path]:
|
|
308
|
+
found: set[Path] = set()
|
|
309
|
+
for marker in ("pyproject.toml", "requirements.txt", "setup.py", "package.json"):
|
|
310
|
+
for path in root.glob(f"*/*/{marker}"):
|
|
311
|
+
if not any(
|
|
312
|
+
part in _IGNORED_DIRECTORIES for part in path.relative_to(root).parts
|
|
313
|
+
):
|
|
314
|
+
found.add(path.parent)
|
|
315
|
+
for path in root.glob(f"*/{marker}"):
|
|
316
|
+
if not any(
|
|
317
|
+
part in _IGNORED_DIRECTORIES for part in path.relative_to(root).parts
|
|
318
|
+
):
|
|
319
|
+
found.add(path.parent)
|
|
320
|
+
return sorted(found)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _explicit_command(runtime: Any | None) -> list[str]:
|
|
324
|
+
value = getattr(runtime, "command", None)
|
|
325
|
+
if not value:
|
|
326
|
+
return []
|
|
327
|
+
if isinstance(value, str):
|
|
328
|
+
try:
|
|
329
|
+
command = shlex.split(value)
|
|
330
|
+
except ValueError as exc:
|
|
331
|
+
raise GeneratedRuntimeError("generated_runtime_command_invalid") from exc
|
|
332
|
+
else:
|
|
333
|
+
command = [str(item) for item in value]
|
|
334
|
+
if not command or any(not item or "\x00" in item for item in command):
|
|
335
|
+
raise GeneratedRuntimeError("generated_runtime_command_invalid")
|
|
336
|
+
return command
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _language_version(value: str, default: str) -> str:
|
|
340
|
+
if not value:
|
|
341
|
+
return default
|
|
342
|
+
match = re.search(r"(?<!\d)(\d{1,2}(?:\.\d{1,2})?)(?!\d)", value)
|
|
343
|
+
return match.group(1) if match else default
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _python_language_version(root: Path, configured: str) -> str:
|
|
347
|
+
"""Choose a concrete image version that satisfies a declared Python range.
|
|
348
|
+
|
|
349
|
+
``>=3.10`` is a compatibility floor, not an instruction to use 3.10. Selecting the first
|
|
350
|
+
number from it makes otherwise reproducible projects fail as transitive dependencies retire
|
|
351
|
+
old interpreter wheels. Exact values remain exact; ranges prefer the harness' stable default.
|
|
352
|
+
"""
|
|
353
|
+
declared = configured.strip()
|
|
354
|
+
if not declared:
|
|
355
|
+
requires = (
|
|
356
|
+
_pyproject_document(root / "pyproject.toml")
|
|
357
|
+
.get("project", {})
|
|
358
|
+
.get("requires-python", "")
|
|
359
|
+
)
|
|
360
|
+
declared = str(requires or "").strip()
|
|
361
|
+
if not declared:
|
|
362
|
+
return "3.12"
|
|
363
|
+
if not any(marker in declared for marker in "<>=!~,*"):
|
|
364
|
+
return _language_version(declared, "3.12")
|
|
365
|
+
try:
|
|
366
|
+
from packaging.specifiers import InvalidSpecifier, SpecifierSet
|
|
367
|
+
from packaging.version import Version
|
|
368
|
+
|
|
369
|
+
specifier = SpecifierSet(declared)
|
|
370
|
+
for candidate in ("3.12", "3.13", "3.11", "3.10", "3.14", "3.9"):
|
|
371
|
+
if Version(candidate) in specifier:
|
|
372
|
+
return candidate
|
|
373
|
+
except (InvalidSpecifier, ValueError):
|
|
374
|
+
pass
|
|
375
|
+
raise GeneratedRuntimeError(
|
|
376
|
+
f"generated_runtime_python_version_unsupported: {declared}"
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _python_install(
|
|
381
|
+
root: Path, runtime: Any | None, command: list[str]
|
|
382
|
+
) -> tuple[str, str, list[str], list[str]]:
|
|
383
|
+
extras = _python_extras(root, runtime, command)
|
|
384
|
+
uv_extras = [value for extra in extras for value in ("--extra", extra)]
|
|
385
|
+
uv_script_project = _uv_script_project(root / "pyproject.toml")
|
|
386
|
+
if root.joinpath("uv.lock").is_file() and root.joinpath("pyproject.toml").is_file():
|
|
387
|
+
return (
|
|
388
|
+
"uv.lock",
|
|
389
|
+
"uv-frozen",
|
|
390
|
+
[
|
|
391
|
+
"RUN pip install --no-cache-dir uv==0.8.13",
|
|
392
|
+
"RUN " + shlex.join(["uv", "sync", "--frozen", *uv_extras]),
|
|
393
|
+
'ENV PATH="/app/.venv/bin:$PATH"',
|
|
394
|
+
],
|
|
395
|
+
extras,
|
|
396
|
+
)
|
|
397
|
+
if root.joinpath("requirements.txt").is_file():
|
|
398
|
+
return (
|
|
399
|
+
"requirements.txt",
|
|
400
|
+
"pip-requirements",
|
|
401
|
+
["RUN pip install --no-cache-dir -r requirements.txt"],
|
|
402
|
+
[],
|
|
403
|
+
)
|
|
404
|
+
if uv_script_project:
|
|
405
|
+
install = [
|
|
406
|
+
"RUN pip install --no-cache-dir uv==0.8.13",
|
|
407
|
+
"RUN " + shlex.join(["uv", "sync", "--no-cache", *uv_extras]),
|
|
408
|
+
'ENV PATH="/app/.venv/bin:$PATH"',
|
|
409
|
+
]
|
|
410
|
+
if _needs_livekit_model_download(root / "pyproject.toml"):
|
|
411
|
+
install.append("RUN python -m livekit.agents download-files")
|
|
412
|
+
return (
|
|
413
|
+
"pyproject.toml",
|
|
414
|
+
"uv-script-project-unlocked",
|
|
415
|
+
install,
|
|
416
|
+
extras,
|
|
417
|
+
)
|
|
418
|
+
if (
|
|
419
|
+
root.joinpath("poetry.lock").is_file()
|
|
420
|
+
and root.joinpath("pyproject.toml").is_file()
|
|
421
|
+
):
|
|
422
|
+
return (
|
|
423
|
+
"poetry.lock",
|
|
424
|
+
"poetry-locked",
|
|
425
|
+
[
|
|
426
|
+
"RUN pip install --no-cache-dir poetry==2.1.3",
|
|
427
|
+
"RUN poetry config virtualenvs.create false && "
|
|
428
|
+
+ shlex.join(
|
|
429
|
+
[
|
|
430
|
+
"poetry",
|
|
431
|
+
"install",
|
|
432
|
+
"--only",
|
|
433
|
+
"main",
|
|
434
|
+
"--no-interaction",
|
|
435
|
+
*(["--extras", " ".join(extras)] if extras else []),
|
|
436
|
+
]
|
|
437
|
+
),
|
|
438
|
+
],
|
|
439
|
+
extras,
|
|
440
|
+
)
|
|
441
|
+
if root.joinpath("pyproject.toml").is_file():
|
|
442
|
+
target = ".[" + ",".join(extras) + "]" if extras else "."
|
|
443
|
+
return (
|
|
444
|
+
"pyproject.toml",
|
|
445
|
+
"pip-project",
|
|
446
|
+
["RUN " + shlex.join(["pip", "install", "--no-cache-dir", target])],
|
|
447
|
+
extras,
|
|
448
|
+
)
|
|
449
|
+
if root.joinpath("setup.py").is_file():
|
|
450
|
+
return (
|
|
451
|
+
"setup.py",
|
|
452
|
+
"pip-setup-project-unlocked",
|
|
453
|
+
["RUN pip install --no-cache-dir ."],
|
|
454
|
+
[],
|
|
455
|
+
)
|
|
456
|
+
raise GeneratedRuntimeError("generated_runtime_python_dependencies_unsupported")
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def _pyproject_document(path: Path) -> dict[str, Any]:
|
|
460
|
+
if not path.is_file():
|
|
461
|
+
return {}
|
|
462
|
+
try:
|
|
463
|
+
import tomllib
|
|
464
|
+
|
|
465
|
+
value = tomllib.loads(path.read_text(encoding="utf-8"))
|
|
466
|
+
except (OSError, ValueError):
|
|
467
|
+
return {}
|
|
468
|
+
return value if isinstance(value, dict) else {}
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def _uv_script_project(path: Path) -> bool:
|
|
472
|
+
uv = _pyproject_document(path).get("tool", {}).get("uv", {})
|
|
473
|
+
return isinstance(uv, dict) and uv.get("package") is False
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def _needs_livekit_model_download(path: Path) -> bool:
|
|
477
|
+
dependencies = _pyproject_document(path).get("project", {}).get("dependencies", [])
|
|
478
|
+
if not isinstance(dependencies, list):
|
|
479
|
+
return False
|
|
480
|
+
normalized = [str(value).lower().replace("_", "-") for value in dependencies]
|
|
481
|
+
return any(value.startswith("livekit-agents") for value in normalized) and any(
|
|
482
|
+
value.startswith(("livekit-plugins-silero", "livekit-plugins-turn-detector"))
|
|
483
|
+
for value in normalized
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def _python_extras(root: Path, runtime: Any | None, command: list[str]) -> list[str]:
|
|
488
|
+
path = root / "pyproject.toml"
|
|
489
|
+
if not path.is_file():
|
|
490
|
+
return []
|
|
491
|
+
try:
|
|
492
|
+
import tomllib
|
|
493
|
+
|
|
494
|
+
document = tomllib.loads(path.read_text(encoding="utf-8"))
|
|
495
|
+
except (OSError, ValueError):
|
|
496
|
+
return []
|
|
497
|
+
declared = document.get("project", {}).get("optional-dependencies", {})
|
|
498
|
+
available = (
|
|
499
|
+
sorted(str(name) for name in declared) if isinstance(declared, dict) else []
|
|
500
|
+
)
|
|
501
|
+
requested = [str(item) for item in (getattr(runtime, "extras", None) or [])]
|
|
502
|
+
if requested:
|
|
503
|
+
unknown = sorted(set(requested) - set(available))
|
|
504
|
+
if unknown:
|
|
505
|
+
raise GeneratedRuntimeError(
|
|
506
|
+
"generated_runtime_extra_unknown: " + ", ".join(unknown)
|
|
507
|
+
)
|
|
508
|
+
return sorted(set(requested))
|
|
509
|
+
tokens = re.split(r"[^a-z0-9]+", " ".join(command).lower())
|
|
510
|
+
return [
|
|
511
|
+
extra
|
|
512
|
+
for extra in available
|
|
513
|
+
if extra.lower() not in {"all", "dev", "test", "tests"}
|
|
514
|
+
and extra.lower() in tokens
|
|
515
|
+
]
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def _python_command(root: Path) -> list[str]:
|
|
519
|
+
scripts = _pyproject_scripts(root / "pyproject.toml")
|
|
520
|
+
if len(scripts) == 1:
|
|
521
|
+
return [scripts[0]]
|
|
522
|
+
if len(scripts) > 1:
|
|
523
|
+
raise GeneratedRuntimeError(
|
|
524
|
+
"generated_runtime_entrypoint_ambiguous: multiple pyproject scripts exist: "
|
|
525
|
+
+ ", ".join(scripts)
|
|
526
|
+
)
|
|
527
|
+
candidates = [name for name in _PYTHON_ENTRYPOINTS if root.joinpath(name).is_file()]
|
|
528
|
+
runnable = [
|
|
529
|
+
name
|
|
530
|
+
for name in candidates
|
|
531
|
+
if "__main__"
|
|
532
|
+
in root.joinpath(name).read_text(encoding="utf-8", errors="replace")
|
|
533
|
+
or "cli.run_app"
|
|
534
|
+
in root.joinpath(name).read_text(encoding="utf-8", errors="replace")
|
|
535
|
+
]
|
|
536
|
+
choices = runnable or candidates
|
|
537
|
+
if len(choices) != 1:
|
|
538
|
+
detail = ", ".join(choices) if choices else "none"
|
|
539
|
+
raise GeneratedRuntimeError(
|
|
540
|
+
"generated_runtime_entrypoint_ambiguous: expected exactly one runnable Python "
|
|
541
|
+
f"entrypoint, found {detail}; set runtime.command"
|
|
542
|
+
)
|
|
543
|
+
command = ["python", choices[0]]
|
|
544
|
+
content = root.joinpath(choices[0]).read_text(encoding="utf-8", errors="replace")
|
|
545
|
+
if "cli.run_app" in content:
|
|
546
|
+
command.append("start")
|
|
547
|
+
return command
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def _pyproject_scripts(path: Path) -> list[str]:
|
|
551
|
+
if not path.is_file():
|
|
552
|
+
return []
|
|
553
|
+
try:
|
|
554
|
+
import tomllib
|
|
555
|
+
|
|
556
|
+
document = tomllib.loads(path.read_text(encoding="utf-8"))
|
|
557
|
+
except (OSError, ValueError):
|
|
558
|
+
return []
|
|
559
|
+
scripts = document.get("project", {}).get("scripts", {})
|
|
560
|
+
return sorted(str(name) for name in scripts) if isinstance(scripts, dict) else []
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def _node_install(root: Path) -> tuple[str, str, list[str], str]:
|
|
564
|
+
if root.joinpath("pnpm-lock.yaml").is_file():
|
|
565
|
+
return (
|
|
566
|
+
"pnpm-lock.yaml",
|
|
567
|
+
"pnpm-frozen",
|
|
568
|
+
["RUN corepack enable && pnpm install --frozen-lockfile"],
|
|
569
|
+
"pnpm",
|
|
570
|
+
)
|
|
571
|
+
if root.joinpath("package-lock.json").is_file():
|
|
572
|
+
return "package-lock.json", "npm-ci", ["RUN npm ci"], "npm"
|
|
573
|
+
if root.joinpath("yarn.lock").is_file():
|
|
574
|
+
return (
|
|
575
|
+
"yarn.lock",
|
|
576
|
+
"yarn-frozen",
|
|
577
|
+
["RUN corepack enable && yarn install --immutable"],
|
|
578
|
+
"yarn",
|
|
579
|
+
)
|
|
580
|
+
return "package.json", "npm-install-unlocked", ["RUN npm install"], "npm"
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def _node_command(root: Path, runner: str) -> list[str]:
|
|
584
|
+
try:
|
|
585
|
+
package = json.loads(root.joinpath("package.json").read_text(encoding="utf-8"))
|
|
586
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
587
|
+
raise GeneratedRuntimeError("generated_runtime_package_json_invalid") from exc
|
|
588
|
+
scripts = package.get("scripts", {})
|
|
589
|
+
if isinstance(scripts, dict) and scripts.get("start"):
|
|
590
|
+
return [runner, "run", "start"] if runner == "npm" else [runner, "start"]
|
|
591
|
+
main = package.get("main")
|
|
592
|
+
if isinstance(main, str) and main and root.joinpath(main).is_file():
|
|
593
|
+
return ["node", main]
|
|
594
|
+
candidates = [name for name in _NODE_ENTRYPOINTS if root.joinpath(name).is_file()]
|
|
595
|
+
if len(candidates) != 1:
|
|
596
|
+
detail = ", ".join(candidates) if candidates else "none"
|
|
597
|
+
raise GeneratedRuntimeError(
|
|
598
|
+
"generated_runtime_entrypoint_ambiguous: package.json needs a start script or "
|
|
599
|
+
f"one existing main file, found {detail}; set runtime.command"
|
|
600
|
+
)
|
|
601
|
+
return ["node", candidates[0]]
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def _node_build_steps(root: Path, runner: str, command: list[str]) -> list[str]:
|
|
605
|
+
"""Build a submitted Node application when its selected start script needs that phase."""
|
|
606
|
+
try:
|
|
607
|
+
package = json.loads(root.joinpath("package.json").read_text(encoding="utf-8"))
|
|
608
|
+
except (OSError, json.JSONDecodeError):
|
|
609
|
+
return []
|
|
610
|
+
scripts = package.get("scripts", {})
|
|
611
|
+
if not isinstance(scripts, dict) or not scripts.get("build"):
|
|
612
|
+
return []
|
|
613
|
+
selected_start = command in (["npm", "run", "start"], [runner, "start"])
|
|
614
|
+
if not selected_start:
|
|
615
|
+
return []
|
|
616
|
+
invocation = [runner, "run", "build"] if runner == "npm" else [runner, "build"]
|
|
617
|
+
return ["RUN " + shlex.join(invocation)]
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def _python_dockerfile(
|
|
621
|
+
version: str,
|
|
622
|
+
dependency: str,
|
|
623
|
+
strategy: str,
|
|
624
|
+
install: list[str],
|
|
625
|
+
command: list[str],
|
|
626
|
+
) -> str:
|
|
627
|
+
del dependency, strategy
|
|
628
|
+
return "\n".join(
|
|
629
|
+
[
|
|
630
|
+
f"FROM python:{version}-slim",
|
|
631
|
+
"ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1",
|
|
632
|
+
# Poorly packaged repositories commonly declare Python extensions that
|
|
633
|
+
# only publish wheels for a subset of Python/CPU combinations. A slim
|
|
634
|
+
# image has no compiler, so otherwise a valid manifest fails depending
|
|
635
|
+
# on the runner architecture (for example webrtcvad on arm64). This is
|
|
636
|
+
# runtime infrastructure, not a rewrite of the submitted application.
|
|
637
|
+
"RUN apt-get update && apt-get install -y --no-install-recommends build-essential && rm -rf /var/lib/apt/lists/*",
|
|
638
|
+
"WORKDIR /app",
|
|
639
|
+
"COPY . /app",
|
|
640
|
+
*install,
|
|
641
|
+
"RUN groupadd --gid 10001 alk && useradd --uid 10001 --gid 10001 --no-create-home alk && chown -R alk:alk /app",
|
|
642
|
+
"USER 10001:10001",
|
|
643
|
+
"CMD " + json.dumps(command),
|
|
644
|
+
"",
|
|
645
|
+
]
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def _node_dockerfile(
|
|
650
|
+
version: str, dependency: str, install: list[str], command: list[str]
|
|
651
|
+
) -> str:
|
|
652
|
+
del dependency
|
|
653
|
+
return "\n".join(
|
|
654
|
+
[
|
|
655
|
+
f"FROM node:{version}-slim",
|
|
656
|
+
"WORKDIR /app",
|
|
657
|
+
"COPY --chown=node:node . /app",
|
|
658
|
+
*install,
|
|
659
|
+
"USER node",
|
|
660
|
+
"CMD " + json.dumps(command),
|
|
661
|
+
"",
|
|
662
|
+
]
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def _context_excludes(
|
|
667
|
+
component: Path, runtime: Any | None, dependency: str, command: list[str]
|
|
668
|
+
) -> tuple[str, ...]:
|
|
669
|
+
required = {
|
|
670
|
+
Path(name)
|
|
671
|
+
for name in (dependency, "pyproject.toml", "package.json")
|
|
672
|
+
if name and component.joinpath(name).is_file()
|
|
673
|
+
}
|
|
674
|
+
required.update(
|
|
675
|
+
Path(token)
|
|
676
|
+
for token in command
|
|
677
|
+
if token
|
|
678
|
+
and not Path(token).is_absolute()
|
|
679
|
+
and ".." not in Path(token).parts
|
|
680
|
+
and component.joinpath(token).is_file()
|
|
681
|
+
)
|
|
682
|
+
values: set[str] = set()
|
|
683
|
+
for raw in getattr(runtime, "context_excludes", None) or []:
|
|
684
|
+
relative = Path(str(raw))
|
|
685
|
+
if relative.is_absolute() or not relative.parts or ".." in relative.parts:
|
|
686
|
+
raise GeneratedRuntimeError(
|
|
687
|
+
f"generated_runtime_context_exclude_invalid: {raw}"
|
|
688
|
+
)
|
|
689
|
+
normalized = relative.as_posix().removeprefix("./")
|
|
690
|
+
target = (component / normalized).resolve()
|
|
691
|
+
try:
|
|
692
|
+
target.relative_to(component.resolve())
|
|
693
|
+
except ValueError as exc:
|
|
694
|
+
raise GeneratedRuntimeError(
|
|
695
|
+
f"generated_runtime_context_exclude_invalid: {raw}"
|
|
696
|
+
) from exc
|
|
697
|
+
if not target.exists():
|
|
698
|
+
# Generated/local state files are commonly listed even before the
|
|
699
|
+
# first run creates them. A missing exclusion is already excluded
|
|
700
|
+
# from the immutable build context and is therefore safe to ignore.
|
|
701
|
+
continue
|
|
702
|
+
excluded_path = Path(normalized)
|
|
703
|
+
if any(
|
|
704
|
+
excluded_path == required_path or excluded_path in required_path.parents
|
|
705
|
+
for required_path in required
|
|
706
|
+
):
|
|
707
|
+
raise GeneratedRuntimeError(
|
|
708
|
+
f"generated_runtime_context_exclude_required: {normalized}"
|
|
709
|
+
)
|
|
710
|
+
values.add(normalized)
|
|
711
|
+
return tuple(sorted(values))
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
def _copy_sanitized_context(
|
|
715
|
+
source: Path, destination: Path, exclusions: tuple[str, ...] = ()
|
|
716
|
+
) -> None:
|
|
717
|
+
count = 0
|
|
718
|
+
total = 0
|
|
719
|
+
source = source.resolve()
|
|
720
|
+
excluded = tuple(Path(value) for value in exclusions)
|
|
721
|
+
|
|
722
|
+
def is_excluded(relative: Path) -> bool:
|
|
723
|
+
return any(relative == value or value in relative.parents for value in excluded)
|
|
724
|
+
|
|
725
|
+
def copy_file(path: Path, relative: Path) -> None:
|
|
726
|
+
nonlocal count, total
|
|
727
|
+
if _sensitive_file(relative):
|
|
728
|
+
return
|
|
729
|
+
info = path.stat()
|
|
730
|
+
if not stat.S_ISREG(info.st_mode):
|
|
731
|
+
raise GeneratedRuntimeError(
|
|
732
|
+
f"generated_runtime_file_type_unsupported: {relative.as_posix()}"
|
|
733
|
+
)
|
|
734
|
+
if info.st_size > _MAX_FILE_BYTES:
|
|
735
|
+
raise GeneratedRuntimeError(
|
|
736
|
+
f"generated_runtime_file_too_large: {relative.as_posix()}"
|
|
737
|
+
)
|
|
738
|
+
count += 1
|
|
739
|
+
total += info.st_size
|
|
740
|
+
if count > _MAX_FILES or total > _MAX_CONTEXT_BYTES:
|
|
741
|
+
raise GeneratedRuntimeError("generated_runtime_context_limit_exceeded")
|
|
742
|
+
with path.open("rb") as stream:
|
|
743
|
+
while chunk := stream.read(1024 * 1024):
|
|
744
|
+
if any(pattern.search(chunk) for pattern in _SECRET_CONTENT):
|
|
745
|
+
raise GeneratedRuntimeError(
|
|
746
|
+
"generated_runtime_secret_material_detected: "
|
|
747
|
+
+ relative.as_posix()
|
|
748
|
+
)
|
|
749
|
+
target = destination / relative
|
|
750
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
751
|
+
shutil.copy2(path, target, follow_symlinks=True)
|
|
752
|
+
|
|
753
|
+
def copy_directory(
|
|
754
|
+
directory: Path, relative_directory: Path, ancestors: frozenset[Path]
|
|
755
|
+
) -> None:
|
|
756
|
+
resolved_directory = directory.resolve()
|
|
757
|
+
try:
|
|
758
|
+
resolved_directory.relative_to(source)
|
|
759
|
+
except ValueError as exc:
|
|
760
|
+
raise GeneratedRuntimeError(
|
|
761
|
+
f"generated_runtime_symlink_forbidden: {relative_directory.as_posix()}"
|
|
762
|
+
) from exc
|
|
763
|
+
if resolved_directory in ancestors:
|
|
764
|
+
raise GeneratedRuntimeError(
|
|
765
|
+
f"generated_runtime_symlink_cycle: {relative_directory.as_posix()}"
|
|
766
|
+
)
|
|
767
|
+
next_ancestors = ancestors | {resolved_directory}
|
|
768
|
+
for path in sorted(directory.iterdir(), key=lambda item: item.name):
|
|
769
|
+
relative = relative_directory / path.name
|
|
770
|
+
if is_excluded(relative):
|
|
771
|
+
continue
|
|
772
|
+
if path.name in _IGNORED_DIRECTORIES and path.is_dir():
|
|
773
|
+
continue
|
|
774
|
+
if path.is_symlink():
|
|
775
|
+
try:
|
|
776
|
+
resolved = path.resolve(strict=True)
|
|
777
|
+
resolved.relative_to(source)
|
|
778
|
+
except (OSError, ValueError) as exc:
|
|
779
|
+
raise GeneratedRuntimeError(
|
|
780
|
+
f"generated_runtime_symlink_forbidden: {relative.as_posix()}"
|
|
781
|
+
) from exc
|
|
782
|
+
if resolved.is_dir():
|
|
783
|
+
copy_directory(resolved, relative, next_ancestors)
|
|
784
|
+
else:
|
|
785
|
+
copy_file(resolved, relative)
|
|
786
|
+
elif path.is_dir():
|
|
787
|
+
copy_directory(path, relative, next_ancestors)
|
|
788
|
+
elif path.is_file():
|
|
789
|
+
copy_file(path, relative)
|
|
790
|
+
else:
|
|
791
|
+
raise GeneratedRuntimeError(
|
|
792
|
+
f"generated_runtime_file_type_unsupported: {relative.as_posix()}"
|
|
793
|
+
)
|
|
794
|
+
|
|
795
|
+
copy_directory(source, Path(), frozenset())
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
def _sensitive_file(relative: Path) -> bool:
|
|
799
|
+
name = relative.name.lower()
|
|
800
|
+
return (
|
|
801
|
+
name in _SECRET_NAMES
|
|
802
|
+
or name.startswith(".env.")
|
|
803
|
+
or relative.suffix.lower() in _SECRET_SUFFIXES
|
|
804
|
+
)
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
__all__ = [
|
|
808
|
+
"GENERATED_DOCKERFILE",
|
|
809
|
+
"GENERATED_PLAN",
|
|
810
|
+
"GeneratedRuntimeError",
|
|
811
|
+
"GeneratedRuntimePlan",
|
|
812
|
+
"can_generate_runtime",
|
|
813
|
+
"detect_generated_runtime",
|
|
814
|
+
"prepare_generated_runtime",
|
|
815
|
+
]
|