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

TELEGRAM_DB_USER=telegram
TELEGRAM_DB_PASSWORD=local_telegram_db_change_me
TELEGRAM_ADAPTER_CLIENT_SECRET=local_telegram_client_change_me
TELEGRAM_ADAPTER_PORT=18086
TELEGRAM_FAKE_PORT=18087
TELEGRAM_BOT_TOKEN=123456789:local_fake_bot_token
TELEGRAM_WEBHOOK_SECRET=local_telegram_webhook_secret_32_chars
TELEGRAM_TOKEN_KEY_BASE64=derive-from-local-webhook-secret

AGENT_RUNTIME_PORT=18080
CHANNEL_GATEWAY_PORT=18084
MCP_GATEWAY_PORT=18083
Expand All @@ -28,6 +37,7 @@ ACTION_SERVICE_CONTEXT=../../portable-agent-action-service
MCP_GATEWAY_CONTEXT=../../portable-agent-mcp-gateway
CALENDAR_MCP_CONTEXT=../../portable-agent-calendar-mcp
CONVERSATION_SERVICE_CONTEXT=../../portable-agent-conversation-service
TELEGRAM_ADAPTER_CONTEXT=../../portable-agent-telegram-adapter
TEST_LAB_PATH=../portable-agent-test-lab

KEYCLOAK_DB_USER=keycloak
Expand Down
10 changes: 10 additions & 0 deletions .github/workflows/app-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ on:
- "scripts/run-test-lab.ps1"
- "scripts/check-compose.ps1"
- "scripts/check-keycloak.ps1"
- "scripts/local-settings.ps1"
- "scripts/start-local.ps1"
- "scripts/service-local.ps1"
- "scripts/stop-local.ps1"
Expand All @@ -32,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
TELEGRAM_ADAPTER_CONTEXT: ${{ github.workspace }}/repos/telegram-adapter
TEST_LAB_PATH: ${{ github.workspace }}/repos/test-lab
steps:
- name: Получить deploy
Expand All @@ -48,6 +50,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
"telegram=$($versions.TELEGRAM_ADAPTER_IMAGE.Split(':')[-1])" >> $env:GITHUB_OUTPUT
"test_lab=$($versions.TEST_LAB_REF)" >> $env:GITHUB_OUTPUT

- name: Получить Channel Gateway
Expand Down Expand Up @@ -92,6 +95,13 @@ jobs:
ref: ${{ steps.versions.outputs.calendar }}
path: repos/calendar-mcp

- name: Получить Telegram Adapter
uses: actions/checkout@v7
with:
repository: portable-agent/telegram-adapter
ref: ${{ steps.versions.outputs.telegram }}
path: repos/telegram-adapter

- name: Получить Test Lab
uses: actions/checkout@v7
with:
Expand Down
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
Платформа разработки включает:

- Compose-профиль `core`: PostgreSQL, Redpanda, Keycloak, Temporal и OPA;
- Compose-профиль `apps`: Channel Gateway, Conversation Service, Agent Runtime, Action Service, MCP Gateway и Calendar MCP;
- Compose-профиль `apps`: Telegram Adapter, fake Telegram API, Channel Gateway, Conversation Service, Agent Runtime, Action Service, MCP Gateway и Calendar MCP;
- Compose-профиль `observe`: OpenTelemetry, Prometheus, Grafana, Tempo и Loki;
- безопасный общий chart `charts/service`;
- изолированный PR preview namespace с quota, limits и default-deny сетью;
Expand Down Expand Up @@ -74,8 +74,12 @@ Compose project и всегда удаляет только созданные

