Skip to content

Enforce per-task file restrictions in implement phase - #911

Closed
james-in-a-box[bot] wants to merge 28 commits into
mainfrom
egg/egg-issue-805-coder/work
Closed

Enforce per-task file restrictions in implement phase#911
james-in-a-box[bot] wants to merge 28 commits into
mainfrom
egg/egg-issue-805-coder/work

Conversation

@james-in-a-box

Copy link
Copy Markdown
Contributor

Enforce per-task file boundaries from the planner's files_affected lists during the implement phase, preventing cross-contamination when multiple agents work in parallel (Tier 3 dispatch).

Today the implement phase blocks .egg-state/ directories but allows modification of any code file. This means an agent working on task A can accidentally modify files belonging to task B. This PR wires the plan's per-task files_affected lists through session registration so the gateway enforces them at push time.

Key changes:

  • Session model (gateway/session_manager.py): Added allowed_files field to Session with register_session() support and a /sessions/{id}/allowed-files management endpoint
  • Push validation (gateway/gateway.py): Warn-then-block enforcement — first out-of-scope file per session triggers a structured warning, repeated violations block the push with an actionable error
  • Phase filter (gateway/phase_filter.py): New check_session_file_restrictions() with glob and directory-sibling expansion (listing dir/foo.py implicitly allows other files in dir/)
  • Post-agent commit (gateway/post_agent_commit.py): Filters uncommitted changes against the session's allowlist — out-of-scope files are restored, not silently dropped, with clear logging
  • Container spawner (orchestrator/container_spawner.py): compute_allowed_files() collects and expands files_affected from assigned tasks, auto-adding directory-level and common config patterns
  • Escape hatch (sandbox/egg_lib/contract_cli.py): egg-contract request-file --path <file> --reason <why> for agents that legitimately need a file outside scope
  • Plan template (docs/templates/plan.md): Documents that files: is an enforced boundary with guidance on generous glob-based listing

Graceful fallback: Empty files_affected means no per-file restriction (only phase-level). Missing or malformed entries are skipped, not fatal. Config files (pyproject.toml, Makefile, etc.) are implicitly allowed.

Test coverage: ~1,500 lines of new tests across gateway push validation, warn-then-block escalation, directory-sibling expansion, post-agent commit filtering, and container spawner file computation.

Closes #805

Test plan:

  • pytest gateway/tests/test_task_file_restrictions.py — core per-session restriction logic
  • pytest gateway/tests/test_session_file_restriction_endpoints.py — API endpoint integration
  • pytest gateway/tests/test_post_agent_commit_session_filter.py — post-agent commit filtering
  • pytest orchestrator/tests/test_compute_allowed_files.py — spawner file computation and expansion
  • Verify graceful fallback: empty files_affected = no per-file restriction
  • Verify warn-then-block: first violation warns, second blocks

Authored-by: egg

egg added 24 commits February 25, 2026 04:49
Analyzes the architecture for enforcing per-task file restrictions from
the planner in the implement phase. Recommends session-scoped
allowed_files with warn-then-block escalation.

Authored-by: egg
Container 0824c59d8c2a0c2a28f6b4ea6c798c291d77e89eac3fb7a95d42f79ac75716f3 exited with uncommitted changes.
This commit preserves the agent's work-in-progress.

Authored-by: egg
Assess 13 risks across correctness, implementation, security,
compatibility, and operational categories. Overall risk: LOW-MEDIUM.
Key findings: pattern matching inconsistency across validation layers,
directory expansion semantics mismatch, lock file conflicts in parallel
dispatch. No blocking risks identified.

Authored-by: egg
Container 21026274c0a6e0a861c76b4746aaf29dc38083276623b609e86b605e1f933b31 exited with uncommitted changes.
This commit preserves the agent's work-in-progress.

Authored-by: egg
Container 20e89c65d0b3a9afd0b1025ffdeaf90bfafb46859595a7702885248a1249fd79 exited with uncommitted changes.
This commit preserves the agent's work-in-progress.

