From bd5710b237f7c44626cdbd134ae91cd32876f8eb Mon Sep 17 00:00:00 2001 From: Shekhar Mudarapu Date: Fri, 31 Jul 2026 00:24:31 -0400 Subject: [PATCH] feat(microvm): make the in-guest tool path usable and assert it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Correction to earlier notes: the in-guest tool runner was never missing. The whole chain already existed — the manager sends a `__lantern_tool_call__` envelope, the guest exec server routes it to `tool_runner::dispatch`, and the typed result comes back. `exec_tool` returned UNAVAILABLE because it needs a heartbeat-derived guest address to dial back on, and the harness could not open a heartbeat stream until the networking and mTLS fixes landed. Verified end to end now: ExecTool shell_exec -> TOOL_STATUS_OK result.stdout = "hello-from-inside-the-microvm\\naarch64\\n0\\n" Two things were still missing to make that path exercisable. The test image had no shell. `shell_exec` spawns `/bin/sh -c`, so every tool call in this image failed with "spawn failed: No such file or directory" — the in-guest tool path could not be exercised at all. build-image.sh now installs a STATIC busybox as /bin/sh (the rootfs has no libc, so it must be static). It prefers FC_BUSYBOX_BIN, falls back to a host /bin/busybox that `file` confirms is statically linked, and when neither is present builds exactly as before and says so — only shell_exec is unavailable. And nothing asserted it. The integration test now invokes a tool through ExecTool and requires the tool's own output back. That covers the workflow-engine's tool_call path end to end, and because ExecTool can only reach the guest via the heartbeat-derived address, it also proves the harness is REACHABLE rather than merely started. The assertion degrades to a skip (not a pass) when the image has no shell, so it can never report success for a path it did not run. Verified on real KVM — 5/5 assertions: Firecracker available · guest live · secret vended over mTLS · tool invoked in-guest · all assertions passed --- infra/firecracker/build-image.sh | 26 ++++++++++++++++++++++++++ infra/firecracker/integration-test.sh | 26 ++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/infra/firecracker/build-image.sh b/infra/firecracker/build-image.sh index 1ab65858..a6e77028 100755 --- a/infra/firecracker/build-image.sh +++ b/infra/firecracker/build-image.sh @@ -199,6 +199,32 @@ build_rootfs() { sudo cp "${HARNESS_BIN}" "${mnt}/sbin/init" sudo chmod 0755 "${mnt}/usr/local/bin/lantern-harness" "${mnt}/sbin/init" + # A static shell, so the guest can actually run a tool. + # + # The harness's `shell_exec` tool spawns `/bin/sh -c `; without a + # shell in the image every tool call fails with "spawn failed: No such file + # or directory", so the in-guest tool path cannot be exercised at all. Must + # be STATIC — this rootfs has no libc. + # + # Optional: when no static busybox is present the image is built without a + # shell exactly as before, and only shell_exec is unavailable. + if [ -n "${FC_BUSYBOX_BIN:-}" ] && [ -x "${FC_BUSYBOX_BIN}" ]; then + BUSYBOX_SRC="${FC_BUSYBOX_BIN}" + elif [ -x /bin/busybox ] && file /bin/busybox 2>/dev/null | grep -q "statically linked"; then + BUSYBOX_SRC=/bin/busybox + else + BUSYBOX_SRC="" + fi + if [ -n "${BUSYBOX_SRC}" ]; then + sudo mkdir -p "${mnt}/bin" + sudo cp "${BUSYBOX_SRC}" "${mnt}/bin/busybox" + sudo chmod 0755 "${mnt}/bin/busybox" + sudo ln -sf busybox "${mnt}/bin/sh" + log "Installed static shell: ${BUSYBOX_SRC} -> /bin/sh" + else + log "No static busybox found — image has NO /bin/sh; shell_exec tool calls will fail" + fi + # Minimal defaults file (ADR-0004). The manager overrides these via the # kernel cmdline (build_boot_args) and the per-VM env contract. sudo tee "${mnt}/etc/lantern/harness.toml" >/dev/null <<'TOML' diff --git a/infra/firecracker/integration-test.sh b/infra/firecracker/integration-test.sh index 90f3ed00..ad70ab63 100755 --- a/infra/firecracker/integration-test.sh +++ b/infra/firecracker/integration-test.sh @@ -301,6 +301,32 @@ else skip "VendSecret round-trip PENDING in-guest harness agent (services/harness manager_client.rs is a stub); set EXPECT_VENDSECRET=1 to assert it. Secret-value-never-leaked invariant: enforced." fi +# --------------------------------------------------------------------------- +# 4b. Invoke a tool in the guest via ExecTool. +# +# This is the workflow-engine's tool_call path end to end: manager -> in-guest +# harness exec server -> tool registry -> real execution -> typed result. It can +# only succeed after the harness has opened its Heartbeat stream (that is what +# gives the manager the guest address to dial back on), so it also proves the +# harness is reachable, not just that it started. +# +# Needs a shell in the image; build-image.sh installs a static busybox when one +# is available, and skips this assertion when it did not. +# --------------------------------------------------------------------------- +log "Invoking a tool in the guest (ExecTool -> shell_exec)" +TOOL_RESP=$(grpcurl -cacert "${WORK}/ca.crt" -cert "${WORK}/client.crt" -key "${WORK}/client.key" -servername localhost \ + -import-path "${PROTO_DIR}" -proto "${PROTO_FILE}" \ + -d "{\"vm_id\":\"${VM_ID}\",\"run_id\":\"itest-run-001\",\"step_id\":\"itest-step-1\",\"tool_name\":\"shell_exec\",\"args\":{\"command\":\"echo lantern-tool-marker\"},\"idempotency_key\":\"itest-run-001:itest-step-1:1\"}" \ + "${MANAGER_ADDR}" lantern.v1.RuntimeManager/ExecTool 2>>"${MANAGER_LOG}") || TOOL_RESP="" + +if printf '%s' "${TOOL_RESP}" | grep -q "lantern-tool-marker"; then + pass "tool invoked in-guest and returned its output (ExecTool -> shell_exec)" +elif printf '%s' "${TOOL_RESP}" | grep -q "No such file or directory"; then + skip "in-guest tool ran but the image has no /bin/sh — install a static busybox to assert execution" +else + fail "ExecTool did not return the tool's output. Response:\n${TOOL_RESP}\nLog:\n$(tail -30 "${MANAGER_LOG}")" +fi + # --------------------------------------------------------------------------- # 5. Tear down. # ---------------------------------------------------------------------------