# SDD Proposal: Declarative Tool Container Compiler

## Status
**Phase:** proposal  
**Date:** 2026-05-28  
**Owner:** el Gentleman  
**Based on:** Exploration `streamline-tool-container-definitions`

---

## User Story

As a platform operator, I want to define a tool container by specifying:
- A **base image** (e.g. `ubuntu:24.04` or a pre-built `headquarter/base:dev-ubuntu`)
- A **list of packages** to install (apt, npm, pip, etc.)
- **Setup scripts** that run at build-time or container-startup
- **Mount policies** that automatically fix permissions for the runtime user

I do **not** want to write Dockerfiles or Compose files by hand.

The system should compile these declarations into Dockerfiles and Compose files automatically, while remaining fully compatible with the existing ConfigProfile mount system.

---

## Core Concept: The Tool Definition Manifest

Replace the monolithic `dockerfile_template` and `compose_template` strings with a single structured **Tool Definition Manifest**.

```yaml
# Tool Definition Manifest (stored as JSON in DB)
name: pi-agent
display_name: "Pi Agent"
description: "Terminal-based coding harness"
category: development
interface_type: terminal  # web | terminal

# ── Base Image ───────────────────────────────────────────────
base_image: ubuntu:24.04
# OR reference a pre-built base definition:
# base_definition_id: "base-ubuntu-24.04-dev"

# ── Packages ─────────────────────────────────────────────────
packages:
  apt:
    - curl
    - wget
    - git
    - neovim
    - ranger
    - tmux
    - htop
    - tree
    - jq
    - ca-certificates
    - python3
    - python3-pip
    - build-essential
  node:
    version: "20"           # triggers nodesource setup
  npm_global:
    - "@earendil-works/pi-coding-agent"
  # pip:
  #   - requests
  #   - httpx

# ── Runtime User ─────────────────────────────────────────────
user:
  name: user
  uid: 1001
  gid: 1001
  create_home: true
  shell: /bin/bash

# ── Environment ──────────────────────────────────────────────
env:
  DEBIAN_FRONTEND: noninteractive

# ── Setup Scripts ────────────────────────────────────────────
scripts:
  # build: runs during `docker build` → becomes RUN commands
  build:
    - |
      git config --global init.defaultBranch main
      git config --global user.email "dev@headquarter.local"
      git config --global user.name "Developer"
    - |
      mkdir -p /home/user/.config/ranger
      echo 'set preview_files true' > /home/user/.config/ranger/rc.conf

  # startup: runs when container starts → becomes entrypoint script
  startup:
    - |
      # Ensure workspace is owned by runtime user
      if [ -d /workspace ]; then
        sudo chown -R user:user /workspace 2>/dev/null || true
      fi

# ── Mount Schema ─────────────────────────────────────────────
mounts:
  - name: workspace
    target: /workspace
    source_type: repo        # resolved from repository path at instance creation
    writable: true
    owner: user              # post-start: chown -R user:user /workspace

  - name: ssh_keys
    target: /home/user/.ssh
    source_type: ssh_key     # resolved from repository's SSH key
    mode: "0700"             # post-start: chmod 0700 /home/user/.ssh
    file_mode: "0600"        # post-start: chmod 0600 files inside
    readonly: true

  - name: pi_state
    target: /tmp/.pi/agents
    source_type: instance    # resolved to {instance_dir}/mounts/tmp_.pi_agents
    writable: true

  - name: pi_config
    target: /home/user/.pi
    source_type: git_mount   # resolved from config profile git_mounts
    git_mount_ref: dotfiles   # references a named git mount in the config profile
    writable: true
    owner: user

# ── Runtime ──────────────────────────────────────────────────
runtime:
  command: ["/bin/bash"]
  stdin_open: true
  tty: true
  working_dir: /workspace
  # ports are auto-derived from interface_type:
  #   web: expose default_port
  #   terminal: no port mapping
```

---

## How It Compiles

### 1. Dockerfile Generation

The manifest compiler transforms the spec into a Dockerfile:

