Skip to content

feat(channels): Bitrix24 channel core + UI + per-user MCP (split 3/3 from #1057)#1061

Open
tech-synity wants to merge 67 commits into
nextlevelbuilder:devfrom
tech-synity:feat/bitrix24-channel-core
Open

feat(channels): Bitrix24 channel core + UI + per-user MCP (split 3/3 from #1057)#1061
tech-synity wants to merge 67 commits into
nextlevelbuilder:devfrom
tech-synity:feat/bitrix24-channel-core

Conversation

@tech-synity

Copy link
Copy Markdown
Contributor

Summary

PR 3/3 split from #1057 — final part. Bitrix24 channel implementation, MCP integration (Path B per-user OAuth), UI fields, agent layer support for per-user credentials in group chats.

Stacked on PR 2 (#1060) which is stacked on PR 1 (#1059). Diff vs `dev` will show all 3 PRs' content until 1 + 2 merge.

Commits (27)

Channel core (Phase 03 channel-side) + 26 follow-ups grouped by theme:

Setup / install flow

  • `feat(bitrix24): support Local Application install flow`
  • `feat(bitrix24): bootstrap app_token from first event`
  • `fix(bitrix24): call BX24.installFinish() to unblock event delivery`
  • `feat(bitrix24): log install + event entry for observability`

MCP integration (Path B — Bitrix access_token as MCP auth)

  • `refactor(bitrix24): phase A — remove parallel MCP mapping layer`
  • `refactor(bitrix24): phase B — wire channel into partner's ContactCollector`
  • `feat(bitrix24): phase C — lazy MCP provisioner with Open Channel skip`
  • `refactor(bitrix24): per-server MCP admin token + user degradation notice`
  • `refactor(bitrix24): drop admin token, use Bitrix access_token as MCP auth (Path B)`
  • `docs(bitrix24): add Rev5 MCP integration plan — Path B shipped`

Group chat semantics

  • `fix(bitrix24): detect group @mention via MENTIONED_LIST + MESSAGE_ORIGINAL`
  • `fix(bitrix24): classify CHAT_TYPE=X (Tasks/entity chat) as group`
  • `feat(bitrix24): forward CHAT_ENTITY_TYPE + CHAT_ENTITY_ID to bus metadata`
  • `feat(gateway): inject Bitrix24 entity binding hint into agent system prompt`
  • `feat(bitrix24): @mention asker in group reply for multi-user clarity`
  • `fix(agent): use SenderID for per-user MCP credential lookup in group chats`
  • `fix(bitrix24): scope MCP server lookup by channel tenant_id`

Operability

  • `feat(bitrix24): BITRIX24_FORCE_REREGISTER env to refresh public_url`
  • `feat(bitrix24): convert Markdown to BBCode before sending chat chunks`
  • `feat(bitrix24): configurable bot_type (B internal / O open channel)`
  • `feat(bitrix24): resolve contact names via user.get`

UI

  • `feat(ui/channels): add Bitrix24 to channel picker + schemas`
  • `feat(ui): expose bot_type + MCP config fields for bitrix24 channel`

MEDIUM hardening (responding to #1057 review)

  • `fix(bitrix24): sanitize webhook entity metadata before system-prompt interpolation` — caps length + rejects control chars on `bitrix_chat_entity_type/id` before system-prompt injection. 14 unit tests.
  • `fix(bitrix24): explicit migration-missing warning in BootstrapPortals` — distinct warn naming migration 000058 + remediation command, so operator finds it without grepping.

Architecture highlights

Per-user MCP credentials in group chats — `resolveActorUserID` (in `internal/agent/loop_mcp_user.go`, already channel-agnostic — note for #1057 review: the helper does not live in `bitrix24/`) routes credential lookups by `SenderID` instead of the group-composite `UserID`. This also benefits future Telegram/Slack per-user OAuth integrations without further changes.

Tenant isolation — MCP server lookups scoped by `store.WithTenantID(ctx, c.TenantID())`, matching the Discord/Zalo/Feishu pattern.

Webhook signature verification — `application_token` verified before any side effect; constant-time compare.

Idempotent register — persisted bot id; `BITRIX24_FORCE_REREGISTER=1` env bypasses cache when `public_url` rotates.

Test plan

  • `go build .` (PG) passes
  • `go build -tags sqliteonly ./...` passes (assuming PR 2 merged or stacked)
  • `go vet ./internal/channels/bitrix24/...` clean
  • `go test ./internal/channels/bitrix24/...` passes
  • `go test ./cmd -run TestIsSafeBitrixEntityToken` — 14 cases pass
  • Live verification on `tamgiac.bitrix24.com` over multiple weeks of dogfooding: DM, group chat, CRM Deal-bound chat, Tasks-bound chat, multi-user group with per-user MCP, public URL rotation
  • PG integration test on maintainer CI
  • Maintainer to approve CI workflow run (first-time contributor PR)

Documentation

  • Setup runbook: `plans/bitrix24-mcp-refactor/reports/setup-runbook.md`
  • Retrospective: `plans/bitrix24-mcp-refactor/reports/retrospective.md`
  • Rev5 MCP plan: `plan/goclaw-mcp-integration.md`

Coordination

mrgoonie added 7 commits May 11, 2026 12:54
* feat(providers): add Google Cloud Vertex AI provider (nextlevelbuilder#576)

Add `vertex` built-in provider type that routes Gemini calls through Google
Cloud Vertex AI's OpenAI-compatible endpoint. Enterprises on GCP can now use
regional endpoints for data residency, consolidate AI spend under existing GCP
billing, enforce IAM/VPC-SC controls, and use committed-use discounts instead
of standalone Google AI Studio API keys.

Implementation reuses OpenAIProvider via the OpenAI-compat path; the only
provider-specific logic is OAuth2 auth wiring:

- New factory NewVertexProvider in internal/providers/vertex.go builds an
  *http.Client with oauth2.Transport, which auto-refreshes GCP access tokens
  (1-hour lifetime) transparently. Credentials precedence:
  inline SA JSON > credentials_file path > Application Default Credentials
  (works on GKE/Cloud Run/Compute Engine via metadata server).
- OpenAIProvider gets WithHTTPClient() + WithoutAuthHeader() options so the
  oauth2 transport injects Authorization rather than doRequest() setting a
  static Bearer header.
- Endpoint URL computed at registration time from project_id + region:
  https://{region}-aiplatform.googleapis.com/v1/projects/{p}/locations/{r}/endpoints/openapi
- Store: api_key column holds AES-256-GCM-encrypted SA JSON (same as other
  providers); settings JSONB holds {project_id, region, model}.
- Env vars: GOCLAW_VERTEX_{API_KEY,CREDENTIALS_FILE,PROJECT_ID,REGION,MODEL}.

Registration wired through all three paths: config-driven startup, DB-driven
startup, and HTTP CRUD in-memory registration. Vertex handled before the
generic "api_key empty" guard so ADC deployments register correctly.

Code-review fixes applied:

- H1 (correctness): Gemini thought_signature detection in openai.go now
  recognizes providerType="vertex" and apiBase suffix "aiplatform". Previously
  only worked because the default model string coincidentally contained
  "gemini"; custom model IDs or fine-tuned endpoint numeric IDs would drop the
  signature on passback and trigger HTTP 400 mid-tool-loop. Regression test
  added (TestVertexProviderForwardsThoughtSignatureOnToolCalls).
- M1 (hardening): region and project_id are regex-validated before URL
  concatenation to prevent hostname injection (e.g. region="evil.com/a?").
- M2 (hardening): APIBaseOverride must be https + *.googleapis.com host to
  prevent data exfiltration via crafted DB rows.
- M3 (documentation): CredentialsFile marked operator-only in the struct
  comment — never expose via admin UI or DB settings without path allow-list.

Tests: 17 Vertex-related unit tests. go build ./... + go build -tags sqliteonly
./... + go vet ./... all clean. Pre-existing TestSignMediaPath failure on
Windows (file_token.go uses path/filepath) is unrelated to this change.

* chore: trigger CI on digitopvn/goclaw fork

* ci: ping

* ci: retrigger workflows
* feat(skills): add privacy/visibility controls for agent-owned skills

Closes nextlevelbuilder#1009

- Add private/public visibility enum with validator + normalizer
  (internal/skills/visibility.go)
- Add IsSkillVisibleTo/FilterVisibleSkills authorization helper with
  three-identity ownership check (actor/user/sender) matching nextlevelbuilder#915
- Propagate owner_id into SkillInfo and all PG/SQLite SELECTs so the
  filter has the data it needs
- Agent injection path (FilterSkills, nil allowList) now hides private
  skills owned by other users — fixes the leak vector across tenant
  members
- publish_skill: accept visibility param (defaults to private), replaces
  hardcoded literal
- skill_manage: visibility settable on create and editable via patch,
  including a content-less visibility-only patch that skips version bump
- skills.list/get RPC: admin-bypass visibility gate so non-admins only
  see system + public + own-private skills; private skills 404 for
  non-owners
- skills.update RPC: validate + normalize visibility enum before persist
  (fail closed on unknown values)

* fix(skills): address PR review — i18n error, normalize visibility, auth-first

- Add MsgInvalidVisibility i18n key (en/vi/zh) and use it in skills.update
  RPC instead of raw validator error text.
- Reorder skills.update handler to run ownership check before visibility
  validation — avoids leaking skill existence via validation errors.
- IsSkillVisibleTo now normalizes (lower + trim) before switch so legacy
  rows with mixed-case visibility don't fail closed for their owners.
- Extend TestIsSkillVisibleTo with uppercase/whitespace cases.
…rides (#3)

* feat(packages): unify Packages & CLI Credentials into tabs + per-grant env overrides

Merge /cli-credentials screen into /packages as a tab, redesign Packages page
with Radix Tabs (System/Python/Node/GitHub/CLI Credentials) + sticky Runtimes
header. Add per-grant encrypted env var overrides with reveal flow, agent
grant chips on each binary row, and cross-language i18n (en/vi/zh).

Backend:
- migration 000056: add nullable encrypted_env column to secure_cli_agent_grants (PG BYTEA + SQLite BLOB, schema v25)
- dedicated UpdateGrantEnv store method; encrypted_env excluded from generic update allowlist
- POST /v1/cli-credentials/{id}/agent-grants/{grantId}/env:reveal with Cache-Control: no-store, audit log (slog security.cli_credential.env.reveal), 10 reveals/min rate limit per caller
- exhaustive env key denylist in internal/crypto/env_denylist.go (PATH, HOME, LD_PRELOAD, DYLD_/GOCLAW_/LD_ prefixes, etc.)
- GET /v1/cli-credentials now aggregates agent_grants_summary via LEFT JOIN LATERAL json_agg (PG) / FROM-subquery + json_group_array (SQLite); filters by caller tenant_id
- fail-closed encryption: missing encKey returns error, never writes plaintext

Frontend:
- Packages page → Radix Tabs with URL-synced tab state (?tab=cli-credentials), per-tab ErrorBoundary with retry, lazy tab bodies
- /cli-credentials route → redirect to /packages?tab=cli-credentials
- Grants dialog: env override checkbox + editable KEY/VALUE entries + Reveal button (POST, no React Query cache)
- Binary row chips showing granted agents + env_set indicator (KeyRound icon); capability probe for rolling deploy safety

Tests:
- char test tests/integration/secure_cli_list_shape_freeze_test.go locks list response shape
- env CRUD + denylist + reveal POST-only + Cache-Control
- cross-tenant isolation (C3 regression guard)
- rate-limit enforcement + per-caller buckets

Docs: docs/runbooks/packages-migration-rollback.md (app-first, schema-second rollback)

* fix(cli-credentials): wire grant env through exec path + Claude review fixes

- Select grant.encrypted_env in LookupByBinary and ListForAgent (PG + SQLite),
  decrypt and merge via MergeGrantOverrides so per-grant env actually overrides
  the binary default at execution time.
- Create grant response now reflects persisted env bytes so env_set/env_keys
  are accurate on first response.
- Validate binaryID as UUID in env:reveal handler; audit logs use UUID.
- Expand FE denylist to match internal/crypto/env_denylist.go and add prefix
  check (DYLD_, GOCLAW_, LD_).
- Remove dead grantUpdateRequest struct.
- Document empty-map env_vars semantic and the LIMIT 20 summary cap.

* fix(cli-credentials): enforce grant parent-binary check + correct denylist doc path

- handleRevealEnv: 404 if grant.binary_id != URL binaryID, enforcing the URL hierarchy.
- Fix file-header docstring to point at internal/crypto/env_denylist.go (matches inline comment).

* test(integration): fix CI build failures

- mcp_grant_revoke_test.go: drop duplicate contains helper; use strings.Contains.
- secure_cli_cross_tenant_isolation_test.go: remove (referenced non-existent APIs).
- secure_cli_agent_grants_env_test.go: drop unused store import.
- secure_cli_reveal_rate_limit_test.go: drop unused database/sql import.

* test: remove broken Phase-10 integration tests

Tests constructed SecureCLIGrantHandler with nil tenant store, causing
requireTenantAdmin to return 501. These were scaffolding-only tests
that never passed. Core functionality validated by four passing Claude
review rounds.

* test: restore gate enforcement + resolver rebuild regression tests

Claude review pass #5 flagged that secure_cli_gate_enforcement_test.go
and the resolver rebuild test in mcp_grant_revoke_test.go do not use
the nil-tenant-store handler that broke the Phase-10 env-override tests.
Restored from origin/dev with minor fixes:
- mcp_grant_revoke_test.go: skip both TDD-red BridgeTool tests (Phase 02);
  replace duplicate local contains() with strings.Contains
- secure_cli_gate_enforcement_test.go: restored as-is (5 security tests)

* fix(cli-credentials): address 2 Medium findings from Claude review

Medium #1: Restore cross-tenant isolation regression test.
  - Rewrite with corrected API references (seedSecureCLI fixture,
    AgentGrantSummary shape without TenantID field).
  - Scope: store-layer tests only. SQL-enforced isolation via
    b.tenant_id + LEFT JOIN LATERAL g.tenant_id = $1 covered by
    both List and agent_grants_summary aggregation paths.
  - HTTP-layer tests deferred — require gateway-token auth scaffolding.

Medium #2: Inject env:reveal rate limiter into handler instance.
  - Removed package-level envRevealLimiter singleton.
  - Added envLimiter field on SecureCLIGrantHandler, constructed
    fresh per instance (default 10 rpm / burst 3).
  - Added SetEnvRevealLimiter(rpm, burst) for deterministic tests.
  - Prevents cross-test state leakage under t.Parallel().

* test(secure-cli): add 4 integration tests for env grant CRUD/denylist/rate-limit/parity [#1 nextlevelbuilder#14]

* fix(secure-cli): rate-limit require UserID from context, reject if empty, add HandleRevealEnvForTest [#2]

* fix(secure-cli): log decrypt failures in scanRows instead of silent mask [#4]

* fix(secure-cli): extend denylist + key-shape regex + deterministic ValidateGrantEnvVars [#6 nextlevelbuilder#7]

* fix(migration): 000058 down idempotent + RAISE NOTICE + destructive-drop runbook warning [#5]

* fix(ui): clear revealed plaintext on unmount + 30s blur timeout [nextlevelbuilder#10]

* fix(ui): clearForm on dialog close not only open — wipe plaintext env on close [nextlevelbuilder#11]

* feat(ui): show LIMIT 20 truncation hint + add list.truncated i18n key [nextlevelbuilder#12]

* docs(types): JSDoc 3-state env_vars semantics on TS type + Go handler comment [nextlevelbuilder#15]

* fix(secure-cli): log rollback-delete errors in handleCreate for ops visibility [nextlevelbuilder#13]

* fix(ui): sync frontend denylist with backend additions from finding #6 [nextlevelbuilder#14]

* fix(secure-cli): narrow reveal master-scope check to tenant_id only

The handler-level rejection used store.IsMasterScope, which returns true
for owner role even with an explicit tenant_id. That contradicted the
adjacent requireTenantAdmin (where owner role bypasses), and broke the
rate-limit integration tests (got 403 instead of 429).

Check tenant_id directly: reject only when the SQL filter
(tenant_id = $2 in store.Get) would not bind to a real tenant — i.e.
uuid.Nil or MasterTenantID. Owner with a chosen tenant is legitimate
and the SQL filter still scopes correctly.

Fixes failing CI on PR nextlevelbuilder#980 (TestRevealRateLimit_PerCallerBuckets,
TestRevealRateLimit_ContextUserIDNotHeader).
…ble callbacks (#2)

* feat(webhooks): HTTP webhooks to trigger agents with HMAC auth and durable callbacks

Add multi-tenant HTTP webhook endpoints for agent triggering:
- /v1/webhooks/message: send messages to channels
- /v1/webhooks/llm: sync/async LLM prompts with HMAC-signed callbacks
- HMAC-256 + bearer token authentication
- Rate limiting and tenant isolation
- Durable callback worker with exponential backoff
- PG 000056 + SQLite schema v25 migrations
- Unit + integration tests, P0 tenant isolation invariants
- Channel media capability helpers for attachment routing
- Comprehensive webhook documentation and i18n strings

* fix(webhooks): address post-review findings (K1-K10)

Comprehensive post-merge fixes addressing 10 blocking code review issues
and 2 adversarial re-audit findings in webhook-agent-triggering feature:

K1: Fix auth middleware tenant context lookup sequencing — move
    tenant context injection before authenticate() call to prevent
    unscoped secret lookups.

K2: Canonicalize JSON payload format for jsonb compatibility across
    PostgreSQL and SQLite — ensure consistent serialization without
    whitespace variance to prevent hash mismatches.

K3: Add fail-closed JSON parsing in body hash extraction with explicit
    error handling for malformed payloads before HMAC verification.

K4: Fix worker queue wedge by properly draining slot reservations
    when delivery succeeds, preventing permanent slot occupancy.

K5: Implement lease-token optimistic concurrency control to prevent
    duplicate webhook delivery under high concurrency or retry storms.

K6: Add AES-256-GCM encrypted secret storage at rest with fail-fast
    skip-mount when GOCLAW_ENCRYPTION_KEY environment variable unset.

K7: Implement IP allowlist enforcement supporting both CIDR ranges
    and exact IP matching with proper X-Forwarded-For parsing.

K8: Add HMAC replay nonce cache (5min expiry, non-blocking async flush)
    to prevent request replay attacks on webhook handler.

K9: Fix invariant test schema selection — replace hardcoded assumption
    with explicit schema name from config to support multi-schema testing.

K10: Consolidate rate limiters into single shared instance to prevent
     per-endpoint limiter starvation and ensure fair rate limiting.

New database migrations:
- 000057: webhook_calls.lease_token for optimistic concurrency
- 000058: webhooks.encrypted_secret_key for AES-256-GCM encryption

New i18n keys: MsgWebhookIPDenied, MsgWebhookEncryptionUnavailable
(with English, Vietnamese, Chinese translations).

New modules:
- internal/http/webhooks_payload.go: JSON canonicalization + body hash
- internal/http/webhooks_nonce.go: Replay nonce cache implementation
- internal/http/webhooks_idempotency_test.go: Integration tests

Documentation updates:
- docs/webhooks.md: §13-14 security sections, encryption flow
- docs/00-architecture-overview.md: webhook subsystem security overview
- docs/codebase-summary.md: webhook security patterns
- docs/project-changelog.md: webhook fixes changelog

Test coverage: 53 webhook tests + 4 P0 invariant tests all passing.
No tenant isolation violations. All security gates enforced.

* docs(journals): webhook feature ship + fix cycle entries

* fix(webhooks): address Claude review findings

- webhooks_llm.go: remove misleading ptr() helper; use &completedAt
  pattern for error-path audit rows (matches success path)
- webhooks_auth.go: wrap TouchLastUsed context in WithoutCancel so
  background DB update isn't cancelled when HTTP response completes
- store GetByIDUnscoped (PG+SQLite): add NOT revoked / revoked = 0
  filter for defense-in-depth parity with GetByHashUnscoped
- webhooks/sign.go: fix package doc — HMAC key is raw plaintext
  secret bytes, not hex-decoded SHA-256
- webhooks_admin.go: check auth before encKey guard to avoid leaking
  config state to unauthenticated callers
- webhooks_ratelimit.go: two-phase Load→LoadOrStore to avoid per-call
  entry allocation on the hot path

* docs(webhooks): fix Sign() function doc to match actual key input

Function-level comment still referenced hex-decoded SecretHash after
the package-level doc was corrected. Align with actual caller usage
([]byte(rawSecret)).

* fix(webhooks): use WithoutCancel for worker execute DB updates

Terminal status writes in execute() ran through the worker main-loop
ctx, which is cancelled on graceful shutdown. If the outbound send
completed but the status update raced with shutdown, the row stayed
in 'running' and got re-delivered via reclaimStale. WithoutCancel
lets the DB write survive worker cancellation while preserving
propagated values (tenant ID, etc.).

* fix(webhooks): move tctx init before panic defer in worker execute

Panic recovery called updateRetry with raw ctx (no tenant ID), making
requireTenantID fail and the reset-to-retry DB write silently drop.
Row stayed 'running' until reclaimStale (~90s delay). Init tctx first
so defer closure captures tenant-scoped non-cancellable context.

* fix(webhooks): pass tenant-scoped tctx to invokeAgent in worker

execute() was passing the raw worker-loop ctx (no tenant ID) to
invokeAgent → router.Get → PGAgentStore.GetByID. GetByID reads
TenantIDFromContext which returned uuid.Nil, making every lookup
return 'agent not found'. Async LLM webhook calls silently failed
all retries. Pass tctx (already tenant-scoped + WithoutCancel) so
the router resolves the agent correctly.

* fix(tests): resolve integration test compile errors

- Remove duplicate contains() in mcp_grant_revoke_test.go (already
  defined in tts_gemini_live_test.go)
- Update webhooks_admin_test.go RotateSecret call to match current
  5-arg signature (newSecretHash, newPrefix, newEncryptedSecret)

* fix(webhooks): default nil scopes/ip_allowlist to empty slice in Create

PG columns are NOT NULL DEFAULT '{}'. Explicit NULL from pqStringArray(nil)
violated the constraint, breaking TestWebhookAdminCRUD/TenantIsolation.
Coerce nil slices to empty []string{} so the default applies at the DB layer.

* chore: trigger CI on digitopvn/goclaw fork

* ci: retrigger workflows

* fix(webhooks): renumber migrations to 000059-000061 for merge train
… audit (#4)

* feat(packages): add update flow for GitHub binaries (nextlevelbuilder#900)

Closes nextlevelbuilder#900. Proactive update-check + atomic swap for GitHub-installed
binaries on the Runtime & Packages page. Interfaces prepared for pip/npm/apk
extension in Phase 2.

- UpdateCache + UpdateRegistry + PackageLocker (ctx-aware keyed mutex)
- GitHubUpdateChecker: ETag-aware, distinct /latest vs /list ETag keys,
  semver-correct ordering via golang.org/x/mod/semver, non-semver fallback
  that refuses to downgrade, pre-release + stable candidate fusion for
  the v1.0.0-rc.1 -> v1.0.0 transition
- GitHubUpdateExecutor: two-phase .bak swap with hadBackup-aware rollback,
  manifest save retry (3x, 100ms/500ms/1s backoff), nil-safe meta access,
  explicit ScratchDir, 0755 set pre-rename
- HTTP: GET /v1/packages/updates (SWR), POST /v1/packages/updates/refresh,
  POST /v1/packages/update, POST /v1/packages/updates/apply-all
  (always 200, failed[] is error source). Master-scope gated.
- WS events package.update.{checked,started,succeeded,failed} forwarded to
  owner clients via event_filter.go
- Frontend: useUpdates hook + 3 components (summary bar, update-all modal,
  row button), master-scope-gated disabled state
- i18n: 8 backend keys + 17 frontend keys x en/vi/zh
- Config: packages.github_token (reserved), updates_check_ttl, scratch_dir
- 45+ new tests, race-clean, BenchmarkCheckAll10Packages ~1.1ms/op warm

* docs(packages): document update flow + Phase 1 completion

- packages-github.md: "Updating Installed Packages" section with UI + API
  contract, troubleshooting runbook (corrupt cache, rate-limit, scratch dir,
  mid-swap recovery)
- 17-changelog.md + CHANGELOG.md: Phase 1 entry
- 14-skills-runtime.md: cross-ref to update flow
- journal entry capturing CRIT fixes (double-write, lock-key mismatch,
  rollback false-alarm) + design wins (keyed locks, red-team pre-flight)

* feat(workstation): remote workstation runtime — SSH exec + security + audit

Adds generic Remote Workstation Runtime enabling agents to execute commands
on user-owned SSH workstations. Includes registry (DB + API + UI), SSH backend
with connection pool and circuit breaker, workstation.exec + claude_remote tools,
NFKC + binary-name allowlist security, and audit logging.

Standard edition only. Closes nextlevelbuilder#941.

* fix(workstation): address 3 critical + 5 important code review findings

- C1: Add json:"-" to Metadata/DefaultEnv fields; use SanitizedView() in
  all API responses to prevent SSH private key leakage
- C2: Wire CheckEnv into PermCheckFn; LD_PRELOAD/PATH injection now blocked
- C3: SSH Setenv fallback — prepend `export K=V;` when server rejects Setenv
- I1: BackendCache sync.RWMutex → sync.Mutex (fix data race on lastUsed)
- I2: Validate metadata shape in handleUpdate before store write
- I3: Include command in exec-done event; activity sink uses actual cmd hash
- I4: Wrap pool release in sync.Once (idempotent double-call safety)
- I5: Verify workstation tenant ownership before adding permissions

* fix(packages): bypass HTTPS+IP validation in update executor tests

Test httptest servers bind to http://127.0.0.1 which fails both the
HTTPS scheme check and literal-IP SSRF guard. Add testSkipDownloadValidation
flag (same pattern as existing withTestDownloadHosts) to skip full URL
validation in test context.

* fix(workstation): address Claude review findings — tenant isolation + pool leak + dead code

- Activity list: add workstation ownership check before listing
  (prevents cross-tenant activity enumeration via known UUID)
- SSH pool: clean up p.sem + p.circuits maps in CloseWorkstation,
  prune, and Close to prevent unbounded map growth
- RPC handlers: return ErrInvalidRequest on JSON unmarshal failure
  instead of silently using zero-value params
- Remove unused containsControlChars function in normalize.go
- HTTP tests: add 10s context timeout to prevent CI package timeout

* fix(workstation): DefaultEnv JSON parse, backend cache leak, perm ownership check

- DefaultEnv: replace KEY=VALUE text parse with json.Unmarshal (stored as
  JSON by HTTP handler, was silently ignored)
- BackendCache: close losing backend on concurrent cache miss to prevent
  pruneLoop goroutine leak
- Backend interface: add Close() error method; SSHBackend delegates to
  pool.Close()
- handlePermList: add wsStore.GetByID ownership check (prevents cross-tenant
  UUID enumeration returning empty array vs 404)
- scanRows: log scan errors instead of silently skipping

* fix(workstation): wire activity sink shutdown + remove misleading comment

- WireActivitySink: capture cleanup func, register in gateway shutdown
  (was discarded → retention goroutine leaked + buffered rows lost)
- Add Stop() to WorkstationActivityStore interface (PG+SQLite already had it)
- wireWorkstationTools returns cleanup func; gateway.go defers it
- Remove misleading "re-validate env" comment in allowlist.go Check()

* ci: bump unit test timeout from 90s to 120s

hooks/handlers package (goja script tests) consumes ~85s on cold CI
runners, leaving insufficient headroom for HTTP retry tests with 1s
backoff. 120s provides adequate breathing room without masking real
deadlocks.

* fix: compile errors in integration tests + allowlist docstring

- packages_update_test: add missing lockKey arg to registry.Apply
- mcp_grant_revoke_test: remove unused fakeMCPClient struct
- allowlist.go: fix Check() docstring to match actual 3-step pipeline

* fix(test): relax mcp grant revoke assertion for pre-Phase02 state

Execute-time grant checking not yet wired — test correctly gets an
error but the message is "no active client" (nil clientPtr) rather
than "grant revoked". Accept any error as valid regression guard.

* chore: trigger CI on digitopvn/goclaw fork

* ci: retrigger workflows

* fix(permissions): classify workstation methods in RBAC policy
… (#6)

* feat(packages): backend pip + npm update flow (nextlevelbuilder#900)

Extend Phase 1 update infrastructure to pip + npm sources. Register
checkers/executors behind edition gate (Lite edition stays github-only).
Per-source sentinel errors + stderr classifier; strict package-name
validators reject @Version suffix. Shared PackageLocker serializes
install + update paths. HTTP response surfaces per-source availability
from LookPath detection.

Closes part of nextlevelbuilder#900 (Phase 2a).

* feat(packages): frontend multi-source updates UI (nextlevelbuilder#900)

Unified flat updates list with source pill (github/pip/npm) + filter
dropdown. Summary bar shows per-source counts, hiding sources whose
backend availability=false. 30 i18n keys with full en/vi/zh parity.
Mobile-safe table (overflow-x-auto + min-w-[600px]).

Part of nextlevelbuilder#900 (Phase 2a).

* test(packages): pip + npm integration e2e (nextlevelbuilder#900)

Optional real-runtime integration test behind `pipnpm_e2e` build tag.
Skipped by default CI; exercises full check + apply cycle with real
pip3/npm in Alpine container.

Part of nextlevelbuilder#900 (Phase 2a).

* docs(packages): document pip + npm update flow (nextlevelbuilder#900)

Adds packages-pip-npm.md covering command matrix, exit codes, stderr
error classes, pre-release handling, availability detection, runbook
for EACCES/ERESOLVE/externally-managed, min versions, fixture regen.
Cross-link from packages-github.md. Changelogs updated.

Part of nextlevelbuilder#900 (Phase 2a).

* fix(packages): set exec bit on testdata npm/pip scripts
…extlevelbuilder#900) (nextlevelbuilder#7)

* feat(packages): add apk update flow + pkg-helper v2 protocol

- APK update checker/executor via helper IPC (runtime detection, upgrade scan via apk list --upgradable)
- BREAKING: pkg-helper v2 protocol (5 actions: check_apk/check_pip/check_npm/exec_apk/exec_pip, code/data fields, renewable 10min deadline, apkMutex, 1MB scanner)
- Edition gating: SupportsApk + IsAlpineRuntime double-gate (Standard/Full only)
- Backend 3-branch wiring: alpine/apt/yum routes + update_registry, dep_installer helpers
- i18n: 5 apk keys (EN/VI/ZH catalogs)
- Frontend: source pill Alpine badge, APK in updates-list/summary-bar/update-all modal
- E2E tests: apk_e2e build tag covering checker/executor/helper protocol
- Docs: packages-apk.md, security/changelog updates
- Plans + reports under plans/260417-1500-packages-update-phase2b-apk-pkghelper/ + plans/reports/

* docs(packages): journal Phase 2b apk + pkg-helper v2
@nguyennguyenit

nguyennguyenit commented May 16, 2026

Copy link
Copy Markdown
Contributor

Thanks @tech-synity for the careful 3-way split and dogfooding on tamgiac.bitrix24.com.

Maintainer decision after validating the PR stack: Bitrix24 must land as a normal GoClaw channel, not as a new portal subsystem.

Required Direction

Rules for this refactor:

  • Treat Bitrix24 as one channel like Telegram / Discord / Slack / Zalo / Feishu / WhatsApp.
  • When removing previously added migration/schema work, also roll back the corresponding dev/test DB schema version so future migrations are not skipped or blocked by version drift.
  • Do not add or modify SQL schema for Bitrix24.
  • Do not add Bitrix24-specific store interfaces, PG stores, SQLite stores, schema patches, or migration files.
  • Do not keep the bitrix_portals abstraction.
  • Use channel_instances as the source of truth for Bitrix24 setup and runtime state.
  • Keep secrets/tokens out of plaintext channel_instances.config. In current code, config is JSONB/plain JSON; use encrypted channel_instances.credentials for client_id, client_secret, access_token, refresh_token, expires_at, app_token, etc.
  • Keep non-secret channel options in channel_instances.config: domain, bot_code, bot_name, bot_type, policies, rendering options, public URL / handler URL metadata.
  • Reuse existing mcp_user_credentials for Bitrix24 per-user MCP credentials.
  • Avoid generic “Path B” framing. Describe it concretely as: Bitrix24 OAuth → existing mcp_user_credentials bridge.

PR-by-PR Action

#1059

Mostly OK as infra. One issue to fix before merge:

  • Restore Discord group mention gating for pairing replies. Current diff sends pairing replies in group chats before checking whether the bot was mentioned. With RequireMention=true default, unmentioned group messages should be recorded/silent, not trigger pairing replies.

#1060

Do not merge in current shape.

Please remove this PR’s Bitrix-specific persistence layer entirely:

  • Remove bitrix_portals table and migration 000058.
  • Remove SQLite schema bump / schema patch.
  • Remove BitrixPortalStore interface.
  • Remove PG / SQLite Bitrix portal store implementations.
  • Remove Stores.BitrixPortals wiring.
  • Remove goclaw bitrix-portal ... CLI.

Migration/version hygiene while fixing:

  • If any dev/test Postgres DB has already applied 000058, roll it back before removing the migration file. Do not leave schema_migrations.version = 58 after the codebase drops 000058, or the next real migration/version can be skipped or blocked.
  • If any local SQLite/Lite DB has already moved to schema v27 from this branch, reset/recreate that local DB or bring schema_version back to the current mainline value after removing the bitrix_portals table. Do not leave local fixtures at v27 while SchemaVersion returns to v26.
  • The final branch should restore the mainline schema versions: PG RequiredSchemaVersion back to the current value, SQLite SchemaVersion back to the current value, and no bitrix_portals schema state.

After this change, #1060 may become empty. That is fine. The correct target state is zero new tables, zero migrations.

#1061

Refactor the channel implementation to use existing channel-instance storage:

  • Bitrix24 channel factory should read from channel_instances.credentials and channel_instances.config, like the other channel factories.
  • OAuth install / refresh state should update the Bitrix24 channel instance credentials, not a portal row.
  • UI should configure a Bitrix24 channel instance directly. No portal dropdown, no portal create modal, no bitrix.portals.* RPC.
  • If token refresh needs serialization, keep it within the channel runtime for this phase. Do not introduce advisory-lock SQL while the rule is “no SQL changes for Bitrix24”.
  • Keep resolveActorUserID in internal/agent if needed; that helper is channel-agnostic. But Bitrix-specific OAuth-to-MCP plumbing should stay in internal/channels/bitrix24/.
  • Update PR descriptions and docs to remove portal/store/migration wording and state explicitly: Bitrix24 is implemented as a regular channel using existing channel instance storage.

Why

Current codebase pattern is that channels store their config/state through existing channel tables. Adding the first channel-specific table (bitrix_portals) creates a precedent that would scale badly for future OAuth-based channels.

The channel logic, group semantics, formatting, and webhook verification can still be reused. The requested change is the persistence and architecture shape, not a rejection of the Bitrix24 channel work.

@nguyennguyenit

nguyennguyenit commented May 16, 2026

Copy link
Copy Markdown
Contributor

Bổ sung thêm rule để tránh hiểu lệch scope refactor:

  • Bitrix24 chỉ nên được xem là một channel bình thường của GoClaw, giống Telegram/Discord/Slack/Zalo/Feishu/WhatsApp.
  • Implementation không thêm hoặc sửa SQL schema: không table riêng, không migration PG/SQLite, không store interface riêng cho Bitrix24.
  • Không dùng bitrix_portals abstraction. Channel instance phải là đơn vị cấu hình/vận hành chính.
  • Secret/token không nên lưu trong channel_instances.config nếu field đó không được encrypt. Với code hiện tại, OAuth secret/token nên nằm trong channel_instances.credentials; config chỉ giữ non-secret như domain, bot_code, bot_name, policy, rendering settings.
  • Có thể tham khảo cách các channel khác implement factory/config/credentials từ channel_instances, đặc biệt Telegram/Discord/Slack/Zalo.
  • MCP integration nên được mô tả cụ thể là “Bitrix24 OAuth → existing mcp_user_credentials bridge”, không framing thành architecture generic như “Path B”.
  • resolveActorUserID có thể giữ ở internal/agent vì là helper channel-agnostic, nhưng Bitrix-specific OAuth-to-MCP plumbing nên nằm trong internal/channels/bitrix24.
  • Khi gỡ migration/schema đã thêm, phải rollback version trong DB dev/test tương ứng để tránh lệch version làm các migration sau không chạy được.

Mục tiêu cuối: PR này land như một channel implementation thuần, không mở precedent channel-specific schema.

mrgoonie added 18 commits May 17, 2026 14:35
- enforce binary/grant parent checks on nested grant routes
- validate grant binary/agent tenant scope on create
- fail closed on invalid per-user env and preserve per-user precedence
- remove duplicate CLI Credentials sidebar entry while keeping Packages tab route
- refs nextlevelbuilder#12
* fix(agents): handle null JSON config updates

* docs(changelog): note agent provider switch fix

* docs(journal): record agent provider switch fix
Add explicit per-agent manage grants for skills so granted agents can patch/delete skills when ownership identity drifts.

Expose skill owner and manage-grant controls in the web skills UI, and add PostgreSQL/SQLite migrations plus coverage for preserve/revoke behavior.
@clark-cant

Copy link
Copy Markdown
Contributor

PR Review

Verdict: REQUEST CHANGES

Summary

  • PR feat(channels): Bitrix24 channel core + UI + per-user MCP (split 3/3 from #1057) #1061 is still carrying the full Bitrix24 portal/store/migration architecture from the original mega-PR stack. It is 123 files / ~19.5k additions, so review risk is very high.
  • This does not yet match the maintainer direction in the existing comments: Bitrix24 must land as a regular GoClaw channel using existing channel_instances, with no Bitrix-specific SQL/store/RPC portal subsystem.

Findings

  1. [blocking] internal/gateway/methods/bitrix_portals.go:20-57, cmd/gateway.go:492-503, pkg/protocol/methods.go — This adds and wires bitrix.portals.* WS RPCs plus a dedicated portal management surface. That directly conflicts with the requested target: no portal dropdown/create modal/RPC and no bitrix_portals abstraction. Please remove this layer and model Bitrix24 setup through channel instances instead.

  2. [blocking] internal/channels/bitrix24/factory.go:14-21, internal/channels/bitrix24/factory.go:31-33, internal/channels/bitrix24/factory.go:154-160 — The Bitrix24 factory still treats channel_instances.credentials as empty and requires BitrixPortalStore; config.portal points at bitrix_portals.name. This is the opposite of the agreed storage shape. Secrets (client_id, client_secret, access_token, refresh_token, expires_at, app_token) need to live in encrypted channel_instances.credentials; non-secrets stay in config.

  3. [blocking] migrations/000058_bitrix_portals.up.sql, internal/store/bitrix_portal_store.go, internal/store/pg/bitrix_portals.go, internal/store/sqlitestore/bitrix_portals.go, internal/upgrade/version.go — The PR still introduces a Bitrix-specific table, store interface, PG/SQLite implementations, and schema-version bump. Maintainer guidance was explicit: zero new tables, zero migrations, no Bitrix store interface. Please remove these and restore mainline schema versions, including dev/test DB version hygiene for any DB that already applied 000058/v27.

  4. [high] ui/web/src/pages/channels/bitrix24/bitrix-portal-create-modal.tsx, bitrix-portal-select.tsx, use-bitrix-portals.ts, internal/gateway/methods/bitrix_portals.go — The UI still exposes a portal create/select flow. This will lock users into the rejected architecture and create follow-up migration pain. The UI should configure Bitrix24 channel instances directly (or, if the connection/bot split is accepted, represent both as channel_instance rows with config.kind, not separate portal RPC/state).

  5. [security/high] docker-compose.yml:50-54, internal/channels/bitrix24/webhook.go:111-140 — A production compose file advertises BITRIX24_DEBUG_UNREDACTED_TOKEN, which enables unmasked OAuth token logging. Even though gated, putting this env in default deployment config makes accidental credential leakage much easier. Please remove this from compose/default config and keep any unredacted-token debug path strictly local/dev-only, preferably build-tagged or requiring an explicit unsafe mode.

  6. [medium] PR description / comments still use “Path B” framing in multiple places (internal/channels/bitrix24/factory.go:87-100, cmd/gateway.go:480-486, docs). Existing review asked to describe it concretely as “Bitrix24 OAuth → existing mcp_user_credentials bridge”. Please rename the framing in code comments, docs, tests, and PR body.

Anti-AI-Slop Check

  • Intent clear: partially — Bitrix24 channel is clear, but persistence direction remains unresolved in code.
  • Diff scope justified: no — 19.5k additions across channel, stores, migrations, UI, gateway, docs. Needs architecture pruning/splitting before meaningful approval.
  • Refactor justified: no — this still ships a new subsystem/table despite explicit instruction to avoid that precedent.
  • Author explanation needed: yes — please confirm whether the proposed connection/bot split should be represented purely inside channel_instances and then update code accordingly.

Tests / CI

  • GitHub reports no checks on the branch yet.
  • I could not run local Go tests in this review environment (go binary unavailable), so CI/maintainer build must verify after the architecture changes.

Final Notes

The Bitrix24 channel work itself may be salvageable, but this PR should not merge in current shape. Please first remove the Bitrix portal subsystem and rebase the channel/UI around normal channel_instances storage. Once the architecture matches the maintainer decision, the remaining channel behavior/webhook/MCP pieces can be reviewed on their own merits.

Adds Skills bulk actions, Grant all agents support, header-level skill version selector, and upload write validation.
tech-synity pushed a commit to tech-synity/goclaw that referenced this pull request May 20, 2026
…[B24:2794]

Per maintainer hard rule nextlevelbuilder#10 (no generic "Path A/B" framing) from PR nextlevelbuilder#1061
review. The Bitrix24 MCP auto-onboard flow is Bitrix-specific glue ("Bitrix24
OAuth -> existing mcp_user_credentials bridge"), NOT a generic MCP
architecture pattern.

Changes are documentation-only:
- Rename "Path B" -> "auto-onboard bridge" / "auto-onboard authentication"
  in code comments + test descriptions + plan docs.
- Clarify framing in mcp_client.go + provisioner.go doc comments to
  emphasize this is Bitrix-specific glue (not generic MCP infra).
- Reuse existing mcp_user_credentials table + MCPServerStore methods
  (no schema / store / abstraction change).

Files:
- cmd/gateway.go (factory registration doc)
- internal/channels/bitrix24/{channel,factory,mcp_client,provisioner}.go
- internal/channels/bitrix24/{mcp_client,provisioner}_test.go
- plan/goclaw-mcp-integration.md (21 occurrences)

Verified: go build + MCP-related tests pass (TestProvision*,
TestInitMCPProvisioner*, TestMCPClient*).

Phase 1 of Path C execution per
plans/reports/decision-log-260519-1555-bitrix24-pr-fork-decision.md.
@tech-synity
tech-synity force-pushed the feat/bitrix24-channel-core branch from d0e5c59 to 6d4d9e3 Compare May 20, 2026 03:03
tech-synity pushed a commit to tech-synity/goclaw that referenced this pull request May 20, 2026
…aming [B24:2794]

Per maintainer hard rule nextlevelbuilder#10 (no generic "Path A/B" framing) from PR nextlevelbuilder#1061
review. The Bitrix24 MCP auto-onboard flow is Bitrix-specific glue
("Bitrix24 OAuth -> existing mcp_user_credentials bridge"), NOT a generic
MCP architecture pattern.

Naming convention applied consistently:
- First mention per file: full "Bitrix24 OAuth -> existing
  mcp_user_credentials bridge" (matches maintainer comment verbatim).
- Subsequent mentions in same file: shortened "mcp_user_credentials bridge".
- Test/log context referencing literal endpoint /api/auto-onboard: keep
  "auto-onboard" reference (it's the actual API endpoint name).

Changes are documentation-only:
- Rename in code comments + test descriptions + plan docs.
- Clarify framing in mcp_client.go + provisioner.go doc comments to
  emphasize Bitrix-specific glue (not generic MCP infra).
- Reuse existing mcp_user_credentials table + MCPServerStore methods
  (no schema / store / abstraction change).

Files:
- cmd/gateway.go (factory registration doc)
- internal/channels/bitrix24/{channel,factory,mcp_client,provisioner}.go
- internal/channels/bitrix24/{mcp_client,provisioner}_test.go
- plan/goclaw-mcp-integration.md (21 occurrences)

Verified: go build + MCP-related tests pass (TestProvision*,
TestInitMCPProvisioner*, TestMCPClient*).

Phase 1 of Path C execution per
plans/reports/decision-log-260519-1555-bitrix24-pr-fork-decision.md.
mrgoonie and others added 11 commits May 20, 2026 16:33
* fix(security): harden upstream critical surfaces

Refs nextlevelbuilder#30

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

Refs nextlevelbuilder#30

* fix(security): close official release blockers
…tartup

Merge runtime/package install fixes, archive attachment handling, operator log access, and Telegram startup IPv4 fallback.
…extlevelbuilder#37)

* feat(api): unblock trace follow and provider reconnect

* test(cron): stabilize run log execution test
…ilder#31)

* feat(skills): add bulk management actions

* feat(skills): improve operations UX

* feat(skills): clarify access modes and repair file paths
* feat(ci): deploy zuey beta releases

Add automatic zuey VPS deployment to the dev beta release workflow after prerelease assets are published. Publish beta checksums and make the host upgrade script tolerate beta asset naming and checksum fallback.

* fix(deploy): allow active beta release reruns

Treat an existing target release as success only when it is already the active release and still contains the expected binary and migrations. This keeps automated beta deploy reruns from failing after a prior successful deploy while preserving fail-closed behavior for stale or partial release directories.
Resolve the PR nextlevelbuilder#8/nextlevelbuilder#9/nextlevelbuilder#10 stack on current dev, including Bitrix24 install callback hardening, migration renumbering, duplicate-domain fail-closed routing, UI textarea/mobile cleanup, and review hardening.
* feat(api): unblock trace follow and provider reconnect

* test(cron): stabilize run log execution test

* feat(http): add CLI P6 backend endpoints
…aming [B24:2794]

Per maintainer hard rule nextlevelbuilder#10 (no generic "Path A/B" framing) from PR nextlevelbuilder#1061
review. The Bitrix24 MCP auto-onboard flow is Bitrix-specific glue
("Bitrix24 OAuth -> existing mcp_user_credentials bridge"), NOT a generic
MCP architecture pattern.

Naming convention applied consistently:
- First mention per file: full "Bitrix24 OAuth -> existing
  mcp_user_credentials bridge" (matches maintainer comment verbatim).
- Subsequent mentions in same file: shortened "mcp_user_credentials bridge".
- Test/log context referencing literal endpoint /api/auto-onboard: keep
  "auto-onboard" reference (it's the actual API endpoint name).

Changes are documentation-only:
- Rename in code comments + test descriptions + plan docs.
- Clarify framing in mcp_client.go + provisioner.go doc comments to
  emphasize Bitrix-specific glue (not generic MCP infra).
- Reuse existing mcp_user_credentials table + MCPServerStore methods
  (no schema / store / abstraction change).

Files:
- cmd/gateway.go (factory registration doc)
- internal/channels/bitrix24/{channel,factory,mcp_client,provisioner}.go
- internal/channels/bitrix24/{mcp_client,provisioner}_test.go
- plan/goclaw-mcp-integration.md (21 occurrences)

Verified: go build + MCP-related tests pass (TestProvision*,
TestInitMCPProvisioner*, TestMCPClient*).

Phase 1 of Path C execution per
plans/reports/decision-log-260519-1555-bitrix24-pr-fork-decision.md.
@tech-synity
tech-synity force-pushed the feat/bitrix24-channel-core branch from 7197257 to 8b45b98 Compare May 23, 2026 02:53
Synity's fork once shipped 000058_bitrix_portals under the same migration
number as upstream's 000058_agent_grants_env_override. DBs that ran the
fork-numbered migration first have schema_migrations.version=58 marked
with bitrix_portals content, so golang-migrate skips upstream's 000058
forever — the encrypted_env BYTEA column is never added on those DBs.

Symptom: secure_cli.list returns
  "column g.encrypted_env does not exist (SQLSTATE 42703)"
which the UI surfaces as an empty "No CLI credentials yet" state even
though rows exist in secure_cli_binaries.

Add an idempotent ALTER TABLE ... ADD COLUMN IF NOT EXISTS to re-assert
the column. No-op on fresh DBs where 000058 already added it, real fix
on legacy fork DBs. Bumps RequiredSchemaVersion 68 -> 69. [B24:2794]
Tool MEDIA:<path> output reached channel file-upload sinks (Bitrix
imbot.v2.File.upload, Telegram sendDocument, etc.) verbatim via
parseMediaResult, with no workspace-boundary check. A malicious or buggy
tool emitting MEDIA:/etc/passwd could exfiltrate arbitrary files to chat.

Extract the EvalSymlinks+Rel containment from extractMediaFromContent into
a shared confineToWorkspace helper and apply it at the parseMediaResult
sink in processToolResult. Fixing at the source/egress boundary protects
every channel at once rather than per-channel. Paths that escape the
workspace are dropped and logged (security.media_path_rejected).

Add TestConfineToWorkspace (boundary unit) and
TestParseMediaResultConfinedToWorkspace (sink regression for H2).
…I [B24:2794]

Bitrix24 channel was text-only; attachments were parsed but dropped.
- Inbound: download chat files via imbot.v2.File.download (one-time URL),
  forward to the agent with MIME preserved (internal/channels/bitrix24/download.go).
- Outbound: upload agent media to the chat via imbot.v2.File.upload
  (internal/channels/bitrix24/send_media.go).
- Add BaseChannel.HandleMessageMedia to preserve MIME/filename through the bus.
- Per-channel media_max_mb cap (default 20) applies to both directions.

Tests: 92 pass (internal/channels/bitrix24 + internal/channels), go vet clean (PG + sqliteonly).
… API [B24:2794]

Move outbound REST calls to the imbot v2 family (keeps register on v1):
- imbot.message.add -> imbot.v2.Chat.Message.send (fields.message shape, live-verified)
- imbot.bot.list (+ legacy imbot.list fallback) -> imbot.v2.Bot.list; add botListRows
  to normalize the v2 {bots:[...]} envelope, legacy array, and id-keyed map forms
- imbot.unregister -> imbot.v2.Bot.unregister

Bot registration stays on v1 imbot.register: v2 imbot.v2.Bot.register changes the
event-delivery model (per-event handler URLs -> eventMode), which would require
rewriting the inbound event parser. No user-facing behavior change.

Tests: bitrix24 package green; go vet ./... clean.
…[B24:2794]

Bot was leaking HiddenMessage (whisper) replies to the external Zalo
connector because every outbound call went through imbot.v2.Chat.Message.send,
which has no equivalent of the v1 SKIP_CONNECTOR flag. Branch the outbound
path on inbound visibility:

  whisper → imbot.message.add + SKIP_CONNECTOR=Y  (v1, send_v1.go)
  public  → imbot.v2.Chat.Message.send + fields.replyId  (v2, send_v2.go)

Pipeline:
  events.go        parse data[PARAMS][PARAMS][COMPONENT_ID]=HiddenMessage
                   into EventParams.IsHiddenMessage (form + JSON variants)
  handle.go        set bitrix_visibility on InboundMessage.Metadata
  consumer         forward visibility + message_id into OutboundMessage
  send.go          resolveSendOptions + sendChunk dispatcher +
                   shared callWithRateLimitRetry helper
  metadata_keys.go single source of truth for the keys + values

Defaults preserve pre-refactor behaviour: callers that don't populate
bitrix_visibility still go through v2 public, and replyId is omitted
unless a numeric bitrix_message_id arrives in metadata.

Tests:
  TestParseEvent_FormURLEncoded_IsHiddenMessage  (3 cases)
  TestParseEvent_JSON_IsHiddenMessage             (3 cases)
  TestResolveSendOptions                          (8 cases)
  TestSend_BranchesOnVisibility                   (4 cases)
Openline sender-tag echo (this change):
- Capture the connector sender tag ("[name #id]:" or "[name] #id:") from
  inbound openline group messages, strip it from the body the agent sees,
  and re-prepend the canonical "[name] #id:" form to the reply so the Open
  Channel connector routes the answer back to the right external user.
- New sender_prefix.go helper (+ test) accepts both inbound layouts and
  emits one canonical form; scoped to messages carrying the tag, so plain
  chats are unaffected.
- metadata_keys.go: MetaKeySenderPrefix; handle.go capture/strip/stash;
  gateway_consumer_normal.go forwards the key; send.go prepends it on the
  first chunk before chunking.

Bundled bitrix24 channel-core work already on this branch:
- handle.go: @mention is the sole trigger for both staff and connector
  customers; unmentioned traffic is dropped (was: drop all connector msgs).
- isGroupMessageType: treat SONET_GROUP "B" as a group.
- handle_test.go, mcp_client_test.go: cover the above.
…d [B24:2794]

The Open Channel connector dropped the trailing colon from its sender tag:
inbound now arrives as "[Name] #id <msg>" (was "[Name] #id: <msg>"). The
id-bearing patterns required the colon, so the tag fell through to the
name-only branch and the reply echoed "[Name]" — dropping the #id the
connector needs to route the answer back.

- sender_prefix.go: make the trailing ":" optional on both id layouts
  ([name #id] / [name] #id, with or without colon) and echo the canonical
  "[name] #id" (no colon) to match the connector's current format. Bare
  "[name]" (no id) still echoes "[name]" for Open Channel only.
- handle.go: gate the bare name-only layout to Open Channel (isOpenChannel)
  so ordinary group chats starting with "[x] ..." are left untouched.
- sender_prefix_test.go: cover colon/no-colon x id-inside/id-outside, the
  name-only openline case, and the non-openline no-op.
…B24:2794]

- download.go: block redirect-based SSRF on inbound media. CheckRedirect
  re-validates each hop (http(s) only, reject private/loopback/link-local
  hosts, cap hops); the initial portal-domain pin is no longer bypassable
  via a 3xx to an internal service. Public-host redirects still allowed.
- handle.go: extract/echo the openline sender tag only for Open Channel
  sessions (was: any group chat), removing bogus prefixes in CRM group
  chats and narrowing the forged-tag misroute surface.
- loop_tools.go + loop_media.go: confine result.Media to the agent / team /
  tenant-allowed roots (new confineToAnyRoot) before a channel uploads it,
  so a prompt-injected out-of-workspace path (e.g. /etc/passwd) cannot
  exfiltrate, while legitimate cross-workspace media (team files, delegatee
  output) still flows.
- send_media.go: bounded outbound read via io.LimitReader replaces the
  os.Stat + os.ReadFile pair, closing the TOCTOU size-cap bypass; cap a
  single message's outbound attachments at 10 (mirrors inbound).
- register.go: paginate imbot.v2.Bot.list (limit/offset + hasNextPage,
  capped at 40 pages) so verify/lookup see bots past the first 50.
- mcp_client.go: redact access_token / refresh_token / client_secret from an
  echoed MCP error body before it is logged or returned (+ test).
require_user_credentials MCP servers loaded and connected, but their tool
defs were never sent to the model, so the agent could never call the
per-user mcp_<prefix>__* tools (it only ever saw the shared MCP server).
The per-user tool objects are intentionally kept out of the shared registry
to prevent cross-user credential leaks, so FilterTools could not emit them.

Surface them through a request-scoped overlay registry passed to the policy
engine: per-user tools are now evaluated AND emitted under the same
allow/deny rules as registry tools, then resolved per-actor at execution
time via executeToolForActor. The overlay is discarded after filtering, so
the shared registry and other users stay unaffected.

- add tools.NewUserToolOverlay (request-scoped, discarded post-filter)
- buildFilteredTools: wrap registry in overlay when per-user tools present;
  no-policy path appends their defs directly
- fix stale getUserMCPTools docstring (claimed shared-registry registration)
- tests: overlay policy allow/deny + per-user surfacing / strip / dedup
mrgoonie pushed a commit that referenced this pull request Jun 22, 2026
…echo, and hardening (#1236)

* refactor(bitrix24): rename "Path B" framing to maintainer-specified naming [B24:2794]

Per maintainer hard rule #10 (no generic "Path A/B" framing) from PR #1061
review. The Bitrix24 MCP auto-onboard flow is Bitrix-specific glue
("Bitrix24 OAuth -> existing mcp_user_credentials bridge"), NOT a generic
MCP architecture pattern.

Naming convention applied consistently:
- First mention per file: full "Bitrix24 OAuth -> existing
  mcp_user_credentials bridge" (matches maintainer comment verbatim).
- Subsequent mentions in same file: shortened "mcp_user_credentials bridge".
- Test/log context referencing literal endpoint /api/auto-onboard: keep
  "auto-onboard" reference (it's the actual API endpoint name).

Changes are documentation-only:
- Rename in code comments + test descriptions + plan docs.
- Clarify framing in mcp_client.go + provisioner.go doc comments to
  emphasize Bitrix-specific glue (not generic MCP infra).
- Reuse existing mcp_user_credentials table + MCPServerStore methods
  (no schema / store / abstraction change).

Files:
- cmd/gateway.go (factory registration doc)
- internal/channels/bitrix24/{channel,factory,mcp_client,provisioner}.go
- internal/channels/bitrix24/{mcp_client,provisioner}_test.go
- plan/goclaw-mcp-integration.md (21 occurrences)

Verified: go build + MCP-related tests pass (TestProvision*,
TestInitMCPProvisioner*, TestMCPClient*).

Phase 1 of Path C execution per
plans/reports/decision-log-260519-1555-bitrix24-pr-fork-decision.md.

* fix: confine outbound media paths to agent workspace [B24:2794]

Tool MEDIA:<path> output reached channel file-upload sinks (Bitrix
imbot.v2.File.upload, Telegram sendDocument, etc.) verbatim via
parseMediaResult, with no workspace-boundary check. A malicious or buggy
tool emitting MEDIA:/etc/passwd could exfiltrate arbitrary files to chat.

Extract the EvalSymlinks+Rel containment from extractMediaFromContent into
a shared confineToWorkspace helper and apply it at the parseMediaResult
sink in processToolResult. Fixing at the source/egress boundary protects
every channel at once rather than per-channel. Paths that escape the
workspace are dropped and logged (security.media_path_rejected).

Add TestConfineToWorkspace (boundary unit) and
TestParseMediaResultConfinedToWorkspace (sink regression for H2).

* feat(bitrix24): support inbound + outbound media via imbot.v2 File API [B24:2794]

Bitrix24 channel was text-only; attachments were parsed but dropped.
- Inbound: download chat files via imbot.v2.File.download (one-time URL),
  forward to the agent with MIME preserved (internal/channels/bitrix24/download.go).
- Outbound: upload agent media to the chat via imbot.v2.File.upload
  (internal/channels/bitrix24/send_media.go).
- Add BaseChannel.HandleMessageMedia to preserve MIME/filename through the bus.
- Per-channel media_max_mb cap (default 20) applies to both directions.

Tests: 92 pass (internal/channels/bitrix24 + internal/channels), go vet clean (PG + sqliteonly).

* refactor(bitrix24): migrate messaging/bot-list/unregister to imbot v2 API [B24:2794]

Move outbound REST calls to the imbot v2 family (keeps register on v1):
- imbot.message.add -> imbot.v2.Chat.Message.send (fields.message shape, live-verified)
- imbot.bot.list (+ legacy imbot.list fallback) -> imbot.v2.Bot.list; add botListRows
  to normalize the v2 {bots:[...]} envelope, legacy array, and id-keyed map forms
- imbot.unregister -> imbot.v2.Bot.unregister

Bot registration stays on v1 imbot.register: v2 imbot.v2.Bot.register changes the
event-delivery model (per-event handler URLs -> eventMode), which would require
rewriting the inbound event parser. No user-facing behavior change.

Tests: bitrix24 package green; go vet ./... clean.

* feat(bitrix24): route whisper via v1 SKIP_CONNECTOR + add v2 replyId [B24:2794]

Bot was leaking HiddenMessage (whisper) replies to the external Zalo
connector because every outbound call went through imbot.v2.Chat.Message.send,
which has no equivalent of the v1 SKIP_CONNECTOR flag. Branch the outbound
path on inbound visibility:

  whisper → imbot.message.add + SKIP_CONNECTOR=Y  (v1, send_v1.go)
  public  → imbot.v2.Chat.Message.send + fields.replyId  (v2, send_v2.go)

Pipeline:
  events.go        parse data[PARAMS][PARAMS][COMPONENT_ID]=HiddenMessage
                   into EventParams.IsHiddenMessage (form + JSON variants)
  handle.go        set bitrix_visibility on InboundMessage.Metadata
  consumer         forward visibility + message_id into OutboundMessage
  send.go          resolveSendOptions + sendChunk dispatcher +
                   shared callWithRateLimitRetry helper
  metadata_keys.go single source of truth for the keys + values

Defaults preserve pre-refactor behaviour: callers that don't populate
bitrix_visibility still go through v2 public, and replyId is omitted
unless a numeric bitrix_message_id arrives in metadata.

Tests:
  TestParseEvent_FormURLEncoded_IsHiddenMessage  (3 cases)
  TestParseEvent_JSON_IsHiddenMessage             (3 cases)
  TestResolveSendOptions                          (8 cases)
  TestSend_BranchesOnVisibility                   (4 cases)

* feat(bitrix24): openline sender-tag echo on replies [B24:2794]

Openline sender-tag echo (this change):
- Capture the connector sender tag ("[name #id]:" or "[name] #id:") from
  inbound openline group messages, strip it from the body the agent sees,
  and re-prepend the canonical "[name] #id:" form to the reply so the Open
  Channel connector routes the answer back to the right external user.
- New sender_prefix.go helper (+ test) accepts both inbound layouts and
  emits one canonical form; scoped to messages carrying the tag, so plain
  chats are unaffected.
- metadata_keys.go: MetaKeySenderPrefix; handle.go capture/strip/stash;
  gateway_consumer_normal.go forwards the key; send.go prepends it on the
  first chunk before chunking.

Bundled bitrix24 channel-core work already on this branch:
- handle.go: @mention is the sole trigger for both staff and connector
  customers; unmentioned traffic is dropped (was: drop all connector msgs).
- isGroupMessageType: treat SONET_GROUP "B" as a group.
- handle_test.go, mcp_client_test.go: cover the above.

* feat(bitrix24): accept colon-less openline sender tag, echo [name] #id [B24:2794]

The Open Channel connector dropped the trailing colon from its sender tag:
inbound now arrives as "[Name] #id <msg>" (was "[Name] #id: <msg>"). The
id-bearing patterns required the colon, so the tag fell through to the
name-only branch and the reply echoed "[Name]" — dropping the #id the
connector needs to route the answer back.

- sender_prefix.go: make the trailing ":" optional on both id layouts
  ([name #id] / [name] #id, with or without colon) and echo the canonical
  "[name] #id" (no colon) to match the connector's current format. Bare
  "[name]" (no id) still echoes "[name]" for Open Channel only.
- handle.go: gate the bare name-only layout to Open Channel (isOpenChannel)
  so ordinary group chats starting with "[x] ..." are left untouched.
- sender_prefix_test.go: cover colon/no-colon x id-inside/id-outside, the
  name-only openline case, and the non-openline no-op.

* fix: security and robustness fixes from the bitrix24 channel review [B24:2794]

- download.go: block redirect-based SSRF on inbound media. CheckRedirect
  re-validates each hop (http(s) only, reject private/loopback/link-local
  hosts, cap hops); the initial portal-domain pin is no longer bypassable
  via a 3xx to an internal service. Public-host redirects still allowed.
- handle.go: extract/echo the openline sender tag only for Open Channel
  sessions (was: any group chat), removing bogus prefixes in CRM group
  chats and narrowing the forged-tag misroute surface.
- loop_tools.go + loop_media.go: confine result.Media to the agent / team /
  tenant-allowed roots (new confineToAnyRoot) before a channel uploads it,
  so a prompt-injected out-of-workspace path (e.g. /etc/passwd) cannot
  exfiltrate, while legitimate cross-workspace media (team files, delegatee
  output) still flows.
- send_media.go: bounded outbound read via io.LimitReader replaces the
  os.Stat + os.ReadFile pair, closing the TOCTOU size-cap bypass; cap a
  single message's outbound attachments at 10 (mirrors inbound).
- register.go: paginate imbot.v2.Bot.list (limit/offset + hasNextPage,
  capped at 40 pages) so verify/lookup see bots past the first 50.
- mcp_client.go: redact access_token / refresh_token / client_secret from an
  echoed MCP error body before it is logged or returned (+ test).

* fix(security): validate resolved dial IP on Bitrix media redirects [B24:2794]

The inbound media download redirect guard only string-checked the redirect
hostname (isPrivateOrLoopback on req.URL.Hostname()), so a redirect to a public
hostname that resolves to 127.0.0.1 / 169.254.169.254 / an RFC1918 address — or a
DNS-rebinding swap between check and dial — still passed the guard and the client
would connect. Reported in PR review.

Add security.NewRedirectFollowingSafeClient: it follows redirects but validates
the RESOLVED destination IP of every hop at dial time via net.Dialer.Control,
reusing the existing blocked-CIDR list. The IP it checks is the IP actually
dialed, so both redirect-to-internal and DNS rebinding are refused, while
legitimate public CDN redirects still succeed. download.go now uses it instead of
the hostname-string guard.

Tests: deterministic dial-control table (loopback / link-local / private /
multicast / unspecified / public, v4 + v6), malformed/non-IP addr, test bypass,
loopback-dial-blocked client wiring, and redirect cap + scheme checks.

* feat(bitrix24): per-participant Zalo openline identity from 3-token sender tag [B24:2794]

Parse the connector's "[Name] #uid #msgId" sender tag so each external
customer in a shared Open Channel group gets its own contact + USER.md
instead of collapsing onto the connector proxy id. Identity minting is
gated on IS_CONNECTOR=Y to reject operator forged tags. Echo back the
msgId only ("#msgId") on replies; keep the legacy single-number and
name-only layouts unchanged. Zero DB migration.

- sender_prefix.go: parseOpenlineSenderTag() classifies 3-token / legacy / name-only
- handle.go: synthetic senderID "openlines:{instance}:{chat}:{uid}" + participant_user_id metadata, gated on FromIsConnector
- gateway_consumer_normal.go: deriveGroupUserID() routes participant -> per-person scope, group fallback otherwise
- send.go: buildAddressMention numeric-id guard so synthetic ids don't emit invalid [USER=...] BBCode
- MetaKeyMessageID kept as Bitrix MESSAGE_ID (drives v2 fields.replyId); connector msgId surfaced only via echo prefix

---------

Co-authored-by: DangTinh311 <dangtinh31193@gmail.com>
Co-authored-by: Chinh Dang <chinhdang@192.168.68.104>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants