Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions .github/workflows/verify.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: verify

on:
pull_request:
push:
branches:
- main

permissions:
contents: read

jobs:
verify:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
cache: pip
- name: Install test dependencies
run: python -m pip install --disable-pip-version-check -e '.[dev]'
- name: Run unit tests
run: python -m pytest -q
- name: Validate secure OpenWebUI boundary
shell: bash
run: |
set -euo pipefail
bash -n scripts/configure-openwebui-subactor.sh
docker compose --profile openwebui config -q
docker compose --profile openwebui config --format json | jq -e '
.services["mcp-gateway"].ports == [{
"mode": "ingress",
"host_ip": "127.0.0.1",
"target": 9000,
"published": "9000",
"protocol": "tcp"
}] and
.services.openwebui.ports == [{
"mode": "ingress",
"host_ip": "127.0.0.1",
"target": 8080,
"published": "3000",
"protocol": "tcp"
}] and
.services.openwebui.environment.WEBUI_AUTH == "True" and
.services.openwebui.environment.ENABLE_SIGNUP == "False" and
any(
.services.openwebui.volumes[];
.target == "/run/secrets/openwebui-mcp-bearer" and
.read_only == true
)
'
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.env.example
.secrets/
.idea
repos
output
Expand Down
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,39 @@ Oficjalny frontend (docker image) podłączony do MCP Gateway:
- Chat z modelami `mcp-skills/refactor` i `mcp-skills/analyze`
- **URL**: http://localhost:3000

Domyślnie OpenWebUI wymaga logowania, nie pozwala na samodzielną rejestrację i
nasłuchuje tylko na `127.0.0.1`. Gateway również jest związany z loopbackiem.
Przed uruchomieniem profilu utwórz trzy pliki o prawach `0600`:

```bash
install -d -m 700 .secrets
umask 077
test -s .secrets/openwebui-session.secret || openssl rand -hex 32 > .secrets/openwebui-session.secret
test -s .secrets/openwebui-mcp-jwt.secret || openssl rand -hex 32 > .secrets/openwebui-mcp-jwt.secret
test -s .secrets/openwebui-mcp-bearer.secret || openssl rand -hex 32 > .secrets/openwebui-mcp-bearer.secret
docker compose --profile openwebui up -d
```

Pliki można trzymać poza repozytorium, ustawiając
`OPENWEBUI_SESSION_SECRET_FILE`, `OPENWEBUI_MCP_JWT_SECRET_FILE` i
`OPENWEBUI_MCP_BEARER_SECRET_FILE`. Port można zmienić przez `PORT_OPENWEBUI`,
ale publiczny bind powinien być wystawiany wyłącznie przez uwierzytelnione TLS
proxy; surowy MCP i Control nie powinny być publikowane do Internetu.

Po uruchomieniu można idempotentnie predefiniować zawężone połączenie Subactor:

```bash
SUBACTOR_ACCOUNT_ID=softreck \
SUBACTOR_PROVIDER=chatgpt \
SUBACTOR_TOOL_ID=codex \
SUBACTOR_CONTROL_URL=http://172.17.0.1:8088 \
./scripts/configure-openwebui-subactor.sh
```

Skrypt nie drukuje tokenu. Konfiguruje wyłącznie `cli.status`, `cli.plan` i
`cli.execute`; wykonanie nadal wymaga osobnego grantu oraz aktywnej intencji po
stronie Subactor.

### MCP Skills - HTTP API
Serwer FastAPI z endpointami (poza MCP STDIO):
- `POST /sync` - Synchronizacja repo z git-proxy
Expand Down
17 changes: 14 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ services:
- ./.env:/app/.env
- audit-storage:/audit
ports:
- "${PORT_GATEWAY:-9000}:9000"
- "${MCP_GATEWAY_BIND_HOST:-127.0.0.1}:${PORT_GATEWAY:-9000}:9000"
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:9000/health', timeout=3)"]
interval: 20s
Expand Down Expand Up @@ -261,7 +261,11 @@ services:
environment:
- OPENAI_API_BASE_URL=http://mcp-gateway:9000/v1
- OPENAI_API_KEY=${WEBUI_API_KEY:-sk-mcp-default-dev-key}
- WEBUI_AUTH=${OPENWEBUI_AUTH:-False}
- WEBUI_AUTH=${OPENWEBUI_AUTH:-True}
- ENABLE_SIGNUP=${OPENWEBUI_ENABLE_SIGNUP:-False}
- ENABLE_FORWARD_USER_INFO_HEADERS=True
- FORWARD_USER_INFO_HEADER_JWT=X-OpenWebUI-User-Jwt
- FORWARD_USER_INFO_HEADER_JWT_EXPIRES_SECONDS=300
# Renderowanie Markdown / artefaktów
- ENABLE_MERMAID=${ENABLE_MERMAID:-true}
- ENABLE_LATEX=${ENABLE_LATEX:-true}
Expand All @@ -273,8 +277,15 @@ services:
- SHOW_ADMIN_DETAILS=${SHOW_ADMIN_DETAILS:-false}
volumes:
- openwebui-data:/app/backend/data
- ${OPENWEBUI_MCP_JWT_SECRET_FILE:-./.secrets/openwebui-mcp-jwt.secret}:/run/secrets/openwebui-mcp-jwt:ro
- ${OPENWEBUI_MCP_BEARER_SECRET_FILE:-./.secrets/openwebui-mcp-bearer.secret}:/run/secrets/openwebui-mcp-bearer:ro
- ${OPENWEBUI_SESSION_SECRET_FILE:-./.secrets/openwebui-session.secret}:/run/secrets/openwebui-session:ro
command:
- bash
- -lc
- export WEBUI_SECRET_KEY="$$(tr -d '\r\n' < /run/secrets/openwebui-session)"; export FORWARD_USER_INFO_HEADER_JWT_SECRET="$$(tr -d '\r\n' < /run/secrets/openwebui-mcp-jwt)"; exec bash start.sh
ports:
- "${PORT_OPENWEBUI:-3000}:8080"
- "${OPENWEBUI_BIND_HOST:-127.0.0.1}:${PORT_OPENWEBUI:-3000}:8080"
depends_on:
mcp-gateway:
condition: service_healthy
Expand Down
111 changes: 111 additions & 0 deletions scripts/configure-openwebui-subactor.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#!/usr/bin/env bash
set -euo pipefail