```dockerfile
# Generated Dockerfile — do not edit manually
FROM ubuntu:24.04

ENV DEBIAN_FRONTEND=noninteractive

# ── System Packages ──
RUN apt-get update && apt-get install -y \
    curl wget git neovim ranger tmux htop tree jq \
    ca-certificates python3 python3-pip build-essential \
    && rm -rf /var/lib/apt/lists/*

# ── Node.js ──
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
    && apt-get install -y nodejs \
    && rm -rf /var/lib/apt/lists/*

# ── NPM Packages ──
RUN npm install -g --ignore-scripts @earendil-works/pi-coding-agent

# ── Runtime User ──
RUN groupadd -g 1001 user && \
    useradd -u 1001 -g 1001 -m -s /bin/bash user

# ── Build Scripts ──
RUN git config --global init.defaultBranch main && \
    git config --global user.email "dev@headquarter.local" && \
    git config --global user.name "Developer"

RUN mkdir -p /home/user/.config/ranger && \
    echo 'set preview_files true' > /home/user/.config/ranger/rc.conf

# ── Environment ──
ENV DEBIAN_FRONTEND=noninteractive

# ── Setup Directories ──
RUN mkdir -p /workspace /tmp/.pi/agents /home/user/.pi && \
    chown -R user:user /workspace /tmp/.pi /home/user/.pi

# ── Entrypoint for Startup Scripts ──
COPY .headquarter/entrypoint.sh /usr/local/bin/headquarter-entrypoint
RUN chmod +x /usr/local/bin/headquarter-entrypoint

USER user
WORKDIR /home/user

ENTRYPOINT ["/usr/local/bin/headquarter-entrypoint"]
CMD ["/bin/bash"]
```

The generated `entrypoint.sh`:

```bash
#!/bin/bash
set -e

# Run startup scripts
echo "Ensure workspace is owned by runtime user"
if [ -d /workspace ]; then
  sudo chown -R user:user /workspace 2>/dev/null || true
fi

# Pass through to the main command
exec "$@"
```

### 2. Compose File Generation

The manifest compiler also generates the Compose file:

```yaml
services:
  app:
    image: ${IMAGE_TAG}
    container_name: ${INSTANCE_NAME}
    stdin_open: true
    tty: true
    working_dir: /workspace
    user: "1001:1001"  # from manifest user.uid/gid
    volumes:
      - ${REPO_PATH}:/workspace
      - ${SSH_PATH}:/home/user/.ssh:ro
      - ${INSTANCE_DIR}/mounts/tmp_.pi_agents:/tmp/.pi/agents
      - ${GIT_MOUNT_dotfiles}:/home/user/.pi
    environment:
      DEBIAN_FRONTEND: noninteractive
    restart: unless-stopped
```

Variables are resolved at instance creation time:
- `${REPO_PATH}` → the repository working directory
- `${SSH_PATH}` → prepared SSH key directory
- `${INSTANCE_DIR}` → the instance working directory
- `${GIT_MOUNT_dotfiles}` → resolved from config profile git_mounts

### 3. Permission Fixer (Post-Start)

After `docker compose up`, the API iterates over the mount schema and applies permission policies:

```python
for mount in manifest.mounts:
    if mount.owner:
        docker_exec(f"chown -R {mount.owner}:{mount.owner} {mount.target}")
    if mount.mode:
        docker_exec(f"chmod {mount.mode} {mount.target}")
    if mount.file_mode:
        docker_exec(f"find {mount.target} -type f -exec chmod {mount.file_mode} {{}} +")
```

This is **systematic and configurable** — not hardcoded to `/workspace`.

---

## Config Profile Compatibility

The existing ConfigProfile system provides:
- `environment_variables` → merged into compose `environment`
- `files` → written to instance dir, mounted via `volumes`
- `mounts` → appended to compose `volumes`
- `git_mounts` → resolved to host paths, appended to compose `volumes`
- `hints.start_command` → overrides `runtime.command`
- `hints.working_directory` → overrides `runtime.working_dir`

With the manifest system, ConfigProfiles **extend** the default mount schema:

1. Tool manifest defines the **default mount schema** (workspace, ssh, state)
2. ConfigProfile can add **additional mounts** or **override runtime hints**
3. Both are merged at instance-start time into the final compose file

The merge precedence:
1. Tool manifest (defaults)
2. ToolConfig overrides (per-user per-tool settings)
3. ConfigProfile overrides (hierarchical, can inherit from parent)
4. User-provided start options (e.g. branch selection)

---

## Base Image Definitions

A base definition is itself a manifest with no `runtime` section:

```yaml
# Base Definition: "ubuntu-24.04-dev"
name: ubuntu-24.04-dev
description: "Ubuntu 24.04 with build tools"
base_image: ubuntu:24.04
packages:
  apt:
    - curl
    - wget
    - git
    - build-essential
    - ca-certificates
user:
  name: user
  uid: 1000
  gid: 1000
  create_home: true
```

A tool definition can reference it:

