agentinputlab 0.1.0a1__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.
- agentinputlab/__init__.py +28 -0
- agentinputlab/algorithms/__init__.py +6 -0
- agentinputlab/algorithms/acquire/__init__.py +145 -0
- agentinputlab/algorithms/acquire/adaptive_rag/__init__.py +43 -0
- agentinputlab/algorithms/acquire/adaptive_rag/replay.py +367 -0
- agentinputlab/algorithms/acquire/adaptive_rag/runtime.py +1202 -0
- agentinputlab/algorithms/acquire/adaptive_rag_common_support.py +542 -0
- agentinputlab/algorithms/acquire/hyde.py +296 -0
- agentinputlab/algorithms/acquire/hyde_controlled.py +200 -0
- agentinputlab/algorithms/acquire/hyde_external.py +148 -0
- agentinputlab/algorithms/acquire/ircot.py +28 -0
- agentinputlab/algorithms/acquire/ircot_common_support.py +478 -0
- agentinputlab/algorithms/acquire/ircot_native.py +355 -0
- agentinputlab/algorithms/acquire/ircot_replay.py +327 -0
- agentinputlab/algorithms/acquire/qrag/__init__.py +79 -0
- agentinputlab/algorithms/acquire/qrag/bridge.py +828 -0
- agentinputlab/algorithms/acquire/qrag/common_support.py +891 -0
- agentinputlab/algorithms/acquire/qrag/native_axis.py +71 -0
- agentinputlab/algorithms/acquire/qrag/reader_adapter.py +139 -0
- agentinputlab/algorithms/acquire/r2ag/__init__.py +57 -0
- agentinputlab/algorithms/acquire/r2ag/bridge.py +1312 -0
- agentinputlab/algorithms/acquire/r2ag/common_support.py +1029 -0
- agentinputlab/algorithms/acquire/r2ag/socket_runtime.py +255 -0
- agentinputlab/algorithms/clarify/__init__.py +61 -0
- agentinputlab/algorithms/clarify/_structured.py +64 -0
- agentinputlab/algorithms/clarify/active_task_disambiguation.py +371 -0
- agentinputlab/algorithms/clarify/ask_or_assume/__init__.py +17 -0
- agentinputlab/algorithms/clarify/ask_or_assume/monitor.py +410 -0
- agentinputlab/algorithms/clarify/future_turns.py +379 -0
- agentinputlab/algorithms/clarify/udcs.py +255 -0
- agentinputlab/algorithms/clarify/udcs_deterministic.py +196 -0
- agentinputlab/algorithms/curate/__init__.py +65 -0
- agentinputlab/algorithms/curate/acon/__init__.py +27 -0
- agentinputlab/algorithms/curate/acon/common.py +284 -0
- agentinputlab/algorithms/curate/acon/common_support.py +136 -0
- agentinputlab/algorithms/curate/acon/history_bridge.py +309 -0
- agentinputlab/algorithms/curate/acon/observation_bridge.py +290 -0
- agentinputlab/algorithms/curate/llmlingua/__init__.py +30 -0
- agentinputlab/algorithms/curate/llmlingua/adapters.py +537 -0
- agentinputlab/algorithms/curate/llmlingua/budget.py +48 -0
- agentinputlab/algorithms/curate/llmlingua/controlled.py +175 -0
- agentinputlab/algorithms/curate/llmlingua/multilingual.py +105 -0
- agentinputlab/algorithms/curate/llmlingua/span_map.py +167 -0
- agentinputlab/algorithms/curate/recomp/__init__.py +47 -0
- agentinputlab/algorithms/curate/recomp/external.py +1129 -0
- agentinputlab/algorithms/curate/recomp/selective.py +304 -0
- agentinputlab/algorithms/decompose/__init__.py +107 -0
- agentinputlab/algorithms/decompose/decomp/__init__.py +53 -0
- agentinputlab/algorithms/decompose/decomp/common_support.py +478 -0
- agentinputlab/algorithms/decompose/decomp/planner_component.py +199 -0
- agentinputlab/algorithms/decompose/decomp/worker_bridge.py +416 -0
- agentinputlab/algorithms/decompose/llm_compiler/__init__.py +35 -0
- agentinputlab/algorithms/decompose/llm_compiler/bridge.py +520 -0
- agentinputlab/algorithms/decompose/llm_compiler/common_support.py +504 -0
- agentinputlab/algorithms/decompose/plan_and_budget/__init__.py +43 -0
- agentinputlab/algorithms/decompose/plan_and_budget/cached_soft_budget.py +362 -0
- agentinputlab/algorithms/decompose/plan_and_budget/decomposer_component.py +107 -0
- agentinputlab/algorithms/decompose/plan_and_budget/full.py +205 -0
- agentinputlab/algorithms/decompose/plan_and_budget/hard_cap.py +246 -0
- agentinputlab/algorithms/decompose/plan_and_budget/regenerated_soft_budget.py +448 -0
- agentinputlab/algorithms/decompose/plan_and_solve/__init__.py +33 -0
- agentinputlab/algorithms/decompose/plan_and_solve/plan_component.py +192 -0
- agentinputlab/algorithms/decompose/plan_and_solve/ps_prompt.py +127 -0
- agentinputlab/algorithms/decompose/self_ask/__init__.py +27 -0
- agentinputlab/algorithms/decompose/self_ask/loop.py +362 -0
- agentinputlab/algorithms/decompose/self_ask/planner_component.py +117 -0
- agentinputlab/algorithms/reformulate/__init__.py +85 -0
- agentinputlab/algorithms/reformulate/infocqr/__init__.py +40 -0
- agentinputlab/algorithms/reformulate/infocqr/cache_replay.py +362 -0
- agentinputlab/algorithms/reformulate/infocqr/external.py +675 -0
- agentinputlab/algorithms/reformulate/infocqr/student.py +189 -0
- agentinputlab/algorithms/reformulate/query2doc/__init__.py +42 -0
- agentinputlab/algorithms/reformulate/query2doc/controlled_live.py +190 -0
- agentinputlab/algorithms/reformulate/query2doc/models.py +95 -0
- agentinputlab/algorithms/reformulate/query2doc/official_cached.py +335 -0
- agentinputlab/algorithms/reformulate/query2doc/querygym_bridge.py +223 -0
- agentinputlab/algorithms/reformulate/query2doc/querygym_runtime.py +326 -0
- agentinputlab/algorithms/reformulate/reformer/__init__.py +23 -0
- agentinputlab/algorithms/reformulate/reformer/external.py +417 -0
- agentinputlab/algorithms/reformulate/rrr.py +696 -0
- agentinputlab/annotation/__init__.py +5 -0
- agentinputlab/annotation/independent.py +428 -0
- agentinputlab/annotation/server.py +233 -0
- agentinputlab/annotation/static/app.js +121 -0
- agentinputlab/annotation/static/index.html +60 -0
- agentinputlab/annotation/static/review.css +2 -0
- agentinputlab/annotation/static/review.html +10 -0
- agentinputlab/annotation/static/review.js +11 -0
- agentinputlab/annotation/static/styles.css +55 -0
- agentinputlab/annotation/store.py +444 -0
- agentinputlab/api.py +368 -0
- agentinputlab/artifacts/__init__.py +15 -0
- agentinputlab/artifacts/json_file.py +104 -0
- agentinputlab/artifacts/manifest.py +98 -0
- agentinputlab/artifacts/store.py +157 -0
- agentinputlab/backends/__init__.py +98 -0
- agentinputlab/backends/embedding/__init__.py +25 -0
- agentinputlab/backends/embedding/base.py +145 -0
- agentinputlab/backends/embedding/callable.py +75 -0
- agentinputlab/backends/embedding/replay.py +129 -0
- agentinputlab/backends/llm/__init__.py +31 -0
- agentinputlab/backends/llm/base.py +149 -0
- agentinputlab/backends/llm/callable.py +108 -0
- agentinputlab/backends/llm/decomp_completion.py +88 -0
- agentinputlab/backends/llm/final_content_stop.py +72 -0
- agentinputlab/backends/llm/gpt_oss_final_channel.py +84 -0
- agentinputlab/backends/llm/openai_compatible.py +332 -0
- agentinputlab/backends/llm/transformers.py +171 -0
- agentinputlab/backends/retrieval/__init__.py +35 -0
- agentinputlab/backends/retrieval/base.py +50 -0
- agentinputlab/backends/retrieval/dense.py +777 -0
- agentinputlab/backends/retrieval/memory.py +119 -0
- agentinputlab/backends/retrieval/sqlite_fts.py +155 -0
- agentinputlab/backends/user/__init__.py +15 -0
- agentinputlab/backends/user/base.py +66 -0
- agentinputlab/backends/user/callable.py +54 -0
- agentinputlab/backends/user/replay.py +89 -0
- agentinputlab/backends/vector.py +38 -0
- agentinputlab/benchmarks/__init__.py +246 -0
- agentinputlab/benchmarks/acquisition.py +106 -0
- agentinputlab/benchmarks/ale.py +540 -0
- agentinputlab/benchmarks/appworld_ul.py +669 -0
- agentinputlab/benchmarks/assets.py +226 -0
- agentinputlab/benchmarks/base.py +68 -0
- agentinputlab/benchmarks/beir_archive.py +235 -0
- agentinputlab/benchmarks/beir_sample.py +269 -0
- agentinputlab/benchmarks/bfcl_native.py +245 -0
- agentinputlab/benchmarks/bfcl_v3.py +607 -0
- agentinputlab/benchmarks/cache.py +218 -0
- agentinputlab/benchmarks/clariq.py +203 -0
- agentinputlab/benchmarks/data/registry.json +321 -0
- agentinputlab/benchmarks/data/smoke_assets.json +511 -0
- agentinputlab/benchmarks/data/statebench_v0_8_1_task_ids.json +475 -0
- agentinputlab/benchmarks/data/toolsandbox_165848b_scenario_names.json +1 -0
- agentinputlab/benchmarks/execution.py +1111 -0
- agentinputlab/benchmarks/interactive.py +257 -0
- agentinputlab/benchmarks/ir_query.py +204 -0
- agentinputlab/benchmarks/longbench.py +203 -0
- agentinputlab/benchmarks/paired_replay.py +479 -0
- agentinputlab/benchmarks/preprocess.py +2313 -0
- agentinputlab/benchmarks/production.py +236 -0
- agentinputlab/benchmarks/qa.py +963 -0
- agentinputlab/benchmarks/registry.py +110 -0
- agentinputlab/benchmarks/retrieval.py +459 -0
- agentinputlab/benchmarks/runtime.py +509 -0
- agentinputlab/benchmarks/statebench.py +938 -0
- agentinputlab/benchmarks/tau3.py +725 -0
- agentinputlab/benchmarks/toolsandbox.py +626 -0
- agentinputlab/benchmarks/trec_dl_sample.py +263 -0
- agentinputlab/campaign.py +654 -0
- agentinputlab/campaign_runner.py +953 -0
- agentinputlab/cli/__init__.py +6 -0
- agentinputlab/cli/main.py +933 -0
- agentinputlab/connectors/__init__.py +46 -0
- agentinputlab/connectors/knowledge/__init__.py +4 -0
- agentinputlab/connectors/tools/__init__.py +30 -0
- agentinputlab/connectors/tools/base.py +57 -0
- agentinputlab/connectors/tools/mcp.py +222 -0
- agentinputlab/connectors/tools/memory.py +41 -0
- agentinputlab/connectors/tools/openapi.py +695 -0
- agentinputlab/connectors/tools/schema.py +102 -0
- agentinputlab/contracts/__init__.py +91 -0
- agentinputlab/contracts/availability.py +24 -0
- agentinputlab/contracts/dependency.py +58 -0
- agentinputlab/contracts/enums.py +82 -0
- agentinputlab/contracts/errors.py +63 -0
- agentinputlab/contracts/evidence.py +43 -0
- agentinputlab/contracts/intent.py +110 -0
- agentinputlab/contracts/lifecycle.py +164 -0
- agentinputlab/contracts/patch.py +167 -0
- agentinputlab/contracts/serialization.py +93 -0
- agentinputlab/contracts/state.py +127 -0
- agentinputlab/contracts/trace.py +59 -0
- agentinputlab/contracts/worker.py +42 -0
- agentinputlab/core/__init__.py +7 -0
- agentinputlab/data/__init__.py +32 -0
- agentinputlab/data/checksums.py +9 -0
- agentinputlab/data/downloader.py +235 -0
- agentinputlab/data/http.py +171 -0
- agentinputlab/data/index.py +21 -0
- agentinputlab/data/overlap.py +28 -0
- agentinputlab/data/preprocess.py +32 -0
- agentinputlab/data/storage.py +57 -0
- agentinputlab/data/trec_sample.py +427 -0
- agentinputlab/evaluation/__init__.py +5 -0
- agentinputlab/evaluation/outcomes.py +32 -0
- agentinputlab/experiments/__init__.py +278 -0
- agentinputlab/experiments/analysis.py +981 -0
- agentinputlab/experiments/artifacts.py +687 -0
- agentinputlab/experiments/automatic_measurement.py +678 -0
- agentinputlab/experiments/auxiliary_matrix.py +203 -0
- agentinputlab/experiments/bindings.py +430 -0
- agentinputlab/experiments/blueprints.py +147 -0
- agentinputlab/experiments/builders/__init__.py +45 -0
- agentinputlab/experiments/builders/executable_external_payload.py +423 -0
- agentinputlab/experiments/builders/external_process_arena.py +1018 -0
- agentinputlab/experiments/builders/registry_intervention.py +463 -0
- agentinputlab/experiments/cli.py +158 -0
- agentinputlab/experiments/controls.py +340 -0
- agentinputlab/experiments/driver_guard.py +205 -0
- agentinputlab/experiments/drivers/__init__.py +89 -0
- agentinputlab/experiments/drivers/external_arena.py +2908 -0
- agentinputlab/experiments/drivers/method_process.py +662 -0
- agentinputlab/experiments/drivers/parity_process.py +1484 -0
- agentinputlab/experiments/drivers/static_production.py +1574 -0
- agentinputlab/experiments/executor.py +448 -0
- agentinputlab/experiments/loader.py +605 -0
- agentinputlab/experiments/measurement.py +675 -0
- agentinputlab/experiments/paper_analysis.py +1818 -0
- agentinputlab/experiments/parity_identity.py +605 -0
- agentinputlab/experiments/plan.py +277 -0
- agentinputlab/experiments/production_builders.py +3739 -0
- agentinputlab/experiments/readiness.py +432 -0
- agentinputlab/experiments/schema.py +514 -0
- agentinputlab/experiments/secrets.py +177 -0
- agentinputlab/experiments/stateful_analysis.py +1738 -0
- agentinputlab/experiments/transfer_analysis.py +1316 -0
- agentinputlab/experiments/unseen_analysis.py +1025 -0
- agentinputlab/integrations/__init__.py +1 -0
- agentinputlab/integrations/action_space/__init__.py +1 -0
- agentinputlab/integrations/external_workers/__init__.py +76 -0
- agentinputlab/integrations/external_workers/ace.py +431 -0
- agentinputlab/integrations/external_workers/base.py +181 -0
- agentinputlab/integrations/external_workers/decomp_live.py +249 -0
- agentinputlab/integrations/external_workers/decomp_planner.py +30 -0
- agentinputlab/integrations/external_workers/infocqr_student.py +280 -0
- agentinputlab/integrations/external_workers/ircot.py +576 -0
- agentinputlab/integrations/external_workers/llmcompiler_live.py +360 -0
- agentinputlab/integrations/external_workers/longllmlingua.py +162 -0
- agentinputlab/integrations/external_workers/method.py +314 -0
- agentinputlab/integrations/external_workers/process.py +678 -0
- agentinputlab/integrations/external_workers/supervisor.py +1071 -0
- agentinputlab/integrations/full_architecture/__init__.py +1 -0
- agentinputlab/integrations/full_architecture/hyde.py +212 -0
- agentinputlab/integrations/full_architecture/plan_and_budget.py +188 -0
- agentinputlab/integrations/live_history.py +222 -0
- agentinputlab/integrations/prompt_optimization/__init__.py +1 -0
- agentinputlab/integrations/training/__init__.py +1 -0
- agentinputlab/metrics/__init__.py +50 -0
- agentinputlab/metrics/catalog.py +313 -0
- agentinputlab/metrics/harm.py +95 -0
- agentinputlab/metrics/paired.py +40 -0
- agentinputlab/metrics/qa.py +76 -0
- agentinputlab/metrics/text.py +33 -0
- agentinputlab/metrics/third_party.py +161 -0
- agentinputlab/models/__init__.py +4 -0
- agentinputlab/observability/__init__.py +56 -0
- agentinputlab/observability/budget.py +316 -0
- agentinputlab/observability/instrumentation.py +754 -0
- agentinputlab/observability/models.py +403 -0
- agentinputlab/observability/monitor.py +380 -0
- agentinputlab/optimizers/__init__.py +1 -0
- agentinputlab/optimizers/clarify/__init__.py +12 -0
- agentinputlab/optimizers/context/__init__.py +22 -0
- agentinputlab/optimizers/decompose/__init__.py +64 -0
- agentinputlab/optimizers/evidence/__init__.py +22 -0
- agentinputlab/optimizers/rewrite/__init__.py +30 -0
- agentinputlab/parity/__init__.py +5 -0
- agentinputlab/parity/base.py +206 -0
- agentinputlab/production_campaign.py +763 -0
- agentinputlab/py.typed +1 -0
- agentinputlab/recipes/__init__.py +333 -0
- agentinputlab/recipes/ace.py +418 -0
- agentinputlab/recipes/ace_bfcl.py +273 -0
- agentinputlab/recipes/ace_common_support.py +169 -0
- agentinputlab/recipes/ace_native_plan.py +322 -0
- agentinputlab/recipes/ace_online.py +711 -0
- agentinputlab/recipes/ace_retrieval.py +330 -0
- agentinputlab/recipes/ace_statebench.py +382 -0
- agentinputlab/recipes/ace_tau.py +463 -0
- agentinputlab/recipes/ace_toolsandbox.py +432 -0
- agentinputlab/recipes/acon_guideline.py +22 -0
- agentinputlab/recipes/adaptive_rag.py +822 -0
- agentinputlab/recipes/llmlingua2_inference.py +146 -0
- agentinputlab/recipes/llmlingua2_training.py +796 -0
- agentinputlab/recipes/mce.py +1486 -0
- agentinputlab/recipes/mce_final.py +335 -0
- agentinputlab/recipes/mce_text_fit.py +499 -0
- agentinputlab/recipes/mce_text_only.py +298 -0
- agentinputlab/recipes/qrag.py +729 -0
- agentinputlab/recipes/qrag_inference.py +364 -0
- agentinputlab/recipes/r2ag_features.py +236 -0
- agentinputlab/recipes/r2ag_inference.py +503 -0
- agentinputlab/recipes/r2ag_training.py +948 -0
- agentinputlab/recipes/recomp_abstractive.py +592 -0
- agentinputlab/recipes/recomp_extractive.py +525 -0
- agentinputlab/recipes/reformer.py +17 -0
- agentinputlab/recipes/rqrag.py +1516 -0
- agentinputlab/recipes/rqrag_common_support.py +193 -0
- agentinputlab/recipes/rqrag_completion.py +147 -0
- agentinputlab/recipes/skillopt.py +462 -0
- agentinputlab/recipes/skillopt_common_support.py +154 -0
- agentinputlab/recipes/skillopt_environment.py +165 -0
- agentinputlab/recipes/skillopt_environment_cli.py +5 -0
- agentinputlab/recipes/skillopt_retrieval.py +188 -0
- agentinputlab/recipes/smartsearch.py +897 -0
- agentinputlab/recipes/smartsearch_curriculum.py +283 -0
- agentinputlab/recipes/smartsearch_grpo_contracts.py +58 -0
- agentinputlab/recipes/smartsearch_grpo_data.py +314 -0
- agentinputlab/recipes/smartsearch_grpo_meter.py +324 -0
- agentinputlab/recipes/smartsearch_grpo_prefix.py +144 -0
- agentinputlab/recipes/smartsearch_grpo_rollout.py +119 -0
- agentinputlab/recipes/smartsearch_grpo_tools.py +364 -0
- agentinputlab/recipes/smartsearch_grpo_training.py +305 -0
- agentinputlab/recipes/smartsearch_grpo_transport.py +248 -0
- agentinputlab/recipes/smartsearch_grpo_worker.py +364 -0
- agentinputlab/recipes/smartsearch_inference.py +524 -0
- agentinputlab/recipes/smartsearch_native_data.py +172 -0
- agentinputlab/recipes/smartsearch_policy.py +616 -0
- agentinputlab/recipes/smartsearch_policy_worker.py +9 -0
- agentinputlab/recipes/smartsearch_sampling.py +232 -0
- agentinputlab/recipes/smartsearch_teacher.py +203 -0
- agentinputlab/recipes/smartsearch_training.py +265 -0
- agentinputlab/registry/__init__.py +46 -0
- agentinputlab/registry/beta.py +727 -0
- agentinputlab/registry/data/algorithms.json +1959 -0
- agentinputlab/registry/data/auxiliary_method_profiles.json +34 -0
- agentinputlab/registry/loader.py +100 -0
- agentinputlab/registry/result_manifest.py +184 -0
- agentinputlab/registry/runtime.py +195 -0
- agentinputlab/registry/schema.py +449 -0
- agentinputlab/runner/__init__.py +30 -0
- agentinputlab/runner/balance.py +248 -0
- agentinputlab/runner/cache.py +135 -0
- agentinputlab/runner/composite.py +130 -0
- agentinputlab/runner/engine.py +285 -0
- agentinputlab/runner/runtime_guard.py +268 -0
- agentinputlab/runner/worker.py +80 -0
- agentinputlab/runtime_bootstrap.py +288 -0
- agentinputlab/selectors/__init__.py +196 -0
- agentinputlab/selectors/advanced.py +1514 -0
- agentinputlab/selectors/artifacts.py +1087 -0
- agentinputlab/selectors/base.py +112 -0
- agentinputlab/selectors/campaigns.py +1893 -0
- agentinputlab/selectors/catalog.py +86 -0
- agentinputlab/selectors/discovery.py +1860 -0
- agentinputlab/selectors/evaluation.py +1348 -0
- agentinputlab/selectors/features.py +383 -0
- agentinputlab/selectors/learned.py +502 -0
- agentinputlab/selectors/no_op.py +28 -0
- agentinputlab/selectors/outcome_table.py +475 -0
- agentinputlab/selectors/producer.py +853 -0
- agentinputlab/selectors/rules.py +109 -0
- agentinputlab/selectors/runtime.py +420 -0
- agentinputlab/studies/__init__.py +16 -0
- agentinputlab/studies/design.py +165 -0
- agentinputlab/studies/paired.py +322 -0
- agentinputlab/studio/__init__.py +6 -0
- agentinputlab/studio/campaigns.py +819 -0
- agentinputlab/studio/engine.py +1408 -0
- agentinputlab/studio/model_backends.py +425 -0
- agentinputlab/studio/prepared.py +410 -0
- agentinputlab/studio/server.py +613 -0
- agentinputlab/studio/static/app.js +397 -0
- agentinputlab/studio/static/campaigns.html +175 -0
- agentinputlab/studio/static/campaigns.js +426 -0
- agentinputlab/studio/static/index.html +192 -0
- agentinputlab/studio/static/result.html +103 -0
- agentinputlab/studio/static/result.js +273 -0
- agentinputlab/studio/static/styles.css +467 -0
- agentinputlab-0.1.0a1.data/data/share/agentinputlab/CITATION.cff +22 -0
- agentinputlab-0.1.0a1.dist-info/METADATA +392 -0
- agentinputlab-0.1.0a1.dist-info/RECORD +469 -0
- agentinputlab-0.1.0a1.dist-info/WHEEL +5 -0
- agentinputlab-0.1.0a1.dist-info/entry_points.txt +6 -0
- agentinputlab-0.1.0a1.dist-info/licenses/LICENSE +21 -0
- agentinputlab-0.1.0a1.dist-info/licenses/NOTICE +31 -0
- agentinputlab-0.1.0a1.dist-info/licenses/THIRD_PARTY_NOTICES.md +94 -0
- agentinputlab-0.1.0a1.dist-info/top_level.txt +2 -0
- workers/README.md +94 -0
- workers/ace/README.md +261 -0
- workers/ace/__init__.py +1 -0
- workers/ace/compatibility_manifest.json +39 -0
- workers/ace/driver.py +1682 -0
- workers/acon/README.md +215 -0
- workers/acon/__init__.py +5 -0
- workers/acon/compatibility_manifest.json +93 -0
- workers/acon/driver.py +1859 -0
- workers/acon/fitted.py +395 -0
- workers/acon/guideline.py +470 -0
- workers/acon/guideline_source_manifest.json +20 -0
- workers/adaptive_rag/README.md +96 -0
- workers/adaptive_rag/__init__.py +15 -0
- workers/adaptive_rag/compatibility_manifest.json +61 -0
- workers/adaptive_rag/driver.py +1470 -0
- workers/adaptive_rag/fixture_runtime.py +163 -0
- workers/adaptive_rag/runtime.py +252 -0
- workers/ask_or_assume/README.md +124 -0
- workers/ask_or_assume/__init__.py +1 -0
- workers/ask_or_assume/compatibility_manifest.json +36 -0
- workers/ask_or_assume/driver.py +1227 -0
- workers/decomp/README.md +133 -0
- workers/decomp/UPSTREAM_LICENSE.Apache-2.0.txt +201 -0
- workers/decomp/__init__.py +5 -0
- workers/decomp/compatibility_manifest.json +205 -0
- workers/decomp/driver.py +1015 -0
- workers/decomp/live.py +137 -0
- workers/decomp/runtime.py +231 -0
- workers/infocqr/README.md +106 -0
- workers/infocqr/__init__.py +1 -0
- workers/infocqr/compatibility_manifest.json +84 -0
- workers/infocqr/driver.py +1766 -0
- workers/ircot/README.md +167 -0
- workers/ircot/__init__.py +7 -0
- workers/ircot/compatibility_manifest.json +121 -0
- workers/ircot/controlled.py +457 -0
- workers/ircot/driver.py +1373 -0
- workers/ircot/fixture_bindings.json +39 -0
- workers/ircot/fixture_config.json +48 -0
- workers/ircot/fixture_prompt.txt +2 -0
- workers/ircot/native_qa.py +398 -0
- workers/ircot/qa_compatibility_manifest.json +57 -0
- workers/ircot/runtime.py +97 -0
- workers/llmcompiler/README.md +160 -0
- workers/llmcompiler/__init__.py +1 -0
- workers/llmcompiler/compatibility_manifest.json +45 -0
- workers/llmcompiler/driver.py +1514 -0
- workers/llmcompiler/export_prompts.py +74 -0
- workers/llmcompiler/live.py +174 -0
- workers/llmcompiler/patch_manifest.json +147 -0
- workers/mce/README.md +198 -0
- workers/mce/__init__.py +29 -0
- workers/mce/compatibility_manifest.json +115 -0
- workers/mce/driver.py +2585 -0
- workers/mce/final_eval.py +173 -0
- workers/official_arenas/README.md +25 -0
- workers/official_arenas/__init__.py +1 -0
- workers/official_arenas/capture_records.py +110 -0
- workers/official_arenas/driver.py +619 -0
- workers/official_arenas/protocol.py +334 -0
- workers/official_arenas/providers/__init__.py +1 -0
- workers/official_arenas/providers/_live_history.py +228 -0
- workers/official_arenas/providers/_message_compat.py +77 -0
- workers/official_arenas/providers/appworld_ul.py +196 -0
- workers/official_arenas/providers/fixture.py +112 -0
- workers/official_arenas/providers/statebench.py +960 -0
- workers/official_arenas/providers/tau3.py +1672 -0
- workers/official_arenas/providers/tau_retail.py +586 -0
- workers/official_arenas/providers/toolsandbox.py +1321 -0
- workers/official_arenas/providers/tooltalk.py +699 -0
- workers/official_arenas/retail_capture.py +188 -0
- workers/official_arenas/retail_driver.py +275 -0
- workers/qrag/README.md +172 -0
- workers/qrag/__init__.py +1 -0
- workers/qrag/compatibility_manifest.json +64 -0
- workers/qrag/driver.py +673 -0
- workers/r2ag/README.md +154 -0
- workers/r2ag/__init__.py +5 -0
- workers/r2ag/compatibility_manifest.json +55 -0
- workers/r2ag/driver.py +1287 -0
- workers/r2ag/feature_schema.json +8 -0
- workers/r2ag/runtime.py +589 -0
- workers/recomp/README.md +120 -0
- workers/recomp/__init__.py +1 -0
- workers/recomp/compatibility_manifest.json +43 -0
- workers/recomp/driver.py +1653 -0
- workers/reformer/README.md +125 -0
- workers/reformer/__init__.py +1 -0
- workers/reformer/compatibility_manifest.json +68 -0
- workers/reformer/driver.py +1459 -0
- workers/reformer/induction.py +457 -0
- workers/rrr/README.md +126 -0
- workers/rrr/__init__.py +1 -0
- workers/rrr/compatibility_manifest.json +57 -0
- workers/rrr/driver.py +1133 -0
- workers/smartsearch/README.md +331 -0
- workers/smartsearch/__init__.py +1 -0
- workers/smartsearch/compatibility_manifest.json +41 -0
- workers/smartsearch/driver.py +1643 -0
|
@@ -0,0 +1,1071 @@
|
|
|
1
|
+
"""Auditable local and container supervisors for external workers.
|
|
2
|
+
|
|
3
|
+
The module has no Docker or accelerator SDK dependency. Docker is invoked as
|
|
4
|
+
an argv-only client through an injectable bounded runner, and accelerator
|
|
5
|
+
accounting is an optional protocol implemented by the experiment environment.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import math
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
import time
|
|
15
|
+
import uuid
|
|
16
|
+
from collections.abc import Mapping, Sequence
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from pathlib import Path, PurePosixPath
|
|
19
|
+
from typing import Any, Protocol, runtime_checkable
|
|
20
|
+
|
|
21
|
+
from agentinputlab.contracts.errors import ExternalDependencyError, ValidationError
|
|
22
|
+
from agentinputlab.contracts.serialization import content_hash, freeze_mapping
|
|
23
|
+
from agentinputlab.observability.models import (
|
|
24
|
+
AcceleratorSample,
|
|
25
|
+
ResourceExitReason,
|
|
26
|
+
ResourceLimits,
|
|
27
|
+
ResourceReceipt,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
from .process import (
|
|
31
|
+
BoundedProcessResult,
|
|
32
|
+
BoundedProcessStartupError,
|
|
33
|
+
BoundedProcessTimeoutError,
|
|
34
|
+
ProcessResourceObservation,
|
|
35
|
+
run_bounded_process,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
_DIGEST_PINNED_IMAGE = re.compile(r"^.+@sha256:([0-9a-f]{64})$")
|
|
39
|
+
_SAFE_CONTAINER_PREFIX = re.compile(r"[^a-z0-9_.-]+")
|
|
40
|
+
WORKER_SUPERVISOR_KIND_ENV = "AILAB_WORKER_SUPERVISOR"
|
|
41
|
+
WORKER_SUPERVISOR_IMAGE_ENV = "AILAB_WORKER_IMAGE"
|
|
42
|
+
WORKER_SUPERVISOR_ASSET_ROOT_ENV = "AILAB_WORKER_ASSET_ROOT"
|
|
43
|
+
_LOCAL_LOCKED_LAUNCHER_PROTOCOL = "agentinputlab.local-locked-python-launcher.v1"
|
|
44
|
+
_DOCKER_LOCKED_LAUNCHER_PROTOCOL = "agentinputlab.docker-locked-python-launcher.v1"
|
|
45
|
+
_LOCKED_PYTHON_LAUNCHER = """\
|
|
46
|
+
import runpy
|
|
47
|
+
import sys
|
|
48
|
+
|
|
49
|
+
upstream, upstream_src, adapter, entrypoint, *arguments = sys.argv[1:]
|
|
50
|
+
trusted_paths = [upstream]
|
|
51
|
+
if upstream_src:
|
|
52
|
+
trusted_paths.append(upstream_src)
|
|
53
|
+
if adapter != upstream:
|
|
54
|
+
trusted_paths.append(adapter)
|
|
55
|
+
sys.path[:0] = trusted_paths
|
|
56
|
+
sys.argv = [entrypoint, *arguments]
|
|
57
|
+
runpy.run_path(entrypoint, run_name="__main__")
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@runtime_checkable
|
|
62
|
+
class AcceleratorSampler(Protocol):
|
|
63
|
+
"""Optional accelerator evidence for exactly one supervised execution.
|
|
64
|
+
|
|
65
|
+
:meth:`stop` may return device-seconds and sampled peak device memory.
|
|
66
|
+
Implementations must state in ``AcceleratorSample.sampler_identity``
|
|
67
|
+
whether seconds are utilization-integrated or allocation-time accounting.
|
|
68
|
+
Unavailable measurements are ``None``, never a fabricated zero.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def start(self) -> object:
|
|
72
|
+
"""Start a sample window and return an opaque, per-call token."""
|
|
73
|
+
|
|
74
|
+
def stop(self, token: object) -> AcceleratorSample:
|
|
75
|
+
"""Stop the matching window and return immutable accounting."""
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@runtime_checkable
|
|
79
|
+
class ProcessRunner(Protocol):
|
|
80
|
+
"""Injectable argv-only process runner used by both supervisors."""
|
|
81
|
+
|
|
82
|
+
def run(
|
|
83
|
+
self,
|
|
84
|
+
argv: Sequence[str],
|
|
85
|
+
*,
|
|
86
|
+
input_text: str,
|
|
87
|
+
timeout_seconds: float,
|
|
88
|
+
max_input_bytes: int,
|
|
89
|
+
max_stdout_bytes: int,
|
|
90
|
+
max_stderr_bytes: int,
|
|
91
|
+
cwd: str | None,
|
|
92
|
+
environment: Mapping[str, str] | None,
|
|
93
|
+
process_name: str,
|
|
94
|
+
) -> BoundedProcessResult: ...
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass(frozen=True, slots=True)
|
|
98
|
+
class DefaultProcessRunner:
|
|
99
|
+
"""Adapter around the library's hard wall-time/pipe-bounded transport."""
|
|
100
|
+
|
|
101
|
+
def run(
|
|
102
|
+
self,
|
|
103
|
+
argv: Sequence[str],
|
|
104
|
+
*,
|
|
105
|
+
input_text: str,
|
|
106
|
+
timeout_seconds: float,
|
|
107
|
+
max_input_bytes: int,
|
|
108
|
+
max_stdout_bytes: int,
|
|
109
|
+
max_stderr_bytes: int,
|
|
110
|
+
cwd: str | None,
|
|
111
|
+
environment: Mapping[str, str] | None,
|
|
112
|
+
process_name: str,
|
|
113
|
+
) -> BoundedProcessResult:
|
|
114
|
+
return run_bounded_process(
|
|
115
|
+
argv,
|
|
116
|
+
input_text=input_text,
|
|
117
|
+
timeout_seconds=timeout_seconds,
|
|
118
|
+
max_input_bytes=max_input_bytes,
|
|
119
|
+
max_stdout_bytes=max_stdout_bytes,
|
|
120
|
+
max_stderr_bytes=max_stderr_bytes,
|
|
121
|
+
cwd=cwd,
|
|
122
|
+
environment=environment,
|
|
123
|
+
process_name=process_name,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@dataclass(frozen=True, slots=True)
|
|
128
|
+
class SupervisedProcessResult:
|
|
129
|
+
returncode: int
|
|
130
|
+
stdout: str
|
|
131
|
+
stderr: str
|
|
132
|
+
resource_receipt: ResourceReceipt
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class SupervisedWorkerError(ExternalDependencyError):
|
|
136
|
+
"""A supervisor failure retaining the audit receipt produced before exit."""
|
|
137
|
+
|
|
138
|
+
def __init__(self, message: str, *, resource_receipt: ResourceReceipt) -> None:
|
|
139
|
+
super().__init__(message)
|
|
140
|
+
self.resource_receipt = resource_receipt
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@runtime_checkable
|
|
144
|
+
class WorkerSupervisor(Protocol):
|
|
145
|
+
"""Execute one bounded worker request and return non-additive evidence."""
|
|
146
|
+
|
|
147
|
+
def fingerprint(self) -> Mapping[str, Any]:
|
|
148
|
+
"""Return stable supervisor identity, excluding credentials."""
|
|
149
|
+
|
|
150
|
+
def execute(
|
|
151
|
+
self,
|
|
152
|
+
argv: Sequence[str],
|
|
153
|
+
*,
|
|
154
|
+
input_text: str,
|
|
155
|
+
limits: ResourceLimits,
|
|
156
|
+
max_input_bytes: int,
|
|
157
|
+
max_stdout_bytes: int,
|
|
158
|
+
max_stderr_bytes: int,
|
|
159
|
+
cwd: str | None,
|
|
160
|
+
environment: Mapping[str, str] | None,
|
|
161
|
+
process_name: str,
|
|
162
|
+
image_identity: Mapping[str, Any] | None = None,
|
|
163
|
+
accelerator_sampler: AcceleratorSampler | None = None,
|
|
164
|
+
) -> SupervisedProcessResult: ...
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@dataclass(frozen=True, slots=True)
|
|
168
|
+
class LocalSubprocessSupervisor:
|
|
169
|
+
"""Host-process supervisor with a real hard wall deadline.
|
|
170
|
+
|
|
171
|
+
The portable transport cannot guarantee a process-tree CPU quota, memory
|
|
172
|
+
cgroup, or PID controller. Requests for any of those limits are rejected
|
|
173
|
+
before process creation rather than being mislabeled as enforced.
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
runner: ProcessRunner = field(default_factory=DefaultProcessRunner, repr=False)
|
|
177
|
+
host_identity: Mapping[str, Any] = field(
|
|
178
|
+
default_factory=lambda: {"kind": "host_process", "pinned": False}
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
def __post_init__(self) -> None:
|
|
182
|
+
object.__setattr__(self, "host_identity", freeze_mapping(self.host_identity))
|
|
183
|
+
|
|
184
|
+
def fingerprint(self) -> Mapping[str, Any]:
|
|
185
|
+
return {
|
|
186
|
+
"kind": "local_subprocess",
|
|
187
|
+
"protocol": "agentinputlab.worker-supervisor.v1",
|
|
188
|
+
"hard_limits": ("wall_timeout_seconds", "pipe_bytes"),
|
|
189
|
+
"optional_measurements": (
|
|
190
|
+
"psutil_process_tree_cpu_rss",
|
|
191
|
+
"pynvml_process_tree_allocation",
|
|
192
|
+
),
|
|
193
|
+
"locked_python_launcher": {
|
|
194
|
+
"protocol": _LOCAL_LOCKED_LAUNCHER_PROTOCOL,
|
|
195
|
+
"source_sha256": content_hash(_LOCKED_PYTHON_LAUNCHER),
|
|
196
|
+
"isolated_mode": True,
|
|
197
|
+
"bytecode_writes": False,
|
|
198
|
+
"import_order": "upstream_then_upstream_src_then_adapter",
|
|
199
|
+
},
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
def execute_locked_worker(
|
|
203
|
+
self,
|
|
204
|
+
argv: Sequence[str],
|
|
205
|
+
*,
|
|
206
|
+
checkout: str | Path,
|
|
207
|
+
adapter_checkout: str | Path,
|
|
208
|
+
entrypoint_relative_path: str,
|
|
209
|
+
input_text: str,
|
|
210
|
+
limits: ResourceLimits,
|
|
211
|
+
max_input_bytes: int,
|
|
212
|
+
max_stdout_bytes: int,
|
|
213
|
+
max_stderr_bytes: int,
|
|
214
|
+
environment: Mapping[str, str] | None,
|
|
215
|
+
process_name: str,
|
|
216
|
+
image_identity: Mapping[str, Any] | None = None,
|
|
217
|
+
accelerator_sampler: AcceleratorSampler | None = None,
|
|
218
|
+
) -> SupervisedProcessResult:
|
|
219
|
+
"""Execute an attested Python entrypoint without implicit script paths."""
|
|
220
|
+
|
|
221
|
+
checkout_source = _host_bind_directory(checkout, label="worker checkout")
|
|
222
|
+
adapter_source = _host_bind_directory(
|
|
223
|
+
adapter_checkout,
|
|
224
|
+
label="worker adapter checkout",
|
|
225
|
+
)
|
|
226
|
+
if checkout_source != adapter_source and (
|
|
227
|
+
checkout_source.is_relative_to(adapter_source)
|
|
228
|
+
or adapter_source.is_relative_to(checkout_source)
|
|
229
|
+
):
|
|
230
|
+
raise ValidationError("worker checkout and adapter checkout must not partially overlap")
|
|
231
|
+
relative = PurePosixPath(entrypoint_relative_path)
|
|
232
|
+
if (
|
|
233
|
+
relative.is_absolute()
|
|
234
|
+
or not relative.parts
|
|
235
|
+
or any(part in {"", ".", ".."} for part in relative.parts)
|
|
236
|
+
):
|
|
237
|
+
raise ValidationError("worker entrypoint_relative_path must stay inside the adapter")
|
|
238
|
+
expected_entrypoint = adapter_source.joinpath(*relative.parts)
|
|
239
|
+
if len(argv) < 2 or Path(argv[1]).resolve() != expected_entrypoint:
|
|
240
|
+
raise ValidationError("worker argv does not match the attested adapter entrypoint")
|
|
241
|
+
upstream_src = checkout_source / "src"
|
|
242
|
+
runtime_argv = (
|
|
243
|
+
argv[0],
|
|
244
|
+
"-I",
|
|
245
|
+
"-B",
|
|
246
|
+
"-c",
|
|
247
|
+
_LOCKED_PYTHON_LAUNCHER,
|
|
248
|
+
str(checkout_source),
|
|
249
|
+
str(upstream_src) if upstream_src.is_dir() else "",
|
|
250
|
+
str(adapter_source),
|
|
251
|
+
str(expected_entrypoint),
|
|
252
|
+
*tuple(argv[2:]),
|
|
253
|
+
)
|
|
254
|
+
return self.execute(
|
|
255
|
+
runtime_argv,
|
|
256
|
+
input_text=input_text,
|
|
257
|
+
limits=limits,
|
|
258
|
+
max_input_bytes=max_input_bytes,
|
|
259
|
+
max_stdout_bytes=max_stdout_bytes,
|
|
260
|
+
max_stderr_bytes=max_stderr_bytes,
|
|
261
|
+
cwd=str(checkout_source),
|
|
262
|
+
environment=environment,
|
|
263
|
+
process_name=process_name,
|
|
264
|
+
image_identity=image_identity,
|
|
265
|
+
accelerator_sampler=accelerator_sampler,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
def execute(
|
|
269
|
+
self,
|
|
270
|
+
argv: Sequence[str],
|
|
271
|
+
*,
|
|
272
|
+
input_text: str,
|
|
273
|
+
limits: ResourceLimits,
|
|
274
|
+
max_input_bytes: int,
|
|
275
|
+
max_stdout_bytes: int,
|
|
276
|
+
max_stderr_bytes: int,
|
|
277
|
+
cwd: str | None,
|
|
278
|
+
environment: Mapping[str, str] | None,
|
|
279
|
+
process_name: str,
|
|
280
|
+
image_identity: Mapping[str, Any] | None = None,
|
|
281
|
+
accelerator_sampler: AcceleratorSampler | None = None,
|
|
282
|
+
) -> SupervisedProcessResult:
|
|
283
|
+
if not isinstance(limits, ResourceLimits):
|
|
284
|
+
raise TypeError("limits must be ResourceLimits")
|
|
285
|
+
unsupported = [
|
|
286
|
+
name
|
|
287
|
+
for name, value in (
|
|
288
|
+
("memory_bytes", limits.memory_bytes),
|
|
289
|
+
("cpus", limits.cpus),
|
|
290
|
+
("pids_limit", limits.pids_limit),
|
|
291
|
+
)
|
|
292
|
+
if value is not None
|
|
293
|
+
]
|
|
294
|
+
if unsupported:
|
|
295
|
+
raise ValidationError(
|
|
296
|
+
"local subprocess supervisor cannot guarantee hard "
|
|
297
|
+
+ ", ".join(unsupported)
|
|
298
|
+
+ " limits; use a container supervisor"
|
|
299
|
+
)
|
|
300
|
+
started = time.perf_counter()
|
|
301
|
+
sample_token = _start_accelerator_sample(accelerator_sampler)
|
|
302
|
+
try:
|
|
303
|
+
result = self.runner.run(
|
|
304
|
+
argv,
|
|
305
|
+
input_text=input_text,
|
|
306
|
+
timeout_seconds=limits.wall_timeout_seconds,
|
|
307
|
+
max_input_bytes=max_input_bytes,
|
|
308
|
+
max_stdout_bytes=max_stdout_bytes,
|
|
309
|
+
max_stderr_bytes=max_stderr_bytes,
|
|
310
|
+
cwd=cwd,
|
|
311
|
+
environment=environment,
|
|
312
|
+
process_name=process_name,
|
|
313
|
+
)
|
|
314
|
+
except BoundedProcessTimeoutError as exc:
|
|
315
|
+
observation = exc.resource_observation
|
|
316
|
+
sample = _stop_accelerator_after_process_error(
|
|
317
|
+
accelerator_sampler,
|
|
318
|
+
sample_token,
|
|
319
|
+
observation=observation,
|
|
320
|
+
)
|
|
321
|
+
receipt = self._receipt(
|
|
322
|
+
limits=limits,
|
|
323
|
+
wall_seconds=time.perf_counter() - started,
|
|
324
|
+
sample=sample,
|
|
325
|
+
observation=observation,
|
|
326
|
+
image_identity=image_identity,
|
|
327
|
+
timed_out=True,
|
|
328
|
+
exit_reason=ResourceExitReason.TIMED_OUT,
|
|
329
|
+
)
|
|
330
|
+
raise SupervisedWorkerError(
|
|
331
|
+
str(exc),
|
|
332
|
+
resource_receipt=receipt,
|
|
333
|
+
) from exc
|
|
334
|
+
except BoundedProcessStartupError as exc:
|
|
335
|
+
observation = exc.resource_observation
|
|
336
|
+
sample = _stop_accelerator_after_process_error(
|
|
337
|
+
accelerator_sampler,
|
|
338
|
+
sample_token,
|
|
339
|
+
observation=observation,
|
|
340
|
+
)
|
|
341
|
+
receipt = self._receipt(
|
|
342
|
+
limits=limits,
|
|
343
|
+
wall_seconds=time.perf_counter() - started,
|
|
344
|
+
sample=sample,
|
|
345
|
+
observation=observation,
|
|
346
|
+
image_identity=image_identity,
|
|
347
|
+
exit_reason=ResourceExitReason.STARTUP_FAILED,
|
|
348
|
+
)
|
|
349
|
+
raise SupervisedWorkerError(
|
|
350
|
+
str(exc),
|
|
351
|
+
resource_receipt=receipt,
|
|
352
|
+
) from exc
|
|
353
|
+
except ExternalDependencyError as exc:
|
|
354
|
+
observation = _error_observation(exc)
|
|
355
|
+
sample = _stop_accelerator_after_process_error(
|
|
356
|
+
accelerator_sampler,
|
|
357
|
+
sample_token,
|
|
358
|
+
observation=observation,
|
|
359
|
+
)
|
|
360
|
+
receipt = self._receipt(
|
|
361
|
+
limits=limits,
|
|
362
|
+
wall_seconds=time.perf_counter() - started,
|
|
363
|
+
sample=sample,
|
|
364
|
+
observation=observation,
|
|
365
|
+
image_identity=image_identity,
|
|
366
|
+
exit_reason=ResourceExitReason.SUPERVISOR_ERROR,
|
|
367
|
+
)
|
|
368
|
+
raise SupervisedWorkerError(
|
|
369
|
+
str(exc),
|
|
370
|
+
resource_receipt=receipt,
|
|
371
|
+
) from exc
|
|
372
|
+
try:
|
|
373
|
+
sample = _stop_accelerator_sample(
|
|
374
|
+
accelerator_sampler,
|
|
375
|
+
sample_token,
|
|
376
|
+
observation=result.resource_observation,
|
|
377
|
+
)
|
|
378
|
+
except Exception as exc:
|
|
379
|
+
receipt = self._receipt(
|
|
380
|
+
limits=limits,
|
|
381
|
+
wall_seconds=time.perf_counter() - started,
|
|
382
|
+
sample=AcceleratorSample(),
|
|
383
|
+
observation=result.resource_observation,
|
|
384
|
+
image_identity=image_identity,
|
|
385
|
+
exit_code=result.returncode,
|
|
386
|
+
exit_reason=ResourceExitReason.SUPERVISOR_ERROR,
|
|
387
|
+
)
|
|
388
|
+
raise SupervisedWorkerError(
|
|
389
|
+
"accelerator sampler could not finalize",
|
|
390
|
+
resource_receipt=receipt,
|
|
391
|
+
) from exc
|
|
392
|
+
reason = (
|
|
393
|
+
ResourceExitReason.COMPLETED
|
|
394
|
+
if result.returncode == 0
|
|
395
|
+
else ResourceExitReason.NONZERO_EXIT
|
|
396
|
+
)
|
|
397
|
+
receipt = self._receipt(
|
|
398
|
+
limits=limits,
|
|
399
|
+
wall_seconds=time.perf_counter() - started,
|
|
400
|
+
sample=sample,
|
|
401
|
+
observation=result.resource_observation,
|
|
402
|
+
image_identity=image_identity,
|
|
403
|
+
exit_code=result.returncode,
|
|
404
|
+
exit_reason=reason,
|
|
405
|
+
)
|
|
406
|
+
return SupervisedProcessResult(
|
|
407
|
+
returncode=result.returncode,
|
|
408
|
+
stdout=result.stdout,
|
|
409
|
+
stderr=result.stderr,
|
|
410
|
+
resource_receipt=receipt,
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
def _receipt(
|
|
414
|
+
self,
|
|
415
|
+
*,
|
|
416
|
+
limits: ResourceLimits,
|
|
417
|
+
wall_seconds: float,
|
|
418
|
+
sample: AcceleratorSample,
|
|
419
|
+
observation: ProcessResourceObservation,
|
|
420
|
+
image_identity: Mapping[str, Any] | None,
|
|
421
|
+
exit_reason: ResourceExitReason,
|
|
422
|
+
exit_code: int | None = None,
|
|
423
|
+
timed_out: bool = False,
|
|
424
|
+
) -> ResourceReceipt:
|
|
425
|
+
return ResourceReceipt(
|
|
426
|
+
limit_hash=limits.limit_hash,
|
|
427
|
+
wall_seconds=wall_seconds,
|
|
428
|
+
cpu_seconds=observation.cpu_seconds,
|
|
429
|
+
peak_rss_bytes=observation.peak_rss_bytes,
|
|
430
|
+
accelerator_seconds=sample.accelerator_seconds,
|
|
431
|
+
accelerator_peak_memory_bytes=(sample.accelerator_peak_memory_bytes),
|
|
432
|
+
timed_out=timed_out,
|
|
433
|
+
exit_reason=exit_reason,
|
|
434
|
+
exit_code=exit_code,
|
|
435
|
+
supervisor_identity={
|
|
436
|
+
**dict(self.fingerprint()),
|
|
437
|
+
"accelerator_sampler": dict(sample.sampler_identity),
|
|
438
|
+
"measurement": dict(observation.measurement_identity),
|
|
439
|
+
},
|
|
440
|
+
image_identity=dict(image_identity or self.host_identity),
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
@dataclass(frozen=True, slots=True)
|
|
445
|
+
class DockerWorkerSupervisor:
|
|
446
|
+
"""Digest-pinned, network-isolated Docker worker supervisor."""
|
|
447
|
+
|
|
448
|
+
image: str
|
|
449
|
+
runner: ProcessRunner = field(default_factory=DefaultProcessRunner, repr=False)
|
|
450
|
+
docker_executable: str = "docker"
|
|
451
|
+
name_prefix: str = "agentinputlab-worker"
|
|
452
|
+
management_timeout_seconds: float = 15.0
|
|
453
|
+
asset_root: str | None = None
|
|
454
|
+
container_python: str = "/usr/local/bin/python"
|
|
455
|
+
container_user: str = "10001:10001"
|
|
456
|
+
tmpfs_size_bytes: int = 512 * 1024 * 1024
|
|
457
|
+
|
|
458
|
+
def __post_init__(self) -> None:
|
|
459
|
+
if not isinstance(self.image, str) or not _DIGEST_PINNED_IMAGE.fullmatch(self.image):
|
|
460
|
+
raise ValidationError("Docker worker image must be pinned as name@sha256:<64 hex>")
|
|
461
|
+
if not isinstance(self.docker_executable, str) or not self.docker_executable:
|
|
462
|
+
raise ValidationError("docker_executable must be a non-empty string")
|
|
463
|
+
if (
|
|
464
|
+
not isinstance(self.container_python, str)
|
|
465
|
+
or not self.container_python.startswith("/")
|
|
466
|
+
or any(character in self.container_python for character in ("\x00", "\n", "\r"))
|
|
467
|
+
):
|
|
468
|
+
raise ValidationError("container_python must be an absolute argument-safe path")
|
|
469
|
+
if self.container_user != "10001:10001":
|
|
470
|
+
raise ValidationError("Docker worker container_user must be 10001:10001")
|
|
471
|
+
if (
|
|
472
|
+
isinstance(self.tmpfs_size_bytes, bool)
|
|
473
|
+
or not isinstance(self.tmpfs_size_bytes, int)
|
|
474
|
+
or self.tmpfs_size_bytes < 1024 * 1024
|
|
475
|
+
):
|
|
476
|
+
raise ValidationError("Docker worker tmpfs_size_bytes must be at least 1 MiB")
|
|
477
|
+
if self.asset_root is not None:
|
|
478
|
+
asset_root = _host_bind_directory(self.asset_root, label="Docker worker asset_root")
|
|
479
|
+
object.__setattr__(self, "asset_root", str(asset_root))
|
|
480
|
+
if (
|
|
481
|
+
isinstance(self.management_timeout_seconds, bool)
|
|
482
|
+
or not isinstance(self.management_timeout_seconds, (int, float))
|
|
483
|
+
or not math.isfinite(self.management_timeout_seconds)
|
|
484
|
+
or self.management_timeout_seconds <= 0
|
|
485
|
+
):
|
|
486
|
+
raise ValidationError("management_timeout_seconds must be finite and positive")
|
|
487
|
+
prefix = _SAFE_CONTAINER_PREFIX.sub("-", self.name_prefix.casefold()).strip("-.")
|
|
488
|
+
if not prefix:
|
|
489
|
+
raise ValidationError("name_prefix must contain a Docker-safe character")
|
|
490
|
+
object.__setattr__(self, "name_prefix", prefix[:40])
|
|
491
|
+
|
|
492
|
+
def fingerprint(self) -> Mapping[str, Any]:
|
|
493
|
+
digest = self.image.rsplit("@sha256:", 1)[1]
|
|
494
|
+
return {
|
|
495
|
+
"kind": "docker",
|
|
496
|
+
"protocol": "agentinputlab.worker-supervisor.v1",
|
|
497
|
+
"docker_executable": self.docker_executable,
|
|
498
|
+
"image_digest": f"sha256:{digest}",
|
|
499
|
+
"network": "none",
|
|
500
|
+
"read_only": True,
|
|
501
|
+
"cap_drop": "ALL",
|
|
502
|
+
"no_new_privileges": True,
|
|
503
|
+
"container_python": self.container_python,
|
|
504
|
+
"container_user": self.container_user,
|
|
505
|
+
"checkout_target": "/opt/upstream",
|
|
506
|
+
"adapter_checkout_target": "/opt/candidate",
|
|
507
|
+
"asset_target": "/opt/assets" if self.asset_root is not None else None,
|
|
508
|
+
"tmpfs_size_bytes": self.tmpfs_size_bytes,
|
|
509
|
+
"container_measurements": "runner_declared_supervised_workload_only",
|
|
510
|
+
"locked_python_launcher": {
|
|
511
|
+
"protocol": _DOCKER_LOCKED_LAUNCHER_PROTOCOL,
|
|
512
|
+
"source_sha256": content_hash(_LOCKED_PYTHON_LAUNCHER),
|
|
513
|
+
"isolated_mode": True,
|
|
514
|
+
"bytecode_writes": False,
|
|
515
|
+
"import_order": "upstream_then_upstream_src_then_adapter",
|
|
516
|
+
},
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
def execute_locked_worker(
|
|
520
|
+
self,
|
|
521
|
+
argv: Sequence[str],
|
|
522
|
+
*,
|
|
523
|
+
checkout: str | Path,
|
|
524
|
+
adapter_checkout: str | Path,
|
|
525
|
+
entrypoint_relative_path: str,
|
|
526
|
+
input_text: str,
|
|
527
|
+
limits: ResourceLimits,
|
|
528
|
+
max_input_bytes: int,
|
|
529
|
+
max_stdout_bytes: int,
|
|
530
|
+
max_stderr_bytes: int,
|
|
531
|
+
environment: Mapping[str, str] | None,
|
|
532
|
+
process_name: str,
|
|
533
|
+
image_identity: Mapping[str, Any] | None = None,
|
|
534
|
+
accelerator_sampler: AcceleratorSampler | None = None,
|
|
535
|
+
) -> SupervisedProcessResult:
|
|
536
|
+
"""Run one host-attested worker through fixed container paths.
|
|
537
|
+
|
|
538
|
+
Host executable/checkouts remain part of the caller's attestation. The
|
|
539
|
+
image digest binds the container Python runtime; only the exact
|
|
540
|
+
entrypoint-relative path and configured worker arguments cross the
|
|
541
|
+
boundary.
|
|
542
|
+
"""
|
|
543
|
+
|
|
544
|
+
checkout_source = _host_bind_directory(checkout, label="worker checkout")
|
|
545
|
+
adapter_source = _host_bind_directory(
|
|
546
|
+
adapter_checkout,
|
|
547
|
+
label="worker adapter checkout",
|
|
548
|
+
)
|
|
549
|
+
if checkout_source != adapter_source and (
|
|
550
|
+
checkout_source.is_relative_to(adapter_source)
|
|
551
|
+
or adapter_source.is_relative_to(checkout_source)
|
|
552
|
+
):
|
|
553
|
+
raise ValidationError("worker checkout and adapter checkout must not partially overlap")
|
|
554
|
+
asset_source = Path(self.asset_root) if self.asset_root is not None else None
|
|
555
|
+
if asset_source is not None:
|
|
556
|
+
for source in (checkout_source, adapter_source):
|
|
557
|
+
if (
|
|
558
|
+
asset_source == source
|
|
559
|
+
or asset_source.is_relative_to(source)
|
|
560
|
+
or source.is_relative_to(asset_source)
|
|
561
|
+
):
|
|
562
|
+
raise ValidationError("worker asset root must not overlap either checkout")
|
|
563
|
+
relative = PurePosixPath(entrypoint_relative_path)
|
|
564
|
+
if (
|
|
565
|
+
relative.is_absolute()
|
|
566
|
+
or not relative.parts
|
|
567
|
+
or any(part in {"", ".", ".."} for part in relative.parts)
|
|
568
|
+
):
|
|
569
|
+
raise ValidationError("worker entrypoint_relative_path must stay inside the adapter")
|
|
570
|
+
expected_host_entrypoint = adapter_source.joinpath(*relative.parts)
|
|
571
|
+
if len(argv) < 2 or Path(argv[1]).resolve() != expected_host_entrypoint:
|
|
572
|
+
raise ValidationError("worker argv does not match the attested adapter entrypoint")
|
|
573
|
+
mapped_environment = _container_worker_environment(
|
|
574
|
+
environment or {},
|
|
575
|
+
checkout=checkout_source,
|
|
576
|
+
adapter_checkout=adapter_source,
|
|
577
|
+
asset_root=asset_source,
|
|
578
|
+
)
|
|
579
|
+
mounts: list[tuple[str, str]] = [
|
|
580
|
+
(str(checkout_source), "/opt/upstream"),
|
|
581
|
+
(str(adapter_source), "/opt/candidate"),
|
|
582
|
+
]
|
|
583
|
+
if self.asset_root is not None:
|
|
584
|
+
mounts.append((self.asset_root, "/opt/assets"))
|
|
585
|
+
container_adapter = (
|
|
586
|
+
PurePosixPath("/opt/upstream")
|
|
587
|
+
if checkout_source == adapter_source
|
|
588
|
+
else PurePosixPath("/opt/candidate")
|
|
589
|
+
)
|
|
590
|
+
container_argv = (
|
|
591
|
+
"-I",
|
|
592
|
+
"-B",
|
|
593
|
+
"-c",
|
|
594
|
+
_LOCKED_PYTHON_LAUNCHER,
|
|
595
|
+
"/opt/upstream",
|
|
596
|
+
"/opt/upstream/src" if (checkout_source / "src").is_dir() else "",
|
|
597
|
+
str(container_adapter),
|
|
598
|
+
str(container_adapter.joinpath(relative)),
|
|
599
|
+
*tuple(argv[2:]),
|
|
600
|
+
)
|
|
601
|
+
return self.execute(
|
|
602
|
+
container_argv,
|
|
603
|
+
input_text=input_text,
|
|
604
|
+
limits=limits,
|
|
605
|
+
max_input_bytes=max_input_bytes,
|
|
606
|
+
max_stdout_bytes=max_stdout_bytes,
|
|
607
|
+
max_stderr_bytes=max_stderr_bytes,
|
|
608
|
+
cwd="/opt/upstream",
|
|
609
|
+
environment=mapped_environment,
|
|
610
|
+
process_name=process_name,
|
|
611
|
+
image_identity=image_identity,
|
|
612
|
+
accelerator_sampler=accelerator_sampler,
|
|
613
|
+
container_mounts=tuple(mounts),
|
|
614
|
+
container_entrypoint=self.container_python,
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
def execute(
|
|
618
|
+
self,
|
|
619
|
+
argv: Sequence[str],
|
|
620
|
+
*,
|
|
621
|
+
input_text: str,
|
|
622
|
+
limits: ResourceLimits,
|
|
623
|
+
max_input_bytes: int,
|
|
624
|
+
max_stdout_bytes: int,
|
|
625
|
+
max_stderr_bytes: int,
|
|
626
|
+
cwd: str | None,
|
|
627
|
+
environment: Mapping[str, str] | None,
|
|
628
|
+
process_name: str,
|
|
629
|
+
image_identity: Mapping[str, Any] | None = None,
|
|
630
|
+
accelerator_sampler: AcceleratorSampler | None = None,
|
|
631
|
+
container_mounts: Sequence[tuple[str, str]] = (),
|
|
632
|
+
container_entrypoint: str | None = None,
|
|
633
|
+
) -> SupervisedProcessResult:
|
|
634
|
+
if not isinstance(limits, ResourceLimits):
|
|
635
|
+
raise TypeError("limits must be ResourceLimits")
|
|
636
|
+
missing = [
|
|
637
|
+
name
|
|
638
|
+
for name, value in (
|
|
639
|
+
("memory_bytes", limits.memory_bytes),
|
|
640
|
+
("cpus", limits.cpus),
|
|
641
|
+
("pids_limit", limits.pids_limit),
|
|
642
|
+
)
|
|
643
|
+
if value is None
|
|
644
|
+
]
|
|
645
|
+
if missing:
|
|
646
|
+
raise ValidationError(
|
|
647
|
+
"Docker supervisor requires explicit hard " + ", ".join(missing) + " limits"
|
|
648
|
+
)
|
|
649
|
+
name = f"{self.name_prefix}-{uuid.uuid4().hex}"
|
|
650
|
+
command = self._run_command(
|
|
651
|
+
name=name,
|
|
652
|
+
argv=argv,
|
|
653
|
+
limits=limits,
|
|
654
|
+
cwd=cwd,
|
|
655
|
+
environment=environment,
|
|
656
|
+
mounts=container_mounts,
|
|
657
|
+
entrypoint=container_entrypoint,
|
|
658
|
+
)
|
|
659
|
+
started = time.perf_counter()
|
|
660
|
+
sample_token = _start_accelerator_sample(accelerator_sampler)
|
|
661
|
+
result: BoundedProcessResult | None = None
|
|
662
|
+
run_error: ExternalDependencyError | None = None
|
|
663
|
+
run_error_reason: ResourceExitReason | None = None
|
|
664
|
+
run_observation = ProcessResourceObservation()
|
|
665
|
+
timed_out = False
|
|
666
|
+
state: Mapping[str, Any] = {}
|
|
667
|
+
cleanup_error: ExternalDependencyError | None = None
|
|
668
|
+
try:
|
|
669
|
+
try:
|
|
670
|
+
result = self.runner.run(
|
|
671
|
+
command,
|
|
672
|
+
input_text=input_text,
|
|
673
|
+
timeout_seconds=limits.wall_timeout_seconds,
|
|
674
|
+
max_input_bytes=max_input_bytes,
|
|
675
|
+
max_stdout_bytes=max_stdout_bytes,
|
|
676
|
+
max_stderr_bytes=max_stderr_bytes,
|
|
677
|
+
cwd=None,
|
|
678
|
+
environment=environment,
|
|
679
|
+
process_name=process_name,
|
|
680
|
+
)
|
|
681
|
+
except BoundedProcessTimeoutError as exc:
|
|
682
|
+
timed_out = True
|
|
683
|
+
run_error = exc
|
|
684
|
+
run_error_reason = ResourceExitReason.TIMED_OUT
|
|
685
|
+
run_observation = exc.resource_observation
|
|
686
|
+
except BoundedProcessStartupError as exc:
|
|
687
|
+
run_error = exc
|
|
688
|
+
run_error_reason = ResourceExitReason.STARTUP_FAILED
|
|
689
|
+
run_observation = exc.resource_observation
|
|
690
|
+
except ExternalDependencyError as exc:
|
|
691
|
+
run_error = exc
|
|
692
|
+
run_error_reason = ResourceExitReason.SUPERVISOR_ERROR
|
|
693
|
+
run_observation = _error_observation(exc)
|
|
694
|
+
if result is not None:
|
|
695
|
+
run_observation = result.resource_observation
|
|
696
|
+
run_observation = _docker_workload_observation(run_observation)
|
|
697
|
+
state = self._inspect_state(name)
|
|
698
|
+
finally:
|
|
699
|
+
try:
|
|
700
|
+
self._remove_container(name)
|
|
701
|
+
except ExternalDependencyError as exc:
|
|
702
|
+
cleanup_error = exc
|
|
703
|
+
|
|
704
|
+
try:
|
|
705
|
+
sample = _stop_accelerator_sample(
|
|
706
|
+
accelerator_sampler,
|
|
707
|
+
sample_token,
|
|
708
|
+
observation=run_observation,
|
|
709
|
+
)
|
|
710
|
+
except Exception as exc:
|
|
711
|
+
sample = AcceleratorSample()
|
|
712
|
+
if run_error is None:
|
|
713
|
+
run_error = ExternalDependencyError("accelerator sampler could not finalize")
|
|
714
|
+
run_error.__cause__ = exc
|
|
715
|
+
run_error_reason = ResourceExitReason.SUPERVISOR_ERROR
|
|
716
|
+
|
|
717
|
+
oom_killed = bool(state.get("OOMKilled", False)) and not timed_out
|
|
718
|
+
exit_code = _docker_exit_code(state, result)
|
|
719
|
+
if timed_out:
|
|
720
|
+
reason = ResourceExitReason.TIMED_OUT
|
|
721
|
+
elif oom_killed:
|
|
722
|
+
reason = ResourceExitReason.OOM_KILLED
|
|
723
|
+
elif run_error is not None:
|
|
724
|
+
reason = run_error_reason or ResourceExitReason.SUPERVISOR_ERROR
|
|
725
|
+
elif cleanup_error is not None:
|
|
726
|
+
reason = ResourceExitReason.SUPERVISOR_ERROR
|
|
727
|
+
elif exit_code == 0:
|
|
728
|
+
reason = ResourceExitReason.COMPLETED
|
|
729
|
+
else:
|
|
730
|
+
reason = ResourceExitReason.NONZERO_EXIT
|
|
731
|
+
receipt = ResourceReceipt(
|
|
732
|
+
limit_hash=limits.limit_hash,
|
|
733
|
+
wall_seconds=time.perf_counter() - started,
|
|
734
|
+
cpu_seconds=run_observation.cpu_seconds,
|
|
735
|
+
peak_rss_bytes=run_observation.peak_rss_bytes,
|
|
736
|
+
accelerator_seconds=sample.accelerator_seconds,
|
|
737
|
+
accelerator_peak_memory_bytes=(sample.accelerator_peak_memory_bytes),
|
|
738
|
+
oom_killed=oom_killed,
|
|
739
|
+
timed_out=timed_out,
|
|
740
|
+
exit_reason=reason,
|
|
741
|
+
exit_code=exit_code,
|
|
742
|
+
supervisor_identity={
|
|
743
|
+
**dict(self.fingerprint()),
|
|
744
|
+
"accelerator_sampler": dict(sample.sampler_identity),
|
|
745
|
+
"measurement": dict(run_observation.measurement_identity),
|
|
746
|
+
},
|
|
747
|
+
image_identity={
|
|
748
|
+
"reference": self.image,
|
|
749
|
+
"digest": self.fingerprint()["image_digest"],
|
|
750
|
+
"worker": dict(image_identity or {}),
|
|
751
|
+
},
|
|
752
|
+
)
|
|
753
|
+
if run_error is not None:
|
|
754
|
+
raise SupervisedWorkerError(
|
|
755
|
+
str(run_error),
|
|
756
|
+
resource_receipt=receipt,
|
|
757
|
+
) from run_error
|
|
758
|
+
if cleanup_error is not None:
|
|
759
|
+
raise SupervisedWorkerError(
|
|
760
|
+
"Docker supervisor could not remove its worker container",
|
|
761
|
+
resource_receipt=receipt,
|
|
762
|
+
) from cleanup_error
|
|
763
|
+
if result is None: # pragma: no cover - guarded by error branches
|
|
764
|
+
raise AssertionError("Docker runner produced neither a result nor an error")
|
|
765
|
+
return SupervisedProcessResult(
|
|
766
|
+
returncode=result.returncode,
|
|
767
|
+
stdout=result.stdout,
|
|
768
|
+
stderr=result.stderr,
|
|
769
|
+
resource_receipt=receipt,
|
|
770
|
+
)
|
|
771
|
+
|
|
772
|
+
def _run_command(
|
|
773
|
+
self,
|
|
774
|
+
*,
|
|
775
|
+
name: str,
|
|
776
|
+
argv: Sequence[str],
|
|
777
|
+
limits: ResourceLimits,
|
|
778
|
+
cwd: str | None,
|
|
779
|
+
environment: Mapping[str, str] | None,
|
|
780
|
+
mounts: Sequence[tuple[str, str]],
|
|
781
|
+
entrypoint: str | None,
|
|
782
|
+
) -> tuple[str, ...]:
|
|
783
|
+
command = [
|
|
784
|
+
self.docker_executable,
|
|
785
|
+
"run",
|
|
786
|
+
"--name",
|
|
787
|
+
name,
|
|
788
|
+
"--init",
|
|
789
|
+
"--pull",
|
|
790
|
+
"never",
|
|
791
|
+
"--network",
|
|
792
|
+
"none",
|
|
793
|
+
"--read-only",
|
|
794
|
+
"--cap-drop",
|
|
795
|
+
"ALL",
|
|
796
|
+
"--security-opt",
|
|
797
|
+
"no-new-privileges",
|
|
798
|
+
"--pids-limit",
|
|
799
|
+
str(limits.pids_limit),
|
|
800
|
+
"--memory",
|
|
801
|
+
f"{limits.memory_bytes}b",
|
|
802
|
+
"--memory-swap",
|
|
803
|
+
f"{limits.memory_bytes}b",
|
|
804
|
+
"--cpus",
|
|
805
|
+
_format_number(limits.cpus),
|
|
806
|
+
"--user",
|
|
807
|
+
self.container_user,
|
|
808
|
+
"--tmpfs",
|
|
809
|
+
(f"/tmp:rw,noexec,nosuid,nodev,size={self.tmpfs_size_bytes}"),
|
|
810
|
+
]
|
|
811
|
+
for source, target in mounts:
|
|
812
|
+
source_path = _host_bind_directory(source, label="Docker worker mount")
|
|
813
|
+
if not target.startswith("/") or any(character in target for character in ",\x00\n\r"):
|
|
814
|
+
raise ValidationError("Docker worker mount target is not argument-safe")
|
|
815
|
+
command.extend(
|
|
816
|
+
(
|
|
817
|
+
"--mount",
|
|
818
|
+
f"type=bind,source={source_path},target={target},readonly",
|
|
819
|
+
)
|
|
820
|
+
)
|
|
821
|
+
if cwd is not None:
|
|
822
|
+
command.extend(("--workdir", cwd))
|
|
823
|
+
if entrypoint is not None:
|
|
824
|
+
command.extend(("--entrypoint", entrypoint))
|
|
825
|
+
for key in sorted((environment or {}).keys()):
|
|
826
|
+
command.extend(("--env", key))
|
|
827
|
+
command.extend(("-i", self.image, *tuple(argv)))
|
|
828
|
+
return tuple(command)
|
|
829
|
+
|
|
830
|
+
def _inspect_state(self, name: str) -> Mapping[str, Any]:
|
|
831
|
+
try:
|
|
832
|
+
result = self.runner.run(
|
|
833
|
+
(
|
|
834
|
+
self.docker_executable,
|
|
835
|
+
"inspect",
|
|
836
|
+
"--format",
|
|
837
|
+
"{{json .State}}",
|
|
838
|
+
name,
|
|
839
|
+
),
|
|
840
|
+
input_text="",
|
|
841
|
+
timeout_seconds=self.management_timeout_seconds,
|
|
842
|
+
max_input_bytes=1,
|
|
843
|
+
max_stdout_bytes=64 * 1024,
|
|
844
|
+
max_stderr_bytes=64 * 1024,
|
|
845
|
+
cwd=None,
|
|
846
|
+
environment=None,
|
|
847
|
+
process_name="Docker worker state inspection",
|
|
848
|
+
)
|
|
849
|
+
except ExternalDependencyError:
|
|
850
|
+
return {}
|
|
851
|
+
if result.returncode != 0:
|
|
852
|
+
return {}
|
|
853
|
+
try:
|
|
854
|
+
state = json.loads(result.stdout.strip())
|
|
855
|
+
except (json.JSONDecodeError, TypeError):
|
|
856
|
+
return {}
|
|
857
|
+
return state if isinstance(state, dict) else {}
|
|
858
|
+
|
|
859
|
+
def _remove_container(self, name: str) -> None:
|
|
860
|
+
result = self.runner.run(
|
|
861
|
+
(self.docker_executable, "rm", "--force", name),
|
|
862
|
+
input_text="",
|
|
863
|
+
timeout_seconds=self.management_timeout_seconds,
|
|
864
|
+
max_input_bytes=1,
|
|
865
|
+
max_stdout_bytes=64 * 1024,
|
|
866
|
+
max_stderr_bytes=64 * 1024,
|
|
867
|
+
cwd=None,
|
|
868
|
+
environment=None,
|
|
869
|
+
process_name="Docker worker cleanup",
|
|
870
|
+
)
|
|
871
|
+
if result.returncode != 0:
|
|
872
|
+
raise ExternalDependencyError("Docker worker cleanup exited non-zero")
|
|
873
|
+
|
|
874
|
+
|
|
875
|
+
def worker_supervisor_from_environment(
|
|
876
|
+
environment: Mapping[str, str] | None = None,
|
|
877
|
+
) -> WorkerSupervisor:
|
|
878
|
+
"""Resolve an explicit local-or-Docker supervisor without guessing.
|
|
879
|
+
|
|
880
|
+
Local execution remains the compatibility default. Docker execution is
|
|
881
|
+
enabled only when the operator sets ``AILAB_WORKER_SUPERVISOR=docker`` and
|
|
882
|
+
supplies an immutable image identity.
|
|
883
|
+
"""
|
|
884
|
+
|
|
885
|
+
values = os.environ if environment is None else environment
|
|
886
|
+
kind = values.get(WORKER_SUPERVISOR_KIND_ENV, "local").strip().casefold()
|
|
887
|
+
image = values.get(WORKER_SUPERVISOR_IMAGE_ENV, "").strip()
|
|
888
|
+
asset_root = values.get(WORKER_SUPERVISOR_ASSET_ROOT_ENV, "").strip()
|
|
889
|
+
if kind == "local":
|
|
890
|
+
if image or asset_root:
|
|
891
|
+
raise ValidationError(
|
|
892
|
+
f"Docker worker image/asset settings require {WORKER_SUPERVISOR_KIND_ENV}=docker"
|
|
893
|
+
)
|
|
894
|
+
return LocalSubprocessSupervisor()
|
|
895
|
+
if kind != "docker":
|
|
896
|
+
raise ValidationError(f"{WORKER_SUPERVISOR_KIND_ENV} must be either local or docker")
|
|
897
|
+
if not image:
|
|
898
|
+
raise ValidationError(f"{WORKER_SUPERVISOR_IMAGE_ENV} is required for Docker supervision")
|
|
899
|
+
return DockerWorkerSupervisor(
|
|
900
|
+
image=image,
|
|
901
|
+
asset_root=asset_root or None,
|
|
902
|
+
)
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
def _host_bind_directory(value: str | Path, *, label: str) -> Path:
|
|
906
|
+
raw = Path(value).expanduser()
|
|
907
|
+
if not raw.is_absolute():
|
|
908
|
+
raise ValidationError(f"{label} must be absolute")
|
|
909
|
+
if raw == Path(raw.anchor):
|
|
910
|
+
raise ValidationError(f"{label} must not be a filesystem root")
|
|
911
|
+
if raw.is_symlink():
|
|
912
|
+
raise ValidationError(f"{label} must not be a symlink")
|
|
913
|
+
resolved = raw.resolve()
|
|
914
|
+
if resolved != raw or not resolved.is_dir():
|
|
915
|
+
raise ValidationError(f"{label} must be an existing canonical directory")
|
|
916
|
+
if any(character in str(resolved) for character in ",\x00\n\r"):
|
|
917
|
+
raise ValidationError(f"{label} contains a Docker-unsafe character")
|
|
918
|
+
return resolved
|
|
919
|
+
|
|
920
|
+
|
|
921
|
+
def _container_worker_environment(
|
|
922
|
+
environment: Mapping[str, str],
|
|
923
|
+
*,
|
|
924
|
+
checkout: Path,
|
|
925
|
+
adapter_checkout: Path,
|
|
926
|
+
asset_root: Path | None,
|
|
927
|
+
) -> Mapping[str, str]:
|
|
928
|
+
roots: tuple[tuple[Path, PurePosixPath], ...] = (
|
|
929
|
+
(checkout, PurePosixPath("/opt/upstream")),
|
|
930
|
+
(adapter_checkout, PurePosixPath("/opt/candidate")),
|
|
931
|
+
*(((asset_root, PurePosixPath("/opt/assets")),) if asset_root is not None else ()),
|
|
932
|
+
)
|
|
933
|
+
result: dict[str, str] = {}
|
|
934
|
+
for key, raw_value in environment.items():
|
|
935
|
+
if not isinstance(key, str) or not isinstance(raw_value, str):
|
|
936
|
+
raise ValidationError("Docker worker environment must contain strings")
|
|
937
|
+
if key == "PWD":
|
|
938
|
+
result[key] = "/opt/upstream"
|
|
939
|
+
continue
|
|
940
|
+
if key == "PYTHONPATH":
|
|
941
|
+
# The attested implementation checkout owns algorithm imports.
|
|
942
|
+
# Keep the adapter root available only after the complete upstream
|
|
943
|
+
# search path so a same-named candidate module cannot contaminate
|
|
944
|
+
# an upstream parity arm. ``candidate/src`` is intentionally not
|
|
945
|
+
# exposed: the adapter executes by absolute path and may import
|
|
946
|
+
# support packages from its root, while AgentInputLab itself comes
|
|
947
|
+
# from the pinned worker image.
|
|
948
|
+
paths = ["/opt/upstream"]
|
|
949
|
+
if (checkout / "src").is_dir():
|
|
950
|
+
paths.append("/opt/upstream/src")
|
|
951
|
+
if adapter_checkout != checkout:
|
|
952
|
+
paths.append("/opt/candidate")
|
|
953
|
+
paths.append("/opt/agentinputlab")
|
|
954
|
+
result[key] = ":".join(paths)
|
|
955
|
+
continue
|
|
956
|
+
value_path = Path(raw_value).expanduser()
|
|
957
|
+
if value_path.is_absolute():
|
|
958
|
+
canonical = value_path.resolve()
|
|
959
|
+
mapped: str | None = None
|
|
960
|
+
for host_root, container_root in roots:
|
|
961
|
+
if canonical == host_root or canonical.is_relative_to(host_root):
|
|
962
|
+
relative = canonical.relative_to(host_root)
|
|
963
|
+
mapped = str(container_root.joinpath(*relative.parts))
|
|
964
|
+
break
|
|
965
|
+
if mapped is None:
|
|
966
|
+
raise ValidationError(
|
|
967
|
+
f"Docker worker environment {key!r} contains an unmapped host path"
|
|
968
|
+
)
|
|
969
|
+
result[key] = mapped
|
|
970
|
+
else:
|
|
971
|
+
result[key] = raw_value
|
|
972
|
+
result["HOME"] = "/tmp/worker-home"
|
|
973
|
+
return freeze_mapping(result)
|
|
974
|
+
|
|
975
|
+
|
|
976
|
+
def _start_accelerator_sample(sampler: AcceleratorSampler | None) -> object | None:
|
|
977
|
+
return None if sampler is None else sampler.start()
|
|
978
|
+
|
|
979
|
+
|
|
980
|
+
def _stop_accelerator_sample(
|
|
981
|
+
sampler: AcceleratorSampler | None,
|
|
982
|
+
token: object | None,
|
|
983
|
+
*,
|
|
984
|
+
observation: ProcessResourceObservation,
|
|
985
|
+
) -> AcceleratorSample:
|
|
986
|
+
if sampler is None:
|
|
987
|
+
identity = observation.measurement_identity.get("accelerator", {})
|
|
988
|
+
if not isinstance(identity, Mapping) or not identity:
|
|
989
|
+
identity = {
|
|
990
|
+
"status": "unavailable",
|
|
991
|
+
"reason": "process_runner_did_not_report",
|
|
992
|
+
}
|
|
993
|
+
return AcceleratorSample(
|
|
994
|
+
accelerator_seconds=observation.accelerator_seconds,
|
|
995
|
+
accelerator_peak_memory_bytes=(observation.accelerator_peak_memory_bytes),
|
|
996
|
+
sampler_identity=identity,
|
|
997
|
+
)
|
|
998
|
+
sample = sampler.stop(token)
|
|
999
|
+
if not isinstance(sample, AcceleratorSample):
|
|
1000
|
+
raise ValidationError("accelerator sampler must return AcceleratorSample")
|
|
1001
|
+
return sample
|
|
1002
|
+
|
|
1003
|
+
|
|
1004
|
+
def _error_observation(error: ExternalDependencyError) -> ProcessResourceObservation:
|
|
1005
|
+
observation = getattr(error, "resource_observation", None)
|
|
1006
|
+
return (
|
|
1007
|
+
observation
|
|
1008
|
+
if isinstance(observation, ProcessResourceObservation)
|
|
1009
|
+
else ProcessResourceObservation()
|
|
1010
|
+
)
|
|
1011
|
+
|
|
1012
|
+
|
|
1013
|
+
def _stop_accelerator_after_process_error(
|
|
1014
|
+
sampler: AcceleratorSampler | None,
|
|
1015
|
+
token: object | None,
|
|
1016
|
+
*,
|
|
1017
|
+
observation: ProcessResourceObservation,
|
|
1018
|
+
) -> AcceleratorSample:
|
|
1019
|
+
try:
|
|
1020
|
+
return _stop_accelerator_sample(
|
|
1021
|
+
sampler,
|
|
1022
|
+
token,
|
|
1023
|
+
observation=observation,
|
|
1024
|
+
)
|
|
1025
|
+
except Exception as exc:
|
|
1026
|
+
return AcceleratorSample(
|
|
1027
|
+
sampler_identity={
|
|
1028
|
+
"status": "unavailable",
|
|
1029
|
+
"reason": f"finalization_failed:{type(exc).__name__}",
|
|
1030
|
+
}
|
|
1031
|
+
)
|
|
1032
|
+
|
|
1033
|
+
|
|
1034
|
+
def _docker_workload_observation(
|
|
1035
|
+
observation: ProcessResourceObservation,
|
|
1036
|
+
) -> ProcessResourceObservation:
|
|
1037
|
+
process_identity = observation.measurement_identity.get("process_tree", {})
|
|
1038
|
+
if (
|
|
1039
|
+
isinstance(process_identity, Mapping)
|
|
1040
|
+
and process_identity.get("scope") == "supervised_workload"
|
|
1041
|
+
):
|
|
1042
|
+
return observation
|
|
1043
|
+
return ProcessResourceObservation(
|
|
1044
|
+
measurement_identity={
|
|
1045
|
+
"process_tree": {
|
|
1046
|
+
"status": "unavailable",
|
|
1047
|
+
"reason": "container_process_is_not_a_child_of_the_docker_client",
|
|
1048
|
+
"launcher_observation": dict(observation.measurement_identity),
|
|
1049
|
+
},
|
|
1050
|
+
"accelerator": {
|
|
1051
|
+
"status": "unavailable",
|
|
1052
|
+
"reason": "container_process_is_not_a_child_of_the_docker_client",
|
|
1053
|
+
},
|
|
1054
|
+
}
|
|
1055
|
+
)
|
|
1056
|
+
|
|
1057
|
+
|
|
1058
|
+
def _docker_exit_code(
|
|
1059
|
+
state: Mapping[str, Any],
|
|
1060
|
+
result: BoundedProcessResult | None,
|
|
1061
|
+
) -> int | None:
|
|
1062
|
+
value = state.get("ExitCode")
|
|
1063
|
+
if isinstance(value, int) and not isinstance(value, bool):
|
|
1064
|
+
return value
|
|
1065
|
+
return result.returncode if result is not None else None
|
|
1066
|
+
|
|
1067
|
+
|
|
1068
|
+
def _format_number(value: float | None) -> str:
|
|
1069
|
+
if value is None: # pragma: no cover - validated by caller
|
|
1070
|
+
raise AssertionError("required Docker numeric limit is missing")
|
|
1071
|
+
return format(value, ".15g")
|