Skip to content

fix(checkpoint): base checkpoint_remote ownership on origin, not the push target - #1898

Merged
gtrrz-victor merged 2 commits into
mainfrom
soph/checkpoint-remote-fork-provenance
Aug 7, 2026
Merged

fix(checkpoint): base checkpoint_remote ownership on origin, not the push target#1898
gtrrz-victor merged 2 commits into
mainfrom
soph/checkpoint-remote-fork-provenance

Conversation

@Soph

@Soph Soph commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

https://entire.io/gh/entireio/cli/trails/971

Rebased onto main after #1905 merged. Both PRs added gitremote.GetPushURLs; this one now consumes the merged copy, so gitremote.go drops out of the diff entirely (7 files → 6) and remote.GetPushURL delegates to the remote.GetPushURLs wrapper that came with #1905 rather than reaching past it to gitremote and re-wrapping the error. No behavior change from the rebase.

Background: what the fork check was for

checkpoint_remote is normally committed in .entire/settings.json, so anyone who forks a project inherits it. e8b589835 added "fork detection" to stop a contributor's session data being pushed into the upstream project's checkpoint repo — one they typically can't write to and shouldn't be writing to. Sound guard.

It was implemented by comparing the push remote's owner to the checkpoint repo's owner.

The problem

That conflates two different questions: "is this checkpoint_remote mine?" and "where is this particular push going?" Two consequences:

  • Pushing to any differently-owned remote disables your own checkpoint_remote — and the fallback then sends the checkpoints to that remote. A backup remote in another org, or a colleague's fork, receives your session transcripts instead of your configured checkpoint repo. Exactly the opposite of what the explicit setting asked for.
  • It has no single answer for a remote with several push URLs. git allows remote.<name>.pushurl to repeat and pushes to all of them, so one boolean verdict has to cover a set of destinations that may have different owners. feat(checkpoint): push checkpoint refs to one destination, and say where #1905 has since removed the ambiguity on the git-refs backend by sending checkpoint refs to the first push URL only; git-branch still fans out to every one of them, so the problem is real there.

Nothing in the suite pinned the intended reading: 16 of 18 TestPushURL cases use pushRemote: "origin", and the two "upstream" cases exercise a missing remote rather than a differently-owned one. "Push remote owner" and "origin owner" were indistinguishable everywhere.

The fix

Ask the ownership question directly, from two local signals — no network, so the pre-push path stays free of SSH passphrase/security-key prompts:

  1. A checkpoint_remote in .entire/settings.local.json is gitignored and per-clone, so it cannot have been inherited by cloning or forking. Always ours.
  2. Otherwise compare the checkpoint repo's owner against origin's owner: "am I working in a repo owned by whoever owns the checkpoint repo?" A fork clone has origin <fork>/app against an upstream-owned checkpoint repo, and mismatches.

The push destination no longer enters the decision, which is what gives the multi-push-URL case a single answer.

