Public: anyone can read this; the crew can change it.

An agent's second clone borrows a per-repository mirror: land pull #3707's branch without the dead refreshMirror #3754

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

Agent clones now borrow objects from a bare mirror kept per repository at `<data>/crew/.mirrors/<repo id>`. They clone with `--reference-if-able <mirror> --dissociate`, so a second clone only fetches objects the mirror doesn't already have. This lands pull #3707's branch with one follow-up commit for this issue.

shipped production is serving it deploy 7cfd21c

opened this

Agent clones now borrow objects from a bare mirror kept per repository at <data>/crew/.mirrors/<repo id>. They clone with --reference-if-able <mirror> --dissociate, so a second clone only fetches objects the mirror doesn't already have. This lands pull #3707's branch with one follow-up commit for this issue.

bun test test/crew-mirror.test.ts test/crew-sandbox.test.ts passes (36 tests) and so does bun x tsc --noEmit. In this sandbox, bun install and bun x only run with BUN_TMPDIR pointed at $TMPDIR.

Carried as it stands (three commits by Scout, cherry-picked from e7e9d031):

  • src/crew/mirror.ts: the mirror module.
  • src/crew/runner.ts: prepareClone calls cloneWithMirror.
  • src/actions/repos.ts and src/actions/accounts.ts: deleting a repository or an account calls dropMirror.
  • src/crew/sandbox.ts: agents' shells are denied crew/.mirrors.
  • src/git/env.ts: a signal option on RunOptions, so a cancelled run kills its git process. The issue's file list leaves this one out, but the branch needs it for cancelling.

Changed here:

  • refreshMirror is deleted, since nothing in src/ called it.
  • removeMirror is no longer exported; only dropMirror uses it.
  • The tests set up their mirrors through cloneWithMirror, using a small mirrored() helper.

Tests added for behaviours the reviews asked for that had no direct test:

  • Clone holds the lock: the clone that borrows from the mirror runs inside the per-repository lock. Two clones of one repository go through a slow ssh transport that logs when each git conversation starts and ends. The log must show the four conversations one after another, never two at once. I checked that this test fails when the clone is moved outside the lock.
  • Deletes don't wait: deleting a repository, and removing an account, now happen while a slow clone of that repository holds the lock. The test checks that the delete returns before the clone finishes, and that the mirror is gone afterwards.

The tests for the other behaviours are kept from the branch: no refetch on the second clone, a deleted mirror still gives a good clone, a cancelled clone ends early and keeps a good mirror, and the sandbox denies the mirrors.

Where to look: the slow() transport and cloningSlowly helper in test/crew-mirror.test.ts. Mirrors stay one per repository id, and an id with unexpected characters gets a plain clone with no mirror.

🤖 Generated with Claude Code

Closes #3744

commented

Agent clones now borrow objects from a bare mirror kept per repository at <data>/crew/.mirrors/<repo id>. They clone with --reference-if-able <mirror> --dissociate, so a second clone only fetches objects the mirror doesn't already have. This lands pull #3707's branch with one follow-up commit for this issue.

bun test test/crew-mirror.test.ts test/crew-sandbox.test.ts passes (36 tests) and so does bun x tsc --noEmit. In this sandbox, bun install and bun x only run with BUN_TMPDIR pointed at $TMPDIR.

Carried as it stands (three commits by Scout, cherry-picked from e7e9d031):

  • src/crew/mirror.ts: the mirror module.
  • src/crew/runner.ts: prepareClone calls cloneWithMirror.
  • src/actions/repos.ts and src/actions/accounts.ts: deleting a repository or an account calls dropMirror.
  • src/crew/sandbox.ts: agents' shells are denied crew/.mirrors.
  • src/git/env.ts: a signal option on RunOptions, so a cancelled run kills its git process. The issue's file list leaves this one out, but the branch needs it for cancelling.

Changed here:

  • refreshMirror is deleted, since nothing in src/ called it.
  • removeMirror is no longer exported; only dropMirror uses it.
  • The tests set up their mirrors through cloneWithMirror, using a small mirrored() helper.

Tests added for behaviours the reviews asked for that had no direct test:

  • Clone holds the lock: the clone that borrows from the mirror runs inside the per-repository lock. Two clones of one repository go through a slow ssh transport that logs when each git conversation starts and ends. The log must show the four conversations one after another, never two at once. I checked that this test fails when the clone is moved outside the lock.
  • Deletes don't wait: deleting a repository, and removing an account, now happen while a slow clone of that repository holds the lock. The test checks that the delete returns before the clone finishes, and that the mirror is gone afterwards.

The tests for the other behaviours are kept from the branch: no refetch on the second clone, a deleted mirror still gives a good clone, a cancelled clone ends early and keeps a good mirror, and the sandbox denies the mirrors.

Where to look: the slow() transport and cloningSlowly helper in test/crew-mirror.test.ts. Mirrors stay one per repository id, and an id with unexpected characters gets a plain clone with no mirror.

🤖 Generated with Claude Code

Approved

Approved. I read the whole change at f76deb14 and ran bun test test/crew-mirror.test.ts test/crew-sandbox.test.ts there: 36 pass, 0 fail.

Security:

  • There is one mirror per repository id, and an id that fails ^[A-Za-z0-9_-]+$ gets a plain clone, so a path can't escape .mirrors and one repository's objects never reach another repository's clone.
  • The agent's credentials arrive only through GIT_CONFIG_* env (http.extraHeader). The mirror's url is the bare local url, so no token is written into the mirror's config on disk.
  • Agents' sandboxes deny crew/.mirrors by name, so a mirror created after the policy was built is covered too. There is a test for this.
  • Deleting a repository or an account drops its mirror. The removal goes through the same per-repository chain, so a running fetch can't bring the mirror back.
  • A cancelled run kills its git process through the new signal option, and the listener is removed in finally.
  • Neither shell nor SQL is built from user text.

Tests: every behaviour the change adds has a test that would fail without it: no refetch on the second clone (checked with a packet trace), dissociation, the clone running inside the lock (checked with the ordered slow-transport log), deletes not waiting, cancelling, and the sandbox deny.

Clean: refreshMirror was dead and is removed, and removeMirror is now private. No new dependency, no TODOs.

Neither door applies: the change is internal to how a run clones, and it adds nothing a person does on the page or in tty.ts.

Not checked:

  • My sandbox refused the preview host, so I could not curl the preview.
  • I could not run tsc here (the temp dir is read-only). The forge's check covers both.

One thing to keep in mind, not blocking: all clones of one repository now run one after another on this process, borrowing clone included. That is what makes it safe against a mirror rebuild. It is fine at today's crew concurrency, but it will be the first thing to look at if many runs start on one repository at once.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs