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)
324 lines
12 KiB
Markdown
324 lines
12 KiB
Markdown
# 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
|