Initial curated ITWorx_MCP_Hub source from 368fba691ea8d23403b4d56b53b5ec670d7c5020

This commit is contained in:
Public Source Publisher committed 2026-10-01 01:36:21 +02:00
commit cb5be898ea
663 files changed
+82674

No files matched your search

+114
View File
@@ -0,0 +1,114 @@
# SAFE TEMPLATE ONLY. Copy outside Git and replace every REPLACE_* value.
# This file contains references and ordinary configuration, never credentials.
# Operator-provided host paths and identity
UNRAID_HOST=REPLACE_WITH_UNRAID_LAN_HOST_OR_IP
APPDATA_ROOT=/srv/example/appdata/itworx-mcp-hub
SECRETS_DIR=/srv/example/appdata/itworx-mcp-hub/secrets
PROJECTS_READONLY_ROOT=/srv/example/appdata/itworx-mcp-hub/sources
KNOWLEDGE_SOURCES_JSON=[{"project":"REPLACE_PROJECT","sourceRef":"repository","path":"REPLACE_RELATIVE_SOURCE_PATH","sourceType":"documentation","revision":"REPLACE_WITH_IMMUTABLE_COMMIT"}]
BACKUP_ROOT=/srv/example/appdata/itworx-mcp-hub/backups
PUID=REPLACE_WITH_NUMERIC_UID
PGID=REPLACE_WITH_NUMERIC_GID
DOCKER_SOCKET_GID=REPLACE_WITH_DOCKER_SOCKET_NUMERIC_GID
# Operator-provided bind addresses and generated Compose ports
HUB_BIND_IP=REPLACE_WITH_UNRAID_LAN_IP
MCP_BIND_IP=REPLACE_WITH_UNRAID_LAN_IP
HUB_PORT=1100
MCP_PORT=18442
INSPECTOR_BIND_IP=REPLACE_ONLY_IF_INSPECTOR_IS_ENABLED
INSPECTOR_PORT=18443
# Registry references. REGISTRY_CREDENTIAL_REF names an external credential
# helper/login context; it is not a password.
REGISTRY_HOST=REPLACE_WITH_REGISTRY_HOST
REGISTRY_NAMESPACE=REPLACE_WITH_REGISTRY_NAMESPACE
REGISTRY_CREDENTIAL_REF=REPLACE_WITH_OPERATOR_CREDENTIAL_REFERENCE
# Immutable owned images: registry/path:tag@sha256:<64 lowercase hex>.
HUB_CONTROL_IMAGE=REPLACE_WITH_IMMUTABLE_IMAGE
HUB_WORKER_IMAGE=REPLACE_WITH_IMMUTABLE_IMAGE
HUB_DATAPLANE_IMAGE=REPLACE_WITH_IMMUTABLE_IMAGE
HUB_WEB_IMAGE=REPLACE_WITH_IMMUTABLE_IMAGE
CONNECTOR_KNOWLEDGE_IMAGE=REPLACE_WITH_IMMUTABLE_IMAGE
CONNECTOR_GITEA_IMAGE=REPLACE_WITH_IMMUTABLE_IMAGE
CONNECTOR_UNRAID_IMAGE=REPLACE_WITH_IMMUTABLE_IMAGE
CONNECTOR_PROJECTBRAIN_IMAGE=REPLACE_WITH_IMMUTABLE_IMAGE
CONNECTOR_APPOPS_IMAGE=REPLACE_WITH_IMMUTABLE_IMAGE
CONNECTOR_STITCH_IMAGE=REPLACE_WITH_IMMUTABLE_IMAGE
DOCKER_READ_BROKER_IMAGE=REPLACE_WITH_IMMUTABLE_IMAGE
OBSERVABILITY_COLLECTOR_IMAGE=REPLACE_WITH_IMMUTABLE_IMAGE
# Authentik/OIDC ordinary configuration. Secret values live only in
# $SECRETS_DIR/authentik_oidc_client_secret and hub_session_signing_key.
HUB_OIDC_ISSUER=https://REPLACE_WITH_AUTHENTIK_HOST/application/o/itworx/
HUB_OIDC_AUDIENCE=itworx-mcp-hub
HUB_OIDC_CLIENT_ID=REPLACE_WITH_AUTHENTIK_CLIENT_ID
HUB_OIDC_REDIRECT_URI=https://REPLACE_WITH_MCP_HOST/auth/callback
HUB_OIDC_ROLE_GROUP_MAP={"REPLACE_WITH_ADMIN_GROUP":"admin","REPLACE_WITH_OPERATOR_GROUP":"operator"}
HUB_MCP_ADDITIONAL_OIDC_ISSUER=https://REPLACE_WITH_AUTHENTIK_HOST/application/o/chatgpt-itworx-gitea/
HUB_MCP_ADDITIONAL_OIDC_AUDIENCE=chatgpt-itworx-gitea
HUB_MCP_ADDITIONAL_OIDC_CLIENT_ID=chatgpt-itworx-gitea
HUB_MCP_ADDITIONAL_OIDC_REDIRECT_URI=https://chatgpt.com/connector/oauth/REPLACE_WITH_CONNECTOR_ID
HUB_PUBLIC_MCP_URL=https://REPLACE_WITH_MCP_HOST
HUB_RATE_LIMIT_PER_MINUTE=600
HUB_UPSTREAM_MAX_CONCURRENCY=24
HUB_UPSTREAM_QUEUE_TIMEOUT_SECONDS=5
AUTHENTIK_PROVIDER_ID=REPLACE_WITH_PROVIDER_ID
AUTHENTIK_HUB_APPLICATION_ID=REPLACE_WITH_APPLICATION_ID
AUTHENTIK_SCOPES=openid,profile,email,groups,offline_access
AUTHENTIK_ALLOWED_GROUPS=REPLACE_WITH_COMMA_SEPARATED_GROUPS
AUTHENTIK_SERVICE_ACCOUNTS=["REPLACE_WITH_SCOPED_SERVICE_ACCOUNT"]
MCP_CLIENT_BINDINGS_JSON=[{"clientId":"REPLACE_WITH_UUID","subject":"REPLACE_WITH_OIDC_SUBJECT","project":"REPLACE_PROJECT","environment":"prod","enabled":true}]
# ContextForge internal endpoint/configuration. Credentials and key material
# live in the named files under SECRETS_DIR.
HUB_CONTEXTFORGE_URL=http://contextforge-control:4444
CONTEXTFORGE_DB_HOST=postgres
CONTEXTFORGE_DB_NAME=contextforge
CONTEXTFORGE_DB_USER=contextforge
REDIS_AUTH_ENABLED=true
# Existing Gitea; this deploys only the connector, never another Gitea server.
GITEA_ENDPOINT=https://REPLACE_WITH_GITEA_HOST
GITEA_REPOSITORIES_JSON=[{"project":"REPLACE_PROJECT","owner":"REPLACE_OWNER","name":"REPLACE_REPOSITORY"}]
GITEA_ADDITIONAL_PROJECTS=gitea-loop
GITEA_AUTO_DISCOVER_PROJECT=
GITEA_WRITE_OWNER=
GITEA_TOKEN_SCOPE=read:repository
GITEA_WRITE_TOKEN_SCOPE=write:repository,write:issue
GITEA_ENABLE_PR_WRITES=true
# Existing ProjectBrain; the Hub uses a dedicated server-to-server secret file.
PROJECTBRAIN_ENDPOINT=https://projectbrain.example.com
PROJECTBRAIN_PROJECTS=gitea-all
# Existing private AppOps MCP; the Hub reuses its dedicated MCP service token.
APPOPS_ENDPOINT=http://198.18.10.150:1216
APPOPS_PROJECTS=server-ops
# Google Stitch remote MCP. The API key lives only in $SECRETS_DIR/stitch_api_key.
STITCH_HUB_PROJECTS=stitch-design
# Existing NPM and DNS inputs. NETWORK_TARGETS_JSON is checked only with the
# explicit --verify-network-targets flag.
#
# Production uses a single public hostname (see
# infrastructure/nginx-proxy-manager/ROUTING.md): one NPM proxy host and one
# DNS record for REPLACE_WITH_MCP_HOST, routed by path (default location ->
# web:8080, "/mcp" custom location -> dataplane-router:8080). The *_HUB_*
# variables below are the management-surface half of that single-host
# routing config, not a second host -- set them to the exact same hostname
# value as the *_MCP_* variables. Never set a distinct hub.example.com value.
NPM_NETWORK_NAME=REPLACE_WITH_EXISTING_NPM_NETWORK
NPM_HUB_PROXY_HOST=REPLACE_WITH_MCP_HOST
NPM_MCP_PROXY_HOST=REPLACE_WITH_MCP_HOST
NPM_CERTIFICATE_REF=REPLACE_WITH_CERTIFICATE_REFERENCE
DNS_HUB_HOSTNAME=REPLACE_WITH_MCP_HOST
DNS_MCP_HOSTNAME=REPLACE_WITH_MCP_HOST
DNS_EXPECTED_ADDRESSES_JSON={"REPLACE_WITH_MCP_HOST":["REPLACE_WITH_INGRESS_IP"]}
NETWORK_TARGETS_JSON={"registry":"REPLACE_WITH_REGISTRY_HOST:443","gitea":"REPLACE_WITH_GITEA_HOST:443"}
# Generated during deployment; do not hand-edit into credentials.
UNRAID_REGISTRATIONS_FILE=/srv/example/appdata/itworx-mcp-hub/config/registered-targets.json
@@ -0,0 +1,52 @@
# Docker Compose implementation requirements
`docker-compose.blueprint.yml` is an executable architecture blueprint, not a release manifest. `WP-020` must replace every `:?` image variable with an exact tag **and digest**, build local images, and generate a validated production override.
## Compose project
- fixed project name: `itworx-mcp-hub`;
- dedicated appdata root, default proposal: `/srv/example/appdata/itworx-mcp-hub`;
- no modification of unrelated containers, networks, volumes, or proxy hosts;
- no service uses `network_mode: host`;
- only the explicit management, dataplane, and optional Inspector bindings may publish host ports;
- PostgreSQL, Redis, ContextForge, workers, connectors, and broker remain internal;
- all local services run non-root, drop Linux capabilities, use `no-new-privileges`, and use a read-only root filesystem where practical;
- each service has bounded memory/CPU/logging and a real healthcheck;
- startup ordering uses health conditions but applications must still tolerate dependency restarts;
- Redis is non-authoritative and may be emptied without data loss;
- the Docker socket is mounted only into `docker-read-broker`, read-only;
- target-system credentials are Compose secrets or equivalent mounted files, never normal environment values;
- release deployments use immutable image digests and record them in release evidence.
## Required profiles
- default/core: production Hub services;
- `project-connectors`: optional connectors requiring external project credentials;
- `inspector`: MCP Inspector bound to an explicitly configured LAN address only;
- `observability`: optional OTel Collector/Grafana stack if not reusing existing services;
- `fake`: deterministic fake targets for clean-room acceptance.
## Required generated outputs in WP-020
- `docker-compose.yml` or an equivalent production base;
- Unraid-specific override;
- development/fake override;
- exact `.env.example` without secrets;
- all Dockerfiles and healthchecks;
- `docker compose config` evidence;
- image pin/digest inventory;
- network and published-port assertion tests.
## Production build-fallback (WP-200 / DIAG-02)
`docker-compose.prod.yml` must cancel every owned service's inherited `build:`
block using the Compose Specification's `!reset` YAML tag — `build: !reset
null` — never plain `build: null`. Plain `null` is not treated as "delete
this key" by the real Compose merge engine; the base file's `build:` mapping
survives and `docker compose up -d` will attempt a local build (and fail in a
confusing way, or in the worst case succeed and deploy an unscanned image)
if a registry pull ever fails. `tests/test_compose.py`'s
`test_real_compose_config_has_no_build_for_owned_services` enforces this
against the real `docker compose config` CLI output; any new owned service
added to `docker-compose.prod.yml` must be added to that test's service list
and use the same `!reset` pattern.
@@ -0,0 +1,17 @@
name: itworx-mcp-hub
services:
postgres:
volumes: [postgres-data-dev:/var/lib/postgresql/data]
contextforge:
environment:
HOST: 0.0.0.0
PORT: 4444
AUTH_REQUIRED: "true"
MCPGATEWAY_UI_ENABLED: "false"
MCPGATEWAY_ADMIN_API_ENABLED: "false"
inspector:
profiles: [inspector]
volumes:
postgres-data-dev: {name: itworx-mcp-hub-postgres-dev}
+528
View File
@@ -0,0 +1,528 @@
name: itworx-mcp-hub
x-owned-service: &owned-service
restart: unless-stopped
read_only: true
cap_drop: ["ALL"]
security_opt: ["no-new-privileges:true"]
tmpfs: ["/tmp:size=64m,mode=1777"]
pids_limit: 128
mem_limit: 512m
cpus: 1.0
logging:
driver: json-file
options: {max-size: "10m", max-file: "3"}
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/healthz', timeout=2)"]
interval: 60s
timeout: 3s
retries: 5
start_period: 10s
x-third-party: &third-party
restart: unless-stopped
security_opt: ["no-new-privileges:true"]
logging:
driver: json-file
options: {max-size: "10m", max-file: "3"}
services:
postgres:
<<: *third-party
image: postgres:18.4@sha256:3a82e1f56c8f0f5616a11103ac3d47e632c3938698946a7ad26da0df1334744a
entrypoint: ["sh", "/opt/itworx/postgres-entrypoint.sh"]
command: ["postgres"]
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD_FILE: /run/secrets/postgres_superuser_password
PGDATA: /var/lib/postgresql/data/pgdata
SECRET_EXPECTED_UID: "${PUID:-1000}"
SECRET_EXPECTED_GID: "${PGID:-1000}"
secrets: [postgres_superuser_password, hub_database_password, contextforge_database_password]
volumes:
- postgres-data:/var/lib/postgresql/data
- ../postgres/init:/docker-entrypoint-initdb.d:ro
- ../images/postgres/entrypoint.sh:/opt/itworx/postgres-entrypoint.sh:ro
tmpfs: ["/run/itworx-secrets:size=1m,mode=0700"]
networks: [hub-data]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"]
interval: 15s
timeout: 5s
retries: 5
redis:
<<: *third-party
image: redis:8.10.0@sha256:c29e49ab2f85760a3827b53882e6dd9f5c6c3f0bb7d724e07bb31cbf275a5236
entrypoint: ["sh", "/opt/itworx/redis-entrypoint.sh"]
environment:
REDIS_AUTH_ENABLED: "${REDIS_AUTH_ENABLED:-false}"
SECRET_EXPECTED_UID: "${PUID:-1000}"
SECRET_EXPECTED_GID: "${PGID:-1000}"
volumes:
- ../images/redis/entrypoint.sh:/opt/itworx/redis-entrypoint.sh:ro
- ../images/redis/healthcheck.sh:/opt/itworx/redis-healthcheck.sh:ro
secrets: [redis_password]
tmpfs: ["/run/itworx:size=1m,mode=0700"]
networks: [hub-data]
healthcheck:
test: ["CMD", "sh", "/opt/itworx/redis-healthcheck.sh"]
interval: 15s
timeout: 3s
retries: 5
contextforge:
<<: *third-party
image: ghcr.io/ibm/mcp-context-forge:v1.0.6@sha256:049b8e61fa1187f72773d94bcf91fc63338f4fea7808949a8a5daacbc89ef92e
user: "${PUID:-1000}:${PGID:-1000}"
entrypoint: ["sh", "/opt/itworx/contextforge-entrypoint.sh"]
environment:
HOST: 0.0.0.0
PORT: 4444
AUTH_REQUIRED: "true"
MCPGATEWAY_UI_ENABLED: "false"
MCPGATEWAY_ADMIN_API_ENABLED: "false"
MCPGATEWAY_MCP_APPS_ENABLED: "true"
ENABLE_HEADER_PASSTHROUGH: "true"
API_ALLOW_BASIC_AUTH: "false"
PLATFORM_ADMIN_EMAIL: "${CONTEXTFORGE_PLATFORM_ADMIN_EMAIL:-contextforge-admin@example.com}"
SSRF_PROTECTION_ENABLED: "true"
SSRF_ALLOW_PRIVATE_NETWORKS: "false"
SSRF_DNS_FAIL_CLOSED: "true"
SSRF_ALLOWED_NETWORKS: '["203.0.113.0/24"]'
UAID_ALLOWED_DOMAINS: '["example.com"]'
GUNICORN_WORKERS: "2"
# ContextForge only receives authenticated internal Hub traffic. Its
# generic limiter groups every caller behind the router's single IP and
# can lock out all ChatGPT sessions at once. Admission control therefore
# belongs to the public dataplane router below.
RATE_LIMITING_ENABLED: "false"
REDIS_AUTH_ENABLED: "${REDIS_AUTH_ENABLED:-false}"
SECRET_EXPECTED_UID: "${PUID:-1000}"
SECRET_EXPECTED_GID: "${PGID:-1000}"
volumes:
- ../images/contextforge/entrypoint.sh:/opt/itworx/contextforge-entrypoint.sh:ro
secrets: [contextforge_database_password, contextforge_auth_encryption_secret, contextforge_jwt_secret, contextforge_admin_credential, redis_password]
depends_on:
postgres: {condition: service_healthy}
redis: {condition: service_healthy}
networks:
hub-control:
aliases: [contextforge-control]
hub-connectors: {}
hub-data: {}
hub-observe: {}
healthcheck:
test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:4444/health', timeout=3)\""]
interval: 60s
timeout: 10s
retries: 5
start_period: 120s
control-api:
<<: *owned-service
build: {context: ../.., dockerfile: infrastructure/images/python-service/Dockerfile, args: {SERVICE_NAME: control-api}}
image: local/itworx-mcp-hub-control:dev
environment:
HUB_OIDC_ISSUER: "${HUB_OIDC_ISSUER:-https://auth.dev.invalid/application/o/itworx/}"
HUB_OIDC_AUDIENCE: "${HUB_OIDC_AUDIENCE:-itworx-mcp-hub}"
HUB_OIDC_CLIENT_ID: "${HUB_OIDC_CLIENT_ID:-itworx-mcp-hub-dev}"
HUB_OIDC_REDIRECT_URI: "${HUB_OIDC_REDIRECT_URI:-https://hub.dev.invalid/auth/callback}"
HUB_OIDC_CLIENT_SECRET_FILE: /run/secrets/authentik_oidc_client_secret
HUB_OIDC_ROLE_GROUP_MAP: '${HUB_OIDC_ROLE_GROUP_MAP:-{"hub-admin":"admin"}}'
HUB_SESSION_SIGNING_KEY_FILE: /run/secrets/hub_session_signing_key
HUB_CONTEXTFORGE_URL: "${HUB_CONTEXTFORGE_URL:-http://contextforge-control:4444}"
HUB_CONTEXTFORGE_TOKEN_FILE: /run/secrets/contextforge_api_token
HUB_CONTEXTFORGE_AUTH_SCHEME: Bearer
HUB_DATABASE_PASSWORD_FILE: /run/secrets/hub_database_password
HUB_DATABASE_HOST: postgres
HUB_DATABASE_NAME: itworx_mcp_hub
HUB_DATABASE_USER: itworx_mcp_hub
HUB_CATALOG_ROOT: /opt/hub/catalog
INGESTION_CONNECTOR_ENABLED: "${INGESTION_CONNECTOR_ENABLED:-false}"
HUB_PUBLIC_MCP_URL: "${HUB_PUBLIC_MCP_URL:-https://mcp.example.com}"
HUB_BACKUP_STATUS_FILE: /var/lib/itworx/backup-status.json
HUB_BACKUP_STALE_AFTER_HOURS: "${HUB_BACKUP_STALE_AFTER_HOURS:-26}"
CONNECTOR_IDENTITY_KEY_FILE: /run/secrets/connector_identity_signing_key
REQUIRED_SECRET_FILES: /run/secrets/authentik_oidc_client_secret,/run/secrets/hub_session_signing_key,/run/secrets/contextforge_api_token,/run/secrets/hub_database_password,/run/secrets/connector_identity_signing_key
SECRET_EXPECTED_UID: "${PUID:-10001}"
SECRET_EXPECTED_GID: "${PGID:-10001}"
secrets: [authentik_oidc_client_secret, hub_session_signing_key, contextforge_api_token, hub_database_password, connector_identity_signing_key]
volumes:
- "${BACKUP_STATUS_FILE_HOST:-../../examples/backup-status.json}:/var/lib/itworx/backup-status.json:ro"
- "../../catalog:/opt/hub/catalog:ro"
depends_on:
postgres: {condition: service_healthy}
redis: {condition: service_healthy}
contextforge: {condition: service_healthy}
networks: [hub-control, hub-connectors, hub-data, hub-observe, hub-egress]
worker:
<<: *owned-service
user: "${PUID:-10001}:${PGID:-10001}"
build: {context: ../.., dockerfile: infrastructure/images/python-service/Dockerfile, args: {SERVICE_NAME: worker}}
image: local/itworx-mcp-hub-worker:dev
environment:
HUB_OIDC_ISSUER: "${HUB_OIDC_ISSUER:-https://auth.dev.invalid/application/o/itworx/}"
HUB_CONTEXTFORGE_URL: "${HUB_CONTEXTFORGE_URL:-http://contextforge-control:4444}"
HUB_CONTEXTFORGE_TOKEN_FILE: /run/secrets/contextforge_api_token
HUB_CONTEXTFORGE_AUTH_SCHEME: Bearer
HUB_DATABASE_PASSWORD_FILE: /run/secrets/hub_database_password
HUB_DATABASE_HOST: postgres
HUB_DATABASE_NAME: itworx_mcp_hub
HUB_DATABASE_USER: itworx_mcp_hub
HUB_CATALOG_ROOT: /opt/hub/catalog
INGESTION_CONNECTOR_ENABLED: "${INGESTION_CONNECTOR_ENABLED:-false}"
MCP_CLIENT_BINDINGS_JSON: '${MCP_CLIENT_BINDINGS_JSON:-[]}'
REQUIRED_SECRET_FILES: /run/secrets/hub_database_password,/run/secrets/contextforge_api_token
SECRET_EXPECTED_UID: "${PUID:-10001}"
SECRET_EXPECTED_GID: "${PGID:-10001}"
secrets: [hub_database_password, contextforge_api_token]
volumes:
- "../../catalog:/opt/hub/catalog:ro"
- "../../services/runtime/state_sync.py:/opt/hub/services/runtime/state_sync.py:ro"
depends_on:
control-api: {condition: service_healthy}
redis: {condition: service_healthy}
connector-knowledge: {condition: service_healthy}
connector-gitea: {condition: service_healthy}
connector-unraid: {condition: service_healthy}
connector-mobilityops: {condition: service_healthy}
connector-projectbrain: {condition: service_healthy}
connector-appops: {condition: service_healthy}
connector-stitch: {condition: service_healthy}
networks: [hub-control, hub-connectors, hub-data, hub-observe]
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/health/ready', timeout=2)"]
interval: 10s
timeout: 3s
retries: 18
start_period: 10s
dataplane-router:
<<: *owned-service
# Docker bind-mounted secret files retain the Unraid PUID/PGID. Run the
# router as that already unprivileged identity so its fail-closed secret
# validation can read files that remain owner-only (0600).
user: "${PUID:-10001}:${PGID:-10001}"
build: {context: ../.., dockerfile: infrastructure/images/python-service/Dockerfile, args: {SERVICE_NAME: dataplane-router}}
image: local/itworx-mcp-hub-dataplane:dev
environment:
HUB_OIDC_ISSUER: "${HUB_OIDC_ISSUER:-https://auth.dev.invalid/application/o/itworx/}"
HUB_OIDC_AUDIENCE: "${HUB_OIDC_AUDIENCE:-itworx-mcp-hub}"
HUB_OIDC_CLIENT_ID: "${HUB_OIDC_CLIENT_ID:-itworx-mcp-hub-dev}"
HUB_OIDC_REDIRECT_URI: "${HUB_OIDC_REDIRECT_URI:-https://hub.dev.invalid/auth/callback}"
HUB_MCP_ADDITIONAL_OIDC_ISSUER: "${HUB_MCP_ADDITIONAL_OIDC_ISSUER:-}"
HUB_MCP_ADDITIONAL_OIDC_AUDIENCE: "${HUB_MCP_ADDITIONAL_OIDC_AUDIENCE:-}"
HUB_MCP_ADDITIONAL_OIDC_CLIENT_ID: "${HUB_MCP_ADDITIONAL_OIDC_CLIENT_ID:-}"
HUB_MCP_ADDITIONAL_OIDC_REDIRECT_URI: "${HUB_MCP_ADDITIONAL_OIDC_REDIRECT_URI:-}"
HUB_PUBLIC_MCP_URL: "${HUB_PUBLIC_MCP_URL:-https://mcp.example.com}"
HUB_CONTEXTFORGE_URL: "${HUB_CONTEXTFORGE_URL:-http://contextforge-control:4444}"
HUB_CONTEXTFORGE_TOKEN_FILE: /run/secrets/contextforge_api_token
HUB_CONTEXTFORGE_AUTH_SCHEME: Bearer
HUB_DATABASE_PASSWORD_FILE: /run/secrets/hub_database_password
HUB_DATABASE_HOST: postgres
HUB_DATABASE_NAME: itworx_mcp_hub
HUB_DATABASE_USER: itworx_mcp_hub
HUB_CATALOG_ROOT: /opt/hub/catalog
HUB_RATE_LIMIT_PER_MINUTE: "${HUB_RATE_LIMIT_PER_MINUTE:-600}"
HUB_UPSTREAM_MAX_CONCURRENCY: "${HUB_UPSTREAM_MAX_CONCURRENCY:-24}"
HUB_UPSTREAM_QUEUE_TIMEOUT_SECONDS: "${HUB_UPSTREAM_QUEUE_TIMEOUT_SECONDS:-5}"
CONNECTOR_IDENTITY_KEY_FILE: /run/secrets/connector_identity_signing_key
INGESTION_CONNECTOR_ENABLED: "${INGESTION_CONNECTOR_ENABLED:-false}"
DEMO_TOKEN_KEY_FILE: /run/secrets/demo_token_signing_key
DEMO_TOKEN_REVOCATIONS_FILE: /etc/itworx/demo-token-revocations.json
DEMO_TOKEN_RATE_LIMIT_PER_MINUTE: "${DEMO_TOKEN_RATE_LIMIT_PER_MINUTE:-10}"
REQUIRED_SECRET_FILES: /run/secrets/contextforge_api_token,/run/secrets/hub_database_password,/run/secrets/connector_identity_signing_key,/run/secrets/demo_token_signing_key
SECRET_EXPECTED_UID: "${PUID:-10001}"
SECRET_EXPECTED_GID: "${PGID:-10001}"
secrets: [contextforge_api_token, hub_database_password, connector_identity_signing_key, demo_token_signing_key]
volumes:
- "../../catalog:/opt/hub/catalog:ro"
- "../../services/runtime/dataplane.py:/opt/hub/services/runtime/dataplane.py:ro"
# Never silently fall back to the repository example: that would make
# the operator's early-revocation file ineffective.
# Mount the containing directory: the operator verifier atomically replaces
# the file and therefore needs a writable parent directory in its profile.
- "${DEMO_TOKEN_REVOCATIONS_DIR_HOST:?Set the host-side demo-token revocations directory}:/etc/itworx:ro"
depends_on: {postgres: {condition: service_healthy}, contextforge: {condition: service_healthy}}
networks: [hub-edge, hub-control, hub-data, hub-observe, hub-egress]
ports: ["${MCP_BIND_IP:-127.0.0.1}:${MCP_PORT:-18442}:8080"]
operator-tools:
# Explicit one-shot profile for operator-only proofs. It receives no Docker
# socket and does not start as part of the normal application topology.
image: local/itworx-mcp-hub-dataplane:dev
profiles: [operator-tools]
user: "${PUID:-10001}:${PGID:-10001}"
working_dir: /opt/hub
entrypoint: ["python", "-m", "scripts.verify_demo_token_flow"]
secrets: [demo_token_signing_key]
volumes:
- "${DEMO_TOKEN_REVOCATIONS_DIR_HOST:?Set the host-side demo-token revocations directory}:/etc/itworx:rw"
networks: [hub-edge]
web:
<<: *owned-service
build: {context: ../.., dockerfile: infrastructure/images/python-service/Dockerfile, args: {SERVICE_NAME: web}}
image: local/itworx-mcp-hub-web:dev
environment:
HUB_PUBLIC_MCP_URL: "${HUB_PUBLIC_MCP_URL:-https://mcp.example.com}"
HUB_MCP_ADDITIONAL_OIDC_ISSUER: "${HUB_MCP_ADDITIONAL_OIDC_ISSUER:-https://auth.example-host.com/application/o/chatgpt-itworx-gitea/}"
HUB_WORKER_URL: "http://worker:8080"
volumes:
- "../../services/runtime/web.py:/opt/hub/services/runtime/web.py:ro"
depends_on:
control-api: {condition: service_healthy}
worker: {condition: service_healthy}
networks: [hub-edge, hub-control, hub-observe]
ports: ["${HUB_BIND_IP:-127.0.0.1}:${HUB_PORT:-1100}:8080"]
connector-knowledge:
<<: *owned-service
build: {context: ../.., dockerfile: infrastructure/images/python-service/Dockerfile, args: {SERVICE_NAME: connector-knowledge}}
image: local/itworx-mcp-hub-connector-knowledge:dev
volumes:
- "${PROJECTS_READONLY_ROOT:-../../}:/sources:ro"
environment:
KNOWLEDGE_SOURCES_JSON: '${KNOWLEDGE_SOURCES_JSON:-[]}'
CONNECTOR_IDENTITY_KEY_FILE: /run/secrets/connector_identity_signing_key
REQUIRED_SECRET_FILES: /run/secrets/connector_identity_signing_key
SECRET_EXPECTED_UID: "${PUID:-10001}"
SECRET_EXPECTED_GID: "${PGID:-10001}"
secrets: [connector_identity_signing_key]
networks: [hub-connectors, hub-observe]
connector-ingestion:
<<: *owned-service
profiles: [knowledge-intake]
# Keep /tmp writable to the non-root connector user; each intake job creates
# its own 0700 directory and 0600 file inside this shared sticky tmpfs.
tmpfs: ["/tmp:size=128m,mode=1777"]
build: {context: ../.., dockerfile: infrastructure/images/python-service/Dockerfile, args: {SERVICE_NAME: connector-ingestion}}
image: local/itworx-mcp-hub-connector-ingestion:dev
environment:
KNOWLEDGE_HUB_ENDPOINT: "${KNOWLEDGE_HUB_ENDPOINT:-https://knowledge-hub.invalid}"
KNOWLEDGE_HUB_ALLOWED_PROJECT_IDS: "${KNOWLEDGE_HUB_ALLOWED_PROJECT_IDS:-}"
KNOWLEDGE_HUB_WORKSPACE_READ_ENABLED: "${KNOWLEDGE_HUB_WORKSPACE_READ_ENABLED:-false}"
KNOWLEDGE_HUB_INGESTION_TOKEN_FILE: /run/secrets/knowledge_hub_ingestion_token
INGESTION_SCANNER_HOST: ingestion-clamd
INGESTION_SCANNER_PORT: "3310"
KNOWLEDGE_HUB_INGESTION_IDENTITY_KEY_FILE: /run/secrets/knowledge_hub_ingestion_identity_key
REQUIRED_SECRET_FILES: /run/secrets/knowledge_hub_ingestion_token,/run/secrets/knowledge_hub_ingestion_identity_key
SECRET_EXPECTED_UID: "${PUID:-10001}"
SECRET_EXPECTED_GID: "${PGID:-10001}"
secrets: [knowledge_hub_ingestion_token, knowledge_hub_ingestion_identity_key]
depends_on: {ingestion-clamd: {condition: service_started}}
networks: [hub-connectors, hub-observe, hub-egress]
ingestion-clamd:
<<: *third-party
profiles: [knowledge-intake]
image: clamav/clamav:1.4.6@sha256:a5f03c12a79dbe9f6d8a527b6bb1ea053fa8dd061d3738a26897f055ee2d9303
cap_drop: ["ALL"]
# /init must chown its signature volume, prepare log files, and drop to clamav.
cap_add: ["CHOWN", "DAC_OVERRIDE", "FOWNER", "SETUID", "SETGID"]
mem_limit: 3g
pids_limit: 128
volumes: ["ingestion-clamav-db:/var/lib/clamav"]
networks: [hub-connectors, hub-egress]
connector-gitea:
<<: *owned-service
tmpfs: ["/tmp:size=512m,mode=1777"]
build: {context: ../.., dockerfile: infrastructure/images/python-service/Dockerfile, args: {SERVICE_NAME: connector-gitea}}
image: local/itworx-mcp-hub-connector-gitea:dev
environment:
GITEA_CONNECTOR_BUILD_SHA: "${GITEA_COMMIT_SHA:-unknown}"
GITEA_ENDPOINT: "${GITEA_ENDPOINT:-https://gitea.dev.invalid}"
GITEA_TOKEN_FILE: /run/secrets/gitea_mcp_token
GITEA_ENABLE_PR_WRITES: "${GITEA_ENABLE_PR_WRITES:-false}"
GITEA_WRITE_TOKEN_FILE: /run/secrets/gitea_mcp_write_token
GITEA_WRITE_OWNER: '${GITEA_WRITE_OWNER:-}'
GITEA_REPOSITORIES_JSON: '${GITEA_REPOSITORIES_JSON:-[{"project":"dev","owner":"dev","name":"dev"}]}'
GITEA_ADDITIONAL_PROJECTS: '${GITEA_ADDITIONAL_PROJECTS:-gitea-loop}'
GITEA_AUTO_DISCOVER_PROJECT: '${GITEA_AUTO_DISCOVER_PROJECT:-}'
CONNECTOR_IDENTITY_KEY_FILE: /run/secrets/connector_identity_signing_key
REQUIRED_SECRET_FILES: /run/secrets/gitea_mcp_token,/run/secrets/gitea_mcp_write_token,/run/secrets/connector_identity_signing_key
SECRET_EXPECTED_UID: "${PUID:-10001}"
SECRET_EXPECTED_GID: "${PGID:-10001}"
secrets: [gitea_mcp_token, gitea_mcp_write_token, connector_identity_signing_key]
volumes:
- "../../services/runtime/connector_gitea.py:/opt/hub/services/runtime/connector_gitea.py:ro"
depends_on: {contextforge: {condition: service_healthy}}
networks: [hub-connectors, hub-observe, hub-egress]
connector-mobilityops:
<<: *owned-service
build: {context: ../.., dockerfile: infrastructure/images/python-service/Dockerfile, args: {SERVICE_NAME: connector-mobilityops}}
image: local/itworx-mcp-hub-connector-mobilityops:dev
environment:
MOBILITYOPS_ENDPOINT: "${MOBILITYOPS_ENDPOINT:-https://mobilityops.dev.invalid}"
MOBILITYOPS_SERVICE_TOKEN_FILE: /run/secrets/mobilityops_service_token
MOBILITYOPS_PROJECTS_JSON: '${MOBILITYOPS_PROJECTS_JSON:-["mobilityops"]}'
CONNECTOR_IDENTITY_KEY_FILE: /run/secrets/connector_identity_signing_key
REQUIRED_SECRET_FILES: /run/secrets/mobilityops_service_token,/run/secrets/connector_identity_signing_key
SECRET_EXPECTED_UID: "${PUID:-10001}"
SECRET_EXPECTED_GID: "${PGID:-10001}"
secrets: [mobilityops_service_token, connector_identity_signing_key]
depends_on: {contextforge: {condition: service_healthy}}
networks: [hub-connectors, hub-observe, hub-egress]
connector-projectbrain:
<<: *owned-service
build: {context: ../.., dockerfile: infrastructure/images/python-service/Dockerfile, args: {SERVICE_NAME: connector-projectbrain}}
image: local/itworx-mcp-hub-connector-projectbrain:dev
environment:
PROJECTBRAIN_ENDPOINT: "${PROJECTBRAIN_ENDPOINT:-https://projectbrain.dev.invalid}"
PROJECTBRAIN_PROJECTS: "${PROJECTBRAIN_PROJECTS:-gitea-all},gitea-analysis,gitea-loop"
PROJECTBRAIN_SERVICE_TOKEN_FILE: /run/secrets/projectbrain_hub_service_token
CONNECTOR_IDENTITY_KEY_FILE: /run/secrets/connector_identity_signing_key
REQUIRED_SECRET_FILES: /run/secrets/projectbrain_hub_service_token,/run/secrets/connector_identity_signing_key
SECRET_EXPECTED_UID: "${PUID:-10001}"
SECRET_EXPECTED_GID: "${PGID:-10001}"
secrets: [projectbrain_hub_service_token, connector_identity_signing_key]
depends_on: {contextforge: {condition: service_healthy}}
networks: [hub-connectors, hub-observe, hub-egress]
connector-appops:
<<: *owned-service
build: {context: ../.., dockerfile: infrastructure/images/python-service/Dockerfile, args: {SERVICE_NAME: connector-appops}}
image: local/itworx-mcp-hub-connector-appops:dev
environment:
APPOPS_ENDPOINT: "${APPOPS_ENDPOINT:-http://198.18.10.150:1216}"
APPOPS_PROJECTS: "${APPOPS_PROJECTS:-server-ops}"
APPOPS_SERVICE_TOKEN_FILE: /run/secrets/appops_hub_service_token
CONNECTOR_IDENTITY_KEY_FILE: /run/secrets/connector_identity_signing_key
REQUIRED_SECRET_FILES: /run/secrets/appops_hub_service_token,/run/secrets/connector_identity_signing_key
SECRET_EXPECTED_UID: "${PUID:-10001}"
SECRET_EXPECTED_GID: "${PGID:-10001}"
secrets: [appops_hub_service_token, connector_identity_signing_key]
depends_on: {contextforge: {condition: service_healthy}}
networks: [hub-connectors, hub-observe, hub-egress]
connector-stitch:
<<: *owned-service
build: {context: ../.., dockerfile: infrastructure/images/python-service/Dockerfile, args: {SERVICE_NAME: connector-stitch}}
image: local/itworx-mcp-hub-connector-stitch:dev
environment:
STITCH_ENDPOINT: "https://stitch.googleapis.com/mcp"
STITCH_HUB_PROJECTS: "${STITCH_HUB_PROJECTS:-stitch-design}"
STITCH_API_KEY_FILE: /run/secrets/stitch_api_key
CONNECTOR_IDENTITY_KEY_FILE: /run/secrets/connector_identity_signing_key
REQUIRED_SECRET_FILES: /run/secrets/stitch_api_key,/run/secrets/connector_identity_signing_key
SECRET_EXPECTED_UID: "${PUID:-10001}"
SECRET_EXPECTED_GID: "${PGID:-10001}"
secrets: [stitch_api_key, connector_identity_signing_key]
depends_on: {contextforge: {condition: service_healthy}}
networks: [hub-connectors, hub-observe, hub-egress]
docker-read-broker:
<<: *owned-service
build: {context: ../.., dockerfile: infrastructure/images/docker-read-broker/Dockerfile}
image: local/itworx-mcp-hub-docker-read-broker:dev
environment:
OTEL_EXPORTER_OTLP_ENDPOINT: http://observability-collector:4318
BROKER_TOKEN_FILE: /run/secrets/docker_broker_service_token
BROKER_REGISTRATIONS_FILE: /etc/itworx/registered-targets.json
SECRET_EXPECTED_UID: "${PUID:-10002}"
SECRET_EXPECTED_GID: "${PGID:-10002}"
secrets: [docker_broker_service_token]
group_add: ["${DOCKER_SOCKET_GID:-10002}"]
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "${UNRAID_REGISTRATIONS_FILE:-../unraid/registered-targets.example.json}:/etc/itworx/registered-targets.json:ro"
networks: [hub-broker, hub-observe]
connector-unraid:
<<: *owned-service
build: {context: ../.., dockerfile: infrastructure/images/python-service/Dockerfile, args: {SERVICE_NAME: connector-unraid}}
image: local/itworx-mcp-hub-connector-unraid:dev
environment:
OTEL_EXPORTER_OTLP_ENDPOINT: http://observability-collector:4318
BROKER_URL: http://docker-read-broker:8080
BROKER_TOKEN_FILE: /run/secrets/docker_broker_service_token
SECRET_EXPECTED_UID: "${PUID:-10001}"
SECRET_EXPECTED_GID: "${PGID:-10001}"
UNRAID_REGISTRATIONS_FILE: /etc/itworx/registered-targets.json
CONNECTOR_IDENTITY_KEY_FILE: /run/secrets/connector_identity_signing_key
REQUIRED_SECRET_FILES: /run/secrets/docker_broker_service_token,/run/secrets/connector_identity_signing_key
secrets: [docker_broker_service_token, connector_identity_signing_key]
volumes:
- "${UNRAID_REGISTRATIONS_FILE:-../unraid/registered-targets.example.json}:/etc/itworx/registered-targets.json:ro"
depends_on: {docker-read-broker: {condition: service_healthy}}
networks: [hub-connectors, hub-broker, hub-observe]
observability-collector:
image: local/itworx-mcp-hub-observability:dev
build: {context: ../.., dockerfile: infrastructure/images/observability/Dockerfile}
profiles: [observability]
networks: [hub-observe]
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:4318/healthz', timeout=2)"]
interval: 60s
timeout: 3s
retries: 5
inspector:
<<: *third-party
image: ghcr.io/modelcontextprotocol/inspector:2.0.0-rc.1@sha256:700500bbce937754d2ac0dd95797c3e057c8ebe2f4faf80f994652b0958a7478
profiles: [inspector]
networks: [hub-edge]
ports: ["${INSPECTOR_BIND_IP:?Set an explicit LAN-only Inspector bind address}:${INSPECTOR_PORT:-18443}:6274"]
networks:
hub-edge:
name: itworx-mcp-hub-edge
ipam: {config: [{subnet: 203.0.113.0/24}]}
hub-egress:
name: itworx-mcp-hub-egress
ipam: {config: [{subnet: 203.0.113.0/24}]}
hub-control:
name: itworx-mcp-hub-control
internal: true
ipam: {config: [{subnet: 203.0.113.0/24}]}
hub-connectors:
name: itworx-mcp-hub-connectors
internal: true
ipam:
config: [{subnet: 203.0.113.0/24}]
hub-broker:
name: itworx-mcp-hub-broker
internal: true
ipam: {config: [{subnet: 203.0.113.0/24}]}
hub-data:
name: itworx-mcp-hub-data
internal: true
ipam: {config: [{subnet: 203.0.113.0/24}]}
hub-observe:
name: itworx-mcp-hub-observe
internal: true
ipam: {config: [{subnet: 203.0.113.0/24}]}
volumes:
postgres-data: {name: itworx-mcp-hub-postgres}
ingestion-clamav-db: {name: itworx-mcp-hub-ingestion-clamav-db}
secrets:
postgres_superuser_password: {file: "${SECRETS_DIR:?Set a secret directory outside Git}/postgres_superuser_password"}
hub_database_password: {file: "${SECRETS_DIR:?Set a secret directory outside Git}/hub_database_password"}
contextforge_database_password: {file: "${SECRETS_DIR:?Set a secret directory outside Git}/contextforge_database_password"}
contextforge_auth_encryption_secret: {file: "${SECRETS_DIR:?Set a secret directory outside Git}/contextforge_auth_encryption_secret"}
contextforge_jwt_secret: {file: "${SECRETS_DIR:?Set a secret directory outside Git}/contextforge_jwt_secret"}
contextforge_admin_credential: {file: "${SECRETS_DIR:?Set a secret directory outside Git}/contextforge_admin_credential"}
contextforge_api_token: {file: "${SECRETS_DIR:?Set a secret directory outside Git}/contextforge_api_token"}
docker_broker_service_token: {file: "${SECRETS_DIR:?Set a secret directory outside Git}/docker_broker_service_token"}
connector_identity_signing_key: {file: "${SECRETS_DIR:?Set a secret directory outside Git}/connector_identity_signing_key"}
demo_token_signing_key: {file: "${SECRETS_DIR:?Set a secret directory outside Git}/demo_token_signing_key"}
authentik_oidc_client_secret: {file: "${SECRETS_DIR:?Set a secret directory outside Git}/authentik_oidc_client_secret"}
hub_session_signing_key: {file: "${SECRETS_DIR:?Set a secret directory outside Git}/hub_session_signing_key"}
gitea_mcp_token: {file: "${SECRETS_DIR:?Set a secret directory outside Git}/gitea_mcp_token"}
gitea_mcp_write_token: {file: "${SECRETS_DIR:?Set a secret directory outside Git}/gitea_mcp_write_token"}
mobilityops_service_token: {file: "${SECRETS_DIR:?Set a secret directory outside Git}/mobilityops_service_token"}
projectbrain_hub_service_token: {file: "${SECRETS_DIR:?Set a secret directory outside Git}/projectbrain_hub_service_token"}
appops_hub_service_token: {file: "${SECRETS_DIR:?Set a secret directory outside Git}/appops_hub_service_token"}
stitch_api_key: {file: "${SECRETS_DIR:?Set a secret directory outside Git}/stitch_api_key"}
knowledge_hub_ingestion_token: {file: "${SECRETS_DIR:?Set a secret directory outside Git}/knowledge_hub_ingestion_token"}
knowledge_hub_ingestion_identity_key: {file: "${SECRETS_DIR:?Set a secret directory outside Git}/knowledge_hub_ingestion_identity_key"}
redis_password: {file: "${SECRETS_DIR:?Set a secret directory outside Git}/redis_password"}
+15
View File
@@ -0,0 +1,15 @@
# Hub database migrations
The Alembic environment in this directory owns only the ITWorx MCP Hub control
database. ContextForge is a separate database/user and is intentionally absent
from the metadata import graph.
Development example:
```powershell
$env:HUB_DATABASE_URL = "sqlite:///./itworx-hub-dev.db"
alembic -c infrastructure/database/alembic.ini upgrade head
```
Production migrations are forward-only. A destructive rollback is a restore
operation after backup and health verification, not an ad-hoc downgrade.
+38
View File
@@ -0,0 +1,38 @@
[alembic]
script_location = infrastructure/database/alembic
prepend_sys_path = .
path_separator = os
sqlalchemy.url = sqlite:///./itworx-hub-dev.db
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
+53
View File
@@ -0,0 +1,53 @@
"""Alembic environment for the Hub database, never the ContextForge database."""
from __future__ import annotations
import os
from logging.config import fileConfig
from alembic import context
from sqlalchemy import engine_from_config, pool
from services.control_api.persistence.models import Base
config = context.config
if config.config_file_name is not None:
fileConfig(config.config_file_name)
database_url = os.environ.get("HUB_DATABASE_URL") or os.environ.get("DATABASE_URL")
if database_url:
config.set_main_option("sqlalchemy.url", database_url.replace("%", "%%"))
target_metadata = Base.metadata
def run_migrations_offline() -> None:
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
compare_type=True,
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
connectable = engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
future=True,
)
with connectable.connect() as connection:
context.configure(connection=connection, target_metadata=target_metadata, compare_type=True)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
@@ -0,0 +1,44 @@
"""Create the Hub control-plane schema.
The metadata is deliberately Hub-only. ContextForge has its own database and
its tables must never be imported into this migration.
"""
from __future__ import annotations
from alembic import op
from services.control_api.persistence.models import Base
revision = "0001_core_schema"
down_revision = None
branch_labels = None
depends_on = None
def upgrade() -> None:
bind = op.get_bind()
Base.metadata.create_all(bind=bind, checkfirst=True)
if bind.dialect.name == "postgresql":
op.execute(
"""
CREATE OR REPLACE FUNCTION hub_prevent_audit_mutation()
RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
RAISE EXCEPTION 'audit_events are append-only';
END;
$$;
DROP TRIGGER IF EXISTS trg_audit_events_append_only ON audit_events;
CREATE TRIGGER trg_audit_events_append_only
BEFORE UPDATE OR DELETE ON audit_events
FOR EACH ROW EXECUTE FUNCTION hub_prevent_audit_mutation();
"""
)
def downgrade() -> None:
bind = op.get_bind()
if bind.dialect.name == "postgresql":
op.execute("DROP TRIGGER IF EXISTS trg_audit_events_append_only ON audit_events")
op.execute("DROP FUNCTION IF EXISTS hub_prevent_audit_mutation()")
Base.metadata.drop_all(bind=bind, checkfirst=True)
@@ -0,0 +1,38 @@
"""Add PostgreSQL lexical search indexes for project knowledge."""
from __future__ import annotations
from alembic import op
revision = "0002_knowledge_search"
down_revision = "0001_core_schema"
branch_labels = None
depends_on = None
def upgrade() -> None:
bind = op.get_bind()
if bind.dialect.name != "postgresql":
return
op.execute("CREATE EXTENSION IF NOT EXISTS pg_trgm")
op.execute(
"CREATE INDEX IF NOT EXISTS ix_knowledge_chunks_text_fts "
"ON knowledge_chunks USING gin (to_tsvector('simple', text))"
)
op.execute(
"CREATE INDEX IF NOT EXISTS ix_knowledge_documents_title_trgm "
"ON knowledge_documents USING gin (title gin_trgm_ops)"
)
op.execute(
"CREATE INDEX IF NOT EXISTS ix_knowledge_documents_external_ref_trgm "
"ON knowledge_documents USING gin (external_ref gin_trgm_ops)"
)
def downgrade() -> None:
bind = op.get_bind()
if bind.dialect.name != "postgresql":
return
op.execute("DROP INDEX IF EXISTS ix_knowledge_chunks_text_fts")
op.execute("DROP INDEX IF EXISTS ix_knowledge_documents_title_trgm")
op.execute("DROP INDEX IF EXISTS ix_knowledge_documents_external_ref_trgm")
@@ -0,0 +1,33 @@
"""Persist explainable policy traces for decisions and invocation audit events."""
from __future__ import annotations
import sqlalchemy as sa
from alembic import op
from sqlalchemy import inspect
revision = "0003_policy_trace"
down_revision = "0002_knowledge_search"
branch_labels = None
depends_on = None
def upgrade() -> None:
inspector = inspect(op.get_bind())
if "decision_trace" not in {
column["name"] for column in inspector.get_columns("policy_decisions")
}:
op.add_column(
"policy_decisions",
sa.Column("decision_trace", sa.JSON(), nullable=False, server_default="[]"),
)
if "policy_trace" not in {column["name"] for column in inspector.get_columns("audit_events")}:
op.add_column(
"audit_events",
sa.Column("policy_trace", sa.JSON(), nullable=False, server_default="[]"),
)
def downgrade() -> None:
op.drop_column("audit_events", "policy_trace")
op.drop_column("policy_decisions", "decision_trace")
@@ -0,0 +1,25 @@
"""Persist the bounded retention deadline emitted by the governance audit lifecycle."""
from __future__ import annotations
import sqlalchemy as sa
from alembic import op
from sqlalchemy import inspect
revision = "0004_audit_retention"
down_revision = "0003_policy_trace"
branch_labels = None
depends_on = None
def upgrade() -> None:
columns = {column["name"] for column in inspect(op.get_bind()).get_columns("audit_events")}
if "retention_until" not in columns:
op.add_column(
"audit_events",
sa.Column("retention_until", sa.DateTime(timezone=True), nullable=True),
)
def downgrade() -> None:
op.drop_column("audit_events", "retention_until")
@@ -0,0 +1,7 @@
# Immutable source/runtime contract for the schema-compatible ContextForge build.
# CONTEXTFORGE_LOCAL_IMAGE is a verified local staging tag only; production uses
# CONTEXTFORGE_IMAGE as an immutable registry digest supplied by promotion.
CONTEXTFORGE_UPSTREAM_COMMIT=1e58a0434a78bbdbe1e975abeb0bed1043ff14c1
CONTEXTFORGE_ALEMBIC_HEAD=12d4a0c7789c
CONTEXTFORGE_MCP_SDK=1.30.0
CONTEXTFORGE_LOCAL_IMAGE=itworx-mcp-hub/contextforge-schema:12d4a0c7789c
@@ -0,0 +1,73 @@
#!/bin/sh
set -eu
fail_secret() {
echo "ContextForge secret-file validation failed" >&2
exit 78
}
fail_session_topology() {
echo "ContextForge multi-worker session topology is unsafe: Redis-backed stateful session affinity is required" >&2
exit 78
}
fail_runtime_contract() {
echo "ContextForge strict runtime contract verification failed" >&2
exit 78
}
read_secret() {
[ -f "$1" ] && [ -r "$1" ] && [ -s "$1" ] || fail_secret
mode="$(stat -c '%a' "$1" 2>/dev/null || true)"
case "$mode" in 400|600) ;; *) fail_secret ;; esac
[ "$(stat -c '%u' "$1" 2>/dev/null || true)" = "${SECRET_EXPECTED_UID:?}" ] || fail_secret
[ "$(stat -c '%g' "$1" 2>/dev/null || true)" = "${SECRET_EXPECTED_GID:?}" ] || fail_secret
value="$(tr -d '\r\n' < "$1")"
[ -n "$value" ] || fail_secret
printf '%s' "$value"
}
cf_password="$(read_secret /run/secrets/contextforge_database_password)"
export DATABASE_URL="postgresql+psycopg://contextforge:${cf_password}@postgres:5432/contextforge"
export JWT_SECRET_KEY="$(read_secret /run/secrets/contextforge_jwt_secret)"
export AUTH_ENCRYPTION_SECRET="$(read_secret /run/secrets/contextforge_auth_encryption_secret)"
export BASIC_AUTH_USER="itworx-contextforge-admin"
export BASIC_AUTH_PASSWORD="$(read_secret /run/secrets/contextforge_admin_credential)"
export PLATFORM_ADMIN_EMAIL="${PLATFORM_ADMIN_EMAIL:-contextforge-admin@example.com}"
export PLATFORM_ADMIN_PASSWORD="$BASIC_AUTH_PASSWORD"
# Base/dev defaults retain the historical safe multi-worker topology. Production
# overrides these values to the separately tested stateless single-worker
# contract. Multi-worker mode is never allowed to weaken its affinity controls.
export CACHE_TYPE="${CACHE_TYPE:-redis}"
export USE_STATEFUL_SESSIONS="${USE_STATEFUL_SESSIONS:-true}"
export MCPGATEWAY_SESSION_AFFINITY_ENABLED="${MCPGATEWAY_SESSION_AFFINITY_ENABLED:-true}"
export MCPGATEWAY_SESSION_AFFINITY_TTL="${MCPGATEWAY_SESSION_AFFINITY_TTL:-3600}"
case "${GUNICORN_WORKERS:-auto}" in
1) ;;
*)
[ "$CACHE_TYPE" = "redis" ] || fail_session_topology
[ "$USE_STATEFUL_SESSIONS" = "true" ] || fail_session_topology
[ "$MCPGATEWAY_SESSION_AFFINITY_ENABLED" = "true" ] || fail_session_topology
;;
esac
if [ "${REDIS_AUTH_ENABLED:-false}" = "true" ]; then
redis_password="$(read_secret /run/secrets/redis_password)"
export REDIS_URL="redis://:${redis_password}@redis:6379/0"
unset redis_password
else
export REDIS_URL="${REDIS_URL:-redis://redis:6379/0}"
fi
# Production strict mode is a hard startup gate. The verifier is read-only: it
# proves the embedded compatibility tuple, installed MCP SDK/API, image Alembic
# head, database revision and transport settings. It never installs packages or
# runs migration mutations. Any failure stops before ContextForge/Gunicorn.
if [ "${ITWORX_CONTEXTFORGE_CONTRACT_MODE:-relaxed}" = "strict" ]; then
[ -r /opt/itworx/contextforge-runtime-contract.py ] || fail_runtime_contract
/app/.venv/bin/python /opt/itworx/contextforge-runtime-contract.py --startup || fail_runtime_contract
fi
exec /app/docker-entrypoint.sh
@@ -0,0 +1,22 @@
{
"schema": "itworx.contextforge-runtime-contract/v1",
"sourceCommit": "1e58a0434a78bbdbe1e975abeb0bed1043ff14c1",
"alembicHead": "12d4a0c7789c",
"mcpSdk": "1.30.0",
"requiredSymbol": "McpError",
"productionEnvironment": {
"GUNICORN_WORKERS": "1",
"USE_STATEFUL_SESSIONS": "false",
"MCPGATEWAY_SESSION_AFFINITY_ENABLED": "false",
"JSON_RESPONSE_ENABLED": "true",
"EMAIL_AUTH_ENABLED": "false",
"MCP_SESSION_POOL_ENABLED": "false",
"MCPGATEWAY_SKIP_MIGRATIONS": "true",
"ITWORX_CONTEXTFORGE_CONTRACT_MODE": "strict"
},
"migrationPolicy": {
"startupMigrations": "forbidden",
"skipEnv": "MCPGATEWAY_SKIP_MIGRATIONS",
"skipValue": "true"
}
}
@@ -0,0 +1,29 @@
# syntax=docker/dockerfile:1.7
FROM python:3.13.14-slim@sha256:6771159cd4fa5d9bba1258caf0b82e6b73458c694d178ad97c5e925c2d0e1a91
COPY infrastructure/images/install-base-security-updates.sh /tmp/install-base-security-updates.sh
RUN sh /tmp/install-base-security-updates.sh \
&& rm /tmp/install-base-security-updates.sh
ARG GIT_REVISION
ARG SOURCE_DATE_EPOCH
ARG BUILD_DATE
RUN groupadd --gid 10002 broker \
&& useradd --uid 10002 --gid 10002 --create-home --shell /usr/sbin/nologin broker
COPY services/docker_read_broker /opt/broker/docker_read_broker
COPY packages/__init__.py /opt/broker/packages/__init__.py
COPY packages/hub_runtime /opt/broker/packages/hub_runtime
RUN chown -R 10002:10002 /opt/broker
LABEL org.opencontainers.image.title="itworx-mcp-hub-docker-read-broker" \
org.opencontainers.image.revision="${GIT_REVISION}" \
org.opencontainers.image.source="https://gitea.example.com/jens/itworx-mcp-hub" \
org.opencontainers.image.created="${BUILD_DATE}" \
org.opencontainers.image.vendor="ITWorx"
WORKDIR /opt/broker
USER 10002:10002
EXPOSE 8080
ENTRYPOINT ["python", "-m", "docker_read_broker.server"]
@@ -0,0 +1,27 @@
from __future__ import annotations
import json
from http import HTTPStatus
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
class BrokerHealthHandler(BaseHTTPRequestHandler):
server_version = "ITWorxDockerReadBroker/0.1"
def do_GET(self) -> None: # noqa: N802
if self.path != "/healthz":
self.send_error(HTTPStatus.NOT_FOUND)
return
body = json.dumps({"status": "ok", "capability": "health-only-bootstrap"}).encode()
self.send_response(HTTPStatus.OK)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, _format: str, *_args: object) -> None:
return
if __name__ == "__main__":
ThreadingHTTPServer(("0.0.0.0", 8080), BrokerHealthHandler).serve_forever() # noqa: S104
@@ -0,0 +1,57 @@
#!/bin/sh
set -eu
# Exact Debian security versions from the staging image scan. Keep these pins
# explicit: a release build must fail rather than silently consume newer OS
# packages that were not scanned.
gzip_version='1.13-1+deb13u1'
pcre2_version='10.46-1~deb13u2'
sqlite3_version='3.46.1-7+deb13u2'
openssl_version='3.5.7-1~deb13u2'
perl_base_version='5.40.1-6+deb13u1'
util_linux_version='2.41.5-0+deb13u1'
bsdutils_version='1:2.41.5-0+deb13u1'
login_version='1:4.16.0-2+really2.41.5-0+deb13u1'
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install --yes --no-install-recommends \
"bsdutils=${bsdutils_version}" \
"gzip=${gzip_version}" \
"libblkid1=${util_linux_version}" \
"liblastlog2-2=${util_linux_version}" \
"libmount1=${util_linux_version}" \
"libpcre2-8-0=${pcre2_version}" \
"libsqlite3-0=${sqlite3_version}" \
"libssl3t64=${openssl_version}" \
"libsmartcols1=${util_linux_version}" \
"libuuid1=${util_linux_version}" \
"login=${login_version}" \
"mount=${util_linux_version}" \
"openssl=${openssl_version}" \
"openssl-provider-legacy=${openssl_version}" \
"perl-base=${perl_base_version}" \
"util-linux=${util_linux_version}"
assert_package_version() {
actual="$(dpkg-query -W -f='${Version}' "$1")"
if [ "${actual}" != "$2" ]; then
printf 'Expected %s=%s but found %s\n' "$1" "$2" "${actual}" >&2
exit 1
fi
}
assert_package_version "bsdutils" "${bsdutils_version}"
assert_package_version "login" "${login_version}"
for package in libblkid1 liblastlog2-2 libmount1 libsmartcols1 libuuid1 mount util-linux; do
assert_package_version "${package}" "${util_linux_version}"
done
assert_package_version "gzip" "${gzip_version}"
assert_package_version "libpcre2-8-0" "${pcre2_version}"
assert_package_version "libsqlite3-0" "${sqlite3_version}"
assert_package_version "libssl3t64" "${openssl_version}"
assert_package_version "openssl" "${openssl_version}"
assert_package_version "openssl-provider-legacy" "${openssl_version}"
assert_package_version "perl-base" "${perl_base_version}"
rm -rf /var/lib/apt/lists/*
@@ -0,0 +1,25 @@
# syntax=docker/dockerfile:1.7
FROM python:3.13.14-slim@sha256:6771159cd4fa5d9bba1258caf0b82e6b73458c694d178ad97c5e925c2d0e1a91
COPY infrastructure/images/install-base-security-updates.sh /tmp/install-base-security-updates.sh
RUN sh /tmp/install-base-security-updates.sh \
&& rm /tmp/install-base-security-updates.sh
ARG GIT_REVISION
ARG SOURCE_DATE_EPOCH
ARG BUILD_DATE
RUN groupadd --gid 10001 hub \
&& useradd --uid 10001 --gid 10001 --create-home --shell /usr/sbin/nologin hub
COPY infrastructure/images/observability/collector.py /opt/hub/collector.py
RUN chown -R 10001:10001 /opt/hub
LABEL org.opencontainers.image.title="itworx-mcp-hub-observability-collector" \
org.opencontainers.image.revision="${GIT_REVISION}" \
org.opencontainers.image.source="https://gitea.example.com/jens/itworx-mcp-hub" \
org.opencontainers.image.created="${BUILD_DATE}" \
org.opencontainers.image.vendor="ITWorx"
USER 10001:10001
EXPOSE 4318
ENTRYPOINT ["python", "/opt/hub/collector.py"]
@@ -0,0 +1,78 @@
"""Minimal internal OTLP/HTTP collector for the optional observer profile.
It accepts bounded trace batches, keeps only counters in memory, and exposes
Prometheus text metrics. It never logs request bodies or trace attributes.
"""
from __future__ import annotations
import json
from http import HTTPStatus
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from threading import Lock
MAX_BODY = 1024 * 1024
_lock = Lock()
_spans = 0
_batches = 0
class CollectorHandler(BaseHTTPRequestHandler):
server_version = "ITWorxOTLPCollector/1.0"
def do_GET(self) -> None: # noqa: N802
if self.path == "/healthz":
self._write(
HTTPStatus.OK,
b'{"status":"ok","component":"observability-collector"}',
"application/json",
)
return
if self.path == "/metrics":
with _lock:
body = (
f"itworx_otel_batches_total {_batches}\\nitworx_otel_spans_total {_spans}\\n"
).encode()
self._write(HTTPStatus.OK, body, "text/plain; version=0.0.4")
return
self.send_error(HTTPStatus.NOT_FOUND)
def do_POST(self) -> None: # noqa: N802
global _batches, _spans
if self.path != "/v1/traces":
self.send_error(HTTPStatus.NOT_FOUND)
return
length = int(self.headers.get("Content-Length", "0"))
if length <= 0 or length > MAX_BODY:
self.send_error(HTTPStatus.REQUEST_ENTITY_TOO_LARGE)
return
try:
document = json.loads(self.rfile.read(length))
batches = document.get("resourceSpans", [])
spans = sum(
len(scope.get("spans", []))
for batch in batches
for scope in batch.get("scopeSpans", [])
)
except (ValueError, TypeError, AttributeError):
self.send_error(HTTPStatus.BAD_REQUEST)
return
with _lock:
_batches += 1
_spans += spans
self._write(HTTPStatus.ACCEPTED, b"", "text/plain")
def _write(self, status: HTTPStatus, body: bytes, content_type: str) -> None:
self.send_response(status)
self.send_header("Content-Type", content_type)
self.send_header("Content-Length", str(len(body)))
self.end_headers()
if body:
self.wfile.write(body)
def log_message(self, _format: str, *_args: object) -> None:
return
if __name__ == "__main__":
ThreadingHTTPServer(("0.0.0.0", 4318), CollectorHandler).serve_forever() # noqa: S104
@@ -0,0 +1,36 @@
#!/bin/sh
set -eu
fail_secret() {
echo "PostgreSQL secret-file validation failed" >&2
exit 78
}
source_root=/run/secrets
runtime_root=/run/itworx-secrets
runtime_uid="$(id -u postgres)"
runtime_gid="$(id -g postgres)"
install -d -o root -g "$runtime_gid" -m 0750 "$runtime_root"
for name in postgres_superuser_password hub_database_password contextforge_database_password; do
source="$source_root/$name"
target="$runtime_root/$name"
[ -f "$source" ] && [ ! -L "$source" ] && [ -r "$source" ] && [ -s "$source" ] || fail_secret
mode="$(stat -c '%a' "$source" 2>/dev/null || true)"
case "$mode" in 400|600) ;; *) fail_secret ;; esac
[ "$(stat -c '%u' "$source" 2>/dev/null || true)" = "${SECRET_EXPECTED_UID:?}" ] || fail_secret
[ "$(stat -c '%g' "$source" 2>/dev/null || true)" = "${SECRET_EXPECTED_GID:?}" ] || fail_secret
[ "$(wc -c < "$source")" -le 8192 ] || fail_secret
value="$(tr -d '\r\n' < "$source")"
[ -n "$value" ] || fail_secret
unset value
install -o "$runtime_uid" -g "$runtime_gid" -m 0400 "$source" "$target"
done
export POSTGRES_PASSWORD_FILE="$runtime_root/postgres_superuser_password"
export POSTGRES_SECRET_ROOT="$runtime_root"
export POSTGRES_SECRET_EXPECTED_UID="$runtime_uid"
export POSTGRES_SECRET_EXPECTED_GID="$runtime_gid"
exec /usr/local/bin/docker-entrypoint.sh "$@"
@@ -0,0 +1,58 @@
# syntax=docker/dockerfile:1.7
FROM node:24.18.1-bookworm-slim@sha256:235600a8101ab264e117b1768e925532262668dc9b581ef1dd7d96ced463b8e7 AS web-build
WORKDIR /workspace
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY apps/web/package.json apps/web/package.json
COPY packages/ui/package.json packages/ui/package.json
RUN npm install --global pnpm@10.33.0 \
&& pnpm install --frozen-lockfile --filter @itworx-mcp-hub/web...
COPY apps/web apps/web
RUN pnpm --filter @itworx-mcp-hub/web build
FROM python:3.13.14-slim@sha256:6771159cd4fa5d9bba1258caf0b82e6b73458c694d178ad97c5e925c2d0e1a91
COPY infrastructure/images/install-base-security-updates.sh /tmp/install-base-security-updates.sh
RUN sh /tmp/install-base-security-updates.sh \
&& rm /tmp/install-base-security-updates.sh
ARG SERVICE_NAME=itworx-mcp-hub-service
ARG GIT_REVISION
ARG SOURCE_DATE_EPOCH
ARG BUILD_DATE
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PATH=/opt/venv/bin:$PATH \
SERVICE_NAME=${SERVICE_NAME}
RUN groupadd --gid 10001 hub \
&& useradd --uid 10001 --gid 10001 --create-home --shell /usr/sbin/nologin hub
COPY pyproject.toml uv.lock /opt/hub/
RUN python -m pip install --no-cache-dir uv==0.10.0 \
&& cd /opt/hub \
&& UV_PROJECT_ENVIRONMENT=/opt/venv uv sync --locked --no-dev --no-install-project \
&& /usr/local/bin/python -m pip uninstall --yes uv
COPY packages /opt/hub/packages
COPY services /opt/hub/services
COPY scripts /opt/hub/scripts
COPY connectors /opt/hub/connectors
COPY catalog /opt/hub/catalog
COPY contracts /opt/hub/contracts
COPY plugins/itworx-gitea /opt/hub/plugins/itworx-gitea
COPY infrastructure/database /opt/hub/infrastructure/database
COPY --from=web-build /workspace/apps/web/dist /opt/hub/web-dist
RUN chown -R 10001:10001 /opt/hub
LABEL org.opencontainers.image.title="itworx-mcp-hub-${SERVICE_NAME}" \
org.opencontainers.image.revision="${GIT_REVISION}" \
org.opencontainers.image.source="https://gitea.example.com/jens/itworx-mcp-hub" \
org.opencontainers.image.created="${BUILD_DATE}" \
org.opencontainers.image.vendor="ITWorx"
WORKDIR /opt/hub
USER 10001:10001
EXPOSE 8080
ENTRYPOINT ["python", "-m", "services.runtime"]
@@ -0,0 +1,129 @@
from __future__ import annotations
import json
import os
import sys
from http import HTTPStatus
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from packages.hub_observability import build_telemetry, trace_from_headers, valid_trace_id
from packages.hub_runtime.secret_files import SecretFileError, validate_required_secret_paths
from services.observability.health import DependencyHealth, aggregate_health
def _validate_startup() -> None:
try:
validate_required_secret_paths(os.environ.get("REQUIRED_SECRET_FILES", ""))
except (SecretFileError, ValueError):
sys.stderr.write("required runtime secret validation failed\n")
raise SystemExit(78) from None
_validate_startup()
_telemetry = build_telemetry(os.environ.get("SERVICE_NAME", "unknown"))
def _trace_id(handler: BaseHTTPRequestHandler) -> str:
candidate = handler.headers.get("X-Trace-Id", "")
if valid_trace_id(candidate):
return candidate.lower()
return trace_from_headers({"traceparent": handler.headers.get("traceparent", "")}).trace_id
def _dependencies() -> list[DependencyHealth]:
raw = os.environ.get("DEPENDENCY_HEALTH_JSON", "")
if not raw:
return [DependencyHealth(os.environ.get("SERVICE_NAME", "unknown"), "healthy")]
try:
document = json.loads(raw)
except ValueError:
return [
DependencyHealth(
"configuration", "unhealthy", detail="invalid dependency health configuration"
)
]
if not isinstance(document, dict):
return [
DependencyHealth(
"configuration", "unhealthy", detail="dependency health must be an object"
)
]
values = []
for name, value in document.items():
if not isinstance(value, dict):
continue
values.append(
DependencyHealth(
str(name)[:64],
str(value.get("status", "unknown")),
bool(value.get("required", True)),
bool(value.get("authoritative", True)),
str(value.get("detail", "")),
)
)
return values or [
DependencyHealth("configuration", "unhealthy", detail="no dependencies configured")
]
class HealthHandler(BaseHTTPRequestHandler):
server_version = "ITWorxHealth/0.2"
def do_GET(self) -> None: # noqa: N802
trace_id = _trace_id(self)
if self.path == "/metrics":
body = _telemetry.metrics.render_prometheus().encode("utf-8")
self._write(HTTPStatus.OK, body, "text/plain; version=0.0.4", trace_id)
return
if self.path == "/health/aggregate":
aggregate = aggregate_health(_dependencies())
status = (
HTTPStatus.OK if aggregate.status != "unhealthy" else HTTPStatus.SERVICE_UNAVAILABLE
)
self._write_json(
status,
aggregate.payload(
service=os.environ.get("SERVICE_NAME", "unknown"), trace_id=trace_id
),
trace_id,
)
return
if self.path not in {"/health", "/healthz", "/ready"}:
self.send_error(HTTPStatus.NOT_FOUND)
return
_telemetry.metrics.inc("itworx_health_requests_total", labels={"path": self.path})
_telemetry.logger.emit("HEALTH_REQUEST", trace_id, fields={"path": self.path})
self._write_json(
HTTPStatus.OK,
{
"status": "ok",
"service": os.environ.get("SERVICE_NAME", "unknown"),
"traceId": trace_id,
},
trace_id,
)
def _write_json(self, status: HTTPStatus, payload: dict[str, object], trace_id: str) -> None:
self._write(
status,
json.dumps(payload, separators=(",", ":")).encode("utf-8"),
"application/json",
trace_id,
)
def _write(self, status: HTTPStatus, body: bytes, content_type: str, trace_id: str) -> None:
self.send_response(status)
self.send_header("Content-Type", content_type)
self.send_header("Content-Length", str(len(body)))
self.send_header("X-Trace-Id", trace_id)
self.end_headers()
if body:
self.wfile.write(body)
def log_message(self, _format: str, *_args: object) -> None:
return
if __name__ == "__main__":
ThreadingHTTPServer(("0.0.0.0", 8080), HealthHandler).serve_forever() # noqa: S104
+29
View File
@@ -0,0 +1,29 @@
#!/bin/sh
set -eu
fail() {
echo "Redis secret-file validation failed" >&2
exit 78
}
if [ "${REDIS_AUTH_ENABLED:-false}" = "true" ]; then
secret=/run/secrets/redis_password
[ -f "$secret" ] && [ -r "$secret" ] || fail
[ -s "$secret" ] || fail
mode="$(stat -c '%a' "$secret" 2>/dev/null || true)"
case "$mode" in 400|600) ;; *) fail ;; esac
[ "$(stat -c '%u' "$secret" 2>/dev/null || true)" = "${SECRET_EXPECTED_UID:?}" ] || fail
[ "$(stat -c '%g' "$secret" 2>/dev/null || true)" = "${SECRET_EXPECTED_GID:?}" ] || fail
password="$(tr -d '\r\n' < "$secret")"
[ -n "$password" ] || fail
umask 077
{
printf 'save ""\nappendonly no\nmaxmemory-policy allkeys-lru\nrequirepass '
printf '%s' "$password" | sed 's/\\/\\\\/g; s/"/\\"/g'
printf '\n'
} > /run/itworx/redis.conf
unset password
exec redis-server /run/itworx/redis.conf
fi
exec redis-server --save "" --appendonly no --maxmemory-policy allkeys-lru
@@ -0,0 +1,12 @@
#!/bin/sh
set -eu
if [ "${REDIS_AUTH_ENABLED:-false}" = "true" ]; then
secret=/run/secrets/redis_password
[ -f "$secret" ] && [ -r "$secret" ] && [ -s "$secret" ] || exit 1
REDISCLI_AUTH="$(tr -d '\r\n' < "$secret")"
export REDISCLI_AUTH
fi
response="$(redis-cli ping 2>/dev/null)"
[ "$response" = "PONG" ]
@@ -0,0 +1,46 @@
#!/usr/bin/env bash
set -euo pipefail
read_secret() {
local name="$1"
local path="${POSTGRES_SECRET_ROOT:-/run/secrets}/${name}"
[[ -f "$path" && -r "$path" && -s "$path" ]] || {
echo "PostgreSQL secret-file validation failed" >&2
exit 78
}
local mode
mode="$(stat -c '%a' "$path" 2>/dev/null || true)"
[[ "$mode" == "400" || "$mode" == "600" ]] || {
echo "PostgreSQL secret-file validation failed" >&2
exit 78
}
[[ "$(stat -c '%u' "$path" 2>/dev/null || true)" == "${POSTGRES_SECRET_EXPECTED_UID:-${SECRET_EXPECTED_UID:?}}" ]] || {
echo "PostgreSQL secret-file validation failed" >&2
exit 78
}
[[ "$(stat -c '%g' "$path" 2>/dev/null || true)" == "${POSTGRES_SECRET_EXPECTED_GID:-${SECRET_EXPECTED_GID:?}}" ]] || {
echo "PostgreSQL secret-file validation failed" >&2
exit 78
}
local value
value="$(tr -d '\r\n' < "$path")"
[[ -n "$value" ]] || { echo "PostgreSQL secret-file validation failed" >&2; exit 78; }
printf '%s' "$value"
}
HUB_PASSWORD="$(read_secret hub_database_password)"
CF_PASSWORD="$(read_secret contextforge_database_password)"
psql --username "$POSTGRES_USER" --dbname postgres --set=ON_ERROR_STOP=1 \
--set=hub_password="$HUB_PASSWORD" --set=cf_password="$CF_PASSWORD" <<'SQL'
SELECT format('CREATE ROLE itworx_mcp_hub LOGIN PASSWORD %L', :'hub_password')
WHERE NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'itworx_mcp_hub')\gexec
SELECT format('CREATE ROLE contextforge LOGIN PASSWORD %L', :'cf_password')
WHERE NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'contextforge')\gexec
SELECT 'CREATE DATABASE itworx_mcp_hub OWNER itworx_mcp_hub'
WHERE NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = 'itworx_mcp_hub')\gexec
SELECT 'CREATE DATABASE contextforge OWNER contextforge'
WHERE NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = 'contextforge')\gexec
REVOKE ALL ON DATABASE itworx_mcp_hub FROM PUBLIC;
REVOKE ALL ON DATABASE contextforge FROM PUBLIC;
SQL
+26
View File
@@ -0,0 +1,26 @@
# Secret provisioning
Create one file per secret under the deployed `${SECRETS_DIR}` with mode `0600` and an owner readable only by the intended deployment operator. Do not commit the directory or copy its contents into evidence.
Required core secrets:
- `postgres_superuser_password`
- `hub_database_password`
- `contextforge_database_password`
- `contextforge_auth_encryption_secret`
- `contextforge_jwt_secret`
- `contextforge_admin_credential`
- `contextforge_api_token` (generated by the controlled ContextForge bootstrap)
- `stitch_api_key` (Google Stitch API key; connector-only, never exposed to MCP clients)
- `authentik_oidc_client_secret`
- `hub_session_signing_key`
`contextforge_api_token` is absent during the initial predeploy check. Generate
it with `scripts/contextforge_bootstrap.py` after ContextForge is healthy and
scope it to the bootstrap's explicit gateway, tool, prompt and virtual-server
permissions. Rotate an older token when the required permission set changes,
then start the Hub services; the poststart preflight requires it.
Optional connector secrets are added only when that connector is enabled. Use least-privilege, read-only credentials in v1.0.
The application may show whether a secret reference is configured and when it was last verified. It must never show, return, audit, or log the value.