Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions agentplane/docs/system_overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
6 changes: 4 additions & 2 deletions agentplane/env/runtime/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down
6 changes: 4 additions & 2 deletions agentplane/env/runtime/control/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down
15 changes: 15 additions & 0 deletions agentplane/env/runtime/internal/coordinator/forward_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Loading