16 Commits
Author SHA1 Message Date
Bruno ClermontandBruno Clermont 72fcf52f5d fix(channels): propagate tenant context through all channel handlers (#1278)
Pairing requests and message handlers were silently using MasterTenantID
instead of the correct tenant because context.Context had no tenant set.

Root cause: instance_loader.go passed bare ctx (no tenant) to
startChannelWithTimeout; channel handlers also missing store.WithTenantID
at entry points.

Channels fixed:
- instance_loader.go: pass instCtx (with tenant) to startChannelWithTimeout
- Telegram, Bitrix24, Feishu: add store.WithTenantID at handler entry
- Facebook: add ctx param to handleMessagingEvent/handleCommentEvent + webhook_handler
- Pancake: add ctx param to handleMessagingEvent/handleCommentEvent + webhook_handler

Also fix consolidation pruner: PruneExpired SQL had tenant_id filter that
caused zero rows to ever be deleted (context had no tenant). Remove tenant
filter — pruning is a global maintenance operation.

Add regression tests:
- internal/channels/pancake/tenant_context_test.go (6 tests)
- internal/channels/facebook/tenant_context_test.go (4 tests)
- internal/consolidation/prune_expired_test.go (5 tests)

Co-authored-by: Bruno Clermont <bruno.clermont@gmail.com>
2026-06-25 07:38:51 +07:00
Duy /zuey/ f0f39ce31f feat(channels): add human-like chat behavior
Squash merge PR #99 after resolving conflicts with current dev. PR CI run 26703381807 passed release-versioning, go, and web.
2026-05-31 11:44:56 +07:00
Duy /zuey/ 532ff91d8e fix(security): harden upstream critical surfaces (#32)
* fix(security): harden upstream critical surfaces

Refs #30

* fix(security): close pre-landing review gaps

Refs #30

* fix(security): close official release blockers
2026-05-20 16:33:49 +07:00
Plateau Nguyenandviettranx 680e54b673 feat(pancake): add TikTok sub-platform support (#990)
Add TikTok account type dropdown (Livestream AIO, Business Messaging,
TikTok Shop) to Pancake channel config with prefix-aware webhook routing.

- Add tt, ttm, tts prefixes to platformPrefixes map for convID parsing
- Add TikTokType field to pancakeInstanceConfig (optional, backward compat)
- Add tiktok_type field to UI with showWhen platform=tiktok
- Add i18n labels for en/vi/zh locales

Co-authored-by: viettranx <viettranx@gmail.com>
2026-04-24 09:11:59 +07:00
Plateau Nguyenandviettranx 8dc101e425 feat(pancake): add Shopee platform support (#975)
* feat(pancake): add Shopee platform support

- Fix Accept header for JSON response negotiation (Shopee GETs)
- Platform-prefix-aware pageID parsing (spo_ convID format)
- 500-char message limit for Shopee DMs
- Rune-safe truncation with slog.Warn on truncation

Phase 3 (integration smoke) pending: requires real credentials.

* fix(pancake): address PR review feedback

- Add RegisterPlatformPrefix() for extensibility (issue #1)
- Rename setCommonHeaders → setAcceptJSONHeader (issue #2)
- Add testdata fixture with page_id for priority testing (issue #3)

* chore: retrigger CI

* fix(permissions): classify sessions.compact in RBAC policy

Add MethodSessionsCompact to isWriteMethod to fix drift coverage test.
Method was added in #958 but missing from permissions classification.

* fix(tests): use strings.Contains to avoid redeclaration

Remove duplicate contains helper in tts_gemini_live_test.go that
conflicts with mcp_grant_revoke_test.go in same package.

* refactor(pancake): guard platformPrefixes with RWMutex for concurrent safety

RegisterPlatformPrefix is exported for future use (Lazada, Tokopedia) but
webhook handler reads the map concurrently. Protect with sync.RWMutex so
callers can register prefixes at any time without data races.

- Add platformPrefixesMu sync.RWMutex
- Wrap RegisterPlatformPrefix with write lock
- Extract isKnownPlatformPrefix helper with read lock
- Document that RegisterPlatformPrefix is currently unused (extension point)

---------

Co-authored-by: viettranx <viettranx@gmail.com>
2026-04-24 08:37:40 +07:00
Plateau Nguyenandviettranx 5cca9d7ac6 refactor(pancake): simplify private-reply to stateless DM (#951)
* feat(pancake): private-reply funnel — 2 modes, scope filter, DB dedup, metrics

Refactor the pancake channel's one-time DM feature from the 5-day-old minimal
`first_inbox` toggle into a complete comment → DM funnel. Breaking rename
(first_inbox → private_reply) matches Pancake + Facebook Graph terminology.

Channel logic
- Two modes: `after_reply` (default — public reply then DM) and `standalone`
  (DM-only; bypasses keyword filter, publishes synthetic outbound via
  `ch.Bus().PublishOutbound` so the LLM pipeline is skipped for template DMs).
- Post-level scope filter (`private_reply_options.allow_post_ids` /
  `deny_post_ids`, deny beats allow).
- Template vars `{{commenter_name}}` / `{{post_title}}` with pre-sanitized
  literal-replace (strips `{{`/`}}` from values — no nested substitution).
- Locale-aware default text via `i18n.T(locale, MsgPancakePrivateReplyDefault)`
  with en/vi/zh catalogs and English fallback.

DB dedup (replaces in-memory sync.Map)
- New `store.PancakePrivateReplyStore` (PG + SQLite impls), tenant-scoped via
  `store.TenantIDFromContext`, fail-closed on missing tenant.
- Atomic `TryClaim` / `Unclaim` (INSERT ... ON CONFLICT DO UPDATE WHERE stale)
  eliminates the concurrent-comment TOCTOU that would have fired duplicate
  DMs — claim first, release on API failure so the next comment can retry.
- Configurable TTL (`private_reply_ttl_days`, default 7).
- Dual-DB: migration 000056 on PG; schema v25 on SQLite.
  Bumps RequiredSchemaVersion 55 → 56.

Wiring
- `pancake.FactoryWithStores(ppReplyStore)` follows the existing
  `FactoryWithStores*` pattern (telegram, discord, whatsapp) and replaces
  `pancake.Factory` in `cmd/gateway.go`.
- `Channel.Send` injects `store.WithTenantID(ctx, ch.TenantID())` before
  dispatch so the outbound path sees a non-nil tenant (regression guard:
  without this the new store fails-closed and drops DMs in production — unit
  fakes masked this by bypassing the tenant check).
- Routing metadata whitelist extended: `private_reply_mode`,
  `private_reply_only`, `post_id`, `display_name` survive inbound → outbound.

Observability
- New `internal/metrics` package with `pancake_private_reply_total{
  page_id, result, reason}` counter and `/metrics` endpoint mounted on the
  gateway.
- Endpoint gated behind Bearer auth when `gateway.token` is configured; open
  (with warning) when unset for local dev scraping. Operators should set a
  token or bind `GOCLAW_HOST=127.0.0.1` to avoid leaking page identifiers.

UI
- 6 additive Pancake fields in `channel-schemas.ts` with `showWhen` gating on
  `features.private_reply` (itself gated on platform = fb/ig).
- 10 schema tests; full web build passes.
- i18n entries in en/vi/zh `channels.json` + 2 new Go keys
  (`MsgPancakePrivateReplyWindowExpired`, `MsgPancakePrivateReplyDefault`).

Tests
- 18 unit tests in pancake package (render/filter/mode switch/scope/dedup/
  metrics/tenant-ctx regression); 6 store-level tests; 5 E2E tests against
  live PG covering happy path, dedup-across-restart, TTL expiry via SQL,
  scope filter, FB 7-day policy error → no claim retained.
- All green on both PG and SQLite build tags with race detector.

* refactor(pancake): simplify private-reply to stateless DM

Roll back the dedup funnel from PR 951 — no table, no in-memory state,
no metrics, no modes, no scope filter. Keep only feature flag + message
template.

Rationale: each platform owning its own dedup table is an anti-pattern
that inflates multi-tenant data footprint. Use the platform itself
(Facebook Graph /comment/private_replies is per-comment idempotent)
combined with the existing webhook comment_id dedup in
comment_handler.go. Zero GoClaw state required.

Removed:
- DB table pancake_private_reply_sent (PG migration 000056, SQLite v25)
- Store interface + PG + SQLite implementations + tests
- internal/metrics package + /metrics endpoint + prometheus dep
- Two modes (after_reply/standalone) + standalone fast-path
- Scope filter (allow/deny post IDs) + PrivateReplyOptions struct
- TTL config (private_reply_ttl_days)
- Locale-aware i18n default (MsgPancakePrivateReplyDefault,
  MsgPancakePrivateReplyWindowExpired)
- FactoryWithStores wiring + tenant ctx injection in Send()
- SetHTTPClientForTest dead code

Kept:
- features.private_reply flag
- private_reply_message template with {{commenter_name}} and
  {{post_title}} vars (literal-replace, injection-safe)
- After-reply flow: comment -> public reply -> DM
- Hard-coded English fallback when message is empty

Schema: RequiredSchemaVersion 56 -> 55; SQLite SchemaVersion 25 -> 24;
schema.sql DDL block removed.

Net delta: -2661 lines across 44 files. Build (PG + sqliteonly) +
vet + race tests clean.

* test(agent): bump none-mode prompt size budget to 3100

vault_read wiring (#948) added ~95 chars to read_file tool summary,
pushing none-mode prompt from <3000 to 3075 chars. Bump budget to
3100 (~775 tokens) to match the intentional addition.

* test(integration): ensure data_migrations table exists in reset helper

The reset helper runs before RunPendingHooks, but RunPendingHooks is
what normally creates data_migrations. On a fresh CI database the
DELETE fails with 'relation does not exist'. Create the table
defensively so reset works regardless of execution order.

* chore(sqlite): remove accidental trailing blank line in schema.sql

---------

Co-authored-by: viettranx <viettranx@gmail.com>
2026-04-24 07:41:38 +07:00
viettranx 9f68f3ee0d feat(pancake): auto-react allow/deny scope filter + UI toggle
Extends #919 with per-channel scope control for Facebook auto-react.

- AutoReactOptions pointer struct (allow/deny post & user IDs)
- filterAutoReact + containsString helpers; deny overrides allow
- UI surfaces features.auto_react toggle + 4 tags fields
- Nil options = react-all (backward compatible)
2026-04-18 17:55:11 +07:00
viettranx ee328d4266 refactor: consolidate hooks table migration and drop deprecated agent_id column
- Consolidated PG migration 054: agent_hooks rename + junction table + deprecated column drop
- Updated SQLite schema v19 with final consolidated migration
- Removed obsolete schema rebuild files (v21, v23)
- Updated Go store layer (pg/hooks.go, sqlitestore/hooks.go)
- Updated integration tests to use new table names (hooks, hook_agents)
- Updated TypeScript protocol, UI components, and i18n strings
- Updated gateway methods to reflect schema changes

Tables: agent_hooks → hooks, agent_hook_agents → hook_agents
2026-04-16 14:17:48 +07:00
Plateau Nguyen 8873548102 feat(pancake): add comment auto-react (like) on Facebook (#919)
Opt-in feature (features.auto_react: true) that automatically likes
a user's Facebook comment via Pancake /likes endpoint when the comment
webhook is received, as an engagement signal.

- Add AutoReact bool to Features config struct
- Add ReactComment() to APIClient (multipart POST to /likes, auth via api_key)
- Add reactCommentAsync() to comment handler; fires before keyword filter,
  independent of comment_reply feature
- Bound with 10-slot semaphore + 5s stopCtx timeout per goroutine
- Guard: only fires on platform=facebook with non-empty convID + msgID
- Startup warning when auto_react=true without webhook_secret (F9)
- Update feature gate: exit only when BOTH auto_react AND comment_reply disabled
- Add 8 tests (ReactComment contract, error body, invalid IDs, handler variants)
2026-04-16 10:14:43 +07:00
Viet Tran 60ca45ec97 fix(pancake): demote hot-path webhook log to Debug; guard commentID before echo (#916)
- webhook_handler.go: demote per-event "page_id resolution" log from Info to
  Debug. Firing on every webhook was the exact anti-pattern called out in the
  routing-metadata refactor review.
- pancake.go: move reply_to_comment_id guard above rememberOutboundEcho so a
  missing-metadata error does not stamp phantom echoes that would pollute
  inbound echo dedup.
- docs/journals: drop three Pancake journals (not needed in repo).
2026-04-15 22:30:23 +07:00
Plateau Nguyen 85e6f216b5 feat(channels): Pancake comment reply fix + platform select + UI improvements (#904)
* feat(channels): make Pancake platform a required select with 11 options

- Add mandatory platform select field (11 options) to Pancake channel schema
- Add config required validation on channel instance form (create-only)
- Add i18n fieldOptions/fieldConfig for platform in en/vi/zh locales
- Add TDD tests for Pancake config schema (channel-schemas.test.ts)
- Add backend test TestFactoryExplicitPlatformPreserved
- Add slog.Debug for auto-detect path in pancake.go
- Update Platform field comment in types.go for clarity
- Update plan status to completed; add changelog entry

* feat(channels): hide comment_reply for non-social Pancake platforms

Extend showWhen to accept string | string[] values. Apply it to
features.comment_reply so the field only appears for platforms that
support public posts/comments (facebook, instagram, threads, tiktok,
youtube). E-commerce platforms (shopee, lazada, tokopedia) and
messaging-only platforms (line, google, chat_plugin) no longer show
the irrelevant Comment Reply toggle.

Also guard depValue before String() coercion to avoid the "undefined"
literal matching hazard.

* feat(channels): Pancake comment reply fix + platform select + UI improvements

- Fix comment reply: pass message_id (reply_to_comment_id) to Pancake API
  ReplyComment now requires messageID param; guard added for empty ID
- Add SendMessageRequest.MessageID field (omitempty) for reply_comment action
- Platform select: required field with 11 options, showWhen gate for comment_reply
- Webhook Page ID moved to Advanced collapsible section in channel form
  (auto-expands when existing value is configured)
- routing_metadata.go: centralize routing metadata key constants
- config-flatten.ts: flatten/unflatten nested config for form state

Closes: Pancake comment reply returns 'Missing required field: message_id'
2026-04-15 22:26:13 +07:00
viettranx 5040fe4cbe fix(channels): suppress agent-error text on public-facing channels
Rework of #911 against dev with correctness fixes.

Public-facing channels (Facebook, Telegram, Discord, Feishu, WhatsApp,
Zalo OA, Zalo Personal, Pancake, Slack) no longer receive raw internal
error text when an agent run fails — an empty outbound is published
instead so channels still clean up placeholders / typing indicators.
Errors continue to be logged server-side at Error level.

Applied on both hot paths:
  - cmd/gateway_consumer_normal.go   (agent run failure)
  - cmd/gateway_subagent_announce_queue.go (announce lead run failure)

Fixes vs #911:
  - Whitelist uses channels.Type* constants (compile-time safe) instead
    of string literals. The original PR's literals ("zalo", "line") did
    not match any real channel type, so Zalo OA/Personal and Pancake
    leaked errors while "line" was a dead branch.
  - Added TypePancake and TypeSlack which were missing.
  - Empty Content in Facebook.Send and Pancake.Send now short-circuits
    (matching Telegram/Discord/Slack). Previously, suppressed errors
    flowed to Graph/Pancake APIs as empty payloads, triggering 400s and
    channel health degradation.
  - Added table-driven test for isExternalChannel covering all real
    channel types + empty + unknown + legacy "zalo" short form.

Closes #911
2026-04-15 20:40:25 +07:00
viettranx 3b4004946e chore: apply go fix syntax upgrades (min/max, switch patterns)
Apply Go 1.21+ min/max builtins and improved switch pattern matching
across test files and utilities. No functional changes.
2026-04-15 11:24:57 +07:00
viettranx 45b9240b3a fix(pancake): correct test assertion and remove unused config field
- Fix TestPrivateReply_ReturnsError: use HTTP 400 instead of 200 since
  doRequest only returns error for status >= 400
- Remove unused MaxThreadDepth config field from CommentReplyOptions
2026-04-12 17:13:53 +07:00
Plateau Nguyen 18c09be21b feat(pancake): comment auto-reply + first-inbox DM (#841)
* feat(pancake): add comment auto-reply + first-inbox inbox implementation

- Route COMMENT webhook events with feature gate and self-reply prevention
- Add PostFetcher with sync.Map cache + singleflight for comment context
- Implement ReplyComment() and PrivateReply() API client methods
- Split Send() into sendCommentReply() / sendInboxReply() flows
- Add sendFirstInbox() for one-time inbox DM with configurable greeting
- New config: Features.FirstInbox, CommentReplyOptions, FirstInboxMessage, PostContextCacheTTL
- 72 tests passing with race detection; both PG and SQLite builds clean

* fix(pancake): address PR 841 review issues

- Add slog.Debug when post context fetch fails in buildCommentContent
- Add comment to GetPosts explaining why it bypasses doRequest
- Add 72h TTL eviction for firstInboxSent in runDedupCleaner
- Add 30s timeout to sendCommentReply to bound API hang risk
- Replace custom containsStr helper with strings.Contains in tests
2026-04-12 17:12:57 +07:00
Plateau Nguyen fb8afd41bf feat(channels): add Facebook Messenger and Pancake channel integrations (#731)
Add two new channel implementations for Facebook Fanpage (comment + Messenger
auto-reply, first inbox DM) and Pancake/pages.fm (multi-platform inbox via
Facebook, Zalo, Instagram, TikTok, WhatsApp, LINE).

Key features:
- Facebook: comment auto-reply, Messenger auto-reply, first inbox DM,
  HMAC-SHA256 webhook verification, multi-page webhook routing
- Pancake: multi-platform inbox, outbound echo dedup with HTML normalization,
  race-condition-safe echo fingerprinting, platform-aware formatting
- Bootstrap skip: pre-fill USER.md from channel metadata (Pancake)
- SanitizeDisplayName across all channels (defense in depth)

Code audit fixes:
- Fix truncateForTikTok byte→rune slicing (UTF-8 corruption)
- Fix empty message.ID shared dedup slot (silent message loss)
- Fix DisplayName markdown injection in buildPrefilledUser
- Consolidate duplicate ChannelMeta type (agent→bootstrap)
- Compile-time interface assertions, alphabetical type constants
- Per-message logs demoted to slog.Debug, errors.As for wrapped errors
- UI: alphabetical channel ordering, complete config schemas
- Remove deprecated WhatsApp bridge_url, fix nested error parsing
2026-04-09 23:15:08 +07:00