Commit Graph
1912 Commits
Author SHA1 Message Date
viettranx b967055560 feat(channels): shared WriterLabel helper with prompt-injection sanitization 2026-04-18 22:49:51 +07:00
viettranx 4bc7ca1320 fix(store): tenant-scope GetContactsBySenderIDs to prevent cross-tenant leak 2026-04-18 22:49:48 +07:00
viettranx 9333fee5b6 feat(agent): add affirmative writer hint + sender identity to group permissions prompt 2026-04-18 22:43:01 +07:00
viettranx 6e935b3314 fix(agent): fallback 'User <id>' in group writer prompt when metadata empty 2026-04-18 22:38:16 +07:00
viettranx d4a08c818f fix(agent): broaden NO_REPLY detection to cover decorative variants
Models occasionally emit NO_REPLY with trailing underscores, quotes,
markdown emphasis, or short explanations (e.g. `NO_REPLY_`,
`**NO_REPLY**`, `NO_REPLY: offline`). Previous word-boundary check
treated `_` as a word char so `NO_REPLY_` slipped through and was
delivered to end users.

Rewrite IsSilentReply to strip decorative wrappers, then prefix-match
case-insensitively. Token is only rejected when glued to another
word character (e.g. `NO_REPLYING`). Intentional divergence from
upstream #19537 — documented in the doc comment.
2026-04-18 21:46:26 +07:00
viettranx 59a8c25349 revert(http): remove admin backfill endpoint — lazy heal on /writers is enough 2026-04-18 21:43:19 +07:00
viettranx 78e2882335 feat(ui): unknown writer label fallback + i18n (en/vi/zh) 2026-04-18 21:35:54 +07:00
viettranx f1c1a466f0 feat(http): admin backfill endpoint for legacy writer metadata 2026-04-18 21:35:50 +07:00
viettranx d9c453cd49 feat(telegram): lazy self-heal writer metadata on /writers + fallback label 2026-04-18 21:35:48 +07:00
viettranx e66badb803 fix(permissions): auto-enrich file_writer metadata on grant from Web UI 2026-04-18 21:35:46 +07:00
viettranx cadca23cab feat(channels): add MemberResolver interface + Telegram getChatMember impl 2026-04-18 21:35:44 +07:00
viettranx 9f68f3ee0d feat(pancake): auto-react allow/deny scope filter + UI toggle
Extends #919 with per-channel scope control for Facebook auto-react.

- AutoReactOptions pointer struct (allow/deny post & user IDs)
- filterAutoReact + containsString helpers; deny overrides allow
- UI surfaces features.auto_react toggle + 4 tags fields
- Nil options = react-all (backward compatible)
2026-04-18 17:55:11 +07:00
viettranx 2abb317e02 docs: update tools system and changelog for web_search refactor
Update docs/03-tools-system.md to document:
- Per-tenant web_search provider chain resolution
- Tenant settings schema (provider_order, per-provider disable)
- Cache TTL and event-driven invalidation
- Config secrets lookup for API keys

Update docs/17-changelog.md with web_search tenant-scoping feature.
2026-04-18 17:47:39 +07:00
viettranx 18e48cd0fc test(integration): add web_search tenant isolation and migration hook tests
Add comprehensive integration tests:
- web_search_tenant_isolation_test: verify per-tenant provider chains, cache
  hit/miss behavior, event-driven invalidation
- web_search_migrate_hook_test: verify migration of inline keys from
  config.json5 and builtin_tool_tenant_configs.settings to config_secrets

Tests cover multi-tenant scenarios, cache expiry, concurrent execution.
2026-04-18 17:47:36 +07:00
viettranx 96b75f0413 refactor(ui): remove web_search config section
Remove ToolsWebSection from config page and clean up i18n keys for web
search provider configuration (duckduckgo, exa, tavily, brave). Web search
now configured entirely via builtin_tool_tenant_configs REST API.

Delete tools-web-section.tsx component and update locale catalogs (en/vi/zh).
2026-04-18 17:47:33 +07:00
viettranx ff1ab33fbc refactor(gateway): wire web_search with tenant-scoped stores
Move web_search tool registration from gateway_setup (where it received
hardcoded config) into wireExtraTools with access to ConfigSecretsStore
and MessageBus for runtime per-tenant resolution.

