From 35ba90f9e8b2f29ac338d86d198088674bcbe7fc Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Mon, 6 Jul 2026 09:58:56 +0000 Subject: [PATCH 1/3] feat(audit): enhance SetupAuditor to include confined process name - Updated SetupAuditor function to accept an additional parameter for confined process name. - Modified related tests to accommodate the new parameter and validate its functionality. - Adjusted SocketAuditor and flush functions to utilize the confined process name in log messages. - Enhanced AppConfig to store the confined process name for session attribution in audit logs. --- audit/multi_auditor.go | 4 +-- audit/multi_auditor_test.go | 8 ++--- audit/socket_auditor.go | 39 ++++++++++++----------- audit/socket_auditor_test.go | 18 ++++++----- config/config.go | 50 ++++++++++++++++++++---------- config/session_correlation.go | 30 ++++++++++++------ config/session_correlation_test.go | 45 +++++++++++++++++++-------- landjail/manager.go | 23 +++++++++----- landjail/parent.go | 2 +- nsjail_manager/manager.go | 23 +++++++++----- nsjail_manager/parent.go | 2 +- proxy/proxy.go | 18 +++++++++++ 12 files changed, 176 insertions(+), 86 deletions(-) diff --git a/audit/multi_auditor.go b/audit/multi_auditor.go index bd0789b..28a8238 100644 --- a/audit/multi_auditor.go +++ b/audit/multi_auditor.go @@ -30,7 +30,7 @@ func (m *MultiAuditor) AuditRequest(req Request) { // provided configuration. It always includes a LogAuditor for stderr logging, // and conditionally adds a SocketAuditor if audit logs are enabled and the // workspace agent's log proxy socket exists. -func SetupAuditor(ctx context.Context, logger *slog.Logger, disableAuditLogs bool, logProxySocketPath string, sessionID uuid.UUID) (Auditor, error) { +func SetupAuditor(ctx context.Context, logger *slog.Logger, disableAuditLogs bool, logProxySocketPath string, sessionID uuid.UUID, confinedProcessName string) (Auditor, error) { stderrAuditor := NewLogAuditor(logger) auditors := []Auditor{stderrAuditor} @@ -50,7 +50,7 @@ func SetupAuditor(ctx context.Context, logger *slog.Logger, disableAuditLogs boo } agentWillProxy := !os.IsNotExist(err) if agentWillProxy { - socketAuditor := NewSocketAuditor(logger, logProxySocketPath, sessionID) + socketAuditor := NewSocketAuditor(logger, logProxySocketPath, sessionID, confinedProcessName) go socketAuditor.Loop(ctx) auditors = append(auditors, socketAuditor) } else { diff --git a/audit/multi_auditor_test.go b/audit/multi_auditor_test.go index 2293ff8..6a434b5 100644 --- a/audit/multi_auditor_test.go +++ b/audit/multi_auditor_test.go @@ -27,7 +27,7 @@ func TestSetupAuditor_DisabledAuditLogs(t *testing.T) { logger := slog.New(slog.NewTextHandler(io.Discard, nil)) ctx := context.Background() - auditor, err := SetupAuditor(ctx, logger, true, "", uuid.New()) + auditor, err := SetupAuditor(ctx, logger, true, "", uuid.New(), "") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -52,7 +52,7 @@ func TestSetupAuditor_EmptySocketPath(t *testing.T) { logger := slog.New(slog.NewTextHandler(io.Discard, nil)) ctx := context.Background() - _, err := SetupAuditor(ctx, logger, false, "", uuid.New()) + _, err := SetupAuditor(ctx, logger, false, "", uuid.New(), "") if err == nil { t.Fatal("expected error for empty socket path, got nil") } @@ -64,7 +64,7 @@ func TestSetupAuditor_SocketDoesNotExist(t *testing.T) { logger := slog.New(slog.NewTextHandler(io.Discard, nil)) ctx := context.Background() - auditor, err := SetupAuditor(ctx, logger, false, "/nonexistent/socket/path", uuid.New()) + auditor, err := SetupAuditor(ctx, logger, false, "/nonexistent/socket/path", uuid.New(), "") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -102,7 +102,7 @@ func TestSetupAuditor_SocketExists(t *testing.T) { t.Fatalf("failed to close temp file: %v", err) } - auditor, err := SetupAuditor(ctx, logger, false, socketPath, uuid.New()) + auditor, err := SetupAuditor(ctx, logger, false, socketPath, uuid.New(), "claude") if err != nil { t.Fatalf("unexpected error: %v", err) } diff --git a/audit/socket_auditor.go b/audit/socket_auditor.go index 5ab7f82..4a7c7e6 100644 --- a/audit/socket_auditor.go +++ b/audit/socket_auditor.go @@ -29,13 +29,14 @@ const ( // as a FIFO i.e., logs are sent in the order they are received and dropped // if the queue is full. type SocketAuditor struct { - dial func() (net.Conn, error) - logger *slog.Logger - logCh chan *agentproto.BoundaryLog - batchSize int - batchTimerDuration time.Duration - socketPath string - sessionID uuid.UUID + dial func() (net.Conn, error) + logger *slog.Logger + logCh chan *agentproto.BoundaryLog + batchSize int + batchTimerDuration time.Duration + socketPath string + sessionID uuid.UUID + confinedProcessName string droppedChannelFull atomic.Int64 droppedBatchFull atomic.Int64 @@ -47,7 +48,7 @@ type SocketAuditor struct { // NewSocketAuditor creates a new SocketAuditor that sends logs to the agent's // boundary log proxy socket after SocketAuditor.Loop is called. The socket path // is read from EnvAuditSocketPath, falling back to defaultAuditSocketPath. -func NewSocketAuditor(logger *slog.Logger, socketPath string, sessionID uuid.UUID) *SocketAuditor { +func NewSocketAuditor(logger *slog.Logger, socketPath string, sessionID uuid.UUID, confinedProcessName string) *SocketAuditor { // This channel buffer size intends to allow enough buffering for bursty // AI agent network requests while a batch is being sent to the workspace // agent. @@ -57,12 +58,13 @@ func NewSocketAuditor(logger *slog.Logger, socketPath string, sessionID uuid.UUI dial: func() (net.Conn, error) { return net.Dial("unix", socketPath) }, - logger: logger, - logCh: make(chan *agentproto.BoundaryLog, logChBufSize), - batchSize: defaultBatchSize, - batchTimerDuration: defaultBatchTimerDuration, - socketPath: socketPath, - sessionID: sessionID, + logger: logger, + logCh: make(chan *agentproto.BoundaryLog, logChBufSize), + batchSize: defaultBatchSize, + batchTimerDuration: defaultBatchTimerDuration, + socketPath: socketPath, + sessionID: sessionID, + confinedProcessName: confinedProcessName, } } @@ -104,7 +106,7 @@ type flushErr struct { func (e *flushErr) Error() string { return e.err.Error() } // flush sends the current batch of logs to the given connection. -func flush(conn net.Conn, sessionID uuid.UUID, logs []*agentproto.BoundaryLog) *flushErr { +func flush(conn net.Conn, sessionID uuid.UUID, confinedProcessName string, logs []*agentproto.BoundaryLog) *flushErr { if len(logs) == 0 { return nil } @@ -112,8 +114,9 @@ func flush(conn net.Conn, sessionID uuid.UUID, logs []*agentproto.BoundaryLog) * msg := &codec.BoundaryMessage{ Msg: &codec.BoundaryMessage_Logs{ Logs: &agentproto.ReportBoundaryLogsRequest{ - Logs: logs, - SessionId: sessionID.String(), + Logs: logs, + SessionId: sessionID.String(), + ConfinedProcessName: confinedProcessName, }, }, } @@ -195,7 +198,7 @@ func (s *SocketAuditor) Loop(ctx context.Context) { return } - if err := flush(conn, s.sessionID, batch); err != nil { + if err := flush(conn, s.sessionID, s.confinedProcessName, batch); err != nil { if err.permanent { // Data error: discard batch to avoid infinite retries. s.logger.Warn("dropping batch due to data error on flush attempt", diff --git a/audit/socket_auditor_test.go b/audit/socket_auditor_test.go index 09e359a..09e6de3 100644 --- a/audit/socket_auditor_test.go +++ b/audit/socket_auditor_test.go @@ -464,12 +464,12 @@ func TestSocketAuditor_Loop_ShutdownFlushIncludesDrops(t *testing.T) { func TestFlush_EmptyBatch(t *testing.T) { t.Parallel() - err := flush(nil, uuid.Nil, nil) + err := flush(nil, uuid.Nil, "", nil) if err != nil { t.Errorf("expected nil error for empty batch, got %v", err) } - err = flush(nil, uuid.Nil, []*agentproto.BoundaryLog{}) + err = flush(nil, uuid.Nil, "", []*agentproto.BoundaryLog{}) if err != nil { t.Errorf("expected nil error for empty slice, got %v", err) } @@ -516,6 +516,9 @@ func TestSocketAuditor_Loop_FlushIncludesSessionID(t *testing.T) { if req.SessionId != expectedSessionID { t.Errorf("expected SessionId=%s, got %q", expectedSessionID, req.SessionId) } + if req.ConfinedProcessName != "claude" { + t.Errorf("expected ConfinedProcessName=%q, got %q", "claude", req.ConfinedProcessName) + } if len(req.Logs) != auditor.batchSize { t.Errorf("expected %d logs, got %d", auditor.batchSize, len(req.Logs)) } @@ -572,11 +575,12 @@ func setupTestAuditor(t *testing.T) (*SocketAuditor, net.Conn) { dial: func() (net.Conn, error) { return clientConn, nil }, - logger: logger, - logCh: make(chan *agentproto.BoundaryLog, 2*defaultBatchSize), - batchSize: defaultBatchSize, - batchTimerDuration: defaultBatchTimerDuration, - sessionID: uuid.MustParse("00000000-0000-4000-8000-000000000001"), + logger: logger, + logCh: make(chan *agentproto.BoundaryLog, 2*defaultBatchSize), + batchSize: defaultBatchSize, + batchTimerDuration: defaultBatchTimerDuration, + sessionID: uuid.MustParse("00000000-0000-4000-8000-000000000001"), + confinedProcessName: "claude", } return auditor, serverConn diff --git a/config/config.go b/config/config.go index 9de181b..3e745b6 100644 --- a/config/config.go +++ b/config/config.go @@ -2,6 +2,7 @@ package config import ( "fmt" + "path/filepath" "strings" "github.com/coder/serpent" @@ -100,6 +101,22 @@ type AppConfig struct { // all audit events produced by this boundary invocation into a // single session. Set by Run, not by configuration. SessionID uuid.UUID + + // ConfinedProcessName is the base name of the process boundary is + // confining (e.g. "claude", "codex"), derived from TargetCMD. It is + // reported alongside audit logs so that sessions can be attributed to + // the process that generated them. + ConfinedProcessName string +} + +// confinedProcessName returns a human-readable name for the process being +// confined, derived from the first element of the target command. It returns +// an empty string when no command is present. +func confinedProcessName(targetCMD []string) string { + if len(targetCMD) == 0 { + return "" + } + return filepath.Base(targetCMD[0]) } func NewAppConfigFromCliConfig(cfg CliConfig, targetCMD []string, environ []string) (AppConfig, error) { @@ -124,20 +141,21 @@ func NewAppConfigFromCliConfig(cfg CliConfig, targetCMD []string, environ []stri } return AppConfig{ - AllowRules: allAllowStrings, - LogLevel: cfg.LogLevel.Value(), - LogDir: cfg.LogDir.Value(), - ProxyPort: cfg.ProxyPort.Value(), - PprofEnabled: cfg.PprofEnabled.Value(), - PprofPort: cfg.PprofPort.Value(), - JailType: jailType, - UseRealDNS: cfg.UseRealDNS.Value(), - NoUserNamespace: cfg.NoUserNamespace.Value(), - TargetCMD: targetCMD, - UserInfo: userInfo, - DisableAuditLogs: cfg.DisableAuditLogs.Value(), - LogProxySocketPath: cfg.LogProxySocketPath.Value(), - SessionCorrelation: sc, + AllowRules: allAllowStrings, + LogLevel: cfg.LogLevel.Value(), + LogDir: cfg.LogDir.Value(), + ProxyPort: cfg.ProxyPort.Value(), + PprofEnabled: cfg.PprofEnabled.Value(), + PprofPort: cfg.PprofPort.Value(), + JailType: jailType, + UseRealDNS: cfg.UseRealDNS.Value(), + NoUserNamespace: cfg.NoUserNamespace.Value(), + TargetCMD: targetCMD, + UserInfo: userInfo, + DisableAuditLogs: cfg.DisableAuditLogs.Value(), + LogProxySocketPath: cfg.LogProxySocketPath.Value(), + SessionCorrelation: sc, + ConfinedProcessName: confinedProcessName(targetCMD), }, nil } @@ -152,8 +170,8 @@ func buildSessionCorrelation(cfg CliConfig, environ []string) (SessionCorrelatio targets := append(cfg.InjectSessionIDTargets.Value(), cfg.InjectSessionIDTarget.Value()...) if len(targets) == 0 && cfg.SessionCorrelationEnabled.Value() { - if t := DefaultInjectTargetFromEnv(environ); t != "" { - targets = []string{t} + if derived := DefaultInjectTargetsFromEnv(environ); len(derived) > 0 { + targets = derived } } diff --git a/config/session_correlation.go b/config/session_correlation.go index 67a7b27..bdf0c0b 100644 --- a/config/session_correlation.go +++ b/config/session_correlation.go @@ -19,8 +19,13 @@ const ( // exactly this header name. SequenceNumberHeaderName = "X-Coder-Agent-Firewall-Sequence-Number" - // DefaultAIBridgePath is the path glob used when auto-deriving an inject - // target from CODER_AGENT_URL. + // DefaultAIGatewayPath is the current AI Gateway route prefix glob used + // when auto-deriving an inject target from CODER_AGENT_URL. + DefaultAIGatewayPath = "/api/v2/ai-gateway/*" + + // DefaultAIBridgePath is the backward-compatible aibridge alias route + // prefix glob used when auto-deriving an inject target from + // CODER_AGENT_URL. DefaultAIBridgePath = "/api/v2/aibridge/*" // CoderAgentURLEnv is the environment variable set by the Coder workspace @@ -46,15 +51,17 @@ type SessionCorrelationConfig struct { InjectTargets []string } -// DefaultInjectTargetFromEnv derives an inject target rule string from the -// CODER_AGENT_URL variable in the provided environment slice. It returns "" +// DefaultInjectTargetsFromEnv derives inject target rule strings from the +// CODER_AGENT_URL variable in the provided environment slice. It returns nil // if the variable is absent, empty, or not a valid URL with a host. The -// derived target uses DefaultAIBridgePath as the path glob so that all AI -// Bridge traffic on the control-plane host is matched. +// derived targets cover both the current AI Gateway route prefix +// (DefaultAIGatewayPath) and its backward-compatible aibridge alias +// (DefaultAIBridgePath) so that clients hitting either path on the +// control-plane host get correlation headers injected. // // The environ parameter is accepted rather than reading os.Environ directly so // that callers (and tests) can supply an arbitrary environment. -func DefaultInjectTargetFromEnv(environ []string) string { +func DefaultInjectTargetsFromEnv(environ []string) []string { var raw string for _, e := range environ { k, v, ok := strings.Cut(e, "=") @@ -64,15 +71,18 @@ func DefaultInjectTargetFromEnv(environ []string) string { } } if raw == "" { - return "" + return nil } u, err := url.Parse(raw) if err != nil || u.Host == "" { - return "" + return nil } - return fmt.Sprintf("domain=%s path=%s", u.Hostname(), DefaultAIBridgePath) + return []string{ + fmt.Sprintf("domain=%s path=%s", u.Hostname(), DefaultAIGatewayPath), + fmt.Sprintf("domain=%s path=%s", u.Hostname(), DefaultAIBridgePath), + } } // ValidateSessionCorrelation checks that the session correlation config diff --git a/config/session_correlation_test.go b/config/session_correlation_test.go index 588f9e6..b3a8d59 100644 --- a/config/session_correlation_test.go +++ b/config/session_correlation_test.go @@ -160,7 +160,10 @@ func TestNewAppConfigFromCliConfig_SessionCorrelation(t *testing.T) { }(), environ: []string{"CODER_AGENT_URL=https://dev.coder.com/"}, wantEnabled: true, - wantTargets: []string{"domain=dev.coder.com path=" + DefaultAIBridgePath}, + wantTargets: []string{ + "domain=dev.coder.com path=" + DefaultAIGatewayPath, + "domain=dev.coder.com path=" + DefaultAIBridgePath, + }, }, { name: "enabled with no targets, CODER_AGENT_URL absent -> error", @@ -217,48 +220,57 @@ func TestNewAppConfigFromCliConfig_SessionCorrelation(t *testing.T) { } } -func TestDefaultInjectTargetFromEnv(t *testing.T) { +func TestDefaultInjectTargetsFromEnv(t *testing.T) { t.Parallel() tests := []struct { name string environ []string - want string + want []string }{ { name: "valid URL with trailing slash", environ: []string{"CODER_AGENT_URL=https://dev.coder.com/"}, - want: "domain=dev.coder.com path=" + DefaultAIBridgePath, + want: []string{ + "domain=dev.coder.com path=" + DefaultAIGatewayPath, + "domain=dev.coder.com path=" + DefaultAIBridgePath, + }, }, { name: "valid URL without trailing slash", environ: []string{"CODER_AGENT_URL=https://dev.coder.com"}, - want: "domain=dev.coder.com path=" + DefaultAIBridgePath, + want: []string{ + "domain=dev.coder.com path=" + DefaultAIGatewayPath, + "domain=dev.coder.com path=" + DefaultAIBridgePath, + }, }, { name: "URL with port", environ: []string{"CODER_AGENT_URL=https://dev.coder.com:8443/"}, - want: "domain=dev.coder.com path=" + DefaultAIBridgePath, + want: []string{ + "domain=dev.coder.com path=" + DefaultAIGatewayPath, + "domain=dev.coder.com path=" + DefaultAIBridgePath, + }, }, { name: "unset variable", environ: []string{}, - want: "", + want: nil, }, { name: "empty value", environ: []string{"CODER_AGENT_URL="}, - want: "", + want: nil, }, { name: "no host in URL", environ: []string{"CODER_AGENT_URL=not-a-url"}, - want: "", + want: nil, }, { name: "other env vars present but not CODER_AGENT_URL", environ: []string{"CODER_URL=https://dev.coder.com/", "HOME=/home/user"}, - want: "", + want: nil, }, } @@ -266,9 +278,15 @@ func TestDefaultInjectTargetFromEnv(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - got := DefaultInjectTargetFromEnv(tc.environ) - if got != tc.want { - t.Errorf("got %q, want %q", got, tc.want) + got := DefaultInjectTargetsFromEnv(tc.environ) + if len(got) != len(tc.want) { + t.Fatalf("len: got %d %q, want %d %q", + len(got), got, len(tc.want), tc.want) + } + for i := range got { + if got[i] != tc.want[i] { + t.Errorf("[%d]: got %q, want %q", i, got[i], tc.want[i]) + } } }) } @@ -381,6 +399,7 @@ func TestBuildSessionCorrelation_AgentURLFallback(t *testing.T) { }, environ: []string{"CODER_AGENT_URL=https://dev.coder.com/"}, wantTargets: []string{ + "domain=dev.coder.com path=" + DefaultAIGatewayPath, "domain=dev.coder.com path=" + DefaultAIBridgePath, }, }, diff --git a/landjail/manager.go b/landjail/manager.go index 4167103..4201f8e 100644 --- a/landjail/manager.go +++ b/landjail/manager.go @@ -31,15 +31,24 @@ func NewLandJail( logger *slog.Logger, config config.AppConfig, ) (*LandJail, error) { + // Build the session-correlation inject engine (nil when disabled). + injectEngine, err := proxy.NewInjectEngine(config.SessionCorrelation, logger) + if err != nil { + return nil, fmt.Errorf("build inject engine: %w", err) + } + // Create proxy server proxyServer := proxy.NewProxyServer(proxy.Config{ - HTTPPort: int(config.ProxyPort), - RuleEngine: ruleEngine, - Auditor: auditor, - Logger: logger, - TLSConfig: tlsConfig, - PprofEnabled: config.PprofEnabled, - PprofPort: int(config.PprofPort), + HTTPPort: int(config.ProxyPort), + RuleEngine: ruleEngine, + Auditor: auditor, + Logger: logger, + TLSConfig: tlsConfig, + PprofEnabled: config.PprofEnabled, + PprofPort: int(config.PprofPort), + SessionCorrelation: config.SessionCorrelation, + InjectEngine: injectEngine, + SessionID: config.SessionID.String(), }) return &LandJail{ diff --git a/landjail/parent.go b/landjail/parent.go index d399d3d..96e3b72 100644 --- a/landjail/parent.go +++ b/landjail/parent.go @@ -27,7 +27,7 @@ func RunParent(ctx context.Context, logger *slog.Logger, config config.AppConfig ruleEngine := rulesengine.NewRuleEngine(allowRules, logger) // Create auditor - auditor, err := audit.SetupAuditor(ctx, logger, config.DisableAuditLogs, config.LogProxySocketPath, config.SessionID) + auditor, err := audit.SetupAuditor(ctx, logger, config.DisableAuditLogs, config.LogProxySocketPath, config.SessionID, config.ConfinedProcessName) if err != nil { return fmt.Errorf("failed to setup auditor: %v", err) } diff --git a/nsjail_manager/manager.go b/nsjail_manager/manager.go index 348620d..28f1fd0 100644 --- a/nsjail_manager/manager.go +++ b/nsjail_manager/manager.go @@ -34,15 +34,24 @@ func NewNSJailManager( logger *slog.Logger, config config.AppConfig, ) (*NSJailManager, error) { + // Build the session-correlation inject engine (nil when disabled). + injectEngine, err := proxy.NewInjectEngine(config.SessionCorrelation, logger) + if err != nil { + return nil, fmt.Errorf("build inject engine: %w", err) + } + // Create proxy server proxyServer := proxy.NewProxyServer(proxy.Config{ - HTTPPort: int(config.ProxyPort), - RuleEngine: ruleEngine, - Auditor: auditor, - Logger: logger, - TLSConfig: tlsConfig, - PprofEnabled: config.PprofEnabled, - PprofPort: int(config.PprofPort), + HTTPPort: int(config.ProxyPort), + RuleEngine: ruleEngine, + Auditor: auditor, + Logger: logger, + TLSConfig: tlsConfig, + PprofEnabled: config.PprofEnabled, + PprofPort: int(config.PprofPort), + SessionCorrelation: config.SessionCorrelation, + InjectEngine: injectEngine, + SessionID: config.SessionID.String(), }) return &NSJailManager{ diff --git a/nsjail_manager/parent.go b/nsjail_manager/parent.go index e5d769b..90b3a72 100644 --- a/nsjail_manager/parent.go +++ b/nsjail_manager/parent.go @@ -28,7 +28,7 @@ func RunParent(ctx context.Context, logger *slog.Logger, config config.AppConfig ruleEngine := rulesengine.NewRuleEngine(allowRules, logger) // Create auditor - auditor, err := audit.SetupAuditor(ctx, logger, config.DisableAuditLogs, config.LogProxySocketPath, config.SessionID) + auditor, err := audit.SetupAuditor(ctx, logger, config.DisableAuditLogs, config.LogProxySocketPath, config.SessionID, config.ConfinedProcessName) if err != nil { return fmt.Errorf("failed to setup auditor: %v", err) } diff --git a/proxy/proxy.go b/proxy/proxy.go index 229cce0..e3dc250 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -83,6 +83,24 @@ func NewProxyServer(config Config) *Server { } } +// NewInjectEngine builds a rule engine for session-correlation inject +// targets from sc, or returns nil when correlation is disabled or has no +// targets. The returned engine uses the same matching semantics as --allow +// rules so that inject-target evaluation is identical to allow-rule +// evaluation. sc is expected to have already been validated via +// config.ValidateSessionCorrelation. +func NewInjectEngine(sc config.SessionCorrelationConfig, logger *slog.Logger) (*rulesengine.Engine, error) { + if !sc.Enabled || len(sc.InjectTargets) == 0 { + return nil, nil + } + rules, err := rulesengine.ParseAllowSpecs(sc.InjectTargets) + if err != nil { + return nil, fmt.Errorf("parse inject targets: %w", err) + } + eng := rulesengine.NewRuleEngine(rules, logger) + return &eng, nil +} + // Start starts the HTTP proxy server with TLS termination capability func (p *Server) Start() error { if p.isStarted() { From f309e802bbd2710561d7acce32dbe85de2abfbc9 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Tue, 7 Jul 2026 06:46:52 +0000 Subject: [PATCH 2/3] refactor(proxy): drop unused Config.SessionCorrelation and clarify correlation docs Remove the proxy Config.SessionCorrelation field and its assignments in the nsjail and landjail managers and the proxy test harness. The field was never read by NewProxyServer; the injection behavior is fully captured by InjectEngine (built from the correlation targets) and SessionID, so the copy was dead wiring that implied the server consulted it at request time. Also mark the aibridge inject path as a transitional alias to be removed once deployments serve the gateway exclusively at DefaultAIGatewayPath, and fix the stale NewSocketAuditor doc comment to describe the socketPath parameter and the confinedProcessName it now reports. --- audit/socket_auditor.go | 5 +++-- config/session_correlation.go | 5 ++++- landjail/manager.go | 19 +++++++++---------- nsjail_manager/manager.go | 19 +++++++++---------- proxy/proxy.go | 8 +++----- proxy/proxy_framework_test.go | 17 ++++++++--------- 6 files changed, 36 insertions(+), 37 deletions(-) diff --git a/audit/socket_auditor.go b/audit/socket_auditor.go index 4a7c7e6..4b1eb54 100644 --- a/audit/socket_auditor.go +++ b/audit/socket_auditor.go @@ -46,8 +46,9 @@ type SocketAuditor struct { } // NewSocketAuditor creates a new SocketAuditor that sends logs to the agent's -// boundary log proxy socket after SocketAuditor.Loop is called. The socket path -// is read from EnvAuditSocketPath, falling back to defaultAuditSocketPath. +// boundary log proxy socket at socketPath after SocketAuditor.Loop is called. +// The confinedProcessName is reported alongside logs so sessions can be +// attributed to the process that generated them. func NewSocketAuditor(logger *slog.Logger, socketPath string, sessionID uuid.UUID, confinedProcessName string) *SocketAuditor { // This channel buffer size intends to allow enough buffering for bursty // AI agent network requests while a batch is being sent to the workspace diff --git a/config/session_correlation.go b/config/session_correlation.go index bdf0c0b..0a43cbf 100644 --- a/config/session_correlation.go +++ b/config/session_correlation.go @@ -25,7 +25,10 @@ const ( // DefaultAIBridgePath is the backward-compatible aibridge alias route // prefix glob used when auto-deriving an inject target from - // CODER_AGENT_URL. + // CODER_AGENT_URL. It is transitional: once all Coder deployments serve + // the gateway exclusively at DefaultAIGatewayPath, this alias and the + // extra inject target derived from it in DefaultInjectTargetsFromEnv + // should be removed. DefaultAIBridgePath = "/api/v2/aibridge/*" // CoderAgentURLEnv is the environment variable set by the Coder workspace diff --git a/landjail/manager.go b/landjail/manager.go index 4201f8e..92b08ee 100644 --- a/landjail/manager.go +++ b/landjail/manager.go @@ -39,16 +39,15 @@ func NewLandJail( // Create proxy server proxyServer := proxy.NewProxyServer(proxy.Config{ - HTTPPort: int(config.ProxyPort), - RuleEngine: ruleEngine, - Auditor: auditor, - Logger: logger, - TLSConfig: tlsConfig, - PprofEnabled: config.PprofEnabled, - PprofPort: int(config.PprofPort), - SessionCorrelation: config.SessionCorrelation, - InjectEngine: injectEngine, - SessionID: config.SessionID.String(), + HTTPPort: int(config.ProxyPort), + RuleEngine: ruleEngine, + Auditor: auditor, + Logger: logger, + TLSConfig: tlsConfig, + PprofEnabled: config.PprofEnabled, + PprofPort: int(config.PprofPort), + InjectEngine: injectEngine, + SessionID: config.SessionID.String(), }) return &LandJail{ diff --git a/nsjail_manager/manager.go b/nsjail_manager/manager.go index 28f1fd0..b764348 100644 --- a/nsjail_manager/manager.go +++ b/nsjail_manager/manager.go @@ -42,16 +42,15 @@ func NewNSJailManager( // Create proxy server proxyServer := proxy.NewProxyServer(proxy.Config{ - HTTPPort: int(config.ProxyPort), - RuleEngine: ruleEngine, - Auditor: auditor, - Logger: logger, - TLSConfig: tlsConfig, - PprofEnabled: config.PprofEnabled, - PprofPort: int(config.PprofPort), - SessionCorrelation: config.SessionCorrelation, - InjectEngine: injectEngine, - SessionID: config.SessionID.String(), + HTTPPort: int(config.ProxyPort), + RuleEngine: ruleEngine, + Auditor: auditor, + Logger: logger, + TLSConfig: tlsConfig, + PprofEnabled: config.PprofEnabled, + PprofPort: int(config.PprofPort), + InjectEngine: injectEngine, + SessionID: config.SessionID.String(), }) return &NSJailManager{ diff --git a/proxy/proxy.go b/proxy/proxy.go index e3dc250..d3ec4c6 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -51,12 +51,10 @@ type Config struct { TLSConfig *tls.Config PprofEnabled bool PprofPort int - // SessionCorrelation controls header injection for AI Bridge - // correlation. See config.SessionCorrelationConfig for details. - SessionCorrelation config.SessionCorrelationConfig // InjectEngine, if non-nil, is used to evaluate whether outgoing - // requests match configured inject targets. Built from - // SessionCorrelation.InjectTargets using rulesengine.ParseAllowSpecs. + // requests match configured inject targets for session correlation. + // Built from config.SessionCorrelationConfig.InjectTargets via + // NewInjectEngine using rulesengine.ParseAllowSpecs. InjectEngine *rulesengine.Engine // SessionID is the boundary session UUID injected as a header // on matching requests. diff --git a/proxy/proxy_framework_test.go b/proxy/proxy_framework_test.go index bfc69e1..9ccad67 100644 --- a/proxy/proxy_framework_test.go +++ b/proxy/proxy_framework_test.go @@ -190,15 +190,14 @@ func (pt *ProxyTest) Start() *ProxyTest { } pt.server = NewProxyServer(Config{ - HTTPPort: pt.port, - RuleEngine: ruleEngine, - Auditor: auditor, - Logger: logger, - TLSConfig: tlsConfig, - SessionCorrelation: pt.sessionCorrelation, - InjectEngine: injectEngine, - SessionID: pt.sessionID, - ForwardTransport: pt.forwardTransport, + HTTPPort: pt.port, + RuleEngine: ruleEngine, + Auditor: auditor, + Logger: logger, + TLSConfig: tlsConfig, + InjectEngine: injectEngine, + SessionID: pt.sessionID, + ForwardTransport: pt.forwardTransport, }) err = pt.server.Start() From f9f79a667d048abcc00d8dba43bdba8488e4eedb Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Tue, 7 Jul 2026 08:15:29 +0000 Subject: [PATCH 3/3] remove overly verbose comments --- audit/socket_auditor.go | 2 -- nsjail_manager/manager.go | 1 - 2 files changed, 3 deletions(-) diff --git a/audit/socket_auditor.go b/audit/socket_auditor.go index 4b1eb54..0d1dc61 100644 --- a/audit/socket_auditor.go +++ b/audit/socket_auditor.go @@ -47,8 +47,6 @@ type SocketAuditor struct { // NewSocketAuditor creates a new SocketAuditor that sends logs to the agent's // boundary log proxy socket at socketPath after SocketAuditor.Loop is called. -// The confinedProcessName is reported alongside logs so sessions can be -// attributed to the process that generated them. func NewSocketAuditor(logger *slog.Logger, socketPath string, sessionID uuid.UUID, confinedProcessName string) *SocketAuditor { // This channel buffer size intends to allow enough buffering for bursty // AI agent network requests while a batch is being sent to the workspace diff --git a/nsjail_manager/manager.go b/nsjail_manager/manager.go index b764348..51169b6 100644 --- a/nsjail_manager/manager.go +++ b/nsjail_manager/manager.go @@ -34,7 +34,6 @@ func NewNSJailManager( logger *slog.Logger, config config.AppConfig, ) (*NSJailManager, error) { - // Build the session-correlation inject engine (nil when disabled). injectEngine, err := proxy.NewInjectEngine(config.SessionCorrelation, logger) if err != nil { return nil, fmt.Errorf("build inject engine: %w", err)