account_id="${SUBACTOR_ACCOUNT_ID:-softreck}"
provider="${SUBACTOR_PROVIDER:-chatgpt}"
tool_id="${SUBACTOR_TOOL_ID:-codex}"
control_url="${SUBACTOR_CONTROL_URL:-http://172.17.0.1:8088}"
connection_id="${SUBACTOR_OPENWEBUI_CONNECTION_ID:-subactor-${account_id}-${provider}-${tool_id}}"

docker compose exec -T \
-e SUBACTOR_ACCOUNT_ID="$account_id" \
-e SUBACTOR_PROVIDER="$provider" \
-e SUBACTOR_TOOL_ID="$tool_id" \
-e SUBACTOR_CONTROL_URL="$control_url" \
-e SUBACTOR_OPENWEBUI_CONNECTION_ID="$connection_id" \
openwebui sh -lc '
export WEBUI_SECRET_KEY="$(tr -d "\r\n" < /run/secrets/openwebui-session)"
python -
' <<'PY'
import asyncio
import json
import os
import re
from pathlib import Path
from urllib.parse import urlsplit

from open_webui.models.config import Config


def identifier(name: str) -> str:
value = os.environ[name].strip().lower()
if re.fullmatch(r"[a-z0-9][a-z0-9-]{0,62}", value) is None:
raise SystemExit(f"invalid {name}")
return value


async def configure() -> None:
account_id = identifier("SUBACTOR_ACCOUNT_ID")
provider = identifier("SUBACTOR_PROVIDER")
tool_id = identifier("SUBACTOR_TOOL_ID")
connection_id = os.environ["SUBACTOR_OPENWEBUI_CONNECTION_ID"].strip()
if re.fullmatch(r"[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}", connection_id) is None:
raise SystemExit("invalid SUBACTOR_OPENWEBUI_CONNECTION_ID")

control_url = os.environ["SUBACTOR_CONTROL_URL"].strip().rstrip("/")
parsed = urlsplit(control_url)
if parsed.scheme not in {"http", "https"} or not parsed.hostname:
raise SystemExit("invalid SUBACTOR_CONTROL_URL")
if parsed.username or parsed.password or parsed.query or parsed.fragment:
raise SystemExit("SUBACTOR_CONTROL_URL must not contain credentials or a query")

secret_path = Path("/run/secrets/openwebui-mcp-bearer")
token = secret_path.read_text(encoding="utf-8").strip()
if len(token) < 32:
raise SystemExit("OpenWebUI MCP bearer secret is missing or too short")

url = (
f"{control_url}/mcp/accounts/{account_id}/providers/{provider}/tools/{tool_id}"
)
connection = {
"auth_type": "bearer",
"config": {
"access_grants": [
{
"permission": "read",
"principal_id": "*",
"principal_type": "user",
}
],
"enable": True,
"function_name_filter_list": "cli.status,cli.plan,cli.execute",
},
"headers": None,
"info": {
"description": (
"Scoped Subactor MCP: closed DSL, grant and intent required for execution."
),
"id": connection_id,
"name": f"Subactor — {account_id} / {provider} / {tool_id}",
},
"key": token,
"path": "",
"type": "mcp",
"url": url,
}
connections = await Config.get("tool_server.connections", [])
if not isinstance(connections, list):
raise SystemExit("OpenWebUI tool_server.connections is not a list")
updated = [
item
for item in connections
if (item.get("info") or {}).get("id") != connection_id
]
updated.append(connection)
await Config.upsert({"tool_server.connections": updated})
print(
json.dumps(
{
"configured": True,
"connection_id": connection_id,
"url": url,
"tools": ["cli.status", "cli.plan", "cli.execute"],
"token_logged": False,
},
sort_keys=True,
)
)


asyncio.run(configure())
PY