Skip to content

Fix #2372: exempt synthetic slice integration pushes from role-path allowlist - #2381

Merged
jwbron merged 2 commits into
mainfrom
egg/issue-2372
May 5, 2026
Merged

Fix #2372: exempt synthetic slice integration pushes from role-path allowlist#2381
jwbron merged 2 commits into
mainfrom
egg/issue-2372

Conversation

@jwbron

@jwbron jwbron commented May 5, 2026

Copy link
Copy Markdown
Owner

Summary

  • Closes Slice integration-branch push blocked by role-based path allowlist (false positive on branch-creation-only pushes) #2372. The role-based path-allowlist check at gateway/gateway.py:1515 was the only file-restriction gate not keyed on is_infrastructure_push. After Fix #2368: synthetic-session exemption for slice integration-branch pushes #2370 exempted synthetic-session slice integration-branch pushes from the pipeline-session push block, the next gate in the chain — the role check — fired on a logical no-op branch-creation push because get_changed_files_in_push falls back to a main-based diff when the target ref doesn't exist yet, surfacing the parent branch's full history (drafts, contracts, brc-history). None of those paths are writable by the coder role, so all 15 slice integration-branch creations on issue-2261-v3 failed with Push denied: Role 'coder' cannot modify: .egg-state/brc-history/....
  • The fix adds not is_infrastructure_push to the role check gate, mirroring the existing gates on the anchor (l.1785), phase (l.1822), and agent-restriction (l.1580) checks. Because is_slice_integration_push=True already promotes is_infrastructure_push=True (l.1316-1318), this only widens the exemption surface for pushes that already cleared the launcher-secret + slice-shape regex.
  • Adds two tests in gateway/tests/test_pipeline_push_block.py::TestSliceIntegrationBranchExemption:
    • test_synthetic_slice_branch_skips_role_path_allowlist — synthetic slice push with would-be-blocked changed_files returns 200, and asserts get_changed_files_in_push / check_file_restrictions are never called (verifies the gate, not just a happy-path mock).
    • test_role_path_allowlist_still_enforced_for_non_infrastructure_pushes — regression guard: a non-pipeline session pushing forbidden files still returns 403 with the path-allowlist error.

