Replace magic number 1000 with goclawGID constant across all chown
calls. Add slog.Warn on chown failures instead of silently ignoring
them, improving debuggability for permission issues in production.
* fix(pkg-helper): restore group ownership on apk-packages persist file
persistRemove() uses write-to-temp-then-rename which creates the new
file as root:root. The goclaw process (uid 1000, gid 1000) can't read
it, causing ListInstalledPackages to return nil for system packages.
After any package uninstall, the UI shows an empty package list.
Fix: chown listFile to root:goclaw (0:1000) after rename in
persistRemove and after initial creation in persistAdd.
* fix(ui): reset uninstall success icon to trash after timeout
After uninstalling a package, the success checkmark icon stayed
permanently instead of reverting to the delete (trash) icon.
Add setTimeout to reset actionStatus from "success" to "idle" after 2s,
matching the existing pattern used for error states.
1. Session detail: isDisplayable() filtered out assistant messages with
only tool_calls or thinking (no text content). Live chat renders these
via MergedToolGroup but session history silently dropped them.
2. Memory document dialog: load error showed "Failed to create document"
(wrong i18n key toast.failedCreate). Save error used the same wrong key.
Fixed to use toast.docLoadFailed and toast.docUpdateFailed respectively.
Added missing docLoadFailed key to en/vi/zh locale files.
3. Memory dialogs: all 5 dialogs missing aria-describedby={undefined} on
DialogContent, causing Radix UI console warnings about missing
Description for accessibility.
Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
- Replace chmod 644 temp file with install(1) -m 600 for atomic copy
with correct ownership — eliminates world-readable window in /tmp
- Move ${HOME}/.claude volume mount from base docker-compose.yml to
optional docker-compose.claude-cli.yml overlay — avoids breaking
deployments where $HOME is unset (CI/CD, systemd, Swarm)
Re-add character-by-character alias validation that was present in the
old tenantClauseNAlias but missing from the new WhereClauseAlias.
Defense-in-depth against SQL injection via table alias parameter.
All callers migrated to scopeClause/scopeClauseAlias in previous commit.
Remove the old functions. tenantIDForInsert and requireTenantID retained
(still have callers).
Migrate 72 calls across 16 files from tenantClauseN/tenantClauseNAlias
to scopeClause/scopeClauseAlias. Old functions retained in helpers.go
as definitions only (no callers). All store queries now use the
scope-based helpers, ready for project-level scoping when needed.
Introduce store.QueryScope struct that generates WHERE clauses for
tenant + optional project scoping. Add scopeClause/scopeClauseAlias
wrappers in pg/helpers.go as drop-in replacements for tenantClauseN.
Old tenant-only helpers remain for existing callers (76 occurrences).
Migrate providers.go (4 calls) as proof-of-concept. Future callers
should use scopeClause for project-scope readiness.
Move sanitization, media dedup, session flush, bootstrap cleanup, and
RunResult construction from runLoop() into dedicated finalizeRun() method.
Reduces loop.go from 1123 to 956 LOC (-167). No behavior change.
Fix mechanical find-replace that leaked rs.iteration into string literals,
slog keys, and event payload map keys. Restores "iteration" as the
user-facing/frontend-facing key name.
Backend:
- PUT /v1/storage/move?from=&to= — admin-only, atomic os.Rename
with protected dir, path traversal, symlink escape, and overwrite
prevention checks.
- PUT /v1/teams/{teamId}/workspace/move — same security with team
membership check and EventWorkspaceFileChanged broadcast.
Frontend:
- Files and folders are draggable in the file tree (HTML5 drag API).
- Folders are drop targets with visual ring highlight on dragover.
- Root-level drop zone for moving items to root directory.
- Guards: can't drop into self, can't drop folder into descendant,
can't drop into protected dirs (server-side enforced).
- Wired in both Storage page and Team Workspace dialog.
DRY: deduplicate tool result processing between single-tool and parallel-tool
paths. New processToolResult() and checkReadOnlyStreak() methods on *Loop
handle loop detection, event emission, media collection, deliverables, and
spawn tracking. Reduces loop.go from 1324 to 1123 LOC (-201). No behavior change.
Consolidate ~20 local variables from runLoop() into a single runState
struct. This enables future extraction of loop sub-operations (tool
execution, media processing, streaming) as methods on *runState without
passing individual variables. No behavior change.
Fields moved: loopDetector, totalUsage, iteration, totalToolCalls,
finalContent, finalThinking, asyncToolCalls, bootstrapWriteDetected,
mediaResults, deliverables, pendingMsgs, blockReplies, lastBlockReply,
checkpointFlushedMsgs, midLoopCompacted, teamTaskCreates,
teamTaskSpawns, skillNudge70Sent, skillNudge90Sent, skillPostscriptSent
Move agent/tenant/user/workspace context enrichment, input guard, and
message truncation from runLoop() into dedicated injectContext() method.
Reduces loop.go from 1553 to 1346 LOC. No behavior change.
- Replace 16x bare db.Query/db.Exec with context-aware variants to prevent
orphan DB operations when parent context is cancelled
- Thread baseCtx through PGCronStore lifecycle (Start/Stop) for all internal
scheduler, executor, and job cache operations
- Add ctx parameter to StoreMissingDeps and ListSystemSkillDirs, update all callers
- Add sync.WaitGroup tracking to fire-and-forget goroutines in gateway consumer
handlers (subagent announce, teammate message) with safego.Recover panic protection
- Add safego.Recover to 3 Slack channel goroutines to prevent WaitGroup hang on panic
When running in Docker, the Claude CLI provider setup showed `claude auth login`
which doesn't work inside a container. This change:
- Mounts host ~/.claude as read-only into the container
- Entrypoint syncs credentials to a writable volume (respects cap_drop: ALL)
- Backend detects Docker via /.dockerenv and returns `in_docker` in auth-status API
- UI shows `docker compose exec goclaw claude auth login` for Docker deployments
Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
- Increase maxFileSizeBytes from 10MB to 50MB across backend and frontend
- Add media/ and tenants/ to protectedDirs — prevent uploads into
system-managed directories (only teams/ and custom dirs are writable)
- Update i18n strings (en/vi/zh) to reflect 50MB limit
Upload was always going to root because activeFolder derived from
activePath which only tracks file selections (folders expand instead
of selecting). Now capture the folder at Upload button click time
and show the target path in the dialog description.
Allow admins/operators to upload files via the web UI so agents can
read them with read_file during collaboration.
Backend:
- POST /v1/teams/{teamId}/workspace/upload — multipart upload with
team membership check, tenant isolation, blocked extensions, 10MB
limit, 100-file quota, symlink escape prevention, and real-time
EventWorkspaceFileChanged broadcast.
- POST /v1/storage/files — admin-only storage upload with protected
dir rejection and size cache invalidation.
- Export MaxFileSizeBytes, MaxFilesPerScope, IsBlockedExtension from
tools package for reuse in HTTP handlers.
Frontend:
- Reusable FileUploadDialog component (drop zone, multi-file,
per-file status, client-side validation).
- Upload button added to Team Workspace dialog (scope-aware) and
Storage page (folder-aware).
- i18n strings for en/vi/zh.
Commit a8815a94 introduced a call to store.WithAgentKey() in
agent/loop.go but never added the function to store/context.go,
breaking the build on main.
Four HTTP handlers (chat completions, wake, tools invoke, responses) called
resolveAuth() directly instead of using requireAuth middleware, missing
WithTenantID and WithRole context injection. This broke tenant isolation
when backend services used tenant-scoped API keys on these endpoints.
Extract enrichContext() helper from requireAuth to DRY the context setup
logic. Refactor requireAuth and requireAuthBearer to use it. Block wake
endpoint body user_id override when API key has bound owner_id to prevent
impersonation.
Move the requireMention early-return check before the dedup LoadOrStore
in handleAppMention. Previously, when requireMention=false, an
app_mention event arriving before its corresponding message event would
store the dedup key and return early without processing — causing the
message handler to see the key as already loaded and silently drop the
message (~50% of @mentions lost).
Remove the !isDM restriction in the Slack handler, allowing thread_ts to
be propagated for DMs. Add BuildScopedThreadSessionKey for string-based
thread IDs (Slack timestamps) and update the message consumer to detect
thread-scoped local_key values and override the session key.
This enables Slack's AI Panel "New Chat" to correctly start fresh GoClaw
sessions with clean history.
Closes#435
Co-authored-by: Erudition <erudition@users.noreply.github.com>
* fix(ui): show toast error when package install/uninstall fails
HttpClient throws ApiError on HTTP 500 responses, but installPackage
and uninstallPackage hooks did not catch the exception. This caused
the error to propagate past the toast logic, so users saw no feedback
when a system package failed to install.
Wrap http.post calls in try/catch to always show toast with the server
error message. Update PackageSection handlers to check res.ok instead
of relying on try/catch for flow control.
* fix(ui): truncate verbose package error in toast notification
Extract last meaningful error line (e.g. "ERROR: ...") from package
manager output instead of dumping the full multi-line response into
the toast, which caused text overflow.
* fix(ui): wrap long text in toast instead of truncating
Add break-words and min-w-0 to toast content so long error messages
(URLs, package manager output) wrap properly within the toast element.
Remove shortError helper — show full error with proper wrapping.
* fix(ui): constrain toast container width to prevent overflow
Move max-w-sm from toast item to the fixed container so long text
(URLs, multi-line errors) is properly bounded. Add overflow-hidden
on toast items as a safety net.
* fix(pkg-helper): re-add packages cascade-removed by apk del
apk del removes orphaned dependencies, which may include other
user-installed packages if they were pulled in as transitive deps.
After uninstalling, check which persisted packages are still present
and re-install any that were accidentally removed.
* revert: remove cascade re-add logic from pkg-helper
apk del does not cascade-remove other world packages (verified via
Docker test). The re-add logic was based on incorrect assumption.
Root cause of the all-packages-deleted issue needs further investigation.
The emitRun wrapper in runLoop (loop.go) is missing TenantID, causing
all streaming events (chunk, thinking, tool.call) to be emitted with
TenantID=Nil. The event filter (event_filter.go:43-44) blocks unscoped
events for non-admin clients, so tenant-bound API key users receive no
streaming — only run.started and run.completed (emitted from Run() in
loop_run.go which already sets TenantID).
Fix: add event.TenantID = l.tenantID, matching the pattern in
loop_run.go's emitRun wrapper.
* fix: add panic recovery to tool, cron, and summarization goroutines
A panicking tool, cron job, or auto-summarization goroutine crashes
the entire server process — disconnecting all clients and aborting
all running agents. Add defer/recover at three levels:
- safeExecute() in tools/registry.go: catches panics from any tool's
Execute method and returns an error result to the LLM
- Agent loop goroutine (loop.go): defense-in-depth for non-tool code
in the parallel tool execution goroutine
- Cron execution goroutine (service_execution.go): prevents a single
panicking cron job from taking down the server
- Summarization goroutine (loop_history.go): prevents background
auto-summarization panics from crashing the process
All recovery points log the panic value and stack trace at ERROR level
for debugging. Tool execution panics return actionable error messages
to the LLM so the agent can recover gracefully.
* refactor: extract panic recovery into safego.Recover helper
DRY the duplicated recover+stack+log boilerplate across 4 goroutine
sites into a shared safego.Recover(onPanic, attrs...) function.
Stack buffer bumped from 4096 to 8192 bytes.
---------
Co-authored-by: viettranx <viettranx@gmail.com>
* fix: clear cron session in DB when not cached (cold-cache after restart)
Fixes#365
PGSessionStore.Reset() only cleared the in-memory cache. After a server
restart the cache is empty, so Reset was a no-op — the next GetOrCreate
loaded the full accumulated history from DB, causing LLM tool loops from
contradictory context.
Add a DB fallback: when the session isn't in cache, issue a direct
UPDATE to clear messages and summary in PostgreSQL. This ensures cron
sessions always start clean regardless of server restarts.
The #294 fix (Reset+Save before each cron run) already had the right
intent but only worked within the same server lifetime.
* fix: move DB call outside mutex and log errors in Reset cold-cache path
ExecContext was running under s.mu.Lock(), blocking all session cache
operations during DB round-trip. Release the lock before the DB call.
Also log ExecContext errors instead of silently discarding them —
silent failure defeats the purpose of the cold-cache fix.
---------
Co-authored-by: viettranx <viettranx@gmail.com>
- Add RoleOwner ("owner") to permissions hierarchy (above admin)
- Replace all IsCrossTenant/client.IsCrossTenant() permission guards with
IsOwnerRole/client.IsOwner() role-based checks (20+ locations)
- Remove cross-tenant bypass from tenantClauseN SQL helper — always adds
AND tenant_id = $N (fail-closed)
- Add explicit unscoped store methods for startup: ListAllProviders,
ListAllInstances, ListAllEnabled, GetByIDUnscoped, GetTeamUnscoped
- Remove CrossTenant from auth layer: owner users get role "owner" with
concrete tenant ID (MasterTenantID fallback), no bypass flag
- Remove crossTenant field from WS Client; connect response sends
is_owner instead of cross_tenant
- Simplify event filter from 3-mode to 2-mode (tenant-scoped + owner sees system events)
- Fix channel instance loader to use inst.TenantID for agent lookup
- Fix agent embedding goroutine to capture tenant from caller context
- Fix provider cache invalidation to use event.TenantID
- Separate skills store: ListAllSystemSkills (system only) vs
ListAllSkills (system + tenant-scoped), StoreMissingDeps restricted
to system skills only
- Frontend: isCrossTenant → isOwner, add "owner" to UserRole type,
RequireAdmin uses hasMinRole for owner > admin hierarchy
The requireAuth and requireAuthBearer middlewares correctly detect
cross-tenant admin status (gateway token + owner user ID) but never
call store.WithCrossTenant(ctx). This causes all HTTP tenant management
endpoints (GET/POST /v1/tenants, etc.) to return 403 "insufficient role"
because handlers check store.IsCrossTenant(ctx) which was always false.
The WebSocket path works correctly because the gateway Client object
tracks cross-tenant status independently via client.IsCrossTenant().
Fix: Add store.WithCrossTenant(ctx) in both auth functions when
auth.CrossTenant is true, matching the design of the WS path.
Affected endpoints:
- GET/POST /v1/tenants (list, create)
- GET/PATCH /v1/tenants/{id} (get, update)
- GET/POST/DELETE /v1/tenants/{id}/users (membership)
- Any future endpoint checking store.IsCrossTenant(ctx)
* fix: check errors in cron store Scan and Unmarshal to prevent data corruption
Three unchecked errors in the cron store could silently corrupt data:
1. cron_update.go:44 — QueryRowContext().Scan() error ignored when
fetching current schedule during partial updates. If DB fails,
all schedule fields default to zero values, corrupting schedule_kind
to empty string and bypassing all validation.
2. cron_update.go:167 — json.Unmarshal error ignored when parsing
existing payload for merge. Corrupted JSON leaves payload zero-valued,
wiping all existing fields (Message, Deliver, Channel, To) when
only one field was being updated.
3. cron_scan.go:63 — json.Unmarshal error ignored when scanning cron
job rows. Malformed payload JSON silently returns zero-valued payload,
masking data corruption.
All three now return descriptive errors instead of proceeding with
corrupted data. NULL payload (valid case) is handled separately.
* fix: add NULL payload guard and check Marshal error in cron update
- Add len(payloadJSON) > 0 guard before Unmarshal in UpdateJob payload
merge path, consistent with the same guard in scanCronRow. Without
this, NULL payload (valid for new jobs) would return a false error
instead of proceeding with the merge.
- Check json.Marshal error instead of discarding it, consistent with
the error-checking theme of the parent commit.
---------
Co-authored-by: viettranx <viettranx@gmail.com>
- Introduced StripMessageDirectives in the agent loop to remove internal
[[name:value]] routing tags from user-facing content while preserving
them in session store for model context
- Narrowed regex from wildcard `(?s)\[\[.*?\]\]` to structured
`\[\[\w+(?::[^\]\n]+)?\]\]` to avoid false positives on legitimate content
- Preserved [[tts...]] tags for TTS AutoTagged pipeline compatibility
- Added 15 unit tests covering strip, preserve, and edge cases
- Fixed step numbering in agent loop (steps 5-9)
- Update README Docker section to use `make up` instead of raw
docker compose commands. `make up` handles network creation,
version embedding from git tags, and database migrations.
- Add WITH_* flags to Makefile for optional compose services:
WITH_BROWSER, WITH_OTEL, WITH_SANDBOX, WITH_TAILSCALE, WITH_REDIS.
Uses WITH_ prefix to avoid conflicts with common env vars (e.g.
BROWSER is set by many desktop environments).
- Document common make commands (up/down/logs/reset) and optional
service flags with descriptions in a table format.
Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
MCP bridge now returns 403 when no gateway token is configured instead
of serving tools without authentication. Startup warnings added for
default Postgres password and open CORS configuration.
When using draft_transport in Telegram DMs, the preview draft sometimes
stays in the input area after the real message is delivered, showing a
brief duplicate reply.
Clear the stale draft by sending an empty sendMessageDraft as soon as
the final message is sent in Send(). The cleanup is asynchronous and
best-effort so it won't block message delivery.
Co-authored-by: Badgerbees <rapabelias@gmail.com>
- Fix hardcoded MIT license → CC BY-NC 4.0 with link to CC website
- Move About button from user menu dropdown to topbar header
- Shorten menu label to "About" / "Giới thiệu" / "关于"
* feat(ui): add About GoClaw dialog to user menu
- Add AboutDialog showing version, source code, license, docs, and bug report links
- Add "About GoClaw" item to UserMenu (with Info icon)
- Add i18n for en, vi, zh
* fix(docker): pass VERSION build arg to Dockerfile
Without this, Docker builds always produce `dev` as the version string.
Users can set GOCLAW_VERSION in .env or environment to embed the correct
version (e.g. from `git describe --tags --always`).
* feat(ui): show update status in About dialog
- Clean version display (strip git commit hash suffix)
- Show "Up to date" badge when on latest version
- Show update banner with link to release notes when new version available
- Fetch health data on dialog open to get latest update info
- Add i18n keys for update status (en, vi, zh)
* refactor(ui): clean version display across all UI locations
Extract cleanVersion() to shared utility. Apply to:
- About dialog (already had it inline)
- Sidebar connection status footer
- Overview page header
"v2.5.1-3-g4fd653c1" now displays as "v2.5.1" everywhere.
* feat(ui): show version in System Health card instead of header
- Add version cell with Tag icon to SystemHealthCard grid
- Green check when up to date, red when update available
- Revert cleanVersion from overview header (keep upstream behavior)
- Add i18n for systemHealth.version (en, vi, zh)
* fix(ui): move version to System Health card header to preserve grid layout
Version now shows in the card header row (right side) instead of as a
grid cell, keeping the original 6-cell (2x3) layout intact.
---------
Co-authored-by: huyluseo <thanhhuylu@example.com>
1. Stale user_context_files after agent file update (#294.1)
- Added PropagateContextFile() to AgentStore — single CTE query copies
agent-level file to all existing user instances
- Added `propagate` param to agents.files.set WS handler
- Response includes `propagated` count
2. Cron session reuse — tool errors block future runs (#294.2)
- Reset + Save cron session before each run
- Each run starts clean, persisted to DB to survive restarts
3. Write-behind cache risk — crash loses messages (#294.3)
- Added periodic checkpoint flush every 5 tool iterations
- Messages persisted to session incrementally during long runs
- Token calibration tracks checkpointed messages correctly
4. Silent cron delivery failures (#294.4)
- Added slog.Warn when deliver=true but channel/chatID missing
Closes#294
All topbar items (Documents, Language, Timezone, User menu) now use
text-sm consistently. Previously, non-select items used text-xs (12px)
while Select triggers inherited md:text-sm (14px) from the base
SelectTrigger component, causing a visible mismatch.
Two issues fixed:
1. effectiveRestrict() now always returns true (multi-tenant security),
so the "unrestricted" test case expected wrong results — paths outside
workspace are now blocked regardless of the restrict parameter.
2. Tests used bare Unix paths (/etc/passwd) which are not absolute on
Windows (no drive letter), causing them to resolve inside the workspace
and pass incorrectly. Added outsidePath() helper that prepends the
volume name for cross-platform absolute paths.
* fix(vision): fix Lark inline image parsing and MCP bridge read_image
Two fixes for image reading through Lark channel with claude-cli provider:
1. Lark post messages use flat format {"content":[[...]]} but resolvePostElements
only handled language-wrapped format {"zh_cn":{"content":[...]}}. Now checks
flat format first, and skips non-map values in fallback iteration.
2. read_image tool failed via MCP bridge because bridge creates fresh HTTP context
losing WithMediaImages. Fix: enrich <media:image> tags with path attribute
(like documents), pass workspace through X-Workspace bridge header with HMAC,
so read_image can load images from disk via path parameter.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(security): add HMAC backward compat and guard workspace injection
- VerifyBridgeContext falls back to old signature format (without workspace)
for sessions with pre-existing MCP configs, preventing 403 on server restart
- Workspace context injection now requires agent context (agentID/userID)
to prevent unauthenticated path injection bypassing HMAC
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: viettranx <viettranx@gmail.com>