- Iterate over all DNS responses instead of just the first IP to prevent
multi-IP SSRF bypass where a blocked IP appears after a public one
- Make resolver injectable via package-level lookupHost variable
- Replace real-DNS test (google.com) with stubbed resolver covering:
single public/private IP, DNS failure, empty results, invalid IP,
multi-IP public→private, private→public, all public, metadata IP,
IPv6 loopback
- Add fmt import to test file for stubbed error messages
- Add validateSelfHostedDomain() with SSRF protection (blocked CIDRs,
hostname resolution, localhost/.local/.localhost rejection)
- Validate port range 1-65535 for self-hosted domains with ports
- Mirror SSRF + port validation in frontend form
- Add comprehensive tests: SSRF blocked IPs, invalid ports, valid public
domains
Split bitrixDomainRegex into bitrixCloudDomainRegex (cloud SaaS domains)
and selfHostedDomainRegex (custom FQDNs). Users can now register portals
with self-hosted Bitrix24 instances like bx.mycompany.com instead of
being restricted to *.bitrix24.{com,eu,...} domains.
Squash merge PR #113 after resolving the project changelog conflict with current dev. Local checks passed: Go store/http/gateway/agent/pipeline tests, SQLite-tagged tests, both Go builds, web Vitest, and web build. PR CI run 26705098712 passed release-versioning, go, and web.
Squash merge PR #112 after resolving the project changelog conflict with current dev. Local checks passed: config gateway tests, provider/http/tools deny-pattern tests, go build ./..., and go build -tags sqliteonly ./.... PR CI run 26704832350 passed release-versioning, go, and web.
Implements 3 coalescing layers to handle rapid multi-attachment inbounds:
- Bus debouncer: delays inbound messages 1s, merges duplicates
- Web chat debouncer: buffers client-side inbound frames for batch RPC
- Telegram album aggregator: collects album members via AfterFunc+Stop timer
Drops media-bypass shortcut (forces 1s media floor). Aggregator enforces:
- AfterFunc+Stop timer discipline with ordered drain on stop
- 2-tuple (album_id, sender) keying for isolation
- Dual DoS caps: max 10 albums per sender, max 100 messages per album
- merged_message_ids dedup seeding across all 3 surfaces
Closes#63
Resolve the PR #8/#9/#10 stack on current dev, including Bitrix24 install callback hardening, migration renumbering, duplicate-domain fail-closed routing, UI textarea/mobile cleanup, and review hardening.
Reject cross-tenant skill grant and revoke operations before grant rows or skill visibility can be changed. Clean legacy invalid grant rows in PostgreSQL and SQLite migrations, hide owner IDs from skill API/UI responses, and cover the tenant-isolation cases with PG and SQLite regression tests.
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.
* feat(skills): add privacy/visibility controls for agent-owned skills
Closes#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 #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.
- Add vault_documents.chat_id + composite index (migration 000056)
- Filter vault_search by chat_id when team.workspace_scope=isolated
- Stamp chat_id on AfterWrite/AfterWriteMedia for isolated teams
- Deny cross-chat vault_read in isolated teams (M2 fix)
- RunContext.TeamIsolated flag resolved once per run
- Fallback WorkspaceChatID → ChatID in loop_context for entry points
that don't set WorkspaceChatID explicitly (WS direct, HTTP, cron)
Fixes cross-chat doc leak where agent in chat A could see vault docs
from chat B within the same isolated team.
Post-review cleanup of Phase 4. Closes Finding #9 properly and corrects
the Finding #13 documentation lie surfaced in the code-review report.
Capability schema:
- Replace `AgentOverridable bool` with `AgentOverridableAs string` on
ParamSchema. Empty string = not overridable; non-empty = the generic
key alias (`"speed"`, `"emotion"`, `"style"`).
- Each provider declaration now carries the alias inline, so the
generic↔native mapping has a single TS-readable source.
Frontend:
- Web `tts-override-block.tsx` drops the inline `GENERIC_TO_NATIVE`
literal and derives the bidirectional adapter from the filtered
capability params (each param self-describes its alias). Adapter
tests rewritten around the new shape.
- Desktop `AgentDetailPanel.tsx` drops the 45-line inline IIFE in
favour of a new `<TtsOverrideFineTune>` component that uses the
same alias-based mapping.
Backend:
- Move `AgentTTSParamsAllowedKeys` + `ValidateAgentTTSParams` to
`internal/audio/agent_params_adapter.go`. HTTP `validate.go` and WS
`gateway/methods/agents_update.go` both delegate, eliminating the
duplicated `{speed, emotion, style}` literal.
Cleanup:
- Delete orphan i18n keys `MsgTtsParamInvalidJSON` and
`MsgTtsParamDependsOn` from `keys.go` + en/vi/zh catalogs (no
in-code references; DependsOn is FE-only, JSON parse failures
already surface via slog).
Documentation:
- `prompt-settings-section.tsx` Finding #13 comment rewritten to
honestly describe the best-effort merge into a fresh local copy of
the cached `otherConfig` prop. Concurrent-tab clobber remains
possible — server-side JSON-merge-patch endpoint planned for v2.
Tests: 9 backend suites (race), web 217/217, desktop build clean,
both Go build tags pass.
Phase 4 — final phase of the TTS params/layout/agent-override plan.
Adds a 3-key allow-list (`speed`, `emotion`, `style`) per agent stored
in `agents.other_config.tts_params`. Backend resolves and merges into
`opts.Params` PER ATTEMPT inside the fallback loop so each provider
sees its own native shape — never the primary's keys when fallback
runs (Finding #1 critical).
Backend:
- `AgentOverridable bool` on `audio.ParamSchema`. UI filter reads this
flag from /v1/tts/capabilities; no separate TS literal mirror —
capabilities API is the single source of truth (Finding #9).
- `audio.AdaptAgentParams(generic, provider)` maps the 3 generic keys
to provider-native paths (e.g. `speed` → `voice_settings.speed` for
ElevenLabs, flat `speed` for OpenAI/MiniMax, dropped for Edge/Gemini).
- `Manager.SynthesizeWithFallbackAdapted` adapts inside the loop so
fallback providers receive correctly-shaped params.
- `manager_auto.go` and `tools/tts.go` Execute do per-attempt adaptation
on the tenant + direct + fallback call sites.
- Drop log bumped to `slog.Info("tts.agent.params.dropped", ...)` for
audit trail when a generic key isn't supported by the active provider.
- Cross-check test asserts every adapter switch case has at least one
capability ParamSchema with `AgentOverridable: true`, and vice versa.
Security (red-team findings):
- Allow-list ENFORCED at write path: `validateAgentTTSParams` in HTTP
`handleUpdate` AND WS `agents_update` rejects any `tts_params` key
outside `{speed, emotion, style}` (Finding #5).
- 64KB body cap on agent PUT via `http.MaxBytesReader` (Finding #6).
- Explicit tenant-scope guard after `agents.GetByID` (Finding #12).
- Concurrent-tab clobber: handleSave merges `tts_params` into a fresh
copy of `otherConfig` rather than reusing stale state (Finding #13).
- Rate-limit verified — RoleAdmin gate sufficient for v1 (Finding #15).
Frontend (web + desktop):
- `TtsOverrideBlock` rewritten: filters capability params to
`agent_overridable === true`, renders via `DynamicParamForm`. Hides
entirely for providers with no overridable params (Edge, Gemini).
- Bidirectional adapter (generic ↔ capability-native form state) so
agent storage stays in generic keys while UI works in native paths.
25 round-trip tests cover all 5 providers.
- Desktop `AgentDetailPanel` gains an inline fine-tune section gated
on `globalProvider`, reusing the desktop `DynamicParamForm`.
i18n: `tts.override.params.title` ("Fine-tune") added to web + desktop
en/vi/zh.
Tests: all 9 backend suites green (race), web 214/214, desktop build
clean, both Go build tags pass.
Add missing ResetStuckSummoning method stub to mock AgentStore
implementations in test files. Follows from recent agent recovery
feature (d7342fbd) which added this method to the AgentStore interface
but missed updating these 4 test mock implementations.
- Add per-tenant TTS configuration endpoints (GET/POST /v1/tts/config)
with RoleAdmin auth instead of master scope
- Implement TenantTTSResolver for channels to use tenant-specific
TTS providers and auto mode settings
- Add per-agent voice override in channel TTS auto-apply:
- Extend OutboundMessage with AgentID and AgentOtherConfig
- Inject AgentAudioSnapshot in dispatch.go from outbound message
- MaybeApply reads tts_voice_id/tts_model_id from agent context
- Fix events.go to use RunContext.TenantID directly (H3)
- Fix dispatch.go error notification to use sendCtx (H1)
- Update UI to use new /v1/tts/config endpoints
When user sends exact cancel keywords (stop, cancel, abort, thôi, dừng,
hủy, 取消, 停, nevermind) while agent is busy, auto-trigger abort instead
of injecting as follow-up message. Exact match only - "stop now" still
injects normally to avoid false positives.
Dashboard users (and other tenant-authenticated admin/operator/owner
actors) dispatching team tasks to agents in Telegram/Discord groups
previously hit the empty-sender DENY rule when the assigned agent
tried to write files — because dashboard users have tenant identities
(e.g. "viettx") that don't match the numeric file-writer grants stored
per channel.
Two-part fix:
1. CheckFileWriterPermission / CheckCronPermission add an RBAC bypass
at the store layer: if ctx carries RoleFromContext in {admin,
operator, owner}, skip the per-user grant check. Tenant RBAC already
pre-authenticated these callers at the gateway edge; requiring a
redundant per-channel grant blocks legitimate admin work.
2. Role propagation through the full dispatch chain:
ctx (RoleFromContext)
→ RunRequest.Role
→ SubagentTask.OriginRole / AnnounceMetadata.OriginRole
/ DelegateRequest.Role
→ InboundMessage.Metadata[MetaOriginRole]
→ subagentAnnounceRouting.Role / teammateRole
→ RunRequest.Role (re-ingress)
→ store.WithRole(ctx, ...) in loop_context
Covers subagent announce, delegate announce, teammate dispatch
(WS dashboard, tool), session_send, and processNormalMessage's
synthetic-sender override path. team_tasks_create persists
origin_role for deferred dispatches.
Viewer and empty roles fall through to the existing sender-based path
(no bypass).
Regression tests:
- A.10 AdminRoleBypass: admin/operator/owner in ctx allowed even with
empty sender (normally DENY).
- A.11 ViewerRoleDoesNotBypass: viewer and empty roles still hit DENY.
Builds + integration + unit tests green.
Closes the two last unpropagated paths identified by the #915 e2e audit.
processNormalMessage now prefers MetaOriginSenderID when the on-wire
SenderID is a synthetic internal sender (notification:, ticker:,
session_send_tool, system:escalation, etc.). Real user senders pass
through unchanged. Defense-in-depth: MetaOriginSenderID is ignored if
it itself looks synthetic, preventing a propagation bug from turning
into a permission bypass.
teams_tasks_mutations dashboard dispatch now injects MetaOriginSenderID
into the teammate InboundMessage metadata, preferring the WS caller's
sender (live dashboard) and falling back to origin_sender_id stored on
the task (for deferred dispatches). Without this, dashboard-triggered
team tasks in group chats would hit the empty-sender DENY rule.
team_tasks_create persists origin_sender_id into task.Metadata at
creation time so the deferred dashboard dispatch path can reconstruct
the original actor identity.
- PG migration 000054 + SQLite v21→v22: add nullable name column
- HookConfig.Name in Go struct, PG/SQLite scan/insert, WS handlers
- Builtin seed writes spec ID as hook name
- UI: name input in create/edit form, displayed in hook list row
- Beta card: "Learn more" button opens modal explaining hooks×skills×MCP
- Default handler_type changed from http to script
- Script editor section: max-h-[50vh] with scroll, border always visible
- i18n: all new keys in en/vi/zh
Code-review findings from Wave 1 audit (commits 097776c5..f76a681e).
Critical (security):
- C1 internal/gateway/methods/hooks.go: parseHookConfigParams strips
caller-supplied source/id/created_by/version. Without this a tenant
admin could POST {"source":"builtin"} and escalate their UI hook into
the dispatcher's builtin capability tier (which is allowed to mutate
event input). Default Source resolves to "ui" via Validate.
- C2 internal/gateway/methods/hooks.go: handleUpdate also strips source +
created_by from the patch map. Same forge surface via PATCH instead of
POST.
High:
- H1 internal/gateway/methods/hooks.go: Update + Delete handlers wrap
hooks.ErrBuiltinReadOnly into the i18n key MsgHookBuiltinReadOnly so
users get a localized "builtin hooks are read-only" message instead of
the raw English sentinel string.
- H3 internal/hooks/handlers/script.go: bound tenantSems map with
opportunistic sweep — once map crosses 64 entries, idle slots
(no in-flight grants AND last-used > 1h ago) are reclaimed under the
same lock the acquire path already takes. Goroutine-free; common-path
cost unchanged.
Medium:
- M1 internal/hooks/dispatcher.go: builtinAllowlistLookup uses
atomic.Pointer instead of plain package var. Fixes the parallel-test
race where one test installs a lookup while another reads.
- M3 ui/web/index.html: drop http: https: from CSP connect-src — keep
'self' + ws:/wss: only. Same-origin XHR/WS still works; cross-origin
HTTP is no longer wildcard-permitted.
Tests:
- internal/gateway/methods/hooks_source_strip_test.go pins the C1+C2
strip behavior at the parse layer with a regression case that posts
every forge field and asserts each is zeroed.
Adds config.defaults WS method so UI can render pruning-knob placeholders
that mirror the server's internal fallback values. Exports
agent.DefaultPruningValues() as the single source of truth used by both
the resolver and this RPC — keeps UI in sync without a parallel table.
Exposes agent hooks over WebSocket (hooks.list/create/update/delete/
toggle/test/history) behind existing auth layer. Additions:
- Gateway wiring: buildHookHandlers() factory reused by dispatcher and
hooks.test runner, so UI test panel exercises production code paths
- GetByID tenant-scope guard: non-master callers only see own tenant +
global rows (matches List() behavior on both PG and SQLite)
- i18n keys + catalogs (en/vi/zh) for hooks error messages
- Protocol method constants for hooks.* and config.defaults