feat(agent): add PSU agent functionality#1840
Conversation
Let maintainers know that an action is required on their side
|
There was a problem hiding this comment.
Pull request overview
Adds a POC “PSU gRPC agent” capability to Devolutions Agent: the agent can connect to a PSU gRPC endpoint, register itself/capabilities, and execute PSU-requested work by spawning child processes and streaming stdin/stdout over the gRPC stream.
Changes:
- Introduces a new
psu_grpc_agenttask that maintains a gRPC connection to PSU with reconnect/backoff and handles server commands. - Implements child-process spawning plus stream multiplexing to forward stdin → process and stdout/stderr → PSU.
- Adds configuration schema + build-time proto generation + container helpers (Dockerfile/entrypoint + run script).
Reviewed changes
Copilot reviewed 13 out of 14 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| devolutions-agent/src/service.rs | Registers the new PSU gRPC agent task when enabled in config. |
| devolutions-agent/src/psu_grpc_agent/mod.rs | Implements the gRPC agent task, registration message, connection loop, and message handling. |
| devolutions-agent/src/psu_grpc_agent/process.rs | Adds process spawning and stream pumping logic between PSU and child processes. |
| devolutions-agent/src/main.rs | Marks new proto/gRPC-related crates as used in the binary crate. |
| devolutions-agent/src/lib.rs | Exposes the new psu_grpc_agent module from the agent library. |
| devolutions-agent/src/config.rs | Adds PsuGrpcAgent configuration DTO + default + deserialization test. |
| devolutions-agent/proto/psu_agent.proto | Defines the POC gRPC protocol (AgentControl bidi stream + messages). |
| devolutions-agent/Cargo.toml | Adds tonic/prost dependencies and build-deps for proto compilation. |
| devolutions-agent/build.rs | Generates Rust code from the PSU agent proto using tonic-build and vendored protoc. |
| devolutions-agent/Dockerfile.psu-grpc-poc | Adds a POC image to build/run the agent alongside PowerShell. |
| devolutions-agent/docker/psu-grpc-entrypoint.sh | Writes a minimal agent config from env vars and starts the agent in the container. |
| devolutions-agent/Run-PsuGrpcAgentContainer.ps1 | Convenience script to build/run the POC container with env configuration. |
| Cargo.lock | Pulls in new dependency graph for tonic/prost/tooling. |
| .dockerignore | Ignores node_modules and .vs in Docker build context. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let server_url = conf | ||
| .server_url | ||
| .as_ref() | ||
| .context("PsuGrpcAgent is enabled but ServerUrl is not configured")? | ||
| .to_string(); |
There was a problem hiding this comment.
The rule is just that we don’t capitalize the first letter so it’s easier to compose, but we don’t need to lowercase acronyms, etc: PSU gRPC Agent enabled but server_url is not configured would be good
| if let Some(sender) = sender { | ||
| let end_of_stream = stream_data.end_of_stream; | ||
| let stream_id = stream_data.stream_id.clone(); | ||
| if sender.send(stream_data).await.is_ok() && end_of_stream { | ||
| self.close_stream(&stream_id).await; | ||
| } | ||
| } |
| let _ = outgoing_tx | ||
| .send(agent_message( | ||
| &agent_id, | ||
| &connection_id, | ||
| AgentPayload::StreamClosed(stream_closed( | ||
| request.stream_id.clone(), | ||
| "child process completed".to_owned(), | ||
| false, | ||
| )), | ||
| )) | ||
| .await; |
Hardens the PSU gRPC agent POC and adds optional AppToken support for authenticating the agent to PowerShell Universal. The POC container also avoids running as root and no longer disables TLS verification during Docker builds. This keeps unauthenticated local development possible when no AppToken is configured, while allowing PSU deployments to pass an application token through agent configuration or the container helper. Issue: N/A --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Benoît Cortier (CBenoit)
left a comment
There was a problem hiding this comment.
Before this graduates from POC, I think we should pull the gRPC/protobuf layer out of devolutions-agent into a dedicated crate (or two). Right now tonic/prost are woven through the agent crate in a few places:
Cargo.tomldeclaresprost,prost-types,tonic, plustonic-build+protoc-bin-vendoredbuild-deps.build.rsruns thetonic_buildcodegen (and theset_var("PROTOC", ...)dance, which we can drop separately viaConfig::protoc_executable).psu_grpc_agent/mod.rspulls in the generated code withtonic::include_proto!(...)and leaksprost_types::Timestamp/tonic::{Request, metadata, transport::Endpoint}into the module.process.rsthreads the generatedprostmessage types (StartProcess,StreamData,AgentMessage, …) through the whole process-management path.
I propose we split into:
psu-agent-proto: ownsproto/psu_agent.proto, the codegen inbuild.rs, and the generated module; re-exports the messages/client. Containsprost/tonic-build/protoc-bin-vendoredin one place (and keeps theprotochandling isolated).psu-agent-client: wraps the endpoint/auth/reconnect logic behind a domain-friendly API so the rest of the agent never touchestonictypes.
devolutions-agent would then depend on those crate(s) instead of prost/tonic directly, and main.rs wouldn't need the use prost as _; use tonic as _; stubs.
One caveat though, the generated prost types currently flow deep into process.rs, so for real containment we want small internal DTOs at the boundary and convert at the edge. Otherwise the split mostly relocates the dependency instead of containing it. Fine as a follow-up rather than blocking this draft. We could merge fast, and I can handle that part if you want.
| let protoc = protoc_bin_vendored::protoc_bin_path().expect("failed to locate vendored protoc"); | ||
| // SAFETY: Build scripts run single-threaded for this crate before prost-build reads PROTOC. | ||
| unsafe { std::env::set_var("PROTOC", protoc) }; | ||
|
|
||
| tonic_build::configure() | ||
| .build_transport(false) | ||
| .compile_protos(&["proto/psu_agent.proto"], &["proto"]) | ||
| .expect("failed to compile PSU agent proto"); |
There was a problem hiding this comment.
issue: It’s suspicious, I don’t think we need the unsafe { std::env::set_var("PROTOC", protoc) };.
suggestion: The clean way seems to be somewhere in these lines:
- prost_build::Config::protoc_executable: https://docs.rs/prost-build/latest/prost_build/struct.Config.html#method.protoc_executable
- https://docs.rs/tonic-build/latest/tonic_build/#getting-started
- https://lib.rs/crates/tonic-prost-build (Prost build integration for tonic)
|
I pushed commits to format the code and address Copilot’s comments. Merging now. |
POC: Adding ability to Devolutions Agent to connect to PSU over GRPC and start child processes to run PSRP requests from PSU.