Fix #1882: gateway auto-filter with commit-authorship registry - #1937
Conversation
Analysis for gateway auto-filter + pulled-commit handling. Covers: - Current state: 403-on-mixed-role-push in gateway.py; author-agnostic get_changed_files_in_push in git_client.py; client-side --scope-filter as opt-in workaround; sandbox entrypoint sets <role>@egg.local git identity; the unmerged #1470 implementation at commit 6f0877f. - Four-axis options table: (A) filtering strategy, (B) authorship signal, (C) rollout, (D) rewritten-commit semantics. - Recommended approach: port 6f0877f forward + author-email attribution in get_changed_files_in_push + same-release cutover + [auto-filtered] suffix on the squashed commit. - Test coverage checklist spanning single-role, all-blocked, mixed, and pulled-commit cases. Drafted at .egg-state/drafts/1882-analysis.md.
Captures 12 risks (3 HIGH, 5 MEDIUM, 4 LOW) for the refine-HITL-resolved design: gateway-side commit-SHA authorship registry + interactive-rebase commit rewrite + client-side local-HEAD realignment + same-PR scope-filter removal. High risks: new durable store (R-01), new post-commit hook endpoint + sandbox wiring (R-02), mixed-history commit-tree rewrite (R-04), client-side realignment protocol (R-05). Flags areas needing human review: durable-store choice, kill-switch coupling, empty-after-filter commit policy, audit retention. Rollback plan has three tiers (env flag / PR revert / registry restore).
Decomposes the refine-phase analysis into 5 phases / 20 tasks that ship as a single PR on egg/issue-1882: 1. Commit authorship registry (state-store table, gateway endpoint, sandbox post-commit hook) 2. Port #1470 auto-filter building blocks (partition helper, filter wrapper, _execute_filtered_push) 3. Wire registry + auto-filter into the push handler 4. Remove --scope-filter and its doc references 5. Test coverage for all 8 scenarios + registry + hook Honors HITL resolutions: B3 registry (decision-1), single-release cutover (decision-6), scope-filter removal (decision-8), fail-closed for unregistered commits (decision-9). Adopts refiner defaults for decisions 2, 3, 4, 5, 7, 17 (squash, nothing_to_push=true, [auto-filtered] suffix, pulled_commits field, checker.py location, fail-closed for unknown-role authorship). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Architecture design for gateway-side auto-filter with commit-authorship registry. Resolves HITL decisions 1-17: per-commit commit-tree rewrite preserving pulled cross-role commits, orchestrator state-store-backed registry, gateway-inline commit observation via /api/v1/git/execute, scope-filter removal in same PR, fail-closed on unregistered commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Incorporates the architect's two key design corrections: 1. Observation point is gateway /api/v1/git/execute, not a sandbox- installed post-commit hook. Sandbox containers have no direct .git access (tmpfs shadow), gateway sets core.hooksPath=/dev/null globally, and --no-verify would bypass any hook. Gateway-inline observation is bypass-proof and catches every commit-creating subcommand (commit, cherry-pick, rebase, revert, amend, merge). This is a strengthening of decision-1(d), documented prominently for reviewer confirmation. 2. Rewrite strategy is per-commit commit-tree/update-ref, not soft-reset + squash. Per-commit rewrite preserves pulled cross- role commits bitwise-unchanged and reparents own-commits individually; squash would rewrite pulled history or bail to 403 on mixed ranges. Adopts HITL decision-4's interactive-rebase- equivalent. Other refinements: partition helper lives in gateway/ agent_restrictions.py per decision-15 (refiner's shared/ default was valid when cli_push remained; scope-filter removal leaves no cross-component caller). Post-rewrite fast-forwards local HEAD and re-stages blocked files per decision-6. Removes EGG_AGENT_FILE_ PATTERNS env injection from orchestrator/concurrent_executor.py since cli_push stops consuming it. Response body gains pushed_ commits + pulled_commits fields. Phases restructured - (1) store + orchestrator routes + gateway observer + registry client; (2) partition helper + attributed-file enumeration + per-commit rewriter as self-contained pieces; (3) push handler integration with three-way dispatch and three audit events; (4) scope-filter + env plumbing removal; (5) tests including per-commit rewrite, 8-scenario end-to-end, observer, registry. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Addresses the two non-blocking details reviewer_plan flagged: 1. Re-record semantics: explicitly pin TASK-1-1 to first-wins. The initial (sha, role) binding is authoritative; re-register with a different role returns collision audit-log and preserves the original. Prevents observer-suppression-then-rewrite attack. Corresponding test added in TASK-5-1. 2. Unknown-role fail-closed: TASK-2-1 acceptance now explicitly cites shared/egg_restrictions/checker.py:42-44 deny-by-default, clarifies that unknown role returns ([], files) — all blocked — and adds a dedicated test case asserting this behavior. The five blocking items from reviewer_plan's NACK (sandbox hook infeasibility, soft-reset squash strategy, helper location, unknown-role semantics, mixed-author edge case) were already addressed in the prior revision that aligned with the architect's per-commit commit-tree walk and gateway-inline observer design. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace the --scope-filter recovery story with the gateway auto-filter story. Agents no longer see 403 for agent-role file violations on push; the gateway rewrites the unpushed range, preserves pulled cross-role commits bitwise, drops own-commits that become empty after filtering, and returns filtered/excluded_files/pushed_commits/pulled_commits in the 200 response. The client-side --scope-filter flag and the EGG_AGENT_FILE_PATTERNS env var are gone. Changes: - docs/architecture/gateway-auto-filter.md (new) — full design: problem, outcome, commit-authorship registry (observation point rationale, first-wins semantics, storage, HTTP surface), push handler dispatch, per-commit rewrite algorithm, fail-closed invariant, what was removed, deploy ordering, monitoring. - docs/guides/agent-development.md — rewrite "Push Recovery and Scope Filtering" as "Push Filtering and Cross-Role Pushes" with the four outcomes table, per-commit rewrite walkthrough, kill switch / audit event reference, and a note that --scope-filter is gone. - docs/reference/orchestrator-cli.md — drop [--scope-filter] from the egg-orch push row; link to the architecture doc. - sandbox/agent-config/rules/push-recovery.md — rewrite the runtime rule: explain the four outcomes, the response-body fields agents should read, how to react in an agent loop, fail-closed semantics, the kill switch, and how to verify scope before committing. - docs/index.md — add the new architecture doc to the Architecture table and the "Gateway changes" task-specific row. - gateway/README.md — add auto-filter note to File-Level Access Restrictions, and add commit_observer.py / commit_registry_client.py to the file layout. - orchestrator/README.md — add commit_authorship_store.py and routes/commit_authorship.py; annotate state_store.py with the new sub-store. Aligned with the plan-phase TASK-4-3 documenter assignment; implements the "scope-filter references scrubbed from docs and agent-config rules" deliverable plus the architecture write-up the design warrants.
Adds the B3 decision's durable registry that maps commit SHAs to the agent role that authored them. The orchestrator's state store hosts per-pipeline shards on the egg/pipeline-state orphan branch; two new HTTP routes expose first-wins register and bulk lookup under /api/v1/commit-authorship; the gateway observes every commit-creating operation in /api/v1/git/execute and registers the resulting SHAs via a dedicated HTTP client. - orchestrator/commit_authorship_store.py: sharded JSON store with first-wins semantics, idempotent re-register, bulk lookup. - orchestrator/routes/commit_authorship.py: register, register-bulk, and lookup endpoints, all behind require_lifecycle_secret. - orchestrator/api.py: register the new blueprint. - gateway/commit_registry_client.py: thin HTTP client (best-effort). - gateway/commit_observer.py: rev-list between captured HEADs, post each new SHA to the registry, swallow all failures. - gateway/gateway.py: instrument git_execute to snapshot HEAD before and invoke the observer after successful commit-producing ops. Phases 2-5 (auto-filter, push wiring, scope-filter removal, tests) follow in subsequent commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds the standalone helpers the push handler will wire up in phase 3: - gateway/agent_restrictions.py::partition_files_by_role — splits a file list into (allowed, blocked) for a given role; unknown role is fail-closed (every file blocked, WARNING logged). - gateway/git_client.py::AttributedFile + get_attributed_changed_files_in_push — per-commit diff-tree plus a bulk registry lookup to tag each file with the role that authored its commit. Fail-closed on diff-tree error or registry unreachable. - gateway/filtered_push.py::execute_filtered_push — the architect's per-commit rewrite algorithm using git commit-tree / update-ref. Pulled cross-role commits pass through bitwise-unchanged; own-role commits with blocked paths get new trees and an [auto-filtered] suffix; own-commits that become empty are dropped. Every error path restores HEAD and the worktree so the agent's local state is untouched after a failure. No push behaviour change yet — phase 3 wires these into git_push. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces the 403 branch in git_push with the auto-filter decision tree: consult the commit-authorship registry to split files into own-authored vs pulled, check restrictions against only the own- authored set, and dispatch based on the result. - All own-allowed → plain push (200 with filtered=false, pulled_commits populated when any commit in the range is attributed to another role). - All own-blocked → 200 nothing_to_push=true, excluded_files populated, worktree untouched. Audit event push_all_blocked_no_op. - Mixed own-allowed + own-blocked → execute_filtered_push rewrites the range via commit-tree/update-ref, preserving pulled commits bitwise. Success surfaces filtered=true, excluded_files, pushed_files, pushed_commits, pulled_commits, rewritten_commits on the 200 response. Audit event push_auto_filtered. - Unregistered commits (fail-closed path) emit push_authorship_unregistered_fallback and are treated as own. - EGG_AGENT_RESTRICTIONS_ENFORCE=false short-circuits to warn-only passthrough (existing kill switch preserved). - The plain-push 200 response now includes filtered=false and pulled_commits for observability parity across paths. Phase / anchor / protected-file / branch-ownership / private-mode checks keep their 403 behaviour unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The gateway's auto-filter handles every case the client-side workaround was built for, so the code paths behind ``egg-orch push --scope-filter`` and the ``EGG_AGENT_FILE_PATTERNS`` env var plumbing are removed: - sandbox/egg_lib/cli_push.py: collapse to a thin passthrough around ``git push`` with the existing EGG_BRANCH refspec retargeting. _filter_files, _matches_pattern, _matches_any_pattern, _get_merge_base, _resolve_push_args, and the --scope-filter argparse flag are gone. - orchestrator/concurrent_executor.py: stop emitting EGG_AGENT_FILE_PATTERNS; no consumer remains. Test updates (flipping test_concurrent_executor.py, test_cli_push.py, test_push_error_enrichment.py, and test_agent_restrictions_enforce.py for the new 200-with-filtered=true response shape) are TASK-5-10 for the tester role and will land in a follow-up commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- gateway/gateway.py: replace the try/except-ImportError module lookups with a _load_sibling_gateway_module helper that works in both the production package layout and the test conftest's flat- module layout (falls back to importlib.util file-path loading when the conftest hasn't preloaded a sibling module). The observer now only fires on the narrow set of commit-producing ops (commit, merge, cherry-pick, revert, rebase, am) — and the post-op registration is gated on a successfully-captured before_head so non-mutating ops (status, restore, checkout, etc.) don't perturb the subprocess.run assertions in existing tests. - gateway/gateway.py: when attribution can't be computed (empty commit range, e.g. because the test mocked only the legacy file-detection path), treat the whole changed_files list as own-authored + unregistered and still run partition_files_by_role. The auto-filter decision tree then falls through as before. - gateway/git_client.py: _enumerate_push_commits parses only valid 7-64-char lowercase-hex SHA lines from git rev-list output so a mocked wrapper that echoes back URLs or arbitrary strings can't smuggle them into the commit list. - gateway/git_client.py + gateway/commit_observer.py: resolve commit_registry_client via the same sys.modules-first, file-path-fallback dance so the registry client module is reachable under both layouts without a conftest edit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Coder placed the per-commit rewriter in its own module gateway/filtered_push.py (function execute_filtered_push) rather than as a private helper inside gateway/gateway.py. Update the architecture doc and gateway/README.md file listing to reference the actual module.
…registry Covers TASK-5-1 through TASK-5-10 from the plan. All new tests pass under pytest; ruff clean. New test files (138 new tests total): - orchestrator/tests/test_commit_authorship_store.py (39 tests) — round-trip, idempotent re-register, first-wins collision, bulk lookup, concurrent writes, per-pipeline sharding, state-branch commit on write, input validation, corrupt-shard handling. - orchestrator/tests/test_commit_authorship_routes.py (28 tests) — 401 auth gate, /register + /register-bulk + /lookup happy paths, collision 409, malformed-input 400, store-unavailable 500. - gateway/tests/test_commit_observer.py (18 tests) — HEAD snapshot, multi-commit detection via register_bulk, best-effort behavior on registry failure, non-agent session skip, observe_after_git_execute wrapper. - gateway/tests/test_commit_registry_client.py (21 tests) — 200 / 409 / 500 / network-error / 401 register paths, bulk register, lookup fail-closed, auth header emission. - gateway/tests/test_partition_files_by_role.py (18 tests) — all-allowed / all-blocked / mixed / empty / unknown-role deny-by-default / three-way precedence / structural invariants. - gateway/tests/test_git_client_attribution.py (16 tests) — attribution happy path, registry fail-closed, diff-tree errors, new-branch merge-base fallback, session_role advisory, fetch failure tolerated. - gateway/tests/test_execute_filtered_push.py (12 tests, skip if git init unavailable) — all 8 architect-defined scenarios. - gateway/tests/test_push_author_attribution.py (10 tests) — 8 refine- phase scenarios + response-schema invariants. - sandbox/tests/test_cli_push_scope_filter_removed.py (9 tests) — argparse rejection, source-level removal, passthrough semantics. - integration_tests/test_gateway_auto_filter_end_to_end.py (4 tests, marked integration) — registry round-trip, collision, push shape. Updated test files (TASK-5-10): - gateway/tests/test_agent_restrictions_enforce.py — rewritten to expect 200 with filtered=true / nothing_to_push=true instead of the old 403 enrichment path. - gateway/tests/test_push_error_enrichment.py — replaced #1527's 403-enrichment assertions with the new 200-response-shape contract. - orchestrator/tests/test_concurrent_executor.py::TestFilePatternEnvVar — inverted assertions: EGG_AGENT_FILE_PATTERNS must NOT be in the agent env after #1882. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1. Security hole in mixed-rewrite fallback (gateway/gateway.py) When get_attributed_changed_files_in_push returned empty/error but partition found an allowed set plus a blocked set, the handler invoked execute_filtered_push with an empty commit list — the rewriter walked nothing, pushed HEAD as-is, and blocked files went to origin unfiltered. Fix: treat attribution_fallback as unconditionally nothing_to_push=true whenever any file is blocked, so execute_filtered_push is never called without a commit range. Audit event carries attribution_fallback=true. 2. EGG_LIFECYCLE_SECRET on the gateway pod (k8s/base/gateway-deployment.yaml) The commit-authorship routes live behind require_lifecycle_secret; without the bearer token the gateway would 401 on every observer register and lookup, degrading the whole feature to fail-closed own-authored. Mirror orchestrator-deployment.yaml and pull the secret from gateway-secrets.lifecycle-secret. 3. Binary-safe restage (gateway/filtered_push.py) _restage_blocked_files ran git show with text=True, so non-UTF-8 blobs (PNG, PDF, compiled artefacts) were silently corrupted on the re-stage after a filtered push. Add a _git_raw helper that runs without text=True and write the content as bytes. 4. Actually stage content (gateway/filtered_push.py) git add --intent-to-add only records the path; the index still had no content so the next role's git commit would be empty. Switch to plain git add after the worktree write so git diff --cached shows the restaged file. 5. Warn-only observability parity (gateway/gateway.py) When EGG_AGENT_RESTRICTIONS_ENFORCE=false and blocked_own is non-empty, the warn-only branch fell through to plain push without setting pulled_commits on the response. Set auto_filter_response with filtered=false + pulled_commits in the warn-only path too. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The NACK-fix commit (ef291f3) introduced three user-visible behavior changes that the docs did not yet cover: 1. attribution-fallback short-circuit: when the handler cannot compute a commit walk, it unconditionally returns 200 nothing_to_push=true for any blocked files rather than invoking the rewriter on an empty commit list. Added a dedicated section under "Push handler dispatch" explaining the trigger, outcome, audit event (attribution_fallback flag on push_all_blocked_no_op), and response-message text. 2. Warn-only observability parity: EGG_AGENT_RESTRICTIONS_ENFORCE=false now returns filtered=false, excluded_files=[], pushed_files, and pulled_commits in the success body so downstream tooling does not have to branch on enforcement mode. Updated the kill-switch bullets in the architecture doc, agent-development guide, and the runtime push-recovery rule that the docs previously claimed returned "no new response fields". 3. Binary-safe re-staging: documented the _git_raw / bytes-mode read that preserves non-UTF-8 blobs (PNG, PDF, compiled artefacts) on re-stage, and the git add vs --intent-to-add choice that makes the re-staged blob visible to the next role's git commit. Also added a Deployment prerequisites section calling out the EGG_LIFECYCLE_SECRET requirement on the gateway pod (the reviewer's k8s-manifest finding), and updated the monitoring section to describe what the new attribution_fallback=true audit field means for triage. Files updated: - docs/architecture/gateway-auto-filter.md — new Attribution-fallback short-circuit subsection, expanded Fail-closed invariant, new Binary-safe re-staging + Deployment prerequisites sections, monitoring bullet enriched. - docs/guides/agent-development.md — kill-switch bullet now says the schema stays consistent in warn-only mode; audit-event list names the attribution_fallback field. - sandbox/agent-config/rules/push-recovery.md — kill-switch paragraph and the unregistered-commits paragraph both updated so agents know what to do if they see the attribution-fallback short-circuit.
Adds gateway/tests/test_push_nack_fix_regressions.py covering the five reviewer_code NACK items that commit ef291f3 fixed: 1. Attribution-fallback security hole — when get_attributed_changed_files_in_push returns empty/errored and the pusher has blocked files, the handler MUST return nothing_to_push=true and MUST NOT invoke execute_filtered_push with an empty commit list (which would push HEAD verbatim, leaking blocked files). 2. Binary-safe restage — _git_raw must be used for git show so non-UTF-8 blobs (PNG, PDF, compiled artifacts) are preserved verbatim; _git (text=True) would corrupt them. 3. git add (no --intent-to-add) — verifies _restage_blocked_files actually stages blob content so the next role's git commit picks up the restored file. 4. Warn-only observability parity — EGG_AGENT_RESTRICTIONS_ENFORCE=false with blocked own files must still surface filtered=false + pulled_commits in the response body. 5. Audit-log attribution_fallback flag on push_all_blocked_no_op so operators can distinguish fail-closed-due-to-missing-attribution from real all-blocked pushes. Also applies ruff format to the 10 test files introduced/updated by the coder's test commit (eb6a5ca) so the lint gate is clean: - gateway/tests/test_agent_restrictions_enforce.py - gateway/tests/test_commit_observer.py - gateway/tests/test_commit_registry_client.py - gateway/tests/test_execute_filtered_push.py - gateway/tests/test_git_client_attribution.py - gateway/tests/test_push_author_attribution.py - gateway/tests/test_push_error_enrichment.py - orchestrator/tests/test_commit_authorship_routes.py - orchestrator/tests/test_commit_authorship_store.py - sandbox/tests/test_cli_push_scope_filter_removed.py All 235 tests related to #1882 pass; the new regression suite adds 10 tests (gateway/tests/test_push_nack_fix_regressions.py). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The reference doc still described "Gateway Push Validation" as blocking pushes on any agent-role violation with EGG_AGENT_RESTRICTIONS_ENFORCE toggling between enforce/warn-only. That contract changed in #1882 and the doc hadn't caught up yet. Updates: - Rename "Gateway push validation" → "Gateway push auto-filter" in the enforcement-layers table; replace the "blocks pushes" description with the per-commit rewrite + pulled-commit-passthrough behavior. - Rewrite the Gateway Push Auto-Filter section to describe the per-commit rewrite (own commits rewritten with [auto-filtered] suffix, commits that become empty dropped, pulled cross-role commits bitwise-unchanged), response fields (filtered, excluded_files, pushed_files, pushed_commits, pulled_commits), and the nothing_to_push short-circuit. - Document the EGG_AGENT_RESTRICTIONS_ENFORCE kill switch as a short- circuit-to-plain-push rather than warn-only, and call out that the response schema is stable across both modes. - Clarify that phase/anchor/protected-file/branch-ownership/private-mode/ concurrent-mode checks keep their 403 — only agent-role restrictions auto-filter. - Replace the get_changed_files_in_push() description with the new get_attributed_changed_files_in_push() walk + registry lookup. - Add a note under "Per-Agent Git Identity" explaining that commit.author is display-only now; authoritative attribution at push time comes from the commit-authorship registry (populated by the gateway's git-execute observer using the session token, not the sandbox-set user.email). Links back to docs/architecture/gateway-auto-filter.md for the full design. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…1882) The architecture README already described the gateway's role in policy enforcement but never referenced the new per-commit rewrite design that landed in #1882: - Access-control bullet still claimed the enforce-mode flag toggles between "enforced" and "warn-only", which no longer matches the auto-filter dispatch. Rewritten to describe the auto-filter + bitwise-unchanged cross-role pass-through, with the kill-switch correctly labelled as a short-circuit to plain push. - Added a "Gateway Auto-Filter" bullet to the "Key Architectural Decisions" list so readers browsing that section can find the new design doc alongside Git Isolation and Credential Injection. The dedicated architecture write-up already exists at docs/architecture/gateway-auto-filter.md (landed in 38b74a8, sharpened in 7a91144 / 39565bc / d473d55). This commit just threads the navigation through the architecture README. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The file imports _filter_files, _matches_pattern, _matches_any_pattern, and _get_merge_base — all of which were removed along with the --scope-filter workflow in commit a087932 ("remove --scope-filter and EGG_AGENT_FILE_PATTERNS (phase 4)"). The replacement behaviour is now covered by sandbox/tests/test_cli_push_scope_filter_removed.py, so this file is not just broken but redundant — it blocks pytest collection for the whole suite with an ImportError at module-load time. Delete it rather than paper over the ImportError. Scope-filter functionality is not coming back; the gateway auto-filter (#1882) is the only remaining path. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… suffix Addresses the two blocking items in reviewer_code's NACK on the #1882 auto-filter implementation: 1. Multi-parent (merge) commits silently lost their 2nd+ parents. `_commit_tree` only emitted one `-p` flag and the call sites read only `orig_parent_list[0]`. A pulled merge-commit following a rewritten own-commit would get collapsed into a single-parent commit, silently dropping the merge's second parent. Fix: extend `_commit_tree` to accept a list of parent SHAs and emit one `-p` per entry. Add `_translate_parents()` which maps the original parent list through `parent_lookup` (preserving merge parents verbatim unless they were themselves rewritten earlier in the walk) and shifts the first parent onto the running chain when needed. All three call sites — pulled pass-through, own no-filter pass-through, own-filter rewrite — now use the new helper. A commit with *all* parents unchanged from their original values AND the first parent matching the running tip is still reused verbatim (no new SHA). 2. Auto-filter suffix corrupted commit trailers. The old code `meta["message"].rstrip() + " [auto-filtered]"` stripped the trailing newline and glued the marker onto the last line — so `Signed-off-by: alice <a@x>` became `Signed-off-by: alice <a@x> [auto-filtered]`, breaking `git interpret-trailers`, GitHub's Co-Authored-By rendering, and DCO parsing. Fix: new helper `_compose_filtered_message()` emits the marker as its own paragraph (one blank line before, one newline after) so the trailer block survives intact. The helper handles empty messages, empty suffixes, and extraneous trailing whitespace. Non-blocking items addressed in the same commit (low-risk): - `gateway/git_client.py` — `get_attributed_changed_files_in_push` now emits a WARNING `commit_authorship_partial_lookup` event when the registry returned a proper subset of requested SHAs. Missing SHAs still fall through to fail-closed (None → own-authored); this just gives operators a signal to notice a flaky registry that would otherwise silently subject every cross-role push to restriction checks. - `gateway/filtered_push.py` — elevate post-push rewritten-commit registration failures from DEBUG to WARNING and include the branch name. A subsequent cross-role push would correctly fail-closed, but the audit trail is now useful. - `gateway/gateway.py` — add `enforce=False` to the warn-only audit log payload so operators scanning logs during a kill-switch window can distinguish the warn-only passthrough from enforced paths. - `orchestrator/commit_authorship_store.py` — replace `str.startswith(base)` path-traversal check with `path.resolve().relative_to(base.resolve())` (path-aware, won't prefix-match a sister directory). Also give `_validate_role` an explicit empty-string error message rather than a confusing regex-mismatch report for direct Python callers. Regression tests for the two blocking issues are sent to the tester via a directed HANDOFF message — coder cannot write to gateway/tests/ under the role file-boundary policy. The tests live at gateway/tests/test_filtered_push_helpers.py in the coder worktree for the tester to pick up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Coder role cannot write to gateway/tests/. Stage the tests here in agent-outputs/ so the tester picks them up and commits them from the tester worktree. - test_filtered_push_helpers.py: 18 pure-Python tests for the two NACK-fix helpers — _compose_filtered_message (trailer preservation) and _translate_parents (merge-parent preservation). - tester-patch.diff: updates to gateway/tests/test_execute_filtered_push.py (assert the "\n\n[auto-filtered]" trailer-safe form) and gateway/tests/test_push_nack_fix_regressions.py (add sys.path insert so it can be collected in isolation). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Coder sent a HANDOFF at 20:48 UTC with 18 new helper tests plus a
2-file patch for existing tester-owned files. The coder's own
file-boundary policy prevented writing to gateway/tests/, so the
tester picks up the artifacts from .egg-state/agent-outputs/1882-coder-tests/
and commits them here.
New file gateway/tests/test_filtered_push_helpers.py (231 lines):
- TestComposeFilteredMessage (8): Signed-off-by / Co-Authored-By /
multi-paragraph / trailing-whitespace preservation for the
trailer-safe \n\n[auto-filtered] suffix.
- TestTranslateParents (8): single-parent chain shift, 2-parent
merge preservation, first-parent rewritten via chain shift,
2nd-parent rewritten via lookup, 3-parent octopus, root commit,
empty-new-parent fallback, identity-lookup no-op.
- TestCommitTreeAcceptsMultipleParents (2): signature back-compat
assertion covering the new list-of-parents interface.
Patch applied to:
- gateway/tests/test_execute_filtered_push.py: tighten the
auto-filter trailer assertion to require '\n\n[auto-filtered]'
(double-newline separator) rather than a loose endswith().
- gateway/tests/test_push_nack_fix_regressions.py: insert
gateway/ onto sys.path so the file can be collected in
isolation without relying on collection order.
Also ran ruff check --fix + ruff format on the newly-landed test
file to satisfy project lint before commit.
All 28 affected tests pass: 18 new helper tests plus the 10 existing
tests in the two patched files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Addresses the six lint/format/mypy blockers tester identified in the HANDOFF at 20:46 UTC, so `make lint` passes end-to-end on the 1882 surface. Ruff (6 errors, all auto-fixed + re-exports explicitly preserved): - gateway/commit_observer.py: typing.Iterable → collections.abc.Iterable (UP035). - gateway/filtered_push.py: drop unused `import dataclasses` (F401). - gateway/gateway.py: drop unused `get_agent_pattern` / `check_agent_restrictions` from the primary relative-import block (F401), then re-export both under `# noqa: F401` from both try and except import branches — they're used via test patching (`gateway.check_agent_restrictions` et al. in test_concurrent_push_block.py, test_phase_filter_restrictions.py, test_agent_restrictions_enforce.py), not directly. - gateway/git_client.py: fix I001 import-order in the inline commit_registry_client loader. - orchestrator/concurrent_executor.py: drop unused `import json`. Ruff format (7 files) — auto-applied via `ruff format`. Mypy (14 relevant errors on the 1882 surface): - gateway/filtered_push.py:394,450,491,569 — false-positive "Incompatible types in assignment (str | None, variable has type str)" that masked a genuine potential-None. Fix: in each of the three walk branches (pulled, own-no-filter, own-filter), use a separate local name for the `_commit_tree` return (`built_sha`/`rewritten_sha`) and assign to the outer `new_sha`/`new_sha_passthrough`/`rewritten_sha` only after the `if err or x is None: rollback+return` narrowing. Also renamed the registry-register loop variable to `reg_sha` so it can't shadow the walk-scope `new_sha`. - gateway/commit_observer.py:173 / git_client.py:1719 — duplicate `get_client` definition inside the conditional import block. Rewrite the fallback to import the module and reach into `_crc.get_client` rather than `from commit_registry_client import get_client` inside an except branch (which mypy saw as a redefinition). - gateway/filtered_push.py / commit_observer.py / commit_registry_client.py — match the egg_logging `get_logger` signature in the ImportError fallback so mypy doesn't complain about divergent conditional function variants; add explicit return-type annotations. - gateway/filtered_push.py::execute_filtered_push — type annotate `push_fn` and `registry_register` parameters (via Callable) so the `# type: ignore[no-untyped-def]` comments can drop. - gateway/gateway.py::_load_sibling_gateway_module and ::_lookup_commit_observer_fn — add `-> Any` return type. - gateway/gateway.py::git_push — thread partition_files_by_role and get_attributed_changed_files_in_push through renamed `_partition_fn` / `_get_attributed_fn` locals (typed `Any`) so the getattr-style resolution no longer conflicts with the subsequent function call (fixes "None not callable" and "name already defined" errors). - gateway/gateway.py — annotate `own_files`, `pulled_files`, `unregistered_files` in the attribution-fallback branch. - gateway/git_client.py:1743 — union-attr: registry_client is typed `object`; assign to an `Any` local before calling `lookup_bulk` so mypy accepts the duck-typed method. Adds `from typing import Any`. A matching tester-owned test-file update (sandbox/tests/test_cli_push_scope_filter_removed.py — `monkeypatch.setattr` switched to a string-based path so mypy doesn't flag the implicit `cli_push.subprocess` attribute re-export) is staged at .egg-state/agent-outputs/1882-coder-tests/tester-mypy-patch.diff for the tester to pick up — coder cannot write to sandbox/tests/ under the role file-boundary policy. Verified: make lint passes (ruff check + format + mypy), 2088 gateway tests pass (only pre-existing health-server port conflicts fail — unrelated). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
No agent-mode design concerns in the delta. The single commit (b221d33) addresses re-review feedback — doc corrections ([auto-filtered] → Auto-Filtered: true trailer), a parameter cleanup (suffix: str → add_trailer: bool), a rollback log-level upgrade, and test improvements. The only agent-facing file touched (push-recovery.md) is a factual correction that helps agents understand the current commit-rewriting behavior. No anti-patterns introduced.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-review: All Five Feedback Items Addressed
Delta: e5367bb..b221d33 (1 commit, 8 files, +37/-30 lines).
Verification of Previous Feedback
| # | Issue | Status | Notes |
|---|---|---|---|
| 1 | push-recovery.md still says [auto-filtered] |
Fixed | All 5 docs updated: push-recovery.md, gateway-auto-filter.md, agent-development.md, agent-roles.md, gateway/README.md. No remaining [auto-filtered] references outside .egg-state/ pipeline artifacts. |
| 2 | suffix parameter is vestigial |
Fixed | _compose_filtered_message(orig_message, suffix: str) → _compose_filtered_message(orig_message, add_trailer: bool = True). The dead suffix content path is gone; only truthiness matters and the type now reflects that. Tests updated. |
| 3 | Scenario 9 test missing schema assertions | Fixed | Added assertions for filtered, pushed_files, pushed_commits, and pulled_commits. Matches the schema coverage of other scenarios. |
| 4 | _TRAILER_LINE_RE false-positive on body text |
Documented | test_body_key_value_lines_treated_as_trailers added — documents that Problem:/Reason: lines match the regex and the trailer joins with a single newline. Correct approach for a cosmetic issue. |
| 5 | Rollback update-ref failure logged at WARNING |
Fixed | Upgraded to logger.error. The reset --hard failure below it stays at WARNING, which is appropriate since the ref is already restored at that point. |
New Code Review
No issues found in the delta. Specifically verified:
auto_filter_suffixparameter: Type changed fromstrtobool, default from" [auto-filtered]"toTrue. The parameter name still says "suffix" which is mildly misleading but not confusing enough to block — the type annotation (bool) makes the intent clear. No callers pass this argument explicitly; all rely on the default._compose_filtered_messagecall site (filtered_push.py:528): Now passesadd_trailer=auto_filter_suffixusing the keyword form — correct.- Test coverage: The old
test_suffix_value_is_ignored_always_emits_trailer(which documented a code smell) is replaced bytest_body_key_value_lines_treated_as_trailers(which documents actual edge-case behavior). Net improvement. - Documentation consistency: All 5 docs now consistently reference
Auto-Filtered: truegit trailer. Cross-checked withgrep— no stale[auto-filtered]references in live code or docs.
Minor suggestion (non-blocking)
auto_filter_suffix parameter name: Now that the type is bool, the name auto_filter_suffix on execute_filtered_push is misleading — "suffix" implies a string. Consider renaming to add_auto_filter_trailer in a follow-up for consistency with the inner add_trailer parameter on _compose_filtered_message. Not blocking since the type annotation makes the intent unambiguous and no external callers exist.
— Authored by egg
There was a problem hiding this comment.
Contract Re-review: All Suggestions Addressed
Incremental verification of commit b221d33 ("Address re-review feedback on #1937") against the 5 suggestions from the previous contract-verification review at e5367bb.
Suggestion Verification
| # | Suggestion | Status | Evidence |
|---|---|---|---|
| 1 | push-recovery.md doc mismatch — still said [auto-filtered] suffix |
Fixed | Line 11 now reads Auto-Filtered: true git trailer |
| 2 | _compose_filtered_message vestigial suffix parameter |
Fixed | Renamed to add_trailer: bool = True — value directly controls behavior, no longer vestigial |
| 3 | Scenario 9 test missing schema assertions | Fixed | Lines 499-502 now assert filtered, pushed_files, pushed_commits, pulled_commits |
| 4 | _TRAILER_LINE_RE false-positive on body text |
Not addressed (was non-blocking) | Regex unchanged at line 253. Acceptable — suggestion was explicitly cosmetic |
| 5 | Rollback incomplete when update-ref fails |
Fixed | Main flow at lines 565-572 now bails out entirely with success=False and calls _rollback(). Rollback logs at ERROR level for update-ref failure |
Regression Check
No regressions found. Full test suite results:
- Gateway tests: 2383 passed, 26 skipped
- Orchestrator authorship + concurrent executor: 111 passed
- Sandbox scope-filter removal: 9 passed
Contract State
The contract (issue-1882.json) has no formal acceptance criteria or phases defined — it remains in refine phase with all 19 HITL decisions resolved. The PR implements the resolved decisions correctly. No acceptance criteria to mark as verified since none are defined in the contract.
Verdict
All blocking suggestions from the previous review are fixed. No regressions. One non-blocking suggestion (trailer regex false-positive) remains open — acceptable per the original review which marked it explicitly non-blocking.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Review Feedback Addressed (52dd6c1)
All 39 gateway tests pass (12 skipped — live git repo tests), lint clean. — Authored by egg |
This comment has been minimized.
This comment has been minimized.
|
egg is investigating the Lint check failure...
|
|
egg check fixer completed for Lint. CI will re-run to verify. View run logs — Authored by egg |
There was a problem hiding this comment.
Re-review: Delta from b221d33 to ffc1da2
Two commits since last review:
52dd6c1— Renameauto_filter_suffix→add_auto_filter_traileronexecute_filtered_pushffc1da2— Fix ruff formatting on the_compose_filtered_messagecall
This addresses the one remaining non-blocking suggestion from the previous review round: the auto_filter_suffix parameter name was misleading now that the type is bool. The rename to add_auto_filter_trailer is consistent with the inner add_trailer parameter on _compose_filtered_message.
Verification
- No remaining references to the old
auto_filter_suffixname anywhere in the codebase. - No callers pass
add_auto_filter_trailerexplicitly — all rely on the defaultTrue— so the rename is safe. - The parameter chain
execute_filtered_push(add_auto_filter_trailer: bool)→_compose_filtered_message(add_trailer=add_auto_filter_trailer)is clear and self-documenting. - The ruff formatting fix is correct (line-length split on the function call).
Previous feedback status
All blocking issues from earlier rounds remain fixed. All deferred items (import dedup, _inner_push extraction, test infra) are still reasonable follow-ups.
No issues found.
— Authored by egg
There was a problem hiding this comment.
No agent-mode design concerns in the delta.
The two commits since b221d33 are a parameter rename (auto_filter_suffix → add_auto_filter_trailer) and a ruff formatting fix — both in gateway/filtered_push.py. No changes to prompt construction, agent interaction patterns, or design boundaries.
— Authored by egg
|
egg review completed. View run logs 25 previous review(s) hidden. |
|
egg agent-mode-design completed. View run logs 25 previous review(s) hidden. |
There was a problem hiding this comment.
Contract Re-Verification (incremental review)
Baseline: commit b221d33ca93a4b72de38fa2a6e229198cc6a3803
Current HEAD: ffc1da21cc6eda389983492c6baf6a7ef33227ca
Delta since last review
Two commits, both touching only gateway/filtered_push.py (+4/-2):
-
52dd6c17— Rename parameterauto_filter_suffix→add_auto_filter_trailerinexecute_filtered_push. This aligns the parameter name with the previous review feedback that changed the auto-filter marker from a[auto-filtered]message suffix to a properAuto-Filtered: truegit trailer. The old name was vestigial. -
ffc1da21— Ruff formatting: line-break the_compose_filtered_message()call to stay within line-length limits.
Verification
- Behavioral change: None. The parameter was renamed but its type (
bool), default (True), and usage are identical. - Call sites: The only caller (
gateway/gateway.py:1265) does not pass this parameter by name — it uses the default. No test passes it by name either. Zero matches for the old nameauto_filter_suffixin the entire repo. - Previously verified criteria: All acceptance criteria spot-checked and still hold:
- Phase 1 (registry): store, routes, observer, client all present
- Phase 2 (building blocks): partition helper, attributed files, filtered push all present
- Phase 3 (push handler wiring): decision tree in gateway.py intact
- Phase 4 (scope-filter removal): no
scope-filterorEGG_AGENT_FILE_PATTERNSremnants - Phase 5 (tests): all 9 test files present
- Doc/code consistency:
Auto-Filtered: truetrailer throughout, no stale[auto-filtered]suffix references in docs
Verdict
Approve (posted as comment due to self-authored PR). The delta is a non-behavioral parameter rename and formatting fix. No contract violations, no regressions.
Note: egg-contract verify-criterion could not be run — orchestrator was unreachable across 5+ attempts during this review session. Criteria verification was done via direct codebase inspection.
— Authored by egg
|
egg contract-verification completed. View run logs 25 previous review(s) hidden. |
| egg is addressing review feedback... |
|
egg feedback addressed. View run logs 25 previous review(s) hidden. |
* Fix #1941: populate contract.pr on advance_phase out of plan advance_phase (especially force=true, the recovery hammer used to unstick plan-stuck pipelines) spawned a fresh _run_pipeline thread directly on the target phase, so the plan-phase populate step that writes contract.pr from the plan draft's yaml-tasks appendix never ran. The PR phase's auto-PR path then fell back to orchestrator placeholders — observed on PR #1937 during the #1938 recovery session. - Extract _populate_contract_from_plan_safe as the shared entrypoint used by _run_pipeline's post-complete block and advance_phase. - In advance_phase, when previous_phase == PLAN, run the helper and commit the populated contract so _sync_worktree_with_remote in the newly-spawned thread pushes (local-ahead path) rather than resets. - Failures warn and continue — blocking the advance hammer on a populate crash would defeat its purpose. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Add call-order assertion to test_populate_is_followed_by_commit Address review feedback: the test name implied it verified that populate was called before commit, but it only checked both were called. Use a shared call tracker to assert ordering across the two mocks. * Fix checks: apply automated formatting fixes --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: egg-reviewer[bot] <261018737+egg-reviewer[bot]@users.noreply.github.com> Co-authored-by: egg <egg@localhost>
* Initialize SDLC contract for issue #1882 * Add refine-phase analysis for #1882 Analysis for gateway auto-filter + pulled-commit handling. Covers: - Current state: 403-on-mixed-role-push in gateway.py; author-agnostic get_changed_files_in_push in git_client.py; client-side --scope-filter as opt-in workaround; sandbox entrypoint sets <role>@egg.local git identity; the unmerged #1470 implementation at commit 6f0877f. - Four-axis options table: (A) filtering strategy, (B) authorship signal, (C) rollout, (D) rewritten-commit semantics. - Recommended approach: port 6f0877f forward + author-email attribution in get_changed_files_in_push + same-release cutover + [auto-filtered] suffix on the squashed commit. - Test coverage checklist spanning single-role, all-blocked, mixed, and pulled-commit cases. Drafted at .egg-state/drafts/1882-analysis.md. * Persist statefiles after refine phase * Persist HITL resolution after refine phase gate * risk_analyst: risk assessment for issue #1882 gateway auto-filter Captures 12 risks (3 HIGH, 5 MEDIUM, 4 LOW) for the refine-HITL-resolved design: gateway-side commit-SHA authorship registry + interactive-rebase commit rewrite + client-side local-HEAD realignment + same-PR scope-filter removal. High risks: new durable store (R-01), new post-commit hook endpoint + sandbox wiring (R-02), mixed-history commit-tree rewrite (R-04), client-side realignment protocol (R-05). Flags areas needing human review: durable-store choice, kill-switch coupling, empty-after-filter commit policy, audit retention. Rollback plan has three tiers (env flag / PR revert / registry restore). * Add plan-phase task breakdown for issue #1882 Decomposes the refine-phase analysis into 5 phases / 20 tasks that ship as a single PR on egg/issue-1882: 1. Commit authorship registry (state-store table, gateway endpoint, sandbox post-commit hook) 2. Port #1470 auto-filter building blocks (partition helper, filter wrapper, _execute_filtered_push) 3. Wire registry + auto-filter into the push handler 4. Remove --scope-filter and its doc references 5. Test coverage for all 8 scenarios + registry + hook Honors HITL resolutions: B3 registry (decision-1), single-release cutover (decision-6), scope-filter removal (decision-8), fail-closed for unregistered commits (decision-9). Adopts refiner defaults for decisions 2, 3, 4, 5, 7, 17 (squash, nothing_to_push=true, [auto-filtered] suffix, pulled_commits field, checker.py location, fail-closed for unknown-role authorship). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Add architect analysis for #1882 Architecture design for gateway-side auto-filter with commit-authorship registry. Resolves HITL decisions 1-17: per-commit commit-tree rewrite preserving pulled cross-role commits, orchestrator state-store-backed registry, gateway-inline commit observation via /api/v1/git/execute, scope-filter removal in same PR, fail-closed on unregistered commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Revise plan to align with architect's structural refinements Incorporates the architect's two key design corrections: 1. Observation point is gateway /api/v1/git/execute, not a sandbox- installed post-commit hook. Sandbox containers have no direct .git access (tmpfs shadow), gateway sets core.hooksPath=/dev/null globally, and --no-verify would bypass any hook. Gateway-inline observation is bypass-proof and catches every commit-creating subcommand (commit, cherry-pick, rebase, revert, amend, merge). This is a strengthening of decision-1(d), documented prominently for reviewer confirmation. 2. Rewrite strategy is per-commit commit-tree/update-ref, not soft-reset + squash. Per-commit rewrite preserves pulled cross- role commits bitwise-unchanged and reparents own-commits individually; squash would rewrite pulled history or bail to 403 on mixed ranges. Adopts HITL decision-4's interactive-rebase- equivalent. Other refinements: partition helper lives in gateway/ agent_restrictions.py per decision-15 (refiner's shared/ default was valid when cli_push remained; scope-filter removal leaves no cross-component caller). Post-rewrite fast-forwards local HEAD and re-stages blocked files per decision-6. Removes EGG_AGENT_FILE_ PATTERNS env injection from orchestrator/concurrent_executor.py since cli_push stops consuming it. Response body gains pushed_ commits + pulled_commits fields. Phases restructured - (1) store + orchestrator routes + gateway observer + registry client; (2) partition helper + attributed-file enumeration + per-commit rewriter as self-contained pieces; (3) push handler integration with three-way dispatch and three audit events; (4) scope-filter + env plumbing removal; (5) tests including per-commit rewrite, 8-scenario end-to-end, observer, registry. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Tighten plan per reviewer_plan NACK feedback Addresses the two non-blocking details reviewer_plan flagged: 1. Re-record semantics: explicitly pin TASK-1-1 to first-wins. The initial (sha, role) binding is authoritative; re-register with a different role returns collision audit-log and preserves the original. Prevents observer-suppression-then-rewrite attack. Corresponding test added in TASK-5-1. 2. Unknown-role fail-closed: TASK-2-1 acceptance now explicitly cites shared/egg_restrictions/checker.py:42-44 deny-by-default, clarifies that unknown role returns ([], files) — all blocked — and adds a dedicated test case asserting this behavior. The five blocking items from reviewer_plan's NACK (sandbox hook infeasibility, soft-reset squash strategy, helper location, unknown-role semantics, mixed-author edge case) were already addressed in the prior revision that aligned with the architect's per-commit commit-tree walk and gateway-inline observer design. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Persist statefiles after plan phase * Document gateway auto-filter and commit-authorship registry (#1882) Replace the --scope-filter recovery story with the gateway auto-filter story. Agents no longer see 403 for agent-role file violations on push; the gateway rewrites the unpushed range, preserves pulled cross-role commits bitwise, drops own-commits that become empty after filtering, and returns filtered/excluded_files/pushed_commits/pulled_commits in the 200 response. The client-side --scope-filter flag and the EGG_AGENT_FILE_PATTERNS env var are gone. Changes: - docs/architecture/gateway-auto-filter.md (new) — full design: problem, outcome, commit-authorship registry (observation point rationale, first-wins semantics, storage, HTTP surface), push handler dispatch, per-commit rewrite algorithm, fail-closed invariant, what was removed, deploy ordering, monitoring. - docs/guides/agent-development.md — rewrite "Push Recovery and Scope Filtering" as "Push Filtering and Cross-Role Pushes" with the four outcomes table, per-commit rewrite walkthrough, kill switch / audit event reference, and a note that --scope-filter is gone. - docs/reference/orchestrator-cli.md — drop [--scope-filter] from the egg-orch push row; link to the architecture doc. - sandbox/agent-config/rules/push-recovery.md — rewrite the runtime rule: explain the four outcomes, the response-body fields agents should read, how to react in an agent loop, fail-closed semantics, the kill switch, and how to verify scope before committing. - docs/index.md — add the new architecture doc to the Architecture table and the "Gateway changes" task-specific row. - gateway/README.md — add auto-filter note to File-Level Access Restrictions, and add commit_observer.py / commit_registry_client.py to the file layout. - orchestrator/README.md — add commit_authorship_store.py and routes/commit_authorship.py; annotate state_store.py with the new sub-store. Aligned with the plan-phase TASK-4-3 documenter assignment; implements the "scope-filter references scrubbed from docs and agent-config rules" deliverable plus the architecture write-up the design warrants. * Fix #1882: commit-authorship registry (phase 1 of gateway auto-filter) Adds the B3 decision's durable registry that maps commit SHAs to the agent role that authored them. The orchestrator's state store hosts per-pipeline shards on the egg/pipeline-state orphan branch; two new HTTP routes expose first-wins register and bulk lookup under /api/v1/commit-authorship; the gateway observes every commit-creating operation in /api/v1/git/execute and registers the resulting SHAs via a dedicated HTTP client. - orchestrator/commit_authorship_store.py: sharded JSON store with first-wins semantics, idempotent re-register, bulk lookup. - orchestrator/routes/commit_authorship.py: register, register-bulk, and lookup endpoints, all behind require_lifecycle_secret. - orchestrator/api.py: register the new blueprint. - gateway/commit_registry_client.py: thin HTTP client (best-effort). - gateway/commit_observer.py: rev-list between captured HEADs, post each new SHA to the registry, swallow all failures. - gateway/gateway.py: instrument git_execute to snapshot HEAD before and invoke the observer after successful commit-producing ops. Phases 2-5 (auto-filter, push wiring, scope-filter removal, tests) follow in subsequent commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Fix #1882: auto-filter building blocks (phase 2) Adds the standalone helpers the push handler will wire up in phase 3: - gateway/agent_restrictions.py::partition_files_by_role — splits a file list into (allowed, blocked) for a given role; unknown role is fail-closed (every file blocked, WARNING logged). - gateway/git_client.py::AttributedFile + get_attributed_changed_files_in_push — per-commit diff-tree plus a bulk registry lookup to tag each file with the role that authored its commit. Fail-closed on diff-tree error or registry unreachable. - gateway/filtered_push.py::execute_filtered_push — the architect's per-commit rewrite algorithm using git commit-tree / update-ref. Pulled cross-role commits pass through bitwise-unchanged; own-role commits with blocked paths get new trees and an [auto-filtered] suffix; own-commits that become empty are dropped. Every error path restores HEAD and the worktree so the agent's local state is untouched after a failure. No push behaviour change yet — phase 3 wires these into git_push. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Fix #1882: wire registry + rewriter into push handler (phase 3) Replaces the 403 branch in git_push with the auto-filter decision tree: consult the commit-authorship registry to split files into own-authored vs pulled, check restrictions against only the own- authored set, and dispatch based on the result. - All own-allowed → plain push (200 with filtered=false, pulled_commits populated when any commit in the range is attributed to another role). - All own-blocked → 200 nothing_to_push=true, excluded_files populated, worktree untouched. Audit event push_all_blocked_no_op. - Mixed own-allowed + own-blocked → execute_filtered_push rewrites the range via commit-tree/update-ref, preserving pulled commits bitwise. Success surfaces filtered=true, excluded_files, pushed_files, pushed_commits, pulled_commits, rewritten_commits on the 200 response. Audit event push_auto_filtered. - Unregistered commits (fail-closed path) emit push_authorship_unregistered_fallback and are treated as own. - EGG_AGENT_RESTRICTIONS_ENFORCE=false short-circuits to warn-only passthrough (existing kill switch preserved). - The plain-push 200 response now includes filtered=false and pulled_commits for observability parity across paths. Phase / anchor / protected-file / branch-ownership / private-mode checks keep their 403 behaviour unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Fix #1882: remove --scope-filter and EGG_AGENT_FILE_PATTERNS (phase 4) The gateway's auto-filter handles every case the client-side workaround was built for, so the code paths behind ``egg-orch push --scope-filter`` and the ``EGG_AGENT_FILE_PATTERNS`` env var plumbing are removed: - sandbox/egg_lib/cli_push.py: collapse to a thin passthrough around ``git push`` with the existing EGG_BRANCH refspec retargeting. _filter_files, _matches_pattern, _matches_any_pattern, _get_merge_base, _resolve_push_args, and the --scope-filter argparse flag are gone. - orchestrator/concurrent_executor.py: stop emitting EGG_AGENT_FILE_PATTERNS; no consumer remains. Test updates (flipping test_concurrent_executor.py, test_cli_push.py, test_push_error_enrichment.py, and test_agent_restrictions_enforce.py for the new 200-with-filtered=true response shape) are TASK-5-10 for the tester role and will land in a follow-up commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Fix #1882: observer test compatibility + fail-closed fallback - gateway/gateway.py: replace the try/except-ImportError module lookups with a _load_sibling_gateway_module helper that works in both the production package layout and the test conftest's flat- module layout (falls back to importlib.util file-path loading when the conftest hasn't preloaded a sibling module). The observer now only fires on the narrow set of commit-producing ops (commit, merge, cherry-pick, revert, rebase, am) — and the post-op registration is gated on a successfully-captured before_head so non-mutating ops (status, restore, checkout, etc.) don't perturb the subprocess.run assertions in existing tests. - gateway/gateway.py: when attribution can't be computed (empty commit range, e.g. because the test mocked only the legacy file-detection path), treat the whole changed_files list as own-authored + unregistered and still run partition_files_by_role. The auto-filter decision tree then falls through as before. - gateway/git_client.py: _enumerate_push_commits parses only valid 7-64-char lowercase-hex SHA lines from git rev-list output so a mocked wrapper that echoes back URLs or arbitrary strings can't smuggle them into the commit list. - gateway/git_client.py + gateway/commit_observer.py: resolve commit_registry_client via the same sys.modules-first, file-path-fallback dance so the registry client module is reachable under both layouts without a conftest edit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Update docs to match coder's module layout (#1882) Coder placed the per-commit rewriter in its own module gateway/filtered_push.py (function execute_filtered_push) rather than as a private helper inside gateway/gateway.py. Update the architecture doc and gateway/README.md file listing to reference the actual module. * Fix #1882: test coverage for gateway auto-filter + commit-authorship registry Covers TASK-5-1 through TASK-5-10 from the plan. All new tests pass under pytest; ruff clean. New test files (138 new tests total): - orchestrator/tests/test_commit_authorship_store.py (39 tests) — round-trip, idempotent re-register, first-wins collision, bulk lookup, concurrent writes, per-pipeline sharding, state-branch commit on write, input validation, corrupt-shard handling. - orchestrator/tests/test_commit_authorship_routes.py (28 tests) — 401 auth gate, /register + /register-bulk + /lookup happy paths, collision 409, malformed-input 400, store-unavailable 500. - gateway/tests/test_commit_observer.py (18 tests) — HEAD snapshot, multi-commit detection via register_bulk, best-effort behavior on registry failure, non-agent session skip, observe_after_git_execute wrapper. - gateway/tests/test_commit_registry_client.py (21 tests) — 200 / 409 / 500 / network-error / 401 register paths, bulk register, lookup fail-closed, auth header emission. - gateway/tests/test_partition_files_by_role.py (18 tests) — all-allowed / all-blocked / mixed / empty / unknown-role deny-by-default / three-way precedence / structural invariants. - gateway/tests/test_git_client_attribution.py (16 tests) — attribution happy path, registry fail-closed, diff-tree errors, new-branch merge-base fallback, session_role advisory, fetch failure tolerated. - gateway/tests/test_execute_filtered_push.py (12 tests, skip if git init unavailable) — all 8 architect-defined scenarios. - gateway/tests/test_push_author_attribution.py (10 tests) — 8 refine- phase scenarios + response-schema invariants. - sandbox/tests/test_cli_push_scope_filter_removed.py (9 tests) — argparse rejection, source-level removal, passthrough semantics. - integration_tests/test_gateway_auto_filter_end_to_end.py (4 tests, marked integration) — registry round-trip, collision, push shape. Updated test files (TASK-5-10): - gateway/tests/test_agent_restrictions_enforce.py — rewritten to expect 200 with filtered=true / nothing_to_push=true instead of the old 403 enrichment path. - gateway/tests/test_push_error_enrichment.py — replaced #1527's 403-enrichment assertions with the new 200-response-shape contract. - orchestrator/tests/test_concurrent_executor.py::TestFilePatternEnvVar — inverted assertions: EGG_AGENT_FILE_PATTERNS must NOT be in the agent env after #1882. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Fix #1882: address reviewer_code NACK (5 blocking items) 1. Security hole in mixed-rewrite fallback (gateway/gateway.py) When get_attributed_changed_files_in_push returned empty/error but partition found an allowed set plus a blocked set, the handler invoked execute_filtered_push with an empty commit list — the rewriter walked nothing, pushed HEAD as-is, and blocked files went to origin unfiltered. Fix: treat attribution_fallback as unconditionally nothing_to_push=true whenever any file is blocked, so execute_filtered_push is never called without a commit range. Audit event carries attribution_fallback=true. 2. EGG_LIFECYCLE_SECRET on the gateway pod (k8s/base/gateway-deployment.yaml) The commit-authorship routes live behind require_lifecycle_secret; without the bearer token the gateway would 401 on every observer register and lookup, degrading the whole feature to fail-closed own-authored. Mirror orchestrator-deployment.yaml and pull the secret from gateway-secrets.lifecycle-secret. 3. Binary-safe restage (gateway/filtered_push.py) _restage_blocked_files ran git show with text=True, so non-UTF-8 blobs (PNG, PDF, compiled artefacts) were silently corrupted on the re-stage after a filtered push. Add a _git_raw helper that runs without text=True and write the content as bytes. 4. Actually stage content (gateway/filtered_push.py) git add --intent-to-add only records the path; the index still had no content so the next role's git commit would be empty. Switch to plain git add after the worktree write so git diff --cached shows the restaged file. 5. Warn-only observability parity (gateway/gateway.py) When EGG_AGENT_RESTRICTIONS_ENFORCE=false and blocked_own is non-empty, the warn-only branch fell through to plain push without setting pulled_commits on the response. Set auto_filter_response with filtered=false + pulled_commits in the warn-only path too. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs: reflect reviewer_code NACK fixes in auto-filter docs (#1882) The NACK-fix commit (ef291f3) introduced three user-visible behavior changes that the docs did not yet cover: 1. attribution-fallback short-circuit: when the handler cannot compute a commit walk, it unconditionally returns 200 nothing_to_push=true for any blocked files rather than invoking the rewriter on an empty commit list. Added a dedicated section under "Push handler dispatch" explaining the trigger, outcome, audit event (attribution_fallback flag on push_all_blocked_no_op), and response-message text. 2. Warn-only observability parity: EGG_AGENT_RESTRICTIONS_ENFORCE=false now returns filtered=false, excluded_files=[], pushed_files, and pulled_commits in the success body so downstream tooling does not have to branch on enforcement mode. Updated the kill-switch bullets in the architecture doc, agent-development guide, and the runtime push-recovery rule that the docs previously claimed returned "no new response fields". 3. Binary-safe re-staging: documented the _git_raw / bytes-mode read that preserves non-UTF-8 blobs (PNG, PDF, compiled artefacts) on re-stage, and the git add vs --intent-to-add choice that makes the re-staged blob visible to the next role's git commit. Also added a Deployment prerequisites section calling out the EGG_LIFECYCLE_SECRET requirement on the gateway pod (the reviewer's k8s-manifest finding), and updated the monitoring section to describe what the new attribution_fallback=true audit field means for triage. Files updated: - docs/architecture/gateway-auto-filter.md — new Attribution-fallback short-circuit subsection, expanded Fail-closed invariant, new Binary-safe re-staging + Deployment prerequisites sections, monitoring bullet enriched. - docs/guides/agent-development.md — kill-switch bullet now says the schema stays consistent in warn-only mode; audit-event list names the attribution_fallback field. - sandbox/agent-config/rules/push-recovery.md — kill-switch paragraph and the unregistered-commits paragraph both updated so agents know what to do if they see the attribution-fallback short-circuit. * Add NACK-fix regression tests + reformat test files [tester] (#1882) Adds gateway/tests/test_push_nack_fix_regressions.py covering the five reviewer_code NACK items that commit ef291f3 fixed: 1. Attribution-fallback security hole — when get_attributed_changed_files_in_push returns empty/errored and the pusher has blocked files, the handler MUST return nothing_to_push=true and MUST NOT invoke execute_filtered_push with an empty commit list (which would push HEAD verbatim, leaking blocked files). 2. Binary-safe restage — _git_raw must be used for git show so non-UTF-8 blobs (PNG, PDF, compiled artifacts) are preserved verbatim; _git (text=True) would corrupt them. 3. git add (no --intent-to-add) — verifies _restage_blocked_files actually stages blob content so the next role's git commit picks up the restored file. 4. Warn-only observability parity — EGG_AGENT_RESTRICTIONS_ENFORCE=false with blocked own files must still surface filtered=false + pulled_commits in the response body. 5. Audit-log attribution_fallback flag on push_all_blocked_no_op so operators can distinguish fail-closed-due-to-missing-attribution from real all-blocked pushes. Also applies ruff format to the 10 test files introduced/updated by the coder's test commit (eb6a5ca) so the lint gate is clean: - gateway/tests/test_agent_restrictions_enforce.py - gateway/tests/test_commit_observer.py - gateway/tests/test_commit_registry_client.py - gateway/tests/test_execute_filtered_push.py - gateway/tests/test_git_client_attribution.py - gateway/tests/test_push_author_attribution.py - gateway/tests/test_push_error_enrichment.py - orchestrator/tests/test_commit_authorship_routes.py - orchestrator/tests/test_commit_authorship_store.py - sandbox/tests/test_cli_push_scope_filter_removed.py All 235 tests related to #1882 pass; the new regression suite adds 10 tests (gateway/tests/test_push_nack_fix_regressions.py). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs: update agent-roles reference for #1882 auto-filter behavior The reference doc still described "Gateway Push Validation" as blocking pushes on any agent-role violation with EGG_AGENT_RESTRICTIONS_ENFORCE toggling between enforce/warn-only. That contract changed in #1882 and the doc hadn't caught up yet. Updates: - Rename "Gateway push validation" → "Gateway push auto-filter" in the enforcement-layers table; replace the "blocks pushes" description with the per-commit rewrite + pulled-commit-passthrough behavior. - Rewrite the Gateway Push Auto-Filter section to describe the per-commit rewrite (own commits rewritten with [auto-filtered] suffix, commits that become empty dropped, pulled cross-role commits bitwise-unchanged), response fields (filtered, excluded_files, pushed_files, pushed_commits, pulled_commits), and the nothing_to_push short-circuit. - Document the EGG_AGENT_RESTRICTIONS_ENFORCE kill switch as a short- circuit-to-plain-push rather than warn-only, and call out that the response schema is stable across both modes. - Clarify that phase/anchor/protected-file/branch-ownership/private-mode/ concurrent-mode checks keep their 403 — only agent-role restrictions auto-filter. - Replace the get_changed_files_in_push() description with the new get_attributed_changed_files_in_push() walk + registry lookup. - Add a note under "Per-Agent Git Identity" explaining that commit.author is display-only now; authoritative attribution at push time comes from the commit-authorship registry (populated by the gateway's git-execute observer using the session token, not the sandbox-set user.email). Links back to docs/architecture/gateway-auto-filter.md for the full design. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs: link Gateway Auto-Filter from architecture README [documenter] (#1882) The architecture README already described the gateway's role in policy enforcement but never referenced the new per-commit rewrite design that landed in #1882: - Access-control bullet still claimed the enforce-mode flag toggles between "enforced" and "warn-only", which no longer matches the auto-filter dispatch. Rewritten to describe the auto-filter + bitwise-unchanged cross-role pass-through, with the kill-switch correctly labelled as a short-circuit to plain push. - Added a "Gateway Auto-Filter" bullet to the "Key Architectural Decisions" list so readers browsing that section can find the new design doc alongside Git Isolation and Credential Injection. The dedicated architecture write-up already exists at docs/architecture/gateway-auto-filter.md (landed in 38b74a8, sharpened in 7a91144 / 39565bc / d473d55). This commit just threads the navigation through the architecture README. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Remove stale test_cli_push.py after --scope-filter removal (#1882) The file imports _filter_files, _matches_pattern, _matches_any_pattern, and _get_merge_base — all of which were removed along with the --scope-filter workflow in commit a087932 ("remove --scope-filter and EGG_AGENT_FILE_PATTERNS (phase 4)"). The replacement behaviour is now covered by sandbox/tests/test_cli_push_scope_filter_removed.py, so this file is not just broken but redundant — it blocks pytest collection for the whole suite with an ImportError at module-load time. Delete it rather than paper over the ImportError. Scope-filter functionality is not coming back; the gateway auto-filter (#1882) is the only remaining path. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Fix #1882: reviewer_code NACK — preserve merge parents + trailer-safe suffix Addresses the two blocking items in reviewer_code's NACK on the #1882 auto-filter implementation: 1. Multi-parent (merge) commits silently lost their 2nd+ parents. `_commit_tree` only emitted one `-p` flag and the call sites read only `orig_parent_list[0]`. A pulled merge-commit following a rewritten own-commit would get collapsed into a single-parent commit, silently dropping the merge's second parent. Fix: extend `_commit_tree` to accept a list of parent SHAs and emit one `-p` per entry. Add `_translate_parents()` which maps the original parent list through `parent_lookup` (preserving merge parents verbatim unless they were themselves rewritten earlier in the walk) and shifts the first parent onto the running chain when needed. All three call sites — pulled pass-through, own no-filter pass-through, own-filter rewrite — now use the new helper. A commit with *all* parents unchanged from their original values AND the first parent matching the running tip is still reused verbatim (no new SHA). 2. Auto-filter suffix corrupted commit trailers. The old code `meta["message"].rstrip() + " [auto-filtered]"` stripped the trailing newline and glued the marker onto the last line — so `Signed-off-by: alice <a@x>` became `Signed-off-by: alice <a@x> [auto-filtered]`, breaking `git interpret-trailers`, GitHub's Co-Authored-By rendering, and DCO parsing. Fix: new helper `_compose_filtered_message()` emits the marker as its own paragraph (one blank line before, one newline after) so the trailer block survives intact. The helper handles empty messages, empty suffixes, and extraneous trailing whitespace. Non-blocking items addressed in the same commit (low-risk): - `gateway/git_client.py` — `get_attributed_changed_files_in_push` now emits a WARNING `commit_authorship_partial_lookup` event when the registry returned a proper subset of requested SHAs. Missing SHAs still fall through to fail-closed (None → own-authored); this just gives operators a signal to notice a flaky registry that would otherwise silently subject every cross-role push to restriction checks. - `gateway/filtered_push.py` — elevate post-push rewritten-commit registration failures from DEBUG to WARNING and include the branch name. A subsequent cross-role push would correctly fail-closed, but the audit trail is now useful. - `gateway/gateway.py` — add `enforce=False` to the warn-only audit log payload so operators scanning logs during a kill-switch window can distinguish the warn-only passthrough from enforced paths. - `orchestrator/commit_authorship_store.py` — replace `str.startswith(base)` path-traversal check with `path.resolve().relative_to(base.resolve())` (path-aware, won't prefix-match a sister directory). Also give `_validate_role` an explicit empty-string error message rather than a confusing regex-mismatch report for direct Python callers. Regression tests for the two blocking issues are sent to the tester via a directed HANDOFF message — coder cannot write to gateway/tests/ under the role file-boundary policy. The tests live at gateway/tests/test_filtered_push_helpers.py in the coder worktree for the tester to pick up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Add regression tests for #1882 NACK fix (for tester to pick up) Coder role cannot write to gateway/tests/. Stage the tests here in agent-outputs/ so the tester picks them up and commits them from the tester worktree. - test_filtered_push_helpers.py: 18 pure-Python tests for the two NACK-fix helpers — _compose_filtered_message (trailer preservation) and _translate_parents (merge-parent preservation). - tester-patch.diff: updates to gateway/tests/test_execute_filtered_push.py (assert the "\n\n[auto-filtered]" trailer-safe form) and gateway/tests/test_push_nack_fix_regressions.py (add sys.path insert so it can be collected in isolation). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Pick up #1882 NACK-fix regression tests from coder HANDOFF Coder sent a HANDOFF at 20:48 UTC with 18 new helper tests plus a 2-file patch for existing tester-owned files. The coder's own file-boundary policy prevented writing to gateway/tests/, so the tester picks up the artifacts from .egg-state/agent-outputs/1882-coder-tests/ and commits them here. New file gateway/tests/test_filtered_push_helpers.py (231 lines): - TestComposeFilteredMessage (8): Signed-off-by / Co-Authored-By / multi-paragraph / trailing-whitespace preservation for the trailer-safe \n\n[auto-filtered] suffix. - TestTranslateParents (8): single-parent chain shift, 2-parent merge preservation, first-parent rewritten via chain shift, 2nd-parent rewritten via lookup, 3-parent octopus, root commit, empty-new-parent fallback, identity-lookup no-op. - TestCommitTreeAcceptsMultipleParents (2): signature back-compat assertion covering the new list-of-parents interface. Patch applied to: - gateway/tests/test_execute_filtered_push.py: tighten the auto-filter trailer assertion to require '\n\n[auto-filtered]' (double-newline separator) rather than a loose endswith(). - gateway/tests/test_push_nack_fix_regressions.py: insert gateway/ onto sys.path so the file can be collected in isolation without relying on collection order. Also ran ruff check --fix + ruff format on the newly-landed test file to satisfy project lint before commit. All 28 affected tests pass: 18 new helper tests plus the 10 existing tests in the two patched files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Fix #1882: tester HANDOFF — lint/format/mypy cleanup in source files Addresses the six lint/format/mypy blockers tester identified in the HANDOFF at 20:46 UTC, so `make lint` passes end-to-end on the 1882 surface. Ruff (6 errors, all auto-fixed + re-exports explicitly preserved): - gateway/commit_observer.py: typing.Iterable → collections.abc.Iterable (UP035). - gateway/filtered_push.py: drop unused `import dataclasses` (F401). - gateway/gateway.py: drop unused `get_agent_pattern` / `check_agent_restrictions` from the primary relative-import block (F401), then re-export both under `# noqa: F401` from both try and except import branches — they're used via test patching (`gateway.check_agent_restrictions` et al. in test_concurrent_push_block.py, test_phase_filter_restrictions.py, test_agent_restrictions_enforce.py), not directly. - gateway/git_client.py: fix I001 import-order in the inline commit_registry_client loader. - orchestrator/concurrent_executor.py: drop unused `import json`. Ruff format (7 files) — auto-applied via `ruff format`. Mypy (14 relevant errors on the 1882 surface): - gateway/filtered_push.py:394,450,491,569 — false-positive "Incompatible types in assignment (str | None, variable has type str)" that masked a genuine potential-None. Fix: in each of the three walk branches (pulled, own-no-filter, own-filter), use a separate local name for the `_commit_tree` return (`built_sha`/`rewritten_sha`) and assign to the outer `new_sha`/`new_sha_passthrough`/`rewritten_sha` only after the `if err or x is None: rollback+return` narrowing. Also renamed the registry-register loop variable to `reg_sha` so it can't shadow the walk-scope `new_sha`. - gateway/commit_observer.py:173 / git_client.py:1719 — duplicate `get_client` definition inside the conditional import block. Rewrite the fallback to import the module and reach into `_crc.get_client` rather than `from commit_registry_client import get_client` inside an except branch (which mypy saw as a redefinition). - gateway/filtered_push.py / commit_observer.py / commit_registry_client.py — match the egg_logging `get_logger` signature in the ImportError fallback so mypy doesn't complain about divergent conditional function variants; add explicit return-type annotations. - gateway/filtered_push.py::execute_filtered_push — type annotate `push_fn` and `registry_register` parameters (via Callable) so the `# type: ignore[no-untyped-def]` comments can drop. - gateway/gateway.py::_load_sibling_gateway_module and ::_lookup_commit_observer_fn — add `-> Any` return type. - gateway/gateway.py::git_push — thread partition_files_by_role and get_attributed_changed_files_in_push through renamed `_partition_fn` / `_get_attributed_fn` locals (typed `Any`) so the getattr-style resolution no longer conflicts with the subsequent function call (fixes "None not callable" and "name already defined" errors). - gateway/gateway.py — annotate `own_files`, `pulled_files`, `unregistered_files` in the attribution-fallback branch. - gateway/git_client.py:1743 — union-attr: registry_client is typed `object`; assign to an `Any` local before calling `lookup_bulk` so mypy accepts the duck-typed method. Adds `from typing import Any`. A matching tester-owned test-file update (sandbox/tests/test_cli_push_scope_filter_removed.py — `monkeypatch.setattr` switched to a string-based path so mypy doesn't flag the implicit `cli_push.subprocess` attribute re-export) is staged at .egg-state/agent-outputs/1882-coder-tests/tester-mypy-patch.diff for the tester to pick up — coder cannot write to sandbox/tests/ under the role file-boundary policy. Verified: make lint passes (ruff check + format + mypy), 2088 gateway tests pass (only pre-existing health-server port conflicts fail — unrelated). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Fix mypy: explicitly re-export subprocess in cli_push * Fix filtered push: use --force-remove to strip blocked paths from index git update-index --remove only removes index entries when the file is also absent from the working directory. Since blocked files still exist on disk during the rewrite walk, --remove silently becomes a no-op — leaving blocked paths in the rewritten tree and preventing empty-commit detection from dropping docs-only commits. Switch to --force-remove which unconditionally removes the index entry regardless of working directory state. * Fix filtered push: strip inherited blocked files from pass-through own commits The pass-through path for own commits (where none of the commit's own files are blocked) was preserving the original tree unchanged. When an earlier commit introduced a blocked file, later commits inherited that file in their tree, causing it to reappear in the pushed result. Now the pass-through path also applies _filter_tree against the full blocked_own_files set, ensuring blocked files from earlier commits don't leak through. * Address review feedback on #1937 - AC-8: Add excluded_files + pulled_commits to push_authorship_unregistered_fallback audit event - Fix observer gate: use _observer_armed flag instead of before_head None check for unborn-branch support - Wrap attribution lookup in try/except to prevent 500 on unexpected exceptions (fail-closed) - Emit Auto-Filtered: true as a proper git trailer instead of appending [auto-filtered] after trailers - Fix import subprocess as subprocess self-alias in cli_push.py - Document observer operation list as intentionally exhaustive - Add test for attribution-lookup exception (scenario 9) * Fix mypy attr-defined errors in test_cli_push_scope_filter_removed Import subprocess directly instead of accessing it via cli_push.subprocess, which mypy flags under strict/no-implicit-reexport mode. * Address re-review feedback on #1937 - Fix doc/code mismatch: update all docs referencing [auto-filtered] suffix to reflect the Auto-Filtered: true git trailer (push-recovery.md, gateway-auto-filter.md, agent-development.md, agent-roles.md, README.md) - Replace vestigial suffix: str parameter with add_trailer: bool in _compose_filtered_message (value was ignored, only truthiness mattered) - Add missing schema assertions to scenario 9 test (filtered, pushed_files, pushed_commits, pulled_commits) - Add test documenting _TRAILER_LINE_RE behavior with body text patterns like Problem:/Reason: that match the trailer regex - Upgrade rollback update-ref failure log from WARNING to ERROR for visibility in alerting * Rename auto_filter_suffix to add_auto_filter_trailer for clarity * Fix ruff formatting in gateway/filtered_push.py --------- Co-authored-by: egg-orchestrator <egg@localhost> Co-authored-by: egg <egg@example.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: jwbron <8340608+jwbron@users.noreply.github.com> Co-authored-by: james-in-a-box[bot] <246424927+james-in-a-box[bot]@users.noreply.github.com> Co-authored-by: egg-reviewer[bot] <261018737+egg-reviewer[bot]@users.noreply.github.com>
…re (#1970) * Fix #1966: wire tester check-coverage validator to the real state store _validate_tester_check_coverage imported a non-existent `pipeline_state` module and swallowed the ImportError, silently no-opping in production since #1459 landed. Its existing tests injected a fake `pipeline_state` module into sys.modules, so CI never caught the break. With the gate disabled, testers were proposing consensus with ad-hoc check names (e.g. `pytest-tester-suite`, `ruff-check-tester-files`) instead of the configured `lint`/`test`/`security` from `repositories.yaml`. That let real `make lint` / `make test` failures through — visible on #1966 as red Lint/Test checks on the initial push of several recent SDLC PRs (#1964, #1937, #1920, …). Fix: use the real `state_store.get_state_store(repo_path)` and `pipeline.repo`. Rewrite the tests to patch `routes.signals.get_state_store` directly, add a regression test that sub-scoped ad-hoc names are rejected, and drop the now-orphaned `types` import. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Fix second broken pipeline_state import in confirmed-signal fallback handle_consensus_confirmed_signal's tracker-reconstruction fallback has the same dead-code import pattern: on tracker loss it tries `from pipeline_state import get_pipeline_state_store`, silently swallows the ImportError, and leaves _phase/_repo at defaults. Consequence (low-severity, defensive path only): on non-egg repos with a lost tracker, _repo stays None and get_review_graph_for_phase returns the egg graph with egg-specific reviewers included, so the subsequent `all_roles.issubset(confirmed_roles)` check in the message-bus authoritative fallback fails against roles the pipeline doesn't have. Fix: reuse the real get_state_store(repo_path) and pipeline.repo, same as the tester-check-coverage fix in the previous commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Address review feedback: widen state-store exception handling - Catch StateStoreError (base class) instead of specific subclasses in both _validate_tester_check_coverage and handle_consensus_confirmed_signal to handle StateValidationError and GitOperationError gracefully. - Re-add defensive try/except around get_repo_checks() with a logger warning, so missing/malformed repositories.yaml degrades gracefully instead of surfacing a 500. - Simplify regression test regex from multi-alternative to single deterministic pattern (sorted() guarantees order). - Add test_state_validation_error_skips_validation to cover the widened catch. * Fix checks: exclude functional tests from unit test target The functional tests (tests/functional/) require Docker to build and run a gateway container. They time out under the 60s pytest-timeout cap in CI because docker build takes longer than 60s. These tests were introduced in 5d8cc69 but not excluded from the default make test invocation. Add -m 'not functional' to the Makefile test target so Docker-dependent functional tests are not collected during unit test runs. * Tester prompt: explicit procedure for source-code check failures The validator in 018fce4 blocks proposals with non-configured check names server-side, but the tester prompt itself still left a behavioural hole: when a configured check failed because of coder source code, the prompt told the tester both "do not fix source" and "all checks must pass before proposing", with no resolution. On #1964 the tester rationalised this by attesting to ad-hoc check names like ruff-check-tester-files — the silent-validator bug then let it through. Add a "When Source-Code Checks Fail (CRITICAL)" subsection that: - forbids fixing source and forbids inventing substitute check names, - prescribes a HANDOFF message to the coder via egg-orch message send, - tells the tester to wait via egg-orch message wait-loop and re-run every configured check before proposing. Also tighten the Attestation paragraph to explicitly forbid ad-hoc names like ruff-check-tester-files (defence-in-depth on top of the server-side validator). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Add test for get_repo_checks failure path --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: egg-reviewer[bot] <261018737+egg-reviewer[bot]@users.noreply.github.com> Co-authored-by: james-in-a-box[bot] <246424927+james-in-a-box[bot]@users.noreply.github.com>
Closes #1882
Today the gateway rejects a push with HTTP 403 whenever any file
in the push diff is outside the pushing role's allowed patterns
(
gateway/gateway.py:967-1034). Agents recover via the opt-in,client-side
egg-orch push --scope-filtercommand — which coststokens, requires the agent to interpret the 403 correctly, and
actively breaks when the push contains commits pulled in from
another role (e.g. during a cross-role handoff or rebase). The
unmerged work from #1470 (commit 6f0877f) was meant to close
this but never landed.
This PR revives the gateway-side auto-filter and extends it so
mixed-author pushes are handled correctly:
gains a per-pipeline
commit-authorshipsub-store mappingcommit_sha → role. A new gateway observer runs inline inPOST /api/v1/git/execute: whenever a git subcommand mutatesHEAD, the gateway POSTs each new SHA to the orchestrator's
/api/v1/commit-authorship/registerendpoint with the rolethat owns the calling session token. This is the only
workable design given the sandbox's tmpfs-shadowed
.gitinvariant — a sandbox-installed post-commit hook (as
decision-1(d) phrased) could not persist or survive
--no-verify. The gateway-inline observer is bypass-proof.partition_files_by_role(ingateway/agent_restrictions.py)and
_execute_filtered_push(ingateway/gateway.py). Whenthe pushing role's own commits mix allowed and blocked files,
the gateway walks the unpushed range with
git commit-tree+git update-ref: pulled cross-rolecommits pass through bitwise-unchanged (parents re-targeted
as needed); own-role commits get new tree objects with
blocked paths removed and an
[auto-filtered]suffix on themessage. Empty own-commits after filtering are dropped.
registry to split the diff into own-authored files (subject
to restrictions) and pulled files (exempt). Unregistered
commits fall back to fail-closed, so observer suppression
cannot bypass restrictions. After a successful filtered
push, local HEAD is fast-forwarded to match origin and the
blocked files are re-staged as uncommitted changes for the
next role (decision-6). All-own-blocked pushes return 200
with
nothing_to_push=true. Responses gain apulled_commitsfield listing{sha, author_role}for everynon-own-authored commit in the push.
--scope-filter— the gateway now handles everycase the client-side workaround was built for, so
sandbox/egg_lib/cli_push.py --scope-filter, its_filter_fileshelper, and theEGG_AGENT_FILE_PATTERNSenv-var injection in
orchestrator/concurrent_executor.pyare all deleted. Docs and sandbox agent-config rules that
reference the flag are updated.
After this PR, agents no longer see "push denied: agent role
cannot modify these files" for mixed-role diffs. Phase / anchor
/ protected-file / branch-ownership / private-mode / concurrent-
mode checks keep their 403 behavior unchanged. The kill switch
EGG_AGENT_RESTRICTIONS_ENFORCE=falseremains for emergencyrollback to warn-only mode.
Deviation from decision-1(d) phrasing: the decision text
describes a sandbox-installed post-commit hook. This PR
implements a gateway-inline observer instead, for the reasons
above. The outcome (every agent commit registered with its
role) is functionally identical; the guarantees are strictly
stronger. Flagged here for reviewer awareness.
Test Plan
orchestrator/tests/test_commit_authorship_store.py—register idempotency, bulk lookup hit / miss / partial,
concurrent writes, per-pipeline sharding, state-branch
commit on write.
orchestrator/tests/test_commit_authorship_routes.py—/register + /lookup endpoint contracts, inter-pod auth,
malformed input, state-store-down.
gateway/tests/test_partition_files_by_role.py— all-allowed, all-blocked, mixed, unknown role, empty, precedence
of blocked / block-exempt / allowed.
gateway/tests/test_commit_observer.py— HEAD snapshotbefore / after, multi-commit detection (cherry-pick of N
fires N POSTs), best-effort under registry-unavailable,
non-agent session skipped.
gateway/tests/test_git_client_attribution.py—get_attributed_changed_files_in_pushhappy path, fail-closed on diff-tree error, registry-unavailable fail-closed.
gateway/tests/test_execute_filtered_push.py— single-commit mixed, multi-commit all-own mixed (structure
preserved), interleaved own+pulled (pulled bitwise
identical), own-commit-becomes-empty drop, new-branch
merge-base path, rollback on mid-walk exception, rollback
on push failure, post-success worktree has blocked files
re-staged.
gateway/tests/test_push_author_attribution.pywith one testper scenario — own-only allowed / blocked / mixed; mixed
clean / pulled-would-be-blocked / own-blocked; registry-
unavailable fail-closed; warn-only mode. Each asserts
response shape (
filtered,excluded_files,pushed_files,pushed_commits,pulled_commits), audit log event type,and worktree state afterwards.
integration_tests/test_gateway_auto_filter_end_to_end.pysimulating an agent container, creating commits through
/api/v1/git/execute(registry writes), then pushing amixed-role range through
/api/v1/git/push(auto-filter +pulled passthrough).
sandbox/tests/test_cli_push_scope_filter_removed.py—argparse rejects
--scope-filtercleanly.confirm from gateway audit logs that
push_auto_filteredfires at least once and that pulled commits appear in the
pulled_commitsresponse field with their attributed role.Confirm the worktree contains the filtered files as staged
changes after the push and that
git log --oneline origin/ <branch>..HEADis empty (local fast-forwarded to origin).Manual Steps
Pre-merge:
commit-authorship/subdirectory on first write (spot-check via
ls .egg-state/pipeline-worktree/commit- authorship/on the orchestrator pod after CI finishes).being sent on /register and /lookup calls (unit test only
verifies shape; a live-deploy smoke-test catches
misconfigured secrets).
Post-merge:
/api/v1/commit-authorship/*endpoints exist before anygateway tries to POST to them. (The observer fails open
when the orchestrator is unreachable, so a brief mismatch
is non-fatal but generates noise.)
commit-observation code path in this PR; the only sandbox
change is the removal of
--scope-filterandEGG_AGENT_FILE_PATTERNS, which simply tightens thesurface.
push_authorship_unregistered_fallbackentries. A sustainedspike means the observer is missing some commit-creating
subcommand — investigate and file a follow-up.
sessions that predate the deploy continue to work; their
commits fail-closed at push time under the pushing role's
restrictions — equivalent to today's behavior.
Pipeline Context
Pipeline:
issue-1882Issue: #1882
Per-phase BRC transcripts:
implement.Authored-by: egg