7ac3f1352c
CI / benchmark-publish (pull_request) Has been skipped
CI / build (pull_request) Successful in 17s
CI / quality (pull_request) Successful in 20s
CI / lint (pull_request) Successful in 26s
CI / security (pull_request) Successful in 37s
CI / typecheck (pull_request) Successful in 57s
CI / unit_tests (pull_request) Successful in 3m14s
CI / integration_tests (pull_request) Successful in 3m33s
CI / docker (pull_request) Successful in 39s
CI / coverage (pull_request) Successful in 5m29s
CI / lint (push) Successful in 16s
CI / quality (push) Successful in 16s
CI / build (push) Successful in 16s
CI / security (push) Successful in 35s
CI / typecheck (push) Successful in 47s
CI / benchmark-regression (push) Has been skipped
CI / unit_tests (push) Successful in 3m1s
CI / integration_tests (push) Successful in 3m31s
CI / docker (push) Successful in 52s
CI / coverage (push) Successful in 5m47s
CI / benchmark-publish (push) Successful in 19m3s
CI / benchmark-regression (pull_request) Successful in 35m19s
Implement ContainerToolExecutor for delegating tool invocations to devcontainer environments with full I/O forwarding. Add PathMapper for bidirectional host/container path translation. Wire container routing into ToolRunner with graceful fallback when no executor is configured. Add container_metadata field to ToolInvocation for tracking execution context. New modules: - tool/container_executor.py: ContainerToolExecutor, ContainerConfig, ContainerMetadata, ContainerExecutionError, ContainerTimeoutError - tool/path_mapper.py: PathMapper with host_to_container/container_to_host Modified: - tool/runner.py: container execution routing via ExecutionEnvironment - domain/models/core/change.py: container_metadata on ToolInvocation - tool/__init__.py: new public exports Review fixes applied: - Add Alembic migration m6_004 for container_metadata_json column - Enforce _MAX_OUTPUT_BYTES (50 MiB) truncation in _run_command() - Fix path traversal in sync_results_to_host (Path.is_relative_to) - Allow spaces in _looks_like_path() for valid filesystem paths - Preserve negative exit codes from signal kills in metadata - Add default=str to json.dumps(invocation.arguments) safety net - Log warnings when path mapping recursion depth exceeded - Warn when devcontainer binary not found on PATH - Use default allow_nan for host-path JSON validation in runner.py (only container path requires RFC 7159 strict mode) - Reject URL-like patterns in _looks_like_path() to avoid false positives on API routes, protocol-relative URIs, and query strings - Add extract_container_metadata() static helper on ContainerToolExecutor as bridge for ToolInvocation wiring - Use raw_stdout bytes in sync_results_to_host to prevent binary file corruption from text-mode decode/re-encode - Apply posixpath.normpath() in workspace_folder validator and reject path components containing '..' - Check result.timed_out in sync_results_to_host and raise ContainerTimeoutError instead of always raising ContainerExecutionError - Detect overlapping host_root/container_root in PathMapper and raise ValueError to prevent corrupt bidirectional mappings - Wrap host-side I/O in sync_results_to_host with try/except OSError to produce ContainerExecutionError on write failure - Enforce int(timeout) in _build_exec_command to prevent shell injection via malicious objects with __str__ methods - Change ToolResult validator from 'not self.error' to 'self.error is None' so empty-string errors are accepted - Iterate required list directly in ToolRunner schema validation to detect fields listed in required but absent from properties Closes #515
154 lines
4.7 KiB
Python
154 lines
4.7 KiB
Python
"""ASV benchmarks for container tool execution overhead (#515).
|
|
|
|
Measures the performance cost of path mapping, input/output
|
|
translation, and container metadata creation — the operations
|
|
that happen on every container-routed tool call.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
|
|
from cleveragents.tool.container_executor import (
|
|
ContainerConfig,
|
|
ContainerMetadata,
|
|
ContainerToolExecutor,
|
|
)
|
|
from cleveragents.tool.path_mapper import PathMapper
|
|
|
|
|
|
class PathMapperBench:
|
|
"""Benchmark PathMapper host-to-container and container-to-host translations."""
|
|
|
|
def setup(self) -> None:
|
|
self.mapper = PathMapper(
|
|
host_root="/tmp/sandbox/project-abc",
|
|
container_root="/workspace",
|
|
)
|
|
self.host_path = "/tmp/sandbox/project-abc/src/cleveragents/tool/runner.py"
|
|
self.container_path = "/workspace/src/cleveragents/tool/runner.py"
|
|
self.external_path = (
|
|
"/usr/local/lib/python3.13/site-packages/pydantic/__init__.py"
|
|
)
|
|
|
|
def time_host_to_container(self) -> None:
|
|
self.mapper.host_to_container(self.host_path)
|
|
|
|
def time_container_to_host(self) -> None:
|
|
self.mapper.container_to_host(self.container_path)
|
|
|
|
def time_external_path_passthrough(self) -> None:
|
|
self.mapper.host_to_container(self.external_path)
|
|
|
|
def time_is_host_path_check(self) -> None:
|
|
self.mapper.is_host_path(self.host_path)
|
|
|
|
def time_is_container_path_check(self) -> None:
|
|
self.mapper.is_container_path(self.container_path)
|
|
|
|
|
|
class InputPathMappingBench:
|
|
"""Benchmark input path mapping with various nesting depths."""
|
|
|
|
def setup(self) -> None:
|
|
config = ContainerConfig(
|
|
workspace_folder="/workspace",
|
|
host_sandbox_path="/tmp/sandbox",
|
|
container_id="bench-ctr",
|
|
)
|
|
self.executor = ContainerToolExecutor(config)
|
|
self.flat_inputs = {
|
|
"path": "/tmp/sandbox/src/main.py",
|
|
"output_dir": "/tmp/sandbox/build",
|
|
"name": "my-tool",
|
|
"count": 42,
|
|
}
|
|
self.nested_inputs = {
|
|
"paths": {
|
|
"source": "/tmp/sandbox/src/main.py",
|
|
"config": "/tmp/sandbox/.config/settings.json",
|
|
"nested": {
|
|
"deep": "/tmp/sandbox/deep/path.txt",
|
|
},
|
|
},
|
|
"list_paths": [
|
|
"/tmp/sandbox/a.py",
|
|
"/tmp/sandbox/b.py",
|
|
"/tmp/sandbox/c.py",
|
|
],
|
|
}
|
|
|
|
def time_flat_input_mapping(self) -> None:
|
|
self.executor._map_input_paths(self.flat_inputs)
|
|
|
|
def time_nested_input_mapping(self) -> None:
|
|
self.executor._map_input_paths(self.nested_inputs)
|
|
|
|
|
|
class OutputPathMappingBench:
|
|
"""Benchmark output path mapping."""
|
|
|
|
def setup(self) -> None:
|
|
config = ContainerConfig(
|
|
workspace_folder="/workspace",
|
|
host_sandbox_path="/tmp/sandbox",
|
|
container_id="bench-ctr",
|
|
)
|
|
self.executor = ContainerToolExecutor(config)
|
|
self.output = {
|
|
"created_file": "/workspace/build/output.txt",
|
|
"log_file": "/workspace/logs/run.log",
|
|
"status": "success",
|
|
"count": 10,
|
|
}
|
|
|
|
def time_output_mapping(self) -> None:
|
|
self.executor._map_output_paths(self.output)
|
|
|
|
|
|
class ContainerMetadataBench:
|
|
"""Benchmark container metadata creation and serialisation."""
|
|
|
|
def setup(self) -> None:
|
|
self.metadata = ContainerMetadata(
|
|
container_id="abc123def456",
|
|
image="ghcr.io/org/devimage:latest",
|
|
workspace_folder="/workspace",
|
|
exec_time_ms=1500.0,
|
|
exit_code=0,
|
|
timed_out=False,
|
|
)
|
|
|
|
def time_metadata_creation(self) -> None:
|
|
ContainerMetadata(
|
|
container_id="abc123def456",
|
|
image="ghcr.io/org/devimage:latest",
|
|
workspace_folder="/workspace",
|
|
exec_time_ms=1500.0,
|
|
exit_code=0,
|
|
timed_out=False,
|
|
)
|
|
|
|
def time_metadata_to_dict(self) -> None:
|
|
self.metadata.model_dump()
|
|
|
|
|
|
class OutputParsingBench:
|
|
"""Benchmark output parsing (JSON vs plain text)."""
|
|
|
|
def setup(self) -> None:
|
|
self.json_output = json.dumps(
|
|
{"files": [f"file_{i}.py" for i in range(50)], "errors": 0}
|
|
)
|
|
self.plain_output = "Build completed successfully\n" * 20
|
|
self.empty_output = ""
|
|
|
|
def time_parse_json_output(self) -> None:
|
|
ContainerToolExecutor._parse_output(self.json_output)
|
|
|
|
def time_parse_plain_output(self) -> None:
|
|
ContainerToolExecutor._parse_output(self.plain_output)
|
|
|
|
def time_parse_empty_output(self) -> None:
|
|
ContainerToolExecutor._parse_output(self.empty_output)
|