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. # ---------------------------------------------------------------------------