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:
parent
dbb71a3174
commit
ab02ae46c7
27 changed files with 3068 additions and 17 deletions
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue