Carry discovery freshness into the cloud-safe context

The pushed cloud-safe operational context told the model a resource's access
pattern, paths, and ports but not how OLD the discovery was — so the Assistant
could present a 2-week-old cached scan as current. For a monitoring assistant,
recency is the most important provenance signal.

FormatCloudSafeContext now appends "Last discovered: <age>" (via the existing
FormatDiscoveryAge helper, previously unused) when the timestamp is known, and
the push-path conversion (cloudSafeOperationalContext) carries UpdatedAt through.
A timestamp is non-identifying, so it adds no PII. Omitted when unknown.

- Tests: FormatCloudSafeContext freshness present/absent; cloudSafeOperationalContext
  carries UpdatedAt end of the push conversion (and still emits no PII).
- Contract: ai-runtime cloud-safe context must carry discovery age when known.

Follow-up: same freshness line on the local/full path (formatSingleDiscovery).
This commit is contained in:
rcourtman 2026-06-09 10:46:10 +01:00
parent a24e944f1b
commit 9ec52406ad
5 changed files with 47 additions and 4 deletions

View file

@ -1803,10 +1803,13 @@ deriving an older display status from `workflowStatusHistory`.
discovery data before choosing any tool. Drawer handoffs route their attached
resources through the same context-prefetch path as explicit `@`-mentions, so
on a cloud turn the resource's cloud-safe operational context (access pattern,
config/data/log paths, ports) reaches the model exactly as it does for an
`@`-mention; identifying fields (hostname, IP, alias) stay redacted at the
model boundary. The handoff path must not withhold operational context that
the `@`-mention path delivers.
config/data/log paths, ports, and discovery freshness/age) reaches the model
exactly as it does for an `@`-mention; identifying fields (hostname, IP, alias)
stay redacted at the model boundary. The cloud-safe context must carry the
discovery's age (a non-identifying timestamp) when known so the model can
caveat staleness rather than presenting cached operational detail as current.
The handoff path must not withhold operational context that the `@`-mention
path delivers.
Patrol deterministic triage signals are prioritized evidence seeds for the
configured model; they must not be described as a Pulse-authored final
diagnosis, proof that unflagged resources are healthy, or a reason to

View file

@ -977,6 +977,9 @@ func cloudSafeOperationalContext(d *tools.ResourceDiscoveryInfo) string {
DataPaths: d.DataPaths,
LogPaths: d.LogPaths,
Ports: ports,
// Carry freshness so the cloud-safe context can tell the model how old
// the discovery is (a timestamp is non-identifying).
UpdatedAt: d.UpdatedAt,
})
}

View file

@ -3,6 +3,7 @@ package chat
import (
"strings"
"testing"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/ai/modelboundary"
"github.com/rcourtman/pulse-go-rewrite/internal/ai/providers"
@ -145,6 +146,22 @@ func TestPrefetcherCloudContext_LocalRoutingUnaffected(t *testing.T) {
}
}
func TestCloudSafeOperationalContext_CarriesDiscoveryFreshness(t *testing.T) {
// The push-path conversion must carry UpdatedAt so the cloud-safe context
// can tell the model how old the discovery is.
d := homeAssistantDiscovery()
d.UpdatedAt = time.Now().Add(-2 * 24 * time.Hour)
out := cloudSafeOperationalContext(d)
if !strings.Contains(out, "Last discovered: 2 days ago") {
t.Fatalf("cloud-safe operational context must carry discovery freshness, got:\n%s", out)
}
// Freshness is non-identifying; PII still must not appear.
if strings.Contains(out, "delly-ha-host") || strings.Contains(out, "192.168.0.101") {
t.Fatalf("freshness must not bring PII into the cloud-safe context, got:\n%s", out)
}
}
func TestCloudContextPolicy_SharesOnCloudRoutingOnly(t *testing.T) {
if !(CloudContextPolicy{CloudRouting: true}).sharesCloudOperationalContext() {
t.Fatal("cloud routing must share PII-free operational context")

View file

@ -99,6 +99,13 @@ func FormatCloudSafeContext(d *ResourceDiscovery) string {
}
}
// Freshness: tell the model how old this discovery is so it can caveat
// staleness ("based on discovery from 2 days ago") instead of presenting
// cached operational detail as current. A timestamp is non-identifying.
if !d.UpdatedAt.IsZero() {
sb.WriteString("Last discovered: " + FormatDiscoveryAge(d) + "\n")
}
return strings.TrimSpace(sb.String())
}

View file

@ -315,4 +315,17 @@ func TestFormatCloudSafeContext(t *testing.T) {
t.Errorf("cloud-safe context leaked PII %q\n--- output ---\n%s", bad, out)
}
}
// No freshness line when the discovery carries no timestamp.
if strings.Contains(out, "Last discovered:") {
t.Errorf("cloud-safe context must omit freshness when UpdatedAt is zero\n--- output ---\n%s", out)
}
// Freshness lets the model caveat staleness when the timestamp is known.
fresh := *d
fresh.UpdatedAt = time.Now().Add(-3 * time.Hour)
freshOut := FormatCloudSafeContext(&fresh)
if !strings.Contains(freshOut, "Last discovered: 3 hours ago") {
t.Errorf("cloud-safe context must report discovery age\n--- output ---\n%s", freshOut)
}
}