v0.88.0: controller-driven escrow ceremony — --output=json machine mode (escrowCeremony extraction, text mode byte-identical), the ONE fixed argv (escrow.CeremonyArgs, shared by exec+manifest+FELHOM_ESCROW sudoers, pin-tested), localapi ceremony job (single-flight, 60s) + one-shot in-memory R claim (10min TTL, unclaimed_void) + preflight; escrow-ceremony capability (Critical, pbs_dr-gated)
This commit is contained in:
@@ -9,6 +9,8 @@ import (
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -99,6 +101,10 @@ type Options struct {
|
||||
// controller-pushed restic repo password (fork-4). "" → escrow.StagedResticPasswordPath() (the
|
||||
// canonical path the escrow-create ceremony reads). Injectable so the stage handler is testable.
|
||||
EscrowStagePath string
|
||||
// EscrowCeremony wires the controller-driven ceremony endpoints (v0.88.0): POST /escrow/ceremony
|
||||
// (+/status, /claim one-shot R) and GET /escrow/preflight. OPTIONAL — when nil, those endpoints
|
||||
// report "not configured".
|
||||
EscrowCeremony *EscrowCeremonyConfig
|
||||
// ControllerSwap runs guest commands (pct exec) for the agentic controller-update swap (Phase 1).
|
||||
// OPTIONAL — when nil, POST /controller/swap reports "not configured". Satisfied by *GuestBinder.
|
||||
ControllerSwap GuestExecutor
|
||||
@@ -240,6 +246,26 @@ type Server struct {
|
||||
// netReachable is the 2 s TCP endpoint pre-probe (sync fast-fail + classification tiebreak).
|
||||
netReachable func(proto storage.NetworkProtocol, server string) bool
|
||||
|
||||
// Controller-driven escrow ceremony (v0.88.0): the single job slot + the ONE-SHOT in-memory R
|
||||
// holder. R lives ONLY in escrowR (never in the job struct — snapshots must be structurally
|
||||
// incapable of carrying it) and is zeroed on claim, supersede, or TTL expiry. See
|
||||
// escrow_ceremony.go for the custody rules.
|
||||
escrowCeremony *EscrowCeremonyConfig
|
||||
escrowMu sync.Mutex
|
||||
escrowJob *escrowCeremonyJob
|
||||
escrowR []byte
|
||||
escrowRClaimed bool
|
||||
escrowRExpiry time.Time
|
||||
escrowDone <-chan struct{} // closes when the detached job finishes (tests wait on it)
|
||||
// ceremonyRun executes the fixed-argv sudo self-invocation (tests inject canned JSON).
|
||||
ceremonyRun ceremonyRunner
|
||||
// escrowSudoCheck is the preflight's list-mode grant probe (`sudo -n -l -- <argv>`).
|
||||
escrowSudoCheck func(ctx context.Context) error
|
||||
// escrowLookPath resolves a binary on PATH for preflight (tests inject).
|
||||
escrowLookPath func(file string) (string, error)
|
||||
// statFile reports whether a path exists (preflight's staged-secret item; tests inject).
|
||||
statFile func(path string) bool
|
||||
|
||||
baseCtx context.Context // for fire-and-forget backups; set in Run
|
||||
}
|
||||
|
||||
@@ -300,6 +326,14 @@ func NewServer(o Options) (*Server, error) {
|
||||
s.netMounted = storage.NetworkMountedAt
|
||||
s.netJournal = readUnitJournal
|
||||
s.netReachable = storage.NetworkEndpointReachable
|
||||
// Controller-driven escrow ceremony (v0.88.0): production seams; tests inject fakes.
|
||||
s.escrowCeremony = o.EscrowCeremony
|
||||
if s.escrowCeremony != nil {
|
||||
s.ceremonyRun = runCeremonySubprocess(s.escrowCeremony.SudoPath)
|
||||
s.escrowSudoCheck = checkCeremonySudoGrant(s.escrowCeremony.SudoPath)
|
||||
}
|
||||
s.escrowLookPath = exec.LookPath
|
||||
s.statFile = func(path string) bool { _, err := os.Stat(path); return err == nil }
|
||||
if o.ControllerSwap != nil {
|
||||
s.swap = NewControllerSwapper(o.ControllerSwap, o.ControllerSwapStateDir, o.Logger)
|
||||
}
|
||||
@@ -350,6 +384,13 @@ func (s *Server) Handler() http.Handler {
|
||||
// fork-4 hygiene: wipe the staged secret once escrowed (controller calls this on confirm). Idempotent.
|
||||
mux.HandleFunc("DELETE /escrow/stage-secret", s.withGuest(s.handleWipeStagedEscrowSecret))
|
||||
|
||||
// Controller-driven escrow ceremony (v0.88.0): preflight checklist, the detached root ceremony
|
||||
// job (fixed-argv sudo self-invocation), its status, and the ONE-SHOT in-memory R claim.
|
||||
mux.HandleFunc("GET /escrow/preflight", s.withGuest(s.handleEscrowPreflight))
|
||||
mux.HandleFunc("POST /escrow/ceremony", s.withGuest(s.handleEscrowCeremonyStart))
|
||||
mux.HandleFunc("GET /escrow/ceremony/status", s.withGuest(s.handleEscrowCeremonyStatus))
|
||||
mux.HandleFunc("POST /escrow/ceremony/claim", s.withGuest(s.handleEscrowCeremonyClaim))
|
||||
|
||||
// v0.83.0 observability: the agent's always-DEBUG capture ring, for the controller's
|
||||
// Debug page agent tab (same auth/self-scoping wrap as every sibling route).
|
||||
mux.HandleFunc("GET /debug/logs", s.withGuest(s.handleDebugLogs))
|
||||
|
||||
Reference in New Issue
Block a user