Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/architecture/git-isolation.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ This document focuses on the specific challenge of **multi-agent git isolation**
| Agent accesses another agent's workspace | Filesystem isolation---other workspaces don't exist in container's view |
| Agent pushes to unauthorized branches | Gateway enforces branch ownership policy |
| Agent pushes malicious code directly to main | Gateway blocks direct pushes to protected branches; PRs require human review |
| Agent bypasses BRC consensus in concurrent mode | Gateway blocks direct `git push` in concurrent mode; requires `consensus_push` marker from `egg-orch consensus propose --push` |
| Agent bypasses BRC consensus in concurrent mode | Gateway blocks direct `git push` in concurrent mode; requires `consensus_push` marker from `mcp__brc__propose` (or fallback `egg-orch consensus propose --push`) |
| Agent discovers or exfiltrates credentials | Credentials only exist in gateway; container never sees them |
| Agent modifies git config to bypass security | Container has no access to git metadata; config is gateway-controlled |
| Agent escapes via git hooks or filters | Hooks universally disabled via `core.hooksPath=/dev/null` in gateway and orchestrator; filters mitigated in containers by metadata isolation (no `.gitattributes` processing); gateway protected by branch ownership policy (agents cannot push to main) and required human review of all commits |
Expand Down Expand Up @@ -258,7 +258,7 @@ Each agent works on its own isolated worktree with its own staging area. This ap

