v0.284.0 — a box deletes old app images (decision 53, R-736); an after_install app is held until its known login is replaced (R-741)
gates / gates (push) Successful in 27s

Image retention: after a done/undone guarded Update and at remove, an app's images older than its running
and previous one are deleted — never an image any container, installed compose or installed/previous record
names (box-wide keep set read at delete time); exact id, never forced or pruned; paused while any update runs;
a one-time sweep of catalog app images at the first start. Install hold: an after_install app is installed
behind the setup gate's door and opens when after_install succeeds or the household says it changed the login.
Tests TestImageRetention_* and TestInstallHold_* with red-proofs; parity fixture for the held card.

MinAgent: 0.131.0 (unchanged).

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-30 22:29:33 +02:00
parent d48da6c7f3
commit 5a3437669f
23 changed files with 1724 additions and 23 deletions
+21
View File
@@ -184,6 +184,12 @@ type AppConfig struct {
// SetupGate (v0.280.0, decision 46) is the app's setup gate: closed from a fresh install until the first
// setup is done. A life record (carried across a restore). See setup_gate.go.
SetupGate *SetupGateRecord `yaml:"setup_gate,omitempty" json:"setup_gate,omitempty"`
// InstallHold (R-741, decision 45): an after_install app is held (the gate's door) from its fresh install until
// its known first login is replaced. Same record shape as SetupGate. See install_hold.go.
InstallHold *SetupGateRecord `yaml:"install_hold,omitempty" json:"install_hold,omitempty"`
// PreviousImages (R-736, decision 53): per service, the image the app ran BEFORE its last done update — kept on
// the box with the running one; older images of the app are deleted (image_retention.go).
PreviousImages map[string]InstalledImage `yaml:"previous_images,omitempty" json:"previous_images,omitempty"`
// DefaultLogin (v0.280.0, R-710) is the household's own word that it changed the template's known default
// login by hand. The page stops naming the default. See internal/web/known_login.go.
DefaultLogin *DefaultLoginRecord `yaml:"default_login,omitempty" json:"default_login,omitempty"`
@@ -434,6 +440,20 @@ func (m *Manager) DeployStack(req DeployRequest) (string, error) {
}
gate = g
}
// R-741: an after_install template is installed HELD, the file written before the first start, like the gate.
var hold *SetupGateRecord
if wantsInstallHold(&meta) {
h, err := m.prepareInstallHold(req.StackName, stack.ComposePath, env)
if err != nil {
clearDeploying()
if gate != nil {
_ = m.removeSetupGateFile(req.StackName)
}
m.logger.Printf("[ERROR] [stacks] Deploy %s REFUSED: the install hold could not be prepared: %v", req.StackName, err)
return "", util.MsgError("err.stacks.setup_gate_failed", err.Error())
}
hold = h
}
// Save app.yaml.
// CTRL-T2-1: persist the env now, but mark the ON-DISK state Deployed:false
@@ -455,6 +475,7 @@ func (m *Manager) DeployStack(req DeployRequest) (string, error) {
// reverts Deployed to false — so a failed deploy can never present as an app owed a restart.
DesiredState: DesiredStateRunning,
SetupGate: gate,
InstallHold: hold,
}
diskCfg := *appCfg