`-Apps` собирает образы из соседних локальных репозиториев через `compose/apps.local.yaml`. Пути можно
переопределить переменными `CHANNEL_GATEWAY_CONTEXT`, `AGENT_RUNTIME_CONTEXT`, `ACTION_SERVICE_CONTEXT`,
`CONVERSATION_SERVICE_CONTEXT`, `MCP_GATEWAY_CONTEXT` и `CALENDAR_MCP_CONTEXT`; вход в GHCR для локального
запуска не нужен.
`CONVERSATION_SERVICE_CONTEXT`, `MCP_GATEWAY_CONTEXT`, `CALENDAR_MCP_CONTEXT` и
`TELEGRAM_ADAPTER_CONTEXT`; вход в GHCR для локального запуска не нужен. Fake Telegram API отвечает
локально и не требует настоящего bot token.
Если в `.env` оставлен placeholder `derive-from-local-webhook-secret`, команда запуска получает
стабильный локальный encryption key из webhook secret только в памяти процесса. Для любого общего
окружения `TELEGRAM_TOKEN_KEY_BASE64` должен приходить отдельным случайным секретом.
`TEST_LAB_PATH` по умолчанию указывает на соседний `../portable-agent-test-lab`; путь можно
переопределить в `.env`. Channel Gateway доступен на `http://localhost:18084`, Agent Runtime — на
`http://localhost:18080`, Action API — на
Expand All @@ -93,8 +97,10 @@ manager, а не из Git.
`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 приходят из `.env`, а не зашиты в image. PostgreSQL создаёт отдельную БД
`actions` для Action Service и `conversations` для Conversation Service.
локального секрета и tenant приходят из `.env`, а не зашиты в image. Отдельный confidential client
`telegram-adapter` включает OAuth Device Authorization Grant и выдаёт пользовательский токен с теми
же `tenant_id` и audience backend-пути. PostgreSQL создаёт отдельные БД `actions`, `conversations` и
`telegram_adapter`.

Проверочный API Calendar MCP включён только в локальном профиле `apps`, привязан к loopback-порту и
защищён `CALENDAR_TEST_API_KEY`. Хранилище fake-calendar пока находится в памяти: перезапуск контейнера
Expand Down
6 changes: 6 additions & 0 deletions compose/apps.local.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,9 @@ services:
build:
context: ${CALENDAR_MCP_CONTEXT:-../../portable-agent-calendar-mcp}
pull_policy: build

telegram-adapter:
image: portable-agent/telegram-adapter:local
build:
context: ${TELEGRAM_ADAPTER_CONTEXT:-../../portable-agent-telegram-adapter}
pull_policy: build
53 changes: 53 additions & 0 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}
TELEGRAM_DB_USER: ${TELEGRAM_DB_USER:?set TELEGRAM_DB_USER}
TELEGRAM_DB_PASSWORD: ${TELEGRAM_DB_PASSWORD:?set TELEGRAM_DB_PASSWORD}
ports:
- "127.0.0.1:${POSTGRES_PORT:-5432}:5432"
volumes:
Expand Down Expand Up @@ -73,6 +75,7 @@ services:
KC_HOSTNAME: http://localhost:${KEYCLOAK_PORT:-8081}
ACTION_SERVICE_CLIENT_SECRET: ${ACTION_SERVICE_CLIENT_SECRET:?set ACTION_SERVICE_CLIENT_SECRET}
ACTION_SERVICE_TENANT_ID: ${ACTION_SERVICE_TENANT_ID:?set ACTION_SERVICE_TENANT_ID}
TELEGRAM_ADAPTER_CLIENT_SECRET: ${TELEGRAM_ADAPTER_CLIENT_SECRET:?set TELEGRAM_ADAPTER_CLIENT_SECRET}
depends_on:
postgres:
condition: service_healthy
Expand Down Expand Up @@ -109,6 +112,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}
TELEGRAM_DB_USER: ${TELEGRAM_DB_USER:?set TELEGRAM_DB_USER}
TELEGRAM_DB_PASSWORD: ${TELEGRAM_DB_PASSWORD:?set TELEGRAM_DB_PASSWORD}
volumes:
- ./postgres:/scripts:ro
depends_on:
Expand Down Expand Up @@ -395,6 +400,54 @@ services:
start_period: 20s
networks: [platform]

fake-telegram:
profiles: [apps]
image: ${WIREMOCK_IMAGE:?set WIREMOCK_IMAGE}
command: [--disable-banner]
volumes:
- ./telegram:/home/wiremock:ro
ports:
- "127.0.0.1:${TELEGRAM_FAKE_PORT:-18087}:8080"
networks: [platform]

telegram-adapter:
profiles: [apps]
image: ${TELEGRAM_ADAPTER_IMAGE:?set TELEGRAM_ADAPTER_IMAGE}
environment:
HOST: 0.0.0.0
PORT: 8080
TELEGRAM_API_URL: http://fake-telegram:8080
TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN:?set TELEGRAM_BOT_TOKEN}
TELEGRAM_WEBHOOK_SECRET: ${TELEGRAM_WEBHOOK_SECRET:?set TELEGRAM_WEBHOOK_SECRET}
KEYCLOAK_URL: http://keycloak:8080
KEYCLOAK_REALM: portable-agent
KEYCLOAK_CLIENT_ID: telegram-adapter
KEYCLOAK_CLIENT_SECRET: ${TELEGRAM_ADAPTER_CLIENT_SECRET:?set TELEGRAM_ADAPTER_CLIENT_SECRET}
DATABASE_URL: postgres://${TELEGRAM_DB_USER:?set TELEGRAM_DB_USER}:${TELEGRAM_DB_PASSWORD:?set TELEGRAM_DB_PASSWORD}@postgres:5432/telegram_adapter
CHANNEL_GATEWAY_URL: http://channel-gateway:8080
CHANNEL_GATEWAY_TIMEOUT_MS: 10000
LINK_POLL_MS: 1000
DEFAULT_LOCALE: ru-RU
DEFAULT_TIME_ZONE: Europe/Moscow
TOKEN_KEY_BASE64: ${TELEGRAM_TOKEN_KEY_BASE64:?set TELEGRAM_TOKEN_KEY_BASE64}
depends_on:
postgres:
condition: service_healthy
keycloak:
condition: service_healthy
channel-gateway:
condition: service_healthy
fake-telegram:
condition: service_started
ports:
- "127.0.0.1:${TELEGRAM_ADAPTER_PORT:-18086}:8080"
healthcheck:
test: [CMD, node, -e, "fetch('http://localhost:8080/health/ready').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"]
interval: 5s
timeout: 3s
retries: 30
networks: [platform]

tempo:
profiles: [observe]
image: ${TEMPO_IMAGE:?set TEMPO_IMAGE}
Expand Down
84 changes: 84 additions & 0 deletions compose/keycloak/portable-agent-realm.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,90 @@
}
]
},
{
"clientId": "telegram-adapter",
"name": "Telegram Adapter",
"enabled": true,
"publicClient": false,
"standardFlowEnabled": true,
"directAccessGrantsEnabled": false,
"serviceAccountsEnabled": false,
"clientAuthenticatorType": "client-secret",
"secret": "${TELEGRAM_ADAPTER_CLIENT_SECRET}",
"protocol": "openid-connect",
"attributes": {
"oauth2.device.authorization.grant.enabled": "true",
"use.refresh.tokens": "true"
},
"defaultClientScopes": ["basic"],
"optionalClientScopes": ["offline_access"],
"protocolMappers": [
{
"name": "tenant-id",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"consentRequired": false,
"config": {
"user.attribute": "tenant_id",
"claim.name": "tenant_id",
"jsonType.label": "String",
"multivalued": "false",
"id.token.claim": "true",
"access.token.claim": "true",
"userinfo.token.claim": "true",
"introspection.token.claim": "true"
}
},
{
"name": "channel-gateway-audience",
"protocol": "openid-connect",
"protocolMapper": "oidc-audience-mapper",
"consentRequired": false,
"config": {
"included.client.audience": "channel-gateway",
"id.token.claim": "false",
"access.token.claim": "true",
"introspection.token.claim": "true"
}
},
{
"name": "agent-runtime-audience",
"protocol": "openid-connect",
"protocolMapper": "oidc-audience-mapper",
"consentRequired": false,
"config": {
"included.client.audience": "agent-runtime",
"id.token.claim": "false",
"access.token.claim": "true",
"introspection.token.claim": "true"
}
},
{
"name": "action-service-audience",
"protocol": "openid-connect",
"protocolMapper": "oidc-audience-mapper",
"consentRequired": false,
"config": {
"included.client.audience": "action-service",
"id.token.claim": "false",
"access.token.claim": "true",
"introspection.token.claim": "true"
}
},
{
"name": "conversation-service-audience",
"protocol": "openid-connect",
"protocolMapper": "oidc-audience-mapper",
"consentRequired": false,
"config": {
"included.client.audience": "conversation-service",
"id.token.claim": "false",
"access.token.claim": "true",
"introspection.token.claim": "true"
}
}
]
},
{
"clientId": "portable-agent-local",
"name": "Portable Agent Local Test",
Expand Down
13 changes: 12 additions & 1 deletion 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}"
: "${TELEGRAM_DB_USER:?TELEGRAM_DB_USER is required}"
: "${TELEGRAM_DB_PASSWORD:?TELEGRAM_DB_PASSWORD is required}"

psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" \
--set=keycloak_user="$KEYCLOAK_DB_USER" \
Expand All @@ -19,7 +21,9 @@ psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" \
--set=action_user="$ACTION_DB_USER" \
--set=action_password="$ACTION_DB_PASSWORD" \
--set=conversation_user="$CONVERSATION_DB_USER" \
--set=conversation_password="$CONVERSATION_DB_PASSWORD" <<-'SQL'
--set=conversation_password="$CONVERSATION_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')
WHERE NOT EXISTS (SELECT FROM pg_roles WHERE rolname = :'keycloak_user') \gexec
SELECT format('ALTER ROLE %I WITH LOGIN PASSWORD %L', :'keycloak_user', :'keycloak_password') \gexec
Expand All @@ -44,4 +48,11 @@ 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', :'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

SELECT format('CREATE DATABASE telegram_adapter OWNER %I', :'telegram_user')
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'telegram_adapter') \gexec
SQL
16 changes: 16 additions & 0 deletions compose/telegram/mappings/bot-api.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"request": {
"method": "POST",
"urlPathPattern": "/bot[^/]+/(sendMessage|answerCallbackQuery)"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"jsonBody": {
"ok": true,
"result": true
}
}
}
2 changes: 2 additions & 0 deletions config/versions.env
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ GRAFANA_IMAGE=grafana/grafana:13.2.0
TEMPO_IMAGE=grafana/tempo:3.0.3
LOKI_IMAGE=grafana/loki:3.7.7
BUSYBOX_IMAGE=busybox:1.37.0
WIREMOCK_IMAGE=wiremock/wiremock:3.13.2
CHANNEL_GATEWAY_IMAGE=ghcr.io/portable-agent/channel-gateway:65e8406b017e81ac6993818290f211a9b2c845aa
AGENT_RUNTIME_IMAGE=ghcr.io/portable-agent/agent-runtime:4dd541e459d0bdbf889ddc1fbe68e1f7c3b47b89
ACTION_SERVICE_IMAGE=ghcr.io/portable-agent/action-service:8d449ab990001e31cd42a7655a16c5716c5d6f20
CONVERSATION_SERVICE_IMAGE=ghcr.io/portable-agent/conversation-service:fb069a35c6a8406f88f665e8197ebfbc3564008a
MCP_GATEWAY_IMAGE=ghcr.io/portable-agent/mcp-gateway:0a3884d52ad1156ed173a4ecf9182ea3a0d2e686
CALENDAR_MCP_IMAGE=ghcr.io/portable-agent/calendar-mcp:bbab64774eab482c9d3543c7b8220b4cb42df56e
TELEGRAM_ADAPTER_IMAGE=ghcr.io/portable-agent/telegram-adapter:232119576688e36a609546267b483da19d3ccc38
TEST_LAB_REF=26a8842c1b403bf50e6c67fdb7eaa7ae83d36d48
17 changes: 12 additions & 5 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ services/catalog.json -> environments/<env>/services/<name>/values.yaml
```mermaid
sequenceDiagram
participant User as Пользователь
participant Telegram as Telegram Adapter
participant Channel as Channel Gateway
participant Conversation as Conversation Service
participant Agent as Agent Runtime
Expand All @@ -38,9 +39,13 @@ sequenceDiagram
participant Gateway as MCP Gateway
participant Calendar as Calendar MCP

