mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-06-10 08:11:23 +00:00
36b43ed7f1
- Revert leader workspace override: leader keeps personal workspace as default, team workspace accessible via ToolTeamWorkspaceFromCtx - Auto-copy: when leader creates team_tasks, scan subject+description for file paths → copy from personal to team workspace so members can access them - Safety: Lstat (reject symlinks), 10MB size cap, .env excluded from allowed extensions, path traversal blocked - Prompt hint: clarify members can only access team workspace files, referenced files are auto-copied
275 lines
10 KiB
Go
275 lines
10 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/bootstrap"
|
|
"github.com/nextlevelbuilder/goclaw/internal/config"
|
|
"github.com/nextlevelbuilder/goclaw/internal/store"
|
|
"github.com/nextlevelbuilder/goclaw/internal/tools"
|
|
)
|
|
|
|
// contextSetupResult holds the outputs of injectContext that are needed by the main loop.
|
|
type contextSetupResult struct {
|
|
ctx context.Context
|
|
resolvedTeamSettings json.RawMessage
|
|
}
|
|
|
|
// injectContext enriches the context with agent, tenant, user, workspace, and tool-level
|
|
// values needed by the agent loop and tool execution. Also runs input guard and message
|
|
// truncation. Returns error only if input guard blocks the message.
|
|
func (l *Loop) injectContext(ctx context.Context, req *RunRequest) (contextSetupResult, error) {
|
|
// Inject agent UUID + key into context for tool routing
|
|
if l.agentUUID != uuid.Nil {
|
|
ctx = store.WithAgentID(ctx, l.agentUUID)
|
|
}
|
|
if l.id != "" {
|
|
ctx = store.WithAgentKey(ctx, l.id)
|
|
}
|
|
// Inject tenant into context for tool-level tenant scoping (spawn, MCP, etc.)
|
|
if l.tenantID != uuid.Nil {
|
|
ctx = store.WithTenantID(ctx, l.tenantID)
|
|
}
|
|
// Inject user ID into context for per-user scoping (memory, context files, etc.)
|
|
if req.UserID != "" {
|
|
ctx = store.WithUserID(ctx, req.UserID)
|
|
}
|
|
// Inject agent type into context for interceptor routing
|
|
if l.agentType != "" {
|
|
ctx = store.WithAgentType(ctx, l.agentType)
|
|
}
|
|
// Inject self-evolve flag for predefined agents that can update SOUL.md
|
|
if l.selfEvolve {
|
|
ctx = store.WithSelfEvolve(ctx, true)
|
|
}
|
|
// Inject original sender ID for group file writer permission checks
|
|
if req.SenderID != "" {
|
|
ctx = store.WithSenderID(ctx, req.SenderID)
|
|
}
|
|
// Inject global builtin tool settings for media tools (provider chain)
|
|
if l.builtinToolSettings != nil {
|
|
ctx = tools.WithBuiltinToolSettings(ctx, l.builtinToolSettings)
|
|
}
|
|
// Inject channel type into context for tools (e.g. message tool needs it for Zalo group routing)
|
|
if req.ChannelType != "" {
|
|
ctx = tools.WithToolChannelType(ctx, req.ChannelType)
|
|
}
|
|
// Inject per-agent overrides from DB so tools honor per-agent settings.
|
|
if l.restrictToWs != nil {
|
|
ctx = tools.WithRestrictToWorkspace(ctx, *l.restrictToWs)
|
|
}
|
|
if l.subagentsCfg != nil {
|
|
ctx = tools.WithSubagentConfig(ctx, l.subagentsCfg)
|
|
}
|
|
// Pass the agent's model and provider so subagents inherit the correct combo.
|
|
if l.model != "" {
|
|
ctx = tools.WithParentModel(ctx, l.model)
|
|
}
|
|
if l.provider != nil {
|
|
ctx = tools.WithParentProvider(ctx, l.provider.Name())
|
|
}
|
|
if l.memoryCfg != nil {
|
|
ctx = tools.WithMemoryConfig(ctx, l.memoryCfg)
|
|
}
|
|
if l.sandboxCfg != nil {
|
|
ctx = tools.WithSandboxConfig(ctx, l.sandboxCfg)
|
|
}
|
|
if l.shellDenyGroups != nil {
|
|
ctx = store.WithShellDenyGroups(ctx, l.shellDenyGroups)
|
|
}
|
|
|
|
// Workspace scope propagation (delegation origin → workspace tools).
|
|
if req.WorkspaceChannel != "" {
|
|
ctx = tools.WithWorkspaceChannel(ctx, req.WorkspaceChannel)
|
|
}
|
|
if req.WorkspaceChatID != "" {
|
|
ctx = tools.WithWorkspaceChatID(ctx, req.WorkspaceChatID)
|
|
}
|
|
if req.TeamTaskID != "" {
|
|
ctx = tools.WithTeamTaskID(ctx, req.TeamTaskID)
|
|
}
|
|
|
|
// --- Per-user setup: file seeding + workspace resolution ---
|
|
// Uses userSetups sync.Map to track both concerns atomically per user.
|
|
// Seeding must run before buildMessages→resolveContextFiles reads context files.
|
|
// Team sessions skip seeding: members process tasks from leader, not end-user onboarding.
|
|
isTeamSession := bootstrap.IsTeamSession(req.SessionKey)
|
|
setup := l.getOrCreateUserSetup(ctx, req.UserID, req.Channel, isTeamSession)
|
|
|
|
// Workspace resolution (layered pipeline).
|
|
// Layer order: tenant → team → project (future) → user/chat
|
|
// Two entry modes: solo agent (base = l.workspace) or team context (base = l.dataDir).
|
|
// Result is always a single folder set via WithToolWorkspace.
|
|
if l.workspace != "" && req.UserID != "" {
|
|
ws := setup.workspace
|
|
if ws == "" {
|
|
ws = l.workspace
|
|
}
|
|
// Apply user isolation layer via pipeline.
|
|
shared := l.shouldShareWorkspace(req.UserID, req.PeerKind)
|
|
effectiveWorkspace := tools.ResolveWorkspace(ws,
|
|
tools.UserChatLayer(tools.SanitizePathSegment(req.UserID), shared),
|
|
)
|
|
if l.shouldShareMemory() {
|
|
ctx = store.WithSharedMemory(ctx)
|
|
}
|
|
if l.shouldShareKnowledgeGraph() {
|
|
ctx = store.WithSharedKG(ctx)
|
|
}
|
|
if err := os.MkdirAll(effectiveWorkspace, 0755); err != nil {
|
|
slog.Warn("failed to create user workspace directory", "workspace", effectiveWorkspace, "user", req.UserID, "error", err)
|
|
}
|
|
ctx = tools.WithToolWorkspace(ctx, effectiveWorkspace)
|
|
} else if l.workspace != "" {
|
|
ctx = tools.WithToolWorkspace(ctx, l.workspace)
|
|
}
|
|
|
|
// Team workspace: dispatched task overrides default workspace.
|
|
if req.TeamWorkspace != "" {
|
|
if err := os.MkdirAll(req.TeamWorkspace, 0755); err != nil {
|
|
slog.Warn("failed to create team workspace directory", "workspace", req.TeamWorkspace, "error", err)
|
|
}
|
|
ctx = tools.WithToolTeamWorkspace(ctx, req.TeamWorkspace)
|
|
ctx = tools.WithToolWorkspace(ctx, req.TeamWorkspace)
|
|
}
|
|
if req.TeamID != "" {
|
|
ctx = tools.WithToolTeamID(ctx, req.TeamID)
|
|
}
|
|
if req.LeaderAgentID != "" {
|
|
ctx = tools.WithLeaderAgentID(ctx, req.LeaderAgentID)
|
|
}
|
|
|
|
// Team workspace: auto-resolve for agents with team membership (not dispatched).
|
|
// Lead agents default to team workspace; non-lead members keep own workspace.
|
|
var resolvedTeamSettings json.RawMessage
|
|
if req.TeamWorkspace == "" && l.teamStore != nil && l.agentUUID != uuid.Nil {
|
|
if team, _ := l.teamStore.GetTeamForAgent(ctx, l.agentUUID); team != nil {
|
|
resolvedTeamSettings = team.Settings
|
|
wsChat := req.ChatID
|
|
if wsChat == "" {
|
|
wsChat = req.UserID
|
|
}
|
|
shared := tools.IsSharedWorkspace(team.Settings)
|
|
// Resolve team workspace via layered pipeline: tenant → team → user/chat.
|
|
wsDir := tools.ResolveWorkspace(l.dataDir,
|
|
tools.TenantLayer(store.TenantIDFromContext(ctx), store.TenantSlugFromContext(ctx)),
|
|
tools.TeamLayer(team.ID),
|
|
tools.UserChatLayer(wsChat, shared),
|
|
)
|
|
if err := os.MkdirAll(wsDir, 0750); err != nil {
|
|
slog.Warn("failed to create team workspace directory", "workspace", wsDir, "error", err)
|
|
}
|
|
ctx = tools.WithToolTeamWorkspace(ctx, wsDir)
|
|
// Leader keeps personal workspace (set at line 110-132) as default.
|
|
// Team workspace accessible via ToolTeamWorkspaceFromCtx for delegation.
|
|
if req.TeamID == "" {
|
|
ctx = tools.WithToolTeamID(ctx, team.ID.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
// Persist agent UUID + user ID on the session (for querying/tracing)
|
|
if l.agentUUID != uuid.Nil || req.UserID != "" {
|
|
l.sessions.SetAgentInfo(ctx, req.SessionKey, l.agentUUID, req.UserID)
|
|
}
|
|
|
|
// Security: scan user message for injection patterns.
|
|
// Action is configurable: "log" (info), "warn" (default), "block" (reject message).
|
|
if l.inputGuard != nil {
|
|
if matches := l.inputGuard.Scan(req.Message); len(matches) > 0 {
|
|
matchStr := strings.Join(matches, ",")
|
|
switch l.injectionAction {
|
|
case "block":
|
|
slog.Warn("security.injection_blocked",
|
|
"agent", l.id, "user", req.UserID,
|
|
"patterns", matchStr, "message_len", len(req.Message),
|
|
)
|
|
return contextSetupResult{}, fmt.Errorf("message blocked: potential prompt injection detected (%s)", matchStr)
|
|
case "log":
|
|
slog.Info("security.injection_detected",
|
|
"agent", l.id, "user", req.UserID,
|
|
"patterns", matchStr, "message_len", len(req.Message),
|
|
)
|
|
default: // "warn"
|
|
slog.Warn("security.injection_detected",
|
|
"agent", l.id, "user", req.UserID,
|
|
"patterns", matchStr, "message_len", len(req.Message),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Inject agent key into context for tool-level resolution (multiple agents share tool registry)
|
|
ctx = tools.WithToolAgentKey(ctx, l.id)
|
|
|
|
// Inject delivered media tracker so write_file and message tool can coordinate:
|
|
// write_file(deliver=true) marks paths, message self-send guard checks before allowing.
|
|
ctx = tools.WithDeliveredMedia(ctx, tools.NewDeliveredMedia())
|
|
|
|
// Security: truncate oversized user messages gracefully (feed truncation notice into LLM)
|
|
maxChars := l.maxMessageChars
|
|
if maxChars <= 0 {
|
|
maxChars = config.DefaultMaxMessageChars
|
|
}
|
|
if len(req.Message) > maxChars {
|
|
originalLen := len(req.Message)
|
|
req.Message = req.Message[:maxChars] +
|
|
fmt.Sprintf("\n\n[System: Message was truncated from %d to %d characters due to size limit. "+
|
|
"Please ask the user to send shorter messages or use the read_file tool for large content.]",
|
|
originalLen, maxChars)
|
|
slog.Warn("security.message_truncated",
|
|
"agent", l.id, "user", req.UserID,
|
|
"original_len", originalLen, "truncated_to", maxChars,
|
|
)
|
|
}
|
|
|
|
// Build RunContext from all resolved values and inject as single context key.
|
|
// This provides a typed, inspectable snapshot of all loop-injected context.
|
|
// Individual With* keys above remain for backward compat during transition.
|
|
providerName := ""
|
|
if l.provider != nil {
|
|
providerName = l.provider.Name()
|
|
}
|
|
rc := &store.RunContext{
|
|
AgentID: l.agentUUID,
|
|
AgentKey: l.id,
|
|
TenantID: l.tenantID,
|
|
UserID: req.UserID,
|
|
AgentType: l.agentType,
|
|
SenderID: req.SenderID,
|
|
SelfEvolve: l.selfEvolve,
|
|
SharedMemory: store.IsSharedMemory(ctx),
|
|
SharedKG: store.IsSharedKG(ctx),
|
|
RestrictToWorkspace: l.restrictToWs != nil && *l.restrictToWs,
|
|
BuiltinToolSettings: l.builtinToolSettings,
|
|
ChannelType: req.ChannelType,
|
|
SubagentsCfg: l.subagentsCfg,
|
|
ParentModel: l.model,
|
|
ParentProvider: providerName,
|
|
MemoryCfg: l.memoryCfg,
|
|
SandboxCfg: l.sandboxCfg,
|
|
ShellDenyGroups: l.shellDenyGroups,
|
|
Workspace: tools.ToolWorkspaceFromCtx(ctx),
|
|
TeamWorkspace: tools.ToolTeamWorkspaceFromCtx(ctx),
|
|
TeamID: tools.ToolTeamIDFromCtx(ctx),
|
|
WorkspaceChannel: req.WorkspaceChannel,
|
|
WorkspaceChatID: req.WorkspaceChatID,
|
|
TeamTaskID: req.TeamTaskID,
|
|
LeaderAgentID: tools.LeaderAgentIDFromCtx(ctx),
|
|
AgentToolKey: l.id,
|
|
}
|
|
ctx = store.WithRunContext(ctx, rc)
|
|
|
|
return contextSetupResult{
|
|
ctx: ctx,
|
|
resolvedTeamSettings: resolvedTeamSettings,
|
|
}, nil
|
|
}
|