SideWire
Drive a Linux box from an AI agent running on Windows. Same LAN is enough. No SSH, nothing to install on the Linux side.
The Windows machine has the agent (DeepSeek Harness, Claude, Codex, doesn't matter). The Linux machine runs one Python file. After that the agent can run commands, read and write files, look at processes, restart services. Roughly like working locally.
you (Windows) -- MCP stdio --> sidewire -- TCP --> sidewire_agentd (Linux)
Why another one
I had winremote-deploy-kit first. That one wrapped someone else's project: install a service, open a firewall port, add a relay to work around Session not found. It also only did Windows to Windows.
The awkward part was the base it sat on. Streamable HTTP keeps session state on the server. Restart the server and the mcp-session-id in the client's hand is stale, but the connection itself stays up. You end up with a client that looks connected and fails every tool call. The relay existed to patch that.
So the problem came from the transport, not from the code around it. This time I wrote the wire protocol.
- One long-lived TCP connection, my own binary frames. 16-byte header, CRC32 over the payload.
- Every frame carries a stream id, so requests really do run in parallel. A 40-minute compile doesn't block the
fs_readbehind it. - No sessions on the server. The connection is the session. Kill the process and the socket dies; the client reconnects and re-authenticates. There's no in-between state to get stuck in.
The cost is that I maintain a protocol now. The benefit is that the above stops happening.
Install
Three steps. Pretend the Linux box is 192.168.1.50.
1. Windows side
powershell -ExecutionPolicy Bypass -File scripts\install-client.ps1
It finds Python, copies things into %LOCALAPPDATA%\sidewire, makes a key, then asks whether it should register itself into DeepSeek Harness. Pass -NoDsh to skip that question, or -InstallDir D:\tools\sidewire if you don't want it under AppData.
2. Linux side
Copy the agent/ directory across. USB stick, scp, whatever you have.
sudo ./install-agent.sh --allow-sudo --allow-pkg
Four things happen: sidewire_agentd.py lands in /opt/sidewire, a key is generated at /etc/sidewire/agent.key, a systemd unit gets written, and it tries to open the firewall (ufw or firewalld, gives up quietly if it finds neither).
It prints the key and address when it finishes:
address : 192.168.1.50
port : 7900
key : sw1_9f3a...e21c
3. Back on Windows
python sw.py add sub 192.168.1.50 --key sw1_9f3a...e21c --default
python sw.py doctor
sub is just an alias. Call it lab, spare, nas, whatever. doctor walks config, key, connectivity and agent version, and tells you which one broke.
Then try it:
python sw.py sh "uname -a && df -h"
Telling the agent about it
For DeepSeek Harness:
python sw.py dsh-install
One warning, because this cost me an evening: DSH keeps MCP config in two different places, and which one matters depends on how you start it.
| How you start DSH | File it reads |
|---|---|
dsh from a terminal |
~/.dsh/profiles/<name>/cordis.patch.yml |
| the desktop app | ~/.dsh/.agent-presets/<name>/agent.cordis.yml |
dsh-install writes both now. Earlier versions only did the first, which meant the desktop app silently ignored the whole thing — no error, nothing in the log, the tools just never showed up. --preset <name> targets one preset, --no-preset stays on profiles only.
The desktop app reads presets once at startup, so close it properly and reopen. Quitting to the tray doesn't count.
For Claude Desktop, Cursor and anything else that takes a JSON block:
python sw.py mcp-config --client claude
Codex uses --client codex. It prints the block, you paste it in.
The 30 tools
Names are short on purpose. Every one takes an optional host argument, which is how you switch between machines.
Which machines: hosts, host_info, host_probe (latency)
Commands: run, run_script (writes a temp file and runs it, so you skip quote hell), job_list, job_poll, job_kill
Interactive: tty_open, tty_write, tty_read, tty_close. Real PTY. You can ssh onward to a third machine or drive an interactive installer.
Files: fs_ls, fs_read, fs_write, fs_edit (patch one spot, leave the rest alone), fs_stat, fs_find, fs_grep (search on the target instead of pulling the tree over), fs_manage (mkdir/rm/mv/cp/chmod/chown/symlink/truncate in one), fs_pull, fs_push, fs_tail, fs_hash
System: proc_list, proc_kill, svc (systemctl, openrc or sysv, detected), net (interfaces, listening ports, reachability, DNS), pkg (apt, dnf, pacman, zypper, apk, detected), power
fs_pull deserves a note. It downloads the file to local disk and hands back a local path, so the agent can use its own file tools on it. Binaries, images, a 50 MB log — all of that goes this way instead of base64 into the context window.
Design choices, briefly
Not HTTP. Tried it. HTTP needs either long polling or SSE, and multiplexing over one connection means inventing a lot of semantics. "Connection fine, session dead" is a state that exists by construction. Frames on raw TCP are simpler: if something is wrong the connection breaks and you reconnect.
Not plain SSH. SSH is right there and the agent could just use it, but a few things get annoying. Credential handling, a new session per exec (slow), assembling output and exit codes by hand, timeouts and process groups. Plenty of Windows boxes also ship without an ssh client. All solvable, but solving them is most of what I wrote here anyway.
Crypto. No TLS, because I didn't want an openssl dependency just for a self-signed cert. It's assembled from stdlib: HMAC-SHA256 in counter mode as the keystream, an 8-byte truncated tag, encrypt-then-MAC. Enough to stop someone casually sniffing the LAN from reading your paths and commands. Not enough for anyone actually trying. Anything off-LAN should go over WireGuard or Tailscale, don't count on this.
Frame ordering. Worth remembering: seal and write have to be one atomic operation. The first version split them, and 12 concurrent requests were enough to put the counter out of order — the peer answered bad frame tag. Both sides now hold one lock across encrypt-and-send.
Tests
python tests/e2e.py # 27
python tests/test_protocol.py # 18
python tests/test_mcp.py # 13
python tests/test_dsh.py # 7
65 total.
e2e.py starts a real agentd and hits it with a real client. Handshake, wrong key rejected, command execution, exit codes, timeout turning into a job, PTY, the whole file family, a 300 KB binary round trip, output truncation, allow_root refusing an escape, 12 concurrent requests, reconnect after a drop.
test_protocol.py covers frame encode/decode, CRC, keystream, tag tampering, counter reordering, plus a byte-for-byte comparison of the two protocol implementations. agentd is a single file, so it carries its own copy of the protocol code, and that test is what keeps the copies honest.
test_dsh.py parses the generated DSH YAML back in both shapes. Wrong nesting still produces a file that looks fine and gets ignored, which is the failure mode worth testing for.
Numbers from this machine: sys.ping around 0.3 ms, file read/write 1-5 ms, 300 KB push and pull 90 ms. Command execution on Linux is basically the command's own time. On Windows every exec spawns a cmd.exe, so add about 120 ms. That's the OS, not the protocol.
Permissions, and things not to do
This hands over a shell. Think about it while installing, not after.
- The key is everything.
/etc/sidewire/agent.keyand the copy in~/.sidewire/on the Windows side. Leaking either is equivalent to losing the machine. - The default is
--allow-root /, meaning the whole filesystem is readable and writable. Narrow it with--allow-root /home/you/project; anything outside comes backdenied. --allow-sudo,--allow-pkgand--allow-powerare all off by default.poweralso wants aconfirmargument that has to match the action name exactly.- Don't port-forward this to the internet. If you need it across networks, WireGuard or Tailscale plus
--allow-cidr 10.0.0.0/24. - Every request goes to the audit log (address, method, duration, result, argument summary).
journalctl -u sidewire-agentd.
Known issues
- Running agentd on Windows gives you a fake
tty(a pipe, not a PTY) and a degradedproc_list. Windows is only there so I could test the protocol. Use Linux for real. proc_listCPU% is a delta between two calls, so the first call after connecting usually reports zeros.fs_grepwalks files in pure Python. Tens of thousands of small files will be slow. If it matters, have the agentrun "grep -r"instead.- No compression. Text goes over as-is. The LAN is fast enough that I didn't bother.
- Reconnect happens on the next request, not proactively. There's a keepalive ping but it doesn't redial on its own.
- If the target is a shared machine and the other person should be able to cut you off, don't install a service. Use
agent/run-on-windows.bat, which binds only to the Radmin LAN address and dies when its window closes.docs/DEPLOY.mdexplains the trade-off.
Layout
sidewire/ client (MCP server + CLI)
protocol.py frame format, sealing, handshake
client.py connection, multiplexing, file transfer
config.py ~/.sidewire/hosts.json
cli.py command line
mcp/server.py JSON-RPC over stdio
mcp/tools.py the 30 tool definitions and handlers
agent/
sidewire_agentd.py single-file daemon, stdlib only, reads /proc directly
install-agent.sh one-shot installer (Linux)
run-on-windows.bat foreground launcher for a Windows target, no service
sw.py launcher, no install needed
docs/ protocol, deployment, security, agent notes
tests/ 65 tests
agentd is a single file on purpose. Copy one .py over and it runs. No dependencies, no virtualenv, no pip install. The price is that the protocol code exists twice, once in the client and once in agentd, and test_protocol.py exists to keep the two identical.
License
MIT.
The name is arbitrary. Wire is the thing between two machines, cross is going across it. Rename it if you don't like it, nothing is hardcoded.