# Deployment Architecture — Matrix Bot + Agents

> Сформировано 2026-04-27 по итогам обсуждения с платформой.

---

## Compose Artifacts

- **Production deploy:** `docker-compose.prod.yml`
  Bot-only handoff через published image (`SURFACES_BOT_IMAGE`). Поднимает только `matrix-bot`, монтирует shared volume в `/agents`.
  Платформа предоставляет 25-30 agent containers/services отдельно; бот подключается к ним через `base_url` из `matrix-agents.yaml`.
- **Internal full-stack E2E:** `docker-compose.fullstack.yml`
  Внутренний harness для тестирования. Локально собирает `matrix-bot` через Dockerfile target `development`, поднимает один `platform-agent`, health-gated startup.

Production operators: `docker-compose.prod.yml`. Internal E2E: `docker-compose.fullstack.yml`.

---

## Топология

```
lambda.coredump.ru
├── :7000  (reverse proxy, path-based routing)
│   ├── /agent_0/ → agent_0 container
│   ├── /agent_1/ → agent_1 container
│   └── /agent_N/ → agent_N container
│
└── Matrix bot instance (один инстанс на всех)
    └── volume /agents/ (shared с агентами)
        ├── /agents/0/   ← workspace agent_0
        ├── /agents/1/   ← workspace agent_1
        └── /agents/N/
```

- **Один инстанс Matrix-бота** обслуживает всех пользователей.
- **Один агент-контейнер на пользователя.** Production scale target: 25-30 внешних агентов. Изоляция по `agent_id`, не через один общий agent instance.
- **Shared volume** `/agents/` смонтирован в Matrix-бот. В internal full-stack harness тот же volume mounted as `/workspace` inside `platform-agent`, чтобы bot-side absolute paths и agent workspace относились к одному и тому же хранилищу.

---

## Конфиг (два словаря)

```yaml
# config/matrix-agents.yaml

user_agents:
  "@user0:matrix.lambda.coredump.ru": agent-0
  "@user1:matrix.lambda.coredump.ru": agent-1
  "@user2:matrix.lambda.coredump.ru": agent-2

agents:
  - id: agent-0
    label: "Agent 0"
    base_url: "http://lambda.coredump.ru:7000/agent_0/"
    workspace_path: "/agents/0"

  - id: agent-1
    label: "Agent 1"
    base_url: "http://lambda.coredump.ru:7000/agent_1/"
    workspace_path: "/agents/1"

  - id: agent-2
    label: "Agent 2"
    base_url: "http://lambda.coredump.ru:7000/agent_2/"
    workspace_path: "/agents/2"
```

- `user_agents` — маппинг Matrix user_id → agent_id. Если пользователь не найден — используется первый агент из списка.
- `agents[].base_url` — HTTP URL агент-эндпоинта. Бот подключается через AgentApi.
- `agents[].workspace_path` — абсолютный путь к воркспейсу агента **внутри контейнера бота** (т.е. на shared volume).
  Бот сохраняет входящие файлы прямо в `{workspace_path}/`, читает исходящие из `{workspace_path}/`.
- Для 25-30 агентов продолжайте тот же паттерн до нужного номера: `/agent_17/` + `/agents/17`, `/agent_29/` + `/agents/29`.

## Surface Image Build Contract

Production image содержит только Matrix surface. Он не содержит `platform-agent` и не требует локального `external/` build context.

```bash
docker login
export SURFACES_BOT_IMAGE=mput1/surfaces-bot:latest

docker build --target production \
  --build-arg LAMBDA_AGENT_API_REF=master \
  -t "$SURFACES_BOT_IMAGE" .
docker push "$SURFACES_BOT_IMAGE"
```

Published image:

```text
mput1/surfaces-bot:latest
sha256:2f135f3535f7765d4377b440cdabe41195ad2efbc3e175def159ae4689ef90bd
```

`SURFACES_BOT_IMAGE` должен указывать на registry namespace, куда текущий Docker account может пушить. Ошибка `insufficient_scope` означает, что пользователь не залогинен в этот namespace, repository не создан, или у аккаунта нет push-доступа.

