Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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