Authored-by: egg
Extends the gateway Session dataclass with allowed_files (list[str] | None)
for per-task file restrictions in the implement phase. The field is
persisted to disk, deserialized on load, and threaded through:

- SessionManager.register_session() accepts allowed_files parameter
- Gateway /api/v1/sessions/create validates and passes allowed_files
- GatewayClient.register_session() includes allowed_files in request
- ContainerSpawner computes allowed_files from contract files_affected

Also adds Session.add_allowed_file() for dynamic allowlist expansion
and SessionManager.update_session_allowed_files() for persistence.

Issue: #805 (Phase 1: TASK-1-1 through TASK-1-5)

Authored-by: egg
Implements warn-then-block semantics for per-task file boundaries:

- build_session_file_restriction() in phase_filter.py creates a
  PhaseFileRestriction from session allowed_files intersected with
  phase-level blocked patterns (phase blocks always win)
- Fourth push validation layer in gateway.py checks session
  allowed_files after role, agent, and phase checks
- First push with out-of-scope files logs warning and succeeds;
  second push with same file returns 403
- Strict mode (EGG_TASK_FILE_RESTRICTIONS_ENFORCE=true) blocks
  immediately; threshold configurable via EGG_TASK_FILE_WARN_THRESHOLD
- Post-agent auto-commit filters by session allowed_files, restoring
  out-of-scope files with clear log messages
- Checkpoint pushes and non-implement phases exempt

Issue: #805 (Phase 2: TASK-2-1 through TASK-2-3)

Authored-by: egg
Implements the request-file escape hatch allowing agents to request
access to files outside their task's allowlist:

- egg-contract request-file --path <file> --reason <why> CLI command
- POST /api/v1/sessions/request-file gateway endpoint
- Default mode: auto-approves and adds file + parent dir glob to
  session allowed_files with structured audit logging
- Strict mode (EGG_TASK_FILE_RESTRICTIONS_ENFORCE=true): queues HITL
  decision, returns 202 pending approval

Issue: #805 (Phase 3: TASK-3-1, TASK-3-2, TASK-3-3)

Authored-by: egg
The files: field in task definitions is now enforced by the gateway
during the implement phase. Updates the template with:

- Explanation that files: entries are enforced boundaries
- Guidelines for generous file listing with globs
- Directory-sibling expansion documentation
- Escape hatch reference (egg-contract request-file)
- YAML example showing glob patterns and config file inclusion

Issue: #805 (Phase 4: TASK-4-1, TASK-4-2)

Authored-by: egg
Tests cover:
- build_session_file_restriction: allowed patterns, phase blocked
  pattern inheritance, intersection semantics, unknown phase
- check_session_file_restrictions: empty/None inputs, in-scope/
  out-of-scope files, mixed files, glob matching, phase override
- Session model: allowed_files creation, _warned_files tracking,
  add_allowed_file with dedup and directory expansion
- Persistence: allowed_files round-trip, None handling, _warned_files
  not persisted, update_session_allowed_files

All 24 new tests pass. No regressions in existing tests.

Issue: #805 (TASK-1-6, TASK-2-4, TASK-3-4)

Authored-by: egg
Update gateway README with new Per-Task File Restrictions section
covering session-level allowlist enforcement, warn-then-block semantics,
escape hatch (egg-contract request-file), and the new
POST /api/v1/sessions/request-file endpoint. Update SDLC pipeline guide
structural enforcement list, multi-agent orchestration section, and
post-agent auto-commit description. Add request-file command to sandbox
contract CLI rules. Update STRUCTURE.md to reflect phase_filter.py
changes and new test file.

Authored-by: egg
Test gaps addressed:
- Warn-then-block semantics: violation counting, threshold behavior
- Session.add_allowed_file edge cases: root-level files, deep nesting
- build_session_file_restriction: PR/refine phases, double-star globs
- compute_allowed_files_from_contract: contract parsing, directory
  expansion, fallbacks, deduplication, root-level files
