mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-14 14:17:40 +00:00
fix(collaboration): harden delegated task isolation (#1486)
* feat(collaboration): isolate delegated artifacts and child runs Isolate delegated inputs and outputs behind secure artifact exchange lifecycles. Scope Agent Link tasks by tenant and root agent, and enforce delegation spawn-tree boundaries. Add process-wide child-run admission and preserve logical media paths across native, MCP, and sandbox execution. * fix(collaboration): harden delegated task isolation Enforce tenant and root-agent task scope across migrations and stores. Add exactly-once async completion delivery, delegated sandbox boundaries, and confined artifact and media recovery across runtime surfaces. * fix(collaboration): recover interrupted async tasks * fix(collaboration): normalize persisted child-run status --------- Co-authored-by: ntduc <ntduc@cpp.ai.vn>
This commit is contained in:
1 parent
496b7ffce6
commit
bb7712a9ff
186 files changed
+18858
-1233
No files matched your search
@@ -27,6 +27,32 @@ type contextSetupResult struct {
|
||||
// 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) {
|
||||
isArtifactDelegation := req.RunKind == "delegate"
|
||||
|
||||
// A nested run must not inherit filesystem, Team, media, or delegation
|
||||
// authority from its caller. Install explicit empty values before resolving
|
||||
// this run's own scope.
|
||||
ctx = store.WithRunContext(ctx, nil)
|
||||
ctx = tools.WithToolWorkspace(ctx, "")
|
||||
ctx = tools.WithToolTeamWorkspace(ctx, "")
|
||||
ctx = tools.WithToolTeamRoot(ctx, "")
|
||||
ctx = tools.WithToolTeamID(ctx, "")
|
||||
ctx = tools.WithTeamTaskID(ctx, "")
|
||||
ctx = tools.WithLeaderAgentID(ctx, "")
|
||||
ctx = tools.WithTenantAllowedPaths(ctx, nil)
|
||||
ctx = tools.WithWorkspaceChannel(ctx, "")
|
||||
ctx = tools.WithWorkspaceChatID(ctx, "")
|
||||
ctx = tools.WithDelegationID(ctx, "")
|
||||
ctx = tools.WithDelegationArtifactInputs(ctx, "")
|
||||
ctx = tools.WithRunKind(ctx, "")
|
||||
ctx = tools.WithRunMediaPaths(ctx, nil)
|
||||
ctx = tools.WithRunMediaNames(ctx, nil)
|
||||
ctx = tools.WithMediaImages(ctx, nil)
|
||||
ctx = tools.WithMediaImageRefs(ctx, nil)
|
||||
ctx = tools.WithMediaDocRefs(ctx, nil)
|
||||
ctx = tools.WithMediaAudioRefs(ctx, nil)
|
||||
ctx = tools.WithMediaVideoRefs(ctx, nil)
|
||||
|
||||
// Inject agent UUID + key into context for tool routing
|
||||
if l.agentUUID != uuid.Nil {
|
||||
ctx = store.WithAgentID(ctx, l.agentUUID)
|
||||
@@ -87,7 +113,7 @@ func (l *Loop) injectContext(ctx context.Context, req *RunRequest) (contextSetup
|
||||
ctx = tools.WithTenantToolSettings(ctx, l.tenantToolSettings)
|
||||
}
|
||||
// Inject tenant-specific allowed paths for filesystem tools.
|
||||
if len(l.tenantAllowedPaths) > 0 {
|
||||
if !isArtifactDelegation && len(l.tenantAllowedPaths) > 0 {
|
||||
ctx = tools.WithTenantAllowedPaths(ctx, l.tenantAllowedPaths)
|
||||
}
|
||||
// Inject channel type into context for tools (e.g. message tool needs it for Zalo group routing)
|
||||
@@ -150,6 +176,9 @@ func (l *Loop) injectContext(ctx context.Context, req *RunRequest) (contextSetup
|
||||
if req.DelegationID != "" {
|
||||
ctx = tools.WithDelegationID(ctx, req.DelegationID)
|
||||
}
|
||||
if req.RunKind != "" {
|
||||
ctx = tools.WithRunKind(ctx, req.RunKind)
|
||||
}
|
||||
|
||||
// --- Per-user setup: file seeding + workspace resolution ---
|
||||
// Uses userSetups sync.Map to track both concerns atomically per user.
|
||||
@@ -163,7 +192,7 @@ func (l *Loop) injectContext(ctx context.Context, req *RunRequest) (contextSetup
|
||||
// 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 != "" {
|
||||
if !isArtifactDelegation && l.workspace != "" && req.UserID != "" {
|
||||
ws := setup.workspace
|
||||
if ws == "" {
|
||||
ws = l.workspace
|
||||
@@ -197,12 +226,21 @@ func (l *Loop) injectContext(ctx context.Context, req *RunRequest) (contextSetup
|
||||
effectiveWorkspace = l.workspace
|
||||
}
|
||||
ctx = tools.WithToolWorkspace(ctx, effectiveWorkspace)
|
||||
} else if l.workspace != "" {
|
||||
} else if !isArtifactDelegation && l.workspace != "" {
|
||||
ctx = tools.WithToolWorkspace(ctx, l.workspace)
|
||||
}
|
||||
|
||||
if isArtifactDelegation {
|
||||
if req.TeamWorkspace != "" ||
|
||||
!validateDelegationArtifactWorkspace(req.DelegationID, req.DelegateInputsPath, req.DelegateOutputsPath) {
|
||||
return contextSetupResult{}, fmt.Errorf("invalid delegation artifact workspace")
|
||||
}
|
||||
ctx = tools.WithDelegationArtifactInputs(ctx, req.DelegateInputsPath)
|
||||
ctx = tools.WithToolWorkspace(ctx, req.DelegateOutputsPath)
|
||||
}
|
||||
|
||||
// Team workspace: dispatched task overrides default workspace.
|
||||
if req.TeamWorkspace != "" {
|
||||
if !isArtifactDelegation && req.TeamWorkspace != "" {
|
||||
if err := os.MkdirAll(req.TeamWorkspace, 0755); err != nil {
|
||||
// See note above on loop_context user workspace fallback. A broken
|
||||
// req.TeamWorkspace would otherwise become cmd.Dir and surface as
|
||||
@@ -214,7 +252,7 @@ func (l *Loop) injectContext(ctx context.Context, req *RunRequest) (contextSetup
|
||||
ctx = tools.WithToolWorkspace(ctx, req.TeamWorkspace)
|
||||
}
|
||||
}
|
||||
if req.TeamID != "" {
|
||||
if !isArtifactDelegation && req.TeamID != "" {
|
||||
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.
|
||||
@@ -228,7 +266,7 @@ func (l *Loop) injectContext(ctx context.Context, req *RunRequest) (contextSetup
|
||||
ctx = tools.WithToolTeamRoot(ctx, teamRoot)
|
||||
}
|
||||
}
|
||||
if req.LeaderAgentID != "" {
|
||||
if !isArtifactDelegation && req.LeaderAgentID != "" {
|
||||
ctx = tools.WithLeaderAgentID(ctx, req.LeaderAgentID)
|
||||
}
|
||||
|
||||
@@ -237,14 +275,14 @@ func (l *Loop) injectContext(ctx context.Context, req *RunRequest) (contextSetup
|
||||
var resolvedTeamSettings json.RawMessage
|
||||
// Dispatched tasks already have TeamWorkspace set but still need team settings
|
||||
// for TeamIsolated flag. Fetch by explicit TeamID in that branch.
|
||||
if req.TeamWorkspace != "" && req.TeamID != "" && l.teamStore != nil {
|
||||
if !isArtifactDelegation && req.TeamWorkspace != "" && req.TeamID != "" && l.teamStore != nil {
|
||||
if teamUUID, err := uuid.Parse(req.TeamID); err == nil {
|
||||
if team, _ := l.teamStore.GetTeam(ctx, teamUUID); team != nil {
|
||||
resolvedTeamSettings = team.Settings
|
||||
}
|
||||
}
|
||||
}
|
||||
if req.TeamWorkspace == "" && l.teamStore != nil && l.agentUUID != uuid.Nil {
|
||||
if !isArtifactDelegation && 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
|
||||
@@ -285,7 +323,16 @@ func (l *Loop) injectContext(ctx context.Context, req *RunRequest) (contextSetup
|
||||
}
|
||||
|
||||
// V3 workspace: resolve once, set immutable context.
|
||||
{
|
||||
if isArtifactDelegation {
|
||||
ctx = workspace.WithContext(ctx, &workspace.WorkspaceContext{
|
||||
ActivePath: req.DelegateOutputsPath,
|
||||
Scope: workspace.ScopeDelegate,
|
||||
MemoryScope: "user",
|
||||
KGScope: "user",
|
||||
OwnerID: req.UserID,
|
||||
EnforcementLabel: workspace.DefaultEnforcementLabel(workspace.ScopeDelegate, false),
|
||||
})
|
||||
} else {
|
||||
var teamIDPtr *string
|
||||
if req.TeamID != "" {
|
||||
teamIDPtr = &req.TeamID
|
||||
@@ -386,6 +433,10 @@ func (l *Loop) injectContext(ctx context.Context, req *RunRequest) (contextSetup
|
||||
}
|
||||
// Extract resolved credential user ID (set earlier via WithCredentialUserID, empty if not resolved).
|
||||
credUserID := store.ExplicitCredentialUserIDFromContext(ctx)
|
||||
tenantAllowedPaths := l.tenantAllowedPaths
|
||||
if isArtifactDelegation {
|
||||
tenantAllowedPaths = nil
|
||||
}
|
||||
rc := &store.RunContext{
|
||||
AgentID: l.agentUUID,
|
||||
AgentKey: l.id,
|
||||
@@ -419,9 +470,10 @@ func (l *Loop) injectContext(ctx context.Context, req *RunRequest) (contextSetup
|
||||
WorkspaceChatID: effectiveWorkspaceChatID,
|
||||
TeamIsolated: resolvedTeamSettings != nil && !tools.IsSharedWorkspace(resolvedTeamSettings),
|
||||
TeamTaskID: req.TeamTaskID,
|
||||
DelegationID: req.DelegationID,
|
||||
LeaderAgentID: tools.LeaderAgentIDFromCtx(ctx),
|
||||
AgentToolKey: l.id,
|
||||
TenantAllowedPaths: l.tenantAllowedPaths,
|
||||
TenantAllowedPaths: tenantAllowedPaths,
|
||||
}
|
||||
ctx = store.WithRunContext(ctx, rc)
|
||||
|
||||
|
||||
Reference in new issue
Block a user