Skip to content

feat(topology): get_neighborhood MCP tool + REST endpoint with RBAC-pre-filtered BFS (T12)#724

Merged
nadaverell merged 19 commits into
mainfrom
feat/t12-neighborhood
May 18, 2026
Merged

feat(topology): get_neighborhood MCP tool + REST endpoint with RBAC-pre-filtered BFS (T12)#724
nadaverell merged 19 commits into
mainfrom
feat/t12-neighborhood

Conversation

@nadaverell
Copy link
Copy Markdown
Contributor

@nadaverell nadaverell commented May 17, 2026

Wave 2 / Phase 6. Adds the get_neighborhood agent surface — a profile-bounded subgraph expansion around a root resource, available as both MCP tool and /api/ai/neighborhood/... REST endpoint.

What lands

pkg/topology/neighborhood.goBuildNeighborhood(t, root, opts) and BuildNeighborhoodWithIndex(t, root, opts, idx, dp) with:

  • Two profiles (auto / all) mapping to EdgeType filter sets. auto picks a bounded edge set per root kind; all is the explicit escape hatch. (Earlier iterations had six profiles — management/networking/policy/security/all/auto — collapsed after benchmarks showed all's wire cost was negligible and agents rarely picked the granular profiles.)
  • Hops clamped to [1, 2]; MaxNodes defaults to 25, ceiling 200.
  • BFS produces deterministic ordering (root first, breadth-major) for stable agent output.

internal/server/neighborhood_handler.goGET /api/ai/neighborhood/{kind}/{namespace}/{name}?profile=…&hops=N&max_nodes=N&group=…. Mounted under /api/ai/* so agent-log middleware applies.

internal/mcp/tools_neighborhood.goget_neighborhood MCP tool with the same wire shape and MCP-side RBAC helpers.

RBAC pre-filter inside BFS

NeighborhoodOptions.Allow func(*Node) bool runs inside the BFS loop. Forbidden nodes never enter the frontier — they can't act as path-fragments bridging two allowed nodes, and they don't consume the MaxNodes truncation budget before being dropped. Subgraph.RBACDenied counts skipped candidates so the caller emits a single aggregated omission (deliberately NOT per-node — that would re-leak existence). The same gate applies to the ROOT node, so e.g. a denied Secret root → empty subgraph + RBACDenied=1 → 404, preserving existence-oracle protection.

Cluster-scoped + pseudo-kind RBAC

topology.ClusterScopedKinds (centralized in pkg/topology/cluster_scoped_kinds.go) is the single source of truth — both /api/topology strip and neighborhood per-node gate iterate the same table; previously each had its own copy that the NodeKind checklist warned to keep in sync. New LookupClusterScopedTopoKind(kind, group) resolves through pseudoKindFor then matches the table; both root preflight (REST + MCP) and per-node gates use it.

For multi-variant kinds like NodeClass (EC2/AKS/GCP) the SAR is per-variant: a user with EC2 RBAC sees ONLY EC2 NodeClass nodes, not AKS or GCP. Per-node gate keys on the node's actual apiVersion group.

Per-kind Secret SAR + Secret roots

KindSecret nodes get an additional per-namespace get secrets SAR (matching handleGetResource server.go:1499). Without this, a user with broad namespace access but no secret-read could see Secret nodes leaked via the topology graph. Neighborhood topology builds override IncludeSecrets=true so Secret roots are findable; the Allow gate then enforces per-caller RBAC.

Group-aware root lookup

BuildNeighborhoodWithIndex takes DynamicProvider and uses it to construct group-aware node IDs. findNodeByRef validates ref.Group against node apiVersion. pseudoKindFor(kind, group) accepts both Pascal singular (MCP) and lowercase singular/plural (REST URL paths after normalizeKind) so cross-group CRDs (Knative Service vs core, CAPI Cluster vs others, Istio Gateway vs Gateway API) resolve correctly on both surfaces.

Performance: inverted index

BuildNeighborhoodWithIndex threads *RelationshipsIndex; BFS uses idx.EdgesFor(id) for O(in-degree + out-degree) neighbor enumeration instead of scanning all topo.Edges per step. REST pulls the memoized index via s.topoMemo.GetIndex(...); MCP builds inline via IndexByResource(topo).

Pseudo-kind root preflight

Root preflight now checks topology.ClusterScopedKinds BEFORE ClassifyKindScope. Without this, an agent calling /api/ai/neighborhood/nodeclass/_/foo (legitimate — NodeClass is what get_topology returns) hit the namespace-required branch and got 400. Now: 403 without provider SAR, 200 with, 404 when the root isn't in topology — never 400.

Wire-surface alignment with T6

Mirrors PR #721's speculative-surface trim of pkg/resourcecontext/types.go — drops ContextRef.Reason/Source/Confidence, the RefReason/RefSource enums, ResourceContext.Truncated, and two unused OmittedReason values. T12's own Subgraph.Truncated stays (it's actually populated). The earlier redundant omitted: budget_exceeded emission alongside truncated: true in the neighborhood handlers was removed in c2f8d49. OmittedBudgetExceeded enum value retained for non-truncation budget overflows (e.g. Kyverno warmup cap, planned in T10).

Verification

go build ./... + go vet ./... + full go test ./... green. Tests pin: skip-forbidden, no-path-fragments, budget-protection, root denial + happy-path, CAPI vs CNPG group disambiguation (both directions), KnativeService pseudo-kind lookup (Pascal + lowercase + plural forms), NodeClass per-variant SAR (EC2 allowed / AKS denied with EC2-only RBAC), Secret root included with IncludeSecrets=true + authorized/unauthorized paths, NodeClass / NodePool root preflight returns 403 or 404 (never 400).


Note

Medium Risk
Adds new get_neighborhood MCP tool and /api/ai/neighborhood endpoint plus shared BFS/RBAC logic, which touches authorization-sensitive topology visibility and could leak or hide resources if the gates/indexing are wrong.

Overview
Adds a new resource neighborhood capability that returns a bounded BFS subgraph around a root resource, exposed as the get_neighborhood MCP tool and GET /api/ai/neighborhood/{kind}/{namespace}/{name} REST endpoint (with profile, hops, max_nodes, and optional group).

Implements pkg/topology neighborhood traversal with profile-based edge filtering, hop/node caps, deterministic output ordering, and an in-traversal Allow hook so RBAC-denied nodes are skipped during expansion (including the root) and reported via aggregated omitted/truncated signals.

Centralizes cluster-scoped topology RBAC mapping into topology.ClusterScopedKinds (removing duplicated tables in REST/MCP), tightens pseudo-kind and per-variant RBAC (e.g. NodeClass provider variants), and adjusts Secret handling so Secret roots are discoverable (IncludeSecrets=true) while still existence-hidden for unauthorized users via per-node checks. Also trims resourcecontext.ContextRef/omitted enums by removing unused fields and associated tests.

Reviewed by Cursor Bugbot for commit 875b356. Bugbot is set up for automated code reviews on this repo. Configure here.

@nadaverell nadaverell requested a review from hisco as a code owner May 17, 2026 07:52
Comment thread internal/server/neighborhood_handler.go
Comment thread pkg/topology/neighborhood.go Outdated
Comment thread internal/server/neighborhood_handler.go
Comment thread internal/server/neighborhood_handler.go
Comment thread internal/server/neighborhood_handler.go
Comment thread internal/server/neighborhood_handler.go
BuildNeighborhood does a BFS slice of the topology graph from a single
root, filtered by edge-type profile. Six profiles: management, networking,
policy, security (reserved/empty in v1), all, and auto (picks per root
kind — workloads get management+networking+policy, GitOps controllers get
management, Services/Ingresses get networking, PDB/NP get policy).

Hops is clamped to 2; max_nodes defaults to 25 and caps at 200. When the
node budget is hit mid-expansion the partial subgraph is returned with
Truncated=true and a `subgraph.nodes` omitted entry.

REST: GET /api/ai/neighborhood/{kind}/{namespace}/{name} under the
existing /api/ai/* subrouter so agent-log middleware applies. Per-user
RBAC sweep drops nodes the caller can't read and records them as
`subgraph.nodes[i]` omitted entries with reason=rbac_denied. Uses the
shared topology memoizer.

MCP: get_neighborhood mirrors the REST shape exactly, with the equivalent
MCP RBAC helpers (canReadClusterScopedKind, checkNamespaceAccess) and
the same omitted-entry semantics.
Reviewer caught that BuildNeighborhood was running full BFS over the
topology graph and then filterNeighborhoodForUser{,MCP} dropped
unauthorized nodes post-hoc. Two leaks resulted:

1. Path-fragment leak: a forbidden node B could act as a bridge in BFS
   between allowed A and allowed C. Post-filter dropped B but A and C
   stayed connected via no visible edge — leaking that "something
   you can't see connects these two."
2. Budget leak: forbidden nodes consumed the MaxNodes truncation budget
   before being dropped, causing allowed nodes further out to be
   truncated as a side channel.

Fix: add an `Allow func(*Node) bool` to NeighborhoodOptions and check
it inside the BFS loop. Forbidden nodes don't enter the frontier, can't
be path-fragments, and don't consume budget. The new
Subgraph.RBACDenied counts how many candidates were skipped so the
caller can emit a single aggregated `subgraph.nodes / rbac_denied`
omission. We intentionally do NOT track which specific nodes were
denied — surfacing those refs would re-introduce the existence-leak
this change exists to prevent.

Both REST and MCP handlers now pass Allow closures backed by their
respective per-user RBAC helpers. filterNeighborhoodForUser{,MCP} and
the strconv-based per-node `subgraph.nodes[i]` omission paths are
removed.

Tests: TestBuildNeighborhood_AllowSkipsForbidden,
TestBuildNeighborhood_AllowPreventsPathFragments,
TestBuildNeighborhood_AllowProtectsBudget pin all three properties.
…leAuto

Reviewer caught two inconsistencies in profile handling:

1. NeighborhoodOptions docstring claims "Zero values are replaced with
   sensible defaults: Profile=auto" — but edgeTypesForProfile mapped
   "" to ProfileAll. Worked accidentally because both REST and MCP
   handlers default Profile=ProfileAuto before passing through, so the
   "" branch was unreachable via the handlers — but a direct library
   caller (e.g. a future internal consumer) leaving Profile zero would
   silently get all-edges expansion instead of the documented auto.

2. Unknown profile strings — agent typos like ?profile=banana — fell
   through edgeTypesForProfile's default branch to ProfileAll on the
   REST side. MCP's resolveProfile already normalized unknown to
   auto. Inconsistent: a typo via REST would silently expand traversal
   to every edge type (the broadest possible search), while the same
   typo via MCP would narrow to a kind-appropriate profile.

Fix both at the source: edgeTypesForProfile now maps "" AND unknown
profiles to ProfileAuto. Callers wanting the broadest expansion must
pass ProfileAll explicitly. Test pins both behaviors (empty profile
matches auto; unknown profile matches auto; ProfileAll remains
strictly broader than auto for a Pod root).
BuildNeighborhoodWithIndex accepts the precomputed inverted index from
T23 and uses idx.EdgesFor(nodeID) for per-step neighbor lookups instead
of pre-building an adjacency map from t.Edges. BuildNeighborhood remains
as a thin nil-idx shim for tests and library callers without a Memoizer.

REST handleAINeighborhood now fetches the cached index via
Memoizer.GetIndex so the BFS shares the same index with
GetRelationshipsWithIndex / SynthesizeManagedBy on the same request.
MCP handleGetNeighborhood builds the index inline (no memoizer there)
and threads it through identically.
@nadaverell nadaverell force-pushed the feat/t12-neighborhood branch from 0279119 to b75cb24 Compare May 17, 2026 09:07
Comment thread internal/server/neighborhood_handler.go
Comment thread internal/server/neighborhood_handler.go
Comment thread internal/server/neighborhood_handler.go
Two reviewer blockers on the T12 neighborhood endpoint:

1. Secret leak via namespace gate. Both canReadNeighborhoodNode (REST)
   and canReadNeighborhoodNodeMCP only checked namespace access for
   namespaced nodes. The Radar SA can carry cluster-wide secrets RBAC
   (Helm release visibility) while the calling user has none — so the
   neighborhood graph leaked Secret nodes the user could not fetch
   directly. Mirror handleGetResource's gate: after namespace access
   passes, Secret nodes additionally require canRead("","secrets",ns,
   "get") (REST) or canReadInNamespace(ctx,"","secrets",ns,"get") (MCP).
   v1 covers Secret only — other kinds ride the namespace gate.

2. Group-blind root resolution. BuildNeighborhoodWithIndex constructed
   the root ID via buildNodeID(..., nil), losing CRD plural resolution,
   and findNodeByRef ignored ResourceRef.Group entirely. Two CRDs with
   the same kind+namespace+name but different API groups (e.g. CAPI
   cluster.x-k8s.io/Cluster vs Fleet cluster.fleet.io/Cluster) collided
   and the first match won. Thread the topology's DynamicProvider into
   BuildNeighborhoodWithIndex so buildNodeID resolves CRD plurals, and
   add a group filter in findNodeByRef driven by ResourceRef.Group
   (back-compat: empty group skips the filter, preserving current
   first-match behavior for callers that don't pass ?group=). Both
   REST and MCP handlers already parse the group input and put it on
   ResourceRef.Group, so the wiring just needs to honor it.

Tests:
- pkg/topology/neighborhood_test.go: SecretFilteredByAllow,
  GroupCollisionRoot, GroupEmptyRootBackCompat
- internal/server/neighborhood_rbac_test.go: per-kind Secret SAR
  enforced/allowed, ConfigMap rides namespace gate
- internal/mcp/tools_neighborhood_test.go: same coverage for the MCP
  helper, including no-auth passthrough
Comment thread internal/mcp/tools_neighborhood.go
…kind cluster-scope SAR

Three independent neighborhood security gaps closed in BuildNeighborhood and
its two handlers:

1. Root bypassed the Allow gate. A Secret root in a namespace the user can
   list still requires `get secrets` for that namespace — only the Allow
   predicate enforces per-kind tightening. Apply opts.Allow(rootNode) before
   adding the root to the included set; on denial return empty subgraph with
   RBACDenied=1 so handlers translate to 404 (preserves existence-hiding —
   user can't distinguish "doesn't exist" from "exists, can't read").

2. Direct rootID match ignored caller-supplied group. Two CRDs sharing the
   lowercase plural (CAPI cluster.x-k8s.io/Cluster vs CNPG
   postgresql.cnpg.io/Cluster) collide on the synthesized nodeID — whichever
   was inserted last wins in nodeByID. Validate the candidate's apiVersion
   group against root.Group; on mismatch fall through to findNodeByRef.

3. findNodeByRef missed pseudo-kinds. KNative serving.knative.dev/Service is
   stored under NodeKind="KnativeService", same for CAPI/Istio variants —
   the direct kind comparison ref.Kind="Service" vs n.Kind="KnativeService"
   never matched. Added pseudoKindFor(kind, group) translating API (kind,
   group) tuples to the topology pseudo-kind label before comparison.

4. NodeClass and other pseudo-kind cluster-scoped nodes leaked via the
   "unclassified + no namespace → allow" fallback in canReadNeighborhoodNode
   (REST) and canReadNeighborhoodNodeMCP. ClassifyKindScope can't recognize
   "NodeClass" — it's a topology-only label, the real K8s resources are
   EC2NodeClass / AKSNodeClass / GCPNodeClass. Added per-package helpers
   (canReadClusterScopedTopoKind / -MCP) that consult the existing
   clusterScopedTopologyKinds table BEFORE ClassifyKindScope. Allow if any
   provider variant present in discovery passes the get-SAR. Mirrors the
   topology-strip semantics for /api/topology gating.

Tests:
- pkg/topology: root denial via Allow returns empty + RBACDenied=1; root
  allow leaves expansion intact; group-aware direct-match collision picks
  CAPI vs CNPG correctly; KnativeService pseudo-kind root lookup hits
  serving.knative.dev/v1 over a same-named core Service.
- internal/server: NodeClass denied without provider get-SAR; allowed with
  any one provider SAR; KnativeService (namespaced pseudo-kind) rides on
  namespace gate.
- internal/mcp: mirror suite for the MCP gate.
Comment thread internal/server/neighborhood_handler.go
…r-scoped kinds table

Reviewer's critical (#724 second pass): pseudoKindFor switched on Pascal-case
kind strings but REST's normalizeKind always lowercases URL paths. So a
GET /api/ai/neighborhood/services/prod/api?group=serving.knative.dev
arrived at pseudoKindFor as ("services", "serving.knative.dev"), fell
through the Pascal-only switch, and findNodeByRef then compared topology
KnativeService against "services" — no match, silent 404. MCP was fine
because displayKindForMCP normalizes back to Pascal singular.

Fix is to accept both lowercase singular AND lowercase plural for every
pseudo-kind branch (REST sends plurals after normalizeKind; MCP sends
Pascal singular; both should resolve). Existing
TestBuildNeighborhood_PseudoKindRootLookup grows a sub-test matrix
over kind=Service / service / services so a regression on either
surface fails loudly.

Also Important: centralize clusterScopedTopologyKinds into a new
pkg/topology/cluster_scoped_kinds.go and have BOTH internal/server/server.go
AND internal/mcp/tools.go import topology.ClusterScopedKinds. The
checklist comment on NodeKind already warned "add an entry to
clusterScopedTopologyKinds in BOTH" — exactly the drift hazard we now
eliminate by having one source of truth. Four iteration sites updated:
two deniedClusterScopedTopoKinds + two canReadClusterScopedTopoKind.

Also tightened the MCP get_neighborhood schema description on the
`group` field to call out the three known collision groups (Knative /
CAPI / Istio) so agents know when to pass it.
…t path

canReadClusterScopedTopoKind(MCP) iterated ALL ClusterScopedKinds entries
under a NodeKind and returned true on the first passing SAR. For NodeClass
(EC2 / AKS / GCP) a user with EC2 RBAC saw AKS and GCP NodeClass nodes
too. The helpers now take the full *topology.Node, derive the provider
group from Data["apiVersion"] (existing apiVersionGroup / apiVersionGroupMCP
helper), and SAR the single (Kind, Group) row in ClusterScopedKinds. For
single-entry kinds (NodePool, GatewayClass, PV, ...) this collapses to
exactly one SAR — same behavior as before.

handleAINeighborhood + MCP handleGetNeighborhood relied on
DefaultBuildOptions which sets IncludeSecrets=false, so root lookup for
kind=secret always produced an empty subgraph and 404 even for authorized
users. Both handlers now override IncludeSecrets=true after
DefaultBuildOptions(). The Allow gate (canReadNeighborhoodNode's per-
namespace `get secrets` SAR) still 404s unauthorized callers via the
empty-subgraph path, so existence-hiding is preserved. The Memoizer's
memoKey already includes IncludeSecrets so this caches in a separate slot
from the IncludeSecrets=false topology used elsewhere.

Tests:
- pkg/topology: TestBuildNeighborhood_NodeClassPerVariantSAR pins the
  per-variant Allow semantics at the topology layer (EC2 allowed, AKS
  denied via apiVersion-group, RBACDenied counter bumps).
- internal/server + internal/mcp: NodeClassPerVariantDeniesWrongProvider
  pins the helper denying AKS to an EC2-RBAC user, allowing EC2.
- internal/server: Neighborhood_SecretRootIncluded uses the smoke-test
  cache (extended with a Volumes Secret ref so the builder surfaces the
  Secret node) and asserts 200 for an authorized user, 404 for an
  unauthorized one via the empty-subgraph existence-hiding path.
- internal/mcp: NeighborhoodMCP_SecretRootIncluded mirrors the REST test
  using a fresh fake-backed cache with a referencing Deployment.
… (NodeClass, etc.)

URL kinds like nodeclass / nodepool / nodeclaim are topology pseudo-kinds —
synthesized labels that aggregate real K8s variants (EC2NodeClass /
AKSNodeClass / GCPNodeClass for NodeClass; karpenter.sh/nodepools for
NodePool, …). ClassifyKindScope doesn't recognize these labels, so root
preflight classified them as namespaced and rejected
/api/ai/neighborhood/nodeclass/_/foo with 400 "namespace is required"
even though the agent naturally sees them in get_topology output.

Adds topology.LookupClusterScopedTopoKind(kind, group) — central kind-only
lookup (pseudoKindFor resolution + case-insensitive table match) that
returns every ClusterScopedKinds row matching the (possibly pseudo) kind.
Both REST and MCP root preflight now consult this BEFORE
ClassifyKindScope; on hit, a kind-only SAR helper iterates the returned
entries and allows on any pass (matching topology-strip semantics: a
single provider grant is sufficient at the kind level; the per-node Allow
gate then drops variants the user can't read).

Discovery filter mirrors the existing per-node helper — entries whose CRD
is absent from discovery are skipped, and when all matches are filtered
out we fall through to allow (the topology builder wouldn't have surfaced
any node for an unprivileged SA either).

Tests pin both shapes:
- REST/MCP integration: /api/ai/neighborhood/nodeclass/_/foo +
  /api/ai/neighborhood/nodepool/_/foo no longer return 400; instead 403
  without provider SAR, 404 (BFS empty) with provider SAR.
- Direct helper tests for NodeClass (multi-row) and NodePool (single-row)
  pin the SAR-each-entry semantics.
… root.Kind in response

Two Bugbot findings on PR #724:

1. REST parseNeighborhoodOptions cast the profile query param directly to
   topology.Profile, while MCP routed through resolveProfile (lowercase +
   trim + validate, fall back to ProfileAuto). `?profile=Management`,
   `?profile=garbage`, or any case-mismatch fell through
   edgeTypesForProfile's default branch (allEdgeTypes) and silently
   exposed more topology edges than the caller intended. Lifted
   resolveProfile to topology.ResolveProfile (exported) so both surfaces
   normalize identically. MCP's local resolveProfile becomes a thin
   shim. New test pins case-insensitive matching, whitespace trim, and
   unknown→auto fallback.

2. REST handler set root.Kind via normalizeKind (lowercase) while
   subgraph nodes carry display-form NodeKind values. Within-response
   `root.kind: "pod"` vs `subgraph.nodes[0].kind: "Pod"` broke
   case-sensitive matching, and diverged from MCP despite the header
   comment claiming both surfaces "parse identically." Now uses the
   resolved root node's actual NodeKind for the response — single
   source of truth from the topology graph itself.
Comment thread internal/mcp/tools_neighborhood.go
REST got this fix earlier (commit 10ac2b6) but MCP shipped the same
shape bug: displayKindForMCP only normalizes built-in kinds via
normalizeDisplayKind's map, so topology pseudo-kinds like "nodeclass"
or "nodepool" pass through lowercase. Subgraph nodes carry the real
display-form NodeKind ("NodeClass"), so the response's root.kind would
disagree with subgraph.nodes[0].kind within the same payload.

Mirror REST's fix: after the BFS lookup, set rootResp.Kind from
sub.Nodes[0].Kind — single source of truth from the topology graph.
Both surfaces now produce identical wire shapes for pseudo-kinds.
Comment thread internal/server/neighborhood_handler.go
…h MaxNodes

Bugbot on PR #724: parseNeighborhoodOptions's doc claimed "clamps (hops max 2,
max_nodes floor 1 / ceiling 200)" but the code only enforced the MaxNodes
ceiling. BFS clamps Hops internally via neighborhoodMaxHops so behavior was
correct, but the asymmetry was confusing and the doc was misleading.

Add the explicit `if opts.Hops > 2 { opts.Hops = 2 }` clamp at the handler
level in both REST and MCP — keeps opts.Hops correct if anything inspects
or logs it before BFS, matches the doc, and makes the two budget fields
symmetric. New TestParseNeighborhoodOptions_HopsClamp pins the behavior.
… topology

Dedup the three byte-equivalent split-on-first-slash helpers Bugbot flagged:
- internal/server/neighborhood_handler.go::apiVersionGroup
- internal/mcp/tools_neighborhood.go::apiVersionGroupMCP
- pkg/topology/neighborhood.go::nodeAPIGroupFromData (inner loop)

Lifted into one exported topology.APIVersionGroup. REST + MCP call it
directly; nodeAPIGroupFromData becomes a thin wrapper that reads
Data["apiVersion"] and delegates the parsing. Test moved to pkg/topology
next to the canonical implementation; extended to cover multi-slash and
leading-slash edge cases the originals omitted.

Doing this inline rather than deferring to a post-wave PR — the
duplicates are local to T12's clone (not cross-PR), and the maintenance
hazard ("inconsistent bug fixes across copies") only stops once there's
exactly one implementation.
Comment thread pkg/topology/cluster_scoped_kinds.go Outdated
Bugbot caught dead code. The RBAC tuple-selection refactor (commit
93f1c20) introduced RBACTuplesForKind / RBACTuplesForNode which
superseded the older LookupClusterScopedTopoKind helper, but I left the
old function in place. No callers remain in the codebase. Delete it
outright (no deprecation alias — saved feedback rule) and clean up the
two doc comments that referenced it.
Comment thread pkg/topology/cluster_scoped_kinds.go
Bugbot caught it: the considered++ and tuples=append() pair always fire
together inside the loop body, so len(tuples) == 0 already implies
considered == 0 after the loop. The variable was never inspected; the
comments referenced it as if it carried independent signal. Delete and
simplify the comment to reference len(tuples) directly.
Mirror the T6 (#721) wire-surface cleanup so types.go stays in sync
across the in-flight wave-2 PRs.

Deletes:
- ContextRef.Reason / RefReason enum
- ContextRef.Source / RefSource enum
- ContextRef.Confidence
- ResourceContext.Truncated
- OmittedKindUnsupported, OmittedProviderDisabled

Note: OmittedBudgetExceeded stays on the wire — c2f8d49 already
removed the redundant emission alongside truncated:true. The enum
value remains for non-truncation budget overflows (e.g. Kyverno
warmup cap, planned in T10).
Comment thread internal/mcp/tools.go Outdated
Comment thread pkg/topology/neighborhood.go
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 3 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit a1400aa. Configure here.

Comment thread pkg/topology/neighborhood.go
- MCP get_neighborhood tool description previously advertised six
  profile values (management/networking/policy/security/all/auto)
  but ResolveProfile only accepts auto+all. The other four silently
  fell back to auto, misleading agents that requested e.g.
  profile=management expecting just the owner chain. c2f8d49 fixed
  the input struct's jsonschema tag but missed the top-level tool
  description. Description now matches implementation.

- Avoid the redundant findNodeByRef call in BuildNeighborhoodWithIndex
  when root.Group == "" and the buildNodeID lookup missed: the
  original code called findNodeByRef twice with identical args in
  the lookup-miss-no-group case. Restructured so the fallback only
  runs once.

- Skip re-invoking opts.Allow on denied nodes. Denied IDs weren't
  added to `included`, so a denied node reached via multiple edges
  re-ran Allow each time. Cache hits keep it cheap, but the wasted
  work is proportional to in-degree of denied nodes. Now checks
  deniedIDs before the Allow call.
@nadaverell nadaverell merged commit 32761d2 into main May 18, 2026
8 checks passed
@nadaverell nadaverell deleted the feat/t12-neighborhood branch May 18, 2026 17:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants