Przejdź do głównej zawartości

Efemeryczne środowiska: pełny stos dla każdego zadania agenta

Efemeryczne środowisko dla zadania agenta to jednorazowa kopia całego stosu z danymi startowymi (checkout kodu, porty, baza danych, zaślepki usług zewnętrznych i wyłącznie testowe poświadczenia), która istnieje przez jedno uruchomienie i znika po nim. Dzięki niemu każdy agent dowodzi działania funkcji od końca do końca, nie kolidując z równoległymi uruchomieniami i nie dotykając wspólnych danych. Worktree izoluje tylko pliki.

Ta strona jest dla developera, który uruchamia kilku agentów naraz, i dla tech leada, który chce, żeby każde z tych uruchomień zostawiało dowody, którym recenzent może zaufać. Trzech agentów dostało po worktree. Pierwszy uruchomił serwer deweloperski na porcie 3000, serwer drugiego po cichu przeskoczył na 3001, a testy end-to-end trzeciego podłączyły się do serwera pierwszego i dały zielony wynik dla kodu, którego nigdy nie sprawdziły. W tym samym czasie dwóch z nich puściło migracje na tym samym lokalnym Postgresie. Każde uruchomienie „przeszło” i żadne niczego nie dowiodło.

  • Sześcioczęściowy kontrakt środowiska (checkout, blok portów, dane, zaślepki, poświadczenia, czas życia), który mówi, co każde uruchomienie ma tylko dla siebie.
  • Trzy skrypty do dostosowania: bin/env-up przydziela blok portów i startuje stos z danymi w osobnym projekcie Compose, bin/env-check dowodzi, że stos działa i jest odizolowany, a bin/env-down go usuwa.
  • Podpięcie w każdym narzędziu: worktree i środowiska chmurowe Claude Code, worktree i zadania chmurowe Codex oraz worktree i Cloud Agents w Cursorze.
  • Trzy prompty do skopiowania: zbuduj kontrakt dla swojego repozytorium, wykonaj w nim zadanie i sprawdź izolację dwoma równoległymi uruchomieniami.
  • Test izolacji, który dowodzi, że konfiguracja działa, zanim powierzysz jej uruchomienia bez nadzoru, oraz typowe awarie, przez które zielony wynik nic nie znaczy.

Worktree w gicie daje każdemu agentowi własne pliki i gałąź. Wszystko, z czym kod rozmawia w czasie działania, nadal jest wspólne: porty sieciowe, baza danych, cache, kolejki wiadomości, nazwy kontenerów Dockera, sandboksy usług zewnętrznych i każde poświadczenie w twojej powłoce. Agentowi, który uruchamia tylko testy jednostkowe, to nie przeszkadza. Agent, który ma udowodnić, że funkcja działa od końca do końca, a o to chodzi w weryfikowaniu zachowania zamiast czytania diffów, potrzebuje tego wszystkiego i to tylko dla siebie.

Środowisko to siódma warstwa harnessu. Warstwa uprawnień i sandboksa ogranicza, co uruchomienie może zrobić; środowisko decyduje, co to uruchomienie widzi i co może zepsuć. Środowisko bez produkcyjnych poświadczeń jest przy okazji najmocniejszą regułą uprawnień: agent nie nadużyje klucza, którego nie ma na maszynie.

Z jakich sześciu części składa się kontrakt środowiska?

Dział zatytułowany „Z jakich sześciu części składa się kontrakt środowiska?”
CzęśćCo ją izolujeCo się psuje bez niej
CheckoutLokalnie worktree, w chmurze świeży klon w maszynie wirtualnejDwóch agentów edytuje te same pliki; checkout niesie niezacommitowane zmiany innego zadania
Blok portówStały blok portów na indeks zadania, zapisany w pliku, który czytają wszystkie narzędziaSerwery deweloperskie przeskakują na następny wolny port, a testy trafiają w serwer sąsiada
DaneOsobna baza na zadanie, zbudowana z migracji i deterministycznego seedaUruchomienia psują sobie nawzajem wiersze; test przechodzi tylko dlatego, że inne uruchomienie zostawiło dane
Zaślepki usługSerwer zaślepek na zadanie dla każdego zewnętrznego API, z zacommitowanymi mapowaniamiTesty wołają prawdziwe API płatności, e-maili albo reklam, albo padają, gdy sandbox dostawcy nie działa
PoświadczeniaGenerowany plik env wyłącznie z wartościami testowymiWstrzyknięty prompt albo błędne polecenie dociera do produkcji, prawdziwych klientów albo prawdziwych pieniędzy
Czas życiaPolecenie sprzątające i okresowe usuwanie porzuconych stosówStosy się mnożą, aż maszynie zabraknie dysku, pamięci albo portów

