Skip to content

Commit a4700bd

Browse files
authored
Refactor duplicated marshal/log paths and unify sanitized sys tool response logging (#4106)
Recent `SendRequestWithServerID` cleanup left three duplication/safety gaps: repeated marshal-for-debug blocks, duplicated sys tool handler skeletons, and unsanitized `sys_list_servers` result logging. This PR consolidates those paths and closes the sanitization gap. - **Consolidate marshal-for-debug logic** - Added `logger.LogMarshaledForDebug(...)` in `internal/logger/marshal_debug.go`. - Replaced repeated marshal+error/success logging blocks in: - `internal/guard/wasm.go` - `internal/server/guard_init.go` - Result: one shared marshaling pattern for debug logging across guard/server paths. - **Unify sys tool call/error/log skeleton** - Added `callAndLogSysTool(...)` in `internal/server/tool_registry.go`. - `sysInitHandler` and `sysListHandler` now share one call path (call sys server → handle error → marshal/log result → return). - **Fix sanitization gap in `sys_list_servers` logging** - Added shared `marshalAndSanitizeForLog(...)` in `tool_registry.go`. - `sys_list_servers` response logs now use `sanitize.SanitizeString(...)`, matching `sys_init` and backend tool logging behavior. - **Focused test coverage** - Added helper tests for marshal debug utility: - `internal/logger/marshal_debug_test.go` - Added server tests for log sanitization/error behavior: - `internal/server/tool_registry_test.go` ```go func (us *UnifiedServer) callAndLogSysTool(sessionID, operationName, sysToolName string) (*sdk.CallToolResult, interface{}, error) { result, err := us.callSysServer(sysToolName) if err != nil { logger.LogError("client", "MCP %s call failed, session=%s, error=%v", operationName, sessionID, err) return mcp.NewErrorCallToolResult(err) } logger.LogInfo("client", "MCP %s response, session=%s, result=%s", operationName, sessionID, marshalAndSanitizeForLog(result)) return nil, result, nil } ``` > [!WARNING] > > <details> > <summary>Firewall rules blocked me from connecting to one or more addresses (expand for details)</summary> > > #### I tried to connect to the following addresses, but was blocked by firewall rules: > > - `example.com` > - Triggering command: `/tmp/go-build1793350213/b510/launcher.test /tmp/go-build1793350213/b510/launcher.test -test.testlogfile=/tmp/go-build1793350213/b510/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true 3350213/b184/_pk-errorsas 64/src/log/slog/-ifaceassert bin/as -p zevo/backend/isa-atomic -lang=go1.25 /opt/hostedtoolc-buildtags -o s/attributes.go idRS/k9xnSFeesAm-ifaceassert x_amd64/compile -p crypto/internal/info -lang=go1.25 x_amd64/compile` (dns block) > - Triggering command: `/tmp/go-build3990767025/b514/launcher.test /tmp/go-build3990767025/b514/launcher.test -test.testlogfile=/tmp/go-build3990767025/b514/testlog.txt -test.paniconexit0 -test.timeout=10m0s /tmp/go-build3990767025/b495/vet.cfg ry=1 -trimpath x_amd64/vet -p github.com/githupush -lang=go1.25 x_amd64/vet -ato�� .cfg -buildtags 64/pkg/tool/linux_amd64/vet -errorsas -ifaceassert -nilfunc 64/pkg/tool/linu-trimpath` (dns block) > - Triggering command: `/tmp/go-build848674421/b514/launcher.test /tmp/go-build848674421/b514/launcher.test -test.testlogfile=/tmp/go-build848674421/b514/testlog.txt -test.paniconexit0 -test.timeout=10m0s know�� l/server/tool_registry_test.go known-linux-gnu/lib/rustlib/x86_--format=format:%H %ct %D stable-x86_64-REDACTED-linux-gnu/--end-of-options 6e1dc71b.rlib ccc.rlib --systemd-cgroup-bool` (dns block) > - `invalid-host-that-does-not-exist-12345.com` > - Triggering command: `/tmp/go-build1793350213/b492/config.test /tmp/go-build1793350213/b492/config.test -test.testlogfile=/tmp/go-build1793350213/b492/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true 3350213/b151/_pk-p o x_amd64/compile` (dns block) > - Triggering command: `/tmp/go-build3990767025/b496/config.test /tmp/go-build3990767025/b496/config.test -test.testlogfile=/tmp/go-build3990767025/b496/testlog.txt -test.paniconexit0 -test.timeout=10m0s n-me�� g_.a pkg/mod/go.opentelemetry.io/otel/exporters/otlp/otlptrace/[email protected]/doc.go x_amd64/vet -p github.com/segme--norc -lang=go1.17 x_amd64/vet -ato�� MQJZCSQ0B -buildtags x_amd64/vet -errorsas -ifaceassert -nilfunc ks...&#34;; \ elif c--revs` (dns block) > - Triggering command: `/tmp/go-build848674421/b496/config.test /tmp/go-build848674421/b496/config.test -test.testlogfile=/tmp/go-build848674421/b496/testlog.txt -test.paniconexit0 -test.timeout=10m0s 05ed�� 05ed-cgu.10.rcgu-errorsas 05ed-cgu.11.rcgu-ifaceassert 05ed-cgu.12.rcgu-nilfunc 05ed-cgu.13.rcgugit 05ed-cgu.14.rcguls-files pt_build-842ae9a--exclude-standard pt_build-842ae9a--others /usr�� --root .rlib 6b2b26a06.rlib 327.rlib 653.rlib 86b29d.rlib ive.12123747d8da05ed-cgu.00.rcgu.o` (dns block) > - `nonexistent.local` > - Triggering command: `/tmp/go-build1793350213/b510/launcher.test /tmp/go-build1793350213/b510/launcher.test -test.testlogfile=/tmp/go-build1793350213/b510/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true 3350213/b184/_pk-errorsas 64/src/log/slog/-ifaceassert bin/as -p zevo/backend/isa-atomic -lang=go1.25 /opt/hostedtoolc-buildtags -o s/attributes.go idRS/k9xnSFeesAm-ifaceassert x_amd64/compile -p crypto/internal/info -lang=go1.25 x_amd64/compile` (dns block) > - Triggering command: `/tmp/go-build3990767025/b514/launcher.test /tmp/go-build3990767025/b514/launcher.test -test.testlogfile=/tmp/go-build3990767025/b514/testlog.txt -test.paniconexit0 -test.timeout=10m0s /tmp/go-build3990767025/b495/vet.cfg ry=1 -trimpath x_amd64/vet -p github.com/githupush -lang=go1.25 x_amd64/vet -ato�� .cfg -buildtags 64/pkg/tool/linux_amd64/vet -errorsas -ifaceassert -nilfunc 64/pkg/tool/linu-trimpath` (dns block) > - Triggering command: `/tmp/go-build848674421/b514/launcher.test /tmp/go-build848674421/b514/launcher.test -test.testlogfile=/tmp/go-build848674421/b514/testlog.txt -test.paniconexit0 -test.timeout=10m0s know�� l/server/tool_registry_test.go known-linux-gnu/lib/rustlib/x86_--format=format:%H %ct %D stable-x86_64-REDACTED-linux-gnu/--end-of-options 6e1dc71b.rlib ccc.rlib --systemd-cgroup-bool` (dns block) > - `slow.example.com` > - Triggering command: `/tmp/go-build1793350213/b510/launcher.test /tmp/go-build1793350213/b510/launcher.test -test.testlogfile=/tmp/go-build1793350213/b510/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true 3350213/b184/_pk-errorsas 64/src/log/slog/-ifaceassert bin/as -p zevo/backend/isa-atomic -lang=go1.25 /opt/hostedtoolc-buildtags -o s/attributes.go idRS/k9xnSFeesAm-ifaceassert x_amd64/compile -p crypto/internal/info -lang=go1.25 x_amd64/compile` (dns block) > - Triggering command: `/tmp/go-build3990767025/b514/launcher.test /tmp/go-build3990767025/b514/launcher.test -test.testlogfile=/tmp/go-build3990767025/b514/testlog.txt -test.paniconexit0 -test.timeout=10m0s /tmp/go-build3990767025/b495/vet.cfg ry=1 -trimpath x_amd64/vet -p github.com/githupush -lang=go1.25 x_amd64/vet -ato�� .cfg -buildtags 64/pkg/tool/linux_amd64/vet -errorsas -ifaceassert -nilfunc 64/pkg/tool/linu-trimpath` (dns block) > - Triggering command: `/tmp/go-build848674421/b514/launcher.test /tmp/go-build848674421/b514/launcher.test -test.testlogfile=/tmp/go-build848674421/b514/testlog.txt -test.paniconexit0 -test.timeout=10m0s know�� l/server/tool_registry_test.go known-linux-gnu/lib/rustlib/x86_--format=format:%H %ct %D stable-x86_64-REDACTED-linux-gnu/--end-of-options 6e1dc71b.rlib ccc.rlib --systemd-cgroup-bool` (dns block) > - `this-host-does-not-exist-12345.com` > - Triggering command: `/tmp/go-build1793350213/b519/mcp.test /tmp/go-build1793350213/b519/mcp.test -test.testlogfile=/tmp/go-build1793350213/b519/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true transport/bdp_estimator.go transport/client_stream.go x_amd64/compile -pthread e/otlptracehttp/--version -fmessage-length=0 x_amd64/compile 3350�� g_.a 3350213/b165/ x_amd64/vet --gdwarf-5 mcpgodebug -o x_amd64/vet` (dns block) > - Triggering command: `/tmp/go-build3990767025/b523/mcp.test /tmp/go-build3990767025/b523/mcp.test -test.testlogfile=/tmp/go-build3990767025/b523/testlog.txt -test.paniconexit0 -test.timeout=10m0s -ato�� .cfg -buildtags 0278e5566a20dc05-d -errorsas -ifaceassert -nilfunc 64/pkg/tool/linu-buildtags --ve�� .cfg -tests 64/pkg/tool/linu-nilfunc 1024825-9d38bb40/usr/bin/runc.original [email protected]/depr--version x_amd64/compile 64/pkg/tool/linu-tests` (dns block) > - Triggering command: `/tmp/go-build848674421/b523/mcp.test /tmp/go-build848674421/b523/mcp.test -test.testlogfile=/tmp/go-build848674421/b523/testlog.txt -test.paniconexit0 -test.timeout=10m0s ogpt�� q7jvxzdnkys32nfz-errorsas 0ok7l9sshaaz07e8-ifaceassert hw1mmy6a1aekmmsh-nilfunc 4p2kq4dabhq5b2kedocker wmuqycv01jbjtq95inspect ntained/cc stable-x86_64-un{{.Config.OpenStdin}} stab�� stable-x86_64-REDACTED-linux-gnu/lib/rustlib/x86_64-REDACTED-linux-gnu/lib/libobject-926daa94a00ee/usr/libexec/docker/docker-init stable-x86_64-REDACTED-linux-gnu/lib/rustlib/x86_64-REDACTED-linux-gnu/lib/libmemchr-48d5b0db80402--version .1/x64/codeql/tools/linux64/java/lib/jspawnhelper stable-x86_64-un/usr/libexec/docker/cli-plugins/docker-compose 7a5585.0r6f2y9pmdocker-cli-plugin-metadata 7a5585.0y8i0suihruczucboywd9kbz6/tmp/go-build985941549/b001/_pkg_.a .1/x64/codeql/tools/linux64/java-trimpath` (dns block) > > If you need me to access, download, or install something from one of these locations, you can either: > > - Configure [Actions setup steps](https://gh.io/copilot/actions-setup-steps) to set up my environment, which run before the firewall is enabled > - Add the appropriate URLs or hosts to the custom allowlist in this repository's [Copilot coding agent settings](https://github.com/github/gh-aw-mcpg/settings/copilot/coding_agent) (admins only) > > </details>
2 parents dfb0ef7 + 8e22a91 commit a4700bd

6 files changed

Lines changed: 145 additions & 40 deletions

File tree

internal/guard/wasm.go

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -695,12 +695,15 @@ func (g *WasmGuard) LabelAgent(ctx context.Context, policy interface{}, backend
695695
logWasm.Printf("LabelAgent normalizePolicyPayload failed: guard=%s, error=%v", g.name, err)
696696
return nil, err
697697
}
698-
normalizedPolicyJSON, normalizeMarshalErr := json.Marshal(normalizedPolicy)
699-
if normalizeMarshalErr != nil {
700-
logWasm.Printf("LabelAgent normalized policy (marshal failed): guard=%s, error=%v", g.name, normalizeMarshalErr)
701-
} else {
702-
logWasm.Printf("LabelAgent normalized policy: guard=%s, policy=%s", g.name, string(normalizedPolicyJSON))
703-
}
698+
logger.LogMarshaledForDebug(
699+
normalizedPolicy,
700+
func(policyJSON string) {
701+
logWasm.Printf("LabelAgent normalized policy: guard=%s, policy=%s", g.name, policyJSON)
702+
},
703+
func(marshalErr error) {
704+
logWasm.Printf("LabelAgent normalized policy (marshal failed): guard=%s, error=%v", g.name, marshalErr)
705+
},
706+
)
704707
_ = caps
705708

706709
input, err := buildStrictLabelAgentPayload(normalizedPolicy)
@@ -727,12 +730,15 @@ func (g *WasmGuard) LabelAgent(ctx context.Context, policy interface{}, backend
727730
return nil, err
728731
}
729732

730-
resultLogJSON, resultMarshalErr := json.Marshal(result)
731-
if resultMarshalErr != nil {
732-
logWasm.Printf("LabelAgent parsed response (marshal failed): guard=%s, error=%v", g.name, resultMarshalErr)
733-
} else {
734-
logWasm.Printf("LabelAgent parsed response: guard=%s, response=%s", g.name, string(resultLogJSON))
735-
}
733+
logger.LogMarshaledForDebug(
734+
result,
735+
func(responseJSON string) {
736+
logWasm.Printf("LabelAgent parsed response: guard=%s, response=%s", g.name, responseJSON)
737+
},
738+
func(marshalErr error) {
739+
logWasm.Printf("LabelAgent parsed response (marshal failed): guard=%s, error=%v", g.name, marshalErr)
740+
},
741+
)
736742

737743
return result, nil
738744
}