- Spawner wiring: allowed_files passed to register_session
- Post-agent commit session filtering: out-of-scope blocking,
  combined phase+session filtering, load helper function
- SessionManager.update_session_allowed_files: invalid token case

63 new tests across 3 files, all 149 related tests pass.

Authored-by: egg
- Remove unused imports in test files (types, json, os, sys, pytest,
  MagicMock, patch, FileRestrictionResult)
- Fix import sorting in test_compute_allowed_files.py
- Apply ruff formatting to 5 files
- Remove stale type: ignore comment in post_agent_commit.py
- Fix make_success call with unsupported status_code kwarg in
  gateway.py escape hatch endpoint (use make_response instead)
All checks passed: lint (ruff, mypy, shellcheck), tests (7035 passed),
security (skipped - bandit not installed).
1. Fix keyword argument mismatch: load_contract() takes repo_root, not
   repo_path. Also convert string path to Path object since the function
   expects pathlib.Path.

2. Handle ValueError from int() parsing of EGG_TASK_FILE_WARN_THRESHOLD
   env var to prevent crashes on misconfigured values.

3. Implement actual HITL decision queueing in strict mode via the
   orchestrator /api/v1/pipelines/{id}/decisions endpoint. Falls back
   to auto-approve with audit logging when orchestrator is unavailable.

4. Add integration tests that exercise real load_contract call path
   (no mocks) to catch argument mismatches and type errors.
Update documentation across architecture, gateway, orchestrator, and
structure docs to reflect the new per-session allowed_files feature:

- architecture/README.md: Add per-task file boundaries to access control list
- gateway/README.md: Add test_post_agent_commit_session_filter.py to test listing
- gateway/post_agent_commit.py: Update auto_commit_worktree docstring to describe
  session-level filtering via allowed_files
- orchestrator/README.md: Note allowed_files computation in container spawning
- docs/development/STRUCTURE.md: Update container_spawner.py and session_manager.py
  descriptions to mention new responsibilities
- docs/index.md: Bump last-updated date
New test file: gateway/tests/test_session_file_restriction_endpoints.py (30 tests)
- session_request_file endpoint: auto-approve, strict HITL, validation, auth
- session_create allowed_files input validation
- Push handler warn-then-block integration via gateway endpoint
- EGG_TASK_FILE_WARN_THRESHOLD ValueError handling verification
- Session file restriction edge cases (union semantics, nested globs)
- Security documentation tests (path traversal, null bytes in paths)

Extended: orchestrator/tests/test_compute_allowed_files.py (+5 tests)
- Contract with None phases/tasks (null safety)
- Task with no files_affected or files attribute
- Very large file lists
- Mixed glob and regular file patterns
All checks passed: lint, test (7076 passed), security (skipped).
@james-in-a-box

This comment has been minimized.

@james-in-a-box

Copy link
Copy Markdown
Contributor Author
Autofix tracking
{}

@james-in-a-box

This comment has been minimized.

@egg-reviewer egg-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No agent-mode design concerns. This PR aligns well with the core principle ("the sandbox is the constraint") by moving file boundary enforcement from prompt-level guidance to gateway-level infrastructure. Agents retain full freedom to explore and read files; the restriction only constrains push/commit operations, which is the correct enforcement point. The warn-then-block escalation and escape hatch (egg-contract request-file) preserve the "guide, don't cage" philosophy.

— Authored by egg

@james-in-a-box

This comment has been minimized.

@egg-reviewer egg-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: Per-task file restriction enforcement

Thorough review of all changed source files, tests, and documentation. The architecture is sound — session-scoped allowlists with warn-then-block is the right approach. However, there are several issues ranging from a correctness bug that defeats the stated purpose to security gaps and missing functionality.