Uruchomienie dowodzi zachowania tylko wtedy, gdy trzyma się wszystkich sześciu części.

Wybierz najlżejszy poziom, który izoluje wszystko, czego dotyka zadanie. Katalog narzędzi, z modelem izolacji każdego produktu i przykładem użycia, znajdziesz w porównaniu sandboksów dla agentów; ta strona opisuje wzorzec.

PoziomCo izolujeKiedy go użyćGłówny koszt
Worktree + blok portów + projekt Compose na twojej maszyniePliki, porty, kontenery, bazęUruchamiasz lokalnie od dwóch do pięciu agentów i przeglądasz wyniki tego samego dniaProcesor i pamięć twojej maszyny; sprzątanie jest po twojej stronie
Kontener na zadanie (na przykład container-use, które daje każdemu agentowi kontener na osobnej gałęzi gita)Wszystko powyżej plus drzewo procesów i zainstalowane narzędziaZadania instalują paczki albo potrzebują różnych toolchainówCzas budowania obrazu; Docker na każdej maszynie
Środowisko chmurowe dostawcy (sesje chmurowe Claude Code, Codex cloud, Cursor Cloud Agents)Cała maszyna wirtualna na zadanie, na infrastrukturze dostawcyUruchomienia w tle, uruchomienia startowane ze zgłoszenia albo czatu i takie, które muszą przetrwać zamknięcie laptopaDyscyplina w skrypcie konfiguracyjnym; limity zasobów dostawcy; listy dozwolonych hostów
Własne runnery w chmurzeMaszyna wirtualna albo kontener na twojej infrastrukturzeKod lub dane nie mogą opuścić twojej sieciUtrzymujesz flotę sam

Żeby wypróbować poziom „kontener na zadanie” z Claude Code, zainstaluj container-use (brew install dagger/tap/container-use) i zarejestruj je jako serwer MCP poleceniem claude mcp add container-use -- container-use stdio; narzędzie nie jest opublikowane w npm, więc konfiguracja z npx container-use nie zadziała.

Niezależnie od poziomu trzymaj definicję środowiska w repozytorium jako skrypty, a nie wyłącznie w ekranie ustawień dostawcy. Wtedy ten sam bin/env-up działa na laptopie, w chmurowej maszynie dostawcy i w CI, a wyniki z tych miejsc da się porównać.

