You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
jinwei-pikmin/instance-mcp linux-rust-port (0ed709c, 2026-09-28) is an independent Rust port of the daemon. Built on rpi1: 3 m 17 s cold, 5 MB binary, cargo test 54/54. It is complementary to our poc/reverse-attach-linux/ (#29, #30): it has the ADR-shaped structure and several capabilities we lack; we have the wlroots desktop backend (its own comment: "wlroots compositors (sway, labwc) … need a grim/ydotool backend, not written yet"; on rpi1 it lists only sys_info exec*), the MCP_UPSTREAM browser proxy (theirs ⏳), and the deployed/verified path (lent session, Connect Screens pane, Pi headless seat).
This issue tracks what we adopt from it into our code, as PRs we own. Each item is one PR, roughly in this order. Baseline for "done": smoke.sh still passes on rpi1 and in CI, the lent kiro-1040 session and the Connect Screens pane keep working.
Items
1. Structure: split main.rs into modules behind a Desktop trait (#18 shape)
Today: one 1.9k-line main.rs. Target layout, mirroring theirs so a later convergence is a diff not a rewrite: auth.rs · http.rs · mcp.rs (dispatch + upstream proxy) · attach/{mod,client}.rs · tools/{sysinfo,screen,bash,input}.rs · platform/desktop.rs (trait: capture, pointer, key, displays) · platform/linux/wlroots.rs (grim/wlrctl/wtype — what we have).
Acceptance: behaviour-preserving; cargo clippy -D warnings; smoke.sh 38/38.
2. Unit tests, especially the attach state machine
They have 54 tests, 13 on attach disposition/backoff/cancel. We have 0 unit tests and rely on smoke.sh + the #24 conformance vectors. Port the disposition/backoff/replacement/DELETE cases as #[test]s against ra_vectors.json so CI does not need the mock runtime for the core logic. Keep smoke.sh as the integration layer.
Acceptance: ≥ the 23 #24 vectors + cancel/replace/deadline cases covered; runs in the linux-hands-node CI job.
3. Concurrent replies on the attach socket
Theirs: each inbound frame is answered on its own task and replies go out through an mpsc by JSON-RPC id, so a slow screenshot (≈1 s on a Pi) does not block sys_info. Ours answers frames serially inside the read loop. With std threads: spawn per request, single writer thread draining a channel; preserve the 1 s read-timeout cancel path and the Close-frame flush.
Acceptance: a bash sleep 3 and a sys_info sent back-to-back over the mock return sys_info first.
Same contract as the Swift daemon (README table): job_id, tee'd .out/.err under $XDG_STATE_HOME/oab-instance-mcp/jobs/, byte-offset incremental poll, setsid + killpg, timeout_secs=0 = no timeout, 10 most recent finished kept, logs GC'd after 7 days. Their tools/jobs.rs is a good reference (background writers survive exec; see their last commit).
Acceptance: start a 30 s job, poll twice with offsets, cancel, poll once more → killed, exit code present; smoke cases added.
5. In-process JPEG via the image crate
Debian grim has no libjpeg, so every frame is PNG (~2 MB at 1080p; Connect polls ≤2 FPS → 4 MB/s). Decode grim's PNG and encode JPEG at the requested quality (Connect sends 0.6; accept both 0–1 and 1–100 like the Swift daemon). Keep PNG when asked.
Acceptance: Connect-shaped call returns image/jpeg ≤ 300 KB at 1080p q0.6; structuredContent.points {width,height} present (Connect reads it).
6. wss:// mint and the TLS revoke fast path
Dial already works over TLS (rustls). Missing: mint over https:// (our http_post is http-only) and the 1 s read-timeout cancel is applied only to MaybeTlsStream::Plain, so DELETE /attach/{id} on a wss:// runtime takes effect at the next inbound frame. Either use rustls directly or switch the tiny client to ureq/hyper — pick whichever keeps the dependency count low.
Acceptance: mock runtime behind a self-signed TLS listener (add --tls to mock_runtime.py): mint + dial + DELETE within 1 s.
7. systemd unit hardening
KillMode=process (apps opened via bash survive a daemon restart), PartOf=graphical-session.target, Documentation=. Apply in scripts/install-linux.sh and docs/linux-setup.md.
8. --log-requests and Swift-style flags
One log line per session open / tools/call / deny (Swift agent.log shape) behind a flag; accept --allow-login, --token-file, --insecure-local, --no-attach, --no-desktop, --host/--port as CLI flags in addition to the env vars, so Connect/Remote docs apply to both platforms unchanged.
9. Unicode / CJK typing
wtype handles unicode natively for most apps, but their IBus ctrl+shift+u + hex + space path covers GTK/Qt apps where virtual-keyboard unicode fails. Add it as a fallback in key.type when the text has non-ASCII and wtype reports failure or the app is known to drop it. Also accept cmd as an alias for ctrl in key.press like theirs.
Acceptance: type 你好 into the Chromium URL bar on rpi1 and read it back via screenshot.
10. Stuck-modifier safety
Their review-fix commit releases modifiers if a press fails mid-sequence. Our tool_key-M … -k … -m … argv is atomic per wtype call, but a wtype failure after -M could leave a modifier down in the compositor. Wrap with a best-effort wtype -m <mod> on error.
Not adopting
xdg-desktop-portal RemoteDesktop backend: GNOME/KDE only, needs a consent dialog; our targets are wlroots seats (Pi OS labwc, sway). Keep as a possible second Desktop impl if a GNOME node ever appears — item 1 makes that a drop-in.
Once items 1–3 land our tree has the same shape as theirs; at that point the sensible end state is one Rust crate with two Desktop impls (portal + wlroots) and the upstream proxy. Whether that happens by us merging their portal backend or them merging ours is a conversation for when their PR appears; this issue is about making our side ready for it.
Context
jinwei-pikmin/instance-mcp
linux-rust-port(0ed709c, 2026-09-28) is an independent Rust port of the daemon. Built on rpi1: 3 m 17 s cold, 5 MB binary,cargo test54/54. It is complementary to ourpoc/reverse-attach-linux/(#29, #30): it has the ADR-shaped structure and several capabilities we lack; we have the wlroots desktop backend (its own comment: "wlroots compositors (sway, labwc) … need a grim/ydotool backend, not written yet"; on rpi1 it lists onlysys_info exec*), theMCP_UPSTREAMbrowser proxy (theirs ⏳), and the deployed/verified path (lent session, Connect Screens pane, Pi headless seat).This issue tracks what we adopt from it into our code, as PRs we own. Each item is one PR, roughly in this order. Baseline for "done":
smoke.shstill passes on rpi1 and in CI, the lentkiro-1040session and the Connect Screens pane keep working.Items
1. Structure: split
main.rsinto modules behind aDesktoptrait (#18 shape)Today: one 1.9k-line
main.rs. Target layout, mirroring theirs so a later convergence is a diff not a rewrite:auth.rs·http.rs·mcp.rs(dispatch + upstream proxy) ·attach/{mod,client}.rs·tools/{sysinfo,screen,bash,input}.rs·platform/desktop.rs(trait:capture,pointer,key,displays) ·platform/linux/wlroots.rs(grim/wlrctl/wtype — what we have).Acceptance: behaviour-preserving;
cargo clippy -D warnings;smoke.sh38/38.2. Unit tests, especially the attach state machine
They have 54 tests, 13 on attach disposition/backoff/cancel. We have 0 unit tests and rely on
smoke.sh+ the #24 conformance vectors. Port the disposition/backoff/replacement/DELETE cases as#[test]s againstra_vectors.jsonso CI does not need the mock runtime for the core logic. Keepsmoke.shas the integration layer.Acceptance: ≥ the 23 #24 vectors + cancel/replace/deadline cases covered; runs in the
linux-hands-nodeCI job.3. Concurrent replies on the attach socket
Theirs: each inbound frame is answered on its own task and replies go out through an
mpscby JSON-RPC id, so a slowscreenshot(≈1 s on a Pi) does not blocksys_info. Ours answers frames serially inside the read loop. With std threads: spawn per request, single writer thread draining a channel; preserve the 1 s read-timeout cancel path and the Close-frame flush.Acceptance: a
bash sleep 3and asys_infosent back-to-back over the mock returnsys_infofirst.4. Background jobs:
exec_start/exec_poll/exec_list/exec_cancelSame contract as the Swift daemon (README table):
job_id, tee'd.out/.errunder$XDG_STATE_HOME/oab-instance-mcp/jobs/, byte-offset incremental poll,setsid+killpg,timeout_secs=0= no timeout, 10 most recent finished kept, logs GC'd after 7 days. Theirtools/jobs.rsis a good reference (background writers surviveexec; see their last commit).Acceptance: start a 30 s job, poll twice with offsets, cancel, poll once more →
killed, exit code present; smoke cases added.5. In-process JPEG via the
imagecrateDebian grim has no libjpeg, so every frame is PNG (~2 MB at 1080p; Connect polls ≤2 FPS → 4 MB/s). Decode grim's PNG and encode JPEG at the requested
quality(Connect sends 0.6; accept both 0–1 and 1–100 like the Swift daemon). Keep PNG when asked.Acceptance: Connect-shaped call returns
image/jpeg≤ 300 KB at 1080p q0.6;structuredContent.points{width,height} present (Connect reads it).6.
wss://mint and the TLS revoke fast pathDial already works over TLS (rustls). Missing: mint over
https://(ourhttp_postis http-only) and the 1 s read-timeout cancel is applied only toMaybeTlsStream::Plain, soDELETE /attach/{id}on awss://runtime takes effect at the next inbound frame. Either userustlsdirectly or switch the tiny client toureq/hyper— pick whichever keeps the dependency count low.Acceptance: mock runtime behind a self-signed TLS listener (add
--tlstomock_runtime.py): mint + dial + DELETE within 1 s.7. systemd unit hardening
KillMode=process(apps opened viabashsurvive a daemon restart),PartOf=graphical-session.target,Documentation=. Apply inscripts/install-linux.shanddocs/linux-setup.md.8.
--log-requestsand Swift-style flagsOne log line per session open /
tools/call/ deny (Swiftagent.logshape) behind a flag; accept--allow-login,--token-file,--insecure-local,--no-attach,--no-desktop,--host/--portas CLI flags in addition to the env vars, so Connect/Remote docs apply to both platforms unchanged.9. Unicode / CJK typing
wtypehandles unicode natively for most apps, but their IBusctrl+shift+u+ hex + space path covers GTK/Qt apps where virtual-keyboard unicode fails. Add it as a fallback inkey.typewhen the text has non-ASCII andwtypereports failure or the app is known to drop it. Also acceptcmdas an alias forctrlinkey.presslike theirs.Acceptance: type
你好into the Chromium URL bar on rpi1 and read it back via screenshot.10. Stuck-modifier safety
Their review-fix commit releases modifiers if a
pressfails mid-sequence. Ourtool_key-M … -k … -m …argv is atomic perwtypecall, but awtypefailure after-Mcould leave a modifier down in the compositor. Wrap with a best-effortwtype -m <mod>on error.Not adopting
Desktopimpl if a GNOME node ever appears — item 1 makes that a drop-in.bashin both profiles).Convergence
Once items 1–3 land our tree has the same shape as theirs; at that point the sensible end state is one Rust crate with two
Desktopimpls (portal + wlroots) and the upstream proxy. Whether that happens by us merging their portal backend or them merging ours is a conversation for when their PR appears; this issue is about making our side ready for it.