From 76ded63f83425022c9cb2b7ca4f4db8421dab391 Mon Sep 17 00:00:00 2001 From: agentydragon-agent <260791330+agentydragon-agent@users.noreply.github.com> Date: Fri, 13 Nov 2026 22:34:07 -0800 Subject: [PATCH] Admit the Runtime proxy service role at native Runtime --- agentplane/docs/system_overview.md | 15 +++++++++++++++ agentplane/env/runtime/SPEC.md | 6 ++++-- agentplane/env/runtime/control/server.go | 6 ++++-- .../runtime/internal/coordinator/forward_test.go | 15 +++++++++++++++ 4 files changed, 38 insertions(+), 4 deletions(-) diff --git a/agentplane/docs/system_overview.md b/agentplane/docs/system_overview.md index 922bfc3a387..dc5fc6e6176 100644 --- a/agentplane/docs/system_overview.md +++ b/agentplane/docs/system_overview.md @@ -646,3 +646,18 @@ external MCP bearers and forwards workload calls under the bound ServiceAccount' - gRPC: [Actions](../action_service/api/contracts/action_service.proto), [operator Actions](../action_service/api/contracts/operator_action_service.proto), [Connections](../mcp_edge/api/connection/connection_service.proto), [Web Push](../action_service/api/web_push/web_push_service.proto), [Environment Service](../environments/v1/environment_service.proto), [Environment Runtime](../env/runtime/control/environment_runtime.proto), [Executor](../env/runtime/execution/executor.proto), [SessionJournalIngress](../history_service/api/ingress/session_ingress.proto), [Session Dispatcher](../runner_gateway/dispatcher/api/session/session_dispatcher.proto), [History Service](../history_service/api/history/history_service.proto), and the [Notification Service](../notification_service/api/notifications/notifications.proto) API (whose HTTP annotations generate the App browser facade; the service itself serves workload calls over gRPC). The Runner Gateway also serves standard gRPC health and proxies the Environment Runtime and Executor services over a direct mTLS connection to each runner. - HTTP and streaming APIs: [Integration app](../app/) (Bazel target: `//agentplane/app/cmd:agentplane-app`), [Electric proxy](../app/internal/proxy/electric.go), [Action Service generated facade](../action_service/api/contracts/action_service.proto), [LLM ingress](../llm_ingress/server.go), and [Git index](../x/indexing/app.py). - Controllers and egress: [Sandbox Controller](../sandbox_controller/internal/controller/controller.go), [Policy Controller](../policy_controller/internal/controller/controller.go), [central egress proxy](../egress/internal/proxy/proxy.go), [Go sidecar](../egress/sidecar/main.go), and [sidecar route configuration](../egress/platform_routes.py). + +## Staged Runtime proxy caller identity + +Runtime admits the exact `spiffe://agentplane/runtime-proxy` certificate for its existing Gateway +control and Executor capabilities, retaining the current `runner-gateway` certificate during +migration. Sessions must call through the proxy; its certificate has no direct Runtime grant. +Controller passive watches retain their separate exact identity and permissions. No caller changes +its credential in this stage. + +```mermaid +flowchart LR + sessions[Sessions service SVID] -->|separate network hop and grant| proxy[Runtime proxy] + proxy -->|mTLS: runtime-proxy or current runner-gateway SVID| runtime[Environment Runtime] + controller[Sandbox Controller SVID] -->|mTLS: passive WatchState only| runtime +``` diff --git a/agentplane/env/runtime/SPEC.md b/agentplane/env/runtime/SPEC.md index fddbece757c..b3b66cce25a 100644 --- a/agentplane/env/runtime/SPEC.md +++ b/agentplane/env/runtime/SPEC.md @@ -106,7 +106,9 @@ process creates a fresh boot ID. Once bound, the UID cannot change during that p including after a failed or uncertain adoption callback; reconnects may settle only the same UID. This lets a replacement workload restore retained state without allowing a Gateway to select or advance the workload identity. The Controller can open only passive watches with its exact SPIFFE -identity; every control and Executor call still requires the Gateway identity. +identity; every control and Executor call still requires the exact Gateway identity or the +role-specific `spiffe://agentplane/runtime-proxy` service SVID. The latter is admitted before its +separate caller cutover; `spiffe://agentplane/sessions` has no direct Runtime grant. Gateway calls with an absent or zero connection epoch authorize against the Controller-bound incarnation and this Runtime boot, independently of Gateway state-watch lifetime. Any authenticated @@ -116,7 +118,7 @@ boot ID invalidates it. A Gateway `WatchState` request without an epoch or with observe only the already-bound incarnation and cannot establish or replace it. Supplied nonzero epochs retain the legacy live-watch checks for calls and the Gateway's connection-owner behavior for watches, still restricted to the Controller-bound incarnation. The epoch field remains during -this receiver compatibility step. Direct Session admission uses Gateway identity and Runtime's +this receiver compatibility step. Direct Session admission uses that authority and Runtime's gate. `CreateSession` acknowledges a durable reservation, without promising process readiness. Model lookup occurs outside the gate; the reservation rechecks its authorization and accepted revision before its retained write. diff --git a/agentplane/env/runtime/control/server.go b/agentplane/env/runtime/control/server.go index da85d64ea12..8852ed50b84 100644 --- a/agentplane/env/runtime/control/server.go +++ b/agentplane/env/runtime/control/server.go @@ -95,7 +95,7 @@ func New(config Config, backend environmentruntimepb.EnvironmentRuntimeServer) * // Listen opens the configured authenticated control listener and registers the // Runner, executor stream, and standard gRPC health services. func (s *Server) Listen(executor executorpb.ExecutorServer) (net.Listener, *grpc.Server, error) { - transportCredentials, err := s.config.Security.ServerCredentials(environmentruntimetransport.GatewayURI, environmentruntimetransport.SandboxControllerURI) + transportCredentials, err := s.config.Security.ServerCredentials(environmentruntimetransport.GatewayURI, environmentruntimetransport.RuntimeProxyURI, environmentruntimetransport.SandboxControllerURI) if err != nil { return nil, nil, err } @@ -279,7 +279,9 @@ func (s *Server) validateLocked(epoch *Epoch) error { func (s *Server) authorizeGatewayPeer(ctx context.Context) error { if err := s.config.Security.AuthorizePeer(ctx, environmentruntimetransport.GatewayURI); err != nil { - return status.Error(codes.Unauthenticated, "Runner control peer is not authenticated as the Runner Gateway") + if err := s.config.Security.AuthorizePeer(ctx, environmentruntimetransport.RuntimeProxyURI); err != nil { + return status.Error(codes.Unauthenticated, "Runtime control peer is not an admitted Runtime proxy") + } } return nil } diff --git a/agentplane/env/runtime/internal/coordinator/forward_test.go b/agentplane/env/runtime/internal/coordinator/forward_test.go index ac02ab6068e..80ebd8b9287 100644 --- a/agentplane/env/runtime/internal/coordinator/forward_test.go +++ b/agentplane/env/runtime/internal/coordinator/forward_test.go @@ -708,6 +708,21 @@ func TestControllerBindsRuntimeForIndependentAuthenticatedGatewayReplicas(t *tes } }) } + proxy := environmentruntimepb.NewEnvironmentRuntimeClient(dial(environmentruntimetransport.RuntimeProxyURI)) + if _, err := proxy.GetStatus(callCtx, &sessionpb.GetStatusRequest{}); err != nil { + t.Fatalf("role-specific Runtime proxy status: %v", err) + } + proxyWatch, err := proxy.WatchState(ctx, passiveRequest) + if err == nil { + _, err = proxyWatch.Recv() + } + if status.Code(err) != codes.Unauthenticated { + t.Fatalf("Runtime proxy must not inherit Controller passive watch: %v", err) + } + unlisted := environmentruntimepb.NewEnvironmentRuntimeClient(dial(environmentruntimetransport.SessionsURI)) + if _, err := unlisted.GetStatus(callCtx, &sessionpb.GetStatusRequest{}); status.Code(err) != codes.Unavailable { + t.Fatalf("Sessions bypassed the Runtime proxy TLS allowlist: %v", err) + } if _, err := controller.GetStatus(callCtx, &sessionpb.GetStatusRequest{}); status.Code(err) != codes.Unauthenticated { t.Fatalf("Controller GetStatus with valid Gateway metadata = %v", err) }