fix: resolve duplicated tenant-path segment in team shared-workspace resolution (#1322)

Root cause: internal/agent/loop_context.go re-applied tenant scoping
(TenantLayer/TenantID) on top of l.dataDir, which was already
tenant-scoped upstream in internal/agent/resolver.go, producing paths
like /app/workspace/tenants/<slug>/tenants/<slug>/teams/<teamID>
instead of the correct /app/workspace/tenants/<slug>/teams/<teamID>.

Also consolidates all previously-duplicated tenant-path-joining logic
(config.TenantDataDir, config.TenantWorkspace, tools.TenantLayer,
workspace.tenantPath) into a single canonical config.TenantScopedDir
function in internal/config/tenant_paths.go, with all other
implementations now delegating to it.

Co-authored-by: Bruno Clermont <bruno.clermont@gmail.com>
This commit is contained in:
Bruno ClermontandBruno Clermont authored and GitHub committed 2026-07-02 04:58:45 +07:00
1 parent 41321251cb
commit 7d95e4ae01
6 files changed
+196 -54

No files matched your search

+14 -10
View File
@@ -212,9 +212,11 @@ func (l *Loop) injectContext(ctx context.Context, req *RunRequest) (contextSetup
ctx = tools.WithToolTeamID(ctx, req.TeamID)
// Team root for dispatched tasks: resolve the UserChatLayer-stripped root
// so the dispatched agent can still read peer-scoped files in the same team.
// l.dataDir is already tenant-scoped (see resolver.go: config.TenantDataDir),
// so TenantLayer must NOT be reapplied here — doing so double-joins the
// tenant segment (tenants/<slug>/tenants/<slug>/teams/<id>).
if teamUUID, err := uuid.Parse(req.TeamID); err == nil && l.dataDir != "" {
teamRoot := tools.ResolveWorkspace(l.dataDir,
tools.TenantLayer(store.TenantIDFromContext(ctx), store.TenantSlugFromContext(ctx)),
tools.TeamLayer(teamUUID),
)
ctx = tools.WithToolTeamRoot(ctx, teamRoot)
@@ -244,9 +246,10 @@ func (l *Loop) injectContext(ctx context.Context, req *RunRequest) (contextSetup
wsChat = req.UserID
}
shared := tools.IsSharedWorkspace(team.Settings)
// Resolve team workspace via layered pipeline: tenant → team → user/chat.
// Resolve team workspace via layered pipeline: team → user/chat.
// l.dataDir is already tenant-scoped (see resolver.go: config.TenantDataDir) —
// do NOT reapply TenantLayer here, it would double-join the tenant segment.
wsDir := tools.ResolveWorkspace(l.dataDir,
tools.TenantLayer(store.TenantIDFromContext(ctx), store.TenantSlugFromContext(ctx)),
tools.TeamLayer(team.ID),
tools.UserChatLayer(wsChat, shared),
)
@@ -264,7 +267,6 @@ func (l *Loop) injectContext(ctx context.Context, req *RunRequest) (contextSetup
// the same team. Writes still default to wsDir above; team root only
// widens the allowed-prefix set for path boundary checks.
teamRoot := tools.ResolveWorkspace(l.dataDir,
tools.TenantLayer(store.TenantIDFromContext(ctx), store.TenantSlugFromContext(ctx)),
tools.TeamLayer(team.ID),
)
ctx = tools.WithToolTeamRoot(ctx, teamRoot)
@@ -294,12 +296,14 @@ func (l *Loop) injectContext(ctx context.Context, req *RunRequest) (contextSetup
// Filesystem path segment must use agent_key, not UUID — matches
// the v2 path in loop_pipeline_callbacks.go and the session_key
// anchor. See docs/agent-identity-conventions.md.
AgentID: l.id,
AgentType: l.agentType,
UserID: req.UserID,
ChatID: req.ChatID,
TenantID: store.TenantIDFromContext(ctx).String(),
TenantSlug: store.TenantSlugFromContext(ctx),
AgentID: l.id,
AgentType: l.agentType,
UserID: req.UserID,
ChatID: req.ChatID,
// TenantID/TenantSlug intentionally left empty: l.dataDir (BaseDir below)
// is already tenant-scoped (see resolver.go: config.TenantDataDir). Setting
// TenantID here would make resolveTeam/resolvePersonal reapply tenantPath()
// and double-join the tenant segment (tenants/<slug>/tenants/<slug>/...).
PeerKind: req.PeerKind,
TeamID: teamIDPtr,
TeamConfig: teamWSConfig,