Skip to content

Add agentic RDP CLI and localhost CI workflow#1289

Open
Marc-André Moreau (mamoreau-devolutions) wants to merge 2 commits into
Devolutions:masterfrom
mamoreau-devolutions:awakecoding/agent-cli-design
Open

Add agentic RDP CLI and localhost CI workflow#1289
Marc-André Moreau (mamoreau-devolutions) wants to merge 2 commits into
Devolutions:masterfrom
mamoreau-devolutions:awakecoding/agent-cli-design

Conversation

@mamoreau-devolutions
Copy link
Copy Markdown
Contributor

Summary

  • Add ironrdp-agent, a daemon-backed CLI for agentic/headless RDP automation over local IPC
  • Add mouse, keyboard, resize, wait-frame, screenshot, status, sessions, connect/disconnect commands
  • Support .rdp files, logging controls, and --desktop-size WxH
  • Add a push/manual Windows workflow that enables localhost RDP, drives ironrdp-agent, verifies PSRemoting, and uploads screenshot/log artifacts

Validation

  • cargo check -p ironrdp-agent
  • cargo test -p ironrdp-agent
  • cargo clippy -p ironrdp-agent --all-targets -- -D warnings
  • cargo xtask check fmt -v
  • cargo test -p ironrdp-agent --test ipc
  • PowerShell script parse checks
  • GitHub Actions Agentic RDP workflow passed on push: run 26121877104
    • Connected to localhost RDP at 127.0.0.1:3389
    • Verified 1920x1080 framebuffer
    • Uploaded non-blank screenshot artifact

Notes

Full workspace cargo xtask check lints -v / cargo xtask check tests --no-run -v were blocked locally by the existing Windows libopus_sys CMake generator issue (Visual Studio 18 2026), unrelated to the agent crate path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@glamberson
Copy link
Copy Markdown
Contributor

Nice to see this drop. Doing a first read; the RdpClient refactor stands out as broadly useful for downstream consumers, opening the client up to embedding outside the GUI. Two of my products would benefit directly: lamco-rdp-test-client (headless regression-test client) and lamco-rdp-tools (RDP automation CLI with image matching, scripting, and session recording). Both currently embed the lower-level IronRDP crates and would have a cleaner path through ironrdp-client after this. More thoughts to come.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 17 out of 18 changed files in this pull request and generated 13 comments.

Comment on lines +789 to +800
let snapshot = self.snapshot.read().await;
let mouse_position = self.input_database.lock().await.mouse_position();

