Files
Duc Nguyenandntduc bb7712a9ff 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>
2026-07-30 14:17:40 +07:00

115 lines
3.0 KiB
Go

package tracing
import (
"context"
"encoding/json"
"github.com/nextlevelbuilder/goclaw/internal/store"
)
type redactorContextKey struct{}
// TextRedactor removes runtime-only values before they cross a trace, event, or
// persistence boundary. It must be deterministic and safe to call repeatedly.
type TextRedactor func(string) string
// WithTextRedactor adds a redactor while preserving any boundary installed by
// an outer run.
func WithTextRedactor(ctx context.Context, redactor TextRedactor) context.Context {
if redactor == nil {
return ctx
}
if outer := textRedactorFromContext(ctx); outer != nil {
inner := redactor
redactor = func(value string) string {
return inner(outer(value))
}
}
return context.WithValue(ctx, redactorContextKey{}, redactor)
}
func textRedactorFromContext(ctx context.Context) TextRedactor {
if ctx == nil {
return nil
}
redactor, _ := ctx.Value(redactorContextKey{}).(TextRedactor)
return redactor
}
func RedactText(ctx context.Context, value string) string {
return RedactTextWith(textRedactorFromContext(ctx), value)
}
func RedactTextWith(redactor TextRedactor, value string) string {
if redactor == nil || value == "" {
return value
}
return redactor(value)
}
// RedactValue preserves the common payload shapes used by agent events and
// trace metadata while redacting every contained string.
func RedactValue(ctx context.Context, value any) any {
return RedactValueWith(textRedactorFromContext(ctx), value)
}
func RedactValueWith(redactor TextRedactor, value any) any {
if redactor == nil || value == nil {
return value
}
switch typed := value.(type) {
case string:
return redactor(typed)
case json.RawMessage:
return json.RawMessage(redactor(string(typed)))
case []byte:
return []byte(redactor(string(typed)))
case map[string]string:
copyValue := make(map[string]string, len(typed))
for key, item := range typed {
copyValue[key] = redactor(item)
}
return copyValue
case map[string]any:
copyValue := make(map[string]any, len(typed))
for key, item := range typed {
copyValue[key] = RedactValueWith(redactor, item)
}
return copyValue
case []string:
copyValue := make([]string, len(typed))
for i, item := range typed {
copyValue[i] = redactor(item)
}
return copyValue
case []any:
copyValue := make([]any, len(typed))
for i, item := range typed {
copyValue[i] = RedactValueWith(redactor, item)
}
return copyValue
default:
return value
}
}
func RedactSpan(ctx context.Context, span store.SpanData) store.SpanData {
redactor := textRedactorFromContext(ctx)
if redactor == nil {
return span
}
span.Name = redactor(span.Name)
span.Error = redactor(span.Error)
span.InputPreview = redactor(span.InputPreview)
span.OutputPreview = redactor(span.OutputPreview)
if len(span.Metadata) > 0 {
span.Metadata = json.RawMessage(redactor(string(span.Metadata)))
}
return span
}
func RedactSpanUpdates(ctx context.Context, updates map[string]any) map[string]any {
redacted, _ := RedactValue(ctx, updates).(map[string]any)
return redacted
}