Skip to content

fix(checkpoint): require the push destination to confirm checkpoint_remote ownership - #1934

Open
Soph wants to merge 2 commits into
mainfrom
soph/checkpoint-remote-push-destination-ownership
Open

fix(checkpoint): require the push destination to confirm checkpoint_remote ownership#1934
Soph wants to merge 2 commits into
mainfrom
soph/checkpoint-remote-push-destination-ownership

Conversation

@Soph

@Soph Soph commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

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

The topology that slips through

checkpoint_remote is normally committed in .entire/settings.json, so anyone who clones or forks a project inherits it. e8b589835 added a guard so a contributor's session transcripts don't get pushed into the upstream project's checkpoint repo. #1898 rebased that guard onto origin's owner: "am I working in a repo owned by whoever owns the checkpoint repo?"

That reading assumes origin is yours. There are two ways to contribute to someone else's project, and it only holds for one:

origin verdict
Fork first, clone the fork contributor/app owner mismatch → ignored ✅
Clone the base repo, add your fork acme/app owner matches → honored ⚠️

In the second row origin belongs to upstream, so an inherited setting reads as ours and a contributor pushing to their own fork aims their transcripts at acme/checkpoints. Probed against main:

PUSH TO myfork -> url="https://github.com/acme/checkpoints.git" enabled=true

The usual outcome is an auth failure and checkpoints that never sync — noisy and broken rather than a leak — but where the contributor does have write access, their session data lands in the upstream project's repo. This is the same class as the regression #1898's second commit already fixed: keying on origin assumes origin is yours.

Why the gate doesn't already stop it

ENT-1451's pre-push gate exempts a configured checkpoint_remote:

if !ps.hasCheckpointURL() && !checkpointSyncAllowedForRemote(ctx, ps.remote) {
    return nil
}

A dedicated store is addressed directly rather than selected by the push, so it deliberately skips the gate. That exemption is correct — but it means a wrong ownership verdict sails straight past the gate. The gate is doing its job; it's being handed the wrong answer.

The fix

Ownership now requires every remote that identifies this repo to be owned by the checkpoint repo's owner — origin (which project is this) and every push URL of the push destination (which repos am I writing to). One mismatch means inherited. The settings.local.json provenance short-circuit and the no-origin fallback are unchanged.

With that, the fork case produces enabled=false, hasCheckpointURL() is false, and the gate drops the sync. Nothing reaches upstream.

The trade, made deliberately

This reverts #1898's headline case: your own repo with a differently-owned secondary remote (a backup in another org) no longer routes checkpoints to your checkpoint repo on pushes to that remote.

The two topologies are genuinely indistinguishable from local git config — both are origin=<owner>/app plus a push destination owned by someone else, and only the network could say which repo is actually yours. Something has to lose.

Choosing this direction because:

  • Pushes to the elected sync remote are unaffected. Your normal git push origin still routes checkpoints to the checkpoint repo exactly as before.
  • ENT-1451's gate already blocks checkpoint sync on pushes to a non-elected remote, so in the git-branch case this costs almost nothing the gate wasn't stopping. On git-refs it strictly helps: the push queue records only a ref, so a delivery to the wrong destination drains the queue and the checkpoint never reaches the right repo at all — misdelivery, not a surplus copy.
  • Erring toward "not ours" is the safe direction. It never writes session transcripts to a repository we cannot confirm we own. The opposite error writes them somewhere they can't be taken back.
  • settings.local.json remains the escape hatch, and it's what the warning points at.

Behavior changes

Scenario Before After
Normal repo, push to origin honored unchanged
Fork clone, inherited setting ignored unchanged
Clone upstream + add fork, push to fork honored → transcripts at upstream ignored; gate drops sync
Own repo, push to a differently-owned remote honored ignored (gate blocks that push anyway)
checkpoint_remote in settings.local.json honored unchanged
No origin, same-owner push remote honored unchanged

