feat: workspace backend integration (PR-2)

- Add workspace_id to CreateInstanceRequest (optional, replaces clone_mode)
- create_instance: resolve workspace, validate repo ownership, use workspace.path
- create_instance: store workspace_id on ToolInstance record
- start_instance: use workspace.path when workspace_id is set (manifest + legacy flows)
- Skip SSH key mount for clone mode when workspace is used
- Backward compatible: clone_mode still works when workspace_id is absent
This commit is contained in:
2026-05-31 23:10:45 +02:00
parent d567225bf7
commit 47b1af8e92
+49 -11
View File
@@ -427,6 +427,9 @@ class CreateInstanceRequest(BaseModel):
display_name: str | None = Field(
default=None, description="Optional display name for the instance"
)
workspace_id: str | None = Field(
default=None, description="UUID of workspace to mount (replaces clone_mode)"
)
clone_mode: str = Field(
default="mount", description="Repository access mode: 'mount' or 'clone'"
)
@@ -849,9 +852,34 @@ async def create_instance(
session, data.config_profile_id, user_id, project_id, tool_type_id
)
# Resolve workspace if provided
workspace = None
workspace_id = None
if data.workspace_id:
from src.models.workspace import Workspace as WorkspaceModel
try:
workspace_id = uuid.UUID(data.workspace_id)
except ValueError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid workspace_id format",
)
workspace = await session.get(WorkspaceModel, workspace_id)
if workspace is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="workspace not found",
)
if workspace.repo_id != repo_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="workspace does not belong to this repository",
)
try:
# Validate clone mode requirements
if data.clone_mode == "clone":
# Validate clone mode requirements (legacy path)
if data.clone_mode == "clone" and not workspace:
if not repo.remote_url:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
@@ -876,8 +904,10 @@ async def create_instance(
# Find free port
tool_port = find_free_port()
# Determine repo path based on clone mode
if data.clone_mode == "clone":
# Determine repo path based on workspace or clone mode
if workspace:
repo_path = workspace.path
elif data.clone_mode == "clone":
# Get SSH key for cloning
ssh_key = await session.get(SSHKey, repo.ssh_key_id)
if ssh_key is None:
@@ -1143,6 +1173,7 @@ services:
status="pending",
compose_path=compose_path,
port=tool_port,
workspace_id=workspace_id,
clone_mode=data.clone_mode,
branch=data.new_branch
if data.new_branch
@@ -1649,11 +1680,18 @@ async def start_instance(
if tool_type and tool_type.definition_type == "manifest" and tool_type.manifest_id:
logger.info("Using manifest-based startup for instance %s", instance.id)
# Determine repo path
repo = await session.get(GitRepository, instance.repository_id)
repo_path = repo.path if repo else ""
if instance.clone_mode == "clone":
repo_path = os.path.join(instance_dir, "repo-clone")
# Determine repo path (workspace takes precedence)
repo_path = ""
if instance.workspace_id:
from src.models.workspace import Workspace as WorkspaceModel
workspace = await session.get(WorkspaceModel, instance.workspace_id)
if workspace:
repo_path = workspace.path
else:
repo = await session.get(GitRepository, instance.repository_id)
repo_path = repo.path if repo else ""
if instance.clone_mode == "clone":
repo_path = os.path.join(instance_dir, "repo-clone")
try:
(
@@ -1686,8 +1724,8 @@ async def start_instance(
)
else:
# ── LEGACY FLOW ──────────────────────────────────────────
# Mount SSH key for clone-mode instances
if instance.clone_mode == "clone":
# Mount SSH key for clone-mode instances (skip for workspace-based)
if instance.clone_mode == "clone" and not instance.workspace_id:
repo = await session.get(GitRepository, instance.repository_id)
if repo and repo.ssh_key_id:
ssh_key = await session.get(SSHKey, repo.ssh_key_id)