Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
9ec248b
installer: drop host bootstrap dep; bundle AmiDock prefs; clean COMBI:
derfsss May 9, 2026
8b169d4
sandbox.* phase A — probe + deploy + run_guest
derfsss May 10, 2026
e00cae1
sandbox.probe: don't gate on banner content, only on exec.cmd success
derfsss May 10, 2026
5f2441e
sandbox.probe: surface SandboxVM upstream URL in MISSING hint
derfsss May 10, 2026
cd58f1a
sandbox.probe: prefer pre-built release URL over from-source URL
derfsss May 10, 2026
40dc191
sandbox.* phase B — sys.debug_ring + run_driver + last_trap
derfsss May 10, 2026
358378f
sandbox.* phase C — sandbox.run_batch (multi-guest, ring-keyed exits)
derfsss May 10, 2026
0d0e414
docs: cover sandbox.* + sys.debug_ring + bootstrap_dir removal
derfsss May 10, 2026
ed0ac43
fleet.run_on_all: register sandbox.* + sys.debug_ring
derfsss May 10, 2026
ae7d751
sandbox.probe: distinguish unreachable target from missing binary
derfsss May 10, 2026
ea472a4
sandbox.probe: detect MCPd-transport TargetError as unreachable
derfsss May 10, 2026
6d28135
cli: add qemu_start/stop/status + qemu_run (headless on-target smoke …
derfsss Jun 8, 2026
f0c453a
wb.screenshot: on-Amiga screen capture to PNG
derfsss Jun 16, 2026
4541163
mcpd: listener priority 1 + debug-ring readiness beacon, bump to 1.3
derfsss Aug 2, 2026
f8b0489
input.*: keyboard and mouse injection, disabled by default
Jul 20, 2026
b4e7bef
input.type: decode UTF-8 before mapping; correct the stale keymap docs
derfsss Sep 10, 2026
7d3f15a
input gate: announce state in the debug ring, not just on stdout
derfsss Sep 10, 2026
3b57998
installer: deploy the MCPd-Enable-Input / -Disable-Input scripts
derfsss Sep 10, 2026
9697a53
docs: input.* tool counts, install-script table, agent guidance
derfsss Sep 10, 2026
70588e2
scripts: validate_input.py -- end-to-end check of the input.* gate
derfsss Sep 10, 2026
0c13cac
validate_input: real-hardware support, and the ring is not authoritative
derfsss Sep 10, 2026
b711eef
Release 1.3 — sandboxed iteration, screen capture, and input injection
derfsss Sep 10, 2026
cea8c7b
deps: cap mcp and pydantic majors so CI resolves something buildable
derfsss Sep 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ REQUIREMENTS.md
INVESTIGATION.md
IMPLEMENTATION.md
MCP_CAPABILITIES.md
SANDBOX_PLAN.md
docs/private/
plans/
notes/
Expand Down
11 changes: 10 additions & 1 deletion AGENTS_SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ amiga-fleet-mcp --inspect

# 2. List the live tool surface (no target reach required).
amiga-fleet-mcp --list-tools | wc -l
# Expected: 121 tools as of v1.2.
# Expected: 137 tools as of v1.3 (121 at v1.2).

# 3. Probe every target's MCPd channel (target reach required).
amiga-fleet-mcp --health-check
Expand Down Expand Up @@ -195,6 +195,7 @@ wiring.
| `paths.amiga_qemu_tests not set in config` | `tests.*` invoked but no `[paths] amiga_qemu_tests`. Set it. |
| `target.qemu_config is required for qemu.start` | Target's `qemu_config` path missing from `[targets.<name>]`. Set it. |
| `NotCapable: channel mcu` | `power.*` invoked but `[targets.<name>.channels.mcu]` not configured. Wire the FTDI cable, set the block. |
| `NotCapable: input injection is not enabled` | `input.*` invoked without both gates open. Host side: `[targets.<name>.input] enabled = true`. Daemon side (the real control): run `MCPd-Enable-Input` on the target and restart MCPd. Do not open either without the user asking. |
| `FileNotFoundError: config not found` | `--config` points at a path that doesn't exist, or no config at the platform default. Run `--init`. |
| `bad config: ...` | TOML doesn't validate against the schema. Re-run `--init`; the wizard pre-validates. |

Expand All @@ -204,6 +205,14 @@ wiring.
procedure (see [INSTALL.md § Installing MCPd on a target](INSTALL.md#installing-mcpd-on-a-target))
— it requires the target to already be reachable and is
inherently more interactive.
- **Doesn't enable keyboard / mouse injection.** `input.*` is off by
default at both layers and this spec leaves it that way. Writing
`[targets.<name>.input] enabled = true` into a generated config is
out of scope — an agent must not open a gate whose whole purpose is
to require a deliberate human action, and the host-side flag alone
does nothing anyway (the daemon gate needs `MCPd-Enable-Input` plus a
restart, on the target). If the user asks for it, point them at
[SECURITY.md](SECURITY.md) first.
- **Doesn't choose secrets or credentials.** MCPd has no
authentication on its TCP listener; the security stance is
network-level isolation (private LAN, host-only QEMU NAT,
Expand Down
230 changes: 230 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,235 @@
# Change log

## 1.3 — Sandboxed iteration, screen capture, and input injection

### Added

- **`input.*` — keyboard and mouse injection**, via `input.device`
`IND_WRITEEVENT` on the Amiga side. Seven methods: `input.state`
(read-only pointer / focus / geometry), `input.type`, `input.key`,
`input.mouse_move`, `input.click`, `input.drag`, `input.scroll`.
Works on real hardware and QEMU alike since it goes through MCPd
rather than QMP. Registered as seven fine-grained MCP tools plus an
`input` namespace dispatcher.
- **Disabled by default at both layers.** The daemon gate is the real
control: `input.*` returns `-32003` unless MCPd was started with the
new `--enable-input` flag *or* the sentinel file
`SYS:System/MCPd/ENABLE-INPUT` exists. The gate is read once at
startup, so no RPC method can switch it on — enabling requires
filesystem access to the target and a daemon restart. The sentinel
is the primary mechanism because `MCPd-Watchdog` relaunches MCPd
with no arguments, so a CLI-flag-only gate would be silently lost on
the first restart. `proto.capabilities` now reports
`input.enabled`, and the methods stay advertised when off so clients
can distinguish "switched off" from "old daemon".
- **Host-side gate**: new `[targets.<name>.input]` config block
(`enabled`, `max_text_len`, `max_events`, `default_delay_ms`,
`allow_drag`), defaulting to absent. Raises `NotCapable` naming both
gates. The `--init` wizard asks about it, defaulting to no.
- **New install scripts** `MCPd-Enable-Input` / `MCPd-Disable-Input`,
copied but never run -- by `MCPd-Install`, by `installer.stage` +
the `install_mcpd` sequence step, and by
`scripts/install_mcpd_autostart.py`, so a machine provisioned any of
the three ways has the documented persistent way to open the gate.
- `confirm: true` required on the committing operations (`type`,
`key`, `click`, `drag`); not on `mouse_move`, `scroll`, `state`.
`ctrl+lamiga+ramiga` additionally requires `confirm_reset: true`
because it reboots the machine.
- Per-call caps enforced daemon-side: 256 events, 20 s wall clock, 512
characters, 64 drag steps, `delay_ms` 0–1000. Held modifiers and
mouse buttons are always released before returning, including on the
abort path, so a truncated drag cannot leave a stuck mouse button.

- **Layout-correct typing.** `input.type` maps characters through the
target's `keymap.library` (`MapANSI`), including dead-key sequences,
so non-US keymaps receive the intended characters. Verified
end-to-end on a German (QWERTZ) AmigaOS 4.1 FE guest: typing
`Echo TYPED-OK >T:typed.txt` into a Shell produced exactly that
file. `keymap="us"` forces the built-in table, which is also the
automatic fallback; the result reports which path ran. Text is
decoded from UTF-8 to codepoints before mapping (both mapping paths
are one-byte ANSI), so an accented character is one keystroke rather
than one per UTF-8 byte; codepoints above U+00FF, which no Amiga
keymap can generate, are reported in `unmapped[]`. The 512 cap
counts characters on both sides.
- **The input gate is visible in the kernel debug ring.** MCPd emits
`[MCPd] input_gate state=... source=...` at startup and adds
`input=on|off` to the `[MCPd] ready` beacon, both readable through
`sys.debug_ring`. The startup banner alone goes to stdout, which is
`NIL:` on the watchdog auto-start path -- invisible exactly where it
matters most.


- **`wb.screenshot`** — capture an AmigaOS screen to a PNG on the
target and bring it back to the host. The daemon grabs the chosen
screen (frontmost, or by `screen_index`) with `graphics.library`
ReadPixelArray and PNG-encodes it using the already-linked
`z.library` (deflate + CRC32); AmigaOS 4.1 ships no PNG *writer*
datatype, so the encoding is done directly rather than via
`datatypes.library`. The host tool downloads the file (`fs.download`)
and optionally inlines it as base64. Unlike `qemu.screenshot` (QMP
`screendump`, QEMU-only), this works on real X5000 / A1222 hardware
too, and fans out via `fleet.run_on_all`.

- **`sandbox.*` namespace** — driver- and program-iteration loop on
top of [SandboxVM](https://github.com/derfsss/SandboxVM), an AOS4
in-process sandbox host that survives guest crashes (DSI / ISI /
alignment / privilege traps). Five tools plus one shared primitive:
- `sandbox.probe` — path resolution + executability +
Pegasos II refusal. Three typed error codes
(`SANDBOXVM_MISSING` / `SANDBOXVM_BROKEN` /
`SANDBOXVM_INCOMPATIBLE_TARGET`). Probe cache (60 s TTL) for
chatty workflows; eager re-probe via the tool itself.
- `sandbox.deploy` — wraps `fs.upload` with SHA-256 verify +
cache invalidation + path-pinned post-deploy probe.
- `sandbox.run_guest` — runs one guest ELF, slurps
`T:sandboxvm-<name>.{out,err}` captures, decodes SandboxVM's
exit-code convention into `trap_kind` (DSI / ISI / alignment /
program / fp_unavailable) and the documented kmod-libcall
NULL+4 fingerprint.
- `sandbox.run_driver` — resident-driver mode (`-r`) with
optional `-t` follow-on test guest. Loads the driver via
`RTF_AUTOINIT` Resident + `CLT_InitFunc` so a test program in
the same Guest can `OpenLibrary` the driver by name.
- `sandbox.run_batch` — up to 16 guests sequentially in one
sandboxvm invocation. Per-guest exit codes recovered from the
kernel debug ring; aggregate exit follows SandboxVM's
`last_nonzero` convention. Multi-target fan-out via
`fleet.run_on_all`.
- `sandbox.last_trap` — filter-on-top of `sys.debug_ring` for
SandboxVM trap signatures. 3-attempt × 200 ms retry handles
the kernel-ring write race after a crashed `run_guest`.
- **`sys.debug_ring`** — new primitive that reads the kernel debug
ring via `c:DumpDebugBuffer`. Lifted out of `sandbox.last_trap`
during the namespace-overlap review so the rest of the project
(post-install forensics, hardware bring-up) can use it too.
- **`[paths] sandboxvm`** — host-side path to a built
`bin/sandboxvm`, used by `sandbox.deploy` source resolution.
- **`[targets.<name>.sandbox]` block** — per-target SandboxVM
overrides: AOS path, default `-m` extmem MB, default `-w`
window MB, always-on `-x` deny-libs.
- **Bundled `AmiDock.amiga.com.xml`** as a package resource. The
installer's `patch_amidock_prefs` step previously skipped
silently when no XML was staged; it now uses the bundled prefs
by default, so a fresh install ships with Filer / Ranger /
IBrowse / DiskImageGUI in the dock subdrawers.
- **`installer.dismount_combi_device`** — new step at the end of
the install pipeline. Ejects the install ISO from
`diskimage.device` unit 50 (defensive, even though `unmount_iso`
ran), deletes the installer-written `DEVS:DOSDrivers/COMBI`
mountfile (only when it carries the installer's marker so a
user-owned COMBI: is left alone), and issues
`c:Dismount COMBI: FORCE` to drop the live DOS entry.

- **MCPd readiness beacon in the kernel debug ring.** MCPd now emits
a single machine-parseable line via `IExec->DebugPrintF` once the
listen socket is bound and accepting:

```
[MCPd] ready name=MCPd version=1.3 build_date=02.08.2026 build_time=18:18:04 port=4322
```

The pre-existing `Printf` banner goes to stdout, which is `NIL:` on
the auto-start path (`S:Network-Startup` -> `Run >NIL: <NIL: Execute
MCPd-Watchdog`), so it never landed anywhere observable. The debug
ring survives regardless of how MCPd was launched, so anything
reading `C:DumpDebugBuffer` (`sys.debug_ring`, a serial capture, a
boot watcher) can detect "MCPd is up" without opening a socket.
Because the line is emitted *after* bind+listen succeed, seeing it
means the port is genuinely accepting rather than merely that the
binary loaded. Two companion lines cover the other outcomes:
`[MCPd] startup_failed version=... reason=bsdsocket|listen ...` and
`[MCPd] shutdown version=...` (clean exit, so a reader can tell a
clean stop from a crash). The `[MCPd] ` prefix and the `key=value`
shape are a parsed interface — keep them stable.

Note that `sys.debug_ring` reaches `C:DumpDebugBuffer` *through*
MCPd, so it cannot detect a daemon that failed to start; for that,
read a serial capture (`serial.*`, or QEMU's `-serial stdio` log
with `debuglevel=1`), which does not depend on the daemon.

Validated on QEMU AmigaOne across two cold boots: the beacon
appears in both the serial log and `sys.debug_ring`, and the
shutdown line is emitted on a clean `Break`.

- **Build time is now stamped into the binary.** New `MCPD_TIME`
macro (`BUILD_TIME := $(shell date +%H:%M:%S)` in `mcpd/Makefile`),
so two builds made on the same day are distinguishable. Surfaced in
the readiness beacon, `MCPd --version`, `proto.version.build_time`
and `proto.capabilities.build.time`. Deliberately *not* added to
the `$VER` cookie: AmigaDOS `Version` parses `(DD.MM.YYYY)` and a
time component would break it.

### Changed

- **MCPd's listener process now runs at priority 1** (was: whatever
it inherited from the launching Shell, i.e. 0). The accept+spawn
loop burns almost no CPU, so running it just above Workbench keeps
the daemon responsive to new connections on a loaded machine. The
per-connection worker processes are unchanged at -1, so the actual
heavy RPC work (chunked uploads, recursive copies, `exec.cmd`
subprocesses) still yields to the user. Confirmed on QEMU
AmigaOne: `Status FULL` reports
`priority 1 ... SYS:System/MCPd/MCPd` after a cold boot, with
`exec.cmd` subprocesses still at -1.

- **MCPd version bumped to 1.3.** `mcpd/src/main.c` `MCPD_VERSION`,
`mcpd/src/rpc.h` `MCPD_SERVER_VERSION` (`mcpd/1.3`), and
`mcpd/Makefile` `VERSION` — the last of which had drifted and was
still reading `1.1` while the shipped v1.2 binary reported `1.2`.

- **`installer.*` no longer requires a host-side
`diskimage-bootstrap/` directory.** The AOS 4.1 diskimage tools
(`MountDiskImage` / `diskimage.device` / `CDFileSystem`) ship
with AOS 4.1 itself; `stage_diskimage_tools` is now a probe
that fails loudly if the running system is missing any of them
(which would mean the install host isn't a working AOS 4.1
install). The dest drive picks up fresh copies via the normal
`copy_base_os` pull from `<ISO>:System/`. Drops `bootstrap_dir`
from `installer.preflight` / `installer.stage` /
`[defaults] bootstrap_dir`; the `--init` wizard no longer
prompts for it.
- **`sandbox.probe` doesn't gate on banner content.** Real-X5000
testing showed sandboxvm's printf-based usage banner is
occasionally captured as empty by MCPd's exec.cmd (clib4 /
newlib stdio buffering not flushed before the process detaches
from its inherited Output() handle). The probe now treats any
structured `exec.cmd` exit as proof the binary started.

### Fixed

- **Dependency majors are now capped** (`mcp[cli]>=1.0.0,<2`,
`pydantic>=2.0,<3`). `uv.lock` is not committed, so CI resolves
dependencies fresh on every run; the unbounded `mcp` requirement
picked up mcp 2.2.0, which removed the 1.x
`from mcp.server.fastmcp import FastMCP` entry point this server is
written against, and the type-check failed for reasons unrelated to
anything in the repo. Lift the cap in a change that ports to the 2.x
API and is tested against it.

### Known limitations (`input.*`)

- F11/F12 are deliberately absent from the rawkey table: their AOS4
codes were never verified on hardware, and an unknown key name fails
cleanly where a wrong code would silently press something else. The
NewMouse wheel constants remain unverified (behind `#ifndef` guards
so a missing header cannot break the build).
- `input.mouse_move` defaults to a read-position-then-relative-delta
strategy and reports the position actually achieved, but does not
retry to converge. `input.click`'s pre-move and `input.drag`'s
move-to-start do not read back at all, so pointer acceleration on
real hardware could land them off-target; only QEMU pegasos2 has
been measured. `absolute_mode="raw"` remains available to A/B the
true-absolute event form.
- `input.click` does not report the achieved pointer position the way
`input.mouse_move` and `input.drag` do.

### Tool count

- 121 → 137 tools (+6 `sandbox.*`, +`sys.debug_ring`,
+`wb.screenshot`, +8 `input.*` including its dispatcher).
13 → 14 namespaces.

## 1.2 — Guided setup and whole-file transfers

### Added
Expand Down
Loading
Loading