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
13 changes: 13 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@ CONVERSATION_DB_USER=conversations
CONVERSATION_DB_PASSWORD=local_conversations_change_me
CONVERSATION_SERVICE_PORT=18085

CONNECTION_DB_USER=connections
CONNECTION_DB_PASSWORD=local_connections_change_me
CONNECTION_SERVICE_PORT=18088
CONNECTION_TOKEN_KEY_BASE64=derive-from-local-action-secret

# Реальный Google OAuth включается только в локальном .env. Секреты не коммитим.
GOOGLE_OAUTH_ENABLED=false
# GOOGLE_OAUTH_CLIENT_ID=
# GOOGLE_OAUTH_CLIENT_SECRET=
# GOOGLE_OAUTH_REDIRECT_URI=http://localhost:18088/api/v1/connections/callback
GOOGLE_CALENDAR_MCP_PORT=18089

TELEGRAM_DB_USER=telegram
TELEGRAM_DB_PASSWORD=local_telegram_db_change_me
TELEGRAM_ADAPTER_CLIENT_SECRET=local_telegram_client_change_me
Expand Down Expand Up @@ -47,6 +59,7 @@ CHANNEL_GATEWAY_CONTEXT=../../portable-agent-channel-gateway
ACTION_SERVICE_CONTEXT=../../portable-agent-action-service
MCP_GATEWAY_CONTEXT=../../portable-agent-mcp-gateway
CALENDAR_MCP_CONTEXT=../../portable-agent-calendar-mcp
CONNECTION_SERVICE_CONTEXT=../../portable-agent-connection-service
CONVERSATION_SERVICE_CONTEXT=../../portable-agent-conversation-service
TELEGRAM_ADAPTER_CONTEXT=../../portable-agent-telegram-adapter
TEST_LAB_PATH=../portable-agent-test-lab
Expand Down
9 changes: 9 additions & 0 deletions .github/workflows/app-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ jobs:
CONVERSATION_SERVICE_CONTEXT: ${{ github.workspace }}/repos/conversation-service
MCP_GATEWAY_CONTEXT: ${{ github.workspace }}/repos/mcp-gateway
CALENDAR_MCP_CONTEXT: ${{ github.workspace }}/repos/calendar-mcp
CONNECTION_SERVICE_CONTEXT: ${{ github.workspace }}/repos/connection-service
TELEGRAM_ADAPTER_CONTEXT: ${{ github.workspace }}/repos/telegram-adapter
TEST_LAB_PATH: ${{ github.workspace }}/repos/test-lab
steps:
Expand All @@ -50,6 +51,7 @@ jobs:
"conversation=$($versions.CONVERSATION_SERVICE_IMAGE.Split(':')[-1])" >> $env:GITHUB_OUTPUT
"gateway=$($versions.MCP_GATEWAY_IMAGE.Split(':')[-1])" >> $env:GITHUB_OUTPUT
"calendar=$($versions.CALENDAR_MCP_IMAGE.Split(':')[-1])" >> $env:GITHUB_OUTPUT
"connection=$($versions.CONNECTION_SERVICE_IMAGE.Split(':')[-1])" >> $env:GITHUB_OUTPUT
"telegram=$($versions.TELEGRAM_ADAPTER_IMAGE.Split(':')[-1])" >> $env:GITHUB_OUTPUT
"test_lab=$($versions.TEST_LAB_REF)" >> $env:GITHUB_OUTPUT

Expand Down Expand Up @@ -95,6 +97,13 @@ jobs:
ref: ${{ steps.versions.outputs.calendar }}
path: repos/calendar-mcp

- name: Получить Connection Service
uses: actions/checkout@v7
with:
repository: portable-agent/connection-service
ref: ${{ steps.versions.outputs.connection }}
path: repos/connection-service