Option 1 from the issue (smallest, symmetric with #2370). Options 2 (branch-creation-only detection) and 3 (new orchestrator-infra role) are not pursued here — option 2 touches security-critical attribution code; option 3 adds role configuration for a single call site. Neither is justified for closing this regression.

Test plan

  • .venv/bin/pytest gateway/tests/test_pipeline_push_block.py — 31 passed (29 existing + 2 new).
  • .venv/bin/pytest gateway/tests/ — 3167 passed.
  • make test — 81182 passed; 1 unrelated failure + 2 unrelated errors are pre-existing local-environment artifacts (stale integration_tests/deployment_validation/__pycache__, port-binding conflict).
  • make lint clean for the changed files (gateway/gateway.py, gateway/tests/test_pipeline_push_block.py).
  • Post-deploy operational step (separate from this PR): mcp__egg__advance_phase target_phase=implement force=true for pipeline issue-2261-v3 to retry the slice loop.

Refs

…llowlist

Synthetic-session slice integration-branch creation pushes (#2368) bypass
the pipeline-session push block (#2370) but were still hitting the
role-based path-allowlist check at gateway/gateway.py:1515 because that
gate was the only file-restriction gate not keyed on
``is_infrastructure_push``.  When ``get_changed_files_in_push`` falls
back to a ``main``-based diff (the target ref doesn't exist yet on origin
for a branch-creation push), the changed-files set surfaces every file
modified on the parent branch's history — drafts, contracts, brc-history
— none of which the ``coder`` role can write, so the push is falsely
blocked.

Mirror the existing ``not is_infrastructure_push`` gate already present
on the anchor (l.1785), phase (l.1822), and agent-restriction (l.1580)
checks.  The gate is set by the synthetic-session + slice-shape branch
exemption (l.1316-1318), so this only widens the exemption surface for
pushes that already cleared the launcher-secret check.

@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

Targeted, well-justified fix. The role-based path-allowlist gate was the lone exception to the not is_infrastructure_push pattern that the other three file-restriction gates already use, and the failure mode (slice integration-branch creation pushes 403'ing because get_changed_files_in_push falls back to a main-based diff) is exactly the symptom that pattern was designed to prevent.

I traced the change end-to-end and could not find any way for the new exemption to be reached by an unprivileged session:

  • is_infrastructure_push is set to True only at gateway/gateway.py:1297 (branch in {CHECKPOINT_BRANCH, PIPELINE_STATE_BRANCH}) or gateway/gateway.py:1318 (synthetic session + _SLICE_INTEGRATION_BRANCH_RE match).
  • Session.synthetic is settable only via /api/v1/sessions/create, which is gated on @require_launcher_auth (gateway/gateway.py:7986-7987). The identity check getattr(g.session, "synthetic", False) is True (gateway/gateway.py:1316) defends against truthy-but-not-True mocks/strings.
  • The downstream agent-restriction gate (gateway/gateway.py:1588), anchor gate (gateway/gateway.py:1793), and phase gate (gateway/gateway.py:1830) already gate on not is_infrastructure_push — so this change does not widen the exemption surface beyond what already existed for those three gates.

The new tests are strong:

  • test_synthetic_slice_branch_skips_role_path_allowlist mocks check_file_restrictions to return allowed=False and get_changed_files_in_push to return forbidden files, then asserts both mocks assert_not_called(). That proves the gate is bypassed, not just that the happy path mocks line up. Better than asserting status_code alone.
  • test_role_path_allowlist_still_enforced_for_non_infrastructure_pushes is a real regression guard: non-pipeline session, ordinary egg/-prefixed branch, forbidden file → 403 with Path allowlist violation.

No blocking issues. A few small non-blocking observations:

Non-blocking

  1. PR description line numbers are stale by +8. The body says "anchor (l.1785), phase (l.1822), and agent-restriction (l.1580)". After this PR's 8-line comment block lands, those gates are at gateway/gateway.py:1793, 1830, and 1588. The numbers in the body match the pre-patch layout. Cosmetic — the in-source comment correctly avoids line numbers.

  2. Comment justification slightly conflates two motivations. The new comment block (gateway/gateway.py:1510-1517) attributes the exemption to "the changed-files diff against main would otherwise pull in every file modified on the parent branch's history". That rationale is specifically the slice integration-branch failure mode (get_changed_files_in_push fallback at gateway/git_client.py:1448). It does not describe why the role check is also skipped for legitimate egg/checkpoints/v2 / egg/pipeline-state pushes — those branches exist on origin and have an orphan/disjoint history, which is a different reason to skip role-based file restrictions. Worth acknowledging both motivations in one sentence so a future reader doesn't think the only justification is the new-branch fallback.

  3. No audit event for the per-gate skip. When a slice integration push is exempted, the operator sees push_slice_integration_exempt (gateway/gateway.py:1320) and push_infrastructure_exempt (gateway/gateway.py:1357). Neither names which downstream gates were skipped. The other three gates have always behaved this way, so this PR is consistent — just flagging that the audit trail tells you "exempted" without telling you "exempted from the role / phase / anchor / agent-restriction gates specifically". Not worth a code change here, but if the audit event ever grows a bypassed_gates: [...] field, the role gate should be added at the same time.

@james-in-a-box

This comment has been minimized.

@james-in-a-box

This comment has been minimized.

Per egg-reviewer feedback on PR #2381: the comment block at
gateway.py:1511 conflated two distinct motivations for skipping the
role-based path-allowlist on infrastructure pushes. Split them out:
(1) checkpoint/pipeline-state branches are orphan/disjoint-history
infrastructure writes where role restrictions don't conceptually apply,
and (2) slice integration-branch creation pushes diff against `main`
because the target ref doesn't exist yet, which would surface the
parent branch's full history.
@james-in-a-box

james-in-a-box Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor

Review feedback disposition

Thanks for the thorough end-to-end trace.

  1. Stale PR description line numbersdisagree (cannot edit PR body — author is jwbron, gateway denies cross-author edits). Recording the corrected post-patch numbers here for the audit trail. After the comment-clarification commit landed in this round, the gates are now at:

    • role check (this PR's added exemption): gateway/gateway.py:1529
    • agent-restriction: gateway/gateway.py:1594
    • anchor: gateway/gateway.py:1799
    • phase: gateway/gateway.py:1836
    • is_infrastructure_push promotion: gateway/gateway.py:1316-1318 (unchanged).
  2. Comment conflates two motivationsfixed-in-PR (commit 17ce05d). Split the rationale into two enumerated reasons in the comment block at gateway/gateway.py:1511: (1) egg/checkpoints/v2 / egg/pipeline-state are orphan/disjoint-history orchestrator infrastructure where role restrictions don't conceptually apply, and (2) slice integration-branch creation pushes diff against main because the target ref doesn't exist yet, which would surface the parent branch's full history. Targeted-pytest run still green (gateway/tests/test_pipeline_push_block.py — 31 passed); ruff check gateway/gateway.py clean.

  3. No audit event names which downstream gates were skippeddisagree (reviewer marked as "Not worth a code change here"; flagged as conditional future work if push_infrastructure_exempt grows a bypassed_gates: [...] field). Leaving as-is per the reviewer's explicit framing.

— 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

Re-reviewed the delta from ebc896317ce05d. The only change is a comment-block rewrite at gateway/gateway.py:1511-1523; no executable code changed.

Disposition of prior review

  • #2 (comment conflated two motivations)fixed cleanly. The new comment block now enumerates both reasons explicitly: (1) egg/checkpoints/v2 / egg/pipeline-state are orphan/disjoint-history orchestrator infrastructure where role-based file restrictions don't conceptually apply, and (2) synthetic-session slice integration-branch creation pushes diff against main because the target ref doesn't yet exist, which would otherwise surface the parent branch's full file history. Both rationales are accurate against the is_infrastructure_push set sites at gateway/gateway.py:1297 and gateway/gateway.py:1318, and against the get_changed_files_in_push main-fallback at gateway/git_client.py:1448. The trailing sentence about the role gate making the pattern symmetric with the downstream anchor/phase/agent-restriction gates is preserved.
  • #1 (stale PR-description line numbers)disagree (cannot edit PR body across authors). Acceptable. The author logged the corrected post-patch line numbers in the disposition comment for the audit trail, and the in-source comment correctly avoids embedded line numbers so it cannot drift.
  • #3 (audit event does not name skipped gates) — author explicitly opted to leave as-is per my "Not worth a code change here" framing. Fine; if the audit envelope ever grows a bypassed_gates: [...] field, the role gate should be added there.

New-commit scrutiny

  • The added comment block is comment-only — verified by reading lines 1490-1610 of the post-patch file. The role-check gate at gateway/gateway.py:1529 (if session_role and not is_infrastructure_push) is byte-for-byte unchanged.
  • The wording is accurate. "Orphan/disjoint-history branches written by orchestrator infrastructure" matches the checkpoint/pipeline-state set; "the target ref doesn't exist yet, which would otherwise pull in every file modified on the parent branch's history (drafts, contracts, brc-history, ...)" matches the slice integration-branch failure mode that motivated #2372.
  • Only gateway/gateway.py is touched in the new commit, no test changes — appropriate for a comment-only edit. The existing tests added in the prior commit (test_synthetic_slice_branch_skips_role_path_allowlist, test_role_path_allowlist_still_enforced_for_non_infrastructure_pushes) continue to exercise both sides of the gate.

No new issues. LGTM.

— Authored by egg

@james-in-a-box

james-in-a-box Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor

egg review completed. View run logs

3 previous review(s) hidden.

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.

Slice integration-branch push blocked by role-based path allowlist (false positive on branch-creation-only pushes)

1 participant