SessionSummary {
session_id: self.session_id.clone(),
status: snapshot.status.clone(),
width: snapshot.frame.as_ref().map(|frame| frame.width),
height: snapshot.frame.as_ref().map(|frame| frame.height),
frame_sequence: snapshot.frame_sequence,
mouse_x: mouse_position.x,
mouse_y: mouse_position.y,
last_error: snapshot.last_error.clone(),
Comment on lines +827 to +837
if self.has_requested_frame(after_frame).await {
return Ok(());
}

tokio::time::timeout(timeout, async {
loop {
self.notify.notified().await;

if self.has_requested_frame(after_frame).await {
break;
}
Comment on lines +1097 to +1099
let listener =
tokio::net::UnixListener::bind(&path).with_context(|| format!("failed to bind {}", path.display()))?;

Comment on lines +299 to +304
(None, Some(env_name)) => {
let password =
std::env::var(env_name).with_context(|| format!("failed to read password from {env_name}"))?;
args.push("--password".to_owned());
args.push(password);
}
Comment on lines +1168 to +1171
session
.input_sender
.send(RdpInputEvent::Close)
.map_err(|_| ApiError::new(StatusCode::CONFLICT, "session input channel is closed"))?;
Comment on lines +3 to +7
[string] $AgentPath = (Join-Path $env:GITHUB_WORKSPACE 'target\release\ironrdp-agent.exe'),

[string] $Endpoint = "pipe:ironrdp-agent-ci-$PID",

[string] $ArtifactsDir = (Join-Path $env:GITHUB_WORKSPACE 'artifacts\agentic-rdp'),
param(
[Parameter(ParameterSetName = 'Enable')]
[Parameter(ParameterSetName = 'Cleanup')]
[string] $StatePath = (Join-Path $env:RUNNER_TEMP 'ironrdp-agentic-rdp-state.json'),
Comment on lines +158 to +162
Set-ItemProperty -Path $terminalServerPath -Name 'fDenyTSConnections' -Value 0
Set-ItemProperty -Path $rdpTcpPath -Name 'UserAuthentication' -Value 0
Set-Service -Name TermService -StartupType Automatic
Start-Service -Name TermService
Enable-NetFirewallRule -DisplayGroup 'Remote Desktop' | Out-Null
Comment on lines +12 to +16
[Parameter(ParameterSetName = 'Register')]
[Parameter(ParameterSetName = 'RunServer')]
[Parameter(ParameterSetName = 'Wait')]
[string] $EndpointPath = (Join-Path $env:GITHUB_WORKSPACE 'artifacts\agentic-rdp\pshost-endpoint.json'),

Comment on lines +6 to +13
[string] $EndpointPath = (Join-Path $env:GITHUB_WORKSPACE 'artifacts\agentic-rdp\pshost-endpoint.json'),

[string] $ArtifactsDir = (Join-Path $env:GITHUB_WORKSPACE 'artifacts\agentic-rdp')
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

@glamberson
Copy link
Copy Markdown
Contributor

Further thoughts...

Marc-André Moreau (@mamoreau-devolutions), the embedding refactor in ironrdp-client is the broadly useful piece here. A few architectural items:

  • Output mpsc is unbounded_channel. Chatty server outpaces process_output_events, memory grows without bound. Snapshot only keeps the latest frame; bounded + try_send + drop-oldest matches the model.

  • Daemon not detached on Unix. Auto-spawn doesn't setsid / setpgid; SIGINT on the spawning terminal kills the daemon. CommandExt::process_group(0) is the one-line fix. Windows pipe + no-console handles this already.

  • Unix socket at /tmp/ironrdp-agent.sock. $XDG_RUNTIME_DIR is the conventional spot (0700, per-user, ephemeral). Fall back to /tmp with chmod 0600.

Aside: #790 has me and Benoît Cortier (@CBenoit) converging on a layered embedding for the WASM side. I built an ~4,600 LOC lamco-rdp-wasm (worker-thread + WebCodecs + EGFX); we're putting ironrdp-wasm as the pure-Rust library underneath ironrdp-web and a lamco-side facade. Structurally the same embedding shape this PR enacts.

Copy link
Copy Markdown
Member

@CBenoit Benoît Cortier (CBenoit) left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice feature enabling interesting use cases.

A few high level observations:

  • ironrdp-client is not intended to be used as a library, but it is used as such by ironrdp-agent
  • Integration with rdpsnd was added to ironrdp-client as part of this PR, but is not directly related to the agentic RDP CLI
  • I would not gate rdpsnd behind a compile-time feature flag; it’s something we want to have by default.
  • Agentic RDP CI scripts living in a testing/ folder at the root of the project is slightly polluting the workspace.

Suggestions:

  • Add support for rdpsnd in a dedicated PR
  • Add support for desktop size CLI in a dedicated PR
  • Separate the ironrdp-client crate into two crates: ironrdp-client (library) and ironrdp-viewer (binary only, equivalent to totay’s ironrdp-client).
    • Keep the refactoring with RdpOutputEventSender trait as a way to abstract the RDP output events.
  • In the current PR:
    • Keep agentic RDP related changes
    • Move the Agentic RDP CI scripts somewhere into .github/ folder

I can handle most of that, and we can shrink this PR significantly

@mamoreau-devolutions
Copy link
Copy Markdown
Contributor Author

Benoît Cortier (@CBenoit) I can leave this PR in your hands if you can handle most of it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

4 participants