diff --git a/.gitignore b/.gitignore index 0dc154a..c892b6c 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ __pycache__/ *.so .python-version .venv/ +.venv-test/ venv/ env/ .pytest_cache/ @@ -56,3 +57,4 @@ Thumbs.db .pi-lens/ minerv3/ .cache/ +openspec-audit-report.md diff --git a/apps/api/src/api/workspace/workspace_instances.py b/apps/api/src/api/workspace/workspace_instances.py index e865446..8f39393 100644 --- a/apps/api/src/api/workspace/workspace_instances.py +++ b/apps/api/src/api/workspace/workspace_instances.py @@ -2,13 +2,16 @@ import uuid -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from src.auth.dependencies import get_current_user_id, get_db_session +from src.models import GitRepository from src.models import ToolInstance from src.models import Workspace +from src.schemas.tool import CreateInstanceRequest, CreateWorkspaceInstanceRequest +from src.services.tool.instance_service import create_tool_instance router = APIRouter(prefix="/workspaces/{workspace_id}/instances") @@ -30,6 +33,63 @@ async def _get_workspace( return workspace +@router.post( + "/", + summary="Create instance from workspace", + description="Create a new tool instance mounted on this workspace.", + status_code=status.HTTP_201_CREATED, +) +async def create_workspace_instance( + workspace_id: uuid.UUID, + data: CreateWorkspaceInstanceRequest, + user_id: uuid.UUID = Depends(get_current_user_id), + session: AsyncSession = Depends(get_db_session), +) -> dict: + """Create a tool instance directly on a workspace.""" + workspace = await _get_workspace(session, workspace_id, user_id) + + repo = await session.get(GitRepository, workspace.repo_id) + if repo is None: + raise HTTPException(status_code=404, detail="Repository not found") + if repo.project_id is None: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Repository is not associated with a project", + ) + + request = CreateInstanceRequest( + tool_type_id=data.tool_type_id, + display_name=data.display_name, + workspace_id=str(workspace.id), + config_profile_id=data.config_profile_id, + ssh_key_ids=data.ssh_key_ids, + ) + + try: + instance = await create_tool_instance( + session, user_id, repo.project_id, repo.id, request + ) + except ValueError as exc: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) + except RuntimeError as exc: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(exc) + ) + + return { + "id": str(instance.id), + "name": instance.name, + "display_name": instance.display_name, + "tool_type_id": str(instance.tool_type_id), + "status": instance.status, + "workspace_id": str(instance.workspace_id) if instance.workspace_id else None, + "selected_config_profile_id": str(instance.selected_config_profile_id) + if instance.selected_config_profile_id + else None, + "created_at": instance.created_at.isoformat(), + } + + @router.get("/") async def list_workspace_instances( workspace_id: uuid.UUID, diff --git a/apps/api/src/schemas/tool/__init__.py b/apps/api/src/schemas/tool/__init__.py index 567fd1c..1322161 100644 --- a/apps/api/src/schemas/tool/__init__.py +++ b/apps/api/src/schemas/tool/__init__.py @@ -1,6 +1,10 @@ """Tool schemas module.""" -from src.schemas.tool.tool_instance import CreateInstanceRequest, StartInstanceRequest +from src.schemas.tool.tool_instance import ( + CreateInstanceRequest, + CreateWorkspaceInstanceRequest, + StartInstanceRequest, +) from src.schemas.tool.tool_type import ( ToolTypeCreate, ToolTypeResponse, @@ -10,6 +14,7 @@ from src.schemas.tool.tool_type import ( __all__ = [ "CreateInstanceRequest", + "CreateWorkspaceInstanceRequest", "StartInstanceRequest", "ToolTypeCreate", "ToolTypeResponse", diff --git a/apps/web/src/api/sessions.ts b/apps/web/src/api/sessions.ts index ed80ce9..fb1a25a 100644 --- a/apps/web/src/api/sessions.ts +++ b/apps/web/src/api/sessions.ts @@ -31,8 +31,6 @@ export interface Session { url: string | null; container_status?: string; probe_status?: string; - clone_mode?: string; - branch?: string | null; created_at?: string; } @@ -51,12 +49,9 @@ export async function createInstance( repoId: string, toolTypeId: string, displayName?: string, - cloneMode?: string, - branch?: string, - newBranch?: string, + workspaceId?: string, configProfileId?: string, sshKeyIds?: string[], - workspaceId?: string, ): Promise { const response = await apiClient.post( `/projects/${projectId}/repositories/${repoId}/instances`, @@ -64,9 +59,6 @@ export async function createInstance( tool_type_id: toolTypeId, display_name: displayName, workspace_id: workspaceId || undefined, - clone_mode: cloneMode || "mount", - branch: branch || undefined, - new_branch: newBranch || undefined, config_profile_id: configProfileId, ssh_key_ids: sshKeyIds || [], }, diff --git a/apps/web/src/components/features/session/create-session-form.tsx b/apps/web/src/components/features/session/create-session-form.tsx index 9fb1e78..e7b72f1 100644 --- a/apps/web/src/components/features/session/create-session-form.tsx +++ b/apps/web/src/components/features/session/create-session-form.tsx @@ -6,11 +6,7 @@ import { type ToolInstance, } from "../../../api/sessions"; import type { Project } from "../../../types"; -import { - listRepositoryBranches, - type GitRepository, - type Branch, -} from "../../../api/git-repositories"; +import type { GitRepository } from "../../../api/git-repositories"; import type { ToolType } from "../../../api/tool-types"; import { listSSHKeys, type SSHKey } from "../../../api/ssh-keys"; import { @@ -26,7 +22,6 @@ interface CreateSessionFormProps { fixedRepoId?: string; projectName?: string; repoName?: string; - showCloneMode?: boolean; showFixedFields?: boolean; onProjectChange?: (projectId: string) => void; onSuccess?: (instance: ToolInstance) => void; @@ -43,7 +38,6 @@ export const CreateSessionForm = ({ fixedRepoId, projectName, repoName, - showCloneMode = true, showFixedFields = true, onProjectChange, onSuccess, @@ -55,19 +49,11 @@ export const CreateSessionForm = ({ const [selectedRepo, setSelectedRepo] = useState(fixedRepoId || ""); const [selectedToolType, setSelectedToolType] = useState(""); const [displayName, setDisplayName] = useState(""); - const [cloneMode, setCloneMode] = useState<"mount" | "clone">("mount"); - const [branch, setBranch] = useState("main"); const [sshKeys, setSshKeys] = useState([]); const [configProfiles, setConfigProfiles] = useState([]); const [selectedConfigProfile, setSelectedConfigProfile] = useState(""); const [selectedSshKeyIds, setSelectedSshKeyIds] = useState([]); - const [branches, setBranches] = useState([]); - const [isLoadingBranches, setIsLoadingBranches] = useState(false); - const [isCreatingNewBranch, setIsCreatingNewBranch] = useState(false); - const [newBranchName, setNewBranchName] = useState(""); - const [baseBranch, setBaseBranch] = useState(""); - const [isSubmitting, setIsSubmitting] = useState(false); const [error, setError] = useState(null); @@ -108,31 +94,6 @@ export const CreateSessionForm = ({ void loadProfiles(); }, [selectedToolType, selectedProject, fixedProjectId]); - // Load branches when selected repo changes - useEffect(() => { - const projectId = fixedProjectId || selectedProject; - if (!selectedRepo || !projectId || !showCloneMode) { - setBranches([]); - return; - } - const loadBranches = async () => { - setIsLoadingBranches(true); - try { - const response = await listRepositoryBranches(projectId, selectedRepo); - setBranches(response.branches); - if (response.default_branch) { - setBranch(response.default_branch); - setBaseBranch(response.default_branch); - } - } catch { - // ignore - } finally { - setIsLoadingBranches(false); - } - }; - void loadBranches(); - }, [selectedRepo, selectedProject, fixedProjectId, showCloneMode]); - // Filter repositories by selected project const availableRepos = selectedProject ? repositories.filter((r) => r.project_id === selectedProject) @@ -143,12 +104,6 @@ export const CreateSessionForm = ({ if (!fixedRepoId) setSelectedRepo(""); setSelectedToolType(""); setDisplayName(""); - setCloneMode("mount"); - setBranch("main"); - setIsCreatingNewBranch(false); - setNewBranchName(""); - setBaseBranch(""); - setBranches([]); setSelectedSshKeyIds([]); setSelectedConfigProfile(""); }; @@ -165,14 +120,6 @@ export const CreateSessionForm = ({ return; } - if (showCloneMode && cloneMode === "clone") { - const repo = repositories.find((r) => r.id === repoId); - if (!repo?.ssh_key_id) { - setError("Repository must have an SSH key assigned for clone mode"); - return; - } - } - setIsSubmitting(true); try { @@ -181,15 +128,7 @@ export const CreateSessionForm = ({ repoId, selectedToolType, displayName || undefined, - showCloneMode ? cloneMode : undefined, - showCloneMode && cloneMode === "clone" - ? isCreatingNewBranch - ? baseBranch - : branch - : undefined, - showCloneMode && cloneMode === "clone" && isCreatingNewBranch - ? newBranchName - : undefined, + undefined, selectedConfigProfile || undefined, selectedSshKeyIds.length > 0 ? selectedSshKeyIds : undefined, ); @@ -240,8 +179,6 @@ export const CreateSessionForm = ({ setSelectedProject(value); setSelectedRepo(""); setSelectedToolType(""); - setCloneMode("mount"); - setIsCreatingNewBranch(false); onProjectChange?.(value); }} disabled={isSubmitting} @@ -277,8 +214,6 @@ export const CreateSessionForm = ({ onChange={(e) => { setSelectedRepo(e.target.value); setSelectedToolType(""); - setCloneMode("mount"); - setIsCreatingNewBranch(false); }} disabled={!hasProject || isSubmitting} > @@ -301,8 +236,6 @@ export const CreateSessionForm = ({ value={selectedToolType} onChange={(e) => { setSelectedToolType(e.target.value); - setCloneMode("mount"); - setIsCreatingNewBranch(false); }} disabled={!hasRepo || isSubmitting} > @@ -382,134 +315,6 @@ export const CreateSessionForm = ({ )} - {/* Clone Mode & Branch */} - {showCloneMode && hasToolType && ( -
- -
- - -
- - {cloneMode === "clone" && ( - <> - - - {isCreatingNewBranch && ( - <> - - - - )} - - {selectedRepo && ( -
- {(() => { - const repo = repositories.find( - (r) => r.id === selectedRepo, - ); - if (!repo) return null; - if (repo.ssh_key_id) { - const key = sshKeys.find( - (k) => k.id === repo.ssh_key_id, - ); - return ( - - SSH key: {key?.name || "Assigned"} - - ); - } - return ( - - No SSH key assigned to this repository. Clone mode - requires an SSH key. - - ); - })()} -
- )} - - )} -
- )} - {/* Display Name */} {hasToolType && (