* fix(security): harden exec path exemption matching (#721)
- Add absolute path exemption for dataDir/skills-store/ (fixes skill
scripts using absolute paths like /app/data/skills-store/ being denied)
- Strip surrounding quotes before prefix matching (LLMs often quote paths)
- Reject path traversal ("..") in exempt fields to prevent escape
- Switch from "any field exempt → skip" to per-field matching: only exempt
if ALL fields that match the deny pattern are individually exempt
- Closes pipe/comment bypass vectors where an exempt path in one argument
would exempt the entire command including non-exempt paths
Includes 27 test cases covering: legitimate access, quoted paths,
path traversal, unicode bypass, pipe/comment bypass, mixed args.
* fix(permissions): use cron-specific permission check for cron tool
Cron tool was hardcoded to check `file_writer` configType via
CheckFileWriterPermission(), ignoring the `cron` configType that
the UI actually saves when granting cron permissions. This caused
agents in group chats to be denied cron access even with correct
permission configured.
Add ConfigTypeCron constant and CheckCronPermission() that checks
`cron` configType first, falling back to `file_writer`.
---------
Co-authored-by: Viet Tran <viettranx@gmail.com>
* fix(chat): load message history when selecting existing conversation from clean state
The skipNextHistoryRef was unconditionally set when sessionKey transitioned
from empty to non-empty. This prevented loadHistory() from running when
clicking an existing conversation from the initial /chat page. The skip
was only intended for the new-chat send flow where the optimistic message
is already displayed.
Guard the skip with expectingRunRef so it only activates when a message
send is in flight.
Closes#729
* docs: add UI diff evidence for PR #730
Before/after screenshots and HTML comparison report showing
first conversation click behavior fix.
- Add absolute path exemption for dataDir/skills-store/ (fixes skill
scripts using absolute paths like /app/data/skills-store/ being denied)
- Strip surrounding quotes before prefix matching (LLMs often quote paths)
- Reject path traversal ("..") in exempt fields to prevent escape
- Switch from "any field exempt → skip" to per-field matching: only exempt
if ALL fields that match the deny pattern are individually exempt
- Closes pipe/comment bypass vectors where an exempt path in one argument
would exempt the entire command including non-exempt paths
Includes 27 test cases covering: legitimate access, quoted paths,
path traversal, unicode bypass, pipe/comment bypass, mixed args.
Sort all non-deterministic map iterations that affect system prompt and
tool definitions sent to LLM APIs. Go map iteration order is random,
causing prompt prefix to change every turn — breaking Anthropic/OpenAI
prompt caching (cache by exact prefix match).
Fixed 5 sources of non-deterministic ordering:
- Registry.List(): sort canonical tool names
- Registry.ProviderDefs(): sort tools + aliases before building defs
- PolicyEngine.FilterTools(): sort alias iteration (single Aliases() call)
- buildMCPToolsInlineSection(): sort MCP tool names in system prompt
- GetAgentContextFiles/GetUserContextFiles: ORDER BY file_name (PG+SQLite)
Based on PR #718 by @therichardngai-code with additional fixes:
- Context files from DB now deterministic (ORDER BY file_name)
- FilterTools() calls registry.Aliases() once instead of 3 times
Compact columns: status as icon-only, merge time+duration into one column.
Truncate long user IDs, clean <media:*> tags from preview, move badges to second line.
Agents with exec in their deny list cannot run CLI commands, so
injecting wrangler/gh credential context is misleading — the LLM
sees instructions for tools it cannot use. Gate the section on
exec being present in the filtered tool list.
* fix(tools): quote-aware shell operator detection in credentialed exec (#700)
- Replace detectShellOperators with detectUnquotedShellOperators in
credentialed exec path — respects single/double quoting so that
characters like | inside argument values (e.g. --jq '.[0] | .name')
are not falsely flagged as shell operators
- Pass raw command string (preserving quotes) to executeCredentialed
instead of reconstructing from parsed args
- Downgrade "no credential found" log from Warn to Debug (fires for
every non-credentialed command, too noisy at Warn)
- Add extractUnquotedSegments() helper with comprehensive tests
* fix(tools): handle backslash escape outside quotes in shell operator detection
extractUnquotedSegments did not handle \ as an escape character outside
of quotes, causing \" to incorrectly enter double-quote mode. This hid
subsequent shell operators from detection (e.g. gh \"arg\" | env would
not detect the unquoted pipe).
Add backslash escape handling in the unquoted state to match
go-shellwords parsing behavior. Both \ and the escaped character are
emitted as unquoted content so operator detection still catches them.
---------
Co-authored-by: viettranx <viettranx@gmail.com>
* fix(ci): skip CI condition in semantic-release for main branch
go-semantic-release auto-detects the default branch from GitHub API
(which is dev), but releases are cut from main. The CI condition
rejects runs on non-default branches. Use --no-ci to bypass this
check since the workflow already gates on push to main.
* docs: document CI/CD pipelines, release flow, and v2.66.0 changelog
- CLAUDE.md: add CI/CD & Releases section with workflow table, tag
patterns, Docker variants, beta/desktop release commands
- CONTRIBUTING.md: expand Releases section with standard (auto),
beta (manual tag), and desktop release workflows
- docs/17-changelog.md: add v2.66.0 entry covering IDOR fix, BytePlus
provider, per-agent grants, beta pipeline, and CI fixes
* fix(telegram): handle group-to-supergroup migration seamlessly
When a Telegram group upgrades to a supergroup, the chat ID changes and
all existing references become stale. This caused send failures (400),
orphaned sessions, and required manual re-pairing.
Add dual-path migration handling:
- Proactive: intercept inbound MigrateToChatID before isServiceMessage
- Reactive: detect 400 + MigrateToChatID on send, migrate DB, retry
DB migration updates in a single transaction (scoped by tenant + channel):
- paired_devices: sender_id, chat_id
- sessions: session_key, user_id
- channel_contacts: sender_id
- channel_pending_messages: history_key
Also invalidates in-memory caches (approvedGroups, pairingReplySent,
groupHistory) and handles media sends via migration retry in Send().
* feat(providers): add OpenRouter identification headers (#704)
Add HTTP-Referer and X-Title headers to OpenRouter API requests
for rankings and analytics visibility on openrouter.ai.
---------
Co-authored-by: viettranx <viettranx@gmail.com>
* fix(ci): skip CI condition in semantic-release for main branch
go-semantic-release auto-detects the default branch from GitHub API
(which is dev), but releases are cut from main. The CI condition
rejects runs on non-default branches. Use --no-ci to bypass this
check since the workflow already gates on push to main.
* docs: document CI/CD pipelines, release flow, and v2.66.0 changelog
- CLAUDE.md: add CI/CD & Releases section with workflow table, tag
patterns, Docker variants, beta/desktop release commands
- CONTRIBUTING.md: expand Releases section with standard (auto),
beta (manual tag), and desktop release workflows
- docs/17-changelog.md: add v2.66.0 entry covering IDOR fix, BytePlus
provider, per-agent grants, beta pipeline, and CI fixes
* fix(telegram): handle group-to-supergroup migration seamlessly
When a Telegram group upgrades to a supergroup, the chat ID changes and
all existing references become stale. This caused send failures (400),
orphaned sessions, and required manual re-pairing.
Add dual-path migration handling:
- Proactive: intercept inbound MigrateToChatID before isServiceMessage
- Reactive: detect 400 + MigrateToChatID on send, migrate DB, retry
DB migration updates in a single transaction (scoped by tenant + channel):
- paired_devices: sender_id, chat_id
- sessions: session_key, user_id
- channel_contacts: sender_id
- channel_pending_messages: history_key
Also invalidates in-memory caches (approvedGroups, pairingReplySent,
groupHistory) and handles media sends via migration retry in Send().
When a Telegram group upgrades to a supergroup, the chat ID changes and
all existing references become stale. This caused send failures (400),
orphaned sessions, and required manual re-pairing.
Add dual-path migration handling:
- Proactive: intercept inbound MigrateToChatID before isServiceMessage
- Reactive: detect 400 + MigrateToChatID on send, migrate DB, retry
DB migration updates in a single transaction (scoped by tenant + channel):
- paired_devices: sender_id, chat_id
- sessions: session_key, user_id
- channel_contacts: sender_id
- channel_pending_messages: history_key
Also invalidates in-memory caches (approvedGroups, pairingReplySent,
groupHistory) and handles media sends via migration retry in Send().
go-semantic-release auto-detects the default branch from GitHub API
(which is dev), but releases are cut from main. The CI condition
rejects runs on non-default branches. Use --no-ci to bypass this
check since the workflow already gates on push to main.
go-semantic-release auto-detects the default branch from GitHub API
(which is dev), but releases are cut from main. The CI condition
rejects runs on non-default branches. Use --no-ci to bypass this
check since the workflow already gates on push to main.
Scheduler tests hung on CI when t.Fatal fired before close(blockCh) —
defer sched.Stop() called wg.Wait() on goroutines still blocked on
blockCh. Fix: defer close(blockCh) after defer Stop() (LIFO order
ensures blockCh closes first).
Add release-beta.yaml for dev branch beta releases:
- Triggers on v*-beta* and v*-rc* tags
- Builds Linux binaries + Docker images (latest, full variants)
- Creates GitHub prerelease
- Docker tags: version-specific + "beta" rolling tag
- No overlap with release.yaml (branch-triggered, clean semver)
Update release-desktop.yaml:
- Auto-detect prerelease from tag name (beta/rc → prerelease: true)
release.yaml already builds and pushes Docker images (4 backend variants
+ web) to GHCR and Docker Hub when semantic-release creates a new
version. docker-publish.yaml triggered on v*.*.* tags, causing
duplicate Docker builds on every release.
The test closed blockCh then immediately exited, racing defer sched.Stop()
against scheduleNext(). Under -race on CI, Stop() could call wg.Wait()
while a goroutine was still being submitted, causing a 600s timeout hang.
Fix: wait for queued runs to complete before allowing Stop() to run.
Closes#677
When starting a new chat, loadHistory() races with chat.send — if
history is fetched before the server persists the user message, it
returns empty results and overwrites the optimistic message.
Skip loadHistory() on the "" → newKey session transition since the
optimistic user message is already in state. All other session
transitions (switch, refresh) continue loading history normally.
Closes#694
When a pool member provider is deleted/disabled, its name persists in
the owner's extra_provider_names config. Validation already skips stale
refs, but the names accumulated as garbage in the DB.
Now stripStalePoolMembers() runs after validation passes, removing
member names that don't exist in the active provider set before the
settings are written to the database.
* fix(secure-cli): resolve ambiguous column in LookupByBinary JOIN query (#641)
LookupByBinary uses LEFT JOIN with secure_cli_user_credentials but
SELECT columns lacked table alias prefix, causing PostgreSQL error:
"column reference 'id' is ambiguous (SQLSTATE 42702)"
This silently broke ALL credentialed CLI exec — commands fell through
to regular shell exec without injected env vars.
Fix: use b.-prefixed column names for JOIN queries.
Also add diagnostic logging to lookupCredentialedBinary for future debugging.
* fix(agent): defer warning messages after parallel tool results (#644)
When parallel tool calls trigger loop detection warnings, the warning
messages (role="user") were inserted between tool result messages
(role="tool"). This breaks the Anthropic API when routed through
OpenAI-compatible proxies (e.g. LiteLLM): the proxy groups consecutive
tool messages into a single user message with tool_result blocks, but
an intervening user warning splits the group, causing orphaned
tool_results and HTTP 400 "tool_use ids without tool_result blocks".
Fix: accumulate warning messages during parallel result processing and
append them after all tool results, preserving the consecutive grouping.
Closes#642
* fix(docker): resolve @rollup/rollup-linux-arm64-musl missing on Alpine (#647)
Added ui/web/.npmrc with supportedArchitectures for musl+glibc/arm64+x64.
Updated Dockerfile to use --no-frozen-lockfile so pnpm fetches native rollup
binding compatible with Alpine's musl libc. Lockfile still pinned by copy order.
* docs(README): add history stars (#462)
* fix(pool): skip stale pool member references during validation
Unknown pool member references (deleted or disabled providers) now
continue instead of returning an error. Prevents stale data from
blocking provider saves.
Closes#670
* fix(ui): redesign pool member selector and add managed-by banner
Pool member selector:
- Replace invisible outline button with custom element using dashed
primary border, + icon badge, and "Click to add" hint text
- Visible in both light and dark themes; hover transitions to solid
border with shadow; active press scales down for tactile feedback
Managed-by banner:
- Show "Pool Defaults" section on pool members with info banner
explaining which provider owns the pool, plus a Link navigation
- Previously this section was completely hidden with no explanation
i18n: add poolManagedByDescription and clickToAdd keys (en/vi/zh)
* docs: add before/after UI evidence for PR #671
Annotated screenshots with red callout borders marking review areas.
Self-contained HTML comparison report with dark/light theme toggle.
* feat(ui): add pool discovery badges and setup wizard
Replace verbose info banner with per-card "Pool available" badge on
unpooled ChatGPT OAuth providers. Clicking the badge opens a new
pool setup wizard dialog where users select owner, members, and
strategy in one step.
* docs: update UI evidence with pool discovery before/after
* fix(ui): hide pool members from provider selector in agent forms
Pool member providers are managed via the pool owner's routing config.
Showing them as standalone options in the agent Provider dropdown is
confusing — users may select a member directly instead of the owner,
bypassing pool routing entirely.
Filter out providers that exist in ownerByMember from the enabled
providers list in ProviderModelSelect.
* fix(ui): hide pool members from provider selector and add Pool badge
Pool member providers are filtered out of the agent Provider dropdown
in both the Create Agent dialog and the shared ProviderModelSelect
component. Pool owners display a "Pool" badge so users know the
provider routes to multiple accounts automatically.
* docs: add provider selector before/after evidence
* fix: revert stale merge in secure_cli.go and fix hardcoded i18n strings
- Revert secureCLISelectColsAliased: b.agent_id → b.is_global
(agent_id was dropped in migration 36, stale merge conflict artifact)
- Replace hardcoded "Pool" badge text with t("providers:list.poolBadge")
in provider-model-select and agent-identity-and-model-fields
- Replace hardcoded "Disabled" with t("common:disabled") in pool wizard
- Add list.poolBadge key to en/vi/zh locale files
---------
Co-authored-by: Viet Tran <viettranx@gmail.com>
Co-authored-by: Plateau Nguyen <nguyennlt.ncc@gmail.com>
Co-authored-by: DNT <ducconit@gmail.com>
- Refresh provider list when AgentCreateDialog, HeartbeatConfigDialog,
and AgentAdvancedDialog open (fixes stale cache from 60s staleTime)
- Remove verify-blocking from agent creation flow; verify remains as
optional manual check but no longer gates the Create button
- Aligns with existing pattern in TeamCreateDialog
chat.send accepted any sessionKey without verifying the caller owns the
session. A non-admin user could write messages into — or inject into an
active run of — another user's session by supplying their sessionKey.
Add the same canSeeAll + UserID guard used by chat.history/inject/abort.
New sessions (Get returns nil) are allowed through so first-message
creation is not blocked.
Closes the last IDOR vector identified during #676 review.
chat.history, chat.inject, chat.abort, and chat.session.status accepted
any sessionKey without verifying the caller owns the session. A non-admin
user could read, write, or disrupt another user's conversations by
supplying their sessionKey.
Apply the same requireSessionOwner() guard already used by sessions.*
methods: canSeeAll() bypass for admin/owner, sess.UserID match for
regular users. Extracted shared helper to access.go to reduce duplication.
Also fixes: handleSessionStatus i18n compliance (was hardcoded English),
and closes runId-only abort gap (non-admin must provide sessionKey).
The Zustand persist migration excluded tenantSelected from the
partialize whitelist, causing it to reset to false on page reload.
This created a redirect loop: require-auth saw the intermediate
state (connected + tenants loaded + tenantSelected=false) and
redirected to /select-tenant before the WS auto-select callback
could fire.
Restore the original localStorage-derived initialization so
tenantSelected is true immediately on rehydration when a tenant
scope was previously saved.
Closes#692
Add BytePlus ModelArk as a new OpenAI-compatible provider for Seed 2.0
models (chat, vision). Two provider types: standard API and Coding Plan
(separate base URLs, same auth).
Integrate Seedream image generation (sync API) and Seedance video
generation (async polling) into the builtin media tool chain, following
the established DashScope/Gemini patterns.
- Add WithAuthPrefix option to OpenAIProvider for future non-standard auth
- Add ProviderBytePlus/ProviderBytePlusCoding store constants and config
- Register provider from config.json and llm_providers DB table
- Add BytePlus to media chain routing, priority lists, and dispatch
- Create create_image_byteplus.go (Seedream, sync response)
- Create create_video_byteplus.go (Seedance, async poll with 5min timeout)
- Add BytePlus to web and desktop UI provider type dropdowns
- Update provider docs with BytePlus entries
Closes#686
Port PR #685 fixes for HTTP 400 on Together AI and strict OpenAI-compat hosts,
with additional improvements:
- Gate reasoning_content on assistant history to allowlisted models only
(OpenAI o-series/GPT-5, DeepSeek, Kimi) — prevents HTTP 400 on Together/Qwen
- Gate reasoning_effort to models that support it (OpenAI reasoning family)
- Skip stream_options for Together endpoints (causes HTTP 400)
- Scope DashScope enable_thinking/thinking_budget to DashScope providers only
- Reorder multimodal parts: text before images (Together/Qwen preferred order)
- Add redacted_thinking tag to sanitization patterns with early-exit guard fix
- Upgrade Together detection from URL-only to URL + providerType + name fallback
(mirrors dashScopePassthroughKeys pattern for reverse-proxy compatibility)
- Add comprehensive tests for all new behavior
Replace agent_id column on secure_cli_binaries with is_global flag
and new secure_cli_agent_grants table for per-agent access control
with optional deny_args, deny_verbose, timeout_seconds, tips overrides.
- Migration 000036: create grants table, migrate agent-specific rows,
dedup binaries, drop agent_id, add is_global
- Store layer: SecureCLIAgentGrantStore interface + PG implementation,
LookupByBinary with LEFT JOIN grant merge, ListForAgent
- HTTP API: CRUD endpoints at /v1/cli-credentials/{id}/agent-grants
- Agent loop: buildCredentialCLIContext uses ListForAgent for scoped
system prompt (agents only see authorized CLIs)
- Web UI: grants dialog with card list + inline form, is_global toggle
replaces agent dropdown, i18n for en/vi/zh
TraceContentPreview was rendering JSON through CodeBlock with oneDark theme,
causing black background spans in light mode. Use plain <pre> with JSON
pretty-print instead — matches the original trace detail rendering.
- H1: rename McpTestResult component import to McpTestResultDisplay (avoid type collision)
- H2: replace local slugify with shared lib/slug import in McpFormDialog
- H3: restore same-path no-op guard in storageService.moveFile
- H4: deduplicate StorageFileContent interface (use service as source of truth)
- M1: replace inline import() syntax with standard import in use-storage
Fix Mistral tool call ID formatting on the outbound OpenAI-compatible payload.
For provider `mistral` only, normalizes both `tool_calls[].id` and `tool`
message `tool_call_id` to a 9-character hex string (SHA-256 based) before
sending the request. Other providers keep the existing `truncateToolCallID`
behavior unchanged.
- Hash full ID via SHA-256 to avoid prefix-dependent collisions
- Detect Mistral via both provider name and providerType (DB-loaded support)
- Add collision uniqueness and DB provider detection tests
SSRF validation in validateProviderURL() blocked all localhost/loopback
addresses, preventing local providers like Ollama from being configured
with http://localhost:11434/v1. Introduce localProviderTypes map to skip
SSRF checks for inherently local provider types (ollama, claude_cli, acp).
Closes#673
ForceGraph2D was rendering at wrong default dimensions when the
KG page first loaded after selecting an agent. Root cause: early
return for empty state removed the container div from DOM, so
useLayoutEffect couldn't read dimensions on first mount.
Fix: always render container div (flex-1 gives it height from
parent), show empty state inside it. Use useLayoutEffect for
synchronous initial measurement + ResizeObserver for ongoing
changes. Gate ForceGraph2D render behind `ready` flag.
Also extracted memory-documents-table from memory-page (gap closure).
Create src/adapters/ with 3 adapter files:
- kg-graph.adapter.ts: KGEntity/KGRelation → GraphData (nodes, links,
degree computation, color mapping, entity limiter)
- chat-message.adapter.ts: moved from chat/hooks/chat-message-transformer.ts
- trace.adapter.ts: buildSpanTree extracted from trace-span-tree-node
Skipped team-task (already in board-utils) and provider-pool (<10 lines,
single call site).
Inline transforms in kg-graph-view.tsx and trace-detail-dialog.tsx
replaced with adapter calls. No behavior changes.