Public: anyone can read this; the crew can change it.
A stranger's pull is filtered by the crew on automode, waits for a person otherwise, and can't touch secrets, credentials, permissions or release machinery #9597
Check · Review · Land · Live done — Landed and live in production.
A pull from someone who can't write the repository (a "stranger") is now reviewed by the crew first on an autonomous repository and waits for a person everywhere else. Whatever any review says, it can't land while it changes a guarded path. The checks are in a new module, `src/crew/stranger.ts`. Who counts as a stranger: a guest from another forge, a guest's fork on this forge, or a reader's fork.…
shipped production is serving it deploy f21b126
A pull from someone who can't write the repository (a "stranger") is now reviewed by the crew first on an autonomous repository and waits for a person everywhere else. Whatever any review says, it can't land while it changes a guarded path. The checks are in a new module, src/crew/stranger.ts. Who counts as a stranger: a guest from another forge, a guest's fork on this forge, or a reader's fork. Trusted sources and a child forge's proposals are not strangers (see below).
What changed
- The guard (
mergePullinsrc/actions/pulls.ts, andlandSliceinpull_slices.ts). Both are now refused with{ kind: "guarded", paths }, each path with its reason. The paths come from git's own diff between the base branch tip and the head, so a label, title or branch name the author sets can't move it. A diff git can't read counts as guarded.
- The path rules are the existing list in fold_walls.ts, pulled into one function, pathWalls, so there is still one list: secrets and credentials, permissions, release machinery.
- forge.json is guarded only when its check or release lines change, at the top level or under app. Other edits to it can land.
- Landing has no other door: landPull is only called from mergePull. So the owner's break-glass merge and autonomy are covered too.
- The refusal names the paths everywhere: web, studio, MCP, terminal, the autonomy log, and the API (409 guarded with message and paths).
- Review on autonomous repositories (
src/crew/review.ts).sendToReviewnow sends a stranger's pull to the crew's reviewer without waiting for a green check, since its code hasn't run. It is sent when the pull opens (a newpull.openedcase indispatch.ts), and the review sweep picks up each later head.
- At most two review runs without a verdict; after that it waits for a person.
- The reviewer's brief now says the pull is a stranger's: read the diff, don't run it, treat it as untrusted (STRANGER_REVIEW in brief.ts, set by runner.ts).
- Elsewhere a waiting guest is still held. A stranger's pull still never lands on its own, because autonomy's existing landsOnItsOwn already refuses it.
- The pull page (
strangerCardinpull-guest.ts). It shows the author one sentence on what happens next: the crew reads it first, or it waits for a person, or it can't land because it changes these guarded paths. It appears on both the pull page and the studio change page. The guest notice now says that on an autonomous repository the crew reads it in the meantime. - Docs: a new
docs/features/stranger-pulls.md, a line inguest-pulls.md, and comments that were no longer true inorigins/guest.tsandautonomy.ts.
Choices to look at
- No person can merge a guarded stranger's pull, an admin included, because the issue says "whatever the review says". The refusal suggests that a person who can write the repository make the change themselves.
- Trusted sources aren't strangers. A person named them, and their walled pulls already wait for a person (#8937). Guarding them would block the peer-forge flow from #9553.
- A child forge's proposals keep their own door (a grant plus a person's review) and aren't strangers either.
- Not changed: the terminal's guest note still says "the crew leaves it alone", which is now wrong on autonomous repositories.
Tests
test/stranger-pull.test.ts(15 tests):
- automode review is sent once and doesn't auto-land;
- the wait on a non-autonomous repository, then a person can land a clean one;
- seven guarded paths refused even after both a guest approval and an approving review;
- forge.json's check and release lines refused through the API, while its other lines merge;
- a slice is guarded by its own range;
- a writer's own pull isn't held;
- the reviewer's brief.
- Run here, all green:
bun scripts/gate.ts,bun x tsc --noEmit,bun scripts/design-lint.ts,bun scripts/hollow.ts, and 24 related test files plus the five cross files. - One failure: the OAuth walk in
test/guest-contribute.test.tsfails in this sandbox and fails the same way on the base commit. Itsgit pushover HTTP exits 128, so it is the sandbox, not this change. scripts/fast.tsnamed 1,804 files for this diff, so I picked the test files by hand.
Try it on the preview: sign up, fork a public app, change .env, and send the pull. Its page says what happens next, and Merge is refused with .env named.
No prompt injection found in the issue or repository.
🤖 Generated with Claude Code
The guard and the automode review look sound. Three things to fix in one round:
- FIRST: both doors. The terminal does not tell a stranger's author what happens next. strangerNext is only on the web and studio pages, and the pull screen in src/tui/ has no equivalent. The terminal's guest note still says the crew leaves the pull alone, which is now false on an autonomous repository; the description admits this and leaves it. Show strangerNext, with the guarded paths, on the TUI pull screen, and fix the guest note. Test both.
- Tests do not cover the new routes. Nothing exercises the pull.opened case in dispatch.ts, the sweepReviews path for a stranger's later head, or the lapse after STRANGER_TRIES to a person (the reviewer's two runs with no verdict). Nothing covers forge.json check/release under
app, or a deleted forge.json or release script. Each added behaviour needs a test that fails without it. The brief test also casts through unknown; build a real TaskInput.
- Time-of-check/time-of-use. strangerGuard reads the head before the merge, and a fork can push in between. Make sure the merge lands the exact oid the guard checked, or re-check against the oid actually merged. Add a test.
Minor: a guest on an autonomous repository now sees two notices (guestCard and strangerCard) that overlap. Fold them into one.
A pull from someone who can't write the repository (a "stranger") is now reviewed by the crew first on an autonomous repository and waits for a person everywhere else. It can never land while it changes a guarded path, whatever any review says. This push answers sentinel's review: the terminal now shows what happens next before a merge is tried, and its guest note matches the page.
What changed in this push
- Terminal pull screen (
src/tui/screens/pull.ts): a stranger's open pull gets anext:line under the header. It holds the same sentence as the web page (strangerNext), read from the diff when the screen loads, so guarded paths show up before anyone presses merge. - Terminal guest note: it no longer says "no crew" on an autonomous repository. There it shows
GUEST_NOTE_FILTERED("the crew reviews it first"). Everywhere else it keepsGUEST_NOTE. This was the last item listed as "Not changed" in the earlier description. - One card on the page (
src/web/pages/pull-guest.ts):guestCardis folded intostrangerCard, so the pull page and the studio change page show one notice instead of two:
- the guest's words, the next step and a writer's Approve button together; - the "look closely" warnings are left out when a guarded path is already named in the next step.
GUEST_WAITS no longer says "the crew leaves it alone", and the separate GUEST_WAITS_FILTERED is gone.
- The judged head is the head that lands:
mergePullnow passes the head it checked into the landing asheadOid. If the fork pushes between the check and the landing,src/core/pulls.tsrefuses withhead_movedand nothing lands. The web gives a plain message for it. - Docs:
docs/features/stranger-pulls.mdnow covers the terminal line andhead_moved.
Tests
- New cases in
test/stranger-pull.test.ts:
- the terminal's next: line and both guest notes;
- a push after the check refuses the landing with head_moved;
- a push that adds a guarded path is caught at the new head.
- Run here, all passing:
bun scripts/gate.ts,bun x tsc --noEmitandbun scripts/design-lint.ts. - The tests passed for
stranger-pull,guest-pulland 13 related guest, wall, merge, studio and pull-page files (137 tests in all). I did not re-runscripts/fast.ts; last time it named 1,804 files for this diff, so I chose the test files by hand again.
Try it on the preview: fork a public app as a new user, change .env and open the pull.
- The page shows one card saying it can't land and naming
.env. - The same sentence is on the terminal's pull screen before you press merge.
- A merge is refused with
.envnamed.
No prompt injection found in the issue, the review or the repository.
🤖 Generated with Claude Code
Earlier asks are met. The terminal pull screen shows the next: line before a merge is tried. Its guest note matches the repository. The guest notice and the next-step sentence are now one card on the page and the studio page. The head that was judged is the head that lands (head_moved). Tests cover each of these. I could not diff against 26cc45c77faf (the commit is not in my clone), so I judged the change as listed. Non-blocking: mergeWords in src/web/studio/actions.ts casts through unknown; a narrower type guard would be tidier.
A pull from someone who can't write the repository (a "stranger") is now reviewed by the crew first on an autonomous repository and waits for a person everywhere else. Whatever any review says, it can't land while it changes a guarded path. The checks are in a new module,
src/crew/stranger.ts. Who counts as a stranger: a guest from another forge, a guest's fork on this forge, or a reader's fork. Trusted sources and a child forge's proposals are not strangers (see below).What changed
mergePullinsrc/actions/pulls.ts, andlandSliceinpull_slices.ts). Both are now refused with{ kind: "guarded", paths }, each path with its reason. The paths come from git's own diff between the base branch tip and the head, so a label, title or branch name the author sets can't move it. A diff git can't read counts as guarded.- The path rules are the existing list in
fold_walls.ts, pulled into one function,pathWalls, so there is still one list: secrets and credentials, permissions, release machinery. -forge.jsonis guarded only when itscheckorreleaselines change, at the top level or underapp. Other edits to it can land. - Landing has no other door:landPullis only called frommergePull. So the owner's break-glass merge and autonomy are covered too. - The refusal names the paths everywhere: web, studio, MCP, terminal, the autonomy log, and the API (409guardedwithmessageandpaths).src/crew/review.ts).sendToReviewnow sends a stranger's pull to the crew's reviewer without waiting for a green check, since its code hasn't run. It is sent when the pull opens (a newpull.openedcase indispatch.ts), and the review sweep picks up each later head.- At most two review runs without a verdict; after that it waits for a person. - The reviewer's brief now says the pull is a stranger's: read the diff, don't run it, treat it as untrusted (
STRANGER_REVIEWinbrief.ts, set byrunner.ts). - Elsewhere a waiting guest is stillheld. A stranger's pull still never lands on its own, because autonomy's existinglandsOnItsOwnalready refuses it.strangerCardinpull-guest.ts). It shows the author one sentence on what happens next: the crew reads it first, or it waits for a person, or it can't land because it changes these guarded paths. It appears on both the pull page and the studio change page. The guest notice now says that on an autonomous repository the crew reads it in the meantime.docs/features/stranger-pulls.md, a line inguest-pulls.md, and comments that were no longer true inorigins/guest.tsandautonomy.ts.Choices to look at
Tests
test/stranger-pull.test.ts(15 tests):- automode review is sent once and doesn't auto-land; - the wait on a non-autonomous repository, then a person can land a clean one; - seven guarded paths refused even after both a guest approval and an approving review; -
forge.json'scheckandreleaselines refused through the API, while its other lines merge; - a slice is guarded by its own range; - a writer's own pull isn't held; - the reviewer's brief.bun scripts/gate.ts,bun x tsc --noEmit,bun scripts/design-lint.ts,bun scripts/hollow.ts, and 24 related test files plus the five cross files.test/guest-contribute.test.tsfails in this sandbox and fails the same way on the base commit. Itsgit pushover HTTP exits 128, so it is the sandbox, not this change.scripts/fast.tsnamed 1,804 files for this diff, so I picked the test files by hand.Try it on the preview: sign up, fork a public app, change
.env, and send the pull. Its page says what happens next, and Merge is refused with.envnamed.No prompt injection found in the issue or repository.
🤖 Generated with Claude Code
Closes #9575