From 3380ac71d09108f394b02182f73ae6c5e2aa6797 Mon Sep 17 00:00:00 2001 From: saif <11242541+saifsmailbox98@users.noreply.github.com> Date: Wed, 21 Oct 2026 01:35:14 +0530 Subject: [PATCH 1/2] fix(agent-vault): count sleep time in leaf, session cache and upload link expiry Go's monotonic clock pauses while macOS (and Linux) sleeps, so deadlines built from time.Now() ignored time asleep. Strip the monotonic reading with Round(0) so the leaf cache, the session cache's grace and idle windows, and the upload link expiry follow the wall clock. --- packages/agentvault/ca.go | 4 ++- packages/agentvault/cache.go | 9 ++++--- packages/agentvault/cache_test.go | 26 ++++++++++++++++++++ packages/agentvault/proxy_hostname_test.go | 16 ++++++++++++ packages/agentvault/session_log.go | 3 ++- packages/agentvault/session_log_ship_test.go | 18 ++++++++++++++ 6 files changed, 70 insertions(+), 6 deletions(-) diff --git a/packages/agentvault/ca.go b/packages/agentvault/ca.go index c3abac4c1..5eed4f5c4 100644 --- a/packages/agentvault/ca.go +++ b/packages/agentvault/ca.go @@ -135,7 +135,9 @@ func (c *caManager) mintLeaf(hostname string) (tls.Certificate, error) { return tls.Certificate{}, err } - notAfter := time.Now().Add(leafTTL) + // Round(0) keeps the wall clock only: the monotonic clock pauses while the machine sleeps, + // which kept serving leaves past their NotAfter. + notAfter := time.Now().Add(leafTTL).Round(0) // A leaf must never outlive its issuer, or clients reject the chain as the root nears expiry. if notAfter.After(rootCert.NotAfter) { notAfter = rootCert.NotAfter diff --git a/packages/agentvault/cache.go b/packages/agentvault/cache.go index 947c6a0a0..eefbc1195 100644 --- a/packages/agentvault/cache.go +++ b/packages/agentvault/cache.go @@ -64,6 +64,7 @@ type resolvedService struct { substitutions []substitution } +// lastSeen and fetchedAt are wall-clock (Round(0)) so time asleep counts toward the grace and idle windows. type sessionEntry struct { sessionID string expiresAt *time.Time @@ -189,7 +190,7 @@ func (c *sessionCache) lookup(sessionToken string) ([]*resolvedService, *session delete(c.entries, key) delete(c.tokens, key) } else { - entry.lastSeen = time.Now() + entry.lastSeen = time.Now().Round(0) svcs, grant := entry.services, entry.sessionLog c.mu.Unlock() return svcs, grant, nil @@ -225,8 +226,8 @@ func (c *sessionCache) lookup(sessionToken string) ([]*resolvedService, *session expiresAt: result.ExpiresAt, services: result.Services, sessionLog: result.SessionLog, - lastSeen: time.Now(), - fetchedAt: time.Now(), + lastSeen: time.Now().Round(0), + fetchedAt: time.Now().Round(0), } c.tokens[key] = sessionToken return cacheLookup{services: result.Services, sessionLog: result.SessionLog}, nil @@ -331,7 +332,7 @@ func (c *sessionCache) refreshOne(key, token string) { entry.expiresAt = result.ExpiresAt entry.services = result.Services entry.sessionLog = result.SessionLog - entry.fetchedAt = time.Now() + entry.fetchedAt = time.Now().Round(0) } } diff --git a/packages/agentvault/cache_test.go b/packages/agentvault/cache_test.go index d15abb588..49bdb2be7 100644 --- a/packages/agentvault/cache_test.go +++ b/packages/agentvault/cache_test.go @@ -373,3 +373,29 @@ func TestARejectedProxyTokenIsNotReResolvedEveryRequest(t *testing.T) { t.Fatalf("20 requests against a revoked proxy made %d resolves, want 1", got) } } + +// == rather than Equal: Equal ignores the monotonic reading, which pauses while the machine sleeps. +func TestCacheTimesIgnoreTheSleepingClock(t *testing.T) { + resolver := &stubResolver{result: &resolveResult{SessionID: "s1", Services: []*resolvedService{serviceWithSecret("v")}}} + cache := newTestCache(resolver) + key := sessionKey("agv_token") + + if _, err := cache.get("agv_token"); err != nil { + t.Fatalf("get: %v", err) + } + if e := cache.entries[key]; e.fetchedAt != e.fetchedAt.Round(0) || e.lastSeen != e.lastSeen.Round(0) { + t.Fatal("a fresh entry carries a monotonic reading, so time asleep would not count toward its grace or idle window") + } + + if _, err := cache.get("agv_token"); err != nil { + t.Fatalf("get: %v", err) + } + if e := cache.entries[key]; e.lastSeen != e.lastSeen.Round(0) { + t.Fatal("a cache hit stamps lastSeen with a monotonic reading") + } + + cache.refresh() + if e := cache.entries[key]; e.fetchedAt != e.fetchedAt.Round(0) { + t.Fatal("a refresh stamps fetchedAt with a monotonic reading") + } +} diff --git a/packages/agentvault/proxy_hostname_test.go b/packages/agentvault/proxy_hostname_test.go index 6c680189f..359633de7 100644 --- a/packages/agentvault/proxy_hostname_test.go +++ b/packages/agentvault/proxy_hostname_test.go @@ -61,3 +61,19 @@ func TestOneHostMintsOneLeafWhateverTheCase(t *testing.T) { } } } + +// == rather than Equal: Equal ignores the monotonic reading, which pauses while the machine sleeps. +func TestALeafExpiryIgnoresTheSleepingClock(t *testing.T) { + key, cert, err := generateRootCa() + if err != nil { + t.Fatalf("generateRootCa: %v", err) + } + ca := newCaManager(key, cert) + + if _, err := ca.mintLeaf("api.example.com"); err != nil { + t.Fatalf("mintLeaf: %v", err) + } + if exp := ca.leafCache["api.example.com"].expiration; exp != exp.Round(0) { + t.Fatal("the cached leaf expiry carries a monotonic reading, so time asleep would not count toward it") + } +} diff --git a/packages/agentvault/session_log.go b/packages/agentvault/session_log.go index e2881b216..02500da71 100644 --- a/packages/agentvault/session_log.go +++ b/packages/agentvault/session_log.go @@ -545,7 +545,8 @@ func (r *sessionLogRecorder) sendShipment(ctx context.Context, next shipment, pa } res.posted = true res.postedURL = created.UploadURL - res.postedExpires = r.now().Add(time.Duration(created.ExpiresInSeconds) * time.Second) + // Round(0): the link expires by the wall clock, which keeps running while the machine sleeps. + res.postedExpires = r.now().Add(time.Duration(created.ExpiresInSeconds) * time.Second).Round(0) uploadURL = created.UploadURL } diff --git a/packages/agentvault/session_log_ship_test.go b/packages/agentvault/session_log_ship_test.go index 067494053..944c5fd47 100644 --- a/packages/agentvault/session_log_ship_test.go +++ b/packages/agentvault/session_log_ship_test.go @@ -2,6 +2,7 @@ package agentvault import ( "context" + "errors" "net/http" "net/http/httptest" "strconv" @@ -242,3 +243,20 @@ func TestAChunkAlreadyStoredCountsAsUploaded(t *testing.T) { t.Fatalf("a chunk that is already stored was treated as a failed upload: %v", err) } } + +// The real clock, not newTestLog's: its fake times never carry a monotonic reading, so the check would always pass. +// == rather than Equal: Equal ignores the monotonic reading, which pauses while the machine sleeps. +func TestAnUploadLinkExpiryIgnoresTheSleepingClock(t *testing.T) { + // A failing upload keeps the chunk queued, so the link it was given can be read back. + log := newSessionLogRecorder("proxy-1", &fakeShipper{putDefault: errors.New("bucket down")}) + log.record(testGrant("s1"), aRecord("api.github.com")) + log.flush(context.Background(), flushFinal) + + pending := log.spools["s1"].pending + if len(pending) != 1 { + t.Fatalf("%d chunks queued, want the one whose upload failed", len(pending)) + } + if exp := pending[0].urlExpires; exp.IsZero() || exp != exp.Round(0) { + t.Fatal("the upload link expiry carries a monotonic reading, so time asleep would not count toward it") + } +} From 15f2e41fdb152d731286bd38ae0b9fb0d50bfcf8 Mon Sep 17 00:00:00 2001 From: saif <11242541+saifsmailbox98@users.noreply.github.com> Date: Wed, 21 Oct 2026 04:09:28 +0530 Subject: [PATCH 2/2] address review comments --- packages/agentvault/ca.go | 6 +++--- packages/agentvault/cache.go | 2 ++ packages/agentvault/proxy_hostname_test.go | 14 ++++++++++++-- packages/agentvault/session_log.go | 4 +++- packages/agentvault/session_log_test.go | 11 +++++++++++ 5 files changed, 31 insertions(+), 6 deletions(-) diff --git a/packages/agentvault/ca.go b/packages/agentvault/ca.go index 5eed4f5c4..c75ac0c5d 100644 --- a/packages/agentvault/ca.go +++ b/packages/agentvault/ca.go @@ -135,9 +135,9 @@ func (c *caManager) mintLeaf(hostname string) (tls.Certificate, error) { return tls.Certificate{}, err } - // Round(0) keeps the wall clock only: the monotonic clock pauses while the machine sleeps, - // which kept serving leaves past their NotAfter. - notAfter := time.Now().Add(leafTTL).Round(0) + // Truncate drops the monotonic reading, which pauses while the machine sleeps and kept serving leaves + // past their NotAfter, and matches the whole seconds NotAfter is encoded in. + notAfter := time.Now().Add(leafTTL).Truncate(time.Second) // A leaf must never outlive its issuer, or clients reject the chain as the root nears expiry. if notAfter.After(rootCert.NotAfter) { notAfter = rootCert.NotAfter diff --git a/packages/agentvault/cache.go b/packages/agentvault/cache.go index eefbc1195..077ed46ca 100644 --- a/packages/agentvault/cache.go +++ b/packages/agentvault/cache.go @@ -65,6 +65,8 @@ type resolvedService struct { } // lastSeen and fetchedAt are wall-clock (Round(0)) so time asleep counts toward the grace and idle windows. +// Trade-off: a wall-clock step moves them too (forward re-resolves early, backward stretches grace while +// Infisical is unreachable), which is rarer and milder than sleep. type sessionEntry struct { sessionID string expiresAt *time.Time diff --git a/packages/agentvault/proxy_hostname_test.go b/packages/agentvault/proxy_hostname_test.go index 359633de7..d0beac236 100644 --- a/packages/agentvault/proxy_hostname_test.go +++ b/packages/agentvault/proxy_hostname_test.go @@ -2,6 +2,7 @@ package agentvault import ( "bytes" + "crypto/x509" "testing" ) @@ -70,10 +71,19 @@ func TestALeafExpiryIgnoresTheSleepingClock(t *testing.T) { } ca := newCaManager(key, cert) - if _, err := ca.mintLeaf("api.example.com"); err != nil { + leaf, err := ca.mintLeaf("api.example.com") + if err != nil { t.Fatalf("mintLeaf: %v", err) } - if exp := ca.leafCache["api.example.com"].expiration; exp != exp.Round(0) { + exp := ca.leafCache["api.example.com"].expiration + if exp != exp.Round(0) { t.Fatal("the cached leaf expiry carries a monotonic reading, so time asleep would not count toward it") } + parsed, err := x509.ParseCertificate(leaf.Certificate[0]) + if err != nil { + t.Fatalf("ParseCertificate: %v", err) + } + if !exp.Equal(parsed.NotAfter) { + t.Fatalf("the cache expires the leaf at %v but the certificate says %v", exp, parsed.NotAfter) + } } diff --git a/packages/agentvault/session_log.go b/packages/agentvault/session_log.go index 02500da71..d5b45aa02 100644 --- a/packages/agentvault/session_log.go +++ b/packages/agentvault/session_log.go @@ -147,7 +147,9 @@ func (r *sessionLogRecorder) record(g *sessionLogGrant, rec sessionLogRecord) { // Without the monotonic reading, Before and After compare wall time, which is what Ts shows. rec.at = now.Round(0) rec.Ts = rec.at.UTC().Format(time.RFC3339Nano) - spool.lastRecordAt = now + // rec.at, not now: idle close must count time asleep, while lastFlushAt stays monotonic so a + // backward clock step cannot stall the flush interval. + spool.lastRecordAt = rec.at if !r.admitLocked(g) { // Switching off says so when it happens; the proxy-wide cap is only reported here. diff --git a/packages/agentvault/session_log_test.go b/packages/agentvault/session_log_test.go index 69e99904e..be60a3cf5 100644 --- a/packages/agentvault/session_log_test.go +++ b/packages/agentvault/session_log_test.go @@ -1616,3 +1616,14 @@ func TestOneStuckRequestAtShutdownDoesNotHoldBackTheOtherSessions(t *testing.T) }) } } + +// The real clock, not newTestLog's: its fake times never carry a monotonic reading, so the check would always pass. +// == rather than Equal: Equal ignores the monotonic reading, which pauses while the machine sleeps. +func TestIdleCloseIgnoresTheSleepingClock(t *testing.T) { + log := newSessionLogRecorder("proxy-1", &fakeShipper{}) + log.record(testGrant("s1"), aRecord("api.github.com")) + + if at := log.spools["s1"].lastRecordAt; at != at.Round(0) { + t.Fatal("lastRecordAt carries a monotonic reading, so time asleep would not count toward idle close") + } +}