Update gateway_setup.go to remove web_search-specific config loading.
2026-04-18 17:47:29 +07:00
viettranx 1af7c2bb2a refactor(config): remove web_search from static config
Remove Tools.Web and related WebToolsConfig, DuckDuckGoConfig from config
struct and Default(). Web search providers now resolved dynamically from
config_secrets (tenant-scoped) and builtin_tool_tenant_configs instead of
hardcoded config.json5.

Update config_load_test to match removed config paths. Separate config_secrets
and config_channels loading to clarify schema evolution.
2026-04-18 17:47:25 +07:00
viettranx 31a6d1a60c refactor(tools): convert web_search to tenant-scoped provider resolution
Refactor WebSearchTool constructor to accept ConfigSecretsStore and
MessageBus instead of hardcoded WebSearchConfig. Implement runtime
per-tenant provider chain resolution via tenantChainCache.

Constructor now:
- Subscribes to cache invalidation events on config_secrets changes
- Returns nil-safe tool instance (no early return on empty providers)
- Passes secrets store and bus for dynamic resolution per Execute() call

Add resolve unit tests; delete legacy config_test.go (functionality
subsumed by cache tests and integration tests).
2026-04-18 17:47:21 +07:00
viettranx d7fae64974 feat(tools): add per-tenant web_search provider chain cache
Implement tenantChainCache with 60-second TTL to amortize DB reads for
per-tenant provider chain resolution. Cache is TTL-based with RWMutex
protection and event-driven invalidation on config_secrets changes.

Chain resolution rules:
- Parse tenant settings from builtin_tool_tenant_configs
- Apply NormalizeWebSearchProviderOrder for user-specified order
- Append DDG last (force-enabled, no API key required)
- Skip disabled providers or providers without API keys

Include comprehensive unit tests for cache hit/miss, expiry, and
concurrent access patterns.
2026-04-18 17:47:17 +07:00
viettranx 7a8f4c6446 feat(upgrade): add data migration hook for web_search inline API keys
Implement 055_web_search_legacy_keys_to_config_secrets migration hook that:
- Extracts inline API keys from config.json5 (tools.web.{exa,tavily,brave})
- Migrates inline api_key fields from builtin_tool_tenant_configs settings JSON
- Inserts all keys into config_secrets under appropriate tenant scope
- Strips inline fields from settings blobs to prevent re-migration

Hook is idempotent, tracked in data_migrations table, PostgreSQL-only.
2026-04-18 17:47:13 +07:00
viettranx afee8b4682 test(tools,agent): cover cross-target guard scenarios
Guard unit coverage:
- MessageTargetEnforced helper table (10 session shapes)
- TestMessageToolCrossTargetGuard — 9 scenarios: DM/group same-target
  pass, cross-target block, forward opt-in pass with notice; cron,
  heartbeat, subagent, team FREE
- TestMessageToolCrossTargetGuard_TraceReplay — anchors the 019d9fcf
  production trace against regression
- NoticeFallbackSender — msgBus-less deployment still gets the audit
  breadcrumb via t.sender
- NoNoticeOnSendFailure — failed forward does not post a phantom notice

System prompt coverage:
- <current_reply_target> emitted for direct + group, omitted when
  ChatID is empty

Pre-existing TestSelfSendGuard/text_to_different_chat_allowed updated
to pass forward=true — the test now correctly isolates the self-send
guard from the new cross-target guard.
2026-04-18 17:45:28 +07:00
viettranx 3715d260a5 feat(agent): inject <current_reply_target> into system prompt
Give the LLM an unambiguous reference for the currently bound chat so
it can compare against any target it plans to pass. Block lists channel,
chat_id and kind (direct/group) with a one-line policy: omit target to
reply here, only set target when forwarding per explicit user request.

