Repository navigation
Conversation
Add seven input.* methods driving input.device IND_WRITEEVENT: state, type, key, mouse_move, click, drag, scroll. Goes through MCPd rather than QMP, so it works on real hardware and QEMU alike. Injection is off at the daemon and returns -32003 until the operator passes --enable-input or creates SYS:System/MCPd/ENABLE-INPUT. The gate is read once at startup, so no RPC can switch it on. The sentinel file is the primary mechanism because MCPd-Watchdog and the S:Network-Startup line both relaunch without arguments, which would silently drop a flag-only gate on the first restart. The host-side [targets.<n>.input] block is a wrong-target guard, not access control. type/key/click/drag also require confirm:true; ctrl+lamiga+ramiga additionally requires confirm_reset:true. Typing maps characters through the target's keymap.library MapANSI, including dead-key sequences, so non-US layouts get the intended characters; a built-in US table is the fallback and the result reports which path ran. Caps enforced daemon-side: 256 events, 20s wall clock, 512 chars, 64 drag steps. Held modifiers and buttons are always released before returning, including on the abort path, so a truncated drag cannot leave a stuck mouse button. Also unify the daemon version behind MCPD_VERSION_STR in rpc.h -- main.c and rpc.h previously carried independent literals, so MCPd --version and proto.version could disagree. Bump to 1.3; the wire protocol stays at 1.0 since input.* is purely additive. Verified against a QEMU Pegasos2 guest (AmigaOS 4.1 FE, Kickstart 54.57): gate closed by default and after a cold boot, enable/disable lifecycle, pointer positioning, and typing end-to-end through a Shell on a German keymap.
|
Merged — this landed in v1.3 via #3, with your commit cherry-picked onto Closing this one rather than merging it directly because it was based on v1.2 and targeted Four things changed on the way in, all documented in #3:
Verified end-to-end by a new The security design was the part worth keeping as-is: a gate resolved once at startup into a read-only static so no RPC can escalate, and the sentinel file rather than a CLI flag precisely because the watchdog relaunches argument-less. Thanks for that reasoning — it's what made this straightforward to accept. |
Summary
Adds an
input.*namespace — keyboard and mouse injection on the target — drivinginput.deviceIND_WRITEEVENTfrom MCPd. Seven methods:state,type,key,mouse_move,click,drag,scroll.It goes through MCPd rather than QMP, so it works on real PowerPC hardware and QEMU guests alike, and composes with the existing read-only
wb.*tools for coordinates.This closes a real gap: an agent could previously see that a requester was on screen but not dismiss it, or launch a GUI installer but not click Proceed.
Security model
Input injection is categorically more dangerous than the rest of the surface — it types and clicks as the logged-in user, into whatever window is focused. So it is off by default at the daemon, and that switch, not the host config, is the real boundary.
input.*returns-32003until the operator either starts MCPd with--enable-inputor createsSYS:System/MCPd/ENABLE-INPUT(MCPd-Enable-Input), then restarts. The gate is read once at startup into a read-only static, so:The sentinel file is the primary mechanism, and that's deliberate.
mcpd/install/MCPd-Watchdog:24relaunches MCPd with no arguments, andMCPd-Installappends an argument-less launch line toS:Network-Startup. A CLI-flag-only gate would therefore be silently dropped on the first watchdog restart — the worst failure mode for a security control, since it fails open in the operator's mental model. The sentinel survives restarts, andMCPd-Uninstall's recursive delete already removes it.proto.capabilitiesreportsinput.enabled, and the methods stay advertised when the gate is closed so clients can distinguish "switched off" from "old daemon".Secondary gates:
[targets.<n>.input] enabled— a wrong-target guard, not access control, since anything reaching TCP 4322 bypasses the host. Documented as such.confirm: trueon the operations that can commit an action (type,key,click,drag); not onmouse_move,scroll,state.ctrl+lamiga+ramigaadditionally requiresconfirm_reset: truebecause it reboots the machine.Typing is layout-correct
Characters are mapped through the target's own
keymap.libraryMapANSI, including dead-key sequences. A built-in US table is the fallback ifkeymap.librarycan't be opened, and the result'skeymapfield reports which path ran. Unmappable characters come back inunmapped[]rather than being silently dropped.Safety
Per-call caps enforced daemon-side (host-side ones are advisory): 256 events, 20 s wall clock, 512 characters, 64 drag steps,
delay_ms0–1000. A call that hits a cap stops cleanly and returnstruncated: true.Every handler releases held modifiers and mouse buttons before returning, including on the abort path — a truncated
input.dragcannot leave the target with a stuck mouse button.input.stateis the "look before you click" method: pointer position, focused window, screen geometry. Deliberately not included is "seconds since last user input" — there's no supported AOS4 API for it short of installing a commodities input handler, so it's omitted rather than approximated.Incidental fix
main.candrpc.hcarried independent version literals, soMCPd --versionandproto.versioncould disagree (andmcpd/Makefilehad already drifted to 1.1). Unified behindMCPD_VERSION_STRinrpc.h. Bumped to 1.3; the wire protocol stays at 1.0 sinceinput.*is purely additive andproto.capabilitiesderives its method list dynamically.Verification
Host: 251 tests pass,
ruffandmypyclean. Newtests/unit/test_input.pyfocuses on the disabled-by-default path — asserting both that the error is raised and that nothing reached the wire — plus a test pinningTargetConfig(...).input is Noneso a refactor can't silently make injection opt-out.Exercised against a QEMU Pegasos2 guest (AmigaOS 4.1 FE Update 3, Kickstart 54.57):
-32003,input.enabled: falseMCPd-Enable-Input→ restart → open;MCPd-Disable-Input→ restart → closedS:Network-Startupauto-startEcho TYPED-OK >T:typed.txtinto a Shell produced exactly that file — on a German (QWERTZ) keymap, which is what forced thekeymap.librarypathconfirmgates and theconfirm_resetreset-chord guard all reject as designedKnown limitations
NM_WHEEL_*NewMouse constants are still unverified — flagged inmcpd/src/methods/input.c. The wheel constants are behind#ifndefguards so a missing header can't break the build.input.mouse_movedefaults to read-position-then-relative-delta (verified accurate).absolute_mode="raw"exposes the true-absolute event form for A/B testing on other hardware.input.clickdoesn't report the achieved pointer position the waymouse_moveanddragdo.input.typeis recorded in cleartext in the run archive. That's intended — it's an audit log, and silently redacting one is worse than not keeping it — but it's called out inSECURITY.md, andtext_sha256lets a reviewer confirm what was typed without re-reading the plaintext.Note on the build
The binary used for the on-target verification was cross-compiled with a local adtools GCC 8.4.0 toolchain, not the
walkero/amigagccondocker:os4-gcc11image CI uses. The daemon reports its toolchain inproto.version/--version, so any target is identifiable. CI's cross-compile job should be treated as the authority for the shipped artifact.Files
mcpd/src/methods/input.c(new), plus wiring inrpc.c,methods.h,main.c,proto.c,Makefilemcpd/install/MCPd-Enable-Input/MCPd-Disable-Input(new), copied byMCPd-Installbut never run by ithost/src/amiga_fleet_mcp/tools/input.py(new),config.pyInputConfig, 7 tools +inputdispatcher inserver.py,--initwizard questionSECURITY.md,COMMANDS.md,USAGE.md,CHANGELOG.md,CONTRIBUTING.md