1. CRITICAL — compute_allowed_files_from_contract collects ALL phases, not per-agent

File: orchestrator/container_spawner.py:98-103

The function collects files_affected from every task in every plan phase:

all_files: list[str] = []
for plan_phase in getattr(contract, "phases", []) or []:
    for task in getattr(plan_phase, "tasks", []) or []:
        files = getattr(task, "files", None) or getattr(task, "files_affected", None) or []
        all_files.extend(files)

This means Agent A (assigned to phase 1: src/auth/) and Agent B (assigned to phase 2: src/payments/) will both get the union of all files across all phases. The entire point of this PR — preventing cross-contamination in Tier 3 parallel dispatch — is undermined because every agent gets the same unrestricted allowlist.

The function needs to accept a plan_phase_index or assigned_tasks parameter and filter to only the tasks assigned to this specific agent. The spawn_agent_container caller knows which tasks/phase the agent is assigned to.

2. CRITICAL — Global config allowlist is not implemented

Files: orchestrator/container_spawner.py, gateway/phase_filter.py

The PR description states: "Config files (pyproject.toml, Makefile, etc.) are implicitly allowed." The architect output (AD-5) specifies a hardcoded default list: pyproject.toml, setup.py, setup.cfg, package.json, package-lock.json, yarn.lock, Makefile, requirements.txt, requirements-dev.txt, .gitignore, tsconfig.json, Cargo.toml, go.mod, go.sum.

This is not implemented anywhere. Neither compute_allowed_files_from_contract nor build_session_file_restriction nor check_session_file_restrictions adds implicit config file patterns. If a plan's files_affected doesn't list pyproject.toml, agents will be warned/blocked on dependency updates. This will cause agents to spin, which the architect specifically identified as a risk.

3. SECURITY — No path validation in session_request_file endpoint

File: gateway/gateway.py:3936-3940

The /api/v1/sessions/request-file endpoint accepts any string as path with no sanitization:

path = data.get("path")
if not path or not isinstance(path, str):
    return make_error("Missing or invalid 'path'")
# No traversal validation — path goes straight to add_allowed_file()

