clear_hold (D64 = B) + the box command through the running controller (R-1343), R-1340, R-1341 — tests red first; commit only
gates / gates (push) Successful in 31s

- clear_hold: a fifth verb in the closed operator-action list; lifts a hold of any reason in the RUNNING
  controller (memory + settings.json together), forgets the update's held flag, drops a waiting put-back
  from the app-stop marker (copy kept). Refused on a bad name or no hold. No new event type.
- --clear-restore-hold asks the running controller over <data dir>/ctl/hold.sock (unix, 0600, peer uid
  checked, no TCP); edits settings.json only when no controller answers, and says so (in-guest restart).
- R-1340: the operator's HELD STOPPED mail is built from the hold record (restore kind, mixed cause,
  failed put-back step, database replay or not, kept copy) — never the option-C database story by default.
- R-1341: the OOM WARN line once per new kill (container, start time, kill count); event unchanged.
This commit is contained in:
admin committed 2026-10-29 12:49:46 +01:00
1 parent c7bb776e00
commit 5b3b541602
25 files changed
+1809 -83

No files matched your search

+42 -4
View File
@@ -21,9 +21,11 @@ import (
// problem, main.go). The result rides the next report as `operator_action_results: [{id, outcome,
// message}]`; the hub lists an action until its result has arrived, then stops.
//
// THE LIST IS CLOSED. Four actions, and run_job names one of four jobs. Nothing here deletes data,
// THE LIST IS CLOSED. Five actions, and run_job names one of four jobs. Nothing here deletes data,
// starts a countdown or shortens one — `03` §4 asks for a signing key only to destroy or overwrite the
// only copy, and none of these does. An unknown action, an unknown job or an argument out of range is
// only copy, and none of these does. clear_hold (D64 = B, operator ruling 2026-10-29 12:10) lifts an app's
// hold of ANY reason with no operator signature: it deletes nothing and starts nothing — the app stays
// stopped until the household (or the operator) starts it, and a waiting put-back's copy stays on disk. An unknown action, an unknown job or an argument out of range is
// answered `refused` and NOTHING is called. Pinned by TestOpActions_ClosedList (which fails when an
// entry is added, so a new action is a decision, not an edit) and TestOpActions_UnknownIsRefused.
//
@@ -38,6 +40,7 @@ const (
OpAbandonStop = "abandon_stop"
OpAbandonExtend = "abandon_extend"
OpRunJob = "run_job"
OpClearHold = "clear_hold"
OutcomeDone = "done"
OutcomeRefused = "refused"
@@ -57,7 +60,7 @@ const (
)
// operatorActionNames is THE closed list. Adding an entry fails TestOpActions_ClosedList by design.
var operatorActionNames = []string{OpOffsiteBackupNow, OpAbandonStop, OpAbandonExtend, OpRunJob}
var operatorActionNames = []string{OpOffsiteBackupNow, OpAbandonStop, OpAbandonExtend, OpRunJob, OpClearHold}
// operatorJobNames is the fixed set run_job may name — the scheduler's own job names (main.go).
var operatorJobNames = []string{"fill-watch", "offsite-integrity", "offsite-proof", "disk-health-check"}
@@ -83,6 +86,21 @@ func contains(list []string, v string) bool {
return false
}
// validHoldStack is clear_hold's argument check: the same alphabet as backup.isSafeStackName (report cannot import
// backup; REUSE.md „Safe-name validators").
func validHoldStack(s string) bool {
if s == "" || len(s) > 64 {
return false
}
for _, c := range s {
if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '_' {
continue
}
return false
}
return true
}
// ErrActionRefused, wrapped by a handler, turns its error into a `refused` outcome (the action could
// not START — e.g. an off-site run already in flight) instead of `failed` (it started and went wrong).
var ErrActionRefused = errors.New("refused")
@@ -101,7 +119,7 @@ type OperatorActionResult struct {
Message string `json:"message"`
}
// OperatorActionHandlers are the four doors, wired by main.go. A nil handler answers `refused`
// OperatorActionHandlers are the five doors, wired by main.go. A nil handler answers `refused`
// („not available on this box").
type OperatorActionHandlers struct {
// OffsiteBackupNow returns a refusal sentence when the run may not start (the household's
@@ -111,6 +129,9 @@ type OperatorActionHandlers struct {
AbandonExtend func(days int) (time.Time, error)
// RunJob is Scheduler.RunNow: an error = not started (unknown/running); the channel = the job's end.
RunJob func(name string) (<-chan error, error)
// ClearHold lifts the app's hold in the RUNNING controller (D64 = B, R-1343) and returns the result
// sentence. actionID names the action in the box's log. An error wrapping ErrActionRefused = nothing changed.
ClearHold func(stack string, actionID int64) (string, error)
// ResultReady sends an out-of-cycle report so a result reaches the hub in seconds (optional).
ResultReady func()
}
@@ -317,6 +338,23 @@ func (o *OperatorActions) dispatch(a OperatorAction) {
}
o.finish(a, outcomeOf(jerr), msg)
}()
case OpClearHold:
// The argument is checked HERE, before any handler is touched (the closed list's rule): one app name,
// the backup package's strict alphabet (letters, digits, '-' and '_', at most 64).
if !validHoldStack(a.Arg) {
o.finish(a, OutcomeRefused, "the argument must be one app name (letters, digits, '-' and '_')")
return
}
if o.h.ClearHold == nil {
o.finish(a, OutcomeRefused, "not available on this box")
return
}
msg, err := o.h.ClearHold(a.Arg, a.ID)
if err != nil {
o.finish(a, outcomeOf(err), err.Error())
return
}
o.finish(a, OutcomeDone, msg)
default:
o.finish(a, OutcomeRefused, "unknown action")
}