#!/bin/bash # Post-create setup for the Explore devcontainer. set -euo pipefail # Install every harness a worker can author with, and point each at the LLM proxy. # Driven by scripts/harness-registry.toml, so adding a harness is a registry entry # rather than an edit here and in the sibling container's post-create. set -a; . /workspace/.env 2>/dev/null || true; set +a . /workspace/scripts/setup-harnesses.sh # Explore is where capture happens, so it is the only surface that gets the capture # hooks — their commands ship in explore/plugins/. RACCOON_SURFACE=explore harness_setup_all # Allow git operations on bind-mounted repo (owned by different uid on host) git config --global --add safe.directory '*' # Check out the default commit from toolkit.json. SINGLE-REPO ONLY: a polyglot toolkit # has no single /workspace/repo and no top-level defaultCommit — each member repo lives # at /workspace/repos/ and is checked out + set up lazily by run-app/setup_repo. IS_POLYGLOT=$(node -e "try{process.stdout.write(require('/workspace/toolkit.json').polyglot?'1':'')}catch{}" 2>/dev/null || true) if [ -z "$IS_POLYGLOT" ]; then DEFAULT_COMMIT=$(node -e "process.stdout.write(require('/workspace/toolkit.json').defaultCommit)") git -C /workspace/repo -c advice.detachedHead=false checkout "$DEFAULT_COMMIT" fi # Install repo-specific runtime deps against the live-mounted /workspace/repo. # Bringing postgres up (and creating the role/db) lives in post-start.sh so it # also runs on every later container start, not just first create; call it here # so the database is ready before db:create / prisma migrate runs below. REPO_NAME=$(node -e "process.stdout.write(require('/workspace/toolkit.json').repo)" 2>/dev/null || true) bash /workspace/.devcontainer/post-start.sh # Symlink ./node_modules (cwd = the dir being installed) to a container-local tree keyed by # — see the call sites below for why. The target must itself be named `node_modules` # (Node resolves the symlink, then walks ancestors for that literal name), and its parent # needs a stub manifest: postinstall scripts that locate the project by truncating their # realpath at `node_modules` require() `/package.json`, and die without it. _nm_link() { local root="/opt/raccoon-node-modules/$1" [ -L node_modules ] || rm -rf node_modules mkdir -p "$root/node_modules" [ -f "$root/package.json" ] \ || printf '{"name":"raccoon-node-modules-root","version":"0.0.0","private":true}\n' > "$root/package.json" ln -sfn "$root/node_modules" node_modules } # g++ peaks near 400MiB on this codebase's biggest translation units, and nproc reports the # HOST's core count, so a many-core laptop with a small Docker VM OOMs mid-build. Bound the # job count by whichever of the VM's memory and the cgroup cap is smaller. _frepple_jobs() { local n mem cg j n=$(nproc) mem=$(awk '/^MemTotal:/{print $2*1024}' /proc/meminfo) cg=$(cat /sys/fs/cgroup/memory.max 2>/dev/null \ || cat /sys/fs/cgroup/memory/memory.limit_in_bytes 2>/dev/null || echo) case "$cg" in ''|max|*[!0-9]*) ;; *) [ "$cg" -lt "$mem" ] && mem=$cg ;; esac j=$(( mem / 734003200 )) [ "$j" -lt 1 ] && j=1 [ "$j" -gt "$n" ] && j=$n echo "$j" } # Docker Desktop's macOS bind mount can write a compiled wheel with the right length and # the wrong bytes, so the venv imports die on a signal (132/135/139) rather than an error. # A reinstall lands the authentic file; a Django-level failure exits 1 and is not retried. _frepple_migrate() { local rc=0 ./frepplectl.py migrate --noinput || rc=$? if [ "$rc" -le 128 ]; then return "$rc"; fi echo "venv extension died on signal $rc — reinstalling requirements and retrying" >&2 python3 -m pip install --force-reinstall --no-cache-dir -r requirements.txt -q ./frepplectl.py migrate --noinput } case "$REPO_NAME" in ZenBill-006) # Install deps + create databases # # node_modules goes to a CONTAINER-LOCAL path, not the bind-mounted repo dir. # On macOS Docker Desktop the repo is a host bind mount; writing yarn's huge, # deeply-nested node_modules tree across the file-sharing layer exhausts the # host open-file table -> ENFILE "file table overflow", failing the install. # Keeping node_modules inside the Linux VM confines that churn to the VM; the # repo stays bind-mounted (worker sees edits) and node_modules is a symlink. # (ZenBill is yarn-classic with a single root node_modules, so one symlink # relocates the whole tree cleanly — unlike Palolo's pnpm workspace, which # uses copy mode instead.) # # The symlink TARGET must itself be named `node_modules`: Node resolves the # symlink to its real path, then walks ancestors looking for a dir literally # named node_modules. If the target were .../zeta- (not node_modules), # child processes spawned by postinstall scripts (e.g. cypress's `node # index.js` requiring minimist) can't resolve hoisted deps -> MODULE_NOT_FOUND. ( cd /workspace/repo \ && cp .env.sample .env 2>/dev/null \ && sed -i "s/^ruby '3\.1\.2'/ruby '~> 3.1.0'/" Gemfile \ && rm -f .ruby-version \ && bundle install \ && _nm_link zenbill-006 \ && yarn install --ignore-engines \ && bundle update jwt \ && (bundle exec rails db:create db:migrate || true) \ && (RAILS_ENV=test bundle exec rails db:create db:migrate || true) ) ;; zeta-heimdall) # API-only Rails 7; Postgres-only; no JS runtime needed. config/database.yml # and .env are gitignored, so materialize them from the committed .example # files. The base image is the exact pinned Ruby (3.2.1), so the Gemfile's # ruby pin needs no loosening. --full-index works around stale-lockfile # transitive deps (the masked repo's lockfile omits a few). db:prepare loads # db/schema.rb into the dev DB; the test DB is created + loaded too (rspec's # maintain_test_schema! reloads it on first run). ( cd /workspace/repo \ && cp config/database.yml.example config/database.yml 2>/dev/null \ && cp .env.example .env 2>/dev/null \ && bundle install --full-index \ && (bundle exec rails db:prepare || true) \ && (RAILS_ENV=test bundle exec rails db:create db:schema:load || true) ) ;; zeta-platform) # Rails 5.1 / Ruby 2.6.6 banking monorepo; Postgres + Redis. config/database.yml # is committed (only .env is gitignored → copy from .env.example for dotenv). # Bundler 1.17.3 matches the lockfile (installed in the image), and the base is # the exact pinned Ruby (2.6.6), so no Gemfile loosening. db:schema:load loads # db/schema.rb into the dev + test DBs. ( cd /workspace/repo \ && cp .env.example .env 2>/dev/null \ && bundle install \ && (bundle exec rails db:create db:schema:load || true) \ && (RAILS_ENV=test bundle exec rails db:create db:schema:load || true) ) # React client (Create React App, react-scripts 2.1.1). Install its JS deps so # `run-app` can boot the full UI (dev server proxies /graphql → the Rails API). # node_modules goes to a CONTAINER-LOCAL path, not the bind-mounted repo dir: # on macOS Docker Desktop the repo is a host bind mount, and writing CRA's huge # node_modules tree across the file-sharing layer is slow AND exhausts the host's # open-file table. Keeping it inside the Linux VM confines that churn; the repo # stays bind-mounted (worker sees edits) and node_modules is a symlink. The # symlink TARGET must itself be named `node_modules` (Node's resolver walks # parents looking for a dir literally named node_modules). yarn is v1 (classic), # matching the committed yarn.lock. ( cd /workspace/repo \ && _nm_link zeta-platform \ && yarn install --frozen-lockfile ) ;; Palolo-031) # Install deps. packages/server/scripts/prisma greps `.env` for # PUBLIC_PALOLO_ENV inside an `if [ -t 0 ]` block — designed for # interactive use where the dev's local .env points at staging/prod # and the script wants confirmation before destructive ops. In a # fresh clone the file doesn't exist, so the grep fails and `set -e` # aborts. We materialize a `local`-pointing stub so the script # finds what it expects, the safety check skips correctly (env is # local, no confirmation needed), and downstream interactive worker # invocations of `pnpm run prisma …` also succeed instead of hitting # the same failure. ( cd /workspace/repo \ && git config core.hooksPath /dev/null \ && echo "PUBLIC_PALOLO_ENV=local" > packages/server/.env \ && pnpm install --frozen-lockfile \ && pnpm run --dir packages/server prisma generate \ && (pnpm run --dir packages/server prisma migrate deploy || true) ) # Seed the dev DB with a superuser, the global/superuser orgs, and a set # of test users so a worker can actually log in when running the app # locally. Without this the schema exists but every table is empty, and # the login screen errors out before you can get into the app. Test # users are @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 --small ) || true # Leave a fresh container's `git status` clean. The two artifacts below # are side effects of bootstrap, not edits anyone made: # # 1. .pnpm-store/ — pnpm's content-addressable store. It must sit on the # same filesystem as node_modules to hardlink; /workspace/repo is a # bind mount on a different fs than HOME, so pnpm can't use the global # ~/.pnpm-store and drops a project-local store instead. The repo's # .gitignore covers it as of commit 3af4366a6, but older commits a # worker may check out don't. Exclude it locally too (idempotent; # the create-snapshot checkpoint hook excludes it as well). # 2. deploy_to_eks.sh — the repo's only symlink (-> ../scripts/...). The # toolkit's zip/unzip packaging path materializes it as a regular file, # so git reports a "typechange". Restore the symlink from the index # (no-op if the filesystem can't represent symlinks). grep -qxF '.pnpm-store/' /workspace/repo/.git/info/exclude 2>/dev/null \ || printf '\n# raccoon-explore: in-repo pnpm store (bind-mount hardlink fallback)\n.pnpm-store/\n' >> /workspace/repo/.git/info/exclude git -C /workspace/repo checkout -- provisioning/kubernetes/palolo-app/deploy_to_eks.sh 2>/dev/null || true ;; human-essentials) # Rails 8 / Ruby 3.4; pure importmap (no JS bundler → no node_modules). The # base image is exact Ruby 3.4.3, so no Gemfile loosening. .env is gitignored; # copy the committed .env.example (public reCAPTCHA test keys etc.) for dotenv, # then drop its empty PG_USERNAME/PG_PASSWORD lines so they don't override the # image ENV (PG_USERNAME=postgres). db:schema:load loads db/schema.rb into the # dev + test DBs; assets:precompile is needed by the Cuprite system specs. # db:seed (dev, offline via Faker) gives a working login out of the box — the app # has no usable self-service signup (a fresh user lands org-less/role-less). ( cd /workspace/repo \ && cp .env.example .env 2>/dev/null || true; \ sed -i '/^PG_USERNAME=/d; /^PG_PASSWORD=/d' .env 2>/dev/null || true; \ bundle install \ && (bundle exec rails db:create db:schema:load || true) \ && (RAILS_ENV=test bundle exec rails db:create db:schema:load || true) \ && (bundle exec rails db:seed || true) \ && (bundle exec rails assets:precompile || true) ) ;; endsideout) # Rails 8.1 / Ruby 4.0; SQLite + importmap (no Node build — tailwindcss-rails # ships its own binary). No .env (no .env.example; tests need no secrets). The # 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 tailwindcss:build || true) ) ;; community-foundation) # Rails 8.1 / Ruby 4.0; SQLite + importmap + tailwind (no Node). Encrypted # credentials aren't needed for tests. SQLite dev + test DBs. # db:seed (dev, offline) creates the 'arlington' tenant + owner@example.com / # password. Self-signup is a dead end here (needs a pre-existing org + a working # 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 tailwindcss:build || true) ) ;; stocks-in-the-future) # Rails 8.1 / Ruby 3.4.4; Postgres + Redis; importmap (no Node build). # config/database.yml is gitignored — materialize from the committed sample. # 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 tailwindcss:build || true) ) ;; casa) # Rails 8.0 / Ruby 4.0.3; Postgres + Node 24 (jsbundling: esbuild + sass). # DB env (POSTGRES_USER/DATABASE_HOST/POSTGRES_PASSWORD) is pinned in the image. # npm ci installs JS deps; `npm run build` + `build:css` (esbuild + sass) write the # bundles to app/assets/builds. The Selenium system specs serve from there because # the test env runs with config.assets.compile=true (Sprockets compiles on demand). # Deliberately NOT `assets:precompile`: that fingerprints untracked copies into # public/assets which the specs don't need and which make `npm run lint` (standard) # report ~197k errors over machine-generated bundles. app/assets/builds is already in # standard's ignore list, so the dev build leaves the tree lint-clean and faithful. # db:seed (dev, offline via Faker + local logo) creates casa_admin1@example.com / # 12345678 — users are admin-invited only (ADR 0002), so seeding is the way in. ( cd /workspace/repo \ && (cp .env.example .env 2>/dev/null || true) \ && bundle install \ && npm ci \ && (bin/rails db:create db:schema:load || true) \ && (RAILS_ENV=test bin/rails db:create db:schema:load || true) \ && (bin/rails db:seed || true) \ && (npm run build && npm run build:css || true) ) ;; awbw) # Rails 8.1 / Ruby 4.0.1; MySQL 8 (Percona, Trilogy) + Node 22 (Vite). .env from # .env.sample; DATABASE_URL (image) points Trilogy at 127.0.0.1 root. npm ci + a # test-mode Vite build for the Selenium system specs. # Use db:schema:load (NOT migrate): the committed schema.rb is clean native-MySQL-8 # JSON; running migrate re-dumps schema.rb from the live DB (which corrupts it under # a non-MySQL-8 engine). tz tables are loaded by post-start.sh (Ahoy charts need them). # db:seed (dev, offline; the seed disables mailer delivery itself) creates the # pre-confirmed umberto.user@example.com / password super_user — no self-service # signup exists and :confirmable would block a hand-made user without a mailer. ( cd /workspace/repo \ && (cp .env.sample .env 2>/dev/null || true) \ && bundle install \ && npm ci \ && (bin/vite build --mode test || true) \ && (bin/rails db:create db:schema:load || true) \ && (RAILS_ENV=test bin/rails db:create db:schema:load || true) \ && (bin/rails db:seed || true) ) ;; alongwithyou) # Rails 8.1 / Ruby 4.0.5; SQLite + importmap (no app-side Node). No .env / credentials # needed to boot. This is a young app (a fresh scaffold with no migrations yet), so # db:prepare just materializes an empty dev/test DB; db:seed is a no-op on the default # seeds.rb. All wrapped in `|| true` so an empty schema never blocks container startup. ( cd /workspace/repo \ && bundle install \ && (bin/rails db:prepare || true) \ && (bin/rails db:test:prepare || true) \ && (bin/rails db:seed || true) ) ;; flaredown) # Polyglot: backend/ Rails 7.1 (Ruby 3.2.3, Mongoid on MongoDB + Postgres + Redis + # Sidekiq) and frontend/ Ember (Node 14). Postgres/Mongo/Redis are started by # post-start.sh (called above). .env is gitignored — materialize from the committed # backend/env-example (public dev secrets). Mongoid creates collections lazily, so # there's no Mongo schema to load; Postgres holds a small relational slice with a # committed db/schema.rb → db:schema:load (NOT db:migrate, which re-dumps schema.rb # from the live DB on a bind-mounted repo). # env-example points PG at host `postgresql` (the docker-compose service name); in this # single container everything is on localhost, so rewrite the PG host. Redis defaults to # localhost already; Mongoid reads MONGODB_HOST (unset → localhost). ( cd /workspace/repo/backend \ && (cp -n env-example .env 2>/dev/null || true) \ && sed -i 's/^PG_DATABASE_HOST=.*/PG_DATABASE_HOST=localhost/' .env 2>/dev/null || true; \ bundle install \ && (bundle exec rails db:create db:schema:load || true) \ && (RAILS_ENV=test bundle exec rails db:create db:schema:load || true) ) # Ember frontend on Node 14 (frontend/.nvmrc = v14.21.3; npm pinned to 6 in the image). # node_modules to a container-local symlink (bind-mount file-sharing exhausts the host fd # table on big node_modules trees). OPENSSL_CONF=/dev/null lets the old webpack md4 hashing # run on bookworm's OpenSSL 3. --unsafe-perm so npm (running as root) actually executes the # postinstall (patch-package + bower install) instead of skipping it with a "cannot run in # wd" warning; without it bower_components is never populated and `ember build` fails. NODE14_BIN=$(ls -d /usr/local/nvm/versions/node/v14.* 2>/dev/null | sort -V | tail -1)/bin ( cd /workspace/repo/frontend \ && export PATH="$NODE14_BIN:$PATH" OPENSSL_CONF=/dev/null \ && _nm_link flaredown-frontend \ && (npm install --unsafe-perm --no-audit --no-fund || echo "WARNING: frontend npm install failed (explore-only)" >&2) ) || true ;; frepple) # The minified JS the app serves is tracked, so pnpm+grunt are for a worker who # edits frontend source, not a prerequisite. The cmake build creates venv/ and # pip-installs into it. odoo_addon is pinned, NOT --remote like upstream CI. # DEBUG_JS=DEBUG, and DEBUG is true under runserver, which points the two Vue # screens at a Vite dev server on :5173 that nothing starts. FREPPLE_PORT is where # the BROWSER posts forecast saves, so the service has to bind 0.0.0.0 to be # reachable. localsettings.py is upstream's own gitignored override hook. # `demo` alone holds no forecasts, which leaves the forecast screens empty; adding # distribution_demo and planning it reproduces upstream's own scenario1 content. # The `doc` target is not in `all`, so without it the Help menu and the help icon on # 89 report screens 404; its own symlink is absolute, so relink it relatively or the # host sees a dangling link into the container's /workspace. ( cd /workspace/repo \ && git submodule update --init freppledb/odoo/odoo_addon \ && pnpm install --frozen-lockfile \ && grunt \ && cmake -S . -B build -DCMAKE_BUILD_TYPE=Release \ && cmake --build build --parallel "$(_frepple_jobs)" \ && { cmake --build build --target doc \ && ln -sfn ../../../build/doc/_build/html freppledb/common/static/doc \ || echo "NOTE: the docs did not build; in-app help links will 404" >&2; } \ && printf 'DEBUG_JS = False\nfor _a in DATABASES:\n DATABASES[_a]["FREPPLE_PORT"] = DATABASES[_a]["FREPPLE_PORT"].replace("127.0.0.1:", "0.0.0.0:")\n' \ > localsettings.py \ && _frepple_migrate \ && ./frepplectl.py loaddata demo \ && ./frepplectl.py loaddata distribution_demo \ && ./frepplectl.py runplan --env=fcst,supply --background \ && ./frepplectl.py shell -c " from django.contrib.auth import get_user_model U = get_user_model() u, _ = U.objects.get_or_create(username='admin', defaults={'email': 'admin@example.com'}) u.is_superuser = True; u.is_staff = True; u.set_password('frepple'); u.save() print('admin user ready') " ) \ || { echo "FATAL: frepple setup did not complete — the app would not serve." >&2; exit 1; } ;; freeitsm) # Plain PHP / Apache, no composer. config.php is left exactly as upstream tracks it # (the image puts its Windows-path require on include_path); db_config.php is the # doc-root stub the test scripts require, excluded locally so git status stays clean. # Apache writes attachments and imports as www-data, but a bind mount can force those # dirs root-owned and swallow the chown, so the directory mode is what has to give. ( cd /workspace/repo \ && cp docker/db_config.php db_config.php \ && _fi_ex="$(git rev-parse --git-path info/exclude)" \ && mkdir -p "$(dirname "$_fi_ex")" \ && { grep -qxF 'db_config.php' "$_fi_ex" 2>/dev/null \ || echo 'db_config.php' >> "$_fi_ex"; } \ && for _fi_d in tickets/attachments change-management/attachments \ uploads/asset-imports uploads/documents; do \ mkdir -p "$_fi_d"; \ chown -R www-data:www-data "$_fi_d" 2>/dev/null || true; \ find "$_fi_d" -type d -exec chmod a+rwx {} +; \ done \ && if [ "$(mysql -u root -N -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='freeitsm'" 2>/dev/null || echo 0)" -lt 10 ]; then mysql -u root freeitsm < database/freeitsm.sql \ || { echo "FATAL: could not load database/freeitsm.sql — is the freeitsm database present?" >&2; exit 1; } fi \ && apache2ctl -k start 2>/dev/null \ && /usr/local/bin/freeitsm-seed.sh \ && apache2ctl -k stop 2>/dev/null ) \ || { echo "FATAL: freeitsm setup did not complete — the app would not log in." >&2; exit 1; } ;; breezy-complete) # Monorepo: Rails 7.0 / Ruby 3.2.0 API (backend/) + Next.js 14 frontend # (frontend/); Postgres + Redis baked in the image. The offline Clerk-bypass # env is injected by run-app at server start only — the ambient env stays # upstream-CI-shaped so a worker's `cd backend && bundle exec rspec` runs # green (ambient DISABLE_CLERK 403s several controller specs, and ambient # RAILS_ENV leaks through rails_helper's `ENV['RAILS_ENV'] ||= 'test'`). # # backend: gems + yarn asset-pipeline deps; db:prepare (retried once — the # first run can race the just-started postgres) + db:seed (offline-safe demo # tenant; the only way into the UI, auth is invite-less) + test DB. Fresh-DB # db:test:prepare trips check_protected_environments → stamp the env first. # db:prepare seeds the DB it creates and the seeds are not idempotent, so the # explicit db:seed is for the retry case only — skip it on a seeded DB. # frontend: npm install (not ci) so platform-specific optional deps resolve # on arm64 + x64. Both node_modules go to CONTAINER-LOCAL paths via symlink # (bind-mount ENFILE; see the ZenBill comment above — target must itself be # named node_modules). ( cd /workspace/repo/backend \ && bundle install --jobs 4 --retry 3 \ && _nm_link breezy-backend \ && yarn install --frozen-lockfile \ && (bundle exec rails db:prepare || bundle exec rails db:prepare) \ && ( psql -tAc 'select 1 from breezy_professionals limit 1' socratic_systems_development 2>/dev/null | grep -q 1 \ || bundle exec rails db:seed || true ) \ && (RAILS_ENV=test bundle exec rails db:environment:set || true) \ && (RAILS_ENV=test bundle exec rails db:test:prepare || true) ) ( cd /workspace/repo/frontend \ && _nm_link breezy-frontend \ && npm install --include=optional ) ;; esac # Mirror Harbor's reduced toolset in the interactive Explore session. Use # Harbor's /opt path when available, but fall back to a user-writable path for # generic devcontainer fixtures that run lifecycle hooks as a non-root user. AGENT_CLI_DIR="/opt/agent-cli" if ! mkdir -p "$AGENT_CLI_DIR" 2>/dev/null; then AGENT_CLI_DIR="$HOME/.agent-cli" mkdir -p "$AGENT_CLI_DIR" fi cp -R /workspace/scripts/str_replace_editor /workspace/scripts/str_replace_editor_vendor "$AGENT_CLI_DIR/" chmod +x "$AGENT_CLI_DIR/str_replace_editor" mkdir -p "$HOME/.local/bin" # Explore launchers (one per authoring harness) come from setup-harnesses.sh, # which reads harness-registry.toml. AGENT_CLI_DIR is where the reduced-toolset # editor was staged above, and the launcher rewrites the toolset note to match. AGENT_CLI_DIR="$AGENT_CLI_DIR" harness_install_launchers mkdir -p "$HOME/.claude" # SKIP_FAST_MODE_NETWORK_ERRORS: the LLM proxy doesn't forward claude's fast-mode # availability probe, and claude reads the failed probe as "no network" and refuses # /fast. The override makes /fast toggleable; fast serving stays OFF until toggled. # Names this container as the surface a proxy call came from: claude reads it from the # settings env below, codex from the shell (its config maps the header to this var name). # Only on the proxy — a provider endpoint must not carry this attribution. CALL_METADATA="" case "$(set -a; . /workspace/.env 2>/dev/null || true; set +a; printf '%s' "${ANTHROPIC_BASE_URL:-}")" in */llm_proxy/*) CALL_METADATA='{"origin":"explore"}' ;; esac CALL_METADATA="$CALL_METADATA" node -e ' const fs = require("fs"); const home = process.env.HOME; const env = { CLAUDE_CODE_DISABLE_AUTO_MEMORY: "1", CLAUDE_CODE_SKIP_FAST_MODE_NETWORK_ERRORS: "1", }; if (process.env.CALL_METADATA) { env.ANTHROPIC_CUSTOM_HEADERS = `X-Surge-Client-Metadata: ${process.env.CALL_METADATA}`; } fs.writeFileSync( `${home}/.claude/settings.json`, JSON.stringify({ env }, null, 2) + "\n" ); // Onboarding preflights api.anthropic.com + platform.claude.com, neither from // ANTHROPIC_BASE_URL, and exits 1 unresolved, so a jailed container never records it done. const cfgPath = `${home}/.claude.json`; let cfg = {}; try { cfg = JSON.parse(fs.readFileSync(cfgPath, "utf-8")); } catch {} cfg.hasCompletedOnboarding = true; fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + "\n"); ' # Reference-data corpus: expose it at the stable /data/zeta-corpus path (the same path a trial # uses) by symlinking to the toolkit's bind-mounted copy. No-op if this toolkit ships no corpus. if [ -d /workspace/data/zeta-corpus ]; then { mkdir -p /data || sudo mkdir -p /data; } 2>/dev/null || true { ln -sfn /workspace/data/zeta-corpus /data/zeta-corpus \ || sudo ln -sfn /workspace/data/zeta-corpus /data/zeta-corpus; } 2>/dev/null || true fi # Shell setup # Ahead of the block below so every shell exports it, interactive or not. if [ -n "$CALL_METADATA" ]; then # A file rather than a .bashrc line alone: the launcher and refresh-harness-auth # read it too, so a non-login shell does not silently lose the attribution. printf 'export LLM_CALL_METADATA=%s\n' "'$CALL_METADATA'" > ~/.raccoon-call-origin printf '. "$HOME/.raccoon-call-origin"\n' >> ~/.bashrc fi cat >> ~/.bashrc <<'BASHRC' export PATH="$HOME/.local/bin:$PATH" set -a && source /workspace/.env && set +a # Everything below this line is for interactive shells only. An agent's shell tool # sources .bashrc too, so without this guard the welcome banner prints into command # output and container_start fires once per command instead of once per session. case $- in *i*) ;; *) return ;; esac alias run-app="bash /workspace/run-app.sh" [ -f /workspace/corpus-viewer/view-corpus.sh ] && alias view-corpus="bash /workspace/corpus-viewer/view-corpus.sh" export PS1="\[\033[1;36m\][raccoon-explore]\[\033[0m\] \w\$ " bash /workspace/welcome.sh explore 2>/dev/null _AK="fde503c3bdb6e5cc9c48b1f8e4c2abeb" _DK="e966e45af5ad1a18005f9fdb831186ea" _WID="w-mun3wr6n-v83f" _VER="1.0.0" _CT="explore" _RP=$(node -e "try{process.stdout.write(require('/workspace/toolkit.json').repo)}catch{}" 2>/dev/null) _SID="$(date +%s)-$$" _LAT=0 _ev() { [ -z "$_AK" ] && return { curl -s -X POST "https://api2.amplitude.com/2/httpapi" \ -H "Content-Type: application/json" \ -d "{\"api_key\":\"$_AK\",\"events\":[{\"user_id\":\"$_WID\",\"event_type\":\"raccoon.$1\",\"event_properties\":{\"product\":\"raccoon\",\"container\":\"$_CT\",\"repo\":\"$_RP\",\"toolkit_version\":\"$_VER\",\"session_id\":\"$_SID\"},\"session_id\":$(date +%s000)}]}" \ >/dev/null 2>&1 & } 2>/dev/null; disown 2>/dev/null } _dl() { [ -z "$_DK" ] && return { curl -s -X POST "https://http-intake.logs.datadoghq.com/api/v2/logs" \ -H "DD-API-KEY: $_DK" -H "Content-Type: application/json" \ -d "[{\"ddsource\":\"raccoon\",\"service\":\"toolkit\",\"hostname\":\"$(hostname)\",\"status\":\"$1\",\"message\":\"$2\",\"ddtags\":\"container:$_CT,worker:$_WID,repo:$_RP,toolkit_version:$_VER\"}]" \ >/dev/null 2>&1 & } 2>/dev/null; disown 2>/dev/null } _pc() { local n; n=$(date +%s); if (( n - _LAT >= 300 )); then _LAT=$n; _ev active; fi; } PROMPT_COMMAND="_pc;${PROMPT_COMMAND:-}" trap '_ev container_stop; _dl info container_stop; wait' EXIT _ev container_start _dl info container_start BASHRC # One alias per authoring harness: `claude` runs claude, `codex` runs codex. harness_alias_lines >> ~/.bashrc