```yaml
base_definition_id: "ubuntu-24.04-dev"
packages:
  apt:
    - neovim
    - ranger
    - tmux
  node:
    version: "20"
```

The compiler **merges** the base definition with the tool-specific overrides:
- Packages are **unioned** (base apt + tool apt)
- Scripts are **appended** (base build scripts, then tool build scripts)
- User/env are **overridden** (tool wins)

This enables a family of tool types to share a common base.

---

## Database Schema (Proposed)

### New Table: `tool_definition_manifests`

| Column | Type | Description |
|--------|------|-------------|
| `id` | UUID | PK |
| `name` | str | Unique identifier |
| `display_name` | str | Human-readable |
| `description` | str | |
| `category` | str | development, data-science, etc. |
| `interface_type` | enum | web, terminal |
| `base_image` | str | e.g. `ubuntu:24.04` |
| `base_definition_id` | UUID? | FK to another manifest |
| `manifest` | JSONB | The full manifest JSON |
| `dockerfile_cache` | TEXT | Last generated Dockerfile (for inspection) |
| `created_at` | datetime | |
| `updated_at` | datetime | |

### Migration: `tool_types` table

Add a nullable `manifest_id` column to `tool_types`.

For backward compatibility:
- If `manifest_id` is set → use the new manifest system
- If `manifest_id` is null → fall back to `dockerfile_template` / `compose_template`

A data migration converts existing pi-agent to the new manifest format.

---

## API Changes

### New Endpoints

```
GET    /tool-definitions              → list all base/tool definitions
GET    /tool-definitions/{id}         → get a definition
POST   /tool-definitions              → create a new definition
PUT    /tool-definitions/{id}         → update a definition
DELETE /tool-definitions/{id}         → delete (if not in use)
POST   /tool-definitions/{id}/compile → preview generated Dockerfile + compose
```

### Modified Endpoints

```
POST   /tool-types                    → can now accept manifest_id instead of templates
GET    /tool-types/{id}               → includes manifest if available
```

### Frontend Changes

New UI page: **Tool Workshop**

- Base image selector (dropdown of existing bases or custom FROM)
- Package manager tabs (apt, npm, pip, etc.)
- Script editor (build vs startup)
- Mount schema designer (drag-drop or form)
- Live preview of generated Dockerfile
- Test build button (builds image and reports success/failure)

---

## Open Questions

1. **Should we support multi-stage builds?**
   - Pros: smaller images, separation of build deps from runtime
   - Cons: more complexity in the manifest schema

2. **Should base definitions be versioned?**
   - Pros: reproducible builds, safe updates
   - Cons: more DB complexity

3. **How do we handle binary build context files?**
   - Current: JSON text in DB
   - Option A: Store in filesystem, reference by path
   - Option B: Upload to object storage (S3/minio)

4. **Should generated images be cached/pushed to a registry?**
   - Currently: built locally per instance
   - Option: push to `headquarter/tools/{tool-name}:{hash}` for reuse

---

## Risks

| Risk | Severity | Mitigation |
|------|----------|------------|
| Migration complexity | Medium | Keep old fields nullable; gradual adoption |
| Build cache invalidation | Medium | Use deterministic Dockerfile generation; hash manifest for image tag |
| User confusion ("yet another abstraction") | Low | Provide live preview + "view generated Dockerfile" button |
| Scope creep into full CI/CD | High | Keep scope to container definition only; no pipeline/orchestration |
| Binary files in manifests | Medium | Limit build context to text; document workaround for binaries |

---

## Effort Estimate

| Phase | Files | Lines (est) | Complexity |
|-------|-------|-------------|------------|
| DB migration + models | 3 | 200 | Low |
| Manifest compiler (Dockerfile) | 2 | 400 | Medium |
| Manifest compiler (Compose) | 2 | 300 | Medium |
| Permission fixer refactor | 2 | 200 | Low |
| API endpoints | 3 | 400 | Medium |
| Frontend Tool Workshop | 8 | 1200 | High |
| Tests | 4 | 600 | Medium |
| **Total** | **24** | **~3300** | **High** |

**Review workload forecast:** ~3300 lines is well above the 400-line budget. This should be split into **chained PRs**:
1. Backend: manifest schema, compiler, API (PR 1)
2. Frontend: Tool Workshop UI (PR 2)
3. Migration + data conversion (PR 3)

---

## Next Recommended Phase

**Design** — Detail the manifest JSON schema, compiler internals, and migration plan.

Should I proceed to design?
