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.
Files changed (127) hide show
  1. coder_eval/.gitattributes +2 -0
  2. coder_eval/__init__.py +3 -0
  3. coder_eval/agent.py +302 -0
  4. coder_eval/agents/__init__.py +41 -0
  5. coder_eval/agents/_logging.py +89 -0
  6. coder_eval/agents/antigravity_agent.py +811 -0
  7. coder_eval/agents/claude_code_agent.py +1701 -0
  8. coder_eval/agents/codex_agent.py +2055 -0
  9. coder_eval/agents/noop_agent.py +100 -0
  10. coder_eval/agents/registry.py +163 -0
  11. coder_eval/agents/watchdog.py +116 -0
  12. coder_eval/analysis.py +88 -0
  13. coder_eval/cli/__init__.py +87 -0
  14. coder_eval/cli/aggregate_command.py +153 -0
  15. coder_eval/cli/console.py +7 -0
  16. coder_eval/cli/evaluate_command.py +164 -0
  17. coder_eval/cli/plan_command.py +152 -0
  18. coder_eval/cli/report_command.py +121 -0
  19. coder_eval/cli/run_command.py +686 -0
  20. coder_eval/cli/run_helpers.py +137 -0
  21. coder_eval/cli/run_task_internal_command.py +210 -0
  22. coder_eval/cli/utils.py +37 -0
  23. coder_eval/config.py +212 -0
  24. coder_eval/criteria/__init__.py +163 -0
  25. coder_eval/criteria/_classification_aggregate.py +106 -0
  26. coder_eval/criteria/agent_judge.py +425 -0
  27. coder_eval/criteria/base.py +248 -0
  28. coder_eval/criteria/classification_match.py +107 -0
  29. coder_eval/criteria/command_executed.py +181 -0
  30. coder_eval/criteria/commands_efficiency.py +73 -0
  31. coder_eval/criteria/file_check.py +119 -0
  32. coder_eval/criteria/file_contains.py +85 -0
  33. coder_eval/criteria/file_exists.py +45 -0
  34. coder_eval/criteria/file_matches_regex.py +86 -0
  35. coder_eval/criteria/json_check.py +184 -0
  36. coder_eval/criteria/llm_judge.py +260 -0
  37. coder_eval/criteria/reference_comparison.py +130 -0
  38. coder_eval/criteria/run_command.py +220 -0
  39. coder_eval/criteria/skill_triggered.py +111 -0
  40. coder_eval/criteria/uipath_eval.py +223 -0
  41. coder_eval/errors/__init__.py +26 -0
  42. coder_eval/errors/agent.py +26 -0
  43. coder_eval/errors/budget.py +28 -0
  44. coder_eval/errors/categories.py +205 -0
  45. coder_eval/errors/categorization.py +240 -0
  46. coder_eval/errors/executor.py +124 -0
  47. coder_eval/errors/judge.py +12 -0
  48. coder_eval/errors/retry.py +211 -0
  49. coder_eval/errors/timeout.py +63 -0
  50. coder_eval/evaluation/__init__.py +10 -0
  51. coder_eval/evaluation/checker.py +260 -0
  52. coder_eval/evaluation/judge_anthropic.py +55 -0
  53. coder_eval/evaluation/judge_bedrock.py +114 -0
  54. coder_eval/evaluation/judge_context.py +497 -0
  55. coder_eval/evaluation/judge_models.py +53 -0
  56. coder_eval/evaluation/judge_persistence.py +291 -0
  57. coder_eval/evaluation/judge_usage.py +50 -0
  58. coder_eval/evaluation/sub_agent.py +231 -0
  59. coder_eval/evaluation/summaries.py +48 -0
  60. coder_eval/evaluation/verdict_tool.py +213 -0
  61. coder_eval/formatting.py +191 -0
  62. coder_eval/isolation/__init__.py +15 -0
  63. coder_eval/isolation/docker_runner.py +1253 -0
  64. coder_eval/logging_config.py +399 -0
  65. coder_eval/models/__init__.py +337 -0
  66. coder_eval/models/agent_config.py +373 -0
  67. coder_eval/models/container_paths.py +26 -0
  68. coder_eval/models/criteria.py +942 -0
  69. coder_eval/models/enums.py +121 -0
  70. coder_eval/models/experiment.py +314 -0
  71. coder_eval/models/judge.py +90 -0
  72. coder_eval/models/judge_defaults.py +11 -0
  73. coder_eval/models/limits.py +89 -0
  74. coder_eval/models/merge_strategy.py +132 -0
  75. coder_eval/models/mutations.py +95 -0
  76. coder_eval/models/results.py +787 -0
  77. coder_eval/models/routing.py +160 -0
  78. coder_eval/models/sandbox.py +371 -0
  79. coder_eval/models/tasks.py +631 -0
  80. coder_eval/models/telemetry.py +454 -0
  81. coder_eval/models/templates.py +108 -0
  82. coder_eval/orchestration/__init__.py +13 -0
  83. coder_eval/orchestration/batch.py +690 -0
  84. coder_eval/orchestration/config.py +111 -0
  85. coder_eval/orchestration/config_merge.py +431 -0
  86. coder_eval/orchestration/evaluation.py +94 -0
  87. coder_eval/orchestration/experiment.py +837 -0
  88. coder_eval/orchestration/overrides.py +206 -0
  89. coder_eval/orchestration/task_loader.py +437 -0
  90. coder_eval/orchestrator.py +2078 -0
  91. coder_eval/path_utils.py +79 -0
  92. coder_eval/plugins.py +81 -0
  93. coder_eval/pricing.py +169 -0
  94. coder_eval/py.typed +0 -0
  95. coder_eval/reports.py +1066 -0
  96. coder_eval/reports_experiment.py +761 -0
  97. coder_eval/reports_html.py +1659 -0
  98. coder_eval/reports_stats.py +250 -0
  99. coder_eval/resources/__init__.py +137 -0
  100. coder_eval/resources/default_experiment.yaml +85 -0
  101. coder_eval/resources/default_ignore_patterns.yaml +60 -0
  102. coder_eval/resources/tags.yaml +55 -0
  103. coder_eval/sandbox.py +1127 -0
  104. coder_eval/scoring/__init__.py +11 -0
  105. coder_eval/scoring/ast_similarity.py +38 -0
  106. coder_eval/scoring/complexity.py +115 -0
  107. coder_eval/scoring/quality.py +154 -0
  108. coder_eval/scoring/signature_similarity.py +57 -0
  109. coder_eval/scoring/similarity.py +103 -0
  110. coder_eval/scoring/token_similarity.py +44 -0
  111. coder_eval/simulation/__init__.py +27 -0
  112. coder_eval/simulation/termination.py +88 -0
  113. coder_eval/simulation/user_simulator.py +324 -0
  114. coder_eval/streaming/__init__.py +44 -0
  115. coder_eval/streaming/callbacks.py +57 -0
  116. coder_eval/streaming/collector.py +193 -0
  117. coder_eval/streaming/events.py +198 -0
  118. coder_eval/streaming/renderers.py +248 -0
  119. coder_eval/streaming/wire.py +115 -0
  120. coder_eval/telemetry.py +407 -0
  121. coder_eval/utils.py +517 -0
  122. coder_eval-0.8.2.dist-info/METADATA +242 -0
  123. coder_eval-0.8.2.dist-info/RECORD +127 -0
  124. coder_eval-0.8.2.dist-info/WHEEL +4 -0
  125. coder_eval-0.8.2.dist-info/entry_points.txt +5 -0
  126. coder_eval-0.8.2.dist-info/licenses/LICENSE +201 -0
  127. 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