Threads ChatID through SystemPromptConfig, IdentityData,
BridgePromptBuilder, and buildMessages so the pipeline populates the
block from pipeline.RunInput.ChatID.
2026-04-18 17:45:14 +07:00
viettranx 4658014ac8 feat(tools): fail-closed cross-target guard in message tool
Production trace 019d9fcf: agent in DM session called
message(target="-1003787954683:topic:1") and leaked a user's image to
the wrong Telegram group topic. No guardrail caught it.

Add explicit forward opt-in: in ENFORCED sessions, cross-target sends
require forward=true + non-empty forward_reason. Otherwise blocked with
a self-explanatory error that tells the LLM how to retry correctly.

Successful forwards emit slog.Warn("message.cross_target_forward") and
post a user-facing notice back to the origin chat via noticeOnSuccess
wrapper — notice only fires when the send itself succeeds, so failed
forwards no longer announce phantom deliveries.

Schema gains forward and forward_reason fields; description explains
when to set target vs omit.
2026-04-18 17:45:08 +07:00
viettranx 3fa8a02d4b feat(tools): add MessageTargetEnforced session guard helper
Classifies session kinds into FREE (cron, heartbeat, subagent, team) vs
ENFORCED (DM, group, ws, default, unknown, empty). FREE kinds compose
targets per job spec; everything else must match the ctx-bound origin.

postCrossTargetNotice posts the audit breadcrumb back to the origin
chat when an explicit forward succeeds: msgBus preferred, falls back to
direct sender, else slog.Warn so the skip is traceable.
2026-04-18 17:44:57 +07:00
viettranx 5b2d856896 feat(i18n): add message cross-target forwarded key
User-facing notice posted back to origin chat when the agent forwards a
message to a different target per explicit user request. Translations
added for en/vi/zh.
2026-04-18 17:44:50 +07:00
viettranx feca0a25e4 fix(docker): swap browser sidecar to chromedp/headless-shell
zenika/alpine-chrome is stuck at v124 — upstream stopped publishing
newer tags — and v124 with default headless mode doesn't support
--load-extension, so users couldn't load Chrome extensions during
automation.

Switch to chromedp/headless-shell:latest (Chrome 147+ at time of
commit, auto-tracks Chrome for Testing) and add --headless=new so
extension loading works. Rod connects to the same raw-CDP
ws://chrome:9222 endpoint — no client code change.

The image's entrypoint already configures the debugging port and
fronts it with socat, so our explicit --remote-debugging-*
flags have to be removed (otherwise Chrome fails with
"bind() failed: Address already in use").

Healthcheck switched from wget (absent in the minimal image) to a
bash /dev/tcp probe (bash is present and sufficient for a liveness
check on port 9222).

Verified: image pulls, Chrome starts, /json/version returns, health
probe succeeds.

Closes #722
2026-04-18 16:55:08 +07:00
viettranx 935b58b7a3 fix(migrate): RFC 8089-compliant file:// URL for Windows paths
golang-migrate's file source driver parsed "file://F:\\project\\..."
with "F" as host and ":\\..." as port → "invalid port" error, so
upgrade/migrate commands crashed on Windows before applying any SQL.

Format the source URL per RFC 8089: forward-slash separators and a
leading "/" before the drive letter → "file:///F:/project/...".
POSIX paths pass through unchanged (leading "/" already present).

Shape-driven, not OS-driven: no runtime.GOOS branch needed, which
also lets the unit test cover both shapes on a single runner.

Closes #872
2026-04-18 16:55:08 +07:00
viettranx 4988f57a9c fix(agent): revert path-normalizing dedup and symlink-resolved storage
Two pieces of the review-hardening turned out to be risky in production:

1. deduplicateMedia used filepath.Abs on each MediaResult.Path to
   normalize the dedup key. filepath.Abs depends on the process CWD,
   which varies across deployment environments. In containers with a
   non-standard CWD, relative paths from parseMediaResult were being
   rewritten into keys that could collide with or differ from
   absolute paths from tools — leading to legitimate media entries
   being silently eaten.