User->>Keycloak: логин
Keycloak-->>User: tenant + audience channel, conversation, agent и action
User->>Channel: текст + безопасный context + JWT
User->>Telegram: /link
Telegram->>Keycloak: начать Device Flow
Keycloak-->>User: ссылка и одноразовый код
User->>Keycloak: логин в браузере
Keycloak-->>Telegram: refresh token + tenant + audience
User->>Telegram: текст
Telegram->>Channel: текст + безопасный context + JWT
Channel->>Channel: JWT + нормализация канала
Channel->>Conversation: сообщение + тот же JWT
Conversation->>Conversation: сохранить сообщение
Expand All @@ -49,8 +54,10 @@ sequenceDiagram
Agent-->>Conversation: готовое предложение
Conversation->>Action: создать действие
Action-->>Conversation: actionId + payloadHash
Conversation-->>User: переносимый виджет подтверждения
User->>Channel: подтвердить виджет
Conversation-->>Telegram: переносимый виджет подтверждения
Telegram-->>User: inline-кнопки
User->>Telegram: подтвердить виджет
Telegram->>Channel: решение + JWT
Channel->>Action: actionId + payloadHash + решение
Action->>Action: JWT issuer + audience + tenant
Action->>Keycloak: client_credentials
Expand Down
Loading
Loading