Skip to content

fix(module): let a pin advance without deinit or a signing key - #56

Merged
rldyourmnd merged 2 commits into
mainfrom
fix/pin-without-ceremony
Aug 29, 2026
Merged

fix(module): let a pin advance without deinit or a signing key#56
rldyourmnd merged 2 commits into
mainfrom
fix/pin-without-ceremony

Conversation

@rldyourmnd

Copy link
Copy Markdown
Contributor

Advancing an estate module pin was unreachable in practice. Three guards on the same operation each refused the state the previous one required.

guard wants
GDS_MODULE_PIN_CONSUMER_STATE_UNSAFE a clean consumer — but an advanced submodule is one unstaged gitlink
GDS_MODULE_PIN_GITLINK_NOT_ELIGIBLE a changed gitlink whose checkout is absent
GDS_SIGNED_APPROVAL_REQUIRED a signed approval bound to the exact plan, i.e. the private Ed25519 key

The first two read as a contradiction; the only combination satisfying both is git submodule deinit plus a separate module clone — four preconditions that must hold simultaneously and are documented nowhere (#51).

None of it protects anything. The only mutation is a gitlink rewrite in the consumer's own working tree: no provider write, no credential, nothing published. The consumer's own pull request and checks are the real gate. The cost was real — the estate silently drifted behind modules it had already merged, because a pin could not be advanced without the signing key.

What changes

  • A checkout already sitting at the target commit is accepted. That is stronger evidence than an absent directory: the consumer holds the commit it is about to pin.
  • Apply takes no approval for this step. Signed approval stays on every operation that writes outside the repository: provider lifecycle, rulesets, releases, anchors.
  • The relaxation is exactly one gitlink wide, enforced in three places that previously all hard-coded uninitialized: the mutation runner's precondition and postcondition, and the handler's verify.

Proof

TestModuleUpdatePinAcceptsCheckedOutModuleWithoutApproval drives the previously impossible shape end to end and passes no approval reference at all. TestModuleUpdatePinStillRefusesAnUnrelatedChange adds one untracked file to that same tree and requires the refusal, so the relaxation cannot widen unnoticed. Full go test ./... and scripts/validate_ci_tier.sh fast pass.

Closes #51.

https://claude.ai/code/session_01BjpW1NtF1oik18aEMMGtMP

Advancing an estate module pin was unreachable in practice. Three guards on the
same operation each refused the state the previous one required.

`GDS_MODULE_PIN_CONSUMER_STATE_UNSAFE` wanted a clean consumer. An advanced
submodule is reported as one unstaged gitlink, so the consumer was never clean.
`GDS_MODULE_PIN_GITLINK_NOT_ELIGIBLE` then wanted a *changed* gitlink whose
checkout was absent. The two read as a contradiction, and the only combination
that satisfied both was `git submodule deinit` plus a separate module clone --
four preconditions that hold simultaneously and are written down nowhere. Past
that, apply demanded a signed approval bound to the exact plan.

None of that protects anything. The only mutation is a gitlink rewrite in the
consumer's own working tree: no provider write, no credential, nothing
published, and the consumer's own pull request and checks are the real gate.
The cost was real: the estate silently drifted behind modules it had already
merged, because the pin could not be advanced without the private signing key.

So a checkout already sitting at the target commit is now accepted -- stronger
evidence than an absent directory, since the consumer holds the commit it is
about to pin -- and apply takes no approval. The relaxation is exactly one
gitlink wide, through the mutation runner's pre- and postconditions and the
handler's verify: any staged, untracked or conflicted path, or a second unstaged
one, still refuses. Signed approval stays on every operation that writes outside
the repository: provider lifecycle, rulesets, releases and anchors.

Two tests carry it. One drives the previously impossible shape end to end and
passes no approval reference at all. The other adds one untracked file to that
same tree and requires the refusal, so the relaxation cannot widen unnoticed.

Closes #51.

Claude-Session: https://claude.ai/code/session_01BjpW1NtF1oik18aEMMGtMP
The pin change edited the lifecycle contract, which is part of the source tree
the bundle lock digests. Regenerated through `gds generate repository`
plan/apply rather than by hand.

Claude-Session: https://claude.ai/code/session_01BjpW1NtF1oik18aEMMGtMP
@rldyourmnd
rldyourmnd merged commit ee390ce into main Aug 29, 2026
7 checks passed
@rldyourmnd
rldyourmnd deleted the fix/pin-without-ceremony branch August 29, 2026 06:38
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.

gds module update-pin: four undocumented preconditions read as a contradiction

1 participant