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

@ -191,6 +191,8 @@ def test_workspace_tools_round_trip(tmp_path: Path) -> None:
source_dir = tmp_path / "seed"
source_dir.mkdir()
(source_dir / "note.txt").write_text("ok\n", encoding="utf-8")
secret_file = tmp_path / "token.txt"
secret_file.write_text("from-file\n", encoding="utf-8")
def _extract_structured(raw_result: object) -> dict[str, Any]:
if not isinstance(raw_result, tuple) or len(raw_result) != 2:
@ -209,6 +211,10 @@ def test_workspace_tools_round_trip(tmp_path: Path) -> None:
"environment": "debian:12-base",
"allow_host_compat": True,
"seed_path": str(source_dir),
"secrets": [
{"name": "API_TOKEN", "value": "expected"},
{"name": "FILE_TOKEN", "file_path": str(secret_file)},
],
},
)
)
@ -231,7 +237,8 @@ def test_workspace_tools_round_trip(tmp_path: Path) -> None:
"workspace_exec",
{
"workspace_id": workspace_id,
"command": "cat subdir/more.txt",
"command": 'sh -lc \'printf "%s\\n" "$API_TOKEN"\'',
"secret_env": {"API_TOKEN": "API_TOKEN"},
},
)
)
@ -264,8 +271,12 @@ def test_workspace_tools_round_trip(tmp_path: Path) -> None:
{
"workspace_id": workspace_id,
"service_name": "app",
"command": "sh -lc 'touch .ready; while true; do sleep 60; done'",
"command": (
'sh -lc \'trap "exit 0" TERM; printf "%s\\n" "$API_TOKEN" >&2; '
'touch .ready; while true; do sleep 60; done\''
),
"ready_file": ".ready",
"secret_env": {"API_TOKEN": "API_TOKEN"},
},
)
)
@ -357,8 +368,12 @@ def test_workspace_tools_round_trip(tmp_path: Path) -> None:
) = asyncio.run(_run())
assert created["state"] == "started"
assert created["workspace_seed"]["mode"] == "directory"
assert created["secrets"] == [
{"name": "API_TOKEN", "source_kind": "literal"},
{"name": "FILE_TOKEN", "source_kind": "file"},
]
assert synced["workspace_sync"]["destination"] == "/workspace/subdir"
assert executed["stdout"] == "more\n"
assert executed["stdout"] == "[REDACTED]\n"
assert diffed["changed"] is True
assert snapshot["snapshot"]["snapshot_name"] == "checkpoint"
assert [entry["snapshot_name"] for entry in snapshots["snapshots"]] == [
@ -370,9 +385,11 @@ def test_workspace_tools_round_trip(tmp_path: Path) -> None:
assert service["state"] == "running"
assert services["count"] == 1
assert service_status["state"] == "running"
assert service_logs["stderr"].count("[REDACTED]") >= 1
assert service_logs["tail_lines"] is None
assert service_stopped["state"] == "stopped"
assert reset["workspace_reset"]["snapshot_name"] == "checkpoint"
assert reset["secrets"] == created["secrets"]
assert reset["command_count"] == 0
assert reset["service_count"] == 0
assert deleted_snapshot["deleted"] is True