delete draft workspace!
This commit is contained in:
@@ -511,6 +511,8 @@ Concrete MCP sequence:
|
|||||||
7. `wf.workflow.create_artifact_from_workspace` after validation is clean.
|
7. `wf.workflow.create_artifact_from_workspace` after validation is clean.
|
||||||
8. `wf.workflow.save_deployment`, then `validate_deployment`, then
|
8. `wf.workflow.save_deployment`, then `validate_deployment`, then
|
||||||
`run_deployment`.
|
`run_deployment`.
|
||||||
|
9. `wf.workflow.delete_draft_workspace` when the mutable authoring session is no
|
||||||
|
longer needed.
|
||||||
|
|
||||||
`create_artifact_from_workspace` also uses a `request` object:
|
`create_artifact_from_workspace` also uses a `request` object:
|
||||||
|
|
||||||
|
|||||||
@@ -353,6 +353,7 @@ resending the full draft each turn.
|
|||||||
| Fetch current draft workspace | `wf.workflow.get_draft_workspace` |
|
| Fetch current draft workspace | `wf.workflow.get_draft_workspace` |
|
||||||
| Patch current draft workspace | `wf.workflow.patch_draft_workspace` |
|
| Patch current draft workspace | `wf.workflow.patch_draft_workspace` |
|
||||||
| Save final workspace as artifact | `wf.workflow.create_artifact_from_workspace` |
|
| Save final workspace as artifact | `wf.workflow.create_artifact_from_workspace` |
|
||||||
|
| Clean up a draft workspace | `wf.workflow.delete_draft_workspace` |
|
||||||
|
|
||||||
Workspace patches are optimistic-concurrency guarded. Pass the current
|
Workspace patches are optimistic-concurrency guarded. Pass the current
|
||||||
`revision` from `get_draft_workspace`; a stale revision returns
|
`revision` from `get_draft_workspace`; a stale revision returns
|
||||||
|
|||||||
@@ -50,6 +50,9 @@ class DraftWorkspaceStore:
|
|||||||
def list_workspaces(self) -> list[WorkflowDraftWorkspace]:
|
def list_workspaces(self) -> list[WorkflowDraftWorkspace]:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def delete_workspace(self, workspace_id: str) -> bool:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class FileDraftWorkspaceStore(DraftWorkspaceStore):
|
class FileDraftWorkspaceStore(DraftWorkspaceStore):
|
||||||
"""JSON file-backed draft workspace store for local development and tests.
|
"""JSON file-backed draft workspace store for local development and tests.
|
||||||
@@ -113,6 +116,14 @@ class FileDraftWorkspaceStore(DraftWorkspaceStore):
|
|||||||
for path in sorted(self.workspaces_dir.glob("*.json"))
|
for path in sorted(self.workspaces_dir.glob("*.json"))
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def delete_workspace(self, workspace_id: str) -> bool:
|
||||||
|
with self._lock:
|
||||||
|
path = self._workspace_path(workspace_id)
|
||||||
|
if not path.exists():
|
||||||
|
return False
|
||||||
|
path.unlink()
|
||||||
|
return True
|
||||||
|
|
||||||
def _workspace_path(self, workspace_id: str) -> Path:
|
def _workspace_path(self, workspace_id: str) -> Path:
|
||||||
safe_id = ensure_workspace_id(workspace_id)
|
safe_id = ensure_workspace_id(workspace_id)
|
||||||
root = self.workspaces_dir.resolve()
|
root = self.workspaces_dir.resolve()
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ _SEARCH_ALWAYS_VISIBLE_TOOL_NAMES = [
|
|||||||
"wf.workflow.list_draft_workspaces",
|
"wf.workflow.list_draft_workspaces",
|
||||||
"wf.workflow.create_draft_workspace",
|
"wf.workflow.create_draft_workspace",
|
||||||
"wf.workflow.get_draft_workspace",
|
"wf.workflow.get_draft_workspace",
|
||||||
|
"wf.workflow.delete_draft_workspace",
|
||||||
"wf.workflow.patch_draft_workspace",
|
"wf.workflow.patch_draft_workspace",
|
||||||
"wf.workflow.create_minimal_draft_workspace",
|
"wf.workflow.create_minimal_draft_workspace",
|
||||||
"wf.workflow.create_artifact_from_workspace",
|
"wf.workflow.create_artifact_from_workspace",
|
||||||
|
|||||||
@@ -411,6 +411,14 @@ class WorkflowSurfaceHandlers:
|
|||||||
include_draft=include_draft,
|
include_draft=include_draft,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def delete_draft_workspace(self, *, workspace_id: str) -> dict[str, Any]:
|
||||||
|
deleted = self._draft_store().delete_workspace(workspace_id)
|
||||||
|
return {
|
||||||
|
"workspace_id": workspace_id,
|
||||||
|
"deleted": deleted,
|
||||||
|
"status": "deleted" if deleted else "not_found",
|
||||||
|
}
|
||||||
|
|
||||||
async def patch_draft_workspace(
|
async def patch_draft_workspace(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
|
|||||||
@@ -115,6 +115,20 @@ class PatchDraftWorkspaceRequest(BaseModel):
|
|||||||
patch: JsonPatchOperations
|
patch: JsonPatchOperations
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteDraftWorkspaceRequest(BaseModel):
|
||||||
|
"""Typed MCP request payload for deleting one draft workspace."""
|
||||||
|
|
||||||
|
workspace_id: WorkspaceId
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteDraftWorkspaceResult(BaseModel):
|
||||||
|
"""Inspector-visible response for draft workspace cleanup."""
|
||||||
|
|
||||||
|
workspace_id: str = Field(description="Draft workspace id targeted for deletion.")
|
||||||
|
deleted: bool = Field(description="True when a stored workspace was removed.")
|
||||||
|
status: Literal["deleted", "not_found"] = Field(description="Cleanup result.")
|
||||||
|
|
||||||
|
|
||||||
class CreateMinimalDraftWorkspaceRequest(BaseModel):
|
class CreateMinimalDraftWorkspaceRequest(BaseModel):
|
||||||
"""Typed MCP request payload for bootstrapping one-capability drafts."""
|
"""Typed MCP request payload for bootstrapping one-capability drafts."""
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ from .models import (
|
|||||||
CreateArtifactFromWorkspaceRequest,
|
CreateArtifactFromWorkspaceRequest,
|
||||||
CreateDraftWorkspaceRequest,
|
CreateDraftWorkspaceRequest,
|
||||||
CreateMinimalDraftWorkspaceRequest,
|
CreateMinimalDraftWorkspaceRequest,
|
||||||
|
DeleteDraftWorkspaceRequest,
|
||||||
|
DeleteDraftWorkspaceResult,
|
||||||
DraftWorkspaceListResult,
|
DraftWorkspaceListResult,
|
||||||
DraftWorkspaceResult,
|
DraftWorkspaceResult,
|
||||||
PatchDraftWorkspaceRequest,
|
PatchDraftWorkspaceRequest,
|
||||||
@@ -240,6 +242,18 @@ def register_workflow_tools(server: FastMCP[Any], service: WfMcpService) -> None
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@server.tool(
|
||||||
|
name="wf.workflow.delete_draft_workspace",
|
||||||
|
title="Delete Draft Workspace",
|
||||||
|
description="Delete one mutable workflow draft workspace by id.",
|
||||||
|
)
|
||||||
|
async def delete_draft_workspace(
|
||||||
|
request: DeleteDraftWorkspaceRequest,
|
||||||
|
) -> DeleteDraftWorkspaceResult:
|
||||||
|
return DeleteDraftWorkspaceResult.model_validate(
|
||||||
|
await handlers.delete_draft_workspace(workspace_id=request.workspace_id)
|
||||||
|
)
|
||||||
|
|
||||||
@server.tool(
|
@server.tool(
|
||||||
name="wf.workflow.patch_draft_workspace",
|
name="wf.workflow.patch_draft_workspace",
|
||||||
title="Patch Draft Workspace",
|
title="Patch Draft Workspace",
|
||||||
|
|||||||
@@ -96,6 +96,28 @@ def test_file_draft_workspace_store_lists_workspaces(tmp_path) -> None:
|
|||||||
assert [workspace.id for workspace in store.list_workspaces()] == ["a", "b"]
|
assert [workspace.id for workspace in store.list_workspaces()] == ["a", "b"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_file_draft_workspace_store_deletes_workspace(tmp_path) -> None:
|
||||||
|
store = FileDraftWorkspaceStore(tmp_path)
|
||||||
|
store.save_workspace(
|
||||||
|
WorkflowDraftWorkspace(
|
||||||
|
id="echo_draft",
|
||||||
|
revision=1,
|
||||||
|
draft=_draft(),
|
||||||
|
status="valid",
|
||||||
|
diagnostics=[],
|
||||||
|
created_at_epoch_ms=100,
|
||||||
|
updated_at_epoch_ms=100,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
deleted = store.delete_workspace("echo_draft")
|
||||||
|
deleted_again = store.delete_workspace("echo_draft")
|
||||||
|
|
||||||
|
assert deleted is True
|
||||||
|
assert deleted_again is False
|
||||||
|
assert store.list_workspaces() == []
|
||||||
|
|
||||||
|
|
||||||
def test_file_draft_workspace_store_rejects_path_traversal_ids(tmp_path) -> None:
|
def test_file_draft_workspace_store_rejects_path_traversal_ids(tmp_path) -> None:
|
||||||
store = FileDraftWorkspaceStore(tmp_path)
|
store = FileDraftWorkspaceStore(tmp_path)
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ def test_server_exposes_upstream_admin_and_workflow_tools() -> None:
|
|||||||
assert "wf.workflow.list_draft_workspaces" in names
|
assert "wf.workflow.list_draft_workspaces" in names
|
||||||
assert "wf.workflow.create_draft_workspace" in names
|
assert "wf.workflow.create_draft_workspace" in names
|
||||||
assert "wf.workflow.get_draft_workspace" in names
|
assert "wf.workflow.get_draft_workspace" in names
|
||||||
|
assert "wf.workflow.delete_draft_workspace" in names
|
||||||
assert "wf.workflow.patch_draft_workspace" in names
|
assert "wf.workflow.patch_draft_workspace" in names
|
||||||
assert "wf.workflow.create_minimal_draft_workspace" in names
|
assert "wf.workflow.create_minimal_draft_workspace" in names
|
||||||
assert "wf.workflow.create_artifact_from_workspace" in names
|
assert "wf.workflow.create_artifact_from_workspace" in names
|
||||||
@@ -191,6 +192,7 @@ def test_server_search_mode_pins_stable_control_and_workflow_tools() -> None:
|
|||||||
assert "wf.workflow.list_draft_workspaces" in names
|
assert "wf.workflow.list_draft_workspaces" in names
|
||||||
assert "wf.workflow.create_draft_workspace" in names
|
assert "wf.workflow.create_draft_workspace" in names
|
||||||
assert "wf.workflow.get_draft_workspace" in names
|
assert "wf.workflow.get_draft_workspace" in names
|
||||||
|
assert "wf.workflow.delete_draft_workspace" in names
|
||||||
assert "wf.workflow.patch_draft_workspace" in names
|
assert "wf.workflow.patch_draft_workspace" in names
|
||||||
assert "wf.workflow.create_minimal_draft_workspace" in names
|
assert "wf.workflow.create_minimal_draft_workspace" in names
|
||||||
assert "wf.workflow.create_artifact_from_workspace" in names
|
assert "wf.workflow.create_artifact_from_workspace" in names
|
||||||
|
|||||||
@@ -407,6 +407,33 @@ def test_workflow_surface_lists_draft_workspaces() -> None:
|
|||||||
assert "draft" not in payload["workspaces"][0]
|
assert "draft" not in payload["workspaces"][0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_workflow_surface_deletes_draft_workspace() -> None:
|
||||||
|
artifact_store = FileWorkflowArtifactStore(
|
||||||
|
local_temp_root() / "surface_workspace_delete"
|
||||||
|
)
|
||||||
|
handlers = _handlers(artifact_store)
|
||||||
|
asyncio.run(
|
||||||
|
handlers.create_draft_workspace(
|
||||||
|
workspace_id="echo_draft",
|
||||||
|
draft=_echo_draft(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
deleted = asyncio.run(
|
||||||
|
handlers.delete_draft_workspace(workspace_id="echo_draft")
|
||||||
|
)
|
||||||
|
deleted_again = asyncio.run(
|
||||||
|
handlers.delete_draft_workspace(workspace_id="echo_draft")
|
||||||
|
)
|
||||||
|
listed = asyncio.run(handlers.list_draft_workspaces())
|
||||||
|
|
||||||
|
assert deleted["deleted"] is True
|
||||||
|
assert deleted["status"] == "deleted"
|
||||||
|
assert deleted_again["deleted"] is False
|
||||||
|
assert deleted_again["status"] == "not_found"
|
||||||
|
assert listed["workspaces"] == []
|
||||||
|
|
||||||
|
|
||||||
def test_workflow_surface_patches_draft_workspace_by_revision() -> None:
|
def test_workflow_surface_patches_draft_workspace_by_revision() -> None:
|
||||||
artifact_store = FileWorkflowArtifactStore(
|
artifact_store = FileWorkflowArtifactStore(
|
||||||
local_temp_root() / "surface_workspace_patch"
|
local_temp_root() / "surface_workspace_patch"
|
||||||
|
|||||||
Reference in New Issue
Block a user