Nothing in Pulse showed a paying customer what Patrol had done for them:
about 164 runs a month per install, findings raised, investigations and
fixes, and none of it summarised. GET /api/ai/patrol/digest rolls the last
N days (default 7, max 30) up from records Pulse already keeps: run
history, the findings store, Patrol-origin action audits, and the usage
cost store. It adds no telemetry and persists nothing. The payload
reports when the bounded run history no longer covers the window and when
model pricing is unknown, rather than quietly under-counting.
This is the first slice of the "Patrol weekly digest" named bet in the
pulse-pro demand ledger; the in-app "This week" card follows once its
browser pass is recorded. docs/PATROL_WEEKLY_DIGEST.md holds the design
note and the honest limits of each line.
status.json registers the patrol-value-visibility coverage gap, the
candidate lane, and its work claim. It also drops the second, identical
copy of the ai-provider-guided-setup coverage gap that landed with #1853;
the duplicate id fails the status audit on main for every pull request.
Project freshly minted installer command intent onto the stable host before returning remote config, and consume that intent once so later admin policy changes remain authoritative.
An OIDC provider upgraded from v5 keeps its v5 redirect URL
(/api/oidc/callback, the DefaultOIDCCallbackPath), so the IdP redirects
the browser back to the 3-segment legacy path carrying only code and
state, with no session cookie and no API token yet. The global auth
middleware only marked the 4-segment per-provider path public, so in
API-token-only mode (AuthUser=="" && AuthPass=="" && HasAPITokens())
the callback was rejected with "API token required via Authorization
header or X-API-Token header", and the route dispatcher 404ed the same
path.
The handler layer (extractOIDCProviderID) already maps the legacy
/api/oidc/login and /api/oidc/callback paths to the migrated legacy
provider, so only the public-path allowlist and the route dispatcher
needed to recognise the 3-segment form. Adds a regression test covering
both legacy paths in API-token-only mode, the exact configuration that
emitted the error.
Refs #1533
Manifest-backed MCP tools, prompts, and resources with surface affordance contracts; agent capability manifest and governance projection; API contract tests and capability route projection; operations-loop and intelligence-funnel telemetry; release-control subsystem documentation, registry, and tooling; licensing and configuration.
Two gaps found by exercising the MSP pilot path live on a throwaway
multi-tenant instance:
1. CheckAccess granted any authenticated principal access to the default
org, so a token bound to a client org could read the provider's own
default-org estate if it leaked from a client site. Org-bound tokens
now fall through to the explicit binding check for the default org;
authenticated users and legacy unbound tokens keep default-org access,
and binding "default" explicitly still grants it.
2. The webhook private-target allowlist (instance-wide system setting)
only ever reached the default org's notification manager on
startup/reload, and only the request-context org on settings update.
Tenant orgs' webhooks to private targets (per-client Gotify over VPN,
the canonical MSP alert route) failed SSRF validation with no org-side
remedy, and any allowlist died with a restart. Settings updates and
reloads now fan out to every live tenant manager via the new
MultiTenantMonitor.ForEachMonitor, and tenant monitors inherit the
persisted allowlist and public URL at creation.
Both fixes verified live: org-bound token vs default org returns 403;
client-org webhooks to a private target succeed after restart and for
orgs created after the allowlist was saved. MSP.md validation checklist
gains the default-org probe and the allowlist guidance; MULTI_TENANT.md
documents the binding semantics. Contracts updated for api-contracts,
security-privacy, and monitoring with adjacency notes for
agent-lifecycle, storage-recovery, and performance-and-scalability.
Dead-code sweep. Functions flagged unreachable by golang.org/x/tools/cmd/deadcode
and confirmed unused across pulse, pulse-enterprise, pulse-pro and pulse-mobile by
adversarial cross-repo verification. Cross-module reachability was checked
explicitly (only pkg/ exported symbols are importable by other modules; internal/
packages and _test.go files are not). go build, go vet and test-compile all pass.
RequirePermission was reading the authenticated username from
w.Header().Get("X-Authenticated-User"). The header is set by checkAuth
upstream, but response headers are mutable across the handler chain:
any middleware sitting between checkAuth and RequirePermission that
wrote that header would substitute an arbitrary identity, and the
RBAC authorizer would make its access decision against the
substituted value. Identity belongs on the request context, not in a
mutable response surface.
Read internalauth.GetUser(r.Context()) first — checkAuth already
calls attachUserContext on every auth path that produces a real user,
so the context is the authoritative source. Keep a defensive,
warn-logged fallback to the response header for the one upstream path
that sets the header but skips attachUserContext (the anonymous-user
flow), so the existing test contract is preserved while the warning
flags the source path to plug.
Regression test TestRequirePermissionUsesContextUsername constructs
the attack: a request whose context says "real-authenticated-user"
and whose response header says "evil-spoofed-user". The RBAC
authorizer must observe the context value.
CheckCSRF was skipping the CSRF check whenever the request carried
Authorization: Bearer, Authorization: Basic, or X-API-Token, without
validating the credential. An attacker on a cross-origin page could
fetch() any state-changing endpoint with credentials: 'include' plus an
arbitrary Authorization header — the browser would auto-attach the
victim's pulse_session cookie, the server would skip CSRF, and the
request would execute as the logged-in user. Full CSRF bypass for every
session-authenticated user.
CSRF protection exists because the browser auto-attaches the session
cookie. That cookie is the only auto-attached credential we issue, so
it is the only correct signal for whether CSRF applies. Header-based
auth is set explicitly per request and is not CSRF-vulnerable, but its
presence does not make a session-cookie-bearing request safe. Skip CSRF
only when no session cookie is present; otherwise require the token
regardless of any Authorization or X-API-Token header.
Tests: the three "header bypasses CSRF" cases in security_test.go were
passing only because they sent no session cookie (so the no-cookie path
returned true). Renamed those to make the no-cookie precondition
explicit. Added TestCheckCSRF_HeaderDoesNotBypassWhenSessionCookiePresent
in security_regression_test.go covering X-API-Token, Authorization:
Basic, Authorization: Bearer, and mixed-case Bearer with a session
cookie present — each must require a valid CSRF token.
Remove local upgrade-metrics API registration, settings payload wiring, startup store migration, and backend conversion recorder hooks from the normal product runtime.
Delete the retired conversion/funnel and metering packages from compiled licensing code, and extend diagnostics boundary audits and governance contracts so maintainer commercial analytics cannot return through Settings or diagnostics.
Make direct CheckAuth failures return explicit auth-required responses while preserving route-specific auth errors through shared response capture.
Align API contract tests with uncapped self-hosted monitoring, route-local config import/export auth, and marketed entitlement upgrade reasons.