Both existing fork tests pass unchanged — they were written with originURL set to the fork (https://github.com/fork/app.git, entire://.../gh/fork/app), which is good evidence this is the reading that was intended all along.

Signal 1 doubles as the escape hatch for a checkpoint repo legitimately owned by a different account or org than origin — a config the previous check had no way to express.

  1. If there is no origin at all, the push remote's URL is the only identity the repo has, so it is used instead — which is what the predecessor effectively did. A repo whose only remote is e.g. upstream must not lose its configured checkpoint remote just because nothing is named origin. In that topology there is no origin-vs-push-remote divergence to worry about: the repo has exactly one identity. (Added in the second commit, from an Entire trail finding — see below.)

What gets compared, case by case

Scenario Identity used checkpoint_remote Verdict
Normal repo origin acme/app acme/checkpoints owners match → honored
Fork contributor, inherited committed setting origin fork/app acme/checkpoints mismatch → ignored; checkpoints go to their own fork
Cross-org, set in settings.local.json — (provenance short-circuit) myorg/checkpoints local-only → honored
Cross-org, committed in settings.json origin me/app myorg/checkpoints mismatch → ignored — known limitation, use settings.local.json
Pushing to a backup remote in another org origin acme/app acme/checkpoints origin decides → honored ← the misroute this PR fixes
No origin; only upstream = acme/app push remote acme/app acme/checkpoints match → honored ← the trail-finding fix
Local-path origin /tmp/repo unparseable acme/checkpoints owner undeterminable → ignored (prior behavior preserved)

The property that matters: the last three rows no longer depend on where this particular push is headed, so the verdict is well-defined however many push URLs the remote carries.

Also in here

  • Read the push remote's URL with git remote get-url --push when deriving transport. A remote's push destination is remote.<name>.pushurl when set, so the plain url can name a different repository than the one being pushed to. Latent until now because url == pushurl in the common case. On a multi-URL remote this takes the first push URL — which post-feat(checkpoint): push checkpoint refs to one destination, and say where #1905 is not an arbitrary tiebreak but the URL checkpoint refs actually land in, so transport and (origin-less) ownership are now derived from the destination itself.
  • The inherited case logs at Warn instead of Debug, with the settings.local.json hint, so a user whose checkpoint_remote is being ignored can find out why.

Behavior changes

Scenario Before After
Fork clone, inherited committed setting ignored, checkpoints → your remote unchanged
Own repo, push to a differently-owned remote ignored, checkpoints → that remote honored, checkpoints → checkpoint repo
checkpoint_remote in settings.local.json, origin owner differs ignored honored
Remote with a pushurl in another repo transport from the fetch url transport from the push url
Origin absent, same-owner push remote honored (push-remote owner) honored (push-remote owner)
Non-forge URL, e.g. a local path (owner undeterminable) fallback unchanged

Second commit: Entire trail finding (trail 971, medium)

The review agent caught that the first commit regressed one topology: keying ownership on origin made a missing origin read as "inherited", so a repo whose only remote is upstream — with an owner matching the checkpoint repo — silently lost checkpoint-remote routing, where the predecessor would have used it. Origin still decides when it exists; only its complete absence falls back to the push remote as identity. The check moved after the push-remote read (needed for transport anyway) to have that URL available, and the reason strings now name which remote was consulted.

Finding resolved on the trail with the fixing commit.

Known interaction with #1893

#1893 adds a precedence tier that elects the current branch's push destination as the checkpoint sync remote, specifically to support the clone-upstream, add-your-fork topology (origin is a base repo you can't push; the branch tracks your fork). This PR's ownership rule assumes the opposite — that origin is yours.

Where they meet, the fork guard stops firing:

origin checkpoint_remote (committed) verdict
Fork clone (assumed here) fork/app acme/checkpoints mismatch → ignored ✅
Clone upstream + add fork (#1893's case) acme/app acme/checkpoints match → honored ⚠️

In the second row a contributor's transcripts target the upstream project's checkpoint repo. The usual outcome is an auth failure and checkpoints never leaving the machine; where they do have write access, the session data lands upstream — the thing e8b589835 exists to prevent.

This is the same class as the trail finding fixed in the second commit (keying on origin assumes origin is yours), and it only becomes reachable when #1893's tracking tier lands. Flagging it here; the resolution belongs to #1893, which is the PR that makes that topology first-class.

Not in scope

Divergence recovery across multiple push URLs. #1905 settled it for git-refs by picking one destination; on git-branch the original constraint stands (fast-forward-only push plus one local ref cannot satisfy N independently diverged remotes with cherry-pick reconciliation), and the executable specs merged with #1897 document the behavior. It needs its own design.

Test plan

  • Three new TestPushURL cases, each verified to fail against the previous implementation: differently-owned push remote with matching origin; settings.local.json provenance; transport derived from the push URL. Reverting just the GetPushURL call makes the last one fail with https://… where git@… is expected.
  • Two further TestPushURL cases for the no-origin fallback, also verified against the previous logic (the first reported exactly enabled = false, want true): no origin + same-owner push remote → honored; no origin + differently-owned push remote → still inherited.
  • All pre-existing TestPushURL cases pass unchanged.
  • After the rebase onto feat(checkpoint): push checkpoint refs to one destination, and say where #1905: mise run test:ci green end to end (unit + integration -race + canary 4/4), mise run lint 0 issues. The multi-push-URL integration matrix feat(checkpoint): push checkpoint refs to one destination, and say where #1905 brought in passes unchanged, which is the check that matters here — remote.GetPushURL and resolveRefsPushDestination now read the same first push URL.
  • README and docs/testing/git-remote-test-plan.md (row B5) updated to describe ownership by origin, including the two cases that must not fall back.

🤖 Generated with Claude Code

@Soph
Soph requested a review from a team as a code owner August 4, 2026 19:35
Copilot AI lite review requested due to automatic review settings August 4, 2026 19:35

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.

Pull request overview

This pull request adjusts how the CLI decides whether a configured checkpoint_remote should be honored, so that “is this setting mine (vs inherited from upstream)?” is determined from local provenance and the repo’s origin owner—rather than from the owner of the current push destination. This prevents misrouting checkpoint/session data when pushing to differently-owned remotes (e.g., backups or forks) and makes the behavior expressible for multi-push-URL remotes.

Changes:

  • Treat checkpoint_remote as “inherited” unless it’s defined in .entire/settings.local.json (gitignored) or origin’s owner matches the checkpoint repo owner.
  • Derive transport details from the actual push URL via git remote get-url --push --all (instead of the fetch URL).
  • Expand unit test coverage and update docs/test plan to reflect the new ownership semantics and the local-only escape hatch.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated no comments.

Show a summary per file
File Description
README.md Updates user-facing documentation to describe inherited-setting detection based on origin ownership and .entire/settings.local.json.
docs/testing/git-remote-test-plan.md Updates test plan B5 to reflect the new “origin owner” inheritance rule and the two “must not fall back” cases.
cmd/entire/cli/strategy/checkpoint_remote.go Updates strategy-layer documentation for the checkpoint remote resolution behavior.
cmd/entire/cli/settings/settings.go Adds CheckpointRemoteIsLocalOnly to detect whether checkpoint_remote is present in .entire/settings.local.json.
cmd/entire/cli/gitremote/gitremote.go Adds GetPushURLs to read push destinations from git config (remote.<name>.pushurl / repeated URLs).
cmd/entire/cli/checkpoint/remote/util.go Moves the “inherited setting” decision to be based on .entire/settings.local.json + origin owner, and derives protocol from the push URL.
cmd/entire/cli/checkpoint/remote/util_test.go Adds new TestPushURL cases and a helper to write .entire/settings.local.json.

gtrrz-victor
gtrrz-victor previously approved these changes Aug 7, 2026
Soph and others added 2 commits August 7, 2026 12:52
…push target

checkpoint_remote is normally committed in .entire/settings.json, so forking a
project inherits it. e8b5898 added "fork detection" to stop a contributor's
session data being pushed into the upstream project's checkpoint repo — a sound
guard, implemented by comparing the PUSH REMOTE's owner to the checkpoint repo's
owner.

That conflates two different questions: "is this checkpoint_remote mine?" and
"where is this particular push going?". Consequences:

- Pushing to any differently-owned remote (a backup, a colleague's fork)
  disabled the user's own checkpoint_remote and pushed the checkpoints to that
  remote instead — the opposite of what the explicit setting asked for.
- It cannot be expressed at all for a remote carrying several push URLs with
  different owners, since one boolean has to cover the whole set.

Nothing pinned the intended reading: 16 of 18 PushURL cases use
pushRemote:"origin", and the two "upstream" cases exercise a missing remote, not
a differently-owned one — so "push remote owner" and "origin owner" were
indistinguishable in the whole suite.

Ask the ownership question directly instead, from two local signals (no network,
so the pre-push path stays free of SSH prompts):

1. A checkpoint_remote in .entire/settings.local.json is gitignored and
   per-clone, so it cannot have been inherited — always ours.
2. Otherwise compare the checkpoint repo's owner against ORIGIN's owner: "am I
   working in a repo owned by whoever owns the checkpoint repo?"

Both existing fork tests keep passing unchanged, because they were written with
origin set to the fork — further evidence this is the reading that was meant.
An origin whose owner cannot be determined still falls back, preserving today's
behavior for local-path remotes. (1) is the escape hatch for a checkpoint repo
legitimately owned by a different account or org than origin, which the previous
check had no way to express.

Also read the push remote's URL with `git remote get-url --push` when deriving
transport. A remote's push destination is remote.<name>.pushurl when set, so the
plain url could name a different repository than the one being pushed to.

The inherited case now logs at Warn with the settings.local.json hint rather
than Debug, so a user whose checkpoint_remote is being ignored can find out why.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01KZ74AZN68SXWB33E5DFRY7BJ
Entire trail finding (trail 971, medium): checkpointRemoteIsInherited treated a
missing origin remote as automatically inherited, so it fell back and disabled
checkpoint_remote. The predecessor compared the *push remote's* owner and never
required a remote literally named origin, so a repo whose only remote is e.g.
"upstream" — with an owner matching the configured checkpoint repo — silently
lost checkpoint-remote routing under the new check.

Origin still decides when it exists. It is only when there is no origin at all
that the push remote serves as the identity, which is what the old code
effectively did; in that topology there is no origin-vs-push-remote divergence to
worry about, because the repo has exactly one identity.

The check therefore moves after the push-remote read (needed for transport
anyway) so that URL is available as the fallback identity. The reason strings now
name which remote was consulted.

Two test cases, both verified to fail against the previous logic (the first
reported `enabled = false, want true`):
  - no origin + same-owner push remote -> checkpoint remote kept
  - no origin + differently-owned push remote -> still reads as inherited

TestPushURL gains //nolint:maintidx: the score tracks the size of the case table,
not branching logic, matching the existing convention in this repo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01KZ9H1VMB9X8J2R5YST5HX62R
@Soph
Soph force-pushed the soph/checkpoint-remote-fork-provenance branch from 0f814e8 to 940d905 Compare August 7, 2026 11:16
@gtrrz-victor
gtrrz-victor merged commit 72e7b96 into main Aug 7, 2026
12 checks passed
@gtrrz-victor
gtrrz-victor deleted the soph/checkpoint-remote-fork-provenance branch August 7, 2026 11:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants