diff --git a/backend/deploy/e2e/docker-compose.e2e.yml b/backend/deploy/e2e/docker-compose.e2e.yml index ceb617b3a..cedfa2af7 100644 --- a/backend/deploy/e2e/docker-compose.e2e.yml +++ b/backend/deploy/e2e/docker-compose.e2e.yml @@ -37,6 +37,8 @@ services: # smoke stack must model. The images prepare these dirs with app ownership; a real deployment mounts volumes at them. Artifacts__StoreDirectory: /var/lib/codespace/artifacts Agents__RunSpoolDirectory: /var/lib/codespace/spool + volumes: + - artifacts:/var/lib/codespace/artifacts ports: - "18080:8080" @@ -62,5 +64,9 @@ services: CODESPACE_CODEX_CLI_PATH: "/opt/fake-codex" volumes: - ./fake-codex:/opt/fake-codex:ro + - artifacts:/var/lib/codespace/artifacts ports: - "18081:8080" + +volumes: + artifacts: diff --git a/backend/deploy/e2e/run.sh b/backend/deploy/e2e/run.sh index cfc917053..12035883b 100755 --- a/backend/deploy/e2e/run.sh +++ b/backend/deploy/e2e/run.sh @@ -43,6 +43,18 @@ echo "==> health probe: worker" wait_ready "$WORKER" "worker" [ "$(curl -fsS -o /dev/null -w '%{http_code}' "$API/health/live" 2>/dev/null)" = "200" ] || fail "/health/live not 200 (anonymous liveness)" +# A matching path string is not shared storage. Prove the actual production topology invariant through the running +# roles: bytes created by the worker must be immediately readable by the API, and the named volume survives each +# container's independent filesystem lifecycle. This catches the split-brain that leaves durable DB metadata pointing +# at a blob that only ever existed inside one worker image layer. +echo "==> artifact root is physically shared between worker and API" +ARTIFACT_PROBE="cross-role-$(date +%s)-$$" +$COMPOSE exec -T worker sh -c "printf '%s' '$ARTIFACT_PROBE' > /var/lib/codespace/artifacts/.cross-role-probe" || fail "worker could not write artifact probe" +READBACK="$($COMPOSE exec -T api sh -c 'cat /var/lib/codespace/artifacts/.cross-role-probe')" || fail "API could not read worker artifact probe" +[ "$READBACK" = "$ARTIFACT_PROBE" ] || fail "artifact probe readback mismatch" +$COMPOSE exec -T api rm -f /var/lib/codespace/artifacts/.cross-role-probe || fail "API could not clean artifact probe" +echo " worker write → API read succeeded on the shared artifact volume" + # The API image carries no agent-EXECUTION machinery. git is the ONE sanctioned exception, documented in # Dockerfile.api's header (S1): TaskLaunchService resolves a launch's immutable base vector synchronously via # `git ls-remote` (RemoteTipResolver) — read-only, no clone, no working tree. This check forbade it anyway and had diff --git a/docker-compose.yml b/docker-compose.yml index cd53bece3..7c567f908 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -74,6 +74,10 @@ services: # Stated explicitly rather than inheriting appsettings.json's committed dev key — a Production-labelled pod # silently signing tokens with a source-controlled key is exactly the footgun this line makes visible. Authentication__Jwt__SymmetricKey: "${CODESPACE_JWT_SYMMETRIC_KEY:-local-dev-only-jwt-key-minimum-32-chars-long}" + volumes: + # Artifact metadata lives in Postgres, but offloaded bytes must be visible to both roles. An image-layer path + # has the same spelling in each container while still being a different filesystem, and disappears on recreate. + - codespace-artifacts:/var/lib/codespace/artifacts # WORKER = THE LOCAL SANDBOX RUNNER — the Hangfire PROCESSING pod (HangfireHosting=Worker, which is also the # default when the key is absent). Agent runs + the per-run MCP endpoint execute HERE; the harness CLI is spawned as a confined child @@ -120,6 +124,9 @@ services: CODESPACE_MCP_PROXY_PATH: "${CODESPACE_MCP_PROXY_PATH:-}" # Prod (userns-capable pod) arms the fail-closed isolation guard; left off for local dev (see comment above). # Sandbox__RequireConfinement: "true" + volumes: + - codespace-artifacts:/var/lib/codespace/artifacts volumes: codespace-pgdata: + codespace-artifacts: