From 2d5f8e119796fcd63ff02d03756464cc9f6ed7cd Mon Sep 17 00:00:00 2001 From: Hermes Bot Date: Tue, 23 Jun 2026 13:26:43 -0400 Subject: [PATCH 1/8] =?UTF-8?q?feat(mcp):=20add=20codebase-memory=20MCP=20?= =?UTF-8?q?=E2=80=94=20code=20knowledge=20graph=20for=20Hermes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gateway-spawned stdio MCP server wrapping the upstream DeusData/codebase-memory-mcp static binary (MIT; bundled offline embeddings, no API keys). Gives Hermes structural code navigation (search_graph, trace_path, get_architecture, get_code_snippet, ...) over the repos under CODE_ROOT, mounted read-only at /c/dev. Architecture: the binary is stdio-only, which is exactly how the docker/mcp-gateway already runs MCP servers (spawned as sibling containers over stdio), so this mirrors qdrant-rag-mcp rather than a long-lived HTTP service (the originally-assumed shape, which the binary does not support). - codebase-memory-mcp/: Dockerfile (pinned, sha256-verified portable static binary v0.8.1) + README. - docker-compose.yml: codebase-memory-mcp-image build service behind profile "codebase-memory" (opt-in; ~266MB binary); mcp-gateway gains CODE_ROOT + MCP_GATEWAY_DOCKER_BIND_ALLOWED_PATHS. - registry-custom.yaml: codebase-memory catalog entry — longLived, disableNetwork (100% local, anti-exfil), /c/dev:ro host bind + codebase-memory-cache named volume for the persistent SQLite index. - gateway-wrapper.sh: substitute PLACEHOLDER_CODE_ROOT from $CODE_ROOT. - .cbmignore: defense-in-depth secret/non-source excludes. - .env.example, CHANGELOG.md. Validated: image builds with checksum verification; binary speaks clean MCP stdio (no stdout pollution) under --network none; secret exclusion proven live (.cbmignore + .gitignore both keep secret content out of the index, positive control retrievable). Co-Authored-By: Claude Opus 4.8 (1M context) --- .cbmignore | 38 +++++++++++++++++++ .env.example | 4 ++ CHANGELOG.md | 11 ++++++ codebase-memory-mcp/Dockerfile | 49 ++++++++++++++++++++++++ codebase-memory-mcp/README.md | 64 ++++++++++++++++++++++++++++++++ docker-compose.yml | 19 ++++++++++ mcp/gateway/gateway-wrapper.sh | 3 +- mcp/gateway/registry-custom.yaml | 25 +++++++++++++ 8 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 .cbmignore create mode 100644 codebase-memory-mcp/Dockerfile create mode 100644 codebase-memory-mcp/README.md diff --git a/.cbmignore b/.cbmignore new file mode 100644 index 0000000..e992bdf --- /dev/null +++ b/.cbmignore @@ -0,0 +1,38 @@ +# codebase-memory-mcp ignore file (gitignore syntax). +# Defense-in-depth so the code indexer never ingests secrets or non-source bulk, +# on top of the hardcoded defaults (.git, node_modules) and the .gitignore layer. +# Keep this in sync with what must never be indexed; .gitignore already covers the +# real secrets, this is the explicit belt-and-suspenders list. + +# Secrets / credentials +.env +.env.* +!.env.example +*.sops +*.sops.* +secrets/ +**/secrets/ +*.pem +*.key +id_rsa +id_ed25519 +*.age + +# Runtime / operator state (gitignored, not source) +data/ +overrides/ + +# Large / generated / non-source +models/ +*.gguf +*.safetensors +*.bin +*.onnx +node_modules/ +.venv/ +venv/ +__pycache__/ +dist/ +build/ +*.stl +*.3mf diff --git a/.env.example b/.env.example index f5ca3e2..cf68209 100644 --- a/.env.example +++ b/.env.example @@ -26,6 +26,10 @@ BASE_PATH=. # Optional: separate data directory (default: BASE_PATH/data) # DATA_PATH=D:/ai-data +# Optional: host path containing your code repos, mounted READ-ONLY into the +# codebase-memory MCP (must match what Hermes sees at /c/dev). Required only if you +# enable the `codebase-memory` server (docker compose --profile codebase-memory). +# CODE_ROOT=C:/dev # --- Models (llama.cpp / GGUF) --- # Main chat model filename under models/gguf/ (must exist before llamacpp starts). diff --git a/CHANGELOG.md b/CHANGELOG.md index 0442a40..93d6f4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ All notable changes to this project are documented here. The format is loosely b ## [Unreleased] ### Added +- **Codebase-Memory MCP — opt-in `--profile codebase-memory` code knowledge graph for Hermes.** + Adds `codebase-memory`, a gateway-spawned stdio MCP server wrapping the upstream + `DeusData/codebase-memory-mcp` static binary (MIT; bundled offline embeddings, no API + keys). Gives Hermes structural code navigation (`search_graph`, `trace_path`, + `get_architecture`, `get_code_snippet`, ...) over the repos under `CODE_ROOT`, mounted + read-only at `/c/dev`. The SQLite index persists in the `codebase-memory-cache` named + volume; the spawned container is `longLived` (warm across calls) and `disableNetwork` + (no egress). Image is checksum-pinned (portable/static build, v0.8.1). Enable with + `docker compose --profile codebase-memory build codebase-memory-mcp-image` then + `./scripts/mcp_add.sh codebase-memory` (set `CODE_ROOT` first). Indexing honors + `.gitignore` + a new root `.cbmignore` (defense-in-depth secret/non-source excludes). - **Voice STT/TTS services — opt-in `--profile voice` with secondary-GPU pinning.** Adds two OpenAI-compatible local speech services: `stt` (`fedirz/faster-whisper-server:latest-cuda`, `/v1/audio/transcriptions` at `http://stt:8000/v1`) and `tts` (`ghcr.io/remsky/kokoro-fastapi-gpu:latest`, diff --git a/codebase-memory-mcp/Dockerfile b/codebase-memory-mcp/Dockerfile new file mode 100644 index 0000000..b3f4032 --- /dev/null +++ b/codebase-memory-mcp/Dockerfile @@ -0,0 +1,49 @@ +# syntax=docker/dockerfile:1 +# Codebase-Memory MCP server (gateway-spawned, stdio transport). +# +# Wraps the upstream DeusData/codebase-memory-mcp release binary so the stack's +# mcp-gateway can spawn it as a sibling container and expose its code knowledge +# graph tools (search_graph, trace_path, get_architecture, get_code_snippet, ...) +# to Hermes. The binary bundles its embedding model (nomic-embed-code) and runs +# 100% offline — no API keys, no network at runtime (we set disableNetwork in the +# gateway catalog entry). +# +# We use the *portable* (statically linked) release so there is no glibc coupling +# to the base image. Pinned + checksum-verified per the stack's "pin, don't float" +# rule: to bump, change CBM_VERSION and CBM_SHA256 together (sha256 comes from the +# release's checksums.txt for codebase-memory-mcp-linux-${CBM_ARCH}-portable.tar.gz). +FROM debian:bookworm-slim + +ARG CBM_VERSION=v0.8.1 +ARG CBM_ARCH=amd64 +ARG CBM_SHA256=6ab87a6c05d049dde57700803ca0ab4199fcf25973a0606618af0fcee73f5abd + +LABEL org.opencontainers.image.title="codebase-memory-mcp" \ + org.opencontainers.image.description="Code knowledge-graph MCP server (stdio) for the Ordo stack" \ + org.opencontainers.image.source="https://github.com/DeusData/codebase-memory-mcp" \ + org.opencontainers.image.licenses="MIT" \ + org.opencontainers.image.version="${CBM_VERSION}" + +# curl + ca-certificates are needed only at build time to fetch + verify the release. +RUN apt-get update \ + && apt-get install -y --no-install-recommends curl ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +RUN set -eux; \ + url="https://github.com/DeusData/codebase-memory-mcp/releases/download/${CBM_VERSION}/codebase-memory-mcp-linux-${CBM_ARCH}-portable.tar.gz"; \ + curl -fsSL -o /tmp/cbm.tar.gz "$url"; \ + echo "${CBM_SHA256} /tmp/cbm.tar.gz" | sha256sum -c -; \ + mkdir -p /usr/local/share/codebase-memory-mcp; \ + tar -xzf /tmp/cbm.tar.gz -C /usr/local/share/codebase-memory-mcp; \ + install -m 0755 /usr/local/share/codebase-memory-mcp/codebase-memory-mcp /usr/local/bin/codebase-memory-mcp; \ + rm -f /tmp/cbm.tar.gz + +# Index cache (SQLite) lives on a named volume mounted here by the gateway catalog +# entry; persists across container respawns. +ENV CBM_CACHE_DIR=/cache +# Keep logs on stderr / quiet so nothing pollutes the stdio JSON-RPC channel. +ENV CBM_LOG_LEVEL=error + +# Default entry point = MCP stdio server (matches the upstream MCP client config, +# which launches the bare binary with no args). +CMD ["codebase-memory-mcp"] diff --git a/codebase-memory-mcp/README.md b/codebase-memory-mcp/README.md new file mode 100644 index 0000000..f7e2b6f --- /dev/null +++ b/codebase-memory-mcp/README.md @@ -0,0 +1,64 @@ +# codebase-memory-mcp + +Gateway-spawned MCP server that gives Hermes a **structural code knowledge graph** +of the repos under your code root — call graphs, trace paths, architecture views, +symbol search — so it can navigate code instead of grepping blindly. + +It wraps the upstream [`DeusData/codebase-memory-mcp`](https://github.com/DeusData/codebase-memory-mcp) +release binary (MIT). The binary is a single static executable with a **bundled, +offline embedding model** (`nomic-embed-code`) — no API keys, no network at runtime. + +## How it's wired + +This is **not** a long-lived compose service. Like the other custom MCPs in this +stack (`qdrant-rag`, `comfyui`, `orchestration`), it is an **image** that the +`mcp-gateway` spawns as a sibling container over **stdio** when the tool is called. + +- `Dockerfile` — downloads + checksum-verifies the pinned release (portable/static + build) and sets the binary as the MCP stdio entry point. +- `docker-compose.yml` → `codebase-memory-mcp-image` (build-only, profile + `codebase-memory`) builds `ordo-ai-stack-codebase-memory-mcp:latest`. +- `mcp/gateway/registry-custom.yaml` → the `codebase-memory` catalog entry tells the + gateway how to spawn it: + - `volumes: ["PLACEHOLDER_CODE_ROOT:/c/dev:ro", "codebase-memory-cache:/cache"]` + — your code root mounted **read-only**, plus a **named volume** for the + persistent index. (`PLACEHOLDER_CODE_ROOT` is substituted from `CODE_ROOT` by + `gateway-wrapper.sh`.) + - `longLived: true` — keep the container warm across calls within a session. + - `disableNetwork: true` — the indexer needs no egress; this blocks exfiltration + of indexed code. + +Because the gateway spawns siblings via the host Docker daemon, the `/c/dev` bind +source is a **host path** and (under the gateway's 2026-06 bind-mount hardening) +must be read-only and allow-listed via `MCP_GATEWAY_DOCKER_BIND_ALLOWED_PATHS` +(set to `CODE_ROOT` in compose). The cache uses a named volume, which is exempt +from those host-path restrictions and so can be read-write. + +## Enabling it + +1. Set `CODE_ROOT` in `.env` to the **host** path that contains your repos, e.g. + `CODE_ROOT=C:/dev` (must match what Hermes sees at `/c/dev`). +2. Build the image (opt-in profile): + `docker compose --profile codebase-memory build codebase-memory-mcp-image` +3. Enable the server in the gateway: `./scripts/mcp_add.sh codebase-memory` + (the gateway hot-reloads in ~10s; no restart needed). + +## Indexing + +The index is built on demand and persists in the `codebase-memory-cache` named +volume. Hermes indexes a repo once (`index_repository` with a `/c/dev/` path), +then queries it (`search_graph`, `trace_path`, `get_architecture`, ...). Subsequent +sessions reuse the persisted index. + +## Security + +- Code root is mounted **read-only**; the container has **no network**. +- Indexing honors `.gitignore` and a project-level **`.cbmignore`** (gitignore + syntax). This repo ships a root `.cbmignore` that excludes secrets and + non-source paths as defense-in-depth; add one to each other indexed repo. +- Results are **navigation hints** — confirm in the actual file before editing. + +## Bumping the version + +Update `CBM_VERSION` + `CBM_SHA256` in the `Dockerfile` together (sha256 from the +release `checksums.txt`, `...-linux-amd64-portable.tar.gz` line), then rebuild. diff --git a/docker-compose.yml b/docker-compose.yml index 3611690..c1ca9c9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -581,6 +581,19 @@ services: restart: "no" command: ["true"] + # Codebase-Memory MCP image (gateway-spawned, stdio). Opt-in (heavy: bundles an + # embedding model). Build with: + # docker compose --profile codebase-memory build codebase-memory-mcp-image + # then enable in the gateway with: ./scripts/mcp_add.sh codebase-memory + # (requires CODE_ROOT set to your host code root). + codebase-memory-mcp-image: + build: ./codebase-memory-mcp + image: ordo-ai-stack-codebase-memory-mcp:latest + pull_policy: build + restart: "no" + command: ["true"] + profiles: ["codebase-memory"] + comfyui-mcp: image: ordo-ai-stack-comfyui-mcp:latest pull_policy: build @@ -716,6 +729,12 @@ services: - OPS_CONTROLLER_TOKEN=${OPS_CONTROLLER_TOKEN:-} # Injected into registry-custom.yaml for orchestration MCP (dashboard Bearer) - DASHBOARD_AUTH_TOKEN=${DASHBOARD_AUTH_TOKEN:-} + # Codebase-Memory MCP — host code root (read-only mount source for the + # spawned container) + bind-mount allowlist. CODE_ROOT must be the HOST path + # that contains your repos (what Hermes sees as /c/dev). Allow-listing it lets + # the gateway's hardened bind logic accept the read-only /c/dev mount. + - CODE_ROOT=${CODE_ROOT:-} + - MCP_GATEWAY_DOCKER_BIND_ALLOWED_PATHS=${CODE_ROOT:-} volumes: - /var/run/docker.sock:/var/run/docker.sock - ${DATA_PATH:-${BASE_PATH:-.}/data}/mcp:/mcp-config diff --git a/mcp/gateway/gateway-wrapper.sh b/mcp/gateway/gateway-wrapper.sh index 34b3d71..05d7284 100644 --- a/mcp/gateway/gateway-wrapper.sh +++ b/mcp/gateway/gateway-wrapper.sh @@ -63,7 +63,8 @@ resolve_registry_custom() { sed -e "s|PLACEHOLDER_OPS_CONTROLLER_TOKEN|${OPS_CONTROLLER_TOKEN:-}|g" \ -e "s|PLACEHOLDER_DASHBOARD_AUTH_TOKEN|${DASHBOARD_AUTH_TOKEN:-}|g" \ -e "s|PLACEHOLDER_COMFY_MCP_DEFAULT_MODEL|${COMFY_MCP_DEFAULT_MODEL:-flux1-schnell-fp8.safetensors}|g" \ - -e "s|PLACEHOLDER_N8N_API_KEY|${N8N_API_KEY:-}|g" "$src" >"$dst" + -e "s|PLACEHOLDER_N8N_API_KEY|${N8N_API_KEY:-}|g" \ + -e "s|PLACEHOLDER_CODE_ROOT|${CODE_ROOT:-}|g" "$src" >"$dst" else cp "$src" "$dst" fi diff --git a/mcp/gateway/registry-custom.yaml b/mcp/gateway/registry-custom.yaml index d3a3cca..074d680 100644 --- a/mcp/gateway/registry-custom.yaml +++ b/mcp/gateway/registry-custom.yaml @@ -82,3 +82,28 @@ registry: value: "false" - name: DISABLE_TELEMETRY value: "true" + codebase-memory: + description: "Code knowledge-graph navigation over the repos under your code root (mounted read-only at /c/dev). Structural queries grep can't do cheaply: call graphs, trace paths between symbols, architecture overviews, symbol/snippet lookup. Index is built on demand and persists in a named volume. Tools - index_repository, list_projects, index_status, search_graph, trace_path, query_graph, get_graph_schema, get_code_snippet, get_architecture, search_code, detect_changes, manage_adr, ingest_traces, delete_project. Results are navigation hints - confirm in the actual file before editing." + title: Codebase Memory + type: server + image: ordo-ai-stack-codebase-memory-mcp:latest + # Keep the indexer container warm across calls within a session. + longLived: true + # The indexer is 100% local (bundled embeddings) — no egress needed. This blocks + # exfiltration of indexed code from the spawned container. + disableNetwork: true + volumes: + # Code root (host path from $CODE_ROOT) mounted READ-ONLY. Under the gateway's + # 2026-06 bind-mount hardening, host binds must be read-only AND allow-listed via + # MCP_GATEWAY_DOCKER_BIND_ALLOWED_PATHS (set to CODE_ROOT in docker-compose.yml). + # PLACEHOLDER_CODE_ROOT is substituted from $CODE_ROOT by gateway-wrapper.sh. + - "PLACEHOLDER_CODE_ROOT:/c/dev:ro" + # Persistent SQLite index — a NAMED volume (exempt from host-path restrictions, + # so it can be read-write). Survives container respawns. + - "codebase-memory-cache:/cache" + env: + - name: CBM_CACHE_DIR + value: /cache + # Keep logs off stdout so they can't corrupt the stdio JSON-RPC channel. + - name: CBM_LOG_LEVEL + value: error From 0c14cf94fc3f579aa911c0c6cd529a030cdaf173 Mon Sep 17 00:00:00 2001 From: Hermes Bot Date: Tue, 23 Jun 2026 14:30:56 -0400 Subject: [PATCH 2/8] feat(codebase-memory): add 3D graph visualization UI service Folds the upstream visualization into the codebase-memory integration: a long-lived `codebase-memory-ui` service that serves the interactive 3D knowledge-graph from the SAME index the headless MCP builds (shared `codebase-memory-cache` named volume). - codebase-memory-ui/: Dockerfile (pinned, sha256-verified portable/static UI binary v0.8.1) + entrypoint. The UI binds 127.0.0.1 only, so the entrypoint bridges it to 0.0.0.0:9750 via socat; it also keeps stdin open so the MCP-stdio process doesn't EOF-exit and the UI stays up as a service. - docker-compose.yml: codebase-memory-ui service (profile codebase-memory, shared cache volume) + a pinned `codebase-memory-cache` named volume + an extra oauth2-proxy --whitelist-domain for the :8443 origin. - auth/caddy/Caddyfile: dedicated SSO-gated :8443 listener. The UI is an absolute-asset SPA (/assets, /api, /rpc at root) whose /api collides with Open WebUI's root catch-all, so it owns its origin rather than a subpath. - overrides/codebase-memory-ui.yml: publishes Caddy :8443 (opt-in). - README + CHANGELOG + .env.example. Validated: UI image builds (checksum verified) and serves HTTP 200 on both the localhost UI port and the socat-bridged port, staying alive as a service; merged compose config valid; Caddyfile validates ("Valid configuration", 3 servers incl :8443). Live OAuth-across-:8443 + in-browser render need the operator gateway/Caddy recreate (make up). Co-Authored-By: Claude Opus 4.8 (1M context) --- .env.example | 3 ++ CHANGELOG.md | 5 +++ auth/caddy/Caddyfile | 29 +++++++++++++++++ codebase-memory-ui/Dockerfile | 54 ++++++++++++++++++++++++++++++++ codebase-memory-ui/README.md | 43 +++++++++++++++++++++++++ codebase-memory-ui/entrypoint.sh | 15 +++++++++ docker-compose.yml | 40 +++++++++++++++++++++++ overrides/codebase-memory-ui.yml | 14 +++++++++ 8 files changed, 203 insertions(+) create mode 100644 codebase-memory-ui/Dockerfile create mode 100644 codebase-memory-ui/README.md create mode 100644 codebase-memory-ui/entrypoint.sh create mode 100644 overrides/codebase-memory-ui.yml diff --git a/.env.example b/.env.example index cf68209..f29879e 100644 --- a/.env.example +++ b/.env.example @@ -30,6 +30,9 @@ BASE_PATH=. # codebase-memory MCP (must match what Hermes sees at /c/dev). Required only if you # enable the `codebase-memory` server (docker compose --profile codebase-memory). # CODE_ROOT=C:/dev +# Optional: to also run the codebase-memory 3D graph UI, add its override to +# COMPOSE_FILE and browse https://:8443/ (Google SSO): +# COMPOSE_FILE=docker-compose.yml;overrides/compute.yml;overrides/codebase-memory-ui.yml # --- Models (llama.cpp / GGUF) --- # Main chat model filename under models/gguf/ (must exist before llamacpp starts). diff --git a/CHANGELOG.md b/CHANGELOG.md index 93d6f4c..7661ee8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,11 @@ All notable changes to this project are documented here. The format is loosely b `docker compose --profile codebase-memory build codebase-memory-mcp-image` then `./scripts/mcp_add.sh codebase-memory` (set `CODE_ROOT` first). Indexing honors `.gitignore` + a new root `.cbmignore` (defense-in-depth secret/non-source excludes). + Also adds an optional **`codebase-memory-ui`** service — the upstream 3D graph + **visualization** of the same index — exposed via Caddy + Google SSO on a dedicated + `:8443` listener (the UI is an absolute-asset SPA whose `/api/*` collides with Open + WebUI's root, so it owns its origin). The image bridges the UI's localhost-only bind + with `socat`; publish the port via `overrides/codebase-memory-ui.yml`. - **Voice STT/TTS services — opt-in `--profile voice` with secondary-GPU pinning.** Adds two OpenAI-compatible local speech services: `stt` (`fedirz/faster-whisper-server:latest-cuda`, `/v1/audio/transcriptions` at `http://stt:8000/v1`) and `tts` (`ghcr.io/remsky/kokoro-fastapi-gpu:latest`, diff --git a/auth/caddy/Caddyfile b/auth/caddy/Caddyfile index cf97f02..76df6d2 100644 --- a/auth/caddy/Caddyfile +++ b/auth/caddy/Caddyfile @@ -116,3 +116,32 @@ } } } + +# ---- codebase-memory 3D graph UI — dedicated SSO-gated listener ---- +# The UI is an absolute-asset SPA: it requests /assets/*, /api/*, and /rpc at the +# origin root. /api/* collides with Open WebUI's root catch-all, so the UI cannot +# be mounted as a subpath on :443 — it gets its own origin on :8443 instead, where +# every absolute path resolves to the UI with no overlap. Same Google SSO as :443 +# (the OAuth callback stays on :443; the post-login redirect returns here — see the +# extra --whitelist-domain=...:8443 on oauth2-proxy). Reachable only when +# overrides/codebase-memory-ui.yml publishes :8443 and the codebase-memory-ui +# service is up (docker compose --profile codebase-memory). +{$CADDY_TAILNET_HOSTNAME}:8443 { + tls /etc/caddy/certs/tailnet.crt /etc/caddy/certs/tailnet.key + + handle /oauth2/* { + reverse_proxy oauth2-proxy:4180 + } + + route { + forward_auth oauth2-proxy:4180 { + uri /oauth2/auth + copy_headers X-Auth-Request-Email>X-Forwarded-Email X-Auth-Request-User>X-Forwarded-User X-Auth-Request-Preferred-Username>X-Forwarded-Preferred-Username + @unauthorized status 401 + handle_response @unauthorized { + redir https://{host}:8443/oauth2/start?rd={scheme}://{host}:8443{uri} 302 + } + } + reverse_proxy codebase-memory-ui:9750 + } +} diff --git a/codebase-memory-ui/Dockerfile b/codebase-memory-ui/Dockerfile new file mode 100644 index 0000000..6ccd0d3 --- /dev/null +++ b/codebase-memory-ui/Dockerfile @@ -0,0 +1,54 @@ +# syntax=docker/dockerfile:1 +# Codebase-Memory 3D graph visualization UI (long-lived service). +# +# Runs the upstream UI-variant binary (`codebase-memory-mcp --ui=true`), which +# serves an interactive 3D knowledge-graph at :9749 as a thread alongside the MCP +# server. Two upstream quirks this image works around: +# 1. The UI HTTP server binds 127.0.0.1 ONLY (src/ui/httpd.c) — unreachable from +# other containers. We run a `socat` bridge on 0.0.0.0:9750 -> 127.0.0.1:9749 +# so Caddy/oauth2-proxy can route to it. +# 2. The process is an MCP stdio server; with no client it would hit EOF on stdin +# and exit. The entrypoint keeps stdin open so the UI stays up. +# +# Shares the SAME index as the headless codebase-memory MCP via the +# `codebase-memory-cache` volume (CBM_CACHE_DIR=/cache), so it visualizes whatever +# Hermes has indexed. +# +# Pinned + checksum-verified (portable/static UI build). Bump CBM_VERSION + +# CBM_UI_SHA256 together (sha256 from the release checksums.txt, +# codebase-memory-mcp-ui-linux-${CBM_ARCH}-portable.tar.gz line). +FROM debian:bookworm-slim + +ARG CBM_VERSION=v0.8.1 +ARG CBM_ARCH=amd64 +ARG CBM_UI_SHA256=74ed63e3af2d85bfd98cbcce7191295536d0a54b084cbc0b868d69f2272172da + +LABEL org.opencontainers.image.title="codebase-memory-ui" \ + org.opencontainers.image.description="3D code knowledge-graph visualization UI for the Ordo stack" \ + org.opencontainers.image.source="https://github.com/DeusData/codebase-memory-mcp" \ + org.opencontainers.image.licenses="MIT" \ + org.opencontainers.image.version="${CBM_VERSION}" + +RUN apt-get update \ + && apt-get install -y --no-install-recommends curl ca-certificates socat \ + && rm -rf /var/lib/apt/lists/* + +RUN set -eux; \ + url="https://github.com/DeusData/codebase-memory-mcp/releases/download/${CBM_VERSION}/codebase-memory-mcp-ui-linux-${CBM_ARCH}-portable.tar.gz"; \ + curl -fsSL -o /tmp/cbm.tar.gz "$url"; \ + echo "${CBM_UI_SHA256} /tmp/cbm.tar.gz" | sha256sum -c -; \ + mkdir -p /usr/local/share/codebase-memory-ui; \ + tar -xzf /tmp/cbm.tar.gz -C /usr/local/share/codebase-memory-ui; \ + install -m 0755 /usr/local/share/codebase-memory-ui/codebase-memory-mcp /usr/local/bin/codebase-memory-mcp; \ + rm -f /tmp/cbm.tar.gz + +COPY entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh + +ENV CBM_CACHE_DIR=/cache +ENV CBM_LOG_LEVEL=error + +# Public (bridged) port. The UI itself listens on 127.0.0.1:9749 inside the container. +EXPOSE 9750 + +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] diff --git a/codebase-memory-ui/README.md b/codebase-memory-ui/README.md new file mode 100644 index 0000000..f414ccd --- /dev/null +++ b/codebase-memory-ui/README.md @@ -0,0 +1,43 @@ +# codebase-memory-ui + +Optional long-lived service that serves the **3D interactive code knowledge-graph** +from the same index the headless `codebase-memory` MCP builds — so you can *browse* +the graph, not just have Hermes query it. + +It runs the upstream UI-variant binary (`codebase-memory-mcp --ui=true --port=9749`), +which serves the visualization as a thread alongside the MCP server. + +## Two upstream quirks this image handles +1. **The UI HTTP server binds `127.0.0.1` only** (`src/ui/httpd.c`) — unreachable from + other containers. `entrypoint.sh` runs a `socat` bridge `0.0.0.0:9750 → 127.0.0.1:9749` + so Caddy can route to it. +2. **The process is an MCP stdio server** — with no client attached it would read EOF + on stdin and exit. The entrypoint keeps stdin open (`tail -f /dev/null | …`) so the + UI stays up as a service. + +## Shared index +Mounts the **`codebase-memory-cache`** named volume at `/cache` (`CBM_CACHE_DIR=/cache`) +— the same volume the gateway-spawned MCP writes to. The UI visualizes whatever Hermes +has indexed. (SQLite WAL allows the UI to read while the MCP writes.) + +## Exposure (SSO) +The UI is an **absolute-asset SPA** — it requests `/assets/*`, `/api/*`, and `/rpc` at +the origin root, and `/api/*` collides with Open WebUI's root catch-all. So it can't be a +subpath on `:443`; it gets its **own origin on Caddy's `:8443`** listener, behind the same +Google SSO. See the `:8443` block in `auth/caddy/Caddyfile`, the extra +`--whitelist-domain=…:8443` on `oauth2-proxy`, and `overrides/codebase-memory-ui.yml` +(which publishes the port). + +## Enable +``` +# include the override + the codebase-memory profile +COMPOSE_FILE=docker-compose.yml;overrides/compute.yml;overrides/codebase-memory-ui.yml +docker compose --profile codebase-memory up -d --build +``` +Then browse **`https://:8443/`** (Google SSO). Index a repo first +(via Hermes `index_repository`, or the UI's own "index" action) or the graph will be empty. + +## Note +The UI exposes server actions (`/api/index`, `/api/process-kill`, …) to the browser; it's +SSO-gated to the single operator and network-isolated, which is the acceptable trust model +here. Pin/bump the binary via `CBM_VERSION` + `CBM_UI_SHA256` in the `Dockerfile`. diff --git a/codebase-memory-ui/entrypoint.sh b/codebase-memory-ui/entrypoint.sh new file mode 100644 index 0000000..3e7b18e --- /dev/null +++ b/codebase-memory-ui/entrypoint.sh @@ -0,0 +1,15 @@ +#!/bin/sh +# Start the codebase-memory 3D graph UI as a long-lived service. +set -eu + +CACHE_DIR="${CBM_CACHE_DIR:-/cache}" +mkdir -p "$CACHE_DIR" + +# The UI HTTP server binds 127.0.0.1:9749 only. Bridge it to 0.0.0.0:9750 so the +# reverse proxy (Caddy) in another container can reach it. +socat TCP-LISTEN:9750,fork,reuseaddr,bind=0.0.0.0 TCP:127.0.0.1:9749 & + +# The binary is an MCP stdio server with the UI as a side thread; with no MCP +# client attached it would read EOF on stdin and exit. Keep stdin open so the +# process (and the UI) stays alive. `tail -f /dev/null` never closes the pipe. +exec sh -c 'tail -f /dev/null | codebase-memory-mcp --ui=true --port=9749' diff --git a/docker-compose.yml b/docker-compose.yml index c1ca9c9..d80f237 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -594,6 +594,36 @@ services: command: ["true"] profiles: ["codebase-memory"] + # Codebase-Memory 3D graph visualization UI (long-lived; opt-in). Serves the + # interactive knowledge-graph from the SAME index the headless codebase-memory + # MCP writes (shared `codebase-memory-cache` volume). The upstream UI binds + # 127.0.0.1:9749 only; the image's entrypoint bridges it to 0.0.0.0:9750 via + # socat so Caddy can reach it. Exposed via Caddy + oauth2-proxy SSO on a + # dedicated :8443 listener (it's an absolute-asset SPA that must own its origin; + # add overrides/codebase-memory-ui.yml to publish the port). Build with: + # docker compose --profile codebase-memory build codebase-memory-ui + codebase-memory-ui: + build: ./codebase-memory-ui + image: ordo-ai-stack-codebase-memory-ui:latest + pull_policy: build + restart: unless-stopped + profiles: ["codebase-memory"] + volumes: + - codebase-memory-cache:/cache + healthcheck: + test: ["CMD-SHELL", "curl -fsS -o /dev/null http://localhost:9750/ || exit 1"] + interval: 30s + timeout: 5s + retries: 5 + start_period: 20s + networks: + - proxy-net + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + comfyui-mcp: image: ordo-ai-stack-comfyui-mcp:latest pull_policy: build @@ -781,6 +811,10 @@ services: - --upstream=static://202 - --redirect-url=https://${CADDY_TAILNET_HOSTNAME}/oauth2/callback - --whitelist-domain=.${CADDY_TAILNET_DOMAIN} + # Allow post-login redirects back to the codebase-memory-ui dedicated + # listener (Caddy :8443). The OAuth callback itself stays on :443 + # (--redirect-url above); this only whitelists the :8443 return target. + - --whitelist-domain=.${CADDY_TAILNET_DOMAIN}:8443 - --cookie-domain=.${CADDY_TAILNET_DOMAIN} - --cookie-secure=true - --cookie-samesite=lax @@ -1137,6 +1171,12 @@ services: volumes: caddy_data: caddy_config: + # Shared code-index store: written by the gateway-spawned codebase-memory MCP + # and read by the codebase-memory-ui visualization. `name:` pins the literal + # volume name (no compose project prefix) so it matches the raw name the + # mcp-gateway uses when it spawns the MCP container (-v codebase-memory-cache:/cache). + codebase-memory-cache: + name: codebase-memory-cache # Hermes data is now bind-mounted from data/hermes/ (see hermes-gateway/dashboard above). # The legacy `ordo-ai-stack_hermes-data` named volume still exists in Docker for # rollback. To revert: re-add `hermes-data:` here, then switch the hermes services' diff --git a/overrides/codebase-memory-ui.yml b/overrides/codebase-memory-ui.yml new file mode 100644 index 0000000..5926f5a --- /dev/null +++ b/overrides/codebase-memory-ui.yml @@ -0,0 +1,14 @@ +# Opt-in: publish Caddy's dedicated :8443 listener for the codebase-memory 3D +# graph visualization UI. The UI is an absolute-asset SPA (/assets, /api, /rpc) +# that must own its origin, so it can't share the :443 root with Open WebUI — it +# gets its own SSO-gated port. The Caddyfile already defines the :8443 site block; +# this override just publishes the host port for it. +# +# Enable (with the codebase-memory profile so the service + image exist): +# COMPOSE_FILE=docker-compose.yml;overrides/compute.yml;overrides/codebase-memory-ui.yml +# docker compose --profile codebase-memory up -d --build +# Then browse: https://:8443/ (Google SSO, same as :443). +services: + caddy: + ports: + - "${CADDY_BIND:?CADDY_BIND must be set to the host tailnet IP}:8443:8443" From abf87e94870b87a1b256f43aafdff3c1400f13da Mon Sep 17 00:00:00 2001 From: Hermes Bot Date: Tue, 23 Jun 2026 15:09:59 -0400 Subject: [PATCH 3/8] fix(codebase-memory-ui): index in-process (mount /c/dev:ro); correct shared-index docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The upstream binary doesn't reliably flush its graph index to CBM_CACHE_DIR across container exits, so the cache volume can't serve as a shared index between the gateway-spawned MCP and the UI. Mount the code root read-only on the UI so its own long-lived process indexes + visualizes the graph (the index is then in-memory; re-index after a restart). Validated live: the UI indexed ordo-ai-stack (2062 nodes / 8411 edges, secrets/ + data/ excluded) and serves it; gateway loads codebase-memory (14 tools); :8443 SSO listener returns 302→oauth2 with the cross-port rd intact; :443 front door unaffected. Corrects the now-inaccurate "shared index" comments in docker-compose.yml + README. Co-Authored-By: Claude Opus 4.8 (1M context) --- codebase-memory-ui/README.md | 15 +++++++++++---- docker-compose.yml | 15 +++++++++++---- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/codebase-memory-ui/README.md b/codebase-memory-ui/README.md index f414ccd..ea802b8 100644 --- a/codebase-memory-ui/README.md +++ b/codebase-memory-ui/README.md @@ -15,10 +15,17 @@ which serves the visualization as a thread alongside the MCP server. on stdin and exit. The entrypoint keeps stdin open (`tail -f /dev/null | …`) so the UI stays up as a service. -## Shared index -Mounts the **`codebase-memory-cache`** named volume at `/cache` (`CBM_CACHE_DIR=/cache`) -— the same volume the gateway-spawned MCP writes to. The UI visualizes whatever Hermes -has indexed. (SQLite WAL allows the UI to read while the MCP writes.) +## Index (in-process) +The UI **indexes the source tree in its own long-lived process** and visualizes that +in-memory graph. It mounts the code root **read-only** at `/c/dev` (`${CODE_ROOT}`) for +this, plus the `codebase-memory-cache` volume at `/cache` for config. + +> The upstream binary does **not** reliably flush its graph index to `CBM_CACHE_DIR` +> across container exits, so the cache volume is **not** a shared index — the gateway +> MCP and the UI each index independently. Practical consequence: **the UI's graph is +> in-memory, so after a container restart you must re-index** (browse the UI's index +> action, or `POST /rpc` `index_repository`). Indexing honors `.gitignore` + `.cbmignore` +> (e.g. `secrets/`, `data/` are excluded — verified). ## Exposure (SSO) The UI is an **absolute-asset SPA** — it requests `/assets/*`, `/api/*`, and `/rpc` at diff --git a/docker-compose.yml b/docker-compose.yml index d80f237..68bde56 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -610,6 +610,11 @@ services: profiles: ["codebase-memory"] volumes: - codebase-memory-cache:/cache + # Source tree (read-only) so the UI's own long-lived process can index and + # visualize it. The graph index lives in-process (it is not reliably flushed + # to the cache volume across container exits), so the UI indexes its own + # graph rather than depending on the gateway-spawned MCP's index. + - ${CODE_ROOT:-/c/dev}:/c/dev:ro healthcheck: test: ["CMD-SHELL", "curl -fsS -o /dev/null http://localhost:9750/ || exit 1"] interval: 30s @@ -1171,10 +1176,12 @@ services: volumes: caddy_data: caddy_config: - # Shared code-index store: written by the gateway-spawned codebase-memory MCP - # and read by the codebase-memory-ui visualization. `name:` pins the literal - # volume name (no compose project prefix) so it matches the raw name the - # mcp-gateway uses when it spawns the MCP container (-v codebase-memory-cache:/cache). + # Per-container config/cache for codebase-memory (holds _config.db + config.json). + # NOTE: the graph index itself lives IN-PROCESS and is not reliably flushed here + # across container exits, so this is NOT a shared index — the gateway-spawned MCP + # and the UI each index in their own process. The volume is still shared/mounted + # by both; `name:` pins the literal name (no compose project prefix) to match the + # raw name the mcp-gateway uses when it spawns the MCP (-v codebase-memory-cache:/cache). codebase-memory-cache: name: codebase-memory-cache # Hermes data is now bind-mounted from data/hermes/ (see hermes-gateway/dashboard above). From 4420811302883030420e2f4fd1aec0ea164ee046 Mon Sep 17 00:00:00 2001 From: Hermes Bot Date: Tue, 23 Jun 2026 15:46:59 -0400 Subject: [PATCH 4/8] feat(dashboard): list codebase-memory-ui in the services section Add a `codebase-memory-ui` entry to dashboard SERVICES (health-checked at http://codebase-memory-ui:9750/ over proxy-net) and to ops-controller ALLOWED_SERVICES (so restart/logs/stats work for it). `url` is omitted so the frontend builds https://:8443 from the port (the SSO Caddy listener); the localhost-only :9749 UI is bridged to :9750, which is what the dashboard health-checks. Validated live: /api/health lists codebase-memory-ui ok=true (9 services); ruff + dashboard service tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- dashboard/services_catalog.py | 8 ++++++++ ops-controller/main.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/dashboard/services_catalog.py b/dashboard/services_catalog.py index 97eeed2..6cab316 100644 --- a/dashboard/services_catalog.py +++ b/dashboard/services_catalog.py @@ -35,6 +35,14 @@ {"id": "hermes", "name": "Hermes Agent", "port": 9119, "url": "http://localhost:9119", "check": "http://hermes-dashboard:9119/", "has_gpu": False, "hint": "Managed by docker compose. Logs: docker compose logs hermes-dashboard"}, + # Opt-in (--profile codebase-memory). 3D code knowledge-graph visualization. No host + # port — reached via Caddy's SSO :8443 listener; the UI binds 127.0.0.1 inside the + # container and is bridged to :9750 (what the dashboard health-checks). `url` is omitted + # so the frontend builds https://:8443 from the port. + {"id": "codebase-memory-ui", "name": "Codebase Memory", "port": 8443, + "check": "http://codebase-memory-ui:9750/", "has_gpu": False, + "hint": "3D code knowledge-graph. Open via SSO at https://:8443/. " + "In-memory index — re-index after a restart. Opt-in: --profile codebase-memory + overrides/codebase-memory-ui.yml"}, ] diff --git a/ops-controller/main.py b/ops-controller/main.py index 205740f..d712321 100644 --- a/ops-controller/main.py +++ b/ops-controller/main.py @@ -60,7 +60,7 @@ # Services we allow operations on (allowlist) ALLOWED_SERVICES = { "llamacpp", "llamacpp-embed", "dashboard", "open-webui", "model-gateway", "mcp-gateway", - "comfyui", "n8n", "qdrant", "stt", "tts", + "comfyui", "n8n", "qdrant", "stt", "tts", "codebase-memory-ui", } # .env keys we allow updating via the API From 3b90a1873bf8cb262d7c8c9dd8317cd336005998 Mon Sep 17 00:00:00 2001 From: Hermes Bot Date: Tue, 23 Jun 2026 15:50:31 -0400 Subject: [PATCH 5/8] fix(dashboard): https link for codebase-memory-ui service The "Open" link was generated as http://:8443 (serviceOpenHref's no-url fallback defaults to http), which hit the TLS :8443 listener and failed with "Client sent an HTTP request to an HTTPS server". Set the catalog `url` to https://localhost:8443 so serviceOpenHref keeps the https scheme + :8443 port and only swaps in the dashboard host -> https://:8443/. Validated live: /api/services returns url=https://localhost:8443, ok=true. Co-Authored-By: Claude Opus 4.8 (1M context) --- dashboard/services_catalog.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/dashboard/services_catalog.py b/dashboard/services_catalog.py index 6cab316..5a01d76 100644 --- a/dashboard/services_catalog.py +++ b/dashboard/services_catalog.py @@ -37,10 +37,13 @@ "hint": "Managed by docker compose. Logs: docker compose logs hermes-dashboard"}, # Opt-in (--profile codebase-memory). 3D code knowledge-graph visualization. No host # port — reached via Caddy's SSO :8443 listener; the UI binds 127.0.0.1 inside the - # container and is bridged to :9750 (what the dashboard health-checks). `url` is omitted - # so the frontend builds https://:8443 from the port. + # container and is bridged to :9750 (what the dashboard health-checks). `url` carries + # the https scheme + :8443 port (the frontend's serviceOpenHref keeps scheme/port and + # swaps in the dashboard's hostname) — the `localhost` placeholder is replaced at render + # time, so the "Open" link becomes https://:8443/. Plain http here would hit + # the TLS listener and fail with "Client sent an HTTP request to an HTTPS server". {"id": "codebase-memory-ui", "name": "Codebase Memory", "port": 8443, - "check": "http://codebase-memory-ui:9750/", "has_gpu": False, + "url": "https://localhost:8443", "check": "http://codebase-memory-ui:9750/", "has_gpu": False, "hint": "3D code knowledge-graph. Open via SSO at https://:8443/. " "In-memory index — re-index after a restart. Opt-in: --profile codebase-memory + overrides/codebase-memory-ui.yml"}, ] From 4798c5625ecb57acc3348597aae43cf3cb9eb9e9 Mon Sep 17 00:00:00 2001 From: Hermes Bot Date: Tue, 23 Jun 2026 16:26:18 -0400 Subject: [PATCH 6/8] feat(codebase-memory-ui): serve at /codebase-memory/ on :443 (drop :8443) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Give the visualization a proper, port-less URL — https:///codebase-memory/ on the shared :443 Google-SSO origin — instead of the dedicated :8443 listener. The UI is an absolute-asset SPA with no base-path option, so the image now runs nginx that proxies the localhost-only UI and sub_filter-rewrites its baked /assets,/api,/rpc,/font-files to the /codebase-memory/ prefix — letting it live under a subpath without colliding with Open WebUI's root. Caddy routes /codebase-memory/* in the existing :443 SSO block; the dashboard links via SSO_ROUTES. Removes the :8443 listener, its oauth2-proxy :8443 whitelist, and the overrides/codebase-memory-ui.yml port override (socat replaced by nginx). Validated live: /codebase-memory/ -> 302 SSO (same-origin rd); assets+api proxy through nginx (re-index succeeded via /codebase-memory/rpc); :8443 removed; :443 front door intact (/healthz 200, /dash/ 302); dashboard /api/health ok=true. Re-verify the nginx sub_filter path list when CBM_VERSION is bumped. Co-Authored-By: Claude Opus 4.8 (1M context) --- overrides/codebase-memory-ui.yml | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 overrides/codebase-memory-ui.yml diff --git a/overrides/codebase-memory-ui.yml b/overrides/codebase-memory-ui.yml deleted file mode 100644 index 5926f5a..0000000 --- a/overrides/codebase-memory-ui.yml +++ /dev/null @@ -1,14 +0,0 @@ -# Opt-in: publish Caddy's dedicated :8443 listener for the codebase-memory 3D -# graph visualization UI. The UI is an absolute-asset SPA (/assets, /api, /rpc) -# that must own its origin, so it can't share the :443 root with Open WebUI — it -# gets its own SSO-gated port. The Caddyfile already defines the :8443 site block; -# this override just publishes the host port for it. -# -# Enable (with the codebase-memory profile so the service + image exist): -# COMPOSE_FILE=docker-compose.yml;overrides/compute.yml;overrides/codebase-memory-ui.yml -# docker compose --profile codebase-memory up -d --build -# Then browse: https://:8443/ (Google SSO, same as :443). -services: - caddy: - ports: - - "${CADDY_BIND:?CADDY_BIND must be set to the host tailnet IP}:8443:8443" From f7dd90b7065da0b6f45aa7af40178106bd6f8f46 Mon Sep 17 00:00:00 2001 From: Hermes Bot Date: Tue, 23 Jun 2026 16:26:37 -0400 Subject: [PATCH 7/8] feat(codebase-memory-ui): serve at /codebase-memory/ on :443 (drop :8443) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Give the visualization a proper, port-less URL — https:///codebase-memory/ on the shared :443 Google-SSO origin — instead of the dedicated :8443 listener. The UI is an absolute-asset SPA with no base-path option, so the image now runs nginx that proxies the localhost-only UI and sub_filter-rewrites its baked /assets,/api,/rpc,/font-files to the /codebase-memory/ prefix — letting it live under a subpath without colliding with Open WebUI's root. Caddy routes /codebase-memory/* in the existing :443 SSO block; the dashboard links via SSO_ROUTES. Removes the :8443 listener, its oauth2-proxy :8443 whitelist, and the overrides/codebase-memory-ui.yml override (socat replaced by nginx). Validated live: /codebase-memory/ -> 302 SSO (same-origin rd); assets+api proxy through nginx (re-index succeeded via /codebase-memory/rpc); :8443 removed; :443 front door intact (/healthz 200, /dash/ 302); dashboard /api/health ok=true. Re-verify the nginx sub_filter path list when CBM_VERSION is bumped. Co-Authored-By: Claude Opus 4.8 (1M context) --- .env.example | 6 ++-- CHANGELOG.md | 12 +++++--- auth/caddy/Caddyfile | 40 +++++++----------------- codebase-memory-ui/Dockerfile | 17 +++++++---- codebase-memory-ui/README.md | 28 +++++++++-------- codebase-memory-ui/entrypoint.sh | 17 ++++++----- codebase-memory-ui/nginx.conf | 52 ++++++++++++++++++++++++++++++++ dashboard/services_catalog.py | 20 ++++++------ dashboard/static/index.html | 1 + docker-compose.yml | 21 +++++-------- 10 files changed, 126 insertions(+), 88 deletions(-) create mode 100644 codebase-memory-ui/nginx.conf diff --git a/.env.example b/.env.example index f29879e..3a452c3 100644 --- a/.env.example +++ b/.env.example @@ -30,9 +30,9 @@ BASE_PATH=. # codebase-memory MCP (must match what Hermes sees at /c/dev). Required only if you # enable the `codebase-memory` server (docker compose --profile codebase-memory). # CODE_ROOT=C:/dev -# Optional: to also run the codebase-memory 3D graph UI, add its override to -# COMPOSE_FILE and browse https://:8443/ (Google SSO): -# COMPOSE_FILE=docker-compose.yml;overrides/compute.yml;overrides/codebase-memory-ui.yml +# Optional: to also run the codebase-memory 3D graph UI, enable the profile; it is +# served at https:///codebase-memory/ (Google SSO, on :443): +# docker compose --profile codebase-memory up -d --build # --- Models (llama.cpp / GGUF) --- # Main chat model filename under models/gguf/ (must exist before llamacpp starts). diff --git a/CHANGELOG.md b/CHANGELOG.md index 7661ee8..b6abcf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,11 +16,13 @@ All notable changes to this project are documented here. The format is loosely b `docker compose --profile codebase-memory build codebase-memory-mcp-image` then `./scripts/mcp_add.sh codebase-memory` (set `CODE_ROOT` first). Indexing honors `.gitignore` + a new root `.cbmignore` (defense-in-depth secret/non-source excludes). - Also adds an optional **`codebase-memory-ui`** service — the upstream 3D graph - **visualization** of the same index — exposed via Caddy + Google SSO on a dedicated - `:8443` listener (the UI is an absolute-asset SPA whose `/api/*` collides with Open - WebUI's root, so it owns its origin). The image bridges the UI's localhost-only bind - with `socat`; publish the port via `overrides/codebase-memory-ui.yml`. + Also adds an optional **`codebase-memory-ui`** service — the upstream interactive 3D + graph **visualization** — served at `https:///codebase-memory/` on the shared + `:443` Google-SSO origin and listed in the dashboard services section. The UI is an + absolute-asset SPA with no base-path option (its `/assets`/`/api` collide with Open + WebUI's root), so the image runs nginx that proxies the localhost-only UI and + `sub_filter`-rewrites its baked paths to the `/codebase-memory/` prefix. It indexes + the code root (`/c/dev:ro`) in its own process (in-memory; re-index after a restart). - **Voice STT/TTS services — opt-in `--profile voice` with secondary-GPU pinning.** Adds two OpenAI-compatible local speech services: `stt` (`fedirz/faster-whisper-server:latest-cuda`, `/v1/audio/transcriptions` at `http://stt:8000/v1`) and `tts` (`ghcr.io/remsky/kokoro-fastapi-gpu:latest`, diff --git a/auth/caddy/Caddyfile b/auth/caddy/Caddyfile index 76df6d2..ad6418c 100644 --- a/auth/caddy/Caddyfile +++ b/auth/caddy/Caddyfile @@ -96,6 +96,17 @@ reverse_proxy comfyui:8188 } + # ---- codebase-memory 3D graph UI at /codebase-memory/ ---- + # Absolute-asset SPA served under a subpath: the codebase-memory-ui container + # runs nginx, which proxies to the UI and rewrites its baked /assets,/api,/rpc, + # /font-files to the /codebase-memory/ prefix (see codebase-memory-ui/nginx.conf). + # `handle` (no strip) keeps the prefix so nginx's location matches. Opt-in + # (--profile codebase-memory); 502s if the service isn't running. + @codebasememory path /codebase-memory /codebase-memory/* + handle @codebasememory { + reverse_proxy codebase-memory-ui:9750 + } + # ---- Open WebUI mounted at /chat (prefix stripped) ---- # /chat/* → open-webui with the /chat prefix removed, so the upstream # sees /, /c/, /models, etc. This is the canonical chat path. @@ -116,32 +127,3 @@ } } } - -# ---- codebase-memory 3D graph UI — dedicated SSO-gated listener ---- -# The UI is an absolute-asset SPA: it requests /assets/*, /api/*, and /rpc at the -# origin root. /api/* collides with Open WebUI's root catch-all, so the UI cannot -# be mounted as a subpath on :443 — it gets its own origin on :8443 instead, where -# every absolute path resolves to the UI with no overlap. Same Google SSO as :443 -# (the OAuth callback stays on :443; the post-login redirect returns here — see the -# extra --whitelist-domain=...:8443 on oauth2-proxy). Reachable only when -# overrides/codebase-memory-ui.yml publishes :8443 and the codebase-memory-ui -# service is up (docker compose --profile codebase-memory). -{$CADDY_TAILNET_HOSTNAME}:8443 { - tls /etc/caddy/certs/tailnet.crt /etc/caddy/certs/tailnet.key - - handle /oauth2/* { - reverse_proxy oauth2-proxy:4180 - } - - route { - forward_auth oauth2-proxy:4180 { - uri /oauth2/auth - copy_headers X-Auth-Request-Email>X-Forwarded-Email X-Auth-Request-User>X-Forwarded-User X-Auth-Request-Preferred-Username>X-Forwarded-Preferred-Username - @unauthorized status 401 - handle_response @unauthorized { - redir https://{host}:8443/oauth2/start?rd={scheme}://{host}:8443{uri} 302 - } - } - reverse_proxy codebase-memory-ui:9750 - } -} diff --git a/codebase-memory-ui/Dockerfile b/codebase-memory-ui/Dockerfile index 6ccd0d3..6791e02 100644 --- a/codebase-memory-ui/Dockerfile +++ b/codebase-memory-ui/Dockerfile @@ -3,11 +3,14 @@ # # Runs the upstream UI-variant binary (`codebase-memory-mcp --ui=true`), which # serves an interactive 3D knowledge-graph at :9749 as a thread alongside the MCP -# server. Two upstream quirks this image works around: +# server. Three upstream quirks this image works around: # 1. The UI HTTP server binds 127.0.0.1 ONLY (src/ui/httpd.c) — unreachable from -# other containers. We run a `socat` bridge on 0.0.0.0:9750 -> 127.0.0.1:9749 -# so Caddy/oauth2-proxy can route to it. -# 2. The process is an MCP stdio server; with no client it would hit EOF on stdin +# other containers. nginx (on 0.0.0.0:9750) proxies to 127.0.0.1:9749. +# 2. The UI is an absolute-asset SPA with no base-path option; nginx serves it +# under /codebase-memory/ and sub_filter-rewrites its baked /assets,/api,/rpc +# paths to that prefix so it can live on the shared :443 SSO origin (see +# nginx.conf). No port, no collision with Open WebUI's root. +# 3. The process is an MCP stdio server; with no client it would hit EOF on stdin # and exit. The entrypoint keeps stdin open so the UI stays up. # # Shares the SAME index as the headless codebase-memory MCP via the @@ -30,7 +33,7 @@ LABEL org.opencontainers.image.title="codebase-memory-ui" \ org.opencontainers.image.version="${CBM_VERSION}" RUN apt-get update \ - && apt-get install -y --no-install-recommends curl ca-certificates socat \ + && apt-get install -y --no-install-recommends curl ca-certificates nginx \ && rm -rf /var/lib/apt/lists/* RUN set -eux; \ @@ -42,13 +45,15 @@ RUN set -eux; \ install -m 0755 /usr/local/share/codebase-memory-ui/codebase-memory-mcp /usr/local/bin/codebase-memory-mcp; \ rm -f /tmp/cbm.tar.gz +COPY nginx.conf /etc/nginx/nginx.conf COPY entrypoint.sh /usr/local/bin/entrypoint.sh RUN chmod +x /usr/local/bin/entrypoint.sh ENV CBM_CACHE_DIR=/cache ENV CBM_LOG_LEVEL=error -# Public (bridged) port. The UI itself listens on 127.0.0.1:9749 inside the container. +# nginx serves the rewritten SPA under /codebase-memory/ here; the UI itself listens +# on 127.0.0.1:9749 inside the container. EXPOSE 9750 ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] diff --git a/codebase-memory-ui/README.md b/codebase-memory-ui/README.md index ea802b8..fef0d64 100644 --- a/codebase-memory-ui/README.md +++ b/codebase-memory-ui/README.md @@ -8,9 +8,12 @@ It runs the upstream UI-variant binary (`codebase-memory-mcp --ui=true --port=97 which serves the visualization as a thread alongside the MCP server. ## Two upstream quirks this image handles -1. **The UI HTTP server binds `127.0.0.1` only** (`src/ui/httpd.c`) — unreachable from - other containers. `entrypoint.sh` runs a `socat` bridge `0.0.0.0:9750 → 127.0.0.1:9749` - so Caddy can route to it. +1. **Absolute-asset SPA that binds `127.0.0.1` only.** The UI binds `127.0.0.1:9749` + and serves `/assets`, `/api`, `/rpc`, `/font-files` at the origin root with no + base-path option. The image runs **nginx** (on `0.0.0.0:9750`) which proxies to the + UI and `sub_filter`-rewrites those baked paths to the `/codebase-memory/` prefix + (see `nginx.conf`) — so Caddy serves it under that subpath on the shared `:443` SSO + origin without colliding with Open WebUI's root. 2. **The process is an MCP stdio server** — with no client attached it would read EOF on stdin and exit. The entrypoint keeps stdin open (`tail -f /dev/null | …`) so the UI stays up as a service. @@ -28,21 +31,20 @@ this, plus the `codebase-memory-cache` volume at `/cache` for config. > (e.g. `secrets/`, `data/` are excluded — verified). ## Exposure (SSO) -The UI is an **absolute-asset SPA** — it requests `/assets/*`, `/api/*`, and `/rpc` at -the origin root, and `/api/*` collides with Open WebUI's root catch-all. So it can't be a -subpath on `:443`; it gets its **own origin on Caddy's `:8443`** listener, behind the same -Google SSO. See the `:8443` block in `auth/caddy/Caddyfile`, the extra -`--whitelist-domain=…:8443` on `oauth2-proxy`, and `overrides/codebase-memory-ui.yml` -(which publishes the port). +Served at **`https:///codebase-memory/`** on the shared `:443` origin, behind the +existing Google SSO — no dedicated port. Caddy routes `/codebase-memory/*` to this +container (the `@codebasememory` handle in `auth/caddy/Caddyfile`); nginx rewrites the +SPA's absolute paths so everything stays under the prefix. The dashboard's services +section links here via `SSO_ROUTES`. ## Enable ``` -# include the override + the codebase-memory profile -COMPOSE_FILE=docker-compose.yml;overrides/compute.yml;overrides/codebase-memory-ui.yml +# set CODE_ROOT in .env first (host path of your repos), then: docker compose --profile codebase-memory up -d --build ``` -Then browse **`https://:8443/`** (Google SSO). Index a repo first -(via Hermes `index_repository`, or the UI's own "index" action) or the graph will be empty. +Then browse **`https:///codebase-memory/`** (Google SSO). Index a +repo first (the UI's "index" action, or `POST /codebase-memory/rpc` `index_repository`) +or the graph will be empty. ## Note The UI exposes server actions (`/api/index`, `/api/process-kill`, …) to the browser; it's diff --git a/codebase-memory-ui/entrypoint.sh b/codebase-memory-ui/entrypoint.sh index 3e7b18e..7c62378 100644 --- a/codebase-memory-ui/entrypoint.sh +++ b/codebase-memory-ui/entrypoint.sh @@ -1,15 +1,16 @@ #!/bin/sh -# Start the codebase-memory 3D graph UI as a long-lived service. +# Start the codebase-memory 3D graph UI as a long-lived service, served under the +# /codebase-memory/ subpath by nginx (which rewrites the SPA's absolute paths). set -eu CACHE_DIR="${CBM_CACHE_DIR:-/cache}" mkdir -p "$CACHE_DIR" -# The UI HTTP server binds 127.0.0.1:9749 only. Bridge it to 0.0.0.0:9750 so the -# reverse proxy (Caddy) in another container can reach it. -socat TCP-LISTEN:9750,fork,reuseaddr,bind=0.0.0.0 TCP:127.0.0.1:9749 & +# The UI binary is an MCP stdio server with the graph UI as a side thread (binds +# 127.0.0.1:9749). With no MCP client attached it would read EOF on stdin and exit, +# so keep stdin open (`tail -f /dev/null`) to hold the process — and the UI — up. +( tail -f /dev/null | codebase-memory-mcp --ui=true --port=9749 ) & -# The binary is an MCP stdio server with the UI as a side thread; with no MCP -# client attached it would read EOF on stdin and exit. Keep stdin open so the -# process (and the UI) stays alive. `tail -f /dev/null` never closes the pipe. -exec sh -c 'tail -f /dev/null | codebase-memory-mcp --ui=true --port=9749' +# nginx (foreground = container lifecycle) proxies /codebase-memory/* -> the UI on +# 127.0.0.1:9749 and rewrites /assets,/api,/rpc,/font-files to the subpath. +exec nginx -g 'daemon off;' diff --git a/codebase-memory-ui/nginx.conf b/codebase-memory-ui/nginx.conf new file mode 100644 index 0000000..4162f9d --- /dev/null +++ b/codebase-memory-ui/nginx.conf @@ -0,0 +1,52 @@ +# Serves the codebase-memory 3D graph UI under the /codebase-memory/ subpath. +# +# The upstream UI is an absolute-asset SPA (it requests /assets, /api, /rpc, +# /font-files at the ORIGIN ROOT) and has no base-path option, so it can't share +# the :443 root with Open WebUI (which owns /assets). nginx proxies the UI under +# /codebase-memory/ and rewrites those baked absolute paths to the prefix so every +# request stays under /codebase-memory/* (no collision, single SSO origin). +# +# NOTE: the sub_filter rules below match the literal paths in the pinned v0.8.1 +# bundle — re-verify them when CBM_VERSION is bumped (curl the served JS and grep +# for any remaining bare "/api or "/assets). +worker_processes 1; +pid /run/nginx.pid; +events { worker_connections 256; } + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + access_log off; + sendfile on; + gzip on; + gzip_types text/css application/javascript application/json; + + server { + listen 9750; + server_name _; + + location = /codebase-memory { return 301 /codebase-memory/; } + + location /codebase-memory/ { + # trailing slash strips the /codebase-memory/ prefix before the upstream + proxy_pass http://127.0.0.1:9749/; + proxy_http_version 1.1; + proxy_set_header Host $host; + # disable upstream compression so sub_filter can rewrite the body + proxy_set_header Accept-Encoding ""; + + sub_filter_once off; + sub_filter_types text/html application/javascript application/json text/css; + # HTML asset refs (src="/assets/...", href="/assets/...") + sub_filter '"/assets/' '"/codebase-memory/assets/'; + sub_filter "'/assets/" "'/codebase-memory/assets/"; + # JS fetch() calls — double-quote, single-quote and template-literal forms + sub_filter '"/api/' '"/codebase-memory/api/'; + sub_filter "'/api/" "'/codebase-memory/api/"; + sub_filter '`/api/' '`/codebase-memory/api/'; + sub_filter '"/rpc"' '"/codebase-memory/rpc"'; + sub_filter '"/font-files/' '"/codebase-memory/font-files/'; + sub_filter "'/font-files/" "'/codebase-memory/font-files/"; + } + } +} diff --git a/dashboard/services_catalog.py b/dashboard/services_catalog.py index 5a01d76..343d31b 100644 --- a/dashboard/services_catalog.py +++ b/dashboard/services_catalog.py @@ -35,17 +35,15 @@ {"id": "hermes", "name": "Hermes Agent", "port": 9119, "url": "http://localhost:9119", "check": "http://hermes-dashboard:9119/", "has_gpu": False, "hint": "Managed by docker compose. Logs: docker compose logs hermes-dashboard"}, - # Opt-in (--profile codebase-memory). 3D code knowledge-graph visualization. No host - # port — reached via Caddy's SSO :8443 listener; the UI binds 127.0.0.1 inside the - # container and is bridged to :9750 (what the dashboard health-checks). `url` carries - # the https scheme + :8443 port (the frontend's serviceOpenHref keeps scheme/port and - # swaps in the dashboard's hostname) — the `localhost` placeholder is replaced at render - # time, so the "Open" link becomes https://:8443/. Plain http here would hit - # the TLS listener and fail with "Client sent an HTTP request to an HTTPS server". - {"id": "codebase-memory-ui", "name": "Codebase Memory", "port": 8443, - "url": "https://localhost:8443", "check": "http://codebase-memory-ui:9750/", "has_gpu": False, - "hint": "3D code knowledge-graph. Open via SSO at https://:8443/. " - "In-memory index — re-index after a restart. Opt-in: --profile codebase-memory + overrides/codebase-memory-ui.yml"}, + # Opt-in (--profile codebase-memory). 3D code knowledge-graph visualization, served at + # https:///codebase-memory/ on the shared :443 SSO origin (the codebase-memory-ui + # container's nginx serves it under that subpath). The "Open" link comes from SSO_ROUTES + # in the frontend (-> /codebase-memory/), so no `url` is needed. The health check hits the + # nginx subpath, which proxies through to the UI. + {"id": "codebase-memory-ui", "name": "Codebase Memory", "port": 9750, + "check": "http://codebase-memory-ui:9750/codebase-memory/", "has_gpu": False, + "hint": "3D code knowledge-graph. Open at https:///codebase-memory/ (Google SSO). " + "In-memory index — re-index after a restart. Opt-in: --profile codebase-memory"}, ] diff --git a/dashboard/static/index.html b/dashboard/static/index.html index 873cddc..1a6a4ef 100644 --- a/dashboard/static/index.html +++ b/dashboard/static/index.html @@ -2240,6 +2240,7 @@

Dashboard login

webui: '/', n8n: '/n8n/', hermes: '/hermes/', + 'codebase-memory-ui': '/codebase-memory/', }; /** Prefer API `url` (paths + query) and match dashboard hostname. */ diff --git a/docker-compose.yml b/docker-compose.yml index 68bde56..415eddb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -594,14 +594,13 @@ services: command: ["true"] profiles: ["codebase-memory"] - # Codebase-Memory 3D graph visualization UI (long-lived; opt-in). Serves the - # interactive knowledge-graph from the SAME index the headless codebase-memory - # MCP writes (shared `codebase-memory-cache` volume). The upstream UI binds - # 127.0.0.1:9749 only; the image's entrypoint bridges it to 0.0.0.0:9750 via - # socat so Caddy can reach it. Exposed via Caddy + oauth2-proxy SSO on a - # dedicated :8443 listener (it's an absolute-asset SPA that must own its origin; - # add overrides/codebase-memory-ui.yml to publish the port). Build with: - # docker compose --profile codebase-memory build codebase-memory-ui + # Codebase-Memory 3D graph visualization UI (long-lived; opt-in). Visualizes the + # code knowledge-graph it indexes in its own process (mounts /c/dev:ro). The + # upstream UI binds 127.0.0.1:9749 and is an absolute-asset SPA with no base path, + # so the image runs nginx (on :9750) which proxies to it and rewrites its baked + # /assets,/api,/rpc paths to the /codebase-memory/ prefix — letting Caddy serve it + # at https:///codebase-memory/ on the shared :443 SSO origin (no extra port). + # Build with: docker compose --profile codebase-memory build codebase-memory-ui codebase-memory-ui: build: ./codebase-memory-ui image: ordo-ai-stack-codebase-memory-ui:latest @@ -616,7 +615,7 @@ services: # graph rather than depending on the gateway-spawned MCP's index. - ${CODE_ROOT:-/c/dev}:/c/dev:ro healthcheck: - test: ["CMD-SHELL", "curl -fsS -o /dev/null http://localhost:9750/ || exit 1"] + test: ["CMD-SHELL", "curl -fsS -o /dev/null http://localhost:9750/codebase-memory/ || exit 1"] interval: 30s timeout: 5s retries: 5 @@ -816,10 +815,6 @@ services: - --upstream=static://202 - --redirect-url=https://${CADDY_TAILNET_HOSTNAME}/oauth2/callback - --whitelist-domain=.${CADDY_TAILNET_DOMAIN} - # Allow post-login redirects back to the codebase-memory-ui dedicated - # listener (Caddy :8443). The OAuth callback itself stays on :443 - # (--redirect-url above); this only whitelists the :8443 return target. - - --whitelist-domain=.${CADDY_TAILNET_DOMAIN}:8443 - --cookie-domain=.${CADDY_TAILNET_DOMAIN} - --cookie-secure=true - --cookie-samesite=lax From 8c1cea7861f3bbd045dcbf248fd7e64294fcfcb9 Mon Sep 17 00:00:00 2001 From: Hermes Bot Date: Tue, 23 Jun 2026 16:27:23 -0400 Subject: [PATCH 8/8] chore: pin codebase-memory-ui/nginx.conf to LF for the image build Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitattributes | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitattributes b/.gitattributes index 077206f..9691d15 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,6 +5,9 @@ dashboard/entrypoint.sh text eol=lf hermes/entrypoint.sh text eol=lf scripts/llamacpp/run-llama-server.sh text eol=lf +# nginx config is COPYed into the codebase-memory-ui image; keep LF so nginx +# doesn't choke on CRLF after a Windows checkout. +codebase-memory-ui/nginx.conf text eol=lf # SOPS-encrypted files must keep LF endings or `sops` chokes parsing the # embedded timestamp metadata. The failure surfaces as