Drive an AROS machine from a host — run commands, push and pull files, inject input, take screenshots — over a connection the guest opens outward.
That direction is the whole design. A VM behind QEMU user-mode networking, or a real machine behind NAT, cannot be connected to. So the agent dials the host, the host keeps a persistent server, and everything else rides that one socket.
Two agents, one protocol:
- ATCPGUI — a MUI window showing connection state and a live log. What you want when a person is watching.
- ATCPBIN — headless,
-nodefaultlibs, no C runtime. What you want when a screen test is running, because the GUI's own window lands in every capture.
Both are plain C against AROS LVOs. Neither depends on the project they were written in; the export that produces this repo fails if a guest source ever includes a Vulkan header.
- QUICKSTART.md — the shortest path to a working link.
- INSTALL.md — building, deploying, and the ABI traps.
- REMOTE-UI.md — input injection and screen capture.
- DESIGN.md — the protocol and the reasoning behind it.
- TRANSFER.md — why file transfer ran at ~100 KB/s, and the pacing + chunk-size fixes already shipped.
Nothing works until AROSTCP is up with a default route. The agent dials out, so a missing stack is not a degraded mode — it is silence, and from the host it looks exactly like a broken agent.
It is worse than silence if the route is missing. Without a default route the guest's first TCP send does not fail, it panics the whole machine:
TCP/IP PANIC: tcp_output
tp->t_template cannot be built without a route, and AROSTCP panics rather than
returning an error. So guest/net/ is not optional furniture:
| file | what it does |
|---|---|
net/NETUP.AROS |
brings up AROSTCP only — no agent |
net/SPIKE.AROS |
brings up AROSTCP and launches the headless agent |
net/AROSTCP_IF |
interface config: static IP for QEMU user-mode networking |
net/AROSTCP_ROUTES |
the default gateway, which is what stops the panic |
net/INSTALL_AGENT.AROS |
one-time: install the boot autostart |
net/USER_STARTUP.AROS |
the autostart itself — network, then agent |
They overlap almost entirely, and merging them is the obvious cleanup. Don't.
The server accepts one agent at a time (backlog 1). SPIKE starts the
headless agent; if you also want the GUI, running SPIKE first leaves two agents
fighting over one slot — the second hangs in recv() and the first stops being
serviced, which presents as an agent that connected and then went deaf. NETUP
exists so the boot script can bring the network up and then start one agent of
its choosing.
Both are safe to run when AROSTCP is already up: WaitForPort AROSTCP doubles as
the is-it-running test. Skip it and a second AROSTCP pops a modal "Another
TCP/IP stack is running, please quit it first" requester, which on a headless
guest is unrecoverable.
QEMU user-mode (SLIRP) networking hands out a fixed subnet, so AROSTCP_IF uses
a static IP rather than DHCP — deterministic, and no client timing to race:
| guest | 10.0.2.15/255.255.255.0 |
| host / gateway | 10.0.2.2 |
| DNS | 10.0.2.3 |
On real hardware or bridged networking, edit both files. The stock AROS
db/static-routes ships DEFAULT GATEWAY 192.168.0.1, which does not exist
under SLIRP — that default is what the override in AROSTCP_ROUTES exists to
correct.
Both scripts copy the stock db into RAM:net/db and overlay these two files
there, so the disk image's own network config is never modified. Nothing to
undo.
Execute CD0:INSTALL_AGENT
once per disk image. It first refreshes the on-disk agent binaries
(SYS:ATCPBIN, SYS:ATCPGUI) from the CD — so the copy that will win the next
boot is the one on the CD, not whatever stale version the disk already held —
then copies USER_STARTUP.AROS to SYS:S/User-Startup (existing file preserved
as SYS:S/User-Startup.bak). The stock Startup-Sequence already executes
SYS:S/User-Startup if present — no system file is edited, and it is undone by
deleting one file. The autostart runs NETUP
inline (it returns, and the agent must not start before the network is up)
and the agent detached with Run <>NIL: (it loops forever, so running it
inline would hang the boot before Workbench appears). See
scripts/build_atcp_deploy_iso.sh for how a deploy CD bundles these.
| path | what it is |
|---|---|
guest/arostcp_gui.c |
ATCPGUI: MUI window, protocol handler, main loop |
guest/arostcp.c |
ATCPBIN: headless agent, single translation unit |
guest/arostcp_agent.c |
the agent loop both front-ends share |
guest/agent_state.c/.h |
shared state, telemetry, status rendering |
guest/frame_parser.c/.h |
length-prefixed frame reassembly |
guest/arostcp_shared.h |
socket helpers, run_exec_cmd, base64 |
guest/arostcp_json.h |
minimal JSON reader plus frame builders |
guest/atcp_dbghook.h |
captures the kernel bug()/kprintf stream |
guest/atcp_ui.c/.h |
input injection and screen capture |
guest/test_ui_geom.c |
host-runnable: close-gadget geometry and title matching |
guest/net/ |
AROSTCP bring-up and autostart — see above |
guest/build.sh |
cross-build both binaries |
host/spike_server.py |
the control server: serve, submit, status |
host/spike_control.py |
protocol and transfer implementation |
host/agent_diag.sh |
says which layer is broken before you restart anything |
host/hello_world.sh |
proves exec, put and get end to end |
host/ui_frames.py |
ui_input/ui_capture frame builders and reply parsing |
host/ui_png.py |
PNG read/write: Pillow when present, stdlib zlib otherwise |
host/ui_sweep.py |
unattended sweep: launch apps, capture each, report |
host/aros_screen_triage.py |
optional screenshot triage behind --triage (needs a VLM) |
host/frame_prefilter.py |
blank/duplicate frame filter used by the triage pass |
host/ui_smoke.sh |
proves input and capture work, and checks the pixels |
host/ui_probe.py |
asserts a capture is a live screen, not a flat buffer |
tools/aros_audit.sh |
repository invariant checks (run before a push) |
tests/ |
host-side suites for the server, pairs routing and UI helpers — no VM needed |
skill/ |
AROS development reference: cross-compiling, ABI traps, debugging |
Length-prefixed JSON frames: a 4-byte little-endian length, then the object.
The agent opens with hello, advertising its capabilities; the server then
drives with ping, exec_cmd, put_begin/put_chunk/put_end,
get_begin/get_chunk/get_end, prepare, run_script, and — only when the
hello advertised them — ui_input, ui_capture, ui_windows, ui_close.
File chunks are base64 with a SHA-256 check at put_end. Every reply has the
same shape, {"id":N,"type":"result","ok":…}; there is no per-command envelope.
Full detail in DESIGN.md; the UI frames in REMOTE-UI.md.
ui_input injects keyboard and mouse events through input.device, so Intuition
routes focus and qualifiers exactly as it does for real hardware. Posting
IntuiMessages at a window's UserPort instead reaches one window and bypasses
all of that; it was tried and rejected.
ui_capture returns an RGB24 snapshot of the active screen.
ui_windows returns Intuition's window list — which is a far better verdict than
a screen diff, because a blank window and a crashed app produce equally valid
PNGs.
Both agents have it. atcp_ui.c opens intuition.library and
cybergraphics.library itself rather than relying on autoinit, which is what
lets the headless binary — linked -nodefaultlibs, no C runtime — share the
code.
atcp_dbghook.h installs a SetFunction hook on exec's RawPutChar (LVO 86)
and captures the guest's bug()/kprintf stream for retrieval over the link.
Dump it with atcpdbg [file].
The hook needs an r12 trampoline: this ABI passes the library base in r12, and plain C reads garbage from RSI.
While the hook is installed, QEMU's -serial log stays empty. That is the
hook working, not the guest failing to print — worth knowing before you spend an
afternoon on it.
- Persistent. It survives agent restarts and reconnects, and takes work from a job spool rather than argv.
- One agent at a time (backlog 1). See above — this is load-bearing.
- It never sends
byeon its own; the agent stays connected between jobs. - When something stops responding, run
host/agent_diag.shbefore restarting anything. "The agent died", "the server is wedged" and "the guest is gone" are three different problems with three different fixes, and guessing between them costs VM restarts.
export TOOLCHAIN=/path/to/x86_64-aros-toolchain/bin
export AROS_SDK=/path/to/AROS/Developer/include
export AROS_LIB=/path/to/AROS/Developer/lib
cd guest && ./build.shbuild.sh carries the flags that are not optional and explains each one. The
short version: -ffixed-r12 because r12 carries a library base inside AROS's own
entry trampolines; -mcmodel=large for ATCPBIN because at small the base-global
accesses emit zero relocations, SysBase reads garbage, and
OpenLibrary("bsdsocket") returns NULL — it links, it runs, it simply never
connects; and -fno-builtin because GCC recognises the shim's byte-copy loops as
strlen/memcpy and emits calls into a libc that is not there.
What has to match across machines is the ELF ABI generation, not the compiler
version — check with x86_64-aros-readelf -h.