fix(vm-agent): block auto-commit pushes to default branch#1672
Open
simple-agent-manager[bot] wants to merge 3 commits into
Open
fix(vm-agent): block auto-commit pushes to default branch#1672simple-agent-manager[bot] wants to merge 3 commits into
simple-agent-manager[bot] wants to merge 3 commits into
Conversation
When a task completes, makeTaskCompletionCallback calls gitPushWorkspaceChanges which runs `git push --set-upstream origin HEAD`. Since workspaces always check out the project's default branch (e.g. "main"), if the agent never switches to the task output branch, the push lands directly on main/master — potentially triggering production deploys with unreviewed changes. Add a guard in gitPushWorkspaceChanges that compares HEAD against the workspace's checkout branch (WorkspaceRuntime.Branch). When they match, the push is blocked with actionable diagnostics explaining that the agent should have switched to the output branch. Local commits are preserved. When the workspace is not found (empty checkout branch), the guard is skipped to avoid false positives. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…fail-open warning - Extract shouldBlockDefaultBranchPush() as a pure, testable predicate so tests exercise the real production code, not a re-derived copy - Add slog.Warn when checkout branch cannot be determined (fail-open path is now observable in production logs) - Document scope limitation: guard covers SAM's auto-commit path only, not agent-initiated git pushes - Rewrite tests to call the real predicate and integration-test the full Server → workspaceCheckoutBranch → shouldBlockDefaultBranchPush chain Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Contributor
Address false-positive blocking identified by go-specialist review: when a workspace is checked out directly on the task output branch (dispatch/retry scenarios), the guard was comparing HEAD against Branch (the checkout branch), which would incorrectly block legitimate pushes. - Add DefaultBranch field to WorkspaceRuntime, createWorkspaceRequest, and workspaceRuntimeOpts structs - Rename workspaceCheckoutBranch → workspaceDefaultBranch with DefaultBranch-first, Branch-fallback resolution - Thread defaultBranch from task-runner workspace-steps through node-agent createWorkspaceOnNode to the VM agent - Add TestGitPushGuardWithDefaultBranch covering dispatch/retry scenario (Branch≠DefaultBranch), legacy fallback, and same-default - Update TestWorkspaceDefaultBranch for new field precedence Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Summary
DefaultBranchfield threaded from control plane → VM agent to correctly distinguish the project default branch from the workspace checkout branchProblem
When a SAM task completes, the VM agent auto-commits uncommitted changes and pushes via
gitPushWorkspaceChanges. Workspaces check out the project'sdefaultBranch(e.g. "main"), and the agent (Claude Code/Codex) is expected to switch to the task output branch during work. If the agent never switches branches,git push --set-upstream origin HEADpushes directly to main/master — contaminating the default branch with task-specific changes.Solution
1. Push guard in
gitPushWorkspaceChanges(server.go)After auto-committing but before pushing, the guard compares HEAD against the project's default branch. If HEAD is still on the default branch, the push is blocked with a descriptive error. Changes are committed locally (SHA preserved in the error message) but not pushed.
2.
DefaultBranchfield (go-specialist finding fix)The initial implementation compared HEAD against
Branch(the checkout branch). A go-specialist review identified a CRITICAL false-positive: in dispatch/retry scenarios, the workspace is checked out directly on the task output branch, soBranch == output_branch. Comparing HEAD against Branch would incorrectly block legitimate pushes.Fix: Added a separate
DefaultBranchfield that carries the project's actual default branch. The guard compares againstDefaultBranch(falling back toBranchfor backwards compatibility with older control planes).3. Control plane threading
defaultBranchis sent from the task runner (workspace-steps.ts) throughcreateWorkspaceOnNode(node-agent.ts) to the VM agent'screateWorkspaceRequest.Files Changed
packages/vm-agent/internal/server/server.goDefaultBranchfield onWorkspaceRuntime;workspaceDefaultBranch()helper with fallback;shouldBlockDefaultBranchPush()predicate; guard ingitPushWorkspaceChangespackages/vm-agent/internal/server/workspaces.goDefaultBranchoncreateWorkspaceRequest; wired throughcreateWorkspaceRuntimeOptionspackages/vm-agent/internal/server/workspace_routing.goDefaultBranchonworkspaceRuntimeOpts; propagated inupsertWorkspaceRuntimepackages/vm-agent/internal/server/git_push_guard_test.goworkspaceDefaultBranch,shouldBlockDefaultBranchPush, integration tests, and dispatch/retry scenarios withDefaultBranch≠Branchapps/api/src/services/node-agent.tsdefaultBranchparameter oncreateWorkspaceOnNodeapps/api/src/durable-objects/task-runner/workspace-steps.tsdefaultBranchfrom task config to workspace creationTest plan
shouldBlockDefaultBranchPushpredicate: main, master, develop, production, output branch, feature branch, emptyworkspaceDefaultBranchhelper: DefaultBranch precedence, Branch fallback, empty, unknown workspaceBranch="sam/task-123",DefaultBranch="main"— HEAD on output branch NOT blocked, HEAD on main IS blockedAgent Preflight (Required)
Classification
External References
N/A: Internal bug fix based on observed production behavior (task auto-commits pushing to main). No external API changes.
Codebase Impact Analysis
packages/vm-agent/internal/server/—WorkspaceRuntimestruct,gitPushWorkspaceChanges, workspace creation/routingapps/api/src/services/node-agent.ts—createWorkspaceOnNodeparameter additionapps/api/src/durable-objects/task-runner/workspace-steps.ts— ThreadingdefaultBranchfrom task configDocumentation & Specs
N/A: Internal bug fix. No user-facing API surface change (the
defaultBranchfield is an internal wire format addition).Constitution & Risk Check
Branchfallback for backwards compatibility.DefaultBranchandBranchare both empty (unknown workspace), the guard is skipped with a warning log rather than blocking. This preserves existing behavior for edge cases.Specialist Review Evidence
Staging Verification (REQUIRED for all code changes — merge-blocking)
Staging Verification Evidence
This PR is marked DO NOT MERGE per task constraints. Staging verification will be performed when the PR is approved for merge by a human reviewer.
End-to-End Verification (Required for multi-component changes)
Data flow trace:
workspace-steps.ts:createWorkspace()passesdefaultBranch: state.config.defaultBranch || 'main'node-agent.ts:createWorkspaceOnNode()includesdefaultBranchin HTTP bodyworkspaces.go:handleCreateWorkspace()parsesdefaultBranchfrom requestWorkspaceRuntime.DefaultBranchviaworkspace_routing.go:upsertWorkspaceRuntime()server.go:gitPushWorkspaceChanges()callsworkspaceDefaultBranch()(prefersDefaultBranch, falls back toBranch)shouldBlockDefaultBranchPush()compares resolved default branch against HEAD🤖 Generated with Claude Code