Path traversal (../../etc/passwd) and absolute paths (/etc/passwd) are accepted and added to the allowlist. While PhaseFileRestriction._normalize_path() rejects traversal during push validation (so these paths won't actually match git-committed files), the allowlist is polluted and the parent directory glob expansion in add_allowed_file creates concerning entries:

  • ../../etc/passwd → also adds ../../etc/*
  • /etc/passwd → also adds /etc/*

The endpoint should reject paths containing .. or starting with /. This is a defense-in-depth issue — the current mitigation relies on downstream _normalize_path which raises ValueError that must be caught correctly at every call site.

4. CORRECTNESS — fnmatch.fnmatch with * matches across directory separators

File: gateway/phase_filter.py:244

_matches_pattern uses fnmatch.fnmatch for wildcard patterns:

if "*" in pattern:
    return fnmatch.fnmatch(file_path, pattern)

Python's fnmatch.fnmatch('src/deep/nested/file.py', 'src/*') returns True — the * matches / separators. This means the "directory-sibling expansion" that adds src/auth/* actually matches src/auth/sub/deep/file.py, not just immediate children. While this makes the restriction more permissive (not a security hole in the blocking direction), it contradicts the documented behavior:

  • AD-4: "Subdirectory expansion (src/auth/utils/helpers.py) is not included by default"
  • Test at line 3694: test_single_star_does_not_match_nested has a conditional assertion that documents but doesn't actually assert the expected behavior

This is a pre-existing issue in _matches_pattern, but it directly affects the semantics this PR relies on. The distinction between * (one level) and ** (recursive) that the docs describe doesn't actually exist.

5. DESIGN — build_session_file_restriction accesses private internals

File: gateway/phase_filter.py:919-921

pf = get_phase_filter()
pf._load_permissions()
phase_restriction = pf._phase_file_restrictions.get(phase)

This reaches into private _load_permissions() and private _phase_file_restrictions dict. While functionally correct (the method is idempotent with a _loaded guard), this creates coupling to internal implementation details. A public method like get_phase_restriction(phase) -> PhaseFileRestriction | None would be cleaner and wouldn't break if the internal storage changes.

6. MINOR — Comment says "non-implement phases are exempt" but code doesn't check phase

File: gateway/gateway.py:2042-2045

# Checkpoint pushes and non-implement phases are exempt.
if not is_checkpoint_push and hasattr(g, "session") and g.session:
    session_allowed_files = getattr(g.session, "allowed_files", None)
    if session_allowed_files and session_phase:

The comment says non-implement phases are exempt, but the code checks if session_phase (truthy), not if session_phase == "implement". The exemption currently works because the orchestrator only sets allowed_files for implement phase, but the comment implies a guarantee the code doesn't enforce. Either add the explicit check or fix the comment.

7. MINOR — _warned_files in-memory limitation is documented but worth flagging

The warn-then-block counter lives in _warned_files on the in-memory Session object. This works correctly within a single gateway process (since validate_session returns the same object from the cache). However, if the gateway restarts mid-session, the counter resets and the agent gets another free warning. This is documented as intentional (fail-open), which is a reasonable choice. Just noting that the "warn-then-block" protection is weaker than it appears — it's really "warn-then-block-within-a-single-gateway-lifetime."

8. TESTING — test_single_star_does_not_match_nested doesn't assert expected behavior

File: gateway/tests/test_task_file_restrictions.py:3685-3699

def test_single_star_does_not_match_nested(self):
    """Single-star glob should not match nested subdirectories."""
    result = check_session_file_restrictions(
        ["src/*"], "implement", ["src/deep/nested/file.py"],
    )
    # ...
    if not result.allowed:
        assert "src/deep/nested/file.py" in result.blocked_files

This test name and docstring claim * should not match nested paths, but the assertion is conditional — it passes whether or not the file is blocked. Since fnmatch does match nested paths with *, this test always passes via the implicit else (no assertion). This should either assert the actual expected behavior or be renamed to test_single_star_matches_nested_due_to_fnmatch.


Summary

# Severity Issue
1 Critical Allowlist collects all phases, not per-agent — defeats Tier 3 isolation
2 Critical Global config allowlist not implemented despite being documented
3 Security No path validation in request-file endpoint
4 Correctness fnmatch * matches across / — contradicts documented sibling-only expansion
5 Design Accesses private PhaseFilter internals
6 Minor Comment/code mismatch on phase exemption
7 Minor _warned_files in-memory limitation
8 Testing Test doesn't assert its stated behavior

Issues 1 and 2 need to be fixed before merge. Issue 3 should be fixed (defense in depth). The rest are advisory.

— Authored by egg

@james-in-a-box

This comment has been minimized.

@james-in-a-box

This comment has been minimized.

Fix critical, security, and correctness issues from review:

1. CRITICAL: compute_allowed_files_from_contract now accepts plan_phase_id
   to filter files to the agent's assigned phase, preventing cross-phase
   allowlist leakage in Tier 3 parallel dispatch. _run_and_wait passes
   plan_phase_id through to spawn_agent_container.

2. CRITICAL: Add GLOBAL_CONFIG_ALLOWLIST constant (pyproject.toml, Makefile,
   package.json, etc.) and merge it into build_session_file_restriction so
   agents aren't blocked on dependency/config updates.

3. SECURITY: Reject path traversal (..) and absolute paths (/) in the
   session_request_file endpoint before adding to allowlist.

4. CORRECTNESS: Fix _matches_pattern to distinguish single-star (one level)
   from double-star (recursive) glob semantics. Single * no longer matches
   across directory separators via fnmatch.

5. DESIGN: Add public get_phase_file_restriction() method to PhaseFilter;
   build_session_file_restriction now uses it instead of private internals.

6. MINOR: Fix misleading comment about non-implement phase exemption.

7. TESTING: test_single_star_does_not_match_nested now asserts the correct
   behavior. Update test using ** for recursive matching. Fix mock sessions
   missing allowed_files=None.

@james-in-a-box james-in-a-box Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review feedback addressed

All actionable items from the review have been fixed in commit 639b4d0:

1. CRITICAL — Per-agent phase filtering ✅

compute_allowed_files_from_contract now accepts a plan_phase_id parameter and filters tasks to only the assigned phase. _run_and_wait passes plan_phase_id through to spawn_agent_container. In Tier 3 dispatch, each agent now gets only its own phase's files.

2. CRITICAL — Global config allowlist ✅

Added GLOBAL_CONFIG_ALLOWLIST constant with the AD-5 specified files (pyproject.toml, package.json, Makefile, etc.). build_session_file_restriction merges these into every session's allowed patterns automatically.

3. SECURITY — Path validation ✅

session_request_file endpoint now rejects paths containing .. components or starting with / before they reach the allowlist. Returns 400 Bad Request.

4. CORRECTNESS — Single-star glob semantics ✅

_matches_pattern now distinguishes * (single directory level) from ** (recursive). Single-star patterns split path and pattern into segments and match component-by-component. This makes src/* match src/file.py but NOT src/deep/nested/file.py.

5. DESIGN — Public API for phase restrictions ✅

Added get_phase_file_restriction(phase) public method to PhaseFilter. build_session_file_restriction now uses this instead of accessing _load_permissions() and _phase_file_restrictions directly.

6. MINOR — Comment accuracy ✅

Updated comment to accurately state that non-implement phases are "implicitly exempt because the orchestrator only sets allowed_files for implement-phase sessions" rather than claiming the code checks for it.

7. MINOR — _warned_files in-memory limitation

Acknowledged. This is documented as intentional fail-open behavior. No code change needed.

8. TESTING — Proper assertions ✅

test_single_star_does_not_match_nested now asserts not result.allowed instead of using a conditional assertion. Updated test_phasefilerestriction_is_file_allowed_blocked_priority to use ** for recursive patterns. Fixed mock sessions missing allowed_files=None.

All 7076 tests pass. Lint clean.

— Authored by egg

@egg-reviewer egg-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All 8 items from the previous review have been addressed in the delta (commit 639b4d0):

  • #1 Per-agent phase filtering: compute_allowed_files_from_contract now filters by plan_phase_id, threaded through _spawn_and_wait — Tier 3 isolation works correctly.
  • #2 Global config allowlist: GLOBAL_CONFIG_ALLOWLIST added and merged into every session via build_session_file_restriction.
  • #3 Path validation: session_request_file rejects .. and absolute paths.
  • #4 Single-star semantics: _matches_pattern now splits on / for single-star patterns, correctly distinguishing * (one level) from ** (recursive).
  • #5 Public API: get_phase_file_restriction() replaces direct access to private internals.
  • #6#8: Comment fixed, test assertions made unconditional, mock sessions updated.

No new agent-mode design concerns in the delta. The enforcement remains correctly at the gateway/infrastructure layer — agents retain full read access and are only constrained at push/commit time, which aligns with "the sandbox is the constraint."

— Authored by egg

@james-in-a-box

This comment has been minimized.

@egg-reviewer egg-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review: Per-task file restriction enforcement (commit 639b4d0)

Reviewed delta from 11f43ee to 639b4d0. All 8 issues from the prior review have been addressed. The fixes are substantive, not cosmetic. Detailed verification below, followed by one new advisory note.


Prior issues — verification

1. CRITICAL — Per-agent phase filtering: Fixed. compute_allowed_files_from_contract now accepts plan_phase_id and filters to the assigned phase. All Tier 3 _spawn_and_wait call sites (coder, tester, documenter, checker, reviewer) pass plan_phase_id=phase_id. Tier 1 and Tier 2 paths correctly omit it (defaulting to None = all phases), which is the right behavior since they don't have per-phase agent isolation.

2. CRITICAL — Global config allowlist: Fixed. GLOBAL_CONFIG_ALLOWLIST constant added with the AD-5 specified files. build_session_file_restriction merges them into combined_allowed before creating the restriction.

3. SECURITY — Path validation: Fixed. session_request_file now rejects .. components (via path.split("/")) and absolute paths (path.startswith("/")). Returns 400. Defense in depth is properly layered with the downstream _normalize_path check.

4. CORRECTNESS — Single-star glob semantics: Fixed. _matches_pattern now splits single-star patterns into path segments and matches component-by-component. src/* correctly fails to match src/deep/nested/file.py because segment counts differ (2 != 4). Double-star patterns (**) still use fnmatch.fnmatch directly, which correctly matches across / separators for suffix patterns like tests/**.

5. DESIGN — Public API for phase restrictions: Fixed. New get_phase_file_restriction(phase) public method on PhaseFilter. build_session_file_restriction uses it instead of reaching into _load_permissions() and _phase_file_restrictions.

6. MINOR — Comment accuracy: Fixed. Comment now correctly states non-implement phases are "implicitly exempt because the orchestrator only sets allowed_files for implement-phase sessions."

7. MINOR — _warned_files in-memory limitation: Acknowledged as intentional. No change needed.

8. TESTING — Proper assertions: Fixed. test_single_star_does_not_match_nested now unconditionally asserts not result.allowed and checks the blocked file. test_phasefilerestriction_is_file_allowed_blocked_priority updated to use ** for recursive matching. Mock sessions updated with allowed_files=None.


New observations (advisory)

A. _matches_pattern prefix fallback is overly broad for exact file entries

_matches_pattern (phase_filter.py:281) falls through to file_path.startswith(pattern) for non-glob, non-directory patterns. This means a GLOBAL_CONFIG_ALLOWLIST entry like "pyproject.toml" also matches "pyproject.toml.bak", and "Makefile" matches "MakefileFoo".

This is pre-existing behavior (not introduced by this commit), and since it affects allowlist patterns (permissive direction only), it's not a security issue — it just means the allowlist is slightly wider than intended. However, now that GLOBAL_CONFIG_ALLOWLIST depends on this method, it's worth noting for a future cleanup: the fallback should use exact match (==) for non-directory, non-glob patterns, with startswith reserved for patterns ending in /.

This is non-blocking. The current behavior errs on the permissive side, which is the right failure mode for an allowlist.


No blocking issues found. The critical fixes are correctly implemented and the threading of plan_phase_id through the Tier 3 dispatch path is complete. The test updates properly assert the expected behavior.

— Authored by egg

@james-in-a-box

This comment has been minimized.

@james-in-a-box

This comment has been minimized.

Use == instead of startswith for non-directory, non-glob patterns in
PhaseFileRestriction._matches_pattern(). This prevents 'pyproject.toml'
from matching 'pyproject.toml.bak' or 'Makefile' from matching
'MakefileFoo'. Directory patterns (ending with /) still use startswith.

Addresses advisory item A from the re-review.

Authored-by: egg
@james-in-a-box

Copy link
Copy Markdown
Contributor Author

Advisory item A addressed

Fixed in aa0a567. _matches_pattern now uses exact match (==) instead of startswith for non-directory, non-glob patterns. Directory patterns (ending with /) continue to use startswith.

This ensures GLOBAL_CONFIG_ALLOWLIST entries like "pyproject.toml" won't match "pyproject.toml.bak" or "Makefile" won't match "MakefileFoo".

Updated the test test_exact_file_does_not_match_similar_name to assert the corrected behavior (was previously documenting the prefix-match quirk as known behavior).

All 289 tests across the affected test files pass. Lint clean.

— Authored by egg

@james-in-a-box

This comment has been minimized.

@egg-reviewer egg-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed delta from 639b4d0 to aa0a567 (1 commit). The advisory item A from my previous re-review has been correctly addressed — _matches_pattern now uses exact match (==) for non-directory, non-glob patterns while preserving startswith for directory patterns. Test updated to assert the corrected behavior.

No agent-mode design concerns. The enforcement model remains correctly at the gateway/infrastructure layer.

— Authored by egg

@james-in-a-box

This comment has been minimized.

@egg-reviewer egg-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review: Advisory item A fix (commit aa0a567)

Reviewed delta from 639b4d0 to aa0a567. The single change addresses the advisory from the previous review.

Advisory A — _matches_pattern prefix fallback: Fixed.

The startswith fallback for non-glob, non-directory patterns is now exact match (==). This correctly prevents "pyproject.toml" from matching "pyproject.toml.bak" and "Makefile" from matching "MakefileFoo". The test test_exact_file_does_not_match_similar_name is updated to assert the corrected behavior.

Verified that no existing patterns in the codebase depend on the old startswith behavior for non-directory, non-glob entries:

  • All directory patterns use trailing / (handled by the endswith("/") branch)
  • All wildcard patterns contain * (handled by the glob branches)
  • All plain-string patterns are root-level filenames (pyproject.toml, Makefile, etc.) where exact match is the intended behavior

Minor documentation nits (non-blocking)

  1. Stale docstring in _matches_pattern (phase_filter.py:256): Still says "Exact prefix match" — should add "Exact file match" to the supported list since non-glob non-directory patterns now use == instead of startswith.

  2. Stale test docstring at test_exact_file_match (test_task_file_restrictions.py:514): Still says "matches via prefix matching" — should say "matches via exact match".

Neither of these affect behavior.

CI passes (lint, unit tests, security scan all green).

— Authored by egg

@james-in-a-box

This comment has been minimized.

@james-in-a-box

This comment has been minimized.

Update _matches_pattern docstring to list both directory prefix match and
exact file match as separate supported modes, replacing the old "Exact
prefix match" bullet that predates the startswith-to-== change.

Update test_exact_file_match docstring to say "exact match" instead of
"prefix matching".

Authored-by: egg
@james-in-a-box

This comment has been minimized.

@egg-reviewer egg-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review: Docstring fixes (commit afc57ea)

Delta since aa0a567: 2 files, 3 insertions, 2 deletions. Documentation-only changes.

Changes verified

Both non-blocking suggestions from my prior review are correctly addressed:

  1. _matches_pattern docstring (phase_filter.py:253-260): "Exact prefix match" replaced with "Directory prefix match" and "Exact file match" as separate entries. Accurately reflects the two code paths at lines 278-285 (startswith for directory patterns, == for plain strings).

  2. test_exact_file_match docstring (test_task_file_restrictions.py:514): "prefix matching" → "exact match". Matches the test's assertion.

No behavioral changes. No new issues.

— Authored by egg

@egg-reviewer egg-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed delta since aa0a567 (branch rebased to afc57ea). The only change is a docstring fix in _matches_pattern and test_exact_file_match to accurately reflect the exact-match semantics introduced in the prior round. No new code, no new agent-mode design concerns.

The overall design remains well-aligned: file boundary enforcement lives at the gateway/infrastructure layer, agents retain full read access, and constraints are only applied at push/commit time.

— Authored by egg

@james-in-a-box

This comment has been minimized.

@james-in-a-box

This comment has been minimized.

@james-in-a-box

Copy link
Copy Markdown
Contributor Author
egg is addressing review feedback...

@james-in-a-box

Copy link
Copy Markdown
Contributor Author

egg feedback addressed. View run logs

15 previous review(s) hidden.

@jwbron jwbron closed this Feb 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Enforce per-task file restrictions from planner in implement phase

1 participant