Add adoption-focused examples, contract docs, and CLI polish
This commit is contained in:
parent
227983a877
commit
0aa5e25dc1
18 changed files with 560 additions and 2 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -7,4 +7,5 @@ __pycache__/
|
||||||
htmlcov/
|
htmlcov/
|
||||||
dist/
|
dist/
|
||||||
build/
|
build/
|
||||||
|
.uv-cache/
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ This repository ships `pyro-mcp`, an MCP-compatible package for ephemeral VM lif
|
||||||
- Install Git LFS before cloning or materializing the packaged runtime bundle: `git lfs install`.
|
- Install Git LFS before cloning or materializing the packaged runtime bundle: `git lfs install`.
|
||||||
- Run `make setup` after cloning.
|
- Run `make setup` after cloning.
|
||||||
- Run `make check` before opening a PR.
|
- Run `make check` before opening a PR.
|
||||||
|
- Run `make dist-check` when you change packaging, entrypoints, or install docs.
|
||||||
- Public user-facing CLI is `pyro`.
|
- Public user-facing CLI is `pyro`.
|
||||||
- Public Python SDK entrypoint is `from pyro_mcp import Pyro`.
|
- Public Python SDK entrypoint is `from pyro_mcp import Pyro`.
|
||||||
- The packaged runtime images under `src/pyro_mcp/runtime_bundle/` are stored in Git LFS.
|
- The packaged runtime images under `src/pyro_mcp/runtime_bundle/` are stored in Git LFS.
|
||||||
|
|
@ -28,6 +29,7 @@ This repository ships `pyro-mcp`, an MCP-compatible package for ephemeral VM lif
|
||||||
- If you need full log payloads from the Ollama demo, use `make ollama-demo OLLAMA_DEMO_FLAGS=-v`.
|
- If you need full log payloads from the Ollama demo, use `make ollama-demo OLLAMA_DEMO_FLAGS=-v`.
|
||||||
- After heavy runtime work, reclaim local space with `rm -rf build` and `git lfs prune`.
|
- After heavy runtime work, reclaim local space with `rm -rf build` and `git lfs prune`.
|
||||||
- The pre-migration `pre-lfs-*` tag is local backup material only; do not push it or it will keep the old giant blobs reachable.
|
- The pre-migration `pre-lfs-*` tag is local backup material only; do not push it or it will keep the old giant blobs reachable.
|
||||||
|
- Public contract documentation lives in `docs/public-contract.md`.
|
||||||
|
|
||||||
## Quality Gates
|
## Quality Gates
|
||||||
|
|
||||||
|
|
|
||||||
8
Makefile
8
Makefile
|
|
@ -1,4 +1,5 @@
|
||||||
PYTHON ?= uv run python
|
PYTHON ?= uv run python
|
||||||
|
UV_CACHE_DIR ?= .uv-cache
|
||||||
OLLAMA_BASE_URL ?= http://localhost:11434/v1
|
OLLAMA_BASE_URL ?= http://localhost:11434/v1
|
||||||
OLLAMA_MODEL ?= llama3.2:3b
|
OLLAMA_MODEL ?= llama3.2:3b
|
||||||
OLLAMA_DEMO_FLAGS ?=
|
OLLAMA_DEMO_FLAGS ?=
|
||||||
|
|
@ -8,7 +9,7 @@ RUNTIME_BUILD_DIR ?= build/runtime_bundle
|
||||||
RUNTIME_BUNDLE_DIR ?= src/pyro_mcp/runtime_bundle
|
RUNTIME_BUNDLE_DIR ?= src/pyro_mcp/runtime_bundle
|
||||||
RUNTIME_MATERIALIZED_DIR ?= build/runtime_sources
|
RUNTIME_MATERIALIZED_DIR ?= build/runtime_sources
|
||||||
|
|
||||||
.PHONY: help setup lint format typecheck test check demo network-demo doctor ollama ollama-demo run-server install-hooks runtime-bundle runtime-binaries runtime-kernel runtime-rootfs runtime-agent runtime-validate runtime-manifest runtime-sync runtime-clean runtime-fetch-binaries runtime-build-kernel-real runtime-build-rootfs-real runtime-materialize runtime-boot-check runtime-network-check
|
.PHONY: help setup lint format typecheck test check dist-check demo network-demo doctor ollama ollama-demo run-server install-hooks runtime-bundle runtime-binaries runtime-kernel runtime-rootfs runtime-agent runtime-validate runtime-manifest runtime-sync runtime-clean runtime-fetch-binaries runtime-build-kernel-real runtime-build-rootfs-real runtime-materialize runtime-boot-check runtime-network-check
|
||||||
|
|
||||||
help:
|
help:
|
||||||
@printf '%s\n' \
|
@printf '%s\n' \
|
||||||
|
|
@ -20,6 +21,7 @@ help:
|
||||||
' typecheck Run mypy' \
|
' typecheck Run mypy' \
|
||||||
' test Run pytest' \
|
' test Run pytest' \
|
||||||
' check Run lint, typecheck, and tests' \
|
' check Run lint, typecheck, and tests' \
|
||||||
|
' dist-check Smoke-test the installed public pyro CLI entrypoint' \
|
||||||
' demo Run the deterministic VM demo' \
|
' demo Run the deterministic VM demo' \
|
||||||
' network-demo Run the deterministic VM demo with guest networking enabled' \
|
' network-demo Run the deterministic VM demo with guest networking enabled' \
|
||||||
' doctor Show runtime and host diagnostics' \
|
' doctor Show runtime and host diagnostics' \
|
||||||
|
|
@ -59,6 +61,10 @@ test:
|
||||||
|
|
||||||
check: lint typecheck test
|
check: lint typecheck test
|
||||||
|
|
||||||
|
dist-check:
|
||||||
|
.venv/bin/pyro --version
|
||||||
|
.venv/bin/pyro --help >/dev/null
|
||||||
|
|
||||||
demo:
|
demo:
|
||||||
uv run pyro demo
|
uv run pyro demo
|
||||||
|
|
||||||
|
|
|
||||||
24
README.md
24
README.md
|
|
@ -9,6 +9,13 @@ It exposes the same runtime in two public forms:
|
||||||
|
|
||||||
It also ships an MCP server so LLM clients can use the same VM runtime through tools.
|
It also ships an MCP server so LLM clients can use the same VM runtime through tools.
|
||||||
|
|
||||||
|
## Start Here
|
||||||
|
|
||||||
|
- Install: [docs/install.md](/home/thales/projects/personal/pyro/docs/install.md)
|
||||||
|
- Host requirements: [docs/host-requirements.md](/home/thales/projects/personal/pyro/docs/host-requirements.md)
|
||||||
|
- Public contract: [docs/public-contract.md](/home/thales/projects/personal/pyro/docs/public-contract.md)
|
||||||
|
- Troubleshooting: [docs/troubleshooting.md](/home/thales/projects/personal/pyro/docs/troubleshooting.md)
|
||||||
|
|
||||||
## Public UX
|
## Public UX
|
||||||
|
|
||||||
Primary install/run path:
|
Primary install/run path:
|
||||||
|
|
@ -26,6 +33,12 @@ pyro mcp serve
|
||||||
The public user-facing interface is `pyro` and `Pyro`.
|
The public user-facing interface is `pyro` and `Pyro`.
|
||||||
`Makefile` targets are contributor conveniences for this repository and are not the primary product UX.
|
`Makefile` targets are contributor conveniences for this repository and are not the primary product UX.
|
||||||
|
|
||||||
|
Check the installed CLI version:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pyro --version
|
||||||
|
```
|
||||||
|
|
||||||
## Repository Storage
|
## Repository Storage
|
||||||
|
|
||||||
This repository uses Git LFS for the packaged runtime images under
|
This repository uses Git LFS for the packaged runtime images under
|
||||||
|
|
@ -124,6 +137,13 @@ Verbose Ollama logs:
|
||||||
pyro demo ollama -v
|
pyro demo ollama -v
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Integration Examples
|
||||||
|
|
||||||
|
- Python one-shot SDK example: [examples/python_run.py](/home/thales/projects/personal/pyro/examples/python_run.py)
|
||||||
|
- Python lifecycle example: [examples/python_lifecycle.py](/home/thales/projects/personal/pyro/examples/python_lifecycle.py)
|
||||||
|
- MCP client config example: [examples/mcp_client_config.md](/home/thales/projects/personal/pyro/examples/mcp_client_config.md)
|
||||||
|
- Agent-ready `vm_run` example: [examples/agent_vm_run.py](/home/thales/projects/personal/pyro/examples/agent_vm_run.py)
|
||||||
|
|
||||||
## Python SDK
|
## Python SDK
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
|
@ -160,6 +180,9 @@ result = pyro.exec_vm(vm_id, command="git --version", timeout_seconds=30)
|
||||||
print(result["stdout"])
|
print(result["stdout"])
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The recommended agent-facing default is still one-shot execution through `run_in_vm(...)` / `vm_run`.
|
||||||
|
Use lifecycle methods only when the agent needs VM state to persist across multiple calls.
|
||||||
|
|
||||||
## MCP Tools
|
## MCP Tools
|
||||||
|
|
||||||
Primary agent-facing tool:
|
Primary agent-facing tool:
|
||||||
|
|
@ -212,6 +235,7 @@ For work inside this repository:
|
||||||
make help
|
make help
|
||||||
make setup
|
make setup
|
||||||
make check
|
make check
|
||||||
|
make dist-check
|
||||||
```
|
```
|
||||||
|
|
||||||
Runtime build and validation helpers remain available through `make`, including:
|
Runtime build and validation helpers remain available through `make`, including:
|
||||||
|
|
|
||||||
14
TASKS.tmp.md
Normal file
14
TASKS.tmp.md
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
# Temporary Task List
|
||||||
|
|
||||||
|
Execution order for the current adoption pass:
|
||||||
|
|
||||||
|
1. Add real integration examples for users.
|
||||||
|
2. Freeze and document the public contract with regression tests.
|
||||||
|
3. Improve distribution ergonomics and verify clean-install paths.
|
||||||
|
4. Add one high-level agent-ready example centered on `vm_run`.
|
||||||
|
5. Polish install, troubleshooting, and host-requirements docs.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- `Makefile` remains contributor-only.
|
||||||
|
- Public user-facing surfaces are `pyro`, `Pyro`, and the MCP tool schemas.
|
||||||
|
- Keep guest networking explicit and documented where examples depend on it.
|
||||||
32
docs/host-requirements.md
Normal file
32
docs/host-requirements.md
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
# Host Requirements
|
||||||
|
|
||||||
|
`pyro-mcp` currently targets Linux x86_64 hosts.
|
||||||
|
|
||||||
|
## Required
|
||||||
|
|
||||||
|
- KVM available at `/dev/kvm`
|
||||||
|
- support for Firecracker microVMs
|
||||||
|
- sufficient disk for the bundled runtime images
|
||||||
|
|
||||||
|
## Required For Guest Networking
|
||||||
|
|
||||||
|
- `/dev/net/tun`
|
||||||
|
- `ip`
|
||||||
|
- `nft` or `iptables`
|
||||||
|
- host IP forwarding enabled
|
||||||
|
- privilege to create TAP devices and NAT rules
|
||||||
|
|
||||||
|
The current implementation uses `sudo -n` for host networking commands when a networked run is requested.
|
||||||
|
|
||||||
|
## Validate The Host
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pyro doctor
|
||||||
|
```
|
||||||
|
|
||||||
|
Check these fields in the output:
|
||||||
|
|
||||||
|
- `runtime_ok`
|
||||||
|
- `kvm`
|
||||||
|
- `networking.tun_available`
|
||||||
|
- `networking.ip_forward_enabled`
|
||||||
46
docs/install.md
Normal file
46
docs/install.md
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
# Install
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Linux host
|
||||||
|
- Python 3.12+
|
||||||
|
- Git LFS
|
||||||
|
- `/dev/kvm`
|
||||||
|
|
||||||
|
If you want outbound guest networking:
|
||||||
|
|
||||||
|
- `ip`
|
||||||
|
- `nft` or `iptables`
|
||||||
|
- privilege to create TAP devices and configure NAT
|
||||||
|
|
||||||
|
## Fastest Start
|
||||||
|
|
||||||
|
Run the MCP server directly from the package without a manual install:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvx --from pyro-mcp pyro mcp serve
|
||||||
|
```
|
||||||
|
|
||||||
|
Run one command in a sandbox:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvx --from pyro-mcp pyro run --profile debian-git --vcpu-count 1 --mem-mib 1024 -- git --version
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installed CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv tool install .
|
||||||
|
pyro --version
|
||||||
|
pyro doctor
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributor Clone
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git lfs install
|
||||||
|
git clone <repo>
|
||||||
|
cd pyro
|
||||||
|
git lfs pull
|
||||||
|
make setup
|
||||||
|
```
|
||||||
76
docs/public-contract.md
Normal file
76
docs/public-contract.md
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
# Public Contract
|
||||||
|
|
||||||
|
This document defines the supported public interface for `pyro-mcp`.
|
||||||
|
|
||||||
|
## Package Identity
|
||||||
|
|
||||||
|
- Distribution name: `pyro-mcp`
|
||||||
|
- Public executable: `pyro`
|
||||||
|
- Public Python import: `from pyro_mcp import Pyro`
|
||||||
|
|
||||||
|
## CLI Contract
|
||||||
|
|
||||||
|
Top-level commands:
|
||||||
|
|
||||||
|
- `pyro mcp serve`
|
||||||
|
- `pyro run`
|
||||||
|
- `pyro doctor`
|
||||||
|
- `pyro demo`
|
||||||
|
- `pyro demo ollama`
|
||||||
|
|
||||||
|
Stable `pyro run` flags:
|
||||||
|
|
||||||
|
- `--profile`
|
||||||
|
- `--vcpu-count`
|
||||||
|
- `--mem-mib`
|
||||||
|
- `--timeout-seconds`
|
||||||
|
- `--ttl-seconds`
|
||||||
|
- `--network`
|
||||||
|
|
||||||
|
Behavioral guarantees:
|
||||||
|
|
||||||
|
- `pyro run -- <command>` returns structured JSON.
|
||||||
|
- `pyro doctor` returns structured JSON diagnostics.
|
||||||
|
- `pyro demo ollama` prints log lines plus a final summary line.
|
||||||
|
|
||||||
|
## Python SDK Contract
|
||||||
|
|
||||||
|
Primary facade:
|
||||||
|
|
||||||
|
- `Pyro`
|
||||||
|
|
||||||
|
Supported public methods:
|
||||||
|
|
||||||
|
- `create_server()`
|
||||||
|
- `list_profiles()`
|
||||||
|
- `create_vm(...)`
|
||||||
|
- `start_vm(vm_id)`
|
||||||
|
- `exec_vm(vm_id, *, command, timeout_seconds=30)`
|
||||||
|
- `stop_vm(vm_id)`
|
||||||
|
- `delete_vm(vm_id)`
|
||||||
|
- `status_vm(vm_id)`
|
||||||
|
- `network_info_vm(vm_id)`
|
||||||
|
- `reap_expired()`
|
||||||
|
- `run_in_vm(...)`
|
||||||
|
|
||||||
|
## MCP Contract
|
||||||
|
|
||||||
|
Primary tool:
|
||||||
|
|
||||||
|
- `vm_run`
|
||||||
|
|
||||||
|
Advanced lifecycle tools:
|
||||||
|
|
||||||
|
- `vm_list_profiles`
|
||||||
|
- `vm_create`
|
||||||
|
- `vm_start`
|
||||||
|
- `vm_exec`
|
||||||
|
- `vm_stop`
|
||||||
|
- `vm_delete`
|
||||||
|
- `vm_status`
|
||||||
|
- `vm_network_info`
|
||||||
|
- `vm_reap_expired`
|
||||||
|
|
||||||
|
## Compatibility Rule
|
||||||
|
|
||||||
|
Changes to any command name, public flag, public method name, or MCP tool name are breaking changes and should be treated as a deliberate contract version change.
|
||||||
64
docs/troubleshooting.md
Normal file
64
docs/troubleshooting.md
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
# Troubleshooting
|
||||||
|
|
||||||
|
## `pyro doctor` reports runtime checksum mismatch
|
||||||
|
|
||||||
|
Cause:
|
||||||
|
- the Git LFS pointer files are present, but the real runtime images have not been checked out
|
||||||
|
|
||||||
|
Fix:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git lfs pull
|
||||||
|
git lfs checkout
|
||||||
|
pyro doctor
|
||||||
|
```
|
||||||
|
|
||||||
|
## `pyro run --network` fails before the guest starts
|
||||||
|
|
||||||
|
Cause:
|
||||||
|
- the host cannot create TAP devices or NAT rules
|
||||||
|
|
||||||
|
Fix:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pyro doctor
|
||||||
|
```
|
||||||
|
|
||||||
|
Then verify:
|
||||||
|
|
||||||
|
- `ip`
|
||||||
|
- `nft` or `iptables`
|
||||||
|
- `/dev/net/tun`
|
||||||
|
- host privilege for `sudo -n`
|
||||||
|
|
||||||
|
## Ollama demo exits with tool-call failures
|
||||||
|
|
||||||
|
Cause:
|
||||||
|
- the model produced an invalid tool call or your Ollama model is not reliable enough for tool use
|
||||||
|
|
||||||
|
Fix:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pyro demo ollama -v
|
||||||
|
```
|
||||||
|
|
||||||
|
Inspect:
|
||||||
|
|
||||||
|
- model output
|
||||||
|
- requested tool calls
|
||||||
|
- tool results
|
||||||
|
|
||||||
|
## Repository clone is still huge after the LFS migration
|
||||||
|
|
||||||
|
Cause:
|
||||||
|
- old refs are still present locally
|
||||||
|
- `build/` or `.venv/` duplicates are consuming disk
|
||||||
|
|
||||||
|
Fix:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rm -rf build
|
||||||
|
git lfs prune
|
||||||
|
```
|
||||||
|
|
||||||
|
If needed, recreate `.venv/`.
|
||||||
62
examples/agent_vm_run.py
Normal file
62
examples/agent_vm_run.py
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
"""Provider-agnostic agent tool example centered on vm_run."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from pyro_mcp import Pyro
|
||||||
|
|
||||||
|
VM_RUN_TOOL: dict[str, Any] = {
|
||||||
|
"name": "vm_run",
|
||||||
|
"description": "Run one command in an ephemeral Firecracker VM and clean it up.",
|
||||||
|
"input_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"profile": {"type": "string"},
|
||||||
|
"command": {"type": "string"},
|
||||||
|
"vcpu_count": {"type": "integer"},
|
||||||
|
"mem_mib": {"type": "integer"},
|
||||||
|
"timeout_seconds": {"type": "integer", "default": 30},
|
||||||
|
"ttl_seconds": {"type": "integer", "default": 600},
|
||||||
|
"network": {"type": "boolean", "default": False},
|
||||||
|
},
|
||||||
|
"required": ["profile", "command", "vcpu_count", "mem_mib"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def call_vm_run(arguments: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
pyro = Pyro()
|
||||||
|
return pyro.run_in_vm(
|
||||||
|
profile=str(arguments["profile"]),
|
||||||
|
command=str(arguments["command"]),
|
||||||
|
vcpu_count=int(arguments["vcpu_count"]),
|
||||||
|
mem_mib=int(arguments["mem_mib"]),
|
||||||
|
timeout_seconds=int(arguments.get("timeout_seconds", 30)),
|
||||||
|
ttl_seconds=int(arguments.get("ttl_seconds", 600)),
|
||||||
|
network=bool(arguments.get("network", False)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
tool_arguments: dict[str, Any] = {
|
||||||
|
"profile": "debian-git",
|
||||||
|
"command": "git --version",
|
||||||
|
"vcpu_count": 1,
|
||||||
|
"mem_mib": 1024,
|
||||||
|
"timeout_seconds": 30,
|
||||||
|
"network": False,
|
||||||
|
}
|
||||||
|
tool_call: dict[str, Any] = {
|
||||||
|
"name": "vm_run",
|
||||||
|
"arguments": tool_arguments,
|
||||||
|
}
|
||||||
|
|
||||||
|
print(json.dumps({"tool": VM_RUN_TOOL, "tool_call": tool_call}, indent=2, sort_keys=True))
|
||||||
|
result = call_vm_run(tool_arguments)
|
||||||
|
print(json.dumps({"tool_result": result}, indent=2, sort_keys=True))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
35
examples/mcp_client_config.md
Normal file
35
examples/mcp_client_config.md
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
# MCP Client Config Example
|
||||||
|
|
||||||
|
`pyro-mcp` is intended to be exposed to LLM clients through the public `pyro` CLI.
|
||||||
|
|
||||||
|
Generic stdio MCP configuration using `uvx`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"pyro": {
|
||||||
|
"command": "uvx",
|
||||||
|
"args": ["--from", "pyro-mcp", "pyro", "mcp", "serve"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If `pyro-mcp` is already installed locally, the same server can be configured with:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"pyro": {
|
||||||
|
"command": "pyro",
|
||||||
|
"args": ["mcp", "serve"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Primary tool for most agents:
|
||||||
|
|
||||||
|
- `vm_run`
|
||||||
|
|
||||||
|
Use lifecycle tools only when the agent needs persistent VM state across multiple tool calls.
|
||||||
30
examples/python_lifecycle.py
Normal file
30
examples/python_lifecycle.py
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
"""Manage VM lifecycle directly through the public Python SDK."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
from pyro_mcp import Pyro
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
pyro = Pyro()
|
||||||
|
created = pyro.create_vm(
|
||||||
|
profile="debian-git",
|
||||||
|
vcpu_count=1,
|
||||||
|
mem_mib=1024,
|
||||||
|
ttl_seconds=600,
|
||||||
|
network=False,
|
||||||
|
)
|
||||||
|
vm_id = str(created["vm_id"])
|
||||||
|
|
||||||
|
try:
|
||||||
|
pyro.start_vm(vm_id)
|
||||||
|
result = pyro.exec_vm(vm_id, command="git --version", timeout_seconds=30)
|
||||||
|
print(json.dumps(result, indent=2, sort_keys=True))
|
||||||
|
finally:
|
||||||
|
pyro.delete_vm(vm_id)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
24
examples/python_run.py
Normal file
24
examples/python_run.py
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
"""Run one command in an ephemeral VM through the public Python SDK."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
from pyro_mcp import Pyro
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
pyro = Pyro()
|
||||||
|
result = pyro.run_in_vm(
|
||||||
|
profile="debian-git",
|
||||||
|
command="git --version",
|
||||||
|
vcpu_count=1,
|
||||||
|
mem_mib=1024,
|
||||||
|
timeout_seconds=30,
|
||||||
|
network=False,
|
||||||
|
)
|
||||||
|
print(json.dumps(result, indent=2, sort_keys=True))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -26,6 +26,7 @@ packages = ["src/pyro_mcp"]
|
||||||
|
|
||||||
[tool.hatch.build.targets.sdist]
|
[tool.hatch.build.targets.sdist]
|
||||||
include = [
|
include = [
|
||||||
|
"docs/**",
|
||||||
"src/pyro_mcp/runtime_bundle/**",
|
"src/pyro_mcp/runtime_bundle/**",
|
||||||
"runtime_sources/**",
|
"runtime_sources/**",
|
||||||
"src/pyro_mcp/**/*.py",
|
"src/pyro_mcp/**/*.py",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
"""Public package surface for pyro_mcp."""
|
"""Public package surface for pyro_mcp."""
|
||||||
|
|
||||||
|
from importlib.metadata import version
|
||||||
|
|
||||||
from pyro_mcp.api import Pyro
|
from pyro_mcp.api import Pyro
|
||||||
from pyro_mcp.server import create_server
|
from pyro_mcp.server import create_server
|
||||||
from pyro_mcp.vm_manager import VmManager
|
from pyro_mcp.vm_manager import VmManager
|
||||||
|
|
||||||
__all__ = ["Pyro", "VmManager", "create_server"]
|
__version__ = version("pyro-mcp")
|
||||||
|
|
||||||
|
__all__ = ["Pyro", "VmManager", "__version__", "create_server"]
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import argparse
|
||||||
import json
|
import json
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from pyro_mcp import __version__
|
||||||
from pyro_mcp.api import Pyro
|
from pyro_mcp.api import Pyro
|
||||||
from pyro_mcp.demo import run_demo
|
from pyro_mcp.demo import run_demo
|
||||||
from pyro_mcp.ollama_demo import DEFAULT_OLLAMA_BASE_URL, DEFAULT_OLLAMA_MODEL, run_ollama_tool_demo
|
from pyro_mcp.ollama_demo import DEFAULT_OLLAMA_BASE_URL, DEFAULT_OLLAMA_MODEL, run_ollama_tool_demo
|
||||||
|
|
@ -18,6 +19,7 @@ def _print_json(payload: dict[str, Any]) -> None:
|
||||||
|
|
||||||
def _build_parser() -> argparse.ArgumentParser:
|
def _build_parser() -> argparse.ArgumentParser:
|
||||||
parser = argparse.ArgumentParser(description="pyro CLI for ephemeral Firecracker VMs.")
|
parser = argparse.ArgumentParser(description="pyro CLI for ephemeral Firecracker VMs.")
|
||||||
|
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
||||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||||
|
|
||||||
mcp_parser = subparsers.add_parser("mcp", help="Run the MCP server.")
|
mcp_parser = subparsers.add_parser("mcp", help="Run the MCP server.")
|
||||||
|
|
|
||||||
41
src/pyro_mcp/contract.py
Normal file
41
src/pyro_mcp/contract.py
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
"""Public contract constants for the CLI, SDK, and MCP server."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
PUBLIC_CLI_COMMANDS = ("mcp", "run", "doctor", "demo")
|
||||||
|
PUBLIC_CLI_DEMO_SUBCOMMANDS = ("ollama",)
|
||||||
|
PUBLIC_CLI_RUN_FLAGS = (
|
||||||
|
"--profile",
|
||||||
|
"--vcpu-count",
|
||||||
|
"--mem-mib",
|
||||||
|
"--timeout-seconds",
|
||||||
|
"--ttl-seconds",
|
||||||
|
"--network",
|
||||||
|
)
|
||||||
|
|
||||||
|
PUBLIC_SDK_METHODS = (
|
||||||
|
"create_server",
|
||||||
|
"create_vm",
|
||||||
|
"delete_vm",
|
||||||
|
"exec_vm",
|
||||||
|
"list_profiles",
|
||||||
|
"network_info_vm",
|
||||||
|
"reap_expired",
|
||||||
|
"run_in_vm",
|
||||||
|
"start_vm",
|
||||||
|
"status_vm",
|
||||||
|
"stop_vm",
|
||||||
|
)
|
||||||
|
|
||||||
|
PUBLIC_MCP_TOOLS = (
|
||||||
|
"vm_run",
|
||||||
|
"vm_list_profiles",
|
||||||
|
"vm_create",
|
||||||
|
"vm_start",
|
||||||
|
"vm_exec",
|
||||||
|
"vm_stop",
|
||||||
|
"vm_delete",
|
||||||
|
"vm_status",
|
||||||
|
"vm_network_info",
|
||||||
|
"vm_reap_expired",
|
||||||
|
)
|
||||||
94
tests/test_public_contract.py
Normal file
94
tests/test_public_contract.py
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import asyncio
|
||||||
|
import io
|
||||||
|
import tomllib
|
||||||
|
from contextlib import redirect_stdout
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, cast
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from pyro_mcp import Pyro, __version__
|
||||||
|
from pyro_mcp.cli import _build_parser
|
||||||
|
from pyro_mcp.contract import (
|
||||||
|
PUBLIC_CLI_COMMANDS,
|
||||||
|
PUBLIC_CLI_DEMO_SUBCOMMANDS,
|
||||||
|
PUBLIC_CLI_RUN_FLAGS,
|
||||||
|
PUBLIC_MCP_TOOLS,
|
||||||
|
PUBLIC_SDK_METHODS,
|
||||||
|
)
|
||||||
|
from pyro_mcp.vm_manager import VmManager
|
||||||
|
from pyro_mcp.vm_network import TapNetworkManager
|
||||||
|
|
||||||
|
|
||||||
|
def _subparser_choice(parser: argparse.ArgumentParser, name: str) -> argparse.ArgumentParser:
|
||||||
|
subparsers = getattr(parser, "_subparsers", None)
|
||||||
|
if subparsers is None:
|
||||||
|
raise AssertionError("parser does not define subparsers")
|
||||||
|
group_actions = cast(list[Any], subparsers._group_actions) # noqa: SLF001
|
||||||
|
if not group_actions:
|
||||||
|
raise AssertionError("parser subparsers are empty")
|
||||||
|
choices = cast(dict[str, argparse.ArgumentParser], group_actions[0].choices)
|
||||||
|
return choices[name]
|
||||||
|
|
||||||
|
|
||||||
|
def test_public_sdk_methods_exist() -> None:
|
||||||
|
assert tuple(sorted(PUBLIC_SDK_METHODS)) == PUBLIC_SDK_METHODS
|
||||||
|
for method_name in PUBLIC_SDK_METHODS:
|
||||||
|
assert hasattr(Pyro, method_name), method_name
|
||||||
|
|
||||||
|
|
||||||
|
def test_public_cli_help_lists_commands_and_run_flags() -> None:
|
||||||
|
parser = _build_parser()
|
||||||
|
help_text = parser.format_help()
|
||||||
|
assert "--version" in help_text
|
||||||
|
for command_name in PUBLIC_CLI_COMMANDS:
|
||||||
|
assert command_name in help_text
|
||||||
|
|
||||||
|
run_parser = _build_parser()
|
||||||
|
run_help = run_parser.parse_args(
|
||||||
|
["run", "--profile", "debian-base", "--vcpu-count", "1", "--mem-mib", "512", "--", "true"]
|
||||||
|
)
|
||||||
|
assert run_help.command == "run"
|
||||||
|
|
||||||
|
run_help_text = _subparser_choice(parser, "run").format_help()
|
||||||
|
for flag in PUBLIC_CLI_RUN_FLAGS:
|
||||||
|
assert flag in run_help_text
|
||||||
|
|
||||||
|
demo_help_text = _subparser_choice(parser, "demo").format_help()
|
||||||
|
for subcommand_name in PUBLIC_CLI_DEMO_SUBCOMMANDS:
|
||||||
|
assert subcommand_name in demo_help_text
|
||||||
|
|
||||||
|
|
||||||
|
def test_public_cli_version_matches_package_version() -> None:
|
||||||
|
parser = _build_parser()
|
||||||
|
stdout = io.StringIO()
|
||||||
|
with pytest.raises(SystemExit, match="0"), redirect_stdout(stdout):
|
||||||
|
parser.parse_args(["--version"])
|
||||||
|
assert stdout.getvalue().strip().endswith(f" {__version__}")
|
||||||
|
|
||||||
|
|
||||||
|
def test_public_mcp_tools_match_contract(tmp_path: Path) -> None:
|
||||||
|
pyro = Pyro(
|
||||||
|
manager=VmManager(
|
||||||
|
backend_name="mock",
|
||||||
|
base_dir=tmp_path / "vms",
|
||||||
|
network_manager=TapNetworkManager(enabled=False),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _run() -> tuple[str, ...]:
|
||||||
|
server = pyro.create_server()
|
||||||
|
tools = await server.list_tools()
|
||||||
|
return tuple(sorted(tool.name for tool in tools))
|
||||||
|
|
||||||
|
tool_names = asyncio.run(_run())
|
||||||
|
assert tool_names == tuple(sorted(PUBLIC_MCP_TOOLS))
|
||||||
|
|
||||||
|
|
||||||
|
def test_pyproject_exposes_single_public_cli_script() -> None:
|
||||||
|
pyproject = tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8"))
|
||||||
|
scripts = pyproject["project"]["scripts"]
|
||||||
|
assert scripts == {"pyro": "pyro_mcp.cli:main"}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue