feat: AgentToolProvider reconciler for cross-cluster MCP tool federation (grid#41) - #56
Conversation
|
Filed #57 for the cross-site SWIM/CRDT propagation of |
…y (grid#41)
Adds the live-probe half of the AgentToolProvider reconciler: an rmcp-based
Streamable HTTP client that calls tools/list against spec.endpoint, with
SSRF protection (blocks loopback/link-local/cloud-metadata targets), TLS
material resolution, and bearer-token auth from a Secret.
Also wires grid_mcp_probe_total{outcome} and grid_mcp_probe_duration_seconds
metrics, following grid#9's bounded-cardinality convention: TlsConfigInvalid's
carried reason string is deliberately collapsed to a single label so metric
cardinality stays fixed regardless of cluster misconfiguration variety.
Signed-off-by: Jordi Gil <jgil@redhat.com>
Wires the AgentToolProvider CRD to the live MCP probe client added in the previous commit: resolves siteSelector matches against GridNetwork/GridSite, runs the probe, and maps its outcome to status.phase/status.reason/ status.discoveredTools. Mirrors InferenceProvider's reconciliation shape, including its documented cross-resource watch limitation as a follow-up. Data-plane MCP tool-catalog aggregation and cross-cluster tools/call routing (praxis-ai#155, #205, #173) are out of scope here — this is the Grid-side control plane only, per grid#41's stated scope. Signed-off-by: Jordi Gil <jgil@redhat.com>
…k MCP server, and E2E check (grid#41) - charts/grid-operator: install the AgentToolProvider CRD and grant its RBAC verbs, with helm-unittest coverage for the new ClusterRole rule; wires charts/grid-operator into the CI helm-unittest job (only charts/grid-site ran before). - mock-providers: new --mcp-server mode built on the real rmcp server SDK (Streamable HTTP), so the E2E check below probes actual MCP wire protocol behavior, not a hand-rolled JSON-RPC approximation. Disables rmcp's default DNS-rebinding Host-header allowlist (localhost/127.0.0.1/::1 only), since this mock is reached over its in-cluster Service DNS name or NodePort address, never loopback. - xtask: cargo xtask env verify-agenttoolprovider-convergence deploys the mock as a real in-cluster NodePort service (the operator under test runs out-of-cluster, so it can't resolve in-cluster .svc DNS names) and proves Pending -> Available with discoveredTools populated end-to-end, plus the unreachable-endpoint failure path landing on Unavailable with a reason. Verified twice against a real kind cluster. Signed-off-by: Jordi Gil <jgil@redhat.com>
d46aa45 to
02a0b14
Compare
…st gaps (grid#41) Backfills the coverage gaps found during grid#56's GA readiness audit: - attach_tls_ca/attach_tls_client_identity/read_tls_material now have real unit-test coverage against a mocked kube::Client (tower::service_fn), which the module's own doc comment had falsely claimed already existed. - Along the way, found and fixed a real bug: reqwest::Certificate::from_pem and reqwest::Identity::from_pem don't reject malformed/empty PEM input, unlike the rustls::pki_types-based validation InferenceProvider's build_tls_client_config already uses. attach_tls_ca and attach_tls_client_identity now eagerly validate PEM material the same strict way before handing it to reqwest, so EndpointTlsMaterialInvalid/ EndpointTlsIdentityMismatch are reachable in practice, not just in name. - Also found (but did not fix here, to keep this change scoped) a pre-existing bug in the shared endpoint_tls.rs/secret.rs Secret-read path that also affects InferenceProvider: a key entirely absent from an existing Secret's data is misreported as SecretMissing instead of KeyMissing. Filed as grid#58, with a test that locks in current behavior and points at the issue. - static_config_failure_reason (GridNetworkNotFound/ProviderConfigInvalid) now has direct unit-test coverage via the same kube::Client mocking pattern, proving wrong an existing code comment that claimed this "cannot be unit-tested without a live cluster or a mock Kubernetes server." - Fixed doc drift: overview.md's claim that AgentToolProvider "does not currently run full controllers" (it does, as of grid#56), crds.md's Degraded phase (unreachable by design for this CRD, per phase_and_reason_from_probe's own doc comment) and missing reason/ observedGeneration status fields, and two stale "once PR 2 lands" code comments left over from the original 3-PR plan that landed as one. Signed-off-by: Jordi Gil <jgil@redhat.com>
praxis-bot
left a comment
There was a problem hiding this comment.
PR Review
Summary: Adds AgentToolProvider CRD reconciler with an rmcp-based MCP tools/list live probe (SSRF-hardened, DNS-pinned), site-selector matching mirroring InferenceProvider, Helm/RBAC scaffolding, a real rmcp-server-backed mock MCP server, and an end-to-end convergence xtask.
Overall: High-quality, well-structured PR. The three-layer test architecture (pure decision logic, mocked Kubernetes I/O, real network integration) is exemplary. SSRF protection is thorough -- loopback, link-local, unspecified, cloud metadata, IPv4-mapped-IPv6 bypass, embedded credentials, DNS-rebinding address pinning, redirect suppression, and proxy bypass are all covered. The reconciler mirrors InferenceProvider's proven pattern, and conventions are followed consistently. The pre-existing items[2] duplication bug in generate-deployment-crds.sh is quietly fixed as well.
| Severity | Count |
|---|---|
| Critical | 0 |
| Large | 1 |
| Medium | 2 |
Findings without inline placement
(none -- all findings are inline)
`router`'s doc comment linked to `FixedToolsServer`, a private struct -- the link only resolved locally because of --document-private-items and failed CI's -D rustdoc::private-intra-doc-links check. Drop the link, keep the type name as plain text with a note that it's module-private. Signed-off-by: Jordi Gil <jgil@redhat.com>
Addresses grid#56 review feedback (pull/56#pullrequestreview, praxis-bot): - PROBE_TIMEOUT was documented as a single combined budget for the live MCP probe, but resolve_endpoint_for_probe (DNS), the connect/handshake, and tools/list each independently got up to the full timeout, and TLS Secret material reads via the Kubernetes API had no timeout at all. Worst-case wall-clock time could exceed 3x the documented 10s budget, plus unbounded Kubernetes API latency. Fixed by wrapping the whole probe sequence in a single outer tokio::time::timeout in probe_agent_tool_provider, so all phases now share one real budget. - auth_header_map silently dropped the Authorization header when a bearer token contained characters invalid in an HTTP header value, with no log signal -- the resulting probe failure would look like an auth/response problem with no trace back to the real cause. Added a tracing::warn! on that branch. - validate_probe_url already blocks IPv6 link-local (fe80::/10) and unique-local (fd00::/8) addresses via is_ssrf_sensitive, but had no test coverage proving it (only the IPv4 link-local and IPv6 loopback cases were tested). Added the two missing regression tests. Signed-off-by: Jordi Gil <jgil@redhat.com>
…lient All existing agent_tool_provider tests exercise resolve_phase_and_sites's constituent resolve_*/pure-logic functions in isolation; none drove the public reconcile() entrypoint itself, so nothing proved the resolved (phase, reason, matchingSites, discoveredTools) tuple actually reaches the Kubernetes API as the status PATCH body a real controller sends. Add a PATCH-capturing mock kube::Client and two reconcile()-level tests covering the two code paths that need no live MCP probe: a config-invalid provider (fully short-circuited, no Kubernetes calls at all) and a provider whose gridNetworkRef doesn't resolve (exercises the live GridNetwork GET). Both mutation-tested by hand against the reason strings they assert on to confirm they fail on regression, not just pass by construction. Signed-off-by: Jordi Gil <jgil@redhat.com>
Summary
Closes #41.
Adds the Grid-side control plane for
AgentToolProvider: a live MCPtools/listprobe client and a reconciler that mirrors the already-provenInferenceProviderpipeline, plus the Helm/E2E scaffolding to validate it end-to-end.operator/src/resources/mcp_probe.rs):rmcp-based Streamable HTTP client with SSRF protection (blocks loopback/link-local/cloud-metadata targets), TLS material resolution, and bearer-token auth resolved from a Secret. Bounded telemetry (grid_mcp_probe_total{outcome},grid_mcp_probe_duration_seconds) follows feat(ingress): publish bounded edge availability for global traffic managers #9's cardinality convention.operator/src/controller/agent_tool_provider.rs): resolvessiteSelectormatches againstGridNetwork/GridSite, runs the probe, and maps outcomes tostatus.phase/status.reason/status.discoveredTools.AgentToolProviderCRD and RBAC in thegrid-operatorchart (withhelm-unittestcoverage, now running in CI), a realrmcp-server-backed mock MCP server (mock-providers --mcp-server), and a newcargo xtask env verify-agenttoolprovider-convergencecommand provingPending -> AvailablewithdiscoveredToolspopulated against a real in-cluster mock, plus the unreachable-endpoint failure path landing onUnavailablewith a populated reason.Data-plane MCP tool-catalog aggregation and cross-cluster
tools/callrouting (praxis-ai#155,#205,#173) are out of scope here, per #41's stated scope — this PR is the Grid-side control plane only.Test plan
cargo test --workspace— all unit/integration tests pass (1121+ operator tests, 473+ xtask tests)cargo clippy --workspace --all-targets -- -D warnings— cleancargo +nightly fmt --all -- --check— cleanhelm unittest charts/grid-operator charts/grid-site— 7/7 passingcargo xtask env verify-agenttoolprovider-convergencerun twice against a real kind cluster (healthy convergence + unreachable-endpoint path both pass reliably)