Add unikernel runtime: run services as Nanos VMs under QEMU/KVM

Implements the hypervisor design's Phase 1: a second runtime.Runtime
backend (QEMU) that runs each service component as a Nanos unikernel VM
instead of a podman container, selected per-component via a new
runtime = "unikernel" service-def field.

- internal/runtime/qemu.go: QEMURuntime. Pull extracts the ELF from the
  OCI image; Run does `ops build` + boots qemu-system-x86_64 with KVM,
  user-mode net port-forwards, QMP control socket and serial console log;
  Stop/Remove/Inspect/List/Logs map onto VM lifecycle + state dir.
- proto/registry/servicedef: add runtime, memory_mb, vcpus fields
  (registry migration 5).
- agent: holds both runtimes; runtimeFor() selects per component;
  listAllContainers() merges containers + VMs so drift/status see both.
  Unikernel runtime auto-enables on nodes with /dev/kvm + ops.

Validated end-to-end on straylight: a test service deploys via
`mcp deploy --direct`, boots as a Nanos unikernel, serves HTTP through
the agent port-forward, and reports running via `mcp status`/`mcp logs`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Kyle Isom
2026-06-11 00:54:49 -07:00
parent 3b08caaa0a
commit d56f224359
30 changed files with 949 additions and 152 deletions

View File

@@ -110,6 +110,9 @@ type ComponentSpec struct {
Cmd []string `protobuf:"bytes,8,rep,name=cmd,proto3" json:"cmd,omitempty"`
Routes []*RouteSpec `protobuf:"bytes,9,rep,name=routes,proto3" json:"routes,omitempty"`
Env []string `protobuf:"bytes,10,rep,name=env,proto3" json:"env,omitempty"`
Runtime string `protobuf:"bytes,11,opt,name=runtime,proto3" json:"runtime,omitempty"` // "container" (default) or "unikernel"
MemoryMb int32 `protobuf:"varint,12,opt,name=memory_mb,json=memoryMb,proto3" json:"memory_mb,omitempty"` // unikernel guest memory in MB (default 256)
Vcpus int32 `protobuf:"varint,13,opt,name=vcpus,proto3" json:"vcpus,omitempty"` // unikernel guest vCPUs (default 1)
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -214,6 +217,27 @@ func (x *ComponentSpec) GetEnv() []string {
return nil
}
func (x *ComponentSpec) GetRuntime() string {
if x != nil {
return x.Runtime
}
return ""
}
func (x *ComponentSpec) GetMemoryMb() int32 {
if x != nil {
return x.MemoryMb
}
return 0
}
func (x *ComponentSpec) GetVcpus() int32 {
if x != nil {
return x.Vcpus
}
return 0
}
type SnapshotConfig struct {
state protoimpl.MessageState `protogen:"open.v1"`
Method string `protobuf:"bytes,1,opt,name=method,proto3" json:"method,omitempty"` // "grpc", "cli", "exec: <cmd>", "full", or "" (default)
@@ -3586,7 +3610,7 @@ const file_proto_mcp_v1_mcp_proto_rawDesc = "" +
"\x04port\x18\x02 \x01(\x05R\x04port\x12\x12\n" +
"\x04mode\x18\x03 \x01(\tR\x04mode\x12\x1a\n" +
"\bhostname\x18\x04 \x01(\tR\bhostname\x12\x16\n" +
"\x06public\x18\x05 \x01(\bR\x06public\"\x80\x02\n" +
"\x06public\x18\x05 \x01(\bR\x06public\"\xcd\x02\n" +
"\rComponentSpec\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" +
"\x05image\x18\x02 \x01(\tR\x05image\x12\x18\n" +
@@ -3598,7 +3622,10 @@ const file_proto_mcp_v1_mcp_proto_rawDesc = "" +
"\x03cmd\x18\b \x03(\tR\x03cmd\x12)\n" +
"\x06routes\x18\t \x03(\v2\x11.mcp.v1.RouteSpecR\x06routes\x12\x10\n" +
"\x03env\x18\n" +
" \x03(\tR\x03env\"D\n" +
" \x03(\tR\x03env\x12\x18\n" +
"\aruntime\x18\v \x01(\tR\aruntime\x12\x1b\n" +
"\tmemory_mb\x18\f \x01(\x05R\bmemoryMb\x12\x14\n" +
"\x05vcpus\x18\r \x01(\x05R\x05vcpus\"D\n" +
"\x0eSnapshotConfig\x12\x16\n" +
"\x06method\x18\x01 \x01(\tR\x06method\x12\x1a\n" +
"\bexcludes\x18\x02 \x03(\tR\bexcludes\"\xe6\x01\n" +