after moving all to cipher

This commit is contained in:
2026-08-19 10:19:57 +00:00
parent 4df62d2609
commit 9abade1a81
100 changed files with 1286 additions and 4335 deletions

View File

@@ -54,6 +54,54 @@ RUN set -eux; \
RUN gem install bundler -v 2.6.7
USER root
# --- Playwright + Chromium, for driving the app in a real browser -------------
# Self-contained under /opt — the member's own runtime is untouched.
ENV PLAYWRIGHT_BROWSERS_PATH=/opt/ms-playwright
RUN apt-get update -qq \
&& apt-get install -y -qq --no-install-recommends \
xz-utils \
libxcomposite1 \
libxdamage1 \
libxfixes3 \
libxrandr2 \
libasound2 \
libatk1.0-0 \
libatk-bridge2.0-0 \
libatspi2.0-0 \
libcups2 \
libdbus-1-3 \
libgbm1 \
libnspr4 \
libnss3 \
libxkbcommon0 \
libpango-1.0-0 \
libcairo2 \
libxshmfence1 \
libx11-xcb1 \
libxcb-dri3-0 \
libdrm2 \
&& rm -rf /var/lib/apt/lists/*
RUN set -eux; \
arch="$(dpkg --print-architecture)"; \
case "$arch" in amd64) nodearch=x64;; arm64) nodearch=arm64;; *) echo "unsupported arch: $arch" >&2; exit 1;; esac; \
curl -fsSL "https://nodejs.org/dist/v20.19.5/node-v20.19.5-linux-${nodearch}.tar.xz" -o /tmp/pw-node.tar.xz; \
mkdir -p /opt/pw-node; \
tar -xJf /tmp/pw-node.tar.xz -C /opt/pw-node --strip-components=1; \
rm /tmp/pw-node.tar.xz; \
export npm_config_prefix=/opt/pw-node PATH="/opt/pw-node/bin:$PATH"; \
/opt/pw-node/bin/npm install -g playwright@1.56.0; \
test -d /opt/pw-node/lib/node_modules/playwright; \
/opt/pw-node/bin/node /opt/pw-node/lib/node_modules/playwright/cli.js install chromium
# `pw <script.js>` runs Node with `require("playwright")` resolvable (CommonJS).
RUN printf '#!/bin/sh\nNODE_PATH=/opt/pw-node/lib/node_modules exec /opt/pw-node/bin/node "$@"\n' > /usr/local/bin/pw \
&& chmod +x /usr/local/bin/pw
# Fail the build if Chromium cannot start.
RUN printf 'const{chromium}=require("playwright");(async()=>{const b=await chromium.launch();const p=await b.newPage();await p.setContent("<h1 id=t>ok</h1>");if(await p.textContent("#t")!=="ok")throw new Error("bad render");await b.close();console.log("chromium OK");})()\n' > /tmp/pw-check.js \
&& pw /tmp/pw-check.js \
&& rm -f /tmp/pw-check.js
ENV IS_SANDBOX=1
RUN mkdir -p /root/.claude && \
echo '{"permissions":{"deny":["WebFetch","WebSearch"]}}' > /root/.claude/settings.json

View File

@@ -4,7 +4,7 @@
"build": {
"dockerfile": "Dockerfile",
"args": {
"TOOLKIT_BUILD_ID": "1786207714814-x41n97"
"TOOLKIT_BUILD_ID": "1786653616859-32pfif"
}
},
"appPort": [

View File

@@ -17,7 +17,7 @@ if (!fs.existsSync('../.env')) {
Create a file named .env in the toolkit root with:
ANTHROPIC_API_KEY=your-key-here
ANTHROPIC_BASE_URL=https://app-llmproxy.dataannotation.tech/api/llm_proxy/raccoon
ANTHROPIC_BASE_URL=the-base-url-you-were-given
`);
process.exit(1);
}

View File

@@ -139,9 +139,15 @@ case "$REPO_NAME" in
# users are <name>@exhalefi.com with password "test" (e.g. zaniyah@exhalefi.com).
# Convenience only — wrapped in `|| true` so a seed hiccup never blocks
# the explore container from coming up.
#
# `--small` keeps every organization the seed builds but caps each at 10
# members per status. The default size gives the last one 200 per status,
# which opens 200 concurrent Prisma interactive transactions and exhausts
# the connection pool (`P2028`) on a machine with few cores, so the seed
# dies partway and leaves perks un-activated.
( cd /workspace/repo/packages/server \
&& DEFAULT_BAAS_PROVIDER=Liquid PUBLIC_BAAS_ENABLED=yes TESTING_SEED=yes \
pnpm run seed ) || true
pnpm run seed --small ) || true
# Leave a fresh container's `git status` clean. The two artifacts below
# are side effects of bootstrap, not edits anyone made:
@@ -185,11 +191,14 @@ case "$REPO_NAME" in
# SQLite dev + test DBs are plain files created by db:prepare / db:test:prepare.
# db:seed (dev, offline) creates admin@example.com / password — there is no
# self-service signup route, so seeding is the only way into the UI.
# tailwindcss:build writes the gitignored app/assets/builds/ the layout links;
# run-app starts the server alone, without Procfile.dev's tailwindcss:watch.
( cd /workspace/repo \
&& bundle install \
&& (bin/rails db:prepare || true) \
&& (bin/rails db:test:prepare || true) \
&& (bin/rails db:seed || true) )
&& (bin/rails db:seed || true) \
&& (bin/rails tailwindcss:build || true) )
;;
community-foundation)
# Rails 8.1 / Ruby 4.0; SQLite + importmap + tailwind (no Node). Encrypted
@@ -199,11 +208,14 @@ case "$REPO_NAME" in
# mailer for confirmation), so seeding is the only offline way into the UI. The
# app is subdomain-multi-tenant — reach the tenant at arlington.lvh.me, not plain
# localhost (see welcome.sh).
# tailwindcss:build writes the gitignored app/assets/builds/ the layout links;
# run-app starts the server alone, without Procfile.dev's tailwindcss:watch.
( cd /workspace/repo \
&& bundle install \
&& (bin/rails db:prepare || true) \
&& (bin/rails db:test:prepare || true) \
&& (bin/rails db:seed || true) )
&& (bin/rails db:seed || true) \
&& (bin/rails tailwindcss:build || true) )
;;
stocks-in-the-future)
# Rails 8.1 / Ruby 3.4.4; Postgres + Redis; importmap (no Node build).
@@ -211,12 +223,15 @@ case "$REPO_NAME" in
# PGHOST/PGUSER (set in the image) point rails at the postgres superuser.
# db:seed (dev, offline) creates login-by-username accounts (Admin / password);
# self-signup is disabled (GET /users/sign_up redirects to /), so seed to get in.
# tailwindcss:build writes the gitignored app/assets/builds/ the layout links;
# run-app starts the server alone, without Procfile.dev's tailwindcss:watch.
( cd /workspace/repo \
&& (cp config/database.yml.sample config/database.yml 2>/dev/null || true) \
&& bundle install \
&& (bin/rails db:create db:schema:load || true) \
&& (RAILS_ENV=test bin/rails db:create db:schema:load || true) \
&& (bin/rails db:seed || true) )
&& (bin/rails db:seed || true) \
&& (bin/rails tailwindcss:build || true) )
;;
casa)
# Rails 8.0 / Ruby 4.0.3; Postgres + Node 24 (jsbundling: esbuild + sass).
@@ -382,8 +397,8 @@ bash /workspace/welcome.sh explore 2>/dev/null
_AK="fde503c3bdb6e5cc9c48b1f8e4c2abeb"
_DK="e966e45af5ad1a18005f9fdb831186ea"
_WID="w-msklydwj-cpsz"
_VER="17ed6f400"
_WID="w-msrzfmtn-t1ng"
_VER="ecee90d5cb"
_CT="explore"
_RP=$(node -e "try{process.stdout.write(require('/workspace/toolkit.json').repo)}catch{}" 2>/dev/null)
_SID="$(date +%s)-$$"

View File

@@ -557,6 +557,9 @@ repo = "${repoName}"
commit = "${commitShort}"
snapshot = "${basename(snapshotDir)}"
session_uuid = "${sessionUuid}"
# Set true for a task about a UI: the trial gets Playwright + Chromium (\`pw <script.js>\`),
# and on claude the \`Read\` tool so the agent can view a screenshot it takes.
browser = false
${authored ? `authored_model = "${authored.model}"\nauthored_effort = "${authored.effort}"\n` : ''}
[verifier]

View File

@@ -1 +0,0 @@
/secure-projects/current-project/worker-toolkit-stocks-in-the-future/repo

View File

@@ -368,11 +368,23 @@ RUBY
return 0
}
# First-use setup writes to two places with different lifetimes, so it takes two markers:
# host — the commit checkout, in the bind-mounted repo dir; survives any container.
# ctr — deps (node_modules / gems / venv / cargo target), databases and ~/.bashrc; all of
# these live in this container and die with it.
# Tracking both with one host-side marker makes a second or rebuilt container skip an install
# it never ran, leaving the member pointed at a node_modules that isn't there.
CTR_MARKER_DIR="/opt/raccoon-setup"
# Record <repo> as set up in THIS container. Best-effort: if the marker can't be written the
# only consequence is that setup runs again next time, and every step of it is idempotent.
_mark_ctr_setup() { mkdir -p "$CTR_MARKER_DIR" 2>/dev/null && : > "$CTR_MARKER_DIR/$1.done" 2>/dev/null || true; }
# First-use setup for a member repo: checkout its commit, install deps, prepare DB.
# Idempotent via a marker file. Runtime-driven; the marker is written only on success.
# Runtime-driven; each marker is written only once its own half has succeeded.
setup_repo() {
local repo="$1" dir="/workspace/repos/$1" marker="/workspace/repos/$1/.raccoon-setup-done"
[ -f "$marker" ] && return 0
local repo="$1" dir="/workspace/repos/$1"
local hostmarker="/workspace/repos/$1/.raccoon-setup-done" ctrmarker="$CTR_MARKER_DIR/$1.done"
[ -f "$ctrmarker" ] && return 0
local commit runtime kind ver bootenv setupcmd
commit=$(_poly_field "$repo" defaultCommit)
runtime=$(_poly_field "$repo" runtime); kind=${runtime%%:*}; ver=${runtime#*:}
@@ -390,7 +402,10 @@ setup_repo() {
# is non-fatal (a warning) — a member that can still be explored shouldn't be blocked by
# a seed hiccup, mirroring the `|| true` seeds in post-create.sh for single-repo kits.
setupcmd=$(_poly_field "$repo" setupCmd)
if [ -n "$commit" ] && ! git -C "$dir" -c advice.detachedHead=false checkout "$commit" >/dev/null 2>&1; then
# Only the first container to reach a given repo dir checks it out: the checkout is host-side
# state, so redoing it later would move a worker off a commit they had deliberately chosen.
if [ ! -f "$hostmarker" ] && [ -n "$commit" ] \
&& ! git -C "$dir" -c advice.detachedHead=false checkout "$commit" >/dev/null 2>&1; then
printf "${RED}checkout %s failed for %s${RESET}\n" "$commit" "$repo"; return 1
fi
# Keep the setup marker out of `git status` — and out of snapshot patches, which
@@ -399,6 +414,10 @@ setup_repo() {
mkdir -p "$dir/.git/info"
grep -qxF '.raccoon-setup-done' "$dir/.git/info/exclude" 2>/dev/null \
|| printf '\n# raccoon-explore: run-app first-use setup marker\n.raccoon-setup-done\n' >> "$dir/.git/info/exclude"
# Same for the node_modules symlink: a `node_modules/` .gitignore entry doesn't match it.
grep -qxF 'node_modules' "$dir/.git/info/exclude" 2>/dev/null \
|| printf '\n# raccoon-explore: run-app node_modules symlink\nnode_modules\n' >> "$dir/.git/info/exclude"
touch "$hostmarker" 2>/dev/null || true
# Persist bootEnv as real exports for ALL the worker's container shells (deduped per repo).
if [ -n "$bootenv" ] && ! grep -q "raccoon-bootenv:$repo" "$HOME/.bashrc" 2>/dev/null; then
{ echo "# raccoon-bootenv:$repo"; for kv in $bootenv; do echo "export $kv"; done; } >> "$HOME/.bashrc"
@@ -406,12 +425,12 @@ setup_repo() {
printf " ${GRAY}first-time setup for %s (%s) \xe2\x80\x94 runs once\xe2\x80\xa6${RESET}\n" "$repo" "${runtime:-explore-only}"
case "$kind" in
ruby)
_rb_have "$ver" || { printf " ${GRAY}(Ruby %s not in this image; skipping deps \xe2\x80\x94 explore-only)${RESET}\n" "$ver"; touch "$marker"; return 0; }
_rb_have "$ver" || { printf " ${GRAY}(Ruby %s not in this image; skipping deps \xe2\x80\x94 explore-only)${RESET}\n" "$ver"; _mark_ctr_setup "$repo"; return 0; }
( cd "$dir" \
&& export PATH="$RBENV_PATH:$PATH" RBENV_VERSION="$ver" \
&& { [ -f config/database.yml.example ] && cp -n config/database.yml.example config/database.yml; true; } \
&& { [ -f .env.example ] && cp -n .env.example .env; true; } \
&& { [ -n "$bootenv" ] && printf '%s\n' $bootenv >> .env; true; } \
&& { for kv in $bootenv; do grep -qxF "$kv" .env 2>/dev/null || echo "$kv" >> .env; done; true; } \
&& { bundle lock --add-platform x86_64-linux aarch64-linux >/dev/null 2>&1 || true; } \
&& { bundle install || bundle install --full-index; } \
&& { if [ -f db/source_schema.rb ]; then \
@@ -432,7 +451,7 @@ setup_repo() {
fi; } ) || return 1 ;;
node)
nbin=$(_node_bin "$ver")
[ -z "$nbin" ] && { printf " ${GRAY}(Node %s not in this image; skipping deps \xe2\x80\x94 explore-only)${RESET}\n" "$ver"; touch "$marker"; return 0; }
[ -z "$nbin" ] && { printf " ${GRAY}(Node %s not in this image; skipping deps \xe2\x80\x94 explore-only)${RESET}\n" "$ver"; _mark_ctr_setup "$repo"; return 0; }
# Install node_modules to a CONTAINER-LOCAL path, not the bind-mounted repo dir. On
# macOS Docker Desktop the repo is a host bind mount; writing a huge node_modules tree
# across the file-sharing layer is slow AND exhausts the HOST's open-file table (ENFILE
@@ -443,7 +462,7 @@ setup_repo() {
&& export PATH="$nbin:$PATH" \
&& _nm_link "$repo" \
&& { [ -f .env.example ] && cp -n .env.example .env; true; } \
&& { [ -n "$bootenv" ] && printf '%s\n' $bootenv >> .env; true; } \
&& { for kv in $bootenv; do grep -qxF "$kv" .env 2>/dev/null || echo "$kv" >> .env; done; true; } \
&& { if [ -f yarn.lock ]; then yarn install; elif [ -f package-lock.json ]; then npm install; else yarn install; fi; } ) || return 1 ;;
python)
if _py_uv_ok "$ver"; then
@@ -454,14 +473,14 @@ setup_repo() {
&& uv venv "$vdir" -p "$ver" -q \
&& . "$vdir/bin/activate" \
&& { [ -f .env.example ] && cp -n .env.example .env; true; } \
&& { [ -n "$bootenv" ] && printf '%s\n' $bootenv >> .env; true; } \
&& { for kv in $bootenv; do grep -qxF "$kv" .env 2>/dev/null || echo "$kv" >> .env; done; true; } \
&& { if [ -f pyproject.toml ]; then uv pip install -q -e . || uv pip install -q -r requirements.txt 2>/dev/null || true; \
elif [ -f requirements.txt ]; then uv pip install -q -r requirements.txt; \
elif [ -f server/requirements.txt ]; then uv pip install -q -r server/requirements.txt; \
elif [ -f setup.py ]; then uv pip install -q -e .; else true; fi; } ) || return 1
touch "$marker"; return 0
_mark_ctr_setup "$repo"; return 0
fi
_py_have "$ver" || { printf " ${GRAY}(Python %s not in this image; skipping deps \xe2\x80\x94 explore-only)${RESET}\n" "$ver"; touch "$marker"; return 0; }
_py_have "$ver" || { printf " ${GRAY}(Python %s not in this image; skipping deps \xe2\x80\x94 explore-only)${RESET}\n" "$ver"; _mark_ctr_setup "$repo"; return 0; }
# Some poetry repos depend on sibling repos via `git = "ssh://git@github.com/AskZeta/<name>.git"`,
# which can't resolve in the container (no SSH key, no network). The deps are TRANSITIVE
# (cx-chatbot → compiler-agent → agent-tools → leaves), so rewrite the target AND every
@@ -472,7 +491,7 @@ setup_repo() {
done
( cd "$dir" && export PATH="$PYENV_PATH:$PATH" PYENV_VERSION="$ver" \
&& { [ -f .env.example ] && cp -n .env.example .env; true; } \
&& { [ -n "$bootenv" ] && printf '%s\n' $bootenv >> .env; true; } \
&& { for kv in $bootenv; do grep -qxF "$kv" .env 2>/dev/null || echo "$kv" >> .env; done; true; } \
&& { if [ -f pyproject.toml ]; then \
# The git→path rewrite invalidates poetry.lock ("changed significantly");
# regenerate it before installing. Poetry 2.x `lock` preserves pins by
@@ -487,12 +506,12 @@ setup_repo() {
elif [ -f requirements.txt ]; then pip install -r requirements.txt; \
elif [ -f setup.py ]; then pip install -e .; else true; fi; } ) || return 1 ;;
rust)
command -v cargo >/dev/null 2>&1 || { printf " ${GRAY}(Rust not in this image; skipping build \xe2\x80\x94 explore-only)${RESET}\n"; touch "$marker"; return 0; }
command -v cargo >/dev/null 2>&1 || { printf " ${GRAY}(Rust not in this image; skipping build \xe2\x80\x94 explore-only)${RESET}\n"; _mark_ctr_setup "$repo"; return 0; }
# Build to a container-local target dir (same ENFILE/bind-mount rationale as
# node_modules): a Cargo workspace target tree is huge and rebuilds often.
( cd "$dir" \
&& { [ -f .env.example ] && cp -n .env.example .env; true; } \
&& { [ -n "$bootenv" ] && printf '%s\n' $bootenv >> .env; true; } \
&& { for kv in $bootenv; do grep -qxF "$kv" .env 2>/dev/null || echo "$kv" >> .env; done; true; } \
&& CARGO_TARGET_DIR="/opt/raccoon-cargo-target/$repo" cargo build --workspace ) || return 1 ;;
none|"") : ;; # no-code / explore-only: nothing to install
*) printf "${YELLOW}unknown runtime '%s' for %s \xe2\x80\x94 explore-only${RESET}\n" "$runtime" "$repo" ;;
@@ -524,7 +543,7 @@ setup_repo() {
printf " ${GRAY}what went wrong: %s${RESET}\n" "$slog"
fi
fi
touch "$marker"
_mark_ctr_setup "$repo"
}
start_poly() {
@@ -649,6 +668,12 @@ start_poly() {
printf " ${RESET}${CYAN}http://localhost:%s/dev-login${RESET}${GRAY} to sign in as a seeded admin\n" "$CLIENT_HOST_PORT"
printf " (${RESET}${GRAY}?role=MSS${RESET}${GRAY} or ${RESET}${GRAY}?role=MEMBER${RESET}${GRAY} for the other roles). The DB was seeded during setup.${RESET}\n"
;;
potion-app)
printf " ${GRAY}Sign-in normally goes through Google or LinkedIn, neither reachable offline,\n"
printf " so setup seeded a verified local account. Log in at\n"
printf " ${RESET}${CYAN}http://localhost:%s/auth/login${RESET}${GRAY} with ${RESET}${GRAY}dev@example.com${RESET}${GRAY} / ${RESET}${GRAY}devpassword123${RESET}${GRAY}\n" "$CLIENT_HOST_PORT"
printf " — note ${RESET}${GRAY}/login${RESET}${GRAY} and ${RESET}${GRAY}/auth${RESET}${GRAY} both redirect elsewhere.${RESET}\n"
;;
esac
else
printf " ${RED}\xe2\x9a\xa0 %s didn't come up in time${RESET} \xe2\x80\x94 ${GRAY}run-app --logs${RESET}\n" "$repo"
@@ -667,13 +692,6 @@ start_rails() {
local login_hint="${1:-}" url_note="${2:-}"
_spawn app /workspace/repo "bin/rails server -b 0.0.0.0 -p 3000"
printf " ${YELLOW}\xe2\x96\xb6${RESET} starting Rails (puma)\xe2\x80\xa6\n"
# Repos built on tailwindcss-rails need the watcher running too, or their
# compiled app/assets/builds/application.css never gets generated and Propshaft
# silently falls back to serving an unrelated same-named stylesheet instead.
if [ -d /workspace/repo/app/assets/tailwind ]; then
_spawn css /workspace/repo "bin/rails tailwindcss:watch"
printf " ${YELLOW}\xe2\x96\xb6${RESET} starting Tailwind CSS watcher\xe2\x80\xa6\n"
fi
printf " ${GRAY}\xe2\x8f\xb3 waiting for the app to come up\xe2\x80\xa6${RESET}\n"
if _wait_tcp 3000; then
printf " ${YELLOW}\xe2\x9c\x85 app is up${RESET}\n"

View File

@@ -4,6 +4,11 @@ version = 1
id = "claude-code"
label = "Claude Code"
agent_import_path = "snapshot_agent:SnapshotClaudeCode"
# `[metadata] browser = true` swaps in these: same reduced toolset plus `Read`, so an agent
# given a browser can look at the screenshot it just took. Distinct classes with distinct
# names, because a different toolset is a different agent.
agent_import_path_browser = "snapshot_agent:BrowserSnapshotClaudeCode"
agent_import_path_single_turn_browser = "snapshot_agent:BrowserPreinstalledClaudeCode"
agent_import_path_single_turn = "snapshot_agent:PreinstalledClaudeCode"
import_path_aliases = [
"snapshot_agent:FullToolsetSnapshotClaudeCode",
@@ -29,7 +34,7 @@ install = "for i in 1 2 3; do curl -fsSL https://claude.ai/install.sh | bash &&
# reduction is a launch flag here and `--tools Bash` in snapshot_agent.py for the trial.
# Two expressions of one intent, which the $RACCOON_AGENT_FLAGS guard cannot police —
# unlike model and effort, which are interpolated from this row.
explore_launch = """exec claude --model '$RACCOON_MODEL' --effort $RACCOON_EFFORT --tools Bash --append-system-prompt "$RACCOON_TOOLSET_NOTE" --plugin-dir /workspace/plugins/create-snapshot --dangerously-skip-permissions "$@""""
explore_launch = """exec claude --model '$RACCOON_MODEL' --effort $RACCOON_EFFORT --tools "$RACCOON_TOOLS" --append-system-prompt "$RACCOON_TOOLSET_NOTE" --plugin-dir /workspace/plugins/create-snapshot --dangerously-skip-permissions "$@""""
[[harness]]
id = "codex"
@@ -84,7 +89,7 @@ explore_config = """
SessionStart = [ { hooks = [ { type = "command", command = "/workspace/plugins/create-snapshot/bin/save-session-info.mjs" } ] } ]
UserPromptSubmit = [ { hooks = [ { type = "command", command = "/workspace/plugins/create-snapshot/bin/checkpoint-workspace.mjs" } ] } ]
"""
explore_launch = """exec codex $RACCOON_AGENT_FLAGS --model $RACCOON_MODEL -c model_reasoning_effort=$RACCOON_EFFORT --dangerously-bypass-approvals-and-sandbox --dangerously-bypass-hook-trust "$@""""
explore_launch = """exec codex $RACCOON_AGENT_FLAGS --model $RACCOON_MODEL -c model_reasoning_effort=$RACCOON_EFFORT ${RACCOON_BROWSER_FLAGS[@]+"${RACCOON_BROWSER_FLAGS[@]}"} --dangerously-bypass-approvals-and-sandbox --dangerously-bypass-hook-trust "$@""""
[[harness]]
id = "gemini-cli"

View File

@@ -36,6 +36,11 @@ class Harness:
seed_native: bool
seed_atif: bool
agent_import_path_single_turn: str | None = None
# Browser-opt-in variants (`[metadata] browser = true`). A harness that has no variant
# keeps its normal class: codex, for instance, gains the browser and its disclosure but
# has no `Read` equivalent to switch toolsets for.
agent_import_path_browser: str | None = None
agent_import_path_single_turn_browser: str | None = None
import_path_aliases: tuple[str, ...] = ()
legacy_bare_model_rows: bool = False
default_model: str | None = None
@@ -67,9 +72,22 @@ class Harness:
# be edited in lockstep with the schema.
extra: dict = field(default_factory=dict, compare=False)
def agent_import_path_for(self, *, multi_turn: bool) -> str:
def agent_import_path_for(self, *, multi_turn: bool, browser: bool = False) -> str:
"""Agent class to launch. Multi-turn tasks need the resuming class; a
single-turn task given it would try to resume a session that isn't there."""
single-turn task given it would try to resume a session that isn't there.
``browser`` selects the opt-in variant, which for claude also carries the ``Read``
built-in — a different toolset is a different agent, so it is a different class with
its own name rather than a flag on the canonical one. Harnesses without a variant fall
through to their normal class."""
if browser:
variant = (
self.agent_import_path_browser
if multi_turn
else (self.agent_import_path_single_turn_browser or self.agent_import_path_browser)
)
if variant:
return variant
if multi_turn:
return self.agent_import_path
return self.agent_import_path_single_turn or self.agent_import_path
@@ -180,6 +198,8 @@ _KNOWN_FIELDS = frozenset(
"label",
"agent_import_path",
"agent_import_path_single_turn",
"agent_import_path_browser",
"agent_import_path_single_turn_browser",
"import_path_aliases",
"legacy_bare_model_rows",
"default_model",
@@ -315,6 +335,8 @@ def _build(entry: dict, index: int) -> Harness:
label=entry["label"],
agent_import_path=entry["agent_import_path"],
agent_import_path_single_turn=entry.get("agent_import_path_single_turn"),
agent_import_path_browser=entry.get("agent_import_path_browser"),
agent_import_path_single_turn_browser=entry.get("agent_import_path_single_turn_browser"),
import_path_aliases=tuple(entry.get("import_path_aliases", ())),
legacy_bare_model_rows=bool(entry.get("legacy_bare_model_rows", False)),
default_model=entry.get("default_model"),

View File

@@ -36,6 +36,7 @@ from __future__ import annotations
import argparse
import json
import os
import re
import shlex
import sys
import tomllib
@@ -77,6 +78,26 @@ def is_multi_turn(task_dir: str | None) -> bool:
return session.is_file() and session.stat().st_size > 0
def wants_browser(task_dir: str | None) -> bool:
"""True when task.toml opts into a browser (`[metadata] browser = true`).
Read straight from the file rather than via tomllib: this must agree with
build-workspace.sh, which decides whether the IMAGE gets Playwright using the same
text match. If the two ever disagree the agent is told about a browser the image
lacks, which is the one failure the disclosure is designed to make impossible.
Accepts the quoted form for the same reason build-workspace.sh does."""
if not task_dir:
return False
toml_path = Path(task_dir) / "task.toml"
if not toml_path.is_file():
return False
try:
text = toml_path.read_text(encoding="utf-8")
except OSError:
return False
return re.search(r'^[ \t]*browser[ \t]*=[ \t]*"?true"?[ \t]*$', text, re.M) is not None
def harness_from_task_toml(task_dir: str | None) -> str | None:
"""The task's own `[agent] harness` — the authoritative record of which harness
this task was authored against.
@@ -388,8 +409,9 @@ def main(argv: list[str] | None = None) -> int:
# Every assignment here becomes a harbor flag. Nothing else: the caller is bash, and
# anything it would only echo back at the worker is said below instead.
browser = wants_browser(args.task_dir)
assignments = {
"AGENT_IMPORT_PATH": harness.agent_import_path_for(multi_turn=multi_turn),
"AGENT_IMPORT_PATH": harness.agent_import_path_for(multi_turn=multi_turn, browser=browser),
"MODEL": normalize_model(harness, model),
"EFFORT_KWARG": harness.effort_kwarg,
"EFFORT_VALUE": (harness.effort_default or "") if harness.effort_kwarg else "",
@@ -398,8 +420,17 @@ def main(argv: list[str] | None = None) -> int:
warn(
f"{harness.label} · model={assignments['MODEL']} · "
f"{'multi-turn' if multi_turn else 'single-turn'} · "
f"{'browser · ' if browser else ''}"
f"agent={assignments['AGENT_IMPORT_PATH']}"
)
if browser and not harness.agent_import_path_browser:
# Not a failure: the image still gets Playwright and the agent is still told about
# it. Only the Read-enabled toolset swap is claude-specific, and saying so beats
# letting someone infer from a log line that the opt-in was ignored entirely.
warn(
f'"{harness.id}" has no browser-specific agent, so it runs its usual toolset. '
f"The browser and its disclosure are unaffected."
)
if harness.flaky_hangs:
warn(
f"{harness.label} is known to hang with no client-side timeout on a small "

View File

@@ -113,11 +113,24 @@ harness_install_launchers() {
mkdir -p "$bin"
# Read at launcher run time so the note stays a file, not a baked-in copy.
local note_src="${HARNESS_TOOLSET_NOTE:-/workspace/scripts/toolset_note.md}"
local browser_note_src="${note_src%.md}_browser.md"
local read_note_src="${note_src%.md}_read.md"
local agent_cli_dir="${AGENT_CLI_DIR:-/opt/agent-cli}"
local id cli launch
local id cli launch switchable
while IFS=$'\t' read -r id cli launch; do
[ -n "$cli" ] && [ -n "$launch" ] || continue
# Whether RACCOON_BROWSER_TASK can change THIS harness's toolset, read off the
# registry rather than hardcoded: a launch line that interpolates $RACCOON_TOOLS
# can, and one that doesn't cannot. codex is the second case — it ships view_image,
# so a browser task needs nothing added and the flag has nothing to switch.
# Match the whole variable name: a substring test also hits RACCOON_TOOLSET_NOTE,
# which every launch line references, and every harness would look switchable.
if [[ "$launch" =~ \$\{?RACCOON_TOOLS\}?([^A-Za-z0-9_]|$) ]]; then
switchable=1
else
switchable=0
fi
cat > "$bin/raccoon-explore-$cli" <<LAUNCHER
#!/bin/bash
# GENERATED by scripts/setup-harnesses.sh from harness-registry.toml — do not edit.
@@ -127,7 +140,38 @@ if [ -f "$note_src" ]; then
else
RACCOON_TOOLSET_NOTE=""
fi
export RACCOON_TOOLSET_NOTE
# RACCOON_BROWSER_TASK=1 explores with the toolset a \`browser = true\` task runs under.
# Named for the flag it mirrors: one word, \`browser\`, whether it's set in task.toml or
# here. Per invocation, not per container — authoring a browser task shouldn't need a
# rebuild, and neither should changing your mind. Default off, so ordinary exploring
# still mirrors an ordinary trial.
#
# The correction must be appended AFTER the base note, which says there is no Read tool.
RACCOON_TOOLS="Bash"
if [ "\${RACCOON_BROWSER_TASK:-0}" = "1" ] && [ "$switchable" = "1" ] && [ -f "$read_note_src" ]; then
RACCOON_TOOLS="Bash,Read"
RACCOON_TOOLSET_NOTE="\${RACCOON_TOOLSET_NOTE}
\$(cat "$read_note_src")"
fi
export RACCOON_TOOLS
# Only mention the browser on an image that actually has one — most don't. Probed at
# launch, not baked in, so the same launcher is correct in whichever container it runs.
#
# Exported two ways because the harnesses take extra instructions differently: claude
# appends the whole toolset note to --append-system-prompt, while codex has no equivalent
# and takes -c developer_instructions=. codex must NOT get the claude-shaped toolset note
# (it has no str_replace_editor), so the browser part is exported on its own too.
RACCOON_BROWSER_NOTE=""
RACCOON_BROWSER_FLAGS=()
if command -v pw >/dev/null 2>&1 && [ -f "$browser_note_src" ]; then
RACCOON_BROWSER_NOTE="\$(cat "$browser_note_src")"
RACCOON_TOOLSET_NOTE="\${RACCOON_TOOLSET_NOTE}
\${RACCOON_BROWSER_NOTE}"
RACCOON_BROWSER_FLAGS=(-c "developer_instructions=\${RACCOON_BROWSER_NOTE}")
fi
export RACCOON_TOOLSET_NOTE RACCOON_BROWSER_NOTE
export RACCOON_HARNESS="$id"
# No RACCOON_SNAPSHOT_DATA here on purpose. capture-snapshot.mjs and save-session-info.mjs
# already share the same default ($HOME/.raccoon/snapshot-data), which is what codex needs
@@ -143,11 +187,33 @@ LAUNCHER
# Alias lines for ~/.bashrc.
harness_alias_lines() {
local id cli launch
local id cli launch switchable
local browser_clis=""
while IFS=$'\t' read -r id cli launch; do
[ -n "$cli" ] && [ -n "$launch" ] || continue
echo "alias $cli=\"raccoon-explore-$cli\""
# Same derivation as the launcher: only a harness whose launch line takes
# $RACCOON_TOOLS has a toolset the flag can change.
if [[ "$launch" =~ \$\{?RACCOON_TOOLS\}?([^A-Za-z0-9_]|$) ]]; then
browser_clis="${browser_clis:+$browser_clis }$cli"
fi
done < <(_harness_query --explore-launchers 2>/dev/null || true)
# The browser hint belongs at the shell prompt, not in the launcher. Claude Code takes the
# alternate screen buffer, so anything printed just before exec is hidden for the whole
# session and resurfaces only after quitting — advice arriving exactly too late. Here it
# lands in ordinary scrollback, before any TUI exists, and there is nothing to quit yet.
#
# `pw` is probed at shell start, so one ~/.bashrc is correct in a container with a browser
# and in one without.
[ -n "$browser_clis" ] || return 0
local first="${browser_clis%% *}"
cat <<HINT
if [[ \$- == *i* ]] && [ "\${RACCOON_BROWSER_TASK:-0}" != "1" ] && command -v pw >/dev/null 2>&1; then
echo "browser available (Playwright + Chromium, \\\`pw <script.js>\\\`)."
echo "Authoring a \\\`browser = true\\\` task? Start it with: RACCOON_BROWSER_TASK=1 $first"
fi
HINT
}
# Write each harness's config file from the registry, replacing whatever was there.

View File

@@ -0,0 +1,4 @@
## Browser
Chromium is available in this environment via Playwright. `pw <script.js>` runs Node with
`require("playwright")` resolvable (CommonJS — `import` will not find it).

View File

@@ -0,0 +1,7 @@
## Correction to the toolset above: you also have `Read`
This task runs with `Read` in addition to `Bash`, so the statement above that there is no `Read`
tool does not apply here. `Read` renders images — use it to look at a screenshot you have
written to disk. Everything else above still holds: no `Grep`, `Glob`, `Edit`, `Write`,
`MultiEdit`, `NotebookEdit`, `Task`, `TodoWrite` or `AskUserQuestion`, and you still create and
edit files with `str_replace_editor`.

View File

@@ -1,10 +0,0 @@
{
"repo": "stocks-in-the-future",
"defaultCommit": "63732df2",
"version": "17ed6f400",
"explorePorts": {
"clientHost": 3700,
"serverHost": null,
"corpusHost": null
}
}

View File

@@ -130,7 +130,7 @@ case "$REPO" in
printf "Then open ${GRAY}http://localhost:${CLIENT_PORT}${RESET} and log in as ${GRAY}zaniyah@exhalefi.com${RESET} / ${GRAY}test${RESET}.\n"
printf "${GRAY}Stop it with ${RESET}${GRAY}run-app --stop${RESET}${GRAY}; follow logs with ${RESET}${GRAY}run-app --logs${RESET}${GRAY}.${RESET}\n"
printf "${GRAY}(The dev DB is seeded automatically during setup — re-run the seed with${RESET}\n"
printf "${GRAY} DEFAULT_BAAS_PROVIDER=Liquid PUBLIC_BAAS_ENABLED=yes TESTING_SEED=yes pnpm run seed.)${RESET}\n\n"
printf "${GRAY} DEFAULT_BAAS_PROVIDER=Liquid PUBLIC_BAAS_ENABLED=yes TESTING_SEED=yes pnpm run seed --small.)${RESET}\n\n"
printf "${GRAY}Want another container with its own separate working tree (e.g. a different commit / repo state)?${RESET}\n"
printf "${GRAY}On the host, from explore/: node instance.js b then: node instance.js shell b${RESET}\n"
printf "${GRAY} (it runs for exploring, but its browser app calls the first container's API.)${RESET}\n\n"