Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
fa81c2f
feat(channels): add Max Messenger channel skeleton
May 6, 2026
2d28d0a
feat(channels/max): Max API client and inbound polling
May 6, 2026
02e087e
fix(channels/max): align with real API responses
May 6, 2026
71efed5
chore: gitignore local smoke test directory
May 6, 2026
e0bc2eb
test(channels/max): client and inbound translator tests
May 6, 2026
ede292a
feat(channels/max): outbound Send with chunking and markdown
May 6, 2026
a803b7c
feat(channels/max): webhook + media + reactions + auth
May 6, 2026
4887daf
feat(channels/max): streaming preview with answer-only mode
May 6, 2026
50895bf
chore: ignore max-smoke binary in repo root
May 6, 2026
4fc46df
docs(channels/max): add Max Messenger section and contributor README
May 6, 2026
5e7cbec
fix(channels/max): review feedback fixes (Day 5b)
May 6, 2026
9a22631
fix(http): allow 'max' in channel_instances POST validator
May 6, 2026
e78d2aa
fix(channels/max): enforce DM/group policy before dispatch (Day 5e)
May 6, 2026
d1cef06
chore(channels/max): apply gofmt
May 6, 2026
a9b15d4
docs(readme): add Max Messenger to channel list
May 6, 2026
fbf63f2
docs(claude): note max/ subpackage in channel manager
May 6, 2026
788b334
docs(channels): add Max to extended interface tables
May 6, 2026
160743b
docs(changelog): announce Max Messenger channel
May 6, 2026
a41ad39
fix(channels/max): use English-default placeholder text
May 6, 2026
396a5a4
feat(ui): add Max Messenger channel to admin UI
May 5, 2026
7a1ce9f
fix(channels/max): prevent silent polling death from HTTP timeouts
May 14, 2026
d45de7d
fix(channels/max): make long-polling resilient to half-broken HTTP/2 …
May 15, 2026
175b4b1
fix(channels/max): deliver attached files to the agent without a caption
May 28, 2026
c1656b6
fix(channels/max): classify files by MIME and publish directly to bus
May 29, 2026
88e49f1
fix(channels/max): detect MIME from downloaded path, not Payload.File…
May 29, 2026
d6ec599
Merge origin/dev into feat/max-channel
May 29, 2026
4551231
fix(channels/max): coalesce multi-attachment inbounds via per-channel…
May 29, 2026
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ compose.d/*
*.test
goclaw-patched-linux-amd64
ui/web/nginx.staging.conf
cmd/max-smoke/
/max-smoke
/cmd/max-smoke/max-smoke


# Local scratchpad / one-shot artifacts (never commit)
**/debug-*.log
Expand Down
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,25 @@ All notable changes to GoClaw are documented here. For full documentation, see [
endpoint handle duplicates platform-side. No GoClaw state required.
- No DB migration.

- **Max Messenger channel.** Adds Max ([https://max.ru](https://max.ru)) — a
Russian messaging platform — as a first-class channel under
`internal/channels/max/`. Mirrors existing channel patterns (Telegram,
WhatsApp, etc.):
- **Two transports**: long polling (default) and webhook (HMAC-SHA256
signed). Polling is the recommended primary mode.
- **Streaming preview** with throttled in-place edits; final reply
re-emitted with markdown.
- **Reactions** mapping goclaw status to Max `typing_on` action with
per-chat refresh.
- **Media** inbound (HTTP fetch with retry) and outbound (two-step
upload).
- **Access control** via existing `dm_policy` / `group_policy` /
`allow_from` mechanisms; pairing replies sent through Max.
- Custom `net/http` client — no third-party Max SDK dependency.
- Tested locally end-to-end against the live `platform-api.max.ru`
API; webhook mode unit-tested but not yet live-validated.
- New channel type `max`. No DB migration.

### Improvements

- **Context pruning cleanup.** Removed redundant Pass 0 (per-result 30% guard),
Expand Down
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ internal/
├── bootstrap/ System prompt files (SOUL.md, IDENTITY.md) + seeding + per-user seed
├── bus/ Event bus system
├── cache/ Caching layer
├── channels/ Channel manager: Telegram, Feishu/Lark, Zalo, Discord, WhatsApp
├── channels/ Channel manager: Telegram, Feishu/Lark, Zalo, Discord, WhatsApp, Max
│ ├── max/ Max Messenger (Russian platform) — long-poll + webhook
│ └── whatsapp/ Native WhatsApp via whatsmeow (v3)
├── config/ Config loading (JSON5) + env var overlay
├── consolidation/ Memory consolidation workers (episodic, semantic, dreaming) (v3)
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ Single binary. Production-tested. Agents that orchestrate for you.
- **Self-Evolution** — Metrics → suggestions → auto-adapt with guardrails. Agents refine their own communication style
- **Multi-Tenant PostgreSQL** — Per-user workspaces, per-user context files, encrypted API keys (AES-256-GCM), RBAC, isolated sessions
- **20+ LLM Providers** — Anthropic (native HTTP+SSE with prompt caching), OpenAI, OpenRouter, Groq, DeepSeek, Gemini, Mistral, xAI, MiniMax, DashScope, Claude CLI, Codex, ACP, and any OpenAI-compatible endpoint
- **7 Messaging Channels** — Telegram, Discord, Slack, Zalo OA, Zalo Personal, Feishu/Lark, WhatsApp
- **8 Messaging Channels** — Telegram, Discord, Slack, Zalo OA, Zalo Personal, Feishu/Lark, WhatsApp, Max
- **Production Security** — 5-layer permission system, rate limiting, prompt injection detection, SSRF protection, AES-256-GCM encryption
- **Single Binary** — ~25 MB static Go binary, no Node.js runtime, <1s startup, runs on a $5 VPS
- **Observability** — Built-in LLM call tracing with spans and prompt cache metrics, optional OpenTelemetry OTLP export
Expand Down Expand Up @@ -103,7 +103,7 @@ irm https://raw.githubusercontent.com/nextlevelbuilder/goclaw/main/scripts/insta
| Teams | Max 1 (5 members) | Unlimited |
| Database | SQLite (local) | PostgreSQL |
| Memory | FTS5 text search | pgvector semantic |
| Channels | — | Telegram, Discord, Slack, Zalo, Feishu, WhatsApp |
| Channels | — | Telegram, Discord, Slack, Zalo, Feishu, WhatsApp, Max |
| Knowledge Graph | — | Full |
| RBAC / Multi-tenant | — | Full |
| Auto-update | GitHub Releases | Docker / binary |
Expand Down Expand Up @@ -326,7 +326,7 @@ Full documentation at **[docs.goclaw.sh](https://docs.goclaw.sh)** — or browse
| [Core Concepts](https://docs.goclaw.sh/#how-goclaw-works) | Agent Loop, Sessions, Tools, Memory, Multi-Tenancy |
| [Agents](https://docs.goclaw.sh/#creating-agents) | Creating Agents, Context Files, Personality, Sharing & Access |
| [Providers](https://docs.goclaw.sh/#providers-overview) | Anthropic, OpenAI, OpenRouter, Gemini, DeepSeek, +15 more |
| [Channels](https://docs.goclaw.sh/#channels-overview) | Telegram, Discord, Slack, Feishu, Zalo, WhatsApp, WebSocket |
| [Channels](https://docs.goclaw.sh/#channels-overview) | Telegram, Discord, Slack, Feishu, Zalo, WhatsApp, Max, WebSocket |
| [Agent Teams](https://docs.goclaw.sh/#teams-what-are-teams) | Teams, Task Board, Messaging, Delegation & Handoff |
| [Advanced](https://docs.goclaw.sh/#custom-tools) | Custom Tools, MCP, Skills, Cron, Sandbox, Hooks, RBAC |
| [Deployment](https://docs.goclaw.sh/#deploy-docker-compose) | Docker Compose, Database, Security, Observability, Tailscale |
Expand Down
2 changes: 2 additions & 0 deletions cmd/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/nextlevelbuilder/goclaw/internal/channels/discord"
"github.com/nextlevelbuilder/goclaw/internal/channels/facebook"
"github.com/nextlevelbuilder/goclaw/internal/channels/feishu"
"github.com/nextlevelbuilder/goclaw/internal/channels/max"
"github.com/nextlevelbuilder/goclaw/internal/channels/pancake"
slackchannel "github.com/nextlevelbuilder/goclaw/internal/channels/slack"
"github.com/nextlevelbuilder/goclaw/internal/channels/telegram"
Expand Down Expand Up @@ -549,6 +550,7 @@ func runGateway() {
// identically to before — the MCPStore arg is nil-safe inside the
// factory.
instanceLoader.RegisterFactory(channels.TypeBitrix24, bitrix24.FactoryWithPortalStoreAndMCP(pgStores.BitrixPortals, pgStores.MCP, bitrixEncKey))
instanceLoader.RegisterFactory(channels.TypeMax, max.FactoryWithPendingStoreAndAudio(pgStores.PendingMessages, audioMgr))
if err := instanceLoader.LoadAll(context.Background()); err != nil {
slog.Error("failed to load channel instances from DB", "error", err)
}
Expand Down
116 changes: 108 additions & 8 deletions docs/05-channels-messaging.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,10 @@ Every channel must implement the base interface:

| Interface | Purpose | Implemented By |
|-----------|---------|----------------|
| `StreamingChannel` | Real-time streaming updates | Telegram, Slack |
| `WebhookChannel` | Webhook HTTP handler mounting | Facebook, Feishu/Lark, Pancake |
| `ReactionChannel` | Status reactions on messages | Telegram, Slack, Feishu |
| `BlockReplyChannel` | Override gateway block_reply setting | Discord, Feishu/Lark, Pancake, Slack, Zalo OA, Zalo Personal |
| `StreamingChannel` | Real-time streaming updates | Telegram, Slack, Max |
| `WebhookChannel` | Webhook HTTP handler mounting | Facebook, Feishu/Lark, Pancake, Max |
| `ReactionChannel` | Status reactions on messages | Telegram, Slack, Feishu, Max |
| `BlockReplyChannel` | Override gateway block_reply setting | Discord, Feishu/Lark, Pancake, Slack, Zalo OA, Zalo Personal, Max |

`BaseChannel` provides a shared implementation that all channels embed: allowlist matching, `HandleMessage()`, `CheckPolicy()`, and user ID extraction.

Expand Down Expand Up @@ -540,6 +540,16 @@ LLM markdown → htmlTagsToMarkdown() → extractSlackTokens() → escapeHTMLEnt

Key conversions: `**bold**` → `*bold*`, `~~strike~~` → `~strike~`, `[text](url)` → `<url|text>`, `# Header` → `*Header*`, tables → code blocks.

### Webhook security

Max API does not provide a shared-secret signature scheme for webhook authenticity. The webhook URL is the only auth — operators MUST treat it as a secret credential. Configure:

- **Hard-to-guess URL.** Embed a UUID or random hex token in the path; rotate on suspected leak.
- **TLS.** Webhook URL MUST be HTTPS (channel enforces this at config-validation time).
- **`dm_policy: "allowlist"` in production.** Even if an attacker forges a webhook update with a spoofed sender, the channel rejects unauthorized senders before invoking the agent.

Recommended: restrict ingress to Max API origin IPs (if published), apply rate limiting at the gateway, monitor for unexpected `update_type` values as a probe-detection signal.

### Environment Variables

```
Expand Down Expand Up @@ -613,7 +623,97 @@ Zalo Personal uses an unofficial, reverse-engineered protocol. The account used

---

## 12. Channel-Isolated Workspaces
## 12. Max Messenger

The Max channel integrates with [Max](https://max.ru) — a Russian messaging platform whose Bot API mirrors many Telegram conventions. Implementation uses a custom HTTP client (no upstream SDK was suitable when this channel landed); production state validated end-to-end against the live `platform-api.max.ru` API.

### Key Behaviors

- **Two transports**: long polling (`mode: polling`, default) and webhook (`mode: webhook`). Polling spawns a goroutine that GETs `/updates` with a sequence marker; webhook is mounted on the main gateway mux at the path derived from `webhook_url`.
- **Authentication**: `Authorization: <token>` header, *no* `Bearer` prefix. Bot tokens are issued via `@MasterBot` in the Max app or [business.max.ru/self](https://business.max.ru/self).
- **Rate limit**: 30 RPS per bot (Max-side). All polling, sending, editing, and reactions share this budget.
- **Message limit**: 4,000-character limit with automatic chunking at paragraph → line → sentence → word → codepoint boundary.
- **Markdown forwarding**: Outbound messages send `format: "markdown"`; Max API parses common syntax (`**bold**`, `_italic_`, `` `code` ``) and converts to native markup. No client-side conversion needed.
- **Streaming**: Eager placeholder ("💭 Печатаю..."), 800ms throttle, plain-text edits during stream, markdown-formatted final edit via `Send()`/`FinalizeStream` handoff. Answer-only mode (`ReasoningStreamEnabled = false`).
- **Reactions**: Maps goclaw status (`thinking`, `tool_exec`, `compacting`, `stall`) to Max `typing_on` action. Per-chat refresher goroutine re-sends every 4s (Max typing indicator expires ~5s); terminal statuses (`done`, `error`) stop the refresher.
- **Media (inbound)**: HTTP fetch with retry (3 attempts), 25 MiB cap, Content-Length pre-check. Files saved to `os.TempDir` with prefix `goclaw_max_<type>_*<ext>`. Failed downloads logged and skipped — text content still flows.
- **Media (outbound)**: Two-step upload: `POST /uploads?type=...` → temporary URL → multipart POST → token; token attached to `POST /messages`. Failed uploads dropped from the attachment list; text chunks still ship.
- **request_contact verification**: HMAC-SHA256(`bot_token`, `vcf_info`) validated via `hmac.Equal` constant-time comparison. Distinct error types for malformed hex vs. tampered payload.
- **Group support**: Translator implemented with mention gating (`require_mention` default true), but Max platform does not yet permit adding bots to groups — group code is unvalidated live and will be exercised once the platform allows.
- **Self-loop guard**: Messages where `sender.user_id == bot.user_id` are dropped (defends against accidental echo of webhooks).
- **Lifecycle**: `Start` probes `/me`, spawns polling, marks healthy. `Stop` cancels poll context, drains reaction refreshers, waits up to 10s for in-flight handlers, then marks stopped.

### API Findings (corrections to docs)

Three discrepancies between Max's published docs and live API surface, all confirmed by PoC against the production endpoint:

| Field | Doc says | Live API | Effect |
|-------|----------|----------|--------|
| Inner message body | `"body"` | `"message"` | JSON tag mismatch causes empty payloads on unmarshal |
| Recipient discriminator | inferred from `user_id` | use `chat_type` ("dialog"/"chat") | Both `user_id` and `chat_id` populated for DMs — heuristic is wrong |
| DM `chat_id` semantics | not specified | dialog thread ID (stable per conversation) | Use as canonical chat key; do not substitute sender ID |

These are corrected in `types.go` and `inbound.go`.

### Configuration

```jsonc
// channel_instances.config (JSONB)
{
"mode": "polling", // "polling" | "webhook"
"webhook_url": "https://...", // required for webhook mode
"polling_timeout": 30, // seconds, range 0-90
"dm_policy": "open", // "open" | "allowlist" | "pairing" | "disabled"
"group_policy": "open", // "open" | "allowlist" | "disabled"
"require_mention": true, // group mention gate
"allow_from": ["12345"], // user IDs for allowlist mode
"history_limit": 50, // pending group messages buffer
"block_reply": null, // override gateway-level block_reply
"dm_stream": true, // streaming preview in DMs (default ON)
"group_stream": false // streaming preview in groups (default OFF)
}
```

```jsonc
// channel_instances.credentials (encrypted JSONB)
{
"bot_token": "<from @MasterBot>",
"bot_id": 256747471, // optional, fetched on first start
"username": "id..._bot" // optional, fetched on first start
}
```

### Streaming Lifecycle

```mermaid
flowchart LR
A[CreateStream] -->|"POST /messages '💭 Печатаю...'"| P((placeholder mid))
P --> U[Update text]
U -->|throttle 800ms| E["PUT /messages (plain text)"]
E --> U
U --> S[Stop]
S -->|"final flush if pending"| F["PUT /messages (plain text)"]
F --> FS[FinalizeStream]
FS -->|"placeholders.Store(chatID, mid)"| SEND[Send]
SEND -->|"consumePlaceholder + edit"| FINAL["PUT /messages (markdown)"]
```

The placeholder is created eagerly so users see "💭 Печатаю..." within ~150ms of sending their message. Plain text during streaming avoids partial-token rendering glitches (e.g. unclosed `**bold`). The final `Send` carries the agent's complete formatted response and applies markdown.

### Limits & Behaviour Notes

- **Streaming text cap**: 4,000 chars (Max per-message limit). Longer streaming previews are truncated; the final `Send()` chunks correctly.
- **Edit failures during streaming** are logged at `debug` and not propagated — the next `Update` retries with fresher text. Stream is best-effort UX.
- **Concurrent runs in one chat**: each run gets its own stream/placeholder via per-`RunContext` storage. Two parallel "💭 Печатаю..." messages may appear briefly; this is correct.
- **Webhook authentication**: Max does *not* send the bot token with webhook updates. URL secrecy is the only auth — operators must include a hard-to-guess path component (UUID recommended).

### Environment Variables

The Max channel is database-instance-only (no top-level config block). Operators provision via `channel_instances` rows. There are no `GOCLAW_MAX_*` environment variables.

---

## 13. Channel-Isolated Workspaces

Each channel instance can target a specific agent, providing workspace isolation across channels.

Expand All @@ -632,7 +732,7 @@ Channel instances are loaded from the database with their assigned agent ID. The

---

## 13. Local Key Propagation
## 14. Local Key Propagation

Thread/topic context is preserved through the entire message pipeline using a `local_key` in message metadata. This ensures subagent, delegation, and team message results land in the correct thread — not the root chat.

Expand All @@ -648,7 +748,7 @@ All channel state — placeholders, streams, reactions, typing controllers, thre

---

## 14. Per-User Isolation
## 15. Per-User Isolation

Channels provide per-user isolation through compound sender IDs and context propagation:

Expand All @@ -659,7 +759,7 @@ Channels provide per-user isolation through compound sender IDs and context prop

---

## 15. Pairing System
## 16. Pairing System

The pairing system provides a DM authentication flow for channels using the `pairing` DM policy.

Expand Down
1 change: 1 addition & 0 deletions internal/channels/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ const (
TypeDiscord = "discord"
TypeFacebook = "facebook"
TypeFeishu = "feishu"
TypeMax = "max"
TypePancake = "pancake"
TypeSlack = "slack"
TypeTelegram = "telegram"
Expand Down
Loading