Przykład to serwis w Node.js z Postgresem i zewnętrznym API płatności. Podmień nazwy obrazów i polecenia na swój stos, ale zachowaj strukturę.

  1. Przydziel każdemu zadaniu blok portów. Każde zadanie dostaje indeks od 1 do 49, a każdy port to wartość bazowa plus dziesięciokrotność indeksu. Krok co dziesięć sprawia, że serwer, który przeskoczy na następny wolny port, zostaje we własnym bloku. Przydział korzysta z mkdir, które jest atomowe, więc dwóch agentów startujących w tej samej chwili nie dostanie tego samego indeksu.

    #!/usr/bin/env bash
    # bin/env-up — start an isolated, seeded stack for one agent task
    set -euo pipefail
    slug=$(printf '%s' "${1:?usage: bin/env-up <task-slug>}" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9' '-' | sed 's/^-*//;s/-*$//')
    state="${AGENT_ENV_HOME:-$HOME/.agent-envs}"
    mkdir -p "$state"
    idx=""
    for dir in "$state"/*/; do # rerun of the same task: reuse its block
    [ -f "$dir/slug" ] && [ "$(cat "$dir/slug")" = "$slug" ] && idx=$(basename "$dir")
    done
    if [ -z "$idx" ]; then
    for i in $(seq 1 49); do
    if mkdir "$state/$i" 2>/dev/null; then idx=$i; echo "$slug" > "$state/$i/slug"; break; fi
    done
    fi
    [ -n "$idx" ] || { echo "no free port block in $state" >&2; exit 1; }
    cat > .env.task <<EOF
    TASK_SLUG=$slug
    TASK_INDEX=$idx
    COMPOSE_PROJECT_NAME=task-$slug
    APP_PORT=$((3000 + idx * 10))
    DB_PORT=$((5432 + idx * 10))
    STUB_PORT=$((8080 + idx * 10))
    DATABASE_URL=postgres://app:app@127.0.0.1:$((5432 + idx * 10))/app
    PAYMENTS_API_URL=http://127.0.0.1:$((8080 + idx * 10))
    PAYMENTS_API_KEY=test_only_not_a_secret
    EOF
    set -a; . ./.env.task; set +a
    docker compose -p "$COMPOSE_PROJECT_NAME" --env-file .env.task -f compose.agent.yml up -d --wait
    npm run db:migrate
    npm run db:seed
    echo "task $slug: app :$APP_PORT, db :$DB_PORT, stubs :$STUB_PORT"

    Dopisz .env.task do .gitignore. Ten plik jest jedynym źródłem portów: serwer deweloperski, runner testów i agent czytają go wszyscy, więc żadne narzędzie nie musi pamiętać numeru.

  2. Zdefiniuj usługi zadania w osobnym projekcie Compose. Nazwa projektu (-p task-<slug>) nadaje przestrzeń nazw każdemu kontenerowi, sieci i wolumenowi, więc dwa zadania nigdy nie współdzielą żadnego z nich. Nigdy nie ustawiaj container_name: stała nazwa omija przestrzeń nazw projektu i drugie zadanie nie wystartuje.

    # compose.agent.yml — services one agent task needs, nothing shared
    services:
    db:
    image: postgres:16
    environment:
    POSTGRES_USER: app
    POSTGRES_PASSWORD: app
    POSTGRES_DB: app
    ports: ["127.0.0.1:${DB_PORT}:5432"]
    tmpfs: /var/lib/postgresql/data # data dies with the container
    healthcheck:
    test: ["CMD-SHELL", "pg_isready -U app"]
    interval: 2s
    retries: 30
    payments-stub:
    image: wiremock/wiremock
    ports: ["127.0.0.1:${STUB_PORT}:8080"]
    volumes: ["./stubs/payments:/home/wiremock"]

    Powiązanie z 127.0.0.1 trzyma stos z dala od twojej sieci LAN. --wait sprawia, że env-up kończy się dopiero wtedy, gdy przejdzie health check bazy, więc pierwsza migracja agenta nie ściga się z kontenerem.

  3. Załaduj deterministyczne dane. db:seed musi dawać te same wiersze przy każdym uruchomieniu: stałe ziarno losowości, stałe znaczniki czasu i żadnych wywołań żywych usług. Zasiej przypadki, które wymieniają twoje kryteria akceptacji (wygasła subskrypcja, użytkownik w dwóch organizacjach, zwrot w toku), a nie ogólną próbkę. Jeśli seedowanie trwa minuty, zasiej raz bazę-szablon i twórz bazę każdego zadania z niej poleceniem Postgresa CREATE DATABASE task_db TEMPLATE app_template, które kopiuje pliki zamiast ponownie odtwarzać seed. Projektowanie seedów szczegółowo opisuje strona o zarządzaniu danymi testowymi.

  4. Zaślep każde zewnętrzne API. Obraz WireMocka czyta mapowania zaślepek z /home/wiremock/mappings i serwuje je na porcie 8080 wewnątrz kontenera. Zacommituj mapowania w stubs/payments/mappings/, łącznie z odpowiedziami błędów (odrzucona karta, timeout, 429), które twój kod musi obsłużyć. Wskaż aplikacji zaślepkę przez plik env (PAYMENTS_API_URL), nigdy przez kod, który agent może edytować.

  5. Generuj wyłącznie testowe poświadczenia. Plik env zawiera tylko wartości sandboksowe albo atrapy. Prawdziwe tokeny w ogóle nie trafiają do środowiska; jak wydać ograniczone poświadczenie zadaniu, które naprawdę go potrzebuje, opisuje strona o tożsamości agentów i sekretach.

  6. Udowodnij, że stos działa i jest odizolowany. Agent uruchamia ten skrypt przed pierwszym testem i wkleja wynik do raportu.

    #!/usr/bin/env bash
    # bin/env-check — prove this task's stack is up, seeded and its own
    set -euo pipefail
    set -a; . ./.env.task; set +a
    docker compose -p "$COMPOSE_PROJECT_NAME" -f compose.agent.yml --env-file .env.task ps --format '{{.Service}} {{.State}}'
    curl -fsS "http://127.0.0.1:$STUB_PORT/__admin/health" > /dev/null && echo "stubs: healthy"
    docker compose -p "$COMPOSE_PROJECT_NAME" -f compose.agent.yml --env-file .env.task exec -T db \
    psql -U app -d app -tAc "select 'seeded users: ' || count(*) from users"
    echo "project=$COMPOSE_PROJECT_NAME app=$APP_PORT db=$DB_PORT stubs=$STUB_PORT"
  7. Usuń stos i sprzątaj to, co wycieka. env-down usuwa kontenery i wolumeny oraz zwalnia indeks. Uruchamiaj okresowo bin/env-sweep dla stosów, których zadanie skończyło się bez teardownu, na przykład po awarii uruchomienia headless.

    #!/usr/bin/env bash
    # bin/env-down — destroy this task's stack and release its port block
    set -euo pipefail
    set -a; . ./.env.task; set +a
    docker compose -p "$COMPOSE_PROJECT_NAME" -f compose.agent.yml --env-file .env.task down -v --remove-orphans
    rm -rf "${AGENT_ENV_HOME:-$HOME/.agent-envs}/$TASK_INDEX" .env.task

    Skrypt zakłada, że katalog każdego worktree nazywa się tak jak slug zadania, co dają claude --worktree task-142 i bin/env-up task-142. Usuwa każdy stos, którego worktree już nie istnieje, więc porzucone worktree najpierw usuń przez git worktree remove.

    #!/usr/bin/env bash
    # bin/env-sweep — remove stacks whose task has no live worktree (run from cron or CI)
    set -euo pipefail
    state="${AGENT_ENV_HOME:-$HOME/.agent-envs}"
    live=$(git worktree list --porcelain | sed -n 's|^worktree ||p')
    for dir in "$state"/*/; do
    [ -f "$dir/slug" ] || continue
    slug=$(cat "$dir/slug")
    grep -q "/$slug\$" <<<"$live" && continue # worktree named after the slug still exists
    echo "sweeping task-$slug"
    docker compose -p "task-$slug" down -v --remove-orphans
    rm -rf "$dir"
    done

Na koniec spraw, żeby serwer deweloperski i runner testów end-to-end czytały plik env i kończyły się błędem, gdy port jest zajęty: ustaw w Vite server.strictPort na true i wyprowadź port serwera deweloperskiego, webServer.url i use.baseURL z APP_PORT, żeby testy mogły sprawdzić tylko serwer tego zadania.

Skrypty są wszędzie te same; różni się to, kto je uruchamia i gdzie działa stos.

Lokalnie. Każde zadanie zaczynaj we własnym worktree poleceniem claude --worktree task-142 (albo -w). Claude Code tworzy je w .claude/worktrees/. Worktree to świeży checkout, więc poproś Claude’a, żeby najpierw uruchomił bin/env-up task-142, albo zrób to sam. Żeby kopiować pliki z .gitignore, takie jak .env, do każdego nowego worktree, wypisz je w pliku .worktreeinclude w katalogu głównym projektu. Nieinteraktywne uruchomienia z -p nie sprzątają swoich worktree, więc połącz je z okresowym sprzątaniem.

Sesje chmurowe (claude.ai/code, claude --cloud "<task>", rutyny) działają w odizolowanej maszynie wirtualnej skonfigurowanej przez środowisko chmurowe: poziom dostępu do sieci, zmienne środowiskowe i skrypt konfiguracyjny. Sprawdzone 2026-09-26 w przewodniku Anthropic o środowiskach chmurowych:

  • Maszyna ma już Dockera z docker compose, PostgreSQL 16 i Redis 7.0, na Ubuntu 24.04. Sesje hostowane przez Anthropic mają przybliżone limity: 4 vCPU, 16 GB RAM i 30 GB dysku.
  • Skrypt konfiguracyjny działa jako root przed startem Claude Code, musi zakończyć się kodem zero i jest zapisywany jako migawka systemu plików, jeśli skończy się w mniej więcej pięć minut; cache buduje się od nowa, gdy zmienisz skrypt albo dozwolone hosty, albo po mniej więcej siedmiu dniach. Działające procesy nie trafiają do cache, więc pobieraj obrazy w skrypcie konfiguracyjnym, a stos startuj w każdej sesji.
  • Domyślny poziom sieci, Trusted, dopuszcza rejestry paczek, GitHub i Docker Hub. Przy poziomie None instalacje i pobieranie obrazów kończą się błędem.

Pobieranie obrazów umieść w polu Setup script środowiska:

#!/bin/bash
# cloud environment setup script: cached, so pull here, start later
docker pull postgres:16 || true
docker pull wiremock/wiremock || true

Następnie startuj stos w każdej sesji chmurowej hookiem SessionStart w pliku .claude/settings.json repozytorium. Hook działa też lokalnie, więc skrypt kończy się od razu, jeśli CLAUDE_CODE_REMOTE nie ma wartości true:

{
"hooks": {
"SessionStart": [
{
"matcher": "startup|resume",
"hooks": [
{ "type": "command", "command": "bash \"$CLAUDE_PROJECT_DIR\"/scripts/cloud-env-up.sh", "timeout": 300 }
]
}
]
}
}
#!/bin/bash
# scripts/cloud-env-up.sh — one task per VM, so one fixed slug is enough
[ "$CLAUDE_CODE_REMOTE" = "true" ] || exit 0
cd "$CLAUDE_PROJECT_DIR" && bin/env-up cloud

Sesja z kilkoma repozytoriami nie ładuje hooków z .claude/settings.json żadnego z nich, więc w takim przypadku umieść „najpierw uruchom bin/env-up cloud” na początku promptu zadania. Żeby uruchamiać sesje chmurowe na własnej infrastrukturze, plany Team i Enterprise mogą użyć środowisk self-hosted (publiczna beta) i startować w nich sesje przez --environment <id>. Zakończoną sesję chmurową przeniesiesz do terminala poleceniem claude --teleport.

Jak udowodnić, że efemeryczne środowisko cokolwiek izoluje?

Dział zatytułowany „Jak udowodnić, że efemeryczne środowisko cokolwiek izoluje?”

Środowisko to kod harnessu, więc dostaje testy jak każdy inny kod. Uruchom te sprawdzenia przy wprowadzaniu środowiska, po każdej zmianie skryptów i po aktualizacji narzędzia agentowego.

  • Test współbieżności. Wystartuj dwa zadania naraz i uruchom w obu pełny zestaw testów. Oba muszą przejść, każdy env-check musi pokazać inne porty i inny projekt Compose, a wiersz wstawiony w jednej bazie nie może pojawić się w drugiej. To ten test pada, gdy cokolwiek z kontraktu jest współdzielone.
  • Test złego serwera. Zatrzymaj serwer deweloperski zadania A, gdy serwer zadania B nadal działa, i uruchom testy end-to-end zadania A. Muszą zakończyć się błędem połączenia, a nie przejść na serwerze zadania B.
  • Test powtarzalności seeda. Uruchom env-up, zrzuć bazę, usuń stos, powtórz i porównaj oba zrzuty. Każda różnica to niedeterminizm, który wróci później jako niestabilny test.
  • Test sprzątania. Po env-down polecenie docker ps -a --filter label=com.docker.compose.project=task-<slug> nic nie zwraca, a katalogu bloku portów już nie ma.
  • Kanarek. Celowo zepsuj mapowanie zaślepki (zwróć 500 dla wywołania, którego funkcja potrzebuje) i sprawdź, czy testy end-to-end agenta robią się czerwone. Zestaw, który zostaje zielony, nie korzysta z zaślepki.

Następnie spraw, żeby każde uruchomienie agenta zostawiało dowody: wynik env-check, podsumowanie testów i commit, na którym działało. Sesja chmurowa Claude Code może też podlinkować własny transkrypt, bo maszyna udostępnia zmienną CLAUDE_CODE_REMOTE_SESSION_ID; wstaw ten link do opisu pull requesta, żeby recenzent mógł otworzyć uruchomienie, które wyprodukowało zmianę. Recenzent, który widzi dwa równoległe zielone uruchomienia na osobnych blokach portów i bazie z danymi testowymi, nie musi ręcznie odtwarzać funkcji. Strona o pakiecie dowodów definiuje, co każde uruchomienie powinno dołączyć do pull requesta.

Własność jest równie ważna jak skrypty. Tech lead, a tam, gdzie istnieje, zespół platformowy, jest właścicielem bin/env-*, compose.agent.yml, seedów i zaślepek. Chroń te ścieżki przez CODEOWNERS, żeby pull request agenta nie mógł po cichu zmienić środowiska, w którym działają jego własne testy.

Co psuje się w efemerycznych środowiskach i jak z tego wyjść?

Dział zatytułowany „Co psuje się w efemerycznych środowiskach i jak z tego wyjść?”
  • Serwer przeskakuje na następny wolny port i nikt tego nie zauważa. Vite na przykład robi to po cichu, jeśli nie ustawisz server.strictPort, więc aplikacja agenta działa poza jego blokiem. Wyjście: ścisłe opcje portu w każdym serwerze i adresy bazowe wyprowadzane z .env.task w jednym miejscu.
  • Runner end-to-end podłącza się do serwera innego zadania. webServer.reuseExistingServer w Playwright podłącza się do czegokolwiek, co odpowiada pod danym adresem, więc wynik jest zielony dla kodu, który nigdy nie był testowany. Wyjście: wyprowadź adres serwera runnera z portu zadania i uruchom opisany wyżej test złego serwera.
  • Stały container_name albo nazwany wolumen zewnętrzny. Drugie zadanie nie startuje albo oba współdzielą stan. Wyjście: pozwól, żeby projekt Compose nazywał każdy zasób, i szukaj container_name podczas review.
  • Stosy wyciekają. Uruchomienia, które padły, i uruchomienia headless pomijają sprzątanie; uruchomienia Claude Code z -p zostawiają też swoje worktree. Wyjście: uruchamiaj okresowo bin/env-sweep z kroku 7.
  • Sesja chmurowa startuje bez stosu. Skrypt konfiguracyjny, który kończy się kodem różnym od zera, wywraca sesję; taki, który trwa dłużej niż mniej więcej pięć minut, nie trafia do cache, więc każda sesja startuje wolno; stos wystartowany w skrypcie konfiguracyjnym znika, bo migawki zachowują pliki, a nie procesy. Wyjście: pobieraj i instaluj w skrypcie konfiguracyjnym, startuj usługi w hooku SessionStart, a jednorazowe długie pobrania wyprowadź poza skrypt.
  • Agent dopasowuje środowisko do swojego kodu. Edytuje mapowanie zaślepki albo seed, żeby padający test przeszedł. Wyjście: CODEOWNERS na stubs/, seedach i compose.agent.yml, reguła w prompcie zakazująca takich zmian i praktyki ze strony chroń wyrocznię testów.
  • Zaślepki rozjeżdżają się z prawdziwym API. Każde uruchomienie przechodzi na zaślepce, która już nie odpowiada dostawcy. Wyjście: cykliczny job CI, który uruchamia te same testy kontraktowe na sandboksie dostawcy i pada, gdy zaślepka i sandbox się różnią.
  • Prawdziwe poświadczenie jedzie razem ze środowiskiem. Token ustawiony jako zwykła zmienna środowiskowa w środowisku chmurowym może przeczytać każdy, kto z tego środowiska korzysta, według przewodnika Anthropic o środowiskach chmurowych (sprawdzone 2026-09-26). Wyjście: wartości testowe w .env.task, a do wszystkiego, co prawdziwe, ograniczona tożsamość agenta albo poświadczenia API w Anthropic, które proxy agenta dołącza poza maszyną wirtualną (tylko plany Pro i Max; według tego samego przewodnika Team i Enterprise jeszcze ich nie mają).
  • Maszynie wirtualnej brakuje zasobów. Pełny stos plus testy przeglądarkowe mogą przekroczyć limity hostowanej maszyny, a ta może zatrzymać zadanie. Wyjście: przytnij compose.agent.yml do tego, czego zadanie potrzebuje, albo przenieś ciężkie zestawy testów na własne runnery lub do CI.