2. extractMediaFromContent overwrote the stored path with the
   EvalSymlinks-resolved path. When the workspace is a bind-mount or
   contains ancestor symlinks, the stored path diverged from what the
   tool originally wrote and queued into rs.mediaResults, so dedup
   against tool-origin entries failed and the same file could be
   delivered twice (or fail altogether if the resolved path wasn't
   reachable from the downstream reader's FS view).

Revert both to exact-string behavior. Keep EvalSymlinks for the
containment check only — the symlink-escape P0 remains closed, but
the stored path now matches what the tool wrote.
2026-04-18 16:19:23 +07:00
viettranx e03a11031f fix(team): propagate origin_sender_id for deferred task dispatches
When tasks are dispatched via ticker (deferred/unblocked), the context
has no real sender — only synthetic "ticker:system". This caused
workers to fail file writes in group chats with "permission denied:
system context cannot write files".

- team_tool_dispatch.go: fallback to task.Metadata["origin_sender_id"]
  when context sender is empty or synthetic
- task_ticker.go: propagate origin_sender_id and origin_role from task
  metadata when building ticker dispatch message

Fixes #915 deferred dispatch flow.
2026-04-18 15:53:40 +07:00
viettranx bfedf8e1d4 fix(telegram): bump timeout for media uploads to 3 min
Photo/video/audio/document uploads were hitting "context deadline
exceeded" mid-flight on slow or mobile networks. Two layers capped
the budget at 60s:

  - sendOverallTimeout (wraps the whole retry sequence)
  - http.Client.Timeout (caps a single HTTP request)

Both are fine for text sends but too tight for multi-MB uploads
(sketchnote images, PDFs, long voice clips).

Split the retry budget by method: text sends keep 60s, media sends
(sendPhoto/Video/Audio/Voice/Document) get a new sendMediaOverallTimeout
of 3 min. Bump the shared http.Client.Timeout to 3 min so a single
upload attempt can actually consume that budget — getUpdates long-poll
is 25s, so the extra headroom doesn't affect polling.

Closes #628
2026-04-18 15:53:25 +07:00
viettranx 3b445d6856 fix(agent): close symlink escape + path-alias dedup + TTS mime type
Review of the medium-batch landed this session surfaced three issues:

P0 — symlink-ancestor escape in extractMediaFromContent. The lexical
filepath.Rel check passed "<ws>/shared/secret" when "shared" was a
symlink pointing outside the workspace. EvalSymlinks the resolved path
(and the workspace root, since macOS uses /var → /private/var) before
the Rel containment check. Leaf symlinks still rejected by Lstat.

P1 — duplicate attachments from path aliases. parseMediaResult stores
raw tool output ("./tts/x.mp3"); extractMediaFromContent stores Abs+
Clean ("<ws>/tts/x.mp3"). deduplicateMedia's exact-string map let both
through. Normalize the dedup key via filepath.Abs + Clean so the
sources collapse.

P1 — TTS Result.Media used the non-standard "audio/mp3" via
"audio/"+Extension. SynthResult.MimeType is already populated by every
provider with the RFC-3003 "audio/mpeg" (or "audio/ogg" for opus).
Prefer it; keep the extension concat as empty-string fallback.

Tests: add symlink-leaf-rejected and ancestor-symlink-escape cases to
lock the P0 behavior.
2026-04-18 15:40:24 +07:00
viettranx 2a61365353 fix(docker): set 0755 on web static assets so nginx worker can read
pnpm build under a restrictive umask produces 0600 files; when copied
into nginx:alpine, the non-root nginx worker hits EACCES and serves
403 for every static asset. Force mode via COPY --chmod=0755 (Docker
BuildKit applies uniformly — directories need the exec bit for
traversal, static files don't care about the extra bit).

Closes #761
2026-04-18 15:34:46 +07:00
viettranx 8fb0300914 fix(tts): set Result.Media so provider bridges forward attachments
TTS relied solely on ForLLM "MEDIA:<path>" parsing. claude_cli's MCP
bridge reshapes ForLLM before the agent sees it, so rs.mediaResults
stayed empty and mp3s never attached to outbound messages.

Populate Result.Media alongside the legacy ForLLM prefix (matching
create_audio.go). The agent loop prefers explicit Result.Media over
the ForLLM fallback.

Closes #806
2026-04-18 15:34:46 +07:00
viettranx 3ed3fd9832 fix(agent): extract MEDIA: paths from LLM response before sanitize
When a tool returns its artifact via ForLLM "MEDIA:<path>" and the LLM
echoes that path in its final response (PDF from exec/weasyprint, TTS
bridged through claude_cli MCP, etc.), sanitize stripped the token
without populating rs.mediaResults — attachments never reached the
channel.

Add extractMediaFromContent() and call it right before
SanitizeAssistantContent. Tight security: workspace must be set,
relative paths resolve against workspace, absolute paths must stay
inside workspace (blocks ../ escapes and sibling-tenant access),
file must exist and be a regular file. Dedup prevents duplicates
when a tool already populated Result.Media.

Closes #882
2026-04-18 15:34:46 +07:00
viettranx 226aa8ed11 fix(make): use --pull always in up, split up-build for local source
'make up' now pulls the latest published image instead of rebuilding
from local context, which was overwriting freshly pulled images with
stale cache. New 'make up-build' target preserves the build-from-source
flow for developers iterating on the backend.

Closes #934
2026-04-18 15:09:46 +07:00
viettranx c06d15be5f fix(ui): canonicalize timezone dropdown to drop deprecated aliases
Resolve each IANA name via Intl.DateTimeFormat().resolvedOptions() so
aliases like Asia/Saigon collapse into their canonical form
(Asia/Ho_Chi_Minh). Prevents cron/heartbeat pickers from saving a
deprecated alias that diverges from the Go-side canonical name.

Closes #760
2026-04-18 15:09:46 +07:00
viettranx 001d9e23e3 feat(tools): append scope-filtered outlinks footer to vault_read
List up to 20 wikilink/reference outlinks as "## Links" after the doc
body, deduped by target, scope-filtered via existing allowed() matrix.
Non-fatal — store errors log and skip the footer.
2026-04-18 15:02:34 +07:00
viettranx e033b357cc fix(tools): route read_audio transcription models to /audio/transcriptions (#858)
Transcription-only models (whisper-1, gpt-4o-transcribe,
gpt-4o-mini-transcribe) were sent to /chat/completions with an
input_audio content part, which OpenAI rejects with "not a chat model".
Add isTranscriptionModel detection and a multipart/form-data call to
/v1/audio/transcriptions for those models; chat-audio models
(gpt-4o-audio-preview) continue to use /chat/completions.

Also fix providerTypeFromName to treat "openai-*" prefixed providers
as "openai" type so a dedicated "openai-audio" provider reaches the
OpenAI-specific path instead of falling through to the generic chat
fallback.
2026-04-18 14:54:51 +07:00
viettranx 497849f746 fix(teams): propagate local_key in team task completion/fail broadcasts (#877)
Forum topic routing (Telegram supergroup threads) relies on local_key
carried through team task events to pick the correct thread for
notifications. Four broadcast sites dropped local_key by relying on
WithContextInfo(ctx) alone — but the member agent's isolated context
does not carry the originating local_key.

Read task.Metadata[TaskMetaLocalKey] explicitly and pass via
WithLocalKey() to match the already-correct pattern in
team_tool_dispatch.go and team_tasks_blocker.go.

Covered sites:
  - team_tasks_lifecycle.executeComplete → EventTeamTaskCompleted
  - gateway_consumer_post_turn agent-error → EventTeamTaskFailed
  - gateway_consumer_post_turn loop-kill → EventTeamTaskFailed
  - gateway_consumer_post_turn auto-complete → EventTeamTaskCompleted
2026-04-18 14:53:42 +07:00
viettranx 564997cfae fix(consolidation): pass parsed agentUUID to ExistsBySourceID (#809, #824)
Line 52 parses agent_id into agentUUID but line 59 passed the raw
event.AgentID string to ExistsBySourceID. PR #826 fixed the emission
side (loop_finalize.go) so event.AgentID is now always a valid UUID,
but the consumption side kept the inconsistent parse-then-discard
pattern.

Use agentUUID.String() to keep the parse-once-use-parsed invariant,
matching the tenantUUID pattern two lines above. Fixes the
"invalid input syntax for type uuid" error reported in #809 and the
downstream "..." Telegram symptom in #824 where episodic failure
cascaded into empty agent replies.
2026-04-18 14:52:18 +07:00
viettranx 91d79ce120 fix(providers): capture claude-cli stream errors and surface real message (#884)
claude-cli emits API errors (rate limits, usage limits) via stream-json
stdout result events, not stderr. Previous code only checked
ev.Subtype == "error" and used an empty stderr buffer in the final
wrap, so users saw a generic "something went wrong" instead of the
real cause.

- Add IsError and Error fields to cliStreamEvent to parse both signals
- Capture stream error message when FinishReason flips to "error"
- Substitute captured message when stderr is empty in cmd.Wait path
- Extend gateway rate-limit classifier to recognize "usage limit"
2026-04-18 14:42:17 +07:00
viettranx 96cc940f2b fix(providers): whitelist effort env vars in filterCLIEnv + wire per-agent --effort (#886)
filterCLIEnv stripped all CLAUDE_* env vars except OAUTH_TOKEN,
preventing CLAUDE_CODE_EFFORT_LEVEL and CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING
from reaching the subprocess. Switch to an explicit allowlist map that
preserves auth + behavioral tuning vars.

Add effort parameter to buildArgs and plumb OptThinkingLevel from
req.Options through Chat/ChatStream so per-agent reasoning level maps
to --effort flag. When an explicit flag is set, strip the env var to
avoid precedence ambiguity. Validate effort with isAlphaOnly to reject
malformed input.
2026-04-18 14:42:10 +07:00
viettranx 339b13e8f0 fix(providers): return error on Codex response.failed instead of silent success (#888)
response.failed was lumped with response.completed/incomplete, causing
silent empty responses with no retry. Split into its own case that
returns a descriptive error from processSSEEvent, propagated up to
ChatStream caller for proper error surface and retry logic.

Added codexErrorDetail to codexAPIResponse to parse the error payload.
2026-04-18 14:42:02 +07:00
Raihan 222ccfbc5b fix(skills): avoid false invalid zip uploads (#893) 2026-04-18 14:23:20 +07:00
viettranx a5520044b7 feat(agent): wire vault_read into system-prompt tool summaries (#948)
Part of the vault_read feature. Amend coreToolSummaries so the LLM sees:
- read_file: workspace-scoped, redirect to vault_read for vault discovery
- vault_search: explicit chaining hint to pass doc_id to vault_read
- vault_read: new summary describing shared/personal/team access
2026-04-18 12:27:28 +07:00
viettranx 2338ca53d5 feat(tools): add vault_read tool for shared/personal/team vault docs (#948)
Agents could not read vault docs registered at the tenant workspace root
because read_file enforces a per-agent workspace boundary. vault_read
fetches content by doc_id (from vault_search) with scope-based access
checks (shared/personal/team), a 3-layer text-only gate (DocType +
extension blocklist + UTF-8 sniff), 500KB default cap / 1MB ceiling,
and symlink/hardlink guards shared with read_file. Updated system-prompt
summaries and boundary-error hints so the LLM chains vault_search →
vault_read instead of retrying read_file.
2026-04-18 12:27:28 +07:00
040b0f1944 fix(acp): Gemini ACP protocol fixes and multi-session architecture (#901)
* fix(cli): add missing X-GoClaw-User-Id header to gateway client

* feat(acp): comprehensive integration with Gemini ACP protocol

- Support nested JSON-RPC notification structures
- Add robust streaming text collection and mapping
- Increase handshake timeout to 60s for heavy model initialization
- Fix WebSocket user_id authentication and schema v47 compatibility
- Allow Google/GCP environment variables for ACP subprocesses

* refactor(acp): multi-session architecture with session tracing and temp session cleanup

- One shared Gemini process, multiple ACP sessions per process (one per goclaw conversation)
- resolveSession: per-key mutex prevents TOCTOU race on concurrent session creation
- Respawn detection via proc pointer comparison; session/load fallback after crash
- sessionReaper: purges ACP sessions idle >30min; temp- sessions purged immediately on completion
- WithGoclawSession context propagation: goclaw session key appears alongside ACP sid in all logs
- dispatchUpdate: Gemini agent_message_chunk protocol mapping normalized here
- session_test.go: full coverage of multi-session API (Initialize, NewSession, Prompt, Cancel, dispatch)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(acp): align with ACP protocol standard (SDK v0.16.1)

- InitializeRequest: "capabilities" → "clientCapabilities" (standard field name)
- LoadSessionRequest: add mcpServers field (required by standard)
- mapStopReason: add standard stop reasons (max_tokens, cancelled)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(acp): address review findings from PR #901

- B1: Restore RequiredSchemaVersion to 55 (was incorrectly set to 47)
- S1: Move X-GoClaw-User-Id header outside token check
- S2: Add env gate (ACP_GEMINI_E2E) to Gemini E2E test
- S3: Fix tab indentation in jsonrpc.go writeMessage
- Build: Extract Pdeathsig to platform-specific files for cross-platform build

* fix(acp): address S4-S7 review findings

S4/S5: Send session/cancel before purging sessions locally
- purgeSession() now cancels ACP session before deleting map entry
- sessionReaper() sends cancel notification for idle sessions
- Updated comments to reflect actual behavior

S6: Tighten GOOGLE_/GCP_ env var filtering
- Add GOOGLE_, GCP_ back to sensitiveEnvPrefixes
- Add allowedEnvExact for safe vars: GOOGLE_API_KEY,
  GOOGLE_APPLICATION_CREDENTIALS, GOOGLE_CLOUD_PROJECT, GCP_PROJECT

S7: Propagate ACP errors to callers
- Chat/ChatStream now return err alongside ChatResponse
- Enables upstream retry logic and error metrics

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: viettranx <viettranx@gmail.com>
2026-04-18 12:17:23 +07:00
Thieu Nguyen 5ee39c74af fix(docker): exclude node deps and dist from build context (#949)
Root .dockerignore did not exclude local dependency folders, so
COPY ui/web/ . shipped ui/web's local install into the web-builder
stage. Stale artifacts (e.g. leftover rollup@4.57.1 from older vite@6)
then overrode the result of `pnpm install --frozen-lockfile`,
breaking the musl build on Alpine with:

  Cannot find module @rollup/rollup-linux-arm64-musl

Add explicit ignores for ui/web dependency and dist folders plus a
catch-all for any nested dep folder so Docker builds are
deterministic and match CI regardless of local dev state. Also
speeds up build context transfer.
2026-04-18 09:17:14 +07:00
Thieu Nguyen 406022e79f fix(security): close auth bypass + default-permit RBAC (issue #866) (#950)
Fixes a 3-vuln chain enabling unauthenticated/underprivileged access to
admin surfaces:

1. Auth bypass (router.go): invalid tokens silently fell through to
   anonymous role. Now reject on token presence with invalid/expired.
2. Default-permit RBAC (policy.go): unknown methods returned a writable
   role. Now fail-closed: unknown -> RoleNone.
3. Missing admin classifications: several sensitive methods weren't in
   any allowlist and slipped through default-permit. Now explicitly
   classified (admin/write/read).

Additional hardening:
- Remove isWriteMethod prefix fallbacks; use exact allowlist only to
  prevent silent scope creep when new methods are added.
- Classify Phase-3 methods (TTS, browser, zalo, whatsapp) into
  admin/write/read per sensitivity.
- Gate empty MethodRole in router dispatcher.

Tests:
- TestMethodRole_ApprovalsList_IsViewer: asserts exec.approval.list is
  viewer and approve/deny are operator.
- TestMethodRole_DriftCoverage_AllProtocolMethodsClassified: AST-parses
  pkg/protocol/methods.go at test time; asserts every Method* constant
  resolves to a non-None role. Permanent drift guard for future adds.
2026-04-18 08:08:29 +07:00
viettranx a29938ceed fix(agent): propagate SenderID in AgentEvent emitters
Expose the original acting sender ID in agent events so group-chat
telemetry and audit logs stay aligned with the real Telegram user
instead of only the group-scoped UserID.
2026-04-17 20:45:48 +07:00