Add seeded task workspace creation

Current persistent tasks started with an empty workspace, which blocked the first useful host-to-task workflow in the task roadmap. This change lets task creation start from a host directory or tar archive without changing the one-shot VM surfaces.

Expose source_path on task create across the CLI, SDK, and MCP, add safe archive upload and extraction support for guest and host-compat backends, persist workspace_seed metadata, and patch the per-task rootfs with the bundled guest agent before boot so seeded guest tasks work without republishing environments. Also switch post--- command reconstruction to shlex.join() so documented sh -lc task examples preserve argument boundaries.

Validation:
- uv lock
- UV_CACHE_DIR=.uv-cache uv run pytest --no-cov tests/test_vm_guest.py tests/test_vm_manager.py tests/test_cli.py tests/test_api.py tests/test_server.py tests/test_public_contract.py
- UV_CACHE_DIR=.uv-cache make check
- UV_CACHE_DIR=.uv-cache make dist-check
- real guest-backed smoke: task create --source-path, task exec -- cat note.txt, task delete
This commit is contained in:
Thales Maciel 2026-03-11 21:45:38 -03:00
parent 58df176148
commit aa886b346e
25 changed files with 1076 additions and 75 deletions

View file

@ -60,9 +60,13 @@ def test_cli_subcommand_help_includes_examples_and_guidance() -> None:
assert "Use this from an MCP client config after the CLI evaluation path works." in mcp_help
task_help = _subparser_choice(parser, "task").format_help()
assert "pyro task create debian:12" in task_help
assert "pyro task create debian:12 --source-path ./repo" in task_help
assert "pyro task exec TASK_ID" in task_help
task_create_help = _subparser_choice(_subparser_choice(parser, "task"), "create").format_help()
assert "--source-path" in task_create_help
assert "seed into `/workspace`" in task_create_help
task_exec_help = _subparser_choice(_subparser_choice(parser, "task"), "exec").format_help()
assert "persistent `/workspace`" in task_exec_help
assert "pyro task exec TASK_ID -- cat note.txt" in task_exec_help
@ -326,12 +330,20 @@ def test_cli_requires_run_command() -> None:
cli._require_command([])
def test_cli_requires_command_preserves_shell_argument_boundaries() -> None:
command = cli._require_command(
["--", "sh", "-lc", 'printf "hello from task\\n" > note.txt']
)
assert command == 'sh -lc \'printf "hello from task\\n" > note.txt\''
def test_cli_task_create_prints_json(
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
class StubPyro:
def create_task(self, **kwargs: Any) -> dict[str, Any]:
assert kwargs["environment"] == "debian:12"
assert kwargs["source_path"] == "./repo"
return {"task_id": "task-123", "state": "started"}
class StubParser:
@ -345,6 +357,7 @@ def test_cli_task_create_prints_json(
ttl_seconds=600,
network=False,
allow_host_compat=False,
source_path="./repo",
json=True,
)
@ -366,6 +379,13 @@ def test_cli_task_create_prints_human(
"environment": "debian:12",
"state": "started",
"workspace_path": "/workspace",
"workspace_seed": {
"mode": "directory",
"source_path": "/tmp/repo",
"destination": "/workspace",
"entry_count": 1,
"bytes_written": 6,
},
"execution_mode": "guest_vsock",
"vcpu_count": 1,
"mem_mib": 1024,
@ -384,6 +404,7 @@ def test_cli_task_create_prints_human(
ttl_seconds=600,
network=False,
allow_host_compat=False,
source_path="/tmp/repo",
json=False,
)
@ -393,6 +414,7 @@ def test_cli_task_create_prints_human(
output = capsys.readouterr().out
assert "Task: task-123" in output
assert "Workspace: /workspace" in output
assert "Workspace seed: directory from /tmp/repo" in output
def test_cli_task_exec_prints_human_output(