- name: Получить Telegram Adapter
uses: actions/checkout@v7
with:
Expand Down
17 changes: 11 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,9 @@ Telegram Update и Bot API response.
`TEST_LAB_PATH` по умолчанию указывает на соседний `../portable-agent-test-lab`; путь можно
переопределить в `.env`. Channel Gateway доступен на `http://localhost:18084`, Agent Runtime — на
`http://localhost:18080`, Action API — на
`http://localhost:18081`, MCP Gateway — на `http://localhost:18083`, а Calendar MCP — на
`http://localhost:18082`. Conversation Service доступен на `http://localhost:18085`. Новый маршрут
`http://localhost:18081`, MCP Gateway — на `http://localhost:18083`, fake Calendar MCP — на
`http://localhost:18082`, а Google Calendar MCP — на `http://localhost:18089`. Conversation Service
доступен на `http://localhost:18085`, Connection Service — на `http://localhost:18088`. Новый маршрут
`/api/v1/conversations/messages` в Channel Gateway передаёт сообщения в него; старый маршрут
`/api/v1/messages` временно сохранён для обратной совместимости. Решение confirmation-виджета
отправляется в `/api/v1/actions/{actionId}/decisions` того же Gateway и только затем попадает в Action
Expand All @@ -148,18 +149,22 @@ manager, а не из Git.
Локальный Keycloak импортирует realm `portable-agent`. Публичный тестовый client
`portable-agent-local` и пользователь `local-user` с паролем `local-user-change-me` существуют только
в Compose fixture. Отдельный confidential client `action-service` выдаёт worker служебный токен с
`tenant_id`, audience `mcp-gateway` и `calendar-mcp`, scopes `mcp:call` и `calendar:write`. Значения
`tenant_id`, audience `mcp-gateway`, `calendar-mcp` и `connection-service`, scopes `mcp:call`,
`calendar:write` и `connection:token`. Значения
локального секрета и tenant приходят из `.env`, а не зашиты в image. Отдельный confidential client
`telegram-adapter` включает OAuth Device Authorization Grant и выдаёт пользовательский токен с теми
же `tenant_id` и audience backend-пути. PostgreSQL создаёт отдельные БД `actions`, `conversations` и
`telegram_adapter`.
`telegram_adapter` и `connections`.

Проверочный API Calendar MCP включён только в локальном профиле `apps`, привязан к loopback-порту и
защищён `CALENDAR_TEST_API_KEY`. Хранилище fake-calendar пока находится в памяти: перезапуск контейнера
очищает созданные тестовые встречи.
очищает созданные тестовые встречи. Второй экземпляр того же Calendar MCP использует стратегию
`google-calendar` и получает короткоживущий Google token через Connection Service. OAuth credentials и
зашифрованные пользовательские refresh tokens не попадают в Calendar MCP.

После запуска скрипт получает настоящий JWT и сверяет пользователя, `tenant_id` и audience
`channel-gateway`, `conversation-service`, `agent-runtime`, `action-service` и `calendar-mcp` с realm fixture.
`channel-gateway`, `conversation-service`, `agent-runtime`, `action-service`, `calendar-mcp` и
`connection-service` с realm fixture.
Один пользовательский токен проходит независимую проверку во всех пользовательских API.
Если Keycloak volume создан старой версией fixture, запуск остановится с командой для явного
пересоздания локальных данных.
Expand Down
12 changes: 12 additions & 0 deletions compose/apps.local.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,18 @@ services:
context: ${CALENDAR_MCP_CONTEXT:-../../portable-agent-calendar-mcp}
pull_policy: build

google-calendar-mcp:
image: portable-agent/calendar-mcp:local
build:
context: ${CALENDAR_MCP_CONTEXT:-../../portable-agent-calendar-mcp}
pull_policy: build

connection-service:
image: portable-agent/connection-service:local
build:
context: ${CONNECTION_SERVICE_CONTEXT:-../../portable-agent-connection-service}
pull_policy: build

telegram-adapter:
image: portable-agent/telegram-adapter:local
build:
Expand Down
76 changes: 74 additions & 2 deletions compose/compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ services:
ACTION_DB_PASSWORD: ${ACTION_DB_PASSWORD:?set ACTION_DB_PASSWORD}
CONVERSATION_DB_USER: ${CONVERSATION_DB_USER:?set CONVERSATION_DB_USER}
CONVERSATION_DB_PASSWORD: ${CONVERSATION_DB_PASSWORD:?set CONVERSATION_DB_PASSWORD}
CONNECTION_DB_USER: ${CONNECTION_DB_USER:?set CONNECTION_DB_USER}
CONNECTION_DB_PASSWORD: ${CONNECTION_DB_PASSWORD:?set CONNECTION_DB_PASSWORD}
TELEGRAM_DB_USER: ${TELEGRAM_DB_USER:?set TELEGRAM_DB_USER}
TELEGRAM_DB_PASSWORD: ${TELEGRAM_DB_PASSWORD:?set TELEGRAM_DB_PASSWORD}
ports:
Expand Down Expand Up @@ -112,6 +114,8 @@ services:
ACTION_DB_PASSWORD: ${ACTION_DB_PASSWORD:?set ACTION_DB_PASSWORD}
CONVERSATION_DB_USER: ${CONVERSATION_DB_USER:?set CONVERSATION_DB_USER}
CONVERSATION_DB_PASSWORD: ${CONVERSATION_DB_PASSWORD:?set CONVERSATION_DB_PASSWORD}
CONNECTION_DB_USER: ${CONNECTION_DB_USER:?set CONNECTION_DB_USER}
CONNECTION_DB_PASSWORD: ${CONNECTION_DB_PASSWORD:?set CONNECTION_DB_PASSWORD}
TELEGRAM_DB_USER: ${TELEGRAM_DB_USER:?set TELEGRAM_DB_USER}
TELEGRAM_DB_PASSWORD: ${TELEGRAM_DB_PASSWORD:?set TELEGRAM_DB_PASSWORD}
volumes:
Expand Down Expand Up @@ -271,6 +275,7 @@ services:
profiles: [apps]
image: ${CALENDAR_MCP_IMAGE:?set CALENDAR_MCP_IMAGE}
environment:
CALENDAR_PROVIDER: fake-calendar
CALENDAR_TEST_API_ENABLED: "true"
CALENDAR_TEST_API_KEY: ${CALENDAR_TEST_API_KEY:?set CALENDAR_TEST_API_KEY}
CALENDAR_OIDC_ISSUER_URL: http://localhost:${KEYCLOAK_PORT:-8081}/realms/portable-agent
Expand All @@ -292,6 +297,71 @@ services:
retries: 30
networks: [platform]

