mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-29 23:17:00 +00:00
Stop counting the Pulse UI as external agent use (#3492)
## Why `pulse_intelligence_external_agent_used_30d` is meant to show installs where an external agent or integration calls the agent capability routes. Pulse's own UI was being counted too: - The setup wizard creates a wildcard admin API token and keeps the browser tab on it (`SecurityStep.tsx`: "The admin token is already active"). - Kiosk links authenticate with `?token=`. - The UI calls `GET /api/config/nodes` and `GET /api/ai/patrol/findings`, both of them manifest capabilities, so normal browsing recorded provisioning and finding activity. In fleet telemetry for the week of 22 Oct, 33-49% of installs under 30 days old reported external-agent use, against 2-3% of older installs, and 97% of reporting installs showed node-list or finding-list activity. The counter's fall from 1,541 to 1,081 installs since September followed fewer new installs, not agents leaving. ## Change `recordExternalAgentActivity` now skips any request the browser marks `Sec-Fetch-Site: same-origin`. Only a page served by Pulse itself can produce that value, and page scripts cannot set it. Requests from scripts, MCP adapters (`pulse_mcp`) and dashboards on other origins still count. This deliberately changes telemetry semantics. Expect the counter to drop on installs that upgrade. The interpretation note in `pulse-pro/docs/TELEMETRY_SIGNALS.md` will record the break. ## Proof `TestExternalAgentActivityIgnoresFirstPartyBrowserRequests` checks that same-origin requests are skipped and that requests with no marker, `cross-site`, `same-site` or `none` are still recorded.
This commit is contained in:
@@ -10626,3 +10626,9 @@ org/authentication/licence boundaries remain. This changes no agent token,
|
||||
enrolment, credential or runtime operation. The API contract and
|
||||
`TestContractRBACIntegrityQueryAdmission` cover the registered fallback;
|
||||
`rbac_integrity_query_test.go` covers rejected and supported selections.
|
||||
|
||||
External-agent activity telemetry no longer records requests from a page Pulse
|
||||
itself served (`Sec-Fetch-Site: same-origin`), so the Pulse UI running on the
|
||||
setup admin token stops reading as external-agent use. This changes no agent
|
||||
token, enrolment, credential scope, authentication or runtime operation; only
|
||||
which requests write the content-free activity marker.
|
||||
|
||||
@@ -7145,6 +7145,14 @@ scope set. Read-only external agents using `monitoring:read` must therefore
|
||||
count for context/event-stream activity without being treated as action,
|
||||
settings, or approval-capable callers. Generic API token last-use timestamps
|
||||
are not canonical proof that the external-agent surface was used.
|
||||
Requests from a page Pulse itself served (browser `Sec-Fetch-Site:
|
||||
same-origin`) never record the marker, whatever token authenticated them: the
|
||||
setup wizard leaves the Pulse UI on the wildcard admin token and kiosk links
|
||||
authenticate with `?token=`, so the UI's own node-list and finding-list
|
||||
fetches otherwise counted as external-agent use and made the counter track
|
||||
new-install volume. Scripts, MCP adapters and dashboards on another origin
|
||||
still count. `TestExternalAgentActivityIgnoresFirstPartyBrowserRequests`
|
||||
covers both sides.
|
||||
The `pulse-mcp` adapter may stamp the content-free
|
||||
`X-Pulse-Agent-Surface: pulse_mcp` header on those same capability requests so
|
||||
the telemetry snapshot can expose adapter-specific recent use while keeping the
|
||||
|
||||
@@ -8343,3 +8343,8 @@ protect grouped metadata, responsive columns and bounded rendering. Browser
|
||||
proof must exercise the consuming list at desktop/narrow widths, inspect actual
|
||||
pixels, identity, state/age fit, touch/keyboard disclosure, filters and live
|
||||
updates. This is safer evidence inspection, not a restore or installed recovery.
|
||||
|
||||
External-agent activity telemetry now skips first-party browser requests
|
||||
(`Sec-Fetch-Site: same-origin`) before writing `external_agent_activity.json`.
|
||||
It writes fewer records to the same file and opens no new storage, backup or
|
||||
recovery path.
|
||||
|
||||
@@ -93,6 +93,9 @@ func (r *Router) recordExternalAgentActivity(req *http.Request, activity string,
|
||||
if r == nil || req == nil || strings.TrimSpace(activity) == "" {
|
||||
return
|
||||
}
|
||||
if isFirstPartyBrowserRequest(req) {
|
||||
return
|
||||
}
|
||||
token := getAPITokenRecordFromRequest(req)
|
||||
if !apiTokenCoversExternalAgentSurface(token, time.Now().UTC(), requiredScopes...) {
|
||||
return
|
||||
@@ -202,6 +205,17 @@ func externalAgentActivitySurface(req *http.Request) string {
|
||||
}
|
||||
}
|
||||
|
||||
// isFirstPartyBrowserRequest reports whether the request came from a page
|
||||
// Pulse itself served. The setup wizard leaves the browser authenticated with
|
||||
// the wildcard admin token, and kiosk links authenticate with ?token=, so the
|
||||
// Pulse UI's own node and finding fetches would otherwise count as external
|
||||
// agent use. Browsers set Sec-Fetch-Site and page scripts cannot override it;
|
||||
// scripts, MCP adapters and dashboards on another origin never send
|
||||
// same-origin.
|
||||
func isFirstPartyBrowserRequest(req *http.Request) bool {
|
||||
return strings.EqualFold(strings.TrimSpace(req.Header.Get("Sec-Fetch-Site")), "same-origin")
|
||||
}
|
||||
|
||||
func apiTokenCoversExternalAgentSurface(token *config.APITokenRecord, now time.Time, requiredScopes ...string) bool {
|
||||
if token == nil {
|
||||
return false
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/agentcapabilities"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/testutil"
|
||||
pkglicensing "github.com/rcourtman/pulse-go-rewrite/pkg/licensing"
|
||||
@@ -53,3 +54,41 @@ func TestMetricsHistoryTelemetryRequiresServedExtendedData(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// The setup wizard leaves the Pulse UI authenticated with the wildcard admin
|
||||
// token, so its own node and finding fetches must not read as external agent
|
||||
// use. Requests without a same-origin fetch marker still count.
|
||||
func TestExternalAgentActivityIgnoresFirstPartyBrowserRequests(t *testing.T) {
|
||||
persistence := config.NewConfigPersistence(t.TempDir())
|
||||
router := &Router{persistence: persistence}
|
||||
adminToken := &config.APITokenRecord{Scopes: []string{config.ScopeWildcard}}
|
||||
|
||||
for _, site := range []string{"same-origin", "Same-Origin"} {
|
||||
uiReq := httptest.NewRequest(http.MethodGet, "/api/config/nodes", nil)
|
||||
uiReq.Header.Set("Sec-Fetch-Site", site)
|
||||
attachAPITokenRecord(uiReq, adminToken)
|
||||
router.recordExternalAgentCapabilityActivity(uiReq, agentcapabilities.ListNodesCapabilityName)
|
||||
}
|
||||
|
||||
for _, site := range []string{"", "cross-site", "same-site", "none"} {
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/ai/patrol/findings", nil)
|
||||
if site != "" {
|
||||
req.Header.Set("Sec-Fetch-Site", site)
|
||||
}
|
||||
attachAPITokenRecord(req, adminToken)
|
||||
router.recordExternalAgentCapabilityActivity(req, agentcapabilities.ListFindingsCapabilityName)
|
||||
}
|
||||
|
||||
history, err := persistence.LoadExternalAgentActivityHistory()
|
||||
if err != nil {
|
||||
t.Fatalf("LoadExternalAgentActivityHistory: %v", err)
|
||||
}
|
||||
if len(history.Events) != 4 {
|
||||
t.Fatalf("expected only the four non-first-party requests to count, got %d: %+v", len(history.Events), history.Events)
|
||||
}
|
||||
for _, event := range history.Events {
|
||||
if event.Activity != config.ExternalAgentActivityFindingList {
|
||||
t.Fatalf("first-party UI request was counted: %+v", event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user