**Pipeline agents:** In concurrent pipeline execution, all agents push to the same shared branch (e.g., `egg/issue-{N}`) but each agent has its own worktree. Since each role has mutually exclusive file write permissions (coder → source code, tester → tests, documenter → docs), push rebases cannot conflict. Reviewer agents sync their worktrees before reviewing by fetching and merging the pipeline branch, ensuring they evaluate up-to-date code from producers. See [Concurrent Execution Guide](../guides/concurrent-execution.md#per-agent-worktree-isolation) for details.

**Concurrent-mode push enforcement:** In BRC mode, the gateway blocks direct `git push` from pipeline agents — all pushes must go through `egg-orch consensus propose --push`, which bundles the push with a BRC proposal. This structurally enforces the "all changes must be reviewed" invariant rather than relying on agent compliance. See [Gateway README — Concurrent-Mode Push Enforcement](../../gateway/README.md#concurrent-mode-push-enforcement-brc-sessions) for details.
**Concurrent-mode push enforcement:** In BRC mode, the gateway blocks direct `git push` from pipeline agents — all pushes must go through `mcp__brc__propose` (which pushes to origin and sends CONSENSUS_PROPOSE in one step; push is on by default). The fallback CLI is `egg-orch consensus propose --push`. This structurally enforces the "all changes must be reviewed" invariant rather than relying on agent compliance. See [Gateway README — Concurrent-Mode Push Enforcement](../../gateway/README.md#concurrent-mode-push-enforcement-brc-sessions) for details.

**Worktree-aware APIs:** All gateway APIs that access the filesystem use `map_container_path_to_worktree()` to resolve container repo paths to worktree paths. This includes git operations, contract operations (`egg-contract show`, `add-commit`, `add-decision`, etc.), and checkpoint operations. The mapping is transparent to agents --- they use their normal repo path and the gateway resolves it to the correct worktree.

Expand Down
2 changes: 1 addition & 1 deletion docs/guides/agent-teams.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ When a producer pushes new commits after proposing, existing reviews become stal

This mechanism enforces the principle that **all changes must be reviewed**: post-proposal pushes cannot bypass the review process. The `check_confirm_guard()` provides a server-side blocking mechanism even if a reviewer misses the `CONSENSUS_RE_REVIEW` notification. See [Concurrent Execution — Auto Re-Propose on Push/Commit](concurrent-execution.md#auto-re-propose-on-pushcommit) for the full details.

Additionally, the gateway enforces that **direct `git push` is blocked** in concurrent mode — agents must use `egg-orch consensus propose --push` to bundle the push with a BRC proposal. This makes the review invariant structural rather than relying on auto-repropose detection. See [Concurrent Execution — Gateway-Level Push Enforcement](concurrent-execution.md#gateway-level-push-enforcement-concurrent-mode) for details.
Additionally, the gateway enforces that **direct `git push` is blocked** in concurrent mode — agents must use `mcp__brc__propose` (or the fallback CLI `egg-orch consensus propose --push`) to bundle the push with a BRC proposal. This makes the review invariant structural rather than relying on auto-repropose detection. See [Concurrent Execution — Gateway-Level Push Enforcement](concurrent-execution.md#gateway-level-push-enforcement-concurrent-mode) for details.

### Agent Crash Mid-Protocol

Expand Down
14 changes: 8 additions & 6 deletions docs/guides/concurrent-execution.md
Original file line number Diff line number Diff line change
Expand Up @@ -480,23 +480,25 @@ When a producer pushes new commits after proposing, existing reviews become stal

### Gateway-Level Push Enforcement (Concurrent Mode)

While auto re-propose provides a **safety net** for stale reviews, it relies on the orchestrator detecting post-proposal pushes. A stronger guarantee comes from the gateway itself: in concurrent mode, **direct `git push` is blocked** — all pushes must go through `egg-orch consensus propose --push`.
While auto re-propose provides a **safety net** for stale reviews, it relies on the orchestrator detecting post-proposal pushes. A stronger guarantee comes from the gateway itself: in concurrent mode, **direct `git push` is blocked** — all pushes must go through `mcp__brc__propose` (the fallback CLI is `egg-orch consensus propose --push`).

**How the marker flows:**

1. Agent runs `egg-orch consensus propose --push`
2. The orch CLI calls the gateway push API directly (bypassing the git wrapper) with `"consensus_push": true` in the JSON payload
1. Agent calls `mcp__brc__propose(...)` (push defaults to true) — or runs `egg-orch consensus propose --push`
2. Both surfaces delegate to `egg_agent_tools.push.consensus_push()`, which calls the gateway push API directly (bypassing the git wrapper) with `"consensus_push": true` in the JSON payload
3. The gateway checks: if `EGG_CONCURRENT_MODE=true` AND the session has a `pipeline_id` AND the push is not infrastructure (checkpoints/pipeline state), then `consensus_push` must be present
4. Pushes without the marker are rejected with HTTP 403
5. Fallback: when `GATEWAY_URL` is not set (e.g., local development), the orch CLI falls back to plain `git push`. No concurrent-mode enforcement exists in this path — the gateway is not running to enforce it
4. Pushes without the marker are rejected with HTTP 403 and the error points at `mcp__brc__propose`
5. Fallback: when `GATEWAY_URL` is not set (e.g., local development), the helper falls back to plain `git push`. No concurrent-mode enforcement exists in this path — the gateway is not running to enforce it

**Relationship to auto re-propose:** Gateway enforcement makes auto re-propose less critical in concurrent mode — every push IS a proposal, so there are no "orphan pushes" to detect. Auto re-propose remains as defense-in-depth for edge cases (e.g., if an agent manages to push through an alternative path).

**Killswitch:** Set `CONCURRENT_PUSH_ENFORCEMENT=false` on the gateway to disable. Use only for emergency bypass.

**Error message for agents:**
```
Direct push blocked in concurrent mode. Use: egg-orch consensus propose --push
Direct git push is blocked in BRC mode. Publish your artifact via the
mcp__brc__propose tool (which pushes to origin and sends CONSENSUS_PROPOSE
in one step). Fallback CLI: `egg-orch consensus propose --push`.
```

See [Gateway README — Concurrent-Mode Push Enforcement](../../gateway/README.md#concurrent-mode-push-enforcement-brc-sessions) for implementation details. See [#1669](https://github.com/jwbron/egg/issues/1669) for the motivating incident and design rationale.
Expand Down
46 changes: 29 additions & 17 deletions gateway/filtered_push.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ def execute_filtered_push(
attributed_commits: list[str],
attributed_files: list[AttributedFile],
blocked_own_files: set[str],
push_fn: Callable[[], tuple[bool, str | None]],
push_fn: Callable[[str], tuple[bool, str | None]],
registry_register: Callable[..., Any],
pipeline_id: str | None = None,
repo: str | None = None,
Expand All @@ -312,10 +312,13 @@ def execute_filtered_push(
blocked_own_files: The set of paths the role cannot write; any
own-role file matching this set is stripped from each commit
it appears in.
push_fn: Callable ``push_fn() -> (ok: bool, error: str | None)``
that performs ``git push`` with whatever refspec / options
the caller wants. Invoked after HEAD has been advanced to
the rewritten tip.
push_fn: Callable ``push_fn(tip_sha) -> (ok: bool, error: str | None)``
that performs the actual ``git push``. The helper hands it
the rewritten tip SHA so the callee can build a SHA-to-refspec
push (``<tip_sha>:refs/heads/<branch>``) without needing a
local ``refs/heads/<branch>`` — see #1994 (directory-style
refs like ``refs/heads/<branch>/work`` from sibling worktrees
otherwise block creating the leaf ref locally).
registry_register: Callable ``(sha, role, pipeline_id, repo,
branch) -> bool`` that registers a rewritten own-commit
with the authorship registry. Best-effort; failures are
Expand Down Expand Up @@ -564,19 +567,15 @@ def execute_filtered_push(
rewritten_commits=rewritten_commits,
)

# update-ref so the local branch matches our rewrite.
ur = _git(exec_path, "update-ref", f"refs/heads/{branch}", final_tip)
if ur.returncode != 0:
_rollback(exec_path, original_head, branch)
return FilteredPushResult(
success=False,
error=f"update-ref refs/heads/{branch} failed: {(ur.stderr or '').strip()}",
)

# Now push. If it fails, roll HEAD + the branch ref back and
# restore the worktree so the caller sees the pre-attempt state.
# Push the rewritten tip straight to the remote. We intentionally
# do NOT ``update-ref refs/heads/<branch>`` beforehand: sibling
# worktrees may hold a directory-style ref like
# ``refs/heads/<branch>/work`` (per-role work branches from #1986)
# which blocks creating ``refs/heads/<branch>`` as a leaf ref in
# the shared ref store. Pushing by SHA sidesteps that entirely
# (see #1994).
try:
ok, push_err = push_fn()
ok, push_err = push_fn(final_tip)
except Exception as exc: # pragma: no cover - defensive
_rollback(exec_path, original_head, branch)
return FilteredPushResult(success=False, error=f"push raised: {exc}")
Expand All @@ -585,6 +584,19 @@ def execute_filtered_push(
_rollback(exec_path, original_head, branch)
return FilteredPushResult(success=False, error=push_err or "Push failed")

# Best-effort: sync the local branch ref to the pushed tip so
# ``git log <branch>`` matches origin. Allowed to fail silently
# when a directory-style ref collision prevents the write — the
# push already landed on origin, and a subsequent ``git fetch``
# reconciles local state.
ur = _git(exec_path, "update-ref", f"refs/heads/{branch}", final_tip)
if ur.returncode != 0:
logger.warning(
"filtered_push_local_ref_sync_failed",
branch=branch,
error=(ur.stderr or "").strip(),
)

# Post-success: fast-forward the worktree + index to the new tip,
# then re-stage the excluded files so the next role sees them as
# uncommitted changes (decision-6).
Expand Down
Loading
Loading