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.
81 lines
3.7 KiB
Python
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()
|