Unify local-mode and issue-mode pipeline behavior - #554
Conversation
…d file restrictions Phase 1 of issue #543: - Add phase_file_restrictions section to phase-permissions.json defining allowed/blocked patterns per phase (refine, plan, implement, pr) - Add PhaseFileRestriction dataclass and check_phase_file_restrictions() method to PhaseFilter for validating files against phase restrictions - Remove blanket local-mode push block from gateway.py - Add phase-based file restriction enforcement in push handler - Add checkpoint branch bypass (egg/checkpoints/v1 always allowed) This enables local-mode pipelines to push files while still enforcing phase-appropriate restrictions: - refine: contracts, analysis drafts, checkpoints only - plan: contracts, plan drafts, checkpoints only - implement: code only, no .egg-state/ (except checkpoints) - pr: everything allowed Authored-by: egg
Phase 2 of issue #543: - Add force_commit parameter to StateStore.save_pipeline() and delete_pipeline() to override mode-based commit skip - Update phase transitions in _run_pipeline_execution() to commit local pipeline state at phase boundaries (phase changes and completion) This ensures local pipeline state is persisted to git history at important checkpoints while still avoiding per-operation commits during normal execution. Authored-by: egg
…dict files Phase 3 of issue #543: - Update _get_draft_path() to use pipeline_id prefix for local mode (e.g., local-abc12345-analysis.md instead of analysis.md) - Update _verdict_path_for_type() to use pipeline_id prefix for local mode (e.g., local-abc12345-refine-unified-review.json) - Update _read_phase_draft() to accept and pass through pipeline_id - Update _read_review_verdict() to accept and pass through pipeline_id - Update _build_phase_prompt() to use _get_draft_path() for agent instructions - Update _build_review_prompt() to pass pipeline_id to helpers - Update all callers to pass pipeline_id parameter This enables multiple concurrent local pipelines in the same repo by ensuring each pipeline uses uniquely-prefixed file paths. Authored-by: egg
…_synced Phase 4 of issue #543: - Add contract CLI instructions (egg-contract show/add-commit) to local mode agent prompts in the implement phase - Update phase restrictions in prompts to reflect new unified push behavior (local mode can now push with phase-specific file restrictions) - Set contract_synced=False when creating pipelines (both modes) since contract is created separately from pipeline - Set contract_synced=True only after contract is successfully created This ensures local-mode pipelines use the same contract workflow as issue-mode pipelines. Authored-by: egg
Phase 6 of issue #543: - Add AgentExecution tracking to _spawn_and_wait() alongside container tracking - Record agent with RUNNING status when container spawns - Update agent status to COMPLETE or FAILED when container exits - Include error message when agent fails This ensures the API accurately reports both containers and agents in PhaseExecution.containers[] and PhaseExecution.agents[]. Authored-by: egg
Phase 7 of issue #543: - Add test_unified_pipeline_behavior.py with integration tests covering: - Local pipeline contract creation with pipeline_id key - Prefixed file paths for concurrent pipelines - contract_synced flag management - Phase-based push restriction session setup - Container/agent tracking via API - State file persistence - Checkpoint branch bypass constant - Add unit tests to test_phase_filter.py for new phase-based restrictions: - refine/plan phases allowing only state files - implement phase allowing code but blocking .egg-state/ - pr phase allowing everything - Checkpoints always allowed - PhaseFileRestriction dataclass tests Authored-by: egg
Fixes test failures from Phase 7 integration tests: - Remove allowed_patterns from implement phase in phase-permissions.json and default restrictions. When no allowed_patterns are specified, all files are allowed except those matching blocked_patterns. - Update default permissions to allow push in refine/plan phases (file restrictions are applied separately via phase_file_restrictions) - Fix tests that expected push to be blocked in refine/plan phases - Change test for unknown phase to expect ValueError (enum validation) The implement phase now correctly allows code files while blocking .egg-state/ files (except checkpoints which don't match blocked patterns). Authored-by: egg
There was a problem hiding this comment.
Code Review: PR #554 - Unify local-mode and issue-mode pipeline behavior
Summary
This PR replaces the blanket local-mode push blocking with phase-based file restrictions, enabling unified behavior between local and issue mode pipelines. The overall design is sound and follows existing patterns, but there are a few issues that need addressing.
Issues Requiring Changes
1. BUG: Unhandled ValueError in gateway.py when session_phase is invalid
File: gateway/gateway.py:680
phase_result = check_phase_file_restrictions(session_phase, changed_files)The convenience function check_phase_file_restrictions raises ValueError for unknown phase strings (line 855 in phase_filter.py: phase = PipelinePhase(phase)). This call is not wrapped in a try-except, so an invalid session_phase value will cause an unhandled exception and a 500 error instead of graceful handling.
Fix: Either:
- Wrap the call in try-except and handle ValueError (return 500 with clear error message), OR
- Modify the convenience function to match the class method behavior (which already handles unknown phases gracefully and allows by default with a log message)
The second option is preferred since it maintains fail-safe behavior without exposing internal implementation details in error messages.
2. BUG: Inconsistency between class method and convenience function for unknown phases
File: gateway/phase_filter.py
The class method PhaseFilter.check_phase_file_restrictions() (lines 593-597) handles unknown phases gracefully:
if isinstance(phase, str):
try:
phase = PipelinePhase(phase)
except ValueError:
return FileRestrictionResult.allow(f"Unknown phase '{phase}', allowing by default")But the convenience function (lines 854-856) raises ValueError:
if isinstance(phase, str):
phase = PipelinePhase(phase) # Raises ValueError
return get_phase_filter().check_phase_file_restrictions(phase, files)This inconsistency means the same input produces different behavior depending on which entry point is used. The test test_unknown_phase_raises_value_error even expects this behavior, but it contradicts the class method's fail-safe design.
Fix: The convenience function should delegate validation to the class method, not duplicate it with different behavior:
def check_phase_file_restrictions(
phase: str | PipelinePhase, files: list[str]
) -> FileRestrictionResult:
return get_phase_filter().check_phase_file_restrictions(phase, files)Then update the test to expect the allow-by-default behavior.
3. Potential security consideration: implement phase allows .egg-state/*.json at root
File: .egg/phase-permissions.json:154-161
The implement phase blocks specific subdirectories:
"blocked_patterns": [
".egg-state/contracts/*",
".egg-state/drafts/*",
".egg-state/pipelines/*",
".egg-state/reviews/*"
]However, files directly in .egg-state/ (not in subdirectories) are NOT blocked. For example:
.egg-state/config.json- ALLOWED.egg-state/malicious.json- ALLOWED
This may be intentional (no such files currently exist at that level), but it's worth explicitly documenting or adding .egg-state/*.json to blocked patterns if the intent is to prevent modification of any .egg-state/ files during implement phase (except checkpoints).
Question: Is this the intended behavior? If so, please document it in the phase-permissions.json description.
Minor Issues / Suggestions
4. Variable reuse check uses dir() - works but unconventional
File: gateway/gateway.py:655
if "changed_files" not in dir() or changed_files is None:This works in Python, but the more conventional approach would be to initialize changed_files = None before the conditional blocks and check if changed_files is None. The dir() approach is unusual and may confuse future maintainers.
5. Missing test: path traversal in phase restrictions
The tests don't verify that path traversal attempts (e.g., foo/../.egg-state/contracts/123.json) are correctly normalized and blocked. The path normalization in _normalize_path() appears correct, but adding an explicit test would strengthen confidence.
Suggestion: Add a test case like:
def test_path_traversal_normalized_and_blocked(self):
"""Path traversal attempts are normalized and correctly blocked."""
result = check_phase_file_restrictions(
"implement",
["foo/../.egg-state/contracts/123.json"], # Normalizes to .egg-state/contracts/123.json
)
assert result.allowed is False6. Integration test uses undefined fixture
File: integration_tests/local_pipeline/test_unified_pipeline_behavior.py:418
def test_pipeline_reports_container_state(self, orchestrator_url: str) -> None:This test method takes orchestrator_url as a parameter but the class TestContainerAgentTracking doesn't have the local_pipeline_stack fixture that other tests use. It appears to expect a simple string fixture, but this may not exist in the test environment.
Positive Observations
-
Security model is sound: Phase-based restrictions with fail-closed behavior on file check errors is the right approach.
-
Path normalization: The
_normalize_path()method correctly handles./prefixes and path traversal viaposixpath.normpath(). -
Checkpoint bypass: Explicitly allowing checkpoint branch pushes (
egg/checkpoints/v1) regardless of phase is correctly implemented. -
Test coverage: Good unit test coverage for the new phase file restrictions, including edge cases like mixed blocked/allowed files.
-
Audit logging: All block scenarios are properly logged before returning errors.
Conclusion
The core design is correct, but the ValueError handling inconsistency between the convenience function and class method should be fixed before merge. This is currently a bug that could cause unexpected 500 errors in production if an invalid phase value is ever passed to the gateway.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
- Fix check_phase_file_restrictions convenience function to delegate validation to the class method instead of raising ValueError for unknown phases. Now matches the class method's fail-safe behavior. - Replace unconventional dir() check with standard None comparison in gateway.py push handler. - Add path traversal tests to verify normalization works correctly. - Fix integration test to use local_pipeline_stack fixture. - Fix test mock to explicitly set phase=None to prevent MagicMock auto-creation triggering the phase check code path. Authored-by: egg
|
Thanks for the thorough review! I've addressed the actionable items: Fixed Issues1. & 2. ValueError inconsistency - Fixed. The convenience function 4. Variable reuse check uses 5. Missing path traversal test - Added three test cases for path traversal scenarios to verify normalization works correctly. 6. Integration test undefined fixture - Fixed. Changed Response to Item #3:
|
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-Review: PR #554 - Address Review Feedback
Summary
All issues from my previous review have been properly addressed. The changes are correct and maintain consistency between the convenience function and class method behavior.
Issues Verified as Fixed
1. ✅ ValueError Inconsistency Fixed
Previous Issue: The convenience function check_phase_file_restrictions() raised ValueError for unknown phases while the class method handled them gracefully.
Fix Verified: The convenience function now delegates directly to the class method without any pre-validation:
# Before (line 854-855):
if isinstance(phase, str):
phase = PipelinePhase(phase) # Raised ValueError
# After (line 854-855):
# Delegate to class method which handles unknown phase gracefully
return get_phase_filter().check_phase_file_restrictions(phase, files)The corresponding test was updated from test_unknown_phase_raises_value_error to test_unknown_phase_allows_by_default, correctly verifying the fail-safe behavior.
2. ✅ Variable Reuse Check Fixed
Previous Issue: Used unconventional if "changed_files" not in dir() pattern.
Fix Verified: Now uses standard Python pattern:
changed_files = None # May be populated by role check, reused by phase check
...
if changed_files is None:3. ✅ Path Traversal Tests Added
Three new test cases verify path normalization:
test_path_traversal_normalized_and_blocked- Single traversal leveltest_path_traversal_multiple_levels_blocked- Multiple traversal levelstest_path_traversal_mixed_with_valid_files- Mixed valid and traversal paths
All tests properly verify that posixpath.normpath() normalizes paths before restriction checks.
4. ✅ Integration Test Fixture Fixed
The test test_pipeline_reports_container_state now correctly uses the local_pipeline_stack fixture instead of the undefined orchestrator_url parameter:
def test_pipeline_reports_container_state(self, local_pipeline_stack) -> None:
orchestrator_url = local_pipeline_stack.orchestrator_url5. ✅ Test Mock Fixed
The mock session now explicitly sets phase = None to prevent MagicMock auto-creation from triggering the phase check code path.
Test Verification
All relevant tests pass:
test_unknown_phase_allows_by_default✓test_path_traversal_normalized_and_blocked✓test_path_traversal_multiple_levels_blocked✓test_path_traversal_mixed_with_valid_files✓test_push_allowed_when_role_unavailable✓
Conclusion
The review feedback has been thoroughly addressed. The code is ready for merge.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
All review feedback has been addressed. The re-review confirmed all fixes are correct and the code is ready for merge. Summary of fixes applied:
All tests pass locally (74 phase_filter tests, 22 gateway phase/push tests). — Authored by egg |
This comment has been minimized.
This comment has been minimized.
Update project structure documentation to include the new integration test file for unified local/issue pipeline behavior (added in #554). Authored-by: egg
Update project structure documentation to include the new integration test file for unified local/issue pipeline behavior (added in #554). Authored-by: egg Co-authored-by: jwbron <8340608+jwbron@users.noreply.github.com>
Resolved merge conflicts: - docs/architecture/README.md: Combined PR's orchestrator.md link with main's enhanced description - gateway/tests/test_phase_api.py: Updated test to use 'gh pr create' instead of 'git push' since PR #554 changed REFINE phase permissions to allow push operations Authored-by: egg
There was a problem hiding this comment.
Contract Verification Report
Performed comprehensive contract verification for issue #543 (Unify local-mode and issue-mode pipeline behavior).
Summary
Core Implementation: Verified ✅
The implementation correctly addresses the main objectives:
- Phase-based push restrictions replace blanket local-mode blocking
- State persistence at phase boundaries via
force_commitflag - File path prefixing with pipeline IDs
- Contract CLI instructions for both modes
- Container and agent tracking populated
Detailed Verification
Phase 1: Git Push Restrictions ✅
- ac-1:
phase_file_restrictionssection present in.egg/phase-permissions.jsonwith correct patterns for refine/plan/implement/pr phases - ac-2:
check_phase_file_restrictions()implemented ingateway/phase_filter.py:830-880with proper pattern matching and path normalization - ac-3: Push handler in
gateway/gateway.py:646-704enforces phase restrictions with audit logging - ac-4: Blanket local-mode push block removed (diff shows removal of lines 520-533)
- ac-5: Checkpoint branch bypass implemented:
CHECKPOINT_BRANCH = "egg/checkpoints/v1"withis_checkpoint_pushcheck
Phase 2: State Persistence ✅
- ac-6:
force_commitflag added toStateStore.save_pipeline()at line 182 - ac-7: Phase transitions use
force_commit=is_localinpipelines.py:2019-2030 - ac-8:
delete_pipeline()acceptsforce_commitparameter
Phase 3: File Paths ✅
- ac-9:
_get_draft_path()returns{pipeline_id}-{phase}.mdformat for local mode - ac-10:
_verdict_path_for_type()returns{pipeline_id}-{phase}-{type}.jsonformat - ac-11:
_read_phase_draft()updated to passpipeline_idparameter - ac-12: Phase prompts use
_get_draft_path()to show correct paths
Phase 4: Contract Usage ✅
- ac-13: Contract CLI instructions (
egg-contract show/add-commit) now shown for both modes in_build_phase_prompt() - ac-14:
contract_synced=Falseset instate_store.create_pipeline(), updated to True after successful creation - ac-15: Contract path uses
{pipeline_id}.json(verified in integration tests)
Phase 5: Checkpoints ✅
- ac-16: Checkpoint capture no longer blocked by local-mode restrictions
- ac-17: Checkpoint patterns included in
allowed_patternsfor refine/plan phases - ac-18:
⚠️ No explicit test for checkpoint JSON on branch; covered implicitly by architecture
Phase 6: Container/Agent Tracking ✅
- ac-19:
_spawn_and_wait()now createsAgentExecutionrecords - ac-20:
phase_execution.agents.append(agent_execution)populates agent tracking - ac-21: Agent status updated with
completed_at,status,errorfields - ac-22: API exposes containers/agents via existing model serialization
Test File Location Issue ⚠️
The contract specifies Phase 7 tests in specific files:
orchestrator/tests/test_pipeline_push_restrictions.pyorchestrator/tests/test_local_pipeline_contracts.pyorchestrator/tests/test_concurrent_pipelines.pyorchestrator/tests/test_container_tracking.pyorchestrator/tests/test_state_persistence.py
However, these tests are consolidated in integration_tests/local_pipeline/test_unified_pipeline_behavior.py.
While the test scenarios ARE covered (push restrictions, concurrent pipelines, container tracking, etc.), the file locations don't match the contract specification. The tests are functionally equivalent but organized differently.
Recommendation: This is a minor deviation. The consolidated test file provides better maintainability. Consider updating the contract to reflect the actual file structure, or accept this as an acceptable implementation choice.
Additional Unit Tests ✅
Comprehensive unit tests added in gateway/tests/test_phase_filter.py:
TestPhaseFileRestrictionsclass with 20+ test cases- Path traversal attack prevention tests
- Pattern matching edge cases
- Unknown phase handling
Verdict
All core acceptance criteria verified. The implementation correctly unifies local-mode and issue-mode pipeline behavior with proper security controls, state persistence, and tracking.
The test file location deviation is a minor issue that doesn't affect functionality. The implementation meets the spirit and intent of all acceptance criteria.
— Authored by egg
|
egg contract-verification completed. View run logs 6 previous review(s) hidden. |
- 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 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>
…st the state store (#3070) (#3084) * fix(orchestrator): commit pipeline state on every save and host-persist the state store (#3070) A redeploy on 2026-06-10 silently erased three in-flight Khan/webapp pipelines, including one parked at an approved refine HITL gate (get_status 404, absent from list_tasks). Root cause, orchestrator side: save_pipeline only committed prompt-driven pipelines (no issue_number) when force_commit=True, which fires solely on the auto-advance and completion paths — so a free-text pipeline parked at its first gate had never been committed to egg/pipeline-state at all. Its record existed only as an uncommitted file in the state worktree on an emptyDir volume; pod recreation rebuilt the worktree from the last committed branch tip and the pipeline vanished. The gate is a vestige of the removed local pipeline mode (#554 -> #1073). - save_pipeline/delete_pipeline: commit whenever commit=True, regardless of pipeline origin; drop the dead force_commit params and the two now-redundant call sites. - k8s local overlay: host-persist the orchestrator's egg-state volume (pipeline-worktree*) alongside repos, mirroring the gateway's #3005 session-store fix. The base no longer declares egg-state (it falls inside the home emptyDir, matching the gateway base) so the overlay add merges cleanly. - deployment validation: new pipeline-state-store-not-persistent rule (error) fires when an overlay persists repos but leaves egg-state ephemeral, mirroring session-store-not-persistent. The gateway-side half of #3070 (startup cleanup destroying parked pipelines' worktrees/branches) is a separate PR. * Address PR #3084 review: rule-7 test parity + HOST_UID coupling note - Add three rule-6-parity tests for the new pipeline-state-store-not-persistent rule (PVC/PVC clean, PVC-repos/emptyDir-state firing, orchestrator-canary dash-variant firing). Mirrors the equivalent rule-6 trio so a future shared-helper refactor of both rules has full per-rule coverage. - Note the HOST_UID coupling in the orchestrator-volumes.yaml comment: the shared /home/egg/.egg-state hostPath works because the gateway entrypoint chowns the parent to HOST_UID:HOST_GID; HOST_UID != 1000 would EACCES the orchestrator (runAsUser: 1000), and cloud overlays on PVCs sidestep this entirely. Follow-up filed for the post-fix commit-volume measurement: #3090. Refs: #3084, #3070. --------- Co-authored-by: egg-reviewer[bot] <261018737+egg-reviewer[bot]@users.noreply.github.com>
Summary
Unifies local-mode and issue-mode pipelines so both follow the same contract/checkpoint/push discipline. The only difference between the two modes is now where initial context comes from (GitHub issue vs user prompt).
Key changes:
.egg-state/files, implement phase can push code but not contracts/drafts, PR phase can push everything.{pipeline_id}-prefixed paths (e.g.,.egg-state/drafts/{pipeline_id}-analysis.md) to support concurrent pipelines.egg-contractCLI instructions.PhaseExecution.containersandPhaseExecution.agentsare now populated during execution.Issue: #543
Test plan
make testto verify all unit and integration tests passegg local "Test task") and verify:egg/checkpoints/v1branch{pipeline_id}-prefixed pathsegg-contract show)/api/v1/pipelines/{id}and verifycontainersarray is populatedAuthored-by: egg