diff --git a/cli/cli.go b/cli/cli.go index 7d1567a..5d0057f 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -37,7 +37,22 @@ func NewCommand(version string) *serpent.Command { # Use allowlist from config file with additional CLI allow rules boundary --allow "domain=example.com" -- curl https://example.com - # Block everything by default (implicit)` + # Block everything by default (implicit) + + # Enable session correlation inside a Coder workspace (inject target auto-derived from CODER_AGENT_URL) + boundary --enable-session-correlation \ + --allow "domain=dev.coder.com" -- python train.py + + # Enable session correlation with an explicit inject target (e.g. outside a workspace or custom deployment) + boundary --enable-session-correlation \ + --session-id-inject-target "domain=mydeployment.coder.com path=/api/v2/aibridge/*" \ + --allow "domain=mydeployment.coder.com" -- python train.py + + # Enable session correlation with multiple inject targets (e.g. staging + prod) + boundary --enable-session-correlation \ + --session-id-inject-target "domain=staging.coder.com path=/api/v2/aibridge/*" \ + --session-id-inject-target "domain=prod.coder.com path=/api/v2/aibridge/*" \ + --allow "domain=staging.coder.com" --allow "domain=prod.coder.com" -- python train.py` return cmd } @@ -169,6 +184,43 @@ func BaseCommand(version string) *serpent.Command { Value: &showVersion, YAML: "", // CLI only }, + // Session correlation header injection options. + { + Flag: "enable-session-correlation", + Env: "BOUNDARY_SESSION_CORRELATION_ENABLED", + Description: "Enable session correlation header injection. When no inject targets are configured, the target is auto-derived from CODER_AGENT_URL (set automatically inside Coder workspaces). Disable for deployments without Coder AI Gateway in front.", + Value: &cliConfig.SessionCorrelationEnabled, + YAML: "session_correlation_enabled", + }, + { + Flag: "session-id-inject-target", + Env: "BOUNDARY_SESSION_ID_INJECT_TARGET", + Description: `Inject target (repeatable via flag; env accepts one value). Requests matching these targets receive session correlation headers. For multiple targets use the YAML config. Format: "domain= [path=]".`, + Value: &cliConfig.InjectSessionIDTarget, + YAML: "", // CLI only, YAML uses session_id_inject_targets. + }, + { + Flag: "", // No CLI flag, YAML only. + Description: "Inject targets from config file (YAML only).", + Value: &cliConfig.InjectSessionIDTargets, + YAML: "session_id_inject_targets", + }, + { + Flag: "session-id-header-name", + Env: "BOUNDARY_SESSION_ID_HEADER_NAME", + Description: "HTTP header name for the boundary session ID.", + Default: config.DefaultSessionIDHeaderName, + Value: &cliConfig.SessionIDHeaderName, + YAML: "session_id_header_name", + }, + { + Flag: "sequence-number-header-name", + Env: "BOUNDARY_SEQUENCE_NUMBER_HEADER_NAME", + Description: "HTTP header name for the boundary sequence number.", + Default: config.DefaultSequenceNumberHeaderName, + Value: &cliConfig.SequenceNumberHeaderName, + YAML: "sequence_number_header_name", + }, }, Handler: func(inv *serpent.Invocation) error { // Handle --version flag early diff --git a/config/config.go b/config/config.go index 73cdb38..9eeee66 100644 --- a/config/config.go +++ b/config/config.go @@ -2,6 +2,7 @@ package config import ( "fmt" + "os" "strings" "github.com/coder/serpent" @@ -70,6 +71,13 @@ type CliConfig struct { NoUserNamespace serpent.Bool `yaml:"no_user_namespace"` DisableAuditLogs serpent.Bool `yaml:"disable_audit_logs"` LogProxySocketPath serpent.String `yaml:"log_proxy_socket_path"` + + // Session correlation header injection. + SessionCorrelationEnabled serpent.Bool `yaml:"session_correlation_enabled"` + InjectSessionIDTarget AllowStringsArray `yaml:"-"` // From CLI flags only + InjectSessionIDTargets serpent.StringArray `yaml:"session_id_inject_targets"` // From config file + SessionIDHeaderName serpent.String `yaml:"session_id_header_name"` + SequenceNumberHeaderName serpent.String `yaml:"sequence_number_header_name"` } type AppConfig struct { @@ -87,6 +95,10 @@ type AppConfig struct { DisableAuditLogs bool LogProxySocketPath string + // SessionCorrelation controls header injection for AI Bridge + // correlation. See SessionCorrelationConfig for details. + SessionCorrelation SessionCorrelationConfig + // SessionID is a UUIDv4 generated at process startup. It groups // all audit events produced by this boundary invocation into a // single session. Set by Run, not by configuration. @@ -108,6 +120,12 @@ func NewAppConfigFromCliConfig(cfg CliConfig, targetCMD []string) (AppConfig, er userInfo := GetUserInfo() + // Build session correlation config from CLI and YAML sources. + sc, err := buildSessionCorrelation(cfg, os.Environ()) + if err != nil { + return AppConfig{}, fmt.Errorf("session correlation config: %w", err) + } + return AppConfig{ AllowRules: allAllowStrings, LogLevel: cfg.LogLevel.Value(), @@ -122,5 +140,54 @@ func NewAppConfigFromCliConfig(cfg CliConfig, targetCMD []string) (AppConfig, er UserInfo: userInfo, DisableAuditLogs: cfg.DisableAuditLogs.Value(), LogProxySocketPath: cfg.LogProxySocketPath.Value(), + SessionCorrelation: sc, }, nil } + +// buildSessionCorrelation merges CLI and YAML inject target sources, +// parses each target string, applies header name defaults, and +// validates the resulting configuration. environ is passed explicitly +// (rather than reading os.Environ inside) so that callers and tests +// can supply a controlled environment. +func buildSessionCorrelation(cfg CliConfig, environ []string) (SessionCorrelationConfig, error) { + // Merge YAML targets with CLI targets. + rawTargets := append(cfg.InjectSessionIDTargets.Value(), cfg.InjectSessionIDTarget.Value()...) + + var targets []InjectTarget + for _, raw := range rawTargets { + t, err := ParseInjectTarget(raw) + if err != nil { + return SessionCorrelationConfig{}, err + } + targets = append(targets, t) + } + + if len(targets) == 0 && cfg.SessionCorrelationEnabled.Value() { + if t := DefaultInjectTargetFromEnv(environ); t != nil { + targets = []InjectTarget{*t} + } + } + + // Apply defaults for header names. + sessionIDHeader := cfg.SessionIDHeaderName.Value() + if sessionIDHeader == "" { + sessionIDHeader = DefaultSessionIDHeaderName + } + seqHeader := cfg.SequenceNumberHeaderName.Value() + if seqHeader == "" { + seqHeader = DefaultSequenceNumberHeaderName + } + + sc := SessionCorrelationConfig{ + Enabled: cfg.SessionCorrelationEnabled.Value(), + InjectTargets: targets, + SessionIDHeaderName: sessionIDHeader, + SequenceNumberHeaderName: seqHeader, + } + + if err := ValidateSessionCorrelation(sc); err != nil { + return SessionCorrelationConfig{}, err + } + + return sc, nil +} diff --git a/config/session_correlation.go b/config/session_correlation.go new file mode 100644 index 0000000..a5589ea --- /dev/null +++ b/config/session_correlation.go @@ -0,0 +1,147 @@ +package config + +import ( + "fmt" + "net/url" + "strings" +) + +// Default header names and paths for session correlation. +const ( + DefaultSessionIDHeaderName = "X-Coder-Agent-Firewall-Session-Id" + DefaultSequenceNumberHeaderName = "X-Coder-Agent-Firewall-Sequence-Number" + + // DefaultAIBridgePath is the path 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 + // agent that points to the control plane. Boundary uses it to derive a + // default inject target when none is explicitly configured. + CoderAgentURLEnv = "CODER_AGENT_URL" +) + +// InjectTarget represents a parsed target for session correlation header +// injection. Requests matching the domain (and optional path glob) will +// receive the session ID and sequence number headers. +type InjectTarget struct { + Domain string + Path string +} + +// SessionCorrelationConfig holds configuration for session correlation +// header injection. When enabled, boundary injects its session ID and +// sequence number as custom headers on matching outbound requests so +// that an upstream AI Bridge can correlate the request back to the +// boundary audit event stream. +type SessionCorrelationConfig struct { + // Enabled controls whether session correlation headers are injected. + // Deployments without AI Bridge in front should set this to false. + Enabled bool + + // InjectTargets is the list of domain/path patterns that should + // receive session correlation headers. + InjectTargets []InjectTarget + + // SessionIDHeaderName is the HTTP header name used to carry the + // boundary session ID. Defaults to DefaultSessionIDHeaderName. + SessionIDHeaderName string + + // SequenceNumberHeaderName is the HTTP header name used to carry + // the boundary sequence number. Defaults to + // DefaultSequenceNumberHeaderName. + SequenceNumberHeaderName string +} + +// ParseInjectTarget parses a string of the form "domain=... path=..." +// into an InjectTarget. The domain key is required; path is optional. +func ParseInjectTarget(raw string) (InjectTarget, error) { + raw = strings.TrimSpace(raw) + if raw == "" { + return InjectTarget{}, fmt.Errorf("inject target must not be empty") + } + + var target InjectTarget + for _, part := range strings.Fields(raw) { + key, value, ok := strings.Cut(part, "=") + if !ok { + return InjectTarget{}, fmt.Errorf( + "inject target: malformed key-value pair %q, expected key=value", part, + ) + } + switch key { + case "domain": + if value == "" { + return InjectTarget{}, fmt.Errorf("inject target: domain must not be empty") + } + target.Domain = value + case "path": + target.Path = value + default: + return InjectTarget{}, fmt.Errorf("inject target: unknown key %q", key) + } + } + + if target.Domain == "" { + return InjectTarget{}, fmt.Errorf("inject target: domain is required") + } + + return target, nil +} + +// DefaultInjectTargetFromEnv derives an InjectTarget 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. +// +// 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) *InjectTarget { + var raw string + for _, e := range environ { + k, v, ok := strings.Cut(e, "=") + if ok && k == CoderAgentURLEnv { + raw = v + break + } + } + if raw == "" { + return nil + } + + u, err := url.Parse(raw) + if err != nil || u.Host == "" { + return nil + } + + return &InjectTarget{ + Domain: u.Hostname(), + Path: DefaultAIBridgePath, + } +} + +// ValidateSessionCorrelation checks that the session correlation config +// is internally consistent. It returns an error describing the first +// problem found, or nil if the config is valid. +func ValidateSessionCorrelation(cfg SessionCorrelationConfig) error { + if !cfg.Enabled { + return nil + } + + if len(cfg.InjectTargets) == 0 { + return fmt.Errorf( + "session correlation is enabled but no inject targets are configured", + ) + } + + if cfg.SessionIDHeaderName == "" { + return fmt.Errorf("session-id-header-name must not be empty when session correlation is enabled") + } + + if cfg.SequenceNumberHeaderName == "" { + return fmt.Errorf("sequence-number-header-name must not be empty when session correlation is enabled") + } + + return nil +} diff --git a/config/session_correlation_test.go b/config/session_correlation_test.go new file mode 100644 index 0000000..9a593ad --- /dev/null +++ b/config/session_correlation_test.go @@ -0,0 +1,465 @@ +package config + +import ( + "testing" +) + +func TestParseInjectTarget(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input string + want InjectTarget + wantErr bool + }{ + { + name: "domain only", + input: "domain=dev.coder.com", + want: InjectTarget{Domain: "dev.coder.com"}, + }, + { + name: "domain and path", + input: "domain=dev.coder.com path=/api/v2/aibridge/*", + want: InjectTarget{Domain: "dev.coder.com", Path: "/api/v2/aibridge/*"}, + }, + { + name: "leading and trailing whitespace", + input: " domain=dev.coder.com path=/api/* ", + want: InjectTarget{Domain: "dev.coder.com", Path: "/api/*"}, + }, + { + name: "empty string", + input: "", + wantErr: true, + }, + { + name: "whitespace only", + input: " ", + wantErr: true, + }, + { + name: "missing domain", + input: "path=/api/*", + wantErr: true, + }, + { + name: "empty domain value", + input: "domain=", + wantErr: true, + }, + { + name: "malformed pair no equals", + input: "domain", + wantErr: true, + }, + { + name: "unknown key", + input: "domain=example.com port=443", + wantErr: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + got, err := ParseInjectTarget(tc.input) + if tc.wantErr { + if err == nil { + t.Fatalf("expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got.Domain != tc.want.Domain { + t.Errorf("Domain: got %q, want %q", got.Domain, tc.want.Domain) + } + if got.Path != tc.want.Path { + t.Errorf("Path: got %q, want %q", got.Path, tc.want.Path) + } + }) + } +} + +func TestValidateSessionCorrelation(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + cfg SessionCorrelationConfig + wantErr bool + }{ + { + name: "disabled is always valid", + cfg: SessionCorrelationConfig{ + Enabled: false, + }, + }, + { + name: "disabled with empty targets is valid", + cfg: SessionCorrelationConfig{ + Enabled: false, + InjectTargets: nil, + }, + }, + { + name: "enabled with targets and default headers", + cfg: SessionCorrelationConfig{ + Enabled: true, + InjectTargets: []InjectTarget{{Domain: "dev.coder.com"}}, + SessionIDHeaderName: DefaultSessionIDHeaderName, + SequenceNumberHeaderName: DefaultSequenceNumberHeaderName, + }, + }, + { + name: "enabled with custom headers", + cfg: SessionCorrelationConfig{ + Enabled: true, + InjectTargets: []InjectTarget{{Domain: "example.com", Path: "/api/*"}}, + SessionIDHeaderName: "X-Custom-Session", + SequenceNumberHeaderName: "X-Custom-Seq", + }, + }, + { + name: "enabled with no targets", + cfg: SessionCorrelationConfig{ + Enabled: true, + InjectTargets: nil, + SessionIDHeaderName: DefaultSessionIDHeaderName, + SequenceNumberHeaderName: DefaultSequenceNumberHeaderName, + }, + wantErr: true, + }, + { + name: "enabled with empty targets slice", + cfg: SessionCorrelationConfig{ + Enabled: true, + InjectTargets: []InjectTarget{}, + SessionIDHeaderName: DefaultSessionIDHeaderName, + SequenceNumberHeaderName: DefaultSequenceNumberHeaderName, + }, + wantErr: true, + }, + { + name: "enabled with empty session id header", + cfg: SessionCorrelationConfig{ + Enabled: true, + InjectTargets: []InjectTarget{{Domain: "example.com"}}, + SessionIDHeaderName: "", + SequenceNumberHeaderName: DefaultSequenceNumberHeaderName, + }, + wantErr: true, + }, + { + name: "enabled with empty sequence number header", + cfg: SessionCorrelationConfig{ + Enabled: true, + InjectTargets: []InjectTarget{{Domain: "example.com"}}, + SessionIDHeaderName: DefaultSessionIDHeaderName, + SequenceNumberHeaderName: "", + }, + wantErr: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + err := ValidateSessionCorrelation(tc.cfg) + if tc.wantErr { + if err == nil { + t.Fatalf("expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + }) + } +} + +func TestNewAppConfigFromCliConfig_SessionCorrelation(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + cli CliConfig + want SessionCorrelationConfig + wantErr bool + }{ + { + name: "defaults when not configured", + cli: baseCliConfig(), + want: SessionCorrelationConfig{ + Enabled: false, + InjectTargets: nil, + SessionIDHeaderName: DefaultSessionIDHeaderName, + SequenceNumberHeaderName: DefaultSequenceNumberHeaderName, + }, + }, + { + name: "enabled with inject targets", + cli: func() CliConfig { + c := baseCliConfig() + _ = c.SessionCorrelationEnabled.Set("true") + _ = c.InjectSessionIDTarget.Set("domain=dev.coder.com path=/api/v2/aibridge/*") + return c + }(), + want: SessionCorrelationConfig{ + Enabled: true, + InjectTargets: []InjectTarget{ + {Domain: "dev.coder.com", Path: "/api/v2/aibridge/*"}, + }, + SessionIDHeaderName: DefaultSessionIDHeaderName, + SequenceNumberHeaderName: DefaultSequenceNumberHeaderName, + }, + }, + { + name: "custom header names", + cli: func() CliConfig { + c := baseCliConfig() + _ = c.SessionCorrelationEnabled.Set("true") + _ = c.InjectSessionIDTarget.Set("domain=example.com") + _ = c.SessionIDHeaderName.Set("X-My-Session") + _ = c.SequenceNumberHeaderName.Set("X-My-Seq") + return c + }(), + want: SessionCorrelationConfig{ + Enabled: true, + InjectTargets: []InjectTarget{{Domain: "example.com"}}, + SessionIDHeaderName: "X-My-Session", + SequenceNumberHeaderName: "X-My-Seq", + }, + }, + // Note: "enabled with no targets" is tested in + // TestBuildSessionCorrelation_AgentURLFallback with a controlled + // environ so that CODER_AGENT_URL in the test runner's environment + // cannot interfere. + { + name: "invalid inject target", + cli: func() CliConfig { + c := baseCliConfig() + _ = c.SessionCorrelationEnabled.Set("true") + _ = c.InjectSessionIDTarget.Set("notakey") + return c + }(), + wantErr: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + got, err := NewAppConfigFromCliConfig(tc.cli, []string{"echo", "hello"}) + if tc.wantErr { + if err == nil { + t.Fatalf("expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + sc := got.SessionCorrelation + if sc.Enabled != tc.want.Enabled { + t.Errorf("Enabled: got %v, want %v", sc.Enabled, tc.want.Enabled) + } + if sc.SessionIDHeaderName != tc.want.SessionIDHeaderName { + t.Errorf("SessionIDHeaderName: got %q, want %q", + sc.SessionIDHeaderName, tc.want.SessionIDHeaderName) + } + if sc.SequenceNumberHeaderName != tc.want.SequenceNumberHeaderName { + t.Errorf("SequenceNumberHeaderName: got %q, want %q", + sc.SequenceNumberHeaderName, tc.want.SequenceNumberHeaderName) + } + if len(sc.InjectTargets) != len(tc.want.InjectTargets) { + t.Fatalf("InjectTargets len: got %d, want %d", + len(sc.InjectTargets), len(tc.want.InjectTargets)) + } + for i := range sc.InjectTargets { + if sc.InjectTargets[i].Domain != tc.want.InjectTargets[i].Domain { + t.Errorf("InjectTargets[%d].Domain: got %q, want %q", + i, sc.InjectTargets[i].Domain, tc.want.InjectTargets[i].Domain) + } + if sc.InjectTargets[i].Path != tc.want.InjectTargets[i].Path { + t.Errorf("InjectTargets[%d].Path: got %q, want %q", + i, sc.InjectTargets[i].Path, tc.want.InjectTargets[i].Path) + } + } + }) + } +} + +func TestDefaultInjectTargetFromEnv(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + environ []string + want *InjectTarget + }{ + { + name: "valid URL with trailing slash", + environ: []string{"CODER_AGENT_URL=https://dev.coder.com/"}, + want: &InjectTarget{Domain: "dev.coder.com", Path: DefaultAIBridgePath}, + }, + { + name: "valid URL without trailing slash", + environ: []string{"CODER_AGENT_URL=https://dev.coder.com"}, + want: &InjectTarget{Domain: "dev.coder.com", Path: DefaultAIBridgePath}, + }, + { + name: "URL with port", // Ports are ignored in the rules engine, so we strip them here. + environ: []string{"CODER_AGENT_URL=https://dev.coder.com:8443/"}, + want: &InjectTarget{Domain: "dev.coder.com", Path: DefaultAIBridgePath}, + }, + { + name: "unset variable", + environ: []string{}, + want: nil, + }, + { + name: "empty value", + environ: []string{"CODER_AGENT_URL="}, + want: nil, + }, + { + name: "no host in URL", + environ: []string{"CODER_AGENT_URL=not-a-url"}, + want: nil, + }, + { + name: "other env vars present but not CODER_AGENT_URL", + environ: []string{"CODER_URL=https://dev.coder.com/", "HOME=/home/user"}, + want: nil, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + got := DefaultInjectTargetFromEnv(tc.environ) + if tc.want == nil { + if got != nil { + t.Errorf("expected nil, got %+v", got) + } + return + } + if got == nil { + t.Fatalf("expected %+v, got nil", tc.want) + } + if got.Domain != tc.want.Domain { + t.Errorf("Domain: got %q, want %q", got.Domain, tc.want.Domain) + } + if got.Path != tc.want.Path { + t.Errorf("Path: got %q, want %q", got.Path, tc.want.Path) + } + }) + } +} + +func TestBuildSessionCorrelation_AgentURLFallback(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + cfg func() CliConfig + environ []string + wantTargets []InjectTarget + wantErr bool + }{ + { + name: "enabled, no explicit targets, CODER_AGENT_URL set → auto-derived", + cfg: func() CliConfig { + c := baseCliConfig() + _ = c.SessionCorrelationEnabled.Set("true") + return c + }, + environ: []string{"CODER_AGENT_URL=https://dev.coder.com/"}, + wantTargets: []InjectTarget{ + {Domain: "dev.coder.com", Path: DefaultAIBridgePath}, + }, + }, + { + name: "enabled, no explicit targets, CODER_AGENT_URL absent → error", + cfg: func() CliConfig { + c := baseCliConfig() + _ = c.SessionCorrelationEnabled.Set("true") + return c + }, + environ: []string{}, + wantErr: true, + }, + { + name: "enabled, explicit target wins over CODER_AGENT_URL", + cfg: func() CliConfig { + c := baseCliConfig() + _ = c.SessionCorrelationEnabled.Set("true") + _ = c.InjectSessionIDTarget.Set("domain=custom.example.com") + return c + }, + environ: []string{"CODER_AGENT_URL=https://dev.coder.com/"}, + wantTargets: []InjectTarget{ + {Domain: "custom.example.com", Path: ""}, + }, + }, + { + name: "disabled, CODER_AGENT_URL absent → valid (no targets needed)", + cfg: func() CliConfig { + return baseCliConfig() + }, + environ: []string{}, + wantTargets: nil, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + sc, err := buildSessionCorrelation(tc.cfg(), tc.environ) + if tc.wantErr { + if err == nil { + t.Fatalf("expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(sc.InjectTargets) != len(tc.wantTargets) { + t.Fatalf("InjectTargets len: got %d, want %d", + len(sc.InjectTargets), len(tc.wantTargets)) + } + for i := range sc.InjectTargets { + if sc.InjectTargets[i].Domain != tc.wantTargets[i].Domain { + t.Errorf("InjectTargets[%d].Domain: got %q, want %q", + i, sc.InjectTargets[i].Domain, tc.wantTargets[i].Domain) + } + if sc.InjectTargets[i].Path != tc.wantTargets[i].Path { + t.Errorf("InjectTargets[%d].Path: got %q, want %q", + i, sc.InjectTargets[i].Path, tc.wantTargets[i].Path) + } + } + }) + } +} + +// baseCliConfig returns a CliConfig with valid defaults for fields that +// NewAppConfigFromCliConfig requires, so tests can focus on the session +// correlation fields without tripping over unrelated validation. +func baseCliConfig() CliConfig { + c := CliConfig{} + _ = c.JailType.Set("nsjail") + _ = c.SessionIDHeaderName.Set(DefaultSessionIDHeaderName) + _ = c.SequenceNumberHeaderName.Set(DefaultSequenceNumberHeaderName) + return c +} diff --git a/go.mod b/go.mod index a5e4732..7b1bfde 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,8 @@ go 1.25.9 require ( github.com/cenkalti/backoff/v5 v5.0.3 - github.com/coder/coder/v2 v2.33.0-rc.3.0.20260501075247-b3e1178358f5 - github.com/coder/serpent v0.14.0 + github.com/coder/coder/v2 v2.34.0-rc.0.0.20260505083626-1ba7139f2154 + github.com/coder/serpent v0.15.0 github.com/google/uuid v1.6.0 github.com/landlock-lsm/go-landlock v0.0.0-20251103212306-430f8e5cd97c github.com/miekg/dns v1.1.72 diff --git a/go.sum b/go.sum index 89d4da8..900f8a7 100644 --- a/go.sum +++ b/go.sum @@ -34,10 +34,14 @@ github.com/clipperhouse/uax29/v2 v2.6.0 h1:z0cDbUV+aPASdFb2/ndFnS9ts/WNXgTNNGFoK github.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= github.com/coder/coder/v2 v2.33.0-rc.3.0.20260501075247-b3e1178358f5 h1:7V2ZTceP3V8EqYA8kC+3xrRwaZ8SOtgw4b8cElm0rcA= github.com/coder/coder/v2 v2.33.0-rc.3.0.20260501075247-b3e1178358f5/go.mod h1:w3FcuW2hJS/QnmY7ilsvt0yVpUhlVYdnBozwTkmpkKE= +github.com/coder/coder/v2 v2.34.0-rc.0.0.20260505083626-1ba7139f2154 h1:f3/+Fbp1/SPq3g8sI/oTjYEc53Zs54Cbd5byw0TEkEE= +github.com/coder/coder/v2 v2.34.0-rc.0.0.20260505083626-1ba7139f2154/go.mod h1:W1EYsVyhiAfMsZSguJK6/g5uJL3UbDPva4zvND7kHwQ= github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 h1:3A0ES21Ke+FxEM8CXx9n47SZOKOpgSE1bbJzlE4qPVs= github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0/go.mod h1:5UuS2Ts+nTToAMeOjNlnHFkPahrtDkmpydBen/3wgZc= github.com/coder/serpent v0.14.0 h1:g7vt2zBMp3nWyAvyhvQduaI53Ku65U3wITMi01+/8pU= github.com/coder/serpent v0.14.0/go.mod h1:7OIvFBYMd+OqarMy5einBl8AtRr8LliopVU7pyrwucY= +github.com/coder/serpent v0.15.0 h1:jobR7DnPsxzEMD0cRiailwlY+4v6HAPS/8emIgBpaIU= +github.com/coder/serpent v0.15.0/go.mod h1:7OIvFBYMd+OqarMy5einBl8AtRr8LliopVU7pyrwucY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=