Add model-native workspace file operations

Remove shell-escaped file mutation from the stable workspace flow by adding explicit file and patch tools across the CLI, SDK, and MCP surfaces.

This adds workspace file list/read/write plus unified text patch application, backed by new guest and manager file primitives that stay scoped to started workspaces and /workspace only. Patch application is preflighted on the host, file writes stay text-only and bounded, and the existing diff/export/reset semantics remain intact.

The milestone also updates the 3.2.0 roadmap, public contract, docs, examples, and versioning, and includes focused coverage for the new helper module and dispatch paths.

Validation:
- uv lock
- UV_CACHE_DIR=.uv-cache make check
- UV_CACHE_DIR=.uv-cache make dist-check
- real guest-backed smoke for workspace file read, patch apply, exec, export, and delete
This commit is contained in:
Thales Maciel 2026-03-12 22:03:25 -03:00
parent dbb71a3174
commit ab02ae46c7
27 changed files with 3068 additions and 17 deletions

View file

@ -552,6 +552,91 @@ def test_workspace_diff_and_export_round_trip(tmp_path: Path) -> None:
assert logs["count"] == 0
def test_workspace_file_ops_and_patch_round_trip(tmp_path: Path) -> None:
seed_dir = tmp_path / "seed"
seed_dir.mkdir()
src_dir = seed_dir / "src"
src_dir.mkdir()
(src_dir / "app.py").write_text('print("bug")\n', encoding="utf-8")
manager = VmManager(
backend_name="mock",
base_dir=tmp_path / "vms",
network_manager=TapNetworkManager(enabled=False),
)
workspace_id = str(
manager.create_workspace(
environment="debian:12-base",
allow_host_compat=True,
seed_path=seed_dir,
)["workspace_id"]
)
listing = manager.list_workspace_files(workspace_id, path="src", recursive=True)
assert listing["entries"] == [
{
"path": "/workspace/src/app.py",
"artifact_type": "file",
"size_bytes": 13,
"link_target": None,
}
]
read_payload = manager.read_workspace_file(workspace_id, "src/app.py")
assert read_payload["content"] == 'print("bug")\n'
written = manager.write_workspace_file(
workspace_id,
"src/generated/out.txt",
text="generated\n",
)
assert written["bytes_written"] == 10
patch_payload = manager.apply_workspace_patch(
workspace_id,
patch=(
"--- a/src/app.py\n"
"+++ b/src/app.py\n"
"@@ -1 +1 @@\n"
'-print("bug")\n'
'+print("fixed")\n'
"--- /dev/null\n"
"+++ b/src/new.py\n"
"@@ -0,0 +1 @@\n"
'+print("new")\n'
),
)
assert patch_payload["changed"] is True
assert patch_payload["summary"] == {
"total": 2,
"added": 1,
"modified": 1,
"deleted": 0,
}
executed = manager.exec_workspace(
workspace_id,
command="python3 src/app.py && cat src/new.py && cat src/generated/out.txt",
timeout_seconds=30,
)
assert executed["stdout"] == 'fixed\nprint("new")\ngenerated\n'
diff_payload = manager.diff_workspace(workspace_id)
assert diff_payload["changed"] is True
assert diff_payload["summary"]["added"] == 2
assert diff_payload["summary"]["modified"] == 1
output_path = tmp_path / "exported-app.py"
export_payload = manager.export_workspace(
workspace_id,
path="src/app.py",
output_path=output_path,
)
assert export_payload["artifact_type"] == "file"
assert output_path.read_text(encoding="utf-8") == 'print("fixed")\n'
def test_workspace_export_directory_uses_exact_output_path(tmp_path: Path) -> None:
seed_dir = tmp_path / "seed"
nested_dir = seed_dir / "src"