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:
+11
-18
@@ -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
|
||||
|
||||
|
||||
@@ -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: 1–65535. |
|
||||
| `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.
|
||||
Reference in New Issue
Block a user