feat(topology): get_neighborhood MCP tool + REST endpoint with RBAC-pre-filtered BFS (T12)#724
Merged
Conversation
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.
0279119 to
b75cb24
Compare
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
…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.
…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.
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.
…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.
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.
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).
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 3 total unresolved issues (including 2 from previous reviews).
❌ 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.
- 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.
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 6. Adds the
get_neighborhoodagent 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.go—BuildNeighborhood(t, root, opts)andBuildNeighborhoodWithIndex(t, root, opts, idx, dp)with:auto/all) mapping toEdgeTypefilter sets.autopicks a bounded edge set per root kind;allis the explicit escape hatch. (Earlier iterations had six profiles —management/networking/policy/security/all/auto— collapsed after benchmarks showedall's wire cost was negligible and agents rarely picked the granular profiles.)Hopsclamped to [1, 2];MaxNodesdefaults to 25, ceiling 200.internal/server/neighborhood_handler.go—GET /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.go—get_neighborhoodMCP tool with the same wire shape and MCP-side RBAC helpers.RBAC pre-filter inside BFS
NeighborhoodOptions.Allow func(*Node) boolruns 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.RBACDeniedcounts 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 inpkg/topology/cluster_scoped_kinds.go) is the single source of truth — both/api/topologystrip and neighborhood per-node gate iterate the same table; previously each had its own copy that the NodeKind checklist warned to keep in sync. NewLookupClusterScopedTopoKind(kind, group)resolves throughpseudoKindForthen 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
apiVersiongroup.Per-kind Secret SAR + Secret roots
KindSecretnodes get an additional per-namespaceget secretsSAR (matchinghandleGetResourceserver.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 overrideIncludeSecrets=trueso Secret roots are findable; the Allow gate then enforces per-caller RBAC.Group-aware root lookup
BuildNeighborhoodWithIndextakesDynamicProviderand uses it to construct group-aware node IDs.findNodeByRefvalidatesref.Groupagainst nodeapiVersion.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
BuildNeighborhoodWithIndexthreads*RelationshipsIndex; BFS usesidx.EdgesFor(id)for O(in-degree + out-degree) neighbor enumeration instead of scanning alltopo.Edgesper step. REST pulls the memoized index vias.topoMemo.GetIndex(...); MCP builds inline viaIndexByResource(topo).Pseudo-kind root preflight
Root preflight now checks
topology.ClusterScopedKindsBEFOREClassifyKindScope. Without this, an agent calling/api/ai/neighborhood/nodeclass/_/foo(legitimate — NodeClass is whatget_topologyreturns) 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— dropsContextRef.Reason/Source/Confidence, theRefReason/RefSourceenums,ResourceContext.Truncated, and two unusedOmittedReasonvalues. T12's ownSubgraph.Truncatedstays (it's actually populated). The earlier redundantomitted: budget_exceededemission alongsidetruncated: truein the neighborhood handlers was removed inc2f8d49.OmittedBudgetExceededenum value retained for non-truncation budget overflows (e.g. Kyverno warmup cap, planned in T10).Verification
go build ./...+go vet ./...+ fullgo 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_neighborhoodMCP tool and/api/ai/neighborhoodendpoint 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_neighborhoodMCP tool andGET /api/ai/neighborhood/{kind}/{namespace}/{name}REST endpoint (withprofile,hops,max_nodes, and optionalgroup).Implements
pkg/topologyneighborhood traversal with profile-based edge filtering, hop/node caps, deterministic output ordering, and an in-traversalAllowhook so RBAC-denied nodes are skipped during expansion (including the root) and reported via aggregatedomitted/truncatedsignals.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 trimsresourcecontext.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.