Pin images that exist; add pinned local builds for upstreams without one

11 of 15 pinned images did not exist in their registries. Published images
now use real release tags (bifrost v2.3.0, searxng 2026.10.16-0ce72dd2c,
docker-android emulator_14.0_v3.8.0-p1, devcontainers/base 2.2.3-bookworm).
Upstreams with no usable image get images/<service>/Dockerfile on a pinned
base, installing a checksum- or commit-verified upstream release, tagged
localhost/heretek/<service>:<version> with pull_policy: build. Stdio-only
servers are wrapped with a pinned mcp-proxy so every MCP service serves
streamable HTTP; compose labels declare each endpoint.

radare2-mcp now runs with -R (read-only); the previous -r enabled the
dangerous run_* tools.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_012AuVk4nEvBXaTjs4G6wdW4
This commit is contained in:
Claude committed 2026-10-16 19:29:57 +00:00
1 parent eb0ea2cbbd
commit 59d38921bb
22 files changed
+765 -43

No files matched your search

+14 -5
View File
@@ -118,7 +118,7 @@ No `VERDICT: PASS`, no merge. No exceptions.
├── tsconfig.json
├── docker-compose.yml # Root compose using `include:` for compose/core.yml + 4 specialist profiles
├── compose/
│ ├── core.yml # Profile `core`: maximhq/bifrost:v1.0.0 (:8080), agentgateway:v1.6.0 (:3000), nats:2.15.1-alpine (:4222)
│ ├── core.yml # Profile `core`: maximhq/bifrost:v2.3.0 (:8080), agentgateway:v1.6.0 (:3000), nats:2.15.1-alpine (:4222)
│ ├── bifrost.yaml # 3-Tier LLM Failover config (OpenCode Go -> OpenRouter -> Local Ollama/vLLM) + Session Affinity
│ ├── agentgateway.yaml # CEL RBAC rules & MCP target routing
│ ├── profile-appsec.yml # Profile `appsec`: bethington/ghidra-mcp-headless:v6.0.0, radareorg/radare2-mcp:1.8.8, morluto/rea:6.5.0, mdb-mcp:1.2.0
@@ -134,14 +134,17 @@ No `VERDICT: PASS`, no merge. No exceptions.
│ │ └── pixi.toml # Conda-forge/PyTorch 2.4/CUDA 12.4/CMake/Ninja/OpenCV deduplicated via rattler hardlinks
│ └── headless-wayland-desktop/
│ └── Dockerfile # Debian trixie-slim + headless Sway + at-spi2-core + ydotool + agent-sh/computer-use-linux:v0.9.4
├── quadlets/ # Systemd rootless Podman Quadlets (`StopWhenUnneeded=true` -> 0 MB idle RAM)
│ ├── heretek-android-kvm.container
│ ├── heretek-ghidra-mcp.container
│ └── heretek-blender-mcp.container
├── images/ # Pinned local builds for upstreams with no usable published image
│ └── <service>/Dockerfile # tagged localhost/heretek/<service>:<version> (see README "Images")
├── quadlets/ # Systemd rootless Podman Quadlets, socket-activated (0 MB idle RAM)
│ ├── heretek-android-kvm.container (+ -vnc-proxy / -adb-proxy .socket/.service)
│ ├── heretek-ghidra-mcp.container (+ -proxy.socket/.service)
│ └── heretek-blender-mcp.container (+ -proxy.socket/.service)
├── scripts/
│ ├── heretek-exec.sh # Sub-5ms local worktree wrapper: bwrap + landrun + mise exec (with /dev/kvm & SELinux support)
│ ├── heretek-box-ctl.sh # Operator CLI: `up <profile>`, `down <profile>`, `status`, `verify`, `exec <task-id> <cmd>`
│ ├── verify-stack.ts # Deterministic static contract verifier auditing all Compose/Devcontainer/Quadlet/Env files
│ ├── check-images.ts # `make images`: resolves every pinned remote image in its registry
│ └── accept/
│ ├── wave-1.sh
│ ├── wave-2.sh
@@ -156,6 +159,12 @@ No `VERDICT: PASS`, no merge. No exceptions.
## 2. EXACT MODULE SPECIFICATIONS
> **Image pins (2026-10 remediation):** several image tags below never existed
> in their registries. The current pins (real published tags, or pinned local
> builds `localhost/heretek/<service>:<version>` from `images/`) are listed in
> README "Images" and enforced by `make images`. radare2-mcp runs with `-R`
> (read-only); `-r` would enable its dangerous run_* tools.
### 2.1 Wave 1: Root Compose, `core.yml`, `bifrost.yaml`, Fail-Closed Static Verifier
1. **`docker-compose.yml` & `compose/core.yml`:**
+84 -7
View File
@@ -20,6 +20,58 @@ All host ports bind `127.0.0.1` loopback only. Every image is pinned to an
exact version tag. The root `docker-compose.yml` orchestrates the five
fragments via `include:`.
### Images
Published images are pinned to tags that exist in their registry
(`make images` checks this). Upstreams that publish no usable image are
built locally from `images/<service>/Dockerfile` and tagged
`localhost/heretek/<service>:<upstream version>`; compose builds them
(`pull_policy: build`) and never pulls those names. Each Dockerfile starts
from a pinned base image and installs a pinned upstream release, verified by
SHA-256/SHA-512 or by checking out the exact upstream commit.
| Service | Image | Source |
|---|---|---|
| `bifrost` | `ghcr.io/maximhq/bifrost:v2.3.0` | published |
| `agentgateway` | `ghcr.io/agentgateway/agentgateway:v1.6.0` | published |
| `nats` | `nats:2.15.1-alpine` | published |
| `android-kvm` | `budtmo/docker-android:emulator_14.0_v3.8.0-p1` | published |
| `searxng` | `searxng/searxng:2026.10.16-0ce72dd2c` | published |
| `crawl4ai` | `unclecode/crawl4ai:0.9.5` | published |
| `ghidra-mcp` | `localhost/heretek/ghidra-mcp:6.0.0` | bethington/ghidra-mcp v6.0.0 + Ghidra 12.1.2 |
| `radare2-mcp` | `localhost/heretek/radare2-mcp:1.9.0` | r2mcp 1.9.0 + radare2 6.2.4 release .debs |
| `rea-mcp` | `localhost/heretek/rea-mcp:6.5.0` | npm `[email protected]` |
| `mdb-mcp` | `localhost/heretek/mdb-mcp:0.2.0-5d03b25` | smadi0x86/mdb-mcp commit 5d03b25 (upstream has no tags) |
| `adb-mcp` | `localhost/heretek/adb-mcp:0.23.0` | binary from `ghcr.io/iksnerd/adb-mcp:0.23.0` |
| `maestro-mcp` | `localhost/heretek/maestro-mcp:2.11.0` | Maestro CLI cli-2.11.0 release |
| `godot-mcp` | `localhost/heretek/godot-mcp:3.1.0` | tugcantopaloglu/godot-mcp v3.1.0 + Godot 4.3-stable |
| `blender-mcp` | `localhost/heretek/blender-mcp:0.11.1` | sandraschi/blender-mcp v0.11.1 + Blender 4.2.3 LTS |
| `computer-use-linux` | `localhost/heretek/computer-use-linux:0.7.17` | agent-sh/computer-use-linux v0.7.17 release binary |
Stdio-only MCP servers are wrapped with a pinned `mcp-proxy` (0.13.0) so every
MCP service speaks streamable HTTP at `/mcp` on its container port (crawl4ai:
SSE at `/mcp/sse`). Compose labels (`heretek.mcp.target`, `.transport`,
`.path`) declare those endpoints.
```sh
make images # resolve every pinned remote image against Docker Hub / GHCR / MCR (network)
```
### Agent gateway
`compose/agentgateway.yaml` is an agentgateway v1.6.0 standalone config
(`agentgateway -f /etc/agentgateway.yaml`). It serves one route per MCP target
at `/mcp/<target>` (`ghidra`, `radare2`, `rea`, `mdb`, `adb`, `maestro`,
`godot`, `blender`, `crawl4ai`, `gui`), each holding exactly one target so
tool names are not prefixed, plus an OpenAI-compatible `/v1` route to Bifrost.
Every request needs `Authorization: Bearer <AGENTGATEWAY_ADMIN_KEY|AGENTGATEWAY_AGENT_KEY>`;
`mdb` and `gui` are admin-only. There is no `core` or `searxng` route yet:
no heretek-core MCP service exists here, and SearXNG is not an MCP server.
```sh
make validate-gateway AGENTGATEWAY=/path/to/agentgateway # real binary, --validate-only
```
## Quickstart
```sh
@@ -58,11 +110,21 @@ violation:
| `SELINUX_VOLUME_LABELS` | any bind mount missing an SELinux label (`:z`, `:Z`, `:ro,Z`) |
| `NO_AGPL_IN_DEV_OR_GAME` | AGPL-licensed or root-escape images |
| `ENV_PLACEHOLDER_LEAK` | a real `.env` still carrying template placeholder tokens |
| `SELINUX_CONFINED` | a quadlet that disables SELinux confinement (`SecurityLabelType=spc`, `SecurityLabelDisable=true`, privileged) |
| `QUADLET_SOCKET_ACTIVATION` | a quadlet container that can start on its own or lacks its loopback `.socket` + idle-exiting proxy `.service` |
`NO_FLOATING_TAGS` also covers local builds (a `build:` service must tag
`localhost/<name>:<version>`) and Dockerfile `FROM` / `COPY --from=` images.
Each rule has a failing fixture tree under `test/fixtures/verify/`
(`bun run scripts/verify-stack.ts --root <dir>`), exercised by
`test/verify-stack.test.ts`.
```sh
bun run scripts/verify-stack.ts # or: ./scripts/heretek-box-ctl.sh verify
make check # verifier + bun test
make accept # all three wave acceptance gates
make validate-compose # docker compose config for every profile
make validate-units # systemd-analyze verify on the quadlet sockets/proxies
```
## Devcontainers
@@ -81,18 +143,33 @@ and commit the bumped pins.
## Quadlets
Rootless Podman units for the heavy services, all with
`StopWhenUnneeded=true` so they sit at 0 MB idle RAM until socket-activated:
Rootless Podman units for the heavy services, socket-activated so they use
no memory until something connects:
| Unit | Image | Loopback ports |
- `<name>-proxy.socket` owns the loopback port (the same one compose uses)
and is the only unit you enable (`WantedBy=sockets.target`).
- `<name>-proxy.service` runs `systemd-socket-proxyd --exit-idle-time=15min`
and `Requires=`/`After=` the container unit, so the first connection starts
the container (`Notify=healthy` holds the proxy until it answers).
- `<name>.container` has `StopWhenUnneeded=true` and no `[Install]` section,
so it never starts at login and stops once its proxy exits idle.
| Container unit | Image | Activation sockets |
|---|---|---|
| `quadlets/heretek-android-kvm.container` | `budtmo/docker-android:emulator_14.0` (+ `/dev/kvm` device grant) | `127.0.0.1:6080`, `127.0.0.1:5555` |
| `quadlets/heretek-ghidra-mcp.container` | `bethington/ghidra-mcp-headless:v6.0.0` (token via `~/.config/heretek/ghidra-mcp.env`, mode 600) | `127.0.0.1:8089` |
| `quadlets/heretek-blender-mcp.container` | `ghcr.io/sandraschi/blender-mcp:v1.2.0` | `127.0.0.1:8781` |
| `heretek-android-kvm.container` | `budtmo/docker-android:emulator_14.0_v3.8.0-p1` (+ `AddDevice=/dev/kvm`) | `127.0.0.1:6080` (vnc), `127.0.0.1:5555` (adb) |
| `heretek-ghidra-mcp.container` | `localhost/heretek/ghidra-mcp:6.0.0` (token via `~/.config/heretek/ghidra-mcp.env`, mode 600) | `127.0.0.1:8089` |
| `heretek-blender-mcp.container` | `localhost/heretek/blender-mcp:0.11.1` | `127.0.0.1:8781` |
All containers keep the default `container_t` SELinux confinement. If an
enforcing host denies the emulator `/dev/kvm`, use
`setsebool -P container_use_devices=true` rather than disabling labels.
```sh
podman build -t localhost/heretek/ghidra-mcp:6.0.0 images/ghidra-mcp # local images first
cp quadlets/*.container ~/.config/containers/systemd/
cp quadlets/*.socket quadlets/*.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now heretek-blender-mcp.service
systemctl --user enable --now heretek-ghidra-mcp-proxy.socket
```
## heretek-exec and box-ctl exec
+28 -6
View File
@@ -8,9 +8,16 @@
#
# `android-kvm` passes through /dev/kvm via `devices:` (a device mapping,
# not a volume mount — no SELinux label rule applies to it).
#
# android-kvm pins the immutable release tag emulator_14.0_v3.8.0-p1 (the bare
# emulator_14.0 tag is re-pointed on every docker-android release). The two MCP
# bridges have no usable published HTTP image, so they are pinned local builds
# (images/<service>/Dockerfile) tagged localhost/heretek/<service>:<version>;
# `pull_policy: build` keeps compose from pulling the local names. Both speak
# MCP over streamable HTTP at /mcp (see the `heretek.mcp.*` labels).
services:
android-kvm:
image: budtmo/docker-android:emulator_14.0
image: budtmo/docker-android:emulator_14.0_v3.8.0-p1
user: "1000:1000"
restart: unless-stopped
profiles: ["android", "all"]
@@ -31,7 +38,10 @@ services:
retries: 3
adb-mcp:
image: ghcr.io/iksnerd/adb-mcp:v1.4.0
image: localhost/heretek/adb-mcp:0.23.0
build:
context: ../images/adb-mcp
pull_policy: build
user: "1000:1000"
restart: unless-stopped
profiles: ["android", "all"]
@@ -41,6 +51,10 @@ services:
environment:
ANDROID_HOST: android-kvm
ADB_PORT: "5555"
labels:
heretek.mcp.target: adb
heretek.mcp.transport: streamable-http
heretek.mcp.path: /mcp
ports:
- "127.0.0.1:8770:8770"
volumes:
@@ -49,23 +63,31 @@ services:
security_opt:
- "no-new-privileges:true"
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8770/healthz"]
test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8770/status', timeout=4)"]
interval: 30s
timeout: 5s
retries: 3
maestro-mcp:
image: ghcr.io/mobile-dev-inc/maestro:v1.39.8
image: localhost/heretek/maestro-mcp:2.11.0
build:
context: ../images/maestro-mcp
pull_policy: build
user: "1000:1000"
restart: unless-stopped
profiles: ["android", "all"]
depends_on:
android-kvm:
condition: service_started
command: ["maestro", "mcp"]
# The image entrypoint attaches adb to android-kvm, then serves
# `maestro mcp` over streamable HTTP via mcp-proxy.
environment:
ANDROID_HOST: android-kvm
ADB_PORT: "5555"
labels:
heretek.mcp.target: maestro
heretek.mcp.transport: streamable-http
heretek.mcp.path: /mcp
ports:
- "127.0.0.1:8771:8771"
volumes:
@@ -74,7 +96,7 @@ services:
security_opt:
- "no-new-privileges:true"
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8771/healthz"]
test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8771/status', timeout=4)"]
interval: 30s
timeout: 5s
retries: 3
+53 -10
View File
@@ -9,14 +9,32 @@
# NOTE: `mdb-mcp` carries the SOLE documented capability exception in this
# repo — a narrow SYS_PTRACE grant required for debugger attachment.
# It still drops all other capabilities and keeps no-new-privileges.
#
# None of these upstreams publishes a usable container image, so each service
# is a pinned local build (images/<service>/Dockerfile) tagged
# localhost/heretek/<service>:<upstream version>. `pull_policy: build` keeps
# compose from ever trying to pull the local name from a registry.
# Every service speaks MCP over streamable HTTP at /mcp on its container port;
# the `heretek.mcp.*` labels are what compose/agentgateway.yaml routes to
# (enforced by test/agentgateway-config.test.ts).
services:
ghidra-mcp:
image: bethington/ghidra-mcp-headless:v6.0.0
image: localhost/heretek/ghidra-mcp:6.0.0
build:
context: ../images/ghidra-mcp
pull_policy: build
user: "1000:1000"
restart: unless-stopped
profiles: ["appsec", "all"]
environment:
GHIDRA_MCP_AUTH_TOKEN: ${GHIDRA_MCP_AUTH_TOKEN:?GHIDRA_MCP_AUTH_TOKEN is mandatory}
# The bridge keeps DNS-rebinding protection on; allow the compose DNS
# name the agent gateway uses as its Host header, plus host loopback.
GHIDRA_MCP_ALLOWED_HOSTS: ghidra-mcp,localhost,127.0.0.1
labels:
heretek.mcp.target: ghidra
heretek.mcp.transport: streamable-http
heretek.mcp.path: /mcp
ports:
- "127.0.0.1:8089:8089"
volumes:
@@ -25,19 +43,29 @@ services:
security_opt:
- "no-new-privileges:true"
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8089/healthz"]
test: ["CMD", "bash", "-c", "exec 3<>/dev/tcp/127.0.0.1/8089"]
interval: 30s
timeout: 5s
retries: 3
start_period: 120s
radare2-mcp:
image: ghcr.io/radareorg/radare2-mcp:1.8.8
image: localhost/heretek/radare2-mcp:1.9.0
build:
context: ../images/radare2-mcp
pull_policy: build
user: "1000:1000"
restart: unless-stopped
profiles: ["appsec", "all"]
# Container-internal listen address (NOT a published host port — the
# published port below is loopback-only). `-r` forces read-only mode.
command: ["r2mcp", "-H", "0.0.0.0:8765", "-r"]
# published port below is loopback-only). `-R` exposes only the read-only
# tool set (`-r` would ENABLE the dangerous run_* tools); `-s /data`
# sandboxes file access to the data volume.
command: ["r2mcp", "-H", "0.0.0.0:8765", "-R", "-s", "/data"]
labels:
heretek.mcp.target: radare2
heretek.mcp.transport: streamable-http
heretek.mcp.path: /mcp
ports:
- "127.0.0.1:8765:8765"
volumes:
@@ -46,16 +74,23 @@ services:
security_opt:
- "no-new-privileges:true"
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8765/healthz"]
test: ["CMD", "bash", "-c", "exec 3<>/dev/tcp/127.0.0.1/8765"]
interval: 30s
timeout: 5s
retries: 3
rea-mcp:
image: ghcr.io/morluto/rea:6.5.0
image: localhost/heretek/rea-mcp:6.5.0
build:
context: ../images/rea-mcp
pull_policy: build
user: "1000:1000"
restart: unless-stopped
profiles: ["appsec", "all"]
labels:
heretek.mcp.target: rea
heretek.mcp.transport: streamable-http
heretek.mcp.path: /mcp
ports:
- "127.0.0.1:8766:8766"
volumes:
@@ -64,16 +99,24 @@ services:
security_opt:
- "no-new-privileges:true"
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8766/healthz"]
test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8766/status', timeout=4)"]
interval: 30s
timeout: 5s
retries: 3
mdb-mcp:
image: ghcr.io/smadi0x86/mdb-mcp:1.2.0
# Upstream has no release tag; the build pins commit 5d03b25 (version 0.2.0).
image: localhost/heretek/mdb-mcp:0.2.0-5d03b25
build:
context: ../images/mdb-mcp
pull_policy: build
user: "1000:1000"
restart: unless-stopped
profiles: ["appsec", "all"]
labels:
heretek.mcp.target: mdb
heretek.mcp.transport: streamable-http
heretek.mcp.path: /mcp
ports:
- "127.0.0.1:8767:8767"
volumes:
@@ -84,7 +127,7 @@ services:
security_opt:
- "no-new-privileges:true"
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8767/healthz"]
test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8767/status', timeout=4)"]
interval: 30s
timeout: 5s
retries: 3
+26 -7
View File
@@ -1,18 +1,30 @@
# compose/profile-game.yml — `game` on-demand domain profile (Wave 2)
#
# Headless Godot 4.3 + Blender FastMCP 3.2 MCP servers. Activated on demand via:
# Headless Godot 4.3 + Blender 4.2 LTS FastMCP servers. Activated on demand via:
# docker compose --profile game up
# (or `--profile all` for everything). Every service is hardened fail-closed:
# pinned image, 127.0.0.1 loopback ports only, cap_drop ALL,
# no-new-privileges, non-root user, zero socket mounts.
#
# Neither upstream publishes a container image, so both are pinned local
# builds (images/<service>/Dockerfile) tagged localhost/heretek/<service>:<version>;
# `pull_policy: build` keeps compose from pulling the local names. Both speak
# MCP over streamable HTTP at /mcp (see the `heretek.mcp.*` labels).
services:
godot-mcp:
image: ghcr.io/tugcantopaloglu/godot-mcp:v3.1.0
image: localhost/heretek/godot-mcp:3.1.0
build:
context: ../images/godot-mcp
pull_policy: build
user: "1000:1000"
restart: unless-stopped
profiles: ["game", "all"]
environment:
GODOT_HEADLESS: "1"
labels:
heretek.mcp.target: godot
heretek.mcp.transport: streamable-http
heretek.mcp.path: /mcp
ports:
- "127.0.0.1:8780:8780"
volumes:
@@ -21,20 +33,27 @@ services:
security_opt:
- "no-new-privileges:true"
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8780/healthz"]
test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8780/status', timeout=4)"]
interval: 30s
timeout: 5s
retries: 3
blender-mcp:
image: ghcr.io/sandraschi/blender-mcp:v1.2.0
image: localhost/heretek/blender-mcp:0.11.1
build:
context: ../images/blender-mcp
pull_policy: build
user: "1000:1000"
restart: unless-stopped
profiles: ["game", "all"]
# Headless Blender background mode serving the FastMCP 3.2 bridge.
command: ["blender", "--background"]
# The image serves the FastMCP server over streamable HTTP; it drives
# Blender 4.2.3 headless (`blender --background`) per operation.
environment:
BLENDER_HEADLESS: "1"
labels:
heretek.mcp.target: blender
heretek.mcp.transport: streamable-http
heretek.mcp.path: /mcp
ports:
- "127.0.0.1:8781:8781"
volumes:
@@ -43,7 +62,7 @@ services:
security_opt:
- "no-new-privileges:true"
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8781/healthz"]
test: ["CMD", "bash", "-c", "exec 3<>/dev/tcp/127.0.0.1/8781"]
interval: 30s
timeout: 5s
retries: 3
+21 -3
View File
@@ -7,9 +7,15 @@
# (or `--profile all` for everything). Every service is hardened fail-closed:
# pinned image, 127.0.0.1 loopback ports only, cap_drop ALL,
# no-new-privileges, non-root user, zero socket mounts.
#
# searxng and crawl4ai are published images pinned to exact release tags.
# computer-use-linux publishes no image, so it is a pinned local build
# (images/computer-use-linux/Dockerfile) tagged localhost/heretek/...:<version>;
# `pull_policy: build` keeps compose from pulling the local name.
# MCP endpoints are declared with `heretek.mcp.*` labels for the gateway.
services:
searxng:
image: searxng/searxng:2026.10.14
image: searxng/searxng:2026.10.16-0ce72dd2c
user: "1000:1000"
restart: unless-stopped
profiles: ["research", "all"]
@@ -35,6 +41,11 @@ services:
restart: unless-stopped
profiles: ["research", "all"]
shm_size: "1g"
# crawl4ai's server exposes its MCP tools over SSE at /mcp/sse.
labels:
heretek.mcp.target: crawl4ai
heretek.mcp.transport: sse
heretek.mcp.path: /mcp/sse
ports:
- "127.0.0.1:11235:11235"
volumes:
@@ -49,7 +60,10 @@ services:
retries: 3
computer-use-linux:
image: ghcr.io/agent-sh/computer-use-linux:v0.9.4
image: localhost/heretek/computer-use-linux:0.7.17
build:
context: ../images/computer-use-linux
pull_policy: build
user: "1000:1000"
restart: unless-stopped
profiles: ["research", "all"]
@@ -57,6 +71,10 @@ services:
environment:
WLR_BACKENDS: headless
SWAY_HEADLESS: "1"
labels:
heretek.mcp.target: gui
heretek.mcp.transport: streamable-http
heretek.mcp.path: /mcp
ports:
- "127.0.0.1:8790:8790"
volumes:
@@ -65,7 +83,7 @@ services:
security_opt:
- "no-new-privileges:true"
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8790/healthz"]
test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8790/status', timeout=4)"]
interval: 30s
timeout: 5s
retries: 3
@@ -1,19 +1,20 @@
# devcontainers/headless-wayland-desktop/Dockerfile — headless Sway + AT-SPI2 desktop (Wave 3)
#
# Debian trixie-slim base (pinned, never floating) with a headless Sway
# Debian trixie-slim base (pinned to a dated release, never floating) with a headless Sway
# compositor, AT-SPI2 accessibility bus, and ydotool for synthetic input.
# Pairs at runtime with the pinned computer-use-linux agent image noted below
# (see compose/profile-research.yml `computer-use-linux` service), which
# consumes this desktop over the headless Wayland socket.
#
# Runtime peer (pinned, Apache-2.0/MIT stack — no AGPL tooling):
# ghcr.io/agent-sh/computer-use-linux:v0.9.4
# Runtime peer (pinned local build of the MIT agent-sh/computer-use-linux
# v0.7.17 release — no AGPL tooling; see images/computer-use-linux/Dockerfile):
# localhost/heretek/computer-use-linux:0.7.17
#
# Security posture: non-root `agent` user (uid 1000), no container-socket
# mounts, no privileged capabilities required (KVM passthrough, where needed,
# is granted by the orchestrator via device mapping, not by this image).
FROM debian:trixie-slim
FROM debian:trixie-20261005-slim
ENV DEBIAN_FRONTEND=noninteractive \
WLR_BACKENDS=headless \
@@ -1,6 +1,6 @@
{
"name": "heretek-polyglot-dev",
"image": "mcr.microsoft.com/devcontainers/base:1.2.2-bookworm",
"image": "mcr.microsoft.com/devcontainers/base:2.2.3-bookworm",
"remoteUser": "agent",
"containerUser": "agent",
"capAdd": [],
+33
View File
@@ -0,0 +1,33 @@
# images/adb-mcp/Dockerfile — adb-mcp (MIT) with a streamable-HTTP front end
#
# The upstream image ghcr.io/iksnerd/adb-mcp:0.23.0 exists but speaks MCP over
# stdio only, so this build copies its pinned binary next to the Debian adb
# client and a pinned mcp-proxy. Compose tags the result
# localhost/heretek/adb-mcp:0.23.0; the agent gateway reaches it at
# http://adb-mcp:8770/mcp.
FROM ghcr.io/iksnerd/adb-mcp:0.23.0 AS upstream
FROM python:3.12.15-slim-trixie
ARG MCP_PROXY_VERSION=0.13.0
COPY --from=upstream /usr/local/bin/adb-mcp /usr/local/bin/adb-mcp
COPY start.sh /usr/local/bin/heretek-adb-mcp
RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends adb; \
rm -rf /var/lib/apt/lists/*; \
pip install --no-cache-dir "mcp-proxy==${MCP_PROXY_VERSION}"; \
chmod 0755 /usr/local/bin/heretek-adb-mcp; \
useradd -m -u 1000 -s /bin/bash mcp; \
mkdir -p /data; \
chown 1000:1000 /data
ENV HOME=/home/mcp
USER 1000:1000
WORKDIR /data
EXPOSE 8770
CMD ["heretek-adb-mcp"]
+8
View File
@@ -0,0 +1,8 @@
#!/bin/sh
# Attach the local adb server to the emulator, then serve adb-mcp over
# streamable HTTP. A failed connect is not fatal: the emulator may still be
# booting, and adb-mcp reports device errors per tool call.
set -eu
adb start-server
adb connect "${ANDROID_HOST:-android-kvm}:${ADB_PORT:-5555}" || true
exec mcp-proxy --host 0.0.0.0 --port "${MCP_PORT:-8770}" --pass-environment -- adb-mcp
+55
View File
@@ -0,0 +1,55 @@
# images/blender-mcp/Dockerfile — pinned local build of sandraschi/blender-mcp (MIT)
#
# Upstream publishes no container image, so this build installs the v0.11.1
# tagged source (commit verified after checkout) next to Blender 4.2.3 LTS —
# the Blender version upstream's own Dockerfile targets — whose tarball is
# verified against the SHA-256 list blender.org publishes for that release.
# Compose tags the result localhost/heretek/blender-mcp:0.11.1. blender-mcp
# serves streamable HTTP natively (FastMCP), at http://blender-mcp:8781/mcp.
FROM debian:trixie-20261005-slim AS src
ARG BLENDER_MCP_TAG=v0.11.1
ARG BLENDER_MCP_COMMIT=5a9e923f6ae68cfc34a221d31bd1fd5c8aff07f6
RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends ca-certificates git; \
git clone -q --depth 1 --branch "${BLENDER_MCP_TAG}" https://github.com/sandraschi/blender-mcp.git /src; \
test "$(git -C /src rev-parse HEAD)" = "${BLENDER_MCP_COMMIT}"; \
rm -rf /src/.git
FROM python:3.12.15-slim-trixie
ARG BLENDER_SERIES=4.2
ARG BLENDER_VERSION=4.2.3
COPY --from=src /src /opt/blender-mcp-src
RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends \
ca-certificates curl xz-utils \
libgl1 libglib2.0-0 libx11-6 libxi6 libxxf86vm1 libxfixes3 libxrender1 libxkbcommon0 libsm6 libice6; \
cd /tmp; \
curl -fsSLO "https://download.blender.org/release/Blender${BLENDER_SERIES}/blender-${BLENDER_VERSION}-linux-x64.tar.xz"; \
curl -fsSLO "https://download.blender.org/release/Blender${BLENDER_SERIES}/blender-${BLENDER_VERSION}.sha256"; \
grep " blender-${BLENDER_VERSION}-linux-x64.tar.xz\$" "blender-${BLENDER_VERSION}.sha256" | sha256sum -c -; \
tar -xJf "blender-${BLENDER_VERSION}-linux-x64.tar.xz"; \
mv "blender-${BLENDER_VERSION}-linux-x64" /opt/blender; \
rm -f /tmp/blender-*; \
apt-get purge -y curl xz-utils; \
apt-get autoremove -y; \
rm -rf /var/lib/apt/lists/*; \
pip install --no-cache-dir /opt/blender-mcp-src; \
useradd -m -u 1000 -s /bin/bash mcp; \
mkdir -p /data; \
chown 1000:1000 /data
ENV HOME=/home/mcp \
BLENDER_EXECUTABLE=/opt/blender/blender
USER 1000:1000
WORKDIR /data
EXPOSE 8781
CMD ["python", "-c", "from blender_mcp.server import main_http; main_http(host='0.0.0.0', port=8781)"]
+50
View File
@@ -0,0 +1,50 @@
# images/computer-use-linux/Dockerfile — pinned local build of agent-sh/computer-use-linux (MIT)
#
# Upstream publishes no container image, so this build installs the v0.7.17
# release binary (verified against the release's published SHA-256) on a
# pinned Debian trixie base with a headless Sway compositor and the AT-SPI2
# accessibility bus. Compose tags the result
# localhost/heretek/computer-use-linux:0.7.17. The stdio MCP server
# (`computer-use-linux mcp`) is wrapped with a pinned mcp-proxy so the agent
# gateway can reach it over streamable HTTP at http://computer-use-linux:8790/mcp.
FROM debian:trixie-20261005-slim
ARG CUL_VERSION=0.7.17
ARG CUL_SHA256=4b9dccfda2e1b593d7ce333f30f88f48dce14c2235efa4f3ef37468e6fbc10b5
ARG MCP_PROXY_VERSION=0.13.0
COPY start.sh /usr/local/bin/heretek-computer-use
COPY sway-config /etc/heretek/sway-config
RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends \
ca-certificates curl \
sway at-spi2-core dbus dbus-user-session wtype grim fonts-dejavu-core \
python3 python3-venv; \
curl -fsSL -o /usr/local/bin/computer-use-linux \
"https://github.com/agent-sh/computer-use-linux/releases/download/v${CUL_VERSION}/computer-use-linux-x86_64-unknown-linux-gnu"; \
echo "${CUL_SHA256} /usr/local/bin/computer-use-linux" | sha256sum -c -; \
chmod 0755 /usr/local/bin/computer-use-linux /usr/local/bin/heretek-computer-use; \
apt-get purge -y curl; \
apt-get autoremove -y; \
rm -rf /var/lib/apt/lists/*; \
python3 -m venv /opt/mcp-proxy; \
/opt/mcp-proxy/bin/pip install --no-cache-dir "mcp-proxy==${MCP_PROXY_VERSION}"; \
ln -s /opt/mcp-proxy/bin/mcp-proxy /usr/local/bin/mcp-proxy; \
useradd -m -u 1000 -s /bin/bash agent; \
mkdir -p /data; \
chown 1000:1000 /data
ENV HOME=/home/agent \
XDG_RUNTIME_DIR=/tmp/xdg-runtime \
WLR_BACKENDS=headless \
WLR_RENDERER=pixman \
WLR_LIBINPUT_NO_DEVICES=1 \
COMPUTER_USE_LINUX_INDICATOR=0
USER 1000:1000
WORKDIR /data
EXPOSE 8790
CMD ["heretek-computer-use"]
+27
View File
@@ -0,0 +1,27 @@
#!/bin/sh
# Start a private D-Bus session bus and a headless Sway compositor, wait for
# the Wayland socket, then serve `computer-use-linux mcp` over streamable HTTP.
set -eu
mkdir -p "$XDG_RUNTIME_DIR"
chmod 0700 "$XDG_RUNTIME_DIR"
DBUS_SESSION_BUS_ADDRESS="unix:path=$XDG_RUNTIME_DIR/bus"
export DBUS_SESSION_BUS_ADDRESS
dbus-daemon --session --address="$DBUS_SESSION_BUS_ADDRESS" --fork --nopidfile
sway --config /etc/heretek/sway-config &
tries=0
while [ ! -S "$XDG_RUNTIME_DIR/wayland-1" ] && [ ! -S "$XDG_RUNTIME_DIR/wayland-0" ]; do
tries=$((tries + 1))
if [ "$tries" -gt 100 ]; then
echo "heretek-computer-use: sway did not create a Wayland socket" >&2
exit 1
fi
sleep 0.1
done
if [ -S "$XDG_RUNTIME_DIR/wayland-1" ]; then WAYLAND_DISPLAY=wayland-1; else WAYLAND_DISPLAY=wayland-0; fi
export WAYLAND_DISPLAY
exec mcp-proxy --host 0.0.0.0 --port "${MCP_PORT:-8790}" --pass-environment -- computer-use-linux mcp
+8
View File
@@ -0,0 +1,8 @@
# Minimal headless Sway session for computer-use-linux (no input devices,
# software rendering, one virtual output).
output HEADLESS-1 resolution 1920x1080 position 0,0
default_border none
focus_follows_mouse no
# AT-SPI2 accessibility bus for the accessibility-tree tools.
exec /usr/libexec/at-spi-bus-launcher --launch-immediately
+106
View File
@@ -0,0 +1,106 @@
# images/ghidra-mcp/Dockerfile — pinned local build of bethington/ghidra-mcp (Apache-2.0)
#
# No image is published for ghidra-mcp v6.0.0, so this build follows the
# upstream docker/Dockerfile recipe from the v6.0.0 tag (commit verified after
# checkout) against the official Ghidra 12.1.2 release (pinned SHA-256; the
# Ghidra version upstream's pom.xml requires). Compose tags the result
# localhost/heretek/ghidra-mcp:6.0.0.
#
# The container runs two processes: the headless Ghidra REST server bound to
# 127.0.0.1:8080 inside the container only, and upstream's MCP bridge serving
# streamable HTTP on 0.0.0.0:8089/mcp for the agent gateway. Both enforce
# GHIDRA_MCP_AUTH_TOKEN.
FROM debian:trixie-20261005-slim AS src
ARG GHIDRA_MCP_TAG=v6.0.0
ARG GHIDRA_MCP_COMMIT=8cd2078e10b9ba28b188cb84ce5b9051a904b995
RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends ca-certificates git; \
git clone -q --depth 1 --branch "${GHIDRA_MCP_TAG}" https://github.com/bethington/ghidra-mcp.git /src; \
test "$(git -C /src rev-parse HEAD)" = "${GHIDRA_MCP_COMMIT}"; \
rm -rf /src/.git
FROM eclipse-temurin:21.0.12.1_1-jdk-noble AS ghidra
ARG GHIDRA_VERSION=12.1.2
ARG GHIDRA_DATE=20260605
ARG GHIDRA_SHA256=b62e81a0390618466c019c60d8c2f796ced2509c4c1aea4a37644a77272cf99d
RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends ca-certificates curl unzip; \
curl -fsSL -o /tmp/ghidra.zip \
"https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_${GHIDRA_VERSION}_build/ghidra_${GHIDRA_VERSION}_PUBLIC_${GHIDRA_DATE}.zip"; \
echo "${GHIDRA_SHA256} /tmp/ghidra.zip" | sha256sum -c -; \
unzip -q /tmp/ghidra.zip -d /opt; \
mv "/opt/ghidra_${GHIDRA_VERSION}_PUBLIC" /opt/ghidra; \
rm /tmp/ghidra.zip; \
jython="$(ls /opt/ghidra/Extensions/Ghidra/*_Jython.zip | head -n1)"; \
mkdir -p /opt/ghidra/Ghidra/Extensions; \
unzip -q "$jython" -d /opt/ghidra/Ghidra/Extensions/
FROM eclipse-temurin:21.0.12.1_1-jdk-noble AS build
ARG GHIDRA_VERSION=12.1.2
RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends maven; \
rm -rf /var/lib/apt/lists/*
COPY --from=ghidra /opt/ghidra /opt/ghidra
COPY --from=src /src/pom.xml /build/pom.xml
COPY --from=src /src/src /build/src
COPY --from=src /src/lib /build/lib
# Install the Ghidra framework JARs the plugin compiles against into the local
# Maven repository at the exact version pom.xml expects (same list as
# upstream's docker/Dockerfile).
RUN set -eux; \
for jar in \
Framework/Generic Framework/SoftwareModeling Framework/Project Framework/Docking \
Framework/Utility Framework/Gui Framework/FileSystem Framework/Help \
Features/Base Features/Decompiler Framework/DB Framework/Emulation \
Debug/Debugger-api Debug/Framework-TraceModeling Debug/Debugger-rmi-trace; do \
name="${jar##*/}"; \
mvn -q install:install-file -Dfile="/opt/ghidra/Ghidra/${jar}/lib/${name}.jar" \
-DgroupId=ghidra -DartifactId="${name}" -Dversion="${GHIDRA_VERSION}" -Dpackaging=jar; \
done; \
cd /build; \
mvn -q clean package -P headless -DskipTests
FROM eclipse-temurin:21.0.12.1_1-jdk-noble
COPY --from=ghidra /opt/ghidra /opt/ghidra
COPY --from=build /build/target/GhidraMCP-*.jar /app/GhidraMCP.jar
COPY --from=src /src/docker/entrypoint.sh /app/entrypoint.sh
COPY --from=src /src /opt/ghidra-mcp-src
COPY start.sh /usr/local/bin/heretek-ghidra-mcp
RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends python3 python3-venv; \
rm -rf /var/lib/apt/lists/*; \
sed -i 's/\r$//' /app/entrypoint.sh; \
chmod 0755 /app/entrypoint.sh /usr/local/bin/heretek-ghidra-mcp; \
python3 -m venv /opt/ghidra-mcp-bridge; \
/opt/ghidra-mcp-bridge/bin/pip install --no-cache-dir /opt/ghidra-mcp-src; \
rm -rf /opt/ghidra-mcp-src; \
ln -s /opt/ghidra-mcp-bridge/bin/bridge-mcp-ghidra /usr/local/bin/bridge-mcp-ghidra; \
mkdir -p /data /projects /home/ghidra; \
chown 1000:1000 /data /projects /home/ghidra
ENV GHIDRA_HOME=/opt/ghidra \
HOME=/home/ghidra \
JAVA_OPTS="-Xmx4g -XX:+UseG1GC" \
GHIDRA_MCP_BIND_ADDRESS=127.0.0.1 \
GHIDRA_MCP_PORT=8080 \
GHIDRA_MCP_URL=http://127.0.0.1:8080
USER 1000:1000
WORKDIR /app
EXPOSE 8089
CMD ["heretek-ghidra-mcp"]
+24
View File
@@ -0,0 +1,24 @@
#!/bin/sh
# Run the headless Ghidra REST server on the container-internal loopback
# (127.0.0.1:${GHIDRA_MCP_PORT}) and expose upstream's MCP bridge over
# streamable HTTP on ${MCP_PORT}/mcp. Fails closed without the auth token.
set -eu
if [ -z "${GHIDRA_MCP_AUTH_TOKEN:-}" ]; then
echo "heretek-ghidra-mcp: GHIDRA_MCP_AUTH_TOKEN is mandatory" >&2
exit 1
fi
/app/entrypoint.sh &
server_pid=$!
trap 'kill "$server_pid" 2>/dev/null || true' EXIT INT TERM
bridge-mcp-ghidra --transport streamable-http --mcp-host 0.0.0.0 --mcp-port "${MCP_PORT:-8089}" &
bridge_pid=$!
# Exit (and let the orchestrator restart us) as soon as either process dies.
while kill -0 "$server_pid" 2>/dev/null && kill -0 "$bridge_pid" 2>/dev/null; do
sleep 2
done
kill "$server_pid" "$bridge_pid" 2>/dev/null || true
exit 1
+60
View File
@@ -0,0 +1,60 @@
# images/godot-mcp/Dockerfile — pinned local build of tugcantopaloglu/godot-mcp (MIT)
#
# Upstream publishes no container image and its npm package lags the v3.1.0
# tag, so this build compiles the tagged source (commit verified after
# checkout) and pairs it with the official Godot 4.3-stable Linux binary
# (verified against the release's published SHA-512). Compose tags the result
# localhost/heretek/godot-mcp:3.1.0. The stdio MCP server is wrapped with a
# pinned mcp-proxy so the agent gateway can reach it over streamable HTTP at
# http://godot-mcp:8780/mcp.
FROM node:22.23.3-trixie-slim AS build
ARG GODOT_MCP_TAG=v3.1.0
ARG GODOT_MCP_COMMIT=fcbc29e03297900b9a1fcc6c7cbae116fe9d532a
RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends ca-certificates git; \
git clone -q --depth 1 --branch "${GODOT_MCP_TAG}" https://github.com/tugcantopaloglu/godot-mcp.git /src; \
test "$(git -C /src rev-parse HEAD)" = "${GODOT_MCP_COMMIT}"; \
cd /src; \
npm ci --ignore-scripts; \
npm run build; \
npm prune --omit=dev
FROM node:22.23.3-trixie-slim
ARG GODOT_VERSION=4.3-stable
ARG GODOT_SHA512=fd52bb4ba8acc30ca5accd1c566d470ad7282f891ccc0995dfafabcf92bcf76280ce182bf9d80ebd885f3ed2165d01e1fc3f2928436b15498dfbd98656c2a45a
ARG MCP_PROXY_VERSION=0.13.0
COPY --from=build /src/build /opt/godot-mcp/build
COPY --from=build /src/node_modules /opt/godot-mcp/node_modules
COPY --from=build /src/package.json /opt/godot-mcp/package.json
RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends ca-certificates curl fontconfig python3 python3-venv unzip; \
curl -fsSL -o /tmp/godot.zip "https://github.com/godotengine/godot/releases/download/${GODOT_VERSION}/Godot_v${GODOT_VERSION}_linux.x86_64.zip"; \
echo "${GODOT_SHA512} /tmp/godot.zip" | sha512sum -c -; \
unzip -q /tmp/godot.zip -d /opt/godot; \
mv "/opt/godot/Godot_v${GODOT_VERSION}_linux.x86_64" /opt/godot/godot; \
rm /tmp/godot.zip; \
apt-get purge -y curl unzip; \
apt-get autoremove -y; \
rm -rf /var/lib/apt/lists/*; \
python3 -m venv /opt/mcp-proxy; \
/opt/mcp-proxy/bin/pip install --no-cache-dir "mcp-proxy==${MCP_PROXY_VERSION}"; \
ln -s /opt/mcp-proxy/bin/mcp-proxy /usr/local/bin/mcp-proxy; \
mkdir -p /projects; \
chown 1000:1000 /projects
# The node base image already ships the non-root `node` user as uid/gid 1000.
ENV HOME=/home/node \
GODOT_PATH=/opt/godot/godot
USER 1000:1000
WORKDIR /projects
EXPOSE 8780
CMD ["mcp-proxy", "--host", "0.0.0.0", "--port", "8780", "--pass-environment", "--", "node", "/opt/godot-mcp/build/index.js"]
+40
View File
@@ -0,0 +1,40 @@
# images/maestro-mcp/Dockerfile — pinned local build of mobile-dev-inc/maestro (Apache-2.0)
#
# Maestro publishes no container image, so compose builds this one as
# localhost/heretek/maestro-mcp:2.11.0. It installs the cli-2.11.0 release
# archive (verified against a pinned SHA-256) on a pinned JRE and wraps the
# stdio `maestro mcp` server with a pinned mcp-proxy so the agent gateway can
# reach it over streamable HTTP at http://maestro-mcp:8771/mcp.
FROM eclipse-temurin:21.0.12.1_1-jre-noble
ARG MAESTRO_VERSION=2.11.0
ARG MAESTRO_SHA256=5384593cb4e7a106489e75a821d157dd43f4e438df6bc308b72e82c685e1283a
ARG MCP_PROXY_VERSION=0.13.0
COPY start.sh /usr/local/bin/heretek-maestro-mcp
RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends adb ca-certificates curl python3 python3-venv unzip; \
rm -rf /var/lib/apt/lists/*; \
curl -fsSL -o /tmp/maestro.zip "https://github.com/mobile-dev-inc/maestro/releases/download/cli-${MAESTRO_VERSION}/maestro.zip"; \
echo "${MAESTRO_SHA256} /tmp/maestro.zip" | sha256sum -c -; \
unzip -q /tmp/maestro.zip -d /opt; \
rm /tmp/maestro.zip; \
ln -s /opt/maestro/bin/maestro /usr/local/bin/maestro; \
python3 -m venv /opt/mcp-proxy; \
/opt/mcp-proxy/bin/pip install --no-cache-dir "mcp-proxy==${MCP_PROXY_VERSION}"; \
ln -s /opt/mcp-proxy/bin/mcp-proxy /usr/local/bin/mcp-proxy; \
chmod 0755 /usr/local/bin/heretek-maestro-mcp; \
if ! getent passwd 1000 >/dev/null; then useradd -m -u 1000 -s /bin/bash mcp; fi; \
mkdir -p /data /home/mcp; \
chown 1000:1000 /data /home/mcp
ENV HOME=/home/mcp \
MAESTRO_CLI_NO_ANALYTICS=1
USER 1000:1000
WORKDIR /data
EXPOSE 8771
CMD ["heretek-maestro-mcp"]
+8
View File
@@ -0,0 +1,8 @@
#!/bin/sh
# Attach the local adb server to the emulator, then serve `maestro mcp` over
# streamable HTTP. A failed connect is not fatal: the emulator may still be
# booting, and Maestro reports device errors per tool call.
set -eu
adb start-server
adb connect "${ANDROID_HOST:-android-kvm}:${ADB_PORT:-5555}" || true
exec mcp-proxy --host 0.0.0.0 --port "${MCP_PORT:-8771}" --pass-environment -- maestro mcp
+43
View File
@@ -0,0 +1,43 @@
# images/mdb-mcp/Dockerfile — pinned local build of smadi0x86/mdb-mcp (GPL-3.0)
#
# Upstream publishes no image, no PyPI package and no release tag, so this
# build pins an exact upstream commit (package version 0.2.0) and verifies it
# after checkout. Compose tags the result localhost/heretek/mdb-mcp:0.2.0-5d03b25.
# GDB is the debugger backend; the compose service grants only SYS_PTRACE.
# The stdio MCP server is wrapped with a pinned mcp-proxy so the agent gateway
# can reach it over streamable HTTP at http://mdb-mcp:8767/mcp.
FROM debian:trixie-20261005-slim AS src
ARG MDB_MCP_COMMIT=5d03b25632c572e3640fd1004e9507600ed431d3
RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends ca-certificates git; \
git init -q /src; \
cd /src; \
git fetch -q --depth 1 https://github.com/smadi0x86/mdb-mcp.git "${MDB_MCP_COMMIT}"; \
git checkout -q FETCH_HEAD; \
test "$(git rev-parse HEAD)" = "${MDB_MCP_COMMIT}"; \
rm -rf .git
FROM python:3.12.15-slim-trixie
ARG MCP_PROXY_VERSION=0.13.0
COPY --from=src /src /opt/mdb-mcp-src
RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends gdb; \
rm -rf /var/lib/apt/lists/*; \
pip install --no-cache-dir /opt/mdb-mcp-src "mcp-proxy==${MCP_PROXY_VERSION}"; \
useradd -m -u 1000 -s /bin/bash mcp; \
mkdir -p /data; \
chown 1000:1000 /data
USER 1000:1000
WORKDIR /data
EXPOSE 8767
CMD ["mcp-proxy", "--host", "0.0.0.0", "--port", "8767", "--pass-environment", "--", "mdb-mcp"]
+39
View File
@@ -0,0 +1,39 @@
# images/radare2-mcp/Dockerfile — pinned local build of radareorg/radare2-mcp
#
# No container image is published for radare2-mcp, so compose builds this one
# as localhost/heretek/radare2-mcp:1.9.0. It installs the upstream release
# Debian packages (MIT r2mcp 1.9.0, LGPL radare2 6.2.4 — the radare2 version
# the 1.9.0 release was built against), each verified against a pinned SHA-256.
# r2mcp serves MCP over HTTP natively (`-H`), so no stdio bridge is needed.
FROM debian:trixie-20261005-slim
ARG R2_VERSION=6.2.4
ARG R2_SHA256=7019eedc0e0e87d1f53d6b8f5fc62b898567679c5efdce7fbfc557c9e7655e90
ARG R2MCP_VERSION=1.9.0
ARG R2MCP_SHA256=d426861a726f7e9bc13a44cc3bbfd8553c51b33fc05f1ff0eda75b433222d9e3
RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends ca-certificates curl; \
cd /tmp; \
curl -fsSLO "https://github.com/radareorg/radare2/releases/download/${R2_VERSION}/radare2_${R2_VERSION}_amd64.deb"; \
curl -fsSLO "https://github.com/radareorg/radare2-mcp/releases/download/${R2MCP_VERSION}/r2mcp_${R2MCP_VERSION}_amd64.deb"; \
echo "${R2_SHA256} radare2_${R2_VERSION}_amd64.deb" | sha256sum -c -; \
echo "${R2MCP_SHA256} r2mcp_${R2MCP_VERSION}_amd64.deb" | sha256sum -c -; \
apt-get install -y --no-install-recommends "./radare2_${R2_VERSION}_amd64.deb" "./r2mcp_${R2MCP_VERSION}_amd64.deb"; \
apt-get purge -y curl; \
apt-get autoremove -y; \
rm -rf /var/lib/apt/lists/* /tmp/*.deb; \
useradd -m -u 1000 -s /bin/bash mcp; \
mkdir -p /data; \
chown 1000:1000 /data; \
r2mcp -v
USER 1000:1000
WORKDIR /data
EXPOSE 8765
# -R: read-only tool set (never -r, which enables the dangerous run_* tools);
# -s /data: sandbox file access to the data volume.
CMD ["r2mcp", "-H", "0.0.0.0:8765", "-R", "-s", "/data"]
+32
View File
@@ -0,0 +1,32 @@
# images/rea-mcp/Dockerfile — pinned local build of morluto/rea (MIT)
#
# No container image is published for REA, so compose builds this one as
# localhost/heretek/rea-mcp:6.5.0. It installs the pinned npm release
# [email protected] (the package behind the upstream rea-agents-6.5.0 tag) and
# wraps its stdio MCP server (`rea mcp`) with a pinned mcp-proxy so the agent
# gateway can reach it over streamable HTTP at http://rea-mcp:8766/mcp.
FROM node:22.23.3-trixie-slim
ARG REA_VERSION=6.5.0
ARG MCP_PROXY_VERSION=0.13.0
RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends python3 python3-venv ca-certificates; \
rm -rf /var/lib/apt/lists/*; \
npm install --global --omit=dev "rea-agents@${REA_VERSION}"; \
npm cache clean --force; \
python3 -m venv /opt/mcp-proxy; \
/opt/mcp-proxy/bin/pip install --no-cache-dir "mcp-proxy==${MCP_PROXY_VERSION}"; \
ln -s /opt/mcp-proxy/bin/mcp-proxy /usr/local/bin/mcp-proxy; \
mkdir -p /data; \
chown 1000:1000 /data
# The node base image already ships the non-root `node` user as uid/gid 1000.
ENV HOME=/home/node
USER 1000:1000
WORKDIR /data
EXPOSE 8766
CMD ["mcp-proxy", "--host", "0.0.0.0", "--port", "8766", "--pass-environment", "--", "rea", "mcp"]