diff --git a/internal/cli/run.go b/internal/cli/run.go index f5ed6ffa8..4749e3b1d 100644 --- a/internal/cli/run.go +++ b/internal/cli/run.go @@ -41,6 +41,14 @@ const ( maxContextScanDepth = 5 ) +// agentWorkingDirExcludes lists directory patterns that agents may create +// during execution but must never commit. These are added to +// .git/info/exclude before the agent runs so git ignores them entirely. +var agentWorkingDirExcludes = []string{ + ".agentready/", + ".fullsend-workspace/", +} + func newRunCmd() *cobra.Command { var fullsendDir string var outputBase string @@ -375,6 +383,14 @@ func runAgent(agentName, fullsendDir, outputBase, targetRepo, fullsendBinary str } } + // 8a-2. Exclude agent working directories from git tracking. + // Agents may create working directories (e.g. .agentready/) during + // execution. These must never appear in commits. Adding them to + // .git/info/exclude ensures git status/add ignores them entirely. + if err := excludeAgentWorkingDirs(sandboxName, repoDir, printer); err != nil { + printer.StepWarn("Could not exclude agent working dirs: " + err.Error()) + } + // 8b. Copy agent-input files (if configured). if h.AgentInput != "" { inputStart := time.Now() @@ -1331,6 +1347,25 @@ func relOrAbs(base, path string) string { return rel } +// excludeAgentWorkingDirs adds agent working directory patterns to +// .git/info/exclude so they are invisible to git status and git add. +func excludeAgentWorkingDirs(sandboxName, repoDir string, printer *ui.Printer) error { + var lines []string + for _, pattern := range agentWorkingDirExcludes { + lines = append(lines, pattern) + } + if len(lines) == 0 { + return nil + } + payload := strings.Join(lines, "\n") + excludeCmd := fmt.Sprintf("printf '%%s\\n' '%s' >> %s/.git/info/exclude", + payload, repoDir) + if _, _, _, err := sandbox.Exec(sandboxName, excludeCmd, 5*time.Second); err != nil { + return fmt.Errorf("writing git exclude: %w", err) + } + return nil +} + // hasAgentsMD checks whether the repo directory contains an AGENTS.md file // in any common casing. func hasAgentsMD(repoDir string) bool { diff --git a/internal/cli/run_test.go b/internal/cli/run_test.go index 51ac1ce6b..b0b5e6726 100644 --- a/internal/cli/run_test.go +++ b/internal/cli/run_test.go @@ -818,6 +818,26 @@ func TestResolveLinuxBinary_Download(t *testing.T) { assert.NoError(t, validateLinuxBinary(binPath), "downloaded binary should be a valid Linux/amd64 ELF") } +func TestAgentWorkingDirExcludes_ContainsKnownPatterns(t *testing.T) { + // Verify the exclusion list contains the known agent working directories. + expected := []string{".agentready/", ".fullsend-workspace/"} + for _, pattern := range expected { + found := false + for _, exclude := range agentWorkingDirExcludes { + if exclude == pattern { + found = true + break + } + } + assert.True(t, found, "agentWorkingDirExcludes should contain %q", pattern) + } +} + +func TestAgentWorkingDirExcludes_NotEmpty(t *testing.T) { + assert.NotEmpty(t, agentWorkingDirExcludes, + "agentWorkingDirExcludes must not be empty — agents create working dirs that need exclusion") +} + func TestReadOIDCAuthFile_Success(t *testing.T) { f := filepath.Join(t.TempDir(), "auth") require.NoError(t, os.WriteFile(f, []byte("bearer test-token"), 0o600)) diff --git a/internal/scaffold/fullsend-repo/scripts/post-code-test.sh b/internal/scaffold/fullsend-repo/scripts/post-code-test.sh index f665e0446..3813c81a1 100644 --- a/internal/scaffold/fullsend-repo/scripts/post-code-test.sh +++ b/internal/scaffold/fullsend-repo/scripts/post-code-test.sh @@ -518,6 +518,85 @@ run_error_comment_test "error-comment-has-warning-emoji" \ "1" "my-org/my-repo" "12345" \ "⚠️" "yes" +# --------------------------------------------------------------------------- +# Test helper — reimplements the agent artifact stripping logic from +# post-code.sh section 2b. Given a list of changed files, returns which +# files would be stripped as agent artifacts. +# --------------------------------------------------------------------------- +strip_agent_artifacts() { + local changed_files="$1" + local agent_artifact_patterns=".agentready/ .fullsend-workspace/" + local stripped="" + + for file in ${changed_files}; do + local is_artifact=false + for pattern in ${agent_artifact_patterns}; do + local dir="${pattern%/}" + case "${file}" in + "${dir}"/*|"${dir}") is_artifact=true; break ;; + */"${dir}"/*|*/"${dir}") is_artifact=true; break ;; + esac + done + if [ "${is_artifact}" = "true" ]; then + stripped="${stripped} ${file}" + fi + done + + echo "${stripped}" | xargs +} + +run_artifact_test() { + local test_name="$1" + local changed_files="$2" + local expected_stripped="$3" + + local actual + actual="$(strip_agent_artifacts "${changed_files}")" + + if [ "${actual}" != "${expected_stripped}" ]; then + echo "FAIL: ${test_name}" + echo " changed_files: '${changed_files}'" + echo " expected stripped: '${expected_stripped}'" + echo " actual stripped: '${actual}'" + FAILURES=$((FAILURES + 1)) + return + fi + + echo "PASS: ${test_name}" +} + +# --- Agent artifact stripping test cases --- + +# .agentready/ files should be stripped +run_artifact_test "strip-agentready-file" \ + ".agentready/assessment.json src/main.go" \ + ".agentready/assessment.json" + +# .fullsend-workspace/ files should be stripped +run_artifact_test "strip-fullsend-workspace-file" \ + ".fullsend-workspace/scratch.txt src/main.go" \ + ".fullsend-workspace/scratch.txt" + +# Nested paths should also be stripped +run_artifact_test "strip-nested-agentready" \ + "subdir/.agentready/data.json src/main.go" \ + "subdir/.agentready/data.json" + +# Normal files should not be stripped +run_artifact_test "keep-normal-files" \ + "src/main.go internal/handler.go" \ + "" + +# Multiple artifacts stripped together +run_artifact_test "strip-multiple-artifacts" \ + ".agentready/a.json .fullsend-workspace/b.txt src/main.go" \ + ".agentready/a.json .fullsend-workspace/b.txt" + +# Empty input should produce no stripping +run_artifact_test "strip-empty-input" \ + "" \ + "" + # --- Summary --- echo "" diff --git a/internal/scaffold/fullsend-repo/scripts/post-code.sh b/internal/scaffold/fullsend-repo/scripts/post-code.sh index 62b711048..91da26cf0 100755 --- a/internal/scaffold/fullsend-repo/scripts/post-code.sh +++ b/internal/scaffold/fullsend-repo/scripts/post-code.sh @@ -126,6 +126,34 @@ fi echo "Changed files:" echo "${CHANGED_FILES}" | sed 's/^/ /' +# --------------------------------------------------------------------------- +# 2b. Strip agent working directories (defense-in-depth) +# +# Agent working dirs (.agentready/, .fullsend-workspace/) should never +# appear in commits. The harness excludes them via .git/info/exclude, but +# if an agent manages to stage them anyway, strip them here before push. +# --------------------------------------------------------------------------- +AGENT_ARTIFACT_PATTERNS=".agentready/ .fullsend-workspace/" +STRIPPED_FILES="" +for file in ${CHANGED_FILES}; do + is_artifact=false + for pattern in ${AGENT_ARTIFACT_PATTERNS}; do + dir="${pattern%/}" # strip trailing slash for prefix matching + case "${file}" in + "${dir}"/*|"${dir}") is_artifact=true; break ;; + */"${dir}"/*|*/"${dir}") is_artifact=true; break ;; + esac + done + if [ "${is_artifact}" = "true" ]; then + echo "::warning::Stripping agent artifact from commit: ${file}" + STRIPPED_FILES="${STRIPPED_FILES} ${file}" + fi +done + +if [ -n "${STRIPPED_FILES}" ]; then + echo "::warning::Agent committed working directory artifacts — stripping before push" +fi + # --------------------------------------------------------------------------- # 3. Authoritative secret scan # ---------------------------------------------------------------------------