feat(issues): add Kyverno as a source for /api/issues + MCP issues tool (T11)#723
Merged
Conversation
4f1f131 to
bbea180
Compare
nadaverell
added a commit
that referenced
this pull request
May 17, 2026
…e in /api/issues meta (T11 follow-up) Two reviewer findings on PR #723: 1. PolicyReport Subject dropped apiVersion/group, so Kyverno-sourced Issues lost Issue.Group. This broke RBAC consistency (the cluster- scoped SAR check uses {kind, group}) and let two CRDs with the same Kind across different groups collide on a single index entry. - Add Group to policyreports.Subject. - Switch the index key from audit.ResourceKey ("Kind/ns/name") to a group-prefixed "group/Kind/ns/name" form so distinct CRDs sharing a Kind don't collide. Group first because both group and namespace can be empty independently; encoding group last would leave cluster-scoped CRD keys ambiguous with namespaced core-group keys under any 3-part parse. - Derive Group from scope.apiVersion (scope-only path) and from results[].resources[].apiVersion (per-resource path). - FindingsFor now takes (group, kind, ns, name); only test callers touched (no production consumers besides All()). - Propagate Subject.Group → Issue.Group in fromKyverno. 2. /api/issues returned nil when the PolicyReport index was nil, collapsing four distinct states (not_installed / deferred / warmup / ready-but-empty) into one indistinguishable empty list. Option A: add k8s.GetKyvernoStatus() backed by an atomic that WarmupKyvernoPolicyReports records its decision into. The /api/issues handler and MCP issues tool emit meta.kyverno when the caller opted into Kyverno (include_kyverno=true or source=kyverno), so the SPA and agents can render the right copy ("Kyverno not installed" vs "Indexing in progress" vs "No violations" vs "Cluster too large — findings deferred"). ResetPolicyReportIndex clears the decision so context switches start a fresh detection pass. Tests pin: - Subject.Group population from scope.apiVersion + per-result apiVersion (TestBuildIndex_GroupFromScopeAPIVersion, _GroupFromResourceAPIVersion) - Group-keyed CRD collisions don't drop findings (TestBuildIndex_GroupCollisionAcrossCRDs) - Updated TestParseSubjectKey for 4-part keys (group/Kind/ns/name) - fromKyverno wires Subject.Group → Issue.Group (TestCompose_KyvernoGroupPropagated) - GetKyvernoStatus state machine across all four lifecycle states (TestGetKyvernoStatus_LifecycleTransitions) - ResetPolicyReportIndex clears the decision atomic (TestResetPolicyReportIndex_ClearsKyvernoDecision) - /api/issues emits meta.kyverno on opt-in, omits it on default request (TestIssuesHandler_KyvernoMetaEmittedOnOptIn, TestIssuesHandler_KyvernoMetaOmittedWhenNotRequested)
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 37c40a2. Configure here.
nadaverell
added a commit
that referenced
this pull request
May 18, 2026
…ption Bugbot on PR #723: the MCP `issues` tool description told agents to "pass include_audit=true / include_events=true / include_kyverno=true", but the issuesInput struct has NO such fields — only namespace, severity, source, kind, since, limit, filter. Agents following the description sent JSON keys that Go's decoder silently dropped, and the include_X opt-in path never actually worked over MCP. The previous round's docs clarification overreached: it described REST /api/issues query params (where include_X DOES exist as boolean shortcuts) as if they applied to the MCP input shape. They don't. Fix is to the description, not the input. Document the MCP-only path: to add an excluded source to defaults, list everything explicitly (source=problem,condition,kyverno). Note that REST has the shortcut flags so cross-surface readers aren't confused. No behavior change.
Surfaces Kyverno PolicyReport findings (read via pkg/policyreports.Index) through the unified issues envelope. Default-off — matches the audit / events opt-in pattern — to keep the default operator view focused on actionable problems rather than best-practice / policy noise. Lift via source=kyverno (or include_kyverno=true on the HTTP handler). Severity mapping is by Kyverno result: fail/error -> critical, warn -> warning, pass/skip omitted. The reporting-API `severity` field is intentionally ignored — it is policy-author-set, inconsistent across policies, and not aligned with the operator-actionable axis the Issue list represents. Index gains an All() iterator returning each indexed subject + findings as defensive copies in stable alphabetical key order, so consumers can enumerate without per-subject lookups and downstream output stays deterministic. Graceful when Kyverno is not installed (GetPolicyReportIndex() returns nil — emits zero issues, no error).
…e in /api/issues meta (T11 follow-up) Two reviewer findings on PR #723: 1. PolicyReport Subject dropped apiVersion/group, so Kyverno-sourced Issues lost Issue.Group. This broke RBAC consistency (the cluster- scoped SAR check uses {kind, group}) and let two CRDs with the same Kind across different groups collide on a single index entry. - Add Group to policyreports.Subject. - Switch the index key from audit.ResourceKey ("Kind/ns/name") to a group-prefixed "group/Kind/ns/name" form so distinct CRDs sharing a Kind don't collide. Group first because both group and namespace can be empty independently; encoding group last would leave cluster-scoped CRD keys ambiguous with namespaced core-group keys under any 3-part parse. - Derive Group from scope.apiVersion (scope-only path) and from results[].resources[].apiVersion (per-resource path). - FindingsFor now takes (group, kind, ns, name); only test callers touched (no production consumers besides All()). - Propagate Subject.Group → Issue.Group in fromKyverno. 2. /api/issues returned nil when the PolicyReport index was nil, collapsing four distinct states (not_installed / deferred / warmup / ready-but-empty) into one indistinguishable empty list. Option A: add k8s.GetKyvernoStatus() backed by an atomic that WarmupKyvernoPolicyReports records its decision into. The /api/issues handler and MCP issues tool emit meta.kyverno when the caller opted into Kyverno (include_kyverno=true or source=kyverno), so the SPA and agents can render the right copy ("Kyverno not installed" vs "Indexing in progress" vs "No violations" vs "Cluster too large — findings deferred"). ResetPolicyReportIndex clears the decision so context switches start a fresh detection pass. Tests pin: - Subject.Group population from scope.apiVersion + per-result apiVersion (TestBuildIndex_GroupFromScopeAPIVersion, _GroupFromResourceAPIVersion) - Group-keyed CRD collisions don't drop findings (TestBuildIndex_GroupCollisionAcrossCRDs) - Updated TestParseSubjectKey for 4-part keys (group/Kind/ns/name) - fromKyverno wires Subject.Group → Issue.Group (TestCompose_KyvernoGroupPropagated) - GetKyvernoStatus state machine across all four lifecycle states (TestGetKyvernoStatus_LifecycleTransitions) - ResetPolicyReportIndex clears the decision atomic (TestResetPolicyReportIndex_ClearsKyvernoDecision) - /api/issues emits meta.kyverno on opt-in, omits it on default request (TestIssuesHandler_KyvernoMetaEmittedOnOptIn, TestIssuesHandler_KyvernoMetaOmittedWhenNotRequested)
Reviewer's critical: an in-flight WarmupKyvernoPolicyReports goroutine
could stamp KyvernoStatusReady (or an early not_installed/deferred
decision) onto the atomic AFTER ResetPolicyReportIndex had wiped it for
a new cluster context. The decision writes lived outside the mutex, so:
1. Old-cluster Warmup releases mutex with index stored (L233).
2. Reset (new cluster) takes mutex, nils index, clears decision (L366).
3. Old Warmup's pending L253 store fires, writing "ready" to the
atomic — but the index is empty and the new cluster is still
warming up.
4. GetKyvernoStatus: index nil, decision "ready" → returns "ready"
for the new cluster while it's actually empty.
Fix is a generation counter (`kyvernoWarmupGen atomic.Int64`). Each
Warmup lambda captures the gen at entry; all decision/index writes go
through setDecision / publishReady helpers that re-check gen under the
mutex before storing. Reset bumps the gen inside its critical section
(line ~395), so any stale Warmup completion finds a mismatch and skips
its writes entirely. Closes both interleaving windows (Reset-during-
Warmup-publish AND Warmup-completes-after-Reset).
Also:
- gofmt: re-align Filters{} and anonymous-struct literals in
internal/server/issues_handler.go + issues_handler_test.go (longest
key changed in this branch).
- StoreKyvernoIndexForTest: panic on wrong type instead of silently
no-op'ing. Test-only hook; a wrong type is a test bug, not a runtime
condition to swallow.
Tests: TestResetPolicyReportIndex_BumpsWarmupGen pins that Reset
advances gen monotonically (the invariant the race protection rests
on). Existing TestResetPolicyReportIndex_ClearsKyvernoDecision still
passes — the visible "decision empty after Reset" contract is unchanged.
…) to close race Snapshot both policyReportInit and kyvernoWarmupGen under policyReportMu BEFORE entering Do(), rather than capturing myGen inside the lambda. Without this, Reset could interleave between the caller's read of policyReportInit and the lambda's Load of kyvernoWarmupGen — the lambda would capture the post-Reset gen, making subsequent gen checks falsely succeed and stamp the previous cluster's outcome onto the new cluster's atomics. 213fc8a only protected the publish-step gen check; this closes the earlier interleaving. Add structural-invariant coverage: - TestWarmupKyvernoPolicyReports_StaleGenSkipsWrites replicates the exact write protocol used by setDecision/publishReady and verifies a manually-bumped gen causes writes to be skipped. - TestWarmupKyvernoPolicyReports_SnapshotsOnceAndGenAtomically spins concurrent snapshotters and resetters under -race and asserts the (once, gen) pair stays coherent — a regression that drops either field from the snapshot-mutex would surface here.
The handler/tool docs previously implied `source=kyverno` was an
additive opt-in ("lifts include_kyverno"), but the implementation
treats Sources as a filter — source=kyverno returns ONLY kyverno
rows, not defaults+kyverno. Same for source=audit/event.
This commit aligns the docs with the implementation across all
three doc sites:
- internal/server/issues_handler.go: rewrite the source= /
include_X param block to call out filter-vs-additive explicitly,
and add kyverno to the "loud sources, opt-in" preamble.
- internal/mcp/tools.go: rewrite the `issues` tool description and
the `source` jsonschema, and fix the local comment near
IncludeAudit/Events/Kyverno hydration to explain that the MCP
source list doubles as both filter and collection trigger.
- internal/issues/issues.go: add a contract doc comment on
wantSource so the implementation site documents the contract.
Also renames TestCompose_KyvernoSourceListOptsIn (misleading — the
contract is "list narrows but does not opt in") and tightens its
docstring. No behavior changes; existing tests pass.
…ption Bugbot on PR #723: the MCP `issues` tool description told agents to "pass include_audit=true / include_events=true / include_kyverno=true", but the issuesInput struct has NO such fields — only namespace, severity, source, kind, since, limit, filter. Agents following the description sent JSON keys that Go's decoder silently dropped, and the include_X opt-in path never actually worked over MCP. The previous round's docs clarification overreached: it described REST /api/issues query params (where include_X DOES exist as boolean shortcuts) as if they applied to the MCP input shape. They don't. Fix is to the description, not the input. Document the MCP-only path: to add an excluded source to defaults, list everything explicitly (source=problem,condition,kyverno). Note that REST has the shortcut flags so cross-surface readers aren't confused. No behavior change.
61ee926 to
e92001b
Compare
nadaverell
added a commit
that referenced
this pull request
May 18, 2026
Two T6 fixes required after rebasing onto post-#723 (Kyverno) and post-#726 (RBAC reverse-lookup + Crossplane) main: - T11 (#723) threaded API group through the PolicyReport lookup, changing policyreports.Index.FindingsFor from (kind, ns, name) to (group, kind, ns, name). Updated resourcecontext.PolicyReportLookup interface, the build.go call site (passing ident.Group), the policyReportLookupAdapter in ai_handlers.go, and the test mock. Group threading is a strict improvement — two CRDs sharing kind+ns+name across API groups now get disjoint findings instead of one inheriting the other's. - internal/server/server_smoke_test.go acquired a `rbacv1` import on main (#726) at the same line our `networkingv1` import lands on this branch. Conflict resolution: keep both, sorted.
nadaverell
added a commit
that referenced
this pull request
May 18, 2026
…vider T11 (#723) added KyvernoFindings + KyvernoStatus to the issues.Provider interface so the composer can route PolicyReport findings into the unified issue stream and surface index-lifecycle status. Our test fake didn't implement either, so summarycontext tests stopped compiling after the rebase onto post-T11 main. Returning nil/"" is correct for these tests: BuildIssueIndex doesn't read Kyverno findings (kindFilter was dropped in the prior commit; the index buckets problem+condition only), and KyvernoStatus is consumed by the issues meta block — not by the index path under test.
nadaverell
added a commit
that referenced
this pull request
May 21, 2026
…vider T11 (#723) added KyvernoFindings + KyvernoStatus to the issues.Provider interface so the composer can route PolicyReport findings into the unified issue stream and surface index-lifecycle status. Our test fake didn't implement either, so summarycontext tests stopped compiling after the rebase onto post-T11 main. Returning nil/"" is correct for these tests: BuildIssueIndex doesn't read Kyverno findings (kindFilter was dropped in the prior commit; the index buckets problem+condition only), and KyvernoStatus is consumed by the issues meta block — not by the index path under test.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Wave 2 / Phase 4c. Adds Kyverno PolicyReport findings as a first-class source for
/api/issuesand the MCPissuestool, with explicit lifecycle signaling so an empty result is never ambiguous.What lands
pkg/policyreports/index.go—Indexover PolicyReport / ClusterPolicyReport CRs. Indexed bygroup/Kind/ns/name(group first because group AND namespace can each be empty independently — encoding group last leaves a cluster-scoped CRD key ambiguous with a namespaced core-group key under any 3-part parse). Group derived fromscope.apiVersionANDresults[].resources[].apiVersionso per-result resources targeting multiple subjects each get the right group.Subjectstruct carries{Group, Kind, Namespace, Name}— Group propagates all the way through toIssue.GroupviafromKyvernoininternal/issues/issues.go.internal/issues/— newSourceKyvernoconstant, composer integration,Composehonors?source=kyvernofilter.internal/server/issues_handler.go— REST/api/issues?source=kyvernoreturns Kyverno-only findings.internal/mcp/tools.go::handleIssues— MCPissuestool acceptssource=kyverno(andinclude_kyverno=truefor additive opt-in).Lifecycle signaling
Empty Kyverno results were silently empty whether Kyverno was unavailable, warmup-deferred, or genuinely had no findings — agents had no way to tell. Added
k8s.KyvernoStatusenum (not_installed/deferred/warmup/ready) backed by anatomic.Value. Both REST and MCP emitmeta.kyverno: "<status>"on the response only when the caller opted into Kyverno (source=kyvernoorinclude_kyverno=true). Default responses skip the meta to keep payload lean.Race protection: Reset vs Warmup
The Reset / Warmup interaction needed two rounds of hardening:
kyvernoWarmupGen(atomic.Int64) bumped insideResetPolicyReportIndex; allsetDecision/publishReadywrites inside the Warmup lambda check gen underpolicyReportMubefore storing.(policyReportInit, kyvernoWarmupGen)happens UNDERpolicyReportMuBEFOREDo()— closes the window where the lambda could capture the post-Reset gen and still write OLD-cluster data with the NEW gen.A
-racetorture test (4 resetters × 4 snapshotters × 500 iterations) plus a structural-invariant test pin the design.Source filter contract
source=Xfilters to ONLY the listed sources.include_X=trueadds X to the defaultproblem + conditionset without silencing. Docs inissues_handler.go, the MCPissuestool description, and thewantSourceimplementation comment all now spell this out — earlier wording (incl. the MCP jsonschema string "Sources are AND'd; opting one in does not silence the defaults") was misleading and asserted the opposite behavior.Wire-surface alignment with T6
Mirrors PR #721's speculative-surface trim of
pkg/resourcecontext/types.go— dropsContextRef.Reason/Source/Confidence, theRefReason/RefSourceenums,ResourceContext.Truncated, and two unusedOmittedReasonvalues. T11 doesn't reference the deleted fields; this keeps the shared types in sync across the wave-2 PRs so they don't conflict on merge regardless of order. RetainedOmittedCacheCold+OmittedNotInstalledper the explicitTODO(T10)ininternal/k8s/policy_reports.gothat plans to populate these when the diagnostic-tier Kyverno consumer arrives.Verification
go vet ./...+go test -count=1 -race ./internal/k8s/...+ fullgo test -count=1 ./...all green. Tests pin: group propagation from scope.apiVersion AND per-result resources[].apiVersion, Issue.Group via fromKyverno, status enum lifecycle, gen monotonicity, stale-gen write skip, Once/gen atomic snapshot under race.Note
Medium Risk
Adds a new Kyverno-backed issues source and modifies PolicyReport warmup/reset concurrency with new lifecycle state tracking; correctness depends on atomic/generation logic and could affect issue completeness or stale state across cluster switches.
Overview
Adds Kyverno PolicyReport/ClusterPolicyReport findings as an opt-in
kyvernosource for the unified issues surfaces (/api/issuesand the MCPissuestool), mappingfail/error→critical,warn→warning, and omittingpass/skip, while propagating the subject’s APIgroupto avoid kind collisions.Introduces Kyverno index lifecycle reporting (
not_installed/deferred/warmup/ready) and returns it asmeta.kyvernoonly when Kyverno is requested (include_kyverno=trueorsource=kyverno), plus clarifies/enforces thatsource=is a filter (not additive) and addsinclude_kyvernofor “defaults + kyverno” behavior.Hardens PolicyReport warmup vs context-switch reset with generation-guarded atomic writes and adds extensive unit tests (issues composition, group keying/index enumeration, lifecycle transitions, and race-protection), along with documentation updates noting Kyverno support and issues mapping.
Reviewed by Cursor Bugbot for commit e92001b. Bugbot is set up for automated code reviews on this repo. Configure here.