mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-15 00:17:27 +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
@@ -29,10 +29,10 @@ func (r *defaultResolver) Resolve(_ context.Context, params ResolveParams) (*Wor
|
||||
return nil, fmt.Errorf("workspace: base dir is required")
|
||||
}
|
||||
|
||||
// Priority: delegation > team > personal/predefined
|
||||
// Agent Link artifact runs construct their fail-closed workspace directly
|
||||
// from the admitted exchange. This resolver handles durable team/personal
|
||||
// workspaces only.
|
||||
switch {
|
||||
case params.DelegateCtx != nil:
|
||||
return r.resolveDelegate(params)
|
||||
case params.TeamID != nil && *params.TeamID != "":
|
||||
return r.resolveTeam(params), nil
|
||||
default:
|
||||
@@ -40,30 +40,6 @@ func (r *defaultResolver) Resolve(_ context.Context, params ResolveParams) (*Wor
|
||||
}
|
||||
}
|
||||
|
||||
// resolveDelegate handles delegated task workspace.
|
||||
// ActivePath = delegate's shared path, read-only exports from delegator.
|
||||
// Validates SharedPath is under BaseDir to prevent directory traversal.
|
||||
func (r *defaultResolver) resolveDelegate(p ResolveParams) (*WorkspaceContext, error) {
|
||||
shared := filepath.Clean(p.DelegateCtx.SharedPath)
|
||||
base := filepath.Clean(p.BaseDir)
|
||||
if !strings.HasPrefix(shared+string(filepath.Separator), base+string(filepath.Separator)) {
|
||||
return nil, fmt.Errorf("workspace: delegate shared path escapes base dir")
|
||||
}
|
||||
|
||||
wc := &WorkspaceContext{
|
||||
ActivePath: shared,
|
||||
Scope: ScopeDelegate,
|
||||
ReadOnlyPaths: p.DelegateCtx.ExportPaths,
|
||||
SharedPath: &p.DelegateCtx.SharedPath,
|
||||
OwnerID: p.UserID,
|
||||
MemoryScope: "user",
|
||||
KGScope: "user",
|
||||
EnforcementLabel: DefaultEnforcementLabel(ScopeDelegate, false),
|
||||
}
|
||||
ensureDir(wc.ActivePath)
|
||||
return wc, nil
|
||||
}
|
||||
|
||||
// resolveTeam handles team workspace (shared or isolated).
|
||||
func (r *defaultResolver) resolveTeam(p ResolveParams) *WorkspaceContext {
|
||||
base := tenantPath(p.BaseDir, p.TenantID, p.TenantSlug)
|
||||
|
||||
@@ -195,55 +195,6 @@ func TestResolve_TeamIsolated(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolve_Delegation(t *testing.T) {
|
||||
base := t.TempDir()
|
||||
sharedPath := filepath.Join(base, "shared-task")
|
||||
exportPath := filepath.Join(base, "exports")
|
||||
|
||||
r := NewResolver()
|
||||
wc, err := r.Resolve(context.Background(), ResolveParams{
|
||||
AgentID: "agent-1",
|
||||
AgentType: "open",
|
||||
UserID: "user-1",
|
||||
BaseDir: base,
|
||||
DelegateCtx: &DelegateContext{
|
||||
LinkID: "link-1",
|
||||
SharedPath: sharedPath,
|
||||
ExportPaths: []string{exportPath},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if wc.ActivePath != sharedPath {
|
||||
t.Errorf("ActivePath = %q, want %q", wc.ActivePath, sharedPath)
|
||||
}
|
||||
if wc.Scope != ScopeDelegate {
|
||||
t.Errorf("Scope = %q, want delegate", wc.Scope)
|
||||
}
|
||||
if len(wc.ReadOnlyPaths) != 1 || wc.ReadOnlyPaths[0] != exportPath {
|
||||
t.Errorf("ReadOnlyPaths = %v", wc.ReadOnlyPaths)
|
||||
}
|
||||
assertDirExists(t, wc.ActivePath)
|
||||
}
|
||||
|
||||
func TestResolve_DelegationEscapesBaseDir(t *testing.T) {
|
||||
base := t.TempDir()
|
||||
r := NewResolver()
|
||||
_, err := r.Resolve(context.Background(), ResolveParams{
|
||||
AgentID: "agent-1",
|
||||
UserID: "user-1",
|
||||
BaseDir: base,
|
||||
DelegateCtx: &DelegateContext{
|
||||
SharedPath: "/etc/shadow",
|
||||
},
|
||||
})
|
||||
if err == nil {
|
||||
t.Error("expected error for delegate path escaping base dir")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolve_EnforcementLabel(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -10,9 +10,9 @@ import "context"
|
||||
type Scope string
|
||||
|
||||
const (
|
||||
ScopePersonal Scope = "personal" // single user, isolated
|
||||
ScopeTeam Scope = "team" // team context, shared or isolated
|
||||
ScopeDelegate Scope = "delegate" // delegated task, scoped access
|
||||
ScopePersonal Scope = "personal" // single user, isolated
|
||||
ScopeTeam Scope = "team" // team context, shared or isolated
|
||||
ScopeDelegate Scope = "delegate" // delegated task, scoped access
|
||||
)
|
||||
|
||||
// WorkspaceContext is resolved ONCE at run start, immutable for the entire run.
|
||||
@@ -24,13 +24,6 @@ type WorkspaceContext struct {
|
||||
// Scope describes the access boundary type.
|
||||
Scope Scope
|
||||
|
||||
// ReadOnlyPaths are additional paths the agent can read but NOT write.
|
||||
ReadOnlyPaths []string
|
||||
|
||||
// SharedPath is the shared delegate area (read/write by both delegator + delegatee).
|
||||
// nil when not in delegation context.
|
||||
SharedPath *string
|
||||
|
||||
// TeamPath is the team workspace root (nil if not in team context).
|
||||
TeamPath *string
|
||||
|
||||
@@ -65,7 +58,6 @@ type ResolveParams struct {
|
||||
PeerKind string // "direct" | "group"
|
||||
TeamID *string
|
||||
TeamConfig *TeamWorkspaceConfig
|
||||
DelegateCtx *DelegateContext
|
||||
BaseDir string
|
||||
}
|
||||
|
||||
@@ -81,13 +73,6 @@ func (c *TeamWorkspaceConfig) IsShared() bool {
|
||||
return c != nil && c.WorkspaceScope == "shared"
|
||||
}
|
||||
|
||||
// DelegateContext carries delegation-specific workspace overrides.
|
||||
type DelegateContext struct {
|
||||
LinkID string
|
||||
SharedPath string
|
||||
ExportPaths []string // read-only exports from delegator
|
||||
}
|
||||
|
||||
// DefaultEnforcementLabel returns a human-readable workspace description
|
||||
// for system prompt injection based on scope and sharing mode.
|
||||
func DefaultEnforcementLabel(scope Scope, shared bool) string {
|
||||
|
||||
Reference in new issue
Block a user