Repository navigation
Conversation
…as given (Agent-Field#1351) A codeaf do --dir worker ran `rm -rf <repo>/.git`, then `git init` and `git commit` in the parent, and the repository's history was gone. The folder guard already refused the parent half, because the parent is outside the worker's ground. It allowed the deletion, because a repository's .git sits inside the tree it belongs to, so the path law read `rm -rf .git` as ordinary work in the folder. The absolute spelling was only stopped by accident, by the critical-command floor's `rm -rf /*`. The guard now refuses any command that removes, moves, overwrites or rewrites the ground's own .git, or removes the ground itself, with the boundary's existing `is outside your copy` sentence. It asks the same hands the path law reads (the file-writing commands, in-place sed/perl and redirections), on both the written and the symlink-resolved spelling, so a link to the ground or to its .git is not a way around it. Git's own verbs are untouched: git commit, checkout and reset write inside .git on the task's behalf and are answered by the git guard as before. A plain folder with no .git is unchanged. The redirection check moves into its own function to keep outsideScopeAim under the endings ceiling. Tests: unit cases for every refused spelling (including symlinks and walking back out of a subdirectory) and for the work that must still pass, and two end-to-end tests through the real headless door with a scripted worker sending the three recorded commands in a nested, non-scratch repository. Each new test fails with the fix removed. The manual pages the issue names now quote the refusal, with a retrieval test.
AbirAbbas
left a comment
There was a problem hiding this comment.
Thanks. The core fix works. On the head, every spelling from #1351 is refused with is outside your copy: relative and absolute rm -rf .git, rm -rf ., mv .git <x> and > .git/HEAD. Links to the folder or to its .git are refused too. The focused tests (./internal/session -run 'Outside|Ground|Metadata|Endings|Ledger', ./cmd/codeaf -run GitMetadata, ./internal/manual) passed 5 runs out of 5 with no flakes. Two things need changing before this can land.
1. Two spellings still replace .git through the worker's real tool path (Agent.executeTool, repository-shaped fixture):
{"command":"rm -rf .g?t", "is_error":false, "HEAD_missing":true}
{"command":"cp -t .git payload/HEAD", "is_error":false, "HEAD_text":"broken\n"}
taskoutside.go:566-568: forwritesItsLastOperandprograms only the last operand is kept, so a destination given as-t .git/--target-directory=.gitis never compared.install -t .git payload/HEADgets through the same way. Please take the-t/--target-directoryvalue as the destination when it's present.taskoutside.go:571: operands are compared before the shell expands them, so.g?tand.[g]itdon't match. Expanding an operand with glob characters againstcwdbefore the comparison would close this. So would refusing a glob that can match.git.
Neither is new (dev allows both), but both are the #1351 defect under another name. Please add regression tests that assert a refusal and an intact .git for each spelling.
2. Ordinary upkeep is now refused, and dev allows all of these:
| command | dev | head |
|---|---|---|
mkdir -p . |
allowed | refused |
chmod -R u+w . |
allowed | refused |
rm -f .git/index.lock |
allowed | refused |
printf 'scratch/\n' >> .git/info/exclude |
allowed | refused |
cp <hook> .git/hooks/pre-commit |
allowed | refused |
The first two come from namesGroundMetadata(..., holding=true) applying to every write-family program. "A path that holds .git" only matters for programs that remove or move (rm, rmdir, mv). mkdir and chmod on the folder don't take its history. For the last three, refusing writes inside .git may be what you intend. If it is, please say so in permissions.md, since removing a stale index.lock is a common recovery step. If it isn't, limit the rule to removing or replacing .git itself and its HEAD, objects/ and refs/.
Once these are in, this needs one more hand test on a built codeaf before it can merge, plus a run on macOS, because it changes how tasks run.
What changed
A
codeaf do --dir <repo>worker ranrm -rf <repo>/.git, thengit initandgit commitin the parent, and the repository's history was gone (#1351). On currentdevthe folder guard already refused the parent half, because the parent is outside the worker's ground. It allowed the deletion: a repository's.gitsits inside the tree it belongs to, sorefusedOutsideGround(ground, "rm -rf .git")returned no refusal, which is the gap @santoshkumarradha's audit found. The absolute spellingrm -rf <repo>/.gitwas stopped only by accident, by the critical-command floor'srm -rf /*pattern, which catches every absolute recursive delete and knows nothing about.git. The relative spelling a worker standing in its folder uses got through every check.internal/session/taskoutside.go:replacedGroundMetadatarefuses any command that removes, moves, overwrites or rewrites the ground's own.git, or removes the ground itself (rm -rf .), with the boundary's existingis outside your copysentence. It asks the same hands the path law already reads: the file-writing commands, in-placesed/perl, and redirections (echo ... > .git/HEAD).namesGroundMetadatacompares both the written and the symlink-resolved spelling, so a link to the ground or to its.gitis not a way around it. I found that bypass while reviewing the first version.git commit,checkoutandresetwrite inside.giton the task's behalf and are answered by the git guard as before. Reading.git,.gitignore,.github, a nestedvendor/x/.git, and every ordinary write still pass. A ground with no.gitis unchanged.redirectionAimedOutsideto keepoutsideScopeAimunder the endings ceiling (TestNoRoadInTheTaskEngineHasMoreEndingsThanItsLedgerRow).The manual pages the issue names (
permissions.md,choosing-a-folder.md) andhow-tasks-run.mdnow explain the rule and quote the refusal.How it was checked
internal/sessiondoes not test-build on my Windows box (internal/seniordev/engine/steploopis//go:build !windows), so everything below ran in agolang:1.26Linux container, on a clean LF checkout with the patch applied.cmd/codeaf/do_git_metadata_test.go, the issue's first two acceptance bullets):doErrandon the run engine, with a scripted worker sending the three recorded commands in a nested repository placed outside the scratch directories (a parent under the temp directory would be let through by the scratch exemption and test nothing). The repo's.gitsurvives,HEADdoes not move, the parent never gets a.git, and all three commands are answered withis outside your copy. The control writesadd.pyandtest_add.pyinside the folder, gets no refusal, and leaves the parent unchanged.taskoutside_test.go): every refused spelling (absolute, relative,./.git,.git/, aftercd,cd sub && rm -rf ../.git, partial deletes,mv,cp -r,rsync --delete,sed -i, redirection,rm -rf ., and the four symlinked spellings) plus the work that must still pass, and a plain folder unchanged. The existing path-law tests are unchanged and pass.taskoutside.gomakes the e2e test fail with "the repository's .git is gone after the run". Removing only the symlink resolution makes the four symlink cases fail.cmd/codeafok;internal/manualok;make test-packed-manualok;go test -run Manual ./internal/tui3/ ./internal/session/ok.internal/sessionfails exactly two tests,TestTheDailyCeilingBindsTaskAndErrandTurnsTooandTestTheBaselineIsBoundedByTheEntryCap, and cleandevfails the same two in the same container, so this change adds none.gofmt,go vet(GOOS=linux) andgit diff --checkare clean.Not done: the issue's third e2e bullet (a stub worker through the tmux suite) and its note that
codeaf doshould exit nonzero. Here the run continues after the refusals and the scripted worker finishes its task. I left the exit code as it is, since a worker that reads a refusal and carries on is the behaviour the rest of this file is built around.Checklist
docs/changes/unreleased/2097-worker-keeps-folder-git-metadata.md, whoseinvalidatesnames the old belief that--diralone protected a repository's history.permissions.md,choosing-a-folder.mdandhow-tasks-run.mdsay what is refused and quote the sentence;TestTheGitMetadataQuestionsReachTheSectionsThatRefuseItasserts the questions reach those sections.Closes #1351.