From 8b2eef10b49f07991ecba0bbcb4d54c62e2f2256 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sun, 12 Jul 2026 20:44:21 +0100 Subject: [PATCH] Normalize bounded APT agent clock skew --- internal/api/host_apt_action_result.go | 11 ++++++++++- internal/api/host_apt_action_result_test.go | 15 +++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/internal/api/host_apt_action_result.go b/internal/api/host_apt_action_result.go index ab8cfe7de..ae3804886 100644 --- a/internal/api/host_apt_action_result.go +++ b/internal/api/host_apt_action_result.go @@ -39,10 +39,19 @@ func hostAPTExecutionResult(resourceID, agentID, operation, output string, succe verificationTruth.ReasonCode = "stale_agent_readback" verificationTruth.Summary = "The agent readback was stale, skewed, or had invalid mutation chronology." } else { + evidenceObservedAt := afterObservedAt.UTC() + evidenceReceivedAt := receivedAt.UTC() + // Validation permits the agent clock to be slightly ahead of the + // server. Canonical evidence cannot claim an observation after its + // receipt, so conservatively bind bounded positive skew to the + // server receipt boundary. + if evidenceObservedAt.After(evidenceReceivedAt) { + evidenceObservedAt = evidenceReceivedAt + } evidence, err := unified.NormalizeActionEvidence(unified.ActionEvidence{ Version: unified.ActionEvidenceVersion, ID: operation + "-agent-readback", ObserverID: agentID, ObserverKind: "unified_agent", ObserverTrustDomain: "agent:" + agentID, ExecutorTrustDomain: "agent:" + agentID, - Method: "typed_read_after_write", SubjectID: resourceID, ObservedAt: afterObservedAt.UTC(), ReceivedAt: receivedAt.UTC(), + Method: "typed_read_after_write", SubjectID: resourceID, ObservedAt: evidenceObservedAt, ReceivedAt: evidenceReceivedAt, Summary: output, }) if err == nil { diff --git a/internal/api/host_apt_action_result_test.go b/internal/api/host_apt_action_result_test.go index 20fc09b14..89f2cc130 100644 --- a/internal/api/host_apt_action_result_test.go +++ b/internal/api/host_apt_action_result_test.go @@ -52,6 +52,21 @@ func TestHostAPTActionResultFutureVerifiedClaimFailsClosed(t *testing.T) { } } +func TestHostAPTActionResultBoundsPermittedPositiveClockSkewToReceipt(t *testing.T) { + now := time.Date(2026, 7, 12, 9, 0, 0, 0, time.UTC) + result, err := hostAPTExecutionResult("agent:host-1", "host-1", agentexec.HostStorageCleanupOperationPackageCache, "cleanup complete", true, true, agentexec.HostStorageCleanupVerificationVerified, true, false, false, false, false, now.Add(-time.Second), now.Add(time.Second), now, now) + if err != nil { + t.Fatal(err) + } + truth := result.ActionResultV2.Verification + if truth.Status != unified.ActionVerificationConfirmed || truth.EvidenceClass != unified.ActionEvidenceAgentAttested || len(truth.Evidence) != 1 { + t.Fatalf("bounded clock skew lost verified readback: %#v", truth) + } + if !truth.Evidence[0].ObservedAt.Equal(now) || !truth.Evidence[0].ReceivedAt.Equal(now) { + t.Fatalf("bounded clock skew was not conservatively normalized: %#v", truth.Evidence[0]) + } +} + func TestHostAPTActionResultStaleReadbackFailsClosed(t *testing.T) { now := time.Date(2026, 7, 12, 9, 0, 0, 0, time.UTC) result, err := hostAPTExecutionResult("agent:host-1", "host-1", agentexec.HostUpdateOperationInstall, "updates complete", true, true, agentexec.HostUpdateVerificationVerified, true, true, true, true, false, now.Add(-time.Hour-time.Minute), now.Add(-time.Hour), now, now)