Files
boshu2__agentops/cli/cmd/ao/git_read.go
Bo 5d61bf8a3f fix: kill the shared-config gremlin class (hook-safe repo-root, git-env scrubbing, config-hygiene gate) (#922)
Fixes the three gremlins observed 2026-07-18 (bead
age-gate-scripts-worktree-gitdir-p62wo):

1. **Gate scripts broke under hook env from linked worktrees** (#919
regression): new scripts/lib/repo-root.sh (GIT_*-scrubbed resolution,
BASH_SOURCE anchor, worktree-correct) swept across 32 scripts. Proof:
this PR's own push ran the pre-push gates from a linked worktree and
passed.
2. **core.bare=true recurrence**: mechanism proven (leaked GIT_DIR +
no-arg git init --bare rewrites the SHARED config); writer traced with
high confidence to a live Gas City city process (concrete suspect:
gascity cmd_rig_test.go runGitInTest, unscrubbed env + no-arg init
--bare — fork-side fix filed separately). Defense: new
always.git-config-hygiene gate fails fast on core.bare/test identities
with the repair command inline (--self-test proves fail-closed).
3. **Test/test@test.com identity leak**: writer convicted — our own
pre-push chain leaked GIT_DIR into go test, whose helpers wrote the
shared config via git -C (mechanism proven). Cut at both ends:
validate-go-fast scrubs env for children; every test git-helper set-site
scrubs; check-test-isolation gains a WARN-ratchet rule (baseline 3 →
FAIL at 0).

Gates: shellcheck rc=0 across 36 scripts; go build/vet clean;
655+379+149 tests; hygiene self-test; both originally-failing validators
pass under the repro env.
2026-07-18 18:20:16 -04:00

88 lines
2.6 KiB
Go

package main
import (
"context"
"fmt"
"os"
"os/exec"
"strings"
"time"
)
// gitDiscoveryEnv strips GIT_DIR/GIT_WORK_TREE/GIT_COMMON_DIR/GIT_INDEX_FILE
// from the environment so git discovery resolves the repository from the
// working directory, never from a leaked parent-process override. A hook-leaked
// GIT_DIR is not just a read hazard: a "scoped" `git -C <dir> config ...` under
// it writes the LEAKED repo's shared config (age-gate-scripts-worktree-gitdir-p62wo).
func gitDiscoveryEnv() []string {
env := make([]string, 0, len(os.Environ()))
for _, entry := range os.Environ() {
switch {
case strings.HasPrefix(entry, "GIT_DIR="):
continue
case strings.HasPrefix(entry, "GIT_WORK_TREE="):
continue
case strings.HasPrefix(entry, "GIT_COMMON_DIR="):
continue
case strings.HasPrefix(entry, "GIT_INDEX_FILE="):
continue
default:
env = append(env, entry)
}
}
return env
}
// gitChangedFiles lists worktree-modified paths (read-only, bounded) for
// handoff evidence. Returns nil when git is unavailable or the tree is clean.
func gitChangedFiles(cwd string, limit int) []string {
ctx, cancel := context.WithTimeout(context.Background(), 1200*time.Millisecond)
defer cancel()
command := exec.CommandContext(ctx, "git", "diff", "--name-only", "HEAD")
command.Dir = cwd
command.Env = gitDiscoveryEnv()
out, err := command.Output()
if err != nil || strings.TrimSpace(string(out)) == "" {
return nil
}
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
if limit > 0 && len(lines) > limit {
lines = lines[:limit]
}
result := make([]string, 0, len(lines))
for _, line := range lines {
if line = strings.TrimSpace(line); line != "" {
result = append(result, line)
}
}
return result
}
// resolveRepoRoot is read-only discovery. AgentOps does not mutate Git state.
func resolveRepoRoot(cwd string) (string, error) {
command := exec.Command("git", "rev-parse", "--show-toplevel")
command.Dir = cwd
command.Env = gitDiscoveryEnv()
output, err := command.Output()
if err != nil {
return "", fmt.Errorf("resolve git repo root: %w", err)
}
return strings.TrimSpace(string(output)), nil
}
// getCurrentBranch is optional read-only metadata for handoff artifacts.
func getCurrentBranch(cwd string) (string, error) {
command := exec.Command("git", "branch", "--show-current")
command.Dir = cwd
command.Env = gitDiscoveryEnv()
output, err := command.Output()
if err != nil {
return "", fmt.Errorf("read current branch: %w", err)
}
branch := strings.TrimSpace(string(output))
if branch == "" {
return "", fmt.Errorf("detached HEAD")
}
return branch, nil
}