AgentBoot
April 24, 2026 · View on GitHub
scripts/agentboot_collector.py is the "target-side" half of
AgentBoot. It runs on whatever minimal OS is currently executing on
the bare-metal machine (a rescue ISO, an installer live environment,
Alpine, etc.) and:
- Reports hardware facts back to the operator side.
- Accepts a handful of commands over serial / stdin-stdout.
- Writes installer config files to the local filesystem.
Design constraints
- Stdlib only. Alpine's minimal install has Python 3 but not pip.
The collector therefore depends on nothing beyond
json,subprocess,pathlib, andos. - Single file. No imports from
agentboot— the collector is shipped as one script that operators cancurl/scp/copy by hand. - No telemetry, no phoning home. Every command is explicit. The only IO is stdin/stdout (or the serial device you point it at).
Running
# One-shot hardware report to stdout
python3 agentboot_collector.py --report-only
# Serve command loop over stdin/stdout (useful over SSH)
python3 agentboot_collector.py
# Serve over a serial device (requires pyserial on the target)
python3 agentboot_collector.py --serial /dev/ttyGS0 --baud 115200
Commands
Every command is one JSON object per line. The envelope:
{"v": 1, "id": "unique-id", "kind": "cmd", "name": "hw.report"}
Responses mirror id:
{"v": 1, "id": "unique-id", "kind": "response", "ok": true, "data": {...}}
hw.report
Emits a structured hardware report:
{
"hostname": "...", "arch": "x86_64",
"os_running": "Alpine 3.20",
"kernel": "Linux 6.6.x",
"is_virtual": false,
"cpu": {"brand": "...", "arch": "x86_64", "logical_cores": 8, ...},
"ram": {"total_bytes": ...},
"storage": [{"path": "/dev/sda", "size_bytes": ..., "model": "..."}, ...],
"nics": [...],
"gpus": [...]
}
ping
Returns {"pong": true} — a liveness check.
config.write
Writes a file to disk. Directories are created as needed.
{"v":1,"id":"w1","kind":"cmd","name":"config.write",
"data":{"path":"/target/cloud-init/user-data","contents":"..."}}
system.reboot, system.poweroff
Executes the corresponding shutdown command after acknowledging.
If you're running inside an unprivileged chroot these will fail,
which is fine — the error is reported back as a regular error frame.
Error frames
{"v": 1, "id": "...", "kind": "error", "code": "BAD_ARGS",
"message": "config.write requires 'path' and 'contents'"}
Error codes: BAD_ARGS, UNKNOWN_COMMAND, INTERNAL, IO_ERROR.
Extending
New handlers plug into the _HANDLERS dict at the top of the script:
def _handle_my_cmd(cmd: dict) -> dict:
# cmd has keys v, id, kind, name, data
return {"v": 1, "id": cmd["id"], "kind": "response",
"ok": True, "data": {...}}
_HANDLERS["my.cmd"] = _handle_my_cmd
Keep them dependency-free — the target may not have psutil, requests
or anything else beyond Python's stdlib. If you need lspci /
dmidecode, call them via subprocess.run and handle missing binaries
gracefully.