pyro-mcp/examples/python_workspace.py
Thales Maciel fc72fcd3a1 Add guest-only workspace secrets
Add explicit workspace secrets across the CLI, SDK, and MCP, with create-time secret definitions and per-call secret-to-env mapping for exec, shell open, and service start. Persist only safe secret metadata in workspace records, materialize secret files under /run/pyro-secrets, and redact secret values from exec output, shell reads, service logs, and surfaced errors.

Fix the remaining real-guest shell gap by shipping bundled guest init alongside the guest agent and patching both into guest-backed workspace rootfs images before boot. The new init mounts devpts so PTY shells work on Firecracker guests, while reset continues to recreate the sandbox and re-materialize secrets from stored task-local secret material.

Validation: uv lock; UV_CACHE_DIR=.uv-cache make check; UV_CACHE_DIR=.uv-cache make dist-check; and a real guest-backed Firecracker smoke covering workspace create with secrets, secret-backed exec, shell, service, reset, and delete.
2026-03-12 15:43:34 -03:00

81 lines
3.7 KiB
Python

from __future__ import annotations
import tempfile
from pathlib import Path
from pyro_mcp import Pyro
def main() -> None:
pyro = Pyro()
with (
tempfile.TemporaryDirectory(prefix="pyro-workspace-seed-") as seed_dir,
tempfile.TemporaryDirectory(prefix="pyro-workspace-sync-") as sync_dir,
tempfile.TemporaryDirectory(prefix="pyro-workspace-export-") as export_dir,
tempfile.TemporaryDirectory(prefix="pyro-workspace-secret-") as secret_dir,
):
Path(seed_dir, "note.txt").write_text("hello from seed\n", encoding="utf-8")
Path(sync_dir, "note.txt").write_text("hello from sync\n", encoding="utf-8")
secret_file = Path(secret_dir, "token.txt")
secret_file.write_text("from-file\n", encoding="utf-8")
created = pyro.create_workspace(
environment="debian:12",
seed_path=seed_dir,
secrets=[
{"name": "API_TOKEN", "value": "expected"},
{"name": "FILE_TOKEN", "file_path": str(secret_file)},
],
)
workspace_id = str(created["workspace_id"])
try:
pyro.push_workspace_sync(workspace_id, sync_dir)
result = pyro.exec_workspace(workspace_id, command="cat note.txt")
print(result["stdout"], end="")
secret_result = pyro.exec_workspace(
workspace_id,
command='sh -lc \'printf "%s\\n" "$API_TOKEN"\'',
secret_env={"API_TOKEN": "API_TOKEN"},
)
print(secret_result["stdout"], end="")
diff_result = pyro.diff_workspace(workspace_id)
print(f"changed={diff_result['changed']} total={diff_result['summary']['total']}")
snapshot = pyro.create_snapshot(workspace_id, "checkpoint")
print(snapshot["snapshot"]["snapshot_name"])
exported_path = Path(export_dir, "note.txt")
pyro.export_workspace(workspace_id, "note.txt", output_path=exported_path)
print(exported_path.read_text(encoding="utf-8"), end="")
shell = pyro.open_shell(workspace_id, secret_env={"API_TOKEN": "API_TOKEN"})
shell_id = str(shell["shell_id"])
pyro.write_shell(
workspace_id,
shell_id,
input='printf "%s\\n" "$API_TOKEN"',
)
shell_output = pyro.read_shell(workspace_id, shell_id, cursor=0)
print(f"shell_output_len={len(shell_output['output'])}")
pyro.close_shell(workspace_id, shell_id)
pyro.start_service(
workspace_id,
"web",
command="touch .web-ready && while true; do sleep 60; done",
readiness={"type": "file", "path": ".web-ready"},
secret_env={"API_TOKEN": "API_TOKEN"},
)
services = pyro.list_services(workspace_id)
print(f"services={services['count']} running={services['running_count']}")
service_status = pyro.status_service(workspace_id, "web")
print(f"service_state={service_status['state']} ready_at={service_status['ready_at']}")
service_logs = pyro.logs_service(workspace_id, "web", tail_lines=20)
print(f"service_stdout_len={len(service_logs['stdout'])}")
pyro.stop_service(workspace_id, "web")
reset = pyro.reset_workspace(workspace_id, snapshot="checkpoint")
print(f"reset_count={reset['reset_count']}")
print(f"secret_count={len(reset['secrets'])}")
logs = pyro.logs_workspace(workspace_id)
print(f"workspace_id={workspace_id} command_count={logs['count']}")
finally:
pyro.delete_workspace(workspace_id)
if __name__ == "__main__":
main()