Skip to content

Promote develop to main: Keep the Scope Probe Out of an Inherited Environment - #1584

Merged
ptr727 merged 1 commit into
mainfrom
develop
Sep 13, 2026
Merged

Promote develop to main: Keep the Scope Probe Out of an Inherited Environment#1584
ptr727 merged 1 commit into
mainfrom
develop

Conversation

@ptr727

@ptr727 ptr727 commented Sep 13, 2026

Copy link
Copy Markdown
Owner

Promotes one commit from develop to main.

origin_owner() resolves the owner a write may target, and in_scope() refuses a write to any other owner. Two holes, both reached through the invoking environment rather than through anything in the tree.

The probe inherited its environment unchanged, and git honours GIT_DIR over the -C argument for repository discovery, so an inherited one made it answer for whatever repository that variable named. GIT_COMMON_DIR does the same, the config being read from the common dir. The admitting direction is the one worth fixing: either variable naming a repository under the target's owner while the script itself sits in a checkout under a different one, so in_scope() returns true on a comparison that never described the script's checkout. Separately, wait requested a Copilot review before its first poll, and that request is a requestReviews mutation that never passed through the owner check at all, so a cross-owner wait issued a state-changing call under the logged-in identity while the refusal that stops the same target on reply never fired.

The probe now runs without the four discovery names and without any name starting with GIT_CONFIG, the second as a prefix test rather than a list, since GIT_CONFIG_KEY_n has no fixed upper bound and no enumeration of literal names can be complete. wait refuses a cross-owner target outright, exit 64, ahead of its first read rather than at the request, so all three write paths state one rule.

What the strip does not close is stated in the code rather than left to inference: an inherited HOME, XDG_CONFIG_HOME or PATH delivers the same rewrite, and a caller controlling any of those already controls the process that would perform the write.

An intermediate design probing with git config --local --get remote.origin.url was reverted, because it inverted the boundary on an origin carrying more than one url, where remote get-url takes the first and config --get the last, and because --local follows no include.path. Two cases pin those shapes against a return to that command.

Copilot reviewed the feature pull request at its merged head, full coverage, approval recommended, no findings and none suppressed. CodeRabbit reviewed the round before it and raised one finding, fixed and resolved on the feature pull request before it merged, then reported itself rate limited on the final head.

Closes #1561
Closes #1562

…Through the Owner Check (#1583)

Closes #1561. Closes #1562.

## The defects

`origin_owner()` resolves the owner a write may target by running `git`
against the script's own directory, and `in_scope()` compares the target
against it. Two holes, both reached through the invoking environment
rather than through anything in the tree.

**The probe answered for the wrong repository (#1561).** The call
inherited the environment unchanged, and git honours `GIT_DIR` over the
`-C` argument for repository discovery, so an inherited one made the
probe answer for whatever repository it named. `GIT_COMMON_DIR` does the
same, since the config is read from the common dir. Both directions are
reachable, and the one worth fixing is the admitting one: `GIT_DIR`
naming a repository under the target's owner while the script itself
sits in a checkout under a different one, so `in_scope()` returns true
on a comparison that never described the script's checkout.

**One write path never reached the check (#1562).** `wait` requests a
Copilot review before its first poll, and that request is a
`requestReviews` mutation. `in_scope()` had exactly two call sites, in
`comment_on_pr` and `reply_to_thread`, and the request path had none, so
`wait` on a repository under another owner issued a state-changing call
under the logged-in identity while the refusal that stops the same
target on `reply` never fired.

## What this changes

**The probe runs on a stripped environment.** `git remote get-url
origin` now runs without the four discovery names, and without any name
starting with `GIT_CONFIG`. The second category is a prefix test rather
than a list, because `GIT_CONFIG_KEY_n` has no fixed upper bound, so no
enumeration of literal names can be complete.

**`wait` refuses a cross-owner target outright**, exit `64`, ahead of
its first read rather than at the request, so a refused run neither
writes nor reads. All three write paths now state one rule, which is
what makes `scripts/README.md`'s "the owner check is also enforced
in-process" true without qualification.

## What this deliberately does not do

The strip closes the channels an inherited tooling environment sets. It
does not close an inherited `HOME`, `XDG_CONFIG_HOME`, or `PATH`, and it
cannot: those deliver the same `insteadOf` injection, and stripping them
would either break `safe.directory` or make the probe ignore a global
config a legitimate user really has. A caller controlling any of the
three already controls the process that would perform the write, so
there is nothing left for this check to defend. The comment above the
strip says so rather than leaving a reader to infer completeness from a
name list.

## A design that was tried and reverted

An intermediate version probed with `git config --local --get
remote.origin.url`, which does close the `insteadOf` vector. It was
reverted, because a review pass proved three defects a reviewer is
likely to rediscover:

- **It inverted the boundary.** A `[remote "origin"]` may carry more
than one `url`, which is ordinary documented config. `remote get-url`
returns the first, git's own fetch identity; `config --get` returns the
last. So the scope anchor moved to the second owner, and a write to that
owner was admitted where it had been refused.
- `--local` does not follow `include.path`, so a checkout configuring
its origin through an include read as no owner and every write refused.
- A URL-shorthand alias origin yielded a raw value with no owner to
parse, refusing the same way.

Two cases pin the first two shapes against a return to that command.

## Correcting the issue text

#1561 motivates the fix by saying a git hook, `git bisect run`, and `git
rebase --exec` each export `GIT_DIR`. Measured against this host's git,
none of the three does: a `pre-commit` hook receives `GIT_AUTHOR_*`,
`GIT_EDITOR`, `GIT_EXEC_PATH`, `GIT_INDEX_FILE` and `GIT_PREFIX` and no
`GIT_DIR`, and `post-commit`, `post-checkout`, `bisect run` and `rebase
--exec` all leave it unset. The fix stands on its own, since an
inherited `GIT_DIR` genuinely overrides `-C` whatever sets it, but the
provenance was deleted rather than narrowed so a reader who re-derives
it does not conclude the guard is unnecessary.

## Verification

Every behavioral claim was proved against a reverted fix rather than
trusted. Reverting `env=` fails five of the eight environment cases;
reverting the `wait` guard fails both refusal cases at the first GraphQL
call. One case is documented as not being a regression guard for any
reverted state here, because `remote get-url` ignores the injection it
covers.

Beyond the new cases, the probe was driven through the real
`origin_owner()` under each of `GIT_DIR`, `GIT_COMMON_DIR`,
`GIT_CONFIG_COUNT` (as an `insteadOf` rewrite and as a direct override),
`GIT_CONFIG_GLOBAL`, `GIT_CONFIG_SYSTEM`, `GIT_CONFIG_PARAMETERS` and
`GIT_CONFIG`, answering the anchor owner every time, and through the
multi-url and `include.path` shapes that broke the reverted design.

A test-setup defect of the same class was fixed alongside: the helper
building the throwaway repositories shelled out to `git init` and `git
remote add` on the inherited environment, so under an ambient `GIT_DIR`
the cases errored and the `remote add` wrote an `origin` into whatever
repository that variable named.

One further test pins the command table to the parser's own `cmd`
choices. `wait` became a write by acquiring a mutation rather than by
being added, so a table keyed to "a new subcommand appeared" would not
have caught it.

## Known and not addressed here

#1582, `in_scope()`'s no-owner refusal gives one remedy for several
unrelated probe failures. `wait` refusing on the same terms is what
makes it matter, since an absent git or an inherited
`GIT_CEILING_DIRECTORIES` now ends a read-only poll with advice that
does not fit the cause.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **Bug Fixes**
- Write commands now reject targets belonging to a different repository
owner before contacting GitHub.
- The `wait` command returns exit code 64 when the target is out of
scope.

- **Documentation**
- Updated command-line documentation to explain scope checks and refusal
behavior for `wait`, `comment`, and `reply`.
- Clarified that owner validation occurs before review requests,
polling, or other GitHub operations.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 13, 2026 16:06
@coderabbitai

coderabbitai Bot commented Sep 13, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: f64a6149-1184-4f5b-9066-32e2af7b7d15


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The scope probe hardening and wait scope enforcement are consistent with the stated boundary policy and are backed by focused regression tests and updated documentation.

Pull request overview

This promotion brings the develop fix into main to harden scripts/pr_review.py's in-process owner boundary against inherited git environment variables, and to ensure wait enforces the same scope refusal as other write-capable subcommands before it performs any GitHub I/O.

Changes:

  • Strip GIT_DIR, GIT_COMMON_DIR, related discovery variables, and any GIT_CONFIG* variables from the environment used by origin_owner() so the scope probe cannot be redirected by inherited git environment configuration.
  • Add an early in_scope() gate for wait, returning exit code 64 before any GraphQL call or auto-request mutation when the target owner is out of scope.
  • Add targeted unit tests for both the environment-stripping behavior and the command-level invariant that all write-capable commands refuse cross-owner targets before touching the network; update docs accordingly.
File summaries
File Description
scripts/pr_review.py Strips git discovery/config env for the owner probe and enforces scope refusal for wait before any GitHub reads/writes.
scripts/tests/test_pr_review.py Adds regression tests for inherited env redirection and for ensuring all write commands refuse cross-owner before transport use.
scripts/README.md Updates wait documentation to reflect the expanded mutation set and the new 64 out-of-scope behavior.
Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@ptr727
ptr727 merged commit 262ce31 into main Sep 13, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

wait's Copilot review request bypasses the in-process owner check pr_review.py's scope probe reads another repository when GIT_DIR is inherited

2 participants