Skip to content

feat(runtime): support remote agent environments (#13) - #48

Open
cf3901646 wants to merge 2 commits into
tuchg:mainfrom
cf3901646:feat/remote-agent-environments
Open

cf3901646 wants to merge 2 commits into
tuchg:mainfrom
cf3901646:feat/remote-agent-environments

Conversation

@cf3901646

Copy link
Copy Markdown

Overview

Resolves #13 by introducing support for remote agent environments across the agent runtime, control plane, and channel projection layers.

Highlights

  • Remote Environment Model & Registry:
    • RemoteEnvironmentConfig supporting multiple transports (Ssh, Gateway, Bridge), custom remote working directory, environment variables, credentials, and execution timeouts.
    • RemoteEnvironmentRegistry for managing multiple remote execution environments and resolving default/named targets.
  • Origin Metadata & UX Visibility:
    • SessionOrigin modeling local vs
      emote:.
    • Added origin tracking to AgentStatus and StatusSnapshot.
    • Integrated origin display in Telegram (
      ender_status_snapshot) and WeChat (
      ender_wechat_workspace_status) interfaces so users always know where their agent is running.
  • Safe File & Path Handling:
    • RemoteFileSafety::validate_relative_path preventing directory traversal (..) and unintended absolute paths outside the remote workspace.
    • RemoteFileSafety::validate_file_size enforcing safe transfer size limits.
  • Observable Error Handling:
    • RemoteEnvironmentError covering environment lookups, authentication failures, reachability, execution timeouts, and rejected file operations.
  • Automated Tests:
    • Comprehensive unit tests in crates/lucarne/src/agent_runtime/remote.rs.
    • End-to-end integration test suite in crates/lucarne/tests/remote_environment.rs.

Fixes #13.

- Introduce RemoteEnvironmentConfig, RemoteTransport, and RemoteEnvironmentRegistry
- Implement SessionOrigin and link origin metadata to AgentStatus and StatusSnapshot
- Add safe file transfer path and size verification (RemoteFileSafety)
- Propagate origin labels to Telegram and WeChat workspace status projections
- Add comprehensive unit and integration tests for remote environment lifecycle
@tuchg

tuchg commented Sep 14, 2026

Copy link
Copy Markdown
Owner

Reviewed the diff against current main (5fe9244). It is purely additive (495+/0−): a data model plus tests. Findings below, ordered by severity.

1. Nothing in this diff is reachable yet

  • RemoteEnvironmentRegistry is never constructed outside tests.
  • Nothing ever assigns AgentStatus.origin, so build_status_snapshot always yields origin: None and both new render paths (crates/lucarne-telegram/src/turn/projection.rs:659, crates/lucarne-wechat/src/service.rs:2903) are unreachable. The Origin: / 来源: lines can never print.
  • RemoteFileSafety::validate_relative_path and validate_file_size have no callers outside tests.
  • RemoteEnvironmentError::{AuthenticationFailed, Unreachable, ExecutionTimeout} are never constructed — there is no transport implementation at all; Ssh / Gateway / Bridge are inert enum variants.
  • No config loading: the registry is in-memory only and is not reachable from lucarned config/onboarding, and no channel command can target a remote environment.

Against issue #13's acceptance criteria, this covers only "an origin field exists". The other three (configure at least one environment, target one explicitly from Telegram/WeChat, observable failure modes) are not met, so #13 should probably stay open.

2. Blocking: the new test fails on macOS/Linux

crates/lucarne/tests/remote_environment.rs:97-100 asserts:

assert!(matches!(
    RemoteFileSafety::validate_relative_path("C:\\Windows\\System32"),
    Err(RemoteEnvironmentError::SafeFileTransferRejected { .. })
));

Component::Prefix is only ever produced on Windows. On Unix the whole string is one normal component:

components: [Normal("C:\\Windows\\System32")]
matches!(c, Component::RootDir | Component::Prefix(_)) => false

So validate_relative_path returns Ok(..) and the assertion fails — cargo test -p lucarne is red on Linux and macOS.

The root cause is worse than the test: validate_relative_path validates a remote path using the local host's std::path semantics, so behaviour depends on which OS the daemon happens to run on. On Unix, C:/Windows/System32 (forward slashes) also passes and is returned unchanged by replace('\\', '/') — the function hands back exactly the absolute drive path it claims to reject. ~ is a Normal component too and passes, and a shell-based remote exec would expand it. Rejecting these needs explicit, host-independent checks (C: drive prefix, UNC \\, leading /, ~), not Component matching. Not exploitable today only because nothing calls it.

3. auth_token is not actually kept safe

crates/lucarne/src/agent_runtime/remote.rs:49:

/// Authorization token or credential reference (kept safe, not logged in plain text).
pub auth_token: Option<SmolStr>,

The struct derives Debug + Serialize + PartialEq, so the token is printed verbatim by {:?} and written verbatim by serde_json; RemoteEnvironmentRegistry is itself Serialize/Deserialize, so persisting it means plaintext credentials on disk. tests/remote_environment.rs even asserts the plaintext round-trip. The convention elsewhere in this repo is a custom Debug that prints <redacted> (e.g. crates/lucarned/src/onboarding/config.rs:129, crates/lucarne-telegram/src/onboarding.rs:194). A secrecy-style wrapper, or manual Debug + #[serde(skip)], would match that.

4. Origin has two string representations

SessionOrigin::Remote("cloud".into()).as_str()           // "cloud"
SessionOrigin::Remote("cloud".into()).to_display_label() // "remote:cloud"

resolve_origin_label() returns the remote:<name> form while as_str() returns the bare name, so anyone who persists one form and compares against the other gets a silent mismatch. RemoteEnvironmentConfig::origin_label() (remote.rs:96) implements the same formatting rule a second time. One representation would be enough.

5. Nits

  • remote.rs:25 and remote.rs:127: manual Default impls for unit-variant enums. cargo clippy reports derivable_impls for both; #[derive(Default)] + #[default] on the variant fixes it.
  • validate_relative_path normalizes as a side effect of validating and returns a different string than it was given. Nothing forces the caller to use the return value, so the normalization is easy to lose.
  • remote.rs starts with a UTF-8 BOM (ef bb bf) — rustc strips it, but it looks like a paste artifact.
  • Per AGENTS.md, domain docs live in CONTEXT.md + docs/adr/. "Remote environment" and "origin" are new domain terms and are not in the glossary, and there is no ADR for the transport choice.
  • Roughly 200 lines of tests cover a data model with no consumers, and the unit tests in remote.rs and the integration tests in tests/remote_environment.rs assert the same things (origin_label, as_str, path traversal). The one assertion with real regression value — the cross-platform path check — is the one that currently fails. Worth trimming the duplicates and keeping that one.

Suggested path

The model is fine as a starting point, but it should be shaped together with the code that consumes it, otherwise the wiring will drag it in a different direction. Minimum before merge: make the path check host-independent, stop the token from being printable/serializable, collapse origin to a single representation, derive Default. Then either land it as (prepare) with #13 still open, or close #13 with a follow-up that implements transport, config loading, and channel targeting.

…chg#48)

- make path safety validation host-independent to resolve Linux/macOS CI failure
- redact auth_token in Debug output and skip in serialization to prevent plaintext credential leaks
- collapse SessionOrigin and RemoteEnvironmentConfig origin labels to a single representation
- derive Default with #[default] on unit-variant enums to resolve clippy warnings
- remove UTF-8 BOM from remote.rs
- update unit and integration tests accordingly
@cf3901646

Copy link
Copy Markdown
Author

Thanks for the thorough review and clear suggestions @tuchg!

I have addressed all the minimum feedback items in commit �cbe0df:

  1. Host-independent path safety check:

    • Refactored RemoteFileSafety::validate_relative_path to avoid host std::path::Component dependence.
    • It now explicitly checks and rejects Windows drive prefixes ([a-zA-Z]:), leading / / , UNC prefixes (\, //), home directory (~), and traversal components (..) uniformly across all platforms (Linux, macOS, Windows).
    • Added cross-platform test cases covering these prefixes in integration and unit tests.
  2. Credential safety (�uth_token):

    • Implemented manual Debug for RemoteEnvironmentConfig to print whenever �uth_token is present.
    • Added #[serde(skip)] on �uth_token to prevent plaintext persistence during config serialization.
    • Added tests verifying debug redaction and serialization omission.
  3. Unified origin representation:

    • Consolidated the origin string representations into a single origin_label() on SessionOrigin (and Display implementation), returning "local" and "remote:".
    • RemoteEnvironmentConfig::origin_label() now directly delegates to SessionOrigin::Remote, preventing format duplication or mismatches.
  4. Clippy derivable_impls:

    • Switched RemoteTransport and SessionOrigin to #[derive(Default)] with #[default] on unit variants.
  5. Cleaned up BOM:

    • Removed the UTF-8 BOM from crates/lucarne/src/agent_runtime/remote.rs.

Please take a look when you have a moment. Thank you!

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support remote agent environments

2 participants