Files
Fusion 8dd350286e 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
2026-05-22 19:06:45 +02:00

21 KiB

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

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

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)

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

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

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

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

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);