New: app/control.py
- ControlConfig: operator_allowlist + control_rooms (frozensets)
- parse_control_config(): validates @user:server + !room:server formats, fail-fast
- parse_command(): parses !verb subcommand [args] [key=value] up to 512 chars
- check_authorization(): AND(is_control_room, is_operator) → (bool, reason)
- Reply helpers: not_implemented, unknown_command, unauthorized, help
- KNOWN_VERBS: runbook, status, help (M3.1+ stubs)
- MAX_CMD_LEN=512, MAX_CMD_TOKENS=20
ingress.py:
- _try_control(): dispatch for control rooms (authorized → audit + reply, unauthorized → audit + optional ⛔)
- join control rooms on startup
- _enqueue_from_sync: control rooms processed first, never forwarded to agents
- on_control_command(sender, verb, subcommand) metric callback
- CONTROL_UNAUTHORIZED_BEHAVIOR: "ignore" | "reply_error"
Audit events:
matrix.control.command — authorised command (verb, subcommand, args, kwargs)
matrix.control.unauthorized — rejected by allowlist (reason: not_operator | not_control_room)
matrix.control.unknown_cmd — authorised but unrecognised verb
Config + main:
- bridge_operator_allowlist, bridge_control_rooms, control_unauthorized_behavior
- matrix_bridge_control_commands_total{sender,verb,subcommand} counter
- /health: control_channel section (enabled, rooms_count, operators_count, behavior)
- /bridge/mappings: control_rooms + control_operators_count
- docker-compose: BRIDGE_OPERATOR_ALLOWLIST, BRIDGE_CONTROL_ROOMS, CONTROL_UNAUTHORIZED_BEHAVIOR
Tests: 40 new → 148 total pass
Made-with: Cursor
100 lines
4.6 KiB
YAML
100 lines
4.6 KiB
YAML
# Matrix Bridge DAGI — Phase M2.1 (multi-room + mixed routing)
|
|
# Include into the main NODA1 stack or run standalone:
|
|
# docker compose -f docker-compose.node1.yml -f docker-compose.matrix-bridge-node1.yml up -d matrix-bridge-dagi
|
|
|
|
version: "3.9"
|
|
|
|
services:
|
|
matrix-bridge-dagi:
|
|
build:
|
|
context: ./services/matrix-bridge-dagi
|
|
args:
|
|
BUILD_SHA: "${BUILD_SHA:-dev}"
|
|
BUILD_TIME: "${BUILD_TIME:-local}"
|
|
container_name: matrix-bridge-dagi-node1
|
|
ports:
|
|
- "127.0.0.1:7030:7030" # internal only — not exposed publicly
|
|
environment:
|
|
- PORT=7030
|
|
- ENV=prod
|
|
- NODE_ID=NODA1
|
|
- BUILD_SHA=${BUILD_SHA:-dev}
|
|
- BUILD_TIME=${BUILD_TIME:-local}
|
|
|
|
# ── Matrix homeserver ────────────────────────────────────────────────
|
|
# Required: set in .env on NODA1 before first launch
|
|
- MATRIX_HOMESERVER_URL=${MATRIX_HOMESERVER_URL:-}
|
|
- MATRIX_ACCESS_TOKEN=${MATRIX_ACCESS_TOKEN:-}
|
|
- MATRIX_USER_ID=${MATRIX_USER_ID:-}
|
|
|
|
# ── Room → Agent mapping (M1: single room for Sofiia) ────────────────
|
|
# Create the room manually, then paste the room_id here
|
|
- SOFIIA_ROOM_ID=${SOFIIA_ROOM_ID:-}
|
|
|
|
# ── DAGI backend — Router for /v1/agents/{id}/infer ─────────────────
|
|
# Router internal port 8000 on dagi-network (ext port 9102 on host)
|
|
- DAGI_GATEWAY_URL=http://dagi-router-node1:8000
|
|
- DEFAULT_NODE_ID=NODA1
|
|
|
|
# ── Sofiia Console (audit write) ─────────────────────────────────────
|
|
- SOFIIA_CONSOLE_URL=http://dagi-sofiia-console-node1:8002
|
|
- SOFIIA_INTERNAL_TOKEN=${SOFIIA_INTERNAL_TOKEN:-}
|
|
|
|
# ── H2: Backpressure queue ───────────────────────────────────────────
|
|
- QUEUE_MAX_EVENTS=100
|
|
- WORKER_CONCURRENCY=2
|
|
- QUEUE_DRAIN_TIMEOUT_S=5
|
|
|
|
# ── Policy ───────────────────────────────────────────────────────────
|
|
# M2.0+: multiple agents separated by comma
|
|
- BRIDGE_ALLOWED_AGENTS=${BRIDGE_ALLOWED_AGENTS:-sofiia}
|
|
# M2.0: "sofiia:!room1:server,helion:!room2:server" (1 room → 1 agent)
|
|
- BRIDGE_ROOM_MAP=${BRIDGE_ROOM_MAP:-}
|
|
- RATE_LIMIT_ROOM_RPM=20
|
|
- RATE_LIMIT_SENDER_RPM=10
|
|
|
|
# ── M2.1: Mixed rooms (1 room → N agents) ───────────────────────────
|
|
# Format: "!roomX:server=sofiia,helion;!roomY:server=druid"
|
|
- BRIDGE_MIXED_ROOM_MAP=${BRIDGE_MIXED_ROOM_MAP:-}
|
|
# Override default agent per mixed room (optional):
|
|
# "!roomX:server=helion;!roomY:server=druid"
|
|
- BRIDGE_MIXED_DEFAULTS=${BRIDGE_MIXED_DEFAULTS:-}
|
|
|
|
# ── M3.0: Operator control channel ──────────────────────────────────
|
|
# Comma-separated Matrix user IDs allowed to issue !commands
|
|
- BRIDGE_OPERATOR_ALLOWLIST=${BRIDGE_OPERATOR_ALLOWLIST:-}
|
|
# Comma-separated room IDs designated as ops control channels
|
|
- BRIDGE_CONTROL_ROOMS=${BRIDGE_CONTROL_ROOMS:-}
|
|
# "ignore" (silent) | "reply_error" (⛔ reply to unauthorised attempts)
|
|
- CONTROL_UNAUTHORIZED_BEHAVIOR=${CONTROL_UNAUTHORIZED_BEHAVIOR:-ignore}
|
|
|
|
# ── M2.2: Mixed room guard rails ────────────────────────────────────
|
|
# Fail-fast if any room defines more agents than this
|
|
- MAX_AGENTS_PER_MIXED_ROOM=${MAX_AGENTS_PER_MIXED_ROOM:-5}
|
|
# Reject slash commands longer than this (anti-garbage / injection guard)
|
|
- MAX_SLASH_LEN=${MAX_SLASH_LEN:-32}
|
|
# What to do when unknown /slash is used: "ignore" (silent) | "reply_error" (inform user)
|
|
- UNKNOWN_AGENT_BEHAVIOR=${UNKNOWN_AGENT_BEHAVIOR:-ignore}
|
|
# Max concurrent Router invocations per (room, agent) pair; 0 = unlimited
|
|
- MIXED_CONCURRENCY_CAP=${MIXED_CONCURRENCY_CAP:-1}
|
|
|
|
healthcheck:
|
|
test:
|
|
- "CMD"
|
|
- "python3"
|
|
- "-c"
|
|
- "import urllib.request; urllib.request.urlopen('http://localhost:7030/health', timeout=5)"
|
|
interval: 30s
|
|
timeout: 10s
|
|
retries: 3
|
|
start_period: 15s
|
|
|
|
networks:
|
|
- dagi-network
|
|
|
|
restart: unless-stopped
|
|
|
|
networks:
|
|
dagi-network:
|
|
external: true
|