feat: implement tool workshop - comprehensive tool system enhancement

- Add Docker Compose and Dockerfile support for tool definitions
- Implement readiness probes with configurable command, timeout, interval
- Create ConfigFolder model for reusable file collections with project overrides
- Add rich tool config fields: port_override, start_command, working_directory, env vars, volumes
- Build unified Tool Workshop UI at /tool-workshop replacing /tool-configs and /tool-types
- Update instance creation to support dockerfile builds, config folder mounting, readiness probes
- Add 3 database migrations for tool_types, tool_configs, and new config_folders table
- Create docker_build.py and readiness_probe.py services
- Add config_folders API with CRUD and project override endpoints

Quality gates: frontend build passes, Python syntax valid, all phases complete

Addresses tool-workshop OpenSpec change
This commit is contained in:
Fusion
2026-05-22 19:06:45 +02:00
parent ae377baa74
commit 8dd350286e
28 changed files with 3328 additions and 79 deletions
+379
View File
@@ -0,0 +1,379 @@
## Context
The tool system currently supports:
- ToolTypes with compose templates and basic metadata
- ToolConfigs as simple key-value pairs (env vars or files)
- Instance creation via compose rendering
- Basic flat-list UI at `/tool-configs`
Users need a much richer system for defining, configuring, and running development tools.
## Goals / Non-Goals
**Goals:**
- Support both Docker Compose and Dockerfile for tool definitions
- Add readiness probes with configurable commands and timeouts
- Create reusable config file collections ("folders") mountable as volumes
- Add rich tool config fields (port, start_command, working_directory, volumes, env vars)
- Build a unified "Tool Workshop" UI for all tool management
- Support per-project overrides on config folders
- Maintain backward compatibility with existing built-in tool types
**Non-Goals:**
- Docker image registry management (assume local builds or public images)
- Real-time collaborative tool editing
- Tool marketplace/sharing between users
- Advanced orchestration (Kubernetes, Swarm)
- Config folder versioning/Git integration
## Architecture
### Data Model
```
┌─────────────────────────────────────────────────────────────────┐
│ TOOL WORKSHOP │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ ToolType │────▶│ ToolConfig │◀────│ ConfigFolder │ │
│ │ (Blueprint) │ │ (Settings) │ │ (Files) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ ToolInstance │ │
│ │ (Runtime + Volumes) │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
```
### ToolType Model
```python
class ToolType:
# Existing fields
name: str # unique identifier
display_name: str
description: str | None
category: str
interfaces: list[str] # ["web", "terminal"]
default_port: int
required_variables: list[str]
is_builtin: bool
# New fields
definition_type: str # "compose" | "dockerfile"
compose_template: str | None # YAML template (if definition_type == "compose")
dockerfile_template: str | None # Dockerfile content (if definition_type == "dockerfile")
build_context: dict | None # {"files": {"path": "content"}} for dockerfile builds
readiness_probe: dict | None # {"command": "...", "timeout": 30, "interval": 2}
```
**Decision**: Store both compose and dockerfile, use `definition_type` to determine which to use. This allows easy switching and migration.
### ToolConfig Model
```python
class ToolConfig:
# Existing fields
user_id: UUID
tool_type_id: UUID
project_id: UUID | None # null = global config
key: str
value: str
config_type: str # "env" | "file"
file_path: str | None
# New fields
port_override: int | None # Override tool type default port
start_command: str | None # Override container start command
working_directory: str | None # Working directory inside container
environment_variables: dict | None # JSON {"KEY": "value", ...}
volumes: list[dict] | None # JSON [{"source": "...", "target": "...", "type": "..."}]
```
**Decision**: Store env vars and volumes as JSONB for flexibility. Port as integer with validation.
### ConfigFolder Model (NEW)
```python
class ConfigFolder:
id: UUID
user_id: UUID
name: str # e.g., "my-dotfiles", "vscode-settings"
description: str | None
mount_path: str # Default mount path in container (e.g., "/home/user/.config")
files: dict # JSON {"relative/path": "content", ...}
project_overrides: dict | None # JSON {project_id: {"mount_path": "...", "files": {...}}}
is_active: bool # Quick toggle
created_at, updated_at
```
**Decision**: Files stored as JSONB with relative paths as keys. This is simple and sufficient for config files (not binary assets).
### Volume Mount Resolution
When creating an instance, volumes are resolved in this priority order:
```
1. ToolConfig.volumes (explicit per-config mounts)
2. ConfigFolder mounts (user's active config folders)
3. ToolType default volumes (from compose/dockerfile)
```
Config folder files are written to the instance directory under `volumes/<folder_name>/` and mounted from there.
### Readiness Probe System
```python
class ReadinessProbe:
command: str # e.g., "curl -f http://localhost:8080/health"
timeout: int # seconds (default: 30)
interval: int # seconds between checks (default: 2)
retries: int # max attempts (default: timeout/interval)
```
**Execution Flow**:
1. Start container
2. Wait for container to be running
3. Execute probe command inside container via `docker exec`
4. If success → mark instance as "running"
5. If timeout → mark instance as "failed" with probe output in logs
**Decision**: Probes run inside the container using `docker exec`. This works for both network-based probes (curl) and command-based probes (binary version checks).
### Instance Creation Flow
```
1. Generate instance ID and directory
2. Resolve ToolConfig (global + project-specific)
3. Write config files:
a. .env file (from env-type ToolConfigs)
b. Config files (from file-type ToolConfigs)
c. Config folder files (to volumes/<folder>/)
4. IF ToolType.definition_type == "dockerfile":
a. Write Dockerfile + build context files
b. Build image: docker build -t <instance_tag> .
c. Generate compose from template using built image
5. IF ToolType.definition_type == "compose":
a. Render compose template with variables
6. Write docker-compose.yml
7. docker compose up -d
8. Connect to backend network
9. IF readiness_probe defined:
a. Execute probe with timeout
b. Update status based on result
10. IF web interface:
a. Create Cloudflare tunnel
b. Update URL
```
## UI Design
### Tool Workshop Page (`/tool-workshop`)
```
┌─────────────────────────────────────────────────────────────────┐
│ Tool Workshop [+ New] │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────────────────────────────┐ │
│ │ │ │ │ │
│ │ MY TOOLS │ │ [Tool Type Builder] │ │
│ │ │ │ │ │
│ │ ▼ Code Editor│ │ Name: [____________] │ │
│ │ □ VS Code │ │ Type: (•) Compose ( ) Dockerfile │ │
│ │ □ Cursor │ │ │ │
│ │ │ │ [Compose Template / Dockerfile] │ │
│ │ ▼ AI Tools │ │ ┌────────────────────────────────┐ │ │
│ │ □ OpenCode │ │ │ version: '3.8' │ │ │
│ │ □ Continue │ │ │ services: │ │ │
│ │ │ │ │ app: │ │ │
│ │ CONFIGS │ │ │ image: ... │ │ │
│ │ │ │ │ ports: │ │ │
│ │ ▼ Global │ │ │ - "{{PORT}}:8080" │ │ │
│ │ □ dotfiles │ │ │ volumes: │ │ │
│ │ □ api-keys │ │ │ - ... │ │ │
│ │ │ │ └────────────────────────────────┘ │ │
│ │ ▼ Project X │ │ │ │
│ │ □ overrides│ │ Readiness Probe: │ │
│ │ │ │ Command: [curl -f localhost:8080] │ │
│ │ │ │ Timeout: [30] seconds │ │
│ │ │ │ │ │
│ │ │ │ [Save Tool Type] │ │
│ │ │ │ │ │
│ └──────────────┘ └──────────────────────────────────────┘ │
│ │
│ Tabs: [Tool Types] [Configs] [Config Folders] │
│ │
└─────────────────────────────────────────────────────────────────┘
```
**Navigation Structure**:
- Left sidebar: Hierarchical tree
- Tool Types (expandable, shows instances count)
- Config Folders (grouped by global/project)
- Right panel: Context-aware editor based on selection
- Tab bar: Switch between Tool Types / Configs / Config Folders views
### Config Editor
```
┌─────────────────────────────────────────────────────────────────┐
│ Edit Config: OpenCode API Keys │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Basic Settings │ Advanced Settings │
│ ────────────────────────────┼──────────────────────────────── │
│ Key: [OPENAI_API_KEY] │ Port Override: [_____] │
│ Value: [sk-... ] │ Start Command: [_____] │
│ Type: (•) Env ( ) File │ Working Dir: [/workspace] │
│ File Path: [__________] │ │
│ │ Environment Variables: │
│ │ ┌──────────────────────────┐ │
│ │ │ KEY │ VALUE │ │
│ │ │ OPENAI_KEY │ sk-... │ │
│ │ │ MODEL │ gpt-4 │ │
│ │ └──────────────────────────┘ │
│ │ │
│ │ Volume Mounts: │
│ │ ┌──────────────────────────┐ │
│ │ │ SOURCE │ TARGET │ │
│ │ │ dotfiles │ ~/.config │ │
│ │ │ vscode-set │ ~/.vscode │ │
│ │ └──────────────────────────┘ │
│ │ │
│ [Delete] [Cancel] [Save] │ │
│ │
└─────────────────────────────────────────────────────────────────┘
```
### Config Folder Manager
```
┌─────────────────────────────────────────────────────────────────┐
│ Config Folder: my-dotfiles │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Name: [my-dotfiles] │
│ Description: [My personal dotfiles] │
│ Default Mount Path: [/home/user] │
│ │
│ Files: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Path │ Size │ Actions │ │
│ │ .zshrc │ 2.1KB │ [Edit] [Delete] │ │
│ │ .gitconfig │ 412B │ [Edit] [Delete] │ │
│ │ .config/starship.toml │ 1.8KB │ [Edit] [Delete] │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ [+ Add File] │
│ │
│ Project Overrides: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Project │ Mount Path │ Files Override │ │
│ │ Project Alpha │ /home/dev │ [3 files] │ │
│ │ Project Beta │ /workspace │ [1 file] │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ [Add Override] [Delete Folder] [Save] │
│ │
└─────────────────────────────────────────────────────────────────┘
```
## Decisions
1. **Dockerfile vs Compose**: Support both. `definition_type` field determines which path to use. Compose is the default for backward compatibility.
2. **Config Folder Storage**: Store files as JSONB keyed by relative path. This avoids file system complexity and works well for text-based config files. Limit: 10MB per folder.
3. **Readiness Probe Execution**: Use `docker exec` to run commands inside the container. This is the most flexible approach (works for HTTP checks, binary checks, file checks).
4. **Volume Resolution Order**: Config-level volumes override config-folder volumes, which override tool-type defaults. Last-write-wins for conflicts.
5. **UI Organization**: Single page with three tabs (Tool Types, Configs, Config Folders) and a left sidebar for navigation. This consolidates the current `/tool-configs` and `/tool-types` pages.
6. **Project Overrides**: ConfigFolders support per-project overrides for mount_path and files. This allows project-specific customizations while keeping the base collection reusable.
## Risks / Trade-offs
- **Dockerfile build times**: Building images on-demand is slow. Mitigation: Document that users should use pre-built images in compose for faster startup; dockerfile is for custom tools.
- **Config folder size limits**: JSONB has practical limits. Mitigation: 10MB limit per folder, enforced in API.
- **Readiness probe complexity**: Commands might hang or fail in unexpected ways. Mitigation: Strict timeout, clear error messages, probe logs stored on instance.
- **Migration complexity**: Existing tool types need `definition_type` set to "compose". Mitigation: Database default, seed function update.
- **UI complexity**: Three tabs with different editors could feel overwhelming. Mitigation: Progressive disclosure (hide advanced fields, collapsible sections).
## API Endpoints
### Tool Types
- `GET /tool-types` - List all (existing)
- `POST /tool-types` - Create with new fields
- `PUT /tool-types/{id}` - Update with new fields
- `GET /tool-types/{id}/validate` - Validate compose/dockerfile syntax
### Tool Configs
- `GET /tool-configs` - List with new fields
- `POST /tool-configs` - Create with new fields
- `PUT /tool-configs/{id}` - Update with new fields
- `GET /tool-configs/defaults/{tool_type_id}` - Get suggested defaults
### Config Folders (NEW)
- `GET /config-folders` - List user's folders
- `POST /config-folders` - Create folder
- `PUT /config-folders/{id}` - Update folder (files, mount_path)
- `DELETE /config-folders/{id}` - Delete folder
- `POST /config-folders/{id}/overrides` - Add project override
- `PUT /config-folders/{id}/overrides/{project_id}` - Update override
- `DELETE /config-folders/{id}/overrides/{project_id}` - Remove override
## Database Schema
### Migration: tool_types
```sql
ALTER TABLE tool_types
ADD COLUMN definition_type VARCHAR(20) NOT NULL DEFAULT 'compose',
ADD COLUMN dockerfile_template TEXT,
ADD COLUMN build_context JSONB DEFAULT '{}',
ADD COLUMN readiness_probe JSONB;
-- Ensure consistency
ALTER TABLE tool_types
ADD CONSTRAINT chk_definition_type
CHECK (definition_type IN ('compose', 'dockerfile'));
```
### Migration: tool_configs
```sql
ALTER TABLE tool_configs
ADD COLUMN port_override INTEGER,
ADD COLUMN start_command TEXT,
ADD COLUMN working_directory TEXT,
ADD COLUMN environment_variables JSONB DEFAULT '{}',
ADD COLUMN volumes JSONB DEFAULT '[]';
ALTER TABLE tool_configs
ADD CONSTRAINT chk_port_range
CHECK (port_override IS NULL OR (port_override >= 1 AND port_override <= 65535));
```
### New Table: config_folders
```sql
CREATE TABLE config_folders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
description TEXT,
mount_path VARCHAR(1024) NOT NULL,
files JSONB NOT NULL DEFAULT '{}',
project_overrides JSONB DEFAULT '{}',
is_active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(user_id, name)
);
CREATE INDEX idx_config_folders_user ON config_folders(user_id);
```