connection-service:
profiles: [apps]
image: ${CONNECTION_SERVICE_IMAGE:?set CONNECTION_SERVICE_IMAGE}
environment:
DATABASE_URL: jdbc:postgresql://postgres:5432/connections
DATABASE_USER: ${CONNECTION_DB_USER:?set CONNECTION_DB_USER}
DATABASE_PASSWORD: ${CONNECTION_DB_PASSWORD:?set CONNECTION_DB_PASSWORD}
OIDC_ISSUER: http://localhost:${KEYCLOAK_PORT:-8081}/realms/portable-agent
OIDC_JWKS_URL: http://keycloak:8080/realms/portable-agent/protocol/openid-connect/certs
OIDC_AUDIENCE: connection-service
OIDC_INTERNAL_CLIENTS: action-service
CONNECTION_TOKEN_KEYS_ENABLED: "true"
CONNECTION_TOKEN_KEYS_CURRENT_VERSION: "1"
CONNECTION_TOKEN_KEYS_ITEMS_0_VERSION: "1"
CONNECTION_TOKEN_KEYS_ITEMS_0_VALUE: ${CONNECTION_TOKEN_KEY_BASE64:?set CONNECTION_TOKEN_KEY_BASE64}
GOOGLE_OAUTH_ENABLED: ${GOOGLE_OAUTH_ENABLED:-false}
GOOGLE_OAUTH_CLIENT_ID: ${GOOGLE_OAUTH_CLIENT_ID:-}
GOOGLE_OAUTH_CLIENT_SECRET: ${GOOGLE_OAUTH_CLIENT_SECRET:-}
GOOGLE_OAUTH_REDIRECT_URI: ${GOOGLE_OAUTH_REDIRECT_URI:-}
depends_on:
postgres:
condition: service_healthy
keycloak:
condition: service_healthy
ports:
- "127.0.0.1:${CONNECTION_SERVICE_PORT:-18088}:8080"
healthcheck:
test:
[
CMD-SHELL,
"bash -c 'exec 3<>/dev/tcp/127.0.0.1/8080 && printf \"GET /actuator/health/readiness HTTP/1.1\\r\\nHost: localhost\\r\\nConnection: close\\r\\n\\r\\n\" >&3 && grep -q \"HTTP/1.1 200 \" <&3'",
]
interval: 5s
timeout: 3s
retries: 30
start_period: 20s
networks: [platform]

google-calendar-mcp:
profiles: [apps]
image: ${CALENDAR_MCP_IMAGE:?set CALENDAR_MCP_IMAGE}
environment:
CALENDAR_PROVIDER: google-calendar
CALENDAR_OIDC_ISSUER_URL: http://localhost:${KEYCLOAK_PORT:-8081}/realms/portable-agent
CALENDAR_OIDC_JWKS_URL: http://keycloak:8080/realms/portable-agent/protocol/openid-connect/certs
CALENDAR_OIDC_AUDIENCE: calendar-mcp
CALENDAR_MCP_RESOURCE_URL: http://google-calendar-mcp:8080/mcp
CALENDAR_MCP_REQUIRED_SCOPES: '["calendar:write"]'
CALENDAR_MCP_ALLOWED_HOSTS: '["google-calendar-mcp:8080","localhost:*","127.0.0.1:*"]'
CALENDAR_MCP_ALLOWED_ORIGINS: '["http://google-calendar-mcp:8080","http://localhost:*","http://127.0.0.1:*"]'
CALENDAR_CONNECTION_URL: http://connection-service:8080
depends_on:
keycloak:
condition: service_healthy
connection-service:
condition: service_healthy
ports:
- "127.0.0.1:${GOOGLE_CALENDAR_MCP_PORT:-18089}:8080"
healthcheck:
test: [CMD, python, -c, "import urllib.request; urllib.request.urlopen('http://localhost:8080/health')"]
interval: 5s
timeout: 3s
retries: 30
networks: [platform]