A contributor who wants checkpoints in their own fork sets checkpoint_push_remote: myfork; election then picks the fork, the gate allows the push, and ownership routes the checkpoints there. Verified:

$ entire status
  Checkpoints sync to: myfork (set by checkpoint_push_remote)

Test plan

  • New integration test, TestCheckpointSyncRemote_InheritedCheckpointRemoteDoesNotBypassGate: HTTPS fixture with acme/app as origin, contributor/app as the fork, committed acme/checkpoints; asserts nothing reaches any of the three after a pre-push to the fork. Negative control verified — reverting just util.go fails it with upstream's checkpoint repo must not receive a contributor's session data from an inherited setting.
  • Two new TestPushURL cases: the fork topology, and settings.local.json still winning inside it.
  • One existing case inverted (differently owned push remote …), which is the trade above — it was the only case in the 25-case table that changed.
  • Two TestPushURL cases for multi-push-URL remotes (second commit, from Copilot's review): a differently-owned URL in second position reads as inherited, and an all-same-owner set still resolves. Negative control: reverting to first-URL-only checking fails the first with enabled = true, want false. Note a real multi-URL remote needs two set-url --push --add calls — the first pushurl replaces the remote's url as a push destination, so one --add yields a one-URL remote (the first version of this test wasn't multi-URL and passed either way).
  • mise run test:ci green (unit + integration -race + canary), mise run lint 0 issues.
  • README and test-plan row B5 updated.

Review follow-up (second commit)

Copilot flagged that checking only the first push URL leaves mirror-style remotes exposed, since git-branch fans out to all of them. Fixed rather than documented: ownership now holds across every push URL, and GetPushURL (single caller) gave way to the plural form. The first URL still drives transport derivation, matching where resolveRefsPushDestination sends checkpoint refs.

Known limitation, not addressed here

FetchURL applies no ownership check at all — checkpointRemoteIsInherited is called only from PushURL. So a repo whose writes were correctly diverted still reads from the configured checkpoint repo, looking where nothing was written. Pre-existing and affecting both fork topologies; it wants its own change, and probably belongs with the wider read-path work in #1634.

Relatedly, entire status reports the destination for a push to the elected remote, so in the fork topology it still shows dedicated checkpoint remote (acme/checkpoints) until checkpoint_push_remote is set. That's the accepted status-vs-push divergence noted in ENT-1451's own comments.

🤖 Generated with Claude Code

…emote ownership

checkpoint_remote is committed in .entire/settings.json, so cloning or
forking a project inherits it, and a guard exists so a contributor's
session transcripts are not pushed into the upstream project's checkpoint
repo. #1898 keyed that guard on origin's owner, which assumes origin is
yours.

It is not, in one of the two ways to contribute to someone else's project:

  fork first, clone the fork -> origin contributor/app -> mismatch, ignored
  clone the base, add a fork -> origin acme/app        -> MATCHES, honored

In the second the setting reads as ours, and a contributor pushing to
their own fork aims their transcripts at acme/checkpoints. The pre-push
gate cannot catch it because a configured checkpoint_remote is exempt
from the gate by design, so a wrong ownership verdict goes straight
through.

Ownership now requires every remote that identifies the repo to be owned
by the checkpoint repo's owner: origin says which project this is, the
push destination says which repo we are writing to. One mismatch means
inherited. The settings.local.json provenance short-circuit and the
no-origin fallback are unchanged.

The trade: a repo of our own with a differently-owned secondary remote no
longer routes checkpoints to our checkpoint repo on pushes to that
remote. The two topologies are indistinguishable from local git config —
both are origin=<owner>/app plus a push destination owned by somebody
else — so one of them has to lose. Pushes to the elected sync remote are
unaffected, ENT-1451's gate already blocks checkpoint sync on pushes to a
non-elected remote, and erring toward "not ours" never writes transcripts
to a repository we cannot confirm we own. On git-refs it strictly helps:
the push queue records only a ref, so a delivery to the wrong destination
drains the queue and the checkpoint never reaches the right repo at all.

Not addressed: FetchURL applies no ownership check, so a repo whose
writes were diverted still reads from the configured checkpoint repo.
Pre-existing, affects both fork topologies, wants its own change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Soph
Soph requested a review from a team as a code owner August 10, 2026 10:37
Copilot AI lite review requested due to automatic review settings August 10, 2026 10:37

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

Tightens the security gate for honoring a configured checkpoint_remote (typically committed in .entire/settings.json) so contributors don’t accidentally sync session transcripts into an upstream project’s checkpoint repository when they cloned upstream and push to their own fork.

Changes:

  • Updates checkpoint_remote ownership detection to require both origin and the push destination owner to match the checkpoint repo owner before enabling the dedicated checkpoint URL.
  • Adds an end-to-end integration test that reproduces the “clone upstream + add fork + push to fork” topology and asserts no checkpoint data reaches upstream repos.
  • Updates docs and unit tests to reflect the new ownership decision and trade-offs.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
README.md Updates user-facing description of when checkpoint_remote is considered “yours”.
docs/testing/git-remote-test-plan.md Updates test plan B5 to reflect the new ownership rule and new integration coverage.
cmd/entire/cli/integration_test/checkpoint_sync_remote_test.go Adds an integration guard test for the upstream-clone + fork-push contributor topology.
cmd/entire/cli/checkpoint/remote/util.go Implements the new ownership logic requiring both origin and push destination owners to match.
cmd/entire/cli/checkpoint/remote/util_test.go Updates and adds unit cases covering the new ownership behavior and settings.local.json override.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +374 to +377
// On a remote with several push URLs only the FIRST is checked, matching the
// destination resolveRefsPushDestination sends checkpoint refs to. The
// git-branch backend still fans out to every push URL, so a multi-URL remote
// whose later URLs are owned by someone else is not covered here.
Comment thread README.md Outdated
- Fetch the checkpoint branch locally if it exists on the remote but not locally (one-time)
- Push `entire/checkpoints/v1` to the checkpoint repo instead of your default push remote
- Ignore the setting if it looks inherited rather than yours, and push checkpoints to your own push remote instead. `checkpoint_remote` is normally committed in `.entire/settings.json`, so forking a project inherits it — without this, a contributor's session data would be pushed into the upstream project's checkpoint repo. A setting is treated as yours when it lives in the gitignored `.entire/settings.local.json`, or when your `origin` remote is owned by the same account or org as the checkpoint repo
- Ignore the setting if it looks inherited rather than yours, and push checkpoints to your own push remote instead. `checkpoint_remote` is normally committed in `.entire/settings.json`, so cloning or forking a project inherits it — without this, a contributor's session data would be pushed into the upstream project's checkpoint repo. A setting is treated as yours when it lives in the gitignored `.entire/settings.local.json`, or when **both** your `origin` and the remote you are pushing to are owned by the same account or org as the checkpoint repo. Requiring both covers contributors who cloned the upstream repo and added their own fork, where `origin` belongs to the upstream project rather than to them
… first

Addresses Copilot's review on #1934. The previous commit checked only the
first push URL and documented the gap; on the git-branch backend git fans
out to ALL push URLs, so a differently-owned mirror in second position
read as ours and would receive session transcripts.

Ownership now requires origin and every push URL of the push remote to be
owned by the checkpoint repo's owner. The first push URL still drives
transport derivation, matching the single destination
resolveRefsPushDestination sends checkpoint refs to.

GetPushURL had exactly one caller and is removed in favour of the plural
form.

Test note: a genuine multi-push-URL remote needs TWO `set-url --push
--add` calls. The first pushurl REPLACES the remote's url as a push
destination (git's push_url_of_remote), so one --add produces a one-URL
remote — the first version of this test was not multi-URL at all and
passed against both implementations. With the same-owner URL first and the
differently-owned one second, reverting to first-URL-only checking fails
with `enabled = true, want false`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

2 participants