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:
parent
18b8fd2a7d
commit
fc72fcd3a1
32 changed files with 1980 additions and 181 deletions
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue