hub v0.167.0 source (not built, not deployed): D31 — the F-14 PBS auto re-issue reads the newest deletion of a host that held a key, a keyless install between no longer blocks it (R-1205); D30 — the new key's handshake clears wg_tunnel_stale (pinned), wording; wire gate entry; Part 1 stray-copy 9202 evidence
gates / gates (push) Successful in 15m46s

This commit is contained in:
admin committed 2026-10-22 21:04:42 +02:00
1 parent 96ac07e168
commit 84751cb0ce
18 files changed
+517 -13

No files matched your search

+16
View File
@@ -1,3 +1,19 @@
## v0.167.0 (unreleased — committed 2026-10-22/23 night, NOT built, NOT deployed) — D31 and the D30 alarm clear
- **D31 (operator 2026-10-22, R-1205).** The F-14 PBS auto-re-issue now reads the customer's newest deletion of a host
that HELD A KEY (`store.LatestKeyHoldingHostDeletion`, `escrow_acked = 1`) instead of the newest deletion of any
host. A later install deleted before it made a key (an un-acked record — `DeleteHost` refuses a host with an escrow
row unless acknowledged) owned no tenancy and no longer leaves the next install's whole-box tier waiting for the
operator's press (seen live 2026-10-21). Only keyless deletions, or none → the manual refusal, byte-unchanged. Tests
`TestLatestKeyHoldingHostDeletion_SkipsAKeylessInstall`, `TestPBSDR_D31_AKeylessInstallBetweenDoesNotBlockTheReissue`,
each red-proved.
- **D30 (R-1204).** The agent's re-key (felhom-agent `ae69ad4`) registers the new key on `POST /hosts/<id>/wg` (re-key
in place, unchanged); the new key's first handshake clears `wg_tunnel_stale` once — pinned by
`TestD30_TheAlarmClearsWhenTheNewKeyShakesHands` (red-proved). The stale text says an agent with D30 re-keys by itself
within about 15 minutes; the `wg_tunnel_ok` log line is INFO; the source comment no longer says the ok event is
mailed (it is stored, not mailed — review 2026-10-22 night).
- `scripts/wire_contract_gate.py`: `wireguard.last_handshake_age_s` left the unread list (the hub reads it since 0.166.0).
## v0.166.0 (unreleased — built 2026-10-22 night, NOT deployed; the deploy is the operator's yes) — the clock-ahead tunnel alarm (R-1204)
- **`wg_tunnel_stale` / `wg_tunnel_ok` (operator-only).** A box that ran once with its clock ahead stamps a WireGuard
+7 -4
View File
@@ -10,8 +10,11 @@ import (
// than the last one accepted from that peer), so the box's tunnel to ep0 — its PBS whole-box copies and the
// operator's door — stays dead until the real date passes the wrong one, while the box itself reports normally.
// Nothing said why. The hub now tells the OPERATOR once per host, edge-triggered from the host's newest tunnel event
// (the host_swap shape), and once when it is healthy again. Operator-only (notify.operatorOnlyEvents): the household
// cannot fix it. The repair (re-key the peer, or ep0 forgets the stored timestamp) is a separate decision.
// (the host_swap shape), and records once when it is healthy again (wg_tunnel_ok is an info event: stored, not
// mailed). Operator-only (notify.operatorOnlyEvents): the household cannot fix it. The repair is D30 (operator
// 2026-10-22): an agent with it makes a new key after 15 minutes of a future-stamped handshake and registers it here
// (re-key in place, the full-list push to ep0); the new key's first handshake clears this alarm — pinned by
// TestD30_TheAlarmClearsWhenTheNewKeyShakesHands.
const (
eventWGTunnelStale = "wg_tunnel_stale"
eventWGTunnelOK = "wg_tunnel_ok"
@@ -36,7 +39,7 @@ func wgTunnelStale(w *hostWireguard) (bool, string) {
return false, ""
}
if w.HandshakeFutureS != nil && *w.HandshakeFutureS > 0 {
return true, fmt.Sprintf("its last handshake is stamped %s in the FUTURE by the box's own clock — the clock ran ahead once, and ep0 refuses every newer handshake until that time passes", humanDuration(*w.HandshakeFutureS))
return true, fmt.Sprintf("its last handshake is stamped %s in the FUTURE by the box's own clock — the clock ran ahead once, and ep0 refuses every newer handshake until that time passes (an agent with the D30 repair makes a new key by itself within about 15 minutes)", humanDuration(*w.HandshakeFutureS))
}
if w.LastHandshakeAgeS != nil && *w.LastHandshakeAgeS > wgTunnelStaleAfterS {
return true, fmt.Sprintf("it has not shaken hands for %s while the box reports — the box's clock may have run ahead (then ep0 refuses it), or ep0 dropped the peer", humanDuration(*w.LastHandshakeAgeS))
@@ -78,7 +81,7 @@ func (h *Handler) checkWGTunnel(hostID, custID string, w *hostWireguard) {
default:
return
}
h.logger.Printf("[WARN] host %s: %s", hostID, typ)
h.logger.Printf("[%s] host %s: %s", map[bool]string{true: "WARN", false: "INFO"}[stale], hostID, typ)
det := map[string]any{"host_id": hostID}
if w.LastHandshakeAgeS != nil {
det["last_handshake_age_s"] = *w.LastHandshakeAgeS
+29
View File
@@ -76,3 +76,32 @@ func TestR1204_WhatIsJudged(t *testing.T) {
}
}
}
// D30 (R-1204): after the agent re-keys, the box reports a restarted tunnel with no handshake yet (nothing to judge),
// then the new key's first handshake — which clears the alarm once, and raises nothing new.
// RED-PROOF (2026-10-22 night): with the `!stale && wasStale` case removed from checkWGTunnel, this fails (no ok event).
func TestD30_TheAlarmClearsWhenTheNewKeyShakesHands(t *testing.T) {
h, st, _ := newTestHandler(t)
st.SaveCustomerConfig(&store.CustomerConfig{CustomerID: "c1", APIKey: "ckey", RetrievalPassword: "p"})
st.UpsertHost(&store.Host{HostID: "h1", CustomerID: "c1", APIKey: "HKEY"})
post := func(wg string) {
t.Helper()
if rr := do(h, http.MethodPost, "/host-report", "HKEY", tunnelReportBody("h1", wg)); rr.Code != http.StatusOK {
t.Fatalf("host-report = %d %s", rr.Code, rr.Body.String())
}
}
count := func(typ string) int {
evs, _ := st.GetEventsByType("c1", typ, time.Time{})
return len(evs)
}
post(`{"pubkey":"OLD","registered":true,"active":true,"last_handshake_age_s":0,"handshake_future_s":10800}`) // 3 h ahead
post(`{"pubkey":"NEW","registered":true,"active":true}`) // re-keyed, no handshake yet
if count(eventWGTunnelStale) != 1 || count(eventWGTunnelOK) != 0 {
t.Fatalf("stale %d ok %d, want 1 / 0 before the new key shakes hands", count(eventWGTunnelStale), count(eventWGTunnelOK))
}
post(`{"pubkey":"NEW","registered":true,"active":true,"last_handshake_age_s":20}`)
post(`{"pubkey":"NEW","registered":true,"active":true,"last_handshake_age_s":110}`)
if count(eventWGTunnelOK) != 1 || count(eventWGTunnelStale) != 1 {
t.Fatalf("stale %d ok %d, want 1 / 1 — the new key's handshake clears the alarm once", count(eventWGTunnelStale), count(eventWGTunnelOK))
}
}
+37
View File
@@ -281,3 +281,40 @@ func TestCountHostArtifacts(t *testing.T) {
t.Errorf("unknown host artifacts = %+v, want zero value", empty)
}
}
// D31 (R-1205): the F-14 gate's row is the newest deletion of a host that HELD A KEY; a later keyless install's
// un-acked deletion is skipped. RED-PROOF (2026-10-22 night): with the `AND escrow_acked = 1` filter removed, this
// fails (it returns the keyless gen2-host).
func TestLatestKeyHoldingHostDeletion_SkipsAKeylessInstall(t *testing.T) {
s := newTestStore(t)
seedHostWithArtifacts(t, s, "gen1-host", "cust-d31")
if err := s.DeleteHost("gen1-host", true); err != nil { // held a key → acked
t.Fatal(err)
}
if err := s.UpsertHost(&Host{HostID: "gen2-host", CustomerID: "cust-d31", APIKey: "k2"}); err != nil {
t.Fatal(err)
}
if err := s.DeleteHost("gen2-host", true); err != nil { // never made a key → un-acked even with the box ticked
t.Fatal(err)
}
rec, err := s.LatestKeyHoldingHostDeletion("cust-d31")
if err != nil || rec == nil {
t.Fatalf("LatestKeyHoldingHostDeletion = %+v, %v", rec, err)
}
if rec.HostID != "gen1-host" || !rec.EscrowAcked {
t.Errorf("row = %+v, want the key-holding gen1-host", rec)
}
if rec, err := s.LatestKeyHoldingHostDeletion("cust-never"); err != nil || rec != nil {
t.Errorf("no deletions → nil, got %+v, %v", rec, err)
}
s2 := newTestStore(t)
if err := s2.UpsertHost(&Host{HostID: "only-keyless", CustomerID: "cust-k", APIKey: "k"}); err != nil {
t.Fatal(err)
}
if err := s2.DeleteHost("only-keyless", true); err != nil {
t.Fatal(err)
}
if rec, err := s2.LatestKeyHoldingHostDeletion("cust-k"); err != nil || rec != nil {
t.Errorf("only a keyless deletion → nil (the manual path stays), got %+v, %v", rec, err)
}
}
+25
View File
@@ -3336,6 +3336,31 @@ func (s *Store) LatestHostDeletion(customerID string) (*HostDeletion, error) {
return &d, nil
}
// LatestKeyHoldingHostDeletion returns the customer's most recent deletion of a host that HELD A KEY — escrow_acked=1,
// the escrow-ack flow over a real escrow row (nil when there is none). D31 (operator 2026-10-22, R-1205): the F-14
// gate reads THIS row. An un-acked deletion is a host that never made a key (DeleteHost refuses a host with an escrow
// row unless acknowledged), so it orphaned no tenancy and must not block the re-issue for the next install — the
// escrow carry's own reasoning (escrow_carry.go). Pinned by TestLatestKeyHoldingHostDeletion_SkipsAKeylessInstall
// and TestPBSDR_D31_AKeylessInstallBetweenDoesNotBlockTheReissue.
func (s *Store) LatestKeyHoldingHostDeletion(customerID string) (*HostDeletion, error) {
var d HostDeletion
var deletedAt string
err := s.db.QueryRow(`
SELECT host_id, customer_id, deleted_at
FROM host_deletions WHERE customer_id = ? AND escrow_acked = 1
ORDER BY id DESC LIMIT 1`, customerID,
).Scan(&d.HostID, &d.CustomerID, &deletedAt)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
d.DeletedAt = parseSQLiteTime(deletedAt)
d.EscrowAcked = true
return &d, nil
}
// UpsertHost creates or updates a host identity (used by the admin mint). On
// conflict it updates only operator-settable identity fields + updated_at; it does
// NOT touch the reality columns (agent_version/last_report_at) or the inert intent
+7 -5
View File
@@ -235,11 +235,13 @@ func (s *Server) pbsdrProvisionAtom(ctx context.Context, customerID string, host
// half-torn earlier attempt, or the F-14 shape: host deleted, tenancy survived).
//
// F-14 gate (operator ruling 2026-07-13): auto-re-issue is permitted ONLY when the
// hub's own deletion record shows the tenancy's owning host — the customer's most
// recent host deletion — was removed through the escrow-ack flow. Acknowledged
// destruction is not silent re-keying; the old secret went down with the acked host.
// No record / un-acked record → the refusal below, byte-unchanged (manual path).
rec, derr := s.store.LatestHostDeletion(customerID)
// hub's own deletion record shows the tenancy's owning host was removed through the
// escrow-ack flow. Acknowledged destruction is not silent re-keying; the old secret went
// down with the acked host. D31 (operator 2026-10-22, R-1205): the owning host is the
// newest deleted host that HELD A KEY — a later install deleted before it made a key
// (an un-acked record) owned nothing and no longer blocks this. No key-holding record →
// the refusal below, byte-unchanged (manual path).
rec, derr := s.store.LatestKeyHoldingHostDeletion(customerID)
if derr != nil {
return "", fmt.Errorf("pbsdr: deletion-provenance lookup for %s: %w", customerID, derr)
}
+35 -1
View File
@@ -457,7 +457,7 @@ func TestPBSDR_F14AutoReissueOnAckedDeletion(t *testing.T) {
}
// F-14 scenario B (the never-silently-re-key law): a surviving tenancy WITHOUT an acked
// deletion record — none at all, or the latest one un-acked — keeps the current refusal and
// deletion record — none at all, or only un-acked ones (D31: a keyless install) — keeps the current refusal and
// records ZERO re-issue calls (the exact non-effect).
// RED-PROOF (§10 B): removing the record check in pbsdrProvisionAtom (auto-reissue
// unconditionally) → the zero-reissue assertions here fail.
@@ -499,6 +499,40 @@ func TestPBSDR_F14NoRecordNeverRekeys(t *testing.T) {
})
}
// D31 (operator 2026-10-22, R-1205 — seen live 2026-10-21): the tenancy's owning host was deleted through the escrow-ack
// flow, then a SECOND install was deleted before it made a key. The re-issue for the third install proceeds.
// RED-PROOF (2026-10-22 night): with the gate reading LatestHostDeletion again (the newest row, un-acked), this fails
// with the 502 refusal and zero re-issue calls.
func TestPBSDR_D31_AKeylessInstallBetweenDoesNotBlockTheReissue(t *testing.T) {
fake := &fakeTenancy{provisionErr: tenantsync.ErrTokenExists, secret: "REISSUED-SECRET"}
s, st, _ := newPBSDRServer(t, fake)
if err := st.UpsertHost(&store.Host{HostID: "peti-00-dead", CustomerID: "peti", APIKey: "oldkey"}); err != nil {
t.Fatal(err)
}
if _, _, err := st.SaveHostEscrow("peti-00-dead", []byte("opaque"), "fp", "posture", "2026-07-01T00:00:00Z", ""); err != nil {
t.Fatal(err)
}
if err := st.DeleteHost("peti-00-dead", true); err != nil {
t.Fatal(err)
}
if err := st.UpsertHost(&store.Host{HostID: "peti-00-short", CustomerID: "peti", APIKey: "shortkey"}); err != nil {
t.Fatal(err)
}
if err := st.DeleteHost("peti-00-short", true); err != nil { // no escrow row → an un-acked record
t.Fatal(err)
}
rr := postUpdate(t, s, url.Values{"dr_tier": {"on"}})
if rr.Code != 303 {
t.Fatalf("enable = %d (%s), want 303 — a keyless install between must not block the re-issue", rr.Code, rr.Body.String())
}
if fake.reissueCalls != 1 {
t.Errorf("reissue calls = %d, want 1", fake.reissueCalls)
}
if desc, _, _ := hostState(t, st); desc == nil || !desc.Enabled {
t.Fatalf("descriptor after the re-issue = %+v", desc)
}
}
func TestPBSDR_Reissue(t *testing.T) {
fake := &fakeTenancy{secret: "OLD-SECRET"}
s, st, logBuf := newPBSDRServer(t, fake)