gatewaysdk 0.3.2__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.
- gatewaysdk/__init__.py +540 -0
- gatewaysdk/adapter/__init__.py +15 -0
- gatewaysdk/adapter/base.py +94 -0
- gatewaysdk/adapter/messages.py +270 -0
- gatewaysdk/adapter/triplet.py +1026 -0
- gatewaysdk/algorithm/__init__.py +39 -0
- gatewaysdk/algorithm/apo/__init__.py +5 -0
- gatewaysdk/algorithm/apo/apo.py +898 -0
- gatewaysdk/algorithm/apo/prompts/apply_edit_variant01.poml +22 -0
- gatewaysdk/algorithm/apo/prompts/apply_edit_variant02.poml +18 -0
- gatewaysdk/algorithm/apo/prompts/text_gradient_variant01.poml +18 -0
- gatewaysdk/algorithm/apo/prompts/text_gradient_variant02.poml +16 -0
- gatewaysdk/algorithm/apo/prompts/text_gradient_variant03.poml +107 -0
- gatewaysdk/algorithm/base.py +262 -0
- gatewaysdk/algorithm/decorator.py +264 -0
- gatewaysdk/algorithm/evals/__init__.py +7 -0
- gatewaysdk/algorithm/evals/evals.py +217 -0
- gatewaysdk/algorithm/fast.py +250 -0
- gatewaysdk/algorithm/gepa/__init__.py +61 -0
- gatewaysdk/algorithm/gepa/adapter.py +495 -0
- gatewaysdk/algorithm/gepa/gepa.py +570 -0
- gatewaysdk/algorithm/gepa/lib/__init__.py +18 -0
- gatewaysdk/algorithm/gepa/lib/adapters/README.md +12 -0
- gatewaysdk/algorithm/gepa/lib/adapters/__init__.py +0 -0
- gatewaysdk/algorithm/gepa/lib/adapters/anymaths_adapter/README.md +341 -0
- gatewaysdk/algorithm/gepa/lib/adapters/anymaths_adapter/__init__.py +1 -0
- gatewaysdk/algorithm/gepa/lib/adapters/anymaths_adapter/anymaths_adapter.py +174 -0
- gatewaysdk/algorithm/gepa/lib/adapters/anymaths_adapter/requirements.txt +1 -0
- gatewaysdk/algorithm/gepa/lib/adapters/default_adapter/README.md +0 -0
- gatewaysdk/algorithm/gepa/lib/adapters/default_adapter/__init__.py +0 -0
- gatewaysdk/algorithm/gepa/lib/adapters/default_adapter/default_adapter.py +209 -0
- gatewaysdk/algorithm/gepa/lib/adapters/dspy_adapter/README.md +7 -0
- gatewaysdk/algorithm/gepa/lib/adapters/dspy_adapter/__init__.py +0 -0
- gatewaysdk/algorithm/gepa/lib/adapters/dspy_adapter/dspy_adapter.py +307 -0
- gatewaysdk/algorithm/gepa/lib/adapters/dspy_full_program_adapter/README.md +99 -0
- gatewaysdk/algorithm/gepa/lib/adapters/dspy_full_program_adapter/dspy_program_proposal_signature.py +137 -0
- gatewaysdk/algorithm/gepa/lib/adapters/dspy_full_program_adapter/full_program_adapter.py +268 -0
- gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/GEPA_RAG.md +621 -0
- gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/__init__.py +56 -0
- gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/evaluation_metrics.py +226 -0
- gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/generic_rag_adapter.py +496 -0
- gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/rag_pipeline.py +238 -0
- gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_store_interface.py +212 -0
- gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/__init__.py +2 -0
- gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/chroma_store.py +196 -0
- gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/lancedb_store.py +422 -0
- gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/milvus_store.py +409 -0
- gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/qdrant_store.py +368 -0
- gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/weaviate_store.py +418 -0
- gatewaysdk/algorithm/gepa/lib/adapters/mcp_adapter/README.md +552 -0
- gatewaysdk/algorithm/gepa/lib/adapters/mcp_adapter/__init__.py +37 -0
- gatewaysdk/algorithm/gepa/lib/adapters/mcp_adapter/mcp_adapter.py +699 -0
- gatewaysdk/algorithm/gepa/lib/adapters/mcp_adapter/mcp_client.py +364 -0
- gatewaysdk/algorithm/gepa/lib/adapters/terminal_bench_adapter/README.md +9 -0
- gatewaysdk/algorithm/gepa/lib/adapters/terminal_bench_adapter/__init__.py +0 -0
- gatewaysdk/algorithm/gepa/lib/adapters/terminal_bench_adapter/terminal_bench_adapter.py +217 -0
- gatewaysdk/algorithm/gepa/lib/api.py +382 -0
- gatewaysdk/algorithm/gepa/lib/core/__init__.py +0 -0
- gatewaysdk/algorithm/gepa/lib/core/adapter.py +180 -0
- gatewaysdk/algorithm/gepa/lib/core/data_loader.py +74 -0
- gatewaysdk/algorithm/gepa/lib/core/engine.py +379 -0
- gatewaysdk/algorithm/gepa/lib/core/result.py +233 -0
- gatewaysdk/algorithm/gepa/lib/core/state.py +636 -0
- gatewaysdk/algorithm/gepa/lib/examples/__init__.py +0 -0
- gatewaysdk/algorithm/gepa/lib/examples/aime.py +24 -0
- gatewaysdk/algorithm/gepa/lib/examples/anymaths-bench/eval_default.py +111 -0
- gatewaysdk/algorithm/gepa/lib/examples/anymaths-bench/prompt-templates/instruction_prompt.txt +9 -0
- gatewaysdk/algorithm/gepa/lib/examples/anymaths-bench/prompt-templates/optimal_prompt.txt +24 -0
- gatewaysdk/algorithm/gepa/lib/examples/anymaths-bench/train_anymaths.py +177 -0
- gatewaysdk/algorithm/gepa/lib/examples/dspy_full_program_evolution/arc_agi.ipynb +25705 -0
- gatewaysdk/algorithm/gepa/lib/examples/dspy_full_program_evolution/example.ipynb +348 -0
- gatewaysdk/algorithm/gepa/lib/examples/mcp_adapter/__init__.py +4 -0
- gatewaysdk/algorithm/gepa/lib/examples/mcp_adapter/mcp_optimization_example.py +456 -0
- gatewaysdk/algorithm/gepa/lib/examples/rag_adapter/RAG_GUIDE.md +613 -0
- gatewaysdk/algorithm/gepa/lib/examples/rag_adapter/__init__.py +9 -0
- gatewaysdk/algorithm/gepa/lib/examples/rag_adapter/rag_optimization.py +820 -0
- gatewaysdk/algorithm/gepa/lib/examples/rag_adapter/requirements-rag.txt +29 -0
- gatewaysdk/algorithm/gepa/lib/examples/terminal-bench/prompt-templates/instruction_prompt.txt +16 -0
- gatewaysdk/algorithm/gepa/lib/examples/terminal-bench/prompt-templates/terminus.txt +9 -0
- gatewaysdk/algorithm/gepa/lib/examples/terminal-bench/train_terminus.py +161 -0
- gatewaysdk/algorithm/gepa/lib/gepa_utils.py +117 -0
- gatewaysdk/algorithm/gepa/lib/logging/__init__.py +0 -0
- gatewaysdk/algorithm/gepa/lib/logging/experiment_tracker.py +187 -0
- gatewaysdk/algorithm/gepa/lib/logging/logger.py +75 -0
- gatewaysdk/algorithm/gepa/lib/logging/utils.py +103 -0
- gatewaysdk/algorithm/gepa/lib/proposer/__init__.py +0 -0
- gatewaysdk/algorithm/gepa/lib/proposer/base.py +31 -0
- gatewaysdk/algorithm/gepa/lib/proposer/merge.py +357 -0
- gatewaysdk/algorithm/gepa/lib/proposer/reflective_mutation/__init__.py +0 -0
- gatewaysdk/algorithm/gepa/lib/proposer/reflective_mutation/base.py +49 -0
- gatewaysdk/algorithm/gepa/lib/proposer/reflective_mutation/reflective_mutation.py +176 -0
- gatewaysdk/algorithm/gepa/lib/py.typed +0 -0
- gatewaysdk/algorithm/gepa/lib/strategies/__init__.py +0 -0
- gatewaysdk/algorithm/gepa/lib/strategies/batch_sampler.py +77 -0
- gatewaysdk/algorithm/gepa/lib/strategies/candidate_selector.py +50 -0
- gatewaysdk/algorithm/gepa/lib/strategies/component_selector.py +36 -0
- gatewaysdk/algorithm/gepa/lib/strategies/eval_policy.py +64 -0
- gatewaysdk/algorithm/gepa/lib/strategies/instruction_proposal.py +126 -0
- gatewaysdk/algorithm/gepa/lib/utils/__init__.py +10 -0
- gatewaysdk/algorithm/gepa/lib/utils/stop_condition.py +196 -0
- gatewaysdk/algorithm/gepa/tracing.py +105 -0
- gatewaysdk/algorithm/utils.py +177 -0
- gatewaysdk/algorithm/verl/__init__.py +5 -0
- gatewaysdk/algorithm/verl/interface.py +202 -0
- gatewaysdk/automations.py +111 -0
- gatewaysdk/benchmark_hub/README.md +88 -0
- gatewaysdk/benchmark_hub/__init__.py +119 -0
- gatewaysdk/benchmark_hub/_archive.py +189 -0
- gatewaysdk/benchmark_hub/_tar.py +6 -0
- gatewaysdk/benchmark_hub/client.py +992 -0
- gatewaysdk/benchmark_hub/dispatch_shard.py +34 -0
- gatewaysdk/benchmark_hub/eval_config.py +20 -0
- gatewaysdk/benchmark_hub/evals.py +351 -0
- gatewaysdk/benchmark_hub/harbor_adapter.py +673 -0
- gatewaysdk/benchmark_hub/path_utils.py +81 -0
- gatewaysdk/benchmark_hub/save_utils.py +101 -0
- gatewaysdk/benchmark_hub/verifiers_adapter.py +327 -0
- gatewaysdk/benchmark_hub/versioning.py +72 -0
- gatewaysdk/build.py +515 -0
- gatewaysdk/cli/__init__.py +58 -0
- gatewaysdk/cli/agent_runner.py +132 -0
- gatewaysdk/cli/http_client.py +115 -0
- gatewaysdk/cli/platform.py +4086 -0
- gatewaysdk/cli/prometheus.py +115 -0
- gatewaysdk/cli/release_gate.py +215 -0
- gatewaysdk/cli/store.py +131 -0
- gatewaysdk/cli/vllm.py +29 -0
- gatewaysdk/client.py +406 -0
- gatewaysdk/config.py +348 -0
- gatewaysdk/connectors/__init__.py +25 -0
- gatewaysdk/connectors/client.py +203 -0
- gatewaysdk/connectors/skill.py +25 -0
- gatewaysdk/connectors/template.py +106 -0
- gatewaysdk/context.py +606 -0
- gatewaysdk/emitter/__init__.py +43 -0
- gatewaysdk/emitter/annotation.py +370 -0
- gatewaysdk/emitter/exception.py +54 -0
- gatewaysdk/emitter/message.py +61 -0
- gatewaysdk/emitter/object.py +117 -0
- gatewaysdk/emitter/reward.py +320 -0
- gatewaysdk/env_var.py +156 -0
- gatewaysdk/environment/__init__.py +108 -0
- gatewaysdk/environment/_bundle.py +281 -0
- gatewaysdk/environment/_harbor.py +276 -0
- gatewaysdk/environment/_materialize.py +97 -0
- gatewaysdk/environment/_tar.py +33 -0
- gatewaysdk/environment/_world.py +346 -0
- gatewaysdk/environment/core.py +527 -0
- gatewaysdk/environment/errors.py +58 -0
- gatewaysdk/environment/runtime.py +150 -0
- gatewaysdk/environment/schema/__init__.py +35 -0
- gatewaysdk/environment/schema/__main__.py +225 -0
- gatewaysdk/environment/schema/_slack_fidelity.py +116 -0
- gatewaysdk/environment/schema/_slack_scenario.py +183 -0
- gatewaysdk/environment/schema/api.py +1628 -0
- gatewaysdk/environment/schema/batch.py +755 -0
- gatewaysdk/environment/schema/compiler.py +615 -0
- gatewaysdk/environment/schema/conform.py +428 -0
- gatewaysdk/environment/schema/connector.py +777 -0
- gatewaysdk/environment/schema/connectors/apple-business-manager/handlers.py +17 -0
- gatewaysdk/environment/schema/connectors/apple-business-manager/parity.json +28 -0
- gatewaysdk/environment/schema/connectors/apple-business-manager/provenance.json +233 -0
- gatewaysdk/environment/schema/connectors/apple-business-manager/scope.toml +112 -0
- gatewaysdk/environment/schema/connectors/apple-business-manager/world.json +1380 -0
- gatewaysdk/environment/schema/connectors/base.json +28 -0
- gatewaysdk/environment/schema/connectors/custom/handlers.py +18 -0
- gatewaysdk/environment/schema/connectors/custom/world.json +123 -0
- gatewaysdk/environment/schema/connectors/github/handlers.py +81 -0
- gatewaysdk/environment/schema/connectors/github/parity.json +37 -0
- gatewaysdk/environment/schema/connectors/github/provenance.json +134 -0
- gatewaysdk/environment/schema/connectors/github/scope.toml +158 -0
- gatewaysdk/environment/schema/connectors/github/world.json +5869 -0
- gatewaysdk/environment/schema/connectors/google-calendar/handlers.py +23 -0
- gatewaysdk/environment/schema/connectors/google-calendar/world.json +209 -0
- gatewaysdk/environment/schema/connectors/google-drive/handlers.py +18 -0
- gatewaysdk/environment/schema/connectors/google-drive/world.json +286 -0
- gatewaysdk/environment/schema/connectors/jamf/handlers.py +34 -0
- gatewaysdk/environment/schema/connectors/jamf/parity.json +49 -0
- gatewaysdk/environment/schema/connectors/jamf/provenance.json +181 -0
- gatewaysdk/environment/schema/connectors/jamf/scope.toml +128 -0
- gatewaysdk/environment/schema/connectors/jamf/world.json +2218 -0
- gatewaysdk/environment/schema/connectors/jira/handlers.py +28 -0
- gatewaysdk/environment/schema/connectors/jira/world.json +389 -0
- gatewaysdk/environment/schema/connectors/kandji/handlers.py +38 -0
- gatewaysdk/environment/schema/connectors/kandji/parity.json +29 -0
- gatewaysdk/environment/schema/connectors/kandji/provenance.json +152 -0
- gatewaysdk/environment/schema/connectors/kandji/scope.toml +120 -0
- gatewaysdk/environment/schema/connectors/kandji/world.json +765 -0
- gatewaysdk/environment/schema/connectors/linear/handlers.py +20 -0
- gatewaysdk/environment/schema/connectors/linear/world.json +363 -0
- gatewaysdk/environment/schema/connectors/microsoft-teams/fidelity.json +45 -0
- gatewaysdk/environment/schema/connectors/microsoft-teams/handlers.py +1 -0
- gatewaysdk/environment/schema/connectors/microsoft-teams/provenance.json +851 -0
- gatewaysdk/environment/schema/connectors/microsoft-teams/scope.md +231 -0
- gatewaysdk/environment/schema/connectors/microsoft-teams/teams-types.json +1710 -0
- gatewaysdk/environment/schema/connectors/microsoft-teams/world.json +858 -0
- gatewaysdk/environment/schema/connectors/netsuite/handlers.py +32 -0
- gatewaysdk/environment/schema/connectors/netsuite/parity.json +50 -0
- gatewaysdk/environment/schema/connectors/netsuite/provenance.json +137 -0
- gatewaysdk/environment/schema/connectors/netsuite/scope.toml +78 -0
- gatewaysdk/environment/schema/connectors/netsuite/world.json +16243 -0
- gatewaysdk/environment/schema/connectors/salesforce/handlers.py +806 -0
- gatewaysdk/environment/schema/connectors/salesforce/parity.json +46 -0
- gatewaysdk/environment/schema/connectors/salesforce/provenance.json +143 -0
- gatewaysdk/environment/schema/connectors/salesforce/scope.toml +174 -0
- gatewaysdk/environment/schema/connectors/salesforce/world.json +4695 -0
- gatewaysdk/environment/schema/connectors/slack/MODEL.md +236 -0
- gatewaysdk/environment/schema/connectors/slack/capabilities.json +1039 -0
- gatewaysdk/environment/schema/connectors/slack/handlers.py +762 -0
- gatewaysdk/environment/schema/connectors/slack/parity.json +65 -0
- gatewaysdk/environment/schema/connectors/slack/provenance.json +294 -0
- gatewaysdk/environment/schema/connectors/slack/scope.toml +44 -0
- gatewaysdk/environment/schema/connectors/slack/world.json +4993 -0
- gatewaysdk/environment/schema/connectors/workday/handlers.py +75 -0
- gatewaysdk/environment/schema/connectors/workday/parity.json +36 -0
- gatewaysdk/environment/schema/connectors/workday/provenance.json +179 -0
- gatewaysdk/environment/schema/connectors/workday/scope.toml +180 -0
- gatewaysdk/environment/schema/connectors/workday/world.json +806 -0
- gatewaysdk/environment/schema/data.py +593 -0
- gatewaysdk/environment/schema/extract.py +263 -0
- gatewaysdk/environment/schema/host.py +2437 -0
- gatewaysdk/environment/schema/host_surfaces.py +275 -0
- gatewaysdk/environment/schema/platform.py +1272 -0
- gatewaysdk/environment/schema/scaffold.py +250 -0
- gatewaysdk/environment/schema/skill.py +118 -0
- gatewaysdk/environment/schema/skills/agents/connector-template-author.md +202 -0
- gatewaysdk/environment/schema/skills/connector-schema-authoring/CONNECTORS.md +331 -0
- gatewaysdk/environment/schema/skills/connector-schema-authoring/CONVENTIONS.md +100 -0
- gatewaysdk/environment/schema/skills/connector-schema-authoring/SKILL.md +313 -0
- gatewaysdk/environment/schema/skills/world-data-ingestion/ROWS.md +223 -0
- gatewaysdk/environment/schema/skills/world-data-ingestion/SKILL.md +293 -0
- gatewaysdk/environment/schema/skills/worlds-getting-started/SKILL.md +390 -0
- gatewaysdk/environment/schema/slack.py +757 -0
- gatewaysdk/environment/schema/snapshot.py +479 -0
- gatewaysdk/environment/schema/store.py +904 -0
- gatewaysdk/environment/schema/tasks.py +525 -0
- gatewaysdk/environment/schema/tools_world.py +322 -0
- gatewaysdk/environment/schema/validation.py +577 -0
- gatewaysdk/environment/schema/worker.py +78 -0
- gatewaysdk/environment/schema/workers.py +500 -0
- gatewaysdk/environment/schema/world_tests.py +336 -0
- gatewaysdk/environments/__init__.py +24 -0
- gatewaysdk/environments/client.py +486 -0
- gatewaysdk/environments/types.py +318 -0
- gatewaysdk/execution/__init__.py +15 -0
- gatewaysdk/execution/base.py +64 -0
- gatewaysdk/execution/client_server.py +443 -0
- gatewaysdk/execution/events.py +69 -0
- gatewaysdk/execution/inter_process.py +16 -0
- gatewaysdk/execution/shared_memory.py +282 -0
- gatewaysdk/experiments/__init__.py +90 -0
- gatewaysdk/experiments/assignment.py +177 -0
- gatewaysdk/experiments/client.py +877 -0
- gatewaysdk/experiments/exposure.py +222 -0
- gatewaysdk/experiments/types.py +81 -0
- gatewaysdk/importers/__init__.py +49 -0
- gatewaysdk/importers/_normalize.py +91 -0
- gatewaysdk/importers/builder.py +173 -0
- gatewaysdk/importers/client.py +95 -0
- gatewaysdk/importers/langsmith.py +196 -0
- gatewaysdk/importers/recipes.py +249 -0
- gatewaysdk/instrumentation/__init__.py +300 -0
- gatewaysdk/instrumentation/agentops.py +314 -0
- gatewaysdk/instrumentation/agentops_langchain.py +45 -0
- gatewaysdk/instrumentation/base.py +119 -0
- gatewaysdk/instrumentation/litellm.py +83 -0
- gatewaysdk/instrumentation/registry.py +273 -0
- gatewaysdk/instrumentation/vllm.py +81 -0
- gatewaysdk/instrumentation/weave.py +500 -0
- gatewaysdk/integrations/__init__.py +15 -0
- gatewaysdk/integrations/gateway/__init__.py +11 -0
- gatewaysdk/integrations/gateway/client.py +171 -0
- gatewaysdk/integrations/tool_access.py +549 -0
- gatewaysdk/litagent/__init__.py +11 -0
- gatewaysdk/litagent/decorator.py +536 -0
- gatewaysdk/litagent/litagent.py +252 -0
- gatewaysdk/llm_proxy.py +1742 -0
- gatewaysdk/logging.py +370 -0
- gatewaysdk/memory.py +278 -0
- gatewaysdk/personas.py +186 -0
- gatewaysdk/platform/__init__.py +17 -0
- gatewaysdk/platform/builder.py +221 -0
- gatewaysdk/platform/compatibility.py +67 -0
- gatewaysdk/platform/manifest.py +185 -0
- gatewaysdk/platform/orchestrator.py +901 -0
- gatewaysdk/platform/registry.py +122 -0
- gatewaysdk/platform/worker.py +864 -0
- gatewaysdk/replay/__init__.py +1032 -0
- gatewaysdk/replay/pytest.py +56 -0
- gatewaysdk/reward.py +7 -0
- gatewaysdk/run.py +1781 -0
- gatewaysdk/runner/__init__.py +11 -0
- gatewaysdk/runner/agent.py +878 -0
- gatewaysdk/runner/base.py +182 -0
- gatewaysdk/runner/legacy.py +309 -0
- gatewaysdk/security.py +700 -0
- gatewaysdk/semconv.py +170 -0
- gatewaysdk/server.py +399 -0
- gatewaysdk/sessions.py +282 -0
- gatewaysdk/store/__init__.py +45 -0
- gatewaysdk/store/base.py +908 -0
- gatewaysdk/store/client_server.py +2093 -0
- gatewaysdk/store/collection/__init__.py +30 -0
- gatewaysdk/store/collection/base.py +587 -0
- gatewaysdk/store/collection/memory.py +970 -0
- gatewaysdk/store/collection/mongo.py +1412 -0
- gatewaysdk/store/collection_based.py +1823 -0
- gatewaysdk/store/gateway.py +983 -0
- gatewaysdk/store/gateway_listener.py +465 -0
- gatewaysdk/store/listener.py +58 -0
- gatewaysdk/store/memory.py +396 -0
- gatewaysdk/store/mongo.py +165 -0
- gatewaysdk/store/redis_stream.py +517 -0
- gatewaysdk/store/sqlite.py +3 -0
- gatewaysdk/store/threading.py +370 -0
- gatewaysdk/store/utils.py +142 -0
- gatewaysdk/tracer/__init__.py +14 -0
- gatewaysdk/tracer/base.py +286 -0
- gatewaysdk/tracer/dummy.py +106 -0
- gatewaysdk/tracer/otel.py +559 -0
- gatewaysdk/tracing/__init__.py +110 -0
- gatewaysdk/tracing/api.py +808 -0
- gatewaysdk/tracing/attributes.py +9 -0
- gatewaysdk/tracing/context.py +272 -0
- gatewaysdk/tracing/exporters/__init__.py +10 -0
- gatewaysdk/tracing/exporters/gateway.py +228 -0
- gatewaysdk/tracing/identity.py +288 -0
- gatewaysdk/tracing/init.py +620 -0
- gatewaysdk/tracing/instrumentors/__init__.py +15 -0
- gatewaysdk/tracing/instrumentors/claude_agent_sdk.py +766 -0
- gatewaysdk/tracing/instrumentors/instrumentation_principles.md +294 -0
- gatewaysdk/tracing/instrumentors/registry.py +352 -0
- gatewaysdk/tracing/mapping.py +729 -0
- gatewaysdk/tracing/processors.py +393 -0
- gatewaysdk/tracing/push.py +617 -0
- gatewaysdk/tracing/push_models.py +247 -0
- gatewaysdk/tracing/semconv.py +294 -0
- gatewaysdk/tracing/span_builder.py +356 -0
- gatewaysdk/trainer/__init__.py +6 -0
- gatewaysdk/trainer/init_utils.py +263 -0
- gatewaysdk/trainer/legacy.py +359 -0
- gatewaysdk/trainer/registry.py +12 -0
- gatewaysdk/trainer/trainer.py +638 -0
- gatewaysdk/types/__init__.py +63 -0
- gatewaysdk/types/core.py +556 -0
- gatewaysdk/types/resources.py +204 -0
- gatewaysdk/types/tracer.py +515 -0
- gatewaysdk/types/tracing.py +162 -0
- gatewaysdk/users.py +251 -0
- gatewaysdk/utils/__init__.py +1 -0
- gatewaysdk/utils/id.py +18 -0
- gatewaysdk/utils/metrics.py +1025 -0
- gatewaysdk/utils/otel.py +550 -0
- gatewaysdk/utils/otlp.py +556 -0
- gatewaysdk/utils/redact.py +22 -0
- gatewaysdk/utils/server_launcher.py +1045 -0
- gatewaysdk/utils/system_snapshot.py +90 -0
- gatewaysdk/verl/__init__.py +8 -0
- gatewaysdk/verl/__main__.py +6 -0
- gatewaysdk/verl/async_server.py +46 -0
- gatewaysdk/verl/config.yaml +27 -0
- gatewaysdk/verl/daemon.py +1154 -0
- gatewaysdk/verl/dataset.py +44 -0
- gatewaysdk/verl/entrypoint.py +248 -0
- gatewaysdk/verl/trainer.py +549 -0
- gatewaysdk/world_browser.py +748 -0
- gatewaysdk/world_data.py +453 -0
- gatewaysdk/world_sessions.py +978 -0
- gatewaysdk/world_tasks.py +285 -0
- gatewaysdk/world_tools.py +115 -0
- gatewaysdk/worlds.py +639 -0
- gatewaysdk-0.3.2.dist-info/METADATA +236 -0
- gatewaysdk-0.3.2.dist-info/RECORD +376 -0
- gatewaysdk-0.3.2.dist-info/WHEEL +4 -0
- gatewaysdk-0.3.2.dist-info/entry_points.txt +5 -0
- gatewaysdk-0.3.2.dist-info/licenses/LICENSE +19 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"""Bundle inspection: kind detection, manifest parsing, handler loading.
|
|
2
|
+
|
|
3
|
+
Two structurally-detected environment kinds (spec §3.1 + addendum):
|
|
4
|
+
|
|
5
|
+
- **mock-tools** — ``gateway-env.toml`` at the bundle root, ``tools/*.py``
|
|
6
|
+
handlers, ``db/schema.sql`` (+ ``db/seed.sql``), ``verifiers/*.sql``.
|
|
7
|
+
- **harbor** — a tree of Harbor task dirs (``tasks/*/task.toml`` or
|
|
8
|
+
``*/task.toml``), mirroring ``benchmark_hub.harbor_adapter.detect_env_kind``.
|
|
9
|
+
|
|
10
|
+
Handler loading executes each ``tools/*.py`` with this SDK's
|
|
11
|
+
:mod:`gatewaysdk.environment.runtime` injected into ``sys.modules`` as
|
|
12
|
+
``gateway_env`` for the duration of the load — the module the generated
|
|
13
|
+
handlers import — and restores ``sys.modules`` afterwards.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import functools
|
|
19
|
+
import importlib.util
|
|
20
|
+
import inspect
|
|
21
|
+
import sys
|
|
22
|
+
import tomllib
|
|
23
|
+
import uuid
|
|
24
|
+
from dataclasses import dataclass, field
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
27
|
+
|
|
28
|
+
from . import runtime
|
|
29
|
+
from .errors import EnvironmentNotReady, WorldStartError
|
|
30
|
+
|
|
31
|
+
KIND_MOCK_TOOLS = "mock-tools"
|
|
32
|
+
KIND_API = "api"
|
|
33
|
+
KIND_HARBOR = "harbor"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def detect_kind(bundle_path: Path) -> str:
|
|
37
|
+
""""api" iff ``gateway-api-world.toml`` sits at the root; else "mock-tools"
|
|
38
|
+
iff ``gateway-env.toml`` does; else "harbor" iff the tree holds Harbor task
|
|
39
|
+
dirs (same glob heuristics as the runner's ``detect_env_kind``); else the
|
|
40
|
+
bundle is not runnable here."""
|
|
41
|
+
from .schema import bundle_mode
|
|
42
|
+
|
|
43
|
+
bundle_mode(bundle_path) # Orphan/invalid schema declarations never downgrade.
|
|
44
|
+
if (bundle_path / "gateway-api-world.toml").is_file():
|
|
45
|
+
return KIND_API
|
|
46
|
+
if (bundle_path / "gateway-env.toml").is_file():
|
|
47
|
+
return KIND_MOCK_TOOLS
|
|
48
|
+
if list(bundle_path.glob("tasks/*/task.toml")) or list(bundle_path.glob("*/task.toml")):
|
|
49
|
+
return KIND_HARBOR
|
|
50
|
+
raise EnvironmentNotReady(
|
|
51
|
+
f"{bundle_path} is neither a mock-tools bundle (no gateway-env.toml) nor "
|
|
52
|
+
"a harbor task tree (no */task.toml) — nothing gwenv can run locally."
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
# mock-tools: manifest + handlers
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class EnvManifest:
|
|
63
|
+
"""Parsed ``gateway-env.toml``."""
|
|
64
|
+
|
|
65
|
+
name: str
|
|
66
|
+
serve: List[str]
|
|
67
|
+
db_engine: str = "sqlite"
|
|
68
|
+
raw: Dict[str, Any] = field(default_factory=dict)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def parse_manifest(bundle_path: Path) -> EnvManifest:
|
|
72
|
+
manifest_path = bundle_path / "gateway-env.toml"
|
|
73
|
+
try:
|
|
74
|
+
data = tomllib.loads(manifest_path.read_text(encoding="utf-8"))
|
|
75
|
+
except (OSError, tomllib.TOMLDecodeError) as error:
|
|
76
|
+
raise EnvironmentNotReady(f"cannot parse {manifest_path}: {error}") from error
|
|
77
|
+
|
|
78
|
+
# Builder-generated manifests use an [environment] table; hand-written
|
|
79
|
+
# ones may use [env] or a top-level name.
|
|
80
|
+
env_table: Dict[str, Any] = {}
|
|
81
|
+
for key in ("environment", "env"):
|
|
82
|
+
if isinstance(data.get(key), dict):
|
|
83
|
+
env_table = data[key]
|
|
84
|
+
break
|
|
85
|
+
name = env_table.get("name") or data.get("name") or bundle_path.name
|
|
86
|
+
|
|
87
|
+
tools_table = data.get("tools") if isinstance(data.get("tools"), dict) else {}
|
|
88
|
+
serve = tools_table.get("serve") or []
|
|
89
|
+
if not isinstance(serve, list):
|
|
90
|
+
raise EnvironmentNotReady(f"{manifest_path}: [tools] serve must be a list of tool names")
|
|
91
|
+
|
|
92
|
+
db_value = data.get("db")
|
|
93
|
+
if isinstance(db_value, dict):
|
|
94
|
+
engine = db_value.get("engine") or env_table.get("db") or "sqlite"
|
|
95
|
+
elif isinstance(db_value, str):
|
|
96
|
+
engine = db_value
|
|
97
|
+
else:
|
|
98
|
+
engine = env_table.get("db") or "sqlite"
|
|
99
|
+
if engine != "sqlite":
|
|
100
|
+
raise WorldStartError(
|
|
101
|
+
f"db engine {engine!r} is not supported in local mode — P1 runs "
|
|
102
|
+
"sqlite in-process (Postgres arrives with hosted worlds, spec §4.9)."
|
|
103
|
+
)
|
|
104
|
+
return EnvManifest(name=str(name), serve=[str(s) for s in serve], db_engine=engine, raw=data)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _wrap_run_handler(fn: Callable[..., Any], tool_name: str) -> Callable[..., Any]:
|
|
108
|
+
"""Adapt the generated-bundle handler contract to the SDK's call shapes.
|
|
109
|
+
|
|
110
|
+
Builder-generated bundles expose ``run(input: dict, conn=None)`` in
|
|
111
|
+
``tools/<tool_name>.py`` — the whole tool input arrives as one dict, and
|
|
112
|
+
the runtime injects an open sqlite connection so writes land in the
|
|
113
|
+
active world (not the bundle's on-disk default db).
|
|
114
|
+
"""
|
|
115
|
+
takes_conn = "conn" in inspect.signature(fn).parameters
|
|
116
|
+
|
|
117
|
+
@functools.wraps(fn)
|
|
118
|
+
def handler(*args: Any, **kwargs: Any) -> Any:
|
|
119
|
+
tool_input = args[0] if args else (kwargs if runtime._active_store.get() is not None else (kwargs or None))
|
|
120
|
+
if takes_conn:
|
|
121
|
+
return fn(tool_input, conn=runtime._active_connection.get())
|
|
122
|
+
return fn(tool_input)
|
|
123
|
+
|
|
124
|
+
handler.__name__ = tool_name
|
|
125
|
+
return handler
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def load_handlers(bundle_path: Path, manifest: EnvManifest) -> Dict[str, Callable[..., Any]]:
|
|
129
|
+
"""Execute ``tools/*.py`` and return the tool-name → handler map.
|
|
130
|
+
|
|
131
|
+
Registration paths, in priority order:
|
|
132
|
+
1. ``@tool(name=...)`` / bare ``@tool`` decorations (captured via the
|
|
133
|
+
runtime's load-registry contextvar during module exec);
|
|
134
|
+
2. fallback discovery — a plain module-level ``def <tool_name>`` whose
|
|
135
|
+
name matches a served tool that no decorator claimed;
|
|
136
|
+
3. the generated-bundle contract — ``run(input, conn=None)`` in the
|
|
137
|
+
module whose FILE STEM is the served tool name (wrapped so the
|
|
138
|
+
active world's connection is injected).
|
|
139
|
+
|
|
140
|
+
``gateway_env`` is importable only inside this function's dynamic extent.
|
|
141
|
+
"""
|
|
142
|
+
from .schema import bundle_mode, check_bundle
|
|
143
|
+
|
|
144
|
+
contract = check_bundle(bundle_path) if bundle_mode(bundle_path) is not None else None
|
|
145
|
+
tools_dir = bundle_path / "tools"
|
|
146
|
+
registry: Dict[str, Callable[..., Any]] = {}
|
|
147
|
+
modules = []
|
|
148
|
+
modules_by_stem: Dict[str, Any] = {}
|
|
149
|
+
|
|
150
|
+
handler_files = sorted(tools_dir.glob("*.py")) if tools_dir.is_dir() else []
|
|
151
|
+
load_token = runtime._load_registry.set(registry)
|
|
152
|
+
strict_token = runtime._strict_registration.set(contract is not None)
|
|
153
|
+
saved = sys.modules.get("gateway_env")
|
|
154
|
+
sys.modules["gateway_env"] = runtime
|
|
155
|
+
# Generated bundles use intra-bundle imports ("from tools._db import …"),
|
|
156
|
+
# so the bundle root must be importable for the load's dynamic extent.
|
|
157
|
+
bundle_root = str(bundle_path)
|
|
158
|
+
path_added = bundle_root not in sys.path
|
|
159
|
+
if path_added:
|
|
160
|
+
sys.path.insert(0, bundle_root)
|
|
161
|
+
saved_pkgs = {
|
|
162
|
+
name: sys.modules.get(name)
|
|
163
|
+
for name in list(sys.modules)
|
|
164
|
+
if name == "tools" or name.startswith("tools.")
|
|
165
|
+
}
|
|
166
|
+
for name in saved_pkgs:
|
|
167
|
+
sys.modules.pop(name, None)
|
|
168
|
+
try:
|
|
169
|
+
for handler_file in handler_files:
|
|
170
|
+
if handler_file.name.startswith("_"):
|
|
171
|
+
continue
|
|
172
|
+
module_name = f"_gateway_env_bundle_{uuid.uuid4().hex[:8]}_{handler_file.stem}"
|
|
173
|
+
spec = importlib.util.spec_from_file_location(module_name, handler_file)
|
|
174
|
+
if spec is None or spec.loader is None: # pragma: no cover — importlib edge
|
|
175
|
+
raise WorldStartError(f"cannot load handler module {handler_file}")
|
|
176
|
+
module = importlib.util.module_from_spec(spec)
|
|
177
|
+
sys.modules[module_name] = module
|
|
178
|
+
try:
|
|
179
|
+
spec.loader.exec_module(module)
|
|
180
|
+
except Exception as error:
|
|
181
|
+
raise WorldStartError(
|
|
182
|
+
f"handler module {handler_file.name} failed to import: {error}"
|
|
183
|
+
) from error
|
|
184
|
+
modules.append(module)
|
|
185
|
+
modules_by_stem[handler_file.stem] = module
|
|
186
|
+
finally:
|
|
187
|
+
if saved is not None:
|
|
188
|
+
sys.modules["gateway_env"] = saved
|
|
189
|
+
else:
|
|
190
|
+
sys.modules.pop("gateway_env", None)
|
|
191
|
+
if path_added:
|
|
192
|
+
try:
|
|
193
|
+
sys.path.remove(bundle_root)
|
|
194
|
+
except ValueError:
|
|
195
|
+
pass
|
|
196
|
+
# Drop bundle-local "tools" modules and restore anything shadowed.
|
|
197
|
+
for name in [
|
|
198
|
+
n for n in sys.modules if n == "tools" or n.startswith("tools.")
|
|
199
|
+
]:
|
|
200
|
+
sys.modules.pop(name, None)
|
|
201
|
+
for name, module in saved_pkgs.items():
|
|
202
|
+
if module is not None:
|
|
203
|
+
sys.modules[name] = module
|
|
204
|
+
runtime._load_registry.reset(load_token)
|
|
205
|
+
runtime._strict_registration.reset(strict_token)
|
|
206
|
+
|
|
207
|
+
# Fallbacks for undecorated bundles.
|
|
208
|
+
for tool_name in manifest.serve:
|
|
209
|
+
if tool_name in registry:
|
|
210
|
+
continue
|
|
211
|
+
# (2) a plain module-level function named exactly like the tool.
|
|
212
|
+
for module in modules:
|
|
213
|
+
candidate = getattr(module, tool_name, None)
|
|
214
|
+
if callable(candidate) and getattr(candidate, "__module__", None) == module.__name__:
|
|
215
|
+
registry[tool_name] = candidate
|
|
216
|
+
break
|
|
217
|
+
else:
|
|
218
|
+
# (3) run(input, conn=None) in tools/<tool_name>.py.
|
|
219
|
+
stem_module = modules_by_stem.get(tool_name)
|
|
220
|
+
run_fn = getattr(stem_module, "run", None) if stem_module else None
|
|
221
|
+
if callable(run_fn):
|
|
222
|
+
registry[tool_name] = _wrap_run_handler(run_fn, tool_name)
|
|
223
|
+
if contract is not None and set(registry) != set(contract.tools):
|
|
224
|
+
from .schema import ContractError
|
|
225
|
+
|
|
226
|
+
raise ContractError("tool", "Authored handlers must exactly cover the declared tools")
|
|
227
|
+
return registry
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# ---------------------------------------------------------------------------
|
|
231
|
+
# harbor: task enumeration
|
|
232
|
+
# ---------------------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
@dataclass
|
|
236
|
+
class HarborTask:
|
|
237
|
+
"""One Harbor task dir (``task.toml`` + ``instruction.md`` + ``tests/``)."""
|
|
238
|
+
|
|
239
|
+
name: str
|
|
240
|
+
path: Path
|
|
241
|
+
instruction: Optional[str]
|
|
242
|
+
docker_image: Optional[str]
|
|
243
|
+
has_tests: bool
|
|
244
|
+
config: Dict[str, Any] = field(default_factory=dict)
|
|
245
|
+
|
|
246
|
+
@property
|
|
247
|
+
def prompt(self) -> Optional[str]:
|
|
248
|
+
"""Alias — the task's instruction is its prompt."""
|
|
249
|
+
return self.instruction
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def harbor_tasks_root(bundle_path: Path) -> Path:
|
|
253
|
+
"""The dir whose CHILDREN are task dirs: ``<src>/tasks`` if present, else
|
|
254
|
+
``<src>`` (mirrors ``harbor_adapter.tasks_root``)."""
|
|
255
|
+
tasks = bundle_path / "tasks"
|
|
256
|
+
return tasks if list(tasks.glob("*/task.toml")) else bundle_path
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def load_harbor_tasks(bundle_path: Path) -> List[HarborTask]:
|
|
260
|
+
tasks: List[HarborTask] = []
|
|
261
|
+
for task_toml in sorted(harbor_tasks_root(bundle_path).glob("*/task.toml")):
|
|
262
|
+
task_dir = task_toml.parent
|
|
263
|
+
try:
|
|
264
|
+
config = tomllib.loads(task_toml.read_text(encoding="utf-8"))
|
|
265
|
+
except (OSError, tomllib.TOMLDecodeError) as error:
|
|
266
|
+
raise EnvironmentNotReady(f"cannot parse {task_toml}: {error}") from error
|
|
267
|
+
environment = config.get("environment") if isinstance(config.get("environment"), dict) else {}
|
|
268
|
+
instruction_path = task_dir / "instruction.md"
|
|
269
|
+
tasks.append(
|
|
270
|
+
HarborTask(
|
|
271
|
+
name=task_dir.name,
|
|
272
|
+
path=task_dir,
|
|
273
|
+
instruction=(
|
|
274
|
+
instruction_path.read_text(encoding="utf-8") if instruction_path.is_file() else None
|
|
275
|
+
),
|
|
276
|
+
docker_image=environment.get("docker_image"),
|
|
277
|
+
has_tests=(task_dir / "tests").is_dir(),
|
|
278
|
+
config=config,
|
|
279
|
+
)
|
|
280
|
+
)
|
|
281
|
+
return tasks
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"""Harbor worlds — per-task docker sandboxes for harbor-kind environments.
|
|
2
|
+
|
|
3
|
+
A harbor env is a tree of task dirs (``task.toml`` + ``instruction.md`` +
|
|
4
|
+
``tests/``). ``env.task(name)`` boots one task's declared ``docker_image`` as
|
|
5
|
+
a local container, copies the task dir in, and hands back a
|
|
6
|
+
:class:`HarborWorld`: ``exec`` commands inside the sandbox, then ``verify()``
|
|
7
|
+
runs the task's own grader (``tests/test.sh``) and parses the harbor reward
|
|
8
|
+
contract (``/logs/verifier/reward.json`` scalar ``reward`` + named numeric
|
|
9
|
+
sub-rewards; ``reward.txt`` fallback; missing/unparseable → score ``None``,
|
|
10
|
+
NEVER 0 — mirrors ``benchmark_hub.harbor_adapter`` / the runner's harvest).
|
|
11
|
+
|
|
12
|
+
The docker layer is an injectable interface (:class:`DockerRuntime`) so tests
|
|
13
|
+
run offline against a fake; the real implementation shells out to the docker
|
|
14
|
+
CLI with timeouts on every call (never hangs).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import re
|
|
21
|
+
import shutil
|
|
22
|
+
import subprocess
|
|
23
|
+
import time
|
|
24
|
+
import uuid
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import TYPE_CHECKING, Dict, List, Optional, Sequence, Tuple, Union
|
|
28
|
+
|
|
29
|
+
from ._bundle import HarborTask
|
|
30
|
+
from ._world import VerifierResult
|
|
31
|
+
from .errors import VerifierError, WorldStartError
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
34
|
+
from .core import Environment
|
|
35
|
+
|
|
36
|
+
Command = Union[str, Sequence[str]]
|
|
37
|
+
|
|
38
|
+
REWARD_JSON_PATH = "/logs/verifier/reward.json"
|
|
39
|
+
REWARD_TXT_PATH = "/logs/verifier/reward.txt"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class ExecResult:
|
|
44
|
+
"""Outcome of one command inside the sandbox."""
|
|
45
|
+
|
|
46
|
+
exit_code: int
|
|
47
|
+
stdout: str
|
|
48
|
+
stderr: str
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def ok(self) -> bool:
|
|
52
|
+
return self.exit_code == 0
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class DockerRuntime:
|
|
56
|
+
"""The container operations a harbor world needs. Subclass (or duck-type)
|
|
57
|
+
to inject a fake for offline tests; :class:`CliDockerRuntime` is the real
|
|
58
|
+
thing."""
|
|
59
|
+
|
|
60
|
+
def available(self) -> Tuple[bool, str]:
|
|
61
|
+
"""(usable, reason-if-not)."""
|
|
62
|
+
raise NotImplementedError
|
|
63
|
+
|
|
64
|
+
def run_container(self, image: str, *, name: str) -> str:
|
|
65
|
+
"""Start a long-lived container from ``image``; return its id."""
|
|
66
|
+
raise NotImplementedError
|
|
67
|
+
|
|
68
|
+
def copy_in(self, container: str, source: Path, dest: str) -> None:
|
|
69
|
+
"""Copy a local file/dir into the container at ``dest``."""
|
|
70
|
+
raise NotImplementedError
|
|
71
|
+
|
|
72
|
+
def exec(self, container: str, cmd: Command, *, timeout: float) -> ExecResult:
|
|
73
|
+
"""Run a command in the container (str → via ``sh -c``)."""
|
|
74
|
+
raise NotImplementedError
|
|
75
|
+
|
|
76
|
+
def read_file(self, container: str, path: str) -> Optional[bytes]:
|
|
77
|
+
"""The file's bytes, or ``None`` when it does not exist."""
|
|
78
|
+
raise NotImplementedError
|
|
79
|
+
|
|
80
|
+
def remove(self, container: str) -> None:
|
|
81
|
+
"""Force-remove the container (best-effort)."""
|
|
82
|
+
raise NotImplementedError
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class CliDockerRuntime(DockerRuntime):
|
|
86
|
+
"""Shells out to the ``docker`` CLI. Every call has a timeout."""
|
|
87
|
+
|
|
88
|
+
def __init__(self, binary: str = "docker", *, pull_timeout: float = 900.0) -> None:
|
|
89
|
+
self._binary = binary
|
|
90
|
+
self._pull_timeout = pull_timeout
|
|
91
|
+
|
|
92
|
+
def _run(self, args: List[str], *, timeout: float, binary_output: bool = False) -> subprocess.CompletedProcess:
|
|
93
|
+
return subprocess.run(
|
|
94
|
+
[self._binary, *args],
|
|
95
|
+
capture_output=True,
|
|
96
|
+
text=not binary_output,
|
|
97
|
+
timeout=timeout,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def available(self) -> Tuple[bool, str]:
|
|
101
|
+
if shutil.which(self._binary) is None:
|
|
102
|
+
return False, f"'{self._binary}' not found on PATH"
|
|
103
|
+
try:
|
|
104
|
+
proc = self._run(["version", "--format", "{{.Server.Version}}"], timeout=15)
|
|
105
|
+
except subprocess.TimeoutExpired:
|
|
106
|
+
return False, "docker daemon did not answer within 15s"
|
|
107
|
+
if proc.returncode != 0:
|
|
108
|
+
return False, (proc.stderr or proc.stdout or "docker daemon unreachable").strip()[:300]
|
|
109
|
+
return True, ""
|
|
110
|
+
|
|
111
|
+
def run_container(self, image: str, *, name: str) -> str:
|
|
112
|
+
try:
|
|
113
|
+
proc = self._run(
|
|
114
|
+
["run", "-d", "--name", name, "--entrypoint", "sleep", image, "infinity"],
|
|
115
|
+
timeout=self._pull_timeout,
|
|
116
|
+
)
|
|
117
|
+
except subprocess.TimeoutExpired as error:
|
|
118
|
+
raise WorldStartError(
|
|
119
|
+
f"docker run {image} timed out after {self._pull_timeout}s (pull too slow?)"
|
|
120
|
+
) from error
|
|
121
|
+
if proc.returncode != 0:
|
|
122
|
+
raise WorldStartError(
|
|
123
|
+
f"docker run {image} failed: {(proc.stderr or proc.stdout).strip()[:500]}"
|
|
124
|
+
)
|
|
125
|
+
return proc.stdout.strip()
|
|
126
|
+
|
|
127
|
+
def copy_in(self, container: str, source: Path, dest: str) -> None:
|
|
128
|
+
proc = self._run(["cp", str(source), f"{container}:{dest}"], timeout=300)
|
|
129
|
+
if proc.returncode != 0:
|
|
130
|
+
raise WorldStartError(f"docker cp {source} failed: {proc.stderr.strip()[:500]}")
|
|
131
|
+
|
|
132
|
+
def exec(self, container: str, cmd: Command, *, timeout: float) -> ExecResult:
|
|
133
|
+
args = ["exec", container]
|
|
134
|
+
args += ["sh", "-c", cmd] if isinstance(cmd, str) else list(cmd)
|
|
135
|
+
try:
|
|
136
|
+
proc = self._run(args, timeout=timeout)
|
|
137
|
+
except subprocess.TimeoutExpired:
|
|
138
|
+
return ExecResult(exit_code=124, stdout="", stderr=f"timed out after {timeout}s")
|
|
139
|
+
return ExecResult(exit_code=proc.returncode, stdout=proc.stdout, stderr=proc.stderr)
|
|
140
|
+
|
|
141
|
+
def read_file(self, container: str, path: str) -> Optional[bytes]:
|
|
142
|
+
try:
|
|
143
|
+
proc = self._run(["exec", container, "cat", path], timeout=60, binary_output=True)
|
|
144
|
+
except subprocess.TimeoutExpired:
|
|
145
|
+
return None
|
|
146
|
+
return proc.stdout if proc.returncode == 0 else None
|
|
147
|
+
|
|
148
|
+
def remove(self, container: str) -> None:
|
|
149
|
+
try:
|
|
150
|
+
self._run(["rm", "-f", container], timeout=60)
|
|
151
|
+
except subprocess.TimeoutExpired: # pragma: no cover — best-effort cleanup
|
|
152
|
+
pass
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def parse_rewards(
|
|
156
|
+
reward_json: Optional[bytes], reward_txt: Optional[bytes]
|
|
157
|
+
) -> Tuple[Optional[float], Dict[str, float], str]:
|
|
158
|
+
"""(scalar score, named sub-rewards, note) from the harbor reward files.
|
|
159
|
+
|
|
160
|
+
``reward.json``: scalar = the ``reward`` key (or the first numeric value);
|
|
161
|
+
every OTHER numeric key is a named sub-reward. ``reward.txt``: bare float
|
|
162
|
+
fallback. Missing/unparseable → ``(None, {}, why)`` — never 0.
|
|
163
|
+
"""
|
|
164
|
+
if reward_json is not None:
|
|
165
|
+
try:
|
|
166
|
+
data = json.loads(reward_json.decode("utf-8"))
|
|
167
|
+
except (ValueError, UnicodeDecodeError):
|
|
168
|
+
data = None
|
|
169
|
+
if isinstance(data, (int, float)) and not isinstance(data, bool):
|
|
170
|
+
return float(data), {}, "scalar reward.json"
|
|
171
|
+
if isinstance(data, dict):
|
|
172
|
+
numeric = {
|
|
173
|
+
key: float(value)
|
|
174
|
+
for key, value in data.items()
|
|
175
|
+
if isinstance(value, (int, float)) and not isinstance(value, bool)
|
|
176
|
+
}
|
|
177
|
+
if "reward" in numeric:
|
|
178
|
+
scalar = numeric.pop("reward")
|
|
179
|
+
return scalar, numeric, "reward.json"
|
|
180
|
+
if numeric:
|
|
181
|
+
first_key = next(iter(numeric))
|
|
182
|
+
scalar = numeric.pop(first_key)
|
|
183
|
+
return scalar, numeric, f"reward.json (no 'reward' key — used '{first_key}')"
|
|
184
|
+
return None, {}, "reward.json held no numeric metrics"
|
|
185
|
+
note = "reward.json unparseable"
|
|
186
|
+
else:
|
|
187
|
+
note = "reward.json missing"
|
|
188
|
+
|
|
189
|
+
if reward_txt is not None:
|
|
190
|
+
try:
|
|
191
|
+
return float(reward_txt.decode("utf-8").strip()), {}, f"{note}; reward.txt fallback"
|
|
192
|
+
except (ValueError, UnicodeDecodeError):
|
|
193
|
+
return None, {}, f"{note}; reward.txt unparseable"
|
|
194
|
+
return None, {}, f"{note}; no reward.txt"
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class HarborWorld:
|
|
198
|
+
"""One task's live docker sandbox."""
|
|
199
|
+
|
|
200
|
+
def __init__(self, env: "Environment", task: HarborTask, docker: DockerRuntime) -> None:
|
|
201
|
+
self.env = env
|
|
202
|
+
self.task = task
|
|
203
|
+
self._docker = docker
|
|
204
|
+
self.id = uuid.uuid4().hex[:8]
|
|
205
|
+
|
|
206
|
+
usable, reason = docker.available()
|
|
207
|
+
if not usable:
|
|
208
|
+
raise WorldStartError(
|
|
209
|
+
f"docker unavailable: {reason} — install/start Docker, or inject "
|
|
210
|
+
"a DockerRuntime (gwenv.init_from_path(..., docker=...))."
|
|
211
|
+
)
|
|
212
|
+
if not task.docker_image:
|
|
213
|
+
raise WorldStartError(
|
|
214
|
+
f"harbor task '{task.name}' declares no [environment].docker_image "
|
|
215
|
+
"(Dockerfile-only tasks have nothing pullable) — build and tag the "
|
|
216
|
+
"image, set docker_image in task.toml, or run it via the harbor "
|
|
217
|
+
"engine / platform Batch instead."
|
|
218
|
+
)
|
|
219
|
+
container_name = re.sub(r"[^a-zA-Z0-9_.-]", "-", f"gwenv-{env.slug}-{task.name}")[:52] + f"-{self.id}"
|
|
220
|
+
try:
|
|
221
|
+
self.container = docker.run_container(task.docker_image, name=container_name)
|
|
222
|
+
except WorldStartError:
|
|
223
|
+
raise
|
|
224
|
+
except Exception as error: # pragma: no cover — runtime-impl specific
|
|
225
|
+
raise WorldStartError(f"failed to start {task.docker_image}: {error}") from error
|
|
226
|
+
try:
|
|
227
|
+
docker.copy_in(self.container, task.path, "/task")
|
|
228
|
+
except Exception:
|
|
229
|
+
self.close()
|
|
230
|
+
raise
|
|
231
|
+
|
|
232
|
+
def exec(self, cmd: Command, *, timeout: float = 120.0) -> ExecResult:
|
|
233
|
+
"""Run a command inside the task sandbox."""
|
|
234
|
+
started = time.perf_counter()
|
|
235
|
+
result = self._docker.exec(self.container, cmd, timeout=timeout)
|
|
236
|
+
summary = cmd if isinstance(cmd, str) else " ".join(cmd)
|
|
237
|
+
self.env._record_route(
|
|
238
|
+
f"{self.task.name}:exec[{summary[:60]}]",
|
|
239
|
+
"env",
|
|
240
|
+
(time.perf_counter() - started) * 1000.0,
|
|
241
|
+
self.id,
|
|
242
|
+
)
|
|
243
|
+
return result
|
|
244
|
+
|
|
245
|
+
def verify(self, name: Optional[str] = None) -> VerifierResult:
|
|
246
|
+
"""Run the task's own grader (``tests/test.sh``) and parse the reward
|
|
247
|
+
files. A missing/failed grader yields score ``None`` (never 0), with
|
|
248
|
+
the failure surfaced in ``detail``."""
|
|
249
|
+
if not self.task.has_tests:
|
|
250
|
+
raise VerifierError(f"harbor task '{self.task.name}' ships no tests/ dir — nothing to verify")
|
|
251
|
+
|
|
252
|
+
verifier_cfg = self.task.config.get("verifier") if isinstance(self.task.config.get("verifier"), dict) else {}
|
|
253
|
+
timeout = float(verifier_cfg.get("timeout_sec") or 600.0)
|
|
254
|
+
|
|
255
|
+
self._docker.copy_in(self.container, self.task.path / "tests", "/tests")
|
|
256
|
+
self._docker.exec(self.container, ["mkdir", "-p", "/logs/verifier"], timeout=30)
|
|
257
|
+
run = self._docker.exec(self.container, ["bash", "/tests/test.sh"], timeout=timeout)
|
|
258
|
+
|
|
259
|
+
score, rewards, note = parse_rewards(
|
|
260
|
+
self._docker.read_file(self.container, REWARD_JSON_PATH),
|
|
261
|
+
self._docker.read_file(self.container, REWARD_TXT_PATH),
|
|
262
|
+
)
|
|
263
|
+
detail = note
|
|
264
|
+
if not run.ok:
|
|
265
|
+
detail = f"test.sh exited {run.exit_code} ({(run.stderr or run.stdout).strip()[:300]}); {note}"
|
|
266
|
+
passed = None if score is None else score >= 1.0
|
|
267
|
+
return VerifierResult(
|
|
268
|
+
name=name or self.task.name, passed=passed, score=score, detail=detail, rewards=rewards
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
def state(self, path: str) -> Optional[bytes]:
|
|
272
|
+
"""Read a file out of the sandbox (harbor worlds have no SQL surface)."""
|
|
273
|
+
return self._docker.read_file(self.container, path)
|
|
274
|
+
|
|
275
|
+
def close(self) -> None:
|
|
276
|
+
self._docker.remove(self.container)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Shared SDK/hosted template materialization; schema mode never falls back."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
import sqlite3
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from tempfile import TemporaryDirectory
|
|
10
|
+
|
|
11
|
+
from ._bundle import load_handlers, parse_manifest
|
|
12
|
+
from .errors import WorldStartError
|
|
13
|
+
from .schema import SQLiteStore, bundle_mode, check_bundle
|
|
14
|
+
from .schema.validation import read_json
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def materialize_template(bundle: Path, destination: Path, *, require_sql: bool = False) -> None:
|
|
18
|
+
bundle, destination = Path(bundle), Path(destination)
|
|
19
|
+
if bundle_mode(bundle) is not None:
|
|
20
|
+
contract = check_bundle(bundle)
|
|
21
|
+
if destination.exists():
|
|
22
|
+
from .schema import ContractError
|
|
23
|
+
|
|
24
|
+
raise ContractError("state_exists", "Refusing to overwrite an existing template")
|
|
25
|
+
from .schema.snapshot import current_default, default_snapshot, pin_path
|
|
26
|
+
|
|
27
|
+
if current_default(bundle, contract) is not None:
|
|
28
|
+
# The compiled snapshot is this exact data through this exact contract,
|
|
29
|
+
# already audited: a copy is the materialized template.
|
|
30
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
31
|
+
shutil.copyfile(default_snapshot(bundle), destination)
|
|
32
|
+
return
|
|
33
|
+
if pin_path(bundle).is_file():
|
|
34
|
+
# A pinned tree's data is the hosted snapshot; its data files cannot
|
|
35
|
+
# stand in for it, so a missing or stale snapshot is refused, not rebuilt.
|
|
36
|
+
from .schema import ContractError
|
|
37
|
+
|
|
38
|
+
raise ContractError(
|
|
39
|
+
"snapshot_pinned",
|
|
40
|
+
"db/snapshot.json pins a hosted snapshot that is not in the tree: fetch the world "
|
|
41
|
+
"with its snapshot (load_world_to_sandbox, gateway worlds pull) instead of "
|
|
42
|
+
"materializing from its data files",
|
|
43
|
+
schema_path="/db/state.sqlite",
|
|
44
|
+
)
|
|
45
|
+
initial = read_json(bundle / "data/initial.json")
|
|
46
|
+
try:
|
|
47
|
+
with SQLiteStore.create(destination, contract) as store:
|
|
48
|
+
store.apply_batch(initial)
|
|
49
|
+
# Immutable refresh journals retain each mapping/source contribution.
|
|
50
|
+
# Coverage never prunes: every change is an explicit store mutation.
|
|
51
|
+
batches = bundle / "data/batches"
|
|
52
|
+
if batches.is_symlink():
|
|
53
|
+
from .schema import ContractError
|
|
54
|
+
|
|
55
|
+
raise ContractError("path", "Refresh batches cannot be symlinks")
|
|
56
|
+
if batches.exists():
|
|
57
|
+
for batch in sorted(batches.iterdir()):
|
|
58
|
+
if batch.is_symlink() or not batch.is_file() or batch.suffix != ".json":
|
|
59
|
+
from .schema import ContractError
|
|
60
|
+
|
|
61
|
+
raise ContractError("path", "Refresh journals must be regular JSON files")
|
|
62
|
+
store.apply_batch(read_json(batch))
|
|
63
|
+
except BaseException:
|
|
64
|
+
destination.unlink(missing_ok=True)
|
|
65
|
+
raise
|
|
66
|
+
return
|
|
67
|
+
schema, seed = bundle / "db/schema.sql", bundle / "db/seed.sql"
|
|
68
|
+
if require_sql and not schema.is_file():
|
|
69
|
+
raise WorldStartError("mock-tools bundle is missing db/schema.sql")
|
|
70
|
+
conn = sqlite3.connect(destination)
|
|
71
|
+
try:
|
|
72
|
+
if schema.is_file():
|
|
73
|
+
conn.executescript(schema.read_text(encoding="utf-8"))
|
|
74
|
+
if seed.is_file():
|
|
75
|
+
conn.executescript(seed.read_text(encoding="utf-8"))
|
|
76
|
+
conn.commit()
|
|
77
|
+
except sqlite3.Error as error:
|
|
78
|
+
raise WorldStartError("Failed to build seed database; check db/schema.sql and db/seed.sql") from error
|
|
79
|
+
finally:
|
|
80
|
+
conn.close()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def validate_schema_materialization(bundle: Path) -> None:
|
|
84
|
+
"""Validate compiled artifacts and initial state without retaining a database."""
|
|
85
|
+
bundle = Path(bundle)
|
|
86
|
+
write_bytecode = sys.dont_write_bytecode
|
|
87
|
+
sys.dont_write_bytecode = True
|
|
88
|
+
try:
|
|
89
|
+
load_handlers(bundle, parse_manifest(bundle))
|
|
90
|
+
finally:
|
|
91
|
+
sys.dont_write_bytecode = write_bytecode
|
|
92
|
+
with TemporaryDirectory(prefix="gateway-schema-validation-") as directory:
|
|
93
|
+
materialize_template(
|
|
94
|
+
bundle,
|
|
95
|
+
Path(directory) / "template.sqlite",
|
|
96
|
+
require_sql=True,
|
|
97
|
+
)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Guarded tar extraction.
|
|
2
|
+
|
|
3
|
+
Shared by the benchmark-container ``pull`` path (untrusted-by-provenance), the
|
|
4
|
+
verifiers runner's bundle extraction and the World Host's version cache. Rejects any
|
|
5
|
+
member that would write outside the destination directory — via ``..`` traversal, an absolute path, or a
|
|
6
|
+
sym/hard link — *before* extracting anything, so a malicious or malformed tarball
|
|
7
|
+
cannot escape ``dest_path``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import tarfile
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def safe_tar_extract(tar: tarfile.TarFile, dest_path: str | Path) -> None:
|
|
17
|
+
"""Extract ``tar`` into ``dest_path``, refusing any member that escapes it.
|
|
18
|
+
|
|
19
|
+
Raises ``ValueError`` on the first unsafe member (absolute path, ``..``
|
|
20
|
+
component, or a symlink/hardlink). All members are validated up front, so
|
|
21
|
+
nothing is written when any member is unsafe.
|
|
22
|
+
"""
|
|
23
|
+
dest = Path(dest_path).resolve()
|
|
24
|
+
for member in tar.getmembers():
|
|
25
|
+
if member.issym() or member.islnk():
|
|
26
|
+
raise ValueError(f"refusing link member in archive: {member.name}")
|
|
27
|
+
member_path = Path(member.name)
|
|
28
|
+
if member_path.is_absolute() or ".." in member_path.parts:
|
|
29
|
+
raise ValueError(f"unsafe path in archive: {member.name}")
|
|
30
|
+
resolved = (dest / member_path).resolve()
|
|
31
|
+
if not resolved.is_relative_to(dest):
|
|
32
|
+
raise ValueError(f"archive member escapes destination: {member.name}")
|
|
33
|
+
tar.extractall(dest)
|