Skip to content

Commit 1a3e5ef

Browse files
authored
Go SDK: Keep audit interceptors non-blocking on errors (#36)
<!-- Provide a brief summary of your changes --> Ensure Go audit-mode interceptors remain non-blocking when validator or mutator invocation returns an error. ## Motivation and Context <!-- Why is this change needed? What problem does it solve? --> 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? <!-- Have you tested this in a real application? Which scenarios were tested? --> Added focused Go chain tests covering audit-mode validator and mutator errors. ## Breaking Changes <!-- Will users need to update their code or configurations? --> None. This only relaxes audit-mode error handling to match SEP semantics. ## Types of changes <!-- What types of changes does your code introduce? Put an `x` in all the boxes that apply: --> - [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 <!-- Go over all the following points, and put an `x` in all the boxes that apply. --> - [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 <!-- Add any other context, implementation notes, or design decisions --> 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 <jeongukjae@gmail.com>
1 parent 28ada74 commit 1a3e5ef

2 files changed

Lines changed: 86 additions & 2 deletions

File tree

go/sdk/interceptors/chain/chain.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ func (c *Chain) recordValidation(entry ChainEntry, result invokeOutcome, cr *Exe
303303
Type: interceptors.TypeValidation,
304304
Phase: cr.Phase,
305305
})
306-
if !entry.Interceptor.FailOpen {
306+
if result.mode != interceptors.ModeAudit && !entry.Interceptor.FailOpen {
307307
cr.AbortedAt = append(cr.AbortedAt, AbortInfo{
308308
Interceptor: entry.Interceptor.Name,
309309
Reason: result.err.Error(),
@@ -383,7 +383,7 @@ func (c *Chain) runMutators(
383383
Type: interceptors.TypeMutation,
384384
Phase: params.Phase,
385385
})
386-
if !m.Interceptor.FailOpen {
386+
if result.mode != interceptors.ModeAudit && !m.Interceptor.FailOpen {
387387
cr.AbortedAt = append(cr.AbortedAt, AbortInfo{
388388
Interceptor: m.Interceptor.Name,
389389
Reason: result.err.Error(),

go/sdk/interceptors/chain/chain_test.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,3 +407,87 @@ func TestChain_FailOpenRecordsExecutionResult(t *testing.T) {
407407
require.Len(t, cr.Results, 2)
408408
})
409409
}
410+
411+
func TestChain_AuditModeErrorsDoNotAbort(t *testing.T) {
412+
t.Parallel()
413+
414+
t.Run("audit validator error is recorded without aborting", func(t *testing.T) {
415+
t.Parallel()
416+
auditValidator := &interceptors.Validator{
417+
Metadata: interceptors.Metadata{
418+
Name: "audit-validator",
419+
Hooks: []interceptors.Hook{{
420+
Events: []string{"test/event"},
421+
Phase: interceptors.PhaseRequest,
422+
}},
423+
Mode: interceptors.ModeAudit,
424+
},
425+
Handler: func(_ context.Context, _ *interceptors.Invocation) (*interceptors.ValidationResult, error) {
426+
return nil, fmt.Errorf("audit sink failed")
427+
},
428+
}
429+
430+
ch := setupChainWithInterceptors(t, auditValidator)
431+
432+
payload, _ := json.Marshal(map[string]any{"value": "hello"})
433+
cr, err := ch.Execute(context.Background(), &chain.ExecutionParams{
434+
Event: "test/event",
435+
Phase: interceptors.PhaseRequest,
436+
Payload: payload,
437+
})
438+
require.NoError(t, err)
439+
440+
assert.Equal(t, chain.ChainSuccess, cr.Status)
441+
assert.Empty(t, cr.AbortedAt)
442+
require.Len(t, cr.Results, 1)
443+
assert.Equal(t, "audit-validator", cr.Results[0].Interceptor)
444+
})
445+
446+
t.Run("audit mutator error is recorded without aborting", func(t *testing.T) {
447+
t.Parallel()
448+
auditMutator := &interceptors.Mutator{
449+
Metadata: interceptors.Metadata{
450+
Name: "audit-mutator",
451+
Hooks: []interceptors.Hook{{
452+
Events: []string{"test/event"},
453+
Phase: interceptors.PhaseResponse,
454+
}},
455+
Mode: interceptors.ModeAudit,
456+
PriorityHint: interceptors.NewPriority(10),
457+
},
458+
Handler: func(_ context.Context, _ *interceptors.Invocation) (*interceptors.MutationResult, error) {
459+
return nil, fmt.Errorf("shadow mutation failed")
460+
},
461+
}
462+
passingMutator := &interceptors.Mutator{
463+
Metadata: interceptors.Metadata{
464+
Name: "passing-mutator",
465+
Hooks: []interceptors.Hook{{
466+
Events: []string{"test/event"},
467+
Phase: interceptors.PhaseResponse,
468+
}},
469+
Mode: interceptors.ModeEnforce,
470+
PriorityHint: interceptors.NewPriority(20),
471+
},
472+
Handler: func(_ context.Context, _ *interceptors.Invocation) (*interceptors.MutationResult, error) {
473+
modified, _ := json.Marshal(map[string]any{"value": "mutated"})
474+
return &interceptors.MutationResult{Modified: true, Payload: modified}, nil
475+
},
476+
}
477+
478+
ch := setupChainWithInterceptors(t, auditMutator, passingMutator)
479+
480+
payload, _ := json.Marshal(map[string]any{"value": "hello"})
481+
cr, err := ch.Execute(context.Background(), &chain.ExecutionParams{
482+
Event: "test/event",
483+
Phase: interceptors.PhaseResponse,
484+
Payload: payload,
485+
})
486+
require.NoError(t, err)
487+
488+
assert.Equal(t, chain.ChainSuccess, cr.Status)
489+
assert.Empty(t, cr.AbortedAt)
490+
require.Len(t, cr.Results, 2)
491+
require.NotNil(t, cr.FinalPayload)
492+
})
493+
}

0 commit comments

Comments
 (0)