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.
This commit is contained in:
Thales Maciel 2026-03-12 15:43:34 -03:00
parent 18b8fd2a7d
commit fc72fcd3a1
32 changed files with 1980 additions and 181 deletions

View file

@ -87,6 +87,7 @@ class Pyro:
network: bool = False,
allow_host_compat: bool = DEFAULT_ALLOW_HOST_COMPAT,
seed_path: str | Path | None = None,
secrets: list[dict[str, str]] | None = None,
) -> dict[str, Any]:
return self._manager.create_workspace(
environment=environment,
@ -96,6 +97,7 @@ class Pyro:
network=network,
allow_host_compat=allow_host_compat,
seed_path=seed_path,
secrets=secrets,
)
def exec_workspace(
@ -104,11 +106,13 @@ class Pyro:
*,
command: str,
timeout_seconds: int = DEFAULT_TIMEOUT_SECONDS,
secret_env: dict[str, str] | None = None,
) -> dict[str, Any]:
return self._manager.exec_workspace(
workspace_id,
command=command,
timeout_seconds=timeout_seconds,
secret_env=secret_env,
)
def status_workspace(self, workspace_id: str) -> dict[str, Any]:
@ -170,12 +174,14 @@ class Pyro:
cwd: str = "/workspace",
cols: int = 120,
rows: int = 30,
secret_env: dict[str, str] | None = None,
) -> dict[str, Any]:
return self._manager.open_shell(
workspace_id,
cwd=cwd,
cols=cols,
rows=rows,
secret_env=secret_env,
)
def read_shell(
@ -234,6 +240,7 @@ class Pyro:
readiness: dict[str, Any] | None = None,
ready_timeout_seconds: int = 30,
ready_interval_ms: int = 500,
secret_env: dict[str, str] | None = None,
) -> dict[str, Any]:
return self._manager.start_service(
workspace_id,
@ -243,6 +250,7 @@ class Pyro:
readiness=readiness,
ready_timeout_seconds=ready_timeout_seconds,
ready_interval_ms=ready_interval_ms,
secret_env=secret_env,
)
def list_services(self, workspace_id: str) -> dict[str, Any]:
@ -403,6 +411,7 @@ class Pyro:
network: bool = False,
allow_host_compat: bool = DEFAULT_ALLOW_HOST_COMPAT,
seed_path: str | None = None,
secrets: list[dict[str, str]] | None = None,
) -> dict[str, Any]:
"""Create and start a persistent workspace."""
return self.create_workspace(
@ -413,6 +422,7 @@ class Pyro:
network=network,
allow_host_compat=allow_host_compat,
seed_path=seed_path,
secrets=secrets,
)
@server.tool()
@ -420,12 +430,14 @@ class Pyro:
workspace_id: str,
command: str,
timeout_seconds: int = DEFAULT_TIMEOUT_SECONDS,
secret_env: dict[str, str] | None = None,
) -> dict[str, Any]:
"""Run one command inside an existing persistent workspace."""
return self.exec_workspace(
workspace_id,
command=command,
timeout_seconds=timeout_seconds,
secret_env=secret_env,
)
@server.tool()
@ -490,9 +502,16 @@ class Pyro:
cwd: str = "/workspace",
cols: int = 120,
rows: int = 30,
secret_env: dict[str, str] | None = None,
) -> dict[str, Any]:
"""Open a persistent interactive shell inside one workspace."""
return self.open_shell(workspace_id, cwd=cwd, cols=cols, rows=rows)
return self.open_shell(
workspace_id,
cwd=cwd,
cols=cols,
rows=rows,
secret_env=secret_env,
)
@server.tool()
async def shell_read(
@ -554,6 +573,7 @@ class Pyro:
ready_command: str | None = None,
ready_timeout_seconds: int = 30,
ready_interval_ms: int = 500,
secret_env: dict[str, str] | None = None,
) -> dict[str, Any]:
"""Start a named long-running service inside a workspace."""
readiness: dict[str, Any] | None = None
@ -573,6 +593,7 @@ class Pyro:
readiness=readiness,
ready_timeout_seconds=ready_timeout_seconds,
ready_interval_ms=ready_interval_ms,
secret_env=secret_env,
)
@server.tool()