feat(FN-003): add tool manifest registry with FastAPI CRUD and built-in manifests

- Add ToolManifest Pydantic models with validators for ports, mounts, health checks, and traefik config

- Implement in-memory ToolRegistry with YAML loading and built-in manifest scanning

- Add FastAPI CRUD routes for listing, retrieving, and creating tool manifests

- Include built-in manifests for runfusion and code-server

- Harden web Dockerfile with unprivileged nginx and port 8080

- Add tool manifest specification documentation and architecture updates

Fusion-Task-Id: FN-003
This commit is contained in:
Fusion
2026-05-14 09:14:40 +02:00
parent 477abad4c9
commit f33a563003
25 changed files with 1038 additions and 24 deletions
+11 -18
View File
@@ -166,26 +166,19 @@ https://code-myapp-alice.tools.example.com
## 7. Tool Manifest & Orchestration
Tools are defined by manifests (FN-003) that declare:
Tools are defined by manifest files validated against the canonical Pydantic v2 schema.
The full schema reference, validation rules, and extension guide are documented in
[`docs/tool-manifest-spec.md`](./tool-manifest-spec.md).
- Runtime image / image tag
- Node/npm version expectations (for executable environments)
- Bootstrap / install commands
- Command execution needs
- Workspace mounts
- Config mounts
- Environment variables
- Secrets
- Ports
- Health checks
- Resource limits
- Traefik routing needs (subdomain pattern, middleware)
**Summary:** Each manifest declares the runtime image, ports, workspace/config mounts,
environment variables, secrets, health checks, resource limits, and Traefik routing rules.
The orchestration backend reads these manifests and generates Docker Compose service
definitions, Traefik labels, and runtime volume/secret bindings.
The platform reads manifests and generates:
- Docker Compose service definitions
- Traefik labels for routing
- Volume mounts for workspace and config
- Secret injection at runtime
Built-in manifests for RunFusion and code-server are shipped in
`apps/api/app/tools/manifests/` and loaded automatically on API startup.
New standard-container tools can be added by creating a YAML manifest—no backend code
changes are required.
## 8. MVP Phases
+209
View File
@@ -0,0 +1,209 @@
# Tool Manifest Specification
> Canonical schema reference for Headquarter's manifest-driven tool registry.
> Version: 1.0.0 — aligned with FN-003.
## Overview
Headquarter is a manifest-driven platform: every containerized tool (RunFusion, code-server, and future tools) is declared by a YAML manifest. The orchestration backend reads these manifests to generate Docker Compose services, Traefik routing labels, volume mounts, and resource constraints.
**Design goal:** Adding a new standard container tool requires only a YAML manifest—no backend code changes.
## Manifest File Format
Manifests are YAML files with a single top-level mapping. They are validated on load by Pydantic v2 models.
### Built-in location
Built-in manifests live in `apps/api/app/tools/manifests/*.yml` and are loaded automatically on API startup.
### Minimal valid manifest
```yaml
id: my-tool
name: My Tool
image: my-org/my-tool:latest
ports:
- container_port: 8080
primary: true
traefik:
enabled: false
```
## Field Reference
### `ToolManifest` (top level)
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `id` | `string` | yes | — | Lowercase slug with hyphens only (`^[a-z0-9\-]+$`). Used as the registry key. |
| `name` | `string` | yes | — | Human-readable tool name. |
| `description` | `string` | no | `""` | Short description of the tool. |
| `version` | `string` | no | `"1.0.0"` | Manifest version (semver-ish). |
| `image` | `string` | yes | — | Docker image reference. |
| `runtime_command` | `string[] \| null` | no | `null` | Override the container default command. |
| `runtime_entrypoint` | `string[] \| null` | no | `null` | Override the container entrypoint. |
| `runtime_user` | `string \| null` | no | `null` | User to run as inside the container. |
| `runtime_working_dir` | `string \| null` | no | `null` | Working directory inside the container. |
| `ports` | `PortConfig[]` | no | `[]` | Exposed ports. |
| `workspace_mounts` | `MountConfig[]` | no | `[]` | Workspace volume mounts (project-scoped). |
| `config_mounts` | `MountConfig[]` | no | `[]` | Config volume mounts (user or tool-scoped). |
| `env` | `dict<string, string>` | no | `{}` | Static environment variables. |
| `secrets` | `SecretRef[]` | no | `[]` | Secrets injected as environment variables. |
| `health_check` | `HealthCheckConfig \| null` | no | `null` | Health check definition. |
| `resource_limits` | `ResourceLimits \| null` | no | `null` | CPU and memory constraints. |
| `executable` | `ExecutableConfig \| null` | no | `null` | Node.js runtime metadata for executable environments. |
| `traefik` | `TraefikConfig \| null` | no | `null` | Traefik routing configuration. |
### `PortConfig`
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `container_port` | `int` | yes | — | Port inside the container. Range: 165535. |
| `protocol` | `"tcp" \| "udp"` | no | `"tcp"` | Transport protocol. |
| `name` | `string \| null` | no | `null` | Logical name, e.g. `"http"`, `"websocket"`. |
| `primary` | `bool` | no | `false` | The port used for default routing and health checks. |
### `MountConfig`
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `type` | `"volume" \| "bind"` | no | `"volume"` | Mount type. |
| `source_pattern` | `string` | yes | — | Template pattern resolved at spawn time, e.g. `"{project_repo}"`. |
| `target` | `string` | yes | — | Absolute path inside the container. Must start with `/`. |
| `read_only` | `bool` | no | `false` | Mount read-only. |
### `SecretRef`
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `name` | `string` | yes | — | Secret identifier in the secret store. |
| `env_var` | `string` | yes | — | Name of the environment variable injected into the container. |
| `required` | `bool` | no | `true` | Whether the tool fails to start if the secret is missing. |
### `HealthCheckConfig`
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `type` | `"http" \| "tcp" \| "command"` | no | `"http"` | Health check mechanism. |
| `path` | `string \| null` | no | `null` | HTTP path. Required when `type == "http"`. |
| `command` | `string[] \| null` | no | `null` | Command to execute. Required when `type == "command"`. |
| `port` | `int \| null` | no | `null` | Override port; defaults to the primary port if unset. |
| `interval_seconds` | `int` | no | `10` | Check interval. ≥ 1. |
| `timeout_seconds` | `int` | no | `5` | Check timeout. ≥ 1. |
| `retries` | `int` | no | `3` | Retries before marking unhealthy. ≥ 1. |
| `start_period_seconds` | `int` | no | `5` | Grace period before checks count. ≥ 0. |
### `ResourceLimits`
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `cpus` | `float \| null` | no | `null` | CPU limit. If set, ≥ 0.01. |
| `memory_mb` | `int \| null` | no | `null` | Memory limit in MiB. If set, ≥ 16. |
| `memory_swap_mb` | `int \| null` | no | `null` | Swap limit in MiB. `-1` disables swap limit. |
### `ExecutableConfig`
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `node_version` | `string \| null` | no | `null` | Expected Node.js version, e.g. `"22"`, `"lts"`. |
| `npm_version` | `string \| null` | no | `null` | Expected npm version. |
| `package_manager` | `"npm" \| "pnpm" \| "yarn" \| "bun"` | no | `"npm"` | Preferred package manager. |
| `bootstrap_commands` | `string[]` | no | `[]` | One-time setup commands run on first start. |
| `install_commands` | `string[]` | no | `[]` | Commands run before the main command. |
### `TraefikConfig`
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `enabled` | `bool` | no | `true` | Whether Traefik routing is generated for this tool. |
| `subdomain_prefix` | `string \| null` | no | `null` | Subdomain prefix. Defaults to the tool `id`. |
| `port` | `int \| null` | no | `null` | Container port to route traffic to. |
| `middlewares` | `string[]` | no | `[]` | Traefik middleware names to apply. |
| `strip_prefix` | `bool` | no | `false` | Strip path prefix before forwarding. |
| `entrypoint` | `string \| null` | no | `null` | Override the environment default Traefik entrypoint. |
| `cert_resolver` | `string \| null` | no | `null` | Override the environment default cert resolver. |
## Validation Rules
1. `id` must match `^[a-z0-9\-]+$` (lowercase, digits, hyphens only).
2. `MountConfig.target` must be an absolute path (`starts with "/"`).
3. When `health_check.type == "http"`, `path` must be set and non-empty.
4. When `health_check.type == "command"`, `command` must be set and non-empty.
5. `container_port` must be between 1 and 65535.
6. `cpus`, if set, must be ≥ 0.01.
7. `memory_mb`, if set, must be ≥ 16.
8. If `traefik.enabled` is `true`, at least one port must have `primary: true`.
## Example: RunFusion Manifest
```yaml
id: runfusion
name: RunFusion
description: Executable Node.js environment for running and developing applications.
version: "1.0.0"
image: node:22-slim
runtime_working_dir: /workspace
ports:
- container_port: 8080
protocol: tcp
name: http
primary: true
workspace_mounts:
- type: volume
source_pattern: "{project_repo}"
target: /workspace
read_only: false
config_mounts:
- type: volume
source_pattern: "{user_config}/runfusion"
target: /home/node/.config
read_only: false
env:
NODE_ENV: development
health_check:
type: http
path: /
port: 8080
interval_seconds: 10
timeout_seconds: 5
retries: 3
start_period_seconds: 10
resource_limits:
cpus: 2.0
memory_mb: 2048
memory_swap_mb: -1
executable:
node_version: "22"
package_manager: npm
bootstrap_commands: []
install_commands: []
traefik:
enabled: true
subdomain_prefix: runfusion
port: 8080
middlewares: []
strip_prefix: false
```
## Extension Guide: Adding a New Tool
To add a new standard container tool:
1. Create a new YAML file in `apps/api/app/tools/manifests/{tool-id}.yml`.
2. Populate all required fields (`id`, `name`, `image`, `ports`).
3. Set `traefik.enabled: true` and mark one port as `primary: true` if the tool needs HTTP routing.
4. Declare `workspace_mounts` and `config_mounts` as needed.
5. Restart the API (or call `registry.load_builtin_manifests()`).
No backend code changes are required for standard containers that expose an HTTP port and need volume mounts.
## Registry API
The in-memory registry exposes FastAPI routes under `/api/v1/tools`:
- `GET /api/v1/tools` — list all registered manifests.
- `GET /api/v1/tools/{id}` — retrieve a single manifest.
- `POST /api/v1/tools` — register a new manifest (returns 409 if `id` already exists).
Built-in manifests are loaded automatically on application startup via the FastAPI lifespan context manager.