Production Dockerfile ставит `platform/agent_api` из Git по тому же принципу, что и `platform-agent` production image:

```bash
git+https://git.lambda.coredump.ru/platform/agent_api.git
```

Локальный `docker-compose.fullstack.yml` остаётся dev/E2E harness: он использует target `development` и `additional_contexts.agent_api=./external/platform-agent_api`, чтобы можно было тестировать surface вместе с локальным checkout SDK.

---

## Agent API (используем master ветку `platform/agent_api`)

```python
from lambda_agent_api.agent_api import AgentApi

connected_agents: dict[tuple[str, int], AgentApi] = {}

def on_agent_disconnect(agent: AgentApi):
    connected_agents.pop((agent.id, agent.chat_id), None)

async def on_message(matrix_user_id: str, matrix_room_id: str, text: str):
    agent_id = get_agent_id_by_user(matrix_user_id)  # из user_agents конфига
    platform_chat_id = get_room_platform_chat_id(matrix_room_id)

    agent = connected_agents.get((agent_id, platform_chat_id))
    if not agent:
        agent = AgentApi(
            agent_id,
            get_agent_base_url(agent_id),        # ws://lambda.coredump.ru:7000/agent_0/
            on_disconnect=on_agent_disconnect,
            chat_id=platform_chat_id,            # отдельный thread на Matrix room
        )
        await agent.connect()
        connected_agents[(agent_id, platform_chat_id)] = agent

    async for event in agent.send_message(text):
        ...
```

**Параметры конструктора (master):**
```python
AgentApi(
    agent_id: str,
    base_url: str,            # ws://host:port/agent_N/
    chat_id: int = 0,         # surfaces must supply per-room platform_chat_id
    on_disconnect: callable,
)
```

**Lifecycle:** агент автоматически отключается после нескольких минут бездействия.
`on_disconnect` удаляет из пула → следующее сообщение создаёт новое соединение.

---

## Передача файлов

### Пользователь → Агент (входящий файл)

1. Matrix-бот получает файл от пользователя
2. Сохраняет в workspace агента: `/agents/{N}/{filename}`
3. Если файл уже существует, выбирает следующее имя: `filename (1).ext`, `filename (2).ext`
4. Вызывает `agent.send_message(text, attachments=["filename"])`
   — путь относительно `/workspace` агента

### Агент → Пользователь (исходящий файл)

1. Агент эмитит `MsgEventSendFile(path="report.pdf")`
2. Matrix-бот читает файл: `/agents/{N}/report.pdf`
3. Отправляет как Matrix file message пользователю

**Ключевое:** production handoff через `docker-compose.prod.yml` и internal E2E через `docker-compose.fullstack.yml` используют один и тот же `/agents` contract на стороне поверхности. Прямой HTTP-доступ к файлам не нужен.

---

## Текущее состояние platform-agent (main)

- Composio интегрирован в main (`#9-интеграция-composIO`)
- Агент требует в `.env`: `AGENT_ID`, `COMPOSIO_API_KEY`
- Backend: `IsolatedShellBackend` (main) / `CompositeBackend` (ветка `#19`, не merged)
- Memory: `MemorySaver` — история слетает при рестарте контейнера (known limitation)

---

## platform-master (будущее, пока не используем)

Ветка `feat/storage` реализует реальный Master-сервис:
- `POST /api/v1/create {chat_id}` → поднимает/переиспользует sandbox-контейнер
- TTL-based lifecycle (300с default, конфигурируемо)
- `ChatStorage` — API для upload/download файлов через Master
- Auth + p2p lease — вне текущего scope MVP

**Для деплоя MVP используем статический конфиг без Master.**
При готовности Master: `get_agent_url()` будет вызывать `POST /api/v1/create`, URL возвращается в ответе.

---

## Открытые вопросы

- `platform-agent_api #9-clientside-tool-call` убирает `attachments` и `MsgEventSendFile` — пока используем master. Уточнить у платформы сроки мержа перед деплоем.
- История при рестарте агента теряется — `platform-agent` использует `MemorySaver` (in-memory). Ограничение платформы.
- `AGENT_ID` и `COMPOSIO_API_KEY` для каждого агент-контейнера — значения предоставляет платформа.