mcp-gateway:
profiles: [apps]
image: ${MCP_GATEWAY_IMAGE:?set MCP_GATEWAY_IMAGE}
Expand All @@ -303,12 +373,14 @@ services:
OIDC_JWKS_URL: http://keycloak:8080/realms/portable-agent/protocol/openid-connect/certs
OIDC_AUDIENCE: mcp-gateway
OIDC_REQUIRED_SCOPE: mcp:call
MCP_CONNECTORS_JSON: '[{"name":"fake-calendar","url":"http://calendar-mcp:8080/mcp","tools":["create_event"]}]'
MCP_CONNECTORS_JSON: '[{"name":"fake-calendar","url":"http://calendar-mcp:8080/mcp","tools":["create_event"]},{"name":"google-calendar","url":"http://google-calendar-mcp:8080/mcp","tools":["create_event"]}]'
depends_on:
keycloak:
condition: service_healthy
calendar-mcp:
condition: service_healthy
google-calendar-mcp:
condition: service_healthy
ports:
- "127.0.0.1:${MCP_GATEWAY_PORT:-18083}:8080"
healthcheck:
Expand Down Expand Up @@ -336,7 +408,7 @@ services:
MCP_GATEWAY_TOKEN_URL: http://keycloak:8080/realms/portable-agent/protocol/openid-connect/token
MCP_GATEWAY_CLIENT_ID: action-service
MCP_GATEWAY_CLIENT_SECRET: ${ACTION_SERVICE_CLIENT_SECRET:?set ACTION_SERVICE_CLIENT_SECRET}
MCP_GATEWAY_SCOPE: mcp:call calendar:write
MCP_GATEWAY_SCOPE: mcp:call calendar:write connection:token
MCP_GATEWAY_TENANT_ID: ${ACTION_SERVICE_TENANT_ID:?set ACTION_SERVICE_TENANT_ID}
MCP_GATEWAY_CONNECT_TIMEOUT: 3s
MCP_GATEWAY_READ_TIMEOUT: 10s
Expand Down
52 changes: 51 additions & 1 deletion compose/keycloak/portable-agent-realm.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@
"include.in.token.scope": "true",
"display.on.consent.screen": "false"
}
},
{
"name": "connection:token",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "true",
"display.on.consent.screen": "false"
}
}
],
"clients": [
Expand All @@ -65,6 +73,13 @@
"bearerOnly": true,
"protocol": "openid-connect"
},
{
"clientId": "connection-service",
"name": "Connection Service Resource Server",
"enabled": true,
"bearerOnly": true,
"protocol": "openid-connect"
},
{
"clientId": "mcp-gateway",
"name": "MCP Gateway Resource Server",
Expand All @@ -90,7 +105,7 @@
"clientAuthenticatorType": "client-secret",
"secret": "${ACTION_SERVICE_CLIENT_SECRET}",
"protocol": "openid-connect",
"defaultClientScopes": ["basic", "mcp:call", "calendar:write"],
"defaultClientScopes": ["basic", "mcp:call", "calendar:write", "connection:token"],
"protocolMappers": [
{
"name": "tenant-id",
Expand Down Expand Up @@ -126,6 +141,17 @@
"access.token.claim": "true",
"introspection.token.claim": "true"
}
},
{
"name": "connection-service-audience",
"protocol": "openid-connect",
"protocolMapper": "oidc-audience-mapper",
"consentRequired": false,
"config": {
"included.client.audience": "connection-service",
"access.token.claim": "true",
"introspection.token.claim": "true"
}
}
]
},
Expand Down Expand Up @@ -210,6 +236,18 @@
"access.token.claim": "true",
"introspection.token.claim": "true"
}
},
{
"name": "connection-service-audience",
"protocol": "openid-connect",
"protocolMapper": "oidc-audience-mapper",
"consentRequired": false,
"config": {
"included.client.audience": "connection-service",
"id.token.claim": "false",
"access.token.claim": "true",
"introspection.token.claim": "true"
}
}
]
},
Expand Down Expand Up @@ -299,6 +337,18 @@
"access.token.claim": "true",
"introspection.token.claim": "true"
}
},
{
"name": "connection-service-audience",
"protocol": "openid-connect",
"protocolMapper": "oidc-audience-mapper",
"consentRequired": false,
"config": {
"included.client.audience": "connection-service",
"id.token.claim": "false",
"access.token.claim": "true",
"introspection.token.claim": "true"
}
}
]
}
Expand Down
11 changes: 11 additions & 0 deletions compose/postgres/init/01-users.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ set -eu
: "${ACTION_DB_PASSWORD:?ACTION_DB_PASSWORD is required}"
: "${CONVERSATION_DB_USER:?CONVERSATION_DB_USER is required}"
: "${CONVERSATION_DB_PASSWORD:?CONVERSATION_DB_PASSWORD is required}"
: "${CONNECTION_DB_USER:?CONNECTION_DB_USER is required}"
: "${CONNECTION_DB_PASSWORD:?CONNECTION_DB_PASSWORD is required}"
: "${TELEGRAM_DB_USER:?TELEGRAM_DB_USER is required}"
: "${TELEGRAM_DB_PASSWORD:?TELEGRAM_DB_PASSWORD is required}"

