Repository navigation
examples(appworld): AppWorld benchmark with cap-evolve vs ALTK-Evolve, and optimizer isolation - #874
Conversation
|
🏷️ Automatic Labeling I've analyzed this pull request and added the following labels:
These labels were selected based on the PR title, description, and changed files. If you believe any labels are incorrect or missing, feel free to adjust them manually. |
…namespace wrapper) - examples/appworld: the evaluation code, same as PR skillberry-ai#874 (cleaned up: configurable paths and model key, settings templates, namespace wrapper for the optimizer, planted-secret check A9, offline tests). - docs/EVAL_SMOKE_TEST_FIX.md: what to re-run and why (cap-evolve training from scratch with the answers out of reach from round 1; the final window for all three arms), with the isolation options tested on this host. - docs/EVAL_SMOKE_TEST.md: findings F17 (Claude Code's sandbox cannot start in this container) and F18 (bwrap variant tested and dropped). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
OsherElhadad
left a comment
There was a problem hiding this comment.
Code review findings
Reviewed the diff plus the checked-out branch (appworld-example @ a0996fb54) and AppWorld's actually-installed source. Most severe first.
1. Lint safety gate likely broken in the real launch path
examples/appworld/adapters/lints.py:32 reads env var APPWORLD_ROOT (hardcoded fallback /tmp/aw_eval/root_opt), while adapter.py's _appworld_root() (lines 48-49) reads a different env var, AW_APPWORLD_ROOT, falling back to AW_EVAL-derived root_opt. launch_cape_hc.sh — the actual search-launch script — sets neither. Only setup_project.sh sets APPWORLD_ROOT, and only for a one-off cap-evolve check call; final_window.py only sets AW_APPWORLD_ROOT.
On top of that, the optimizer is instructed to run lints.py itself inside its sandboxed mount-namespace (sandbox/claude:22), where root_opt is not mounted — only venv-aw, appworld/src, appworld/experiments, reference, lint_cache.json are allow-listed back in. The lint cache that would let lints.py run without touching root_opt is only ever built by write_cache() / --write-cache, which no script, README step, or CI step calls — only the unit test does.
Net effect: in the realistic launch path, the optimizer's required python lints.py . call hits a cold cache, tries to read root_opt/data/tasks/*/specs.json (invisible inside its sandbox), and raises FileNotFoundError instead of the {"ok": true} its instructions require before stopping — breaking the "lint before you stop" safety gate rather than just degrading it.
2. os-module sandbox bypass via submodule attributes
aw_agent.py:217 (harden_guard) removes os/pathlib/io/shutil/glob from AppWorld's ALLOWED_MODULE_NAMES, but AppWorld's installed safety_guard.py only inspects literal import statements (parse_imports) — it never walks attribute chains. Modules still allowed (e.g. uuid, contextlib) expose a live os submodule as a plain attribute (verified: uuid.os, contextlib.os both resolve to the real os module). Agent code can do import uuid; uuid.os.environ.get(...) and reach os directly, undermining the sandboxing this example's secrets story depends on.
3. Unredacted traceback on the exception path
aw_agent.py:356 — main()'s except branch writes traceback.format_exc()[-4000:] verbatim into the output record, unlike the normal path where evaluation_fields() carefully strips AppWorld's gold-bearing failure trace first. Any internal exception can leak task state/paths into out.json and downstream logs/optimizer feedback.
4. Lint read ordering skips the readiness wait
adapter.py:199 — run_trials() calls lints.lint_candidate(cdir) before _wait_readable(root) (line 206), while every other root_opt read in this file (including _split_ids and run_trials itself a few lines later) waits for readability first. Per the README, root_opt is locked while the optimizer runs; a cold-cache lint call during that window risks a transient read error or reading a half-written tree.
5. Secrets cleanup only covers one key, not all loaded
aw_agent.py:74 — load_secrets() applies os.environ.setdefault for every KEY=VALUE line in the secrets file, but cleanup (aw_agent.py:267, os.environ.pop(KEY_ENV, None)) and adapter.py's subprocess env filter (line 166) only strip the single configured key. A second credential in the secrets file would persist in the episode subprocess's env for the rest of the episode — exploitable via finding #2's os-bypass.
6. Sandbox bind-mount prefix-strip bug
sandbox/claude:54 — rel="${WORK#"$AW"/}" strips $AW/ (with trailing slash) from $WORK (no trailing slash). If the optimizer's cwd is exactly $AW_EVAL itself, the strip doesn't match and rel becomes $AW's own absolute path, producing a nonsense nested bind mount while leaving the real working directory unbound inside the sandboxed namespace.
7. Duplicated bootstrap-CI logic
scripts/analyze.py:50 — bootstrap() reimplements a percentile-bootstrap CI from scratch, duplicating core/cap_evolve/stats.py's existing bootstrap_ci() (the project's designated single source of truth for eval-statistics aggregation).
8. Hand-rolled sandbox reimplements the host's own isolation
examples/appworld/sandbox/claude (whole file, ~75 lines) reimplements mount-namespace/bind-mount isolation that Claude Code's own bubblewrap sandbox should provide, only needed because of a container limitation (/dev/null bind issue). This now requires its own permanent isolation audit (check_optimizer_isolation.sh) instead of fixing the actual gap.
9. Unverified upstream constructor compatibility (lower confidence)
aw_agent.py:252-265 — build_agent_class() passes 8 kwargs into the upstream appworld_agents.code.simplified.react_code_agent.SimplifiedReActCodeAgent constructor via **kwargs. That package isn't vendored and couldn't be installed in review (no registry access), so the signature match couldn't be verified. If upstream renamed/dropped a kwarg, episode construction fails with a generic opaque infra_error. Worth a smoke-test run before merge.
Lower-confidence items dropped after verification: minor duplicate progress-logger/token-counter/dotenv code across adapter.py/altk_common.py (cosmetic); a latent joblib cache secret-persistence path in cached_lm_call (unreachable today since use_cache=False); Adapter.tasks() hardcoding ROOT_OPT instead of AW_APPWORLD_ROOT (currently unreachable in the shipped call graph).
🤖 Generated with Claude Code
…h (review skillberry-ai#874, 1) lints.py read APPWORLD_ROOT with a hardcoded /tmp/aw_eval fallback, while adapter.py reads AW_APPWORLD_ROOT. Nothing built the lint cache, and the optimizer was told to lint against root_opt, which its namespace hides. A cold cache raised FileNotFoundError instead of printing {"ok": ...}. - lints.py resolves the cache (AW_LINT_CACHE, default $AW_EVAL/lint_cache.json) and the build root (AW_LINT_ROOT, default $AW_EVAL/root_opt). It never follows the episode root, which is root_full in the final window. - Missing cache and unreadable root raise LintDataUnavailable. The CLI prints "ok": false with "lint data unavailable" and exits 2. - make_roots.py writes the cache from root_opt (mode 0644, sha256 in the manifest). - setup_project.sh, launch_cape_hc.sh and check_optimizer_isolation.sh refuse to start without the cache. The optimizer instructions point at the cache, not root_opt. - Tests: tests/test_lint_data_path.py. The shared fixture moves to tests/aw_testlib.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s (review skillberry-ai#874, 4) The lint ran before _wait_readable, unlike every other root read in the adapter. Wait first, then lint. The _wait_readable docstring said root_opt is locked while the optimizer runs; nothing locks it any more (the namespace wrapper isolates the optimizer), so it now says what the wait is for. A comment says Adapter.tasks() reads root_opt on purpose. Test: tests/test_run_trials_order.py (fails before this change). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rops it (review skillberry-ai#874, 5) load_secrets() set every KEY=VALUE line of the secrets file, but only the configured key was removed afterwards. The adapter also gave the worker the whole `cap-evolve run` environment minus that one key, including the optimizer's credentials. - load_secrets() sets only the agent key (AW_KEY_ENV) and the endpoint URL and returns what it set. drop_secrets() removes every secret name once the client is built. - aw_agent.worker_env() builds the worker's environment from an allowlist (WORKER_ENV_NAMES plus LC_*/APPWORLD_*). The adapter uses it in _episode, so the final window is covered too. - README: Setup step 4 and Harness describe this. Tests: tests/test_worker_secrets.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… (review skillberry-ai#874, 3) main() wrote str(e) and traceback.format_exc() into the record. The message reached the optimizer through Rollout.error and the score feedback, and a crashed worker's stderr did too. An exception raised in world.evaluate() can carry gold values. - run_episode tracks its phase (setup / episode / evaluate). redact_error() reports the exception type, phase and frames (file:line in func, no source). The message is kept only outside evaluate, with the root, candidate, secrets file and key replaced, capped at 300 chars. - The raw traceback is gone from the record. With AW_KEEP_OUTPUTS=1 it is written next to AppWorld's own outputs, which are kept only in that mode. - adapter._episode reduces a crashed worker's stderr to its exception type. - README Harness says so. Tests: tests/test_error_redaction.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… ones (review skillberry-ai#874, 2) AppWorld's SafetyGuard (42b5bcf) checks literal imports and a list of function paths only. Allowed modules hand out blocked ones as attributes, so `import uuid; uuid.os.environ` got through. Dunder walks, frame access and getattr(module, "o" + "s") did too. - harden_guard() wraps SafetyGuard.is_syntax_safe (idempotent). After AppWorld's own check, code_guard_violations() refuses: .os/.sys/.io/... attributes; dunders other than __name__/__qualname__/__doc__/__module__/__init__; frame attributes; eval/exec/compile/globals/locals/vars/__import__/__builtins__; getattr with a blocked constant or on a module; and IPython !/% lines. The refusal reads like AppWorld's own "Usage of ... is not allowed". The seed prompt's examples, the setup snippet and getattr(apis, app_name) pass. - Each episode records guard_blocked (refused steps) and guard_probe. final_window.py keeps them and analyze.py reports them per arm. - README Harness says this is defence in depth, not a sandbox, and lists what remains. Tests: tests/test_code_guard.py (bypasses, legitimate code, the seed prompt, and harden_guard against a stand-in for AppWorld's module). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…workdir (review skillberry-ai#874, 6) rel="${WORK#"$AW"/}" left an absolute path when the working dir was $AW_EVAL itself, so the bind landed under $AW/$AW and the real cwd was unbound. AW was also not resolved while WORK was (pwd -P), so a symlinked AW_EVAL failed the guard. - AW is resolved with readlink -f. - A working dir equal to $AW is refused (exit 97): binding all of it read-write would bring root_opt back. - Inside the namespace, an empty or absolute rel aborts the call. Tests: tests/test_sandbox_shim.py runs the shim for real (unprivileged user and mount namespaces, skipped where they are unavailable) with a stand-in claude. The cwd == $AW_EVAL and symlinked-AW_EVAL cases fail before this change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s fallback (review skillberry-ai#874, 8) The review asked why the example hand-rolls mount-namespace isolation instead of using Claude Code's built-in sandbox. The namespace path is still needed where bubblewrap cannot start (containers whose /dev/null is bound from the host), so it stays, but as the fallback. - sandbox/claude is now a ~40-line dispatcher on AW_ISOLATION. In sandbox mode (the default) it passes the rendered optimizer-sandbox.json to the real CLI and requires failIfUnavailable. namespace mode runs sandbox/ns_exec.sh (the former shim, unchanged apart from taking the CLI path as $1). It fails closed without a settings file and finds the real claude on PATH past itself. - Both settings templates also deny AppWorld's generate/ and the secrets file, as the namespace mode already hid them. - check_optimizer_isolation.sh runs both modes through the same shim. launch_cape_hc.sh refuses to start without a passing check for its mode that is newer than the rendered settings and the shim (scripts/isolation_gate.py). - README Isolation leads with the built-in sandbox. Tests: tests/test_isolation_modes.py (dispatch, fail-closed, template parity, launch gate). tests/test_sandbox_shim.py now drives namespace mode through the dispatcher. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…view skillberry-ai#874, 7) analyze.py had its own percentile bootstrap for the scenario-clustered paired CI of ΔTGC and ΔSGC. With equal-size scenarios (test_normal: 56 x 3 tasks), both differences are the mean over scenarios of a per-scenario paired difference. Resampling scenarios is then exactly bootstrap_ci over those 56 values. - scenario_diffs() computes the per-scenario differences; paired_cis() asserts equal-size scenarios and calls bootstrap_ci (10,000 resamples, seed 0). - bootstrap() and its RNG are gone. CI endpoints can move slightly against numbers from the old code (different random stream); re-run analyze.py on an existing results.jsonl to refresh them. Tests: tests/test_analyze_bootstrap.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d's agent (review skillberry-ai#874, 9) The review could not check that the 10 keyword arguments passed to SimplifiedReActCodeAgent match upstream. They do at 42b5bcf (agent.py Agent.__init__ and react_code_agent.py). A future pin change would surface only as an opaque infra_error. - aw_agent.agent_kwargs() builds the arguments in one place, and unaccepted_kwargs() walks the MRO's __init__ signatures to name any argument nothing accepts. run_episode raises AgentConstructionError with those names before construction, and labels a construction TypeError. - check_injection.py (A4) runs the check first (A4.0). README: re-run it after changing the AppWorld pin. Tests: tests/test_agent_signature.py, against copies of the two upstream signatures at 42b5bcf plus a drifted one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…erry-ai#874, 2) A second pass over finding 2 in AppWorld's real REPL found blocked modules still reachable by four routes: - private aliases in allowed modules: random._os, collections._sys, enum.bltns, fnmatch.posixpath (42 aliases in all) - dynamic importers: pydoc.locate("os"), pydoc.resolve, unittest.mock.patch("os...") - yaml tags that build any object: yaml.load("!!python/name:os.environ") - attribute walks from a string: operator.attrgetter("_os.environ"), "{0._os.environ[K]}".format(m) - code_guard_violations refuses every private (`_x`) attribute and getattr constant, the public aliases (posix, posixpath, ntpath, genericpath, bltns), attrgetter/methodcaller, and .format/.format_map strings with an attribute or index field (or a non-literal format string). Plain "{}" and "{name:d}" fields and f-strings still pass. - pydoc, pydoc_data, unittest and yaml are removed from AppWorld's import allowlist. The seed prompt's code imports none of them. Verified in the real REPL: all 42 aliases and every route above are refused, and no planted secret leaks. A scripted agent episode (API docs, Spotify login, playlist listing, json/datetime/re/format, complete_task) runs with 0 guard blocks. check_injection.py (A4) passes. Tests: tests/test_aw_agent.py (bypass and legitimate cases). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks for the review. All nine findings are fixed on this branch, one commit per finding (each with the tests that show it), plus a second pass on #2. Everything was checked against AppWorld at the pin (
1. Lint gate. Confirmed. The step that built the lint cache was lost when this PR was cut. 2. Attribute bypass. Confirmed.
All of these are refused now:
In the real REPL, all 42 aliases of a blocked module across AppWorld's allowed modules are refused, and no planted secret leaks. A scripted agent episode doing normal work (API docs, login, listing, A Python-level guard can't be complete, and the README now says so. The other controls are #5, the per-episode counters 3. Traceback. Confirmed, though the exposure was narrower than stated. The 4. Readiness wait. Fixed: 5. Secrets. Confirmed, and the bigger leak was the adapter: it gave the worker the whole 6. Prefix strip. Fixed. A working dir equal to 7. Bootstrap. Fixed. With equal-size scenarios (56 × 3), the clustered paired ΔTGC and ΔSGC are means over scenarios of per-scenario differences, so 8. Hand-rolled sandbox. Partly agreed. Claude Code's built-in sandbox is now the default ( 9. Constructor compatibility. Checked against the source at Items you dropped:
Other changes in this push:
Not verified yet: a scored end-to-end run. RITS is unreachable from my machine, so the itest finished every step but scored nothing. Every episode timed out, and the asserter correctly failed it rather than passing a 0.0-vs-0.0 "no regression". The first 🤖 Generated with Claude Code |
…olve A cap-evolve adapter and harness for AppWorld's official simplified ReAct code agent, an ALTK-Evolve arm on the same harness, and the scripts for an honest comparison: official splits only, one interleaved final window, gold-free feedback, candidate lints, and optimizer isolation. - adapters/: adapter (one subprocess per episode), CapReActAgent (prompt, REPL helpers, API notes, memory slot; byte-identical to stock when empty), lints. REPL hardening and a per-task token cap applied to every arm. - altk/: mine, consolidate, render and select ALTK-Evolve doses. - sandbox/: a claude wrapper that runs every optimizer call in a private mount namespace with only an allowlist visible; settings templates for Claude Code's own sandbox where it can start. - scripts/: data roots, splits, acceptance checks (incl. a planted-secret isolation check), launch, final window, analysis, isolation scan. - tests/: 15 offline tests (no AppWorld install, no model call). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…al-window dir; reuse frozen artifacts The optimizer instructions pointed at the project's copy of lints.py, which lives under $AW_EVAL and is hidden by the namespace wrapper, so the optimizer could not lint its own candidate. Point at the example's copy instead. AW_FINAL selects the final-window dir so a re-run keeps an earlier window; AW_REUSE_ARTIFACTS keeps the seed and altk artifacts byte-identical. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The wrapper's allowlist exposed the whole AppWorld clone, and appworld/generate holds the task generators: the solution and evaluation logic of every train/dev scenario. An optimizer subagent read six of them. Expose only src/ and experiments/ (all the venv's editable installs need), and plant an A9 secret in the generator dir. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…h (review skillberry-ai#874, 1) lints.py read APPWORLD_ROOT with a hardcoded /tmp/aw_eval fallback, while adapter.py reads AW_APPWORLD_ROOT. Nothing built the lint cache, and the optimizer was told to lint against root_opt, which its namespace hides. A cold cache raised FileNotFoundError instead of printing {"ok": ...}. - lints.py resolves the cache (AW_LINT_CACHE, default $AW_EVAL/lint_cache.json) and the build root (AW_LINT_ROOT, default $AW_EVAL/root_opt). It never follows the episode root, which is root_full in the final window. - Missing cache and unreadable root raise LintDataUnavailable. The CLI prints "ok": false with "lint data unavailable" and exits 2. - make_roots.py writes the cache from root_opt (mode 0644, sha256 in the manifest). - setup_project.sh, launch_cape_hc.sh and check_optimizer_isolation.sh refuse to start without the cache. The optimizer instructions point at the cache, not root_opt. - Tests: tests/test_lint_data_path.py. The shared fixture moves to tests/aw_testlib.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s (review skillberry-ai#874, 4) The lint ran before _wait_readable, unlike every other root read in the adapter. Wait first, then lint. The _wait_readable docstring said root_opt is locked while the optimizer runs; nothing locks it any more (the namespace wrapper isolates the optimizer), so it now says what the wait is for. A comment says Adapter.tasks() reads root_opt on purpose. Test: tests/test_run_trials_order.py (fails before this change). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rops it (review skillberry-ai#874, 5) load_secrets() set every KEY=VALUE line of the secrets file, but only the configured key was removed afterwards. The adapter also gave the worker the whole `cap-evolve run` environment minus that one key, including the optimizer's credentials. - load_secrets() sets only the agent key (AW_KEY_ENV) and the endpoint URL and returns what it set. drop_secrets() removes every secret name once the client is built. - aw_agent.worker_env() builds the worker's environment from an allowlist (WORKER_ENV_NAMES plus LC_*/APPWORLD_*). The adapter uses it in _episode, so the final window is covered too. - README: Setup step 4 and Harness describe this. Tests: tests/test_worker_secrets.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… (review skillberry-ai#874, 3) main() wrote str(e) and traceback.format_exc() into the record. The message reached the optimizer through Rollout.error and the score feedback, and a crashed worker's stderr did too. An exception raised in world.evaluate() can carry gold values. - run_episode tracks its phase (setup / episode / evaluate). redact_error() reports the exception type, phase and frames (file:line in func, no source). The message is kept only outside evaluate, with the root, candidate, secrets file and key replaced, capped at 300 chars. - The raw traceback is gone from the record. With AW_KEEP_OUTPUTS=1 it is written next to AppWorld's own outputs, which are kept only in that mode. - adapter._episode reduces a crashed worker's stderr to its exception type. - README Harness says so. Tests: tests/test_error_redaction.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… ones (review skillberry-ai#874, 2) AppWorld's SafetyGuard (42b5bcf) checks literal imports and a list of function paths only. Allowed modules hand out blocked ones as attributes, so `import uuid; uuid.os.environ` got through. Dunder walks, frame access and getattr(module, "o" + "s") did too. - harden_guard() wraps SafetyGuard.is_syntax_safe (idempotent). After AppWorld's own check, code_guard_violations() refuses: .os/.sys/.io/... attributes; dunders other than __name__/__qualname__/__doc__/__module__/__init__; frame attributes; eval/exec/compile/globals/locals/vars/__import__/__builtins__; getattr with a blocked constant or on a module; and IPython !/% lines. The refusal reads like AppWorld's own "Usage of ... is not allowed". The seed prompt's examples, the setup snippet and getattr(apis, app_name) pass. - Each episode records guard_blocked (refused steps) and guard_probe. final_window.py keeps them and analyze.py reports them per arm. - README Harness says this is defence in depth, not a sandbox, and lists what remains. Tests: tests/test_code_guard.py (bypasses, legitimate code, the seed prompt, and harden_guard against a stand-in for AppWorld's module). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…workdir (review skillberry-ai#874, 6) rel="${WORK#"$AW"/}" left an absolute path when the working dir was $AW_EVAL itself, so the bind landed under $AW/$AW and the real cwd was unbound. AW was also not resolved while WORK was (pwd -P), so a symlinked AW_EVAL failed the guard. - AW is resolved with readlink -f. - A working dir equal to $AW is refused (exit 97): binding all of it read-write would bring root_opt back. - Inside the namespace, an empty or absolute rel aborts the call. Tests: tests/test_sandbox_shim.py runs the shim for real (unprivileged user and mount namespaces, skipped where they are unavailable) with a stand-in claude. The cwd == $AW_EVAL and symlinked-AW_EVAL cases fail before this change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s fallback (review skillberry-ai#874, 8) The review asked why the example hand-rolls mount-namespace isolation instead of using Claude Code's built-in sandbox. The namespace path is still needed where bubblewrap cannot start (containers whose /dev/null is bound from the host), so it stays, but as the fallback. - sandbox/claude is now a ~40-line dispatcher on AW_ISOLATION. In sandbox mode (the default) it passes the rendered optimizer-sandbox.json to the real CLI and requires failIfUnavailable. namespace mode runs sandbox/ns_exec.sh (the former shim, unchanged apart from taking the CLI path as $1). It fails closed without a settings file and finds the real claude on PATH past itself. - Both settings templates also deny AppWorld's generate/ and the secrets file, as the namespace mode already hid them. - check_optimizer_isolation.sh runs both modes through the same shim. launch_cape_hc.sh refuses to start without a passing check for its mode that is newer than the rendered settings and the shim (scripts/isolation_gate.py). - README Isolation leads with the built-in sandbox. Tests: tests/test_isolation_modes.py (dispatch, fail-closed, template parity, launch gate). tests/test_sandbox_shim.py now drives namespace mode through the dispatcher. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…view skillberry-ai#874, 7) analyze.py had its own percentile bootstrap for the scenario-clustered paired CI of ΔTGC and ΔSGC. With equal-size scenarios (test_normal: 56 x 3 tasks), both differences are the mean over scenarios of a per-scenario paired difference. Resampling scenarios is then exactly bootstrap_ci over those 56 values. - scenario_diffs() computes the per-scenario differences; paired_cis() asserts equal-size scenarios and calls bootstrap_ci (10,000 resamples, seed 0). - bootstrap() and its RNG are gone. CI endpoints can move slightly against numbers from the old code (different random stream); re-run analyze.py on an existing results.jsonl to refresh them. Tests: tests/test_analyze_bootstrap.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d's agent (review skillberry-ai#874, 9) The review could not check that the 10 keyword arguments passed to SimplifiedReActCodeAgent match upstream. They do at 42b5bcf (agent.py Agent.__init__ and react_code_agent.py). A future pin change would surface only as an opaque infra_error. - aw_agent.agent_kwargs() builds the arguments in one place, and unaccepted_kwargs() walks the MRO's __init__ signatures to name any argument nothing accepts. run_episode raises AgentConstructionError with those names before construction, and labels a construction TypeError. - check_injection.py (A4) runs the check first (A4.0). README: re-run it after changing the AppWorld pin. Tests: tests/test_agent_signature.py, against copies of the two upstream signatures at 42b5bcf plus a drifted one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…verified Tests: one file per module under test (test_adapter, test_aw_agent, test_lints, test_sandbox, test_analyze, test_altk) instead of one file per review finding plus a shared helper module. Each file sets up its own sys.path the way examples/parsec/tests does, with core/ appended as a fallback, so `python3 examples/appworld/tests/<file>.py` runs from a clean shell without an installed cap-evolve. The same 65 test methods; classes are named Test<Thing>. README Setup step 1 now has the install that a from-scratch smoke run used. "-e core" named a directory AppWorld does not have. cap-evolve itself must be in venv-aw, because launch_cape_hc.sh runs venv-aw/bin/cap-evolve. AppWorld 42b5bcf leaves fastapi and starlette unpinned and breaks with fastapi >= 0.155 (Path(embed=)) and starlette < 0.48 (HTTP_422_UNPROCESSABLE_CONTENT). With fastapi 0.135.0 and starlette >= 0.48, check_injection.py passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ALTK-Evolve is a competing optimizer, not a benchmark, so it does not belong in a benchmark example. The example now compares two arms, the stock seed and cape-hc. - Deleted altk/ and its test, and seed_capability/memory/, which only the ALTK arm filled. - The harness loses the memory block: CapReActAgent has no memory_block argument, the adapter no _memory_block, the lints no memory lint, and check_injection.py no A4.2 placement check. With no helpers and no notes the first request is still byte-identical to stock (A4.1). - final_window.py freezes and runs seed and cape-hc only. analyze.py has one pair (cape-hc - seed). status.py and scan_isolation.py drop the altk entries; awpaths drops ALTK_PYTHON. - The sandbox templates and the isolation check stop listing runs/altk. - README: two arms, no ALTK setup step or section. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
scripts/itest.sh is the example's end-to-end regression check, modelled on
examples/tau2_airline/{itest.sh, assert_itest.py, capevolve.itest.yaml}: one
train task (82e2fac_1, difficulty 1), 1 trial, 1 iteration, and the same budget
keys as tau2's itest spec. It runs the real launch path: setup_project.sh
(cap-evolve check), a fresh isolation check, then launch_cape_hc.sh, which now
takes AW_SPEC for a spec other than capevolve.yaml.
scripts/assert_itest.py makes tau2's three assertions (completed, an iteration
ran, final test >= baseline val) and adds two:
- episodes ran: no rollout is an infrastructure error or a lint reject. A broken
key or model scores 0.0 like a failed task and would otherwise pass as
non-regression.
- gold-free: no rollout names a gold file, and AppWorld's failure `trace` is
absent from the evaluation fields. cap-evolve's own Rollout.trace (the agent's
steps) is allowed.
`itest.sh --dry-run` covers everything up to the first model call. Run here
against a real AppWorld install, it passes. The full run was not done here (no
agent-model key on this host).
Tests: tests/test_itest.py.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The appworld example's one-task regression (train task 82e2fac_1, 1 trial, 1 iteration) as a leg of the Integration tests workflow, next to tau2 and the tau2_custom arms. Dispatch only: bench = appworld. It is not in benchmarks.yml. - ci_setup.sh appworld does the example's README Setup steps 1-2 in the runner cache ($CAPEVOLVE_CI_CACHE/appworld, exported as AW_EVAL). It clones AppWorld at 42b5bcf, pulls the LFS bundles, applies the header patch, installs into its own Python 3.11 venv with the fastapi/starlette pins, downloads the data, builds root_opt and the lint cache, and writes the reference docs. Each step is skipped when its output is cached. It ends with the A4 injection check (no model call). git-lfs is required. - run_suite.sh appworld copies the example's adapter, seed and rendered optimizer instructions, and writes the spec keys the example's capevolve.yaml sets. The agent reaches AGENT_MODEL through its provider's LiteLLM gateway with the gateway's bearer key, which goes into a mode-600 file that each episode worker reads and drops, never into the optimizer's environment. Every optimizer call goes through the example's sandbox/claude shim with the rendered settings. - The workflow sets AGENT_MODEL=gpt-oss-120b-a100 (RITS) for this leg, because ci_setup.sh's provider preflight reads AGENT_MODEL and does not load tier overrides. After assert_run.py it runs the example's assert_itest.py. - aw_agent.py: AW_KEY_HEADER=bearer sends the key as the standard API key (api_key_env_name) instead of a named header; RITS's header stays the default. Verified here: provisioning from an empty cache (77 s) and warm (26 s, every step skipped); cap-evolve check passes on the project run_suite.sh generates; one real AppWorld episode through the gateway path, with a local stand-in gateway, sent `Authorization: Bearer` and the rits/ wire model id. Tests: core/tests/test_benchmarks_appworld_leg.py drives run_suite.sh appworld with a stub interpreter (no model call) and checks the project, spec, secrets file, settings, the missing-cache failure, the provisioning steps and the workflow entry. examples/appworld/tests/test_aw_agent.py covers both auth modes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…erry-ai#874, 2) A second pass over finding 2 in AppWorld's real REPL found blocked modules still reachable by four routes: - private aliases in allowed modules: random._os, collections._sys, enum.bltns, fnmatch.posixpath (42 aliases in all) - dynamic importers: pydoc.locate("os"), pydoc.resolve, unittest.mock.patch("os...") - yaml tags that build any object: yaml.load("!!python/name:os.environ") - attribute walks from a string: operator.attrgetter("_os.environ"), "{0._os.environ[K]}".format(m) - code_guard_violations refuses every private (`_x`) attribute and getattr constant, the public aliases (posix, posixpath, ntpath, genericpath, bltns), attrgetter/methodcaller, and .format/.format_map strings with an attribute or index field (or a non-literal format string). Plain "{}" and "{name:d}" fields and f-strings still pass. - pydoc, pydoc_data, unittest and yaml are removed from AppWorld's import allowlist. The seed prompt's code imports none of them. Verified in the real REPL: all 42 aliases and every route above are refused, and no planted secret leaks. A scripted agent episode (API docs, Spotify login, playlist listing, json/datetime/re/format, complete_task) runs with 0 guard blocks. check_injection.py (A4) passes. Tests: tests/test_aw_agent.py (bypass and legitimate cases). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
22ba65e to
11c6efa
Compare
Adds
examples/appworld: a ready-to-run AppWorld benchmark for cap-evolve. It includes an ALTK-Evolve arm on the same harness, so cap-evolve can be compared with another learning method under equal conditions. It also closes an isolation hole that let the optimizer read benchmark answers.Value
claudewrapper runs every optimizer call in a private mount namespace in which the answers simply do not exist. A planted-secret check proves this before a run.Evidence (smoke test, 13–15 Oct 2026: gpt-oss-120b agent, Claude Sonnet 5.5 optimizer)
Detailed log: issue #804 (interim updates 1–6 and summary). Plan, findings (Appendix A) and results (Appendix B):
docs/EVAL_SMOKE_TEST.mdon the fork brancherezh-eval1. Plain-language summary:docs/EVAL_SMOKE_TEST_SUMMARY.html.The example ran end to end. Acceptance checks A1–A8 passed:
Held-out results (test_normal, 168 tasks × 3 arms × 2 trials = 1,008 episodes, 0 infra errors):
⚠ The first run is labelled isolation breach. In round 2, the optimizer's subagents read train/dev ground truth and ran AppWorld's evaluator; no test data was reached. That breach is why the isolation in this PR exists. A clean re-run with it is planned.
AppWorld gaps found and handled in the harness, for every arm:
os,pathlib,ioandos.environ. Agent code could list and read files, including ground truth, and read env vars. Those modules are now blocked, and the key is dropped from the environment.max_output_tokens_per_taskis never enforced at42b5bcf, so the episode loop enforces it.traceholds gold values. Only requirement texts and counters are kept, and native output dirs are deleted.42b5bcf(pydantic 2.12).ALTK-Evolve integration:
tool_choice, so a process-local shim routes the library to its own prompt-JSON path. The library itself is unmodified.Isolation (check A9, plants 6 secrets and runs a real optimizer call that tries cat, Read, Grep,
find /, a copy and a sandbox escape):sandbox/claudenamespace wrapper/dev/nullbind cannot be remounted. 0 secrets found, but no shell command can runTests:
python -m pytest examples/appworld/testsgives 15 passed (lints, injection, gold-free record, scoring, ALTK conversion), with no AppWorld install or model needed.The framework-level gaps that this example works around are filed as separate issues.
🤖 Generated with Claude Code