Replace inline workspace path computation in loop_context.go with composable
WorkspaceLayer pipeline (tenant → team → project → user/chat). Each layer is
a pure function that appends a path segment or is a no-op.
- New workspace_resolver.go: ResolveWorkspace, TenantLayer, TeamLayer,
ProjectLayer (future), UserChatLayer, SanitizePathSegment
- 16 unit tests covering all layer combinations
- Migrate loop_context.go, loop_history.go, team_tasks_mutations.go
- Move sanitizePathSegment from agent to tools package (exported)
- Zero behavior change — identical paths for all scenarios
Prevent resource leaks and hanging actions in the browser tool:
- Per-action context timeout (default 30s, configurable via timeoutMs param or config)
- Idle page reaper goroutine closes pages unused for 10min (configurable)
- Max pages per tenant (default 5) with LRU eviction
- RefStore cleanup on page close/evict/reap to prevent memory leaks
Message tool sent raw MEDIA: paths to channels when LLMs embedded them
in multi-line text instead of using standalone prefix. Also added tenant
validation to prevent agents from sending to channels belonging to other
tenants.
- Extract embedded MEDIA: tokens from message text, resolve as media
attachments, strip from visible content (keeps surrounding text)
- Add ChannelTenantChecker callback to validate channel belongs to
calling agent's tenant before any send path
- Add ChannelTenantID() to channel Manager for tenant lookup
- Fix sendMedia() missing ContentType on MediaAttachment
- Fix .docx/.xlsx MIME types to proper OpenXML values
SignFileURLs converts bare /app/workspace/ paths in markdown links to
/v1/files/ URLs so they can be served. File handler uses workspace root
for ft= auth fallback search (no tenant context with signed tokens).
Strip standalone media links from markdown when MediaGallery already
renders the same file (prevents image/file appearing twice). Backend
dedup uses URL-context basename matching to avoid false positives.
SignFileURLs now heals legacy session data at delivery time:
- Dedup double /v1/files/v1/files/ prefix from old mutation bug
- Strip stale ?ft= from markdown link display text
- Re-sign all URLs with fresh tokens (never skip)
File serving: use workspace root for ft= auth fallback search
(no tenant context available with signed tokens).
Deduplicate ContentSuffix media by basename to prevent duplicate
file links when agent text already references the same file.
Frontend dedup as defense-in-depth for legacy data.
SignFileURLs previously skipped URLs containing ft=, leaving expired
tokens from legacy data that persisted signed URLs to the database.
Now strips any existing ft= parameter and always re-signs with a
fresh token at delivery time.
Subscribe to team.task.commented and new team.task.attachment_added
events in chat view. Show inline notifications and increment counters
on active task cards. Add toggleable sidebar panel (auto-opens on
first task dispatch, auto-closes when all tasks complete). Mobile
renders as slide-over with backdrop overlay.
OnEvent handler mutated MediaResult.Path in-place on the shared
RunResult.Media slice, corrupting paths for downstream consumers
(announce queue, outbound channels). Clone the media slice before
signing URLs. Add defense-in-depth guard in mediaToMarkdown to
skip prefix when path already starts with v1/files/ or v1/media/.
Defensive check prevents panic if a nil handler is appended to the
handlers slice. Restores the nil safety that existed with per-handler
field checks in the pre-refactoring code.
Replace 27 individual handler fields with single handlers []routeRegistrar
slice. BuildMux() registers all handlers via one loop instead of 27
nil-check blocks. Setter methods now append to slice. Reduces server.go
from 735 to 612 LOC (-123). No caller changes, no behavior change.
Previously owners were required to explicitly provide tenant_id when
creating agents. Now it falls back to the auth context tenant_id,
matching the behavior of non-owner callers.
Prevent cross-tenant config manipulation by verifying caller has
owner/admin role within the target tenant before allowing set/delete
operations. Also adds MaxBytesReader, audit trail, and fixes middleware
consistency between skills and builtin_tools endpoints.
Add PUT/DELETE /v1/skills/{id}/tenant-config routes for managing
per-tenant skill visibility overrides. Enrich GET /v1/skills list
with tenant_enabled field when tenant-scoped. Add ListAll method
to SkillTenantConfigStore interface + PG implementation.
Fix latent bug where explicit false on individual key would fall through
to RunContext. SelfEvolveFromContext, IsSharedMemory, IsSharedKG now
use (v, ok) type assertion to respect explicit false values.
Add store.RunContext struct that consolidates 27 loop-injected context
values into a single typed struct. RunContext is built once in
loop_context.go and injected via store.WithRunContext(ctx, rc).
24 accessor functions (10 in store/context.go, 14 in tools/context_keys.go)
updated with RunContext fallback — check individual key first, then
RunContext. Consumers (170+ files) require zero changes.
This enables adding new scope fields (e.g. ProjectID) by adding a single
struct field instead of a new context key + With/From function pair.
OnEvent callback only handled map[string]string payloads, but
run.completed uses map[string]any (contains usage map + media slice).
File URLs in content were never signed, and media paths were sent as
raw local paths without ?ft= hash tokens.
Switch on payload type to handle both shapes: sign content URLs and
convert media local paths to /v1/files/basename?ft=hash.
Restore 3 godoc comments that were accidentally dropped during
AgentStore/SessionStore interface split: GetDefault hint, CanAccess
return values, and Get vs GetOrCreate distinction.
Move task create, assign, delete, bulk delete, and dispatch methods from
teams_tasks.go (858 LOC) to teams_tasks_mutations.go (415 LOC).
Original file reduced to 460 LOC (get, review, comments). No logic change.
Move relation CRUD, batch ingest, prune, stats, and scan helpers from
knowledge_graph.go (731 LOC) to knowledge_graph_relations.go (378 LOC).
Original file reduced to 365 LOC (entity ops + search). No logic change.
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.