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
|
|
@ -36,6 +36,10 @@ def test_create_server_registers_vm_tools(tmp_path: Path) -> None:
|
|||
assert "workspace_stop" in tool_names
|
||||
assert "workspace_diff" in tool_names
|
||||
assert "workspace_export" in tool_names
|
||||
assert "workspace_file_list" in tool_names
|
||||
assert "workspace_file_read" in tool_names
|
||||
assert "workspace_file_write" in tool_names
|
||||
assert "workspace_patch_apply" in tool_names
|
||||
assert "workspace_disk_export" in tool_names
|
||||
assert "workspace_disk_list" in tool_names
|
||||
assert "workspace_disk_read" in tool_names
|
||||
|
|
@ -247,6 +251,51 @@ def test_workspace_tools_round_trip(tmp_path: Path) -> None:
|
|||
},
|
||||
)
|
||||
)
|
||||
listed_files = _extract_structured(
|
||||
await server.call_tool(
|
||||
"workspace_file_list",
|
||||
{
|
||||
"workspace_id": workspace_id,
|
||||
"path": "/workspace",
|
||||
"recursive": True,
|
||||
},
|
||||
)
|
||||
)
|
||||
file_read = _extract_structured(
|
||||
await server.call_tool(
|
||||
"workspace_file_read",
|
||||
{
|
||||
"workspace_id": workspace_id,
|
||||
"path": "note.txt",
|
||||
"max_bytes": 4096,
|
||||
},
|
||||
)
|
||||
)
|
||||
file_written = _extract_structured(
|
||||
await server.call_tool(
|
||||
"workspace_file_write",
|
||||
{
|
||||
"workspace_id": workspace_id,
|
||||
"path": "src/app.py",
|
||||
"text": "print('hello from file op')\n",
|
||||
},
|
||||
)
|
||||
)
|
||||
patched = _extract_structured(
|
||||
await server.call_tool(
|
||||
"workspace_patch_apply",
|
||||
{
|
||||
"workspace_id": workspace_id,
|
||||
"patch": (
|
||||
"--- a/note.txt\n"
|
||||
"+++ b/note.txt\n"
|
||||
"@@ -1 +1 @@\n"
|
||||
"-ok\n"
|
||||
"+patched\n"
|
||||
),
|
||||
},
|
||||
)
|
||||
)
|
||||
diffed = _extract_structured(
|
||||
await server.call_tool("workspace_diff", {"workspace_id": workspace_id})
|
||||
)
|
||||
|
|
@ -338,6 +387,10 @@ def test_workspace_tools_round_trip(tmp_path: Path) -> None:
|
|||
created,
|
||||
synced,
|
||||
executed,
|
||||
listed_files,
|
||||
file_read,
|
||||
file_written,
|
||||
patched,
|
||||
diffed,
|
||||
snapshot,
|
||||
snapshots,
|
||||
|
|
@ -357,6 +410,10 @@ def test_workspace_tools_round_trip(tmp_path: Path) -> None:
|
|||
created,
|
||||
synced,
|
||||
executed,
|
||||
listed_files,
|
||||
file_read,
|
||||
file_written,
|
||||
patched,
|
||||
diffed,
|
||||
snapshot,
|
||||
snapshots,
|
||||
|
|
@ -379,6 +436,10 @@ def test_workspace_tools_round_trip(tmp_path: Path) -> None:
|
|||
]
|
||||
assert synced["workspace_sync"]["destination"] == "/workspace/subdir"
|
||||
assert executed["stdout"] == "[REDACTED]\n"
|
||||
assert any(entry["path"] == "/workspace/note.txt" for entry in listed_files["entries"])
|
||||
assert file_read["content"] == "ok\n"
|
||||
assert file_written["path"] == "/workspace/src/app.py"
|
||||
assert patched["changed"] is True
|
||||
assert diffed["changed"] is True
|
||||
assert snapshot["snapshot"]["snapshot_name"] == "checkpoint"
|
||||
assert [entry["snapshot_name"] for entry in snapshots["snapshots"]] == [
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue