Skip to content

Commit 04498a9

Browse files
bai-uipathclaude
andauthored
fix(agents): run codex + antigravity harnesses on the tempdir/host path (#33)
* fix(antigravity): pin google-antigravity 0.1.7 to load on glibc 2.35 The pinned 0.1.5 localharness binary requires the GLIBC_ABI_DT_RELR ABI (glibc 2.36+), so it fails to load on the eval agents' Ubuntu 22.04 (glibc 2.35), taking down the antigravity tempdir/host path (the activation suite and container-less skills tasks) while docker-driver tasks stay fine. 0.1.7 links without DT_RELR (max required symbol GLIBC_2.26) and loads on 2.35+. The manylinux_2_17 wheel tag alone does not guarantee this, so the pin comment documents verifying ELF load before any future bump. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(codex): use full-access sandbox on coder_eval-managed tempdir path Codex's read-only / workspace-write sandboxes shell out to an OS-level helper (Landlock, or a bwrap re-exec) that fails to initialize on managed CI hosts ("bwrap: execvp .../codex: Permission denied" where unprivileged user namespaces or exec mounts are restricted); its writes/execs then fail silently and the task produces no artifacts. The docker path already avoids this via CODER_EVAL_IN_CONTAINER. Thread a `sandbox_managed` signal through Agent.start() so the tempdir driver — where coder_eval's ephemeral per-task dir is the isolation boundary — also falls back to full-access, matching Claude Code (no in-agent OS sandbox on that path). Hard isolation of untrusted actions remains the docker driver's job. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test: accept sandbox_managed kwarg in agent test doubles The orchestrator now threads `sandbox_managed` into `Agent.start()`, and the codebase's convention is that agent test doubles mirror the ABC signature exactly. Add the keyword-only param (accepted and ignored, like `env_path_prepend`) to every double that declares `start()`, so the orchestrator-routed doubles (MockAgent and the inline DummyAgents) stop raising `unexpected keyword argument 'sandbox_managed'`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(deps): bump pyasn1 to 0.6.4 for GHSA-8ppf-4f7h-5ppj / GHSA-hm4w-wwcw-mr6r pyasn1 0.6.3 (transitive via google-auth → pyasn1-modules) carries two High advisories fixed in 0.6.4. Pre-existing on main; bumping the lock clears both osv-scanner and pip-audit. No suppression entry is added because a fix is available (osv-scanner.toml is only for unfixable advisories). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(deps): upgrade agent SDKs to latest (claude 0.2.124, codex 0.144.4) - claude-agent-sdk 0.2.82 -> 0.2.124 - openai-codex 0.1.0b3 -> 0.144.4, and its pinned cli-bin 0.137.0a4 -> 0.144.4 (the SDK version now tracks the codex CLI line; the 0.144.x jump is a re-numbering, not an API rewrite). The public API surface codex_agent.py depends on — Codex/Thread/turn, the notification method strings, item types, and token-usage fields — stayed source-compatible, so no agent code changed. - google-antigravity already at 0.1.7 (latest; pinned by the tempdir fix). Full suite incl. the live codex integration tests is green on all three. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci(release): publish a prerelease from a non-main branch Dispatching release.yml from a branch (github.ref != main) now runs a prerelease instead of a real release: stamp a throwaway <next-patch>rcN version into pyproject/__init__, refresh uv.lock, build, publish the wheel to public PyPI (OIDC) and a :<version> GHCR agent image. It does NOT move :latest, tag, commit, or push to main. This lets a branch be dry-run on the ADO nightly infra (pinned via coderEvalVersion) before merge. The main path is unchanged; the bump input is ignored off main. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(codex): always run full-access; drop the in-process OS sandbox coder_eval always owns a run's isolation boundary — a docker container (docker driver) or an ephemeral per-task tempdir it creates and discards (tempdir driver); those are the only two drivers — so Codex's own in-process OS sandbox (Landlock/seatbelt) is always redundant. Worse, it breaks on the paths we rely on: Landlock is unavailable in the container, the bwrap re-exec is denied on constrained CI agents, and Windows has no OS sandbox at all — in each case a read-only/workspace-write run fails its writes/execs silently and scores 0. This is what left the Windows uipath-rpa tasks unable to run `uip` (exit -1) even after the earlier container/tempdir fallback landed. Run full-access unconditionally (approval_mode stays deny_all), matching Claude Code and Antigravity, which run with no in-agent OS sandbox; hard isolation of untrusted actions is the docker driver's job. Removes the now-obsolete sandbox_managed plumbing and the per-mode sandbox mapping. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(codex): drop dead sandbox branch + honest full-access messaging Addresses review on PR #33 (always-full-access): - Collapse the sandbox assignment to `Sandbox.full_access` and delete the now-unreachable `workspace-write` network-opener branch (dead since the mode became a constant). - `_log_config_enforcement` now emits the [SECURITY] full-access notice for EVERY permission_mode, not just bypassPermissions — all modes resolve to full-access, so the old per-mode warning misled operators. - Document that permission_mode does not confine Codex and that the docker driver is the OS-level write boundary (code comment + TASK_DEFINITION_GUIDE). - Note the test matrix's os_name/in_container dims are invariance-by-construction. - Disclose in the release.yml `bump` input that it is ignored off-main. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent f1f4f9f commit 04498a9

9 files changed

Lines changed: 255 additions & 173 deletions

File tree

.github/workflows/release.yml

Lines changed: 120 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,19 @@ name: Release
1313
# and push.
1414
# (Continuous :latest / :sha- agent images still publish on every main push via
1515
# docker-publish.yml -- only the versioned release artifacts gate on this run.)
16+
#
17+
# PRERELEASE mode (dispatched from a NON-main branch): instead of bumping and
18+
# pushing main, stamp a throwaway `<next-patch>rc<run#>` version, then build and
19+
# publish the wheel to public PyPI + a `:<version>` GHCR image. It does NOT move
20+
# `:latest`, tag, commit, or push to main. This lets a branch be dry-run on the
21+
# ADO nightly infra before merge (pinned via the pipeline's `coderEvalVersion`).
22+
# The `bump` input is ignored off main. On `main` the behavior is unchanged.
1623

1724
on:
1825
workflow_dispatch:
1926
inputs:
2027
bump:
21-
description: 'Version bump to release'
28+
description: 'Version bump to release (ignored on non-main / prerelease dispatch, which always stamps a next-patch rc)'
2229
type: choice
2330
default: patch
2431
options:
@@ -47,9 +54,9 @@ jobs:
4754
runs-on: ubuntu-latest
4855
timeout-minutes: 15
4956
outputs:
50-
# Exposed so the downstream publish-pypi job can gate on a real release
51-
# having been cut (empty => no version bumped => skip PyPI publish).
52-
version: ${{ steps.release.outputs.version }}
57+
# Exposed so the downstream publish-pypi job gates on a version having been
58+
# produced (real release on main, or a stamped prerelease on a branch).
59+
version: ${{ steps.ver.outputs.version }}
5360
env:
5461
# The self-hosted `uipath-ubuntu-latest` runners enforce a minimum
5562
# package-age safe-chain check on uv installs; on GitHub-hosted runners
@@ -58,8 +65,13 @@ jobs:
5865
SAFE_CHAIN_MINIMUM_PACKAGE_AGE_EXCLUSIONS: "openai-codex-cli-bin,openai-codex"
5966

6067
steps:
68+
# Only a real release (main) needs the app token: semantic-release pushes the
69+
# bump commit + tag to the ruleset-protected main branch, and only this app
70+
# has the bypass. A prerelease from a branch never commits or pushes, so it
71+
# skips the token and checks out with the default GITHUB_TOKEN.
6172
- name: Mint release app token
6273
id: app-token
74+
if: github.ref == 'refs/heads/main'
6375
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
6476
with:
6577
app-id: ${{ secrets.RELEASE_APP_ID }}
@@ -70,8 +82,9 @@ jobs:
7082
with:
7183
fetch-depth: 0 # semantic-release needs full history for tags + changelog
7284
# Persisted in .git config so semantic-release's push to main is
73-
# authenticated as the app (which bypasses the branch ruleset).
74-
token: ${{ steps.app-token.outputs.token }}
85+
# authenticated as the app (which bypasses the branch ruleset). Falls back
86+
# to the default token for a prerelease (no push, read-only checkout).
87+
token: ${{ steps.app-token.outputs.token || github.token }}
7588

7689
- name: Set up Python 3.13
7790
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
@@ -83,11 +96,38 @@ jobs:
8396
with:
8497
enable-cache: true
8598

99+
# Release mode is chosen by the dispatched ref: main => real release (the
100+
# semantic-release path below); any other branch => prerelease (stamp a
101+
# throwaway rc version, publish, never touch main). Keying off the ref (not a
102+
# new input) keeps this dispatchable from a branch — a new input would have to
103+
# exist on the default branch first to be accepted.
104+
- name: Determine release mode
105+
id: mode
106+
env:
107+
REF: ${{ github.ref }}
108+
RUN_NUMBER: ${{ github.run_number }}
109+
run: |
110+
set -euo pipefail
111+
if [ "$REF" = "refs/heads/main" ]; then
112+
echo "prerelease=false" >> "$GITHUB_OUTPUT"
113+
echo "Mode: RELEASE (main)"
114+
else
115+
# Next patch of the current version, suffixed with the run number so
116+
# repeated dispatches never collide on PyPI. An exact `==` pin installs
117+
# it even though pip/uv skip prereleases by default.
118+
NEXT=$(python3 -c 'import re,tomllib; v=tomllib.load(open("pyproject.toml","rb"))["project"]["version"]; m=re.match(r"(\d+)\.(\d+)\.(\d+)",v); print("{}.{}.{}".format(int(m[1]),int(m[2]),int(m[3])+1))')
119+
PRE="${NEXT}rc${RUN_NUMBER}"
120+
echo "prerelease=true" >> "$GITHUB_OUTPUT"
121+
echo "version=${PRE}" >> "$GITHUB_OUTPUT"
122+
echo "Mode: PRERELEASE ${PRE}"
123+
fi
124+
86125
- name: Install build + release tools
87126
run: uv tool install python-semantic-release && uv tool install twine
88127

89128
- name: Run semantic-release (bump + tag, no push yet)
90129
id: release
130+
if: steps.mode.outputs.prerelease != 'true'
91131
env:
92132
GH_TOKEN: ${{ steps.app-token.outputs.token }}
93133
# Passed via env (not interpolated into the script) per GitHub's
@@ -105,8 +145,45 @@ jobs:
105145
# A dispatch always cuts a release; report the just-published version.
106146
echo "version=$($PSR version --print)" >> "$GITHUB_OUTPUT"
107147
148+
# PRERELEASE: stamp the rc version into the two canonical spots (mirrors
149+
# publish-testpypi.yml), then refresh uv.lock so the Dockerfile's
150+
# `uv export --frozen` accepts the bumped project version. No commit is made —
151+
# the working tree is what `uv build` and the image build below consume.
152+
- name: Stamp prerelease version
153+
if: steps.mode.outputs.prerelease == 'true'
154+
env:
155+
PRE_VERSION: ${{ steps.mode.outputs.version }}
156+
run: |
157+
set -euo pipefail
158+
python3 - <<'PY'
159+
import os, re, pathlib
160+
version = os.environ["PRE_VERSION"]
161+
for path, key in (("pyproject.toml", "version"), ("src/coder_eval/__init__.py", "__version__")):
162+
p = pathlib.Path(path)
163+
new, n = re.subn(rf'(?m)^{key}\s*=\s*".+?"$', f'{key} = "{version}"', p.read_text(), count=1)
164+
if n != 1:
165+
raise SystemExit(f"version pattern did not match {path} (matched {n})")
166+
p.write_text(new)
167+
print(f"Stamped prerelease version: {version}")
168+
PY
169+
uv lock
170+
171+
# Single source of truth for the steps below: the real release version (main)
172+
# or the stamped prerelease version (branch).
173+
- name: Resolve published version
174+
id: ver
175+
env:
176+
REL: ${{ steps.release.outputs.version }}
177+
PRE: ${{ steps.mode.outputs.version }}
178+
run: |
179+
set -euo pipefail
180+
V="${REL:-$PRE}"
181+
if [ -z "$V" ]; then echo "no version resolved" >&2; exit 1; fi
182+
echo "version=$V" >> "$GITHUB_OUTPUT"
183+
echo "Publishing version: $V"
184+
108185
- name: Regenerate uv.lock and amend release commit
109-
if: steps.release.outputs.version != ''
186+
if: steps.mode.outputs.prerelease != 'true' && steps.release.outputs.version != ''
110187
run: |
111188
uv lock
112189
if ! git diff --quiet uv.lock; then
@@ -119,43 +196,64 @@ jobs:
119196
fi
120197
121198
- name: Push release commit and tags
122-
if: steps.release.outputs.version != ''
199+
if: steps.mode.outputs.prerelease != 'true' && steps.release.outputs.version != ''
123200
run: git push origin main "v${{ steps.release.outputs.version }}"
124201

125202
- name: Build wheel + sdist
126-
if: steps.release.outputs.version != ''
203+
if: steps.ver.outputs.version != ''
127204
run: uv build
128205

129206
# Hand the exact built artifacts to the publish-pypi job. Publishing to
130207
# public PyPI runs in its own environment-gated job (OIDC), so it must
131208
# consume these files rather than rebuild them.
132209
- name: Upload dist for PyPI publish
133-
if: steps.release.outputs.version != ''
210+
if: steps.ver.outputs.version != ''
134211
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
135212
with:
136213
name: release-dist
137214
path: dist/
138215
if-no-files-found: error
139216

140-
# Build + push the agent image HERE, in the same job that cut the release,
141-
# so the `:<version>` tag is built from the BUMPED pyproject (the version
142-
# only exists after the semantic-release step above). docker-publish.yml
143-
# runs on the triggering commit, BEFORE the bump, so it can never tag the
144-
# release version -- this is the authoritative versioned image. It also
145-
# repoints `:latest` to the exact release artifact.
217+
# Build + push the agent image HERE, in the same job that produced the
218+
# version, so the `:<version>` tag is built from the correct pyproject (bumped
219+
# by semantic-release on main, or the stamped rc on a prerelease).
220+
# docker-publish.yml runs on the triggering commit, BEFORE a main bump, so it
221+
# can never tag the release version -- this is the authoritative versioned
222+
# image. A real release also repoints `:latest`; a prerelease publishes only
223+
# its `:<version>` tag (see Compute image tags).
146224
- name: Lowercase owner for GHCR
147-
if: steps.release.outputs.version != ''
225+
if: steps.ver.outputs.version != ''
148226
id: img
149227
run: echo "owner_lc=$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_OUTPUT"
150228

229+
# `:<version>` always; `:latest` only for a real release. A prerelease must
230+
# not move `:latest`, which the nightly treats as tip-of-main.
231+
- name: Compute image tags
232+
if: steps.ver.outputs.version != ''
233+
id: tags
234+
env:
235+
OWNER: ${{ steps.img.outputs.owner_lc }}
236+
VERSION: ${{ steps.ver.outputs.version }}
237+
IS_PRERELEASE: ${{ steps.mode.outputs.prerelease }}
238+
run: |
239+
set -euo pipefail
240+
{
241+
echo "tags<<EOF"
242+
echo "ghcr.io/${OWNER}/coder-eval-agent:${VERSION}"
243+
if [ "$IS_PRERELEASE" != "true" ]; then
244+
echo "ghcr.io/${OWNER}/coder-eval-agent:latest"
245+
fi
246+
echo "EOF"
247+
} >> "$GITHUB_OUTPUT"
248+
151249
- name: Set up Docker Buildx
152250
continue-on-error: true # GHCR image is internal/best-effort; don't block PyPI
153-
if: steps.release.outputs.version != ''
251+
if: steps.ver.outputs.version != ''
154252
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
155253

156254
- name: Log in to GHCR
157255
continue-on-error: true # GHCR image is internal/best-effort; don't block PyPI
158-
if: steps.release.outputs.version != ''
256+
if: steps.ver.outputs.version != ''
159257
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
160258
with:
161259
registry: ghcr.io
@@ -164,17 +262,15 @@ jobs:
164262

165263
- name: Build and push versioned agent image
166264
continue-on-error: true # GHCR image is internal/best-effort; don't block PyPI
167-
if: steps.release.outputs.version != ''
265+
if: steps.ver.outputs.version != ''
168266
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
169267
with:
170268
context: .
171269
file: docker/Dockerfile
172270
push: true
173-
tags: |
174-
ghcr.io/${{ steps.img.outputs.owner_lc }}/coder-eval-agent:${{ steps.release.outputs.version }}
175-
ghcr.io/${{ steps.img.outputs.owner_lc }}/coder-eval-agent:latest
271+
tags: ${{ steps.tags.outputs.tags }}
176272
build-args: |
177-
CODER_EVAL_VERSION=${{ steps.release.outputs.version }}
273+
CODER_EVAL_VERSION=${{ steps.ver.outputs.version }}
178274
secrets: |
179275
"uv_index_username=${{ secrets.UV_INDEX_UIPATH_USERNAME }}"
180276
"uv_index_password=${{ secrets.UV_INDEX_UIPATH_PASSWORD }}"

docs/TASK_DEFINITION_GUIDE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ an error.
130130
- `plan` — Agent proposes changes, waits for approval
131131
- `bypassPermissions` — No permission checks (use with caution)
132132

133+
> **Codex note:** `permission_mode` confines the **`claude-code`** agent only. The **`codex`** agent always runs full-access regardless of the mode — its in-process OS sandbox is redundant given coder_eval's docker/tempdir isolation and unusable on our CI hosts (and on Windows). Run adversarial or untrusted Codex evals under the **docker driver**, which is the OS-level write boundary; the tempdir/host driver is a working directory, not a confinement boundary.
134+
133135
**Agent Types:**
134136
- `claude-code` (default) — Claude Code SDK agent. Supports `sdk_options`, `claude_settings`, and all permission modes.
135137
- `codex` — OpenAI Codex agent (requires `[codex]` extra; set `CODEX_API_KEY` and optional `CODEX_BASE_URL` environment variables).

pyproject.toml

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ dependencies = [
3535
"rich>=14.3.3",
3636
"python-dotenv>=1.2.2",
3737
"anthropic>=0.86.0",
38-
"claude-agent-sdk>=0.2.82",
38+
"claude-agent-sdk>=0.2.124",
3939
"anyio>=4.13.0",
4040
"radon>=6.0.1",
4141
"tqdm>=4.67.3",
@@ -86,19 +86,24 @@ uipath = [
8686
# Without this extra, the framework still installs and runs; Codex-dependent
8787
# code paths fail at dispatch with a clear hint pointing back here.
8888
codex = [
89-
"openai-codex>=0.1.0b3",
89+
"openai-codex>=0.144.4",
9090
]
9191
# Optional extra that enables Antigravity agent support:
9292
# - AntigravityAgent implementation using Google's official google-antigravity SDK
9393
# (drives the bundled `localharness` binary, authenticated via GEMINI_API_KEY).
9494
# Pinned exactly: the harness binary is a dominant non-model driver of eval results,
9595
# so it travels with the coder_eval release tag and is bumped deliberately (mirrors
96-
# the claude-code CLI / codex cli-bin pins). The wheel ships platform-specific
97-
# (manylinux x86_64 + aarch64, macOS, Windows), so CI/Linux/Docker installs work.
96+
# the claude-code CLI / codex cli-bin pins).
97+
# glibc note — verify before bumping: the wheel's manylinux_2_17 tag is NOT a
98+
# reliable floor; the bundled `localharness` ELF can require a newer glibc than the
99+
# tag advertises. Eval agents run on Ubuntu 22.04 (glibc 2.35), so the pinned build
100+
# must load there. 0.1.7 links without DT_RELR (max required symbol GLIBC_2.26) and
101+
# loads on 2.35+; confirm any future pin does too (readelf -d | grep RELR must be
102+
# empty / max GLIBC symbol <= the oldest target's glibc).
98103
# Without this extra the framework still installs and runs; Antigravity-dependent
99104
# code paths fail at start() with a clear hint pointing back here.
100105
antigravity = [
101-
"google-antigravity==0.1.5",
106+
"google-antigravity==0.1.7",
102107
]
103108

104109
[project.scripts]
@@ -130,12 +135,14 @@ packages = ["src/coder_eval"]
130135
allow-direct-references = true
131136

132137
[tool.uv]
133-
# openai-codex 0.1.0b3 hardpins `openai-codex-cli-bin==0.137.0a4`, a
134-
# PRE-RELEASE. Because that pin is transitive (not referenced directly by us),
135-
# uv won't select it without an explicit opt-in. Naming it here as an override
136-
# both enables the pre-release and documents the pinned cli-bin build; 0.137.0a4
137-
# publishes manylinux wheels (x86_64 + aarch64), so CI/Linux installs work.
138-
override-dependencies = ["openai-codex-cli-bin==0.137.0a4"]
138+
# openai-codex 0.144.4 hardpins `openai-codex-cli-bin==0.144.4` (a stable
139+
# release; the SDK version now tracks the codex CLI version line). Naming it
140+
# here documents and holds the pinned cli-bin build — the harness binary is a
141+
# dominant non-model driver of eval results, so it travels with the coder_eval
142+
# release tag and is bumped deliberately (mirrors the antigravity localharness
143+
# and claude-code CLI pins). 0.144.4 publishes manylinux wheels (x86_64 +
144+
# aarch64), so CI/Linux installs work.
145+
override-dependencies = ["openai-codex-cli-bin==0.144.4"]
139146

140147
constraint-dependencies = [
141148
# Fix known CVEs in transitive dependencies

0 commit comments

Comments
 (0)