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>
Add ?user_id query param to MCP credential endpoints so system admins
and tenant admin/owner can manage credentials for other users.
Backend:
- Add resolveTargetUserID() with permission checks (system admin,
tenant admin/owner) and cross-tenant defense-in-depth
- Add TenantStore dependency to MCPUserCredentialsHandler
Frontend:
- Add searchable Combobox user selector for admin users in the
credentials dialog (visible only when canManageUsers)
- Update credential API calls to pass optional userId param
- Add i18n keys for admin mode (en/vi/zh)
Replace per-handler `token string` field with package-level
`pkgGatewayToken` via `InitGatewayToken()`, matching the existing
pattern for `pkgAPIKeyCache`, `pkgOwnerIDs`, etc.
- Remove `token` from ~30 HTTP handler structs and constructors
- Simplify `requireAuth(minRole, next)` signature (was 3 params)
- Simplify `resolveAuth(r)` signature (was 2 params)
- Rename `resolveAuthBearer` → `resolveAuthWithBearer` (clearer)
- Add `store.WithRole()`/`RoleFromContext()` to propagate caller
role through context
- Inject role into context in `requireAuth` and `requireAuthBearer`
Block team lead from being assigned as task owner across all dispatch
paths. When a leader assigns a task to itself, it gets dispatched to a
team session while still running on the main session — causing race
conditions, duplicate work, and potential infinite announce loops.
Guards added:
- executeCreate: reject assigneeID == team.LeadAgentID
- dispatchTaskToAgent: safety net auto-fail (retry/ticker edge cases)
- DispatchUnblockedTasks: auto-fail leader-owned pending tasks
- ProcessPendingTasks: skip before AssignTask
- executeRetry: explicit error before dispatch
- TeamsMethods.dispatchTaskToAgent: guard for WS/dashboard path
Also removed redundant GetTeamForAgent DB call in dispatchTaskToAgent
by passing *store.TeamData directly from callers.
- Remove "API Docs" from sidebar menu
- Add external "Documents" link (docs.goclaw.sh) to topbar header
- Remove unused notification bell icon from topbar
* feat(gateway): add version update checker
Check GitHub releases periodically (1h) and surface update availability
in the health endpoint + dashboard header. Version is auto-detected from
git tags via Makefile, falling back to VERSION file or build arg.
Backend: new UpdateChecker goroutine, health response includes
latestVersion/updateAvailable/updateUrl fields.
Frontend: version badge in overview header with clickable "available"
link when a newer release exists.
* fix(gateway): fix update checker compile errors and harden GitHub API call
- Export UpdateChecker/NewUpdateChecker to match server.go references
- Move StartUpdateChecker(ctx) after ctx declaration in gateway startup
- Add User-Agent header for GitHub API compliance
- Limit response body to 1MB via io.LimitReader
- Use strconv.Atoi instead of manual int parsing in parseSemver
---------
Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
Co-authored-by: viettranx <viettranx@gmail.com>