# Spec: Tool Definition Manifest System ## Status **Phase:** spec **Date:** 2026-05-28 **Owner:** el Gentleman **Based on:** Proposal `streamline-tool-container-definitions` --- ## Requirements ### R1. Declarative Tool Definitions Users must be able to define a tool container without writing Dockerfiles or Compose files. The definition is a structured manifest specifying base image, packages, scripts, mounts, and runtime configuration. ### R2. Base Image Versioning Base definitions must be versioned. Tool definitions reference a specific base version. Updating a base creates a new version; existing tools remain pinned to their version until explicitly updated. ### R3. Package Managers The manifest must support multiple package managers: `apt`, `npm` (global), `pip`, and `node` (version installation). ### R4. Build vs Startup Scripts Scripts are categorized by execution phase: - **Build scripts**: Run during `docker build` (e.g., `git config`, config file setup) - **Startup scripts**: Run when the container starts (e.g., permission fixes, dynamic setup) ### R5. Mount Schema with Permission Policies Mounts declare: - `target`: Container path - `source_type`: How the source is resolved (`repo`, `ssh_key`, `instance`, `git_mount`, `host_path`) - `writable`: Whether the mount is read-write - `owner`: Container user to own the target path (post-start chown) - `mode`: Directory permissions (post-start chmod) - `file_mode`: File permissions inside the directory - `readonly`: Whether mounted read-only in compose ### R6. Config Profile Compatibility ConfigProfiles continue to add `env`, `files`, `mounts`, and `git_mounts` on top of the manifest defaults. The merge precedence is: manifest defaults → ToolConfig → ConfigProfile → user options. ### R7. Backward Compatibility Existing `dockerfile_template` and `compose_template` columns remain functional. New tool types use the manifest system; old types continue to work. A data migration converts the existing pi-agent to the new format. ### R8. Local Builds Images are built locally per instance using the standard `docker build` command. No registry integration in this phase. ### R9. Live Preview The API provides a `compile` endpoint that returns the generated Dockerfile and Compose file without building. ### R10. Deterministic Image Tags The image tag is derived from a hash of the manifest content, enabling build cache reuse when the manifest hasn't changed. --- ## Scenarios ### S1. Creating a New Tool Definition **Given** a user on the Tool Workshop page **When** they select base "ubuntu-24.04-dev:v1", add packages `[neovim, tmux]`, add a build script for git config, and define mounts for workspace + ssh **Then** the system generates a manifest, compiles a Dockerfile + Compose preview, and upon save stores the manifest in the database. ### S2. Building an Instance from a Manifest **Given** a tool instance created from a manifest-based tool type **When** `start_instance` is called **Then** the API compiles the manifest to a Dockerfile, builds the image, generates the Compose file with resolved mount paths, starts the container, and applies permission policies post-start. ### S3. Permission Fix on Non-Root Containers **Given** a manifest with `user: {name: user, uid: 1001}` and a mount `target: /workspace, owner: user` **When** the container starts with the workspace bind-mounted from host (root-owned) **Then** the post-start permission fixer runs `docker exec --user root chown -R user:user /workspace`, making the directory writable for the container user. ### S4. SSH Key Mount for Non-Root User **Given** a manifest with a mount `target: /home/user/.ssh, source_type: ssh_key, mode: "0700"` **When** the container starts **Then** SSH keys are mounted from the instance `.ssh` directory to `/home/user/.ssh`, and post-start fixes permissions to `0700` for the directory and `0600` for key files. ### S5. Config Profile Extends Manifest **Given** a manifest with default mount `workspace: /workspace` and a ConfigProfile that adds `git_mounts: [{remote_url: "...", target_path: "/home/user/.config"}]` **When** the instance starts with that profile selected **Then** the final Compose includes both the workspace mount and the config git mount, merged in the correct precedence. ### S6. Base Version Pinning **Given** a tool definition referencing `base_definition_id: "ubuntu-24.04-dev", base_version: "v1"` **When** the base definition is updated to "v2" **Then** the tool definition continues to use "v1" until explicitly updated. New tool definitions default to the latest version. ### S7. Deterministic Image Tag **Given** a manifest with specific packages and scripts **When** compiled **Then** the generated image tag is `headquarter/{tool-name}-{manifest-hash}:latest`, and rebuilding the same manifest reuses the cached image layer. ### S8. Live Preview Without Build **Given** a manifest being edited **When** the user clicks "Preview" **Then** the API returns the generated Dockerfile and Compose file within 500ms, without invoking Docker. --- ## Acceptance Criteria ### A1. Manifest Schema Validation - [ ] The manifest JSON must validate against a defined JSON Schema - [ ] Invalid manifests return 400 with detailed field-level errors - [ ] Missing required fields (name, base_image or base_definition_id) are rejected ### A2. Dockerfile Compilation - [ ] Generated Dockerfile builds successfully with `docker build` - [ ] Build scripts appear as `RUN` commands in order - [ ] Startup scripts appear in the generated entrypoint script - [ ] Packages are installed in a single layer per package manager - [ ] User creation uses the declared uid/gid ### A3. Compose Compilation - [ ] Generated Compose file starts successfully with `docker compose up` - [ ] Mounts are resolved from `source_type` to actual host paths - [ ] `stdin_open` and `tty` are set for terminal interface types - [ ] Ports are only included for web interface types ### A4. Permission Fixer - [ ] Post-start chown runs for all mounts with an `owner` declared - [ ] Post-start chmod runs for all mounts with `mode` or `file_mode` declared - [ ] Permission fixes complete within 5 seconds of container start - [ ] If the container has no `root` user, permission fixes are skipped with a warning ### A5. Config Profile Merge - [ ] ConfigProfile env vars override manifest defaults - [ ] ConfigProfile mounts are appended to manifest mounts - [ ] ConfigProfile git_mounts are resolved and appended - [ ] ToolConfig values override both manifest and ConfigProfile ### A6. Backward Compatibility - [ ] Existing tool types with `dockerfile_template` continue to work - [ ] Existing tool types with `compose_template` continue to work - [ ] The pi-agent tool type is migrated to the new manifest format - [ ] Old and new tool types can coexist in the same project ### A7. Base Versioning - [ ] Base definitions store a version string - [ ] Tool definitions store the base version they reference - [ ] Updating a base creates a new version; old versions remain accessible - [ ] The "latest" version can be referenced explicitly or by omission ### A8. Image Tag Determinism - [ ] Same manifest produces the same image tag - [ ] Changing any field (package, script, env) produces a different tag - [ ] The tag is lowercased and valid as a Docker image reference ### A9. API Endpoints - [ ] `POST /tool-definitions` creates a definition (201) - [ ] `GET /tool-definitions/{id}` returns the definition with compiled preview - [ ] `POST /tool-definitions/{id}/compile` returns Dockerfile + Compose (no build) - [ ] `PUT /tool-definitions/{id}` updates and re-validates ### A10. Frontend Tool Workshop - [ ] Users can create a tool definition via form (no raw JSON editing required) - [ ] Live preview shows generated Dockerfile and Compose - [ ] Package lists support add/remove/reorder - [ ] Mount schema supports add/remove with visual feedback - [ ] Base image selector shows available versions --- ## Non-Goals - **Multi-stage builds** — Out of scope for this phase. The compiler generates single-stage Dockerfiles. - **Docker registry integration** — Images are built locally per instance. - **Binary build context files** — Build context is limited to text files stored in the DB. - **Custom Dockerfile editing** — Users work exclusively through the manifest; no raw Dockerfile editing. - **Container orchestration beyond Compose** — No Kubernetes, Swarm, or other orchestrators. - **Real-time collaborative editing** — Tool Workshop is single-user editing. --- ## API Contract ### POST /tool-definitions **Request:** ```json { "name": "pi-agent", "display_name": "Pi Agent", "description": "Terminal coding harness", "category": "development", "interface_type": "terminal", "base_image": "ubuntu:24.04", "packages": { "apt": ["curl", "git", "neovim", "tmux"], "node": {"version": "20"}, "npm_global": ["@earendil-works/pi-coding-agent"] }, "user": { "name": "user", "uid": 1001, "gid": 1001, "create_home": true, "shell": "/bin/bash" }, "env": {"DEBIAN_FRONTEND": "noninteractive"}, "scripts": { "build": [ "git config --global init.defaultBranch main", "mkdir -p /home/user/.config/ranger" ], "startup": [ "if [ -d /workspace ]; then sudo chown -R user:user /workspace; fi" ] }, "mounts": [ { "name": "workspace", "target": "/workspace", "source_type": "repo", "writable": true, "owner": "user" }, { "name": "ssh", "target": "/home/user/.ssh", "source_type": "ssh_key", "mode": "0700", "file_mode": "0600", "readonly": true } ], "runtime": { "command": ["/bin/bash"], "stdin_open": true, "tty": true, "working_dir": "/workspace" } } ``` **Response (201):** ```json { "id": "d07b8376-2151-4119-8c1d-27f792aae9a3", "name": "pi-agent", "display_name": "Pi Agent", "manifest": { ... }, "dockerfile_preview": "FROM ubuntu:24.04\n...", "compose_preview": "services:\n app:\n image: ...", "created_at": "2026-05-28T10:00:00Z" } ``` ### POST /tool-definitions/{id}/compile **Response (200):** ```json { "dockerfile": "FROM ubuntu:24.04\n...", "compose": "services:\n app:\n...", "image_tag": "headquarter/pi-agent-a3f7c2d9:latest", "mounts_resolved": [ {"name": "workspace", "source": "/data/repos/headquarter", "target": "/workspace"} ] } ``` --- ## Database Schema ### `tool_definition_manifests` | Column | Type | Constraints | |--------|------|-------------| | `id` | UUID | PK | | `name` | VARCHAR(64) | UNIQUE, NOT NULL | | `display_name` | VARCHAR(128) | NOT NULL | | `description` | TEXT | | | `category` | VARCHAR(64) | | | `interface_type` | VARCHAR(16) | CHECK IN ('web', 'terminal') | | `base_image` | VARCHAR(256) | | | `base_definition_id` | UUID | FK → `tool_definition_manifests.id` | | `base_version` | VARCHAR(32) | DEFAULT 'latest' | | `manifest` | JSONB | NOT NULL | | `dockerfile_cache` | TEXT | | | `compose_cache` | TEXT | | | `version` | VARCHAR(32) | DEFAULT 'v1' | | `is_base` | BOOLEAN | DEFAULT FALSE | | `created_by_id` | UUID | FK → `users.id` | | `created_at` | TIMESTAMPTZ | DEFAULT now() | | `updated_at` | TIMESTAMPTZ | DEFAULT now() | **Check constraint:** Exactly one of `base_image` or `base_definition_id` must be set. ### Alter `tool_types` ```sql ALTER TABLE tool_types ADD COLUMN manifest_id UUID REFERENCES tool_definition_manifests(id), ADD COLUMN definition_type VARCHAR(16) DEFAULT 'legacy'; -- 'legacy' | 'manifest' ``` ### Alter `tool_instances` ```sql ALTER TABLE tool_instances ADD COLUMN manifest_compiled_at TIMESTAMPTZ, ADD COLUMN image_tag VARCHAR(256); ``` --- ## Related Files - `apps/api/src/models/tool_definition_manifest.py` — New model - `apps/api/src/models/tool_type.py` — Add manifest_id, definition_type - `apps/api/src/services/manifest_compiler.py` — New compiler - `apps/api/src/services/permission_fixer.py` — Refactored mount policy applier - `apps/api/src/api/tool_definitions.py` — New endpoints - `apps/api/src/api/tool_instances.py` — Modified start_instance flow - `apps/api/alembic/versions/20260528_add_tool_definition_manifests.py` — Migration