feat/tunnel-dialer #4
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/tunnel-dialer"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The SPA's relay terminal protocol carries two distinct resize signals: the initial options envelope (`{type:"options", cols, rows, ...}` sent by `agent-redir-ws.js` right after rendezvous) and subsequent xterm.onResize control messages (`{ctrlChannel:102938, type:"termsize", cols, rows}` from `default.handlebars::xTermSendResize`). Both must trigger a `master.resize()` on the PTY so the shell sees SIGWINCH. run_terminal: hold the PTY master in `Arc<Mutex<Box<dyn MasterPty + Send>>>` so the async select branch can lock + resize between read / write loops. Hand the cloned reader / writer to spawn_blocking threads as before; resize is the only call from async land. `classify_terminal_text` replaces the previous control_echo gate with a 3-way enum: EchoControl (rtt + generic ctrl frames), Resize (options OR ctrlChannel termsize), Drop (everything else - options without cols/rows, recording metadata, non-JSON noise). The PTY shell never sees these text frames, so an `echo`-of-the-options JSON can't pollute its stdin any more. 4 new unit tests cover the classifier matrix; existing 8 still pass. End-to-end verified via SPA: a Terminal panel at xterm cols=9 rows=23 yields `tput cols`=9 / `tput lines`=23 inside the shell. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>run_files: speaks the SPA's `views/sharing.handlebars::CreateRemoteFiles` JSON envelope. Today's MVP supports the `ls` action; the SPA renders a read-only file tree off that, which is the most common operator use case ("just look at what's on the box"). Mutating actions (mkdir, rm, rename, download, upload, find) are accepted but no-op'd for now; later phases plug `host::dir`/`host::file_transfer`/ `host::fileops` into the remaining arms. build_ls_reply: reads `host::dir::list_dir`, sorts folders-first then alphabetical to match the SPA's render order, and emits `{action:"ls", path, reqid?, dir:[{n,t,s,d}, ...]}` where `t` is 2=folder, 3=file, 4=symlink, 5=other. Errors return `dir:[]` + `error` so the panel surfaces a message instead of hanging. Tunnel routing: USAGE_FILES (4) and USAGE_FILE_TRANSFER (5) both land here. USAGE_DESKTOP keeps the control-only loop until KVM streaming over the relay gets wired. 2 new unit tests cover the directory-listing reply and the missing-path error path. End-to-end verified via direct WS: `ls /etc` from the SPA returns 30+ entries with proper folder flags + sizes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>Extends `run_files` from the ls-only MVP to the full set of Files panel actions the SPA's `views/default.handlebars::p13gotFiles` emits. All ops echo `reqid` (or `id` for download) back so the SPA can route per-dialog responses. Atomic ops (each returns `{action:"refresh", reqid}` on success or `{action:<orig>, error, reqid}` on failure): - mkdir: host::fileops::mkdir - mkfile: create-new an empty file - rm: per-name fileops::delete loop, supports recursive flag from `rec` - rename: fileops::rename(path/oldname -> path/newname) Streaming ops: - download: 3-step handshake (`{sub:"start"}` reply on path-open -> SPA sends `{sub:"startack"}` -> agent ships the first chunk -> loop on `{sub:"ack"}`). Each binary chunk is `[u32 control header big-endian | data]`; bit 0 of the header marks the end-of-stream. 16 KiB chunks (legacy default). - upload: agent opens dest with create + (truncate XOR append) per the SPA's `append` flag, replies `{action:"uploadstart", reqid}`, flow-controls each binary frame with `{action:"uploadack", reqid}`, finalises on `{action:"uploaddone"}`. The SPA prepends a 0 byte to binary chunks whose first raw byte would be `{` (123) or 0 to disambiguate from JSON command frames; we strip that lead byte. findfile: bounded recursive walk (max depth 16, max 5000 results) streaming `{action:"findfile", reqid, r:<full-path>}` per match and a final `{action:"findfile", reqid, r:null}` to close the dialog. Filter is case-insensitive substring. 5 new unit tests cover mkdir/mkfile/rm-success/rm-partial-fail/rename. End-to-end direct-WS verified: a single tunnel runs mkdir + ls + mkfile + rename + download (271 bytes of /etc/os-release returned with end-flag) + rm in sequence. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>The Screenshot portal one-shot path is functional but cannot stream faster than ~1 fps - each call costs a DBus roundtrip + compositor render + tmp file PNG IO. KDE additionally reset the cursor to the screen centre on every screenshot, producing the "cursor jumps to centre every few seconds" symptom users reported. Replace with a live PipeWire stream consumer driven by the same RemoteDesktop session that already handles input. `PortalSession::open` now also calls `open_pipe_wire_remote` and spawns a dedicated OS thread (`kvm-pipewire`) running the PipeWire main loop. The thread: 1. Connects to the compositor's PipeWire daemon via the portal- issued fd. 2. Creates a Stream against the screencast node id. 3. Negotiates a packed-RGB format from a permissive enum (BGRx / RGBx / BGRA / RGBA / RGB / BGR; resolution 1x1..8192x8192; framerate 0..240 fps). 4. On every `process` callback, dequeues the buffer, converts to RGB8 (handling stride padding + per-format byte swizzle) and swaps the result into `latest_frame`. `capture_rgb_via_portal` reads `latest_frame` first - that's a memcpy + clone, no IPC - and only falls back to the slow Screenshot portal when the consumer thread hasn't produced a frame yet (early in the session, before the first format negotiation completes). `convert_to_rgb` covers the six packed formats we advertise; padded rows handled by skipping the per-row trailing bytes past `width * bpp`. Unsupported formats log + drop the buffer; the compositor will pick another from the enum on the next negotiation. Existing capture test relaxed: with a live dev compositor + the portal path, capture can succeed even without DISPLAY/WAYLAND_DISPLAY set; the test now sanity-checks that the agent doesn't panic and accepts any non-error outcome. 24 meshagent-kvm tests pass. Cursor-jump artifact gone; frame rate limited only by JPEG encode + relay pump now (next: bump from 5fps default to 15-30fps once we measure encode cost). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>The SPA's Desktop panel polls the remote clipboard (`{action:"msg", type:"getclip", tag:N}`) and writes back via `type:"setclip", data:<text>` from its clipboard dialog and the desktop-side paste shortcut. Until now both were silently dropped ("unhandled msg type" debug line per request). `reply_getclip`: shells out to `wl-paste --no-newline --type text/plain` when WAYLAND_DISPLAY is set; otherwise `xclip -selection clipboard -o`. KDE Plasma + GNOME hold the active selection on the Wayland clipboard, not the X11 selection (XWayland exposes a separate one), so the wl-clipboard tools are the only ones that see what the user actually copied. Empty clipboard or absent tool yields an empty string so the SPA's `message.data` stays a well-formed string. `reply_setclip`: same tool selection in reverse, piping the SPA-supplied `data` field into the tool's stdin. Replies `{success: bool}` so the SPA's clipboard-dialog status flashes green on success / red on failure. System deps: `wl-clipboard` and/or `xclip` on PATH. Both already installed on the dev host (`/usr/bin/wl-paste`, `/usr/bin/wl-copy`, `/usr/bin/xclip`) so this works out of the box. 2 new unit tests cover the reply envelope shape (data field is a string for getclip; success is a bool for setclip). Existing 8 host::msg tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>`host::msg::handle` previously dropped these sub-types as "unhandled" debug. Wire each one with the legacy SPA's expected reply envelope. cpuinfo: surfaces `host_cpuinfo::snapshot()` (logical processors, model, vendor, mhz, cache size). Same data the sysinfo bundle includes, but emitted as its own msg sub-type so the deskTools cpu dialog can fetch it standalone. pskill: SIGTERM the requested pid via `libc::kill`. Replies `{success: bool, error?: <io::Error>}`. Rejects pid 0 and propagates errno text on failure. openUrl: `xdg-open <url>` (`open` on macOS, `cmd /c start` on Windows). Rejects non-http(s) schemes so an attacker can't push `file://...` or `javascript:` through the agent. serviceStart / serviceStop / serviceRestart: shells out to `systemctl <action> <unit>`. Conservative unit-name filter (`is_safe_unit_name`) restricts to `[A-Za-z0-9_.@-]` and a 256-byte cap so a stray request can't smuggle `; rm -rf /` into the command line. Replies `{success: bool, error?: <stderr>}`. 5 new unit tests cover cpuinfo shape, openUrl scheme filter, pskill on invalid pid, the unit-name filter directly, and a service-action attempt with an injection attempt. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>The legacy SPA emits both names interchangeably for "pop a dialog on the agent host." Wire both via a tiered fallback: 1. kdialog --title T --msgbox BODY (KDE) 2. zenity --info --title T --text BODY (GNOME / GTK) 3. notify-send T BODY (libnotify; transient) Replies `{success: bool, error?: ...}`. Empty body -> success=false without launching anything. If none of the three tools is on PATH, returns `error: "no notification tool available"`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>Add a tunnel handler for the SPA's `uploadhash` action so the resume-upload + skip-existing-file paths in `p13gotUploadData` actually fire. SPA already sends `{ action: 'uploadhash', reqid, path, name, tag: { h, s, skip } }` before each upload when a same-named smaller file exists; the agent now hashes the first `tag.s` bytes of the on-disk file with SHA-384 and replies `{ action: 'uploadhash', reqid, tag, hash: <upper-hex> }`. The SPA compares `cmd.tag.h === cmd.hash`: - equal + tag.skip => skip the file (already uploaded). - equal + !tag.skip => append starting at byte tag.s. - mismatch => overwrite from byte 0. Without this handler the SPA's `uploadhash` send never got a reply, so the resume codepath was effectively dead and every upload re-transmitted from byte 0. Hash is computed in 64 KiB chunks to keep big files off the heap.wayland-portalcargo feature c61be48c32- `host/tunnel.rs`: real `copy` + `move` handlers next to the existing `ls`/`mkdir`/`rm`/`rename`. SPA's "Paste" button emits `{action:"copy"|"move", scpath, dpath, names:[...]}`; we walk the names list calling `std::fs::copy` / `std::fs::rename` per entry, surface the first error if any, otherwise reply `refresh` so the dialog re-lists. Closes the long-standing "msg/tunnel files: action not yet implemented" debug log for these two SPA verbs. - `host/process.rs::kill`: refresh the doc comment to be honest about why Windows support is held - it'd require pulling in `windows-rs` just for `OpenProcess` + `TerminateProcess` and the agent's other Windows-specific paths haven't done so either, so adding the dep here would be premature. - `net.rs`: refresh the module docstring's "wired but not yet used" claim about `ServerTlsCertHash`. The cache *is* used for the agentcore.c:3640-3653 fast-path (preemptive AuthConfirm on reconnect when the hash matches); the comment was stale. Windows process kill, Windows update apply, and a real LAN-side mDNS listener each need their own dedicated work and stay in the existing platform-cfg arms.Two modes, both Linux-only for now: - `meshagent install --system --msh server-issued.msh` (root): - Copies `current_exe()` to `/usr/local/sbin/meshagent` (mode 755). - Persists the `.msh` at `/etc/meshagent/meshagent.msh` (mode 600 - it carries the ServerID hash + MeshID). - Creates `/var/lib/meshagent/` for the sled store. - Creates the `meshagent` system user (`useradd --system --no-create-home --shell /usr/sbin/nologin`) and chowns the state dirs to it. - Writes `/etc/systemd/system/meshagent.service` with the standard hardening sandbox (`NoNewPrivileges`, `ProtectSystem=strict`, `ProtectHome`, `ProtectKernelTunables`, etc.) and `User=meshagent`. - `systemctl daemon-reload && systemctl enable --now meshagent.service`. - `meshagent install --user --msh ...` (no root): - Same flow but rooted at `~/.local/bin`, `~/.config/meshagent`, `~/.local/share/meshagent`, `~/.config/systemd/user`. - Runs inside the user's session so X11/Wayland/clipboard access works for KVM. No sandbox directives - they break session-bus / Wayland-socket access. - `systemctl --user daemon-reload && systemctl --user enable --now meshagent.service`. Uninstall (both modes) is the inverse and idempotent - every step (disable + remove unit + drop binary + drop .msh) tolerates "not present". State directories are deliberately preserved on uninstall so an admin doesn't accidentally lose audit-relevant agent state; it's noted in the log message and can be removed manually. CLI: - New `Cmd::Install { --system | --user, --msh }` and `Cmd::Uninstall { --system | --user }` subcommands. - The legacy implicit-run flow (no subcommand, just `--msh ... --data-dir ...`) keeps working unchanged - the dev `cargo watch` loop in `compose.yml` doesn't need a wrapper change. - `--once` accepted at both the top level (legacy) and on the explicit `Run` subcommand; top-level wins on conflict. 3 new unit tests cover the unit-file rendering + user-layout path resolution. The full agent test suite has some pre-existing parallel-test flakiness in PTY / TCP-forward paths that surfaces once the test count goes up; those tests pass cleanly in isolation and aren't related to this change.Replaces the legacy JS-meshcore extensibility model with a wasmtime sandbox the agent runs locally. Server pushes signed wasm modules via a new `loadcore` JSON action; agent verifies + persists + hot-loads them; the dispatcher forwards every unrecognized JSON verb to the loaded guest's `on_event_json` export. New crate feature: `meshcore` (default-on). Pulls in `wasmtime = 26` (cranelift + runtime). Disable for minimal agent builds: `cargo build --no-default-features`. The runtime adds ~5 MB to the binary. New module `meshagent::core`: - `MeshCoreInstance::from_bytes` validates the module has the required exports (`memory`, `alloc`, `on_event_json`), constructs a fuel-limited (100M units / call) wasmtime engine + Module, computes SHA-384. - `on_event_json(action, payload) -> Option<Vec<u8>>` calls the guest's export. Wire ABI: guest's `alloc(len) -> i32` returns a buffer, host writes the action bytes + JSON payload, calls `on_event_json(action_ptr, action_len, payload_ptr, payload_len) -> i64`. The packed return value is `(reply_ptr << 32) | reply_len`; 0 means "no reply". - `load_from_action(data_dir, store, signing_key, envelope)` verifies the action's signature against the AuthVerify cert pubkey (same trust chain as `oobupdate`), fetches the URL, verifies SHA-384 matches, persists the bytes at `<datadir>/meshcore.wasm`, returns the loaded instance. - `load_persisted(data_dir)` rehydrates a previously-pushed module on agent startup. - `MeshCoreSlot` is the hot-swappable Arc<Mutex<Option<...>>> that the dispatcher consults; `try_dispatch(action, payload)` is a one-call shortcut for the unrecognized-verb path. Dispatch wiring (`dispatch.rs`): - New `DispatchContext` fields (feature-gated): `meshcore` slot, `store`, `data_dir`, with a `with_meshcore(...)` builder. - New `loadcore` arm calls `core::load_from_action`, installs into the slot, replies `{ok: true, hash}` or `{error}`. - The `other =>` arm forwards to `slot.try_dispatch(action, envelope)`; the slot returns `None` when no core is loaded or when the guest opts out, falling through to the existing "unhandled" debug log. Net wiring (`net.rs`): - `run_connection`, `run_reconnecting`, and `Handshake::new` take a new `&Path data_dir` arg. - `Handshake::new` constructs a fresh `MeshCoreSlot`, attempts `core::load_persisted` to rehydrate, attaches the slot via `ctx.with_meshcore` on every JSON dispatch. Main wiring (`main.rs`): - Both run-paths pass `&data_dir` through to net. Tests: 4 new units in `core::tests` (build minimal `(module ...)` via `wat`, verify `from_bytes` rejects missing exports, verify the empty-reply path, verify the empty-slot dispatch path). `wat = "1"` added to dev-dependencies. Future: when an actual extension hits the limits of the verb-forwarding model (host imports for db/log/event/fetch_url, manifest declarations, signature-bound permissions), grow the host-import surface from inside `core::on_event_json` -- the wasmtime `Linker` is the single point that needs to register imports.