diff --git a/Dockerfile b/Dockerfile index 1b53a30..bc28135 100644 --- a/Dockerfile +++ b/Dockerfile @@ -132,6 +132,7 @@ COPY sshd_config /etc/ssh/sshd_config COPY microagent-init.sh /usr/local/bin/microagent-init COPY microagent-desktop-session.sh /usr/local/bin/microagent-desktop-session COPY microagent-network-up.sh /usr/local/bin/microagent-network-up +COPY microagent-ready-agent.py /usr/local/bin/microagent-ready-agent COPY defaults/.zshrc /home/node/.zshrc COPY defaults/.bashrc /home/node/.bashrc COPY defaults/.profile /home/node/.profile @@ -140,7 +141,7 @@ COPY defaults/AGENTS.md /home/node/AGENTS.md COPY terminfo/xterm-ghostty.terminfo /tmp/xterm-ghostty.terminfo COPY terminfo/xterm-kitty.terminfo /tmp/xterm-kitty.terminfo -RUN chmod 755 /usr/local/bin/microagent-init /usr/local/bin/microagent-desktop-session /usr/local/bin/microagent-network-up \ +RUN chmod 755 /usr/local/bin/microagent-init /usr/local/bin/microagent-desktop-session /usr/local/bin/microagent-network-up /usr/local/bin/microagent-ready-agent \ && chmod 755 /opt/desktop/scripts/apply-desktop-profile.sh \ && chown node:node /home/node/.zshrc /home/node/.bashrc /home/node/.profile /home/node/AGENTS.md \ && ln -sf /home/node/AGENTS.md /home/node/CLAUDE.md \ diff --git a/microagent-init.sh b/microagent-init.sh index 973cdc2..b08de5d 100644 --- a/microagent-init.sh +++ b/microagent-init.sh @@ -7,8 +7,6 @@ log() { printf '[microagent-init] %s\n' "$*" >&2 } -MMDS_IPV4_ADDRESS="169.254.170.2" - read_machine_name() { if [ -r /etc/microagent/machine-name ]; then tr -d '\r\n' /dev/null 2>&1 || return 1 - - while [ "$attempt" -lt 50 ]; do - token="$(curl -fsS -X PUT "http://${MMDS_IPV4_ADDRESS}/latest/api/token" \ - -H 'X-metadata-token-ttl-seconds: 30' 2>/dev/null || true)" - if [ -n "$token" ]; then - payload="$(curl -fsS "http://${MMDS_IPV4_ADDRESS}/latest/meta-data" \ - -H 'Accept: application/json' \ - -H "X-metadata-token: ${token}" 2>/dev/null || true)" - if [ -n "$payload" ]; then - printf '%s' "$payload" - return 0 - fi - fi - attempt=$((attempt + 1)) - sleep 0.1 - done - return 1 -} - -apply_guest_metadata() { - local payload="${1:-}" - local machine_name - - [ -n "$payload" ] || return 1 - install -d -m 0755 /etc/microagent - - machine_name="$(printf '%s' "$payload" | jq -r '.hostname // .machine_id // empty')" - if [ -n "$machine_name" ]; then - printf '%s\n' "$machine_name" >/etc/microagent/machine-name - printf '%s\n' "$machine_name" >/etc/hostname - cat >/etc/hosts </dev/null 2>&1 || true - fi - - if printf '%s' "$payload" | jq -e '.authorized_keys | length > 0' >/dev/null 2>&1; then - log "installing MMDS authorized_keys for node" - install -d -m 0700 -o node -g node /home/node/.ssh - printf '%s' "$payload" | jq -r '.authorized_keys[]' >/home/node/.ssh/authorized_keys - chmod 0600 /home/node/.ssh/authorized_keys - chown node:node /home/node/.ssh/authorized_keys - printf '%s' "$payload" | jq -r '.authorized_keys[]' >/etc/microagent/authorized_keys - chmod 0600 /etc/microagent/authorized_keys - fi - - if printf '%s' "$payload" | jq -e '.trusted_user_ca_keys | length > 0' >/dev/null 2>&1; then - log "installing MMDS trusted user CA keys" - printf '%s' "$payload" | jq -r '.trusted_user_ca_keys[]' >/etc/microagent/trusted_user_ca_keys - chmod 0644 /etc/microagent/trusted_user_ca_keys - fi - - printf '%s' "$payload" | jq '{authorized_keys, trusted_user_ca_keys, login_webhook}' >/etc/microagent/guest-config.json - chmod 0600 /etc/microagent/guest-config.json - return 0 -} - mountpoint -q /proc || mount -t proc proc /proc mountpoint -q /sys || mount -t sysfs sysfs /sys mountpoint -q /dev || mount -t devtmpfs devtmpfs /dev @@ -105,6 +35,7 @@ resize2fs /dev/vda >/dev/null 2>&1 || true cleanup() { trap - INT TERM [ -n "${rng_pid:-}" ] && kill "$rng_pid" >/dev/null 2>&1 || true + [ -n "${ready_agent_pid:-}" ] && kill "$ready_agent_pid" >/dev/null 2>&1 || true [ -n "${sshd_pid:-}" ] && kill "$sshd_pid" >/dev/null 2>&1 || true [ -n "${desktop_pid:-}" ] && kill "$desktop_pid" >/dev/null 2>&1 || true wait >/dev/null 2>&1 || true @@ -130,6 +61,13 @@ start_sshd() { sshd_pid=$! } +start_ready_agent() { + reap_if_needed "${ready_agent_pid:-}" + log "starting ready agent on vsock 1024" + /usr/local/bin/microagent-ready-agent >>/var/log/ready-agent.log 2>&1 & + ready_agent_pid=$! +} + start_desktop() { reap_if_needed "${desktop_pid:-}" log "starting noVNC desktop on 6080" @@ -145,12 +83,6 @@ if ! /usr/local/bin/microagent-network-up >/var/log/network.log 2>&1; then exit 1 fi -primary_iface="$(find /sys/class/net -mindepth 1 -maxdepth 1 -printf '%f\n' | grep -v '^lo$' | head -n1 || true)" -if metadata_payload="$(fetch_mmds_metadata "$primary_iface")"; then - log "applying guest metadata from MMDS" - apply_guest_metadata "$metadata_payload" || true -fi - machine_name="$(read_machine_name)" export COMPUTER_NAME="$machine_name" printf '%s\n' "$machine_name" >/etc/hostname @@ -163,11 +95,6 @@ ff02::2 ip6-allrouters EOF hostname "$machine_name" >/dev/null 2>&1 || true -if [ ! -f /etc/ssh/ssh_host_ed25519_key ]; then - log "generating ssh host keys" - ssh-keygen -A -fi - if [ -f /etc/microagent/authorized_keys ]; then log "installing injected authorized_keys for node" install -d -m 0700 -o node -g node /home/node/.ssh @@ -195,10 +122,15 @@ if command -v jitterentropy-rngd >/dev/null 2>&1; then rng_pid=$! fi +start_ready_agent start_sshd start_desktop while true; do + if ! pid_running "${ready_agent_pid:-}"; then + log "ready agent exited; restarting" + start_ready_agent + fi if ! pid_running "${sshd_pid:-}"; then log "sshd exited; restarting" start_sshd diff --git a/microagent-ready-agent.py b/microagent-ready-agent.py new file mode 100644 index 0000000..b695ef5 --- /dev/null +++ b/microagent-ready-agent.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +import json +import socket +import sys +import time +from pathlib import Path + + +LISTEN_PORT = 1024 +SSH_PORT = 2222 +WAIT_TIMEOUT_SECONDS = 15.0 +POLL_INTERVAL_SECONDS = 0.1 +HOST_KEY_PATH = Path("/etc/ssh/ssh_host_ed25519_key.pub") + + +def log(message: str) -> None: + print(f"[microagent-ready-agent] {message}", file=sys.stderr, flush=True) + + +def local_port_ready(port: int) -> bool: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe: + probe.settimeout(POLL_INTERVAL_SECONDS) + try: + probe.connect(("127.0.0.1", port)) + except OSError: + return False + return True + + +def wait_for_ready() -> str: + deadline = time.monotonic() + WAIT_TIMEOUT_SECONDS + while time.monotonic() < deadline: + if HOST_KEY_PATH.is_file() and local_port_ready(SSH_PORT): + return HOST_KEY_PATH.read_text(encoding="utf-8").strip() + time.sleep(POLL_INTERVAL_SECONDS) + raise TimeoutError("guest services did not become ready before timeout") + + +def handle_connection(connection: socket.socket) -> None: + with connection: + payload_line = b"" + while not payload_line.endswith(b"\n"): + chunk = connection.recv(4096) + if not chunk: + break + payload_line += chunk + if not payload_line: + return + + try: + payload = json.loads(payload_line.decode("utf-8")) + guest_ssh_public_key = wait_for_ready() + response = { + "status": "ok", + "ready_nonce": str(payload.get("ready_nonce") or "").strip(), + "guest_ssh_public_key": guest_ssh_public_key, + } + except Exception as exc: + response = {"status": "error", "error": str(exc)} + connection.sendall((json.dumps(response) + "\n").encode("utf-8")) + + +def main() -> int: + cid_any = getattr(socket, "VMADDR_CID_ANY", 0xFFFFFFFF) + with socket.socket(socket.AF_VSOCK, socket.SOCK_STREAM) as listener: + listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + listener.bind((cid_any, LISTEN_PORT)) + listener.listen() + log(f"listening on vsock port {LISTEN_PORT}") + while True: + connection, _ = listener.accept() + handle_connection(connection) + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except KeyboardInterrupt: + raise SystemExit(0) diff --git a/sshd_config b/sshd_config index caebe74..96dec00 100644 --- a/sshd_config +++ b/sshd_config @@ -2,7 +2,6 @@ Port 2222 ListenAddress 0.0.0.0 HostKey /etc/ssh/ssh_host_ed25519_key -HostKey /etc/ssh/ssh_host_rsa_key PermitRootLogin no PubkeyAuthentication yes