offsiteapply: include QuotaGB in the descriptor hash — quota changes re-apply

A hub-side quota raise now reaches the target: the marker hash changes and
the bridge re-applies via key-auth-first (no password consumed). Test:
quota-only change remaps the new quota with a panicking consumer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
2026-07-09 23:58:30 +02:00
parent 8917014991
commit f830325ca3
2 changed files with 24 additions and 3 deletions
@@ -72,10 +72,12 @@ func (b *Bridge) logf(f string, a ...any) {
} }
} }
// descriptorHash is the applied-marker key: a hash of the identity-bearing descriptor fields. A change // descriptorHash is the applied-marker key: a hash of the APPLY-RELEVANT descriptor fields. A change
// (re-provision → new host/user/fingerprint) yields a new hash → the bridge re-applies (new password). // (re-provision → new host/user/fingerprint, or a quota adjustment) yields a new hash → the bridge
// re-applies. QuotaGB is included (SLICE 4) so a hub-side quota raise reaches the target — on an
// already-provisioned guest that re-apply is a cheap key-auth-first re-pin (no password consumed).
func descriptorHash(o config.OffsiteConfig) string { func descriptorHash(o config.OffsiteConfig) string {
s := fmt.Sprintf("%s|%s|%s|%d|%s|%s", o.Type, o.Host, o.User, o.Port, o.RepoPath, o.HostFingerprint) s := fmt.Sprintf("%s|%s|%s|%d|%s|%s|%d", o.Type, o.Host, o.User, o.Port, o.RepoPath, o.HostFingerprint, o.QuotaGB)
sum := sha256.Sum256([]byte(s)) sum := sha256.Sum256([]byte(s))
return hex.EncodeToString(sum[:]) return hex.EncodeToString(sum[:])
} }
@@ -144,6 +144,25 @@ func TestBridge_AppliesEndToEnd(t *testing.T) {
} }
} }
// SLICE 4 — a quota-only descriptor change re-applies (the hash includes QuotaGB), and with a working
// key it costs no password: key-auth-first re-pins + remaps the quota.
func TestBridge_QuotaChangeReappliesWithoutConsume(t *testing.T) {
b, cons, _, en, _ := newBridge(t, goodOffsite())
cons.panics = true
b.Prober = &fakeProber{pem: "EXISTINGPEM", ok: true}
// marker for the OLD quota (25) already applied; the descriptor now says 50
old := b.Cfg.Offsite
old.QuotaGB = 25
_ = os.MkdirAll(filepath.Dir(b.MarkerPath), 0o700)
_ = os.WriteFile(b.MarkerPath, []byte(descriptorHash(old)), 0o600)
if err := b.Reconcile(context.Background()); err != nil {
t.Fatalf("quota-change reconcile: %v", err)
}
if en.calls != 1 || en.gotQuotaGB != 50 {
t.Fatalf("a quota raise must re-apply and map the NEW quota (no consume): %+v", en)
}
}
// Key-auth-first (Scenario B) — the existing key still works: NO consume, NO install; re-verify + re-pin + // Key-auth-first (Scenario B) — the existing key still works: NO consume, NO install; re-verify + re-pin +
// reconfigure with the EXISTING key, marker updated. // reconfigure with the EXISTING key, marker updated.
func TestBridge_KeyAuthFirstSkipsConsume(t *testing.T) { func TestBridge_KeyAuthFirstSkipsConsume(t *testing.T) {