Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/prc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ on:
pull_request:
merge_group:
push:
branches:
- main
workflow_dispatch:
permissions:
contents: read
Expand Down
24 changes: 22 additions & 2 deletions graph/checkpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1283,17 +1283,37 @@ func TestProcessModelResponse_EventAndErrors(t *testing.T) {
require.NotNil(t, <-evch)

// Model API error path
errRsp := &model.Response{Error: &model.ResponseError{Message: "boom"}}
errCode := "api_key=sk-testsecret"
errParam := "Authorization: Bearer raw-token"
errRsp := &model.Response{
Error: &model.ResponseError{
Message: sensitiveGraphErrorMessage(),
Code: &errCode,
Param: &errParam,
},
}
errCh := make(chan *event.Event, 1)
_, _, err = processModelResponse(context.Background(), modelResponseConfig{
Response: errRsp,
EventChan: make(chan *event.Event, 1),
EventChan: errCh,
InvocationID: "inv",
SessionID: "sid",
LLMModel: &dummyModel{},
Request: &model.Request{Messages: []model.Message{model.NewUserMessage("hi")}},
Span: span,
})
require.Error(t, err)
requireGraphErrorMessageRedacted(t, err.Error())
errEvent := <-errCh
require.NotNil(t, errEvent)
require.NotNil(t, errEvent.Response)
require.NotNil(t, errEvent.Response.Error)
require.NotSame(t, errRsp, errEvent.Response)
requireGraphErrorMessageRedacted(t, errEvent.Response.Error.Message)
require.NotNil(t, errEvent.Response.Error.Code)
requireGraphErrorMessageRedacted(t, *errEvent.Response.Error.Code)
require.NotNil(t, errEvent.Response.Error.Param)
requireGraphErrorMessageRedacted(t, *errEvent.Response.Error.Param)

// Context done path when sending event
ctx, cancel := context.WithCancel(context.Background())
Expand Down
64 changes: 64 additions & 0 deletions graph/error_redaction.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//
// Tencent is pleased to support the open source community by making
// trpc-agent-go available.
//
// Copyright (C) 2025 Tencent. All rights reserved.
//
// trpc-agent-go is licensed under the Apache License Version 2.0.
//

package graph

import (
"trpc.group/trpc-go/trpc-agent-go/model"
"trpc.group/trpc-go/trpc-agent-go/platform"
)

func redactErrorMessage(err error) string {
if err == nil {
return ""
}
return redactErrorText(err.Error())
}

func redactErrorText(message string) string {
redactor, err := platform.NewRedactor()
if err != nil {
return "redacted error detail unavailable"
}
return redactor.Redact(message)
}

func redactedResponseErrorFromError(err error, fallbackType string) *model.ResponseError {
return redactResponseError(model.ResponseErrorFromError(err, fallbackType))
}

func redactedResponse(resp *model.Response) *model.Response {
if resp == nil || resp.Error == nil {
return resp
}
clone := resp.Clone()
clone.Error = redactResponseError(resp.Error)
return clone
}

func redactResponseError(respErr *model.ResponseError) *model.ResponseError {
if respErr == nil {
return nil
}
clone := *respErr
if clone.Message != "" {
clone.Message = redactErrorText(clone.Message)
}
if respErr.Code != nil {
code := *respErr.Code
code = redactErrorText(code)
clone.Code = &code
}
if respErr.Param != nil {
param := *respErr.Param
param = redactErrorText(param)
clone.Param = &param
}
return &clone
}
32 changes: 11 additions & 21 deletions graph/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -1273,7 +1273,7 @@ func NewNodeErrorEvent(opts ...NodeEventOption) *event.Event {
StartTime: options.StartTime,
EndTime: options.EndTime,
Duration: options.EndTime.Sub(options.StartTime),
Error: options.Error,
Error: redactErrorText(options.Error),
StepNumber: options.StepNumber,
Attempt: options.Attempt,
MaxAttempts: options.MaxAttempts,
Expand All @@ -1286,21 +1286,16 @@ func NewNodeErrorEvent(opts ...NodeEventOption) *event.Event {
WithNodeMetadata(metadata))
SetNodeEventEmitterInStateDelta(graphEvent.StateDelta, options.Emitter)

respErr := options.ResponseError
respErr := redactResponseError(options.ResponseError)
if respErr == nil && options.Error != "" {
respErr = &model.ResponseError{
Type: model.ErrorTypeFlowError,
Message: options.Error,
Message: redactErrorText(options.Error),
}
}
if respErr != nil {
if options.ResponseError != nil &&
(respErr.Message == "" || respErr.Type == "") {
clone := *respErr
respErr = &clone
}
if respErr.Message == "" {
respErr.Message = options.Error
respErr.Message = redactErrorText(options.Error)
}
if respErr.Type == "" {
respErr.Type = model.ErrorTypeFlowError
Expand All @@ -1319,7 +1314,7 @@ func NewToolExecutionEvent(opts ...ToolEventOption) *event.Event {

var errorMsg string
if options.Error != nil {
errorMsg = options.Error.Error()
errorMsg = redactErrorMessage(options.Error)
}

metadata := ToolExecutionMetadata{
Expand Down Expand Up @@ -1352,7 +1347,7 @@ func NewToolExecutionEvent(opts ...ToolEventOption) *event.Event {
if options.Error != nil {
resp.Error = &model.ResponseError{
Type: model.ErrorTypeFlowError,
Message: options.Error.Error(),
Message: redactErrorMessage(options.Error),
}
}
if resp.Timestamp.IsZero() {
Expand All @@ -1374,7 +1369,7 @@ func NewModelExecutionEvent(opts ...ModelEventOption) *event.Event {

var errorMsg string
if options.Error != nil {
errorMsg = options.Error.Error()
errorMsg = redactErrorMessage(options.Error)
}

metadata := ModelExecutionMetadata{
Expand Down Expand Up @@ -1431,29 +1426,24 @@ func NewPregelErrorEvent(opts ...PregelEventOption) *event.Event {
StartTime: options.StartTime,
EndTime: options.EndTime,
Duration: options.EndTime.Sub(options.StartTime),
Error: options.Error,
Error: redactErrorText(options.Error),
}
// Build base graph event with metadata.
ge := NewGraphEvent(options.InvocationID, AuthorGraphPregel, ObjectTypeGraphPregelStep,
WithPregelMetadata(metadata))
// Mirror error to Event.Error for easier consumption by clients that
// only check event.Error, while keeping object as graph.pregel.step
// for compatibility with existing consumers.
respErr := options.ResponseError
respErr := redactResponseError(options.ResponseError)
if respErr == nil && options.Error != "" {
respErr = &model.ResponseError{
Type: model.ErrorTypeFlowError,
Message: options.Error,
Message: redactErrorText(options.Error),
}
}
if respErr != nil {
if options.ResponseError != nil &&
(respErr.Message == "" || respErr.Type == "") {
clone := *respErr
respErr = &clone
}
if respErr.Message == "" {
respErr.Message = options.Error
respErr.Message = redactErrorText(options.Error)
}
if respErr.Type == "" {
respErr.Type = model.ErrorTypeFlowError
Expand Down
90 changes: 90 additions & 0 deletions graph/events_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,96 @@ func TestNewToolAndModelEvents(t *testing.T) {
require.Equal(t, "resp-1", mmeta.ResponseID)
}

func TestGraphErrorEventsRedactSensitiveMessages(t *testing.T) {
raw := sensitiveGraphErrorMessage()

nodeEvent := NewNodeErrorEvent(
WithNodeEventInvocationID("inv"),
WithNodeEventNodeID("node"),
WithNodeEventNodeType(NodeTypeFunction),
WithNodeEventError(raw),
)
require.NotNil(t, nodeEvent.Response)
require.NotNil(t, nodeEvent.Response.Error)
require.Contains(t, nodeEvent.Response.Error.Message, "boom")
requireGraphErrorMessageRedacted(t, nodeEvent.Response.Error.Message)
var nodeMeta NodeExecutionMetadata
require.NoError(t, json.Unmarshal(nodeEvent.StateDelta[MetadataKeyNode], &nodeMeta))
requireGraphErrorMessageRedacted(t, nodeMeta.Error)

code := "E_AUTH"
param := "Authorization: Bearer raw-token"
nodeWithResponseError := NewNodeErrorEvent(
WithNodeEventInvocationID("inv"),
WithNodeEventNodeID("node"),
WithNodeEventNodeType(NodeTypeFunction),
WithNodeEventResponseError(&model.ResponseError{
Type: model.ErrorTypeFlowError,
Message: raw,
Code: &code,
Param: &param,
}),
)
require.NotNil(t, nodeWithResponseError.Response)
require.NotNil(t, nodeWithResponseError.Response.Error)
requireGraphErrorMessageRedacted(t, nodeWithResponseError.Response.Error.Message)
require.NotNil(t, nodeWithResponseError.Response.Error.Code)
require.Equal(t, code, *nodeWithResponseError.Response.Error.Code)
require.NotNil(t, nodeWithResponseError.Response.Error.Param)
requireGraphErrorMessageRedacted(t, *nodeWithResponseError.Response.Error.Param)

toolEvent := NewToolExecutionEvent(
WithToolEventInvocationID("inv"),
WithToolEventNodeID("node"),
WithToolEventToolID("tool-call"),
WithToolEventToolName("tool"),
WithToolEventError(errors.New(raw)),
WithToolEventIncludeResponse(true),
)
require.NotNil(t, toolEvent.Response)
require.NotNil(t, toolEvent.Response.Error)
requireGraphErrorMessageRedacted(t, toolEvent.Response.Error.Message)
var toolMeta ToolExecutionMetadata
require.NoError(t, json.Unmarshal(toolEvent.StateDelta[MetadataKeyTool], &toolMeta))
requireGraphErrorMessageRedacted(t, toolMeta.Error)

modelEvent := NewModelExecutionEvent(
WithModelEventInvocationID("inv"),
WithModelEventNodeID("node"),
WithModelEventModelName("model"),
WithModelEventError(errors.New(raw)),
)
var modelMeta ModelExecutionMetadata
require.NoError(t, json.Unmarshal(modelEvent.StateDelta[MetadataKeyModel], &modelMeta))
requireGraphErrorMessageRedacted(t, modelMeta.Error)

pregelEvent := NewPregelErrorEvent(
WithPregelEventInvocationID("inv"),
WithPregelEventStepNumber(1),
WithPregelEventError(raw),
)
require.NotNil(t, pregelEvent.Response)
require.NotNil(t, pregelEvent.Response.Error)
requireGraphErrorMessageRedacted(t, pregelEvent.Response.Error.Message)
var pregelMeta PregelStepMetadata
require.NoError(t, json.Unmarshal(pregelEvent.StateDelta[MetadataKeyPregel], &pregelMeta))
requireGraphErrorMessageRedacted(t, pregelMeta.Error)
}

func sensitiveGraphErrorMessage() string {
return "boom: Authorization: Bearer raw-token api_key=sk-testsecret token=raw-token secret: raw-secret password=raw-password Cookie: session=raw-cookie"
}

func requireGraphErrorMessageRedacted(t *testing.T, message string) {
t.Helper()
require.NotContains(t, message, "raw-token")
require.NotContains(t, message, "sk-testsecret")
require.NotContains(t, message, "raw-secret")
require.NotContains(t, message, "raw-password")
require.NotContains(t, message, "raw-cookie")
require.Contains(t, message, "****")
}

func TestNewPregelAndChannelStateEvents(t *testing.T) {
start := time.Now().UTC()
end := start.Add(5 * time.Millisecond)
Expand Down
4 changes: 2 additions & 2 deletions graph/execution_error.go
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ func NewExecutionError(
err error,
severity ExecutionErrorSeverity,
) ExecutionError {
respErr := model.ResponseErrorFromError(err, model.ErrorTypeFlowError)
respErr := redactedResponseErrorFromError(err, model.ErrorTypeFlowError)
record := ExecutionError{
Severity: severity,
Timestamp: time.Now(),
Expand Down Expand Up @@ -386,7 +386,7 @@ func (c *ExecutionErrorCollector) afterNode(
}
record := NewExecutionError(callbackCtx, nodeErr, severity)
if policy.ResponseError != nil {
record.Error = cloneResponseError(policy.ResponseError)
record.Error = redactResponseError(policy.ResponseError)
}

update := State{
Expand Down
5 changes: 3 additions & 2 deletions graph/execution_error_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -499,13 +499,14 @@ func TestExecutionErrorCollector_EmptyOptionsKeepDefaults(t *testing.T) {
func TestNewExecutionError_NilCallbackContext(t *testing.T) {
record := NewExecutionError(
nil,
errors.New("boom"),
errors.New(sensitiveGraphErrorMessage()),
ExecutionErrorSeverityFatal,
)

require.Equal(t, ExecutionErrorSeverityFatal, record.Severity)
require.NotNil(t, record.Error)
require.Equal(t, "boom", record.Error.Message)
require.Contains(t, record.Error.Message, "boom")
requireGraphErrorMessageRedacted(t, record.Error.Message)
require.Empty(t, record.NodeID)
require.Empty(t, record.NodeName)
}
Expand Down
10 changes: 5 additions & 5 deletions graph/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -1830,9 +1830,9 @@ func emitTerminalGraphErrorEvent(
agent.EmitEvent(ctx, invocation, eventChan, NewPregelErrorEvent(
WithPregelEventInvocationID(invocationID),
WithPregelEventStepNumber(-1),
WithPregelEventError(err.Error()),
WithPregelEventError(redactErrorMessage(err)),
WithPregelEventResponseError(
model.ResponseErrorFromError(
redactedResponseErrorFromError(
err,
model.ErrorTypeFlowError,
),
Expand All @@ -1856,7 +1856,7 @@ func emitTerminalGraphErrorEvent(
invocationID,
author,
model.ErrorTypeFlowError,
err.Error(),
redactErrorMessage(err),
),
)
}
Expand Down Expand Up @@ -4155,9 +4155,9 @@ func (e *Executor) emitNodeErrorEvent(
WithNodeEventNodeType(nodeType),
WithNodeEventEmitter(NodeEventEmitterExecutor),
WithNodeEventStepNumber(step),
WithNodeEventError(err.Error()),
WithNodeEventError(redactErrorMessage(err)),
WithNodeEventResponseError(
model.ResponseErrorFromError(
redactedResponseErrorFromError(
err,
model.ErrorTypeFlowError,
),
Expand Down
Loading
Loading