Add deployment validation check for DinD devserver integration - #653
Conversation
Analyzes the approach for enabling Docker-in-Docker deployment validation in the check phase. Recommends orchestrator-driven DinD where the orchestrator manages the devserver lifecycle and the sandbox runs HTTP validation. Identifies #644 as a hard dependency and proposes a 5-phase implementation plan. Authored-by: egg
Implement the deployment validation system that runs target application devserver stacks during the check phase to validate agent changes against locally running services before PR creation. New components: - DeploymentConfig Pydantic models with path traversal and credential validation (shared/egg_contracts/deployment.py) - DevserverManager lifecycle class handling compose extraction from committed state, air-gapped network creation, and resource-limited container orchestration (orchestrator/devserver.py) - REST API endpoints for start/status/teardown coordination (orchestrator/routes/checks.py) - DeploymentCheck runner that communicates with the orchestrator from the sandbox to validate health checks and run smoke tests (.github/scripts/checks/deployment_check.py) - egg-check network and resource limit constants (shared/egg_config/constants.py) - Auto-teardown on phase complete/fail transitions - 94 new unit tests covering models, loader, DevserverManager, API endpoints, and check runner Issue: #645 Authored-by: egg
Clean up lint errors flagged by ruff: unused imports, Generator from collections.abc, list comprehension variable naming, and type annotation updates. Authored-by: egg
…uard, e2e test Address all 5 issues from cycle-1 review: 1. TASK-6-2 (HIGH): Remove seccomp:unconfined — Docker applies its default seccomp profile automatically when no option is specified. The previous value disabled seccomp entirely, contradicting the security intent. 2. C1 (HIGH): Remove hardcoded IPAM subnet (172.34.0.0/24) from check network creation. Let Docker auto-assign subnets to avoid collisions when multiple pipelines run deployment checks concurrently. 3. S2 (MEDIUM): Add threading.Lock to guard _active_devservers dict in routes/checks.py. The start endpoint now atomically checks-and-sets to prevent TOCTOU races under waitress's multi-threaded model. 4. R1 (LOW): Add early docker SDK None check in DevserverManager.start() to produce a clear error message instead of opaque AttributeError. 5. TASK-7-5: Create integration test at integration_tests/deployment_validation/test_deployment_check_e2e.py covering full lifecycle, teardown idempotency, HEAD extraction, network scoping, and concurrent subnet allocation. Tests skip gracefully when Docker daemon is unavailable. All 94 unit tests pass, 5 integration tests collected (skip in sandbox). Authored-by: egg
No agent-mode anti-patterns found. Implementation is infrastructure code with sandbox-enforced security. All cycle-1 feedback addressed. Authored-by: egg
All 5 cycle-1 issues verified as resolved: - S1/TASK-6-2: seccomp:unconfined removed, Docker default applied - C1: Hardcoded IPAM subnet removed, Docker auto-assigns - S2: threading.Lock guards _active_devservers with TOCTOU prevention - R1: Early docker SDK None check in start() - TASK-7-5: Integration test with 5 e2e test cases 94 unit tests pass. No new issues found. Authored-by: egg
All 30/30 tasks pass acceptance criteria after cycle-2 fixes. Prior issues resolved: seccomp profile, subnet collision, thread safety, docker SDK guard, and integration test. Authored-by: egg
There was a problem hiding this comment.
Deployment Validation Review — PR #653
Thorough review of all changed files. Issues categorized by severity.
Security Issues
1. pipeline_id not validated before use in start_deployment_check (routes/checks.py:107-119)
The start_deployment_check endpoint checks _active_devservers[pipeline_id] before calling store.load_pipeline(pipeline_id) (which triggers _validate_pipeline_id). This means a request with pipeline_id = "../../evil" reaches the dict lookup and the resolve_worktree_path call before validation. While the dict lookup is benign, resolve_worktree_path does filesystem operations with the unvalidated ID.
The pipeline ID validation should happen first, before any logic that uses the ID. Other route handlers (decisions, phases, pipelines) consistently validate the ID early via store.load_pipeline(). Move the store.load_pipeline() call to be the first operation after acquiring the lock, or add an explicit _validate_pipeline_id(pipeline_id) call at the top.
2. docker_client constructor parameter is accepted but never used (devserver.py:143,156)
The constructor accepts a docker_client parameter and stores it as self.docker_client, but every method that needs a Docker client calls docker.from_env() instead (lines 334, 388, 426, 652, 856). This means:
- The injected client is silently ignored, breaking the dependency injection contract
- Tests cannot control Docker client behavior through the constructor
- Every network/container operation creates a new client instance, which is wasteful
Either use self.docker_client (falling back to docker.from_env() if None) or remove the parameter.
3. compose_file path traversal validation is incomplete (deployment.py:147-153)
The validator rejects .. anywhere in the string, but this is a substring check, not a path component check. A value like legit..file.yml would be rejected despite being valid, while the actual security concern (path component ..) is the threat. More importantly, the validation does not check for symlink traversal patterns or encoded path separators. The .. substring check is likely sufficient for practical purposes, but consider using PurePosixPath component-level validation for correctness.
4. Credential warnings are logged but don't block the devserver start (devserver.py:726-728)
When suspicious credential env vars are detected in the compose file (e.g., AWS_SECRET_ACCESS_KEY), the code logs warnings but proceeds to start the stack. Given the security model described in the PR (air-gapped, no credentials), this should either fail the check or at minimum surface the warning in the API response so the caller can make an informed decision. A silently logged warning in the orchestrator's log stream is easy to miss.
Correctness Issues
5. TOCTOU race in start_deployment_check — devserver started outside lock (routes/checks.py:107-164)
The current flow is:
- Acquire lock, check
_active_devservers→ release lock - Start devserver (slow, no lock held)
- Acquire lock, check again, register manager
Between steps 1 and 2, a concurrent request for the same pipeline_id could also pass the check and start its own devserver. The code handles this at step 3 by tearing down the loser, but this means two full Docker compose stacks are started and one is immediately torn down. This wastes resources and could cause network name collisions in _create_check_network (since both call existing.remove() on the same network name).
Consider holding a per-pipeline "starting" sentinel in the dict (not the lock itself) to prevent concurrent starts for the same pipeline.
6. _wait_for_health uses docker compose exec wget which may not exist in target containers (devserver.py:599-611)
The health check executes wget inside the service container via docker compose exec. Many production containers (distroless, Alpine without wget, scratch-based) won't have wget. The health check method should either:
- Use
docker compose exec curlas a fallback - Make HTTP requests from the orchestrator side (it has network access to the containers)
- Use Docker's native health check status (
container.attrs['State']['Health']['Status'])
The E2E test acknowledges this by accepting UNHEALTHY as valid (test_deployment_check_e2e.py:199), which means the health check mechanism is known to be unreliable.
7. _get_changed_files fallback to HEAD~1 is incorrect (devserver.py:503-510)
When git diff --name-only origin/main...HEAD fails, the fallback is HEAD~1..HEAD. This only captures the last commit, not all agent changes. If the agent made multiple commits, the fallback silently misses files, which means services won't get their code mounted. The fallback should at minimum log a warning that it's returning a partial result.
8. _safe_request redirect blocking is incomplete (deployment_check.py:109-114)
The redirect check compares hostnames:
if redirect_host and redirect_host != original_host:
return NoneBut it doesn't follow same-host redirects, meaning a 301 from /health to /healthz on the same host will silently return None (since allow_redirects=False and the redirect response is neither processed nor followed). The method should follow same-host redirects (with a hop limit) instead of returning the 3xx response.
Design Issues
9. _create_check_network creates a new docker.from_env() client on every call (devserver.py:334)
Each of _create_check_network, _create_scoped_network, _remove_check_network, pre_pull_images, and attach_checker independently creates a Docker client via docker.from_env(). This is 5+ client instantiations per lifecycle. Use a single shared client instance (the constructor already accepts one).
10. No size/count limit on _active_devservers dict (routes/checks.py:49)
The _active_devservers dict is a module-level global that grows unbounded. If devservers are leaked (teardown never called), this dict retains references to DevserverManager objects indefinitely. Consider adding a max-size check or periodic cleanup of stale entries.
11. _wait_for_healthy sleep loop blocks the thread (devserver.py:590-631)
The start() method calls _wait_for_health() which sleeps in a loop for up to startup_timeout_seconds (max 600s). Since start() is called from a Flask route handler served by waitress, this blocks one of the 16 waitress threads for the entire duration. For a 120s timeout, that's a significant thread holdout. Consider returning immediately after docker compose up and letting the sandbox poll for health via the status endpoint.
12. Phase teardown integration is asymmetric (phases.py:412,487)
teardown_devserver is called on both complete_phase and fail_phase, but not on advance_phase. If a phase is advanced without being explicitly completed (e.g., forced advance), the devserver for the previous phase would leak. The teardown should also be called in advance_phase.
Minor Issues
13. E2E test cleanup uses 'net1' in dir() (test_deployment_check_e2e.py:306-307)
mgr1._network_id = net1 if 'net1' in dir() else ""This should be 'net1' in locals(), not 'net1' in dir(). dir() returns module-level attributes, not local variables. If _create_check_network() raises before net1 is assigned, this will either use an undefined variable or incorrectly fall back.
14. _check_suspicious_env_vars_in_compose discards env values (devserver.py:557-558)
When parsing list-format environment variables (- KEY=VALUE), the code extracts keys but sets all values to "". The credential regex only checks key names (which is fine), but the discarded values could be useful for a more sophisticated check (e.g., detecting if the value looks like a real credential vs. a placeholder like changeme).
15. Missing __init__.py is empty (integration_tests/deployment_validation/init.py)
This is fine for pytest discovery but the file is empty — just confirming this is intentional.
Test Coverage Gaps
- No test for the TOCTOU race in
start_deployment_check(concurrent requests for the same pipeline) - No test for the
_wait_for_healthtimeout path inDevserverManager.start()(the unit test only testsstart()with no services) - No test for
_safe_requestwith same-host redirects - No test for
attach_checkerfailure scenarios - No negative test for
pre_pull_imageswhenimage_registrycauses image name resolution to fail
Summary
The overall architecture is sound — keeping Docker socket access in the orchestrator, running agent code on air-gapped networks with resource limits, and reading compose config from committed state. The main concerns are:
- The
pipeline_idshould be validated before any use, not after the_active_devserverscheck - The Docker client is accepted in the constructor but never used — pick one pattern
- The health check mechanism (
wgetinside containers) is unreliable as acknowledged by the E2E test - The TOCTOU race in
start_deployment_checkcan cause double-start with network name collisions - The thread-blocking
_wait_for_healthin a route handler will be a scalability problem
Items 1, 4, 5, and 6 should be addressed before merge.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
- Validate pipeline_id before any use in start_deployment_check to prevent path traversal reaching resolve_worktree_path before validation - Use self.docker_client throughout DevserverManager instead of creating new docker.from_env() clients in every method - Use PurePosixPath component-level validation for path traversal checks so filenames like 'legit..file.yml' are not falsely rejected - Surface credential warnings in DevserverStatus API response instead of only logging them - Add per-pipeline starting sentinel to prevent TOCTOU race where two concurrent requests both start Docker compose stacks - Replace wget-inside-container health checks with orchestrator-side HTTP probes so health checks work regardless of container contents - Add warning log when _get_changed_files falls back to HEAD~1 - Follow same-host redirects in _safe_request with a 5-hop limit - Call teardown_devserver in advance_phase to prevent devserver leaks - Fix 'net1' in dir() to 'net1' in locals() in E2E test cleanup
Review Feedback ResponsePushed Security Issues1. 2. 3. 4. Credential warnings not surfaced — Agreed. Added a Correctness Issues5. TOCTOU race in 6. 7. 8. Design Issues9. Docker client created per method — Resolved by fix #2 (single 10. No size limit on 11. Thread-blocking 12. Phase teardown asymmetry — Agreed. Added Minor Issues13. 14. 15. Empty Test UpdatesUpdated test mocks in — Authored by egg |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
…r/tests/test_sse.py
Conflict Resolution SummaryResolved merge conflicts with
Details: The conflict was in Other auto-merged files ( Verification:
No concerns — this was a trivial formatting conflict with no semantic implications. — Authored by egg |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-Review — Deployment Validation PR #653
Delta reviewed: 6e7d1ee..3a3ba9e (review feedback commit) + 5e5a6af (merge conflict resolution).
Previous Issue Verification
Issue #1 — pipeline_id not validated before use: Fixed. store.load_pipeline(pipeline_id) is now the first operation in start_deployment_check, with InvalidPipelineIdError and PipelineNotFoundError handlers returning 400/404. The regex-based validation (issue-[0-9]+|local-[0-9a-f]{8}) plus path traversal check in _get_pipeline_path runs before any dict lookup or resolve_worktree_path call. Correct.
Issue #2 — docker_client constructor parameter never used: Fixed. Constructor now initializes self.docker_client = docker_client or (docker.from_env() if docker else None) and all methods (_create_check_network, _create_scoped_network, _remove_check_network, pre_pull_images, attach_checker) use self.docker_client instead of calling docker.from_env(). This also resolves issue #9. Correct.
Issue #3 — compose_file path traversal using substring check: Fixed. All three validators now use ".." in PurePosixPath(v).parts for component-level checking. legit..file.yml is correctly accepted while ../../evil is rejected. Correct.
Issue #4 — Credential warnings not surfaced: Fixed. DevserverStatus has a new warnings field, to_dict() conditionally includes it, and start() populates it from the credential check. Callers see warnings in the API response. Correct.
Issue #5 — TOCTOU race in start_deployment_check: Fixed. A _starting_devservers sentinel set is checked and populated atomically under _devservers_lock. Concurrent requests for the same pipeline get 409. The sentinel is cleared in all exit paths (success, config-not-found, and error). Correct.
Issue #6 — _wait_for_health uses wget inside containers: Fixed. Replaced with orchestrator-side HTTP probes using urllib.request.urlopen. New _get_container_ip() method looks up the container IP on the check network via Docker SDK. Correct approach — no longer depends on tools inside containers.
Issue #7 — _get_changed_files fallback to HEAD~1 is incomplete: Fixed. A logger.warning is now emitted when the origin/main diff fails, noting the fallback may return a partial list. Acceptable — the warning provides visibility into the incomplete fallback.
Issue #8 — _safe_request redirect blocking is incomplete: Fixed. Same-host redirects are now followed recursively with a 5-hop limit via _redirect_depth parameter. Cross-host redirects still blocked. Correct.
Issue #12 — Phase teardown asymmetry: Fixed. teardown_devserver(pipeline_id) is now called in advance_phase after store.save_pipeline(), matching complete_phase and fail_phase. Correct.
Issue #13 — 'net1' in dir() in E2E test: Fixed. Changed to 'net1' in locals(). Correct.
Issues #10, #11, #14 — Not addressed: Acknowledged in the response as intentional deferral. Acceptable — #10 (no size limit on _active_devservers) is low-risk given the sentinel fix, #11 (thread-blocking) is bounded and infrequent, #14 (env value check) is a nice-to-have.
New Issues in the Delta
1. _safe_request redirect preserves stream=True and allow_redirects=False through kwargs on recursive calls (deployment_check.py:126-128)
When _safe_request follows a same-host redirect, it passes **kwargs to the recursive call. The method also force-sets kwargs["stream"] = True and kwargs["allow_redirects"] = False on every call (lines 111-112), so this is actually fine — the settings are re-applied. However, the timeout value is also passed through via kwargs, meaning each redirect hop gets a fresh full timeout rather than sharing a total time budget. With 5 hops × 10s timeout, a malicious same-host redirect chain could block for up to 50 seconds. This is unlikely to be exploitable in practice (the services are on an air-gapped network the orchestrator controls), but worth noting for awareness.
Not blocking.
2. _get_container_ip silently swallows all exceptions (devserver.py:607)
The bare except Exception: return "" in _get_container_ip catches everything, including docker.errors.NotFound (container was removed), AttributeError (if self.docker_client is None), and network-level errors. When the container genuinely doesn't exist, the health check loop will retry for the full startup_timeout_seconds instead of failing fast. This is the same category as the pre-existing "thread-blocking" concern — bounded but wasteful.
At minimum, logging the exception at DEBUG level inside the except block would help with troubleshooting when health checks fail to detect containers.
Not blocking — the timeout bounds the impact.
3. _get_container_ip can return an IP for a stopped/exited container (devserver.py:570-608)
docker compose ps -q returns container IDs regardless of whether they're running. If a container crashes right after starting, _get_container_ip will still return an IP for it, and the urllib.request.urlopen call will hang until timeout rather than getting a quick connection-refused. Not a correctness issue (the health check will correctly report unhealthy), but it means the timeout feedback loop is slower than necessary.
Not blocking.
4. E2E test still accepts UNHEALTHY (test_deployment_check_e2e.py:201-209)
The test comment was updated from "wget may not be available" to "container may not be reachable from orchestrator network", but it still accepts UNHEALTHY as a passing test state. With the orchestrator-side probes, the test should now reliably get HEALTHY if the E2E test's busybox httpd is responding. The continued acceptance of UNHEALTHY means the E2E test cannot catch regressions where health probes silently fail. Consider tightening this to only accept HEALTHY once the orchestrator-side probes are confirmed working in CI.
Not blocking — the test is conservative, which is understandable for an initial iteration.
Verification Summary
All 5 items I flagged as must-fix (issues #1, #4, #5, #6, #12 — the pipeline_id validation, TOCTOU race, health check mechanism, and phase teardown asymmetry) have been properly addressed. The fixes are structurally correct, not superficial.
No new blocking issues found in the delta. The changes are clean, the test mocks have been correctly updated to use constructor injection, and the merge conflict resolution is trivial (loop variable rename).
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Add DEBUG-level logging in _get_container_ip exception handler to aid troubleshooting when health checks fail to detect containers. Tighten E2E test assertion to only accept HEALTHY now that orchestrator-side HTTP probes are in use, ensuring the test catches regressions. Authored-by: egg
Re-Review Feedback ResponsePushed Issue #2 — Issue #4 — E2E test still accepts UNHEALTHY: Agreed. Tightened the assertion to only accept Issues #1, #3 — Redirect timeout accumulation and stopped container IPs: Acknowledged as awareness items. The timeout accumulation is bounded (5 hops × 10s on an air-gapped network) and the stopped-container case is handled correctly by the health check timeout. No changes made. — Authored by egg |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-Review — Deployment Validation PR #653 (commit 15c9631)
Delta reviewed: 5e5a6af..15c9631 (re-review feedback commit). Full PR re-read for context.
Previous Issue Verification
All items from the previous re-review have been addressed:
Issue #2 — _get_container_ip silently swallows exceptions: Fixed. logger.debug() with exc_info=True added in the exception handler (devserver.py:608-611). Correct.
Issue #4 — E2E test still accepts UNHEALTHY: Fixed. Assertion tightened to assert status.status == DevserverStatusValue.HEALTHY (test_deployment_check_e2e.py:205). Correct.
New Issues Found
1. Orchestrator-side health probes lack port awareness — will fail for any service not on port 80 (devserver.py:654)
_wait_for_health constructs the probe URL as:
url = f"http://{ip}{health_path}"This hits port 80 by default. But devserver services typically listen on custom ports (the E2E test's busybox httpd listens on 8080). The DeploymentConfig.health_endpoints maps service name → path only, with no port field. The ServiceStatus.port field exists (default 0) but is never populated by _wait_for_health or _get_container_ip.
The E2E test (test_deployment_check_e2e.py:205) asserts HEALTHY, but this can only succeed if busybox httpd's health check (which uses Docker's native healthcheck, not the orchestrator probe) causes the container to report healthy through some other mechanism — or if the test environment happens to have port 80 routing. In a real deployment with a service on port 8080, the orchestrator probe will connect to port 80, get connection refused, and report UNHEALTHY every time.
The DeploymentConfig needs a port field in the health endpoint specification (e.g., health_endpoints: {echo: {path: /health, port: 8080}}), or the _get_container_ip method should also extract the exposed port from the container's config. The sandbox-side checker (deployment_check.py:258) already handles ports correctly (f"http://{ip}:{port}{health_path}" if port else ...), so the data model supports it — it's just not populated on the orchestrator side.
This is a correctness bug that will cause all orchestrator-side health probes to fail for services on non-standard ports. The health check timeout will elapse, status will be UNHEALTHY, but the check continues anyway (UNHEALTHY is not terminal in the sandbox-side runner). So this won't block deployments, but it means the orchestrator's health status is unreliable and the _wait_for_health call wastes up to startup_timeout_seconds (120s default) blocking a waitress thread every time.
2. _safe_request silently fails on relative redirect URLs (deployment_check.py:116-125)
When a service returns a relative redirect (e.g., Location: /healthz):
urlparse("/healthz").hostnamereturnsNone- The cross-host check
if redirect_host and redirect_host != original_hostis bypassed (None is falsy) - The recursive call passes
"/healthz"as the URL torequests.request() requestsraisesMissingSchema("No scheme supplied")- The bare
except Exception: return Noneswallows it
The net effect: relative redirects (which are common and valid per RFC 7231 §7.1.2) are silently treated as failures. The fix is to resolve relative URLs against the original request URL before following them:
from urllib.parse import urljoin
# ...
redirect_url = urljoin(url, resp.headers.get("Location", ""))This also correctly handles the cross-host check for absolute redirects.
Not blocking — the air-gapped network makes redirects to relative URLs a minor edge case, but the current code is incorrect.
Verification Summary
The two non-blocking suggestions from the previous re-review (debug logging and tighter E2E assertion) are correctly implemented.
Issue #1 (port-unaware health probes) is a correctness bug that affects any real-world usage — it should be fixed before merge. The health probe URL must include the service port, which means either the DeploymentConfig.health_endpoints schema needs a port field, or the port should be extracted from the container/compose config.
Issue #2 (relative redirect handling) is not blocking but should be noted for a follow-up.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Health probes now extract the container's exposed port from Docker config (ExposedPorts) and include it in the probe URL, fixing a bug where all probes hit port 80 regardless of the service's actual port. Also populate ServiceStatus.port so the sandbox-side checker receives correct port info. Fix _safe_request to resolve relative redirect URLs (e.g. "/healthz") against the original request URL using urljoin before following them, preventing MissingSchema errors that were silently swallowed.
Addresses non-blocking review suggestions from the latest re-review: - Document the single-port assumption in _get_container_endpoint with a comment noting that multi-port containers will use the first exposed port (Dockerfile EXPOSE order) and that an optional port field in DeploymentConfig.health_endpoints could remove ambiguity. - Add 6 unit tests for _get_container_endpoint covering: single port extraction, multi-port first-key selection, no exposed ports, malformed port keys, empty compose output, and Docker client exceptions. - Add 6 unit tests for _safe_request covering: same-host absolute redirects, relative redirect resolution (urljoin), cross-host redirect blocking, redirect depth limit enforcement, non-redirect passthrough, and connection error handling. All 106 PR-related tests pass (38 devserver + 17 deployment_check + 51 deployment_config).
|
Addressed the non-blocking suggestions from the latest re-review in commit 74dabce: Issue #1 (multi-port ambiguity in Issue #2 (no unit tests for Issue #3 (no unit tests for All 106 PR-related tests pass (38 devserver + 17 deployment_check + 51 deployment_config). — Authored by egg |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-Review — Deployment Validation PR #653 (commit 74dabce)
Delta reviewed: 47f6f93..74dabce (non-blocking suggestion fixes: multi-port comment, endpoint tests, redirect tests).
Previous Suggestion Verification
Suggestion #1 — Multi-port ambiguity in _get_container_endpoint: Addressed. Comment added at devserver.py:614-620 documenting the single-port assumption, the fragility for multi-port containers, and the future enhancement path. Clear and accurate.
Suggestion #2 — No unit tests for _get_container_endpoint: Addressed. Six tests added in TestGetContainerEndpoint (test_devserver.py:340-451):
test_single_port— happy path, verifies IP and port extraction. Correct.test_multi_port_picks_first— verifies first-key behavior. Correct (relies on CPython 3.7+ dict insertion order, which the comment documents).test_no_exposed_ports_returns_zero— verifies fallback whenConfighas noExposedPorts. Correct.test_malformed_port_key_returns_zero— verifies"notaport/tcp"triggersValueErrorinint()and falls through. Correct.test_empty_compose_output_returns_empty— verifies""stdout → early return. Correct.test_docker_client_exception_returns_empty— verifies the bareexcept Exceptionpath returns("", 0). Correct.
All tests properly mock subprocess.run to control docker compose ps -q output and use the Docker client mock for containers.get. The hardcoded network name "egg-check-issue-645" matches the EGG_CHECK_NETWORK_PREFIX constant ("egg-check") + pipeline_id ("issue-645").
Suggestion #3 — No unit tests for _safe_request redirects: Addressed. Six tests added in TestSafeRequestRedirects (test_deployment_check.py:256-349):
test_follows_same_host_absolute_redirect— verifies 301 with same-host absolute URL is followed. Correct.test_follows_relative_redirect— verifies/healthzis resolved tohttp://172.20.0.2:8080/healthzviaurljoin. Verified the expected URL is correct.test_blocks_cross_host_redirect— verifies cross-host 301 returnsNone. Correct.test_enforces_redirect_depth_limit— verifies 6 requests (1 original + 5 redirects) thenNone. Matches the implementation:_redirect_depth > 5triggers at depth 6, which is the 7th call to_safe_requestbut only 6requests.requestcalls since depth 6 returns early. Correct.test_non_redirect_returned_directly— verifies 200 is returned without recursion. Correct.test_connection_error_returns_none— verifiesConnectionErroris caught. Correct.
Minor Observations
1. Duplicate section header comment (test_devserver.py:337,454)
The "DevserverStatus Tests" section divider comment appears twice — once before TestGetContainerEndpoint (line 337) and once before TestDevserverStatus (line 454). The first one was the original header; the new class was inserted between the header and the class it labeled. The TestGetContainerEndpoint class now sits under a "DevserverStatus Tests" header, which is slightly misleading. Trivial.
2. Only the formatting-only change in test_deployment_check.py:88-90
The with patch.object(...) call was reformatted for line length. No semantic change. Fine.
Verification Summary
All three non-blocking suggestions from the previous approval have been properly addressed. The new tests are well-structured, correctly mock the dependency boundaries (subprocess.run, Docker SDK, requests.request), and cover the important edge cases identified in the review (malformed ports, empty compose output, relative redirects, depth limits, cross-host blocking).
No new issues found. The delta is clean.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Rename 'DevserverStatus Tests' header above TestGetContainerEndpoint to 'Container Endpoint Tests' — the original header was duplicated when the new test class was inserted between the header and the TestDevserverStatus class it labeled.
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-Review — Deployment Validation PR #653 (commit df165cd)
Delta reviewed: 74dabce..df165cd (section header fix in test_devserver.py). Full PR re-read for context.
Previous Suggestion Verification
Suggestion #1 — Duplicate section header comment in test_devserver.py: Fixed. The section header above TestGetContainerEndpoint (line 337) now reads "Container Endpoint Tests" instead of "DevserverStatus Tests". The original "DevserverStatus Tests" header (line 454) correctly labels TestDevserverStatus (line 457). Each section header now matches the class it precedes. Correct.
New Issue Found
1. _starting_devservers sentinel leaked on non-DevserverError exceptions (routes/checks.py:139-173)
After the sentinel is set (_starting_devservers.add(pipeline_id)) inside the lock, several operations run outside any try/finally that would clear it:
_starting_devservers.add(pipeline_id) # line ~131
worktree_path = resolve_worktree_path(...) # can raise
deployment_config = load_deployment_config(...) # can raise ValueError
# (None return is handled — sentinel cleared correctly)
manager = DevserverManager(...) # can raise if docker.from_env() failsThen manager.start() is wrapped in try/except DevserverError, but non-DevserverError exceptions propagate uncaught.
If any of these raise an exception that is not DevserverError — for example:
load_deployment_config()raisesValueErrorfor malformed YAMLDevserverManager()raisesdocker.errors.DockerExceptionif the Docker daemon is unreachablemanager.start()raises an unexpectedTypeError,KeyError, etc.
…then pipeline_id stays in _starting_devservers permanently. All future start requests for that pipeline return 409 ("Devserver is already being started") with no way to recover short of restarting the orchestrator.
The fix: wrap everything after the sentinel is set (and before it's cleared on success) in a try/finally:
_starting_devservers.add(pipeline_id)
try:
worktree_path = resolve_worktree_path(pipeline_id, repo_path)
deployment_config = load_deployment_config(worktree_path)
if deployment_config is None:
return jsonify(...), 422 # finally clears sentinel
manager = DevserverManager(...)
status = manager.start(deployment_config)
with _devservers_lock:
_active_devservers[pipeline_id] = manager
return jsonify(...), 200
except DevserverError as e:
manager.teardown()
return jsonify(...), 500
except Exception as e:
logger.error("Unexpected error during devserver start", ...)
if 'manager' in locals():
manager.teardown()
return jsonify(...), 500
finally:
with _devservers_lock:
_starting_devservers.discard(pipeline_id)This ensures the sentinel is always cleaned up regardless of exception type.
This is a correctness bug that can permanently wedge a pipeline's deployment check capability.
Verification Summary
The section header fix from the previous approval is trivially correct.
However, a full re-read of the source reveals the sentinel leak bug in routes/checks.py that was missed in earlier reviews. The sentinel _starting_devservers is only cleared in two paths: the success path and the except DevserverError path. Any other exception type (ValueError from config loading, DockerException from client init, or any unexpected error from manager.start()) permanently blocks future start requests for that pipeline. This should be fixed before merge.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
…ceptions Wrap all operations after _starting_devservers.add() in try/finally so the sentinel is always cleaned up. Previously, exceptions from resolve_worktree_path, load_deployment_config (ValueError), DevserverManager constructor (DockerException), or unexpected errors from manager.start() would leave pipeline_id in _starting_devservers permanently, causing all future start requests for that pipeline to return 409 with no recovery path short of restarting the orchestrator.
|
Pushed Issue #1 — All 114 PR-related tests pass (38 devserver + 8 route checks + 17 deployment check + 51 deployment config). — Authored by egg |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-Review — Deployment Validation PR #653 (commit 80dd80b)
Delta reviewed: df165cd..80dd80b (sentinel leak fix).
Previous Issue Verification
Sentinel leak in start_deployment_check for non-DevserverError exceptions: Fixed. The entire post-sentinel block is now wrapped in try/finally. The finally block unconditionally calls _starting_devservers.discard(pipeline_id) under _devservers_lock, covering all exit paths:
- Early return on no deployment config (422)
- Success (200) — sentinel discarded after manager is registered in
_active_devservers DevserverError(500)- Unexpected
Exception(500) BaseExceptionsubclasses (SystemExit,KeyboardInterrupt)
The manager = None initialization before the try block correctly guards the if manager is not None: manager.teardown() calls in both except handlers, preventing NameError when exceptions occur before DevserverManager() is constructed (e.g., from resolve_worktree_path or load_deployment_config). Correct.
The window between _active_devservers[pipeline_id] = manager (line 184, under lock) and _starting_devservers.discard(pipeline_id) (line 227, under lock) on the success path is harmless — concurrent requests would hit the _active_devservers check first (line 132) and return 409 before reaching the _starting_devservers check (line 146).
New Observations
1. No tests verify sentinel cleanup in error paths (test_routes_checks.py)
The test suite verifies response codes and bodies for the basic paths (200, 404, 409, 422) but no test asserts that _starting_devservers is empty after an error scenario. Specifically:
- No test for
DevserverErrorduringmanager.start()→ verifying sentinel is cleaned up - No test for unexpected
Exception(e.g.,ValueErrorfrom config loading,DockerExceptionfrom constructor) → verifying sentinel is cleaned up - No test for the 409 "already being started" path (a request while
pipeline_idis in_starting_devservers)
The sentinel leak was the exact bug this commit fixes. Without regression tests, the fix could be silently broken by future refactoring. A test like the following would cover it:
def test_start_cleans_sentinel_on_unexpected_error(self, ...):
"""Sentinel is cleaned up even for non-DevserverError exceptions."""
mock_resolve_wt.side_effect = RuntimeError("boom")
resp = client.post("/api/v1/pipelines/issue-123/deployment-check/start")
assert resp.status_code == 500
assert "issue-123" not in _starting_devserversNot blocking — the fix is structurally correct and the try/finally pattern is a well-understood Python idiom. But this is the kind of bug that warrants a regression test.
Verification Summary
The sentinel leak fix is correct. The try/finally approach is the right pattern — it's simpler and more robust than the previous approach of manually discarding the sentinel in each exit path. The code is clear and the comment (lines 155-158) explains the rationale well.
No blocking issues found. The only suggestion is adding tests that assert sentinel cleanup after error scenarios to prevent regression.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
Pushed Suggestion #1 — No tests verify sentinel cleanup in error paths: Agreed. Added three regression tests to
All 11 route check tests pass. — Authored by egg |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-Review — Deployment Validation PR #653 (commit d7e2ce5)
Delta reviewed: 80dd80b..d7e2ce5 (sentinel cleanup regression tests).
Previous Issue Verification
Sentinel cleanup regression tests: Addressed. Three tests added covering all three scenarios requested:
test_start_conflict_already_starting— verifies 409 whenpipeline_idis already in_starting_devserverstest_start_cleans_sentinel_on_devserver_error— verifies sentinel cleanup andmanager.teardown()call whenmanager.start()raisesDevserverErrortest_start_cleans_sentinel_on_unexpected_error— verifies sentinel cleanup whenresolve_worktree_pathraisesRuntimeErrorbeforeDevserverManageris constructed
All three tests correctly import _starting_devservers from the route module (same object reference, not a copy), and the fixture's setup/teardown clears both _starting_devservers and _active_devservers. Mock decorator ordering matches parameter ordering. Test assertions align with the production code paths.
Observations on the New Tests
1. test_start_cleans_sentinel_on_unexpected_error does not assert that teardown() was NOT called
In the DevserverError test, mock_manager.teardown.assert_called_once() verifies cleanup of the Docker stack. In the unexpected error test, manager is never assigned (the exception fires at resolve_worktree_path, before DevserverManager() is constructed), so there's no mock manager to assert against. The test correctly verifies the sentinel is cleaned, but it doesn't verify that the manager is not None guard in the except block prevented a teardown attempt. This is fine — the guard is trivially correct and the test's primary purpose is sentinel cleanup verification.
2. Module-level DevserverError import (line 23)
The new from devserver import DevserverError at module level is the right approach — it avoids repeated function-scoped imports and is consistent with how DevserverStatus and DevserverStatusValue are already imported in function scope in other tests. Slightly inconsistent (module-level for the error class, function-scoped for status classes), but not worth changing.
Verification Summary
The requested regression tests are correctly implemented. The three tests cover the sentinel set lifecycle across success, DevserverError, and unexpected exception paths — directly targeting the bug that commit 80dd80b fixed.
No issues found.
— Authored by egg
|
egg review completed. View run logs 23 previous review(s) hidden. |
Update documentation to reflect deployment validation feature added in #653: - Add deployment_check.py, deployment.py, and devserver.py to STRUCTURE.md - Document deployment validation in architecture README and SDLC guide - Add comprehensive devserver management section to orchestrator architecture - Update phase default checks to include deployment validation (optional) Authored-by: egg
The deployment_check.py script was deleted during merge conflict resolution (it depended on deleted .github/scripts/checks/ base infrastructure from the GHA SDLC removal). The CheckDefinition referencing it remained as a dangling reference from PR #653. Remove it to avoid a potential ValueError if run_check.py tries to dispatch an unknown 'deployment' check.
* docs: Document deployment validation check system Update documentation to reflect deployment validation feature added in #653: - Add deployment_check.py, deployment.py, and devserver.py to STRUCTURE.md - Document deployment validation in architecture README and SDLC guide - Add comprehensive devserver management section to orchestrator architecture - Update phase default checks to include deployment validation (optional) Authored-by: egg * Fix docs: correct API paths, YAML schema, and credential patterns - Fix API endpoint paths: checks/devserver → deployment-check to match actual routes in orchestrator/routes/checks.py - Fix YAML config examples to match DeploymentConfig schema: remove version field, rename tests→validation_tests, rename name→description, url→path, add service field, add health_endpoints top-level dict, remove health_endpoint from services - Expand credential safety pattern list to include all patterns from the actual regex: *_API_KEY, *_ACCESS_KEY, *_PASSWORD, *_CREDENTIALS, plus GCP_*, AZURE_*, GOOGLE_CLOUD_* prefixes * Fix check ordering in docs to match phase_defaults.py The implement phase check list in sdlc-pipeline.md and README.md listed deployment validation before auto-fixer, but phase_defaults.py defines check-fixer before check-deployment. Reorder docs to match the code. Authored-by: egg --------- Co-authored-by: jwbron <8340608+jwbron@users.noreply.github.com> Co-authored-by: egg-reviewer[bot] <261018737+egg-reviewer[bot]@users.noreply.github.com>
* Add analysis for issue #545: Remove GitHub Actions SDLC architecture * Add implementation plan for issue #545: Remove GitHub Actions SDLC architecture * Remove GitHub Actions SDLC workflows and supporting scripts The SDLC pipeline has been fully migrated to the local distributed orchestrator (PR #524). This removes the now-superseded GitHub Actions implementation: 8 workflow files, 3 supporting scripts, the checks/ directory, 14 action prompt builders, and associated tests. All documentation is updated to reference the orchestrator equivalents. Issue: #545 Authored-by: egg * Add implement phase check results * Fix lint errors and stale test for invalid schema validation - Fix 8 auto-fixable ruff errors: remove unused imports (F401), update typing imports to collections.abc (UP035), sort import block (I001), remove unused variable (F841) - Fix 6 E741 ambiguous variable names: rename `l` to `line` in list comprehensions across SSE test files - Fix test_load_invalid_schema_fails: Pipeline model fields became optional in #554, so {"id": "issue-9998"} is now valid. Updated test to use an invalid enum value for status instead. Authored-by: egg * Add implement phase check results for issue-545 * Add unified review verdict for issue-545 implement phase * Add agent-design review verdict for issue-545 implement phase * Add code review verdict for issue-545 implement phase Reviewed security, correctness, robustness, and design across the v2 checkpoint system, session manager changes, workflow removals, and orchestrator updates. No critical issues introduced by this diff. Authored-by: egg * Add contract review verdict for issue-545 implement phase * Address review feedback: remove stale references and fix docstring Remove dangling references to deleted files (sdlc-work-loop.yml, build-sdlc-prompt.sh) from comments and docstrings. Fix redundant phrasing in _populate_contract_from_plan docstring. PR description updated separately to correct inaccurate claims about action/ and config/ directory removal. * Restore PR-operational checks, contract validator, and prompt builder Restores files that were incorrectly removed as part of the SDLC cleanup: - .github/scripts/checks/ directory (check_fixer, lint_check, test_check, merge_conflict_check, draft_validation_check, plan_yaml_check, base, run_check) - .github/workflows/on-pull-request-contract-verify.yml - action/build-contract-verification-prompt.sh - tests/scripts/test_checks.py Removes deployment_check from run_check.py registry (SDLC-only, correctly deleted). Updates docs, README, and test-action.yml shellcheck to include restored files. * Remove stale check-deployment definition from phase defaults The deployment_check.py script was deleted during merge conflict resolution (it depended on deleted .github/scripts/checks/ base infrastructure from the GHA SDLC removal). The CheckDefinition referencing it remained as a dangling reference from PR #653. Remove it to avoid a potential ValueError if run_check.py tries to dispatch an unknown 'deployment' check. * Address review feedback: fix EDITOR handling, add subprocess timeouts, add reconnection backoff - Fix _launch_editor to use shlex.split for multi-word $EDITOR values (e.g. "code --wait", "vim +10") - Add timeout=30 to all subprocess.run calls in _commit_statefiles_to_worktree to prevent indefinite hangs on git lock contention - Add exponential backoff (1s-30s) and max retry limit (20) to watch_pipeline reconnection loop to prevent resource exhaustion - Use consistent word-boundary regex matching in _detect_phase for all phase keywords instead of mixing substring and regex strategies - Narrow _find_repo_path exception handler from bare Exception to specific subprocess/OS error types Authored-by: egg --------- Co-authored-by: egg <egg@localhost> Co-authored-by: egg-reviewer[bot] <261018737+egg-reviewer[bot]@users.noreply.github.com> Co-authored-by: jwbron <8340608+jwbron@users.noreply.github.com>
Add deployment validation check for DinD devserver integration
The check phase currently validates agent-authored changes with lint, tests, and
merge-conflict detection — all static or unit-level. This PR adds orchestrator-driven
Docker-in-Docker deployment validation so the check phase can spin up a target
application's devserver stack and run HTTP health checks and API smoke tests against
real running services with the agent's code mounted in.
Key components:
orchestrator/devserver.py— Full devserver lifecycle manager: extracts composeconfig from committed state (not worktree), generates override files with read-only
agent code mounts, creates air-gapped
egg-checknetworks (internal: true, nogateway, restricted inter-container traffic), manages startup/health-wait/teardown
with resource limits and hard time caps.
orchestrator/routes/checks.py— New/checks/deploymentroutes that coordinatethe orchestrator-managed devserver with sandbox-based validation. The orchestrator
starts services before spawning the checker and tears them down after.
shared/egg_contracts/deployment.py— Deployment configuration contracts: servicemappings, health check definitions, network policies, and resource limits with
validation.
.github/scripts/checks/deployment_check.py— Sandbox-side checker that runshealth probes, API smoke tests, and service readiness validation against the
orchestrator-managed devserver stack. Parses responses defensively against
attacker-controlled service output.
shared/egg_contracts/phase_defaults.py— Addscheck-deploymentdefinition(optional, retry-enabled) to the check phase.
Security model: The sandbox never gets Docker socket access. The orchestrator
(trusted) manages the entire devserver lifecycle. Agent code runs inside devserver
containers on an air-gapped network with no egress, no credentials, no capabilities,
default seccomp, and read-only source mounts. Compose config is read from committed
state so the agent cannot modify Dockerfiles or init scripts.
Also removes the deprecated
sandbox/egg_lib/orch_cli.pyand related sandbox-sideorchestrator shims that are superseded by the orchestrator-driven approach.
Issue: #645
Test plan:
pytest orchestrator/tests/test_devserver.py— devserver lifecycle,network creation, compose override generation, cleanup
pytest orchestrator/tests/test_routes_checks.py— deployment checkAPI endpoints
pytest tests/shared/egg_contracts/test_deployment_config.py—configuration validation, service mapping, resource limits
pytest tests/scripts/test_deployment_check.py— sandbox-sidechecker logic, health probing, defensive parsing
pytest tests/integration/deployment_validation/test_deployment_check_e2e.py— full orchestrator + checker integration with mock Docker
Authored-by: egg