5deee8c65c
- Add ToolDefinitionManifest model with base image versioning - Add manifest compiler: Dockerfile + Compose generation from JSON manifests - Add permission fixer: post-start chown/chmod for mount policies - Add tool definition CRUD API with live compile preview endpoint - Integrate manifest-based startup flow in start_instance - Add Alembic migration with data conversion for pi-agent - Add 48 unit tests for manifest compiler, permission fixer, docker service - Keep backward compatibility with legacy dockerfile_template/compose_template Migration: applied successfully. Pi-agent converted to manifest. Quality gates: pytest (146 passed, 4 pre-existing unrelated failures)
448 lines
14 KiB
Markdown
448 lines
14 KiB
Markdown
# 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?
|