agent (unreleased): the report carries the box's clock at the moment of sending (R-1384, D79)
gates / gates (push) Successful in 46s
gates / gates (push) Successful in 46s
sent_at is stamped by Client.Report at every send, never by the collector. The hub reads it to tell a wrong box clock. Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
1 parent
386a0c4cf4
commit
ede5d0e6c7
4 files changed
+71
-2
No files matched your search
@@ -86,6 +86,7 @@ func (e *HTTPError) Error() string {
|
||||
// IS the heartbeat (locked decision 1). Errors are typed (transport vs HTTP) and
|
||||
// never include the bearer token.
|
||||
func (c *Client) Report(ctx context.Context, r *HostReport) (*ControlEnvelope, error) {
|
||||
r.SentAt = time.Now().UTC().Format(time.RFC3339Nano) // the box clock at SENDING (R-1384); see HostReport.SentAt
|
||||
body, err := json.Marshal(r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("hub: marshaling report: %w", err)
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package hub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// R-1384 (D79): the hub tells a wrong box clock by the time stamped at the moment of SENDING. A report whose collect
|
||||
// started long ago (an old reported_at) must still leave with a fresh sent_at, and a second send of the SAME report
|
||||
// struct must carry a new one — or a report that waited on the box reads as a clock that is behind.
|
||||
// COMPANION RED-PROOFS (observed, felhom.eu audits/night-build-2026-11-14/B/redproof-agent-sent-at.txt).
|
||||
func TestReport_StampsSentAtAtSending(t *testing.T) {
|
||||
var bodies []map[string]any
|
||||
c := testClient(func(r *http.Request) (*http.Response, error) {
|
||||
raw, _ := io.ReadAll(r.Body)
|
||||
var m map[string]any
|
||||
if err := json.Unmarshal(raw, &m); err != nil {
|
||||
t.Fatalf("body: %v", err)
|
||||
}
|
||||
bodies = append(bodies, m)
|
||||
return httpResp(200, `{"status":"ok"}`), nil
|
||||
})
|
||||
old := time.Now().Add(-40 * time.Minute).UTC()
|
||||
rep := &HostReport{HostID: "h", ReportedAt: old.Format(time.RFC3339), SentAt: old.Format(time.RFC3339Nano)}
|
||||
for i := 0; i < 2; i++ {
|
||||
if _, err := c.Report(context.Background(), rep); err != nil {
|
||||
t.Fatalf("Report: %v", err)
|
||||
}
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
var sent []time.Time
|
||||
for i, m := range bodies {
|
||||
s, _ := m["sent_at"].(string)
|
||||
at, err := time.Parse(time.RFC3339Nano, s)
|
||||
if err != nil {
|
||||
t.Fatalf("send %d: sent_at = %q, want an RFC3339 time", i, s)
|
||||
}
|
||||
if d := time.Since(at); d < 0 || d > 5*time.Second {
|
||||
t.Errorf("send %d: sent_at is %s old, want the moment of sending (reported_at was 40 minutes old)", i, d)
|
||||
}
|
||||
if m["reported_at"] != old.Format(time.RFC3339) {
|
||||
t.Errorf("send %d: reported_at changed to %v — the collect time is not the send time's to touch", i, m["reported_at"])
|
||||
}
|
||||
sent = append(sent, at)
|
||||
}
|
||||
if len(sent) == 2 && !sent[1].After(sent[0]) {
|
||||
t.Errorf("the second send carries %s, not after the first %s — the stamp is not taken per send", sent[1], sent[0])
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,13 @@ import (
|
||||
// server-side received_at is the hub's dead-man's-switch liveness signal. There is
|
||||
// no separate heartbeat endpoint.
|
||||
type HostReport struct {
|
||||
HostID string `json:"host_id"` // echoes config.Hub.HostID
|
||||
ReportedAt string `json:"reported_at"` // RFC3339, agent clock
|
||||
HostID string `json:"host_id"` // echoes config.Hub.HostID
|
||||
ReportedAt string `json:"reported_at"` // RFC3339, agent clock
|
||||
// SentAt (R-1384, D79) is this box's clock at the moment the report is SENT — stamped by Client.Report, never by
|
||||
// the collector. The hub compares it with its own clock to tell a wrong box clock. ReportedAt cannot serve: it is
|
||||
// stamped when the collect STARTS, and a collect that waits on Proxmox makes it old on a box whose clock is right.
|
||||
// Pinned by TestReport_StampsSentAtAtSending.
|
||||
SentAt string `json:"sent_at,omitempty"`
|
||||
AgentVersion string `json:"agent_version"`
|
||||
// AgentSHA256 is the sha256 of the binary this process is RUNNING (read through /proc/self/exe,
|
||||
// once per process), R-349. The version string cannot tell a hand-built proof binary from the
|
||||
|
||||
Reference in new issue
Block a user