| name | launcher-registry |
| description | Maintain the Launcher Project Registry — a FastMCP-based server that tracks development projects, ports, URLs, and tech stacks. Covers adding projects to the registry, extending MCP tools, managing the registry.json data store, and generating Docker Agent Blueprints for ephemeral sandboxed development. |
| version | 2.1.0 |
| author | Hermes Agent |
| license | MIT |
| platforms | ["linux"] |
| metadata | {"hermes":{"tags":["launcher","mcp","registry","fastmcp","port-management","docker-blueprints"],"related_skills":["github-pr-workflow","mcporter","native-mcp"]}} |
Launcher Project Registry
Version History
| Version | Date | Change |
|---|
| 1.0.0 | 2026-05-15 | Initial schema |
| 1.1.0 | 2026-05-19 | Added has_compose field; port-conflict resolution for Docker Compose projects in registry |
Registry Schema (v1.1.0)
{
"version": "1.1.0",
"updated_at": "2026-05-19T10:15:00Z",
"port_range": { "start": 9000, "end": 9999 },
"projects": [
{
"name": "Project Name",
"slug": "kebab-case-slug",
"summary": "One-line human description",
"path": "/absolute/path/to/project",
"url": "http://192.168.5.215:PORT" or "",
"github": "https://github.com/owner/repo" or "",
"ports": [3000] or [{ "host_port": 8082, "port": 80 }],
"tech_stack": ["node.js", "express"],
"dependencies": {},
"status": "active",
"migrated_from": "projects.json" or omitted,
"has_compose": true ← 1.1.0: true when docker-compose.yml exists in project dir
}
]
}
has_compose field rules (backwards-compatible):
- Set
true when the project directory contains a docker-compose.yml or compose.yaml
- Set
false or omit when no compose file is present
- Pre-existing entries without
has_compose must be read as "Docker status unknown" — never a blocking assumption
- Code consuming this field: use
if p.get("has_compose") is True (strict True), not truthy-checking on absent field
Ports field formats:
[3000] — plain integer = a public host port assigned to the project
[{host_port: 8082, port: 80}] — object mapping: host_port is the host-facing port, port is the container port
[] — no host ports assigned (internal services, databases, external-only stacks)
Registering Docker Compose projects with conflicting ports:
When multiple compose files share identical service/port rules (e.g. three WordPress suites all map web:80, db:3306, phpmyadmin:8080):
- Use
launcher_find_free_port_tool + launcher_get_used_ports before assigning any host port
- Scan both registry host ports (the
host_port field of all entries) and system listening ports (ss -tunl) for actual conflicts
- Internal-only DB services (Docker-namespace isolated, no
ports: section) get no entry in ports — leave as []
- Allocate disjoint port blocks for conflicting WordPress triples:
wp-super-all-in-one: WordPress at 8082:80, MySQL 3307:3306, phpMyAdmin 8083:80, has_compose: true
ss-suite: WordPress at 8084:80, phpMyAdmin 8085:80, DB internal, has_compose: true
ssp-suite: WordPress at 8086:80, phpMyAdmin 8087:80, DB internal, has_compose: true
ssadmin-suite: WordPress at 8088:80, phpMyAdmin 8089:80, DB internal, has_compose: true
- Record re-mapped host ports in the project
summary so agents find them without reading the compose file
Overview
The Launcher Project Registry (/var/www/html/launcher/) tracks development projects with their locations, port assignments, URLs, and tech stacks. It provides:
- An MCP server (port 8765) with 8 tools for querying and managing projects
- A Web Admin UI (port 8766) for browser-based registry management
- A port scanner for detecting in-use ports on the system
Architecture
launcher/
├── config.py # Shared I/O helpers + port discovery
├── registry.json # Main project data store
├── projects.json # Legacy import (migrated entries marked)
├── mcp-server/server.py # FastMCP server (port 8765)
├── web-ui/api.py # Flask REST + admin UI (port 8766)
├── scanner/scanner.py # Port scanning CLI (cron-compatible)
└── requirements.txt # Python deps (fastmcp, flask)
Docker Agent Blueprint System
The ~/.agent-blueprints/ directory stores declarative Docker-Compose sandbox definitions.
Blueprints do not modify the host project tree — they live in a parallel namespace
(~/.agent-blueprints/<slug>/) alongside the real code at ~/Documents/www/ or ~/projects/.
Directory Layout
~/.agent-blueprints/
├── _shared/
│ ├── .env.example # Env template (GITHUB_TOKEN, AGENT_MODE)
│ └── scripts/
│ ├── snapshot-repo.sh # Tier 2: rsync → /tmp/agent-snapshots/<slug>/
│ └── discover-repos.sh # Inventory scan: find all .git repos + remotes
├── <project-slug>/
│ ├── docker-compose.yml # Declarative Compose file
│ └── README.md # Self-destruct runbook for agent on-boarding
├── BLUEPRINT-INDEX.md # Central registry: ports / tier / status
└── PROJECT-INVENTORY.md # Full project map: every repo, stack, port, status
Nothing under /tmp/agent-snapshots/ and no Docker volumes created during agent work survive
a docker compose down -v. Tier 2 is the default — it copies the worktree, leaves the
original repo completely untouched, and purges the copy when done. Bluesprinted-only repos should have scripts/ and README.md present.
Port Assignment Strategy
Before assigning a new port, always run launcher_find_free_port_tool and cross-check launcher_get_used_ports.
| Range | Purpose |
|---|
| 3000–3999 | Node / Express services (preferred) |
| 4000–4999 | Next.js frontends / Next API |
| 5000–5999 | Reserved (Laragon / legacy) |
| 8000–8999 | Python / Django / FastAPI |
| 9000–9999 | Games, Three.js, miscellaneous |
Pre-assigned blueprints tracked in BLUEPRINT-INDEX.md. Add new assignments there whenever a new project gets a dedicated host port.
Isolation Tiers
Tier 1 — Pristine Clone: Set GITHUB_TOKEN in _shared/.env, AGENT_MODE=tier1, GIT_REPO_URL=<https or git URL>. Container clones fresh from GitHub on boot — host repo is never touched.
Tier 2 — Snapshot (default): _shared/scripts/snapshot-repo.sh runs rsync from the host path into /tmp/agent-snapshots/<slug>, excluding node_modules/, .venv/, __pycache__/, .git/. Container mounts the ephemeral snapshot. Host project is never mutated.
Tier 3 — Bind Mount (discouraged): mount host path directly. Only safe for projects with no local state risk (docs, static sites).
Scanner → Map → Verify → Create Workflow
Products: BLUEPRINT-INDEX.md + PROJECT-INVENTORY.md
Detail: references/project-inventory.md · references/compose-18-batch.md
-
Scanner & Map — discover + categorise in one pass:
...
-
Voice Log Protocol — if a project writes milestone reports to /tmp/antigravity_reports/
For each repo that has a GitHub remote, extract: slug, path, lang, port (from .env), owner,
blueprinted (yes/no), status (🟢 blueprinted / ⚠️ unprocessed / 🔵 external / 🟡 local-only).
Categorise by owner: SPhillips1337 repos versus external/open-source forks.
-
Priority Selection (next-6) — build a short candidate list:
- Start with drafted-but-unconfirmed blueprints (pending sign-off)
- Add highest-value unprocessed repos (large disk footprint, active venv, networked stack)
- Add local-only repos that already ship a
Dockerfile or docker-compose.yml
- Present a short
slug / path / stack / reason table; wait for user to confirm or swap items
-
Verify — never create compose files on first pass.
Show port source for every assignment (.env value, launcher registry, or fresh find_free_port_tool result).
Wait for explicit approval before writing any file.
-
Create — only after user signs off:
- Confirm remaining free ports with
launcher_find_free_port_tool(start=9000, end=9999)
mkdir -p ~/.agent-blueprints/<slug>/scripts
- Pick the right compose template:
python-single (Django/FastAPI), node-single (Express/API),
phaser (Phaser game), next-phaser-split (Next + Node + Phaser combo)
- Assign host port from
BLUEPRINT-INDEX.md or allocate fresh
- Write
<slug>/docker-compose.yml
— never use the host project path as a volume source; always /tmp/agent-snapshots/<slug>
or a fresh ephemeral volume
- Write
<slug>/README.md with self-destruct instructions (boot → work → commit → down -v)
- Append entry to
BLUEPRINT-INDEX.md
- For local-only projects (no GitHub remote), set
AGENT_MODE=tier2 and LOCAL_REPO_PATH to the snapshot
-
Voice Log Protocol — if a project writes milestone reports to /tmp/antigravity_reports/
(e.g. glass-vibes-dashboard), annotate in its README.md that the TTS announce system
listens on http://192.168.5.215:3000 and add a write example:
echo "# Task complete" > /tmp/antigravity_reports/YYYY-MM-DD_HHMM_Summary.md
Compose Template Patterns
CRITICAL — YAML shell command escaping:
Docker Compose's command: >- (folded scalar) feeds a YAML-parsed string to the image's shell.
Double-quoting the shell block inside YAML (sh -c "...") requires escaping every $ and
every inner " with backslashes — fragile and easy to break at scale.
Always use single-quote sh -c '...' instead. Inside single quotes the shell does NOT
expand $VARS — that is intentional; Docker injects env vars before exec runs. To echo
diagnostics and keep readability, use plain $VAR without backslash escapes.
command: >
sh -c '
echo "AGENT_MODE=$AGENT_MODE"
if [ "$AGENT_MODE" = "tier1" ] && [ -n "$GIT_REPO_URL" ]; then
git clone --depth 1 "$GIT_REPO_URL" /workspace/repo 2>/dev/null
fi
cd /workspace/repo
[ -f package.json ] || { echo "No code mounted." >&2; exit 1; }
[ -d node_modules ] || npm ci --prefer-offline
exec node server/index.js
'
Python-Django (single service + Postgres + Redis):
services:
db:
image: postgres:15-alpine
environment:
POSTGRES_DB: ${DB_NAME:-<slug>_db}
redis:
image: redis:7-alpine
<slug>:
build:
context: ${LOCAL_REPO_PATH:-/tmp/agent-snapshots/<slug>}
dockerfile: Dockerfile
environment:
DB_HOST: db; REDIS_HOST: redis; GITHUB_TOKEN: ${GITHUB_TOKEN:}
ports: ["<host_port>:<container_port>"]
volumes: [${LOCAL_REPO_PATH:-/tmp/agent-snapshots/<slug>}:/app, agent_work:/app]
Node single service:
services:
<slug>:
image: node:22-bookworm-slim
environment:
NODE_ENV: development; GITHUB_TOKEN: ${GITHUB_TOKEN:}
ports: ["<host_port>:<container_port>"]
volumes: [${LOCAL_REPO_PATH:-/tmp/agent-snapshots/<slug>}:/workspace/repo, agent_work:/workspace/repo]
command: >
sh -c '
if [ "$AGENT_MODE" = "tier1" ] && [ -n "$GIT_REPO_URL" ]; then
git clone --depth 1 "$GIT_REPO_URL" /workspace/repo 2>/dev/null
fi
cd /workspace/repo
[ -f package.json ] || { echo "No code mounted." >&2; exit 1; }
[ -d node_modules ] || npm ci --prefer-offline
exec node server/index.js
'
if [ "$AGENT_MODE" = "tier1" ] && [ -n "$GIT_REPO_URL" ]; then
git clone --depth 1 "$GIT_REPO_URL" /workspace/repo 2>/dev/null
fi
cd /workspace/repo
[ -f package.json ] || { echo "No code mounted." >&2; exit 1; }
[ -d node_modules ] || npm ci --prefer-offline
exec node server/index.js
'
### Runtime Conventions
- **Entry command asymmetry**: headless services that wait for agent interaction must end with `tail -f /dev/null` to keep the container running in background.
- **`restart: unless-stopped`** — survives daemon restarts; tears down explicitly via `docker compose down -v`
- **Secrets**: host `.env` file is passed to the container via `env_file:`; never hardcode live credentials in blueprint YAML
### Common Snapshot Issues
- **`node_modules` bloat in snapshot**: always exclude via rsync `--exclude node_modules`; `snapshot-repo.sh` enforces this.
- **Large virtualenv (`myenv/`, `venv/`) bloat**: always exclude via `--exclude=myenv --exclude=venv`; snapshots with 300+ MB venvs should timeout and fall back to staged copy (see CMD-009).
- **Large binary assets in repo**: if snapshot exceeds 300 MB, consider Tier 1 fresh clone or add a `.find /data -name \"*.glb\" -delete` shim into `snapshot-repo.sh`.
- **Permission errors on mounted volume**: Docker runs as root by default; add `user: ${UID:-1000}:${GID:-1000}` under a root mount to pass through filesystem ownership.
- **Nested-dir after rsync (`<slug>/<slug>/`)**: `snapshot-repo.sh` must detect this and `mv` the inner tree up one level (see CMD-008). Do not rely on flat structure staying flat — always normalise post-rsync.
### Safest Prune Order
1. `docker compose down` in any stacks that persist DB data across restarts
2. `docker system prune -a` — removes dangling images + stopped containers
3. `docker volume prune` (only after step 1 — ensure no active compose needs that data)
### Compose Pitfalls (session-hardened)
| # | Pitfall | Fix |
|---|---|---|
| CMD-001 | `sh -c "…"` with escaped `\"` and `\\$` inside YAML breaks silently | Use `sh -c '…'` single-quote block; `$VAR` is not expanded inside single-quotes — Docker injects env before `exec` |
| CMD-002 | `{{PROJECT_SLUG}}` (mustache) in `.env.example` — Docker only expands `${VAR}` | Never put non-`$` placeholders in `env_file:` targets; put `${VAR}` defaults inline in each compose |
| CMD-003 | Two volumes both target `/workspace/repo` — Docker silently drops one mount | Split: bind mount → code dir; named volume → `.agent-state` dir (different container path) |
| CMD-004 | HTTPS server healthprobed over `http://` → permanent `unhealthy` | Use `CMD-SHELL` + `NODE_TLS_REJECT_UNAUTHORIZED=0` (Node) or `curl -sk` (Alpine); probe the correct protocol |
| CMD-005 | `start_period < 60 s` when npm/pip install runs on first boot | Set `start_period: 60s` minimum for Node/Python images |
| CMD-006 | `_shared/.env.example` containing `LOCAL_REPO_PATH=` overrides per-compose inline defaults | Remove `LOCAL_REPO_PATH` from shared env; each compose sets its own `${LOCAL_REPO_PATH:-/tmp/agent-snapshots/<slug>}` |
| CMD-007 | `docker compose config` drops port override `${EXTERNAL_PORT:-N}` when $EXTERNAL_PORT unset → `invalid hostPort` | For config validation: pass `EXTERNAL_PORT=<N>` in the command env. Keep the override in the committed file for runtime use. Confirmed in 18-batch A/B/C builds. |
---