Expand All @@ -22,6 +24,8 @@ psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" \
--set=action_password="$ACTION_DB_PASSWORD" \
--set=conversation_user="$CONVERSATION_DB_USER" \
--set=conversation_password="$CONVERSATION_DB_PASSWORD" \
--set=connection_user="$CONNECTION_DB_USER" \
--set=connection_password="$CONNECTION_DB_PASSWORD" \
--set=telegram_user="$TELEGRAM_DB_USER" \
--set=telegram_password="$TELEGRAM_DB_PASSWORD" <<-'SQL'
SELECT format('CREATE ROLE %I LOGIN PASSWORD %L', :'keycloak_user', :'keycloak_password')
Expand Down Expand Up @@ -49,6 +53,13 @@ SELECT format('ALTER ROLE %I WITH LOGIN PASSWORD %L', :'conversation_user', :'co
SELECT format('CREATE DATABASE conversations OWNER %I', :'conversation_user')
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'conversations') \gexec

SELECT format('CREATE ROLE %I LOGIN PASSWORD %L', :'connection_user', :'connection_password')
WHERE NOT EXISTS (SELECT FROM pg_roles WHERE rolname = :'connection_user') \gexec
SELECT format('ALTER ROLE %I WITH LOGIN PASSWORD %L', :'connection_user', :'connection_password') \gexec

SELECT format('CREATE DATABASE connections OWNER %I', :'connection_user')
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'connections') \gexec

SELECT format('CREATE ROLE %I LOGIN PASSWORD %L', :'telegram_user', :'telegram_password')
WHERE NOT EXISTS (SELECT FROM pg_roles WHERE rolname = :'telegram_user') \gexec
SELECT format('ALTER ROLE %I WITH LOGIN PASSWORD %L', :'telegram_user', :'telegram_password') \gexec
Expand Down
3 changes: 2 additions & 1 deletion config/versions.env
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ AGENT_RUNTIME_IMAGE=ghcr.io/portable-agent/agent-runtime:095509e8c51c6bc311b86d6
ACTION_SERVICE_IMAGE=ghcr.io/portable-agent/action-service:aa854b24555984ba92106ce3de4407f2940d7f6a
CONVERSATION_SERVICE_IMAGE=ghcr.io/portable-agent/conversation-service:fb069a35c6a8406f88f665e8197ebfbc3564008a
MCP_GATEWAY_IMAGE=ghcr.io/portable-agent/mcp-gateway:922390b293345f7483dcdee20d5020f417bd7f39
CALENDAR_MCP_IMAGE=ghcr.io/portable-agent/calendar-mcp:1bcaf92919c31b9abce62cf4210467051a8cb668
CALENDAR_MCP_IMAGE=ghcr.io/portable-agent/calendar-mcp:30b869b4f9f1c23a4be59e8cf5a3b3a3d3c89799
CONNECTION_SERVICE_IMAGE=ghcr.io/portable-agent/connection-service:f86ff7b1b3fa4da16ff580d5e36f29eccdbf1c1a
TELEGRAM_ADAPTER_IMAGE=ghcr.io/portable-agent/telegram-adapter:232119576688e36a609546267b483da19d3ccc38
TEST_LAB_REF=6969e1bbae930146cd149276167137ff74fb2e98
Loading
Loading