From 1a3e5ef459c3792c86973b282b3d0e5e36e5abd0 Mon Sep 17 00:00:00 2001 From: Ukjae Jeong Date: Wed, 12 Aug 2026 22:47:57 +0100 Subject: [PATCH] Go SDK: Keep audit interceptors non-blocking on errors (#36) Ensure Go audit-mode interceptors remain non-blocking when validator or mutator invocation returns an error. ## Motivation and Context SEP says audit-mode interceptors MUST NOT block execution regardless of results. The Go SDK already avoided blocking on audit-mode validation findings, but validator/mutator handler or RPC errors could still abort unless `FailOpen` was set. This change makes audit mode consistently non-blocking for invocation errors as well, matching the existing C# orchestrator behavior where `isAudit || failOpen` allows execution to continue. ## How Has This Been Tested? Added focused Go chain tests covering audit-mode validator and mutator errors. ## Breaking Changes None. This only relaxes audit-mode error handling to match SEP semantics. ## Types of changes - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [x] I have added appropriate error handling - [x] I have added or updated documentation as needed ## Additional context C# already treats audit-mode invocation failures as non-blocking. This aligns the Go chain behavior with that implementation and SEP audit-mode semantics. Signed-off-by: Ukjae Jeong --- go/sdk/interceptors/chain/chain.go | 4 +- go/sdk/interceptors/chain/chain_test.go | 84 +++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 2 deletions(-) diff --git a/go/sdk/interceptors/chain/chain.go b/go/sdk/interceptors/chain/chain.go index 471ad90..2691364 100644 --- a/go/sdk/interceptors/chain/chain.go +++ b/go/sdk/interceptors/chain/chain.go @@ -303,7 +303,7 @@ func (c *Chain) recordValidation(entry ChainEntry, result invokeOutcome, cr *Exe Type: interceptors.TypeValidation, Phase: cr.Phase, }) - if !entry.Interceptor.FailOpen { + if result.mode != interceptors.ModeAudit && !entry.Interceptor.FailOpen { cr.AbortedAt = append(cr.AbortedAt, AbortInfo{ Interceptor: entry.Interceptor.Name, Reason: result.err.Error(), @@ -383,7 +383,7 @@ func (c *Chain) runMutators( Type: interceptors.TypeMutation, Phase: params.Phase, }) - if !m.Interceptor.FailOpen { + if result.mode != interceptors.ModeAudit && !m.Interceptor.FailOpen { cr.AbortedAt = append(cr.AbortedAt, AbortInfo{ Interceptor: m.Interceptor.Name, Reason: result.err.Error(), diff --git a/go/sdk/interceptors/chain/chain_test.go b/go/sdk/interceptors/chain/chain_test.go index f6c8a97..a7adb79 100644 --- a/go/sdk/interceptors/chain/chain_test.go +++ b/go/sdk/interceptors/chain/chain_test.go @@ -407,3 +407,87 @@ func TestChain_FailOpenRecordsExecutionResult(t *testing.T) { require.Len(t, cr.Results, 2) }) } + +func TestChain_AuditModeErrorsDoNotAbort(t *testing.T) { + t.Parallel() + + t.Run("audit validator error is recorded without aborting", func(t *testing.T) { + t.Parallel() + auditValidator := &interceptors.Validator{ + Metadata: interceptors.Metadata{ + Name: "audit-validator", + Hooks: []interceptors.Hook{{ + Events: []string{"test/event"}, + Phase: interceptors.PhaseRequest, + }}, + Mode: interceptors.ModeAudit, + }, + Handler: func(_ context.Context, _ *interceptors.Invocation) (*interceptors.ValidationResult, error) { + return nil, fmt.Errorf("audit sink failed") + }, + } + + ch := setupChainWithInterceptors(t, auditValidator) + + payload, _ := json.Marshal(map[string]any{"value": "hello"}) + cr, err := ch.Execute(context.Background(), &chain.ExecutionParams{ + Event: "test/event", + Phase: interceptors.PhaseRequest, + Payload: payload, + }) + require.NoError(t, err) + + assert.Equal(t, chain.ChainSuccess, cr.Status) + assert.Empty(t, cr.AbortedAt) + require.Len(t, cr.Results, 1) + assert.Equal(t, "audit-validator", cr.Results[0].Interceptor) + }) + + t.Run("audit mutator error is recorded without aborting", func(t *testing.T) { + t.Parallel() + auditMutator := &interceptors.Mutator{ + Metadata: interceptors.Metadata{ + Name: "audit-mutator", + Hooks: []interceptors.Hook{{ + Events: []string{"test/event"}, + Phase: interceptors.PhaseResponse, + }}, + Mode: interceptors.ModeAudit, + PriorityHint: interceptors.NewPriority(10), + }, + Handler: func(_ context.Context, _ *interceptors.Invocation) (*interceptors.MutationResult, error) { + return nil, fmt.Errorf("shadow mutation failed") + }, + } + passingMutator := &interceptors.Mutator{ + Metadata: interceptors.Metadata{ + Name: "passing-mutator", + Hooks: []interceptors.Hook{{ + Events: []string{"test/event"}, + Phase: interceptors.PhaseResponse, + }}, + Mode: interceptors.ModeEnforce, + PriorityHint: interceptors.NewPriority(20), + }, + Handler: func(_ context.Context, _ *interceptors.Invocation) (*interceptors.MutationResult, error) { + modified, _ := json.Marshal(map[string]any{"value": "mutated"}) + return &interceptors.MutationResult{Modified: true, Payload: modified}, nil + }, + } + + ch := setupChainWithInterceptors(t, auditMutator, passingMutator) + + payload, _ := json.Marshal(map[string]any{"value": "hello"}) + cr, err := ch.Execute(context.Background(), &chain.ExecutionParams{ + Event: "test/event", + Phase: interceptors.PhaseResponse, + Payload: payload, + }) + require.NoError(t, err) + + assert.Equal(t, chain.ChainSuccess, cr.Status) + assert.Empty(t, cr.AbortedAt) + require.Len(t, cr.Results, 2) + require.NotNil(t, cr.FinalPayload) + }) +}