coder-eval 0.8.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.
- coder_eval/.gitattributes +2 -0
- coder_eval/__init__.py +3 -0
- coder_eval/agent.py +302 -0
- coder_eval/agents/__init__.py +41 -0
- coder_eval/agents/_logging.py +89 -0
- coder_eval/agents/antigravity_agent.py +811 -0
- coder_eval/agents/claude_code_agent.py +1701 -0
- coder_eval/agents/codex_agent.py +2055 -0
- coder_eval/agents/noop_agent.py +100 -0
- coder_eval/agents/registry.py +163 -0
- coder_eval/agents/watchdog.py +116 -0
- coder_eval/analysis.py +88 -0
- coder_eval/cli/__init__.py +87 -0
- coder_eval/cli/aggregate_command.py +153 -0
- coder_eval/cli/console.py +7 -0
- coder_eval/cli/evaluate_command.py +164 -0
- coder_eval/cli/plan_command.py +152 -0
- coder_eval/cli/report_command.py +121 -0
- coder_eval/cli/run_command.py +686 -0
- coder_eval/cli/run_helpers.py +137 -0
- coder_eval/cli/run_task_internal_command.py +210 -0
- coder_eval/cli/utils.py +37 -0
- coder_eval/config.py +212 -0
- coder_eval/criteria/__init__.py +163 -0
- coder_eval/criteria/_classification_aggregate.py +106 -0
- coder_eval/criteria/agent_judge.py +425 -0
- coder_eval/criteria/base.py +248 -0
- coder_eval/criteria/classification_match.py +107 -0
- coder_eval/criteria/command_executed.py +181 -0
- coder_eval/criteria/commands_efficiency.py +73 -0
- coder_eval/criteria/file_check.py +119 -0
- coder_eval/criteria/file_contains.py +85 -0
- coder_eval/criteria/file_exists.py +45 -0
- coder_eval/criteria/file_matches_regex.py +86 -0
- coder_eval/criteria/json_check.py +184 -0
- coder_eval/criteria/llm_judge.py +260 -0
- coder_eval/criteria/reference_comparison.py +130 -0
- coder_eval/criteria/run_command.py +220 -0
- coder_eval/criteria/skill_triggered.py +111 -0
- coder_eval/criteria/uipath_eval.py +223 -0
- coder_eval/errors/__init__.py +26 -0
- coder_eval/errors/agent.py +26 -0
- coder_eval/errors/budget.py +28 -0
- coder_eval/errors/categories.py +205 -0
- coder_eval/errors/categorization.py +240 -0
- coder_eval/errors/executor.py +124 -0
- coder_eval/errors/judge.py +12 -0
- coder_eval/errors/retry.py +211 -0
- coder_eval/errors/timeout.py +63 -0
- coder_eval/evaluation/__init__.py +10 -0
- coder_eval/evaluation/checker.py +260 -0
- coder_eval/evaluation/judge_anthropic.py +55 -0
- coder_eval/evaluation/judge_bedrock.py +114 -0
- coder_eval/evaluation/judge_context.py +497 -0
- coder_eval/evaluation/judge_models.py +53 -0
- coder_eval/evaluation/judge_persistence.py +291 -0
- coder_eval/evaluation/judge_usage.py +50 -0
- coder_eval/evaluation/sub_agent.py +231 -0
- coder_eval/evaluation/summaries.py +48 -0
- coder_eval/evaluation/verdict_tool.py +213 -0
- coder_eval/formatting.py +191 -0
- coder_eval/isolation/__init__.py +15 -0
- coder_eval/isolation/docker_runner.py +1253 -0
- coder_eval/logging_config.py +399 -0
- coder_eval/models/__init__.py +337 -0
- coder_eval/models/agent_config.py +373 -0
- coder_eval/models/container_paths.py +26 -0
- coder_eval/models/criteria.py +942 -0
- coder_eval/models/enums.py +121 -0
- coder_eval/models/experiment.py +314 -0
- coder_eval/models/judge.py +90 -0
- coder_eval/models/judge_defaults.py +11 -0
- coder_eval/models/limits.py +89 -0
- coder_eval/models/merge_strategy.py +132 -0
- coder_eval/models/mutations.py +95 -0
- coder_eval/models/results.py +787 -0
- coder_eval/models/routing.py +160 -0
- coder_eval/models/sandbox.py +371 -0
- coder_eval/models/tasks.py +631 -0
- coder_eval/models/telemetry.py +454 -0
- coder_eval/models/templates.py +108 -0
- coder_eval/orchestration/__init__.py +13 -0
- coder_eval/orchestration/batch.py +690 -0
- coder_eval/orchestration/config.py +111 -0
- coder_eval/orchestration/config_merge.py +431 -0
- coder_eval/orchestration/evaluation.py +94 -0
- coder_eval/orchestration/experiment.py +837 -0
- coder_eval/orchestration/overrides.py +206 -0
- coder_eval/orchestration/task_loader.py +437 -0
- coder_eval/orchestrator.py +2078 -0
- coder_eval/path_utils.py +79 -0
- coder_eval/plugins.py +81 -0
- coder_eval/pricing.py +169 -0
- coder_eval/py.typed +0 -0
- coder_eval/reports.py +1066 -0
- coder_eval/reports_experiment.py +761 -0
- coder_eval/reports_html.py +1659 -0
- coder_eval/reports_stats.py +250 -0
- coder_eval/resources/__init__.py +137 -0
- coder_eval/resources/default_experiment.yaml +85 -0
- coder_eval/resources/default_ignore_patterns.yaml +60 -0
- coder_eval/resources/tags.yaml +55 -0
- coder_eval/sandbox.py +1127 -0
- coder_eval/scoring/__init__.py +11 -0
- coder_eval/scoring/ast_similarity.py +38 -0
- coder_eval/scoring/complexity.py +115 -0
- coder_eval/scoring/quality.py +154 -0
- coder_eval/scoring/signature_similarity.py +57 -0
- coder_eval/scoring/similarity.py +103 -0
- coder_eval/scoring/token_similarity.py +44 -0
- coder_eval/simulation/__init__.py +27 -0
- coder_eval/simulation/termination.py +88 -0
- coder_eval/simulation/user_simulator.py +324 -0
- coder_eval/streaming/__init__.py +44 -0
- coder_eval/streaming/callbacks.py +57 -0
- coder_eval/streaming/collector.py +193 -0
- coder_eval/streaming/events.py +198 -0
- coder_eval/streaming/renderers.py +248 -0
- coder_eval/streaming/wire.py +115 -0
- coder_eval/telemetry.py +407 -0
- coder_eval/utils.py +517 -0
- coder_eval-0.8.2.dist-info/METADATA +242 -0
- coder_eval-0.8.2.dist-info/RECORD +127 -0
- coder_eval-0.8.2.dist-info/WHEEL +4 -0
- coder_eval-0.8.2.dist-info/entry_points.txt +5 -0
- coder_eval-0.8.2.dist-info/licenses/LICENSE +201 -0
- coder_eval-0.8.2.dist-info/licenses/NOTICE +18 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""API routing configuration for the Claude Code agent SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import TYPE_CHECKING, Literal
|
|
7
|
+
|
|
8
|
+
from coder_eval.models.enums import ApiBackend
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from coder_eval.config import Settings
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Resolved-at-startup transport for the `llm_judge` criterion under DirectRoute.
|
|
16
|
+
# - "anthropic": call api.anthropic.com via the Anthropic SDK (needs ANTHROPIC_API_KEY).
|
|
17
|
+
# - None: no ANTHROPIC_API_KEY; any enabled `llm_judge` under DirectRoute fails at
|
|
18
|
+
# dispatch. The Bedrock backend routes the judge through the run's own backend
|
|
19
|
+
# and never reaches this transport selection.
|
|
20
|
+
JudgeTransport = Literal["anthropic"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Bedrock cross-region inference profile prefixes.
|
|
24
|
+
_BEDROCK_KNOWN_PREFIXES: tuple[str, ...] = ("eu.", "us.", "apac.", "global.")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def to_bedrock_inference_profile(model: str | None, region: str | None) -> str | None:
|
|
28
|
+
"""Qualify a Claude alias into a Bedrock cross-region inference-profile id.
|
|
29
|
+
|
|
30
|
+
Two transforms are applied in order:
|
|
31
|
+
|
|
32
|
+
1. Vendor qualifier — a bare alias like ``claude-sonnet-4-6`` gets
|
|
33
|
+
``anthropic.`` prepended (skipped if it's already qualified or carries a
|
|
34
|
+
region prefix).
|
|
35
|
+
2. Region qualifier — the AWS region's inference-profile prefix
|
|
36
|
+
(``eu.``/``us.``/``apac.``) is prepended so the same input works across
|
|
37
|
+
regions. Ids that already carry a known prefix pass through unchanged so
|
|
38
|
+
a user can pin a specific profile (e.g. ``global.anthropic.…``).
|
|
39
|
+
|
|
40
|
+
Examples (region=eu-north-1):
|
|
41
|
+
``claude-sonnet-4-6`` → ``eu.anthropic.claude-sonnet-4-6``
|
|
42
|
+
``anthropic.claude-sonnet-4-6`` → ``eu.anthropic.claude-sonnet-4-6``
|
|
43
|
+
``us.anthropic.claude-sonnet-4-6`` → ``us.anthropic.claude-sonnet-4-6``
|
|
44
|
+
"""
|
|
45
|
+
if not model or not region:
|
|
46
|
+
return model
|
|
47
|
+
model = model.strip()
|
|
48
|
+
if not model:
|
|
49
|
+
return None
|
|
50
|
+
# 1. Vendor qualifier.
|
|
51
|
+
if (
|
|
52
|
+
not model.startswith(_BEDROCK_KNOWN_PREFIXES)
|
|
53
|
+
and not model.startswith("anthropic.")
|
|
54
|
+
and model.startswith("claude-")
|
|
55
|
+
):
|
|
56
|
+
model = f"anthropic.{model}"
|
|
57
|
+
# 2. Region qualifier.
|
|
58
|
+
if model.startswith(_BEDROCK_KNOWN_PREFIXES):
|
|
59
|
+
return model
|
|
60
|
+
region_lower = region.lower()
|
|
61
|
+
if region_lower.startswith("eu-"):
|
|
62
|
+
return f"eu.{model}"
|
|
63
|
+
if region_lower.startswith("us-"):
|
|
64
|
+
return f"us.{model}"
|
|
65
|
+
if region_lower.startswith("ap-"):
|
|
66
|
+
return f"apac.{model}"
|
|
67
|
+
return model
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen=True)
|
|
71
|
+
class DirectRoute:
|
|
72
|
+
"""Route directly to Anthropic API for the agent.
|
|
73
|
+
|
|
74
|
+
The agent inherits parent-env auth and lets the Claude Agent SDK pick its
|
|
75
|
+
credential (API key / OAuth token / cached `claude login`). The
|
|
76
|
+
``judge_transport`` field separately controls which transport the
|
|
77
|
+
``llm_judge`` criterion uses, since the bare ``anthropic`` SDK can only
|
|
78
|
+
authenticate via ``ANTHROPIC_API_KEY``:
|
|
79
|
+
|
|
80
|
+
- ``"anthropic"``: judge calls api.anthropic.com (requires ANTHROPIC_API_KEY).
|
|
81
|
+
- ``None``: ANTHROPIC_API_KEY is absent; ``llm_judge`` fails fast at dispatch
|
|
82
|
+
with a clear error. Non-judge runs are unaffected.
|
|
83
|
+
|
|
84
|
+
Resolution happens once in ``resolve_route`` so the choice is deterministic
|
|
85
|
+
across criteria and recorded in ``EvaluationResult.environment_info``.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
judge_transport: JudgeTransport | None = "anthropic"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass(frozen=True)
|
|
92
|
+
class BedrockRoute:
|
|
93
|
+
"""Route through AWS Bedrock with bearer token authentication."""
|
|
94
|
+
|
|
95
|
+
bearer_token: str
|
|
96
|
+
region: str
|
|
97
|
+
model: str | None = None # Cross-region model ID, e.g. "eu.anthropic.claude-sonnet-4-6"
|
|
98
|
+
small_model: str | None = None # Cross-region small model ID
|
|
99
|
+
# FIXME(SDK#24168): Claude Code SDK injects x-anthropic-billing-header which
|
|
100
|
+
# Bedrock rejects as a reserved keyword (HTTP 400). Set to False once SDK fixes this.
|
|
101
|
+
disable_attribution_header: bool = True
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
ApiRoute = DirectRoute | BedrockRoute
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# Stable string names for environment_info recording (decoupled from class names)
|
|
108
|
+
ROUTE_NAMES: dict[type, str] = {
|
|
109
|
+
DirectRoute: "anthropic_direct",
|
|
110
|
+
BedrockRoute: "aws_bedrock",
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def resolve_route(settings: Settings) -> ApiRoute:
|
|
115
|
+
"""Resolve an ``ApiRoute`` from static settings.
|
|
116
|
+
|
|
117
|
+
Handles the two supported backends (``DIRECT`` and ``BEDROCK``), whose
|
|
118
|
+
route is fully determined by ``Settings``.
|
|
119
|
+
|
|
120
|
+
Called after ``validate_api_keys()`` has verified credentials. Uses
|
|
121
|
+
``assert`` for type narrowing (not ``ValueError``) since the Bedrock
|
|
122
|
+
credential checks are an internal contract.
|
|
123
|
+
"""
|
|
124
|
+
match settings.api_backend:
|
|
125
|
+
case ApiBackend.BEDROCK:
|
|
126
|
+
assert settings.aws_bearer_token_bedrock is not None, "Bedrock requires aws_bearer_token_bedrock"
|
|
127
|
+
assert settings.aws_region is not None, "Bedrock requires aws_region"
|
|
128
|
+
# BEDROCK_MODEL is the only route-level model source. CLI --model /
|
|
129
|
+
# -D agent.model and task-YAML agent.model are resolved later in the
|
|
130
|
+
# agent layer (via _resolve_effective_model), which also handles
|
|
131
|
+
# the anthropic.* + region prefix qualification on bare aliases.
|
|
132
|
+
# Fall back to the main model when no small/fast model is configured.
|
|
133
|
+
# Claude Code routes WebFetch's page-summarization (and other "small,
|
|
134
|
+
# fast" steps) through ANTHROPIC_SMALL_FAST_MODEL; on Bedrock that env
|
|
135
|
+
# var is only exported when small_model is set (see
|
|
136
|
+
# ClaudeCodeAgent._build_sdk_env). Leaving it unset made every
|
|
137
|
+
# WebFetch fail with "model issues" under the Bedrock backend. The main
|
|
138
|
+
# model is always a valid fallback, so default to it.
|
|
139
|
+
small_model = settings.bedrock_small_model or settings.bedrock_model
|
|
140
|
+
return BedrockRoute(
|
|
141
|
+
bearer_token=settings.aws_bearer_token_bedrock,
|
|
142
|
+
region=settings.aws_region,
|
|
143
|
+
model=to_bedrock_inference_profile(settings.bedrock_model, settings.aws_region),
|
|
144
|
+
small_model=to_bedrock_inference_profile(small_model, settings.aws_region),
|
|
145
|
+
)
|
|
146
|
+
case ApiBackend.DIRECT:
|
|
147
|
+
return DirectRoute(judge_transport=_resolve_direct_judge_transport(settings))
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _resolve_direct_judge_transport(settings: Settings) -> JudgeTransport | None:
|
|
151
|
+
"""Pick the judge transport for ``DirectRoute``.
|
|
152
|
+
|
|
153
|
+
``"anthropic"`` when ``ANTHROPIC_API_KEY`` is set (the judge calls
|
|
154
|
+
api.anthropic.com); ``None`` otherwise — the run still starts, but any
|
|
155
|
+
enabled ``llm_judge`` criterion fails at dispatch with a clear error.
|
|
156
|
+
The Bedrock backend never reaches this path: its judge routes through the
|
|
157
|
+
same backend as the run. The choice is made once at startup so it is
|
|
158
|
+
deterministic across criteria and recorded in ``environment_info``.
|
|
159
|
+
"""
|
|
160
|
+
return "anthropic" if settings.anthropic_api_key else None
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
"""Sandbox configuration models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import warnings
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
from pydantic import AliasChoices, BaseModel, ConfigDict, Field, field_validator, model_validator
|
|
9
|
+
|
|
10
|
+
from coder_eval.models.container_paths import CONTAINER_WORK_DIR, RESERVED_CONTAINER_DIRS
|
|
11
|
+
from coder_eval.models.merge_strategy import MergeField
|
|
12
|
+
from coder_eval.models.templates import TemplateSource
|
|
13
|
+
from coder_eval.resources import normalize_ignore_pattern_entry
|
|
14
|
+
from coder_eval.utils import get_default_docker_image_tag
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ResourceLimits(BaseModel):
|
|
18
|
+
"""Resource limits for sandbox execution.
|
|
19
|
+
|
|
20
|
+
Under ``driver: tempdir`` only ``timeout`` is actively enforced (via
|
|
21
|
+
``subprocess.run(timeout=...)``); other fields are accepted but not
|
|
22
|
+
enforced -- the agent can consume arbitrary host memory/CPU/PIDs/disk.
|
|
23
|
+
|
|
24
|
+
Under ``driver: docker`` ``max_memory_mb``, ``max_cpus``, and
|
|
25
|
+
``max_pids`` translate to ``--memory``, ``--cpus``, and ``--pids-limit``
|
|
26
|
+
respectively. ``max_disk_mb`` remains reserved (no portable docker knob).
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
model_config = ConfigDict(extra="forbid")
|
|
30
|
+
|
|
31
|
+
timeout: int = Field(default=300, description="Maximum execution time in seconds")
|
|
32
|
+
max_memory_mb: int | None = Field(
|
|
33
|
+
default=None,
|
|
34
|
+
description="Maximum memory in MB. Mapped to `docker run --memory` under driver:docker; reserved otherwise.",
|
|
35
|
+
)
|
|
36
|
+
max_cpus: float | None = Field(
|
|
37
|
+
default=None,
|
|
38
|
+
gt=0,
|
|
39
|
+
description="Max CPU shares (fractional). Mapped to `docker --cpus` under driver:docker; reserved otherwise.",
|
|
40
|
+
)
|
|
41
|
+
max_pids: int | None = Field(
|
|
42
|
+
default=None,
|
|
43
|
+
gt=0,
|
|
44
|
+
description="Max PID count. Mapped to `docker run --pids-limit` under driver:docker; reserved otherwise.",
|
|
45
|
+
)
|
|
46
|
+
max_disk_mb: int | None = Field(
|
|
47
|
+
default=None,
|
|
48
|
+
description="Maximum disk usage in MB (reserved -- no portable docker knob).",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class PythonEnvConfig(BaseModel):
|
|
53
|
+
"""Configuration for the Python virtual environment in the sandbox."""
|
|
54
|
+
|
|
55
|
+
model_config = ConfigDict(extra="forbid")
|
|
56
|
+
|
|
57
|
+
env_packages: list[str] = MergeField(strategy="replace", default_factory=list, description="Packages to install")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class NodeEnvConfig(BaseModel):
|
|
61
|
+
"""Configuration for Node.js environment in the sandbox."""
|
|
62
|
+
|
|
63
|
+
model_config = ConfigDict(extra="forbid")
|
|
64
|
+
|
|
65
|
+
env_packages: list[str] = MergeField(
|
|
66
|
+
strategy="replace", default_factory=list, description="npm packages to install (e.g., '@uipath/cli@0.1.5')"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def validate_template_sources_list(sources: list[TemplateSource]) -> None:
|
|
71
|
+
"""Validate a list of template sources for correctness.
|
|
72
|
+
|
|
73
|
+
Checks:
|
|
74
|
+
- At most one RepoSource
|
|
75
|
+
- RepoSource must be first (git clone requires empty directory)
|
|
76
|
+
- Warns if more than 10 sources
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
sources: List of template sources to validate.
|
|
80
|
+
|
|
81
|
+
Raises:
|
|
82
|
+
ValueError: If validation fails.
|
|
83
|
+
"""
|
|
84
|
+
from coder_eval.models.templates import RepoSource
|
|
85
|
+
|
|
86
|
+
repo_sources = [src for src in sources if isinstance(src, RepoSource)]
|
|
87
|
+
if len(repo_sources) > 1:
|
|
88
|
+
raise ValueError("Only one RepoSource is allowed in template_sources.")
|
|
89
|
+
|
|
90
|
+
if len(repo_sources) == 1 and not isinstance(sources[0], RepoSource):
|
|
91
|
+
raise ValueError(
|
|
92
|
+
"RepoSource must be the first element in template_sources (git clone requires an empty directory)."
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
if len(sources) > 10:
|
|
96
|
+
warnings.warn(
|
|
97
|
+
f"Many template sources ({len(sources)}) - this may be a misconfiguration",
|
|
98
|
+
UserWarning,
|
|
99
|
+
stacklevel=2,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class DockerBuildConfig(BaseModel):
|
|
104
|
+
"""``docker build`` customization for a ``dockerfile_path`` task image.
|
|
105
|
+
|
|
106
|
+
Only consulted when ``DockerDriverConfig.dockerfile_path`` is set. BuildKit
|
|
107
|
+
(required for ``secrets``) is inherited from the invoking environment by
|
|
108
|
+
default; set ``buildkit`` to force it on or off.
|
|
109
|
+
|
|
110
|
+
SECURITY: these fields are task-author-controlled and flow straight into the
|
|
111
|
+
``docker build`` argv. Task YAMLs are trusted infra; treat ``extra_args``
|
|
112
|
+
like any other shell-adjacent config.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
model_config = ConfigDict(extra="forbid")
|
|
116
|
+
|
|
117
|
+
buildkit: bool | None = Field(
|
|
118
|
+
default=None,
|
|
119
|
+
description=(
|
|
120
|
+
"Controls the DOCKER_BUILDKIT env var for `docker build`. None (default) inherits the "
|
|
121
|
+
"invoker's environment -- export DOCKER_BUILDKIT before running coder-eval to control it. "
|
|
122
|
+
"true forces it on; false forces it off. BuildKit is REQUIRED for `secrets`: set this true "
|
|
123
|
+
"(or export DOCKER_BUILDKIT=1) when using build secrets, else the build fails."
|
|
124
|
+
),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
args: dict[str, str] = Field(
|
|
128
|
+
default_factory=dict,
|
|
129
|
+
description=(
|
|
130
|
+
"Build-time variables -> `--build-arg KEY=VALUE`. Values are environment-expanded "
|
|
131
|
+
"($VAR / ${VAR}) against the host env, so you can forward host values "
|
|
132
|
+
"(e.g. {VERSION: '${BUILD_VERSION}'}). For credentials, prefer `secrets` -- build-args "
|
|
133
|
+
"are recorded in the image history."
|
|
134
|
+
),
|
|
135
|
+
)
|
|
136
|
+
secrets: list[str] = MergeField(
|
|
137
|
+
strategy="replace",
|
|
138
|
+
default_factory=list,
|
|
139
|
+
description=(
|
|
140
|
+
"BuildKit secret specs -> `--secret <spec>`, e.g. 'id=mytoken,env=MY_TOKEN' (forward a "
|
|
141
|
+
"host env var) or 'id=mytoken,src=/path/to/file'. Exposed only to RUN steps that mount "
|
|
142
|
+
"them, never baked into image layers. Reference in the Dockerfile via "
|
|
143
|
+
"`RUN --mount=type=secret,id=mytoken ...`."
|
|
144
|
+
),
|
|
145
|
+
)
|
|
146
|
+
extra_args: list[str] = MergeField(
|
|
147
|
+
strategy="replace",
|
|
148
|
+
default_factory=list,
|
|
149
|
+
description=(
|
|
150
|
+
"Additional raw `docker build` flags inserted before the build context, e.g. "
|
|
151
|
+
"['--target', 'runtime'] or ['--network', 'host']. Escape hatch for options without a "
|
|
152
|
+
"dedicated field."
|
|
153
|
+
),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class DockerDriverConfig(BaseModel):
|
|
158
|
+
"""Per-task overrides for ``driver: docker``.
|
|
159
|
+
|
|
160
|
+
Only consulted when ``SandboxConfig.driver == "docker"``. Controls which
|
|
161
|
+
host environment variables are forwarded to the container via an explicit
|
|
162
|
+
allowlist. Use ``env_passthrough_extra`` to add vars to the default allowlist
|
|
163
|
+
without replacing it.
|
|
164
|
+
|
|
165
|
+
Default allowlist covers credentials the in-container Orchestrator needs
|
|
166
|
+
(Anthropic API keys, Bedrock credentials, etc.).
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
model_config = ConfigDict(extra="forbid")
|
|
170
|
+
|
|
171
|
+
image: str = Field(
|
|
172
|
+
default_factory=get_default_docker_image_tag,
|
|
173
|
+
description=(
|
|
174
|
+
"Container image (default: coder-eval-agent:<pkg-version>). Override to use a custom image "
|
|
175
|
+
"(e.g., for BYOD: Bring Your Own Docker)."
|
|
176
|
+
),
|
|
177
|
+
)
|
|
178
|
+
dockerfile_path: str | None = Field(
|
|
179
|
+
default=None,
|
|
180
|
+
description=(
|
|
181
|
+
"Optional path to a Dockerfile to build a custom image for this task, relative to the "
|
|
182
|
+
"task YAML directory (resolved to an absolute path at load time). When set it overrides "
|
|
183
|
+
"`image`: the image is built with the Dockerfile's parent directory as the build context, "
|
|
184
|
+
"so relative COPY paths resolve. Example: `./environment/Dockerfile`."
|
|
185
|
+
),
|
|
186
|
+
)
|
|
187
|
+
build: DockerBuildConfig = Field(
|
|
188
|
+
default_factory=DockerBuildConfig,
|
|
189
|
+
description=(
|
|
190
|
+
"`docker build` customization (build args / BuildKit secrets / extra flags) applied when "
|
|
191
|
+
"`dockerfile_path` is set. Ignored when building is not in play."
|
|
192
|
+
),
|
|
193
|
+
)
|
|
194
|
+
network: Literal["bridge", "none"] = Field(
|
|
195
|
+
default="bridge",
|
|
196
|
+
description="Container network. 'bridge' for tasks needing LLM/pkg access; 'none' for fully sealed runs.",
|
|
197
|
+
)
|
|
198
|
+
working_dir: str | None = Field(
|
|
199
|
+
default=None,
|
|
200
|
+
description=(
|
|
201
|
+
"Run the agent at this absolute path inside the container instead of the default "
|
|
202
|
+
"run_dir/artifacts workspace, and copy it out to run_dir/artifacts/<task> afterward. "
|
|
203
|
+
"Use the exact WORKDIR the task's Dockerfile/inputs assume (e.g. '/root', '/app'). "
|
|
204
|
+
"The sentinel 'auto' detects the image's WORKDIR at run time (falls back to /root). "
|
|
205
|
+
"None (default) keeps the standard behavior. Docker driver only; ignored under driver:tempdir."
|
|
206
|
+
),
|
|
207
|
+
)
|
|
208
|
+
env_passthrough: list[str] = MergeField(
|
|
209
|
+
strategy="replace",
|
|
210
|
+
default_factory=lambda: [
|
|
211
|
+
"ANTHROPIC_API_KEY",
|
|
212
|
+
# Selects routing: direct Anthropic vs. Bedrock.
|
|
213
|
+
"API_BACKEND",
|
|
214
|
+
"UIPATH_LLM_BACKEND",
|
|
215
|
+
"UIPATH_ACCESS_TOKEN",
|
|
216
|
+
"UIPATH_URL",
|
|
217
|
+
"UIPATH_TENANT_ID",
|
|
218
|
+
"UIPATH_ORGANIZATION_ID",
|
|
219
|
+
# Disable uip CLI version-sync: the shared ~/.uipath mount lets one
|
|
220
|
+
# task's post-login re-pin downgrade later tasks' CLI/tools.
|
|
221
|
+
"UIPATH_CLI_DISABLE_VERSION_SYNC",
|
|
222
|
+
"AWS_BEARER_TOKEN_BEDROCK",
|
|
223
|
+
"AWS_REGION",
|
|
224
|
+
"BEDROCK_MODEL",
|
|
225
|
+
# Claude Code SDK Bedrock toggle + optional model override; required
|
|
226
|
+
# alongside AWS_BEARER_TOKEN_BEDROCK to route the in-container SDK
|
|
227
|
+
# through Bedrock instead of falling back to ~/.claude OAuth.
|
|
228
|
+
"CLAUDE_CODE_USE_BEDROCK",
|
|
229
|
+
"ANTHROPIC_MODEL",
|
|
230
|
+
# Codex agent auth/routing — without these the in-container codex
|
|
231
|
+
# binary falls back to a ChatGPT login that doesn't exist in the
|
|
232
|
+
# container and auth fails. CODEX_API_KEY drives login_api_key;
|
|
233
|
+
# CODEX_BASE_URL routes to a custom endpoint (e.g. gateway);
|
|
234
|
+
# CODEX_MODEL selects the model when agent.model is unset.
|
|
235
|
+
"CODEX_API_KEY",
|
|
236
|
+
"CODEX_BASE_URL",
|
|
237
|
+
"CODEX_MODEL",
|
|
238
|
+
# Antigravity agent auth/routing — the google-antigravity local harness
|
|
239
|
+
# authenticates against the Gemini API with GEMINI_API_KEY; without it
|
|
240
|
+
# the in-container harness has no credential and fails. ANTIGRAVITY_MODEL
|
|
241
|
+
# selects the Gemini model when agent.model is unset.
|
|
242
|
+
"GEMINI_API_KEY",
|
|
243
|
+
"ANTIGRAVITY_MODEL",
|
|
244
|
+
# User HOME used to keep ~/.claude resolution symmetric with the host.
|
|
245
|
+
# See docs/DOCKER_ISOLATION.md "HOME is forwarded by default" for the
|
|
246
|
+
# contract. tl;dr: Path.home() inside the container returns the
|
|
247
|
+
# host's HOME (the dir is auto-created by the ~/.claude bind mount);
|
|
248
|
+
# writes outside ~/.claude land in the container's ephemeral rootfs.
|
|
249
|
+
# Remove this entry if you don't want host HOME leakage.
|
|
250
|
+
"HOME",
|
|
251
|
+
],
|
|
252
|
+
description=(
|
|
253
|
+
"Allowlist of host environment variables to forward to the container. Only vars that exist in the host "
|
|
254
|
+
"environment are forwarded. To extend the default with custom vars (e.g., MY_API_TOKEN), use "
|
|
255
|
+
"env_passthrough_extra instead of replacing this field. Note: HOME is intentional in default "
|
|
256
|
+
"(keeps ~/.claude path symmetric with host); see docs/DOCKER_ISOLATION.md for details."
|
|
257
|
+
),
|
|
258
|
+
)
|
|
259
|
+
env_passthrough_extra: list[str] = MergeField(
|
|
260
|
+
strategy="append",
|
|
261
|
+
default_factory=list,
|
|
262
|
+
description=(
|
|
263
|
+
"Additional environment variables to forward beyond the default allowlist. Merged with env_passthrough "
|
|
264
|
+
"at runtime. Use this to add one or two custom vars (e.g., MY_API_TOKEN) without replacing the defaults. "
|
|
265
|
+
"Example: env_passthrough_extra: ['MY_CUSTOM_TOKEN', 'DEBUG_MODE']. Appended across config layers."
|
|
266
|
+
),
|
|
267
|
+
)
|
|
268
|
+
extra_mounts: list[str] = MergeField(
|
|
269
|
+
strategy="replace",
|
|
270
|
+
default_factory=list,
|
|
271
|
+
description="Extra `-v src:dst[:ro]` mount specs forwarded to `docker run`. Validated for basic syntax.",
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
@field_validator("working_dir")
|
|
275
|
+
@classmethod
|
|
276
|
+
def _validate_working_dir(cls, v: str | None) -> str | None:
|
|
277
|
+
"""Reject a WORKDIR that collides with a framework-reserved container path.
|
|
278
|
+
|
|
279
|
+
Reserved set is the single source of truth in ``models.container_paths``;
|
|
280
|
+
``docker_runner`` re-asserts against the same set host-side.
|
|
281
|
+
"""
|
|
282
|
+
if v is None or v == "auto":
|
|
283
|
+
return v
|
|
284
|
+
if not v.startswith("/"):
|
|
285
|
+
raise ValueError(f"working_dir {v!r} must be an absolute path or the sentinel 'auto'.")
|
|
286
|
+
norm = v.rstrip("/") or "/"
|
|
287
|
+
if norm in RESERVED_CONTAINER_DIRS or norm.startswith(CONTAINER_WORK_DIR + "/"):
|
|
288
|
+
raise ValueError(
|
|
289
|
+
f"working_dir {v!r} collides with a framework-reserved container path "
|
|
290
|
+
+ "(/, /work, /work/*). Choose the task image's own WORKDIR (e.g. /root, /app)."
|
|
291
|
+
)
|
|
292
|
+
return v
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
class SandboxConfig(BaseModel):
|
|
296
|
+
"""Configuration for the sandboxed execution environment.
|
|
297
|
+
|
|
298
|
+
``driver: tempdir`` (default) runs the agent in a plain temp directory on
|
|
299
|
+
the host with no container isolation -- agent commands share the host's
|
|
300
|
+
network, process table, and filesystem outside the temp dir.
|
|
301
|
+
``driver: docker`` runs each task inside its own container; see
|
|
302
|
+
:class:`DockerDriverConfig` for knobs. ``ResourceLimits.timeout`` is the
|
|
303
|
+
only limit enforced in tempdir mode; under docker, ``max_memory_mb`` also
|
|
304
|
+
maps to ``--memory`` when set.
|
|
305
|
+
"""
|
|
306
|
+
|
|
307
|
+
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
|
308
|
+
|
|
309
|
+
driver: Literal["tempdir", "docker"] = Field(
|
|
310
|
+
default="tempdir",
|
|
311
|
+
description="Sandbox driver: 'tempdir' = in-process on host; 'docker' = one container per task.",
|
|
312
|
+
)
|
|
313
|
+
docker: DockerDriverConfig = Field(
|
|
314
|
+
default_factory=DockerDriverConfig,
|
|
315
|
+
description="Docker-driver overrides; ignored unless driver == 'docker'.",
|
|
316
|
+
)
|
|
317
|
+
python: PythonEnvConfig | None = Field(
|
|
318
|
+
default_factory=PythonEnvConfig,
|
|
319
|
+
description="Python environment config; set to null in YAML (or None in Python) to skip venv creation",
|
|
320
|
+
)
|
|
321
|
+
node: NodeEnvConfig | None = Field(
|
|
322
|
+
default=None,
|
|
323
|
+
description="Node.js environment config; set to enable npm package installation in the sandbox",
|
|
324
|
+
)
|
|
325
|
+
limits: ResourceLimits = Field(default_factory=ResourceLimits, description="Resource limits for execution")
|
|
326
|
+
|
|
327
|
+
# Multi-source template support
|
|
328
|
+
template_sources: list[TemplateSource] | None = MergeField(
|
|
329
|
+
strategy="append",
|
|
330
|
+
default=None,
|
|
331
|
+
description="Sequential list of template sources to apply. Appended across config layers.",
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
mock_path_dirs: list[str] | None = MergeField(
|
|
335
|
+
strategy="replace",
|
|
336
|
+
default=None,
|
|
337
|
+
description=(
|
|
338
|
+
"Sandbox-relative directories whose contents act as PATH-prepended mock "
|
|
339
|
+
"binaries for the agent subprocess. After templates are applied, the "
|
|
340
|
+
"sandbox marks plain files in each listed directory executable (+x) and "
|
|
341
|
+
"returns absolute paths to the orchestrator, which forwards them to the "
|
|
342
|
+
"agent. Missing entries are skipped silently. Example: "
|
|
343
|
+
'["mocks"] with a `mocks/uip` script placed via `template_sources`.'
|
|
344
|
+
),
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
# Customizable ignore patterns
|
|
348
|
+
ignore_patterns: list[str] = MergeField(
|
|
349
|
+
strategy="replace",
|
|
350
|
+
default_factory=list,
|
|
351
|
+
description=(
|
|
352
|
+
"Pattern overrides applied during template setup. "
|
|
353
|
+
"Plain entries add to the defaults; entries prefixed with '!' "
|
|
354
|
+
"remove a default (gitignore-style negation). Use e.g. ['!dist', "
|
|
355
|
+
"'!node_modules'] to let vendored JS build outputs survive the "
|
|
356
|
+
"template copy."
|
|
357
|
+
),
|
|
358
|
+
validation_alias=AliasChoices("ignore_patterns", "additional_ignore_patterns"),
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
@field_validator("ignore_patterns")
|
|
362
|
+
@classmethod
|
|
363
|
+
def _validate_ignore_patterns(cls, values: list[str]) -> list[str]:
|
|
364
|
+
return [normalize_ignore_pattern_entry(v) for v in values]
|
|
365
|
+
|
|
366
|
+
@model_validator(mode="after")
|
|
367
|
+
def validate_template_sources(self) -> SandboxConfig:
|
|
368
|
+
"""Validate template sources configuration."""
|
|
369
|
+
if self.template_sources:
|
|
370
|
+
validate_template_sources_list(self.template_sources)
|
|
371
|
+
return self
|