internal/logger/marshal_debug.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package logger
2+
3+
import "encoding/json"
4+
5+
// LogMarshaledForDebug marshals value for debug logging and dispatches to the
6+
// provided callbacks for success or marshal failure paths.
7+
func LogMarshaledForDebug(value interface{}, onMarshalSuccess func(string), onMarshalFailure func(error)) {
8+
resultJSON, err := json.Marshal(value)
9+
if err != nil {
10+
onMarshalFailure(err)
11+
return
12+
}
13+
onMarshalSuccess(string(resultJSON))
14+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package logger
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func TestLogMarshaledForDebug_Success(t *testing.T) {
11+
assert := assert.New(t)
12+
require := require.New(t)
13+
14+
var (
15+
gotJSON string
16+
gotMarshalErr error
17+
)
18+
19+
LogMarshaledForDebug(
20+
map[string]string{"status": "ok"},
21+
func(resultJSON string) {
22+
gotJSON = resultJSON
23+
},
24+
func(err error) {
25+
gotMarshalErr = err
26+
},
27+
)
28+
29+
require.NoError(gotMarshalErr)
30+
assert.JSONEq(`{"status":"ok"}`, gotJSON)
31+
}
32+
33+
func TestLogMarshaledForDebug_MarshalFailure(t *testing.T) {
34+
assert := assert.New(t)
35+
require := require.New(t)
36+
37+
var (
38+
gotJSON string
39+
gotMarshalErr error
40+
)
41+
42+
LogMarshaledForDebug(
43+
make(chan int),
44+
func(resultJSON string) {
45+
gotJSON = resultJSON
46+
},
47+
func(err error) {
48+
gotMarshalErr = err
49+
},
50+
)
51+
52+
require.Error(gotMarshalErr)
53+
assert.Empty(gotJSON)
54+
}

internal/server/guard_init.go

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -351,12 +351,15 @@ func (us *UnifiedServer) ensureGuardInitialized(
351351
log.Printf("[DIFC] label_agent returned nil result: server=%s, session=%s, guard=%s", serverID, sessionID, g.Name())
352352
return defaultMode, fmt.Errorf("label_agent returned nil result")
353353
}
354-
resultJSON, marshalErr := json.Marshal(labelAgentResult)
355-
if marshalErr != nil {
356-
log.Printf("[DIFC] label_agent returned result (failed to serialize for logging): server=%s, session=%s, guard=%s, error=%v", serverID, sessionID, g.Name(), marshalErr)
357-
} else {
358-
log.Printf("[DIFC] label_agent response: server=%s, session=%s, guard=%s, response=%s", serverID, sessionID, g.Name(), string(resultJSON))
359-
}
354+
logger.LogMarshaledForDebug(
355+
labelAgentResult,
356+
func(resultJSON string) {
357+
log.Printf("[DIFC] label_agent response: server=%s, session=%s, guard=%s, response=%s", serverID, sessionID, g.Name(), resultJSON)
358+
},
359+
func(marshalErr error) {
360+
log.Printf("[DIFC] label_agent returned result (failed to serialize for logging): server=%s, session=%s, guard=%s, error=%v", serverID, sessionID, g.Name(), marshalErr)
361+
},
362+
)
360363

361364
mode := defaultMode
362365
if labelAgentResult.DIFCMode != "" {

internal/server/tool_registry.go

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -285,9 +285,7 @@ func (us *UnifiedServer) registerToolsFromBackend(serverID string) error {
285285
if err != nil {
286286
logger.LogError("client", "MCP tool call error, session=%s, tool=%s, error=%v", sessionID, toolNameCopy, err)
287287
} else {
288-
resultJSON, _ := json.Marshal(data)
289-
sanitizedResult := sanitize.SanitizeString(string(resultJSON))
290-
logger.LogInfo("client", "MCP tool call response, session=%s, tool=%s, result=%s", sessionID, toolNameCopy, sanitizedResult)
288+
logger.LogInfo("client", "MCP tool call response, session=%s, tool=%s, result=%s", sessionID, toolNameCopy, marshalAndSanitizeForLog(data))
291289
}
292290

293291
return result, data, err
@@ -351,6 +349,22 @@ func (us *UnifiedServer) callSysServer(toolName string) (interface{}, error) {
351349
return result, nil
352350
}
353351

352+
func marshalAndSanitizeForLog(value interface{}) string {
353+
resultJSON, _ := json.Marshal(value)
354+
return sanitize.SanitizeString(string(resultJSON))
355+
}
356+
357+
func (us *UnifiedServer) callAndLogSysTool(sessionID, operationName, sysToolName string) (*sdk.CallToolResult, interface{}, error) {
358+
result, err := us.callSysServer(sysToolName)
359+
if err != nil {
360+
logger.LogError("client", "MCP %s call failed, session=%s, error=%v", operationName, sessionID, err)
361+
return mcp.NewErrorCallToolResult(err)
362+
}
363+
364+
logger.LogInfo("client", "MCP %s response, session=%s, result=%s", operationName, sessionID, marshalAndSanitizeForLog(result))
365+
return nil, result, nil
366+
}
367+
354368
// registerSysTools registers built-in sys tools
355369
func (us *UnifiedServer) registerSysTools() error {
356370
// Create sys_init handler
@@ -392,16 +406,7 @@ func (us *UnifiedServer) registerSysTools() error {
392406
logger.LogInfo("client", "MCP session initialized successfully, session=%s, available_servers=%v", sessionID, us.launcher.ServerIDs())
393407

394408
// Call sys_init
395-
result, err := us.callSysServer("sys_init")
396-
if err != nil {
397-
logger.LogError("client", "MCP session initialization: sys_init call failed, session=%s, error=%v", sessionID, err)
398-
return mcp.NewErrorCallToolResult(err)
399-
}
400-
401-
resultJSON, _ := json.Marshal(result)
402-
sanitizedResult := sanitize.SanitizeString(string(resultJSON))
403-
logger.LogInfo("client", "MCP session initialization complete, session=%s, result=%s", sessionID, sanitizedResult)
404-
return nil, result, nil
409+
return us.callAndLogSysTool(sessionID, "session initialization", "sys_init")
405410
}
406411

407412
// Register sys_init tool using helper
@@ -431,15 +436,7 @@ func (us *UnifiedServer) registerSysTools() error {
431436
return mcp.NewErrorCallToolResult(err)
432437
}
433438

434-
result, err := us.callSysServer("sys_list_servers")
435-
if err != nil {
436-
logger.LogError("client", "MCP sys_list_servers error, session=%s, error=%v", sessionID, err)
437-
return mcp.NewErrorCallToolResult(err)
438-
}
439-
440-
resultJSON, _ := json.Marshal(result)
441-
logger.LogInfo("client", "MCP sys_list_servers response, session=%s, result=%s", sessionID, string(resultJSON))
442-
return nil, result, nil
439+
return us.callAndLogSysTool(sessionID, "sys_list_servers", "sys_list_servers")
443440
}
444441

445442
// Register sys_list_servers tool using helper

internal/server/tool_registry_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,37 @@ func TestCallSysServer_UnknownTool(t *testing.T) {
496496
require.Error(err, "callSysServer with unknown tool should return an error")
497497
}
498498

499+
func TestMarshalAndSanitizeForLog_RedactsSecrets(t *testing.T) {
500+
assert := assert.New(t)
501+
502+
const secret = "ghp_1234567890123456789012345678901234567890"
503+
sanitized := marshalAndSanitizeForLog(map[string]interface{}{
504+
"token": secret,
505+
})
506+
507+
assert.Contains(sanitized, "[REDACTED]")
508+
assert.NotContains(sanitized, secret)
509+
}
510+
511+
func TestCallAndLogSysTool_UnknownToolReturnsErrorResult(t *testing.T) {
512+
assert := assert.New(t)
513+
require := require.New(t)
514+
515+
cfg := &config.Config{
516+
Servers: map[string]*config.ServerConfig{},
517+
}
518+
519+
us, err := NewUnified(context.Background(), cfg)
520+
require.NoError(err)
521+
defer us.Close()
522+
523+
result, data, callErr := us.callAndLogSysTool("session-id", "sys test", "nonexistent_tool")
524+
require.Error(callErr)
525+
require.NotNil(result)
526+
assert.Nil(data)
527+
assert.True(result.IsError)
528+
}
529+
499530
// TestRegisterAllToolsParallel_EmptyList verifies that parallel registration with no
500531
// servers does not block and returns immediately.
501532
func TestRegisterAllToolsParallel_EmptyList(t *testing.T) {

0 commit comments

Comments
 (0)