v0.275.0: a backup's data and its version travel together (R-696, 07 §6.6, D4 option A); R-695, R-691, R-694
gates / gates (push) Successful in 23s

The unit's data files are stamped with the versions that wrote them; the capture keeps the
definition the data belongs to; a restore never starts data under another version's
definition (unit restores refuse a mismatch; the off-site restore writes the snapshot's
definition); every tier's time is its data's; the conversion-copy release needs a dump on
the new engine. File-browser sync single-flight + no empty kept folder (R-695); the kept
view joins the folder's owning group, language switch resyncs (R-691); a restore-generated
login is not shown as the password (R-694). Red-proofs in
felhom.eu/documentation/audits/version-travel-2026-09-26/.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-09-26 10:35:22 +02:00
parent fb2bcdd5d6
commit b6810f14ff
47 changed files with 3771 additions and 135 deletions
+35
View File
@@ -2827,9 +2827,27 @@ func (a *stackAdapter) GetStackRecoveryInfo(name string) (backup.RecoveryInfo, b
DataKeyEnvVars: dataKeys,
PortableSecretEnvVars: portableNames,
PortableSecrets: portable,
InstalledImages: installedRefs(s.AppConfig),
}, true
}
// installedRefs renders app.yaml's `installed_images` observation as service -> `ref@digest` (v0.275.0,
// R-696) for the data stamps. Nil when nothing was observed — absent means UNKNOWN (deploy.go).
func installedRefs(cfg *stacks.AppConfig) map[string]string {
if cfg == nil || len(cfg.InstalledImages) == 0 {
return nil
}
out := make(map[string]string, len(cfg.InstalledImages))
for svc, im := range cfg.InstalledImages {
ref := im.Ref
if im.Digest != "" {
ref += "@" + im.Digest
}
out[svc] = ref
}
return out
}
// RecoverStackSecrets returns the live decrypted values for the named secret env vars present in the
// stack's app.yaml (the guest's own — live rootfs or PBS-restored). Absent/empty names are omitted;
// the caller's fail-closed gate decides. Secrets come from the guest, never from the recovery unit.
@@ -3594,6 +3612,23 @@ type updateGuardsAdapter struct {
q *quiesce.Loop
}
// DumpStamps (v0.275.0, A4) — the app's own unit's database dumps with the versions that wrote them;
// the conversion-copy release's second condition (stacks.DumpStampSource).
// The release reads the stamps only through this optional interface — a method nobody satisfies would
// silently keep every conversion copy forever; this line fails the build instead (v0.275.0).
var _ stacks.DumpStampSource = (*updateGuardsAdapter)(nil)
func (a *updateGuardsAdapter) DumpStamps(name string) []stacks.DataDumpStamp {
if a.b == nil {
return nil
}
var out []stacks.DataDumpStamp
for _, d := range a.b.UnitDumpStamps(name) {
out = append(out, stacks.DataDumpStamp{File: d.File, At: d.At, Images: d.Images})
}
return out
}
func (a *updateGuardsAdapter) HoldFor(name string) (bool, string) {
if a.b == nil {
return false, ""