Ship trust-first CLI and runtime defaults
This commit is contained in:
parent
fb718af154
commit
5d63e4c16e
26 changed files with 894 additions and 134 deletions
|
|
@ -19,13 +19,19 @@ from pyro_mcp.runtime import (
|
|||
resolve_runtime_paths,
|
||||
runtime_capabilities,
|
||||
)
|
||||
from pyro_mcp.vm_environments import EnvironmentStore, get_environment
|
||||
from pyro_mcp.vm_environments import EnvironmentStore, default_cache_dir, get_environment
|
||||
from pyro_mcp.vm_firecracker import build_launch_plan
|
||||
from pyro_mcp.vm_guest import VsockExecClient
|
||||
from pyro_mcp.vm_network import NetworkConfig, TapNetworkManager
|
||||
|
||||
VmState = Literal["created", "started", "stopped"]
|
||||
|
||||
DEFAULT_VCPU_COUNT = 1
|
||||
DEFAULT_MEM_MIB = 1024
|
||||
DEFAULT_TIMEOUT_SECONDS = 30
|
||||
DEFAULT_TTL_SECONDS = 600
|
||||
DEFAULT_ALLOW_HOST_COMPAT = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class VmInstance:
|
||||
|
|
@ -41,6 +47,7 @@ class VmInstance:
|
|||
workdir: Path
|
||||
state: VmState = "created"
|
||||
network_requested: bool = False
|
||||
allow_host_compat: bool = DEFAULT_ALLOW_HOST_COMPAT
|
||||
firecracker_pid: int | None = None
|
||||
last_error: str | None = None
|
||||
metadata: dict[str, str] = field(default_factory=dict)
|
||||
|
|
@ -262,7 +269,7 @@ class FirecrackerBackend(VmBackend): # pragma: no cover
|
|||
)
|
||||
instance.firecracker_pid = process.pid
|
||||
instance.metadata["execution_mode"] = (
|
||||
"guest_vsock" if self._runtime_capabilities.supports_guest_exec else "host_compat"
|
||||
"guest_vsock" if self._runtime_capabilities.supports_guest_exec else "guest_boot_only"
|
||||
)
|
||||
instance.metadata["boot_mode"] = "native"
|
||||
|
||||
|
|
@ -342,6 +349,11 @@ class VmManager:
|
|||
MAX_MEM_MIB = 32768
|
||||
MIN_TTL_SECONDS = 60
|
||||
MAX_TTL_SECONDS = 3600
|
||||
DEFAULT_VCPU_COUNT = DEFAULT_VCPU_COUNT
|
||||
DEFAULT_MEM_MIB = DEFAULT_MEM_MIB
|
||||
DEFAULT_TIMEOUT_SECONDS = DEFAULT_TIMEOUT_SECONDS
|
||||
DEFAULT_TTL_SECONDS = DEFAULT_TTL_SECONDS
|
||||
DEFAULT_ALLOW_HOST_COMPAT = DEFAULT_ALLOW_HOST_COMPAT
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -355,7 +367,7 @@ class VmManager:
|
|||
) -> None:
|
||||
self._backend_name = backend_name or "firecracker"
|
||||
self._base_dir = base_dir or Path("/tmp/pyro-mcp")
|
||||
resolved_cache_dir = cache_dir or self._base_dir / ".environment-cache"
|
||||
resolved_cache_dir = cache_dir or default_cache_dir()
|
||||
self._runtime_paths = runtime_paths
|
||||
if self._backend_name == "firecracker":
|
||||
self._runtime_paths = self._runtime_paths or resolve_runtime_paths()
|
||||
|
|
@ -420,10 +432,11 @@ class VmManager:
|
|||
self,
|
||||
*,
|
||||
environment: str,
|
||||
vcpu_count: int,
|
||||
mem_mib: int,
|
||||
ttl_seconds: int,
|
||||
vcpu_count: int = DEFAULT_VCPU_COUNT,
|
||||
mem_mib: int = DEFAULT_MEM_MIB,
|
||||
ttl_seconds: int = DEFAULT_TTL_SECONDS,
|
||||
network: bool = False,
|
||||
allow_host_compat: bool = DEFAULT_ALLOW_HOST_COMPAT,
|
||||
) -> dict[str, Any]:
|
||||
self._validate_limits(vcpu_count=vcpu_count, mem_mib=mem_mib, ttl_seconds=ttl_seconds)
|
||||
get_environment(environment, runtime_paths=self._runtime_paths)
|
||||
|
|
@ -446,7 +459,9 @@ class VmManager:
|
|||
expires_at=now + ttl_seconds,
|
||||
workdir=self._base_dir / vm_id,
|
||||
network_requested=network,
|
||||
allow_host_compat=allow_host_compat,
|
||||
)
|
||||
instance.metadata["allow_host_compat"] = str(allow_host_compat).lower()
|
||||
self._backend.create(instance)
|
||||
self._instances[vm_id] = instance
|
||||
return self._serialize(instance)
|
||||
|
|
@ -456,11 +471,12 @@ class VmManager:
|
|||
*,
|
||||
environment: str,
|
||||
command: str,
|
||||
vcpu_count: int,
|
||||
mem_mib: int,
|
||||
timeout_seconds: int = 30,
|
||||
ttl_seconds: int = 600,
|
||||
vcpu_count: int = DEFAULT_VCPU_COUNT,
|
||||
mem_mib: int = DEFAULT_MEM_MIB,
|
||||
timeout_seconds: int = DEFAULT_TIMEOUT_SECONDS,
|
||||
ttl_seconds: int = DEFAULT_TTL_SECONDS,
|
||||
network: bool = False,
|
||||
allow_host_compat: bool = DEFAULT_ALLOW_HOST_COMPAT,
|
||||
) -> dict[str, Any]:
|
||||
created = self.create_vm(
|
||||
environment=environment,
|
||||
|
|
@ -468,6 +484,7 @@ class VmManager:
|
|||
mem_mib=mem_mib,
|
||||
ttl_seconds=ttl_seconds,
|
||||
network=network,
|
||||
allow_host_compat=allow_host_compat,
|
||||
)
|
||||
vm_id = str(created["vm_id"])
|
||||
try:
|
||||
|
|
@ -486,6 +503,12 @@ class VmManager:
|
|||
self._ensure_not_expired_locked(instance, time.time())
|
||||
if instance.state not in {"created", "stopped"}:
|
||||
raise RuntimeError(f"vm {vm_id} cannot be started from state {instance.state!r}")
|
||||
self._require_guest_boot_or_opt_in(instance)
|
||||
if not self._runtime_capabilities.supports_vm_boot:
|
||||
instance.metadata["execution_mode"] = "host_compat"
|
||||
instance.metadata["boot_mode"] = "compat"
|
||||
if self._runtime_capabilities.reason is not None:
|
||||
instance.metadata["runtime_reason"] = self._runtime_capabilities.reason
|
||||
self._backend.start(instance)
|
||||
instance.state = "started"
|
||||
return self._serialize(instance)
|
||||
|
|
@ -498,8 +521,11 @@ class VmManager:
|
|||
self._ensure_not_expired_locked(instance, time.time())
|
||||
if instance.state != "started":
|
||||
raise RuntimeError(f"vm {vm_id} must be in 'started' state before vm_exec")
|
||||
self._require_guest_exec_or_opt_in(instance)
|
||||
if not self._runtime_capabilities.supports_guest_exec:
|
||||
instance.metadata["execution_mode"] = "host_compat"
|
||||
exec_result = self._backend.exec(instance, command, timeout_seconds)
|
||||
execution_mode = instance.metadata.get("execution_mode", "host_compat")
|
||||
execution_mode = instance.metadata.get("execution_mode", "unknown")
|
||||
cleanup = self.delete_vm(vm_id, reason="post_exec_cleanup")
|
||||
return {
|
||||
"vm_id": vm_id,
|
||||
|
|
@ -587,12 +613,35 @@ class VmManager:
|
|||
"expires_at": instance.expires_at,
|
||||
"state": instance.state,
|
||||
"network_enabled": instance.network is not None,
|
||||
"allow_host_compat": instance.allow_host_compat,
|
||||
"guest_ip": instance.network.guest_ip if instance.network is not None else None,
|
||||
"tap_name": instance.network.tap_name if instance.network is not None else None,
|
||||
"execution_mode": instance.metadata.get("execution_mode", "host_compat"),
|
||||
"execution_mode": instance.metadata.get("execution_mode", "pending"),
|
||||
"metadata": instance.metadata,
|
||||
}
|
||||
|
||||
def _require_guest_boot_or_opt_in(self, instance: VmInstance) -> None:
|
||||
if self._runtime_capabilities.supports_vm_boot or instance.allow_host_compat:
|
||||
return
|
||||
reason = self._runtime_capabilities.reason or "runtime does not support real VM boot"
|
||||
raise RuntimeError(
|
||||
"guest boot is unavailable and host compatibility mode is disabled: "
|
||||
f"{reason}. Set allow_host_compat=True (CLI: --allow-host-compat) to opt into "
|
||||
"host execution."
|
||||
)
|
||||
|
||||
def _require_guest_exec_or_opt_in(self, instance: VmInstance) -> None:
|
||||
if self._runtime_capabilities.supports_guest_exec or instance.allow_host_compat:
|
||||
return
|
||||
reason = self._runtime_capabilities.reason or (
|
||||
"runtime does not support guest command execution"
|
||||
)
|
||||
raise RuntimeError(
|
||||
"guest command execution is unavailable and host compatibility mode is disabled: "
|
||||
f"{reason}. Set allow_host_compat=True (CLI: --allow-host-compat) to opt into "
|
||||
"host execution."
|
||||
)
|
||||
|
||||
def _get_instance_locked(self, vm_id: str) -> VmInstance:
|
||||
try:
|
||||
return self._instances[vm_id]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue